from __future__ import annotations import argparse import json from pathlib import Path from botty_next.capture.mss_backend import MssCaptureBackend, save_frame from botty_next.capture.window import find_window_region from botty_next.config import load_config from botty_next.vision.fixtures import load_image from botty_next.vision.ocr import run_tesseract_ocr, save_ocr_preprocess_debug from botty_next.vision.template_matching import match_template, save_match_debug def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(prog="botty-next") subparsers = parser.add_subparsers(dest="command", required=True) config_parser = subparsers.add_parser("config") config_subparsers = config_parser.add_subparsers(dest="config_command", required=True) validate_parser = config_subparsers.add_parser("validate") validate_parser.add_argument("-c", "--config", required=True, type=Path) validate_parser.set_defaults(handler=validate_config) detect_parser = subparsers.add_parser("detect") detect_parser.add_argument("detector", choices=["template"], help="detector to run") detect_parser.add_argument("--image", required=True, type=Path) detect_parser.add_argument("--template", required=True, type=Path) detect_parser.add_argument("--threshold", type=float, default=0.85) detect_parser.add_argument("--debug-output", type=Path) detect_parser.set_defaults(handler=detect) capture_parser = subparsers.add_parser("capture") capture_parser.add_argument("--output", required=True, type=Path) capture_parser.add_argument("--window-title", type=str) capture_parser.set_defaults(handler=capture) ocr_parser = subparsers.add_parser("ocr") ocr_parser.add_argument("--image", required=True, type=Path) ocr_parser.add_argument("--lang", default="eng") ocr_parser.add_argument("--psm", type=int, default=6) ocr_parser.add_argument("--tesseract-cmd") ocr_parser.add_argument("--debug-output", type=Path) ocr_parser.set_defaults(handler=ocr) return parser def validate_config(args: argparse.Namespace) -> int: config = load_config(args.config) print(json.dumps(config.model_dump(mode="json"), indent=2)) return 0 def detect(args: argparse.Namespace) -> int: image = load_image(args.image) template = load_image(args.template) result = match_template(image, template, threshold=args.threshold) if args.debug_output: save_match_debug(image, result, args.debug_output) print(json.dumps(_result_to_dict(result), indent=2)) return 0 if result.passed else 1 def capture(args: argparse.Namespace) -> int: region = find_window_region(args.window_title) if args.window_title else None frame = MssCaptureBackend().grab(region) output = save_frame(frame, args.output) print( json.dumps( { "output": str(output), "shape": tuple(map(int, frame.shape)), "window": region.title if region else None, }, indent=2, ) ) return 0 def ocr(args: argparse.Namespace) -> int: image = load_image(args.image) if args.debug_output: save_ocr_preprocess_debug(image, args.debug_output) try: result = run_tesseract_ocr( image, lang=args.lang, psm=args.psm, tesseract_cmd=args.tesseract_cmd, ) except RuntimeError as exc: print( json.dumps( { "error": str(exc), "debug_output": str(args.debug_output) if args.debug_output else None, }, indent=2, ) ) return 2 print(json.dumps(_ocr_result_to_dict(result), indent=2)) return 0 def _result_to_dict(result) -> dict: return { "confidence": result.confidence, "bbox": result.bbox, "passed": result.passed, "method": result.method, "debug": result.debug, } def _ocr_result_to_dict(result) -> dict: return { "text": result.text, "confidence": result.confidence, "bbox": result.bbox, "debug": result.debug, } def main(argv: list[str] | None = None) -> int: parser = build_parser() args = parser.parse_args(argv) return args.handler(args) if __name__ == "__main__": raise SystemExit(main())