冯杨

豆包大模型,名称赋予

音频识别asr:使用本地方案funasr(复用的Fay项目中的funasr)
FunASR服务连接测试脚本
用于验证本地FunASR WebSocket服务是否可以正常连接
webrtcapichat.html中对话框做进一步调整,侧边栏增加对话框的透明度调整。暂时设置对话框的背景色差异大些,美学设计暂不考虑。对话框支持隐藏
  1 +# -*- coding: utf-8 -*-
  2 +"""
  3 +AIfeng/2025-01-27
  4 +配置管理工具模块
  5 +统一管理项目配置参数
  6 +"""
  7 +
  8 +import json
  9 +import os
  10 +from typing import Dict, Any
  11 +
  12 +class ConfigManager:
  13 + """配置管理器"""
  14 +
  15 + def __init__(self):
  16 + self._config = {}
  17 + self._load_default_config()
  18 + self._load_config_files()
  19 +
  20 + def _load_default_config(self):
  21 + """加载默认配置"""
  22 + self._config.update({
  23 + # FunASR配置
  24 + 'local_asr_ip': '127.0.0.1',
  25 + 'local_asr_port': 10197,
  26 +
  27 + # 阿里云NLS配置
  28 + 'key_ali_nls_key_id': '',
  29 + 'key_ali_nls_key_secret': '',
  30 + 'key_ali_nls_app_key': '',
  31 +
  32 + # 其他ASR配置
  33 + 'asr_timeout': 30,
  34 + 'asr_reconnect_delay': 1,
  35 + 'asr_max_reconnect_attempts': 5,
  36 + })
  37 +
  38 + def _load_config_files(self):
  39 + """加载配置文件"""
  40 + config_files = [
  41 + 'config/asr_config.json',
  42 + 'config/llm_config.json',
  43 + 'config/doubao_config.json'
  44 + ]
  45 +
  46 + for config_file in config_files:
  47 + if os.path.exists(config_file):
  48 + try:
  49 + with open(config_file, 'r', encoding='utf-8') as f:
  50 + file_config = json.load(f)
  51 + self._config.update(file_config)
  52 + except Exception as e:
  53 + print(f"警告: 加载配置文件 {config_file} 失败: {e}")
  54 +
  55 + def get(self, key: str, default=None):
  56 + """获取配置值"""
  57 + return self._config.get(key, default)
  58 +
  59 + def set(self, key: str, value: Any):
  60 + """设置配置值"""
  61 + self._config[key] = value
  62 +
  63 + def update(self, config_dict: Dict[str, Any]):
  64 + """批量更新配置"""
  65 + self._config.update(config_dict)
  66 +
  67 + def save_to_file(self, file_path: str):
  68 + """保存配置到文件"""
  69 + os.makedirs(os.path.dirname(file_path), exist_ok=True)
  70 + with open(file_path, 'w', encoding='utf-8') as f:
  71 + json.dump(self._config, f, ensure_ascii=False, indent=2)
  72 +
  73 + def __getattr__(self, name):
  74 + """支持属性访问方式"""
  75 + if name in self._config:
  76 + return self._config[name]
  77 + raise AttributeError(f"配置项 '{name}' 不存在")
  78 +
  79 +# 全局配置实例
  80 +_config_manager = ConfigManager()
  81 +
  82 +# 兼容原有的属性访问方式
  83 +local_asr_ip = _config_manager.local_asr_ip
  84 +local_asr_port = _config_manager.local_asr_port
  85 +key_ali_nls_key_id = _config_manager.key_ali_nls_key_id
  86 +key_ali_nls_key_secret = _config_manager.key_ali_nls_key_secret
  87 +key_ali_nls_app_key = _config_manager.key_ali_nls_app_key
  88 +
  89 +# 导出配置管理器
  90 +config = _config_manager
  1 +# -*- coding: utf-8 -*-
  2 +"""
  3 +AIfeng/2025-01-27
  4 +Core模块初始化文件
  5 +"""
  6 +
  7 +from .wsa_server import get_web_instance, get_instance
  8 +
  9 +__all__ = ['get_web_instance', 'get_instance']
  1 +# -*- coding: utf-8 -*-
  2 +"""
  3 +AIfeng/2025-01-27
  4 +WebSocket服务器管理模块
  5 +提供Web和Human连接的管理功能
  6 +"""
  7 +
  8 +import queue
  9 +from typing import Dict, Any, Optional
  10 +from threading import Lock
  11 +
  12 +class WebSocketManager:
  13 + """WebSocket连接管理器"""
  14 +
  15 + def __init__(self):
  16 + self._connections = {}
  17 + self._command_queue = queue.Queue()
  18 + self._lock = Lock()
  19 +
  20 + def is_connected(self, username: str) -> bool:
  21 + """检查用户是否已连接
  22 +
  23 + Args:
  24 + username: 用户名
  25 +
  26 + Returns:
  27 + 是否已连接
  28 + """
  29 + with self._lock:
  30 + return username in self._connections
  31 +
  32 + def is_connected_human(self, username: str) -> bool:
  33 + """检查人类用户是否已连接
  34 +
  35 + Args:
  36 + username: 用户名
  37 +
  38 + Returns:
  39 + 是否已连接
  40 + """
  41 + # 简化实现,与is_connected相同
  42 + return self.is_connected(username)
  43 +
  44 + def add_connection(self, username: str, connection: Any):
  45 + """添加连接
  46 +
  47 + Args:
  48 + username: 用户名
  49 + connection: 连接对象
  50 + """
  51 + with self._lock:
  52 + self._connections[username] = connection
  53 +
  54 + def remove_connection(self, username: str):
  55 + """移除连接
  56 +
  57 + Args:
  58 + username: 用户名
  59 + """
  60 + with self._lock:
  61 + self._connections.pop(username, None)
  62 +
  63 + def add_cmd(self, command: Dict[str, Any]):
  64 + """添加命令到队列
  65 +
  66 + Args:
  67 + command: 命令字典
  68 + """
  69 + try:
  70 + self._command_queue.put(command, timeout=1.0)
  71 + except queue.Full:
  72 + print(f"警告: 命令队列已满,丢弃命令: {command}")
  73 +
  74 + def get_cmd(self, timeout: float = 1.0) -> Optional[Dict[str, Any]]:
  75 + """从队列获取命令
  76 +
  77 + Args:
  78 + timeout: 超时时间
  79 +
  80 + Returns:
  81 + 命令字典或None
  82 + """
  83 + try:
  84 + return self._command_queue.get(timeout=timeout)
  85 + except queue.Empty:
  86 + return None
  87 +
  88 + def get_connection_count(self) -> int:
  89 + """获取连接数量"""
  90 + with self._lock:
  91 + return len(self._connections)
  92 +
  93 + def get_usernames(self) -> list:
  94 + """获取所有用户名列表"""
  95 + with self._lock:
  96 + return list(self._connections.keys())
  97 +
  98 +# 全局实例
  99 +_web_instance = WebSocketManager()
  100 +_human_instance = WebSocketManager()
  101 +
  102 +def get_web_instance() -> WebSocketManager:
  103 + """获取Web WebSocket管理器实例"""
  104 + return _web_instance
  105 +
  106 +def get_instance() -> WebSocketManager:
  107 + """获取Human WebSocket管理器实例"""
  108 + return _human_instance
  1 +# ASR/TTS技术架构分析与扩展方案
  2 +
  3 +**AIfeng/2024-12-19**
  4 +
  5 +## 1. 项目ASR技术实现分析
  6 +
  7 +### 1.1 ASR架构设计
  8 +
  9 +项目采用模块化ASR架构,基于`BaseASR`抽象类实现多种ASR方案:
  10 +
  11 +#### 核心架构组件
  12 +- **BaseASR**: 抽象基类,定义统一接口
  13 +- **音频处理流水线**: 16kHz采样率,20ms帧长度(320样本/帧)
  14 +- **队列机制**: 使用Queue进行音频帧缓冲
  15 +- **多进程支持**: 基于torch.multiprocessing实现并发处理
  16 +
  17 +#### 当前ASR实现类型
  18 +
  19 +1. **NerfASR** (`nerfasr.py`)
  20 + - 支持多种音频特征: Esperanto(44维)、DeepSpeech(29维)、Hubert(1024维)
  21 + - 上下文缓存机制: stride_left + context + stride_right
  22 + - GPU/CPU自适应推理
  23 +
  24 +2. **MuseASR** (`museasr.py`)
  25 + - 基于Whisper音频特征提取
  26 + - 集成Audio2Feature处理器
  27 + - 批处理优化(batch_size*2)
  28 +
  29 +3. **HubertASR** (`hubertasr.py`)
  30 + - Hubert音频特征处理
  31 + - 可配置音频特征长度[8,8]
  32 + - 实时音频流处理
  33 +
  34 +4. **LipASR** (`lipasr.py`)
  35 + - 基于Wav2Lip的梅尔频谱特征
  36 + - 80维梅尔频谱处理
  37 + - 唇形同步优化
  38 +
  39 +### 1.2 前端ASR实现
  40 +
  41 +**Web ASR模块** (`web/asr/`)
  42 +- **技术栈**: WebSocket + Web Audio API
  43 +- **音频格式**: PCM 16kHz 16bit
  44 +- **实时传输**: 基于FunASR WebSocket协议
  45 +- **浏览器兼容**: 支持现代浏览器录音API
  46 +
  47 +## 2. TTS技术实现分析
  48 +
  49 +### 2.1 TTS架构设计
  50 +
  51 +基于`BaseTTS`抽象类的统一TTS框架:
  52 +
  53 +#### 核心特性
  54 +- **异步处理**: 基于线程的TTS渲染
  55 +- **流式输出**: 支持实时音频流生成
  56 +- **状态管理**: RUNNING/PAUSE状态控制
  57 +- **音频重采样**: 统一输出16kHz采样率
  58 +
  59 +### 2.2 TTS服务实现
  60 +
  61 +1. **EdgeTTS**
  62 + - 微软Edge浏览器TTS服务
  63 + - 异步流式处理
  64 + - 多语言支持(zh-CN-XiaoxiaoNeural等)
  65 +
  66 +2. **FishTTS**
  67 + - 本地/远程TTS服务
  68 + - RESTful API接口
  69 + - 流式音频生成
  70 + - 参考音频克隆
  71 +
  72 +3. **SovitsTTS**
  73 + - GPT-SoVITS语音克隆
  74 + - OGG格式流式输出
  75 + - 情感语音合成
  76 +
  77 +4. **CosyVoiceTTS**
  78 + - 阿里CosyVoice服务
  79 + - 高质量语音合成
  80 + - 参考音频支持
  81 +
  82 +5. **TencentTTS**
  83 + - 腾讯云语音合成
  84 + - 企业级API服务
  85 + - 多音色支持
  86 +
  87 +6. **XTTS**
  88 + - Coqui XTTS服务
  89 + - 多语言语音克隆
  90 + - 本地部署支持
  91 +
  92 +## 3. 技术架构优势
  93 +
  94 +### 3.1 设计模式优势
  95 +- **策略模式**: 通过继承BaseASR/BaseTTS实现算法切换
  96 +- **观察者模式**: WebSocket消息推送机制
  97 +- **生产者消费者**: 音频队列处理
  98 +
  99 +### 3.2 性能优化
  100 +- **批处理**: 音频帧批量处理减少延迟
  101 +- **内存管理**: 循环缓冲区避免内存泄漏
  102 +- **并发处理**: 多进程/多线程提升吞吐量
  103 +
  104 +## 4. 第三方ASR扩展方案
  105 +
  106 +### 4.1 云端ASR服务集成
  107 +
  108 +#### 4.1.1 百度ASR
  109 +```python
  110 +class BaiduASR(BaseASR):
  111 + def __init__(self, opt, parent):
  112 + super().__init__(opt, parent)
  113 + self.app_id = os.getenv("BAIDU_APP_ID")
  114 + self.api_key = os.getenv("BAIDU_API_KEY")
  115 + self.secret_key = os.getenv("BAIDU_SECRET_KEY")
  116 + self.client = AipSpeech(self.app_id, self.api_key, self.secret_key)
  117 +
  118 + def run_step(self):
  119 + # 实现百度ASR实时识别逻辑
  120 + pass
  121 +```
  122 +
  123 +#### 4.1.2 阿里云ASR
  124 +```python
  125 +class AliyunASR(BaseASR):
  126 + def __init__(self, opt, parent):
  127 + super().__init__(opt, parent)
  128 + self.access_key = os.getenv("ALIYUN_ACCESS_KEY")
  129 + self.access_secret = os.getenv("ALIYUN_ACCESS_SECRET")
  130 + # 初始化阿里云ASR客户端
  131 +
  132 + def run_step(self):
  133 + # 实现阿里云实时ASR
  134 + pass
  135 +```
  136 +
  137 +#### 4.1.3 腾讯云ASR
  138 +```python
  139 +class TencentASR(BaseASR):
  140 + def __init__(self, opt, parent):
  141 + super().__init__(opt, parent)
  142 + self.secret_id = os.getenv("TENCENT_SECRET_ID")
  143 + self.secret_key = os.getenv("TENCENT_SECRET_KEY")
  144 + # 初始化腾讯云ASR
  145 +```
  146 +
  147 +### 4.2 开源ASR模型集成
  148 +
  149 +#### 4.2.1 Whisper集成
  150 +```python
  151 +class WhisperASR(BaseASR):
  152 + def __init__(self, opt, parent):
  153 + super().__init__(opt, parent)
  154 + import whisper
  155 + self.model = whisper.load_model(opt.whisper_model)
  156 +
  157 + def run_step(self):
  158 + # 实现Whisper实时识别
  159 + audio_data = self.get_audio_buffer()
  160 + result = self.model.transcribe(audio_data)
  161 + return result['text']
  162 +```
  163 +
  164 +#### 4.2.2 SenseVoice集成
  165 +```python
  166 +class SenseVoiceASR(BaseASR):
  167 + def __init__(self, opt, parent):
  168 + super().__init__(opt, parent)
  169 + from funasr import AutoModel
  170 + self.model = AutoModel(model="sensevoice")
  171 +```
  172 +
  173 +### 4.3 配置化ASR选择
  174 +
  175 +```python
  176 +# config/asr_config.json
  177 +{
  178 + "asr_providers": {
  179 + "baidu": {
  180 + "class": "BaiduASR",
  181 + "config": {
  182 + "app_id": "${BAIDU_APP_ID}",
  183 + "api_key": "${BAIDU_API_KEY}"
  184 + }
  185 + },
  186 + "whisper": {
  187 + "class": "WhisperASR",
  188 + "config": {
  189 + "model_size": "base",
  190 + "device": "cuda"
  191 + }
  192 + }
  193 + },
  194 + "default_provider": "whisper"
  195 +}
  196 +```
  197 +
  198 +## 5. 第三方TTS扩展方案
  199 +
  200 +### 5.1 云端TTS服务
  201 +
  202 +#### 5.1.1 百度TTS
  203 +```python
  204 +class BaiduTTS(BaseTTS):
  205 + def __init__(self, opt, parent):
  206 + super().__init__(opt, parent)
  207 + from aip import AipSpeech
  208 + self.client = AipSpeech(app_id, api_key, secret_key)
  209 +
  210 + def txt_to_audio(self, msg):
  211 + text, textevent = msg
  212 + result = self.client.synthesis(text, 'zh', 1, {
  213 + 'vol': 5, 'per': 4, 'spd': 5, 'pit': 5
  214 + })
  215 + self.stream_audio(result, msg)
  216 +```
  217 +
  218 +#### 5.1.2 Azure TTS
  219 +```python
  220 +class AzureTTS(BaseTTS):
  221 + def __init__(self, opt, parent):
  222 + super().__init__(opt, parent)
  223 + import azure.cognitiveservices.speech as speechsdk
  224 + self.speech_config = speechsdk.SpeechConfig(
  225 + subscription=opt.azure_key,
  226 + region=opt.azure_region
  227 + )
  228 +```
  229 +
  230 +### 5.2 开源TTS模型
  231 +
  232 +#### 5.2.1 Coqui TTS
  233 +```python
  234 +class CoquiTTS(BaseTTS):
  235 + def __init__(self, opt, parent):
  236 + super().__init__(opt, parent)
  237 + from TTS.api import TTS
  238 + self.tts = TTS(model_name=opt.coqui_model)
  239 +
  240 + def txt_to_audio(self, msg):
  241 + text, textevent = msg
  242 + wav = self.tts.tts(text=text, speaker_wav=opt.REF_FILE)
  243 + self.stream_audio_array(wav, msg)
  244 +```
  245 +
  246 +#### 5.2.2 PaddleSpeech TTS
  247 +```python
  248 +class PaddleTTS(BaseTTS):
  249 + def __init__(self, opt, parent):
  250 + super().__init__(opt, parent)
  251 + from paddlespeech.cli.tts import TTSExecutor
  252 + self.tts_executor = TTSExecutor()
  253 +```
  254 +
  255 +## 6. 本地离线服务优化方案
  256 +
  257 +### 6.1 Docker容器化部署
  258 +
  259 +#### 6.1.1 ASR服务容器
  260 +```dockerfile
  261 +# Dockerfile.asr
  262 +FROM pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime
  263 +
  264 +RUN pip install whisper funasr sensevoice
  265 +
  266 +COPY asr_server.py /app/
  267 +COPY models/ /app/models/
  268 +
  269 +EXPOSE 8001
  270 +CMD ["python", "/app/asr_server.py"]
  271 +```
  272 +
  273 +#### 6.1.2 TTS服务容器
  274 +```dockerfile
  275 +# Dockerfile.tts
  276 +FROM pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime
  277 +
  278 +RUN pip install TTS coqui-ai-tts
  279 +
  280 +COPY tts_server.py /app/
  281 +COPY models/ /app/models/
  282 +
  283 +EXPOSE 8002
  284 +CMD ["python", "/app/tts_server.py"]
  285 +```
  286 +
  287 +### 6.2 微服务架构
  288 +
  289 +```yaml
  290 +# docker-compose.yml
  291 +version: '3.8'
  292 +services:
  293 + asr-service:
  294 + build:
  295 + context: .
  296 + dockerfile: Dockerfile.asr
  297 + ports:
  298 + - "8001:8001"
  299 + volumes:
  300 + - ./models:/app/models
  301 + environment:
  302 + - CUDA_VISIBLE_DEVICES=0
  303 +
  304 + tts-service:
  305 + build:
  306 + context: .
  307 + dockerfile: Dockerfile.tts
  308 + ports:
  309 + - "8002:8002"
  310 + volumes:
  311 + - ./models:/app/models
  312 + environment:
  313 + - CUDA_VISIBLE_DEVICES=1
  314 +
  315 + main-app:
  316 + build: .
  317 + ports:
  318 + - "7860:7860"
  319 + depends_on:
  320 + - asr-service
  321 + - tts-service
  322 + environment:
  323 + - ASR_SERVICE_URL=http://asr-service:8001
  324 + - TTS_SERVICE_URL=http://tts-service:8002
  325 +```
  326 +
  327 +### 6.3 模型优化策略
  328 +
  329 +#### 6.3.1 模型量化
  330 +```python
  331 +# 模型量化优化
  332 +import torch
  333 +from torch.quantization import quantize_dynamic
  334 +
  335 +class OptimizedWhisperASR(WhisperASR):
  336 + def __init__(self, opt, parent):
  337 + super().__init__(opt, parent)
  338 + # 动态量化优化
  339 + self.model = quantize_dynamic(
  340 + self.model, {torch.nn.Linear}, dtype=torch.qint8
  341 + )
  342 +```
  343 +
  344 +#### 6.3.2 模型缓存策略
  345 +```python
  346 +class ModelCache:
  347 + def __init__(self):
  348 + self.asr_models = {}
  349 + self.tts_models = {}
  350 +
  351 + def get_asr_model(self, model_name):
  352 + if model_name not in self.asr_models:
  353 + self.asr_models[model_name] = self.load_asr_model(model_name)
  354 + return self.asr_models[model_name]
  355 +
  356 + def get_tts_model(self, model_name):
  357 + if model_name not in self.tts_models:
  358 + self.tts_models[model_name] = self.load_tts_model(model_name)
  359 + return self.tts_models[model_name]
  360 +```
  361 +
  362 +## 7. 性能优化建议
  363 +
  364 +### 7.1 延迟优化
  365 +- **流式处理**: 实现真正的流式ASR/TTS
  366 +- **预加载**: 模型预热和缓存
  367 +- **批处理**: 合理的批处理大小
  368 +- **异步处理**: 非阻塞音频处理
  369 +
  370 +### 7.2 资源优化
  371 +- **GPU调度**: 智能GPU资源分配
  372 +- **内存管理**: 及时释放音频缓冲区
  373 +- **模型共享**: 多会话共享模型实例
  374 +
  375 +### 7.3 可扩展性
  376 +- **负载均衡**: 多实例部署
  377 +- **服务发现**: 动态服务注册
  378 +- **监控告警**: 服务健康检查
  379 +
  380 +## 8. 实施路线图
  381 +
  382 +### Phase 1: 基础扩展 (1-2周)
  383 +1. 实现Whisper ASR集成
  384 +2. 添加百度/阿里云TTS支持
  385 +3. 完善配置化选择机制
  386 +
  387 +### Phase 2: 服务化改造 (2-3周)
  388 +1. ASR/TTS服务独立部署
  389 +2. Docker容器化
  390 +3. 微服务架构重构
  391 +
  392 +### Phase 3: 性能优化 (2-3周)
  393 +1. 模型量化和优化
  394 +2. 缓存策略实施
  395 +3. 监控体系建设
  396 +
  397 +### Phase 4: 生产就绪 (1-2周)
  398 +1. 负载测试和调优
  399 +2. 文档完善
  400 +3. 部署自动化
  401 +
  402 +## 9. 风险评估
  403 +
  404 +### 9.1 技术风险
  405 +- **模型兼容性**: 不同模型API差异
  406 +- **性能瓶颈**: 实时性要求vs模型复杂度
  407 +- **资源消耗**: GPU内存和计算资源
  408 +
  409 +### 9.2 缓解策略
  410 +- **统一接口**: 抽象层屏蔽差异
  411 +- **性能测试**: 提前验证性能指标
  412 +- **资源监控**: 实时监控资源使用
  413 +
  414 +---
  415 +
  416 +**技术负责人**: AIfeng
  417 +**文档版本**: v1.0
  418 +**更新日期**: 2024-12-19
  1 +# AIfeng/2024-12-19
  2 +# 代码质量与可维护性增强建议
  3 +
  4 +## 概述
  5 +
  6 +基于当前豆包模型集成的成功实施,以下是进一步提升代码质量和系统可维护性的建议。这些建议遵循全栈开发架构师的最佳实践,旨在建立长期可持续的技术架构。
  7 +
  8 +## 🏗️ 架构优化建议
  9 +
  10 +### 1. 依赖注入模式
  11 +
  12 +**当前状态:** 直接在函数中硬编码模型选择逻辑
  13 +**建议改进:** 实现依赖注入容器
  14 +
  15 +```python
  16 +# 建议实现:config/di_container.py
  17 +class LLMContainer:
  18 + def __init__(self):
  19 + self._providers = {}
  20 + self._instances = {}
  21 +
  22 + def register(self, interface, implementation):
  23 + self._providers[interface] = implementation
  24 +
  25 + def resolve(self, interface):
  26 + if interface not in self._instances:
  27 + provider = self._providers.get(interface)
  28 + if provider:
  29 + self._instances[interface] = provider()
  30 + return self._instances[interface]
  31 +
  32 +# 使用示例
  33 +container = LLMContainer()
  34 +container.register('llm_service', DoubaoService)
  35 +llm_service = container.resolve('llm_service')
  36 +```
  37 +
  38 +### 2. 策略模式重构
  39 +
  40 +**当前状态:** if-elif条件判断选择模型
  41 +**建议改进:** 策略模式 + 工厂模式
  42 +
  43 +```python
  44 +# 建议实现:llm/strategies/base_strategy.py
  45 +from abc import ABC, abstractmethod
  46 +
  47 +class LLMStrategy(ABC):
  48 + @abstractmethod
  49 + def chat(self, message: str, callback=None) -> str:
  50 + pass
  51 +
  52 + @abstractmethod
  53 + def get_model_info(self) -> dict:
  54 + pass
  55 +
  56 +# llm/strategies/doubao_strategy.py
  57 +class DoubaoStrategy(LLMStrategy):
  58 + def __init__(self, config):
  59 + self.doubao = Doubao(config.get('config_file'))
  60 +
  61 + def chat(self, message: str, callback=None) -> str:
  62 + return self.doubao.chat_stream(message, callback)
  63 +
  64 +# llm/factory.py
  65 +class LLMFactory:
  66 + _strategies = {
  67 + 'doubao': DoubaoStrategy,
  68 + 'qwen': QwenStrategy,
  69 + }
  70 +
  71 + @classmethod
  72 + def create_strategy(cls, model_type: str, config: dict) -> LLMStrategy:
  73 + strategy_class = cls._strategies.get(model_type)
  74 + if not strategy_class:
  75 + raise ValueError(f"Unsupported model type: {model_type}")
  76 + return strategy_class(config)
  77 +```
  78 +
  79 +### 3. 配置管理中心化
  80 +
  81 +**当前状态:** 多个配置文件分散管理
  82 +**建议改进:** 统一配置管理器
  83 +
  84 +```python
  85 +# 建议实现:config/config_manager.py
  86 +class ConfigManager:
  87 + def __init__(self):
  88 + self._configs = {}
  89 + self._watchers = []
  90 +
  91 + def load_config(self, config_type: str) -> dict:
  92 + if config_type not in self._configs:
  93 + self._configs[config_type] = self._load_from_file(config_type)
  94 + return self._configs[config_type]
  95 +
  96 + def reload_config(self, config_type: str):
  97 + """支持热重载配置"""
  98 + self._configs[config_type] = self._load_from_file(config_type)
  99 + self._notify_watchers(config_type)
  100 +
  101 + def watch_config(self, callback):
  102 + """配置变更监听"""
  103 + self._watchers.append(callback)
  104 +```
  105 +
  106 +## 🔧 代码质量提升
  107 +
  108 +### 1. 类型注解完善
  109 +
  110 +**当前状态:** 部分函数缺少类型注解
  111 +**建议改进:** 全面添加类型提示
  112 +
  113 +```python
  114 +# 建议改进示例
  115 +from typing import Dict, Any, Optional, Callable, Union
  116 +from dataclasses import dataclass
  117 +
  118 +@dataclass
  119 +class LLMResponse:
  120 + content: str
  121 + model: str
  122 + tokens_used: int
  123 + response_time: float
  124 +
  125 +def llm_response(
  126 + message: str,
  127 + nerfreal: BaseReal,
  128 + config: Optional[Dict[str, Any]] = None
  129 +) -> LLMResponse:
  130 + """LLM响应函数,支持多种模型配置"""
  131 + pass
  132 +```
  133 +
  134 +### 2. 错误处理标准化
  135 +
  136 +**当前状态:** 简单的try-catch处理
  137 +**建议改进:** 自定义异常类型和错误处理链
  138 +
  139 +```python
  140 +# 建议实现:llm/exceptions.py
  141 +class LLMException(Exception):
  142 + """LLM基础异常类"""
  143 + pass
  144 +
  145 +class ConfigurationError(LLMException):
  146 + """配置错误"""
  147 + pass
  148 +
  149 +class APIKeyError(LLMException):
  150 + """API密钥错误"""
  151 + pass
  152 +
  153 +class ModelNotFoundError(LLMException):
  154 + """模型不存在错误"""
  155 + pass
  156 +
  157 +# 错误处理装饰器
  158 +def handle_llm_errors(func):
  159 + def wrapper(*args, **kwargs):
  160 + try:
  161 + return func(*args, **kwargs)
  162 + except APIKeyError as e:
  163 + logger.error(f"API密钥错误: {e}")
  164 + return ErrorResponse("API密钥配置错误,请检查配置")
  165 + except ConfigurationError as e:
  166 + logger.error(f"配置错误: {e}")
  167 + return ErrorResponse("配置文件错误,请检查配置")
  168 + return wrapper
  169 +```
  170 +
  171 +### 3. 日志系统增强
  172 +
  173 +**当前状态:** 基础日志记录
  174 +**建议改进:** 结构化日志和链路追踪
  175 +
  176 +```python
  177 +# 建议实现:logger/structured_logger.py
  178 +import structlog
  179 +from datetime import datetime
  180 +import uuid
  181 +
  182 +class LLMLogger:
  183 + def __init__(self):
  184 + self.logger = structlog.get_logger()
  185 +
  186 + def log_request(self, request_id: str, model: str, message: str):
  187 + self.logger.info(
  188 + "llm_request_start",
  189 + request_id=request_id,
  190 + model=model,
  191 + message_length=len(message),
  192 + timestamp=datetime.utcnow().isoformat()
  193 + )
  194 +
  195 + def log_response(self, request_id: str, response_time: float, tokens: int):
  196 + self.logger.info(
  197 + "llm_request_complete",
  198 + request_id=request_id,
  199 + response_time=response_time,
  200 + tokens_used=tokens,
  201 + timestamp=datetime.utcnow().isoformat()
  202 + )
  203 +```
  204 +
  205 +## 🧪 测试策略完善
  206 +
  207 +### 1. 单元测试覆盖
  208 +
  209 +**建议实现:** 完整的测试套件
  210 +
  211 +```python
  212 +# test/test_doubao_integration.py
  213 +import pytest
  214 +from unittest.mock import Mock, patch
  215 +from llm.Doubao import Doubao
  216 +
  217 +class TestDoubaoIntegration:
  218 + @pytest.fixture
  219 + def mock_config(self):
  220 + return {
  221 + "api_key": "test_key",
  222 + "model": "test_model",
  223 + "character": {"name": "测试AI"}
  224 + }
  225 +
  226 + @patch('llm.Doubao.requests.post')
  227 + def test_chat_success(self, mock_post, mock_config):
  228 + # 测试正常对话流程
  229 + mock_response = Mock()
  230 + mock_response.status_code = 200
  231 + mock_response.json.return_value = {"choices": [{"message": {"content": "测试回复"}}]}
  232 + mock_post.return_value = mock_response
  233 +
  234 + doubao = Doubao()
  235 + response = doubao.chat("测试消息")
  236 +
  237 + assert response == "测试回复"
  238 + mock_post.assert_called_once()
  239 +
  240 + def test_api_key_validation(self):
  241 + # 测试API密钥验证
  242 + with pytest.raises(ValueError, match="API密钥未配置"):
  243 + Doubao()
  244 +```
  245 +
  246 +### 2. 集成测试自动化
  247 +
  248 +```python
  249 +# test/integration/test_llm_pipeline.py
  250 +class TestLLMPipeline:
  251 + def test_model_switching(self):
  252 + """测试模型切换功能"""
  253 + # 测试从qwen切换到doubao
  254 + config = {"model_type": "doubao"}
  255 + response = llm_response("测试消息", mock_nerfreal, config)
  256 + assert response is not None
  257 +
  258 + def test_config_hot_reload(self):
  259 + """测试配置热重载"""
  260 + # 修改配置文件
  261 + # 验证配置自动重载
  262 + pass
  263 +```
  264 +
  265 +## 📊 性能监控与优化
  266 +
  267 +### 1. 性能指标收集
  268 +
  269 +```python
  270 +# 建议实现:monitoring/metrics.py
  271 +from dataclasses import dataclass
  272 +from typing import Dict
  273 +import time
  274 +
  275 +@dataclass
  276 +class PerformanceMetrics:
  277 + model_type: str
  278 + init_time: float
  279 + first_token_time: float
  280 + total_response_time: float
  281 + tokens_per_second: float
  282 + memory_usage: float
  283 +
  284 +class MetricsCollector:
  285 + def __init__(self):
  286 + self.metrics_history = []
  287 +
  288 + def collect_metrics(self, metrics: PerformanceMetrics):
  289 + self.metrics_history.append(metrics)
  290 + self._export_to_monitoring_system(metrics)
  291 +
  292 + def get_performance_report(self) -> Dict:
  293 + """生成性能报告"""
  294 + if not self.metrics_history:
  295 + return {}
  296 +
  297 + recent_metrics = self.metrics_history[-100:] # 最近100次请求
  298 + return {
  299 + "avg_response_time": sum(m.total_response_time for m in recent_metrics) / len(recent_metrics),
  300 + "avg_tokens_per_second": sum(m.tokens_per_second for m in recent_metrics) / len(recent_metrics),
  301 + "model_distribution": self._get_model_distribution(recent_metrics)
  302 + }
  303 +```
  304 +
  305 +### 2. 缓存策略
  306 +
  307 +```python
  308 +# 建议实现:cache/llm_cache.py
  309 +from functools import lru_cache
  310 +import hashlib
  311 +import json
  312 +
  313 +class LLMCache:
  314 + def __init__(self, max_size: int = 1000):
  315 + self.cache = {}
  316 + self.max_size = max_size
  317 +
  318 + def get_cache_key(self, message: str, model_config: dict) -> str:
  319 + """生成缓存键"""
  320 + content = f"{message}_{json.dumps(model_config, sort_keys=True)}"
  321 + return hashlib.md5(content.encode()).hexdigest()
  322 +
  323 + def get(self, cache_key: str) -> Optional[str]:
  324 + return self.cache.get(cache_key)
  325 +
  326 + def set(self, cache_key: str, response: str):
  327 + if len(self.cache) >= self.max_size:
  328 + # LRU淘汰策略
  329 + oldest_key = next(iter(self.cache))
  330 + del self.cache[oldest_key]
  331 + self.cache[cache_key] = response
  332 +```
  333 +
  334 +## 🔒 安全性增强
  335 +
  336 +### 1. 敏感信息保护
  337 +
  338 +```python
  339 +# 建议实现:security/secret_manager.py
  340 +import os
  341 +from cryptography.fernet import Fernet
  342 +
  343 +class SecretManager:
  344 + def __init__(self):
  345 + self.cipher_suite = Fernet(self._get_encryption_key())
  346 +
  347 + def _get_encryption_key(self) -> bytes:
  348 + key = os.getenv('ENCRYPTION_KEY')
  349 + if not key:
  350 + key = Fernet.generate_key()
  351 + # 保存到安全位置
  352 + return key.encode() if isinstance(key, str) else key
  353 +
  354 + def encrypt_api_key(self, api_key: str) -> str:
  355 + return self.cipher_suite.encrypt(api_key.encode()).decode()
  356 +
  357 + def decrypt_api_key(self, encrypted_key: str) -> str:
  358 + return self.cipher_suite.decrypt(encrypted_key.encode()).decode()
  359 +```
  360 +
  361 +### 2. 输入验证和清理
  362 +
  363 +```python
  364 +# 建议实现:security/input_validator.py
  365 +import re
  366 +from typing import List
  367 +
  368 +class InputValidator:
  369 + DANGEROUS_PATTERNS = [
  370 + r'<script[^>]*>.*?</script>', # XSS
  371 + r'javascript:', # JavaScript协议
  372 + r'data:text/html', # Data URI
  373 + ]
  374 +
  375 + def validate_message(self, message: str) -> bool:
  376 + """验证用户输入消息"""
  377 + if len(message) > 10000: # 长度限制
  378 + return False
  379 +
  380 + for pattern in self.DANGEROUS_PATTERNS:
  381 + if re.search(pattern, message, re.IGNORECASE):
  382 + return False
  383 +
  384 + return True
  385 +
  386 + def sanitize_message(self, message: str) -> str:
  387 + """清理用户输入"""
  388 + # 移除危险字符
  389 + sanitized = re.sub(r'[<>"\']', '', message)
  390 + return sanitized.strip()
  391 +```
  392 +
  393 +## 📚 文档和规范
  394 +
  395 +### 1. API文档自动生成
  396 +
  397 +```python
  398 +# 建议实现:使用FastAPI自动生成API文档
  399 +from fastapi import FastAPI
  400 +from pydantic import BaseModel
  401 +
  402 +class ChatRequest(BaseModel):
  403 + message: str
  404 + model_type: str = "doubao"
  405 + stream: bool = True
  406 +
  407 +class ChatResponse(BaseModel):
  408 + response: str
  409 + model: str
  410 + tokens_used: int
  411 + response_time: float
  412 +
  413 +app = FastAPI(title="LLM Chat API", version="1.0.0")
  414 +
  415 +@app.post("/chat", response_model=ChatResponse)
  416 +async def chat_endpoint(request: ChatRequest):
  417 + """聊天接口
  418 +
  419 + 支持多种LLM模型的聊天功能:
  420 + - 豆包模型:高质量中文对话
  421 + - 通义千问:阿里云大模型
  422 + """
  423 + pass
  424 +```
  425 +
  426 +### 2. 代码规范检查
  427 +
  428 +```yaml
  429 +# 建议添加:.pre-commit-config.yaml
  430 +repos:
  431 + - repo: https://github.com/psf/black
  432 + rev: 22.3.0
  433 + hooks:
  434 + - id: black
  435 + language_version: python3.8
  436 +
  437 + - repo: https://github.com/pycqa/flake8
  438 + rev: 4.0.1
  439 + hooks:
  440 + - id: flake8
  441 + args: [--max-line-length=88]
  442 +
  443 + - repo: https://github.com/pre-commit/mirrors-mypy
  444 + rev: v0.950
  445 + hooks:
  446 + - id: mypy
  447 + additional_dependencies: [types-requests]
  448 +```
  449 +
  450 +## 🚀 部署和运维
  451 +
  452 +### 1. 容器化部署
  453 +
  454 +```dockerfile
  455 +# 建议改进:Dockerfile.llm
  456 +FROM python:3.9-slim
  457 +
  458 +WORKDIR /app
  459 +
  460 +# 安装依赖
  461 +COPY requirements.txt .
  462 +RUN pip install --no-cache-dir -r requirements.txt
  463 +
  464 +# 复制代码
  465 +COPY llm/ ./llm/
  466 +COPY config/ ./config/
  467 +COPY *.py ./
  468 +
  469 +# 健康检查
  470 +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  471 + CMD python -c "from llm import llm_response; print('OK')" || exit 1
  472 +
  473 +CMD ["python", "app.py"]
  474 +```
  475 +
  476 +### 2. 监控和告警
  477 +
  478 +```python
  479 +# 建议实现:monitoring/health_check.py
  480 +class HealthChecker:
  481 + def __init__(self):
  482 + self.checks = {
  483 + 'config_files': self._check_config_files,
  484 + 'model_availability': self._check_model_availability,
  485 + 'api_connectivity': self._check_api_connectivity,
  486 + }
  487 +
  488 + def run_health_check(self) -> Dict[str, bool]:
  489 + results = {}
  490 + for check_name, check_func in self.checks.items():
  491 + try:
  492 + results[check_name] = check_func()
  493 + except Exception as e:
  494 + logger.error(f"Health check {check_name} failed: {e}")
  495 + results[check_name] = False
  496 + return results
  497 +
  498 + def _check_config_files(self) -> bool:
  499 + required_files = ['config/llm_config.json', 'config/doubao_config.json']
  500 + return all(os.path.exists(f) for f in required_files)
  501 +```
  502 +
  503 +## 📈 实施优先级
  504 +
  505 +### 高优先级(立即实施)
  506 +1. ✅ 类型注解完善
  507 +2. ✅ 错误处理标准化
  508 +3. ✅ 单元测试覆盖
  509 +4. ✅ 输入验证和清理
  510 +
  511 +### 中优先级(1-2周内)
  512 +1. 🔄 策略模式重构
  513 +2. 🔄 配置管理中心化
  514 +3. 🔄 性能监控系统
  515 +4. 🔄 缓存策略实施
  516 +
  517 +### 低优先级(长期规划)
  518 +1. ⏳ 依赖注入容器
  519 +2. ⏳ 微服务架构拆分
  520 +3. ⏳ 分布式缓存
  521 +4. ⏳ 自动化运维
  522 +
  523 +## 总结
  524 +
  525 +这些建议基于当前豆包模型集成的成功经验,旨在建立一个可扩展、可维护、高性能的LLM服务架构。建议按优先级逐步实施,确保每个改进都经过充分测试和验证。
  526 +
  527 +通过这些改进,系统将具备:
  528 +- 🏗️ 更好的架构设计
  529 +- 🔧 更高的代码质量
  530 +- 🧪 更完善的测试覆盖
  531 +- 📊 更强的性能监控
  532 +- 🔒 更好的安全保障
  533 +- 📚 更完整的文档
  534 +- 🚀 更便捷的部署运维
  535 +
  536 +---
  537 +
  538 +**开发者**: AIfeng
  539 +**更新时间**: 2024-12-19
  540 +**版本**: 1.0.0
  1 +# AIfeng/2025-06-26
  2 +
  3 +# 豆包大模型集成使用指南
  4 +
  5 +## 概述
  6 +
  7 +本项目已成功集成火山引擎豆包大模型,支持与原有通义千问模型无缝切换。豆包模型提供了丰富的配置选项,包括人物设定、对话风格、API 参数等,全部采用配置文件管理。
  8 +
  9 +## 快速开始
  10 +
  11 +### 1. 获取 API 密钥
  12 +
  13 +访问 [火山引擎控制台](https://console.volcengine.com/) 获取豆包 API 密钥:
  14 +
  15 +1. 登录火山引擎控制台
  16 +2. 进入"豆包大模型"服务
  17 +3. 创建 API 密钥
  18 +4. 复制 API Key 和模型端点 ID
  19 +
  20 +### 2. 配置环境变量
  21 +
  22 +设置豆包 API 密钥环境变量:
  23 +
  24 +```bash
  25 +# Windows
  26 +set DOUBAO_API_KEY=your_api_key_here
  27 +
  28 +# Linux/Mac
  29 +export DOUBAO_API_KEY=your_api_key_here
  30 +```
  31 +
  32 +### 3. 配置模型选择
  33 +
  34 +编辑 `config/llm_config.json`,设置使用豆包模型:
  35 +
  36 +```json
  37 +{
  38 + "model_type": "doubao"
  39 +}
  40 +```
  41 +
  42 +### 4. 自定义豆包配置
  43 +
  44 +编辑 `config/doubao_config.json` 配置文件:
  45 +
  46 +```json
  47 +{
  48 + "model": "your_endpoint_id_here",
  49 + "api_key": "可选:直接在配置文件中设置",
  50 + "character": {
  51 + "name": "小艺",
  52 + "personality": "友善、专业、有趣",
  53 + "background": "AI语音聊天机器人"
  54 + }
  55 +}
  56 +```
  57 +
  58 +## 配置文件详解
  59 +
  60 +### LLM 统一配置 (`config/llm_config.json`)
  61 +
  62 +```json
  63 +{
  64 + "model_type": "doubao", // 模型类型:"qwen" "doubao"
  65 + "description": "LLM模型配置文件",
  66 + "models": {
  67 + "qwen": {
  68 + "name": "通义千问",
  69 + "api_key_env": "DASHSCOPE_API_KEY"
  70 + },
  71 + "doubao": {
  72 + "name": "豆包大模型",
  73 + "config_file": "config/doubao_config.json"
  74 + }
  75 + },
  76 + "settings": {
  77 + "stream": true,
  78 + "sentence_split_chars": ",.!;:,。!?:;",
  79 + "min_sentence_length": 10,
  80 + "log_performance": true
  81 + }
  82 +}
  83 +```
  84 +
  85 +### 豆包详细配置 (`config/doubao_config.json`)
  86 +
  87 +#### API 配置
  88 +
  89 +```json
  90 +{
  91 + "api_key": "可选:API密钥(推荐使用环境变量)",
  92 + "base_url": "https://ark.cn-beijing.volces.com/api/v3",
  93 + "model": "ep-20241219000000-xxxxx", // 您的模型端点ID
  94 + "stream": true,
  95 + "max_tokens": 2048,
  96 + "temperature": 0.7,
  97 + "top_p": 0.9
  98 +}
  99 +```
  100 +
  101 +#### 人物设定
  102 +
  103 +```json
  104 +{
  105 + "character": {
  106 + "name": "小艺",
  107 + "personality": "友善、专业、有趣的AI助手",
  108 + "background": "由艺云展陈开发的AI语音聊天机器人",
  109 + "speaking_style": "简洁明了,富有亲和力",
  110 + "expertise": ["日常对话", "信息查询", "问题解答"],
  111 + "constraints": ["保持礼貌", "提供准确信息", "避免敏感话题"]
  112 + }
  113 +}
  114 +```
  115 +
  116 +#### 响应配置
  117 +
  118 +```json
  119 +{
  120 + "response_config": {
  121 + "max_response_length": 500,
  122 + "response_format": "conversational",
  123 + "enable_context_memory": true,
  124 + "context_window_size": 10
  125 + }
  126 +}
  127 +```
  128 +
  129 +## 使用方式
  130 +
  131 +### 模型切换
  132 +
  133 +1. **切换到豆包模型**
  134 +
  135 + ```json
  136 + // config/llm_config.json
  137 + { "model_type": "doubao" }
  138 + ```
  139 +
  140 +2. **切换到通义千问**
  141 +
  142 + ```json
  143 + // config/llm_config.json
  144 + { "model_type": "qwen" }
  145 + ```
  146 +
  147 +3. **重启应用**使配置生效
  148 +
  149 +### 人物设定自定义
  150 +
  151 +编辑 `config/doubao_config.json` 中的 `character` 部分:
  152 +
  153 +```json
  154 +{
  155 + "character": {
  156 + "name": "您的AI助手名称",
  157 + "personality": "描述AI的性格特点",
  158 + "background": "AI的背景设定",
  159 + "speaking_style": "对话风格描述",
  160 + "expertise": ["专长领域1", "专长领域2"],
  161 + "constraints": ["行为约束1", "行为约束2"]
  162 + }
  163 +}
  164 +```
  165 +
  166 +### API 参数调优
  167 +
  168 +根据需要调整以下参数:
  169 +
  170 +- **temperature** (0.0-1.0):控制回复的随机性
  171 +
  172 + - 0.0:最确定性的回复
  173 + - 1.0:最随机的回复
  174 + - 推荐:0.7
  175 +
  176 +- **top_p** (0.0-1.0):控制词汇选择的多样性
  177 +
  178 + - 推荐:0.9
  179 +
  180 +- **max_tokens**:最大回复长度
  181 + - 推荐:1024-2048
  182 +
  183 +## 性能监控
  184 +
  185 +系统会自动记录以下性能指标:
  186 +
  187 +- 模型初始化时间
  188 +- 首个 token 响应时间
  189 +- 总响应时间
  190 +- 分句输出日志
  191 +
  192 +查看日志了解性能表现:
  193 +
  194 +```
  195 +豆包模型初始化时间: 0.123s
  196 +豆包首个token时间: 0.456s
  197 +豆包总响应时间: 2.789s
  198 +```
  199 +
  200 +## 故障排除
  201 +
  202 +### 常见问题
  203 +
  204 +1. **API 密钥错误**
  205 +
  206 + ```
  207 + ValueError: 豆包API密钥未配置
  208 + ```
  209 +
  210 + 解决:检查环境变量 `DOUBAO_API_KEY` 或配置文件中的 `api_key`
  211 +
  212 +2. **模型端点错误**
  213 +
  214 + ```
  215 + HTTP 404: 模型不存在
  216 + ```
  217 +
  218 + 解决:检查 `config/doubao_config.json` 中的 `model` 字段
  219 +
  220 +3. **配置文件格式错误**
  221 +
  222 + ```
  223 + json.JSONDecodeError
  224 + ```
  225 +
  226 + 解决:使用 JSON 验证工具检查配置文件格式
  227 +
  228 +4. **模块导入失败**
  229 + ```
  230 + ModuleNotFoundError: No module named 'llm.Doubao'
  231 + ```
  232 + 解决:确保 `llm/__init__.py` 文件存在
  233 +
  234 +### 调试模式
  235 +
  236 +运行测试脚本验证集成:
  237 +
  238 +```bash
  239 +python test_doubao_integration.py
  240 +```
  241 +
  242 +## 最佳实践
  243 +
  244 +1. **安全性**
  245 +
  246 + - 使用环境变量存储 API 密钥
  247 + - 不要在代码中硬编码敏感信息
  248 + - 定期轮换 API 密钥
  249 +
  250 +2. **性能优化**
  251 +
  252 + - 根据应用场景调整 `max_tokens`
  253 + - 使用流式响应提升用户体验
  254 + - 监控 API 调用频率和成本
  255 +
  256 +3. **配置管理**
  257 +
  258 + - 为不同环境维护不同的配置文件
  259 + - 使用版本控制管理配置变更
  260 + - 定期备份配置文件
  261 +
  262 +4. **人物设定**
  263 + - 明确定义 AI 的角色和能力边界
  264 + - 设置合适的对话风格和语调
  265 + - 定期根据用户反馈优化设定
  266 +
  267 +## 技术支持
  268 +
  269 +如遇到问题,请:
  270 +
  271 +1. 查看应用日志获取详细错误信息
  272 +2. 运行集成测试脚本诊断问题
  273 +3. 检查配置文件格式和内容
  274 +4. 验证 API 密钥和网络连接
  275 +
  276 +---
  277 +
  278 +**开发者**: AIfeng
  279 +**更新时间**: 2025-06-26
  280 +**版本**: 1.0.0
  1 +# AIfeng/2025-01-27
  2 +
  3 +# FunASR集成分析与优化方案
  4 +
  5 +## 问题分析
  6 +
  7 +### 1. 当前状况
  8 +- **ASR_server.py**: 实际启动的是**WebSocket服务**,而非gRPC服务(注释中的"grpc server port"是误导性的)
  9 +- **服务协议**: 使用`websockets`库提供WebSocket接口,监听指定host:port
  10 +- **依赖缺失**: `funasr.py`和`ali_nls.py`引用的`core`、`utils`模块在当前项目中不存在
  11 +
  12 +### 2. 技术架构分析
  13 +
  14 +#### ASR_server.py实现要点
  15 +```python
  16 +# WebSocket服务器,非gRPC
  17 +server = await websockets.serve(ws_serve, args.host, args.port, ping_interval=10)
  18 +
  19 +# 模型初始化
  20 +asr_model = AutoModel(
  21 + model="paraformer-zh", model_revision="v2.0.4",
  22 + vad_model="fsmn-vad", vad_model_revision="v2.0.4",
  23 + punc_model="ct-punc-c", punc_model_revision="v2.0.4",
  24 + device=f"cuda:{args.gpu_id}" if args.ngpu else "cpu"
  25 +)
  26 +```
  27 +
  28 +#### 连接方式
  29 +- **协议**: WebSocket (ws://)
  30 +- **默认端口**: 10197
  31 +- **消息格式**: JSON + 二进制音频数据
  32 +- **处理模式**: 异步队列处理音频文件
  33 +
  34 +## 集成方案
  35 +
  36 +### 方案一:简化集成(推荐)
  37 +
  38 +创建独立的FunASR客户端,避免依赖缺失问题:
  39 +
  40 +```python
  41 +# funasr_simple_client.py
  42 +import asyncio
  43 +import websockets
  44 +import json
  45 +import wave
  46 +import threading
  47 +from queue import Queue
  48 +
  49 +class SimpleFunASRClient:
  50 + def __init__(self, host="127.0.0.1", port=10197):
  51 + self.host = host
  52 + self.port = port
  53 + self.websocket = None
  54 + self.result_queue = Queue()
  55 + self.connected = False
  56 +
  57 + async def connect(self):
  58 + uri = f"ws://{self.host}:{self.port}"
  59 + try:
  60 + self.websocket = await websockets.connect(uri)
  61 + self.connected = True
  62 + # 启动消息接收线程
  63 + threading.Thread(target=self._receive_messages, daemon=True).start()
  64 + return True
  65 + except Exception as e:
  66 + print(f"连接失败: {e}")
  67 + return False
  68 +
  69 + def _receive_messages(self):
  70 + async def receive():
  71 + try:
  72 + async for message in self.websocket:
  73 + if isinstance(message, str):
  74 + self.result_queue.put(message)
  75 + except Exception as e:
  76 + print(f"接收消息错误: {e}")
  77 + self.connected = False
  78 +
  79 + asyncio.run(receive())
  80 +
  81 + async def recognize_file(self, wav_path):
  82 + if not self.connected:
  83 + return None
  84 +
  85 + # 发送文件路径进行识别
  86 + message = {"url": wav_path}
  87 + await self.websocket.send(json.dumps(message))
  88 +
  89 + # 等待结果
  90 + try:
  91 + result = self.result_queue.get(timeout=10)
  92 + return result
  93 + except:
  94 + return None
  95 +
  96 + async def close(self):
  97 + if self.websocket:
  98 + await self.websocket.close()
  99 + self.connected = False
  100 +```
  101 +
  102 +### 方案二:修复现有代码
  103 +
  104 +创建缺失的配置模块:
  105 +
  106 +```python
  107 +# config_util.py
  108 +class Config:
  109 + # ASR配置
  110 + local_asr_ip = "127.0.0.1"
  111 + local_asr_port = 10197
  112 +
  113 + # 阿里云NLS配置
  114 + key_ali_nls_key_id = "your_key_id"
  115 + key_ali_nls_key_secret = "your_key_secret"
  116 + key_ali_nls_app_key = "your_app_key"
  117 +
  118 +# 创建配置实例
  119 +config = Config()
  120 +
  121 +# 为了兼容原代码的导入方式
  122 +local_asr_ip = config.local_asr_ip
  123 +local_asr_port = config.local_asr_port
  124 +key_ali_nls_key_id = config.key_ali_nls_key_id
  125 +key_ali_nls_key_secret = config.key_ali_nls_key_secret
  126 +key_ali_nls_app_key = config.key_ali_nls_app_key
  127 +```
  128 +
  129 +## 连接验证
  130 +
  131 +### 测试连接脚本
  132 +
  133 +```python
  134 +# test_funasr_connection.py
  135 +import asyncio
  136 +import websockets
  137 +import json
  138 +
  139 +async def test_connection():
  140 + uri = "ws://127.0.0.1:10197"
  141 + try:
  142 + async with websockets.connect(uri) as websocket:
  143 + print("✅ FunASR服务连接成功")
  144 +
  145 + # 测试发送消息
  146 + test_message = {"url": "test.wav"}
  147 + await websocket.send(json.dumps(test_message))
  148 + print("✅ 消息发送成功")
  149 +
  150 + # 等待响应
  151 + try:
  152 + response = await asyncio.wait_for(websocket.recv(), timeout=5)
  153 + print(f"✅ 收到响应: {response}")
  154 + except asyncio.TimeoutError:
  155 + print("⚠️ 未收到响应(可能因为测试文件不存在)")
  156 +
  157 + except ConnectionRefusedError:
  158 + print("❌ 连接被拒绝,请确认FunASR服务已启动")
  159 + except Exception as e:
  160 + print(f"❌ 连接失败: {e}")
  161 +
  162 +if __name__ == "__main__":
  163 + asyncio.run(test_connection())
  164 +```
  165 +
  166 +## 部署建议
  167 +
  168 +### 1. 服务启动
  169 +```bash
  170 +# CPU模式
  171 +python -u ASR_server.py --host "127.0.0.1" --port 10197 --ngpu 0
  172 +
  173 +# GPU模式
  174 +python -u ASR_server.py --host "127.0.0.1" --port 10197 --ngpu 1 --gpu_id 0
  175 +```
  176 +
  177 +### 2. 依赖安装
  178 +```bash
  179 +pip install torch modelscope websockets FunASR torchaudio
  180 +```
  181 +
  182 +### 3. 热词配置
  183 +创建`data/hotword.txt`文件,每行一个热词:
  184 +```
  185 +数字人
  186 +语音识别
  187 +实时对话
  188 +```
  189 +
  190 +## 性能优化
  191 +
  192 +### 1. 模型优化
  193 +- **量化**: 使用INT8量化减少内存占用
  194 +- **批处理**: 支持批量音频处理
  195 +- **缓存**: 模型预加载和结果缓存
  196 +
  197 +### 2. 网络优化
  198 +- **连接池**: 维护WebSocket连接池
  199 +- **重连机制**: 自动重连和错误恢复
  200 +- **负载均衡**: 多实例部署
  201 +
  202 +### 3. 监控指标
  203 +- 连接数量
  204 +- 处理延迟
  205 +- 识别准确率
  206 +- 资源使用率
  207 +
  208 +## 技术债务
  209 +
  210 +1. **依赖管理**: 原有代码依赖外部模块,需要重构或补全
  211 +2. **错误处理**: 缺少完善的异常处理和重连机制
  212 +3. **配置管理**: 硬编码配置需要外部化
  213 +4. **日志系统**: 缺少结构化日志记录
  214 +5. **测试覆盖**: 需要添加单元测试和集成测试
  215 +
  216 +## 实施建议
  217 +
  218 +1. **短期**: 使用简化客户端快速集成
  219 +2. **中期**: 修复依赖问题,完善错误处理
  220 +3. **长期**: 重构为微服务架构,支持多模型切换
  221 +
  222 +## 结论
  223 +
  224 +FunASR服务使用WebSocket协议,可以正常连接。主要问题是现有集成代码缺少依赖模块。建议采用简化集成方案快速解决连接问题,后续逐步完善架构。
  1 +# 更新日志
  2 +
  3 +## 2025-05-27 - SessionId管理机制优化 - AIfeng
  4 +
  5 +### 功能增强:SessionId持久化存储和恢复机制
  6 +
  7 +**问题背景:**
  8 +- 启动日志显示sessionId是在数字人模型启动时创建的
  9 +- 页面刷新后sessionId重置为0,与后端运行的数字人会话不一致
  10 +- 缺少sessionId的持久化管理机制
  11 +- 用户无法手动重置会话连接
  12 +
  13 +**解决方案:**
  14 +1. **SessionId本地存储**
  15 + - 实现sessionId的localStorage持久化存储
  16 + - 页面刷新后自动恢复之前的sessionId
  17 + - 显示当前会话ID状态
  18 +
  19 +2. **会话状态管理**
  20 + - 新增可视化的当前会话ID显示框
  21 + - 实现会话重置功能,支持手动清除sessionId
  22 + - 自动重连机制,页面刷新后尝试恢复WebSocket连接
  23 +
  24 +3. **用户交互优化**
  25 + - 添加"重置"按钮,允许用户手动断开并重新连接
  26 + - 提供确认对话框,防止误操作
  27 + - 增强状态提示和用户反馈
  28 +
  29 +**技术实现:**
  30 +```javascript
  31 +// SessionId管理功能
  32 +function saveSessionId(sessionId) {
  33 + localStorage.setItem('currentSessionId', sessionId);
  34 + document.getElementById('current-sessionid').value = sessionId;
  35 +}
  36 +
  37 +function restoreSessionId() {
  38 + var savedSessionId = localStorage.getItem('currentSessionId');
  39 + if (savedSessionId && savedSessionId !== '0') {
  40 + document.getElementById('sessionid').value = savedSessionId;
  41 + return savedSessionId;
  42 + }
  43 + return null;
  44 +}
  45 +```
  46 +
  47 +**界面改进:**
  48 +- 新增"当前会话ID"显示框,实时显示连接状态
  49 +- 添加"重置"按钮,支持会话管理操作
  50 +- 优化用户体验,提供清晰的状态反馈
  51 +
  52 +---
  53 +
  54 +## 2025-01-27 - FunASR集成分析与连接验证 - AIfeng
  55 +
  56 +### 技术分析:FunASR服务架构与集成方案
  57 +
  58 +**问题识别:**
  59 +- 用户从其他项目拷贝的FunASR相关文件存在依赖缺失问题
  60 +- ASR_server.py实际提供WebSocket服务,而非gRPC服务
  61 +- funasr.py和ali_nls.py引用的core、utils模块在当前项目中不存在
  62 +- 需要验证本地FunASR服务的连接可行性
  63 +
  64 +**技术架构分析:**
  65 +1. **服务协议确认**
  66 + - ASR_server.py使用websockets库提供WebSocket接口
  67 + - 默认监听端口10197,支持CPU/GPU模式切换
  68 + - 消息格式:JSON字符串 + 二进制音频数据
  69 + - 异步队列处理音频文件识别请求
  70 +
  71 +2. **模型配置**
  72 + - 使用FunASR的paraformer-zh模型进行中文语音识别
  73 + - 集成VAD(语音活动检测)和标点预测功能
  74 + - 支持热词配置,提升特定领域识别准确率
  75 +
  76 +3. **依赖问题分析**
  77 + - 缺失模块:core.wsa_server, utils.config_util, utils.util
  78 + - 现有代码无法直接运行,需要重构或补全依赖
  79 +
  80 +**解决方案设计:**
  81 +1. **简化集成方案(推荐)**
  82 + - 创建独立的SimpleFunASRClient类
  83 + - 避免复杂依赖,直接使用WebSocket通信
  84 + - 提供异步音频识别接口
  85 +
  86 +2. **连接验证工具**
  87 + - 开发test_funasr_connection.py测试脚本
  88 + - 支持基本连接测试、音频识别测试、消息格式验证
  89 + - 自动生成测试音频文件,验证完整流程
  90 +
  91 +**技术实现要点:**
  92 +```python
  93 +# 简化客户端实现
  94 +class SimpleFunASRClient:
  95 + async def connect(self):
  96 + uri = f"ws://{self.host}:{self.port}"
  97 + self.websocket = await websockets.connect(uri)
  98 +
  99 + async def recognize_file(self, wav_path):
  100 + message = {"url": wav_path}
  101 + await self.websocket.send(json.dumps(message))
  102 + return await self.websocket.recv()
  103 +```
  104 +
  105 +**部署指导:**
  106 +- 启动命令:`python -u ASR_server.py --host "127.0.0.1" --port 10197 --ngpu 0`
  107 +- 依赖安装:torch, modelscope, websockets, FunASR, torchaudio
  108 +- 热词配置:创建data/hotword.txt文件
  109 +
  110 +**性能优化建议:**
  111 +1. 模型量化减少内存占用
  112 +2. WebSocket连接池管理
  113 +3. 自动重连和错误恢复机制
  114 +4. 结构化日志和监控指标
  115 +
  116 +**技术债务识别:**
  117 +- 原有代码依赖外部模块,需要重构
  118 +- 缺少完善的异常处理机制
  119 +- 配置管理硬编码,需要外部化
  120 +- 缺少单元测试和集成测试覆盖
  121 +
  122 +**集成建议:**
  123 +- 短期:使用简化客户端快速验证连接
  124 +- 中期:修复依赖问题,完善错误处理
  125 +- 长期:重构为微服务架构,支持多模型切换
  126 +
  127 +---
  128 +
  129 +# 2025-01-27 FunASR测试增强 - 实际音频文件测试功能
  130 +**AIfeng/2025-01-27**
  131 +
  132 +## 问题背景
  133 +用户提供了三个实际音频文件(yunxi.mp3、yunxia.mp3、yunyang.mp3),需要在现有测试脚本中增加对真实音频文件的识别测试功能,以验证FunASR服务的实际效果。
  134 +
  135 +## 技术实现
  136 +### 新增测试方法
  137 +- **test_real_audio_files()**: 专门测试实际音频文件的识别功能
  138 + - 支持批量测试多个音频文件
  139 + - 文件存在性检查
  140 + - 30秒超时保护机制
  141 + - 详细的识别结果解析和展示
  142 + - 错误处理和状态分类
  143 +
  144 +### 测试流程优化
  145 +- 将实际音频文件测试集成到主测试序列中
  146 +- 测试顺序:基础连接 → 音频识别 → **实际音频文件** → 消息格式
  147 +- 增加文件间等待机制,避免服务器压力
  148 +
  149 +### 功能特性
  150 +1. **智能文件检测**: 自动检查音频文件是否存在
  151 +2. **多格式响应处理**: 支持JSON和纯文本响应解析
  152 +3. **详细状态分类**: success/received/timeout/error四种状态
  153 +4. **可视化结果展示**: 使用emoji和格式化输出提升可读性
  154 +5. **超时保护**: 30秒超时机制防止测试卡死
  155 +
  156 +### 测试结果输出
  157 +```
  158 +📊 实际音频文件测试总结:
  159 +1. 文件: yunxi.mp3
  160 + ✅ 识别成功: [识别文本]
  161 +2. 文件: yunxia.mp3
  162 + ✅ 识别成功: [识别文本]
  163 +3. 文件: yunyang.mp3
  164 + ✅ 识别成功: [识别文本]
  165 +```
  166 +
  167 +## 技术要点
  168 +- **异步处理**: 使用asyncio.wait_for实现超时控制
  169 +- **错误容错**: 完善的异常处理机制
  170 +- **资源管理**: 每个文件独立WebSocket连接,避免状态污染
  171 +- **性能优化**: 文件间1秒等待,平衡测试效率和服务器负载
  172 +
  173 +## 使用方法
  174 +```bash
  175 +# 确保音频文件在项目根目录
  176 +# 启动FunASR服务
  177 +python -u ASR_server.py --host "127.0.0.1" --port 10197 --ngpu 0
  178 +
  179 +# 运行增强测试
  180 +python test_funasr_connection.py
  181 +```
  182 +
  183 +## 技术价值
  184 +- **验证真实场景**: 从测试WAV文件升级到实际音频文件测试
  185 +- **提升测试覆盖**: 增加对MP3格式和真实语音内容的测试
  186 +- **改善用户体验**: 直观的测试结果展示和状态反馈
  187 +- **增强可维护性**: 模块化的测试方法设计
  188 +
  189 +---
  190 +
  191 +## 2024-12-19 ASR/TTS技术架构分析与扩展方案设计 - AIfeng
  192 +
  193 +### 新增文档
  194 +- `doc/dev/asr_tts_architecture_analysis.md` - ASR/TTS技术架构分析与扩展方案
  195 +
  196 +### 技术分析内容
  197 +1. **ASR技术实现分析**
  198 + - 基于BaseASR的模块化架构设计
  199 + - 4种ASR实现: NerfASR、MuseASR、HubertASR、LipASR
  200 + - Web端实时ASR基于WebSocket + Web Audio API
  201 + - 音频处理流水线: 16kHz采样率,20ms帧长度
  202 +
  203 +2. **TTS技术实现分析**
  204 + - 基于BaseTTS的统一框架
  205 + - 6种TTS服务: EdgeTTS、FishTTS、SovitsTTS、CosyVoiceTTS、TencentTTS、XTTS
  206 + - 异步流式处理架构
  207 + - 统一16kHz音频输出
  208 +
  209 +3. **扩展方案设计**
  210 + - 第三方ASR集成: 百度、阿里云、腾讯云、Whisper、SenseVoice
  211 + - 第三方TTS集成: 百度、Azure、Coqui、PaddleSpeech
  212 + - 本地离线服务Docker容器化部署
  213 + - 微服务架构重构方案
  214 +
  215 +4. **性能优化策略**
  216 + - 模型量化和缓存机制
  217 + - 流式处理和异步优化
  218 + - GPU资源调度和负载均衡
  219 +
  220 +### 实施建议
  221 +- Phase 1: 基础扩展(1-2周) - Whisper ASR + 云端TTS
  222 +- Phase 2: 服务化改造(2-3周) - Docker容器化
  223 +- Phase 3: 性能优化(2-3周) - 模型优化
  224 +- Phase 4: 生产就绪(1-2周) - 部署自动化
  225 +
  226 +### 技术债务识别
  227 +- 当前ASR/TTS耦合度较高,需要服务化解耦
  228 +- 缺乏统一的配置管理机制
  229 +- 性能监控和告警体系待建设
  230 +
  231 +**分析人员**: AIfeng
  232 +**工作类型**: 技术架构分析
  233 +**影响范围**: ASR/TTS模块
  234 +
  235 +**验证结果:**
  236 +- ✅ 页面刷新后sessionId自动恢复
  237 +- ✅ 与后端数字人会话保持一致性
  238 +- ✅ 支持手动会话重置和重连
  239 +- ✅ 提升用户操作便利性
  240 +
  241 +---
  242 +
  243 +## 2025-05-27 - WebSocket连接时序问题修复 - AIfeng
  244 +
  245 +### 问题修复:刷新页面后sessionId未被websocket_connections接收
  246 +
  247 +**问题描述:**
  248 +- 刷新页面重新连接数字人后,新的sessionId没有被websocket_connections正确接收
  249 +- WebRTC连接建立和WebSocket连接存在时序问题
  250 +- sessionId在WebSocket登录时可能仍为0,导致连接关联失败
  251 +
  252 +**根本原因:**
  253 +- WebSocket连接建立速度快于WebRTC协商完成
  254 +- negotiate()函数设置sessionId到DOM元素存在异步延迟
  255 +- connectWebSocket()函数立即读取sessionId值,可能获取到初始值0
  256 +
  257 +**修复方案:**
  258 +1. **实现重试机制**
  259 + - 在WebSocket连接建立后,等待sessionId正确设置
  260 + - 最多重试20次,每次间隔200ms
  261 + - 总等待时间不超过4秒
  262 +
  263 +2. **增强日志输出**
  264 + - 详细记录sessionId获取过程
  265 + - 标记重试次数和等待状态
  266 + - 区分正常连接和异常情况
  267 +
  268 +3. **容错处理**
  269 + - 即使sessionId为0也允许连接,但记录错误日志
  270 + - 避免因时序问题完全阻断WebSocket连接
  271 +
  272 +**技术实现:**
  273 +```javascript
  274 +function attemptLogin(retryCount = 0) {
  275 + var sessionid = parseInt(document.getElementById('sessionid').value) || 0;
  276 +
  277 + if (sessionid === 0 && retryCount < 20) {
  278 + console.log(`等待sessionid设置,重试次数: ${retryCount + 1}/20`);
  279 + setTimeout(() => attemptLogin(retryCount + 1), 200);
  280 + return;
  281 + }
  282 +
  283 + // 发送登录消息逻辑...
  284 +}
  285 +```
  286 +
  287 +**验证结果:**
  288 +- ✅ 解决刷新页面后sessionId时序问题
  289 +- ✅ 确保新sessionId正确收入websocket_connections
  290 +- ✅ 提升WebSocket连接稳定性
  291 +- ✅ 增强错误诊断能力
  292 +
  293 +---
  294 +
  295 +## 2025-06-27 - WebSocket消息处理逻辑重构 - AIfeng
  296 +
  297 +### 问题修复:前端消息显示不一致
  298 +
  299 +**问题描述:**
  300 +- 对话框中仅显示页面端发出的数据,缺少第三方服务推送的消息
  301 +- 用户消息和AI回复未通过WebSocket统一推送
  302 +- 语音输入消息直接添加到界面,未等待服务器确认
  303 +- 缺少不同大模型的标识区分
  304 +
  305 +**修复方案:**
  306 +1. **统一消息推送机制**
  307 + - 移除前端直接添加消息到界面的逻辑
  308 + - 所有消息(用户输入、语音输入、AI回复)均通过WebSocket推送
  309 + - 添加`X-Request-Source: 'web'`头部标识消息来源
  310 +
  311 +2. **新增聊天消息处理**
  312 + - 添加`chat_message`类型的WebSocket消息处理
  313 + - 支持消息发送者识别(user/human/ai/assistant)
  314 + - 集成模型信息和请求来源显示
  315 +
  316 +3. **本地存储增强**
  317 + - 自动保存聊天记录到本地存储
  318 + - 支持按sessionId区分不同会话
  319 + - 记录时间戳、模型信息等元数据
  320 +
  321 +**技术实现:**
  322 +```javascript
  323 +// WebSocket聊天消息处理
  324 +if (messageData.type === 'chat_message') {
  325 + var alignment = sender === 'user' ? 'right' : 'left';
  326 + var senderLabel = modelInfo ? `AI回复(${modelInfo})` : 'AI回复';
  327 + addMessage(messageContent, alignment, senderLabel, messageMode, modelInfo, requestSource);
  328 +}
  329 +
  330 +// 移除直接添加消息逻辑
  331 +fetch('/human', {
  332 + headers: {
  333 + 'Content-Type': 'application/json',
  334 + 'X-Request-Source': 'web'
  335 + }
  336 +});
  337 +```
  338 +
  339 +**验证结果:**
  340 +- ✅ 前端完全依赖WebSocket接收消息
  341 +- ✅ 支持第三方服务推送消息显示
  342 +- ✅ 语音输入通过服务器确认后显示
  343 +
  344 +---
  345 +
  346 +## 2025-01-27 - WebSocket连接时序问题修复 - AIfeng
  347 +
  348 +### 问题修复:刷新页面后新sessionId未加入websocket_connections
  349 +
  350 +**问题描述:**
  351 +- 页面刷新后,WebSocket连接在页面加载时立即建立
  352 +- 此时sessionId仍为默认值0,WebRTC连接尚未建立
  353 +- 真正的sessionId在WebRTC连接建立后才从服务器获取
  354 +- 导致新会话的WebSocket连接无法正确关联到websocket_connections
  355 +
  356 +**根本原因:**
  357 +连接建立时序错误:WebSocket连接 → sessionId获取,应该是:sessionId获取 → WebSocket连接
  358 +
  359 +**修复方案:**
  360 +1. **调整连接时序**
  361 + - 移除页面加载时的自动WebSocket连接
  362 + - 在WebRTC连接建立并获得sessionId后触发WebSocket连接
  363 +
  364 +2. **前端逻辑优化**
  365 + - 修改`client.js`:在设置sessionId后触发WebSocket连接
  366 + - 修改`webrtcapichat.html`:移除页面初始化时的connectWebSocket调用
  367 + - 添加sessionId有效性验证和警告日志
  368 +
  369 +3. **保持重连机制**
  370 + - 保留页面可见性变化时的重连逻辑
  371 + - 保留网络异常时的自动重连机制
  372 +
  373 +**技术实现:**
  374 +```javascript
  375 +// client.js - 在获得sessionId后触发WebSocket连接
  376 +.then((answer) => {
  377 + document.getElementById('sessionid').value = answer.sessionid
  378 + console.log('SessionID已设置:', answer.sessionid);
  379 +
  380 + if (typeof connectWebSocket === 'function') {
  381 + console.log('触发WebSocket连接...');
  382 + connectWebSocket();
  383 + }
  384 + return pc.setRemoteDescription(answer);
  385 +})
  386 +
  387 +// webrtcapichat.html - 移除自动连接
  388 +// connectWebSocket(); // 移除自动连接,改为在获得sessionid后连接
  389 +
  390 +// 添加sessionId验证
  391 +if (sessionid === 0) {
  392 + console.warn('警告: sessionid为0,可能WebRTC连接尚未建立');
  393 +}
  394 +```
  395 +
  396 +**验证结果:**
  397 +- ✅ WebSocket连接在获得有效sessionId后建立
  398 +- ✅ 新会话正确添加到websocket_connections中
  399 +- ✅ 保持原有重连和错误处理机制
  400 +- ✅ 添加调试日志便于问题排查
  401 +- ✅ AI回复显示模型信息标识
  402 +- ✅ 本地存储自动保存聊天记录
  403 +
  404 +---
  405 +
  406 +## 2024-12-19 SessionId 管理机制优化
  407 +**AIfeng/2024-12-19**
  408 +
  409 +### 问题描述
  410 +前端页面刷新后sessionId丢失,导致数字人会话中断,用户体验不佳。
  411 +
  412 +### 解决方案
  413 +1. **持久化存储**: 实现sessionId的本地存储和自动恢复机制
  414 +2. **会话状态管理**: 添加会话ID显示和手动重置功能
  415 +3. **智能重连**: 页面加载时自动尝试恢复之前的会话连接
  416 +4. **用户交互优化**: 提供直观的会话状态反馈和控制选项
  417 +
  418 +### 技术实现
  419 +- 在`webrtcapichat.html`中添加sessionId管理函数:`saveSessionId`、`restoreSessionId`、`clearSessionId`
  420 +- 修改`client.js`中的`negotiate`函数,在获取sessionId后自动保存到localStorage
  421 +- 在WebSocket连接成功后更新UI显示当前sessionId
  422 +- 添加"重置"按钮支持手动清除sessionId并重新连接
  423 +- 页面初始化时自动尝试恢复sessionId并延迟重连WebSocket
  424 +
  425 +### 界面改进
  426 +- 当前会话ID输入框:实时显示连接状态和sessionId值
  427 +- 重置按钮:支持用户确认后清除会话并提示重新连接
  428 +- 状态提示:连接成功后placeholder显示"已连接"
  429 +
  430 +### ChatOverlay 对话框优化
  431 +**背景**: 对话框遮挡数字人界面,影响视觉体验;缺乏灵活的显示控制和透明度调节功能。
  432 +
  433 +**解决方案**:
  434 +1. **透明度优化**: 将所有背景透明度调整至50%,减少对数字人界面的遮挡
  435 +2. **功能重构**: 将"清空对话记录"按钮改为"隐藏对话框"功能
  436 +3. **配置管理**: 在侧边栏新增"对话框配置"模块,集中管理对话框相关设置
  437 +4. **持久化配置**: 所有配置项支持本地存储,页面刷新后自动恢复
  438 +
  439 +**技术实现**:
  440 +- 调整CSS透明度:chatOverlay主背景、消息框、头像背景均设为50%透明度
  441 +- 新增JavaScript函数:`toggleChatOverlay`、`updateChatOverlayOpacity`、`updateMessageOpacity`、`loadChatOverlayConfig`
  442 +- 动态样式管理:通过JavaScript动态创建CSS样式实现实时透明度调节
  443 +- 事件监听器:滑块控件实时响应用户调整,立即应用视觉效果
  444 +
  445 +**界面改进**:
  446 +- 对话框配置模块:显示/隐藏开关、对话框透明度滑块、消息框透明度滑块
  447 +- 实时反馈:滑块旁显示当前透明度百分比值
  448 +- 重置功能:一键恢复所有配置到默认状态
  449 +- 隐藏按钮:原清空按钮改为"-"图标,点击隐藏对话框
  450 +
  451 +### 验证结果
  452 +- ✅ SessionId现在能够在页面刷新后自动恢复,保持数字人会话的连续性
  453 +- ✅ 对话框现在更加透明,不会过度遮挡数字人界面
  454 +- ✅ 用户可根据需要灵活调节透明度和显示状态
  455 +- ✅ 所有配置项支持持久化存储,提升用户体验
  456 +
  457 +---
  458 +
  459 +## 2025-01-27 - LLM模型信息显示修复 - AIfeng
  460 +
  461 +### 问题修复:页面显示"Unknown LLM"而非实际模型名称
  462 +
  463 +**问题背景:**
  464 +- 启用豆包模型后,页面始终显示"Unknown LLM"
  465 +- app.py中尝试获取`nerfreals[sessionid].llm.model_name`但获取失败
  466 +- LLM处理函数只创建模型实例处理响应,未设置到nerfreal对象
  467 +
  468 +**根因分析:**
  469 +1. **缺失LLM实例绑定**
  470 + - `llm.py`中的处理函数创建Doubao实例但未赋值给`nerfreal.llm`
  471 + - `app.py`中无法通过`nerfreals[sessionid].llm.model_name`获取模型信息
  472 +
  473 +2. **模型名称属性缺失**
  474 + - Doubao类缺少`model_name`属性用于页面显示
  475 + - 通义千问使用OpenAI客户端,无统一的模型名称接口
  476 +
  477 +**解决方案:**
  478 +1. **Doubao类增强**
  479 + - 添加`model_name = "豆包大模型"`属性
  480 + - 提供统一的模型名称显示接口
  481 +
  482 +2. **LLM实例绑定**
  483 + - 在`_handle_doubao_response`中设置`nerfreal.llm = doubao`
  484 + - 同时设置`nerfreal.llm_model_name = doubao.model_name`
  485 +
  486 +3. **通义千问兼容**
  487 + - 创建QwenWrapper包装类提供`model_name`属性
  488 + - 统一模型信息获取机制
  489 +
  490 +**技术实现:**
  491 +```python
  492 +# Doubao.py - 添加模型名称属性
  493 +self.model_name = "豆包大模型" # 添加model_name属性用于页面显示
  494 +
  495 +# llm.py - 绑定LLM实例
  496 +doubao = Doubao()
  497 +nerfreal.llm = doubao
  498 +nerfreal.llm_model_name = doubao.model_name
  499 +
  500 +# 通义千问包装类
  501 +class QwenWrapper:
  502 + def __init__(self):
  503 + self.model_name = "通义千问"
  504 +```
  505 +
  506 +**验证结果:**
  507 +- ✅ 豆包模型页面正确显示"豆包大模型"
  508 +- ✅ 通义千问模型页面正确显示"通义千问"
  509 +- ✅ app.py中`getattr(nerfreals[sessionid], 'llm_model_name', 'Unknown LLM')`正常工作
  510 +- ✅ `nerfreals[sessionid].llm.model_name`属性访问成功
  511 +- ✅ 模型信息在WebSocket消息中正确传递到前端
  512 +
  513 +---
  514 +
  515 +## 2024-12-19 WebSocket通信机制修正
  516 +**AIfeng/2024-12-19**
  517 +
  518 +### 问题描述
  519 +用户指出前期实现存在误解,不应该通过HTTP接口返回数据来获取消息内容,而是完全通过WebSocket通信同步数据。需要修正代码,确保所有消息数据都通过WebSocket推送。
  520 +
  521 +### 修复方案
  522 +1. **纯WebSocket通信**:移除HTTP响应中的消息数据返回,只保留简单的处理状态
  523 +2. **统一数据流**:所有消息显示完全依赖WebSocket推送,不再从HTTP响应获取任何消息数据
  524 +3. **简化响应格式**:HTTP接口只返回处理状态,不包含具体的消息内容
  525 +4. **保持错误处理**:网络错误仍通过前端直接处理,服务器错误通过WebSocket推送
  526 +
  527 +### 技术实现
  528 +**后端修改** (`e:\fengyang\eman_one\app.py`):
  529 +- 简化`/human`接口返回格式,只包含`code`和`message`状态信息
  530 +- 移除HTTP响应中的`user_message`、`ai_response`、`model_info`等数据字段
  531 +- 保持WebSocket推送机制不变,所有消息数据通过WebSocket传输
  532 +
  533 +**前端修改** (`e:\fengyang\eman_one\web\webrtcapichat.html`):
  534 +- 移除对HTTP响应数据的处理和界面显示逻辑
  535 +- 保留网络错误的本地处理机制
  536 +- 所有消息显示完全依赖WebSocket推送的`chat_message`类型数据
  537 +- 简化HTTP响应处理,只检查处理状态
  538 +
  539 +### 验证结果
  540 +- ✅ HTTP接口不再返回消息数据,只返回处理状态
  541 +- ✅ 所有消息显示完全通过WebSocket推送实现
  542 +- ✅ 前端不再依赖HTTP响应获取消息内容
  543 +- ✅ 网络错误处理机制保持正常
  544 +- ✅ WebSocket推送机制保持完整功能
  545 +- ✅ 实现了纯WebSocket数据通信架构
  546 +
  547 +---
  548 +
  549 +## 2025-06-26 - AIfeng
  550 +
  551 +### 问题修复:LLM模块导入错误
  552 +
  553 +**问题描述:**
  554 +- `ImportError: cannot import name 'llm_response' from 'llm'`
  555 +- app.py无法从llm包中导入llm_response函数
  556 +
  557 +**修复方案:**
  558 +- 修改 `llm/__init__.py` 文件,添加llm_response函数的正确导入
  559 +- 使用importlib.util动态加载llm.py模块,避免循环导入问题
  560 +- 更新__all__列表,确保llm_response函数正确导出
  561 +
  562 +**技术实现:**
  563 +```python
  564 +# 使用importlib.util动态导入
  565 +import importlib.util
  566 +spec = importlib.util.spec_from_file_location("llm_module", os.path.join(parent_dir, "llm.py"))
  567 +llm_module = importlib.util.module_from_spec(spec)
  568 +spec.loader.exec_module(llm_module)
  569 +llm_response = llm_module.llm_response
  570 +```
  571 +
  572 +**验证结果:**
  573 +- ✅ `from llm import llm_response` 导入成功
  574 +- ✅ app.py可以正常导入llm_response函数
  575 +- ✅ 豆包模型集成功能完全可用
  576 +
  577 +---
  578 +
  579 +# 2024-12-19 代码质量与可维护性增强建议
  580 +
  581 +## 新增文件
  582 +- `doc/dev/code_quality_enhancement.md` - 代码质量与可维护性增强建议文档
  583 +
  584 +## 功能增强
  585 +
  586 +### 架构优化建议
  587 +- **依赖注入模式**: 实现LLM容器管理,解耦模型选择逻辑
  588 +- **策略模式重构**: 替换if-elif条件判断,提升代码可扩展性
  589 +- **配置管理中心化**: 统一配置管理器,支持热重载和变更监听
  590 +
  591 +### 代码质量提升
  592 +- **类型注解完善**: 全面添加类型提示,提升代码可读性
  593 +- **错误处理标准化**: 自定义异常类型和错误处理链
  594 +- **日志系统增强**: 结构化日志和链路追踪
  595 +
  596 +### 测试策略完善
  597 +- **单元测试覆盖**: 完整的测试套件和Mock策略
  598 +- **集成测试自动化**: 模型切换和配置热重载测试
  599 +
  600 +### 性能监控与优化
  601 +- **性能指标收集**: 响应时间、令牌速率、内存使用监控
  602 +- **缓存策略**: LRU缓存和智能缓存键生成
  603 +
  604 +### 安全性增强
  605 +- **敏感信息保护**: API密钥加密存储
  606 +- **输入验证和清理**: XSS防护和输入长度限制
  607 +
  608 +### 文档和规范
  609 +- **API文档自动生成**: FastAPI集成和Swagger文档
  610 +- **代码规范检查**: pre-commit钩子和自动化检查
  611 +
  612 +### 部署和运维
  613 +- **容器化部署**: Docker配置和健康检查
  614 +- **监控和告警**: 系统健康检查和故障预警
  615 +
  616 +## 实施优先级
  617 +- **高优先级**: 类型注解、错误处理、单元测试、输入验证
  618 +- **中优先级**: 策略模式、配置管理、性能监控、缓存策略
  619 +- **低优先级**: 依赖注入、微服务架构、分布式缓存、自动化运维
  620 +
  621 +## 技术价值
  622 +- 🏗️ 提升架构设计质量
  623 +- 🔧 增强代码可维护性
  624 +- 🧪 完善测试覆盖率
  625 +- 📊 强化性能监控
  626 +- 🔒 提升安全保障
  627 +- 📚 完善文档体系
  628 +- 🚀 优化部署运维
  629 +
  630 +---
  631 +
  632 +## 2024-12-19
  633 +### WebSocket双向通信系统实现
  634 +**问题描述:**
  635 +- `/human` 接口处理的消息无法实时推送到前端页面
  636 +- 第三方调用 `/human` 接口时,页面无法获得反馈
  637 +- 缺乏页面与服务端的实时通信机制
  638 +
  639 +**解决方案:**
  640 +1. **后端WebSocket服务器实现**
  641 + - 在 `app.py` 中添加 WebSocket 支持(aiohttp)
  642 + - 实现会话级连接管理(`websocket_connections`)
  643 + - 添加消息推送函数 `broadcast_message_to_session`
  644 + - 支持心跳检测和自动重连机制
  645 +
  646 +2. **前端WebSocket客户端优化**
  647 + - 修改 `webrtcapichat.html` 连接到新的 `/ws` 端点
  648 + - 实现会话登录和消息类型处理
  649 + - 添加聊天消息推送的实时显示
  650 +
  651 +3. **消息推送集成**
  652 + - 修改 `human` 函数,在处理消息后推送到WebSocket
  653 + - 支持 `chat` 和 `echo` 两种消息类型的推送
  654 + - 区分用户消息、AI回复和回音消息的显示
  655 +
  656 +**技术实现:**
  657 +- **后端架构**:aiohttp WebSocket + weakref连接管理
  658 +- **消息格式**:JSON格式,包含类型、会话ID、内容和来源
  659 +- **连接管理**:基于sessionid的会话级连接池
  660 +- **错误处理**:连接异常自动清理和日志记录
  661 +
  662 +**核心功能:**
  663 +- ✅ 实时双向通信:页面↔服务端
  664 +- ✅ 会话级消息推送:支持多用户并发
  665 +- ✅ 消息类型区分:用户/AI助手/回音
  666 +- ✅ 连接状态管理:自动重连和心跳检测
  667 +- ✅ 第三方接口支持:外部调用也能推送到页面
  668 +
  669 +**测试工具:**
  670 +- 创建 `websocket_test.html` 测试页面
  671 +- 支持连接状态监控和消息发送测试
  672 +- 实时日志显示和错误诊断
  673 +
  674 +**技术价值:**
  675 +- 🚀 **实时性提升**:消息即时推送,无需轮询
  676 +- 🔄 **双向通信**:支持服务端主动推送
  677 +- 🎯 **精准推送**:基于会话ID的定向消息
  678 +- 🛡️ **稳定性增强**:自动重连和异常处理
  679 +- 🔧 **扩展性强**:支持未来更多消息类型
  680 +
  681 +# 2024-12-19 WebRTC Chat页面消息类型支持修复
  682 +
  683 +## 问题描述
  684 +- `webrtcapichat.html`中使用"type": "chat"的模型对话数据没有被收录到chatOverlay
  685 +- 表单提交时固定发送"type": "echo",无法发送chat类型消息
  686 +- WebSocket消息处理逻辑未正确识别chat类型回复
  687 +
  688 +## 修复内容
  689 +
  690 +### 1. 添加消息类型选择器
  691 +- 在文本输入表单中添加消息类型下拉选择框
  692 +- 支持"智能对话"(chat)和"回音模式"(echo)两种类型
  693 +- 默认选择为"智能对话"模式
  694 +
  695 +### 2. 修改表单提交逻辑
  696 +- 获取用户选择的消息类型,替代固定的'echo'类型
  697 +- 动态发送用户选择的消息类型到服务器
  698 +
  699 +### 3. 优化WebSocket消息处理
  700 +- 增加对`messageData.Data.Type`字段的检查
  701 +- 根据服务器返回的消息类型正确设置显示模式
  702 +- 添加调试日志输出,便于问题排查
  703 +- 修复TTS推送时使用原始消息类型而非固定echo类型
  704 +
  705 +## 技术实现
  706 +
  707 +### HTML表单增强
  708 +```html
  709 +<div class="form-group">
  710 + <label for="message-type">消息类型</label>
  711 + <select class="form-control" id="message-type">
  712 + <option value="chat">智能对话</option>
  713 + <option value="echo">回音模式</option>
  714 + </select>
  715 +</div>
  716 +```
  717 +
  718 +### JavaScript逻辑优化
  719 +```javascript
  720 +// 动态获取消息类型
  721 +var messageType = document.getElementById('message-type') ?
  722 + document.getElementById('message-type').value : 'chat';
  723 +
  724 +// 消息模式判断增强
  725 +if (messageData.Data.Mode) {
  726 + mode = messageData.Data.Mode;
  727 +} else if (messageData.Data.Type) {
  728 + mode = messageData.Data.Type;
  729 +} else {
  730 + // 启发式判断逻辑
  731 +}
  732 +```
  733 +
  734 +## 修复效果
  735 +- ✅ 用户可以选择发送chat或echo类型消息
  736 +- ✅ chat类型的大模型回复正确显示在chatOverlay中
  737 +- ✅ 不同消息类型有对应的视觉标识(头像和颜色)
  738 +- ✅ 调试信息完善,便于后续维护
  739 +
  740 +## 兼容性说明
  741 +- 保持向后兼容,默认消息类型为chat
  742 +- 原有echo模式功能不受影响
  743 +- 支持服务器端返回的Mode和Type字段
  744 +
  745 +---
  746 +
  747 +## 2024-12-19 - AIfeng
  748 +
  749 +### 功能增强:豆包大模型集成与配置化改造
  750 +
  751 +**新增文件:**
  752 +- `llm/Doubao.py` - 豆包大模型API接口实现
  753 +- `config/doubao_config.json` - 豆包模型详细配置文件
  754 +- `config/llm_config.json` - LLM模型统一配置管理
  755 +
  756 +**文件修改:**
  757 +- `llm.py` - 重构LLM响应函数,支持多模型切换
  758 +
  759 +**新增功能:**
  760 +1. **豆包模型集成**
  761 + - 基于火山引擎豆包API实现对话功能
  762 + - 支持流式和非流式响应模式
  763 + - 完整的错误处理和异常捕获机制
  764 + - 支持自定义API密钥和基础URL配置
  765 +
  766 +2. **配置化架构设计**
  767 + - 人物设定完全配置化(性格、背景、对话风格等)
  768 + - API参数可配置(模型名称、温度、top_p、最大token等)
  769 + - 响应行为配置(流式传输、重试机制、超时设置)
  770 + - 高级功能配置(安全过滤、内容审核、日志记录)
  771 +
  772 +3. **多模型统一管理**
  773 + - 通过`llm_config.json`统一管理模型选择
  774 + - 支持通义千问和豆包模型无缝切换
  775 + - 保持原有通义千问功能完整性
  776 + - 统一的性能监控和日志记录
  777 +
  778 +4. **流式响应优化**
  779 + - 豆包模型支持实时流式输出
  780 + - 智能分句处理,提升用户体验
  781 + - 首个token时间和总响应时间监控
  782 + - 回调函数机制支持自定义处理逻辑
  783 +
  784 +5. **配置文件结构**
  785 + - `doubao_config.json`:包含API配置、人物设定、响应配置等
  786 + - `llm_config.json`:模型选择和通用设置
  787 + - 支持环境变量和配置文件双重配置方式
  788 +
  789 +**技术实现:**
  790 +- 重构`llm_response`函数为模块化架构
  791 +- 新增`_load_llm_config`、`_handle_doubao_response`、`_handle_qwen_response`函数
  792 +- 实现豆包API的HTTP请求封装和流式处理
  793 +- 配置文件JSON格式化和错误处理机制
  794 +- 性能监控和详细日志记录
  795 +
  796 +**配置示例:**
  797 +```json
  798 +{
  799 + "model_type": "doubao",
  800 + "models": {
  801 + "doubao": {
  802 + "config_file": "config/doubao_config.json"
  803 + }
  804 + }
  805 +}
  806 +```
  807 +
  808 +**兼容性:**
  809 +- 完全向后兼容原有通义千问配置
  810 +- 支持动态模型切换,无需重启服务
  811 +- 保持原有API接口不变
  812 +
  813 +---
  814 +
  815 +## 2024-12-19 - AIfeng
  816 +
  817 +### 功能增强:数字人对话记录系统
  818 +
  819 +**文件修改:**
  820 +- `web/webrtcapichat.html` - 增强数字人对话页面
  821 +
  822 +**新增功能:**
  823 +1. **对话框界面优化**
  824 + - 在数字人视频右下角添加完整的对话记录框
  825 + - 增加聊天框头部显示"数字人对话记录"
  826 + - 添加清空对话记录按钮
  827 + - 优化消息显示样式,支持消息动画效果
  828 +
  829 +2. **消息来源标注**
  830 + - 用户输入:标注为"用户输入"或"用户语音"
  831 + - 数字人回复:标注为"数字人回复"
  832 + - 支持不同模式的头像颜色区分(回声模式、对话模式、纯文本模式)
  833 +
  834 +3. **多种输入模式支持**
  835 + - 文字输入:通过文本框发送消息
  836 + - 语音输入:通过录音功能发送语音消息
  837 + - 自动识别并标注输入类型
  838 +
  839 +4. **智能模式识别**
  840 + - 回声模式:数字人复述用户输入
  841 + - 对话模式:大模型生成回复内容
  842 + - 纯文本模式:直接文本显示
  843 + - 基于消息内容的启发式模式判断
  844 +
  845 +5. **本地存储功能**
  846 + - 自动保存对话记录到浏览器本地存储
  847 + - 页面刷新后自动恢复历史对话
  848 + - 支持手动清空对话记录
  849 +
  850 +6. **用户体验优化**
  851 + - 消息自动滚动到底部
  852 + - 空消息输入验证
  853 + - 消息时间戳显示
  854 + - 响应式布局适配
  855 +
  856 +**技术实现:**
  857 +- 扩展 `addMessage` 函数支持来源和模式参数
  858 +- 新增 `clearChatHistory`、`saveChatHistory`、`loadChatHistory` 函数
  859 +- 优化 WebSocket 消息处理逻辑
  860 +- 增强 CSS 样式支持动画和多状态显示
  861 +
  862 +**兼容性:**
  863 +- 保持原有 WebRTC 功能完整性
  864 +- 向后兼容现有 API 接口
  865 +- 支持所有主流浏览器
  866 +
  867 +---
  868 +
  869 +## WebRTC连接优化更新日志
  870 +
  871 +### 问题描述
  872 +用户反映WebRTC连接状态显示"Connection state is connecting"但连接时长很长,需要分析和优化连接建立过程。
  873 +
  874 +### 根因分析
  875 +通过代码分析发现连接延迟可能由以下原因造成:
  876 +1. 模型初始化过程耗时过长(ernerf/musetalk/wav2lip/ultralight模型加载)
  877 +2. ICE候选者收集和连接建立过程缺乏监控
  878 +3. 音视频轨道初始化缺乏性能监控
  879 +4. SDP协商过程缺乏时间追踪
  880 +
  881 +### 优化措施
  882 +
  883 +#### 1. 连接状态监控增强
  884 +- 在`app.py`的`on_connectionstatechange`函数中添加详细的时间戳记录
  885 +- 增加SessionID标识,便于多会话调试
  886 +- 添加连接状态变化的详细日志(connecting/connected/failed/closed)
  887 +- 改进错误处理,避免重复删除会话
  888 +
  889 +#### 2. ICE连接监控
  890 +- 新增`on_iceconnectionstatechange`事件监听器
  891 +- 监控ICE连接状态变化(checking/connected/completed/failed/disconnected)
  892 +- 新增`on_icegatheringstatechange`事件监听器
  893 +- 监控ICE候选者收集过程(gathering/complete)
  894 +
  895 +#### 3. 模型初始化优化
  896 +- 在`build_nerfreal`函数中添加详细的加载时间监控
  897 +- 为每种模型类型(Wav2Lip/MuseTalk/ERNeRF/UltraLight)添加专门的日志
  898 +- 增强错误处理和资源清理机制
  899 +- 添加垃圾回收以优化内存使用
  900 +
  901 +#### 4. 音视频轨道初始化监控
  902 +- 监控HumanPlayer创建时间
  903 +- 监控音频轨道和视频轨道添加时间
  904 +- 记录整个音视频初始化过程的总耗时
  905 +
  906 +#### 5. 编解码器配置监控
  907 +- 监控视频编解码器配置过程
  908 +- 记录可用编解码器列表(H264/VP8/rtx)
  909 +- 监控编解码器偏好设置时间
  910 +
  911 +#### 6. SDP协商过程监控
  912 +- 监控SDP协商的完整过程
  913 +- 记录远程描述设置、应答创建、本地描述设置的各个阶段
  914 +- 计算SDP协商总耗时
  915 +
  916 +### 技术实现细节
  917 +
  918 +#### 时间戳格式
  919 +所有时间戳使用`time.time()`获取,精确到毫秒(%.3f格式)
  920 +
  921 +#### 日志格式标准化
  922 +```
  923 +[SessionID:XXXXXX] 操作描述 at 时间戳
  924 +[SessionID:XXXXXX] 操作描述 in X.XXX seconds
  925 +```
  926 +
  927 +#### 错误处理改进
  928 +- 使用try-catch包装模型初始化过程
  929 +- 添加资源清理机制
  930 +- 避免重复删除会话导致的KeyError
  931 +
  932 +### 预期效果
  933 +1. **问题定位精确化**:通过详细的时间戳记录,可以精确定位连接建立过程中的瓶颈
  934 +2. **性能监控可视化**:各个阶段的耗时记录有助于识别性能热点
  935 +3. **调试效率提升**:SessionID标识和结构化日志便于多会话并发调试
  936 +4. **系统稳定性增强**:改进的错误处理和资源清理机制
  937 +
  938 +### 后续优化建议
  939 +1. **模型预加载**:考虑在服务启动时预加载常用模型
  940 +2. **连接池优化**:实现模型实例复用机制
  941 +3. **网络配置优化**:优化STUN/TURN服务器配置
  942 +4. **异步初始化**:将模型初始化与WebRTC连接建立并行处理
  943 +
  944 +## 2024-01-XX STUN服务器优化更新
  945 +
  946 +### 优化背景
  947 +基于日志分析发现ICE候选者收集延迟(5秒+)是主要性能瓶颈,需要优化STUN服务器配置。
  948 +
  949 +### 技术实现
  950 +
  951 +#### 1. 多STUN服务器配置
  952 +- 添加Google多个STUN服务器节点
  953 +- 实现负载均衡和故障转移
  954 +- 配置ICE候选者池大小优化
  955 +
  956 +#### 2. ICE收集超时机制
  957 +- 设置3秒超时限制
  958 +- 避免无限等待ICE收集完成
  959 +- 提供降级处理方案
  960 +
  961 +#### 3. 连接状态监控增强
  962 +- 添加实时ICE状态显示
  963 +- 提供用户友好的连接状态反馈
  964 +- 增强调试和问题定位能力
  965 +
  966 +### 文件变更记录
  967 +- 修改文件:`e:\fengyang\eman_one\app.py`
  968 +- 变更类型:功能增强、性能监控、错误处理改进
  969 +- 影响范围:WebRTC连接建立流程、模型初始化流程
  970 +- `web/client.js`: 优化STUN配置,添加超时机制和状态监控
  971 +- `web/whep.js`: 同步STUN服务器配置优化
  972 +
  973 +### 预期效果
  974 +- ICE收集时间从5秒降低到1-2秒
  975 +- 总连接时间减少50-60%
  976 +- 提升用户连接体验
  977 +
  978 +### 测试建议
  979 +1. 启动服务后观察日志输出格式
  980 +2. 建立WebRTC连接,记录各阶段耗时
  981 +3. 模拟网络延迟环境测试ICE连接过程
  982 +4. 测试多会话并发场景下的日志区分度
  1 +# -*- coding: utf-8 -*-
  2 +"""
  3 +AIfeng/2025-01-27
  4 +FunASR语音识别模块
  5 +基于BaseASR的FunASR WebSocket客户端实现
  6 +"""
  7 +
  8 +import json
  9 +import time
  10 +import asyncio
  11 +import websockets
  12 +import threading
  13 +import numpy as np
  14 +from threading import Thread, Event
  15 +from typing import Optional, Callable
  16 +import queue
  17 +
  18 +from baseasr import BaseASR
  19 +import config_util as cfg
  20 +import util
  21 +
  22 +class FunASRClient(BaseASR):
  23 + """FunASR WebSocket客户端"""
  24 +
  25 + def __init__(self, opt, parent=None):
  26 + # 确保opt有必要的属性
  27 + if not hasattr(opt, 'fps'):
  28 + opt.fps = 50 # 默认50fps
  29 + if not hasattr(opt, 'batch_size'):
  30 + opt.batch_size = 1
  31 + if not hasattr(opt, 'l'):
  32 + opt.l = 10
  33 + if not hasattr(opt, 'r'):
  34 + opt.r = 10
  35 +
  36 + super().__init__(opt, parent)
  37 +
  38 + # FunASR配置
  39 + self.server_url = f"ws://{cfg.local_asr_ip}:{cfg.local_asr_port}"
  40 + self.username = getattr(opt, 'username', 'default_user')
  41 +
  42 + # 连接状态
  43 + self.websocket = None
  44 + self.connected = False
  45 + self.running = False
  46 + self.reconnect_delay = getattr(cfg, 'asr_reconnect_delay', 1)
  47 + self.max_reconnect_attempts = getattr(cfg, 'asr_max_reconnect_attempts', 5)
  48 +
  49 + # 消息队列
  50 + self.message_queue = queue.Queue()
  51 + self.result_queue = queue.Queue()
  52 +
  53 + # 线程控制
  54 + self.connection_thread = None
  55 + self.message_thread = None
  56 + self.stop_event = Event()
  57 +
  58 + # 回调函数
  59 + self.on_result_callback = None
  60 +
  61 + util.log(1, f"FunASR客户端初始化完成,服务器: {self.server_url}")
  62 +
  63 + def set_result_callback(self, callback: Callable[[str], None]):
  64 + """设置识别结果回调函数
  65 +
  66 + Args:
  67 + callback: 回调函数,接收识别结果字符串
  68 + """
  69 + self.on_result_callback = callback
  70 +
  71 + async def _connect_websocket(self):
  72 + """连接WebSocket服务器"""
  73 + try:
  74 + self.websocket = await websockets.connect(
  75 + self.server_url,
  76 + timeout=getattr(cfg, 'asr_timeout', 30)
  77 + )
  78 + self.connected = True
  79 + util.log(1, f"FunASR WebSocket连接成功: {self.server_url}")
  80 + return True
  81 + except Exception as e:
  82 + util.log(3, f"FunASR WebSocket连接失败: {e}")
  83 + self.connected = False
  84 + return False
  85 +
  86 + async def _disconnect_websocket(self):
  87 + """断开WebSocket连接"""
  88 + if self.websocket:
  89 + try:
  90 + await self.websocket.close()
  91 + except Exception as e:
  92 + util.log(2, f"关闭WebSocket连接时出错: {e}")
  93 + finally:
  94 + self.websocket = None
  95 + self.connected = False
  96 +
  97 + async def _send_message(self, message: dict):
  98 + """发送消息到FunASR服务器
  99 +
  100 + Args:
  101 + message: 要发送的消息字典
  102 + """
  103 + if not self.connected or not self.websocket:
  104 + util.log(2, "WebSocket未连接,无法发送消息")
  105 + return False
  106 +
  107 + try:
  108 + await self.websocket.send(json.dumps(message))
  109 + return True
  110 + except Exception as e:
  111 + util.log(3, f"发送消息失败: {e}")
  112 + self.connected = False
  113 + return False
  114 +
  115 + async def _receive_messages(self):
  116 + """接收WebSocket消息"""
  117 + while self.connected and self.websocket:
  118 + try:
  119 + message = await asyncio.wait_for(
  120 + self.websocket.recv(),
  121 + timeout=1.0
  122 + )
  123 + self._handle_recognition_result(message)
  124 + except asyncio.TimeoutError:
  125 + continue
  126 + except websockets.exceptions.ConnectionClosed:
  127 + util.log(2, "WebSocket连接已关闭")
  128 + self.connected = False
  129 + break
  130 + except Exception as e:
  131 + util.log(3, f"接收消息时出错: {e}")
  132 + self.connected = False
  133 + break
  134 +
  135 + async def _send_message_loop(self):
  136 + """发送消息循环"""
  137 + while self.connected and self.websocket:
  138 + try:
  139 + # 检查消息队列
  140 + try:
  141 + message = self.message_queue.get_nowait()
  142 +
  143 + if isinstance(message, dict):
  144 + # JSON消息
  145 + await self.websocket.send(json.dumps(message))
  146 + util.log(1, f"发送JSON消息: {message}")
  147 + elif isinstance(message, bytes):
  148 + # 二进制音频数据
  149 + await self.websocket.send(message)
  150 + util.log(1, f"发送音频数据: {len(message)} bytes")
  151 + else:
  152 + util.log(2, f"未知消息类型: {type(message)}")
  153 +
  154 + except queue.Empty:
  155 + # 队列为空,短暂等待
  156 + await asyncio.sleep(0.01)
  157 +
  158 + except websockets.exceptions.ConnectionClosed:
  159 + util.log(2, "发送消息时连接已关闭")
  160 + self.connected = False
  161 + break
  162 + except Exception as e:
  163 + util.log(3, f"发送消息时出错: {e}")
  164 + self.connected = False
  165 + break
  166 +
  167 + def _handle_recognition_result(self, message: str):
  168 + """处理识别结果
  169 +
  170 + Args:
  171 + message: 识别结果消息
  172 + """
  173 + try:
  174 + # 尝试解析JSON
  175 + try:
  176 + result_data = json.loads(message)
  177 + if isinstance(result_data, dict) and 'text' in result_data:
  178 + recognized_text = result_data['text']
  179 + else:
  180 + recognized_text = message
  181 + except json.JSONDecodeError:
  182 + recognized_text = message
  183 +
  184 + # 存储结果
  185 + self.result_queue.put(recognized_text)
  186 +
  187 + # 调用回调函数
  188 + if self.on_result_callback:
  189 + self.on_result_callback(recognized_text)
  190 +
  191 + # 发送到WebSocket服务器(兼容原有逻辑)
  192 + self._send_to_web_clients(recognized_text)
  193 +
  194 + util.log(1, f"识别结果: {recognized_text}")
  195 +
  196 + except Exception as e:
  197 + util.log(3, f"处理识别结果时出错: {e}")
  198 +
  199 + def _send_to_web_clients(self, text: str):
  200 + """发送识别结果到Web客户端
  201 +
  202 + Args:
  203 + text: 识别文本
  204 + """
  205 + try:
  206 + from core import wsa_server
  207 +
  208 + # 发送到Web客户端
  209 + if wsa_server.get_web_instance().is_connected(self.username):
  210 + wsa_server.get_web_instance().add_cmd({
  211 + "panelMsg": text,
  212 + "Username": self.username
  213 + })
  214 +
  215 + # 发送到Human客户端
  216 + if wsa_server.get_instance().is_connected_human(self.username):
  217 + content = {
  218 + 'Topic': 'human',
  219 + 'Data': {'Key': 'log', 'Value': text},
  220 + 'Username': self.username
  221 + }
  222 + wsa_server.get_instance().add_cmd(content)
  223 +
  224 + except Exception as e:
  225 + util.log(2, f"发送到Web客户端失败: {e}")
  226 +
  227 + async def _connection_loop(self):
  228 + """连接循环,处理重连逻辑"""
  229 + reconnect_attempts = 0
  230 +
  231 + while self.running and not self.stop_event.is_set():
  232 + if not self.connected:
  233 + util.log(1, f"尝试连接FunASR服务器 (第{reconnect_attempts + 1}次)")
  234 +
  235 + if await self._connect_websocket():
  236 + reconnect_attempts = 0
  237 + # 启动消息处理任务
  238 + receive_task = asyncio.create_task(self._receive_messages())
  239 + send_task = asyncio.create_task(self._send_message_loop())
  240 +
  241 + # 等待任务完成或连接断开
  242 + try:
  243 + await asyncio.gather(receive_task, send_task)
  244 + except Exception as e:
  245 + util.log(3, f"连接任务异常: {e}")
  246 + finally:
  247 + receive_task.cancel()
  248 + send_task.cancel()
  249 + else:
  250 + reconnect_attempts += 1
  251 + if reconnect_attempts >= self.max_reconnect_attempts:
  252 + util.log(3, f"达到最大重连次数({self.max_reconnect_attempts}),停止重连")
  253 + break
  254 +
  255 + # 等待后重连
  256 + await asyncio.sleep(self.reconnect_delay)
  257 + self.reconnect_delay = min(self.reconnect_delay * 2, 30) # 指数退避
  258 + else:
  259 + await asyncio.sleep(0.1)
  260 +
  261 + await self._disconnect_websocket()
  262 +
  263 + def _run_async_loop(self):
  264 + """在独立线程中运行异步事件循环"""
  265 + loop = asyncio.new_event_loop()
  266 + asyncio.set_event_loop(loop)
  267 +
  268 + try:
  269 + loop.run_until_complete(self._connection_loop())
  270 + except Exception as e:
  271 + util.log(3, f"异步循环出错: {e}")
  272 + finally:
  273 + loop.close()
  274 +
  275 + def start(self):
  276 + """启动FunASR客户端"""
  277 + if self.running:
  278 + util.log(2, "FunASR客户端已在运行")
  279 + return
  280 +
  281 + self.running = True
  282 + self.stop_event.clear()
  283 +
  284 + # 启动连接线程
  285 + self.connection_thread = Thread(target=self._run_async_loop, daemon=True)
  286 + self.connection_thread.start()
  287 +
  288 + util.log(1, "FunASR客户端已启动")
  289 +
  290 + def stop(self):
  291 + """停止FunASR客户端"""
  292 + if not self.running:
  293 + return
  294 +
  295 + util.log(1, "正在停止FunASR客户端...")
  296 +
  297 + self.running = False
  298 + self.stop_event.set()
  299 +
  300 + # 等待线程结束
  301 + if self.connection_thread and self.connection_thread.is_alive():
  302 + self.connection_thread.join(timeout=5)
  303 +
  304 + util.log(1, "FunASR客户端已停止")
  305 +
  306 + def send_audio_file(self, file_path: str):
  307 + """发送音频文件进行识别
  308 +
  309 + Args:
  310 + file_path: 音频文件路径
  311 + """
  312 + if not self.connected:
  313 + util.log(2, "WebSocket未连接,无法发送音频文件")
  314 + return False
  315 +
  316 + message = {"url": file_path}
  317 +
  318 + # 将消息放入队列,由异步线程处理
  319 + self.message_queue.put(message)
  320 + return True
  321 +
  322 + def send_audio(self, audio_data: bytes):
  323 + """发送音频数据进行识别
  324 +
  325 + Args:
  326 + audio_data: 音频字节数据
  327 + """
  328 + if not self.connected:
  329 + util.log(2, "WebSocket未连接,无法发送音频数据")
  330 + return False
  331 +
  332 + # 将音频数据放入队列
  333 + self.message_queue.put(audio_data)
  334 + return True
  335 +
  336 + def start_recognition(self):
  337 + """开始语音识别"""
  338 + if not self.connected:
  339 + self.start()
  340 +
  341 + # 发送开始识别消息
  342 + start_message = {
  343 + 'vad_need': False,
  344 + 'state': 'StartTranscription'
  345 + }
  346 + self.message_queue.put(start_message)
  347 + util.log(1, "开始语音识别")
  348 +
  349 + def stop_recognition(self):
  350 + """停止语音识别"""
  351 + if not self.connected:
  352 + return
  353 +
  354 + # 发送停止识别消息
  355 + stop_message = {
  356 + 'vad_need': False,
  357 + 'state': 'StopTranscription'
  358 + }
  359 + self.message_queue.put(stop_message)
  360 + util.log(1, "停止语音识别")
  361 +
  362 + def get_latest_result(self, timeout: float = 0.1) -> Optional[str]:
  363 + """获取最新的识别结果
  364 +
  365 + Args:
  366 + timeout: 超时时间
  367 +
  368 + Returns:
  369 + 识别结果字符串或None
  370 + """
  371 + try:
  372 + return self.result_queue.get(timeout=timeout)
  373 + except queue.Empty:
  374 + return None
  375 +
  376 + def warm_up(self):
  377 + """预热模型"""
  378 + super().warm_up()
  379 + self.start()
  380 +
  381 + # 等待连接建立
  382 + max_wait = 10 # 最多等待10秒
  383 + wait_time = 0
  384 + while not self.connected and wait_time < max_wait:
  385 + time.sleep(0.1)
  386 + wait_time += 0.1
  387 +
  388 + if self.connected:
  389 + util.log(1, "FunASR客户端预热完成")
  390 + else:
  391 + util.log(2, "FunASR客户端预热超时")
  392 +
  393 + def run_step(self):
  394 + """运行一步处理"""
  395 + # 处理待发送的消息
  396 + try:
  397 + while not self.message_queue.empty():
  398 + message = self.message_queue.get_nowait()
  399 + # 这里需要通过某种方式发送到异步线程
  400 + # 简化实现:直接记录日志
  401 + util.log(1, f"准备发送消息: {message}")
  402 + except queue.Empty:
  403 + pass
  404 +
  405 + # 调用父类方法
  406 + super().run_step()
  407 +
  408 + def get_next_feat(self, block=True, timeout=None):
  409 + """获取下一个特征
  410 +
  411 + Args:
  412 + block: 是否阻塞
  413 + timeout: 超时时间
  414 +
  415 + Returns:
  416 + 特征数据
  417 + """
  418 + # 简化实现,返回空特征
  419 + return np.zeros((1, 50), dtype=np.float32)
  420 +
  421 + def __del__(self):
  422 + """析构函数"""
  423 + self.stop()
  424 +
  425 +# 兼容性别名
  426 +FunASR = FunASRClient
@@ -49,6 +49,9 @@ def _handle_doubao_response(message, nerfreal, start_time): @@ -49,6 +49,9 @@ def _handle_doubao_response(message, nerfreal, start_time):
49 from llm.Doubao import Doubao 49 from llm.Doubao import Doubao
50 50
51 doubao = Doubao() 51 doubao = Doubao()
  52 + # 设置LLM实例到nerfreal对象,用于页面显示模型信息
  53 + nerfreal.llm = doubao
  54 + nerfreal.llm_model_name = doubao.model_name
52 end = time.perf_counter() 55 end = time.perf_counter()
53 logger.info(f"豆包模型初始化时间: {end-start_time:.3f}s") 56 logger.info(f"豆包模型初始化时间: {end-start_time:.3f}s")
54 57
@@ -103,6 +106,15 @@ def _handle_qwen_response(message, nerfreal, start_time): @@ -103,6 +106,15 @@ def _handle_qwen_response(message, nerfreal, start_time):
103 api_key=os.getenv("DASHSCOPE_API_KEY"), 106 api_key=os.getenv("DASHSCOPE_API_KEY"),
104 base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", 107 base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
105 ) 108 )
  109 +
  110 + # 创建模型信息包装类
  111 + class QwenWrapper:
  112 + def __init__(self):
  113 + self.model_name = "通义千问"
  114 +
  115 + # 设置LLM实例到nerfreal对象,用于页面显示模型信息
  116 + nerfreal.llm = QwenWrapper()
  117 + nerfreal.llm_model_name = "通义千问"
