Docs/Skills and code/skill video-visual-production

.claude/skills/video-visual-production/render/make-plan-format.js

Code109 lines9.0 KBSkill: video-visual-production

What the file says about itself

Regenerates references/plan-format.md from the verified EXAMPLES of the scene files, so the cheat sheet cannot drift from the code. node render/make-plan-format.js

#!/usr/bin/env node
// Regenerates references/plan-format.md from the verified EXAMPLES of the scene files, so the cheat sheet cannot drift from the code.
//   node render/make-plan-format.js
const fs = require('fs');
const path = require('path');
const ex = { ...require('./scenes-data.js').EXAMPLES, ...require('./scenes-more.js').EXAMPLES };
const B = String.fromCharCode(96);
const j = (o) => B + JSON.stringify(o) + B;
const q = (s) => B + s + B;

const intro = `# Plan format: how to storyboard a beat with few words

You do not write the beat sheet. You write a short **plan** (${q('research/the-fall-of-moser-baer/video/plan-B#.json')}), and ${q('compose-beat.js')} turns it into the full beat sheet and ledger: it works out word indexes, the on-screen text list, spoken-as mappings and default sounds. Run: ${q('node .claude/skills/video-visual-production/scripts/compose-beat.js --beat B#')}. It prints errors in plain words (which sentence is not covered, which word it could not find) and writes nothing until the plan is right.

## The file

${B}${B}${B}json
{
  "beat": "B7", "title": "Sunlight and a date",
  "facts": { "id": ["V", "the claim in one line", "source name and date", "https://url"] },
  "assets": [ ],
  "scenes": [
    { "s": 1, "type": "slam", "props": { "lines": [ { "text": "A huge bill, and a date", "cue": "line", "size": 100, "tone": "ink" } ] }, "cues": { "line": "@bill" } },
    { "s": [2, 3], "type": "stat", "props": { "value": "1,700", "label": "crore, the bill" }, "cues": { "stat": "@crore" }, "facts": ["id"], "say": { "1,700": "1,700" } }
  ]
}
${B}${B}${B}

## Fields of a scene

| Field | Meaning |
|---|---|
| ${q('s')} | the sentence number from the brief (S1 is 1), or ${q('[from, to]')} for several. Every sentence of the beat must be covered, in order, once |
| ${q('w')} | optional ${q('["@firstword", "@lastword"]')}: use only part of the sentence(s), so a long sentence can be two scenes (the next scene then also says ${q('s')} and its own ${q('w')}) |
| ${q('type')} | a scene type from the lists below |
| ${q('props')} | what to draw; strings you write appear on screen. Copy the example for the type and change the words |
| ${q('cues')} | name -> the spoken word when that build starts. ${q('"@word"')} = first time the word is said in this scene (case and punctuation ignored); ${q('"@word#2"')} = second time; ${q('"@start"')}, ${q('"@end"')}; ${q('"@+3"')} = 3 words after the start; or a word index. Use the cue names the example uses |
| ${q('facts')} | ids of facts (defined in the plan's ${q('facts')}) that this scene draws. A scene of class D needs at least one |
| ${q('say')} | for a number shown on screen: how the voice says it, when the voice does not say the digits (${q('{"3rd": "teesra"}')}). If the voice does say the digits, no ${q('say')} is needed |
| ${q('recall')} | ${q('{"1983": 3}')}: the number was spoken earlier in the beat at word 3 and is repeated on screen |
| ${q('class')} | E evidence, D data, M diagram, T typography. Defaults: stat/bars/line/unitgrid D, doc/photo2 E, slam/date T, the rest M |
| ${q('sfx')} | leave out for the default sounds (one or two per scene of a data type, thinned to the limits); ${q('"none"')} for silence; ${q('"paper": true')} on a doc adds the slide-in sound |
| ${q('enter')} | ${q('"fade"')} for a short cross-fade (rare); default is a cut |
| ${q('waive_numerals')} | ${q('{"17": "reason"}')}: a spoken number that must NOT be drawn (an unverified fact). Use only with a reason, and report it |
| ${q('black')} | ${q('true')}: a declared black frame (a pause), with type ${q('black')} |
| ${q('text')} | only for scene types with no automatic text list (the error says so) |

## Facts

${q('"facts": { "id": [status, claim, source, url] }')}. Status ${q('V')} (checked on a page), ${q('V-1source')}, ${q('derived')}, ${q('analysis')} (the narrator's reasoning). Never ${q('CHECK')} or ${q('V-search')}: the lint refuses them. Copy claim, source and url from the script's fact ledger (${q('research/the-fall-of-moser-baer/script/SCRIPT-v7.md')} section 5); the brief lists candidate rows.

## Assets (only for doc and photo2 scenes)

- Article passage as a picture of the page: ${q('{ "id": "bt2011", "kind": "capture", "fallback": true, "url": "...", "outlet": "Business Today", "published": "17 Apr 2011", "crops": [ { "id": "bt2011-a", "anchor": ["first words of the passage", "last words of the passage"], "highlights": ["phrase the voice quotes"], "fallback_text": "the passage, verbatim" } ] }')}. The words must exist on the page: check with ${q('curl -s -A "Mozilla/5.0" URL')} and search. Then ${q('props: { "asset": "bt2011-a", "tag": "Source: Business Today, 17 Apr 2011", "highlights": [ { "phrase": "...", "cue": "p1" } ] }')} and a cue for each highlight. The crop shows whole lines, so check the picture for unspoken figures.
- A short quote typeset on purpose: ${q('{ "id": "q1", "kind": "quote", "url": "...", "outlet": "...", "published": "...", "text": "... fragment ...", "highlights": ["fragment"] }')}; only wording you have read on the page or that a [V] ledger row quotes.
- A photo: Wikimedia Commons, free licence only: ${q('{ "id": "photo1", "kind": "commons", "title": "File:Name.jpg", "width": 1500, "use": "what it shows" }')}; find files with ${q('node research/video-production/probes/probe-commons.js "search words"')}. Use scene ${q('photo2')} with credit lines; if the photo shows a different thing from the one the voice names, the caption says so.
- After composing run ${q('node .claude/skills/video-visual-production/scripts/assets.js --sheet research/the-fall-of-moser-baer/video/beat-sheet-B#.json')} and read its output.

## Which scene for which sentence

| The sentence | Scene |
|---|---|
| a single plain statement, or nothing honest to draw | ${q('slam')} (never more than 3 in a row, never more than 40% of the beat) |
| one big figure | ${q('stat')} (status ${q('dotted')} if single-source or derived, ${q('ghost')} if searched for and not found) |
| two or more figures compared in the same unit | ${q('bars')} (a negative value hangs below zero; ${q('ghost: true')} bar for "no record found") |
| a figure that moves over time | ${q('line')} (one or two series) |
| a date | ${q('date')} (a year) or ${q('calendar')} (a day circled) |
| a number of days | ${q('deadline')} |
| a published passage the voice quotes or paraphrases | ${q('doc')} (capture or quote asset) |
| a photograph from Commons | ${q('photo2')} |
| a list of reasons or items, one of them missing | ${q('chips')} (${q('ghost: true')} for the missing one) |
| several problems counted aloud | ${q('cards')} |
| two things beside each other with prices | ${q('objects')} |
| a count too big to show | ${q('unitgrid')} |
| a repeated result | ${q('stamps')} |
| a late start, a race | ${q('race')} |
| a price war with no winner | ${q('stalls')} |
| running to stay in place | ${q('treadmill')}; a stop button that is not pressed: ${q('stopbtn')} |
| a commitment that keeps growing | ${q('queue')} |
| a stack of layers | ${q('layers')} |
| a choice between two paths | ${q('paths')} |
| the pivot count (first used in B0 and B1) | ${q('rungs')} |

## Examples by type (props and cue names that draw correctly; in a plan replace the cue values with ${q('"@word"')})

`;

const rows = Object.entries(ex).map(([k, v]) => `- ${q(k)}${v.type ? ' (scene type ' + q(v.type) + ')' : ''}: props ${j(v.props)}; cue names: ${Object.keys(v.cues).join(', ')}`).join('\n');

const outro = `

${q('photo2')} (no self-test image): props ${j({ asset: 'photo1', caption: 'what the photo is', caption2: 'a note if it is not the thing itself', credit1: 'Photo: Author Name', credit2: 'CC BY-SA 4.0, Wikimedia Commons', notes: [{ text: 'a note', cue: 'n1' }] })}; cue names: photo, n1.
${q('doc')}: props ${j({ asset: 'bt2011-a', tag: 'Source: Business Today, 17 Apr 2011', highlights: [{ phrase: 'phrase one', cue: 'p1' }], focus: 'phrase one' })}; cue names: the highlight cues.
${q('slam')}: props ${j({ lines: [{ text: 'two to seven words', cue: 'line', size: 100, tone: 'ink' }] })} (tone: ink, muted, accent, good; up to two lines); cue names: the line cues.
${q('date')}: props ${j({ year: '2012', chapter: 'OPTIONAL SMALL LABEL' })}; cue name: year.

## Rules that the checks enforce (so the plan should respect them)

- Text stays on screen at least 1.2 s, or 0.35 s per word if longer: do not put a long text on the last short sentence of a scene. Cue it on an early word.
- Every number the voice says is shown in that scene; every number shown is said (or recalled).
- A source line (${q('source')} or ${q('tag')}) on every evidence or data scene.
- Never draw a fact whose status is CHECK or V-search. Never invent a number, a name, a face, a logo or a picture of a real place.
- Reuse the same look for the same idea (the ladder is ${q('rungs')}).
- Keep a scene 3 to 8 seconds; split a long sentence with ${q('w')}; merge two very short ones with ${q('s: [a, b]')}.
`;
const out = path.join(__dirname, '..', 'references', 'plan-format.md');
fs.writeFileSync(out, intro + rows + outro);
console.log('written', out, (intro + rows + outro).length, 'chars');

Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.