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

.claude/skills/video-script-production/scripts/retime.js

Code48 lines2.6 KBSkill: video-script-production

What the file says about itself

Keeps the time ranges in the beat headings equal to the word counts (cumulative words / wpm). node retime.js --script <SCRIPT-vN.md> [--write] [--check] [--wpm 150] Without flags it prints what each heading should say.

Mentioned on: page 08, page 09, page 10, page 11, page 12

#!/usr/bin/env node
// Keeps the time ranges in the beat headings equal to the word counts (cumulative words / wpm).
//   node retime.js --script <SCRIPT-vN.md> [--write] [--check] [--wpm 150]
// Without flags it prints what each heading should say. --write rewrites the headings in place. --check exits 1 if any heading is stale.
// Heading form: "### [B3] Title (4:16 to 5:37)". A heading with no time range gets one when --write is used.

const fs = require('fs');

const args = {};
for (const a of process.argv.slice(2)) { if (a.startsWith('--')) { const k = a.slice(2); args[k] = true; } else { const last = Object.keys(args).pop(); if (last) args[last] = a; } }
if (!args.script || args.script === true) { console.error('usage: node retime.js --script <SCRIPT-vN.md> [--write] [--check] [--wpm 150]'); process.exit(2); }
const WPM = Number(args.wpm && args.wpm !== true ? args.wpm : 150);

const clean = (s) => s
  .replace(/\*\*\[[^\]]*\]\*\*/g, ' ').replace(/\[(?:V|CHECK)[^\]]*\]/gi, ' ')
  .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1').replace(/\*+/g, '').replace(/\s+/g, ' ').trim();
const fmt = (sec) => { const t = Math.round(sec); return `${Math.floor(t / 60)}:${String(t % 60).padStart(2, '0')}`; }; // round first: 59.6 s is 1:00, not 0:60
const wc = (s) => (s.match(/\S+/g) || []).length;

const raw = fs.readFileSync(args.script, 'utf8');
const eol = raw.includes('\r\n') ? '\r\n' : '\n';
const lines = raw.split(/\r?\n/);
// pass 1: words per beat
const beatWords = {}; let cur = null;
for (const l of lines) {
  const h = l.match(/^###\s*\[(B\d+)\]/);
  if (h) { cur = h[1]; beatWords[cur] = beatWords[cur] || 0; continue; }
  if (/^#{1,3}\s/.test(l)) { cur = null; continue; }
  const m = l.match(/^\*\*VO:\*\*\s*(.*)$/);
  if (m && cur) beatWords[cur] += wc(clean(m[1]));
}
// pass 2: expected ranges
let acc = 0; const want = {};
for (const [id, w] of Object.entries(beatWords)) { if (!w) continue; want[id] = `${fmt((acc / WPM) * 60)} to ${fmt(((acc + w) / WPM) * 60)}`; acc += w; }

let stale = 0;
const out = lines.map((l) => {
  const h = l.match(/^(###\s*\[(B\d+)\]\s*)(.*?)(?:\s*\((\d+:\d+ to \d+:\d+)\))?\s*$/);
  if (!h || !want[h[2]]) return l;
  const fresh = `${h[1]}${h[3]} (${want[h[2]]})`;
  if (h[4] !== want[h[2]]) { stale++; console.log(`${h[2]}: ${h[4] || '(none)'} -> ${want[h[2]]}`); }
  return fresh;
});
if (args.write) { fs.writeFileSync(args.script, out.join(eol), 'utf8'); console.log(stale ? `rewrote ${stale} heading(s)` : 'headings already current'); }
else console.log(stale ? `${stale} heading(s) stale` : 'headings current');
console.log(`total ${(acc / WPM).toFixed(1)} min (${acc} words at ${WPM} wpm)`);
if (args.check && stale) process.exit(1);

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.