Files
vllm-omni/tests/e2e/online_serving/test_audex_tts.py
5014fea4a0 [New Model] support nemotron Audex (#4976)
Signed-off-by: Yuekai Zhang <zhangyuekai@foxmail.com>
Signed-off-by: Yueqian Lin <linyueqian@outlook.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Yueqian Lin <linyueqian@outlook.com>
Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
2026-07-28 03:58:22 +00:00

231 lines
8.6 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""E2E online tests for Audex (Nemotron-Labs-Audex-2B) TTS.
Covers /v1/audio/speech non-streaming and audio-byte streaming, plus the
Audex request policies: single built-in voice (missing/empty/"default" only),
no reference audio, and cfg_scale reserved at 1.0.
"""
import os
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
import pytest
from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniServerParams
from tests.helpers.stage_config import get_deploy_config_path
pytestmark = [pytest.mark.slow, pytest.mark.tts]
MODEL = "nvidia/Nemotron-Labs-Audex-2B"
PROMPT = "The weather is so good, and I want to enjoy the beautiful morning in the park."
audex_server_params = [
pytest.param(
OmniServerParams(
model=MODEL,
stage_config_path=get_deploy_config_path("audex_tts.yaml"),
server_args=["--trust-remote-code"],
),
id="audex",
)
]
# 30B-A3B variant: opt-in (pulls ~60 GB); enable with AUDEX_E2E_30B=1.
_AUDEX_30B_MODEL = os.environ.get("VLLM_OMNI_AUDEX_30B_MODEL_DIR") or "nvidia/Nemotron-Labs-Audex-30B-A3B"
if os.environ.get("AUDEX_E2E_30B") == "1":
audex_server_params.append(
pytest.param(
OmniServerParams(
model=_AUDEX_30B_MODEL,
stage_config_path=get_deploy_config_path("audex_tts_30b.yaml"),
server_args=["--trust-remote-code"],
),
id="online_tts_30b",
)
)
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@pytest.mark.parametrize("omni_server", audex_server_params, indirect=True)
def test_audex_tts_nonstream(omni_server, openai_client) -> None:
"""Plain English TTS, non-streaming WAV response."""
request_config = {
"model": omni_server.model,
"input": PROMPT,
"stream": False,
"response_format": "wav",
}
openai_client.send_audio_speech_request(request_config)
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@pytest.mark.parametrize("omni_server", audex_server_params, indirect=True)
def test_audex_tts_stream_audio(omni_server, openai_client) -> None:
"""Plain English TTS, raw audio-byte streaming."""
request_config = {
"model": omni_server.model,
"input": PROMPT,
"stream": True,
"stream_format": "audio",
"response_format": "wav",
}
openai_client.send_audio_speech_request(request_config)
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@pytest.mark.parametrize("omni_server", audex_server_params, indirect=True)
def test_audex_tts_max_new_tokens_caps_duration(omni_server, openai_client, run_level) -> None:
"""max_new_tokens must cap stage-0 generation (review P2 regression)."""
import io
import wave
import requests
if run_level not in {"advanced_model", "full_model"}:
pytest.skip("duration cap needs real weights")
url = f"{openai_client.base_url.rstrip('/')}/v1/audio/speech"
resp = requests.post(
url,
json={
"model": omni_server.model,
"input": PROMPT,
"response_format": "wav",
"max_new_tokens": 25,
},
)
assert resp.status_code == 200, resp.text
with wave.open(io.BytesIO(resp.content)) as wav:
duration_s = wav.getnframes() / wav.getframerate()
# 25 codec frames at 50 fps = 0.5 s (+ small lookahead tail); the full
# sentence would be ~3 s, so a capped result proves the override applied.
assert duration_s <= 1.0, f"max_new_tokens ignored: duration={duration_s:.2f}s"
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@pytest.mark.parametrize("omni_server", audex_server_params, indirect=True)
def test_audex_tts_request_policies(omni_server, openai_client) -> None:
"""Unsupported voice / CFG / empty input must be rejected with 400."""
import requests
url = f"{openai_client.base_url.rstrip('/')}/v1/audio/speech"
ok = requests.post(url, json={"model": omni_server.model, "input": PROMPT, "voice": "default"})
assert ok.status_code == 200, ok.text
for bad_payload, needle in (
({"input": PROMPT, "voice": "alloy"}, "voice"),
({"input": " "}, "non-empty"),
({"input": PROMPT, "extra_params": {"cfg_scale": 0.5}}, "cfg_scale"),
({"input": PROMPT, "extra_params": {"cfg_scale": 10.5}}, "cfg_scale"),
({"input": PROMPT, "extra_params": {"cfg_scale": "big"}}, "cfg_scale"),
({"input": PROMPT, "extra_params": {"cfg_role": "cond"}}, "cfg_role"),
({"input": PROMPT, "extra_params": {"cfg_pair_id": "x"}}, "cfg_pair_id"),
({"input": PROMPT, "ref_audio": "https://example.com/ref.wav"}, "reference audio"),
):
resp = requests.post(url, json={"model": omni_server.model, **bad_payload})
assert resp.status_code == 400, f"{bad_payload} -> {resp.status_code}: {resp.text[:200]}"
assert needle in resp.text, f"{bad_payload} error missing {needle!r}: {resp.text[:200]}"
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@pytest.mark.parametrize("omni_server", audex_server_params, indirect=True)
def test_audex_tts_cfg_nonstream(omni_server, openai_client) -> None:
"""Guided (cfg_scale=1.5) non-streaming request returns one WAV."""
request_config = {
"model": omni_server.model,
"input": PROMPT,
"stream": False,
"response_format": "wav",
"extra_params": {"cfg_scale": 1.5},
}
openai_client.send_audio_speech_request(request_config)
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@pytest.mark.parametrize("omni_server", audex_server_params, indirect=True)
def test_audex_tts_cfg_stream_audio(omni_server, openai_client) -> None:
"""Guided (cfg_scale=1.5) streaming request yields a single audio stream."""
request_config = {
"model": omni_server.model,
"input": PROMPT,
"stream": True,
"stream_format": "audio",
"response_format": "wav",
"extra_params": {"cfg_scale": 1.5},
}
openai_client.send_audio_speech_request(request_config)
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@pytest.mark.parametrize("omni_server", audex_server_params, indirect=True)
def test_audex_tts_mixed_cfg_concurrency(omni_server, openai_client) -> None:
"""Mixed guided/unguided requests at concurrency 4 all complete with audio."""
import concurrent.futures
import requests
url = f"{openai_client.base_url.rstrip('/')}/v1/audio/speech"
payloads = [
{"model": omni_server.model, "input": f"{PROMPT} Take {i}.", "response_format": "wav"}
| ({"extra_params": {"cfg_scale": 1.5}} if i % 2 == 0 else {})
for i in range(4)
]
def _post(payload):
return requests.post(url, json=payload, timeout=300)
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
responses = list(pool.map(_post, payloads))
for payload, resp in zip(payloads, responses):
assert resp.status_code == 200, f"{payload} -> {resp.status_code}: {resp.text[:200]}"
assert resp.content[:4] == b"RIFF", f"{payload}: not a WAV response"
assert len(resp.content) > 44, f"{payload}: empty audio body"
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@pytest.mark.parametrize("omni_server", audex_server_params, indirect=True)
def test_audex_tts_stream_abort_leaves_server_healthy(omni_server, openai_client) -> None:
"""Aborting a guided streaming request mid-flight must not wedge the server.
The abort cascades to the CFG companion (cfg_companion_tracker); a
follow-up guided request on the same server must still succeed.
"""
import requests
url = f"{openai_client.base_url.rstrip('/')}/v1/audio/speech"
long_input = " ".join([PROMPT] * 4)
with requests.post(
url,
json={
"model": omni_server.model,
"input": long_input,
"stream": True,
"stream_format": "audio",
"response_format": "wav",
"extra_params": {"cfg_scale": 1.5},
},
stream=True,
timeout=300,
) as resp:
assert resp.status_code == 200, resp.text
# Read one chunk, then abort the connection mid-stream.
next(resp.iter_content(chunk_size=4096))
follow_up = requests.post(
url,
json={
"model": omni_server.model,
"input": PROMPT,
"response_format": "wav",
"extra_params": {"cfg_scale": 1.5},
},
timeout=300,
)
assert follow_up.status_code == 200, follow_up.text[:200]
assert follow_up.content[:4] == b"RIFF" and len(follow_up.content) > 44