mirror of
https://github.com/modelscope/FunASR.git
synced 2026-10-12 02:19:18 +00:00
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:
+30
-12
@@ -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
@@ -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"╚══════════════════════════════════════════════╝")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user