修复2个问题: OCR拼接合并 + 微信定位冻结3-5s

问题1: OCR拼接合并 (2处改动)
  - easyocr_worker.py: ' '.join→'\n'.join, 各检测区域用换行而非空格拼接
    避免 '兑换码导入错误时如何处置?游戏专用小筑憩-草一' 式合并
  - _process_ocr_output 尾部新增 _split_merged_ocr_text:
    在中文句尾标点([。?!?])后紧跟CJK处拆分,两侧CJK>=3且非续接语气词
    将合并文本分为多条消息,各自独立参与KB匹配

问题2: 微信 _fresh_window 冻结3-5s (2处改动)
  - _poll: 对wechat跳过 _fresh_window(ControlFromHandle构建Electron UIA树冻结)
    用 win=None 代替
  - _read_messages(wechat): 接收window=None, OCR失败后惰性创建UIA控件
    大多数轮次 OCR 读到消息就直接返回,不碰UIA树
This commit is contained in:
youpaishidifu
2026-07-29 18:11:29 +08:00
parent 722d2a5438
commit ef227eb7b0
2 changed files with 74 additions and 6 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ def _process_image(img_b64: str, reader) -> str:
img = img.convert("RGBA") img = img.convert("RGBA")
img_np = np.array(img) img_np = np.array(img)
results = reader.readtext(img_np) results = reader.readtext(img_np)
return " ".join([r[1] for r in results]).strip() return "\n".join([r[1] for r in results]).strip()
def _main(): def _main():
+73 -5
View File
@@ -1500,7 +1500,12 @@ class MessageMonitor:
if self._is_reply_fragment(msg): if self._is_reply_fragment(msg):
logger.debug(f"[ocr] stage[{i}] 回复残片跳过: {msg[:60]!r}") logger.debug(f"[ocr] stage[{i}] 回复残片跳过: {msg[:60]!r}")
else: else:
expanded.append(msg) # EasyOCR 常把横向相邻的两条消息(用户消息+群昵称/UI文字)
# 检测为同一行,导致"兑换码导入错误时如何处置?游戏专用小筑憩-草一"
# 这种合并文本。尝试在中文句尾标点处拆分为多条。
split_msgs = self._split_merged_ocr_text(msg)
for sm in split_msgs:
expanded.append(sm)
# —— 逐行过滤 —— # —— 逐行过滤 ——
ocr_lines = expanded ocr_lines = expanded
@@ -2189,6 +2194,57 @@ class MessageMonitor:
# 拿不准保守处理 # 拿不准保守处理
return True return True
@staticmethod
def _split_merged_ocr_text(text: str) -> list:
"""尝试将 OCR 单行合并文本在中文句尾标点处拆分。
EasyOCR 常把横向相邻的两条消息(用户问句 + 群昵称/UI文字/其他人消息)
检测为同一行文本,导致"兑换码导入错误时如何处置?游戏专用小筑憩-草一"式合并。
启发式: 在[。?!?]后紧跟CJK字符的位置拆分,前提是两侧都有足够中文。
不拆分续接句(第二段以"但/而/并/且/或/还/也/又/就/才/再/变"开头)。
"""
if len(text) < 10:
return [text]
_continuations = {'但', '而', '并', '且', '或', '还', '也', '又',
'就', '才', '再', '变', '便', '则', '却', '虽',
'因', '为', '与', '和', '同', '跟'}
result = []
remaining = text
max_splits = 2 # 最多拆 2 次(3 段),防过度拆分
while max_splits > 0:
# 找[。?!?]后紧跟CJK的位置
m = re.search(r'([。?!?])([\u4e00-\u9fff])', remaining)
if not m:
break
split_pos = m.start() + 1 # 标点后
first = remaining[:split_pos]
second = remaining[split_pos:]
# 两侧都需有足够 CJK
first_cjk = sum(1 for c in first if '\u4e00' <= c <= '\u9fff')
second_cjk = sum(1 for c in second if '\u4e00' <= c <= '\u9fff')
if first_cjk < 3 or second_cjk < 3:
break
# 第二段不以续接词开头
if second[0] in _continuations:
break
# 第二段太短(少于 3 CJK)→ 不拆
first_part = first
second_part = second
result.append(first_part)
remaining = second_part
max_splits -= 1
result.append(remaining)
if len(result) > 1:
logger.debug(f"[ocr] 句尾标点拆分: {[r[:60] for r in result]}")
return result
def _is_reply_fragment(self, text: str) -> bool: def _is_reply_fragment(self, text: str) -> bool:
"""检查文本是否为 bot 回复内容的碎片(OCR 截取的 md 残片/跨行碎片)。""" """检查文本是否为 bot 回复内容的碎片(OCR 截取的 md 残片/跨行碎片)。"""
if not self._replied_texts or len(text) < 6: if not self._replied_texts or len(text) < 6:
@@ -2961,6 +3017,12 @@ class MessageMonitor:
# 截图未变→跳过 fallback # 截图未变→跳过 fallback
if self._ocr_unchanged.get("wechat"): if self._ocr_unchanged.get("wechat"):
return [] return []
# 未提供 UIA 控件(_poll 跳过了 _fresh_window 避免 Electron 冻结)
# 此时惰性创建,仅在 OCR 失败后 fallback 时才需要
if window is None:
window = self._fresh_window(platform)
if window is None:
return []
# 微信: UIA 兜底(可能读到陈旧数据或 UI 文字) # 微信: UIA 兜底(可能读到陈旧数据或 UI 文字)
texts = self._read_message_list(window) texts = self._read_message_list(window)
if texts: if texts:
@@ -3238,10 +3300,16 @@ class MessageMonitor:
return return
# 2. 从 HWND 创建全新 Control(绕过缓存) # 2. 从 HWND 创建全新 Control(绕过缓存)
win = self._fresh_window(platform) # QQ: 需要 UIA 控件做多种 fallback 读取(ListControl、全窗爬文本等)
if not win: # 微信( Electron ): ControlFromHandle 构建 UIA 控件树冻结 3-5s,
logger.debug(f"[{platform}] 创建窗口控件失败") # 而微信 OCR 读到消息就直接返回,不需要 UIA。只在 OCR 空/fallback 时创建。
return if platform == "qq":
win = self._fresh_window(platform)
if not win:
logger.debug(f"[{platform}] 创建窗口控件失败")
return
else:
win = None # 惰性创建:OCR 失败后需要 fallback 时再建
# 3. 读消息 # 3. 读消息
texts = self._read_messages(win, platform) texts = self._read_messages(win, platform)