{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "urn:akari-video:schema:captions:v0",
  "title": "AKARI Video captions.json v0",
  "description": "従来の配列ルートと、字幕の既定テキストスタイル・語レベル演出を持てる object ルートの公開契約。",
  "oneOf": [
    { "$ref": "#/$defs/captionsArray" },
    { "$ref": "#/$defs/captionsRootObject" }
  ],
  "$defs": {
    "hexColor": {
      "type": "string",
      "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$"
    },
    "captionZone": {
      "type": "string",
      "enum": [
        "top-left",
        "top",
        "top-right",
        "left",
        "center",
        "right",
        "bottom-left",
        "bottom",
        "bottom-right"
      ],
      "default": "bottom",
      "description": "字幕の 3x3 配置ゾーン。省略時の実効値は bottom。"
    },
    "textStrokeStyle": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "method": { "enum": ["webkit-outline"] },
        "color": { "$ref": "#/$defs/hexColor" },
        "width_px": { "type": "number", "minimum": 0 }
      }
    },
    "richTextStrokeStyle": {
      "type": "object",
      "additionalProperties": false,
      "required": ["color", "width_px"],
      "properties": {
        "color": { "$ref": "#/$defs/hexColor" },
        "width_px": { "type": "number", "minimum": 0 },
        "offset_x": { "type": "number" },
        "offset_y": { "type": "number" }
      }
    },
    "textFillStop": {
      "type": "object",
      "additionalProperties": false,
      "required": ["at", "color"],
      "properties": {
        "at": { "type": "number", "minimum": 0, "maximum": 100 },
        "color": { "$ref": "#/$defs/hexColor" }
      }
    },
    "textFillStyle": {
      "oneOf": [
        {
          "type": "object",
          "additionalProperties": false,
          "required": ["type", "color"],
          "properties": {
            "type": { "const": "solid" },
            "color": { "$ref": "#/$defs/hexColor" }
          }
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": ["type", "stops", "angle_deg"],
          "properties": {
            "type": { "const": "gradient" },
            "stops": { "type": "array", "minItems": 2, "items": { "$ref": "#/$defs/textFillStop" } },
            "angle_deg": { "type": "number" }
          }
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": ["type", "pattern"],
          "properties": {
            "type": { "const": "pattern" },
            "pattern": {
              "type": "object",
              "additionalProperties": false,
              "required": ["id", "scale", "fg", "bg"],
              "properties": {
                "id": { "enum": ["diamond", "dot", "stripe", "gingham", "skull", "hazard", "night", "heart", "thunder"] },
                "scale": { "type": "number", "exclusiveMinimum": 0 },
                "fg": { "$ref": "#/$defs/hexColor" },
                "bg": {
                  "oneOf": [
                    { "$ref": "#/$defs/hexColor" },
                    {
                      "type": "object",
                      "additionalProperties": false,
                      "required": ["stops", "angle_deg"],
                      "properties": {
                        "stops": { "type": "array", "minItems": 2, "items": { "$ref": "#/$defs/textFillStop" } },
                        "angle_deg": { "type": "number" }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      ]
    },
    "textInnerStrokeStyle": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "color": { "$ref": "#/$defs/hexColor" },
        "width_px": { "type": "number", "minimum": 0 }
      }
    },
    "textFillGradient": {
      "type": "object",
      "additionalProperties": false,
      "required": ["colors", "angle_deg"],
      "properties": {
        "colors": { "type": "array", "minItems": 2, "maxItems": 3, "items": { "$ref": "#/$defs/hexColor" } },
        "angle_deg": { "type": "number" }
      }
    },
    "textExtrude": {
      "type": "object",
      "additionalProperties": false,
      "required": ["depth_px", "color", "angle_deg"],
      "properties": {
        "depth_px": { "type": "integer", "minimum": 1, "maximum": 32 },
        "color": { "$ref": "#/$defs/hexColor" },
        "color_end": { "$ref": "#/$defs/hexColor" },
        "angle_deg": { "type": "number" }
      }
    },
    "referencePixelLayout": {
      "type": "object",
      "additionalProperties": false,
      "required": ["mode", "reference_width_px", "reference_height_px", "left_px", "width_px", "bottom_px", "text_align", "max_lines"],
      "properties": {
        "mode": { "const": "reference-pixel" },
        "reference_width_px": { "type": "integer", "minimum": 1 },
        "reference_height_px": { "type": "integer", "minimum": 1 },
        "left_px": { "type": "number", "minimum": 0 },
        "width_px": { "type": "number", "exclusiveMinimum": 0 },
        "bottom_px": { "type": "number", "minimum": 0 },
        "text_align": { "const": "center" },
        "max_lines": { "const": 1 }
      }
    },
    "textBackgroundStyle": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "color": { "$ref": "#/$defs/hexColor" },
        "opacity": { "type": "number", "minimum": 0, "maximum": 1 },
        "radius_px": { "type": "number", "minimum": 0 },
        "padding_px": { "type": "number", "minimum": 0 },
        "width_pct": { "type": "number", "minimum": 0 },
        "fit": { "type": "string", "enum": ["text", "frame"], "default": "text" },
        "height_pct": { "type": "number", "minimum": 0 },
        "offset_x": { "type": "number" },
        "offset_y": { "type": "number" },
        "mode": {
          "type": "string",
          "enum": ["per-line", "block"],
          "default": "per-line",
          "description": "座布団の形。省略時は行ごとに分かれる per-line。"
        }
      },
      "description": "字幕の座布団スタイル。mode 省略時の実効値は per-line。width_pct / height_pct のいずれかを指定すると座布団は文字box比の拡張として解釈され、padding_px より優先する。"
    },
    "textAnchor": {
      "type": "string",
      "enum": ["tl", "tc", "tr", "ml", "mc", "mr", "bl", "bc", "br"],
      "description": "9 点アンカー。1 文字目が縦（t/m/b）、2 文字目が横（l/c/r）。position.y 指定時の縦成分は b = 下端基準、t = 上端基準、m = 中心基準。"
    },
    "textPosition": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "x": { "type": "number" },
        "y": { "type": "number" }
      },
      "description": "フレーム比 0..1 の配置座標。指定した軸だけが効き、未指定の軸は zone / text_anchor に従う。position.y 指定時の text_anchor 縦成分は b = 下端基準、t = 上端基準、m = 中心基準。"
    },
    "textShadowStyle": {
      "type": "object",
      "additionalProperties": false,
      "required": ["color"],
      "properties": {
        "color": { "$ref": "#/$defs/hexColor" },
        "opacity": { "type": "number", "minimum": 0, "maximum": 1 },
        "blur_px": { "type": "number", "minimum": 0 },
        "distance_px": { "type": "number", "minimum": 0 },
        "angle_deg": { "type": "number" }
      },
      "description": "角度 + 距離をオフセットへ変換して組む落ち影。color が無いと影を組めないため必須。"
    },
    "textGlowStyle": {
      "type": "object",
      "additionalProperties": false,
      "required": ["color"],
      "properties": {
        "color": { "$ref": "#/$defs/hexColor" },
        "density": { "type": "number", "minimum": 0 },
        "spread": { "type": "number", "minimum": 0 },
        "offset_x": { "type": "number" },
        "offset_y": { "type": "number" }
      },
      "description": "ぼかしのみの多重影。shadow と併用でき、1 本の text-shadow へ合成される。"
    },
    "textAnimationId": {
      "type": "string",
      "enum": [
        "fade-in-out",
        "soft-fade",
        "fade-up",
        "fade-down",
        "cinematic-fade",
        "slide-left",
        "slide-right",
        "slide-up",
        "slide-down",
        "push-left",
        "push-right",
        "push-up",
        "push-down",
        "rise-soft",
        "drop-in",
        "zoom-in-out",
        "zoom-pop",
        "zoom-pulse",
        "pop",
        "bounce",
        "squash-pop",
        "stretch-in",
        "stomp",
        "snap",
        "rotate-in",
        "spin-in",
        "roll-in",
        "spiral-in",
        "swing",
        "shake",
        "jitter",
        "glitch",
        "flash",
        "heartbeat",
        "typewriter",
        "wipe-left",
        "wipe-right",
        "wobble",
        "float",
        "breath",
        "neon-flicker",
        "hologram",
        "retro-flicker",
        "caption-rise",
        "news-ticker",
        "marquee-left",
        "crawl-up"
      ],
      "$comment": "presets/textanim/index.jsonl の id と同一集合。schemas test が正本索引とのドリフトを検査する。"
    },
    "textAnimationSlot": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id"],
      "properties": {
        "id": { "$ref": "#/$defs/textAnimationId" },
        "duration_sec": { "type": "number", "exclusiveMinimum": 0 },
        "ease": {
          "oneOf": [
            { "type": "string", "minLength": 1, "pattern": "\\S" },
            { "type": "null" }
          ]
        },
        "amp": {
          "oneOf": [
            { "type": "number", "exclusiveMinimum": 0 },
            { "type": "null" }
          ]
        }
      }
    },
    "textAnimation": {
      "type": "object",
      "additionalProperties": false,
      "minProperties": 1,
      "properties": {
        "in": { "$ref": "#/$defs/textAnimationSlot" },
        "out": { "$ref": "#/$defs/textAnimationSlot" },
        "loop": { "$ref": "#/$defs/textAnimationSlot" }
      },
      "description": "字幕の登場・退場・ループを独立指定する textanim v0 契約。"
    },
    "textStyle": {
      "type": "object",
      "additionalProperties": false,
      "allOf": [
        { "not": { "required": ["zone", "layout"] } },
        { "not": { "required": ["layout", "reference_height_px"] } }
      ],
      "properties": {
        "karaoke": {
          "type": "object", "additionalProperties": false,
          "properties": {
            "done_color": { "$ref": "#/$defs/hexColor" },
            "fill": { "enum": ["char", "word", "smooth"] },
            "start_index": { "type": "integer", "minimum": 0 }
          }
        },
        "color": { "$ref": "#/$defs/hexColor" },
        "size_px": { "type": "number", "exclusiveMinimum": 0 },
        "reference_height_px": {
          "type": "integer",
          "minimum": 1,
          "$comment": "zone 方式の基準出力高さ（issue #40 §2）。この text_style の px 系フィールド（size_px、stroke.width_px、stroke_inner.width_px、extrude.depth_px、shadow.blur_px / distance_px、glow.spread / offset_x / offset_y、background.radius_px / padding_px / offset_x / offset_y）は高さ reference_height_px の出力向けに書かれた値として扱われ、実効 px = 宣言値 × output.height / reference_height_px。省略時は scale = 1（従来どおり）。layout（reference-pixel）とは併用不可（allOf/not）。layout は従来どおり output.width / reference_width_px で拡縮する。"
        },
        "font_family": { "type": "string", "minLength": 1 },
        "font_weight": { "type": "integer", "minimum": 1, "maximum": 1000 },
        "weight": { "type": "integer", "minimum": 100, "maximum": 900 },
        "italic": { "type": "boolean" },
        "underline": { "type": "boolean" },
        "strikethrough": { "type": "boolean" },
        "list": { "enum": ["bullet", null] },
        "opacity": { "type": "number", "minimum": 0, "maximum": 1 },
        "letter_spacing_em": { "type": "number" },
        "line_height": { "type": "number", "exclusiveMinimum": 0 },
        "align": { "type": "string", "enum": ["left", "center", "right"] },
        "vertical_align": { "type": "string", "enum": ["top", "middle", "bottom"] },
        "vertical": { "type": "boolean" },
        "text_transform": { "type": "string", "enum": ["upper", "uppercase", "lower", "lowercase", "title", "capitalize", "none"] },
        "max_width_pct": { "type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 100 },
        "wrap_width_pct": { "type": "number", "exclusiveMinimum": 0, "maximum": 100 },
        "max_characters": { "type": "integer", "minimum": 1 },
        "text_anchor": { "$ref": "#/$defs/textAnchor" },
        "position": { "$ref": "#/$defs/textPosition" },
        "scale": { "type": "number", "minimum": 0.4, "maximum": 3, "default": 1 },
        "rotate": { "type": "number", "minimum": -180, "maximum": 180, "default": 0 },
        "shadow": { "$ref": "#/$defs/textShadowStyle" },
        "glow": { "$ref": "#/$defs/textGlowStyle" },
        "animation": { "$ref": "#/$defs/textAnimation" },
        "stroke": { "$ref": "#/$defs/textStrokeStyle" },
        "strokes": { "type": "array", "items": { "$ref": "#/$defs/richTextStrokeStyle" } },
        "fill": { "$ref": "#/$defs/textFillStyle" },
        "stroke_inner": { "$ref": "#/$defs/textInnerStrokeStyle" },
        "fill_gradient": { "$ref": "#/$defs/textFillGradient" },
        "extrude": { "$ref": "#/$defs/textExtrude" },
        "background": { "$ref": "#/$defs/textBackgroundStyle" },
        "zone": { "$ref": "#/$defs/captionZone" },
        "layout": { "$ref": "#/$defs/referencePixelLayout" }
      },
      "$comment": "全フィールド任意。captionRecord.text_style は default_text_style をフィールド単位（animation はスロット単位）で上書きする。合成処理は消費側の責務。zone 省略時の実効値は bottom。font_weight と weight は同じ CSS font-weight を指す別名で、weight（100..900）が textstyle v0 の正式名、font_weight（1..1000）は display_policy 経路からの既存名。両方あるときは weight を優先する。max_characters は 1 行の折り返し文字数を指定する。max_width_pct はプレート（座布団）の幅だけを広げ、折り返し文字数には影響しない。scale は字幕プレートを等比拡大する。rotate は字幕プレートを度単位で回転する。reference_height_px は zone 方式の px 系フィールドの基準出力高さで、default_text_style と captions[].text_style の両方で宣言でき cue 側がフィールド単位で上書きする。layout との併用は不可。"
    },
    "captionWord": {
      "type": "object",
      "additionalProperties": false,
      "required": ["start", "end", "text"],
      "properties": {
        "start": { "type": "number", "minimum": 0 },
        "end": { "type": "number", "minimum": 0 },
        "raw_start": { "type": "number", "minimum": 0 },
        "raw_end": { "type": "number", "minimum": 0 },
        "text": { "type": "string", "minLength": 1, "pattern": "\\S" }
      },
      "$comment": "raw_start/raw_end は発話吸着前の監査用時刻。end >= start、raw_end >= raw_start と親 caption の範囲外警告は JSON Schema 標準だけでは兄弟値を比較できないため検証器が扱う。"
    },
    "captionRun": {
      "type": "object",
      "additionalProperties": false,
      "required": ["from", "to"],
      "allOf": [{
        "if": { "anyOf": [
          { "required": ["look"] }, { "required": ["motion"] },
          { "required": ["anchor"] }, { "required": ["fire"] },
          { "required": ["style_sources"] }, { "required": ["migrated_from"] }
        ] },
        "then": { "required": ["id"] }
      }],
      "properties": {
        "id": { "type": "string", "pattern": "^r-\\d{4,}$" },
        "from": { "type": "integer" },
        "to": { "type": "integer" },
        "role": { "type": "string", "minLength": 1 },
        "look": { "$ref": "#/$defs/captionRunLook" },
        "motion": { "$ref": "#/$defs/textAnimation" },
        "emotion": { "type": "string", "minLength": 1 },
        "anchor": { "$ref": "#/$defs/captionRunAnchor" },
        "fire": { "enum": ["shown", "spoken", "whole"] },
        "fire_was": { "enum": ["shown", "spoken", "whole"] },
        "style_sources": { "$ref": "#/$defs/styleSources" },
        "needs_review": { "type": "boolean" },
        "split_from": { "type": "string", "pattern": "^r-\\d{4,}$" },
        "migrated_from": { "type": "string", "pattern": "^e-\\d{4,}$" },
        "migrated_emphasis": {
          "type": "object",
          "additionalProperties": false,
          "required": ["index", "item"],
          "properties": {
            "index": { "type": "integer", "minimum": 0 },
            "item": { "$ref": "#/$defs/emphasisWordItem" }
          }
        },
        "dormant_animation": { "$ref": "#/$defs/textAnimation" },
        "style": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "color": { "$ref": "#/$defs/hexColor" },
            "font_weight": { "type": "integer", "minimum": 1, "maximum": 1000 },
            "scale": { "type": "number", "exclusiveMinimum": 0 },
            "baseline_shift_em": { "type": "number" },
            "rotate_deg": { "type": "number", "minimum": -180, "maximum": 180 },
            "letter_spacing_em": { "type": "number" },
            "stroke": { "$ref": "#/$defs/textStrokeStyle" },
            "italic": { "type": "boolean" },
            "underline": { "type": "boolean" }
          }
        },
        "animation": { "$ref": "#/$defs/textAnimation" }
      },
      "$comment": "from/to は display_text または text の書記素オフセット。to は排他。旧 runs v0 の空・範囲外は edit-lint warning、runs v1 は文書意味の error。"
    },
    "captionRunLook": {
      "type": "object", "additionalProperties": false,
      "properties": {
        "color": { "$ref": "#/$defs/hexColor" },
        "font_weight": { "type": "integer", "minimum": 1, "maximum": 1000 },
        "italic": { "type": "boolean" }, "underline": { "type": "boolean" },
        "letter_spacing_em": { "type": "number" },
        "scale": { "type": "number", "exclusiveMinimum": 0 },
        "baseline_shift_em": { "type": "number" },
        "rotate_deg": { "type": "number", "minimum": -180, "maximum": 180 },
        "stroke": { "$ref": "#/$defs/textStrokeStyle" },
        "shadow": { "$ref": "#/$defs/textShadowStyle" },
        "glow": { "$ref": "#/$defs/textGlowStyle" },
        "background": { "$ref": "#/$defs/textBackgroundStyle" }
      }
    },
    "captionRunAnchor": {
      "type": "object", "additionalProperties": false,
      "required": ["start", "end", "domain"],
      "properties": {
        "src": { "type": "string", "minLength": 1 },
        "start": { "type": "number", "minimum": 0 },
        "end": { "type": "number", "minimum": 0 },
        "domain": { "enum": ["source", "output"] }
      }
    },
    "styleSource": {
      "type": "object", "additionalProperties": false,
      "required": ["kind", "id", "original_hash", "applied_hash"],
      "properties": {
        "kind": { "enum": ["builtin", "library", "my"] },
        "id": { "type": "string", "minLength": 1 },
        "uid": { "type": "string", "minLength": 1 },
        "revision": { "type": "integer", "minimum": 0 },
        "original_hash": { "type": "string", "minLength": 1 },
        "applied_hash": { "type": "string", "minLength": 1 },
        "applied_at": { "type": "string", "minLength": 1 },
        "requires": { "type": "array", "items": { "type": "object", "additionalProperties": false,
          "required": ["id", "version", "hash"], "properties": {
            "id": { "type": "string", "minLength": 1 },
            "version": { "type": "string" }, "hash": { "type": "string", "minLength": 1 }
          } } }
      }
    },
    "styleSources": {
      "type": "object", "additionalProperties": false,
      "properties": { "look": { "$ref": "#/$defs/styleSource" }, "motion": { "$ref": "#/$defs/styleSource" } }
    },
    "unresolvedRun": {
      "type": "object", "additionalProperties": false,
      "required": ["run", "old_caption_id", "old_text", "old_src", "old_start", "old_end", "reason"],
      "properties": {
        "run": { "$ref": "#/$defs/captionRun" },
        "old_caption_id": { "type": "string" }, "old_text": { "type": "string" },
        "old_src": { "type": ["string", "null"] },
        "old_start": { "type": "number" }, "old_end": { "type": "number" },
        "reason": { "type": "string", "minLength": 1 }
      }
    },
    "sourceRef": {
      "oneOf": [
        { "type": "null" },
        {
          "type": "object",
          "additionalProperties": false,
          "required": ["segment"],
          "properties": {
            "segment": { "type": "integer", "minimum": 0 }
          }
        }
      ]
    },
    "captionId": {
      "oneOf": [
        { "type": "string", "pattern": "^c-\\d{4}$" },
        { "type": "string", "pattern": "^caption-[A-Za-z0-9][A-Za-z0-9_-]*$" }
      ],
      "$comment": "正準 ID は c- に続く 4 桁の数字。caption-* は既存 captions.json 資産を無変更で受理するためのレガシー互換形。"
    },
    "captionRecord": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "start", "end", "text", "speaker", "sourceRef", "edited"],
      "properties": {
        "id": { "$ref": "#/$defs/captionId" },
        "start": { "type": "number", "minimum": 0 },
        "end": { "type": "number", "exclusiveMinimum": 0 },
        "text": { "type": "string", "minLength": 1, "pattern": "\\S" },
        "speaker": {
          "oneOf": [
            { "type": "string" },
            { "type": "null" }
          ]
        },
        "sourceRef": { "$ref": "#/$defs/sourceRef" },
        "edited": { "type": "boolean" },
        "src": { "type": "string", "minLength": 1, "pattern": "\\S" },
        "time_domain": {
          "enum": ["source", "output"],
          "default": "source",
          "$comment": "start/end と words[].start/end の時刻ドメイン。省略時は後方互換の source。output は edit.json の出力時間軸を直接参照し、src は任意の出所情報として保持できる。"
        },
        "words": {
          "type": "array",
          "items": { "$ref": "#/$defs/captionWord" }
        },
        "unrecognized": {
          "type": "array",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["start", "end"],
            "properties": {
              "start": { "type": "number", "minimum": 0 },
              "end": { "type": "number", "minimum": 0 }
            }
          },
          "$comment": "音はあるが文字にできなかった区間。既定は source 秒で、caption の time_domain 宣言がある場合は同じ時刻ドメインに従う。"
        },
        "style": { "enum": ["plain", "karaoke", "pop", "reveal", "reveal-word"] },
        "display_text": { "type": "string" },
        "display_fragments": {
          "type": "array",
          "minItems": 1,
          "maxItems": 6,
          "items": { "type": "string", "minLength": 1, "pattern": "\\S" }
        },
        "style_preset": {
          "type": "string",
          "pattern": "^[a-z0-9][a-z0-9-]*$",
          "$comment": "解決順は default_text_style < style_preset の style < 同じレコードの text_style。未知 id は値を適用せず、edit-lint が warning を出す。"
        },
        "text_style": { "$ref": "#/$defs/textStyle" },
        "style_sources": { "$ref": "#/$defs/styleSources" },
        "display_timing": {
          "type": "string",
          "enum": ["full", "speech-tight"],
          "description": "表示ウィンドウの決め方。full（既定・省略時）= start〜end をそのまま使う。speech-tight = words[] の先頭 start 〜 末尾 end だけ表示する（start/end 自体は書き換えない）。"
        },
        "runs": { "type": "array", "items": { "$ref": "#/$defs/captionRun" } }
      },
      "$comment": "end > start、ID 一意性、words[].end >= start は JSON Schema 標準だけでは兄弟値を比較できないため validate-captions.mjs でも検証する。time_domain 省略時は source。style は既存の単語リビール演出、text_style は見た目のスタイルであり別契約。"
    },
    "emphasisWordItem": {
      "type": "object",
      "additionalProperties": true,
      "required": ["id", "t_start", "t_end", "word", "emotion"],
      "properties": {
        "id": { "type": "string", "pattern": "^e-\\d{4}$" },
        "src": { "type": "string", "minLength": 1, "pattern": "\\S" },
        "t_start": { "type": "number", "minimum": 0 },
        "t_end": { "type": "number", "minimum": 0 },
        "word": { "type": "string", "minLength": 1, "pattern": "\\S" },
        "emotion": { "type": "string", "minLength": 1, "pattern": "\\S" },
        "style_preset": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
        "style_hint": { "type": "string" }
      },
      "$comment": "docs/contract-2026-08-23-captions-emphasis-words-v0.md。レコード形・語彙・source 秒アンカーは edit.json v1 の emphasis_words と同形。style_preset は textstyle-catalog の id を使う任意の描画提案。emotion は joy/pain/surprise/anger/sadness/emphasis の例示に留め enum 強制しない。t_end > t_start と id のファイル内一意性は validate-captions.mjs が検証する。"
    },
    "captionBreakHints": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "preferred_second_starts": { "type": "array", "items": { "type": "string", "minLength": 1, "pattern": "\\S" } },
        "preferred_first_ends": { "type": "array", "items": { "type": "string", "minLength": 1, "pattern": "\\S" } },
        "protected_terms": { "type": "array", "items": { "type": "string", "minLength": 1, "pattern": "\\S" } }
      }
    },
    "captionDisplayPolicy": {
      "type": "object",
      "additionalProperties": false,
      "required": ["mode", "algorithm", "unit_metric", "max_line_units", "minimum_fragment_duration_seconds", "locale"],
      "properties": {
        "mode": { "const": "single_line_sequential" },
        "algorithm": { "const": "a4-ja-two-fragment-v1" },
        "unit_metric": { "const": "ascii-half-other-one-v1" },
        "max_line_units": { "type": "number", "exclusiveMinimum": 0 },
        "minimum_fragment_duration_seconds": { "type": "number", "exclusiveMinimum": 0 },
        "locale": { "type": "string", "minLength": 1, "pattern": "\\S" },
        "lines": { "type": "integer", "minimum": 1, "maximum": 6, "default": 1 },
        "wrap": { "type": "string", "enum": ["multi", "fold"], "default": "multi" },
        "word_style": { "type": "string", "enum": ["none", "karaoke"], "default": "none" },
        "break_hints": { "$ref": "#/$defs/captionBreakHints" }
      },
      "$comment": "lines は同時に出す行数。wrap の multi は N 断片を同時に表示し、fold は 1 断片を N 行に折る。"
    },
    "captionsArray": {
      "type": "array",
      "items": { "$ref": "#/$defs/captionRecord" }
    },
    "captionsRootObject": {
      "type": "object",
      "additionalProperties": false,
      "required": ["captions"],
      "properties": {
        "default_text_style": { "$ref": "#/$defs/textStyle" },
        "display_policy": { "$ref": "#/$defs/captionDisplayPolicy" },
        "emphasis_words": {
          "type": "array",
          "items": { "$ref": "#/$defs/emphasisWordItem" }
        },
        "captions": { "$ref": "#/$defs/captionsArray" },
        "unresolved_runs": { "type": "array", "items": { "$ref": "#/$defs/unresolvedRun" } }
      },
      "$comment": "default_text_style と emphasis_words は任意。省略時は従来表示と同値。emphasis_words の席は object ルートだけにあり、配列ルートには存在しない。"
    }
  }
}
