Review follow-up. The budget matched model names by substring, so a custom
repo whose name contains "turbo", "small" or "base" (or a word such as
"database") got a reduced budget and could be admitted to CUDA without
enough memory. Reduced budgets now apply only to the exact OpenAI
checkpoint ids, .en variants included. Any other repository, fine-tunes
included, keeps the conservative 5.0 GB, as the engine doc says.
Review follow-up. A generation first waits in the GPU pool's queue, on its
own clock (GPU_QUEUE_TIMEOUT_S, 1800 s by default), before its execution
budget starts, so a 630 s wait could still give up before the backend
returned its queue error. The generate wait now adds the queue budget.
Transcription is unchanged: run_transcribe_guarded starts its 300 s clock
at submission, so queue time already counts against it.
Parity tests pin each tool's wait above the backend's own worst case,
from ASR_TRANSCRIBE_TIMEOUT_S, GPU_QUEUE_TIMEOUT_S and generate_timeout_s
on cpu, cuda and mps. A later backend change that outgrows the MCP wait
now fails a test. A new case covers a CPU budget larger than the GPU one.
Review follow-up. The shared loader resamples soundfile-readable audio to
16 kHz with a linear interpolation, which can alias 44.1/48 kHz input into
Whisper's band. PyTorch Whisper now reads such audio at its native rate,
as before, so the pipeline's band-limited resampler converts it. Only
files soundfile can't open (MP4/M4A) take the ffmpeg path, which resamples
properly. New tests: a 48 kHz stereo WAV reaches the pipeline at 48 kHz
without ffmpeg, a POST /transcribe with an .m4a returns 200, and the MCP
tool names M4A bytes .m4a.
The CUDA preflight demanded 5.0 GB of free VRAM for every model, sized for
full large-v3. The default model is large-v3-turbo, which the pipeline
loads in fp16: about 1.6 GB of weights, not the 3.2 GiB fp32 figure in the
old comment. So a 6 GB card with nothing else resident reported 5.0 GB
free and was sent to CPU every time, although CUDA ran the same audio in
37 s against minutes on CPU.
The budget is now fp16 weights + 1.5 GB workspace (batch 16) + 0.5 GB
headroom, per model and capped at the old 5.0 GB. That is 3.6 GB for turbo
and 5.0 GB for full large-v3 and any unrecognised model. Both CPU-fallback
warnings name the model and the OMNIVOICE_ASR_VRAM_PREFLIGHT=0 opt-out.
The engine doc lists the budgets.
Fixes#2041
The MCP tools gave up on a backend POST after OMNIVOICE_MCP_TIMEOUT_S,
default 120 s, while the backend's own budgets run longer: 300 s for ASR,
and 300 s or more for generation. A transcription the backend would have
finished came back as an empty client-side timeout, and the abandoned
job kept holding the worker. The docstring said the timeout followed
OMNIVOICE_GENERATE_TIMEOUT_S, but it never read it.
Unset, each tool now waits for the backend's budget plus 30 s, never less
than 120 s. transcribe follows OMNIVOICE_ASR_TRANSCRIBE_TIMEOUT_S, and
generate_speech follows the larger of the GPU and CPU generation budgets,
scaled by text length like the backend. An explicit
OMNIVOICE_MCP_TIMEOUT_S still wins. docs/mcp.md says so.
Fixes#2040
PyTorchWhisperBackend.transcribe read uploads with soundfile, and
libsndfile cannot open MP4/M4A (AAC). /transcribe and the MCP tool both
accept .m4a, so every such upload failed with "Format not recognised"
before ASR ran. It now uses _load_audio_16k_mono_f32, the loader the
sherpa backend already uses: soundfile first, then the validated ffmpeg
path, returning the 16 kHz mono float32 Whisper expects.
Fixes#2039
The floors test checked only uv.lock, and pyproject still allowed
transformers>=5.5.0 and protobuf>=4.25. A fresh resolution, or an install
without the lock, could still pick a release below the advisory fixes.
The manifest now requires transformers>=5.10.0 and protobuf>=5.29.6. The
lock already resolved 5.15.1 and 7.36.0, so no package version changes;
only the recorded requirements do.
The drain thread took its buffer from spawn but read self._proc when it
started, so a drain that started late could read a replacement process's
stderr into the old buffer. Spawn now passes the process and its buffer
together. Tests cover a late drain and the error-frame branch of the ready
diagnostics.
test_blocked_reconnect_persistence_does_not_stall_another_worker failed on
a Windows runner (#2020's Smoke job) when its final wait for the Control
stream to end expired at 2s. That wait only proves the stream ends: after
both releases, the real disconnect path persists all 64 queued tasks, and
a slow runner took longer. The assertions that matter, another worker's
heartbeat and prewarm staying under 0.2s while reconciliation is blocked,
are untouched.
The three waits that only bound a Control stream's exit now share
_CONTROL_EXIT_TIMEOUT_S = 10s. A stream that never ends still fails.
A shared deque cleared on spawn could still receive late lines from the
previous process's drain thread, and a new start-up failure would quote
them. Each spawn now gets a fresh buffer passed to its own drain thread.
The app's own lock already resolves transformers 5.15.1 and protobuf
7.36.0, but PYTHON_FLOORS still allowed transformers 5.5.0 and did not
pin protobuf. A later lock change could have slipped below the fixes for
the advisories Dependabot raised on CosyVoice's manifest (#2030, #2031).
A sidecar that failed its ready handshake reported "did not signal ready:
None" for the two most likely causes. _recv returns None on EOF, and a
deadline kill closes stdout just like a crash, so the report could not
tell a slow start from a start-up crash. The exit code was never read,
and stderr went only to the log.
The error now names which it was:
- no ready frame within the deadline, so it was stopped;
- it exited with code N before signalling ready;
- it sent the wrong op, or reported an error frame (with its message).
Each ends with the sidecar's last stderr lines, scrubbed of home paths
and secrets and capped at 800 characters. The prefix is unchanged, so
existing matching still works. The echo sidecar gets test-mode hooks to
exit, stall or send the wrong op before ready.
Fixes#2026
A YouTube link that hits "Sign in to confirm you're not a bot" showed
yt-dlp's raw advice to pass --cookies-from-browser or --cookies, which are
CLI flags nobody using VoiceStudio can pass. It now has its own failure
class, VIDEO_DOWNLOAD_BOT_CHECK. Its hint says to export signed-in cookies
as a cookies.txt file and attach it in Dub, or to upload the video
instead. The class is terminal, so it is neither retried as a network drop
nor escalated through player clients like a 403.
Fixes#2034
The rename to "the engine's Weights list in Model Catalogue" left several
messages without a verb, and pointed others at the wrong place:
- The offline and create-voice messages say what to do again.
- pyannote has no owning engine, so diarization points at Other weights.
- The Hugging Face mirror moved to Settings → Network, and voice previews
moved to Settings → Storage.
- Unloading and switching engines happen in the engine list, not a
Weights list.
- A bad saved path points at Settings → Storage or the env file.
- Docstrings that read "the the" are fixed.
The dub stream-drop fallback goes through i18n in all 21 locales. A
Dictation pick on a row that is already downloading no longer starts a
second install: the row's radio is disabled while it works, and
useModelDownloads refuses a second mutation for a repo already in flight.
The Supertonic-3 license test checks for the Accept wording.
The floors came from OSV alone, which did not list the protobuf and
transformers advisories GitHub flagged once the manifest reached main
(Dependabot #2030, #2031). Raise them to the first fixed releases so a
later downgrade fails the test. Note that transformers 5 was checked
against CosyVoice's tokenizer and cached decoding.
XNU stops signalling a process as soon as it starts exiting but posts
NOTE_EXIT later in the same exit. A KILL that lands in that window gets
EPERM while the exit probe still reads "alive", so stopping a child that
TERM had just ended could fail with "Operation not permitted". This
flaked the macOS run of contained_exit_probe_preserves_a_live_child.
The EPERM branch now re-probes for up to 250 ms before treating the error
as a live, unsignalable root. Live roots, reaped roots and probe failures
are still errors.
Conflicts: RecoBanner (deleted here: the recommendation card is gone),
ModelStoreTab + EngineCompatibilityMatrix (this branch's rewrite kept),
supertonic3 backend (main's own-venv check kept, message without the
retired "→ Engines" step), CHANGELOG (base layout + the #2020 line).
Carried over from main and the review:
- The list row hides Install while only the license review is left
(main's #2017 rule, now in the row; Accept lives in the panel).
- Engine action aria labels go through i18n (engines.aria*, all 21
locales) instead of hardcoded English.
- Every "Model Catalogue → Engines/Models" path main added, plus the
frontend strings that still named the retired panes, now point at the
one-page catalogue.
- test_engine_unavailable_reason_1866 reads the license matcher from its
new home, engines/engineDisplay.js.
Conflicts: CHANGELOG (main cut 0.5.2 and opened a new Unreleased; the
#2013 lines move there), pockettts/supertonic3 docs (main's new one-click
install text kept, with the retired "Model Catalogue → Engines →" step
dropped, as in the rest of docs).
Review fixes on top:
- Bulk installs name the right repository on failure: every "install
several" button now pairs allSettled results with their request before
filtering (shared failedInstalls/installFailureMessage + unit test).
- confucius4-tts docs: "The first synthesis triggers".
Bandit B202 flagged the custom filter callable. Links are now left out by
choosing the members, and extractall keeps filter="data", which validates
everything extracted. Same behaviour: the Matcha-TTS tarball's absolute
link is skipped instead of aborting the fetch.
Built the real environment on Windows to check the one-click install.
- The pins upstream carries with published advisories (diffusers,
hydra-core, lightning, modelscope, onnx, protobuf, transformers) are
raised to fixed releases; that exact set installs and passes the
installer's import probe. pyarrow, not needed, is dropped. A test keeps
every pin at or above its advisory fix.
- wetext is dropped with its install-time fetch. Its data exists only on
ModelScope, which rate-limits downloads; a throttled fetch left files
missing while reporting success, so the normaliser failed silently.
CosyVoice reads text as written without it, and nothing reaches
ModelScope at install or synthesis time. The unused post-install hook
goes with it.
- openai-whisper builds from source, and under a long cache path the build
hits Windows' 260-character limit; the install failure now says to turn
on long-path support.
The stdlib data filter raises on a link to an absolute path, and the
pinned Matcha-TTS tarball ships one (data -> its author's training
folder), which aborted the fetch and with it the CosyVoice install. Found
by building the real environment. Links are now skipped on every
interpreter, as the pre-3.11.4 path already did.
The guard now proves the job exits on a missing platform before it
rewrites the notes or publishes, so removing that exit fails CI.
RELEASING.md's end-to-end updater test still said to publish the draft by
hand; the workflow publishes it.
When OMNIVOICE_COSYVOICE_MODEL pointed at a folder that no longer existed,
the sidecar quietly loaded the installed CosyVoice 3 model instead, so it
spoke with a model and voice the user had not chosen while
model_identity() still named theirs. It now fails with a message naming
the path and how to fix or clear it.
Each build leg appended its checksums to the shared release notes with
softprops/action-gh-release. Two consequences, both seen on v0.5.2:
- The appends were concurrent read-modify-writes, so a leg that read the
notes before another wrote them lost its section. v0.5.1 and v0.5.2
both shipped without the macOS Apple Silicon checksums in the notes,
though the SHA256SUMS file was attached.
- softprops defaults to draft: false, so the first leg to finish
published tauri-action's draft while the others were still building.
v0.5.2 went public at 17:27; its complete latest.json landed at 17:38.
Legs now only attach their SHA256SUMS file with gh release upload. A new
release-notes-checksums job runs once after the matrix, the updater-manifest
repair and the uninstall scripts, writes all four platforms' checksums in
matrix order, fails if one is missing (leaving the release a draft), and
then publishes it. contributors-strip edits the notes after it, so the
notes have one writer at a time. A test pins all of that.
docs/RELEASING.md described a manual publish and .exe / .deb / .nsis.zip
artifacts the workflow no longer builds; it now matches what ships.
The one-section-per-version check matched raw heading text, so [v0.5.2]
and [0.5.2] would pass as two versions while core.changelog, which serves
the in-app notes, strips the v and whitespace and reads them as one. The
check now uses parse_changelog itself.
engine_venv_python() accepted any venv interpreter, so a reinstall that
failed partway made the resolver pick the sidecar and report it ready,
hiding the working in-process engine. It now also requires the completion
marker, which the import probe writes only after the engine imported in
that venv and which a new dependency step removes. That covers VoxCPM2
here and PocketTTS / Supertonic-3 from #2016.
The parent reads sidecar PCM at the engine's fixed 48 kHz, so the VoxCPM2
sidecar resamples a model that reports another rate instead of passing its
samples through mislabelled. The retry test now proves a permanent error
runs once.
Cut the Unreleased notes as 0.5.2 (2026-09-10) and fold in the untagged
0.5.2 section prepared on 2026-09-02, so release.yml publishes one
complete section. A test now requires one section per version.
The #1428 guard pins the exact set of sidecars it scans, and
docs/STRUCTURE.md must list every engine adapter; both failed CI on the
new engines/voxcpm2_subprocess.
CosyVoice ran only in-process, which needed it importable from the app's
own interpreter; upstream's setup (its own Python 3.10 environment, pins
that clash with the app's) never gives that, so the engine was unusable
from an installer build. The one-click installer now clones a reviewed
commit (074ca6dc) and its Matcha-TTS submodule (dd9105b3) into
DATA_DIR/engines/cosyvoice/, builds a Python 3.10 venv, downloads the
CosyVoice 3 weights, and a self-contained sidecar runs the model there.
Upstream's requirements cannot be used as they are: they add a
third-party Azure DevOps index, pin torch 2.3.1 (CUDA 12.1 only, no
RTX 50-series), pull TensorRT and DeepSpeed on Linux, and do not even
resolve together (fastapi). A trimmed list ships with the app
(engines/cosyvoice_subprocess/requirements.txt, which says what was
dropped and why); torch comes from the per-host 2.7.0 pins. Every package
installs from a wheel except two pure-Python ones, so nothing needs a
compiler, and SoX is not used.
The installer gains what CosyVoice needs, generally:
- ExtraSource fetches a pinned submodule tree, which neither a depth-1
clone nor GitHub's tarball includes;
- weights_allow_patterns downloads only the files the model loads
(5.4 of 9.8 GB);
- post_install_code runs an optional fetch in the engine's venv: here,
wetext's normalisation data, which it otherwise downloads from
ModelScope on every model load. A failure only leaves that to first use.
- The completion marker now applies to engines with weights too, since
weights left by an earlier run do not prove the dependency step
finished. IndexTTS keeps its weights check, so no existing install is
asked to reinstall.
The sidecar formats prompts as upstream's v3 examples do, speaks in
upstream's sample voice when there is no reference clip (v3 has no
built-in speakers), and never hands AutoModel a missing folder, which
would start a ModelScope download. The gateway keeps offering the
CosyVoice weights as a plain download to remote workers that run the
in-process engine.
Other suites purge services from sys.modules, so importing
subprocess_backend inside the test can yield a different module than the
one VoxCPM2SubprocessBackend subclassed, and the patch then misses: the
real generate() ran and failed. Patch VoxCPM2SubprocessBackend's own base.
The backend runs inside the app's tree, and the uv processes it starts for
engine installs had no working directory of their own. uv therefore
discovered VoiceStudio's pyproject.toml and applied its [tool.uv]
constraint-dependencies, torch==2.8.0 among them, to each engine's venv.
Resolved that way, MOSS-TTS-v1.5 (torch==2.9.1+cu128) and Confucius4
(torch==2.7.0) are unsatisfiable, so their one-click installs and
bootstraps could never succeed. An engine that pins no torch got the app's
instead of its own, so its venv was not really independent.
uv_subprocess_env, which every one-click install step and every engine
bootstrap already uses, now always sets UV_NO_CONFIG=1. It used to return
None in two cases and let the call inherit the environment; it now always
returns a copy. Region and custom mirrors still apply, because they reach
uv as UV_INDEX_URL. The app's own `uv sync` and the translation installer,
which install into the app's environment on purpose, are unchanged.
The translation uninstall route runs `pip uninstall -y <package>` on the
app's own environment. Two entries made that break something else:
- The LLM engine's package is openai, a core dependency that Settings →
LLM Providers also uses. Unlike Argos, it was not marked builtin, so
uninstalling that engine removed it from the app.
- Google, DeepL, Microsoft and MyMemory share deep_translator. Uninstalling
any one removed it for all four.
The route now asks uninstall_blocker() first. It refuses (400) a package
VoiceStudio itself depends on, read from the installed package metadata so
there is no second list to keep in step, and refuses (409) a package
another engine shares, naming the engines that would stop working. The
openai entry is marked builtin as well, and a test requires every entry
backed by an app dependency to be.
mlx_supported() fails three ways. Only the non-Apple branch is a platform
gap; Apple Silicon whose PyTorch cannot use MPS was left on the generic
line, and the live-host test asserted a platform reason even there, so it
would fail on such a Mac. That case now has its own owned sentence, and
the live test asserts only the branch its host can produce, with literal
cases covering the rest on every machine.