# 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 — module sărite + motiv "modules_run": ["m25", "m27", "m28", "m29"], // array — 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 — 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, 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 — 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.