emo-online-api / emo /desktop /actions /screen_processor.py
Emo Online
Deploy Emo API
dd87944
Raw
History Blame Contribute Delete
4.72 kB
"""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))