From 36cdeb8fdaf76c32223b364171678b4c5b55bb25 Mon Sep 17 00:00:00 2001 From: liuyihua95 Date: Tue, 22 Sep 2026 10:13:03 +0800 Subject: [PATCH] [Bugfix][TTS] Fix Audio8 voice clone reference resolution (#7815) Signed-off-by: liuyihua --- .../openai/test_audio8_tts_adapter.py | 36 +++++++++++++++++++ .../openai/tts_adapters/audio8_tts.py | 5 +-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/tests/entrypoints/openai/test_audio8_tts_adapter.py b/tests/entrypoints/openai/test_audio8_tts_adapter.py index ffd22d9f3..e7b5a7f32 100644 --- a/tests/entrypoints/openai/test_audio8_tts_adapter.py +++ b/tests/entrypoints/openai/test_audio8_tts_adapter.py @@ -13,15 +13,21 @@ model (falling through to "unknown TTS model") instead of failing at startup. These tests pin the contract from both ends. """ +import asyncio +from dataclasses import dataclass + import pytest +from vllm_omni.entrypoints.openai.protocol.audio import OpenAICreateSpeechRequest from vllm_omni.entrypoints.openai.tts_adapters import ( + SpeechServingContext, all_tts_model_types, all_tts_stage_keys, detect_tts_model_type, resolve_adapter, ) from vllm_omni.entrypoints.openai.tts_adapters.audio8_tts import Audio8TTSAdapter +from vllm_omni.entrypoints.openai.tts_adapters.base import conditioning_cache_salt from vllm_omni.model_executor.models.audio8_tts.pipeline import AUDIO8_TTS_PIPELINE pytestmark = [pytest.mark.core_model, pytest.mark.cpu] @@ -29,6 +35,15 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu] SLOW_AR_STAGE_KEY = "audio8_tts_slow_ar" +@dataclass +class _FakeSpeechServer: + _tts_executor: object | None = None + + async def _resolve_ref_audio(self, ref_audio: str) -> tuple[list[float], int, str]: + assert ref_audio == "data:audio/wav;base64,UklGRg==" + return [0.0, 0.1], 44_100, "cache-key" + + def _pipeline_stage_keys() -> list[str]: return [stage.model_stage for stage in AUDIO8_TTS_PIPELINE.stages] @@ -83,3 +98,24 @@ def test_apply_sampling_overrides_defaults_tts_local_seed_for_fast_ar(): # No seed at all -> nothing injected (non-deterministic by request contract). out_unseeded = adapter.apply_sampling_overrides([SamplingParams(seed=None)], request) assert (out_unseeded[0].extra_args or {}).get("tts_local_seed") is None + + +def test_build_accepts_ref_audio_resolver_cache_key(monkeypatch): + """The shared resolver returns a cache key in addition to audio and rate.""" + adapter = Audio8TTSAdapter(SpeechServingContext(server=_FakeSpeechServer())) + request = OpenAICreateSpeechRequest( + input="Clone this voice.", + ref_audio="data:audio/wav;base64,UklGRg==", + ) + monkeypatch.setattr( + adapter, + "_build_prompt", + lambda request, ref_audio_data, has_inline_ref_audio: {"ref_audio_data": ref_audio_data}, + ) + + prepared = asyncio.run(adapter.build(request, [], has_inline_ref_audio=True)) + + assert prepared.prompt["ref_audio_data"] == ([0.0, 0.1], 44_100) + assert prepared.tts_params == {"ref_audio_cache_key": "cache-key"} + assert prepared.prompt["cache_salt"] == conditioning_cache_salt(request, prepared.tts_params) + assert prepared.prompt["cache_salt"] != conditioning_cache_salt(request, {}) diff --git a/vllm_omni/entrypoints/openai/tts_adapters/audio8_tts.py b/vllm_omni/entrypoints/openai/tts_adapters/audio8_tts.py index 1706a64d3..520f01b08 100644 --- a/vllm_omni/entrypoints/openai/tts_adapters/audio8_tts.py +++ b/vllm_omni/entrypoints/openai/tts_adapters/audio8_tts.py @@ -97,9 +97,11 @@ class Audio8TTSAdapter(ARTTSAdapter): del sampling_params_list server = self.ctx.server ref_audio_data = None + tts_params: dict = {} if request.ref_audio is not None: - wav_list, sample_rate = await server._resolve_ref_audio(request.ref_audio) + wav_list, sample_rate, cache_key = await server._resolve_ref_audio(request.ref_audio) ref_audio_data = (wav_list, sample_rate) + tts_params["ref_audio_cache_key"] = cache_key # Prompt building tokenizes and, for voice clone, allocates tensors; keep # it off the event loop via the server's single-worker TTS executor. build_prompt = make_async(self._build_prompt, executor=server._tts_executor) @@ -108,7 +110,6 @@ class Audio8TTSAdapter(ARTTSAdapter): ref_audio_data=ref_audio_data, has_inline_ref_audio=has_inline_ref_audio, ) - tts_params: dict = {} prompt["cache_salt"] = conditioning_cache_salt(request, tts_params) return PreparedRequest(prompt=prompt, tts_params=tts_params, model_type=self.name)