import argparse import ctypes import ctypes.wintypes import json import os import platform import signal import subprocess import threading import time import webbrowser from datetime import datetime, timezone from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from io import BytesIO from pathlib import Path import numpy as np import onnxruntime as ort from PIL import Image, ImageDraw try: from PIL import ImageGrab except ImportError: ImageGrab = None try: import Quartz except ImportError: Quartz = None IS_WINDOWS = platform.system() == "Windows" IS_DARWIN = platform.system() == "Darwin" DWMWA_EXTENDED_FRAME_BOUNDS = 9 PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 if IS_WINDOWS: ctypes.windll.kernel32.OpenProcess.restype = ctypes.wintypes.HANDLE ctypes.windll.kernel32.QueryFullProcessImageNameW.argtypes = [ ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD, ctypes.wintypes.LPWSTR, ctypes.POINTER(ctypes.wintypes.DWORD), ] ctypes.windll.user32.EnumWindows.argtypes = [ctypes.c_void_p, ctypes.wintypes.LPARAM] from ffmpeg_realtime_detect import ( CLASS_NAMES, MODEL_PATH, detect, detection_event, draw_detections, image_to_jpeg_bytes, no_detection_event, ) class StreamState: def __init__(self): self.condition = threading.Condition() self.jpeg = None self.event = None self.sequence = 0 def update(self, jpeg, event): with self.condition: self.jpeg = jpeg self.event = event self.sequence += 1 self.condition.notify_all() def snapshot(self): with self.condition: return self.jpeg, self.event, self.sequence def parse_args(): parser = argparse.ArgumentParser( description="Capture only the WeChat window, run ONNX, and expose a live stream." ) parser.add_argument("--model", default=str(MODEL_PATH), help="ONNX model path") parser.add_argument("--fps", type=float, default=2.0, help="capture FPS") parser.add_argument( "--infer-interval", type=float, default=3.0, help="seconds between ONNX inference runs; preview frames reuse the latest detections", ) parser.add_argument( "--log-every", type=int, default=60, help="log every N preview frames in addition to inference frames; 0 disables periodic logs", ) parser.add_argument("--confidence", type=float, default=0.05, help="minimum score") parser.add_argument("--target-class", type=int, default=0, help="target class id") parser.add_argument("--host", default="127.0.0.1", help="HTTP host") parser.add_argument("--port", type=int, default=8765, help="HTTP port") parser.add_argument("--max-frames", type=int, default=0, help="stop after N frames; 0 runs forever") parser.add_argument("--open-browser", action="store_true", help="open the live preview in browser") parser.add_argument("--open-ffplay", action="store_true", help="open ffplay for the MJPEG stream") return parser.parse_args() def utc_now(): return datetime.now(timezone.utc).isoformat() def emit(event): print(json.dumps(event, ensure_ascii=False, separators=(",", ":")), flush=True) def _window_matches_wechat(owner, title): owner = owner or "" title = title or "" owner_lower = owner.lower() title_lower = title.lower() return ( owner in {"微信", "WeChat"} or owner_lower in {"wechat", "wechat.exe", "weixin", "weixin.exe"} or "wechat" in owner_lower or title == "微信" or "wechat" in title_lower ) def _windows_text(hwnd): user32 = ctypes.windll.user32 length = user32.GetWindowTextLengthW(hwnd) if length <= 0: return "" buffer = ctypes.create_unicode_buffer(length + 1) copied = user32.GetWindowTextW(hwnd, buffer, length + 1) if copied <= 0: return "" return buffer.value.strip() def _windows_process_name(pid): if not pid: return "" kernel32 = ctypes.windll.kernel32 handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid) if not handle: return "" try: size = ctypes.wintypes.DWORD(32768) buffer = ctypes.create_unicode_buffer(size.value) ok = kernel32.QueryFullProcessImageNameW(handle, 0, buffer, ctypes.byref(size)) if not ok: return "" return os.path.basename(buffer.value) finally: kernel32.CloseHandle(handle) def _windows_rect(hwnd): rect = ctypes.wintypes.RECT() try: result = ctypes.windll.dwmapi.DwmGetWindowAttribute( hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, ctypes.byref(rect), ctypes.sizeof(rect), ) except AttributeError: result = -1 if result != 0: ok = ctypes.windll.user32.GetWindowRect(hwnd, ctypes.byref(rect)) if not ok: return None if rect.right <= rect.left or rect.bottom <= rect.top: return None return rect def _find_windows_wechat_window(): user32 = ctypes.windll.user32 windows = [] enum_proc = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.wintypes.HWND, ctypes.wintypes.LPARAM) @enum_proc def callback(hwnd, _lparam): if not user32.IsWindowVisible(hwnd) or user32.IsIconic(hwnd): return True rect = _windows_rect(hwnd) if rect is None: return True width = rect.right - rect.left height = rect.bottom - rect.top if width < 80 or height < 80: return True title = _windows_text(hwnd) pid = ctypes.wintypes.DWORD() user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid)) owner = _windows_process_name(pid.value) if not _window_matches_wechat(owner, title): return True windows.append( { "id": int(hwnd), "owner": owner or "Windows", "title": title, "x": float(rect.left), "y": float(rect.top), "width": float(width), "height": float(height), "platform": "windows", "pid": int(pid.value) if pid.value else None, } ) return True user32.EnumWindows(callback, 0) if not windows: return None windows.sort(key=lambda item: (item["owner"].lower() != "wechat.exe", -item["width"] * item["height"])) return windows[0] def _capture_windows_window_image(window_id): if ImageGrab is None: raise RuntimeError("Pillow ImageGrab is required for Windows window capture") rect = _windows_rect(ctypes.wintypes.HWND(int(window_id))) if rect is None: return None bbox = (rect.left, rect.top, rect.right, rect.bottom) try: image = ImageGrab.grab(bbox=bbox, all_screens=True) except TypeError: image = ImageGrab.grab(bbox=bbox) return image.convert("RGB") def find_wechat_window(): if IS_WINDOWS: return _find_windows_wechat_window() if not IS_DARWIN or Quartz is None: return None options = Quartz.kCGWindowListOptionOnScreenOnly | Quartz.kCGWindowListExcludeDesktopElements windows = Quartz.CGWindowListCopyWindowInfo(options, Quartz.kCGNullWindowID) or [] for window in windows: owner = window.get(Quartz.kCGWindowOwnerName, "") or "" title = window.get(Quartz.kCGWindowName, "") or "" layer = window.get(Quartz.kCGWindowLayer, -1) if layer != 0: continue if _window_matches_wechat(owner, title): bounds = window.get(Quartz.kCGWindowBounds, {}) or {} return { "id": int(window.get(Quartz.kCGWindowNumber)), "owner": owner, "title": title, "x": float(bounds.get("X", 0)), "y": float(bounds.get("Y", 0)), "width": float(bounds.get("Width", 0)), "height": float(bounds.get("Height", 0)), "platform": "darwin", } return None def capture_window_image(window_id): if IS_WINDOWS: return _capture_windows_window_image(window_id) if not IS_DARWIN or Quartz is None: return None image_ref = Quartz.CGWindowListCreateImage( Quartz.CGRectNull, Quartz.kCGWindowListOptionIncludingWindow, window_id, Quartz.kCGWindowImageBoundsIgnoreFraming, ) if image_ref is None: return None width = Quartz.CGImageGetWidth(image_ref) height = Quartz.CGImageGetHeight(image_ref) bytes_per_row = Quartz.CGImageGetBytesPerRow(image_ref) provider = Quartz.CGImageGetDataProvider(image_ref) data = Quartz.CGDataProviderCopyData(provider) array = np.frombuffer(data, dtype=np.uint8) array = array.reshape((height, bytes_per_row))[:, : width * 4] bgra = array.reshape((height, width, 4)) rgba = bgra[:, :, [2, 1, 0, 3]] return Image.fromarray(rgba, "RGBA").convert("RGB") def screen_event(event, window): event = dict(event) event["window"] = window frame_width, frame_height = event.get("frame_size") or [window["width"], window["height"]] scale_x = frame_width / max(window["width"], 1) scale_y = frame_height / max(window["height"], 1) if event.get("bbox_screen"): x1, y1, x2, y2 = event["bbox_screen"] event["bbox_image"] = [x1, y1, x2, y2] wx1, wy1, wx2, wy2 = x1 / scale_x, y1 / scale_y, x2 / scale_x, y2 / scale_y event["bbox_window"] = [round(wx1, 2), round(wy1, 2), round(wx2, 2), round(wy2, 2)] event["bbox_screen"] = [ round(window["x"] + wx1, 2), round(window["y"] + wy1, 2), round(window["x"] + wx2, 2), round(window["y"] + wy2, 2), ] if event.get("center_screen"): cx, cy = event["center_screen"] event["center_image"] = [cx, cy] wcx, wcy = cx / scale_x, cy / scale_y event["center_window"] = [round(wcx, 2), round(wcy, 2)] event["center_screen"] = [round(window["x"] + wcx, 2), round(window["y"] + wcy, 2)] return event def event_from_detections(detections, frame_index, frame_size, class_name, target_class): if len(detections) > 0: best = detections[np.argmax(detections[:, 4])] return detection_event(best, frame_index, frame_size, class_name) return no_detection_event(frame_index, frame_size, target_class, class_name) def draw_overlay(image, event): draw = ImageDraw.Draw(image) text = f"frame={event.get('frame_index', '-')} {event.get('timestamp', '')}" draw.rectangle((12, 12, 720, 56), fill=(0, 0, 0)) draw.text((22, 24), text, fill=(80, 255, 120)) return image def html_page(): return """