{
  "contract_version": "gab_171_contract_v1",
  "gab_id": "GAB-171",
  "canonical_name": "AudioReadingCoach",
  "module_owner": "EdTechAudioLearning",
  "renderer_key": "media_viewer",
  "required_fields": [
    "gab_id",
    "title",
    "transcript",
    "items"
  ],
  "optional_fields": [
    "audio_reading_coach_id",
    "subtitle",
    "listen_label",
    "tts_rate",
    "accessibility"
  ],
  "field_types": {
    "title": "string — titre affiché dans l'en-tête de la carte",
    "subtitle": "string — sous-titre affiché sous le titre",
    "transcript": "string — texte complet du passage (affiché en bas, toujours visible)",
    "items": "array<{phrase_html:string, phrase_plain:string, tips:array<{type:enum['respire','articule','accent'], label:string}>}> — au moins 1 item",
    "listen_label": "string — libellé du bouton d'écoute TTS",
    "tts_rate": "number(0.5..2.0) — vitesse de la synthèse vocale (défaut 0.9)",
    "accessibility": "object{keyboard_navigable:boolean, focus_visible:boolean, prefers_reduced_motion:boolean, fallback_text:string}"
  },
  "constraints": [
    "items doit contenir au moins 1 élément.",
    "Chaque item doit avoir phrase_html (pour l'affichage avec <mark>) et phrase_plain (pour TTS).",
    "tips est optionnel par item mais doit être un tableau s'il est présent.",
    "type dans tips : enum strict 'respire' | 'articule' | 'accent' — sert au style CSS.",
    "transcript doit être non vide : c'est le fallback toujours visible (accessibilité + SEO).",
    "tts_rate dans [0.5, 2.0] si présent."
  ],
  "blocked_conditions": [
    "items absent ou vide (BLOCKED)",
    "transcript absent ou vide (BLOCKED)",
    "title absent (BLOCKED)"
  ],
  "accessibility": [
    "keyboard_navigable",
    "focus_visible",
    "prefers_reduced_motion",
    "fallback_text (transcript complet toujours visible)"
  ],
  "qa_cases": [
    { "case": "instance conforme", "expected": "rendu complet, navigation phrase 1/3 → 3/3, TTS play, 0 erreur" },
    { "case": "champ requis manquant (items vide)", "expected": "BLOCKED affiché, aucun rendu partiel" },
    { "case": "champ requis manquant (transcript absent)", "expected": "BLOCKED affiché" },
    { "case": "instance externe injectée via init(ext)", "expected": "le rendu change sans modifier le HTML" },
    { "case": "bouton Écouter", "expected": "TTS déclenché sur phrase_plain de l'item courant" },
    { "case": "navigation prev/next", "expected": "prev désactivé sur phrase 1, next désactivé sur dernière phrase" },
    { "case": "responsive 375/768/1024", "expected": "aucun débordement horizontal, contrôles wrappent correctement" }
  ],
  "traceability": {
    "derived_from_core_gab": "GAB-171",
    "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)."
  }
}
