Docs/Skills and code/skill video-visual-production
.claude/skills/video-visual-production/scripts/captions.js
What the file says about itself
V9 Captions (sidecar only): SRT from the aligned script words, 3 to 6 words a cue, breaking at punctuation and pauses. Not burned in: the Roman-Hinglish versus Devanagari caption decision is still open…
#!/usr/bin/env node
// V9 Captions (sidecar only): SRT from the aligned script words, 3 to 6 words a cue, breaking at punctuation and pauses.
// Not burned in: the Roman-Hinglish versus Devanagari caption decision is still open (research/video-production/README.md).
// node captions.js --sheet <beat-sheet.json> [--out <file.srt>]
const fs = require('fs');
const path = require('path');
const U = require('./lib/util');
const ts = (s) => { const ms = Math.round(s * 1000); const p = (n, w = 2) => String(n).padStart(w, '0'); return `${p(Math.floor(ms / 3600000))}:${p(Math.floor(ms / 60000) % 60)}:${p(Math.floor(ms / 1000) % 60)},${p(ms % 1000, 3)}`; };
function build(words, lead) {
const cues = [];
let cur = [];
const flush = () => { if (cur.length) { cues.push({ s: cur[0].start + lead, e: cur[cur.length - 1].end + lead, text: cur.map((w) => w.text).join(' ') }); cur = []; } };
words.forEach((w, i) => {
cur.push(w);
const next = words[i + 1];
const brk = /[.?!]$/.test(w.text) || (/[,:;]$/.test(w.text) && cur.length >= 3) || cur.length >= 6 || (next && next.start - w.end > 0.45);
if (brk) flush();
});
flush();
// keep each cue on screen at least 0.8 s without overlapping the next
cues.forEach((c, i) => { const nxt = cues[i + 1]; c.e = Math.max(c.e, Math.min(c.s + 0.8, nxt ? nxt.s - 0.02 : c.e + 1)); });
return cues;
}
if (require.main === module) {
const sheetPath = U.fromRoot(U.arg('sheet', ''));
const sheet = U.readJSON(sheetPath);
const words = U.loadWords(sheet).filter((w) => w.beat === sheet.beat);
const cues = build(words, sheet.lead_in_s);
const out = U.arg('out', path.join(U.buildDir(sheet.slug, sheet.beat), `${sheet.beat}.srt`));
fs.writeFileSync(out, cues.map((c, i) => `${i + 1}\n${ts(c.s)} --> ${ts(c.e)}\n${c.text}\n`).join('\n'));
U.say(`captions: ${cues.length} cues -> ${out}`);
}
module.exports = { build };
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.