r"""Reproduce /ss and /resume Telegram screenshot pipelines for offline inspection. Brings the `m2d` window to front (Win32 trick — same as live loop), captures via mss using the active config's chart_window_region, then runs the EXACT same annotators that /ss and /resume use: - _save_inspect_frame → /ss path (top-3 dots per detected strip + caption) - _save_annotated_frame → /resume path (cyan rect on dot_roi/sub_roi) Outputs are saved under logs/repro/ alongside a JSON summary of detections. Usage: .\.venv\Scripts\python.exe scripts\repro_ss_resume.py .\.venv\Scripts\python.exe scripts\repro_ss_resume.py --no-focus """ from __future__ import annotations import argparse import json import sys import time from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SRC = ROOT / "src" if str(SRC) not in sys.path: sys.path.insert(0, str(SRC)) import cv2 # noqa: E402 import numpy as np # noqa: E402 from atm.config import Config # noqa: E402 from atm.layout import detect_strips # noqa: E402 from atm.main import ( # noqa: E402 _focus_window_by_title, _format_inspect_caption, _save_annotated_frame, _save_inspect_frame, ) from atm.vision import crop_roi # noqa: E402 def _capture_via_region(cfg) -> np.ndarray | None: import mss # type: ignore[import-untyped] reg = cfg.chart_window_region if reg is None: # Fallback: grab full primary monitor with mss.mss() as sct: mon = sct.monitors[1] img = sct.grab(mon) return cv2.cvtColor(np.array(img), cv2.COLOR_BGRA2BGR) with mss.mss() as sct: mon = {"top": reg.y, "left": reg.x, "width": reg.w, "height": reg.h} img = sct.grab(mon) return cv2.cvtColor(np.array(img), cv2.COLOR_BGRA2BGR) def main() -> int: p = argparse.ArgumentParser() p.add_argument("--no-focus", action="store_true", help="Skip Win32 focus call.") p.add_argument("--delay", type=float, default=0.5, help="Seconds to sleep after focus.") p.add_argument( "--out", type=Path, default=ROOT / "logs" / "repro", help="Output directory for annotated frames + JSON.", ) args = p.parse_args() cfg = Config.load_current(ROOT / "configs") args.out.mkdir(parents=True, exist_ok=True) # 1) Focus if not args.no_focus and cfg.window_title: title = _focus_window_by_title(cfg.window_title) print(f"[focus] needle={cfg.window_title!r} -> {title!r}") if args.delay > 0: time.sleep(args.delay) # 2) Capture frame = _capture_via_region(cfg) if frame is None: print("[capture] FAILED — no frame") return 1 print(f"[capture] frame shape={frame.shape}") now = time.time() ts_str = time.strftime("%Y%m%d_%H%M%S") # Save raw too so we can hand-inspect what mss actually grabbed raw_path = args.out / f"{ts_str}_raw.png" cv2.imwrite(str(raw_path), frame) print(f"[raw] {raw_path}") # 3) Detect strips (same logic as live multi-chart split) strip_h = cfg.dot_roi.h min_strip_px = max(150, strip_h * 8) min_gap_px = max(20, int(strip_h * 0.8)) palette = { name: (spec.rgb, spec.tolerance) for name, spec in cfg.colors.items() if name != "background" } full_dot_crop = crop_roi(frame, cfg.dot_roi) raw_strips = detect_strips(full_dot_crop, palette, min_gap_px, min_strip_px) # Translate back to absolute frame coords from atm.config import ROI # noqa: E402 strips = [ ROI(x=cfg.dot_roi.x + r.x, y=cfg.dot_roi.y + r.y, w=r.w, h=r.h) for r in raw_strips ] print(f"[strips] dot_roi={cfg.dot_roi} detected={len(strips)} strips") for i, s in enumerate(strips): print(f" strip[{i}] x={s.x} y={s.y} w={s.w} h={s.h}") # Also dump a copy of the full dot_roi crop for visual sanity-check crop_path = args.out / f"{ts_str}_dot_roi_crop.png" cv2.imwrite(str(crop_path), full_dot_crop) print(f"[crop] {crop_path}") # 4) /ss inspect-annotate (top-3 per strip, FSM-pick markers) inspect_path, detections = _save_inspect_frame( frame, cfg, args.out, now, audit=None, strips=strips if strips else None, ) caption = _format_inspect_caption(detections, cfg) print(f"[ss] {inspect_path}") print(f"[ss] caption:\n{caption}") # 5) /resume annotate (cyan rect on dot_roi or first strip) roi_for_resume = strips[0] if strips else cfg.dot_roi resume_path = _save_annotated_frame( frame, cfg, args.out, "resume_repro", now, audit=None, roi=roi_for_resume, ) print(f"[resume] {resume_path}") # 6) JSON summary summary = { "ts": ts_str, "frame_shape": list(frame.shape), "dot_roi": {"x": cfg.dot_roi.x, "y": cfg.dot_roi.y, "w": cfg.dot_roi.w, "h": cfg.dot_roi.h}, "strips": [ {"x": s.x, "y": s.y, "w": s.w, "h": s.h} for s in strips ], "detections": [ { "strip_idx": d["strip_idx"], "idx": d["idx"], "name": d["name"], "rgb": list(d["rgb"]), "distance": round(float(d["distance"]), 3), "confidence": round(float(d["confidence"]), 3), "pos_abs": list(d["pos_abs"]), } for d in detections ], "files": { "raw": str(raw_path), "dot_roi_crop": str(crop_path), "inspect": str(inspect_path) if inspect_path else None, "resume": str(resume_path) if resume_path else None, }, "ss_caption": caption, } json_path = args.out / f"{ts_str}_summary.json" json_path.write_text(json.dumps(summary, indent=2), encoding="utf-8") print(f"[json] {json_path}") return 0 if __name__ == "__main__": raise SystemExit(main())