This commit is contained in:
HuangHai
2026-01-25 14:11:04 +08:00
parent 1df968a2f7
commit b4d0e56037
6 changed files with 129 additions and 86 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -23,15 +23,30 @@ log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Logs")
if not os.path.exists(log_dir): if not os.path.exists(log_dir):
os.makedirs(log_dir) os.makedirs(log_dir)
logging.basicConfig( # 设置 logger
level=logging.INFO, logger = logging.getLogger("T6_AutoChatMonitor")
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', logger.setLevel(logging.INFO)
handlers=[
logging.FileHandler(os.path.join(log_dir, "T6_AutoChatMonitor.log"), encoding='utf-8', mode='w'), # 清除现有的 handlers,防止重复打印或配置冲突
logging.StreamHandler() if logger.hasHandlers():
] logger.handlers.clear()
)
logger = logging.getLogger("T5_Monitor") # 创建 FileHandler
log_file_path = os.path.join(log_dir, "T6_AutoChatMonitor.log")
file_handler = logging.FileHandler(log_file_path, encoding='utf-8', mode='w')
file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
logger.addHandler(file_handler)
# 创建 StreamHandler
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
logger.addHandler(stream_handler)
# 防止日志传播到 root logger,避免重复输出
logger.propagate = False
# 打印日志文件位置,方便确认
logger.info(f"日志文件路径: {log_file_path}")
# 配置参数 # 配置参数
CHECK_INTERVAL = 10 # 检查频率 (秒) CHECK_INTERVAL = 10 # 检查频率 (秒)
@@ -93,10 +108,10 @@ class ChatBot:
full_response += chunk full_response += chunk
return full_response.strip().strip('"').strip('“').strip('”') return full_response.strip().strip('"').strip('“').strip('”')
async def process_single_voice(self, voice_msg): async def process_single_voice(self, voice_msg, next_msg=None, input_box_y=None):
""" """
处理单个语音消息的完整流程: 处理单个语音消息的完整流程:
长按 -> VLM找转文字 -> 点击 -> 等待 -> 截图OCR -> 长按 -> VLM找取消转文字 -> 点击 长按 -> CV找转文字 -> 点击 -> 等待 -> 截图OCR -> 长按 -> CV找取消转文字 -> 点击
返回: 转换后的文本内容 (如果没有转换成功,返回 None) 返回: 转换后的文本内容 (如果没有转换成功,返回 None)
""" """
vx, vy = voice_msg['coordinates'] vx, vy = voice_msg['coordinates']
@@ -137,16 +152,52 @@ class ChatBot:
self.d.screenshot(ocr_shot_path) self.d.screenshot(ocr_shot_path)
# OCR 识别 # OCR 识别
# 策略:识别整个屏幕,找到位于语音消息下方的文字 # 策略:识别整个屏幕,但只提取位于当前语音消息下方,且在下一条消息(如果有)上方的内容
# 简单起见,我们假设转换出的文字在语音气泡下方 y > vy
ocr_results = self.ocr_kit.read_text(ocr_shot_path) ocr_results = self.ocr_kit.read_text(ocr_shot_path)
# 按 Y 坐标排序,确保从上往下处理
ocr_results.sort(key=lambda x: (x[0][0][1] + x[0][2][1]) / 2)
extracted_text = [] extracted_text = []
# 准备下一条消息的内容片段作为停止条件
next_msg_snippet = None
if next_msg and next_msg.get("type") == "text":
c = next_msg.get("content", "").strip()
if c:
next_msg_snippet = c[:8] # 取前8个字符作为指纹
for bbox, text, conf in ocr_results: for bbox, text, conf in ocr_results:
# bbox center y # bbox center y
c_y = (bbox[0][1] + bbox[2][1]) / 2 c_y = (bbox[0][1] + bbox[2][1]) / 2
# 过滤条件:在语音气泡下方,且距离不太远 (例如 300 像素内)
if c_y > vy and c_y < vy + 400: # 1. 过滤掉当前语音气泡及以上的内容
# 语音气泡中心是 vy,底部大概在 vy + 30 左右
if c_y <= vy + 25:
continue
# 2. 如果有输入框坐标,过滤掉输入框以下的内容
if input_box_y and c_y >= input_box_y - 30:
continue
# 3. 如果遇到下一条消息的内容,停止读取
if next_msg_snippet and next_msg_snippet in text:
logger.info(f"🛑 遇到下一条消息内容 '{text}',停止 OCR 录入。")
break
# 4. 如果下一条是语音,尝试通过时长文本判断停止
if next_msg and next_msg.get("type") == "voice":
v_dur = next_msg.get("content", "").strip()
# 语音时长通常比较短,且包含 " 符号
if v_dur and v_dur in text and len(text) < 10:
logger.info(f"🛑 遇到下一条语音时长 '{text}',停止 OCR 录入。")
break
# 5. 安全兜底:如果距离当前语音气泡太远(超过600像素),停止
# 这可以防止读取到屏幕底部无关的内容
if c_y > vy + 600:
break
extracted_text.append(text) extracted_text.append(text)
full_text = " ".join(extracted_text) full_text = " ".join(extracted_text)
@@ -208,6 +259,43 @@ class ChatBot:
messages = result_data.get("messages", []) messages = result_data.get("messages", [])
input_center = result_data.get("input_box") input_center = result_data.get("input_box")
# --- Debug Visualization ---
try:
import cv2
import numpy as np
debug_img = cv2.imread(tmp_shot)
if debug_img is not None:
# Draw input box
if input_center:
ix, iy = input_center
cv2.circle(debug_img, (int(ix), int(iy)), 10, (0, 0, 255), -1) # Red dot
cv2.putText(debug_img, "Input", (int(ix), int(iy)-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)
for msg in messages:
m_type = msg.get("type", "text")
coords = msg.get("center") or msg.get("coordinates")
if coords:
cx, cy = int(coords[0]), int(coords[1])
if m_type == "voice":
# Green box for voice
# We don't have w/h, so just draw a fixed size box or circle
# Let's draw a rectangle around the center
cv2.rectangle(debug_img, (cx-50, cy-25), (cx+50, cy+25), (0, 255, 0), 3)
cv2.circle(debug_img, (cx, cy), 5, (0, 255, 0), -1)
cv2.putText(debug_img, "Voice", (cx, cy-30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
# else:
# # Blue box for text
# cv2.rectangle(debug_img, (cx-50, cy-25), (cx+50, cy+25), (255, 0, 0), 2)
# cv2.putText(debug_img, "Text", (cx, cy-30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0), 2)
debug_path = os.path.join(self.screenshot_dir, "t6_debug_result.jpg")
cv2.imwrite(debug_path, debug_img)
logger.info(f"🐛 Debug 标记图已保存: {debug_path}")
except Exception as e:
logger.error(f"Debug drawing failed: {e}")
# ---------------------------
dialogue_log = [] dialogue_log = []
voice_messages = [] # 存储所有语音消息 voice_messages = [] # 存储所有语音消息
@@ -245,12 +333,20 @@ class ChatBot:
# 4. 语音处理逻辑 # 4. 语音处理逻辑
processed_voice_content = None processed_voice_content = None
input_y = input_center[1] if input_center else None
if self.is_first_run: if self.is_first_run:
logger.info("🌟 首次运行:处理屏幕上所有语音消息...") logger.info("🌟 首次运行:处理屏幕上所有语音消息...")
for v_msg in voice_messages: for v_msg in voice_messages:
# 查找下一条消息,用于限定 OCR 范围
try:
idx = messages.index(v_msg)
next_msg = messages[idx + 1] if idx + 1 < len(messages) else None
except ValueError:
next_msg = None
# 无论是否未读,都处理 # 无论是否未读,都处理
text = await self.process_single_voice(v_msg) text = await self.process_single_voice(v_msg, next_msg, input_y)
if text: if text:
# 更新日志中的内容 (这比较复杂,因为 log 是 append 的) # 更新日志中的内容 (这比较复杂,因为 log 是 append 的)
# 简单起见,我们只记录最后一条处理的内容用于回复判断 # 简单起见,我们只记录最后一条处理的内容用于回复判断
@@ -267,7 +363,15 @@ class ChatBot:
last_voice = voice_messages[-1] last_voice = voice_messages[-1]
if last_voice.get("is_unread", False): if last_voice.get("is_unread", False):
logger.info("🔴 发现未读语音消息 (最后一条),正在处理...") logger.info("🔴 发现未读语音消息 (最后一条),正在处理...")
processed_voice_content = await self.process_single_voice(last_voice)
# 查找下一条消息
try:
idx = messages.index(last_voice)
next_msg = messages[idx + 1] if idx + 1 < len(messages) else None
except ValueError:
next_msg = None
processed_voice_content = await self.process_single_voice(last_voice, next_msg, input_y)
else: else:
logger.info("⚪ 最后一条语音消息已读,跳过处理。") logger.info("⚪ 最后一条语音消息已读,跳过处理。")
@@ -308,7 +412,7 @@ class ChatBot:
if reply and input_center: if reply and input_center:
# 输入并发送 # 输入并发送
await perform_input_action(self.d, input_center, reply) perform_input_action(self.d, input_center, reply)
# 记录回复时间 # 记录回复时间
self.last_interaction_time = time.time() self.last_interaction_time = time.time()
@@ -319,73 +423,12 @@ class ChatBot:
logger.error(f"❌ 主循环发生错误: {e}", exc_info=True) logger.error(f"❌ 主循环发生错误: {e}", exc_info=True)
await asyncio.sleep(CHECK_INTERVAL) await asyncio.sleep(CHECK_INTERVAL)
# 5. 常规回复逻辑
if not dialogue_log:
logger.info("⏳ 未发现有效对话内容,继续监控...")
await asyncio.sleep(CHECK_INTERVAL)
continue
# 2. 检查是否有新消息
current_last_msg = dialogue_log[-1]
logger.info(f"💬 当前最后一条消息: {current_last_msg}")
history_text = "\n".join(dialogue_log)
# 判断逻辑:如果最后一条消息是“对方”发的,且与上次不同,则回复
if "对方:" in current_last_msg and current_last_msg != self.last_message_text:
# 关键检查:如果包含 "(待转换)",说明语音还没转文字 (或者OCR失败),跳过
if "(待转换)" in current_last_msg:
logger.info(f"🚫 检测到未转换的语音消息,跳过回复生成,等待转文字... ({current_last_msg})")
await asyncio.sleep(2)
continue
logger.info(f"📩 检测到新消息: {current_last_msg}")
reply = await self.get_reply(history_text)
logger.info(f"🤖 生成回复: {reply}")
# 执行输入发送
if input_center:
center_point = input_center
logger.info(f"📍 使用 VLM 识别的输入框坐标: {center_point}")
else:
center_point, _ = find_input_box_center(tmp_shot)
logger.info(f"📍 使用 CV 识别的输入框坐标: {center_point}")
perform_input_action(self.d, center_point, reply, auto_send=True)
self.last_message_text = f"我: {reply}" # 更新状态
self.last_interaction_time = time.time()
self.proactive_count = 0
# 3. 检查是否需要主动询问
elif "我:" in current_last_msg:
elapsed = time.time() - self.last_interaction_time
if elapsed > SILENCE_THRESHOLD and self.proactive_count < MAX_PROACTIVE_PROMPTS:
logger.info(f"⏳ 用户长时间未响应 ({int(elapsed)}s),准备主动询问...")
proactive_reply = await self.get_reply(history_text, is_proactive=True)
logger.info(f"🤖 发起主动询问: {proactive_reply}")
if input_center:
center_point = input_center
else:
center_point, _ = find_input_box_center(tmp_shot)
perform_input_action(self.d, center_point, proactive_reply, auto_send=True)
self.proactive_count += 1
self.last_interaction_time = time.time()
self.last_message_text = f"我: {proactive_reply}"
# 更新最后一条消息记录
if "对方:" in current_last_msg:
self.last_message_text = current_last_msg
except Exception as e:
logger.error(f"❌ 监控循环出错: {e}", exc_info=True)
await asyncio.sleep(CHECK_INTERVAL)
if __name__ == "__main__": if __name__ == "__main__":
Win32Patch.patch() Win32Patch.patch()
bot = ChatBot() bot = ChatBot()
try:
asyncio.run(bot.run()) asyncio.run(bot.run())
except KeyboardInterrupt:
logger.info("🛑 用户手动停止程序。")
except Exception as e:
logger.error(f"❌ 程序异常退出: {e}", exc_info=True)