Commit Graph
100 Commits
Author SHA1 Message Date
Palash Debnath 5f3f35f134 Merge remote-tracking branch 'origin/main' into fix/darwin-eperm-exit-settle
# Conflicts:
#	CHANGELOG.md
2026-09-10 13:04:31 -07:00
Palash Debnath 2c83f5c16c Merge pull request #2013 from debpalash/feat/catalogue-one-page
feat(catalogue): one page — setup summary over per-family engines and weights
2026-09-10 13:02:39 -07:00
Palash Debnath a76e743415 docs(changelog): note the Darwin exit-settle fix (#2032) 2026-09-10 13:02:08 -07:00
Palash Debnath 8cf7bebb61 fix(lifecycle): wait for a Darwin exit to register after EPERM
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.
2026-09-10 13:02:00 -07:00
Palash Debnath 5980fce852 Merge pull request #2030 from debpalash/dependabot/pip/backend/engines/cosyvoice_subprocess/protobuf-5.29.6
chore(deps): bump protobuf from 4.25.9 to 5.29.6 in /backend/engines/cosyvoice_subprocess
2026-09-10 12:58:02 -07:00
Palash Debnath 0eaa1bdc3e Merge branch 'main' into feat/catalogue-one-page 2026-09-10 12:34:20 -07:00
Palash Debnath 44d3639c02 Merge pull request #2025 from debpalash/feat/cosyvoice3-isolated
feat(engines): CosyVoice 3 installs in one click into its own venv
2026-09-10 12:33:01 -07:00
Palash Debnath 80ec0d06c9 Merge origin/main into feat/catalogue-one-page; harvest remaining review threads
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".
2026-09-10 12:09:39 -07:00
Palash Debnath 85ce7206b5 fix(engines): keep the literal data filter so tar extraction is visibly validated
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.
2026-09-10 12:02:33 -07:00
Palash Debnath 180371910e fix(cosyvoice): patched dependencies, no wetext download, Windows long-path hint
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.
2026-09-10 11:39:50 -07:00
Palash Debnath efdec50081 fix(engines): a source tarball with a link in it still extracts
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.
2026-09-10 11:34:03 -07:00
Palash Debnath 655db866d1 Merge pull request #2029 from debpalash/fix/release-checksum-race
fix(release): one job writes the notes and publishes, after every platform
2026-09-10 11:31:19 -07:00
Palash Debnath 7c1599b73a test(release): pin the missing-checksum gate; docs drop the manual publish step
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.
2026-09-10 11:08:10 -07:00
Palash Debnath 399b9ec3f6 Merge remote-tracking branch 'origin/main' into feat/cosyvoice3-isolated 2026-09-10 11:06:04 -07:00
Palash Debnath b804a6f23f Merge pull request #2022 from debpalash/feat/moss-tts-nano-isolated
feat(engines): MOSS-TTS-Nano installs into its own venv, pinned to a working upstream
2026-09-10 11:05:21 -07:00
Palash Debnath 9561d41fb6 fix(cosyvoice): a missing model-folder override is an error, not a silent swap
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.
2026-09-10 10:51:13 -07:00
Palash Debnath 6305f577e1 docs(changelog): link the release-publish entry to #2029 2026-09-10 10:49:16 -07:00
Palash Debnath dd1dce75cb fix(release): one job writes the notes and publishes, after every platform
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.
2026-09-10 10:49:08 -07:00
Palash Debnath 9c925b17a0 Merge remote-tracking branch 'origin/main' into feat/moss-tts-nano-isolated 2026-09-10 10:38:43 -07:00
Palash Debnath 30c64dc44b Merge pull request #2021 from debpalash/feat/voxcpm2-isolated
feat(engines): VoxCPM2 installs into its own venv and runs in a sidecar
2026-09-10 10:37:43 -07:00
Palash Debnath e8be51f751 Merge remote-tracking branch 'origin/feat/moss-tts-nano-isolated' into feat/cosyvoice3-isolated
# Conflicts:
#	CHANGELOG.md
2026-09-10 10:13:53 -07:00
Palash Debnath 919116dfa5 Merge remote-tracking branch 'origin/feat/voxcpm2-isolated' into feat/moss-tts-nano-isolated
# Conflicts:
#	CHANGELOG.md
2026-09-10 10:13:21 -07:00
Palash Debnath 9dd005eab5 docs(changelog): list VoxCPM2 under Unreleased, not the released 0.5.2
Merging main placed the entry beside its old neighbour, which now sits in
the released [0.5.2] section.
2026-09-10 10:11:47 -07:00
Palash Debnath 8a497aa4db Merge remote-tracking branch 'origin/main' into feat/voxcpm2-isolated 2026-09-10 10:10:37 -07:00
Palash Debnath 38c2405fa1 Merge pull request #2007 from debpalash/release/v0.5.2
docs(changelog): cut the 0.5.2 release section
2026-09-10 10:09:20 -07:00
Palash Debnath 97b153df3a test(changelog): count versions the way the app's changelog parser reads them
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.
2026-09-10 09:47:54 -07:00
Palash Debnath 6b62866fb5 test(cosyvoice): its fake venv carries the completion marker 2026-09-10 09:46:21 -07:00
Palash Debnath ab565e9b5c Merge remote-tracking branch 'origin/feat/moss-tts-nano-isolated' into feat/cosyvoice3-isolated 2026-09-10 09:46:02 -07:00
Palash Debnath 594daf9251 test(moss-tts-nano): its fake venv carries the completion marker 2026-09-10 09:45:53 -07:00
Palash Debnath 8114132a72 Merge remote-tracking branch 'origin/feat/voxcpm2-isolated' into feat/moss-tts-nano-isolated 2026-09-10 09:45:33 -07:00
Palash Debnath 63a1c1791a fix(engines): an own-venv engine needs a finished install, and VoxCPM2 PCM is 48 kHz
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.
2026-09-10 09:45:20 -07:00
Palash Debnath 4e37e21745 chore(release): v0.5.2
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.
2026-09-10 09:42:08 -07:00
Palash Debnath 3bfdeeddba Merge pull request #2024 from debpalash/fix/uv-no-app-config
fix(engines): engine installs never inherit VoiceStudio's own uv config
2026-09-10 09:42:02 -07:00
Palash Debnath be722ff608 test: register the CosyVoice sidecar with the stdout guard and STRUCTURE.md 2026-09-10 09:22:24 -07:00
Palash Debnath 0b7117b833 Merge remote-tracking branch 'origin/feat/moss-tts-nano-isolated' into feat/cosyvoice3-isolated 2026-09-10 09:22:06 -07:00
Palash Debnath a88c39eac1 test: register the MOSS-TTS-Nano sidecar with the stdout guard and STRUCTURE.md 2026-09-10 09:21:58 -07:00
Palash Debnath 3f19b2e9a7 Merge remote-tracking branch 'origin/feat/voxcpm2-isolated' into feat/moss-tts-nano-isolated 2026-09-10 09:21:40 -07:00
Palash Debnath 172a142825 test: register the VoxCPM2 sidecar with the stdout guard and STRUCTURE.md
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.
2026-09-10 09:21:32 -07:00
Palash Debnath ac35351000 Merge remote-tracking branch 'origin/main' into fix/uv-no-app-config
# Conflicts:
#	CHANGELOG.md
2026-09-10 09:11:59 -07:00
Palash Debnath f9da18842a docs(changelog): link the CosyVoice entry to #2025 2026-09-10 09:10:09 -07:00
Palash Debnath c4206ee1aa Merge remote-tracking branch 'origin/feat/moss-tts-nano-isolated' into feat/cosyvoice3-isolated 2026-09-10 09:09:41 -07:00
Palash Debnath ec1529479d feat(engines): CosyVoice 3 installs in one click into its own venv
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.
2026-09-10 09:09:37 -07:00
Palash Debnath 61a5418c6a Merge remote-tracking branch 'origin/feat/voxcpm2-isolated' into feat/moss-tts-nano-isolated 2026-09-10 09:08:38 -07:00
Palash Debnath b28390af30 test(voxcpm2): patch the base class the engine really inherits
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.
2026-09-10 09:07:59 -07:00
Palash Debnath 764fed09bc Merge pull request #2018 from debpalash/fix/license-reason-passthrough
fix(engines): say when a license or the platform is what blocks an engine
2026-09-10 09:04:07 -07:00
Palash Debnath 4fdde15079 Merge pull request #2023 from debpalash/fix/translation-uninstall-guard
fix(translation): uninstalling an engine never removes what others need
2026-09-10 09:00:07 -07:00
Palash Debnath 14a8a9cd46 docs(changelog): link the uv config entry to #2024 2026-09-10 08:46:57 -07:00
Palash Debnath 1af1ecc5bd fix(engines): engine installs never inherit VoiceStudio's own uv config
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.
2026-09-10 08:46:49 -07:00
Palash Debnath e0879027d5 Merge remote-tracking branch 'origin/main' into fix/license-reason-passthrough
# Conflicts:
#	CHANGELOG.md
#	tests/test_engine_unavailable_reason_1866.py
2026-09-10 08:41:45 -07:00
Palash Debnath 5298c8e5e1 Merge pull request #2016 from debpalash/feat/isolated-engine-installs
feat(engines): isolated one-click installs for five engines
2026-09-10 08:38:39 -07:00
Palash Debnath 1de4fedad7 docs(changelog): file the translation guard under Fixed, not Highlights 2026-09-10 08:33:55 -07:00
Palash Debnath c5f32d020a fix(translation): uninstalling an engine never removes what others need
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.
2026-09-10 08:26:53 -07:00
Palash Debnath f5ea363724 fix(engines): name the missing MPS on Apple Silicon, and hold the MLX test on every host
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.
2026-09-10 08:22:20 -07:00
Palash Debnath 4280157407 docs(changelog): link the MOSS-TTS-Nano entry to #2022 2026-09-10 08:21:06 -07:00
Palash Debnath 240a64f086 feat(engines): MOSS-TTS-Nano installs into its own venv, pinned upstream
The in-process MOSS-TTS-Nano engine needs moss_tts_nano installed into the
app's own environment, with upstream's exact pins (torch 2.7.0,
transformers 4.57.1) landing there too. It also cannot work with today's
upstream: moss_tts_nano now exports only __version__, and the model class
the engine looks for is gone. The entry point is the top-level
moss_tts_nano_runtime.NanoTTSService.

The one-click installer now clones a reviewed commit (8b7bcc93,
2026-09-06) into DATA_DIR/engines/moss-tts-nano/ with its own venv. A
self-contained sidecar drives NanoTTSService there: it preloads the model,
heartbeats through the downloads (the model on load, the audio tokenizer on
the first synthesis) and never after, downmixes to mono as the in-process
engine did, resamples to the reported 48 kHz if needed, and reuses one
output file instead of leaving one per call.

The resolver becomes a table (_OWN_VENV_SIDECARS) now that two engines use
it, with a test that each entry's env var is the one its installer sets.
The three identical Intel-Mac gates become one factory.
2026-09-10 08:20:58 -07:00
Palash Debnath 388d554ba0 docs(changelog): link the VoxCPM2 entry to #2021 2026-09-10 08:13:57 -07:00
Palash Debnath 21105b24a5 feat(engines): VoxCPM2 installs into its own venv and runs in a sidecar
VoxCPM2 ran only in-process, so using it meant installing voxcpm, and a
torch of its choosing, into the app's own environment. It now has a
one-click install into DATA_DIR/engines/voxcpm2/.venv and a self-contained
sidecar (engines/voxcpm2_subprocess) that imports nothing from the app.
tts_backend resolves the voxcpm2 id to the sidecar once that venv exists,
and to the in-process class otherwise, so an existing pip install keeps
working.

voxcpm leaves torch unpinned. Resolved with the CUDA index, that paired
PyPI's newest torch (CPU-only on Windows) with a +cu128 torchaudio, and
uv's --torch-backend fell back to voxcpm 1.5.0 on Windows. A new
torch_pins spec field pins the pair, and the host picks the build: +cu128
on CUDA hosts, +cpu on other Windows and Linux hosts, plain on macOS. Each
was resolved with voxcpm==2.0.3. Not offered on Intel Macs, where torch
2.11 has no build.

The parent keeps the reference-clip preparation and the trailing-silence
trim, so output matches the in-process engine. The sidecar retries a
transient weight download like the app's loader does.
2026-09-10 08:13:57 -07:00
Palash Debnath ae68dd5bfd fix(engines): a half-finished install is repaired, not reported installed
An engine with no weights download counted as installed once its venv
interpreter existed, so a dependency install that died halfway made the
next attempt answer already_installed and the engine failed at its first
import. The import probe now writes a completion marker, and a fresh
dependency step removes the old one. IndexTTS keeps its weights check, so
no existing install is asked to reinstall.

The MOSS bootstrap no longer blames a non-CUDA host for an install
failure; the index is always supplied, and uv's error says what failed.
The PocketTTS and Supertonic guides now say where the sidecar runs, and
the three repository-engine guides say what to do if the first weight
download outlasts the compute-time budget.
2026-09-10 08:12:17 -07:00
Palash Debnath 2fd4cb3caf test(engines): cover dots.tts's own Windows reason in the platform category 2026-09-10 07:53:10 -07:00
Palash Debnath e532c9d8dd fix(engines): say when an engine can't run on this platform
The public reason sanitizer had no category for a host the engine cannot
run on at all, the same gap that hid the license button. "MLX requires
Apple Silicon" and PocketTTS's Intel-Mac reason became the generic
"check installation" line, and "not supported on this platform" became
"isn't installed yet", which an existing test asserted. Each sent people
after an install that could never work.

A platform category, matched after the license and before the install
and file checks, now says the engine doesn't run on this platform and
points at its guide. mlx-audio's "Apple Silicon only" wording is left
out of the markers: it also appears on an M-series Mac when the package
is simply missing, where installing does help.
2026-09-10 07:52:46 -07:00
Palash Debnath 5516750a4f fix(engines): accept Confucius4's requirements.txt-only source layout
Source validation demanded a pyproject.toml in every checkout, and
Confucius4 ships none, so its install could never get past fetching the
source. The manifest file is now per spec. The regression test fabricates
each pinned upstream's real root files.
2026-09-10 07:48:52 -07:00
Palash Debnath c079b721ed feat(engines): Supertonic-3 and PocketTTS install into their own venvs
Both engines ran with the app's interpreter, installed as optional extras
into the app's own environment (`uv sync --extra`). They now get one-click
installs like the sidecar engines: a PyPI-only spec (no source to fetch)
creates DATA_DIR/engines/<id>/.venv and installs the app's own pinned wheel
there, so nothing they install can touch the app or another engine.

Each engine prefers its own venv and falls back to the app's interpreter,
so an existing `uv sync --extra` install keeps working and is never
provisioned over: the spec counts a package found in the app environment
as installed.

PocketTTS installs from PyTorch's CPU index: it never uses a GPU, and
PyPI's Linux torch pulls ~15 NVIDIA packages. It stays unoffered on Intel
Macs, where no usable torch exists.

Supertonic's sidecar loads its constants by path when the revision env var
is absent, instead of importing the engines package, whose __init__
imports the app backend that its own venv does not have.

The Install button is hidden once only the license review stands between
the user and the engine. The installer tests' autouse fixture now removes
every spec's env var on teardown: a bare delenv of an unset var restored
nothing, and a persisted path leaked into later suites.
2026-09-10 07:44:42 -07:00
Palash Debnath 664c9ea4b5 fix(engines): keep the license reason so Supertonic-3 and PocketTTS can be enabled
The Model Catalogue shows an engine's license Accept button only when its
reason matches /license not accepted/i. public_backends() replaces probe
text with owned sentences, and no category covered a license gate, so the
reason arrived as the generic "Engine unavailable" line and the only way
to enable Supertonic-3 or PocketTTS never rendered (#2017).

A license category, matched first, keeps those words. The test reads the
regex out of EngineCompatibilityMatrix.jsx, so a wording change on either
side fails CI instead of silently hiding the button.
2026-09-10 07:34:06 -07:00
Palash Debnath faa3d39836 feat(engines): isolated one-click installs for MOSS-TTS-v1.5, Confucius4 and dots.tts
Three engines that shipped as terminal-only setups now install from Model
Catalogue → Engines with the existing sidecar installer, which is
generalised to take a per-engine venv interpreter, install target, import
probe and host gate.

Each engine gets DATA_DIR/engines/<id>/ with its own checkout and .venv;
every uv pip install passes --python for that venv, never the app's
interpreter. Switching the active engine only changes a pref, so moving
between engines and back cannot corrupt a working one, and uninstalling one
removes only its own folder. Tests pin both invariants for every spec.

MOSS-TTS-v1.5's [torch-runtime] extra pins torch==2.9.1+cu128, which exists
only on PyTorch's index, so its manual install and its bootstrap could never
resolve (#2015). core.torch_indexes defines the index once for the
installer and the bootstrap, and a test ties it to the app's own
pytorch-cuda index.

Install buttons appear only where the install can work: MOSS on CUDA hosts,
dots.tts off Windows (upstream publishes no Windows install). A direct POST
on an unsupported host gets a 409 with the reason. An engine with no
one-click install now points at its guide, not at a page with no Install
button.
2026-09-10 07:28:42 -07:00
Palash Debnath b0cae840c4 Merge pull request #2010 from debpalash/fix/pill-visibility-desync
fix(dictation): pill window, card and stale model on Windows (#2009, #2012)
2026-09-10 07:18:22 -07:00
Palash Debnath a7cfe288cb fix(catalogue): harvest review findings on #2013
- SetupSummary: an installed engine whose routing is "unavailable" reads
  Needs setup, not Ready (select is refused for it too); a failed /engines
  or /dictation/models fetch renders as an error with Retry instead of
  posing as "Off" / "Needs setup".
- Bulk installs (summary, model store, recommendation card) wait for every
  request to settle before re-enabling, and report which repos failed —
  one early rejection can no longer re-arm the button mid-flight.
- The weights list stays mounted across family switches (hidden under LLM)
  so download progress and Retry/Dismiss state survive navigation.
- Settings search: Hugging Face mirror terms route to Network; the legacy
  "models" tab id resolves to Storage. "Manage models" opens the TTS tab.
- Locales: uk "Рушії", zh-TW "引擎", vi "Engine" for the Engines heading.
- Docs name the family tab wherever the instruction depends on it.
2026-09-10 07:08:43 -07:00
Palash Debnath 566aad7bba fix(dictation): mirror the widget's shadow setting in the macOS overlay
Tauri applies platform config as a JSON Merge Patch, so the windows array in
tauri.macos.conf.json REPLACES the base one rather than merging window objects.
desktopWindowConfig.test.js pins that every base window property survives on
macOS, and it caught the base gaining "shadow": false without the overlay.

The programmatic builder already sets shadow(false) on every platform; this
keeps the macOS declaration in step with it. The Linux and Windows overlays
declare no widget window, so the base entry applies there unchanged.
2026-09-10 06:53:49 -07:00
Palash Debnath 204a7294ca docs(changelog): one-page Model Catalogue (#2013) 2026-09-10 06:46:49 -07:00
Palash Debnath 3cae853440 feat(catalogue): one page, one axis — setup summary over per-family engines and weights
The Model Catalogue put the same decision on two axes: an Engines pane with
TTS/ASR/LLM tabs and a Models pane with TTS/ASR/Dictation/Diarisation
sections, dictation shown in both, plus storage stats, the HF token and the
voice-preview toggle parked on the model list. Settings → Voice still carried
Engines and Models entries that only pointed back here.

Now the page reads top-down: a SetupSummary (speech, transcription,
dictation, language model — engine, device, one status word, Change), the
engine list for one family, and that family's downloadable weights under it
(TTS under TTS; offline ASR, streaming dictation and diarisation under ASR;
nothing for LLM, whose engines bring their own). One storage line points at
Settings → Storage.

- ModelStoreTab takes a `family` and scopes sections and the recommendation
  preset to it (scopeReco); stats strip, HF-token toolbar and previews
  panel removed from it.
- Settings: Engines/Models categories and CataloguePointer removed; models
  directory → Storage, HF mirror → Network (both restart-flagged), voice
  previews → Storage. "Manage models" in disk usage opens the catalogue.
- Store: openCatalogue takes a family (pane key tolerated, ignored);
  pendingCatalogueTab gone.
- Engine matrix title is now the locale's plain "Engines".
- i18n: catalogue.* summary keys in all 21 locales; pane/pointer keys dropped.
- Docs: "Model Catalogue → Engines" is "Model Catalogue"; "→ Models" is
  "→ Downloaded weights".
2026-09-10 06:46:09 -07:00
Palash Debnath e9965b2cb4 fix(dictation): never show the pill natively when Tauri's show fails
Greptile on #2010: if win.show() fails while hwnd() succeeds, the native
SW_SHOWNOACTIVATE show still ran, putting an always-on-top window on screen
that Tauri believes is hidden. dismiss() and the idle reconcile then cannot
remove it — the stranded-window bug this PR fixes, reached by a different
door.

The previous commit degraded to the native show on purpose, reasoning that a
visible pill beats none. That was the wrong trade: the tray's red dot already
tells the user they are being recorded, while an unhidable always-on-top window
is left behind for the rest of the session. The native show now runs only
after Tauri's show has succeeded, and the test that pinned the fallback is
flipped to pin its absence.
2026-09-10 06:36:06 -07:00
Palash Debnath 52887ae44d fix(dictation): no card around the pill, and the pill uses your model (#2009, #2012)
Two more defects from the same live report.

The card. Tauri's default window shadow on Windows gives an undecorated
window a 1px white border and, on Windows 11, rounded corners — drawn around
the whole 460x164 pill window, which is far wider than the pill (at most
284px). That is the bordered card framing empty space, visible whether or not
the pill is showing. The widget window now sets shadow(false), in the builder
and in its tauri.conf.json declaration; the capsule draws its own edge in CSS.

The stale model (#2012). The pill runs in its own window with its own store,
created at app start, usually before the backend listens. CaptureWidget
hydrated the dictation prefs once and memoized the promise whether or not the
load worked, and loadDictationPrefs swallowed the failure and marked itself
loaded. So the widget kept the store's seed, sherpa-whisper-tiny, for the
session. The main window checked the model actually picked (Parakeet,
installed) and said ready; the widget asked the server for the seed, and the
server correctly answered that it was not installed.

loadDictationPrefs now reports whether the backend answered. Only a successful
load is kept; a failed one is retried, and a capture start re-reads so a model
chosen in the main window reaches the widget. An in-flight load is shared.

On the same path, the missing-model install toast was called from the widget,
whose window has no <Toaster> in the desktop app, so it rendered nowhere. The
install recommendation now rides the dictation notice to the main window,
which shows the one-click download. The browser build, where the widget lives
inside the main window, keeps its local toast. The pill labels a missing model
as that rather than "Transcription failed: ...", and clamps error text to two
lines instead of spilling a paragraph past the capsule.

Tests: a failed first load is retried instead of pinning the seed, and a model
changed elsewhere is picked up at the next capture — both fail against main's
widget. The notice routes a missing model to the install toast. The
setup-race test now asserts no local toast in Tauri and the notice payload
instead. 91 frontend tests and 256 Rust lib tests pass; typecheck:ci passes.
2026-09-10 06:34:26 -07:00
Palash Debnath b0f1558db0 fix(dictation): tell Tauri the pill is visible, not just Windows (#2009)
Closing the dictation pill on Windows left an empty dark rectangle on screen,
always on top, removable only by quitting the app.

show_pill_noactivate called the raw Win32 ShowWindow(hwnd, SW_SHOWNOACTIVATE)
and never Tauri's own win.show(). The flag is there for a good reason (#982: a
pill that takes foreground makes the dictated text paste into the pill instead
of the user's document), but going straight to Win32 puts the window on screen
behind Tauri's back. Tauri went on believing it was hidden, and every mechanism
that could have removed it was disabled by that one desync:

  - isVisible() answered false while the user was looking at the window;
  - hide() was a no-op on a window Tauri thought was already hidden, so
    dismiss() in CaptureWidget could not remove it;
  - the idle reconcile — the backstop that exists precisely to clean up a
    stranded pill — asks isVisible() first, and concluded there was nothing
    to clean up.

win.show() now runs first, then the native flag. The no-activation behaviour is
carried by the WS_EX_NOACTIVATE style bit that mark_pill_noactivate applies at
creation, which is what makes the Tauri show safe: the style bit, not the show
flag, is what refuses activation. The flag stays as a second line of defence,
since hwnd() can fail and the bit might not have been applied.

Windows only. macOS and Linux already took the win.show() branch.

The ordering is now a function with both shows as parameters, so two tests can
pin it: Tauri's show runs and runs first, and a failing Tauri show still puts
the pill on screen — degrading to the old behaviour beats not showing the user
that they are being recorded.

256 Rust lib tests green; the 10 stranded-pill frontend tests unchanged and
still passing.
2026-09-10 05:57:22 -07:00
Palash Debnath dea884a22d Merge pull request #2006 from debpalash/fix/worker-artifact-id-posix
fix(worker): identify a staged input the same way on every OS (#2005)
2026-09-10 05:31:39 -07:00
Palash Debnath bee9fa25ff Merge remote-tracking branch 'origin/main' into fix/worker-artifact-id-posix
# Conflicts:
#	CHANGELOG.md
2026-09-10 05:02:02 -07:00
Palash Debnath 06c15ce37f fix(worker): identify a staged input the same way on every OS (#2005)
A staged task input's artifact id was built with os.path.join, so a Windows
control plane produced `inputs\<sha256>.wav`. That id is not a local path. It
is persisted into remote_tasks.params_json, shipped to remote workers over
gRPC as the identifier for the input they must fetch, and compared against a
later disk sweep to decide whether a staged file is still referenced.

So a Windows host hands a Linux worker `inputs\abc.wav`, where the backslash is
an ordinary filename character and no such file exists. Remote GPU workers are
a shipped feature; this broke them for every Windows control plane. The same
ids also stop matching when an omnivoice_data/ directory moves between
operating systems.

artifact_id_for() makes it canonical POSIX — resolve_within already treats both
separators as structural, so resolution is unchanged. normalize_artifact_id()
covers the upgrade: rows written by the old code carry a backslash, and the
sweeper decides "unreferenced" by comparing ids, so without it an upgraded
install reads every legacy row as garbage and deletes inputs that surviving
tasks still point at.

Two other tests in this run asserted POSIX-only behaviour rather than product
behaviour, and are corrected here too:

  - the durability-barrier test required a directory fsync, which
    _fsync_parent_directory deliberately skips without os.O_DIRECTORY. It now
    gates on that same attribute rather than on the OS name, so the test and
    the code it checks cannot drift apart.
  - the read-only-cache test built its scenario with chmod(0o500), which on
    Windows only toggles a read-only FILE attribute and does not stop a file
    being created inside the directory. It verifies its premise by probing and
    skips when the host writes anyway — which also covers root and anything
    holding CAP_DAC_OVERRIDE, replacing a geteuid check that named only one of
    them.

Then the reason none of this was visible: CI runs tests/ on Linux only. The two
worker suites join the existing Windows step in the smoke matrix. They need no
ffmpeg, so they cost seconds. Verified green on Windows first — 244 tests
across the four suites in that step.

Fails before, passes after, both directions: a staged id containing a
backslash, and a legacy-id input deleted by the sweeper.
2026-09-10 04:51:36 -07:00
Palash Debnath bb9149ce1f Merge pull request #2004 from debpalash/fix/grpc-loop-budget
test(worker): size the loop-responsiveness budget against what it measures
2026-09-10 04:40:54 -07:00
Palash Debnath 4c10e802ab test(worker): size the loop-responsiveness budget against what it measures
main is red. Smoke (Windows) failed on
test_upload_durability_barrier_does_not_block_the_grpc_loop with

    assert 0.20299999999997453 < 0.2

Three milliseconds of scheduling noise on a shared runner, and a red build
that says nothing about the product.

The three tests here prove a blocking filesystem call does NOT stall the gRPC
event loop: they park the call on a barrier and check the loop still ran their
own coroutine promptly. That is a wall clock on shared hardware, so the two
numbers have to be chosen against each other. The discriminator was a 0.5 s
watchdog — a stalled loop could not proceed until it fired — while the budget
sat at 0.2 s. Responsive measured ~0.2. The line was drawn exactly where the
noise lives.

Both numbers are named constants now, with the reasoning next to them:
a 1.5 s hold, a 0.75 s budget. Responsive lands near 0.2, stalled lands at 1.5,
and the line sits between them with room on both sides. The waiter's own cap
moved above the hold too, so a genuinely stalled loop is reported by the budget
assertion that names the problem rather than by a bare TimeoutError.

Verified the assertion is still worth having: with the blocking call made to
stall the loop for real, the test fails. It is a wider net, not a hole.

55 tests in the file, three runs in a row.
2026-09-10 04:15:06 -07:00
Palash Debnath c021fac1d8 Merge pull request #2003 from debpalash/land/2002-inert-badge
feat(pronunciation): badge a stored-but-inert entry in the list (#1949)
2026-09-10 04:05:28 -07:00
Palash Debnath 24e0737e0d fix(pronunciation): badge only the entries the backend calls inert
Both review bots found the same thing independently: `e.type !== 'respelling'`
badged rows the user had switched OFF. A disabled entry is indeed not applied,
but for a reason the toggle already shows — labelling it "not applied yet"
reads as a defect rather than their own choice. `inert_entries_for_language`
excludes disabled rows for exactly that reason, so the badge now matches it.

Not taken: the suggestion to name `ipa` and `cmu` explicitly instead of testing
against respelling. The backend's rule is that everything which is not
respelling is inert today, and mirroring it keeps the two in step. An explicit
list would silently stop badging a notation added later — an entry that saves,
validates, toggles on and quietly does nothing, which is the invisibility #1949
exists to remove. A test pins that direction with an unfamiliar type.

Two tests, one per direction. The disabled case fails without the enabled gate.
2860 vitest tests green.
2026-09-10 03:38:44 -07:00
Palash Debnath 06f7c4af7b feat(pronunciation): badge a stored-but-inert entry in the list (#1949)
Takes the part of #2002 by @utkarsha741 that #1984 did not already cover.

An IPA or CMU row saves, validates and toggles on, and is then dropped before
term matching — Phase 1 only substitutes respelling. #1984 made that visible in
the "Test a sentence" preview, which the user sees only if they run a test. The
entry LIST is where they look at what they have saved, and there it still
looked like every other working row.

So the row carries the same fact: a warning badge next to the type and scope
badges, on IPA and CMU only. Badging a respelling row would be the opposite
lie — those do take effect.

The rest of #2002 is already on main under a different name: it re-added the
backend skip detection and the test-preview line as `skipped_terms`, where
`inert_entries_for_language` and `inert_entries` have shipped since #1984.
Landing that half would have been a second implementation of one behaviour with
two response fields for it.

String added to all 21 locales, not just en, so the badge is not an English
island in a translated panel. Fails before, passes after: with the badge
condition disabled the new test cannot find it. 2858 vitest tests, 529 locale
and CJK guard tests green.
2026-09-10 03:26:44 -07:00
Palash Debnath c9a587ea75 Merge pull request #2000 from debpalash/land/1998-powershell-key
docs(docker): make the PowerShell key URL-safe too (#1998)
2026-09-10 03:21:39 -07:00
Palash Debnath 50b1184df3 Merge pull request #1996 from debpalash/fix/1933-port-holder
fix(backend): name who actually holds port 3900 (#1933)
2026-09-10 02:52:16 -07:00
Palash Debnath b6a0f96e90 Merge pull request #1992 from debpalash/fix/1900-attempt-id
fix(bootstrap): emit an attempt id so the splash stops guessing (#1900)
2026-09-10 02:22:28 -07:00
Palash Debnath 93771a0201 Merge pull request #2001 from debpalash/fix/worker-barrier-flake
test(worker): stop spinning the loop the awaited work needs
2026-09-10 02:22:22 -07:00
Palash Debnath 9a7f5fd7ed Merge pull request #1994 from debpalash/fix/1850-crash-tail
fix(crash): capture the dying backend's last words, not the log so far (#1850)
2026-09-10 01:52:06 -07:00
Palash Debnath e3efe4a39a Merge pull request #1999 from debpalash/fix/windows-backend-tests
test: make the isolated backend session pass on Windows, and gate it there
2026-09-10 01:07:27 -07:00
Palash Debnath 5b842280c8 Merge pull request #1997 from debpalash/fix/1931-blackwell-docs
docs+test: finish the RTX 50-series story (#1931)
2026-09-10 00:56:32 -07:00
Palash Debnath 770fe35da9 Merge pull request #1995 from debpalash/fix/1927-crash-hint
fix(crash): say what the exit code means in the crash details (#1927)
2026-09-10 00:36:16 -07:00
Palash Debnath 924172e012 Merge pull request #1986 from debpalash/fix/1960-name-the-language
fix(dub): name the source language code that was rejected
2026-09-10 00:04:53 -07:00
Palash Debnath 2bf922d9e5 Merge pull request #1993 from debpalash/land/1987-followup
docs(docker): finish the ARM64 Compose guidance (#1987)
2026-09-10 00:00:11 -07:00
Palash Debnath d52a4c89a7 Merge pull request #1991 from debpalash/fix/windows-symlink-tests
test: let a stock Windows checkout run the symlink tests (#1990)
2026-09-09 23:59:58 -07:00
Palash Debnath 525a2809eb Merge pull request #1989 from debpalash/land/1981-structure
docs: refresh STRUCTURE.md and pin its counts to the tree (#1981)
2026-09-09 23:48:11 -07:00
Palash Debnath 5e4f863c4d Merge pull request #1988 from debpalash/land/1987-docker-arch
docs(docker): document the amd64-only images and the ARM64 workaround (#1987)
2026-09-09 23:39:41 -07:00
Palash Debnath 12794f3dae Merge pull request #1985 from debpalash/fix/1773-classic-error-class
fix(errors): carry the backend error class onto a classic 500
2026-09-09 23:39:30 -07:00
Palash Debnath 60b696f37f Merge pull request #1984 from debpalash/fix/1949-phoneme-visible
fix(pronunciation): say when an IPA/CMU entry is stored but not applied
2026-09-09 23:23:18 -07:00
Palash Debnath 0f237a9b25 Merge pull request #1983 from debpalash/fix/1847-bootstrap-log-file
fix(bootstrap): keep the first-run install log after setup finishes
2026-09-09 22:57:53 -07:00
Palash Debnath f905230fd8 Merge pull request #1982 from debpalash/fix/1974-dev-port-ownership
fix(dev): reclaim the port from a backend the app itself left running
2026-09-09 22:39:58 -07:00
Palash Debnath 8e2928ca56 Merge pull request #1980 from debpalash/fix/1858-shortcut-registration
fix(dictation): say when another app already owns the shortcut
2026-09-09 22:22:01 -07:00
Palash Debnath 3013cdc241 Merge pull request #1979 from debpalash/fix/1898-windows-quit
fix(windows): stop reporting every deliberate quit as a crash
2026-09-09 22:04:19 -07:00
Palash Debnath 939bdc7248 Merge pull request #1978 from debpalash/fix/1857-reduced-motion
feat(a11y): add an in-app Reduce motion switch
2026-09-09 21:44:50 -07:00