Add FunASR streaming model download config

main
Bifang 2026-09-23 11:09:09 +08:00
parent 78bd71ff89
commit 2daa939b1f
10 changed files with 177 additions and 47 deletions

View File

@ -3,7 +3,12 @@
The frontend and backend start separately. The backend launcher loads three The frontend and backend start separately. The backend launcher loads three
required local assets: streaming ASR, FSMN VAD, and CAM++ speaker verification. required local assets: streaming ASR, FSMN VAD, and CAM++ speaker verification.
It starts the CAM++ model service and the WebSocket service and stops both It starts the CAM++ model service and the WebSocket service and stops both
together. No model is downloaded by the launcher. together. No model is downloaded by the launcher. Use the shared model manifest
and downloader to prepare the three required snapshots:
~~~powershell
python scripts/download_models.py --funasr-runtime
~~~
## Model directories ## Model directories
@ -11,7 +16,7 @@ Put the assets under models/ or set MODEL_DIR in .env. The default
FunASR names resolve to these local directories: FunASR names resolve to these local directories:
- models/iic/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online - models/iic/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online
- models/iic/speech_fsmn_vad_zh-cn-16k-common-pytorch (or the damo/ variant) - models/damo/speech_fsmn_vad_zh-cn-16k-common-pytorch
- models/iic/speech_campplus_sv_zh-cn_16k-common (or the damo/ variant) - models/iic/speech_campplus_sv_zh-cn_16k-common (or the damo/ variant)
If your directories have different names, set FUNASR_ASR_MODEL and If your directories have different names, set FUNASR_ASR_MODEL and
@ -28,6 +33,7 @@ then install the project dependencies:
cd D:\github-project\ASR\Asr-demo cd D:\github-project\ASR\Asr-demo
python -m pip install -r requirements-funasr.txt python -m pip install -r requirements-funasr.txt
python -m pip install -r requirements-auxiliary.txt python -m pip install -r requirements-auxiliary.txt
python scripts/download_models.py --funasr-runtime
if (-not (Test-Path .env)) { Copy-Item .env.funasr.example .env } if (-not (Test-Path .env)) { Copy-Item .env.funasr.example .env }
~~~ ~~~

View File

@ -7,7 +7,12 @@ The browser UI starts as a separate service.
Install a torch/torchaudio build for the host, then install Install a torch/torchaudio build for the host, then install
requirements-funasr.txt and requirements-auxiliary.txt. Copy requirements-funasr.txt and requirements-auxiliary.txt. Copy
.env.funasr.example to .env if no .env exists, and check MODEL_DIR. .env.funasr.example to .env if no .env exists, then download the three
required FunASR models with the shared project manifest:
~~~powershell
python scripts/download_models.py --funasr-runtime
~~~
Start the backend (CAM++ model service plus WebSocket) in one terminal: Start the backend (CAM++ model service plus WebSocket) in one terminal:

View File

