didi-lot1-ai/ai_platform/modules/forensic_features/docs/ARCHITECTURE.md

11 KiB
Raw Permalink Blame History

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():

{
  "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:

  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 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.