feat: agent Rust Phase 1 — POC headless fonctionnel

1527 lignes Rust, compile sans warnings, testé sur Linux.
- Capture d'écran (xcap) + JPEG base64 + hash dedup
- Heartbeat toutes les 5s vers streaming server
- Poll replay + exécution actions (clic, frappe, combos)
- Serveur HTTP port 5006 (capture, health, file-action)
- Compatible avec le streaming server Python existant

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-03-18 20:22:04 +01:00
parent 792cc2aa9a
commit 757432ee19
10 changed files with 1702 additions and 0 deletions

111
agent_rust/src/capture.rs Normal file
View File

@@ -0,0 +1,111 @@
//! Capture d'écran via xcap.
//!
//! Fournit la capture du moniteur principal, l'encodage JPEG en base64,
//! et un hash perceptuel rapide pour la déduplication des heartbeats.
use base64::Engine;
use image::codecs::jpeg::JpegEncoder;
use image::DynamicImage;
use std::io::Cursor;
/// Capture le moniteur principal et retourne un DynamicImage.
///
/// Utilise xcap pour la capture cross-platform (DXGI sur Windows, X11/Wayland sur Linux).
pub fn capture_screenshot() -> Option<DynamicImage> {
let monitors = match xcap::Monitor::all() {
Ok(m) => m,
Err(e) => {
eprintln!("[CAPTURE] Erreur enumeration moniteurs : {}", e);
return None;
}
};
let primary = monitors
.into_iter()
.find(|m| m.is_primary().unwrap_or(false));
let monitor = match primary {
Some(m) => m,
None => {
eprintln!("[CAPTURE] Aucun moniteur principal trouve");
return None;
}
};
match monitor.capture_image() {
Ok(rgba_image) => Some(DynamicImage::ImageRgba8(rgba_image)),
Err(e) => {
eprintln!("[CAPTURE] Erreur capture ecran : {}", e);
None
}
}
}
/// Encode une image en JPEG et retourne le résultat en base64.
///
/// La qualité doit être entre 1 (mauvaise) et 100 (excellente).
/// 85 est un bon compromis taille/qualité pour le streaming réseau.
pub fn screenshot_to_jpeg_base64(img: &DynamicImage, quality: u8) -> String {
let rgb = img.to_rgb8();
let mut buffer = Cursor::new(Vec::new());
let mut encoder = JpegEncoder::new_with_quality(&mut buffer, quality);
if let Err(e) = encoder.encode(
rgb.as_raw(),
rgb.width(),
rgb.height(),
image::ExtendedColorType::Rgb8,
) {
eprintln!("[CAPTURE] Erreur encodage JPEG : {}", e);
return String::new();
}
base64::engine::general_purpose::STANDARD.encode(buffer.into_inner())
}
/// Encode une image en JPEG et retourne les bytes bruts.
pub fn screenshot_to_jpeg_bytes(img: &DynamicImage, quality: u8) -> Vec<u8> {
let rgb = img.to_rgb8();
let mut buffer = Cursor::new(Vec::new());
let mut encoder = JpegEncoder::new_with_quality(&mut buffer, quality);
if let Err(e) = encoder.encode(
rgb.as_raw(),
rgb.width(),
rgb.height(),
image::ExtendedColorType::Rgb8,
) {
eprintln!("[CAPTURE] Erreur encodage JPEG : {}", e);
return Vec::new();
}
buffer.into_inner()
}
/// Calcule un hash perceptuel rapide pour la déduplication.
///
/// Réduit l'image à 16x16 en niveaux de gris, puis calcule
/// un hash simple basé sur les pixels. Identique à la logique
/// Python (_quick_hash) dans agent_v1.
pub fn image_hash(img: &DynamicImage) -> u64 {
let small = img.resize_exact(16, 16, image::imageops::FilterType::Nearest);
let gray = small.to_luma8();
// Hash FNV-1a simple sur les pixels (rapide, pas besoin de crypto)
let mut hash: u64 = 0xcbf29ce484222325;
for pixel in gray.as_raw() {
hash ^= *pixel as u64;
hash = hash.wrapping_mul(0x100000001b3);
}
hash
}
/// Retourne les dimensions du moniteur principal (largeur, hauteur).
pub fn screen_dimensions() -> Option<(u32, u32)> {
let monitors = xcap::Monitor::all().ok()?;
let primary = monitors
.into_iter()
.find(|m| m.is_primary().unwrap_or(false))?;
let w = primary.width().ok()?;
let h = primary.height().ok()?;
Some((w, h))
}