Commit Graph
19 Commits
Author SHA1 Message Date
Palash Debnath 75eb7c6099 test: make repository ID bound deterministic (#1757)
Replaces a runner-speed-dependent security assertion with a deterministic proof that oversized repository IDs are rejected before library validation. Repairs the red post-merge main run from #1755.
2026-09-02 02:51:20 +05:30
Palash Debnath ef1cb57944 fix: route OmniVoice to ROCm GPUs (#1647) 2026-08-24 03:25:56 +05:30
Paolo Antinori 871d68a6ff fix(auth): offer API-key login on server-mode admin 403s (#1569)
Fix server-mode admin authentication recovery without trapping PIN-only deployments, and prevent stale 403 responses from clearing or superseding newly issued sessions. Includes backend/frontend regression coverage, docs, and changelog credit for @paoloantinori.
2026-08-20 00:03:37 +00:00
Palash DebnathandClaude Fable 5 9832fbd693 feat(engines): switch engines from anywhere — footer quick switch, workspace chips, shortcuts (#1530)
* feat(engines): add quick switching controls

* docs(changelog): note engine quick switching (#1530)

* fix(support): theme amount cards

* fix(support): restore themed amount cards

* fix(engines): address quick switch review findings

* fix(engines): green the full frontend suite around the quick switch

Three failure classes the targeted runs missed:
- the popover referenced --chrome-radius, which does not exist; it now
  wears the footer's shared MENU_SURFACE like the compute popover
- LogsFooter tests hand-wrote their api/system and api/hooks mocks, which
  drop every export the footer gains next; they are partial mocks now
- DubHeader/AudiobookHero tests rendered without a QueryClientProvider,
  which useEngines needs

Also: workspace-header chips open the popover downward (dropUp stays on
the footer instance) so it cannot clip off the top of the viewport.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(engines): one QueryClient per test module, not per render

CodeRabbit: the inline client made every wrapper render a fresh cache,
so rerender() restarted the /engines query mid-test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 15:21:05 +00:00
Giuseppe Rojas cd54113173 fix(security): close server-mode admin bypasses (#1525)
* fix(security): require keys for remote admin actions

* fix(frontend): guard unavailable scrollIntoView

* docs: link changelog to PR 1525

* fix(security): align PIN-only discovery policy

* fix(security): preserve strict sidecar boundary

* fix(security): normalize remote API keys

* fix(auth): normalize credential fallback order
2026-08-13 05:04:29 +00:00
Palash DebnathandClaude Opus 5 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>
2026-08-12 23:46:07 +00:00
Palash Debnath 7cde2fcd06 fix(desktop): make recording and dubbing reliable (#1481)
* fix(ui): keep scaled desktop shell responsive

* fix(linux): support desktop microphone capture

* fix(ui): update the centered VoiceStudio brand

* fix(audio): fall back when recorder start is unsupported

* fix(desktop): use the app header as titlebar

* feat(audio): add live microphone input controls

* fix(dub): recover from missing transcription models

* fix(dub): make pipeline stages actionable

* fix(asr): recover low-memory transcription

* docs: record desktop reliability fixes

* fix(ui): use semantic error banner border

* fix(dub): harden recovery and recording fallbacks
2026-08-10 22:43:37 +00:00
Palash Debnath 27a8f477b7 fix(security): keep private diagnostics out of API responses (#1454)
* fix(security): keep private diagnostics out of API responses

* docs: reference response-safety PR

* fix(security): preserve constant recovery guidance

* fix(security): keep recovery and logs data-independent

* fix(security): close remaining response sinks

* test(security): keep SOCKS diagnostics private

* fix: keep Tailscale exceptions local

* fix: keep Tailscale CLI output private
2026-08-10 12:01:12 +00:00
Palash Debnath 38a00cbf30 fix(security): stabilize engine discovery metadata (#1460)
* fix(security): stabilize engine discovery metadata

* fix(security): preserve stable routing outcomes

* fix: preserve safe engine routing outcomes
2026-08-10 10:57:54 +00:00
debpalash cf59a508fe fix(security): align bounded model validation 2026-08-10 00:04:53 +00:00
debpalash 6f443ad387 fix(models): accept valid underscore repo IDs 2026-08-09 21:13:10 +00:00
debpalash 400fd97810 fix(security): bound model and voice validators 2026-08-09 20:59:12 +00:00
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>
2026-07-09 04:43:16 +05:30
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>
2026-07-08 04:32:24 +05:30
5bd8968aea feat(engines): real synthesis "Self-test" + copy-paste setup snippet for opt-in engines (#930)
* 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>
2026-07-03 17:02:08 +05:30
Palash DebnathandClaude Opus 4.8 2a1c3eee3d feat(routing): synth-time no-silent-fallback gating at all TTS entry points (#21 follow-up) (#440)
Closes the last #21 gap: a per-request engine=/model= override bypasses the
/engines/select host-gate, so an engine that can't use this host's GPU could
still be triggered at synth time and silently fall back to CPU (or die mid-
synth). Now enforced at every TTS synth entry point, reusing the SAME probe +
resolver — never re-deriving routing.

Shared helpers (services/engine_routing.py):
- `routing_notice(result)` → (status, reason) to surface, or None. Fires for
  cpu_fallback (always) and accelerated-with-caveat (driver/arch); silent for
  cpu_only / clean-accelerated / n/a.
- `header_safe_reason(reason)` → scrubbed + ASCII-sanitized (headers are
  latin-1; a non-ASCII device name would 500 otherwise) + ≤256 chars. No regex.

Entry points:
- REST `POST /generate` (generation.py): after engine resolution, resolve
  routing once; `unavailable` → 400; cpu_fallback / accelerated-caveat → 200 +
  `X-OmniVoice-Routing` + `X-OmniVoice-Routing-Reason` headers on the WAV
  StreamingResponse; benign → no headers. Covers OmniVoice + adapter branches.
- OpenAI-compat `POST /v1/audio/speech` (openai_compat.py): same gate + same
  headers; the tts-1/tts-1-hd alias inherits the active engine's routing.
- WebSocket `/ws/tts` (tts_stream.py): no headers → frames. `unavailable` →
  `{"type":"error",...}` + skip stream; cpu_fallback / caveat → one
  `{"type":"routing","status","reason"}` frame before any audio.
- `select_engine` response now echoes routing_status / effective_device /
  routing_reason (PR #432 added the gate; this adds the fields so the UI can
  warn on a cpu_fallback pick). New fields on SelectEngineResponse.

Frontend: `useTTS` reads the X-OmniVoice-Routing header and shows a one-time,
non-blocking toast (in-memory de-dup by status — a 50-clip batch fires once,
no localStorage). i18n keys `tts.routingFallback`/`tts.routingCaveat`.

Tests: routing_notice + header_safe_reason (ASCII/length/scrub) unit tests;
REST synth gate (unavailable→400, cpu_fallback→headers, cpu_only→none) via the
fake-engine harness with a mocked host; select response routing fields.

Deferred (small follow-up): dub-pipeline ASR routing note on the preflight_error
SSE channel — separate path, not a TTS synth entry point. No frontend /ws/tts
client exists today (the routing frame serves external API consumers).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:18:37 +05:30
Palash DebnathandClaude Opus 4.8 8c8d525397 feat(routing): wire effective-device into /engines + select gate (#21 PR 3/5) (#432)
* feat(routing): wire effective-device + routing_status into /engines (#21 PR 3/5)

Surfaces the PR-1 probe + resolver through the engine registries so the
matrix UI (PR 5) and the no-silent-fallback gates can consume it.

- `engine_routing.routing_fields()`: shared helper returning the three
  serialization-ready keys, centralizing the scrub rule — routing_reason is
  scrubbed via `core.scrub.scrub_text` only when truthy, so a None reason
  stays JSON `null` (never coerced to "").
- TTS/ASR `list_backends()` each gain `effective_device` / `routing_status` /
  `routing_reason`, computed from a SINGLE `detect_host_caps()` call per
  request (host caps are constant per process). ASR is brought to full TTS
  parity: it now also carries `install_hint` / `last_error` / `isolation_mode`
  and a SCRUBBED `reason` (closing a pre-existing ASR token-leak gap) — an
  identical 11-key shape across families. ASR also gains the same
  is_available()-raises resilience TTS has (degrade to available:false, never
  500).
- LLM `list_backends()` reaches 11-key parity too but emits literal
  `effective_device:"network"` / `routing_status:"n/a"` / `routing_reason:null`
  (NOT via resolve_routing — LLM runs no local GPU model). `LLMBackend.gpu_compat
  = ()`. "network" is a label, not a probe — nothing here touches the network.
- `select_engine` host-routing gate: refuses a pick whose `routing_status` is
  `unavailable` on this host (400 with an actionable detail), while ALLOWING
  `cpu_fallback` (it runs, just slower). LLM is never gated. Defensive `.get`
  so legacy payloads still select. New typed `SelectEngineResponse`.

Tests: 11-key shape across all 3 families, well-formed tts/asr routing keys
(+ None-not-"" contract), LLM network/n/a labels, select gate (block
unavailable / allow cpu_fallback / never-gate LLM). Updated the registry
exact-shape test for the 3 new keys.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(cjk): allowlist docs/specs/ in the hardcoded-CJK guard

PR #429 merged the longform design specs, which legitimately quote functional
CJK (test-fixture descriptions, CosyVoice speaker IDs, multilingual sample
text). The CJK guard scans every tracked file, so those docs turned main red.
Specs are documentation, not shipped UI strings — allowlist the docs/specs/
prefix, matching the individually-allowlisted docs already in the set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 01:59:33 +05:30
Palash DebnathandClaude Opus 4.8 e8705a106d feat(dictation): opt-in NLMS AEC for dictate-over-playback (Wave 8b) (#399)
* feat(dictation): opt-in NLMS AEC for dictate-over-playback (Wave 8b)

Dictating while OmniVoice plays audio (TTS preview, dub, video) leaks the
loudspeaker signal into the mic, and the streaming ASR transcribes that
bleed. Browser echoCancellation varies per platform/webview — it can't be a
cross-platform default — so this adds a server-side canceller that behaves
identically everywhere.

services/aec.py ports Patter's NlmsEchoCanceller (MIT): a time-domain NLMS
adaptive filter with a Geigel double-talk detector, warm-up step ramp, and
far-end staleness pass-through. /ws/transcribe gains an opt-in '?aec=1[&sr=]'
mode: frames are raw int16 mono PCM tagged with a 1-byte prefix (0x00 mic,
0x01 playback reference); the mic is cleaned against the reference before
buffering, and the cleaned PCM is muxed via stdlib wave (not ffmpeg). Without
the param the protocol and behaviour are byte-for-byte unchanged.

Backend ships dark (no new deps — numpy already pinned); frontend far-end
streaming is a follow-up. Tests cover echo attenuation, double-talk
preservation, cold/stale pass-through, param validation, and the framing
helpers — all pure-numpy/stdlib so they skip the torch ASR stack.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(capture_ws): stubs accept the new pcm_sr kwarg

_transcribe_buffer/_transcribe_buffer_full gained an optional pcm_sr kwarg
for the AEC PCM path; the protocol-test stubs had fixed signatures and
raised TypeError on it, so the handler sent 'error' instead of 'final'.
Accept **kw in the stubs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 23:08:45 +05:30
Palash DebnathandClaude Opus 4.7 84fffa5409 Phase 2 Plan 02-04: Engine Compatibility Matrix API + UI (#99)
* Phase 2 Plan 02-04: GET /engines/{id}/health + gpu_compat + HF mask

ENGINE-06 backend half. Adds the data + spawn-on-demand endpoint the new
Engine Compatibility Matrix UI will consume:

* `gpu_compat: tuple[str, ...]` class attribute on `TTSBackend`, overridden
  per backend with reasonable defaults (cuda+mps+cpu for OmniVoice/VoxCPM2;
  cpu-only for KittenTTS; mps+cpu for MLX-Audio; etc.). `list_backends()`
  serializes it as a list.
* `_HF_TOKEN_MASK_RE` (`hf_[A-Za-z0-9]{30,}`) scrubs the `reason` and
  `last_error` fields before they leave the registry — Phase 1's
  HFTokenRedactor logging filter does not run on FastAPI response bodies,
  so this closes T-02-12.
* `GET /engines/{engine_id}/health` — loopback-gated route that resolves
  the backend across tts/asr/llm registries, then either calls
  `SubprocessBackend.health_check()` (spawn-and-ping) for subprocess
  engines or falls back to `is_available()` for in-process engines.
  Returns `{ id, ok, message, latency_ms }`. Engine instances are cached
  per-class so repeated checks don't leak atexit hooks or spawn extra
  sidecars. The masked-redactor is reapplied on the way out.

Test coverage (tests/backend/api/test_engines_route_shape.py, 11 tests):
  * Response shape includes the new fields for every TTS entry
  * IndexTTS2 isolation_mode == "subprocess", OmniVoice == "in-process"
  * Health route round-trips with mocked SubprocessBackend success
  * Health route falls back to is_available for in-process backends
  * Unknown engine id → 404
  * Non-loopback origin → 403
  * Engine instance cache reuses the singleton across calls
  * HF tokens leaked into is_available() / health_check() are masked
    in both the /engines and /engines/{id}/health response bodies

Existing tts_backend_registry shape test updated to include `gpu_compat`.
Full suite: 402 passed, 0 failures (up from 391+ baseline).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Phase 2 Plan 02-04: EngineCompatibilityMatrix UI + Settings wiring

ENGINE-06 frontend half. Mounts a new component on Settings → Engines
that surfaces, end-to-end, the data shape Plan 02-01 + Plan 02-03 added
to the backend registry:

* `frontend/src/components/EngineCompatibilityMatrix.jsx` (270 lines) —
  semantic <table> with role=row/cell so RTL queries work; one row per
  registered backend. Columns:
    - Engine name + install hint + Last error line
    - Install state badge (Available / Unavailable + inline reason)
    - GPU compat chips (CUDA / MPS / ROCm / CPU with colored variants)
    - Isolation mode badge (subprocess for IndexTTS, in-process for the
      rest — makes the Phase 2 architectural shift legible to users)
    - "Test engine" button → `/engines/{id}/health` round-trip; renders
      latency in ms inline next to the button; disabled while inflight;
      5 s cooldown to prevent click-storms.
  Mount does NOT auto-test any engine — per the plan's Open Question #2,
  spawning sidecars is gated on user action.
* `frontend/src/components/EngineCompatibilityMatrix.css` — minimal
  styling that reuses chrome tokens; chip colors per GPU target.
* `frontend/src/api/engines.ts` — `getEngineHealth(id)` client function
  wraps the new backend route through the shared apiJson helper.
* `frontend/src/api/types.ts` — extends EngineBackend with optional
  `isolation_mode`, `last_error`, `install_hint`, `gpu_compat` so the
  TypeScript surface tracks the backend wire shape, and adds
  EngineHealthResponse.
* `frontend/src/pages/Settings.jsx` — replaces the hand-rolled Engines
  table inside EnginesTab with `<EngineCompatibilityMatrix family="tts"
  onSelect={...} />`. selectEngine still wires up the picker; the
  matrix's onSelect prop renders the Use button per row when provided.
  Removes the now-unused FAMILY_META local map.

Test coverage (`frontend/src/test/EngineCompatibilityMatrix.test.jsx`,
8 tests via vitest):
  * Renders one row per backend with documented columns
  * isolation_mode badge: subprocess for IndexTTS2, in-process for
    OmniVoice / KittenTTS
  * GPU compat chips: omnivoice → cuda/mps/cpu; kittentts → cpu only
  * Unavailable rows render the failure reason inline
  * last_error line renders below status when populated; masked HF
    token sentinel survives verbatim
  * Test engine click fires getEngineHealth(id) and renders latency_ms
  * Test button disabled while inflight; second click is a no-op
  * Failure path (ok=false) renders a failure marker

Frontend suite: 65 passed (8 new). Lint: 0 new errors. typecheck:ci: clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Phase 2 Plan 02-04: SUMMARY

Recap of Engine Compatibility Matrix delivery — backend route +
gpu_compat metadata + HF-token redaction, frontend EngineCompatibility-
Matrix component, full test counts, deviations, gpu_compat confidence
matrix, frontend test-runner command notes for Phase 6 CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 07:51:58 +05:30