{
  "name": "gen-script",
  "toolName": "gen_script",
  "tier": "orchestration",
  "category": "authoring",
  "title": "Video Script Generation",
  "description": "Video script generation: turn a topic into a structured Video DSL (JSON) that describes the full video — scene structure, asset requirements, and narrative flow.",
  "auth": "none",
  "envVars": [
    "DEFAULT_IMAGE_MODEL",
    "DEFAULT_VIDEO_MODEL",
    "STUB_IMAGE_URL",
    "STUB_VIDEO_URL"
  ],
  "scriptPath": "scripts/gen_script.py",
  "parameters": {
    "type": "object",
    "properties": {
      "topic": {
        "type": "string",
        "description": "Video topic (required)"
      },
      "duration": {
        "type": "number",
        "description": "Target duration in seconds"
      },
      "style": {
        "type": "string",
        "description": "Style tag"
      },
      "ratio": {
        "type": "string",
        "description": "Aspect ratio, e.g. 16:9 or 9:16"
      },
      "scenes": {
        "type": "number",
        "description": "Scene count"
      },
      "voice_id": {
        "type": "string",
        "description": "Narration voice id. Default depends on the bound template's outputLanguage; query gen_voice with list_voices=true to see available ids."
      },
      "speed": {
        "type": "number",
        "description": "Narration speech rate, 0.5-2.0 (default 1.0). Stored at global.narration.speed and applied when prepare_video_assets synthesizes the voice-over. **Must** pass when the user picked a speech rate — a template's 语速 / speed parameter arrives as a multiplier like 1.1, and without this flag it is silently dropped and the narration comes back at 1.0. Above ~1.3 subtitle alignment drifts and the delivery sounds mechanical; suggest shortening the script instead of raising it further."
      },
      "template_id": {
        "type": "string",
        "description": "Template id (e.g. html-slide). The template owns outputLanguage and may also declare a defaultVoiceId; both flow into the produced DSL."
      },
      "headline": {
        "type": "string",
        "description": "On-screen headline. Stored at meta.headline and pushed into every scene's textLayers[role=headline] so the template can render it as the top big-text. **Must** be set when the user explicitly provided a headline / main title; without it, headline falls back to the long-form topic and overflows the top text layer. This is on-screen copy, so write it for a viewer who has never heard of the subject — a bare identifier (repo name, file name, product code) is not a title. Length limits, line breaks (\\n) and emphasis syntax are per-template: follow the chosen template's llmHint, which gen_script prints in full on stderr."
      },
      "subheadline": {
        "type": "string",
        "description": "On-screen subheadline: the line under the headline, saying why the viewer should care. Multi-line via \\n. Stored at meta.subheadline and pushed into every scene's textLayers[role=subheadline] as the top small-text. **Must** be set when the user explicitly provided a subtitle / slogan. **Never a bare URL** — text layers are painted on screen, where a link is neither readable nor clickable; say what the thing is instead. How many lines the template keeps, and whether it parses **emphasis**, is declared in that template's llmHint (gen_script prints it in full on stderr). Note: this is the on-screen subheadline, not the CC subtitle (global.subtitle) — they are independent."
      },
      "carousel_items": {
        "type": "array",
        "items": {
          "type": "string"
        },
        "description": "Media URLs for the template's image/video carousel. When provided together with a template_id whose capabilities.payloadStyle=carousel-caption, these URLs are placed into customPayload.sequence.items when payloadDefaults.sequence is declared, otherwise customPayload.carousel.items, as existing assets — NO AI image generation is triggered. **Must** pass when the user explicitly provides image/video URLs for such a template. Each element is a full URL string."
      },
      "scene_images": {
        "type": "array",
        "items": {
          "type": "string"
        },
        "description": "Per-scene visuals for a visual-overlay template (image-slide etc.) — the counterpart of carousel_items for templates that have real scenes. Each element is EITHER a full URL (use that existing asset for the scene, no gen-image call) OR the literal string 'ai' / 'ai:<prompt>' (leave that scene to gen-image; the text after the colon, when present, is the user's own image prompt for it). Mapping is POSITIONAL: element i is scene i, so an AI scene can sit anywhere in the order — pass ['url1','ai','url2'] verbatim when the user arranged it that way. Scenes past the last element still get an AI-generated image, so 2 images still produce a full video. When `scenes` is not set the scene count is raised (never lowered) to fit the elements, so nothing the user arranged is dropped and handing over fewer images does not shrink the video. **Must** pass when the user supplies images for such a template — otherwise their images are ignored entirely and every scene is AI-generated. Do NOT pass for carousel-caption templates; those take carousel_items."
      },
      "caption_lines": {
        "type": "array",
        "items": {
          "type": "string"
        },
        "description": "On-screen caption lines for templates that declare a caption area (capabilities.payloadDefaults.caption). For image sequences (payloadDefaults.sequence), lines map in order to sequence.items[].caption, with at most one line per image. Otherwise each element is one line. Line-count limits and whether **emphasis** is parsed are declared by the template — read its llmHint, which gen_script prints in full on stderr. **Must** pass when the user explicitly provides bullet-point text / bottom copy. **Also must pass — written by you — when the user did NOT provide any copy but the template is typewriter-driven** (capabilities.durationStrategy=fit-caption): such templates have no narration, so these lines are both the video's content and the thing that decides its duration. Draft them from the material you researched (repo README, page screenshots, the topic). Leaving this empty for a fit-caption template is rejected: nothing auto-generates caption text, and an empty caption renders a titled carousel with no words at all."
      },
      "stub_image_url": {
        "type": "string",
        "description": "Test-mode image stub URL. Only pass when the user explicitly says things like 'just testing / don't actually generate / use a placeholder image / stub URL / save credits' AND provides a concrete URL. With this set, every image AssetRef in the produced DSL is written as source=existing, status=generated, url=<this URL> — no gen-image call. Do not pass otherwise; if the user expressed the intent without a URL, ask for one — do not invent one."
      },
      "stub_video_url": {
        "type": "string",
        "description": "Test-mode video stub URL. Only pass when the user explicitly says things like 'just testing / don't actually generate the video / placeholder clip / save credits' AND provides a concrete URL. With this set, every video AssetRef in the produced DSL is written as source=existing, status=generated, url=<this URL> — no gen-video call. Do not pass otherwise; if the user expressed the intent without a URL, ask for one — do not invent one."
      },
      "skip_asset_generation": {
        "type": "boolean",
        "description": "All-in-one switch for downstream agents (e.g. template-creator) that only want the DSL shape: every produced AssetRef is marked as already generated with placeholder URLs (image: https://placeholder.local/stub.png, video: stub.mp4, audio: stub.mp3). Implies the equivalent of --stub-image-url + --stub-video-url with sentinel defaults plus the same rewrite for gen-voice / gen-digital-human assets. Useful when the agent only needs to inspect DSL structure or feed it into try_render_local with all assets pre-stubbed."
      },
      "item_count": {
        "type": "integer",
        "minimum": 1,
        "description": "Number of content beats, for templates declaring capabilities.sceneStrategy=countdown — the '一个开场 + N 个同类节拍，倒数编号' shape (Top-N lists, '5 个技巧', '7 个常见错误'). Produces one opening plus N scenes counting down from #N to #1. The accepted range and the default are declared by the chosen template (template_registry prints capabilities.countdown and the count field's bounds); a value outside it fails with the template's own numbers in the message. **Pass the count the user asked for, not the total scene count** — `scenes` is the total and the two are cross-checked, so passing the user's 5 here yields 5 beats while passing it as `scenes` would yield 4. The skeleton only carries each beat's index: fill in the real per-scene content before preparing assets. Keep any count stated in headline/subheadline consistent with this value — gen_script warns on a mismatch but never rewrites your copy."
      }
    },
    "required": [
      "topic"
    ]
  },
  "ui": {
    "primary": [
      "topic"
    ],
    "advanced": [
      "item_count"
    ],
    "hidden": [
      "duration",
      "style",
      "ratio",
      "scenes",
      "voice_id",
      "speed",
      "template_id",
      "headline",
      "subheadline",
      "carousel_items",
      "scene_images",
      "caption_lines",
      "stub_image_url",
      "stub_video_url",
      "skip_asset_generation"
    ]
  }
}
