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

10 KiB

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

{
  // ── 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ă:

{
  // ── 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ă:

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

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