diff --git a/skills/faceless-explainer/scripts/assemble-index.mjs b/skills/faceless-explainer/scripts/assemble-index.mjs index 88dc7a9965..e48907066a 100644 --- a/skills/faceless-explainer/scripts/assemble-index.mjs +++ b/skills/faceless-explainer/scripts/assemble-index.mjs @@ -171,7 +171,17 @@ for (const visual of visualClips) { const start = Number(visual.start_s); if (!isFinite(start) || start < 0) die(`${visual.id}: visual start_s invalid (${visual.start_s})`); - const rootDur = rootDataDuration(readFileSync(compAbs, "utf8")); + // Guard against blank/partial scene files: a worker that errors or is + // interrupted mid-write leaves an empty (or markup-less) file that exists but + // fails at render with "Composition HTML is empty or could not be parsed". + // Catch it here — before emitting data-composition-src — and re-dispatch. + const compHtml = readFileSync(compAbs, "utf8"); + if (!compHtml.trim() || !/<\w/.test(compHtml)) { + die( + `visual file ${compRel} is empty or has no HTML — the worker for ${visual.id} wrote a blank/partial file. Re-dispatch that scene worker before assembling.`, + ); + } + const rootDur = rootDataDuration(compHtml); if (rootDur == null) { anomalies.push( `${visual.id}: could not read root data-duration from ${compRel} — skipped duration cross-check`, diff --git a/skills/pr-to-video/scripts/assemble-index.mjs b/skills/pr-to-video/scripts/assemble-index.mjs index 4368197ae5..01599835ca 100644 --- a/skills/pr-to-video/scripts/assemble-index.mjs +++ b/skills/pr-to-video/scripts/assemble-index.mjs @@ -171,7 +171,17 @@ for (const visual of visualClips) { const start = Number(visual.start_s); if (!isFinite(start) || start < 0) die(`${visual.id}: visual start_s invalid (${visual.start_s})`); - const rootDur = rootDataDuration(readFileSync(compAbs, "utf8")); + // Guard against blank/partial scene files: a worker that errors or is + // interrupted mid-write leaves an empty (or markup-less) file that exists but + // fails at render with "Composition HTML is empty or could not be parsed". + // Catch it here — before emitting data-composition-src — and re-dispatch. + const compHtml = readFileSync(compAbs, "utf8"); + if (!compHtml.trim() || !/<\w/.test(compHtml)) { + die( + `visual file ${compRel} is empty or has no HTML — the worker for ${visual.id} wrote a blank/partial file. Re-dispatch that scene worker before assembling.`, + ); + } + const rootDur = rootDataDuration(compHtml); if (rootDur == null) { anomalies.push( `${visual.id}: could not read root data-duration from ${compRel} — skipped duration cross-check`, diff --git a/skills/product-launch-video/scripts/assemble-index.mjs b/skills/product-launch-video/scripts/assemble-index.mjs index c86e5d5db5..2187d6a3fb 100644 --- a/skills/product-launch-video/scripts/assemble-index.mjs +++ b/skills/product-launch-video/scripts/assemble-index.mjs @@ -125,7 +125,17 @@ for (const { sid, scene } of playOrder) { `${sid}: group_spec estimatedDuration_s missing or non-positive (${scene.estimatedDuration_s})`, ); } - const rootDur = rootDataDuration(readFileSync(compAbs, "utf8")); + // Guard against blank/partial scene files: a worker that errors or is + // interrupted mid-write leaves an empty (or markup-less) file that exists but + // fails at render with "Composition HTML is empty or could not be parsed". + // Catch it here — before emitting data-composition-src — and re-dispatch. + const compHtml = readFileSync(compAbs, "utf8"); + if (!compHtml.trim() || !/<\w/.test(compHtml)) { + die( + `scene file ${compRel} is empty or has no HTML — the worker for ${sid} wrote a blank/partial file. Re-dispatch that scene worker before assembling.`, + ); + } + const rootDur = rootDataDuration(compHtml); if (rootDur == null) { anomalies.push( `${sid}: could not read root data-duration from ${compRel} — skipped duration cross-check`,