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
## 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
- Promote JWeBot from alpha to beta after the local, staging, privacy, and
+2 -2
View File
@@ -8,7 +8,7 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
并通过带视觉确认的 GUI 发送链路回复。
当前版本:`beta 1.0.0.1016`,更新日期:2026-10-16。
当前版本:`beta 1.0.1.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.0.1016
beta 1.0.1.1016
```
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
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
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;
- 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
`components/debug.toml` generates:
+2 -2
View File
@@ -14,9 +14,9 @@ Rules:
- `X.Y.Z` is the project version.
- `MMDD` is the delivery date.
- 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
`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
milestone approval.
- Local repository migration does not imply a production deployment.
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-config"
version = "1.0.0b1+1016"
version = "1.0.1b1+1016"
description = "Profile-based configuration compiler for JWeBot"
requires-python = ">=3.9"
dependencies = [
@@ -36,4 +36,4 @@ __all__ = [
"write_default_profile",
]
__version__ = "1.0.0b1+1016"
__version__ = "1.0.1b1+1016"
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-extension-host"
version = "1.0.0b1+1016"
version = "1.0.1b1+1016"
description = "Single-release, single-invocation process host for JWeBot extensions"
requires-python = ">=3.9"
dependencies = [
"jwebot-sdk==1.0.0b1+1016",
"jwebot-kernel==1.0.0b1+1016",
"jwebot-sdk==1.0.1b1+1016",
"jwebot-kernel==1.0.1b1+1016",
]
[project.scripts]
@@ -16,4 +16,4 @@ __all__ = [
"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]
name = "jwebot-kernel"
version = "1.0.0b1+1016"
version = "1.0.1b1+1016"
description = "Stable routing and extension authority for JWeBot"
requires-python = ">=3.9"
dependencies = [
"jwebot-config==1.0.0b1+1016",
"jwebot-sdk==1.0.0b1+1016",
"jwebot-official-extensions==1.0.0b1+1016",
"jwebot-config==1.0.1b1+1016",
"jwebot-sdk==1.0.1b1+1016",
"jwebot-official-extensions==1.0.1b1+1016",
"tomli>=2; python_version < '3.11'",
]
@@ -107,4 +107,4 @@ __all__ = [
"verify_installed_release_signature",
]
__version__ = "1.0.0b1+1016"
__version__ = "1.0.1b1+1016"
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-official-extensions"
version = "1.0.0b1+1016"
version = "1.0.1b1+1016"
description = "Reviewed local command adapters and broker implementation for JWeBot"
requires-python = ">=3.9"
dependencies = [
"jwebot-sdk==1.0.0b1+1016",
"jwebot-sdk==1.0.1b1+1016",
]
[tool.setuptools.packages.find]
@@ -90,4 +90,4 @@ __all__ = [
"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]
name = "jwebot-sdk"
version = "1.0.0b1+1016"
version = "1.0.1b1+1016"
description = "Typed, capability-limited extension API for JWeBot plugins"
requires-python = ">=3.9"
dependencies = []
@@ -38,4 +38,4 @@ __all__ = [
"SingleGroupContext",
]
__version__ = "1.0.0b1+1016"
__version__ = "1.0.1b1+1016"
+1 -1
View File
@@ -1,5 +1,5 @@
[tool.jwebot]
release = "beta 1.0.0.1016"
release = "beta 1.0.1.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.0.1016", version["text"])
self.assertIn("最新版本:beta 1.0.1.1016", 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.assertIn("调试埋点", debug["text"])
self.assertEqual(balance["route"], "local")
+51
View File
@@ -4,6 +4,7 @@ import importlib.util
import subprocess
import sys
import tempfile
import types
import unittest
from contextlib import nullcontext
from datetime import datetime, timezone
@@ -517,6 +518,56 @@ class GuiCoordinationTests(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):
engine = MODULE.OcrEngine.__new__(MODULE.OcrEngine)
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
OCR_MESSAGE_CACHE_SIZE = 256
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
FOLLOW_LATEST_SETTLE_SECONDS = 0.20
FOLLOW_LATEST_MOUSE_X = 1432
@@ -224,6 +226,17 @@ def normalize_text(value: str) -> str:
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(
sender_crop: Image.Image,
body_crop: Image.Image,
@@ -1099,7 +1112,18 @@ class OcrEngine:
from rapidocr import RapidOCR
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[
str,
tuple[str, str, float, float, float],