hyperframes 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -54,7 +54,7 @@ var VERSION;
54
54
  var init_version = __esm({
55
55
  "src/version.ts"() {
56
56
  "use strict";
57
- VERSION = true ? "0.4.2" : "0.0.0-dev";
57
+ VERSION = true ? "0.4.4" : "0.0.0-dev";
58
58
  }
59
59
  });
60
60
 
@@ -6333,7 +6333,9 @@ function lintProject(project) {
6333
6333
  }
6334
6334
  const projectFindings = [
6335
6335
  ...lintProjectAudioFiles(project.dir, allHtmlSources),
6336
- ...lintAudioSrcNotFound(project.dir, allHtmlSources)
6336
+ ...lintAudioSrcNotFound(project.dir, allHtmlSources),
6337
+ ...lintMultipleRootCompositions(project.dir),
6338
+ ...lintDuplicateAudioTracks(allHtmlSources)
6337
6339
  ];
6338
6340
  if (projectFindings.length > 0) {
6339
6341
  for (const finding of projectFindings) {
@@ -6370,7 +6372,7 @@ function lintProjectAudioFiles(projectDir, htmlSources) {
6370
6372
  code: "audio_file_without_element",
6371
6373
  severity: "warning",
6372
6374
  message: `Found audio file(s) in project (${audioFiles.join(", ")}) but no <audio> element in any composition. The rendered video will be silent.`,
6373
- fixHint: 'Add an <audio id="my-audio" src="' + audioFiles[0] + '" data-start="0" data-track-index="0" data-volume="1"></audio> element inside the composition root.'
6375
+ fixHint: 'Add an <audio id="my-audio" src="' + audioFiles[0] + '" data-start="0" data-duration="__DURATION__" data-track-index="0" data-volume="1"></audio> element inside the composition root. Replace __DURATION__ with the audio length in seconds.'
6374
6376
  });
6375
6377
  }
6376
6378
  return findings;
@@ -6402,6 +6404,74 @@ function lintAudioSrcNotFound(projectDir, htmlSources) {
6402
6404
  }
6403
6405
  return findings;
6404
6406
  }
6407
+ function lintMultipleRootCompositions(projectDir) {
6408
+ const findings = [];
6409
+ try {
6410
+ const rootHtmlFiles = readdirSync2(projectDir).filter((f3) => f3.endsWith(".html"));
6411
+ const rootCompositions = [];
6412
+ for (const file of rootHtmlFiles) {
6413
+ const content = readFileSync7(join9(projectDir, file), "utf-8");
6414
+ if (/data-composition-id/i.test(content)) {
6415
+ rootCompositions.push(file);
6416
+ }
6417
+ }
6418
+ if (rootCompositions.length > 1) {
6419
+ findings.push({
6420
+ code: "multiple_root_compositions",
6421
+ severity: "error",
6422
+ message: `Multiple root-level HTML files with data-composition-id: ${rootCompositions.join(", ")}. The runtime may discover both as entry points, causing duplicate audio playback.`,
6423
+ fixHint: "A project should have exactly one root index.html with data-composition-id. Remove or rename extra files."
6424
+ });
6425
+ }
6426
+ } catch {
6427
+ }
6428
+ return findings;
6429
+ }
6430
+ function lintDuplicateAudioTracks(htmlSources) {
6431
+ const findings = [];
6432
+ function extractAttr(tag, name) {
6433
+ const re2 = new RegExp(`\\b${name}\\s*=\\s*["']([^"']+)["']`, "i");
6434
+ const m2 = tag.match(re2);
6435
+ return m2?.[1] ?? null;
6436
+ }
6437
+ const tracks = [];
6438
+ const seen = /* @__PURE__ */ new Set();
6439
+ for (const html of htmlSources) {
6440
+ const audioTagRe = /<audio\b[^>]*>/gi;
6441
+ let match;
6442
+ while ((match = audioTagRe.exec(html)) !== null) {
6443
+ const tag = match[0];
6444
+ const trackStr = extractAttr(tag, "data-track-index");
6445
+ const startStr = extractAttr(tag, "data-start");
6446
+ const durStr = extractAttr(tag, "data-duration");
6447
+ const src = extractAttr(tag, "src") ?? "unknown";
6448
+ if (!trackStr || !startStr) continue;
6449
+ const trackIndex = parseInt(trackStr, 10);
6450
+ const start = parseFloat(startStr);
6451
+ const duration = durStr ? parseFloat(durStr) : Infinity;
6452
+ const key2 = `${src}:${start}:${duration}:${trackIndex}`;
6453
+ if (seen.has(key2)) continue;
6454
+ seen.add(key2);
6455
+ tracks.push({ trackIndex, start, end: start + duration, src });
6456
+ }
6457
+ }
6458
+ for (let i2 = 0; i2 < tracks.length; i2++) {
6459
+ for (let j2 = i2 + 1; j2 < tracks.length; j2++) {
6460
+ const a = tracks[i2];
6461
+ const b = tracks[j2];
6462
+ if (a.trackIndex !== b.trackIndex) continue;
6463
+ if (a.start < b.end && b.start < a.end) {
6464
+ findings.push({
6465
+ code: "duplicate_audio_track",
6466
+ severity: "warning",
6467
+ message: `Multiple <audio> elements on track ${a.trackIndex} overlap (${a.src} at ${a.start}-${Number.isFinite(a.end) ? a.end.toFixed(1) : "end"}s, ${b.src} at ${b.start}-${Number.isFinite(b.end) ? b.end.toFixed(1) : "end"}s). This causes layered audio playback.`,
6468
+ fixHint: "Use non-overlapping time windows or different track indices."
6469
+ });
6470
+ }
6471
+ }
6472
+ }
6473
+ return findings;
6474
+ }
6405
6475
  function shouldBlockRender(strictErrors, strictAll, totalErrors, totalWarnings) {
6406
6476
  return strictErrors && totalErrors > 0 || strictAll && (totalErrors > 0 || totalWarnings > 0);
6407
6477
  }
@@ -6480,10 +6550,12 @@ function isPortAvailableOnHost(port, host) {
6480
6550
  });
6481
6551
  });
6482
6552
  }
6483
- async function testPortOnAllHosts(port) {
6484
- const hosts = ["127.0.0.1", "0.0.0.0", "::1", "::"];
6485
- const results = await Promise.all(hosts.map((h3) => isPortAvailableOnHost(port, h3)));
6486
- return results.every(Boolean);
6553
+ async function testPortOnAllHosts(port, probe = isPortAvailableOnHost) {
6554
+ for (const host of PORT_PROBE_HOSTS) {
6555
+ const available = await probe(port, host);
6556
+ if (!available) return false;
6557
+ }
6558
+ return true;
6487
6559
  }
6488
6560
  function detectHyperframesServer(port, normalizedProjectDir) {
6489
6561
  return new Promise((resolveResult) => {
@@ -6680,7 +6752,7 @@ async function findPortAndServe(fetch4, startPort, projectDir, forceNew) {
6680
6752
  `Ports ${startPort}\u2013${endPort} are all in use. Use --port to specify a different starting port.`
6681
6753
  );
6682
6754
  }
6683
- var execFileAsync, MAX_PORT_SCAN, PROBE_TIMEOUT_MS, PROBE_MAX_BYTES;
6755
+ var execFileAsync, MAX_PORT_SCAN, PROBE_TIMEOUT_MS, PROBE_MAX_BYTES, PORT_PROBE_HOSTS;
6684
6756
  var init_portUtils = __esm({
6685
6757
  "src/server/portUtils.ts"() {
6686
6758
  "use strict";
@@ -6689,6 +6761,7 @@ var init_portUtils = __esm({
6689
6761
  MAX_PORT_SCAN = 100;
6690
6762
  PROBE_TIMEOUT_MS = 300;
6691
6763
  PROBE_MAX_BYTES = 4096;
6764
+ PORT_PROBE_HOSTS = ["127.0.0.1", "0.0.0.0", "::1", "::"];
6692
6765
  }
6693
6766
  });
6694
6767
 
@@ -20850,12 +20923,20 @@ async function createCaptureSession(serverUrl, outputDir, options, onBeforeCaptu
20850
20923
  config
20851
20924
  };
20852
20925
  }
20926
+ function isFontResourceError(type, text, locationUrl) {
20927
+ if (type !== "error") return false;
20928
+ if (!text.startsWith("Failed to load resource")) return false;
20929
+ return /fonts\.googleapis|fonts\.gstatic|\.(woff2?|ttf|otf)(\b|$)/i.test(
20930
+ `${locationUrl} ${text}`
20931
+ );
20932
+ }
20853
20933
  async function initializeSession(session) {
20854
20934
  const { page, serverUrl } = session;
20855
20935
  page.on("console", (msg) => {
20856
20936
  const type = msg.type();
20857
20937
  const text = msg.text();
20858
- const isFontLoadError = type === "error" && text.startsWith("Failed to load resource") && /fonts\.googleapis|fonts\.gstatic|\.woff2?(\b|$)/i.test(text);
20938
+ const locationUrl = msg.location()?.url ?? "";
20939
+ const isFontLoadError = isFontResourceError(type, text, locationUrl);
20859
20940
  const isResourceLoadError = type === "error" && text.startsWith("Failed to load resource") && !isFontLoadError;
20860
20941
  const prefix = isResourceLoadError ? "[non-blocking]" : type === "error" ? "[Browser:ERROR]" : type === "warn" ? "[Browser:WARN]" : "[Browser]";
20861
20942
  if (!isFontLoadError) {
@@ -73405,7 +73486,7 @@ async function captionImagesWithGemini(outputDir, progress, warnings) {
73405
73486
  const imageFiles = readdirSync13(join47(outputDir, "assets")).filter(
73406
73487
  (f3) => /\.(png|jpg|jpeg|webp|gif)$/i.test(f3)
73407
73488
  );
73408
- const model = "gemini-2.5-flash";
73489
+ const model = process.env.HYPERFRAMES_GEMINI_MODEL || "gemini-3.1-flash-lite-preview";
73409
73490
  const BATCH_SIZE = 20;
73410
73491
  for (let i2 = 0; i2 < imageFiles.length; i2 += BATCH_SIZE) {
73411
73492
  const batch = imageFiles.slice(i2, i2 + BATCH_SIZE);
@@ -73667,50 +73748,7 @@ function loadEnvFile(startDir) {
73667
73748
  }
73668
73749
  }
73669
73750
  async function generateProjectScaffold(outputDir, url, tokens, animationCatalog, hasScreenshots, hasLotties, hasShaders, catalogedAssets, progress, warnings) {
73670
- const indexPath = join49(outputDir, "index.html");
73671
73751
  const metaPath = join49(outputDir, "meta.json");
73672
- if (!existsSync45(indexPath)) {
73673
- writeFileSync20(
73674
- indexPath,
73675
- `<!doctype html>
73676
- <html lang="en">
73677
- <head>
73678
- <meta charset="UTF-8" />
73679
- <meta name="viewport" content="width=1920, height=1080" />
73680
- <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
73681
- <style>
73682
- * { margin: 0; padding: 0; box-sizing: border-box; }
73683
- html, body { margin: 0; width: 1920px; height: 1080px; overflow: hidden; background: #000; }
73684
- </style>
73685
- </head>
73686
- <body>
73687
- <!-- Root composition wrapper \u2014 AGENT: update data-duration to match total video length -->
73688
- <div data-composition-id="main" data-width="1920" data-height="1080" data-start="0" data-duration="28">
73689
-
73690
- <!-- SCENE SLOTS \u2014 AGENT: adjust count, durations, and IDs to match your scene plan -->
73691
- <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>
73692
- <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>
73693
- <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>
73694
- <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>
73695
-
73696
- <!-- NARRATION \u2014 AGENT: update src after generating TTS -->
73697
- <audio id="narration" data-start="0" data-duration="28" data-track-index="0" data-volume="1" src="narration.wav"></audio>
73698
-
73699
- <!-- CAPTIONS (optional \u2014 only add if user requests captions/subtitles) -->
73700
-
73701
- </div>
73702
-
73703
- <script>
73704
- window.__timelines = window.__timelines || {};
73705
- var tl = gsap.timeline({ paused: true });
73706
- window.__timelines["main"] = tl;
73707
- </script>
73708
- </body>
73709
- </html>
73710
- `,
73711
- "utf-8"
73712
- );
73713
- }
73714
73752
  if (!existsSync45(metaPath)) {
73715
73753
  const hostname = new URL(url).hostname.replace(/^www\./, "");
73716
73754
  writeFileSync20(