import pyautogui import pygetwindow as gw from common.config_loader import loadOcrConfig from common.project_paths import RUNTIME_DIR SCREENSHOT_PATH = RUNTIME_DIR / "latest_screenshot.png" OCR_SCREENSHOT_PATH = RUNTIME_DIR / "latest_ocr.png" EXCLUDED_WINDOW_KEYWORDS = [ "Google Chrome", "Microsoft Edge", "Mozilla Firefox", "Visual Studio Code", "PowerShell", "Windows Terminal", "ChatGPT", ] def log(level, message): from datetime import datetime now = datetime.now().strftime("%H:%M:%S") print(f"[{level}] {now} {message}", flush=True) def findVrchatWindow(): windows = gw.getAllWindows() candidates = [] for window in windows: title = window.title or "" normalized = title.strip().lower() if not normalized: continue if any(keyword.lower() in normalized for keyword in EXCLUDED_WINDOW_KEYWORDS): continue if normalized == "vrchat": candidates.append((0, window)) continue if normalized.startswith("vrchat "): candidates.append((1, window)) continue if normalized.startswith("vrchat"): candidates.append((2, window)) if not candidates: visible_titles = [] for window in windows: title = (window.title or "").strip() if title: visible_titles.append(title) log("ERROR", "VRChatウィンドウが見つかりません") log("CHECK", "現在検出できるウィンドウ") for title in visible_titles[:30]: print(title, flush=True) raise RuntimeError("VRChatウィンドウが見つかりません") candidates = sorted( candidates, key=lambda item: (item[0], -(item[1].width * item[1].height)), ) window = candidates[0][1] if window.width <= 0 or window.height <= 0: raise RuntimeError("VRChatウィンドウサイズが不正です") return window def getVrchatRegion(): window = findVrchatWindow() try: if window.isMinimized: window.restore() except Exception: pass left = int(window.left) top = int(window.top) width = int(window.width) height = int(window.height) if width <= 0 or height <= 0: raise RuntimeError("VRChatウィンドウ範囲が不正です") log("INFO", f"VRChat window title={window.title}") log("INFO", f"VRChat region left={left} top={top} width={width} height={height}") return left, top, width, height def clampRatio(value): return max(0.0, min(1.0, value)) def getOcrRegion(): left, top, width, height = getVrchatRegion() config = loadOcrConfig() # Default to the lower central area. It is much faster than OCRing the # whole VRChat window and usually contains chat/subtitle text. crop_left = clampRatio(config["crop_left"]) crop_top = clampRatio(config["crop_top"]) crop_right = clampRatio(config["crop_right"]) crop_bottom = clampRatio(config["crop_bottom"]) if crop_right <= crop_left: crop_left = 0.0 crop_right = 1.0 if crop_bottom <= crop_top: crop_top = 0.0 crop_bottom = 1.0 region_left = left + int(width * crop_left) region_top = top + int(height * crop_top) region_width = max(1, int(width * (crop_right - crop_left))) region_height = max(1, int(height * (crop_bottom - crop_top))) log( "INFO", f"OCR crop left={region_left} top={region_top} width={region_width} height={region_height}", ) return region_left, region_top, region_width, region_height def captureRegionTo(image_path, region): image_path.parent.mkdir(parents=True, exist_ok=True) left, top, width, height = region image = pyautogui.screenshot(region=(left, top, width, height)) image.save(image_path) log("ACTION", f"captureVrchatWindow saved={image_path}") return image_path def captureVrchatWindowTo(image_path): return captureRegionTo( image_path, getVrchatRegion(), ) def captureVrchatWindow(): return captureVrchatWindowTo(SCREENSHOT_PATH) def captureOcrRegion(): return captureRegionTo( OCR_SCREENSHOT_PATH, getOcrRegion(), ) def captureScreen(): return captureVrchatWindow() def takeScreenshot(): return captureVrchatWindow()