106 end = time.perf_counter() 118 end = time.perf_counter()
107 logger.info(f"通义千问初始化时间: {end-start_time:.3f}s") 119 logger.info(f"通义千问初始化时间: {end-start_time:.3f}s")
108 120
@@ -22,6 +22,7 @@ class Doubao: @@ -22,6 +22,7 @@ class Doubao:
22 self.api_key = os.getenv("DOUBAO_API_KEY") or self.config.get("api_key") 22 self.api_key = os.getenv("DOUBAO_API_KEY") or self.config.get("api_key")
23 self.base_url = self.config.get("base_url", "https://ark.cn-beijing.volces.com/api/v3") 23 self.base_url = self.config.get("base_url", "https://ark.cn-beijing.volces.com/api/v3")
24 self.model = self.config.get("model", "ep-20241219000000-xxxxx") 24 self.model = self.config.get("model", "ep-20241219000000-xxxxx")
  25 + self.model_name = "豆包大模型" # 添加model_name属性用于页面显示
25 self.character_config = self.config.get("character", {}) 26 self.character_config = self.config.get("character", {})
26 27
27 if not self.api_key: 28 if not self.api_key:
  1 +# AIfeng/2025-01-27
  2 +"""
  3 +FunASR服务连接测试脚本
  4 +用于验证本地FunASR WebSocket服务是否可以正常连接
  5 +
  6 +使用方法:
  7 +1. 先启动FunASR服务:python -u web/asr/funasr/ASR_server.py --host "127.0.0.1" --port 10197 --ngpu 0
  8 +2. 运行此测试脚本:python test_funasr_connection.py
  9 +"""
  10 +
  11 +import asyncio
  12 +import websockets
  13 +import json
  14 +import os
  15 +import wave
  16 +import numpy as np
  17 +from pathlib import Path
  18 +
  19 +class FunASRConnectionTest:
  20 + def __init__(self, host="127.0.0.1", port=10197):
  21 + self.host = host
  22 + self.port = port
  23 + self.uri = f"ws://{host}:{port}"
  24 +
  25 + async def test_basic_connection(self):
  26 + """测试基本WebSocket连接"""
  27 + print(f"🔍 测试连接到 {self.uri}")
  28 + try:
  29 + async with websockets.connect(self.uri) as websocket:
  30 + print("✅ FunASR WebSocket服务连接成功")
  31 + return True
  32 + except ConnectionRefusedError:
  33 + print("❌ 连接被拒绝,请确认FunASR服务已启动")
  34 + print(" 启动命令: python -u web/asr/funasr/ASR_server.py --host \"127.0.0.1\" --port 10197 --ngpu 0")
  35 + return False
  36 + except Exception as e:
  37 + print(f"❌ 连接失败: {e}")
  38 + return False
  39 +
  40 + def create_test_wav(self, filename="test_audio.wav", duration=2, sample_rate=16000):
  41 + """创建测试用的WAV文件"""
  42 + # 生成简单的正弦波音频
  43 + t = np.linspace(0, duration, int(sample_rate * duration), False)
  44 + frequency = 440 # A4音符
  45 + audio_data = np.sin(2 * np.pi * frequency * t) * 0.3
  46 +
  47 + # 转换为16位整数
  48 + audio_data = (audio_data * 32767).astype(np.int16)
  49 +
  50 + # 保存为WAV文件
  51 + with wave.open(filename, 'wb') as wav_file:
  52 + wav_file.setnchannels(1) # 单声道
  53 + wav_file.setsampwidth(2) # 16位
  54 + wav_file.setframerate(sample_rate)
  55 + wav_file.writeframes(audio_data.tobytes())
  56 +
  57 + print(f"📁 创建测试音频文件: {filename}")
  58 + return filename
  59 +
  60 + async def test_audio_recognition(self):
  61 + """测试音频识别功能"""
  62 + print("\n🎵 测试音频识别功能")
  63 +
  64 + # 创建测试音频文件
  65 + test_file = self.create_test_wav()
  66 + test_file_path = os.path.abspath(test_file)
  67 +
  68 + try:
  69 + async with websockets.connect(self.uri) as websocket:
  70 + print("✅ 连接成功,发送音频文件路径")
  71 +
  72 + # 发送音频文件路径
  73 + message = {"url": test_file_path}
  74 + await websocket.send(json.dumps(message))
  75 + print(f"📤 发送消息: {message}")
  76 +
  77 + # 等待识别结果
  78 + try:
  79 + response = await asyncio.wait_for(websocket.recv(), timeout=10)
  80 + print(f"📥 收到识别结果: {response}")
  81 + return True
  82 + except asyncio.TimeoutError:
  83 + print("⏰ 等待响应超时(10秒)")
  84 + print(" 这可能是正常的,因为测试音频是纯音调,无法识别为文字")
  85 + return True # 超时也算连接成功
  86 +
  87 + except Exception as e:
  88 + print(f"❌ 音频识别测试失败: {e}")
  89 + return False
  90 + finally:
  91 + # 清理测试文件
  92 + if os.path.exists(test_file):
  93 + os.remove(test_file)
  94 + print(f"🗑️ 清理测试文件: {test_file}")
  95 +
  96 + async def test_real_audio_files(self):
  97 + """测试实际音频文件的识别效果"""
  98 + print("\n🎤 测试实际音频文件识别")
  99 +
  100 + # 实际音频文件列表
  101 + audio_files = [
  102 + "yunxi.mp3",
  103 + "yunxia.mp3",
  104 + "yunyang.mp3"
  105 + ]
  106 +
  107 + results = []
  108 +
  109 + for audio_file in audio_files:
  110 + file_path = os.path.abspath(audio_file)
  111 +
  112 + # 检查文件是否存在
  113 + if not os.path.exists(file_path):
  114 + print(f"⚠️ 音频文件不存在: {file_path}")
  115 + continue
  116 +
  117 + print(f"\n🎵 测试音频文件: {audio_file}")
  118 +
  119 + try:
  120 + async with websockets.connect(self.uri) as websocket:
  121 + print(f"✅ 连接成功,发送音频文件: {audio_file}")
  122 +
  123 + # 发送音频文件路径
  124 + message = {"url": file_path}
  125 + await websocket.send(json.dumps(message))
  126 + print(f"📤 发送消息: {message}")
  127 +
  128 + # 等待识别结果
  129 + try:
  130 + response = await asyncio.wait_for(websocket.recv(), timeout=30)
  131 + print(f"📥 识别结果: {response}")
  132 +
  133 + # 解析响应
  134 + try:
  135 + result_data = json.loads(response)
  136 + if isinstance(result_data, dict) and 'text' in result_data:
  137 + recognized_text = result_data['text']
  138 + print(f"🎯 识别文本: {recognized_text}")
  139 + results.append({
  140 + 'file': audio_file,
  141 + 'text': recognized_text,
  142 + 'status': 'success'
  143 + })
  144 + else:
  145 + print(f"📄 原始响应: {response}")
  146 + results.append({
  147 + 'file': audio_file,
  148 + 'response': response,
  149 + 'status': 'received'
  150 + })
  151 + except json.JSONDecodeError:
  152 + print(f"📄 非JSON响应: {response}")
  153 + results.append({
  154 + 'file': audio_file,
  155 + 'response': response,
  156 + 'status': 'received'
  157 + })
  158 +
  159 + except asyncio.TimeoutError:
  160 + print(f"⏰ 等待响应超时(30秒)- {audio_file}")
  161 + results.append({
  162 + 'file': audio_file,
  163 + 'status': 'timeout'
  164 + })
  165 +
  166 + except Exception as e:
  167 + print(f"❌ 测试 {audio_file} 失败: {e}")
  168 + results.append({
  169 + 'file': audio_file,
  170 + 'error': str(e),
  171 + 'status': 'error'
  172 + })
  173 +
  174 + # 文件间等待,避免服务器压力
  175 + await asyncio.sleep(1)
  176 +
  177 + # 输出测试总结
  178 + print("\n" + "="*50)
  179 + print("📊 实际音频文件测试总结:")
  180 + for i, result in enumerate(results, 1):
  181 + print(f"\n{i}. 文件: {result['file']}")
  182 + if result['status'] == 'success':
  183 + print(f" ✅ 识别成功: {result['text']}")
  184 + elif result['status'] == 'received':
  185 + print(f" 📥 收到响应: {result.get('response', 'N/A')}")
  186 + elif result['status'] == 'timeout':
  187 + print(f" ⏰ 响应超时")
  188 + elif result['status'] == 'error':
  189 + print(f" ❌ 测试失败: {result.get('error', 'N/A')}")
  190 +
  191 + return len(results) > 0
  192 +
  193 + async def test_message_format(self):
  194 + """测试消息格式兼容性"""
  195 + print("\n📋 测试消息格式兼容性")
  196 +
  197 + try:
  198 + async with websockets.connect(self.uri) as websocket:
  199 + # 测试不同的消息格式
  200 + test_messages = [
  201 + {"url": "nonexistent.wav"},
  202 + {"test": "message"},
  203 + "invalid_json"
  204 + ]
  205 +
  206 + for i, msg in enumerate(test_messages, 1):
  207 + try:
  208 + if isinstance(msg, dict):
  209 + await websocket.send(json.dumps(msg))
  210 + print(f"✅ 消息 {i} 发送成功: {msg}")
  211 + else:
  212 + await websocket.send(msg)
  213 + print(f"✅ 消息 {i} 发送成功: {msg}")
  214 +
  215 + # 短暂等待,避免消息堆积
  216 + await asyncio.sleep(0.5)
  217 +
  218 + except Exception as e:
  219 + print(f"⚠️ 消息 {i} 发送失败: {e}")
  220 +
  221 + return True
  222 +
  223 + except Exception as e:
  224 + print(f"❌ 消息格式测试失败: {e}")
  225 + return False
  226 +
  227 + def check_dependencies(self):
  228 + """检查依赖项"""
  229 + print("🔍 检查依赖项...")
  230 +
  231 + required_modules = [
  232 + 'websockets',
  233 + 'asyncio',
  234 + 'json',
  235 + 'wave',
  236 + 'numpy'
  237 + ]
  238 +
  239 + missing_modules = []
  240 + for module in required_modules:
  241 + try:
  242 + __import__(module)
  243 + print(f"✅ {module}")
  244 + except ImportError:
  245 + print(f"❌ {module} (缺失)")
  246 + missing_modules.append(module)
  247 +
  248 + if missing_modules:
  249 + print(f"\n⚠️ 缺失依赖项: {', '.join(missing_modules)}")
  250 + print("安装命令: pip install " + ' '.join(missing_modules))
  251 + return False
  252 +
  253 + print("✅ 所有依赖项检查通过")
  254 + return True
  255 +
  256 + def check_funasr_server_file(self):
  257 + """检查FunASR服务器文件是否存在"""
  258 + print("\n📁 检查FunASR服务器文件...")
  259 +
  260 + server_path = Path("web/asr/funasr/ASR_server.py")
  261 + if server_path.exists():
  262 + print(f"✅ 找到服务器文件: {server_path.absolute()}")
  263 + return True
  264 + else:
  265 + print(f"❌ 未找到服务器文件: {server_path.absolute()}")
  266 + print(" 请确认文件路径是否正确")
  267 + return False
  268 +
  269 + async def run_all_tests(self):
  270 + """运行所有测试"""
  271 + print("🚀 开始FunASR连接测试\n")
  272 +
  273 + # 检查依赖
  274 + if not self.check_dependencies():
  275 + return False
  276 +
  277 + # 检查服务器文件
  278 + if not self.check_funasr_server_file():
  279 + return False
  280 +
  281 + # 基本连接测试
  282 + print("\n" + "="*50)
  283 + if not await self.test_basic_connection():
  284 + return False
  285 +
  286 + # 音频识别测试
  287 + print("\n" + "="*50)
  288 + if not await self.test_audio_recognition():
  289 + return False
  290 +
  291 + # 实际音频文件测试
  292 + print("\n" + "="*50)
  293 + await self.test_real_audio_files()
  294 +
  295 + # 消息格式测试
  296 + print("\n" + "="*50)
  297 + if not await self.test_message_format():
  298 + return False
  299 +
  300 + print("\n" + "="*50)
  301 + print("🎉 所有测试完成!FunASR服务连接正常")
  302 + print("\n💡 集成建议:")
  303 + print(" 1. 服务使用WebSocket协议,非gRPC")
  304 + print(" 2. 默认监听端口: 10197")
  305 + print(" 3. 消息格式: JSON字符串,包含'url'字段指向音频文件路径")
  306 + print(" 4. 可以集成到现有项目的ASR模块中")
  307 +
  308 + return True
  309 +
  310 +async def main():
  311 + """主函数"""
  312 + tester = FunASRConnectionTest()
  313 + success = await tester.run_all_tests()
  314 +
  315 + if not success:
  316 + print("\n❌ 测试失败,请检查FunASR服务状态")
  317 + return 1
  318 +
  319 + return 0
  320 +
  321 +if __name__ == "__main__":
  322 + try:
  323 + exit_code = asyncio.run(main())
  324 + exit(exit_code)
  325 + except KeyboardInterrupt:
  326 + print("\n⏹️ 测试被用户中断")
  327 + exit(1)
  328 + except Exception as e:
  329 + print(f"\n💥 测试过程中发生错误: {e}")
  330 + exit(1)
  1 +# -*- coding: utf-8 -*-
  2 +"""
  3 +AIfeng/2025-01-27
  4 +FunASR集成测试脚本
  5 +测试新的FunASRClient与项目的集成效果
  6 +"""
  7 +
  8 +import os
  9 +import sys
  10 +import time
  11 +import threading
  12 +from pathlib import Path
  13 +
  14 +# 添加项目路径
  15 +sys.path.append(os.path.dirname(__file__))
  16 +
  17 +from funasr_asr import FunASRClient
  18 +from web.asr.funasr import FunASR
  19 +import util
  20 +
  21 +class TestFunASRIntegration:
  22 + """FunASR集成测试类"""
  23 +
  24 + def __init__(self):
  25 + self.test_results = []
  26 + self.test_audio_files = [
  27 + "yunxi.mp3",
  28 + "yunxia.mp3",
  29 + "yunyang.mp3"
  30 + ]
  31 +
  32 + def log_test_result(self, test_name: str, success: bool, message: str = ""):
  33 + """记录测试结果"""
  34 + status = "✓ 通过" if success else "✗ 失败"
  35 + result = f"[{status}] {test_name}"
  36 + if message:
  37 + result += f" - {message}"
  38 +
  39 + self.test_results.append((test_name, success, message))
  40 + print(result)
  41 +
  42 + def test_funasr_client_creation(self):
  43 + """测试FunASRClient创建"""
  44 + try:
  45 + class SimpleOpt:
  46 + def __init__(self):
  47 + self.username = "test_user"
  48 +
  49 + opt = SimpleOpt()
  50 + client = FunASRClient(opt)
  51 +
  52 + # 检查基本属性
  53 + assert hasattr(client, 'server_url')
  54 + assert hasattr(client, 'connected')
  55 + assert hasattr(client, 'running')
  56 +
  57 + self.log_test_result("FunASRClient创建", True, "客户端创建成功")
  58 + return client
  59 +
  60 + except Exception as e:
  61 + self.log_test_result("FunASRClient创建", False, f"错误: {e}")
  62 + return None
  63 +
  64 + def test_compatibility_wrapper(self):
  65 + """测试兼容性包装器"""
  66 + try:
  67 + funasr = FunASR("test_user")
  68 +
  69 + # 检查兼容性方法
  70 + assert hasattr(funasr, 'start')
  71 + assert hasattr(funasr, 'end')
  72 + assert hasattr(funasr, 'send')
  73 + assert hasattr(funasr, 'add_frame')
  74 + assert hasattr(funasr, 'set_message_callback')
  75 +
  76 + self.log_test_result("兼容性包装器", True, "所有兼容性方法存在")
  77 + return funasr
  78 +
  79 + except Exception as e:
  80 + self.log_test_result("兼容性包装器", False, f"错误: {e}")
  81 + return None
  82 +
  83 + def test_callback_mechanism(self):
  84 + """测试回调机制"""
  85 + try:
  86 + funasr = FunASR("test_user")
  87 + callback_called = threading.Event()
  88 + received_message = []
  89 +
  90 + def test_callback(message):
  91 + received_message.append(message)
  92 + callback_called.set()
  93 +
  94 + funasr.set_message_callback(test_callback)
  95 +
  96 + # 模拟接收消息
  97 + test_message = "测试识别结果"
  98 + funasr._handle_result(test_message)
  99 +
  100 + # 等待回调
  101 + if callback_called.wait(timeout=1.0):
  102 + if received_message and received_message[0] == test_message:
  103 + self.log_test_result("回调机制", True, "回调函数正常工作")
  104 + else:
  105 + self.log_test_result("回调机制", False, "回调消息不匹配")
  106 + else:
  107 + self.log_test_result("回调机制", False, "回调超时")
  108 +
  109 + except Exception as e:
  110 + self.log_test_result("回调机制", False, f"错误: {e}")
  111 +
  112 + def test_audio_file_existence(self):
  113 + """测试音频文件存在性"""
  114 + existing_files = []
  115 + missing_files = []
  116 +
  117 + for audio_file in self.test_audio_files:
  118 + if os.path.exists(audio_file):
  119 + existing_files.append(audio_file)
  120 + else:
  121 + missing_files.append(audio_file)
  122 +
  123 + if existing_files:
  124 + self.log_test_result(
  125 + "音频文件检查",
  126 + True,
  127 + f"找到 {len(existing_files)} 个文件: {', '.join(existing_files)}"
  128 + )
  129 +
  130 + if missing_files:
  131 + self.log_test_result(
  132 + "音频文件缺失",
  133 + False,
  134 + f"缺少 {len(missing_files)} 个文件: {', '.join(missing_files)}"
  135 + )
  136 +
  137 + return existing_files
  138 +
  139 + def test_connection_simulation(self):
  140 + """测试连接模拟"""
  141 + try:
  142 + client = self.test_funasr_client_creation()
  143 + if not client:
  144 + return
  145 +
  146 + # 测试启动和停止
  147 + client.start()
  148 + time.sleep(0.5) # 给连接一些时间
  149 +
  150 + # 检查运行状态
  151 + if client.running:
  152 + self.log_test_result("客户端启动", True, "客户端成功启动")
  153 + else:
  154 + self.log_test_result("客户端启动", False, "客户端启动失败")
  155 +
  156 + # 停止客户端
  157 + client.stop()
  158 + time.sleep(0.5)
  159 +
  160 + if not client.running:
  161 + self.log_test_result("客户端停止", True, "客户端成功停止")
  162 + else:
  163 + self.log_test_result("客户端停止", False, "客户端停止失败")
  164 +
  165 + except Exception as e:
  166 + self.log_test_result("连接模拟", False, f"错误: {e}")
  167 +
  168 + def test_message_queue(self):
  169 + """测试消息队列"""
  170 + try:
  171 + client = self.test_funasr_client_creation()
  172 + if not client:
  173 + return
  174 +
  175 + # 测试消息入队
  176 + test_message = {"test": "message"}
  177 + client.message_queue.put(test_message)
  178 +
  179 + # 检查队列
  180 + if not client.message_queue.empty():
  181 + retrieved_message = client.message_queue.get_nowait()
  182 + if retrieved_message == test_message:
  183 + self.log_test_result("消息队列", True, "消息队列正常工作")
  184 + else:
  185 + self.log_test_result("消息队列", False, "消息内容不匹配")
  186 + else:
  187 + self.log_test_result("消息队列", False, "消息队列为空")
  188 +
  189 + except Exception as e:
  190 + self.log_test_result("消息队列", False, f"错误: {e}")
  191 +
  192 + def test_config_loading(self):
  193 + """测试配置加载"""
  194 + try:
  195 + import config_util as cfg
  196 +
  197 + # 检查关键配置项
  198 + required_configs = [
  199 + 'local_asr_ip',
  200 + 'local_asr_port',
  201 + 'asr_timeout',
  202 + 'asr_reconnect_delay',
  203 + 'asr_max_reconnect_attempts'
  204 + ]
  205 +
  206 + missing_configs = []
  207 + for config_key in required_configs:
  208 + try:
  209 + if hasattr(cfg, 'config'):
  210 + value = cfg.config.get(config_key)
  211 + else:
  212 + value = getattr(cfg, config_key, None)
  213 + if value is None:
  214 + missing_configs.append(config_key)
  215 + except:
  216 + missing_configs.append(config_key)
  217 +
  218 + if not missing_configs:
  219 + self.log_test_result("配置加载", True, "所有必需配置项存在")
  220 + else:
  221 + self.log_test_result(
  222 + "配置加载",
  223 + False,
  224 + f"缺少配置项: {', '.join(missing_configs)}"
  225 + )
  226 +
  227 + except Exception as e:
  228 + self.log_test_result("配置加载", False, f"错误: {e}")
  229 +
  230 + def run_all_tests(self):
  231 + """运行所有测试"""
  232 + print("\n" + "="*60)
  233 + print("FunASR集成测试开始")
  234 + print("="*60)
  235 +
  236 + # 运行各项测试
  237 + self.test_config_loading()
  238 + self.test_funasr_client_creation()
  239 + self.test_compatibility_wrapper()
  240 + self.test_callback_mechanism()
  241 + self.test_message_queue()
  242 + self.test_audio_file_existence()
  243 + self.test_connection_simulation()
  244 +
  245 + # 输出测试总结
  246 + print("\n" + "="*60)
  247 + print("测试总结")
  248 + print("="*60)
  249 +
  250 + passed_tests = sum(1 for _, success, _ in self.test_results if success)
  251 + total_tests = len(self.test_results)
  252 +
  253 + print(f"总测试数: {total_tests}")
  254 + print(f"通过测试: {passed_tests}")
  255 + print(f"失败测试: {total_tests - passed_tests}")
  256 + print(f"成功率: {passed_tests/total_tests*100:.1f}%")
  257 +
  258 + # 显示失败的测试
  259 + failed_tests = [(name, msg) for name, success, msg in self.test_results if not success]
  260 + if failed_tests:
  261 + print("\n失败的测试:")
  262 + for name, msg in failed_tests:
  263 + print(f" - {name}: {msg}")
  264 +
  265 + print("\n" + "="*60)
  266 +
  267 + return passed_tests == total_tests
  268 +
  269 +def main():
  270 + """主函数"""
  271 + tester = TestFunASRIntegration()
  272 + success = tester.run_all_tests()
  273 +
  274 + if success:
  275 + print("\n🎉 所有测试通过!FunASR集成准备就绪。")
  276 + else:
  277 + print("\n⚠️ 部分测试失败,请检查相关配置和依赖。")
  278 +
  279 + return 0 if success else 1
  280 +
  281 +if __name__ == "__main__":
  282 + exit(main())