@ -10,6 +10,24 @@
"alias": "0.6b", "alias": "0.6b",
"directory": "Qwen/Qwen3-ASR-0.6B", "directory": "Qwen/Qwen3-ASR-0.6B",
"description": "Qwen3-ASR 0.6B,GPU 默认轻量模型" "description": "Qwen3-ASR 0.6B,GPU 默认轻量模型"
},
"iic/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online": {
"alias": "paraformer-zh-streaming",
"directory": "iic/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online",
"description": "FunASR Paraformer streaming ASR used by the realtime backend",
"required_files": [
"configuration.json",
"config.yaml"
],
"any_files": [
"*.pb",
"*.pt",
"*.onnx",
"*.bin",
"*.model",
"*.safetensors"
],
"min_total_size_bytes": 1000000
} }
}, },
"auxiliary_models": { "auxiliary_models": {
@ -19,8 +37,13 @@
"kind": "vad", "kind": "vad",
"description": "FunASR FSMN VAD", "description": "FunASR FSMN VAD",
"revision": "v2.0.2", "revision": "v2.0.2",
"required_files": ["configuration.json", "config.yaml", "model.pb"], "required_files": [
"min_total_size_bytes": 1000000 "configuration.json",
"config.yaml",
"model.pb"
],
"min_total_size_bytes": 1000000,
"funasr_alias": "fsmn-vad"
}, },
"iic/speech_campplus_speaker-diarization_common": { "iic/speech_campplus_speaker-diarization_common": {
"alias": "diarization", "alias": "diarization",
@ -43,7 +66,11 @@
"kind": "speaker_verification", "kind": "speaker_verification",
"description": "Configured CAM++ speaker verification", "description": "Configured CAM++ speaker verification",
"revision": "v2.0.2", "revision": "v2.0.2",
"required_files": ["configuration.json", "config.yaml", "campplus_cn_common.bin"], "required_files": [
"configuration.json",
"config.yaml",
"campplus_cn_common.bin"
],
"min_total_size_bytes": 10000000 "min_total_size_bytes": 10000000
}, },
"iic/speech_eres2netv2_sv_zh-cn_16k-common": { "iic/speech_eres2netv2_sv_zh-cn_16k-common": {
@ -51,8 +78,12 @@
"directory": "iic/speech_eres2netv2_sv_zh-cn_16k-common", "directory": "iic/speech_eres2netv2_sv_zh-cn_16k-common",
"kind": "realtime_speaker_verification", "kind": "realtime_speaker_verification",
"description": "Realtime speaker verification", "description": "Realtime speaker verification",
"required_files": ["configuration.json"], "required_files": [
"any_files": ["*"], "configuration.json"
],
"any_files": [
"*"
],
"min_total_size_bytes": 10000000 "min_total_size_bytes": 10000000
}, },
"damo/speech_campplus_sv_zh-cn_16k-common": { "damo/speech_campplus_sv_zh-cn_16k-common": {
@ -60,7 +91,11 @@
"directory": "damo/speech_campplus_sv_zh-cn_16k-common", "directory": "damo/speech_campplus_sv_zh-cn_16k-common",
"kind": "speaker_verification", "kind": "speaker_verification",
"description": "CAM++ speaker verification dependency", "description": "CAM++ speaker verification dependency",
"required_files": ["configuration.json", "config.yaml", "campplus_cn_common.bin"], "required_files": [
"configuration.json",
"config.yaml",
"campplus_cn_common.bin"
],
"min_total_size_bytes": 10000000 "min_total_size_bytes": 10000000
}, },
"damo/speech_campplus-transformer_scl_zh-cn_16k-common": { "damo/speech_campplus-transformer_scl_zh-cn_16k-common": {
@ -68,7 +103,11 @@
"directory": "damo/speech_campplus-transformer_scl_zh-cn_16k-common", "directory": "damo/speech_campplus-transformer_scl_zh-cn_16k-common",
"kind": "speaker_transformer", "kind": "speaker_transformer",
"description": "CAM++ Transformer dependency", "description": "CAM++ Transformer dependency",
"required_files": ["configuration.json", "campplus_cn_encoder.pt", "transformer_backend.pt"], "required_files": [
"configuration.json",
"campplus_cn_encoder.pt",
"transformer_backend.pt"
],
"min_total_size_bytes": 10000000 "min_total_size_bytes": 10000000
}, },
"Qwen/Qwen3-ForcedAligner-0.6B": { "Qwen/Qwen3-ForcedAligner-0.6B": {
@ -76,8 +115,13 @@
"directory": "Qwen/Qwen3-ForcedAligner-0.6B", "directory": "Qwen/Qwen3-ForcedAligner-0.6B",
"kind": "forced_aligner", "kind": "forced_aligner",
"description": "Qwen3 word-level forced aligner", "description": "Qwen3 word-level forced aligner",
"required_files": ["config.json"], "required_files": [
"any_files": ["*.safetensors", "*.bin"], "config.json"
],
"any_files": [
"*.safetensors",
"*.bin"
],
"min_total_size_bytes": 500000000 "min_total_size_bytes": 500000000
} }
} }

View File

@ -19,6 +19,7 @@ dependencies = [
[project.scripts] [project.scripts]
funasr-realtime-demo = "scripts.run_funasr_demo:main" funasr-realtime-demo = "scripts.run_funasr_demo:main"
funasr-download-models = "scripts.download_models:main"
[tool.setuptools] [tool.setuptools]
packages = ["scripts", "realtime_websocket"] packages = ["scripts", "realtime_websocket"]

View File

@ -197,7 +197,8 @@ class AuxiliaryRuntime:
) )
raise RuntimeError( raise RuntimeError(
"Auxiliary core model is missing or failed to load: " + details "Auxiliary core model is missing or failed to load: " + details
+ ". Set MODEL_DIR or CAM_MODEL_PATH to the local CAM++ asset." + ". Run python scripts/download_models.py --funasr-runtime, "
"or set MODEL_DIR/CAM_MODEL_PATH to the local CAM++ asset."
) )
def _find_model(self, kind: str) -> Any: def _find_model(self, kind: str) -> Any:

View File

