fix: 修复bot回复一次后不再回复——filter4误杀混合消息
- 新增_strip_bot_reply_fragments:在_process_ocr_output的filter4(回复残片)判定前, 先剥离OCR拼接到一起的bot旧回复片段,保留用户真实提问(OCR噪声鲁棒:归一化标点/空格) - 修复根因:bot回复后,后续截图把'用户新消息+bot旧回复'拼成一行,filter4检测到bot旧 回复文本即把整段(含用户新消息)误判为残片丢弃,导致bot永远不再回复 - _is_reply_fragment改分数阈值:原Latin>=6/CJK>=5过于敏感,用户昵称含'Minecraft'或 问句含服务器名'草原探险服务器'即被误杀;改为需公共子串>=15或(>=8且占较短文本>=60%), 仅当文本'大部分'是bot回复时才判为残片,保留正常用户问句 - 同步修复_group聊天慢于私聊:消息不再被误杀→不再陷入同帧反复OCR死循环,回复延迟回归15~20s
This commit is contained in:
+98
-14
@@ -3139,6 +3139,11 @@ class MessageMonitor:
|
||||
msg = re.sub(r'\s*(?:[A-Za-z0-9]~)+\s*$', '', buf.strip())
|
||||
if msg:
|
||||
msg = re.sub(r'\s+#[\u4e00-\u9fff][^\s#\n]{0,30}$', '', msg).strip()
|
||||
if msg:
|
||||
# ⭐ 先剥离 bot 旧回复片段:OCR 把「用户新消息 + bot 旧回复」
|
||||
# 拼成一行时,filter4 会误判整段为残片丢弃,导致用户新消息
|
||||
# 永久丢失(bot 回复一次后再也不回复)。剥离后保留用户提问。
|
||||
msg = self._strip_bot_reply_fragments(msg)
|
||||
if msg:
|
||||
if self._is_reply_fragment(msg):
|
||||
logger.info(f"[ocr] stage[{i}] [filter4/回复残片] DROP 回复残片跳过: {msg[:60]!r}")
|
||||
@@ -3180,9 +3185,12 @@ class MessageMonitor:
|
||||
msg = re.sub(r'\s*(?:[A-Za-z0-9]~)+\s*$', '', buf.strip())
|
||||
if msg:
|
||||
msg = re.sub(r'\s+#[\u4e00-\u9fff][^\s#\n]{0,30}$', '', msg).strip()
|
||||
if msg:
|
||||
# ⭐ 同上:尾部消息也要先剥离 bot 旧回复片段,避免整段误判为残片
|
||||
msg = self._strip_bot_reply_fragments(msg)
|
||||
if msg:
|
||||
if self._is_reply_fragment(msg):
|
||||
logger.info(f"[ocr] stage[{i}] [filter4/回复残片] DROP 回复残片跳过: {msg[:60]!r}")
|
||||
logger.info(f"[ocr] stage[{i}] [filter4/回复残片] DROP 回复残片跳过(尾部): {msg[:60]!r}")
|
||||
else:
|
||||
logger.info(f"[ocr] stage[{i}] [filter/放行] 尾部消息通过所有过滤器,添加到expanded: {msg[:60]!r}")
|
||||
# EasyOCR 常把横向相邻的两条消息(用户消息+群昵称/UI文字)
|
||||
@@ -4222,8 +4230,82 @@ class MessageMonitor:
|
||||
logger.debug(f"[ocr] 句尾标点拆分: {[r[:60] for r in result]}")
|
||||
return result
|
||||
|
||||
def _norm_frag(self, s: str) -> str:
|
||||
"""bot 回复片段比对用的归一化:去空格、去中英文标点,抵抗 OCR 噪声。"""
|
||||
s = re.sub(r'\s+', '', s)
|
||||
for a in (',', '。', '、', ';', ':', '!', '?', ',', '.', ';', ':', '!', '?',
|
||||
' ', ' ', '~', '~'):
|
||||
s = s.replace(a, '')
|
||||
return s
|
||||
|
||||
def _strip_bot_reply_fragments(self, text: str) -> str:
|
||||
"""从 OCR 识别出的单条消息中剥离 bot 自己的旧回复片段,保留用户真实提问。
|
||||
|
||||
与 _poll 中的「反向包含剥离」逻辑保持一致,但此处用于在 _process_ocr_output
|
||||
的 filter4(回复残片)判定【之前】清理消息,避免「用户新消息 + bot 旧回复」
|
||||
被 OCR 拼成一行后,filter4 因检测到 bot 旧回复文本而把整段(含用户新消息)
|
||||
误判为残片整体丢弃。
|
||||
|
||||
两种策略:
|
||||
1. 精确子串:某条 bot 旧回复 r 完整出现在 text 中 → 直接删除 r
|
||||
2. 反向包含:OCR 把「用户新消息 + bot 旧回复」按逗号拼成一行,且 r 不是
|
||||
text 的子串,但 text 中逗号分隔的片段可能是 r 的子串 → 逐片段判断,
|
||||
剥离属于 bot 回复的片段,保留用户片段。
|
||||
|
||||
⭐ OCR 噪声鲁棒性:bot 旧回复被 OCR 重新识别时,标点(,。)、全/半角、空格
|
||||
常与原文不一致(如原文"Minecraft 生存服务器,拥有完善的领地系统"被识别成
|
||||
"Minecraft生存服务器。拥有完善的领地系统"),导致精确子串匹配失效。
|
||||
比较前对双方做归一化(去空格、去标点),让被 OCR 扭曲的 bot 回复片段也能被识别并剥离。
|
||||
"""
|
||||
if not text or not text.strip():
|
||||
return text
|
||||
replied = self._replied_texts
|
||||
if not replied:
|
||||
return text
|
||||
ts = text
|
||||
if len(ts) > 5:
|
||||
stripped_bot = False
|
||||
# 策略1:精确子串(归一化后比对,抵抗 OCR 标点/空格噪声)
|
||||
for r in replied:
|
||||
if len(r) >= 6 and self._norm_frag(r) and self._norm_frag(r) in self._norm_frag(ts):
|
||||
ts = re.sub(re.escape(r), ' ', ts)
|
||||
stripped_bot = True
|
||||
# 策略2:反向包含剥离——逗号分段后逐段判断是否属于 bot 回复
|
||||
_has_comma = ',' in ts or (',' in ts and re.search(r'[\u4e00-\u9fff],[\u4e00-\u9fff]', ts))
|
||||
if not stripped_bot and _has_comma:
|
||||
parts = re.split(r'[,,]', ts)
|
||||
parts = [p.strip() for p in parts if p.strip()]
|
||||
if len(parts) >= 2:
|
||||
user_parts = []
|
||||
for part in parts:
|
||||
if len(part) < 2:
|
||||
continue
|
||||
is_bot_frag = False
|
||||
npart = self._norm_frag(part)
|
||||
if npart:
|
||||
for r in replied:
|
||||
nr = self._norm_frag(r)
|
||||
if len(nr) >= 2 and npart in nr:
|
||||
is_bot_frag = True
|
||||
break
|
||||
if not is_bot_frag:
|
||||
user_parts.append(part)
|
||||
if len(user_parts) < len(parts):
|
||||
ts = ','.join(user_parts).strip(',').strip()
|
||||
stripped_bot = True
|
||||
if stripped_bot:
|
||||
ts = re.sub(r'\s+', ' ', ts).strip().strip(',').strip()
|
||||
return ts
|
||||
|
||||
def _is_reply_fragment(self, text: str) -> bool:
|
||||
"""检查文本是否为 bot 回复内容的碎片(OCR 截取的 md 残片/跨行碎片)。"""
|
||||
"""检查文本是否为 bot 回复内容的碎片(OCR 截取的 md 残片/跨行碎片)。
|
||||
|
||||
⭐ 判定稳健性(防误杀用户新消息):仅当文本「大部分」是 bot 旧回复时才判为残片,
|
||||
要求公共子串:长度≥15,或(长度≥8 且 占较短文本≥60%)。这样既能拦住
|
||||
「bot 回复回声」(整段≈bot 回复,公共子串占文本大头),又不会因用户消息中恰好
|
||||
含有 bot 回复里的词(如服务器名「草原探险服务器」7字、专有名词「Minecraft」)
|
||||
而被整体误杀——用户问句只与 bot 回复共享一个短语,远不到 60%。
|
||||
"""
|
||||
logger.debug(f"[ocr] 过滤器4: _is_reply_fragment 检查: {text[:80]!r}")
|
||||
if not self._replied_texts:
|
||||
logger.debug(f"[ocr] 过滤器4结果: 无已回复文本,返回False")
|
||||
@@ -4232,23 +4314,25 @@ class MessageMonitor:
|
||||
logger.debug(f"[ocr] 过滤器4结果: 文本太短({len(text)}<4),返回False")
|
||||
return False
|
||||
import difflib
|
||||
# 原始文本匹配(含字母数字)
|
||||
t_norm = self._norm_frag(text)
|
||||
t_cjk = ''.join(c for c in text if '\u4e00' <= c <= '\u9fff')
|
||||
for r in self._replied_texts:
|
||||
if len(r) < 5:
|
||||
continue
|
||||
sm = difflib.SequenceMatcher(None, text, r)
|
||||
if sm.find_longest_match(0, len(text), 0, len(r)).size >= 6:
|
||||
logger.debug(f"[ocr] 过滤器4结果: 原始文本匹配成功,返回True")
|
||||
# Latin/归一化匹配(要求占比较短文本≥60%,或足够长)
|
||||
r_norm = self._norm_frag(r)
|
||||
if t_norm and r_norm:
|
||||
sm = difflib.SequenceMatcher(None, t_norm, r_norm)
|
||||
lm = sm.find_longest_match(0, len(t_norm), 0, len(r_norm)).size
|
||||
if lm >= 15 or (lm >= 8 and lm >= 0.6 * min(len(t_norm), len(r_norm))):
|
||||
logger.debug(f"[ocr] 过滤器4结果: 文本匹配成功,返回True")
|
||||
return True
|
||||
# CJK-only 降阈值
|
||||
cjk_t = ''.join(c for c in text if '\u4e00' <= c <= '\u9fff')
|
||||
if len(cjk_t) >= 5:
|
||||
for r in self._replied_texts:
|
||||
# CJK-only 匹配(同样要求占比较短文本≥60%,避免用户问句含服务器名被误杀)
|
||||
r_cjk = ''.join(c for c in r if '\u4e00' <= c <= '\u9fff')
|
||||
if len(r_cjk) < 5:
|
||||
continue
|
||||
sm = difflib.SequenceMatcher(None, cjk_t, r_cjk)
|
||||
if sm.find_longest_match(0, len(cjk_t), 0, len(r_cjk)).size >= 5:
|
||||
if len(t_cjk) >= 5 and len(r_cjk) >= 5:
|
||||
sm = difflib.SequenceMatcher(None, t_cjk, r_cjk)
|
||||
clm = sm.find_longest_match(0, len(t_cjk), 0, len(r_cjk)).size
|
||||
if clm >= 15 or (clm >= 8 and clm >= 0.6 * min(len(t_cjk), len(r_cjk))):
|
||||
logger.debug(f"[ocr] 过滤器4结果: CJK文本匹配成功,返回True")
|
||||
return True
|
||||
logger.debug(f"[ocr] 过滤器4结果: 未匹配到已回复文本,返回False")
|
||||
|
||||
Reference in New Issue
Block a user