Limit OCR runtime threads
check / local-gate (push) Failing after 44s

This commit is contained in:
JWeBot Contributors
2026-10-16 11:16:23 +08:00
parent 3fa04755d0
commit 99a251c19a
20 changed files with 116 additions and 26 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog # Changelog
## beta 1.0.1.1016 - 2026-10-16
- Limit RapidOCR ONNXRuntime intra/inter operation threads by default so the
adaptive OCR recorder no longer saturates CPU after restart and slow recovery
windows do not stretch local command detection into 10+ second scans.
## beta 1.0.0.1016 - 2026-10-16 ## beta 1.0.0.1016 - 2026-10-16
- Promote JWeBot from alpha to beta after the local, staging, privacy, and - Promote JWeBot from alpha to beta after the local, staging, privacy, and
+2 -2
View File
@@ -8,7 +8,7 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件, 观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
并通过带视觉确认的 GUI 发送链路回复。 并通过带视觉确认的 GUI 发送链路回复。
当前版本:`beta 1.0.0.1016`,更新日期:2026-10-16。 当前版本:`beta 1.0.1.1016`,更新日期:2026-10-16。
1.0 Beta 收口了当前稳定功能:本地命令插件、人物与上下文索引、 1.0 Beta 收口了当前稳定功能:本地命令插件、人物与上下文索引、
Steam 状态与时长查询、虚拟金币游戏、多人彩票、21 点、CS Major Steam 状态与时长查询、虚拟金币游戏、多人彩票、21 点、CS Major
@@ -263,7 +263,7 @@ visually confirmed GUI bridge.
Current version: Current version:
```text ```text
beta 1.0.0.1016 beta 1.0.1.1016
``` ```
Updated: 2026-10-16. Updated: 2026-10-16.
+1 -1
View File
@@ -1 +1 @@
beta 1.0.0.1016 beta 1.0.1.1016
+1 -1
View File
@@ -1,6 +1,6 @@
# JWeBot Command Catalog # JWeBot Command Catalog
Current baseline: `beta 1.0.0.1016`. Current baseline: `beta 1.0.1.1016`.
This catalog records the reviewed commands shipped with the built-in local This catalog records the reviewed commands shipped with the built-in local
extensions. Runtime ownership is frozen at startup from the generated extensions. Runtime ownership is frozen at startup from the generated
+9
View File
@@ -90,6 +90,15 @@ Steam uses `JWEBOT_EDGE_STEAM_ID64` and
- Host timeout and fallback; - Host timeout and fallback;
- GUI sender timeout. - GUI sender timeout.
The OCR runtime also accepts optional environment overrides for constrained
hosts:
- `WECHAT_OCR_ONNX_INTRA_OP_THREADS` defaults to `1`;
- `WECHAT_OCR_ONNX_INTER_OP_THREADS` defaults to `1`.
These values limit RapidOCR's ONNXRuntime CPU thread pools so service restarts
do not temporarily saturate the host while the OCR cache is cold.
### Debug ### Debug
`components/debug.toml` generates: `components/debug.toml` generates:
+2 -2
View File
@@ -14,9 +14,9 @@ Rules:
- `X.Y.Z` is the project version. - `X.Y.Z` is the project version.
- `MMDD` is the delivery date. - `MMDD` is the delivery date.
- Python package versions use PEP 440 pre-release form. For beta builds, - Python package versions use PEP 440 pre-release form. For beta builds,
`beta 1.0.0.1016` maps to `1.0.0b1+1016`. `beta 1.0.1.1016` maps to `1.0.1b1+1016`.
- Normal small updates increment only the patch version, for example - Normal small updates increment only the patch version, for example
`beta 1.0.0.1016` to `beta 1.0.1.1016`. `beta 1.0.1.1016` to `beta 1.0.2.1016`.
- Minor, major, or stable-release changes require explicit user review and - Minor, major, or stable-release changes require explicit user review and
milestone approval. milestone approval.
- Local repository migration does not imply a production deployment. - Local repository migration does not imply a production deployment.
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "jwebot-config" name = "jwebot-config"
version = "1.0.0b1+1016" version = "1.0.1b1+1016"
description = "Profile-based configuration compiler for JWeBot" description = "Profile-based configuration compiler for JWeBot"
requires-python = ">=3.9" requires-python = ">=3.9"
dependencies = [ dependencies = [
@@ -36,4 +36,4 @@ __all__ = [
"write_default_profile", "write_default_profile",
] ]
__version__ = "1.0.0b1+1016" __version__ = "1.0.1b1+1016"
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "jwebot-extension-host" name = "jwebot-extension-host"
version = "1.0.0b1+1016" version = "1.0.1b1+1016"
description = "Single-release, single-invocation process host for JWeBot extensions" description = "Single-release, single-invocation process host for JWeBot extensions"
requires-python = ">=3.9" requires-python = ">=3.9"
dependencies = [ dependencies = [
"jwebot-sdk==1.0.0b1+1016", "jwebot-sdk==1.0.1b1+1016",
"jwebot-kernel==1.0.0b1+1016", "jwebot-kernel==1.0.1b1+1016",
] ]
[project.scripts] [project.scripts]
@@ -16,4 +16,4 @@ __all__ = [
"process_request", "process_request",
] ]
__version__ = "1.0.0b1+1016" __version__ = "1.0.1b1+1016"
+4 -4
View File
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "jwebot-kernel" name = "jwebot-kernel"
version = "1.0.0b1+1016" version = "1.0.1b1+1016"
description = "Stable routing and extension authority for JWeBot" description = "Stable routing and extension authority for JWeBot"
requires-python = ">=3.9" requires-python = ">=3.9"
dependencies = [ dependencies = [
"jwebot-config==1.0.0b1+1016", "jwebot-config==1.0.1b1+1016",
"jwebot-sdk==1.0.0b1+1016", "jwebot-sdk==1.0.1b1+1016",
"jwebot-official-extensions==1.0.0b1+1016", "jwebot-official-extensions==1.0.1b1+1016",
"tomli>=2; python_version < '3.11'", "tomli>=2; python_version < '3.11'",
] ]
@@ -107,4 +107,4 @@ __all__ = [
"verify_installed_release_signature", "verify_installed_release_signature",
] ]
__version__ = "1.0.0b1+1016" __version__ = "1.0.1b1+1016"
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "jwebot-official-extensions" name = "jwebot-official-extensions"
version = "1.0.0b1+1016" version = "1.0.1b1+1016"
description = "Reviewed local command adapters and broker implementation for JWeBot" description = "Reviewed local command adapters and broker implementation for JWeBot"
requires-python = ">=3.9" requires-python = ">=3.9"
dependencies = [ dependencies = [
"jwebot-sdk==1.0.0b1+1016", "jwebot-sdk==1.0.1b1+1016",
] ]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
@@ -90,4 +90,4 @@ __all__ = [
"render_feature_list", "render_feature_list",
] ]
__version__ = "1.0.0b1+1016" __version__ = "1.0.1b1+1016"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "jwebot-sdk" name = "jwebot-sdk"
version = "1.0.0b1+1016" version = "1.0.1b1+1016"
description = "Typed, capability-limited extension API for JWeBot plugins" description = "Typed, capability-limited extension API for JWeBot plugins"
requires-python = ">=3.9" requires-python = ">=3.9"
dependencies = [] dependencies = []
@@ -38,4 +38,4 @@ __all__ = [
"SingleGroupContext", "SingleGroupContext",
] ]
__version__ = "1.0.0b1+1016" __version__ = "1.0.1b1+1016"
+1 -1
View File
@@ -1,5 +1,5 @@
[tool.jwebot] [tool.jwebot]
release = "beta 1.0.0.1016" release = "beta 1.0.1.1016"
production_deployments_enabled_by_default = false production_deployments_enabled_by_default = false
[tool.jwebot.workspace] [tool.jwebot.workspace]
+2 -2
View File
@@ -867,9 +867,9 @@ class LedgerAndApplicationTests(unittest.TestCase):
self.assertEqual(version["route"], "local") self.assertEqual(version["route"], "local")
self.assertIn("JWeBot 版本", version["text"]) self.assertIn("JWeBot 版本", version["text"])
self.assertIn("kernel", version["text"]) self.assertIn("kernel", version["text"])
self.assertIn("最新版本:beta 1.0.0.1016", version["text"]) self.assertIn("最新版本:beta 1.0.1.1016", version["text"])
self.assertIn("更新时间:2026-10-16", version["text"]) self.assertIn("更新时间:2026-10-16", version["text"])
self.assertIn("更新摘要:Promote JWeBot", version["text"]) self.assertIn("更新摘要:Limit RapidOCR", version["text"])
self.assertEqual(debug["route"], "local") self.assertEqual(debug["route"], "local")
self.assertIn("调试埋点", debug["text"]) self.assertIn("调试埋点", debug["text"])
self.assertEqual(balance["route"], "local") self.assertEqual(balance["route"], "local")
+51
View File
@@ -4,6 +4,7 @@ import importlib.util
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import types
import unittest import unittest
from contextlib import nullcontext from contextlib import nullcontext
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -517,6 +518,56 @@ class GuiCoordinationTests(unittest.TestCase):
class OcrFallbackTests(unittest.TestCase): class OcrFallbackTests(unittest.TestCase):
def test_ocr_engine_limits_onnxruntime_threads_by_default(self):
captured = {}
class FakeRapidOCR:
def __init__(self, **kwargs):
captured.update(kwargs)
fake_module = types.SimpleNamespace(RapidOCR=FakeRapidOCR)
with mock.patch.dict(sys.modules, {"rapidocr": fake_module}), mock.patch.dict(
MODULE.os.environ,
{
"WECHAT_OCR_ONNX_INTRA_OP_THREADS": "",
"WECHAT_OCR_ONNX_INTER_OP_THREADS": "",
},
):
MODULE.OcrEngine()
self.assertEqual(
captured["params"],
{
"EngineConfig.onnxruntime.intra_op_num_threads": 1,
"EngineConfig.onnxruntime.inter_op_num_threads": 1,
},
)
def test_ocr_engine_allows_thread_limit_overrides(self):
captured = {}
class FakeRapidOCR:
def __init__(self, **kwargs):
captured.update(kwargs)
fake_module = types.SimpleNamespace(RapidOCR=FakeRapidOCR)
with mock.patch.dict(sys.modules, {"rapidocr": fake_module}), mock.patch.dict(
MODULE.os.environ,
{
"WECHAT_OCR_ONNX_INTRA_OP_THREADS": "2",
"WECHAT_OCR_ONNX_INTER_OP_THREADS": "3",
},
):
MODULE.OcrEngine()
self.assertEqual(
captured["params"],
{
"EngineConfig.onnxruntime.intra_op_num_threads": 2,
"EngineConfig.onnxruntime.inter_op_num_threads": 3,
},
)
def test_low_confidence_line_uses_detection_fallback(self): def test_low_confidence_line_uses_detection_fallback(self):
engine = MODULE.OcrEngine.__new__(MODULE.OcrEngine) engine = MODULE.OcrEngine.__new__(MODULE.OcrEngine)
bubble = MODULE.Bubble(50, 50, 132, 68, 5000) bubble = MODULE.Bubble(50, 50, 132, 68, 5000)
+25 -1
View File
@@ -67,6 +67,8 @@ DAMAGE_HEARTBEAT_SECONDS = 2.0
CAPTURE_BUFFER_FRAMES = 12 CAPTURE_BUFFER_FRAMES = 12
OCR_MESSAGE_CACHE_SIZE = 256 OCR_MESSAGE_CACHE_SIZE = 256
OCR_CACHE_REFRESH_SCANS = 120 OCR_CACHE_REFRESH_SCANS = 120
DEFAULT_OCR_ONNX_INTRA_OP_THREADS = 1
DEFAULT_OCR_ONNX_INTER_OP_THREADS = 1
WINDOW_LAYOUT_SETTLE_SECONDS = 0.25 WINDOW_LAYOUT_SETTLE_SECONDS = 0.25
FOLLOW_LATEST_SETTLE_SECONDS = 0.20 FOLLOW_LATEST_SETTLE_SECONDS = 0.20
FOLLOW_LATEST_MOUSE_X = 1432 FOLLOW_LATEST_MOUSE_X = 1432
@@ -224,6 +226,17 @@ def normalize_text(value: str) -> str:
return re.sub(r"\s+", " ", value).strip() return re.sub(r"\s+", " ", value).strip()
def env_int(name: str, default: int) -> int:
raw = os.environ.get(name, "").strip()
if not raw:
return default
try:
return int(raw)
except ValueError:
logging.warning("invalid integer environment value name=%s", name)
return default
def bubble_sequence_signature( def bubble_sequence_signature(
sender_crop: Image.Image, sender_crop: Image.Image,
body_crop: Image.Image, body_crop: Image.Image,
@@ -1099,7 +1112,18 @@ class OcrEngine:
from rapidocr import RapidOCR from rapidocr import RapidOCR
self._numpy = np self._numpy = np
self._engine = RapidOCR() self._engine = RapidOCR(
params={
"EngineConfig.onnxruntime.intra_op_num_threads": env_int(
"WECHAT_OCR_ONNX_INTRA_OP_THREADS",
DEFAULT_OCR_ONNX_INTRA_OP_THREADS,
),
"EngineConfig.onnxruntime.inter_op_num_threads": env_int(
"WECHAT_OCR_ONNX_INTER_OP_THREADS",
DEFAULT_OCR_ONNX_INTER_OP_THREADS,
),
}
)
self._message_cache: OrderedDict[ self._message_cache: OrderedDict[
str, str,
tuple[str, str, float, float, float], tuple[str, str, float, float, float],