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

.claude/skills/video-visual-production/scripts/new-beat.js

Code50 lines3.6 KBSkill: video-visual-production

What the file says about itself

Scaffold a beat sheet for a new beat: one plain typographic scene per sentence, every cue on a real word, ledger stub. The result builds and lints (with warnings); the agent then upgrades scenes by following references/scene-catalogue.md.

#!/usr/bin/env node
// Scaffold a beat sheet for a new beat: one plain typographic scene per sentence, every cue on a real word, ledger stub.
// The result builds and lints (with warnings); the agent then upgrades scenes by following references/scene-catalogue.md.
//   node new-beat.js --beat B4 --title "The treadmill" --slug moser-baer-v7-hinglish --audio <beat.wav> --words <words.json> [--local] --dir research/<slug>/video
const fs = require('fs');
const path = require('path');
const U = require('./lib/util');

const beat = U.arg('beat', null), title = U.arg('title', ''), slug = U.arg('slug', null), audio = U.arg('audio', null), wordsFile = U.arg('words', null), dir = U.arg('dir', null);
if (!beat || !slug || !audio || !wordsFile || !dir) U.die('usage: new-beat.js --beat B4 --title "..." --slug <slug> --audio <wav, repo-relative> --words <words.json, repo-relative> --dir research/<topic>/video');
const allWords = U.readJSON(U.fromRoot(wordsFile)).words;
const words0 = allWords.filter((w) => w.beat === beat);
// words.json lays the beats end to end on one timeline; this beat starts where its first word starts
const offset = U.flag('local') ? 0 : (words0.length ? Math.round(words0[0].start * 100) / 100 : 0); // --local: a words-<beat>.json file, already local to the beat
const words = words0.map((w) => ({ ...w, start: w.start - offset, end: w.end - offset }));
if (!words.length) U.die(`words file has no words for ${beat}: align the beat first (references/runbook.md, step 1)`);

const sentences = [];
let cur = [];
words.forEach((w, i) => { cur.push(i); if (/[.?!]["')]*$/.test(w.text)) { sentences.push(cur); cur = []; } });
if (cur.length) sentences.push(cur);

const clean = (t) => t.replace(/[.,;:?!"']+$/g, '');
const scenes = sentences.map((idx, n) => {
  const num = idx.find((i) => words[i].number);
  const id = `${beat}.${String(n + 1).padStart(2, '0')}`;
  const base = { id, span: [idx[0], idx[idx.length - 1]], type: 'slam', class: 'T', enter: { type: 'cut' } };
  if (num != null) {
    const v = clean(words[num].text);
    return { ...base, props: { lines: [{ text: v, cue: 'line', size: 200, tone: 'accent' }] }, cues: { line: num }, text: [{ value: v, cue: 'line', role: 'number', number: true }], ledger: [], sfx: [] };
  }
  const first = idx.slice(0, 5).map((i) => clean(words[i].text)).join(' ');
  return { ...base, props: { lines: [{ text: first, cue: 'line', size: 80, tone: 'ink' }] }, cues: { line: idx[0] }, text: [{ value: first, cue: 'line', role: 'keyword', spoken_as: first }], ledger: [], sfx: [] };
});

const sheet = {
  version: 1, slug, beat, title, audio, words: wordsFile, ledger: path.join(dir, `ledger-${beat}.json`).split(path.sep).join('/'),
  time_offset_s: offset, fps: 30, lead_in_s: 0.5, tail_s: 0.9, lead_frames: 2,
  bed: { enabled: true, level_db: -24, label: 'placeholder synthesised drone, not music; replace with a library bed the user auditions' },
  assets: [], scenes,
};
const outDir = U.fromRoot(dir);
fs.mkdirSync(outDir, { recursive: true });
const sheetPath = path.join(outDir, `beat-sheet-${beat}.json`);
const ledgerPath = path.join(outDir, `ledger-${beat}.json`);
if (fs.existsSync(sheetPath)) U.die('refusing to overwrite ' + sheetPath);
fs.writeFileSync(sheetPath, JSON.stringify(sheet, null, 2) + '\n');
if (!fs.existsSync(ledgerPath)) U.writeJSON(ledgerPath, { note: 'Copy the rows for this beat from the script fact ledger. status: V, V-1source, derived, analysis only.', rows: {} });
U.say(`${sentences.length} scenes scaffolded -> ${sheetPath}\nnext: node .claude/skills/video-visual-production/scripts/beatsheet-lint.js --sheet ${path.relative(U.ROOT, sheetPath)}`);

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.