Skip to content

Phase 2 Plan 02-03: IndexTTS on SubprocessBackend (closes #42) - #98

Merged
debpalash merged 1 commit into
mainfrom
worktree-agent-a6ba2bc6c3b63348d
May 20, 2026
Merged

Phase 2 Plan 02-03: IndexTTS on SubprocessBackend (closes #42)#98
debpalash merged 1 commit into
mainfrom
worktree-agent-a6ba2bc6c3b63348d

Conversation

@debpalash

@debpalash debpalash commented May 20, 2026

Copy link
Copy Markdown
Owner

Summary

Migrates IndexTTS-2 off the in-process import path and onto the SubprocessBackend primitive shipped in Plan 02-01. Closes #42 with a structural fix — IndexTTS runs in its own subprocess + dedicated venv with transformers<5 while OmniVoice keeps its transformers>=5.3 pin. The two libraries now live in different OS processes and can never collide.

  • New: backend/engines/indextts/ — sidecar package
    • __init__.py — hosts IndexTTS2Backend(SubprocessBackend) and parent-side emotion/duration arbitration
    • main.py — sidecar entrypoint; JSON-stdio loop; lazy IndexTTS2 model load with progress frames
    • bootstrap.py — 3-step venv probe (${OMNIVOICE_INDEXTTS_DIR}/.venvengines/indextts/.venvuv venv + uv pip install -e)
  • Refactor: services.tts_backend — IndexTTS2Backend's in-process body removed (~150 LOC); registry resolves the class lazily via _LazyRegistry + PEP 562 __getattr__ re-export. Breaks the import cycle that arose when both subprocess_backend and tts_backend tried to import each other.
  • Docs: docs/engines/indextts.md — install walkthrough + venv resolution order; linked from is_available()'s unavailable message.

Requirements covered

Req Evidence
ENGINE-02 test_hf_home_marker_present_after_bootstrap proves the HF cache survives bootstrap byte-for-byte. SubprocessBackend's os.environ.copy() forwards HF_HOME / HF_HUB_CACHE / HF_ENDPOINT / HF_TOKEN to the sidecar (verified by test_env_forwarding_to_indextts_sidecar).
ENGINE-03 IndexTTS2Backend is now a SubprocessBackend subclass. test_indextts_no_inprocess_import_attempted proves no import indextts.* ever fires in the parent process.
ENGINE-04 test_coexist_with_omnivoice_in_one_session — OmniVoiceBackend (in-process, transformers>=5.3) and IndexTTS2Backend (subprocess, transformers<5) both serve generate() in the same Python interpreter. The headline #42 closure test.
ENGINE-07 test_venv_probe_prefers_omnivoice_indextts_dir — existing v0.2.7 users with OMNIVOICE_INDEXTTS_DIR + .venv reach a working generation with zero re-download and zero re-install. The cache-marker test asserts the bootstrap never mutates $HF_HOME/hub/models--IndexTeam--IndexTTS-2/.
#42 closure Combination of ENGINE-03 + ENGINE-04 — the conflict no longer exists because the libraries live in different processes.

New artifact paths

  • backend/engines/indextts/__init__.pyIndexTTS2Backend class
  • backend/engines/indextts/main.py — sidecar entrypoint
  • backend/engines/indextts/bootstrap.pyresolve_indextts_venv / is_indextts_installed / INDEXTTS_SIDECAR_SCRIPT
  • tests/backend/services/test_indextts_backward_compat.py (8 tests)
  • tests/backend/services/test_indextts_sidecar.py (17 tests, includes the back-compat regression test)
  • tests/fixtures/mock_indextts_sidecar.py (stdlib-only mock for unit tests)
  • docs/engines/indextts.md
  • .planning/phases/02-engine-isolation-subprocessbackend-indextts-wav-export-dubbi/02-03-SUMMARY.md

Back-compat regression test

tests/backend/services/test_indextts_sidecar.py::test_coexist_with_omnivoice_in_one_session — instantiates both OmniVoiceBackend (in-process import of omnivoice.models.omnivoice) and IndexTTS2Backend (via the mock sidecar fixture), then asserts that calling IndexTTS's generate does not break OmniVoice's is_available(). Pre-Plan 02-03 this would have hit the OffloadedCache ImportError; now the second engine lives in a separate interpreter and the two transformers versions never meet.

Test results

  • Plan-related: 44 passed in 8 s (test_subprocess_backend + test_tts_backend_registry + test_indextts_sidecar + test_indextts_backward_compat)
  • Full suite: 391 passed, 10 skipped, 13 xfailed, 1 xpassed in 57 s
  • Smoke: 4 passed in 2.25 s

Hard constraints honored

  • backend/services/sonitranslate.py unchanged (D1)
  • backend/services/gpu_sandbox.py unchanged (D4)
  • Cross-platform: probe handles Unix bin/python and Windows Scripts/python.exe
  • Zero new Python dependencies in the parent venv
  • Test failures in tests/test_issue_fixes.py (which asserted the old in-process error message) were rewritten to validate the new subprocess contract — no functional regression elsewhere

Deviation from RESEARCH.md sidecar skeleton

The plan's <interfaces> block put IndexTTS2Backend inside backend/services/tts_backend.py. I moved it into backend/engines/indextts/__init__.py to break the subprocess_backend ↔ tts_backend import cycle (the alternative — extracting TTSBackend into a separate module — was a bigger refactor with broader blast radius). The registry uses a _LazyRegistry indirection + PEP 562 __getattr__ so callers can still write from services.tts_backend import IndexTTS2Backend. Details in .planning/phases/02-.../02-03-SUMMARY.md — relevant for the Phase 3 Supertonic-3 author who will follow the same sidecar pattern.

Test plan

  • uv run pytest tests/backend/services/test_indextts_backward_compat.py -x -v → 8/8 pass
  • uv run pytest tests/backend/services/test_indextts_sidecar.py -x -v → 17/17 pass
  • uv run pytest tests/backend/services/test_subprocess_backend.py tests/backend/services/test_tts_backend_registry.py -v → 19/19 pass (no regressions in the SubprocessBackend primitive or the registry surface)
  • uv run pytest tests/smoke/ -q → 4 passed
  • uv run pytest tests/ -q --ignore=tests/manual → 391 passed
  • git diff backend/services/sonitranslate.py → empty (D1)
  • git diff backend/services/gpu_sandbox.py → empty (D4)
  • Manual smoke (not gated): boot the backend with a real IndexTTS clone + populated HF cache → /engines reports available=true, isolation_mode="subprocess" → IndexTTS generate via /api/tts/generate returns audio → backend shutdown reaps the sidecar within 5 s

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • IndexTTS-2 engine now available with emotion control (via emotion vector, audio, or text) and duration support
    • Improved engine isolation to prevent library conflicts
  • Documentation

    • Added IndexTTS-2 setup guide with installation and environment configuration
  • Bug Fixes

    • Resolved issue #42: Library version conflicts with IndexTTS integration

Review Change Stack

Migrates IndexTTS-2 off the in-process import path and onto the
SubprocessBackend primitive shipped in Plan 02-01. Closes issue #42 with
a structural fix — the parent's transformers>=5.3 and IndexTTS's
transformers<5 now live in separate OS processes and can never collide.

* New: backend/engines/indextts/ — sidecar package (__init__.py hosts
  IndexTTS2Backend, main.py is the sidecar entrypoint, bootstrap.py owns
  the 3-step venv probe + lazy uv-based bootstrap).
* services.tts_backend: IndexTTS2Backend's in-process body removed;
  registry resolves the class lazily via a _LazyRegistry indirection +
  PEP 562 __getattr__ re-export. This breaks the import cycle that
  arose when both subprocess_backend and tts_backend tried to import
  each other at module load.
* docs/engines/indextts.md: install walkthrough + venv resolution order
  + common errors (linked from is_available()'s unavailable message).
* tests:
  - test_indextts_backward_compat.py (8) — probe priority, no-spawn
    discipline, HF cache marker preservation (ENGINE-07).
  - test_indextts_sidecar.py (17) — subclass shape, isolation_mode,
    parent-side emotion arbitration (vector/audio/text/description),
    coexist-with-OmniVoice (headline #42 closure), env forwarding.
  - tests/fixtures/mock_indextts_sidecar.py — stdlib-only sidecar
    mimicking the production wire protocol; emits 1 s sine wave.
  - test_issue_fixes.py: two obsolete in-process-conflict tests rewritten
    to assert the new subprocess contract (no indextts.* import in the
    parent).

Hard constraints honored: backend/services/sonitranslate.py and
gpu_sandbox.py are untouched (D1 / D4). Existing v0.2.7 users with
OMNIVOICE_INDEXTTS_DIR and a populated HF cache reach a working
generation with zero re-download and zero re-install.

44 tests pass across the four exercised files. Full suite: 391 passed,
10 skipped, 13 xfailed, 1 xpassed in 57 s. Smoke: 4 passed.

Closes #42. Requirements: ENGINE-02, ENGINE-03, ENGINE-04, ENGINE-07.

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

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR implements subprocess isolation for the IndexTTS-2 TTS engine to resolve incompatible transformers version requirements: OmniVoice requires >=5.3 while IndexTTS requires <5. The solution runs IndexTTS in a dedicated subprocess with its own venv, eliminating in-process import collisions while preserving the engine's emotion-control capabilities through parent-side parameter arbitration.

Changes

IndexTTS Subprocess Backend Architecture

Layer / File(s) Summary
Bootstrap venv resolution system
backend/engines/indextts/bootstrap.py
Introduces cached venv discovery that probes user's OMNIVOICE_INDEXTTS_DIR clone first, then package's own venv, then bootstraps with uv venv and uv pip install -e. Exports functions for cache invalidation, installation checks, and venv path resolution with bounded timeout for import verification.
Parent-side IndexTTS2Backend with emotion arbitration
backend/engines/indextts/__init__.py
Implements IndexTTS2Backend(SubprocessBackend) that handles emotion/duration parameter arbitration in parent process before delegating to sidecar. Validates ref_audio requirement, prioritizes emotion inputs (emo_vector > emo_audio > emo_text), converts duration to target tokens, and forwards allowlisted kwargs only.
Sidecar entry point, wire protocol, and lazy model loading
backend/engines/indextts/main.py
Sidecar subprocess implementing length-prefixed JSON wire protocol matching SubprocessBackend framing. Lazy-loads IndexTTS2 model on first request with progress reporting (0/50/100%), handles per-request ops (ping/synthesize/shutdown), provides WAV-to-PCM-base64 conversion, and structured error handling with graceful cleanup.
Registry integration and import cycle fix via lazy PEP 562 hook
backend/services/tts_backend.py
Eliminates import cycle between services.tts_backend and services.subprocess_backend by moving IndexTTS2Backend implementation to engines.indextts and re-exporting it via _LazyRegistry dict and module-level __getattr__(). Preserves legacy import behavior while deferring actual import until first access.
User-facing setup and troubleshooting guide
docs/engines/indextts.md
Comprehensive documentation covering installation steps (clone, venv bootstrap, model download), venv/interpreter resolution order with lazy bootstrap fallback, common bootstrap/import failures with shell remediation, design rationale for subprocess isolation, and licensing information.
Mock sidecar test fixture with protocol matching
tests/fixtures/mock_indextts_sidecar.py
Stdlib-only mock sidecar implementing length-prefixed JSON protocol matching production sidecar. Provides framing helpers, deterministic 440 Hz sine-wave PCM generation, and op dispatch (ping/synthesize/shutdown/probe_env) with structured error handling for testing without real model/dependencies.
Bootstrap venv probe/resolution test suite
tests/backend/services/test_indextts_backward_compat.py
Tests venv probe priority (user clone vs engines venv), fallback behavior, bootstrap path when neither exists (stubbing uv), spawn discipline (no subprocess for existence checks), HF cache marker survival, and caching behavior (no re-probe on subsequent calls).
Sidecar subprocess integration and emotion arbitration tests
tests/backend/services/test_indextts_sidecar.py
Validates IndexTTS2Backend as subprocess-isolated backend: subclass/marker guarantees, is_available() without spawning children, registry integration with subprocess isolation mode, end-to-end generate() round-trips, emotion arbitration rules (emo_vector priority, emo_text alpha capping, emo_audio forwarding, description mapping), ref_audio validation, issue #42 coexistence regression, environment forwarding, and source-level invariants.
Issue #42 regression tests for transformers isolation
tests/test_issue_fixes.py
Updates test suite to validate IndexTTS2Backend.is_available() never attempts in-process indextts imports and returns actionable error messages pointing to install docs/venv setup instead of transformer-conflict wording.
Phase 02-03 execution summary and design decisions
.planning/phases/.../02-03-SUMMARY.md
Planning document recording subprocess isolation outcome, test coverage, implementation decisions (import cycle resolution, lazy registry with PEP 562, parent-side emotion arbitration, allowlisted kwargs), venv probe order, mock sidecar protocol, threat mitigations, and ENGINE requirement closure mapping.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • debpalash/OmniVoice-Studio#97: Introduces the SubprocessBackend base class and subprocess isolation mode contract that IndexTTS2Backend directly builds upon, including IPC framing, wire protocol, and registry integration patterns.

Poem

🐰 A transformers dance, now in separate rooms,
Parent and child, each with their own looms,
IndexTTS sings in a quiet nook,
While OmniVoice turns every page in the book,
No conflicts, no clashes—just harmony found,
In subprocess isolation, where peace does abound! 🎵

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 59.49% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically describes the main change: migrating IndexTTS-2 to SubprocessBackend to resolve issue #42. It is concise, specific, and accurately reflects the primary objective.
Description check ✅ Passed The PR description is comprehensive and well-structured. It includes a clear summary, itemized changes, type classification (implied as refactor + new feature), testing evidence, checklist verification, and detailed requirement/artifact mapping. All template sections are addressed substantively.
Linked Issues check ✅ Passed The PR directly addresses issue #42 by isolating IndexTTS into a subprocess with its own venv, eliminating the in-process transformers version conflict. The changeset implements SubprocessBackend integration, parent-side emotion arbitration, lazy venv bootstrap, and comprehensive tests validating coexistence (ENGINE-03, ENGINE-04) and backward compatibility (ENGINE-07).
Out of Scope Changes check ✅ Passed All changes are strictly scoped to IndexTTS subprocess isolation and its supporting infrastructure. Core constraints (sonitranslate.py, gpu_sandbox.py, zero new parent dependencies) are honored. No unrelated refactoring or feature creep is present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-agent-a6ba2bc6c3b63348d

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
backend/services/tts_backend.py (1)

1170-1175: 💤 Low value

Unreachable branch and confusing conditional in __getattr__.

The condition on line 1171-1172 is effectively dead code:

  1. _LAZY_REGISTRY keys are engine IDs ("indextts2"), not class names
  2. Users import IndexTTS2Backend (the class name), which is handled by lines 1173-1174
  3. The else None branch on line 1172 is unreachable since _LazyRegistry.__contains__ returns True for any key in _LAZY_REGISTRY

The code works correctly, but could be simplified:

🔧 Proposed simplification
 def __getattr__(name: str):  # pragma: no cover - exercised via tests
-    if name in _LAZY_REGISTRY:
-        return _REGISTRY[name if name in _REGISTRY else None]
     if name == "IndexTTS2Backend":
         return _REGISTRY["indextts2"]
     raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

If future lazy-loaded class names need support, the pattern can be extended explicitly rather than relying on the registry key lookup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/tts_backend.py` around lines 1170 - 1175, The __getattr__
function contains an unreachable/ confusing branch: remove the conditional that
checks name in _LAZY_REGISTRY and the expression returning _REGISTRY[name if
name in _REGISTRY else None]; instead, handle the known class-name alias
explicitly (return _REGISTRY["indextts2"] when name == "IndexTTS2Backend") and
otherwise raise AttributeError. If you want to support future lazy-loaded class
names, add an explicit mapping from class-name strings to registry keys rather
than relying on _LAZY_REGISTRY membership.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/engines/indextts/bootstrap.py`:
- Around line 226-256: The try/except blocks around the two subprocess.run calls
(the "uv venv" block and the "uv pip install" block that uses python_path from
_venv_python_path and indextts_clone) only catch subprocess.CalledProcessError
and will surface raw tracebacks on timeouts; update both exception handlers to
also catch subprocess.TimeoutExpired and raise a RuntimeError with a clear,
actionable message (include which command timed out, the timeout value from
_UV_VENV_TIMEOUT_S or _UV_PIP_INSTALL_TIMEOUT_S, the _ENGINES_VENV_DIR or
indextts_clone context as appropriate, and a link to the docs/help) so timeouts
produce user-friendly errors instead of tracebacks.

In `@docs/engines/indextts.md`:
- Line 99: The heading currently uses nested backticks and is malformed; fix it
by removing the outer backticks and keeping a single inline code span for the
import reference so the heading reads like: use a plain heading text with
`import indextts.infer_v2` (e.g., replace "### `IndexTTS bootstrap completed but
`import indextts.infer_v2` still fails`" with "### IndexTTS bootstrap completed
but `import indextts.infer_v2` still fails"). Ensure only one pair of backticks
surrounds the symbol import indextts.infer_v2 and no extra backticks wrap the
entire heading.
- Around line 24-33: The install docs currently omit required uv mirror/index
and retry settings; update the installation steps around the `uv pip install -e
.` guidance to document the environment variables `UV_PYTHON_INSTALL_MIRROR`,
`UV_DEFAULT_INDEX`, `UV_HTTP_TIMEOUT`, and `UV_HTTP_RETRIES`, show example
export commands, and add region-specific notes: recommend Tsinghua as primary
and Aliyun as fallback for China, and explicitly state that Russia has no
blessed PyPI mirror and users must tunnel via VPN to reach official indexes;
ensure the new text sits immediately before the `cd index-tts` / `uv venv .venv`
block and uses the same plain-shell style as surrounding docs.
- Around line 35-58: Add a short HuggingFace token setup section to the IndexTTS
doc that promotes the in-app Settings token field as the primary method and
documents HF_TOKEN only as an override; mention the Settings field path
explicitly (e.g., "Settings → HuggingFace token") and then show
platform-specific environment variable examples for macOS/Linux and Windows
(PowerShell and CMD) to set HF_TOKEN, and note that OMNIVOICE_INDEXTTS_DIR
remains the repo root containing checkpoints/ and pyproject.toml.

In `@tests/backend/services/test_indextts_sidecar.py`:
- Around line 347-353: The test currently only checks that
OmniVoiceBackend.is_available() boolean (ok) didn't change after IndexTTS ran;
strengthen it to also assert the returned message is unchanged so a different
post-IndexTTS failure message is caught. After calling
OmniVoiceBackend.is_available() the second time (ok2, msg2), assert ok2 == ok
and msg2 == msg (using the existing ok, msg, ok2, msg2 variables) and include a
failure message that shows both old and new values for easier debugging.
- Around line 73-76: The teardown is currently swallowing all exceptions from
backend.shutdown() which hides failures and can leave the sidecar running;
update the try/except around backend.shutdown() to capture the exception (e.g.,
except Exception as e) and surface it instead of silently passing — either log
the error and re-raise, or call pytest.fail with a descriptive message including
e, so failures in backend.shutdown() are visible and tests can't silently leave
processes running.

In `@tests/test_issue_fixes.py`:
- Around line 126-134: The test allows the vague token "conflict" which makes
is_available() messages too permissive; update the assertion in
tests/test_issue_fixes.py (the msg_lower variable and the is_available() failure
check) to remove the "conflict" branch and instead assert only explicit
actionable cues (e.g., keep "omnivoice_indextts_dir",
"docs/engines/indextts.md", "git clone", and ensure an install/venv marker such
as "pip install", "venv" or "virtualenv" is present) so the failure message must
point to install/venv/doc guidance rather than the old "conflict" wording.

---

Nitpick comments:
In `@backend/services/tts_backend.py`:
- Around line 1170-1175: The __getattr__ function contains an unreachable/
confusing branch: remove the conditional that checks name in _LAZY_REGISTRY and
the expression returning _REGISTRY[name if name in _REGISTRY else None];
instead, handle the known class-name alias explicitly (return
_REGISTRY["indextts2"] when name == "IndexTTS2Backend") and otherwise raise
AttributeError. If you want to support future lazy-loaded class names, add an
explicit mapping from class-name strings to registry keys rather than relying on
_LAZY_REGISTRY membership.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2dfe2cac-77b9-45fa-9427-a24726cb7dc1

📥 Commits

Reviewing files that changed from the base of the PR and between 0fc5ea6 and 52c2f98.

📒 Files selected for processing (10)
  • .planning/phases/02-engine-isolation-subprocessbackend-indextts-wav-export-dubbi/02-03-SUMMARY.md
  • backend/engines/indextts/__init__.py
  • backend/engines/indextts/bootstrap.py
  • backend/engines/indextts/main.py
  • backend/services/tts_backend.py
  • docs/engines/indextts.md
  • tests/backend/services/test_indextts_backward_compat.py
  • tests/backend/services/test_indextts_sidecar.py
  • tests/fixtures/mock_indextts_sidecar.py
  • tests/test_issue_fixes.py

Comment on lines +226 to +256
try:
subprocess.run(
[uv, "venv", str(_ENGINES_VENV_DIR)],
check=True,
timeout=_UV_VENV_TIMEOUT_S,
capture_output=True,
)
except subprocess.CalledProcessError as exc:
raise RuntimeError(
f"uv venv failed for IndexTTS bootstrap at {_ENGINES_VENV_DIR}: "
f"{exc.stderr.decode('utf-8', errors='replace') if exc.stderr else exc}"
) from exc

python_path = _venv_python_path(_ENGINES_VENV_DIR)
try:
subprocess.run(
[
uv, "pip", "install",
"--python", str(python_path),
"-e", str(indextts_clone),
],
check=True,
timeout=_UV_PIP_INSTALL_TIMEOUT_S,
capture_output=True,
)
except subprocess.CalledProcessError as exc:
raise RuntimeError(
"uv pip install -e failed during IndexTTS bootstrap "
f"({indextts_clone}): "
f"{exc.stderr.decode('utf-8', errors='replace') if exc.stderr else exc}"
) from exc

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle TimeoutExpired alongside CalledProcessError for clearer error messaging.

Both subprocess.run calls catch CalledProcessError but not TimeoutExpired. If uv venv or uv pip install exceeds the timeout (120s/900s respectively), users see a raw traceback instead of an actionable error message with docs link.

Proposed fix
     try:
         subprocess.run(
             [uv, "venv", str(_ENGINES_VENV_DIR)],
             check=True,
             timeout=_UV_VENV_TIMEOUT_S,
             capture_output=True,
         )
-    except subprocess.CalledProcessError as exc:
+    except subprocess.TimeoutExpired:
+        raise RuntimeError(
+            f"uv venv timed out after {_UV_VENV_TIMEOUT_S}s while bootstrapping "
+            f"IndexTTS at {_ENGINES_VENV_DIR}. This can happen on slow systems. "
+            "See docs/engines/indextts.md for manual install steps."
+        ) from None
+    except subprocess.CalledProcessError as exc:
         raise RuntimeError(
             f"uv venv failed for IndexTTS bootstrap at {_ENGINES_VENV_DIR}: "
             f"{exc.stderr.decode('utf-8', errors='replace') if exc.stderr else exc}"
         ) from exc

     python_path = _venv_python_path(_ENGINES_VENV_DIR)
     try:
         subprocess.run(
             [
                 uv, "pip", "install",
                 "--python", str(python_path),
                 "-e", str(indextts_clone),
             ],
             check=True,
             timeout=_UV_PIP_INSTALL_TIMEOUT_S,
             capture_output=True,
         )
-    except subprocess.CalledProcessError as exc:
+    except subprocess.TimeoutExpired:
+        raise RuntimeError(
+            f"uv pip install timed out after {_UV_PIP_INSTALL_TIMEOUT_S}s while "
+            f"installing IndexTTS from {indextts_clone}. This can happen on slow "
+            "connections or cold caches. See docs/engines/indextts.md."
+        ) from None
+    except subprocess.CalledProcessError as exc:
         raise RuntimeError(
             "uv pip install -e failed during IndexTTS bootstrap "
             f"({indextts_clone}): "
             f"{exc.stderr.decode('utf-8', errors='replace') if exc.stderr else exc}"
         ) from exc
🧰 Tools
🪛 Ruff (0.15.13)

[error] 227-227: subprocess call: check for execution of untrusted input

(S603)


[error] 241-241: subprocess call: check for execution of untrusted input

(S603)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/engines/indextts/bootstrap.py` around lines 226 - 256, The try/except
blocks around the two subprocess.run calls (the "uv venv" block and the "uv pip
install" block that uses python_path from _venv_python_path and indextts_clone)
only catch subprocess.CalledProcessError and will surface raw tracebacks on
timeouts; update both exception handlers to also catch subprocess.TimeoutExpired
and raise a RuntimeError with a clear, actionable message (include which command
timed out, the timeout value from _UV_VENV_TIMEOUT_S or
_UV_PIP_INSTALL_TIMEOUT_S, the _ENGINES_VENV_DIR or indextts_clone context as
appropriate, and a link to the docs/help) so timeouts produce user-friendly
errors instead of tracebacks.

Comment thread docs/engines/indextts.md
Comment on lines +24 to +33
2. Install the editable package into a fresh venv. Use
`uv pip install -e .` — **never** `uv sync --all-extras`, which
would overwrite OmniVoice's lock file with `transformers<5` and
break the parent process:

```bash
cd index-tts
uv venv .venv
uv pip install -e .
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Document restricted-network uv mirror/index settings required by docs policy.

The install section currently omits mirror/index/retry guidance for constrained regions. Please add UV_PYTHON_INSTALL_MIRROR, UV_DEFAULT_INDEX, retry/timeout variables, China mirror guidance (Tsinghua primary, Aliyun fallback), and explicit Russia VPN guidance.

Proposed doc patch
 2. Install the editable package into a fresh venv. Use
    `uv pip install -e .` — **never** `uv sync --all-extras`, which
    would overwrite OmniVoice's lock file with `transformers<5` and
    break the parent process:
+
+   Restricted-network bootstrap (optional, recommended where needed):
+
+   ```bash
+   export UV_PYTHON_INSTALL_MIRROR=https://github.com/astral-sh/python-build-standalone/releases/download
+   export UV_DEFAULT_INDEX=https://pypi.org/simple
+   export UV_HTTP_TIMEOUT=120
+   export UV_HTTP_RETRIES=5
+   ```
+
+   China: prefer Tsinghua mirror, fallback to Aliyun for package index.
+   Russia: no blessed PyPI mirror; use a VPN tunnel to reach official indexes.

As per coding guidelines, docs/**/*.md: “Installation documentation: Document UV_PYTHON_INSTALL_MIRROR and region-specific PyPI mirrors (Tsinghua for China, Aliyun fallback) with explicit guidance that Russia has no blessed PyPI mirror and users should tunnel via VPN”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/engines/indextts.md` around lines 24 - 33, The install docs currently
omit required uv mirror/index and retry settings; update the installation steps
around the `uv pip install -e .` guidance to document the environment variables
`UV_PYTHON_INSTALL_MIRROR`, `UV_DEFAULT_INDEX`, `UV_HTTP_TIMEOUT`, and
`UV_HTTP_RETRIES`, show example export commands, and add region-specific notes:
recommend Tsinghua as primary and Aliyun as fallback for China, and explicitly
state that Russia has no blessed PyPI mirror and users must tunnel via VPN to
reach official indexes; ensure the new text sits immediately before the `cd
index-tts` / `uv venv .venv` block and uses the same plain-shell style as
surrounding docs.

Comment thread docs/engines/indextts.md
Comment on lines +35 to +58
3. Download the model weights (~6 GB). Either:

```bash
hf download IndexTeam/IndexTTS-2 --local-dir=checkpoints
```

or let HuggingFace cache them on first synthesize call (the parent
forwards `HF_HOME` / `HF_HUB_CACHE` to the sidecar so the cache is
shared with the rest of OmniVoice's downloads).

4. Set the `OMNIVOICE_INDEXTTS_DIR` environment variable to the repo
root (the directory that contains `checkpoints/` and
`pyproject.toml`):

```bash
# macOS / Linux
echo 'export OMNIVOICE_INDEXTTS_DIR=$HOME/code/index-tts' >> ~/.zshrc
source ~/.zshrc
```

```powershell
# Windows PowerShell
[Environment]::SetEnvironmentVariable("OMNIVOICE_INDEXTTS_DIR","$env:USERPROFILE\code\index-tts","User")
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add required HuggingFace token setup guidance (Settings-first + env override).

This install flow uses Hugging Face downloads but does not document the required token setup path. Please add a short section that promotes the in-app Settings token field as primary, and documents HF_TOKEN only as an override with macOS/Linux/Windows examples.

Proposed doc patch
+### HuggingFace token setup (required for gated/rate-limited downloads)
+
+Primary (recommended): set your token in OmniVoice UI:
+- **Settings → HuggingFace Token**
+
+Override-only path (advanced): `HF_TOKEN` environment variable.
+
+```bash
+# macOS / Linux
+export HF_TOKEN=hf_xxx
+```
+
+```powershell
+# Windows PowerShell
+[Environment]::SetEnvironmentVariable("HF_TOKEN","hf_xxx","User")
+```
+
+```cmd
+:: Windows CMD
+setx HF_TOKEN hf_xxx
+```

As per coding guidelines, docs/**/*.md: “HuggingFace token setup documentation must include both the in-app Settings field path (promoted) and the environment variable override path (documented but not promoted), with platform-specific examples for macOS, Linux, and Windows”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/engines/indextts.md` around lines 35 - 58, Add a short HuggingFace token
setup section to the IndexTTS doc that promotes the in-app Settings token field
as the primary method and documents HF_TOKEN only as an override; mention the
Settings field path explicitly (e.g., "Settings → HuggingFace token") and then
show platform-specific environment variable examples for macOS/Linux and Windows
(PowerShell and CMD) to set HF_TOKEN, and note that OMNIVOICE_INDEXTTS_DIR
remains the repo root containing checkpoints/ and pyproject.toml.

Comment thread docs/engines/indextts.md
into your `PATH` (https://docs.astral.sh/uv/) or pre-create the venv
manually with `uv venv` and `uv pip install -e` as in step 2.

### `IndexTTS bootstrap completed but `import indextts.infer_v2` still fails`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix malformed inline code in heading (markdownlint MD038).

The heading nests backticks and is parsed incorrectly. Use a plain heading with a single code span.

Proposed doc patch
-### `IndexTTS bootstrap completed but `import indextts.infer_v2` still fails`
+### IndexTTS bootstrap completed but `import indextts.infer_v2` still fails
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### `IndexTTS bootstrap completed but `import indextts.infer_v2` still fails`
### IndexTTS bootstrap completed but `import indextts.infer_v2` still fails
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 99-99: Spaces inside code span elements

(MD038, no-space-in-code)


[warning] 99-99: Spaces inside code span elements

(MD038, no-space-in-code)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/engines/indextts.md` at line 99, The heading currently uses nested
backticks and is malformed; fix it by removing the outer backticks and keeping a
single inline code span for the import reference so the heading reads like: use
a plain heading text with `import indextts.infer_v2` (e.g., replace "###
`IndexTTS bootstrap completed but `import indextts.infer_v2` still fails`" with
"### IndexTTS bootstrap completed but `import indextts.infer_v2` still fails").
Ensure only one pair of backticks surrounds the symbol import indextts.infer_v2
and no extra backticks wrap the entire heading.

Comment on lines +73 to +76
try:
backend.shutdown()
except Exception:
pass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid swallowing fixture teardown failures.

The bare except Exception: pass can hide failed shutdowns and leave sidecar processes alive, which can make later tests flaky.

Proposed fix
     backend = IndexTTS2Backend()
     yield backend
     try:
         backend.shutdown()
-    except Exception:
-        pass
+    except Exception:
+        proc = getattr(backend, "_proc", None)
+        if proc is not None and proc.poll() is None:
+            proc.kill()
+            proc.wait(timeout=5)
🧰 Tools
🪛 Ruff (0.15.13)

[error] 75-76: try-except-pass detected, consider logging the exception

(S110)


[warning] 75-75: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/backend/services/test_indextts_sidecar.py` around lines 73 - 76, The
teardown is currently swallowing all exceptions from backend.shutdown() which
hides failures and can leave the sidecar running; update the try/except around
backend.shutdown() to capture the exception (e.g., except Exception as e) and
surface it instead of silently passing — either log the error and re-raise, or
call pytest.fail with a descriptive message including e, so failures in
backend.shutdown() are visible and tests can't silently leave processes running.

Comment on lines +347 to +353
ok2, msg2 = OmniVoiceBackend.is_available()
# Whatever state OmniVoiceBackend was in before, it's the same
# after IndexTTS ran — no new import errors, no new AttributeErrors.
assert ok2 == ok, (
f"OmniVoice availability changed after IndexTTS generate "
f"({ok}->{ok2}, msg={msg2})"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Strengthen the coexistence regression assertion.

This only asserts availability boolean stability. If OmniVoice is unavailable both times, a different post-IndexTTS error message would be missed.

Proposed fix
     ok2, msg2 = OmniVoiceBackend.is_available()
     # Whatever state OmniVoiceBackend was in before, it's the same
     # after IndexTTS ran — no new import errors, no new AttributeErrors.
     assert ok2 == ok, (
         f"OmniVoice availability changed after IndexTTS generate "
         f"({ok}->{ok2}, msg={msg2})"
     )
+    if not ok:
+        assert msg2 == pre, (
+            "OmniVoice error message changed after IndexTTS generate; "
+            f"before={pre!r}, after={msg2!r}"
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/backend/services/test_indextts_sidecar.py` around lines 347 - 353, The
test currently only checks that OmniVoiceBackend.is_available() boolean (ok)
didn't change after IndexTTS ran; strengthen it to also assert the returned
message is unchanged so a different post-IndexTTS failure message is caught.
After calling OmniVoiceBackend.is_available() the second time (ok2, msg2),
assert ok2 == ok and msg2 == msg (using the existing ok, msg, ok2, msg2
variables) and include a failure message that shows both old and new values for
easier debugging.

Comment thread tests/test_issue_fixes.py
Comment on lines +126 to +134
# Must point at the env-var-driven install path OR the docs.
msg_lower = msg.lower()
assert (
"omnivoice_indextts_dir" in msg_lower
or "docs/engines/indextts.md" in msg_lower
or "uv pip install" in msg_lower
or "git clone" in msg_lower
or "conflict" in msg_lower
), f"is_available() failure message not actionable: {msg!r}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Tighten actionable-message assertion; conflict makes this regression too permissive.

Including "conflict" allows older non-actionable wording to pass and weakens the #42 guidance check. Prefer install/venv/doc cues only.

Proposed test patch
         assert (
             "omnivoice_indextts_dir" in msg_lower
             or "docs/engines/indextts.md" in msg_lower
             or "uv pip install" in msg_lower
             or "git clone" in msg_lower
-            or "conflict" in msg_lower
         ), f"is_available() failure message not actionable: {msg!r}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_issue_fixes.py` around lines 126 - 134, The test allows the vague
token "conflict" which makes is_available() messages too permissive; update the
assertion in tests/test_issue_fixes.py (the msg_lower variable and the
is_available() failure check) to remove the "conflict" branch and instead assert
only explicit actionable cues (e.g., keep "omnivoice_indextts_dir",
"docs/engines/indextts.md", "git clone", and ensure an install/venv marker such
as "pip install", "venv" or "virtualenv" is present) so the failure message must
point to install/venv/doc guidance rather than the old "conflict" wording.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] index-tts not compatible with omnivoice

1 participant