Renames what users see. The app, the installers, the window title, the
docs and all 21 locales now say VoiceStudio, with "(previously
OmniVoice-Studio)" noted near the title of each doc surface so people
recognise it.
Deliberately NOT renamed, because renaming any of them silently breaks
an existing install — there is no legacy-path fallback anywhere in this
codebase:
- bundle identifier com.debpalash.omnivoice-studio (MSI UpgradeCode,
macOS TCC grants, managed venv, WebView localStorage, the
single-instance lock)
- data directories OmniVoice / .omnivoice and omnivoice.db
- the ~150 OMNIVOICE_* environment variables
- the X-OmniVoice-* HTTP headers (a wire protocol)
- the published Docker image paths
- the OmniVoice ENGINE, which is a model name and not this product
tests/test_identity_paths_survive_the_rename.py pins every one of those
so a future well-meaning sweep cannot orphan a user's library.
Linux .deb users install a new package name and should apt remove
omnivoice-studio; that note is in the changelog.
1435 lines
108 KiB
Markdown
1435 lines
108 KiB
Markdown
# Competitive Analysis — voicebox · pyvideotrans · Patter (+ second-tier landscape)
|
||
|
||
*Compiled 2026-06-11 from four parallel research passes (one per competitor repo + a
|
||
full feature-surface inventory of this codebase). Star counts and versions are
|
||
as-of-date snapshots. VoiceStudio grades (A–D) come from the self-inventory: code
|
||
signals, test coverage, TODO density, and open-issue mentions — not marketing.*
|
||
|
||
*Expanded 2026-06-11 (second pass, six additional research agents): second-tier
|
||
landscape survey, source-level deep dives into the flagged components of all three
|
||
competitors (clean-room functional specs for the GPL one), implementation specs
|
||
mapping every ranked action onto this codebase, and user-sentiment / market-positioning
|
||
research. Three grades in the matrix were corrected where the original self-inventory
|
||
was stale — see the matrix legend.*
|
||
|
||
*Expanded again 2026-06-12 (third pass): the
|
||
[Roadmap directions](#roadmap-directions-community-discussion-346) section grounds
|
||
the feature roadmap announced in discussion #346 — agentic voice workflows, remote
|
||
GPU backends, audiobook creator, persona gallery, model/env + GPU-compat management —
|
||
in landscape, license, and regulatory research, with scope ladders and eight new
|
||
consolidated actions (15–22).*
|
||
|
||
## TL;DR
|
||
|
||
| | [voicebox](https://github.com/jamiepine/voicebox) | [pyvideotrans](https://github.com/jianchang512/pyvideotrans) | [Patter](https://github.com/PatterAI/Patter) |
|
||
|---|---|---|---|
|
||
| What | Local-first voice studio (ElevenLabs + WisprFlow alt) — **our most direct competitor** | Desktop video translate/dub pipeline (GUI + CLI) | Telephony voice-agent SDK (self-hosted Vapi/Retell alt) — adjacent, not competing |
|
||
| Stack | Tauri v2 + React + FastAPI (same as us) | PySide6 (Qt) + FFmpeg, 100 % Python | Python + TS dual SDK |
|
||
| Maturity | v0.5.0, ~29.7k★, fast cadence, beta-grade hardware backlog | V4.01, ~17.9k★, 2.5 yrs mature, monthly releases | v0.6.x, ~511★, 2 months old, exceptionally well-engineered |
|
||
| License | **MIT** | **GPL-3.0** | **MIT** |
|
||
| Code reuse for us | ✅ **Port directly** (keep MIT attribution header) | ⚠️ **Reimplement ideas only — never copy** | ✅ **Port directly** (keep MIT attribution header) |
|
||
|
||
**Second-pass promotion:** [KrillinAI / KlicStudio](https://github.com/krillinai/KlicStudio)
|
||
(Go, ~10.3k★, GPL-3.0, v2.0.3 released 2026-06-09, only 28 open issues) is now rated
|
||
**as direct a competitor as voicebox for the dubbing user** — desktop+web+CLI form
|
||
factors and a deliberate "for AI agents" strategy that competes with our MCP angle.
|
||
GPL-3.0 → study-only. Full profile in the [second-tier landscape](#second-tier-landscape)
|
||
below; it has not been folded into the per-capability matrix because we have not done
|
||
a capability-by-capability source pass on it yet.
|
||
|
||
**License ground rule.** VoiceStudio is **AGPL-3.0-only with a commercial
|
||
dual-license offering**. MIT code can be incorporated (attribution preserved) and
|
||
stays compatible with selling commercial exceptions. GPL-3.0 code is technically
|
||
combinable with AGPL-3.0 (GPLv3 §13), **but** copied GPL files stay GPL-3.0 forever
|
||
under the original author's copyright — which would break the commercial-license
|
||
model (we can only sell exceptions for code we own). So pyvideotrans is a
|
||
*design-document*, not a code source: study `_rate.py`, write our own.
|
||
The same logic applies to **third-party AGPL code** (e.g. alltalk_tts): even though
|
||
we are AGPL ourselves, we cannot sublicense someone else's AGPL code under our
|
||
commercial exception — study-only, same as GPL.
|
||
|
||
Fun fact discovered en route: **pyvideotrans already integrates VoiceStudio as
|
||
a first-class TTS/clone backend** (`videotrans/tts/_omnivoice.py`, via our Gradio
|
||
API). We are upstream for 17.9k-star project users. **Second-pass update: the
|
||
integration is verified broken** — it speaks Gradio to an endpoint we never exposed.
|
||
Details in the [pyvideotrans deep dive](#7-fact-check-the-pyvideotrans--omnivoice-integration);
|
||
the action item is now "ship a compat shim or upstream a REST integration", not
|
||
"verify".
|
||
|
||
---
|
||
|
||
## Big feature matrix
|
||
|
||
Legend — **Us**: A–D maturity grade from the self-inventory. **Them**: ✅ stable ·
|
||
🟡 beta/partial · ❌ absent. *(bold = they beat us; this is the gap list)*
|
||
*Second-pass corrections (marked △): three "Us" cells in the original were stale —
|
||
the self-inventory missed `scripts/validate-install-docs.py`, the probe-judge +
|
||
`omnivoice/eval/` stack, and the real FastMCP server at `backend/mcp_server.py`.*
|
||
|
||
| Capability | Us | voicebox | pyvideotrans | Patter | Notes |
|
||
|---|---|---|---|---|---|
|
||
| **Generation & cloning** |
|
||
| Zero-shot voice cloning | B | ✅ | ✅ (via clone-TTS engines) | ❌ | Parity; their multi-sample profiles are slightly ahead |
|
||
| Preset voice library (no reference audio) | B+ (20+ archetypes) | ✅ **50+ presets** (Kokoro/Qwen) | ❌ | ❌ | They win on count, we win on curation + degenerate-check |
|
||
| Voice design from text description | B | 🟡 (personality descriptors) | ❌ | ❌ | We're ahead (#317 shipped a deterministic mapper) |
|
||
| **Unlimited-length generation (chunk + crossfade)** | ❌ (no auto-chunking) | ✅ `chunked_tts.py` | ✅ (per-subtitle by design) | ❌ | **Gap.** Their crossfade chunker removes the length ceiling |
|
||
| Paralinguistic tags (`[laugh]`, `[sigh]`) | B (13 native reaction tags via ⊕ Insert; no `[breath]` yet) | ✅ (Chatterbox Turbo) | ❌ | ❌ | Near-parity — see docs/expressive-speech.md; CosyVoice 3 adds `[breath]`/`[laughter]` |
|
||
| Delivery instructions ("whisper", "slowly") | B (instruct field) | ✅ (Qwen NL control) | ❌ | ❌ | Parity-ish |
|
||
| Generation queue w/ cancel + SSE | B+ (job store, SSE replay) | ✅ | ✅ (9-queue pipeline) | n/a | Parity; our SSE reconnect-replay is ahead of voicebox |
|
||
| Post-processing FX chain (reverb/pitch/comp) | B (effect chain exists) | ✅ **Pedalboard, per-profile presets** | ❌ | ❌ | Theirs is richer + has preset UX |
|
||
| Multi-track timeline editor (stories/podcasts) | ❌ | ✅ **Stories editor (v0.5.0)** | ❌ | ❌ | **Gap** — also the #280-item-3 timeline ask |
|
||
| Audio watermarking (AudioSeal) | B | ❌ | ❌ | ❌ | **We're unique here** |
|
||
| **Dubbing pipeline** |
|
||
| Full video dub (ASR→translate→TTS→mux) | A– | ❌ | ✅ (1200-line battle-tested pipeline) | ❌ | Two-horse race; we're competitive |
|
||
| Incremental re-dub (change 1 line, regen 1 segment) | A– (#281 fixed) | ❌ | ❌ | ❌ | **We're unique here** |
|
||
| **Dub-length fitting (audio speedup + video slowdown)** | A– (Smart Fit complete: planner + generate path + fit fingerprints + two-tier video-retime export with drift absorption and fitted subtitles) | ❌ | ✅ **`_rate.py` — the crown jewel** | ❌ | **Gap closed** — Action 1 reimplemented clean-room (`services/fit_planner.py`, `services/video_retime.py`, [Spec 1](#spec-1--dub-length-fitting-v2)) |
|
||
| Vocal/BGM separation + re-mix | A– (Demucs 4-stem) | ❌ | ✅ (UVR/Spleeter ONNX) | ❌ | Parity; their ONNX models are lighter than Demucs |
|
||
| **Clone refs cut from separated vocals per segment** | 🟡 (speaker_clone refs 5–15 s/speaker) | ❌ | ✅ per-subtitle-line refs | ❌ | Their per-line granularity beats our per-speaker. Action 4 |
|
||
| Speaker diarization → multi-voice dub | B+ (pyannote) | ❌ | ✅ (4 backends incl. CAM++) | ❌ | Parity; their backend choice is wider |
|
||
| **Second-pass ASR on dubbed audio** (regenerate exact subtitle timings) | ❌ | ❌ | ✅ | ❌ | **Gap** — clever QC step. Action 5 |
|
||
| Subtitle styling / burn-in / dual-language | A– (#309 fixed) | ❌ | ✅ | ❌ | Parity |
|
||
| Batch processing (N videos) | B (50-job queue) | ❌ | ✅ (wave control, multi-GPU scaling) | ❌ | Their `batch_nums` waves + per-GPU thread scaling is ahead |
|
||
| Translation channel breadth | B (LLM 3-step chain + glossary) | ❌ | ✅ **~25 channels** | ❌ | Breadth vs depth: our reflect/adapt chain is deeper, their coverage wider |
|
||
| Translation caching + line-count validation | 🟡 (fingerprints #281) | ❌ | ✅ MD5 cache + timeline re-match | ❌ | Worth studying |
|
||
| **Dictation** |
|
||
| Global-hotkey dictation pill | B+ (#323 fixed) | ✅ (v0.5.0, auto-paste **macOS-only**) | ❌ | ❌ | We're ahead on cross-platform (their gap violates our parity rule) |
|
||
| **LLM transcript refinement (filler-word removal)** | ❌ | ✅ local Qwen3 0.6B–4B | ❌ | ❌ | **Gap.** Action 3 |
|
||
| **Captures library (replay / re-transcribe / refine)** | 🟡 (transcription history page) | ✅ richer (v0.5.0) | ❌ | ❌ | Partial gap — we store, they iterate |
|
||
| Dictation while audio plays (echo cancel) | ✅ opt-in (Settings → Capture): server-side NLMS AEC on `/ws/transcribe?aec=1` + AudioWorklet PCM mic + player far-end tap — Action 8 | ❌ | ❌ | ✅ NLMS AEC | Ported Patter's NLMS canceller. Action 8 |
|
||
| **Engines & platform** |
|
||
| TTS engine count | B (6) | ✅ 7 | ✅ **33 channels** (22 ASR, 25 translate) | ✅ 7 (cloud) | pyvideotrans = breadth king (incl. cloud); we + voicebox are local-only by design |
|
||
| Engine plugin protocol | B+ (ABC + registry) | ✅ Protocol + ModelConfig registry, **agent skill for adding engines** | ✅ lazy dataclass plugins | ✅ provider SDK | Everyone converged on the same pattern; their `requires_cuda`-gap lesson is free for us |
|
||
| **MLX runtime on Apple Silicon** | 🟡 (MLX-Audio engine only) | ✅ **MLX for TTS+STT, 4–5× claimed** | ❌ | ❌ | **Gap** — dual-runtime per engine. Action 6 |
|
||
| **CUDA binary auto-download (small installer)** | ❌ (venv on first run ships everything) | ✅ in-app CUDA swap incl. sm_120 | ❌ | ❌ | Different bootstrap philosophy; their #1 bug source too. Study only — failure-mode autopsy in the deep dive |
|
||
| Crash-isolated engine subprocesses | 🟡 (Demucs/ffmpeg subprocesses) | ❌ | ✅ (whisper.cpp etc. in child procs) | ❌ | Their JSON-log polling pattern is a cheap stability win. Action 7 |
|
||
| ROCm support | A– (with edge cases) | 🟡 (large breakage backlog) | 🟡 | n/a | We're ahead |
|
||
| **Integration surface** |
|
||
| OpenAI-compatible API | B+ | ✅ REST | ❌ | n/a | Parity |
|
||
| **MCP server (agent speaks in your voice)** | △ B– (FastMCP `backend/mcp_server.py`: 4 tools, stdio + SSE; **not mounted on the main app, no per-agent voice binding**) | ✅ **FastMCP at `/mcp` + stdio shim, per-agent voice binding** | ❌ | ✅ (client + server) | Gap is narrower than originally graded; what's missing is exactly the half voicebox shipped. Action 2 |
|
||
| Web/Docker deployment | B– (headless image exists) | ✅ (`docker compose up`) | ❌ (desktop only) | ✅ | Parity-ish; our :latest/:stable retag (PR #338) helps |
|
||
| CLI / headless batch | 🟡 (API only) | ❌ | ✅ `cli.py` (stt/tts/sts/vtv) | ✅ | Partial gap for power users |
|
||
| Streaming TTS (websocket, low TTFA) | C+ (`/ws/tts` experimental) | ❌ | ❌ | ✅ **sentence-chunked streaming, first-flush** | Patter's chunker + first-flush are portable. Action 8 |
|
||
| **Ops & quality discipline** |
|
||
| Eval harness for output quality | △ 🟡 (probe judges `tests/probe/judges/` + `omnivoice/eval/` WER/MOS/speaker-sim — deterministic tier exists, **no semantic/LLM-judge tier**) | ❌ | ❌ | ✅ LLM-judge evals + CLI | Patter's harness adds the missing *semantic* tier. Action 9 |
|
||
| **Docs-drift CI** | △ 🟡 (`scripts/validate-install-docs.py` gates `docs/install/*.md` in ci.yml — **no inventory-wide drift job**) | ❌ | ❌ | ✅ daily inventory-vs-docs diff job | Patter's rolling-issue automation is the missing half. Action 9 |
|
||
| Model-evaluation decision log | 🟡 (ROADMAP phases) | ✅ `PROJECT_STATUS.md` accepted/abandoned log | ❌ | ❌ | Cheap practice to adopt |
|
||
| Telemetry design (consent-bounded) | n/a (opt-in GH Issues only) | ❌ | ❌ | ✅ consent module, bucketed values | Reference design for the bug reporter — allowlist pattern in the deep dive |
|
||
|
||
### Where we are unique (defend these)
|
||
|
||
- **Incremental re-dub** with fingerprint tracking — nobody else has it.
|
||
- **646-language claim** via VoiceStudio model — voicebox tops out at 23, pyvideotrans is engine-dependent.
|
||
- **AudioSeal watermarking + detection** — unique among all three.
|
||
- **Cross-platform dictation as a default** (their auto-paste is macOS-only).
|
||
- **3-step LLM translation chain (translate → reflect → adapt) + glossary** — deeper than anyone's single-pass.
|
||
|
||
*Second-pass reality check: market evidence per item — which of these users actually
|
||
ask for — is in [User sentiment & positioning](#honest-verdicts-on-our-unique-five).
|
||
Short version: (a) and (d) are strong levers, (b) is a reach lever with a quality-risk
|
||
tail, (c) and (e) are real but nobody searches for them by name.*
|
||
|
||
---
|
||
|
||
## Second-tier landscape
|
||
|
||
*Surveyed 2026-06-11; stars / last-push / open-issue counts verified via the GitHub
|
||
API. These either compete for the same user or carry portable ideas, but none (except
|
||
KrillinAI) warrants a per-capability matrix column yet.*
|
||
|
||
| Project | What | Stack | Maturity (2026-06-11) | License | Overlap w/ us | Verdict |
|
||
|---|---|---|---|---|---|---|
|
||
| [KrillinAI/KlicStudio](https://github.com/krillinai/KlicStudio) | LLM video translate+dub for humans *and AI agents* | **Go** + web/desktop/CLI | 10.3k★, v2.0.3 **2026-06-09**, 28 issues | GPL-3.0 | Dubbing, cloning, API server | **Direct competitor (promoted)**; reimplement only |
|
||
| [VideoLingo](https://github.com/Huanshere/VideoLingo) | One-click "Netflix-grade" subtitle + dubbing pipeline | Python/Streamlit, cloud LLM | 17.4k★, v3.0.1 2026-02, 208 issues | Apache-2.0 | Dubbing, cloning (GPT-SoVITS), subtitles | Direct competitor (web, cloud-LLM-dependent); **port OK** |
|
||
| [voice-pro](https://github.com/abus-aikorea/voice-pro) | All-in-one local cloning/TTS/Whisper/Demucs/translate WebUI | Python/Gradio (Windows-leaning) | 10.9k★, push 2025-12, 47 issues | GPL-3.0 | Cloning, TTS, STT, separation, translation | Direct competitor (web form factor, 6 months quiet); **reimplement only** |
|
||
| [ebook2audiobook](https://github.com/DrewThomasson/ebook2audiobook) | Ebook → audiobook w/ cloning, 1,158+ languages | Python/Gradio + CLI/Docker | 19.2k★, push **2026-06-11**, **10 issues** | Apache-2.0 | Cloning, multi-engine TTS | Adjacent vertical; **port OK** |
|
||
| [GPT-SoVITS](https://github.com/RVC-Boss/GPT-SoVITS) | Few-shot cloning + TTS WebUI w/ training tools | Python/Gradio + API | 58.6k★, push 2026-04, 872 issues | MIT | Cloning, TTS (we already wrap it as `GPTSoVITSBackend`) | Component + partial competitor; **port OK** |
|
||
| [SoniTranslate](https://github.com/r3gm/SoniTranslate) | Gradio video-dubbing pipeline w/ diarization + cloning | Python/Gradio | 1.4k★, push 2026-04, **no release since 2024-05**, 122 issues | Apache-2.0 | Dubbing, cloning, multi-TTS (we sidecar it) | Component + adjacent; **port-with-attribution OK** |
|
||
| [Speaches](https://github.com/speaches-ai/speaches) | OpenAI-compatible local STT+TTS+Realtime server | Python/Docker | 3.4k★, push 2026-06-10, 136 issues | MIT | API-server surface only | Component-not-competitor; **port OK** |
|
||
|
||
### Notable ideas / gaps per project
|
||
|
||
**KrillinAI / KlicStudio** — the promotion case: released two days before this survey,
|
||
28 open issues at 10.3k★ (most operationally disciplined direct competitor), and a
|
||
`skills/` Agent-Skills framework exposing stable CLI contracts + JSON artifact
|
||
manifests so AI agents drive each pipeline stage independently — it validates and
|
||
extends our MCP-server direction. Also: portrait/landscape re-rendering + cover
|
||
generation for TikTok/Shorts, a social-publishing tail we ignore. GPL-3.0: ideas only.
|
||
|
||
**VideoLingo** — its "Translate-Reflect-Adaptation" chain is the same 3-step idea as
|
||
our `translator.py` chain (convergent evolution; theirs predates the comparison —
|
||
worth a diff of prompt strategies, Apache-2.0 so even portable). Netflix-standard
|
||
subtitle segmentation with word-level alignment overlaps our `subtitle_segmenter.py`.
|
||
Per-step pause/resume/stop on long pipelines is a UX gap we have. Counter-positioning:
|
||
it *requires* a cloud LLM key — our local-first translation is the differentiator to
|
||
message against it.
|
||
|
||
**voice-pro** — closest single-app feature overlap in the tier (cloning + TTS +
|
||
Whisper + Demucs + YouTube ingest + translation). Proves 10k+★ demand for exactly our
|
||
bundle in a clunkier (Gradio, Windows-leaning, GPL) package; quiet since 2025-12 —
|
||
its users are capturable if it stalls. The recurring expectation across this whole
|
||
tier (voice-pro, VideoLingo, KrillinAI, SoniTranslate): built-in
|
||
YouTube-download → process loop. Our URL-ingest dub path covers part of this.
|
||
|
||
**ebook2audiobook** — the long-form vertical we don't serve: chapterized m4b output,
|
||
inline SML tags (`break`/`pause`/voice-switch mid-text), OCR'd PDFs, per-file voice
|
||
mapping in batch. A "narrate a whole book" mode is a credible extension (Apache-2.0,
|
||
portable). Separately: 10 open issues at 19.2k★ is the best issue-hygiene benchmark
|
||
in the landscape — study their template/triage setup for our bug-reporting milestone.
|
||
|
||
**GPT-SoVITS** — dual role: an engine we wrap *and* a competitor for the DIY cloning
|
||
user. The big capability we lack vs. that crowd is its in-app fine-tuning chain
|
||
(dataset slicing → ASR → labeling → train); MIT, so the training-tool code is
|
||
portable. Version churn (v2/v2Pro/v3/v4) is a live threat to our `GPTSoVITSBackend`
|
||
engine-compat constraint — pin and contract-test the API surface.
|
||
|
||
**SoniTranslate** — we already depend on it: `backend/services/sonitranslate.py` runs
|
||
it as an isolated Gradio sidecar (port 7860) via `gradio_client`. Apache-2.0 means we
|
||
may also vendor its code in-tree with attribution if sidecar reliability ever becomes
|
||
a problem (process-level integration carries no derivative-work questions at all, so
|
||
the current setup is the safest). **Risk: its release cadence stalled (v0.5.0,
|
||
May 2024)** while we depend on it at runtime — consider pinning a fork.
|
||
|
||
**Speaches** — "Ollama for audio": dynamic model auto-load/unload per request is
|
||
directly applicable to our multi-engine VRAM juggling (MIT, portable). Its OpenAI
|
||
Realtime-API emulation is the compatibility surface third-party clients will
|
||
eventually ask our API server for. From the same bucket:
|
||
[Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI) (Apache-2.0, 5.0k★) ships
|
||
**weighted voice mixing** (blend voices by ratio) — a cheap, differentiating voice-design
|
||
feature. New Action 12.
|
||
|
||
### Watch items (not competitors)
|
||
|
||
- **[TTS-WebUI](https://github.com/rsxdalv/TTS-WebUI)** (MIT, 3.2k★, active) — a
|
||
40+-model local audio hub whose repo description **already advertises a VoiceStudio
|
||
extension**. Verify what that extension wraps and that AGPL terms are respected.
|
||
Its per-extension `uv` venv isolation parallels our sidecar approach.
|
||
- **[F5-TTS](https://github.com/SWivid/F5-TTS)** (14.7k★) — engine candidate, not an
|
||
app. Trap: code is MIT but **pre-trained weights are CC-BY-NC** — an F5-TTS engine
|
||
would need prominent non-commercial-weights labeling like our existing
|
||
engine-license gates.
|
||
- **[alltalk_tts](https://github.com/erew123/alltalk_tts)** (AGPL-3.0, 2.4k★, solo
|
||
maintainer) — adjacent TTS server. Third-party AGPL = study-only for us (see
|
||
license ground rule). Its narrator/character voice-switching markup and low-VRAM
|
||
modes are reimplementable ideas.
|
||
- **[Linly-Dubbing](https://github.com/Kedreamix/Linly-Dubbing)** (Apache-2.0, 3.2k★,
|
||
~15 months stale) — idea quarry only; its lip-sync integration (re-syncing mouth
|
||
movements to the dubbed track) is the one feature nobody in the landscape, us
|
||
included, ships.
|
||
- **[resemble-ai/chatterbox](https://github.com/resemble-ai/chatterbox)** (~25.0k★,
|
||
MIT) — engine candidate; full evaluation in the appendix.
|
||
|
||
---
|
||
|
||
## Ranked actions
|
||
|
||
Effort: S < 1 day · M = 1–3 days · L = 1–2 weeks. "Port" = copy + adapt MIT/Apache code
|
||
with attribution header. "Reimplement" = clean-room from the functional spec in this
|
||
doc — **do not open pyvideotrans source files while writing ours** (the specs below
|
||
were written by a dedicated spec pass precisely so implementers never have to).
|
||
Each action now links to an implementation spec mapped onto our codebase.
|
||
|
||
| # | Action | From | Mode | Effort | Why now |
|
||
|---|---|---|---|---|---|
|
||
| 1 | **Dub-length fitting v2**: absorb inter-segment silence slack → if speedup ≤ 1.2× stretch audio only (pitch-preserving) → else split burden ~50/50 with per-segment video slowdown → regenerate subtitle timeline from actual dub durations → freeze last frame for drift | pyvideotrans `_rate.py` design | **Reimplement** ([spec](#spec-1--dub-length-fitting-v2)) | L | Our #280 onset-snapping is a band-aid; this is the algorithm that makes dubs *fit*. Highest user-visible quality win available |
|
||
| 2 | **MCP server v1**: mount the existing FastMCP server on the main FastAPI app + stdio shim + per-agent voice binding | voicebox `backend/mcp_server/`, `mcp_shim/` | **Port** ([spec](#spec-2--mcp-server-v1)) | M | We already have the FastMCP half (`backend/mcp_server.py`); voicebox shipped exactly the missing half. Agents-speak-in-your-voice is organic marketing |
|
||
| 3 | **Dictation refinement**: deterministic repetition-collapse pre-pass + optional local-LLM filler-word removal before paste | voicebox `services/refinement.py` | **Port** (adapt to our `llm_backend.py`) ([spec](#spec-3--dictation-refinement)) | M | Biggest dictation quality jump per line of code; WisprFlow's whole pitch. The deterministic pre-pass alone (fixes Whisper hallucination loops) is worth shipping first |
|
||
| 4 | **Per-segment clone refs**: cut the voice-clone reference for each dub segment from the separated vocal track at that segment's timestamps, instead of one 5–15 s ref per speaker | pyvideotrans per-line ref idea | **Reimplement** ([spec](#spec-4--per-segment-clone-refs)) | S–M | Prosody of each line matches its source line; cheap because Demucs stems + segment times already exist |
|
||
| 5 | **Second-pass ASR QC**: after dub generation, re-run ASR on the synthetic audio to regenerate exactly-timed subtitles (and flag segments whose recognized text drifts from the target text) | pyvideotrans pipeline stage | **Reimplement** ([spec](#spec-5--second-pass-asr-qc)) | M | Turns subtitle timing from "trusted math" into "measured truth"; doubles as an automatic dub-quality check |
|
||
| 6 | **MLX runtime pass**: route Whisper + at least one TTS engine through MLX on Apple Silicon via backend-aware model-repo mapping | voicebox `mlx_backend.py` pattern | **Port** pattern ([spec](#spec-6--mlx-runtime-pass)) | L | M-series Macs are a huge slice of local-AI users; 4–5× claimed speedup |
|
||
| 7 | **Crash-isolated ASR subprocess**: run native-crashy engines (whisper.cpp class) in a single-use child process, so a segfault never kills the backend | pyvideotrans subprocess pattern | **Reimplement** ([spec](#spec-7--crash-isolated-asr)) | M | Directly serves "first-run that actually works"; engine crashes become per-job failures. The sentiment pass confirmed whisper GPU-teardown crashes are endemic to the category |
|
||
| 8 | **Streaming polish kit**: sentence-aware chunker (abbreviation + multilingual punctuation, aggressive first-flush for TTFA) for `/ws/tts` + NLMS AEC so dictation works during playback | Patter `sentence_chunker.py`, `aec.py` | **Port** ([spec](#spec-8--streaming-polish-kit)) | M | Moves `/ws/tts` from C+ experiment toward production; AEC unlocks dictate-over-playback |
|
||
| 9 | **Quality rails**: (a) extend our existing install-docs gate into a daily inventory-vs-docs drift job with rolling-issue automation; (b) LLM-judge eval harness as a *non-gating* semantic tier above our deterministic probe judges | Patter `docs-feature-drift.yml`, `evals/` | **Port** ([spec](#spec-9--quality-rails)) | S + M | (a) we already have `validate-install-docs.py` — this is the second half; (b) gives dub translation quality what the probe judges give DSP correctness |
|
||
| 10 | **Practice adoptions** (no code): `PROJECT_STATUS.md`-style engine decision log; platform-gating flag audit in our engine registry (pre-empt voicebox's top bug class); add Chatterbox Turbo to the engine roster for paralinguistic tags | voicebox | n/a | S each | Cheap, compounding |
|
||
| 11 | **Fix the pyvideotrans bridge**: their `_omnivoice.py` integration is **verified broken** (speaks Gradio `/_clone_fn` to our FastAPI :3900 — hard-stops on connect). Either ship a small Gradio-compatible shim or upstream a REST/OpenAI-style integration PR to pyvideotrans | pyvideotrans | **Build/PR** ([spec](#spec-11--pyvideotrans-bridge)) | S–M | 17.9k★ project routing users to us — the bridge is down right now |
|
||
| 12 | **Weighted voice mixing**: blend preset/cloned voices by ratio as a voice-design primitive | Kokoro-FastAPI (Apache-2.0) | **Port** | S–M | Cheap, differentiating, and feeds the voice-design surface where we're already ahead |
|
||
| 13 | **Dynamic engine load/unload**: per-request model lifecycle (load on demand, unload on idle/VRAM pressure) — 🟡 **idle-unload shipped for both engine classes**: in-process default model via `model_manager.idle_worker`; subprocess-engine sidecars via `subprocess_backend.reap_idle_sidecars` (lock-guarded, `OMNIVOICE_SIDECAR_IDLE_TIMEOUT_S`, default 300 s). Cross-engine VRAM-pressure preemption (unload B to make room for A) still TODO | Speaches (MIT) | **Port** pattern | M | Multi-engine VRAM juggling is our chronic background pain; "Ollama for audio" solved it |
|
||
| 14 | **Positioning kit** (no code): ElevenLabs-dubbing-cost comparison page + incremental-re-dub demo GIF; Show HN; name-collision disambiguation; "WisprFlow alternative" docs entry point | sentiment research | n/a | S each | Grounded in what users actually search; details in [positioning moves](#positioning-moves) |
|
||
|
||
### Explicitly not recommended
|
||
|
||
- **Copying any pyvideotrans / KrillinAI / voice-pro code** — GPL-3.0 files would stay
|
||
GPL under their author's copyright inside our AGPL tree and break the commercial
|
||
dual-license. Ideas are fair game; code is not. Same for third-party **AGPL** code
|
||
(alltalk_tts).
|
||
- **Cloud TTS/ASR/translate channel breadth** (pyvideotrans's 33/22/25) — violates
|
||
the local-first constraint. Our breadth play is local engines only.
|
||
- **voicebox's CUDA-binary-swap bootstrap** — their own top bug category; our uv-venv
|
||
bootstrap is healthier. The deep dive below documents six concrete failure modes to
|
||
avoid; the lessons transfer to our sidecar pattern, the mechanism does not.
|
||
- **Patter's telephony stack** — different product. Only the audio/streaming/ops
|
||
pieces above are relevant.
|
||
- **Patter's opt-out telemetry default** — its *allowlist/bucketing design* informs
|
||
our opt-in bug reporter (see deep dive), but the consent default and the
|
||
third-party endpoint both violate our local-first constraint.
|
||
- **F5-TTS as an unlabeled engine** — MIT code, but CC-BY-NC weights; only with the
|
||
same prominent license gate we use for OpenRAIL engines.
|
||
|
||
---
|
||
|
||
## Deep dives
|
||
|
||
*Source-level briefs from the second research pass. voicebox and Patter are MIT —
|
||
briefs may quote and porting means copying with attribution. The pyvideotrans
|
||
section is a **clean-room functional spec**: written by a dedicated spec pass that
|
||
read the GPL source so implementers never have to; it contains no code, only
|
||
behavior and constants. File paths are repo-relative, with approximate line ranges,
|
||
for verification only.*
|
||
|
||
### voicebox (MIT — portable)
|
||
|
||
#### 1. Chunked long-form TTS (`backend/utils/chunked_tts.py`, 299 LOC)
|
||
|
||
Engine-agnostic wrapper around any `generate()`. Text ≤ `max_chunk_chars` (default
|
||
**800**, per-request overridable) takes a zero-overhead single-shot path. Longer text
|
||
splits greedily left-to-right with descending boundary priority: sentence end
|
||
(`[.!?]` + CJK equivalents, skipping a 25-entry abbreviation set and decimals) →
|
||
clause boundary (`;:,—`) → whitespace → safe hard cut. Paralinguistic tags
|
||
(`[laugh]`) are atomic — a regex guards every split candidate. Per-chunk seed is
|
||
`seed + i` (decorrelates RNG artifacts, keeps (text, seed) reproducible). Optional
|
||
per-chunk `trim_fn` for engines that hallucinate trailing noise. Joining is a linear
|
||
crossfade, default **50 ms** (0 = hard cut), overlap clamped to the shorter side.
|
||
Known latent bug: sample rate is taken from the *first* chunk; later mismatches are
|
||
silently ignored. No per-chunk failure recovery — any chunk exception fails the whole
|
||
generation. Deps: numpy only.
|
||
|
||
**Porting:** new `backend/utils/` module invoked from our `tts_backend.py` synthesis
|
||
orchestration — it only needs an awaitable `generate()` returning `(ndarray, sr)`,
|
||
which our `TTSBackend` ABC already provides. Make the abbreviation set and
|
||
sentence-end regexes language-aware for our 646-language scope; wire
|
||
`max_chunk_chars`/`crossfade_ms` into the request models. Not a fit for streaming
|
||
`/ws/tts` as-is (batch concat, not incremental emit), but `split_text_into_chunks`
|
||
alone is reusable there. **Effort: S.**
|
||
|
||
#### 2. Dictation LLM refinement (`backend/services/refinement.py`, 295 LOC + capture plumbing)
|
||
|
||
Local Qwen3 **0.6B/1.7B/4B** (mlx-community 4-bit on Apple Silicon, upstream
|
||
elsewhere), default 0.6B, `temperature=0.2`, thinking off. Prompts are assembled from
|
||
three boolean toggles (`smart_cleanup`, `self_correction`, `preserve_technical`, all
|
||
default on) — no raw prompt editor. The base instruction frames the model as "a text
|
||
filter, not an assistant" with explicit anti-instruction-following rules. Seven
|
||
few-shot examples are passed as **structured chat turns**, not inline prompt text —
|
||
small models pattern-match and echo inline examples; ordering is recency-weighted
|
||
with hardest rules last.
|
||
|
||
The underrated half is a **deterministic pre-pass**, `collapse_repetitive_artifacts()`:
|
||
strips Whisper hallucination loops *before* the LLM — word-level (token repeated ≥6×)
|
||
and character-level (any 2–60-char unit repeating ≥6×, catches CJK loops with no
|
||
spaces). Rhetorical repeats below 6 survive. This alone fixes the classic Whisper
|
||
"thanks for watching" loop and works with no LLM configured.
|
||
|
||
Flow: record → full-audio STT (raw transcript persisted) → if auto-refine, one LLM
|
||
round-trip → refined-or-raw text is what auto-pastes. Refinement is re-runnable with
|
||
different flags. A readiness endpoint checks *on-disk* model presence so the global
|
||
hotkey never hangs on an un-downloaded model; refine errors leave the raw transcript
|
||
standing.
|
||
|
||
**Porting:** maps almost 1:1 onto our dictation stack. Pre-pass + prompt builder →
|
||
new `backend/services/refinement.py`; the LLM call goes through our existing
|
||
`llm_backend.py` OpenAI-compat adapter (Ollama/LM Studio) instead of an in-process
|
||
Qwen — simpler: the few-shot examples become standard `messages` pairs. Hook after
|
||
the **final** transcript in `capture_ws.py`, never on partials. Readiness becomes
|
||
"is an LLM endpoint configured/reachable". Needs Settings toggles + i18n keys.
|
||
**Effort: M** (service S; UX, persistence, ws-protocol addition make it M).
|
||
|
||
#### 3. MCP server + stdio shim (`backend/mcp_server/` ~650 LOC, shim ~200 LOC)
|
||
|
||
FastMCP **mounted on the main FastAPI app** at `/mcp` (Streamable HTTP); FastMCP's
|
||
session manager must run inside the ASGI lifespan — they stack their existing
|
||
startup/shutdown with FastMCP's via `AsyncExitStack`. Tools (dotted names):
|
||
`voicebox.speak(text, profile, engine, personality, language)` → async
|
||
`{generation_id, poll_url}`; `voicebox.transcribe(audio_base64 | audio_path)` —
|
||
`audio_path` **restricted to loopback callers** (so a 0.0.0.0-bound server isn't an
|
||
arbitrary-file-read primitive), 200 MB cap; `voicebox.list_captures`,
|
||
`voicebox.list_profiles`.
|
||
|
||
**Per-agent voice binding** — the headline: every MCP client sends an
|
||
`X-Voicebox-Client-Id` header (from its MCP config, or forwarded by the shim from an
|
||
env var). Middleware copies it into a `ContextVar` so tool handlers read it without
|
||
plumbing, and stamps `last_seen_at`. A `mcp_client_bindings` table holds per-client
|
||
`{label, profile_id, default_engine, default_personality}` — "Claude Code speaks in
|
||
Morgan, Cursor in Scarlett." Resolution precedence: explicit tool arg → per-client
|
||
binding → global default → helpful error. Bindings managed from Settings over plain
|
||
REST. The stdio shim is a ~200-line stdio↔HTTP proxy (waits for `/health`, relays
|
||
JSON-RPC, captures/replays the MCP session id, maps HTTP errors to JSON-RPC errors);
|
||
only dep is httpx.
|
||
|
||
**Porting:** we already have the FastMCP half (`backend/mcp_server.py`, stdio+SSE,
|
||
4 tools) — this is precisely the missing half. Order: (1) mount + lifespan
|
||
composition on our main app (wrap our startup hooks, don't replace); (2) client-id
|
||
middleware + bindings table (alembic migration per the backward-compat constraint) +
|
||
resolve chain over our voice profiles; (3) shim nearly verbatim. The loopback gate on
|
||
file-path tools is a security pattern worth copying anywhere. **Effort: M.**
|
||
|
||
#### 4. MLX runtime (`backend/backends/mlx_backend.py`, 367 LOC + factory routing)
|
||
|
||
Not per-engine plugins — a **fork inside the lazy backend factory**: only Qwen TTS,
|
||
Whisper STT, and the Qwen3 LLM branch on `get_backend_type()`; everything else is
|
||
torch-only everywhere. Detection: Darwin+arm64, then `import mlx.core` inside
|
||
try/except catching `ImportError, OSError, RuntimeError` — in a PyInstaller bundle
|
||
the native dylib/metallib can fail to load even when the package imports. The fork
|
||
extends to **model selection**: the same `model_size` key resolves to
|
||
`mlx-community/...` weights on MLX (~3× smaller downloads) vs upstream weights on
|
||
torch. Backends are duck-typed Protocols; inference runs via `asyncio.to_thread`;
|
||
clone failure degrades to generation without the voice prompt. Claimed 4–5× speedup
|
||
is README-grade (no benchmark file in the repo).
|
||
|
||
**Porting:** we already have `MLXAudioBackend` in the registry, so the structure
|
||
exists. Worth stealing: (a) backend-aware ModelConfig repo mapping — one engine key,
|
||
platform-resolved artifact; (b) the robust `import mlx.core` probe (we will hit the
|
||
same PyInstaller failure in bundled builds); (c) MLX Whisper for the dictation path —
|
||
we already have `MLXWhisperBackend` in `asr_backend.py`, the gap is routing dictation
|
||
through it. Cross-platform rule satisfied: MLX is implementation-level, default
|
||
behavior identical. **Effort: S–M.**
|
||
|
||
#### 5. Stories multi-track editor (shape only; `backend/services/stories.py`, 966 LOC)
|
||
|
||
Two tables: `Story {id, name, description, timestamps}` and `StoryItem {id, story_id,
|
||
generation_id FK, version_id FK nullable, start_time_ms, track, trim_start_ms,
|
||
trim_end_ms, volume, created_at}`. The key design move: **a clip references a
|
||
Generation, never copies audio** — trims are non-destructive offsets, and
|
||
`version_id` pins a clip to a specific regeneration take while sibling clips can use
|
||
different takes. Split = two items sharing one `generation_id` with complementary
|
||
trims (row-locked against double-click races). Export sums clips into a float32
|
||
buffer at sample offsets (overlaps mix additively), then peak-normalizes if needed.
|
||
|
||
**Porting:** the 9-column schema is the valuable part — a full multi-track NLE over
|
||
our existing generation history (additive alembic migration, backward-compatible).
|
||
The frontend timeline is the actual cost. **Effort: L full feature, S for data model
|
||
+ export mixer alone.**
|
||
|
||
#### 6. CUDA binary swap bootstrap — study-only failure autopsy (`backend/services/cuda.py`, 422 LOC + Rust launch logic)
|
||
|
||
Mechanism: CPU PyInstaller sidecar ships in the installer; a CUDA variant downloads
|
||
as two independently versioned tarballs (server core, versioned with the app; CUDA
|
||
libs ~4 GB, keyed on a toolkit string). At every launch the Rust shell runs the
|
||
downloaded binary with `--version` and compares; mismatch or error → silent CPU
|
||
fallback. Six failure modes documented from their tracker, **all to avoid**:
|
||
|
||
1. Staleness detection requires *spawning the possibly-broken binary* — corrupt
|
||
onedir → `--version` fails → silently CPU, never repaired this launch. (They also
|
||
paid a 30 s torch import on every version check until adding a fast path.)
|
||
2. One-launch GPU lag after every app update ("update disabled my GPU" reports).
|
||
3. Extract-over-old-dir without wiping — orphaned files from previous layouts shadow
|
||
the new binary while `--version` still passes.
|
||
4. Auto-update vs manual download raced on the same temp file (fixed late with a
|
||
lock that now silently *skips* user-initiated downloads).
|
||
5. Manifest-vs-disk drift: libs staleness reads a JSON manifest that survives failed
|
||
or hand-deleted extractions; toolkit bumps force full 4 GB re-downloads.
|
||
6. Every failure path degrades to CPU with stdout-only logging — users discover it
|
||
as "the app got slow", not as an error.
|
||
|
||
**Lessons for our sidecars** (`subprocess_backend.py` + engine bootstraps): write the
|
||
version manifest atomically *after* successful extract — never interrogate a binary
|
||
to learn its version; extract to temp dir + atomic rename; make GPU/CPU fallback
|
||
loudly visible in the UI; the split-archive idea (app-versioned core vs
|
||
toolkit-versioned libs) genuinely fixed their 4 GB re-download complaint and is the
|
||
one piece worth keeping. **Effort: n/a (declined) — this list is the deliverable.**
|
||
|
||
### pyvideotrans (GPL-3.0 — clean-room functional specs, no code)
|
||
|
||
#### 1. Dub-length fitting (`videotrans/task/_rate.py`, ~lines 288–877)
|
||
|
||
The algorithm that makes dubs fit their slots. All constants verified against source:
|
||
|
||
| Parameter | Value | Meaning |
|
||
|---|---|---|
|
||
| Audio-only threshold (both-mode) | **1.2** | Required speedup ≤ 1.2× → audio absorbs everything, video untouched |
|
||
| Burden split beyond 1.2× | **50/50** | Joint target = slot + (dub − slot)/2, applied to both audio stretch and video slowdown; caps deliberately ignored in this branch |
|
||
| Max audio speed (audio-only mode) | 100 (default setting) | Effectively unlimited; past it, slot is overrun instead |
|
||
| Max video slowdown (video-only mode) | 10× | Past it, video target clamps to slot × 10, dub truncated later |
|
||
| Video retime bias | +0.005 | Slowdown factor padded to compensate frame-rounding undershoot |
|
||
| Retime no-op epsilon | 0.001 | Near-1.0 factors skip the filter |
|
||
| Min valid video clip | 1024 bytes | Smaller output = failed; retried without retiming, then dropped |
|
||
| Audio stretch clamp | 0.2–50.0 (rubberband); ≤2.0-step chained tempo filter as fallback | Pitch-preserving when rubberband present |
|
||
| Working audio format | 48 kHz / 2ch / 16-bit PCM | All silence + dub segments normalized |
|
||
| Dub-clip silence trim | threshold = clip dBFS − 20; min silence 100 ms; keep 80 ms head / 200 ms tail | Applied to every synthesized clip, default on |
|
||
| First-segment snap | < 100 ms start → cut from 0 | Avoids sub-frame clips when video slowdown is on |
|
||
| Video encode for cuts | x264 CRF 20, veryfast, **GOP=1**, yuv420p | Every frame an I-frame → clean concat boundaries |
|
||
| End-of-video freeze | last-frame clone pad = audio − video duration | Applied at final mux |
|
||
|
||
**Decision tree.** *Pre-pass (slack absorption):* every segment's slot end is
|
||
rewritten to the next segment's start — the silent gap after each line is donated to
|
||
that line; the last line's end becomes total media duration. Failed/missing dubs get
|
||
a silent placeholder exactly one slot long so the pipeline never stalls. *Per-line:*
|
||
dub ≤ slot → untouched. Otherwise in the combined mode: ratio ≤ 1.2 → audio-only
|
||
compress to exactly the slot; ratio > 1.2 → both sides meet at slot + overflow/2.
|
||
*Execution:* audio stretches run on a CPU process pool; a stretch is skipped if the
|
||
target exceeds current length (silence padding covers that). Each line becomes an
|
||
independent cut of the original video (seek, window, retime, hard duration limit);
|
||
a pre-roll clip covers 0 → first line; failed clips are re-cut without retiming, then
|
||
dropped; clips concat-copied in order. *Timeline regeneration:* the **measured**
|
||
duration of each generated clip (not the requested target) becomes the line's final
|
||
slot; a running cursor rewrites every subtitle to start at the cursor and end at
|
||
cursor + slot — subtitles track actual dub placement exactly; overruns are
|
||
hard-truncated when video slowdown is active, appended whole otherwise; short dubs
|
||
get tail silence. Overlapping segments never reach this stage (an ASR post-fix clamps
|
||
each line's end to the next line's start upstream). A TTS-only variant forces fit to
|
||
the slot and restores the *original* timeline on output.
|
||
|
||
#### 2. Per-subtitle-line clone references (`trans_create.py` ~882–1009)
|
||
|
||
Reference source priority: separated vocal stem → fresh mono 44.1 kHz extraction from
|
||
the original media. Cut timestamps are the line's **original** SRT start/end (before
|
||
gap-absorption rewriting). One 16 kHz/16-bit wav per line, indexed by line number;
|
||
the source-language subtitle text at the same index rides along as the ref
|
||
transcript. Thread pool of min(8, line count, CPU count); all cuts complete before
|
||
TTS starts. **Deliberately no min/max duration and no neighbor-borrowing** — a 400 ms
|
||
subtitle yields a 400 ms reference and quality degradation is accepted; engines that
|
||
can't handle it fail that line only (a dub job fails only when *zero* lines succeed).
|
||
Dub-file cache key: MD5 of text + role + rate + volume + pitch + channel id — lines
|
||
whose output already exists are skipped entirely.
|
||
|
||
#### 3. Second-pass ASR QC (`trans_create.py` ~419–482)
|
||
|
||
Runs after alignment, before final mux, opt-in, only when source ≠ target language
|
||
and the subtitle-embed mode doesn't require matched line counts. The assembled dub
|
||
track is downsampled to 16 kHz mono and re-recognized with **deliberately halved VAD
|
||
windows** (min speech halved with 500 ms floor; max speech halved; min silence halved,
|
||
clamped 50–1000 ms) so recognized lines come out short; short-line merging is
|
||
disabled. The resulting SRT **wholesale replaces** the target-language subtitle file —
|
||
recognized text wins unconditionally; the design accepts ASR drift in exchange for
|
||
frame-accurate display timing (dub audio is already rendered and untouched). Every
|
||
failure is a silent skip leaving the align-stage subtitles in place, so this stage
|
||
can never break a job. Engine falls back to faster-whisper large-v3-turbo when the
|
||
user's ASR channel can't do the target language.
|
||
|
||
*Our spec deviates here (see Spec 5): we want drift flagged, not silently accepted.*
|
||
|
||
#### 4. Translation cache + line-count validation (`videotrans/translator/_base.py` ~46–175)
|
||
|
||
Cache: one text file **per batch** (5 lines plain mode / 20 lines or whole-file SRT
|
||
mode), keyed by MD5 of channel id + API URL + srt-mode flag + model + source lang +
|
||
target lang + serialized batch content — any config or content change is the
|
||
invalidation mechanism (no TTL). Empty results are never cached; an all-empty batch
|
||
set raises. Line-count defense, plain mode: response split on newlines, extra lines
|
||
discarded, short responses padded with empty strings, then mapped 1:1 by index. SRT
|
||
mode: on count mismatch (LLM merged/split lines), recovery is **exact
|
||
timestamp-string matching** — translated cues map back by their time-range string;
|
||
unmatched source cues get empty text rather than shifting subsequent lines. Source
|
||
timeline is always authoritative. A final cosmetic pass strips the leading/trailing
|
||
ellipsis runs LLMs tend to add.
|
||
|
||
#### 5. Crash-isolated engine subprocesses (`videotrans/configure/base.py` ~170–276)
|
||
|
||
Every native-crashy stage (ASR, separation, diarization, VAD, retiming, stretch) runs
|
||
in a process pool with **spawn** start method and **one task per child** — every job
|
||
gets a fresh interpreter, memory fully returned, a segfault can't poison a warm
|
||
worker, and "restart" semantics are free (the pool replaces dead workers
|
||
automatically). CPU pool size: manual cap → min(cap, 8, CPUs); else
|
||
clamp(available-RAM-GB / 4, 2, 8). GPU pool defaults to 1 (strict serialization);
|
||
multi-GPU opt-in scales to min(GPU count, 8, CPUs). Progress transport: the child
|
||
overwrites a single-JSON log file; a parent daemon thread polls mtime every 1 s and
|
||
forwards to the UI bus; monitor gives up after ~1 h of no change. Child contract:
|
||
return (result, error); falsy result or non-empty error → typed task error. Native
|
||
death → the pool's broken-pool exception, decorated with model name + GPU index
|
||
before surfacing. GPU selection: first card with > 24 GB free VRAM, else most-free;
|
||
CUDA re-verified at submit time, unavailable → kwargs rewritten to CPU.
|
||
|
||
#### 6. Batch wave control (`videotrans/task/mult_video.py`, `job.py`)
|
||
|
||
Nine FIFO stage queues (prepare → recognize → diarize → translate → dub → align →
|
||
second-pass ASR → assemble → done), each with dedicated workers. GPU-heavy stages get
|
||
1 thread by default (2 with 2–3 GPUs in multi-GPU mode, 4 with ≥ 4); network-bound or
|
||
globally stateful stages (translate, dub, align, second-pass, done) are always 1.
|
||
With waves off, all videos enter the conveyor at once — video A can be translating
|
||
while B is in ASR. With `batch_nums` > 0, the file list is chunked; a dispatcher
|
||
busy-waits (1 s ticks) until every task in a chunk finishes or is stopped before
|
||
releasing the next — bounding peak temp-disk and VRAM at the cost of inter-wave
|
||
overlap. Inside the dub stage, lines fan out on a thread pool (`dubbing_thread`,
|
||
default 1); per-line errors are collected and a job fails only if zero lines succeed.
|
||
Stage workers convert any exception into a stage-prefixed UI error and mark the task
|
||
ended, so a wave can never hang on a failed member.
|
||
|
||
#### 7. Fact-check: the pyvideotrans ↔ VoiceStudio integration
|
||
|
||
`videotrans/tts/_omnivoice.py` (~12–77) speaks **Gradio, not REST**: it builds a
|
||
`gradio_client.Client` against a user-pasted URL and calls the named endpoint
|
||
**`/_clone_fn`** with text, a natural-language language name (~35 ISO codes mapped),
|
||
a per-line reference wav + transcript, and a knob set (steps/guidance/denoise/
|
||
duration/post-process flags) that matches a 12-input Gradio clone function from an
|
||
older or forked VoiceStudio build. It expects a filesystem path to a wav back.
|
||
|
||
**Verdict: broken against current VoiceStudio.** Our backend is FastAPI on
|
||
:3900 (`backend/main.py`) with REST routers; we expose no Gradio app and no
|
||
`/_clone_fn` (our only Gradio surface is the optional SoniTranslate *subprocess* on
|
||
:7860 — a different application). A gradio client pointed at :3900 fails fetching
|
||
the Gradio config, which lands in pyvideotrans's fatal "Could not fetch config"
|
||
branch — users get an immediate hard stop. Their tracker already has an open bug
|
||
against this integration. Remedies in Spec 11.
|
||
|
||
### Patter (MIT — portable)
|
||
|
||
#### 1. Sentence chunker (`libraries/python/getpatter/services/sentence_chunker.py`, 565 LOC)
|
||
|
||
Streaming sentence segmentation for low-TTFA TTS. Accumulates tokens; `push(token)`
|
||
returns zero or more complete sentences. Boundary detection is regex
|
||
marker-replacement: protect non-terminal periods (honorifics for EN/IT/ES/DE/FR/PT,
|
||
website TLDs, decimals, ellipses, initials, acronym chains, company suffixes), mark
|
||
real terminators, split. Terminator tables cover Latin + CJK + 8 non-Latin scripts
|
||
(Devanagari, Arabic, Armenian, Ethiopic, Khmer, Burmese, Tibetan). Three emission
|
||
paths: standard (≥ min length and > 1 sentence → emit all but the buffered tail);
|
||
**short flush** for instant single-sentence replies (guards: one terminator, ≥ 1 word,
|
||
no preceding digit, no ALL-CAPS acronym tail, no honorific tail); **aggressive
|
||
first-clause flush** (opt-in, first clause of each turn only) on soft punctuation
|
||
`, — –` at ≥ 40 chars — claimed 200–500 ms TTFA savings — with seven guards
|
||
(decimals, currency within 8 chars, unbalanced brackets/quotes, ellipsis, sub-token
|
||
ambiguity) and a hard disable for Italian (comma = decimal separator). Constants:
|
||
`min_sentence_len=20`, `aggressive_first_min_len=40` (comment: below ~40 chars hurts
|
||
prosody; ElevenLabs buffers ~120 internally). `flush()` emits the remainder;
|
||
`reset()` discards (barge-in). Deps: stdlib `re` only. A TS mirror + golden parity
|
||
scenarios ship alongside.
|
||
|
||
**Porting:** feeds `/ws/tts` (`tts_stream.py`) so text synthesizes sentence-by-
|
||
sentence ahead of the PCM stream; aggressive first-flush directly serves the
|
||
< 100 ms TTFA goal. Distinct from `subtitle_segmenter.py` (offline length-balancing)
|
||
but the terminator/honorific tables could become shared constants. Port the parity
|
||
scenarios as pytest fixtures. **Effort: S.**
|
||
|
||
#### 2. NLMS acoustic echo canceller (`libraries/python/getpatter/audio/aec.py`, 333 LOC)
|
||
|
||
Time-domain sample-by-sample NLMS adaptive filter with leakage + frame-wise Geigel
|
||
double-talk detector. Far-end (TTS) PCM feeds a ring buffer; near-end (mic) frames
|
||
get `e = near − w·x` as output, with the weight update frozen during double-talk
|
||
(`max|near| > 0.6 · max|far|`) and when the far reference is near-silent (≤ −60 dBFS
|
||
— raised from a smaller epsilon after a weight-blowup bug). Two-phase step schedule:
|
||
mu 0.5 for the first 0.5 s, then 0.1. Pass-through guards: far buffer not primed,
|
||
and a **250 ms staleness window** — no recent far-end push → pass through rather
|
||
than convolve against a frozen reference (was producing an audible buzz during
|
||
silence). Defaults: **512 taps** (= 32 ms @ 16 kHz; 2048 tested → 8–12 s convergence,
|
||
rejected), leakage 0.9999, far buffer 0.5 s. I/O: int16 mono PCM, **8 or 16 kHz
|
||
only**, not thread-safe (one instance per session). Self-declared limitations: no
|
||
frequency-domain partitioning, no residual-echo suppressor, no delay estimation —
|
||
docstring recommends libwebrtc AEC3 for production-grade. Deps: numpy.
|
||
|
||
**Porting:** sits ahead of `/ws/transcribe` in `capture_ws.py` — `push_far_end()`
|
||
fed from every chunk `/ws/tts` ships, `process_near_end()` on mic frames. The real
|
||
work: our TTS streams 24 kHz while the canceller accepts 8/16 kHz (resample the
|
||
far-end to the capture rate, or relax the check and scale taps); the staleness clock
|
||
must track *playout* time, not send time (desktop speaker latency ≠ carrier RTT);
|
||
rho likely needs tuning for loud desktop-speaker bleed. Pure numpy → identical
|
||
default behavior on all three platforms. **Effort: M** (the port is an hour;
|
||
sample-rate plumbing, delay alignment, and tuning are the work).
|
||
|
||
#### 3. LLM-judge eval harness (`libraries/python/getpatter/evals/`, 1,636 LOC)
|
||
|
||
YAML/JSON suites of `EvalCase {name, turns, expected_behavior, rubric, tags}`;
|
||
per-case error containment (a mid-case exception keeps the partial transcript and
|
||
still judges it; a judge failure records score 0 + reasoning instead of aborting the
|
||
suite). Judge: chat-completions with JSON response format, temperature 0, pass
|
||
threshold 0.7 — with two hardening details worth copying verbatim: tolerant JSON
|
||
parsing (strips code fences; invalid JSON → fail with reasoning), and **the verdict
|
||
is recomputed locally** (`passed = score >= threshold`) because trusting the model's
|
||
self-reported `passed` once let a hallucinated pass through at score 0.2. A
|
||
deterministic chainable assertions layer (`expect(...).tool_called(...).judge(...)`)
|
||
raises plain `AssertionError`s so pytest reports work. CLI exits non-zero unless all
|
||
cases pass — CI-gateable. The judge backend is injectable (any object with
|
||
`judge(prompt)`).
|
||
|
||
**Porting:** complements our deterministic stack — probe judges (`tests/probe/judges/`)
|
||
score DSP correctness, `omnivoice/eval/` scores WER/MOS/speaker-sim; Patter's harness
|
||
adds the missing *semantic* tier (dub translation naturalness, dictation-correction
|
||
quality). Keep our "no LLM on the verdict path" rule for CI gates: run LLM-judge
|
||
suites as a separate **non-gating** job. Swap the judge backend for our
|
||
`llm_backend.py` (local model, keeps local-first). Port `case.py` + `runner.py` +
|
||
`assertions.py` nearly verbatim; drop `session.py` (telephony) in favor of our probe
|
||
Actor. **Effort: M.**
|
||
|
||
#### 4. Docs-drift CI (`.github/workflows/docs-feature-drift.yml` 112 LOC + checker 211 LOC)
|
||
|
||
Daily cron (03:00 UTC) + manual dispatch; `contents: read, issues: write`. Three-way
|
||
cross-reference: canonical feature inventory (theirs is an xlsx in a private sibling
|
||
repo — their weakest design point, soft-failing when the token is missing) × docs
|
||
filename stems × regex-parsed public SDK exports. Three drift buckets; only
|
||
inventory↔docs mismatches gate (export drift is report-only). The best part is the
|
||
**issue automation**: on failure, look up the open `docs-drift`-labeled issue and
|
||
**update its body in place** (single rolling issue, no spam); create it if absent;
|
||
on success, comment "drift resolved" and auto-close. Idempotent and self-healing.
|
||
|
||
**Porting:** we already gate `docs/install/*.md` via `scripts/validate-install-docs.py`
|
||
in ci.yml — this is the second half. Replace the private-repo xlsx with an
|
||
**in-repo** canonical inventory (a checked-in `features.yaml`, or generated from the
|
||
engine registry + FastAPI route table); adopt the rolling-issue pattern verbatim.
|
||
Note this auto-filed issue is maintainer-facing CI, distinct from the user-facing
|
||
opt-in bug reporter. **Effort: S** (workflow is copy-adapt; defining the inventory is
|
||
the only design decision).
|
||
|
||
#### 5. Consent-bounded telemetry (reference design only; `getpatter/telemetry/`, 1,235 LOC)
|
||
|
||
**Not adopting telemetry** — Patter's default is opt-OUT with a third-party endpoint,
|
||
both of which violate our local-first constraint. What transfers to our opt-in
|
||
prefilled-URL GitHub bug reporter (CLAUDE.md Capability 2):
|
||
|
||
1. **Two-layer key+value allowlist** before an event is built: unknown keys dropped,
|
||
values checked against closed enums with off-list values coerced to `"other"` —
|
||
making a leaked custom name "structurally impossible to emit, even from a buggy
|
||
caller". Stronger than regex-scrubbing after the fact; directly implements our
|
||
planned token/key/home-path exclusions for the issue body.
|
||
2. **Model-name sanitization**: anything with separators, whitespace, or > 40 chars
|
||
(fine-tune IDs, self-hosted paths) collapses to `"{vendor}-other"`; date suffixes
|
||
stripped; final shape re-checked by regex. Apply the same to user voice-profile
|
||
and engine names in bug reports.
|
||
3. **Coarse buckets**: counts → `{0, 1, 2_3, 4_6, 7_12, 13_plus}`, versions →
|
||
major.minor only, OS → family only, arch → `x86_64/arm64/other` — explicitly
|
||
anti-fingerprinting.
|
||
4. **Precedence-ordered consent resolver**, inverted to opt-IN for us (default OFF;
|
||
Settings toggle → marker file; `DO_NOT_TRACK` honored as an absolute OFF), with
|
||
the invariant "checking consent never writes to the filesystem".
|
||
|
||
**Effort: S** for the allowlist/sanitizer port (~250 LOC of pure stdlib functions);
|
||
the reporter UI around it is separate work.
|
||
|
||
---
|
||
|
||
## Implementation specs
|
||
|
||
*Each ranked action mapped onto this codebase: integration points, shapes, and a
|
||
test plan. File references verified against main as of 2026-06-11. These are
|
||
work-item-grade specs, not designs — the implementer still owns the details.*
|
||
|
||
### Spec 1 — Dub-length fitting v2
|
||
|
||
**Goal:** replace "trust the math + onset snap" with the measured-fit algorithm from
|
||
the functional spec above (reimplemented; do not open pyvideotrans source).
|
||
|
||
- **Files:** new `backend/services/length_fit.py` (the decision tree); integrate in
|
||
the dub orchestration in `backend/services/dub_pipeline.py` after TTS, before mux;
|
||
`backend/services/speech_rate.py` keeps its role as the *pre-generation* estimator
|
||
(LLM trim/expand to fit the slot) — length-fit is the *post-generation* enforcer;
|
||
`backend/services/onset_align.py` stays (it solves start alignment, not duration);
|
||
video cuts/retime/concat via `backend/services/ffmpeg_utils.py` (respect the
|
||
ffmpeg semaphore and `register_proc` for abortability).
|
||
- **Design deltas vs pyvideotrans:** keep their 1.2× audio-only threshold and 50/50
|
||
burden split as defaults but expose both in dub settings; integrate with our
|
||
incremental re-dub — slack absorption must be computed over the *full* segment
|
||
list even when only stale segments regenerate (`backend/services/incremental.py`
|
||
fingerprints must include the two new knobs in `_GEN_INPUT_FIELDS`, since they
|
||
affect output); time-stretch with pitch preservation (rubberband when available,
|
||
chained ffmpeg tempo fallback) under the existing GPU/CPU job queue.
|
||
- **Subtitle regeneration:** rewrite segment times from measured clip durations with
|
||
a running cursor (as specced) before `dub_export.py` renders SRT/burn-in.
|
||
- **Tests:** probe spec `dub_export.probe.yaml` + `tests/probe/judges/dubbing.py`
|
||
already gate segment duration ratio [0.5–1.6×] — add a judge check that final
|
||
audio fits final video ±1 frame and that regenerated SRT cue times equal measured
|
||
placements; unit tests for the decision tree at the 1.2 boundary, zero-length
|
||
slots, last-segment, and overlap-clamped inputs.
|
||
- **Effort: L.** Land the audio-only path first (pure win, no video retime), video
|
||
slowdown second.
|
||
|
||
### Spec 2 — MCP server v1
|
||
|
||
**Goal:** ship the missing half of our MCP story: mounted endpoint + per-agent voice
|
||
binding + stdio shim (ported from voicebox, MIT attribution).
|
||
|
||
- **Files:** `backend/mcp_server.py` (existing FastMCP: keep tools, add
|
||
`transcribe`); mount on the main app in `backend/main.py` with lifespan
|
||
composition (wrap existing startup hooks via AsyncExitStack — do not replace);
|
||
new middleware + `ContextVar` for `X-VoiceStudio-Client-Id`; new alembic migration
|
||
for `mcp_client_bindings {client_id, label, profile_id, default_engine,
|
||
last_seen_at}` (additive — satisfies the backward-compat constraint); REST CRUD
|
||
router `backend/api/routers/mcp_bindings.py`; new `backend/mcp_shim/` (port
|
||
nearly verbatim — httpx-only stdio↔HTTP proxy); Settings UI section in
|
||
`frontend/src/pages/Settings.jsx`; update `docs/mcp.json` + `docs/` MCP doc.
|
||
- **Resolution chain:** explicit tool arg → client binding → global default profile
|
||
→ helpful error. Copy voicebox's loopback-only gate for any file-path-accepting
|
||
tool argument.
|
||
- **Tests:** pytest for the resolve chain + middleware; probe spec addition driving
|
||
`speak`/`transcribe` over the mounted endpoint; shim smoke test against a live
|
||
backend.
|
||
- **Effort: M** (S shim, M mount + bindings + Settings).
|
||
|
||
### Spec 3 — Dictation refinement
|
||
|
||
**Goal:** refined-by-default-quality dictation: deterministic artifact collapse for
|
||
everyone, LLM filler-word removal for users with a local LLM configured.
|
||
|
||
- **Phase 1 (S, no LLM):** port `collapse_repetitive_artifacts()` (word-level ≥6
|
||
repeats; char-level 2–60-char units ≥6, catches no-space scripts) into a new
|
||
`backend/services/refinement.py`; apply to final transcripts in
|
||
`backend/api/routers/capture_ws.py` after `_transcribe_buffer`, never to partials.
|
||
Port voicebox's test corpus pattern.
|
||
- **Phase 2 (M):** prompt builder with the three toggles (`smart_cleanup`,
|
||
`self_correction`, `preserve_technical`), few-shot examples as structured
|
||
`messages` pairs, executed through `backend/services/llm_backend.py`
|
||
(`get_active_llm_backend().chat(...)`, timeout-bounded). Readiness = active LLM
|
||
backend reachable; on any failure the raw transcript stands. Persist both raw and
|
||
refined in the `transcriptions` table (`backend/core/db.py`, additive column via
|
||
alembic) so history supports re-refine. WS protocol: `{type:"final", text,
|
||
refined_text?, ...}` — frontend pastes `refined_text ?? text`.
|
||
- **Settings:** auto-refine toggle (default ON only when an LLM backend is active —
|
||
cross-platform default behavior stays identical: no LLM → identical pass-through
|
||
everywhere) + the three flag toggles; i18n keys for all labels.
|
||
- **Tests:** unit tests for the collapse pass (incl. CJK-free fixtures using Latin
|
||
repetition patterns); contract test that a dead LLM endpoint yields the raw
|
||
transcript within timeout.
|
||
|
||
### Spec 4 — Per-segment clone refs
|
||
|
||
**Goal:** per-line prosody matching — cut each dub segment's clone reference from
|
||
the separated vocal track at that segment's own timestamps.
|
||
|
||
- **Files:** `backend/services/speaker_clone.py` — add
|
||
`extract_segment_refs(vocals_path, segments, out_dir)` alongside the existing
|
||
per-speaker `extract_speaker_clones()`; dub pipeline passes the per-segment ref to
|
||
the TTS call when the engine supports reference audio.
|
||
- **Design deltas vs pyvideotrans:** unlike their no-floor policy, keep a quality
|
||
floor — segment shorter than `MIN_REF_DURATION_S` falls back to the existing
|
||
per-speaker 5–15 s reference (we already have it; they don't). Use original
|
||
(pre-slack-absorption) segment times. Cut with the thread pool pattern already
|
||
used in the pipeline; ride the source-language text along as ref transcript.
|
||
- **Mode:** per-segment refs default ON with per-speaker fallback; expose a dub
|
||
setting to force per-speaker (long-form consistency sometimes beats per-line
|
||
prosody). Add the mode to `incremental.py` `_GEN_INPUT_FIELDS`.
|
||
- **Tests:** unit test slicing math + fallback threshold; probe `voice_clone` /
|
||
`dub_export` flows with a two-speaker fixture asserting each segment got a ref
|
||
file of its own span (or the fallback).
|
||
- **Effort: S–M.**
|
||
|
||
### Spec 5 — Second-pass ASR QC
|
||
|
||
**Goal:** measured subtitle truth + automatic dub-quality flagging.
|
||
|
||
- **Files:** new stage in `backend/services/dub_pipeline.py` after assembly, before
|
||
export; reuse `backend/services/asr_backend.py` (active backend; fall back to
|
||
WhisperX defaults when the active backend can't do the target language); job
|
||
events via `backend/core/job_store.py` `append_event`.
|
||
- **Design deltas vs pyvideotrans:** they let recognized text *replace* subtitles
|
||
unconditionally; we keep generated text authoritative for *content* and use the
|
||
second pass for *timing* + *QC*: re-recognize the dubbed track with halved VAD
|
||
windows, re-time cues from recognized boundaries, and compute per-segment drift
|
||
(normalized WER between recognized and target text — scorer exists in
|
||
`omnivoice/eval/wer/`). Segments above a drift threshold get flagged in the job
|
||
events and surfaced in `DubTab.jsx` as "verify this line" markers feeding the
|
||
incremental re-dub loop. Stage is opt-out, never fatal: any failure leaves
|
||
align-stage subtitles in place.
|
||
- **Tests:** pipeline test with an injected mispronounced segment asserting the flag
|
||
fires; probe judge asserting second-pass SRT stays well-formed and within the
|
||
existing dubbing duration-ratio gates.
|
||
- **Effort: M.**
|
||
|
||
### Spec 6 — MLX runtime pass
|
||
|
||
**Goal:** Apple Silicon speedup via dual-runtime routing, no behavior divergence.
|
||
|
||
- **Files:** `backend/services/tts_backend.py` — adopt voicebox's backend-aware
|
||
model mapping inside the registry: one engine key resolving to
|
||
`mlx-community/...` vs upstream weights by platform probe; harden the probe to
|
||
`import mlx.core` catching `ImportError, OSError, RuntimeError` (PyInstaller
|
||
bundles); `backend/services/asr_backend.py` — route dictation + dub ASR through
|
||
the existing `MLXWhisperBackend` when the probe passes (today it exists but isn't
|
||
the default path on Apple Silicon).
|
||
- **Constraint check:** implementation-level only — output behavior, defaults, and
|
||
UI identical on all platforms (explicitly allowed by the parity rule).
|
||
- **Tests:** probe `engines` spec on macOS runner asserting MLX route is selected
|
||
and produces passing DSP judges; regression: CUDA/CPU platforms unaffected
|
||
(registry resolution unit tests with mocked probes).
|
||
- **Effort: L** across engines; ship Whisper-first (M) since `MLXWhisperBackend`
|
||
already exists.
|
||
|
||
### Spec 7 — Crash-isolated ASR
|
||
|
||
**Goal:** a native ASR crash becomes a failed job, never a dead backend.
|
||
|
||
- **Files:** generalize `backend/services/subprocess_backend.py` (today TTS-oriented:
|
||
length-prefixed JSON, GPU slots, op allowlists) with an ASR sidecar subclass —
|
||
`sidecar_script()` wrapping the crashy engine; wire as an `ASRBackend`
|
||
implementation in `asr_backend.py`.
|
||
- **Design choice vs pyvideotrans:** they use one-task-per-child process pools
|
||
(fresh interpreter per job — max isolation, max model-reload cost). Our sidecar is
|
||
long-lived with a handshake + health check. Hybrid: keep the long-lived sidecar
|
||
for warm-model latency, add their **automatic respawn-on-death** semantics —
|
||
parent detects EOF/broken pipe, marks the in-flight job failed with a decorated
|
||
error (engine + device, like their broken-pool message), respawns lazily on next
|
||
request. Their single-JSON progress-file pattern is unnecessary here — we already
|
||
have a frame protocol with `progress` ops.
|
||
- **Tests:** kill the sidecar mid-transcription in a pytest (send SIGKILL) — assert
|
||
job fails with the decorated error, backend stays healthy, next request respawns;
|
||
smoke-test addition for the respawn path.
|
||
- **Effort: M.**
|
||
|
||
### Spec 8 — Streaming polish kit
|
||
|
||
**Goal:** production-grade `/ws/tts` TTFA + dictation-during-playback.
|
||
|
||
- **Chunker (S):** port Patter's `sentence_chunker.py` (MIT) to
|
||
`backend/services/sentence_chunker.py`; use in `backend/api/routers/tts_stream.py`
|
||
to synthesize sentence-by-sentence and flush the first clause aggressively
|
||
(≥ 40 chars on soft punctuation, their seven guards, Italian comma disable).
|
||
Port their golden parity scenarios as pytest fixtures. Extend terminator/honorific
|
||
tables toward our language list; share constants with `subtitle_segmenter.py`
|
||
where they overlap.
|
||
- **AEC (M):** port Patter's `aec.py` (MIT) to `backend/services/aec.py`; in
|
||
`capture_ws.py`, feed `push_far_end()` from the audio `/ws/tts` ships (resampled
|
||
24 kHz → capture rate) and run `process_near_end()` on mic frames before
|
||
transcription. Key adaptations (from the deep dive): staleness clock must track
|
||
playout time, not send time; Geigel rho needs desktop-speaker tuning; one AEC
|
||
instance per ws session (not thread-safe). Behind a Settings toggle initially
|
||
("dictate during playback"), defaulting ON only once probe-verified on all three
|
||
platforms — until then it's opt-in, per the platform-default rule.
|
||
- **Tests:** chunker parity fixtures; AEC unit tests (echo-only input converges to
|
||
near-silence; double-talk freezes adaptation); probe dictation spec variant with
|
||
synthetic far-end playback.
|
||
- **Effort: M total.**
|
||
|
||
### Spec 9 — Quality rails
|
||
|
||
**(a) Docs-drift CI (S).** New scheduled workflow alongside
|
||
`.github/workflows/ci.yml` (daily cron + dispatch, `issues: write`): a checked-in
|
||
canonical inventory (`docs/features.yaml` — engines, capabilities, platform flags;
|
||
seed it from the README feature grid + engine tables) diffed against docs stems and
|
||
the engine registry (`list_backends()` output). Reuse the self-test pattern from
|
||
`tests/scripts/test_validate_install_docs.py` for the new checker. Adopt Patter's
|
||
rolling-issue automation verbatim: update one `docs-drift`-labeled issue in place,
|
||
auto-close on green. Existing `scripts/validate-install-docs.py` stays as the
|
||
PR-gating half.
|
||
|
||
**(b) LLM-judge eval tier (M).** Port Patter's `case.py` + `runner.py` +
|
||
`assertions.py` into `tests/evals/` with the judge backend swapped to
|
||
`llm_backend.py` (local model — keeps local-first). Hard rule preserved: **LLM
|
||
judges never gate CI** — they run as a separate non-blocking scheduled job whose
|
||
report lands as an artifact; deterministic probe judges remain the only gates. First
|
||
suites: dub translation naturalness (segments from the probe dub fixture) and
|
||
dictation-refinement quality (Spec 3 outputs). Copy their two hardening details:
|
||
recompute pass locally from score; tolerant JSON parsing.
|
||
|
||
### Spec 11 — pyvideotrans bridge
|
||
|
||
**Goal:** restore the inbound bridge from a 17.9k★ upstream integrator.
|
||
|
||
- **Option A — upstream a REST integration (preferred, S–M):** PR to pyvideotrans
|
||
replacing the Gradio call in their `_omnivoice.py` with our REST API (clone-TTS
|
||
endpoint or the OpenAI-compatible surface in
|
||
`backend/api/routers/openai_compat.py`). They already ship OpenAI-style TTS
|
||
channels, so the precedent exists. Friendly-fork etiquette: file their open
|
||
integration bug first, reference it.
|
||
- **Option B — Gradio compat shim (fallback, M):** a tiny optional Gradio app in our
|
||
backend exposing a `/_clone_fn`-compatible signature that proxies to our REST
|
||
pipeline. Only if upstream declines — it adds a gradio runtime dep for one
|
||
integration and another surface to keep compatible.
|
||
- **Either way:** add a contract test pinning whatever surface they consume, so the
|
||
bridge can't silently break again (the engine-compat constraint extended to an
|
||
external consumer).
|
||
|
||
### Specs 12–13 — voice mixing · dynamic engine lifecycle (sized, not yet designed)
|
||
|
||
**12 (S–M):** weighted voice mixing from Kokoro-FastAPI (Apache-2.0) — blend
|
||
embeddings/style vectors by ratio where the engine exposes them; surface as a
|
||
"blend" control in `CloneDesignTab.jsx`. Engine-dependent: start with the preset
|
||
engines whose voice representations are vectors.
|
||
**13 (M):** Speaches-style (MIT) per-request model lifecycle — idle-unload timers
|
||
and VRAM-pressure eviction layered on the existing `gpu_queue` + `unload()`
|
||
contract in `tts_backend.py`/`asr_backend.py`. Design doc first: interaction with
|
||
the GPU slot accounting in `subprocess_backend.py` is the tricky part.
|
||
|
||
*Actions 10 and 14 are practice/positioning items — no code spec needed; 14's
|
||
content is in [positioning moves](#positioning-moves).*
|
||
|
||
---
|
||
|
||
## Roadmap directions (community discussion #346)
|
||
|
||
*Researched 2026-06-12 (third pass, four research agents + five verification
|
||
sub-agents). The maintainer's [discussion #346](https://github.com/debpalash/VoiceStudio/discussions/346)
|
||
announced a feature roadmap toward full ElevenLabs feature-parity. This section
|
||
grounds each direction in the landscape: what exists, what's license-clean, what
|
||
the honest constraints are, and a scope ladder per direction.*
|
||
|
||
**Mapping the announcement to this doc** — several items are already covered:
|
||
|
||
| Discussion item | Status |
|
||
|---|---|
|
||
| Unlimited-length generation | Covered — voicebox chunked TTS ([deep dive](#1-chunked-long-form-tts-backendutilschunked_ttspy-299-loc), port, S) |
|
||
| WisprFlow-like dictation for agentic/code editors | Covered — [Spec 3](#spec-3--dictation-refinement) + [positioning move 4](#positioning-moves) |
|
||
| Polished dubbing experience | Covered — [Specs 1](#spec-1--dub-length-fitting-v2), [4](#spec-4--per-segment-clone-refs), [5](#spec-5--second-pass-asr-qc) |
|
||
| Better MLX / Nvidia / AMD / CPU | [Spec 6](#spec-6--mlx-runtime-pass) + new compat-matrix research (§R4) |
|
||
| Polished OpenAPI specs with Scalar | **Mostly shipped** — Scalar mounted at `/docs` since #307; remaining work is spec hygiene (§R2) |
|
||
| Agentic voice workflow | New — §R1 |
|
||
| Remote GPU · Tailscale · remote API in UI | New — §R2 |
|
||
| Ebook/audiobook/stories creator · persona gallery | New — §R3 |
|
||
| Better model & env management | New — §R4 |
|
||
|
||
### R1 — Agentic voice workflow
|
||
|
||
**Runtime landscape (licenses verified against LICENSE files, 2026-06):**
|
||
|
||
| Runtime | License | Fit |
|
||
|---|---|---|
|
||
| [pipecat](https://github.com/pipecat-ai/pipecat) (12.8k★, v1.0) | **BSD-2** | **Best fit.** A Python library that runs *inside* our existing FastAPI process (`FastAPIWebsocketTransport`) — no extra server. Local VAD (Silero) + smart turn detection + barge-in. Its `OpenAITTSService` takes a `base_url` and defaults to 24 kHz — our `/v1/audio/speech` plugs in with configuration, not code |
|
||
| [LiveKit Agents](https://github.com/livekit/agents) (10.9k★) | Apache-2.0 | Good, heavier: needs a LiveKit media server alongside. The right choice only if self-hosted SIP at scale becomes the priority (their [SIP server](https://github.com/livekit/sip) is Apache-2.0). Their openai plugin TTS/STT classes accept `base_url` (verified in source) — OVS works as a provider today |
|
||
| [Patter](https://github.com/PatterAI/Patter) (MIT) | MIT | Parts donor (already deep-dived). If embedded, its **opt-out telemetry must be hard-disabled** to honor our local-first guarantee |
|
||
| [vocode-core](https://github.com/vocodedev/vocode-core) | MIT | **Avoid as runtime** — no commits since Nov 2024 |
|
||
| [TEN Framework](https://github.com/TEN-framework/ten-framework) | Apache-2.0 **+ conditions** | **Disqualified**: LICENSE bans hosting on "End User devices" (fatal for a desktop app) + an Agora non-compete |
|
||
|
||
**Telephony honesty.** There is **no fully-local path to the PSTN** — reaching a real
|
||
phone number requires a carrier (Telnyx ~$0.005–0.007/min, Twilio ~$0.014/min; even
|
||
self-hosted Asterisk/FreeSWITCH needs a SIP trunk as the gateway). So under our
|
||
constraints, outbound calling must be an **explicit opt-in integration where the user
|
||
supplies carrier credentials** — never a default. Two prerequisite spikes before
|
||
promising calls: (a) TTFA benchmark of our engines in a streaming pipeline against
|
||
the ~600 ms p95 voice-to-voice budget; (b) AudioSeal detection survival through the
|
||
8 kHz G.711 phone leg (untested anywhere — phone-band downsampling may strip the
|
||
watermark, and it certainly reduces cloned-voice fidelity).
|
||
|
||
**Persona/community bots.** Prior art exists but is assembled hobby-grade
|
||
(closest: [Discord-Local-LLM-VoiceChat-Bot](https://github.com/KickerMix/Discord-Local-LLM-VoiceChat-Bot)
|
||
— local Whisper + LM Studio + cloning). Text-persona bots are trivial on our stack
|
||
(LLM adapter + `/v1/audio/speech` voice replies). Live voice-channel bots are harder:
|
||
discord.py has never shipped voice *receive* (years-open RFC); the working options
|
||
are Pycord's recording sinks or [discord-ext-voice-recv](https://github.com/imayhaveborkedit/discord-ext-voice-recv)
|
||
(both MIT, both single-maintainer risk). The conversation loop (VAD/turn-taking)
|
||
should come from pipecat, not from this prior art.
|
||
|
||
**Safety/regulatory (binding, not optional).** FCC ruling
|
||
[FCC 24-17](https://www.fcc.gov/document/fcc-makes-ai-generated-voices-robocalls-illegal)
|
||
(Feb 2024): AI/cloned voices are "artificial" under the TCPA — consumer calls require
|
||
prior express consent ($500–1,500/call private right of action). Texas SB 140
|
||
requires AI disclosure within the first 30 seconds of a call. Tennessee's ELVIS Act
|
||
extends liability to **tool providers**. **EU AI Act Article 50 applies from
|
||
2026-08-02**: people must be told they're talking to an AI, and generative-audio
|
||
output must be marked machine-readably — **the open-source exemption does not cover
|
||
Article 50**, and our AudioSeal default maps directly onto the marking obligation
|
||
(a structural advantage no competitor ships). The "my own cloned voice, my own
|
||
errand" single-call case is a genuine legal gray zone — docs should say so rather
|
||
than imply it's safe.
|
||
|
||
**Guardrails to build in (concrete):** (1) non-removable disclosure preamble on every
|
||
outbound call — satisfies Texas + FCC direction + EU Art 50(1) in one stroke;
|
||
(2) **consent-locked voice profiles** — agentic features require a profile flagged
|
||
verified-own-voice (recorded consent phrase), exactly the lock voicebox was
|
||
criticized for lacking; (3) AudioSeal always-on for agentic output, no toggle;
|
||
(4) destination allowlist + daily call cap, and no bulk-dial API surface ever —
|
||
architecturally incapable of being robocall infrastructure; (5) local immutable call
|
||
log (with two-party-consent warning before audio recording); (6) honest jurisdiction
|
||
notice in docs.
|
||
|
||
**Scope ladder:** **v1 (S–M, mostly docs):** OVS as TTS/STT provider for
|
||
pipecat/LiveKit — both verified to point at `localhost:3900/v1` via `base_url`
|
||
today; ship a `docs/agentic-voice.md` recipe + a pipecat smoke test, fix whatever
|
||
param mismatches it exposes. Users wire their own agent; we stay a model server.
|
||
**v2 (M–L):** built-in Discord persona bot (opt-in by construction — user supplies
|
||
their own bot token; identical on all platforms): text replies via the LLM adapter +
|
||
voice replies via `/v1/audio/speech`, persona attached to a consent-locked profile;
|
||
live voice-channel mode as a stretch. Mount the MCP server in the same milestone
|
||
(Spec 2) so external agents can drive OVS voices. **v3 (L, only after guardrails
|
||
1–5 exist):** telephony via opt-in carrier credentials, pipecat embedded with
|
||
Telnyx/Twilio serializers — disclosure preamble, watermark, allowlist, and call log
|
||
land in the same PR, not a follow-up.
|
||
|
||
### R2 — Remote GPU, Tailscale, remote API, Scalar
|
||
|
||
**The pattern is settled** across Ollama / LM Studio / Open WebUI / Jellyfin: server
|
||
binds a port, client has a *base URL* setting, optional bearer key. Nobody
|
||
comparable ships custom tunneling — LM Studio's remote story (LM Link, June 2026)
|
||
took a **Tailscale partnership on tsnet** to do more, which is exactly the bar we
|
||
should not chase. The existing Tauri app *is* the thin client; it needs a Backend
|
||
URL setting + `/health` handshake, with the local backend supervisor disabled when
|
||
remote.
|
||
|
||
**Security is the non-negotiable half.** The cautionary tale:
|
||
[~175,000 publicly exposed no-auth Ollama servers](https://thehackernews.com/2026/01/researchers-find-175000-publicly.html)
|
||
found in early-2026 scans, with documented LLMjacking. Our voice-cloning endpoints
|
||
are *more* sensitive than chat. The consensus mechanism (LM Studio, vLLM, Speaches):
|
||
optional bearer key — `OMNIVOICE_API_KEY`; when set, all non-loopback HTTP+WS
|
||
requires `Authorization: Bearer`. Our existing `NetworkAccessMiddleware` PIN gate
|
||
has the right ASGI shape and needs a bearer variant. Loopback-only stays the desktop
|
||
default. Note: Tailscale Serve terminates on-node and forwards from `127.0.0.1` —
|
||
Serve traffic looks loopback to the PIN gate, so the token must still apply in
|
||
server mode. Docs say plainly: bearer-over-plain-HTTP is sniffable; use Tailscale
|
||
(WireGuard) or Serve (TLS) beyond a trusted LAN; never Funnel without the key.
|
||
|
||
**Tailscale depth:** ship rung (a) — documentation ("install Tailscale both ends,
|
||
paste the MagicDNS URL"), plus a Serve recipe — which is all Home Assistant, Open
|
||
WebUI, and Jellyfin actually ship. Embedding is not viable from Python: tsnet is
|
||
Go-only; libtailscale's Python binding and tailscale-rs are explicitly
|
||
experimental/unaudited and not on PyPI. Mention [headscale](https://github.com/juanfont/headscale)
|
||
for users wanting a fully open control plane. Tailscale's client core is BSD-3;
|
||
documenting it imposes nothing on us.
|
||
|
||
**Remote LLM endpoint UI:** vLLM's OpenAI-compat server is verified drop-in for our
|
||
`llm_backend.py` (today env-only via `TRANSLATE_BASE_URL`) — the work is Settings
|
||
fields for base URL + model + optional API key, which Ollama ignores and
|
||
vLLM/LM Studio require. Watch item: **vLLM-Omni** now serves TTS first-class with an
|
||
OpenAI-compatible `/v1/audio/speech` — including **CosyVoice3, an engine we wrap** —
|
||
so "OVS on the GPU box" will eventually compete with "vLLM-Omni on the GPU box"; a
|
||
future option is consuming a remote vLLM-Omni endpoint *as an engine*.
|
||
|
||
**Scalar:** already shipped (#307 — mounted at `/docs`, `scalar-fastapi` is MIT,
|
||
actively maintained). The remaining "polished spec" work is OpenAPI hygiene Scalar
|
||
renders but can't create: stable `operation_id`s, router tags + descriptions,
|
||
`response_model` + examples on every endpoint — `/v1` and core TTS routes first,
|
||
since those are what remote users hit. ~1 day for tags/IDs; the response-model long
|
||
tail is incremental.
|
||
|
||
**Ladder:** (1) Backend URL setting + health handshake — S; (2) bearer token incl.
|
||
WS paths + tests — S–M; (3) "Remote GPU over Tailscale" docs page — S;
|
||
(4) remote LLM endpoint UI — S–M; (5) OpenAPI hygiene — M incremental.
|
||
**Don't build:** custom tunneling/relay, tsnet embedding, Funnel as a promoted
|
||
path, mTLS/OAuth (overkill vs bearer + WireGuard), a second thin-client binary.
|
||
|
||
### R3 — Audiobook/stories creator + persona gallery
|
||
|
||
> **Status (2026-06-13):** backend shipped — `backend/services/audiobook.py`
|
||
> parses a chapter-delimited script (Markdown `# H1` chapters + inline
|
||
> `[voice:NAME]`; `[pause …]` delegated to the shared `parse_pause_markers`)
|
||
> into a chapter/span plan, renders each chapter through the active TTS engine
|
||
> (`synthesize_chapter` + `chunked_tts`), and muxes a chapterized **m4b**
|
||
> (FFMETADATA1 chapters). `POST /audiobook/plan` previews the plan;
|
||
> `POST /audiobook` runs the synth job streaming SSE progress (ffmpeg-gated).
|
||
> A dedicated **Audiobook** tab (script editor → plan preview → streamed synth
|
||
> → m4b player/download) ships in the frontend. Deferred: epub/pdf/docx ingest,
|
||
> ACX `loudnorm` mastering, and crash-resume.
|
||
|
||
**The production bar** (verified against [ebook2audiobook](https://github.com/DrewThomasson/ebook2audiobook),
|
||
audiblez, epub2tts, abogen, Pandrator): broad ingest (epub/mobi/pdf/docx + OCR for
|
||
image PDFs), chapter detection (TOC-driven for epub — even the 19.2k★ leader's
|
||
algorithm is thinly documented), **chapterized m4b** output (FFMETADATA1 chapters +
|
||
cover via `attached_pic` — ffmpeg writes both Nero and QuickTime chapter forms),
|
||
inline tags (`[pause:N]`, `[voice:...]` — ebook2audiobook's square-bracket dialect
|
||
is Apache-2.0 and portable), batch with per-file voice mapping, and **crash resume**
|
||
(their `--session` pattern). The mastering bar is the
|
||
[ACX technical spec](https://help.acx.com/s/article/what-are-the-acx-audio-submission-requirements):
|
||
RMS −23 to −18 dB, peaks < −3 dB, noise floor < −60 dB RMS, ≥192 kbps CBR MP3,
|
||
≤120 min/file, room tone 1–5 s both ends — implementable as two-pass ffmpeg
|
||
`loudnorm` + an `astats` verifier. **Framing caveat:** ACX itself prohibits AI
|
||
narration unless authorized — market this as "masters to ACX technical spec," never
|
||
"Audible-ready."
|
||
|
||
**License traps in the parser stack (verified — this is the §R3 landmine):**
|
||
`ebooklib` is **AGPL-3.0 with no commercial-license option** — it poisons our
|
||
commercial build; parse EPUB with `zipfile` + `lxml` instead (EPUB is zip+XHTML).
|
||
PyMuPDF is AGPL (Artifex sells exceptions) — use **pypdf (BSD-3)**. The PyPI `mobi`
|
||
package is GPL — shell out to Calibre's `ebook-convert` instead (process boundary is
|
||
safe). OCR via pytesseract/tesseract is Apache-2.0, clean.
|
||
|
||
**What we already have:** voicebox's Stories schema (deep-dived above) is the
|
||
assembly/timeline half. The missing halves are symmetric: ingest (parsing,
|
||
chapterization, long-form batch with per-chapter resume) and export (m4b + ACX
|
||
mastering). **Ladder:** A1 EPUB ingest + TOC chapters + resumable per-chapter TTS
|
||
queue (M) → A2 chapterized m4b export (S) → A3 ACX mastering pass (S–M) → A4
|
||
PDF/txt/docx (M) → A5 inline tags + per-chapter voices (M) → A6 OCR + Calibre
|
||
shell-out (M) → A7 book→Stories timeline round-trip (L) — the differentiator no
|
||
surveyed tool has.
|
||
|
||
> **Status (2026-06-13):** the browse-preview-install surface already exists —
|
||
> `VoiceGallery.jsx`'s Community zone over the `omnivoice-gallery` git manifest
|
||
> (`backend/api/routers/community.py`), plus `.omnivoice` bundle export/import
|
||
> (`marketplace.py`). The missing piece for the synthetic-only gate was data
|
||
> integrity: imported community presets and bundle round-trips silently dropped
|
||
> `kind`/`vd_states`, demoting designed personas to clones. Fixed — community
|
||
> "preset" imports as `kind='design'` (a "voice" as `clone`), and bundles now
|
||
> carry `kind`+`vd_states` (old bundles import as clone). This makes the
|
||
> "accept only designed/synthetic" gate enforceable. Still to do: the consent
|
||
> attestation + AudioSeal-on-preview gate and the curation workflow below.
|
||
|
||
**Persona gallery — the territory is genuinely unoccupied.** The field splits into
|
||
consent-heavy commercial (ElevenLabs Voice Library: live-read Voice Captcha
|
||
verification, human review, sharing limited to professional clones), a consent-free
|
||
gray market (voice-models.com, ~28k RVC models), and read-only single-project
|
||
registries ([piper-voices](https://huggingface.co/rhasspy/piper-voices)' single
|
||
`voices.json` is the proven local-first pattern). **No OSS, consent-aware,
|
||
browse-preview-install voice gallery exists.** The build recipe: piper-voices-style
|
||
JSON index in a public git repo (checksums + preview URLs, payloads on HF) +
|
||
Obsidian-style PR curation + our existing `VoiceGallery.jsx` as the browser (it
|
||
already has community hooks: `useCommunityX`, `communitySubmitUrl`).
|
||
|
||
**Consent gates (legal floor, not nice-to-have):** Tennessee ELVIS reaches tools
|
||
whose "primary purpose" is unauthorized voice likeness — a gallery distributing
|
||
named-person clones is much closer to that line than a TTS engine; Illinois HB 4875
|
||
reaches distribution *facilitators*; EU Art 50 marking applies from 2026-08-02.
|
||
Gates: accept only **designed/synthetic voices** (#317 mapper personas) and
|
||
**self-recorded voices with a recorded consent statement** (spoken attestation, not
|
||
a checkbox — the Consumer Reports critique); AudioSeal watermark mandatory on
|
||
preview audio (it's already a direct dependency and now fully MIT incl. weights,
|
||
with a 16-bit payload — enough to carry a persona ID); PR-based human curation;
|
||
takedown via issue template propagating on index refresh. Honest note: none of this
|
||
stops a determined fork — the gates protect *the project* and set norms; the index
|
||
is the one chokepoint we actually control.
|
||
|
||
**Portable persona format:** no standard exists (the one attempt, vox-format, has
|
||
zero adoption; the de-facto reality is five incompatible engine-native formats).
|
||
Recommend a minimal **`.ovsvoice`** zip: manifest (schema version, engine + design
|
||
params for deterministic-mapper reproducibility, tags), optional reference audio +
|
||
transcript, `consent.json` (creation method + attestation + timestamp), SPDX-style
|
||
license tag, watermarked preview. Demand signal: voicebox's
|
||
[#138](https://github.com/jamiepine/voicebox/issues/138) (export profiles for
|
||
Piper/Home Assistant) — design the format so a Piper-ONNX export target can be added
|
||
later. **Ladder:** G1 `.ovsvoice` export/import (S–M, standalone value) → G2
|
||
community index + Gallery "Community" tab (M) → G3 in-app submission via prefilled
|
||
GitHub PR/issue — mirrors our bug-reporter pattern, no accounts (M) → G4
|
||
similarity-search/ratings/ONNX export (L, later).
|
||
|
||
### R4 — Model & env management, GPU compat matrix
|
||
|
||
**Environment management — what the field converged on** (licenses verified):
|
||
ComfyUI's one-shared-env model is the cautionary tale (conflict UIs, downgrade
|
||
blacklists, and pip-state-repair files *as product features*; its 2026 fix is uv +
|
||
whole-env lockfiles, not isolation). Pinokio (MIT) and StabilityMatrix (AGPL —
|
||
patterns only, no code) both landed on **one venv per app** — exactly our sidecar
|
||
architecture — then clawed disk back at the filesystem layer. LM Studio/Ollama
|
||
sidestep Python entirely with decoupled, hot-swappable native runtime packs — the
|
||
strongest pattern, but ours only if we ever ship prebuilt engine binaries.
|
||
Code-portable references: Pinokio, Ollama, llama.cpp, lms CLI (all MIT),
|
||
huggingface_hub/hf-xet (Apache-2.0), uv (MIT/Apache). **Not portable:**
|
||
StabilityMatrix (AGPL), ComfyUI + Manager + comfy-cli (all GPL-3.0).
|
||
|
||
**The torch-duplication math (measured 2026-06):** uv's global cache dedupes via
|
||
link mode — default **`clone` (CoW) on macOS *and* Linux, `hardlink` on Windows**;
|
||
same wheel across N venvs ≈ one copy on disk, *iff* cache and venvs share a
|
||
filesystem. But dedup is per-identical-wheel: our IndexTTS2 sidecar (torch 2.6.x)
|
||
vs parent (torch 2.8.0) shares nothing — the Windows cu128 torch wheel alone is
|
||
**~3.2 GiB** (measured), Linux ~0.83 GiB + multi-GB `nvidia-*` deps. Partial
|
||
consolation on Linux: `nvidia-*` packages dedupe independently wherever pinned
|
||
versions coincide across torch versions. Levers: keep `UV_CACHE_DIR` + sidecar
|
||
venvs on one filesystem; consider pinning `UV_LINK_MODE=hardlink` on Linux (reflink
|
||
degrades on ext4); and treat "align the sidecar's torch pin with the parent
|
||
whenever the engine permits" as the single biggest disk decision. Watch item:
|
||
PyTorch **wheel variants** (shipped experimental in 2.8, NVIDIA+Astral
|
||
collaboration) will eventually make `uv install torch` auto-select the right CUDA
|
||
build; uv already ships `--torch-backend=auto`.
|
||
|
||
**The compat matrix is two-dimensional** — `(torch version, CUDA wheel variant) →
|
||
supported sm_XX set`, published in
|
||
[pytorch RELEASE.md](https://github.com/pytorch/pytorch/blob/main/RELEASE.md):
|
||
Blackwell sm_120 needs **2.7.0 + cu128 or later**; from 2.8 the cu128+ wheels
|
||
dropped Maxwell/Pascal (Turing sm_75 is the floor; Pascal users must pin cu126
|
||
variants). Driver minimums: CUDA 12.x wheels ≥ 525, 13.x ≥ 580. Both failure
|
||
directions ("GPU too new" sm_120-on-cu126 and "GPU too old" sm_61-on-cu128) throw
|
||
the same lazy `no kernel image` error *after* `cuda.is_available()` returns True —
|
||
which is why preflight must check capability, not availability. The documented
|
||
antipattern is Ollama's silent CPU fallback
|
||
([their own #14258](https://github.com/ollama/ollama/issues/14258)); voicebox's
|
||
silent-fallback bootstrap is our other autopsy. Build: detect (capability via
|
||
torch, driver via NVML) → gate engine installs with a specific message ("this
|
||
engine's cu128 build needs Turing+; you have Pascal — installing the cu126 build
|
||
instead") → **loud persistent CPU-fallback banner**, never silent. We already have
|
||
probes to build on (`engine_env.py` compute-capability check — today it only gates
|
||
`torch.compile`; `hardware_probe.py`).
|
||
|
||
**Model management — the HF cache is the blessed single source of truth.** The hub
|
||
cache layout is now a [language-agnostic spec](https://huggingface.co/docs/hub/local-cache)
|
||
adopted by llama.cpp among others; blobs are content-addressed (LFS SHA-256 =
|
||
filename, so integrity is re-checkable offline), and the v1.x CLI ships exactly the
|
||
manager primitives a UI needs: `hf cache ls --filter "accessed>30d"`, `hf cache rm`,
|
||
`hf cache prune`, **`hf cache verify`**. Gotchas verified: Windows without
|
||
Developer Mode falls back to copy-per-snapshot (degraded dedup); concurrent
|
||
downloads are lock-protected (`.locks/`) but **deletion is not** — a delete UI over
|
||
a shared cache must handle delete-vs-reader races (fine on Linux fd semantics,
|
||
breaks on Windows); env vars are read at import time, so a Settings-controlled
|
||
cache path needs a restart. Offline/restricted: `HF_HUB_OFFLINE`,
|
||
`HF_HUB_ETAG_TIMEOUT` (falls back to cache on timeout), `HF_ENDPOINT` for mirrors —
|
||
hf-mirror.com is community-run, not HF-official, and its compatibility with the new
|
||
Xet/CAS download path is untested (escape hatch: `HF_HUB_DISABLE_XET=1`). Also:
|
||
`hf_transfer` is now fully deprecated (Xet is the default transfer path) —
|
||
consistent with our existing stack guidance.
|
||
|
||
**Ladder:** (a) uv link-mode + shared-cache audit, document the dedupe behavior +
|
||
sidecar pin-alignment policy — S; (b) in-app preflight compat gate (capability +
|
||
driver → engine × wheel-variant table) with specific errors + loud CPU banner — M;
|
||
(c) model manager UI over `scan_cache_dir()` (per-model disk usage, evict,
|
||
re-verify, mirror setting) — M; (d) LM-Studio-style decoupled runtime packs — L,
|
||
**not recommended now**: our sidecar architecture already decouples engines; revisit
|
||
only if we ship prebuilt binaries.
|
||
|
||
### Consolidated new actions
|
||
|
||
| # | Action | Mode | Effort | First rung |
|
||
|---|---|---|---|---|
|
||
| 15 | Agentic v1: provider recipe + pipecat smoke test against `:3900/v1` | Docs + test | S–M | §R1 v1 |
|
||
| 16 | Remote backend: URL setting + bearer token + Tailscale docs page | Build | M total | §R2 rungs 1–3 |
|
||
| 17 | Audiobook v1: EPUB ingest → chapterized m4b → ACX mastering | Build (+ port Apache-2.0 pieces) | M+S+S–M | §R3 A1–A3 |
|
||
| 18 | `.ovsvoice` portable persona export/import | Build | S–M | §R3 G1 |
|
||
| 19 | Engine preflight compat gate + loud CPU-fallback banner | Build | M | §R4 (b) |
|
||
| 20 | Model manager UI over the HF cache primitives | Build | M | §R4 (c) |
|
||
| 21 | OpenAPI hygiene pass under the shipped Scalar UI | Build | M incremental | §R2 rung 5 |
|
||
| 22 | Consent-locked voice profiles (prerequisite for §R1 v2/v3 and §R3 G2+) | Build | M | §R1 guardrail 2 |
|
||
|
||
---
|
||
|
||
## User sentiment & market positioning
|
||
|
||
*Researched 2026-06-11. Issue volumes are hand-clustered from title analysis (both
|
||
trackers use almost no labels): the 250 most recent open voicebox issues, and all 32
|
||
open + 80 recent closed pyvideotrans issues. Reddit data is partly secondhand
|
||
(reddit.com blocks direct fetching); flagged where so.*
|
||
|
||
### voicebox: what its users hit (~377 open / ~156 closed — maintainer drowning)
|
||
|
||
| Complaint theme | Volume | Representative | For us |
|
||
|---|---|---|---|
|
||
| CUDA/GPU bring-up failures (sm_120 "no kernel image", Pascal unsupported, AMD/Intel ignored, 2.4 GB CUDA re-downloads) | ~45–50 of 250; 4 of their top-10 most-commented ever | [#417](https://github.com/jamiepine/voicebox/issues/417), [#594](https://github.com/jamiepine/voicebox/issues/594), [#728](https://github.com/jamiepine/voicebox/issues/728), [#676](https://github.com/jamiepine/voicebox/issues/676) | **Warning + opportunity.** Endemic local-AI tax we share — but their Windows installers shipped with CUDA silently broken for months. GPU auto-detect with explicit per-arch errors is a real differentiator *if it holds on the edges* |
|
||
| Model download / offline failures (cached models phoning home, infinite offline retry, no mirror option) | ~25 of 250 | [#557](https://github.com/jamiepine/voicebox/issues/557), [#434](https://github.com/jamiepine/voicebox/issues/434), [#546](https://github.com/jamiepine/voicebox/issues/546) | **Opportunity.** Maps exactly to our uv-mirror + HF-token capabilities. A "local-first" app that breaks offline is a betrayed promise users notice loudly |
|
||
| Startup crashes / white screens / **no Linux binary at all** | ~30 of 250 | [#513](https://github.com/jamiepine/voicebox/issues/513), [#617](https://github.com/jamiepine/voicebox/issues/617), [#606](https://github.com/jamiepine/voicebox/issues/606) (regression of an earlier fix), [#682](https://github.com/jamiepine/voicebox/issues/682) | **Opportunity.** Regressions recur because fixes ship fast without cross-platform gates — our parity rule + 3-platform smoke matrix is precisely this gap |
|
||
| Generation quality bugs (30 s transcription cutoff, reference audio leaking into output, refinement silently translating to English) | ~25 of 250 | [#604](https://github.com/jamiepine/voicebox/issues/604), [#609](https://github.com/jamiepine/voicebox/issues/609), [#603](https://github.com/jamiepine/voicebox/issues/603) | **Warning.** Engine-level artifacts we inherit too; the fixable subset (silent language handling, truncation) are pipeline bugs — testable |
|
||
| Dictation/capture friction (Windows keyboard hooks, double auto-paste) | ~10 of 250 | [#687](https://github.com/jamiepine/voicebox/issues/687), [#697](https://github.com/jamiepine/voicebox/issues/697) | **Warning.** We shipped the same class of fix (#287/#299). OS-hook dictation is a permanent treadmill on all three platforms |
|
||
| Docs rage — their single most-reacted issue ever is a failed first run blamed on missing docs | top-reacted (17 reactions) | [#108](https://github.com/jamiepine/voicebox/issues/108); [#185](https://github.com/jamiepine/voicebox/issues/185) (32 comments, top open: fine-tune instructions) | **Opportunity.** This is literally our core value. The community reply in #108 — "this is open-source software, not free support" — is the failure mode we exist to avoid |
|
||
|
||
**Praise** (consistent across coverage): cloning quality ("near-perfect" from 3–5 s;
|
||
one reviewer scored it above ElevenLabs Multilingual v2 on cloning accuracy);
|
||
privacy + zero cost as the hook of every viral post; the Stories timeline editor and
|
||
MCP agent-voice as "genuinely innovative"; out-of-box Metal acceleration; maintainer
|
||
responsiveness — even as the backlog grows. Notably its 29.7k★ came from
|
||
X/Threads/LinkedIn/Reddit virality; it [barely registered on HN](https://news.ycombinator.com/item?id=47831411)
|
||
(1 point).
|
||
|
||
**Abandonment:** mostly switch-*backs* to ElevenLabs on Windows — the
|
||
[substack reviewer's verdict](https://theaitoolkit2.substack.com/p/i-tested-voicebox-the-free-local):
|
||
"Windows users should wait weeks for GPU fixes. Low-volume creators should stick
|
||
with ElevenLabs' $5/month simplicity." Plus churn-risk from unstable main
|
||
([#648](https://github.com/jamiepine/voicebox/issues/648)) and a reputational drag:
|
||
[TechTimes covered](https://www.techtimes.com/articles/316850/20260519/voicebox-clones-any-voice-3-seconds-audio-runs-locally-free-has-no-consent-lock.htm)
|
||
voicebox having "no consent lock" amid voice-fraud concerns — an angle where our
|
||
AudioSeal default is the counter-story.
|
||
|
||
**Most-engaged requests:** fine-tune instructions (top open, 32 comments); export
|
||
voice profiles to ONNX for Piper/Home Assistant
|
||
([#138](https://github.com/jamiepine/voicebox/issues/138) — a self-hosting crowd
|
||
signal); SenseVoice/FunASR STT requested **five separate times in one week**; AMD
|
||
DirectML; Linux support.
|
||
|
||
### pyvideotrans: what its users hit (32 open / ~887 closed — aggressive solo triage)
|
||
|
||
| Complaint theme | Volume | Representative | For us |
|
||
|---|---|---|---|
|
||
| Pipeline hangs + faster-whisper GPU teardown crashes (long-standing per users) | recurring across versions | [#1129](https://github.com/jianchang512/pyvideotrans/issues/1129), [#1118](https://github.com/jianchang512/pyvideotrans/issues/1118) | **Warning.** Upstream faster-whisper lifecycle bugs — we run the same stack. User-found mitigations (pre-segment audio, CPU fallback for long audio) are worth implementing as automatic fallbacks; Spec 7 contains the blast radius |
|
||
| Subtitle/audio sync drift, silence-removal eating final words, merged batch translations | ~10 open + the most-reacted closed bugs | [#923](https://github.com/jianchang512/pyvideotrans/issues/923) (22 comments), [#1012](https://github.com/jianchang512/pyvideotrans/issues/1012) | **Warning (endemic to dubbing).** Speech-rate mismatch is the hardest unsolved problem in the category — anyone evaluating our dub pipeline judges us on exactly this. Specs 1 + 5 are the answer |
|
||
| External TTS engine integration breakage (GPT-SoVITS, F5-TTS, index-tts break at the API seam) | ~10 open | [#636](https://github.com/jianchang512/pyvideotrans/issues/636) (26 comments), [#954](https://github.com/jianchang512/pyvideotrans/issues/954) | **Opportunity.** They delegate TTS to a zoo of self-hosted side-servers; every seam is a support ticket. Our bundled-engine model removes this entire class — a concrete pitch. (Their broken `_omnivoice.py` is this same theme pointed at us — Spec 11) |
|
||
| Install failures, especially macOS (source-only; Windows gets a praised .exe) | ~6 recent cluster; all-time #2 most-commented issue is literally "Installation tutorial" ([#193](https://github.com/jianchang512/pyvideotrans/issues/193), 67 comments) | [#950](https://github.com/jianchang512/pyvideotrans/issues/950), [#952](https://github.com/jianchang512/pyvideotrans/issues/952) | **Opportunity.** macOS/Linux users are second-class there; a signed mac installer with a working first run is a direct wedge |
|
||
| CUDA errors (GPU fails, silently falls back to CPU) | steady trickle; [#287](https://github.com/jianchang512/pyvideotrans/issues/287) 22 comments | [#177](https://github.com/jianchang512/pyvideotrans/issues/177), [#980](https://github.com/jianchang512/pyvideotrans/issues/980) | **Warning** — though notably smaller than voicebox's, because they treat CPU as the default path and GPU as opt-in |
|
||
| LLM translation plumbing (thinking-tags leaking into subtitles, stripped punctuation, merged lines) | ~8 | [#921](https://github.com/jianchang512/pyvideotrans/issues/921), [#979](https://github.com/jianchang512/pyvideotrans/issues/979) | **Opportunity (partial).** LLM-output sanitization is cheap, testable hygiene that visibly differentiates output quality |
|
||
| **Their VoiceStudio integration is reported broken by users** | open | [#1124](https://github.com/jianchang512/pyvideotrans/issues/1124) | Confirms the Spec 11 finding from their side of the bridge |
|
||
|
||
**Praise:** the packaged Windows .exe needing zero Python setup; completely free
|
||
with no login/registration/gates; breadth of integrations; responsive maintainer.
|
||
The [Aug 2024 HN thread (182 points)](https://news.ycombinator.com/item?id=41234713)
|
||
praised the democratization angle — dubbing for material nobody would pay a human
|
||
to dub. **Abandonment:** toward subtitles-over-dubbing entirely, Yandex Browser /
|
||
YouTube auto-dub for casual use, paid dubbing (ElevenLabs, DeepDub) when emotional
|
||
fidelity matters, and VideoLingo within the OSS niche for subtitle quality.
|
||
**Most-engaged requests:** model-chasing (SenseVoice — same ask hit voicebox 5× the
|
||
same month; index-TTS v2; Fish Audio; Qwen3-TTS), automatic per-speaker role
|
||
assignment, emotion transfer into dubs, a manual subtitle-proofread checkpoint
|
||
before merge, per-line audio export.
|
||
|
||
### Comparative read
|
||
|
||
- **voicebox↔pyvideotrans comparisons are essentially absent** — they own different
|
||
frames (English-social "ElevenLabs alternative" vs Chinese-ecosystem "video
|
||
translation pipeline"). **We are unusual in straddling both**, which is a
|
||
positioning asset nobody currently contests.
|
||
- **vs ElevenLabs, what tips the decision:** toward local — cost at volume and
|
||
privacy, every time; back toward cloud — (1) first-run failure (especially
|
||
Windows GPU), (2) raw quality ceiling / emotional fidelity, (3) "just works"
|
||
simplicity for low-volume users. Notably, *quality is no longer the automatic
|
||
cloud win* (voicebox cloning reviews beat ElevenLabs Multilingual v2) — the
|
||
deciding factor has shifted to **reliability of install and GPU bring-up**, i.e.
|
||
exactly our stated core value.
|
||
- **The category wishlist** (duplicated across both trackers): SenseVoice/FunASR
|
||
ASR, index-TTS v2, Fish Audio, per-speaker dubbing, emotion control, fine-tuning
|
||
instructions.
|
||
|
||
### ElevenLabs pricing pressure (verified on [elevenlabs.io/pricing](https://elevenlabs.io/pricing), 2026-06-11)
|
||
|
||
Free $0 / 10k credits (no commercial license, no cloning) · Starter $6/mo / instant
|
||
cloning · Creator $22/mo / professional cloning · Pro $99 · Scale $299 (3 pro
|
||
clones) · Business $990 (10 pro clones). The complaints that push users local:
|
||
|
||
- **Dubbing multiplies cost per target language** — a 10-min video into 3 languages
|
||
bills as 30 minutes; Creator includes ~50 dubbing minutes with $0.60/min overage
|
||
([their help article](https://help.elevenlabs.io/hc/en-us/articles/23338815703697-How-much-does-Dubbing-cost)).
|
||
This is exactly the multi-language batch workload where local-and-free is most
|
||
compelling — and our 50-video batch users' workload.
|
||
- **Editing a dub costs credits** — regenerating a clip bills each time; the credit
|
||
rebate covers roughly one full re-dub
|
||
([dubbing studio docs](https://elevenlabs.io/docs/dubbing/studio)). They built
|
||
per-segment regeneration *and had to bolt a rebate scheme onto it* — direct
|
||
evidence the incremental-re-dub pain is real and monetized against.
|
||
- **Cloning is paywalled** at every tier boundary; commercial use paywalled on Free.
|
||
- **Credits-as-abstraction** generates its own churn-intent search ecosystem
|
||
(third-party "what do credits actually cost" explainers).
|
||
|
||
### Honest verdicts on our unique five
|
||
|
||
| Differentiator | Verdict | Evidence |
|
||
|---|---|---|
|
||
| Incremental re-dub | **Strong lever, unmarketed** | ElevenLabs monetizes against this exact pain (rebate scheme). Nobody *searches* the term — show it (demo GIF), don't name it |
|
||
| 646 languages | **Strong reach/press lever; niche for retention** | It's the hook in all existing coverage of us; Chatterbox's English-only limit draws recurring complaints. Caveat: most users need 1–3 languages, and no third party has verified long-tail quality — overselling invites "language #412 sounds terrible" backlash |
|
||
| AudioSeal watermarking | **Nobody-asked (users) / press + compliance asset** | Zero end-user search demand (hobbyists prefer unwatermarked); but reviewers spontaneously praise it, roundups list missing watermarking as an open-source *limitation*, 2026 disclosure regulation makes it a compliance story, and voicebox's "no consent lock" press is the counter-example. Frame as the commercial-use objection-killer; don't lead with it |
|
||
| Cross-platform dictation default | **Strong lever, especially Linux** | The WisprFlow-alternative market is crowded on macOS, thin on Windows, and served only by single-purpose tools on Linux (active 2026 development = live demand). Nobody offers dictation+cloning+dubbing in one cross-platform app. Caveat: dictation searchers want a small focused app — message it as "already on your machine", not as a lightweight utility it isn't |
|
||
| 3-step translation chain | **Niche; users value the outcome, never the mechanism** | Real evidence that LLM translation quality matters (pyvideotrans documents it beats Google/DeepL; context-aware modes sell subtitle tools). Nobody searches "3-step chain" — market as "translations that don't sound like Google Translate" with a before/after; bury the architecture in docs. Note VideoLingo ships the same idea — it's a parity feature inside the niche, a differentiator outside it |
|
||
|
||
### Our own footprint (2026-06-11)
|
||
|
||
- Repo: 6,808★ / 1,045 forks, created 2026-04-09 — strong two-month trajectory.
|
||
- Press: [MarkTechPost](https://www.marktechpost.com/2026/05/26/meet-omnivoice-studio-a-local-open-source-alternative-to-elevenlabs/)
|
||
(accurate, positive) and an uncritical blog endorsement; a viral X post framing
|
||
us as killing "$700/year in ElevenLabs and HeyGen subscriptions" — note it
|
||
positions *dubbing* as the hero feature. No independent quality review of the
|
||
646-language claim exists yet — **our most exposed flank**: expectations are
|
||
being set high with no third-party validation behind them.
|
||
- **Hacker News: effectively absent.** No submission with traction; every
|
||
comparable tool got its bump there. An unclaimed opportunity.
|
||
- **Name collision (the big misconception risk):** three entities share "VoiceStudio"
|
||
— (1) the k2-fsa VoiceStudio *model* (our default engine; their community-projects
|
||
page lists us, underselling us as "desktop application for voice generation");
|
||
(2) omnivoice.app, an unrelated commercial cloud product; (3) us. Most "VoiceStudio"
|
||
YouTube traffic and the pyvideotrans VoiceStudio docs page are about the *model* —
|
||
search demand is being split three ways and both competitor trackers contain
|
||
"support VoiceStudio" requests that mean the model, not us.
|
||
|
||
### Positioning moves
|
||
|
||
1. **"vs ElevenLabs dubbing cost" comparison page** anchored on the multiplier math
|
||
(10 min × 3 languages = 30 billed minutes; $0.60/min overage; pay-to-edit), with
|
||
one table: "20-min video → 3 languages → fix 5 lines → re-export" priced on
|
||
Creator vs $0 local. Natural home for the incremental-re-dub demo (30 s GIF:
|
||
edit one line → only that segment regenerates). Targets the highest-intent query
|
||
cluster ("elevenlabs pricing/dubbing cost") that third parties currently
|
||
monetize.
|
||
2. **Show HN, leading with the install story, not the model.** HN's documented
|
||
objections to local TTS are install friction and English-only — our installer +
|
||
GPU auto-detect + 646 languages answer both. Title shape: "Show HN: Local
|
||
ElevenLabs alternative — dub, clone, dictate on your own GPU, one installer."
|
||
3. **Claim the name before the collision hardens:** README/FAQ disambiguation
|
||
("VoiceStudio **Studio**, the desktop app built on the k2-fsa VoiceStudio engine —
|
||
not omnivoice.app"); ask k2-fsa to upgrade our one-line community listing to
|
||
mention dubbing/dictation; get the pyvideotrans VoiceStudio docs page pointing at
|
||
Studio as the GUI path (pairs with Spec 11 — arrive with the fixed integration).
|
||
4. **A discoverable dictation entry point:** a docs/landing section "open-source
|
||
WisprFlow alternative for Mac, Windows, and Linux (built into VoiceStudio
|
||
Studio)" + PRs to the alternative-list aggregators. Proven, high-conversion
|
||
query pool; no incumbent covers Linux well.
|
||
|
||
---
|
||
|
||
## Appendix: engine evaluation — ResembleAI Chatterbox (2026-06-11)
|
||
|
||
**Verdict: integrate later — not now.** Full facts verified against the HF cards,
|
||
GitHub pyproject, and PyPI (0.1.7, 2026-03-26).
|
||
|
||
- **License: clean.** MIT on code *and* all three weight variants (original 0.5B EN,
|
||
Multilingual 23-lang, Turbo 350M) — compatible with our AGPL + commercial
|
||
dual-license. The "Resemble uses special weight terms" worry did not materialize.
|
||
- **What it would add:** Turbo's inline paralinguistic tags (`[laugh]`, `[cough]`,
|
||
`[chuckle]`) and the single-knob `exaggeration` expressiveness control — genuinely
|
||
unique in our roster. Fast English cloning (cloning + speed is a gap; KittenTTS is
|
||
fast but can't clone). The 23-lang multilingual cloning is **not** differentiating
|
||
for us (VoiceStudio 646, VoxCPM2 30 @ 48 kHz).
|
||
- **Why not now:**
|
||
1. `chatterbox-tts` hard-pins `torch==2.6.0` + `transformers==5.2.0`; we constrain
|
||
`torch==2.8.0` and require `transformers>=5.3.0` — **unresolvable in the parent
|
||
venv**, forcing a dedicated-venv sidecar (IndexTTS2 pattern, ~800–1000 LOC) that
|
||
downloads a *second multi-GB torch*. The disk/download cost is the price, not
|
||
the code.
|
||
2. `resemble-perth` (its built-in PerTh watermarker) is a **git-URL dependency** —
|
||
unmirrorable on restricted networks, against our bootstrap story. Also untested
|
||
interaction: PerTh + our AudioSeal = double watermarking.
|
||
3. MPS is buggy upstream (float64 conversion crash on Turbo; placeholder-storage
|
||
errors); honest Apple-Silicon support means carrying community patches. Mac-ARM
|
||
users already get Chatterbox today via our MLX-Audio curated list
|
||
(`mlx-community/Chatterbox-TTS-4bit`).
|
||
- **Cheapest path / re-eval triggers:** ResembleAI publishes official
|
||
[chatterbox-turbo-ONNX](https://huggingface.co/ResembleAI/chatterbox-turbo-ONNX)
|
||
exports. If a 1-day spike proves it runs on plain `onnxruntime`, Turbo slots into
|
||
the lightweight supertonic3-style sidecar (~700 LOC, **no second torch**) and we
|
||
get the paralinguistic tags cheaply. Also re-evaluate if upstream relaxes the
|
||
torch/transformers pins or publishes `resemble-perth` to PyPI.
|