[Higgs-Audio] bosonai/higgs-audio-v2-generation-3B-base TTS model support (#3762)
Signed-off-by: Yuekai Zhang <zhangyuekai@foxmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
This commit is contained in:
25 files changed
+4835
-2
No files matched your search
@@ -0,0 +1,231 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
End-to-end offline inference tests for higgs-audio v2.
|
||||
|
||||
Mirrors examples/offline_inference/text_to_speech/higgs_audio_v2/end2end.py:
|
||||
build Stage-0 prompt_token_ids with the model's own processor + plain-text
|
||||
chat template, drive the engine via ``Omni.generate``, and inspect the
|
||||
Stage-1 PCM payload directly. The OmniRunnerHandler.send_audio_speech_request
|
||||
helper applies a Qwen-style ChatML wrap that is not valid for higgs, so this
|
||||
test bypasses it (same approach as test_voxtral_tts.py).
|
||||
|
||||
v1 scope is plain text -> 24 kHz speech; voice cloning, multi-speaker prompts,
|
||||
language overrides, and task_type selection are out of scope.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
# Match the example script: keep DeepGEMM FP8 off so engine init does not
|
||||
# require the optional ``deep_gemm`` backend.
|
||||
os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
|
||||
os.environ.setdefault("VLLM_USE_DEEP_GEMM", "0")
|
||||
os.environ.setdefault("VLLM_MOE_USE_DEEP_GEMM", "0")
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
from vllm import SamplingParams
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.media import get_asset_path
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
from tests.helpers.stage_config import get_deploy_config_path
|
||||
|
||||
MODEL = "bosonai/higgs-audio-v2-generation-3B-base"
|
||||
STAGE_CONFIG = get_deploy_config_path("higgs_audio_v2.yaml")
|
||||
SAMPLE_RATE = 24_000
|
||||
# A short sentence at 24 kHz must produce at least ~0.5 s of audio.
|
||||
MIN_AUDIO_SAMPLES = 12_000
|
||||
TEST_TEXT = "Hello world."
|
||||
|
||||
# Reuse the qwen3_tts ref clip + transcript for voice clone tests (see
|
||||
# tests/e2e/online_serving/test_qwen3_tts_base.py:27-28 for the source).
|
||||
_REF_AUDIO_ASSET = "qwen3_tts/clone_2.wav"
|
||||
_REF_TEXT = "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you."
|
||||
|
||||
# (model, stage_config_path, extra_omni_kwargs) — same shape as test_voxtral_tts.py.
|
||||
# Stage configs already pin enforce_eager=true per stage, so no Omni-level override.
|
||||
_OMNI_RUNNER_PARAM = (MODEL, STAGE_CONFIG, {"trust_remote_code": True})
|
||||
|
||||
|
||||
def _compose_request(model_name: str, text: str) -> dict:
|
||||
"""Build a higgs prompt dict via the upstream processor + plain-text template.
|
||||
|
||||
The serving layer's ``_build_higgs_audio_v2_params`` does the same thing
|
||||
(see vllm_omni/entrypoints/openai/serving_speech.py:1520) — keeping the
|
||||
offline and online prompt-construction paths identical is what guarantees
|
||||
input-token parity with the HF reference.
|
||||
"""
|
||||
from transformers import AutoProcessor
|
||||
|
||||
from vllm_omni.model_executor.models.higgs_audio_v2.higgs_audio_v2_tokenizer import (
|
||||
build_plain_text_prompt,
|
||||
input_ids_to_python_list,
|
||||
)
|
||||
|
||||
processor = AutoProcessor.from_pretrained(model_name, trust_remote_code=True)
|
||||
inputs = build_plain_text_prompt(processor, text)
|
||||
return {"prompt_token_ids": input_ids_to_python_list(inputs)}
|
||||
|
||||
|
||||
def _extract_pcm(outputs) -> torch.Tensor:
|
||||
"""Pull the concatenated PCM tensor out of the Stage-1 multimodal_output."""
|
||||
for stage_output in outputs:
|
||||
mm = getattr(stage_output, "multimodal_output", None)
|
||||
if not mm:
|
||||
continue
|
||||
audio = mm.get("model_outputs") if "model_outputs" in mm else mm.get("audio")
|
||||
if audio is None:
|
||||
continue
|
||||
if isinstance(audio, list):
|
||||
chunks = [torch.as_tensor(a).float().cpu().reshape(-1) for a in audio if a is not None]
|
||||
if not chunks:
|
||||
continue
|
||||
return torch.cat(chunks, dim=0) if len(chunks) > 1 else chunks[0]
|
||||
return torch.as_tensor(audio).float().cpu().reshape(-1)
|
||||
raise AssertionError(f"no audio payload in any stage output (got {len(outputs)} stages)")
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_runner_function", [_OMNI_RUNNER_PARAM], indirect=True)
|
||||
def test_higgs_audio_v2_offline_plain_text(omni_runner_function: OmniRunner) -> None:
|
||||
"""
|
||||
Offline plain-text -> 24 kHz PCM via the bundled higgs_audio_v2 deploy.
|
||||
|
||||
Deploy Setting: vllm_omni/deploy/higgs_audio_v2.yaml (codec_streaming=true,
|
||||
async_chunk=false, enforce_eager=true per stage).
|
||||
Input Modal: text
|
||||
Output Modal: audio (PCM tensor, 24 kHz mono float)
|
||||
Input Setting: single prompt, default sampling
|
||||
"""
|
||||
omni = omni_runner_function.omni
|
||||
inputs = _compose_request(MODEL, TEST_TEXT)
|
||||
|
||||
# max_tokens caps Stage-0 codec frames; Stage 1 max_tokens lifts the
|
||||
# per-codebook prefill cap. Mirror the deploy yaml's defaults.
|
||||
stage_sampling = [
|
||||
SamplingParams(temperature=1.0, top_p=0.95, top_k=50, max_tokens=1024, seed=42),
|
||||
SamplingParams(temperature=0.0, top_p=1.0, top_k=-1, max_tokens=65536, seed=42),
|
||||
]
|
||||
|
||||
outputs = list(omni.generate([inputs], stage_sampling))
|
||||
assert outputs, "no outputs returned from Omni.generate"
|
||||
|
||||
pcm = _extract_pcm(outputs)
|
||||
arr = pcm.numpy()
|
||||
|
||||
assert arr.size > MIN_AUDIO_SAMPLES, (
|
||||
f"audio too short: {arr.size} samples, expected > {MIN_AUDIO_SAMPLES} "
|
||||
f"({MIN_AUDIO_SAMPLES / SAMPLE_RATE:.2f} s @ {SAMPLE_RATE} Hz)"
|
||||
)
|
||||
# Threshold 5e-4 (not 1e-3) keeps us above true silence while tolerating
|
||||
# the ``load_format=dummy`` Stage-1 codec — random RVQ/DAC weights collapse
|
||||
# the output to a near-constant peak around 7.4e-4. Real-weight runs reach
|
||||
# 0.1+ peak so this still catches genuine silence.
|
||||
assert np.max(np.abs(arr)) > 5e-4, "audio output is silent (peak abs amplitude < 5e-4)"
|
||||
assert np.isfinite(arr).all(), "audio contains non-finite samples (nan/inf)"
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_runner_function", [_OMNI_RUNNER_PARAM], indirect=True)
|
||||
def test_higgs_audio_v2_offline_batch_two_prompts(omni_runner_function: OmniRunner) -> None:
|
||||
"""
|
||||
Submit two prompts in one engine call and verify both yield non-trivial PCM.
|
||||
|
||||
Guards the Stage-0 talker's per-slot audio state under a batched call —
|
||||
the prior example ran prompts one at a time citing 'request-scoped audio
|
||||
state'; this test pins the batched-2 behavior the engine path actually
|
||||
needs to support for /v1/audio/speech under concurrent traffic.
|
||||
"""
|
||||
omni = omni_runner_function.omni
|
||||
prompts = [
|
||||
_compose_request(MODEL, "Hello world."),
|
||||
_compose_request(MODEL, "The quick brown fox jumps over the lazy dog."),
|
||||
]
|
||||
stage_sampling = [
|
||||
SamplingParams(temperature=1.0, top_p=0.95, top_k=50, max_tokens=1024, seed=42),
|
||||
SamplingParams(temperature=0.0, top_p=1.0, top_k=-1, max_tokens=65536, seed=42),
|
||||
]
|
||||
|
||||
outputs = list(omni.generate(prompts, stage_sampling))
|
||||
assert outputs, "no outputs returned from Omni.generate"
|
||||
|
||||
pcm = _extract_pcm(outputs)
|
||||
# Whether the engine returns one concatenated PCM or per-request tensors is
|
||||
# implementation-defined; for either shape, total samples must clear the
|
||||
# floor for at least one of the two prompts.
|
||||
assert pcm.numel() > MIN_AUDIO_SAMPLES, (
|
||||
f"batched audio too short: {pcm.numel()} samples, expected > {MIN_AUDIO_SAMPLES}"
|
||||
)
|
||||
# See the silence-threshold note in test_higgs_audio_v2_offline_plain_text.
|
||||
assert np.max(np.abs(pcm.numpy())) > 5e-4, "batched audio output is silent"
|
||||
|
||||
|
||||
def _compose_voice_clone_request(model_name: str, text: str, ref_text: str, ref_wav_path) -> dict:
|
||||
"""Build the voice-clone prompt + additional_information payload.
|
||||
|
||||
Calls the same ``build_voice_clone_prompt`` the serving layer uses online —
|
||||
the HF processor encodes the reference clip on the spot via the bundled
|
||||
HiggsAudioV2TokenizerModel. The talker's
|
||||
``_maybe_apply_ref_audio_substitution`` consumes the tensors at prefill.
|
||||
"""
|
||||
import soundfile as sf
|
||||
from transformers import AutoProcessor
|
||||
|
||||
from vllm_omni.model_executor.models.higgs_audio_v2.higgs_audio_v2_tokenizer import (
|
||||
build_voice_clone_prompt,
|
||||
)
|
||||
|
||||
wav, sr = sf.read(str(ref_wav_path), always_2d=False)
|
||||
if wav.ndim == 2:
|
||||
wav = wav.mean(axis=1) # downmix any stereo asset to mono
|
||||
processor = AutoProcessor.from_pretrained(model_name, trust_remote_code=True)
|
||||
out = build_voice_clone_prompt(processor, text, wav, int(sr), ref_text)
|
||||
return {
|
||||
"prompt_token_ids": out["prompt_token_ids"],
|
||||
# Pass tensors bare (see serving_speech._build_higgs_audio_v2_params
|
||||
# — list-wrapped tensors get dropped by the msgspec serializer).
|
||||
"additional_information": {
|
||||
"audio_input_ids": out["audio_input_ids"],
|
||||
"audio_input_ids_mask": out["audio_input_ids_mask"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_runner_function", [_OMNI_RUNNER_PARAM], indirect=True)
|
||||
def test_higgs_audio_v2_offline_voice_clone(omni_runner_function: OmniRunner) -> None:
|
||||
"""
|
||||
Shallow voice clone via ref_audio + ref_text, offline.
|
||||
|
||||
Deploy Setting: vllm_omni/deploy/higgs_audio_v2.yaml.
|
||||
Input Modal: text + reference audio (qwen3_tts/clone_2.wav vendored asset)
|
||||
Output Modal: audio (PCM tensor, 24 kHz mono float)
|
||||
Input Setting: single prompt
|
||||
"""
|
||||
omni = omni_runner_function.omni
|
||||
ref_path = get_asset_path(_REF_AUDIO_ASSET)
|
||||
inputs = _compose_voice_clone_request(MODEL, TEST_TEXT, _REF_TEXT, ref_path)
|
||||
|
||||
stage_sampling = [
|
||||
SamplingParams(temperature=1.0, top_p=0.95, top_k=50, max_tokens=1024, seed=42),
|
||||
SamplingParams(temperature=0.0, top_p=1.0, top_k=-1, max_tokens=65536, seed=42),
|
||||
]
|
||||
|
||||
outputs = list(omni.generate([inputs], stage_sampling))
|
||||
assert outputs, "no outputs returned from Omni.generate for voice clone"
|
||||
|
||||
pcm = _extract_pcm(outputs)
|
||||
arr = pcm.numpy()
|
||||
assert arr.size > MIN_AUDIO_SAMPLES, f"cloned audio too short: {arr.size} samples, expected > {MIN_AUDIO_SAMPLES}"
|
||||
assert np.max(np.abs(arr)) > 5e-4, "voice-clone output is silent"
|
||||
assert np.isfinite(arr).all(), "voice-clone audio contains non-finite samples"
|
||||
@@ -0,0 +1,299 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
End-to-end online tests for higgs-audio v2 against /v1/audio/speech.
|
||||
|
||||
v1 scope is plain text -> 24 kHz speech plus shallow voice clone via
|
||||
ref_audio + ref_text (inline) or voice=<name> (after POST /v1/audio/voices).
|
||||
The model-aware request validator
|
||||
(vllm_omni/entrypoints/openai/serving_speech.py::_validate_higgs_audio_v2_request)
|
||||
rejects multi-speaker tags, language overrides, task_type, and bare
|
||||
voice=<name> for names that do not match an uploaded speaker — this suite
|
||||
exercises both the happy path (plain text in, audio bytes out) and the
|
||||
validator rejections, so a regression that loosens the schema will fail
|
||||
loudly.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
|
||||
# Match run_server.sh: DeepGEMM FP8 kernels are optional and trip warmup on
|
||||
# images without the deep_gemm backend, so disable them by default.
|
||||
os.environ.setdefault("VLLM_USE_DEEP_GEMM", "0")
|
||||
os.environ.setdefault("VLLM_MOE_USE_DEEP_GEMM", "0")
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.media import load_test_audio_data_url
|
||||
from tests.helpers.runtime import OmniServerParams
|
||||
from tests.helpers.stage_config import get_deploy_config_path
|
||||
|
||||
MODEL = "bosonai/higgs-audio-v2-generation-3B-base"
|
||||
STAGE_CONFIG = get_deploy_config_path("higgs_audio_v2.yaml")
|
||||
SERVER_ARGS = ["--trust-remote-code", "--disable-log-stats"]
|
||||
# DeepGEMM warmup is optional; mirror run_server.sh and switch it off in the
|
||||
# server subprocess env too (parent-process os.environ above only affects this
|
||||
# test driver; the engine subprocesses inherit through env_dict).
|
||||
SERVER_ENV = {"VLLM_USE_DEEP_GEMM": "0", "VLLM_MOE_USE_DEEP_GEMM": "0"}
|
||||
|
||||
TEST_PARAMS = [
|
||||
pytest.param(
|
||||
OmniServerParams(
|
||||
model=MODEL,
|
||||
stage_config_path=STAGE_CONFIG,
|
||||
server_args=SERVER_ARGS,
|
||||
env_dict=SERVER_ENV,
|
||||
),
|
||||
id="higgs_audio_v2_plain_text",
|
||||
)
|
||||
]
|
||||
|
||||
DEFAULT_SPEECH_TIMEOUT_S = 180.0
|
||||
# Floor for ~0.5 s of 24 kHz mono PCM_16: 24000 * 0.5 * 2 bytes ≈ 24 KiB.
|
||||
# A WAV header adds 44 bytes; pick a conservative floor that catches truncated
|
||||
# / silence-only outputs without flagging short legitimate clips.
|
||||
_MIN_AUDIO_BYTES = 20_000
|
||||
|
||||
# Reuse the qwen3_tts vendored reference clip (clean ~5 s 24 kHz mono human
|
||||
# speech) + its transcript. See tests/e2e/online_serving/test_qwen3_tts_base.py
|
||||
# for the asset rationale — keeping a single shared reference clip across TTS
|
||||
# tests avoids duplicating WAVs in the repo.
|
||||
_REF_AUDIO_URL = load_test_audio_data_url("qwen3_tts/clone_2.wav")
|
||||
_REF_TEXT = "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you."
|
||||
|
||||
|
||||
@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True)
|
||||
class TestHiggsAudioV2OnlineHappyPath:
|
||||
"""Plain-text -> audio happy paths against the live HTTP server."""
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
def test_plain_text_wav(self, omni_server, openai_client) -> None:
|
||||
"""Single non-streaming WAV request — covers the canonical TTS happy path."""
|
||||
openai_client.send_audio_speech_request(
|
||||
{
|
||||
"model": omni_server.model,
|
||||
"input": "Hello world.",
|
||||
"stream": False,
|
||||
"response_format": "wav",
|
||||
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
|
||||
"min_audio_bytes": _MIN_AUDIO_BYTES,
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
def test_plain_text_pcm_streaming(self, omni_server, openai_client) -> None:
|
||||
"""Streaming PCM via the shared-memory connector's codec_streaming path.
|
||||
|
||||
higgs_audio_v2.yaml pins ``codec_streaming: true`` + ``async_chunk: false``,
|
||||
so the only streaming surface exposed to clients is the WAV/PCM bytes
|
||||
served chunk-by-chunk from Stage 1 — exercise it directly.
|
||||
"""
|
||||
openai_client.send_audio_speech_request(
|
||||
{
|
||||
"model": omni_server.model,
|
||||
"input": "Streaming the quick brown fox over the lazy dog.",
|
||||
"stream": True,
|
||||
"response_format": "pcm",
|
||||
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
|
||||
"min_audio_bytes": _MIN_AUDIO_BYTES,
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
def test_plain_text_with_max_new_tokens(self, omni_server, openai_client) -> None:
|
||||
"""max_new_tokens is one of the few extra fields the higgs validator accepts."""
|
||||
openai_client.send_audio_speech_request(
|
||||
{
|
||||
"model": omni_server.model,
|
||||
"input": "Innovation distinguishes between a leader and a follower.",
|
||||
"stream": False,
|
||||
"response_format": "wav",
|
||||
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
|
||||
"max_new_tokens": 500,
|
||||
"min_audio_bytes": _MIN_AUDIO_BYTES,
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
def test_concurrent_plain_text(self, omni_server, openai_client) -> None:
|
||||
"""Three concurrent non-streaming requests — guards the per-slot audio state."""
|
||||
openai_client.send_audio_speech_request(
|
||||
{
|
||||
"model": omni_server.model,
|
||||
"input": "It was the night before my birthday.",
|
||||
"stream": False,
|
||||
"response_format": "wav",
|
||||
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
|
||||
"min_audio_bytes": _MIN_AUDIO_BYTES,
|
||||
},
|
||||
request_num=3,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Validator rejections served over HTTP. Each case targets one out-of-scope
|
||||
# field; the validator returns 4xx via the OpenAI error path. We keep the
|
||||
# error-message substring loose so a phrasing tweak in the validator does not
|
||||
# break CI, but tight enough to catch a regression that silently accepts the
|
||||
# field.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True)
|
||||
class TestHiggsAudioV2OnlineValidatorRejections:
|
||||
"""Out-of-scope fields must come back as 4xx with a higgs-named message."""
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
def test_rejects_ref_audio_without_ref_text(self, omni_server, openai_client) -> None:
|
||||
"""Voice clone needs the transcript too — half-supplied is a 4xx."""
|
||||
openai_client.send_audio_speech_request(
|
||||
{
|
||||
"model": omni_server.model,
|
||||
"input": "Hello world.",
|
||||
"ref_audio": _REF_AUDIO_URL,
|
||||
"response_format": "wav",
|
||||
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
|
||||
"status_code": (400, 422),
|
||||
"err_message": "ref_text",
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
def test_rejects_ref_text_without_ref_audio(self, omni_server, openai_client) -> None:
|
||||
"""Symmetric guard: ref_text alone is not enough — must come with ref_audio."""
|
||||
openai_client.send_audio_speech_request(
|
||||
{
|
||||
"model": omni_server.model,
|
||||
"input": "Hello world.",
|
||||
"ref_text": "some transcript",
|
||||
"response_format": "wav",
|
||||
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
|
||||
"status_code": (400, 422),
|
||||
"err_message": "ref_audio",
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
def test_rejects_task_type(self, omni_server, openai_client) -> None:
|
||||
openai_client.send_audio_speech_request(
|
||||
{
|
||||
"model": omni_server.model,
|
||||
"input": "Hello world.",
|
||||
"task_type": "Base",
|
||||
"response_format": "wav",
|
||||
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
|
||||
"status_code": (400, 422),
|
||||
"err_message": "task_type",
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
def test_rejects_language_override(self, omni_server, openai_client) -> None:
|
||||
openai_client.send_audio_speech_request(
|
||||
{
|
||||
"model": omni_server.model,
|
||||
"input": "Hello world.",
|
||||
"language": "Chinese",
|
||||
"response_format": "wav",
|
||||
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
|
||||
"status_code": (400, 422),
|
||||
"err_message": "language",
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
def test_rejects_multi_speaker_tag_in_text(self, omni_server, openai_client) -> None:
|
||||
openai_client.send_audio_speech_request(
|
||||
{
|
||||
"model": omni_server.model,
|
||||
"input": "[SPEAKER0] hi",
|
||||
"response_format": "wav",
|
||||
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
|
||||
"status_code": (400, 422),
|
||||
"err_message": "multi-speaker",
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
def test_rejects_empty_input(self, omni_server, openai_client) -> None:
|
||||
openai_client.send_audio_speech_request(
|
||||
{
|
||||
"model": omni_server.model,
|
||||
"input": " ",
|
||||
"response_format": "wav",
|
||||
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
|
||||
"status_code": (400, 422),
|
||||
"err_message": "empty",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True)
|
||||
class TestHiggsAudioV2OnlineVoiceClone:
|
||||
"""Shallow voice clone: ref_audio + ref_text -> speech in the cloned voice.
|
||||
|
||||
The HF processor that the serving layer calls (lazy-loaded at the first
|
||||
request) ships with the bundled HiggsAudioV2TokenizerModel and encodes
|
||||
the reference clip in-process. The talker substitutes the encoded codes
|
||||
at the prompt-side audio placeholders via
|
||||
:meth:`HiggsAudioV2TalkerForConditionalGeneration._maybe_apply_ref_audio_substitution`.
|
||||
"""
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
def test_voice_clone_basic(self, omni_server, openai_client) -> None:
|
||||
"""Single non-streaming WAV request driven by the qwen3_tts ref clip."""
|
||||
openai_client.send_audio_speech_request(
|
||||
{
|
||||
"model": omni_server.model,
|
||||
"input": "Hello world.",
|
||||
"ref_audio": _REF_AUDIO_URL,
|
||||
"ref_text": _REF_TEXT,
|
||||
"stream": False,
|
||||
"response_format": "wav",
|
||||
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
|
||||
"min_audio_bytes": _MIN_AUDIO_BYTES,
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
def test_voice_clone_pcm_streaming(self, omni_server, openai_client) -> None:
|
||||
"""Voice clone over the streaming PCM path."""
|
||||
openai_client.send_audio_speech_request(
|
||||
{
|
||||
"model": omni_server.model,
|
||||
"input": "Innovation distinguishes a leader from a follower.",
|
||||
"ref_audio": _REF_AUDIO_URL,
|
||||
"ref_text": _REF_TEXT,
|
||||
"stream": True,
|
||||
"response_format": "pcm",
|
||||
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
|
||||
"min_audio_bytes": _MIN_AUDIO_BYTES,
|
||||
}
|
||||
)
|
||||
Reference in new issue
Block a user