* feat(engines): one-click IndexTTS-2 sidecar install from Settings → Engines
IndexTTS-2 required four manual terminal steps (git clone, uv venv,
uv pip install -e ., export OMNIVOICE_INDEXTTS_DIR). This turns that into
a guided in-app install:
- backend/services/sidecar_install.py — parametrized sidecar provisioner
(SidecarSpec/SPECS so future sidecar engines are one entry, not another
installer). Resumable background job with step-by-step status: disk-space
preflight (needs-X/have-Y message), source fetch (git clone --depth 1
primary, GitHub tarball fallback when git is absent/fails), dedicated
venv via uv (OMNIVOICE_BUNDLED_UV → PATH resolution; transformers<5
isolation preserved — the parent env is never touched), import-probe
verification, IndexTeam/IndexTTS-2 weights into <checkout>/checkpoints
(where the sidecar actually loads from) via snapshot_download with the
auto-selected/configured HF endpoint + token — no hardcoded
huggingface.co — and persistence of OMNIVOICE_INDEXTTS_DIR (os.environ
for immediate use, prefs.json env.* for the next launch). Idempotent:
partial installs repair, downloads resume, healthy installs (incl. a
user's own clone) report already_installed and are never touched.
- API: POST /engines/{id}/install starts the job, GET
/engines/{id}/install/status polls it, DELETE /engines/{id}/install
removes an app-managed install (loopback-gated; refuses user-managed
clones). list_backends() gains one_click_install.
- Frontend: Settings → Engines shows an Install button on the IndexTTS2
row with per-step progress, live log tail, weight-download %, and
error+remediation; the manual setup snippet is demoted to a collapsed
"Manual install" fallback. All strings via i18n (en.json).
- OMNIVOICE_INDEXTTS_DIR joins the Settings env-var allowlist
(single-sourced from the installer SPECS).
- Docs: docs/engines/indextts.md leads with the one-click flow; manual
steps become the fallback section. CHANGELOG Unreleased entry added.
- Tests: tests/test_sidecar_install.py (24 cases — happy path, disk-space
fail, git-absent/git-failing tarball fallback, partial-install repair,
already-installed/running gating, uninstall safety, spec↔bootstrap
contract, router wiring) + 6 new EngineCompatibilityMatrix RTL cases.
API route snapshot regenerated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(engines): harden the sidecar installer — review findings
- Route namespace: /engines/sidecar/{id}/install — a dynamic
/engines/{id}/install would shadow the literal
POST /engines/sonitranslate/install (engines router registers first);
regression-guarded by test_sidecar_routes_never_shadow_literal_engine_routes.
- Weights completion marker: a killed-mid-download multi-shard weights dir
(config.yaml + plausible shards) no longer passes for healthy; the marker
is written only after snapshot_download returns, so re-runs resume.
- _run_logged: drain thread + proc.wait(timeout) + POSIX process-group kill
— a grandchild holding the stdout pipe can no longer hang the step past
its timeout.
- Job log lock: the status poll's list(deque) copy no longer races the
worker's appends (RuntimeError under active logging).
- Self-heal: a healthy managed install whose env var was lost (prefs wiped)
is re-pointed by start_install instead of reported already_installed
while the engine stays unavailable; legacy bootstrap installs (Probe-2
venv) are trusted via the engine's own probe.
- Single-sourced uv/venv-layout resolution: engines.indextts.bootstrap now
delegates _locate_uv/_venv_python_path to services.sidecar_install.
- Frontend: stable poll interval (keyed on the running-id set, not the
status map), reload on a job that finishes before the first poll,
re-attach to an in-flight job on remount, i18n'd Install aria-label,
manual-install <details> auto-opens on failure, snippet block hoisted
out of the JSX IIFE.
- list_backends: sidecar-installable set hoisted out of the per-engine
loop; exhaustive-shape registry test updated for one_click_install.
- Tests rebind the live services.sidecar_install module per test (other
suites purge sys.modules["services"], which made router tests
order-dependent).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(changelog): fill in the PR ref (#1083)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(security): validated tarball fallback + scanner-clean installer
- The pre-filter= extractall fallback (Python < 3.11.4) now extracts
member-by-member behind the same guards extractall(filter="data")
enforces — regular files/dirs only, no absolute paths, no ../ escapes,
resolved-path containment. Kills the new CodeQL py/tarslip (high) and
Bandit B202 (error) alerts; regression-tested with a malicious tarball
(test_safe_extract_members_blocks_tar_slip).
- snapshot_download tracks the weights repo's default branch on purpose
(same policy as every other model download; artifacts are
checksum-verified by hf_hub) — documented + B615 waived at the call.
- Explanatory comments on the intentional empty-except blocks
(CodeQL py/empty-except notes).
Verified locally: bandit -ll -ii on the module reports 0 MEDIUM+ findings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(engines): address Greptile review — Windows tree kill, prefs write race, poll robustness
- _kill_tree: Windows now uses taskkill /F /T so a git/uv helper spawned by
the timed-out child can't keep writing into the checkout (POSIX already
killed the process group). Unit-tested with os.name patched to nt.
- core/prefs: mutations (set_/delete) are serialized behind a module lock —
the installer worker persisting its env.* key concurrently with a Settings
write could previously drop whichever key saved first (whole-class fix:
every threaded prefs writer, not just the installer). Fail-before/
pass-after: tests/test_prefs_thread_safety.py.
- Matrix polling: at most one in-flight status request per engine (an old
'running' response can no longer land after a newer 'succeeded' and
restart the poller), and four consecutive poll failures drop the stale
snapshot instead of showing "Installing…" and hammering a dead backend
forever.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
7.7 KiB
OmniVoice Studio — IndexTTS-2 Engine
IndexTTS-2 (Bilibili) is OmniVoice's emotion-controlled zero-shot TTS
engine. It runs in its own subprocess + dedicated Python venv with
transformers<5, isolated from the OmniVoice parent process which
pins transformers>=5.3. This isolation is the resolution of
#42 — the
canonical OffloadedCache ImportError that resulted from loading
both libraries inside one Python interpreter.
Install (one-click, recommended)
IndexTTS-2 is not bundled with OmniVoice — the model weights are ~6 GB and the package itself pins a conflicting transformers version. OmniVoice ships with a sidecar runner that loads IndexTTS into an isolated venv on demand, plus a guided installer that provisions everything for you:
- Open Settings → Engines, expand the IndexTTS2 row ("Why unavailable?"), and click Install.
- Watch the step-by-step progress: preflight (uv + disk space), source fetch, isolated venv creation, dependency install, verification, model-weight download (~6 GB, resumable), and configuration save.
- Done — the engine flips to
available: trueimmediately, no restart needed.
What the installer does under the hood (all cross-platform — macOS / Windows / Linux):
- Fetches the IndexTTS source with
git clone --depth 1(or, when git isn't installed, downloads the GitHub source tarball over HTTPS) into OmniVoice's data directory (<data-dir>/engines/indextts2/index-tts). - Creates a dedicated venv inside the checkout with
uv venvand runsuv pip install -e .against it — thetransformers<5isolation is preserved; the parent app's environment is never touched. uv is resolved fromOMNIVOICE_BUNDLED_UV, thenPATH. - Downloads the
IndexTeam/IndexTTS-2weights intocheckpoints/(where the sidecar loads them from), honouring your configured/auto-selected Hugging Face endpoint and HF token. - Persists
OMNIVOICE_INDEXTTS_DIRfor you (in-process for immediate use +prefs.jsonfor the next launch).
Preflight requires roughly 12 GB free disk space (source + venv +
weights, checked before anything is written); the install fails early
with an actionable message otherwise. Re-running the installer is
always safe: it repairs partial installs and resumes interrupted
downloads instead of starting over. An app-managed install can be
removed again with DELETE /engines/sidecar/indextts2/install (a
user-managed clone is never touched).
If you already installed IndexTTS manually (any OmniVoice version),
the installer detects it via OMNIVOICE_INDEXTTS_DIR and reports
already_installed — nothing is re-downloaded or moved.
Manual install (fallback)
The manual flow still works and is what the installer automates. Use it if you want the clone somewhere specific, share one clone across tools, or can't use the in-app installer:
-
Clone the IndexTTS repo on disk:
git clone https://github.com/index-tts/index-tts.git -
Install the editable package into a fresh venv. Use
uv pip install -e .— neveruv sync --all-extras, which would overwrite OmniVoice's lock file withtransformers<5and break the parent process:cd index-tts uv venv .venv uv pip install -e . -
Download the model weights (~6 GB):
hf download IndexTeam/IndexTTS-2 --local-dir=checkpoints -
Set the
OMNIVOICE_INDEXTTS_DIRenvironment variable to the repo root (the directory that containscheckpoints/andpyproject.toml):# macOS / Linux echo 'export OMNIVOICE_INDEXTTS_DIR=$HOME/code/index-tts' >> ~/.zshrc source ~/.zshrc# Windows PowerShell [Environment]::SetEnvironmentVariable("OMNIVOICE_INDEXTTS_DIR","$env:USERPROFILE\code\index-tts","User") -
Restart OmniVoice. IndexTTS-2 will appear in Settings → Engines with
available: trueandisolation_mode: subprocess.
Venv resolution order
OmniVoice probes for a usable IndexTTS Python interpreter in this
priority order (see backend/engines/indextts/bootstrap.py):
${OMNIVOICE_INDEXTTS_DIR}/.venv/— the install dir's own venv. This is what BOTH the one-click installer (which setsOMNIVOICE_INDEXTTS_DIRto its managed checkout) and a manual clone resolve to. Highest priority, so v0.2.7 users who already ranuv pip install -e .get zero migration cost on the upgrade to v0.3.x.backend/engines/indextts/.venv/— OmniVoice's own venv, created on demand by the lazy bootstrap below.- Lazy bootstrap — if neither venv exists, OmniVoice runs
uv venv backend/engines/indextts/.venvanduv pip install --python <python> -e ${OMNIVOICE_INDEXTTS_DIR}on first launch. RequiresOMNIVOICE_INDEXTTS_DIRto be set; raises a clear error otherwise.
The cache marker test
(tests/backend/services/test_indextts_backward_compat.py::test_hf_home_marker_present_after_bootstrap)
proves that the bootstrap path never mutates
$HF_HOME/hub/models--IndexTeam--IndexTTS-2/ — so the 6 GB model
weights survive the upgrade byte-for-byte.
Common errors
IndexTTS-2 venv not found. Set OMNIVOICE_INDEXTTS_DIR ...
You haven't installed IndexTTS yet. Click Install on the IndexTTS2 row in Settings → Engines (recommended), or follow the Manual install steps above.
uv was not found / uv is required to bootstrap the IndexTTS-2 venv but was not found on PATH
Both the one-click installer and the bootstrap path need a working
uv binary (resolved from OMNIVOICE_BUNDLED_UV, then PATH).
Either install uv into your PATH (https://docs.astral.sh/uv/) or
pre-create the venv manually with uv venv and uv pip install -e
as in the manual steps.
Not enough disk space to install IndexTTS-2 ...
The installer's preflight found less free space than the estimated source + venv + weights footprint (plus headroom). The message names the exact numbers; free up space (or move OmniVoice's data directory to a larger volume) and click Install again — it resumes where it stopped.
IndexTTS bootstrap completed but import indextts.infer_v2 still fails
The clone at OMNIVOICE_INDEXTTS_DIR is missing the indextts
package. Verify with:
ls "$OMNIVOICE_INDEXTTS_DIR/pyproject.toml" # should exist
ls "$OMNIVOICE_INDEXTTS_DIR/indextts/" # should exist
If the directory is correct but the import still fails, delete
backend/engines/indextts/.venv/ and re-launch — OmniVoice will
re-bootstrap from scratch.
Why a subprocess?
IndexTTS-2 pins transformers<5. OmniVoice pins transformers>=5.3.
The two cannot share a Python interpreter — at import time, one of
them blows up trying to find a class the other moved or removed (the
canonical failure is OffloadedCache from transformers.cache_utils,
which v5 renamed). Running IndexTTS in its own subprocess + its own
venv lets both libraries coexist in the same OmniVoice session.
This is the structural fix for issue #42; the previous
graceful-degradation wrap (which simply detected the conflict and
disabled IndexTTS) is replaced by a real isolation primitive
(backend/services/subprocess_backend.py::SubprocessBackend, shipped
in Plan 02-01).
License
IndexTTS-2 ships under a custom Bilibili research license — free for
research / non-commercial use. Commercial use requires contacting
indexspeech@bilibili.com. See the upstream
README
for the full terms.
IndexTTS2 runs in a dedicated sidecar venv (it pins transformers<5, which
conflicts with the parent's transformers>=5.3). For why that adds disk and
how uv keeps the cost down, see Engine venvs & disk usage.