@ -0,0 +1,13 @@
#!/usr/bin/env python3
"""Download models using the project's shared model manifest."""
from __future__ import annotations
try:
from .download_models_qwen_legacy import main
except ImportError:
from download_models_qwen_legacy import main
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -73,7 +73,7 @@ def download_model(
from modelscope.hub.snapshot_download import snapshot_download from modelscope.hub.snapshot_download import snapshot_download
except ImportError as exc: except ImportError as exc:
raise RuntimeError( raise RuntimeError(
"ModelScope is required for downloading; install requirements-download.txt first" "ModelScope is required for downloading; install requirements-funasr.txt first"
) from exc ) from exc
model_path.parent.mkdir(parents=True, exist_ok=True) model_path.parent.mkdir(parents=True, exist_ok=True)
@ -176,17 +176,41 @@ def main() -> int:
action="store_true", action="store_true",
help="Only download/check VAD, speaker, diarization, and aligner assets", help="Only download/check VAD, speaker, diarization, and aligner assets",
) )
auxiliary_group.add_argument(
"--funasr-runtime",
action="store_true",
help="Download/check only streaming Paraformer, FSMN-VAD, and CAM++",
)
args = parser.parse_args() args = parser.parse_args()
manifest = load_manifest() manifest = load_manifest()
models_dir = args.models_dir.resolve() models_dir = args.models_dir
if not models_dir.is_absolute():
models_dir = Path(__file__).resolve().parents[1] / models_dir
models_dir = models_dir.resolve()
cache_dir = args.cache_dir.resolve() if args.cache_dir else None cache_dir = args.cache_dir.resolve() if args.cache_dir else None
selected_assets: list[tuple[str, dict[str, object]]] = [] selected_assets: list[tuple[str, dict[str, object]]] = []
if not args.auxiliary_only: if args.funasr_runtime:
model_id = resolve_model_id(args.model, manifest) # The realtime branch needs the streaming ASR plus its VAD and CAM++ only.
selected_assets.append((model_id, manifest["models"][model_id])) asr_id = resolve_model_id("paraformer-zh-streaming", manifest)
if not args.skip_auxiliary: selected_assets.append((asr_id, manifest["models"][asr_id]))
selected_assets.extend(auxiliary_models(manifest).items()) assets = auxiliary_models(manifest)
vad_id = next(
model_id for model_id, config in assets.items()
if config.get("kind") == "vad"
)
cam_id = next(
model_id for model_id, config in assets.items()
if config.get("kind") == "speaker_verification"
and model_id.startswith("iic/")
)
selected_assets.extend((model_id, assets[model_id]) for model_id in (vad_id, cam_id))
else:
if not args.auxiliary_only:
model_id = resolve_model_id(args.model, manifest)
selected_assets.append((model_id, manifest["models"][model_id]))
if not args.skip_auxiliary:
selected_assets.extend(auxiliary_models(manifest).items())
missing: list[tuple[str, Path, dict[str, object]]] = [] missing: list[tuple[str, Path, dict[str, object]]] = []
for model_id, config in selected_assets: for model_id, config in selected_assets:

View File

@ -52,3 +52,28 @@ def auxiliary_models(manifest: dict[str, Any]) -> dict[str, dict[str, Any]]:
if not isinstance(models, dict): if not isinstance(models, dict):
raise ValueError("model_manifest.json 的 auxiliary_models 必须是对象") raise ValueError("model_manifest.json 的 auxiliary_models 必须是对象")
return {str(model_id): config for model_id, config in models.items() if isinstance(config, dict)} return {str(model_id): config for model_id, config in models.items() if isinstance(config, dict)}
def resolve_auxiliary_model_id(
model: str | None,
manifest: dict[str, Any],
kind: str | None = None,
) -> str:
"""Resolve a configured auxiliary asset by ID, alias, or FunASR alias."""
assets = auxiliary_models(manifest)
requested = (model or "").strip()
if requested in assets and (kind is None or assets[requested].get("kind") == kind):
return requested
for model_id, config in assets.items():
if kind is not None and config.get("kind") != kind:
continue
aliases = [config.get("alias"), config.get("funasr_alias")]
if requested.lower() in {
str(alias).lower() for alias in aliases if isinstance(alias, str)
}:
return model_id
expected = ", ".join(
model_id for model_id, config in assets.items()
if kind is None or config.get("kind") == kind
)
raise ValueError(f"Unsupported auxiliary model '{model}'; configured assets: {expected}")

View File

