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