fix: double-audio bug + lint rules + docs guide + capture improvements

Double-audio bug fix:
- scaffolding.ts: stop writing index.html in captures/ (root cause —
  runtime discovered scaffold + real index.html as two compositions)
- New lint rule: multiple_root_compositions — errors if >1 root HTML
- New lint rule: duplicate_audio_track — warns on overlapping audio

Capture improvements (from testing 30+ websites):
- Catalog runs BEFORE extractHtml (which mutates DOM — converts img src
  to data URLs). HeyKuba: 2 images → 78.
- networkidle2 instead of networkidle0 (unblocks SPAs with WebSockets)
- Lazy-load image wait, CSS background-image cataloging
- SVG naming from class/id/parent (not just aria-label)
- Gemini batch 5→20, pause 12s→2s, maxOutputTokens 300→500
- Asset descriptions sorted: captioned first

Docs:
- New guide: guides/website-to-video.mdx (full tutorial)
- CLI docs: added capture and snapshot commands
- docs.json: website-to-video in Guides nav

C
This commit is contained in:
ularkim
2026-04-16 22:58:50 -04:00
committed by ukimsanov
parent 3256551a5e
commit a77a6cbbf7
5 changed files with 362 additions and 49 deletions
+6 -44
View File
@@ -61,51 +61,13 @@ export async function generateProjectScaffold(
progress: (stage: string, detail?: string) => void,
warnings: string[],
): Promise<void> {
// Ensure capture output is a valid HyperFrames project (index.html + meta.json)
const indexPath = join(outputDir, "index.html");
// Capture output is a DATA folder, not a video project.
// The agent builds index.html + compositions/ during step 6.
// We only write meta.json (project metadata) — NOT index.html.
// Writing index.html here caused a double-audio bug: the runtime
// discovered both the scaffold and the agent's real index.html as
// valid compositions, playing two audio tracks offset in time.
const metaPath = join(outputDir, "meta.json");
if (!existsSync(indexPath)) {
writeFileSync(
indexPath,
`<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { margin: 0; width: 1920px; height: 1080px; overflow: hidden; background: #000; }
</style>
</head>
<body>
<!-- Root composition wrapper — AGENT: update data-duration to match total video length -->
<div data-composition-id="main" data-width="1920" data-height="1080" data-start="0" data-duration="28">
<!-- SCENE SLOTS — AGENT: adjust count, durations, and IDs to match your scene plan -->
<div id="scene-1" data-composition-src="compositions/scene-1.html" data-start="0" data-duration="7" data-track-index="1" data-width="1920" data-height="1080"></div>
<div id="scene-2" data-composition-src="compositions/scene-2.html" data-start="7" data-duration="7" data-track-index="1" data-width="1920" data-height="1080"></div>
<div id="scene-3" data-composition-src="compositions/scene-3.html" data-start="14" data-duration="7" data-track-index="1" data-width="1920" data-height="1080"></div>
<div id="scene-4" data-composition-src="compositions/scene-4.html" data-start="21" data-duration="7" data-track-index="1" data-width="1920" data-height="1080"></div>
<!-- NARRATION — AGENT: update src after generating TTS -->
<audio id="narration" data-start="0" data-duration="28" data-track-index="0" data-volume="1" src="narration.wav"></audio>
<!-- CAPTIONS (optional — only add if user requests captions/subtitles) -->
</div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
window.__timelines["main"] = tl;
</script>
</body>
</html>
`,
"utf-8",
);
}
if (!existsSync(metaPath)) {
const hostname = new URL(url).hostname.replace(/^www\./, "");
writeFileSync(