11 Commits
Author SHA1 Message Date
debpalash 037a5689de fix(worker): address legacy transport review findings 2026-08-11 23:49:46 +00:00
velixio b7caa494eb feat(workers): remote downloads, audiobook chapters, and one port that stays honest
Five workstreams that finish the remote-GPU line, plus the test hole that
let a broken signature reach a commit.

**Downloads go through the normal path** (Phase 5). Rather than a second
remote-only route, the existing Models install flow became target-aware,
so a model landing on a worker uses the same code, the same progress
events and the same UI as a local one. Progress rows key on
(target, repo_id) — the aggregator keyed on bare repo_id, so the same
model downloading here and on a worker at once collapsed into one row
that told the user nothing true about either.

**Audiobooks render chapter by chapter on the worker** (Phase 8), with
per-chapter local fallback and ONE aggregated notice. The failure that
shape exists to prevent: a remote GPU that sleeps at chapter 40 of 200
must not turn a working book into 160 rows of PROGRESS_LEASE_EXPIRED.
Dictation is deliberately NOT ported — it runs ASR per utterance inside a
live WebSocket loop, and paying queue admission plus a round trip there
would spend the one thing that route is for.

**Dubbing stays local, and says so** (Phase 7). The coarse worker
operation is not finished, so the picker still reports dubbing as local
rather than showing a green remote chip over work this machine is doing.
What could not wait is the in-loop OOM retry: it sniffed the error string
and flushed the *local* CUDA cache, which under remote execution is the
wrong machine's GPU entirely. That is fixed now, before the path that
would have exercised it exists.

**Two instances can no longer share the control plane.** A second
VoiceStudio silently bound the same worker port and coexisted, so remote
workers landed on whichever process won the race — a session that
registers with one instance and appears dead to the other. This produced
hours of misdiagnosis during hardware testing and would hit any user with
the app open twice. The second instance now keeps running locally and
explains the conflict instead of quietly competing.

**And the hole that allowed all this to be missable.** gpu_gateway called
Scheduler.submit(pinned_worker_id=...) one commit before that parameter
existed. Every remote generation raised TypeError; 5236 tests passed
anyway, because nothing exercised the gateway against the real scheduler.
tests/test_gpu_gateway_scheduler_contract.py now runs that path for real
and binds every gateway→dependency call signature. Verified by renaming
the parameter away and watching both tests fail with the original error.

Gallery previews also fall back to a local render when a downloaded clip
cannot be decoded, rather than yielding silence.

Backend 5274 passed, frontend 1812 passed.

