diff --git a/funasr/bin/_server_app.py b/funasr/bin/_server_app.py new file mode 100644 index 000000000..dde92f0bf --- /dev/null +++ b/funasr/bin/_server_app.py @@ -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 diff --git a/funasr/bin/server.py b/funasr/bin/server.py new file mode 100644 index 000000000..7a4e6dd86 --- /dev/null +++ b/funasr/bin/server.py @@ -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() diff --git a/setup.py b/setup.py index 6c2a81254..2e7545dce 100644 --- a/setup.py +++ b/setup.py @@ -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",