@ -18,35 +18,41 @@ PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path: if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT)) sys.path.insert(0, str(PROJECT_ROOT))
from scripts.model_manifest import auxiliary_models, load_manifest, model_directory from scripts.model_manifest import (
auxiliary_models,
load_manifest,
model_directory,
resolve_auxiliary_model_id,
resolve_model_id,
)
load_dotenv(PROJECT_ROOT / ".env") load_dotenv(PROJECT_ROOT / ".env")
# FunASR's published short names resolve to these ModelScope local directories.
LOCAL_MODEL_NAMES = {
"paraformer-zh-streaming": (
"iic/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online",
),
"fsmn-vad": (
"iic/speech_fsmn_vad_zh-cn-16k-common-pytorch",
"damo/speech_fsmn_vad_zh-cn-16k-common-pytorch",
),
}
def local_model(requested: str, models_dir: Path, kind: str) -> Path:
def local_model(requested: str, models_dir: Path) -> Path: """Resolve ASR/VAD IDs through the shared model manifest."""
"""Resolve a model ID or path beneath MODEL_DIR without a network fallback."""
name = requested.strip() name = requested.strip()
direct = Path(name) configured_path = Path(name)
candidates = [direct] if direct.is_absolute() else [ direct_candidates = (
PROJECT_ROOT / direct, models_dir / direct, [configured_path]
*(models_dir / alias for alias in LOCAL_MODEL_NAMES.get(name, ())), if configured_path.is_absolute()
] else [PROJECT_ROOT / configured_path, models_dir / configured_path]
for candidate in candidates: )
for candidate in direct_candidates:
if candidate.is_dir() and (candidate / "configuration.json").is_file(): if candidate.is_dir() and (candidate / "configuration.json").is_file():
return candidate.resolve() return candidate.resolve()
checked = ", ".join(str(path) for path in candidates)
raise FileNotFoundError(f"Local model '{name}' was not found; checked: {checked}") manifest = load_manifest()
if kind == "asr":
model_id = resolve_model_id(name, manifest)
else:
model_id = resolve_auxiliary_model_id(name, manifest, kind=kind)
candidate = model_directory(model_id, manifest, models_dir)
if candidate.is_dir() and (candidate / "configuration.json").is_file():
return candidate.resolve()
raise FileNotFoundError(
f"Local {kind.upper()} model '{name}' is missing; expected: {candidate}"
)
def local_cam_model(models_dir: Path) -> Path: def local_cam_model(models_dir: Path) -> Path:
@ -115,8 +121,10 @@ def main() -> None:
if not models_dir.is_absolute(): if not models_dir.is_absolute():
models_dir = PROJECT_ROOT / models_dir models_dir = PROJECT_ROOT / models_dir
models_dir = models_dir.resolve() models_dir = models_dir.resolve()
asr = local_model(os.getenv("FUNASR_ASR_MODEL", "paraformer-zh-streaming"), models_dir) asr = local_model(
vad = local_model(os.getenv("FUNASR_VAD_MODEL", "fsmn-vad"), models_dir) os.getenv("FUNASR_ASR_MODEL", "paraformer-zh-streaming"), models_dir, "asr"
)
vad = local_model(os.getenv("FUNASR_VAD_MODEL", "fsmn-vad"), models_dir, "vad")
cam = local_cam_model(models_dir) cam = local_cam_model(models_dir)
print(f"Local models: ASR={asr}; VAD={vad}; CAM++={cam}", flush=True) print(f"Local models: ASR={asr}; VAD={vad}; CAM++={cam}", flush=True)

View File

@ -22,10 +22,13 @@ class ModelManifestTests(unittest.TestCase):
self.assertEqual(resolve_model_id("1.7b", self.manifest), "Qwen/Qwen3-ASR-1.7B") self.assertEqual(resolve_model_id("1.7b", self.manifest), "Qwen/Qwen3-ASR-1.7B")
self.assertEqual(resolve_model_id("0.6b", self.manifest), "Qwen/Qwen3-ASR-0.6B") self.assertEqual(resolve_model_id("0.6b", self.manifest), "Qwen/Qwen3-ASR-0.6B")
def test_manifest_has_only_asr_models(self) -> None: def test_manifest_contains_legacy_and_funasr_asr_models(self) -> None:
self.assertTrue({"Qwen/Qwen3-ASR-0.6B", "Qwen/Qwen3-ASR-1.7B"}.issubset(
set(self.manifest["models"])
))
self.assertEqual( self.assertEqual(
set(self.manifest["models"]), resolve_model_id("paraformer-zh-streaming", self.manifest),
{"Qwen/Qwen3-ASR-0.6B", "Qwen/Qwen3-ASR-1.7B"}, "iic/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online",
) )
def test_manifest_has_auxiliary_runtime_assets(self) -> None: def test_manifest_has_auxiliary_runtime_assets(self) -> None: