b1ffdf2387c3037c2515fb4d8a05aedaa0c83c65
371
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b1ffdf2387 |
fix(mlx): harden import guards against PyInstaller dylib failures (Wave 4.4) (#390)
MLXWhisperBackend / MLXAudioBackend is_available() caught only ImportError. In a PyInstaller bundle mlx's native dylib/metallib can fail to load even when the package imports, raising OSError/RuntimeError — which would propagate and crash the registry scan instead of reporting the backend unavailable. Broaden to (ImportError, OSError, RuntimeError) so the picker falls back cleanly. 6 tests across all three exception types. The capture ASR path already prefers MLX Turbo on Apple Silicon (get_capture_asr_backend), so this hardening is the remaining slice of Spec 6 / Wave 4.4. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
5361264d12 |
fix(history): real 2-line title clamp + de-noised display + click-to-expand (#389)
The old max-height:3em guillotined the third line mid-glyph. Now a true -webkit-line-clamp with ellipsis, leading [tag] control tokens stripped from the display (full text stays in the tooltip and restore flows), and clicking the title toggles the full prompt. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b60cceb3e6 |
docs(engines): uv dedupe + sidecar torch-pin disk-usage policy (Wave 4.5) (#392)
Explain why dedicated-venv engines (IndexTTS2) add disk (a second torch + CUDA libs: Linux cu128 ~0.83 GiB, Windows ~3.2 GiB), and how uv's link-mode dedup (clone on macOS/Linux, hardlink on Windows) shares identical wheels for free — provided UV_CACHE_DIR and the venvs are on the same filesystem. Key policy: pin the same torch build as the parent whenever the engine allows, since only identical wheels dedupe; UV_LINK_MODE=hardlink on Linux ext4. Linked from the IndexTTS engine doc. Spec §R4(a) / parity program Wave 4.5. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6306f6edae |
docs: official Docker Hub image palashdeb/omnivoice-studio (#388)
Link the published Docker Hub repo (https://hub.docker.com/r/palashdeb/ omnivoice-studio) as an official image alongside GHCR in the README install list and docker.md header. Same images, same tags. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
3957d333b9 |
perf(dub): retime batches seek to their window instead of decoding from frame 0 (#387)
Each batch now uses input seeking (-ss before -i, frame-accurate under re-encode) plus a bounded read (-t window+0.5s), with chunk times shifted into window-relative coordinates — long-video Smart Fit exports drop from O(n²) decode cost to O(n). Fixes #382 Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a93abcfc5e |
chore(i18n): backfill 36 studio-overhaul keys into en.json + all 20 locales (#386)
Canonical English added from the t() defaultValues introduced by the overhaul PRs (#374-#381); 20 parallel translation passes added each key to every locale (placeholders, product names, and existing per-locale terminology preserved). All locale files parse; CJK gate + 312 frontend tests green. Fixes #383 Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3161166328 |
fix: stale-chunk preload recovery (#380) + surface unsupported-GPU-arch in notifications (#284) (#385)
- #380: vite:preloadError (old hashed assets after an update) triggers a one-time reload to pick up the fresh manifest; session flag prevents loops - #284: check_device_compatibility's warning (e.g. Blackwell sm_120 on a pre-cu128 torch) now appears in the notification panel as an error with the pip fix — a log line never reached affected users while synthesis silently produced noise. Cached once per process. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
77d6477fc9 |
fix(player): WaveformPlayer paused itself on play — idempotent claim + hard listener teardown (#384)
Live-debugged in Playwright WebKit with a pause() stack hook: the media 'play' event fired twice (a stale WaveSurfer instance's listeners survive a destroy() that throws mid-teardown under StrictMode double-mount), so the second claimPlayback stopped the current owner — this very element. play → instant self-pause → 'click does nothing'. - 'play' handler only claims when it doesn't already own the slot - per-instance stale flag inert-izes leaked handlers - cleanup detaches handlers (unAll) BEFORE destroy so a throwing destroy can't leak them Verified in WebKit: paused=false, currentTime advancing, 0 stray pause calls. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f5579b40aa |
fix: issue-triage batch — timeline box flicker, truncated-model detection, stale history pruning (#381)
- #373: drop will-change:transform on the segment lane (persistent compositor layer made the semi-transparent boxes vanish during playback/drag on some Windows GPUs) + raise region alpha 0.30→0.45 - #352: validate a finished snapshot actually contains weights (>5 MB file) so interrupted downloads fail at install time with a re-download hint; loader translates the opaque transformers error into the same guidance - GET /history prunes rows whose audio file is gone instead of serving dead 404 players forever Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
171febfd59 |
fix(player): WaveformPlayer click did nothing — media element never got a src (#379)
With an external `media`, wavesurfer's `url` option only fetches for peak decoding and never assigns the element's src — so the waveform drew but play() had nothing to play. Set src on the in-DOM <audio> via JSX (same pattern as WaveformTimeline) and stop passing `url`. Also surface playPause() rejections instead of swallowing them. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
35de7d03b4 |
feat(studio): consolidate Clone + Design into one Voice workspace (spec P4) (#378)
One 'studio' navigation mode replaces the clone/design pair; the split
lives on as a 'Define voice' toggle (From audio / By design) at the top
of Voice Source. Selecting a saved profile sets the method from its kind.
- uiSlice: AppMode + 'studio'; defineMethod ('audio'|'design') persisted
- legacy shims: localStorage mode + restoreHistory map clone/design →
studio + method; history mode VALUES unchanged
- NavRail/Header: single Voice entry (Fingerprint, #d3869b)
- CloneDesignTab/WorkspaceVoices/useTTS/useProfiles/Gallery/Launchpad/
Sidebar: definition-method semantics moved off the navigation mode
Build clean; 312/312 tests; tsc clean; no setMode('clone'|'design') left.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
6140f888e1 |
fix(dub+win): dialect↔cinematic guidance loop + WinError 193 ffmpeg validation (#377)
* fix(dub): break the dialect↔cinematic guidance loop (#372, #373) - Cinematic toggle refuses the pick when no LLM endpoint is configured, pointing at Settings → Credentials → LLM endpoint - backend Fast fallback now syncs the quality toggle to 'fast' - the dialect warning no longer fires alongside the cinematic-no-LLM warning (the pair formed the loop), and both messages point at the LLM endpoint settings instead of each other Fixes #372 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ffmpeg): validate resolved ffmpeg/ffprobe actually runs — fall through on WinError 193 (#360, #361, #362) A corrupt or wrong-arch imageio-ffmpeg download (and WindowsApps alias stubs) passes os.path.isfile/shutil.which but explodes at spawn with '[WinError 193] %1 is not a valid Win32 application', killing transcription with an opaque 500. Every resolution step now probes the candidate with '-version' (cached per process), logs the rejected basename, and falls through to the next source. Fixes #362 Fixes #361 Fixes #360 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
66f2ea7e50 |
feat(profiles): unified profile model — kind discriminator + stored design params (spec P3) (#376)
Migration 0005_unified_profiles (0004 taken by mcp bindings):
- voice_profiles.kind TEXT DEFAULT 'clone' ('clone' | 'design'), backfilled
- voice_profiles.vd_states TEXT NULL — JSON of design category picks
- mirrored in _BASE_SCHEMA; idempotent _has_column guards; downgrade drops
POST /profiles:
- ref_audio now optional; kind + vd_states form fields with validation
(clone requires audio; design requires vd_states JSON object + instruct)
- design profiles render a deterministic identity sample (seed 42) through
the shared archetype renderer — one TTS code path
POST /generate:
- profile resolution branches on profile.kind (authoritative) instead of
the brittle is_locked/instruct inference; legacy pre-0005 rows keep the
old inference as fallback; history.mode records profile.kind
Frontend:
- 'Save design as profile' in the Design tab (vd_states + buildDesignInstruct)
- selecting a design profile restores its sliders (vd_states) for re-editing
Also unforks the alembic chain (0004_mcp + my 0004 both revised 0003 →
multiple heads broke alembic upgrade head and the 0003 migration tests).
Tests: tests/test_profile_unification.py — validation, design-create with
mocked renderer, migration up/backfill/downgrade. 18/18 profile tests,
312/312 frontend, related backend suite green.
Note: docs/specs/voice-studio-unification.md (on feat/studio-ux-overhaul)
still says 0004 — renumber to 0005 when branches meet.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
851ca4e012 |
feat(studio): workspace UX overhaul — right-side panels, shared waveform player, dub pipeline UX, setup polish (#374)
* 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> |
||
|
|
1561ff4428 |
ci(docker): also publish to Docker Hub palashdeb/omnivoice-studio (#375)
Push the same images (same tag set: :latest rolling main, :stable/:X.Y.Z releases, :sha-) to docker.io/palashdeb/omnivoice-studio alongside GHCR. Gated on DOCKERHUB_USERNAME/DOCKERHUB_TOKEN secrets — without them the build still publishes to GHCR only. Docs-sync: docker.md mirror note. Requires repo secrets: DOCKERHUB_USERNAME, DOCKERHUB_TOKEN. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d6562d6f30 |
feat(dub): Smart Fit phase B — per-segment video retime export, drift absorption, fitted subtitles (#350)
* feat(dub): Smart Fit phase B — per-segment video retime export, drift absorption, fitted subtitles Executes the video side of the Smart Fit plans persisted by Phase A (job["fit_plans"], #347) at export and preview time. Backend: - services/video_retime.py (new, clean-room): two-tier retime executor. ≤48 chunks → the proven single-pass split/trim/setpts/concat filter_complex; above → batches of 40 chunks rendered to intermediate slices (identical libx264 medium/crf20 params, keyframe at t=0) joined losslessly with the concat demuxer. Slices are CFR-resampled (fps=) because setpts leaves VFR-ish timestamps that broke tpad and drifted a frame per retimed chunk on ffmpeg 7.x. Temp slices cleaned on success AND failure/abort. - Drift absorption: fitted track longer than retimed video → freeze-frame tail (tpad=stop_mode=clone) predicted into the last slice / single-pass graph, with residual mux-side tpad; video longer → silence-pad the dub audio chain (apad=whole_dur). ±50 ms tolerance. - VFR guard: probe r_frame_rate vs avg_frame_rate; normalise with fps= before trim/setpts; probe failure degrades gracefully. - Plan resolution: _video_retime_plan_for spans legacy video_stretch_plans (byte-identical resolution + command construction) and fit_plans, gated on the track's own timing_strategy so stale plans never retime a track re-generated under another strategy. - Fitted subtitles: /dub/srt + /dub/vtt accept ?lang= and serve cue times from fitted_segments for Smart Fit tracks; _write_burn_srt does the same for burn-in. burn_subs+retime is now allowed for smart_fit (burn runs AFTER the retime graph); still rejected for legacy stretch_video. - /dub/preview-video resolves the same plan so in-app preview matches export. - Fallback ladder: batch encode failure/timeouts → un-retimed export with a structured core.failure warning (X-Dub-Export-Warning header + job["last_export_warning"]); concat join rejection → one single-pass retry while ≤96 chunks; abort → 409 + proc kill via run_ffmpeg job_id registration (/dub/abort reaches export encodes now) + temp cleanup. Frontend: - Export drawer passes ?lang= on subtitle exports and shows an i18n'd re-encode cost note (~0.5–2× video length on CPU) when a retiming strategy is active — translated in all 21 locales. Tests: tests/test_smart_fit_export.py — plan resolution, batch math, graph parity + new stages, fitted-cue SRT/VTT/burn selection, burn policy, VFR detection; ffmpeg-gated integration renders both executor tiers (batch size forced to 2) and the real /dub/download endpoint, ffprobing durations within ±50 ms across both pad branches. All existing dub export/subtitle/preview/timing tests pass unchanged. Refs docs/competitive-analysis.md Action 1 (dub-length fitting v2); completes Smart Fit (Phase A = #347). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): sanitize Smart Fit retime work paths at every sink (CodeQL py/path-injection) The job_id-derived retime work path (retimed_*.mp4 / preview_retimed_*.tmp.mp4) flowed unguarded from dub_export into prepare_smart_fit_video / render_retimed_video and their derived slice/concat paths and ffmpeg argv. Apply the repo's proven inline realpath+startswith containment pattern (helpers/commonpath are not recognized — see #309/#328/#329/#348): - dub_export.py: validate work_path against DUB_DIR at both construction sites (export + preview) and pass the validated realpath onward. - video_retime.py: make both entry points self-defending — realpath + DUB_DIR containment on out_path/work_path before any derivation, raising RetimeError(stage="plan") on escape; slices_dir/slice_path/list_path and RetimeDecision.file_path now all derive from the sanitized value. DUB_DIR is read via module attribute so test fixtures reloading core.config work. - ffmpeg_utils.py: document that all caller-assembled argv paths are realpath-validated upstream. - tests: sandbox DUB_DIR in the executor integration tests (tmp_path) so the new guard sees the test workspace. No behavior change for valid (server-built) paths — the guard only fires on traversal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(smart-fit): patch DUB_DIR on video_retime's own config ref — survives suite-wide reload The retime guard reads video_retime._config.DUB_DIR at call time; the sandbox fixture patched a fresh 'import core.config' instead. Another test reloads core.config in the full suite, so the two module refs diverged — the patch missed and the guard rejected the test's tmp paths (green in isolation, red in CI's full run). Patch the exact ref the guard dereferences. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dub): resolve DUB_DIR live at call time in retime guards — survive full-suite reload The path-containment guards bound DUB_DIR via a module-level 'from core import config as _config'. Other tests importlib.reload() core.config (sandboxing OMNIVOICE_DATA_DIR), after which the guard checked containment against a stale DUB_DIR while dub_export built the path under the reloaded one — every retime path then 'escaped the dub workspace' (green file-alone, red full-suite: the 5 integration failures CI hit). Re-import DUB_DIR locally in each guard so it always reads the current sys.modules value; simplify the sandbox fixture to patch the canonical module. Verified: full backend suite green on the Smart Fit tests (the 2 remaining settings_store failures are pre-existing on main, unrelated — local data-dir artifact). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): clear CodeQL alerts on Smart Fit export — job_id allowlist, proc-registry decouple - py/path-injection (8, video_retime.py): validate job_id with a strict inline regex allowlist (re.fullmatch [A-Za-z0-9_-]{1,64}) at the entry of dub_download and dub_preview_video, before it reaches any filesystem path or ffmpeg argv. The existing realpath containment guards stay as defense-in-depth; the regex barrier is the sanitizer CodeQL recognizes through the service-module call chain. - py/log-injection (4): newline-strip job_id inline at the logger calls in ffmpeg_utils.run_ffmpeg and the two retime-fallback logger.error sites in dub_export. - py/empty-except (3): best-effort cleanup os.remove handlers now log the OSError at debug instead of bare pass (video_retime + both dub_export mux finally blocks; _discard_tmp too for consistency). - py/cyclic-import (2): break the dub_pipeline <-> ffmpeg_utils cycle for real — the subprocess registry (register_proc/unregister_proc/ kill_job_procs/has_active_procs + state) moves to a new stdlib-only leaf module services/proc_registry.py. ffmpeg_utils now imports it at module top (no lazy import); dub_pipeline re-exports every name so dub_core aliases and tests keep working unchanged. No behavior change for valid inputs; invalid job ids now get a clean 400 instead of a 404/containment error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dub): address #350 review — cancelled-vs-failed retime, logged best-effort excepts, redacted probe logs, narrowed test assert - rc<0 (killed by user cancel) now raises RetimeError(stage='aborted') instead of reporting an ordinary render failure (CodeRabbit) - best-effort cleanup/QC-event excepts log at debug instead of bare pass (CodeQL empty-except x3) - probe failure logs use basename, not full user paths (CodeRabbit/CodeQL) - test_render_cleans_slices_on_failure asserts RetimeError, not Exception Rebuttals (no change needed, see PR comment): fitted-cue subtitles track the fitted AUDIO timeline which is correct even on retime fallback; the planner only emits stretch ratios >1 so the early-exit guard is a true no-op check; '\'' is ffmpeg's own utility quoting for concat lists; has_active_procs is an intentional re-export (noqa'd). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
825f4f7ac6 |
feat(dub): regenerate subtitle timeline on the fitted timeline (Wave 3.1) (#371)
Smart Fit Phase A (planner) + the export-side video retime + audio stretch already shipped (#347 + dub_export stretch filter). The last piece of Spec 1 was the subtitle timeline: under stretch_video the dubbed audio plays at FITTED positions, but the standalone SRT/VTT export still used the original segment times — so external subtitles drifted against the dubbed video. - services/fitted_subtitles.py (pure, tested): map_time_to_fitted() + fitted_cues() remap original cue times onto the same per-chunk {orig→new, stretch_ratio} plan the video stretch uses, with a monotonicity guard. - dub_export SRT + VTT endpoints: when a job used stretch_video, cues are regenerated from the plan (subtitles track actual dub placement); no plan → original times, unchanged. New optional ?lang= selects the track. 7 pure tests (chunk-bound mapping, linear interpolation, unit-rate tail, fitted cues, monotonicity, empty-plan identity). Spec 1 (remaining) / parity program Wave 3.1. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a12492af07 |
feat(dub): second-pass ASR QC — flag lines whose dub drifts from target (Wave 3.3) (#370)
After a dub is generated, re-recognize the synthetic audio and compare what
the ASR heard against what we asked the TTS to say. Lines that drift are
flagged for the user to re-listen / re-dub — turning subtitle timing and
pronunciation from trusted math into measured truth, and doubling as an
automatic dub-quality check.
Design delta from pyvideotrans (which lets recognized text REPLACE the
subtitles wholesale): we keep the generated text authoritative and use the
second pass only for MEASUREMENT — a per-line drift score + measured
start/end that feed the incremental re-dub loop, never silently overwriting
the translation.
- services/dub_qc.py (pure, tested): word_error_rate (normalized token edit
distance, case/punct-insensitive, script-agnostic) + score_dub (matches
recognized segments to dub segments by time overlap, concatenates the
hypothesis, scores drift, derives measured bounds).
- POST /dub/qc/{job_id}: runs the active ASR backend on the dubbed track in
the GPU pool, annotates each segment with qc_drift/qc_flagged/
qc_recognized/qc_measured_start-end (non-destructive — content untouched),
persists, emits a qc_done job event. Opt-in, never fatal.
- Frontend: dubQc() API fn + a red 'Verify' badge on flagged segment rows
(en.json keys; other locales fall back).
12 pure scoring tests (identical/substitution/empty/no-overlap/multi-segment
matching/measured-timing); endpoint validated in CI.
Spec 5 / parity program Wave 3.3.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
8cce99298a |
feat(dub): per-segment clone references (Wave 3.2) (#369)
Cut each long-enough dub segment's clone reference from the isolated vocals
at that segment's own timestamps, so the dub of each line carries the
prosody/emotion of its source line — finer than one reference per speaker.
Reimplemented from the clean-room spec (pyvideotrans per-line ref idea); our
design delta is a quality floor with fallback.
- services/speaker_clone.py: extract_segment_refs() keyed by segment id;
reference transcript is the SOURCE text (text_original), since the vocals
slice is source-language audio. Floor at MIN_SEGMENT_REF_DURATION_S=3.0
(not the per-speaker 5.0, which most dialogue lines fall under) — shorter
lines are omitted and fall back to the per-speaker clone, so it's a strict
improvement, never a regression.
- dub_core: run extraction at transcribe (per_segment_refs query param,
default on), store job['segment_clones'], default each unassigned
segment's profile_id to 'auto-seg:{id}' when it has its own ref, else the
existing 'auto:{speaker}'. Forcing per-speaker (per_segment_refs=false)
is supported for long-form consistency.
- dub_generate _gen: resolve 'auto-seg:' from segment_clones, ahead of the
per-speaker 'auto:' path. profile_id is already a fingerprint field, so
flipping the mode re-dubs automatically (no _GEN_INPUT_FIELDS change).
7 pure tests over a synthetic vocals wav (own-ref for long lines,
short-line omission/fallback, source-text transcript, bounds clamping,
floor boundary). Pipeline wiring validated in CI.
Spec 4 / parity program Wave 3.2.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
99357e8c5b |
feat(mcp): MCP server v1 — mount on /mcp, per-agent voice binding, stdio shim (Wave 2.2) (#368)
* feat(mcp): MCP server v1 — mount on /mcp, per-agent voice binding, stdio shim (Wave 2.2) The FastMCP server (previously dead code, never mounted) is now mounted on the main FastAPI app at /mcp via Streamable HTTP, with its session manager composed into the app lifespan through an AsyncExitStack (best-effort: a missing mcp package or OMNIVOICE_MCP_DISABLE=1 never breaks startup). streamable_http_path set to '/' so the sub-mount lands at /mcp, not /mcp/mcp. Adds the 'mcp' dependency (1.27.x). Per-agent voice binding (Spec 2 headline): each MCP client sends an X-OmniVoice-Client-Id header; generate_speech resolves the voice as explicit arg > the client's binding > global default > app default. New mcp_client_bindings table (alembic 0004 + _BASE_SCHEMA, additive/idempotent), services/mcp_bindings.py (CRUD + resolve_voice + best-effort last_seen), and a loopback-gated REST router (/api/mcp/bindings) the Settings panel drives. New transcribe tool (base64 audio in, 200 MB cap). Stdio shim (backend/mcp_shim, httpx-only, ported from voicebox MIT) proxies stdio clients to the mounted endpoint and forwards OMNIVOICE_CLIENT_ID as the binding header. Settings → Sharing gains an MCP bindings panel. Docs: docs/mcp.md (both connection modes + binding REST) and docs/mcp.json updated to the shim form. Tests: bindings service + resolution precedence + migration up/down (pure, run locally); REST CRUD + mount-not-404 + disable-flag (main-importing, validated in CI). MCP build + mount + initialize handshake verified out-of-band (no torch). Spec: docs/competitive-analysis.md Spec 2 / parity program Wave 2.2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(mcp): assert /mcp mount via app.routes, not a lifespan client The two main-importing mount tests ran the app lifespan, which now starts the FastMCP session manager and binds asyncio queues to the test loop — contaminating later lifespan-running tests ('bound to a different event loop'). The mount happens at import time, so inspecting app.routes for the /mcp Mount is the correct loop-free assertion. Same fix shape as the Wave 0.2 consent tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(mcp): stop reload-main poisoning across the MCP test files Root cause of the CI failure: the bindings REST fixture set OMNIVOICE_MCP_DISABLE=1 and reloaded main but never restored it, so a later 'from main import app' in test_mcp_mount saw /mcp un-mounted ({'/audio','/voice_audio'}). Reloading main mutates the shared module for every subsequent test. - REST fixture: drop the disable flag (the mount is harmless without a lifespan), yield the client, and restore main (+ core.config/db) to the default data dir in teardown so the global module is clean again. - test_main_mounts_mcp_route: reload main with the disable flag cleared so the assertion is independent of any earlier reload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c8fdcb619a |
fix(settings): remove stray rebase conflict marker in settings.py (#367)
A '>>>>>>>' marker from the #365 rebase was committed at the tail of the LLM-endpoint block, making the module unparseable. Strip it; settings.py parses clean and the endpoint tests pass. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d0b46f249e |
feat(settings): remote LLM endpoint UI — Ollama/vLLM/LM Studio (Wave 2.4) (#365)
A focused Settings panel for the OpenAI-compatible LLM that powers cinematic translate, glossary auto-extract, and dictation refinement (Wave 2.1). Persistence reuses the existing TRANSLATE_BASE_URL / TRANSLATE_MODEL / TRANSLATE_API_KEY env vars (already in system.py PERSISTENT_KEYS, restored at startup), so llm_backend/translator resolution is unchanged — vLLM is a verified drop-in, Ollama ignores the key, vLLM/LM Studio require it. - GET/PUT /api/settings/llm-endpoint (loopback-gated): read shape returns base_url, model, masked key, and live availability; PUT treats a null field as unchanged and an empty string as clear (so the key isn't wiped by a base-url-only save). Key is masked to last-4 in the read path, never echoed. - Credentials-tab panel with one-click presets (Ollama/LM Studio/vLLM/ OpenAI), base URL + model + optional key fields, and a reachable/not status badge. 6 endpoint tests (read shape, set+mask, null-unchanged, empty-clears, local-url-no-key, short-key masking); availability assertions guarded on openai being installed. Spec: parity program Wave 2.4 / competitive-analysis §R2 rung 4. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9b6d1d0863 |
docs(agentic): OmniVoice as a TTS/STT provider for pipecat/LiveKit (Wave 2.5) (#366)
Agentic v1: OmniVoice is a provider, not the orchestrator. Its existing OpenAI-compatible API already serves everything pipecat/LiveKit need (POST /v1/audio/speech with pcm/wav, voice-profile id, speed; default 24 kHz output matching pipecat's OpenAITTSService) — so this is docs + an example + a contract test, no new endpoint. - docs/agentic-voice.md: the provider recipe for pipecat (base_url to :3900/v1) and LiveKit, the remote-backend note (bearer from 2.3), the consent-locked-voice nudge (0.2), and an explicit telephony-is-deferred scope box. - examples/agentic/pipecat_minimal.py: lazy-import skeleton wiring the OmniVoice STT/TTS services (importable without pipecat installed). - tests/test_agentic_provider_contract.py: pins the /v1/audio/speech request shape pipecat sends (pcm + wav formats, voice-profile passthrough, speed) so the documented recipe can't silently break. Validated in CI. Spec: Action 15 / §R1 v1 / parity program Wave 2.5. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
22ba348f17 |
feat(remote): backend URL + bearer key + Tailscale docs (Wave 2.3) (#364)
Run inference on a remote GPU box, drive it from the desktop app — opt-in,
off by default (loopback-only is unchanged when no key is set).
Backend:
- BearerKeyMiddleware (main.py): when OMNIVOICE_API_KEY is set, every
non-loopback HTTP + WebSocket request must present it (Authorization:
Bearer, ?api_key=, or the ov_key cookie set on first auth). Pure ASGI
(no response buffering), loopback always bypasses, SPA shell stays
reachable. Constant-time compare, never logged.
- ws_remote_authorized() in dependencies; capture_ws lets a keyed
non-loopback client through its inline loopback guard (the thin-client
dictation case: mic local, GPU remote).
Frontend:
- api/client.ts: ov_backend_url (localStorage) is the top-precedence base
override; new wsUrl() derives ws scheme + host from the API base (not
window.location, which lies in the Tauri webview) and appends ?api_key.
apiFetch attaches the bearer header. Both WS call sites (dictation,
events) routed through wsUrl; the HTTP transcribe fallback through
apiFetch.
- Settings > Sharing > Remote backend panel: URL + key fields, a
test-connection probe against {url}/health, save-and-reload.
Docs: docs/remote-gpu.md — the Tailscale recipe (MagicDNS + Serve, never
Funnel, headscale note, plain-HTTP-is-sniffable warning, PIN-vs-key split).
Tests: 10 bearer-middleware cases (inert without env, loopback bypass,
401 without/pass with key via header+query, wrong key, shell exemption,
plain-ASGI guard, WS handshake reject/accept). Validated in CI.
Spec: parity program Wave 2.3 / competitive-analysis §R2 rungs 1-3.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
10806fea4f |
feat(dictation): optional local-LLM refinement of finals (Wave 2.1) (#363)
Phase 2 of Spec 3, on top of Wave 1.1's deterministic collapse. Prompt design ported from voicebox (MIT): 'text filter, not an assistant' base instruction + three toggleable sections (smart_cleanup, self_correction, preserve_technical) + 7 few-shot examples passed as STRUCTURED chat turns (small local models echo inline examples). Runs through the user's own Ollama/LM Studio/OpenAI-compat endpoint via llm_backend — new additive chat_messages() on the adapter; chat() now delegates to it. Pass-through is the contract: with no LLM configured (backend 'off'), on any error/timeout, or on an empty reply, the raw transcript stands — identical default behavior on every platform. Refinement runs off-thread on FINALS only; the WS final dict gains optional refined_text and the dictation pill pastes refined_text ?? text (raw kept in history). Settings: GET/PUT /api/settings/dictation-refinement (loopback-gated, persisted in the settings table) + a Capture-tab panel with the master switch + per-flag toggles and a 'no LLM configured' hint. 15 new unit tests: prompt sections per flag, structured few-shot message shape, and the full maybe_refine pass-through matrix (off backend, disabled config, LLM failure, empty reply, empty input). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ac8bdecfa6 |
test(api): pin the /generate surface pyvideotrans's integration consumes (Wave 1.3) (#359)
pyvideotrans drives OmniVoice as a per-line clone backend (their videotrans/tts/_omnivoice.py — being replaced upstream with a REST integration against POST /generate). This contract suite pins the exact multipart shape that integration sends (text + uploaded ref_audio + ref_text + language name + num_step/guidance_scale/speed/denoise/ postprocess flags -> audio/wav with X-Audio-Duration) so a /generate change that would silently break the 17.9k-star upstream fails our CI — the engine-compat constraint extended to an external consumer. Engine stubbed; validated in CI (local torch/Triton segfault on main-importing tests, see project memory). Spec 11 / parity program Wave 1.3 (our-repo half). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9162f2b9e7 |
feat(stream): sentence-by-sentence /ws/tts via ported chunker (Wave 1.4) (#358)
Ports Patter's SentenceChunker (MIT, attribution header) behavior-identical — all 61 upstream golden parity scenarios ship as fixtures and pass, including documented quirks (current_behavior xfail semantics mirrored from their parity runner). Terminator tables carry functional CJK; file added to the test_no_hardcoded_cjk allowlist per convention. /ws/tts now splits the request into sentences and synthesizes each in turn, streaming the first sentence's PCM while later sentences are still generating — the time-to-first-audio win on multi-sentence input. Single-sentence requests behave exactly like the old single-shot path; 'start' metadata still waits for the first generation so lazy-loading engines report their true sample rate. Italian comma-decimal guard hard-disables aggressive first-clause flush per upstream. Spec 8a (docs/competitive-analysis.md) / parity program Wave 1.4. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
454affb6e9 |
feat(tts): unlimited-length generation — sentence-boundary chunking + crossfade (Wave 1.2) (#357)
Ports voicebox's chunked TTS (MIT, attribution header) with two deliberate changes: the concat half is reworked for torch tensors (matching what our inference helpers feed the effect chain, incl. multi-channel on the last axis), and the sample rate comes from the engine's declared rate instead of the first chunk (fixes a latent upstream bug). Long text (> max_chunk_chars, default 800) splits at sentence boundaries (abbreviation/decimal-aware, bracket tags atomic, fullwidth enders via unicode escapes for the CJK gate) -> per-chunk generation with deterministic seed variation (seed+i) -> linear crossfade join (default 50 ms, 0 = hard cut) -> effect chain + watermark once on the joined audio. Wired into BOTH inference paths (OmniVoice-native _run_inference and the engine-adapter _run_backend_inference) beside the existing [pause] stitcher; [pause] inputs keep their dedicated path. Short text is byte-for-byte the old single-shot path; max_chunk_chars=0 disables. New /generate form params: max_chunk_chars (>=0, default 800), crossfade_ms (0-1000, default 50). Tests: 15 model-free unit tests (split priorities, abbreviation/decimal/ tag guards, crossfade math incl. multichannel + clamping) + 3 stubbed- engine endpoint tests (long text fans out with no words lost, short text single-shot, 0 disables). Endpoint tests validated in CI — this machine has a pre-existing local torch/Triton segfault on any main-importing test. Spec: voicebox deep dive 1 / parity program Wave 1.2 / #346 unlimited-length item. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
93723c2789 |
feat(dictation): collapse Whisper hallucination loops in final transcripts (Wave 1.1) (#356)
Deterministic pre-pass ported from voicebox (MIT, attribution header): word-level (token repeated >=6x, punctuation-normalized) + character-level (2-60-char unit repeated >=6x, catches multi-word and no-space-script loops). Rhetorical repeats below 6 survive; no LLM involved; identical on every platform. Applied to the FINAL text in /ws/transcribe and POST /transcribe — segments keep raw recognition so timings stay truthful. Phase 1 of Spec 3 (docs/competitive-analysis.md); the optional local-LLM refinement pass (phase 2) lands with parity program Wave 2.1 in the same module. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7422f20a63 |
feat(profiles): consent-locked voice profiles — verified_own_voice + spoken consent flow (Wave 0.2) (#354)
* feat(profiles): consent-locked voice profiles — verified_own_voice + spoken consent flow (Wave 0.2)
A profile becomes 'verified own voice' when its owner records themselves
reading a consent statement (spoken attestation, not a checkbox). Agentic
features and gallery sharing will gate on the flag; plain local synthesis
never does.
- alembic 0003 (additive, PRAGMA-guarded, downgrade supported) +
_BASE_SCHEMA columns: verified_own_voice, consent_text,
consent_audio_path, consent_recorded_at
- POST/DELETE /profiles/{id}/consent — stores the recording as provenance
in VOICES_DIR ({id}_consent.*), replaces on re-record, cleans up on
revoke and on profile delete; 422 on empty statement / too-short audio
- VoiceProfile page: Verified badge + Voice ownership panel (record via
the existing useRecording denoise flow, revoke with confirm); en.json
keys only (other locales fall back per the advisory i18n parity policy)
Spec: docs/competitive-analysis.md Action 22 / parity program Wave 0.2.
Prerequisite for agentic v2/v3 and the persona gallery.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(profiles): harden consent paths against py/path-injection; drop lifespan in tests
- _voices_path(): resolve DB-stored filenames strictly inside VOICES_DIR
(bare-filename check + realpath containment); extension whitelist on the
uploaded consent filename (fallback .wav) so a crafted filename can never
steer the on-disk path. Applied to write, re-record cleanup, revoke, and
profile-delete cleanup. New test: malicious upload filename falls back.
- Test fixture no longer runs the app lifespan: startup/shutdown touched
module-level asyncio primitives bound to another module's event loop,
making the suite order-dependent in full-suite CI. init_db() is called
directly; endpoints under test need only the schema.
Fixes the CodeQL (3x py/path-injection high) and full-suite event-loop
failures on PR #354.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
1195b4e0dd |
test(evals): LLM-judge eval tier — non-gating semantic suites (Wave 0.3) (#355)
Ports Patter's eval harness (MIT, attribution headers) into tests/evals/ with the judge transport swapped to services/llm_backend.py — the judge runs against whatever local Ollama/LM Studio/OpenAI-compat endpoint the user configured, keeping local-first. Both Patter hardening details kept verbatim: verdict recomputed locally from the score (hallucinated 'passed: true' at score 0.2 fails), and tolerant JSON parsing (fences stripped, invalid JSON -> fail-with-reasoning). Per-case containment: agent exceptions keep the partial transcript and still judge it; a judge failure records score 0 instead of aborting the suite. HARD RULE preserved: LLM judges never gate CI. The scheduled workflow (weekly + dispatch) is continue-on-error with the JSON report as artifact; run_evals.py exits 0 always and skips cleanly when the active LLM backend is 'off'. Deterministic probe judges remain the only gates; the harness unit tests (10, no LLM needed) do run in gating CI. First suite: dub translation naturalness v1 (4 cases) driving the real cinematic_refine_sync reflect+adapt chain. The telephony-specific session/assertions layers were deliberately not ported. The dictation-refinement suite lands with Wave 1.1/2.1. Spec: docs/competitive-analysis.md Spec 9b / parity program Wave 0.3. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
11c498eeb5 |
ci(docs): daily docs-drift job — canonical inventory vs README/docs/registries (Wave 0.1) (#353)
docs/features.yaml is the curated single source of truth (12 features, 11 TTS + 7 ASR engine ids, required install docs). scripts/check-docs-drift.py diffs it against README.md, docs/, and the engine registries — parsing registry keys from source so the CI runner never imports torch. The daily workflow updates ONE rolling 'docs-drift' issue in place and auto-closes it when clean (pattern adapted from Patter, MIT). Self-test includes a real-repo-is-clean gate, so any PR that changes engines/features without updating the inventory fails CI too. Spec: docs/competitive-analysis.md Spec 9a / parity program Wave 0.1. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
73de4f9277 |
docs(specs): ElevenLabs-parity program — waved implementation plan from #346 + #345 (#349)
Turns the discussion #346 roadmap and the competitive-analysis research (#345) into an executable program of small PRs: 6 waves, dependency-aware, each item citing its Spec/§R section with effort and acceptance criteria. Accounts for Smart Fit Phase A (#347), the timeline editor (#348), and Scalar (#307) having already shipped. Telephony explicitly deferred behind guardrails + two spikes. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
eea2053a5e |
docs: competitive analysis v2 — second-tier landscape, source deep dives, action specs, market sentiment (#345)
* docs: expand competitive analysis — second-tier landscape, deep dives, action specs, market sentiment Second research pass over PR #339's analysis (six parallel agents): - Second-tier landscape: 13 projects surveyed, 7 profiled; KrillinAI/KlicStudio promoted to direct-competitor status - Source-level deep dives: voicebox + Patter (MIT, portable briefs) and pyvideotrans (GPL, clean-room functional specs incl. the full _rate.py decision tree with verified constants) - pyvideotrans's OmniVoice integration verified broken (Gradio /_clone_fn vs our FastAPI :3900) — Action 11 reframed as fix-the-bridge - Implementation specs mapping all ranked actions onto our codebase - User-sentiment + market-positioning research (issue clustering, ElevenLabs pricing pressure, honest verdicts on our five differentiators, name-collision risk, four positioning moves) - Three stale matrix grades corrected (docs-drift CI, eval harness, MCP) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: ground the #346 roadmap in research — agentic voice, remote GPU, audiobooks, persona gallery, model/env management Third research pass (four agents + five verification sub-agents) adding a 'Roadmap directions' section that maps every item from discussion #346 to either an existing spec or new research: - Agentic voice workflow: pipecat (BSD-2) as the license-clean in-process runtime; honest telephony constraints (no local PSTN path — opt-in carrier creds only); FCC/TCPA, Texas SB 140, ELVIS Act, EU AI Act Art 50 (2026-08-02, OSS exemption does not cover it); six concrete guardrails; v1/v2/v3 scope ladder - Remote GPU/Tailscale/remote API: base-URL + bearer-token consensus pattern; 175k-exposed-Ollama cautionary tale; Tailscale rung (a) docs-only; vLLM drop-in for llm_backend; Scalar already shipped (#307), remaining work is OpenAPI hygiene - Audiobook creator + persona gallery: ACX technical-spec mastering bar; ebooklib/PyMuPDF/mobi AGPL/GPL parser traps with clean alternatives; unoccupied consent-aware-gallery territory; .ovsvoice portable format - Model/env + GPU compat: uv link-mode dedupe math (measured wheel sizes); two-dimensional (torch x cuda-variant) -> sm_XX compat matrix; HF cache as single source of truth (hf cache ls/rm/verify); preflight gate + loud CPU-fallback banner vs the Ollama/voicebox silent-fallback antipattern - Eight consolidated new actions (15-22) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
65fc5245dc |
feat(dub): timeline segment editor — drag, snap-to-onset, keyboard a11y (#280) (#348)
* feat(dub): full-track speech-onset detection + GET /dub/onsets/{job_id} (#280)
detect_speech_onsets() lists every speech rise across the track (frame RMS,
adaptive threshold, 150ms hysteresis) — powers the timeline editor's
snap-to-onset ticks. Route prefers the Demucs vocals stem, falls back to the
mix, and caches onsets.json per job (mtime-invalidated).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(dub): timeline editor math core — windowing, snap, clamp, fingerprint-safe commit (#280)
Pure helpers for the segment track: binary-search windowing, snapTime with
deterministic ties, neighbour/min-duration clamps with Alt-overlap (<=200ms),
commitMoveResize with fingerprint parity (move touches only start/end; resize
sets speed exactly like the old Regions handler and DELETES the key at 1.0 so
_canon_value's missing-vs-1.0 hashing can't mark untouched segments stale),
and overlap detection.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(dub): SegmentTrack editing lane replaces the Regions plugin (#280)
Custom DOM segment boxes (6px edge handles, body-drag move, speaker colors,
stale/fresh tint, hatched overlap warning) virtualized by time over a single
{pxPerSec, scrollLeft} alignment source read off WaveSurfer's wrapper.
Snap-to-onset ticks on a viewport-sized canvas light up in snap range;
Ctrl/Cmd-wheel zooms centered on the cursor; double-click plays the slot via
playRange (timeupdate watcher pauses at slot end). Roving-tabindex listbox
keyboard model (arrows / Enter / Shift / Alt / Delete / S) with polite
aria-live announcements. WebKit fallback keeps a self-scrolling lane at a
fixed px/sec. timeline.* strings translated in all 21 locales.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(dub): wire timeline editor — per-gesture undo, id fix, table selection sync (#280)
segmentMoveResize() pushes undo ONCE per gesture (drag commits on pointerup;
keyboard nudges coalesce per focus session) and matches by String(id) — the
old parseInt('seg-3_a') path edited the wrong segment after a split. Commits
go through commitMoveResize for fingerprint parity, and the existing
recomputeIncremental effect picks up every commit. Clicking a timeline box
scrolls + highlights its row in DubSegmentTable; 'preview dub here' parks
the player at the slot start, then synthesizes the line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dub): inline the onsets-cache containment guard — CodeQL can't track helpers
Same lesson as #328/#329: the realpath+startswith sanitizer must sit at
the sink, not behind a function return. Unused helper removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
4b21f82619 |
feat(dub): Smart Fit timing strategy — planner, fingerprints, generate path (phase A) (#347)
* feat(dub): Smart Fit planner, fit fingerprints, shared ffmpeg stretch helpers - services/fit_planner.py: pure, I/O-free planner for dub-length fitting v2 — slack absorption (gap guard), audio-only band (<=1.2x), geometric 50/50 audio/video split capped at 1.5x / 2.0x, residual overflow accounting, and a stretch_video-compatible video_plan + fitted timeline cursor. Clean-room reimplementation from a published description. - services/incremental.py: fit_fingerprint() over the fit params with the same _canon_value canonicalisation as segment hashes (#281 class). Fit params stay OUT of segment_fingerprint — a fit change re-mixes, never re-TTSes. - services/ffmpeg_utils.py: move _atempo_chain/_pitch_preserving_stretch out of the dub_generate router (lazy torch/numpy imports) so the Phase B export pipeline can reuse them; add probe_duration() ffprobe helper. - schemas/requests.py: timing_strategy gains "smart_fit"; optional fit_options knob overrides default server-side. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(dub): smart_fit branch in the generate path TTS loop unchanged (dur_s=None, natural-rate WAVs on disk). After the loop, plan_fit() decides per segment; the mix loop applies audio_rate via the pitch-preserving atempo pipe (linear-interp fallback), trims residual overflow with the existing fades, and places audio at the planned new_start on a fitted-length canvas. Truthful fit_status entries (audio_rate / video_ratio / overflow_s) feed the row badges. Persists job["fit_plans"][lang] = {plan (exact _build_video_stretch_filter_graph shape), fitted_segments (cue times from ACTUAL stretched sample positions), total/orig duration, params, fit_fp} and mirrors fit_fp on dubbed_tracks[lang]. video_stretch_plans untouched. Strategy-transition guard: job["seg_wav_kind"] records whether on-disk seg WAVs are natural or slot-squeezed; a smart_fit partial regen over slotted (or unknown) WAVs forces one full regen instead of double-compressing. Old strategies and old persisted jobs are byte-identical (all new reads via .get()). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ui): Smart Fit option in the dub timing picker (all 21 locales) - prefsSlice: TimingStrategy union gains 'smart_fit'; optional FitOptions overrides (null by default — backend defaults apply identically on every platform); persisted alongside timingStrategy. - DubTab: Segmented gains Smart Fit with i18n label + tooltip. - useDubWorkflow: sends fit_options only when set and strategy is smart_fit. Default strategy stays 'concise' — no default behaviour change on any platform. - locales: dub.timing_smart_fit{,_title} translated in all 21 languages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(dub): fit planner unit + golden suites, smart_fit generate-path integration - test_fit_planner.py: threshold boundaries (0.9/1.0/1.2/1.21/4.0), cap saturation -> overflow, slack absorption incl. gap guard, last-segment tail, cursor monotonicity, allow_video_retime=False, video_plan fed straight into _build_video_stretch_filter_graph, fit_fingerprint canonicalisation (int vs float, omitted vs default — the #281 class) and a pinned stable digest. - tests/fixtures/fit_planner/*.json: 4 golden FitPlans; algorithm drift is a deliberate fixture diff, never a silent change. - test_smart_fit_generate.py: hermetic end-to-end runs (mock TTS, no ffmpeg) covering audio-only stretch, hybrid timeline growth + persisted plan shape, fit_options override, strict_slot->smart_fit forced regen then zero-TTS fit-only re-mix, and concise back-compat. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(competitive): dub-length fitting row reflects Smart Fit Phase A Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(incremental): mark fingerprint hashes usedforsecurity=False — dedup keys, not security (Bandit) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4288863f50 |
docs: model-source support policy — verifiable public sources only (#310) (#344)
* docs: model-source support policy — verifiable public sources only Owner decision (issue #310): the local-loading mechanism stays, but official support covers only models from verifiable public sources (HF repos, official releases with license + checksums). Privately distributed / paywalled model files are use-at-your-own-risk; never run bundled executables. Mirrored in SECURITY.md as a supply-chain note. Per the docs-sync rule, shipped alongside the policy decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: firm up model-source policy — open, public, verifiable only; no private/paid models Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2574fccaf6 |
docs: community docs refresh — README, CONTRIBUTING, SECURITY, SUPPORT, Docker/macOS install (#341)
* docs: refresh community docs to match the project's current reality - README: download badges now point to releases/latest (were frozen at v0.2.7); Intel-Mac note (pre-built bundle is Apple Silicon; source works on Intel; pre-built Intel tracked in #279) - SECURITY: supported-versions table 0.2.x -> 0.3.x + 0.2.7 legacy row - docs/install/docker.md: tag mapping matches docker.yml after #338 — :latest is the rolling main preview, :stable (new) pins releases - PR template: removed the abolished two-RC/48h-soak ceremony; documents continuous-to-main - CONTRIBUTING: new sections — what bot review looks like (CodeRabbit + Greptile), conventional-commit + issue-link expectations, the quality gates (cross-platform parity, 21-locale i18n + CJK allowlist, alembic, engine back-compat, local-first, loopback security posture), and a contribution-licensing grant that keeps the AGPL + commercial dual-license viable - SUPPORT.md: new — channels, before-you-file checklist, expectations - docs/install/macos.md: Intel caveat aligned with reality Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: codify the docs-sync hard rule — behavior changes update their docs in the same PR Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(agents): rtk rules for Antigravity — token-compressed tool output Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
101cf2a6e7 |
ci(release): reinstate macOS Intel (x86_64) build target on macos-15-intel (#342)
Intel MacBook users had no installable artifact: the release matrix only built aarch64-apple-darwin, and Rosetta 2 cannot run arm64 apps on Intel (it only translates the other direction) — the rationale in the old "Intel dropped" comment was backwards. Refs #279. - Add a native `macos-15-intel` matrix leg (GitHub's designated x86_64 migration target after macos-13 retired Dec 2025; standard image, supported through Aug 2027) building --target x86_64-apple-darwin with app,dmg,updater bundles. - Existing per-TRIPLE steps already carry x86_64-apple-darwin cases (uv sidecar tar.gz, evermeet.cx ffmpeg/ffprobe — x86_64 Mach-O, natively correct on Intel), so the leg flows through the same Bundle/Build/Smoke/Verify steps untouched. - The PR #290 signing path applies automatically: ad-hoc seal from tauri.conf.json signingIdentity "-", opt-in APPLE_* stable signing, and scripts/verify-macos-signing.sh both gated on runner.os == macOS. - tauri-action includeUpdaterJson merges the new darwin-x86_64 platform key into latest.json alongside darwin-aarch64, so Intel installs auto-update on both Stable and Preview channels. - docs/install/macos.md: table telling users which DMG (aarch64 vs x64) matches their Mac, and the from-source fallback for old releases. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
acb7c90083 |
fix(tts): pin cudagraph-compiled model inference to one dedicated thread (#315) (#343)
torch.compile(mode="reduce-overhead") captures CUDA graphs whose state is thread-local (torch/_inductor/cudagraph_trees keys its tree manager off the capturing thread). The _gpu_pool ThreadPoolExecutor runs up to 4 workers, so the first render captured the graph on worker A and a later render dispatched to worker B replayed mismatched cudagraph state — silently corrupting the audio (static noise + slowed playback from the second render onward, no exception, so the #327 eager fallback never fired). Fix: when the model is compiled with a cudagraph mode, wrap model.generate (the same single choke point #327 uses) so every call hops to a dedicated 1-thread "compiled-infer" executor — capture and replay always happen on the same thread, deterministically. A thread-ident re-entrancy guard runs inline when already on that thread (a 1-worker executor submitting to itself would deadlock). Installed after the #327 fallback wrapper, so the eager retry path also runs on the dedicated thread. No behavior change for CPU / MPS / Windows-no-Triton / compile-disabled paths: should_torch_compile() gates exactly as before and uncompiled models keep the full pool. Closes #315 Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
948bc76543 |
macOS: ad-hoc sign so users open without Terminal + signing/notarization verification (#290)
* chore(release): add macOS signing/Gatekeeper/notarization verification Codify and enforce the macOS build-signing requirements. The release pipeline built bundles and had opt-in Apple signing, but never verified codesign/spctl/notarization — unsigned or broken bundles could ship silently. - scripts/verify-macos-signing.sh: runs codesign --verify --deep --strict, spctl Gatekeeper assessment, per-nested-Mach-O signature check, stapler validate, and (opt-in) notarytool history. Report-only by default (unsigned dev/preview is expected); --require-signed fails on any unsigned/un-notarized component so a broken release stops instead of publishing an unsigned artifact. - scripts/macos-dev-unquarantine.sh: local-dev-only quarantine stripper, with a loud "never a substitute for notarization" warning. - release.yml: new "Verify macOS signing" step on the macOS leg — report-only on unsigned paths, STRICT on the opt-in signed stable path (same condition as "Configure Apple signing"), so signing/notarization failures fail the job. - docs/macos-signing-verification.md: the canonical 10-point requirements + how-to-verify checklist, cross-linked to docs/install/macos.md and DESKTOP_RELEASE.md. Verified locally: report-only PASS (exit 0) and --require-signed FAIL (exit 1) against the real unsigned debug .app; release.yml parses as valid YAML. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(macos): ad-hoc sign bundle so users open it without Terminal (no Apple ID) The "app is damaged and can't be opened" error is caused by a broken/incomplete code-signature seal (codesign --verify failed: "code has no resources but signature indicates they must be present") on the quarantined download — there is no GUI bypass for that variant on modern macOS, forcing users to run `xattr`. Give the bundle a VALID ad-hoc signature at build time (free, no Apple Developer account) via tauri.conf.json bundle.macOS.signingIdentity = "-". Verified through a real `tauri build`: the produced .app is now flags=adhoc,runtime and passes codesign --verify --deep --strict. A valid seal flips the Gatekeeper prompt from the un-bypassable "damaged" to the GUI-bypassable "unidentified developer", which users clear with right-click → Open / Settings → "Open Anyway" — no Terminal. Still not notarized (that needs the paid Apple ID), so there's a one-time confirmation rather than a clean double-click. The opt-in Developer-ID path is unchanged: APPLE_SIGNING_IDENTITY (env) overrides the "-" default on the signed stable release. - tauri.conf.json: signingIdentity "-" (ad-hoc default). - verify-macos-signing.sh: detect ad-hoc tier; report the no-Terminal GUI path in report-only, still FAIL it under --require-signed (production must notarize). - docs/install/macos.md: lead the Gatekeeper section with right-click → Open; keep xattr as fallback for the harsher "damaged"/corrupted-download case. - docs/macos-signing-verification.md: signing-tiers table + ad-hoc default note. - release.yml: comment the ad-hoc default + env override on the signed path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3c780dced9 |
feat(dub): speech-onset alignment + regional dialect targeting (#280) (#330)
Items 1 and 2 from the improvement list: 1. Synchronization — Whisper-family ASR stretches segment starts back over leading non-speech (intro music, silence), so the dub starts at 0:00 while the speaker starts at 0:02-0:03. New onset_align service snaps each segment start forward to the first audible vocal onset (adaptive RMS threshold over the Demucs-isolated vocals when available). Forward-only and conservative: never moves a start earlier, ignores sub-100ms shifts, preserves minimum duration, leaves silent-window segments untouched. Pure NumPy — identical across platforms. 2. Accent/vocabulary by country — a Dialect picker in the Dub panel (BCP-47 codes per target language) injects a regional instruction into LLM translation prompts (OpenAI/Ollama engines and the Cinematic refine pass): Argentina yields 'Vos sos muy listo', not 'Tú eres muy listo'. Non-LLM engines show a clear hint that the dialect needs an LLM. New i18n keys translated in all 21 locales. Item 3 (segment rectangles: move/crop/stretch on the timeline) is a larger editor feature and stays open on #280. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: mergetest <test@local> |
||
|
|
c0924f5eba |
fix(tts): torch.compile failures fall back to eager — generation never fails on unsupported GPUs (#278) (#327)
* fix(tts): torch.compile failures fall back to eager — generation never fails on unsupported GPUs (#278) On GPU architectures the bundled Triton doesn't support (e.g. Blackwell sm_120 / RTX 5060), the compiled model dies mid-generation inside the Dynamo/Inductor/Triton/cudagraph stack — previously surfaced as a fake 'ran out of memory' error and a dead Archetype preview. Now: - up-front arch gate: skip compile when the GPU's compute capability is not in this torch build's arch list (OMNIVOICE_FORCE_TORCH_COMPILE=1 overrides for PTX forward-compat setups) - runtime fallback: model.generate is wrapped once; a compile-stack failure (classified by exception chain: module, message, traceback paths — the cudagraph case is a bare AssertionError) logs a warning, restores the eager module, disables compile for the session, resets dynamo state, and retries eagerly. Non-compile errors propagate unchanged. - the /generate OOM handler no longer mislabels compile crashes as OOM and points users at the actual remedy. Fixes #278 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Potential fix for pull request finding 'CodeQL / Empty except' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for pull request finding 'CodeQL / Empty except' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Update backend/api/routers/generation.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: mergetest <test@local> |
||
|
|
e2027c1291 |
ci(security): never cancel main scans — merge trains left red ✗ on every intermediate commit (#340)
PR branches keep cancel-in-progress (superseded scans are wasted work). On main each commit gets its own concurrency group, so a burst of merges runs every scan to completion instead of cancelling all but the last — 'cancelled' renders as a permanent red ✗ in the commit history even though nothing failed. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a949b2c78a |
chore(version): main is always latest release + 1 — rule, bump to 0.3.6, Docker retag, auto-bump job (#338)
Versioning hard rule (owner-set 2026-06-11), codified in CLAUDE.md: - main's three version sources (tauri.conf.json, Cargo.toml, pyproject.toml) always carry last release + 1 patch; bumped 0.3.5 -> 0.3.6 now. - Preview builds stamp BASE-N which now sorts ABOVE the last stable (0.3.6-N > 0.3.5) — the updater ordering becomes natural and the Windows MSI ProductVersion wrinkle disappears. - Docker: :latest = rolling main preview; :stable + :X.Y.Z + :X.Y = tagged releases. workflow_dispatch still only emits throwaway :sha-. - release.yml gains a version-bump job: on every stable v* tag it bumps main to the next patch automatically. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
853b9eefc7 |
fix(dub): burn translated subtitles, fix subtitle save JSON error (#309) (#328)
* fix(dub): burn translated subtitles, fix subtitle save JSON error (#309) Two symptoms, one root: the job kept the original-language ASR transcript while the editor only sent translated/edited text in the generate request. - dub_generate now persists the segments the dub was actually generated from back onto the job (metadata carried over by stable id, fallback index; text_original retained for dual-subtitle layouts) — SRT/VTT export and ffmpeg burn-in now render the dub language, not the source. - The SRT/VTT export endpoints honor the save_path query param the Tauri save dialog appends (like every other export) and return the standard JSON envelope — previously they ignored it and returned the raw body, so the frontend's JSON.parse choked on the SRT cue index ('Unexpected non-whitespace character after JSON'). - Frontend guards the save response content-type so any future raw-body response surfaces as a clear error. Fixes #309 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Potential fix for pull request finding 'CodeQL / Uncontrolled data used in path expression' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fix(dub): use the file's established realpath+startswith containment idiom (CodeQL) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dub): write subtitle saves from the Tauri process, not the backend (#309) The backend save_path variant on /dub/srt and /dub/vtt routed a user-controlled destination through the loopback HTTP surface — six new CodeQL path-injection flows plus two log-injection flows. Subtitles are small text bodies, so the frontend now fetches them raw and writes the file via a new save_text_file Tauri command: the OS save dialog in the trusted process is the write authorization, and the backend never sees a destination path. Binary exports keep the established save_path flow. Also strips newlines from user-derived values in the two flagged log lines. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dub): leave _native_save byte-identical to main The newline-strip on the log line moved a path sink onto a changed line, which made CodeQL re-attribute the long-standing binary-export flow to this PR as a new alert. The subtitle endpoints no longer feed this function at all, so restore the exact original line — the baseline alert stays baseline, and hardening pre-existing flows belongs in its own PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> |
||
|
|
1ed22af6ca |
docs: competitive analysis — voicebox, pyvideotrans, Patter (feature matrix + ranked adoption plan) (#339)
* docs: competitive analysis — voicebox, pyvideotrans, Patter Feature matrix vs our self-inventoried maturity grades, license-aware reuse verdicts (MIT = port with attribution, GPL-3.0 = reimplement only — copied GPL files would break the AGPL + commercial dual-license), and an 11-item ranked action plan with effort estimates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: append Chatterbox engine evaluation to the competitive analysis Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: mergetest <test@local> |
||
|
|
d0517fdb87 |
chore(review-bots): diagrams + ASCII UI sketches in every PR walkthrough (#337)
* chore(review-bots): visual walkthroughs — diagrams for mechanics, ASCII sketches for UI CodeRabbit: enable sequence_diagrams explicitly and instruct the high-level summary to sketch UI changes as compact ASCII before/after and behavior changes as a small mermaid flow. Greptile: new repo-level greptile.json turning on the sequence-diagram and summary sections with matching instructions, plus the project's local-first and cross-platform hard rules so both bots review against them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(review-bots): expert-panel review rubrics, pre-merge rule audits, knowledge base Encode one senior-domain-expert lens per subsystem (ML inference for backend/services, product frontend for src, desktop systems for src-tauri, test infra for tests) as path instructions; add non-gating pre-merge checks for the project's four hard rules (cross-platform default parity, 21-locale i18n completeness, local-first guarantee, backward compatibility); feed CLAUDE.md and docs into CodeRabbit's knowledge base; mirror it all in greptile.json with customContext rules and strictness tuning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
668d824e86 |
feat(setup): unified first-run journey — install gate, studio-console wizard, platform awareness (#295)
* feat(setup): first-run install gate — nothing installs until the user confirms a plan New `setup` module parks first runs in BootstrapStage::AwaitingSetup instead of auto-installing. complete_setup validates the user's InstallPlan and only then starts the existing bootstrap: - install modes: installed (platform dirs) / portable (one folder next to the exe / AppImage, config.json travels with it) - user-chosen storage: env dir, data dir (OMNIVOICE_DATA_DIR), model cache (OMNIVOICE_CACHE_DIR) — None = legacy default, byte-identical behavior - minimum-space gate: per-volume free-space check (fs4 statvfs), grouped by filesystem so dirs sharing a disk sum their requirements; install refused when short (9 GiB env + 7 GiB models + 1 GiB data, measured + headroom) - custom mirrors (PyPI index, HF endpoint, python-build-standalone) take precedence over region presets in the venv/sync/backend env wiring - ROCm torch variant selectable via config (env var still wins) - existing installs migrate silently: venv present → setup_complete=true, no questions re-asked; dev trees skip the gate entirely 19 unit tests (disk probing, space grouping, mirror validation, legacy config compat). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): first-run setup screen — mode, storage with space gate, mirrors, compute FirstRunSetup renders when the Rust side reports awaiting_setup (lazy-loaded; regular launches pay nothing). One screen, defaults all work: - language picker first (rest re-renders translated), 21 locales shipped - Installed / Portable mode cards (portable disabled with reason when the exe-adjacent folder isn't writable) - storage rows with live per-path free-space probes (debounced check_install_target), 'needs ~X / Y free' readouts, folder pickers - client mirrors the Rust per-volume space gate: Start installation is disabled with an explicit reason until every volume fits - compute (CUDA-auto / ROCm), update channel, region + custom mirror URLs - complete_setup errors surface inline; on success the normal bootstrap progress UI takes over on the next status poll Verified on a wiped machine: gate parks (no spawn, no downloads), screen renders, 450 GB ≥ 17 GB requirement → Start enabled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): studio-console redesign of the first-run screen The setup screen now reads as powering on studio hardware rather than a web form — true to a voice studio, and self-sufficient offline (every font and asset is bundled; a first run may be on a restricted network): - breathing waveform masthead (CSS-only, deterministic speech-cadence silhouette, staggered per-bar delays) - Source Serif 4 display headline + engraved IBM Plex Mono panel labels + Inter body — the three faces the app already ships - rack-unit panels with corner screws, engraved title rules, serial plate (OVS · vX.Y.Z) - disk space as segmented LED capacity meters: lit = what the install consumes, alarm-blink red on insufficient volumes - mode cards with indicator LEDs; 'armed' Start button — LED lights and a halo pulses only once every volume passes the space gate - atmosphere: corner accent glows + SVG film grain; staggered rise-in choreography on load - all motion transform/opacity only; prefers-reduced-motion holds every frame still; theme-token derived colors; focus-visible rings throughout No logic changes: same IPC calls, same i18n keys, same space-gate math. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): wide desktop deck, hardware-aware Compute + Update channel cards Three pieces of feedback addressed: - width: the console is now a 1240px two-column deck (storage rail left, decision rail right) that uses desktop real estate; collapses to one column under 980px and stacks fully under 620px - no outer chassis box: panels float directly on the atmospheric backdrop, each carrying its own rack-unit treatment - Compute and Update channel split into separate cards with real information: get_setup_state now detects hardware (nvidia-smi → CUDA name, /sys/class/drm vendor 0x1002 → AMD/ROCm, Apple Silicon → MPS, CPU cores + RAM via sysinfo; best-effort, never blocks) — the Compute card shows a live 'Detected: …' readout, badges the option that matches the machine, and pre-selects ROCm on AMD boxes; both cards use LED radio options with full descriptions (6 new i18n keys × 21 locales) Also pins playwright-core as an explicit devDep — bun did not materialize it through @playwright/test, breaking programmatic browser use. 20/20 Rust tests · vite build · CJK guard green. Verified live (gate engaged, responsive single-column) and at 1600×1000 via mocked-IPC browser shot (two-column deck). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): move network (region + mirrors) into the masthead with language Language and download region are the two 'where am I' choices — they now sit together top-right of the masthead, with the custom-mirrors disclosure tucked beneath the subtitle. The Network panel is gone, leaving a balanced deck: Install mode + Storage left, Compute + Update channel right. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): strip the boxes — fills and rules carry the structure One design rule now: borders only where state demands them. Panels lose their boxes entirely (engraved mono title + rule separates sections); option cards, storage rows, selects/inputs, the hw readout, the version plate and the ghost buttons are all flat fills; active options glow with an accent tint + LED; blocked rows and errors use a red tint + 2px inset edge bar instead of a border. The badge chip is fill-only too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): quiet pass — every element earns its visual weight - waveform becomes a whisper: 22px trace, 2px bars, ~half opacity — an ambient signature instead of a billboard - storage readouts collapse to one mono line ('needs ~9 GB · 449 GB free'); the LED meter now appears only when it carries information (install would consume >35% of free space, or the volume is blocked) — at 449 GB free a bar was a meaningless sliver - Change… buttons go text-quiet (transparent until hover) - custom-mirrors disclosure right-aligns under the region select it extends, instead of floating under the subtitle - version plate moves to the footer next to the disk total — the masthead keeps only title, subtitle, and the two locale/region selects Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): platform-matrix awareness — distro+arch detection, ROCm gated to Linux, no Windows console flash The install matrix is OS family × distro × arch × GPU vendor, and the setup screen now both shows it and only offers choices valid for it: - HardwareInfo gains os_name (distro PRETTY_NAME from /etc/os-release on Linux, macOS/Windows elsewhere) and arch (x86_64/aarch64) — the detected line reads 'CachyOS x86_64 · NVIDIA RTX 4070 · 32×CPU · 31 GB RAM', exactly what bug reports cite - SetupState gains os; the ROCm option renders on Linux only (wheels don't exist elsewhere) and complete_setup clamps rocm→auto on non-Linux as the server-side backstop - nvidia-smi probe gets CREATE_NO_WINDOW on Windows — no cmd flash on the first screen a user ever sees - Apple Silicon → MPS, Intel mac → CPU, ARM Linux → CPU: all matrix cells resolve through the same base constructor Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): unify the whole first-run journey under the studio-console system Setup → Installing → Model wizard now read as one continuous experience: the same atmosphere, whisper waveform masthead, serif/mono type, LED language and quiet fills across all three acts. - Installing (BootstrapSplash): rebuilt in frs-* — segmented LED journey meter (completed steps + live byte progress), LED step rail (done=green, active=pulsing accent, pending=dim), engraved ACTIVITY panel with the quiet mono log (collapse/copy as text-quiet actions), failure act with red-tint error + hints + armed Retry. All logic untouched: stage poll, event subscription + backfill, dedupe, hints, region/language selects. - Model wizard (SetupWizard): same masthead with the step rail as engraved mono LED steps top-right, welcome cards as option-card surfaces, preflight as LED check rows (pass/warn/fail), frs nav buttons with armed primaries, embedded Model Store / Engines / Dictation panels scroll inside the act. Old 556-line stylesheet replaced by ~60 lines of glue; BootstrapSplash.css reduced to a resolving stub. - FirstRunSetup.css is now the journey's shared design system (step rails, log panel, banners, hints, wizard chrome, check rows appended). - 2 new strings (Installing / Activity) translated across all 21 locales. Validated end-to-end on this machine: setup screen → Start installation → real venv bootstrap (~10 min) → backend healthy on 3900 → model wizard. 20/20 Rust tests · vite build · CJK guard green · installing act verified via mocked-IPC screenshot at stage=installing_deps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): --setup re-entry flag + make the install-plan screen un-stealable The setup stage is first-run-only by design (completed installs skip it), but it must be reachable on demand and must actually win the mount when engaged. Three fixes: - 'omnivoice-studio --setup' parks the bootstrap in AwaitingSetup on any launch — checked before the attach-to-healthy-backend shortcut, so a running backend can't skip past it - App routing: awaiting_setup now outranks everything (a live backend answering /setup/status used to route straight to the model wizard); the wizard additionally requires stage === 'ready' so it can't mount during the initial stage race - useBootstrapStage: a transient IPC miss no longer permanently declares 'ready' (which killed the poll loop and silently skipped the setup / progress screens) — it retries up to 5 ticks before conceding Plus journey-wide titlebar clearance (content never sits under the GTK headerbar / macOS traffic lights / Windows controls) and drag-region mastheads on all three acts. Verified: mocked-IPC harness with stage=awaiting_setup + a LIVE backend answering /setup/status renders the setup screen, not the wizard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(setup): remove backdrop decoration — flat surface, state-only emphasis The corner accent glows and SVG film grain rendered as visible banding / noise artifacts on many panels — both gone; the journey now sits on a clean flat chrome background. Also swept the remaining decorative bloom: the active option card drops its glow shadow (flat accent tint + LED carry the state), and the armed Start button loses its pulsing halo (the lit LED already signals actionable). Remaining shadows are functional micro-detail only: 6px LED glows, meter track inset, red edge bars. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): journey rail + verbosity diet — clean, smooth, elegant The setup page is now visibly stage 1 of the install flow: a quiet breadcrumb rail (SETUP → INSTALLING → MODELS & ENGINES) sits between the waveform and the headline on both the setup and installing acts, LEDs marking done/active/pending — one continuous story across the journey. Verbosity halved without hiding information: - option descriptions unfold (260ms ease) only on the selected card; the page shows exactly one explanation per group, collapsed cards keep the text as a tooltip - storage rows drop their always-on caption (label + path + readout + Change… on one line; caption lives in the row tooltip) The whole page now fits a laptop window without scrolling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): merge Models + Engines into one wizard act Two tabs weren't necessary: models are the required gate, engines the optional extras — now two stacked panels in a single 'Models & engines' step (label reuses the journey-rail key, translated in 21 locales). Wizard shrinks to 4 steps: Welcome → System check → Models & engines → Dictation. Continue still gates on models_ready only; engines stay optional. Welcome cards updated to the 3 remaining acts; static cards keep their descriptions visible (the active-only fold is for radios). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(setup): wizard was skipped after first-run install — probe /setup/status on bootstrap ready The models-needed probe started at mount with a ~30s retry ceiling. On a first run, mount happens at the setup page — by the time the user reads it and the multi-minute install finishes, the attempts were long burned, so setupChecked landed as 'no wizard needed' and the studio rendered with zero models on disk. The probe is now keyed on bootstrapStage and runs when it hits 'ready' — the first moment a backend exists to answer. Normal launches (backend up quickly) behave exactly as before. Caught by running the full journey three times end-to-end: rounds 2–3 skipped Models & engines after install; with the fix the wizard mounts with models_ready=false (Whisper large-v3 listed missing). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): drop the Welcome step — wizard opens on System check The welcome act had nothing left to say: the journey rail names the stages, the setup page already oriented the user, and the cards repeated both. The wizard is now three steps — System check (auto-runs on mount) → Models & engines → Try dictation — landing the user directly on live preflight results instead of a page about the pages to come. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(setup): true unified library — models + engines as ONE list 'Merge them' meant one list, not two panels stacked — fair criticism. The wizard's Models & engines act is now a purpose-built WizardLibrary: every installable is a row of the same grammar (LED · name · chip · size · action): - required models lead (REQUIRED chip, Download action, live SSE progress bar + percent, green LED when installed) — they gate continue - TTS engines follow (ENGINE chip): active engine glows accent, available ones offer one-click Use (selectEngine), heavy installs defer honestly to Settings ('install later in Settings' + reason tooltip) - the optional-model tail folds behind 'Show N optional models' The full management surface (search, HF token, deletes, sorting) stays in Settings — a first run needs a checklist, not a store. 9 new strings × 21 locales. Verified against the live backend via the browser harness: required/installed/engine/active/Use/defer states all render in one list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(diagnostics): local-first self-check, error journal, and bug-report pipeline (#296) * feat(diagnostics): local self-check + scrubbed bug-report pipeline Closes the gap between 'something broke' and 'a useful GitHub issue exists' — entirely within the local-first constraint: the only outbound path remains the user's own browser opening a prefilled issues/new URL. Backend: - core/scrub.py: privacy scrubber for anything leaving the machine — env-var secret values (*TOKEN*|*KEY*|*SECRET*|*PASSWORD*), credential shapes (hf_/ghp_/github_pat_/sk-), home dirs on all three OSes - core/diagnose.py: 9-check self-check (device+GPU, ffmpeg, HF token, disk, data-dir writability, RAM, engine registry, hub reachability), pre-scrubbed, ASCII-safe output - GET /system/diagnose + 'python main.py --diagnose' (exit 0/1) - /system/info: hardware inventory (os_version, cpu_model, cpu_count, ram_total_gb, gpu_name, vram_total_gb, disk_free_gb), cached statics Frontend: - utils/bugReport.js: single source for the prefilled-URL builder — scrubText twin, hardware context capture, scrubbed error+stack embed, URL-length cap; ReportBugButton refactored onto it - ErrorBoundary 'Report this bug' action with the error attached - utils/errorToast.jsx toastErrorWithReport(); wired into export toasts - Settings > About 'Run self-check' with per-check status badges Tests: 27 pytest (scrub, diagnose) + 15 vitest (bugReport); existing suites green; verified live (--diagnose, TestClient, vite build). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(diagnostics): error journal, diagnostic bundle, crash notice, global handlers Second slice of the bug-tracking work — still zero outbound paths beyond the user's own browser/file manager. - core/error_journal.py: deduped ring of recent unhandled backend errors (fingerprint counts, error_class triage: GPU_OOM, HF_AUTH_FAILED, PYANNOTE_LICENSE_REQUIRED, DISK_FULL, FFMPEG_MISSING, NETWORK_ERROR), scrubbed, JSONL-persisted so the error that killed the last run survives restart. Wired into the global exception handler; 500 bodies now carry error_class; GET /system/errors/recent. - core/diagnostic_bundle.py + POST /system/diagnostic-bundle + Settings > About 'Save diagnostic bundle': zip of self-check report, error journal, scrubbed log tails — drag onto a GitHub issue; bypasses the ~8k prefill-URL ceiling. - crash-on-next-launch: /system/notifications flags a crash logged before this session started (size vs acked-size in prefs, mtime vs process start); POST /system/crash/ack; LogsFooter acks on action click. - utils/globalErrorHandlers.js: uncaught errors + unhandled rejections get a throttled, noise-filtered 'Report this bug' toast. - sidecar log parity fix: _tauri_log_candidates() now lists the Rust sidecar's backend.log/backend_err.log on Linux (XDG state dir) and Windows (LOCALAPPDATA) — sidecar crashes were only visible on macOS. Tests: +19 pytest (journal, bundle); suite at 102 passed. Vitest 124 passed; vite build green. Live-verified: journal recorded and classified a real HF 401 from the test run (HF_AUTH_FAILED, paths scrubbed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(diagnostics): breadcrumbs, deep self-check, report sweep, issue search Final slice of the bug-tracking work. - toastErrorWithReport adopted at the high-traffic failure sites: TTS generation, dub upload/ingest/transcribe, engine install, engines-matrix load, voice profile save/delete/test, batch enqueue/cancel/delete. Validation toasts and cancellations stay plain on purpose. - utils/breadcrumbs.js: local-only ring of the last 20 action names (closed-set names only — never content or paths), embedded as a 'Recent actions' section in the prefilled report. Instrumented: view changes, generate, dub pipeline, export, engine switch. - deep self-check: /system/diagnose?deep=true and --diagnose --deep load the active engine and synthesize a short utterance (num_step=4) — catches 'installed but broken'. 180s time-box, skips during model load, scrubbed failure detail. Verified live: cold-loaded omnivoice and produced 2.2s of audio in 43.9s on CUDA. - 'Search similar issues' action on the ErrorBoundary: scrubbed, noise-stripped GitHub issue search URL — dedupe before filing. - bug_report.md template now points at the diagnostic bundle and the --diagnose CLI so manual reports arrive with the same evidence. Tests: pytest 107 passed (4 new deep-check tests, CJK gate green); vitest 218 passed (breadcrumbs + issue-search suites); vite build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(diagnostics): self-diagnosis section in troubleshooting + README pointer Settings > About self-check / --diagnose / --deep / diagnostic bundle are now the documented first step before the per-error entries — and the support team's first ask on every issue. 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(setup): flush sticky action bar, global dbl-click maximize, open maximized First-run polish on the studio-console journey: - FirstRunSetup: fixed-footer / scrollable-middle layout — mast + decision grid live in a dedicated .frs__scroll region; the install action bar is the last flex item, so it sits flush at the window's bottom edge and nothing (e.g. an expanded compute-option description) can render beneath it on small windows. - Double-click-to-maximize on the custom borderless titlebar now works on EVERY drag region (splash, first-run, wizard, main header) via one delegated listener in main.jsx, on all platforms; removed App.jsx's redundant inline handler so it doesn't double-toggle. Skips interactive controls in the bar. - Window opens maximized to the available desktop size (tauri.conf.json). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(diagnostics): quiet Bandit on the journal hash and hub probe The journal fingerprint is a dedup key, not a security boundary — usedforsecurity=False. The hub reachability probe gets an explicit https scheme guard on its constant URL so the urlopen sink is audited. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(setup): address PR #295 review findings — security, lifecycle, privacy, i18n Security: - setup.rs valid_mirror: reject plaintext http:// mirror URLs (MITM supply-chain path into UV_PYTHON_INSTALL_MIRROR / UV_INDEX_URL / HF_ENDPOINT); explicit http://localhost / 127.0.0.1 / [::1] exceptions only. Tests extended incl. loopback-lookalike hosts. - setup.rs detect_hardware: AMD vendor ID alone no longer maps to kind="rocm" — a cheap ROCm userspace probe (/opt/rocm or rocminfo on PATH) gates it; bare AMD GPUs report kind="amd" so the UI offers ROCm without pre-selecting it ("matches this machine" only when verified). Functional: - lib.rs/setup.rs --setup re-entry: complete_setup now kills any backend still serving on the port before retry_bootstrap, so changed env/mirror/layout settings actually apply instead of re-attaching. - setup.rs: nvidia-smi probe runs behind a 3 s timeout thread — a wedged driver degrades to CPU instead of hanging the first-run IPC. - setup.rs: is_first_run is now a pure read; the existing-install migration write moved to migrate_existing_install_if_needed, invoked only from the bootstrap thread (get_setup_state no longer writes). - setup.rs complete_setup: config save errors now abort setup and surface in the UI instead of bootstrapping into a stale on-disk layout. - setup.rs complete_setup: logs default-vs-custom flags instead of the user's absolute env/data/models paths (privacy rule). - scrub.py + bugReport.js: also redact forward-slash Windows homes (C:/Users/<name>, file:///C:/Users/...), ordered before the macOS pattern so "C:~" residue can't form. Tests added on both sides. - bugReport.js: context fetches bounded by a 2.5 s AbortController timeout so report assembly degrades to partial context instead of hanging on a stalled backend. - system.py: crash ack is now {size, mtime} (legacy size-only ack still honored) and /system/logs/clear drops the ack — truncation can no longer permanently suppress 'crash-last-session'. - system.py: Linux Tauri-log probe honors XDG_DATA_HOME. - setup.ts/WizardLibrary.jsx: SetupProgressEvent type now documents the full phase taxonomy actually emitted (per-file start/progress/done + install_*/delete_* lifecycle); reducer verified correct against the backend stream and annotated — a file-level 'done' must not clear the repo row. - SetupWizard.jsx: step rail clamps to the highest unlocked step (preflight/models gates) — no more jumping straight to "Enter studio". Polish: - BootstrapSplash.jsx: Waveform heights wrapped in useMemo([bars]) like its siblings. - BootstrapSplash.jsx: detectHints returns i18n keys (bootstrap.hint_*) rendered through t(); translated in all 21 locales. - SetupWizard.jsx: step rail aria-label localized (setup.step_aria / setup.step_completed) in all 21 locales. - FirstRunSetup.css: deprecated word-break: break-word → overflow-wrap: anywhere; reduced-motion override also stops the frs-hw-pulse LEDs (.frs-step.is-active LED + .swiz-lib__led--busy). Deferred (design-level, follow-up PR): --setup re-entry round-tripping of custom dirs/mirrors into the form (setup.rs), and worker-thread leak on timed-out deep checks (diagnose.py). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(i18n): translate back-filled keys in all 20 locales, drop inline fallbacks The reconciliation merge back-filled 16 new keys (about.self_check*, about.*bundle*, dub.num_speakers_*, errors.*) with English text in every non-English locale — CodeRabbit flagged 9 locales; fixed all 20. Interpolation tokens preserved and asserted during the rewrite. Also removed the two inline English fallback strings in App.jsx (firstrun.first_sound_*) so copy lives only in locales/*.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: mergetest <test@local> |
||
|
|
7419986c8b |
fix(dictation): microphone permission — OS usage descriptions, WebView grant handler, actionable denied-state UI (#323) (#336)
On Windows 11 the dictation pill (Ctrl+Shift+Space) always reported "Microphone access denied" even though OS-level mic permission was granted (Voice Clone worked, backend transcribed fine). Root cause: no WebView2 PermissionRequested handler was registered, so WebView2 fell back to its own permission UI — which the 300x64 transparent, undecorated, deliberately-unfocused pill window can never host — and getUserMedia() rejected with NotAllowedError. Per-platform fixes: - Windows (WebView2): register a PermissionRequested handler on both the main and widget webviews that allows microphone/camera requests in code, for the app's own origin only (tauri.localhost + dev loopback). The Windows privacy toggle still applies on top. - Linux (WebKitGTK): the media-stream enable + permission auto-grant previously covered only the "main" window — the dictation widget is a separate WebView and was silently denied. Now applied to both. - macOS: already correct — NSMicrophoneUsageDescription ships in src-tauri/Info.plist and wry grants media capture to the app origin; documented in the shared helper. Frontend: getUserMedia failures are now mapped by error name (utils/micError.js) instead of one blanket "access denied" toast — permission denials get a per-OS "where to re-enable it" hint (Windows hint now mentions the desktop-apps mic toggle), missing devices and busy devices get their own messages, and the previously hardcoded English toast in useRecording goes through i18n. New keys added to all 21 locales. Tests: vitest unit tests for the error mapping (19 cases) and a Rust unit test for the WebView2 origin allow-list; Windows handler code cross-checked against webview2-com 0.38.2 / windows-core 0.61.2. Fixes #323 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |