Swap the static social-preview banner for the live v0.3.9 Launchpad shot
and drop the now-duplicate Launchpad row from the gallery (shown once).
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- Emoji section headers with explicit <a id> anchors. Emoji breaks
GitHub's auto-generated heading slugs, so every in-page nav target keeps
a stable explicit anchor (verified all href="#..." resolve).
- Refresh the screenshot gallery. The prior set was from April, predating
the launchpad / settings / dictation UI overhaul, so it misrepresented
the app. Captured fresh at retina from the live v0.3.9 UI and led the
gallery with the new Launchpad home: launchpad, studio, voice design,
voice gallery, dubbing, engine-compatibility matrix, model store,
embedded API reference (Scalar), and the in-app changelog reader.
- Fix the stale engine count (11 -> 14 TTS engines) in the comparison
table, FAQ, and roadmap to match the engine table + backend registry.
- Use <kbd> keycaps for the dictation shortcut (Opal detail).
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(engines): snapshot lazy registry keys so /engines can't 500 under concurrency
`list_backends()` runs in a FastAPI threadpool and iterates the lazy TTS/ASR
registries via `items()` → `__iter__`, which held a *live* `dict.__iter__(self)`
open across each engine's slow `is_available()` probe. Meanwhile the lazy
`__getitem__` resolves a deferred entry by mutating the dict (`self[key] = cls`).
A second concurrent `/engines` request (or any ASR op) materializing the lazy
`faster-whisper-isolated` entry therefore changed the dict size mid-iteration:
RuntimeError: dictionary changed size during iteration
asr_backend.py:1729 list_backends → _REGISTRY.items()
asr_backend.py:1665 __iter__ → for k in dict.__iter__(self)
Both `_LazyRegistry` (TTS) and `_LazyASRRegistry` (ASR) now snapshot their live
keys up front with `list(dict.__iter__(self))` — consumed atomically under the
GIL — so a concurrent lazy insert can no longer trip the iteration. The slow
per-engine probes then run over the snapshot, not the live iterator.
Deterministic fail-before/pass-after regression for both registries:
tests/backend/services/test_lazy_registry_concurrency.py.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(changelog): add the /engines concurrency fix under [Unreleased] (#940)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
A per-chunk temp-WAV write that fails with OSError EINVAL ("[Errno 22]
Invalid argument") — a missing/read-only/full temp dir, a removed drive,
or antivirus — collapsed into "Transcription produced no segments.
[Errno 22] Invalid argument" with no next step. classify() now names the
class (OS_INVALID_ARGUMENT) so build_failure attaches an actionable
temp-dir/disk/AV hint at the exact surface the streaming dub path already
feeds it (dub_core.py:672) — same treatment the ffmpeg and compute-type
classes get. Fail-before/pass-after regression added; the errno-22 token
keeps it from colliding with the errno-2 transformers-import class.
Also stamps the [0.3.9] CHANGELOG section with today's release date
(2026-07-04) ahead of tagging.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
A user selected the sherpa-onnx TTS engine and got a 500 that read "TTS
engine stopped mid-generation. This usually means it ran out of memory.
Try the Flush button…" — when the real cause was a pure setup problem:
"OMNIVOICE_SHERPA_MODEL not set. Point it to a sherpa-onnx TTS model
directory (containing model.onnx + tokens.txt)." Same misclassification
class as #880/#893, which tightened the OOM catch-all on the generation
path — but the engine-not-configured case still fell through to memory.
Two layers, fixing the whole class:
1. Error classification (backend/api/routers/generation.py): a new
`_is_config_failure()` recognizes "required engine model path / env
var not set" over the whole exception chain (OMNIVOICE_* named with
"not set"/"point it to"/"set omnivoice_…", sherpa's "no model.onnx
found in", "not configured", "venv not found. set" for the dedicated-
venv opt-ins). `_oom_friendly_reraise` checks it BEFORE the OOM branch
and re-raises actionable setup guidance that names the variable, points
at Settings → Engines, and never mentions memory or Flush. Generalizes
to sherpa/Confucius4/dots/MOSS and any future env-gated engine.
2. Engine gating (backend/services/tts_backend.py): SherpaOnnxBackend
ships no bundled model, so is_available() now gates on
OMNIVOICE_SHERPA_MODEL (set + contains model.onnx) — like the other
path-configured opt-in engines — returning False with an actionable
reason instead of "ready", so the picker marks it unavailable-with-a-
reason rather than selectable-but-broken. Added the copy-paste setup
snippet for the Compat Matrix. Backward-compatible: a correctly
configured OMNIVOICE_SHERPA_MODEL keeps the engine available.
Tests (fail-before/pass-after): config-classification of the sherpa
"model not set" error and the wider not-configured class (no "out of
memory"/"Flush"); is_available gating on the env var + model.onnx and the
setup-snippet registration.
Fixes#919
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Two sides of the same class the router-smoke leak (#932) traced to
test_pronunciation_api's importlib.reload teardown:
- test_pronunciation_api now re-runs init_db() on the restored data dir so
the reloaded core.db/main.app is never left on a schema-less DB.
- test_db_migration_safety catches db_module.MigrationError dynamically
instead of the collection-bound name, so a reload that rebinds the class
can't make pytest.raises miss it.
Both orderings (real + reversed) now pass; no product change.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
tests/test_router_smoke.py showed ~10 `sqlite3.OperationalError: no such
table: jobs` failures in the full suite (and in isolation on a clean data
dir), but passed when a schema-creating module ran first.
Root cause: the `client` fixture builds a bare `TestClient(app)` with no
`with` block, so the FastAPI lifespan never runs — and `init_db()` (the
only place the schema is created) lives in that lifespan (main.py). The
smoke tests therefore free-rode on whatever schema an earlier module left
on the active DB. A module that reloads `core.config`/`core.db` and leaves
`core.db.DB_PATH` pointed at a fresh, schema-less DB (test_pronunciation_api's
`importlib.reload` teardown restores the env var but never re-runs init_db
on the restored data dir) strands router-smoke on a DB with no tables ->
every DB-backed route 500s. Same class as #878 / #917.
Fix (test-only, zero blast radius): the `client` fixture now calls
`core.db.init_db()` against whatever DB is active at run time before serving
requests — the same `init_db()` pattern test_api.py / test_personas_api.py
use. Because it targets the live `core.db.DB_PATH`, it re-creates the schema
regardless of which path any prior module left active, making the suite
self-sufficient and order-independent.
Verify:
- `pytest tests/test_router_smoke.py` alone: 10 failed -> 24 passed
- `pytest tests/test_pronunciation_api.py tests/test_router_smoke.py`
(deterministic reproducer): 10 failed -> 38 passed
- `pytest tests/` full suite: 2215 passed, 20 skipped, 10 xfailed,
4 xpassed, 0 failed / 0 errors
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Bullets for #922 (release titles), #923+#924 (sponsors), #925 (contact),
#927 (models), #928 (openapi), #930 (engines) — the agents kept off
CHANGELOG.md during the merge chain. Plus a portable how-we-set-up-
sponsorship playbook for reuse on other projects.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Add a Settings → OpenAPI page that renders an interactive Scalar API
reference for OmniVoice's own local backend, plus a compact footer button
that opens it.
- New OpenApiPanel fetches the live spec from the resolved backend base
(getApiBase()+"/openapi.json", via apiFetch so it follows remote-backend /
LAN-share overrides), owns loading + unreachable-backend fallback (with
Retry), and hands the parsed spec inline to Scalar.
- Scalar is bundled via the @scalar/api-reference-react npm package — NO CDN
script tag. It is lazy-loaded (ScalarApiReference.jsx) so its ~heavy Vue
bundle stays out of the initial load and only downloads when the page opens.
- CDN-free hardening: withDefaultFonts:false (drops the fonts.scalar.com
@font-face rules), proxyUrl:'' (Test Request client goes direct to the local
backend, not proxy.scalar.com), spec passed as inline content (no external
spec fetch). The Tauri CSP is the hard backstop. Verified the built dist:
external hosts appear only as inert/gated strings inside the on-demand Scalar
chunks and are absent from the initial-load chunks.
- settingsCategories: new 'openapi' category (Braces icon, api/openapi/scalar/
rest/swagger/docs keywords) in the System group; Settings render case wired.
- LogsFooter: compact Braces icon button (openSettingsTab('openapi')) next to
the discord/mail cluster, chrome-muted → accent on hover, uniform 14px icon.
- i18n: all strings via t() with English defaultValue fallbacks (openapi.*,
logs.open_api*, settings.openapi); keys added to en.json.
- Test: OpenApiPanel.test.jsx (mocks the spec fetch + stubs Scalar) — renders
the reference container on success, shows the unreachable fallback on failure,
recovers on Retry.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(engines): real synthesis "Self-test" + copy-paste setup snippet for opt-in engines
Builds on #905's Engines-settings fixes (verified still green: license dialog
mounts, matrix reloads on select, cpu_fallback routing toast, cpu-native →
cpu_only). Two enhancements, no #905 behavior touched.
Real "Self-test" for in-process TTS engines
-------------------------------------------
The existing /engines/{id}/health probe only imports the package and reports
"deps OK" for in-process engines — it never proves the engine can emit audio.
New POST /engines/{id}/selftest runs a *tiny real synthesis* from a fixed short
ASCII phrase and reports ok + duration + sample-rate + sample count, proving the
engine actually produces audio. Guardrails keep it cross-platform-identical and
CPU-cheap: TTS + available + in-process only, bounded wall-clock timeout
(OMNIVOICE_SELFTEST_TIMEOUT_S, default 90s) that returns ok=false/timed_out
instead of hanging the panel, a process-wide lock so a click-storm can't stack
model loads, loopback-gated, and only ever on user click (never on load). The
Compat Matrix gains a "Self-test" button (with cooldown) that renders
"0.82s @ 24 kHz in 820 ms". HF tokens in a synth error are redacted like the
health route. Verified end-to-end: kittentts synthesized 89,200 samples @ 24 kHz.
Copy-paste setup snippet for path-gated opt-in engines
------------------------------------------------------
IndexTTS / MOSS-v1.5 / dots.tts / Confucius4 gate on an OMNIVOICE_*_DIR env var.
list_backends() now emits a single-sourced `setup_snippet` (the exact
`export VAR=/path/...` line) surfaced with a Copy button inside the matrix's
"Why unavailable?" disclosure, so users don't reconstruct it from the docs.
Also tightened the incomplete SelectEngineResponse TS type to include the
routing echo (routing_status/effective_device/routing_reason) the post-select
toast already reads at runtime.
Tests: backend selftest success/subprocess-reject/unavailable/unknown/loopback/
exception-capture/timeout/HF-redaction + setup_snippet shape; frontend self-test
render, timeout marker, subprocess+ASR gating, setup-snippet render. New route
added to the API route snapshot. Full vitest (808) + backend engine/routing/asr/
route-inventory/no-CJK green; lint 0 errors; format + typecheck:ci clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(test): allow setup_snippet key in list_backends shape assertion
The engine self-test PR added setup_snippet to each backend entry but only
updated the route-shape test; test_list_backends_shape strict-asserts the key
set. Add setup_snippet there too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Two Model-management enhancements building on #908 (no re-do of its fixes).
Unify the two HF-token entry points. The Model Store toolbar saved the
token via /system/set-env (env var + HF-CLI file) while Settings →
Credentials saves to the encrypted app store — two stores with an
asymmetric clear path, so a toolbar-set token silently outlived the
Credentials "Clear" (a support-ticket generator). The toolbar now POSTs
the SAME canonical endpoint Credentials uses (/api/settings/hf-token →
encrypted store + huggingface_hub.login()), so there is one store with
one clear path. In-process parity is preserved (login() populates the HF
canonical file, so downloads pick it up immediately).
Surface an incomplete/partial cache. A truncated download (config landed,
weight shard didn't) occupies disk but used to read as a plain "not
installed". The backend already flags it as `incomplete`; the row now
shows an "incomplete · N MB" warn badge, relabels the primary action to
"Repair" (re-runs snapshot_download to finish the missing shard), and
offers a Delete to clear the partial bytes.
Tests: modelStoreTokenPath (toolbar hits /api/settings/hf-token, never
/system/set-env) + modelStoreIncomplete (badge, Repair→onInstall, Delete,
no false positives on normal not-installed/installed rows). Full vitest
green; lint + format clean. i18n keys added to en.json (models.incomplete,
incomplete_title, repair_btn, repair_title).
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(contact): make "Get in touch" a guided, well-typeset help page
Replace the flat 4-row link list (Discord / Email / Issues / Website) with
five guidance cards, each an icon + heading + a "use this when…" sentence so
users pick the right channel instead of guessing:
- Report a bug → reuses ReportBugButton (prefilled GitHub issue + scrubbed
diagnostics; nothing sent until the user reviews & submits)
- Request a feature / ask → GitHub Issues
- Get help & community → Discord (setup help, sharing dubs)
- Support the project → routes to the existing Support page (no Ko-fi
duplication)
- Report a security issue → GitHub Security Advisories (private, per
SECURITY.md)
Bigger, friendlier typography ("We'd love to hear from you" header, roomier
measure and spacing) and a container-reflow card grid (CSS grid auto-fit, no
viewport @media, so it stays correct under --ui-scale zoom). Email + website
kept as quieter direct channels. External CTAs are real <a rel="noreferrer">
links, keyboard-focusable, opened via the shared openExternal helper. All
strings go through i18n under contact.* with English defaultValues for locale
fallback.
Adds a ContactPage render test (sections render, each channel targets the
right URL, bug-report affordance present, Support routes to donate).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(i18n): prune 4 orphaned contact.* keys from 20 locales (Contact-page rewrite)
The Contact page rewrite renamed its i18n keys; the old keys lingered in the
20 non-English locales as orphans, failing the locale_no_orphan_keys probe.
Pruned; new keys fall back to English per i18n config.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Adds a way for companies and people to visibly support OmniVoice.
- config/sponsors.js: single source of truth — an (empty) SPONSORS array with
a documented { name, logoUrl, url, tier } shape + tier order, and a
SPONSOR_CONTACT object whose githubIssue is a prefilled, zero-token
"become a sponsor" issue (same pattern as the bug reporter) plus the Ko-fi
link and a SPONSORS.md docs URL. Logos are added here + in SPONSORS.md.
- LogsFooter: a compact "Sponsors" link next to the donate heart (a link, not
a logo strip in the 28px bar) that opens the in-app Support/Sponsors view.
- SupportPage: a Sponsors section — logo grid grouped by tier when populated,
a tasteful outlined "be the first — your logo here" slot while empty, a
primary "Become a sponsor" button opening the prefilled issue, and a
one-line explainer linking to SPONSORS.md.
- SPONSORS.md: what sponsors get + how to become one, kept in lockstep with
the config.
- All strings via i18n (support.sponsors_* / logs.sponsors) with English
defaultValues so non-English locales fall back cleanly. Logo links are lazy,
max-height capped, aria-labelled, rel="noreferrer", and open in the system
browser via the app's external-open helper.
- Test: SupportPageSponsors renders the empty placeholder + asserts the
become-a-sponsor CTA targets the contact URL, and (with injected sponsors)
that each renders as an external logo link.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Add a sponsorship home (SPONSORS.md) with Backer/Bronze/Silver/Gold tiers —
described as placements/benefits, with $ amounts left as `<!-- OWNER: set
amounts -->` placeholders (no invented prices). Primary "become a sponsor"
path is a prefilled GitHub issue form (.github/ISSUE_TEMPLATE/sponsor.yml:
name/org, logo URL, tier, contact), with Ko-fi/PayPal as direct paths and an
OWNER placeholder for a public contact email.
README gains a Sponsors subsection (logo-slot placeholder + SPONSORS.md link),
a Sponsors nav entry, and a note about GitHub's native Sponsor button.
FUNDING.yml adds the SPONSORS.md link alongside the existing ko_fi/PayPal.
Keeps the honest "agent bills" framing; sponsorship is a thank-you, not a
paywall — OmniVoice stays fully free and AGPL-3.0. Docs-only; no fabricated
sponsors, prices, or testimonials.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
GitHub's release-list sidebar clips the title mid-string, hiding the version
when it trails 'OmniVoice Studio'. Name stable releases 'vX.Y.Z — OmniVoice
Studio' and the preview 'Preview — OmniVoice Studio'. Existing releases were
renamed to match.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The PR #909 data-safe-update tests passed in isolation but failed only in
full-suite CI order. Two independent, order-dependent leaks were at play:
1. Module-identity leak (the #878/#894 class). The `isolated_db`/`fresh_app`/
`fresh_resolver` fixtures in tests/backend/** purge `core.*`/`services.*`
from `sys.modules` and never restore them, so `sys.modules["core.db"]`
afterward is a DIFFERENT object than the one the migration-safety tests
imported at collection. `monkeypatch.setattr("core.db.DB_PATH", ...)`
re-resolved the dotted string to the re-imported module, while
`_run_alembic_upgrade`/`init_db` (bound at collection) kept reading the
ORIGINAL module's globals — so the patch missed and the upgrade ran against
the ambient session DB. Result: no backup at the asserted path, and the
mid-flight-failure injection never hit the expected DB (DID NOT RAISE).
The same divergence hit the lazy `from core import db_backup` inside
`_run_alembic_upgrade`, so patching `MAX_BACKUP_DB_BYTES` was silently lost.
2. Logger-disable leak. Alembic's env.py called `fileConfig(...)` with the
default `disable_existing_loggers=True`, which disabled the already-created
`omnivoice.db.backup` logger the first time any earlier test ran a real
`alembic upgrade` — so the oversized-DB "Skipping pre-migration DB backup"
line was never emitted and the caplog assertion failed. This also silently
mutes the live app's logging after a real startup migration.
Fixes:
- env.py: `fileConfig(..., disable_existing_loggers=False)` so a migration
never mutes the app's (or another test's) loggers.
- core/db.py: import `db_backup`/`APP_VERSION` at module level so
`_run_alembic_upgrade` uses a stable reference immune to a `sys.modules`
purge, matching what tests patch at collection.
- test_db_migration_safety.py: patch DB_PATH on the imported `core.db` module
object rather than the re-resolvable dotted string — the correct,
self-contained seam.
Verified: the four migration-safety tests + the oversized-backup test pass in
full-suite order and in isolation; full `pytest tests/` is green
(2206 passed, 0 failed).
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
PR #904's deck-of-cards fan pinned the seven Launchpad feature cards
(Voice Clone, Voice Design, Video Dubbing, Stories, Audiobook, Voice
Gallery, Transcripts) inside a fixed ~780px box, leaving dead margins on
a maximized display. Replace it with one full-width grid that fills the
content edge-to-edge and reflows its column count from a maximized
~2560px display down to the 900x600 minimum.
- LaunchpadDeck renders a single `.lp-cards` grid at every shell width
(no deck-vs-fallback split): `repeat(auto-fit, minmax(--lp-card-min,
1fr))` derives the column count from the grid's OWN width, so columns
reflow 7->1 with zero viewport @media (which fire at the wrong width
under the shell's `zoom: --ui-scale` model). Every column stretches
(1fr) -> no dead margins, no horizontal scroll.
- The only responsive knob is `--lp-card-min`, set inline from
useShellNarrow (the `.app-container` shell-narrow/shell-mini own-width
classes): 200px wide, 240px narrow -> fewer, comfier columns on narrow
shells. No viewport media queries.
- Cards keep #904's character: animated waveform faces, cursor
spotlight + eternal breath ring (phase-offset per card via --lp-i),
and a hover/focus-forward raise (`lp-action-card--raised`) driven from
React state so pointer and keyboard share one path. Reduced-motion
freezes the waveform. All 7 navigation targets and i18n keys preserved.
- Removed the old `.lp-deck*` fan CSS, the ActionCard narrow fallback,
and the launchpad viewport @media overrides. Rewrote the regression
suite to assert full-width grid layout, the narrow-vs-wide floor, and
the raise interaction for pointer AND focus.
Verified in a real browser (chromium): 7 cards fill the full width in
one row at 2472px content, reflow to 3 columns at 920/876px, and the
grid width equals the container at every size (no overflow).
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
New Settings → System → LLM Skills area: every LLM-powered capability
(Cinematic & Autofit translation, speech-rate slot fitting, glossary
auto-extract, direction parsing, dictation cleanup) becomes a "skill" the
user can toggle or route to a specific provider (local Ollama/LM Studio vs
a remote key) instead of everything riding the one global active provider.
Backend:
- services/llm_skills.py — skill registry + settings_store persistence
(llm_skill.<id>.enabled / .provider), resolution precedence
override > active > none, resolve_skill_client() (OpenAI-compat client
bound to the effective provider; None when disabled/unconfigured) and
skill_backend() (OffBackend when disabled — the exact no-LLM object every
caller already degrades on).
- All five consumption points wired through the registry; a disabled skill
degrades exactly like "no LLM configured" today (Fast translation
fallback, refinement pass-through, heuristic direction parse, no-llm slot
fit, 503 on glossary auto-extract). No new degradation modes; defaults
(enabled + no override) keep existing setups byte-identical.
- OpenAICompatBackend gains an optional bound provider (None = active, the
historical behavior).
- GET /api/settings/llm-skills + PUT /api/settings/llm-skills/{skill_id}
(404 unknown skill/provider); route snapshot updated.
Frontend:
- LLMSkillsPanel (Sparkles, next to LLM Providers): one row per skill —
i18n name/description, enable toggle, provider Select ("Use active
provider" + configured providers, local ones tagged), ready /
needs-setup badge linking to LLM Providers. All strings via t()
(settings.llmskills_*).
Tests: 30 backend (precedence, per-consumption-point disabled semantics,
endpoint round-trips, validation) + 4 panel render/PUT tests. Docs:
translation-engines.md gains an LLM Skills section.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Nine PRs (#904-912) shipped without their changelog bullets (agents were
kept off CHANGELOG.md to avoid merge conflicts across the wave); this
backfills them per the changelog hard rule.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
P0 — Cinematic/Autofit silently no-op'd on argos/nllb/openai. Those three
branches returned BEFORE _maybe_cinematic, so only the deep_translator
fall-through reached the refine/fit pass. A user on the DEFAULT Argos engine
who picked Cinematic/Autofit got plain Fast output with a success toast and
no quality_used/cinematic_skipped/rate_ratio. All three now route through
_maybe_cinematic. provider=openai is already an LLM translation, so it skips
the reflect/adapt re-refine (new already_llm flag) but still stamps
rate-ratio badges and runs the Autofit fit pass; the dialect it baked into
its translate prompt is now reported applied.
P1 — the Autofit fit pass ran one blocking adjust_for_slot per segment in the
merge loop, OUTSIDE any budget (a 50-seg dub vs a slow provider spun
~50×timeout unbounded). New speech_rate.adjust_for_slot_many fans it out
concurrently under a wall-clock deadline SHARED with the cinematic refine;
segments still running at the deadline degrade to their literal with
rate_error='fit-budget'. Also set max_retries=0 on the OpenAI clients used
for translate/refine/fit so a 429 + Retry-After can't sleep through the budget.
P2 — glossary auto-extract's no-LLM message now points at Settings → LLM
Providers (was the stale TRANSLATE_BASE_URL/TRANSLATE_API_KEY). Provider error
bodies on the glossary auto-extract, the OpenAI translate-segment path, and the
DeepL/Microsoft translate-segment path are now scrubbed
(core.scrub.scrub_provider_error) — they could echo the API key / a user_id.
DubTab re-polls LLM availability on window focus / visibility so configuring a
provider in Settings lifts the Cinematic gate without a remount. Documented
LLM_DEFAULT_PROVIDER in docs/dubbing/translation-engines.md.
Tests: fail-before/pass-after for argos+cinematic (refine runs), argos+cinematic
no-LLM (cinematic_skipped), argos Fast (rate_ratio stamped), openai+autofit
budget bound, and provider-error scrubbing on the translate + glossary paths.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
P0 — Refinement blocked every dictation final with no timeout. With refinement
auto:true and a slow/dead LLM endpoint, maybe_refine ran unbounded and blocked
the final send in all three capture_ws handlers (~51s measured; the pill hung
"Transcribing…" until the widget's 15s fallback fired). Fix the class: a hard,
env-tunable budget (OMNIVOICE_REFINE_TIMEOUT_S, default 4s) via a new
maybe_refine_async — a slow/dead endpoint now falls back to the unrefined (but
polished) text within the budget and can NEVER delay the final beyond it. The
LLM HTTP call is bounded to the same budget so the orphaned worker unwinds
instead of holding a connection for the client's full 45s. Refinement is now
also fully best-effort in the legacy handler (it can't turn a good final into
an error frame).
P1 — REST /transcribe lacked polish parity. capture.py never applied
polish_text, so REST returned raw "…test" while the WS returned "…test."
Apply text_polish.polish_text to `text` and `refined_text` (segments stay raw),
so the widget POST fallback and MCP/CLI callers match the live socket.
P1 — The #888 "instant first dictation" preload was a no-op. The preload called
warmup() only `if hasattr`, but SherpaDictationBackend had none, and the WS
handlers built a FRESH backend per session so a warm singleton wasn't reused.
Add SherpaDictationBackend.warmup() (builds the recognizer) and share one warm
recognizer per model id across sessions (get_sherpa_dictation_backend, same
invalidation + a shared lock as the capture singleton); each session keeps its
own decode stream. First dictation no longer pays the 1.3–2.5s load.
P1 — llm_ready is a lie (feeds the P0). It only means "an endpoint is
configured", so a placeholder key reads as ready. The P0 timeout makes a dead
endpoint harmless; add last_refine_status so RefinementPanel flags a
configured-but-failing LLM and links to LLM Providers → Test.
Regression tests (fail-before/pass-after): slow-LLM WS final arrives < budget;
maybe_refine_async hard timeout + status; REST polish parity + refined_text
polish; warmup builds the recognizer and a second session reuses it; the panel
honesty note. Backend refinement/capture_ws/capture/sherpa suites, CJK + route
inventory gates, full vitest (733), lint (0 errors) and format all green.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Backend:
- core/db_backup.py: WAL-safe SQLite snapshot to omnivoice.db.backup-<version>-<n>
before pending alembic migrations run; keep newest 3, prune older; skip >500MB
with a log line. Restore is never automatic.
- core/db.py: _run_alembic_upgrade now plans the run (up_to_date / pending /
unknown_revision), snapshots first when migrations will execute, and raises
MigrationError on a mid-flight failure — startup stops with the backup path
named instead of continuing on a half-migrated DB. The #552/#547
unknown-revision class stays non-fatal (warn + additive reconcile).
- core/changelog.py + GET /api/settings/changelog: parse the shipped
CHANGELOG.md (single-line and wrapped bullet styles) into structured releases.
- GET /api/settings/db-backup: newest pre-migration backup for the panel.
Rust (bootstrap.rs):
- #314 heal guard: an exit-signature match alone can no longer delete the venv —
venv_rebuild_justified requires a structural problem or a failed direct
interpreter probe; a venv that probes healthy is kept and the real error
surfaced. Drift/repair remains in-place `uv sync` (non-destructive).
- CHANGELOG.md now ships as a bundle resource and is copied/refreshed into the
project dir so the changelog endpoint works in packaged installs.
Frontend (Settings → Updates):
- Available update shows its actual release notes (updater metadata body)
through a safe markdown-lite renderer (text nodes only, refs stay plain).
- "Your data is backed up before every update" line with the latest backup
timestamp from the new endpoint.
- "What's new" changelog reader (accordion, newest expanded) over the shipped
CHANGELOG.md; GitHub releases list reuses the same renderer.
- One-time, non-blocking "What's new" footer pill after an update
(persisted last-seen version; fresh installs baseline silently).
- All strings via t() with en keys (other locales fall back to English).
Tests: db backup/rotation/failure-path units, migration-safety units, changelog
parser (both bullet styles + real CHANGELOG.md), endpoint tests, route
inventory regenerated, Rust decision-logic + probe tests, vitest suites for
renderer/viewer/panel/pill logic.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Live-audit fixes for the Models settings surface — the P1s were cases where
the feature silently didn't work for the user.
P1-A — Async install errors were invisible. The `install_error` SSE event
carries excellent mirror-aware text (#890 core/failure.py), but the Model
Store auto-purged the errored row ~800ms later (same as a success) and the
first-run WizardLibrary DELETED the row without ever reading `ev.error`. The
SSE→rowState reduction is now a pure, tested reducer (downloadReducer.js /
reduceWizardDownloadEvent); only SUCCESS terminals auto-purge
(isAutoPurgeTerminal), an error persists on the row with inline text + Retry +
Dismiss (Model Store) / a Retry (wizard).
P1-B — No disk-space check on install. `POST /models/install` now compares the
FDL-05 plan's exact `to_download_bytes` (+ MIN_FREE_GB headroom) against
`shutil.disk_usage(cache).free` BEFORE downloading and emits an actionable
install_error naming the sizes (needs X, headroom Y, have Z) instead of failing
mid-download. `/models` also surfaces `disk_free_gb` in the header. MIN_FREE_GB
+ disk_free_bytes are single-sourced in setup/models.py (wizard delegates).
P2-A — Wired the orphaned cancel. `POST /models/install/cancel` (FDL-11) had
zero frontend refs; the in-progress row now shows a Cancel button that calls it
and transitions the row to install_cancelled.
P2-B — Honest restart_required. The HF-mirror PUT returned restart_required:true
unconditionally; it now returns true only when the persisted value actually
changed, with accurate copy (Model Store downloads use the new mirror
immediately — resolved per-call; only transformers model loads need a restart).
P3 — i18n the un-localized panels (HFMirrorPanel, ApiKeysPanel source
labels/help/status, MODEL_ROLE_LABEL) via new en.json keys; other locales fall
back to en.
Tests: new tests/test_install_disk_space.py (reject-when-over-budget incl. the
worker wiring; allow-when-fits; degrade on unknown size/unprobeable volume),
updated tests/test_hf_mirror_settings.py (change-only restart_required), and new
frontend reducer + column-render tests for install_error persistence, Retry,
Dismiss, and Cancel.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Settings → Storage now opens with a Disk usage panel backed by a new
loopback-gated GET /api/settings/storage endpoint:
- Per-volume totals (grouped by st_dev) + du-style sizes for everything
the app owns: the HF model cache (with its ~10 largest models), the
app data dir broken into voices/outputs/dub_jobs/batch/preview/
database/logs/other subtotals, engine venvs (backend/engines/*/.venv
+ the app venv), and omnivoice* entries in the OS temp dir.
- Bounded scanning: per-category 10 s deadline → partial totals with an
"unreadable" warning instead of a hung request; results cached
in-process for 5 minutes, ?refresh=1 forces a rescan; the walk runs
in a worker thread so the event loop never blocks.
- Server-side warnings reuse the setup wizard's MIN_FREE_GB: free <
min → critical, free < 2×min → low, volume holding the cache/data
>90% full → volume_pressure, unreadable/timed-out paths → unreadable.
The panel renders severity-colored banners, a data-volume gauge,
proportion bars per category, Open-folder buttons (existing
/export/reveal pattern), a Model Store jump for reclaiming model
space, and the existing clear-logs action on the logs row. A critical
warning is also surfaced outside Settings via the app-wide toast —
once per session. All strings via i18n (en fallback).
Tests: tests/test_storage_report.py (sizes, thresholds, cache/refresh,
timeout partials, endpoint wiring) + StorageUsagePanel.test.jsx
(categories, banners, once-per-session toast, refresh=1, error state);
route added to tests/fixtures/api_routes.txt via the dump script.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Live-audit fixes for Settings → LLM Providers / Translation.
Retire the legacy LLMEndpointPanel from the UI (backend endpoint kept).
TranslationTab no longer embeds the inline endpoint panel — it now points to
Settings → LLM Providers (openSettingsTab('llm-providers')), which fully covers
it via the `custom` provider (a lone TRANSLATE_BASE_URL still resolves to
`custom`). Kills the panel's lying "reachable" badge, its hardcoded-English
strings, and one of three duplicate TRANSLATE_* surfaces. The third duplicate —
TranslationTab's "Provider keys" collapsible — drops the TRANSLATE_* trio
(now owned by LLM Providers) and keeps only the DeepL/Microsoft translator
keys; its toast no longer claims "saved for session" (these are in
PERSISTENT_KEYS, restored at startup). GET/PUT /api/settings/llm-endpoint is
untouched (DubTab gates Cinematic off it; tests + route inventory cover it).
Surface env overrides. describe() now reports base_url_from_env / model_from_env
/ active_from_env (mirroring key_from_env). The panel disables env-pinned
base_url/model/account fields with an explainer, and — when
LLM_DEFAULT_PROVIDER pins the active provider — disables make-active and shows a
banner, instead of silently reverting the user's edit / no-oping the button.
Fix the Cloudflare account-id flow (broken two ways): describe() now returns the
stored account_id (the field no longer resets to empty) and shows the RAW
base_url template ({account_id} kept literal) instead of the substituted value;
save_overrides drops a base_url override equal to the built-in default, so the
UI posting the shown value back can't freeze the URL — later account-id changes
take effect again (also self-heals if a default URL changes in a release).
Fast-fail the Test / Fetch-models probes. Pass max_retries=0 to the probe
OpenAI clients so a 429/timeout returns in seconds instead of ~34s on the SDK's
default retry ladder. /models now returns truncated:true when capped at 200 and
the UI hint reads "first 200 shown".
Tests: registry env-flag + Cloudflare round-trip/no-freeze regressions; router
truncation + max_retries=0 assertions; panel disabled+explained + banner;
new TranslationTab test (pointer wired, legacy panel gone, TRANSLATE_* dropped).
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Six live-audit fixes for the Engines settings surface:
- P1-A: the Supertonic license dialog was dead since #101 — `useState`
threw away the state value (`const [, setLicenseDialogFor]`) and the
imported dialog was never mounted, so "Accept license" did nothing.
Keep the value and render LICENSE_DIALOGS[selected] with open/onClose/
onAccepted (accept → matrix reload).
- P1-B: the matrix went stale after "Use" — active badge, Use buttons and
family-tab captions stayed old until a manual Refresh. Await onSelect,
then reload() so the picked engine reflects immediately.
- P2-A: consume the /engines/select routing echo. A `cpu_fallback` pick now
shows a warn-tone toast naming the reason ("running on CPU — …"); the
plain success toast stays for accelerated/cpu_only. Shared helper used by
both Settings→Engines and the first-run WizardLibrary.
- P2-B: a CPU-native engine (gpu_compat == ("cpu",)) has nothing to fall
back FROM, yet on a GPU/MPS host it was mis-classed cpu_fallback (warn).
New routing rule classifies ("cpu",) as cpu_only (neutral) on any
accelerator host; multi-target engines that could accelerate elsewhere
are untouched.
- P3-A: the routing reason was only a badge `title` (unreachable on
keyboard/touch) — surface it as small visible text under the badge.
- P3-B: an in-process "Test engine" pass is an import/liveness check, not a
synthesis test — label it "deps OK" instead of a misleading "0 ms"
latency; subprocess rows keep their real ping latency.
Adds RTL + unit regression tests for all six and updates the routing unit
tests to the corrected cpu-native intent. i18n keys added to en.json.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The seven launchpad feature cards now render as an overlapping deck fanned
left-to-right: each card sits in a fan slot with a subtle tilt (±4°) and
vertical stagger (≤14px), peeking ~33% out from under its right neighbor.
Every card face carries its lucide icon + name on the always-visible peek
edge, a one-line description, and a decorative CSS-only animated waveform
strip in the card's accent color (stagger-delayed scaleY bars, aria-hidden,
static under prefers-reduced-motion).
Hovering OR keyboard-focusing any card brings it fully forward — it
straightens, scales up and takes the top of the stack while every other
card slides toward it and tucks underneath (dimmed, scaled down, overlap
increased). The raise/tuck classes are React-state-driven so pointer and
focus share one code path and tests can assert it. Fixed deck height —
zero layout jump.
Navigation targets, i18n keys, per-feature accent hues and profile/project
counts are unchanged; both renderings share a single feature list so they
can't drift. On shell-narrow/shell-mini (the app-container's own width
classes — not viewport @media, per the UI-scale rationale in App.jsx) the
deck degrades to the pre-existing flat ActionCard grid, tracked live via
MutationObserver (new useShellNarrow hook), keeping 900×600 usable.
New LaunchpadDeck.test.jsx covers: 7 cards in canonical order, every
navigation target (incl. clone/design → studio + defineMethod), raise/tuck
partitioning for hover and focus, waveform decorativeness, the narrow
fallback under both shell classes, and the runtime class flip.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(bootstrap): port cuDNN 8 (NVIDIA CUDA GPU) + VC++ redist install into ensure_venv_ready()
* fix(bootstrap): address #869 review — drop dead VC++ half, cache negative CUDA probe, gate on ROCm, sync docs
Per maintainer review on #869:
1. Drop the VC++ Redistributable half: LoadLibraryA("vcruntime140.dll")
from the running Tauri exe is a tautology (the exe itself links the
MSVC CRT, so the process wouldn't be running without it), and torch's
real failure mode is msvcp140.dll inside the venv python process.
Dead code removed; a comment records why for future readers.
2. Stop taxing every non-CUDA launch: a negative torch probe (CPU /
Intel / AMD — most installs) is now cached in a
.venv/.cudnn8_probe_negative marker, so the synchronous `import
torch` runs at most once per venv lifetime. Invalidated on every
path that can change the torch build (drift sync #307, repair sync,
first-run sync, ROCm reinstall) and implicitly by a venv rebuild.
A probe that fails to run cleanly is skipped WITHOUT caching so a
transient error can't wedge a real CUDA machine.
3. Rewrite docs/install/troubleshooting.md §10 to the actual root
cause: packaged installs never had the cudnn8_compat libs (so
reinstalling never restored them); the bootstrap now installs them
automatically on CUDA machines, with the manual uv pip command as
the offline fallback and PyTorch Whisper as the sidestep.
4. Gate the ~700 MB nvidia-cudnn-cu12 download on the venv torch being
a real CUDA build: the probe now reports 'hip' before checking
cuda.is_available() (which HIP spoofs), so opt-in ROCm installs
(#124) never fetch the CUDA wheel.
Also reflow the CHANGELOG entry to house style (bold one-line lead,
1-3 lines of why, (#827, #869) refs) and extend the bootstrap unit
tests: classify_cuda_probe verdict mapping and the marker
write/invalidate round-trip (6 cuDNN tests total, 43 lib tests green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Residual A — the chunked dub-stream had a PARALLEL wedge mechanism (its own
ping-loop timeout, its own _reset_pool_on_wedge, a dead-end "Try restarting
the server" message). A wedged chunk now routes through the SAME
run_transcribe_guarded bound+reset as the whole-file paths (#731/#851): the
guard resets the poisoned pool once per wedged attempt (no double-reset on
retry) and the user sees the actionable ASRTimeoutError. The reset logic is
extracted to asr_backend.reset_pool_after_wedge — one shared mechanism, so
the semantics can't drift again. run_transcribe_guarded also gains a
timeout_env param so chunk errors name OMNIVOICE_TRANSCRIBE_CHUNK_TIMEOUT_S
instead of the whole-file knob.
Residual B — the crash-isolated ASR sidecar (#393, faster-whisper-isolated)
is wired as an explicit ESCAPE HATCH, not a default:
- selectable end-to-end: Settings engine list gets an explanatory
install_hint; honest gpu_compat ("cuda","cpu" — it wraps the same
CTranslate2 engine as faster-whisper); get_active_asr_backend now hands
back a process-wide singleton for subprocess-isolated backends (a fresh
instance per request would leak atexit hooks and respawn the sidecar —
reloading its model — on every transcribe).
- on the SECOND consecutive guarded timeout-with-reset in one session
(resets aren't recovering the hang; the wedged thread keeps its VRAM),
the error the user sees + the log recommend switching to the isolated
engine in Settings → Engines. Never auto-switched (owner rule: no silent
behavior divergence); a completed transcribe resets the streak.
Tests (fail-before/pass-after verified against origin/main): wedged-chunk
SSE integration (reset count + actionable error + recommendation surfaces),
consecutive-timeout streak (fires at 2, resets on success, suppressed when
already on the isolated engine), timeout_env parametrization, shared-reset
helper, isolated backend in list_backends with hint + honest availability,
singleton caching, gpu_compat matrix entry. Docs: troubleshooting §14 gains
the chunk knob + escape-hatch guidance.
Closes the residuals tracked on #730.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Root cause: LLM provider selection reads three process-global surfaces —
env vars (LLM_DEFAULT_PROVIDER, per-provider *_API_KEY/*_BASE_URL,
TRANSLATE_*), the SQLite settings store (llm.active_provider & co.), and
prefs.json (llm_backend). Importing `main` (TestClient fixtures do)
dotenv-loads the developer's .env and ~/.config/omnivoice/env straight
into os.environ, and several tests/endpoints mutate these surfaces
without teardown — so whichever test imported the app first flipped what
later tests' active_backend_id()/active_provider_id() resolved to
(order-dependent failures in test_engines.py,
test_llm_endpoint_settings.py, test_llm_providers.py).
Fix the class, not the instances:
- tests/conftest.py: redirect OMNIVOICE_DATA_DIR to a per-session tmp dir
and OMNIVOICE_ENV_FILE into it (before collection freezes
core.config.DATA_DIR), so tests never read or write the developer's
real app state and local runs behave like clean CI.
- tests/conftest.py: autouse `_isolate_llm_provider_state` fixture
snapshots env (derived from llm_providers._PROVIDERS, so new providers
are guarded automatically), llm.* / secret.llm_key.* settings rows, and
the prefs llm_backend/env.TRANSLATE* keys before every test and
restores them exactly afterwards.
- shared `clean_llm_env` fixture clears the FULL provider env surface;
the four LLM test modules' hand-picked partial delenv lists (which left
e.g. LLM_DEFAULT_PROVIDER / OPENROUTER_API_KEY standing) now use it.
- tests/test_llm_state_isolation.py: deterministic fail-before/pass-after
regression pair — pollutes all three surfaces without cleanup, then
asserts the guard restored them.
Verified: the issue's two-test repro passes; the five LLM-related test
files pass in order; full suite green (2046 passed, 20 skipped,
10 xfailed, 4 xpassed).
Fixes#878
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
After an unclean shutdown (Windows BSOD), the WebView2 profile cache
(%LOCALAPPDATA%\com.debpalash.omnivoice-studio\EBWebView) can corrupt:
Tauri's IPC custom protocol fails AND the postMessage fallback breaks,
so invoke() hangs forever. useBootstrapStage's poll loop rode entirely
on that IPC — a hung bootstrap_status call silently killed the loop and
the splash sat at "preparing" forever, even with a fully healthy
backend answering over plain HTTP.
Class fix, three parts:
- splashWatchdog.js: IPC-independent escape hatch. If no IPC signal
arrives within 10s, poll GET /health over plain HTTP; healthy →
proceed to the app as if 'ready' was received (console.warn
breadcrumb so diagnostic bundles carry it). Any successful IPC
response disarms it for good.
- Recovery panel (stage 'ipc_lost'): if neither IPC nor HTTP succeed
within 45s, show an actionable panel instead of the infinite
spinner — "Open logs" (with an inline path fallback when IPC is
dead) and, Windows-only and only in this error state, "Repair and
restart". Health polling continues behind the panel so a slow
first-run install with broken IPC still reaches the app.
- clear_webview_cache_and_relaunch (Rust): writes a marker and
relaunches; the fresh process deletes EBWebView at the top of run()
before any webview exists (WebView2 holds locks while running),
with a bounded retry while the old instance exits. Runtime cfg!
guards keep the whole path compiling on every platform.
Tauri 2 exposes no reliable flag for the postMessage-fallback mode
(closure-local in its injected ipc.js), so the logged detector is the
observable combination: zero IPC signals + working plain HTTP.
Fail-before/pass-after regression tests: hung invoke + healthy HTTP →
ready; hung invoke + dead backend → recovery panel, then auto-continue;
working IPC → normal path untouched, zero HTTP polling. Plus watchdog
state-machine unit tests and recovery-panel render/interaction tests
(6/7 fail on the pre-fix component). Troubleshooting doc gains the
matching section (docs-sync).
Fixes#879
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
A kittentts first-use HuggingFace download died with httpx's "Cannot send
a request, as the client has been closed", and the generation error
classifier's catch-all fallback told the user (CPU-only ~80 MB ONNX engine,
12 GB-VRAM box) they were OUT OF MEMORY and to press Flush — the wrong
remedy for a network failure.
Three-part class fix:
- generation.py: new #880 branch (before the OOM hint) classifies
httpx/requests transport failures — matched over the whole exception
chain (type names like ConnectError/ReadTimeout plus stringified
signatures like "client has been closed") — as a download/network
problem with a retry/check-connection remedy.
- generation.py (the real class bug): the OOM hint is no longer the
catch-all. It now requires an actual OOM signature (typed
OutOfMemoryError/MemoryError anywhere in the chain, or CUDA/MPS/CPU
allocator wording); genuinely unknown errors surface as unrecognized
with the underlying detail instead of a false "ran out of memory".
- tts_backend.py: KittenTTS's first-use load retries exactly once with a
fresh HF Hub client (huggingface_hub.utils.close_session()) on the
specific closed-client failure — hub ≥1.x shares one global httpx
client, and a closed one is recoverable, so the download self-heals
instead of failing the generation.
Fail-before/pass-after tests: classifier (closed-client message, wrapped
httpx type names, unknown error, real OOM signatures incl. typed
OutOfMemoryError, WinError 1455) + the retry helper (recovers once,
walks the chain, no retry on unrelated errors, single-shot).
Fixes#880
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
When a non-default HF_ENDPOINT (Settings → Models → Hugging Face mirror,
e.g. hf-mirror.com) is configured and a model load/download fails with a
connectivity error, the raw transformers message ("We couldn't connect to
'https://hf-mirror.com' to load the files…") leaked to the UI as a bare 500
with no next step.
Class fix — one shared classifier in core/failure.py covers every surface:
- classify()/build_failure(): new HF_MIRROR_UNREACHABLE class with a dynamic
hint that names the configured mirror, says it may be down, points at
Settings → Models → Hugging Face mirror, suggests the official endpoint
when the model isn't cached, and notes the restart requirement (HF reads
HF_ENDPOINT at backend start). Checked before the video-download network
class so a model download's "timed out" no longer gets the "video server"
hint. Feeds /model/status and every build_failure event (dub, tasks).
- main.py global 500 handler: appends the hint to the surfaced detail, so
ALL routes that can leak a model-load error benefit (generate, dub,
archetypes, …), not just TTS generate.
- setup/download.py install SSE: the install_error event gets the same hint.
- error_journal: "couldn't connect to" / "max retries exceeded" now classify
as NETWORK_ERROR (was UNKNOWN) for auto-attached bug reports.
- model_manager (#886 family): the "cache incomplete and could not be
auto-repaired" message now names WHY the auto-repair failed (mirror
outage, offline mode, full disk no longer read identically), which also
lets the mirror hint fire on that surface when applicable.
Fail-before/pass-after regression tests in tests/test_hf_mirror_error_class.py
(12 of 13 fail on main).
Fixes#874
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
torch >=2.3 ships no macOS x86_64 wheels (transformers 5.x needs torch >=2.6),
so `uv sync` can never resolve on an Intel Mac — per the platform-parity rule
the honest option is declaring the platform unsupported, not letting first
launch die in a raw resolver error:
- bootstrap.rs: pre-check on macOS x86_64 before any venv create / uv sync
(first-run AND repair paths) fails fast with an actionable message
(remote-backend escape hatch + docs link); healthy pre-torch-bump venvs are
deliberately untouched. Unit test pins the message's load-bearing phrases.
- BootstrapSplash: routes the failure to a dedicated localized hint
(bootstrap.hint_intel_mac, all 21 locales) and suppresses the useless
Retry-oriented hints for it.
- README + docs/install/macos.md (+ troubleshooting #9): every Intel-Mac
support claim now says UI-installs-but-backend-cannot-run, including the
from-source path (also broken); remote backend documented as the only use.
- release.yml: #889 note on the macos-15-intel leg — artifact is UI-only;
keep-or-drop is an owner call, deliberately not changed here.
- docs/install/windows.md: new "Portable install (Windows)" section promised
in #766 — custom MSI wizard folder / msiexec INSTALLDIR=..., what lives in
OmniVoiceStudio-Data next to the exe, and the Program-Files-greyed-out why.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The per-segment call already has a 45s timeout and concurrency is capped, but a
slow or rate-limited provider on a large dub (hundreds of segments) can still keep
the "Translating…" spinner spinning for minutes as segments queue through the
bounded pool. There was no ceiling on the *whole* pass.
Add an overall wall-clock budget, OMNIVOICE_CINEMATIC_BUDGET_S (default 180s,
<=0 disables). Segments that finish in time keep their cinematic refine; any still
in-flight when the budget hits is cancelled and degrades to its literal (Fast)
translation with error="cinematic-budget", so the translate ALWAYS returns instead
of hanging. Order and length of the result are preserved. Abandoned executor
threads follow the same fire-and-forget pattern as the GPU-pool wedge guard (#730).
Regression tests: a 3s-per-segment refine under a 0.3s budget returns in <2s with
literal fallbacks; budget<=0 runs every segment to completion.
Co-authored-by: mergetest <test@local>
Two bugs from real reports:
1. LLM not wired — translator._llm_client()/_llm_model() read TRANSLATE_*/OPENAI_*
directly, bypassing the LLM Providers registry (#854). So a provider set up
in Settings → LLM Providers never powered Cinematic/Autofit. Now resolves the
ACTIVE provider (base_url/key/model) via llm_providers; the 'custom' provider
still maps TRANSLATE_* so legacy env setups keep working.
2. Transcription 'missing the beginning' — the chunked dub transcribe dropped a
whole chunk's window on failure/timeout (returned empty segments, no retry).
A transient wedge on the FIRST chunk (whisperx cold-loads its model there, the
#730 hang) therefore lost the start and left only middle+end. Now retries a
failed/timed-out chunk once on a fresh pool (OMNIVOICE_TRANSCRIBE_CHUNK_ATTEMPTS,
default 2) so the recovered chunk fills the hole.
Imports + dub_transcribe/translator/llm_providers tests green.
Co-authored-by: mergetest <test@local>
Redesign ArchetypeCard for a calmer visual hierarchy and design-token
surfaces, no behavior change (all props/handlers/loading states identical).
- Replace hardcoded surfaces with tokens: chips/wand/preview bg → tokens
(bg-white/[0.05] → --color-bg-elev-2, bg-white/[0.03] hover → --chrome-hover-bg),
text → --color-fg / --color-fg-muted / --color-fg-subtle, and the literal
#1d2021 hover text on Use voice → --color-fg-inverse.
- Borderless by direction: drop the hover/state border classes on the action
buttons and the card; convey hover via background tint + text color and the
playing state via an accent box-shadow ring (no literal/token borders).
- Hierarchy: name is the focal point (semibold, --color-fg); metadata line is
smaller/muted (--color-fg-muted) so it recedes.
- Chip row renders only when there are chips (no empty min-h reserve); the grid
stretches rows so mt-auto still bottom-aligns actions.
- Accent used tastefully: tinted Use voice → solid accent on hover/focus with
inverse text; favorite star stays subtle until hover/active. Focus-visible
rings intact.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>