diff --git a/.gitattributes b/.gitattributes index f837fd43e..9f1b343de 100644 --- a/.gitattributes +++ b/.gitattributes @@ -27,6 +27,7 @@ *.mov binary *.mp4 binary *.mp3 binary +*.wav binary *.flv binary *.fla binary *.swf binary diff --git a/.gitignore b/.gitignore index a7cd8f74e..bf5e28510 100644 --- a/.gitignore +++ b/.gitignore @@ -264,6 +264,8 @@ tmp_test vllm_omni/_version.py # output files *.wav +# Vendored test reference audio (small, intentionally checked in) +!tests/assets/**/*.wav .worktrees/ # CI overlay yamls materialized from tests/utils.py:_CI_OVERLAYS at test time tests/.ci_generated/ diff --git a/tests/assets/cosyvoice3/zero_shot_prompt.wav b/tests/assets/cosyvoice3/zero_shot_prompt.wav new file mode 100644 index 000000000..a7b9d9542 Binary files /dev/null and b/tests/assets/cosyvoice3/zero_shot_prompt.wav differ diff --git a/tests/assets/qwen3_tts/clone_2.wav b/tests/assets/qwen3_tts/clone_2.wav new file mode 100644 index 000000000..9b6eab230 Binary files /dev/null and b/tests/assets/qwen3_tts/clone_2.wav differ diff --git a/tests/e2e/offline_inference/test_cosyvoice3.py b/tests/e2e/offline_inference/test_cosyvoice3.py index 7206f1e7b..3624214a1 100644 --- a/tests/e2e/offline_inference/test_cosyvoice3.py +++ b/tests/e2e/offline_inference/test_cosyvoice3.py @@ -13,10 +13,8 @@ verifies a stable reference recipe: from __future__ import annotations import functools -import io import os from pathlib import Path -from urllib.request import urlopen import numpy as np import pytest @@ -25,6 +23,7 @@ from huggingface_hub import snapshot_download from vllm.sampling_params import SamplingParams from tests.helpers.mark import hardware_test +from tests.helpers.media import test_asset_path from tests.helpers.runtime import OmniRunner from tests.helpers.stage_config import get_deploy_config_path from vllm_omni.model_executor.models.cosyvoice3.config import CosyVoice3Config @@ -33,7 +32,9 @@ from vllm_omni.model_executor.models.cosyvoice3.tokenizer import get_qwen_tokeni MODEL = "FunAudioLLM/Fun-CosyVoice3-0.5B-2512" MODEL_DIR_ENV = "VLLM_OMNI_COSYVOICE3_MODEL_DIR" -REFERENCE_PROMPT_WAV_URL = "https://raw.githubusercontent.com/FunAudioLLM/CosyVoice/main/asset/zero_shot_prompt.wav" +# Vendored under tests/assets/cosyvoice3/ so the test does not depend on +# raw.githubusercontent.com being reachable from CI runners. +REFERENCE_PROMPT_WAV_PATH = test_asset_path("cosyvoice3/zero_shot_prompt.wav") REFERENCE_PROMPT_TEXT = "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。" REFERENCE_SYNTH_TEXT = ( "CosyVoice is undergoing a comprehensive upgrade, providing more accurate, " @@ -51,9 +52,7 @@ ASYNC_CHUNK_MODES = [ @functools.lru_cache(maxsize=1) def _load_reference_prompt_wav() -> tuple[np.ndarray, int]: - with urlopen(REFERENCE_PROMPT_WAV_URL, timeout=30) as resp: - data = resp.read() - audio, sr = sf.read(io.BytesIO(data), dtype="float32", always_2d=False) + audio, sr = sf.read(str(REFERENCE_PROMPT_WAV_PATH), dtype="float32", always_2d=False) if isinstance(audio, np.ndarray) and audio.ndim > 1: audio = np.mean(audio, axis=-1) return np.asarray(audio, dtype=np.float32), int(sr) diff --git a/tests/e2e/offline_inference/test_qwen3_tts_base.py b/tests/e2e/offline_inference/test_qwen3_tts_base.py index af2b5195b..135421e6b 100644 --- a/tests/e2e/offline_inference/test_qwen3_tts_base.py +++ b/tests/e2e/offline_inference/test_qwen3_tts_base.py @@ -16,10 +16,12 @@ os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0" import pytest from tests.helpers.mark import hardware_test +from tests.helpers.media import load_test_audio_data_url from tests.helpers.stage_config import get_deploy_config_path, modify_stage_config MODEL = "Qwen/Qwen3-TTS-12Hz-0.6B-Base" -REF_AUDIO_URL = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-TTS-Repo/clone_2.wav" +# See tests/e2e/online_serving/test_qwen3_tts_base.py for the vendored-asset rationale. +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." diff --git a/tests/e2e/online_serving/test_cosyvoice3_tts.py b/tests/e2e/online_serving/test_cosyvoice3_tts.py index 76e5e4f49..5851e26ff 100644 --- a/tests/e2e/online_serving/test_cosyvoice3_tts.py +++ b/tests/e2e/online_serving/test_cosyvoice3_tts.py @@ -15,13 +15,16 @@ os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "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 = "FunAudioLLM/Fun-CosyVoice3-0.5B-2512" -# Official CosyVoice zero-shot prompt audio and its transcript -REF_AUDIO_URL = "https://raw.githubusercontent.com/FunAudioLLM/CosyVoice/main/asset/zero_shot_prompt.wav" +# Official CosyVoice zero-shot prompt audio and its transcript. Vendored under +# tests/assets/ so the server does not depend on raw.githubusercontent.com being +# reachable at request time (same rationale as issue #3263 for Qwen3-TTS). +REF_AUDIO_URL = load_test_audio_data_url("cosyvoice3/zero_shot_prompt.wav") REF_TEXT = "希望你以后能够做的比我还好呦。" diff --git a/tests/e2e/online_serving/test_qwen3_tts_base.py b/tests/e2e/online_serving/test_qwen3_tts_base.py index fd7bc43b5..dd1ef2bf4 100644 --- a/tests/e2e/online_serving/test_qwen3_tts_base.py +++ b/tests/e2e/online_serving/test_qwen3_tts_base.py @@ -15,12 +15,16 @@ os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "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 = "Qwen/Qwen3-TTS-12Hz-0.6B-Base" -REF_AUDIO_URL = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-TTS-Repo/clone_2.wav" +# Vendored under tests/assets/qwen3_tts/clone_2.wav so the server does not need +# to fetch the reference audio over HTTPS at request time (CI runners in some +# regions cannot reach the original Aliyun OSS host; see issue #3263). +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." diff --git a/tests/e2e/online_serving/test_qwen3_tts_base_expansion.py b/tests/e2e/online_serving/test_qwen3_tts_base_expansion.py index d86f96af0..6346a0712 100644 --- a/tests/e2e/online_serving/test_qwen3_tts_base_expansion.py +++ b/tests/e2e/online_serving/test_qwen3_tts_base_expansion.py @@ -12,6 +12,7 @@ import os 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 @@ -22,7 +23,8 @@ os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0" MODEL = "Qwen/Qwen3-TTS-12Hz-0.6B-Base" -REF_AUDIO_URL = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-TTS-Repo/clone_2.wav" +# See tests/e2e/online_serving/test_qwen3_tts_base.py for the vendored-asset rationale. +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." diff --git a/tests/helpers/media.py b/tests/helpers/media.py index c0fb97171..4c46153b4 100644 --- a/tests/helpers/media.py +++ b/tests/helpers/media.py @@ -496,6 +496,33 @@ def generate_synthetic_image( } +_TEST_ASSETS_ROOT = Path(__file__).resolve().parents[1] / "assets" + +_AUDIO_MIME_BY_SUFFIX = { + ".wav": "audio/wav", + ".mp3": "audio/mpeg", + ".flac": "audio/flac", + ".ogg": "audio/ogg", +} + + +def test_asset_path(relative_path: str | os.PathLike) -> Path: + """Resolve a path under ``tests/assets/`` to its absolute on-disk location.""" + return _TEST_ASSETS_ROOT / Path(relative_path) + + +def load_test_audio_data_url(relative_path: str | os.PathLike) -> str: + """Load a vendored test audio file under ``tests/assets/`` as a base64 data URL. + + Used by tests that need real reference audio (e.g. voice cloning) without + relying on the server's ability to fetch external URLs at request time. + """ + path = test_asset_path(relative_path) + mime = _AUDIO_MIME_BY_SUFFIX.get(path.suffix.lower(), "application/octet-stream") + encoded = base64.b64encode(path.read_bytes()).decode("ascii") + return f"data:{mime};base64,{encoded}" + + def decode_b64_image(b64: str): img = Image.open(io.BytesIO(base64.b64decode(b64))) img.load() @@ -653,5 +680,7 @@ __all__ = [ "generate_synthetic_audio", "generate_synthetic_image", "generate_synthetic_video", + "load_test_audio_data_url", "preprocess_text", + "test_asset_path", ]