Adds the sherpa-onnx dictation model picker under the Transcription engine
row so the model the hotkey loads is switchable without opening Settings,
routes the Sherpa transcription path through that same preference, and makes
the Windows desktop dev stack recover instead of demanding Task Manager.
Refreshes the Tauri and npm dependency pins that went with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
Fail before backend/window startup when Linux source hosts lack Enigo’s libxdo linker input or WebKit’s GStreamer audio sink. Print an exact distro package command, sync source-build docs, and lock the probes with deterministic tests.
Closes#1680Closes#1682
Refreshes frontend, tooling, and Python dependencies; regenerates the frozen lockfiles and worker protocol stubs. Keeps compatibility caps for FastAPI, Oxlint, and Vitest where newer releases break repository contracts. Updates the Uvicorn bind-failure regression test for its new nonzero exit code. Fully tested after merging current main.
* fix(scripts): desktop-prod:run wiped the data it was documented to preserve (#1333)
`scripts/desktop-prod.sh` emulates a first install, so wiping is its default: it
removes the app data dir, `~/.omnivoice` (the SQLite database, every voice
profile, all outputs), the Tauri logs and the WebKit profile. `--keep-data` is
the only thing that suppresses that block.
`--skip-build` is an independent flag that only skips the cargo compile, and
`desktop-prod:run` passed it alone — while the script's own header calls that
command "re-launch last build (skip compile)" and its closing banner tells you
to use it that way. So "just start it again without recompiling" silently
deleted the developer's voice profiles and project database, every time.
The fix is in the package scripts rather than the flag parsing: making
--skip-build imply --keep-data would remove a legitimate combination (fresh
data without paying for a recompile). The two stay independent, and the help
text now says so.
desktop-fresh:run is deliberately untouched — that script is a stricter
new-user emulation, so wiping is the point of its name.
Tests pin all three rules, and were confirmed fail-before by reverting the
desktop-prod:run line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(scripts): kill the live instance before every launch, not only before a wipe
Greptile P1 on this PR, and it was a regression I introduced.
The app registers tauri_plugin_single_instance, and that callback ignores the
incoming argv — it just refocuses the window the RUNNING process already owns.
So starting a second copy over a live one does nothing visible.
That was previously masked: kill_running_instances sat inside the
`KEEP_DATA = false` branch, so every run happened to kill first *because*
every run wiped. Adding --keep-data to the re-launch aliases removed the wipe
and would have taken the kill with it — `desktop-prod:run:pill` would have left
the user in studio mode with --pill silently discarded, and plain
`desktop-prod:run` would have refocused the OLD build instead of the one just
compiled, which is the entire point of that command.
The kill is now unconditional, before the wipe branch. Its two reasons are
independent — zombie-backend-after-wipe, and single-instance-swallows-argv —
and only the first was ever about wiping. Adjusted its closing line, which
said "safe to wipe" and now also runs when nothing is being wiped.
New test asserts the call is not nested inside the KEEP_DATA branch;
confirmed fail-before by moving it back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(scripts): scope the kill to this checkout, warn about an installed app
Making kill_running_instances unconditional (so --keep-data re-launches
still get past single-instance) widened the blast radius of its pgrep:
"OmniVoice Studio.app" also matches an installed /Applications copy, so
desktop-prod:run would kill the shipped app a developer was using and take
their unsaved work with it. That was previously masked — the kill only ran
on wipe runs, where a clean slate had been asked for explicitly.
Scope the pattern to ${TAURI_DIR}/target/debug/, which covers both launch
shapes and nothing else. An installed instance still gets named rather than
ignored: single-instance keys on the bundle id, so it swallows this launch
too, and silence would just trade one confusing failure for another.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
In dev there is no supervisor: concurrently's --kill-others-on-fail tears
the whole stack down the moment uvicorn exits, the cause scrolls away with
the terminal, and the browser tab just says it can't reach the backend —
which is exactly how #1164 arrived with zero diagnostics.
- scripts/dev-backend.mjs: dev:api now runs uvicorn through a wrapper
(command args byte-identical, stdio inherited). On a non-Ctrl+C, non-zero
exit it prints a boxed banner: exit code/signal, the last 20 lines of
omnivoice.log (data dir resolved exactly like backend/core/config.py),
an OOM hint (SIGKILL/137 + the Linux journalctl -k check), and a pointer
to the crash notice the run sentinel raises on the next backend start.
Exits with the child's own code so --kill-others-on-fail still works.
Verified live: started the dev backend, SIGKILLed it, banner printed
with the real log tail and exit code 137.
- docs-sync: troubleshooting.md gains §14c (browser/dev/Docker crash
forensics: the mode-aware error, the dev banner, run_sentinel.json /
last_run_crash.json / GET /system/last-run-crash, cap+ack+version-gate
semantics) and §14's crash-notice blockquote no longer implies the
notice is desktop-only; CONTRIBUTING.md documents the dev:api wrapper.
- CHANGELOG.md: [Unreleased] entry for the #1164 class fix.
Tests: tests/frontend/devBackend.test.mjs (5) — the uvicorn args are
pinned byte-identical, data-dir resolution mirrors config.py, tail/banner
content incl. the OOM shapes.
desktop-prod fixes:
- `tauri build --debug` used to produce every bundle and THEN exit 1 at the
updater-artifact signing step (no TAURI_SIGNING_PRIVATE_KEY on dev
machines); the script papered over it with a blanket "non-fatal bundle
error" grep that also swallowed real bundling failures. Local emulation
builds now pass `--config '{"bundle":{"createUpdaterArtifacts":false}}'`
and only build the bundle the script launches (--bundles app / appimage,
--no-bundle on Windows), so the build exits 0. Any nonzero exit now FAILS
the script — the sole tolerated case is a specifically-detected
linuxdeploy/FUSE failure on Linux when the raw debug binary was produced.
- The HF cache wipe ran `rm -rf ~/.cache/huggingface` on macOS/Linux — the
SHARED global cache (backend/core/config.py only relocates it on Windows),
deleting models unrelated to OmniVoice. Non-app-scoped cache paths are now
kept with a "models will be reused" notice; FRESH_NUKE_HF=1 opts in.
- Honest clean marks (removed ✓ / already-clean ○ instead of ✗ for success),
`open -n` always (plain `open` focused a stale running instance instead of
launching the freshly built one), stale-AppImage removal on Linux.
New `bun desktop-fresh` (+ desktop-fresh:run), macOS-only with explicit
refusal elsewhere: true new-user emulation.
- Blank slate: everything desktop-prod cleans PLUS the traces that survive a
reinstall + data wipe — ~/Library/WebKit (webview localStorage), Caches,
HTTPStorages*, Preferences plist (+ defaults delete), Saved Application
State. Per-path found/removed/absent status with sizes; --dry-run prints
the full plan without touching anything.
- Dev-machine camouflage: launches by direct exec of the bundle's Mach-O
(which inherits env — `open` hands off to launchd and drops it) with PATH
stripped of /opt/homebrew/{bin,sbin} + /usr/local/bin and HF_TOKEN /
HUGGING_FACE_HUB_TOKEN / HF_HOME / HF_HUB_CACHE / HF_ENDPOINT /
OMNIVOICE_* unset, and prints a banner of what is hidden.
Shared pure helpers live in scripts/desktop-common.mjs, covered by 9 node
tests (tests/frontend/desktopScripts.test.mjs): every cleanable path is
app-scoped and under $HOME, the PATH/env sanitizers strip exactly the
intended entries, and the build args carry the updater-artifacts-off config.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
`bun desktop`/`bun dev` assumed node_modules was current, so after pulling a
branch that adds a frontend dep (e.g. the shadcn migration's tw-animate-css /
@radix-* packages) vite failed with "Can't resolve '<pkg>'" until the user
manually ran bun install. CI never caught it (CI does a frozen install).
predev/predesktop now run `bun install` first (a no-op ~25ms when up-to-date),
so a fresh pull just works.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds taze (root devDep) + `bun run deps:check` = `taze -r --maturity-period 7`:
recurses the bun workspace (root + frontend), lists available updates, and is
READ-ONLY (never writes package.json without -w). The 7-day maturity window
skips just-published versions as a supply-chain precaution.
Manual tool by design — no auto-update, no Renovate infra, nothing added to CI.
Run `bun run deps:check` when you want a refresh overview; `taze major` for major
bumps; add `-w` to apply.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`bun desktop` runs concurrently[dev:api, dev:desktop] with --kill-others-on-fail.
dev:api is a uvicorn backend on :3900, but the Tauri app launched by dev:desktop
ALSO manages a backend — on boot it sees :3900 in use (and not yet healthy,
because the dev backend is still importing torch + loading 32 models) and
'takes ownership', killing the dev:api process. That exits 137, which trips
--kill-others-on-fail and tears the whole session down.
The Tauri app already supports TAURI_SKIP_BACKEND to skip backend management
(lib.rs:654) — it just wasn't wired for the concurrently-managed dev flow. Set
it on dev:desktop so the dev app attaches to concurrently's backend instead of
fighting it. Set only on dev:desktop (not dev:api, and not the standalone
`frontend` desktop script, which legitimately self-manages the backend).
bun's script shell evaluates the inline VAR=val cross-platform (verified), so no
cross-env dep / lockfile churn. Prod (desktop-prod) is unaffected — there the
Tauri app is the sole backend manager and orphan-kill is correct.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`bun desktop-prod` (clean) wipes everything including the HF model cache, so
every fresh-install emulation re-downloads multi-GB weights — slow and bandwidth
-heavy, and the exact pain users on flaky networks hit. --keep-models wipes
app/backend data, logs, and webview state for an honest first-run, but KEEPS the
model cache so the weights aren't re-pulled. Ignored under --keep-data (which
keeps everything). Adds the `desktop-prod:keep-models` convenience script.
Scripts-only package.json change — no deps, bun.lock unaffected.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(studio): workspace UX overhaul — right-side panels, shared waveform player, dub pipeline UX, setup polish, UI-wide fixes
Voice workspace (specs: docs/specs/voice-studio-unification.md, workspace-connectivity.md):
- Right-side panels replace the left sidebar for clone/design and dub:
WorkspaceVoices (saved profiles), WorkspaceHistory (scoped history with
All/Clone/Design filters), WorkspaceProjects (dub projects)
- Prompt restacked over Voice Source in one definition column (spec §1)
- Gallery "Use voice" now hands off via pendingProfileId and lands in clone
- Shared <WaveformPlayer> (wavesurfer + in-DOM media element for Tauri
WebKit, blob routing via preview endpoint, 404 -> "audio file missing")
replaces every bare <audio controls>; lazy-mounted via IntersectionObserver
Dub:
- Pipeline stepper (Upload -> Prepare -> Transcribe -> Edit -> Generate -> Export)
- Multi-language preview switcher pills (Original + per-track, ElevenLabs-style)
- Batch multi-language generation via langOverride loop
- FloatingPill: bottom-center, suppressed on its homeMode tab (no dup progress)
- Transcript skeleton shimmer (no fake data), progress overlays the video,
exports demoted behind Generate, empty right-panels collapse
Chrome/layout:
- Nav rail is full-window-height; content yields to the logs footer via
padding-bottom; footer joins the rail edge (no overlap at any UI scale)
- UI scale 60–175% slider with zoom-compensated container sizing
- LogsFooter: merged single Logs tab when collapsed, per-source tabs on
expand; Updates chip lives with the logs tabs
- Gallery: three independently scrollable filter lanes, uniform 26px controls
- Font picker as live-preview grid; double-click titlebar maximize fixed
(single mousedown detail-2 handler)
First-run:
- Setup wizard: pinned action row + scrollable content at every window size,
one-line head-ellipsized paths, height budget for short windows, library
rows back to one-line grammar, raw i18n key + duplicate host fixed
Performance/i18n/consistency sweep (10-agent scan, 47 fixes):
- i18n locales lazy-loaded per language (i18n chunk 1.84 MB -> 76 kB)
- Undefined CSS vars replaced with real tokens across 8 stylesheets;
hardcoded hexes tokenized; emoji swept to lucide icons app-wide
- Poll throttling (sysinfo subscription scoped to Header, logs 45s when
collapsed, rAF only during playback), hardcoded strings moved to t()
Build clean; 312/312 tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(studio): re-flow clone/design columns (grid rows collapsed in restack) + strip placeholder emoji across locales
The base .studio-column grid (minmax(0,1fr) rows) collapsed to 0 height
inside the new auto-height definition column, overlapping every panel in
design mode — found via Playwright visual pass. Columns now re-flow as
natural-height flex stacks. Also removed the leftover pencil emoji from
clone.prompt_placeholder in all 21 locales.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(design): compact the design control stack — 2-up facet selects, scrollable tag row, tighter rhythm
English accent + Chinese dialect dropdowns share one row (full-width on
narrow), insertable tag chips collapse from three wrapped rows to one
scrollable line, and describe/personality spacing tightens — the whole
design stack now fits a single viewport.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(spec): unification migration renumbered 0004 — upstream 0003 is voice-profile consent
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ci): clear hardcoded-CJK gate — ASCII '+' in spec wireframes, reword voiceIcons comment
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(spec): migration is 0005 — 0004 taken by mcp bindings upstream
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
`bun run desktop-prod` (and its :run/:upgrade/:pill/:run:pill variants)
invoked `bash scripts/desktop-prod.sh` directly. On Windows, cmd and
PowerShell have no `bash` on PATH unless Git Bash happens to be there,
so the documented from-source install path died with a cryptic spawn
failure before printing anything — the exact first step in issue #282's
repro.
Add scripts/desktop-prod.mjs, a tiny launcher (runs under bun or node):
- macOS/Linux: execs the bash script unchanged — zero behavior change.
- Windows: locates Git Bash via `where.exe bash`, well-known Git for
Windows install paths, or derived from git.exe's location; explicitly
skips C:\Windows\System32\bash.exe (the WSL launcher, which would run
the script inside Linux and wipe/launch the wrong paths).
- No usable bash: prints an actionable error (install Git for Windows,
use `bun run desktop`, or use the installer) instead of a spawn error.
All flags are forwarded untouched and the child's exit code is
propagated. scripts/desktop-prod.sh itself is unchanged, and
docs/install/windows.md now lists Git for Windows as a prerequisite
for from-source installs.
Refs #282
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Replace the Functional Source License with the GNU Affero General Public
License v3 across the project, with a paid commercial license retained for
proprietary/closed-source use without AGPL obligations (open-core, like
Firecrawl).
- LICENSE: verbatim AGPL-3.0 text under an AGPL Notice + Scope header;
drops the FSL "Competing Use" framing and the 2-year Apache-2.0 conversion.
The bundled omnivoice/ TTS model stays Apache-2.0 upstream (AGPL-compatible).
- Manifests now declare SPDX AGPL-3.0-only: pyproject.toml, Cargo.toml
(normalized from bare AGPL-3.0), and both package.json (added license field).
- README.md / README_CN.md: badge, pricing, commercial-use FAQ, License section.
- en.json: in-app Commercial License copy reworded to AGPL; the false
"converts to Apache 2.0" FAQ removed (+ its renderer block in SupportPage.jsx).
Non-English locale strings still describe the old FSL model and are left for a
follow-up translation pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(tailscale): serve over HTTP when tailnet has no HTTPS certs
Real-world failure: 'tailscale serve --https=443' on a tailnet without the
HTTPS Certificates feature (CertDomains: None) fails with 'error enabling
https feature: 404'. Detect cert availability from status --json and use
--https only when certs exist; otherwise serve over --http (the WireGuard
tunnel encrypts transport anyway). Also surface a clear note/error instead
of the raw 404, and a 'run tailscale up' hint when not running. Verified the
--http path live on a real tailnet. SharingPanel now shows the returned note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(dev): launch app in parallel with backend (drop wait:api gate)
dev/desktop no longer block the Tauri/vite launch on the API being HTTP-ready
— the window appears immediately and the frontend's setup-status check
already retries (30x1s) until the API answers. Matches prod, where the window
shows BootstrapSplash while the sidecar boots. Dev-only; no shipped change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: onboarding demos, opt-in bug reporting, error-docs deeplinks + issue triage
Working-tree snapshot bundling several in-flight workstreams (v0.3.0):
- Onboarding/demo system: DemoPresetGrid, DictationDemo, DubbingDemo components
+ tests, render scripts (render_demos_omnivoice.py, build_demos.sh,
build_dub_demo.sh), personalities preview URLs, alembic 0002 voice-profile
demo fields.
- Opt-in bug reporting: ReportBugButton (prefilled GitHub-issue URL path).
- Error transparency UX: errorDocsMap deeplinks + BootstrapSplash/error wiring.
- Dub workspace: DubSegmentRow/Table, WaveformTimeline, dubSlice tweaks.
- Issue triage: .planning/issue-clusters/ (plan-01..05 root-cause masters,
GH #128-#132).
- CLAUDE.md: hard rule — everything ships on v0.3.0, no version bumps.
KNOWN GAP (why this is a draft): the generated demo audio assets are NOT in
this tree, and backend/assets/samples/demo_voice.wav is deleted. onboarding.py
guards the missing file (skips seeding the demo profile with a warning), so no
crash — but first-run Launchpad will be empty and /demo_audio/ preview URLs
404 until assets are regenerated via scripts/build_demos.sh. Do not merge
before regenerating + committing the demo assets.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(dub): timing strategies — kill audio compression, add Concise + Stretch Video
Replaces the current audio time-compression default (atempo squeeze to fit
slot) that produced chipmunk/alien output on high-density target languages
like Bengali. Two new user-selectable modes; legacy behaviour kept behind
an explicit "Strict slot" choice.
New `DubRequest.timing_strategy` enum (default "concise"):
- "concise" Translator trims text to fit at natural rate; if it
still overflows, hard-trim at slot with a fade so we
never overlap the next speaker. Surface overflow_s
per segment so the user can shorten the text.
- "stretch_video" Audio plays at natural 1.0× rate. Backend computes a
per-segment new timeline; persists a video_stretch_plan
on the job. Mux step (dub_export) builds an ffmpeg
trim+setpts+concat filter graph that stretches each
segment's video portion to match the natural-rate dub
audio. Gaps/pre-roll/tail pass through at 1.0×.
Sub burn under stretch_video is skipped in one pass
(cues would drift).
- "strict_slot" Legacy atempo squeeze. Retained for back-compat.
Director rate-bias side-effect (seg_speed *= bias) now gated on strict_slot
only, so "urgent"/"slow" direction tokens keep their instruct effect in
the new modes without chipmunking.
Per-segment fit_status emitted in the SSE done event:
{status: "fits" | "overflows" | "video_stretched", overflow_s?, stretch_ratio?}
DubSegmentRow's "Sync: 100%" badge (which was lying — sync_ratio was always
~1.0 because the TTS loop pre-trimmed to slot) is replaced with a truthful
"Fits / Overflows +Ns / Video 1.18×" label.
Frontend:
- prefsSlice.timingStrategy (persisted, store v3→v4 with safe migrate).
- DubTab footer Segmented control: "Concise · Stretch Video · Strict slot".
- useDubWorkflow passes timing_strategy on /dub/generate; consumes fit_status.
Tests: tests/test_dub_timing_strategy.py — 13 cases covering schema
defaults/validation, _build_video_stretch_filter_graph (pre-roll, gap,
tail, empty-plan early return, post-subtitle chain-in), and
_video_stretch_plan_for guards. 30/30 existing dub tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(waveform): surface missing source as "Source media missing" instead of code-4 black box
When a project's underlying media file is gone (moved or deleted between
save and reload) the <video> element fires MediaError code 4 and the
companion audio fetch returns HTTP 404 — both were silently warned to
the console while the user stared at an unresponsive black panel and an
empty waveform.
- WaveformTimeline now flips loadError when the video element rejects
code 3 (decode) or 4 (src not supported), and tracks `sourceMissing`
separately so the error UI can name the actual problem.
- The audio decode fallback chain catches HTTP 404 specifically and
treats it as source-missing instead of loading silent empty peaks —
an empty waveform on a deleted source is more confusing than a clear
"Re-upload the video to continue" message.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tray): "Show OmniVoice" reloads when the webview is blank
When the dev Vite server restarts (or the main window is created before
the backend is ready), the webview load fails and the window is left
with `<body></body>` plus a "Could not connect to the server" console
error. Clicking "Show OmniVoice" from the tray menu just re-showed the
broken window — there was no recovery path short of quit+relaunch.
Now the show handler runs a tiny eval after `show()`/`set_focus()` that
calls `location.reload()` only when `document.body.childElementCount === 0`.
A healthy window doesn't blink (body is non-empty); a blank one
self-recovers as soon as the user clicks Show.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#133): bug-report diagnostics field mapping + drop unused imports
Address PR #133 review:
- ReportBugButton: /system/info exposes `platform` + `device`, not
`os`/`torch_device`/`gpu` — those reads silently dropped OS/GPU from every
bug report. Map to the real fields (CodeRabbit). Also remove the dead
`home` local in stripHome (CodeQL unused-variable).
- DictationDemo: drop unused `Loader` import (CodeQL unused-import).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: initialize OmniVoice stabilization milestone project
* chore: add project config (yolo + balanced)
* docs: domain research for stabilization milestone
* docs: define v1 requirements for stabilization milestone
* docs: add GGUF + singing engine spike requirements (Phase 4 new)
* docs: roadmap revision + CLAUDE.md (7 phases, 62 reqs, +GGUF/SING spikes)
* docs(phase-0): add Gates phase RESEARCH.md
Phase 0 research synthesizes the cross-platform CI matrix, frozen
omnivoice_data fixture, installer post-build smoke, SHA-256 checksum
publishing, and PR-template extension into copy-paste-ready YAML and
Python snippets composed entirely from existing in-repo patterns.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(phase-0): add Gates phase CONTEXT, PATTERNS, and PLAN
Phase 0 — Gates is the hard pre-condition for v0.3.x stabilization.
Lays cross-platform CI matrix (macos-14/windows-2022/ubuntu-22.04),
regression fixture (≤200 KB), installer smoke on tag push, SHA-256
checksums in release body + per-OS SHA256SUMS-*.txt assets, PR
template with RC cadence + fixture line, and the open-PR landing
for #51.
Plan covers GATE-01..06; structured into 7 slices (A–G) with explicit
Slice C → Slice G dependency reordering so the new smoke-matrix lands
on main before PR #51 (CONTEXT.md L86 interleave decision).
Plan-checker iteration 2: APPROVED — all 3 BLOCKERs + 3 MAJORs from
iteration 1 resolved (file truncation/Slice-G missing, GATE-06 sibling
PR verification, Slice C ordering, Truth #5 wording, macOS Tauri
WebView avoidance per Pitfall #5, Windows taskkill per Pitfall #2).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(00-gates): seed regression fixture (GATE-01)
- scripts/seed-test-fixture.py — deterministic builder for tests/fixtures/omnivoice_data/
- wipes + rebuilds; fixed created_at=1700000000.0; all-zero PCM for byte-deterministic diffs
- calls backend.core.db.init_db() directly (alembic versions/ is empty — see CONTEXT.md)
- checkpoints WAL → DELETE on close so no -shm/-wal sidecars pollute git status
- exits non-zero if fixture > 200 KB
- tests/fixtures/omnivoice_data/{omnivoice.db, README.md} — 8-table empty DB + 1 voice_profiles row
- tests/fixtures/omnivoice_data/voices/test-voice/{profile.json, sample.wav} — 1-sec 24 kHz mono silence
- .gitignore — explicit allow-list (!tests/fixtures/omnivoice_data/**) so the existing
omnivoice_data/, *.db, *.wav patterns don't hide the fixture from git
Verifies: du = 144 KB on disk; sqlite_master lists 8 init_db tables + sqlite_sequence;
voice_profiles has exactly 1 row id='test-voice'; 0 rows in generation_history.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(00-gates): add tests/smoke/test_boot_smoke.py (GATE-01)
- tests/smoke/__init__.py — package marker so pytest treats tests/smoke/ as a module
- tests/smoke/test_boot_smoke.py — 4 in-process FastAPI TestClient smoke tests:
* test_health_returns_ok — /health returns 200 + {status:ok, device:...}
* test_profiles_endpoint_lists_fixture_voice — /profiles surfaces the seeded
test-voice row (validates OMNIVOICE_DATA_DIR wiring → DB_PATH → init_db schema)
* test_system_info_includes_data_dir — /system/info resolves data_dir
* test_history_endpoint_empty — /history reaches DB and returns []
Test isolation env vars (OMNIVOICE_MODEL=test, OMNIVOICE_DISABLE_FILE_LOG=1)
set at module top BEFORE any backend import — pattern from tests/test_router_smoke.py.
Fixture is copied to a per-session temp dir so the test never mutates the
checked-in artifact (SQLite file-change counter + runtime subdirs like dub_jobs/
would otherwise dirty `git status` after every run).
Failure mode: if tests/fixtures/omnivoice_data/ is missing, pytest.fail at
import time with the regenerate command.
- .gitignore — tighten the GATE-01 allow-list to ONLY the seed-produced files
(README.md, omnivoice.db, voices/test-voice/profile.json, sample.wav).
Prevents future runtime subdirs the backend may create under the fixture
from being accidentally committed.
Verifies: `uv run pytest tests/smoke/ -q --tb=short` → 4 passed in 1.31 s
(target was < 30 s). `git status` clean after a test run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(triage): record post-planning GitHub state — PR #62, new issues, OOS deferrals
- GATE-06: mark #53 + #61 merged (2026-05-16); add #62 (Wave 1 quick wins) to gate set
- INST-01: note PR #62 implements setuptools pin (closes#58)
- INST-04: note PR #62 lands README docs for #56 workaround
- INST-12: new requirement for #65 Windows Triton/torch.compile OOM (filed post-planning)
- Out of Scope: defer #67/PR #68 (audio effects), #64 (custom model dir),
PR #66 zh-CN (i18n milestone), #63 (empty-template bug)
PR #62 is the user's own Wave 1 work landed as a separate PR while
GSD planning ran in parallel. Merging it eliminates duplicate work
in Phase 1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(00-gates): add cross-platform smoke matrix (GATE-02)
- New smoke-matrix job on macos-14, windows-2022, ubuntu-22.04
- needs: test, fail-fast: false, timeout-minutes: 10
- Pinned actions: checkout@v4, setup-python@v5, setup-uv@v3 (cache enabled)
- Per-OS ffmpeg + libsndfile install (brew/choco/apt via awalsh128 cache)
- UV_HTTP_TIMEOUT=120, UV_HTTP_RETRIES=5 for restricted-network resilience
- Narrow scope: uv run pytest tests/smoke/ -q --tb=short
- Existing `test` and `tauri-cross-platform` jobs untouched
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: add workflow_dispatch to ci.yml so smoke-matrix can run on feature branches
* feat(00-gates): add --health-check CLI flag to backend entrypoint (GATE-03)
- argparse on __main__ block; --health-check boots uvicorn in a daemon
thread and polls http://127.0.0.1:3900/health every 5s for up to 60s.
- Prints 'OK — /health responded 200 after Ns' and exits 0 on first 200.
- Prints 'FAIL — /health did not respond 200 within 60s' to stderr and
exits 1 on timeout. Default invocation behavior unchanged.
- No new deps (stdlib argparse/threading/time/urllib.request/sys + uvicorn).
- Consumed by per-OS installer-smoke step in .github/workflows/release.yml.
Verified locally: exits 0 in 5s against tests/fixtures/omnivoice_data/.
* ci(00-gates): add per-OS installer smoke to release.yml (GATE-03)
Adds three matrix-leg-specific steps after 'Build + release (Tauri)',
each gated by runner.os with timeout-minutes: 5:
- macOS (macos-14): hdiutil attach DMG → locate bundled Python backend
inside *.app/Contents (NOT the Tauri WebView shell — RESEARCH Pitfall
#5: WebView hangs on headless runners) → invoke --health-check →
hdiutil detach. Falls back to *.app/Contents/Resources and hard-fails
with a directory listing if no backend binary found.
- Windows (windows-2022): msiexec /quiet install → find backend.exe
under 'C:/Program Files/OmniVoice Studio' → invoke --health-check in
background, wait, then taskkill //F //T //PID to cleanup orphaned
PyInstaller child processes on port 3900 (RESEARCH Pitfall #2).
- Linux (ubuntu-22.04): --appimage-extract (no FUSE on GH runners),
locate binary or AppRun, run under xvfb-run -a.
Bundle-only regressions (PyInstaller missing-module, Tauri sidecar
path mismatch) are invisible to ci.yml's in-process smoke matrix —
this step closes that gap before any release is published.
Verified: YAML parses; all three steps present; gating + timeout
correct; Pitfall #2/#5 mitigations preserved.
* ci(00-gates): publish SHA-256 checksums in release body + as asset (GATE-05)
- Add 'Compute SHA-256 checksums' step writing SHA256SUMS-<label>.txt
per matrix leg using native shasum/sha256sum (Git Bash on Windows).
- Add 'Append checksums to release + attach SHA256SUMS file' step using
softprops/action-gh-release@v2 with append_body: true so the hashes
land in the release body alongside tauri-action's content (not
replacing it) and the file is uploaded as a release asset for
'shasum -c SHA256SUMS-<label>.txt' verification.
- Both steps gated by 'github.event_name == push && refs/tags/v*' so
workflow_dispatch dry-runs do not attempt to attach to a non-existent
release (per CONTEXT.md L70 + RESEARCH Pitfall #7 deferral of any
aggregate cross-leg SHA256SUMS job).
- fail_on_unmatched_files: true to surface path-resolution errors loudly.
* docs(00-gates): document RC cadence + regression-fixture check in PR template (GATE-04)
* docs(setup): add HF token persistence guide for macOS/Windows/Linux (DOCS-05)
Covers two persistent paths:
- Method A — canonical ~/.cache/huggingface/token via huggingface-cli login
- Method B — shell env var (~/.zshrc / ~/.bashrc / Windows User scope)
Documents the v0.2.7 "session only" in-app behavior + notes that
Phase 1 AUTH-03 will make in-app pastes write to the canonical file.
Bundled with Phase 0 PR per user request. Strictly DOCS-05 scope —
zero code changes, no engine touches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* spec(auth): redesign HF token resolution as 3-source cascade with fallback (AUTH-01..06)
Replaces the env_store.py file-based design with a SQLite-backed app
store + cascade resolver that checks app → env var → ~/.cache/huggingface/token
in priority order, with automatic fallback to next source on HTTP 401.
User-explicit design decision:
- App-stored token (SQLite settings table, AES-GCM encrypted) wins
- Env var ($HF_TOKEN) second
- Global huggingface-cli login file third
- All three sources visible in Settings → API Keys with "Active" badge
- Save action populates BOTH app store AND canonical HF file (defense in depth)
New requirement:
- AUTH-06 — on 401, auto-retry next source in cascade before erroring
Also: traceability count corrected (62 → 74 — undercount at planning +
INST-12 + AUTH-06 added post-planning). All 74 v1 reqs mapped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(auth): backend recognizes HF token from canonical file, not just env var
Two call sites were only checking $HF_TOKEN env var, missing the canonical
~/.cache/huggingface/token file written by `huggingface-cli login` (or the
app's future Save action):
- system.py `/system/info` `has_hf_token` flag — UI showed "No HF token"
even when `huggingface-cli login` had populated the file.
- model_manager.get_diarization_pipeline — pyannote diarization silently
returned None when only the canonical file was set. This is the bug
behind issue #35 (speaker diarization setup failure).
Both fixes use the same pattern: env var > huggingface_hub.get_token()
(which reads the canonical file). Adds a local _has_hf_token() helper
to system.py with a comment marking it as prelude to the AUTH-01..06
cascade (Phase 1 token_resolver.py will layer SQLite app-store on top).
Closes#35 sub-issue (canonical token invisible to diarization).
Cross-cuts AUTH-02 + AUTH-06 design for Phase 1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(dictation): make pill-widget mode reachable from GUI + scripts (INST-13)
The dictation widget infrastructure shipped in PR #40 but was only reachable
via the undocumented --pill CLI flag. Adds three discovery paths:
1. Tray menu: "Switch to Dictation Widget" (studio mode) — saves
launch_as_widget=true to config, relaunches with --pill, exits current.
Mirrors the existing "Open Studio" path in pill-mode tray.
2. Persistent config: AppConfig.launch_as_widget (bool, default false). Read
at startup via load_config_pre_app() (uses dirs-next, no AppHandle
required). CLI --pill still takes precedence when explicitly passed.
3. Tauri commands: get_launch_as_widget / set_launch_as_widget for the
Phase 2 Settings UI to bind a checkbox to.
4. Scripts: bun desktop-prod:pill / desktop-prod:run:pill — forward --pill
to the bundled app launch. macOS uses `open -n --args` to spawn fresh
instance with the flag.
Closes the GUI half of INST-13. Phase 2 closes the Settings UI half.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(dictation): show widget unconditionally on pill-mode launch + visible Suspense fallback
Before: pill mode set up correctly but the widget window stayed hidden
until ⌘⇧Space was pressed. New users saw absolutely nothing on launch
(no main window, no dock icon, hidden widget) and assumed the app
failed. If global-shortcut Accessibility permission wasn't granted,
they had no path to discover the widget at all.
Two changes:
1. lib.rs: in pill_mode_setup, explicitly show + position + focus the
widget window after hiding main. With per-call error logging so we
can diagnose failures (and a clear error log if widget window
wasn't created at all — points at tauri.conf.json regression).
2. main-app.jsx: Suspense fallback was `null`, which combined with
widget's transparent+decorations:false config made any lazy-import
delay or failure invisible. Now renders a dark pill saying
"Loading dictation…" so even if CaptureWidget lazy-import stalls,
the user sees the window exists.
Studio mode behavior unchanged — widget stays hidden until hotkey
or tray click triggers it (existing show() call in the shortcut/
menu handlers is preserved).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(dictation): create widget window programmatically; Tauri 2 silently dropped config-array creation
Root cause: declaring the widget window in tauri.conf.json's app.windows[]
silently failed in Tauri 2 — get_webview_window("widget") returned None
even though the config was syntactically valid. Probable culprit was the
transparent + decorations:false + visible:false combo, but Tauri offered
no error message either at startup or via webview_windows() enumeration.
Diagnosed by adding webview_windows() enumeration logging at setup start
(only ["main"] ever appeared) and a programmatic WebviewWindowBuilder
fallback that surfaces real Result errors.
Fix:
- tauri.conf.json: widget entry now has `create: false` to make the
config-vs-programmatic handoff explicit.
- lib.rs setup(): call WebviewWindowBuilder::new(app, "widget", ...).build()
with the exact same surface attributes the config used to declare.
- capabilities/default.json: include "widget" in windows array so the new
window inherits the same Tauri permissions as main.
- tauri.conf.json: remove the invalid `"url": "/?window=widget"` field —
WebviewUrl::App takes a path only, query strings aren't supported.
Both windows now load index.html.
- main-app.jsx: replace URL-query-based widget detection with
getCurrentWindow().label === 'widget' via @tauri-apps/api/window. This
is the Tauri 2-recommended pattern for multi-window apps and works
regardless of URL routing.
Closes the immediate UX bug behind the dictation widget being invisible.
Builds cleanly + manually verified: pill widget visible on screen at
top-center after `bun desktop-prod:pill`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- tqdm hook emits progress every 0.3s with backend rate (bytes/sec)
- Frontend uses backend rate for instant speed display, no 2s warmup
- Shows 'Connecting to HuggingFace…' during connect phase
- Shows 'measuring speed…' before rate is available
- Re-check button moved to top-right header in system preflight
- Retry + Clean & Retry buttons on failed splash screen
- Smart error hints (missing README, network timeout, port in use)
- README.md + omnivoice/ source package copied during bootstrap
- desktop-prod.sh wipes HF cache + all app data for fresh testing
- Implement donate page and migrate API fetching to react-query hooks
- Add setup wizard for batch job management and voice clip editing
- Refactor setup router into package (wizard, models, download sub-modules)
- Fix 9 CI test failures from setup router refactor
- Fix cross-device link error in prefs.py atomic writes
- Fix event loop mismatch in export test fixtures
- Modernize README with architecture diagram and 13 app screenshots
- Defer per-segment disk writes in dub_generate for ~6s faster dubs
- Extract 45 inline styles from Launchpad, KeyboardCheatsheet, DubSegmentRow
- Add playwright dev dep and screenshot capture script
Updated the dev:api script to use `uv run` instead of a hardcoded virtual environment path.
Changes:
- replaced `.venv/bin/uvicorn` with `uv run uvicorn`
Reason:
The previous implementation relied on a POSIX-specific path, which breaks on Windows
(where executables are located in `.venv/Scripts`). Using `uv run` ensures the command
works consistently across different operating systems by resolving the environment automatically.