diff --git a/README.md b/README.md index 08b23e8..ffce600 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,8 @@ docker compose down | --- | --- | --- | --- | | Speakr | `voice-speakr` | `8899` | 页面和后端 | | ASR | `asr-test` | `59805` | Paraformer HTTP / WebSocket 识别 | -| 官方实时 ASR | `funasr-online` | `10096` | FunASR 官方实时 WebSocket | +| Python 实时 ASR | `funasr-online` | `10096` | FunASR Python WebSocket,保留用于回退 | +| 官方 Runtime 实时 ASR | `funasr-runtime-2pass` | `10097` | FunASR 官方 C++/ONNX 2pass WebSocket | | 声纹提取 | `voice-dected` | `18000` | `/extract_embedding` 声纹向量服务 | | 实时桥接 | `fastapi-wss` | `10095` | 页面实时 ASR WebSocket 桥接 | | Redis | `voice-redis` | `16380` | 实时桥接的缓存/会话存储 | @@ -86,7 +87,9 @@ docker compose up -d ```env ASR_BASE_URL=http://asr-test:59805 FUNASR_WEBSOCKET_IP=ws://fastapi-wss:10095/websocket_offline -TARGET_WS_URL=ws://funasr-online:10096 +TARGET_WS_URL=ws://funasr-runtime-2pass:10095 +# 如需回退到 Python 版实时 ASR,可改为: +# TARGET_WS_URL=ws://funasr-online:10096 VOICE_DETECTED_URL=http://voice-dected:8000 SPEAKER_DET_URL=http://voice-dected:8000/extract_embedding REDIS_URL=redis://redis:6379/0 diff --git a/config.env b/config.env index 95349d9..53e04c9 100644 --- a/config.env +++ b/config.env @@ -14,8 +14,10 @@ TEXT_MODEL_API_KEY=none # Realtime WebSocket bridge FUNASR_WEBSOCKET_IP=ws://fastapi-wss:10095/websocket_offline -TARGET_WS_URL=ws://funasr-online:10096 -REDIS_URL=redis://redis:6379/0 +TARGET_WS_URL=ws://funasr-runtime-2pass:10095 +# Optional A/B target for the official C++/ONNX FunASR runtime service: +# TARGET_WS_URL=ws://funasr-online:10096 +REDIS_URL=redis://redis:6379/0 LLM_API_KEY=none LLM_BASE_URL=http://192.168.0.46:59800/v1 LLM_MODEL=46-qwen3.5-35B diff --git a/docker-compose.yml b/docker-compose.yml index b634997..c6bc377 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -94,9 +94,9 @@ services: - "1" - funasr-online: - image: paraformer-asr:latest - container_name: funasr-online + funasr-online: + image: paraformer-asr:latest + container_name: funasr-online restart: unless-stopped runtime: nvidia gpus: @@ -153,23 +153,53 @@ services: - --keyfile - "" + funasr-runtime-2pass: + image: registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.13 + container_name: funasr-runtime-2pass + restart: unless-stopped + working_dir: /workspace/FunASR/runtime + volumes: + - ./deploy/data/funasr-runtime-models:/workspace/models + ports: + - "10097:10095" + command: + - bash + - -lc + - > + touch /workspace/models/hotwords.txt && + exec /workspace/FunASR/runtime/websocket/build/bin/funasr-wss-server-2pass + --download-model-dir /workspace/models + --vad-dir damo/speech_fsmn_vad_zh-cn-16k-common-onnx + --model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-onnx + --online-model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx + --punc-dir damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx + --lm-dir damo/speech_ngram_lm_zh-cn-ai-wesp-fst + --itn-dir thuduj12/fst_itn_zh + --decoder-thread-num 4 + --model-thread-num 1 + --io-thread-num 1 + --port 10095 + --hotword /workspace/models/hotwords.txt + --certfile "" + --keyfile "" + fastapi-wss: - image: fastapi-wss:runtime - container_name: fastapi-wss + image: fastapi-wss:runtime + container_name: fastapi-wss restart: unless-stopped working_dir: /app/src - env_file: - - ./config.env - environment: - TARGET_WS_URL: ws://funasr-online:10096 - REDIS_URL: redis://redis:6379/0 + env_file: + - ./config.env + environment: + TARGET_WS_URL: ws://funasr-runtime-2pass:10095 + REDIS_URL: redis://redis:6379/0 volumes: - ./fastapi_wss:/app ports: - "10095:10095" - depends_on: - - funasr-online - - redis + depends_on: + - funasr-runtime-2pass + - redis command: - python - a2a_wss.py diff --git a/fastapi_wss/src/a2a_wss.py b/fastapi_wss/src/a2a_wss.py index 020e2a5..0e51d02 100644 --- a/fastapi_wss/src/a2a_wss.py +++ b/fastapi_wss/src/a2a_wss.py @@ -19,6 +19,7 @@ import os import websockets import re import logging +from contextlib import asynccontextmanager from pathlib import Path from typing import List, Optional, Dict, Set from dataclasses import dataclass @@ -789,6 +790,33 @@ def build_role_enriched_message(speaker_msg: Dict, role_result: Dict) -> Dict: # 5. WebSocket 处理与任务追踪 # ======================== +@asynccontextmanager +async def connect_target_asr(): + connect_options = { + "close_timeout": 10, + "ping_interval": None, + "ping_timeout": None, + "max_queue": 1024, + } + try: + target_ws = await websockets.connect( + TARGET_WS_URL, + **connect_options, + subprotocols=["binary"], + ) + except websockets.exceptions.InvalidMessage as exc: + logger.warning( + "ASR WebSocket binary subprotocol handshake failed, retrying without subprotocol: %s", + exc, + ) + target_ws = await websockets.connect(TARGET_WS_URL, **connect_options) + + try: + yield target_ws + finally: + await target_ws.close() + + async def forward_audio(websocket): """ WebSocket 连接处理函数(核心入口) @@ -824,14 +852,7 @@ async def forward_audio(websocket): try: # 连接到目标 ASR 服务 # 添加连接参数以提高鲁棒性 - async with websockets.connect( - TARGET_WS_URL, - close_timeout=10, # 关闭超时 - ping_interval=None, - ping_timeout=None, - max_queue=1024, # 消息队列大小 - subprotocols=["binary"], - ) as target_ws: + async with connect_target_asr() as target_ws: logger.info(f"已连接到 ASR 服务: {TARGET_WS_URL}") # 发送 ASR 配置(双通道模式,PCM 音频格式)