11 KiB
Arhitectură
Document care explică cum funcționează fiecare strat și de ce e separat așa. Citire necesară înainte de a modifica codul.
Filozofia: extracție de features, NU clasificare
Sistemul are un singur scop: să producă pentru LLM-ul tău extern un set de măsurători numerice + hărți vizuale pe care LLM-ul nu le poate calcula din imagine.
LLM-ul tău face deja:
- Vision generală pe imaginea originală
- OCR
- Recunoaștere de tipologii custom
- Raționament semantic
Acest serviciu adaugă:
- Puls cardiac din variația de culoare facială (rPPG)
- Drift lip-sync în milisecunde
- Direcția luminii estimată via shape-from-shading
- Hartă PNG cu zonele suspect de blending
- Scoruri statistice care diferențiază imagini AI vs naturale
LLM-ul integrează totul și decide singur. Noi nu decidem nimic.
Cele 4 straturi
┌─────────────────────────────────────────────────────────────┐
│ STRAT 4: API HTTP (api.py) │
│ - Multipart upload │
│ - Routing: /api/forensic-evidence, /forensic-modules etc. │
│ - Sync sau async mode (job store in-memory) │
└────────────────────────────┬────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ STRAT 3: Glue Layer (forensic/) │
│ │
│ orchestrator.py — rulează modulele cu input corect │
│ - adaptive every_n_frames │
│ - auto-skip m26 dacă lipsește audio │
│ - captură excepții per modul │
│ │
│ scoring.py — fuziune ponderată Dempster-Shafer │
│ - score per modul × confidence × weight │
│ - penalizare disagreement │
│ │
│ prompt_builder.py — formatare evidence_text │
│ + encoding base64 PNG pentru LLM │
└────────────────────────────┬────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ STRAT 2: Detectoare Forensice (tools/m25-m29) │
│ │
│ Fiecare modul: │
│ - Primește frame_paths (sau video_path pentru m26) │
│ - Aplică algoritmul propriu (POS rPPG, NPR, etc.) │
│ - Salvează PNG-uri pentru consum vizual LLM │
│ - Returnează schema CONTRACT.md uniformă │
│ │
│ _contract.py — helper make_response() / empty_response()│
└────────────────────────────┬────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ STRAT 1: Infrastructure (preprocessing.py + libraries) │
│ - ffmpeg pentru extracție frame-uri și audio │
│ - MediaPipe FaceLandmarker (478 landmarks) │
│ - OpenCV pentru imread/imwrite + image ops │
│ - scipy/numpy pentru DSP │
└─────────────────────────────────────────────────────────────┘
Fluxul unui request
Pentru POST /api/forensic-evidence cu un video:
1. Upload + validare (api.py:106-180)
- Multipart streaming în chunks 64KB (nu buffer tot fișierul)
- Validare extensie: mp4/mov/avi/mkv/webm/jpg/png
- Asignare
job_idUUID hex 16 chars - Salvare temporară la
/app/data/inference/{job_id}/
2. Orchestrator setup (forensic/orchestrator.py:75-110)
- Adaptive
every_n_framesbazat pe durata video:- < 5s → every_n=1 (toate cadrele)
- < 30s → every_n=3 (~10 fps efectiv)
- < 120s → every_n=10 (~3 fps)
- else → every_n=30 (~1 fps)
- Auto-skip m26 dacă videoul nu are pistă audio
- Extracție cadre cu ffmpeg via
preprocessing.extract_frames() - Scriere
_meta.jsoncu fps efectiv (m25 îl folosește pentru rPPG)
3. Rulare module (forensic/orchestrator.py:120-145)
Fiecare modul rulează secvențial (default), izolat în try/except:
- Crash într-un modul NU oprește restul
- Modulul eșuat returnează
empty_response()cu primary_score=None - Toate cele 5 module returnează aceeași schemă (CONTRACT.md)
4. Fuziune (forensic/scoring.py)
Dempster-Shafer-inspired weighted average:
weighted_score = Σ(score_i × confidence_i × weight_i) / Σ(confidence_i × weight_i)
- Module cu
confidence=0(NO_SIGNAL) sunt ignorate - Module cu confidence mic contribuie mai puțin
- Disagreement (varianța scorurilor) penalizează confidence final
- Weight default per modul: m28=1.3, m25=1.2, m27=1.0, m26=0.9, m29=0.8
Rezultatul fuziunii e opțional — îl includem pentru context, dar LLM-ul poate să-l ignore.
5. Format pentru LLM (forensic/prompt_builder.py)
Construiește:
- evidence_text — bloc text formatat, ~2-3 KB tipic
- images — lista cu metadata + opțional data URLs base64
- summary — obiect cu scoruri agregate pentru parsing programatic
- instruction_for_llm — text fix care explică LLM-ului cum să folosească datele
6. Răspuns (api.py:225-265)
- Sync (default): JSON imediat
- Async (
async_mode=1): 202 cu job_id, polling pe/api/status/{id}
Schema unificată
Toate cele 5 module returnează exact aceleași chei top-level, garantat de
tools/_contract.py:make_response():
{
"tool": {"id": "m25", "name": "...", "version": "1.0", "input_type": "..."},
"summary": {
"primary_score": 0..1 | None,
"primary_label": "FAKE"|"REAL"|"INCERT"|"NO_SIGNAL",
"confidence": 0..1,
"evidence": [...],
"frames_analyzed": int,
"frames_with_signal": int,
// câmpuri specifice tool — vezi MODULES.md
},
"per_frame": [...],
"metrics": {...},
"artifacts": {"images": [str]},
"errors": [],
"warnings": [],
"execution_time_ms": float
}
Asta înseamnă că:
- Un client nou care vrea să consume un singur modul îl poate parsa cu același cod
- Orchestrator-ul nu trebuie să cunoască câmpurile specifice fiecărui modul
- Modulele noi pot fi adăugate fără să schimbi nimic în glue layer
Vezi CONTRACT.md pentru schema completă.
Module dependențe
| Modul | Cere MediaPipe? | Cere audio? | Cere ≥N cadre |
|---|---|---|---|
| m25 Physiology | DA (FaceLandmarker, 478 lm) | NU | ≥5 cadre, fps≥4 pt rPPG |
| m26 Audio | DA (pentru mouth aperture) | DA | ≥10 cadre + audio |
| m27 AI Detector | NU | NU | ≥1 cadru |
| m28 Forgery Heatmap | DA (face mask + landmarks) | NU | ≥1 cadru cu față |
| m29 Lighting | DA (face mask + landmarks) | NU | ≥1 cadru cu față + scene highlights |
orchestrator.py auto-detectează aceste condiții:
- Dacă nu există audio → m26 e omis (auto_skipped)
- Dacă MediaPipe ratează față → modul returnează
empty_response("face not detected") - Dacă cadrele sunt prea puține pentru rPPG → m25 returnează NO_SIGNAL pe pulse
Decizii arhitecturale importante
De ce orchestrator secvențial, nu paralel?
Default use_parallel=False. Motiv: MediaPipe nu e thread-safe la load
(crearea instanței FaceLandmarker face I/O + alocare GPU buffers). Doi
workers care încarcă concomitent pot avea race condition.
Pe viitor, paralelizare prin proces dedicate per modul, nu thread.
De ce job store in-memory?
Simplitate pentru baseline. Pentru producție serioasă, înlocuiește dict-ul
_jobs din api.py cu Redis sau SQLite (vezi nota din docs/API.md
secțiunea Async mode).
De ce nu un model neural unic?
Trei motive:
- Interpretabilitate: LLM-ul vede 5 semnale separate, poate combina cu vision
- Robustețe: când un modul eșuează (ex. fără audio), restul funcționează
- Cost: rulare CPU, fără GPU obligatoriu
Pentru clasificare end-to-end SOTA pe GPU, folosește un model dedicat ca Face X-ray sau UnivFD — nu acest sistem.
De ce HF detector e opt-in dezactivat?
Testat empiric pe 30 samples reale:
- Cu HF agresiv (weight 0.75): regresie de la 27% strict → 20%
- Cu HF defensiv (doar la confidence extremă): 13% strict
- Fără HF: 27% strict, 77% lenient (cel mai bun)
Cauza: Organika/sdxl-detector e antrenat pe imagini SD vs photos. Pe video
TikTok/news (distribuția ta), produce fals-pozitive masive. Cod prezent în
m27 pentru când se găsește un model potrivit; activează cu M27_USE_HF=1.
Performanță
| Tip input | Procesare tipică |
|---|---|
| Imagine statică (1-3 cadre) | 1-3 s |
| Video scurt (<5s) cu față clară | 5-15 s |
| Video mediu (30s) | 30-60 s |
| Video lung (>2 min) | 1-5 min |
Bottleneck-uri:
- MediaPipe FaceLandmarker pe CPU: ~80-150 ms/frame
- m28 forgery heatmap: 3 component maps × Gaussian blur — heavy
- rPPG POS: rapid (~50ms/clip) pe semnal mic
Următorii pași dacă vrei extindere
Vezi MODULES.md pentru cum să adaugi un modul nou.
Pe scurt:
- Creează
tools/mXX_nume/nume.py+nume.jsonconfig - Funcția
run()returnează schema CONTRACT prinmake_response() - Adaugă
(import_path, fn_name, input_type)înAVAILABLE_MODULESdinforensic/orchestrator.py - Adaugă weight default în
DEFAULT_WEIGHTSdinforensic/scoring.py - Rebuild + test cu
/api/forensic-modules— apare în catalog
Nu trebuie modificat api.py — orchestrator-ul descoperă modulele automat.