d91beef0fd314250d8d9b94de86dfea019a8bd96
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
41c098e009 |
feat(demos): ship the demo audio and video the app already advertises (#1517)
* feat(demos): ship the demo audio and video the app already advertises Every demo asset in the app was a dead link on anything but a Mac. `personalities.py` has carried a `preview_url` for each of the seven voice-design presets since they were added; DictationDemo.jsx posts three bundled WAVs to /transcribe so the feature can be shown without microphone permission; the Dub workspace reads a manifest and plays a source video plus four dubbed languages. None of those files were committed, because the tooling that renders them (scripts/build_demos.sh, scripts/build_dub_demo.sh) hard- requires macOS `say` — it even carries a `TODO: add espeak-ng path for Linux contributors`. So the presets returned 404, the replay buttons did nothing, and the dubbing demo never loaded. Rendered with VoiceStudio's own engine, which runs wherever the app does: - 7 voice-design previews (2.2 MB) - 3 dictation replay clips (1.1 MB) — verified by transcribing them back: the conversational and French clips round-trip exactly - dubbing demo: source + 4 dubbed videos with subtitles and manifest (9.6 MB) Tooling fixes this turned up: - build_dub_demo.sh wrote to backend/assets/demo/dubbing, but main.py mounts backend/assets/samples at /demo_audio — so the frontend's /demo_audio/demo/dubbing/manifest.json could never have resolved even after a successful Mac build. Output moved under the mount. - `say` is now the fallback rather than the requirement: the new scripts/render_dub_demo_audio.py renders the five tracks with the engine and the shell script picks them up. - The five demo paragraphs lived in two files. They are now one JSON both read — two copies is one edit away from a video whose subtitles disagree with it. - render_demos_omnivoice.py peak-normalized, which a single-sample transient defeats: the Helpdesk preset landed at -30 dB RMS against -17 dB for its neighbours, so the preview row played at wildly different volumes. Now EBU R128 at -18 LUFS with a -1.5 dBTP ceiling. - …and pinning the output rate, because loudnorm resamples to 192 kHz internally and writes there unless told otherwise, which turned 2.1 MB of previews into 17.5 MB of identical-sounding audio. - update_manifest() looked for a manifest at a path nothing writes, so it always printed "not found" and did nothing. - Dictation is rendered here now too. It was excluded on the grounds that `say` was good enough and engine TTS was overkill — true only on macOS. tests/test_demo_assets_exist.py resolves every advertised URL against the directory main.py actually mounts, and checks each dubbing subtitle matches the script its manifest entry claims. A missing static file is not an import error and not a failing request; nothing would have caught this otherwise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(changelog): stamp the demo-asset entries with their PR ref Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(demos): watermark rendered demo audio, and harden the render scripts Review findings on #1517: - Greptile P1: the renderers wrote engine output straight to disk, so a re-render shipped demo audio with no provenance mark. These clips play back to users as VoiceStudio output — they are synthetic audio leaving the app like any other, and now go through mark_synthetic (#1169), the one chokepoint every producing route uses. It runs on the file AFTER loudnorm, since loudnorm re-encodes what it is handed, and says so loudly when marking is unavailable rather than committing an unmarked asset. The dubbing renderer shares the same helper. - CodeRabbit: build_dub_demo.sh checked only source.src.wav before deciding it could run without macOS `say`, so a Linux or Windows run with four of five tracks present reached a missing one, called `say`, and left a half-built bundle. It now requires all five. - CodeRabbit: shutil.move over an existing path delegates to os.rename, which raises FileExistsError on Windows — os.replace overwrites atomically everywhere. - CodeRabbit: the preview test discovered presets in a parametrize argument, importing app code at collection time and leaving core.personalities in sys.modules for later tests. Discovery moved into the test body. CI: the rendered dub bundle's zh/ja subtitles, its manifest and the script source are dubbing CONTENT, not UI strings — allowlisted in test_no_hardcoded_cjk.py with that justification. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(demos): a render that cannot be watermarked fails instead of warning CodeRabbit and Greptile, #1517: mark_synthetic degrades rather than raising — correct for generation, wrong for a render script, whose whole job is to produce files a human then commits. A printed warning on a scrolling console is not a gate, so both scripts exited 0 with unmarked assets sitting on disk ready to commit. They now raise, with the reason and the fix; OMNIVOICE_DEMO_ALLOW_UNMARKED=1 stays for a local listen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: stop a flaky dependency fetch from failing green runs en-core-web-sm resolves to a direct GitHub release URL, and github.com intermittently answers `http2 error: refused stream before processing any application logic`. uv's own three retries all land within the same few seconds and fail together, so the whole job dies on a dependency that has nothing to do with the change under test — it cost #1518 and #1517 an otherwise-green run tonight. Two changes: back off between whole `uv sync` attempts, which is what actually clears it, and pass --no-sync to the pytest steps. `uv run` re-resolves the environment before running, so every test step was a fresh chance to hit the same fetch even though the install step had already synced — that is exactly how #1518 failed, in the isolated backend/tests step, with all 5467 tests already passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: one retry seam for every uv sync, not just the job that failed last en-core-web-sm resolves to a direct GitHub *release* URL rather than a package index, and github.com intermittently answers `http2 error: refused stream before processing any application logic`. uv's own retries all land inside the same ~10 seconds and fail together, so a job dies on a dependency unrelated to the change under test. Tonight that cost four otherwise-green runs across #1515, #1517 and #1518 — and the first fix only covered the Tests job, so the next failure simply moved to Smoke (Linux), which syncs separately. The fetch is per-job, so the fix has to be per-job: scripts/uv-sync-retry.sh backs off between whole attempts (15s, 45s, 90s) and every workflow that syncs now goes through it — ci.yml (tests + the platform matrix), release.yml, security.yml, evals.yml. It still fails loudly after four attempts, so a genuinely broken lockfile is not disguised as a flake. The Tests job also lacked the UV_HTTP_TIMEOUT / UV_HTTP_RETRIES the smoke matrix has always set, which is part of why it was the one that kept dying; it has them now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(ci): pin the Intel-Mac contract by intent, not by command spelling test_ci_verifies_intel_mac_as_the_documented_remote_only_host asserted the literal line `run: uv sync --extra pockettts`, so routing every sync through scripts/uv-sync-retry.sh read as a broken Intel-Mac contract. The contract it exists to protect is that the pockettts extra installs ONLY on backend_supported legs — which the regex now pins, while leaving how the sync is invoked free to change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: keep every uv run out of the resolver, and bound the retry budget CodeRabbit, #1517: - `uv run` re-resolves before running, so the smoke suite, the worker-artifact tests, the release test run and the eval run were each a fresh chance to hit the flaky direct-URL fetch outside the retry loop. All of them pass --no-sync now; the environment is already synced by the step that owns the retries. security.yml's `uv run --with pip-audit` is deliberately left alone — it layers an ephemeral package rather than running the project's own tests. - The retry count multiplied uv's own budget (UV_HTTP_RETRIES=5 with a 120 s timeout on the smoke matrix). Three attempts and 60 s of total backoff outlast the refusals actually observed while staying well inside the jobs' timeout-minutes. - The Intel-Mac contract test pinned the smoke command literally too, so --no-sync tripped it exactly like the sync line did. Same fix: assert the contract (smoke runs only on backend_supported legs), not its spelling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a310141114 |
feat(workers): join from the app, share by QR, and a status-bar Compute control (#1516)
* feat(workers): join from the app, share by QR, and a status-bar Compute control Remote workers shipped with a hole in the middle: the control plane could mint join codes, and on the other machine there was nothing to paste them into. Becoming a worker meant launching with OMNIVOICE_WORKER_MODE and OMNIVOICE_WORKER_TOKEN in the environment and relaunching — on the machine that is usually the least convenient one to configure by hand. Backend - GET /workers/agent, POST /workers/agent/join, POST /workers/agent/enabled. Join redeems a code and starts the agent live; no restart. - Worker mode now persists in settings as well as the environment (env still wins, and the panel is told so it can disable a switch it cannot honour), and it is written only after a join that actually worked — a failed enrolment must not have the app retrying on every launch. - The endpoint carried by the redeemed code is remembered. Without that a machine that joined from the UI came back up enrolled but with nowhere to dial, and the only fix was OMNIVOICE_WORKER_ENDPOINT. UI - "Lend this machine's GPU": paste the code, Join. Once joined it offers a switch rather than another code, because the pinned certificate survives. - <OneTimeSecret/> renders join codes and connection strings as a QR next to the text, with a live expiry countdown, and is used by both halves. QR generation is best-effort: a string past the format's capacity still shows the code and Copy, because losing the QR is a degraded share and losing the only copy of a one-time secret is data loss. - Status-bar Compute control: pick local or a machine, flip the feature, mint a join code — without opening Settings. Absent entirely until the user has opted in or enrolled something. - Remote workers now reads as a device list: status dot, address, latency, live task meter, resident models, last seen; housekeeping actions revealed on hover; a three-step empty state. - Approve is on the row. A worker could connect, sit there labelled "Not approved" and never be usable, with no way out of it in the UI. Fixes found on the way - Status dots and menu surfaces in the GPU picker were painted from fixed Tailwind palette classes (bg-emerald-400, text-amber-400, hover:bg-white/5), so on Midnight or Catppuccin they showed Gruvbox colours next to the theme's own. Both controls now paint from themed --color-* tokens, shared in computeTarget.jsx along with the JSON wrapper all three copies duplicated. - Button funnels every child into one <span>, so an icon passed as a child renders glued to its label — the flex gap only applies to the `leading` slot. Six buttons across these panels were affected. - InboundNodePanel passed `variant="warning"` to Badge, which takes `tone`; the "on your network" warning rendered as an ordinary neutral pill. Docs updated in the same change (docs/remote-workers.md): the join flow, the QR, the status-bar control, and the new environment variable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(changelog): stamp the remote-workers entries with their PR ref Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(workers): a join is not done until the control plane accepts it Review findings on #1516: - Greptile P1: `start()` only SCHEDULES the dial-out loop, so a control plane that rejected this worker — expired token, wrong address, a server that never answers — looked identical to a successful join. The route persisted worker mode, reported success, and the machine retried forever on every launch. The agent now signals first registration, and join waits for it before persisting anything. - CodeRabbit: a failed REJOIN left the machine unable to reconnect to the control plane it was already serving, because pinning the new certificate overwrites the old one on disk. Snapshot the pinned certificate, endpoint and setting up front, and restore them (and the running agent) when the join fails. - CodeRabbit: join and the enable toggle awaited stop()/start() with no exclusion, so two concurrent requests could interleave their pairs and have `start()` return early — reporting success for a control plane it never dialled. Both now hold one lifecycle lock. - CodeRabbit: with OMNIVOICE_WORKER_MODE set, the toggle still started or stopped the agent and wrote a setting the rest of the app ignores, contradicting the env_pinned status it reports. It now answers 409 and says which variable is in charge. - CodeRabbit: the QR code kept encoding the previous secret until the new one finished encoding, so the code on screen could disagree with the text beside it. CI: regenerated tests/fixtures/api_routes.txt for the three /workers/agent routes. Tests: a join the control plane never accepts is a 409 that persists nothing and leaves no agent dialling; a failed rejoin restores the previous certificate, endpoint and setting; an env-pinned machine refuses the toggle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(workers): the environment pin governs joining too, not just the toggle CodeRabbit, #1516: - join_control_plane skipped the env_pinned guard set_agent_enabled enforces, and joining is precisely what ENABLES worker mode: under OMNIVOICE_WORKER_MODE it wrote a setting nothing consults, and with the variable pinned off it handed back a machine that reported a successful join and lent nothing. One shared guard now covers both routes. - Two of the three rollback assertions could not fail before the fix (nothing wrote those settings on the failure path). The test now pins the behaviour only the rollback produces: the previous enrollment is dialling again, rather than left stopped until someone notices. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5832a81bb6 | feat(onboarding): refresh the bundled demo voice | ||
|
|
a28a19dc63 |
fix(onboarding): commit + bundle the demo voice clip (#621) (#633)
backend/assets/samples/demo_voice.wav is a build artifact (generated by scripts/build_demos.sh) that was never committed, so it shipped absent from installs: onboarding logged 'Demo audio not found', seeded nothing, and the Launchpad was empty on first run + the /demo_audio route was unavailable. The file is already un-ignored in .gitignore and bundled via the Tauri 'backend' resource — it just needed to exist in git. Commit it (regenerated via the script's say/Samantha path, 24kHz mono 16-bit, content matching DEMO_REF_TEXT) so first-run works on every platform. Onboarding keeps its graceful skip (now with a regenerate hint) for a partial checkout. Regression test guards the asset is present + valid and that onboarding seeds the demo profile from it (and is a no-op on a non-empty DB). No version bump. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8b00dc1f4f |
feat: onboarding demos, opt-in bug reporting, error-docs deeplinks + issue triage (#133)
* 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> |
||
|
|
c77bf18ac4 |
feat: onboarding demo profile, voice personalities, i18n framework
- Onboarding: seed 'OmniVoice Demo' profile on first run (empty DB) with bundled reference audio so Launchpad isn't empty - Voice Personalities: 6 built-in presets (Narrator, Casual, News Anchor, Storyteller, Corporate, Energetic) with instruct text auto-fill in Voice Design mode - i18n: react-i18next with English locale, browser language detection, Launchpad & CloneDesignTab strings extracted to en.json - DB migration v4: personality TEXT column on voice_profiles - New API: GET /personalities returns preset list - CSS: demo callout banner + personality picker strip |