dba8934aa2fc8700a33b7adce6a6737f958ae22f
67
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b7cecde57e |
feat(setup): faster downloads by default + prominent, encouraged HF-token entry (#669)
Two changes that make first-run downloads faster and easier to speed up further.
1. Segmented (multi-connection) downloader is now ON by default. The app forces
the legacy-LFS path (HF_HUB_DISABLE_XET=1) for clear progress, but that path
is single-stream and slow — which is why downloads felt sluggish. The built-in
IDM/uGet-style segmented accelerator (parallel byte-ranges, live speed/ETA)
was already implemented but defaulted OFF. Flip it ON: it only engages when
Xet is inactive (the default), and ANY failure falls back to snapshot_download
("can never compromise a correct install"). Pure-httpx, cross-platform,
auth-safe (token never forwarded to a CDN). Override with
OMNIVOICE_SEGMENTED_DOWNLOAD=0.
2. The Hugging Face token field is now a prominent, always-visible card right
above Continue — was a collapsed "advanced" fold almost nobody opened. A free
token gives authenticated downloads (higher rate limits, fewer stalls), so it
pairs with change #1 to keep the parallel fetch from getting throttled. The
card leads with the speed benefit, shows a saved-state, and adds a one-click
"Get one free →" link to huggingface.co/settings/tokens.
Docs: downloading-models.md updated — the legacy-LFS section now documents the
default-on segmented accelerator + the HF-token speed tip, and the tuning table
reflects OMNIVOICE_SEGMENTED_DOWNLOAD=0 as the disable knob (docs-sync).
Test: test_segmented_download_default.py pins the new default ON and that the
env override still disables it; existing FDL-08 behavior tests stay green.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
252f0d4fac |
fix(asr): bound whole-file transcription so a stall isn't reported as "can't reach backend" (#656)
A Windows/CUDA user (Vietnam) hit "Can't reach the local backend" only when dubbing/transcribing. Their log proves the backend started fine — model loaded, preload complete, 25 models — and the log ends right after `whisperx transcribing …tmp.wav`. The backend was alive; the *transcription* stalled (large-v3 ASR contending with the resident TTS model for VRAM on an 8 GB-class GPU), which the UI surfaces as an unreachable backend. Root cause (class, not instance): the chunked dub pipeline already bounds each chunk (OMNIVOICE_TRANSCRIBE_CHUNK_TIMEOUT_S), but the *whole-file* transcribe paths ran unbounded: - dub QC re-transcribe (dub_export) - dictation (capture) - OpenAI-compat /audio/transcriptions A slow/stuck transcribe on any of these hung the request AND held a GPU-pool worker — indistinguishable from a dead backend. Fix: add run_transcribe_guarded() in services/asr_backend.py — a shared asyncio.wait_for wrapper (ASRTimeoutError, a TimeoutError subclass) with a generous env-tunable bound (OMNIVOICE_ASR_TRANSCRIBE_TIMEOUT_S, default 300 s). On timeout the request returns 504 with actionable guidance (backend is alive; free VRAM / pick a smaller ASR model / use CPU; restart to clear the stuck worker) instead of hanging forever. Wired into all three whole-file paths. Docs: new troubleshooting §14 — "Can't reach the local backend during transcription/dubbing" — explains it's ASR weight/VRAM pressure, not a network/ mirror problem, and corrects the misconception that a "Network → Restricted/Global mirror" Settings toggle exists (the Network control is LAN sharing). Serves the #602/#585/#567 "can't reach backend" cluster. Test: backend/tests/test_asr_transcribe_timeout.py — slow fn raises ASRTimeoutError with the actionable message, fast fn passes through, subclass-of-TimeoutError so the openai_compat broad catch still maps to 504. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
79f3e35682 |
docs(troubleshooting): add stuck-download / incomplete-cache recovery (#622) (#643)
The 'stuck on the download page, model folder has only refs/ no weights' case (a connection dropping/blocking mid-pull) is a recurring support report but wasn't in the install troubleshooting guide. Add section 13 with the recovery steps + antivirus/VPN/mirror escalation + a huggingface-cli manual fallback. Docs-only; no version bump. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0e17caa52a |
fix(install): actionable torch-wheel-download failure + local-wheel recovery (#569) (#574)
#569: on a restricted network the first-run install fails downloading the ~2.5 GB cu128 PyTorch wheel from download.pytorch.org, and the app won't launch. Two problems: the error told users to "set UV_DEFAULT_INDEX to a mirror" — which CANNOT redirect torch, because it comes from a *named, explicit* uv index (uv 0.11 rejects index-name override values and `--frozen` pins the exact wheel URLs); and there was no way to supply a manually-downloaded wheel. - Detect a torch/pytorch-host `uv sync` failure and emit torch-specific guidance (Clean & Retry → VPN → drop the wheel locally) instead of the wrong mirror advice. - Add a local wheel-drop dir `<env_root>/wheels` (survives Clean & Retry) wired via `UV_FIND_LINKS`. On a frozen-sync torch-download failure WITH wheels present, retry NON-frozen with find-links so uv re-resolves from the local wheels. Verified empirically: a non-frozen find-links sync installs from a local wheel fully offline, while a `--frozen` sync ignores find-links — so the retry is the only mechanism that can consume a dropped wheel. Best-effort: if it can't satisfy, it fails identically to before and the actionable error still fires. - docs/install/troubleshooting.md: new "#12 CUDA PyTorch wheel download fails" entry (docs-sync) — the offline wheel-drop path + why a PyPI mirror can't fix this index. Note: an automatic mirror redirect for the cu128 index is intentionally NOT shipped — uv provides no working override for a named explicit index, so it couldn't be verified; the offline wheel path is the reliable escape hatch. Test: sync_failure_is_torch_download host/keyword detection + negative guard. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3777d3a62c |
feat(tts): add MOSS-TTS-v1.5 (8B) and dots.tts (2B) as opt-in engines (#498)
Adds two zero-shot voice-cloning TTS engines requested in #498, both opt-in and subprocess-isolated with their own dedicated venv — the same pattern as IndexTTS-2. The dedicated venv is forced, not just chosen: each upstream pins a transformers version that conflicts with the parent's >=5.3 (MOSS-TTS-v1.5 ==5.0.0, dots.tts ==4.57.0), so they cannot share the parent interpreter. Because they use the clone+venv bootstrap (env var -> clone -> uv venv), this touches no pyproject.toml / uv.lock / bun.lock — `uv sync --all-extras` and Docker's `bun install --frozen-lockfile` are unchanged, so main's CI/Docker matrix stays green. Engines: - moss-tts-v15: 8B, 31 langs, ~16 GB weights, 24 kHz. AutoModel/ AutoProcessor via trust_remote_code. gpu_compat=(cuda,cpu) — MPS is undocumented/untested upstream so it is never claimed; on a Mac it runs on CPU. Apache-2.0, no license gate. - dots-tts: 2B, 24 langs, ~9 GB weights, 48 kHz. DotsTtsRuntime; continuation cloning (prompt_audio_path+prompt_text). Upstream is Linux/macOS-only, so is_available() gates it off cleanly on Windows (cross-platform parity rule — it is opt-in, never a broken default). Wiring: registered in _LAZY_REGISTRY + _INSTALL_HINTS. list_backends() surfaces both as subprocess/[cuda,cpu]/available-until-installed; the data-driven Settings engine picker needs no frontend change. Tests (19, fail-before/pass-after): registry resolution, subprocess marker, no-MPS gpu_compat, the Windows gate, not-installed honesty, and the parent-side generate() kwarg arbitration. Existing engine suite still 55 passed / 5 skipped. Sidecar inference follows the upstream-documented APIs but, like IndexTTS/Supertonic, can't be executed in CI without the multi-GB model clones. Docs (same-PR per docs-sync rule): README + README_CN engine tables, new docs/engines/moss-tts-v15.md + dots-tts.md, disk-usage.md (torch-dedup note), CHANGELOG. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e58106d552 |
fix(updater): preview channel builds nightly from main + prerelease/parity guards (#500)
The Preview update channel was effectively dead: its only build trigger was a manual workflow_dispatch, so "preview = main" was never enforced — the live preview manifest was stuck at 0.3.5-41 (June 7) while main moved to 0.3.7. It also shipped two latent hazards: the `preview` GitHub release had drifted to isPrerelease=false (a non-prerelease `preview` is eligible to become GitHub's "Latest" — the exact URL the *stable* updater reads, so it could hijack the Stable channel), and its updater manifest dropped darwin-x86_64 (Intel-Mac preview users silently got no updates — a cross-platform-parity breach). Changes (release.yml): - Add a nightly `schedule` (07:00 UTC) that rebuilds the rolling `preview` prerelease from main. A new `preview-gate` job no-ops the 4-platform matrix on nights when main didn't move, so idle days cost only a ~30s gate job. - Centralize the preview-vs-stable decision in `preview-gate.outputs.is_preview` (schedule OR workflow_dispatch+publish_preview), consumed by the stamp step, tauri-action, and preview-notes — replacing the repeated inline conditions. - Harden the prerelease flag: preview-notes' `gh release edit` now re-asserts `--prerelease` every run, and a new post-publish step fails the run if the preview release isn't a prerelease or its manifest is missing any platform stable ships (catches the Intel-Mac regression in CI). Docs (docs-sync): update docs/update-channels.md — previews are no longer "manual / no scheduled spend"; they build nightly from main (+ on demand). The live `preview` release was re-flagged prerelease out-of-band to close the hazard immediately; this makes it recurrence-proof. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
af3da58584 |
fix(bootstrap): force-reinstall setuptools so pkg_resources repair actually works (#248) (#495)
The auto-repair ran `uv pip install setuptools>=75,<80`, which `uv` treats as "already satisfied" (no-op, "Checked 1 package in 5ms") whenever setuptools' *metadata* is present but its `pkg_resources` files are gone — the common cause being Windows Defender quarantining `pkg_resources/`, or a partial extract on a restricted network. So the repair never restored the files, the post-check failed, and users hit the #248 dead-end. The error message *also* told them to run the same no-op command, so the suggested manual fix didn't work either (reported on Discord, Win11 + RTX 5070 Ti). Fix: both repair sites in bootstrap.rs now use `--reinstall` (the flag already used for the ROCm torch repair), which force re-extracts pkg_resources even when uv thinks setuptools is satisfied. The fail() message and the failure.py hint now suggest `uv pip install --reinstall 'setuptools>=75,<80'` + an antivirus-exclusion note, and docs/install/troubleshooting.md (#pkg_resources-missing) is updated with the real cause (metadata-present/files-missing) + AV guidance. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3656f0a4ef |
fix(profiles): decouple design-profile save from TTS render (#476) (#488)
* fix(profiles): decouple design-profile save from TTS render (#476) Saving a design voice profile forced a full TTS model load + inference to render a deterministic identity sample. On a fresh model-less image (Docker first-run) that 503'd, so the save failed. A secondary guard also rejected an all-Auto design (empty instruct) with a 422. Saving a design profile is now a pure persistence operation: - The seed-42 identity sample render is attempted opportunistically but is non-fatal — if the engine isn't ready the row is persisted with ref_audio_path=NULL (sample pending). The row's vd_states + instruct already make the voice fully usable (generation.py falls back to instruct-only conditioning for design profiles with no ref audio). - The sample is rendered lazily + cached on the first GET /profiles/{id}/audio request; if the engine is still unavailable that path returns a precise "model not ready — finish setup / download a model" 503. - The all-Auto (empty-instruct) design is now saveable (vd_states still required). Adds tests/test_profile_design_save_decouple.py (top-level tests/, asyncio.run per test) covering: design save with model unavailable creates the row instead of 503-ing; all-Auto design is saveable; the pending sample materializes on first /audio request. Updates the unification spec (docs-sync). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(profiles): contain profile-audio paths under VOICES_DIR (CodeQL CWE-22) The lazy design-sample path was built as `os.path.join(VOICES_DIR, f"{profile_id}.wav")` / `os.path.join(VOICES_DIR, audio_file)` where profile_id is the request path param — CodeQL flagged 5 high-severity path-injection alerts (profiles.py + the taint flowing into archetypes.py's torchaudio save). Add `_safe_voice_path()` (basename + safe-char sanitise + realpath containment, mirroring core.config.dub_seg_path) and route both the read and lazy-render sites through it; a traversal id now 404s instead of escaping VOICES_DIR. Regression test covers the containment guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(profiles): use CodeQL-recognized path-injection guards (CWE-22) The previous `_safe_voice_path()` helper was correct (basename + realpath containment) but CodeQL's taint tracking didn't propagate the barrier through the function return, so the 5 path-injection alerts persisted. Switch to guards CodeQL recognizes, inline at each file-op site: - validate `profile_id` against the generated-id charset (`[A-Za-z0-9_-]{1,64}`) with `re.fullmatch` and 404 on mismatch (covers the `f"{profile_id}.wav"` render path); - read only `os.path.join(VOICES_DIR, os.path.basename(name))` so a stored/derived filename is always a direct child of VOICES_DIR (covers the read + the taint flowing into archetypes.py's torchaudio save). Drop the helper. Test now asserts a traversal/separator/NUL profile_id 404s at the guard. Same security property, recognized by CodeQL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(profiles): inline realpath+commonpath containment for CodeQL (CWE-22) CodeQL didn't recognize the earlier sanitizers — neither the helper (barrier hidden behind a function return) nor os.path.basename / a cross-function regex guard cleared the 5 path-injection alerts. Use the canonical, CodeQL-recognized form INLINE at each file-op site: resolve the path with os.path.realpath (which collapses any `..`) and confirm os.path.commonpath((base, path)) == base before the read / the render, returning 404 / raising on escape. Same property the helper had, now in a shape CodeQL's taint tracking follows. Keeps the profile_id charset guard as defense-in-depth. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(profiles): route design-sample path through shared _voices_path guard (#476) The inline realpath+commonpath containment in get_profile_audio and _materialize_design_sample wasn't recognized by CodeQL as a path-injection sanitizer (5 new high-severity py/path-injection alerts at the file-op sites, incl. archetypes.py mkdir via the rendered Path). Both now reuse the existing _voices_path() helper, which applies the os.path.basename() barrier plus symlink-resolved containment — the same guard the consent endpoint uses and that CodeQL already accepts. Behavior is unchanged: the DB columns only ever hold bare {profile_id}.wav filenames, so basename() is a no-op here. Tests: tests/test_profile_design_save_decouple, test_profile_unification, test_profile_consent, test_archetype_blank_guard — 25 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d0e1c19e88 |
docs(persona): document the .ovsvoice portable format (#29 slice D) (#464)
- docs/persona-format.md: export (privacy/include-reference, watermarked preview), import (consent/verification non-forgeability rule), the ZIP layout table, SPDX-license semantics (metadata only), and the local-first / zero- network guarantee. Notes legacy .omnivoice compatibility. - CHANGELOG.md: [Unreleased] → Added entry for portable personas. Satisfies the docs-sync hard rule for the new bundle format. |
||
|
|
e0b59f3984 |
docs(longform): implementation specs for the 14 roadmap tasks (#21–#34) (#429)
* docs(longform): implementation specs for the 14 roadmap/integration tasks (#21–#34) Per-task implementation specs under docs/specs/longform/ for the remaining longform + #346-roadmap work: GPU compat matrix, shared VoiceSelector, Transcriptions import, Story⇄Audiobook export, inline Create Voice, gallery handoff, parser unification, two-pass ACX, .ovsvoice format, Dub→Stories, unified LongformProject store, phone calls, cue-sheet, runtime-verify. Authored by a draft + iterative-refinement workflow (codebase-grounded: exact file:line anchors, API/data shapes, test plans, constraints, deps, risk, PR slices). NOTE: the 10-round refinement was cut to ~rounds 4–5 by an account session limit; rounds 5–10 (incl. the final de-bloat/polish pass) are pending — the specs carry per-round revision-note preambles that the polish round trims. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(longform): strip accreted (this-revision) note preambles from specs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4cc55ab852 |
Fast model downloads: Xet fast path + accurate progress (FDL W0–W2 + W4) (#424)
* feat(downloads): Xet fast path + accurate progress (FDL W0–W2)
Make model downloads fast and show accurate downloaded/remaining/speed.
Research confirmed hf-xet already implements the IDM/uGet technique
(content-defined chunking, parallel byte-range gets, dedup, resume), and
the spike found all 25 catalog repos are Xet-backed — so the win is
driving Xet well + accurate progress, not a custom downloader.
W1 — maximize + guarantee Xet:
- pin huggingface_hub>=1.7 + hf-xet>=1.1 (was transitive); no hf_transfer
- drive snapshot_download with explicit tqdm_class + max_workers + endpoint
- opt-in HF_XET_HIGH_PERFORMANCE / HDD sequential-write knobs (default off)
- /system/info reports fast_download {xet_enabled, xet_version, high_perf}
W2 — accurate progress:
- dry_run preflight -> install_plan event (exact total/cached/remaining)
- utils/download_aggregator.py: one overall bar; byte bars (by id) vs the
"Fetching N files" count bar; windowed rate; emits one 'aggregate' event
- frontend overall bar (speed/remaining/ETA), cached-skip, ⚡ fast badge
Known limit (verified live): under Xet+hf_hub 1.7.2 per-file byte bars
never advance/close via tqdm, so mid-download the bar is file-granular and
bytes flush to the exact total on completion. Classic-LFS/mirror repos get
true byte progress (W4).
Drive-by: download.py used os.walk without importing os (latent NameError
in _validate_snapshot_has_weights on every install) — fixed.
Tests: tests/backend/setup/test_download_preflight.py (10). Spike + plan
under .planning/quick/260613-fdl-fast-model-downloads/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(downloads): opt-in mirror + cancel + docs (FDL W4)
- mirror (FDL-10): snapshot_download(endpoint=) honours prefs hf_endpoint /
env HF_ENDPOINT on preflight + download (per-call, no process-wide env).
Documented as the classic-LFS path (no Xet) for restricted networks.
- cancel (FDL-11): POST /models/install/cancel {repo_id} stops further
retries at the next boundary, emits install_cancelled, clears the cooldown
(cancel is intent, not failure). Frontend treats it as a terminator.
- docs (FDL-12): docs/downloading-models.md (Xet fast path, progress
semantics + byte-speed limitation, opt-in tuning, mirror, cancel,
troubleshooting) + README pointer. Docs-sync rule satisfied.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(planning): model-management v2 cleanup plan (mm2)
GSD plan for cleaning the model-management subsystem: registry unload-on-
switch + per-engine unload() (fixes VRAM leak), model_lifecycle facade,
unified idle/timeout config, bounded cooldowns, sidecar VRAM self-report,
cache-fallback logging. Planning artifact only — no code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(downloads): reconcile with main's HF_HUB_DISABLE_XET; honest status
Rebasing onto main surfaced that main forces HF_HUB_DISABLE_XET=1 (classic
LFS) because Xet progress bypasses the tqdm hook — the same limitation found
here. Reconcile instead of fight:
- /system/info fast_download now reports runtime truth: xet_installed +
xet_active (installed AND not HF_HUB_DISABLE_XET) + xet_enabled alias. The
⚡ badge only shows when Xet actually runs; startup log says
"downloads: Xet disabled → legacy LFS".
- complete(): clear the rate window before the final flush so crediting the
full size in one step can't emit an absurd instantaneous rate.
- docs/downloading-models.md rewritten: default is legacy LFS for accurate
progress; Xet is opt-in via HF_HUB_DISABLE_XET=0. hf-xet pin stays (ready
for a future Xet progress hook).
W2 (preflight total/remaining + aggregate bar + exact completion) is the
value on either path; W1's "maximize Xet" is dormant by main's design.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(downloads): opt-in segmented multi-connection accelerator (FDL W3)
Since main forces Xet off (HF_HUB_DISABLE_XET=1), the default path is
single-stream legacy LFS — so a segmented downloader is the way to get BOTH
parallel speed and live byte progress.
- services/segmented_download.py: async multi-connection Range downloader for
one file — parallel byte-ranges, resume (.part + manifest), per-segment
short-read truncation guard, optional sha256/etag verify, cancel, and a
single-stream fallback when the server won't range. Auth-safe: the HF
Authorization header is sent only to huggingface.co/hf.co and never
forwarded to a CDN host on redirect (unit-tested).
- dispatch (download.py): opt-in via prefs segmented_downloader / env
OMNIVOICE_SEGMENTED_DOWNLOAD (default off). When on and Xet inactive,
fetches each file into the HF cache mirroring hf_hub_download (blobs +
snapshot symlinks + refs/main), feeding real bytes to the aggregator. Any
failure falls back to snapshot_download — never breaks a correct install.
- fix: complete() was adding a full total on top of accumulated segmented
bytes (2x); now replaces byte bars so the sum is exactly total.
Verified live (accelerator on): real byte progress to ~16.6 MB/s, final
bytes==total, /models installed=True, delete frees correctly.
Tests: test_segmented_download.py (7) + aggregator double-count regression.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(downloads): relocate FDL tests to top-level; loop-isolate segmented test
CI runs the full suite, which exposed a pre-existing test-isolation leak:
several tests/backend/** fixtures purge core.*/services.* from sys.modules
under a temp OMNIVOICE_DATA_DIR and never restore, leaving core.config/core.db
bound to a dead temp dir. It only bites when collection order puts a purging
test ahead of a real-DB reader (test_longform_jobs). Adding tests under
tests/backend/setup/ reordered collection and tripped it.
Fix without touching the shared (fragile) fixtures or risking class-identity
breakage from a blanket sys.modules restore:
- move the two FDL test files to top-level tests/ (tests/test_fdl_*.py) so
tests/backend/** collection order is identical to main — longform passes.
- rewrite the segmented test to run each case under asyncio.run() (fresh loop)
instead of asyncio.get_event_loop(), which an earlier async test can leave
closed in the full suite.
Full suite green locally: 1364 passed, 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
e9481ef307 |
feat(longform): shared render core — loudness, metadata, cover art (PR 1/8) (#408)
First slice of the Stories+Audiobook convergence (spec: docs/specs/2026-06-13-stories-audiobook-maturity.md). Both features will compile to one server-side chapterized renderer; this lands the shared pure builders and wires them behind Audiobook. New `backend/services/longform_render.py` (all pure, unit-tested without ffmpeg/torch): - build_ffmetadata(chapters, global_meta) — FFMETADATA1 with an optional global tag block (title/author→artist/narrator→composer/year→date/genre/description→ comment) + chapter table. - build_loudnorm_filter(preset) — `-af loudnorm` for ACX (~-19 LUFS, -3 dBTP) or podcast (-16 LUFS); off/unknown → None. Opt-in, so default behavior stays platform-identical. - validate_cover_image — jpg/png + 8 MB cap guard. - build_render_cmd — generalizes the m4b mux: m4b|mp3, optional cover (attached_pic) + loudness, bitrate validated. - build_concat_list — moved here. `services/audiobook.py`: build_chapter_ffmetadata / build_m4b_cmd / build_concat_list are now backward-compatible wrappers over the core (existing imports + tests unchanged). `POST /audiobook`: now accepts optional `format` (m4b|mp3), `loudness`, `cover_path`, and `metadata` and passes them through — backend-complete; the UI for these lands in PR 2. Tests: tests/test_longform_render.py (28) + existing test_audiobook.py (11) green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6704d062fc |
fix(persona): preserve design kind + vd_states across share/import (Wave 5 §R3) (#405)
The persona-gallery surface already exists (VoiceGallery Community zone + community.py manifest + marketplace .omnivoice bundles). The blocker for §R3's 'synthetic-only' gate was data integrity: a *designed* persona lost its kind='design' (and vd_states) when imported from the community gallery or round-tripped through a bundle — silently demoting it to a clone. - community.py /use: a 'preset' (rendered from instruct) imports as kind='design'; a 'voice' (real reference clip) as 'clone'. - marketplace.py: extract a pure _bundle_metadata() (dedupes export+publish) that captures kind + vd_states; import restores them. Old bundles without the keys import as 'clone' (backward-compatible). This makes 'accept only designed/synthetic voices' enforceable instead of everything defaulting to clone. No new persona-gallery feature was built — that would duplicate the existing community/marketplace surface. 4 torch-free tests (isolated DB): _bundle_metadata captures design + defaults to clone; import round-trip preserves design kind+vd_states; legacy bundle → clone. docs §R3 status updated. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
151f73f794 |
feat(audiobook): Audiobook tab — script → plan → m4b (Wave 5 UI) (#404)
Frontend for the audiobook backend (#402/#403): a dedicated Audiobook tab. - pages/AudiobookTab.jsx: script textarea + default-voice picker (reuses the app's profiles), 'Preview plan' (POST /audiobook/plan → chapter list) and 'Create' (POST /audiobook → reads the SSE stream, shows per-chapter progress + assembling, then an <audio> player + m4b download via the /audio mount). - api/audiobook.ts: typed plan() + generate() (returns the raw streaming Response). - utils/sseParse.js: pure splitSSEBuffer/parseSSELine helpers for reading the POST event-stream (EventSource is GET-only) — unit-tested (the buffer/line handling is the easy thing to get subtly wrong). - NavRail + App.jsx wiring (lazy tab, hideSidebar); i18n keys in en.json. All strings via i18n (CJK gate green). 7 new SSE tests; full vitest 326 + vite build green. Runtime-unverifiable here (Tauri webview) — wants an in-app pass. docs §R3 updated. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
9441274ab6 |
feat(audiobook): synth job → chapterized m4b, SSE progress (Wave 5) (#403)
Completes the audiobook backend: POST /audiobook renders each chapter through the active TTS engine (synthesize_chapter + chunked_tts), writes per-chapter WAVs, then muxes a chapterized m4b (FFMETADATA1 chapters via build_m4b_cmd + concat demuxer). Progress streams as SSE (started/chapter/assembling/done/ error), recorded to job_store. ffmpeg-gated — emits an error event and stops when ffmpeg is absent (m4b is the only output). - services/audiobook.build_concat_list: pure ffmpeg concat-list builder with proper single-quote escaping (no arg injection). Unit-tested. - router: voice resolution (compact form of generation.py's locked/design/ clone cases) cached per id; OmniVoice native model path + generic TTSBackend path; chapter synthesis runs on the GPU pool, ffmpeg via run_ffmpeg. Reuses the tested building blocks from #402 (parser, synthesize_chapter, FFMETADATA + m4b argv builders) — the new router glue is thin and import-checked by CI. Deferred: epub/pdf ingest, ACX loudnorm mastering, crash-resume, UI. 15 audiobook tests (added concat-list); docs §R3 updated. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
34b47282af |
feat(audiobook): chapterized audiobook core + plan preview (Wave 5) (#402)
* feat(audiobook): chapterized audiobook core + plan preview (Wave 5) First cut of the long-form vertical (parity §R3). Engine-agnostic core in services/audiobook.py: - parse_audiobook_script: pure parser. Markdown '# H1' headings → chapters; inline [voice:NAME] switches the narrator; [pause …] is delegated to the shared omnivoice.utils.text.parse_pause_markers so audiobooks and single-shot synthesis keep one pause dialect. Returns a chapter/span plan. - synthesize_chapter: orchestration via an injected synth(text, voice) callable (reuses chunked_tts split + crossfade, stitches inter-span silence) — so it's unit-testable with a stub backend, no model/GPU. - build_chapter_ffmetadata + build_m4b_cmd: pure FFMETADATA1 [CHAPTER] builder and faststart-m4b concat-demux argv (bitrate-validated, no injection). POST /audiobook/plan returns the parsed plan (no TTS/ffmpeg, no side effects). Deferred (follow-ups): the streaming synth job + chapterized-m4b run, epub/pdf ingest (new dep), ACX loudnorm mastering, crash-resume, UI. 14 tests: parser (chapters/voice/pause/intro/empties/to_dict), FFMETADATA offsets+escaping, m4b argv + bitrate guard, and stub-synth orchestration (span+silence stitching, voice threading). docs §R3 status updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(audiobook): linear-time regexes (CodeQL ReDoS) CodeQL flagged polynomial backtracking on user-provided input in three regexes reachable from the new POST /audiobook/plan endpoint: - _VOICE_RE: \s*(...)\s* → single [^\]]* class, stripped in code. - _HEADING_RE: trailing [ \t]* removed; title captured greedily + stripped. - _PAUSE_RE (omnivoice/utils/text.py): the numeric spec is now an atomic group (?>…) so its leading \s+ can't backtrack against the trailing \s*. Behavior-preserving (Python >=3.11 already required); 14 pause tests + 14 audiobook tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(audiobook): require non-space heading title start (CodeQL ReDoS) The previous _HEADING_RE '[ \t]+(.+)' still let the leading whitespace class and the title '.+' both match the same tab run (overlap → polynomial). Anchor the title capture with \S so the two can't overlap. 14 audiobook tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(audiobook): exclude '[' from voice-tag content (CodeQL ReDoS) [^\]]* still matched '[', so a run of nested [voice: prefixes produced overlapping finditer match attempts → O(n^2). Excluding both brackets ([^\]\[]) makes matches non-overlapping and linear. A voice name never contains a bracket. 14 audiobook tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
34c8ab2409 |
feat(engines): idle-reap subprocess-engine sidecars to free VRAM (Wave 13) (#401)
Parity Action 13 (dynamic load/unload), subprocess-engine half. A subprocess engine's sidecar holds a process — and, for GPU engines, VRAM — for the life of the backend, even after the user switches engines. The default in-process OmniVoice model already idle-unloads (model_manager.idle_worker); this gives the subprocess engine class the same treatment. subprocess_backend gains a background reaper (lazy daemon thread, started on first spawn) that shuts down sidecars idle past OMNIVOICE_SIDECAR_IDLE_TIMEOUT_S (default 300 s; <= 0 disables). The next request transparently respawns one via the existing dead-process relaunch. Safety: the reaper only acts while holding the per-backend lock acquired NON-blockingly, so it can never run mid-op — if an op holds the lock it skips that backend this round. Reuses the idempotent shutdown() (which doesn't take the lock, so no re-entrancy). Each backend tracks last-use and registers in a weak live-set. Scope: subprocess engines only (the heavy, VRAM-holding, process-isolated class). In-process non-default engines and cross-engine VRAM preemption remain TODO — get_active_tts_backend returns a fresh instance per call, so those need an instance-tracking refactor. 6 reaper tests via the stdlib echo sidecar (no torch): kills idle, respawns, skips busy (lock held), recent-use kept, disabled at <=0, ignores dead. The 3 subprocess suites pass together (24). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
7d7d07c8fc |
feat(dictation): wire AEC end-to-end in the frontend (Wave 8, opt-in) (#400)
Completes Action 8: dictate-over-playback echo cancellation now works
end-to-end, gated behind a new off-by-default 'aecEnabled' pref so the
standard dictation + playback paths are untouched when off.
- utils/aec/{pcm,farEndBus,micCapture,playbackTap}.js + public/aec-worklet.js:
AudioWorklet captures the mic as raw int16 PCM; a player tap routes playback
output through Web Audio to a singleton far-end bus. Pure framing/encode
helpers are unit-tested.
- CaptureWidget: when aecEnabled, opens /ws/transcribe?aec=1, streams tagged
PCM (0x00 mic / 0x01 far-end) instead of MediaRecorder/WebM. Default path
unchanged; no POST fallback in AEC mode (the WS is the sole channel).
- WaveformPlayer: while actually playing AND aecEnabled, taps its decoded
output as the echo reference. Gated on isPlaying so only the one active
player holds an AudioContext (well under the browser cap); audio stays
audible (source always reconnected to destination).
- Settings → Capture: AecPanel toggle. prefsSlice: aecEnabled (persisted).
Runtime-unverifiable here (jsdom has no Web Audio); needs in-app testing in
the Tauri shell. 7 new pure-helper tests; full vitest (319) + vite build green.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
e8705a106d |
feat(dictation): opt-in NLMS AEC for dictate-over-playback (Wave 8b) (#399)
* feat(dictation): opt-in NLMS AEC for dictate-over-playback (Wave 8b) Dictating while OmniVoice plays audio (TTS preview, dub, video) leaks the loudspeaker signal into the mic, and the streaming ASR transcribes that bleed. Browser echoCancellation varies per platform/webview — it can't be a cross-platform default — so this adds a server-side canceller that behaves identically everywhere. services/aec.py ports Patter's NlmsEchoCanceller (MIT): a time-domain NLMS adaptive filter with a Geigel double-talk detector, warm-up step ramp, and far-end staleness pass-through. /ws/transcribe gains an opt-in '?aec=1[&sr=]' mode: frames are raw int16 mono PCM tagged with a 1-byte prefix (0x00 mic, 0x01 playback reference); the mic is cleaned against the reference before buffering, and the cleaned PCM is muxed via stdlib wave (not ffmpeg). Without the param the protocol and behaviour are byte-for-byte unchanged. Backend ships dark (no new deps — numpy already pinned); frontend far-end streaming is a follow-up. Tests cover echo attenuation, double-talk preservation, cold/stale pass-through, param validation, and the framing helpers — all pure-numpy/stdlib so they skip the torch ASR stack. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(capture_ws): stubs accept the new pcm_sr kwarg _transcribe_buffer/_transcribe_buffer_full gained an optional pcm_sr kwarg for the AEC PCM path; the protocol-test stubs had fixed signatures and raised TypeError on it, so the handler sent 'error' instead of 'final'. Accept **kw in the stubs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
a330c9774c |
docs(spec): Voice Console 10/10 polish spec (#394)
* docs(spec): Voice Console 10/10 — pinned action bar, two-kicker hierarchy, unified presets, identity-first right rail Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(spec): ASCII '+' in wireframes — clears the CJK gate Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- 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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
ea26893bfc |
fix(scripts): desktop-prod works from cmd/PowerShell via cross-platform launcher (#282) (#333)
`bun run desktop-prod` (and its :run/:upgrade/:pill/:run:pill variants) invoked `bash scripts/desktop-prod.sh` directly. On Windows, cmd and PowerShell have no `bash` on PATH unless Git Bash happens to be there, so the documented from-source install path died with a cryptic spawn failure before printing anything — the exact first step in issue #282's repro. Add scripts/desktop-prod.mjs, a tiny launcher (runs under bun or node): - macOS/Linux: execs the bash script unchanged — zero behavior change. - Windows: locates Git Bash via `where.exe bash`, well-known Git for Windows install paths, or derived from git.exe's location; explicitly skips C:\Windows\System32\bash.exe (the WSL launcher, which would run the script inside Linux and wipe/launch the wrong paths). - No usable bash: prints an actionable error (install Git for Windows, use `bun run desktop`, or use the installer) instead of a spawn error. All flags are forwarded untouched and the child's exit code is propagated. scripts/desktop-prod.sh itself is unchanged, and docs/install/windows.md now lists Git for Windows as a prerequisite for from-source installs. Refs #282 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
63a0d00f09 |
fix(asr): PyTorch-Whisper fallback works without cuDNN 8 or preload (#255) (#268)
Windows + NVIDIA users hit `Could not locate cudnn_ops_infer64_8.dll`: WhisperX/faster-whisper run on CTranslate2, which needs cuDNN 8, but PyTorch 2.8 ships cuDNN 9 and the side-loaded `cudnn8_compat` libs were missing from the venv. The PyTorch-Whisper backend should have been the fallback, but it errored "set OMNIVOICE_PRELOAD_TTS_ASR=1" because it only worked when the TTS model preloaded an ASR head. - `PyTorchWhisperBackend._ensure_pipe()` now builds its OWN transformers ASR pipeline on demand (PyTorch stack → cuDNN 9, no CTranslate2/cuDNN-8), without loading the full TTS model and without the preload env var. A constructor- passed pipe (when the TTS model already has one) is still reused. Model is overridable via OMNIVOICE_PYTORCH_ASR_MODEL. - dub_core transcribe preflight no longer hard-rejects pytorch-whisper when no pipe is preloaded — it lazy-loads; any failure surfaces per-chunk with the real cause. So a Windows box without cuDNN 8 can switch ASR backend to "PyTorch Whisper" in Settings → Models and transcription works. Docs: troubleshooting entry. Tests: tests/test_pytorch_whisper_fallback.py (lazy standalone build, reuse of a passed pipe, no get_model() call, env override). Full tests/ suite: 700 pass. Does NOT close #255 — pending the reporter confirming the fallback works on their machine; the cuDNN-8 install gap (faster-whisper path) is a follow-up. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b4f1fe18d7 |
fix(server): relax loopback gate in headless server mode so Docker admin UI works (#261) (#263)
In Docker the loopback origin gate (`require_loopback`) is unenforceable: Docker's NAT rewrites `request.client.host` to the bridge gateway (e.g. 172.17.0.1) even for a localhost-only `-p 127.0.0.1:3900:3900` mapping, so every request looks non-loopback. The gate then 403s the operator out of the routes the web UI needs — `/system/*` (incl. `/system/info`, which left the version blank, re-breaking #249 in Docker) and `/api/settings/*` (HF-token entry) — surfacing as "Loopback origin required" all over the UI. Fix: add an explicit, opt-in `OMNIVOICE_SERVER_MODE` flag. When set, `require_loopback` becomes a no-op; exposure is then governed by the operator's port mapping plus the optional share PIN (NetworkAccessMiddleware still 401s unauthenticated non-loopback clients whenever a PIN is set). The Docker image sets `OMNIVOICE_SERVER_MODE=1` (Dockerfile + documented in compose). Security: the desktop build NEVER sets this, so its loopback boundary is unchanged — LAN share guests are still denied the admin/system routes. New unit tests lock the contract (strict 403 by default incl. the PR #81 vectors; relaxed only under the flag). Existing non-loopback 403 tests still pass. Docs: docker.md troubleshooting entry for "Loopback origin required". Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
621c263354 |
fix(version): surface running version in web/Docker UI + hide desktop-only updater (#249) (#258)
The Docker web build has no Tauri runtime, so Settings → About → Version read `getVersion()` (Tauri-only) and rendered a dash — leaving Docker users unable to tell which version they were running (issue #249). The update-channel toggle was also shown there even though the auto-updater is desktop-only. - Backend: expose the single-source `APP_VERSION` over HTTP — add it to `/system/info` (`app_version`) and `/health` (`version`). Both are model-free and the latter is zero-auth. - Frontend: Settings → About → Version falls back to `info.app_version` when no Tauri `getVersion()` is available, so Docker shows the real 0.3.x version. - Frontend: hide the update-channel toggle, update-endpoint row, and the "Check for updates" button outside Tauri — the Docker image updates by pulling a new tag, not via the in-app updater. - Docs: fix the wrong package name in the version-check command (`omnivoice-studio` → `omnivoice`) and document the new `/health` version field + the in-UI version row. Tests: assert `/system/info.app_version` and `/health.version` equal APP_VERSION (test_router_smoke.py). The stale `:latest` tag itself was already fixed in #252; cutting a v0.3.x release repopulates it. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ab492c0e8e |
fix(docker): fix stale :latest tag + add push-to-main trigger (#252)
* fix(docker): fix stale :latest tag and add push-to-main trigger (closes #251, addresses #249) Two bugs caused the Docker image to be stale (showing v0.2.7 inside a :latest/:0.3.x-tagged image): 1. **`:latest` was never set on tag pushes.** The metadata-action rule `type=raw,value=latest,enable={{is_default_branch}}` evaluates `is_default_branch` as false on tag-triggered runs (which run in a detached-HEAD context, not on the default branch). The tag rule was replaced with `enable=${{ github.ref_type == 'tag' }}` so `:latest` is updated on every `v*` tag push. 2. **No trigger for main-branch pushes.** There was no way to keep an up-to-date `:main` edge image between releases. Added `push: branches: [main]` which produces a `:main` rolling tag. Also added a note in the workflow and docs clarifying that the update-channel toggle (Settings → About) is a Tauri desktop feature and does not apply to the Docker image (headless web-server build). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(docker): gate mutable tags to push events + guard :latest against prereleases (PR #252 review) - semver / :latest / :main now require github.event_name == 'push' so a manual workflow_dispatch only ever emits a throwaway :sha- tag (no mutable-tag rollback) - :latest excludes prerelease tags (ref contains '-') so an rc/beta can't clobber it - header + tag-strategy comments corrected (:sha- emits on every trigger) Addresses greptile + coderabbit review on #252. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8690833137 |
feat(update): move update pill into status bar + Updates panel (changelog/channel/history) (#240)
* docs(spec): updates-in-status-bar design (move pill to LogsFooter + Updates panel) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(plan): updates-in-status-bar implementation plan (11 tasks, TDD) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(plan): pin i18n task to scripts/translate_all.py backfill * feat(update): pure chip + release presentation helpers * feat(update): listReleases + fetchAppVersion wrappers * feat(update): transient releasesSlice composed into store * feat(update): app version + channel in updaterSlice * feat(update): list_releases Tauri command (GitHub releases) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(update): UpdateStatusChip bar indicator * feat(update): UpdatesPanel (live row + channel + releases list) * feat(update): mount chip+panel in LogsFooter, retire floating UpdateBadge Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(update): Settings channel switcher shares store value (auto-sync) * i18n(update): add updates.* keys across 21 locales Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(deps): lock reqwest for list_releases command * polish(update): a11y radiogroup on channel switch, safer release key, drop dead test seam Addresses final-review nits (non-blocking): role=radiogroup/radio + aria-checked on the channel Segmented; key={r.name||r.version} to avoid collisions; remove the unused vi import + __loader seam in releasesSlice.test.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(update): i18n the channel-set error + correct flagged updates.* translations (PR #240 review) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(update): add 10s timeout to list_releases HTTP client (PR #240 review) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(update): guard chip Restart against in-flight dub job (greptile P1, PR #240) The always-visible status chip's one-click Restart (ready state) called installUpdate→relaunch without the dub-busy guard the panel uses, so a user with a dub/transcription job running could lose in-flight work. Mirror the panel's gate: toast update.busy and bail when dubStep === 'generating'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(update): surface channel-switch failures in the Updates panel (greptile, PR #240) Mirror Settings' error handling: the panel's stable/preview switch now catches a failed set_update_channel and toasts settings.channel_set_failed instead of an unhandled rejection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
94f2363fa2 |
feat(release): version preview builds (0.3.0-preview.N) + rollback spec (#226)
Phase A: stamp each preview build with a unique monotonic semver prerelease (<base>-preview.<run_number>) via an ephemeral tauri.conf.json rewrite on the preview path. Today every preview reported the static 0.3.0, so the updater never saw a newer version and never delivered preview updates. The prerelease ordering makes each new preview offer-able and converges to stable when <base> ships. (Windows MSI ProductVersion strips the prerelease — caveat noted to verify; mac/linux unaffected.) Phase B (rollback) is captured as a design spec for review, not implemented: per-version preview releases + retention, an in-app Preview-builds picker, an allow_downgrades install path, and the alembic-head data-safety boundary. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
506fa8ea49 |
Add Arch Linux installation instructions (#209)
* Add Arch Linux installation instructions Added installation instructions for Arch Linux. * Update docs/install/linux.md Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> |
||
|
|
e1850b4bd9 |
feat(gallery): designed-voice archetype gallery + neutral importer (#203)
* feat(gallery): designed-voice archetype gallery + neutral importer Adds a browsable library of ~1,100 designed voice archetypes (no real people), generated from OmniVoice's own voice-design taxonomy and organized ElevenLabs-style: 24 curated Featured voices plus a facet-filtered "Browse all" explorer (595 English + 504 Chinese-dialect). Every generated instruct is built from the validator's own vocabulary, so none can trigger the issue-#89 synthesis crash. Backend: - core/archetypes.py: catalog engine (featured + generated, implausible combos pruned, stable hashed ids); loads the taxonomy by file path to stay torch-free in tests. - api/routers/archetypes.py: categories / list+filter+paginate / get / preview (render-on-demand + disk cache) / use (materialize a voice profile). Preview/use reuse generation.py's proven inference path. - gallery.py: drop the celebrity/character catalog; the importer is now a neutral, user-driven "My Imports" (paste a URL you have the rights to). No project-shipped directory of named real people. Frontend: - Gallery UI rewrite: Archetypes zone (featured grid + facet filters + favorites) and My Imports zone; per-card Use voice / Open in Designer. - api/archetypes.ts, useArchetypes/useArchetypeCategories hooks (v5 placeholderData:keepPreviousData), gallerySlice, en.json keys. Tests: 27 new (engine contract + API), full backend suite green (72); CJK guard allowlists the one functional Chinese preview sample. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(gallery): clear new bandit alerts (sha1 + SQL false-positive) The PR's code-scanning "Bandit" check fires on NEW alerts vs main's baseline. The archetype work introduced three: - archetypes.py / core/archetypes.py: hashlib.sha1 used to derive a deterministic preview-cache key and archetype id (not a security digest) — flagged B324 (HIGH). Add usedforsecurity=False; the digest is unchanged. - gallery.py: the UPDATE query interpolates only static, code-controlled column fragments ("is_favorite = ?", "description = ?"); every user value is bound via a ? placeholder — flagged B608 (false positive). Annotate `# nosec B608` with the justification. Behavior-preserving. Net new bandit alerts after this: zero (verified with bandit -ll -ii; only main's pre-existing baseline remains). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(gallery): resolve PR #203 CI (SHA-256 ids, log sanitization, CJK allowlist) All failures stemmed from the initial commit: - Bandit + CodeQL (2 high): SHA-1 weak-hash on the archetype id and the preview cache key. These are deterministic identifiers, never security digests — switched to SHA-256, which the SAST scanners accept. - CodeQL (log injection): the render-failure logs echoed the raw user-supplied archetype_id; log the catalog's canonical a["id"] instead (untainted — it comes from the trusted in-memory catalog, not the request). - CodeQL (superfluous argument): declare createGallerySlice's StateCreator store param so its arity matches the 3-arg call site. - Tests (test_no_hardcoded_cjk): the committed design spec's Chinese-dialect reference table tripped the guard; allowlist it under documentation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(gallery): clear CodeQL clear-text-logging on archetype render errors CodeQL's sensitive-data heuristic flags any request-derived value interpolated into a log call (it persisted even after switching the raw id to the catalog's canonical a["id"]). Log a static message with exc_info=True instead: the full traceback still reaches the backend log for debugging, but no data expression remains for the query to flag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
672f106f05 |
feat(update): Stable/Preview update channels with opt-in toggle (#199)
Adds a user-selectable updater release channel (Settings -> About -> Update channel). Stable (default, every install + launch) tracks tagged vX.Y.Z releases; Preview tracks the latest main build via a rolling "preview" prerelease, falling back to stable if a stable release is ahead. Why Rust: tauri-plugin-updater reads its endpoints from tauri.conf.json and neither the JS check() nor the plugin's registration Builder can change them at runtime (verified against the 2.10.1 source). The only runtime-endpoint API is UpdaterExt::endpoints, so check+install move into two Rust commands that mirror the plugin's own check/download_and_install -- the Stable path behaves identically to the JS flow it replaces; only which manifest is consulted changes. Switching is instant (channel is read per check), no restart. backend (Rust): - config.rs: update_channel field (default "stable", VALID_CHANNELS) + get/set_update_channel commands. - updater_channel.rs: channel_endpoints() (preview -> [preview, stable]) + check_update / install_update commands; install emits update://progress. frontend: - utils/updateChannel.js (+test): single source of truth, normalizeChannel. - utils/updater.js: routes the badge flow (#198) through the Rust commands via the same store contract -- UpdateBadge/App.jsx unchanged. - Settings About: Stable/Preview segmented toggle, channel-aware endpoint row + diagnostics; Check-for-updates honors the live channel. - i18n en + zh-CN. release.yml: additive, workflow_dispatch-guarded preview publish to a rolling "preview" prerelease. The v* tag-push stable path evaluates to its exact prior values (verified) and is never affected. Preview builds are manual -- no scheduled CI spend, nothing auto-published. docs/update-channels.md. Verified: cargo check (compiles clean), tsc, vitest 162/162, build, CJK guard, release.yml YAML parses. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
06560e5555 |
feat(stories): Pro Studio Phase 1 — real audiobook output, cast, persistence, reorder, i18n (#177)
* docs(spec): Stories Editor pro-studio design (line cards, auto-cast, pro output, projects) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(stories): Phase 1 — real audiobook output, cast, persistence, reorder, i18n First phase of the pro-studio Stories Editor (spec: docs/superpowers/specs/2026-05-30-stories-editor-studio-design.md). Makes the editor actually produce audiobooks and remember your work: - Persistence: storiesSlice (tracks + cast) via zustand persist -> localStorage; transient fields (generating/audioUrl) stripped on persist; id counter reseeds from persisted tracks. Dropped the hardcoded sample seed -> clean empty state. - Cast: editable CastMember[] (name, color, voice) with a Cast panel; each line picks a character and inherits its voice (per-line override still available). - Real Generate: exportStoryAudio() stitches every line + [pause] gaps into one WAV via the Web Audio API (job-less /generate per chunk) with a % progress indicator and download. Per-line preview already shipped (#176). - Reorder: native HTML5 drag-and-drop (pure reorder() helper). - i18n: all Stories strings via t('stories.*') (en + zh-CN). - Tests: storiesSlice reducers, storyCast resolution, storyExport WAV/concat/ silence, storyReorder. 18 new unit tests. No DB/alembic; localStorage only. Same-origin + PIN-safe synth (apiFetch). No new deps. Cross-platform-identical default behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1b08c03da9 |
fix(docker): runtime API-base override so served deployments reach the backend (#174)
Docker users reported Settings -> Engines failing with 'Failed to load engines: Failed to fetch'. Two problems: (1) the two API-base resolvers diverged (client.ts honored VITE_API_URL; apiBase.ts honored VITE_OMNIVOICE_API), and the docs documented VITE_OMNIVOICE_API -- which the Engines request path ignored; (2) VITE_* is inlined at BUILD time, so a prebuilt ghcr.io image has no working runtime override at all for reverse-proxy / split-origin deploys. - backend: when OMNIVOICE_PUBLIC_API_BASE is set, inject it into index.html as window.__OMNIVOICE_API_BASE__ (core/spa_inject.py; validated to a plain http(s) URL so it can't break out of the <script>). Unset (default) -> StaticFiles serves index.html untouched (same-origin, zero overhead). - frontend: both resolvers (client.ts _resolveApiBase + utils/apiBase.ts) now read the runtime global FIRST, then VITE_OMNIVOICE_API/VITE_API_URL, then fall through to same-origin. client.ts also strips trailing slashes and recognises __TAURI_INTERNALS__ (parity with apiBase.ts/external.ts). - docs: docker.md + troubleshooting.md document OMNIVOICE_PUBLIC_API_BASE as the runtime override that works on the prebuilt image (the old VITE_OMNIVOICE_API docker run -e example never worked -- build-time inlining). - tests: spa_inject helpers (inject + URL validation/breakout); resolver precedence for the runtime global + VITE_OMNIVOICE_API in both test files. Default same-origin behavior is unchanged on every platform; override is opt-in. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
40cf9bf0e5 |
chore: gitignore Spec Kit/GSD local tooling; track network-sharing plan (#172)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |