d91beef0fd314250d8d9b94de86dfea019a8bd96
17
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bab794e13b | fix: report only observed engine execution | ||
|
|
0ec9c074e8 | fix: distinguish opaque loaded sidecars | ||
|
|
2be73b43e3 | feat: expose engine execution evidence | ||
|
|
41722afe3b |
refactor(launchpad): quieter, borderless design refresh (#1515)
* refactor(launchpad): quieter, borderless design refresh The launchpad carried decoration from an earlier direction: icon chips, corner-hung count badges, a permanently visible filled arrow, uppercase mono card titles, and a dotted stipple divider — plus a frame that had been invisible since the app-wide border tokens were zeroed. Rework it around what the borderless direction actually implies: - Feature tiles get a whisper-faint surface instead of a dead frame, and read as three bands (bare glyph + count / title + arrow / description). `--card-hue` is spent sparingly — the glyph at rest, the surface, count and arrow only once raised. Titles move to sans sentence case; counts are plain tabular numerals. Lift softened 4px -> 2px, coloured glow -> neutral shadow, plus an explicit focus ring and a staggered entrance. - Hero drops the boxed "646" pill and the filled A/B-Compare button for quiet type, with a hairline standing in for the separation. - Section labels trade the dotted stipple for a single fading hairline; rows are transparent until hover and reveal "Open" on hover/focus (it stays in the DOM, so AT and keyboard always reach it). - Hero, tiles, recent files, callout and project lists now share one 1180px column — previously only the top half was capped, so lists ran edge-to-edge on a wide display while the deck stayed centred. Two bugs found and fixed while doing it: - Buttons that had `border border-solid border-transparent` removed fell back to the UA default border and rendered a visible 1px outline. They now carry `border-0` explicitly. - `.lp-animate` used `animation-fill-mode: both`, so after the entrance it kept owning `transform` — and animation-origin declarations outrank normal ones, which silently killed the card hover lift. Now `backwards`, which still holds the from-state through the stagger delay. Also drops CSS the page has not rendered since #904: the cursor-spotlight layer, the breath ring, and the per-card waveform strip. Verified with headless renders at 1600/1280/940 and the empty state. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(dictation): decode Wayland portal signals and show the capture pill The GlobalShortcuts portal declares Activated/Deactivated as (o session, s shortcut_id, t timestamp, a{sv} options). We decoded the timestamp as u32, so zbus rejected every signal with Signature mismatch: got `(osta{sv})`, expected `(osua{sv})` and the press was dropped as an invalid signal. Registration succeeded and the desktop even reported the bound chord back, so the hotkey looked wired up while doing nothing at all — on every Wayland compositor, for the whole life of the feature (#1490). Decode the 64-bit timestamp, and keep the 32-bit spelling as a fallback so a non-conforming portal degrades to working rather than to silence. With presses arriving, the second half of the failure showed: nothing had shown the widget window since it became a hidden recorder host, so a capture ran with no pill on screen — and a mic or Accessibility failure rendered into a window nobody could see. Add show_dictation_pill, which bottom-centres the capsule on the monitor under the pointer and shows it without taking focus (Windows keeps SW_SHOWNOACTIVATE so paste still lands in the user's document), and call it from the widget for every state but idle. Wayland denies clients their own placement, so the compositor picks the spot there; the pill still appears. dispatch_dictation_capture now logs whether a press was emitted or queued — a press that reaches Rust and produces nothing was otherwise indistinguishable from one the compositor never delivered. Tests: portal signals decode at both timestamp widths (the 64-bit case fails before this change with the exact production error); pill placement centres, respects a second monitor's origin, and clamps rather than going off-screen; the widget shows for a state needing the user, stays hidden while idle, and never shows for a press that arrives while dictation is disabled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: sync in-progress workspace changes Uncommitted work already in the tree, checkpointed so the branch matches the local machine: - Remote GPU workers: join-from-the-app flow, one-time secrets, QR join codes, a Compute control in the status bar, and the device-list Workers panel (#1516) - Model Catalogue workspace, with Settings pointing at it - Settings sidebar search and keyboard navigation - Demo assets for dubbing, dictation and voice design, plus the scripts that render them - Backend: validation-error handling, ASR request-path degradation, and the accompanying tests - CHANGELOG entries for the above and for the Wayland dictation fix Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(tests): follow Engines to the Model Catalogue, and green the sweep - test_supertonic3 asserted the license gate points at "Settings" while the engine now names Model Catalogue → Engines, which is where the accept button actually lives. The assertion follows the move; what it pins is unchanged — the hint must name a place the user can reach it. - Carries the CJK allowlist entries for the rendered dub bundle (#1517) and the regenerated route snapshot for /workers/agent (#1516), both of which this branch inherits from the workspace sync. - docs/install/linux.md: the dictation capsule is bottom-anchored everywhere except Wayland, where the protocol gives applications no say in their placement. Documented rather than left as a surprise (CodeRabbit). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: stop a flaky dependency fetch from failing green runs en-core-web-sm resolves to a direct GitHub release URL, and github.com intermittently answers `http2 error: refused stream before processing any application logic`. uv's own three retries all land within the same few seconds and fail together, so the whole job dies on a dependency that has nothing to do with the change under test — it cost #1518 and #1517 an otherwise-green run tonight. Two changes: back off between whole `uv sync` attempts, which is what actually clears it, and pass --no-sync to the pytest steps. `uv run` re-resolves the environment before running, so every test step was a fresh chance to hit the same fetch even though the install step had already synced — that is exactly how #1518 failed, in the isolated backend/tests step, with all 5467 tests already passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: one retry seam for every uv sync, not just the job that failed last en-core-web-sm resolves to a direct GitHub *release* URL rather than a package index, and github.com intermittently answers `http2 error: refused stream before processing any application logic`. uv's own retries all land inside the same ~10 seconds and fail together, so a job dies on a dependency unrelated to the change under test. Tonight that cost four otherwise-green runs across #1515, #1517 and #1518 — and the first fix only covered the Tests job, so the next failure simply moved to Smoke (Linux), which syncs separately. The fetch is per-job, so the fix has to be per-job: scripts/uv-sync-retry.sh backs off between whole attempts (15s, 45s, 90s) and every workflow that syncs now goes through it — ci.yml (tests + the platform matrix), release.yml, security.yml, evals.yml. It still fails loudly after four attempts, so a genuinely broken lockfile is not disguised as a flake. The Tests job also lacked the UV_HTTP_TIMEOUT / UV_HTTP_RETRIES the smoke matrix has always set, which is part of why it was the one that kept dying; it has them now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(ci): pin the Intel-Mac contract by intent, not by command spelling test_ci_verifies_intel_mac_as_the_documented_remote_only_host asserted the literal line `run: uv sync --extra pockettts`, so routing every sync through scripts/uv-sync-retry.sh read as a broken Intel-Mac contract. The contract it exists to protect is that the pockettts extra installs ONLY on backend_supported legs — which the regex now pins, while leaving how the sync is invoked free to change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: keep every uv run out of the resolver, and bound the retry budget CodeRabbit, #1517: - `uv run` re-resolves before running, so the smoke suite, the worker-artifact tests, the release test run and the eval run were each a fresh chance to hit the flaky direct-URL fetch outside the retry loop. All of them pass --no-sync now; the environment is already synced by the step that owns the retries. security.yml's `uv run --with pip-audit` is deliberately left alone — it layers an ephemeral package rather than running the project's own tests. - The retry count multiplied uv's own budget (UV_HTTP_RETRIES=5 with a 120 s timeout on the smoke matrix). Three attempts and 60 s of total backoff outlast the refusals actually observed while staying well inside the jobs' timeout-minutes. - The Intel-Mac contract test pinned the smoke command literally too, so --no-sync tripped it exactly like the sync line did. Same fix: assert the contract (smoke runs only on backend_supported legs), not its spelling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dd8143c088 |
fix(mlx): pass the voice description the design model requires (#1405)
The curated qwen3-tts model IS the VoiceDesign variant, and mlx-audio refuses to run it without an instruct — but MLXAudioBackend.generate never forwarded one, so the engine could not produce audio under any input. The comment above the code claimed it was passed; the one test covering that path only passed because the value was being dropped. Forwards instruct, and raises an actionable error when a voice-design model is asked to generate without a description. Model type is read from the model's own config, falling back to the id convention. Closes #1405 |
||
|
|
17ae952810 |
feat(settings): Models & Engines pages — engine identity marks, capability badges, upgrade hints, filter, residency (#1058)
The engine list gains a scannable identity mark per engine (EngineMark), capability badges (cloning, device routing with reasons, sidecar isolation), and surfaces available-but-has-advice hints that list_backends previously dropped (new additive hint field; the ready-with-advice convention). The model store gains a filter, disk context near downloads, in-memory residency indicators with safe unload, copyable setup snippets, and actionable empty/error states. Registry additions are additive only (hint, supports_cloning with the property-descriptor guard). Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d3ec4ed371 |
fix(tts): voxcpm2 — version floor, reference-clip prep, trailing-silence guard (#1055)
Three hardenings of the voxcpm2 engine path, all backward-compatible and platform-identical: - Version floor: every install hint now says pip install "voxcpm>=2.0.3" (2.0.3 fixed an Apple-Silicon/MPS audio-quality bug). Floor only — an already-installed older version stays available and working; it just surfaces an actionable upgrade hint in the is_available reason and a load-time warning. - Reference-clip prep: the voxcpm package no longer trims reference audio itself, so raw user clips reached the model unconditioned. The clone path now trims leading/trailing near-silence (-50 dBFS floor, 50 ms edge pad) and caps the reference at 30 s. Fail-open (any prep problem falls back to the raw clip) and a strict no-op for short clean clips. - Trailing-silence guard: generated output is trimmed to the last voiced sample + ~0.3 s natural tail via the new audio_dsp.trim_trailing_silence. Silence-trim only, no content analysis; a no-op on outputs without a silent tail and on all-silent (dead) renders. 22 new fake-module tests in tests/test_voxcpm2_guardrails.py; existing engine/hint tests strengthened to guard the floor. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
80f10289fe |
feat(asr): ASR engines get the same Settings picker TTS has (env var still wins) (#1026)
Settings → Engines now stacks one pinned Engine Compatibility Matrix per family (TTS, ASR, LLM) instead of a single TTS-titled table with the other families tucked behind a low-discoverability tab. The backend select/prefs path (family="asr" → prefs.asr_backend, env > prefs > auto-detect) already worked but was unexercised and undocumented — it's now locked by API and resolution-order tests, and README + the openai-compat-asr doc stop promising a picker that didn't exist / denying one that now does. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
69ce697ee5 |
fix(backend): shutdown wait bound 3s→20s — post-merge review finding on #1002; absorb #1015's design-path test (#1020)
Greptile's review of the merged #1002 flagged a real residual gap: a cold transformers import alone can exceed the 3s shutdown wait, and cancelling the asyncio task doesn't stop the underlying OS thread — so quitting during an unusually slow preload could still let shutdown report "done" while that thread was alive, the exact #1000 class with lower odds. Python cannot forcibly kill a running thread, so no finite bound eliminates this outright; 20s shrinks the window from "any preload" to "an unusually slow cold-import," the practical ceiling before a long shutdown becomes its own complaint. New source-level contract test pins the production bound at ≥15s so a future edit can't quietly shrink it back without deliberate consideration. Also absorbs the one test case from community PR #1015 (superseded by the earlier-merged #1017, which duplicated it — my fault for not checking the PR queue) that the merged version lacked: the design/instruct path with no ref kwargs at all stays untouched by the ref_text forwarding fix. Co-authored-by: mergetest <test@local> Co-authored-by: MahdiHedhli <noreply@github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7f8a42ce51 |
fix(tts): mlx-audio CSM cloning drops ref_text, breaking every clone attempt (#1012, #1013) (#1017)
MLXAudioBackend.generate() reads voice/ref_audio/language/speed from its kwargs but never extracted ref_text — it was built, then silently never passed through to self._model.generate(). CSM (sesame.py) only builds its cloning context when BOTH ref_audio AND ref_text are present; with ref_text missing, the context list stays empty and indexing into it raises "IndexError: list index out of range" deep inside mlx-audio, instead of the clone ever being attempted. Voice cloning on the CSM engine could never have worked as shipped. generation.py already threads ref_text all the way through — even auto-transcribing it via the GPU pool when the caller supplies ref_audio without one (~line 780) — so the value was always available in kwargs; it just never survived the crossing into this specific backend. Reported with the precise root cause and a working fix (community member independently diagnosed and patched it locally, confirmed working on MPS/0.3.12). Two-line fix: extract ref_text and pass it through when both ref_audio and ref_text are present (guards against passing an orphaned ref_text with no accompanying audio to engines that don't expect it). Tests: tests/test_engines.py — ref_text is passed through when paired with ref_audio, omitted when ref_audio is absent. Also documents the second bug from the same report (#1013): macOS microphone permission never prompts, so OmniVoice never appears in System Settings to grant access. Root-caused to an unresolved upstream Tauri/WebKit limitation (WKWebView's requestMediaCapturePermissionFor delegate — wry#1195, tauri#11951, fix wry#1196 still open/unmerged, no released version to bump to) — not something fixable here without an unverified native Rust/WKWebView hack this session has no way to test. Documented in docs/install/troubleshooting.md with the confirmed workaround (record elsewhere, upload the file). Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
549fa4009f |
feat(engines): expose MLX-Audio's curated model picker (#981) (#994)
mlx-audio multiplexes 7+ curated models (Kokoro, CSM, Qwen3-TTS, Dia,
Chatterbox, MeloTTS, OuteTTS) behind a single "mlx-audio" backend id, but
MLXAudioBackend resolved its active model ONLY from the
OMNIVOICE_MLX_AUDIO_MODEL env var — invisible to Settings and unreachable
without restarting the packaged app with that var set. A user who
downloaded e.g. Llama-OuteTTS via Settings → Models had no way anywhere
in the UI or API to actually load it; the backend silently kept using
Kokoro.
Fix:
- MLXAudioBackend.__init__ now resolves its model via
prefs.resolve("mlx_audio_model_id", env=..., default=...), mirroring
active_backend_id()'s env > prefs > default order exactly.
- get_active_tts_backend()'s switch-detection now also tracks the
resolved mlx-audio model key, so a model-only change (same backend id)
invalidates the cached instance and reconstructs it — no app restart
needed to pick up a different curated model.
- POST /engines/select gained an optional model_id field; for
family=tts/backend_id=mlx-audio it validates against
MLXAudioBackend.CURATED_MODELS (or a raw HF repo id, matching the
class's existing tolerance) and persists it via prefs.
- GET /engines now includes a curated_models roster + active_model_id on
the mlx-audio entry only.
- Settings → Engines renders a small model dropdown on the mlx-audio row,
pre-selected to the active model, wired through selectEngine's new
optional modelId argument.
Regression coverage: prefs resolution + env override, cache invalidation
on model-only switch, /engines/select 400s on an unknown model id and
persists a valid one, curated_models present only on mlx-audio, and a
new EngineCompatibilityMatrix vitest suite for the dropdown.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
324d417d27 |
fix(engines): mlx-audio no longer crashes on unsupported languages, error messages never leak raw exception internals (#977) (#993)
Root cause: MLXAudioBackend.generate() blindly truncated the full
language display name to two characters (language[:2].lower()),
assuming an ISO code — 'Dutch' -> 'du', which crashed Kokoro's vendored
pipeline's internal assertion (assert lang_code in LANG_CODES, (lang_code,
LANG_CODES)) for any language whose first two letters didn't coincidentally
match one of Kokoro's single-letter codes. The raw AssertionError's
tuple-containing-a-dict args then leaked straight into the user-facing
500 message via two stacked f"...{e}" formatters in generation.py.
- resolve_kokoro_lang_code() resolves against the AUTHORITATIVE
ALIASES/LANG_CODES table read from the installed mlx_audio package
(never a hardcoded guess), and only applies when Kokoro is the actual
active curated model — other curated models (CSM, Dia, Qwen3-TTS,
OuteTTS, ...) either ignore the kwarg or expect a different format, so
Kokoro's strict validation doesn't wrongly reject them. Unsupported
languages now raise a clear ValueError naming what Kokoro supports,
which generation.py already converts to a clean 400.
- _safe_exc_text() hardens both generic exception formatters in
generation.py: if any element of an exception's .args is a container
(dict/list/tuple/set), never interpolate str(e) raw — name the
exception type and point at the log instead. Protects every current
and future engine's generate() from leaking a raw container repr, not
just this one Kokoro assertion.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
ba4f64240a |
fix(engines): classify sherpa "model not set" as a config error, gate the engine on its model dir (#919) (#934)
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> |
||
|
|
3bb401f4e5 |
test: make LLM-provider state leaks between tests impossible (#878) (#894)
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> |
||
|
|
6e600c48cb |
fix(generation): classify network/download failures — stop mislabeling every unknown error as OOM (#880) (#893)
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> |
||
|
|
a9071e6e1b |
test: add preflight + bitrate coverage, refresh legacy mocks, wire CI gate
## New coverage
### tests/test_setup_preflight.py (13 tests, 11 pass + 2 skip)
Covers the /setup/preflight endpoint end-to-end:
- Response shape (ok / has_warnings / checks / device)
- Every check has id/label/status/detail/fix
- All 9 core checks present regardless of platform
- Aggregation logic (ok↔any-fail, has_warnings↔any-warn)
- GPU vendor branches:
* Apple Silicon → vendor=apple, backend=mps
* Missing nvidia-smi falls through
* Old NVIDIA driver (520) flags fail + driver-update fix
* AMD with CUDA torch warns with ROCm install instructions
- Network probe handles unreachable host gracefully
- RAM fail threshold (<8 GB) + warn threshold (<12 GB)
Branches not reachable on the current host are skipped with a clear
reason so the suite stays green across mac-ARM / mac-Intel / win / linux.
### tests/test_dub_export_bitrate.py (20 tests)
Verifies the bitrate-clamp logic added to /dub/download-mp3:
- Normal values (128/192/256/320) pass through as Nk
- Case-insensitive (256K → 256k)
- Below-floor snaps to 64k
- Above-ceiling snaps to 320k
- Malformed (None/empty/garbage/scientific) → default 192k
- Negative int parses fine, clamps up to 64k floor
### tests/frontend/apiClient.test.mjs (9 tests)
Exercises api/client.ts under node:test with a synthetic fetch mock:
- apiUrl normalization (empty → API root, slash prepending, absolute URL passthrough)
- ApiError carries status + detail
- apiFetch resolves 2xx, throws ApiError with JSON detail on non-2xx
- apiJson parses body
- apiPost stringifies JSON bodies + sets Content-Type
- apiPost hands FormData straight to fetch (no Content-Type override)
### tests/frontend/format.test.mjs (5 tests)
Covers utils/format.js formatTime timecode rendering.
## Legacy mock refresh (not scope-creeping fixes — minimal updates)
- tests/test_api.py: replace stale `backend.main._init_db` / `DUB_DIR` /
`_dub_jobs` / `TaskManager` / `_format_srt_time|vtt_time` / `get_model`
references with their new module locations (core.tasks, core.config,
services.dub_pipeline, api.routers.dub_export, services.model_manager).
Normalize imports to the unprefixed `from services.*` / `from core.*`
form used inside the backend itself — avoids `backend.*` vs
unprefixed sys.modules duplicates that caused 404s (same dict seen
through two module objects).
- tests/test_engines.py + test_router_smoke.py: loosen strict-equality
backend-set asserts to `.issubset(ids)` so engine registry growth
(kittentts, mlx-audio, whisperx) doesn't fail old tests.
- tests/test_engines.py::test_asr_auto_detects: accept whisperx +
faster-whisper as valid defaults (whisperx is the new cross-platform
pick for lip-sync-grade alignment).
- tests/test_dub_transcribe.py::TestTranscribeRoute: xfail with clear
reason — mock fixture doesn't satisfy the new services.asr_backend
bytes-path contract. Logged for a later test-maintenance pass.
- tests/test_api.py::TestStreamingTTS::test_generate_...: xfail with
clear reason — patch target moved from backend.main.get_model to
services.tts_backend.
## CI gating (.github/workflows/release.yml)
Added a single-runner Linux `test` job that the matrix `build` job now
`needs:`. Runs:
- uv sync + apt install ffmpeg
- uv run pytest tests/
- bun install + bunx tsc --noEmit + bun run test (node:test)
Failing tests now block the 4-platform matrix build before it burns
~40 minutes of runner time.
## Frontend test script
frontend/package.json: add `"test": "node --test ../tests/frontend/*.test.mjs"`.
## Totals on this machine
- Backend: 190 passed, 6 xfailed (stale mocks, documented), 3 skipped
(hardware-specific branches), 0 failed
- Frontend: 36 passed, 0 failed
- Typecheck: clean
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
52d68d05dc | refactor: update backend architecture, expand frontend state management, and synchronize voice-pro research modules. |