MVP polish: W2TG default, tier order, app branding - #163
Conversation
Extract get_dataset_data_path(meta) into voxkit.storage.datasets and use it from viewer_stacker, training_stacker, and pllr_stacker. The "cached -> root/cache, else original_path" rule now lives in one place next to _get_dataset_root. Side-effect fixes: - training_stacker previously passed the dataset root (not root/cache) as audio_path for cached datasets, inconsistent with the viewer and pllr paths. It now resolves to root/cache like the others. - pllr_stacker dropped the brittle string "True"/bool dual-check on meta["cached"], matching the typed DatasetMetadata schema.
* Collapse code quality workflow to one job/VM * Rename job to specify sub component of code quality * Install uv with setup naturally instead * Remove python installation since uv handles python installation * Remove soft code quality assurance in ubuntu test workflow * Enable caching for uv across jobs * Switchng to develop instead of release * Add testing jobs in working branch develop
* Make config/VERSION the single source of truth for app version Previously the version was duplicated across pyproject.toml, the voxkit package __init__, three app_info.yaml files, and the Windows installer script, and had drifted (0.1.0 / 0.4.0 / 0.4.1). All consumers now read from config/VERSION (canonicalized to 0.4.1). * Adapt testing for new version convention * Update AGENTS.md
* Move possible audio extensions to shared constants * Move possible tools of an engine to shared constant * Move audio types to storage instead * Move help url to shared constants instead * Remove deprecated function get_profile_config * Reformat badges in readme ocd * Copilot fix Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Match constant to duplicate definition per feedback * Copilot feedback Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Lint code [skip ci] --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
MFA shells out to conda, which previously had to be on PATH or in one of a few hardcoded Windows install locations. Users with a non-standard Anaconda/Miniconda install (common on Windows) had no way to point VoxKit at their conda.exe. - Add a "Conda Path" field to the MFA align and train settings dialogs. - _find_conda() now accepts an explicit path and also honors a VOXKIT_CONDA_PATH env var, both taking precedence over auto-detection. - Thread the configured path through the MFA service entry points (run_mfa_align, run_mfa_adapt, ensure_dictionary_downloaded, _ensure_mfa_server_running). - Blank/whitespace settings normalize to None, preserving auto-detection. - Add unit tests for the resolution precedence.
* fix: add working browse button to Register New Model dialog
The Model Path field's "Browse for model directory..." placeholder was
misleading -- no QFileDialog was ever wired to it, since _create_lineedit()
just built a bare QLineEdit. Adds a FieldType.DIRPATH field type with a real
"Browse..." button (QFileDialog.getExistingDirectory) in the settings_modal
framework, applied to the Register New Model dialog. Also updates the
placeholder text and increases the dialog height so the new button row
doesn't clip the row above it.
* feat: add progress bar to processing status on pipeline pages
Adds an indeterminate QProgressBar (Containers.PROGRESS_BAR style) that
shows/hides alongside the existing status label. BaseStacker.set_status()
shows it automatically for any subclass -- covers Predict Alignments (the
MFA/align page) with no per-page changes needed there.
* feat: add progress bar and run-metadata CSV enrichment to PLLR extraction
Two related changes to the PLLR extraction page, committed together because
they touch overlapping lines (e.g. extract_pllr_logic's signature change for
metadata is adjacent to the progress-bar visibility call):
- Adds the same indeterminate progress bar as base_stacker.py's pages,
since PLLRStacker doesn't inherit BaseStacker and manages its own status
label separately.
- Appends run_datetime/corpus_name/corpus_id/engine_id/model_name/model_id
columns to every row of the phonewise/framewise output CSVs, and renames
each output file to embed corpus/engine/timestamp, so multiple runs'
outputs can coexist in the same folder instead of overwriting each other.
Handles compute_pllr() writing multiple phonewise_proba_{method}.csv
variants (a dict-returning aggregation function) rather than assuming a
single fixed filename.
* chore: rename default W2TG model display name
"prads_model" -> "default". Only affects fresh installs since
startup_routine() is gated behind is_first_launch().
* feat: show corpus/alignment filesystem locations and alignment type on Dataset Management
Nina asked for the dataset's original corpus path and each alignment's
TextGrid output path to be visible on the Dataset Management page, since
neither was surfaced anywhere in the GUI before this.
- Datasets table gains a "Location" column (dataset's original_path).
- Alignments table gains "Location" (tg_path) and "Type" columns. Type
distinguishes corrected alignments from the automatic ones they were
derived from -- without it they're visually identical (same engine,
same model). Backed by a new get_alignment_type() in
storage/alignments.py, which infers "hand"/"automatic" for older
alignments that predate the alignment_type field.
- Both Location columns elide from the left (not the default right), so
long paths show their distinguishing tail (e.g. ".../alignments/<id>/
textgrids") instead of the shared "C:\Users\..." prefix; full path
remains available via tooltip.
* fix: use directory-picker fields for dataset and hand-alignment paths
Replace the plain line-edit path inputs on the Register New Dataset dialog
with DIRPATH fields so users get a browse button, and drop the now-unused
browse_dataset_path helper. Bump the dialog height to fit the added control.
* fix: narrow DIRPATH widget to QLineEdit before wiring browse button
The browse-button handler expects a QLineEdit, but _wrap_with_browse_button
only had the widget typed as QWidget, tripping mypy. Guard with isinstance so
the type narrows and non-line-edit widgets fall through unchanged.
---------
Co-authored-by: Beckett Frey <beckett.frey@gmail.com>
* feat: bake MFA setup into the app via a bundled micromamba environment
VoxKit previously required a user to manually install conda and run
`conda create -n aligner -c conda-forge montreal-forced-aligner` in a
terminal -- a recipe documented nowhere in the repo except inside a
FileNotFoundError message. Most users (SLP researchers, not engineers)
should never have to do this.
Ships a vendored static micromamba binary (vendor/micromamba/) plus a
pinned, explicit lockfile (config/mfa-env/aligner-win-64.lock) for an
environment equivalent to the manual recipe above. On first launch,
config/startup_config.py provisions VoxKit's own managed "aligner"
environment at ~/.voxkit/mfa-env via `micromamba create --file <lockfile>`
-- a fast, solve-free, resumable install of pinned package URLs, fully
invisible to the user beyond the existing first-run loading dialog.
services/mfa.py's four subprocess call sites (run_mfa_align, run_mfa_adapt,
ensure_dictionary_downloaded, _ensure_mfa_server_running) now share a new
_mfa_invocation() helper that prefers this bundled environment when ready,
falling back to exactly the previous conda_path/_find_conda + "conda run -n
aligner" mechanism otherwise -- zero behavior change for anyone with their
own pre-existing conda + aligner setup.
Real-world testing while building this (documented in docs/BUILD.md)
surfaced three non-obvious issues the implementation works around:
- The `mfa`/`mfa.exe` entry-point stub can fail to launch on Windows even
inside a correctly-activated environment; invoking `python
<env>/Scripts/mfa-script.py` directly works reliably where the stub does
not.
- MFA's global config directory (~/Documents/MFA by default) must be
isolated per-environment via MFA_ROOT_DIR, or a version mismatch between
a pre-existing user setup and the bundled environment can make
global_config.yaml unreadable.
- PostgreSQL's Unix-domain-socket path has a hard 107-byte limit, which a
deeply nested MFA_ROOT_DIR can exceed and silently break `mfa server
init` -- ~/.voxkit/mfa-root is short enough to be safe.
Also adds a "Repair/Reinstall MFA Environment" button to Generate
Alignments as a manual retry path, since first-run provisioning failures
aren't automatically retried on next launch (to avoid blocking W2TG-only
usage or the rest of first-run setup).
Reviewed with Fable (a second model) before implementation: considered
true bundling via conda-constructor/conda-pack (rejected as a
disproportionate ongoing burden -- multi-GB installer, per-OS release CI
that doesn't exist today, Kaldi/Postgres relocation risk, no known prior
art) versus this micromamba + pinned-lockfile approach, which delivers
most of the same "never touch a terminal" UX at a fraction of the
build/release complexity.
* fix: silence mypy attr-defined error on sys._MEIPASS
mfa_provision.py isn't in pyproject.toml's mypy exclude list (unlike
main.py, which uses the identical frozen-bundle pattern but is
excluded), so mypy correctly flags sys._MEIPASS as an unknown
attribute -- it's only set by PyInstaller at runtime, not part of the
stdlib sys module stubs. Add a targeted type: ignore rather than
excluding the whole file, since the rest of the module should stay
type-checked.
* fix: pin kaldi to a CPU build in the bundled MFA environment lockfile
The lockfile pinned kaldi-5.5.1172-cuda129hf00e934_5, a CUDA build whose
kaldi-cudamatrix.dll links against nvcuda.dll. That DLL ships with the
NVIDIA display driver and never in a conda package, so on any machine
without an NVIDIA GPU the provisioned environment failed to import:
ImportError: DLL load failed while importing _kalpy:
The specified module could not be found.
This broke every mfa invocation, surfacing to users as a failed
english_us_arpa dictionary download.
conda-forge ships both CPU and CUDA variants of kaldi at the same
version, and the solver picks between them via the __cuda virtual
package -- i.e. based on whether the machine that generated the lockfile
had an NVIDIA driver. The shipped installer was therefore broken for
every non-NVIDIA user and worked on NVIDIA machines only by coincidence.
Pin kaldi=*=cpu*, which resolves to kaldi-5.5.1172-cpu_hf03c2bf_5 --
identical version and build number, so no functional change beyond
dropping CUDA. The CUDA variant bought us nothing regardless: kaldi's
CUDA components accelerate nnet3 online decoding, while align/adapt are
GMM-based and CPU-only. MFA's alignment/ and acoustic_modeling/ packages
contain zero CUDA references; the only CUDA use in MFA is torch-based,
in diarization/transcription/vad, none of which VoxKit invokes.
Also drops 12 CUDA packages (libcublas, libcusolver, libmagma, ...)
from the installer.
Document the pin in BUILD.md, since the regeneration command there is
what introduced this, and add two validation steps: grep the lockfile
for CUDA packages, and import _kalpy explicitly. The latter matters
because `mfa version` still passes with a CUDA build on a GPU machine,
so the previous validation could not catch this on the machine that
generated the bad lockfile.
* ci: bump setup-uv cache-suffix to invalidate corrupted git cache
The uv git cache restored by setup-uv was missing objects, causing
`git clone` from the local cache db to fail with "empty repository" /
"failed to copy file ... No such file or directory" when building the
pypllrcomputer git dependency. Bump cache-suffix to v2 to force a fresh
cache across all workflows.
---------
Co-authored-by: beckett <bfrey6@wisc.edu>
Co-authored-by: Beckett Frey <beckett.frey@gmail.com>
…#153) * feat: add time-synced waveform and Praat-style spectrogram to View Alignments Adds a shared TimeAxisMixin (pixel<->time mapping + playhead drawing) used by TextGridTimeline, a new WaveformPanel, and a new SpectrogramPanel, so all three stay pixel-aligned to one playhead. Waveform envelope and spectrogram STFT are each computed once per file on a background WorkerThread and cached; paintEvent only blits cached data, never recomputes per frame. Spectrogram mimics Praat's default view (grayscale, 0-5000 Hz, 50 dB dynamic range, 5ms analysis window) and is opt-in via a "Show Spectrograms" toggle since it's slower to compute than the waveform. Adds librosa and matplotlib as explicit dependencies (previously only transitive). * feat: add zoom/scroll/selection/play-selection to the timeline viewer Adds Praat-like interaction to the waveform/spectrogram/TextGrid timeline: mouse-wheel zoom (cursor-anchored), shift/middle-drag pan, a scrollbar, zoom in/out buttons, click-drag time-range selection synced across all three panels, and a Play Selection control (Tab-shortcutable) that plays back only the selected span sample-accurately via a dedicated player and temp-clip file, since QMediaPlayer.pause() can't stop a longer stream exactly at a short phone boundary. Also folds in feedback from live GUI testing: fixed spectrogram frequency range (0-10kHz, no longer auto-scaling to the file's own Nyquist), tier names staying visible while scrolled, clicking a tier interval selecting the whole interval, and Praat-parity spectrogram DSP (Gaussian window, pre-emphasis, autoscaling, automatic time/frequency steps). * feat: add progress bar to processing status on pipeline pages Adds an indeterminate QProgressBar (Containers.PROGRESS_BAR style) that shows/hides alongside the existing status label. BaseStacker.set_status() shows it automatically for any subclass -- covers Predict Alignments (the MFA/align page) with no per-page changes needed there. * refactor: promote TextGrid helpers to public names parse_textgrid/find_textgrid/find_lab are needed by other pipeline pages (Compare Alignments' dual-tier inspector, and upcoming Correct Alignments) that want to resolve/parse TextGrid paths the same way ViewerStacker does, without reaching into underscored names across files. * feat: add create_corrected_alignment for boundary corrections Always creates and owns its own textgrids directory (local=True) regardless of the source dataset's cached flag, unlike create_hand_alignment's non-cached branch which can point tg_path at the original dataset directory -- that's exactly the overwrite risk a correction workflow must avoid. Baseline-copies the full source TextGrid set in immediately so untouched files stay byte-identical, and the source alignment is never modified. Adds source_alignment_id (NotRequired) to AlignmentMetadata for provenance. Switches the AlignmentMetadata "all required keys present" test assertions from __annotations__ to __required_keys__, since __annotations__ doesn't distinguish NotRequired fields. * feat: add Correct Alignments stacker with drag-to-edit boundaries New CorrectAlignmentsStacker page: dataset -> source alignment -> speaker -> file, then drag phone/word interval boundaries on the TextGrid timeline (EditableTextGridTimeline, a subclass so the shared read-only TextGridTimeline that ViewerStacker/ComparisonStacker depend on stays untouched). Phone/word boundaries that coincide move together in lockstep; drags are clamped so intervals can't invert or cross neighbors; v1 is boundaries-only with no undo/redo (reload to discard). Reuses WaveformPanel/SpectrogramPanel and the same sample-accurate Play Selection + Tab-shortcut mechanism as ViewerStacker/ComparisonStacker. TextGrid read/write goes through praatio (scoped to this feature only) rather than the regex parser, to avoid getting label-escaping/precision wrong. Saving always goes through create_corrected_alignment: first save of a session creates a new, fully-owned alignment; subsequent saves reuse it. The source alignment is never modified. Registered in STACKER_REGISTRY, the reload() dispatch, and all three pipeline_definitions.yaml profiles (base, default, explanatory) as a new step G. * feat: add dual-tier file inspector to Compare Alignments New "Inspect a File" section, visible once both Alignment A and B are selected, independent of the aggregate-metrics Compare button's lifecycle (that button rebuilds its own tab widget from scratch on every click, which would destroy an inspector living there). Reuses WaveformPanel/SpectrogramPanel/TextGridTimeline from viewer_stacker.py verbatim: one shared waveform/spectrogram/player above two labeled TextGridTimeline instances (Alignment A / Alignment B), with the same zoom/scroll/selection/sample-accurate-Play-Selection/Tab-shortcut wiring as ViewerStacker. A file with a TextGrid in only one alignment shows a placeholder note instead of aborting the whole inspector. Also adds a loading indicator for the aggregate comparison: computing all four phoneme comparisons ran synchronously on the UI thread, which froze the window before the "Running comparison..." status could even paint. Moved onto the existing WorkerThread pattern instead. * chore: move Correct Alignments before PLLR Extraction in nav order Swaps steps F/G so Correct Alignments (built on View Alignments' components) appears right after Compare Alignments, ahead of PLLR Extraction, across all three pipeline_definitions.yaml profiles. * refactor: button-only zoom, and a drag preview line for boundary edits Mouse-wheel/touchpad zoom was too easy to trigger by accident while just trying to scroll the page, so TimeAxisMixin no longer overrides wheelEvent at all -- the event now propagates to the enclosing QScrollArea for normal scrolling instead. Zooming is now +/- button only; relabeled those buttons "Zoom In"/"Zoom Out" (was a bare +/- with a tooltip) across all three timeline-based stackers. Also adds a dashed reference line to TimeAxisMixin (set_preview_time/ _draw_preview_line, alongside the existing playhead/selection drawing), shown in the waveform and spectrogram while dragging a TextGrid boundary in Correct Alignments -- lets a boundary be lined up precisely against spectral/amplitude detail while dragging. Kept visually distinct (dashed, purple) from the solid red playhead so it doesn't read as a playback-position change. * feat: name corrected alignments, show their storage path, add a Type column create_corrected_alignment now preserves the source alignment's real engine_id/model_metadata instead of replacing them with a "corrected" placeholder, and accepts an optional custom_name (shown in the Model column) so a correction session can be recognized and picked back up later -- including from a different install of the app against the same dataset storage. Provenance now lives in a new alignment_type field ("automatic"/"hand"/"corrected") instead of overloading engine_id; get_alignment_type() infers it for alignments created before this field existed. Every alignment dropdown across the app (View/Compare/Correct Alignments, PLLR extraction, Train Aligners) gains a "Type" column using get_alignment_type(), so corrected/hand alignments are clearly distinguishable from automatic ones without losing which real engine produced them. Correct Alignments gains an optional "Corrected Alignment Name" field (locked once the first save creates the alignment) and a persistent label showing exactly where the corrected alignment is stored on disk. * refactor: editable Engine/Type fields instead of a single name field Replaces Correct Alignments' single "Corrected Alignment Name" input with a "Save corrected alignment to dataset" section containing two editable fields, Engine and Type, prepopulated with the source alignment's real engine_id and "corrected" respectively. Both lock once the first save creates the alignment, showing the values that were actually used. create_corrected_alignment's custom_name param is replaced by explicit engine_id/alignment_type overrides (still defaulting to the source's engine_id and "corrected" if not given), so a user can directly control what shows in the Engine/Type dropdown columns rather than blending that into the Model name. * fix: PLLR extraction failing on alignments with local=True pllr_stacker.py assumed any "local" alignment's TextGrids live in a tg_path/cache subfolder, conflating a dataset's own cached-audio directory (get_dataset_data_path's /cache) with an alignment's tg_path, which no alignment-creation path (create_alignment, create_hand_alignment, create_corrected_alignment) has ever nested under a "cache" folder. This silently worked before only because no alignment type previously had local=True without also being on a non-cached dataset; corrected alignments are always local=True regardless of the dataset's own cached flag, which is what surfaced the bug ("Alignment output path does not exist: .../textgrids/cache"). * perf: background the slow part of Save Corrections Creating a corrected alignment (first save of a session) baseline- copies the entire source TextGrid tree via shutil.copytree, which was running synchronously on the UI thread and could take a while for a large dataset. Moved onto the existing WorkerThread pattern; the Save button disables and BaseStacker's progress bar shows automatically (via set_status(..., "working")) while it runs, then the current file's correction is written once the corrected alignment is ready. Subsequent saves in the same session are already a single fast file write and stay synchronous. - viewer_stacker.py: TimeAxisMixin is a plain-object mixin at runtime (by design -- concrete subclasses provide the QWidget base and the actual pyqtSignals), so mypy couldn't see update()/width()/the signals it calls. Given it a TYPE_CHECKING-only QWidget base and signal type declarations, changing nothing at runtime. Also: annotated _samples/ _sr/_freqs/_times/_Sxx_db (previously inferred as bare None from their first assignment), coerced librosa's sr to int to match the existing int-typed _sr, widened _regenerate_pixmap's readiness guard to cover _times/_freqs (only _Sxx_db was checked, though all three are always set together), and asserted _pixmap is non-None in _live_source_rect (only ever called from a call site that already checked it). - correct_alignments_stacker.py / comparison_stacker.py: several Optional-typed attributes (_pending_create_result, _corrected_alignment_meta, _current_speaker/_stem, _a/_b_alignment_meta, _pending_comparison_data) are validated non-None by an early-return guard, but mypy can't carry that narrowing across a subsequent method call -- added asserts at the top of each downstream method documenting exactly which caller already guarantees it. No behavior change: every fix is either a type annotation, a TYPE_CHECKING-only construct, or an assert on a condition already guaranteed true by the existing call sites. * style: apply ruff format to the mypy-fix commit's files CI's "Check code formatting" step (ruff format --check) failed on these three files -- pre-existing line-wrap drift unrelated to the mypy fixes themselves, just never run through ruff format before. No logic change. * fix: annotate AlignmentStatus/AlignmentType as explicit TypeAlias mypy intermittently failed in CI with "Cannot assign multiple types to name AlignmentType without an explicit type[...] annotation" — the alias was ambiguous between a value and a type depending on module processing order. An explicit TypeAlias annotation resolves it deterministically. * fix: remove duplicate unannotated AlignmentType definition --------- Co-authored-by: Beckett <83560790+BeckettFrey@users.noreply.github.com> Co-authored-by: Beckett Frey <beckett.frey@gmail.com>
The first-launch splash gave no indication it was VoxKit and its "Retrieving assets..." text left users unsure what was happening. - Add a "VoxKit" wordmark so users can tell what launched - Replace the circular spinner with an animated audio waveform that reuses the toolbar's decorative strip design - Paint the card background/border on a child QFrame (a stylesheet background on the translucent window itself renders transparent) - Pull all colors from the shared Colors palette so the splash matches the app's primary-blue scheme - Clarify messaging: explain this is one-time first-launch setup and that VoxKit starts automatically when downloads finish
Clicking "Predict Alignments" without a selected model passed model_id = None to the engine, surfacing a raw "Model 'None' for engine 'MFAENGINE' not found" error (issue #147). Add pre-flight validation in on_predict_alignments mirroring the existing "No Dataset Selected" guard: if no engine or model is selected, show a QMessageBox.warning that names the engine and tells the user to pick a model (or download/import one from the Models page) before the request reaches the engine. Add tests covering the dataset, engine, and model guards.
* Add clearer tooltips to the Register New Dataset page The registration fields only set tooltips on their input widgets, so hovering the field label (the natural target) showed nothing, and the Dataset Path / Analysis Method hints didn't explain the expected format or what each option does. - settings_modal: attach the field tooltip to the form label too, so it shows when hovering either the label or the input (helps every dialog) - Dataset Path: describe the speaker-subfolder layout and the .lab requirement when Transcribed is on - Analysis Method: build the tooltip from each analyzer's own description so it lists what every option does and stays accurate - Expand the Caching, De-identified, Transcribed, and Hand Alignments Path tooltips with fuller explanations - Add a regression test covering label+widget tooltips * Apply Ruff formatting to datasets_page.py
* Initial plan * Add feedback toolbar action with prefilled email template * Correct email to long lived inbox * Make Feedback button a standalone, prominent toolbar action Move the Feedback action out of the left navigation cluster to the far right of the toolbar and give it a distinct outlined-amber style so its purpose is clear and it isn't mistaken for a page-navigation tab. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Beckett Frey <beckett.frey@gmail.com>
* Initial plan * Require confirmation before dataset deletion --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* Add github project badge and clarifiy distinction vs jira
The alignments table declares seven columns (Engine, Model, Type, Location, Date Aligned, Status, Actions) but populated Status at index 4 and Actions at index 5. Status therefore overwrote the Date Aligned cell and Actions landed in the Status column, leaving Actions empty.
PyInstaller's onefile bootloader calls SetDllDirectory(sys._MEIPASS), and that directory is inherited by every child process VoxKit spawns, so _MEIPASS is searched ahead of System32. PyInstaller bundles MSVC runtime 14.29.x there while micromamba is built against 14.4x, so micromamba loaded the older MSVCP140.dll and died instantly with an access violation (0xC0000005) producing no stdout or stderr. That took out provisioning and every micromamba run in services/mfa.py. Windows searches an executable's own directory first, so a matching runtime next to micromamba.exe wins over the inherited _MEIPASS. Only the frozen build is affected, which is what made this easy to miss. Documents the constraint in docs/BUILD.md with refresh instructions, and adds a test asserting the DLLs ship alongside the binary.
Two separate faults in the Windows SQLite-race workaround: Starting the server was never enough on its own. MFA picks its backend from use_postgres in its global config, which defaults to False, so without `configure --enable_use_postgres` it ignored the running server and used SQLite anyway. The race stayed live while everything looked healthy: init and start both reported success and a real Postgres server sat there unused. Capturing the output deadlocked the app. `server start` has pg_ctl spawn a detached Postgres that outlives the call and inherits our stdout and stderr handles. Under capture_output those are pipe write ends, so the pipes never reach EOF and communicate() blocks forever. A timeout does not help: on Windows subprocess.run answers TimeoutExpired by killing the child and calling communicate() again with no timeout, which hangs inside subprocess.run before any except clause runs. Uses DEVNULL instead; the output was discarded regardless. Also adds describe_process_failure so a process killed by the OS reports its exit code rather than an empty stderr that reads like "not found".
Provisioning ran inside the first-launch routine, where a failure was swallowed, the first-launch flag was marked complete anyway, and the step never ran again. A single interrupted setup left MFA permanently unavailable with nothing in the log to say why. Moves it out into ensure_mfa_environment(), gated on is_aligner_env_ready() rather than on the first-launch flag, so a failed or interrupted attempt is simply retried next launch. It is idempotent and cheap once the environment is ready, and micromamba resumes from its package cache after an interruption, so running it every launch costs nothing after setup succeeds. Also switches this path from print() to logging: under a --windowed PyInstaller build sys.stdout goes to devnull, so print() diagnostics were discarded exactly when they were most needed.
Both failure modes surface as an opaque "MFA alignment failed (exit 1)" and are environmental rather than code bugs, so agents should rule them out before debugging VoxKit. A half-deleted Postgres data directory deadlocks alignment permanently: the detached server holds handles under pg_mfa_global, so deleting ~/.voxkit while it runs strips the contents but cannot unlink the directory. MFA keys both server init and server start off directory existence, so the resulting empty shell is unrecoverable via its CLI. Developer Mode also changes which alignment code path runs. With it on, os.symlink succeeds unprivileged and MFA's shutil.copyfile fallback never fires, so the copy path can look like dead code on a dev machine.
…a-provisioning-and-server
…ding - W2TG aligner: default Use Speaker Adaptation to on. The bundled Wav2TextGrid library hard-raises when it's off, so the previous default broke every out-of-the-box W2TG alignment attempt. - TextGrid timelines: render the word tier above the phone tier everywhere they appear together (Viewer, Compare, Correct Alignments), including the inline active-segment label. - Train Aligners: gray out the sidebar step and disable its page whenever no registered dataset has a manual/hand alignment, since training against machine-generated alignments alone just reinforces their own errors. Explained in the Pipeline Overview under stage B rather than a sidebar tooltip (the sidebar is too narrow, and Qt does not reliably show tooltips on disabled QListWidgetItems). - App branding: wire assets/vk.ico (Windows taskbar/window icon) and add a university credit line to the first-launch/MFA-provisioning splash dialog. Bundle assets/ into PyInstaller builds so this also works in packaged builds, not just uv run main.py.
|
Thank you for these. @nrgslp this is going to be easier for me to review if I just redo each modular change one at a time. It's too much code churn for one PR. v0.5.0 seem to hold up on Tristan's machine now, I'm worried I'll miss something If I don't do these one at a time, and I don't want to leave you hanging at the prez. Also note that testing with |
|
@nrgslp Another quick note: The app icon is in use, it just only shows in the real bundled version, not the development version. So I'm going to exclude that, let me know if I'm misunderstanding. |
Moved to its own branch and PR (#166) so it can be reviewed on its own merits, since gating a whole pipeline step is a bigger call than the rest of this branch's polish. No behavior change intended here beyond reverting to the pre-gating state: the Train Aligners step is available again, and the Pipeline Overview goes back to its original stage B copy. Reverts the gating hunks only. The W2TG speaker-adaptation default, the word/phone tier order, and the app branding work all stay.
|
Superseded — this branch bundled four independent changes, so each has been split into its own PR against
Together the four reproduce this branch's diff exactly — verified by merging all four onto Closing in favor of those. The |
Summary
Last round of polish before MVP, three independent fixes:
Wav2TextGridlibrary hard-raises (ValueError("Disabling speaker adaptation is not supported as of now.")) whenever it's off, so the previous default broke every out-of-the-box W2TG alignment attempt.assets/vk.ico(previously unset anywhere, so the app showed the generic Python icon), and the first-launch/MFA-provisioning splash dialog now shows a university credit line.assets/is now bundled into PyInstaller builds (--add-data) so this also works in packaged.exe/.app/AppImage builds, not justuv run main.py.Verification
uv run main.py) that the app launches cleanly and the splash dialog renders with the new credit line.