# Module Deep Dive Detaliu tehnic pentru fiecare din cele 5 module. Algoritm exact, parametri, câmpuri de output specifice, limitări cunoscute. --- ## m25 — Physiology (rPPG + Blink Dynamics) **Scop**: detectează semnale fiziologice care nu pot fi falsificate de AI: puls cardiac prin remote photoplethysmography (rPPG) din variația de culoare facială, plus dinamica clipitului ochilor. **Fișier**: [tools/m25_physiology/physiology.py](../tools/m25_physiology/physiology.py) ### Algoritm #### Partea 1: rPPG via POS (Plane Orthogonal to Skin, Wang et al. 2017) 1. **Per cadru**: detectează fața cu MediaPipe FaceMesh (478 landmarks). Extrage ROI obraz stânga + dreapta din landmarks-uri specifice (`LEFT_CHEEK_LM=[101,207,187]`, `RIGHT_CHEEK_LM=[330,427,411]`). Mediază RGB pe fiecare ROI. 2. **Construiește seria temporală** RGB(t) — câte un sample per cadru. 3. **POS algorithm**: ``` C_n(t) = C(t) / mean(C) # normalizare temporală X = 3*R_n - 2*G_n Y = 1.5*R_n + G_n - 1.5*B_n α = std(X) / std(Y) P(t) = X(t) + α * Y(t) ``` 4. **Detrending polynomial ordin 3** pe P(t) — elimină drift slow din variația de iluminare (cloud cover, AGC cameră). Critical pentru rPPG fiabil pe video real. 5. **FFT + bandpass** 0.7-4 Hz (40-240 BPM range fiziologic): - Identifică peak în banda 50-110 BPM - BPM = peak_freq × 60 - SNR = power(peak ± 0.2 Hz) / power(restul benzii) #### Partea 2: Blink Dynamics 1. **EAR per ochi** (Eye Aspect Ratio, Soukupová & Čech 2016): ``` EAR = (||p2-p6|| + ||p3-p5||) / (2 × ||p1-p4||) ``` Calculat separat pe ochi stâng (landmarks `[33, 160, 158, 133, 153, 144]`) și drept (`[362, 385, 387, 263, 373, 380]`). 2. **Detectare blink event**: tranziție EAR < 0.20 → EAR > 0.25. Pentru fiecare blink: start_idx, min_idx, end_idx, min_ear. 3. **Asimetrie L-R temporală**: pentru fiecare blink stâng, găsește perechea cea mai apropiată în timp pe dreapta. Calculează diferența în ms. Real human: 30-80 ms asimetrie (asimetrie neurologică naturală). AI face: simetric perfect sau jitter aleator. ### Output specific (summary) ```jsonc { "pulse_bpm": 82.4, // BPM detectat (null dacă fără semnal) "pulse_snr_db": 1.1, // SNR în dB (>3 = semnal credibil) "blink_count_left": 1, "blink_count_right": 1, "blink_count_total": 1, "blink_asymmetry_ms": 42.0, // ms diferență temporală L-R "fps_used": 8.0, // fps efectiv (din _meta.json) "rppg_method": "POS", "mediapipe_used": true } ``` ### Limitări - **Sparse extraction**: dacă fps efectiv < 4, rPPG e imposibil (sub Nyquist pentru banda 0.7-4 Hz). Modulul returnează NO_SIGNAL clar. - **Iluminare variabilă**: face mai dificil detrending-ul. Pe video cu flickering puternic, semnal degradat. - **Față mică** (< 150 px lățime): ROI obraz prea mic pentru sample RGB stabil. - **MediaPipe rateaza fața**: pe ochi închiși, profil oblic >45°, low-light. ### PNG-uri generate - `m25_pulse_signal.png` — grafic P(t) detrendat + spectrul FFT cu peak marcat - `m25_blink_timeline.png` — EAR L și R în timp + zonele de blink evidențiate --- ## m26 — Audio Forensics (Lip-Sync + Voice Clone Heuristic) **Scop**: analizează coloana sonoră pentru drift între mișcarea buzelor și audio (lip-sync offset) + caracteristici statistice ale vocii sintetice (F0 prea stabil, centroidă spectrală prea constantă, lipsa pauzelor respiratorii). **Fișier**: [tools/m26_audio/audio.py](../tools/m26_audio/audio.py) ### Algoritm #### Audio extraction (in-memory, fără disk I/O) `ffmpeg -i video.mp4 -vn -ac 1 -ar 16000 -f f32le pipe:1` Output: PCM float32 mono 16 kHz în memorie. #### VAD (Voice Activity Detection) inline Combină energie locală + zero-crossing rate per fereastră 30ms: - Threshold energie adaptiv = 30th percentile × 1.5 - Voce = energie > threshold AND ZCR în [0.02, 0.30] - Returnează `voiced` mask + `voice_ratio` global Folosit ca **gate** pe restul analizei — analizăm F0/centroid DOAR pe ferestrele unde VAD spune că e voce. #### F0 (pitch fundamental) via autocorelație Per fereastră 25ms (hop 10ms): - Autocorelație normalizată a semnalului - Caută peak în banda 75-400 Hz (range voce umană) - F0 = sample_rate / peak_lag dacă ac[peak] > 0.3, altfel 0 #### Spectral centroid Per fereastră 25ms cu Hann window: - FFT magnitude spectrum - Centroid = Σ(freq × magnitude) / Σ(magnitude) #### Lip-sync offset (cross-correlation) 1. Pe video, extrage MediaPipe FaceMesh per cadru, calculează: ``` mouth_aperture = ||lm_13 - lm_14|| / ||lm_61 - lm_291|| ``` (deschidere verticală / lățime gură). 2. Resample mouth_aperture la 100 Hz (același rate ca audio envelope). 3. Cross-correlation FFT-based între: - audio RMS envelope (per fereastră 10ms) - mouth_aperture differential (|d_aperture/dt|) 4. Lag care maximizează corelația = lip_sync_offset_ms. - |offset| < 60 ms → real video - |offset| > 150 ms → deepfake lip-sync (Wav2Lip-class drift) ### Output specific (summary) ```jsonc { "lip_sync_offset_ms": 187.0, // ms (null dacă fără față) "lip_sync_correlation": 0.42, "f0_std_hz": 12.3, // <25 Hz = TTS-like "spectral_centroid_std_hz": 145.0, // <250 Hz = TTS-like "silence_ratio": 0.05, "voice_ratio": 0.95, "n_voiced_frames": 480, "audio_duration_s": 20.4, "audio_sample_rate": 16000, "lip_sync_component": 0.9, "voice_clone_component": 0.7 } ``` ### Limitări - **Fără audio**: m26 returnează NO_SIGNAL. Orchestrator-ul auto-skip cu ffprobe verificare înainte de execuție. - **Audio cu zgomot puternic**: VAD raporteaza voce când e doar zgomot; F0 e brittle pe semnale noise-y. - **Profil oblic**: mouth_aperture devine instabilă când unghiul depășește 30° (raport vertical/horiz inflate). - **Voice clone heuristic e PROXY**: pentru detector real, integrează AASIST sau RawNet2. ### PNG-uri generate - `m26_audio_visual_sync.png` — overlay audio envelope normalizat + mouth aperture, cu lag marcat. Util pentru LLM să vadă vizual decalajul. --- ## m27 — AI-Generated Image Detector (Black-Box) **Scop**: distinge imagini fotografice naturale de imagini generate AI (GAN, diffusion models). **Fișier**: [tools/m27_ai_detector/ai_detector.py](../tools/m27_ai_detector/ai_detector.py) ### Algoritm Trei semnale combinate: #### NPR (Neighboring Pixel Relationships, Tan et al. 2024 — adapted) 1. Construiește **piramida Gaussian** 4 niveluri din imaginea grayscale. 2. Resize toate nivelurile la dimensiunea originală. 3. Pentru fiecare pereche de niveluri consecutive: calculează **reziduul** și **varianța locală 3×3** a reziduului². 4. Medianul varianțelor → `mean_residual_var`. 5. **Sigmoid centrat pe 3.5** → score 0..1 (mai mic mean → mai suspect AI). Real video natural: mean_residual_var ~5-30 (textură fluctuantă). AI generated: mean_residual_var ~0.5-4 (smooth predict). #### JPEG Reconstruction Error (in-memory) 1. `cv2.imencode(".jpg", frame, [JPEG_QUALITY, 65])` 2. `cv2.imdecode(buf)` — recompressed 3. `error_norm = mean(|orig - recomp|) / std(orig)` 4. **Sigmoid centrat pe 0.04** (steep) → score 0..1. Imagini cu detalii naturale → error_norm mare (~0.10-0.15). AI imagery → error_norm mic (~0.02-0.06). #### HuggingFace Detector (OPT-IN, dezactivat default) Dacă `M27_USE_HF=1`: - Încarcă `Organika/sdxl-detector` (ViT base ~330MB) prin transformers - Inferință per cadru, output: score 0..1 = probabilitate AI **Defensive fusion**: HF folosit DOAR când e confident extrem (>0.85 sau <0.15). În rest cade pe NPR + JPEG. **Status empiric**: testat pe 30 samples reale (TikTok video + AI images). Out-of-distribution masiv → regresie de la 27% strict (fără HF) la 13-20% (cu HF). **NU recomandat pe video data**. Cod prezent pentru când se găsește un model mai bine adaptat distribuției tale. ### Fusion internă Dacă există HF score și e confident: ``` primary = 0.50 × hf_mean + 0.30 × npr_mean + 0.20 × jpeg_mean ``` Altfel (default): ``` primary = max(npr_mean, jpeg_mean) ``` ### Output specific (summary) ```jsonc { "hf_score_mean": null, // sau float 0..1 dacă activat "hf_model_loaded": false, "hf_model_status": "HF detector disabled (set M27_USE_HF=1)", "npr_score_mean": 0.48, "npr_score_std": 0.10, "jpeg_recon_score_mean": 0.78, "jpeg_recon_score_std": 0.05, "torch_score_mean": null, "torch_model_loaded": false, "n_frames_sampled": 16 } ``` ### Limitări - **Statistical-only fără HF**: discriminative power limitat. Pe video real cu compresie puternică, JPEG-recon dă fals-pozitive (vezi tabel din experimente: pe REAL video tipic 0.70-0.85 FAKE). - **NPR sensibil la rezoluție**: imagini sub 64×64 → fallback la 0.5 neutru. - **Pe video re-encodat**: codec deja a smoothed detalii fine → ambele semnale se confundă cu AI-generated. ### PNG generat - `m27_score_timeline.png` — line plot scoruri NPR + JPEG per cadru sample. --- ## m28 — Forgery Localization Heatmap **Scop**: produce o **hartă 2D** unde fiecare pixel are probabilitate de manipulare locală. **Cel mai util output pentru LLM** — îi dai imagine PNG cu zona suspect colorată, LLM-ul confirmă vizual. **Fișier**: [tools/m28_forgery_heatmap/forgery_heatmap.py](../tools/m28_forgery_heatmap/forgery_heatmap.py) ### Algoritm (Face X-ray-inspired, simplificat fără rețea) 1. **Detectează fața** cu MediaPipe FaceMesh, extrage contour din `FACE_OVAL` landmarks. Construiește mască poligonală binară. 2. **Boundary band adaptiv** la mărimea feței: `max(8, face_width * 0.05)`. 3. **3 hărți de discontinuitate**: **(a) Multi-scale Laplacian discrepancy**: - Aplică Laplacian la scale [3, 7, 15] (Gaussian smoothed) - Stivuiește; pentru fiecare pixel: std cross-scale - Limitează la boundary band **(b) Frequency-domain split inconsistency**: - FFT global → high-pass (cutoff 15% rază) - Gradient Sobel pe high-freq map - Limitează la boundary band **(c) Chrominance step in LAB**: - Convert LAB; gradient pe canalele a și b - Localizat strict pe boundary band 4. **Compunere ponderată** (NU max, ca să nu satureze la outlier): ``` combined = 0.5*map_chroma + 0.3*map_freq + 0.2*map_laplacian heatmap = sigmoid(6 * (combined - 0.5)) ``` 5. **Peak detection**: bounding box al regiunii cu max suspicion. `boundary_mean_suspicion` = media heatmap pe banda boundary. ### Output specific (summary) ```jsonc { "peak_suspicion_max": 0.90, "peak_suspicion_mean": 0.88, "boundary_mean_suspicion": 0.14, // metric principal — mai stabil "frames_with_face": 5, "mediapipe_used": true } ``` ### Scoring final ``` score = 0.4 * peak_max + 0.6 * min(1.0, (boundary_mean - 0.05) / 0.20) ``` Peak ridicat + boundary_mean mic = noise (un pixel anomalous). Peak ridicat + boundary_mean ridicat = blending real detectat. ### Limitări - **Pe full-AI generated** (fără boundary face-swap), m28 nu vede mare diferență. Pentru asta folosește m27. - **MediaPipe rateaza fața** → fallback la **Haar eliptic** (mai puțin precis). Pe video oblice/low-light, fallback dă fals-pozitive frecvente. - **Boundary band fix vs. mărime față**: adaptiv 5% lățime — funcționează bine, dar pe fețe foarte mici (<80 px) banda devine prea îngustă. ### PNG generat (cel mai util pentru LLM) - `m28_heatmap_{frame_idx}.png` — overlay original + heatmap fierbinte cu zona suspect colorată. Câte unul per 5 frame samples = 5 PNG-uri. **LLM-ul vede aceste imagini** și poate spune: "da, văd zona aceea evidențiată — și văd EFECTIV un edge artificial la jawline". Confirmare vizuală + numerică = signal puternic. --- ## m29 — Lighting 3D Consistency **Scop**: verifică dacă fața și scena sunt iluminate de aceleași surse de lumină. Mismatch unghi = subiect compus. **Fișier**: [tools/m29_lighting/lighting.py](../tools/m29_lighting/lighting.py) ### Algoritm #### Estimare direcție lumină față (Lambertian SfS simplificat) 1. **Aproximare normale 3D ca sferă**: centrul = centroid landmark-uri, rază = jumătate lățime față. ``` N(x, y) = ((x-cx)/r, (y-cy)/r, sqrt(1 - dx² - dy²)) ``` 2. **Lambertian model**: ``` I(x,y) ≈ ρ * max(N · L, 0) + ambient ``` 3. **Least squares fit** pe pixeli mască: ``` [N | 1] @ [L_x, L_y, L_z, ambient]^T = I ``` Rezolvă cu `np.linalg.lstsq`. 4. **Normalizare** și conversie sferică: - azimuth = atan2(L_x, L_z) - elevation = asin(-L_y) #### Estimare direcție lumină scenă 1. **Highlight detection** în zona NON-face: - `V > percentila 95` în HSV - `S < 80` (saturație scăzută — highlights sunt aproape albe) 2. **Connected components** pe highlight mask. 3. **Weighted centroid** (greutate = area cluster): - dx = mediu_x_clusters - W/2 - dy = mediu_y_clusters - H/2 4. Convertit în (azimuth_scene, elevation_scene). #### Angular discrepancy ``` angle = acos(L_face · L_scene) ``` - < 60° → consistent - 60-90° → marginal - > 90° → mismatch suspect #### Catchlight consistency 1. ROI ochi stâng + drept (din landmarks). 2. Top 1% intensitate per ochi → centroid + intensitate. 3. Comparare: poziții relative trebuie să fie aproximativ **oglindite** (ochiul drept e oglindit pe x). 4. Score = 1 - normalize(|dx_diff_mirror| + |dy_diff| + |intensity_diff|). ### Output specific (summary) ```jsonc { "lighting_mismatch_deg_mean": 95.0, "lighting_mismatch_deg_max": 105.4, "catchlight_consistency_mean": null, // sau float 0..1 "frames_with_lighting_signal": 5, "frames_with_catchlights": 0, "lighting_component": 0.76, "catchlight_component": 0.40 } ``` ### Limitări - **Spherical approximation** pentru normale 3D e grosier — pentru SOTA folosește 3DMM (FLAME, BFM2009) fittat real cu eos-py. - **Scene fără highlights**: nu putem estima direcția scenă → NO_SIGNAL. Lumină ambientă uniformă (interior office, cer înnorat) e cazul tipic. - **Catchlight pe ochi mici** (<30×30 px): primul pixel câștigă, semnal zgomot. ### PNG generat - `m29_lighting_{frame_idx}.png` — overlay imagine + 2 săgeți care arată direcția estimată: **roșie** pentru lumină față, **albastră** pentru scenă. Mismatch vizibil instant. --- ## Cum să adaugi un modul nou (mXX) 1. Creează folder `tools/mXX_nume/`: ``` tools/mXX_nume/ ├── __init__.py ├── nume.json # config: id, input_type, parameters └── nume.py # cod algoritm ``` 2. În `nume.py`: ```python from tools._contract import make_response, empty_response TOOL_ID = "mXX" TOOL_NAME = "Numele tău" VERSION = "1.0" INPUT_TYPE = "overview_frames" # sau "video_path" def run(frame_paths, results_dir=None): # ... algoritmul tău ... return make_response( tool_id=TOOL_ID, tool_name=TOOL_NAME, version=VERSION, input_type=INPUT_TYPE, primary_score=0.5, confidence=0.7, evidence=["...", "..."], frames_analyzed=N, frames_with_signal=M, summary_extras={"custom_field": value}, artifacts_images=["mXX_visualization.png"], ) ``` 3. În `forensic/orchestrator.py:AVAILABLE_MODULES`, adaugă: ```python "mXX": ("tools.mXX_nume.nume", "run", "overview_frames"), ``` 4. În `forensic/scoring.py:DEFAULT_WEIGHTS`, adaugă weight (default 1.0): ```python "mXX": 1.0, ``` 5. Rebuild container. Modulul apare automat în `/api/forensic-modules`.