Livrare LOT 1 - Didi

This commit is contained in:
Dezvoltari Evotech 2026-06-25 14:13:25 -07:00
commit 5380c3fc63
990 changed files with 133308 additions and 0 deletions

View file

@ -0,0 +1,496 @@
# 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`.