Review findings from greptile on this PR.
P1 — a profile-derived language refused by a *remote* worker never
reached the new branch. The worker's ValueError travels home as
RemoteJobFailed, a GatewayError caught ahead of the ValueError handler,
so the user got a retryable 503 offering "run it on this machine
instead" — which cannot help, because the same engine refuses the same
language wherever it runs. Both handlers now build the response through
one helper, so the local and remote paths cannot drift again. Only
language refusals change class there; a genuine worker failure keeps its
retryable 503 and its "run it here" offer.
P2 — a *generic* rejection was wrapped twice. `_language_rejection_or`
adds the engine remedy before the handler sees it, and the profile
message then quoted that wrapper, repeating both the engine-switch
advice and "Engine's own message:". The wrapper now keeps the engine's
own error reachable and the profile message quotes that, once. The
Kokoro path never showed this because its wording is self-describing and
skips the first wrapper, which is why the first cut's assertion passed.
Three regression tests, each failing before this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A user on mlx-audio with the language picker on "Auto" got:
400: mlx-audio's Kokoro model doesn't support language='Persian'.
… Pick one of those, leave language as 'Auto', or switch to a
multilingual engine …
They had left it on Auto. The UI omits `language` entirely while its
picker reads "Auto" (useProfiles.js only appends a non-Auto value), and
#533 fills that gap from the selected voice profile. So Auto is exactly
how 'Persian' got there: the remedy the message leads with is the state
the user was already in, and nothing points at the profile that actually
supplied the language. Three generate attempts in their action log, a
detour through Settings, then the report.
Provenance was the missing fact, and only the request scope has it — the
engine adapters are handed a language with no idea who chose it. So
_resolve_profile_conditioning now reports whether it filled the language,
and /generate uses that to answer a refused profile language by naming
the profile and the remedies that exist: change the profile's language,
pick a supported one explicitly, or switch engine.
An explicitly requested language is untouched — the user really did pick
it, so blaming the profile would be a lie — and #533 still drives
generation whenever the engine can speak the profile's language.
Recognising the refusal needed one more thing: Kokoro's wording
("doesn't support language=…") matched none of #1257's signatures, so
the engine that issue was written for was the one engine its rewrite
never fired for. That wording is now recognised, but kept out of #1257's
rewrite path — it already names its engine and its languages, and
re-wrapping it only nests "Engine's own message:" twice.
No per-model language map: #1257 weighed that and chose engine-naming
over "a brittle map that goes stale on each engine update". This follows
the same principle — say where the language came from, don't enumerate.
docs/engines/mlx-audio.md repeated the same "leave language on Auto"
advice and is corrected here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Installing a gated model failed the fast download path with "401
Unauthorized" even with a valid token and the licence accepted, then fell
back to snapshot_download and logged a 401 that reads like the token or
the licence grant is at fault when it is neither (#2163).
`token_resolver.resolve()` returns a ResolvedToken record, not the bearer
string. Two call sites handed that record straight to consumers typed
`token: str | None`, and both fail silently rather than loudly:
- `_segmented_snapshot` passes it to HfApi, get_hf_file_metadata and our
own segmented_download. huggingface_hub's build_hf_headers ignores a
non-str token and falls back to its own ambient discovery, so a token
held only in VoiceStudio's Settings produces NO Authorization header
and every gated file 401s. segmented_download instead interpolates it
into `f"Bearer {token}"`, sending a malformed header that also inlines
the raw secret into the request.
- `_step_fetch_weights` passes it to snapshot_download, so gated engine
weights 401 the same way.
Every other resolve() caller already unwraps `.token`; these two were the
outliers. Both now unwrap once, at the seam.
The existing weights tests all stubbed resolve() to return None, so no
test ever exercised a resolved token — which is why this went unnoticed.
The new tests drive a real ResolvedToken through both seams and assert a
`str` reaches every consumer, plus an integration test that installs the
pyannote diarisation pipeline end to end: a weightless config_only repo
validates, both dependency repos are fetched, and every call carries the
bearer string.
Two catalogue invariants keep the rest of #2163 from returning by edit:
dependency repos must be revision-pinned (revision_for raises otherwise,
so an unpinned one ships an always-failing install), and a config_only
entry must declare config_required_files (without them the completeness
check can never pass and the error lists no files at all — the shape the
report hit on 0.5.2).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ctranslate2 <=4.4.0 marks its native library's stack RWE; kernels that
refuse the request fail the dlopen, taking whisperx, faster-whisper and
Argos translation down. main learned to detect and fall back; this also
fixes the library: core.execstack clears the one ELF bit in place on
first probe (Linux-only, memoized, never raises), so the engines load
normally on hardened kernels. Argos's probe moves to
argostranslate.translate so it cannot advertise an engine whose every
request 500s, and the dub route returns one actionable 400.
Also: pytorch-whisper survives a CUDA OOM at transcribe time by
stepping the batch down (16->4->1, 8->2->1 with word timestamps) and
finishing on CPU rather than dropping the chunk.
On a Tesla T4 the backend exited during the first /generate with no
traceback and no HTTP response, leaving the client with
RemoteDisconnected and every later call with ConnectionRefused. Three
separate defects combined, which is why none of the reporter's
workarounds helped.
1. torch.compile(mode="reduce-overhead") captures CUDA graphs. T4
(sm_75) passed the existing arch gate, so capture was attempted and
aborted the process from inside the native CUDA library — below the
interpreter, where neither the #278 eager-fallback wrapper nor any
except clause can see it. The compile mode is now resolved per GPU:
Ampere (sm_80) and newer keep the cudagraph mode, older cards drop to
the non-cudagraph "default" mode and keep their compiled Inductor
kernels. Fails open on any probe error, so no GPU that works today
loses the optimization. OMNIVOICE_FORCE_CUDAGRAPH=1 restores it.
2. should_torch_compile() never read TORCH_COMPILE_DISABLE. main.py sets
it on win32, build_engine_env injected it into subprocesses, and
docs/install/windows.md tells users to export it — but the in-process
gate ignored it, so the reporter exported the documented variable and
still got "torch.compile applied". The gate now honours
TORCH_COMPILE_DISABLE / TORCHDYNAMO_DISABLE / TORCHINDUCTOR_DISABLE on
every platform, and an env opt-out on the parent propagates to engine
subprocesses. The settings DB path is logged alongside the toggle:
the reporter had three omnivoice.db files and edited one the backend
never opened.
3. Settings -> Performance -> "Disable torch.compile" was rendered
disabled outside Windows in both the Tauri and Electron UIs, so the
one control that would have stopped this was unreachable for the
affected Linux user. The toggle is now live on every platform, and
build_engine_env honours it everywhere rather than only on win32.
Also arms faulthandler before torch is imported, so a fatal native
signal writes the faulting thread's Python stack to backend_err.log
instead of the process vanishing silently. This does not prevent a
crash; it makes one diagnosable. OMNIVOICE_DISABLE_FAULTHANDLER=1 skips
it.
Tests fail before / pass after, verified by stashing the source and
running the new tests against unfixed code. The crash test kills a real
child interpreter with a real SIGSEGV and requires a named Python frame
in the output. test_torch_compile_path_gate's fixture now clears the
compile-disable env vars: main.py setdefaults them on win32, so on a
Windows runner they leaked into os.environ and decided those tests.
Not verified on real hardware — no Turing GPU available. The sm_80 floor
is inferred from the crash report and from docs/hardware-notes-tesla-t4.md,
which already flagged cudagraphs on T4 as attempted by default and never
evaluated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- _ping_while retrieves failures that occur after the consumer disconnects,
preventing "exception was never retrieved" at GC; the early-exit test
covers this case
- stream_cut_backend_alive_local now builds the Settings → Logs → Backend
path from each locale's actual UI labels (`settings.title`,
`settings.logs`, `common.backend`)
- Fix incorrect section names in 15 locales, including ru, de, and zh-TW
Addresses CodeRabbit review on #2138
`_ping_while` runs awaited work as its own task, so a client disconnect
previously cancelled only the ping loop. Reference-text refinement could
continue running while `run_transcribe_guarded` skipped its abandon path
and the stream finalizer unloaded the ASR model underneath it.
Cancel unfinished work on early exit, matching the cancellation behavior
of the bare `await` it replaced. Do not await it in `finally`, which can
also run under `GeneratorExit`.
- **dub_core**: `_ping_while` cancels an unfinished future in `finally`,
covering all seven call sites
- **test**: verify that `_ping_while` cancels work on `aclose()` while
leaving completed work untouched (fail-before / pass-after)
- **CHANGELOG**: add a Highlights entry for the user-visible fix; keep
Fixed entries under `### Fixed` per `CLAUDE.md`
Addresses Greptile P1/P2 on #2138
After transcription, several minutes of backend work could produce no
SSE bytes, causing the desktop webview to close the idle connection while
the job continued and eventually completed. This also led to a misleading
reverse-proxy error on local connections.
- dub_core: keep post-transcript awaits alive with `_ping_while` (5s pings)
- dub_export: send SSE comments after 15s of stream silence
- backendCrash.ts: show deployment-aware connection-loss guidance
- Add regression tests for post-transcript pings, task-stream keepalive,
and local-mode error messaging
Fixes#2108
The SRT/VTT formatters in dub_export and openai_compat truncated
(seconds % 1) * 1000. Most decimal times are not exact in binary (2.3 is
2.29999...), so a cue imported as 00:00:02,300 exported as 00:00:02,299:
every such cue moved a millisecond early in the /dub/srt and /dub/vtt
downloads, burned-in subtitles, and /v1/audio/transcriptions srt/vtt.
All four now call srt_parser.format_cue_timestamp, which rounds the whole
value to milliseconds once and splits it, so 59.9996 carries to
00:01:00,000 rather than printing ",1000" -- the same round-then-divmod
shape karaoke_ass._ass_time already uses.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
/dub/import-srt fell back to Latin-1 when UTF-8 failed, so a UTF-16 .srt
(Notepad's "Unicode", many subtitle editors) decoded with a NUL between
every character and was rejected as having no cues, and a Windows-1252
one turned curly quotes and dashes into C1 control characters.
/audiobook/import decoded .txt/.md with errors="ignore", silently dropping
every accent, dash and curly quote from a Windows-1252 manuscript,
returning NUL-interleaved text for a UTF-16 one, and keeping a UTF-8 BOM
at the start of the editor text.
Both now use decode_text_upload: a BOM names the encoding, valid UTF-8
stays UTF-8, and anything else is read as Windows-1252, with Latin-1 for
the bytes cp1252 leaves undefined so the decode never raises.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The rename to "the engine's Weights list in Model Catalogue" left several
messages without a verb, and pointed others at the wrong place:
- The offline and create-voice messages say what to do again.
- pyannote has no owning engine, so diarization points at Other weights.
- The Hugging Face mirror moved to Settings → Network, and voice previews
moved to Settings → Storage.
- Unloading and switching engines happen in the engine list, not a
Weights list.
- A bad saved path points at Settings → Storage or the env file.
- Docstrings that read "the the" are fixed.
The dub stream-drop fallback goes through i18n in all 21 locales. A
Dictation pick on a row that is already downloading no longer starts a
second install: the row's radio is disabled while it works, and
useModelDownloads refuses a second mutation for a repo already in flight.
The Supertonic-3 license test checks for the Accept wording.
Conflicts: RecoBanner (deleted here: the recommendation card is gone),
ModelStoreTab + EngineCompatibilityMatrix (this branch's rewrite kept),
supertonic3 backend (main's own-venv check kept, message without the
retired "→ Engines" step), CHANGELOG (base layout + the #2020 line).
Carried over from main and the review:
- The list row hides Install while only the license review is left
(main's #2017 rule, now in the row; Accept lives in the panel).
- Engine action aria labels go through i18n (engines.aria*, all 21
locales) instead of hardcoded English.
- Every "Model Catalogue → Engines/Models" path main added, plus the
frontend strings that still named the retired panes, now point at the
one-page catalogue.
- test_engine_unavailable_reason_1866 reads the license matcher from its
new home, engines/engineDisplay.js.