621c2633544332d489e3626a14fab3268be3d459
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
e2a33e8c40 |
fix(frontend): browser/Docker fallback for file export (closes #256) (#257)
The history-item export button (and the dub/audio export path) called the
Tauri `save` dialog unconditionally. In the Docker web-server build there is
no Tauri shell, so the plugin's internal invoke() dereferences an undefined
__TAURI_INTERNALS__ and crashes with:
TypeError: Cannot read properties of undefined (reading 'invoke')
…which is exactly what users hit when downloading a freshly cloned voice from
the browser/Docker UI.
Fix: extract a shared `browserDownload` helper (utils/download.js) that does a
plain HTTP-blob download via a temporary <a download>, and guard
`handleNativeExport` on `isTauri` — falling back to that helper (streaming the
file already served at /audio/<path>) when no Tauri runtime is present.
`triggerDownload`'s browser branch now reuses the same helper instead of
duplicating the blob-download logic.
Adds utils/download.test.js covering the Content-Disposition parser and the
no-Tauri download path (regression guard for #256).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
f555fe0720 |
fix(bootstrap): surface setuptools-repair failures + verify pkg_resources (follow-up to #253) (#254)
* fix(bootstrap): surface setuptools-repair failures + verify pkg_resources (follow-up to #253) Three gaps flagged by review bots on PR #253 are addressed: 1. **Layer-2 repair result captured** (`bootstrap.rs` ~line 481): the `let _ = run_streaming(...)` that silently discarded network/permission failures from the targeted `uv pip install setuptools>=75,<80` is replaced with a `match` block that logs `log::info!` on success and `log::error!` on failure (consistent with the Layer-3 path). 2. **Post-repair re-verification added** (`bootstrap.rs`): after the targeted install in Layer 2, a second `import pkg_resources` check is run. If pkg_resources is *still* absent, a `log::error!` with an actionable remediation message is emitted before returning. This closes the gap where bootstrap handed back a known-bad venv that caused the dubbing crash (#248) with no clear signal in the log. 3. **Test strengthened** (`bootstrap.rs` `setuptools_repair_uses_correct_specifier`): the test now mirrors the exact `&[&str]` slice used in both repair branches and asserts `repair_args[2] == "setuptools>=75,<80"` as a single positional argument. This catches the split-arg regression the review bot identified (e.g. `["setuptools>=75", ",<80"]`) which would silently install the latest setuptools and leave pkg_resources absent. 4. **Smoke-test INST-01/02 hardened** (`scripts/smoke-test.sh`): exports `UV_PYTHON_PREFERENCE=only-system`, `UV_HTTP_TIMEOUT=120`, and `UV_HTTP_RETRIES=5` before the `uv run` import checks so that failures reflect real bootstrap regressions, not harness-network timeouts. `cargo test bootstrap` → 5 passed, 0 failed. Closes review findings on #253. Related: #248. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bootstrap): fail clearly when pkg_resources repair fails (PR #254 review) - ensure_venv_ready now returns None (via fail()) when pkg_resources is still missing after the targeted setuptools repair, instead of returning a venv that crashes on the first ASR/dub call. The 'pkg_resources' message routes to the PKG_RESOURCES_MISSING failure mapping for a clear, doc-linked remediation. - smoke-test.sh: correct the comment (timeout+retry vars, not a non-existent index var). --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ef98aae4db |
test(probe): whole-app coverage — dubbing, i18n, engines, security, migration, dictation, design (#247)
* test(probe): expand coverage — dubbing, i18n, engines, security, migration, dictation, design, coverage-critic Broadens the probe harness from one happy-path spec per layer to whole-app feature coverage (web, backend, dictation, clone, design), keeping the Actor/Judge split and offline-by-default + enable-on-demand for heavy paths. New specs + judges (one subprocess boot shared across backend-touching specs): - dubbing (L4): segment duration-ratio, SRT/VTT well-formed, export-archive contents, output language-ID (advisory) - i18n: locale files valid JSON (gate); orphan-keys + coverage (advisory). NOTE: surfaced a real bug — all 20 non-en locales carry gallery.cat_*/ bootstrap.lines keys absent from the en reference (reported, not gated). - engine matrix: active engine available + every unavailable engine explains why (11 TTS / 7 ASR backends via /engines/*) - loopback security: system routes reject non-loopback origins (403) - DB migration: alembic UPGRADE on the seeded omnivoice_data fixture - Coverage Critic: every declared layer still has a spec (gate) + API inventory - dictation: streaming-ASR WebSocket /ws/transcribe registered + handshake - voice design: reuses the audio-correctness ladder - real ASR round-trip: enable-on-demand (PROBE_E2E=1) Enriched _boot_runner.py to capture engines/asr/loopback/openapi/ws in ONE isolated boot (conftest boot_capture session fixture); added env.seeded_data_dir. 13 specs total. probe suite 74 passed / 5 skipped; full repo 687 passed, 0 failures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(probe): address all 15 unresolved review findings on #247 - coverage.py:22 — use `with open(...)` context to close spec files after yaml.safe_load (file handle leak) - _boot_runner.py:80 — store only `type(exc).__name__` for WS errors; drop raw str(exc) that could leak home paths / secrets into capture JSON - _boot_runner.py:99 — snapshot DB files before boot; set db_created=True only when boot creates NEW files (not when fixture already had one) - dubbing.py:46 — FAIL segments_duration_ratio when validated==0 (guards against empty/corrupt segment list passing vacuously) - i18n.py:49 — FAIL locale_valid_json when locales_dir is empty/missing - i18n.py:7 — fix docstring: locale_no_orphan_keys is advisory, not blocking - test_probe_i18n.py:59 — assert r.passed is False, not just r.advisory - coverage_critic.probe.yaml:15 — add "meta" to required layers list - dub_export.probe.yaml:17 — capture dub_audio in steps before advisory reads it - migration.probe.yaml:13 — add path_exists(db_path) data-integrity check - test_probe_asr_e2e.py:33 — os.path.exists → os.path.isfile for PROBE_ASR_SAMPLE - test_probe_migration.py:24 — assert context["db_path"] (presence) not db_created (new creation), aligning with the boot_runner fix Two findings intentionally skipped with reasons (see review thread replies): test_probe_design.py:36 — offline pattern is intentional; actor step is bypassed by design throughout the probe suite for CI compatibility test_probe_engines.py:22 — whisperx pin is intentional; it verifies the shipped default ASR engine is available out-of-the-box Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(probe): ASCII x in dubbing detail (ruff) + run migration judges inside seeded dir Two regressions from the hardening pass: - dubbing.py: replace non-ASCII '×' with 'x' (Ruff ambiguous-unicode → Tests lint fail) - test_probe_migration: move run_judges inside the seeded_data_dir with-block so the new path_exists check sees the DB before the temp dir is torn down (was always failing) --------- 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> |
||
|
|
c98b0aaf6f |
fix(bootstrap): guarantee pkg_resources in backend venv (closes #248) (#253)
Root cause: the existing-venv fast-path in ensure_venv_ready() only
checks `import uvicorn` before returning — it never verified that
`pkg_resources` (dropped by setuptools≥80, issue #224) was present.
Users who installed before commit
|
||
|
|
24a00bea64 |
test(probe): spec-driven AI-agent test harness (L1–L5 + HTML report + triage) (#245)
* test(probe): add spec-driven AI-agent test harness (L1/L2/L4/L5 + report + triage) Introduces `tests/probe/`, a portable, mostly-deterministic test harness built on the Actor/Judge split: AI agents may drive and self-heal, but verdicts are always deterministic code + metrics — no LLM on the verdict path. Layers: - L1 API: Schemathesis property-fuzz over in-process ASGI (enable-on-demand). - L2 web: Playwright Driver + deterministic self-heal (id→test-id→text, loosened CSS) → pluggable Healer; LLMHealer/anthropic_healer for genuine agentic heal. Judges + self-heal logic unit-tested offline via FakePage; live browser skips. - L4 media: audio correctness — exists/decode/duration/not-silent/clipping/NaN, round-trip ASR WER (pure-python, faster-whisper backend), speaker similarity. No golden-WAV (device-stable metrics only); naturalness is advisory-only. - L5 env/first-run: fresh-data-dir backend boot in a SUBPROCESS (no session contamination), asserts health + DB init + endpoint reachability. Docker gated. Plus: hybrid YAML spec engine + JudgeResult/registry; self-contained HTML report that auto-opens (suppressed in CI/headless/PROBE_NO_OPEN); Triager that clusters failures and drafts a prefilled GitHub issue URL (sanitized, no auto-submit) with a one-click button in the report. Dependency-light: runs in the base venv; schemathesis/resemblyzer/playwright/ anthropic are enable-on-demand and skip cleanly. Generated reports gitignored. Full suite green (657 passed); no contamination of existing tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(probe): add L3 desktop layer (Tauri config-integrity + guarded launch) Per the architecture decision, desktop E2E is substituted by backend-over-HTTP (L5) + browser (L2) since Tauri has no official macOS WebDriver. L3 guards the packaging/shell contract a browser test can't see, against the real tauri.conf.json (with platform-override merge), running on any platform with no Tauri toolchain: - version parity between tauri.conf.json and pyproject (release integrity) - dev/build wiring (devUrl matches the Vite frontend, frontendDist, before* cmds) - bundled binaries first-run depends on (uv / ffmpeg / ffprobe in externalBin) - CSP actually permits the local backend origins (desktop-only failure mode: packaged app can't reach :3900 while the browser build works) Adds desktop.py (config load + platform deep-merge + bundle discovery + launch guard), judges/desktop.py (config_present/config_eq/config_contains/csp_allows), desktop_smoke.probe.yaml, and tests covering integrity, platform-merge replace semantics, and a live bundle launch that skips without a built bundle/display. Full suite green (662 passed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
208719a555 |
fix: dub OOM fallback, watermark on /generate, Settings responsiveness (#241 follow-up) (#244)
* fix(dub): whisperx CUDA OOM → CPU fallback instead of a bare 500 Found while exercising the dub pipeline on an 8 GB RTX 4070 Laptop GPU: with the TTS model + GPU worker pool resident, whisperx's CTranslate2 load of large-v3 dies with "CUDA failed with error out of memory", and POST /dub/transcribe surfaced it as an unhandled 500 with no guidance. WhisperXBackend now catches a CUDA OOM at load and retries on CPU (int8, same model + accuracy, just slower) after clearing the CUDA cache. Dubbing keeps working on small/laptop GPUs instead of dead-ending. Only triggers on a CUDA OOM, so the MPS/CPU paths are untouched (cross-platform parity). Verified: /dub/transcribe on the prepped job went 500 → 200 with correct segments. Added a deterministic unit test (forces the OOM, asserts the device switches cuda→cpu; a non-OOM RuntimeError still propagates). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(watermark): embed invisible watermark on /generate output, not just dubs embed_watermark was wired only into the dub pipeline (dub_generate.py), so plain TTS from /generate came out unmarked even with invisible watermarking enabled — i.e. the setting silently did nothing for the main generate path. Embed it on the final audio in the generate handler too. embed_watermark self-gates on the setting + AudioSeal availability and passes audio through unchanged on failure, so it's a no-op when off and never breaks generation. Verified: detector on a fresh /generate clip went is_watermarked:false → true, confidence 1.0, message OMNI ("OM"), is_omnivoice:true. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(settings): wrap the settings sub-nav so tabs don't clip out of view The settings sub-nav has 10 tabs (General…Privacy) but the shared .ui-tabs primitive is a non-wrapping inline-flex row, so on a narrow Settings pane the later tabs (Credentials/Logs/About/Privacy) overflowed the right edge and were unreachable. Scope flex-wrap to `.ui-tabs.settings-tabs-ui` only — the bar now grows to 2–3 rows instead of running off-screen. The shared primitive (used by the models role tabs, log-source tabs, etc.) is unchanged. Verified at 900px (2 rows) and 700px (3 rows): all 10 tabs visible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(settings): connect active tab to content via accent + tighten spacing Make the active settings tab read as connected to the panel below: each tab carries its own semantic accent (already in TAB_DEFS — Models pink, Engines purple, …) instead of a uniform pink, and that accent is threaded down as --settings-accent to paint a matching hairline along the top of the content panel. The shared colour ties tab→content subtly and wrap-proof (no fragile positional connector). Content wrapped in .settings-content with deliberate margin/padding so it breathes under the bar; the bar's own bottom margin is dropped so the bridge owns that gap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(engines): make the compatibility matrix responsive (scroll, don't overlap) On a narrow Settings pane the matrix's fixed-width columns (status/gpu/ isolation/actions ≈ 630px) plus the flexible name column couldn't fit, so the cells collapsed and OVERLAPPED — name text rendered under the AVAILABLE/ACTIVE badges and GPU chips. Give the table a horizontal-scroll container with a shared header/body min-width (840px) and stop the fixed cells from shrinking, so columns keep their shape and stay legible at any width (scroll for the overflow) — the same data-table treatment used elsewhere. Fills normally on wide panes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(settings): border-connect the active tab to its content panel Refine the tab→content connection from a single accent hairline to a "border-connect": the pill bar opens at its bottom (flat corners, no bottom border) into a 3-sided panel (.settings-content) framed in the active tab's accent, with a 2px full-accent top edge at the seam. The bar + panel read as one outlined container, and the active tab's colour visibly feeds into the panel it opens. Accent is threaded per-tab via --settings-accent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
a94fc435a5 |
fix(asr): repoint two 404 ASR model repo IDs in catalog (closes #239) (#242)
* fix(asr): repoint two 404 ASR model repo IDs in catalog (closes #239) Model install failed with HTTP 404 for two ASR entries whose Hugging Face repos don't exist: - UsefulSensors/moonshine-small -> UsefulSensors/moonshine-tiny (Moonshine ships tiny/base; there is no 300M 'small') - Systran/faster-whisper-large-v3-turbo -> deepdml/faster-whisper-large-v3-turbo-ct2 (Systran publishes no turbo repo; deepdml is a valid CTranslate2 build) Audited all 25 catalog repo_ids — every one resolves 200 on HF after the swap. Adds a static (no-network, CI-safe) regression test asserting repo_ids are well-formed and the known-404 IDs can't reappear. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test/docs(asr): safer repo_id access + flag turbo repo as community build (PR #242 review) - test_known_404_repo_ids_absent: m.get('repo_id','') so a missing field gives a clean assertion instead of KeyError regardless of test order. - models.yaml: note the turbo entry is a community CTranslate2 conversion to re-verify on future audits (greptile). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5ad160a2e9 |
fix(license): declare actual FSL-1.1-ALv2 in pyproject (was Apache-2.0) (#236)
pyproject declared license = "Apache-2.0", but the repo's LICENSE is FSL-1.1-ALv2 (Functional Source License; each release converts to Apache-2.0 two years after publication). The Apache-2.0 declaration was inaccurate for the current grant. Declared as a PEP 639 LicenseRef since FSL isn't an OSI/SPDX-listed identifier. Validated: hatchling accepts the expression and builds the project cleanly (uv build OK), so uv sync / packaging in CI is unaffected. 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> |
||
|
|
c857be816a |
fix(gallery): OmniVoice Gallery rename, dark dropdowns, and noisy/stale archetype previews (#241)
* fix(gallery): rename to "OmniVoice Gallery" + fix dark-theme dropdown colors The gallery heading now reads "OmniVoice Gallery" (gallery.title, all 21 locales — brand prefix on each localized word). The facet filter <select>s (Gender/Age/Pitch/Accent/Language) rendered with the OS-default light control surface on the dark theme: .facet-select set background/border from --bg-tertiary / --border-color, which are defined nowhere. An undefined var() reads as transparent on the sibling <div> filters (fine over the dark page) but falls back to the native light background on a form control. Switch to the defined dark-chrome tokens and add color-scheme: dark + an explicit dark option list so the popup matches across WebKit / WebView2 / WebKitGTK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(gallery): archetype previews render a noise buzz instead of voice The Hype Host, The Podcaster and The Vlogger previews played a loud tonal buzz, not speech. The preview renderer pinned num_step=16 and seed=42; the "social" sample script at that exact point lands on a degenerate diffusion trajectory and collapses to a near-pure tone. The blank-audio guard missed it because the buzz is loud (peaks near -2 dBFS), not silent — so the garbage was cached and served. The cache key is (instruct, language) only, so it never self-corrected. - Bump preview num_step 16 -> 32: reliably converges to speech across the gallery's instruct/script space (one-time, cached render cost). - Add a spectral-flatness floor (_is_unusable_audio) so a degenerate tonal render is rejected like a blank one, reusing the existing retry-on-new-seed path. Whisper/breathy voices are broadband (high flatness) so they're safe. Verified: flatness Hype Host 0.001->0.050, Podcaster 0.0002->0.083, Vlogger 0.004->0.039; whisper control (Calm Guide) 0.239, not flagged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(gallery): stop preview playback replaying stale cached audio Preview audio is re-rendered server-side when an archetype is fixed, but the URL is stable and the response carried no Cache-Control — so the WebView's HTTP cache replayed the first clip it ever fetched (e.g. the old buzz) indefinitely, even after the server file was corrected. - Frontend: fetch previews with { cache: 'no-store' } so playback always pulls current bytes. - Backend: send Cache-Control: no-cache on the preview response so any client revalidates against the ETag instead of serving a stale clip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(e2e): add Playwright UI smoke + gallery specs and preview-quality unit test UI testing system to catch regressions like "Use design → Importing a module script failed" (a dead Vite/module server) and the noisy-preview bug. - Playwright (frontend/e2e): drives the system chromium (no browser download) against the Vite dev server. ui-smoke mounts all 12 routable views and fails on any code-split/import failure, uncaught exception, or ErrorBoundary fallback. gallery.spec asserts the "OmniVoice Gallery" heading, the dark facet dropdowns (computed bg = rgba(255,255,255,0.04), not the OS-default light surface), and that opening an archetype in the Designer mounts the lazy CloneDesignTab. `bun run e2e`. - backend/tests/test_archetype_preview_quality.py: unit-tests the _spectral_flatness / _is_unusable_audio guard with synthetic signals (tone < threshold < speech < noise; loud tone + silence are unusable) and pins the render constants. CI-safe — no model/GPU. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d58e5a9e38 |
feat(launchpad): Transcripts card + recent OmniDrive files strip (#235)
- New Transcripts action card (lime accent, FileText) → opens the Transcriptions view, alongside Clone/Design/Dub/Stories/Gallery. - Below the cards, a "Recent files" strip shows the last few exports from OmniDrive (GET /export/history, already loaded on mount) with a "View all files →" link that jumps to the full OmniDrive browser (Projects page). Hidden when there are no exports yet. App.jsx passes exportHistory to Launchpad; reuses existing lp-project-card chrome, adds a small files-head + view-all + grid in index.css. 5 new launchpad.* keys, backfilled across 21 locales. Verified: tsc clean, build OK, vitest 167/167, CJK guard passes. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
07479be04f |
feat(support): unify Donate + Commercial License behind one toggle (#234)
* feat(support): unify Donate + Commercial License behind one toggle The donate page and the commercial-license (enterprise) page were two separate full-screen modes reached from different places. Merge them into a single SupportPage with a charming segmented toggle: - Segmented "💛 Support ⇄ 🏢 Commercial License" control with a sliding active pill that carries each panel's accent hue (pink for Support, teal for License) and an icon that pops on selection. - Switching cross-fades the panel (key remount replays the hero/card entry animations) over the shared Launchpad aurora + a single Back button. - Both legacy modes still work: 'donate' opens the Support tab, 'enterprise' opens the Commercial License tab — so the footer heart and the dub/export "commercial license" links land on the right tab unchanged. Reuses the existing donate/enterprise chrome (DonatePage.css + EnterprisePage.css kept and imported); SupportPage.css only owns the toggle + transitions. Both views share one 640px container width so the frame doesn't jump on toggle. DonatePage.jsx + EnterprisePage.jsx removed (content folded in). 3 new support.* i18n keys, backfilled across 21 locales. Verified: tsc clean, build OK (SupportPage chunk replaces the two old ones), vitest 167/167, CJK guard passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(support): add 'Other ways to help' chips + FSL hero_note fix + i18n backfill - SupportPage: add Star GitHub / Join Discord chips below donation methods - SupportPage CSS: vertically center short Support panel, single-column donation grid, ghost-pill chip styles - Fix FSL hero_note wording across all 21 locales to accurately reflect the license (internal use at any scale is free; only competing product/service triggers commercial license) - New i18n keys: support.other_ways, support.star_github, support.join_discord * feat(support): polish Support panel + correct Commercial License wording Support panel (from screenshot feedback): - Donation methods now stack in a single clean column — no orphaned PayPal card floating in a half-empty second row. - Short Support panel is vertically centered so it no longer clings to the top of an empty page (License stays top-aligned; it's tall enough to fill). - New "Other ways to help" row: Star on GitHub + Join Discord ghost chips, so people who can't donate still have a real way to support — and it balances the layout. Commercial License wording: - Fixed enterprise.hero_note: it implied "deploying at scale (pay-per-use API)" triggers a commercial license. Per the actual FSL-1.1-ALv2, scale does NOT trigger licensing — internal use is free at any scale; the trigger is offering OmniVoice to others as a competing product/service. Reworded to say exactly that, and re-translated across all 21 locales. 3 new support.* keys. Verified: tsc clean, build OK, vitest 167/167, CJK guard passes, 21 locales at parity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: drop stray stories-editor plan doc that slipped into the branch This planning artifact (with CJK i18n examples) was accidentally swept into an earlier commit on this branch; it isn't part of the Support-page feature and isn't on main. Removing it so the CJK guard passes — the committed tree no longer carries hardcoded CJK outside the translation layer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
dc453cf9da |
fix(i18n): close coverage gap the translation PR missed (#230 follow-up) (#232)
The "full i18n coverage" PR (#230) was based on a stale snapshot, so strings added to main after that point — the updater/channel dialog, dictation shortcut, and a batch of export/save/project toasts — were still hardcoded English. This extracts the remaining user-facing imperative strings (toasts + the update ask() dialog) plus the adjacent JSX labels in DubFailureNotice: - App.jsx (17): export/save/download/project/flush toasts → i18n.t('app.toast_*') (App already imports the configured i18n instance; reused it rather than plumbing a hook through 17 handlers). - Settings.jsx (14): save/clear failures, engine-switch, channel, updater download/install + the "Update available" ask() dialog, dictation-shortcut set/register/reset → t('settings.*'). - DubTab.jsx (DubFailureNotice): added the useTranslation hook; "Diagnostic copied"/"Copy failed" toasts + "Open docs"/"Copy diagnostic" labels. 38 new keys added to en.json, backfilled across all 21 locales. No regressions: the only shared-key value change from the #230 merge was the intentional engines.unavailable casing fix. Verified: 21 locales at parity, tsc clean, build OK, vitest 167/167, CJK guard. NOTE: this covers imperative strings (toast/ask) in these 3 files. A full codebase audit of all JSX text/placeholders is a larger separate sweep. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
dec67dc619 |
feat(gallery): calmer, more elegant archetype cards (#231)
The "Use voice" buttons were solid, fully-saturated per-category color fills — 16 loud, differently-hued blocks on screen drew the eye to the buttons instead of the voice names. Polish pass: - Use-voice button: tonal by default (13% accent wash + accent-colored text + hairline accent border), going solid only on hover/focus. Keeps the per-category hue as identity but lets the resting grid stay calm; the CTA lights up on the card you're pointing at. - Chip row: always rendered with a reserved min-height so cards without an accent/whisper chip (e.g. Captain Crusty) no longer leave a ragged void — action rows now line up across the grid. - Designer (wand) button: quiet at 0.5 opacity at rest, full on card hover/focus — it's tertiary, so it no longer competes on every tile. - Card hover border softened a touch. color-mix() is already used in 17 frontend files (proven on all WebView targets). Verified: tsc clean, build OK, vitest 167/167. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
40f96db1d7 |
feat(l10n): extract remaining 531 hardcoded strings — full i18n coverage (#230)
* feat(l10n): complete translations for all 21 languages (837 keys each)
Translate all UI keys across every component for 20 non-English locales:
ar, de, es, fr, hi, id, it, ja, ko, nl, pl, pt, ru, sv, th, tr, uk, vi, zh-CN, zh-TW
- 837 flattened keys per language (100% coverage)
- Covers settings, splash, main UI, dialogs, tooltips, errors
- Placeholders ({{var}}) and HTML tags (<1>) preserved
- Add translate_all.py batch script for future re-translations
* feat(l10n): extract remaining 531 hardcoded strings and translate all UI
Scan found ~250 hardcoded user-facing strings across ~30 component files.
Extracted all into en.json (837 → 1368 keys, 49 namespaces) and updated
every component to use t() / i18next.t().
Components updated (35 files):
- Zero-i18n: AudioTrimmer, CaptureWidget, CastingView, CheckpointBanner,
CompareModal, DirectionDialog, EngineCompatibilityMatrix, ErrorBoundary,
FloatingPill, KeyboardCheatsheet, NetworkToggle, ReadinessChecklist,
SupertonicLicenseDialog, VoicePreview, BatchAddDialog
- Partial-i18n: BootstrapSplash, DubSegmentRow, Header, LogsFooter,
DictationDemo, DubbingDemo, NavRail, MultiLangPicker, SearchableSelect,
Sidebar, Settings, SetupWizard, EnterprisePage, VoiceGallery,
SharingPanel, ReportBugButton, StoriesEditor, App.jsx
- Hooks: useDubWorkflow.js, useTTS.js (using i18next.t directly)
New namespaces: trimmer, casting, checkpoint, compare, direction, errors,
keyboard, header, sidebar, network, readiness, license, voicePreview,
models, enterprise_faq, gallery_extra, dub_workflow, tts_errors, sharing,
reportBug, app
All 20 non-English locales translated to 100% (1368 keys each).
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
f290575593 |
fix(logs): import useAppStore in LogsFooter (donate + notification nav crashed) (#229)
LogsFooter.jsx called useAppStore.getState().setMode(...) in four handlers
(donate button + notification action targets, lines ~405/447/880/922) but
never imported useAppStore — clicking any of them threw
'ReferenceError: Can't find variable: useAppStore' and the handler died.
Add the canonical 'import { useAppStore } from "../store"'. tsc + build clean.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
c2ce92cf69 |
feat(launchpad): surface Stories + Voice Gallery; fix gallery key-spread warning (#228)
- Launchpad showed only Clone/Design/Dub. Add ActionCards for the two newer
modes: Stories (multi-voice audiobooks → setMode('stories')) and Voice Gallery
(browse designed-voice archetypes → setMode('gallery')). i18n in en + zh-CN,
backfilled across all 21 locales.
- VoiceGallery: stop spreading a 'key' prop into <ArchetypeCard {...cardProps}>
(React dev warning + ignored). cardProps no longer carries key; pass key={a.id}
directly at the two render sites.
Verified: tsc clean, build OK, vitest green, CJK guard passes.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
94ed7e6347 |
feat(stories): redesign — chapter section bars, grouped toolbar, readable column (#227)
Full design polish pass (behavior unchanged; JSX structure + CSS only): - Chapters render as distinct section bars (heading title + grip + delete) with an accent left-border — no speaker/voice/tune/preview controls. Detection (isChapterText) is lenient so clearing the title doesn't flip the bar back to a voiced line mid-edit; unified with the chapter auto-numberer. - Toolbar split into three labelled clusters with thin dividers — Project (Projects · Cast) · Content (Import · Paste&Split · +Line · +Chapter) · Output (Stems · format · Generate) — and wraps instead of cramming one row. - Editor centered at a 1040px reading column so lines no longer stretch edge-to-edge on wide windows. - A line's secondary actions (inline-voice / tune / pause / preview / delete) are quieted to 0.5 opacity and revealed on row hover/active, cutting visual noise. Drag handlers factored into a shared dragProps (reused by both bar and line) so reordering still works across chapters + lines. Verified: tsc clean, build OK, vitest 167/167. 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> |
||
|
|
675cc203ee |
fix(asr): cap setuptools <80 so pkg_resources stays present (#224) (#225)
Windows v0.3 users hit two transcription failures with models installed: - WhisperX: "Transcription produced no segments. No module named 'pkg_resources'" - Whisper PyTorch fallback: "No ASR backend is ready. … set OMNIVOICE_PRELOAD_TTS_ASR=1" Shared root cause: whisperx / faster-whisper import `pkg_resources` at runtime, and setuptools 80+ DROPPED the bundled pkg_resources. The existing pin `setuptools>=75` therefore resolved to 82.0.1 — which has no pkg_resources — so `import whisperx` fails. That both breaks WhisperX transcription and makes its is_available() return false, which is why every backend reports "not ready" and the engine asks for the PyTorch fallback (the user's PowerShell env var never reached the GUI-launched app, a separate red herring). Fix: pin `setuptools>=75,<80`. Verified: <80 resolves to 79.0.1 which ships pkg_resources; 82 does not. `uv lock` changed only setuptools (82.0.1→79.0.1). This fixes BOTH errors — WhisperX imports again, so it's available and the fallback is no longer needed. Adds tests/test_pkg_resources_available.py to guard the pin from regressing. Full suite 602 passed (incl. the new test), 0 failures. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6a86d2151f |
refactor(settings): move Performance toggles to the General tab (#223)
The Performance panel (Disable torch.compile / Show live system metrics in header) was nested under the Credentials tab — an odd home. Render it in the General tab instead, where users look for app-level toggles. Pure relocation: PerformancePanel is unchanged; removed its render from CredentialsTab and added it after GeneralTab in the general view. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5320b57de5 | fix(release): macOS smoke mount path preserves space in volume name (#221) | ||
|
|
fa9cda3381 | fix(release): macOS signing env must be ABSENT not empty (unblocks mac build) (#220) | ||
|
|
3fd326b845 |
feat(update): dismiss button for the failed-update pill (#219)
#216 stopped the 6h periodic re-check from clobbering a failed-install error badge — correct, but it left no way to clear that badge except retrying, so a transient install failure pins a red "Update failed · Retry" pill until the user retries or restarts the app. Add a × to dismiss it (mirrors FloatingPill), returning the updater surface to idle. - updaterSlice: dismissUpdate() → idle + clears error/progress - UpdateBadge: × dismiss button on the error state (i18n: update.dismiss) - updaterSlice.test: dismiss returns to idle and clears the error - en.json: update.dismiss ("Dismiss"); other locales fall back to en Verified: vitest 10/10 (updaterSlice + updater guard), typecheck:ci clean, build OK. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6aa581c5f1 | fix(release): resolve AppImage path before cd in Linux smoke (exit 127) (#218) | ||
|
|
1ece49a080 |
fix(release): make macOS signing opt-in so a bad cert can't break builds (#217)
The APPLE_CERTIFICATE secret is currently set-but-invalid, so tauri-action's 'security import' fails and kills the whole macOS build — on stable v* releases too, not just preview. Make Developer-ID signing OPT-IN: pass the Apple creds only on a v* tag push AND when the repo variable MACOS_SIGNING_ENABLED == 'true'. Otherwise pass empty -> the build stays unsigned and succeeds (users clear quarantine via xattr -cr, as documented). Preview is always unsigned. To re-enable signed stable releases: fix the signing secrets, then set MACOS_SIGNING_ENABLED=true (Settings -> Secrets and variables -> Actions -> Variables). No code change needed to flip it. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
679b2b5e4f |
fix(update): keep error/retry pill across periodic re-check (#214 follow-up) (#216)
* fix(update): keep error/retry pill across periodic re-check (#214 follow-up) PR #214's 6h periodic re-check guard skipped only downloading/ready, not error. setUpdateChecking() clears updateError and the badge renders null for 'checking', so a tick while 'Update failed · Retry' was showing silently erased the prompt the user still needed to act on — defeating the PR's own error-surfacing goal (greptile P1, unresolved). - updater.js: also short-circuit the re-check on 'error'. Retry is user-initiated (installUpdate → downloading), so auto re-check is unnecessary in that state. - updater.test.js: new regression test — guard no-ops on error/downloading/ready, proceeds from idle. - UpdateBadge.jsx: add aria-controls + panel id to the 'What's new' disclosure (greptile P2 a11y). - UpdateBadge.css: word-break:break-word → overflow-wrap:break-word (CodeRabbit; the deprecated value). Verified: vitest 166/166, typecheck:ci clean, bun run build OK, CJK guard pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(update): keep notes panel mounted so aria-controls always resolves greptile P2: aria-controls pointed at a conditionally-rendered panel, so the IDREF dangled while collapsed. Render the panel whenever notes exist and toggle with the hidden attribute (canonical disclosure pattern) — the reference now always resolves. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
891e819b4e |
fix(release): unblock all-platform preview/release builds + auto-generated notes (#215)
The first preview build surfaced four real release-pipeline issues (all of which also affect a stable v* release): - macOS: build died at codesign — `security import: failed to import keychain certificate` (the APPLE_CERTIFICATE secret is set but invalid). Preview now force-skips Apple signing (passes empty creds) so it can't fail on a bad/absent cert; stable v* tags still receive the secrets, so signing engages once the cert is fixed. - Linux: .deb bundling fails with "Failed to create control scripts: No such file or directory" (no custom deb config of ours). Drop .deb, ship AppImage only — the universal Linux format and the Linux auto-update target. - Installer smoke (all 3 OSes): the steps hunted for a frozen backend binary to boot with --health-check, but the thin uv-venv installer ships no such binary (the venv builds on first launch). Rewrite to structural verification — assert the bundle carries the shell binary + bundled uv sidecar + backend source resources (pyproject.toml + backend/main.py). Also: a new preview-notes job regenerates the rolling preview release body with GitHub's auto-generated notes (What's Changed by PR + New Contributors + Full Changelog) plus a Contributors avatar strip built from the PR authors — instead of the bare "Auto-generated release for main…" fallback. Runs once after the matrix, preview-only; stable keeps its CHANGELOG section + appended checksums. Stable v* tag-push behavior is otherwise unchanged. YAML validated. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8cf3dad2f4 |
feat(gallery): multilingual designed-voice archetypes (#213)
* feat(gallery): multilingual designed-voice archetypes Ship curated designed voices in 9 more languages (Spanish, French, German, Italian, Portuguese, Russian, Hindi, Japanese, Korean) so the gallery offers more than English + Chinese out of the box -- 27 new featured archetypes across three reusable roles (Narrator / Explainer / Companion). Voice-design timbre (gender/age/pitch) is language-independent, and a designed voice's spoken language is driven by the preview text, not the instruct. So these reuse a neutral instruct + a localized sample script + a `language` value matching frontend/src/languages.json -- byte-for-byte the same model.generate(text, language, instruct) call the Generate tab already makes. They carry no accent/dialect token (accents are English-only, dialects Chinese-only; an invented "spanish accent" would crash synthesis, the issue-#89 mode), so every instruct stays inside the validator vocabulary. - backend/core/archetypes.py: _ML_SAMPLES + _ML_ROLES + _make_multilingual() - frontend VoiceGallery: extend the language facet filter - tests: assert the 9 languages are present, neutral-timbre, valid-token - test_no_hardcoded_cjk: note JA/KO sample text in the existing allowlist entry Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(gallery): surface featured-only languages through the filter Address review feedback (greptile/coderabbit) on #213: - VoiceGallery: the Browse query hard-coded `featured: false` while the Featured strip is hidden whenever a filter is active. The 9 new languages have *only* featured archetypes, so selecting Spanish/French/etc. produced an empty Browse AND a hidden Featured strip -> "No voices match these filters" despite 3 archetypes existing per language. Now Browse includes featured exactly when the Featured strip is hidden (i.e. when filtering), with no duplication when nothing is filtered. - archetypes.py: module docstring said the Featured tier was "~24"; it is now ~51 (24 English + 27 multilingual). Added a docstring to _make_multilingual(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
edbf0af43f |
feat(update): release notes in badge + periodic re-check + error surfacing (#214)
Closes the three highest-value gaps from the auto-update audit vs best-in-class: 1. Release notes — the available pill gains a 'What's new' expander showing the release body (already captured as updateNotes) so users see what changed before installing. 2. Periodic re-check — App.jsx re-checks every 6h, not only on boot, so long-running sessions get notified. checkForUpdate now no-ops while a download/restart is in flight, so the interval can't interrupt an install. 3. Error surfacing — the badge no longer returns null on 'error'; it shows a 'Update failed - Retry' pill (with the error as tooltip) that re-attempts the install, instead of silently vanishing. i18n: update.whats_new/failed/retry added to en + zh-CN and backfilled across all 21 locales (placeholders intact). Verified: tsc clean, vitest 162/162, build OK, CJK guard passes, 21 locales valid + key-complete. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c4d66a8cf5 |
docs(readme): add ASR Engines table (surfaces SenseVoice/FunASR) (#212)
* docs(readme): add ASR Engines table (surfaces SenseVoice/FunASR) The README documents the multi-engine TTS backend but never listed the ASR backends, so users filed requests (#206, #208) for engines that already ship. Add an ASR Engines section mirroring the TTS table, grounded in backend/services/asr_backend.py engine ids + display names, plus a nav anchor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(readme): correct Parakeet language scope + MLX framework name Address review feedback on PR #212: - Parakeet TDT: NeMoASRBackend docstring documents 25+ European languages w/ auto language detection (not English-only); note GPU req. - MLX Whisper: the engine uses Apple's MLX (Metal-backed) framework, not the CoreML inference stack. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cf816b54f7 |
fix(ci): skip supertonic license/cpu tests when the optional dep is absent (#211)
release.yml installs deps with plain 'uv sync' (no optional engines), so test_cpu_only_honest and test_license_gate failed there — is_available() short-circuits with 'supertonic package not installed' before reaching the license check those tests assert on. This blocked EVERY release (preview and stable) at the test gate, not just the preview build that surfaced it. Skip the two when 'supertonic' isn't importable (optional opt-in engine). They still run fully under ci.yml's 'uv sync --all-extras'; the absent-package path is covered independently by test_optional_dep_missing. Also fixes the same two failing on a local '.venv' without the extra. Verified: tests/test_supertonic3.py now 8 passed, 5 skipped, 0 failed without supertonic installed. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
47729057bd |
chore(lint): remove unused imports + variables (ruff F401/F841) (#210)
Autofixes the genuine lint behind the CodeQL py/unused-import and py/unused-local-variable note-level alerts — actually removing the dead code rather than dismissing it. 68 safe fixes via 'ruff check --select F401,F841 --fix' across 29 backend files (dead stdlib/symbol imports like io/sys/json/torch/typing.Optional and unused locals). Only ruff's safe fixes applied — the 9 'unsafe' fixes and the audio_dsp numpy availability import were left untouched. Not touched: empty-except (needs per-site judgement, not autofixable); frontend js/unused-local-variable (eslint no-unused-vars has no autofix); the loopback-low-risk path/log/stack-trace alerts (real, left visible). Verified: full tests/ suite unchanged at 601 passed (the 2 test_supertonic3 failures are pre-existing on main, local .venv state, green in CI). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
253122325c |
feat(gallery): lucide/flag icon redesign + community marketplace (omnivoice-gallery) (#207)
* feat(gallery): lucide icons + country flags + card redesign (replace emoji) - backend archetypes emit lucide-react icon *names* (cross-platform; emoji render inconsistently across OSes) for use-cases and the 24 featured voices. - new frontend/src/utils/archetypeIcons.jsx: name→lucide map, accent→country flag (country-flag-icons, tree-shaken to ~11), per-category color scale, color-coded avatar tile, and a CSS-animated now-playing equalizer (prefers-reduced-motion aware). - card redesign: real elevated surfaces (cards were invisible on the dark bg), avatar + name + facet sub-line, accent/flag chips, and a footer with Preview / category-colored "Use voice" / Open-in-Designer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(marketplace): community voice gallery via omnivoice-gallery submodule Offloads curated + community gallery content to the standalone debpalash/omnivoice-gallery repo — added here as a submodule for authoring, loaded at runtime via the jsDelivr CDN so the binary stays small. Content repo (seeded + pushed separately): manifest.json (24-voice starter pack generated from the featured archetypes), a JSON schema, CONTRIBUTING, and GitHub submission templates carrying consent / no-impersonation guardrails. Backend (api/routers/community.py): configurable sources (env var > file > default), CDN fetch with offline disk cache, strict validation (invalid presets and non-allow-listed audio URLs are dropped, so a bad community entry can neither crash synthesis nor fetch from an arbitrary host), filtering, the prefilled submit URL, and "use" (preset → archetype render path; voice → sha256-verified download). 11 tests. Frontend: a third gallery zone, "Community", reusing the redesigned card, plus "Submit a preset / voice" buttons opening the prefilled GitHub forms. Local-first preserved: network only on open/refresh; everything cached; the built-in generated archetypes need no network, so the gallery is never empty. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(marketplace): address review feedback on the community gallery - community_use: run the blocking manifest read + voice download in a thread (asyncio.to_thread) so they don't stall the event loop (greptile P2). - community_submit_url: validate the `source` override against an owner/repo pattern, falling back to the configured default (greptile P1 hardening). - rename the `type` query param to `item_type` (alias="type") so it no longer shadows the Python builtin (coderabbit). - frontend submit buttons use the canonical openExternal() (Tauri-aware) instead of window.open, which doesn't open the system browser in the desktop app. - lowercase the `currentcolor` CSS keyword (stylelint). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(marketplace): rename useCommunityItem -> addCommunityItem (not a hook) Avoids the use-prefix on a plain API function (rules-of-hooks smell). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9e04d5c683 |
i18n: backfill gallery + archetypes keys across 20 locales (#205)
The de-celebrified Voice Gallery (#203) added gallery.* (45) + archetypes.* (15) keys to en.json only, so all 20 other locales fell back to English for the new gallery UI. Backfill all 60 keys into every locale via the project's own scripts/translate_all.py (Google Translate, placeholder-masked, incremental) — the same tool/path fixed in the earlier update-channel backfill. Also removes 8 stale gallery.cat_* keys per locale (cat_celebs, cat_marvel, cat_disney, cat_politicians, cat_anime, cat_books, cat_gaming, cat_news) — the dead celebrity categories #203 removed from en.json but left behind in the other locales. Finishes the de-celebrification across the whole i18n layer. en.json / source untouched. Verified: all 21 locales valid JSON and key-aligned to en for gallery + archetypes (0 missing); {{version}}/{{pct}}/{{channel}}-style placeholders intact (0 losses); tsc clean; build OK; CJK guard passes (locales are the allowlisted translation layer). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fd6f213517 |
fix(audio): stop near-silent renders becoming blank noise + guard archetype renders (#204)
* fix(audio): stop near-silent renders becoming "blank noise" + guard archetypes Root cause of the blank/hiss voices: normalize_audio peak-normalized to -2 dBFS whenever max(|audio|) > 0. When the model emits a near-silent clip (peak at the noise floor, e.g. 1e-4), that applies thousands of × of gain and lifts the noise floor to full scale — silence turned into loud hiss. This affected every generation path (clone/dub/design/archetypes), which is why "some voices" came out as blank noise. - services/audio_dsp.py: normalize_audio gains a -50 dBFS silence floor. At or below it the audio is left untouched (stays inaudible) instead of being amplified. Real speech — even a whisper — peaks well above the floor, so normal output is unchanged. - api/routers/archetypes.py: after rendering, _is_blank_audio() detects a dead clip (empty / non-finite / peak < 0.02 — a real normalized clip peaks ~0.79). The render retries once with a different seed, then fails loudly (503 via the existing handlers) so a blank preview or voice profile is never cached/saved. Also extracts the script with a non-empty fallback. - core/archetypes.py: _build never falls back to an empty script (empty text synthesizes to silence). Tests (tests/, runs in CI): normalize_audio doesn't amplify silence but still normalizes real audio to target; _is_blank_audio flags dead renders and passes real audio; every archetype carries a non-empty sample script. Verified: full tests/ suite 601 passed incl. 8 new (the 2 test_supertonic3 failures are pre-existing on main — local .venv engine/license state, green in CI — and unrelated to this diff). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(gallery): static log message in blank-render retry (clears py/clear-text-logging) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.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> |
||
|
|
87e4df95cd |
i18n: backfill update-channel + auto-update keys across 18 languages (#202)
PR #200 added 18 new locale files, but they predated #199 (auto-update badge + Stable/Preview channel toggle), so they were missing the `update.*` namespace (6 keys) and `about.channel_*` (5 keys) — those strings fell back to English in ar/de/es/fr/hi/id/it/ja/ko/nl/pl/pt/ru/sv/th/tr/uk/vi/zh-TW. Backfill all 11 keys in every one of those languages so the updater UI is fully localized. en.json / zh-CN.json already had them and are untouched. Placeholders ({{version}}, {{pct}}, {{channel}}) preserved verbatim; files re-emitted in the exact format scripts/translate_all.py writes (ensure_ascii=False, indent=2) so the diff is additions only (+13 lines/file, 0 deletions). Also fix scripts/translate_all.py: LOCALES_DIR was hardcoded to a contributor's absolute path (/Users/.../orca/...) — make it repo-relative so the generator actually runs for anyone. Verified: all 21 locales valid JSON + key-complete, placeholders intact; tsc clean; vitest 162/162; build OK; CJK guard passes (locales are the allowlisted translation layer). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
52d7321287 |
feat(l10n): complete translations for all 21 languages (837 keys each) (#200)
Translate all UI keys across every component for 20 non-English locales:
ar, de, es, fr, hi, id, it, ja, ko, nl, pl, pt, ru, sv, th, tr, uk, vi, zh-CN, zh-TW
- 837 flattened keys per language (100% coverage)
- Covers settings, splash, main UI, dialogs, tooltips, errors
- Placeholders ({{var}}) and HTML tags (<1>) preserved
- Add translate_all.py batch script for future re-translations
|
||
|
|
4ec5b4cb7d |
chore(security): scope CodeQL to shipped product code (#201)
CodeQL flagged 459 alerts on main, but triage showed the bulk are in code
that never ships in the installer's runtime path: file-not-closed in the
omnivoice/eval harnesses, unused-global "FPs" in alembic migration boilerplate
(revision/down_revision), bind-all in tests, and path sinks in the legacy
Gradio research UI. They drowned out the handful of real findings.
Add a CodeQL config (inline, supported because build-mode is `none`/interpreted)
that scopes analysis to product code via paths-ignore: omnivoice/eval, research,
tests, backend/migrations, and *.test.* files. Queries move into the inline
config so security-and-quality stays the single source of truth alongside
paths-ignore.
Net effect on the next scan: the non-shipped-code alerts auto-resolve, leaving
the security tab focused on shipped backend + frontend. No product code changes.
Deliberately NOT touched (assessed, left as-is):
- Stack-trace-exposure (detail=str(e) in routers) — these are intentional,
helpful one-line diagnostics (the error-transparency work in
|
||
|
|
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> |
||
|
|
588177c3ad | Merge branch 'debpalash/translation' | ||
|
|
73c771fe87 | feat(l10n): add 15 more major UI languages and update auto-detector | ||
|
|
fa047166d3 | Merge branch 'debpalash/translation' | ||
|
|
8cb2f54dee | feat(l10n): add Spanish, French, German, Japanese UI locales, auto-detection, and splash page switching | ||
|
|
dfc86da9d7 |
feat(update): non-blocking auto-update with progress + states + busy-gating (#198)
Replaces the blocking ask() dialog with a state-driven, progress-visible flow that never interrupts in-flight work — and preserves existing work by design (user data lives outside the bundle; alembic migrates on next backend start). - updaterSlice: idle→checking→available→downloading(pct)→ready/error state machine (transient, not persisted). 5 reducer tests. - utils/updater: checkForUpdate() (launch, non-blocking → store) + installUpdate() (downloadAndInstall with a Started/Progress/Finished → progress callback, then relaunch). No-ops outside packaged Tauri. - UpdateBadge: a non-intrusive pill — 'Update vX available · Install & Restart' → progress bar → 'Restart to update'. Install is gated while a dub job is generating (toast 'finish your dub first') so a relaunch can't lose work. - App.jsx: launch check now just surfaces availability into the store + mounts the badge (no blocking dialog, no silent auto-install). - i18n (en + zh-CN). Builds on the existing tauri-plugin-updater (signed, GH-release latest.json). Preview/main channel (a release.yml latest-preview.json + channel toggle) is a follow-up; this is the stable-channel core. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cd155f4384 |
feat(dub): FunASR cam++ inline diarization → dub speakers (#182 Phase 2) (#197)
When FunASR is the active ASR backend, use its cam++ per-segment speaker IDs
directly and skip pyannote — the 'all-in-one' diarization the issue asked for.
- asr_backend: FunASRBackend loads spk_model='cam++' (ASR_FUNASR_SPK, set '' to
disable); transcribe()/_normalize_funasr already surface per-segment speakers.
- segmentation.assign_speakers_from_turns(segments, turns): generalised
overlap-weighted speaker assignment from {start,end,speaker} turns (mirrors
assign_speakers_from_diarization without a pyannote object; falls back to the
silence-gap heuristic when no turns). Pure + tested.
- dub_core: _transcribe_chunk collects offset-shifted speaker turns; they
accumulate across chunks; _diarize() uses them and skips pyannote when present.
Default (WhisperX) flow unchanged — no turns → existing pyannote/heuristic path.
4 new tests (overlap winner, containing turn, malformed-turn filtering, empty→
heuristic). Router smoke confirms boot.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
7b346d95f7 |
feat(ui): density follow-up — tighten studio-panel padding (14/16 → 10/12) (#196)
Reduces the panel chrome padding across the studio (left + right columns), reclaiming vertical + horizontal space uniformly. Safe global compaction; the remaining bar-merge + borders→tints are a visual-iteration follow-up. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e690ab79a7 |
feat(dub): density pass — tighten segment-row rhythm (min-height 30→27, padding 4→3) (#194)
Safe slice of the compactness pass: shave per-row vertical space without clipping the two-line (translated + ORIG) content. The higher-impact wins (collapsing the stacked TRANSCRIPT/GLOSSARY/translations-ready bars, borders→ tints) are a follow-up best tuned visually. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b4b829095c |
fix(ui): compact the top bar height (per request — keep it, don't move) (#193)
Reduce .header-area vertical padding 6px → 3px and tighten the title (line-height 1.15, 1.05rem) so the top bar is shorter, reclaiming vertical space for content without relocating the bar. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c70dd4c9bc |
fix(ui): larger Projects/History/Exports icons when the sidebar is collapsed (#192)
The collapsed sidebar rail showed the tab icons at size 13 — too small to read as the only affordance. Bump to 18 when collapsed; the expanded tab bar keeps 13. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e9bb4516b0 |
feat(asr): add FunASR (SenseVoice) as an opt-in alternative backend (#182) (#191)
FunASR is an all-in-one multilingual ASR (50+ languages, punctuation, optional
cam++ speaker diarization). ASR is already pluggable, so this is a new
ASRBackend:
- FunASRBackend (id 'funasr'): deferred funasr import in is_available() (reports
an install hint when absent — opt-in, NOT a hard dep); _ensure_model loads
AutoModel(SenseVoiceSmall + fsmn-vad); transcribe() normalises output.
- _normalize_funasr(): pure, defensive normaliser → OmniVoice's
{chunks, segments, language} shape (handles VAD sentence_info with ms
timestamps + optional speaker, single-utterance fallback, strips SenseVoice
rich tokens). Unit-tested without funasr installed.
- Registered in _REGISTRY → auto-appears in /system/asr-backends → the Settings
ASR picker, with availability/install hint. WhisperX stays the default.
Phase 2 (future): wire FunASR's cam++ speaker ids into dub diarization.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
7d0420066c |
feat(dub): move Output Options + Timing to the top of the right (transcript) section (#190)
Relocates the OUTPUT OPTIONS (Mix BG / Dual subs / Burn subs / Default Track) and Timing-strategy rows from the full-width footer panel to the top of the right transcript column, where the export-relevant settings sit next to the segments they affect. Footer keeps only the done/error banners + export-track toggles. typecheck/build green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8040863f32 |
feat(dub): move Generate Dub + Export to the header bar (with Save/Reset) (#189)
Relocate the stateful primary-action cluster (Generate Dub / Stop / Stopping / Regen-changed + Export) from the bulky footer button bar up to the header bar next to Save/Reset, rendered as compact sm FooterBtns behind a thin divider. Frees the footer to hold just output settings — tighter, less scrolling, and the primary CTA sits where the file/Save/Reset context already is. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8098e7537e |
fix(win): SoniTranslate venv paths cross-platform — Scripts/ on Windows (#186) (#188)
sonitranslate.py hardcoded SONI_VENV/'bin'/{pip,python} (POSIX). On Windows,
python -m venv creates Scripts\ with .exe, so is_venv_ready() was always False
(install looped) and start() fell back to the wrong interpreter -> 30s timeout.
- Add _venv_bin(name): Scripts/{name}.exe on win32 else bin/{name} (mirrors
engines/indextts/bootstrap.py). Use it for the is_venv_ready/install/start
pip+python paths.
- stop(): _proc.terminate() instead of send_signal(SIGTERM) (cross-platform);
drop the now-unused signal import.
- test: _venv_bin returns Scripts/pip.exe on win32, bin/python on posix.
Closes #186.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
7d9338bd2d |
fix(dub): key per-segment WAVs by stable id, not list index (#185) (#187)
* fix(dub): key per-segment WAVs by stable id, not list index (#185) Partial regeneration ('regenerate only changed segments') reloaded/wrote seg_{i}.wav by LIST INDEX while the regen allow-list + fingerprints were keyed by STABLE id. After a delete/merge/split (ids preserved, positions shifted), unchanged segments reused a different segment's audio → silently corrupted dub output on the default in-UI incremental path. - core.config.dub_seg_path(job_id, seg_id): per-segment path keyed by stable id, sanitized to a bare filename (defends against path traversal via crafted ids). A numeric index sanitizes to the legacy seg_{i}.wav, so old jobs resolve through the same helper. - dub_generate: write/reload per-segment WAVs by stable seg_id (deferred write, RVC write, regen reload) with a legacy seg_{i}.wav fallback; persist a job['seg_order'] manifest (index -> stable id) for index-keyed readers. - dub_export: preview + stems-zip resolve the file via seg_order (-> stable id) with legacy fallback, so they keep finding the right audio. - test: dub_seg_path id-naming, legacy-index equivalence, traversal sanitization. Back-compatible with in-flight jobs (legacy index files still resolve). Closes #185. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dub): harden dub_seg_path with realpath containment; route all seg paths through it (CodeQL path-injection) Both job_id and seg_id are request-derived. Sanitise both and verify the resolved path stays inside DUB_DIR (realpath + startswith) — raises on escape. Route the legacy index fallbacks in dub_generate/dub_export through dub_seg_path so no raw os.path.join(DUB_DIR, job_id, ...) remains and a bare '..' component can't traverse. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dub): assert path containment at export sinks (recognized CodeQL barrier) dub_seg_path already validates realpath containment, but CodeQL doesn't propagate the barrier across the call. Re-assert at the FileResponse / zf.write sinks (realpath + startswith on the value used) so the guard is recognized in-function — clears the py/path-injection false positives. * fix(dub): realpath+containment guard before any path sink in export/preview CodeQL flags os.path.exists/FileResponse/zf.write as path sinks and won't propagate dub_seg_path's internal guard. Resolve each candidate, realpath it, and containment-check (startswith DUB_DIR) BEFORE any filesystem access — the guard now dominates every sink in-function, clearing py/path-injection. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f28f5fc749 |
fix(dub): ExportModal crash — t-shadowing in .map callbacks (#183) (#184)
Opening the Export modal on a dub job threw 'TypeError: e is not a function' inside an Array.map during render, tripping DubTab's ErrorBoundary. Cause: the zh-CN i18n sweep (#157) added t('…') translation calls INSIDE .map(t => …) / .filter(t => …) callbacks where 't' was the loop variable (a track object / a lang-code string), shadowing the useTranslation 't'. Calling the shadowed 't' as a function threw (minified to 'e is not a function'). Rename the loop vars (t -> track / code) at the three <option>/<label> render sites so they no longer shadow the translation fn; strings still go through t(). The harmless non-rendering shadows (useMemo/handlers that never call t()) are left as-is. Regression test renders ExportModal with a dub track equal to dubLangCode (the exact crashing branch) and asserts it doesn't throw. Full suite 154/154, typecheck/build/legacy ✓. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a07da5f302 |
feat(stories): MP3 export — backend ffmpeg encode + format selector (#181)
Completes the pro-output story: an Export-format selector (WAV / MP3) on the Stories toolbar. WAV stays fully client-side; MP3 routes the client-stitched WAV through a new backend POST /stories/encode (ffmpeg via the Windows-reload- safe spawn_subprocess, #175), with a strict format whitelist (mp3/m4b/ogg) and bitrate validation so the uploaded format can't inject ffmpeg args. Both the audiobook and per-character stems honor the selected format; MP3 falls back to WAV with a toast if ffmpeg is unavailable. - backend/api/routers/stories.py + main.py registration; temp-file cleanup. - frontend/src/api/stories.ts encodeAudio() (apiFetch: same-origin + PIN). - tests: format-whitelist 400, ffmpeg-missing 501, real mp3 encode (skips if ffmpeg absent). Backend 26 incl. router smoke; frontend 152/152, typecheck/ build/CJK ✓. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
004a87e149 |
feat(stories): Phase 4 — pro output (stems + chapters) + named projects (#180)
Final phase of the pro-studio Stories Editor.
- Named projects: storiesSlice gains storyProjects[] + currentProjectId with
save/load/new/delete/rename (transient fields stripped on snapshot); a
Tauri-safe Projects panel (name input + Save + New + load/delete list).
Persisted to localStorage.
- Per-character stems: 'Stems' export renders one WAV per cast voice
(exportStems → exportStoryAudio per character group) and downloads each.
- Chapter markers: lines starting with '# ' are chapter headings (not spoken);
Generate emits the audiobook WAV + a story-chapters.txt cue sheet with
HH:MM:SS timecodes. 'Add Chapter' inserts a heading line.
- Pure + tested: isChapterLine/chapterTitle/formatTimecode/tracksByCharacter/
buildCueSheet + project reducers. exportStoryAudio now returns
{blob, chapters, durationSec}.
- i18n (en + zh-CN); 9 new unit tests. Full suite 152/152, typecheck/build/CJK/
legacy ✓.
Client-side, no new deps, no DB. (MP3/M4B encode — a backend ffmpeg step — is
the one remaining optional add; WAV output is universal.)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
e07d7d92eb |
feat(stories): Phase 3 — per-line studio drawer (tone tags + speed) (#179)
Click the tune button on any line to reveal a drawer:
- Tone chips insert OmniVoice's native inline emotion/sound tags ([laughter],
[sigh], [question-en], [surprise-wa], [confirmation-en], [dissatisfaction-hnn])
at the cursor — the model-native way to direct tone (not the instruct param,
which only whitelists gender/age/pitch/style/accent and rejects free emotion).
- Per-line speed slider (0.5–2.0x) → threaded into /generate for both preview
and the audiobook export; reset-to-default.
- insertToken extracted to storyTokens (pure + tested); insertPauseInto + tone
chips share it. exportStoryAudio now resolves per-track {profileId, speed}.
- i18n (en + zh-CN); 4 new unit tests. Full suite 143/143, typecheck/build/CJK ✓.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
7eb8186df7 |
feat(stories): Pro Studio Phase 2 — auto-cast from text + import (.txt/.srt) (#178)
* feat(stories): Phase 2 — auto-cast from text + import (.txt/.srt) The no-brain ingestion path. Paste or import a story, click Auto-cast ✨, and the editor detects who's speaking and builds the cast + lines for you. - parseScript(text): screenplay 'NAME: dialogue' + prose quote attribution ('said the fox' / 'the fox asked', straight + curly quotes); narration → Narrator. Pure + tested (speaker normalization, URL guard, fallbacks). - importStory: parseSrt strips indices/timestamps → cue text; importToText routes .srt vs .txt. File import button (accept .txt/.srt) fills the panel. - Auto-cast wiring: distinct speakers → cast members (round-robin voice assign from installed profiles), lines appended; existing cast/work preserved. - i18n (en + zh-CN); 14 new unit tests. Full suite 139/139, typecheck/build/CJK ✓. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(stories): use substring check for SRT arrow (avoid CodeQL js/bad-tag-filter false positive) --------- 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> |
||
|
|
762f5bfdc5 |
fix(stories): preview via job-less /generate (was 404 on /api/dub/preview-segment/__stories__) (#176)
The Stories Editor preview POSTed to a relative /api/dub/preview-segment/__stories__
URL: that dub route requires a real dub job ("__stories__" -> 404) and the bare
relative path skipped the API base + PIN header entirely. Route through the
standalone /generate endpoint via the shared api client (generateSpeech), which
is same-origin and PIN-aware. Per-line and marker-chained preview now work.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
e69dcbb6b1 |
fix(win): subprocess spawns work under bun run dev (--reload) on Windows (#122) (#175)
issue #122 'Extract: Unknown Error' on Windows: 'bun run dev' fails, running backend+frontend separately works. Root cause: dev:api launches uvicorn with --reload, so use_subprocess=True, and uvicorn 0.42's asyncio_loop_factory EXPLICITLY forces the SelectorEventLoop on Windows in that case (passed as loop_factory to asyncio_run, overriding any policy). The SelectorEventLoop has no subprocess support -> asyncio.create_subprocess_exec raises NotImplementedError. 'python backend/main.py' (no reload) uses ProactorEventLoop -> works. So an event-loop-policy fix is futile; the thread fallback is the fix. The ffmpeg extract path already routed through _spawn_async's thread fallback (landed in #157), but several other spawn sites used raw create_subprocess_exec and stayed broken on the dev loop: - add public spawn_subprocess() (drop-in for create_subprocess_exec) that routes through _spawn_with_retry -> _spawn_async (NotImplementedError -> thread fallback + EAGAIN retry); native asyncio path unchanged on supported loops. - fix _spawn_thread_fallback to forward cwd/env/etc. to subprocess.Popen (was silently dropping them -- breaks sonitranslate's cwd= pip install). - convert raw spawns: dub_generate atempo, tools ffprobe, gallery yt-dlp (x2), sonitranslate install (x4). translation_engines already had its own fallback. - tests: NotImplementedError -> thread fallback; cwd forwarding; stdin input (atempo); native path unchanged. No behavior change off the broken loop (macOS/Linux/Windows-prod): the native asyncio subprocess is still used; the fallback only triggers on NotImplementedError. 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> |
||
|
|
5a723c0408 |
fix(model): bound first-run model load/download so it never hangs forever (#173)
Windows users reported 'create demo voice runs indefinitely, no audio, no error'. Root cause: the first /generate triggers OmniVoice.from_pretrained() which downloads multi-GB weights via the legacy LFS path (HF_HUB_DISABLE_XET=1), with NO timeout anywhere. A stalled socket (proxy/firewall/AV) blocks the GPU- pool worker forever inside get_model() -- before the try/except that would surface an error -- and the frontend /generate fetch had no abort, so the spinner spun forever with no toast. - backend/main.py: set HF_HUB_ETAG_TIMEOUT=15 + HF_HUB_DOWNLOAD_TIMEOUT=30 (per-read timeout: resets on each chunk, so slow-but-progressing downloads are never punished; only a dead socket trips it). Set before hf import. - model_manager: get_model()/preload_model() now load via _load_model_with_timeout(), an asyncio.wait_for backstop (OMNIVOICE_MODEL_LOAD_TIMEOUT, default 1200s) that drops the poisoned GPU pool and raises a clear, actionable RuntimeError so a retry gets a fresh worker instead of queueing behind the wedged one. - useTTS.js: AbortController backstop on /generate so the UI never spins forever even if the backend is unreachable; friendly timeout toast. - tests: watchdog raises + resets pool + releases lock; env/floor parsing. Cross-platform (no OS-specific behavior); backward-compatible with installed models; local-first preserved. 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> |
||
|
|
0ec6357760 |
fix(network): remote LAN-share UI actually works (same-origin API + safe clipboard) (#171)
* fix(network): remote share UI must use same-origin API, not hardcoded :3900 When a device opens the LAN-share URL, the SPA is served by the share listener on :5050 but client.ts hardcoded the API to :3900 — cross-origin (CORS-blocked) AND loopback-only/unreachable from another machine, so every fetch failed. The share listener serves the same app+API, so the remote SPA must hit its OWN origin. _resolveApiBase: Tauri→127.0.0.1; vite-dev→:3900 (CORS-allowed); else (share listener / docker / prod build)→window.location.origin. +6 unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ui): safe clipboard copy over plain HTTP (LAN-share remote devices) navigator.clipboard is secure-context-only (https/localhost); on a LAN-shared instance at http://<ip>:<port> it's undefined, so unguarded navigator.clipboard.writeText(...) threw 'Cannot read properties of undefined (reading writeText)' — crashing copy buttons on remote devices. Add a copyText util (clipboard API when available, hidden-textarea + execCommand fallback otherwise) and route all 9 call sites through it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1a0798f7f1 |
feat(settings): dedicated Appearance tab + global font selection (#170)
* feat(settings): global font preference applied app-wide Add a persisted, system-safe global font selection. A new `font` pref in the prefs slice overrides the root `--font-sans` CSS variable (the whole UI uses `font-family: var(--font-sans)`), so the choice applies app-wide; `default` removes the override and falls back to the :root Inter stack. - prefsSlice: FontId type, FONT_OPTIONS/FONT_STACKS tables, font + setFont - store/index: persist `font` via partialize; re-export font tables/type - App.jsx: re-apply persisted font on launch in the rehydrate effect - AppearancePanel: Font row (Select) next to UI-scale and color-theme - AppearancePanel.test: covers render, selection, and default reset All stacks are system fonts (no web-font downloads) so behavior is identical offline across macOS/Windows/Linux. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(settings): move Appearance into its own Settings tab Promote the Appearance panel out of the Credentials tab into a dedicated top-level tab (with the Palette icon), placed before Credentials. Add the English "appearance" label so the tab renders via t(`settings.${id}`). Remove the AppearancePanel render (and its stale comment) from CredentialsTab. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3901a3cf4a |
fix(network): footer Local toggle dead in Tauri (window.confirm no-op) (#169)
The footer Local/Network pill called window.confirm() before enabling, but window.confirm is a no-op in the Tauri webview (returns false), so the enable action was silently swallowed — the button appeared to do nothing. Replace it with a reliable in-app confirm popover (Cancel/Enable). The backend endpoint was always working (verified: enable opens a real listener on the share port). Adds a regression test for the Local -> confirm -> Enable -> POST flow. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c993e14072 |
fix(diarization): use_auth_token->token shim for pyannote on HF Hub 1.x (#167) (#168)
* fix(diarization): shim use_auth_token->token for pyannote on HF Hub 1.x (#167) pyannote-audio 3.x (pipeline.py:102) calls hf_hub_download(use_auth_token=...), which huggingface_hub 1.x removed (only 'token' now) -> 'unexpected keyword argument use_auth_token', breaking speaker diarization. Wrap hf_hub_download/ snapshot_download to translate the dead kwarg, applied before pyannote's 'from huggingface_hub import hf_hub_download' binds it (+ patch already-loaded pyannote modules). Verified the real pyannote reference binds the shim. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(diarization): use pytest.importorskip (fixes CodeQL uninitialized-local) CodeQL doesn't model pytest.skip() as no-return, so it flagged _pp as a possibly-uninitialized local (py/uninitialized-local-variable, error). Switch to pytest.importorskip — cleaner and CodeQL-clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e3d7815df0 |
fix(scripts): Windows (Git Bash/MSYS) support across install/run/smoke-test (#164) (#166)
Follow-up to #165 — the same uname Darwin/Linux-only pattern in the other dev scripts: - smoke-test.sh: add MINGW*/MSYS*/CYGWIN* detection + Windows data paths (%APPDATA%/%LOCALAPPDATA%, per backend/core/config.py) + .exe binary suffix. - run.sh: Windows detection + %LOCALAPPDATA% log dir; backend launch already uses 'uv run' which is cross-platform. - install.sh: detect Windows and print a clear 'use the .msi / WSL' message (the brew/apt system-dep installer can't run on native Windows) instead of silently treating it as Linux. build-omnivoice-tts.sh already handled windows-x86_64; record-reference.sh already guards macOS-only with a clear message. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
41176f6ef1 |
fix(scripts): support Windows (Git Bash/MSYS) in desktop-prod.sh (#164) (#165)
uname -s returns MINGW64_NT/MSYS_NT/CYGWIN_NT under Git Bash, which hit the catch-all 'Unsupported platform' error. Add a windows case + a Windows data- path branch (%APPDATA%\OmniVoice backend data + %LOCALAPPDATA% Tauri/HF dirs, per backend/core/config.py) + launch the debug .exe. The cross-platform 'tauri build' step is unchanged; every rm stays [ -d ]-guarded so an off path is a no-op, never a wrong delete. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c2869e73d5 |
feat(network): user-configurable backend / LAN-share / UI ports (#163)
* feat(ports): make backend/share/UI ports configurable via env vars Single-source the backend port from OMNIVOICE_PORT and derive the LAN-share base from it (OMNIVOICE_SHARE_PORT override). Previously network_share.py hardcoded BACKEND_PORT=3900, so a user running the backend on a custom port got LAN-share/Tailscale pointed at the wrong port. - network_share: replace BACKEND_PORT constant with backend_port() / share_port_base() helpers (env-driven, never-throw fallback to defaults); enable() probes from share_port_base(). - tailscale: serve_enable(port=None) defaults to network_share.backend_port(). - main.py: direct-run + --health-check ports and HEALTH_URL read OMNIVOICE_PORT; CORS default origins use OMNIVOICE_UI_PORT (default 3901). - /system/info + SystemInfoResponse: expose backend_port, share_port_base, ui_port (both success and never-throw except branches). - set-env: persist OMNIVOICE_PORT/SHARE_PORT/UI_PORT; validate numeric and 1024-65535, reject otherwise with 400. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ports): pin child OMNIVOICE_PORT in backend spawn Push OMNIVOICE_PORT=backend_port() onto the spawned Python child's env so network_share.backend_port() always agrees with the uvicorn --port Rust passes. Without this, a user-set OMNIVOICE_PORT would move the LAN-share / Tailscale target while the listener stayed on the Rust-resolved port. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ports): Ports subsection in Sharing settings + env-driven Vite port - vite.config.js: dev-server port reads OMNIVOICE_UI_PORT (default 3901). - SharingPanel: new Ports subsection reads /system/info and displays backend_port / ui_port (with their env-var names + restart-to-apply note) and makes the LAN-share port editable — Save POSTs OMNIVOICE_SHARE_PORT to /system/set-env (persisted), "applies next time you enable sharing". - Tests: extend SharingPanel.test.jsx for the ports subsection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0fbc65f29d |
test: scrub brand name from whisper segmentation fixture (#162)
The whisper_screenshot transcription fixture + its segmentation test referenced a real product/brand name. Swap it for the neutral placeholder 'Acme' (fixture text + chunks + the expected-segment assertions), keeping the test's consolidation behavior identical. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4af5d69111 |
fix(tailscale): HTTP serve fallback when tailnet lacks HTTPS certs + parallel dev launch (#161)
* fix(tailscale): serve over HTTP when tailnet has no HTTPS certs Real-world failure: 'tailscale serve --https=443' on a tailnet without the HTTPS Certificates feature (CertDomains: None) fails with 'error enabling https feature: 404'. Detect cert availability from status --json and use --https only when certs exist; otherwise serve over --http (the WireGuard tunnel encrypts transport anyway). Also surface a clear note/error instead of the raw 404, and a 'run tailscale up' hint when not running. Verified the --http path live on a real tailnet. SharingPanel now shows the returned note. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(dev): launch app in parallel with backend (drop wait:api gate) dev/desktop no longer block the Tauri/vite launch on the API being HTTP-ready — the window appears immediately and the frontend's setup-status check already retries (30x1s) until the API answers. Matches prod, where the window shows BootstrapSplash while the sidecar boots. Dev-only; no shipped change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f7cfd33994 |
polish(network): outermost PIN gate + non-buffering ASGI middleware + listener test (#160)
* fix(network-share): mount RemoteAuthGate at outermost provider Move the <RemoteAuthGate> wrap from App.jsx's main-studio return up to main-app.jsx, inside QueryClientProvider and wrapping the entire app tree (both the dictation widget and <App />). Previously the gate only wrapped the studio return, so a remote device opening a bare URL (no ?pin=) during first-run states — the /setup/status check, SetupWizard, or BootstrapSplash early returns — would 401 with no gate rendered to collect the PIN. The QR path was fine (PIN captured pre-fetch in client.ts); only bare-URL was broken. Remove the App.jsx wrap to avoid double-gating (two PIN dialogs). No behavior change for loopback or QR users. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(network-share): make NetworkAccessMiddleware non-buffering ASGI Rewrite NetworkAccessMiddleware from a starlette BaseHTTPMiddleware into a pure ASGI middleware (class with __init__(app) and __call__(scope, receive, send)). BaseHTTPMiddleware buffers StreamingResponse/SSE bodies before forwarding them, so PIN'd LAN clients on streaming endpoints (dictation SSE, tts streaming, /system/logs/stream) got buffered/laggy responses. Loopback was unaffected (bypasses early), but remote-share streaming was degraded. The ASGI form forwards send untouched on every pass-through path, and only wraps send to inject Set-Cookie on the http.response.start message for the first valid-PIN request — the body keeps streaming chunk-by-chunk. request.app resolves in ASGI scope (Starlette sets scope["app"]), so the inert/loopback/ shell/PIN logic is identical to before. Registered after CORS (unchanged) so CORS stays outermost. All 5 existing behavior tests pass unchanged. Adds three tests: a guard that the middleware is not a BaseHTTPMiddleware subclass, a StreamingResponse pass-through (401 without PIN, full chunked stream with PIN, no buffered Content-Length), and a Set-Cookie-via-ASGI assertion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(network-share): integration test for real listener lifecycle Add tests/test_network_share_lifecycle.py exercising the real second uvicorn listener: await network_share.enable(app) on a minimal FastAPI app, assert get_state().enabled is True with a share_port set and a live TCP listener on that port (real socket connect), then await disable(app) and assert the state resets and the port stops accepting connections. Uses the returned share_port (never a hardcoded port) and tolerates teardown timing by polling for socket close. Wrapped in asyncio.run inside a sync test so it does not depend on a pytest-asyncio event-loop mode; skips gracefully if binding 0.0.0.0 is not permitted in the sandbox. Defensive cleanup resets the module-level state on any failure 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> |
||
|
|
fa1503c4eb |
feat: network sharing (PIN-gated LAN + QR) & Tailscale remote access (#125) (#159)
* docs(spec): network sharing + Tailscale remote access design Same-state LAN sharing via a second in-process uvicorn listener on a dedicated share port (no restart, model/jobs preserved), PIN-gated for non-loopback clients, with QR + all-LAN-addresses panel. Tailscale serve for private remote access. Supersedes the raw 0.0.0.0 default-flip in #125. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(spec): control endpoints reuse existing require_loopback gate Security review of #157 confirmed the /system router is already loopback-gated via Depends(require_loopback) (non-spoofable request.client.host). The network control endpoints inherit it and /system/set-env is auto-protected from the LAN listener — no new guard needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(network): share-listener module — LAN enumeration + PIN + lifecycle Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(network): loopback-only control endpoints + /system/info sharing fields Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(cjk): scan git-tracked files only, not untracked vendored dirs The no-hardcoded-CJK guard walked the filesystem, so local untracked vendored experiments (research/voice-pro etc. with JP issue templates) caused false local failures while CI (committed files) passed. Scan via git ls-files so local-only and CI behavior match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(network): PIN middleware — gate non-loopback API access when sharing on Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(network): inject X-OmniVoice-Pin globally + capture ?pin= from QR URL Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(network): remote PIN gate on 401 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(network): add qrcode dep for share QR Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(network): footer Local/Network toggle with LAN addresses, QR, copy/open Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(tailscale): CLI status + serve enable/disable + endpoints Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(network): Settings → Sharing & Remote Access panel (LAN + Tailscale) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(network): sharing & remote access guide (LAN PIN/QR + Tailscale) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(network): enable() tears down and raises if the share listener never binds Defensive guard (spec §7): if the second uvicorn server doesn't reach 'started' (e.g. the share port was taken in the race after the free-port probe), cancel the task, reset state, and raise — so the API surfaces the failure and the UI stays Local rather than reporting a dead 'Network' state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(network): use globalThis (not Node global) in client.test.ts for tsc CI runs 'tsc --noEmit --checkJs false', which type-checks .ts files; Node's 'global' isn't typed there (TS2304). vitest (esbuild) tolerated it locally. Use globalThis (standard, typed) + cast the mock. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(network): apiFetch leaves opts untouched when no PIN set The unconditional headers merge changed the request shape for callers with no headers (e.g. FormData posts), breaking the legacy 'apiPost passes FormData without Content-Type override' node test. Only spread opts + inject X-OmniVoice-Pin when a PIN is actually present; otherwise pass opts through unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b4f238aeb1 |
fix(bootstrap): scrub PYTHONHOME/PYTHONPATH before uv so AppImage venv build succeeds (#144, #127) (#158)
On the Linux AppImage, the bundled runtime exports PYTHONHOME / PYTHONPATH (and sometimes LD_LIBRARY_PATH) pointing at the AppImage's *own* bundled Python. When first-run bootstrap shells out to `uv` to create/sync the venv, that build subprocess inherits those vars, so the freshly-built managed interpreter resolves its stdlib against the wrong (AppImage) Python and dies with `ModuleNotFoundError: No module named 'encodings'` while compiling a transitive dep (dora-search/demucs). This surfaces downstream as "Backend process exited (never started) — no error output captured" (#144). The backend spawn in backend.rs already scrubs these vars before launching uvicorn; the uv/venv/pip subprocesses in bootstrap.rs were scrubbing them too but via five inline copies, which is drift-prone. Factor the scrub into a single `scrub_python_env(cmd)` helper documenting the #144 root cause, and apply it at every uv/venv/pip call site (uvicorn import check, repair sync, venv create, sync, ROCm reinstall). Add a unit test asserting the helper queues removals for all three vars. Safe cross-platform: those vars are normally unset on macOS/Windows and `env_remove` on an unset var is a no-op, so default behavior is unchanged everywhere. Compile-validated (cargo check + cargo test pass); no AppImage in CI to reproduce the original failure end-to-end. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8fa54ccae4 |
l10n(zh-CN): full Chinese localization + Windows/settings fixes (absorbed from #66) (#157)
* @
fix: skip torch.compile on Windows where Triton is unavailable
torch.compile with mode="reduce-overhead" depends on Triton, which has no
official Windows support. On Windows the compile call succeeds but generates
code paths that crash at inference time with an OOM-like error
("Cannot find a working triton installation").
Check for Triton availability before compiling so TTS gracefully falls back
to eager mode on platforms without Triton.
Closes #65
SummerSec
@
* feat: comprehensive Chinese (zh-CN) localization for Settings and navigation
Add full Chinese (zh-CN) translation support across the frontend:
- NavRail, Launchpad, Clone/Design tabs, Settings (all tabs)
- Sidebar navigation labels, hero text, action cards, section headings
- Fix: Settings missing General tab in TABS array
- Fix: i18n locale not persisted after page reload (useEffect deps)
- Fix: NavRail key prop spreading into JSX elements
Co-authored-by: SummerSec
* fix: translate production override parameter labels (Speed, t_shift, etc.)
* fix: translate voice design category labels (Gender, Age, Pitch, etc.)
* feat: translate Transcriptions and Voice Gallery pages
* fix: translate gallery category names (Disney, Anime, etc.)
* feat: translate DubTab, personality presets, and voice design presets
* fix: remove duplicated emoji in personality name translations
* fix: correct preset translation keys to match actual preset IDs
* fix: filter natural language from personality instruct to prevent validation error
* fix: handle edge case where instruct has no valid tags
* chore: remove debug logging from personality instruct filter
* fix: address CodeRabbit review — importlib.util, English comment, grammar, theme, placeholder
* fix: localize selected category label in VoiceGallery header
* feat: translate remaining DubTab UI text (CAST, Generate Dub, Translate All, etc.)
* fix: improve ffmpeg detection on Windows, error messages, and yt-dlp download timeout
* feat: add proxy setting in Settings → General for downloading via proxy
* fix: improve ffmpeg detection on Windows, error messages, and yt-dlp download timeout
* feat: allow HTTP_PROXY/HTTPS_PROXY env vars via /system/set-env
* fix: support SOCKS5 proxy, also set ALL_PROXY env var
* fix: increase yt-dlp extractor retries for subtitle 429 errors
* feat: translate prep overlay stage labels (download, extract, demucs, scene)
* feat: translate BatchQueue, VoiceProfile, ToolsPage, Projects pages
* feat: translate SetupWizard, DonatePage, EnterprisePage + fix NotImplementedError handling
* feat: add ffmpeg status + manual path setting in Settings → General
* fix: validate ffmpeg path exists when user sets it manually
* fix: fall back to thread-based subprocess when asyncio raises NotImplementedError on Windows
* fix: pin setuptools<70 — ctranslate2 requires pkg_resources removed in 70+
* fix: translate transcribing overlay text
* fix: complete DubTab zh-CN localization + argostranslate preflight check
* fix: add cmn-Hans language code mapping for Google Translate
* fix: fall back to thread-based pip install on Windows when asyncio subprocess raises NotImplementedError
* feat: add DeepL/Microsoft/LLM credential fields to Settings
* feat(i18n): localize GlossaryPanel, DubSegmentTable, DubSegmentRow
* fix: Windows-safe log rotation handler avoids PermissionError on rename
* feat: persist proxy/FFmpeg/LLM/translation credentials, separate DeepL/Microsoft keys, add glossary max-height scroll and collapse
- /system/set-env writes env.* to prefs.json via prefs.set_()/delete()
- Backend startup reads env.* from prefs.json into os.environ (.setdefault)
- PERSISTENT_KEYS covers proxy, FFmpeg, LLM, DeepL/Microsoft keys
- DeepL uses DEEPL_API_KEY, Microsoft uses MICROSOFT_API_KEY (fallback TRANSLATE_API_KEY)
- DeepL/Microsoft _build_translator reads DEEPL_BASE_URL/MICROSOFT_BASE_URL
- Google/MyMemory/Microsoft translators bypass Windows registry proxy
- Frontend CREDENTIAL_GROUPS splits into 4 groups with password/text fields
- Glossary panel body max-height: 35vh + overflow-y: auto
- Glossary panel can be collapsed via ChevronDown button
- queryClient.invalidateQueries after save for immediate refresh
- SystemInfoResponse adds proxy_url, ffmpeg_ok, ffmpeg_path
* feat(i18n): localize ExportModal with zh-CN support
- Add useTranslation + replace ~50 hardcoded strings with t() calls
- Add exportModal namespace to en.json and zh-CN.json
- Cover presets, tracks, tabs, video/audio/subs/package tabs, license notice, and output footer
* fix: address PR #66 security review feedback
- Regenerate uv.lock against pypi.org (remove TUNA mirror URLs)
- Route HF_TOKEN through huggingface_hub.login() instead of prefs.json
- Add os.chmod(prefs_path, 0o600) for restricted file permissions
- Add warning logs to _WindowsSafeRotatingFileHandler bare except blocks
* docs: add Chinese translation README_CN.md
* docs: add link to Simplified Chinese translation in README.md
* docs: add English/Simplified Chinese cross-links between READMEs
* fix(l10n): restore clickable Discord/email footer links in EnterprisePage
The i18n extraction replaced main's clickable <button onClick=openExternal>
footer links with bare {t()} labels, dropping both the clickable behavior
and the literal Discord URL — which broke test_discord_link_updated
(EnterprisePage missing discord.gg/bzQavDfVV9). Restore both as clickable
links wrapping the translated label, with the hardcoded URL/mailto (URLs are
not translated). Keeps i18n, restores functional parity with main.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(l10n): no-hardcoded-CJK rule + enforce; clean dead LLM block; harden set-env
- Add 'Localization (hard rule)' to CLAUDE.md: no hardcoded non-English UI
text outside frontend/src/i18n/; functional CJK allowed via allowlist.
- New tests/test_no_hardcoded_cjk.py enforces it (allowlists text-processing
regexes, model/engine vocab & IDs, error matching, demo/eval data, fixtures).
- Settings.jsx: remove dead saveLlm block (Chinese toasts + unused llm* state,
flagged by CodeQL js/unused-local-variable); render language-picker native
names from new LANGUAGES export in i18n/index.ts instead of hardcoding.
- main.py: drop unused 'import shutil' (CodeQL py/unused-import).
- system.py: harden FFMPEG_PATH/FFPROBE_PATH set-env (reject control chars;
defense-in-depth for the py/path-injection finding). Endpoint stays
loopback-only — network sharing must never expose /system/set-env.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: drop unused ui import (Panel) + fix implicit str-concat in cjk test
Clears the two CodeQL notes introduced/attributed to this PR:
- Settings.jsx: remove unused 'Panel' from the '../ui' import (js/unused-local-variable).
- test_no_hardcoded_cjk.py: collapse multi-line message strings to single lines
(py/implicit-string-concatenation-in-list).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: SummerSec <summersec@qq.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
028d7b0151 |
fix(windows): port-conflict kill + Triton/torch.compile disable (salvaged from #85) (#156)
* fix(windows): port-conflict kill + Triton/torch.compile disable (salvaged from #85) Salvages the two safe, valuable Windows fixes from community PR #85 without the changes that would regress all users. backend.rs — implement the Windows branch of `kill_orphan_on_port`, which was a no-op (`pub fn kill_orphan_on_port(_port: u16) {}`). It now parses `netstat -ano -p TCP` for the LISTENING socket on exactly `port` (suffix-matched on ":PORT" to avoid e.g. :3900 matching 39000) and kills the owning PID via `taskkill /PID <pid> /F`. Behind `#[cfg(not(unix))]`; the unix branch is untouched. Signature now matches the unix branch. main.py — on Windows (sys.platform == "win32"), default TORCH_COMPILE_DISABLE / TORCHDYNAMO_DISABLE / TORCHINDUCTOR_DISABLE to "1" before torch is imported. Triton has no Windows wheel, so these prevent TritonMissing/dynamo errors. Uses os.environ.setdefault (never overrides an explicit user value) and is win32-guarded, so cross-platform default behavior is unchanged. Intentionally NOT salvaged from #85: - The unconditional `os.environ["HF_HUB_OFFLINE"] = "1"` in main.py and the `local_files_only=True` / HF_HUB_OFFLINE save-restore in model_manager.py. This breaks first-run model downloads for every user (downloading models on first use is the core value prop). Offline mode must stay opt-in — only when the user sets HF_HUB_OFFLINE themselves. - The model_manager.py torch.compile/_get_gpu_pool changes: already present on main in a superior form (`should_torch_compile()` gating from plan-02/#65 and the existing `_get_gpu_pool()` lazy pool), so applying #85's cruder `TORCH_COMPILE_DISABLE` env check would regress. - The 512-line README rewrite, the ~50 frontend .jsx formatting-only diffs, and the personalities.py attrs additions (out of scope). Refs #85. Co-Authored-By: caaaaaleb <caaaaaleb@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: move win32 torch-disable block below sys.path preamble test_main_py_bootstrap_adds_backend_dir asserts the first 15 lines of main.py contain sys.path.insert + _backend_dir. The win32 block was placed above the preamble, pushing them out of range. The torch env vars only need to precede torch's (lazy) import, so moving the block below the sys.path bootstrap keeps behavior identical and restores the test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: caaaaaleb <caaaaaleb@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f757b77a59 |
docs(install): clarify macOS Gatekeeper "damaged" workaround (#134) (#155)
Reword the macOS install doc's Gatekeeper section so it's findable by the
exact symptom ("App is 'damaged' / can't be opened"), and spell out both the
GUI path (right-click -> Open, or System Settings -> Privacy & Security ->
Open Anyway) and the Terminal path (xattr -dr com.apple.quarantine ...).
Explains WHY macOS shows "damaged" (the build isn't notarised yet, so it gets
quarantined) and why the workaround is safe (downloaded from the official
repo/Releases). Proper fix remains Apple Developer signing + notarisation,
already wired in release.yml behind the documented secrets.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
54004fcced |
feat(bootstrap): opt-in AMD ROCm torch install (#124) (#154)
Detection already routes ROCm through torch.cuda (get_best_device + _configure_rocm_if_needed), but the default install ships the CUDA torch build, so AMD-only machines fall back to CPU. Add an opt-in post-sync step: when OMNIVOICE_TORCH_VARIANT=rocm is set, the bootstrap reinstalls torch/torchaudio from the ROCm wheel index (default https://download.pytorch.org/whl/rocm6.2, overridable via OMNIVOICE_TORCH_INDEX). Strictly gated — default (unset) leaves the CUDA/CPU path untouched — and non-fatal: a failed ROCm reinstall keeps the working default build and points the user at docs/install/linux.md. rocm_opt_in() + rocm_torch_reinstall_args() are pure and unit-tested (gating + index override + arg shape). cargo test green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
034d1c4811 |
fix(onboarding): hide DictationDemo when sample assets are absent (#119 follow-up) (#153)
DubbingDemo and DemoPresetGrid already degrade gracefully (hide) when their assets / is_demo profiles are missing, but DictationDemo always rendered its three hardcoded cards — which fail on click without the bundled sample WAVs (rendered by scripts/build_demos.sh; absent in a plain source checkout). Add a mount-time HEAD probe of the first sample; if it's not present, hide the whole demo (mirrors DubbingDemo's missing-manifest behavior). When assets are present, behavior is unchanged. Test: HEAD 404 → demo renders nothing. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
993e6cf6a5 |
fix(dub): async-ify _pitch_preserving_stretch (#133 Greptile P1) (#152)
_pitch_preserving_stretch ran a blocking subprocess.run() inside the `_stream` async generator (on the event loop). Each ffmpeg atempo call is ~50-100 ms, so on a multi-segment time_stretch dub job it froze health checks, status SSE, and every other concurrent request for seconds. Convert to asyncio.create_subprocess_exec + await communicate() (same pattern as run_proc_streaming_stderr); await the call site in _stream. Drop the now-unused `import subprocess`. Tests: async coroutine + target-length + no-op cases (real ffmpeg). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
79473c4c01 |
docs(#124): document AMD GPU (ROCm) install path (#151)
Detection already works (get_best_device + HSA_OVERRIDE_GFX_VERSION); the gap was that the default install ships CUDA torch, so AMD users fell back to CPU with no guidance. Document the opt-in ROCm wheel swap (rocm6.2), the device-verify one-liner, and the HSA override for unsupported GFX. Linux-only, opt-in — default cross-platform behavior unchanged. An installer-integrated env-var-driven wheel selection is a tracked follow-up. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
87eb5ad078 |
feat(dub): audio-only dubbing mode (#119) (#150)
* feat(dub): audio-only dubbing mode (#119) Add an audio→audio dubbing path: upload an audio file, get dubbed audio out, with no video processing. The transcribe → translate → TTS core is unchanged; only the video-coupled stages are skipped. Backend: - dub_core /dub/upload: new `input_type` form field ("video"|"audio"). Audio mode validates the upload is a known audio container (else 400) and threads input_type into the ingest source dict. - dub_pipeline ingest: for audio input, skip scene detection + thumbnail ffmpeg passes (still emits scene_done count=0 so the prep SSE contract the frontend waits on is unchanged); stores input_type on the job. - dub_export /dub/download: for audio jobs, branch to an audio-only export (_build_audio_export_cmd) — no video input/map/codec/subtitle pass. Outputs dubbed_audio_{lang}_{stamp}.{wav|m4a|mp3|flac} via `out_format` (default m4a), optionally mixed with the separated background. Unknown formats fall back to AAC. Frontend: - dubSlice: dubInputType state + setter (default 'video'). - DubTab: auto-select audio-only mode when an audio file is dropped/picked. - dub.ts/useDubWorkflow: pass input_type on upload. Tests (11): _build_audio_export_cmd format/mix matrix; end-to-end audio-only export produces an audio file (no video mux); unknown-format fallback; upload rejects a video extension in audio mode. Closes #119. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * harden(#119): allowlist-sanitize lang_code in audio export path The track id is already constrained to an existing track key, but allowlist-sanitize it before it reaches the output path (same pattern as the existing safe_name) so a path component can never carry separators — clears the CodeQL path-injection flag on the new audio-export branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * polish(#119): address Greptile P2s on audio-only dubbing - dub_pipeline: emit scene_start before scene_done(count=0) for audio so the prep SSE stage sequence is symmetric with the video path. - useDubWorkflow: 'Preparing audio…' pill for audio jobs (was always 'Preparing video…'). - DubTab: widen the drop-accept regex + file-input accept to the full supported audio set (aac/opus/wma) so it matches the input-type detection and the backend allowlist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#119): drop unused dubInputType read in DubTab (CodeQL) Only setDubInputType is used; the value read was dead. Clears the CodeQL unused-variable alert. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8b00dc1f4f |
feat: onboarding demos, opt-in bug reporting, error-docs deeplinks + issue triage (#133)
* feat: onboarding demos, opt-in bug reporting, error-docs deeplinks + issue triage Working-tree snapshot bundling several in-flight workstreams (v0.3.0): - Onboarding/demo system: DemoPresetGrid, DictationDemo, DubbingDemo components + tests, render scripts (render_demos_omnivoice.py, build_demos.sh, build_dub_demo.sh), personalities preview URLs, alembic 0002 voice-profile demo fields. - Opt-in bug reporting: ReportBugButton (prefilled GitHub-issue URL path). - Error transparency UX: errorDocsMap deeplinks + BootstrapSplash/error wiring. - Dub workspace: DubSegmentRow/Table, WaveformTimeline, dubSlice tweaks. - Issue triage: .planning/issue-clusters/ (plan-01..05 root-cause masters, GH #128-#132). - CLAUDE.md: hard rule — everything ships on v0.3.0, no version bumps. KNOWN GAP (why this is a draft): the generated demo audio assets are NOT in this tree, and backend/assets/samples/demo_voice.wav is deleted. onboarding.py guards the missing file (skips seeding the demo profile with a warning), so no crash — but first-run Launchpad will be empty and /demo_audio/ preview URLs 404 until assets are regenerated via scripts/build_demos.sh. Do not merge before regenerating + committing the demo assets. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dub): timing strategies — kill audio compression, add Concise + Stretch Video Replaces the current audio time-compression default (atempo squeeze to fit slot) that produced chipmunk/alien output on high-density target languages like Bengali. Two new user-selectable modes; legacy behaviour kept behind an explicit "Strict slot" choice. New `DubRequest.timing_strategy` enum (default "concise"): - "concise" Translator trims text to fit at natural rate; if it still overflows, hard-trim at slot with a fade so we never overlap the next speaker. Surface overflow_s per segment so the user can shorten the text. - "stretch_video" Audio plays at natural 1.0× rate. Backend computes a per-segment new timeline; persists a video_stretch_plan on the job. Mux step (dub_export) builds an ffmpeg trim+setpts+concat filter graph that stretches each segment's video portion to match the natural-rate dub audio. Gaps/pre-roll/tail pass through at 1.0×. Sub burn under stretch_video is skipped in one pass (cues would drift). - "strict_slot" Legacy atempo squeeze. Retained for back-compat. Director rate-bias side-effect (seg_speed *= bias) now gated on strict_slot only, so "urgent"/"slow" direction tokens keep their instruct effect in the new modes without chipmunking. Per-segment fit_status emitted in the SSE done event: {status: "fits" | "overflows" | "video_stretched", overflow_s?, stretch_ratio?} DubSegmentRow's "Sync: 100%" badge (which was lying — sync_ratio was always ~1.0 because the TTS loop pre-trimmed to slot) is replaced with a truthful "Fits / Overflows +Ns / Video 1.18×" label. Frontend: - prefsSlice.timingStrategy (persisted, store v3→v4 with safe migrate). - DubTab footer Segmented control: "Concise · Stretch Video · Strict slot". - useDubWorkflow passes timing_strategy on /dub/generate; consumes fit_status. Tests: tests/test_dub_timing_strategy.py — 13 cases covering schema defaults/validation, _build_video_stretch_filter_graph (pre-roll, gap, tail, empty-plan early return, post-subtitle chain-in), and _video_stretch_plan_for guards. 30/30 existing dub tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(waveform): surface missing source as "Source media missing" instead of code-4 black box When a project's underlying media file is gone (moved or deleted between save and reload) the <video> element fires MediaError code 4 and the companion audio fetch returns HTTP 404 — both were silently warned to the console while the user stared at an unresponsive black panel and an empty waveform. - WaveformTimeline now flips loadError when the video element rejects code 3 (decode) or 4 (src not supported), and tracks `sourceMissing` separately so the error UI can name the actual problem. - The audio decode fallback chain catches HTTP 404 specifically and treats it as source-missing instead of loading silent empty peaks — an empty waveform on a deleted source is more confusing than a clear "Re-upload the video to continue" message. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tray): "Show OmniVoice" reloads when the webview is blank When the dev Vite server restarts (or the main window is created before the backend is ready), the webview load fails and the window is left with `<body></body>` plus a "Could not connect to the server" console error. Clicking "Show OmniVoice" from the tray menu just re-showed the broken window — there was no recovery path short of quit+relaunch. Now the show handler runs a tiny eval after `show()`/`set_focus()` that calls `location.reload()` only when `document.body.childElementCount === 0`. A healthy window doesn't blink (body is non-empty); a blank one self-recovers as soon as the user clicks Show. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#133): bug-report diagnostics field mapping + drop unused imports Address PR #133 review: - ReportBugButton: /system/info exposes `platform` + `device`, not `os`/`torch_device`/`gpu` — those reads silently dropped OS/GPU from every bug report. Map to the real fields (CodeRabbit). Also remove the dead `home` local in stripHome (CodeQL unused-variable). - DictationDemo: drop unused `Loader` import (CodeQL unused-import). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1cfda2f44e |
feat(settings): configurable models directory (#64) (#149)
* feat(settings): configurable models directory (#64) Let users pick where model weights download (the HuggingFace / Torch cache) instead of being pinned to ~/.cache/huggingface — useful when the system drive is small or slow. Backend: - core/user_env.py: durable per-user env file (~/.config/omnivoice/env) helper with upsert/unset that preserves other keys and writes 0600. main.py already loads this at startup before importing torch/HF, so the value takes effect on the next launch. Path resolves at call time via an OMNIVOICE_ENV_FILE override so it's robust to module re-import in tests. - settings.py: GET/PUT /api/settings/storage/models-dir — validates the dir is writable (mkdir + write-probe → 400 if not), persists the choice, and writes OMNIVOICE_CACHE_DIR to the durable env. Empty path clears → reverts to default. Returns restart_required since an in-use cache can't be safely moved mid-process. Loopback-gated like the other settings. Frontend: - StoragePanel: Models tab panel to view/set/reset the directory, shows effective vs configured vs default + a restart note. Cross-platform default parity preserved (default cache path is the HF default on every OS); local-first (no network); backward-compatible (absent setting → existing behavior). No version bump. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#64): harden models-dir input + clear CodeQL hygiene flags - settings.py: reject control/NUL chars in the path with a 400 before any filesystem call (an embedded NUL otherwise raised ValueError → 500). Also serves as the explicit input-validation barrier for the user-chosen path (loopback-gated same-user local file picker — no cross-privilege boundary). - test_user_env.py: use `with open(...)` so the file is closed and the assert has no side effects. - user_env.py: comment the best-effort chmod except clause. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(#64): single source of truth for models dir + review fixes Address CodeRabbit + Greptile review on PR #149: - P1 (both bots): the settings_store copy of the models dir was only ever read by this GET endpoint, so it was a redundant cache that could diverge from the durable env file (the value main.py actually reads). Drop it — the per-user env file (OMNIVOICE_CACHE_DIR) is now the single source of truth: PUT writes it, GET reads it back. No divergence possible. - XDG-aware default (CodeRabbit): _default_models_dir now honors XDG_CACHE_HOME, matching huggingface_hub's real default on Linux. - Atomic 0600 write (Greptile, security): user_env writes via an os.open opener that creates the file 0600 from the start — no world-readable window before chmod for a file that can hold HF_TOKEN. - _read_lines only swallows FileNotFoundError; other OSErrors propagate so an upsert can't silently drop existing keys on a transient read failure. - Guard makedirs("") when the env path is a bare filename (no parent). - Best-effort write-probe cleanup in a finally; raise ... from e. - a11y: label the models-dir input via aria-labelledby/aria-describedby. - OS-neutral unwritable-dir test (mock makedirs) instead of Unix-only /dev/null path semantics. 12 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
15958d3860 |
fix(bootstrap): surface why the backend "never started" (refs #144, #127) (#148)
* fix(bootstrap): surface why the backend "never started" (#144, #127) AppImage users hit "Backend process exited (never started) — no error output captured" with nothing to act on. Root gap: when `Command::spawn()` of the venv Python fails (the common Linux/AppImage case — interpreter can't exec, missing system lib, stale venv), spawn_backend logged the OS error but returned None silently, so the bootstrap reported "no error output captured". Now the spawn failure writes a diagnostic (the interpreter path, whether it exists on disk, the OS error, and an actionable "Clean & Retry / run from a terminal" hint) to backend_err.log, which the bootstrap's read_error_log_tail already surfaces. The "no output" dead-end becomes the real launch error. This makes #144/#127 diagnosable (the underlying AppImage cause then routes from the now-visible error). Pure message builder is unit-tested; cargo test + cargo check clean. Refs #144, #127. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bootstrap): platform-specific spawn-failure hint (Greptile #148) The diagnostic tail said "run the AppImage from a terminal… dynamic-loader error" — meaningless on macOS/Windows (spawn can fail on any OS). Pick the hint by build-target OS via cfg!: AppImage/loader wording on Linux, venv/quarantine on macOS, missing-Python/AV-block on Windows. "Clean & Retry" stays universal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ee671300be |
ci: gate omnivoice-tts build to pin changes; drop hanging Intel-Mac leg (#147)
* ci: gate omnivoice-tts build to pin changes; drop hanging Intel-Mac leg The omnivoice-tts C++ runtime is pinned to a commit SHA in quant_map.json, so it only needs rebuilding when that pin (or the build script) changes — not on every PR/push. Running it per-push left the heavily-contended hosted macOS runners (esp. Intel macos-13) sitting in "Waiting for a runner…" for hours as a perpetual queued check (the UNSTABLE state on every PR). - Moved the build out of ci.yml into its own workflow, .github/workflows/build-omnivoice-tts.yml, gated to: paths [quant_map.json, scripts/build-omnivoice-tts.sh, the workflow] + workflow_dispatch. Normal PRs no longer trigger (or hang on) it. - Dropped the Intel darwin-x86_64 (macos-13) matrix leg: that hosted pool is unusably contended and Apple's momentum is on arm64; Intel-Mac users get the in-process OmniVoiceBackend fallback (already the documented behavior). Kept linux-x86_64, windows-x86_64, darwin-arm64. Re-add macos-13 here if first-class Intel binaries are ever needed. Both workflows YAML-validated. Matches ci.yml's stated philosophy of keeping heavy platform builds off the per-PR path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: timeout-minutes + injection-harden the omnivoice-tts build (bot review) - Greptile: add `timeout-minutes: 45` so a hung leg (esp. experimental darwin-arm64 Metal) can't run to GitHub's 6h ceiling — same resource-drain class this PR addresses. - CodeRabbit: stop interpolating the pinned SHA / platform directly into the run block. Validate the SHA is a git hash in the pin step, then pass it + platform via quoted env vars (no shell-injection surface from quant_map.json). Declined: SHA-pinning actions@v4 — matches the repo's floating-tag convention (ci.yml/release.yml); belongs in a repo-wide hardening pass + Dependabot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
adf486ee03 |
chore: set version to 0.3.0 across all sources (+ drop v0.4 references) (#145)
* chore: drop stray v0.4 references — everything ships on the v0.3.0 line Per the project's versioning rule (no v0.4, no unprompted version chatter): - backend/main.py + marketplace.py: the app reported version "0.4.0" (ahead of even pyproject's 0.2.7 and referencing a forbidden version). Aligned to "0.2.7" to match pyproject.toml / tauri.conf.json — a consistency fix, not a bump. - errorDocsMap.ts / indextts/bootstrap.py / _secret_key.py: reworded "v0.4" deferral comments to version-agnostic "deferred / later hardening pass". - docs/install/troubleshooting.md: the "tracked for v0.4" notarization line now matches macos.md (signing is wired; activates on the Apple cert secrets). Note: historical planning records under .planning/ still contain "defer to v0.4" notes; left as-is (a record of superseded decisions) — CLAUDE.md + the constitution are the live source of truth. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: set version to 0.3.0 across all sources (current dev line) The current/upcoming version is v0.3.0 (0.2.7 is the prior stable). Bump every version source so the codebase consistently reports 0.3.0 — the in-code dev version; the git *tag* still happens later per the release cadence. - pyproject.toml, frontend/src-tauri/Cargo.toml, tauri.conf.json, frontend/package.json: 0.2.7 → 0.3.0 - backend/main.py (FastAPI) + marketplace.py export metadata → 0.3.0 (these had drifted to a phantom "0.4.0") - CHANGELOG.md: "[0.2.7] — Unreleased" → "[0.3.0] — Unreleased" - uv.lock + Cargo.lock reconciled (1-line each) so `--frozen` installs hold. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(version): read app version from package metadata (no more drift) Greptile (#145): the FastAPI version + marketplace bundle metadata were bare string literals — they'd go stale-wrong again at the next bump (the exact class of bug this PR fixes; that's how "0.4.0" happened). Read once from importlib.metadata.version("omnivoice") via core.version.APP_VERSION, with a "0.3.0" fallback only for a non-installed source checkout. pyproject.toml is now the single source of truth for the runtime version. Tests: tests/test_app_version.py (semver + equals installed metadata). 2 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
50954f7f43 |
feat(macos): wire Developer-ID signing + notarization; fix "app is damaged" docs (#134, #72) (#143)
The unsigned DMG triggers macOS Gatekeeper's misleading "app is damaged" block (#134, #72). Two parts: - release.yml: pass APPLE_CERTIFICATE / _PASSWORD / APPLE_SIGNING_IDENTITY / APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID to tauri-action. It signs + notarizes the macOS bundle when these repo secrets are set, and is a no-op (today's unsigned build) when they're absent — so this is safe to merge now and "activates" the moment the maintainer adds an Apple Developer cert. - docs/install/macos.md: explain the "damaged" message is Gatekeeper (not corruption), give the `xattr -cr` + right-click→Open workarounds, and add a "For maintainers" table of the required secrets. Removed the stale "tracked for v0.4" line (versioning rule: everything's on v0.3.0). The in-app error→docs deeplink (GATEKEEPER_QUARANTINE) already targets the #gatekeeper-quarantine anchor. Refs #134, #72. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
285e3d8d6e |
fix(bootstrap): always try only-system fallback; drop the too-strict gate (#142)
Verification of #140 (driving real uv) found system_python_ge_311() was stricter than uv's own interpreter discovery: it probed only `python3`/`python`, so on a machine where `python3` is the macOS 3.9 but a Homebrew 3.14 exists, the gate returned false and the only-system fallback was skipped — even though `UV_PYTHON_PREFERENCE=only-system uv venv` resolves 3.14 fine. Fix: drop the pre-gate (and the now-unused parse_py_version/system_python_ge_311 helpers + the parse test) and always add the system-python attempt as the last resort. uv's discovery is the authority; with `requires-python = ">=3.11"` it resolves any compatible system interpreter or fails fast → remediation. Verified live: `only-system uv venv` created a venv from system CPython 3.14.5 on this host (no 3.11.x present). cargo test + cargo check clean. Refs #130. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c37a932784 |
fix(voice-design): validator-safe instruct builder (plan-05, closes #114 #115) (#141)
* fix(voice-design): build validator-safe instruct on the frontend (#132) plan-05 (option A — frontend guard). The engine validator is whitelist-strict by design; the #114/#115 failures came from useTTS.js merging the free-text instruct field with the category dropdowns, producing unsupported items (#115) or two items in one category (#114). - voiceInstruct.js buildDesignInstruct(vdStates, freeText): dropdowns win their category; free-text accepted only as a known tag in an open category; unknown/duplicate items are dropped and returned so the UI can warn. Derives TAG_TO_CATEGORY from CATEGORIES (single source of truth). - useTTS.js design mode uses it instead of the raw merge; toasts dropped items. Engine validator (_resolve_instruct) untouched — whitelist contract preserved, no vendored-engine change. Tests (TDD, vitest): voiceInstruct.test.js (6). Full frontend suite 72 passed; typecheck + build green. Closes #114, #115. Addresses #132. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(voice-design): split unsupported vs duplicate instruct; warn on dropdown drift (Greptile #141) - buildDesignInstruct now returns { instruct, unsupported, duplicates }: `unsupported` = free-text prose (not a known tag, #115); `duplicates` = a valid tag whose category was already set (e.g. dropdown low pitch outranks a typed high pitch, #114). useTTS shows an accurate toast per bucket instead of calling a valid-but-outranked tag "unsupported". - console.warn when a *dropdown* value isn't in CATEGORIES (option-list ↔ whitelist drift) instead of silently dropping it. Tests updated + 1 added (7/7); typecheck + build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c34bc002a3 |
fix(bootstrap): mirror cascade + system-Python fallback for blocked networks (plan-03, closes #60) (#140)
* fix(bootstrap): mirror cascade + system-Python fallback for blocked networks (#130) plan-03. First-run bootstrap downloaded managed Python from GitHub with no mirror and a short retry budget, so a GitHub-blocked/unresolvable network killed the install dead-on-arrival (#60). bootstrap.rs (Rust/Tauri): - apply_uv_http_env(): UV_HTTP_TIMEOUT=120 / CONNECT_TIMEOUT=30 / RETRIES=5 on both `uv venv` and `uv sync`. - `uv venv` cascade: default GitHub → gh-proxy mirror (UV_PYTHON_INSTALL_MIRROR) → system Python (UV_PYTHON_PREFERENCE=only-system, only if a system Python >=3.11 is detected). First success wins. - Actionable failure messages (install python.org Python / set a mirror / Clean & Retry) instead of a raw uv exit code. Frontend: BootstrapSplash hint for the GitHub-blocked / can't-download-Python case. Docs: troubleshooting.md restricted-network section (mirror env vars, China PyPI index, honest VPN note) — referenced by the remediation text. Tests: Rust #[cfg(test)] for parse_py_version + apply_uv_http_env (cargo test: 2 passed, crate compiles); docs-drift validator + frontend build green. NOTE: the restricted-network E2E paths (mirror install, only-system fallback) need MANUAL verification on a real GitHub-blocked network — not reproducible in the dev/CI harness. cargo + the unit tests cover compile + the pure helpers only. Closes #60. Addresses #130, #57, #127. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bootstrap): drop --python 3.11 pin on system-Python fallback (Greptile #140) system_python_ge_311() accepts 3.12/3.13, but the fallback passed `--python 3.11`, forcing uv to find a 3.11.x interpreter exactly — so a machine with only 3.12/3.13 failed the fallback and wrongly hit the remediation. Drop the pin; `only-system` + the project's `requires-python = ">=3.11"` lets uv resolve any compatible system interpreter. cargo test: 2 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
898f41a57d |
fix(windows): gate torch.compile on Triton + ASR critical-path smoke (plan-02, closes #65) (#138)
* fix(windows): gate torch.compile on Triton availability (#129, closes #65) plan-02. torch.compile(mode="reduce-overhead") needs Triton at runtime; Triton has no Windows wheel, so the old `device=="cuda"`-only guard in model_manager.py failed on Windows+CUDA and surfaced as a confusing "OOM" (#65). Inference-time, hard to diagnose. - engine_env.should_torch_compile(device): requires CUDA + find_spec("triton") + the existing perf.torch_compile_disabled setting being off; logs the skip reason at INFO and falls back to eager. - model_manager.py call site uses it instead of the bare cuda check. - smoke-test.sh INST-02: import torch + ctranslate2 + whisperx (full ASR path) so a missing transitive dep fails the build instead of crashing mid- transcription (#116). Runs in the CI smoke-matrix on Win/macOS/Linux. setuptools>=75.0 (fix-sequence step 1) already pinned (#58). Linux/CUDA+Triton behaviour unchanged. Tests (TDD): tests/test_torch_compile_gate.py (4). Closes #65; addresses #129/#116. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(windows): also gate subprocess torch.compile on Triton (Greptile #138) Greptile flagged that the in-process gate left a parallel gap: engine subprocesses honour TORCH_COMPILE_DISABLE, but build_engine_env() only set it on the user's Performance toggle — so a Triton-absent host (Windows, or macOS) still exposed subprocess engines to the same crash this PR fixes in-process. - build_engine_env(): set TORCH_COMPILE_DISABLE=1 when the user disabled compile OR Triton is unavailable (find_spec), cross-platform — mirrors should_torch_compile(). Drops the Windows-only scoping (and the now-unused `import sys`). - Refreshed the stale module docstring. - 3 new tests cover the subprocess gate (triton-missing, triton-present, user-opt-out). 7/7 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * revert(engine_env): keep subprocess TORCH_COMPILE_DISABLE user-driven Reverts the build_engine_env() broadening from the previous commit. Auto- disabling subprocess torch.compile on Triton-absence conflicts with a deliberate, tested contract (test_perf_settings: Windows + flag-off ⇒ no injection; non-Windows ⇒ never inject) — the subprocess var is intentionally under the user's explicit control. The #65 fix is the in-process should_torch_compile() gate (unchanged here), which IS automatic and fully tested. Pushing back on the subprocess auto-gate as a separate, deliberate contract change rather than forcing it through by rewriting established tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ea868386e3 |
fix(windows): HF cache disk-fallback for WinError 448 (plan-01, closes #117 #118) (#137)
* fix(models): disk fallback when scan_cache_dir raises WinError 448 (#128) plan-01 fix-sequence step 2. On Windows, huggingface_hub's scan_cache_dir() raises WinError 448 "untrusted mount point"; the three call sites in setup/models.py swallowed it and reported "not cached", so the app re-downloaded models it already had — looping 5× and giving up (#117/#118). - _is_cached_on_disk / _scan_cache_on_disk: walk the canonical HF layout <cache>/models--<org>--<name>/snapshots/<rev>/ directly (honours HF_HUB_CACHE/HF_HOME, so a relocated models dir works too). - is_cached / list_models / recommendations now fall back to the disk scan when scan_cache_dir() raises. An empty snapshot dir is not counted. Symlink-disable env + local_dir_use_symlinks=False were already shipped (main.py, setup/download.py); this closes the remaining failure path. Tests (TDD, fail-before/pass-after): tests/test_hf_cache_fallback.py (4). No regression on the non-Windows path (fallback only triggers on raise). Closes #117, #118. Addresses #128 (#64 configurable-dir is the follow-up). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(models): probe HF /hub subdir + close scandir handle (bot review) Addresses #137 review: - CodeRabbit (critical): hf_cache_dir() returns HF_HOME when HF_HUB_CACHE is unset, but repos live under $HF_HOME/hub/models--…. Added _hub_cache_roots() so the WinError-448 fallback probes both <dir> (HF_HUB_CACHE-set case) and <dir>/hub (HF_HOME-only case); previously it could miss the cache and re-download. Regression test added (HF_HOME-only layout). - Greptile: wrap os.scandir() in `with` so the dir handle closes even when any() short-circuits (avoids handle leaks on repeated /models polls). - CodeQL: drop unused `os` import in the test. 5 tests pass, incl. -W error::ResourceWarning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |