Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 30 additions & 12 deletions funasr/bin/_server_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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 = {
Expand All @@ -135,9 +137,12 @@ def _load_vllm_engine():

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,
Expand All @@ -154,28 +159,34 @@ def _load_vllm_engine():
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},
"device": device,
"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
Expand Down Expand Up @@ -258,7 +269,11 @@ def _process_fallback(model_name, audio_path, language=None):
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)
Expand Down Expand Up @@ -288,7 +303,7 @@ async def transcribe(
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)
Expand All @@ -298,7 +313,8 @@ async def transcribe(
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()

Expand Down Expand Up @@ -352,6 +368,8 @@ async def asr_endpoint(
@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")
Expand Down
11 changes: 10 additions & 1 deletion funasr/bin/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
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
Expand All @@ -21,6 +23,8 @@ def main():
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
Expand All @@ -32,6 +36,8 @@ def main():
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:
Expand All @@ -49,12 +55,15 @@ def main():
# 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"╚══════════════════════════════════════════════╝")
Expand Down
73 changes: 72 additions & 1 deletion tests/test_server_app_openai_segments.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,18 @@


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
Expand All @@ -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 = (
Expand All @@ -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