Spaces:
Sleeping
Sleeping
| """Capture écran / webcam — port Mark-XLVIII.""" | |
| from __future__ import annotations | |
| import io | |
| import json | |
| import sys | |
| from pathlib import Path | |
| from typing import Any | |
| from emo.desktop.actions._base import SkillResult | |
| from emo.desktop.config import load_config | |
| _IMG_MAX_W = 1280 | |
| _IMG_MAX_H = 720 | |
| _JPEG_Q = 82 | |
| try: | |
| import cv2 | |
| import numpy as np | |
| _CV2 = True | |
| except ImportError: | |
| _CV2 = False | |
| np = None # type: ignore | |
| try: | |
| import mss | |
| import mss.tools | |
| _MSS = True | |
| except ImportError: | |
| _MSS = False | |
| try: | |
| import PIL.Image | |
| _PIL = True | |
| except ImportError: | |
| _PIL = False | |
| _OUT = Path(__file__).resolve().parent.parent / "data" / "screenshots" | |
| def _get_os() -> str: | |
| return str(load_config().get("os_system") or "windows").lower() | |
| def _compress(img_bytes: bytes, source_format: str = "PNG") -> tuple[bytes, str]: | |
| if not _PIL: | |
| return img_bytes, f"image/{source_format.lower()}" | |
| try: | |
| img = PIL.Image.open(io.BytesIO(img_bytes)).convert("RGB") | |
| img.thumbnail((_IMG_MAX_W, _IMG_MAX_H), PIL.Image.BILINEAR) | |
| buf = io.BytesIO() | |
| img.save(buf, format="JPEG", quality=_JPEG_Q, optimize=False) | |
| return buf.getvalue(), "image/jpeg" | |
| except Exception: | |
| return img_bytes, f"image/{source_format.lower()}" | |
| def capture_screen() -> tuple[bytes, str]: | |
| if not _MSS: | |
| try: | |
| import pyautogui | |
| _OUT.mkdir(parents=True, exist_ok=True) | |
| dest = _OUT / "screen_fallback.png" | |
| img = pyautogui.screenshot() | |
| img.save(dest) | |
| return _compress(dest.read_bytes(), "PNG") | |
| except ImportError as e: | |
| raise RuntimeError("Installez mss ou pyautogui pour les captures") from e | |
| with mss.mss() as sct: | |
| monitors = sct.monitors | |
| target = monitors[1] if len(monitors) > 1 else monitors[0] | |
| shot = sct.grab(target) | |
| png = mss.tools.to_png(shot.rgb, shot.size) | |
| return _compress(png, "PNG") | |
| def _cv2_backend() -> int: | |
| if not _CV2: | |
| return 0 | |
| os_name = _get_os() | |
| if os_name == "windows": | |
| return cv2.CAP_DSHOW | |
| if os_name == "mac": | |
| return cv2.CAP_AVFOUNDATION | |
| return cv2.CAP_ANY | |
| def _probe_camera(index: int, backend: int, warmup: int = 5) -> bool: | |
| if not _CV2: | |
| return False | |
| cap = cv2.VideoCapture(index, backend) | |
| if not cap.isOpened(): | |
| cap.release() | |
| return False | |
| for _ in range(warmup): | |
| cap.read() | |
| ret, frame = cap.read() | |
| cap.release() | |
| if not ret or frame is None: | |
| return False | |
| return bool(np.mean(frame) > 8) | |
| def _detect_camera_index() -> int: | |
| backend = _cv2_backend() | |
| for idx in range(6): | |
| if _probe_camera(idx, backend): | |
| return idx | |
| return 0 | |
| def _get_camera_index() -> int: | |
| cfg = load_config() | |
| if "camera_index" in cfg: | |
| return int(cfg["camera_index"]) | |
| return _detect_camera_index() | |
| def capture_camera() -> tuple[bytes, str]: | |
| if not _CV2: | |
| raise RuntimeError("OpenCV (cv2) requis pour la webcam") | |
| index = _get_camera_index() | |
| backend = _cv2_backend() | |
| cap = cv2.VideoCapture(index, backend) | |
| if not cap.isOpened(): | |
| raise RuntimeError(f"Camera index {index} could not be opened.") | |
| for _ in range(10): | |
| cap.read() | |
| ret, frame = cap.read() | |
| cap.release() | |
| if not ret or frame is None: | |
| raise RuntimeError("Camera returned no frame.") | |
| if _PIL: | |
| rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) | |
| img = PIL.Image.fromarray(rgb) | |
| img.thumbnail((_IMG_MAX_W, _IMG_MAX_H), PIL.Image.BILINEAR) | |
| buf = io.BytesIO() | |
| img.save(buf, format="JPEG", quality=_JPEG_Q) | |
| return buf.getvalue(), "image/jpeg" | |
| _, buf = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, _JPEG_Q]) | |
| return buf.tobytes(), "image/jpeg" | |
| def run(args: dict) -> Any: | |
| angle = (args.get("angle") or "screen").lower().strip() | |
| text = (args.get("text") or args.get("prompt") or "").strip() | |
| try: | |
| if angle == "camera": | |
| img_b, mime = capture_camera() | |
| label = "camera" | |
| else: | |
| img_b, mime = capture_screen() | |
| label = "screen" | |
| _OUT.mkdir(parents=True, exist_ok=True) | |
| ext = "jpg" if mime == "image/jpeg" else "png" | |
| dest = _OUT / f"{label}_capture.{ext}" | |
| dest.write_bytes(img_b) | |
| return SkillResult.ok( | |
| path=str(dest), | |
| bytes=len(img_b), | |
| mime=mime, | |
| message=f"Capture {label}: {len(img_b):,} octets — {text[:60] or 'analyse en cours'}", | |
| ) | |
| except Exception as e: | |
| return SkillResult.fail(str(e)) | |