A localized video handoff can contain every expected audio file and still be inconsistent. One take may use an old script, two worksheet rows may point to the same filename, or one language may be missing a demonstration segment. These are metadata problems a small preflight check can catch before an editor begins arranging clips.
This Node.js example audits a local JSON handoff. It checks declared locale/shot coverage, script-version agreement, unique MP3 basenames, and declared duration against a shot window. It does not synthesize speech, decode audio, inspect pronunciation, or verify that files actually exist.
1. Define the handoff contract before writing the check
Use one row per locale and shot. The example has two locales, en-US and es-MX, and two shots, S01 and S02. That means four take rows are required. If a locale intentionally omits narration, change the contract explicitly rather than inserting a silent placeholder and calling it complete.
This implementation accepts a deliberately narrow language-REGION label, such as en-US. It is not a validator for every BCP 47 language tag. Shot IDs begin with an uppercase letter and use uppercase letters, digits, or underscores. Durations and version numbers are positive safe integers; time is stored in whole milliseconds.
The approved script version comes from the reviewed localization worksheet. The rendered script version records which script was used for that take. Matching numbers are a consistency check on the record, not proof of what someone actually said in the MP3.
2. Audit coverage, versions, and filenames
Save the following module as handoff.mjs. Sets detect duplicate locales, duplicate locale/shot pairs, and reused filenames. A Map stores each shot’s declared window. An incomplete matrix or inconsistent version fails immediately with a message identifying the problem.
function requireValue(ok, message) {
if (!ok) throw new Error(message);
}
const positiveInt = n => Number.isSafeInteger(n) && n > 0;
export function auditHandoff(data) {
requireValue(data && typeof data === "object", "Expected an object");
requireValue(Array.isArray(data.locales) && data.locales.length > 0,
"locales must be a non-empty array");
const locales = new Set();
for (const locale of data.locales) {
requireValue(typeof locale === "string" && /^[a-z]{2,3}-[A-Z]{2}$/.test(locale),
"Use a language-REGION locale such as en-US");
requireValue(!locales.has(locale), "Duplicate locale: " + locale);
locales.add(locale);
}
requireValue(Array.isArray(data.shots) && data.shots.length > 0,
"shots must be a non-empty array");
const shots = new Map();
for (const shot of data.shots) {
requireValue(shot && typeof shot.id === "string" &&
/^[A-Z][A-Z0-9_]{0,15}$/.test(shot.id), "Invalid shot ID");
requireValue(!shots.has(shot.id), "Duplicate shot: " + shot.id);
requireValue(positiveInt(shot.windowMs), "Invalid windowMs: " + shot.id);
shots.set(shot.id, shot.windowMs);
}
requireValue(Array.isArray(data.takes), "takes must be an array");
const seen = new Set();
const files = new Set();
const warnings = [];
for (const take of data.takes) {
requireValue(take && locales.has(take.locale) && shots.has(take.shotId),
"Take refers to an unknown locale or shot");
const key = take.locale + "/" + take.shotId;
requireValue(!seen.has(key), "Duplicate take: " + key);
seen.add(key);
requireValue(positiveInt(take.approvedScriptVersion) &&
positiveInt(take.renderedScriptVersion) &&
take.approvedScriptVersion === take.renderedScriptVersion,
"Stale or invalid script version: " + key);
requireValue(positiveInt(take.durationMs), "Invalid durationMs: " + key);
requireValue(typeof take.file === "string" &&
/^[A-Za-z0-9_][A-Za-z0-9_.-]*\.mp3$/i.test(take.file),
"Use a plain MP3 basename: " + key);
const fileKey = take.file.toLowerCase();
requireValue(!files.has(fileKey), "Reused filename: " + take.file);
files.add(fileKey);
const overrunMs = take.durationMs - shots.get(take.shotId);
if (overrunMs > 0) warnings.push({ key, overrunMs });
}
for (const locale of locales) {
for (const shotId of shots.keys()) {
requireValue(seen.has(locale + "/" + shotId),
"Missing take: " + locale + "/" + shotId);
}
}
return { takeCount: seen.size, expectedCount: locales.size * shots.size,
warnings };
}
Filename comparisons are case-insensitive by project convention. That catches two records that differ only by letter case, which is easy to miss during a handoff. The basename rule excludes directory separators. It describes this manifest format; it is not a filesystem sandbox or an audio-format inspection.
Timing is reported differently from a structural error. A complete take can be longer than its shot window and still be a legitimate candidate for editorial revision. The function returns the overrun as a warning instead of silently trimming the take or rejecting the entire locale.
3. Use a fixture that exercises a real mismatch
Save this synthetic example as handoff.json. All four combinations are present and their approved/rendered versions agree. The last take declares 10,200 milliseconds for a 9,000-millisecond shot, so its overrun is 1,200 milliseconds. These are invented durations; no real audio was measured for this fixture.
{
"locales": [
"en-US",
"es-MX"
],
"shots": [
{
"id": "S01",
"windowMs": 4000
},
{
"id": "S02",
"windowMs": 9000
}
],
"takes": [
{
"locale": "en-US",
"shotId": "S01",
"approvedScriptVersion": 2,
"renderedScriptVersion": 2,
"durationMs": 3400,
"file": "lamp_en-US_S01_v2.mp3"
},
{
"locale": "en-US",
"shotId": "S02",
"approvedScriptVersion": 2,
"renderedScriptVersion": 2,
"durationMs": 8200,
"file": "lamp_en-US_S02_v2.mp3"
},
{
"locale": "es-MX",
"shotId": "S01",
"approvedScriptVersion": 3,
"renderedScriptVersion": 3,
"durationMs": 3900,
"file": "lamp_es-MX_S01_v3.mp3"
},
{
"locale": "es-MX",
"shotId": "S02",
"approvedScriptVersion": 3,
"renderedScriptVersion": 3,
"durationMs": 10200,
"file": "lamp_es-MX_S02_v3.mp3"
}
]
}
Versions may differ between locales. English version 2 and Mexican Spanish version 3 are not inherently inconsistent: each localized script has its own revision history. The comparison is between the approved script and the rendered take within the same row.
Keep the visible product spelling and pronunciation instructions in the source worksheet. This manifest is a small technical view of that process, not a substitute for the approved words, speaker direction, or final audio review.
4. Return an exit code your pipeline can distinguish
Save this command-line wrapper as audit.mjs next to the module. It reads the JSON using Node’s promise-based readFile API, prints the successful report, and keeps input errors on standard error. There is no network request and no generated audio cost.
import { readFile } from "node:fs/promises";
import { auditHandoff } from "./handoff.mjs";
try {
if (!process.argv[2]) throw new Error("Usage: node audit.mjs handoff.json");
const data = JSON.parse(await readFile(process.argv[2], "utf8"));
const report = auditHandoff(data);
console.log(JSON.stringify(report, null, 2));
process.exitCode = report.warnings.length ? 2 : 0;
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
node audit.mjs handoff.json
The fixture returns takeCount 4, expectedCount 4, and one warning for es-MX/S02 with overrunMs 1200. Exit code 0 means the declared handoff is complete with no overruns. Exit code 1 means invalid JSON, a missing argument, or a contract failure. Exit code 2 means the records are structurally complete but timing needs a decision.
Treat code 2 as a request for review in your own build or handoff process. Do not automatically speed up, crop, or discard audio based on this report. The checker has no information about spoken meaning or whether an important product action can be extended.
5. Test missing and stale records, not only the happy path
Check the four-row count and the exact 1,200-millisecond warning. Set the last duration to exactly 9,000 milliseconds and verify that no overrun remains. The equality boundary matters: a take that exactly fills the declared window should not be reported as too long.
Rejection fixtures should cover an empty locale list, a duplicate locale, an unsupported locale-label shape, a duplicate shot, a bad shot window, an unknown locale, an unknown shot, a duplicate take, a missing take, a stale rendered version, a zero approved version, a fractional duration, a directory in the filename, and a filename reused with different letter case.
The module shown here was checked locally with Node.js v24.20.0 using four report assertions and fourteen rejection fixtures. The command-line clean-success, timing-warning, and missing-input paths were also checked. These tests validate declared data and exit codes, not the contents, availability, or sound quality of any MP3.
6. Connect the report back to the editing decision
For a missing row, locate the absent locale/shot take before packaging the handoff. For a stale version, confirm which approved script should be rendered and replace the recorded take after that work is complete. Changing a version number in JSON without doing the corresponding audio work only hides the discrepancy.
For the 1.2-second overrun, return to the spoken copy and the shot context. A reviewer might shorten a nonessential phrase, or the editor might give the action more room. After a new take or edit is approved, update the recorded duration and shot window and run the check again.
Build or retime each locale’s captions against its final cut, then package the approved worksheet, the matching files, and this report together. A complete metadata matrix is a useful handoff gate; listening and final-cut checks still determine whether the localized video communicates correctly.
Editorial note: This developer tutorial was adapted with AI-assisted writing from the PixMind article linked below. Its Node.js implementation and synthetic fixtures were checked locally as described. No actual audio was generated or measured, and no illustrative images are used.
Original source: How to Produce Multilingual Voiceovers for the Same Product Video. Technical reference: Node.js file-system documentation.
About the author
- The PixMind team shares practical AI image and video workflows. PixMind brings text-to-image, reference-based creation, editing, and video tools into one workspace. Website: https://www.pixmind.io/
Comments
Join the discussionNo comments yet. Be the first to comment.