hyperframes 0.4.3 → 0.4.5

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.3" : "0.0.0-dev";
57
+ VERSION = true ? "0.4.5" : "0.0.0-dev";
58
58
  }
59
59
  });
60
60
 
@@ -6245,7 +6245,15 @@ function runSkillsAdd(repo) {
6245
6245
  return new Promise((resolve35, reject) => {
6246
6246
  const child = spawn("npx", ["skills", "add", repo, "--all"], {
6247
6247
  stdio: "inherit",
6248
- timeout: 12e4
6248
+ timeout: 12e4,
6249
+ // GH #316 — the upstream `skills` CLI shells out to `git clone`.
6250
+ // When Git's clone-hook protection is active (shipped on by
6251
+ // default in 2.45.1, reverted in 2.45.2, still present on many
6252
+ // corporate and CI setups), any globally-registered
6253
+ // `git lfs install` post-checkout hook aborts the clone. The
6254
+ // `repo` reaching this function is hardcoded in SOURCES below
6255
+ // — no user input reaches the spawn — so opting out here is safe.
6256
+ env: { ...process.env, GIT_CLONE_PROTECTION_ACTIVE: "0" }
6249
6257
  });
6250
6258
  child.on("close", (code, signal) => {
6251
6259
  if (code === 0) resolve35();
@@ -6333,7 +6341,9 @@ function lintProject(project) {
6333
6341
  }
6334
6342
  const projectFindings = [
6335
6343
  ...lintProjectAudioFiles(project.dir, allHtmlSources),
6336
- ...lintAudioSrcNotFound(project.dir, allHtmlSources)
6344
+ ...lintAudioSrcNotFound(project.dir, allHtmlSources),
6345
+ ...lintMultipleRootCompositions(project.dir),
6346
+ ...lintDuplicateAudioTracks(allHtmlSources)
6337
6347
  ];
6338
6348
  if (projectFindings.length > 0) {
6339
6349
  for (const finding of projectFindings) {
@@ -6370,7 +6380,7 @@ function lintProjectAudioFiles(projectDir, htmlSources) {
6370
6380
  code: "audio_file_without_element",
6371
6381
  severity: "warning",
6372
6382
  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.'
6383
+ 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
6384
  });
6375
6385
  }
6376
6386
  return findings;
@@ -6402,6 +6412,74 @@ function lintAudioSrcNotFound(projectDir, htmlSources) {
6402
6412
  }
6403
6413
  return findings;
6404
6414
  }
6415
+ function lintMultipleRootCompositions(projectDir) {
6416
+ const findings = [];
6417
+ try {
6418
+ const rootHtmlFiles = readdirSync2(projectDir).filter((f3) => f3.endsWith(".html"));
6419
+ const rootCompositions = [];
6420
+ for (const file of rootHtmlFiles) {
6421
+ const content = readFileSync7(join9(projectDir, file), "utf-8");
6422
+ if (/data-composition-id/i.test(content)) {
6423
+ rootCompositions.push(file);
6424
+ }
6425
+ }
6426
+ if (rootCompositions.length > 1) {
6427
+ findings.push({
6428
+ code: "multiple_root_compositions",
6429
+ severity: "error",
6430
+ message: `Multiple root-level HTML files with data-composition-id: ${rootCompositions.join(", ")}. The runtime may discover both as entry points, causing duplicate audio playback.`,
6431
+ fixHint: "A project should have exactly one root index.html with data-composition-id. Remove or rename extra files."
6432
+ });
6433
+ }
6434
+ } catch {
6435
+ }
6436
+ return findings;
6437
+ }
6438
+ function lintDuplicateAudioTracks(htmlSources) {
6439
+ const findings = [];
6440
+ function extractAttr(tag, name) {
6441
+ const re2 = new RegExp(`\\b${name}\\s*=\\s*["']([^"']+)["']`, "i");
6442
+ const m2 = tag.match(re2);
6443
+ return m2?.[1] ?? null;
6444
+ }
6445
+ const tracks = [];
6446
+ const seen = /* @__PURE__ */ new Set();
6447
+ for (const html of htmlSources) {
6448
+ const audioTagRe = /<audio\b[^>]*>/gi;
6449
+ let match;
6450
+ while ((match = audioTagRe.exec(html)) !== null) {
6451
+ const tag = match[0];
6452
+ const trackStr = extractAttr(tag, "data-track-index");
6453
+ const startStr = extractAttr(tag, "data-start");
6454
+ const durStr = extractAttr(tag, "data-duration");
6455
+ const src = extractAttr(tag, "src") ?? "unknown";
6456
+ if (!trackStr || !startStr) continue;
6457
+ const trackIndex = parseInt(trackStr, 10);
6458
+ const start = parseFloat(startStr);
6459
+ const duration = durStr ? parseFloat(durStr) : Infinity;
6460
+ const key2 = `${src}:${start}:${duration}:${trackIndex}`;
6461
+ if (seen.has(key2)) continue;
6462
+ seen.add(key2);
6463
+ tracks.push({ trackIndex, start, end: start + duration, src });
6464
+ }
6465
+ }
6466
+ for (let i2 = 0; i2 < tracks.length; i2++) {
6467
+ for (let j2 = i2 + 1; j2 < tracks.length; j2++) {
6468
+ const a = tracks[i2];
6469
+ const b = tracks[j2];
6470
+ if (a.trackIndex !== b.trackIndex) continue;
6471
+ if (a.start < b.end && b.start < a.end) {
6472
+ findings.push({
6473
+ code: "duplicate_audio_track",
6474
+ severity: "warning",
6475
+ 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.`,
6476
+ fixHint: "Use non-overlapping time windows or different track indices."
6477
+ });
6478
+ }
6479
+ }
6480
+ }
6481
+ return findings;
6482
+ }
6405
6483
  function shouldBlockRender(strictErrors, strictAll, totalErrors, totalWarnings) {
6406
6484
  return strictErrors && totalErrors > 0 || strictAll && (totalErrors > 0 || totalWarnings > 0);
6407
6485
  }
@@ -6480,10 +6558,12 @@ function isPortAvailableOnHost(port, host) {
6480
6558
  });
6481
6559
  });
6482
6560
  }
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);
6561
+ async function testPortOnAllHosts(port, probe = isPortAvailableOnHost) {
6562
+ for (const host of PORT_PROBE_HOSTS) {
6563
+ const available = await probe(port, host);
6564
+ if (!available) return false;
6565
+ }
6566
+ return true;
6487
6567
  }
6488
6568
  function detectHyperframesServer(port, normalizedProjectDir) {
6489
6569
  return new Promise((resolveResult) => {
@@ -6680,7 +6760,7 @@ async function findPortAndServe(fetch4, startPort, projectDir, forceNew) {
6680
6760
  `Ports ${startPort}\u2013${endPort} are all in use. Use --port to specify a different starting port.`
6681
6761
  );
6682
6762
  }
6683
- var execFileAsync, MAX_PORT_SCAN, PROBE_TIMEOUT_MS, PROBE_MAX_BYTES;
6763
+ var execFileAsync, MAX_PORT_SCAN, PROBE_TIMEOUT_MS, PROBE_MAX_BYTES, PORT_PROBE_HOSTS;
6684
6764
  var init_portUtils = __esm({
6685
6765
  "src/server/portUtils.ts"() {
6686
6766
  "use strict";
@@ -6689,6 +6769,7 @@ var init_portUtils = __esm({
6689
6769
  MAX_PORT_SCAN = 100;
6690
6770
  PROBE_TIMEOUT_MS = 300;
6691
6771
  PROBE_MAX_BYTES = 4096;
6772
+ PORT_PROBE_HOSTS = ["127.0.0.1", "0.0.0.0", "::1", "::"];
6692
6773
  }
6693
6774
  });
6694
6775
 
@@ -10434,10 +10515,10 @@ function compareDocumentPosition(nodeA, nodeB) {
10434
10515
  function uniqueSort(nodes) {
10435
10516
  nodes = nodes.filter((node, i2, arr) => !arr.includes(node, i2 + 1));
10436
10517
  nodes.sort((a, b) => {
10437
- const relative4 = compareDocumentPosition(a, b);
10438
- if (relative4 & DocumentPosition.PRECEDING) {
10518
+ const relative5 = compareDocumentPosition(a, b);
10519
+ if (relative5 & DocumentPosition.PRECEDING) {
10439
10520
  return -1;
10440
- } else if (relative4 & DocumentPosition.FOLLOWING) {
10521
+ } else if (relative5 & DocumentPosition.FOLLOWING) {
10441
10522
  return 1;
10442
10523
  }
10443
10524
  return 0;
@@ -20850,12 +20931,20 @@ async function createCaptureSession(serverUrl, outputDir, options, onBeforeCaptu
20850
20931
  config
20851
20932
  };
20852
20933
  }
20934
+ function isFontResourceError(type, text, locationUrl) {
20935
+ if (type !== "error") return false;
20936
+ if (!text.startsWith("Failed to load resource")) return false;
20937
+ return /fonts\.googleapis|fonts\.gstatic|\.(woff2?|ttf|otf)(\b|$)/i.test(
20938
+ `${locationUrl} ${text}`
20939
+ );
20940
+ }
20853
20941
  async function initializeSession(session) {
20854
20942
  const { page, serverUrl } = session;
20855
20943
  page.on("console", (msg) => {
20856
20944
  const type = msg.type();
20857
20945
  const text = msg.text();
20858
- const isFontLoadError = type === "error" && text.startsWith("Failed to load resource") && /fonts\.googleapis|fonts\.gstatic|\.woff2?(\b|$)/i.test(text);
20946
+ const locationUrl = msg.location()?.url ?? "";
20947
+ const isFontLoadError = isFontResourceError(type, text, locationUrl);
20859
20948
  const isResourceLoadError = type === "error" && text.startsWith("Failed to load resource") && !isFontLoadError;
20860
20949
  const prefix = isResourceLoadError ? "[non-blocking]" : type === "error" ? "[Browser:ERROR]" : type === "warn" ? "[Browser:WARN]" : "[Browser]";
20861
20950
  if (!isFontLoadError) {
@@ -24221,6 +24310,40 @@ var init_ffprobe2 = __esm({
24221
24310
  }
24222
24311
  });
24223
24312
 
24313
+ // ../producer/src/utils/paths.ts
24314
+ import { resolve as resolve13, basename, join as join27, relative as relative2, isAbsolute as isAbsolute3 } from "path";
24315
+ function isPathInside(childPath, parentPath) {
24316
+ const absChild = resolve13(childPath);
24317
+ const absParent = resolve13(parentPath);
24318
+ if (absChild === absParent) return true;
24319
+ const rel = relative2(absParent, absChild);
24320
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute3(rel);
24321
+ }
24322
+ function toExternalAssetKey(absPath) {
24323
+ if (absPath.startsWith("hf-ext/")) return absPath;
24324
+ let normalised = absPath.replace(/\\/g, "/");
24325
+ normalised = normalised.replace(/^\/\/\?\/UNC\//i, "//");
24326
+ normalised = normalised.replace(/^\/\/\?\//, "");
24327
+ normalised = normalised.replace(/^\/\/([^/]+)\//, "unc/$1/");
24328
+ normalised = normalised.replace(/^\/+/, "");
24329
+ normalised = normalised.replace(/^([A-Za-z]):\/?/, "$1/");
24330
+ return "hf-ext/" + normalised;
24331
+ }
24332
+ function resolveRenderPaths(projectDir, outputPath, rendersDir = DEFAULT_RENDERS_DIR) {
24333
+ const absoluteProjectDir = resolve13(projectDir);
24334
+ const projectName = basename(absoluteProjectDir);
24335
+ const resolvedOutputPath = outputPath ?? join27(rendersDir, `${projectName}.mp4`);
24336
+ const absoluteOutputPath = resolve13(resolvedOutputPath);
24337
+ return { absoluteProjectDir, absoluteOutputPath };
24338
+ }
24339
+ var DEFAULT_RENDERS_DIR;
24340
+ var init_paths = __esm({
24341
+ "../producer/src/utils/paths.ts"() {
24342
+ "use strict";
24343
+ DEFAULT_RENDERS_DIR = process.env.PRODUCER_RENDERS_DIR ?? resolve13(new URL(import.meta.url).pathname, "../../..", "renders");
24344
+ }
24345
+ });
24346
+
24224
24347
  // ../producer/src/utils/urlDownloader.ts
24225
24348
  var init_urlDownloader2 = __esm({
24226
24349
  "../producer/src/utils/urlDownloader.ts"() {
@@ -24284,7 +24407,7 @@ var init_fontData_generated = __esm({
24284
24407
  // ../producer/src/services/deterministicFonts.ts
24285
24408
  import { existsSync as existsSync25, mkdirSync as mkdirSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync9 } from "fs";
24286
24409
  import { homedir as homedir6 } from "os";
24287
- import { join as join27 } from "path";
24410
+ import { join as join28 } from "path";
24288
24411
  function normalizeFamilyName(family) {
24289
24412
  return family.trim().replace(/^['"]|['"]$/g, "").trim().toLowerCase();
24290
24413
  }
@@ -24395,14 +24518,14 @@ function fontSlug(familyName) {
24395
24518
  return familyName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
24396
24519
  }
24397
24520
  function fontCacheDir(slug) {
24398
- const dir = join27(GOOGLE_FONTS_CACHE_DIR, slug);
24521
+ const dir = join28(GOOGLE_FONTS_CACHE_DIR, slug);
24399
24522
  if (!existsSync25(dir)) {
24400
24523
  mkdirSync15(dir, { recursive: true });
24401
24524
  }
24402
24525
  return dir;
24403
24526
  }
24404
24527
  function cachedWoff2Path(slug, weight, style) {
24405
- return join27(fontCacheDir(slug), `${weight}-${style}.woff2`);
24528
+ return join28(fontCacheDir(slug), `${weight}-${style}.woff2`);
24406
24529
  }
24407
24530
  async function fetchGoogleFont(familyName) {
24408
24531
  const slug = fontSlug(familyName);
@@ -24614,14 +24737,14 @@ var init_deterministicFonts = __esm({
24614
24737
  poppins: "poppins",
24615
24738
  "segoe ui": "roboto"
24616
24739
  };
24617
- GOOGLE_FONTS_CACHE_DIR = join27(homedir6(), ".cache", "hyperframes", "fonts");
24740
+ GOOGLE_FONTS_CACHE_DIR = join28(homedir6(), ".cache", "hyperframes", "fonts");
24618
24741
  WOFF2_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
24619
24742
  }
24620
24743
  });
24621
24744
 
24622
24745
  // ../producer/src/services/htmlCompiler.ts
24623
24746
  import { readFileSync as readFileSync19, existsSync as existsSync26, mkdirSync as mkdirSync16 } from "fs";
24624
- import { join as join28, dirname as dirname9, resolve as resolve13 } from "path";
24747
+ import { join as join29, dirname as dirname9, resolve as resolve14 } from "path";
24625
24748
  import postcss from "postcss";
24626
24749
  function dedupeElementsById(elements) {
24627
24750
  const deduped = /* @__PURE__ */ new Map();
@@ -24640,7 +24763,7 @@ async function resolveMediaDuration(src, mediaStart, baseDir, downloadDir, tagNa
24640
24763
  return { duration: 0, resolvedPath: src };
24641
24764
  }
24642
24765
  } else if (!filePath.startsWith("/")) {
24643
- filePath = join28(baseDir, filePath);
24766
+ filePath = join29(baseDir, filePath);
24644
24767
  }
24645
24768
  if (!existsSync26(filePath)) {
24646
24769
  return { duration: 0, resolvedPath: filePath };
@@ -24706,7 +24829,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, parentOffset
24706
24829
  const elEnd = elEndRaw ? parseFloat(elEndRaw) : Infinity;
24707
24830
  const absoluteStart = parentOffset + elStart;
24708
24831
  const absoluteEnd = Math.min(parentEnd, isFinite(elEnd) ? parentOffset + elEnd : Infinity);
24709
- const filePath = resolve13(projectDir, srcPath);
24832
+ const filePath = resolve14(projectDir, srcPath);
24710
24833
  if (visited.has(filePath)) {
24711
24834
  continue;
24712
24835
  }
@@ -24906,7 +25029,7 @@ function inlineSubCompositions(html, subCompositions, projectDir) {
24906
25029
  if (!srcPath) continue;
24907
25030
  let compHtml = subCompositions.get(srcPath) || null;
24908
25031
  if (!compHtml) {
24909
- const filePath = resolve13(projectDir, srcPath);
25032
+ const filePath = resolve14(projectDir, srcPath);
24910
25033
  if (existsSync26(filePath)) {
24911
25034
  compHtml = readFileSync19(filePath, "utf-8");
24912
25035
  }
@@ -25119,7 +25242,7 @@ ${safeText}
25119
25242
  return result;
25120
25243
  }
25121
25244
  function collectExternalAssets(html, projectDir) {
25122
- const absProjectDir = resolve13(projectDir);
25245
+ const absProjectDir = resolve14(projectDir);
25123
25246
  const externalAssets = /* @__PURE__ */ new Map();
25124
25247
  const CSS_URL_RE2 = /\burl\(\s*(["']?)([^)"']+)\1\s*\)/g;
25125
25248
  function processPath(rawPath) {
@@ -25127,12 +25250,12 @@ function collectExternalAssets(html, projectDir) {
25127
25250
  if (!trimmed || trimmed.startsWith("/") || trimmed.startsWith("http://") || trimmed.startsWith("https://") || trimmed.startsWith("//") || trimmed.startsWith("data:") || trimmed.startsWith("#")) {
25128
25251
  return null;
25129
25252
  }
25130
- const absPath = resolve13(absProjectDir, trimmed);
25131
- if (absPath.startsWith(absProjectDir + "/") || absPath === absProjectDir) {
25253
+ const absPath = resolve14(absProjectDir, trimmed);
25254
+ if (isPathInside(absPath, absProjectDir)) {
25132
25255
  return null;
25133
25256
  }
25134
25257
  if (!existsSync26(absPath)) return null;
25135
- const safeKey = "hf-ext/" + absPath.replace(/^\//, "");
25258
+ const safeKey = toExternalAssetKey(absPath);
25136
25259
  externalAssets.set(safeKey, absPath);
25137
25260
  return safeKey;
25138
25261
  }
@@ -25204,7 +25327,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
25204
25327
  const audios = dedupeElementsById([...mainAudios, ...subAudios]);
25205
25328
  for (const video of videos) {
25206
25329
  if (isHttpUrl(video.src)) continue;
25207
- const videoPath = resolve13(projectDir, video.src);
25330
+ const videoPath = resolve14(projectDir, video.src);
25208
25331
  const reencode = `ffmpeg -i "${video.src}" -c:v libx264 -r 30 -g 30 -keyint_min 30 -movflags +faststart -c:a copy output.mp4`;
25209
25332
  Promise.all([analyzeKeyframeIntervals(videoPath), extractVideoMetadata(videoPath)]).then(([analysis, metadata]) => {
25210
25333
  if (analysis.isProblematic) {
@@ -25339,6 +25462,7 @@ var init_htmlCompiler2 = __esm({
25339
25462
  init_esm10();
25340
25463
  init_src();
25341
25464
  init_ffprobe2();
25465
+ init_paths();
25342
25466
  init_src2();
25343
25467
  init_urlDownloader2();
25344
25468
  init_deterministicFonts();
@@ -25397,7 +25521,7 @@ import {
25397
25521
  copyFileSync as copyFileSync2,
25398
25522
  appendFileSync
25399
25523
  } from "fs";
25400
- import { join as join29, dirname as dirname10, resolve as resolve14 } from "path";
25524
+ import { join as join30, dirname as dirname10, resolve as resolve15 } from "path";
25401
25525
  import { randomUUID as randomUUID2 } from "crypto";
25402
25526
  import { freemem as freemem2 } from "os";
25403
25527
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -25453,17 +25577,17 @@ function installDebugLogger(logPath, log = defaultLogger) {
25453
25577
  };
25454
25578
  }
25455
25579
  function writeCompiledArtifacts(compiled, workDir, includeSummary) {
25456
- const compileDir = join29(workDir, "compiled");
25580
+ const compileDir = join30(workDir, "compiled");
25457
25581
  mkdirSync17(compileDir, { recursive: true });
25458
- writeFileSync10(join29(compileDir, "index.html"), compiled.html, "utf-8");
25582
+ writeFileSync10(join30(compileDir, "index.html"), compiled.html, "utf-8");
25459
25583
  for (const [srcPath, html] of compiled.subCompositions) {
25460
- const outPath = join29(compileDir, srcPath);
25584
+ const outPath = join30(compileDir, srcPath);
25461
25585
  mkdirSync17(dirname10(outPath), { recursive: true });
25462
25586
  writeFileSync10(outPath, html, "utf-8");
25463
25587
  }
25464
25588
  for (const [relativePath, absolutePath] of compiled.externalAssets) {
25465
- const outPath = resolve14(join29(compileDir, relativePath));
25466
- if (!outPath.startsWith(compileDir + "/")) {
25589
+ const outPath = resolve15(join30(compileDir, relativePath));
25590
+ if (!isPathInside(outPath, compileDir)) {
25467
25591
  console.warn(`[Render] Skipping external asset with unsafe path: ${relativePath}`);
25468
25592
  continue;
25469
25593
  }
@@ -25491,7 +25615,7 @@ function writeCompiledArtifacts(compiled, workDir, includeSummary) {
25491
25615
  })),
25492
25616
  subCompositions: Array.from(compiled.subCompositions.keys())
25493
25617
  };
25494
- writeFileSync10(join29(compileDir, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
25618
+ writeFileSync10(join30(compileDir, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
25495
25619
  }
25496
25620
  }
25497
25621
  function createRenderJob(config) {
@@ -25535,9 +25659,9 @@ function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
25535
25659
  }
25536
25660
  async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSignal) {
25537
25661
  const moduleDir = dirname10(fileURLToPath3(import.meta.url));
25538
- const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve14(process.env.PRODUCER_RENDERS_DIR, "..") : resolve14(moduleDir, "../..");
25539
- const debugDir = join29(producerRoot, ".debug");
25540
- const workDir = job.config.debug ? join29(debugDir, job.id) : join29(dirname10(outputPath), `work-${job.id}`);
25662
+ const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve15(process.env.PRODUCER_RENDERS_DIR, "..") : resolve15(moduleDir, "../..");
25663
+ const debugDir = join30(producerRoot, ".debug");
25664
+ const workDir = job.config.debug ? join30(debugDir, job.id) : join30(dirname10(outputPath), `work-${job.id}`);
25541
25665
  const pipelineStart = Date.now();
25542
25666
  const log = job.config.logger ?? defaultLogger;
25543
25667
  let fileServer = null;
@@ -25545,7 +25669,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25545
25669
  let lastBrowserConsole = [];
25546
25670
  let restoreLogger = null;
25547
25671
  const perfStages = {};
25548
- const perfOutputPath = join29(workDir, "perf-summary.json");
25672
+ const perfOutputPath = join30(workDir, "perf-summary.json");
25549
25673
  const cfg = { ...job.config.producerConfig ?? resolveConfig() };
25550
25674
  const outputFormat = job.config.format ?? "mp4";
25551
25675
  const isWebm = outputFormat === "webm";
@@ -25567,19 +25691,19 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25567
25691
  assertNotAborted();
25568
25692
  if (!existsSync27(workDir)) mkdirSync17(workDir, { recursive: true });
25569
25693
  if (job.config.debug) {
25570
- const logPath = join29(workDir, "render.log");
25694
+ const logPath = join30(workDir, "render.log");
25571
25695
  restoreLogger = installDebugLogger(logPath, log);
25572
25696
  }
25573
25697
  const entryFile = job.config.entryFile || "index.html";
25574
- let htmlPath = join29(projectDir, entryFile);
25698
+ let htmlPath = join30(projectDir, entryFile);
25575
25699
  if (!existsSync27(htmlPath)) {
25576
25700
  throw new Error(`Entry file not found: ${htmlPath}`);
25577
25701
  }
25578
25702
  assertNotAborted();
25579
25703
  const rawEntry = readFileSync20(htmlPath, "utf-8");
25580
25704
  if (entryFile !== "index.html" && rawEntry.trimStart().startsWith("<template")) {
25581
- const wrapperPath = join29(workDir, "standalone-entry.html");
25582
- const projectIndexPath = join29(projectDir, "index.html");
25705
+ const wrapperPath = join30(workDir, "standalone-entry.html");
25706
+ const projectIndexPath = join30(projectDir, "index.html");
25583
25707
  if (!existsSync27(projectIndexPath)) {
25584
25708
  throw new Error(
25585
25709
  `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
@@ -25603,7 +25727,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25603
25727
  const stage1Start = Date.now();
25604
25728
  updateJobStatus(job, "preprocessing", "Compiling composition", 5, onProgress);
25605
25729
  const compileStart = Date.now();
25606
- let compiled = await compileForRender(projectDir, htmlPath, join29(workDir, "downloads"));
25730
+ let compiled = await compileForRender(projectDir, htmlPath, join30(workDir, "downloads"));
25607
25731
  assertNotAborted();
25608
25732
  perfStages.compileOnlyMs = Date.now() - compileStart;
25609
25733
  writeCompiledArtifacts(compiled, workDir, Boolean(job.config.debug));
@@ -25632,7 +25756,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25632
25756
  reasons.push(`${compiled.unresolvedCompositions.length} unresolved composition(s)`);
25633
25757
  fileServer = await createFileServer2({
25634
25758
  projectDir,
25635
- compiledDir: join29(workDir, "compiled"),
25759
+ compiledDir: join30(workDir, "compiled"),
25636
25760
  port: 0
25637
25761
  });
25638
25762
  assertNotAborted();
@@ -25645,7 +25769,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25645
25769
  };
25646
25770
  probeSession = await createCaptureSession(
25647
25771
  fileServer.url,
25648
- join29(workDir, "probe"),
25772
+ join30(workDir, "probe"),
25649
25773
  captureOpts,
25650
25774
  null,
25651
25775
  cfg
@@ -25677,7 +25801,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25677
25801
  compiled,
25678
25802
  resolutions,
25679
25803
  projectDir,
25680
- join29(workDir, "downloads")
25804
+ join30(workDir, "downloads")
25681
25805
  );
25682
25806
  assertNotAborted();
25683
25807
  composition.videos = compiled.videos;
@@ -25812,12 +25936,12 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25812
25936
  const stage2Start = Date.now();
25813
25937
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress);
25814
25938
  let frameLookup = null;
25815
- const compiledDir = join29(workDir, "compiled");
25939
+ const compiledDir = join30(workDir, "compiled");
25816
25940
  if (composition.videos.length > 0) {
25817
25941
  const extractionResult = await extractAllVideoFrames(
25818
25942
  composition.videos,
25819
25943
  projectDir,
25820
- { fps: job.config.fps, outputDir: join29(workDir, "video-frames") },
25944
+ { fps: job.config.fps, outputDir: join30(workDir, "video-frames") },
25821
25945
  abortSignal,
25822
25946
  void 0,
25823
25947
  compiledDir
@@ -25851,13 +25975,13 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25851
25975
  }
25852
25976
  const stage3Start = Date.now();
25853
25977
  updateJobStatus(job, "preprocessing", "Processing audio tracks", 20, onProgress);
25854
- const audioOutputPath = join29(workDir, "audio.aac");
25978
+ const audioOutputPath = join30(workDir, "audio.aac");
25855
25979
  let hasAudio = false;
25856
25980
  if (composition.audios.length > 0) {
25857
25981
  const audioResult = await processCompositionAudio(
25858
25982
  composition.audios,
25859
25983
  projectDir,
25860
- join29(workDir, "audio-work"),
25984
+ join30(workDir, "audio-work"),
25861
25985
  audioOutputPath,
25862
25986
  job.duration,
25863
25987
  abortSignal,
@@ -25875,12 +25999,12 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25875
25999
  if (!fileServer) {
25876
26000
  fileServer = await createFileServer2({
25877
26001
  projectDir,
25878
- compiledDir: join29(workDir, "compiled"),
26002
+ compiledDir: join30(workDir, "compiled"),
25879
26003
  port: 0
25880
26004
  });
25881
26005
  assertNotAborted();
25882
26006
  }
25883
- const framesDir = join29(workDir, "captured-frames");
26007
+ const framesDir = join30(workDir, "captured-frames");
25884
26008
  if (!existsSync27(framesDir)) mkdirSync17(framesDir, { recursive: true });
25885
26009
  const captureOptions = {
25886
26010
  width,
@@ -25892,7 +26016,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25892
26016
  const workerCount = calculateOptimalWorkers(job.totalFrames, job.config.workers, cfg);
25893
26017
  const FORMAT_EXT2 = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
25894
26018
  const videoExt = FORMAT_EXT2[outputFormat] ?? ".mp4";
25895
- const videoOnlyPath = join29(workDir, `video-only${videoExt}`);
26019
+ const videoOnlyPath = join30(workDir, `video-only${videoExt}`);
25896
26020
  const preset = getEncoderPreset(job.config.quality, outputFormat);
25897
26021
  const effectiveQuality = job.config.crf ?? preset.quality;
25898
26022
  const effectiveBitrate = job.config.videoBitrate;
@@ -26168,7 +26292,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
26168
26292
  }
26169
26293
  if (job.config.debug) {
26170
26294
  if (existsSync27(outputPath)) {
26171
- const debugOutput = join29(workDir, `output${videoExt}`);
26295
+ const debugOutput = join30(workDir, `output${videoExt}`);
26172
26296
  copyFileSync2(outputPath, debugOutput);
26173
26297
  }
26174
26298
  } else {
@@ -26269,6 +26393,7 @@ var init_renderOrchestrator = __esm({
26269
26393
  init_fileServer2();
26270
26394
  init_htmlCompiler2();
26271
26395
  init_logger();
26396
+ init_paths();
26272
26397
  RenderCancelledError = class extends Error {
26273
26398
  reason;
26274
26399
  constructor(message = "render_cancelled", reason = "aborted") {
@@ -26306,7 +26431,7 @@ var init_config3 = __esm({
26306
26431
 
26307
26432
  // ../producer/src/services/hyperframeLint.ts
26308
26433
  import { existsSync as existsSync28, readFileSync as readFileSync21, statSync as statSync8 } from "fs";
26309
- import { resolve as resolve15, join as join30 } from "path";
26434
+ import { resolve as resolve16, join as join31 } from "path";
26310
26435
  function isStringRecord(value) {
26311
26436
  if (!value || typeof value !== "object" || Array.isArray(value)) {
26312
26437
  return false;
@@ -26333,7 +26458,7 @@ function pickEntryFile(files, preferredEntryFile) {
26333
26458
  return null;
26334
26459
  }
26335
26460
  function readProjectEntryFile(projectDir, preferredEntryFile) {
26336
- const absProjectDir = resolve15(projectDir);
26461
+ const absProjectDir = resolve16(projectDir);
26337
26462
  if (!existsSync28(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
26338
26463
  return { error: `Project directory not found: ${absProjectDir}` };
26339
26464
  }
@@ -26341,7 +26466,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
26341
26466
  (value) => typeof value === "string" && value.trim().length > 0
26342
26467
  );
26343
26468
  for (const entryFile of entryCandidates) {
26344
- const absoluteEntryPath = resolve15(absProjectDir, entryFile);
26469
+ const absoluteEntryPath = resolve16(absProjectDir, entryFile);
26345
26470
  if (!absoluteEntryPath.startsWith(absProjectDir)) {
26346
26471
  return { error: `Entry file must stay inside project directory: ${entryFile}` };
26347
26472
  }
@@ -26354,7 +26479,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
26354
26479
  }
26355
26480
  }
26356
26481
  return {
26357
- error: `No HTML entry file found in project directory: ${join30(absProjectDir, preferredEntryFile || "index.html")}`
26482
+ error: `No HTML entry file found in project directory: ${join31(absProjectDir, preferredEntryFile || "index.html")}`
26358
26483
  };
26359
26484
  }
26360
26485
  function prepareHyperframeLintBody(body) {
@@ -26400,23 +26525,6 @@ var init_hyperframeLint = __esm({
26400
26525
  }
26401
26526
  });
26402
26527
 
26403
- // ../producer/src/utils/paths.ts
26404
- import { resolve as resolve16, basename, join as join31 } from "path";
26405
- function resolveRenderPaths(projectDir, outputPath, rendersDir = DEFAULT_RENDERS_DIR) {
26406
- const absoluteProjectDir = resolve16(projectDir);
26407
- const projectName = basename(absoluteProjectDir);
26408
- const resolvedOutputPath = outputPath ?? join31(rendersDir, `${projectName}.mp4`);
26409
- const absoluteOutputPath = resolve16(resolvedOutputPath);
26410
- return { absoluteProjectDir, absoluteOutputPath };
26411
- }
26412
- var DEFAULT_RENDERS_DIR;
26413
- var init_paths = __esm({
26414
- "../producer/src/utils/paths.ts"() {
26415
- "use strict";
26416
- DEFAULT_RENDERS_DIR = process.env.PRODUCER_RENDERS_DIR ?? resolve16(new URL(import.meta.url).pathname, "../../..", "renders");
26417
- }
26418
- });
26419
-
26420
26528
  // ../producer/src/utils/semaphore.ts
26421
26529
  var Semaphore;
26422
26530
  var init_semaphore = __esm({
@@ -28219,7 +28327,7 @@ __export(add_exports, {
28219
28327
  runAdd: () => runAdd
28220
28328
  });
28221
28329
  import { existsSync as existsSync33 } from "fs";
28222
- import { resolve as resolve21, relative as relative2 } from "path";
28330
+ import { resolve as resolve21, relative as relative3 } from "path";
28223
28331
  function remapTarget(item, originalTarget, paths) {
28224
28332
  if (item.type === "hyperframes:block") {
28225
28333
  const blocksDir = paths.blocks.replace(/\/+$/, "");
@@ -28358,7 +28466,7 @@ var init_add = __esm({
28358
28466
  console.log("");
28359
28467
  console.log(`${c.success("\u2713")} Added ${c.accent(result.name)} (${result.type})`);
28360
28468
  for (const file of result.written) {
28361
- console.log(` ${c.dim(relative2(projectDir, file))}`);
28469
+ console.log(` ${c.dim(relative3(projectDir, file))}`);
28362
28470
  }
28363
28471
  if (result.snippet) {
28364
28472
  console.log("");
@@ -30787,18 +30895,18 @@ function checkFFmpeg() {
30787
30895
  return {
30788
30896
  ok: false,
30789
30897
  detail: "Not found",
30790
- hint: process.platform === "darwin" ? "brew install ffmpeg" : "sudo apt install ffmpeg"
30898
+ hint: getFFmpegInstallHint()
30791
30899
  };
30792
30900
  }
30793
30901
  function checkFFprobe() {
30794
30902
  try {
30795
- const result = execSync3("which ffprobe", { encoding: "utf-8", timeout: 5e3 }).trim();
30796
- return { ok: true, detail: result };
30903
+ const version = execSync3("ffprobe -version", { encoding: "utf-8", timeout: 5e3 }).split("\n")[0] ?? "";
30904
+ return { ok: true, detail: version.trim() };
30797
30905
  } catch {
30798
30906
  return {
30799
30907
  ok: false,
30800
30908
  detail: "Not found",
30801
- hint: "Installed with ffmpeg"
30909
+ hint: `Installed with ffmpeg \u2014 ${getFFmpegInstallHint()}`
30802
30910
  };
30803
30911
  }
30804
30912
  }
@@ -31450,7 +31558,7 @@ __export(snapshot_exports, {
31450
31558
  examples: () => examples18
31451
31559
  });
31452
31560
  import { existsSync as existsSync44, readFileSync as readFileSync31, mkdirSync as mkdirSync24 } from "fs";
31453
- import { resolve as resolve32, join as join44, dirname as dirname20, relative as relative3, isAbsolute as isAbsolute3 } from "path";
31561
+ import { resolve as resolve32, join as join44, dirname as dirname20, relative as relative4, isAbsolute as isAbsolute4 } from "path";
31454
31562
  import { fileURLToPath as fileURLToPath8 } from "url";
31455
31563
  async function captureSnapshots(projectDir, opts) {
31456
31564
  const { bundleToSingleHtml: bundleToSingleHtml2 } = await Promise.resolve().then(() => (init_compiler(), compiler_exports));
@@ -31483,8 +31591,8 @@ async function captureSnapshots(projectDir, opts) {
31483
31591
  return;
31484
31592
  }
31485
31593
  const filePath = resolve32(projectDir, decodeURIComponent(url).replace(/^\//, ""));
31486
- const rel = relative3(projectDir, filePath);
31487
- if (rel.startsWith("..") || isAbsolute3(rel)) {
31594
+ const rel = relative4(projectDir, filePath);
31595
+ if (rel.startsWith("..") || isAbsolute4(rel)) {
31488
31596
  res.writeHead(403);
31489
31597
  res.end();
31490
31598
  return;
@@ -73405,7 +73513,7 @@ async function captionImagesWithGemini(outputDir, progress, warnings) {
73405
73513
  const imageFiles = readdirSync13(join47(outputDir, "assets")).filter(
73406
73514
  (f3) => /\.(png|jpg|jpeg|webp|gif)$/i.test(f3)
73407
73515
  );
73408
- const model = "gemini-2.5-flash";
73516
+ const model = process.env.HYPERFRAMES_GEMINI_MODEL || "gemini-3.1-flash-lite-preview";
73409
73517
  const BATCH_SIZE = 20;
73410
73518
  for (let i2 = 0; i2 < imageFiles.length; i2 += BATCH_SIZE) {
73411
73519
  const batch = imageFiles.slice(i2, i2 + BATCH_SIZE);
@@ -73667,50 +73775,7 @@ function loadEnvFile(startDir) {
73667
73775
  }
73668
73776
  }
73669
73777
  async function generateProjectScaffold(outputDir, url, tokens, animationCatalog, hasScreenshots, hasLotties, hasShaders, catalogedAssets, progress, warnings) {
73670
- const indexPath = join49(outputDir, "index.html");
73671
73778
  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
73779
  if (!existsSync45(metaPath)) {
73715
73780
  const hostname = new URL(url).hostname.replace(/^www\./, "");
73716
73781
  writeFileSync20(