{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://video.livepartner.fans/docs/mv-shots.schema.json",
  "title": "MV 镜头脚本 mv-shots.json",
  "description": "MV 物料包内 input/mv-shots.json 的规范。本 schema 覆盖**结构**校验（字段类型、必填、枚举、取值范围）；跨字段的四项渲染前自检请用配套脚本 docs/validate-mv-shots.mjs（或按 §15.4 自查伪代码）。字段说明与完整对接契约见 https://video.livepartner.fans/api-docs.html#mv-provider",
  "type": "object",
  "required": ["timeline", "shots"],
  "properties": {
    "schema_version": {
      "type": "string",
      "description": "脚本格式版本（可选），例：cd-mv-shots/1.0"
    },
    "timeline": {
      "type": "object",
      "description": "全片时间轴声明。我方以 timeline 的声明值 + 音频实测时长共同决定成片长度",
      "required": ["durationMs", "shotCount"],
      "properties": {
        "durationMs": {
          "type": "integer",
          "minimum": 1,
          "description": "甲方声明的全片时长（毫秒）。必须等于 Σ shots[].durationMs（容差 ±200ms）；缺失或为 0 会被自检闸门拦截（error_code=MV_SELFCHECK_FAILED）"
        },
        "audioDurationMs": {
          "type": "integer",
          "minimum": 1,
          "description": "音频实测时长（毫秒）。强烈建议提供 —— 我方对轴的首选基准。常见的 durationMs 整秒取整差异（如 122000 vs 实测 122376）由此消解"
        },
        "shotCount": {
          "type": "integer",
          "minimum": 1,
          "description": "镜头数。必须 = shots.length = 包内分镜图张数，三者任一不符即被闸门拦截"
        },
        "frameRate": {
          "type": "integer",
          "enum": [24, 25, 30, 50, 60],
          "default": 24,
          "description": "成片帧率（★ 2026-10-06 起默认 24 —— 交付规格 1920×1080@24（镜头 960×544@24 生成 + AI 超分回 1080P）；甲方包内若显式声明 30 将按 24 统一交付）"
        },
        "shotMaxSeconds": {
          "type": "number",
          "exclusiveMinimum": 0,
          "maximum": 60,
          "default": 15,
          "description": "单镜时长上限（默认 15 秒）。超过请拆成多个镜头，否则超出部分会被截断"
        }
      },
      "additionalProperties": true
    },
    "script": {
      "type": "object",
      "description": "全片脚本元信息（可选）",
      "properties": {
        "type": {
          "type": "string",
          "description": "叙事类型，例：story / lyric"
        },
        "tone": {
          "type": "string",
          "description": "全片风格锚（如「暖金与深红为主色，高对比胶片颗粒」）。会作为风格前缀注入每一镜提示词，用于统一全片观感"
        },
        "style": {
          "type": "string",
          "description": "【可选·2026-10-01 新增】画面制作风格约束（比 tone 更硬，如「赛博朋克霓虹夜景，高对比低饱和暗部，胶片颗粒，无柔光」）。注入每一镜提示词最前（优先级最高）；平台还会按它做抽帧风格质检：镜头内风格跳动、偏离首帧基准、黑边/过曝/欠曝会自动换 seed 重生成。不提供则一切照旧"
        },
        "story": { "type": "string" },
        "stages": { "type": "array" }
      },
      "additionalProperties": true
    },
    "shots": {
      "type": "array",
      "minItems": 1,
      "description": "镜头表。数组顺序即成片顺序（按 segmentIndex → shotIndex 排序）；我方不增、不减、不重排，每镜在成片里只出现一次",
      "items": {
        "type": "object",
        "required": ["durationMs", "videoPrompt"],
        "properties": {
          "segmentIndex": {
            "type": "integer",
            "minimum": 0,
            "description": "段号（缺省 0）。与 shotIndex 一起决定成片顺序"
          },
          "shotIndex": {
            "type": "integer",
            "minimum": 0,
            "description": "镜号（缺省为数组下标）"
          },
          "segmentType": {
            "type": "string",
            "enum": ["poster", "intro", "verse", "chorus", "bridge", "interlude", "outro", "lyric", "main"],
            "description": "镜头类型。intro / interlude / outro 视为空镜，不得引入人物主体。poster（2026-10-14 新增；★ 2026-10-19 尾帧口径修正）= 甲方自带海报分镜，必须位于 shots 首位（镜号 0/0）：渲染为独立静音前置段（海报段无歌声无字幕，歌曲音频/歌词由平台自动整体后移）；此时 timeline.durationMs 与 shotCount 必须包含海报镜（成片总时长 = 海报段 + 音频实测）；平台检测到后不再自动插入海报分镜。★ 海报镜与普通分镜同权：imageUrl（首帧）+ endImageUrl（尾帧）+ videoPrompt 三者必填，缺尾帧即打回（error_code=MATERIAL_MISSING_END_FRAME，平台不做任何自动补齐）；原「海报不参与首尾帧衔接 / 尾帧不下发」口径已作废"
          },
          "lyricLine": {
            "type": "string",
            "description": "该镜对应歌词（可空）"
          },
          "imageUrl": {
            "type": "string",
            "description": "图生视频的输入底图。与包内 shot/NNN.jpg 同源同内容（包内图优先，按镜号取值）；底图是该镜的**起始帧**"
          },
          "endImageUrl": {
            "type": "string",
            "description": "【2026-10-01 新增 · v3.4 起硬要求 · 2026-10-19 覆盖海报镜】图生视频的**尾帧**底图，与 imageUrl 配对构成首尾帧生成，用于锚定本镜的结束画面（相邻镜头因此能接得上）。★ 硬要求：每一镜都必须提供尾帧；包内缺失、上传失败或仅给包内相对路径 → 任务直接打回（awaiting_materials），不再静默退化为单首帧（原「可选·不提供则照旧」口径已作废）。★ 海报镜（segmentType=poster）同样必填：缺尾帧按 MATERIAL_MISSING_END_FRAME 打回，**平台不自动补齐**（不用包内海报图、也不用首帧代替）。包内若单独提供尾帧，请放在 end/ · shot-end/ · lastframe/ 目录（按镜号取值，与 shot/ 严格分开，避免首帧被尾帧覆盖）。画面上要出现的文字请写进 videoPrompt 由模型生成，尾帧图作为构图与收束参照"
          },
          "videoPrompt": {
            "type": "string",
            "minLength": 1,
            "description": "该镜画面怎么动（人物动作 + 环境变化）。必填：提示词决定画面内容，缺失会导致生成异常"
          },
          "camera": {
            "type": "string",
            "description": "运镜。推荐值：push（推近）/ pull（拉远）/ orbit（环绕）/ pan / tilt / static / front / side / closeup；也接受中文，平台会自动归一化为规范运镜语言"
          },
          "stage": {
            "type": "string",
            "description": "归属段落（可选），例：intro / main / outro"
          },
          "startMs": {
            "type": "integer",
            "minimum": 0,
            "description": "该镜在音频中的起点（毫秒，可选）。建议提供以便双方逐镜对账"
          },
          "endMs": {
            "type": "integer",
            "minimum": 1,
            "description": "该镜在音频中的终点（毫秒）。必须 ≤ 音频实测时长（max(endMs) 越界会被闸门拦截）"
          },
          "durationMs": {
            "type": "integer",
            "minimum": 1,
            "maximum": 60000,
            "description": "该镜时长（毫秒）。这是「每镜在成片里占多久」的唯一依据；≤ timeline.shotMaxSeconds（默认 15 秒）"
          }
        },
        "additionalProperties": true
      }
    }
  },
  "$comment": "以下为平台渲染前自检闸门（不通过则任务打回 awaiting_materials + error_code=MV_SELFCHECK_FAILED，不出片、不耗算力）。本 schema 无法表达跨字段求和/比较，请用配套脚本 docs/validate-mv-shots.mjs 本地自查：① len(shots) == timeline.shotCount == 包内分镜图张数；② |Σ shots[].durationMs − timeline.durationMs| ≤ 200ms；③ max(shots[].endMs) ≤ 音频实测时长；④ timeline.durationMs 与音频实测的偏差仅告警（整秒取整常见 300–500ms，时间轴一律以音频实测为准）；⑤ 每一镜都必须有 endImageUrl 尾帧（v3.4 起硬要求，缺帧打回）；⑥ 主角定妆照；⑦ 包内尾帧图数覆盖每一镜；⑧【2026-10-19】segmentType=poster 的海报镜必须自带 endImageUrl + videoPrompt（与普通分镜同权），缺尾帧按 MATERIAL_MISSING_END_FRAME 打回，平台不自动补齐。",
  "examples": [
    {
      "schema_version": "cd-mv-shots/1.0",
      "timeline": { "frameRate": 30, "durationMs": 134000, "audioDurationMs": 133944, "shotCount": 52, "shotMaxSeconds": 15 },
      "script": { "type": "story", "tone": "暖橙与霓虹青蓝对撞，黄昏逆光加海雾柔化" },
      "shots": [
        {
          "segmentIndex": 1,
          "shotIndex": 1,
          "segmentType": "verse",
          "lyricLine": "",
          "imageUrl": "input/objects/shot/001.jpg",
          "videoPrompt": "女孩站在雨夜街头，霓虹灯牌倒映在积水里，她缓缓抬头看向镜头，发丝随风微动",
          "camera": "push",
          "stage": "main",
          "startMs": 0,
          "endMs": 2576,
          "durationMs": 2576
        }
      ]
    },
    {
      "schema_version": "cd-mv-shots/1.0",
      "$comment": "甲方自带海报镜范例（2026-10-14）：poster 必须是 shots[0]，timeline.durationMs/shotCount 含海报镜；海报段静音无字幕，歌曲音频/歌词由平台自动整体后移海报时长；成片总时长 = 海报段 + 音频实测",
      "timeline": { "frameRate": 24, "durationMs": 36900, "audioDurationMs": 31900, "shotCount": 4, "shotMaxSeconds": 15 },
      "shots": [
        {
          "segmentIndex": 0,
          "shotIndex": 0,
          "segmentType": "poster",
          "imageUrl": "input/objects/shot/001.jpg",
          "endImageUrl": "input/objects/shot-end/001.jpg",
          "videoPrompt": "海报动态化：缓慢推近，光效流动，氛围粒子；保持画面主体与构图不变；画面标题文字随推近保持清晰可读",
          "hasPerson": true,
          "startMs": 0,
          "endMs": 5000,
          "durationMs": 5000
        },
        {
          "segmentIndex": 1,
          "shotIndex": 1,
          "segmentType": "intro",
          "imageUrl": "input/objects/shot/002.jpg",
          "endImageUrl": "input/objects/shot-end/002.jpg",
          "videoPrompt": "（本镜画面描述，含人物约束原文）",
          "hasPerson": false,
          "transition": { "type": "fadeblack", "duration": 0.6 },
          "startMs": 5000,
          "endMs": 11200,
          "durationMs": 6200
        }
      ]
    }
  ]
}
