diff --git a/monitor.py b/monitor.py index 75e3341..c81dbb2 100644 --- a/monitor.py +++ b/monitor.py @@ -91,6 +91,11 @@ class MessageMonitor: # 上次回复时间戳(冷却期内跳过该平台 poll) self._last_reply_time: dict = {} # platform → timestamp + # 本轮选中的 OCR 引擎(用于日志标记) + self._last_chosen_engine: dict = {} # platform → "windows"|"easyocr" + # 最近 N 轮 OCR 指纹(去重环路:屏幕内容没变则跳过本轮) + self._recent_ocr_fingerprints: dict = {} # platform → deque of fingerprint strings + # OCR 引擎(Windows 原生,lazy init) self._ocr_engine = None # EasyOCR 备用引擎(lazy init) @@ -694,15 +699,14 @@ class MessageMonitor: score += min(timestamps * 2, 6) return max(score, 0.0) - def _pick_ocr_text(self, win_text: str, easy_text: str, image: Image.Image) -> str: - """双引擎对比,选质量更高的结果。日志记录引擎胜负统计。""" + def _pick_ocr_text(self, win_text: str, easy_text: str, image: Image.Image) -> tuple: + """双引擎对比,选质量更高的结果。日志记录引擎胜负统计。返回 (text, engine_name)。""" if not easy_text: - return win_text + return (win_text, "windows") if not win_text: - return easy_text + return (easy_text, "easyocr") win_score = self._score_ocr_text(win_text) easy_score = self._score_ocr_text(easy_text) - # 引擎选择统计 chosen = "windows" if win_score >= easy_score else "easyocr" self._ocr_engine_wins[chosen] = self._ocr_engine_wins.get(chosen, 0) + 1 total = sum(self._ocr_engine_wins.values()) @@ -711,7 +715,7 @@ class MessageMonitor: f"[ocr] 引擎对比: win={win_score:.1f} vs easy={easy_score:.1f} → 选{chosen} " f"(胜率{ratio:.0%}, {total}轮)" ) - return win_text if chosen == "windows" else easy_text + return (win_text, chosen) if chosen == "windows" else (easy_text, chosen) def _ocr_read_qq(self) -> list: """OCR 截屏 QQ 消息区域 -> 双引擎(Windows OCR + EasyOCR)提取文本。 @@ -834,9 +838,10 @@ class MessageMonitor: logger.debug(f"[ocr] Windows OCR: {win_text[:100]!r}...") logger.debug(f"[ocr] EasyOCR: {easy_text[:100]!r}...") - raw_text = self._pick_ocr_text(win_text, easy_text, im) + raw_text, chosen_engine = self._pick_ocr_text(win_text, easy_text, im) if not raw_text: return [] + self._last_chosen_engine["qq"] = chosen_engine return self._process_ocr_output(raw_text, "qq") except Exception as e: logger.debug(f"[qq] OCR失败: {e}") @@ -922,9 +927,10 @@ class MessageMonitor: logger.debug(f"[wechat] Windows OCR: {win_text[:100]!r}...") logger.debug(f"[wechat] EasyOCR: {easy_text[:100]!r}...") - raw_text = self._pick_ocr_text(win_text, easy_text, im) + raw_text, chosen_engine = self._pick_ocr_text(win_text, easy_text, im) if not raw_text: return [] + self._last_chosen_engine["wechat"] = chosen_engine return self._process_ocr_output(raw_text, "wechat") except Exception as e: logger.debug(f"[wechat] OCR失败: {e}") @@ -948,14 +954,25 @@ class MessageMonitor: self._ocr_garbage_count[platform] = 0 self._ocr_last_text[platform] = raw_text + # —— 跨轮次 OCR 指纹去重(屏幕内容未变→跳过) —— + clean_fp = re.sub(r'\s', '', raw_text)[:500] + recent = self._recent_ocr_fingerprints.setdefault(platform, []) + if clean_fp in recent: + logger.debug(f"[{platform}] OCR 指纹重复(近{len(recent)}轮内相同),跳过本轮") + return [] + recent.append(clean_fp) + if len(recent) > 3: + recent.pop(0) + # —— 行切割 + 尾标清理 —— ocr_lines = [] for l in raw_text.split('\n'): l = re.sub(r'\s*(?:[A-Za-z0-9]~)+\s*$', '', l).strip() if l: ocr_lines.append(l) + engine_label = self._last_chosen_engine.get(platform, "") for i, rl in enumerate(ocr_lines): - logger.info(f"[ocr] raw[{i}] raw_line<{rl!r}>") + logger.info(f"[ocr] raw[{i}][{engine_label}] raw_line<{rl!r}>") # —— 时间戳分割 —— expanded = [] @@ -1383,6 +1400,9 @@ class MessageMonitor: # 行首是 # 标题(# ## ### #### 等) if re.match(r'^#{1,6}\s', text): return True + # ##直接跟汉字(OCR 丢失 ##后空格时常见:##文档导航) + if re.match(r'^#{1,6}[\u4e00-\u9fff]', text): + return True # 包含 markdown 链接 [text](url) if re.search(r'\[.+?\]\(.+?\)', text): return True @@ -1413,12 +1433,16 @@ class MessageMonitor: 本方法找到第一个 md 标记位置,提取其前面的用户文本; 再找到末尾连续非 md 尾巴,提取为第二条用户文本。 """ - # 找第一个 markdown 起始标记的位置 + # ── 整段以 ## 开头 → 无用户文本(无空格 ##中文 也捕获)── + if re.match(r'^#{1,6}(?:\s|\[|[\u4e00-\u9fff])', text): + return [] + + # ── 找第一个 markdown 起始标记的位置 ── # 标记:## 标题、[text](url)、.md)、| 表格分隔、``` 代码块 md_start = len(text) # 默认整段都是 md - # ## 标题(在文本中间出现的) - m = re.search(r'(?<=\S)\s*#{1,6}\s', text) + # ## 标题(放宽:允许 ##后直接跟汉字/[,不限于空格) + m = re.search(r'(?<=\S)\s*#{1,6}(?:\s|\[|[\u4e00-\u9fff])', text) if m and m.start() < md_start: md_start = m.start() @@ -1432,7 +1456,7 @@ class MessageMonitor: if m and m.start() < md_start: md_start = m.start() - # | 表格(前面有非空白字符说明不是行首) + # | 表格 m = re.search(r'(?<=\S)\s*\|', text) if m and m.start() < md_start: md_start = m.start() @@ -1612,6 +1636,12 @@ class MessageMonitor: if part and len(part) > 3: self._replied_texts.add(part) self._seen_messages.add(part) + # 同时追踪去掉 ## 前缀的标题文本(OCR 常丢失 ## 后的空格) + if part.startswith('#'): + no_hash = re.sub(r'^#+\s*', '', part).strip() + if no_hash and len(no_hash) > 2: + self._replied_texts.add(no_hash) + self._seen_messages.add(no_hash) self._last_reply_time[platform] = time.time() return True except Exception as e: @@ -2049,7 +2079,8 @@ class MessageMonitor: # 有纯粹的新文本 → 逐一报告 for item in new_items: self._last_msg[platform] = item - logger.info(f"[{platform}] 新消息: {item[:60]}") + engine_tag = self._last_chosen_engine.get(platform, "") + logger.info(f"[{platform}] 新消息[{engine_tag}]: {item[:60]}") if self._on_message: self._on_message(platform, item) if platform == "qq":