# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project """E2E offline inference tests for MOSS-TTS-Nano single-stage pipeline. Every request needs a reference audio clip. We test upstream's recommended ``voice_clone`` mode (no transcript needed). We fetch the upstream sample (assets/audio/zh_1.wav, ~50 KB) once per test session and reuse it for all cases. """ from __future__ import annotations import os import urllib.request import pytest import torch from vllm import SamplingParams from tests.helpers.mark import hardware_test from tests.helpers.runtime import OmniRunner from tests.helpers.stage_config import get_deploy_config_path from vllm_omni import Omni MODEL_NAME = "OpenMOSS-Team/MOSS-TTS-Nano" STAGE_CONFIG = get_deploy_config_path("moss_tts_nano.yaml") # (model, deploy_config_path, extra_omni_kwargs) for ``omni_runner`` indirect parametrize _OMNI_RUNNER_PARAM = ( MODEL_NAME, STAGE_CONFIG, {"trust_remote_code": True}, ) pytestmark = [ pytest.mark.slow, pytest.mark.tts, pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True), ] SAMPLE_RATE = 48000 REF_AUDIO_URL = "https://raw.githubusercontent.com/OpenMOSS/MOSS-TTS-Nano/main/assets/audio/zh_1.wav" DEFAULT_SAMPLING = SamplingParams( temperature=1.0, top_p=1.0, top_k=50, max_tokens=4096, seed=42, detokenize=False, ) @pytest.fixture(scope="session") def ref_audio_path(tmp_path_factory) -> str: """Download the upstream reference clip once per test session. Bounded with a 30 s timeout (urlretrieve has no timeout kwarg, so we use urlopen + write ourselves). Fetch failures are hard failures, not skips, so a broken network path can't silently mask regressions. Set ``MOSS_TTS_NANO_SKIP_ON_NET_FAIL=1`` to opt into skipping for air-gapped environments. """ cache_dir = tmp_path_factory.mktemp("moss_tts_nano_ref") target = cache_dir / "zh_1.wav" try: with urllib.request.urlopen(REF_AUDIO_URL, timeout=30) as resp: data = resp.read() target.write_bytes(data) except Exception as e: msg = f"Cannot fetch upstream reference clip {REF_AUDIO_URL}: {e}" if os.environ.get("MOSS_TTS_NANO_SKIP_ON_NET_FAIL"): pytest.skip(msg) pytest.fail(msg) if not target.exists() or os.path.getsize(target) == 0: pytest.fail(f"Reference clip empty after download: {target}") return str(target) def _build_request( text: str, prompt_audio_path: str, prompt_text: str | None = None, mode: str = "voice_clone", max_new_frames: int = 100, # short for tests seed: int = 42, ) -> dict: """Build a MOSS-TTS-Nano offline request. Upstream forbids ``prompt_text`` in ``voice_clone`` mode; only forward it when explicitly supplied (and typically with ``mode='continuation'``). """ additional: dict = { "text": [text], "mode": [mode], "prompt_audio_path": [prompt_audio_path], "max_new_frames": [max_new_frames], "seed": [seed], } if prompt_text is not None: additional["prompt_text"] = [prompt_text] return { "prompt": "<|im_start|>assistant\n", "additional_information": additional, } def _collect_audio(omni: Omni, request: dict) -> tuple[torch.Tensor, int]: """Run a single request and return (waveform, sample_rate).""" for stage_outputs in omni.generate(request, DEFAULT_SAMPLING): req_output = stage_outputs if req_output is not None: mm = req_output.outputs[0].multimodal_output assert mm is not None, "Expected multimodal_output to be non-None" audio = mm.get("audio") sr = mm.get("sr") assert audio is not None, "Expected 'audio' key in multimodal_output" assert isinstance(audio, torch.Tensor), f"audio should be Tensor, got {type(audio)}" return audio.cpu(), int(sr.item()) if sr is not None else SAMPLE_RATE raise AssertionError("No stage outputs received") @pytest.mark.advanced_model @hardware_test(res={"cuda": "L4"}) def test_moss_tts_nano_english(omni_runner: OmniRunner, ref_audio_path) -> None: """English TTS produces non-empty 48 kHz stereo audio.""" req = _build_request("Hello, this is a short voice cloning demo for testing.", ref_audio_path) audio, sr = _collect_audio(omni_runner.omni, req) assert sr == SAMPLE_RATE, f"Expected sample_rate={SAMPLE_RATE}, got {sr}" assert audio.numel() > 0, "Audio tensor should not be empty" assert not torch.all(audio == 0), "Audio should not be all-zeros (silence)" @pytest.mark.advanced_model @hardware_test(res={"cuda": "L4"}) def test_moss_tts_nano_chinese(omni_runner: OmniRunner, ref_audio_path) -> None: """Chinese TTS produces non-empty audio.""" req = _build_request("你好,这是语音合成测试。", ref_audio_path) audio, sr = _collect_audio(omni_runner.omni, req) assert sr == SAMPLE_RATE assert audio.numel() > 0 assert not torch.all(audio == 0) @pytest.mark.advanced_model @hardware_test(res={"cuda": "L4"}) def test_moss_tts_nano_deterministic(omni_runner: OmniRunner, ref_audio_path) -> None: """Same seed produces identical waveforms.""" req = _build_request("Reproducible output test.", ref_audio_path, seed=123) audio1, _ = _collect_audio(omni_runner.omni, req) audio2, _ = _collect_audio(omni_runner.omni, req) assert audio1.shape == audio2.shape, "Waveform shapes should match with same seed" assert torch.allclose(audio1, audio2, atol=1e-4), "Waveforms should match with same seed" @pytest.mark.advanced_model @hardware_test(res={"cuda": "L4"}) def test_moss_tts_nano_batch(omni_runner: OmniRunner, ref_audio_path) -> None: """Batch of two requests returns audio for each.""" requests = [ _build_request("First request.", ref_audio_path), _build_request("Second request.", ref_audio_path), ] results = [] # Single-stage model (num_stages=1): one sampling param for all requests. for stage_outputs in omni_runner.omni.generate(requests, [DEFAULT_SAMPLING]): req_output = stage_outputs if req_output is not None: mm = req_output.outputs[0].multimodal_output assert mm is not None results.append(mm["audio"].cpu()) assert len(results) == 2, f"Expected 2 outputs, got {len(results)}" for i, audio in enumerate(results): assert audio.numel() > 0, f"Audio {i} is empty"