Docs/Skills and code/skill video-script-production
.claude/skills/video-script-production/scripts/calibrate.js
What the file says about itself
Measure a folder of winner transcripts with the same ruler the linter uses, so thresholds come from data and not from the author's taste.
Mentioned on: page 07, page 09, page 10, page 12
#!/usr/bin/env node
// Measure a folder of winner transcripts with the same ruler the linter uses, so thresholds come from data and not from the author's taste.
// node calibrate.js --dir <folder> [--dir <folder2> ...] [--script <SCRIPT-vN.md> ...] [--lang English] [--out calibration.md]
// Input transcripts are the repo's layout (a "=== TRANSCRIPT ===" marker, optional "Language: English" line). Each is cut into pseudo-beats and
// linted with no chain, so only the text-level measures are read: quantities, voice (F), rhythm windows (H), comparison markers (G4).
// Persona measures (first-person admissions, "you", names, quotes) are computed here. Add --script to see where a script sits among the winners.
// This is how the rhythm thresholds in rules.json were set (8 English winners; see references/texture.md). Re-run it when new winners arrive.
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const dirs = [], scripts = []; const args = {};
for (let i = 2; i < process.argv.length; i++) {
const a = process.argv[i]; if (!a.startsWith('--')) continue;
const k = a.slice(2), v = process.argv[i + 1];
if (k === 'dir') { dirs.push(v); i++; } else if (k === 'script') { scripts.push(v); i++; } else { args[k] = v; i++; }
}
if (!dirs.length && !scripts.length) { console.error('usage: node calibrate.js --dir <folder> [--dir ...] [--script SCRIPT.md] [--lang English] [--out calibration.md]'); process.exit(2); }
const lang = args.lang || 'English';
const lint = path.join(__dirname, 'lint.js');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'calibrate-'));
const P = {
admit: /\b(I (don'?t|do not) know|I'?m not sure|I could be wrong|I can'?t (say|tell|find)|I couldn'?t find|I haven'?t found|I think|I'd argue|honestly|to be fair|we don'?t know|nobody knows|no one knows)\b/gi,
you: /\b(you|your|you're|you'd|you'll|you've)\b/gi,
quote: /\b(said|told|wrote|says|argued|admitted|recalled|put it)\b/gi,
names: /(?<![.?!]\s)(?<=[a-z,]\s)[A-Z][a-z]+ [A-Z][a-z]+/g,
};
const spokenOf = (file) => fs.readFileSync(file, 'utf8').split(/\r?\n/).filter((l) => /^\*\*VO:\*\*/.test(l)).map((l) => l.replace(/^\*\*VO:\*\*\s*/, '').replace(/\*+/g, '')).join(' ');
const transcriptOf = (file) => { const s = fs.readFileSync(file, 'utf8'); return s.slice(s.indexOf('=== TRANSCRIPT ===') + 18).replace(/\[\d+:\d+(:\d+)?\]/g, ' ').replace(/\s+/g, ' ').trim(); };
function measure(label, textRaw, pseudoScript) {
const words = textRaw.split(/\s+/).filter(Boolean);
const sp = path.join(tmp, label.replace(/\W+/g, '_') + '.md');
if (pseudoScript) { let md = ''; for (let i = 0, b = 0; i < words.length; i += 240, b++) md += `### [B${b}] x\n**VO:** ${words.slice(i, i + 240).join(' ')}\n`; fs.writeFileSync(sp, md); }
const r = spawnSync(process.execPath, [lint, '--script', pseudoScript ? sp : textRaw, '--label', label.replace(/\W+/g, '_'), '--out', tmp], { encoding: 'utf8' });
let j; try { j = JSON.parse(fs.readFileSync(path.join(tmp, `lint-${label.replace(/\W+/g, '_')}.json`), 'utf8')).summary; } catch { return null; }
const n = words.length, c = (re) => (textRaw.match(re) || []).length;
return {
label, min: j.minutes, q_per_min: j.quantities_per_min, contractions: j.voice.contractions_per_100, sentence_cv: j.voice.sentence_cv,
neg_pivots: j.voice.negation_pivots, run_ups: j.voice.run_ups,
comparisons_per_1k: +(j.texture.figure_markers / n * 1000).toFixed(2), admissions_per_1k: +(c(P.admit) / n * 1000).toFixed(2),
you_per_100: +(c(P.you) / n * 100).toFixed(2), quotes_per_1k: +(c(P.quote) / n * 1000).toFixed(2), names_per_1k: +(c(P.names) / n * 1000).toFixed(1),
hard_share: j.texture.hard_share, quiet_share: j.texture.quiet_share, longest_hard_run: j.texture.longest_hard_run, swaps_per_10min: j.texture.swaps_per_10min,
};
}
const rows = [], mine = [];
for (const d of dirs) {
for (const f of fs.readdirSync(d).filter((x) => x.endsWith('.txt'))) {
const s = fs.readFileSync(path.join(d, f), 'utf8');
if (!s.includes('=== TRANSCRIPT ===')) continue;
if (lang !== 'any' && !new RegExp(`Language: ${lang}`, 'i').test(s)) continue;
const m = measure(f.slice(0, 18), transcriptOf(path.join(d, f)), true); if (m) rows.push(m);
}
}
for (const s of scripts) { const m = measure(path.basename(s), spokenOf(s), true); if (m) mine.push(m); }
const keys = rows.length ? Object.keys(rows[0]).filter((k) => k !== 'label') : [];
const med = (a) => { const b = [...a].sort((x, y) => x - y); const m = Math.floor(b.length / 2); return b.length % 2 ? b[m] : +((b[m - 1] + b[m]) / 2).toFixed(2); };
const out = [`# Calibration (${rows.length} ${lang} winner transcripts${scripts.length ? `, ${scripts.length} script(s)` : ''})`, '', 'Small samples. These are descriptions of winners, not proof that any measure causes performance.', '',
`| | ${keys.join(' | ')} |`, `|---|${keys.map(() => '---').join('|')}|`,
...rows.map((r) => `| ${r.label} | ${keys.map((k) => r[k]).join(' | ')} |`),
...(rows.length ? [`| **median** | ${keys.map((k) => med(rows.map((r) => r[k]))).join(' | ')} |`, `| **min** | ${keys.map((k) => Math.min(...rows.map((r) => r[k]))).join(' | ')} |`, `| **max** | ${keys.map((k) => Math.max(...rows.map((r) => r[k]))).join(' | ')} |`] : []),
...mine.map((r) => `| **${r.label}** | ${keys.length ? keys.map((k) => r[k]).join(' | ') : Object.keys(r).filter((k) => k !== 'label').map((k) => r[k]).join(' | ')} |`)].join('\n');
console.log(out);
if (args.out) { fs.writeFileSync(args.out, out + '\n', 'utf8'); console.log(`\nwrote ${args.out}`); }
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.