From 99a251c19a4b9567660b9d33f6ea655dd468c648 Mon Sep 17 00:00:00 2001 From: JWeBot Contributors Date: Fri, 16 Oct 2026 11:16:23 +0800 Subject: [PATCH] Limit OCR runtime threads --- CHANGELOG.md | 6 +++ README.md | 4 +- VERSION | 2 +- docs/command-catalog.md | 2 +- docs/configuration-guide.md | 9 ++++ docs/versioning.md | 4 +- packages/jwebot-config/pyproject.toml | 2 +- .../src/jwebot_config/__init__.py | 2 +- packages/jwebot-extension-host/pyproject.toml | 6 +-- .../src/jwebot_extension_host/__init__.py | 2 +- packages/jwebot-kernel/pyproject.toml | 8 +-- .../src/jwebot_kernel/__init__.py | 2 +- .../jwebot-official-extensions/pyproject.toml | 4 +- .../jwebot_official_extensions/__init__.py | 2 +- packages/jwebot-sdk/pyproject.toml | 2 +- .../jwebot-sdk/src/jwebot_sdk/__init__.py | 2 +- pyproject.toml | 2 +- tests/test_jwebot_edge.py | 4 +- tests/test_wechat_local_ocr.py | 51 +++++++++++++++++++ wechat_local_ocr.py | 26 +++++++++- 20 files changed, 116 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f94987..41c6224 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 959467f..40040ec 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/VERSION b/VERSION index 8024b6a..de79c2e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -beta 1.0.0.1016 +beta 1.0.1.1016 diff --git a/docs/command-catalog.md b/docs/command-catalog.md index 852bce9..95b11f7 100644 --- a/docs/command-catalog.md +++ b/docs/command-catalog.md @@ -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 diff --git a/docs/configuration-guide.md b/docs/configuration-guide.md index 6492834..affacd3 100644 --- a/docs/configuration-guide.md +++ b/docs/configuration-guide.md @@ -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: diff --git a/docs/versioning.md b/docs/versioning.md index 05b8836..3be3f0e 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -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. diff --git a/packages/jwebot-config/pyproject.toml b/packages/jwebot-config/pyproject.toml index 63dd3ce..75e3820 100644 --- a/packages/jwebot-config/pyproject.toml +++ b/packages/jwebot-config/pyproject.toml @@ -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 = [ diff --git a/packages/jwebot-config/src/jwebot_config/__init__.py b/packages/jwebot-config/src/jwebot_config/__init__.py index cea55d4..15e6b04 100644 --- a/packages/jwebot-config/src/jwebot_config/__init__.py +++ b/packages/jwebot-config/src/jwebot_config/__init__.py @@ -36,4 +36,4 @@ __all__ = [ "write_default_profile", ] -__version__ = "1.0.0b1+1016" +__version__ = "1.0.1b1+1016" diff --git a/packages/jwebot-extension-host/pyproject.toml b/packages/jwebot-extension-host/pyproject.toml index 58bc83f..6a5e817 100644 --- a/packages/jwebot-extension-host/pyproject.toml +++ b/packages/jwebot-extension-host/pyproject.toml @@ -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] diff --git a/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py b/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py index 4e1cb7a..274207e 100644 --- a/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py +++ b/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py @@ -16,4 +16,4 @@ __all__ = [ "process_request", ] -__version__ = "1.0.0b1+1016" +__version__ = "1.0.1b1+1016" diff --git a/packages/jwebot-kernel/pyproject.toml b/packages/jwebot-kernel/pyproject.toml index c50887c..38aa0a1 100644 --- a/packages/jwebot-kernel/pyproject.toml +++ b/packages/jwebot-kernel/pyproject.toml @@ -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'", ] diff --git a/packages/jwebot-kernel/src/jwebot_kernel/__init__.py b/packages/jwebot-kernel/src/jwebot_kernel/__init__.py index e893cf7..fcdc10f 100644 --- a/packages/jwebot-kernel/src/jwebot_kernel/__init__.py +++ b/packages/jwebot-kernel/src/jwebot_kernel/__init__.py @@ -107,4 +107,4 @@ __all__ = [ "verify_installed_release_signature", ] -__version__ = "1.0.0b1+1016" +__version__ = "1.0.1b1+1016" diff --git a/packages/jwebot-official-extensions/pyproject.toml b/packages/jwebot-official-extensions/pyproject.toml index 8489b77..f70b46f 100644 --- a/packages/jwebot-official-extensions/pyproject.toml +++ b/packages/jwebot-official-extensions/pyproject.toml @@ -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] diff --git a/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py b/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py index d3ce791..93396d9 100644 --- a/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py @@ -90,4 +90,4 @@ __all__ = [ "render_feature_list", ] -__version__ = "1.0.0b1+1016" +__version__ = "1.0.1b1+1016" diff --git a/packages/jwebot-sdk/pyproject.toml b/packages/jwebot-sdk/pyproject.toml index eb18497..842f040 100644 --- a/packages/jwebot-sdk/pyproject.toml +++ b/packages/jwebot-sdk/pyproject.toml @@ -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 = [] diff --git a/packages/jwebot-sdk/src/jwebot_sdk/__init__.py b/packages/jwebot-sdk/src/jwebot_sdk/__init__.py index 72156ab..3fb968c 100644 --- a/packages/jwebot-sdk/src/jwebot_sdk/__init__.py +++ b/packages/jwebot-sdk/src/jwebot_sdk/__init__.py @@ -38,4 +38,4 @@ __all__ = [ "SingleGroupContext", ] -__version__ = "1.0.0b1+1016" +__version__ = "1.0.1b1+1016" diff --git a/pyproject.toml b/pyproject.toml index 3664cd5..dc547e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] diff --git a/tests/test_jwebot_edge.py b/tests/test_jwebot_edge.py index a4972b1..0621576 100644 --- a/tests/test_jwebot_edge.py +++ b/tests/test_jwebot_edge.py @@ -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") diff --git a/tests/test_wechat_local_ocr.py b/tests/test_wechat_local_ocr.py index e9d8e21..eb41b71 100755 --- a/tests/test_wechat_local_ocr.py +++ b/tests/test_wechat_local_ocr.py @@ -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) diff --git a/wechat_local_ocr.py b/wechat_local_ocr.py index be3341d..160af56 100755 --- a/wechat_local_ocr.py +++ b/wechat_local_ocr.py @@ -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],