diff --git a/.env.funasr.example b/.env.funasr.example index 3102cc3..7d45898 100644 --- a/.env.funasr.example +++ b/.env.funasr.example @@ -18,7 +18,7 @@ FUNASR_FINALIZE_TIMEOUT_SECONDS=300 FUNASR_NATIVE_WS_HOST=127.0.0.1 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_DEVICE=cpu AUXILIARY_PRELOAD_KINDS=speaker_verification diff --git a/FUNASR_README.md b/FUNASR_README.md index abab0fc..ec62fee 100644 --- a/FUNASR_README.md +++ b/FUNASR_README.md @@ -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: ~~~powershell -python -m pip install -r requirements-funasr.txt -python -m pip install -r requirements-auxiliary.txt +python -m pip install -r backend/requirements-funasr.txt +python -m pip install -r backend/requirements-auxiliary.txt python scripts/download_models.py --funasr-runtime 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: ~~~powershell -python scripts\run_backend.py -python scripts\run_frontend.py +python backend\run_backend.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}`. diff --git a/README.md b/README.md index 8e9dd5e..4ddf12d 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,32 @@ # 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 +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 ~~~ -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 -python scripts\run_backend.py +python backend/run_backend.py +python frontend/run_frontend.py ~~~ -Start the frontend in another terminal: - -~~~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). +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. diff --git a/README_QWEN_LEGACY.md b/README_QWEN_LEGACY.md index 0140208..3a49185 100644 --- a/README_QWEN_LEGACY.md +++ b/README_QWEN_LEGACY.md @@ -30,7 +30,7 @@ Qwen/Qwen3-ASR-1.7B cd D:\github-project\ASR\Qwen-Asr\demo python -m venv .venv .\.venv\Scripts\Activate.ps1 -pip install -r requirements-download.txt +pip install modelscope==1.34.0 python scripts\download_models.py ``` @@ -70,7 +70,7 @@ python scripts\download_models.py 需要宿主机具备与 VLLM 兼容的 Python、CUDA 和 NVIDIA 驱动环境。安装部署依赖: ```bash -python -m pip install -r requirements-deploy.txt +python -m pip install -r backend/requirements-qwen-legacy.txt ``` 先复制并按服务器实际路径修改 `.env`,启动器会自动读取该文件: @@ -82,24 +82,24 @@ cp .env.example .env 默认启动 `Qwen/Qwen3-ASR-0.6B`,监听地址为 `0.0.0.0:9950`: ```bash -python scripts/serve.py +python backend/serve_qwen_legacy.py ``` -模型和启动检查循环可以通过命令行或环境变量传入;端口统一在 `scripts/serve.py` 的 `SERVER_PORT` 变量中维护: +模型和启动检查循环可以通过命令行或环境变量传入;端口统一在 `backend/serve_qwen_legacy.py` 的 `SERVER_PORT` 变量中维护: ```bash 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`,下载和启动必须指定同一个模型: ```powershell 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 cd D:\github-project\ASR\Qwen-Asr\demo -pip install -r requirements-auxiliary.txt -python scripts\auxiliary_server.py +pip install -r backend/requirements-auxiliary.txt +python -m backend.auxiliary_server ``` 辅助服务默认监听 `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/serve.py`:读取 `.env`,解析模型选择、宿主机参数并启动新版 `vllm serve`。 -- `requirements-deploy.txt`:安装宿主机部署所需的官方 Qwen3-ASR VLLM 依赖。 +- `backend/serve_qwen_legacy.py`:读取 `.env`,解析模型选择、宿主机参数并启动新版 `vllm serve`。 +- `backend/requirements-qwen-legacy.txt`:安装宿主机部署所需的官方 Qwen3-ASR VLLM 依赖。 - `tests/`:只验证本项目自己的模型清单和选择逻辑,不依赖原项目。 模型服务就绪后,新的实时 ASR demo 放在同级 `demo` 项目中继续开发,但不得通过 Python import 或 HTTP/WebSocket 调用原项目服务。 diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..4f65ab4 --- /dev/null +++ b/backend/__init__.py @@ -0,0 +1 @@ +"""Backend services and the realtime ASR protocol adapter.""" diff --git a/scripts/auxiliary_server.py b/backend/auxiliary_server.py similarity index 99% rename from scripts/auxiliary_server.py rename to backend/auxiliary_server.py index b6198c6..315e9b4 100644 --- a/scripts/auxiliary_server.py +++ b/backend/auxiliary_server.py @@ -6,6 +6,7 @@ from __future__ import annotations import asyncio import math import os +import sys import tempfile import time from collections.abc import Mapping @@ -16,16 +17,17 @@ from aiohttp import web from aiohttp.web_request import FileField from dotenv import load_dotenv -try: - from .model_manifest import auxiliary_models, load_manifest, model_directory -except ImportError: - from model_manifest import auxiliary_models, load_manifest, model_directory +PROJECT_ROOT = Path(__file__).resolve().parents[1] +if str(PROJECT_ROOT) not in sys.path: + # Support direct script startup while sharing the root model manifest. + 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_PORT = 8010 -PROJECT_ROOT = Path(__file__).resolve().parents[1] # 独立启动辅助服务也必须读取部署配置,不能只在启动 vLLM 时才加载 .env。 load_dotenv(PROJECT_ROOT / ".env") MODELS_DIR = Path(os.getenv("MODEL_DIR", str(PROJECT_ROOT / "models"))).resolve() diff --git a/realtime_websocket/FIXES_QWEN_LEGACY.md b/backend/realtime_websocket/FIXES_QWEN_LEGACY.md similarity index 100% rename from realtime_websocket/FIXES_QWEN_LEGACY.md rename to backend/realtime_websocket/FIXES_QWEN_LEGACY.md diff --git a/realtime_websocket/README_QWEN_LEGACY.md b/backend/realtime_websocket/README_QWEN_LEGACY.md similarity index 96% rename from realtime_websocket/README_QWEN_LEGACY.md rename to backend/realtime_websocket/README_QWEN_LEGACY.md index 7b30b98..a61f762 100644 --- a/realtime_websocket/README_QWEN_LEGACY.md +++ b/backend/realtime_websocket/README_QWEN_LEGACY.md @@ -31,15 +31,15 @@ VAD、CAM++ 声纹模型及在线聚类,WebSocket 只做音频流编排,不 ```powershell cd D:\github-project\ASR\Qwen-Asr\demo python scripts\download_models.py -python scripts\serve.py +python backend/serve_qwen_legacy.py ``` 另一个终端启动辅助模型服务: ```powershell cd D:\github-project\ASR\Qwen-Asr\demo -pip install -r requirements-auxiliary.txt -python scripts\auxiliary_server.py +pip install -r backend/requirements-auxiliary.txt +python -m backend.auxiliary_server ``` 另开一个终端启动 WebSocket 页面: @@ -78,7 +78,7 @@ python server.py --no-browser `pipeline([wav_path], output_emb=True)`。不能绕过 pipeline 预处理后直接调用 `pipeline.model`,否则采样率、声道和 waveform 预处理不会执行,部分 ModelScope 版本会直接抛异常,WebSocket 仍会继续输出 ASR 并把说话人保留为 pending。 -更新辅助服务代码后需要重启 `python scripts/auxiliary_server.py`,仅重启页面 +更新辅助服务代码后需要重启 `python -m backend.auxiliary_server`,仅重启页面 服务不会替换已经驻留在 GPU 中的旧辅助服务进程。 ## 页面操作 diff --git a/realtime_websocket/__init__.py b/backend/realtime_websocket/__init__.py similarity index 100% rename from realtime_websocket/__init__.py rename to backend/realtime_websocket/__init__.py diff --git a/realtime_websocket/auxiliary_service.py b/backend/realtime_websocket/auxiliary_service.py similarity index 100% rename from realtime_websocket/auxiliary_service.py rename to backend/realtime_websocket/auxiliary_service.py diff --git a/realtime_websocket/funasr_engine.py b/backend/realtime_websocket/funasr_engine.py similarity index 99% rename from realtime_websocket/funasr_engine.py rename to backend/realtime_websocket/funasr_engine.py index 77abbee..51cfd3a 100644 --- a/realtime_websocket/funasr_engine.py +++ b/backend/realtime_websocket/funasr_engine.py @@ -123,7 +123,7 @@ class FunASRModelService: from funasr import AutoModel except ImportError as exc: # pragma: no cover - deployment-only branch 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 def load_models() -> tuple[Any, Any]: diff --git a/realtime_websocket/funasr_native_wss.py b/backend/realtime_websocket/funasr_native_wss.py similarity index 100% rename from realtime_websocket/funasr_native_wss.py rename to backend/realtime_websocket/funasr_native_wss.py diff --git a/realtime_websocket/funasr_server.py b/backend/realtime_websocket/funasr_server.py similarity index 99% rename from realtime_websocket/funasr_server.py rename to backend/realtime_websocket/funasr_server.py index 6739d66..8ff4630 100644 --- a/realtime_websocket/funasr_server.py +++ b/backend/realtime_websocket/funasr_server.py @@ -26,7 +26,7 @@ except ImportError: 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") LOGGER = logging.getLogger(__name__) WEB_HOST = os.getenv("WEB_HOST", "0.0.0.0") diff --git a/realtime_websocket/model_service_qwen_legacy.py b/backend/realtime_websocket/model_service_qwen_legacy.py similarity index 100% rename from realtime_websocket/model_service_qwen_legacy.py rename to backend/realtime_websocket/model_service_qwen_legacy.py diff --git a/realtime_websocket/requirements.txt b/backend/realtime_websocket/requirements.txt similarity index 100% rename from realtime_websocket/requirements.txt rename to backend/realtime_websocket/requirements.txt diff --git a/realtime_websocket/server_qwen_legacy.py b/backend/realtime_websocket/server_qwen_legacy.py similarity index 99% rename from realtime_websocket/server_qwen_legacy.py rename to backend/realtime_websocket/server_qwen_legacy.py index b6b81f0..cfa3f48 100644 --- a/realtime_websocket/server_qwen_legacy.py +++ b/backend/realtime_websocket/server_qwen_legacy.py @@ -25,7 +25,7 @@ from speaker_assembler import SegmentAssembler # 与部署启动器读取同一配置;外部环境变量优先于 demo/.env。 -DEPLOY_ROOT = Path(__file__).resolve().parents[1] +DEPLOY_ROOT = Path(__file__).resolve().parents[2] load_dotenv(DEPLOY_ROOT / ".env") # 监听所有网卡,允许同一局域网内的浏览器访问服务器上的 Demo;端口集中在代码 diff --git a/realtime_websocket/speaker_assembler.py b/backend/realtime_websocket/speaker_assembler.py similarity index 100% rename from realtime_websocket/speaker_assembler.py rename to backend/realtime_websocket/speaker_assembler.py diff --git a/realtime_websocket/tests/qwen_legacy_model_service_test.py b/backend/realtime_websocket/tests/qwen_legacy_model_service_test.py similarity index 100% rename from realtime_websocket/tests/qwen_legacy_model_service_test.py rename to backend/realtime_websocket/tests/qwen_legacy_model_service_test.py diff --git a/realtime_websocket/tests/qwen_legacy_server_test.py b/backend/realtime_websocket/tests/qwen_legacy_server_test.py similarity index 100% rename from realtime_websocket/tests/qwen_legacy_server_test.py rename to backend/realtime_websocket/tests/qwen_legacy_server_test.py diff --git a/realtime_websocket/tests/test_funasr_engine.py b/backend/realtime_websocket/tests/test_funasr_engine.py similarity index 97% rename from realtime_websocket/tests/test_funasr_engine.py rename to backend/realtime_websocket/tests/test_funasr_engine.py index 0d6ec6b..0106c6a 100644 --- a/realtime_websocket/tests/test_funasr_engine.py +++ b/backend/realtime_websocket/tests/test_funasr_engine.py @@ -7,7 +7,7 @@ import unittest from unittest.mock import patch try: - from realtime_websocket.funasr_engine import ( + from backend.realtime_websocket.funasr_engine import ( FunASRRealtimeSession, FunASRServiceConfig, _result_text, diff --git a/realtime_websocket/tests/test_funasr_server.py b/backend/realtime_websocket/tests/test_funasr_server.py similarity index 95% rename from realtime_websocket/tests/test_funasr_server.py rename to backend/realtime_websocket/tests/test_funasr_server.py index a4f45c9..333bb76 100644 --- a/realtime_websocket/tests/test_funasr_server.py +++ b/backend/realtime_websocket/tests/test_funasr_server.py @@ -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 @@ -10,7 +10,7 @@ from unittest.mock import patch from aiohttp import web from aiohttp.test_utils import AioHTTPTestCase -from realtime_websocket.funasr_server import ( +from backend.realtime_websocket.funasr_server import ( AUXILIARY_KEY, SESSION_REGISTRY_KEY, websocket_handler, @@ -78,7 +78,7 @@ class FunASRBridgeTests(AioHTTPTestCase): async def test_tencent_ui_messages_use_native_funasr_and_keep_speaker_label(self): native = FakeNativeWebSocket() with patch( - "realtime_websocket.funasr_server.websocket_connect", + "backend.realtime_websocket.funasr_server.websocket_connect", return_value=native, ): ws = await self.client.ws_connect("/ws") diff --git a/realtime_websocket/tests/test_speaker_assembler.py b/backend/realtime_websocket/tests/test_speaker_assembler.py similarity index 98% rename from realtime_websocket/tests/test_speaker_assembler.py rename to backend/realtime_websocket/tests/test_speaker_assembler.py index 035ef85..4a44c33 100644 --- a/realtime_websocket/tests/test_speaker_assembler.py +++ b/backend/realtime_websocket/tests/test_speaker_assembler.py @@ -5,7 +5,7 @@ from __future__ import annotations import unittest try: - from realtime_websocket.speaker_assembler import SegmentAssembler + from backend.realtime_websocket.speaker_assembler import SegmentAssembler except ModuleNotFoundError: from speaker_assembler import SegmentAssembler diff --git a/requirements-auxiliary.txt b/backend/requirements-auxiliary.txt similarity index 100% rename from requirements-auxiliary.txt rename to backend/requirements-auxiliary.txt diff --git a/requirements-funasr.txt b/backend/requirements-funasr.txt similarity index 100% rename from requirements-funasr.txt rename to backend/requirements-funasr.txt diff --git a/requirements-qwen-legacy.txt b/backend/requirements-qwen-legacy.txt similarity index 100% rename from requirements-qwen-legacy.txt rename to backend/requirements-qwen-legacy.txt diff --git a/scripts/run_backend.py b/backend/run_backend.py similarity index 97% rename from scripts/run_backend.py rename to backend/run_backend.py index 59174c3..d4157c4 100644 --- a/scripts/run_backend.py +++ b/backend/run_backend.py @@ -191,7 +191,7 @@ def main() -> None: websocket = None try: auxiliary = subprocess.Popen( - [sys.executable, "-m", "scripts.auxiliary_server"], + [sys.executable, "-m", "backend.auxiliary_server"], cwd=PROJECT_ROOT, env=env, ) @@ -199,7 +199,7 @@ def main() -> None: native_args = [ sys.executable, - str(PROJECT_ROOT / "realtime_websocket" / "funasr_native_wss.py"), + str(PROJECT_ROOT / "backend" / "realtime_websocket" / "funasr_native_wss.py"), "--host", native_host, "--port", @@ -230,7 +230,7 @@ def main() -> None: wait_for_tcp(probe_host, native_port, native) websocket = subprocess.Popen( - [sys.executable, "-m", "scripts.run_funasr_demo", "--no-browser"], + [sys.executable, "-m", "backend.run_funasr_demo", "--no-browser"], cwd=PROJECT_ROOT, env=env, ) diff --git a/scripts/run_funasr_demo.py b/backend/run_funasr_demo.py similarity index 84% rename from scripts/run_funasr_demo.py rename to backend/run_funasr_demo.py index ce598e8..6957589 100644 --- a/scripts/run_funasr_demo.py +++ b/backend/run_funasr_demo.py @@ -10,7 +10,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: 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__": diff --git a/scripts/serve_qwen_legacy.py b/backend/serve_qwen_legacy.py similarity index 96% rename from scripts/serve_qwen_legacy.py rename to backend/serve_qwen_legacy.py index b871028..a102c93 100644 --- a/scripts/serve_qwen_legacy.py +++ b/backend/serve_qwen_legacy.py @@ -19,16 +19,16 @@ from dotenv import load_dotenv # 启动器自动读取 demo/.env;系统环境变量仍然优先,便于部署平台临时覆盖配置。 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") # 将服务端口集中在代码变量中维护,启动时不需要额外传入端口参数;健康检查、 # VLLM 子进程命令和就绪提示都使用同一个端口,避免配置不一致导致误判。 SERVER_PORT = int(os.getenv("VLLM_PORT", "9950")) -try: - from .model_manifest import load_manifest, model_directory, resolve_model_id -except ImportError: - from model_manifest import load_manifest, model_directory, resolve_model_id +from scripts.model_manifest import load_manifest, model_directory, resolve_model_id 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 = shutil.which(executable_name) 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 command = [ diff --git a/tests/qwen_legacy_model_manifest_test.py b/backend/tests/qwen_legacy_model_manifest_test.py similarity index 98% rename from tests/qwen_legacy_model_manifest_test.py rename to backend/tests/qwen_legacy_model_manifest_test.py index 18c1621..0d96460 100644 --- a/tests/qwen_legacy_model_manifest_test.py +++ b/backend/tests/qwen_legacy_model_manifest_test.py @@ -39,7 +39,7 @@ class ModelManifestTests(unittest.TestCase): self.assertIn("Qwen/Qwen3-ForcedAligner-0.6B", assets) 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)]: self.assertTrue(model_directory(model_id, self.manifest, models_dir).is_relative_to(models_dir)) diff --git a/tests/qwen_legacy_serve_test.py b/backend/tests/qwen_legacy_serve_test.py similarity index 90% rename from tests/qwen_legacy_serve_test.py rename to backend/tests/qwen_legacy_serve_test.py index 4c7003a..6a4b6f8 100644 --- a/tests/qwen_legacy_serve_test.py +++ b/backend/tests/qwen_legacy_serve_test.py @@ -7,7 +7,7 @@ import unittest from pathlib import Path 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): @@ -29,7 +29,7 @@ class ServeConfigTests(unittest.TestCase): self.assertEqual(args.startup_check_loops, 12) 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: """启动器应生成已验证的新版 vllm serve 命令。""" args = build_parser().parse_args([]) diff --git a/tests/test_auxiliary_server.py b/backend/tests/test_auxiliary_server.py similarity index 94% rename from tests/test_auxiliary_server.py rename to backend/tests/test_auxiliary_server.py index 08e4ed0..716336c 100644 --- a/tests/test_auxiliary_server.py +++ b/backend/tests/test_auxiliary_server.py @@ -7,7 +7,7 @@ import numpy as np from types import SimpleNamespace from unittest.mock import patch -from scripts.auxiliary_server import ( +from backend.auxiliary_server import ( AuxiliaryRuntime, _coerce_finite_float, _normalize_diarization_segments, @@ -42,8 +42,8 @@ class AuxiliaryServerTests(unittest.TestCase): "aligner": {"kind": "forced_aligner"}, } loaded_kinds = [] - with patch("scripts.auxiliary_server._asset_ready", return_value=True), \ - patch("scripts.auxiliary_server.model_directory", return_value=runtime.manifest and SimpleNamespace()), \ + with patch("backend.auxiliary_server._asset_ready", return_value=True), \ + 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()): runtime.preload() self.assertEqual(loaded_kinds, ["vad"]) @@ -57,8 +57,8 @@ class AuxiliaryServerTests(unittest.TestCase): "vad": {"kind": "vad"}, "campplus": {"kind": "speaker_verification"}, } - with patch("scripts.auxiliary_server._asset_ready", side_effect=[True, False]), \ - patch("scripts.auxiliary_server.model_directory", return_value=SimpleNamespace()): + with patch("backend.auxiliary_server._asset_ready", side_effect=[True, False]), \ + patch("backend.auxiliary_server.model_directory", return_value=SimpleNamespace()): with self.assertRaisesRegex(RuntimeError, "campplus"): runtime.preload() @@ -76,8 +76,8 @@ class AuxiliaryServerTests(unittest.TestCase): """VAD 是核心依赖,缺失时错误必须给出可执行的修复方向。""" runtime = AuxiliaryRuntime() runtime.assets = {"vad": {"kind": "vad"}} - with patch("scripts.auxiliary_server._asset_ready", return_value=False), \ - patch("scripts.auxiliary_server.model_directory", return_value=SimpleNamespace(__str__=lambda self: "/models/vad")): + with patch("backend.auxiliary_server._asset_ready", return_value=False), \ + patch("backend.auxiliary_server.model_directory", return_value=SimpleNamespace(__str__=lambda self: "/models/vad")): with self.assertRaisesRegex(RuntimeError, "download_models.py --auxiliary-only"): runtime.preload() diff --git a/frontend/requirements.txt b/frontend/requirements.txt new file mode 100644 index 0000000..4d3226d --- /dev/null +++ b/frontend/requirements.txt @@ -0,0 +1,3 @@ +# Dependencies for serving the unchanged Tencent demo UI. +aiohttp==3.11.11 +python-dotenv>=1.0 diff --git a/scripts/run_frontend.py b/frontend/run_frontend.py similarity index 98% rename from scripts/run_frontend.py rename to frontend/run_frontend.py index d35abf8..5b5cb91 100644 --- a/scripts/run_frontend.py +++ b/frontend/run_frontend.py @@ -13,7 +13,7 @@ from aiohttp import ClientSession, ClientTimeout, WSMsgType, web from dotenv import load_dotenv 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") FRONTEND_HOST = os.getenv("FRONTEND_HOST", "127.0.0.1") FRONTEND_PORT = int(os.getenv("FRONTEND_PORT", "8080")) diff --git a/realtime_websocket/static/app.js b/frontend/static/app.js similarity index 100% rename from realtime_websocket/static/app.js rename to frontend/static/app.js diff --git a/realtime_websocket/static/index.html b/frontend/static/index.html similarity index 100% rename from realtime_websocket/static/index.html rename to frontend/static/index.html diff --git a/realtime_websocket/static/style.css b/frontend/static/style.css similarity index 100% rename from realtime_websocket/static/style.css rename to frontend/static/style.css diff --git a/realtime_websocket/tests/test_frontend.cjs b/frontend/tests/test_frontend.cjs similarity index 100% rename from realtime_websocket/tests/test_frontend.cjs rename to frontend/tests/test_frontend.cjs diff --git a/pyproject.toml b/pyproject.toml index b2bbe73..a9e9a24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,8 +18,8 @@ dependencies = [ ] [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" [tool.setuptools] -packages = ["scripts", "realtime_websocket"] +packages = ["scripts", "backend", "backend.realtime_websocket"] diff --git a/pyproject_qwen_legacy.toml b/pyproject_qwen_legacy.toml index 30ed923..f446659 100644 --- a/pyproject_qwen_legacy.toml +++ b/pyproject_qwen_legacy.toml @@ -14,7 +14,7 @@ dependencies = [ [project.scripts] qwen3-asr-download = "scripts.download_models:main" -qwen3-asr-serve = "scripts.serve:main" +qwen3-asr-serve = "backend.serve_qwen_legacy:main" [tool.setuptools] -packages = ["scripts"] +packages = ["scripts", "backend", "backend.realtime_websocket"] diff --git a/scripts/download_models_qwen_legacy.py b/scripts/download_models_qwen_legacy.py index 91fe8c0..c742305 100644 --- a/scripts/download_models_qwen_legacy.py +++ b/scripts/download_models_qwen_legacy.py @@ -73,7 +73,7 @@ def download_model( from modelscope.hub.snapshot_download import snapshot_download except ImportError as exc: 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 model_path.parent.mkdir(parents=True, exist_ok=True)