refactor: organize backend and frontend directories

main
Bifang 2026-09-23 15:05:10 +08:00
parent 13278d3aea
commit 24b4b62fa9
40 changed files with 83 additions and 70 deletions

View File

@ -18,7 +18,7 @@ FUNASR_FINALIZE_TIMEOUT_SECONDS=300
FUNASR_NATIVE_WS_HOST=127.0.0.1 FUNASR_NATIVE_WS_HOST=127.0.0.1
FUNASR_NATIVE_WS_PORT=10095 FUNASR_NATIVE_WS_PORT=10095
# CAM++ is required and started by scripts/run_backend.py. # CAM++ is required and started by backend/run_backend.py.
AUXILIARY_SERVICE_URL=http://127.0.0.1:8010 AUXILIARY_SERVICE_URL=http://127.0.0.1:8010
AUXILIARY_DEVICE=cpu AUXILIARY_DEVICE=cpu
AUXILIARY_PRELOAD_KINDS=speaker_verification AUXILIARY_PRELOAD_KINDS=speaker_verification

View File

@ -23,8 +23,8 @@ If directories have different names, set `FUNASR_ASR_MODEL` and `FUNASR_VAD_MODE
Install a torch/torchaudio build suitable for the host, then install the project dependencies and models: Install a torch/torchaudio build suitable for the host, then install the project dependencies and models:
~~~powershell ~~~powershell
python -m pip install -r requirements-funasr.txt python -m pip install -r backend/requirements-funasr.txt
python -m pip install -r requirements-auxiliary.txt python -m pip install -r backend/requirements-auxiliary.txt
python scripts/download_models.py --funasr-runtime 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 }
~~~ ~~~
@ -32,8 +32,8 @@ if (-not (Test-Path .env)) { Copy-Item .env.funasr.example .env }
Start the backend and frontend in separate terminals: Start the backend and frontend in separate terminals:
~~~powershell ~~~powershell
python scripts\run_backend.py python backend\run_backend.py
python scripts\run_frontend.py python frontend\run_frontend.py
~~~ ~~~
Defaults are frontend port 8080, browser backend port 8082, CAM++ HTTP port 8010, and native FunASR WS port 10095 bound to loopback. Change `FRONTEND_PORT`, `WEB_PORT`, `AUXILIARY_SERVICE_URL`, `FUNASR_NATIVE_WS_HOST`, and `FUNASR_NATIVE_WS_PORT` together when needed. `BACKEND_INTERNAL_URL` is the backend origin reachable from the frontend process; it defaults to `http://127.0.0.1:${WEB_PORT}`. Defaults are frontend port 8080, browser backend port 8082, CAM++ HTTP port 8010, and native FunASR WS port 10095 bound to loopback. Change `FRONTEND_PORT`, `WEB_PORT`, `AUXILIARY_SERVICE_URL`, `FUNASR_NATIVE_WS_HOST`, and `FUNASR_NATIVE_WS_PORT` together when needed. `BACKEND_INTERNAL_URL` is the backend origin reachable from the frontend process; it defaults to `http://127.0.0.1:${WEB_PORT}`.

View File

@ -1,25 +1,32 @@
# FunASR realtime ASR demo # FunASR realtime ASR demo
This branch runs FunASR's native online WebSocket flow with local streaming ASR and FSMN VAD models. CAM++ speaker embeddings are handled by the separate auxiliary service. The browser page, JavaScript, and CSS are copied byte-for-byte from the local `tencent-demo/static` directory. This project keeps the frontend and backend in separate directories. The frontend serves the original Tencent demo assets from frontend/static/ unchanged.
## Start ## Project layout
Install a torch/torchaudio build for the host, then install `requirements-funasr.txt` and `requirements-auxiliary.txt`. Copy `.env.funasr.example` to `.env` if no `.env` exists, then download the three required FunASR assets: - backend/: FunASR WebSocket adapter, native engine launcher, CAM++ service, and backend dependencies.
- frontend/: Tencent demo files, static HTTP/WebSocket proxy, and frontend dependencies.
- scripts/: shared model download tools and the model manifest.
## Install and download models
Install a host-compatible PyTorch/torchaudio build first. Then install backend dependencies, copy the example environment file, and download the FunASR runtime models:
~~~powershell ~~~powershell
python -m pip install -r backend/requirements-funasr.txt
python -m pip install -r backend/requirements-auxiliary.txt
python -m pip install -r frontend/requirements.txt
if (-not (Test-Path .env)) { Copy-Item .env.funasr.example .env }
python scripts/download_models.py --funasr-runtime python scripts/download_models.py --funasr-runtime
~~~ ~~~
Start the backend (CAM++, native FunASR WSS, and the browser protocol bridge) in one terminal: ## Start
Run the backend and frontend in separate terminals from the project root:
~~~powershell ~~~powershell
python scripts\run_backend.py python backend/run_backend.py
python frontend/run_frontend.py
~~~ ~~~
Start the frontend in another terminal: The backend supervises CAM++, FunASR native WSS, and the browser protocol adapter. The frontend serves the unchanged Tencent page and proxies its same-origin /ws and /api/stop requests to the backend. Configure ports and model locations in .env.
~~~powershell
python scripts\run_frontend.py
~~~
Open the URL configured by `FRONTEND_HOST` and `FRONTEND_PORT`. The frontend keeps the Tencent demo's same-origin `/ws` and `/api/stop` calls and proxies them to `BACKEND_INTERNAL_URL`. The public backend bridge defaults to port 8082; the native FunASR WS socket defaults to loopback port 10095. Model paths and device settings are described in [FUNASR_README.md](FUNASR_README.md).

View File

@ -30,7 +30,7 @@ Qwen/Qwen3-ASR-1.7B
cd D:\github-project\ASR\Qwen-Asr\demo cd D:\github-project\ASR\Qwen-Asr\demo
python -m venv .venv python -m venv .venv
.\.venv\Scripts\Activate.ps1 .\.venv\Scripts\Activate.ps1
pip install -r requirements-download.txt pip install modelscope==1.34.0
python scripts\download_models.py python scripts\download_models.py
``` ```
@ -70,7 +70,7 @@ python scripts\download_models.py
需要宿主机具备与 VLLM 兼容的 Python、CUDA 和 NVIDIA 驱动环境。安装部署依赖: 需要宿主机具备与 VLLM 兼容的 Python、CUDA 和 NVIDIA 驱动环境。安装部署依赖:
```bash ```bash
python -m pip install -r requirements-deploy.txt python -m pip install -r backend/requirements-qwen-legacy.txt
``` ```
先复制并按服务器实际路径修改 `.env`,启动器会自动读取该文件: 先复制并按服务器实际路径修改 `.env`,启动器会自动读取该文件:
@ -82,24 +82,24 @@ cp .env.example .env
默认启动 `Qwen/Qwen3-ASR-0.6B`,监听地址为 `0.0.0.0:9950`: 默认启动 `Qwen/Qwen3-ASR-0.6B`,监听地址为 `0.0.0.0:9950`:
```bash ```bash
python scripts/serve.py python backend/serve_qwen_legacy.py
``` ```
模型和启动检查循环可以通过命令行或环境变量传入;端口统一在 `scripts/serve.py` 的 `SERVER_PORT` 变量中维护: 模型和启动检查循环可以通过命令行或环境变量传入;端口统一在 `backend/serve_qwen_legacy.py` 的 `SERVER_PORT` 变量中维护:
```bash ```bash
QWEN3_ASR_MODEL=0.6b VLLM_STARTUP_CHECK_LOOPS=120 \ QWEN3_ASR_MODEL=0.6b VLLM_STARTUP_CHECK_LOOPS=120 \
VLLM_STARTUP_CHECK_INTERVAL_SECONDS=2 python scripts/serve.py VLLM_STARTUP_CHECK_INTERVAL_SECONDS=2 python backend/serve_qwen_legacy.py
``` ```
如果使用 `1.7B`,下载和启动必须指定同一个模型: 如果使用 `1.7B`,下载和启动必须指定同一个模型:
```powershell ```powershell
python scripts\download_models.py --model 1.7b python scripts\download_models.py --model 1.7b
python -m scripts.serve --model 1.7b python -m backend.serve_qwen_legacy --model 1.7b
``` ```
启动器会按 `VLLM_STARTUP_CHECK_LOOPS` 次数轮询 `/health`,每次间隔由 `VLLM_STARTUP_CHECK_INTERVAL_SECONDS` 指定。服务端口、健康检查端口和就绪提示统一使用 `scripts/serve.py` 中的 `SERVER_PORT`。 启动器会按 `VLLM_STARTUP_CHECK_LOOPS` 次数轮询 `/health`,每次间隔由 `VLLM_STARTUP_CHECK_INTERVAL_SECONDS` 指定。服务端口、健康检查端口和就绪提示统一使用 `backend/serve_qwen_legacy.py` 中的 `SERVER_PORT`。
服务启动后可检查: 服务启动后可检查:
@ -125,8 +125,8 @@ curl "http://${VLLM_DISPLAY_HOST:-127.0.0.1}:9950/v1/audio/transcriptions" \
```powershell ```powershell
cd D:\github-project\ASR\Qwen-Asr\demo cd D:\github-project\ASR\Qwen-Asr\demo
pip install -r requirements-auxiliary.txt pip install -r backend/requirements-auxiliary.txt
python scripts\auxiliary_server.py python -m backend.auxiliary_server
``` ```
辅助服务默认监听 `0.0.0.0:8010`。实时链路启动时严格加载 VAD 和 CAM++ `speaker_verification` 声纹模型,用于每个 turn 的特征提取与在线聚类;完整 CAM++ 分离、Transformer 和 ForcedAligner 不阻断核心服务,完整分离模型会在调用 `/v1/diarization` 时按需加载。检查状态: 辅助服务默认监听 `0.0.0.0:8010`。实时链路启动时严格加载 VAD 和 CAM++ `speaker_verification` 声纹模型,用于每个 turn 的特征提取与在线聚类;完整 CAM++ 分离、Transformer 和 ForcedAligner 不阻断核心服务,完整分离模型会在调用 `/v1/diarization` 时按需加载。检查状态:
@ -144,8 +144,8 @@ WebSocket demo 默认连接 `9950` 的 ASR VLLM,辅助服务使用 `8010`。`/
## 项目边界 ## 项目边界
- `scripts/download_models.py`:下载选定 ASR 和全部辅助模型资产。 - `scripts/download_models.py`:下载选定 ASR 和全部辅助模型资产。
- `scripts/serve.py`:读取 `.env`,解析模型选择、宿主机参数并启动新版 `vllm serve`。 - `backend/serve_qwen_legacy.py`:读取 `.env`,解析模型选择、宿主机参数并启动新版 `vllm serve`。
- `requirements-deploy.txt`:安装宿主机部署所需的官方 Qwen3-ASR VLLM 依赖。 - `backend/requirements-qwen-legacy.txt`:安装宿主机部署所需的官方 Qwen3-ASR VLLM 依赖。
- `tests/`:只验证本项目自己的模型清单和选择逻辑,不依赖原项目。 - `tests/`:只验证本项目自己的模型清单和选择逻辑,不依赖原项目。
模型服务就绪后,新的实时 ASR demo 放在同级 `demo` 项目中继续开发,但不得通过 Python import 或 HTTP/WebSocket 调用原项目服务。 模型服务就绪后,新的实时 ASR demo 放在同级 `demo` 项目中继续开发,但不得通过 Python import 或 HTTP/WebSocket 调用原项目服务。

View File

@ -0,0 +1 @@
"""Backend services and the realtime ASR protocol adapter."""

View File

@ -6,6 +6,7 @@ from __future__ import annotations
import asyncio import asyncio
import math import math
import os import os
import sys
import tempfile import tempfile
import time import time
from collections.abc import Mapping from collections.abc import Mapping
@ -16,16 +17,17 @@ from aiohttp import web
from aiohttp.web_request import FileField from aiohttp.web_request import FileField
from dotenv import load_dotenv from dotenv import load_dotenv
try: PROJECT_ROOT = Path(__file__).resolve().parents[1]
from .model_manifest import auxiliary_models, load_manifest, model_directory if str(PROJECT_ROOT) not in sys.path:
except ImportError: # Support direct script startup while sharing the root model manifest.
from model_manifest import auxiliary_models, load_manifest, model_directory sys.path.insert(0, str(PROJECT_ROOT))
from scripts.model_manifest import auxiliary_models, load_manifest, model_directory
# 将辅助服务端口固定在代码变量中,服务器启动时只需执行脚本,便于部署和排查。 # 将辅助服务端口固定在代码变量中,服务器启动时只需执行脚本,便于部署和排查。
AUXILIARY_HOST = "0.0.0.0" AUXILIARY_HOST = "0.0.0.0"
AUXILIARY_PORT = 8010 AUXILIARY_PORT = 8010
PROJECT_ROOT = Path(__file__).resolve().parents[1]
# 独立启动辅助服务也必须读取部署配置,不能只在启动 vLLM 时才加载 .env。 # 独立启动辅助服务也必须读取部署配置,不能只在启动 vLLM 时才加载 .env。
load_dotenv(PROJECT_ROOT / ".env") load_dotenv(PROJECT_ROOT / ".env")
MODELS_DIR = Path(os.getenv("MODEL_DIR", str(PROJECT_ROOT / "models"))).resolve() MODELS_DIR = Path(os.getenv("MODEL_DIR", str(PROJECT_ROOT / "models"))).resolve()

View File

@ -31,15 +31,15 @@ VAD、CAM++ 声纹模型及在线聚类,WebSocket 只做音频流编排,不
```powershell ```powershell
cd D:\github-project\ASR\Qwen-Asr\demo cd D:\github-project\ASR\Qwen-Asr\demo
python scripts\download_models.py python scripts\download_models.py
python scripts\serve.py python backend/serve_qwen_legacy.py
``` ```
另一个终端启动辅助模型服务: 另一个终端启动辅助模型服务:
```powershell ```powershell
cd D:\github-project\ASR\Qwen-Asr\demo cd D:\github-project\ASR\Qwen-Asr\demo
pip install -r requirements-auxiliary.txt pip install -r backend/requirements-auxiliary.txt
python scripts\auxiliary_server.py python -m backend.auxiliary_server
``` ```
另开一个终端启动 WebSocket 页面: 另开一个终端启动 WebSocket 页面:
@ -78,7 +78,7 @@ python server.py --no-browser
`pipeline([wav_path], output_emb=True)`。不能绕过 pipeline 预处理后直接调用 `pipeline([wav_path], output_emb=True)`。不能绕过 pipeline 预处理后直接调用
`pipeline.model`,否则采样率、声道和 waveform 预处理不会执行,部分 ModelScope `pipeline.model`,否则采样率、声道和 waveform 预处理不会执行,部分 ModelScope
版本会直接抛异常,WebSocket 仍会继续输出 ASR 并把说话人保留为 pending。 版本会直接抛异常,WebSocket 仍会继续输出 ASR 并把说话人保留为 pending。
更新辅助服务代码后需要重启 `python scripts/auxiliary_server.py`,仅重启页面 更新辅助服务代码后需要重启 `python -m backend.auxiliary_server`,仅重启页面
服务不会替换已经驻留在 GPU 中的旧辅助服务进程。 服务不会替换已经驻留在 GPU 中的旧辅助服务进程。
## 页面操作 ## 页面操作

View File

@ -123,7 +123,7 @@ class FunASRModelService:
from funasr import AutoModel from funasr import AutoModel
except ImportError as exc: # pragma: no cover - deployment-only branch except ImportError as exc: # pragma: no cover - deployment-only branch
raise RuntimeError( raise RuntimeError(
"FunASR is not installed; run pip install -r requirements-deploy.txt" "FunASR is not installed; run pip install -r backend/requirements-funasr.txt"
) from exc ) from exc
def load_models() -> tuple[Any, Any]: def load_models() -> tuple[Any, Any]:

View File

@ -26,7 +26,7 @@ except ImportError:
from auxiliary_service import AuxiliaryModelService, AuxiliaryServiceConfig from auxiliary_service import AuxiliaryModelService, AuxiliaryServiceConfig
PROJECT_ROOT = Path(__file__).resolve().parents[1] PROJECT_ROOT = Path(__file__).resolve().parents[2]
load_dotenv(PROJECT_ROOT / ".env") load_dotenv(PROJECT_ROOT / ".env")
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
WEB_HOST = os.getenv("WEB_HOST", "0.0.0.0") WEB_HOST = os.getenv("WEB_HOST", "0.0.0.0")

View File

@ -25,7 +25,7 @@ from speaker_assembler import SegmentAssembler
# 与部署启动器读取同一配置;外部环境变量优先于 demo/.env。 # 与部署启动器读取同一配置;外部环境变量优先于 demo/.env。
DEPLOY_ROOT = Path(__file__).resolve().parents[1] DEPLOY_ROOT = Path(__file__).resolve().parents[2]
load_dotenv(DEPLOY_ROOT / ".env") load_dotenv(DEPLOY_ROOT / ".env")
# 监听所有网卡,允许同一局域网内的浏览器访问服务器上的 Demo;端口集中在代码 # 监听所有网卡,允许同一局域网内的浏览器访问服务器上的 Demo;端口集中在代码

View File

@ -7,7 +7,7 @@ import unittest
from unittest.mock import patch from unittest.mock import patch
try: try:
from realtime_websocket.funasr_engine import ( from backend.realtime_websocket.funasr_engine import (
FunASRRealtimeSession, FunASRRealtimeSession,
FunASRServiceConfig, FunASRServiceConfig,
_result_text, _result_text,

View File

@ -1,4 +1,4 @@
"""Contract test for the unchanged Tencent UI to FunASR native WS bridge.""" """Contract test for the unchanged Tencent UI to FunASR native WS bridge."""
from __future__ import annotations from __future__ import annotations
@ -10,7 +10,7 @@ from unittest.mock import patch
from aiohttp import web from aiohttp import web
from aiohttp.test_utils import AioHTTPTestCase from aiohttp.test_utils import AioHTTPTestCase
from realtime_websocket.funasr_server import ( from backend.realtime_websocket.funasr_server import (
AUXILIARY_KEY, AUXILIARY_KEY,
SESSION_REGISTRY_KEY, SESSION_REGISTRY_KEY,
websocket_handler, websocket_handler,
@ -78,7 +78,7 @@ class FunASRBridgeTests(AioHTTPTestCase):
async def test_tencent_ui_messages_use_native_funasr_and_keep_speaker_label(self): async def test_tencent_ui_messages_use_native_funasr_and_keep_speaker_label(self):
native = FakeNativeWebSocket() native = FakeNativeWebSocket()
with patch( with patch(
"realtime_websocket.funasr_server.websocket_connect", "backend.realtime_websocket.funasr_server.websocket_connect",
return_value=native, return_value=native,
): ):
ws = await self.client.ws_connect("/ws") ws = await self.client.ws_connect("/ws")

View File

@ -5,7 +5,7 @@ from __future__ import annotations
import unittest import unittest
try: try:
from realtime_websocket.speaker_assembler import SegmentAssembler from backend.realtime_websocket.speaker_assembler import SegmentAssembler
except ModuleNotFoundError: except ModuleNotFoundError:
from speaker_assembler import SegmentAssembler from speaker_assembler import SegmentAssembler

View File

@ -191,7 +191,7 @@ def main() -> None:
websocket = None websocket = None
try: try:
auxiliary = subprocess.Popen( auxiliary = subprocess.Popen(
[sys.executable, "-m", "scripts.auxiliary_server"], [sys.executable, "-m", "backend.auxiliary_server"],
cwd=PROJECT_ROOT, cwd=PROJECT_ROOT,
env=env, env=env,
) )
@ -199,7 +199,7 @@ def main() -> None:
native_args = [ native_args = [
sys.executable, sys.executable,
str(PROJECT_ROOT / "realtime_websocket" / "funasr_native_wss.py"), str(PROJECT_ROOT / "backend" / "realtime_websocket" / "funasr_native_wss.py"),
"--host", "--host",
native_host, native_host,
"--port", "--port",
@ -230,7 +230,7 @@ def main() -> None:
wait_for_tcp(probe_host, native_port, native) wait_for_tcp(probe_host, native_port, native)
websocket = subprocess.Popen( websocket = subprocess.Popen(
[sys.executable, "-m", "scripts.run_funasr_demo", "--no-browser"], [sys.executable, "-m", "backend.run_funasr_demo", "--no-browser"],
cwd=PROJECT_ROOT, cwd=PROJECT_ROOT,
env=env, env=env,
) )

View File

@ -10,7 +10,7 @@ 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 realtime_websocket.funasr_server import main from backend.realtime_websocket.funasr_server import main
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -19,16 +19,16 @@ from dotenv import load_dotenv
# 启动器自动读取 demo/.env;系统环境变量仍然优先,便于部署平台临时覆盖配置。 # 启动器自动读取 demo/.env;系统环境变量仍然优先,便于部署平台临时覆盖配置。
PROJECT_ROOT = Path(__file__).resolve().parents[1] PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
# Resolve the shared model manifest for direct script startup.
sys.path.insert(0, str(PROJECT_ROOT))
load_dotenv(PROJECT_ROOT / ".env") load_dotenv(PROJECT_ROOT / ".env")
# 将服务端口集中在代码变量中维护,启动时不需要额外传入端口参数;健康检查、 # 将服务端口集中在代码变量中维护,启动时不需要额外传入端口参数;健康检查、
# VLLM 子进程命令和就绪提示都使用同一个端口,避免配置不一致导致误判。 # VLLM 子进程命令和就绪提示都使用同一个端口,避免配置不一致导致误判。
SERVER_PORT = int(os.getenv("VLLM_PORT", "9950")) SERVER_PORT = int(os.getenv("VLLM_PORT", "9950"))
try: from scripts.model_manifest import load_manifest, model_directory, resolve_model_id
from .model_manifest import load_manifest, model_directory, resolve_model_id
except ImportError:
from model_manifest import load_manifest, model_directory, resolve_model_id
def has_model_weights(model_path: Path) -> bool: def has_model_weights(model_path: Path) -> bool:
@ -125,7 +125,7 @@ def build_server_command(args: argparse.Namespace, model_id: str, model_path: Pa
executable_name = os.getenv("VLLM_EXECUTABLE", "vllm") executable_name = os.getenv("VLLM_EXECUTABLE", "vllm")
executable = shutil.which(executable_name) executable = shutil.which(executable_name)
if executable is None: if executable is None:
raise RuntimeError(f"{executable_name} was not found; install requirements-deploy.txt first") raise RuntimeError(f"{executable_name} was not found; install backend/requirements-qwen-legacy.txt first")
served_model_name = args.served_model_name or model_id served_model_name = args.served_model_name or model_id
command = [ command = [

View File

@ -39,7 +39,7 @@ class ModelManifestTests(unittest.TestCase):
self.assertIn("Qwen/Qwen3-ForcedAligner-0.6B", assets) self.assertIn("Qwen/Qwen3-ForcedAligner-0.6B", assets)
def test_model_directory_is_under_demo_models(self) -> None: def test_model_directory_is_under_demo_models(self) -> None:
models_dir = Path(__file__).resolve().parents[1] / "models" models_dir = Path(__file__).resolve().parents[2] / "models"
for model_id in [*self.manifest["models"], *auxiliary_models(self.manifest)]: for model_id in [*self.manifest["models"], *auxiliary_models(self.manifest)]:
self.assertTrue(model_directory(model_id, self.manifest, models_dir).is_relative_to(models_dir)) self.assertTrue(model_directory(model_id, self.manifest, models_dir).is_relative_to(models_dir))

View File

@ -7,7 +7,7 @@ import unittest
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
from scripts.serve import SERVER_PORT, build_parser, build_server_command from backend.serve_qwen_legacy import SERVER_PORT, build_parser, build_server_command
class ServeConfigTests(unittest.TestCase): class ServeConfigTests(unittest.TestCase):
@ -29,7 +29,7 @@ class ServeConfigTests(unittest.TestCase):
self.assertEqual(args.startup_check_loops, 12) self.assertEqual(args.startup_check_loops, 12)
self.assertEqual(args.startup_check_interval, 0.5) self.assertEqual(args.startup_check_interval, 0.5)
@patch("scripts.serve.shutil.which", return_value="/opt/asr-gb10/bin/vllm") @patch("backend.serve_qwen_legacy.shutil.which", return_value="/opt/asr-gb10/bin/vllm")
def test_builds_native_vllm_serve_command(self, _which: object) -> None: def test_builds_native_vllm_serve_command(self, _which: object) -> None:
"""启动器应生成已验证的新版 vllm serve 命令。""" """启动器应生成已验证的新版 vllm serve 命令。"""
args = build_parser().parse_args([]) args = build_parser().parse_args([])

View File

@ -7,7 +7,7 @@ import numpy as np
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import patch from unittest.mock import patch
from scripts.auxiliary_server import ( from backend.auxiliary_server import (
AuxiliaryRuntime, AuxiliaryRuntime,
_coerce_finite_float, _coerce_finite_float,
_normalize_diarization_segments, _normalize_diarization_segments,
@ -42,8 +42,8 @@ class AuxiliaryServerTests(unittest.TestCase):
"aligner": {"kind": "forced_aligner"}, "aligner": {"kind": "forced_aligner"},
} }
loaded_kinds = [] loaded_kinds = []
with patch("scripts.auxiliary_server._asset_ready", return_value=True), \ with patch("backend.auxiliary_server._asset_ready", return_value=True), \
patch("scripts.auxiliary_server.model_directory", return_value=runtime.manifest and SimpleNamespace()), \ patch("backend.auxiliary_server.model_directory", return_value=runtime.manifest and SimpleNamespace()), \
patch.object(runtime, "_load_asset", side_effect=lambda model_id, config, path: loaded_kinds.append(config["kind"]) or object()): patch.object(runtime, "_load_asset", side_effect=lambda model_id, config, path: loaded_kinds.append(config["kind"]) or object()):
runtime.preload() runtime.preload()
self.assertEqual(loaded_kinds, ["vad"]) self.assertEqual(loaded_kinds, ["vad"])
@ -57,8 +57,8 @@ class AuxiliaryServerTests(unittest.TestCase):
"vad": {"kind": "vad"}, "vad": {"kind": "vad"},
"campplus": {"kind": "speaker_verification"}, "campplus": {"kind": "speaker_verification"},
} }
with patch("scripts.auxiliary_server._asset_ready", side_effect=[True, False]), \ with patch("backend.auxiliary_server._asset_ready", side_effect=[True, False]), \
patch("scripts.auxiliary_server.model_directory", return_value=SimpleNamespace()): patch("backend.auxiliary_server.model_directory", return_value=SimpleNamespace()):
with self.assertRaisesRegex(RuntimeError, "campplus"): with self.assertRaisesRegex(RuntimeError, "campplus"):
runtime.preload() runtime.preload()
@ -76,8 +76,8 @@ class AuxiliaryServerTests(unittest.TestCase):
"""VAD 是核心依赖,缺失时错误必须给出可执行的修复方向。""" """VAD 是核心依赖,缺失时错误必须给出可执行的修复方向。"""
runtime = AuxiliaryRuntime() runtime = AuxiliaryRuntime()
runtime.assets = {"vad": {"kind": "vad"}} runtime.assets = {"vad": {"kind": "vad"}}
with patch("scripts.auxiliary_server._asset_ready", return_value=False), \ with patch("backend.auxiliary_server._asset_ready", return_value=False), \
patch("scripts.auxiliary_server.model_directory", return_value=SimpleNamespace(__str__=lambda self: "/models/vad")): patch("backend.auxiliary_server.model_directory", return_value=SimpleNamespace(__str__=lambda self: "/models/vad")):
with self.assertRaisesRegex(RuntimeError, "download_models.py --auxiliary-only"): with self.assertRaisesRegex(RuntimeError, "download_models.py --auxiliary-only"):
runtime.preload() runtime.preload()

View File

@ -0,0 +1,3 @@
# Dependencies for serving the unchanged Tencent demo UI.
aiohttp==3.11.11
python-dotenv>=1.0

View File

@ -13,7 +13,7 @@ from aiohttp import ClientSession, ClientTimeout, WSMsgType, web
from dotenv import load_dotenv from dotenv import load_dotenv
PROJECT_ROOT = Path(__file__).resolve().parents[1] PROJECT_ROOT = Path(__file__).resolve().parents[1]
STATIC_ROOT = PROJECT_ROOT / "realtime_websocket" / "static" STATIC_ROOT = Path(__file__).resolve().parent / "static"
load_dotenv(PROJECT_ROOT / ".env") load_dotenv(PROJECT_ROOT / ".env")
FRONTEND_HOST = os.getenv("FRONTEND_HOST", "127.0.0.1") FRONTEND_HOST = os.getenv("FRONTEND_HOST", "127.0.0.1")
FRONTEND_PORT = int(os.getenv("FRONTEND_PORT", "8080")) FRONTEND_PORT = int(os.getenv("FRONTEND_PORT", "8080"))

View File

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

View File

@ -14,7 +14,7 @@ dependencies = [
[project.scripts] [project.scripts]
qwen3-asr-download = "scripts.download_models:main" qwen3-asr-download = "scripts.download_models:main"
qwen3-asr-serve = "scripts.serve:main" qwen3-asr-serve = "backend.serve_qwen_legacy:main"
[tool.setuptools] [tool.setuptools]
packages = ["scripts"] packages = ["scripts", "backend", "backend.realtime_websocket"]

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-funasr.txt first" "ModelScope is required for downloading; install backend/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)