修复:OCR环路五个靶点—①##中文(md检测)②位置0##不提取③跨轮指纹去重④引擎标签⑤回复标题去##追踪
This commit is contained in:
+45
-14
@@ -91,6 +91,11 @@ class MessageMonitor:
|
|||||||
# 上次回复时间戳(冷却期内跳过该平台 poll)
|
# 上次回复时间戳(冷却期内跳过该平台 poll)
|
||||||
self._last_reply_time: dict = {} # platform → timestamp
|
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)
|
# OCR 引擎(Windows 原生,lazy init)
|
||||||
self._ocr_engine = None
|
self._ocr_engine = None
|
||||||
# EasyOCR 备用引擎(lazy init)
|
# EasyOCR 备用引擎(lazy init)
|
||||||
@@ -694,15 +699,14 @@ class MessageMonitor:
|
|||||||
score += min(timestamps * 2, 6)
|
score += min(timestamps * 2, 6)
|
||||||
return max(score, 0.0)
|
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:
|
if not easy_text:
|
||||||
return win_text
|
return (win_text, "windows")
|
||||||
if not win_text:
|
if not win_text:
|
||||||
return easy_text
|
return (easy_text, "easyocr")
|
||||||
win_score = self._score_ocr_text(win_text)
|
win_score = self._score_ocr_text(win_text)
|
||||||
easy_score = self._score_ocr_text(easy_text)
|
easy_score = self._score_ocr_text(easy_text)
|
||||||
# 引擎选择统计
|
|
||||||
chosen = "windows" if win_score >= easy_score else "easyocr"
|
chosen = "windows" if win_score >= easy_score else "easyocr"
|
||||||
self._ocr_engine_wins[chosen] = self._ocr_engine_wins.get(chosen, 0) + 1
|
self._ocr_engine_wins[chosen] = self._ocr_engine_wins.get(chosen, 0) + 1
|
||||||
total = sum(self._ocr_engine_wins.values())
|
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"[ocr] 引擎对比: win={win_score:.1f} vs easy={easy_score:.1f} → 选{chosen} "
|
||||||
f"(胜率{ratio:.0%}, {total}轮)"
|
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:
|
def _ocr_read_qq(self) -> list:
|
||||||
"""OCR 截屏 QQ 消息区域 -> 双引擎(Windows OCR + EasyOCR)提取文本。
|
"""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] Windows OCR: {win_text[:100]!r}...")
|
||||||
logger.debug(f"[ocr] EasyOCR: {easy_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:
|
if not raw_text:
|
||||||
return []
|
return []
|
||||||
|
self._last_chosen_engine["qq"] = chosen_engine
|
||||||
return self._process_ocr_output(raw_text, "qq")
|
return self._process_ocr_output(raw_text, "qq")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"[qq] OCR失败: {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] Windows OCR: {win_text[:100]!r}...")
|
||||||
logger.debug(f"[wechat] EasyOCR: {easy_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:
|
if not raw_text:
|
||||||
return []
|
return []
|
||||||
|
self._last_chosen_engine["wechat"] = chosen_engine
|
||||||
return self._process_ocr_output(raw_text, "wechat")
|
return self._process_ocr_output(raw_text, "wechat")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"[wechat] OCR失败: {e}")
|
logger.debug(f"[wechat] OCR失败: {e}")
|
||||||
@@ -948,14 +954,25 @@ class MessageMonitor:
|
|||||||
self._ocr_garbage_count[platform] = 0
|
self._ocr_garbage_count[platform] = 0
|
||||||
self._ocr_last_text[platform] = raw_text
|
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 = []
|
ocr_lines = []
|
||||||
for l in raw_text.split('\n'):
|
for l in raw_text.split('\n'):
|
||||||
l = re.sub(r'\s*(?:[A-Za-z0-9]~)+\s*$', '', l).strip()
|
l = re.sub(r'\s*(?:[A-Za-z0-9]~)+\s*$', '', l).strip()
|
||||||
if l:
|
if l:
|
||||||
ocr_lines.append(l)
|
ocr_lines.append(l)
|
||||||
|
engine_label = self._last_chosen_engine.get(platform, "")
|
||||||
for i, rl in enumerate(ocr_lines):
|
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 = []
|
expanded = []
|
||||||
@@ -1383,6 +1400,9 @@ class MessageMonitor:
|
|||||||
# 行首是 # 标题(# ## ### #### 等)
|
# 行首是 # 标题(# ## ### #### 等)
|
||||||
if re.match(r'^#{1,6}\s', text):
|
if re.match(r'^#{1,6}\s', text):
|
||||||
return True
|
return True
|
||||||
|
# ##直接跟汉字(OCR 丢失 ##后空格时常见:##文档导航)
|
||||||
|
if re.match(r'^#{1,6}[\u4e00-\u9fff]', text):
|
||||||
|
return True
|
||||||
# 包含 markdown 链接 [text](url)
|
# 包含 markdown 链接 [text](url)
|
||||||
if re.search(r'\[.+?\]\(.+?\)', text):
|
if re.search(r'\[.+?\]\(.+?\)', text):
|
||||||
return True
|
return True
|
||||||
@@ -1413,12 +1433,16 @@ class MessageMonitor:
|
|||||||
本方法找到第一个 md 标记位置,提取其前面的用户文本;
|
本方法找到第一个 md 标记位置,提取其前面的用户文本;
|
||||||
再找到末尾连续非 md 尾巴,提取为第二条用户文本。
|
再找到末尾连续非 md 尾巴,提取为第二条用户文本。
|
||||||
"""
|
"""
|
||||||
# 找第一个 markdown 起始标记的位置
|
# ── 整段以 ## 开头 → 无用户文本(无空格 ##中文 也捕获)──
|
||||||
|
if re.match(r'^#{1,6}(?:\s|\[|[\u4e00-\u9fff])', text):
|
||||||
|
return []
|
||||||
|
|
||||||
|
# ── 找第一个 markdown 起始标记的位置 ──
|
||||||
# 标记:## 标题、[text](url)、.md)、| 表格分隔、``` 代码块
|
# 标记:## 标题、[text](url)、.md)、| 表格分隔、``` 代码块
|
||||||
md_start = len(text) # 默认整段都是 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:
|
if m and m.start() < md_start:
|
||||||
md_start = m.start()
|
md_start = m.start()
|
||||||
|
|
||||||
@@ -1432,7 +1456,7 @@ class MessageMonitor:
|
|||||||
if m and m.start() < md_start:
|
if m and m.start() < md_start:
|
||||||
md_start = m.start()
|
md_start = m.start()
|
||||||
|
|
||||||
# | 表格(前面有非空白字符说明不是行首)
|
# | 表格
|
||||||
m = re.search(r'(?<=\S)\s*\|', text)
|
m = re.search(r'(?<=\S)\s*\|', text)
|
||||||
if m and m.start() < md_start:
|
if m and m.start() < md_start:
|
||||||
md_start = m.start()
|
md_start = m.start()
|
||||||
@@ -1612,6 +1636,12 @@ class MessageMonitor:
|
|||||||
if part and len(part) > 3:
|
if part and len(part) > 3:
|
||||||
self._replied_texts.add(part)
|
self._replied_texts.add(part)
|
||||||
self._seen_messages.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()
|
self._last_reply_time[platform] = time.time()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -2049,7 +2079,8 @@ class MessageMonitor:
|
|||||||
# 有纯粹的新文本 → 逐一报告
|
# 有纯粹的新文本 → 逐一报告
|
||||||
for item in new_items:
|
for item in new_items:
|
||||||
self._last_msg[platform] = item
|
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:
|
if self._on_message:
|
||||||
self._on_message(platform, item)
|
self._on_message(platform, item)
|
||||||
if platform == "qq":
|
if platform == "qq":
|
||||||
|
|||||||
Reference in New Issue
Block a user