Skip to content

MVP polish: W2TG default, tier order, app branding - #163

Closed
nrgslp wants to merge 28 commits into
mainfrom
feature/mvp-final-polish
Closed

MVP polish: W2TG default, tier order, app branding#163
nrgslp wants to merge 28 commits into
mainfrom
feature/mvp-final-polish

Conversation

@nrgslp

@nrgslp nrgslp commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Last round of polish before MVP, three independent fixes:

  1. W2TG "Use Speaker Adaptation" now defaults on. The bundled Wav2TextGrid library 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.
  2. Word tier renders above the phone tier everywhere a TextGrid timeline is drawn (Viewer, Compare Alignments, Correct Alignments), including the inline active-segment label in the Viewer.
  3. App branding wired up: the taskbar/window icon now uses 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 just uv run main.py.

Note: this branch originally also gated the Train Aligners step on having a hand-aligned dataset. That change has been split out into #166 so it can be reviewed on its own; the last commit here reverts it from this branch.

Verification

  • Confirmed via live run (uv run main.py) that the app launches cleanly and the splash dialog renders with the new credit line.
  • Syntax-checked all touched files.

BeckettFrey and others added 26 commits May 19, 2026 08:09
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.
…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.
@BeckettFrey

BeckettFrey commented Aug 18, 2026

Copy link
Copy Markdown
Member

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 uv run main.py is actually doing less than it seems. All the recent issues have arisen from problems specific to the bundled app, so invoke windows-build and then following that instance is much more assuring.

@BeckettFrey

BeckettFrey commented Aug 18, 2026

Copy link
Copy Markdown
Member

@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.

@BeckettFrey
BeckettFrey changed the base branch from develop to main August 19, 2026 17:36
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.
@BeckettFrey BeckettFrey changed the title MVP polish: W2TG default, tier order, Train Aligners gating, app branding MVP polish: W2TG default, tier order, app branding Aug 19, 2026
@BeckettFrey

Copy link
Copy Markdown
Member

Superseded — this branch bundled four independent changes, so each has been split into its own PR against main for separate review:

PR Change
#166 Gray out Train Aligners until a dataset has manual alignments
#167 Default W2TG "Use Speaker Adaptation" to on
#168 Render the word tier above the phone tier in TextGrid timelines
#169 Wire up app branding: window icon and splash credit line

Together the four reproduce this branch's diff exactly — verified by merging all four onto main and diffing against 6267567; the only delta is ee37272, which this branch predated and the splits correctly retain. They also merge cleanly with each other. The one file two of them touch is src/voxkit/gui/__init__.py (#166 adds a disabled-item style rule, #169 sets the window icon); the hunks don't overlap, so merge order doesn't matter.

Closing in favor of those. The feature/mvp-final-polish branch is left on the remote for reference.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants