[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6X7TA7P8SC011PSEYCNHKY9":3,"skill-same:01M6X7TA7P8SC011PSEYCNHKY9":87},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":59,"commitSha":60,"skillDirectory":61,"forksCount":62,"repositoryHtmlUrl":63,"repositoryHomepageUrl":64,"ownerHtmlUrl":65,"ownerBlogUrl":64,"ownerDescription":66,"updatedAt":67,"install":68,"resources":80},"01M6X7TA7P8SC011PSEYCNHKY9","song-to-script",null,"Turn an audio track into a timed `## Scene N` script: song sections → per-scene durations, auto mode adds mood + lip-sync lines. Triggers 'music video', 'from the song', 'cut to the beat'.","event4u-app\u002Fagent-config","event4u-app","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F199339632?v=4","01M6X5TWBNCQCE1VDKT4E9KF7N","event4u.app","01M6X5TWF4HRM266RG2Q5VVH1X",19,"MIT","https:\u002F\u002Fgithub.com\u002Fevent4u-app\u002Fagent-config\u002Fblob\u002Fmain\u002Fsrc\u002Fskills\u002Fsong-to-script\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000016","图像与多媒体","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43],"song to script","song","script","false","lip-sync","track","music video","mode","true","ai-video","sync","cut","triggers","director","auto","business","music","Audio","Video","product","2026-11-10 10:12:30",false,"---\nmodel_tier: inherit\nname: song-to-script\ndescription: \"Turn an audio track into a timed `## Scene N` script: song sections → per-scene durations, auto mode adds mood + lip-sync lines. Triggers 'music video', 'from the song', 'cut to the beat'.\"\npersonas:\n  - hollywood-director\ndomain: product\nworkspaces:\n  - small-business\npacks:\n  - ai-video\nlifecycle: experimental\ntrust:\n  level: experimental\ninstall:\n  default: false\n  removable: true\n---\n\n# song-to-script\n\n> Turn a song into `\u003Cproject>\u002Fscript.md` — a sequence of `## Scene N`\n> blocks whose `duration:` values sum to the track length and whose cut\n> points land on real section boundaries. Consumed by\n> [`\u002Fvideo:from-song`](..\u002F..\u002Fcommands\u002Fvideo\u002Ffrom-song.md), then handed\n> to [`scene-expander`](..\u002Fscene-expander\u002FSKILL.md) and\n> [`video-director`](..\u002Fvideo-director\u002FSKILL.md). Never invents timing —\n> every boundary comes from the audio probe, and the probe's `method`\n> tells this skill how musical (or not) those boundaries actually are.\n\n## When to use\n\n- A music-video run needs scenes cut to the song (`\u002Fvideo:from-song`).\n- An existing script must be **re-timed** to a track after the edit\n  drifted from the beat (the named second consumer — re-time without a\n  full re-author).\n\nDo NOT use when:\n\n- The operator already supplies a `## Scene N` script with `duration:`\n  values — feed it straight to `scene-expander`.\n- There is no audio — use the operator brief with `scene-expander`\n  directly.\n\n## Inputs\n\n- **Audio analysis** — adapter-first, probe as the floor:\n  - **Analysis adapter** (when an `audio-analysis` provider is\n    configured — see\n    [`audio-adapter-contract.md`](..\u002F..\u002Fscripts\u002Fai-video\u002Flib\u002Faudio-adapter-contract.md)):\n    `{bpm, beats, downbeats, sections:[{start,end,label,energy?}]}` —\n    real musical structure. Beats\u002Fdownbeats become the candidate cut\n    grid; section labels are musical (`verse`, `chorus`, …).\n  - **Audio probe** — JSON from\n    [`scripts\u002Fai-video\u002Flib\u002Fprobe-audio.sh`](..\u002F..\u002Fscripts\u002Fai-video\u002Flib\u002Fprobe-audio.sh):\n    `{duration, method, warning?, sections:[{start,end,energy,label}]}`.\n    - `method: silence` — boundaries are real quiet gaps; trust them as cuts.\n    - `method: rms` — boundaries are energy-delta inflections; usable but\n      coarse.\n    - `method: interval` — **the track is structurally flat** (brick-walled\n      \u002F sustained); sections are fixed-interval, NOT musical. When `method`\n      is `interval` (or `warning` is set), the emitted script header states\n      that timing is interval-based and the operator should pass\n      `--scene-durations` for musical sync. Never present interval cuts as\n      beat-synced.\n- **Model capabilities** — the chosen video model's renderable envelope\n  from the multiplexer manifest:\n  `scripts\u002Fai-video\u002Fadapters\u002F\u003Cprovider>.sh capability --model \u003Cid>` →\n  `{min_duration, max_duration, audio_sync, aspect, verified}`. Scene\n  durations MUST land inside `[min_duration, max_duration]`; a\n  `verified: false` manifest entry is surfaced in the report, never\n  trusted silently.\n- **Mode** — `brief` (operator text is the creative source) or `auto`\n  (infer mood + action from energy).\n- **Brief** (brief mode only) — free text: story, settings, look.\n- **Character lock** (optional) — `\u003Cproject>\u002Fcharacter.json` if a human\n  subject was locked. **Absent is normal** — abstract \u002F landscape \u002F\n  visualiser videos have no locked subject; see Step 2.\n\n## Procedure\n\n### Step 1: Map sections → scenes (capability-clamped, beat-aligned)\n\nOne `## Scene N` per analysis section. `duration:` = `end - start`\n(rounded to 0.5 s). Then **clamp the plan to the chosen model's\nrenderable envelope** — read `min_duration` \u002F `max_duration` from the\nmodel-capabilities manifest (`\u003Cprovider>.sh capability --model \u003Cid>`),\nfalling back to the provider tuning for single-model adapters:\n\n- **Section shorter than `min_duration`** → merge into its neighbour.\n  With beat data, merge toward the neighbour that keeps the joined cut\n  on a **downbeat** (else any beat); without beat data, merge into the\n  shorter neighbour.\n- **Section longer than `max_duration`** → split into sub-scenes. With\n  beat data, place every split point on the **nearest downbeat** (else\n  beat) to the equal-division point — never mid-beat; without beat\n  data, split equally.\n- **No valid plan exists** (e.g. the whole song is shorter than\n  `min_duration`, or a section cannot be split onto any beat inside the\n  envelope) → **halt and surface the conflict** with the model id and\n  the violated bound — an unbuildable plan never reaches the renderer.\n\nEvery emitted scene satisfies\n`min_duration ≤ duration ≤ max_duration`. When the manifest entry is\n`verified: false`, say so in the report — the envelope is\ndocumented-best-effort, not a smoke-traced fact.\n\nWhen the manifest entry declares `allowed_durations` (a grid — Veo\n`gemini-veo` renders exactly 4, 6 or 8 seconds), every scene length is a\nmember of it: split a long section into grid lengths (30 s on Veo →\n8 + 8 + 8 + 6) and merge or extend a short one onto the nearest grid value.\nAny other length is clamped at submit (the adapter says\n`requested \u003Cn>s -> rendered \u003Cm>s`), and the clip no longer fits its scene.\n\n### Step 2: Assign mood + action\n\nFirst decide the **subject mode**:\n\n- **Character mode** — `character.json` exists: every scene's `action:`\n  names the locked subject, never a fresh description.\n- **Style mode** — no `character.json`: scenes describe setting, palette,\n  and motion continuity (the recurring *look*), not a person. This is the\n  valid abstract \u002F landscape \u002F visualiser path — do not invent a human\n  subject to fill the slot.\n\nThen pick the **prompt source per segment — the modality switch**:\n\n- **Lyric segment** (the vocal map places ≥1 transcribed line inside\n  it) → the scene prompt derives from the **lyric line itself**: its\n  imagery, subjects, and verbs seed `mood:` + `action:` (in character\n  mode, acted by the locked subject; in style mode, rendered as\n  setting \u002F weather \u002F palette — never an invented human). The line\n  lands in `dialogue:` per Step 3.\n- **Instrumental segment** (no vocal-map line) → the scene prompt\n  derives from the **audio features**: section `label` + `energy` via\n  the intent table below. Never recycle a lyric from another segment\n  into an instrumental one.\n\nThen assign per scene:\n\n- **Brief mode** — distribute the brief's beats across scenes in order;\n  the modality switch still applies (lyric segments quote the brief's\n  matching beat through the lyric's lens), and energy modulates pacing.\n  Do not add story the brief did not state.\n- **Auto mode** — derive mood per section from `energy` and `label`\n  (probe labels and musical labels from the analysis adapter both map):\n\n  | label \u002F energy | default scene intent |\n  |---|---|\n  | intro \u002F low | establishing wide, slow camera, calm subject\u002Fscene |\n  | verse \u002F mid | narrative motion, medium framing, follow the subject |\n  | build \u002F rising | approach, tightening framing |\n  | chorus · drop \u002F peak | dynamic motion, weather\u002FFX, fast push |\n  | bridge · breakdown \u002F dip | close-up \u002F detail, quiet, single light source |\n  | outro \u002F fade | pull-back, resolve, hold |\n\n**Energy → cut frequency + motion intensity.** Section energy (0..1,\nrelative to the track mean) drives both how often the edit cuts and how\nhard the camera moves — chorus = faster cuts \u002F more motion:\n\n| energy vs. track mean | cut length target | `camera:` motion intensity |\n|---|---|---|\n| ≥ mean + 0.10 (chorus \u002F drop) | short — split the section toward `min_duration`, one scene per 1–2 downbeat bars | fast push \u002F whip \u002F handheld shake |\n| within ±0.10 of mean (verse \u002F build) | medium — one scene per section or per 4-bar phrase | steady dolly, slow tighten |\n| ≤ mean − 0.10 (breakdown \u002F outro) | long — merge toward `max_duration`, hold shots | locked-off or slow drift |\n\nHigh-energy splitting and low-energy merging both stay inside the\nStep 1 capability envelope and land on downbeats — the energy table\nchooses *where inside the envelope* a scene length falls, never\noutside it.\n\n### Step 3: Vocal map — transcribe, never guess (vocal tracks)\n\n```\nLYRIC TIMING AND SINGER COME FROM THE TRANSCRIBED AUDIO, NEVER FROM A\nBRIEF \u002F STORY SKELETON OR A GUESSED STRETCH. NEVER PUT ONE SINGER'S\nLINE ON ANOTHER SINGER'S SCENE.\n```\n\nWhen the track has vocals and the run intends lip-sync, build a\n**vocal map** from the real audio before assigning any `dialogue:`:\n\n1. **Transcribe** the audio to timestamped lines. Adapter-first: a\n   configured `lyrics` provider (e.g.\n   [`audio-adapters\u002Fwhisperx.sh`](..\u002F..\u002Fscripts\u002Fai-video\u002Faudio-adapters\u002Fwhisperx.sh))\n   returns word-level timestamps **plus per-line diarization labels**\n   (`SPEAKER_00`, …, or `\"?\"` when ambiguous):\n   ```bash\n   echo '{\"audio_path\":\"\u003Cvocal-stem-or-song>\"}' \\\n     | scripts\u002Fai-video\u002Faudio-adapters\u002Fwhisperx.sh analyze\n   ```\n   No lyrics provider configured → OpenAI `\u002Fv1\u002Faudio\u002Ftranscriptions`\n   (`response_format=verbose_json` → `segments[].{start,end,text}`) or\n   local whisper as before (no speaker labels — every line starts as\n   `\"?\"`). Either way the transcript is the only source of lyric timing.\n2. **Label the singer** per line — map diarization labels (or unlabeled\n   lines) to cast names via the operator's who-sings reference (a\n   roster, a brief that names who sings which line, or a character\n   cast): one diarization label ↦ one cast name, consistently. If a\n   line's singer is genuinely ambiguous (label `\"?\"`, mixed-speaker\n   line, or no roster match), keep `singer: \"?\"` and surface it —\n   never guess a singer to fill the slot.\n3. **Emit** `\u003Cproject>\u002Fvocal-map.json`:\n   `[{start, end, text, singer}]`, timing verbatim from the transcript.\n4. **Validate** — run the ground-truth enforcer before handing the map\n   to the sign-off gate:\n   ```bash\n   scripts\u002Fai-video\u002Flib\u002Fvalidate-vocal-map.sh \u003Cproject>\u002Fvocal-map.json \\\n     \u003Cproject>\u002Ftranscript.json --roster \"\u003Ccast names>\"\n   ```\n   It rejects re-timed lines, lyrics not in the transcript, and missing\n   singers (exit 7, specific line named). A red validator is a halt —\n   fix the map, never bypass.\n5. **Place lines into the matching scene's** `dialogue:` block using the\n   transcript timing, tagged with the singer (`singer: \"\u003Cline>\"`). A\n   scene's lip-sync subject MUST be the line's labelled singer; a\n   `\"?\"` line gets NO lip-sync scene until the operator resolves it.\n\nNo vocals \u002F no transcript \u002F no lip-sync intent → leave `dialogue:` empty;\nthe scene is performance \u002F B-roll. **Never fabricate lyrics**, **never\nre-time a line off the brief**, and in style mode `dialogue:` stays empty\n(lip-sync needs a character subject). The `\u002Fvideo:from-song` sign-off\ngate (its Step 6) shows this map for approval before any render.\n\n### Step 4: Emit + reconcile\n\nWrite `\u003Cproject>\u002Fscript.md` (and `\u003Cproject>\u002Fvocal-map.json` when the\ntrack has vocals). Report the delta, the section→scene map, **the probe\n`method`** (so the operator sees whether cuts are silence-derived,\nenergy-derived, or interval-fallback), **and whether lyric timing is\ntranscript-derived** (it must be — never brief-derived). If the sum\ncannot be reconciled (e.g. provider max-duration forces more time than\nthe song has), **halt and surface the conflict** — do not pad silently.\n\n### Step 5: Validate before handoff\n\nConcrete checks (all must pass before the script is handed to\n`scene-expander`):\n\n- **Assert** `Σ(duration) == probe.duration` within ±1.0 s; report the\n  exact delta. A larger delta → halt, do not pad.\n- **Verify** every scene boundary equals a probe section boundary (or a\n  `--scene-durations` value) — no invented cut points.\n- **Confirm** no scene `duration:` exceeds the model's `max_duration`\n  or falls below `min_duration` (model-capabilities manifest, or the\n  provider tuning for single-model adapters).\n- **Verify** every lyric-segment scene derives its prompt from its own\n  vocal-map line and every instrumental scene from audio features — no\n  cross-segment lyric recycling (modality switch).\n- **Ensure** every `## Scene N` carries all five keys (`duration` ·\n  `mood` · `action` · `camera` · `dialogue`), and that `dialogue:` is\n  empty in style mode.\n\n## Output format\n\n1. `script.md` opens with the derivation header —\n   `# \u003Cproject> — derived from \u003Csong-file> (\u003Cmode> mode · cuts: \u003Cmethod>)`\n   — so the probe `method` stays visible downstream.\n2. One `## Scene N` block per cut carrying exactly the keys\n   `duration` · `mood` · `action` · `camera` · `dialogue` —\n   `scene-expander` consumes this verbatim; keep the keys exact.\n3. `dialogue:` stays empty unless operator-supplied lyrics cover the\n   section — detected vocal energy alone never fills it.\n\n```markdown\n# \u003Cproject> — derived from \u003Csong-file> (\u003Cmode> mode · cuts: \u003Cmethod>)\n\n## Scene 1\nduration: 6.0\nmood: establishing, cold, pre-storm\naction: \u003Csubject from character.json, OR style description in style mode>\ncamera: slow push-in\ndialogue:\n\n## Scene 2\nduration: 4.5\nmood: build, rising tension\naction: close on \u003Csubject \u002F detail>, wind picking up\ncamera: handheld tighten\ndialogue:\n  - \"\u003Csubject>: \\\"\u003Clyric line for this section, if any>\\\"\"\n```\n\n## Gotcha\n\n- **`method: interval` is the brick-walled-master signal, not a bug.**\n  A compressed modern master has near-constant RMS and no silence, so the\n  probe degrades to fixed intervals. That is the honest floor — surface\n  it and point the operator at `--scene-durations`; never dress interval\n  cuts up as beat-synced.\n- **A vocal section without supplied lyrics is B-roll, not lip-sync.**\n  Detected vocal energy alone does not authorise `dialogue:` — only\n  operator-supplied lyrics do.\n- **Style mode is the default for a no-character run**, not an error\n  path. Landscape \u002F abstract \u002F visualiser videos never get a fabricated\n  human subject.\n\n## Do NOT\n\n- **Do NOT invent timing.** Every cut maps to a probe boundary or a\n  `--scene-durations` value — never to taste.\n- **Do NOT present `interval`-fallback cuts as beat-synced.** Always\n  surface the probe `method`.\n- **Do NOT emit a clip outside the provider's min\u002Fmax duration** —\n  split\u002Fmerge in Step 1 instead.\n- **Do NOT fabricate lyrics or story** beyond the brief \u002F detected vocals.\n- **Do NOT invent a human subject** in style mode; defer identity to\n  `character.json` only when a lock exists.\n- **Do NOT pad a unreconcilable timing sum** — halt and surface it.\n\n## See also\n\n- [`\u002Fvideo:from-song`](..\u002F..\u002Fcommands\u002Fvideo\u002Ffrom-song.md) — the command\n  that drives this skill\n- [`scene-expander`](..\u002Fscene-expander\u002FSKILL.md) — consumes the emitted\n  script\n- [`character-consistency`](..\u002Fcharacter-consistency\u002FSKILL.md) — supplies\n  the locked subject referenced in `action:` (character mode only)\n","# song-to-script\n\n> Turn a song into `\u003Cproject>\u002Fscript.md` — a sequence of `## Scene N`\n> blocks whose `duration:` values sum to the track length and whose cut\n> points land on real section boundaries. Consumed by\n> [`\u002Fvideo:from-song`](..\u002F..\u002Fcommands\u002Fvideo\u002Ffrom-song.md), then handed\n> to [`scene-expander`](..\u002Fscene-expander\u002FSKILL.md) and\n> [`video-director`](..\u002Fvideo-director\u002FSKILL.md). Never invents timing —\n> every boundary comes from the audio probe, and the probe's `method`\n> tells this skill how musical (or not) those boundaries actually are.\n\n## When to use\n\n- A music-video run needs scenes cut to the song (`\u002Fvideo:from-song`).\n- An existing script must be **re-timed** to a track after the edit\n  drifted from the beat (the named second consumer — re-time without a\n  full re-author).\n\nDo NOT use when:\n\n- The operator already supplies a `## Scene N` script with `duration:`\n  values — feed it straight to `scene-expander`.\n- There is no audio — use the operator brief with `scene-expander`\n  directly.\n\n## Inputs\n\n- **Audio analysis** — adapter-first, probe as the floor:\n  - **Analysis adapter** (when an `audio-analysis` provider is\n    configured — see\n    [`audio-adapter-contract.md`](..\u002F..\u002Fscripts\u002Fai-video\u002Flib\u002Faudio-adapter-contract.md)):\n    `{bpm, beats, downbeats, sections:[{start,end,label,energy?}]}` —\n    real musical structure. Beats\u002Fdownbeats become the candidate cut\n    grid; section labels are musical (`verse`, `chorus`, …).\n  - **Audio probe** — JSON from\n    [`scripts\u002Fai-video\u002Flib\u002Fprobe-audio.sh`](..\u002F..\u002Fscripts\u002Fai-video\u002Flib\u002Fprobe-audio.sh):\n    `{duration, method, warning?, sections:[{start,end,energy,label}]}`.\n    - `method: silence` — boundaries are real quiet gaps; trust them as cuts.\n    - `method: rms` — boundaries are energy-delta inflections; usable but\n      coarse.\n    - `method: interval` — **the track is structurally flat** (brick-walled\n      \u002F sustained); sections are fixed-interval, NOT musical. When `method`\n      is `interval` (or `warning` is set), the emitted script header states\n      that timing is interval-based and the operator should pass\n      `--scene-durations` for musical sync. Never present interval cuts as\n      beat-synced.\n- **Model capabilities** — the chosen video model's renderable envelope\n  from the multiplexer manifest:\n  `scripts\u002Fai-video\u002Fadapters\u002F\u003Cprovider>.sh capability --model \u003Cid>` →\n  `{min_duration, max_duration, audio_sync, aspect, verified}`. Scene\n  durations MUST land inside `[min_duration, max_duration]`; a\n  `verified: false` manifest entry is surfaced in the report, never\n  trusted silently.\n- **Mode** — `brief` (operator text is the creative source) or `auto`\n  (infer mood + action from energy).\n- **Brief** (brief mode only) — free text: story, settings, look.\n- **Character lock** (optional) — `\u003Cproject>\u002Fcharacter.json` if a human\n  subject was locked. **Absent is normal** — abstract \u002F landscape \u002F\n  visualiser videos have no locked subject; see Step 2.\n\n## Procedure\n\n### Step 1: Map sections → scenes (capability-clamped, beat-aligned)\n\nOne `## Scene N` per analysis section. `duration:` = `end - start`\n(rounded to 0.5 s). Then **clamp the plan to the chosen model's\nrenderable envelope** — read `min_duration` \u002F `max_duration` from the\nmodel-capabilities manifest (`\u003Cprovider>.sh capability --model \u003Cid>`),\nfalling back to the provider tuning for single-model adapters:\n\n- **Section shorter than `min_duration`** → merge into its neighbour.\n  With beat data, merge toward the neighbour that keeps the joined cut\n  on a **downbeat** (else any beat); without beat data, merge into the\n  shorter neighbour.\n- **Section longer than `max_duration`** → split into sub-scenes. With\n  beat data, place every split point on the **nearest downbeat** (else\n  beat) to the equal-division point — never mid-beat; without beat\n  data, split equally.\n- **No valid plan exists** (e.g. the whole song is shorter than\n  `min_duration`, or a section cannot be split onto any beat inside the\n  envelope) → **halt and surface the conflict** with the model id and\n  the violated bound — an unbuildable plan never reaches the renderer.\n\nEvery emitted scene satisfies\n`min_duration ≤ duration ≤ max_duration`. When the manifest entry is\n`verified: false`, say so in the report — the envelope is\ndocumented-best-effort, not a smoke-traced fact.\n\nWhen the manifest entry declares `allowed_durations` (a grid — Veo\n`gemini-veo` renders exactly 4, 6 or 8 seconds), every scene length is a\nmember of it: split a long section into grid lengths (30 s on Veo →\n8 + 8 + 8 + 6) and merge or extend a short one onto the nearest grid value.\nAny other length is clamped at submit (the adapter says\n`requested \u003Cn>s -> rendered \u003Cm>s`), and the clip no longer fits its scene.\n\n### Step 2: Assign mood + action\n\nFirst decide the **subject mode**:\n\n- **Character mode** — `character.json` exists: every scene's `action:`\n  names the locked subject, never a fresh description.\n- **Style mode** — no `character.json`: scenes describe setting, palette,\n  and motion continuity (the recurring *look*), not a person. This is the\n  valid abstract \u002F landscape \u002F visualiser path — do not invent a human\n  subject to fill the slot.\n\nThen pick the **prompt source per segment — the modality switch**:\n\n- **Lyric segment** (the vocal map places ≥1 transcribed line inside\n  it) → the scene prompt derives from the **lyric line itself**: its\n  imagery, subjects, and verbs seed `mood:` + `action:` (in character\n  mode, acted by the locked subject; in style mode, rendered as\n  setting \u002F weather \u002F palette — never an invented human). The line\n  lands in `dialogue:` per Step 3.\n- **Instrumental segment** (no vocal-map line) → the scene prompt\n  derives from the **audio features**: section `label` + `energy` via\n  the intent table below. Never recycle a lyric from another segment\n  into an instrumental one.\n\nThen assign per scene:\n\n- **Brief mode** — distribute the brief's beats across scenes in order;\n  the modality switch still applies (lyric segments quote the brief's\n  matching beat through the lyric's lens), and energy modulates pacing.\n  Do not add story the brief did not state.\n- **Auto mode** — derive mood per section from `energy` and `label`\n  (probe labels and musical labels from the analysis adapter both map):\n\n  | label \u002F energy | default scene intent |\n  |---|---|\n  | intro \u002F low | establishing wide, slow camera, calm subject\u002Fscene |\n  | verse \u002F mid | narrative motion, medium framing, follow the subject |\n  | build \u002F rising | approach, tightening framing |\n  | chorus · drop \u002F peak | dynamic motion, weather\u002FFX, fast push |\n  | bridge · breakdown \u002F dip | close-up \u002F detail, quiet, single light source |\n  | outro \u002F fade | pull-back, resolve, hold |\n\n**Energy → cut frequency + motion intensity.** Section energy (0..1,\nrelative to the track mean) drives both how often the edit cuts and how\nhard the camera moves — chorus = faster cuts \u002F more motion:\n\n| energy vs. track mean | cut length target | `camera:` motion intensity |\n|---|---|---|\n| ≥ mean + 0.10 (chorus \u002F drop) | short — split the section toward `min_duration`, one scene per 1–2 downbeat bars | fast push \u002F whip \u002F handheld shake |\n| within ±0.10 of mean (verse \u002F build) | medium — one scene per section or per 4-bar phrase | steady dolly, slow tighten |\n| ≤ mean − 0.10 (breakdown \u002F outro) | long — merge toward `max_duration`, hold shots | locked-off or slow drift |\n\nHigh-energy splitting and low-energy merging both stay inside the\nStep 1 capability envelope and land on downbeats — the energy table\nchooses *where inside the envelope* a scene length falls, never\noutside it.\n\n### Step 3: Vocal map — transcribe, never guess (vocal tracks)\n\n```\nLYRIC TIMING AND SINGER COME FROM THE TRANSCRIBED AUDIO, NEVER FROM A\nBRIEF \u002F STORY SKELETON OR A GUESSED STRETCH. NEVER PUT ONE SINGER'S\nLINE ON ANOTHER SINGER'S SCENE.\n```\n\nWhen the track has vocals and the run intends lip-sync, build a\n**vocal map** from the real audio before assigning any `dialogue:`:\n\n1. **Transcribe** the audio to timestamped lines. Adapter-first: a\n   configured `lyrics` provider (e.g.\n   [`audio-adapters\u002Fwhisperx.sh`](..\u002F..\u002Fscripts\u002Fai-video\u002Faudio-adapters\u002Fwhisperx.sh))\n   returns word-level timestamps **plus per-line diarization labels**\n   (`SPEAKER_00`, …, or `\"?\"` when ambiguous):\n   ```bash\n   echo '{\"audio_path\":\"\u003Cvocal-stem-or-song>\"}' \\\n     | scripts\u002Fai-video\u002Faudio-adapters\u002Fwhisperx.sh analyze\n   ```\n   No lyrics provider configured → OpenAI `\u002Fv1\u002Faudio\u002Ftranscriptions`\n   (`response_format=verbose_json` → `segments[].{start,end,text}`) or\n   local whisper as before (no speaker labels — every line starts as\n   `\"?\"`). Either way the transcript is the only source of lyric timing.\n2. **Label the singer** per line — map diarization labels (or unlabeled\n   lines) to cast names via the operator's who-sings reference (a\n   roster, a brief that names who sings which line, or a character\n   cast): one diarization label ↦ one cast name, consistently. If a\n   line's singer is genuinely ambiguous (label `\"?\"`, mixed-speaker\n   line, or no roster match), keep `singer: \"?\"` and surface it —\n   never guess a singer to fill the slot.\n3. **Emit** `\u003Cproject>\u002Fvocal-map.json`:\n   `[{start, end, text, singer}]`, timing verbatim from the transcript.\n4. **Validate** — run the ground-truth enforcer before handing the map\n   to the sign-off gate:\n   ```bash\n   scripts\u002Fai-video\u002Flib\u002Fvalidate-vocal-map.sh \u003Cproject>\u002Fvocal-map.json \\\n     \u003Cproject>\u002Ftranscript.json --roster \"\u003Ccast names>\"\n   ```\n   It rejects re-timed lines, lyrics not in the transcript, and missing\n   singers (exit 7, specific line named). A red validator is a halt —\n   fix the map, never bypass.\n5. **Place lines into the matching scene's** `dialogue:` block using the\n   transcript timing, tagged with the singer (`singer: \"\u003Cline>\"`). A\n   scene's lip-sync subject MUST be the line's labelled singer; a\n   `\"?\"` line gets NO lip-sync scene until the operator resolves it.\n\nNo vocals \u002F no transcript \u002F no lip-sync intent → leave `dialogue:` empty;\nthe scene is performance \u002F B-roll. **Never fabricate lyrics**, **never\nre-time a line off the brief**, and in style mode `dialogue:` stays empty\n(lip-sync needs a character subject). The `\u002Fvideo:from-song` sign-off\ngate (its Step 6) shows this map for approval before any render.\n\n### Step 4: Emit + reconcile\n\nWrite `\u003Cproject>\u002Fscript.md` (and `\u003Cproject>\u002Fvocal-map.json` when the\ntrack has vocals). Report the delta, the section→scene map, **the probe\n`method`** (so the operator sees whether cuts are silence-derived,\nenergy-derived, or interval-fallback), **and whether lyric timing is\ntranscript-derived** (it must be — never brief-derived). If the sum\ncannot be reconciled (e.g. provider max-duration forces more time than\nthe song has), **halt and surface the conflict** — do not pad silently.\n\n### Step 5: Validate before handoff\n\nConcrete checks (all must pass before the script is handed to\n`scene-expander`):\n\n- **Assert** `Σ(duration) == probe.duration` within ±1.0 s; report the\n  exact delta. A larger delta → halt, do not pad.\n- **Verify** every scene boundary equals a probe section boundary (or a\n  `--scene-durations` value) — no invented cut points.\n- **Confirm** no scene `duration:` exceeds the model's `max_duration`\n  or falls below `min_duration` (model-capabilities manifest, or the\n  provider tuning for single-model adapters).\n- **Verify** every lyric-segment scene derives its prompt from its own\n  vocal-map line and every instrumental scene from audio features — no\n  cross-segment lyric recycling (modality switch).\n- **Ensure** every `## Scene N` carries all five keys (`duration` ·\n  `mood` · `action` · `camera` · `dialogue`), and that `dialogue:` is\n  empty in style mode.\n\n## Output format\n\n1. `script.md` opens with the derivation header —\n   `# \u003Cproject> — derived from \u003Csong-file> (\u003Cmode> mode · cuts: \u003Cmethod>)`\n   — so the probe `method` stays visible downstream.\n2. One `## Scene N` block per cut carrying exactly the keys\n   `duration` · `mood` · `action` · `camera` · `dialogue` —\n   `scene-expander` consumes this verbatim; keep the keys exact.\n3. `dialogue:` stays empty unless operator-supplied lyrics cover the\n   section — detected vocal energy alone never fills it.\n\n```markdown\n# \u003Cproject> — derived from \u003Csong-file> (\u003Cmode> mode · cuts: \u003Cmethod>)\n\n## Scene 1\nduration: 6.0\nmood: establishing, cold, pre-storm\naction: \u003Csubject from character.json, OR style description in style mode>\ncamera: slow push-in\ndialogue:\n\n## Scene 2\nduration: 4.5\nmood: build, rising tension\naction: close on \u003Csubject \u002F detail>, wind picking up\ncamera: handheld tighten\ndialogue:\n  - \"\u003Csubject>: \\\"\u003Clyric line for this section, if any>\\\"\"\n```\n\n## Gotcha\n\n- **`method: interval` is the brick-walled-master signal, not a bug.**\n  A compressed modern master has near-constant RMS and no silence, so the\n  probe degrades to fixed intervals. That is the honest floor — surface\n  it and point the operator at `--scene-durations`; never dress interval\n  cuts up as beat-synced.\n- **A vocal section without supplied lyrics is B-roll, not lip-sync.**\n  Detected vocal energy alone does not authorise `dialogue:` — only\n  operator-supplied lyrics do.\n- **Style mode is the default for a no-character run**, not an error\n  path. Landscape \u002F abstract \u002F visualiser videos never get a fabricated\n  human subject.\n\n## Do NOT\n\n- **Do NOT invent timing.** Every cut maps to a probe boundary or a\n  `--scene-durations` value — never to taste.\n- **Do NOT present `interval`-fallback cuts as beat-synced.** Always\n  surface the probe `method`.\n- **Do NOT emit a clip outside the provider's min\u002Fmax duration** —\n  split\u002Fmerge in Step 1 instead.\n- **Do NOT fabricate lyrics or story** beyond the brief \u002F detected vocals.\n- **Do NOT invent a human subject** in style mode; defer identity to\n  `character.json` only when a lock exists.\n- **Do NOT pad a unreconcilable timing sum** — halt and surface it.\n\n## See also\n\n- [`\u002Fvideo:from-song`](..\u002F..\u002Fcommands\u002Fvideo\u002Ffrom-song.md) — the command\n  that drives this skill\n- [`scene-expander`](..\u002Fscene-expander\u002FSKILL.md) — consumes the emitted\n  script\n- [`character-consistency`](..\u002Fcharacter-consistency\u002FSKILL.md) — supplies\n  the locked subject referenced in `action:` (character mode only)\n",{"name":5,"packs":49,"trust":50,"domain":43,"install":52,"personas":54,"lifecycle":51,"model_tier":56,"workspaces":57,"description":7},[33],{"level":51},"experimental",{"default":45,"removable":53},true,[55],"hollywood-director","inherit",[58],"small-business","main","12bd765ce1d800501d45dfb8232102793af01290","src\u002Fskills\u002Fsong-to-script",1,"https:\u002F\u002Fgithub.com\u002Fevent4u-app\u002Fagent-config","https:\u002F\u002Fevent4u.app","https:\u002F\u002Fgithub.com\u002Fevent4u-app","Your events in your hand","2026-11-14 02:29:41",{"source":8,"skillName":5,"commands":69,"prompt":79,"zipAvailable":53},[70,73,76],{"tool":71,"command":72},"npx","npx skills add event4u-app\u002Fagent-config --skill song-to-script",{"tool":74,"command":75},"bunx","bunx skills add event4u-app\u002Fagent-config --skill song-to-script",{"tool":77,"command":78},"pnpm","pnpm dlx skills add event4u-app\u002Fagent-config --skill song-to-script","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fevent4u-app\u002Fagent-config\u002Fblob\u002Fmain\u002Fsrc\u002Fskills\u002Fsong-to-script\u002FSKILL.md",[81],{"path":82,"resourceType":83,"resourceTypeDesc":84,"sizeBytes":85,"sourceHtmlUrl":86},"evals\u002Ftriggers.json","REFERENCE","参考文档",1772,"https:\u002F\u002Fgithub.com\u002Fevent4u-app\u002Fagent-config\u002Fblob\u002Fmain\u002Fsrc\u002Fskills\u002Fsong-to-script\u002Fevals\u002Ftriggers.json",[88,104,117,129,141,151],{"skillNo":89,"name":90,"nameZh":6,"description":91,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":92,"categories":93,"keywords":97,"publishedAt":44},"01M6X84PK96XBPENXH66JCTSD1","unknown-key-fixture","Declares a key the schema does not know.","https:\u002F\u002Fgithub.com\u002Fevent4u-app\u002Fagent-config\u002Fblob\u002Fmain\u002Ftests\u002Fscripts\u002Ffixtures\u002Fbody-portable\u002Funknown-key\u002FSKILL.md",[94],{"categoryNo":95,"categoryName":96,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[98,99,100,101,102,103],"unknown key fixture","Unknown key","fixture","unknown","schema","package",{"skillNo":105,"name":106,"nameZh":6,"description":107,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":108,"categories":109,"keywords":111,"publishedAt":44},"01M6X84HW7NAHZ7GZMQ9RXK1XJ","portable-fixture","Declares only semantics the MCP-lite carrier transports whole.","https:\u002F\u002Fgithub.com\u002Fevent4u-app\u002Fagent-config\u002Fblob\u002Fmain\u002Ftests\u002Fscripts\u002Ffixtures\u002Fbody-portable\u002Fportable\u002FSKILL.md",[110],{"categoryNo":95,"categoryName":96,"taxonomyType":21,"taxonomyTypeDesc":22},[100,112,113,114,115,103,116],"portable","portable fixture","engineer","only","MCP",{"skillNo":118,"name":119,"nameZh":6,"description":120,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":121,"categories":122,"keywords":124,"publishedAt":44},"01M6X84F29FE0RGGZ1ZTFGZTX2","non-portable-fixture","Declares a runtime semantic the carrier drops.","https:\u002F\u002Fgithub.com\u002Fevent4u-app\u002Fagent-config\u002Fblob\u002Fmain\u002Ftests\u002Fscripts\u002Ffixtures\u002Fbody-portable\u002Fnon-portable\u002FSKILL.md",[123],{"categoryNo":95,"categoryName":96,"taxonomyType":21,"taxonomyTypeDesc":22},[113,125,112,100,126,127,128,103],"non portable fixture","semantic","high","runtime",{"skillNo":130,"name":131,"nameZh":6,"description":132,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":133,"categories":134,"keywords":136,"publishedAt":44},"01M6X848F706PT9MDCX75JKJ6B","candidate-zero-width","A candidate whose prose carries an invisible joiner.","https:\u002F\u002Fgithub.com\u002Fevent4u-app\u002Fagent-config\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fskill-scout-candidates\u002Fzero-width-injection\u002FSKILL.md",[135],{"categoryNo":95,"categoryName":96,"taxonomyType":21,"taxonomyTypeDesc":22},[137,138,139,140],"candidate zero width","candidate","ZeRO","prose",{"skillNo":142,"name":143,"nameZh":6,"description":144,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":145,"categories":146,"keywords":148,"publishedAt":44},"01M6X841SSC2T31VDF41SA6DG1","candidate-disclosure-suppression","A candidate whose prose tells the agent to withhold from the user.","https:\u002F\u002Fgithub.com\u002Fevent4u-app\u002Fagent-config\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fskill-scout-candidates\u002Fdisclosure-suppression\u002FSKILL.md",[147],{"categoryNo":95,"categoryName":96,"taxonomyType":21,"taxonomyTypeDesc":22},[149,138,140,150],"candidate disclosure suppression","user",{"skillNo":152,"name":153,"nameZh":6,"description":154,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":155,"categories":156,"keywords":158,"publishedAt":44},"01M6X83TFS5AGPBPVTQNWP0BB3","candidate-dangerous-frontmatter","A candidate claiming an unscoped tool grant and a permission bypass.","https:\u002F\u002Fgithub.com\u002Fevent4u-app\u002Fagent-config\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fskill-scout-candidates\u002Fdangerous-frontmatter\u002FSKILL.md",[157],{"categoryNo":95,"categoryName":96,"taxonomyType":21,"taxonomyTypeDesc":22},[159,138,160,161,162,163],"candidate dangerous frontmatter","frontmatter","shell","permission","tool"]