{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Storyboard",
  "description": "Canonical storyboard format for the microdrama visual pipeline. Written by /storyboard, edited in Storyboard Editor, consumed by generate_storyboard_keyframes.py. Version 3: generation approach classification, triptych/hero prompts, asset naming convention.",
  "type": "object",
  "required": ["version", "project", "episode", "title", "characters", "location", "cinematic", "lens_package", "beats", "shots"],
  "properties": {
    "version": {
      "type": "integer",
      "const": 3,
      "description": "Schema version. 3 = generation approach classification, triptych/hero prompts, asset naming."
    },
    "project": {
      "type": "string",
      "description": "Project folder name (e.g. 'leviathan')."
    },
    "episode": {
      "type": "integer",
      "minimum": 1,
      "maximum": 60,
      "description": "Episode number."
    },
    "title": {
      "type": "string",
      "description": "Episode title (e.g. 'Salvage')."
    },
    "characters": {
      "type": "object",
      "description": "Character visual descriptions keyed by lowercase name.",
      "additionalProperties": {
        "type": "object",
        "required": ["visual"],
        "properties": {
          "visual": {
            "type": "string",
            "description": "Full visual description for prompt construction."
          },
          "wardrobe": {
            "type": "string",
            "description": "Current wardrobe description for this episode (from breakdown.json wardrobe phase)."
          },
          "hair_makeup": {
            "type": "string",
            "description": "Current hair/makeup state for this episode (from breakdown.json)."
          },
          "reference_images": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Filenames of reference images (loaded in Storyboard Editor)."
          },
          "height_cm": {
            "type": "integer",
            "description": "Character height in cm for relative scaling in multi-character shots."
          },
          "scale_prompt_fragment": {
            "type": "string",
            "description": "Pre-written prose for multi-character scale context. Injected into prompts when 2+ characters share a shot."
          }
        }
      }
    },
    "location": {
      "type": "string",
      "description": "Primary location description for prompt construction."
    },
    "atmosphere": {
      "type": "string",
      "description": "Inferred atmospheric details — fog, dust, temperature, ambient sound textures that imply visual quality. Derived from emotional context and location."
    },
    "color_palette": {
      "type": "array",
      "items": { "type": "string" },
      "description": "HEX color codes for this episode's palette (from visual_bible.md or breakdown.json)."
    },
    "cinematic": {
      "type": "string",
      "description": "Shared cinematic modifiers appended to all prompts (lighting, film grain, etc.)."
    },
    "lens_package": {
      "type": "object",
      "description": "Project lens package — established in Visual Design phase. Limits lens choices to maintain consistent look.",
      "required": ["primary", "close_up", "wide"],
      "properties": {
        "primary": {
          "type": "string",
          "description": "Primary lens for standard coverage (MS, MCU, dialogue). E.g. '50mm f/2.0'."
        },
        "close_up": {
          "type": "string",
          "description": "Close-up lens for ECU, CU, emotional beats. E.g. '85mm f/1.4'."
        },
        "wide": {
          "type": "string",
          "description": "Wide lens for establishing shots, WIDE, environmental. E.g. '24mm f/8'."
        },
        "specialty": {
          "type": "string",
          "default": "",
          "description": "Optional specialty lens for specific shots (macro, tilt-shift, etc.)."
        },
        "film_stock": {
          "type": "string",
          "default": "Kodak Vision3 500T",
          "description": "Film stock reference for overall look (appended to all prompts)."
        }
      }
    },
    "beats": {
      "type": "array",
      "description": "Episode beats in chronological order. Each beat groups related shots.",
      "items": {
        "type": "object",
        "required": ["name", "time"],
        "properties": {
          "name": {
            "type": "string",
            "enum": ["THE HOOK", "THE SETUP", "THE ESCALATION", "THE TURN", "THE CLIFFHANGER"],
            "description": "Kill Box beat name."
          },
          "time": {
            "type": "string",
            "pattern": "^\\d{2}:\\d{2} - \\d{2}:\\d{2}$",
            "description": "Timing range (e.g. '00:00 - 00:05')."
          },
          "script_text": {
            "type": "string",
            "description": "Raw script text for this beat (scene headings, action, dialogue)."
          }
        }
      }
    },
    "shots": {
      "type": "array",
      "description": "Ordered shot list. Each shot maps to a beat and contains all generation parameters.",
      "items": {
        "type": "object",
        "required": ["id", "name", "beat", "shot_type", "subject", "first_frame", "last_frame", "width", "height"],
        "properties": {
          "id": {
            "type": "integer",
            "minimum": 1,
            "description": "Sequential shot number."
          },
          "name": {
            "type": "string",
            "description": "Snake_case descriptive name (e.g. 'debt_counter_pulse')."
          },
          "beat": {
            "type": "string",
            "enum": ["THE HOOK", "THE SETUP", "THE ESCALATION", "THE TURN", "THE CLIFFHANGER"],
            "description": "Which beat this shot belongs to."
          },
          "script_excerpt": {
            "type": "string",
            "description": "The script lines this shot covers."
          },
          "shot_type": {
            "type": "string",
            "enum": ["ECU", "CU", "MCU", "MS", "LS", "WIDE", "POV", "VFX"],
            "description": "Shot size / type."
          },
          "camera_angle": {
            "type": "string",
            "enum": ["eye", "low", "high", "overhead", "dutch"],
            "default": "eye",
            "description": "Camera angle relative to subject."
          },
          "camera_movement": {
            "type": "string",
            "enum": ["static", "pan", "dolly", "track", "handheld", "crane"],
            "default": "static",
            "description": "Camera movement during shot."
          },
          "focal_length": {
            "type": "string",
            "description": "Lens focal length for this shot (from lens_package). E.g. '50mm', '85mm', '24mm'."
          },
          "aperture": {
            "type": "string",
            "description": "Aperture for this shot (from lens_package). E.g. 'f/1.4', 'f/2.0', 'f/8'."
          },
          "subject": {
            "type": "string",
            "description": "What the camera sees (short description)."
          },
          "action": {
            "type": "string",
            "description": "What happens during the shot."
          },
          "description": {
            "type": "string",
            "default": "",
            "description": "Free-form plain-English description of the shot for human directors. Not used in generation prompts."
          },
          "emotion": {
            "type": "string",
            "description": "Dominant emotion (e.g. 'Dread', 'Determination', 'Shock')."
          },
          "atmosphere": {
            "type": "string",
            "description": "Shot-specific atmospheric inference — fog density, particle effects, temperature feel. Derived from emotion + location."
          },
          "lighting": {
            "type": "string",
            "description": "Shot-specific lighting notes (e.g. 'harsh amber emergency lighting, deep shadows')."
          },
          "color_palette": {
            "type": "array",
            "items": { "type": "string" },
            "description": "HEX colors active in this shot (from visual_bible.md palette)."
          },
          "aspect": {
            "type": "string",
            "enum": ["16:9", "9:16"],
            "default": "9:16",
            "description": "Aspect ratio."
          },
          "width": {
            "type": "integer",
            "multipleOf": 16,
            "description": "Frame width in pixels. Must be multiple of 16."
          },
          "height": {
            "type": "integer",
            "multipleOf": 16,
            "description": "Frame height in pixels. Must be multiple of 16."
          },
          "first_frame": {
            "type": "string",
            "description": "Hybrid prose prompt for the first frame — novelistic scene description (30-80 words) combining subject, action, location, atmosphere, and cinematic details."
          },
          "last_frame": {
            "type": "string",
            "description": "Hybrid prose prompt for the last frame — novelistic scene description showing the end state of the shot."
          },
          "generation_metadata": {
            "type": "object",
            "description": "Structured metadata for generation pipeline (camera, lighting, color). Separate from prose prompts.",
            "properties": {
              "camera": {
                "type": "object",
                "properties": {
                  "angle": { "type": "string" },
                  "lens": { "type": "string", "description": "E.g. '85mm f/1.4'" },
                  "depth_of_field": { "type": "string" }
                }
              },
              "lighting": {
                "type": "object",
                "properties": {
                  "type": { "type": "string" },
                  "source": { "type": "string" },
                  "color_temp": { "type": "string" }
                }
              },
              "color_palette": {
                "type": "array",
                "items": { "type": "string" },
                "description": "HEX codes for this shot."
              },
              "film_stock": { "type": "string" },
              "reference_slots": {
                "type": "object",
                "description": "Which Flux 2 reference image slots are active for this shot. Slots 1-5: Character identity (front, profile, 3/4, full body, back), 6: Wardrobe/props, 7-8: Environment, 9: Lighting, 10: Pose/layout (see CONSTANTS.md).",
                "propertyNames": { "pattern": "^([1-9]|10)$" },
                "additionalProperties": { "type": "string" }
              }
            }
          },
          "generation_approach": {
            "type": "string",
            "enum": ["triptych_split_flf", "standard_flf", "held_frame_push", "held_frame_static"],
            "description": "How this shot will be generated. triptych_split_flf: 3-panel strip → split → upscale → two-segment FLF. standard_flf: first+last frames → upscale → single FLF. held_frame_push: single keyframe + Ken Burns push. held_frame_static: single keyframe, no motion."
          },
          "hero_frame": {
            "type": ["string", "null"],
            "default": null,
            "description": "E-style prose (~150-180 words) for the decisive moment. Active verbs, Arri Alexa Mini LF, Kodak Vision3 500T. Triptych shots only."
          },
          "triptych_prompt": {
            "type": ["string", "null"],
            "default": null,
            "description": "Full 3-panel triptych strip prompt (1536x912). Left=anticipation, Center=peak action, Right=aftermath. Triptych shots only."
          },
          "asset_name": {
            "type": "string",
            "description": "Naming convention base following {PRJ}_EP{NNN}_S{NN}_T{NN}_{CHAR} format. Generated by asset_naming.py."
          },
          "characters_in_shot": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Explicit list of character names present in this shot. Lowercase. Empty for ENV/PROP shots."
          },
          "motion_prompt": {
            "type": "string",
            "description": "WAN 2.2 I2V motion description. Active verbs with timing cues (e.g. '0-1s action, 1-2s reaction'). Not used for held_frame_static."
          },
          "director_notes": {
            "type": "string",
            "default": "",
            "description": "Human director notes (added in Storyboard Editor)."
          },
          "seed": {
            "type": "integer",
            "minimum": 1,
            "description": "Generation seed. Set during generation, null in initial JSON."
          },
          "frames": {
            "type": "integer",
            "default": 81,
            "description": "Video frame count (81 = 5s at 16fps, 41 = 2.5s)."
          },
          "notes": {
            "type": "string",
            "default": "",
            "description": "Production notes (VFX needs, compositing, etc.)."
          },
          "preview_path": {
            "type": ["string", "null"],
            "default": null,
            "description": "Path to preview image (set after frame generation or manual load)."
          },
          "prompt_mode": {
            "type": "string",
            "enum": ["legacy", "structured"],
            "default": "legacy",
            "description": "Prompt construction mode. 'legacy': pre-baked prose in first_frame/last_frame. 'structured': 10-layer prompt engine with verb states."
          },
          "hero_action": {
            "type": ["string", "null"],
            "default": null,
            "description": "Decisive moment verb state — ACTIVE verbs, peak action with 2+ physical micro-details. E.g. 'Wrenches salvage hook mid-pull, body torqued into leverage, rust cascading from seam'."
          },
          "anticipation_action": {
            "type": ["string", "null"],
            "default": null,
            "description": "First frame verb state — PREPARATION verbs. E.g. 'Braces against wall, fingers finding the corroded seam, testing the give'."
          },
          "aftermath_action": {
            "type": ["string", "null"],
            "default": null,
            "description": "Last frame verb state — RESULT verbs. E.g. 'Stumbles back as panel scrapes free, breathing hard, hook trailing rust dust'."
          },
          "edge_continuity": {
            "type": ["object", "null"],
            "default": null,
            "description": "Prompt-level inheritance for angle-change cuts. Carries environment/lighting/color DNA from previous shot without img2img (different composition).",
            "properties": {
              "spatial_note": {
                "type": "string",
                "description": "Describes the spatial relationship at the cut boundary. E.g. 'Reverse angle — Kian's face, Jinx's arm entering from camera-right'."
              },
              "inherit_layers": {
                "type": "array",
                "items": { "type": "string", "enum": ["environment", "lighting", "color_objects", "wardrobe_props", "film_style"] },
                "description": "Which prompt layers carry forward from previous shot."
              }
            }
          },
          "same_angle_from": {
            "type": ["object", "null"],
            "default": null,
            "description": "img2img from previous shot when camera angle is unchanged (same-side dialogue continuation, tracking shot segments). Rare.",
            "properties": {
              "shot_id": {
                "type": "integer",
                "description": "ID of the previous shot to inherit from."
              },
              "frame": {
                "type": "string",
                "enum": ["hero", "first", "last"],
                "description": "Which frame from the previous shot to use as img2img source."
              },
              "strength": {
                "type": "number",
                "minimum": 0.0,
                "maximum": 1.0,
                "default": 0.35,
                "description": "img2img strength (0.0=exact copy, 1.0=fully new)."
              }
            }
          },
          "continuity_from": {
            "type": ["object", "null"],
            "default": null,
            "description": "img2img crop for punch-in detail shots. Crops a region from previous shot's frame as source.",
            "properties": {
              "shot_id": {
                "type": "integer",
                "description": "ID of the previous shot to crop from."
              },
              "frame": {
                "type": "string",
                "enum": ["hero", "first", "last"],
                "description": "Which frame from the previous shot to crop."
              },
              "region": {
                "type": "string",
                "enum": ["full", "upper_third", "lower_third", "center", "face", "wrist_left", "hands", "custom"],
                "description": "Preset crop region from the source frame."
              },
              "method": {
                "type": "string",
                "enum": ["img2img_crop"],
                "default": "img2img_crop"
              },
              "strength": {
                "type": "number",
                "minimum": 0.0,
                "maximum": 1.0,
                "default": 0.30,
                "description": "img2img strength for the cropped source."
              }
            }
          },
          "spatial": {
            "type": ["object", "null"],
            "default": null,
            "description": "Machine-parseable spatial continuity data for 180° rule enforcement, screen direction consistency, and blocking tracking.",
            "properties": {
              "camera_side": {
                "type": "string",
                "enum": ["A", "B"],
                "default": "A",
                "description": "Which side of the 180° line the camera is on. Same-side shots in a scene should share this value. Crossing = deliberate creative choice."
              },
              "screen_direction": {
                "type": "string",
                "enum": ["left-to-right", "right-to-left", "toward-camera", "away-from-camera"],
                "description": "Dominant action/motion direction in the frame."
              },
              "blocking": {
                "type": "object",
                "description": "Per-character screen position and facing direction. Keyed by lowercase character name.",
                "additionalProperties": {
                  "type": "object",
                  "properties": {
                    "position": {
                      "type": "string",
                      "enum": ["screen-left", "center", "screen-right", "foreground", "background"],
                      "description": "Character's position in the frame."
                    },
                    "facing": {
                      "type": "string",
                      "enum": ["left", "right", "toward-camera", "away-from-camera"],
                      "description": "Direction the character is facing."
                    }
                  }
                }
              }
            }
          },
          "scene_break_before": {
            "type": "boolean",
            "default": false,
            "description": "If true, all continuity resets (new location, time skip). No prompt inheritance, no img2img from previous shot."
          },
          "reference_image_url": {
            "type": ["string", "null"],
            "default": null,
            "description": "Optional CDN URL for location reference img2img conditioning on first frame."
          }
        }
      }
    }
  }
}
