Files
2025-12-01 03:42:34 +08:00

168 lines
6.3 KiB
Python

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
import uvicorn
from starlette.middleware.cors import CORSMiddleware
import wave
import io
import asyncio
import websockets
import json
app = FastAPI()
# 跨域配置
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 存储活跃连接
active_connections = []
# 音频参数(必须和前端一致!)
AUDIO_PARAMS = {
"sample_rate": 16000, # 前端设置的 16000
"channels": 1, # 单声道
"sample_width": 2, # 16位深度(2字节/样本)
"frame_size": 1024 # 前端设置的 1024
}
# 语音识别服务配置(替换为你的识别服务地址)
ASR_CONFIG = {
# "host": "192.168.109.129",
"host": "10.10.10.202",
"port": 10096,
"mode": "2pass", # 识别模式:online/offline/2pass
"chunk_size": [5, 10, 5],
"chunk_interval": 10,
"use_itn": 1,
"hotwords": "" # 热词(可选)
}
# 封装 PCM 为 WAV(如果识别服务需要 WAV 格式)
def pcm_to_wav(pcm_data: bytes, params: dict) -> bytes:
output = io.BytesIO()
with wave.open(output, 'wb') as wf:
wf.setnchannels(params["channels"])
wf.setsampwidth(params["sample_width"])
wf.setframerate(params["sample_rate"])
wf.writeframes(pcm_data)
output.seek(0)
return output.read()
# 语音识别客户端协程:连接 ASR 服务,发送音频,接收结果
async def asr_client(websocket: WebSocket, audio_queue: asyncio.Queue):
"""
连接语音识别服务,处理音频转发和结果回传
:param websocket: 前端的 WebSocket 连接
:param audio_queue: 音频数据队列(接收前端的 PCM 数据)
"""
# 1. 连接 ASR 服务
asr_uri = f"ws://{ASR_CONFIG['host']}:{ASR_CONFIG['port']}"
try:
async with websockets.connect(
asr_uri,
subprotocols=["binary"],
ping_interval=None
) as asr_ws:
# 2. 发送 ASR 初始化配置
init_msg = json.dumps({
"mode": ASR_CONFIG["mode"],
"chunk_size": ASR_CONFIG["chunk_size"],
"chunk_interval": ASR_CONFIG["chunk_interval"],
"wav_name": "frontend_stream",
"is_speaking": True,
"hotwords": ASR_CONFIG["hotwords"],
"itn": bool(ASR_CONFIG["use_itn"]),
"audio_fs": AUDIO_PARAMS["sample_rate"]
})
await asr_ws.send(init_msg)
print("已发送 ASR 初始化配置", init_msg)
# 3. 协程1:从队列读取前端音频,发送给 ASR 服务
async def send_audio():
while True:
try:
# 从队列获取前端的 PCM 数据
pcm_data = await audio_queue.get()
# 如果识别服务需要 WAV 格式,取消下面注释
# wav_data = pcm_to_wav(pcm_data, AUDIO_PARAMS)
# await asr_ws.send(wav_data)
# 否则直接发送 PCM 裸数据
await asr_ws.send(pcm_data)
await asyncio.sleep(0.005) # 控制发送速率
except Exception as e:
print(f"发送音频到 ASR 失败:{e}")
break
# 4. 协程2:接收 ASR 识别结果,回传给前端
async def recv_asr_result():
while True:
try:
asr_result = await asr_ws.recv()
asr_result_json = json.loads(asr_result)
# 仅保留核心识别结果(按需调整)
result = {
"text": asr_result_json.get("text", ""),
"mode": asr_result_json.get("mode", ""),
"timestamp": asr_result_json.get("timestamp", ""),
"is_final": asr_result_json.get("is_final", False)
}
# 回传给当前前端客户端2
print('result', result)
ORIGINAL_STR = "Hello 世界!123@#"
binary_data = ORIGINAL_STR.encode("utf-8")
print(f"原始字符串:{ORIGINAL_STR}")
print(f"转二进制后:{binary_data}(长度:{len(binary_data)} 字节)")
# 2. 发送二进制数据
await websocket.send_bytes(binary_data)
except Exception as e:
print(f"接收 ASR 结果失败:{e}")
break
# 5. 并发运行两个协程
await asyncio.gather(send_audio(), recv_asr_result())
except Exception as e:
print(f"ASR 客户端连接失败:{e}")
# 给前端返回错误
await websocket.send_json({"error": str(e), "text": ""})
@app.websocket("/ws/audio")
async def websocket_audio(websocket: WebSocket):
await websocket.accept()
active_connections.append(websocket)
print(f"客户端连接成功,当前连接数:{len(active_connections)}")
# 创建音频队列:前端音频 -> 队列 -> ASR 客户端
audio_queue = asyncio.Queue(maxsize=100) # 队列大小防止积压
# 启动 ASR 客户端协程
asr_task = asyncio.create_task(asr_client(websocket, audio_queue))
try:
while True:
# 1. 接收前端的裸 PCM 音频帧
pcm_data = await websocket.receive_bytes()
# 2. 将音频放入队列,交给 ASR 客户端处理
if not audio_queue.full():
audio_queue.put_nowait(pcm_data)
except WebSocketDisconnect:
active_connections.remove(websocket)
print(f"客户端断开,当前连接数:{len(active_connections)}")
except Exception as e:
if websocket in active_connections:
active_connections.remove(websocket)
print(f"处理音频失败:{e}")
finally:
# 停止 ASR 客户端协程
asr_task.cancel()
await asr_task
if __name__ == "__main__":
# 启动 FastAPI 服务
uvicorn.run(app, host="0.0.0.0", port=8000)