{
  "contract_version": "gab_134_contract_v1",
  "gab_id": "GAB-134",
  "canonical_name": "ExerciseAudioPrompt",
  "module_owner": "EdTechExerciseLearning",
  "renderer_key": "media_viewer",
  "required_fields": [
    "gab_id",
    "exercise_id",
    "instruction",
    "audio_tts_text",
    "transcript",
    "question",
    "options"
  ],
  "optional_fields": [
    "title",
    "audio_src",
    "transcript_label",
    "feedback_correct",
    "feedback_incorrect",
    "accessibility"
  ],
  "field_types": {
    "instruction": "string — consigne affichée au-dessus du player (ex: '🎧 Écoute (ou lis) puis réponds')",
    "audio_src": "string — URL ou chemin vers le fichier audio réel (optionnel ; fallback: speechSynthesis sur audio_tts_text)",
    "audio_tts_text": "string — texte lu par speechSynthesis si audio_src absent ou en fallback visuel",
    "transcript": "string — transcription complète de l'audio, TOUJOURS affichée",
    "transcript_label": "string — libellé de l'en-tête transcript (ex: 'Transcript · toujours fourni')",
    "question": "string — question posée à l'élève après écoute",
    "options": "array<{id:string, label:string, correct:boolean}> — au moins 2 options, exactement 1 correct:true",
    "feedback_correct": "string — message affiché si bonne réponse",
    "feedback_incorrect": "string — message affiché si mauvaise réponse"
  },
  "constraints": [
    "transcript est OBLIGATOIRE et TOUJOURS visible — un exercice audio sans transcript est INTERDIT (RGPD/accessibilité).",
    "options : exactement 1 élément avec correct:true.",
    "options : minimum 2 éléments.",
    "audio_tts_text sert de fallback speechSynthesis si audio_src absent.",
    "Après sélection d'une option, toutes les options sont désactivées (no double-submit)."
  ],
  "blocked_conditions": [
    "transcript absent ou vide (BLOCKED)",
    "question absente (BLOCKED)",
    "options absentes ou vides (BLOCKED)",
    "aucune option avec correct:true (BLOCKED)"
  ],
  "accessibility": [
    "keyboard_navigable",
    "focus_visible",
    "prefers_reduced_motion",
    "transcript_always_visible",
    "audio_without_transcript_forbidden"
  ],
  "qa_cases": [
    { "case": "instance conforme", "expected": "rendu complet, transcript visible, options cliquables, 0 erreur" },
    { "case": "transcript absent", "expected": "BLOCKED listant le champ manquant" },
    { "case": "options vides", "expected": "BLOCKED listant le champ manquant" },
    { "case": "aucun correct:true", "expected": "BLOCKED — impossible de valider" },
    { "case": "click bonne réponse", "expected": "option colorée mint, feedback_correct affiché, toutes options disabled" },
    { "case": "click mauvaise réponse", "expected": "option colorée coral, bonne réponse révélée mint, feedback_incorrect affiché" },
    { "case": "instance externe injectée (init(ext))", "expected": "rendu change sans modifier le HTML" },
    { "case": "responsive 375/768/1024", "expected": "aucun débordement horizontal" }
  ],
  "traceability": {
    "derived_from_core_gab": "GAB-134",
    "note": "Ce schema VALIDE l'instance. Le contrat pédagogique complet (input_contract/validation_logic/feedback_scoring_logic) vit dans le CORE-GAB officiel, pas ici (évite la duplication). renderer_key 'media_viewer' = à concevoir (pas de moteur kit de référence) — renderer dérivé de l'écran source INDEX-300."
  }
}
