# 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:152-211, în `handle_forensic_evidence`) - Multipart streaming în chunks 64KB (nu buffer tot fișierul) - Validare extensie: mp4/mov/avi/mkv/webm/jpg/png - Asignare `job_id` UUID hex 16 chars - Salvare temporară la `/app/data/inference/{job_id}/` ### 2. Orchestrator setup (forensic/orchestrator.py:220-270, în `run_forensic_pipeline`) - **Adaptive `every_n_frames`** bazat 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.json` cu fps efectiv (m25 îl folosește pentru rPPG) ### 3. Rulare module (forensic/orchestrator.py:283-296, via `_run_single_module` la :77) 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:235-256, ramura sync din `handle_forensic_evidence`) - 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()`: ```jsonc { "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](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: 1. **Interpretabilitate**: LLM-ul vede 5 semnale separate, poate combina cu vision 2. **Robustețe**: când un modul eșuează (ex. fără audio), restul funcționează 3. **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: 1. **MediaPipe FaceLandmarker** pe CPU: ~80-150 ms/frame 2. **m28 forgery heatmap**: 3 component maps × Gaussian blur — heavy 3. **rPPG POS**: rapid (~50ms/clip) pe semnal mic ## Următorii pași dacă vrei extindere Vezi [MODULES.md](MODULES.md) pentru cum să adaugi un modul nou. Pe scurt: 1. Creează `tools/mXX_nume/nume.py` + `nume.json` config 2. Funcția `run()` returnează schema CONTRACT prin `make_response()` 3. Adaugă `(import_path, fn_name, input_type)` în `AVAILABLE_MODULES` din `forensic/orchestrator.py` 4. Adaugă weight default în `DEFAULT_WEIGHTS` din `forensic/scoring.py` 5. Rebuild + test cu `/api/forensic-modules` — apare în catalog Nu trebuie modificat `api.py` — orchestrator-ul descoperă modulele automat.