Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/develop/one_dragon/initialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@
3. 运行应用前等待步骤2初始化完成,或超时退出。
4. 应用运行。

### OCR 模型选择(v5 → v6 平滑迁移)

`ZContext.init_ocr` 在正常模式(非调试)下会忽略配置里写的是哪个模型,按本地模型文件状态向 v6 收敛:

| 本地模型状态 | 本次启动行为 |
|---|---|
| v6 文件齐全 | 直接用 v6,配置落盘为 v6 |
| 有 v5、无 v6 | 用 v5 顶住(立即可用),后台下载 v6;下载成功后只落盘配置为 v6,**不立刻切换**,下次启动生效 |
| v5、v6 都没有 | 直接用 v6 并触发下载 |

要点:

- 配置改写只发生在 v6 文件就绪之后;v6 下载失败时 v5 继续可用,不会出现无 OCR 可用的情况。
- 后台下载只落盘配置、不立刻重建 OCR 引擎:立刻切换需要先清掉当前 v5 实例再初始化 v6,若 v6 初始化失败,当前可用的 v5 也丢了。改为下次启动生效后,本次会话始终稳定。
- 调试模式(`is_debug`)下不做任何自动选择,直接用配置里的模型,便于手动对比 v5 / v6。

## 特定应用需要

1. 运行应用。
Expand Down
86 changes: 84 additions & 2 deletions src/one_dragon/base/operation/one_dragon_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@
from one_dragon.base.controller.pc_button.pc_button_listener import PcButtonListener
from one_dragon.base.matcher.ocr.ocr_matcher import OcrMatcher
from one_dragon.base.matcher.ocr.ocr_service import OcrService
from one_dragon.base.matcher.ocr.onnx_ocr_matcher import OnnxOcrMatcher, OnnxOcrParam
from one_dragon.base.matcher.ocr.onnx_ocr_matcher import (
DEFAULT_OCR_MODEL_NAME,
PPOCRV6_MODEL_NAME,
OnnxOcrMatcher,
OnnxOcrParam,
get_final_file_list,
)
from one_dragon.base.matcher.template_matcher import TemplateMatcher
from one_dragon.base.operation.application.application_factory_manager import (
ApplicationFactoryManager,
Expand Down Expand Up @@ -76,6 +82,7 @@ def __init__(self):
)
self.ocr.overlay_debug_bus = self.overlay_debug_bus
self.ocr_service: OcrService = OcrService(ocr_matcher=self.ocr)
self._ocr_v6_downloading: bool = False # 后台下载 V6 是否进行中 防止重复启动
self.controller: ControllerBase | None = None

self.keyboard_controller = keyboard.Controller()
Expand Down Expand Up @@ -484,14 +491,16 @@ def init_ocr(self) -> None:
初始化OCR
:return:
"""
ocr_model_name = self._decide_ocr_model_name()

# 清理旧实例资源
if hasattr(self, 'ocr') and self.ocr is not None:
if hasattr(self.ocr, 'cleanup'):
self.ocr.cleanup()

self.ocr = OnnxOcrMatcher(
OnnxOcrParam(
ocr_model_name=self.model_config.ocr,
ocr_model_name=ocr_model_name,
use_gpu=self.model_config.ocr_use_gpu,
det_limit_side_len=max(self.project_config.screen_standard_width, self.project_config.screen_standard_height),
)
Expand All @@ -505,6 +514,79 @@ def init_ocr(self) -> None:
proxy_url=self.env_config.personal_proxy if self.env_config.is_personal_proxy else None,
)

# 正常模式下 向 V6 收敛
if not self.env_config.is_debug:
if self._is_ocr_model_ready(PPOCRV6_MODEL_NAME):
# V6 已就绪(启动就有 或 刚同步下载完成) 落盘配置
self.model_config.ocr = PPOCRV6_MODEL_NAME
elif ocr_model_name == DEFAULT_OCR_MODEL_NAME:
# 当前用 V5 顶住 后台下载 V6 成功后自动切换
self._download_ocr_v6_in_background()

def _decide_ocr_model_name(self) -> str:
"""
决定本次初始化使用的 OCR 模型名

