Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3cf4c2a
Merge pull request #2 from LGG686/dev
LGG686 Mar 10, 2026
bdde043
Merge branch 'main' into dev
LGG686 Mar 14, 2026
a5a129b
fix(GuildActivityMonitor): 调整代码结构
LGG686 Mar 14, 2026
4d70800
feat(ocr): 实现OCR服务
Mar 14, 2026
26db51f
refactor(DemonEncounter): 重构答题逻辑提升性能和准确性
Mar 15, 2026
8113a32
fix(quiz): 修复多进程同时写入日志bug
Mar 15, 2026
e17d32c
fix(SoulsTidy): 无法进入御魂界面
Mar 16, 2026
0d06893
fix(ActivityShikigami): 优化爬塔结算点击逻辑
yEs1do Mar 16, 2026
40e5b5c
feat(ocr): 添加OCR服务器关闭功能并优化资源管理
Mar 19, 2026
9be9032
feat: 吉闻OCR识别改进及多模块优化
lhye Mar 21, 2026
6bb818d
fix(GameUi): 更新 I_CHECK_FRIENDS 图片及坐标
lhye Mar 21, 2026
2d3f003
修改每周分享任务的图片识别范围
yEs1do Mar 22, 2026
e2bce6c
Merge remote-tracking branch 'origin/dev' into dev
LGG686 Mar 23, 2026
118e040
fix(GameUi): adjust 'exploration_goto_six_gates' roi
LGG686 Mar 23, 2026
a2b4963
fix(WantedQuests): 修复云景阆苑和望月幽庭已有悬赏完成时不识别悬赏图标
LGG686 Mar 27, 2026
d55ffa7
fix(GuildActivityMonitor): 修复活动勾选状态失效
LGG686 Mar 30, 2026
d6f62f6
Merge pull request #1473 from LGG686/dev
runhey Apr 2, 2026
6d73794
Merge pull request #1475 from AzurTian/dev_ocrserver
runhey Apr 2, 2026
145474e
Merge pull request #1476 from AzurTian/dev_answer
runhey Apr 2, 2026
388eeb4
Merge pull request #1478 from AzurTian/dev_soulstidy
runhey Apr 2, 2026
d305bd7
Merge pull request #1479 from yEs1do/fix(ActivityShikigami)
runhey Apr 2, 2026
73ea68e
Merge pull request #1484 from lhye/dev
runhey Apr 2, 2026
9fc5423
Merge pull request #1485 from yEs1do/fix(WT_share)
runhey Apr 2, 2026
3fe7cfe
Merge pull request #1486 from LGG686/fix-six_gates
runhey Apr 2, 2026
724f6c0
fix(ActivityShikigami): Update activity t
runhey Apr 4, 2026
bbb081b
fix(tasks/DailyTrifles): rollback ocr assets
runhey Apr 4, 2026
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
15 changes: 6 additions & 9 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,12 @@ oas-backend.bat
.eslintignore
.eslintrc.js
.prettierrc.js








# ai
AGENTS.md
CLAUDE.md
.codex/
.claude/
openspec/


