修复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:
+1
-1
@@ -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
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user