mirror of
https://github.com/modelscope/FunASR.git
synced 2026-10-12 02:19:18 +00:00
fix(server): reuse Fun-ASR-Nano fallback after vLLM failure (#3402)
Closes #3401.
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user