Cache OCR-negative bubbles
check / local-gate (push) Failing after 42s

This commit is contained in:
JWeBot Contributors
2026-10-16 11:38:03 +08:00
parent 572159f571
commit 22edf6ff15
17 changed files with 79 additions and 27 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## beta 1.0.2.1016 - 2026-10-16
- Cache OCR-negative visible bubbles and reuse stable visual signatures as the
OCR cache key so unreadable or lightly re-rendered bubbles do not trigger
repeated 10+ second RapidOCR passes during active chat windows.
## beta 1.0.1.1016 - 2026-10-16
- Limit RapidOCR ONNXRuntime intra/inter operation threads by default so the
+2 -2
View File
@@ -8,7 +8,7 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
并通过带视觉确认的 GUI 发送链路回复。
当前版本:`beta 1.0.1.1016`,更新日期:2026-10-16。
当前版本:`beta 1.0.2.1016`,更新日期:2026-10-16。
1.0 Beta 收口了当前稳定功能:本地命令插件、人物与上下文索引、
Steam 状态与时长查询、虚拟金币游戏、多人彩票、21 点、CS Major
@@ -263,7 +263,7 @@ visually confirmed GUI bridge.
Current version:
```text
beta 1.0.1.1016
beta 1.0.2.1016
```
Updated: 2026-10-16.
+1 -1
View File
@@ -1 +1 @@
beta 1.0.1.1016
beta 1.0.2.1016
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-config"
version = "1.0.1b1+1016"
version = "1.0.2b1+1016"
description = "Profile-based configuration compiler for JWeBot"
requires-python = ">=3.9"
dependencies = [
@@ -36,4 +36,4 @@ __all__ = [
"write_default_profile",
]
__version__ = "1.0.1b1+1016"
__version__ = "1.0.2b1+1016"
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-extension-host"
version = "1.0.1b1+1016"
version = "1.0.2b1+1016"
description = "Single-release, single-invocation process host for JWeBot extensions"
requires-python = ">=3.9"
dependencies = [
"jwebot-sdk==1.0.1b1+1016",
"jwebot-kernel==1.0.1b1+1016",
"jwebot-sdk==1.0.2b1+1016",
"jwebot-kernel==1.0.2b1+1016",
]
[project.scripts]
@@ -16,4 +16,4 @@ __all__ = [
"process_request",
]
__version__ = "1.0.1b1+1016"
__version__ = "1.0.2b1+1016"
+4 -4
View File
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-kernel"
version = "1.0.1b1+1016"
version = "1.0.2b1+1016"
description = "Stable routing and extension authority for JWeBot"
requires-python = ">=3.9"
dependencies = [
"jwebot-config==1.0.1b1+1016",
"jwebot-sdk==1.0.1b1+1016",
"jwebot-official-extensions==1.0.1b1+1016",
"jwebot-config==1.0.2b1+1016",
"jwebot-sdk==1.0.2b1+1016",
"jwebot-official-extensions==1.0.2b1+1016",
"tomli>=2; python_version < '3.11'",
]
@@ -107,4 +107,4 @@ __all__ = [
"verify_installed_release_signature",
]
__version__ = "1.0.1b1+1016"
__version__ = "1.0.2b1+1016"
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-official-extensions"
version = "1.0.1b1+1016"
version = "1.0.2b1+1016"
description = "Reviewed local command adapters and broker implementation for JWeBot"
requires-python = ">=3.9"
dependencies = [
"jwebot-sdk==1.0.1b1+1016",
"jwebot-sdk==1.0.2b1+1016",
]
[tool.setuptools.packages.find]
@@ -90,4 +90,4 @@ __all__ = [
"render_feature_list",
]
__version__ = "1.0.1b1+1016"
__version__ = "1.0.2b1+1016"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-sdk"
version = "1.0.1b1+1016"
version = "1.0.2b1+1016"
description = "Typed, capability-limited extension API for JWeBot plugins"
requires-python = ">=3.9"
dependencies = []
@@ -38,4 +38,4 @@ __all__ = [
"SingleGroupContext",
]
__version__ = "1.0.1b1+1016"
__version__ = "1.0.2b1+1016"
+1 -1
View File
@@ -1,5 +1,5 @@
[tool.jwebot]
release = "beta 1.0.1.1016"
release = "beta 1.0.2.1016"
production_deployments_enabled_by_default = false
[tool.jwebot.workspace]
+2 -2
View File
@@ -867,9 +867,9 @@ class LedgerAndApplicationTests(unittest.TestCase):
self.assertEqual(version["route"], "local")
self.assertIn("JWeBot 版本", version["text"])
self.assertIn("kernel", version["text"])
self.assertIn("最新版本:beta 1.0.1.1016", version["text"])
self.assertIn("最新版本:beta 1.0.2.1016", version["text"])
self.assertIn("更新时间:2026-10-16", version["text"])
self.assertIn("更新摘要:Limit RapidOCR", version["text"])
self.assertIn("更新摘要:Cache OCR-negative", version["text"])
self.assertEqual(debug["route"], "local")
self.assertIn("调试埋点", debug["text"])
self.assertEqual(balance["route"], "local")
+40
View File
@@ -689,6 +689,46 @@ class OcrCacheTests(unittest.TestCase):
self.assertEqual(engine.last_cache_hits, 0)
self.assertEqual(engine.last_cache_misses, 1)
def test_antialias_drift_reuses_visual_signature_cache(self):
engine = MODULE.OcrEngine.__new__(MODULE.OcrEngine)
bubble = MODULE.Bubble(50, 60, 132, 40, 5000)
first = MODULE.Image.new("RGB", (300, 180), (238, 238, 240))
second = MODULE.Image.new("RGB", (300, 180), (242, 242, 244))
first.putpixel((70, 75), (20, 20, 20))
second.putpixel((70, 75), (35, 35, 35))
with mock.patch.object(
engine,
"_recognize_bubble",
return_value=self.message(60),
) as recognize:
engine.recognize_messages(first, [bubble])
messages, _ = engine.recognize_messages(second, [bubble])
self.assertEqual(recognize.call_count, 1)
self.assertEqual(len(messages), 1)
self.assertEqual(engine.last_cache_hits, 1)
self.assertEqual(engine.last_cache_misses, 0)
def test_unrecognized_bubble_reuses_negative_cache(self):
engine = MODULE.OcrEngine.__new__(MODULE.OcrEngine)
image = MODULE.Image.new("RGB", (300, 180), "white")
bubble = MODULE.Bubble(50, 60, 132, 40, 5000)
with mock.patch.object(
engine,
"_recognize_bubble",
return_value=None,
) as recognize:
first_messages, _ = engine.recognize_messages(image, [bubble])
second_messages, _ = engine.recognize_messages(image, [bubble])
self.assertEqual(first_messages, [])
self.assertEqual(second_messages, [])
self.assertEqual(recognize.call_count, 1)
self.assertEqual(engine.last_cache_hits, 1)
self.assertEqual(engine.last_cache_misses, 0)
def test_periodic_refresh_revalidates_cached_bubbles(self):
engine = MODULE.OcrEngine.__new__(MODULE.OcrEngine)
image = MODULE.Image.new("RGB", (300, 180), "white")
+11 -5
View File
@@ -1126,7 +1126,7 @@ class OcrEngine:
)
self._message_cache: OrderedDict[
str,
tuple[str, str, float, float, float],
tuple[str, str, float, float, float] | None,
] = OrderedDict()
self._scan_count = 0
self.last_cache_hits = 0
@@ -1202,7 +1202,7 @@ class OcrEngine:
def _cache(self) -> OrderedDict[
str,
tuple[str, str, float, float, float],
tuple[str, str, float, float, float] | None,
]:
cache = getattr(self, "_message_cache", None)
if cache is None:
@@ -1282,15 +1282,17 @@ class OcrEngine:
bubble_crop = image.crop(
(bubble.x, bubble.y, bubble.right, bubble.bottom)
)
key = self._cache_key(sender_crop, bubble_crop)
sequence_signature = bubble_sequence_signature(
sender_crop,
bubble_crop,
)
cached = cache.get(key)
if cached is not None:
key = sequence_signature
if key in cache:
cached = cache[key]
hits += 1
cache.move_to_end(key)
if cached is None:
continue
messages.append(
IncomingMessage(
sender=cached[0],
@@ -1311,6 +1313,10 @@ class OcrEngine:
visual_signature=sequence_signature,
)
if message is None:
cache[key] = None
cache.move_to_end(key)
while len(cache) > OCR_MESSAGE_CACHE_SIZE:
cache.popitem(last=False)
continue
cache[key] = (
message.sender,