[Bugfix] Fix Qwen3-TTS Base ICL garbled output when ref_audio is a local path (#2984)
Signed-off-by: Dnoob <dxpouo@gmail.com>
This commit is contained in:
1 parent
4c2bea782e
commit
08cf5a6083
2 files changed
+141
-3
No files matched your search
@@ -83,12 +83,29 @@ def _estimate_prompt_len(
|
||||
if not isinstance(audio_path, str) or not audio_path.strip():
|
||||
return None
|
||||
try:
|
||||
from vllm.multimodal.media import MediaConnector
|
||||
from urllib.parse import urlparse
|
||||
|
||||
connector = MediaConnector(allowed_local_media_path="/")
|
||||
audio, sr = connector.fetch_audio(audio_path)
|
||||
import numpy as np
|
||||
|
||||
def _is_url(path: str) -> bool:
|
||||
try:
|
||||
parsed = urlparse(path)
|
||||
if parsed.scheme in ("http", "https"):
|
||||
return bool(parsed.netloc)
|
||||
return parsed.scheme in ("file", "data")
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if _is_url(audio_path):
|
||||
from vllm.multimodal.media import MediaConnector
|
||||
|
||||
connector = MediaConnector(allowed_local_media_path="/")
|
||||
audio, sr = connector.fetch_audio(audio_path)
|
||||
else:
|
||||
from vllm.multimodal.media.audio import load_audio
|
||||
|
||||
audio, sr = load_audio(audio_path, sr=None, mono=True)
|
||||
|
||||
wav_np = np.asarray(audio, dtype=np.float32)
|
||||
|
||||
if speech_tok is not None:
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
Offline inference regression tests: Qwen3-TTS _estimate_prompt_len.
|
||||
|
||||
Covers the four ref_audio input forms handled by the estimator in
|
||||
examples/offline_inference/qwen3_tts/end2end.py:
|
||||
- bare local path
|
||||
- file:// URI
|
||||
- http:// URL
|
||||
- data: URI
|
||||
"""
|
||||
|
||||
import base64
|
||||
import io
|
||||
import sys
|
||||
import wave
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
_examples_dir = str(Path(__file__).parent.parent.parent.parent / "examples" / "offline_inference" / "qwen3_tts")
|
||||
sys.path.insert(0, _examples_dir)
|
||||
from end2end import _estimate_prompt_len # noqa: E402
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
TEST_SR = 16000
|
||||
TEST_DURATION_S = 3
|
||||
CODEC_FRAME_RATE = 12
|
||||
EXPECTED_REF_CODE_LEN = TEST_DURATION_S * CODEC_FRAME_RATE # 36, distinct from fallback 2048
|
||||
|
||||
|
||||
def _wav_bytes() -> bytes:
|
||||
buf = io.BytesIO()
|
||||
samples = np.zeros(TEST_SR * TEST_DURATION_S, dtype=np.int16)
|
||||
with wave.open(buf, "wb") as wf:
|
||||
wf.setnchannels(1)
|
||||
wf.setsampwidth(2)
|
||||
wf.setframerate(TEST_SR)
|
||||
wf.writeframes(samples.tobytes())
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def wav_file(tmp_path: Path) -> Path:
|
||||
path = tmp_path / "ref.wav"
|
||||
path.write_bytes(_wav_bytes())
|
||||
return path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def estimator_cache(monkeypatch):
|
||||
"""
|
||||
Bypass model loading by pre-populating the estimator cache, and shortcut
|
||||
Qwen3TTSTalker.estimate_prompt_len_from_additional_information so that
|
||||
the test directly observes the real _estimate_ref_code_len closure.
|
||||
"""
|
||||
from vllm_omni.model_executor.models.qwen3_tts.qwen3_tts_talker import (
|
||||
Qwen3TTSTalkerForConditionalGeneration,
|
||||
)
|
||||
|
||||
def _shortcut(**kwargs):
|
||||
ref_audio = kwargs["additional_information"]["ref_audio"][0]
|
||||
return kwargs["estimate_ref_code_len"](ref_audio)
|
||||
|
||||
monkeypatch.setattr(
|
||||
Qwen3TTSTalkerForConditionalGeneration,
|
||||
"estimate_prompt_len_from_additional_information",
|
||||
staticmethod(_shortcut),
|
||||
)
|
||||
|
||||
mock_tcfg = SimpleNamespace(codec_frame_rate=CODEC_FRAME_RATE, codec_language_id=None, spk_is_dialect=None)
|
||||
return {"test_model": (MagicMock(), mock_tcfg, None)}
|
||||
|
||||
|
||||
def _run(cache: dict, ref_audio: str) -> int | None:
|
||||
return _estimate_prompt_len(
|
||||
additional_information={"task_type": ["Base"], "ref_audio": [ref_audio]},
|
||||
model_name="test_model",
|
||||
_cache=cache,
|
||||
)
|
||||
|
||||
|
||||
def test_bare_local_path(estimator_cache, monkeypatch, wav_file):
|
||||
"""Bare local path must be loaded via load_audio, not MediaConnector."""
|
||||
from vllm.multimodal.media import MediaConnector
|
||||
|
||||
def _must_not_be_called(self, path):
|
||||
pytest.fail(f"MediaConnector.fetch_audio should not be called for bare local path: {path}")
|
||||
|
||||
monkeypatch.setattr(MediaConnector, "fetch_audio", _must_not_be_called)
|
||||
|
||||
assert _run(estimator_cache, str(wav_file)) == EXPECTED_REF_CODE_LEN
|
||||
|
||||
|
||||
def test_file_uri(estimator_cache, wav_file):
|
||||
"""file:// URI is routed through MediaConnector, which handles it natively."""
|
||||
assert _run(estimator_cache, f"file://{wav_file}") == EXPECTED_REF_CODE_LEN
|
||||
|
||||
|
||||
def test_http_url(estimator_cache, monkeypatch):
|
||||
"""http:// URL is routed through MediaConnector.fetch_audio (mocked to avoid network)."""
|
||||
from vllm.multimodal.media import MediaConnector
|
||||
|
||||
audio = np.zeros(TEST_SR * TEST_DURATION_S, dtype=np.float32)
|
||||
|
||||
def _fake_fetch_audio(self, path):
|
||||
return audio, TEST_SR
|
||||
|
||||
monkeypatch.setattr(MediaConnector, "fetch_audio", _fake_fetch_audio)
|
||||
|
||||
assert _run(estimator_cache, "http://example.com/ref.wav") == EXPECTED_REF_CODE_LEN
|
||||
|
||||
|
||||
def test_data_uri(estimator_cache):
|
||||
"""data: URI is routed through MediaConnector, which decodes base64 natively."""
|
||||
data_uri = "data:audio/wav;base64," + base64.b64encode(_wav_bytes()).decode("ascii")
|
||||
assert _run(estimator_cache, data_uri) == EXPECTED_REF_CODE_LEN
|
||||
Reference in new issue
Block a user