diff --git a/.gitignore b/.gitignore index 9fa501c0c..ec8d56cfe 100644 --- a/.gitignore +++ b/.gitignore @@ -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) @@ -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 diff --git a/gui.py b/gui.py index 6e22211f0..151a4e0bf 100644 --- a/gui.py +++ b/gui.py @@ -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() diff --git a/module/ocr/base_ocr.py b/module/ocr/base_ocr.py index 30dd738e9..19c98503b 100644 --- a/module/ocr/base_ocr.py +++ b/module/ocr/base_ocr.py @@ -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 @@ -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): """ diff --git a/module/ocr/models.py b/module/ocr/models.py index 5005fe781..e4c781a25 100644 --- a/module/ocr/models.py +++ b/module/ocr/models.py @@ -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: @@ -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__": diff --git a/module/ocr/rpc.py b/module/ocr/rpc.py new file mode 100644 index 000000000..ff16c12b3 --- /dev/null +++ b/module/ocr/rpc.py @@ -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) diff --git a/module/server/home_router.py b/module/server/home_router.py index ad2174be6..c48ef4aa9 100644 --- a/module/server/home_router.py +++ b/module/server/home_router.py @@ -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 @@ -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' diff --git a/script.py b/script.py index 8c964c04c..b77b44203 100644 --- a/script.py +++ b/script.py @@ -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 @@ -547,5 +548,6 @@ def start_loop(self) -> None: if __name__ == "__main__": + ensure_ocr_server_started() script = Script("oas1") script.loop() diff --git a/server.py b/server.py index 33cb3de7f..fe8bbe446 100644 --- a/server.py +++ b/server.py @@ -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 @@ -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) - diff --git a/tasks/ActivityShikigami/as/as_check_battle_main.png b/tasks/ActivityShikigami/as/as_check_battle_main.png index 44c295859..faa5d51ef 100644 Binary files a/tasks/ActivityShikigami/as/as_check_battle_main.png and b/tasks/ActivityShikigami/as/as_check_battle_main.png differ diff --git a/tasks/ActivityShikigami/as/as_shi.png b/tasks/ActivityShikigami/as/as_shi.png index a1c895295..71fbd52c2 100644 Binary files a/tasks/ActivityShikigami/as/as_shi.png and b/tasks/ActivityShikigami/as/as_shi.png differ diff --git a/tasks/ActivityShikigami/as/as_to_battle_main.png b/tasks/ActivityShikigami/as/as_to_battle_main.png index c17e77ff1..cb954596f 100644 Binary files a/tasks/ActivityShikigami/as/as_to_battle_main.png and b/tasks/ActivityShikigami/as/as_to_battle_main.png differ diff --git a/tasks/ActivityShikigami/as/pages.json b/tasks/ActivityShikigami/as/pages.json index ce6158877..b7c85f4ff 100644 --- a/tasks/ActivityShikigami/as/pages.json +++ b/tasks/ActivityShikigami/as/pages.json @@ -2,7 +2,7 @@ { "itemName": "shi", "imageName": "as_shi.png", - "roiFront": "1184,384,47,29", + "roiFront": "1187,301,43,29", "roiBack": "1164,134,83,393", "method": "Template matching", "threshold": 0.7, @@ -38,8 +38,8 @@ { "itemName": "to_battle_main", "imageName": "as_to_battle_main.png", - "roiFront": "223,287,39,176", - "roiBack": "100,242,238,269", + "roiFront": "861,229,39,176", + "roiBack": "752,170,238,269", "method": "Template matching", "threshold": 0.8, "description": "进入主要的战斗界面" @@ -56,8 +56,8 @@ { "itemName": "check_battle_main", "imageName": "as_check_battle_main.png", - "roiFront": "842,565,39,45", - "roiBack": "688,499,415,203", + "roiFront": "845,560,39,45", + "roiBack": "626,477,415,203", "method": "Template matching", "threshold": 0.8, "description": "description" @@ -77,7 +77,7 @@ "roiFront": "1017,572,39,42", "roiBack": "976,539,146,100", "method": "Template matching", - "threshold": 0.8, + "threshold": 0.7, "description": "从main进入到式神录" }, { diff --git a/tasks/ActivityShikigami/assets.py b/tasks/ActivityShikigami/assets.py index 3ef457b8d..aebac94db 100644 --- a/tasks/ActivityShikigami/assets.py +++ b/tasks/ActivityShikigami/assets.py @@ -38,7 +38,7 @@ class ActivityShikigamiAssets: # Image Rule Assets # 进入活动 - I_SHI = RuleImage(roi_front=(1184,384,47,29), roi_back=(1164,134,83,393), threshold=0.7, method="Template matching", file="./tasks/ActivityShikigami/as/as_shi.png") + I_SHI = RuleImage(roi_front=(1187,301,43,29), roi_back=(1164,134,83,393), threshold=0.7, method="Template matching", file="./tasks/ActivityShikigami/as/as_shi.png") # 左上角返回 I_BACK_GREEN = RuleImage(roi_front=(27,19,34,38), roi_back=(2,1,170,75), threshold=0.8, method="Template matching", file="./tasks/ActivityShikigami/as/as_back_green.png") # 右上跳过按钮 @@ -46,15 +46,15 @@ class ActivityShikigamiAssets: # 红色退出 I_RED_EXIT = RuleImage(roi_front=(1162,96,39,38), roi_back=(1120,49,110,135), threshold=0.8, method="Template matching", file="./tasks/ActivityShikigami/as/as_red_exit.png") # 进入主要的战斗界面 - I_TO_BATTLE_MAIN = RuleImage(roi_front=(223,287,39,176), roi_back=(100,242,238,269), threshold=0.8, method="Template matching", file="./tasks/ActivityShikigami/as/as_to_battle_main.png") + I_TO_BATTLE_MAIN = RuleImage(roi_front=(861,229,39,176), roi_back=(752,170,238,269), threshold=0.8, method="Template matching", file="./tasks/ActivityShikigami/as/as_to_battle_main.png") # 点击进入boss战斗页面 I_TO_BATTLE_BOSS = RuleImage(roi_front=(1076,241,37,136), roi_back=(934,209,234,327), threshold=0.8, method="Template matching", file="./tasks/ActivityShikigami/as/as_to_battle_boss.png") # description - I_CHECK_BATTLE_MAIN = RuleImage(roi_front=(842,565,39,45), roi_back=(688,499,415,203), threshold=0.8, method="Template matching", file="./tasks/ActivityShikigami/as/as_check_battle_main.png") + I_CHECK_BATTLE_MAIN = RuleImage(roi_front=(845,560,39,45), roi_back=(626,477,415,203), threshold=0.8, method="Template matching", file="./tasks/ActivityShikigami/as/as_check_battle_main.png") # description I_CHECK_BATTLE_BOSS = RuleImage(roi_front=(34,365,45,35), roi_back=(20,328,100,100), threshold=0.8, method="Template matching", file="./tasks/ActivityShikigami/as/as_check_battle_boss.png") # 从main进入到式神录 - I_BATTLE_MAIN_TO_RECORDS = RuleImage(roi_front=(1017,572,39,42), roi_back=(976,539,146,100), threshold=0.8, method="Template matching", file="./tasks/ActivityShikigami/as/as_battle_main_to_records.png") + I_BATTLE_MAIN_TO_RECORDS = RuleImage(roi_front=(1017,572,39,42), roi_back=(976,539,146,100), threshold=0.7, method="Template matching", file="./tasks/ActivityShikigami/as/as_battle_main_to_records.png") # description I_TO_BATTLE_MAIN_2 = RuleImage(roi_front=(15,94,247,38), roi_back=(2,68,311,100), threshold=0.65, method="Template matching", file="./tasks/ActivityShikigami/as/as_to_battle_main_2.png") # 确认跳过 @@ -92,9 +92,9 @@ class ActivityShikigamiAssets: # Ocr Rule Assets # 挑战 - O_FIRE = RuleOcr(roi=(1137,599,83,44), area=(1129,570,100,100), mode="Single", method="Default", keyword="挑战", name="fire") + O_FIRE = RuleOcr(roi=(1124,599,96,50), area=(1123,570,106,100), mode="Single", method="Default", keyword="挑战", name="fire") # 体力的数量检测 - O_REMAIN_AP = RuleOcr(roi=(930,18,98,29), area=(925,12,109,40), mode="DigitCounter", method="Default", keyword="", name="remain_ap") + O_REMAIN_AP = RuleOcr(roi=(836,25,51,30), area=(836,25,51,30), mode="DigitCounter", method="Default", keyword="", name="remain_ap") # 活动体力的剩余检测 O_REMAIN_PASS = RuleOcr(roi=(518,18,88,28), area=(514,12,97,38), mode="DigitCounter", method="Default", keyword="", name="remain_pass") # 还有多少次购买体力的机会 diff --git a/tasks/ActivityShikigami/fire/ocr.json b/tasks/ActivityShikigami/fire/ocr.json index e6eb8cbba..1f89b9f71 100644 --- a/tasks/ActivityShikigami/fire/ocr.json +++ b/tasks/ActivityShikigami/fire/ocr.json @@ -1,8 +1,8 @@ [ { "itemName": "fire", - "roiFront": "1137,599,83,44", - "roiBack": "1129,570,100,100", + "roiFront": "1124,599,96,50", + "roiBack": "1123,570,106,100", "mode": "Single", "method": "Default", "keyword": "挑战", @@ -10,8 +10,8 @@ }, { "itemName": "remain_ap", - "roiFront": "930,18,98,29", - "roiBack": "925,12,109,40", + "roiFront": "836,25,51,30", + "roiBack": "836,25,51,30", "mode": "DigitCounter", "method": "Default", "keyword": "", diff --git a/tasks/ActivityShikigami/script_task.py b/tasks/ActivityShikigami/script_task.py index 1bb89690f..56bfdbbed 100644 --- a/tasks/ActivityShikigami/script_task.py +++ b/tasks/ActivityShikigami/script_task.py @@ -151,6 +151,10 @@ def run(self) -> None: self.limit_time: timedelta = self.conf.general_climb.limit_time_v # for climb_type in self.conf.general_climb.run_sequence_v: + # 2026.04.04>>>---------------------------------------------------------------- + if climb_type not in ['ap']: + continue + # 2026.04.04<<<---------------------------------------------------------------- # 进入到活动的主页面,不是具体的战斗页面 self.ui_get_current_page() self.ui_goto(game.page_climb_act) @@ -220,7 +224,9 @@ def _run_ap(self): self.ui_clicks([self.I_TO_BATTLE_MAIN, self.I_TO_BATTLE_MAIN_2], stop=self.I_CHECK_BATTLE_MAIN, interval=1) self.switch_soul(self.I_BATTLE_MAIN_TO_RECORDS, self.I_CHECK_BATTLE_MAIN) - self.switch_climb_mode_in_game('ap') + # 2026.04.04>>>---------------------------------------------------------------- + # self.switch_climb_mode_in_game('ap') + # 2026.04.04<<<---------------------------------------------------------------- ocr_limit_timer = Timer(1).start() while 1: @@ -258,7 +264,7 @@ def _run_ap100(self): logger.hr(f'Start run climb type AP100') def start_battle(self): - click_times, max_times = 0, random.randint(2, 4) + click_times, max_times = 0, random.randint(4, 8) while 1: self.screenshot() if self.is_in_battle(False): @@ -284,7 +290,7 @@ def battle_wait(self, random_click_swipt_enable: bool) -> bool: self.count_map[self.climb_type] = self.current_count for btn in (self.C_RANDOM_LEFT, self.C_RANDOM_RIGHT, self.C_RANDOM_TOP, self.C_RANDOM_BOTTOM): btn.name = "BATTLE_RANDOM" - ok_cnt, max_retry = 0, 5 + ok_cnt, max_retry = 0, 8 while 1: sleep(random.uniform(0.5, 1.5)) self.screenshot() @@ -305,12 +311,13 @@ def battle_wait(self, random_click_swipt_enable: bool) -> bool: # 出现 “魂” 和 紫蛇皮 if self.appear(self.I_REWARD) or self.appear(self.I_REWARD_PURPLE_SNAKE_SKIN) or \ self.appear(self.I_REWARD_GOLD) or self.appear(self.I_REWARD_GOLD_SNAKE_SKIN): - self.random_reward_click(exclude_click=[self.C_RANDOM_RIGHT]) + self.random_reward_click(exclude_click=[self.C_RANDOM_TOP, self.C_RANDOM_LEFT]) ok_cnt += 1 continue # 已经不在战斗中了, 且奖励也识别过了, 则随机点击 - if ok_cnt > 0 and not self.is_in_battle(False): - self.random_reward_click(exclude_click=[self.C_RANDOM_RIGHT]) + if ok_cnt > 3 and not self.is_in_battle(False): + self.random_reward_click(exclude_click=[self.C_RANDOM_TOP, self.C_RANDOM_LEFT]) + self.device.stuck_record_clear() ok_cnt += 1 continue # 战斗中随机滑动 diff --git a/tasks/Component/CostumeBattle/assets.py b/tasks/Component/CostumeBattle/assets.py index 8ccfa4833..84c942ad5 100644 --- a/tasks/Component/CostumeBattle/assets.py +++ b/tasks/Component/CostumeBattle/assets.py @@ -37,6 +37,21 @@ class CostumeBattleAssets: I_LOCAL_11 = RuleImage(roi_front=(29,566,29,28), roi_back=(29,566,40,40), threshold=0.8, method="Template matching", file="./tasks/Component/CostumeBattle/battle11/battle11_local_11.png") + # Image Rule Assets + # 左上角退出 + I_EXIT_12 = RuleImage(roi_front=(16,15,22,25), roi_back=(16,15,40,40), threshold=0.8, method="Template matching", file="./tasks/Component/CostumeBattle/battle12/battle12_exit_12.png") + # 左上角好友 + I_FRIENDS_12 = RuleImage(roi_front=(90,14,22,30), roi_back=(90,14,40,40), threshold=0.8, method="Template matching", file="./tasks/Component/CostumeBattle/battle12/battle12_friends_12.png") + # 指针 + I_LOCAL_12 = RuleImage(roi_front=(29,566,28,28), roi_back=(29,566,40,40), threshold=0.8, method="Template matching", file="./tasks/Component/CostumeBattle/battle12/battle12_local_12.png") + # 战斗胜利 + I_WIN_12 = RuleImage(roi_front=(390,30,46,88), roi_back=(390,30,510,290), threshold=0.8, method="Template matching", file="./tasks/Component/CostumeBattle/battle12/battle12_win_12.png") + # 针对封魔的特殊 + I_DE_WIN_12 = RuleImage(roi_front=(390,30,46,88), roi_back=(390,30,510,290), threshold=0.8, method="Template matching", file="./tasks/Component/CostumeBattle/battle12/battle12_de_win_12.png") + # 失败 + I_FALSE_12 = RuleImage(roi_front=(390,30,58,86), roi_back=(390,30,510,290), threshold=0.8, method="Template matching", file="./tasks/Component/CostumeBattle/battle12/battle12_false_12.png") + + # Image Rule Assets # description I_LOCAL_2 = RuleImage(roi_front=(30,569,21,22), roi_back=(30,569,21,22), threshold=0.8, method="Template matching", file="./tasks/Component/CostumeBattle/battle2/battle2_local_2.png") @@ -115,18 +130,3 @@ class CostumeBattleAssets: I_LOCAL_9 = RuleImage(roi_front=(114,489,37,37), roi_back=(114,489,37,37), threshold=0.8, method="Template matching", file="./tasks/Component/CostumeBattle/battle9/battle9_local_9.png") - # Image Rule Assets - # 左上角退出 - I_EXIT_12 = RuleImage(roi_front=(16,15,22,25), roi_back=(16,15,40,40), threshold=0.8, method="Template matching", file="./tasks/Component/CostumeBattle/battle12/battle12_exit_12.png") - # 左上角好友 - I_FRIENDS_12 = RuleImage(roi_front=(90,14,22,30), roi_back=(90,14,40,40), threshold=0.8, method="Template matching", file="./tasks/Component/CostumeBattle/battle12/battle12_friends_12.png") - # 指针 - I_LOCAL_12 = RuleImage(roi_front=(29,566,28,28), roi_back=(29,566,40,40), threshold=0.8, method="Template matching", file="./tasks/Component/CostumeBattle/battle12/battle12_local_12.png") - # 战斗胜利 - I_WIN_12 = RuleImage(roi_front=(390,30,46,88), roi_back=(390,30,510,290), threshold=0.8, method="Template matching", file="./tasks/Component/CostumeBattle/battle12/battle12_win_12.png") - # 针对封魔的特殊 - I_DE_WIN_12 = RuleImage(roi_front=(390,30,46,88), roi_back=(390,30,510,290), threshold=0.8, method="Template matching", file="./tasks/Component/CostumeBattle/battle12/battle12_de_win_12.png") - # 失败 - I_FALSE_12 = RuleImage(roi_front=(390,30,58,86), roi_back=(390,30,510,290), threshold=0.8, method="Template matching", file="./tasks/Component/CostumeBattle/battle12/battle12_false_12.png") - - diff --git a/tasks/Component/CostumeShikigami/assets.py b/tasks/Component/CostumeShikigami/assets.py index d81c3a185..8f67cfa1c 100644 --- a/tasks/Component/CostumeShikigami/assets.py +++ b/tasks/Component/CostumeShikigami/assets.py @@ -311,7 +311,7 @@ class CostumeShikigamiAssets: I_ST_REPLACE_7 = RuleImage(roi_front=(858,162,100,100), roi_back=(858,162,100,100), threshold=0.8, method="Template matching", file="./tasks/Component/CostumeShikigami/sk7/sk7_st_replace_7.png") - # Image Rule Assets - sk8 童梦基地 + # Image Rule Assets # 用于判断在式神录 I_CHECK_RECORDS_8 = RuleImage(roi_front=(265,71,34,36), roi_back=(265,71,60,50), threshold=0.8, method="Template matching", file="./tasks/Component/CostumeShikigami/sk8/sk8_check_records_8.png") # 退出式神录 diff --git a/tasks/DailyTrifles/assets.py b/tasks/DailyTrifles/assets.py index 09ab4af48..67f248e96 100644 --- a/tasks/DailyTrifles/assets.py +++ b/tasks/DailyTrifles/assets.py @@ -17,8 +17,6 @@ class DailyTriflesAssets: I_L_LOVE = RuleImage(roi_front=(123,625,67,72), roi_back=(123,625,67,72), threshold=0.9, method="Template matching", file="./tasks/DailyTrifles/love/love_l_love.png") # 一键收取 I_L_COLLECT = RuleImage(roi_front=(47,537,129,56), roi_back=(47,537,129,56), threshold=0.8, method="Template matching", file="./tasks/DailyTrifles/love/love_l_collect.png") - # 吉闻 - I_LUCK_MSG = RuleImage(roi_front=(22,47,46,27), roi_back=(22,47,46,27), threshold=0.8, method="Template matching", file="./tasks/DailyTrifles/love/Screenshots_luck_msg.png") # 一键祝福 I_ONE_CLICK_BLESS = RuleImage(roi_front=(1115,500,93,33), roi_back=(1115,500,93,33), threshold=0.8, method="Template matching", file="./tasks/DailyTrifles/love/Screenshots_one_click_bless.png") # 点击祝福 @@ -71,5 +69,7 @@ class DailyTriflesAssets: O_SELECT_SM3 = RuleOcr(roi=(26,304,45,38), area=(26,304,45,38), mode="Single", method="Default", keyword="", name="select_sm3") # description O_SELECT_SM4 = RuleOcr(roi=(26,397,45,38), area=(26,397,45,38), mode="Single", method="Default", keyword="", name="select_sm4") + # 吉闻 + O_LUCK_MSG = RuleOcr(roi=(15,38,70,70), area=(15,38,70,70), mode="Single", method="Default", keyword="吉闻", name="luck_msg") diff --git a/tasks/DailyTrifles/love/image.json b/tasks/DailyTrifles/love/image.json index e3b8800be..853903b29 100644 --- a/tasks/DailyTrifles/love/image.json +++ b/tasks/DailyTrifles/love/image.json @@ -26,15 +26,6 @@ "threshold": 0.8, "description": "一键收取" }, - { - "itemName": "luck_msg", - "imageName": "Screenshots_luck_msg.png", - "roiFront": "22,47,46,27", - "roiBack": "22,47,46,27", - "method": "Template matching", - "threshold": 0.8, - "description": "吉闻" - }, { "itemName": "one_click_bless", "imageName": "Screenshots_one_click_bless.png", @@ -71,4 +62,4 @@ "threshold": 0.8, "description": "好友羁绊提升弹窗" } -] \ No newline at end of file +] diff --git a/tasks/DailyTrifles/script_task.py b/tasks/DailyTrifles/script_task.py index c5a877140..4da99842c 100644 --- a/tasks/DailyTrifles/script_task.py +++ b/tasks/DailyTrifles/script_task.py @@ -162,7 +162,7 @@ def run_luck_msg(self): break if self.appear_then_click(self.I_FRIENDSHIP_UP, interval=1): continue - if self.appear_then_click(self.I_LUCK_MSG, interval=1): + if self.ocr_appear_click(self.O_LUCK_MSG, interval=1): continue logger.info('Start luck msg') check_timer = Timer(2) diff --git a/tasks/DailyTrifles/summonRecall/ocr.json b/tasks/DailyTrifles/summonRecall/ocr.json index a52307e7c..1e6ff38fc 100644 --- a/tasks/DailyTrifles/summonRecall/ocr.json +++ b/tasks/DailyTrifles/summonRecall/ocr.json @@ -56,5 +56,14 @@ "method": "Default", "keyword": "", "description": "description" + }, + { + "itemName": "luck_msg", + "roiFront": "15,38,70,70", + "roiBack": "15,38,70,70", + "mode": "Single", + "method": "Default", + "keyword": "吉闻", + "description": "吉闻" } ] \ No newline at end of file diff --git a/tasks/DemonEncounter/data/answer.py b/tasks/DemonEncounter/data/answer.py index 72f6c9568..392f233d2 100644 --- a/tasks/DemonEncounter/data/answer.py +++ b/tasks/DemonEncounter/data/answer.py @@ -10,179 +10,220 @@ from module.logger import logger -def count_intersection(str1, str2): - set1 = set(str1) - set2 = set(str2) - intersection = set1.intersection(set2) - return len(intersection) def remove_symbols(text): return re.sub(r'[^\w\s]', '', text) + +def _tokenize_question(text: str) -> list[str]: + # 2-gram 题目切词,长度不足时退化为 1-gram + text = text.replace(' ', '') + if len(text) < 2: + return list(text) + return [text[i:i + 2] for i in range(len(text) - 1)] + + +def _normalize(q: str, opts: list[str]) -> tuple[str, list[str]]: + # 统一清洗 OCR 字符 + q = q.replace('「', '').replace('」', '').replace('?', '') + q = remove_symbols(q) + opts = [remove_symbols(opt) for opt in opts] + return q, opts + + class Answer: - def __init__(self): - self.data: dict[str, list] = {} - self.data_options: dict[str, list] = {} - # 添加缓存以确保相同问题的答案一致性 - # 修改:缓存存储答案字符串而不是索引,避免选项顺序变化的影响 - self.question_cache: dict[str, str] = {} - file = str(Path(__file__).parent / 'data.csv') - with open(file, newline='', encoding='utf-8-sig') as csvfile: + question_sim_threshold = 0.8 + answer_sim_threshold = 0.5 + data_file = Path(__file__).parent / 'data.csv' + question_answer: dict[str, set[str]] = {} # 题目: {标准答案 + answer_question: dict[str, set[str]] = {} # 标准答案: {题目} + question_index: dict[str, set[str]] = {} # 倒排索引 + question_cache: dict[str, set[str]] = {} # 缓存:识别到的问题 -> {标准答案, 选项答案} + + def __init__(self, top_n: int = 30): + self.top_n = top_n + with open(self.data_file, newline='', encoding='utf-8') as csvfile: reader = csv.reader(csvfile) next(reader) for row in reader: key = remove_symbols(row[0]) value = remove_symbols(row[1]) - if key not in self.data: - self.data[key] = [] - self.data[key].append(value) - if value not in self.data_options: - self.data_options[value] = [] - self.data_options[value].append(key) - - def answer_one(self, question: str, options: list[str]) -> int|None: + self.question_answer.setdefault(key, set()).add(value) + self.answer_question.setdefault(value, set()).add(key) + for token in _tokenize_question(key): + self.question_index.setdefault(token, set()).add(key) + + def answer_one(self, question: str, options: list[str]) -> int | None: """ 每一个问题有三个选项, 返回选项的序号(1、2、3) :param question: :param options: :return: """ - - # 检查缓存,如果已经处理过类似问题,直接返回结果 - # 修改:现在缓存的是答案字符串,需要在选项中查找对应的索引 - for cached_q, cached_answer_str in self.question_cache.items(): - similarity = difflib.SequenceMatcher(None, question, cached_q).ratio() - if similarity >= 0.9: # 如果问题相似度超过90%,直接返回缓存的答案 - # 在当前选项中查找缓存的答案字符串 - for idx, option in enumerate(options): - if option == cached_answer_str: - return idx + 1 # 返回选项索引(从1开始) - # 如果缓存的答案在当前选项中不存在,则尝试模糊匹配 - for idx, option in enumerate(options): - if difflib.SequenceMatcher(None, option, cached_answer_str).ratio() >= 0.9: - logger.info(f"Fuzzy match cache: '{cached_answer_str}' ~ '{option}'") - return idx + 1 - def decide_by_question_foreach(ques: str, ops: list[str]): - # 这个是最耗时的操作,遍历数据库中的所有题目,找到最相识的题目和选项 - for key, values in self.data.items(): - question_match_ratio = difflib.SequenceMatcher(None, ques, key).ratio() - if question_match_ratio < 0.7: - continue - for value in values: - for index, option in enumerate(ops): - option_match_ratio = difflib.SequenceMatcher(None, option, value).ratio() - if option_match_ratio >= 0.5: - logger.warning('The worst case: no match found for question and option') - logger.warning('Now traversing the entire database to find the most similar question and option') - logger.warning(f'Most similar question: {key} and most similar option: {value}') - result_index = index + 1 - # 修改:缓存问题和答案字符串 - self.question_cache[ques] = ops[index] - return result_index - return None - - def decide_by_options(question: str, ops: list[str]): - # 瞎猫当死耗子 - matches = {} - for index, option in enumerate(ops): - if option not in self.data_options.keys(): - continue - for ques in self.data_options[option]: - ques_match_ratio = difflib.SequenceMatcher(None, ques, question).ratio() - if ques_match_ratio > 0.8: - matches[index + 1] = ques_match_ratio - if matches: - opts = sorted(matches.items(), key=lambda x: x[1], reverse=True) - logger.warning('No match found for question,\n Now traversing the entire database to find the most similar question') - logger.warning(f'Most similar answer: {opts[0][0]}, and similarity char number is {opts[0][1]}') - result_index = opts[0][0] - # 修改:缓存问题和答案字符串 - self.question_cache[question] = ops[result_index - 1] - return result_index - index = decide_by_question_foreach(question, ops) - return index if index else None - - - question = question.replace('「', '').replace('」', '').replace('?', '') - question = remove_symbols(question) - options = [remove_symbols(option) for option in options] - + question, options = _normalize(question, options) for index, option in enumerate(options): if option == '其余选项皆对': result_index = index + 1 - # 特殊选项才缓存 - # 修改:缓存问题和答案字符串 - self.question_cache[question] = option + # 缓存问题和答案字符串 + self._cache_store(question, option, option) + logger.debug('Answer strategy: special option hit.') return result_index - - question_matches: list = [] - try: - question_matches = self.data[question] - except Exception as e: - # 没有出现题目,从选项反入手 - logger.error('Exception: %s', e) - result = decide_by_options(question=question, ops=options) - if result is not None: - # 只有在使用模糊匹配的情况下才缓存结果 - # 修改:缓存问题和答案字符串 - self.question_cache[question] = options[result - 1] + # 1) 优先命中缓存 + cached_index = self._cache_hit(question, options) + if cached_index is not None: + return cached_index + # 2) 题目命中:精确或相似选项匹配 + if question in self.question_answer: + return self._handle_known_question(question, options) + # 3) 相似题:先精确,再相似匹配 + result = self._handle_similar_questions(question, options) + if result is not None: return result - # 出现了题目,开始对答案 - for index, option in enumerate(options): - for match in question_matches: - if match == option: - return index + 1 # 精确匹配,不缓存,直接返回 - # 可能选项识别某一个字错误 - if all(options): - for index, option in enumerate(options): - for match in question_matches: - if len(match) != len(option): - continue - if len(match) - count_intersection(match, option) <= 1 : - logger.warning('Option is not match: %s, %s', match, option) - return index + 1 # 这也是精确匹配的一种,不缓存,直接返回 - # 最保守策略,答案匹配度最高 - opts = {} - for index, option in enumerate(options): - item_match_ratio = 0 - for match in question_matches: - match_ratio = difflib.SequenceMatcher(None, match, option).ratio() - if match_ratio >= 0.33 and match_ratio > item_match_ratio: - item_match_ratio = match_ratio - if item_match_ratio > 0: - opts[index + 1] = item_match_ratio - if opts: - opts = sorted(opts.items(), key=lambda x: x[1], reverse=True) - logger.warning(f'Use SequenceMatcher and get answers: {opts[0][0]}, score: {opts[0][1]}') - result_index = opts[0][0] - # 只有在这种情况下才缓存 - # 修改:缓存问题和答案字符串 - self.question_cache[question] = options[result_index - 1] - return result_index - - # 选项一个都对不上可能是,正确答案检测为空 - for index, option in enumerate(options): - if option == '': - logger.error('Option is empty: %s', options) - result_index = index + 1 - # 只有在这种情况下才缓存 - # 修改:缓存问题和答案字符串 - self.question_cache[question] = option - return result_index - - # 如果所有方法都失败,尝试使用decide_by_options - result = decide_by_options(question=question, ops=options) + # 4) 选项是否存在于答案集合 + result = self._handle_answer_option_match(question, options) if result is not None: - # 只有在使用模糊匹配的情况下才缓存结果 - # 修改:缓存问题和答案字符串 - self.question_cache[question] = options[result - 1] - return result + return result + # 5) 完全未知:返回 None + logger.error('Unknown question: %s', question) + return None + + def _cache_store(self, question: str, std_answer: str, chosen: str) -> None: + # 更新缓存,便于下一次直接命中 + cache = self.question_cache.setdefault(question, set()) + if std_answer: + cache.add(std_answer) + if chosen: + cache.add(chosen) + + def _find_option_index(self, opts: list[str], target: str) -> int | None: + # 精确查找选项索引 + for idx, option in enumerate(opts): + if option == target: + return idx + 1 + return None + + def _cache_hit(self, question: str, opts: list[str]) -> int | None: + # 优先命中缓存答案 + cached = self.question_cache.get(question) + if not cached: + return None + for ans in cached: + idx = self._find_option_index(opts, ans) + if idx is not None: + logger.info('Hit cache.') + return idx + return None + + def _best_similarity_option(self, options: list[str], answer: str) -> tuple[int | None, float]: + # 从选项中找与答案最相似的项 + best_idx = None + best_score = 0.0 + for idx, option in enumerate(options): + score = difflib.SequenceMatcher(None, option, answer).ratio() + if score > best_score: + best_score = score + best_idx = idx + 1 + return best_idx, best_score + + def _get_similar_questions(self, question: str) -> list[tuple[str, float]]: + # 倒排索引过滤候选问题,再按相似度排序 + tokens = _tokenize_question(question) + if not tokens: + return [] + candidate_scores: dict[str, int] = {} + for token in tokens: + for key in self.question_index.get(token, []): + candidate_scores[key] = candidate_scores.get(key, 0) + 1 + if not candidate_scores: + return [] + candidates = sorted(candidate_scores.items(), key=lambda x: x[1], reverse=True) + if self.top_n and len(candidates) > self.top_n: + candidates = candidates[:self.top_n] + scored = [] + for key, _ in candidates: + score = difflib.SequenceMatcher(None, question, key).ratio() + if score >= self.question_sim_threshold: + scored.append((key, score)) + scored.sort(key=lambda x: x[1], reverse=True) + return scored + + def _handle_known_question(self, question: str, options: list[str]) -> int | None: + # 题目命中时,优先精确匹配答案 + for answer in self.question_answer[question]: + idx = self._find_option_index(options, answer) + if idx is not None: + logger.info('Hit question, correct answer.') + self._cache_store(question, answer, options[idx - 1]) + return idx + # 题目命中但选项不一致:按相似度选最接近答案 + best_idx, best_score, best_answer = None, 0.0, "" + for answer in self.question_answer[question]: + idx, score = self._best_similarity_option(options, answer) + if idx is not None and score > best_score: + best_idx, best_score, best_answer = idx, score, answer + if best_idx is not None: + logger.info('Hit question, similar answer.') + self._cache_store(question, best_answer, options[best_idx - 1]) + return best_idx + + def _handle_similar_questions(self, question: str, options: list[str]) -> int | None: + # 相似题优先:逐题匹配答案是否在选项中 + similar_questions = self._get_similar_questions(question) + for key, _ in similar_questions: + for answer in self.question_answer.get(key, []): + idx = self._find_option_index(options, answer) + if idx is not None: + logger.info('Hit similar question, correct answer') + self._cache_store(question, answer, options[idx - 1]) + return idx + # 相似题答案不在选项中:做相似度匹配 + best_idx, best_score, best_answer = None, 0.0, "" + for key, _ in similar_questions: + for answer in self.question_answer.get(key, []): + idx, score = self._best_similarity_option(options, answer) + if idx is not None and score > best_score: + best_idx, best_score, best_answer = idx, score, answer + if best_idx is not None and best_score >= self.answer_sim_threshold: + logger.info('Hit similar question, similar answer') + self._cache_store(question, best_answer, options[best_idx - 1]) + return best_idx + return None + + def _handle_answer_option_match(self, question: str, options: list[str]) -> int | None: + # 选项命中答案集合时,根据题目相似度校验 + for idx, option in enumerate(options): + if option in self.answer_question: + questions = self.answer_question.get(option, set()) + best_question, best_score = None, 0.0 + for candidate in questions: + score = difflib.SequenceMatcher(None, question, candidate).ratio() + if score > best_score: + best_score = score + best_question = candidate + # 选项在答案中, 则问题相似度降级处理 + if best_question is not None and best_score >= self.answer_sim_threshold: + self._cache_store(question, option, option) + logger.info('Hit correct answer, lower similar question') + return idx + 1 + return None if __name__ == "__main__": answer = Answer() - question = '冥界中谁拥阁魔之目一双审善度恶' - options = ['判官', '孟婆', '荒川之主', '阁魔'] - start_time = datetime.now() - print(answer.answer_one(question, options)) - print(datetime.now() - start_time) + test_cases = [ + # 精确命中 + ('冥界中谁拥阁魔之目一双审善度恶', ['判官', '孟婆', '荒川之主', '阁魔']), + # 相似题命中(缺尾) + ('冥界中谁拥阁魔之目一双', ['判官', '孟婆', '荒川之主', '阁魔']), + # 相似题命中(字形误差) + ('冥界中谁用阎魔之日一双审善渡恶', ['判官', '孟婆', '荒川之主', '阁魔']), + # 选项存在于答案集合 + ('冥界中谁拥阁魔', ['六百六十六', '七百七十七', '八百八十八', '阁魔']), + # 完全未知 + ('这是一条完全未知的问题', ['甲', '乙', '丙', '丁']), + ] + for question, options in test_cases: + start_time = datetime.now() + result = answer.answer_one(question, options) + cost_ms = (datetime.now() - start_time).microseconds / 1000 + print(f'Q: {question} -> {result}, cost: {cost_ms:.2f}ms') diff --git a/tasks/FrogBoss/config.py b/tasks/FrogBoss/config.py index c4db7e39b..0550785ba 100644 --- a/tasks/FrogBoss/config.py +++ b/tasks/FrogBoss/config.py @@ -14,6 +14,8 @@ class Strategy(str, Enum): Minority = 'frog_minority' Bilibili = 'frog_bilibili' Dashen = 'frog_dashen' + AlwaysRed = 'frog_always_red' + AlwaysBlue = 'frog_always_blue' class FrogBossConfig(ConfigBase): before_end_frog: Time = Field(default=Time(0, 15, 0), description='before_end_frog_help') diff --git a/tasks/FrogBoss/script_task.py b/tasks/FrogBoss/script_task.py index 5e0fb42d2..1b37471b1 100644 --- a/tasks/FrogBoss/script_task.py +++ b/tasks/FrogBoss/script_task.py @@ -105,6 +105,10 @@ def do_bet(self): click_image = self.I_BET_LEFT if count_left > count_right else self.I_BET_RIGHT case Strategy.Dashen: click_image = self.get_dashen(count_left, count_right) + case Strategy.AlwaysRed: + click_image = self.I_BET_LEFT + case Strategy.AlwaysBlue: + click_image = self.I_BET_RIGHT case _: raise ValueError(f'Unknown bet mode: {self.config.model.frog_boss.frog_boss_config.strategy_frog}') logger.info(f'You strategy is {self.config.model.frog_boss.frog_boss_config.strategy_frog} and bet on {click_image}') diff --git a/tasks/GameUi/assets.py b/tasks/GameUi/assets.py index dbbfff37d..1ea326026 100644 --- a/tasks/GameUi/assets.py +++ b/tasks/GameUi/assets.py @@ -62,7 +62,7 @@ class GameUiAssets: # 探索前往平安奇谭 I_EXPLORATION_GOTO_HEIAN_KITAN = RuleImage(roi_front=(739,643,52,44), roi_back=(739,643,180,44), threshold=0.8, method="Template matching", file="./tasks/GameUi/page/page_exploration_goto_heian_kitan.png") # 探索前往六道之门 - I_EXPLORATION_GOTO_SIX_GATES = RuleImage(roi_front=(938,640,60,49), roi_back=(918,631,200,66), threshold=0.65, method="Template matching", file="./tasks/GameUi/page/page_exploration_goto_six_gates.png") + I_EXPLORATION_GOTO_SIX_GATES = RuleImage(roi_front=(938,640,60,49), roi_back=(820,631,296,66), threshold=0.65, method="Template matching", file="./tasks/GameUi/page/page_exploration_goto_six_gates.png") # 探索前往契灵之境 I_EXPLORATION_GOTO_BONDLING_FAIRYLAND = RuleImage(roi_front=(1037,635,56,44), roi_back=(918,623,218,72), threshold=0.65, method="Template matching", file="./tasks/GameUi/page/page_exploration_goto_bondling_fairyland.png") # description @@ -158,7 +158,7 @@ class GameUiAssets: # description I_CHECK_ONMYODO = RuleImage(roi_front=(1166,117,84,547), roi_back=(1166,117,84,547), threshold=0.8, method="Template matching", file="./tasks/GameUi/page/page_check_onmyodo.png") # description - I_CHECK_FRIENDS = RuleImage(roi_front=(1011,592,133,60), roi_back=(1011,592,133,60), threshold=0.8, method="Template matching", file="./tasks/GameUi/page/page_check_friends.png") + I_CHECK_FRIENDS = RuleImage(roi_front=(74,628,58,66), roi_back=(74,628,58,66), threshold=0.8, method="Template matching", file="./tasks/GameUi/page/page_check_friends.png") # description I_CHECK_DAILY = RuleImage(roi_front=(28,515,58,62), roi_back=(1,487,154,145), threshold=0.8, method="Template matching", file="./tasks/GameUi/page/page_check_daily.png") # description diff --git a/tasks/GameUi/page/page_check_friends.png b/tasks/GameUi/page/page_check_friends.png index 3db64b88e..6a4c2e5cc 100644 Binary files a/tasks/GameUi/page/page_check_friends.png and b/tasks/GameUi/page/page_check_friends.png differ diff --git a/tasks/GameUi/page/page_img.json b/tasks/GameUi/page/page_img.json index 70dea66e2..2b0a0aa9d 100644 --- a/tasks/GameUi/page/page_img.json +++ b/tasks/GameUi/page/page_img.json @@ -111,7 +111,7 @@ "itemName": "exploration_goto_six_gates", "imageName": "page_exploration_goto_six_gates.png", "roiFront": "938,640,60,49", - "roiBack": "918,631,200,66", + "roiBack": "820,631,296,66", "method": "Template matching", "threshold": 0.65, "description": "探索前往六道之门" diff --git a/tasks/GameUi/page/page_img_3.json b/tasks/GameUi/page/page_img_3.json index a14eaaf57..4ca56c63c 100644 --- a/tasks/GameUi/page/page_img_3.json +++ b/tasks/GameUi/page/page_img_3.json @@ -92,8 +92,8 @@ { "itemName": "check_friends", "imageName": "page_check_friends.png", - "roiFront": "1011,592,133,60", - "roiBack": "1011,592,133,60", + "roiFront": "74,628,58,66", + "roiBack": "74,628,58,66", "method": "Template matching", "threshold": 0.8, "description": "description" diff --git a/tasks/GuildActivityMonitor/config.py b/tasks/GuildActivityMonitor/config.py index 28a5c2d0d..e39ccca16 100644 --- a/tasks/GuildActivityMonitor/config.py +++ b/tasks/GuildActivityMonitor/config.py @@ -20,6 +20,15 @@ class GuildActivity(BaseModel): # 退治 DemonRetreat: bool = Field(default=True) + # 保持前端翻译,自动转换格式 + def __getattr__(self, name): + camel = ''.join(w.capitalize() for w in name.split('_')) + return getattr(self, camel) if hasattr(self, camel) else super().__getattr__(name) + + def __setattr__(self, name, value): + camel = ''.join(w.capitalize() for w in name.split('_')) + super().__setattr__(camel if hasattr(self, camel) else name, value) + class GuildActivityMonitor(ConfigBase): scheduler: Scheduler = Field(default_factory=Scheduler) diff --git a/tasks/GuildActivityMonitor/script_task.py b/tasks/GuildActivityMonitor/script_task.py index 91be2d3b6..29578ea4e 100644 --- a/tasks/GuildActivityMonitor/script_task.py +++ b/tasks/GuildActivityMonitor/script_task.py @@ -16,6 +16,7 @@ def run(self): :return: """ # 构建关键字映射 + self.set_next_run(task='GuildActivityMonitor', success=True, finish=True) self.ui_get_current_page() self.ui_goto(page_main) guild_config = self.config.guild_activity_monitor.guild_activity @@ -48,7 +49,6 @@ def run(self): while True: if check_timer.reached(): logger.info("监控时间到,任务结束") - self.set_next_run(task='GuildActivityMonitor', success=True, finish=True) raise TaskEnd('GuildActivityMonitor') if log_timer.reached(): @@ -131,4 +131,4 @@ def clear_notifications(self): c = Config('oas1') d = Device(c) t = ScriptTask(c, d) - t.run() \ No newline at end of file + t.run() diff --git a/tasks/MemoryScrolls/script_task.py b/tasks/MemoryScrolls/script_task.py index a74958517..18e679a3e 100644 --- a/tasks/MemoryScrolls/script_task.py +++ b/tasks/MemoryScrolls/script_task.py @@ -61,6 +61,10 @@ def goto_memoryscrolls_main(self, con): logger.info('Small Memory Scrolls fragments reached 50, planning tomorrow exploration') # 安排下次探索 self.custom_next_run(task='Exploration', custom_time=self.config.memory_scrolls.memory_scrolls_finish.next_exploration_time, time_delta=1) + else: + logger.warning('Small Memory Scrolls fragments not reached 50, task failed') + self.set_next_run(task='MemoryScrolls', success=False) + raise TaskEnd self.ui_click_until_smt_disappear(self.I_MS_FRAGMENT_S, stop=self.I_MS_FRAGMENT_S_VERIFICATION, interval=1.5) # 进入指定分卷 self.goto_scroll(con) diff --git a/tasks/Quiz/debug.py b/tasks/Quiz/debug.py index abe1de852..02fc89a7b 100644 --- a/tasks/Quiz/debug.py +++ b/tasks/Quiz/debug.py @@ -3,35 +3,38 @@ # github https://github.com/runhey import re -from cached_property import cached_property from pathlib import Path -from module.logger import logger +from filelock import FileLock def remove_symbols(text): return re.sub(r'[^\w\s]', '', text) + class Debugger: - @cached_property - def fn(self): - # 以添加方式打开一个文件 + def _log_file(self) -> Path: file: Path = Path(f'./log/quiz/supplement.txt') if not file.parent.exists(): file.parent.mkdir(parents=True) if not file.exists(): file.touch() - f = open(str(file), 'a', encoding='utf-8') - return f + return file def append_one(self, question: str, options: list[str]): question = remove_symbols(question) options = [remove_symbols(option) for option in options] - self.fn.write(f'{question},{options[0]},{options[1]},{options[2]},{options[3]}\n') + file = self._log_file() + lock = FileLock(f"{file}.lock") + line = f'{question},{options[0]},{options[1]},{options[2]},{options[3]}\n' + with lock: + with open(file, 'a', encoding='utf-8') as f: + f.write(line) + f.flush() def close_fn(self): - self.fn.close() + return if __name__ == '__main__': diff --git a/tasks/Quiz/script_task.py b/tasks/Quiz/script_task.py index 3f8563681..f8dca5e20 100644 --- a/tasks/Quiz/script_task.py +++ b/tasks/Quiz/script_task.py @@ -20,9 +20,11 @@ from module.atom.click import RuleClick from module.device.screenshot import Screenshot + class NoTicket(Exception): pass + class ScriptTask(GameUi, QuizAssets, ActivityShikigamiAssets, Debugger): answer_cnt = 0 @@ -169,11 +171,6 @@ def _deal_quiz(self): self.answer_cnt += 1 logger.info(f'Question count: {self.answer_cnt}') - # questions = self.O_QUESTION.detect_and_ocr(self.device.image) - # question = ''.join([q.ocr_text for q in questions]) - # question = question.replace('?', '').replace('?', '').replace(' ', '').replace(',', ',') - # question = remove_symbols(question) - index = self.anwser.answer_one(question=question, options=[answer_1, answer_2, answer_3, answer_4]) if index is None: logger.error('Now question has no answer, please check') @@ -186,7 +183,7 @@ def _deal_quiz(self): index_options = {1, 2, 3, 4} index_options.remove(index) index = random.choice(list(index_options)) - logger.info(f'Question: {question}, Answer: {index}{[answer_1, answer_2, answer_3, answer_4]}') + logger.attr(index, 'Answer') self.click(self.click_options[index-1], interval=1) time.sleep(0.5) if index == 1: @@ -203,10 +200,6 @@ def _deal_quiz(self): def detect_question_and_answers(self) -> tuple: results = self.O_QUESTION.detect_and_ocr(self.device.image) question = '' - answer_1 = '' - answer_2 = '' - answer_3 = '' - answer_4 = '' answer_1 = remove_symbols(self.O_ANSWER1.ocr(self.device.image)) answer_2 = remove_symbols(self.O_ANSWER2.ocr(self.device.image)) answer_3 = remove_symbols(self.O_ANSWER3.ocr(self.device.image)) @@ -224,9 +217,6 @@ def detect_question_and_answers(self) -> tuple: return question, answer_1, answer_2, answer_3, answer_4 - - - if __name__ == '__main__': from module.config.config import Config from module.device.device import Device diff --git a/tasks/RichMan/assets.py b/tasks/RichMan/assets.py index cfe1ab3ec..57a5b6200 100644 --- a/tasks/RichMan/assets.py +++ b/tasks/RichMan/assets.py @@ -190,7 +190,7 @@ class RichManAssets: # 特殊 I_SIDE_SURE_SPECIAL = RuleImage(roi_front=(1172,91,70,74), roi_back=(1172,91,70,74), threshold=0.7, method="Template matching", file="./tasks/RichMan/mall/navbar/navbar_side_sure_special.png") # 特殊 - I_SIDE_CHECK_SPECIAL = RuleImage(roi_front=(155,8,42,42), roi_back=(155,8,42,42), threshold=0.7, method="Template matching", file="./tasks/RichMan/mall/navbar/navbar_side_check_special.png") + I_SIDE_CHECK_SPECIAL = RuleImage(roi_front=(0,0,26,34), roi_back=(1075,0,100,100), threshold=0.7, method="Template matching", file="./tasks/RichMan/mall/navbar/navbar_side_check_special.png") # 荣誉 I_SIDE_SUER_HONOR = RuleImage(roi_front=(1180,191,59,60), roi_back=(1150,159,103,132), threshold=0.6, method="Template matching", file="./tasks/RichMan/mall/navbar/navbar_side_suer_honor.png") # 荣誉 diff --git a/tasks/RichMan/mall/navbar/image_side.json b/tasks/RichMan/mall/navbar/image_side.json index 8c6038a1d..1a8104117 100644 --- a/tasks/RichMan/mall/navbar/image_side.json +++ b/tasks/RichMan/mall/navbar/image_side.json @@ -11,8 +11,8 @@ { "itemName": "side_check_special", "imageName": "navbar_side_check_special.png", - "roiFront": "155,8,42,42", - "roiBack": "155,8,42,42", + "roiFront": "0,0,26,34", + "roiBack": "1075,0,100,100", "method": "Template matching", "threshold": 0.7, "description": "特殊" diff --git a/tasks/RichMan/mall/navbar/navbar_side_check_special.png b/tasks/RichMan/mall/navbar/navbar_side_check_special.png index aef0d1fa6..08b321bd5 100644 Binary files a/tasks/RichMan/mall/navbar/navbar_side_check_special.png and b/tasks/RichMan/mall/navbar/navbar_side_check_special.png differ diff --git a/tasks/SixRealms/oas_ocr.py b/tasks/SixRealms/oas_ocr.py index 3a58c81f5..1ddc14bc4 100644 --- a/tasks/SixRealms/oas_ocr.py +++ b/tasks/SixRealms/oas_ocr.py @@ -33,6 +33,12 @@ def rotate_image(image): return image def detect_and_ocr(self, *args, **kwargs): + if getattr(self.model, "is_proxy", False): + params = {"drop_score": 0.1, "box_thresh": 0.2, "vertical": True} + for key in list(params.keys()): + if key in kwargs: + params.pop(key) + return self.model.detect_and_ocr(*args, **params, **kwargs) # Try hard to lower TextSystem.box_thresh backup = self.model.text_detector.box_thresh # Patch text_recognizer diff --git a/tasks/SoulsTidy/assets.py b/tasks/SoulsTidy/assets.py index d963ce00e..2d0f3ee46 100644 --- a/tasks/SoulsTidy/assets.py +++ b/tasks/SoulsTidy/assets.py @@ -52,6 +52,8 @@ class SoulsTidyAssets: I_ST_SOUL_OVERFLOW = RuleImage(roi_front=(447,260,384,42), roi_back=(447,260,384,42), threshold=0.8, method="Template matching", file="./tasks/SoulsTidy/simple/simple_st_soul_overflow.png") # 狗粮御魂的堆叠标识 I_ST_SOUL_STACK = RuleImage(roi_front=(142,234,20,16), roi_back=(142,234,20,16), threshold=0.8, method="Template matching", file="./tasks/SoulsTidy/simple/simple_st_soul_stack.png") + # 御魂关闭状态 + I_ST_SOULS_CLOSE = RuleImage(roi_front=(1180,227,56,83), roi_back=(1166,215,84,107), threshold=0.8, method="Template matching", file="./tasks/SoulsTidy/simple/simple_st_souls_close.png") # Long Click Rule Assets diff --git a/tasks/SoulsTidy/script_task.py b/tasks/SoulsTidy/script_task.py index 5063de24a..0e74e191d 100644 --- a/tasks/SoulsTidy/script_task.py +++ b/tasks/SoulsTidy/script_task.py @@ -42,7 +42,9 @@ def goto_souls(self): continue if self.appear_then_click(self.I_ST_SOULS, interval=1): continue - if self.click(self.C_ST_DETAIL, interval=1.5): + if self.appear_then_click(self.I_ST_SOULS_CLOSE, interval=1): + continue + if self.click(self.C_ST_DETAIL, interval=2): continue # 御魂超过上限的提示 self.ocr_appear_click(self.O_ST_OVERFLOW) diff --git a/tasks/SoulsTidy/simple/image.json b/tasks/SoulsTidy/simple/image.json index b9fa13154..31c76f41f 100644 --- a/tasks/SoulsTidy/simple/image.json +++ b/tasks/SoulsTidy/simple/image.json @@ -151,5 +151,14 @@ "method": "Template matching", "threshold": 0.8, "description": "狗粮御魂的堆叠标识" + }, + { + "itemName": "st_souls_close", + "imageName": "simple_st_souls_close.png", + "roiFront": "1180,227,56,83", + "roiBack": "1166,215,84,107", + "method": "Template matching", + "threshold": 0.8, + "description": "御魂关闭状态" } ] \ No newline at end of file diff --git a/tasks/SoulsTidy/simple/simple_st_souls_close.png b/tasks/SoulsTidy/simple/simple_st_souls_close.png new file mode 100644 index 000000000..2403abe4e Binary files /dev/null and b/tasks/SoulsTidy/simple/simple_st_souls_close.png differ diff --git a/tasks/WantedQuests/assets.py b/tasks/WantedQuests/assets.py index 9d1d2b090..a9a474d56 100644 --- a/tasks/WantedQuests/assets.py +++ b/tasks/WantedQuests/assets.py @@ -141,7 +141,7 @@ class WantedQuestsAssets: # 封印 I_WQ_SEAL = RuleImage(roi_front=(174,184,20,29), roi_back=(56,93,664,455), threshold=0.8, method="Template matching", file="./tasks/WantedQuests/wq/wq_wq_seal.png") # 勾号 - I_WQ_DONE = RuleImage(roi_front=(248,183,37,39), roi_back=(107,147,570,389), threshold=0.8, method="Template matching", file="./tasks/WantedQuests/wq/wq_wq_done.png") + I_WQ_DONE = RuleImage(roi_front=(248,183,37,39), roi_back=(63,134,624,401), threshold=0.8, method="Template matching", file="./tasks/WantedQuests/wq/wq_wq_done.png") # 一键追踪 I_TRACE_ENABLE = RuleImage(roi_front=(1097,588,101,70), roi_back=(1097,588,101,70), threshold=0.8, method="Template matching", file="./tasks/WantedQuests/wq/wq_trace_enable.png") # 取消追踪 diff --git a/tasks/WantedQuests/script_task.py b/tasks/WantedQuests/script_task.py index 6b5be4c42..c3491c9ae 100644 --- a/tasks/WantedQuests/script_task.py +++ b/tasks/WantedQuests/script_task.py @@ -203,7 +203,7 @@ def pre_work(self): break if self.appear_then_click(self.I_WQ_SEAL, interval=1): continue - if self.appear_then_click(self.I_WQ_DONE, interval=1): + if self.appear_then_click_multi_scale(self.I_WQ_DONE,scale_range=(0.8, 1.2) ,interval=1): continue if self.appear_then_click(self.I_TRACE_ENABLE, interval=1): continue @@ -242,7 +242,7 @@ def pre_work_cooperation_only(self): break if self.appear_then_click(self.I_WQ_SEAL, interval=1): continue - if self.appear_then_click(self.I_WQ_DONE, interval=1): + if self.appear_then_click_multi_scale(self.I_WQ_DONE,scale_range=(0.8, 1.2), interval=1): continue if self.special_main and self.click(self.C_SPECIAL_MAIN, interval=3): logger.info('Click special main left to find wanted quests') diff --git a/tasks/WantedQuests/wq/image.json b/tasks/WantedQuests/wq/image.json index 48391aa2a..1338493f2 100644 --- a/tasks/WantedQuests/wq/image.json +++ b/tasks/WantedQuests/wq/image.json @@ -12,7 +12,7 @@ "itemName": "wq_done", "imageName": "wq_wq_done.png", "roiFront": "248,183,37,39", - "roiBack": "107,147,570,389", + "roiBack": "63,134,624,401", "method": "Template matching", "threshold": 0.8, "description": "勾号" diff --git a/tasks/WeeklyTrifles/area_boss/image.json b/tasks/WeeklyTrifles/area_boss/image.json index bc810e0f0..caeabc5c8 100644 --- a/tasks/WeeklyTrifles/area_boss/image.json +++ b/tasks/WeeklyTrifles/area_boss/image.json @@ -39,7 +39,7 @@ "itemName": "wt_ab_wechat", "imageName": "area_boss_wt_ab_wechat.png", "roiFront": "920,646,50,41", - "roiBack": "829,620,172,85", + "roiBack": "760,620,240,85", "method": "Template matching", "threshold": 0.8, "description": "微信分享" diff --git a/tasks/WeeklyTrifles/assets.py b/tasks/WeeklyTrifles/assets.py index dbaf2ebfd..b5ba821e6 100644 --- a/tasks/WeeklyTrifles/assets.py +++ b/tasks/WeeklyTrifles/assets.py @@ -25,7 +25,7 @@ class WeeklyTriflesAssets: # 分享勾玉 I_WT_AB_JADE = RuleImage(roi_front=(977,552,44,47), roi_back=(977,552,44,47), threshold=0.8, method="Template matching", file="./tasks/WeeklyTrifles/area_boss/area_boss_wt_ab_jade.png") # 微信分享 - I_WT_AB_WECHAT = RuleImage(roi_front=(920,646,50,41), roi_back=(829,620,172,85), threshold=0.8, method="Template matching", file="./tasks/WeeklyTrifles/area_boss/area_boss_wt_ab_wechat.png") + I_WT_AB_WECHAT = RuleImage(roi_front=(920,646,50,41), roi_back=(760,620,240,85), threshold=0.8, method="Template matching", file="./tasks/WeeklyTrifles/area_boss/area_boss_wt_ab_wechat.png") # Image Rule Assets @@ -76,7 +76,7 @@ class WeeklyTriflesAssets: # 秘闻分享 I_WT_SE_SHARE = RuleImage(roi_front=(911,570,46,43), roi_back=(886,547,92,95), threshold=0.8, method="Template matching", file="./tasks/WeeklyTrifles/secret/secret_wt_se_share.png") # 微信 - I_WT_SE_WECHAT = RuleImage(roi_front=(823,641,45,37), roi_back=(786,601,148,89), threshold=0.8, method="Template matching", file="./tasks/WeeklyTrifles/secret/secret_wt_se_wechat.png") + I_WT_SE_WECHAT = RuleImage(roi_front=(823,641,45,37), roi_back=(700,601,220,89), threshold=0.8, method="Template matching", file="./tasks/WeeklyTrifles/secret/secret_wt_se_wechat.png") # 勾玉 I_WT_SE_JADE = RuleImage(roi_front=(1126,533,35,39), roi_back=(1118,525,50,55), threshold=0.8, method="Template matching", file="./tasks/WeeklyTrifles/secret/secret_wt_se_jade.png") # 排行 diff --git a/tasks/WeeklyTrifles/secret/image.json b/tasks/WeeklyTrifles/secret/image.json index 5a8706e9c..ad195bba3 100644 --- a/tasks/WeeklyTrifles/secret/image.json +++ b/tasks/WeeklyTrifles/secret/image.json @@ -21,7 +21,7 @@ "itemName": "wt_se_wechat", "imageName": "secret_wt_se_wechat.png", "roiFront": "823,641,45,37", - "roiBack": "786,601,148,89", + "roiBack": "700,601,220,89", "method": "Template matching", "threshold": 0.8, "description": "微信"