Py update config (#1120)

* update:优化获取默认配置

* update:优化未绑定用户的连接

* update:修复智控台模式下,所选模块的日志名称

* update:优化参数配置敏感密钥的显示方式

* update:更新服务器配置并重新初始化组件
This commit is contained in:
欣南科技
2025-05-07 09:15:52 +08:00
committed by GitHub
11 changed files with 719 additions and 254 deletions
@@ -61,15 +61,15 @@ public class ConfigServiceImpl implements ConfigService {
// 构建模块配置 // 构建模块配置
buildModuleConfig( buildModuleConfig(
agent.getAgentName(), null,
null, null,
null, null,
agent.getVadModelId(), agent.getVadModelId(),
agent.getAsrModelId(), agent.getAsrModelId(),
agent.getLlmModelId(), null,
agent.getTtsModelId(), null,
agent.getMemModelId(), null,
agent.getIntentModelId(), null,
result, result,
isCache); isCache);
@@ -117,18 +117,6 @@ public class ConfigServiceImpl implements ConfigService {
if (alreadySelectedAsrModelId != null && alreadySelectedAsrModelId.equals(agent.getAsrModelId())) { if (alreadySelectedAsrModelId != null && alreadySelectedAsrModelId.equals(agent.getAsrModelId())) {
agent.setAsrModelId(null); agent.setAsrModelId(null);
} }
String alreadySelectedLlmModelId = (String) selectedModule.get("LLM");
if (alreadySelectedLlmModelId != null && alreadySelectedLlmModelId.equals(agent.getLlmModelId())) {
agent.setLlmModelId(null);
}
String alreadySelectedMemModelId = (String) selectedModule.get("Memory");
if (alreadySelectedMemModelId != null && alreadySelectedMemModelId.equals(agent.getMemModelId())) {
agent.setMemModelId(null);
}
String alreadySelectedIntentModelId = (String) selectedModule.get("Intent");
if (alreadySelectedIntentModelId != null && alreadySelectedIntentModelId.equals(agent.getIntentModelId())) {
agent.setIntentModelId(null);
}
// 构建模块配置 // 构建模块配置
buildModuleConfig( buildModuleConfig(
@@ -25,8 +25,19 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="参数编码" prop="paramCode" align="center"></el-table-column> <el-table-column label="参数编码" prop="paramCode" align="center"></el-table-column>
<el-table-column label="参数值" prop="paramValue" align="center" <el-table-column label="参数值" prop="paramValue" align="center" show-overflow-tooltip>
show-overflow-tooltip></el-table-column> <template slot-scope="scope">
<div v-if="isSensitiveParam(scope.row.paramCode)">
<span v-if="!scope.row.showValue">{{ maskSensitiveValue(scope.row.paramValue)
}}</span>
<span v-else>{{ scope.row.paramValue }}</span>
<el-button size="mini" type="text" @click="toggleSensitiveValue(scope.row)">
{{ scope.row.showValue ? '隐藏' : '查看' }}
</el-button>
</div>
<span v-else>{{ scope.row.paramValue }}</span>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" align="center"></el-table-column> <el-table-column label="备注" prop="remark" align="center"></el-table-column>
<el-table-column label="操作" align="center"> <el-table-column label="操作" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
@@ -100,6 +111,7 @@ export default {
dialogVisible: false, dialogVisible: false,
dialogTitle: "新增参数", dialogTitle: "新增参数",
isAllSelected: false, isAllSelected: false,
sensitive_keys: ["api_key", "personal_access_token", "access_token", "token", "secret", "access_key_secret", "secret_key"],
paramForm: { paramForm: {
id: null, id: null,
paramCode: "", paramCode: "",
@@ -152,7 +164,8 @@ export default {
if (data.code === 0) { if (data.code === 0) {
this.paramsList = data.data.list.map(item => ({ this.paramsList = data.data.list.map(item => ({
...item, ...item,
selected: false selected: false,
showValue: false
})); }));
this.total = data.data.total; this.total = data.data.total;
} else { } else {
@@ -314,7 +327,18 @@ export default {
goToPage(page) { goToPage(page) {
this.currentPage = page; this.currentPage = page;
this.fetchParams(); this.fetchParams();
} },
isSensitiveParam(paramCode) {
return this.sensitive_keys.some(key => paramCode.toLowerCase().includes(key.toLowerCase()));
},
maskSensitiveValue(value) {
if (!value) return '';
if (value.length <= 8) return '****';
return value.substring(0, 4) + '****' + value.substring(value.length - 4);
},
toggleSensitiveValue(row) {
this.$set(row, 'showValue', !row.showValue);
},
}, },
}; };
</script> </script>
+10 -4
View File
@@ -8,10 +8,16 @@ SERVER_VERSION = "0.3.14"
def get_module_abbreviation(module_name, module_dict): def get_module_abbreviation(module_name, module_dict):
"""获取模块名称的缩写,如果为空则返回00""" """获取模块名称的缩写,如果为空则返回00
return ( 如果名称中包含下划线,则返回下划线后面的前两个字符
module_dict.get(module_name, "")[:2] if module_dict.get(module_name) else "00" """
) module_value = module_dict.get(module_name, "")
if not module_value:
return "00"
if "_" in module_value:
parts = module_value.split("_")
return parts[-1][:2] if parts[-1] else "00"
return module_value[:2]
def build_module_string(selected_module): def build_module_string(selected_module):
+7 -75
View File
@@ -53,9 +53,9 @@ class ConnectionHandler:
server=None, server=None,
): ):
self.config = config self.config = config
self.server = server
self.logger = setup_logging() self.logger = setup_logging()
self.auth = AuthMiddleware(config) self.auth = AuthMiddleware(config)
self.server = server # 保存server实例的引用
self.need_bind = False self.need_bind = False
self.bind_code = None self.bind_code = None
@@ -151,11 +151,9 @@ class ConnectionHandler:
self.headers["device-id"] = query_params["device-id"][0] self.headers["device-id"] = query_params["device-id"][0]
self.headers["client-id"] = query_params["client-id"][0] self.headers["client-id"] = query_params["client-id"][0]
else: else:
self.logger.bind(tag=TAG).error( await ws.send("端口正常,如需测试连接,请使用test_page.html")
"无法从请求头和URL查询参数中获取device-id" await self.close(ws)
)
return return
# 获取客户端ip地址 # 获取客户端ip地址
self.client_ip = ws.remote_address[0] self.client_ip = ws.remote_address[0]
self.logger.bind(tag=TAG).info( self.logger.bind(tag=TAG).info(
@@ -212,7 +210,8 @@ class ConnectionHandler:
async def _save_and_close(self, ws): async def _save_and_close(self, ws):
"""保存记忆并关闭连接""" """保存记忆并关闭连接"""
try: try:
await self.memory.save_memory(self.dialogue.dialogue) if self.memory:
await self.memory.save_memory(self.dialogue.dialogue)
except Exception as e: except Exception as e:
self.logger.bind(tag=TAG).error(f"保存记忆失败: {e}") self.logger.bind(tag=TAG).error(f"保存记忆失败: {e}")
finally: finally:
@@ -234,74 +233,6 @@ class ConnectionHandler:
elif isinstance(message, bytes): elif isinstance(message, bytes):
await handleAudioMessage(self, message) await handleAudioMessage(self, message)
async def handle_config_update(self, message):
"""处理配置更新请求"""
content = message.get("content", {})
new_config = content
# 遍历所有支持的配置模块
updated_modules = []
for config_model in ["tts", "llm", "vad", "asr", "memory", "intent"]:
if config_model not in new_config:
continue
new_content = new_config[config_model]
old_content = self.config.get(config_model, {})
# 记录配置变更
self.logger.bind(tag=TAG).info(
f"配置更新: {config_model} 旧值: {json.dumps(old_content, ensure_ascii=False)} "
f"新值: {json.dumps(new_content, ensure_ascii=False)}"
)
# 深度合并配置
if isinstance(old_content, dict) and isinstance(new_content, dict):
merged = {**old_content, **new_content}
self.config[config_model] = merged
else:
self.config[config_model] = new_content
# 标记需要重新初始化的模块
if config_model in ["llm", "tts", "asr", "vad", "intent", "memory"]:
updated_modules.append(config_model)
# 同步更新 WebSocketServer 的配置
if self.server:
async with self.server.config_lock: # 使用锁确保线程安全
for config_model in updated_modules:
self.server.config[config_model].update(new_config[config_model])
# 批量初始化模块
if updated_modules:
try:
self._initialize_components(self.config)
self.logger.bind(tag=TAG).info(
f"已重新初始化模块: {', '.join(updated_modules)}"
)
except Exception as e:
self.logger.bind(tag=TAG).error(f"模块初始化失败: {str(e)}")
await self.websocket.send(
json.dumps(
{
"type": "config_update_response",
"status": "error",
"message": f"模块初始化失败: {str(e)}",
}
)
)
return
# 返回成功响应
await self.websocket.send(
json.dumps(
{
"type": "config_update_response",
"status": "success",
"message": f"已更新配置: {', '.join(updated_modules)}",
}
)
)
def _initialize_components(self, private_config): def _initialize_components(self, private_config):
"""初始化组件""" """初始化组件"""
if private_config is not None: if private_config is not None:
@@ -318,7 +249,7 @@ class ConnectionHandler:
def _init_report_threads(self): def _init_report_threads(self):
"""初始化ASR和TTS上报线程""" """初始化ASR和TTS上报线程"""
if not self.read_config_from_api: if not self.read_config_from_api or self.need_bind:
return return
if self.tts_report_thread is None or not self.tts_report_thread.is_alive(): if self.tts_report_thread is None or not self.tts_report_thread.is_alive():
self.tts_report_thread = threading.Thread( self.tts_report_thread = threading.Thread(
@@ -1094,6 +1025,7 @@ def filter_sensitive_info(config: dict) -> dict:
"personal_access_token", "personal_access_token",
"access_token", "access_token",
"token", "token",
"secret",
"access_key_secret", "access_key_secret",
"secret_key", "secret_key",
] ]
@@ -44,7 +44,7 @@ async def checkWakeupWords(conn, text):
if file is None: if file is None:
asyncio.create_task(wakeupWordsResponse(conn)) asyncio.create_task(wakeupWordsResponse(conn))
return False return False
opus_packets, duration = conn.tts.audio_to_opus_data(file) opus_packets, _ = conn.tts.audio_to_opus_data(file)
text_hello = WAKEUP_CONFIG["text"] text_hello = WAKEUP_CONFIG["text"]
if not text_hello: if not text_hello:
text_hello = text text_hello = text
@@ -6,6 +6,7 @@ from core.handle.sendAudioHandle import send_stt_message
from core.handle.intentHandler import handle_user_intent from core.handle.intentHandler import handle_user_intent
from core.utils.output_counter import check_device_output_limit from core.utils.output_counter import check_device_output_limit
from core.handle.ttsReportHandle import enqueue_tts_report from core.handle.ttsReportHandle import enqueue_tts_report
from core.providers.tts.base import audio_to_opus_data
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
@@ -110,7 +111,7 @@ async def max_out_size(conn):
conn.tts_last_text_index = 0 conn.tts_last_text_index = 0
conn.llm_finish_task = True conn.llm_finish_task = True
file_path = "config/assets/max_output_size.wav" file_path = "config/assets/max_output_size.wav"
opus_packets, _ = conn.tts.audio_to_opus_data(file_path) opus_packets, _ = audio_to_opus_data(file_path)
conn.audio_play_queue.put((opus_packets, text, 0)) conn.audio_play_queue.put((opus_packets, text, 0))
conn.close_after_chat = True conn.close_after_chat = True
@@ -132,7 +133,7 @@ async def check_bind_device(conn):
# 播放提示音 # 播放提示音
music_path = "config/assets/bind_code.wav" music_path = "config/assets/bind_code.wav"
opus_packets, _ = conn.tts.audio_to_opus_data(music_path) opus_packets, _ = audio_to_opus_data(music_path)
conn.audio_play_queue.put((opus_packets, text, 0)) conn.audio_play_queue.put((opus_packets, text, 0))
# 逐个播放数字 # 逐个播放数字
@@ -140,7 +141,7 @@ async def check_bind_device(conn):
try: try:
digit = conn.bind_code[i] digit = conn.bind_code[i]
num_path = f"config/assets/bind_code/{digit}.wav" num_path = f"config/assets/bind_code/{digit}.wav"
num_packets, _ = conn.tts.audio_to_opus_data(num_path) num_packets, _ = audio_to_opus_data(num_path)
conn.audio_play_queue.put((num_packets, None, i + 1)) conn.audio_play_queue.put((num_packets, None, i + 1))
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"播放数字音频失败: {e}") logger.bind(tag=TAG).error(f"播放数字音频失败: {e}")
@@ -152,5 +153,5 @@ async def check_bind_device(conn):
conn.tts_last_text_index = 0 conn.tts_last_text_index = 0
conn.llm_finish_task = True conn.llm_finish_task = True
music_path = "config/assets/bind_not_found.wav" music_path = "config/assets/bind_not_found.wav"
opus_packets, _ = conn.tts.audio_to_opus_data(music_path) opus_packets, _ = audio_to_opus_data(music_path)
conn.audio_play_queue.put((opus_packets, text, 0)) conn.audio_play_queue.put((opus_packets, text, 0))
+48 -2
View File
@@ -80,7 +80,7 @@ async def handleTextMessage(conn, message):
await conn.websocket.send( await conn.websocket.send(
json.dumps( json.dumps(
{ {
"type": "config_update_response", "type": "server",
"status": "error", "status": "error",
"message": "服务器密钥验证失败", "message": "服务器密钥验证失败",
} }
@@ -89,6 +89,52 @@ async def handleTextMessage(conn, message):
return return
# 动态更新配置 # 动态更新配置
if msg_json["action"] == "update_config": if msg_json["action"] == "update_config":
await conn.handle_config_update(msg_json) try:
# 更新WebSocketServer的配置
if not conn.server:
await conn.websocket.send(
json.dumps(
{
"type": "config_update_response",
"status": "error",
"message": "无法获取服务器实例",
}
)
)
return
if not await conn.server.update_config():
await conn.websocket.send(
json.dumps(
{
"type": "config_update_response",
"status": "error",
"message": "更新服务器配置失败",
}
)
)
return
# 发送成功响应
await conn.websocket.send(
json.dumps(
{
"type": "config_update_response",
"status": "success",
"message": "配置更新成功",
}
)
)
except Exception as e:
conn.logger.bind(tag=TAG).error(f"更新配置失败: {str(e)}")
await conn.websocket.send(
json.dumps(
{
"type": "config_update_response",
"status": "error",
"message": f"更新配置失败: {str(e)}",
}
)
)
except json.JSONDecodeError: except json.JSONDecodeError:
await conn.websocket.send(message) await conn.websocket.send(message)
@@ -95,7 +95,7 @@ def opus_to_wav(opus_data):
def enqueue_tts_report(conn, type, text, opus_data): def enqueue_tts_report(conn, type, text, opus_data):
if not conn.read_config_from_api: if not conn.read_config_from_api or conn.need_bind:
return return
"""将TTS数据加入上报队列 """将TTS数据加入上报队列
@@ -108,7 +108,7 @@ def enqueue_tts_report(conn, type, text, opus_data):
# 使用连接对象的队列,传入文本和二进制数据而非文件路径 # 使用连接对象的队列,传入文本和二进制数据而非文件路径
conn.tts_report_queue.put((type, text, opus_data)) conn.tts_report_queue.put((type, text, opus_data))
logger.bind(tag=TAG).info( logger.bind(tag=TAG).debug(
f"TTS数据已加入上报队列: {conn.device_id}, 音频大小: {len(opus_data)} " f"TTS数据已加入上报队列: {conn.device_id}, 音频大小: {len(opus_data)} "
) )
except Exception as e: except Exception as e:
+5 -48
View File
@@ -1,11 +1,9 @@
import asyncio import asyncio
from config.logger import setup_logging from config.logger import setup_logging
import os import os
import numpy as np
import opuslib_next
from pydub import AudioSegment
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from core.utils.tts import MarkdownCleaner from core.utils.tts import MarkdownCleaner
from core.utils.util import audio_to_opus_data
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
@@ -29,7 +27,9 @@ class TTSProviderBase(ABC):
try: try:
asyncio.run(self.text_to_speak(text, tmp_file)) asyncio.run(self.text_to_speak(text, tmp_file))
except Exception as e: except Exception as e:
logger.bind(tag=TAG).warning(f"语音生成失败{5 - max_repeat_time + 1}次: {text},错误: {e}") logger.bind(tag=TAG).warning(
f"语音生成失败{5 - max_repeat_time + 1}次: {text},错误: {e}"
)
# 未执行成功,删除文件 # 未执行成功,删除文件
if os.path.exists(tmp_file): if os.path.exists(tmp_file):
os.remove(tmp_file) os.remove(tmp_file)
@@ -54,47 +54,4 @@ class TTSProviderBase(ABC):
pass pass
def audio_to_opus_data(self, audio_file_path): def audio_to_opus_data(self, audio_file_path):
"""音频文件转换为Opus编码""" return audio_to_opus_data(audio_file_path)
# 获取文件后缀名
file_type = os.path.splitext(audio_file_path)[1]
if file_type:
file_type = file_type.lstrip(".")
# 读取音频文件,-nostdin 参数:不要从标准输入读取数据,否则FFmpeg会阻塞
audio = AudioSegment.from_file(
audio_file_path, format=file_type, parameters=["-nostdin"]
)
# 转换为单声道/16kHz采样率/16位小端编码(确保与编码器匹配)
audio = audio.set_channels(1).set_frame_rate(16000).set_sample_width(2)
# 音频时长(秒)
duration = len(audio) / 1000.0
# 获取原始PCM数据(16位小端)
raw_data = audio.raw_data
# 初始化Opus编码器
encoder = opuslib_next.Encoder(16000, 1, opuslib_next.APPLICATION_AUDIO)
# 编码参数
frame_duration = 60 # 60ms per frame
frame_size = int(16000 * frame_duration / 1000) # 960 samples/frame
opus_datas = []
# 按帧处理所有音频数据(包括最后一帧可能补零)
for i in range(0, len(raw_data), frame_size * 2): # 16bit=2bytes/sample
# 获取当前帧的二进制数据
chunk = raw_data[i : i + frame_size * 2]
# 如果最后一帧不足,补零
if len(chunk) < frame_size * 2:
chunk += b"\x00" * (frame_size * 2 - len(chunk))
# 转换为numpy数组处理
np_frame = np.frombuffer(chunk, dtype=np.int16)
# 编码Opus数据
opus_data = encoder.encode(np_frame.tobytes(), frame_size)
opus_datas.append(opus_data)
return opus_datas, duration
+517 -89
View File
@@ -2,35 +2,40 @@ import json
import socket import socket
import subprocess import subprocess
import re import re
import os
import numpy as np
import requests import requests
import opuslib_next
from pydub import AudioSegment
from typing import Dict, Any from typing import Dict, Any
from core.utils import tts, llm, intent, memory, vad, asr from core.utils import tts, llm, intent, memory, vad, asr
TAG = __name__ TAG = __name__
emoji_map = { emoji_map = {
'neutral': '😶', "neutral": "😶",
'happy': '🙂', "happy": "🙂",
'laughing': '😆', "laughing": "😆",
'funny': '😂', "funny": "😂",
'sad': '😔', "sad": "😔",
'angry': '😠', "angry": "😠",
'crying': '😭', "crying": "😭",
'loving': '😍', "loving": "😍",
'embarrassed': '😳', "embarrassed": "😳",
'surprised': '😲', "surprised": "😲",
'shocked': '😱', "shocked": "😱",
'thinking': '🤔', "thinking": "🤔",
'winking': '😉', "winking": "😉",
'cool': '😎', "cool": "😎",
'relaxed': '😌', "relaxed": "😌",
'delicious': '🤤', "delicious": "🤤",
'kissy': '😘', "kissy": "😘",
'confident': '😏', "confident": "😏",
'sleepy': '😴', "sleepy": "😴",
'silly': '😜', "silly": "😜",
'confused': '🙄' "confused": "🙄",
} }
def get_local_ip(): def get_local_ip():
try: try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -117,9 +122,9 @@ def is_punctuation_or_emoji(char):
"", # 中文顿号 "", # 中文顿号
"", "",
"", "",
"\"", # 中文双引号 + 英文引号 '"', # 中文双引号 + 英文引号
"", "",
":", # 中文冒号 + 英文冒号 ":", # 中文冒号 + 英文冒号
} }
if char.isspace() or char in punctuation_set: if char.isspace() or char in punctuation_set:
return True return True
@@ -353,12 +358,13 @@ def initialize_modules(
return modules return modules
def analyze_emotion(text): def analyze_emotion(text):
""" """
分析文本情感并返回对应的emoji名称(支持中英文) 分析文本情感并返回对应的emoji名称(支持中英文)
""" """
if not text or not isinstance(text, str): if not text or not isinstance(text, str):
return 'neutral' return "neutral"
original_text = text original_text = text
text = text.lower().strip() text = text.lower().strip()
@@ -369,84 +375,444 @@ def analyze_emotion(text):
return emotion return emotion
# 标点符号分析 # 标点符号分析
has_exclamation = '!' in original_text or '' in original_text has_exclamation = "!" in original_text or "" in original_text
has_question = '?' in original_text or '' in original_text has_question = "?" in original_text or "" in original_text
has_ellipsis = '...' in original_text or '' in original_text has_ellipsis = "..." in original_text or "" in original_text
# 定义情感关键词映射(中英文扩展版) # 定义情感关键词映射(中英文扩展版)
emotion_keywords = { emotion_keywords = {
'happy': ['开心', '高兴', '快乐', '愉快', '幸福', '满意', '', '', '不错', '完美', '棒极了', '太好了', "happy": [
'好呀', '好的', 'happy', 'joy', 'great', 'good', 'nice', 'awesome', 'fantastic', 'wonderful'], "开心",
'laughing': ['哈哈', '哈哈哈', '呵呵', '嘿嘿', '嘻嘻', '笑死', '太好笑了', '笑死我了', 'lol', 'lmao', 'haha', "高兴",
'hahaha', 'hehe', 'rofl', 'funny', 'laugh'], "快乐",
'funny': ['搞笑', '滑稽', '', '幽默', '笑点', '段子', '笑话', '太逗了', 'hilarious', 'joke', 'comedy'], "愉快",
'sad': ['伤心', '难过', '悲哀', '悲伤', '忧郁', '郁闷', '沮丧', '失望', '想哭', '难受', '不开心', '', '呜呜', "幸福",
'sad', 'upset', 'unhappy', 'depressed', 'sorrow', 'gloomy'], "满意",
'angry': ['生气', '愤怒', '气死', '讨厌', '烦人', '可恶', '烦死了', '恼火', '暴躁', '火大', '愤怒', '气炸了', "",
'angry', 'mad', 'annoyed', 'furious', 'pissed', 'hate'], "",
'crying': ['哭泣', '泪流', '大哭', '伤心欲绝', '泪目', '流泪', '哭死', '哭晕', '想哭', '泪崩', "不错",
'cry', 'crying', 'tears', 'sob', 'weep'], "完美",
'loving': ['爱你', '喜欢', '', '亲爱的', '宝贝', '么么哒', '抱抱', '想你', '思念', '最爱', '亲亲', '喜欢你', "棒极了",
'love', 'like', 'adore', 'darling', 'sweetie', 'honey', 'miss you', 'heart'], "太好了",
'embarrassed': ['尴尬', '不好意思', '害羞', '脸红', '难为情', '社死', '丢脸', '出丑', "好呀",
'embarrassed', 'awkward', 'shy', 'blush'], "好的",
'surprised': ['惊讶', '吃惊', '天啊', '哇塞', '', '居然', '竟然', '没想到', '出乎意料', "happy",
'surprise', 'wow', 'omg', 'oh my god', 'amazing', 'unbelievable'], "joy",
'shocked': ['震惊', '吓到', '惊呆了', '不敢相信', '震撼', '吓死', '恐怖', '害怕', '吓人', "great",
'shocked', 'shocking', 'scared', 'frightened', 'terrified', 'horror'], "good",
'thinking': ['思考', '考虑', '想一下', '琢磨', '沉思', '冥想', '', '思考中', '在想', "nice",
'think', 'thinking', 'consider', 'ponder', 'meditate'], "awesome",
'winking': ['调皮', '眨眼', '你懂的', '坏笑', '邪恶', '奸笑', '使眼色', "fantastic",
'wink', 'teasing', 'naughty', 'mischievous'], "wonderful",
'cool': ['', '', '厉害', '棒极了', '真棒', '牛逼', '', '优秀', '杰出', '出色', '完美', ],
'cool', 'awesome', 'amazing', 'great', 'impressive', 'perfect'], "laughing": [
'relaxed': ['放松', '舒服', '惬意', '悠闲', '轻松', '舒适', '安逸', '自在', "哈哈",
'relax', 'relaxed', 'comfortable', 'cozy', 'chill', 'peaceful'], "哈哈哈",
'delicious': ['好吃', '美味', '', '', '可口', '香甜', '大餐', '大快朵颐', '流口水', '垂涎', "呵呵",
'delicious', 'yummy', 'tasty', 'yum', 'appetizing', 'mouthwatering'], "嘿嘿",
'kissy': ['亲亲', '么么', '', 'mua', 'muah', '亲一下', '飞吻', "嘻嘻",
'kiss', 'xoxo', 'hug', 'muah', 'smooch'], "笑死",
'confident': ['自信', '肯定', '确定', '毫无疑问', '当然', '必须的', '毫无疑问', '确信', '坚信', "太好笑了",
'confident', 'sure', 'certain', 'definitely', 'positive'], "笑死我了",
'sleepy': ['', '睡觉', '晚安', '想睡', '好累', '疲惫', '疲倦', '困了', '想休息', '睡意', "lol",
'sleep', 'sleepy', 'tired', 'exhausted', 'bedtime', 'good night'], "lmao",
'silly': ['', '', '', '', '', '', '憨憨', '傻乎乎', '呆萌', "haha",
'silly', 'stupid', 'dumb', 'foolish', 'goofy', 'ridiculous'], "hahaha",
'confused': ['疑惑', '不明白', '不懂', '困惑', '疑问', '为什么', '怎么回事', '啥意思', '不清楚', "hehe",
'confused', 'puzzled', 'doubt', 'question', 'what', 'why', 'how'] "rofl",
"funny",
"laugh",
],
"funny": [
"搞笑",
"滑稽",
"",
"幽默",
"笑点",
"段子",
"笑话",
"太逗了",
"hilarious",
"joke",
"comedy",
],
"sad": [
"伤心",
"难过",
"悲哀",
"悲伤",
"忧郁",
"郁闷",
"沮丧",
"失望",
"想哭",
"难受",
"不开心",
"",
"呜呜",
"sad",
"upset",
"unhappy",
"depressed",
"sorrow",
"gloomy",
],
"angry": [
"生气",
"愤怒",
"气死",
"讨厌",
"烦人",
"可恶",
"烦死了",
"恼火",
"暴躁",
"火大",
"愤怒",
"气炸了",
"angry",
"mad",
"annoyed",
"furious",
"pissed",
"hate",
],
"crying": [
"哭泣",
"泪流",
"大哭",
"伤心欲绝",
"泪目",
"流泪",
"哭死",
"哭晕",
"想哭",
"泪崩",
"cry",
"crying",
"tears",
"sob",
"weep",
],
"loving": [
"爱你",
"喜欢",
"",
"亲爱的",
"宝贝",
"么么哒",
"抱抱",
"想你",
"思念",
"最爱",
"亲亲",
"喜欢你",
"love",
"like",
"adore",
"darling",
"sweetie",
"honey",
"miss you",
"heart",
],
"embarrassed": [
"尴尬",
"不好意思",
"害羞",
"脸红",
"难为情",
"社死",
"丢脸",
"出丑",
"embarrassed",
"awkward",
"shy",
"blush",
],
"surprised": [
"惊讶",
"吃惊",
"天啊",
"哇塞",
"",
"居然",
"竟然",
"没想到",
"出乎意料",
"surprise",
"wow",
"omg",
"oh my god",
"amazing",
"unbelievable",
],
"shocked": [
"震惊",
"吓到",
"惊呆了",
"不敢相信",
"震撼",
"吓死",
"恐怖",
"害怕",
"吓人",
"shocked",
"shocking",
"scared",
"frightened",
"terrified",
"horror",
],
"thinking": [
"思考",
"考虑",
"想一下",
"琢磨",
"沉思",
"冥想",
"",
"思考中",
"在想",
"think",
"thinking",
"consider",
"ponder",
"meditate",
],
"winking": [
"调皮",
"眨眼",
"你懂的",
"坏笑",
"邪恶",
"奸笑",
"使眼色",
"wink",
"teasing",
"naughty",
"mischievous",
],
"cool": [
"",
"",
"厉害",
"棒极了",
"真棒",
"牛逼",
"",
"优秀",
"杰出",
"出色",
"完美",
"cool",
"awesome",
"amazing",
"great",
"impressive",
"perfect",
],
"relaxed": [
"放松",
"舒服",
"惬意",
"悠闲",
"轻松",
"舒适",
"安逸",
"自在",
"relax",
"relaxed",
"comfortable",
"cozy",
"chill",
"peaceful",
],
"delicious": [
"好吃",
"美味",
"",
"",
"可口",
"香甜",
"大餐",
"大快朵颐",
"流口水",
"垂涎",
"delicious",
"yummy",
"tasty",
"yum",
"appetizing",
"mouthwatering",
],
"kissy": [
"亲亲",
"么么",
"",
"mua",
"muah",
"亲一下",
"飞吻",
"kiss",
"xoxo",
"hug",
"muah",
"smooch",
],
"confident": [
"自信",
"肯定",
"确定",
"毫无疑问",
"当然",
"必须的",
"毫无疑问",
"确信",
"坚信",
"confident",
"sure",
"certain",
"definitely",
"positive",
],
"sleepy": [
"",
"睡觉",
"晚安",
"想睡",
"好累",
"疲惫",
"疲倦",
"困了",
"想休息",
"睡意",
"sleep",
"sleepy",
"tired",
"exhausted",
"bedtime",
"good night",
],
"silly": [
"",
"",
"",
"",
"",
"",
"憨憨",
"傻乎乎",
"呆萌",
"silly",
"stupid",
"dumb",
"foolish",
"goofy",
"ridiculous",
],
"confused": [
"疑惑",
"不明白",
"不懂",
"困惑",
"疑问",
"为什么",
"怎么回事",
"啥意思",
"不清楚",
"confused",
"puzzled",
"doubt",
"question",
"what",
"why",
"how",
],
} }
# 特殊句型判断(中英文) # 特殊句型判断(中英文)
# 赞美他人 # 赞美他人
if any(phrase in text for phrase in if any(
['你真', '你好', '您真', '你真棒', '你好厉害', '你太强了', '你真好', '你真聪明', phrase in text
'you are', 'you\'re', 'you look', 'you seem', 'so smart', 'so kind']): for phrase in [
return 'loving' "你真",
"你好",
"您真",
"你真棒",
"你好厉害",
"你太强了",
"你真好",
"你真聪明",
"you are",
"you're",
"you look",
"you seem",
"so smart",
"so kind",
]
):
return "loving"
# 自我赞美 # 自我赞美
if any(phrase in text for phrase in ['我真', '我最', '我太棒了', '我厉害', '我聪明', '我优秀', if any(
'i am', 'i\'m', 'i feel', 'so good', 'so happy']): phrase in text
return 'cool' for phrase in [
"我真",
"我最",
"我太棒了",
"我厉害",
"我聪明",
"我优秀",
"i am",
"i'm",
"i feel",
"so good",
"so happy",
]
):
return "cool"
# 晚安/睡觉相关 # 晚安/睡觉相关
if any(phrase in text for phrase in ['睡觉', '晚安', '睡了', '好梦', '休息了', '去睡了', if any(
'sleep', 'good night', 'bedtime', 'go to bed']): phrase in text
return 'sleepy' for phrase in [
"睡觉",
"晚安",
"睡了",
"好梦",
"休息了",
"去睡了",
"sleep",
"good night",
"bedtime",
"go to bed",
]
):
return "sleepy"
# 疑问句 # 疑问句
if has_question and not has_exclamation: if has_question and not has_exclamation:
return 'thinking' return "thinking"
# 强烈情感(感叹号) # 强烈情感(感叹号)
if has_exclamation and not has_question: if has_exclamation and not has_question:
# 检查是否是积极内容 # 检查是否是积极内容
positive_words = emotion_keywords['happy'] + emotion_keywords['laughing'] + emotion_keywords['cool'] positive_words = (
emotion_keywords["happy"]
+ emotion_keywords["laughing"]
+ emotion_keywords["cool"]
)
if any(word in text for word in positive_words): if any(word in text for word in positive_words):
return 'laughing' return "laughing"
# 检查是否是消极内容 # 检查是否是消极内容
negative_words = emotion_keywords['angry'] + emotion_keywords['sad'] + emotion_keywords['crying'] negative_words = (
emotion_keywords["angry"]
+ emotion_keywords["sad"]
+ emotion_keywords["crying"]
)
if any(word in text for word in negative_words): if any(word in text for word in negative_words):
return 'angry' return "angry"
return 'surprised' return "surprised"
# 省略号(表示犹豫或思考) # 省略号(表示犹豫或思考)
if has_ellipsis: if has_ellipsis:
return 'thinking' return "thinking"
# 关键词匹配(带权重) # 关键词匹配(带权重)
emotion_scores = {emotion: 0 for emotion in emoji_map.keys()} emotion_scores = {emotion: 0 for emotion in emoji_map.keys()}
@@ -466,18 +832,33 @@ def analyze_emotion(text):
# 根据分数选择最可能的情感 # 根据分数选择最可能的情感
max_score = max(emotion_scores.values()) max_score = max(emotion_scores.values())
if max_score == 0: if max_score == 0:
return 'happy' # 默认 return "happy" # 默认
# 可能有多个情感同分,根据上下文选择最合适的 # 可能有多个情感同分,根据上下文选择最合适的
top_emotions = [e for e, s in emotion_scores.items() if s == max_score] top_emotions = [e for e, s in emotion_scores.items() if s == max_score]
# 如果多个情感同分,使用以下优先级 # 如果多个情感同分,使用以下优先级
priority_order = [ priority_order = [
'laughing', 'crying', 'angry', 'surprised', 'shocked', # 强烈情感优先 "laughing",
'loving', 'happy', 'funny', 'cool', # 积极情感 "crying",
'sad', 'embarrassed', 'confused', # 消极情感 "angry",
'thinking', 'winking', 'relaxed', # 中性情感 "surprised",
'delicious', 'kissy', 'confident', 'sleepy', 'silly' # 特殊场景 "shocked", # 强烈情感优先
"loving",
"happy",
"funny",
"cool", # 积极情感
"sad",
"embarrassed",
"confused", # 消极情感
"thinking",
"winking",
"relaxed", # 中性情感
"delicious",
"kissy",
"confident",
"sleepy",
"silly", # 特殊场景
] ]
for emotion in priority_order: for emotion in priority_order:
@@ -485,3 +866,50 @@ def analyze_emotion(text):
return emotion return emotion
return top_emotions[0] # 如果都不在优先级列表里,返回第一个 return top_emotions[0] # 如果都不在优先级列表里,返回第一个
def audio_to_opus_data(audio_file_path):
"""音频文件转换为Opus编码"""
# 获取文件后缀名
file_type = os.path.splitext(audio_file_path)[1]
if file_type:
file_type = file_type.lstrip(".")
# 读取音频文件,-nostdin 参数:不要从标准输入读取数据,否则FFmpeg会阻塞
audio = AudioSegment.from_file(
audio_file_path, format=file_type, parameters=["-nostdin"]
)
# 转换为单声道/16kHz采样率/16位小端编码(确保与编码器匹配)
audio = audio.set_channels(1).set_frame_rate(16000).set_sample_width(2)
# 音频时长(秒)
duration = len(audio) / 1000.0
# 获取原始PCM数据(16位小端)
raw_data = audio.raw_data
# 初始化Opus编码器
encoder = opuslib_next.Encoder(16000, 1, opuslib_next.APPLICATION_AUDIO)
# 编码参数
frame_duration = 60 # 60ms per frame
frame_size = int(16000 * frame_duration / 1000) # 960 samples/frame
opus_datas = []
# 按帧处理所有音频数据(包括最后一帧可能补零)
for i in range(0, len(raw_data), frame_size * 2): # 16bit=2bytes/sample
# 获取当前帧的二进制数据
chunk = raw_data[i : i + frame_size * 2]
# 如果最后一帧不足,补零
if len(chunk) < frame_size * 2:
chunk += b"\x00" * (frame_size * 2 - len(chunk))
# 转换为numpy数组处理
np_frame = np.frombuffer(chunk, dtype=np.int16)
# 编码Opus数据
opus_data = encoder.encode(np_frame.tobytes(), frame_size)
opus_datas.append(opus_data)
return opus_datas, duration
+91 -8
View File
@@ -3,6 +3,7 @@ import websockets
from config.logger import setup_logging from config.logger import setup_logging
from core.connection import ConnectionHandler from core.connection import ConnectionHandler
from core.utils.util import initialize_modules from core.utils.util import initialize_modules
from config.config_loader import get_config_from_api
TAG = __name__ TAG = __name__
@@ -13,14 +14,21 @@ class WebSocketServer:
self.logger = setup_logging() self.logger = setup_logging()
self.config_lock = asyncio.Lock() self.config_lock = asyncio.Lock()
modules = initialize_modules( modules = initialize_modules(
self.logger, self.config, True, True, True, True, True, True self.logger,
self.config,
"VAD" in self.config["selected_module"],
"ASR" in self.config["selected_module"],
"LLM" in self.config["selected_module"],
"TTS" in self.config["selected_module"],
"Memory" in self.config["selected_module"],
"Intent" in self.config["selected_module"],
) )
self._vad = modules["vad"] self._vad = modules["vad"] if "vad" in modules else None
self._asr = modules["asr"] self._asr = modules["asr"] if "asr" in modules else None
self._tts = modules["tts"] self._tts = modules["tts"] if "tts" in modules else None
self._llm = modules["llm"] self._llm = modules["llm"] if "llm" in modules else None
self._intent = modules["intent"] self._intent = modules["intent"] if "intent" in modules else None
self._memory = modules["memory"] self._memory = modules["memory"] if "memory" in modules else None
self.active_connections = set() self.active_connections = set()
async def start(self): async def start(self):
@@ -44,7 +52,7 @@ class WebSocketServer:
self._tts, self._tts,
self._memory, self._memory,
self._intent, self._intent,
self # 传入当前 WebSocketServer 实例 self, # 传入server实例
) )
self.active_connections.add(handler) self.active_connections.add(handler)
try: try:
@@ -60,3 +68,78 @@ class WebSocketServer:
else: else:
# 如果是普通 HTTP 请求,返回 "server is running" # 如果是普通 HTTP 请求,返回 "server is running"
return websocket.respond(200, "Server is running\n") return websocket.respond(200, "Server is running\n")
async def update_config(self) -> bool:
"""更新服务器配置并重新初始化组件
Returns:
bool: 更新是否成功
"""
try:
async with self.config_lock:
# 重新获取配置
new_config = get_config_from_api(self.config)
if new_config is None:
self.logger.bind(tag=TAG).error("获取新配置失败")
return False
# 检查 VAD 和 ASR 类型是否需要更新
update_vad = False
update_asr = False
# 获取当前和新的 VAD 类型
current_vad_module = self.config["selected_module"]["VAD"]
new_vad_module = new_config["selected_module"]["VAD"]
current_vad_type = (
current_vad_module
if "type" not in self.config["VAD"][current_vad_module]
else self.config["VAD"][current_vad_module]["type"]
)
new_vad_type = (
new_vad_module
if "type" not in new_config["VAD"][new_vad_module]
else new_config["VAD"][new_vad_module]["type"]
)
update_vad = current_vad_type != new_vad_type
# 获取当前和新的 ASR 类型
current_asr_module = self.config["selected_module"]["ASR"]
new_asr_module = new_config["selected_module"]["ASR"]
current_asr_type = (
current_asr_module
if "type" not in self.config["ASR"][current_asr_module]
else self.config["ASR"][current_asr_module]["type"]
)
new_asr_type = (
new_asr_module
if "type" not in new_config["ASR"][new_asr_module]
else new_config["ASR"][new_asr_module]["type"]
)
update_asr = current_asr_type != new_asr_type
# 更新配置
self.config = new_config
# 重新初始化组件
modules = initialize_modules(
self.logger,
new_config,
update_vad,
update_asr,
"LLM" in new_config["selected_module"],
"TTS" in new_config["selected_module"],
"Memory" in new_config["selected_module"],
"Intent" in new_config["selected_module"],
)
# 更新组件实例
self._vad = modules["vad"] if "vad" in modules else None
self._asr = modules["asr"] if "asr" in modules else None
self._tts = modules["tts"] if "tts" in modules else None
self._llm = modules["llm"] if "llm" in modules else None
self._intent = modules["intent"] if "intent" in modules else None
self._memory = modules["memory"] if "memory" in modules else None
return True
except Exception as e:
self.logger.bind(tag=TAG).error(f"更新服务器配置失败: {str(e)}")
return False