调试模式: 直接使用配置里的模型 不自动切换
正常模式: 向 V6 收敛
- V6 文件齐全 -> 用 V6
- V6 不齐 但 V5 齐全 -> 先用 V5 顶住 后台下载 V6
- 都没有 -> 直接用 V6(会触发下载)
"""
if self.env_config.is_debug:
return self.model_config.ocr

if self._is_ocr_model_ready(PPOCRV6_MODEL_NAME):
return PPOCRV6_MODEL_NAME

if self._is_ocr_model_ready(DEFAULT_OCR_MODEL_NAME):
return DEFAULT_OCR_MODEL_NAME

return PPOCRV6_MODEL_NAME

@staticmethod
def _is_ocr_model_ready(ocr_model_name: str) -> bool:
"""
判断某个 OCR 模型的文件是否已经全部就绪
"""
return all(Path(f).exists() for f in get_final_file_list(ocr_model_name))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 \
  'get_final_file_list|get_ocr_model_dict_name|dict\.txt|character_dict|def init_model' \
  src/one_dragon/base/matcher/ocr || true

Repository: OneDragon-Anything/ZenlessZoneZero-OneDragon

Length of output: 10366


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== one_dragon_context around _is_ocr_model_ready =="
fd -a 'one_dragon_context.py' . | while read -r f; do
  echo "--- $f"
  wc -l "$f"
  sed -n '520,570p' "$f"
done

echo "== onnx_ocr_matcher get_final_file_list and model init =="
sed -n '54,72p' src/one_dragon/base/matcher/ocr/onnx_ocr_matcher.py
sed -n '74,180p' src/one_dragon/base/matcher/ocr/onnx_ocr_matcher.py
sed -n '200,290p' src/one_dragon/base/matcher/ocr/onnx_ocr_matcher.py

echo "== usages of _is_ocr_model_ready / get_final_file_list =="
rg -n '_is_ocr_model_ready|get_final_file_list\(' src/one_dragon || true

echo "== common downloader check_existed semantics =="
fd -a '.*downloader.*\.py' src/one_dragon | while read -r f; do
  echo "--- $f"
  rg -n -C 5 'check_existed|check_existed_list|exists|is_file|skip_if_existed' "$f" || true
done

Repository: OneDragon-Anything/ZenlessZoneZero-OneDragon

Length of output: 16743


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== dict usage around model loading =="
fd -i '*onnx*ocr*.py' src/one_dragon base | while read -r f; do
  echo "--- $f"
  rg -n -C 6 '_dict|dict_name|character_dict|PaddleOCR|ONNXPaddleOcr|rec_config|det|rec|cls' "$f" || true
done

echo "== download URLs / expected files =="
rg -n -C 4 'PPOCRV6_MODEL_NAME|DEFAULT_OCR_MODEL_NAME|ocr_model_dir|get_ocr_download_url|_dict\.txt|simfang\.ttf' src tests .github docs README* ipp* 2>/dev/null || true

Repository: OneDragon-Anything/ZenlessZoneZero-OneDragon

Length of output: 564


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== dict/model config usages =="
rg -n -C 6 '_dict|dict_name|character_dict|PaddleOCR|ONNXPaddleOcr|rec_config|det_config|cls_config|skip_crnn' src/one_dragon || true

echo "== OCR model names and file expectations =="
rg -n -C 4 'PPOCRV6_MODEL_NAME|DEFAULT_OCR_MODEL_NAME|ocr_model_dir|get_ocr_download_url|_dict\.txt|simfang\.ttf|det\.onnx|rec\.onnx|cls\.onnx' src/one_dragon tests .github README* docs ipp* 2>/dev/null || true

echo "== common path semantics probe =="
tmp="$(mktemp -d)"
python3 - <<'PY' "$tmp" | sed 's#$tmp#<tmp>`#g`'
import sys, pathlib, os
tmp = pathlib.Path(sys.argv[1])
(p := tmp/'pathy_dir').mkdir(parents=True)
paths = [tmp/'missing', p, tmp/'existing.txt']
for f in paths:
    f.touch() if f.name == 'existing.txt' else None
    print(f, 'exists=', f.exists(), 'is_file=', f.is_file())
PY
rm -rf "$tmp"

Repository: OneDragon-Anything/ZenlessZoneZero-OneDragon

Length of output: 45638


更换 OCR 模型路径的存在性检查为普通文件检查。

_is_ocr_model_ready() 使用 Path.exists(),同名目录也会返回 True,会在下载或解压残留同名目录时错误选择 V6;get_final_file_list() 只在发现 _dict.txt 时才将其加入就绪列表,若字典文件必需则当前逻辑可能漏检。应改用 Path.is_file(),并按实际必需文件范围构造检查列表。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/one_dragon/base/operation/one_dragon_context.py` at line 552, 更新
_is_ocr_model_ready() 的 OCR 模型就绪检查,使用 Path.is_file() 排除同名目录;同时按 OCR
模型实际必需的文件范围构造检查列表,确保必需的 _dict.txt 被纳入校验,不要仅依赖 get_final_file_list() 的条件性收集结果。


def _download_ocr_v6_in_background(self) -> None:
"""
后台下载 V6 模型 下载成功后落盘配置 下次启动自动生效
已有下载任务进行中时 不重复启动
"""
if self._ocr_v6_downloading:
return
self._ocr_v6_downloading = True

def download_task() -> None:
try:
v6_matcher = OnnxOcrMatcher(
OnnxOcrParam(
ocr_model_name=PPOCRV6_MODEL_NAME,
)
)
done = v6_matcher.download(
download_by_github=True,
download_by_gitee=False,
download_by_mirror_chan=False,
ghproxy_url=self.env_config.gh_proxy_url if self.env_config.is_gh_proxy else None,
proxy_url=self.env_config.personal_proxy if self.env_config.is_personal_proxy else None,
)
if done:
# 只落盘配置 不立刻切换 避免切换失败导致当前可用的 V5 失效
self.model_config.ocr = PPOCRV6_MODEL_NAME
log.info('OCR V6 后台下载完成 配置已更新 下次启动自动切换')
else:
log.error('OCR V6 后台下载失败 保持当前 V5 可用 下次启动再试')
except Exception:
log.error('OCR V6 后台下载异常 保持当前 V5 可用 下次启动再试', exc_info=True)
finally:
self._ocr_v6_downloading = False

threading.Thread(target=download_task, daemon=True, name='ocr_v6_download').start()

def after_app_shutdown(self) -> None:
"""
App关闭后进行的操作 关闭一切可能资源操作
Expand Down