Docs/Skills and code/skill video-script-production
.claude/skills/video-script-production/scripts/hinglish-parity.js
What the file says about itself
Parity check between an English script and its Hinglish (or other-language) version. It checks that nothing was lost or invented in the facts and the structure;
Mentioned on: page 10, page 11, page 12
#!/usr/bin/env node
// Parity check between an English script and its Hinglish (or other-language) version. It checks that nothing was lost or invented
// in the facts and the structure; it cannot say whether the Hinglish sounds right (lint.js and the chain are English-anchored).
// node hinglish-parity.js --english SCRIPT-v7.md --hinglish SCRIPT-v7-hinglish.md [--allow Earth,Say] [--max-ratio 1.3]
// Per beat, in order: same beat id and title; same number of **VO:** paragraphs; the same digits in each paragraph (multiset);
// the same number of question marks; every direct quote of four words or more present verbatim; every capitalised name (not at the
// start of a sentence) present; identical graphics lines. Reports the word-count ratio per beat (Hinglish usually runs about 15 percent longer).
// Exit 1 on any mismatch. Same parsing as lint.js: beats are "### [Bn] ..." headings, spoken text is every line starting "**VO:**".
const fs = require('fs');
const args = {};
for (let i = 2; i < process.argv.length; i += 2) args[process.argv[i].replace(/^--/, '')] = process.argv[i + 1];
if (!args.english || !args.hinglish) { console.error('usage: node hinglish-parity.js --english <SCRIPT.md> --hinglish <SCRIPT-hinglish.md> [--allow Earth,Say] [--max-ratio 1.3]'); process.exit(2); }
const allow = new Set(['Earth', 'Say', 'Think', 'Call', 'Start', 'Back', 'Many', 'Almost', 'Even', 'Still', 'Soon', ...(args.allow ? args.allow.split(',') : [])]);
const maxRatio = Number(args['max-ratio'] || 1.3);
const clean = (s) => s.replace(/\*\*\[[^\]]*\]\*\*/g, ' ').replace(/\[(?:V|CHECK)[^\]]*\]/gi, ' ').replace(/\[([^\]]+)\]\([^)]+\)/g, '$1').replace(/\*+/g, '').replace(/\s+/g, ' ').trim();
function parse(file) {
const beats = []; let cur = null;
for (const l of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
const h = l.match(/^###\s*\[(B\d+)\]\s*(.*?)(?:\s*\(\d+:\d+ to \d+:\d+\))?\s*$/);
if (h) { cur = { id: h[1], title: h[2], vo: [], graphics: [] }; beats.push(cur); continue; }
if (/^#{1,3}\s/.test(l)) { cur = null; continue; }
if (!cur) continue;
const m = l.match(/^\*\*VO:\*\*\s*(.*)$/);
if (m && clean(m[1])) cur.vo.push(clean(m[1]));
else if (/^\*\(Graphic|^\*\(End card/.test(l)) cur.graphics.push(l.trim());
}
return beats;
}
const en = parse(args.english), hi = parse(args.hinglish);
const wc = (s) => (s.match(/\S+/g) || []).length;
const digits = (s) => (s.replace(/(\d),(\d)/g, '$1$2').match(/\d+(?:\.\d+)?/g) || []).sort().join(' ');
const quotes = (s) => [...s.matchAll(/"([^"]{12,})"/g)].map((m) => m[1]).filter((q) => wc(q) >= 4);
const names = (s) => {
const out = new Set();
for (const sent of s.split(/(?<=[.?!])["')]*\s+/)) {
const toks = sent.split(/\s+/);
toks.forEach((t, i) => {
if (i === 0) return;
const w = t.replace(/^[("“]+/, '').replace(/[.,;:!?)"”]+$/g, '').replace(/['’]s$/, '');
if (/^[A-Z][a-z]{2,}/.test(w) && !allow.has(w)) out.add(w);
});
}
return [...out];
};
const problems = [], notes = [];
const bad = (m) => problems.push(m);
if (en.length !== hi.length) bad(`beats: English ${en.length}, Hinglish ${hi.length}`);
let enWords = 0, hiWords = 0, hookRatio = null;
en.forEach((e, k) => {
const h = hi[k]; if (!h) return;
if (e.id !== h.id || e.title.toLowerCase() !== h.title.toLowerCase()) bad(`${e.id}: heading differs ("${e.title}" / "${h.title}")`);
if (e.vo.length !== h.vo.length) { bad(`${e.id}: ${e.vo.length} paragraphs in English, ${h.vo.length} in Hinglish`); }
const n = Math.min(e.vo.length, h.vo.length);
for (let i = 0; i < n; i++) {
const tag = `${e.id} P${i + 1}`;
if (digits(e.vo[i]) !== digits(h.vo[i])) bad(`${tag}: digits differ (English: ${digits(e.vo[i]) || 'none'}; Hinglish: ${digits(h.vo[i]) || 'none'})`);
const qe = (e.vo[i].match(/\?/g) || []).length, qh = (h.vo[i].match(/\?/g) || []).length;
if (qe !== qh) bad(`${tag}: ${qe} question mark(s) in English, ${qh} in Hinglish`);
for (const q of quotes(e.vo[i])) if (!h.vo[i].includes(q)) bad(`${tag}: direct quote not verbatim: "${q.slice(0, 50)}..."`);
for (const nm of names(e.vo[i])) if (!h.vo[i].includes(nm)) bad(`${tag}: name missing in Hinglish: ${nm}`);
}
if (e.graphics.join('|') !== h.graphics.join('|')) bad(`${e.id}: graphics lines differ`);
const we = e.vo.reduce((a, s) => a + wc(s), 0), wh = h.vo.reduce((a, s) => a + wc(s), 0);
enWords += we; hiWords += wh;
const r = wh / Math.max(1, we);
if (k === 0) hookRatio = r;
if (r > maxRatio) notes.push(`${e.id}: Hinglish runs ${(r * 100 - 100).toFixed(0)}% longer than English (${we} to ${wh} words)`);
});
console.log(`beats: ${en.length} English, ${hi.length} Hinglish; paragraphs: ${en.reduce((a, b) => a + b.vo.length, 0)} and ${hi.reduce((a, b) => a + b.vo.length, 0)}`);
console.log(`words: ${enWords} English, ${hiWords} Hinglish (${((hiWords / enWords) * 100 - 100).toFixed(0)}% longer); hook ${hookRatio ? ((hookRatio - 1) * 100).toFixed(0) + '% longer' : 'n/a'}`);
for (const n of notes) console.log('note ' + n);
if (problems.length) { console.log(`\n${problems.length} PARITY PROBLEM(S):`); for (const p of problems) console.log(' - ' + p); process.exit(1); }
console.log('\nPARITY OK: same beats, paragraphs, digits, question marks, quotes, names and graphics.');
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.