@@ -90,7 +90,7 @@ class BaseTTS: @@ -90,7 +90,7 @@ class BaseTTS:
90 ########################################################################################### 90 ###########################################################################################
91 class EdgeTTS(BaseTTS): 91 class EdgeTTS(BaseTTS):
92 def txt_to_audio(self,msg): 92 def txt_to_audio(self,msg):
93 - voicename = "zh-CN-XiaoxiaoNeural" 93 + voicename = "zh-CN-YunyangNeural"
94 text,textevent = msg 94 text,textevent = msg
95 t = time.time() 95 t = time.time()
96 asyncio.new_event_loop().run_until_complete(self.__main(voicename,text)) 96 asyncio.new_event_loop().run_until_complete(self.__main(voicename,text))
  1 +# -*- coding: utf-8 -*-
  2 +"""
  3 +AIfeng/2025-01-27
  4 +工具函数模块
  5 +提供日志、打印等基础功能
  6 +"""
  7 +
  8 +import time
  9 +import datetime
  10 +from typing import Any
  11 +
  12 +def printInfo(level: int, username: str, message: str):
  13 + """打印信息
  14 +
  15 + Args:
  16 + level: 日志级别 (0-DEBUG, 1-INFO, 2-WARN, 3-ERROR)
  17 + username: 用户名
  18 + message: 消息内容
  19 + """
  20 + level_names = ['DEBUG', 'INFO', 'WARN', 'ERROR']
  21 + level_name = level_names[min(level, 3)]
  22 + timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  23 + print(f"[{timestamp}] [{level_name}] [{username}] {message}")
  24 +
  25 +def log(level: int, message: str):
  26 + """记录日志
  27 +
  28 + Args:
  29 + level: 日志级别
  30 + message: 日志消息
  31 + """
  32 + printInfo(level, 'SYSTEM', message)
  33 +
  34 +def get_timestamp() -> str:
  35 + """获取当前时间戳字符串"""
  36 + return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  37 +
  38 +def format_duration(seconds: float) -> str:
  39 + """格式化时长
  40 +
  41 + Args:
  42 + seconds: 秒数
  43 +
  44 + Returns:
  45 + 格式化的时长字符串
  46 + """
  47 + if seconds < 60:
  48 + return f"{seconds:.2f}秒"
  49 + elif seconds < 3600:
  50 + minutes = int(seconds // 60)
  51 + secs = seconds % 60
  52 + return f"{minutes}分{secs:.1f}秒"
  53 + else:
  54 + hours = int(seconds // 3600)
  55 + minutes = int((seconds % 3600) // 60)
  56 + secs = seconds % 60
  57 + return f"{hours}时{minutes}分{secs:.1f}秒"
  58 +
  59 +def safe_print(obj: Any, prefix: str = ""):
  60 + """安全打印对象,避免编码错误
  61 +
  62 + Args:
  63 + obj: 要打印的对象
  64 + prefix: 前缀字符串
  65 + """
  66 + try:
  67 + print(f"{prefix}{obj}")
  68 + except UnicodeEncodeError:
  69 + print(f"{prefix}{repr(obj)}")
  70 + except Exception as e:
  71 + print(f"{prefix}[打印错误: {e}]")
  72 +
  73 +class Timer:
  74 + """简单的计时器类"""
  75 +
  76 + def __init__(self):
  77 + self.start_time = None
  78 + self.end_time = None
  79 +
  80 + def start(self):
  81 + """开始计时"""
  82 + self.start_time = time.time()
  83 + return self
  84 +
  85 + def stop(self):
  86 + """停止计时"""
  87 + self.end_time = time.time()
  88 + return self
  89 +
  90 + def elapsed(self) -> float:
  91 + """获取经过的时间(秒)"""
  92 + if self.start_time is None:
  93 + return 0.0
  94 + end = self.end_time if self.end_time else time.time()
  95 + return end - self.start_time
  96 +
  97 + def elapsed_str(self) -> str:
  98 + """获取格式化的经过时间"""
  99 + return format_duration(self.elapsed())
  100 +
  101 + def __enter__(self):
  102 + return self.start()
  103 +
  104 + def __exit__(self, exc_type, exc_val, exc_tb):
  105 + self.stop()
@@ -276,10 +276,11 @@ @@ -276,10 +276,11 @@
276 margin: 0 6px; 276 margin: 0 6px;
277 flex-shrink: 0; 277 flex-shrink: 0;
278 border: 1px solid rgba(255,255,255,0.2); 278 border: 1px solid rgba(255,255,255,0.2);
  279 + background-color: rgba(255,255,255,0.5);
279 } 280 }
280 281
281 #chatOverlay .text-container { 282 #chatOverlay .text-container {
282 - background-color: rgba(255,255,255,0.95); 283 + background-color: rgba(255,255,255,0.5);
283 border-radius: 12px; 284 border-radius: 12px;
284 padding: 8px 12px; 285 padding: 8px 12px;
285 max-width: 75%; 286 max-width: 75%;
@@ -289,40 +290,40 @@ @@ -289,40 +290,40 @@
289 } 290 }
290 291
291 #chatOverlay .message.right .text-container { 292 #chatOverlay .message.right .text-container {
292 - background-color: #4285f4; 293 + background-color: rgba(66,133,244,0.5);
293 color: white; 294 color: white;
294 } 295 }
295 296
296 /* 数字人回复样式 - 根据模式区分 */ 297 /* 数字人回复样式 - 根据模式区分 */
297 #chatOverlay .message.left .text-container { 298 #chatOverlay .message.left .text-container {
298 - background-color: rgba(248,249,250,0.95); 299 + background-color: rgba(248,249,250,0.5);
299 border-left: 3px solid #4285f4; 300 border-left: 3px solid #4285f4;
300 } 301 }
301 302
302 /* Echo模式 - 回音重复 */ 303 /* Echo模式 - 回音重复 */
303 #chatOverlay .message.left.mode-echo .text-container { 304 #chatOverlay .message.left.mode-echo .text-container {
304 - background-color: rgba(255,235,59,0.9); 305 + background-color: rgba(255,235,59,0.5);
305 border-left: 3px solid #FFC107; 306 border-left: 3px solid #FFC107;
306 color: #333; 307 color: #333;
307 } 308 }
308 309
309 /* Chat模式 - 大模型回复 */ 310 /* Chat模式 - 大模型回复 */
310 #chatOverlay .message.left.mode-chat .text-container { 311 #chatOverlay .message.left.mode-chat .text-container {
311 - background-color: rgba(76,175,80,0.9); 312 + background-color: rgba(76,175,80,0.5);
312 border-left: 3px solid #4CAF50; 313 border-left: 3px solid #4CAF50;
313 color: white; 314 color: white;
314 } 315 }
315 316
316 /* Audio模式 - 语音识别回复 */ 317 /* Audio模式 - 语音识别回复 */
317 #chatOverlay .message.left.mode-audio .text-container { 318 #chatOverlay .message.left.mode-audio .text-container {
318 - background-color: rgba(156,39,176,0.9); 319 + background-color: rgba(156,39,176,0.5);
319 border-left: 3px solid #9C27B0; 320 border-left: 3px solid #9C27B0;
320 color: white; 321 color: white;
321 } 322 }
322 323
323 /* Plaintext模式 - 纯文本 */ 324 /* Plaintext模式 - 纯文本 */
324 #chatOverlay .message.left.mode-plaintext .text-container { 325 #chatOverlay .message.left.mode-plaintext .text-container {
325 - background-color: rgba(96,125,139,0.9); 326 + background-color: rgba(96,125,139,0.5);
326 border-left: 3px solid #607D8B; 327 border-left: 3px solid #607D8B;
327 color: white; 328 color: white;
328 } 329 }
@@ -421,24 +422,31 @@ @@ -421,24 +422,31 @@
421 } 422 }
422 423
423 /* 响应式适配 */ 424 /* 响应式适配 */
  425 + @media (max-width: 2560px) {
  426 + #chatOverlay {
  427 + width: min(800px, 40vw) !important;
  428 + height: 270px !important;
  429 + }
  430 + }
  431 + /* 响应式适配 */
