diff --git a/Util/AlyTtsKit.py b/Util/AlyTtsKit.py index e7f2f88..7306680 100644 --- a/Util/AlyTtsKit.py +++ b/Util/AlyTtsKit.py @@ -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}') diff --git a/Util/__pycache__/AlyTtsKit.cpython-310.pyc b/Util/__pycache__/AlyTtsKit.cpython-310.pyc index 2573873..0476139 100644 Binary files a/Util/__pycache__/AlyTtsKit.cpython-310.pyc and b/Util/__pycache__/AlyTtsKit.cpython-310.pyc differ diff --git a/WeiXin/ChatMonitorAll_ReplyAudio.py b/WeiXin/ChatMonitorAll_ReplyAudio.py index 5a2f8d1..98214ad 100644 --- a/WeiXin/ChatMonitorAll_ReplyAudio.py +++ b/WeiXin/ChatMonitorAll_ReplyAudio.py @@ -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 diff --git a/WeiXin/ChatMonitorAll_ReplyTxt.py b/WeiXin/ChatMonitorAll_ReplyTxt.py index b76b18e..8ee8c10 100644 --- a/WeiXin/ChatMonitorAll_ReplyTxt.py +++ b/WeiXin/ChatMonitorAll_ReplyTxt.py @@ -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) # 检查该消息是否已经处理过 (通过内容哈希) diff --git a/WeiXin/WxUtil.py b/WeiXin/WxUtil.py index 6855d65..bc24f36 100644 --- a/WeiXin/WxUtil.py +++ b/WeiXin/WxUtil.py @@ -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 = ["听筒播放", "收藏", "背景播放", "删除", "多选", "取消转文字", "转文字", "引用", "提醒"] diff --git a/WeiXin/__pycache__/WxUtil.cpython-310.pyc b/WeiXin/__pycache__/WxUtil.cpython-310.pyc index b757945..ac1f5c1 100644 Binary files a/WeiXin/__pycache__/WxUtil.cpython-310.pyc and b/WeiXin/__pycache__/WxUtil.cpython-310.pyc differ