From e0fbedfbacf26b1daaca9656e6e26a64970b6b3e Mon Sep 17 00:00:00 2001 From: zhifu gao Date: Fri, 24 Jul 2026 01:33:53 +0800 Subject: [PATCH] fix(server): reuse Fun-ASR-Nano fallback after vLLM failure (#3402) Closes #3401. --- funasr/bin/_server_app.py | 10 ++- tests/test_server_app_openai_segments.py | 106 +++++++++++++++++++++-- 2 files changed, 106 insertions(+), 10 deletions(-) diff --git a/funasr/bin/_server_app.py b/funasr/bin/_server_app.py index 64e821da3..3a7e47808 100644 --- a/funasr/bin/_server_app.py +++ b/funasr/bin/_server_app.py @@ -150,7 +150,7 @@ def create_app(device: str = "cuda", preload_model: str = "auto", model_path: st def _load_vllm_engine(): """Load Fun-ASR-Nano vLLM engine. Falls back to AutoModel if vLLM unavailable.""" - if app.state.engine is not None: + if app.state.engine is not None or "fun-asr-nano" in app.state.fallback_models: return try: from funasr.models.fun_asr_nano.inference_vllm import FunASRNanoVLLM @@ -162,7 +162,7 @@ def create_app(device: str = "cuda", preload_model: str = "auto", model_path: st # cases, honor the server-level hub selection. vllm_model = app.state.model_path if app.state.model_path else "FunAudioLLM/Fun-ASR-Nano-2512" vllm_hub = app.state.hub - app.state.engine = FunASRNanoVLLM.from_pretrained( + engine = FunASRNanoVLLM.from_pretrained( model=vllm_model, hub=vllm_hub, device=device, @@ -171,10 +171,12 @@ def create_app(device: str = "cuda", preload_model: str = "auto", model_path: st gpu_memory_utilization=0.5, ) logger.info(f"vLLM engine ready in {time.time()-t0:.1f}s") - app.state.use_vllm = True logger.info("Loading VAD model...") - app.state.vad_model = _AutoModel(model="fsmn-vad", device=device, disable_update=True) + vad_model = _AutoModel(model="fsmn-vad", device=device, disable_update=True) + app.state.engine = engine + app.state.vad_model = vad_model + app.state.use_vllm = True logger.info("VAD ready.") except Exception as e: logger.warning(f"vLLM failed ({e}), falling back to AutoModel for fun-asr-nano") diff --git a/tests/test_server_app_openai_segments.py b/tests/test_server_app_openai_segments.py index fadbdaa2a..3e3e3eb4d 100644 --- a/tests/test_server_app_openai_segments.py +++ b/tests/test_server_app_openai_segments.py @@ -1,8 +1,11 @@ +import asyncio import importlib.util import sys import types from pathlib import Path +import pytest + REPO_ROOT = Path(__file__).resolve().parents[1] SERVICE_PATH = REPO_ROOT / "funasr" / "bin" / "_server_app.py" @@ -12,12 +15,21 @@ def load_server_app(monkeypatch): class DummyFastAPI: def __init__(self, *args, **kwargs): self.state = types.SimpleNamespace() + self.routes = {} - def post(self, *args, **kwargs): - return lambda func: func + def post(self, path, *args, **kwargs): + def decorator(func): + self.routes[("POST", path)] = func + return func - def get(self, *args, **kwargs): - return lambda func: func + return decorator + + def get(self, path, *args, **kwargs): + def decorator(func): + self.routes[("GET", path)] = func + return func + + return decorator fastapi_stub = types.ModuleType("fastapi") fastapi_stub.FastAPI = DummyFastAPI @@ -41,14 +53,27 @@ def load_server_app(monkeypatch): return module -def install_dummy_funasr(monkeypatch): +def install_dummy_funasr(monkeypatch, fail_once_for_models=()): + remaining_failures = {model: 1 for model in fail_once_for_models} + class DummyAutoModel: instances = [] + attempts = [] def __init__(self, **kwargs): + self.__class__.attempts.append(kwargs) + model = kwargs.get("model") + if remaining_failures.get(model, 0): + remaining_failures[model] -= 1 + raise RuntimeError(f"{model} unavailable") self.kwargs = kwargs self.__class__.instances.append(kwargs) + def generate(self, **kwargs): + if self.kwargs.get("model") == "fsmn-vad": + return [{"value": [[0, 1000]]}] + return [{"text": "transcript"}] + funasr_stub = types.ModuleType("funasr") funasr_stub.AutoModel = DummyAutoModel monkeypatch.setitem(sys.modules, "funasr", funasr_stub) @@ -64,7 +89,10 @@ def install_dummy_vllm(monkeypatch, raise_on_load=False): cls.calls.append(kwargs) if raise_on_load: raise RuntimeError("vllm unavailable") - return object() + return cls() + + def generate(self, inputs, **kwargs): + return [{"text": "transcript"} for _ in inputs] monkeypatch.setitem(sys.modules, "funasr.models", types.ModuleType("funasr.models")) monkeypatch.setitem( @@ -76,6 +104,26 @@ def install_dummy_vllm(monkeypatch, raise_on_load=False): return DummyVLLM +class DummyUpload: + filename = "audio.wav" + + async def read(self): + return b"not-a-real-wave-file" + + +def transcribe_nano(app): + transcribe = app.routes[("POST", "/v1/audio/transcriptions")] + return asyncio.run( + transcribe( + file=DummyUpload(), + model="fun-asr-nano", + language=None, + response_format="json", + spk=False, + ) + ) + + def test_fallback_segments_split_long_fun_asr_server_text(monkeypatch): module = load_server_app(monkeypatch) text = ( @@ -150,6 +198,52 @@ def test_default_fun_asr_nano_fallback_uses_requested_modelscope_hub(monkeypatch assert fallback["hub"] == "ms" +def test_fun_asr_nano_reuses_fallback_after_vllm_failure(monkeypatch): + module = load_server_app(monkeypatch) + DummyAutoModel = install_dummy_funasr(monkeypatch) + DummyVLLM = install_dummy_vllm(monkeypatch, raise_on_load=True) + monkeypatch.setattr(module.sf, "info", lambda path: types.SimpleNamespace(duration=1.0)) + + app = module.create_app(device="cuda", preload_model="fun-asr-nano", hub="ms") + + for _ in range(2): + assert transcribe_nano(app) == {"text": "transcript"} + + nano_fallbacks = [ + config + for config in DummyAutoModel.instances + if config.get("model") == "FunAudioLLM/Fun-ASR-Nano-2512" + ] + assert len(DummyVLLM.calls) == 1 + assert len(nano_fallbacks) == 1 + + +def test_partial_vllm_setup_is_not_cached_after_fallback_failure(monkeypatch): + module = load_server_app(monkeypatch) + nano_model = "FunAudioLLM/Fun-ASR-Nano-2512" + DummyAutoModel = install_dummy_funasr( + monkeypatch, + fail_once_for_models=("fsmn-vad", nano_model), + ) + DummyVLLM = install_dummy_vllm(monkeypatch) + monkeypatch.setattr( + module.sf, + "read", + lambda stream: (module.np.ones(16000, dtype=module.np.float32), 16000), + ) + + app = module.create_app(device="cuda", preload_model="sensevoice", hub="ms") + + with pytest.raises(RuntimeError, match=f"{nano_model} unavailable"): + transcribe_nano(app) + + assert app.state.engine is None + + assert transcribe_nano(app) == {"text": "transcript"} + assert len(DummyVLLM.calls) == 2 + assert len([attempt for attempt in DummyAutoModel.attempts if attempt.get("model") == "fsmn-vad"]) == 2 + + def test_custom_model_path_fallback_uses_empty_config_and_requested_hub(monkeypatch): module = load_server_app(monkeypatch) DummyAutoModel = install_dummy_funasr(monkeypatch)