16 KiB
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
Algoritm
Partea 1: rPPG via POS (Plane Orthogonal to Skin, Wang et al. 2017)
-
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. -
Construiește seria temporală RGB(t) — câte un sample per cadru.
-
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) -
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.
-
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
-
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]). -
Detectare blink event: tranziție EAR < 0.20 → EAR > 0.25. Pentru fiecare blink: start_idx, min_idx, end_idx, min_ear.
-
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)
{
"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 marcatm25_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
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ă
voicedmask +voice_ratioglobal
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)
-
Pe video, extrage MediaPipe FaceMesh per cadru, calculează:
mouth_aperture = ||lm_13 - lm_14|| / ||lm_61 - lm_291||(deschidere verticală / lățime gură).
-
Resample mouth_aperture la 100 Hz (același rate ca audio envelope).
-
Cross-correlation FFT-based între:
- audio RMS envelope (per fereastră 10ms)
- mouth_aperture differential (|d_aperture/dt|)
-
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)
{
"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
Algoritm
Trei semnale combinate:
NPR (Neighboring Pixel Relationships, Tan et al. 2024 — adapted)
- Construiește piramida Gaussian 4 niveluri din imaginea grayscale.
- Resize toate nivelurile la dimensiunea originală.
- Pentru fiecare pereche de niveluri consecutive: calculează reziduul și varianța locală 3×3 a reziduului².
- Medianul varianțelor →
mean_residual_var. - 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)
cv2.imencode(".jpg", frame, [JPEG_QUALITY, 65])cv2.imdecode(buf)— recompressederror_norm = mean(|orig - recomp|) / std(orig)- 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)
{
"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
Algoritm (Face X-ray-inspired, simplificat fără rețea)
-
Detectează fața cu MediaPipe FaceMesh, extrage contour din
FACE_OVALlandmarks. Construiește mască poligonală binară. -
Boundary band adaptiv la mărimea feței:
max(8, face_width * 0.05). -
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
-
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)) -
Peak detection: bounding box al regiunii cu max suspicion.
boundary_mean_suspicion= media heatmap pe banda boundary.
Output specific (summary)
{
"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
Algoritm
Estimare direcție lumină față (Lambertian SfS simplificat)
-
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²)) -
Lambertian model:
I(x,y) ≈ ρ * max(N · L, 0) + ambient -
Least squares fit pe pixeli mască:
[N | 1] @ [L_x, L_y, L_z, ambient]^T = IRezolvă cu
np.linalg.lstsq. -
Normalizare și conversie sferică:
- azimuth = atan2(L_x, L_z)
- elevation = asin(-L_y)
Estimare direcție lumină scenă
-
Highlight detection în zona NON-face:
V > percentila 95în HSVS < 80(saturație scăzută — highlights sunt aproape albe)
-
Connected components pe highlight mask.
-
Weighted centroid (greutate = area cluster):
- dx = mediu_x_clusters - W/2
- dy = mediu_y_clusters - H/2
-
Convertit în (azimuth_scene, elevation_scene).
Angular discrepancy
angle = acos(L_face · L_scene)
- < 60° → consistent
- 60-90° → marginal
-
90° → mismatch suspect
Catchlight consistency
- ROI ochi stâng + drept (din landmarks).
- Top 1% intensitate per ochi → centroid + intensitate.
- Comparare: poziții relative trebuie să fie aproximativ oglindite (ochiul drept e oglindit pe x).
- Score = 1 - normalize(|dx_diff_mirror| + |dy_diff| + |intensity_diff|).
Output specific (summary)
{
"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)
-
Creează folder
tools/mXX_nume/:tools/mXX_nume/ ├── __init__.py ├── nume.json # config: id, input_type, parameters └── nume.py # cod algoritm -
În
nume.py: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"], ) -
În
forensic/orchestrator.py:AVAILABLE_MODULES, adaugă:"mXX": ("tools.mXX_nume.nume", "run", "overview_frames"), -
În
forensic/scoring.py:DEFAULT_WEIGHTS, adaugă weight (default 1.0):"mXX": 1.0, -
Rebuild container. Modulul apare automat în
/api/forensic-modules.