Docs/Skills and code/skill video-visual-production
.claude/skills/video-visual-production/scripts/words-from-alignment.js
What the file says about itself
Turn the output of the `script-audio-align` skill into a words file this pipeline reads (beat, i, text, start, end, source, flags).
#!/usr/bin/env node
// Turn the output of the `script-audio-align` skill into a words file this pipeline reads (beat, i, text, start, end, source, flags).
// node words-from-alignment.js --beat B0 --alignment <alignment.json> --reference <reference-words.json> --out <words-B0.json>
// The reference file is the list of words that was sent to the voice (video/timing/reference-words.json): it carries the flags
// (stressed, number, question, tags) that the aligner does not know. Times are local to the beat (offset 0).
const fs = require('fs');
const U = require('./lib/util');
const beat = U.arg('beat', null), alignment = U.arg('alignment', null), reference = U.arg('reference', null), out = U.arg('out', null);
if (!beat || !alignment || !reference || !out) U.die('usage: words-from-alignment.js --beat B0 --alignment <alignment.json> --reference <reference-words.json> --out <words-B0.json>');
const ref = U.readJSON(U.fromRoot(reference)).beats.find((b) => b.beat === beat);
if (!ref) U.die('reference file has no beat ' + beat);
const al = U.readJSON(alignment);
if (al.words.length !== ref.words.length) U.die(`the aligner saw ${al.words.length} words but the reference has ${ref.words.length}: align the same text that was sent to the voice (build script.txt from the reference words)`);
const words = ref.words.map((r, i) => {
const a = al.words[i];
if (a.text !== r.text) U.die(`word ${i} differs: aligner "${a.text}" vs reference "${r.text}"`);
return { beat, i: r.i, text: r.text, start: a.start, end: a.end, source: a.source === 'matched' ? 'whisper' : 'interpolated', stressed: r.stressed, number: r.number, question: r.question, tags_before: r.tags_before, para_end: r.para_end };
});
for (let i = 1; i < words.length; i++) { // keep order and a minimum duration
if (words[i].start < words[i - 1].end) words[i].start = words[i - 1].end;
if (words[i].end < words[i].start + 0.03) words[i].end = +(words[i].start + 0.03).toFixed(3);
}
const measured = words.filter((w) => w.source === 'whisper').length;
U.writeJSON(U.fromRoot(out), { note: `${beat} aligned from a Whisper transcript (script-audio-align); times are local to the beat. whisper = measured, interpolated = estimated.`, total_s: words[words.length - 1].end, words });
U.say(`${words.length} words, ${measured} measured (${((measured / words.length) * 100).toFixed(1)}%), ${words.length - measured} interpolated -> ${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.