Not yet verified on hardware: Phases 4, 5, 6, 7, 8. Only the TTS path and
its artifact transport have been proven on a real GPU.
2026-08-11 16:53:33 +05:30
mergetestandClaude Fable 5 a601db8448 fix(win): stop the Fortran-runtime console-close abort and cp1252 UnicodeEncodeError crash classes (#1153, #1155)
Two Windows-only backend crash classes, one boundary (process spawn/stdio):

forrtl: error (200) (#1153 and the crash markers in #1155/#1152): MKL's
Intel Fortran runtime installs a console CTRL handler that aborts the
whole backend (exit 2 / 0xC000013A) when a console CLOSE/LOGOFF event
reaches it. The backend was spawned with no console isolation, so OS
console events could reach it mid-session. Now:
- the desktop shell spawns the backend with CREATE_NO_WINDOW |
  CREATE_NEW_PROCESS_GROUP (no console → no console events, stdio is
  piped anyway) and sets FOR_DISABLE_CONSOLE_CTRL_HANDLER=1;
- backend/main.py setdefaults the same var before torch/numpy can load
  MKL, covering scripts/run.sh and bare uvicorn launches too.

'charmap' codec can't encode (#1155): kittentts print()s the user's text
on every generate; on Windows the child's stdout is cp1252, so Vietnamese
text raised UnicodeEncodeError and surfaced as a bogus '400 Bad Request'.
The process-wide SafeFileWrapper only swallowed OSError (its EPIPE job).
Now:
- stdio is reconfigured to UTF-8 (errors=backslashreplace) at startup;
- SafeFileWrapper also swallows UnicodeError — logs are best-effort,
  synthesis is not;
- the shell sets PYTHONUTF8=1 for the child (Windows→parity with
  macOS/Linux; process env wins for power users);
- the crash-log append opens with encoding=utf-8 so tracebacks carrying
  user text can't re-trip the same codec.

Regression tests: tests/test_windows_stdio_guards.py (cp1252 stream write
must not raise; main must set the Fortran guard + UTF-8 stdio).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 01:45:23 +05:30
5302170688 fix(engines): contain SystemExit at the pool boundary — a CLI-shaped dependency killed the backend (#1133) (#1143)
* fix(engines): contain SystemExit at the pool boundary — a CLI-shaped dependency killed the backend (#1133)

Auto-report #1133 (8GB M1, v0.3.21, engine mlx-audio, exit code 1 at 21s
uptime) carried the whole story in its stderr tail: mlx-audio's Kokoro
pipeline uses misaki's G2P, whose __init__ runs spacy.cli.download() IN
PROCESS when en_core_web_sm is missing. spaCy's downloader is written as a
CLI: with no pip in the venv (uv-managed venvs ship none), its error printer
calls sys.exit(1). SystemExit is not an Exception, so every except Exception
on the path waved it through; it rode the executor future into the event
loop, where uvicorn treats SystemExit as "shut down" — backend dead.

Class fix, not a spacy special-case: _contain_system_exit() wraps every
callable dispatched through run_on_gpu_pool_guarded (all engine loads AND
generates funnel through it, #1033) and asr_backend.run_transcribe_guarded,
converting SystemExit into a RuntimeError that names the real failure mode.
Any engine dependency written as a CLI is now covered on both the TTS and
ASR sides.

Not done here (follow-up candidates): pre-provisioning en_core_web_sm for
the Kokoro/mlx-audio path so the download never triggers, and/or shipping
pip into the managed venv. Both are provisioning decisions; this PR makes
the failure survivable and honest first.

Tests: SystemExit from a pool job -> RuntimeError naming SystemExit(code),
executor still usable afterwards; same for the transcribe guard. Both fail
with the containment reverted. Full suite: 3016 passed.

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

* fix(engines): containment helper moves to a leaf module (CodeQL cyclic-import)

utils/containment is stdlib-only, so model_manager and asr_backend both
import it at module top with no cycle — the call-time back-import CodeQL
flagged is gone.

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

---------

Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 00:12:23 +05:30
d6f24dafd5 feat(hardening): six recurrence guards from the closed-issue-history audit (#1141)
* feat(hardening): six recurrence guards from the closed-issue-history audit

An agent audit swept every closed issue, clustered the error classes, and
checked each for fix + regression test + upgrade/reinstall survival. Six of
the "fixed but fragile" gaps are closed here; each guard has a regression
test in tests/test_recurrence_hardening.py (9 tests).

1. Evict-then-load (class 1, ~90 issues): a plain TTS load on a tight
   unified-memory box could still be OS-killed — the dub path frees memory
   before ASR loads (#1119) but nothing did before a TTS load.
   _make_room_before_tts_load() releases the idle capture-ASR model, clone
   prompts, and allocator caches when free RAM < the unified headroom.
   Deliberately NOT admission control: the #1111 decision (advisory-only,
   never refuse a load on an estimate) stands; this only does earlier what
   idle reclaim does later, and roomy machines skip it entirely.
2. Honest SIGKILL attribution (class 1): crashCauseHint() says "the OS ran
   out of memory (RAM)" for signal 9 instead of guessing VRAM on machines
   that have none. VRAM guidance kept for real GPU aborts (signal 6 etc.).
3. Clone-kind save sanitize (class 3, recurred 3x): the server-side instruct
   heal was gated to design-kind; a clone profile saved by any bypassing
   client could persist prose that 400s on every use. profiles.py now
   sanitizes both kinds at the single choke point.
4. Stale user_env validation (class 5): ~/.config/omnivoice/env is inherited
   verbatim by reinstalls; path-valued keys (OMNIVOICE_CACHE_DIR/DATA_DIR)
   that don't exist and can't be created are dropped for the run with a loud
   log line (file untouched — replugging the drive restores the setting).
   The two #480 precedence tests updated to use creatable paths (they test
   precedence, not path validity).
5. omni_ui schema guard (class 6): sanitizeOmniUi() whitelists + shape-checks
   every persisted field before restore — one malformed field used to throw
   mid-restore and silently discard everything after it, and every future
   field re-opened the #1067 class. Includes a lockstep test failing when
   useAppData reads a field missing from the schema.
6. safe_replace EXDEV helper (class 7): os.replace across devices raises
   EXDEV (the Windows D:-drive Errno 18/22 class); utils/fsops.safe_replace
   degrades to copy+fsync+replace. Adopted at the two cross-directory movers
   (log rotation, persona restore); temp-sibling writers stay on os.replace.
Plus: the generate timeout scales with text length (class 4's 503 wave —
   +1s per 40 chars past the first 1200, env floor respected), so long texts
   on slow hardware stop dying at exactly 300s with a "set an env var" remedy.

Deliberately NOT done, with reasons:
- ASR auto-promotion to the crash-isolated engine after a wedge: the code
  records an explicit owner rule against silent engine switching
  (asr_backend.py "we never switch engines automatically") — flagged to the
  owner instead of overridden.
- Rust items (webview cache-clear unit test, crash-marker versioning across
  updates): deferred to their own PR — the local cargo target was reclaimed
  for disk space, so they can't be verified locally right now.

Full suite: 2999 backend + 1243 frontend.

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

* docs(changelog): correct PR ref to #1141

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

* fix(hardening): review round — reclaim at the shared load boundary, write-probe path validation

Both Greptile P1s were real:

- "Startup preload skips reclaim": _make_room_before_tts_load() ran only in
  get_model(); preload_model() calls _load_model_with_timeout() directly, so
  a memory-tight machine was protected on demand loads but could still be
  OS-killed during the startup preload — the exact window the guard exists
  for. The reclaim now lives in _load_model_with_timeout(), the boundary both
  callers share.
- "Read-only paths pass validation": an existing directory on a read-only
  mount passes makedirs+isdir but fails on first real use, so the stale
  setting survived validation only to break downloads later. The check now
  probes actual write capability (create+delete a probe file). New test with
  a chmod-0o500 dir (skipped under root, where the probe cannot fail).
- CodeQL: the two intentional best-effort excepts in fsops.py now carry
  their explanatory comments.

Full suite: 3000 passed.

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

---------

Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 23:19:58 +05:30
4cc55ab852 Fast model downloads: Xet fast path + accurate progress (FDL W0–W2 + W4) (#424)
* feat(downloads): Xet fast path + accurate progress (FDL W0–W2)

Make model downloads fast and show accurate downloaded/remaining/speed.
Research confirmed hf-xet already implements the IDM/uGet technique
(content-defined chunking, parallel byte-range gets, dedup, resume), and
the spike found all 25 catalog repos are Xet-backed — so the win is
driving Xet well + accurate progress, not a custom downloader.

W1 — maximize + guarantee Xet:
- pin huggingface_hub>=1.7 + hf-xet>=1.1 (was transitive); no hf_transfer
- drive snapshot_download with explicit tqdm_class + max_workers + endpoint
- opt-in HF_XET_HIGH_PERFORMANCE / HDD sequential-write knobs (default off)
- /system/info reports fast_download {xet_enabled, xet_version, high_perf}

W2 — accurate progress:
- dry_run preflight -> install_plan event (exact total/cached/remaining)
- utils/download_aggregator.py: one overall bar; byte bars (by id) vs the
  "Fetching N files" count bar; windowed rate; emits one 'aggregate' event
- frontend overall bar (speed/remaining/ETA), cached-skip,  fast badge

Known limit (verified live): under Xet+hf_hub 1.7.2 per-file byte bars
never advance/close via tqdm, so mid-download the bar is file-granular and
bytes flush to the exact total on completion. Classic-LFS/mirror repos get
true byte progress (W4).

Drive-by: download.py used os.walk without importing os (latent NameError
in _validate_snapshot_has_weights on every install) — fixed.

Tests: tests/backend/setup/test_download_preflight.py (10). Spike + plan
under .planning/quick/260613-fdl-fast-model-downloads/.

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

* feat(downloads): opt-in mirror + cancel + docs (FDL W4)

- mirror (FDL-10): snapshot_download(endpoint=) honours prefs hf_endpoint /
  env HF_ENDPOINT on preflight + download (per-call, no process-wide env).
  Documented as the classic-LFS path (no Xet) for restricted networks.
- cancel (FDL-11): POST /models/install/cancel {repo_id} stops further
  retries at the next boundary, emits install_cancelled, clears the cooldown
  (cancel is intent, not failure). Frontend treats it as a terminator.
- docs (FDL-12): docs/downloading-models.md (Xet fast path, progress
  semantics + byte-speed limitation, opt-in tuning, mirror, cancel,
  troubleshooting) + README pointer. Docs-sync rule satisfied.

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

* docs(planning): model-management v2 cleanup plan (mm2)

GSD plan for cleaning the model-management subsystem: registry unload-on-
switch + per-engine unload() (fixes VRAM leak), model_lifecycle facade,
unified idle/timeout config, bounded cooldowns, sidecar VRAM self-report,
cache-fallback logging. Planning artifact only — no code.

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

* fix(downloads): reconcile with main's HF_HUB_DISABLE_XET; honest status

Rebasing onto main surfaced that main forces HF_HUB_DISABLE_XET=1 (classic
LFS) because Xet progress bypasses the tqdm hook — the same limitation found
here. Reconcile instead of fight:

- /system/info fast_download now reports runtime truth: xet_installed +
  xet_active (installed AND not HF_HUB_DISABLE_XET) + xet_enabled alias. The
   badge only shows when Xet actually runs; startup log says
  "downloads: Xet disabled → legacy LFS".
- complete(): clear the rate window before the final flush so crediting the
  full size in one step can't emit an absurd instantaneous rate.
- docs/downloading-models.md rewritten: default is legacy LFS for accurate
  progress; Xet is opt-in via HF_HUB_DISABLE_XET=0. hf-xet pin stays (ready
  for a future Xet progress hook).

W2 (preflight total/remaining + aggregate bar + exact completion) is the
value on either path; W1's "maximize Xet" is dormant by main's design.

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

* feat(downloads): opt-in segmented multi-connection accelerator (FDL W3)

Since main forces Xet off (HF_HUB_DISABLE_XET=1), the default path is
single-stream legacy LFS — so a segmented downloader is the way to get BOTH
parallel speed and live byte progress.

- services/segmented_download.py: async multi-connection Range downloader for
  one file — parallel byte-ranges, resume (.part + manifest), per-segment
  short-read truncation guard, optional sha256/etag verify, cancel, and a
  single-stream fallback when the server won't range. Auth-safe: the HF
  Authorization header is sent only to huggingface.co/hf.co and never
  forwarded to a CDN host on redirect (unit-tested).
- dispatch (download.py): opt-in via prefs segmented_downloader / env
  OMNIVOICE_SEGMENTED_DOWNLOAD (default off). When on and Xet inactive,
  fetches each file into the HF cache mirroring hf_hub_download (blobs +
  snapshot symlinks + refs/main), feeding real bytes to the aggregator. Any
  failure falls back to snapshot_download — never breaks a correct install.
- fix: complete() was adding a full total on top of accumulated segmented
  bytes (2x); now replaces byte bars so the sum is exactly total.

Verified live (accelerator on): real byte progress to ~16.6 MB/s, final
bytes==total, /models installed=True, delete frees correctly.

Tests: test_segmented_download.py (7) + aggregator double-count regression.

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

* test(downloads): relocate FDL tests to top-level; loop-isolate segmented test

CI runs the full suite, which exposed a pre-existing test-isolation leak:
several tests/backend/** fixtures purge core.*/services.* from sys.modules
under a temp OMNIVOICE_DATA_DIR and never restore, leaving core.config/core.db
bound to a dead temp dir. It only bites when collection order puts a purging
test ahead of a real-DB reader (test_longform_jobs). Adding tests under
tests/backend/setup/ reordered collection and tripped it.

Fix without touching the shared (fragile) fixtures or risking class-identity
breakage from a blanket sys.modules restore:
- move the two FDL test files to top-level tests/ (tests/test_fdl_*.py) so
  tests/backend/** collection order is identical to main — longform passes.
- rewrite the segmented test to run each case under asyncio.run() (fresh loop)
  instead of asyncio.get_event_loop(), which an earlier async test can leave
  closed in the full suite.

Full suite green locally: 1364 passed, 0 failed.

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

---------

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:15:15 +05:30
Palash Debnath 545b39c912 feat: Scalar API docs, community health files, Quickstart cards (#41)
* feat: Scalar API docs, community health files, Quickstart cards, GHCR Docker

Backend:
- Replace Swagger UI with Scalar at /docs (scalar-fastapi)
- Add OpenAI-compatible /v1/audio endpoints (openai_compat router)
- Add TTS streaming endpoint (tts_stream router)
- Add voice marketplace router (marketplace)
- Update TTS backend registry

Frontend:
- Refine CaptureWidget, WaveformTimeline, App layout
- CSS polish and index.css updates

Community health:
- SECURITY.md — vulnerability reporting policy
- CODE_OF_CONDUCT.md — Contributor Covenant v2.1
- .github/FUNDING.yml — GitHub Sponsors
- .github/ISSUE_TEMPLATE/ — bug report + feature request
- .github/pull_request_template.md — PR checklist

README:
- Quickstart redesigned as 3-column progressive cards
- Docker section updated with GHCR pull instructions
- API Docs row added to service table

Infra:
- scalar-fastapi added to pyproject.toml + uv.lock
- research/ added to .gitignore

* refactor: clean up documentation and logging while enhancing desktop packaging dependencies and capture UI performance.

* fix: address CodeRabbit review — streaming, escaping, thresholds

Backend:
- marketplace: stream zip entries via ZipFile.open()/copyfileobj, add 100MB
  upload cap, fix raise-from exception chaining (OOM prevention)
- openai_compat: _encode_audio returns actual file ext so Content-Disposition
  matches real format; forward non-profile voices when DB row not found
- tts_stream: send 'start' frame after generation so sample_rate is real;
  forward non-profile voices on DB miss
- capture_ws: split MIN_BUFFER_BYTES into separate partial/final thresholds
  so short utterances (<2s) still get transcribed

Frontend (Tauri):
- lib.rs: tray 'dictate' now toggles start/stop based on widget visibility
- commands.rs: XML-escape exe path in LaunchAgent plist, shell-quote in
  .desktop Exec line to prevent injection from special-char paths
- CaptureWidget.css: fix Stylelint violations (empty lines, font-family quotes)
2026-05-04 10:57:37 +05:30
riyaaa-04 81c4b7d1ed Fix transcription stream drops, IndexError, and Tauri CSP 2026-04-30 18:49:42 +05:30
debpalash 79826e19bc feat: realtime download speed, retry buttons, recheck top-right
- tqdm hook emits progress every 0.3s with backend rate (bytes/sec)
- Frontend uses backend rate for instant speed display, no 2s warmup
- Shows 'Connecting to HuggingFace…' during connect phase
- Shows 'measuring speed…' before rate is available
- Re-check button moved to top-right header in system preflight
- Retry + Clean & Retry buttons on failed splash screen
- Smart error hints (missing README, network timeout, port in use)
- README.md + omnivoice/ source package copied during bootstrap
- desktop-prod.sh wipes HF cache + all app data for fresh testing
2026-04-28 23:10:32 +05:30
debpalash 4a8b06c25e feat: implement structured progress tracking for model downloads and add local environment variable loading support. 2026-04-24 18:51:53 +05:30
debpalashandClaude Opus 4.7 994c6cf065 feat(backend): setup wizard router, translation engines, export options, client-disconnect handling
- Add setup router (backend/api/routers/setup.py) for first-run wizard:
  system checks, engine probes, model downloads with progress
- Add translation engines service with pluggable backends
- Add utils/hf_progress for HuggingFace download progress streaming
- Add PyInstaller runtime hooks (numpy compat, torch compiler disable)
- Global exception handler short-circuits h11 LocalProtocolError and
  Starlette ClientDisconnect with HTTP 499 to silence noisy stack traces
  when users scrub or cancel video mid-stream
- /dub/download-mp3 accepts bitrate query param (clamped 64–320kbps)
- Refactor ASR/TTS backends, dub pipeline, engine management
- Update backend.spec for PyInstaller packaging
- Bump pyproject version to 0.2.0; refresh uv.lock

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:49:04 +05:30