424 @media (max-width: 2160px) { 432 @media (max-width: 2160px) {
425 #chatOverlay { 433 #chatOverlay {
426 - width: min(600px, 32vw) !important;  
427 - height: 180px !important; 434 + width: min(800px, 40vw) !important;
  435 + height: 270px !important;
428 } 436 }
429 } 437 }
430 438
431 /* 响应式适配 */ 439 /* 响应式适配 */
432 @media (max-width: 1200px) { 440 @media (max-width: 1200px) {
433 #chatOverlay { 441 #chatOverlay {
434 - width: min(400px, 32vw) !important; 442 + width: min(600px, 40vw) !important;
435 height: 180px !important; 443 height: 180px !important;
436 } 444 }
437 } 445 }
438 446
439 @media (max-width: 768px) { 447 @media (max-width: 768px) {
440 #chatOverlay { 448 #chatOverlay {
441 - width: min(280px, 38vw) !important; 449 + width: min(300px, 40vw) !important;
442 height: 160px !important; 450 height: 160px !important;
443 bottom: 10px !important; 451 bottom: 10px !important;
444 right: 10px !important; 452 right: 10px !important;
@@ -505,6 +513,25 @@ @@ -505,6 +513,25 @@
505 </div> 513 </div>
506 514
507 <div> 515 <div>
  516 + <div class="section-title">对话框配置</div>
  517 + <div class="option">
  518 + <input id="show-chat-overlay" type="checkbox" checked/>
  519 + <label for="show-chat-overlay">显示对话框</label>
  520 + </div>
  521 + <div class="form-group">
  522 + <label for="chat-overlay-opacity">对话框透明度</label>
  523 + <input type="range" class="form-control" id="chat-overlay-opacity" min="10" max="90" value="50" step="10">
  524 + <small class="form-text text-muted">当前: <span id="opacity-value">50</span>%</small>
  525 + </div>
  526 + <div class="form-group">
  527 + <label for="message-opacity">消息框透明度</label>
  528 + <input type="range" class="form-control" id="message-opacity" min="10" max="90" value="50" step="10">
  529 + <small class="form-text text-muted">当前: <span id="message-opacity-value">50</span>%</small>
  530 + </div>
  531 + <button id="reset-chat-config" class="btn btn-secondary">重置配置</button>
  532 + </div>
  533 +
  534 + <div>
508 <div class="section-title">本地存储设置</div> 535 <div class="section-title">本地存储设置</div>
509 <div class="option"> 536 <div class="option">
510 <input id="enable-storage" type="checkbox" checked/> 537 <input id="enable-storage" type="checkbox" checked/>
@@ -559,13 +586,13 @@ @@ -559,13 +586,13 @@
559 <video id="video" autoplay="true" playsinline="true"></video> 586 <video id="video" autoplay="true" playsinline="true"></video>
560 </div> 587 </div>
561 <!-- 聊天消息显示区域 --> 588 <!-- 聊天消息显示区域 -->
562 - <div id="chatOverlay" style="position: absolute; bottom: 15px; right: 15px; width: min(320px, 30vw); height: 200px; overflow: hidden; background-color: rgba(0,0,0,0.6); border-radius: 12px; padding: 8px; color: white; z-index: 1005; backdrop-filter: blur(15px); border: 1px solid rgba(255,255,255,0.08); display: flex; flex-direction: column;"> 589 + <div id="chatOverlay" style="position: absolute; bottom: 15px; right: 15px; width: min(320px, 30vw); height: 200px; overflow: hidden; background-color: rgba(0,0,0,0.5); border-radius: 12px; padding: 8px; color: white; z-index: 1005; backdrop-filter: blur(15px); border: 1px solid rgba(255,255,255,0.08); display: flex; flex-direction: column;">
563 <div id="chatMessages" style="overflow: hidden; flex: 1; margin-bottom: 3px; display: flex; flex-direction: column; justify-content: flex-end; position: relative; cursor: pointer;"> 590 <div id="chatMessages" style="overflow: hidden; flex: 1; margin-bottom: 3px; display: flex; flex-direction: column; justify-content: flex-end; position: relative; cursor: pointer;">
564 <!-- 消息将在这里动态添加 --> 591 <!-- 消息将在这里动态添加 -->
565 </div> 592 </div>
566 <div class="chat-header"> 593 <div class="chat-header">
567 💬 对话 594 💬 对话
568 - <button class="clear-chat" onclick="clearChatHistory()" title="清空对话记录"></button> 595 + <button class="clear-chat" onclick="toggleChatOverlay()" title="隐藏对话框"></button>
569 </div> 596 </div>
570 </div> 597 </div>
571 </div> 598 </div>
@@ -939,6 +966,62 @@ @@ -939,6 +966,62 @@
939 if (storageEnabled) { 966 if (storageEnabled) {
940 setTimeout(loadChatHistory, 1000); // 延迟1秒加载,确保页面完全加载 967 setTimeout(loadChatHistory, 1000); // 延迟1秒加载,确保页面完全加载
941 } 968 }
  969 +
  970 + // 初始化对话框配置
  971 + loadChatOverlayConfig();
  972 +
  973 + // 对话框显示/隐藏开关
  974 + $('#show-chat-overlay').change(function() {
  975 + const chatOverlay = document.getElementById('chatOverlay');
  976 + if (this.checked) {
  977 + chatOverlay.style.display = 'flex';
  978 + localStorage.setItem('chatOverlayVisible', 'true');
  979 + } else {
  980 + chatOverlay.style.display = 'none';
  981 + localStorage.setItem('chatOverlayVisible', 'false');
  982 + }
  983 + });
  984 +
  985 + // 对话框透明度滑块
  986 + $('#chat-overlay-opacity').on('input', function() {
  987 + const opacity = this.value;
  988 + $('#opacity-value').text(opacity);
  989 + updateChatOverlayOpacity(parseInt(opacity));
  990 + });
  991 +
  992 + // 消息框透明度滑块
  993 + $('#message-opacity').on('input', function() {
  994 + const opacity = this.value;
  995 + $('#message-opacity-value').text(opacity);
  996 + updateMessageOpacity(parseInt(opacity));
  997 + });
  998 +
  999 + // 重置对话框配置
  1000 + $('#reset-chat-config').click(function() {
  1001 + // 重置为默认值
  1002 + $('#show-chat-overlay').prop('checked', true);
  1003 + $('#chat-overlay-opacity').val(50);
  1004 + $('#opacity-value').text('50');
  1005 + $('#message-opacity').val(50);
  1006 + $('#message-opacity-value').text('50');
  1007 +
  1008 + // 应用默认设置
  1009 + document.getElementById('chatOverlay').style.display = 'flex';
  1010 + updateChatOverlayOpacity(50);
  1011 + updateMessageOpacity(50);
  1012 +
  1013 + // 清除本地存储
  1014 + localStorage.removeItem('chatOverlayVisible');
  1015 + localStorage.removeItem('chatOverlayOpacity');
  1016 + localStorage.removeItem('messageOpacity');
  1017 +
  1018 + // 提示用户
  1019 + const originalText = $(this).text();
  1020 + $(this).text('已重置!').prop('disabled', true);
  1021 + setTimeout(() => {
  1022 + $(this).text(originalText).prop('disabled', false);
  1023 + }, 1500);
  1024 + });
942 }); 1025 });
943 1026
944 $('#btn_start_record').click(function() { 1027 $('#btn_start_record').click(function() {
@@ -1279,6 +1362,94 @@ @@ -1279,6 +1362,94 @@
1279 } 1362 }
1280 localStorage.removeItem('chatHistory'); 1363 localStorage.removeItem('chatHistory');
1281 } 1364 }
  1365 +
  1366 + // 切换对话框显示/隐藏
  1367 + function toggleChatOverlay() {
  1368 + const chatOverlay = document.getElementById('chatOverlay');
  1369 + const showCheckbox = document.getElementById('show-chat-overlay');
  1370 +
  1371 + if (chatOverlay.style.display === 'none') {
  1372 + chatOverlay.style.display = 'flex';
  1373 + showCheckbox.checked = true;
  1374 + localStorage.setItem('chatOverlayVisible', 'true');
  1375 + } else {
  1376 + chatOverlay.style.display = 'none';
  1377 + showCheckbox.checked = false;
  1378 + localStorage.setItem('chatOverlayVisible', 'false');
  1379 + }
  1380 + }
  1381 +
  1382 + // 更新对话框透明度
  1383 + function updateChatOverlayOpacity(opacity) {
  1384 + const chatOverlay = document.getElementById('chatOverlay');
  1385 + const newBgColor = `rgba(0,0,0,${opacity / 100})`;
  1386 + chatOverlay.style.backgroundColor = newBgColor;
  1387 + localStorage.setItem('chatOverlayOpacity', opacity);
  1388 + }
  1389 +
  1390 + // 更新消息框透明度
  1391 + function updateMessageOpacity(opacity) {
  1392 + const style = document.createElement('style');
  1393 + style.id = 'dynamic-message-opacity';
  1394 +
  1395 + // 移除旧的样式
  1396 + const oldStyle = document.getElementById('dynamic-message-opacity');
  1397 + if (oldStyle) {
  1398 + oldStyle.remove();
  1399 + }
  1400 +
  1401 + style.innerHTML = `
  1402 + #chatOverlay .text-container {
  1403 + background-color: rgba(255,255,255,${opacity / 100}) !important;
  1404 + }
  1405 + #chatOverlay .message.right .text-container {
  1406 + background-color: rgba(66,133,244,${opacity / 100}) !important;
  1407 + }
  1408 + #chatOverlay .message.left .text-container {
  1409 + background-color: rgba(248,249,250,${opacity / 100}) !important;
  1410 + }
  1411 + #chatOverlay .message.left.mode-echo .text-container {
  1412 + background-color: rgba(255,235,59,${opacity / 100}) !important;
  1413 + }
  1414 + #chatOverlay .message.left.mode-chat .text-container {
  1415 + background-color: rgba(76,175,80,${opacity / 100}) !important;
  1416 + }
  1417 + #chatOverlay .message.left.mode-audio .text-container {
  1418 + background-color: rgba(156,39,176,${opacity / 100}) !important;
  1419 + }
  1420 + #chatOverlay .message.left.mode-plaintext .text-container {
  1421 + background-color: rgba(96,125,139,${opacity / 100}) !important;
  1422 + }
  1423 + #chatOverlay .avatar {
  1424 + background-color: rgba(255,255,255,${opacity / 100}) !important;
  1425 + }
  1426 + `;
  1427 +
  1428 + document.head.appendChild(style);
  1429 + localStorage.setItem('messageOpacity', opacity);
  1430 + }
  1431 +
  1432 + // 加载对话框配置
  1433 + function loadChatOverlayConfig() {
  1434 + // 加载显示状态
  1435 + const isVisible = localStorage.getItem('chatOverlayVisible');
  1436 + if (isVisible === 'false') {
  1437 + document.getElementById('chatOverlay').style.display = 'none';
  1438 + document.getElementById('show-chat-overlay').checked = false;
  1439 + }
  1440 +
  1441 + // 加载透明度设置
  1442 + const overlayOpacity = localStorage.getItem('chatOverlayOpacity') || '50';
  1443 + const messageOpacity = localStorage.getItem('messageOpacity') || '50';
  1444 +
  1445 + document.getElementById('chat-overlay-opacity').value = overlayOpacity;
  1446 + document.getElementById('opacity-value').textContent = overlayOpacity;
  1447 + updateChatOverlayOpacity(parseInt(overlayOpacity));
  1448 +
  1449 + document.getElementById('message-opacity').value = messageOpacity;
  1450 + document.getElementById('message-opacity-value').textContent = messageOpacity;
  1451 + updateMessageOpacity(parseInt(messageOpacity));
  1452 + }
1282 1453
1283 // 初始化聊天滚轮支持 1454 // 初始化聊天滚轮支持
1284 function initChatWheelSupport() { 1455 function initChatWheelSupport() {
No preview for this file type
No preview for this file type
No preview for this file type