1+ import asyncio
12import importlib .util
23import sys
34import types
45from pathlib import Path
56
7+ import pytest
8+
69
710REPO_ROOT = Path (__file__ ).resolve ().parents [1 ]
811SERVICE_PATH = REPO_ROOT / "funasr" / "bin" / "_server_app.py"
@@ -12,12 +15,21 @@ def load_server_app(monkeypatch):
1215 class DummyFastAPI :
1316 def __init__ (self , * args , ** kwargs ):
1417 self .state = types .SimpleNamespace ()
18+ self .routes = {}
19+
20+ def post (self , path , * args , ** kwargs ):
21+ def decorator (func ):
22+ self .routes [("POST" , path )] = func
23+ return func
1524
16- def post (self , * args , ** kwargs ):
17- return lambda func : func
25+ return decorator
1826
19- def get (self , * args , ** kwargs ):
20- return lambda func : func
27+ def get (self , path , * args , ** kwargs ):
28+ def decorator (func ):
29+ self .routes [("GET" , path )] = func
30+ return func
31+
32+ return decorator
2133
2234 fastapi_stub = types .ModuleType ("fastapi" )
2335 fastapi_stub .FastAPI = DummyFastAPI
@@ -41,14 +53,27 @@ def get(self, *args, **kwargs):
4153 return module
4254
4355
44- def install_dummy_funasr (monkeypatch ):
56+ def install_dummy_funasr (monkeypatch , fail_once_for_models = ()):
57+ remaining_failures = {model : 1 for model in fail_once_for_models }
58+
4559 class DummyAutoModel :
4660 instances = []
61+ attempts = []
4762
4863 def __init__ (self , ** kwargs ):
64+ self .__class__ .attempts .append (kwargs )
65+ model = kwargs .get ("model" )
66+ if remaining_failures .get (model , 0 ):
67+ remaining_failures [model ] -= 1
68+ raise RuntimeError (f"{ model } unavailable" )
4969 self .kwargs = kwargs
5070 self .__class__ .instances .append (kwargs )
5171
72+ def generate (self , ** kwargs ):
73+ if self .kwargs .get ("model" ) == "fsmn-vad" :
74+ return [{"value" : [[0 , 1000 ]]}]
75+ return [{"text" : "transcript" }]
76+
5277 funasr_stub = types .ModuleType ("funasr" )
5378 funasr_stub .AutoModel = DummyAutoModel
5479 monkeypatch .setitem (sys .modules , "funasr" , funasr_stub )
@@ -64,7 +89,10 @@ def from_pretrained(cls, **kwargs):
6489 cls .calls .append (kwargs )
6590 if raise_on_load :
6691 raise RuntimeError ("vllm unavailable" )
67- return object ()
92+ return cls ()
93+
94+ def generate (self , inputs , ** kwargs ):
95+ return [{"text" : "transcript" } for _ in inputs ]
6896
6997 monkeypatch .setitem (sys .modules , "funasr.models" , types .ModuleType ("funasr.models" ))
7098 monkeypatch .setitem (
@@ -76,6 +104,26 @@ def from_pretrained(cls, **kwargs):
76104 return DummyVLLM
77105
78106
107+ class DummyUpload :
108+ filename = "audio.wav"
109+
110+ async def read (self ):
111+ return b"not-a-real-wave-file"
112+
113+
114+ def transcribe_nano (app ):
115+ transcribe = app .routes [("POST" , "/v1/audio/transcriptions" )]
116+ return asyncio .run (
117+ transcribe (
118+ file = DummyUpload (),
119+ model = "fun-asr-nano" ,
120+ language = None ,
121+ response_format = "json" ,
122+ spk = False ,
123+ )
124+ )
125+
126+
79127def test_fallback_segments_split_long_fun_asr_server_text (monkeypatch ):
80128 module = load_server_app (monkeypatch )
81129 text = (
@@ -150,6 +198,52 @@ def test_default_fun_asr_nano_fallback_uses_requested_modelscope_hub(monkeypatch
150198 assert fallback ["hub" ] == "ms"
151199
152200
201+ def test_fun_asr_nano_reuses_fallback_after_vllm_failure (monkeypatch ):
202+ module = load_server_app (monkeypatch )
203+ DummyAutoModel = install_dummy_funasr (monkeypatch )
204+ DummyVLLM = install_dummy_vllm (monkeypatch , raise_on_load = True )
205+ monkeypatch .setattr (module .sf , "info" , lambda path : types .SimpleNamespace (duration = 1.0 ))
206+
207+ app = module .create_app (device = "cuda" , preload_model = "fun-asr-nano" , hub = "ms" )
208+
209+ for _ in range (2 ):
210+ assert transcribe_nano (app ) == {"text" : "transcript" }
211+
212+ nano_fallbacks = [
213+ config
214+ for config in DummyAutoModel .instances
215+ if config .get ("model" ) == "FunAudioLLM/Fun-ASR-Nano-2512"
216+ ]
217+ assert len (DummyVLLM .calls ) == 1
218+ assert len (nano_fallbacks ) == 1
219+
220+
221+ def test_partial_vllm_setup_is_not_cached_after_fallback_failure (monkeypatch ):
222+ module = load_server_app (monkeypatch )
223+ nano_model = "FunAudioLLM/Fun-ASR-Nano-2512"
224+ DummyAutoModel = install_dummy_funasr (
225+ monkeypatch ,
226+ fail_once_for_models = ("fsmn-vad" , nano_model ),
227+ )
228+ DummyVLLM = install_dummy_vllm (monkeypatch )
229+ monkeypatch .setattr (
230+ module .sf ,
231+ "read" ,
232+ lambda stream : (module .np .ones (16000 , dtype = module .np .float32 ), 16000 ),
233+ )
234+
235+ app = module .create_app (device = "cuda" , preload_model = "sensevoice" , hub = "ms" )
236+
237+ with pytest .raises (RuntimeError , match = f"{ nano_model } unavailable" ):
238+ transcribe_nano (app )
239+
240+ assert app .state .engine is None
241+
242+ assert transcribe_nano (app ) == {"text" : "transcript" }
243+ assert len (DummyVLLM .calls ) == 2
244+ assert len ([attempt for attempt in DummyAutoModel .attempts if attempt .get ("model" ) == "fsmn-vad" ]) == 2
245+
246+
153247def test_custom_model_path_fallback_uses_empty_config_and_requested_hub (monkeypatch ):
154248 module = load_server_app (monkeypatch )
155249 DummyAutoModel = install_dummy_funasr (monkeypatch )
0 commit comments