Merge remote-tracking branch 'origin/feat/moss-tts-nano-isolated' into feat/cosyvoice3-isolated
This commit is contained in:
@@ -142,6 +142,20 @@ def _sample_rate(model) -> int:
|
||||
return VOXCPM2_SAMPLE_RATE
|
||||
|
||||
|
||||
def _at_engine_rate(wav, sample_rate: int):
|
||||
"""The waveform at VOXCPM2_SAMPLE_RATE. The parent reads the PCM at that
|
||||
fixed rate (it trims the tail and labels the audio with it), so a model
|
||||
reporting another rate is resampled here rather than mislabelled."""
|
||||
if sample_rate == VOXCPM2_SAMPLE_RATE:
|
||||
return wav
|
||||
import torch # noqa: PLC0415
|
||||
import torchaudio # noqa: PLC0415
|
||||
|
||||
tensor = torch.as_tensor(wav.detach().cpu() if hasattr(wav, "detach") else wav,
|
||||
dtype=torch.float32).reshape(-1)
|
||||
return torchaudio.functional.resample(tensor, sample_rate, VOXCPM2_SAMPLE_RATE)
|
||||
|
||||
|
||||
def _to_pcm_b64(wav) -> tuple[str, int]:
|
||||
"""A float waveform in [-1, 1] (numpy or torch) as base64 int16 PCM."""
|
||||
import numpy as np # noqa: PLC0415
|
||||
@@ -189,11 +203,11 @@ def _handle_synthesize(msg: dict, stdout) -> None:
|
||||
prompt_wav_path=ref_audio if ref_text else None,
|
||||
prompt_text=ref_text,
|
||||
)
|
||||
pcm_b64, n_samples = _to_pcm_b64(wav)
|
||||
pcm_b64, n_samples = _to_pcm_b64(_at_engine_rate(wav, _sample_rate(model)))
|
||||
_send(stdout, {
|
||||
"op": "audio",
|
||||
"audio_pcm_b64": pcm_b64,
|
||||
"sample_rate": _sample_rate(model),
|
||||
"sample_rate": VOXCPM2_SAMPLE_RATE,
|
||||
"n_samples": n_samples,
|
||||
})
|
||||
|
||||
|
||||
@@ -648,7 +648,14 @@ def engine_venv_python(env_var: str) -> Optional[Path]:
|
||||
if not env_dir:
|
||||
return None
|
||||
py = _venv_python(Path(env_dir) / ".venv")
|
||||
return py if py.is_file() else None
|
||||
# The interpreter alone proves nothing: a reinstall that failed partway
|
||||
# leaves it behind. The completion marker is written only after the
|
||||
# engine's import probe passed in this venv, and removed when a new
|
||||
# dependency step starts, so it is the probe's verdict without running a
|
||||
# multi-second import on every engine-list refresh.
|
||||
if not py.is_file() or not (Path(env_dir) / _INSTALL_COMPLETE_MARKER).is_file():
|
||||
return None
|
||||
return py
|
||||
|
||||
|
||||
def _legacy_managed_checkouts(spec: SidecarSpec) -> tuple[Path, ...]:
|
||||
|
||||
@@ -120,7 +120,7 @@ def test_the_sidecar_imports_nothing_from_the_app():
|
||||
def test_the_class_switches_to_the_sidecar_once_its_venv_exists(monkeypatch, tmp_path):
|
||||
from engines.moss_tts_nano_subprocess import MossTTSNanoSubprocessBackend
|
||||
from services import tts_backend
|
||||
from services.sidecar_install import _venv_python
|
||||
from services.sidecar_install import _INSTALL_COMPLETE_MARKER, _venv_python
|
||||
|
||||
monkeypatch.setenv("OMNIVOICE_MOSS_TTS_NANO_DIR", "")
|
||||
monkeypatch.delenv("OMNIVOICE_MOSS_TTS_NANO_DIR")
|
||||
@@ -129,6 +129,7 @@ def test_the_class_switches_to_the_sidecar_once_its_venv_exists(monkeypatch, tmp
|
||||
py = _venv_python(tmp_path / ".venv")
|
||||
py.parent.mkdir(parents=True)
|
||||
py.write_text("#!fake\n")
|
||||
(tmp_path / _INSTALL_COMPLETE_MARKER).write_text("x\n", encoding="utf-8")
|
||||
monkeypatch.setenv("OMNIVOICE_MOSS_TTS_NANO_DIR", str(tmp_path))
|
||||
|
||||
cls = tts_backend.get_backend_class("moss-tts-nano")
|
||||
|
||||
@@ -289,7 +289,7 @@ def test_prefers_the_venv_its_one_click_install_made(monkeypatch, tmp_path, mock
|
||||
interpreter, where `uv sync --extra pockettts` installs it."""
|
||||
from pathlib import Path
|
||||
|
||||
from services.sidecar_install import _venv_python
|
||||
from services.sidecar_install import _INSTALL_COMPLETE_MARKER, _venv_python
|
||||
|
||||
mock_settings_store["pockettts"] = True
|
||||
monkeypatch.delenv("OMNIVOICE_POCKETTTS_DIR", raising=False)
|
||||
@@ -298,6 +298,7 @@ def test_prefers_the_venv_its_one_click_install_made(monkeypatch, tmp_path, mock
|
||||
py = _venv_python(tmp_path / ".venv")
|
||||
py.parent.mkdir(parents=True)
|
||||
py.write_text("#!fake\n")
|
||||
(tmp_path / _INSTALL_COMPLETE_MARKER).write_text("x\n", encoding="utf-8")
|
||||
monkeypatch.setenv("OMNIVOICE_POCKETTTS_DIR", str(tmp_path))
|
||||
assert _backend_cls().venv_python() == py
|
||||
# Available without pocket_tts importable in the app's own environment.
|
||||
|
||||
@@ -1221,6 +1221,9 @@ def test_engine_venv_python_needs_a_real_interpreter(monkeypatch, tmp_path):
|
||||
py = si._venv_python(tmp_path / ".venv")
|
||||
py.parent.mkdir(parents=True)
|
||||
py.write_text("#!fake\n")
|
||||
# An interpreter without the marker is a failed or unfinished install.
|
||||
assert si.engine_venv_python("OMNIVOICE_FAKE_SIDE_DIR") is None
|
||||
(tmp_path / si._INSTALL_COMPLETE_MARKER).write_text("x\n", encoding="utf-8")
|
||||
assert si.engine_venv_python("OMNIVOICE_FAKE_SIDE_DIR") == py
|
||||
|
||||
|
||||
|
||||
@@ -389,7 +389,7 @@ def test_prefers_the_venv_its_one_click_install_made(monkeypatch, tmp_path, mock
|
||||
from pathlib import Path
|
||||
|
||||
from engines.supertonic3.backend import Supertonic3Backend
|
||||
from services.sidecar_install import _venv_python
|
||||
from services.sidecar_install import _INSTALL_COMPLETE_MARKER, _venv_python
|
||||
|
||||
mock_settings_store["supertonic3"] = True
|
||||
monkeypatch.delenv("OMNIVOICE_SUPERTONIC3_DIR", raising=False)
|
||||
@@ -398,6 +398,7 @@ def test_prefers_the_venv_its_one_click_install_made(monkeypatch, tmp_path, mock
|
||||
py = _venv_python(tmp_path / ".venv")
|
||||
py.parent.mkdir(parents=True)
|
||||
py.write_text("#!fake\n")
|
||||
(tmp_path / _INSTALL_COMPLETE_MARKER).write_text("x\n", encoding="utf-8")
|
||||
monkeypatch.setenv("OMNIVOICE_SUPERTONIC3_DIR", str(tmp_path))
|
||||
assert Supertonic3Backend.venv_python() == py
|
||||
# Available without supertonic importable in the app's own environment.
|
||||
|
||||
@@ -129,11 +129,15 @@ def test_retries_a_transient_download_failure_only(monkeypatch):
|
||||
assert sidecar._with_retries(flaky) == "model"
|
||||
assert len(attempts) == 3
|
||||
|
||||
broken_calls = []
|
||||
|
||||
def broken():
|
||||
broken_calls.append(1)
|
||||
raise ValueError("bad config")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
sidecar._with_retries(broken)
|
||||
assert len(broken_calls) == 1 # a permanent error is not retried
|
||||
|
||||
|
||||
def test_the_sidecar_imports_nothing_from_the_app():
|
||||
@@ -145,7 +149,7 @@ def test_the_sidecar_imports_nothing_from_the_app():
|
||||
def test_the_class_switches_to_the_sidecar_once_its_venv_exists(monkeypatch, tmp_path):
|
||||
from engines.voxcpm2_subprocess import VoxCPM2SubprocessBackend
|
||||
from services import tts_backend
|
||||
from services.sidecar_install import _venv_python
|
||||
from services.sidecar_install import _INSTALL_COMPLETE_MARKER, _venv_python
|
||||
|
||||
monkeypatch.setenv("OMNIVOICE_VOXCPM2_DIR", "")
|
||||
monkeypatch.delenv("OMNIVOICE_VOXCPM2_DIR")
|
||||
@@ -154,6 +158,7 @@ def test_the_class_switches_to_the_sidecar_once_its_venv_exists(monkeypatch, tmp
|
||||
py = _venv_python(tmp_path / ".venv")
|
||||
py.parent.mkdir(parents=True)
|
||||
py.write_text("#!fake\n")
|
||||
(tmp_path / _INSTALL_COMPLETE_MARKER).write_text("x\n", encoding="utf-8")
|
||||
monkeypatch.setenv("OMNIVOICE_VOXCPM2_DIR", str(tmp_path))
|
||||
|
||||
cls = tts_backend.get_backend_class("voxcpm2")
|
||||
@@ -197,3 +202,29 @@ def test_the_sidecar_class_prepares_the_reference_and_trims_the_tail(monkeypatch
|
||||
assert sent["ref_audio"] == "/clip.wav.prepared.wav"
|
||||
assert trimmed["sr"] == 48000
|
||||
assert tuple(out.shape) == (1, 10)
|
||||
|
||||
|
||||
def test_output_is_resampled_to_the_48_khz_the_parent_assumes(monkeypatch):
|
||||
"""The parent trims and labels the PCM at a fixed 48 kHz, so a model that
|
||||
reports another rate must be resampled, not passed through."""
|
||||
sidecar = _load_sidecar(monkeypatch, [])
|
||||
sidecar._handle_synthesize({"text": "warm up"}, io.BytesIO())
|
||||
type(sidecar._MODEL).sample_rate = 24000
|
||||
out = io.BytesIO()
|
||||
sidecar._handle_synthesize({"text": "hi"}, out)
|
||||
audio = _frames(out)[-1]
|
||||
assert audio["sample_rate"] == 48000
|
||||
assert audio["n_samples"] == 960 # 480 samples at 24 kHz
|
||||
|
||||
|
||||
def test_a_failed_install_leaves_voxcpm2_in_process(monkeypatch, tmp_path):
|
||||
"""A venv interpreter without the completion marker (a reinstall that
|
||||
failed partway) must not hide the working in-process engine."""
|
||||
from services import tts_backend
|
||||
from services.sidecar_install import _venv_python
|
||||
|
||||
py = _venv_python(tmp_path / ".venv")
|
||||
py.parent.mkdir(parents=True)
|
||||
py.write_text("#!fake\n")
|
||||
monkeypatch.setenv("OMNIVOICE_VOXCPM2_DIR", str(tmp_path))
|
||||
assert tts_backend.get_backend_class("voxcpm2") is tts_backend.VoxCPM2Backend
|
||||
|
||||
Reference in New Issue
Block a user