fix: 播放咋音问题

This commit is contained in:
lizhongxiang
2025-04-11 10:25:43 +08:00
130 changed files with 8311 additions and 2085 deletions
+48 -42
View File
@@ -79,7 +79,7 @@ class ConnectionHandler:
self.intent = _intent
# vad相关变量
self.client_audio_buffer = bytes()
self.client_audio_buffer = bytearray()
self.client_have_voice = False
self.client_have_voice_last_time = 0.0
self.client_no_voice_last_time = 0.0
@@ -139,9 +139,6 @@ class ConnectionHandler:
await self.websocket.send(json.dumps(self.welcome_msg))
# Load private configuration if device_id is provided
bUsePrivateConfig = self.config.get("use_private_config", False)
self.logger.bind(tag=TAG).info(
f"bUsePrivateConfig: {bUsePrivateConfig}, device_id: {device_id}"
)
if bUsePrivateConfig and device_id:
try:
self.private_config = PrivateConfig(
@@ -225,50 +222,58 @@ class ConnectionHandler:
self.prompt = self.private_config.private_config.get("prompt", self.prompt)
self.dialogue.put(Message(role="system", content=self.prompt))
"""加载插件"""
self.func_handler = FunctionHandler(self)
self.mcp_manager = MCPManager(self)
"""加载记忆"""
device_id = self.headers.get("device-id", None)
self.memory.init_memory(device_id, self.llm)
"""为意图识别设置LLM,优先使用专用LLM"""
# 检查是否配置了专用的意图识别LLM
intent_llm_name = self.config["Intent"]["intent_llm"]["llm"]
# 记录开始初始化意图识别LLM的时间
intent_llm_init_start = time.time()
if (
not self.use_function_call_mode
and intent_llm_name
and intent_llm_name in self.config["LLM"]
):
# 如果配置了专用LLM,则创建独立的LLM实例
from core.utils import llm as llm_utils
intent_llm_config = self.config["LLM"][intent_llm_name]
intent_llm_type = intent_llm_config.get("type", intent_llm_name)
intent_llm = llm_utils.create_instance(intent_llm_type, intent_llm_config)
self.logger.bind(tag=TAG).info(
f"为意图识别创建了专用LLM: {intent_llm_name}, 类型: {intent_llm_type}"
)
self.intent.set_llm(intent_llm)
else:
# 否则使用主LLM
self.intent.set_llm(self.llm)
# 记录意图识别LLM初始化耗时
intent_llm_init_time = time.time() - intent_llm_init_start
self._initialize_memory()
"""加载意图识别"""
self._initialize_intent()
"""加载位置信息"""
self.client_ip_info = get_ip_info(self.client_ip)
if self.client_ip_info is not None and "city" in self.client_ip_info:
self.logger.bind(tag=TAG).info(f"Client ip info: {self.client_ip_info}")
self.prompt = self.prompt + f"\nuser location:{self.client_ip_info}"
self.dialogue.update_system_message(self.prompt)
def _initialize_memory(self):
"""初始化记忆模块"""
device_id = self.headers.get("device-id", None)
self.memory.init_memory(device_id, self.llm)
def _initialize_intent(self):
"""初始化意图识别模块"""
# 获取意图识别配置
intent_config = self.config["Intent"]
intent_type = self.config["selected_module"]["Intent"]
# 如果使用 nointent,直接返回
if intent_type == "nointent":
return
# 使用 intent_llm 模式
elif intent_type == "intent_llm":
intent_llm_name = intent_config["intent_llm"]["llm"]
if intent_llm_name and intent_llm_name in self.config["LLM"]:
# 如果配置了专用LLM,则创建独立的LLM实例
from core.utils import llm as llm_utils
intent_llm_config = self.config["LLM"][intent_llm_name]
intent_llm_type = intent_llm_config.get("type", intent_llm_name)
intent_llm = llm_utils.create_instance(
intent_llm_type, intent_llm_config
)
self.logger.bind(tag=TAG).info(
f"为意图识别创建了专用LLM: {intent_llm_name}, 类型: {intent_llm_type}"
)
self.intent.set_llm(intent_llm)
else:
# 否则使用主LLM
self.intent.set_llm(self.llm)
self.logger.bind(tag=TAG).info("使用主LLM作为意图识别模型")
"""加载插件"""
self.func_handler = FunctionHandler(self)
self.mcp_manager = MCPManager(self)
"""加载MCP工具"""
asyncio.run_coroutine_threadsafe(
self.mcp_manager.initialize_servers(), self.loop
@@ -652,7 +657,8 @@ class ConnectionHandler:
async def close(self, ws=None):
"""资源清理方法"""
# 清理MCP资源
await self.mcp_manager.cleanup_all()
if hasattr(self, "mcp_manager") and self.mcp_manager:
await self.mcp_manager.cleanup_all()
# 触发停止事件并清理资源
if self.stop_event:
@@ -688,7 +694,7 @@ class ConnectionHandler:
# q.queue.put(None)
def reset_vad_states(self):
self.client_audio_buffer = bytes()
self.client_audio_buffer = bytearray()
self.client_have_voice = False
self.client_have_voice_last_time = 0
self.client_voice_stop = False
@@ -69,12 +69,13 @@ async def process_intent_result(conn, intent_result, original_text):
# 检查是否有function_call
if "function_call" in intent_data:
# 直接从意图识别获取了function_call
logger.bind(tag=TAG).info(
logger.bind(tag=TAG).debug(
f"检测到function_call格式的意图结果: {intent_data['function_call']['name']}"
)
function_name = intent_data["function_call"]["name"]
if function_name == "continue_chat":
return False
function_args = None
if "arguments" in intent_data["function_call"]:
function_args = intent_data["function_call"]["arguments"]
@@ -50,6 +50,12 @@ def create_iot_function(device_name, method_name, method_info):
conn, response_success=None, response_failure=None, **params
):
try:
# 设置默认响应消息
if not response_success:
response_success = "操作成功"
if not response_failure:
response_failure = "操作失败"
# 打印响应参数
logger.bind(tag=TAG).info(
f"控制函数接收到的响应参数: success='{response_success}', failure='{response_failure}'"
@@ -263,6 +269,8 @@ def register_device_type(descriptor):
# 用于接受前端设备推送的搜索iot描述
async def handleIotDescriptors(conn, descriptors):
if not conn.use_function_call_mode:
return
wait_max_time = 5
while conn.func_handler is None or not conn.func_handler.finish_init:
await asyncio.sleep(1)
@@ -2,14 +2,16 @@
Opus编码工具类
将PCM音频数据编码为Opus格式
"""
import array
import logging
import traceback
import numpy as np
from typing import List, Optional
from opuslib import Encoder
from opuslib import constants
from opuslib_next import Encoder
from opuslib_next import constants
class OpusEncoderUtils:
"""PCM到Opus的编码器"""
@@ -17,7 +19,7 @@ class OpusEncoderUtils:
def __init__(self, sample_rate: int, channels: int, frame_size_ms: int):
"""
初始化Opus编码器
Args:
sample_rate: 采样率 (Hz)
channels: 通道数 (1=单声道, 2=立体声)
@@ -30,20 +32,18 @@ class OpusEncoderUtils:
self.frame_size = (sample_rate * frame_size_ms) // 1000
# 总帧大小 = 每帧样本数 * 通道数
self.total_frame_size = self.frame_size * channels
# 比特率和复杂度设置
self.bitrate = 24000 # bps
self.complexity = 10 # 最高质量
# 缓冲区初始化为空
self.buffer = np.array([], dtype=np.int16)
try:
# 创建Opus编码器
self.encoder = Encoder(
sample_rate,
channels,
constants.APPLICATION_AUDIO # 音频优化模式
sample_rate, channels, constants.APPLICATION_AUDIO # 音频优化模式
)
self.encoder.bitrate = self.bitrate
self.encoder.complexity = self.complexity
@@ -60,48 +60,48 @@ class OpusEncoderUtils:
def encode_pcm_to_opus(self, pcm_data: bytes, end_of_stream: bool) -> List[bytes]:
"""
将PCM数据编码为Opus格式
Args:
pcm_data: PCM字节数据
end_of_stream: 是否为流的结束
Returns:
Opus数据包列表
"""
# 将字节数据转换为short数组
new_samples = self._convert_bytes_to_shorts(pcm_data)
# 校验PCM数据
self._validate_pcm_data(new_samples)
# 将新数据追加到缓冲区
self.buffer = np.append(self.buffer, new_samples)
opus_packets = []
offset = 0
# 处理所有完整帧
while offset <= len(self.buffer) - self.total_frame_size:
frame = self.buffer[offset:offset + self.total_frame_size]
frame = self.buffer[offset : offset + self.total_frame_size]
output = self._encode(frame)
if output:
opus_packets.append(output)
offset += self.total_frame_size
# 保留未处理的样本
self.buffer = self.buffer[offset:]
# 流结束时处理剩余数据
if end_of_stream and len(self.buffer) > 0:
# 创建最后一帧并用0填充
last_frame = np.zeros(self.total_frame_size, dtype=np.int16)
last_frame[:len(self.buffer)] = self.buffer
last_frame[: len(self.buffer)] = self.buffer
output = self._encode(last_frame)
if output:
opus_packets.append(output)
self.buffer = np.array([], dtype=np.int16)
return opus_packets
def _encode(self, frame: np.ndarray) -> Optional[bytes]:
@@ -134,4 +134,4 @@ class OpusEncoderUtils:
def close(self):
"""关闭编码器并释放资源"""
# opuslib没有明确的关闭方法,Python的垃圾回收会处理
pass
pass
@@ -9,20 +9,23 @@ logger = setup_logging()
class IntentProviderBase(ABC):
def __init__(self, config):
self.config = config
self.intent_options = config.get("intent_options", {
"handle_exit_intent": "结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候",
"play_music": "播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图",
"get_weather": "查询天气, 用户希望查询某个地点的天气情况",
"get_news": "查询新闻, 用户希望查询最新新闻或特定类型的新闻",
"get_lunar": "用于获取今天的阴历/农历和黄历信息",
"get_time": "获取今天日期或者当前时间信息",
"continue_chat": "继续聊天",
})
self.intent_options = [
{
"name": "handle_exit_intent",
"desc": "结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候",
},
{
"name": "play_music",
"desc": "播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图",
},
{"name": "get_time", "desc": "获取今天日期或者当前时间信息"},
{"name": "continue_chat", "desc": "继续聊天"},
]
def set_llm(self, llm):
self.llm = llm
# 获取模型名称和类型信息
model_name = getattr(llm, 'model_name', str(llm.__class__.__name__))
model_name = getattr(llm, "model_name", str(llm.__class__.__name__))
# 记录更详细的日志
logger.bind(tag=TAG).info(f"意图识别设置LLM: {model_name}")
@@ -35,7 +38,7 @@ class IntentProviderBase(ABC):
Returns:
返回识别出的意图,格式为:
- "继续聊天"
- "结束聊天"
- "结束聊天"
- "播放音乐 歌名""随机播放音乐"
- "查询天气 地点名""查询天气 [当前位置]"
"""
@@ -16,5 +16,7 @@ class IntentProvider(IntentProviderBase):
Returns:
固定返回"继续聊天"
"""
logger.bind(tag=TAG).debug("Using functionCallProvider, always returning continue chat")
return self.intent_options["continue_chat"]
logger.bind(tag=TAG).debug(
"Using functionCallProvider, always returning continue chat"
)
return '{"function_call": {"name": "continue_chat"}}'
@@ -27,119 +27,94 @@ class IntentProvider(IntentProviderBase):
Returns:
格式化后的系统提示词
"""
intent_list = []
prompt = (
"你是一个意图识别助手。请分析用户的最后一句话,判断用户意图属于以下哪一类:\n"
"<start>"
f"{', '.join(intent_list)}"
f"{str(self.intent_options)}"
"<end>\n"
"处理步骤:"
"1. 思考意图类型,生成function_call格式"
"\n\n"
"返回格式示例:\n"
"1. 播放音乐意图: {\"function_call\": {\"name\": \"play_music\", \"arguments\": {\"song_name\": \"音乐名称\"}}}\n"
"2. 查询天气意图: {\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": \"地点名称\", \"lang\": \"zh_CN\"}}}\n"
"3. 查询新闻意图: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"新闻类别\", \"detail\": false, \"lang\": \"zh_CN\"}}}\n"
"4. 结束对话意图: {\"function_call\": {\"name\": \"handle_exit_intent\", \"arguments\": {\"say_goodbye\": \"goodbye\"}}}\n"
"5. 获取当天日期时间: {\"function_call\": {\"name\": \"get_time\"}}\n"
"6. 获取当前黄历意图: {\"function_call\": {\"name\": \"get_lunar\"}}\n"
"7. 继续聊天意图: {\"function_call\": {\"name\": \"continue_chat\"}}\n"
'1. 播放音乐意图: {"function_call": {"name": "play_music", "arguments": {"song_name": "音乐名称"}}}\n'
'2. 结束对话意图: {"function_call": {"name": "handle_exit_intent", "arguments": {"say_goodbye": "goodbye"}}}\n'
'3. 获取当天日期时间: {"function_call": {"name": "get_time"}}\n'
'4. 继续聊天意图: {"function_call": {"name": "continue_chat"}}\n'
"\n"
"注意:\n"
"- 播放音乐:无歌名时,song_name设为\"random\"\n"
"- 查询天气:无地点时,location设为null\n"
"- 查询新闻:无类别时,category设为null;查询详情时,detail设为true\n"
'- 播放音乐:无歌名时,song_name设为"random"\n'
"- 如果没有明显的意图,应按照继续聊天意图处理\n"
"- 只返回纯JSON,不要任何其他内容\n"
"\n"
"示例分析:\n"
"```\n"
"用户: 你好小智\n"
"返回: {\"function_call\": {\"name\": \"continue_chat\"}}\n"
"```\n"
"```\n"
"用户: 你今天怎么样?\n"
"返回: {\"function_call\": {\"name\": \"continue_chat\"}}\n"
"用户: 你也太搞笑了\n"
'返回: {"function_call": {"name": "continue_chat"}}\n'
"```\n"
"```\n"
"用户: 现在是几号了?现在几点了?\n"
"返回: {\"function_call\": {\"name\": \"get_time\"}}\n"
"```\n"
"```\n"
"用户: 今天农历是多少?\n"
"返回: {\"function_call\": {\"name\": \"get_lunar\"}}\n"
'返回: {"function_call": {"name": "get_time"}}\n'
"```\n"
"```\n"
"用户: 我们明天再聊吧\n"
"返回: {\"function_call\": {\"name\": \"handle_exit_intent\"}}\n"
'返回: {"function_call": {"name": "handle_exit_intent"}}\n'
"```\n"
"```\n"
"用户: 播放中秋月\n"
"返回: {\"function_call\": {\"name\": \"play_music\", \"arguments\": {\"song_name\": \"中秋月\"}}}\n"
'返回: {"function_call": {"name": "play_music", "arguments": {"song_name": "中秋月"}}}\n'
"```\n"
"```\n"
"用户: 北京天气怎么样\n"
"返回: {\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": \"北京\", \"lang\": \"zh_CN\"}}}\n"
"```\n"
"```\n"
"用户: 今天天气怎么样\n"
"返回: {\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": null, \"lang\": \"zh_CN\"}}}\n"
"```\n"
"```\n"
"用户: 播报财经新闻\n"
"返回: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"财经\", \"detail\": false, \"lang\": \"zh_CN\"}}}\n"
"```\n"
"```\n"
"用户: 有什么最新新闻\n"
"返回: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": null, \"detail\": false, \"lang\": \"zh_CN\"}}}\n"
"```\n"
"```\n"
"用户: 详细介绍一下这条新闻\n"
"返回: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"detail\": true, \"lang\": \"zh_CN\"}}}\n"
"```\n"
"可用的音乐名称:\n"
)
return prompt
def clean_cache(self):
"""清理过期缓存"""
now = time.time()
# 找出过期键
expired_keys = [k for k, v in self.intent_cache.items() if now - v['timestamp'] > self.cache_expiry]
expired_keys = [
k
for k, v in self.intent_cache.items()
if now - v["timestamp"] > self.cache_expiry
]
for key in expired_keys:
del self.intent_cache[key]
# 如果缓存太大,移除最旧的条目
if len(self.intent_cache) > self.cache_max_size:
# 按时间戳排序并保留最新的条目
sorted_items = sorted(self.intent_cache.items(), key=lambda x: x[1]['timestamp'])
for key, _ in sorted_items[:len(sorted_items) - self.cache_max_size]:
sorted_items = sorted(
self.intent_cache.items(), key=lambda x: x[1]["timestamp"]
)
for key, _ in sorted_items[: len(sorted_items) - self.cache_max_size]:
del self.intent_cache[key]
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
if not self.llm:
raise ValueError("LLM provider not set")
# 记录整体开始时间
total_start_time = time.time()
# 打印使用的模型信息
model_info = getattr(self.llm, 'model_name', str(self.llm.__class__.__name__))
logger.bind(tag=TAG).info(f"使用意图识别模型: {model_info}")
model_info = getattr(self.llm, "model_name", str(self.llm.__class__.__name__))
logger.bind(tag=TAG).debug(f"使用意图识别模型: {model_info}")
# 计算缓存键
cache_key = hashlib.md5(text.encode()).hexdigest()
# 检查缓存
if cache_key in self.intent_cache:
cache_entry = self.intent_cache[cache_key]
# 检查缓存是否过期
if time.time() - cache_entry['timestamp'] <= self.cache_expiry:
if time.time() - cache_entry["timestamp"] <= self.cache_expiry:
cache_time = time.time() - total_start_time
logger.bind(tag=TAG).info(f"使用缓存的意图: {cache_key} -> {cache_entry['intent']}, 耗时: {cache_time:.4f}")
return cache_entry['intent']
logger.bind(tag=TAG).debug(
f"使用缓存的意图: {cache_key} -> {cache_entry['intent']}, 耗时: {cache_time:.4f}"
)
return cache_entry["intent"]
# 清理缓存
self.clean_cache()
@@ -158,38 +133,41 @@ class IntentProvider(IntentProviderBase):
music_file_names = music_config["music_file_names"]
prompt_music = f"{self.promot}\n<start>{music_file_names}\n<end>"
logger.bind(tag=TAG).debug(f"User prompt: {prompt_music}")
# 记录预处理完成时间
preprocess_time = time.time() - total_start_time
logger.bind(tag=TAG).debug(f"意图识别预处理耗时: {preprocess_time:.4f}")
# 使用LLM进行意图识别
llm_start_time = time.time()
logger.bind(tag=TAG).info(f"开始LLM意图识别调用, 模型: {model_info}")
logger.bind(tag=TAG).debug(f"开始LLM意图识别调用, 模型: {model_info}")
intent = self.llm.response_no_stream(
system_prompt=prompt_music,
user_prompt=user_prompt
system_prompt=prompt_music, user_prompt=user_prompt
)
# 记录LLM调用完成时间
llm_time = time.time() - llm_start_time
logger.bind(tag=TAG).info(f"LLM意图识别完成, 模型: {model_info}, 调用耗时: {llm_time:.4f}")
logger.bind(tag=TAG).debug(
f"LLM意图识别完成, 模型: {model_info}, 调用耗时: {llm_time:.4f}"
)
# 记录后处理开始时间
postprocess_start_time = time.time()
# 清理和解析响应
intent = intent.strip()
# 尝试提取JSON部分
match = re.search(r'\{.*\}', intent, re.DOTALL)
match = re.search(r"\{.*\}", intent, re.DOTALL)
if match:
intent = match.group(0)
# 记录总处理时间
total_time = time.time() - total_start_time
logger.bind(tag=TAG).info(f"【意图识别性能】模型: {model_info}, 总耗时: {total_time:.4f}秒, LLM调用: {llm_time:.4f}秒, 查询: '{text[:20]}...'")
logger.bind(tag=TAG).debug(
f"【意图识别性能】模型: {model_info}, 总耗时: {total_time:.4f}秒, LLM调用: {llm_time:.4f}秒, 查询: '{text[:20]}...'"
)
# 尝试解析为JSON
try:
intent_data = json.loads(intent)
@@ -198,38 +176,42 @@ class IntentProvider(IntentProviderBase):
function_data = intent_data["function_call"]
function_name = function_data.get("name")
function_args = function_data.get("arguments", {})
# 记录识别到的function call
logger.bind(tag=TAG).info(f"识别到function call: {function_name}, 参数: {function_args}")
logger.bind(tag=TAG).info(
f"识别到function call: {function_name}, 参数: {function_args}"
)
# 添加到缓存
self.intent_cache[cache_key] = {
'intent': intent,
'timestamp': time.time()
"intent": intent,
"timestamp": time.time(),
}
# 后处理时间
postprocess_time = time.time() - postprocess_start_time
logger.bind(tag=TAG).debug(f"意图后处理耗时: {postprocess_time:.4f}")
# 确保返回完全序列化的JSON字符串
return intent
else:
# 添加到缓存
self.intent_cache[cache_key] = {
'intent': intent,
'timestamp': time.time()
"intent": intent,
"timestamp": time.time(),
}
# 后处理时间
postprocess_time = time.time() - postprocess_start_time
logger.bind(tag=TAG).debug(f"意图后处理耗时: {postprocess_time:.4f}")
# 返回普通意图
return intent
except json.JSONDecodeError:
# 后处理时间
postprocess_time = time.time() - postprocess_start_time
logger.bind(tag=TAG).error(f"无法解析意图JSON: {intent}, 后处理耗时: {postprocess_time:.4f}")
logger.bind(tag=TAG).error(
f"无法解析意图JSON: {intent}, 后处理耗时: {postprocess_time:.4f}"
)
# 如果解析失败,默认返回继续聊天意图
return "{\"intent\": \"继续聊天\"}"
return '{"intent": "继续聊天"}'
@@ -16,5 +16,7 @@ class IntentProvider(IntentProviderBase):
Returns:
固定返回"继续聊天"
"""
logger.bind(tag=TAG).debug("Using NoIntentProvider, always returning continue chat")
return self.intent_options["continue_chat"]
logger.bind(tag=TAG).debug(
"Using NoIntentProvider, always returning continue chat"
)
return '{"function_call": {"name": "continue_chat"}}'
+13 -2
View File
@@ -53,6 +53,7 @@ class TTSProviderBase(ABC):
"",
)
self.tts_request = False
self.tts_stop_request = False
self.processed_chars = 0
self.stream = False
self.last_to_opus_raw = b""
@@ -93,6 +94,9 @@ class TTSProviderBase(ABC):
)
self.processed_chars += len(segment_text_raw) # 更新已处理字符位置
return segment_text
elif self.tts_stop_request and current_text:
segment_text = current_text
return segment_text
else:
return None
@@ -115,8 +119,11 @@ class TTSProviderBase(ABC):
def tts_one_sentence(self, conn, text, u_id=None):
if not u_id:
u_id = str(uuid.uuid4()).replace("-", "")
conn.u_id = u_id
if conn.u_id:
u_id = conn.u_id
else:
u_id = str(uuid.uuid4()).replace("-", "")
conn.u_id = u_id
self.tts_text_queue.put(
TTSMessageDTO(u_id=u_id, msg_type=MsgType.START_TTS_REQUEST, content="")
)
@@ -135,6 +142,7 @@ class TTSProviderBase(ABC):
if msg_type == MsgType.START_TTS_REQUEST:
# 开始传输tts文本
self.tts_request = True
self.tts_stop_request = False
self.u_id = ttsMessageDTO.u_id
# 开启session
future = asyncio.run_coroutine_threadsafe(
@@ -152,6 +160,7 @@ class TTSProviderBase(ABC):
future.result()
elif msg_type == MsgType.STOP_TTS_REQUEST:
self.tts_request = False
self.tts_stop_request = True
future = asyncio.run_coroutine_threadsafe(
self.finish_session(ttsMessageDTO.u_id), loop=self.loop
)
@@ -183,6 +192,7 @@ class TTSProviderBase(ABC):
if msg_type == MsgType.START_TTS_REQUEST:
# 开始传输tts文本
self.tts_request = True
self.tts_stop_request = False
self.processed_chars = 0
self.tts_text_buff = []
elif self.tts_request and msg_type == MsgType.TTS_TEXT_REQUEST:
@@ -190,6 +200,7 @@ class TTSProviderBase(ABC):
elif msg_type == MsgType.STOP_TTS_REQUEST:
# 结束传输tts文本,处理最尾巴的数据
self.tts_request = False
self.tts_stop_request = True
segment_text = self._get_segment_text()
if segment_text:
# 修改部分:创建协程对象
+67 -38
View File
@@ -10,7 +10,10 @@ import requests
def get_project_dir():
"""获取项目根目录"""
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + '/'
return (
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+ "/"
)
def get_local_ip():
@@ -24,6 +27,7 @@ def get_local_ip():
except Exception as e:
return "127.0.0.1"
def is_private_ip(ip_addr):
"""
Check if an IP address is a private IP address (compatible with IPv4 and IPv6).
@@ -33,46 +37,48 @@ def is_private_ip(ip_addr):
"""
try:
# Validate IPv4 or IPv6 address format
if not re.match(r"^(\d{1,3}\.){3}\d{1,3}$|^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$", ip_addr):
if not re.match(
r"^(\d{1,3}\.){3}\d{1,3}$|^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$", ip_addr
):
return False # Invalid IP address format
# IPv4 private address ranges
if '.' in ip_addr: # IPv4 address
ip_parts = list(map(int, ip_addr.split('.')))
if "." in ip_addr: # IPv4 address
ip_parts = list(map(int, ip_addr.split(".")))
if ip_parts[0] == 10:
return True # 10.0.0.0/8 range
elif ip_parts[0] == 172 and 16 <= ip_parts[1] <= 31:
return True # 172.16.0.0/12 range
elif ip_parts[0] == 192 and ip_parts[1] == 168:
return True # 192.168.0.0/16 range
elif ip_addr == '127.0.0.1':
elif ip_addr == "127.0.0.1":
return True # Loopback address
elif ip_parts[0] == 169 and ip_parts[1] == 254:
return True # Link-local address 169.254.0.0/16
return True # Link-local address 169.254.0.0/16
else:
return False # Not a private IPv4 address
else: # IPv6 address
ip_addr = ip_addr.lower()
if ip_addr.startswith('fc00:') or ip_addr.startswith('fd00:'):
if ip_addr.startswith("fc00:") or ip_addr.startswith("fd00:"):
return True # Unique Local Addresses (FC00::/7)
elif ip_addr == '::1':
elif ip_addr == "::1":
return True # Loopback address
elif ip_addr.startswith('fe80:'):
return True # Link-local unicast addresses (FE80::/10)
elif ip_addr.startswith("fe80:"):
return True # Link-local unicast addresses (FE80::/10)
else:
return False # Not a private IPv6 address
except (ValueError, IndexError):
return False # IP address format error or insufficient segments
def get_ip_info(ip_addr):
try:
url = "https://whois.pconline.com.cn/ipJson.jsp?json=true"
if is_private_ip(ip_addr):
ip_addr = ""
url = f"https://whois.pconline.com.cn/ipJson.jsp?json=true&ip={ip_addr}"
resp = requests.get(url).json()
ip_info = {
"city": resp.get("city")
}
ip_info = {"city": resp.get("city")}
return ip_info
except Exception as e:
logging.error(f"Error getting client ip info: {e}")
@@ -87,7 +93,7 @@ def read_config(config_path):
def write_json_file(file_path, data):
"""将数据写入 JSON 文件"""
with open(file_path, 'w', encoding='utf-8') as file:
with open(file_path, "w", encoding="utf-8") as file:
json.dump(data, file, ensure_ascii=False, indent=4)
@@ -95,21 +101,28 @@ def is_punctuation_or_emoji(char):
"""检查字符是否为空格、指定标点或表情符号"""
# 定义需要去除的中英文标点(包括全角/半角)
punctuation_set = {
'', ',', # 中文逗号 + 英文逗号
'', '.', # 中文号 + 英文
'', '!', # 中文感叹号 + 英文感叹号
'-', '', # 英文连字符 + 中文全角横线
'' # 中文顿号
"",
",", # 中文号 + 英文
"",
".", # 中文句号 + 英文句号
"",
"!", # 中文感叹号 + 英文感叹号
"-",
"", # 英文连字符 + 中文全角横线
"", # 中文顿号
}
if char.isspace() or char in punctuation_set:
return True
# 检查表情符号(保留原有逻辑)
code_point = ord(char)
emoji_ranges = [
(0x1F600, 0x1F64F), (0x1F300, 0x1F5FF),
(0x1F680, 0x1F6FF), (0x1F900, 0x1F9FF),
(0x1FA70, 0x1FAFF), (0x2600, 0x26FF),
(0x2700, 0x27BF)
(0x1F600, 0x1F64F),
(0x1F300, 0x1F5FF),
(0x1F680, 0x1F6FF),
(0x1F900, 0x1F9FF),
(0x1FA70, 0x1FAFF),
(0x2600, 0x26FF),
(0x2700, 0x27BF),
]
return any(start <= code_point <= end for start, end in emoji_ranges)
@@ -125,27 +138,42 @@ def get_string_no_punctuation_or_emoji(s):
end = len(chars) - 1
while end >= start and is_punctuation_or_emoji(chars[end]):
end -= 1
return ''.join(chars[start:end + 1])
return "".join(chars[start : end + 1])
def remove_punctuation_and_length(text):
# 全角符号和半角符号的Unicode范围
full_width_punctuations = '!"#$%&'()*+,-。/:;<=>?@[\]^_`{|}~'
full_width_punctuations = (
"!"#$%&'()*+,-。/:;<=>?@[\]^_`{|}~"
)
half_width_punctuations = r'!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~'
space = ' ' # 半角空格
full_width_space = ' ' # 全角空格
space = " " # 半角空格
full_width_space = " " # 全角空格
# 去除全角和半角符号以及空格
result = ''.join([char for char in text if
char not in full_width_punctuations and char not in half_width_punctuations and char not in space and char not in full_width_space])
result = "".join(
[
char
for char in text
if char not in full_width_punctuations
and char not in half_width_punctuations
and char not in space
and char not in full_width_space
]
)
if result == "Yeah":
return 0, ""
return len(result), result
def check_model_key(modelType, modelKey):
if "" in modelKey:
logging.error("你还没配置" + modelType + "的密钥,请在配置文件中配置密钥,否则无法正常工作")
logging.error(
"你还没配置"
+ modelType
+ "的密钥,请在配置文件中配置密钥,否则无法正常工作"
)
return False
return True
@@ -155,15 +183,15 @@ def check_ffmpeg_installed():
try:
# 执行ffmpeg -version命令,并捕获输出
result = subprocess.run(
['ffmpeg', '-version'],
["ffmpeg", "-version"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True # 如果返回码非零则抛出异常
check=True, # 如果返回码非零则抛出异常
)
# 检查输出中是否包含版本信息(可选)
output = result.stdout + result.stderr
if 'ffmpeg version' in output.lower():
if "ffmpeg version" in output.lower():
ffmpeg_installed = True
return False
except (subprocess.CalledProcessError, FileNotFoundError):
@@ -175,11 +203,12 @@ def check_ffmpeg_installed():
error_msg += "1、按照项目的安装文档,正确进入conda环境\n"
error_msg += "2、查阅安装文档,如何在conda环境中安装ffmpeg\n"
raise ValueError(error_msg)
def extract_json_from_string(input_string):
"""提取字符串中的 JSON 部分"""
pattern = r'(\{.*\})'
pattern = r"(\{.*\})"
match = re.search(pattern, input_string)
if match:
return match.group(1) # 返回提取的 JSON 字符串
return None
return None
+1 -1
View File
@@ -31,7 +31,7 @@ class SileroVAD(VAD):
def is_vad(self, conn, opus_packet):
try:
pcm_frame = self.decoder.decode(opus_packet, 960)
conn.client_audio_buffer += pcm_frame # 将新数据加入缓冲区
conn.client_audio_buffer.extend(pcm_frame) # 将新数据加入缓冲区
# 处理缓冲区中的完整帧(每次处理512采样点)
client_have_voice = False