feat: agent Rust complet — systray, chat, enregistrement, floutage (2.4 MB)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-03-18 23:18:09 +01:00
parent ad7ff3bce4
commit 90ee91caf9
11 changed files with 2329 additions and 191 deletions

340
agent_rust/src/blur.rs Normal file
View File

@@ -0,0 +1,340 @@
//! Floutage des zones sensibles dans les captures d'ecran.
//!
//! Detecte les champs de saisie (zones claires rectangulaires) et applique
//! un flou gaussien pour proteger les donnees sensibles (mots de passe, etc.).
//! Equivalent de agent_v1/vision/blur_sensitive.py.
//!
//! Algorithme :
//! 1. Conversion en niveaux de gris
//! 2. Seuillage binaire (detecter les zones claires = champs de saisie)
//! 3. Detection de contours rectangulaires > 50px de large
//! 4. Application d'un flou gaussien sur les zones detectees
//!
//! Utilise le crate image pour le traitement et imageproc pour le flou.
use image::{DynamicImage, GrayImage, Rgba, RgbaImage};
/// Seuil de luminosite pour detecter les champs de saisie (0-255).
/// Les zones plus claires que ce seuil sont considerees comme des champs.
const BRIGHTNESS_THRESHOLD: u8 = 220;
/// Largeur minimale d'un champ de saisie detecte (en pixels).
const MIN_FIELD_WIDTH: u32 = 50;
/// Hauteur minimale d'un champ de saisie detecte (en pixels).
const MIN_FIELD_HEIGHT: u32 = 15;
/// Hauteur maximale d'un champ de saisie (evite de flouter l'ecran entier).
const MAX_FIELD_HEIGHT: u32 = 80;
/// Largeur maximale d'un champ (evite les faux positifs sur grandes zones blanches).
const MAX_FIELD_WIDTH: u32 = 800;
/// Intensite du flou gaussien (sigma).
const BLUR_SIGMA: f32 = 10.0;
/// Rectangle representant une zone a flouter.
#[derive(Debug, Clone)]
pub struct BlurRegion {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
/// Detecte les champs de saisie dans une image et les floute.
///
/// Retourne l'image modifiee avec les zones sensibles floutees.
/// Si aucun champ n'est detecte, retourne l'image inchangee.
pub fn blur_sensitive_fields(img: &DynamicImage) -> DynamicImage {
let regions = detect_input_fields(img);
if regions.is_empty() {
return img.clone();
}
println!(
"[BLUR] {} zone(s) sensible(s) detectee(s) — floutage...",
regions.len()
);
let mut result = img.to_rgba8();
for region in &regions {
blur_region(&mut result, region);
}
DynamicImage::ImageRgba8(result)
}
/// Detecte les champs de saisie (zones claires rectangulaires).
///
/// Algorithme simplifie :
/// 1. Convertir en niveaux de gris
/// 2. Seuillage binaire
/// 3. Scanner les lignes horizontales pour trouver les series de pixels clairs
/// 4. Regrouper les series adjacentes en rectangles
pub fn detect_input_fields(img: &DynamicImage) -> Vec<BlurRegion> {
let gray = img.to_luma8();
let (width, height) = gray.dimensions();
let mut regions = Vec::new();
// Creer une image binaire (seuillage)
let binary = threshold_image(&gray, BRIGHTNESS_THRESHOLD);
// Scanner par bandes horizontales pour detecter les champs
// On cherche des sequences continues de pixels blancs sur plusieurs lignes
let mut y = 0;
while y < height {
// Pour chaque ligne, trouver les segments horizontaux blancs
let segments = find_white_segments(&binary, y, width);
for (seg_start, seg_end) in &segments {
let seg_width = seg_end - seg_start;
if seg_width < MIN_FIELD_WIDTH || seg_width > MAX_FIELD_WIDTH {
continue;
}
// Verifier combien de lignes consecutives partagent ce segment
let field_height = count_vertical_extent(
&binary,
*seg_start,
*seg_end,
y,
height,
);
if field_height >= MIN_FIELD_HEIGHT && field_height <= MAX_FIELD_HEIGHT {
// Verifier que cette region ne chevauche pas une region existante
let new_region = BlurRegion {
x: *seg_start,
y,
width: seg_width,
height: field_height,
};
if !overlaps_existing(&regions, &new_region) {
regions.push(new_region);
}
}
}
// Avancer de la hauteur du dernier champ detecte, ou de 1 ligne
y += 1;
}
// Deduplication : fusionner les regions tres proches
merge_close_regions(&mut regions);
regions
}
/// Applique un seuillage binaire simple.
fn threshold_image(gray: &GrayImage, threshold: u8) -> GrayImage {
let (width, height) = gray.dimensions();
let mut binary = GrayImage::new(width, height);
for y in 0..height {
for x in 0..width {
let pixel = gray.get_pixel(x, y).0[0];
if pixel >= threshold {
binary.put_pixel(x, y, image::Luma([255]));
} else {
binary.put_pixel(x, y, image::Luma([0]));
}
}
}
binary
}
/// Trouve les segments horizontaux de pixels blancs sur une ligne.
fn find_white_segments(binary: &GrayImage, y: u32, width: u32) -> Vec<(u32, u32)> {
let mut segments = Vec::new();
let mut in_segment = false;
let mut seg_start = 0u32;
for x in 0..width {
let is_white = binary.get_pixel(x, y).0[0] > 128;
if is_white && !in_segment {
seg_start = x;
in_segment = true;
} else if !is_white && in_segment {
segments.push((seg_start, x));
in_segment = false;
}
}
if in_segment {
segments.push((seg_start, width));
}
segments
}
/// Compte le nombre de lignes consecutives ou le segment est blanc.
fn count_vertical_extent(
binary: &GrayImage,
seg_start: u32,
seg_end: u32,
start_y: u32,
max_y: u32,
) -> u32 {
let mut count = 0u32;
let check_width = seg_end - seg_start;
let threshold = (check_width as f64 * 0.7) as u32; // 70% doivent etre blancs
for y in start_y..max_y.min(start_y + MAX_FIELD_HEIGHT + 5) {
let mut white_count = 0u32;
for x in seg_start..seg_end {
if binary.get_pixel(x, y).0[0] > 128 {
white_count += 1;
}
}
if white_count >= threshold {
count += 1;
} else {
break;
}
}
count
}
/// Verifie si une region chevauche une region existante.
fn overlaps_existing(regions: &[BlurRegion], new_region: &BlurRegion) -> bool {
for region in regions {
let x_overlap = new_region.x < region.x + region.width
&& new_region.x + new_region.width > region.x;
let y_overlap = new_region.y < region.y + region.height
&& new_region.y + new_region.height > region.y;
if x_overlap && y_overlap {
return true;
}
}
false
}
/// Fusionne les regions tres proches (< 10px de distance).
fn merge_close_regions(regions: &mut Vec<BlurRegion>) {
if regions.len() < 2 {
return;
}
// Tri par position (y, puis x)
regions.sort_by(|a, b| a.y.cmp(&b.y).then(a.x.cmp(&b.x)));
let mut merged = Vec::new();
let mut current = regions[0].clone();
for region in regions.iter().skip(1) {
let x_close = (current.x + current.width + 10 >= region.x)
&& (region.x + region.width + 10 >= current.x);
let y_close = (current.y + current.height + 5 >= region.y)
&& (region.y + region.height + 5 >= current.y);
if x_close && y_close {
// Fusionner
let min_x = current.x.min(region.x);
let min_y = current.y.min(region.y);
let max_x = (current.x + current.width).max(region.x + region.width);
let max_y = (current.y + current.height).max(region.y + region.height);
current = BlurRegion {
x: min_x,
y: min_y,
width: max_x - min_x,
height: max_y - min_y,
};
} else {
merged.push(current);
current = region.clone();
}
}
merged.push(current);
*regions = merged;
}
/// Applique un flou gaussien sur une region de l'image.
///
/// Implementation simplifiee : box blur avec plusieurs passes
/// (approximation du gaussien, plus rapide que le vrai gaussien).
fn blur_region(img: &mut RgbaImage, region: &BlurRegion) {
let (img_w, img_h) = img.dimensions();
// Borner la region aux dimensions de l'image
let x_start = region.x.min(img_w);
let y_start = region.y.min(img_h);
let x_end = (region.x + region.width).min(img_w);
let y_end = (region.y + region.height).min(img_h);
if x_start >= x_end || y_start >= y_end {
return;
}
let radius = BLUR_SIGMA as u32;
let kernel_size = (radius * 2 + 1) as i32;
let kernel_area = (kernel_size * kernel_size) as u32;
// Box blur : moyenne des pixels dans un carre de rayon `radius`
// On fait 3 passes pour approximer un flou gaussien
for _pass in 0..3 {
// Copier les pixels de la region dans un buffer temporaire
let reg_w = (x_end - x_start) as usize;
let reg_h = (y_end - y_start) as usize;
let mut buffer: Vec<[u8; 4]> = Vec::with_capacity(reg_w * reg_h);
for y in y_start..y_end {
for x in x_start..x_end {
buffer.push(img.get_pixel(x, y).0);
}
}
// Appliquer le box blur
for y in y_start..y_end {
for x in x_start..x_end {
let mut sum_r = 0u32;
let mut sum_g = 0u32;
let mut sum_b = 0u32;
let mut count = 0u32;
for ky in -(radius as i32)..=(radius as i32) {
for kx in -(radius as i32)..=(radius as i32) {
let sx = x as i32 + kx;
let sy = y as i32 + ky;
if sx >= x_start as i32
&& sx < x_end as i32
&& sy >= y_start as i32
&& sy < y_end as i32
{
let bx = (sx - x_start as i32) as usize;
let by = (sy - y_start as i32) as usize;
let pixel = buffer[by * reg_w + bx];
sum_r += pixel[0] as u32;
sum_g += pixel[1] as u32;
sum_b += pixel[2] as u32;
count += 1;
}
}
}
if count > 0 {
let pixel = Rgba([
(sum_r / count) as u8,
(sum_g / count) as u8,
(sum_b / count) as u8,
255,
]);
img.put_pixel(x, y, pixel);
}
}
}
}
let _ = kernel_area; // suppress unused warning
}