Skip to content

Commit 012b83e

Browse files
MomiJiSanHongzhi Wenclaude
authored
fix(galgame): 让 RapidOCR 模型配置实际生效 (#1194)
* 修复 Galgame 插件中 RapidOCR 的模型配置未生效问题。 本次更改为 RapidOCR 运行时补充模型路径解析逻辑,按用户模型缓存优先、包内 models 目录兜底的顺序查找 det/cls/rec ONNX 模型,并在构造 RapidOCR 实例时传入解析后的模型路径,使 lang_type、ocr_version、 model_type 配置能够实际影响模型加载。同时将默认 OCR 版本从 PP-OCRv5 调整为与当前内置模型一致的 PP-OCRv4,并更新手动 OCR pipeline 验证脚本以复用统一默认值。 新增单元测试覆盖模型路径解析、用户缓存优先级、缺失模型回退以及从实际导入包定位 models 目录等场景,防止配置再次失效。 * fix(galgame): honor rapidocr_model_type when resolving ONNX filenames - _resolve_rapidocr_model_paths 真正使用 model_type,server 变体走 PaddleOCR 的 `_server_infer` 命名(cls 跨变体共享 mobile) - _build_runtime_constructor_kwargs 把 model_search_dirs 列表换成具名 package_models_dir 参数,去掉魔术索引和冗余的 per-path 判空 - 新增 server 变体回归测试,更新现有测试切到新参数名 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(galgame): correct rapidocr server filename infix + harden empty-module-file fallback - 修正 server 变体文件名:infix 应在 `_det`/`_rec` 之后(PaddleOCR 实际命名是 `ch_PP-OCRv4_det_server_infer.onnx`,参见 SWHL/RapidOCR HuggingFace repo), 上一版误把 infix 放到了 lang/version 后面 - module_file 缺失时 package_models_dir 从 Path()(= CWD)改为 None, 避免 _resolve_rapidocr_model_paths 误扫工作目录 - 同步修正 server 变体回归测试的文件名 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Hongzhi Wen <cartabio.coder1@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9ae00e8 commit 012b83e

5 files changed

Lines changed: 245 additions & 4 deletions

File tree

plugin/plugins/galgame_plugin/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ class GalgameRapidOcrConfig:
586586
rapidocr_engine_type: str = "onnxruntime"
587587
rapidocr_lang_type: str = "ch"
588588
rapidocr_model_type: str = "mobile"
589-
rapidocr_ocr_version: str = "PP-OCRv5"
589+
rapidocr_ocr_version: str = "PP-OCRv4"
590590

591591

592592
@dataclass(slots=True, init=False)

plugin/plugins/galgame_plugin/plugin.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ install_timeout_seconds = 180
118118
engine_type = "onnxruntime"
119119
lang_type = "ch"
120120
model_type = "mobile"
121-
ocr_version = "PP-OCRv5"
121+
ocr_version = "PP-OCRv4"
122122

123123
[plugin.i18n]
124124
default_locale = "zh-CN"

plugin/plugins/galgame_plugin/rapidocr_support.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
DEFAULT_RAPIDOCR_ENGINE_TYPE = "onnxruntime"
2121
DEFAULT_RAPIDOCR_LANG_TYPE = "ch"
2222
DEFAULT_RAPIDOCR_MODEL_TYPE = "mobile"
23-
DEFAULT_RAPIDOCR_OCR_VERSION = "PP-OCRv5"
23+
DEFAULT_RAPIDOCR_OCR_VERSION = "PP-OCRv4"
2424
_INSTALL_STATE_NAME = "install_state.json"
2525
# Leave one core free for the OS / interactive use; floor at 2 so 1-2 core hosts still parallelise.
2626
_RAPIDOCR_INFERENCE_THREAD_LIMIT = max(2, (os.cpu_count() or 2) - 1)
@@ -99,6 +99,50 @@ def rapidocr_selected_model_name(
9999
)
100100

101101

102+
def _resolve_rapidocr_model_paths(
103+
*,
104+
model_cache_dir: Path,
105+
package_models_dir: Path | None,
106+
lang_type: str,
107+
ocr_version: str,
108+
model_type: str,
109+
) -> tuple[str | None, str | None, str | None]:
110+
lang = str(lang_type or DEFAULT_RAPIDOCR_LANG_TYPE).strip() or DEFAULT_RAPIDOCR_LANG_TYPE
111+
version = str(ocr_version or DEFAULT_RAPIDOCR_OCR_VERSION).strip() or DEFAULT_RAPIDOCR_OCR_VERSION
112+
# RapidOCR / PaddleOCR file naming (see SWHL/RapidOCR on HuggingFace):
113+
# mobile: ch_PP-OCRv4_det_infer.onnx
114+
# server: ch_PP-OCRv4_det_server_infer.onnx (i.e. "_server" sits between "_det" and "_infer")
115+
# Anything other than "server" falls back to mobile silently — invalid values
116+
# would otherwise miss every candidate file and the runtime default mobile model
117+
# would still load via RapidOCR's bundled config.yaml.
118+
mt = (str(model_type or DEFAULT_RAPIDOCR_MODEL_TYPE).strip() or DEFAULT_RAPIDOCR_MODEL_TYPE).lower()
119+
server_infix = "_server" if mt == "server" else ""
120+
det_name = f"{lang}_{version}_det{server_infix}_infer.onnx"
121+
rec_name = f"{lang}_{version}_rec{server_infix}_infer.onnx"
122+
# cls is shared across mobile/server variants in PaddleOCR's released model zoo.
123+
cls_name = "ch_ppocr_mobile_v2.0_cls_infer.onnx"
124+
125+
det_path: str | None = None
126+
cls_path: str | None = None
127+
rec_path: str | None = None
128+
for search_dir in (model_cache_dir, package_models_dir):
129+
if not search_dir or not search_dir.is_dir():
130+
continue
131+
if det_path is None:
132+
candidate = search_dir / det_name
133+
if candidate.is_file():
134+
det_path = str(candidate)
135+
if cls_path is None:
136+
candidate = search_dir / cls_name
137+
if candidate.is_file():
138+
cls_path = str(candidate)
139+
if rec_path is None:
140+
candidate = search_dir / rec_name
141+
if candidate.is_file():
142+
rec_path = str(candidate)
143+
return det_path, cls_path, rec_path
144+
145+
102146
@contextmanager
103147
def _rapidocr_import_context(
104148
*,
@@ -177,11 +221,35 @@ def _build_runtime_constructor_kwargs(
177221
model_type: str,
178222
ocr_version: str,
179223
model_cache_dir: Path,
224+
package_models_dir: Path | None = None,
180225
) -> dict[str, Any]:
181226
try:
182227
parameters = inspect.signature(runtime_class).parameters
183228
except (TypeError, ValueError):
184229
return {}
230+
231+
has_var_kwargs = any(
232+
parameter.kind == inspect.Parameter.VAR_KEYWORD
233+
for parameter in parameters.values()
234+
)
235+
if has_var_kwargs:
236+
det_path, cls_path, rec_path = _resolve_rapidocr_model_paths(
237+
model_cache_dir=model_cache_dir,
238+
package_models_dir=package_models_dir,
239+
lang_type=lang_type,
240+
ocr_version=ocr_version,
241+
model_type=model_type,
242+
)
243+
kwargs: dict[str, Any] = {}
244+
if det_path and rec_path:
245+
kwargs["det_model_path"] = det_path
246+
kwargs["rec_model_path"] = rec_path
247+
if cls_path:
248+
kwargs["cls_model_path"] = cls_path
249+
if engine_type:
250+
kwargs["engine_type"] = engine_type
251+
return kwargs
252+
185253
kwargs: dict[str, Any] = {}
186254
direct_values = {
187255
"engine_type": engine_type,
@@ -273,6 +341,13 @@ def load_rapidocr_runtime(
273341
runtime_class = getattr(module, "RapidOCR", None)
274342
if runtime_class is None:
275343
raise RuntimeError("RapidOCR runtime class not found")
344+
module_file = getattr(module, "__file__", "") or ""
345+
# Sentinel must be None (not Path()) — Path() resolves to CWD and would
346+
# let _resolve_rapidocr_model_paths inadvertently scan the working
347+
# directory if `__file__` were ever missing.
348+
package_models_dir: Path | None = (
349+
Path(module_file).resolve().parent / "models" if module_file else None
350+
)
276351
with _onnxruntime_intra_op_thread_cap(_RAPIDOCR_INFERENCE_THREAD_LIMIT):
277352
runtime = runtime_class(
278353
**_build_runtime_constructor_kwargs(
@@ -282,6 +357,7 @@ def load_rapidocr_runtime(
282357
model_type=model_type,
283358
ocr_version=ocr_version,
284359
model_cache_dir=model_cache_dir,
360+
package_models_dir=package_models_dir,
285361
)
286362
)
287363
metadata = {

plugin/plugins/galgame_plugin/test_ocr_pipeline.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
TesseractOcrBackend,
3333
_default_window_scanner,
3434
)
35+
from plugin.plugins.galgame_plugin.rapidocr_support import DEFAULT_RAPIDOCR_OCR_VERSION
3536

3637

3738
def _noop_logger():
@@ -159,7 +160,7 @@ async def main() -> None:
159160
rapidocr_engine_type="onnxruntime",
160161
rapidocr_lang_type="ch",
161162
rapidocr_model_type="mobile",
162-
rapidocr_ocr_version="PP-OCRv5",
163+
rapidocr_ocr_version=DEFAULT_RAPIDOCR_OCR_VERSION,
163164
)
164165
mgr = OcrReaderManager(logger=_noop_logger(), config=config)
165166
tick = await mgr.tick(bridge_sdk_available=False, memory_reader_runtime={})
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
from __future__ import annotations
2+
3+
from contextlib import nullcontext
4+
from pathlib import Path
5+
from types import SimpleNamespace
6+
7+
import pytest
8+
9+
from plugin.plugins.galgame_plugin import rapidocr_support
10+
11+
12+
pytestmark = pytest.mark.plugin_unit
13+
14+
15+
class _RapidOcrWithKwargs:
16+
captured_kwargs: dict[str, object] | None = None
17+
18+
def __init__(self, config_path=None, **kwargs) -> None:
19+
del config_path
20+
type(self).captured_kwargs = dict(kwargs)
21+
22+
23+
def _touch(path: Path) -> Path:
24+
path.parent.mkdir(parents=True, exist_ok=True)
25+
path.write_text("", encoding="utf-8")
26+
return path
27+
28+
29+
def test_rapidocr_kwargs_resolve_configured_model_paths(tmp_path: Path) -> None:
30+
model_cache_dir = tmp_path / "RapidOCR" / "models"
31+
package_models_dir = tmp_path / "package" / "models"
32+
det_path = _touch(package_models_dir / "ch_PP-OCRv4_det_infer.onnx")
33+
cls_path = _touch(package_models_dir / "ch_ppocr_mobile_v2.0_cls_infer.onnx")
34+
rec_path = _touch(package_models_dir / "ch_PP-OCRv4_rec_infer.onnx")
35+
36+
kwargs = rapidocr_support._build_runtime_constructor_kwargs(
37+
_RapidOcrWithKwargs,
38+
engine_type="onnxruntime",
39+
lang_type="ch",
40+
model_type="mobile",
41+
ocr_version="PP-OCRv4",
42+
model_cache_dir=model_cache_dir,
43+
package_models_dir=package_models_dir,
44+
)
45+
46+
assert kwargs == {
47+
"det_model_path": str(det_path),
48+
"cls_model_path": str(cls_path),
49+
"rec_model_path": str(rec_path),
50+
"engine_type": "onnxruntime",
51+
}
52+
53+
54+
def test_rapidocr_kwargs_prefers_user_model_cache(tmp_path: Path) -> None:
55+
model_cache_dir = tmp_path / "RapidOCR" / "models"
56+
package_models_dir = tmp_path / "package" / "models"
57+
user_det_path = _touch(model_cache_dir / "japan_PP-OCRv4_det_infer.onnx")
58+
user_rec_path = _touch(model_cache_dir / "japan_PP-OCRv4_rec_infer.onnx")
59+
package_cls_path = _touch(package_models_dir / "ch_ppocr_mobile_v2.0_cls_infer.onnx")
60+
_touch(package_models_dir / "japan_PP-OCRv4_det_infer.onnx")
61+
_touch(package_models_dir / "japan_PP-OCRv4_rec_infer.onnx")
62+
63+
kwargs = rapidocr_support._build_runtime_constructor_kwargs(
64+
_RapidOcrWithKwargs,
65+
engine_type="onnxruntime",
66+
lang_type="japan",
67+
model_type="mobile",
68+
ocr_version="PP-OCRv4",
69+
model_cache_dir=model_cache_dir,
70+
package_models_dir=package_models_dir,
71+
)
72+
73+
assert kwargs["det_model_path"] == str(user_det_path)
74+
assert kwargs["rec_model_path"] == str(user_rec_path)
75+
assert kwargs["cls_model_path"] == str(package_cls_path)
76+
77+
78+
def test_rapidocr_kwargs_resolves_server_variant_filenames(tmp_path: Path) -> None:
79+
model_cache_dir = tmp_path / "RapidOCR" / "models"
80+
package_models_dir = tmp_path / "package" / "models"
81+
server_det_path = _touch(model_cache_dir / "ch_PP-OCRv4_det_server_infer.onnx")
82+
server_rec_path = _touch(model_cache_dir / "ch_PP-OCRv4_rec_server_infer.onnx")
83+
cls_path = _touch(package_models_dir / "ch_ppocr_mobile_v2.0_cls_infer.onnx")
84+
# Mobile variants exist alongside server ones to ensure model_type drives selection.
85+
_touch(package_models_dir / "ch_PP-OCRv4_det_infer.onnx")
86+
_touch(package_models_dir / "ch_PP-OCRv4_rec_infer.onnx")
87+
88+
kwargs = rapidocr_support._build_runtime_constructor_kwargs(
89+
_RapidOcrWithKwargs,
90+
engine_type="onnxruntime",
91+
lang_type="ch",
92+
model_type="server",
93+
ocr_version="PP-OCRv4",
94+
model_cache_dir=model_cache_dir,
95+
package_models_dir=package_models_dir,
96+
)
97+
98+
assert kwargs == {
99+
"det_model_path": str(server_det_path),
100+
"rec_model_path": str(server_rec_path),
101+
"cls_model_path": str(cls_path),
102+
"engine_type": "onnxruntime",
103+
}
104+
105+
106+
def test_rapidocr_kwargs_omits_model_paths_when_configured_model_is_missing(tmp_path: Path) -> None:
107+
model_cache_dir = tmp_path / "RapidOCR" / "models"
108+
package_models_dir = tmp_path / "package" / "models"
109+
_touch(package_models_dir / "ch_PP-OCRv4_det_infer.onnx")
110+
_touch(package_models_dir / "ch_ppocr_mobile_v2.0_cls_infer.onnx")
111+
_touch(package_models_dir / "ch_PP-OCRv4_rec_infer.onnx")
112+
113+
kwargs = rapidocr_support._build_runtime_constructor_kwargs(
114+
_RapidOcrWithKwargs,
115+
engine_type="onnxruntime",
116+
lang_type="ch",
117+
model_type="mobile",
118+
ocr_version="PP-OCRv5",
119+
model_cache_dir=model_cache_dir,
120+
package_models_dir=package_models_dir,
121+
)
122+
123+
assert kwargs == {"engine_type": "onnxruntime"}
124+
125+
126+
def test_load_rapidocr_runtime_uses_imported_package_models_dir(
127+
tmp_path: Path,
128+
monkeypatch: pytest.MonkeyPatch,
129+
) -> None:
130+
install_target = tmp_path / "RapidOCR"
131+
bundled_package_dir = tmp_path / "bundled" / "rapidocr_onnxruntime"
132+
_touch(bundled_package_dir / "__init__.py")
133+
det_path = _touch(bundled_package_dir / "models" / "ch_PP-OCRv4_det_infer.onnx")
134+
cls_path = _touch(bundled_package_dir / "models" / "ch_ppocr_mobile_v2.0_cls_infer.onnx")
135+
rec_path = _touch(bundled_package_dir / "models" / "ch_PP-OCRv4_rec_infer.onnx")
136+
_RapidOcrWithKwargs.captured_kwargs = None
137+
138+
monkeypatch.setattr(
139+
rapidocr_support.importlib,
140+
"import_module",
141+
lambda name: SimpleNamespace(
142+
RapidOCR=_RapidOcrWithKwargs,
143+
__file__=str(bundled_package_dir / "__init__.py"),
144+
),
145+
)
146+
monkeypatch.setattr(rapidocr_support, "_onnxruntime_intra_op_thread_cap", lambda _limit: nullcontext())
147+
148+
runtime, metadata = rapidocr_support.load_rapidocr_runtime(
149+
install_target_dir_raw=str(install_target),
150+
engine_type="onnxruntime",
151+
lang_type="ch",
152+
model_type="mobile",
153+
ocr_version="PP-OCRv4",
154+
)
155+
156+
assert isinstance(runtime, _RapidOcrWithKwargs)
157+
assert _RapidOcrWithKwargs.captured_kwargs == {
158+
"det_model_path": str(det_path),
159+
"cls_model_path": str(cls_path),
160+
"rec_model_path": str(rec_path),
161+
"engine_type": "onnxruntime",
162+
}
163+
assert metadata["detected_path"] == str(bundled_package_dir.resolve())
164+
assert metadata["selected_model"] == "PP-OCRv4/ch/mobile"

0 commit comments

Comments
 (0)