Livrare LOT 1 - Didi
This commit is contained in:
commit
5380c3fc63
990 changed files with 133308 additions and 0 deletions
260
ai_platform/modules/forensic_features/docs/API.md
Normal file
260
ai_platform/modules/forensic_features/docs/API.md
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
# API Reference
|
||||
|
||||
Toate endpoint-urile, parametrii, status codes, exemple curl.
|
||||
|
||||
## Base URL
|
||||
|
||||
```
|
||||
http://localhost:8080
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
|
||||
### `POST /api/forensic-evidence`
|
||||
|
||||
**Singurul endpoint care contează în practică.** Upload un video/imagine,
|
||||
primește înapoi evidence formatat pentru LLM.
|
||||
|
||||
#### Request
|
||||
|
||||
Content-Type: `multipart/form-data`
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-----------------|----------|----------|---------|-------------|
|
||||
| `video` | file | DA | — | Fișier video sau imagine (mp4, mov, avi, mkv, webm, jpg, jpeg, png). Max 2 GB. |
|
||||
| `modules` | string | NU | toate | CSV de module IDs ("m25,m27,m28"). Default rulează toate cele 5. |
|
||||
| `encode_images` | string | NU | "1" | "1" → atașează data URLs base64; "0" → doar paths absolute pe disk |
|
||||
| `every_n_frames`| int | NU | adaptiv | Pas extracție cadre. None → orchestrator alege bazat pe durata video |
|
||||
| `async_mode` | string | NU | "0" | "1" → returnează 202 cu job_id; "0" → blochează până la rezultat |
|
||||
|
||||
#### Response
|
||||
|
||||
**Sync mode (default) — 200 OK** — JSON cu schema completă (vezi [CONTRACT.md](CONTRACT.md)):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"video_path": "video.mp4",
|
||||
"n_frames_extracted": 64,
|
||||
"every_n_frames_used": 3,
|
||||
"auto_skipped": ["m26 (no audio track)"],
|
||||
"modules_run": ["m25", "m27", "m28", "m29"],
|
||||
"execution_time_ms": 48432.0,
|
||||
|
||||
"fusion": {
|
||||
"score": 0.67,
|
||||
"label": "FAKE",
|
||||
"confidence": 0.78,
|
||||
"n_contributing": 4,
|
||||
"n_no_signal": 0,
|
||||
"disagreement": 0.005,
|
||||
"contributions": {"m25": 0.09, "m27": 0.23, "m28": 0.24, "m29": 0.11}
|
||||
},
|
||||
|
||||
"explanation": [
|
||||
"Verdict forensic: FAKE (score=0.67, confidence=0.78, din 4 detectoare active)",
|
||||
"Top contributors: m28 INCERT, m27 FAKE, m25 INCERT"
|
||||
],
|
||||
|
||||
"modules": {
|
||||
"m25": { ... schema completă CONTRACT.md ... },
|
||||
"m27": { ... },
|
||||
"m28": { ... },
|
||||
"m29": { ... }
|
||||
},
|
||||
|
||||
"evidence_text": "FORENSIC EVIDENCE (objective measurements...)\n\n[m25 ...] score=...\n...",
|
||||
|
||||
"images": [
|
||||
{
|
||||
"name": "m25_pulse_signal.png",
|
||||
"tool_id": "m25",
|
||||
"abs_path": "/app/data/results/{job_id}/images/m25_pulse_signal.png",
|
||||
"size_bytes": 69384,
|
||||
"data_url": "data:image/png;base64,iVBORw0KG..." // dacă encode_images=1
|
||||
},
|
||||
// ... 8-15 imagini total
|
||||
],
|
||||
|
||||
"summary": {
|
||||
"overall_score": 0.67,
|
||||
"overall_label": "FAKE",
|
||||
"overall_confidence": 0.78,
|
||||
"n_modules_run": 4,
|
||||
"n_modules_signal": 4,
|
||||
"disagreement": 0.005
|
||||
},
|
||||
|
||||
"instruction_for_llm": "HOW TO USE FORENSIC EVIDENCE ABOVE:\n...",
|
||||
|
||||
"errors": []
|
||||
}
|
||||
```
|
||||
|
||||
**Async mode (`async_mode=1`) — 202 Accepted**:
|
||||
|
||||
```json
|
||||
{
|
||||
"job_id": "abc123def456789a",
|
||||
"status": "queued",
|
||||
"modules": ["m25", "m26", "m27", "m28", "m29"]
|
||||
}
|
||||
```
|
||||
|
||||
Apoi polling pe `/api/status/{job_id}` și preluare cu `/api/result/{job_id}`.
|
||||
|
||||
#### Error codes
|
||||
|
||||
| Code | Cauza |
|
||||
|------|-------|
|
||||
| 400 | Câmp `video` lipsește, tip fișier nesuportat, sau modul necunoscut |
|
||||
| 500 | Eroare în pipeline (vezi mesaj eroare în body) |
|
||||
|
||||
#### Examples
|
||||
|
||||
**Sync, toate modulele**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/forensic-evidence \
|
||||
-F "video=@suspicious_video.mp4"
|
||||
```
|
||||
|
||||
**Sync, doar 2 module + fără base64 (mai rapid)**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/forensic-evidence \
|
||||
-F "video=@image.jpg" \
|
||||
-F "modules=m27,m28" \
|
||||
-F "encode_images=0"
|
||||
```
|
||||
|
||||
**Async, polling**:
|
||||
```bash
|
||||
# Submit
|
||||
JOB=$(curl -s -X POST http://localhost:8080/api/forensic-evidence \
|
||||
-F "video=@long_video.mp4" -F "async_mode=1" \
|
||||
| python -c "import sys,json; print(json.load(sys.stdin)['job_id'])")
|
||||
|
||||
# Wait
|
||||
while [ "$(curl -s http://localhost:8080/api/status/$JOB \
|
||||
| python -c "import sys,json; print(json.load(sys.stdin)['status'])")" != "done" ]; do
|
||||
sleep 5
|
||||
done
|
||||
|
||||
# Get result
|
||||
curl http://localhost:8080/api/result/$JOB | jq .summary
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/forensic-modules`
|
||||
|
||||
Listează modulele disponibile, pentru introspection.
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/api/forensic-modules
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"available_modules": [
|
||||
{"id": "m25", "input_type": "overview_frames",
|
||||
"import_path": "tools.m25_physiology.physiology", "function": "run"},
|
||||
{"id": "m26", "input_type": "video_path",
|
||||
"import_path": "tools.m26_audio.audio", "function": "run"},
|
||||
{"id": "m27", "input_type": "overview_frames",
|
||||
"import_path": "tools.m27_ai_detector.ai_detector", "function": "run"},
|
||||
{"id": "m28", "input_type": "overview_frames",
|
||||
"import_path": "tools.m28_forgery_heatmap.forgery_heatmap", "function": "run"},
|
||||
{"id": "m29", "input_type": "overview_frames",
|
||||
"import_path": "tools.m29_lighting.lighting", "function": "run"}
|
||||
],
|
||||
"default": ["m25", "m26", "m27", "m28", "m29"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/status/{job_id}`
|
||||
|
||||
Polling pentru cereri async. Returns 200 cu status string.
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/api/status/abc123def456789a
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"job_id": "abc123def456789a",
|
||||
"status": "running",
|
||||
"progress": "Running forensic modules m25-m29..."
|
||||
}
|
||||
```
|
||||
|
||||
Status values: `queued`, `running`, `done`, `error`.
|
||||
|
||||
Error code 404 dacă job_id nu există.
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/result/{job_id}`
|
||||
|
||||
Preluare rezultat job async. Comportament:
|
||||
|
||||
| Job status | Răspuns |
|
||||
|------------|--------------------------------------------|
|
||||
| `queued` sau `running` | 202 cu status + progress |
|
||||
| `done` | 200 cu rezultatul complet (același ca sync)|
|
||||
| `error` | 500 cu mesajul de eroare |
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/api/result/abc123def456789a
|
||||
```
|
||||
|
||||
Error code 404 dacă job_id nu există.
|
||||
|
||||
---
|
||||
|
||||
### `GET /health`
|
||||
|
||||
Healthcheck pentru Docker / load balancer / monitoring.
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"service": "forensic-features",
|
||||
"modules": ["m25", "m26", "m27", "m28", "m29"]
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
### Limita upload
|
||||
|
||||
`client_max_size=2 * 1024**3` = **2 GB** per request. Pe video >2GB,
|
||||
preprocesezi cu ffmpeg înainte de upload.
|
||||
|
||||
### Concurența
|
||||
|
||||
Job store este **in-memory** (Python dict). Asta înseamnă:
|
||||
- Joburile se pierd la restart container
|
||||
- Multi-replica fără sticky sessions = nu funcționează
|
||||
|
||||
Pentru producție serioasă, înlocuiește `_jobs` din `api.py:30` cu un store
|
||||
persistent (Redis recomandat).
|
||||
|
||||
### CORS
|
||||
|
||||
Activat default cu `*` pentru orice origine. Pentru producție public,
|
||||
restrictionează în `api.py:build_app()`.
|
||||
|
||||
### Rate limiting
|
||||
|
||||
**NU există**. Pentru producție public, pune un reverse proxy
|
||||
(nginx, Traefik) în față cu rate limiting.
|
||||
|
||||
### Auth
|
||||
|
||||
**NU există**. Endpoint-urile sunt deschise. Pentru producție, integrează cu
|
||||
sistemul tău de auth via reverse proxy sau adaugă middleware.
|
||||
235
ai_platform/modules/forensic_features/docs/ARCHITECTURE.md
Normal file
235
ai_platform/modules/forensic_features/docs/ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
# 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_id` UUID hex 16 chars
|
||||
- Salvare temporară la `/app/data/inference/{job_id}/`
|
||||
|
||||
### 2. Orchestrator setup (forensic/orchestrator.py:75-110)
|
||||
- **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: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()`:
|
||||
|
||||
```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.
|
||||
246
ai_platform/modules/forensic_features/docs/CONTRACT.md
Normal file
246
ai_platform/modules/forensic_features/docs/CONTRACT.md
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
# Output Schema Contract
|
||||
|
||||
Schema completă returnată de `POST /api/forensic-evidence`. Acest contract
|
||||
este **stabil** între versiuni — clienții se pot baza pe câmpurile listate aici.
|
||||
|
||||
## Top-level structure
|
||||
|
||||
```jsonc
|
||||
{
|
||||
// ── Metadata request ─────────────────────────────────────────────────
|
||||
"video_path": "video.mp4", // string — numele fișierului uploadat
|
||||
"n_frames_extracted": 64, // int — câte cadre s-au extras cu ffmpeg
|
||||
"every_n_frames_used": 3, // int — pasul efectiv folosit
|
||||
"auto_skipped": ["m26 (no audio)"], // array<string> — module sărite + motiv
|
||||
"modules_run": ["m25", "m27", "m28", "m29"], // array<string> — IDs efectiv rulate
|
||||
"execution_time_ms": 48432.0, // float — timp total în ms
|
||||
|
||||
// ── Verdict fuzionat (info, NU obligatoriu) ──────────────────────────
|
||||
"fusion": {
|
||||
"score": 0.67, // float 0..1 | null
|
||||
"label": "FAKE", // "FAKE" | "REAL" | "INCERT" | "NO_SIGNAL"
|
||||
"confidence": 0.78, // float 0..1
|
||||
"n_contributing": 4, // int — module care au contribuit
|
||||
"n_no_signal": 0, // int — module fără semnal
|
||||
"disagreement": 0.005, // float — varianța scorurilor
|
||||
"contributions": { // dict<module_id, float> — pondere efectivă
|
||||
"m25": 0.09,
|
||||
"m27": 0.23,
|
||||
"m28": 0.24,
|
||||
"m29": 0.11
|
||||
}
|
||||
},
|
||||
|
||||
// ── Explicație human-readable (1-3 propoziții) ───────────────────────
|
||||
"explanation": [
|
||||
"Verdict forensic: FAKE (score=0.67, confidence=0.78, din 4 detectoare active)",
|
||||
"Top contributors: m28 INCERT, m27 FAKE, m25 INCERT"
|
||||
],
|
||||
|
||||
// ── Output per modul (cel mai important pentru parsing detaliat) ─────
|
||||
"modules": {
|
||||
"m25": { ... vezi „Per-module schema" mai jos ... },
|
||||
"m27": { ... },
|
||||
"m28": { ... },
|
||||
"m29": { ... }
|
||||
},
|
||||
|
||||
// ── Text formatat pentru injectare directă în prompt LLM ─────────────
|
||||
"evidence_text": "FORENSIC EVIDENCE (objective measurements)...", // string ~1500-3000 chars
|
||||
|
||||
// ── Imagini pentru atașare la apel multimodal LLM ────────────────────
|
||||
"images": [
|
||||
{
|
||||
"name": "m25_pulse_signal.png", // numele fișierului
|
||||
"tool_id": "m25", // care modul l-a generat
|
||||
"abs_path": "/app/data/results/{job_id}/images/m25_pulse_signal.png",
|
||||
"size_bytes": 69384, // dimensiune pe disk
|
||||
"data_url": "data:image/png;base64,..." // OPTIONAL, doar dacă encode_images=1
|
||||
}
|
||||
// ... 8-15 imagini total tipic
|
||||
],
|
||||
|
||||
// ── Summary structurat pentru parsing programatic ────────────────────
|
||||
"summary": {
|
||||
"overall_score": 0.67, // = fusion.score (duplicat pentru convenience)
|
||||
"overall_label": "FAKE",
|
||||
"overall_confidence": 0.78,
|
||||
"n_modules_run": 4,
|
||||
"n_modules_signal": 4,
|
||||
"disagreement": 0.005
|
||||
},
|
||||
|
||||
// ── Instrucțiune fixă pentru LLM (cum să folosească evidence) ────────
|
||||
"instruction_for_llm": "HOW TO USE FORENSIC EVIDENCE ABOVE:\n- These are objective...",
|
||||
|
||||
// ── Erori globale (NU per modul) ─────────────────────────────────────
|
||||
"errors": []
|
||||
}
|
||||
```
|
||||
|
||||
## Per-module schema
|
||||
|
||||
Fiecare modul în `modules.{module_id}` are **EXACT aceeași structură**:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
// ── Identificare ─────────────────────────────────────────────────────
|
||||
"tool": {
|
||||
"id": "m25", // string — module ID stabil
|
||||
"name": "Physiology", // human-readable
|
||||
"version": "1.0", // versiune algoritm (semantic)
|
||||
"input_type": "overview_frames" // "overview_frames" | "video_path"
|
||||
},
|
||||
|
||||
// ── Sumar — primary contract LLM ─────────────────────────────────────
|
||||
"summary": {
|
||||
// Câmpuri OBLIGATORII (toate modulele le au):
|
||||
"frames_analyzed": 20, // int — cadre procesate efectiv
|
||||
"frames_with_signal": 18, // int — cadre cu semnal valid
|
||||
"primary_score": 0.63, // float 0..1 | null
|
||||
// 1.0 = maxim suspect FAKE
|
||||
// 0.0 = maxim natural REAL
|
||||
// null = NU s-a putut calcula
|
||||
"primary_label": "INCERT", // derivat din primary_score:
|
||||
// null → "NO_SIGNAL"
|
||||
// >=0.65 → "FAKE"
|
||||
// <=0.35 → "REAL"
|
||||
// else → "INCERT"
|
||||
"confidence": 0.41, // float 0..1 — încredere în primary_score
|
||||
"evidence": [ // array<string>, max 5 — pentru LLM
|
||||
"Pulse: 82 BPM, SNR=1.1 dB (no plausible cardiac signal)",
|
||||
"Blink count: 1 over 5.1s (natural)"
|
||||
],
|
||||
|
||||
// Câmpuri SPECIFICE modulului (vezi docs/MODULES.md pentru lista completă):
|
||||
"pulse_bpm": 82.35, // (m25)
|
||||
"pulse_snr_db": 1.1, // (m25)
|
||||
"blink_count_total": 1, // (m25)
|
||||
"blink_asymmetry_ms": null, // (m25)
|
||||
"lip_sync_offset_ms": 187.0, // (m26, doar dacă audio prezent)
|
||||
"f0_std_hz": 12.3, // (m26)
|
||||
"voice_ratio": 0.95, // (m26)
|
||||
"npr_score_mean": 0.48, // (m27)
|
||||
"jpeg_recon_score_mean": 0.78, // (m27)
|
||||
"hf_score_mean": null, // (m27, doar dacă M27_USE_HF=1)
|
||||
"peak_suspicion_max": 0.90, // (m28)
|
||||
"boundary_mean_suspicion": 0.137, // (m28)
|
||||
"lighting_mismatch_deg_mean": 95.0, // (m29)
|
||||
"catchlight_consistency_mean": null // (m29)
|
||||
// ... (vezi MODULES.md pentru lista exhaustivă per modul)
|
||||
},
|
||||
|
||||
// ── Per-frame breakdown (opțional, pentru debug) ─────────────────────
|
||||
"per_frame": [
|
||||
{
|
||||
"frame_index": 0,
|
||||
"signal_present": true,
|
||||
// câmpuri specifice modulului
|
||||
"ear_left": 0.31,
|
||||
"ear_right": 0.29
|
||||
}
|
||||
// ...
|
||||
],
|
||||
|
||||
// ── Metrici la nivel de clip (date raw care nu sunt în summary) ──────
|
||||
"metrics": {
|
||||
"rppg_samples": 20,
|
||||
"blinks_left": [{...}], // detalii per blink (m25)
|
||||
"blinks_right": [{...}],
|
||||
// ...
|
||||
},
|
||||
|
||||
// ── PNG-uri generate de acest modul ──────────────────────────────────
|
||||
"artifacts": {
|
||||
"images": [
|
||||
"m25_pulse_signal.png",
|
||||
"m25_blink_timeline.png"
|
||||
]
|
||||
},
|
||||
|
||||
"errors": [], // array<string> — erori non-fatale
|
||||
"warnings": ["MediaPipe lipsă; fallback la Haar"],
|
||||
|
||||
"execution_time_ms": 1247.3 // float — timp execuție modul
|
||||
}
|
||||
```
|
||||
|
||||
## Reguli stricte (garantate de `tools/_contract.py`)
|
||||
|
||||
1. **`primary_score`** este **întotdeauna** în [0, 1] sau `null`.
|
||||
|
||||
2. **`primary_label`** este derivat strict din `primary_score`:
|
||||
| Score range | Label |
|
||||
|-------------|-------|
|
||||
| `null` | `"NO_SIGNAL"` |
|
||||
| `>= 0.65` | `"FAKE"` |
|
||||
| `<= 0.35` | `"REAL"` |
|
||||
| `(0.35, 0.65)` | `"INCERT"` |
|
||||
|
||||
3. **`confidence`** reflectă cât material valid a avut tool-ul. Video scurte
|
||||
sau cu puține cadre valide → confidence scăzut.
|
||||
|
||||
4. **`evidence`** conține **max 5** propoziții human-readable, fără jargon
|
||||
tehnic excesiv. Gata de inserat în prompt LLM.
|
||||
|
||||
5. **`artifacts.images`** conține **doar nume de fișier** (nu paths absolute).
|
||||
Paths se construiesc cu `results_dir/images/{name}`.
|
||||
|
||||
6. **`errors`** se umple **doar cu erori non-fatale** (modul produce un
|
||||
rezultat parțial). Crash → excepție → orchestrator capturează în `errors`
|
||||
global, nu per modul.
|
||||
|
||||
7. **NU există NaN/Inf** în output JSON. Toate float-urile sunt finite sau `null`.
|
||||
|
||||
8. **NO_SIGNAL este normal**, NU eroare. Modul care nu poate calcula nimic
|
||||
(ex. m26 fără audio, m25 cu prea puține cadre) returnează:
|
||||
```json
|
||||
{
|
||||
"summary": {
|
||||
"primary_score": null,
|
||||
"primary_label": "NO_SIGNAL",
|
||||
"confidence": 0.0,
|
||||
"evidence": ["Reason: no audio track in video"]
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Versionare
|
||||
|
||||
Versiunea schemei e implicită în structură. Schimbări breaking sunt anunțate
|
||||
prin **versiune nouă a modulului** (`tool.version`). Câmpurile noi pot fi
|
||||
adăugate la `summary` fără să rupă clienții existenți care le ignoră.
|
||||
|
||||
Câmpurile **OBLIGATORII** listate mai sus sunt stabile între versiuni.
|
||||
|
||||
## Validare schema în client
|
||||
|
||||
```python
|
||||
def validate_response(data: dict) -> bool:
|
||||
required_top = {"video_path", "modules_run", "fusion", "modules",
|
||||
"evidence_text", "images", "summary"}
|
||||
if not required_top.issubset(data.keys()):
|
||||
return False
|
||||
|
||||
for mid, mod in data["modules"].items():
|
||||
if "summary" not in mod or "tool" not in mod:
|
||||
return False
|
||||
s = mod["summary"]
|
||||
required_summary = {"primary_score", "primary_label",
|
||||
"confidence", "evidence"}
|
||||
if not required_summary.issubset(s.keys()):
|
||||
return False
|
||||
|
||||
return True
|
||||
```
|
||||
|
||||
## Câmpuri specifice complete per modul
|
||||
|
||||
Pentru lista exhaustivă a câmpurilor `summary.tool_specific_field_*` pentru
|
||||
fiecare modul m25-m29, vezi:
|
||||
- [MODULES.md](MODULES.md) — secțiunea "Output specific" pentru fiecare modul
|
||||
|
||||
Acestea sunt **opționale** pentru clienți — toți ar trebui să se bazeze pe
|
||||
câmpurile obligatorii (primary_score, primary_label, confidence, evidence)
|
||||
plus `evidence_text` la nivel top-level care e gata pentru LLM.
|
||||
366
ai_platform/modules/forensic_features/docs/INTEGRATION.md
Normal file
366
ai_platform/modules/forensic_features/docs/INTEGRATION.md
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
# Integrare în aplicația ta LLM
|
||||
|
||||
Document care arată **EXACT** cum apelezi acest serviciu din aplicația ta
|
||||
care folosește deja un LLM multimodal (Qwen Vision, GPT-4V, Claude Sonnet).
|
||||
|
||||
## Pattern de bază
|
||||
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ User uploadează video │
|
||||
│ în aplicația ta │
|
||||
└───────────┬─────────────┘
|
||||
│
|
||||
├─ Pas 1: Trimite videoul la Forensic Features API
|
||||
│ → primești evidence_text + base64 imagini
|
||||
│
|
||||
├─ Pas 2: Construiește prompt-ul TĂU existent
|
||||
│ (cu tipologiile tale, instrucțiunile tale)
|
||||
│ + APPEND evidence_text
|
||||
│
|
||||
├─ Pas 3: Atașează la apelul LLM:
|
||||
│ - imaginile originale ale userului
|
||||
│ - imaginile noastre (heatmap-uri)
|
||||
│
|
||||
└─ Pas 4: LLM-ul tău returnează verdictul
|
||||
cu signal îmbogățit de la noi
|
||||
```
|
||||
|
||||
## Exemple complete
|
||||
|
||||
### Python — Qwen Vision (compatible cu OpenAI ChatCompletions API)
|
||||
|
||||
```python
|
||||
import requests
|
||||
import base64
|
||||
from pathlib import Path
|
||||
|
||||
FORENSIC_API = "http://localhost:8080"
|
||||
QWEN_API = "http://your-qwen-host:14011/v1/chat/completions"
|
||||
|
||||
def analyze_video(video_path: str, your_typologies: list[str]) -> dict:
|
||||
"""
|
||||
Apelează Forensic Features API → construiește prompt → apelează Qwen.
|
||||
"""
|
||||
|
||||
# ── Pas 1: Forensic Features API ──────────────────────────────────
|
||||
with open(video_path, "rb") as f:
|
||||
resp = requests.post(
|
||||
f"{FORENSIC_API}/api/forensic-evidence",
|
||||
files={"video": f},
|
||||
data={
|
||||
"encode_images": "1", # cu base64 pentru atașare directă
|
||||
# Optional: "modules": "m25,m27,m28" pt doar 3 module
|
||||
},
|
||||
timeout=300,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
forensic = resp.json()
|
||||
|
||||
# ── Pas 2: Construiește mesajul multimodal pentru LLM ─────────────
|
||||
# PROMPT-UL TĂU EXISTENT — așa cum îl ai acum
|
||||
your_prompt = f"""
|
||||
Ești un analist video forensic. Analizează acest video și verifică:
|
||||
|
||||
Tipologii de elemente vizuale de căutat:
|
||||
{chr(10).join(f"- {t}" for t in your_typologies)}
|
||||
|
||||
Răspunde structurat...
|
||||
"""
|
||||
|
||||
# Append evidence-ul nostru
|
||||
augmented_prompt = your_prompt + "\n\n" + forensic["evidence_text"]
|
||||
|
||||
# ── Pas 3: Construiește content multimodal ───────────────────────
|
||||
# Lista de imagini originale ale userului (din aplicația ta) +
|
||||
# imaginile noastre forensice (heatmap-uri, plot-uri)
|
||||
content = [
|
||||
{"type": "text", "text": augmented_prompt},
|
||||
]
|
||||
|
||||
# Imaginile TALE existente — pe care le aveai deja în pipeline
|
||||
your_keyframes = extract_keyframes_yourself(video_path) # funcția ta existentă
|
||||
for img_path in your_keyframes:
|
||||
img_b64 = base64.b64encode(open(img_path, "rb").read()).decode()
|
||||
content.append({
|
||||
"type": "image_url",
|
||||
"image_url": {"url": f"data:image/jpeg;base64,{img_b64}"},
|
||||
})
|
||||
|
||||
# Imaginile NOASTRE forensice — heatmap-uri zone suspect, lighting arrows
|
||||
for img in forensic["images"]:
|
||||
if "data_url" in img:
|
||||
content.append({
|
||||
"type": "image_url",
|
||||
"image_url": {"url": img["data_url"]},
|
||||
})
|
||||
|
||||
# ── Pas 4: Apelează LLM-ul tău ───────────────────────────────────
|
||||
qwen_payload = {
|
||||
"model": "Qwen3.5-397B-A17B",
|
||||
"messages": [
|
||||
{"role": "system", "content": "Ești analist forensic. Răspunzi în JSON valid."},
|
||||
{"role": "user", "content": content},
|
||||
],
|
||||
"max_tokens": 1500,
|
||||
"temperature": 0.1,
|
||||
}
|
||||
|
||||
qwen_resp = requests.post(QWEN_API, json=qwen_payload, timeout=120)
|
||||
qwen_resp.raise_for_status()
|
||||
|
||||
# Parse JSON din răspuns
|
||||
import json
|
||||
llm_text = qwen_resp.json()["choices"][0]["message"]["content"]
|
||||
verdict = json.loads(llm_text)
|
||||
|
||||
return {
|
||||
"verdict": verdict, # ce decide LLM-ul tău
|
||||
"forensic_evidence": forensic, # pentru debug / audit
|
||||
"augmented_prompt": augmented_prompt, # pentru replicare
|
||||
}
|
||||
|
||||
|
||||
# Usage
|
||||
result = analyze_video(
|
||||
"user_uploaded.mp4",
|
||||
your_typologies=["face_swap_visible", "background_anomaly", "logo_overlay"],
|
||||
)
|
||||
print(result["verdict"])
|
||||
```
|
||||
|
||||
### JavaScript / Node — direct fetch
|
||||
|
||||
```javascript
|
||||
async function analyzeWithForensic(videoFile) {
|
||||
// Pas 1: Forensic Features
|
||||
const fd = new FormData();
|
||||
fd.append("video", videoFile);
|
||||
fd.append("encode_images", "1");
|
||||
|
||||
const forensicResp = await fetch("http://localhost:8080/api/forensic-evidence", {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
});
|
||||
const forensic = await forensicResp.json();
|
||||
|
||||
// Pas 2: Construiește content multimodal pentru LLM-ul tău
|
||||
const content = [
|
||||
{ type: "text", text: yourExistingPrompt + "\n\n" + forensic.evidence_text }
|
||||
];
|
||||
|
||||
// Imaginile tale + ale noastre
|
||||
for (const img of yourKeyframes) {
|
||||
content.push({ type: "image_url", image_url: { url: img.dataUrl } });
|
||||
}
|
||||
for (const img of forensic.images) {
|
||||
if (img.data_url) {
|
||||
content.push({ type: "image_url", image_url: { url: img.data_url } });
|
||||
}
|
||||
}
|
||||
|
||||
// Pas 3: Apelează LLM-ul tău
|
||||
const llmResp = await callYourLLM({ content });
|
||||
return llmResp;
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern async pentru video lung
|
||||
|
||||
Pe video >30 secunde, procesarea poate dura 1-5 minute. Folosește **async mode**:
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
def analyze_long_video_async(video_path):
|
||||
# Submit
|
||||
with open(video_path, "rb") as f:
|
||||
resp = requests.post(
|
||||
f"{FORENSIC_API}/api/forensic-evidence",
|
||||
files={"video": f},
|
||||
data={"async_mode": "1"},
|
||||
)
|
||||
job_id = resp.json()["job_id"]
|
||||
print(f"Job submitted: {job_id}")
|
||||
|
||||
# Poll
|
||||
while True:
|
||||
status_resp = requests.get(f"{FORENSIC_API}/api/status/{job_id}")
|
||||
status = status_resp.json()
|
||||
print(f"Status: {status['status']} — {status['progress']}")
|
||||
|
||||
if status["status"] == "done":
|
||||
break
|
||||
if status["status"] == "error":
|
||||
raise Exception(f"Forensic pipeline failed: {status}")
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
# Retrieve
|
||||
result_resp = requests.get(f"{FORENSIC_API}/api/result/{job_id}")
|
||||
return result_resp.json()
|
||||
```
|
||||
|
||||
## Cum interpretează LLM-ul tău evidence-ul
|
||||
|
||||
Când inserezi `evidence_text` în prompt, LLM-ul vede ceva de genul:
|
||||
|
||||
```
|
||||
FORENSIC EVIDENCE (objective measurements you cannot recompute)
|
||||
======================================================================
|
||||
|
||||
OVERALL VERDICT: FAKE (score=0.67, confidence=0.78)
|
||||
Detectors active: 4, no signal: 0, disagreement: 0.005
|
||||
|
||||
Individual detectors:
|
||||
----------------------------------------------------------------------
|
||||
[m25 Physiology] score=0.63 (INCERT) confidence=0.41 contrib=+0.091
|
||||
- Pulse: 82 BPM, SNR=1.1 dB (no plausible cardiac signal)
|
||||
- Blink count: 1 over 5.1s (natural)
|
||||
Visuals: m25_pulse_signal.png, m25_blink_timeline.png
|
||||
|
||||
[m27 AI-Generated Image Detector] score=0.78 (FAKE) confidence=1.00 contrib=+0.231
|
||||
- JPEG-recon: 0.78 (above 0.65 = AI suspect)
|
||||
- NPR: 0.48
|
||||
Visuals: m27_score_timeline.png
|
||||
|
||||
[m28 Forgery Localization Heatmap] score=0.62 (INCERT) confidence=1.00 contrib=+0.237
|
||||
- Forgery score: 0.62 (peak=0.90, boundary_mean=0.137) — intermediate
|
||||
- Frames with face: 5/5
|
||||
Visuals: m28_heatmap_0000.png, ..., m28_heatmap_0040.png
|
||||
|
||||
[m29 Lighting 3D Consistency] score=0.61 (INCERT) confidence=0.76
|
||||
- Face vs scene lighting: 95° (mismatch >90°, suspect compus)
|
||||
- Catchlights: nedetectabile (ochi închiși/ochelari/rezoluție mică)
|
||||
Visuals: m29_lighting_0000.png, ..., m29_lighting_0030.png
|
||||
|
||||
======================================================================
|
||||
HOW TO USE FORENSIC EVIDENCE ABOVE:
|
||||
- These are objective numerical measurements that you CANNOT recompute from
|
||||
images alone. They are produced by classical signal-processing detectors...
|
||||
- For each detector that flags FAKE, search the keyframes for the visual
|
||||
artifact that explains the score...
|
||||
======================================================================
|
||||
```
|
||||
|
||||
LLM-ul are toate aceste informații + imaginile reale + tipologiile tale.
|
||||
Combinat cu reasoning-ul lui semantic, decide singur cu signal MULT mai bogat
|
||||
decât doar din imagine.
|
||||
|
||||
## Best practices
|
||||
|
||||
### 1. Cache rezultatul forensic per video
|
||||
|
||||
Forensic features sunt **deterministe** pe același video. Cache prin
|
||||
hash SHA256 al fișierului — economie de zeci de secunde per request repetat.
|
||||
|
||||
```python
|
||||
import hashlib
|
||||
|
||||
def video_hash(path):
|
||||
with open(path, "rb") as f:
|
||||
return hashlib.sha256(f.read()).hexdigest()[:16]
|
||||
|
||||
# Cache in Redis sau SQLite cu key = video_hash
|
||||
```
|
||||
|
||||
### 2. Selectează module relevante per tip conținut
|
||||
|
||||
Nu rula toate cele 5 mereu:
|
||||
- **Imagine statică** (jpg/png) → m27 + m28 (rest sunt no-op temporale)
|
||||
- **Talking head video** → m25 + m26 + m28 + m29
|
||||
- **AI-generated landscape** (fără față) → m27 doar
|
||||
- **Screen recording** → m24 (din pipeline vechi v3, NU în această versiune)
|
||||
|
||||
Setezi cu `-F "modules=m27,m28"`.
|
||||
|
||||
### 3. Truncare evidence_text pe LLM cu context mic
|
||||
|
||||
Dacă LLM-ul tău are context window mic (<8K), poți cere doar `summary`:
|
||||
|
||||
```python
|
||||
# În prompt, în loc de evidence_text complet (1500-3000 chars), folosește:
|
||||
short_evidence = f"""
|
||||
Forensic signals on this video:
|
||||
- Overall: {forensic['fusion']['label']} (score={forensic['fusion']['score']:.2f})
|
||||
- m25 Physiology: {forensic['modules']['m25']['summary']['primary_label']}
|
||||
- m27 AI Detector: {forensic['modules']['m27']['summary']['primary_label']}
|
||||
- m28 Blending: {forensic['modules']['m28']['summary']['primary_label']}
|
||||
"""
|
||||
```
|
||||
|
||||
### 4. Atașează DOAR cele mai relevante PNG-uri
|
||||
|
||||
Pe LLM cu limite multimodal (4-6 imagini per call), nu trimite toate 12-15
|
||||
din output. Filtrează:
|
||||
|
||||
```python
|
||||
# Doar PNG-uri din module flagged FAKE
|
||||
relevant_images = [
|
||||
img for img in forensic["images"]
|
||||
if forensic["modules"][img["tool_id"]]["summary"]["primary_label"] == "FAKE"
|
||||
]
|
||||
```
|
||||
|
||||
### 5. Loghează verdictul + evidence pentru audit
|
||||
|
||||
```python
|
||||
# Salvează atât verdictul LLM cât și evidence-ul nostru
|
||||
# pentru audit ulterior și calibrare
|
||||
audit_log.write({
|
||||
"video_id": video_hash(video_path),
|
||||
"forensic_fusion": forensic["fusion"],
|
||||
"llm_verdict": verdict,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
})
|
||||
```
|
||||
|
||||
## Health check și monitoring
|
||||
|
||||
```python
|
||||
# Verifică serviciul e disponibil înainte de a procesa
|
||||
def is_forensic_healthy():
|
||||
try:
|
||||
r = requests.get(f"{FORENSIC_API}/health", timeout=5)
|
||||
return r.status_code == 200 and r.json().get("status") == "ok"
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# Fallback graceful dacă serviciul e down
|
||||
if not is_forensic_healthy():
|
||||
logger.warning("Forensic features API down, proceeding without augmentation")
|
||||
# Doar apel LLM cu prompt-ul tău original, fără evidence
|
||||
else:
|
||||
# Apel complet cu augmentation
|
||||
```
|
||||
|
||||
## Limitări la integrare
|
||||
|
||||
1. **Cost de timp**: +5-90s pe request (depinde de durata video). Pentru
|
||||
UX, folosește async mode cu indicator de progres.
|
||||
|
||||
2. **Cost de tokens LLM**: evidence_text adaugă ~500-1000 tokens.
|
||||
Imaginile noastre ~12-15 imagini × cost per image LLM.
|
||||
|
||||
3. **Determinism**: forensic features sunt deterministe, dar LLM-ul nu.
|
||||
Pe același video, evidence e mereu același, dar verdict LLM poate varia.
|
||||
|
||||
4. **Limită upload**: 2 GB max. Video peste asta — split sau downscale înainte.
|
||||
|
||||
## Test live
|
||||
|
||||
```bash
|
||||
# Verifică serviciu
|
||||
curl http://localhost:8080/health
|
||||
|
||||
# Test cu un video real
|
||||
curl -X POST http://localhost:8080/api/forensic-evidence \
|
||||
-F "video=@your_test_video.mp4" \
|
||||
-o response.json
|
||||
|
||||
# Extrage doar partea text pentru LLM
|
||||
python -c "import json; print(json.load(open('response.json'))['evidence_text'])"
|
||||
|
||||
# Numără imaginile generate
|
||||
python -c "import json; print(f'{len(json.load(open(\"response.json\"))[\"images\"])} PNG-uri pentru LLM')"
|
||||
```
|
||||
496
ai_platform/modules/forensic_features/docs/MODULES.md
Normal file
496
ai_platform/modules/forensic_features/docs/MODULES.md
Normal 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`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue