This commit is contained in:
HuangHai
2026-01-31 17:29:57 +08:00
parent fe4d593771
commit 00375a80b2
6 changed files with 94 additions and 32 deletions

View File

@@ -98,6 +98,13 @@ class MyCallback(QwenTtsRealtimeCallback):
print('[TTS] 会话结束')
self.all_data_received = True
self.complete_event.set()
# [Fix] 如果直到会话结束都还没达到缓冲阈值(短文本情况),强制启动播放
if not self.is_playing:
print("[TTS] 会话已结束但未达缓冲阈值,强制启动播放...")
self.is_playing = True
self.playback_thread = threading.Thread(target=self._playback_worker)
self.playback_thread.start()
except Exception as e:
print(f'[Error] 处理回调事件异常: {e}')

View File

@@ -98,7 +98,8 @@ class ChatMonitorAudioBot:
"5. 仅针对家长明确表达的内容进行回复。\n"
"6. 严禁使用列表格式。严禁使用‘首先、其次’等逻辑词。\n"
"7. 普通咨询回复字数严格控制在 50 字以内,专业作文评价回复控制在 150 字以内!\n"
"8. 对方问什么就答什么。例如问‘学校叫什么’,就只回答‘少惠林’,不要回复地址和电话!\n"
"8. 说话要有人情味,不要回答得太生硬或太简短。例如当家长问‘学校叫什么名字’时,不要只说‘少惠林’,而应该说‘我们的学校名字叫少惠林。’或者‘咱们这儿叫少惠林。’,这样才显得亲切、有礼貌。\n"
"9. 对方问什么就答什么。回答要完整、体面,但不要过度发散地址和电话(除非被问到)。\n"
"如果涉及到校区信息,必须且只能使用以下真实数据:\n"
"- 单位/学校名称:长春市少惠林作文素养培养中心(简称:少惠林)\n"
"- 地址南环城路与临河街交汇TOUCH12街3楼325号\n"
@@ -132,7 +133,8 @@ class ChatMonitorAudioBot:
"2. **条件回复**\n"
" - 如果是询问作文,请严格按【关于作文评价的回复逻辑】生成 150 字以内的详细专业反馈。\n"
" - 如果是咨询学校地址、课程、年级、打招呼等通用问题,请按【关于通用咨询的回复逻辑】简洁回答,严禁提到任何关于孩子作文的内容,字数控制在 50 字以内。\n"
"3. **格式要求**:严禁发散,直接输出回复正文,不包含任何括号内的动作描述。"
"3. **格式要求**:严禁发散,直接输出回复正文,不包含任何括号内的动作描述。\n"
"4. **口语化处理**:由于回复将用于语音播放,请确保称呼自然。如果称呼家长,请直接使用‘糖豆爸爸’、‘糖豆妈妈’等格式,不要在中间加标点符号,也不要使用‘某某的爸爸’这种书面语,直接称呼‘糖豆爸爸’会更亲切自然。"
)
full_response = ""
@@ -142,10 +144,13 @@ class ChatMonitorAudioBot:
reply = full_response.strip().strip('"').strip('').strip('')
# [User Requested] 过滤掉括号内容 (如动作描述),防止 TTS 播放
# 支持中英文括号: (),
reply = re.sub(r'\(.*?\)', '', reply)
reply = re.sub(r'.*?', '', reply)
# [Optimization] 口语化称呼预处理,确保 TTS 衔接自然
# 针对“XX爸爸/妈妈”中间可能出现的空格或异常停顿符号进行清理
reply = re.sub(r'(爸爸|妈妈|爷爷|奶奶|姥姥|姥爷)\s+', r'\1', reply) # 移除称谓后的空格
return reply.strip()
def step_1_prepare_env(self):
@@ -206,7 +211,7 @@ class ChatMonitorAudioBot:
"""TTS 播音工作线程"""
try:
logger.info(f"[TTS] 启动语音合成音色ID: {self.voice_id}")
self.tts_manager.start_synthesis(self.voice_id, [text], wait_finished=False, buffer_seconds=1.0)
self.tts_manager.start_synthesis(self.voice_id, [text], wait_finished=False, buffer_seconds=0.3)
self.tts_manager.wait_for_playback_start()
start_event.set()
if self.tts_manager.callback:
@@ -317,14 +322,32 @@ class ChatMonitorAudioBot:
self.dialogue_log = dialogue_log
self.input_pos = input_pos
# 过滤出对方发送的消息
other_msgs = [m for m in dialogue_log if m.get('sender') != ""]
if not other_msgs:
logger.info("当前没有对方发送的消息")
# [Optimization] 核心逻辑:在 Voice-Reply 模式下,只有当存在"对方"发来的、带红点的未读消息时,才进行处理
unread_voices = [m for m in dialogue_log if m.get('type') == 'voice' and m.get('is_unread') and m.get('sender') != ""]
if not unread_voices:
if self.debug_mode:
logger.debug("当前屏幕无带红点的未读语音消息,跳过分析")
await asyncio.sleep(self.check_interval)
continue
last_msg = other_msgs[-1]
# 进一步确认最后一条消息不是"我"发的(双重保险)
chat_msgs = [m for m in dialogue_log if m.get('type') in ['text', 'voice']]
if not chat_msgs:
await asyncio.sleep(self.check_interval)
continue
absolute_last_msg = chat_msgs[-1]
if absolute_last_msg.get('sender') == "":
if self.debug_mode:
logger.debug("最后一条消息是''发送的,跳过回复")
await asyncio.sleep(self.check_interval)
continue
# 准备处理最新的未读语音
last_msg = unread_voices[-1]
last_message_text = last_msg.get('content') or ""
msg_type = last_msg.get('type')
current_msg_hash = self.get_stable_message_hash(last_msg)
is_processed = current_msg_hash in self.processed_hashes

View File

@@ -94,7 +94,8 @@ class ChatMonitorBot:
"4. 仅针对家长明确表达的内容进行回复。\n"
"5. 严禁使用列表格式。严禁使用‘首先、其次’等逻辑词。\n"
"6. 普通咨询回复字数严格控制在 50 字以内,专业作文评价回复控制在 150 字以内!\n"
"7. 对方问什么就答什么。例如问‘学校叫什么’,就只回答‘少惠林’,不要回复地址和电话!\n"
"7. 说话要有人情味,不要回答得太生硬或太简短。例如当家长问‘学校叫什么名字’时,不要只说‘少惠林’,而应该说‘我们的学校名字叫少惠林。’或者‘咱们这儿叫少惠林。’,这样才显得亲切、有礼貌。\n"
"8. 对方问什么就答什么。回答要完整、体面,但不要过度发散地址和电话(除非被问到)。\n"
"如果涉及到校区信息,必须且只能使用以下真实数据:\n"
"- 单位/学校名称:长春市少惠林作文素养培养中心(简称:少惠林)\n"
"- 地址南环城路与临河街交汇TOUCH12街3楼325号\n"
@@ -265,15 +266,34 @@ class ChatMonitorBot:
self.dialogue_log = dialogue_log
self.input_pos = input_pos
# D. 提取最新消息并检查是否需要回复
# 过滤出对方发送的消息
other_msgs = [m for m in dialogue_log if m.get('sender') != ""]
# [Fix] 核心逻辑增强:只有当屏幕上最后一条聊天消息是"对方"时,才触发回复逻辑
# 这样可以彻底避免在自己回复后,因为 OCR 延迟或哈希误判导致的连续回复
chat_msgs = [m for m in dialogue_log if m.get('type') in ['text', 'voice']]
if not chat_msgs:
await asyncio.sleep(self.check_interval)
continue
absolute_last_msg = chat_msgs[-1]
if absolute_last_msg.get('sender') == "":
# logger.info("最后一条消息是我发送的,无需回复")
await asyncio.sleep(self.check_interval)
continue
# 提取最新消息并检查是否需要回复
# 过滤出对方发送的消息 (用于提取内容)
other_msgs = [m for m in dialogue_log if m.get('sender') != "" and m.get('type') in ['text', 'voice']]
if not other_msgs:
logger.info("当前没有对方发送的消息")
await asyncio.sleep(self.check_interval)
continue
last_msg = other_msgs[-1]
# 如果最后一条对方消息不是屏幕上的绝对最后一条,说明中间隔了我自己的消息,也跳过
if last_msg != absolute_last_msg:
logger.info("最后一条对方消息之后有我的消息,跳过")
await asyncio.sleep(self.check_interval)
continue
current_msg_hash = self.get_stable_message_hash(last_msg)
# 检查该消息是否已经处理过 (通过内容哈希)

