diff --git a/audio_ai_chat/.env b/audio_ai_chat/.env
new file mode 100644
index 0000000..6eda825
--- /dev/null
+++ b/audio_ai_chat/.env
@@ -0,0 +1,32 @@
+# 服务端口
+APP_PORT=8000
+# 日志级别(DEBUG/INFO/WARNING/ERROR)
+LOG_LEVEL=INFO
+# 日志文件路径
+LOG_FILE=logs/app.log
+
+# ASR服务配置
+ASR_SERVICE_URL=http://localhost:5000/asr
+ASR_TIMEOUT=30 # 超时时间(秒)
+ASR_RETRY_TIMES=2 # 重试次数
+
+# LLM服务配置
+LLM_SERVICE_URL=http://localhost:6000/chat
+LLM_TIMEOUT=60
+LLM_RETRY_TIMES=2
+LLM_MODEL=gpt-3.5-turbo # 可选:指定大模型版本
+
+# TTS服务配置
+TTS_SERVICE_URL=http://localhost:7000/tts
+TTS_TIMEOUT=30
+TTS_RETRY_TIMES=2
+TTS_VOICE=female # 可选:指定语音类型
+
+# WebSocket配置
+WS_MAX_SIZE=10485760 # 最大消息大小(10MB)
+WS_PING_INTERVAL=30 # 心跳检测间隔(秒)
+WS_PING_TIMEOUT=10 # 心跳超时时间(秒)
+
+# 加密配置(ProtocolCodec依赖的密钥等)
+ENCRYPT_KEY=your_secret_key_123
+ENCRYPT_IV=your_iv_456
\ No newline at end of file
diff --git a/audio_ai_chat/.env.example b/audio_ai_chat/.env.example
new file mode 100644
index 0000000..6eda825
--- /dev/null
+++ b/audio_ai_chat/.env.example
@@ -0,0 +1,32 @@
+# 服务端口
+APP_PORT=8000
+# 日志级别(DEBUG/INFO/WARNING/ERROR)
+LOG_LEVEL=INFO
+# 日志文件路径
+LOG_FILE=logs/app.log
+
+# ASR服务配置
+ASR_SERVICE_URL=http://localhost:5000/asr
+ASR_TIMEOUT=30 # 超时时间(秒)
+ASR_RETRY_TIMES=2 # 重试次数
+
+# LLM服务配置
+LLM_SERVICE_URL=http://localhost:6000/chat
+LLM_TIMEOUT=60
+LLM_RETRY_TIMES=2
+LLM_MODEL=gpt-3.5-turbo # 可选:指定大模型版本
+
+# TTS服务配置
+TTS_SERVICE_URL=http://localhost:7000/tts
+TTS_TIMEOUT=30
+TTS_RETRY_TIMES=2
+TTS_VOICE=female # 可选:指定语音类型
+
+# WebSocket配置
+WS_MAX_SIZE=10485760 # 最大消息大小(10MB)
+WS_PING_INTERVAL=30 # 心跳检测间隔(秒)
+WS_PING_TIMEOUT=10 # 心跳超时时间(秒)
+
+# 加密配置(ProtocolCodec依赖的密钥等)
+ENCRYPT_KEY=your_secret_key_123
+ENCRYPT_IV=your_iv_456
\ No newline at end of file
diff --git a/audio_ai_chat/.idea/.gitignore b/audio_ai_chat/.idea/.gitignore
new file mode 100644
index 0000000..10b731c
--- /dev/null
+++ b/audio_ai_chat/.idea/.gitignore
@@ -0,0 +1,5 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
diff --git a/audio_ai_chat/.idea/audio_ai_chat.iml b/audio_ai_chat/.idea/audio_ai_chat.iml
new file mode 100644
index 0000000..4a03caf
--- /dev/null
+++ b/audio_ai_chat/.idea/audio_ai_chat.iml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/audio_ai_chat/.idea/inspectionProfiles/Project_Default.xml b/audio_ai_chat/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..7d133ac
--- /dev/null
+++ b/audio_ai_chat/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/audio_ai_chat/.idea/inspectionProfiles/profiles_settings.xml b/audio_ai_chat/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/audio_ai_chat/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/audio_ai_chat/.idea/misc.xml b/audio_ai_chat/.idea/misc.xml
new file mode 100644
index 0000000..d6019ad
--- /dev/null
+++ b/audio_ai_chat/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/audio_ai_chat/.idea/modules.xml b/audio_ai_chat/.idea/modules.xml
new file mode 100644
index 0000000..f7af7c8
--- /dev/null
+++ b/audio_ai_chat/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/audio_ai_chat/.idea/vcs.xml b/audio_ai_chat/.idea/vcs.xml
new file mode 100644
index 0000000..6c0b863
--- /dev/null
+++ b/audio_ai_chat/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/audio_ai_chat/README.md b/audio_ai_chat/README.md
new file mode 100644
index 0000000..aeb6dc3
--- /dev/null
+++ b/audio_ai_chat/README.md
@@ -0,0 +1,71 @@
+# 语音AI对话系统(WebSocket版)
+基于FastAPI实现,支持前端通过WebSocket推送语音数据,串联ASR(语音转文字)→ LLM(大模型对话)→ TTS(文字转语音)全流程,返回语音结果给前端。
+
+## 快速启动
+1. 创建Conda虚拟环境:`conda create -n audio-ai-chat python=3.10 -y`
+2. 激活环境:`conda activate audio-ai-chat`
+3. 安装依赖:`pip install -r requirements.txt`
+4. 配置环境变量:复制`.env.example`为`.env`,修改服务地址、密钥等配置
+5. 启动服务:`python run.py`
+6. 接口文档:访问 `http://localhost:8000/docs`(FastAPI自动生成)
+
+## 目录说明
+ audio_ai_chat/ # 项目根目录(包名,Python可导入)
+ ├── audio_ai_chat/ # 核心代码目录(与根目录同名,避免导入冲突)
+ │ ├── __init__.py # 包初始化文件(空文件即可)
+ │ ├── main.py # 项目启动类(FastAPI入口+WebSocket路由)
+ │ ├── config/ # 配置文件目录
+ │ │ ├── __init__.py
+ │ │ ├── settings.py # 核心配置(ASR/LLM/TTS服务地址、WS参数等)
+ │ │ └── logger.py # 日志配置(格式、输出路径、级别)
+ │ ├── core/ # 核心业务逻辑(调整服务调用方式)
+ │ │ ├── __init__.py
+ │ │ ├── websocket_handler.py# 调整为:通过工厂类获取服务实例
+ │ │ ├── asr/ # ASR服务目录(多版本实现)
+ │ │ │ ├── __init__.py
+ │ │ │ ├── base.py # ASR统一接口抽象类
+ │ │ │ ├── version1.py # ASR版本1(如:本地离线版)
+ │ │ │ ├── version2.py # ASR版本2(如:百度云ASR)
+ │ │ │ └── factory.py # ASR工厂类(根据配置创建实例)
+ │ │ ├── llm/ # LLM服务目录(多版本实现)
+ │ │ │ ├── __init__.py
+ │ │ │ ├── base.py # LLM统一接口抽象类
+ │ │ │ ├── openai_llm.py # LLM版本1(OpenAI)
+ │ │ │ ├── local_llm.py # LLM版本2(本地部署LLM)
+ │ │ │ └── factory.py # LLM工厂类
+ │ │ └── tts/ # TTS服务目录(多版本实现)
+ │ │ ├── __init__.py
+ │ │ ├── base.py # TTS统一接口抽象类
+ │ │ ├── ali_tts.py # TTS版本1(阿里云)
+ │ │ ├── pyttsx3_tts.py # TTS版本2(本地pyttsx3)
+ │ │ └── factory.py # TTS工厂类
+ │ ├── codec/ # 数据编解码目录(存放加密/解密逻辑)
+ │ │ ├── __init__.py
+ │ │ └── ProtocolCodec.py # 已有:WS数据加密/解密、协议编解码
+ │ ├── models/ # 数据模型目录(Pydantic/数据结构定义)
+ │ │ ├── __init__.py
+ │ │ └── ws_models.py # WebSocket消息结构(请求/响应模型、错误模型)
+ │ └── utils/ # 工具函数目录
+ │ ├── __init__.py
+ │ ├── exceptions.py # 自定义异常(如服务调用失败、解码失败)
+ │ └── helpers.py # 通用工具(日志封装、异步重试、数据格式转换)
+ ├── logs/ # 日志输出目录(自动创建)
+ │ └── app.log # 应用日志文件(按配置滚动生成)
+ ├── .env # 环境变量文件(敏感配置,不提交Git)
+ ├── .env.example # 环境变量示例(提交Git,指导配置)
+ ├── requirements.txt # 依赖包清单
+ ├── README.md # 项目说明(启动方式、配置说明、接口文档)
+ └── run.py # 项目启动脚本(简化启动命令)
+
+## 握手流程
+ 前端 后端
+ | |
+ |--- 建立 WebSocket 连接 ---->|
+ |<--- 连接接受(101状态码)---|
+ | |
+ |--- 发送身份信息(user_id+token)--->|
+ | |
+ |<--- 校验结果(成功/失败)---|
+ | |
+ |--- 发送业务数据(音频/文本)--->|
+ |<--- 推送业务结果(TTS/回复)---|
\ No newline at end of file
diff --git a/audio_ai_chat/audio_ai_chat/__init__.py b/audio_ai_chat/audio_ai_chat/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/audio_ai_chat/audio_ai_chat/__pycache__/__init__.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..2d4294d
Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/__pycache__/__init__.cpython-310.pyc differ
diff --git a/audio_ai_chat/audio_ai_chat/__pycache__/main.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/__pycache__/main.cpython-310.pyc
new file mode 100644
index 0000000..7271d9f
Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/__pycache__/main.cpython-310.pyc differ
diff --git a/python/WebSocketFrameHeader.py b/audio_ai_chat/audio_ai_chat/codec/ProtocolCodec.py
similarity index 88%
rename from python/WebSocketFrameHeader.py
rename to audio_ai_chat/audio_ai_chat/codec/ProtocolCodec.py
index 909f56c..de8b82e 100644
--- a/python/WebSocketFrameHeader.py
+++ b/audio_ai_chat/audio_ai_chat/codec/ProtocolCodec.py
@@ -5,7 +5,7 @@ import gzip
from typing import Optional, Union, Dict, Any, List, Tuple
-# -------------------------- 协议常量定义(与JS完全一致) --------------------------
+# -------------------------- 协议常量定义 --------------------------
class ProtocolConst:
PROTOCOL_VERSION = 0b0001 # 协议版本(4位,字节0低4位,0~15)
HEADER_SIZE = 8 # 头部固定字节数(字节0~7)
@@ -13,13 +13,14 @@ class ProtocolConst:
STRING_ENCODING = "utf-8" # 字符串默认编码
-# -------------------------- 枚举定义(与JS码值完全对齐) --------------------------
+# -------------------------- 枚举定义 --------------------------
class MessageType(IntEnum):
"""消息类型(4位,字节0高4位,0~15)"""
PING = 0b0001 # 心跳(支持空包体)
AUDIO_DATA = 0b0010 # 纯音频数据(pcm)
TEXT_MESSAGE = 0b0011 # 纯文本消息
- CONTROL_CMD = 0b0100 # 控制指令(暂停/继续等)
+ CONTROL_CMD = 0b0100 # 控制指令
+ IDENTITY = 0b0101 # 身份校验包
# 预留12种类型(0b0100 ~ 0b1111)
@@ -161,11 +162,11 @@ class ProtocolCodec:
return header + compressed_body
@staticmethod
- def unpack(packet: bytes) -> Tuple[MessageType, SerializationType, CompressionType, int, Any]:
+ def unpack(packet: bytes) -> Tuple[MessageType, int, Any]:
"""
解析协议包(与JS unpack 方法完全兼容)
:param packet: 完整协议包(头部 + 包体)
- :return: (消息类型, 序列化方式, 压缩方式, 顺序号, 原始包体数据)
+ :return: (消息类型, 顺序号, 原始包体数据) # 仅返回核心必要信息
"""
# 1. 校验包长度
if len(packet) < ProtocolConst.HEADER_SIZE:
@@ -193,7 +194,7 @@ class ProtocolCodec:
# 字节4~6:包体长度(24位大端序),字节7:保留位(忽略)
body_len = (header[4] << 16) | (header[5] << 8) | header[6]
- # 校验版本和包体长度
+ # 校验版本和包体长度(校验逻辑保留,确保数据有效性)
if version != ProtocolConst.PROTOCOL_VERSION:
raise ValueError(
f"协议版本不匹配:收到v{version}(0b{version:04b}),支持v{ProtocolConst.PROTOCOL_VERSION}(0b{ProtocolConst.PROTOCOL_VERSION:04b})"
@@ -236,16 +237,17 @@ class ProtocolCodec:
else:
raise ValueError(f"不支持的序列化方式:{serialization}")
- return msg_type, serialization, compression, sequence, original_body
+ # 仅返回核心必要信息:消息类型、顺序号、原始包体数据
+ return msg_type, sequence, original_body
-# -------------------------- 使用示例(验证与JS兼容性) --------------------------
+# -------------------------- 使用示例(验证修改后功能正常) --------------------------
if __name__ == "__main__":
# 示例1:PING消息(空包体,默认顺序号0)
ping_packet = ProtocolCodec.pack(MessageType.PING)
print(f"PING消息包长度:{len(ping_packet)}字节(仅头部)")
- msg_type1, ser1, comp1, seq1, body1 = ProtocolCodec.unpack(ping_packet)
- print(f"PING解析结果:类型={msg_type1.name},序列化={ser1.name},压缩={comp1.name},顺序号={seq1},包体={body1}\n")
+ msg_type1, seq1, body1 = ProtocolCodec.unpack(ping_packet)
+ print(f"PING解析结果:类型={msg_type1.name},顺序号={seq1},包体={body1}\n")
# 示例2:纯文本消息(STRING序列化,指定顺序号)
text_body = "Python与JS协议兼容测试(纯字符串)"
@@ -256,8 +258,8 @@ if __name__ == "__main__":
compression=CompressionType.NONE
)
print(f"文本消息包长度:{len(text_packet)}字节")
- msg_type2, ser2, comp2, seq2, body2 = ProtocolCodec.unpack(text_packet)
- print(f"文本解析结果:类型={msg_type2.name},序列化={ser2.name},顺序号={seq2},内容={body2}\n")
+ msg_type2, seq2, body2 = ProtocolCodec.unpack(text_packet)
+ print(f"文本解析结果:类型={msg_type2.name},顺序号={seq2},内容={body2}\n")
# 示例3:控制指令(JSON序列化)
control_body = {"cmd": ControlCommand.PAUSE.value, "reason": "用户主动暂停"}
@@ -267,8 +269,8 @@ if __name__ == "__main__":
sequence=1002
)
print(f"控制指令包长度:{len(control_packet)}字节")
- msg_type3, ser3, comp3, seq3, body3 = ProtocolCodec.unpack(control_packet)
- print(f"控制指令解析结果:类型={msg_type3.name},序列化={ser3.name},顺序号={seq3},内容={body3}\n")
+ msg_type3, seq3, body3 = ProtocolCodec.unpack(control_packet)
+ print(f"控制指令解析结果:类型={msg_type3.name},顺序号={seq3},内容={body3}\n")
# 示例4:音频数据(RAW序列化)
audio_body = b"\x00\x01\x02\x03\x04\x05" * 100 # 模拟PCM数据
@@ -278,8 +280,8 @@ if __name__ == "__main__":
sequence=1003
)
print(f"音频数据包长度:{len(audio_packet)}字节")
- msg_type4, ser4, comp4, seq4, body4 = ProtocolCodec.unpack(audio_packet)
- print(f"音频解析结果:类型={msg_type4.name},序列化={ser4.name},顺序号={seq4},数据长度={len(body4)}字节\n")
+ msg_type4, seq4, body4 = ProtocolCodec.unpack(audio_packet)
+ print(f"音频解析结果:类型={msg_type4.name},顺序号={seq4},数据长度={len(body4)}字节\n")
# 示例5:GZIP压缩测试(需JS端启用GZIP解压)
long_text_body = "这是一段很长的文本,用于测试GZIP压缩效果" * 100
@@ -290,5 +292,5 @@ if __name__ == "__main__":
compression=CompressionType.GZIP
)
print(f"GZIP压缩后包长度:{len(gzip_packet)}字节(原始文本长度:{len(long_text_body.encode())}字节)")
- msg_type5, ser5, comp5, seq5, body5 = ProtocolCodec.unpack(gzip_packet)
- print(f"GZIP解析结果:类型={msg_type5.name},压缩={comp5.name},内容前50字:{body5[:50]}...")
\ No newline at end of file
+ msg_type5, seq5, body5 = ProtocolCodec.unpack(gzip_packet)
+ print(f"GZIP解析结果:类型={msg_type5.name},顺序号={seq5},内容前50字:{body5[:50]}...")
\ No newline at end of file
diff --git a/audio_ai_chat/audio_ai_chat/codec/__init__.py b/audio_ai_chat/audio_ai_chat/codec/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/audio_ai_chat/audio_ai_chat/codec/__pycache__/ProtocolCodec.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/codec/__pycache__/ProtocolCodec.cpython-310.pyc
new file mode 100644
index 0000000..94f60fc
Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/codec/__pycache__/ProtocolCodec.cpython-310.pyc differ
diff --git a/audio_ai_chat/audio_ai_chat/codec/__pycache__/__init__.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/codec/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..2e10ddf
Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/codec/__pycache__/__init__.cpython-310.pyc differ
diff --git a/audio_ai_chat/audio_ai_chat/config/__init__.py b/audio_ai_chat/audio_ai_chat/config/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/audio_ai_chat/audio_ai_chat/config/__pycache__/__init__.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/config/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..4a3dac3
Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/config/__pycache__/__init__.cpython-310.pyc differ
diff --git a/audio_ai_chat/audio_ai_chat/config/__pycache__/logger.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/config/__pycache__/logger.cpython-310.pyc
new file mode 100644
index 0000000..3874682
Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/config/__pycache__/logger.cpython-310.pyc differ
diff --git a/audio_ai_chat/audio_ai_chat/config/__pycache__/settings.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/config/__pycache__/settings.cpython-310.pyc
new file mode 100644
index 0000000..caedb7c
Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/config/__pycache__/settings.cpython-310.pyc differ
diff --git a/audio_ai_chat/audio_ai_chat/config/logger.py b/audio_ai_chat/audio_ai_chat/config/logger.py
new file mode 100644
index 0000000..77f2653
--- /dev/null
+++ b/audio_ai_chat/audio_ai_chat/config/logger.py
@@ -0,0 +1,28 @@
+from loguru import logger
+import sys
+from audio_ai_chat.config.settings import settings
+
+# 移除默认日志输出
+logger.remove()
+
+# 添加控制台输出(开发环境)
+logger.add(
+ sink=sys.stdout,
+ level=settings.LOG_LEVEL,
+ format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} - {message}",
+ enqueue=True, # 异步日志,提高性能
+)
+
+# 添加文件输出(按大小滚动,保留10个文件,每个文件最大50MB)
+logger.add(
+ sink=settings.LOG_FILE,
+ level=settings.LOG_LEVEL,
+ format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} - {message}",
+ rotation="50 MB",
+ retention=10,
+ compression="zip",
+ enqueue=True,
+)
+
+# 导出logger供其他模块使用
+__all__ = ["logger"]
\ No newline at end of file
diff --git a/audio_ai_chat/audio_ai_chat/config/settings.py b/audio_ai_chat/audio_ai_chat/config/settings.py
new file mode 100644
index 0000000..b99fa65
--- /dev/null
+++ b/audio_ai_chat/audio_ai_chat/config/settings.py
@@ -0,0 +1,70 @@
+from pydantic_settings import BaseSettings, SettingsConfigDict
+from pydantic import Field
+from pathlib import Path
+
+ROOT_DIR = Path(__file__).parent.parent.parent
+
+class Settings(BaseSettings):
+ # 应用配置
+ APP_PORT: int = Field(default=8000, description="服务端口")
+ LOG_LEVEL: str = Field(default="INFO", description="日志级别")
+ LOG_FILE: Path = Field(default=ROOT_DIR / "logs/app.log", description="日志文件路径")
+
+ # ASR服务配置
+ ASR_SERVICE_URL: str = Field(..., description="ASR服务地址")
+ ASR_TIMEOUT: int = Field(default=30, description="ASR超时时间(秒)")
+ ASR_RETRY_TIMES: int = Field(default=2, description="ASR重试次数")
+
+ # LLM服务配置
+ LLM_SERVICE_URL: str = Field(..., description="LLM服务地址")
+ LLM_TIMEOUT: int = Field(default=60, description="LLM超时时间(秒)")
+ LLM_RETRY_TIMES: int = Field(default=2, description="LLM重试次数")
+ LLM_MODEL: str = Field(default="default", description="LLM模型版本")
+
+ # TTS服务配置
+ TTS_SERVICE_URL: str = Field(..., description="TTS服务地址")
+ TTS_TIMEOUT: int = Field(default=30, description="TTS超时时间(秒)")
+ TTS_RETRY_TIMES: int = Field(default=2, description="TTS重试次数")
+ TTS_VOICE: str = Field(default="default", description="TTS语音类型")
+
+ # WebSocket配置
+ WS_MAX_SIZE: int = Field(default=10 * 1024 * 1024, description="WS最大消息大小(字节)")
+ WS_PING_INTERVAL: int = Field(default=30, description="WS心跳间隔(秒)")
+ WS_PING_TIMEOUT: int = Field(default=10, description="WS心跳超时(秒)")
+
+ # 加密配置
+ ENCRYPT_KEY: str = Field(..., description="数据加密密钥")
+ ENCRYPT_IV: str = Field(..., description="数据加密向量")
+
+ # -------------------------- 新增:服务版本配置 --------------------------
+ # ASR当前使用版本(对应ASR_REGISTRY中的key)
+ ASR_CURRENT_VERSION: str = Field(default="local_v1", description="ASR服务当前版本")
+ # 本地ASR专属配置(仅local_v1版本使用)
+ LOCAL_ASR_MODEL_PATH: Path = Field(default=ROOT_DIR / "models/asr/local_model", description="本地ASR模型路径")
+ # 百度云ASR专属配置(仅baidu_v2版本使用)
+ BAIDU_ASR_API_KEY: str = Field(default="", description="百度云ASR API Key")
+ BAIDU_ASR_SECRET_KEY: str = Field(default="", description="百度云ASR Secret Key")
+
+ # LLM当前使用版本(对应LLM_REGISTRY中的key)
+ LLM_CURRENT_VERSION: str = Field(default="local", description="LLM服务当前版本")
+ # OpenAI LLM专属配置(仅openai版本使用)
+ OPENAI_API_KEY: str = Field(default="", description="OpenAI API Key")
+ OPENAI_BASE_URL: str = Field(default="https://api.openai.com/v1", description="OpenAI接口地址")
+ # 本地LLM专属配置(仅local版本使用)
+ LOCAL_LLM_MODEL_PATH: Path = Field(default=ROOT_DIR / "models/llm/local_model", description="本地LLM模型路径")
+
+ # TTS当前使用版本(对应TTS_REGISTRY中的key)
+ TTS_CURRENT_VERSION: str = Field(default="pyttsx3", description="TTS服务当前版本")
+ # 阿里云TTS专属配置(仅aliyun版本使用)
+ ALI_TTS_ACCESS_KEY: str = Field(default="", description="阿里云TTS Access Key")
+ ALI_TTS_ACCESS_SECRET: str = Field(default="", description="阿里云TTS Access Secret")
+ ALI_TTS_REGION_ID: str = Field(default="cn-hangzhou", description="阿里云TTS区域ID")
+
+ # 原有配置...(WS_MAX_SIZE、ENCRYPT_KEY等)
+
+ model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
+
+settings = Settings()
+# 确保模型目录存在(本地版本需要)
+# settings.LOCAL_ASR_MODEL_PATH.parent.mkdir(parents=True, exist_ok=True)
+# settings.LOCAL_LLM_MODEL_PATH.parent.mkdir(parents=True, exist_ok=True)
\ No newline at end of file
diff --git a/audio_ai_chat/audio_ai_chat/core/__init__.py b/audio_ai_chat/audio_ai_chat/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/audio_ai_chat/audio_ai_chat/core/__pycache__/__init__.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..f7ab12e
Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/core/__pycache__/__init__.cpython-310.pyc differ
diff --git a/audio_ai_chat/audio_ai_chat/core/__pycache__/connection.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/__pycache__/connection.cpython-310.pyc
new file mode 100644
index 0000000..4a03bd7
Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/core/__pycache__/connection.cpython-310.pyc differ
diff --git a/audio_ai_chat/audio_ai_chat/core/__pycache__/websocket_handler.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/__pycache__/websocket_handler.cpython-310.pyc
new file mode 100644
index 0000000..94e62b8
Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/core/__pycache__/websocket_handler.cpython-310.pyc differ
diff --git a/audio_ai_chat/audio_ai_chat/core/asr/__init__.py b/audio_ai_chat/audio_ai_chat/core/asr/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/__init__.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..331d32e
Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/__init__.cpython-310.pyc differ
diff --git a/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/base.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/base.cpython-310.pyc
new file mode 100644
index 0000000..3dce723
Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/base.cpython-310.pyc differ
diff --git a/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/factory.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/factory.cpython-310.pyc
new file mode 100644
index 0000000..cd9c8db
Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/core/asr/__pycache__/factory.cpython-310.pyc differ
diff --git a/audio_ai_chat/audio_ai_chat/core/asr/base.py b/audio_ai_chat/audio_ai_chat/core/asr/base.py
new file mode 100644
index 0000000..5f941cc
--- /dev/null
+++ b/audio_ai_chat/audio_ai_chat/core/asr/base.py
@@ -0,0 +1,25 @@
+from abc import ABC, abstractmethod
+from typing import Optional, Coroutine
+from audio_ai_chat.config.settings import settings
+
+class ASRBase(ABC):
+ """ASR服务统一抽象接口"""
+ def __init__(self):
+ # 公共配置(所有ASR版本共享的超时、重试次数等)
+ self.timeout = settings.ASR_TIMEOUT
+ self.retry_times = settings.ASR_RETRY_TIMES
+
+ @abstractmethod
+ async def recognize(
+ self,
+ voice_data: bytes,
+ user_id: Optional[str] = None,
+ **kwargs # 兼容不同版本的额外参数
+ ) -> str:
+ """
+ 语音识别核心方法(所有ASR版本必须实现)
+ :param voice_data: 语音二进制数据
+ :param user_id: 用户ID(可选)
+ :return: 识别后的文本
+ """
+ pass
\ No newline at end of file
diff --git a/audio_ai_chat/audio_ai_chat/core/asr/factory.py b/audio_ai_chat/audio_ai_chat/core/asr/factory.py
new file mode 100644
index 0000000..7e52290
--- /dev/null
+++ b/audio_ai_chat/audio_ai_chat/core/asr/factory.py
@@ -0,0 +1,26 @@
+from typing import Type
+from audio_ai_chat.config.settings import settings
+from audio_ai_chat.utils.exceptions import ServiceCallError
+from .base import ASRBase
+# from .version1 import LocalOfflineASR
+# from .version2 import BaiduASR
+#
+# # 注册所有ASR版本:key=配置中的版本名,value=对应的类
+# ASR_REGISTRY: dict[str, Type[ASRBase]] = {
+# "local_v1": LocalOfflineASR,
+# "baidu_v2": BaiduASR,
+# # 新增版本时,只需在这里注册:"新版本名": 新类名
+# }
+
+# class ASRFactory:
+# """ASR服务工厂类:根据配置创建对应版本的实例"""
+# @staticmethod
+# def get_asr_client() -> ASRBase:
+# # 从配置中获取当前指定的ASR版本
+# current_version = settings.ASR_CURRENT_VERSION
+# if current_version not in ASR_REGISTRY:
+# raise ServiceCallError(
+# f"不支持的ASR版本:{current_version},可选版本:{list(ASR_REGISTRY.keys())}"
+# )
+# # 创建并返回对应版本的实例
+# return ASR_REGISTRY[current_version]()
\ No newline at end of file
diff --git a/audio_ai_chat/audio_ai_chat/core/connection.py b/audio_ai_chat/audio_ai_chat/core/connection.py
new file mode 100644
index 0000000..f2e0098
--- /dev/null
+++ b/audio_ai_chat/audio_ai_chat/core/connection.py
@@ -0,0 +1,145 @@
+from typing import Optional, Dict, Any, List
+import asyncio
+from audio_ai_chat.config.logger import logger
+# from audio_ai_chat.core.llm.factory import LLMFactory
+from audio_ai_chat.core.llm.base import LLMBase
+
+
+class ConnectionContext:
+ """
+ 单个WebSocket连接的上下文管理器:封装用户信息、大模型Session、消息队列等资源
+ 每个WebSocket连接对应一个该类实例,确保资源隔离
+ """
+
+ def __init__(self, client_id: str):
+ """
+ 初始化连接上下文
+ :param client_id: WebSocket连接唯一标识(如id(websocket))
+ :param user_id: 用户唯一标识(从前端请求中获取)
+ """
+
+ self.client_id = client_id # 连接唯一ID
+ self.created_at = asyncio.get_event_loop().time() # 连接创建时间
+
+ # 1. 大模型独立Session(每个连接创建一个新的LLM客户端实例)
+ # self.llm_session: LLMBase = LLMFactory.get_llm_client() # 独立Session
+ self.chat_history: List[Dict[str, str]] = [] # 该连接的对话历史([(user: "...", assistant: "..."), ...])
+
+ # 2. 异步消息队列(用于缓存TTS结果,有序推送给前端)
+ self.message_queue: asyncio.Queue[bytes] = asyncio.Queue()
+
+ # 3. 连接状态(可选:如是否正在处理请求、是否断开等)
+ self.is_active: bool = True
+ self.is_processing: bool = False
+ self.name = None
+ self.user_id = None
+ self.token = None
+
+ def set_user_info(self, token: str, user_id: str, name: str = "匿名用户"):
+ """
+ 二次设置用户信息(身份校验通过后调用)
+ :param token:
+ :param user_id: 用户唯一标识(必填)
+ :param name: 用户名(可选,默认匿名)
+ """
+ if not user_id:
+ raise ValueError("user_id不能为空")
+ self.name = name
+ self.user_id = user_id
+ self.token = token
+ logger.debug(f"客户端 {self.client_id} 设置用户信息:user_id={user_id}, name={name}")
+
+ async def add_message_to_queue(self, message: bytes):
+ """将TTS结果添加到消息队列(异步安全)"""
+ if not self.is_active:
+ raise ValueError(f"连接已断开(client_id={self.client_id}),无法添加消息")
+ await self.message_queue.put(message)
+ logger.debug(f"消息队列添加数据:client_id={self.client_id},队列长度={self.message_queue.qsize()}")
+
+ async def get_message_from_queue(self) -> Optional[bytes]:
+ """从消息队列获取消息(异步阻塞,直到有消息或连接断开)"""
+ try:
+ # 超时时间:30秒(避免无限阻塞)
+ return await asyncio.wait_for(self.message_queue.get(), timeout=30.0)
+ except asyncio.TimeoutError:
+ logger.debug(f"消息队列超时:client_id={self.client_id},无新消息")
+ return None
+
+ def update_chat_history(self, user_text: str, assistant_text: str):
+ """更新该连接的对话历史"""
+ self.chat_history.append({
+ "user": user_text,
+ "assistant": assistant_text
+ })
+ # 可选:限制历史长度(避免内存溢出)
+ if len(self.chat_history) > 50:
+ self.chat_history.pop(0) # 删除最早的历史
+
+ def close(self):
+ """关闭连接上下文,释放资源"""
+ self.is_active = False
+ self.is_processing = False
+ # 清空消息队列(可选)
+ while not self.message_queue.empty():
+ try:
+ self.message_queue.get_nowait()
+ except asyncio.QueueEmpty:
+ break
+ logger.info(f"连接上下文已关闭:client_id={self.client_id},user_id={self.user_id}")
+
+ def __del__(self):
+ """析构函数:确保资源释放"""
+ self.close()
+
+
+class ConnectionManager:
+ """
+ WebSocket连接全局管理器:维护所有活跃连接的上下文
+ 提供创建、查询、删除连接上下文的接口(线程/异步安全)
+ """
+
+ def __init__(self):
+ # 存储所有活跃连接:key=client_id(int),value=ConnectionContext实例
+ self.connections: Dict[int, ConnectionContext] = {}
+ # 异步锁:确保多连接并发操作时的数据安全
+ self._lock = asyncio.Lock()
+
+ async def create_connection(self, client_id: int, user_id: Optional[str] = None) -> ConnectionContext:
+ """创建新的连接上下文(线程安全)"""
+ async with self._lock:
+ # 避免重复创建(同一client_id不会重复连接)
+ if client_id in self.connections:
+ logger.warning(f"连接已存在:client_id={client_id},将覆盖旧连接")
+ self.connections[client_id].close()
+
+ # 创建新的连接上下文(包含独立LLM Session和消息队列)
+ context = ConnectionContext(client_id=client_id, user_id=user_id)
+ self.connections[client_id] = context
+ logger.info(
+ f"创建新连接上下文:client_id={client_id},user_id={user_id},当前活跃连接数={len(self.connections)}")
+ return context
+
+ async def get_connection(self, client_id: int) -> Optional[ConnectionContext]:
+ """获取指定client_id的连接上下文(线程安全)"""
+ async with self._lock:
+ context = self.connections.get(client_id)
+ if context and not context.is_active:
+ # 清理已断开的连接
+ del self.connections[client_id]
+ return None
+ return context
+
+ async def remove_connection(self, client_id: int):
+ """删除连接上下文(线程安全)"""
+ async with self._lock:
+ context = self.connections.pop(client_id, None)
+ if context:
+ context.close()
+ logger.info(f"移除连接上下文:client_id={client_id},当前活跃连接数={len(self.connections)}")
+
+ async def get_active_connections_count(self) -> int:
+ """获取当前活跃连接数(线程安全)"""
+ async with self._lock:
+ # 过滤已断开的连接
+ self.connections = {k: v for k, v in self.connections.items() if v.is_active}
+ return len(self.connections)
\ No newline at end of file
diff --git a/audio_ai_chat/audio_ai_chat/core/llm/__init__.py b/audio_ai_chat/audio_ai_chat/core/llm/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/__init__.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..c2daf54
Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/__init__.cpython-310.pyc differ
diff --git a/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/base.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/base.cpython-310.pyc
new file mode 100644
index 0000000..ab938f1
Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/base.cpython-310.pyc differ
diff --git a/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/factory.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/factory.cpython-310.pyc
new file mode 100644
index 0000000..6f0a486
Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/core/llm/__pycache__/factory.cpython-310.pyc differ
diff --git a/audio_ai_chat/audio_ai_chat/core/llm/base.py b/audio_ai_chat/audio_ai_chat/core/llm/base.py
new file mode 100644
index 0000000..48087c4
--- /dev/null
+++ b/audio_ai_chat/audio_ai_chat/core/llm/base.py
@@ -0,0 +1,26 @@
+from abc import ABC, abstractmethod
+from typing import Optional, Coroutine
+
+class LLMBase(ABC):
+ """LLM服务统一抽象接口"""
+ def __init__(self):
+ self.timeout = settings.LLM_TIMEOUT
+ self.retry_times = settings.LLM_RETRY_TIMES
+ self.model = settings.LLM_MODEL # 模型版本(不同LLM可能支持不同模型)
+
+ @abstractmethod
+ async def chat(
+ self,
+ text: str,
+ user_id: Optional[str] = None,
+ history: Optional[list] = None, # 对话历史(部分LLM支持)
+ **kwargs
+ ) -> str:
+ """
+ 大模型对话核心方法
+ :param text: 用户输入文本(ASR识别结果)
+ :param user_id: 用户ID(可选)
+ :param history: 对话历史(可选,格式:[(用户输入, 模型回答), ...])
+ :return: 模型生成的回答文本
+ """
+ pass
\ No newline at end of file
diff --git a/audio_ai_chat/audio_ai_chat/core/llm/factory.py b/audio_ai_chat/audio_ai_chat/core/llm/factory.py
new file mode 100644
index 0000000..548e2c3
--- /dev/null
+++ b/audio_ai_chat/audio_ai_chat/core/llm/factory.py
@@ -0,0 +1,22 @@
+from typing import Type
+from audio_ai_chat.config.settings import settings
+from audio_ai_chat.utils.exceptions import ServiceCallError
+from .base import LLMBase
+#
+# from .openai_llm import OpenAILLM
+# from .local_llm import LocalLLM
+#
+# LLM_REGISTRY: dict[str, Type[LLMBase]] = {
+# "openai": OpenAILLM,
+# "local": LocalLLM,
+# }
+#
+# class LLMFactory:
+# @staticmethod
+# def get_llm_client() -> LLMBase:
+# current_version = settings.LLM_CURRENT_VERSION
+# if current_version not in LLM_REGISTRY:
+# raise ServiceCallError(
+# f"不支持的LLM版本:{current_version},可选版本:{list(LLM_REGISTRY.keys())}"
+# )
+# return LLM_REGISTRY[current_version]()
\ No newline at end of file
diff --git a/audio_ai_chat/audio_ai_chat/core/tts/__init__.py b/audio_ai_chat/audio_ai_chat/core/tts/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/audio_ai_chat/audio_ai_chat/core/tts/base.py b/audio_ai_chat/audio_ai_chat/core/tts/base.py
new file mode 100644
index 0000000..9721b67
--- /dev/null
+++ b/audio_ai_chat/audio_ai_chat/core/tts/base.py
@@ -0,0 +1,26 @@
+from abc import ABC, abstractmethod
+from typing import Optional, Coroutine
+
+class TTSBase(ABC):
+ """TTS服务统一抽象接口"""
+ def __init__(self):
+ self.timeout = settings.TTS_TIMEOUT
+ self.retry_times = settings.TTS_RETRY_TIMES
+ self.voice = settings.TTS_VOICE # 语音类型
+
+ @abstractmethod
+ async def synthesize(
+ self,
+ text: str,
+ user_id: Optional[str] = None,
+ speed: float = 1.0, # 语速(默认1.0)
+ **kwargs
+ ) -> bytes:
+ """
+ 文本转语音核心方法
+ :param text: 待合成文本(LLM回答结果)
+ :param user_id: 用户ID(可选)
+ :param speed: 语速(0.5~2.0)
+ :return: 语音二进制数据
+ """
+ pass
\ No newline at end of file
diff --git a/audio_ai_chat/audio_ai_chat/core/tts/factory.py b/audio_ai_chat/audio_ai_chat/core/tts/factory.py
new file mode 100644
index 0000000..b381fdb
--- /dev/null
+++ b/audio_ai_chat/audio_ai_chat/core/tts/factory.py
@@ -0,0 +1,22 @@
+from typing import Type
+from audio_ai_chat.config.settings import settings
+from audio_ai_chat.utils.exceptions import ServiceCallError
+from .base import TTSBase
+#
+# from .ali_tts import AliTTS
+# from .pyttsx3_tts import Pyttsx3TTS
+#
+# TTS_REGISTRY: dict[str, Type[TTSBase]] = {
+# "aliyun": AliTTS,
+# "pyttsx3": Pyttsx3TTS,
+# }
+#
+# class TTSFactory:
+# @staticmethod
+# def get_tts_client() -> TTSBase:
+# current_version = settings.TTS_CURRENT_VERSION
+# if current_version not in TTS_REGISTRY:
+# raise ServiceCallError(
+# f"不支持的TTS版本:{current_version},可选版本:{list(TTS_REGISTRY.keys())}"
+# )
+# return TTS_REGISTRY[current_version]()
\ No newline at end of file
diff --git a/audio_ai_chat/audio_ai_chat/core/websocket_handler.py b/audio_ai_chat/audio_ai_chat/core/websocket_handler.py
new file mode 100644
index 0000000..cd8b4a4
--- /dev/null
+++ b/audio_ai_chat/audio_ai_chat/core/websocket_handler.py
@@ -0,0 +1,302 @@
+from fastapi import WebSocket
+from typing import Dict, List, Optional
+
+from pyexpat.errors import messages
+
+from audio_ai_chat.config.logger import logger
+# from audio_ai_chat.core.asr.factory import ASRFactory # 导入ASR工厂
+# from audio_ai_chat.core.llm.factory import LLMFactory # 导入LLM工厂
+# from audio_ai_chat.core.tts.factory import TTSFactory # 导入TTS工厂
+from audio_ai_chat.config.settings import settings
+from audio_ai_chat.utils.exceptions import ServiceCallError
+from fastapi import WebSocket, WebSocketDisconnect
+from audio_ai_chat.codec.ProtocolCodec import ProtocolCodec
+from typing import Optional, Dict, Callable, Awaitable, List, Any, Coroutine
+from dataclasses import dataclass, field
+import sys
+import json
+import asyncio
+import websockets
+import uuid
+import logging
+from audio_ai_chat.core.connection import ConnectionContext
+from audio_ai_chat.codec.ProtocolCodec import ProtocolCodec, MessageType
+
+
+class WebSocketConnectionManager:
+ """WebSocket连接管理器"""
+
+ def __init__(self):
+ # 活跃连接列表
+ self.active_connections: List[WebSocket] = []
+ # 关键映射:client_id -> ConnectionContext(快速获取用户专属上下文)
+ self.client_context_map: Dict[str, ConnectionContext] = {}
+ # self.asr_client = ASRFactory.get_asr_client()
+ # self.tts_client = TTSFactory.get_tts_client()
+ # 用户LLM会话存储
+ # self.user_llm_conversations: Dict[str, LLMConversation] = {}
+ # 全局唤醒事件
+ self.consume_wakeup = asyncio.Event()
+
+ async def connect(self, websocket: WebSocket) -> ConnectionContext:
+ """
+ 建立连接+身份校验(前端主动发送身份信息)
+ 超时逻辑:5秒内未收到前端身份信息,自动关闭连接
+ """
+ # 1. 接受连接
+ await websocket.accept()
+ self.active_connections.append(websocket)
+ # 生成唯一标识
+ client_id = str(id(websocket))
+ context = ConnectionContext(
+ client_id=client_id
+ )
+ logger.info(
+ f"连接 {client_id} 已接受,等待前端发送身份信息(5秒超时)...,当前连接数: {len(self.active_connections)}")
+
+ try:
+ # 2. 超时控制:5秒内未收到身份信息,关闭连接
+ try:
+ # 关键:用 receive_bytes 接收二进制包(而非 receive_json)
+ ping_packet = await asyncio.wait_for(
+ websocket.receive_bytes(),
+ timeout=5.0 # 身份校验超时时间
+ )
+ except asyncio.TimeoutError:
+ # 发送超时错误响应(二进制格式)
+ # todo 告知前端失败
+ # await websocket.close(code=1008) # todo 错误码值待定
+ # self.active_connections.remove(websocket)
+ raise TimeoutError(f"连接 {client_id} 身份包超时")
+
+ # 3. 解包二进制包
+ msg_type, _, body = ProtocolCodec.unpack(ping_packet)
+ if msg_type != MessageType.IDENTITY:
+ raise ValueError(f"连接 {client_id} 首个包类型错误(期望1,实际{MessageType.IDENTITY})")
+
+ logger.debug(f"解包成功:, body={body}")
+ # 4. 提取并校验身份信息(核心:按需扩展校验逻辑)
+ identity_data = json.loads(body.decode("utf-8"))
+ user_id = identity_data.get("user_id")
+ token = identity_data.get("token")
+ name = identity_data.get("name")
+ # todo 校验身份信息 raise PermissionError(f"用户 {user_id} 身份校验失败")
+ context.set_user_info(token, user_id, name)
+ self.client_context_map[client_id] = context
+ # 5. 响应前端:校验成功
+ await context.add_message_to_queue(
+ message=ProtocolCodec.pack(MessageType.IDENTITY, {'messages': '身份校验成功,连接已就绪'}))
+
+ logger.info(f"用户 {user_id}({name})身份校验通过,连接就绪(client_id: {client_id})")
+ return context
+ except:
+ pass
+
+ # 初始化LLM会话
+ # self._init_llm_conversation(user_id)
+
+ # return conn_id, user_id, conn_id # conn_id 同时作为 tts_session_id
+
+ # def _init_llm_conversation(self, user_id: str):
+ # """初始化用户LLM会话"""
+ # if user_id not in self.user_llm_conversations:
+ # self.user_llm_conversations[user_id] = LLMConversation(
+ # user_id=user_id,
+ # scene_description="语音识别对话场景"
+ # )
+
+ def disconnect(self, websocket: WebSocket, conn_id: str):
+ """断开连接并清理资源"""
+ if websocket in self.active_connections:
+ self.active_connections.remove(websocket)
+ logger.info(f"连接 {conn_id} 已断开,当前连接数: {len(self.active_connections)}")
+
+ # async def setup_tts_manager(self, result_queue: asyncio.Queue) -> TTSManager:
+ # """初始化TTS管理器"""
+ #
+ # def handle_tts_result(req_id: str, result: Dict[str, Any]):
+ # """TTS结果回调处理"""
+ # try:
+ # status = result.get("status")
+ # if status == "completed":
+ # audio_data = result.get("audio_data")
+ # if audio_data is not None and len(audio_data) > 0:
+ # # 转换为PCM格式
+ # pcm_data = (audio_data.astype(np.float32) * 32767).astype(np.int16)
+ # pcm_bytes = pcm_data.tobytes()
+ # result_queue.put_nowait(pcm_bytes)
+ # except Exception as e:
+ # logger.error(f"TTS结果处理失败: {str(e)}")
+ #
+ # self.tts_client = TTSFactory.get_tts_client()
+ # tts_manager = TTSManager()
+ # tts_manager.set_result_callback(handle_tts_result)
+ # tts_manager.set_playback_enabled(False)
+ # await tts_manager.initialize()
+ # return tts_manager
+
+ async def asr_result_callback(self, result: dict, websocket: WebSocket,
+ user_id: str, result_queue: asyncio.Queue):
+ """ASR结果回调处理"""
+ try:
+ logger.info(f"ASR识别结果: {result}")
+ final_asr_text = result.get("text", "").strip()
+
+ # 转发ASR结果到前端队列
+ if final_asr_text:
+ print(f"插入ASR结果时队列大小: {result_queue.qsize()}")
+ self.consume_wakeup.set() # 唤醒消费协程
+
+ # 异步调用大模型
+ llm_conversation = self.user_llm_conversations.get(user_id)
+ if llm_conversation:
+ asyncio.create_task(
+ self.call_llm_and_send(
+ query=final_asr_text,
+ conversation=llm_conversation,
+ websocket=websocket
+ )
+ )
+ except Exception as e:
+ logger.error(f"ASR回调执行失败: {str(e)}")
+
+ # async def llm_stream_callback(self, chunk: str, tts_manager: TTSManager):
+ # """大模型流式回调处理"""
+ # if not chunk:
+ # return
+ # try:
+ # # 提交TTS合成请求
+ # await tts_manager.synthesize(chunk)
+ # await asyncio.sleep(0) # 让出调度权
+ # except Exception as e:
+ # logger.error(f"LLM流式回调处理失败: {str(e)}")
+
+ # async def call_llm_and_send(self, query: str, conversation: LLMConversation, websocket: WebSocket):
+ # """调用大模型并处理结果"""
+ # logger.info(f"调用大模型 - 用户({conversation.user_id}): {query}")
+ # try:
+ # conv_id, full_reply = await llm_client.send_message(
+ # query=query,
+ # conversation=conversation,
+ # stream_callback=self.llm_stream_callback,
+ # response_mode="streaming"
+ # )
+ # logger.info(f"大模型回复完成 - 会话ID: {conv_id}, 完整回复: {full_reply}")
+ # except Exception as e:
+ # logger.error(f"大模型调用失败: {str(e)}")
+ # if not websocket.client_state.disconnected:
+ # await websocket.send_json({
+ # "type": "llm_error",
+ # "data": {"error": str(e)}
+ # })
+
+ async def recv_frontend_data(self, websocket: WebSocket, asr_conn):
+ """接收前端音频数据并推送到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:
+ # logger.warning("音频数据插入ASR失败(队列满/连接失效)")
+ except WebSocketDisconnect:
+ logger.info("前端主动断开连接")
+ asr_conn.stop_event.set()
+ break
+ except Exception as e:
+ logger.error(f"接收前端数据失败: {str(e)}")
+ # asr_conn.stop_event.set()
+ # if not websocket.client_state.disconnected:
+ # await websocket.send_json({"error": f"接收数据失败: {str(e)}"})
+ # break
+
+ async def send_results(self, websocket: WebSocket, result_queue: asyncio.Queue, asr_conn):
+ """从结果队列发送数据到前端"""
+ while True:
+ try:
+ # 等待队列数据或超时
+ result = await asyncio.wait_for(result_queue.get(), timeout=0.05)
+ if not websocket.client_state.disconnected:
+ await websocket.send_bytes(result)
+ except asyncio.TimeoutError:
+ if asr_conn.stop_event.is_set():
+ break
+ continue
+ except Exception as e:
+ logger.error(f"发送结果到前端失败: {str(e)}")
+ asr_conn.stop_event.set()
+ break
+
+ async def handle_connection(self, websocket: WebSocket):
+ """处理单个WebSocket连接的完整生命周期"""
+
+ tts_manager = None
+ asr_conn = None
+ asr_task = None
+ result_queue = asyncio.Queue(maxsize=10000)
+
+ context = await self.connect(websocket)
+ print(context)
+
+ try:
+ pass
+ # 1. 建立连接
+
+ # 2. 初始化TTS
+ # tts_manager = await self.setup_tts_manager(result_queue)
+
+ # 3. 获取ASR连接
+ # asr_conn = await get_idle_asr_connection()
+ # if not asr_conn:
+ # await websocket.send_json({"error": "ASR服务暂时不可用", "text": ""})
+ # return
+
+ # 4. 启动ASR通信协程
+ # asr_callback = lambda res: self.asr_result_callback(res, websocket, user_id, result_queue)
+ # asr_task = asyncio.create_task(handle_asr_communication(asr_conn, asr_callback))
+ #
+ # # 5. 启动数据接收和发送协程
+ # task_recv = asyncio.create_task(self.recv_frontend_data(websocket, asr_conn))
+ # task_send = asyncio.create_task(self.send_results(websocket, result_queue, asr_conn))
+ #
+ # # 6. 等待任一任务完成
+ # done, pending = await asyncio.wait(
+ # [task_recv, task_send],
+ # return_when=asyncio.FIRST_COMPLETED
+ # )
+
+ except Exception as e:
+ logger.error(f"WebSocket连接处理异常: {str(e)}")
+ if asr_conn:
+ asr_conn.stop_event.set()
+ if not websocket.client_state.disconnected:
+ await websocket.send_json({"error": str(e)})
+ finally:
+ pass
+ # 7. 资源清理
+ # logger.info(f"开始清理连接 {conn_id} 的资源")
+ # # 停止ASR
+ # if asr_conn:
+ # asr_conn.stop_event.set()
+ #
+ # # 取消任务
+ # if asr_task and not asr_task.done():
+ # asr_task.cancel()
+ # try:
+ # await asr_task
+ # except asyncio.CancelledError:
+ # pass
+ #
+ # # 清理TTS
+ # if tts_manager:
+ # await tts_manager.cleanup() # 假设TTSManager有cleanup方法,无则忽略
+ #
+ # # 断开连接
+ # if websocket:
+ # self.disconnect(websocket, conn_id)
+ # try:
+ # await websocket.close()
+ # except Exception:
+ # pass
+ #
+ # logger.info(f"连接 {conn_id} 资源清理完成")
diff --git a/audio_ai_chat/audio_ai_chat/main.py b/audio_ai_chat/audio_ai_chat/main.py
new file mode 100644
index 0000000..eb9b63f
--- /dev/null
+++ b/audio_ai_chat/audio_ai_chat/main.py
@@ -0,0 +1,56 @@
+from fastapi import FastAPI, WebSocket, WebSocketDisconnect
+from fastapi.middleware.cors import CORSMiddleware
+from audio_ai_chat.config.settings import settings
+from audio_ai_chat.config.logger import logger
+from audio_ai_chat.core.websocket_handler import WebSocketConnectionManager
+# from audio_ai_chat.models.ws_models import WSRequest, WSResponse, WSError
+from audio_ai_chat.codec.ProtocolCodec import ProtocolCodec # 已有加密类
+from audio_ai_chat.utils.exceptions import CodecError, ServiceCallError
+from contextlib import asynccontextmanager
+from frontend_ws import frontend_websocket_handler
+
+# FastAPI 启动时初始化 ASR 连接池
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ # 启动时执行(原 startup 逻辑)
+ print(' FastAPI 启动时初始化 ASR 连接池')
+ # await init_asr_pool()
+ yield # 应用运行中
+ # 关闭时执行(可选,比如清理连接池)
+ print("应用关闭,开始清理 ASR 连接池...")
+ # await close_asr_pool()
+
+
+app = FastAPI(
+ title="语音AI对话系统",
+ description="整合前端/ASR/大模型/TTS的异步网关服务",
+ version="1.0",
+ lifespan=lifespan # 绑定生命周期
+)
+
+# 跨域配置(允许前端访问)
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"], # 生产环境替换为具体前端域名
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# 初始化WebSocket连接管理器
+ws_manager = WebSocketConnectionManager()
+# 初始化编解码器(从配置读取密钥)
+codec = ProtocolCodec()
+
+
+@app.websocket("/ws/audio")
+async def websocket_audio(websocket: WebSocket):
+ # 完全委托给frontend_websocket_handler处理
+ await ws_manager.handle_connection(websocket)
+
+
+
+@app.get("/health")
+async def health_check():
+ """服务健康检查接口"""
+ return {"status": "healthy", "service": "audio-ai-chat"}
\ No newline at end of file
diff --git a/audio_ai_chat/audio_ai_chat/utils/__init__.py b/audio_ai_chat/audio_ai_chat/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/audio_ai_chat/audio_ai_chat/utils/__pycache__/__init__.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/utils/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..ccec488
Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/utils/__pycache__/__init__.cpython-310.pyc differ
diff --git a/audio_ai_chat/audio_ai_chat/utils/__pycache__/exceptions.cpython-310.pyc b/audio_ai_chat/audio_ai_chat/utils/__pycache__/exceptions.cpython-310.pyc
new file mode 100644
index 0000000..6a7230c
Binary files /dev/null and b/audio_ai_chat/audio_ai_chat/utils/__pycache__/exceptions.cpython-310.pyc differ
diff --git a/audio_ai_chat/audio_ai_chat/utils/exceptions.py b/audio_ai_chat/audio_ai_chat/utils/exceptions.py
new file mode 100644
index 0000000..d298a7b
--- /dev/null
+++ b/audio_ai_chat/audio_ai_chat/utils/exceptions.py
@@ -0,0 +1,15 @@
+class AudioAIChatBaseException(Exception):
+ """项目基础异常"""
+ pass
+
+class CodecError(AudioAIChatBaseException):
+ """编解码异常(加密/解密失败)"""
+ pass
+
+class ServiceCallError(AudioAIChatBaseException):
+ """服务调用异常(ASR/LLM/TTS调用失败)"""
+ pass
+
+class ValidationError(AudioAIChatBaseException):
+ """数据验证异常(请求参数不符合模型)"""
+ pass
\ No newline at end of file
diff --git a/audio_ai_chat/audio_ai_chat/utils/helpers.py b/audio_ai_chat/audio_ai_chat/utils/helpers.py
new file mode 100644
index 0000000..78042a5
--- /dev/null
+++ b/audio_ai_chat/audio_ai_chat/utils/helpers.py
@@ -0,0 +1,26 @@
+from typing import Callable, Awaitable, TypeVar
+from tenacity import retry, stop_after_attempt, wait_exponential
+from audio_ai_chat.config.logger import logger
+
+T = TypeVar("T")
+
+def async_retry(
+ times: int = 3,
+ delay: int = 1,
+ max_delay: int = 5
+) -> Callable[[Callable[..., Awaitable[T]]], Callable[..., Awaitable[T]]]:
+ """异步重试装饰器(通用)"""
+ def decorator(func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
+ @retry(
+ stop=stop_after_attempt(times),
+ wait=wait_exponential(multiplier=delay, min=delay, max=max_delay),
+ reraise=True
+ )
+ async def wrapper(*args, **kwargs) -> T:
+ try:
+ return await func(*args, **kwargs)
+ except Exception as e:
+ logger.warning(f"函数 {func.__name__} 调用失败,将重试(剩余次数:{retry.statistics.get('attempt_number', 0)}):{str(e)}")
+ raise
+ return wrapper
+ return decorator
\ No newline at end of file
diff --git a/audio_ai_chat/logs/app.log b/audio_ai_chat/logs/app.log
new file mode 100644
index 0000000..e69de29
diff --git a/audio_ai_chat/requirements.txt b/audio_ai_chat/requirements.txt
new file mode 100644
index 0000000..d336d12
--- /dev/null
+++ b/audio_ai_chat/requirements.txt
@@ -0,0 +1,9 @@
+fastapi>=0.104.1
+uvicorn>=0.24.0 # FastAPI运行服务器
+websockets>=12.0 # WebSocket支持
+pydantic>=2.4.2 # 数据模型校验
+python-dotenv>=1.0.0 # 读取.env文件
+loguru>=0.7.2 # 日志工具(比原生logging更简洁)
+requests>=2.31.0 # 同步HTTP请求(若ASR/LLM/TTS用HTTP接口)
+aiohttp>=3.9.1 # 异步HTTP请求(推荐,契合FastAPI异步特性)
+tenacity>=8.2.3 # 重试机制(服务调用失败自动重试)
diff --git a/audio_ai_chat/run.py b/audio_ai_chat/run.py
new file mode 100644
index 0000000..bd05115
--- /dev/null
+++ b/audio_ai_chat/run.py
@@ -0,0 +1,16 @@
+# run.py
+import uvicorn
+from dotenv import load_dotenv
+
+# 加载环境变量
+load_dotenv()
+
+if __name__ == "__main__":
+ uvicorn.run(
+ app="audio_ai_chat.main:app",
+ host="0.0.0.0",
+ port=8000,
+ reload=True,
+ log_level="info", # 关键:开启详细日志
+ access_log=True,
+ )
\ No newline at end of file
diff --git a/audio_ai_chat/ws_server.log b/audio_ai_chat/ws_server.log
new file mode 100644
index 0000000..e69de29
diff --git a/python/ProtocolCodec.py b/python/ProtocolCodec.py
new file mode 100644
index 0000000..10e63fe
--- /dev/null
+++ b/python/ProtocolCodec.py
@@ -0,0 +1,295 @@
+from enum import IntEnum
+import struct
+import json
+import gzip
+from typing import Optional, Union, Dict, Any, List, Tuple
+
+
+# -------------------------- 协议常量定义 --------------------------
+class ProtocolConst:
+ PROTOCOL_VERSION = 0b0001 # 协议版本(4位,字节0低4位,0~15)
+ HEADER_SIZE = 8 # 头部固定字节数(字节0~7)
+ MAX_BODY_SIZE = 0xFFFFFF # 最大包体大小(24位,≈16MB,字节4~6存储)
+ STRING_ENCODING = "utf-8" # 字符串默认编码
+
+
+# -------------------------- 枚举定义 --------------------------
+class MessageType(IntEnum):
+ """消息类型(4位,字节0高4位,0~15)"""
+ PING = 0b0001 # 心跳(支持空包体)
+ AUDIO_DATA = 0b0010 # 纯音频数据(pcm)
+ TEXT_MESSAGE = 0b0011 # 纯文本消息
+ CONTROL_CMD = 0b0100 # 控制指令
+ # 预留12种类型(0b0100 ~ 0b1111)
+
+
+class SerializationType(IntEnum):
+ """序列化方式(3位,字节1高3位,1~8)"""
+ RAW = 0b001 # 原始二进制(1)
+ JSON = 0b010 # JSON格式(2)
+ STRING = 0b011 # 直接字符串(3)
+ # 预留5种方式(0b100 ~ 0b111)
+
+
+class CompressionType(IntEnum):
+ """压缩方式(3位,字节1中3位,1~8)"""
+ NONE = 0b001 # 无压缩(默认值1)
+ GZIP = 0b010 # gzip压缩(2)
+ # 预留6种方式(0b011 ~ 0b111)
+
+
+class ControlCommand(IntEnum):
+ """控制指令类型(配合MessageType.CONTROL_CMD使用)"""
+ HEARTBEAT = 0b0001 # 心跳响应
+ PAUSE = 0b0010 # 暂停
+ RESUME = 0b0011 # 继续
+ STOP = 0b0100 # 停止
+
+
+# -------------------------- 协议工具类(与JS协议结构一致) --------------------------
+class ProtocolCodec:
+ @staticmethod
+ def pack(
+ msg_type: MessageType,
+ body: Union[bytes, str, Dict[str, Any], List[Any], None] = None,
+ sequence: int = 0,
+ serialization: Optional[SerializationType] = None,
+ compression: CompressionType = CompressionType.NONE
+ ) -> bytes:
+ """
+ 封装协议包(与JS pack 方法完全兼容)
+ :param msg_type: 消息类型
+ :param body: 包体数据(PING消息可传None/空,其他类型必填)
+ :param sequence: 消息顺序号(0~65535,默认0)
+ :param serialization: 序列化方式(None时自动推导)
+ :param compression: 压缩方式(默认无压缩)
+ :return: 完整协议包(bytes)
+ """
+ # 校验顺序号范围(0~65535)
+ if not isinstance(sequence, int) or not (0 <= sequence <= 0xFFFF):
+ raise ValueError(f"消息顺序号必须是0~65535的整数,当前传入:{sequence}")
+
+ # 特殊处理:PING消息支持空包体
+ if msg_type == MessageType.PING:
+ # PING消息强制RAW序列化
+ serialization = SerializationType.RAW
+ # 空包体转为空bytes
+ if body is None:
+ body = b""
+ if not isinstance(body, bytes):
+ raise TypeError(f"PING消息仅支持空包体或bytes类型,当前传入:{type(body)}")
+ else:
+ # 非PING消息包体不能为空
+ if body is None:
+ raise ValueError(f"非PING消息({msg_type.name})包体不能为空")
+
+ # 1. 自动推导序列化方式(非PING消息)
+ if serialization is None and msg_type != MessageType.PING:
+ if msg_type == MessageType.AUDIO_DATA:
+ serialization = SerializationType.RAW
+ elif msg_type == MessageType.TEXT_MESSAGE:
+ serialization = SerializationType.STRING
+ elif msg_type == MessageType.CONTROL_CMD:
+ serialization = SerializationType.JSON
+ else:
+ raise ValueError(f"不支持的消息类型:{msg_type}")
+
+ # 2. 校验枚举值范围
+ if not (0b001 <= serialization.value <= 0b111):
+ raise ValueError(f"序列化方式必须在1~8(0b001~0b111)范围内,当前传入:{serialization.value}")
+ if not (0b001 <= compression.value <= 0b111):
+ raise ValueError(f"压缩方式必须在1~8(0b001~0b111)范围内,当前传入:{compression.value}")
+
+ # 3. 序列化包体
+ serialized_body: bytes
+ if serialization == SerializationType.RAW:
+ if not isinstance(body, bytes):
+ raise TypeError("RAW序列化要求body必须是bytes类型")
+ serialized_body = body
+ elif serialization == SerializationType.STRING:
+ if not isinstance(body, str):
+ raise TypeError("STRING序列化要求body必须是str类型")
+ serialized_body = body.encode(ProtocolConst.STRING_ENCODING)
+ elif serialization == SerializationType.JSON:
+ if isinstance(body, str):
+ serialized_body = body.encode(ProtocolConst.STRING_ENCODING)
+ elif isinstance(body, (dict, list)):
+ serialized_body = json.dumps(body, ensure_ascii=False, separators=(',', ':')).encode(ProtocolConst.STRING_ENCODING)
+ else:
+ raise TypeError("JSON序列化要求body必须是str/dict/list类型")
+ else:
+ raise ValueError(f"不支持的序列化方式:{serialization}")
+
+ # 4. 压缩包体
+ compressed_body: bytes
+ if compression == CompressionType.GZIP:
+ compressed_body = gzip.compress(serialized_body)
+ elif compression == CompressionType.NONE:
+ compressed_body = serialized_body
+ else:
+ raise ValueError(f"不支持的压缩方式:{compression}")
+
+ # 5. 校验包体大小(24位最大支持0xFFFFFF字节)
+ body_len = len(compressed_body)
+ if body_len > ProtocolConst.MAX_BODY_SIZE:
+ raise OverflowError(
+ f"包体过大({body_len}字节),最大支持{ProtocolConst.MAX_BODY_SIZE}字节(≈16MB)"
+ )
+
+ # 6. 构造头部(与JS头部结构完全一致)
+ # 字节0:消息类型(高4位) + 协议版本(低4位)
+ byte0 = ((msg_type.value & 0x0F) << 4) | (ProtocolConst.PROTOCOL_VERSION & 0x0F)
+ # 字节1:序列化方式(高3位) + 压缩方式(中3位) + 保留位(低2位)
+ byte1 = ((serialization.value & 0x07) << 5) | ((compression.value & 0x07) << 2) | 0x00
+ # 字节2~3:消息顺序号(16位大端序)
+ byte2_3 = struct.pack(">H", sequence)
+ # 字节4~6:包体长度(24位大端序)
+ byte4 = (body_len >> 16) & 0xFF
+ byte5 = (body_len >> 8) & 0xFF
+ byte6 = body_len & 0xFF
+ # 字节7:保留位(固定0x00)
+ byte7 = 0x00
+
+ # 拼接头部
+ header = (
+ bytes([byte0, byte1]) +
+ byte2_3 +
+ bytes([byte4, byte5, byte6, byte7])
+ )
+ assert len(header) == ProtocolConst.HEADER_SIZE, f"头部长度错误:实际{len(header)}字节,预期{ProtocolConst.HEADER_SIZE}字节"
+
+ return header + compressed_body
+
+ @staticmethod
+ def unpack(packet: bytes) -> Tuple[MessageType, int, Any]:
+ """
+ 解析协议包(与JS unpack 方法完全兼容)
+ :param packet: 完整协议包(头部 + 包体)
+ :return: (消息类型, 顺序号, 原始包体数据) # 仅返回核心必要信息
+ """
+ # 1. 校验包长度
+ if len(packet) < ProtocolConst.HEADER_SIZE:
+ raise ValueError(
+ f"包长度过短({len(packet)}字节),至少需要{ProtocolConst.HEADER_SIZE}字节头部"
+ )
+
+ # 2. 解析头部
+ header = packet[:ProtocolConst.HEADER_SIZE]
+ body = packet[ProtocolConst.HEADER_SIZE:]
+
+ # 字节0:消息类型(高4位) + 协议版本(低4位)
+ byte0 = header[0]
+ msg_type = MessageType((byte0 >> 4) & 0x0F)
+ version = byte0 & 0x0F
+
+ # 字节1:序列化方式(高3位) + 压缩方式(中3位) + 保留位(低2位)
+ byte1 = header[1]
+ serialization = SerializationType((byte1 >> 5) & 0x07)
+ compression = CompressionType((byte1 >> 2) & 0x07)
+
+ # 字节2~3:消息顺序号(16位大端序)
+ sequence = struct.unpack(">H", header[2:4])[0]
+
+ # 字节4~6:包体长度(24位大端序),字节7:保留位(忽略)
+ body_len = (header[4] << 16) | (header[5] << 8) | header[6]
+
+ # 校验版本和包体长度(校验逻辑保留,确保数据有效性)
+ if version != ProtocolConst.PROTOCOL_VERSION:
+ raise ValueError(
+ f"协议版本不匹配:收到v{version}(0b{version:04b}),支持v{ProtocolConst.PROTOCOL_VERSION}(0b{ProtocolConst.PROTOCOL_VERSION:04b})"
+ )
+ if len(body) != body_len:
+ raise ValueError(
+ f"包体长度不匹配:头部声明{body_len}字节,实际{len(body)}字节"
+ )
+
+ # 3. 解压包体
+ decompressed_body: bytes
+ if compression == CompressionType.GZIP:
+ try:
+ decompressed_body = gzip.decompress(body)
+ except Exception as e:
+ raise ValueError(f"GZIP解压失败:{str(e)}")
+ elif compression == CompressionType.NONE:
+ decompressed_body = body
+ else:
+ raise ValueError(f"不支持的压缩方式:{compression}")
+
+ # 4. 反序列化包体(空包体返回None)
+ original_body: Any
+ if len(decompressed_body) == 0:
+ original_body = None
+ elif serialization == SerializationType.RAW:
+ original_body = decompressed_body
+ elif serialization == SerializationType.STRING:
+ try:
+ original_body = decompressed_body.decode(ProtocolConst.STRING_ENCODING)
+ except UnicodeDecodeError:
+ raise ValueError(f"STRING反序列化失败:{ProtocolConst.STRING_ENCODING}解码错误")
+ elif serialization == SerializationType.JSON:
+ try:
+ original_body = json.loads(decompressed_body.decode(ProtocolConst.STRING_ENCODING))
+ except UnicodeDecodeError:
+ raise ValueError(f"JSON反序列化失败:{ProtocolConst.STRING_ENCODING}解码错误")
+ except json.JSONDecodeError as e:
+ raise ValueError(f"JSON反序列化失败:格式错误({str(e)})")
+ else:
+ raise ValueError(f"不支持的序列化方式:{serialization}")
+
+ # 仅返回核心必要信息:消息类型、顺序号、原始包体数据
+ return msg_type, sequence, original_body
+
+
+# -------------------------- 使用示例(验证修改后功能正常) --------------------------
+if __name__ == "__main__":
+ # 示例1:PING消息(空包体,默认顺序号0)
+ ping_packet = ProtocolCodec.pack(MessageType.PING)
+ print(f"PING消息包长度:{len(ping_packet)}字节(仅头部)")
+ msg_type1, seq1, body1 = ProtocolCodec.unpack(ping_packet)
+ print(f"PING解析结果:类型={msg_type1.name},顺序号={seq1},包体={body1}\n")
+
+ # 示例2:纯文本消息(STRING序列化,指定顺序号)
+ text_body = "Python与JS协议兼容测试(纯字符串)"
+ text_packet = ProtocolCodec.pack(
+ msg_type=MessageType.TEXT_MESSAGE,
+ body=text_body,
+ sequence=1001,
+ compression=CompressionType.NONE
+ )
+ print(f"文本消息包长度:{len(text_packet)}字节")
+ msg_type2, seq2, body2 = ProtocolCodec.unpack(text_packet)
+ print(f"文本解析结果:类型={msg_type2.name},顺序号={seq2},内容={body2}\n")
+
+ # 示例3:控制指令(JSON序列化)
+ control_body = {"cmd": ControlCommand.PAUSE.value, "reason": "用户主动暂停"}
+ control_packet = ProtocolCodec.pack(
+ msg_type=MessageType.CONTROL_CMD,
+ body=control_body,
+ sequence=1002
+ )
+ print(f"控制指令包长度:{len(control_packet)}字节")
+ msg_type3, seq3, body3 = ProtocolCodec.unpack(control_packet)
+ print(f"控制指令解析结果:类型={msg_type3.name},顺序号={seq3},内容={body3}\n")
+
+ # 示例4:音频数据(RAW序列化)
+ audio_body = b"\x00\x01\x02\x03\x04\x05" * 100 # 模拟PCM数据
+ audio_packet = ProtocolCodec.pack(
+ msg_type=MessageType.AUDIO_DATA,
+ body=audio_body,
+ sequence=1003
+ )
+ print(f"音频数据包长度:{len(audio_packet)}字节")
+ msg_type4, seq4, body4 = ProtocolCodec.unpack(audio_packet)
+ print(f"音频解析结果:类型={msg_type4.name},顺序号={seq4},数据长度={len(body4)}字节\n")
+
+ # 示例5:GZIP压缩测试(需JS端启用GZIP解压)
+ long_text_body = "这是一段很长的文本,用于测试GZIP压缩效果" * 100
+ gzip_packet = ProtocolCodec.pack(
+ msg_type=MessageType.TEXT_MESSAGE,
+ body=long_text_body,
+ sequence=1004,
+ compression=CompressionType.GZIP
+ )
+ print(f"GZIP压缩后包长度:{len(gzip_packet)}字节(原始文本长度:{len(long_text_body.encode())}字节)")
+ msg_type5, seq5, body5 = ProtocolCodec.unpack(gzip_packet)
+ print(f"GZIP解析结果:类型={msg_type5.name},顺序号={seq5},内容前50字:{body5[:50]}...")
\ No newline at end of file
diff --git a/python/__pycache__/frontend_ws.cpython-310.pyc b/python/__pycache__/frontend_ws.cpython-310.pyc
index c7646e6..cf95f18 100644
Binary files a/python/__pycache__/frontend_ws.cpython-310.pyc and b/python/__pycache__/frontend_ws.cpython-310.pyc differ
diff --git a/python/tts_output_33cfc4e1.pcm b/python/tts_output_33cfc4e1.pcm
deleted file mode 100644
index 294df1a..0000000
Binary files a/python/tts_output_33cfc4e1.pcm and /dev/null differ
diff --git a/python/tts_output_46c5a16f.pcm b/python/tts_output_46c5a16f.pcm
deleted file mode 100644
index 1e42a4c..0000000
Binary files a/python/tts_output_46c5a16f.pcm and /dev/null differ
diff --git a/python/tts_output_4cafe62e.mp3 b/python/tts_output_4cafe62e.mp3
deleted file mode 100644
index 5f6e332..0000000
Binary files a/python/tts_output_4cafe62e.mp3 and /dev/null differ
diff --git a/python/tts_output_83430d06.pcm b/python/tts_output_83430d06.pcm
deleted file mode 100644
index 7f9a3e0..0000000
Binary files a/python/tts_output_83430d06.pcm and /dev/null differ
diff --git a/python/tts_output_ab2c61fb.pcm b/python/tts_output_ab2c61fb.pcm
deleted file mode 100644
index 7a805ff..0000000
Binary files a/python/tts_output_ab2c61fb.pcm and /dev/null differ
diff --git a/python/tts_output_c5f4106a.mp3 b/python/tts_output_c5f4106a.mp3
deleted file mode 100644
index 9174284..0000000
Binary files a/python/tts_output_c5f4106a.mp3 and /dev/null differ
diff --git a/python/tts_output_cb0e0ad4.pcm b/python/tts_output_cb0e0ad4.pcm
deleted file mode 100644
index d88283b..0000000
Binary files a/python/tts_output_cb0e0ad4.pcm and /dev/null differ
diff --git a/测试流式传输uniapp/pages/index/index - 副本 (2).vue b/测试流式传输uniapp/pages/index/index - 副本 (2).vue
new file mode 100644
index 0000000..f4c2824
--- /dev/null
+++ b/测试流式传输uniapp/pages/index/index - 副本 (2).vue
@@ -0,0 +1,285 @@
+
+
+
+
+ {{ status }}
+ 当前分贝:{{ currentDecibels }}
+
+
+ xx
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/测试流式传输uniapp/pages/index/index - 副本.vue b/测试流式传输uniapp/pages/index/index - 副本.vue
index da453ab..8d3870c 100644
--- a/测试流式传输uniapp/pages/index/index - 副本.vue
+++ b/测试流式传输uniapp/pages/index/index - 副本.vue
@@ -79,6 +79,7 @@
success: () => {
console.log('web');
this.ws.onOpen((res) => {
+
console.log('WebSocket连接已打开', res);
this.ws.onMessage((res) => {
let messageData = res.data;
diff --git a/测试流式传输uniapp/pages/index/index.vue b/测试流式传输uniapp/pages/index/index.vue
index 4dc9bdd..3d8ad99 100644
--- a/测试流式传输uniapp/pages/index/index.vue
+++ b/测试流式传输uniapp/pages/index/index.vue
@@ -4,225 +4,40 @@
{{ status }}
当前分贝:{{ currentDecibels }}
-
-
+
xx
-
+
-
\ No newline at end of file
+
diff --git a/测试流式传输uniapp/pages/index/useWebSocket.js b/测试流式传输uniapp/pages/index/useWebSocket.js
new file mode 100644
index 0000000..e7efe35
--- /dev/null
+++ b/测试流式传输uniapp/pages/index/useWebSocket.js
@@ -0,0 +1,77 @@
+import { ref } from 'vue';
+
+// 在组件 setup 函数中使用
+export default function useWebSocket() {
+ // 存储 WebSocket 实例
+ const ws = ref(null);
+ const wsUrl = 'ws://127.0.0.1:8000/ws/audio';
+ // 初始化 WebSocket 连接
+ const initWebSocket = () => {
+ ws.value = uni.connectSocket({
+ url: wsUrl,
+ success: () => {
+ console.log('web');
+
+ // 监听连接打开
+ ws.value.onOpen((res) => {
+ console.log('WebSocket连接已打开', res);
+
+ // 监听消息接收
+ ws.value.onMessage((res) => {
+ let messageData = res.data;
+
+ // 处理不同类型的数据
+ if (typeof messageData === 'string') {
+ // 文本数据
+ try {
+ const parsedData = JSON.parse(messageData);
+ handleMessage(parsedData);
+ } catch (e) {
+ handleMessage(messageData);
+ }
+ } else if (messageData instanceof ArrayBuffer) {
+ // 二进制数据
+ try {
+ // 注意:组合式 API 中需通过 ref 获取子组件实例
+ const aaaRef = ref(null); // 需在组件中声明
+ aaaRef.value?.appendBuffer(messageData);
+ } catch (e) {
+ console.log('二进制数据解析失败:', e);
+ }
+ }
+ });
+ });
+ },
+ fail: () => {
+ console.log('fail');
+ },
+ });
+ };
+
+ // 消息处理函数(根据实际业务逻辑修改)
+ const handleMessage = (data) => {
+ // 原组件中的 handleMessage 逻辑迁移到这里
+ console.log('收到消息:', data);
+ };
+
+ // 辅助函数:ArrayBuffer 转字符串(如果后续需要使用)
+ const arrayBufferToStringCompat = (buffer) => {
+ const decoder = new TextDecoder('utf-8');
+ return decoder.decode(buffer);
+ };
+
+ // 关闭连接函数(可选,按需暴露)
+ const closeWebSocket = () => {
+ if (ws.value) {
+ ws.value.close();
+ console.log('WebSocket连接已关闭');
+ }
+ };
+
+ return {
+ ws,
+ initWebSocket,
+ closeWebSocket,
+ handleMessage
+ };
+};
\ No newline at end of file
diff --git a/测试流式传输uniapp/unpackage/dist/cache/.vite/deps/_metadata.json b/测试流式传输uniapp/unpackage/dist/cache/.vite/deps/_metadata.json
index 568a6e3..11f7af3 100644
--- a/测试流式传输uniapp/unpackage/dist/cache/.vite/deps/_metadata.json
+++ b/测试流式传输uniapp/unpackage/dist/cache/.vite/deps/_metadata.json
@@ -1,8 +1,8 @@
{
- "hash": "030e727a",
- "configHash": "c22f3258",
- "lockfileHash": "c10b225f",
- "browserHash": "77dd6173",
+ "hash": "56a2a6b1",
+ "configHash": "0d2436b6",
+ "lockfileHash": "1c743963",
+ "browserHash": "42eed156",
"optimized": {},
"chunks": {}
}
\ No newline at end of file