Files
VoiceStudio/backend/engines/cosyvoice_subprocess/__init__.py
T
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

103 lines
3.6 KiB
Python

"""cosyvoice-subprocess: CosyVoice 3 from its own venv (one-click install).
The in-process engine needs CosyVoice importable from VoiceStudio's own
interpreter, which upstream's setup (its own Python 3.10 environment, pins
that conflict with the app's) never provides. The one-click installer clones a
reviewed CosyVoice commit and the Matcha-TTS code it vendors as a submodule
into ``DATA_DIR/engines/cosyvoice/``, builds a Python 3.10 venv from a trimmed
requirements list (``requirements.txt`` beside this file), downloads the
CosyVoice 3 weights, and this class runs the model there in a sidecar.
The engine id stays ``cosyvoice``. ``tts_backend._effective_backend_class``
resolves to this class once that venv exists, and to the in-process
``CosyVoiceBackend`` otherwise, so an existing source installation keeps
working as it did.
"""
from __future__ import annotations
import math
import os
from pathlib import Path
from services.subprocess_backend import SubprocessBackend
VENV_ENV_VAR = "OMNIVOICE_COSYVOICE_DIR"
#: Where the installer puts the CosyVoice 3 weights, inside the checkout.
MANAGED_MODEL_SUBDIR = "pretrained_models/Fun-CosyVoice3-0.5B"
def own_venv_python() -> "Path | None":
"""The venv the one-click installer made for CosyVoice, if any."""
from services.sidecar_install import engine_venv_python
return engine_venv_python(VENV_ENV_VAR)
class CosyVoiceSubprocessBackend(SubprocessBackend):
"""CosyVoice in a killable sidecar running the engine's own venv."""
id = "cosyvoice"
display_name = "CosyVoice 3 (9 langs, zero-shot, instruct, Apache-2.0)"
gpu_compat = ("cuda", "cpu")
_DEFAULT_SAMPLE_RATE = 24_000
@classmethod
def is_available(cls) -> tuple[bool, str]:
if own_venv_python() is None:
return False, (
"cosyvoice package not installed. Install it from "
"Model Catalogue → Engines."
)
return True, "ready"
@classmethod
def venv_python(cls) -> Path:
py = own_venv_python()
if py is None:
raise RuntimeError(
"CosyVoice's environment is missing. Reinstall it from "
"Model Catalogue → Engines."
)
return py
@classmethod
def sidecar_script(cls) -> Path:
return Path(__file__).resolve().parent / "main.py"
@property
def recv_timeout_s(self) -> float:
# Loading the model and its text normalizers takes a while on a cold
# start; the sidecar heartbeats progress frames meanwhile.
try:
v = float(os.environ.get("OMNIVOICE_COSYVOICE_RECV_TIMEOUT_S", "900"))
except (TypeError, ValueError):
return 900.0
if not math.isfinite(v): # reject inf/nan so the deadline can't be disabled
return 900.0
return max(30.0, v)
@property
def sample_rate(self) -> int:
# The sidecar resamples to this rate if a model ever reports another.
return self._DEFAULT_SAMPLE_RATE
@property
def supported_languages(self) -> list[str]:
from services.tts_backend import CosyVoiceBackend
return CosyVoiceBackend.supported_languages.fget(self)
def model_identity(self) -> str:
# v1/v2/v3 share the one "cosyvoice" id; the folder name tells them
# apart, as it does for the in-process engine.
model_dir = os.environ.get("OMNIVOICE_COSYVOICE_MODEL") or MANAGED_MODEL_SUBDIR
return os.path.basename(os.path.normpath(model_dir))
__all__ = [
"CosyVoiceSubprocessBackend",
"MANAGED_MODEL_SUBDIR",
"VENV_ENV_VAR",
"own_venv_python",
]