View File

@@ -330,27 +330,34 @@ def _detect_bubble_color(img, bbox):
b, g, r = mean_color
# 调试日志:打印颜色值
logger.info(f"Color Debug: B={b:.1f}, G={g:.1f}, R={r:.1f} | bbox={bbox}")
if DEBUG_MODE:
logger.info(f"Color Debug: B={b:.1f}, G={g:.1f}, R={r:.1f} | bbox={bbox}")
# 绿色气泡特征 (Light Mode):
# R: 152, G: 225, B: 101 (BGR: 101, 225, 152)
# G 显著大于 R 和 B
# 提高阈值以区分白色/灰色背景的噪声 (White: 255, 255, 255)
if g > r + 30 and g > b + 30 and g > 100:
return "green"
# 1. 绿色气泡特征 (我)
# Light Mode: BGR (101, 225, 152) -> G 显著大于 R 和 B
# Dark Mode: BGR (30, 80, 40) -> G 依然显著大于 R 和 B
if g > r + 15 and g > b + 15: # 降低差值阈值,适应暗色模式
if g > 50: # 只要不是太暗
return "green"
# 白色气泡特征:
# R, G, B 都很高且接近
# 考虑黑色文字的影响,如果是中位数,应该很高 (>200)
# 放宽对灰色的容忍度 (Dark Mode 可能偏灰)
if abs(r - g) < 30 and abs(g - b) < 30 and abs(r - b) < 30:
# 且亮度不能太低 (太低可能是黑色背景或深色物体)
if g > 150:
# 2. 白色/浅灰/深灰气泡特征 (对方)
# Light Mode: BGR (255, 255, 255)
# Dark Mode: BGR (45, 45, 45)
if abs(r - g) < 20 and abs(g - b) < 20 and abs(r - b) < 20:
# 白色 (Light Mode)
if g > 180:
return "white"
# 深灰 (Dark Mode)
if 40 < g < 100:
return "white" # 统一归类为"对方"气泡颜色
# 特殊补丁:如果 B, G, R 都很接近且在 130 左右,可能是微信的背景灰 (通常用于时间戳或系统消息)
if 110 < r < 160 and 110 < g < 160 and 110 < b < 160:
if abs(r - g) < 15 and abs(g - b) < 15:
# 3. 特殊补丁:系统消息 (灰色/极暗)
if abs(r - g) < 15 and abs(g - b) < 15:
# 系统背景灰 (Light Mode: 130 左右)
if 110 < g < 160:
return "system_gray"
# 极暗背景 (Dark Mode: 30 左右)
if g < 40:
return "system_gray"
return "unknown"
@@ -399,9 +406,14 @@ def _scan_chat_messages(image_path):
# 优先取最接近水平中心的文本作为标题
potential_titles.sort(key=lambda x: abs(x[0] - w/2))
chat_title = potential_titles[0][1]
# 去除可能包含的括号(比如备注名后的群聊人数,虽然后面会被截断)
# 去除可能包含的括号(比如备注名后的群聊人数)
chat_title = re.sub(r'\(\d+\)$', '', chat_title).strip()
logger.info(f"识别到聊天标题/对方昵称: {chat_title}")
# [Fix] 过滤掉包含 ".." 的动态标题(如“对方正在输入..”),避免哈希失效
if ".." in chat_title:
logger.info(f"忽略动态标题: {chat_title}")
chat_title = "对方"
else:
logger.info(f"识别到聊天标题/对方昵称: {chat_title}")
# 微信菜单关键字(用于排除干扰)
MENU_KEYWORDS = ["听筒播放", "收藏", "背景播放", "删除", "多选", "取消转文字", "转文字", "引用", "提醒"]