Commit Graph
100 Commits
Author SHA1 Message Date
Palash Debnath 2477dde688 docs: streamline README structure and specifications (#1560)
* docs: streamline README structure and specifications

* docs: address README review findings
2026-08-15 13:06:44 +00:00
Palash DebnathandClaude Fable 5 48c9a3b1f8 feat(settings): compute-device override (auto / CUDA / ROCm / XPU / MPS / CPU) (#1557)
* feat(settings): compute-device override — auto | CUDA | ROCm | XPU | MPS | CPU

Auto-detect stays the default; the override kills the 'auto-detect picked
wrong' issue class. Applied at the single choke point (_probe()'s family
selection) so routing, get_best_device(), and every badge inherit it.
Resolution: OMNIVOICE_DEVICE env > Settings pick (prefs.json) > auto (#981
pattern). An override can steer, never invent hardware: a family the host
lacks is noted and ignored; cpu is always honorable. Applies at next
backend start (host caps are immutable per process — same restart contract
as the rest of the Performance tab, RestartBadge shown).

GET/PUT /api/settings/compute-device (admin-gated) reports resolved vs
applied so the panel shows restart-required truthfully and disables itself
under an env pin instead of pretending.

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

* docs(changelog): entry for the compute-device override (#1557)

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

* fix(device-override): harvest — the override reaches CT2 ASR, full i18n, honest edge states

- _ctranslate2_cuda_ok() and the ASR sidecar now gate on the probe's family,
  so a cpu pin (or ROCm host) can never hand CTranslate2 a CUDA device —
  the override reaches every CT2 loader through one shared gate
- override_ignored exposed by the API and shown by the panel (env pin naming
  a device this machine lacks: auto is in effect, restart won't change it)
- all 8 panel strings + 5 device-family labels translated into all 21
  locales; failed saves keep their error visible through the re-sync
- test isolation: cleanup drops OMNIVOICE_DEVICE before re-probing so no
  overridden caps leak into later tests; panel tests wait for loaded state
- xpu/intel search keywords; oxfmt formatting

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

* fix(device-override): round 2 — fail-safe probe fallbacks, complete i18n, combined pin state

- a broken capability probe now means CPU everywhere (CT2 gate + ASR
  sidecar) — never a torch-derived guess that would bypass a cpu pin or
  re-open #1529 on ROCm; regression test added
- env-pinned AND not-detected shows both facts in one subtitle
- device_load_failed/perf_save_failed translated into all 21 locales;
  CJK/th/vi/ar strings no longer say literal 'Auto'
- test_ctranslate2_never_gets_cuda_on_a_rocm_build pins the probe family
  (it was order-dependent on the lru_cache before)

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

* fix(test): pin the probe family in the faster-whisper OOM-fallback test

Same class as the rocm-build test: it mocked torch but not the probe the
new override gate consults first, so on a cpu-family CI host the CUDA
fallback chain under test was unreachable.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 05:08:51 +00:00
Palash DebnathandClaude Fable 5 030d5ea01f docs(engines): a guide for every engine + index; fix two engine-metadata bugs (#1556)
* docs(engines): a guide for every engine + index; fix two engine-metadata bugs

21 new pages under docs/engines/ (10 TTS, 10 ASR, index README) — every
registered engine now has one: what it's for, platform support, model env
vars, quirks with issue refs. Linked from both READMEs' engine sections.

Code fixes found while verifying facts against the registries:
- KittenTTS docstring claimed default voice 'Jasper'; the code default is
  expr-voice-2-f
- the isolated-ASR sidecar read only ASR_MODEL_FW while the download
  preflight read ASR_MODEL_FASTER — set one and the other quietly used a
  different model; both now resolve ASR_MODEL_FW-override → ASR_MODEL_FASTER
- moonshine's install hint named 'useful-moonshine', a package the backend
  never imports; now moonshine-onnx / moonshine-voice

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

* docs(changelog): entries for the engine guides + sidecar model fix (#1556)

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

* docs(engines): second-harvest fixes — TLS guidance, matrix/code alignment, CN counts

- README matrix aligned to gpu_compat (the code is the source of truth):
  CosyVoice macOS is CPU not MPS, IndexTTS and GGUF gain their real
  CUDA/CPU/MPS cells
- gpt-sovits guide: prefer https/tunnel for non-loopback servers, plaintext
  warning; first-use download guidance on both OmniVoice pages
- preflight empty-env fallback matches the sidecar (ASR_MODEL_FASTER='' no
  longer resolves a different repo)
- nano installs via uv pip; kitten log level wording; index links install
  guides incl. the Gatekeeper step; README_CN engine counts 16/11

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

* docs(readme-cn): the all-engines-local claim now excludes the remote client

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 03:55:59 +00:00
Palash DebnathandClaude Fable 5 b79ba9bd3b docs(readme): lead with download + first clone; seed benchmarks page (#1555)
* docs(readme): lead with download + first clone; seed benchmarks page

Quickstart (installers, install guides, a three-step first-clone walkthrough)
moves above What's-new/Features in both READMEs — visitors get the action
before the pitch. New docs/benchmarks.md anchors measured per-engine/device
numbers on the bench_pipeline.py harness, community-contributed, no estimates.

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

* docs(changelog): entry for the README conversion restructure (#1555)

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

* fix(bench): emit RTF + CUDA peak VRAM; guard NaN RAM; define the benchmarks schema

Bot harvest on #1555: the tts stage now prints RTF per warm measurement and
CUDA peak VRAM (None elsewhere — no made-up zeros), the stage floor refuses
unmeasurable RAM instead of sailing past a NaN comparison (FLOOR_GB=0
overrides), docs/benchmarks.md columns map 1:1 to what the harness prints,
and the download badges say they open the release page.

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

* docs(readme): link palash.dev from the maker section

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

* fix(bench): name the resolved engine, track VRAM from resolution, comment the guards

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

* docs(readme): the quick-switch gif is the hero image

The hero shows motion now; the Launchpad screenshot moves into the 0.5.0
What's-new slot so nothing appears twice.

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

* fix(bench): peak VRAM is reserved memory; adapter engines name their model

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

* fix(bench): subprocess-isolated engines report VRAM n/a, not a parent-side zero

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

* fix(bench): out-of-process detection is declarative; sherpa rows name their model

'runs_out_of_process' is now a TTSBackend attribute set by SubprocessBackend
AND omnivoice-gguf (which inherits TTSBackend directly but spawns a binary
per generate — the isinstance check missed it). Duck-typed for the same
module-purge reason as _is_subprocess_isolated. Sherpa-onnx identity comes
from _model_dir's basename when _model_id is absent.

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

* fix(bench): backends self-report model identity via TTSBackend.model_identity()

Greptile enumerated the adapter engines one at a time (mlx _model_id,
sherpa _model_dir, cosyvoice env-only) — the attribute sniffing rots per
engine. The hook fixes the class: each multi-model backend reports its
own identity, the profiler just asks.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 02:41:40 +00:00
Palash DebnathandClaude Fable 5 3d0c9605df test(shell): backend-lifecycle fault-injection harness (#1551)
* test(shell): backend-lifecycle fault-injection harness

Runs spawn_backend_and_wait/supervise_backend against REAL dying child
processes and asserts the user receives the correct NAMED diagnosis —
not merely that recovery happens. Wrong/missing explanation was 61% of
the historical "can't reach the backend" class; this rig is the
permanent regression harness for every future lifecycle fix.

Seam: OMNIVOICE_BACKEND_CMD (JSON argv or whitespace form) runs any
command as "the backend" — venv bootstrap and ffmpeg resolution are
skipped, everything else (err-log run offsets, drainer threads, env
pinning, real OS pipes, spawn-failure diagnostics) stays real. Plus
OMNIVOICE_LOG_DIR (per-test log+marker dirs, also a support tool) and
harness-only timing overrides OMNIVOICE_STARTUP_BUDGET_S /
OMNIVOICE_SUPERVISOR_POLL_MS whose production defaults are pinned by
unit tests. Lifecycle fns genericized over tauri::Runtime for the
MockRuntime app; behavior-neutral with the env unset (unit-pinned).

Scenarios (tests/backend_lifecycle.rs, scenario children = this test
binary re-invoking itself; serial by mutex + CI --test-threads=1):
- port conflict (exit 78) → the detectHints-matchable port phrasing
- generic chained traceback → root cause survives into the diagnosis
  and the crash marker
- spawn failure → spawn diagnostic reaches the user, NO bogus marker
- slow start past budget → timeout names the budget + last stderr
- post-Ready crash loop → 3 restarts announced, markers before restarts,
  "kept crashing" diagnosis naming the last exit
- SIGKILL (unix) → named as signal 9
- deliberate kill → supervisor yields silently, no marker, never Failed
- deferred-startup FATAL → the named step reaches the user, forensics,
  and the splash narration

CI: harness added to the 3-OS tauri-cross-platform matrix.

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

* chore(ci): temporary Windows loader bisect probe for the harness binary

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

* fix(shell): embed Common-Controls v6 manifest into Windows test binaries

Bisected on #1551: EVERY integration-test binary of this crate died at
load on Windows with STATUS_ENTRYPOINT_NOT_FOUND (0xc0000139) — cargo
gives test binaries no manifest, so the loader resolves comctl32 v5,
which lacks the TaskDialogIndirect entry point tauri's dialog/tray stack
imports. build.rs now embeds tests/windows-test.manifest via
rustc-link-arg-tests on Windows targets. Bisect probe removed.

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

* fix(test): scenario gate is PID-valued — the parent can't self-inject

CodeRabbit on #1551: in a parallel local `cargo test`, the parent's own
scenario_child test could observe the armed env and start playing the
backend in-process (binding the port, idling 600s). The gate value is
now the arming process's PID; a matching PID stays inert, so only the
spawned child — a different process — runs the scenario.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 18:45:27 +00:00
Palash DebnathandClaude Fable 5 bb813ff676 feat(startup): bind the socket in ~1s and narrate startup step by step (#1550)
* feat(startup): bind the socket in ~1s and narrate startup step by step

The structural fix for the "can't reach the local backend" class (~1 in 5
of every issue ever filed): uvicorn served nothing until torch import
(10-20s cold), the 30-router fan-out, an import-time DB migration, the
cuDNN preload, and alembic all finished — every slow or fragile step
rendered as an unexplained dead backend.

main.py now keeps module scope fast and defers the heavy work:
- _phase_a_build (executor thread): prefs/env restore + #963 migration,
  yt-dlp overlay, cuDNN preload, torchaudio, model_manager, router
  imports — order preserved, literal imports so PyInstaller still traces.
- _phase_a_finalize (event loop, no awaits → atomic wrt requests):
  include_router, mounts, MCP, SPA, openapi bust.
- _phase_b: the old lifespan startup body; handles on app.state so
  shutdown survives a startup that never finished.
- Eager mode (pytest / OMNIVOICE_EAGER_INIT=1) runs everything at import
  — byte-equivalent behavior for the ~100 lifespan-less TestClient sites
  and for embedders (dump_api_routes, probe boot runner opt in).

While starting: /health answers 503 with the current step, new
/startup/progress serves the full ledger (always 200), and
StartupGateMiddleware 503s everything else with the [starting] marker
(same skip-the-Report-button convention as [shutting_down]). A deferred
failure keeps import-crash semantics: traceback to stderr → shell crash
forensics, run sentinel stays uncleared, exit 1 names the failed step.

Shell: startup_progress() probe (marker-header-gated so a foreign
responder can't narrate the splash) feeds per-step log lines into the
launch poll and the supervisor's reconnect wait. --health-check absorbs
the deferred init (60→180s); --diagnose runs Phase A up front so it
still sees restored prefs. Docker HEALTHCHECK semantics unchanged
(curl -f fails on 503 exactly as it did on connection-refused).

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

* fix(startup): join the Phase A thread on shutdown; async fail-path sleep

Bot-review harvest on #1550: cancelling the deferred-startup task cannot
stop the executor thread inside Phase A's blocking imports — shutdown now
waits (bounded, only when a build started and hasn't finished) on a
thread-completion event so interpreter teardown can't race a mid-import
(#1000 class). The failure path's last-poll beat is now awaited, not
time.sleep — a blocking sleep froze the very loop that beat exists to let
serve. Also: dump_api_routes forces eager (assignment, not setdefault),
and the integration test's child gets DEVNULL instead of an undrained
pipe that could wedge a cold boot.

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

* fix(startup): close the Phase A submission race; CodeQL nits

Review finds on #1550: shutdown could sample _phase_a_started unset
while the executor callable was queued-but-not-running, skipping the
thread join. started is now set BEFORE submission, the submission is
shielded so a cancel can't strand a queued callable that would never set
_phase_a_finished, and the wrapper sets finished on every exit including
the already-built early return. Contract pinned by
test_phase_a_thread_join_contract. Plus explanatory comments on the new
bare excepts and a consistent return in the gate's websocket branch.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 15:23:02 +00:00
Palash DebnathandClaude Fable 5 bc6acec5a3 fix(shell): gate Ready on the deep health probe; pace crash-loop restarts (#1548)
* fix(shell): gate Ready on the deep health probe; pace crash-loop restarts

Two supervisor hardenings from the backend-reliability root-cause pass:

Ready now requires backend_ready() — the identity probe (/system/info
string-sniff) AND the deep probe (/profiles must 200) — at both Ready
transitions (startup poll, supervisor respawn wait). The shallow probe
alone announced a backend whose install/DB had broken underneath as up;
the UI looked alive while every real request 500'd or dead-ended on
"can't reach the backend". Death detection stays process-exit-only, so a
busy-but-alive backend is still never killed.

Supervisor respawns now back off: first respawn immediate (a one-off
crash self-heals fast), then 5s, then 15s, capped — the budget check
ends a hopeless loop, not an unbounded sleep. The pause runs behind the
already-visible "reconnecting" banner and yields within 500ms to app
quit or a deliberate retry-flow replace.

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

* fix(shell): yield backoff to a tracked replacement child, not just the flag

Greptile P1 on #1548: a completed Retry/Clean&Retry sets the deliberate-
kill flag and track_backend_child clears it — possibly both between two
500ms backoff samples, so the flag alone can be missed and the old
supervisor would free_port() the retry's healthy replacement. The dead
child we observed can never read as alive again, so a live tracked child
during backoff can only be a replacement — yield to it promptly so the
retry's spawn_backend_and_wait can claim the supervisor slot at Ready.

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

* fix(shell): backoff yields on spawn-generation change, not liveness

Second Greptile pass on #1548: a replacement child that itself exits
before the old supervisor's next 500ms sample read as "still dead" under
the liveness check, so ownership transfer was missed. The spawn
generation (bumped by every track_backend_child, never un-bumped) is
observable regardless of the replacement's fate — snapshot it at death
detection, yield the moment it changes.

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

* fix(shell): snapshot spawn generation before observing the exit

Third-pass review find: sampled after try_wait, a replacement tracked in
the gap bakes its own generation into the snapshot and the ownership
transfer is missed. Snapshot first, and re-check once more before
touching the port so the zero-backoff first respawn is covered too.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 14:41:21 +00:00
Palash DebnathandClaude Fable 5 94ba362ef2 feat(triage): crash-class recurrence report — the reliability metric (#1549)
* feat(triage): crash-class recurrence report — the reliability metric

scripts/crash_class_report.py measures the "backend died / never came
up" class (the project's #1 lifetime failure, ~1 in 5 of all issues)
filtered to reports from the current version — the definition of done
for the reliability cycle. Buckets by the bug reporter's Build-status
stamp (#1547): current / outdated / unknown (pre-deflection builds), so
deflection-miss noise never pollutes the number the work is judged on.

tests/scripts/test_crash_class_report.py pins the title→sub-class
mapping against the real historical title shapes and locks the stamp
literals to frontend/src/utils/bugReport.js so a reworded marker fails
in CI instead of silently zeroing the metric.

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

* fix(triage): --version is authoritative; loud fetch-cap warning

Bot-review harvest on #1549: with --version, the Environment Version
line now decides the bucket (extracted to pure classify_build + tests) —
a report stamped "current at filing time" during another version's
window no longer counts toward this version's recurrence. Hitting the
500-issue fetch cap now warns loudly instead of silently understating.
The stamp lockstep test asserts the full Build-status prefix.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 14:20:35 +00:00
Palash DebnathandClaude Fable 5 aabe5783f3 fix(report): offer the latest release before filing from an outdated build (#1547)
* fix(report): offer the latest release before filing from an outdated build

6 in 10 sampled "can't reach the backend" reports came from builds that
were already obsolete when filed, and were closed with "please update" —
pure triage noise. Every Report-bug affordance now funnels through
openBugReport(): on an outdated build it offers the latest release first
(with a "File anyway" escape hatch), and the report body carries a
triage-greppable "**Build status:**" line either way, so current-version
recurrence — the reliability metric — is countable separately from
stale-build reports.

Freshness sources per deployment (behavior identical, implementation per
mode): desktop reads the Rust updater's channel-aware verdict from the
store (no new network path, no CSP widening); browser/dev/Docker make one
bounded latest-release GET, only once the user has initiated the report
flow whose destination is github.com. An 'unknown' dev build stays
silent entirely — never nudged, never claimed current.

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

* fix(report): anchor version parsing; zh-TW reportBug.title in Traditional

Bot-review harvest on #1547: parseVersionTriple now rejects trailing
non-semver data (1.2.3.4, 1.2.3garbage) instead of silently reading the
leading triple into an outdated/current verdict; the pre-existing zh-TW
reportBug.title was Simplified-script — now properly Traditional.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 13:44:24 +00:00
Palash DebnathandGius 854b4852ed perf(frontend): coalesce persistence writes off input paths (#1546)
Defer and coalesce omnivoice.app and omni_ui persistence behind a 250 ms
quiet window with a 1,000 ms hard maximum, preserving storage schemas,
synchronous pending reads, legacy formats, Factory Reset semantics,
widget read-only ownership, and lifecycle (pagehide/visibilitychange)
durability. Adds scheduler, restore, reset, StrictMode, concurrent-render,
role-ownership, and migration regression tests plus an opt-in
production-bundle responsiveness harness.

Lands #1541 by @bultodepapas (maintainer landing branch; the out-of-scope
attribution-policy commit was dropped).

Co-authored-by: Gius <bultodepapas@gmail.com>
2026-08-14 13:25:36 +00:00
Palash DebnathandClaude Fable 5 420bc73e78 fix(release): harden the AppImage repair — pinned tooling, final-writer manifest (#1545)
* fix(release): harden the AppImage repair step

All three review findings on #1544, fixed before the tag re-runs it:
- appimagetool pinned to the immutable 1.9.1 release with a verified
  SHA-256 — a mutable 'continuous' binary must not execute with the
  updater signing key and a release-write token in its environment
- the release tag reaches the script as env data, never interpolated
  into shell source (zizmor template-injection)
- a failed latest.json download now fails the step unless the asset is
  confirmed absent, and the patch refuses to upload unless at least one
  linux signature was actually replaced — a repacked AppImage can never
  ship paired with stale updater metadata

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

* fix(release): the updater manifest gets one final writer, after the matrix

CodeRabbit + Greptile on #1545: every tauri-action leg re-uploads the
shared latest.json, so patching it inside the Linux leg races the other
platforms — a later leg's upload could resurrect the stale pre-repack
signature. The manifest patch moves to a post-matrix job that runs once
after all legs: it aligns the manifest's linux entries with the .sig
asset that actually shipped (self-verifying — no cross-job state), and
no-ops when they already agree. The leg keeps asset repack/re-sign only.

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

* fix(release): a failed .sig download fails the manifest-align job

Same fail-closed rule as the manifest itself: absence is decided by the
asset list; any other download failure must not exit 0 with a stale
signature left in latest.json.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 01:55:10 +00:00
Palash DebnathandClaude Fable 5 fb46fa4788 fix(release): repack the AppImage with a real .DirIcon file, re-sign, re-upload (#1544)
The v0.5.0 tag build — the first real release since #1518's guard — proved
the files-map fix loses: linuxdeploy re-links .DirIcon to an ABSOLUTE
build-machine path after tauri places the real bytes, and the guard
correctly refused to publish. tauri-action's atomic build+sign+upload
leaves only a post-upload seam, so the Linux job now repairs the packed
artifact: extract, replace .DirIcon with the icon bytes as a regular file
(nothing left to dangle), repack with appimagetool, re-sign with the
updater key, clobber the draft release's asset and patch the linux
signature inside latest.json. The existing smoke then validates the
repaired AppImage. No-ops cleanly when .DirIcon already resolves.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 00:59:14 +00:00
Palash DebnathandClaude Fable 5 f83b7371c3 docs: remote-workers guide speaks VoiceStudio (#1543)
CodeRabbit's last-cycle Minor on #1540, applied as the immediate
follow-up the docs-sync rule prescribes: three OmniVoice mentions in
docs/remote-workers.md now carry the product's name.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 00:07:05 +00:00
Palash DebnathandClaude Fable 5 9184d7d625 chore: bump to 0.5.0 (#1540)
* chore: bump to 0.5.0

Owner-requested minor bump. package.json is the source of truth; the
three mirrors (Cargo.toml, pyproject.toml, _FALLBACK_VERSION), the three
lockfiles and the branding pin move in lockstep, and the accumulated
Unreleased section becomes the curated 0.5.0 release notes — quiet
Highlights first, one-liner subsections after, duplicates folded.

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

* docs: fresh README for the VoiceStudio era; 0.5.0 notes wear the release

README: 551 lines from 656 — a What's-new-in-0.5.0 section with real
captures, the engine tables corrected to the actual 16 TTS registrations,
a stale Settings path and a broken Colab link fixed, roadmap/FAQ/credits
trimmed to what earns its place.

Release notes: the quick-switch GIF and catalogue/gallery screenshots,
captured from the running app during the pre-bump test pass, embedded
after the Highlights; #1542's gallery work and the ffmpeg CI fallback
recorded in their subsections.

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

* docs: the feature inventory's Remote Model Downloads mention survives the README trim

check-docs-drift requires every docs/features.yaml name verbatim in the
README; the overhaul folded the phrase away. It now lives in the remote
workers feature line.

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

* docs: one blockquote, and an architecture claim that survives the opt-ins

CodeRabbit on #1540: MD028 blank line inside adjacent blockquotes, and
'every layer is on your machine' contradicted the opt-in remote paths
documented two sections away — it now states local-by-default with the
opt-ins named.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 23:50:23 +00:00
Palash DebnathandClaude Fable 5 0ee62b2261 feat(gallery): save gallery voices as profiles, with validated audio references (#1542)
* feat(gallery): save gallery voices as profiles, with validated audio references

Work-in-progress lifted from the concurrent gallery session at the
owner's request (its uncommitted working tree, preserved verbatim from
base 92b1ee5d; safety snapshot remains at rescue/gallery-wip):

- gallery voices can be saved as local profiles: audio is copied into
  the profile store with content-addressed filenames, existing profiles
  are detected and refreshed only when the source clip changed
- backend/core/audio_validation.py: symlink-rejecting, root-contained
  resolution for persisted profile WAV references, with tests
- archetype/community routers and the Voice Gallery UI updated for the
  save-as-profile handoff (spec: docs/specs/longform/26-gallery-use-handoff.md)
- locale updates for the new gallery strings across all 21 files

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

* chore: drop a stray local screenshot script that rode in with the tree copy

* fix(community): explain the tolerated Content-Length parse failure; drop an unused import

CodeQL on #1542: the empty except now says why it is safe (the streamed
byte counter enforces the same cap regardless), and the test file loses
an unused Path import.

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

* fix(gallery): review findings — copy outside the write lock, no stale completions

CodeRabbit on #1542, all findings addressed:
- the profile-audio copy stages to a .part temp BEFORE BEGIN IMMEDIATE
  and publishes via atomic os.replace inside it — other backend writers
  no longer block for the duration of an audio copy; a mid-copy failure
  leaves no temp droppings and no profile row (both pinned by tests)
- VoiceGallery async ops carry per-operation generation tokens: a
  preview or save-as-profile that resolves after unmount (or after a
  newer operation) can no longer play audio, redirect into a workspace,
  or touch state — three fail-before regression tests
- VoiceGalleryActions imports the page at test runtime; the e2e locator
  uses a stable data-testid instead of a translated string; symlink
  tests skip cleanly where the OS can't create symlinks; the changelog
  line carries its PR ref

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

* ci: static ffmpeg fallback when the chocolatey feed is down

Third feed outage to break a PR run (2026-07-20, 2026-07-28, today —
three attempts, three 'installed 0/1'). Chocolatey is a distribution
channel, not the dependency: after the retry loop exhausts, fetch the
static gyan.dev build from its GitHub release mirror and put it on
PATH — same binary, no feed in the path. URL verified live (HTTP 200).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 22:48:05 +00:00
Palash DebnathandClaude Fable 5 77ae194f9c fix(asr): ROCm torch is not CUDA — keep CTranslate2 off the HIP GPU (#1539)
An RX 7900 XTX in the :rocm image crashed ASR init with 'CUDA driver
version is insufficient for CUDA runtime version' (#1529): ROCm torch
answers torch.cuda.is_available() and hands out 'cuda' device strings,
but whisperx/faster-whisper run on CTranslate2, whose CUDA runtime is
NVIDIA-only. Same class as the Apple/#1127 lesson, on the AMD axis.

- _ctranslate2_cuda_ok(): 'cuda' for CTranslate2 only when torch is a
  real CUDA build (torch.version.hip is the honest tell); ROCm hosts
  take CPU int8 instead of a native crash.
- _auto_detect(): on a ROCm-GPU host prefer pytorch-whisper — a pure
  transformers pipeline riding torch itself, so it actually uses the
  HIP GPU while CTranslate2 engines would idle on the CPU.

Fail-before/pass-after: 4 new tests fail on the old device pick/order.

Fixes #1529

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 21:05:53 +00:00
Palash DebnathandClaude Fable 5 d3822c4976 feat(engines): open the door between the LLM family and its providers (#1538)
* feat(engines): open the door between the LLM family and its providers

The openai-compat family entry and the LLM Providers panel are one
system — llm_backend resolves every call through the active provider —
but the UI presented them as unrelated (council coherence finding). Now:

- the catalogue's openai-compat row carries a 'Provider · model' hint
  naming the endpoint that actually answers (decorative: a provider
  registry hiccup degrades to no hint, never a failed listing)
- the row offers 'Configure providers' straight into Settings → LLM
  Providers; the panel gains the backlink into catalogue → LLM family
- three new strings in all 21 locales, matching each file's provider
  terminology

Also: bugReport's encoded-ceiling test is hermetic now — it was the one
test in its file trusting ambient fetch, and hung on any machine where a
local backend holds the port without answering.

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

* fix(i18n): the catalogue note names the ACTIVE provider, not the edited one

CodeRabbit on #1538: the panel can be editing a provider that is not
active, and 'this provider answers…' then points at the wrong one. The
note now says the provider MARKED ACTIVE answers, which is true under
any selection — no state-dependent copy needed.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 20:46:36 +00:00
Palash DebnathandClaude Fable 5 b982192011 fix(worker): reserve the capacity slot before announcing the accept (#1537)
* fix(worker): reserve the capacity slot before announcing the accept

Two changes for #1536 (flaky test_worker_at_capacity_rejects_without_penalty):

The client now inserts into _running BEFORE awaiting the accept send.
Today _send enqueues synchronously so the old order could not actually
interleave — but the reserve-then-announce order is the invariant that
stays correct if _send ever gains backpressure (a bounded outbox is the
natural evolution), instead of silently reopening an over-accept window.
If the accept send fails, the reserved task is cancelled: work the
scheduler never saw accepted must not run to double-execution.

The test now pins the real invariant — no over-concurrency — rather than
the scheduler's bookkeeping timing: on a loaded CI runner the first
attempt can die environmentally (a stream hiccup fails _run, whose
finally frees the slot), after which accepting the second task is the
CORRECT behaviour the old flat assertion punished as a failure. The
assertion now applies only while the first attempt is still running, and
names the over-accept explicitly when it fires.

Fixes #1536

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

* fix(worker): release the reserved slot on a cancelled accept-send

CodeRabbit on #1537:
- except BaseException, not Exception: a handler cancelled while the
  accept send is in flight must release the reserved slot too, or the
  unaccepted task keeps running and double-executes after reassignment.
  Fail-before/pass-after regression test included.
- the capacity test now polls the second task out of its dispatch states
  instead of sleeping 0.5s, and asserts the penalty-free invariant
  (excluded_workers empty) unconditionally — capacity rejections never
  exclude the worker regardless of the first attempt's health.
- TaskAccepted.envelope finding skipped: the field is read nowhere
  server-side (registration is the only envelope consumer) — pre-existing
  unused-field design, not introduced here.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 19:19:22 +00:00
Palash DebnathandClaude Fable 5 81b146f53b fix(i18n): 53 default-value-only strings now speak all 21 languages (#1534)
* fix(i18n): 53 default-value-only strings now speak all 21 languages

Every string used via t(key, { defaultValue }) without a locale entry
rendered English for every non-English user — the worker/compute chrome
(quick settings, join panel, QR enrolment), clone/design labels, workspace
voice strip, and two crash explainers. All 53 keys now exist in en.json
and carry reviewed translations in the 20 other locales, matching each
file's established terminology (existing worker/token/engine vocabulary,
catalogue tab names for the in-text path references, registers preserved,
{{placeholders}} byte-identical, the ovw_ token prefix untranslated).

The two crash explainers are translated from their FULL concatenated
source text — the extraction initially captured only the first string
segment, which src/test/streamDropError.test.ts caught by failing on the
missing proxy/buffering guidance.

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

* fix(i18n): reference localized UI labels inside diagnostic strings

CodeRabbit on #1534, fixed as the class: every locale's crash_broken_env
quoted the English "Clean & Retry" although the button itself is
localized — all 19 now quote each file's own clean_retry label. Plus the
flagged singles: es unload verb disambiguated from downloading, hi unload
verb aligned with crash_oom_kill, sv kontrollplan gender agreement, de
crash_broken_env moved to the file's Sie register, ru seed_reroll_hint
mistranslation, zh-TW path label matched to the real 系統日誌 section name.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 18:27:27 +00:00
Palash DebnathandClaude Fable 5 72cabb3daf feat(launchpad): wear the signal-field waveform in the hero (#1533)
The same cover artwork the project uses on the web, bleeding in from the
hero's right — where the layout holds only air — behind a radial feather
plus right-edge fade so no box edge survives, screen-blended so its dark
field vanishes into the chrome. Decorative: aria-hidden, empty alt,
pointer-inert; 22 KB webp bundled via vite.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:48:38 +00:00
Palash DebnathandClaude Fable 5 4a228a00b4 fix(crash): bound crash evidence to the run that produced it (#1532)
* fix(crash): bound crash evidence to the run that produced it

backend_err.log is one file shared by every backend run, and it was
TRUNCATED on each spawn. Both properties destroyed the evidence a crash
marker exists to carry: a respawn wiped the dead process's final words,
and an unbounded tail read afterwards attached the replacement's healthy
startup to the old run's crash marker — the undiagnosable report in #1510
(startup lines, no traceback, timestamps after the recorded crash).

The file is append-only now with a run-start header; each spawn records
the byte offset where its run begins; and every death path (crash markers,
venv-heal detection, restart-budget message, the 300s startup timeout)
reads through read_error_log_tail_for_run(), which cannot see another
run's output. The log rotates to backend_err.log.1 past 1 MiB so
append-only cannot grow unbounded. Bootstrap-phase reads (uv sync) keep
the whole-file reader — no backend run exists yet there.

Fail-before/pass-after: the new tests fail under the old File::create
truncation and unbounded tail.

Fixes #1510

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

* fix(crash): flush the dying run's stderr before the next offset; redact home paths

CodeRabbit on #1532:
- the stderr drainer is tracked now and joined (2s bound) before a new
  spawn records its offset, so a dead run's buffered tail cannot be
  appended after the new run's start and misattributed. Full per-child
  offset binding is unnecessary: spawns are serialized by the #1223
  spawn-once flow; the buffered tail was the only remaining window.
- the spawn-failure diagnostic redacts the home-directory prefix — it is
  retained across runs now and lands verbatim in bug reports.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:41:22 +00:00
Palash DebnathandClaude Fable 5 f81ace68d1 feat(engines): frame uninstalled engines as headroom, not failures (#1531)
The catalogue's group captions read "Available" / "Not installed", which
renders a fresh install (3 of 16 engines ready) as a mostly-broken app.
The sections now say "Ready to use" / "Add more engines", and the
unavailable-row details toggle asks "What it needs" instead of "Why
unavailable?" — same information, framed as headroom to unlock.

Council outcome (first-run seat): unavailable engines must read as more
you could install, never as brokenness. Keys added to all 21 locales.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 15:38:35 +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
Palash DebnathandClaude Opus 5 6948399e61 fix(wayland): rewrite a stale portal identity instead of trusting it (#1526)
* fix(wayland): rewrite a stale portal identity instead of trusting it

Found live: with the desktop entry's Exec pointing at a binary that had
been moved, GLib resolves the entry to NULL, the host portal answers
"Could not register app ID: App info not found", CreateSession then
fails with "An app id is required" — and the dictation shortcut is
silently dead for the entire session. Only the focused-window fallback
keeps working, which reads as "the shortcut randomly stopped".

ensure_desktop_identity() trusted any existing entry. It now validates
the USER-LOCAL entry's Exec target and rewrites the entry when the
program is gone (parsing both the current quoted spelling and the
unquoted one older builds wrote). System-dir entries stay untouched —
deb installs manage their own.

The stale-entry class is easy to hit in the wild: a dev entry pinned to
target/debug survives cargo clean; an AppImage entry survives the file
being moved or renamed.

(Rebuilt from the first push, whose `git add -A` had swept in another
working session's unrelated in-progress files; this commit carries only
the wayland fix and its changelog line.)

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

* fix(wayland): read Exec from the Desktop Entry group only

CodeRabbit, #1526: find_map over every line accepted an Exec= from a
[Desktop Action …] group, so an entry with no main-group Exec — which
GLib resolves to NULL — could be retained as healthy, keeping exactly
the stale identity the rewrite exists to replace. Parsing is scoped to
[Desktop Entry] now, with an action-only regression case.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 05:00:23 +00:00
Palash DebnathandClaude Opus 5 92b1ee5d1b test(asr): parse the selector guard instead of grepping it (#1524)
* test(asr): parse the guard, don't grep it

Two Majors CodeRabbit raised on #1523 — which I merged before reading
them, so this is the follow-up rather than a fix on the branch.

- Approved files were matched by BASENAME, so any future
  `<anything>/asr_backend.py` was exempt from the guard it exists to
  enforce. Matching is by relative path now; a decoy
  `backend/engines/asr_backend.py` calling the selector is caught.
- Detection was a line regex, wrong in both directions: it missed
  `import get_active_asr_backend as pick` and fired on the name inside
  docstrings and comments. It walks the AST now, alias-aware, so only
  real calls count.

Both verified by planting the exact bypasses: an aliased call in
services/tts_backend.py and the decoy module above. Neither was caught
before this change.

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

* test(asr): resolve the selector's bindings before flagging a call

CodeRabbit, #1524: matching any call named get_active_asr_backend also
reported a local helper or an unrelated object's method that happens to
share the name. False positives are how a guard stops being believed —
people add allowlist entries for code that was never the bug.

Bindings are resolved first now: a bare call counts only if the name was
imported FROM services.asr_backend, an attribute call only if it hangs
off a module alias for it. Six shapes are pinned in the suite — direct,
aliased and module-attribute calls flagged; a same-named local function,
an unrelated method, and the name inside a docstring not.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 01:40:01 +00:00
Palash DebnathandClaude Opus 5 9760f0c501 feat(ui): one support page, and tabs where there were toggles (#1522)
* feat(ui): one support page, and tabs where there were toggles

Sponsor, commercial licence and contact were three destinations for one
question — how do I support this / how do I reach these people — and each
one made you leave to find the others. They are now three sections of a
single page: support, licence, contact, in that order, separated by a
hairline rather than more chrome. Every existing entry point still works;
`initialView` scrolls to the right section instead of hiding the other
two, so the footer heart, the dub/export commercial-licence links and
Contact all land where they meant to.

ContactPage becomes `ContactSections` — the body without the shell — and
its "Support the project" CTA now scrolls up to the support section
rather than navigating, because that surface is on the same page.

Model Catalogue: the Engines/Models switch and the matrix's TTS/ASR/LLM
switch are tabs, not Segmented. These pick between workspaces, not
between the two states of one setting, and Tabs carries roving tabindex
and role="tab" from the primitive. The matrix tabs keep their active
engine chip and now keep their hover title too — Tabs passes `title`
through.

Tests: the pane/family switches are driven by pointer down, not click —
Radix activates on pointer down, so a bare fireEvent.click leaves the
pane unchanged and reads as a switcher that ignores itself. The contact
suite now covers the section (its host owns the header), and asserts the
support CTA scrolls without ever reaching for Ko-fi.

Full frontend suite: 2022 passed. The one unhandled `window is not
defined` rejection in the parallel run predates this change — same error,
same count, on the base commit.

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

* feat(engines): available engines first, and unavailable ones recede

Two things the matrix got wrong for a list you pick FROM: it rendered in
payload order, so a usable engine could sit under four you cannot select,
and an unavailable row was faded WHOLE — which took its status badge and
GPU chips down with it, the two things that say why it is unavailable.

Available rows now sort to the top, preserving registration order inside
each group (that order is meaningful — it puts the defaults first). The
name of an unavailable engine recedes instead, and its mark dims with it;
the evidence stays at full contrast.

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

* fix(support): the footer heart returns you to the support section

CodeRabbit, #1522: App.jsx renders SupportPage in the same tree position
for donate / enterprise / contact, so React keeps ONE instance and only
swaps props. The scroll effect treated 'support' as "already at the top"
and returned early — correct for a fresh mount, wrong for the only way
this page is actually reached. Clicking the footer heart from the contact
section left you sitting on contact.

Every view scrolls now. The regression test drives the prop change the
way the router does and fails without the fix.

Also adds the (#NNN) refs the Unreleased entries were missing.

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

* test(support): cover the enterprise route's licence section

CodeRabbit, #1522: the suite drove support and contact but not the third
destination — and every section renders regardless, so only the scroll
target proves the mapping. Uses the exact initialView App.jsx passes for
mode === 'enterprise'.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 01:37:09 +00:00
Palash DebnathandClaude Opus 5 41a8a7b644 test(asr): guard the raw selector across the whole backend (#1523)
The recurrence guard from #1512 scanned api/routers only. A service or
engine module that transcribes on a request's behalf skips ensure_loaded()
just as thoroughly, so the guard could be sidestepped by moving the call
one module down the stack — verified: adding a get_active_asr_backend()
call to services/tts_backend.py passes the router scan and fails this one.

The broader scan is the one thing #1519 did better than the fix that
landed in #1515; absorbing it here rather than leaving it in a PR that
now conflicts. Thanks @ahov520.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 01:03:40 +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 DebnathandClaude Opus 5 41c098e009 feat(demos): ship the demo audio and video the app already advertises (#1517)
* feat(demos): ship the demo audio and video the app already advertises

Every demo asset in the app was a dead link on anything but a Mac.

`personalities.py` has carried a `preview_url` for each of the seven
voice-design presets since they were added; DictationDemo.jsx posts three
bundled WAVs to /transcribe so the feature can be shown without microphone
permission; the Dub workspace reads a manifest and plays a source video plus
four dubbed languages. None of those files were committed, because the tooling
that renders them (scripts/build_demos.sh, scripts/build_dub_demo.sh) hard-
requires macOS `say` — it even carries a `TODO: add espeak-ng path for Linux
contributors`. So the presets returned 404, the replay buttons did nothing, and
the dubbing demo never loaded.

Rendered with VoiceStudio's own engine, which runs wherever the app does:

- 7 voice-design previews (2.2 MB)
- 3 dictation replay clips (1.1 MB) — verified by transcribing them back:
  the conversational and French clips round-trip exactly
- dubbing demo: source + 4 dubbed videos with subtitles and manifest (9.6 MB)

Tooling fixes this turned up:

- build_dub_demo.sh wrote to backend/assets/demo/dubbing, but main.py mounts
  backend/assets/samples at /demo_audio — so the frontend's
  /demo_audio/demo/dubbing/manifest.json could never have resolved even after
  a successful Mac build. Output moved under the mount.
- `say` is now the fallback rather than the requirement: the new
  scripts/render_dub_demo_audio.py renders the five tracks with the engine and
  the shell script picks them up.
- The five demo paragraphs lived in two files. They are now one JSON both read
  — two copies is one edit away from a video whose subtitles disagree with it.
- render_demos_omnivoice.py peak-normalized, which a single-sample transient
  defeats: the Helpdesk preset landed at -30 dB RMS against -17 dB for its
  neighbours, so the preview row played at wildly different volumes. Now EBU
  R128 at -18 LUFS with a -1.5 dBTP ceiling.
- …and pinning the output rate, because loudnorm resamples to 192 kHz
  internally and writes there unless told otherwise, which turned 2.1 MB of
  previews into 17.5 MB of identical-sounding audio.
- update_manifest() looked for a manifest at a path nothing writes, so it
  always printed "not found" and did nothing.
- Dictation is rendered here now too. It was excluded on the grounds that
  `say` was good enough and engine TTS was overkill — true only on macOS.

tests/test_demo_assets_exist.py resolves every advertised URL against the
directory main.py actually mounts, and checks each dubbing subtitle matches the
script its manifest entry claims. A missing static file is not an import error
and not a failing request; nothing would have caught this otherwise.

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

* docs(changelog): stamp the demo-asset entries with their PR ref

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

* fix(demos): watermark rendered demo audio, and harden the render scripts

Review findings on #1517:

- Greptile P1: the renderers wrote engine output straight to disk, so a
  re-render shipped demo audio with no provenance mark. These clips play
  back to users as VoiceStudio output — they are synthetic audio leaving
  the app like any other, and now go through mark_synthetic (#1169), the
  one chokepoint every producing route uses. It runs on the file AFTER
  loudnorm, since loudnorm re-encodes what it is handed, and says so
  loudly when marking is unavailable rather than committing an unmarked
  asset. The dubbing renderer shares the same helper.
- CodeRabbit: build_dub_demo.sh checked only source.src.wav before
  deciding it could run without macOS `say`, so a Linux or Windows run
  with four of five tracks present reached a missing one, called `say`,
  and left a half-built bundle. It now requires all five.
- CodeRabbit: shutil.move over an existing path delegates to os.rename,
  which raises FileExistsError on Windows — os.replace overwrites
  atomically everywhere.
- CodeRabbit: the preview test discovered presets in a parametrize
  argument, importing app code at collection time and leaving
  core.personalities in sys.modules for later tests. Discovery moved into
  the test body.

CI: the rendered dub bundle's zh/ja subtitles, its manifest and the
script source are dubbing CONTENT, not UI strings — allowlisted in
test_no_hardcoded_cjk.py with that justification.

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

* fix(demos): a render that cannot be watermarked fails instead of warning

CodeRabbit and Greptile, #1517: mark_synthetic degrades rather than
raising — correct for generation, wrong for a render script, whose whole
job is to produce files a human then commits. A printed warning on a
scrolling console is not a gate, so both scripts exited 0 with unmarked
assets sitting on disk ready to commit. They now raise, with the reason
and the fix; OMNIVOICE_DEMO_ALLOW_UNMARKED=1 stays for a local listen.

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:29:19 +00:00
Palash DebnathandClaude Opus 5 a310141114 feat(workers): join from the app, share by QR, and a status-bar Compute control (#1516)
* feat(workers): join from the app, share by QR, and a status-bar Compute control

Remote workers shipped with a hole in the middle: the control plane could
mint join codes, and on the other machine there was nothing to paste them
into. Becoming a worker meant launching with OMNIVOICE_WORKER_MODE and
OMNIVOICE_WORKER_TOKEN in the environment and relaunching — on the machine
that is usually the least convenient one to configure by hand.

Backend

- GET /workers/agent, POST /workers/agent/join, POST /workers/agent/enabled.
  Join redeems a code and starts the agent live; no restart.
- Worker mode now persists in settings as well as the environment (env still
  wins, and the panel is told so it can disable a switch it cannot honour),
  and it is written only after a join that actually worked — a failed
  enrolment must not have the app retrying on every launch.
- The endpoint carried by the redeemed code is remembered. Without that a
  machine that joined from the UI came back up enrolled but with nowhere to
  dial, and the only fix was OMNIVOICE_WORKER_ENDPOINT.

UI

- "Lend this machine's GPU": paste the code, Join. Once joined it offers a
  switch rather than another code, because the pinned certificate survives.
- <OneTimeSecret/> renders join codes and connection strings as a QR next to
  the text, with a live expiry countdown, and is used by both halves. QR
  generation is best-effort: a string past the format's capacity still shows
  the code and Copy, because losing the QR is a degraded share and losing the
  only copy of a one-time secret is data loss.
- Status-bar Compute control: pick local or a machine, flip the feature, mint
  a join code — without opening Settings. Absent entirely until the user has
  opted in or enrolled something.
- Remote workers now reads as a device list: status dot, address, latency,
  live task meter, resident models, last seen; housekeeping actions revealed
  on hover; a three-step empty state.
- Approve is on the row. A worker could connect, sit there labelled "Not
  approved" and never be usable, with no way out of it in the UI.

Fixes found on the way

- Status dots and menu surfaces in the GPU picker were painted from fixed
  Tailwind palette classes (bg-emerald-400, text-amber-400, hover:bg-white/5),
  so on Midnight or Catppuccin they showed Gruvbox colours next to the
  theme's own. Both controls now paint from themed --color-* tokens, shared
  in computeTarget.jsx along with the JSON wrapper all three copies duplicated.
- Button funnels every child into one <span>, so an icon passed as a child
  renders glued to its label — the flex gap only applies to the `leading`
  slot. Six buttons across these panels were affected.
- InboundNodePanel passed `variant="warning"` to Badge, which takes `tone`;
  the "on your network" warning rendered as an ordinary neutral pill.

Docs updated in the same change (docs/remote-workers.md): the join flow, the
QR, the status-bar control, and the new environment variable.

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

* docs(changelog): stamp the remote-workers entries with their PR ref

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

* fix(workers): a join is not done until the control plane accepts it

Review findings on #1516:

- Greptile P1: `start()` only SCHEDULES the dial-out loop, so a control
  plane that rejected this worker — expired token, wrong address, a
  server that never answers — looked identical to a successful join. The
  route persisted worker mode, reported success, and the machine retried
  forever on every launch. The agent now signals first registration, and
  join waits for it before persisting anything.
- CodeRabbit: a failed REJOIN left the machine unable to reconnect to the
  control plane it was already serving, because pinning the new
  certificate overwrites the old one on disk. Snapshot the pinned
  certificate, endpoint and setting up front, and restore them (and the
  running agent) when the join fails.
- CodeRabbit: join and the enable toggle awaited stop()/start() with no
  exclusion, so two concurrent requests could interleave their pairs and
  have `start()` return early — reporting success for a control plane it
  never dialled. Both now hold one lifecycle lock.
- CodeRabbit: with OMNIVOICE_WORKER_MODE set, the toggle still started or
  stopped the agent and wrote a setting the rest of the app ignores,
  contradicting the env_pinned status it reports. It now answers 409 and
  says which variable is in charge.
- CodeRabbit: the QR code kept encoding the previous secret until the new
  one finished encoding, so the code on screen could disagree with the
  text beside it.

CI: regenerated tests/fixtures/api_routes.txt for the three
/workers/agent routes.

Tests: a join the control plane never accepts is a 409 that persists
nothing and leaves no agent dialling; a failed rejoin restores the
previous certificate, endpoint and setting; an env-pinned machine
refuses the toggle.

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

* fix(workers): the environment pin governs joining too, not just the toggle

CodeRabbit, #1516:

- join_control_plane skipped the env_pinned guard set_agent_enabled
  enforces, and joining is precisely what ENABLES worker mode: under
  OMNIVOICE_WORKER_MODE it wrote a setting nothing consults, and with the
  variable pinned off it handed back a machine that reported a successful
  join and lent nothing. One shared guard now covers both routes.
- Two of the three rollback assertions could not fail before the fix
  (nothing wrote those settings on the failure path). The test now pins
  the behaviour only the rollback produces: the previous enrollment is
  dialling again, rather than left stopped until someone notices.

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 22:20:41 +00:00
Palash DebnathandClaude Opus 5 4fc07c21fd fix(appimage): stop shipping a dangling .DirIcon, and prove it in CI (#1518)
* fix(appimage): stop shipping a dangling .DirIcon, and prove it in CI

The Linux icon is blank because the AppImage's .DirIcon is an absolute
symlink into the machine that built it. From the published v0.4.2:

  .DirIcon -> /home/runner/work/OmniVoice-Studio/OmniVoice-Studio/frontend/
              src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/
              appimage/OmniVoice Studio.AppDir/OmniVoice Studio.png

That path exists on nobody's computer. The link dangles the moment the
AppImage leaves CI, so file managers have no icon for the file, and the
integration tools that read .DirIcon install nothing. A dangling symlink is
not a build error — the bundle packs, runs, and passes every check we had —
which is how it shipped for a whole release without anyone noticing.

Locally built AppDirs are worse: both .DirIcon AND the root .desktop symlink
come out absolute, so a from-source bundle has no readable desktop entry
either, which is why the icon is missing in the menu and the dock too.

- `.DirIcon` is now a real file, copied in through `appimage.files` — the
  same seam that already places the WebKitGTK marker.
- `bundle.category` is set, so the generated desktop entry stops emitting an
  empty `Categories=`. That is not the same as omitting the key:
  desktop-file-validate rejects the entry and menu builders skip it.
- verify-apprun-bundle.sh — already run against the extracted AppImage in the
  release job — now fails when .DirIcon is missing or resolves outside the
  bundle, when the .desktop entry does not resolve inside it, when Icon=
  names a file that is not at the AppImage root, or when Categories= is
  present but empty. Its unit test covers each of those, including the exact
  shape v0.4.2 shipped.

The `.DirIcon` copy cannot be verified without a full release build, so the
guard is the load-bearing part: the next release either passes it or fails
loudly. It can no longer ship blank in silence.

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

* docs(changelog): stamp the AppImage icon entries with their PR ref

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

* docs(changelog): fold the AppImage icon fix into the existing Fixed section

CodeRabbit (#1518): the Unreleased block must carry one `### Fixed`
section of one-line entries. Merge the two entries in and drop the
narrative and the version reference.

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 22:08:26 +00:00
Palash Debnath 8fdc5292e4 Merge pull request #1508 from anyingiit/fix/i18n-zh-cn-polish
fix(i18n): polish Simplified Chinese translations
2026-08-12 15:33:46 +00:00
debpalash a4fd62f85f fix(i18n): finish zh-CN review fixes 2026-08-12 15:15:49 +00:00
debpalash 6eaeb23403 Merge remote-tracking branch 'origin/main' into fix/i18n-zh-cn-polish 2026-08-12 15:15:06 +00:00
Palash Debnath fa8fddc456 Merge pull request #1506 from debpalash/fix/appimage-custom-apprun
fix(release): validate wrapped AppImage launcher
2026-08-12 14:57:23 +00:00
debpalash a089359b02 docs: place release smoke under changes 2026-08-12 13:52:43 +00:00
debpalash 4227cbd07d docs: record AppImage smoke fix 2026-08-12 13:48:34 +00:00
debpalash b99eadfafc Merge remote-tracking branch 'origin/main' into fix/appimage-custom-apprun 2026-08-12 12:42:52 +00:00
Palash Debnath 3f32f97e09 Merge pull request #1505 from debpalash/fix/worker-id-restart-race
fix(tests): synchronize worker identity persistence
2026-08-12 12:26:03 +00:00
debpalash 45eda7db01 test(worker): await identity persistence signal 2026-08-12 12:04:44 +00:00
debpalash fce5075e77 docs: record worker identity CI fix 2026-08-12 11:36:08 +00:00
debpalash 0fb9b38bfa fix(tests): synchronize worker identity persistence 2026-08-12 11:35:29 +00:00
debpalash 64973d1829 fix(release): validate wrapped AppImage launcher 2026-08-12 11:24:01 +00:00
Palash Debnath 57e435d3c0 Merge pull request #1504 from debpalash/fix/remote-port-and-ui-schema
fix: restore dub tracks and identify worker ports
2026-08-12 11:22:41 +00:00
debpalash 20d9cb5f1b docs: record remote recovery fixes 2026-08-12 11:00:33 +00:00
debpalash d9a939dff8 fix(desktop): restore dub tracks and identify worker ports 2026-08-12 02:54:50 +00:00
Palash Debnath 6f1e26a950 Merge pull request #1503 from debpalash/fix/remote-backend-port
fix: let remote backend bypass local setup
2026-08-12 02:01:08 +00:00
debpalash 06ca67650e docs: record remote startup recovery 2026-08-12 01:43:03 +00:00
debpalash 2e8a08973d fix(desktop): let remote backend bypass local setup 2026-08-12 01:42:32 +00:00
debpalash f47cec164f Merge pull request #1495 from velixio/main 2026-08-12 01:08:55 +00:00
debpalash 6c58294d56 fix(worker): close remaining public error flows 2026-08-12 00:28:35 +00:00
debpalash b548a7ab9d fix(security): close worker transport disclosure flows 2026-08-12 00:22:59 +00:00
debpalash b74703d506 style(worker): format integrated UI changes 2026-08-11 23:56:03 +00:00
debpalash 76d8c11553 fix(worker): persist pinned reconnect credentials 2026-08-11 23:54:28 +00:00
debpalash 037a5689de fix(worker): address legacy transport review findings 2026-08-11 23:49:46 +00:00
debpalash 5b806124c2 Merge remote-tracking branch 'origin/main' into fix/pr1495-final
# Conflicts:
#	CHANGELOG.md
2026-08-11 22:29:56 +00:00
Palash Debnath 50dd851bf8 Merge pull request #1502 from debpalash/feat/ui-scale-onboarding
feat: add first-run interface scaling
2026-08-11 22:13:26 +00:00
debpalash ffc6fade50 fix(ui): preview first-run scale choices 2026-08-11 21:45:52 +00:00
debpalash 0f69b4d3ca docs: record interface scale setup 2026-08-11 21:02:08 +00:00
debpalash 232784cb5a Merge remote-tracking branch 'origin/main' into feat/ui-scale-onboarding 2026-08-11 21:01:41 +00:00
Palash Debnath 32bd5cfedb Merge pull request #1491 from debpalash/feat/workspace-design-refresh
feat(ui): refresh core workspaces and settings
2026-08-11 20:39:56 +00:00
debpalash 6ba2e2a914 feat(ui): add first-run interface scaling 2026-08-11 20:35:11 +00:00
debpalash d5a496a8ad fix(ui): complete workspace review follow-ups 2026-08-11 20:24:49 +00:00
debpalash 8a8059b9c6 fix(ui): address workspace review findings 2026-08-11 20:03:15 +00:00
debpalash ce5f051252 fix(ui): remove framed structural borders 2026-08-11 19:49:10 +00:00
debpalash 6535ed3284 Merge commit '99e865600b2ce730ee7ac9860f97deea75ff57a1' into feat/workspace-design-refresh-consolidated 2026-08-11 19:38:59 +00:00
debpalash 5ef5d9b73a style: format consolidated workspace changes 2026-08-11 19:35:41 +00:00
debpalash 735cdd6b6f docs: record workspace design refresh 2026-08-11 19:34:17 +00:00
debpalash 6c679bfd75 feat(launchpad): simplify creative entry screen 2026-08-11 19:34:08 +00:00
debpalash 16a1ee63ee feat(profile): simplify the voice inspector 2026-08-11 19:30:52 +00:00
debpalash 2b0cd04599 docs: align workspace design specifications 2026-08-11 19:28:52 +00:00
debpalash f0382e0290 feat(workspaces): refine voice story and audiobook flows 2026-08-11 19:28:43 +00:00
Palash Debnath 99e865600b Merge pull request #1501 from debpalash/fix/remote-backend-recovery-1496
fix: recover from unreachable remote backends
2026-08-11 19:22:57 +00:00
debpalash 680fa8fdef feat(settings): refresh responsive preferences workspace 2026-08-11 19:16:21 +00:00
debpalash cf316b18bc test(remote): exercise streamed health responses 2026-08-11 19:04:05 +00:00
debpalash 5832a81bb6 feat(onboarding): refresh the bundled demo voice 2026-08-11 19:02:38 +00:00
debpalash 8e5a023058 fix(remote): bound startup health responses 2026-08-11 18:52:17 +00:00
debpalash 8d8765315f docs: link remote recovery changelog 2026-08-11 18:04:36 +00:00
debpalash dc5c9cf43e fix(remote): recover from unreachable backends
Closes #1496
2026-08-11 18:03:51 +00:00
Palash Debnath 008c8a70a6 Merge pull request #1500 from debpalash/fix/wavesurfer-abort-report-1498
fix(ui): ignore expected aborted audio streams
2026-08-11 17:47:10 +00:00
debpalash eaad1017df Merge commit '9930a0b41ab4a97560ddc30afdadc9728293d796' into fix/wavesurfer-abort-report-1498 2026-08-11 17:28:14 +00:00
Palash Debnath 9930a0b41a Merge pull request #1499 from Marc-oss-hub/feat/orcarouter-provider
feat(llm-providers): add OrcaRouter as a named LLM provider
2026-08-11 17:11:12 +00:00
debpalash 29ccf8ee52 docs: normalize OrcaRouter changelog credit 2026-08-11 16:53:58 +00:00
debpalash e6096a4eab test(llm): complete OrcaRouter provider contract 2026-08-11 16:47:43 +00:00
debpalash eb97a54105 Merge commit '1fc0b89778d056ca664723e4959621dc5fed9153' into audit/pr1499-fixes 2026-08-11 16:47:22 +00:00
debpalash 9687611e6f fix(ui): ignore cancelled waveform reports 2026-08-11 16:36:19 +00:00
Palash Debnath 1fc0b89778 Merge pull request #1490 from debpalash/fix/wayland-capture-shortcut
fix(dictation): support global shortcuts on Wayland
2026-08-11 16:23:16 +00:00
debpalash a95afc28ea Merge commit '19e352560e41feaf37b65d5007a57d93b9c0e1d4' into fix/wayland-capture-shortcut 2026-08-11 15:55:32 +00:00
Palash Debnath 19e352560e Merge pull request #1493 from debpalash/feat/dub-footer-action-polish
Polish dubbing workflow actions
2026-08-11 15:34:15 +00:00
debpalash 606d02a3ea Merge remote-tracking branch 'origin/main' into feat/dub-footer-action-polish
# Conflicts:
#	CHANGELOG.md
#	frontend/src/components/dub/DubHeader.jsx
2026-08-11 15:18:04 +00:00
Palash Debnath b28d0f5f08 Merge pull request #1489 from debpalash/feat/dub-workspace-polish
Polish Dub workspace controls and media history
2026-08-11 14:56:33 +00:00
debpalash 539a8bb571 fix(i18n): cover Arabic dub history plurals 2026-08-11 14:32:05 +00:00
debpalash e369179efb Merge commit '283642340b60054dfd67c3390831a94f45f34063' into fix/wayland-capture-shortcut 2026-08-11 13:59:55 +00:00
debpalash 918fca3ead Merge commit '283642340b60054dfd67c3390831a94f45f34063' into feat/dub-workspace-polish 2026-08-11 13:58:48 +00:00
debpalash 465b08cb9c fix(dub): pluralize history metadata 2026-08-11 13:58:34 +00:00
debpalash b52165d2a7 fix(dub): dismiss stale QC progress 2026-08-11 13:58:29 +00:00
debpalash 441099bf68 fix(dictation): close capture startup edge cases 2026-08-11 13:58:29 +00:00
debpalash c960dcb7e2 Merge remote-tracking branch 'origin/main' into feat/dub-footer-action-polish 2026-08-11 13:56:02 +00:00
Palash Debnath 283642340b Merge pull request #1494 from debpalash/fix/desktop-prod-appimage-stop
fix: stop extracted AppImage before prod reset
2026-08-11 13:38:40 +00:00
debpalash 8b6b9383f7 test: gate AppImage execution contract to Linux 2026-08-11 13:22:10 +00:00