feat: add funasr-server CLI command for one-line API deployment

pip install funasr && funasr-server --device cuda
Starts OpenAI-compatible /v1/audio/transcriptions endpoint.
Verified: import, startup, transcription all work end-to-end.
This commit is contained in:
游雁
2026-05-24 02:17:41 +08:00
parent 6fdfac3115
commit 97ec3ae2ee
3 changed files with 171 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
"""Internal: FastAPI app for funasr-server command."""
import tempfile
import time
import os
import re
import logging
from typing import Optional
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
from fastapi.responses import JSONResponse
logger = logging.getLogger("funasr.server")
MODEL_CONFIGS = {
"sensevoice": {
"model": "iic/SenseVoiceSmall",
"vad_model": "fsmn-vad",
"vad_kwargs": {"max_single_segment_time": 30000},
},
"paraformer": {
"model": "paraformer-zh",
"vad_model": "fsmn-vad",
"punc_model": "ct-punc",
},
"paraformer-en": {
"model": "paraformer-en",
"vad_model": "fsmn-vad",
},
"fun-asr-nano": {
"model": "FunAudioLLM/Fun-ASR-Nano-2512",
"hub": "hf",
"trust_remote_code": True,
"vad_model": "fsmn-vad",
"vad_kwargs": {"max_single_segment_time": 30000},
},
}
def create_app(device: str = "cuda", preload_model: str = "sensevoice") -> FastAPI:
app = FastAPI(title="FunASR Server", version="1.3.2")
app.state.models = {}
app.state.device = device
def _load_model(name: str):
if name in app.state.models:
return app.state.models[name]
if name not in MODEL_CONFIGS:
raise ValueError(f"Unknown model: {name}. Available: {list(MODEL_CONFIGS.keys())}")
from funasr import AutoModel
cfg = MODEL_CONFIGS[name].copy()
cfg["device"] = app.state.device
cfg["disable_update"] = True
logger.info(f"Loading '{name}' on {device}...")
t0 = time.time()
model = AutoModel(**cfg)
logger.info(f"'{name}' ready in {time.time()-t0:.1f}s")
app.state.models[name] = model
return model
# Pre-load
_load_model(preload_model)
@app.post("/v1/audio/transcriptions")
async def transcribe(
file: UploadFile = File(...),
model: str = Form(default="sensevoice"),
language: Optional[str] = Form(default=None),
response_format: Optional[str] = Form(default="json"),
):
if model not in MODEL_CONFIGS:
raise HTTPException(400, f"Unknown model '{model}'. Available: {list(MODEL_CONFIGS.keys())}")
suffix = os.path.splitext(file.filename)[1] if file.filename else ".wav"
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
tmp.write(await file.read())
tmp_path = tmp.name
try:
asr = _load_model(model)
kwargs = {"input": tmp_path, "batch_size": 1}
if language:
kwargs["language"] = language
result = asr.generate(**kwargs)
text = re.sub(r'<\|[^|]*\|>', '', result[0]["text"]).strip()
if response_format == "verbose_json":
segments = []
if "sentence_info" in result[0]:
for s in result[0]["sentence_info"]:
segments.append({"start": s.get("start",0)/1000, "end": s.get("end",0)/1000, "text": re.sub(r'<\|[^|]*\|>','',s.get("text","")).strip(), "speaker": s.get("spk")})
return JSONResponse({"text": text, "segments": segments, "model": model})
return JSONResponse({"text": text})
except Exception as e:
raise HTTPException(500, str(e))
finally:
os.unlink(tmp_path)
@app.get("/v1/models")
async def list_models():
return JSONResponse({"object": "list", "data": [{"id": n, "object": "model", "owned_by": "funasr"} for n in MODEL_CONFIGS]})
@app.get("/health")
async def health():
return {"status": "ok", "device": app.state.device, "models_loaded": list(app.state.models.keys())}
return app
+66
View File
@@ -0,0 +1,66 @@
"""
FunASR Server — OpenAI-compatible speech recognition API.
Usage:
funasr-server # default: sensevoice on cuda:0, port 8000
funasr-server --device cpu --port 9000
funasr-server --model paraformer
"""
import argparse
import sys
def main():
parser = argparse.ArgumentParser(
description="FunASR OpenAI-Compatible API Server",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
funasr-server # Start with SenseVoice on GPU
funasr-server --device cpu # Start on CPU
funasr-server --model paraformer # Use Paraformer model
funasr-server --port 9000 # Custom port
Then use with OpenAI SDK:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="x")
result = client.audio.transcriptions.create(model="sensevoice", file=open("a.wav","rb"))
""",
)
parser.add_argument("--host", default="0.0.0.0", help="Bind address (default: 0.0.0.0)")
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="sensevoice", help="Pre-load model: sensevoice, paraformer, fun-asr-nano (default: sensevoice)")
args = parser.parse_args()
try:
import uvicorn
import fastapi
except ImportError:
print("Error: funasr-server requires additional packages.")
print("Install with: pip install fastapi uvicorn python-multipart")
sys.exit(1)
# Import and configure the app
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'examples', 'openai_api'))
# 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)
print(f"╔══════════════════════════════════════════════╗")
print(f"║ FunASR Server v1.3.2 ║")
print(f"║ Device: {args.device:<8} ║")
print(f"║ Model: {args.model:<12} ║")
print(f"║ URL: http://{args.host}:{args.port}/v1 ║")
print(f"║ Docs: http://{args.host}:{args.port}/docs ║")
print(f"╚══════════════════════════════════════════════╝")
uvicorn.run(app, host=args.host, port=args.port)
if __name__ == "__main__":
main()
+1
View File
@@ -154,6 +154,7 @@ setup(
entry_points={
"console_scripts": [
"funasr = funasr.bin.inference:main_hydra",
"funasr-server = funasr.bin.server:main",
"funasr-train = funasr.bin.train:main_hydra",
"funasr-train-ds = funasr.bin.train_ds:main_hydra",
"funasr-export = funasr.bin.export:main_hydra",