Files
aistream-test/python/frontend_ws_buck.py
2025-12-01 03:42:34 +08:00

301 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# frontend_ws.py
from fastapi import WebSocket, WebSocketDisconnect
from session_manager import create_session, close_session
# 导入大模型模块
from llm_client import call_llm, LLMConversation, llm_client
import numpy as np
from typing import Optional, Dict, Callable, Awaitable, List
from dataclasses import dataclass, field
import sys
import json
import asyncio
import logging
logger = logging.getLogger(__name__)
from ws_message_manager import (
ws_queue_manager,
ClientMsgType,
ServerMsgType,
)
from asr_client import (
init_asr_pool,
get_idle_asr_connection,
handle_asr_communication,
push_audio_data, # 导入音频插入接口
close_asr_pool
)
from tts_client import CosyVoiceTTSSocketClient
active_connections = []
user_llm_conversations: dict[str, LLMConversation] = {}
async def frontend_websocket_handler(websocket: WebSocket):
"""前端WebSocket入口处理器(保留SessionASR/TTS无状态)"""
# 1. 握手+创建会话(核心:Session 管理连接上下文)
await websocket.accept()
active_connections.append(websocket)
conn_id = id(websocket)
user_id = f"user_{id(websocket)}" # 实际场景替换为真实用户ID
session = await create_session(websocket) # 创建会话(无用户逻辑)
logger.info(f"连接 {conn_id} 建立成功")
# await ws_queue_manager.create_queue(websocket)
tts_session_id = user_id
result_queue = asyncio.Queue(maxsize=100)
asr_task = None
asr_conn = None
# 初始化用户LLM会话
if user_id not in user_llm_conversations:
user_llm_conversations[user_id] = LLMConversation(
user_id=user_id,
scene_description="语音识别对话场景" # 自定义大模型场景
)
llm_conversation = user_llm_conversations[user_id]
# 1. 创建客户端
client = CosyVoiceTTSSocketClient(
ws_url="ws://10.10.10.202:50000/ws/tts",
max_queue_size=500 # 最大队列长度
)
# 2. 注册回调(按需自定义)
client.on_audio_chunk = lambda req_id, chunk: print(f"收到[{req_id[:8]}]音频块,长度: {len(chunk)}")
# 3. 建立连接(自动启动队列消费)
await client.connect()
# ---------------------- TTS 音频回调(转发前端) ----------------------
async def tts_audio_callback(audio_chunk, sample_rate, is_finished):
"""将音频块转发给前端的逻辑"""
print(f"音频块长度:{len(audio_chunk)},采样率:{sample_rate},是否结束:{is_finished}")
#todo
# ---------------------- 定义 ASR 结果回调函数 ----------------------
async def asr_result_callback(result: dict):
"""
ASR 结果回调:同时转发给前端 + 推送大模型
:param result: ASR 识别结果字典
"""
try:
print('结果回调:同时转发给前端 + 推送大模型', result)
final_asr_text = result["text"]
print(f"ASR最终识别结果: {final_asr_text}")
# 调用大模型(异步,不阻塞)
asyncio.create_task(
call_llm_and_send(
query=final_asr_text,
conversation=llm_conversation
)
)
# # 1. 转发给前端(确保连接未断开)
# if not websocket.client_state.disconnected:
# await websocket.send_json(result)
#
# # 2. 推送到大模型(过滤错误/空结果)
# if not result.get("error") and result.get("text"):
# # 异步推送,不阻塞回调
# asyncio.create_task(send_to_llm(result))
except Exception as e:
print(f"回调函数执行失败:{e}")
# -------------------------------------------------------------------
# ---------------------- 大模型流式回调(转发前端) ----------------------
async def llm_stream_callback(chunk: str, conversation_id: str, is_finished: bool):
req_id1 = client.add_tts_request(tts_text=chunk)
print('req_id1', req_id1)
"""
大模型流式回调:将实时回复转发给前端
"""
# if websocket.client_state.disconnected:
# return
# try:
# print('大模型流式回调', chunk)
# await websocket.send_json({
# "type": "llm_result",
# "data": {
# "chunk": chunk,
# "conversation_id": conversation_id,
# "is_finished": is_finished,
# "query": final_asr_text # 关联ASR查询文本
# }
# })
# except Exception as e:
# print(f"LLM回调执行失败: {e}")
# if websocket.client_state.disconnected:
# return
try:
# 1. 转发大模型文本给前端
# await websocket.send_json({
# "type": "llm_result",
# "data": {
# "chunk": chunk,
# "conversation_id": conversation_id,
# "is_finished": is_finished,
# "user_id": user_id
# }
# })
# print('大模型返回的', chunk)
# await tts_client.send_text_chunk(tts_session_id, chunk)
pass
# 2. 流式文本发送给 TTS(非错误文本 + 非结束标识)
# if chunk and not chunk.startswith("[错误]") and not is_finished:
# await tts_client.send_text_chunk(tts_session_id, chunk)
# # 合成结束标识
# elif is_finished:
# # 发送空文本块触发 TTS 结束
# await tts_client.send_text_chunk(tts_session_id, "")
except Exception as e:
print(f"LLM回调联动TTS失败({user_id}): {e}")
# ---------------------- 调用大模型并转发结果 ----------------------
async def call_llm_and_send(query: str, conversation: LLMConversation):
"""调用大模型,流式结果通过WebSocket返回前端"""
if not query:
return
print(f"调用大模型 - 用户({user_id}): {query}")
# 调用大模型(流式)
conv_id, full_reply = await llm_client.send_message(
query=query,
conversation=conversation,
stream_callback=llm_stream_callback,
response_mode="streaming"
)
print(f"大模型回复完成 - 会话ID: {conv_id}, 完整回复: {full_reply}")
# -------------------------------------------------------------------
try:
# 1. 获取 ASR 连接
asr_conn = await get_idle_asr_connection()
if not asr_conn:
await websocket.send_json({"error": "ASR 服务暂时不可用", "text": ""})
return
# 启动 ASR 协程(传递回调函数,移除结果队列)
asr_task = asyncio.create_task(
handle_asr_communication(asr_conn, asr_result_callback)
)
# 3. 并行处理:接收前端数据 + 发送 ASR 结果
async def recv_frontend_data():
"""接收前端所有数据,仅将音频数据插入 ASR 队列"""
while not asr_conn.stop_event.is_set():
try:
raw_bytes = await websocket.receive_bytes()
success = await push_audio_data(asr_conn, raw_bytes)
if not success:
print("音频数据插入失败(队列满/连接失效)")
# # 接收前端数据(自动区分类型)
# data_type = websocket.receive()
# if data_type.type == "bytes":
# # 二进制数据 = 音频数据,调用 ASR 接口插入
# raw_bytes = await websocket.receive_bytes()
# # 调用 ASR 模块的插入接口,无需管理队列
# success = await push_audio_data(asr_conn, raw_bytes)
# if not success:
# print("音频数据插入失败(队列满/连接失效)")
# elif data_type.type == "json":
# # JSON 数据 = 控制指令
# control_data = await websocket.receive_json()
# print(f"收到前端控制指令:{control_data}")
# if control_data.get("action") == "stop":
# asr_conn.stop_event.set() # 触发 ASR 停止
# await websocket.send_json({"text": "", "error": "用户主动停止"})
# break
# elif data_type.type == "text":
# # 文本数据 = 其他指令
# text_data = await websocket.receive_text()
# print(f"收到前端文本数据:{text_data}")
except WebSocketDisconnect:
print("前端主动断开连接")
asr_conn.stop_event.set()
break
except Exception as e:
print(f"接收前端数据失败:{e}")
asr_conn.stop_event.set()
await websocket.send_json({"error": f"接收数据失败:{str(e)}", "text": ""})
break
async def send_asr_result():
"""从结果队列取数据,发送给前端"""
while not asr_conn.stop_event.is_set():
try:
result = await asyncio.wait_for(result_queue.get(), timeout=1.0)
# if not websocket.client_state.disconnected:
print('result', result)
await websocket.send_json(result)
except asyncio.TimeoutError:
continue
except Exception as e:
print(f"发送 ASR 结果失败:{e}")
asr_conn.stop_event.set()
break
# 4. 并发执行
await asyncio.gather(recv_frontend_data(), send_asr_result())
except Exception as e:
print(f"WebSocket 处理异常:{e}")
if asr_conn:
asr_conn.stop_event.set()
await websocket.send_json({"error": str(e), "text": ""})
finally:
# 清理资源
if asr_conn:
asr_conn.stop_event.set()
# 取消 ASR 任务
if asr_task and not asr_task.done():
asr_task.cancel()
try:
await asr_task
except asyncio.CancelledError:
pass
# 移除活跃连接
if websocket in active_connections:
active_connections.remove(websocket)
print(f"客户端连接已清理,当前连接数:{len(active_connections)}")
# 关闭前端连接
# if not websocket.client_state.disconnected:
# await websocket.close()
# try:
#
# # 4. 持续接收前端消息(二进制,兼容音频/文本/指令)
# while not session.is_closed:
# raw_bytes = await websocket.receive_bytes()
# # print('raw_bytes', raw_bytes)
# # todo 前段暂时传入的全部都是语音消息
# if not audio_queue.full():
# audio_queue.put_nowait(raw_bytes)
# # await forward_frontend_to_asr(session, raw_bytes)
# # 解析前端消息类型(复用之前的编解码器)
# # msg_type, data = WsMsgCodec.decode_client_msg(raw_bytes)
# # # 分发处理(不同类型消息走不同逻辑)
# # if msg_type == ClientMsgType.AUDIO:
# # # 音频直接转发给ASR(Session 传递ASR客户端,无用户标识)
# # if session.asr_client and not session.is_closed:
# # await send_audio_to_asr(session.asr_client, data)
# # elif msg_type == ClientMsgType.TEXT:
# # # 文本直接走LLM(无需ASR)
# # await forward_text_to_llm(session, data)
# # elif msg_type == ClientMsgType.ACTION:
# # # 操作指令处理
# # await handle_frontend_action(session, data)
#
# except WebSocketDisconnect:
# logger.info(f"会话 [{session.session_id}] 前端主动断开")
# except Exception as e:
# logger.error(f"会话 [{session.session_id}] 异常: {str(e)}", exc_info=True)
# finally:
# # 5. 清理会话(一键关闭所有资源,核心价值)
# await close_session(session)
# await websocket.close()