# Created by .ignore support plugin (hsz.mobi)
Expand Down Expand Up @@ -272,6 +270,5 @@ adb_port.ini
*.zip
console.bat
.vscode/*
.claude/settings.local.json
tasks/AutoCheckinBigGod/frida-server-17.6.2-android-x86_64
oas.bat
2 changes: 2 additions & 0 deletions gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
from module.gui.register_type.paint_image import PaintImage
from module.gui.register_type.rule_file import RuleFile
from module.gui.fluent_app import FluentApp
from module.ocr.rpc import ensure_ocr_server_started

if __name__ == "__main__":
ensure_ocr_server_started()
# 检查是不是以管理员身份运行,脚本启动的其他进程会继承权限
# 但是貌似有问题的这个函数
# check_admin()
Expand Down
9 changes: 5 additions & 4 deletions module/ocr/base_ocr.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@

from module.base.decorator import cached_property
from module.base.utils import area_pad, crop, float2str
from module.ocr.ppocr import TextSystem
from module.ocr.models import OCR_MODEL
from typing import Any

from module.ocr.models import get_ocr_model
from module.exception import ScriptError
from module.logger import logger

Expand Down Expand Up @@ -94,8 +95,8 @@ def __repr__(self):
return f"{self.name}"

@cached_property
def model(self) -> TextSystem:
return OCR_MODEL.__getattribute__(self.lang)
def model(self) -> Any:
return get_ocr_model(self.lang)

def pre_process(self, image):
"""
Expand Down
15 changes: 15 additions & 0 deletions module/ocr/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from typing import Dict

from module.base.decorator import cached_property
from module.ocr.ppocr import TextSystem
from module.ocr.rpc import ModelProxy
from module.server.setting import State


class OcrModel:
Expand All @@ -10,6 +14,17 @@ def ch(self):

OCR_MODEL = OcrModel()

_OCR_PROXY_CACHE: Dict[str, ModelProxy] = {}


def get_ocr_model(lang: str = "ch"):
deploy_config = State.deploy_config
if deploy_config.UseOcrServer:
address = deploy_config.OcrClientAddress or "127.0.0.1:22268"
if address not in _OCR_PROXY_CACHE:
_OCR_PROXY_CACHE[address] = ModelProxy(address)
return _OCR_PROXY_CACHE[address]
return getattr(OCR_MODEL, lang)


if __name__ == "__main__":
Expand Down
210 changes: 210 additions & 0 deletions module/ocr/rpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# This Python file uses the following encoding: utf-8
# @author runhey
# github https://github.com/runhey
import atexit
import multiprocessing
import pickle
import socket
import time
from typing import Any, Dict, List, Optional

import numpy as np
import zerorpc

from module.exception import ScriptError
from module.logger import logger
from module.ocr.ppocr import TextSystem

_OCR_SERVER_PROCESS: Optional[multiprocessing.Process] = None


def _normalize_address(address: str) -> str:
if address.startswith("tcp://"):
return address
return f"tcp://{address}"


def _split_host_port(address: str) -> tuple[str, int]:
addr = address.replace("tcp://", "")
if ":" not in addr:
return addr, 22268
host, port = addr.rsplit(":", 1)
return host, int(port)


def _is_port_in_use(host: str, port: int) -> bool:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.settimeout(0.5)
s.connect((host, port))
s.shutdown(2)
return True
except Exception:
return False
finally:
s.close()


def ensure_ocr_server_started() -> bool:
from module.server.setting import State

deploy_config = State.deploy_config
if not deploy_config.StartOcrServer:
return False

if deploy_config.OcrServerPort:
port = int(deploy_config.OcrServerPort)
else:
_, port = _split_host_port(str(deploy_config.OcrClientAddress))
host = "0.0.0.0"

if _is_port_in_use("127.0.0.1", port):
logger.info(f"OCR server already running on port {port}")
return True

global _OCR_SERVER_PROCESS
if _OCR_SERVER_PROCESS is not None and _OCR_SERVER_PROCESS.is_alive():
logger.info("OCR server process already started")
return True

_OCR_SERVER_PROCESS = multiprocessing.Process(
target=run_ocr_server,
args=(host, port),
name="ocr_server",
daemon=True,
)
_OCR_SERVER_PROCESS.start()
logger.info(f"Start OCR server on {host}:{port}")
for _ in range(50):
if _is_port_in_use("127.0.0.1", port):
return True
time.sleep(0.1)
logger.error(f"OCR server is not ready on port {port}")
return False


def shutdown_ocr_server(timeout: float = 2.0) -> bool:
global _OCR_SERVER_PROCESS

process = _OCR_SERVER_PROCESS
if process is None:
return False

if not process.is_alive():
_OCR_SERVER_PROCESS = None
return False

logger.info("Stopping OCR server process")
try:
process.terminate()
process.join(timeout=timeout)
if process.is_alive():
logger.warning("OCR server process did not exit in time, force killing")
process.kill()
process.join(timeout=1.0)
logger.info("OCR server process stopped")
return True
except Exception as e:
logger.exception(e)
return False
finally:
_OCR_SERVER_PROCESS = None


def run_ocr_server(host: str, port: int) -> None:
server = zerorpc.Server(OcrServer())
server.bind(f"tcp://{host}:{port}")
server.run()


class OcrServer:
def __init__(self) -> None:
self.model = TextSystem()

def ping(self) -> bool:
return True

@staticmethod
def _rotate_vertical(image: np.ndarray) -> np.ndarray:
height, width = image.shape[0:2]
if height * 1.0 / width >= 1.5:
return np.rot90(image)
return image

def ocr_single_line(self, image_bytes: bytes):
image = pickle.loads(image_bytes)
result, score = self.model.ocr_single_line(image)
return result, float(score)

def detect_and_ocr(
self,
image_bytes: bytes,
drop_score: float = 0.5,
unclip_ratio: Optional[float] = None,
box_thresh: Optional[float] = None,
vertical: bool = False,
) -> List[Dict[str, Any]]:
image = pickle.loads(image_bytes)
if not vertical:
results = self.model.detect_and_ocr(image, drop_score=drop_score,
unclip_ratio=unclip_ratio,
box_thresh=box_thresh)
return [
{"box": r.box.tolist(), "ocr_text": r.ocr_text, "score": float(r.score)}
for r in results
]

text_recognizer = self.model.text_recognizer

def vertical_text_recognizer(img_crop_list):
img_crop_list = [self._rotate_vertical(i) for i in img_crop_list]
return text_recognizer(img_crop_list)

self.model.text_recognizer = vertical_text_recognizer
try:
results = self.model.detect_and_ocr(image, drop_score=drop_score,
unclip_ratio=unclip_ratio,
box_thresh=box_thresh)
finally:
self.model.text_recognizer = text_recognizer

return [
{"box": r.box.tolist(), "ocr_text": r.ocr_text, "score": float(r.score)}
for r in results
]


class ModelProxy:
is_proxy = True

def __init__(self, address: str) -> None:
self.address = _normalize_address(address)
self.client = zerorpc.Client()
try:
self.client.connect(self.address)
self.client.ping()
except Exception as e:
raise ScriptError(f"OCR server connection failed: {self.address}") from e

def ocr_single_line(self, image: np.ndarray):
payload = pickle.dumps(image, protocol=4)
return self.client.ocr_single_line(payload)

def detect_and_ocr(
self,
image: np.ndarray,
drop_score: float = 0.5,
unclip_ratio: Optional[float] = None,
box_thresh: Optional[float] = None,
vertical: bool = False,
):
payload = pickle.dumps(image, protocol=4)
results = self.client.detect_and_ocr(payload, drop_score, unclip_ratio, box_thresh, vertical)
from ppocronnx.predict_system import BoxedResult
return [
BoxedResult(np.array(item["box"]), None, item["ocr_text"], item["score"])
for item in results
]


atexit.register(shutdown_ocr_server)
2 changes: 2 additions & 0 deletions module/server/home_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from module.config.utils import write_file
from module.logger import logger
from module.ocr.rpc import shutdown_ocr_server
from module.server.main_manager import MainManager
from module.server.updater import Updater
from module.server.i18n import I18n
Expand Down Expand Up @@ -46,6 +47,7 @@ async def notify_test(setting: str, title: str, content: str):

@home_app.get('/kill_server')
async def kill_server():
shutdown_ocr_server()
MainManager.signal_kill_server = True
return 'success'

Expand Down
2 changes: 2 additions & 0 deletions script.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from module.logger import logger
from module.exception import *
from module.server.i18n import I18n
from module.ocr.rpc import ensure_ocr_server_started



Expand Down Expand Up @@ -547,5 +548,6 @@ def start_loop(self) -> None:


if __name__ == "__main__":
ensure_ocr_server_started()
script = Script("oas1")
script.loop()
15 changes: 10 additions & 5 deletions server.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

from module.logger import logger
from module.server.setting import State
from module.ocr.rpc import ensure_ocr_server_started, shutdown_ocr_server


def fun(ev: threading.Event):
import argparse
Expand Down Expand Up @@ -78,13 +80,16 @@ def fun(ev: threading.Event):
logger.attr("Port", port)
logger.attr("Reload", ev is not None)

uvicorn.run("module.server.app:fastapi_app",
host=host,
port=port,
factory=True)
ensure_ocr_server_started()

try:
uvicorn.run("module.server.app:fastapi_app",
host=host,
port=port,
factory=True)
finally:
shutdown_ocr_server()


if __name__ == "__main__":
fun(None)

Binary file modified tasks/ActivityShikigami/as/as_check_battle_main.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tasks/ActivityShikigami/as/as_shi.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tasks/ActivityShikigami/as/as_to_battle_main.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading