Best for
- A music-video run needs scenes cut to the song (/video:from-song).
- An existing script must be re-timed to a track after the edit
- The operator already supplies a Scene N script with duration:
event4u-app/agent-config/src/skills/song-to-script/SKILL.md
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'.
Decision brief
Turn a song into /script.md — a sequence of Scene N blocks whose duration: values sum to the track length and whose cut points land on real section boundaries. Consumed by /video:from-song, then handed to scene-expander and video-director. Never invents timing — every boundary c…
Compatibility matrix
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/event4u-app/agent-config --skill "src/skills/song-to-script"Inspect the Agent Skill "song-to-script" from https://github.com/event4u-app/agent-config/blob/0adf49a8ae84b0ff6e2de8759eea43257e020eff/src/skills/song-to-script/SKILL.md at commit 0adf49a8ae84b0ff6e2de8759eea43257e020eff. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
One Scene N per analysis section. duration: = end - start (rounded to 0.5 s). Then clamp the plan to the chosen model's renderable envelope — read minduration / maxduration from the model-capabilities manifest (.sh capability --model ), falling back to the provider tuning for si…
One Scene N per analysis section. duration: = end - start (rounded to 0.5 s). Then clamp the plan to the chosen model's renderable envelope — read minduration / maxduration from the model-capabilities manifest (.sh capability --model ), falling back to the provider tuning for si…
First decide the subject mode:
When the track has vocals and the run intends lip-sync, build a vocal map from the real audio before assigning any dialogue::
Write /script.md (and /vocal-map.json when the track has vocals). Report the delta, the section→scene map, the probe method (so the operator sees whether cuts are silence-derived, energy-derived, or interval-fallback), and whether lyric timing is transcript-derived (it must be —…
Permission review
No configured static risk pattern was detected
This is not proof of safety. Runtime behavior, indirect dependencies, and hidden external systems are outside the static scan.
Evidence record
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 93/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 7 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
Turn a song into
<project>/script.md— a sequence of## Scene Nblocks whoseduration:values sum to the track length and whose cut points land on real section boundaries. Consumed by/video:from-song, then handed toscene-expanderandvideo-director. Never invents timing — every boundary comes from the audio probe, and the probe'smethodtells this skill how musical (or not) those boundaries actually are.
/video:from-song).Do NOT use when:
## Scene N script with duration:
values — feed it straight to scene-expander.scene-expander
directly.audio-analysis provider is
configured — see
audio-adapter-contract.md):
{bpm, beats, downbeats, sections:[{start,end,label,energy?}]} —
real musical structure. Beats/downbeats become the candidate cut
grid; section labels are musical (verse, chorus, …).scripts/ai-video/lib/probe-audio.sh:
{duration, method, warning?, sections:[{start,end,energy,label}]}.
method: silence — boundaries are real quiet gaps; trust them as cuts.method: rms — boundaries are energy-delta inflections; usable but
coarse.method: interval — the track is structurally flat (brick-walled
/ sustained); sections are fixed-interval, NOT musical. When method
is interval (or warning is set), the emitted script header states
that timing is interval-based and the operator should pass
--scene-durations for musical sync. Never present interval cuts as
beat-synced.scripts/ai-video/adapters/<provider>.sh capability --model <id> →
{min_duration, max_duration, audio_sync, aspect, verified}. Scene
durations MUST land inside [min_duration, max_duration]; a
verified: false manifest entry is surfaced in the report, never
trusted silently.brief (operator text is the creative source) or auto
(infer mood + action from energy).<project>/character.json if a human
subject was locked. Absent is normal — abstract / landscape /
visualiser videos have no locked subject; see Step 2.One ## Scene N per analysis section. duration: = end - start
(rounded to 0.5 s). Then clamp the plan to the chosen model's
renderable envelope — read min_duration / max_duration from the
model-capabilities manifest (<provider>.sh capability --model <id>),
falling back to the provider tuning for single-model adapters:
min_duration → merge into its neighbour.
With beat data, merge toward the neighbour that keeps the joined cut
on a downbeat (else any beat); without beat data, merge into the
shorter neighbour.max_duration → split into sub-scenes. With
beat data, place every split point on the nearest downbeat (else
beat) to the equal-division point — never mid-beat; without beat
data, split equally.min_duration, or a section cannot be split onto any beat inside the
envelope) → halt and surface the conflict with the model id and
the violated bound — an unbuildable plan never reaches the renderer.Every emitted scene satisfies
min_duration ≤ duration ≤ max_duration. When the manifest entry is
verified: false, say so in the report — the envelope is
documented-best-effort, not a smoke-traced fact.
First decide the subject mode:
character.json exists: every scene's action:
names the locked subject, never a fresh description.character.json: scenes describe setting, palette,
and motion continuity (the recurring look), not a person. This is the
valid abstract / landscape / visualiser path — do not invent a human
subject to fill the slot.Then pick the prompt source per segment — the modality switch:
mood: + action: (in character
mode, acted by the locked subject; in style mode, rendered as
setting / weather / palette — never an invented human). The line
lands in dialogue: per Step 3.label + energy via
the intent table below. Never recycle a lyric from another segment
into an instrumental one.Then assign per scene:
Brief mode — distribute the brief's beats across scenes in order; the modality switch still applies (lyric segments quote the brief's matching beat through the lyric's lens), and energy modulates pacing. Do not add story the brief did not state.
Auto mode — derive mood per section from energy and label
(probe labels and musical labels from the analysis adapter both map):
| label / energy | default scene intent |
|---|---|
| intro / low | establishing wide, slow camera, calm subject/scene |
| verse / mid | narrative motion, medium framing, follow the subject |
| build / rising | approach, tightening framing |
| chorus · drop / peak | dynamic motion, weather/FX, fast push |
| bridge · breakdown / dip | close-up / detail, quiet, single light source |
| outro / fade | pull-back, resolve, hold |
Energy → cut frequency + motion intensity. Section energy (0..1, relative to the track mean) drives both how often the edit cuts and how hard the camera moves — chorus = faster cuts / more motion:
| energy vs. track mean | cut length target | camera: motion intensity |
|---|---|---|
| ≥ mean + 0.10 (chorus / drop) | short — split the section toward min_duration, one scene per 1–2 downbeat bars | fast push / whip / handheld shake |
| within ±0.10 of mean (verse / build) | medium — one scene per section or per 4-bar phrase | steady dolly, slow tighten |
| ≤ mean − 0.10 (breakdown / outro) | long — merge toward max_duration, hold shots | locked-off or slow drift |
High-energy splitting and low-energy merging both stay inside the Step 1 capability envelope and land on downbeats — the energy table chooses where inside the envelope a scene length falls, never outside it.
LYRIC TIMING AND SINGER COME FROM THE TRANSCRIBED AUDIO, NEVER FROM A
BRIEF / STORY SKELETON OR A GUESSED STRETCH. NEVER PUT ONE SINGER'S
LINE ON ANOTHER SINGER'S SCENE.
When the track has vocals and the run intends lip-sync, build a
vocal map from the real audio before assigning any dialogue::
lyrics provider (e.g.
audio-adapters/whisperx.sh)
returns word-level timestamps plus per-line diarization labels
(SPEAKER_00, …, or "?" when ambiguous):
echo '{"audio_path":"<vocal-stem-or-song>"}' \
| scripts/ai-video/audio-adapters/whisperx.sh analyze
No lyrics provider configured → OpenAI /v1/audio/transcriptions
(response_format=verbose_json → segments[].{start,end,text}) or
local whisper as before (no speaker labels — every line starts as
"?"). Either way the transcript is the only source of lyric timing."?", mixed-speaker
line, or no roster match), keep singer: "?" and surface it —
never guess a singer to fill the slot.<project>/vocal-map.json:
[{start, end, text, singer}], timing verbatim from the transcript.scripts/ai-video/lib/validate-vocal-map.sh <project>/vocal-map.json \
<project>/transcript.json --roster "<cast names>"
It rejects re-timed lines, lyrics not in the transcript, and missing
singers (exit 7, specific line named). A red validator is a halt —
fix the map, never bypass.dialogue: block using the
transcript timing, tagged with the singer (singer: "<line>"). A
scene's lip-sync subject MUST be the line's labelled singer; a
"?" line gets NO lip-sync scene until the operator resolves it.No vocals / no transcript / no lip-sync intent → leave dialogue: empty;
the scene is performance / B-roll. Never fabricate lyrics, never
re-time a line off the brief, and in style mode dialogue: stays empty
(lip-sync needs a character subject). The /video:from-song sign-off
gate (its Step 6) shows this map for approval before any render.
Write <project>/script.md (and <project>/vocal-map.json when the
track has vocals). Report the delta, the section→scene map, the probe
method (so the operator sees whether cuts are silence-derived,
energy-derived, or interval-fallback), and whether lyric timing is
transcript-derived (it must be — never brief-derived). If the sum
cannot be reconciled (e.g. provider max-duration forces more time than
the song has), halt and surface the conflict — do not pad silently.
Concrete checks (all must pass before the script is handed to
scene-expander):
Σ(duration) == probe.duration within ±1.0 s; report the
exact delta. A larger delta → halt, do not pad.--scene-durations value) — no invented cut points.duration: exceeds the model's max_duration
or falls below min_duration (model-capabilities manifest, or the
provider tuning for single-model adapters).## Scene N carries all five keys (duration ·
mood · action · camera · dialogue), and that dialogue: is
empty in style mode.script.md opens with the derivation header —
# <project> — derived from <song-file> (<mode> mode · cuts: <method>)
— so the probe method stays visible downstream.## Scene N block per cut carrying exactly the keys
duration · mood · action · camera · dialogue —
scene-expander consumes this verbatim; keep the keys exact.dialogue: stays empty unless operator-supplied lyrics cover the
section — detected vocal energy alone never fills it.# <project> — derived from <song-file> (<mode> mode · cuts: <method>)
## Scene 1
duration: 6.0
mood: establishing, cold, pre-storm
action: <subject from character.json, OR style description in style mode>
camera: slow push-in
dialogue:
## Scene 2
duration: 4.5
mood: build, rising tension
action: close on <subject / detail>, wind picking up
camera: handheld tighten
dialogue:
- "<subject>: \"<lyric line for this section, if any>\""
method: interval is the brick-walled-master signal, not a bug.
A compressed modern master has near-constant RMS and no silence, so the
probe degrades to fixed intervals. That is the honest floor — surface
it and point the operator at --scene-durations; never dress interval
cuts up as beat-synced.dialogue: — only
operator-supplied lyrics do.--scene-durations value — never to taste.interval-fallback cuts as beat-synced. Always
surface the probe method.character.json only when a lock exists./video:from-song — the command
that drives this skillscene-expander — consumes the emitted
scriptcharacter-consistency — supplies
the locked subject referenced in action: (character mode only)