feat(server): support custom model path and hub

Adds funasr-server --model-path/--hub support with maintainer follow-up for default Fun-ASR-Nano hub routing and custom fallback loading. Validated with py_compile, focused CLI/server tests, and git diff check.
This commit is contained in:
liudonghua
2026-07-22 17:54:16 +08:00
committed by GitHub
parent dc9758dd73
commit 16e3654942
3 changed files with 112 additions and 14 deletions
+30 -12
View File
@@ -101,7 +101,7 @@ def prepare_audio_for_inference(audio_data, sr, target_sr=16000):
return audio_data.astype(np.float32), sr
def create_app(device: str = "cuda", preload_model: str = "auto") -> FastAPI:
def create_app(device: str = "cuda", preload_model: str = "auto", model_path: str = None, hub: str = "ms") -> FastAPI:
if preload_model == "auto":
preload_model = "fun-asr-nano" if device.startswith("cuda") else "sensevoice"
@@ -110,6 +110,8 @@ def create_app(device: str = "cuda", preload_model: str = "auto") -> FastAPI:
app.state.engine = None
app.state.vad_model = None
app.state.fallback_models = {}
app.state.model_path = model_path
app.state.hub = hub
# Non-LLM model configs (use AutoModel, no vLLM)
FALLBACK_CONFIGS = {
@@ -135,9 +137,12 @@ def create_app(device: str = "cuda", preload_model: str = "auto") -> FastAPI:
logger.info("Loading Fun-ASR-Nano vLLM engine...")
t0 = time.time()
# Use custom model_path if provided, otherwise default
vllm_model = app.state.model_path if app.state.model_path else "FunAudioLLM/Fun-ASR-Nano-2512"
vllm_hub = app.state.hub if app.state.model_path else "hf"
app.state.engine = FunASRNanoVLLM.from_pretrained(
model="FunAudioLLM/Fun-ASR-Nano-2512",
hub="hf",
model=vllm_model,
hub=vllm_hub,
device=device,
dtype="bf16",
max_model_len=4096,
@@ -154,8 +159,8 @@ def create_app(device: str = "cuda", preload_model: str = "auto") -> FastAPI:
app.state.use_vllm = False
from funasr import AutoModel
cfg = {
"model": "FunAudioLLM/Fun-ASR-Nano-2512",
"hub": "hf",
"model": app.state.model_path if app.state.model_path else "FunAudioLLM/Fun-ASR-Nano-2512",
"hub": app.state.hub if app.state.model_path else "hf",
"trust_remote_code": True,
"vad_model": "fsmn-vad",
"vad_kwargs": {"max_single_segment_time": 30000},
@@ -163,19 +168,25 @@ def create_app(device: str = "cuda", preload_model: str = "auto") -> FastAPI:
"disable_update": True,
}
app.state.fallback_models["fun-asr-nano"] = AutoModel(**cfg)
logger.info("Fallback AutoModel loaded for fun-asr-nano.")
logger.info(f"Fallback AutoModel loaded for fun-asr-nano with model={cfg['model']}, hub={cfg['hub']}.")
def _load_fallback(name: str):
"""Load non-LLM model via AutoModel."""
if name in app.state.fallback_models:
return app.state.fallback_models[name]
if name not in FALLBACK_CONFIGS:
if name not in FALLBACK_CONFIGS and not app.state.model_path:
return None
from funasr import AutoModel
cfg = FALLBACK_CONFIGS[name].copy()
cfg = FALLBACK_CONFIGS.get(name, {}).copy()
# Override with custom model_path and hub if provided
if app.state.model_path:
cfg["model"] = app.state.model_path
cfg["hub"] = app.state.hub
elif app.state.hub:
cfg["hub"] = app.state.hub
cfg["device"] = device
cfg["disable_update"] = True
logger.info(f"Loading fallback model '{name}'...")
logger.info(f"Loading fallback model '{name}' with model={cfg['model']}, hub={cfg['hub']}...")
model = AutoModel(**cfg)
app.state.fallback_models[name] = model
return model
@@ -258,7 +269,11 @@ def create_app(device: str = "cuda", preload_model: str = "auto") -> FastAPI:
return {"text": text, "segments": segments, "duration": duration}
# Pre-load
if preload_model == "fun-asr-nano":
if app.state.model_path:
# When custom model_path is provided, use it as the model name for loading
logger.info(f"Loading custom model: {app.state.model_path} (hub: {app.state.hub})")
_load_fallback("custom")
elif preload_model == "fun-asr-nano":
_load_vllm_engine()
else:
_load_fallback(preload_model)
@@ -288,7 +303,7 @@ def create_app(device: str = "cuda", preload_model: str = "auto") -> FastAPI:
result = _process_fallback("fun-asr-nano", tmp_path, language=language)
finally:
os.unlink(tmp_path)
elif model in FALLBACK_CONFIGS:
elif model in FALLBACK_CONFIGS or model == "custom":
suffix = os.path.splitext(file.filename)[1] if file.filename else ".wav"
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
tmp.write(content)
@@ -298,7 +313,8 @@ def create_app(device: str = "cuda", preload_model: str = "auto") -> FastAPI:
finally:
os.unlink(tmp_path)
else:
raise HTTPException(400, f"Unknown model '{model}'. Available: fun-asr-nano, {', '.join(FALLBACK_CONFIGS.keys())}")
available = ["fun-asr-nano", "custom"] + list(FALLBACK_CONFIGS.keys())
raise HTTPException(400, f"Unknown model '{model}'. Available: {', '.join(available)}")
t1 = time.perf_counter()
@@ -352,6 +368,8 @@ def create_app(device: str = "cuda", preload_model: str = "auto") -> FastAPI:
@app.get("/v1/models")
async def list_models():
all_models = ["fun-asr-nano"] + list(FALLBACK_CONFIGS.keys())
if app.state.model_path:
all_models.append("custom")
return JSONResponse({"object": "list", "data": [{"id": n, "object": "model"} for n in all_models]})
@app.get("/health")
+10 -1
View File
@@ -5,6 +5,8 @@ Usage:
funasr-server # default: sensevoice on cuda:0, port 8000
funasr-server --device cpu --port 9000
funasr-server --model paraformer
funasr-server --model-path /path/to/local/model
funasr-server --model-path username/paraformer --hub hf
"""
import argparse
@@ -21,6 +23,8 @@ Examples:
funasr-server --device cpu # Start on CPU
funasr-server --model paraformer # Use Paraformer model
funasr-server --port 9000 # Custom port
funasr-server --model-path /path/to/local/model # Use local model
funasr-server --model-path username/model --hub hf # Use HuggingFace model
Then use with OpenAI SDK:
from openai import OpenAI
@@ -32,6 +36,8 @@ Then use with OpenAI SDK:
parser.add_argument("--port", type=int, default=8000, help="Port (default: 8000)")
parser.add_argument("--device", default="cuda", help="Device: cuda, cpu, mps (default: cuda)")
parser.add_argument("--model", default="auto", help="Pre-load model: auto (GPU=fun-asr-nano, CPU=sensevoice), sensevoice, paraformer, fun-asr-nano")
parser.add_argument("--model-path", default=None, help="Local model path or model ID (overrides --model)")
parser.add_argument("--hub", default="ms", help="Model hub: ms (ModelScope), hf (HuggingFace) (default: ms)")
args = parser.parse_args()
try:
@@ -49,12 +55,15 @@ Then use with OpenAI SDK:
# Use inline app to avoid path issues
from funasr.bin._server_app import create_app
app = create_app(device=args.device, preload_model=args.model)
app = create_app(device=args.device, preload_model=args.model, model_path=args.model_path, hub=args.hub)
print(f"╔══════════════════════════════════════════════╗")
print(f"║ FunASR Server v1.3.6 ║")
print(f"║ Device: {args.device:<8} ║")
print(f"║ Model: {args.model:<12} ║")
if args.model_path:
print(f"║ Model Path: {args.model_path:<25} ║")
print(f"║ Hub: {args.hub:<8} ║")
print(f"║ URL: http://{args.host}:{args.port}/v1 ║")
print(f"║ Docs: http://{args.host}:{args.port}/docs ║")
print(f"╚══════════════════════════════════════════════╝")
+72 -1
View File
@@ -9,8 +9,18 @@ SERVICE_PATH = REPO_ROOT / "funasr" / "bin" / "_server_app.py"
def load_server_app(monkeypatch):
class DummyFastAPI:
def __init__(self, *args, **kwargs):
self.state = types.SimpleNamespace()
def post(self, *args, **kwargs):
return lambda func: func
def get(self, *args, **kwargs):
return lambda func: func
fastapi_stub = types.ModuleType("fastapi")
fastapi_stub.FastAPI = object
fastapi_stub.FastAPI = DummyFastAPI
fastapi_stub.UploadFile = object
fastapi_stub.File = lambda *args, **kwargs: None
fastapi_stub.Form = lambda *args, **kwargs: None
@@ -31,6 +41,39 @@ def load_server_app(monkeypatch):
return module
def install_dummy_funasr(monkeypatch):
class DummyAutoModel:
instances = []
def __init__(self, **kwargs):
self.kwargs = kwargs
self.__class__.instances.append(kwargs)
funasr_stub = types.ModuleType("funasr")
funasr_stub.AutoModel = DummyAutoModel
monkeypatch.setitem(sys.modules, "funasr", funasr_stub)
return DummyAutoModel
def install_dummy_vllm(monkeypatch):
class DummyVLLM:
calls = []
@classmethod
def from_pretrained(cls, **kwargs):
cls.calls.append(kwargs)
return object()
monkeypatch.setitem(sys.modules, "funasr.models", types.ModuleType("funasr.models"))
monkeypatch.setitem(
sys.modules, "funasr.models.fun_asr_nano", types.ModuleType("funasr.models.fun_asr_nano")
)
vllm_module = types.ModuleType("funasr.models.fun_asr_nano.inference_vllm")
vllm_module.FunASRNanoVLLM = DummyVLLM
monkeypatch.setitem(sys.modules, "funasr.models.fun_asr_nano.inference_vllm", vllm_module)
return DummyVLLM
def test_fallback_segments_split_long_fun_asr_server_text(monkeypatch):
module = load_server_app(monkeypatch)
text = (
@@ -56,3 +99,31 @@ def test_fallback_segments_keep_short_text_single_cue(monkeypatch):
assert module.build_openai_fallback_segments("hello", duration=1.25) == [
{"start": 0.0, "end": 1.25, "text": "hello"}
]
def test_default_fun_asr_nano_uses_huggingface_hub(monkeypatch):
module = load_server_app(monkeypatch)
DummyAutoModel = install_dummy_funasr(monkeypatch)
DummyVLLM = install_dummy_vllm(monkeypatch)
module.create_app(device="cuda", preload_model="fun-asr-nano", hub="ms")
assert DummyVLLM.calls[0]["model"] == "FunAudioLLM/Fun-ASR-Nano-2512"
assert DummyVLLM.calls[0]["hub"] == "hf"
assert DummyAutoModel.instances[0]["model"] == "fsmn-vad"
def test_custom_model_path_fallback_uses_empty_config_and_requested_hub(monkeypatch):
module = load_server_app(monkeypatch)
DummyAutoModel = install_dummy_funasr(monkeypatch)
app = module.create_app(
device="cpu",
preload_model="sensevoice",
model_path="org/custom-sensevoice",
hub="hf",
)
assert DummyAutoModel.instances[0]["model"] == "org/custom-sensevoice"
assert DummyAutoModel.instances[0]["hub"] == "hf"
assert app.state.fallback_models["custom"] is not None