hyperframes 0.6.64 → 0.6.66

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
@@ -50,7 +50,7 @@ var VERSION;
50
50
  var init_version = __esm({
51
51
  "src/version.ts"() {
52
52
  "use strict";
53
- VERSION = true ? "0.6.64" : "0.0.0-dev";
53
+ VERSION = true ? "0.6.66" : "0.0.0-dev";
54
54
  }
55
55
  });
56
56
 
@@ -58212,7 +58212,7 @@ function isSafeAttributeValue(name, value) {
58212
58212
  function patchElementInHtml(source, target, operations) {
58213
58213
  const { document: document2, wrappedFragment } = parseSourceDocument(source);
58214
58214
  const el = findTargetElement(document2, target);
58215
- if (!el || !isHTMLElement(el)) return source;
58215
+ if (!el || !isHTMLElement(el)) return { html: source, matched: false };
58216
58216
  const htmlEl = el;
58217
58217
  for (const op of operations) {
58218
58218
  switch (op.type) {
@@ -58247,7 +58247,10 @@ function patchElementInHtml(source, target, operations) {
58247
58247
  break;
58248
58248
  }
58249
58249
  }
58250
- return wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
58250
+ return {
58251
+ html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
58252
+ matched: true
58253
+ };
58251
58254
  }
58252
58255
  function probeElementInSource(source, target) {
58253
58256
  if (!target.id && !target.selector) return false;
@@ -58531,12 +58534,16 @@ function registerFileRoutes(api, adapter2) {
58531
58534
  } catch {
58532
58535
  return c3.json({ error: "not found" }, 404);
58533
58536
  }
58534
- return writeIfChanged(
58535
- c3,
58536
- ctx.absPath,
58537
+ const { html: patched, matched } = patchElementInHtml(
58537
58538
  originalContent,
58538
- patchElementInHtml(originalContent, parsed.target, parsed.body.operations)
58539
+ parsed.target,
58540
+ parsed.body.operations
58539
58541
  );
58542
+ if (patched === originalContent) {
58543
+ return c3.json({ ok: true, changed: false, matched, content: originalContent });
58544
+ }
58545
+ writeFileSync8(ctx.absPath, patched, "utf-8");
58546
+ return c3.json({ ok: true, changed: true, matched, content: patched });
58540
58547
  });
58541
58548
  api.post("/projects/:id/file-mutations/probe-element/*", async (c3) => {
58542
58549
  const ctx = await resolveFileMutationContext(c3, adapter2, "probe-element");
@@ -71433,7 +71440,7 @@ var init_urlDownloader2 = __esm({
71433
71440
 
71434
71441
  // ../producer/src/services/htmlCompiler.ts
71435
71442
  import { readFileSync as readFileSync27, existsSync as existsSync35, mkdirSync as mkdirSync20 } from "fs";
71436
- import { join as join39, dirname as dirname13, resolve as resolve19 } from "path";
71443
+ import { join as join39, dirname as dirname13, resolve as resolve19, basename as basename5 } from "path";
71437
71444
  function dedupeElementsById(elements) {
71438
71445
  const deduped = /* @__PURE__ */ new Map();
71439
71446
  for (const element of elements) {
@@ -71566,6 +71573,7 @@ async function compileHtmlFile(html, baseDir, downloadDir) {
71566
71573
  }
71567
71574
  compiledHtml = compiledHtml.replace(/(<video\b[^>]*)\s+crossorigin(?:=["'][^"']*["'])?/gi, "$1");
71568
71575
  compiledHtml = compiledHtml.replace(/(<img\b[^>]*)\s+crossorigin(?:=["'][^"']*["'])?/gi, "$1");
71576
+ compiledHtml = compiledHtml.replace(/(<audio\b[^>]*)\s+crossorigin(?:=["'][^"']*["'])?/gi, "$1");
71569
71577
  return { html: compiledHtml, unresolvedCompositions };
71570
71578
  }
71571
71579
  async function parseSubCompositions(html, projectDir, downloadDir, parentOffset = 0, parentEnd = Infinity, visited = /* @__PURE__ */ new Set()) {
@@ -71978,6 +71986,46 @@ function collectExternalAssets(html, projectDir) {
71978
71986
  externalAssets
71979
71987
  };
71980
71988
  }
71989
+ async function localizeRemoteMediaSources(html, downloadDir) {
71990
+ const remoteDir = join39(downloadDir, REMOTE_MEDIA_SUBDIR);
71991
+ const urlSet = /* @__PURE__ */ new Set();
71992
+ const re2 = new RegExp(REMOTE_MEDIA_TAG_RE.source, REMOTE_MEDIA_TAG_RE.flags);
71993
+ let m2;
71994
+ while ((m2 = re2.exec(html)) !== null) {
71995
+ if (m2[1]) urlSet.add(m2[1]);
71996
+ }
71997
+ if (urlSet.size === 0) return { html, remoteMediaAssets: /* @__PURE__ */ new Map() };
71998
+ if (!existsSync35(remoteDir)) mkdirSync20(remoteDir, { recursive: true });
71999
+ const urlToLocal = /* @__PURE__ */ new Map();
72000
+ await Promise.all(
72001
+ [...urlSet].map(async (url) => {
72002
+ try {
72003
+ const localPath = await downloadToTemp(url, remoteDir);
72004
+ urlToLocal.set(url, localPath);
72005
+ } catch (err) {
72006
+ console.warn(
72007
+ `[Compiler] Remote media download failed for ${url} \u2014 using original URL as fallback. ${err instanceof Error ? err.message : String(err)}`
72008
+ );
72009
+ }
72010
+ })
72011
+ );
72012
+ if (urlToLocal.size === 0) return { html, remoteMediaAssets: /* @__PURE__ */ new Map() };
72013
+ const remoteMediaAssets = /* @__PURE__ */ new Map();
72014
+ const urlToRelPath = /* @__PURE__ */ new Map();
72015
+ for (const [url, absPath] of urlToLocal) {
72016
+ const relPath = `${REMOTE_MEDIA_SUBDIR}/${basename5(absPath)}`;
72017
+ remoteMediaAssets.set(relPath, absPath);
72018
+ urlToRelPath.set(url, relPath);
72019
+ }
72020
+ let result = html;
72021
+ for (const [url, relPath] of urlToRelPath) {
72022
+ result = result.replaceAll(`"${url}"`, `"${relPath}"`).replaceAll(`'${url}'`, `'${relPath}'`);
72023
+ }
72024
+ console.log(
72025
+ `[Compiler] Localized ${urlToLocal.size} remote media source(s) to ${REMOTE_MEDIA_SUBDIR}/`
72026
+ );
72027
+ return { html: result, remoteMediaAssets };
72028
+ }
71981
72029
  function rewriteUnresolvableGsapToCdn(html, projectDir) {
71982
72030
  return html.replace(
71983
72031
  /(<script\b[^>]*\bsrc=["'])([^"']*gsap[^"']*\/dist\/([^"']+))(["'][^>]*>)/gi,
@@ -72028,10 +72076,17 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
72028
72076
  'data-hf-studio-motion="'
72029
72077
  ];
72030
72078
  const hasPositionEdits = HF_POSITION_ATTRS.some((attr) => htmlWithAssets.includes(attr));
72031
- const html = hasPositionEdits ? htmlWithAssets.replace(
72079
+ const htmlWithPositionScript = hasPositionEdits ? htmlWithAssets.replace(
72032
72080
  /<\/body>/i,
72033
72081
  `<script>${createStudioPositionSeekReapplyScript()}</script></body>`
72034
72082
  ) : htmlWithAssets;
72083
+ const { html, remoteMediaAssets } = await localizeRemoteMediaSources(
72084
+ htmlWithPositionScript,
72085
+ downloadDir
72086
+ );
72087
+ for (const [relPath, absPath] of remoteMediaAssets) {
72088
+ externalAssets.set(relPath, absPath);
72089
+ }
72035
72090
  const mainVideos = parseVideoElements(html);
72036
72091
  const mainAudios = parseAudioElements(html);
72037
72092
  const mainImages = parseImageElements(html);
@@ -72312,7 +72367,7 @@ async function recompileWithResolutions(compiled, resolutions, projectDir, downl
72312
72367
  hasShaderTransitions: compiled.hasShaderTransitions
72313
72368
  };
72314
72369
  }
72315
- var INLINE_SCRIPT_PATTERN, COMPILER_MOUNT_BLOCK_START, COMPILER_MOUNT_BLOCK_END, SHADER_TRANSITION_USAGE_PATTERN, GSAP_CDN_BASE;
72370
+ var INLINE_SCRIPT_PATTERN, COMPILER_MOUNT_BLOCK_START, COMPILER_MOUNT_BLOCK_END, SHADER_TRANSITION_USAGE_PATTERN, REMOTE_MEDIA_SUBDIR, REMOTE_MEDIA_TAG_RE, GSAP_CDN_BASE;
72316
72371
  var init_htmlCompiler2 = __esm({
72317
72372
  "../producer/src/services/htmlCompiler.ts"() {
72318
72373
  "use strict";
@@ -72329,6 +72384,8 @@ var init_htmlCompiler2 = __esm({
72329
72384
  COMPILER_MOUNT_BLOCK_START = "/* __HF_COMPILER_MOUNT_START__ */";
72330
72385
  COMPILER_MOUNT_BLOCK_END = "/* __HF_COMPILER_MOUNT_END__ */";
72331
72386
  SHADER_TRANSITION_USAGE_PATTERN = /\b(?:(?:window|globalThis)\s*\.\s*)?HyperShader\s*\.\s*init\s*\(|\b__hf\s*\.\s*transitions\s*=/;
72387
+ REMOTE_MEDIA_SUBDIR = "_remote_media";
72388
+ REMOTE_MEDIA_TAG_RE = /<(?:video|audio)\b[^>]*?\bsrc\s*=\s*["'](https?:\/\/[^"']+)["'][^>]*>/gi;
72332
72389
  GSAP_CDN_BASE = "https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/";
72333
72390
  }
72334
72391
  });
@@ -73033,7 +73090,8 @@ async function runCaptureStreamingStage(input2) {
73033
73090
  streamingEncoder = await spawnStreamingEncoder(
73034
73091
  videoOnlyPath,
73035
73092
  streamingEncoderOptions,
73036
- abortSignal
73093
+ abortSignal,
73094
+ cfg
73037
73095
  );
73038
73096
  assertNotAborted();
73039
73097
  } catch (err) {
@@ -78343,7 +78401,7 @@ __export(studioServer_exports, {
78343
78401
  import { Hono as Hono5 } from "hono";
78344
78402
  import { streamSSE as streamSSE3 } from "hono/streaming";
78345
78403
  import { existsSync as existsSync48, readFileSync as readFileSync35, writeFileSync as writeFileSync25, statSync as statSync17 } from "fs";
78346
- import { resolve as resolve24, join as join58, basename as basename5 } from "path";
78404
+ import { resolve as resolve24, join as join58, basename as basename6 } from "path";
78347
78405
  function resolveDistDir() {
78348
78406
  return resolveStudioBundle().dir;
78349
78407
  }
@@ -78470,7 +78528,7 @@ async function loadPreviewServerBuildSignature() {
78470
78528
  }
78471
78529
  function createStudioServer(options) {
78472
78530
  const { projectDir, projectName } = options;
78473
- const projectId = projectName || basename5(projectDir);
78531
+ const projectId = projectName || basename6(projectDir);
78474
78532
  const studioDir = resolveDistDir();
78475
78533
  const runtimePath = resolveRuntimePath();
78476
78534
  const watcher = createProjectWatcher(projectDir);
@@ -78646,19 +78704,19 @@ function createStudioServer(options) {
78646
78704
  async installRegistryBlock(opts) {
78647
78705
  const { resolveItem: resolveItem2 } = await Promise.resolve().then(() => (init_resolver(), resolver_exports));
78648
78706
  const { installItem: installItem2 } = await Promise.resolve().then(() => (init_installer(), installer_exports));
78649
- const { readFileSync: readFileSync58, writeFileSync: writeFileSync40, existsSync: existsSync81 } = await import("fs");
78707
+ const { readFileSync: readFileSync59, writeFileSync: writeFileSync40, existsSync: existsSync81 } = await import("fs");
78650
78708
  const { join: join92 } = await import("path");
78651
78709
  const item = await resolveItem2(opts.blockName);
78652
78710
  const { written } = await installItem2(item, { destDir: opts.project.dir });
78653
78711
  const indexPath = join92(opts.project.dir, "index.html");
78654
78712
  if (existsSync81(indexPath)) {
78655
- const indexHtml = readFileSync58(indexPath, "utf-8");
78713
+ const indexHtml = readFileSync59(indexPath, "utf-8");
78656
78714
  const hostW = indexHtml.match(/data-width="(\d+)"/)?.[1];
78657
78715
  const hostH = indexHtml.match(/data-height="(\d+)"/)?.[1];
78658
78716
  if (hostW && hostH) {
78659
78717
  for (const absPath of written) {
78660
78718
  if (!absPath.endsWith(".html")) continue;
78661
- let content = readFileSync58(absPath, "utf-8");
78719
+ let content = readFileSync59(absPath, "utf-8");
78662
78720
  content = content.replace(
78663
78721
  /(<meta\s+name="viewport"\s+content="width=)\d+(,\s*height=)\d+/i,
78664
78722
  `$1${hostW}$2${hostH}`
@@ -78856,14 +78914,14 @@ __export(preview_exports, {
78856
78914
  });
78857
78915
  import { spawn as spawn11 } from "child_process";
78858
78916
  import { existsSync as existsSync49, lstatSync as lstatSync2, symlinkSync as symlinkSync2, unlinkSync as unlinkSync5, readlinkSync, mkdirSync as mkdirSync30 } from "fs";
78859
- import { resolve as resolve25, dirname as dirname19, basename as basename6, join as join59 } from "path";
78917
+ import { resolve as resolve25, dirname as dirname19, basename as basename7, join as join59 } from "path";
78860
78918
  import { fileURLToPath as fileURLToPath6 } from "url";
78861
78919
  import { createRequire as createRequire2 } from "module";
78862
78920
  async function runDevMode(dir, options) {
78863
78921
  const thisFile = fileURLToPath6(import.meta.url);
78864
78922
  const repoRoot2 = resolve25(dirname19(thisFile), "..", "..", "..", "..");
78865
78923
  const projectsDir = join59(repoRoot2, "packages", "studio", "data", "projects");
78866
- const pName = options?.projectName ?? basename6(dir);
78924
+ const pName = options?.projectName ?? basename7(dir);
78867
78925
  const symlinkPath = join59(projectsDir, pName);
78868
78926
  mkdirSync30(projectsDir, { recursive: true });
78869
78927
  let createdSymlink = false;
@@ -78953,7 +79011,7 @@ function hasLocalStudio(dir) {
78953
79011
  async function runLocalStudioMode(dir, options) {
78954
79012
  const req = createRequire2(join59(dir, "package.json"));
78955
79013
  const studioPkgPath = dirname19(req.resolve("@hyperframes/studio/package.json"));
78956
- const pName = options?.projectName ?? basename6(dir);
79014
+ const pName = options?.projectName ?? basename7(dir);
78957
79015
  const projectsDir = join59(studioPkgPath, "data", "projects");
78958
79016
  const symlinkPath = join59(projectsDir, pName);
78959
79017
  mkdirSync30(projectsDir, { recursive: true });
@@ -79024,7 +79082,7 @@ async function runLocalStudioMode(dir, options) {
79024
79082
  }
79025
79083
  async function runEmbeddedMode(dir, startPort, options) {
79026
79084
  const { createStudioServer: createStudioServer2, loadPreviewServerBuildSignature: loadPreviewServerBuildSignature2, resolveStudioBundle: resolveStudioBundle2 } = await Promise.resolve().then(() => (init_studioServer(), studioServer_exports));
79027
- const pName = options?.projectName ?? basename6(dir);
79085
+ const pName = options?.projectName ?? basename7(dir);
79028
79086
  const studioBundle = resolveStudioBundle2();
79029
79087
  ge(c2.bold("hyperframes preview"));
79030
79088
  const s2 = ft();
@@ -79254,7 +79312,7 @@ var init_preview2 = __esm({
79254
79312
  const rawArg = args.dir;
79255
79313
  const dir = resolve25(rawArg ?? ".");
79256
79314
  const isImplicitCwd = !rawArg || rawArg === "." || rawArg === "./";
79257
- const projectName = isImplicitCwd ? basename6(process.env.PWD ?? dir) : basename6(dir);
79315
+ const projectName = isImplicitCwd ? basename7(process.env.PWD ?? dir) : basename7(dir);
79258
79316
  const indexPath = join59(dir, "index.html");
79259
79317
  if (existsSync49(indexPath)) {
79260
79318
  const project = { dir, name: projectName, indexPath };
@@ -79342,7 +79400,7 @@ import {
79342
79400
  readFileSync as readFileSync36,
79343
79401
  readdirSync as readdirSync21
79344
79402
  } from "fs";
79345
- import { resolve as resolve26, basename as basename7, join as join60, dirname as dirname20 } from "path";
79403
+ import { resolve as resolve26, basename as basename8, join as join60, dirname as dirname20 } from "path";
79346
79404
  import { fileURLToPath as fileURLToPath7 } from "url";
79347
79405
  import { execFileSync as execFileSync5, spawn as spawn12 } from "child_process";
79348
79406
  function probeVideo(filePath) {
@@ -79423,7 +79481,7 @@ function getSharedTemplateDir() {
79423
79481
  return resolveAssetDir(["..", "templates", "_shared"], ["templates", "_shared"]);
79424
79482
  }
79425
79483
  function toPackageName(projectName) {
79426
- const normalized = basename7(projectName).trim().toLowerCase().replace(/^[._]+/, "").replace(/[^a-z0-9._~-]+/g, "-").replace(/-+/g, "-").replace(/^[-.]+|[-.]+$/g, "");
79484
+ const normalized = basename8(projectName).trim().toLowerCase().replace(/^[._]+/, "").replace(/[^a-z0-9._~-]+/g, "-").replace(/-+/g, "-").replace(/^[-.]+|[-.]+$/g, "");
79427
79485
  return normalized || "hyperframes-project";
79428
79486
  }
79429
79487
  function getHyperframesPackageSpecifier() {
@@ -79534,7 +79592,7 @@ async function patchTranscript(dir, transcriptPath) {
79534
79592
  async function handleVideoFile(videoPath, destDir, interactive) {
79535
79593
  const probed = probeVideo(videoPath);
79536
79594
  let meta = { ...DEFAULT_META };
79537
- let localVideoName = basename7(videoPath);
79595
+ let localVideoName = basename8(videoPath);
79538
79596
  if (probed) {
79539
79597
  meta = probed;
79540
79598
  if (interactive) {
@@ -79884,8 +79942,8 @@ var init_init = __esm({
79884
79942
  process.exit(1);
79885
79943
  }
79886
79944
  sourceFilePath2 = audioPath;
79887
- copyFileSync5(audioPath, resolve26(destDir2, basename7(audioPath)));
79888
- console.log(`Audio: ${basename7(audioPath)}`);
79945
+ copyFileSync5(audioPath, resolve26(destDir2, basename8(audioPath)));
79946
+ console.log(`Audio: ${basename8(audioPath)}`);
79889
79947
  }
79890
79948
  if (sourceFilePath2 && !skipTranscribe) {
79891
79949
  try {
@@ -79909,7 +79967,7 @@ var init_init = __esm({
79909
79967
  try {
79910
79968
  await scaffoldProject(
79911
79969
  destDir2,
79912
- basename7(destDir2),
79970
+ basename8(destDir2),
79913
79971
  templateId2,
79914
79972
  localVideoName2,
79915
79973
  videoDuration2,
@@ -80016,8 +80074,8 @@ var init_init = __esm({
80016
80074
  }
80017
80075
  mkdirSync31(destDir, { recursive: true });
80018
80076
  sourceFilePath = audioPath;
80019
- copyFileSync5(audioPath, resolve26(destDir, basename7(audioPath)));
80020
- R2.info(`Audio copied to ${c2.accent(basename7(audioPath))}`);
80077
+ copyFileSync5(audioPath, resolve26(destDir, basename8(audioPath)));
80078
+ R2.info(`Audio copied to ${c2.accent(basename8(audioPath))}`);
80021
80079
  }
80022
80080
  if (sourceFilePath) {
80023
80081
  const transcribeChoice = await ue({
@@ -80579,10 +80637,10 @@ var init_format = __esm({
80579
80637
 
80580
80638
  // src/utils/project.ts
80581
80639
  import { existsSync as existsSync52, statSync as statSync18 } from "fs";
80582
- import { resolve as resolve29, basename as basename8 } from "path";
80640
+ import { resolve as resolve29, basename as basename9 } from "path";
80583
80641
  function resolveProject(dirArg) {
80584
80642
  const dir = resolve29(dirArg ?? ".");
80585
- const name = basename8(dir);
80643
+ const name = basename9(dir);
80586
80644
  const indexPath = resolve29(dir, "index.html");
80587
80645
  if (!existsSync52(dir) || !statSync18(dir).isDirectory()) {
80588
80646
  errorBox("Not a directory: " + dir);
@@ -80874,7 +80932,7 @@ var init_play = __esm({
80874
80932
  });
80875
80933
 
80876
80934
  // src/utils/publishProject.ts
80877
- import { basename as basename9, join as join61, relative as relative9 } from "path";
80935
+ import { basename as basename10, join as join61, relative as relative9 } from "path";
80878
80936
  import { readdirSync as readdirSync22, readFileSync as readFileSync38, statSync as statSync19 } from "fs";
80879
80937
  import AdmZip from "adm-zip";
80880
80938
  function isRecord2(value) {
@@ -81088,7 +81146,7 @@ async function publishProjectArchiveStaged(apiBaseUrl2, title, archive) {
81088
81146
  return publishedProject;
81089
81147
  }
81090
81148
  async function publishProjectArchive(projectDir) {
81091
- const title = basename9(projectDir);
81149
+ const title = basename10(projectDir);
81092
81150
  const archive = createPublishArchive(projectDir);
81093
81151
  const apiBaseUrl2 = getPublishApiBaseUrl();
81094
81152
  const stagedResult = await publishProjectArchiveStaged(apiBaseUrl2, title, archive);
@@ -81114,7 +81172,7 @@ __export(publish_exports, {
81114
81172
  default: () => publish_default,
81115
81173
  examples: () => examples6
81116
81174
  });
81117
- import { basename as basename10, resolve as resolve31 } from "path";
81175
+ import { basename as basename11, resolve as resolve31 } from "path";
81118
81176
  import { existsSync as existsSync54 } from "fs";
81119
81177
  import { join as join62 } from "path";
81120
81178
  var examples6, publish_default;
@@ -81150,7 +81208,7 @@ var init_publish = __esm({
81150
81208
  const rawArg = args.dir;
81151
81209
  const dir = resolve31(rawArg ?? ".");
81152
81210
  const isImplicitCwd = !rawArg || rawArg === "." || rawArg === "./";
81153
- const projectName = isImplicitCwd ? basename10(process.env["PWD"] ?? dir) : basename10(dir);
81211
+ const projectName = isImplicitCwd ? basename11(process.env["PWD"] ?? dir) : basename11(dir);
81154
81212
  const indexPath = join62(dir, "index.html");
81155
81213
  if (existsSync54(indexPath)) {
81156
81214
  const lintResult = await lintProject({ dir, name: projectName, indexPath });
@@ -81573,7 +81631,7 @@ __export(render_exports, {
81573
81631
  });
81574
81632
  import { mkdirSync as mkdirSync32, readdirSync as readdirSync23, readFileSync as readFileSync40, statSync as statSync20, writeFileSync as writeFileSync27, rmSync as rmSync14 } from "fs";
81575
81633
  import { cpus as cpus4, freemem as freemem4, tmpdir as tmpdir5 } from "os";
81576
- import { resolve as resolve33, dirname as dirname22, join as join63, basename as basename11 } from "path";
81634
+ import { resolve as resolve33, dirname as dirname22, join as join63, basename as basename12 } from "path";
81577
81635
  import { execFileSync as execFileSync6, spawn as spawn13 } from "child_process";
81578
81636
  function formatFpsParseError(input2, reason) {
81579
81637
  switch (reason) {
@@ -81677,7 +81735,7 @@ async function renderDocker(projectDir, outputPath, options) {
81677
81735
  process.exit(1);
81678
81736
  }
81679
81737
  const outputDir = dirname22(outputPath);
81680
- const outputFilename = basename11(outputPath);
81738
+ const outputFilename = basename12(outputPath);
81681
81739
  const dockerArgs = buildDockerRunArgs({
81682
81740
  imageTag,
81683
81741
  projectDir: resolve33(projectDir),
@@ -81734,6 +81792,14 @@ async function renderDocker(projectDir, outputPath, options) {
81734
81792
  }
81735
81793
  async function renderLocal(projectDir, outputPath, options) {
81736
81794
  const producer = await loadProducer();
81795
+ if (!findFFmpeg()) {
81796
+ errorBox(
81797
+ "FFmpeg not found",
81798
+ "FFmpeg is required to encode video. The render cannot proceed without it.",
81799
+ getFFmpegInstallHint()
81800
+ );
81801
+ process.exit(1);
81802
+ }
81737
81803
  const startTime = Date.now();
81738
81804
  if (options.browserPath && !process.env.PRODUCER_HEADLESS_SHELL_PATH) {
81739
81805
  process.env.PRODUCER_HEADLESS_SHELL_PATH = options.browserPath;
@@ -81760,7 +81826,14 @@ async function renderLocal(projectDir, outputPath, options) {
81760
81826
  try {
81761
81827
  await producer.executeRenderJob(job, projectDir, outputPath, onProgress);
81762
81828
  } catch (error) {
81763
- handleRenderError(error, options, startTime, false, "Try --docker for containerized rendering");
81829
+ handleRenderError(
81830
+ error,
81831
+ options,
81832
+ startTime,
81833
+ false,
81834
+ "Try --docker for containerized rendering",
81835
+ job.failedStage
81836
+ );
81764
81837
  }
81765
81838
  const elapsed = Date.now() - startTime;
81766
81839
  trackRenderMetrics(job, elapsed, options, false);
@@ -81784,7 +81857,7 @@ function getMemorySnapshot() {
81784
81857
  memoryFreeMb: bytesToMb(freemem4())
81785
81858
  };
81786
81859
  }
81787
- function handleRenderError(error, options, startTime, docker, hint) {
81860
+ function handleRenderError(error, options, startTime, docker, hint, failedStage) {
81788
81861
  const message = normalizeErrorMessage2(error);
81789
81862
  trackRenderError({
81790
81863
  fps: fpsToNumber(options.fps),
@@ -81794,6 +81867,7 @@ function handleRenderError(error, options, startTime, docker, hint) {
81794
81867
  gpu: options.gpu,
81795
81868
  elapsedMs: Date.now() - startTime,
81796
81869
  errorMessage: message,
81870
+ failedStage,
81797
81871
  ...getMemorySnapshot()
81798
81872
  });
81799
81873
  errorBox("Render failed", message, hint);
@@ -81884,6 +81958,7 @@ var init_render2 = __esm({
81884
81958
  init_env();
81885
81959
  init_dockerRunArgs();
81886
81960
  init_errorMessage2();
81961
+ init_ffmpeg();
81887
81962
  init_src();
81888
81963
  examples7 = [
81889
81964
  ["Render to MP4", "hyperframes render --output output.mp4"],
@@ -82184,17 +82259,6 @@ var init_render2 = __esm({
82184
82259
  }
82185
82260
  console.log("");
82186
82261
  }
82187
- if (!useDocker) {
82188
- const { findFFmpeg: findFFmpeg2, getFFmpegInstallHint: getFFmpegInstallHint2 } = await Promise.resolve().then(() => (init_ffmpeg(), ffmpeg_exports));
82189
- if (!findFFmpeg2()) {
82190
- errorBox(
82191
- "FFmpeg not found",
82192
- "Rendering requires FFmpeg for video encoding.",
82193
- `Install: ${getFFmpegInstallHint2()}`
82194
- );
82195
- process.exit(1);
82196
- }
82197
- }
82198
82262
  let browserPath;
82199
82263
  if (!useDocker) {
82200
82264
  const { ensureBrowser: ensureBrowser2 } = await Promise.resolve().then(() => (init_manager2(), manager_exports2));
@@ -84666,7 +84730,7 @@ __export(synthesize_exports, {
84666
84730
  });
84667
84731
  import { execFileSync as execFileSync7 } from "child_process";
84668
84732
  import { existsSync as existsSync63, writeFileSync as writeFileSync29, mkdirSync as mkdirSync35, readdirSync as readdirSync25, unlinkSync as unlinkSync6 } from "fs";
84669
- import { join as join70, dirname as dirname26, basename as basename12 } from "path";
84733
+ import { join as join70, dirname as dirname26, basename as basename13 } from "path";
84670
84734
  import { homedir as homedir11 } from "os";
84671
84735
  function findPython() {
84672
84736
  for (const name of ["python3", "python"]) {
@@ -84705,7 +84769,7 @@ function ensureSynthScript() {
84705
84769
  if (!existsSync63(SCRIPT_PATH)) {
84706
84770
  mkdirSync35(SCRIPT_DIR, { recursive: true });
84707
84771
  writeFileSync29(SCRIPT_PATH, SYNTH_SCRIPT);
84708
- const currentName = basename12(SCRIPT_PATH);
84772
+ const currentName = basename13(SCRIPT_PATH);
84709
84773
  try {
84710
84774
  for (const entry of readdirSync25(SCRIPT_DIR)) {
84711
84775
  if (entry !== currentName && /^synth(-v\d+)?\.py$/.test(entry)) {
@@ -85977,7 +86041,7 @@ __export(contactSheet_exports, {
85977
86041
  });
85978
86042
  import sharp from "sharp";
85979
86043
  import { readdirSync as readdirSync26, readFileSync as readFileSync48, writeFileSync as writeFileSync30, unlinkSync as unlinkSync7, existsSync as existsSync67 } from "fs";
85980
- import { join as join73, extname as extname13, basename as basename13, dirname as dirname29 } from "path";
86044
+ import { join as join73, extname as extname13, basename as basename14, dirname as dirname29 } from "path";
85981
86045
  async function createContactSheet(imagePaths, outputPath, opts = {}) {
85982
86046
  const {
85983
86047
  cols = 3,
@@ -86010,7 +86074,7 @@ async function createContactSheet(imagePaths, outputPath, opts = {}) {
86010
86074
  overlays.push({ input: resized, left: x3, top: y + labelH });
86011
86075
  let labelText = `${i2 + 1}`;
86012
86076
  if (labelMode === "filename") {
86013
- labelText = `${i2 + 1}. ${basename13(files[i2]).replace(extname13(files[i2]), "")}`;
86077
+ labelText = `${i2 + 1}. ${basename14(files[i2]).replace(extname13(files[i2]), "")}`;
86014
86078
  } else if (labelMode === "custom" && labels?.[i2]) {
86015
86079
  labelText = `${i2 + 1}. ${labels[i2]}`;
86016
86080
  }
@@ -93108,7 +93172,7 @@ var require_node_domexception = __commonJS({
93108
93172
 
93109
93173
  // ../../node_modules/.bun/fetch-blob@3.2.0/node_modules/fetch-blob/from.js
93110
93174
  import { statSync as statSync23, createReadStream as createReadStream2, promises as fs2 } from "fs";
93111
- import { basename as basename14 } from "path";
93175
+ import { basename as basename15 } from "path";
93112
93176
  var import_node_domexception, stat, blobFromSync, blobFrom, fileFrom, fileFromSync, fromBlob, fromFile, BlobDataItem;
93113
93177
  var init_from = __esm({
93114
93178
  "../../node_modules/.bun/fetch-blob@3.2.0/node_modules/fetch-blob/from.js"() {
@@ -93132,7 +93196,7 @@ var init_from = __esm({
93132
93196
  size: stat3.size,
93133
93197
  lastModified: stat3.mtimeMs,
93134
93198
  start: 0
93135
- })], basename14(path2), { type, lastModified: stat3.mtimeMs });
93199
+ })], basename15(path2), { type, lastModified: stat3.mtimeMs });
93136
93200
  BlobDataItem = class _BlobDataItem {
93137
93201
  #path;
93138
93202
  #start;
@@ -131866,6 +131930,57 @@ ${HELP}`);
131866
131930
  }
131867
131931
  });
131868
131932
 
131933
+ // src/cloud/detectAspectRatio.ts
131934
+ import { readFileSync as readFileSync58 } from "fs";
131935
+ function extractAttributeNumber(tag, re2) {
131936
+ const match = tag.match(re2);
131937
+ if (!match) return null;
131938
+ const raw = match[1] ?? match[2] ?? match[3];
131939
+ if (raw === void 0) return null;
131940
+ const value = Number(raw);
131941
+ return Number.isFinite(value) ? value : null;
131942
+ }
131943
+ function detectAspectRatioFromHtml(entryHtmlPath) {
131944
+ let html;
131945
+ try {
131946
+ html = readFileSync58(entryHtmlPath, "utf-8");
131947
+ } catch (err) {
131948
+ return { kind: "read-error", error: err instanceof Error ? err.message : String(err) };
131949
+ }
131950
+ return detectAspectRatioFromHtmlString(html);
131951
+ }
131952
+ function detectAspectRatioFromHtmlString(html) {
131953
+ const tagMatch = html.match(ROOT_COMPOSITION_DIV_RE);
131954
+ if (!tagMatch) return { kind: "no-root-div" };
131955
+ const openTag = tagMatch[0];
131956
+ const width = extractAttributeNumber(openTag, DATA_WIDTH_RE);
131957
+ const height = extractAttributeNumber(openTag, DATA_HEIGHT_RE);
131958
+ if (width === null || height === null) return { kind: "no-dims" };
131959
+ if (width <= 0 || height <= 0) return { kind: "invalid-dims", width, height };
131960
+ const ratio = width / height;
131961
+ for (const candidate of SUPPORTED_RATIOS) {
131962
+ if (Math.abs(ratio - candidate.ratio) <= RATIO_TOLERANCE) {
131963
+ return { kind: "matched", aspectRatio: candidate.value, width, height };
131964
+ }
131965
+ }
131966
+ return { kind: "no-match", width, height, ratio };
131967
+ }
131968
+ var RATIO_TOLERANCE, SUPPORTED_RATIOS, ROOT_COMPOSITION_DIV_RE, DATA_WIDTH_RE, DATA_HEIGHT_RE;
131969
+ var init_detectAspectRatio = __esm({
131970
+ "src/cloud/detectAspectRatio.ts"() {
131971
+ "use strict";
131972
+ RATIO_TOLERANCE = 0.05;
131973
+ SUPPORTED_RATIOS = [
131974
+ { value: "16:9", ratio: 16 / 9 },
131975
+ { value: "9:16", ratio: 9 / 16 },
131976
+ { value: "1:1", ratio: 1 }
131977
+ ];
131978
+ ROOT_COMPOSITION_DIV_RE = /<div\b[^>]*?\bdata-composition-id\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)[^>]*>/i;
131979
+ DATA_WIDTH_RE = /\bdata-width\s*=\s*(?:"(\d+(?:\.\d+)?)"|'(\d+(?:\.\d+)?)'|(\d+(?:\.\d+)?))(?=\s|>|\/)/i;
131980
+ DATA_HEIGHT_RE = /\bdata-height\s*=\s*(?:"(\d+(?:\.\d+)?)"|'(\d+(?:\.\d+)?)'|(\d+(?:\.\d+)?))(?=\s|>|\/)/i;
131981
+ }
131982
+ });
131983
+
131869
131984
  // src/cloud/poll.ts
131870
131985
  function isTerminal(status) {
131871
131986
  return TERMINAL_STATUSES.has(status);
@@ -133321,6 +133436,40 @@ function resolveProjectInput(opts) {
133321
133436
  if (explicit.url) return { kind: "url", url: opts.url };
133322
133437
  return { kind: "dir", dir: opts.dir ?? "." };
133323
133438
  }
133439
+ function maybeAutoDetectAspectRatio(project, compositionArg, asJson) {
133440
+ if (project.kind !== "dir") {
133441
+ const reason = project.kind === "asset_id" ? "--asset-id" : "--url";
133442
+ logDetection(asJson, `Auto-detect skipped (project is ${reason})`);
133443
+ return void 0;
133444
+ }
133445
+ const dir = project.dir ?? ".";
133446
+ const entryRelative = compositionArg ?? "index.html";
133447
+ const entryPath = resolvePath4(dir, entryRelative);
133448
+ const detection = detectAspectRatioFromHtml(entryPath);
133449
+ logDetection(asJson, summarizeDetection(detection, entryRelative));
133450
+ return detection.kind === "matched" ? detection.aspectRatio : void 0;
133451
+ }
133452
+ function logDetection(asJson, message) {
133453
+ if (asJson) return;
133454
+ const suffix = message.startsWith("Detected aspect ratio") ? "" : `; ${ASPECT_FALLBACK_HINT}`;
133455
+ console.log(c2.dim(` ${message}${suffix}`));
133456
+ }
133457
+ function summarizeDetection(detection, entryRelative) {
133458
+ switch (detection.kind) {
133459
+ case "matched":
133460
+ return `Detected aspect ratio: ${detection.aspectRatio} (from ${entryRelative} dims ${detection.width}\xD7${detection.height})`;
133461
+ case "no-root-div":
133462
+ return `No <div data-composition-id> found in ${entryRelative}`;
133463
+ case "no-dims":
133464
+ return `${entryRelative} root composition has no data-width / data-height`;
133465
+ case "invalid-dims":
133466
+ return `${entryRelative} root has invalid dims (${detection.width}\xD7${detection.height})`;
133467
+ case "no-match":
133468
+ return `${entryRelative} dims ${detection.width}\xD7${detection.height} (ratio ${detection.ratio.toFixed(2)}) don't match 16:9, 9:16, or 1:1`;
133469
+ case "read-error":
133470
+ return `Couldn't read ${entryRelative} for aspect-ratio detection (${detection.error})`;
133471
+ }
133472
+ }
133324
133473
  function resolveVariablesAndValidateIfLocal(inline, filePath, strict, source) {
133325
133474
  const variables = resolveVariablesArg(inline, filePath);
133326
133475
  if (!variables || Object.keys(variables).length === 0) return variables;
@@ -133398,6 +133547,7 @@ function buildRenderBody(opts) {
133398
133547
  if (opts.quality !== void 0) body.quality = opts.quality;
133399
133548
  if (opts.format !== void 0) body.format = opts.format;
133400
133549
  if (opts.resolution !== void 0) body.resolution = opts.resolution;
133550
+ if (opts.aspectRatio !== void 0) body.aspect_ratio = opts.aspectRatio;
133401
133551
  if (opts.composition !== void 0) body.composition = opts.composition;
133402
133552
  if (opts.variables !== void 0) body.variables = opts.variables;
133403
133553
  if (opts.title !== void 0) body.title = opts.title;
@@ -133490,11 +133640,12 @@ async function streamVideo(url, destPath, asJson) {
133490
133640
  process.exit(1);
133491
133641
  }
133492
133642
  }
133493
- var VALID_QUALITY2, VALID_FORMAT2, VALID_RESOLUTION, FORMAT_EXT2, examples26, render_default2, IDEMPOTENCY_KEY_RE;
133643
+ var VALID_QUALITY2, VALID_FORMAT2, VALID_RESOLUTION, VALID_ASPECT_RATIO, FORMAT_EXT2, examples26, render_default2, IDEMPOTENCY_KEY_RE, ASPECT_FALLBACK_HINT;
133494
133644
  var init_render4 = __esm({
133495
133645
  "src/commands/cloud/render.ts"() {
133496
133646
  "use strict";
133497
133647
  init_dist();
133648
+ init_detectAspectRatio();
133498
133649
  init_colors();
133499
133650
  init_format();
133500
133651
  init_project();
@@ -133507,14 +133658,8 @@ var init_render4 = __esm({
133507
133658
  init_statusColor();
133508
133659
  VALID_QUALITY2 = ["draft", "standard", "high"];
133509
133660
  VALID_FORMAT2 = ["mp4", "webm", "mov"];
133510
- VALID_RESOLUTION = [
133511
- "landscape",
133512
- "portrait",
133513
- "landscape-4k",
133514
- "portrait-4k",
133515
- "square",
133516
- "square-4k"
133517
- ];
133661
+ VALID_RESOLUTION = ["1080p", "4k"];
133662
+ VALID_ASPECT_RATIO = ["16:9", "9:16", "1:1"];
133518
133663
  FORMAT_EXT2 = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
133519
133664
  examples26 = [
133520
133665
  ["Render the current directory in the cloud", "hyperframes cloud render"],
@@ -133542,7 +133687,11 @@ var init_render4 = __esm({
133542
133687
  format: { type: "string", description: "mp4 | webm | mov (default: mp4)" },
133543
133688
  resolution: {
133544
133689
  type: "string",
133545
- description: "Resolution preset: landscape | portrait | landscape-4k | portrait-4k | square | square-4k"
133690
+ description: "Resolution tier: 1080p | 4k (default: 1080p; 4k is billed at 1.5x)"
133691
+ },
133692
+ "aspect-ratio": {
133693
+ type: "string",
133694
+ description: "Aspect ratio: 16:9 | 9:16 | 1:1 (default: 16:9)"
133546
133695
  },
133547
133696
  composition: {
133548
133697
  type: "string",
@@ -133625,6 +133774,9 @@ var init_render4 = __esm({
133625
133774
  const resolution = parseEnumFlag(args.resolution, VALID_RESOLUTION, {
133626
133775
  flag: "--resolution"
133627
133776
  });
133777
+ const explicitAspectRatio = parseEnumFlag(args["aspect-ratio"], VALID_ASPECT_RATIO, {
133778
+ flag: "--aspect-ratio"
133779
+ });
133628
133780
  const pollIntervalMs = parsePollIntervalMs(args["poll-interval"]);
133629
133781
  const maxWaitMs = parseMaxWaitMs(args["max-wait"]);
133630
133782
  validateIdempotencyKey(args["idempotency-key"]);
@@ -133633,6 +133785,7 @@ var init_render4 = __esm({
133633
133785
  assetId: args["asset-id"],
133634
133786
  url: args.url
133635
133787
  });
133788
+ const aspectRatio = explicitAspectRatio ?? maybeAutoDetectAspectRatio(project, args.composition, asJson);
133636
133789
  const variables = resolveVariablesAndValidateIfLocal(
133637
133790
  args.variables,
133638
133791
  args["variables-file"],
@@ -133647,6 +133800,7 @@ var init_render4 = __esm({
133647
133800
  quality,
133648
133801
  format,
133649
133802
  resolution,
133803
+ aspectRatio,
133650
133804
  composition: args.composition,
133651
133805
  variables,
133652
133806
  title: args.title,
@@ -133707,6 +133861,7 @@ var init_render4 = __esm({
133707
133861
  }
133708
133862
  });
133709
133863
  IDEMPOTENCY_KEY_RE = /^[A-Za-z0-9_:.-]{1,255}$/;
133864
+ ASPECT_FALLBACK_HINT = "server will default aspect_ratio to 16:9. Pass --aspect-ratio to override.";
133710
133865
  }
133711
133866
  });
133712
133867
 
@@ -135091,10 +135246,10 @@ if (rootVersionRequested) {
135091
135246
  process.exit(0);
135092
135247
  }
135093
135248
  try {
135094
- const { readFileSync: readFileSync58 } = await import("fs");
135249
+ const { readFileSync: readFileSync59 } = await import("fs");
135095
135250
  const { resolve: resolve46 } = await import("path");
135096
135251
  const envPath = resolve46(process.cwd(), ".env");
135097
- const envContent = readFileSync58(envPath, "utf-8");
135252
+ const envContent = readFileSync59(envPath, "utf-8");
135098
135253
  for (const rawLine of envContent.split("\n")) {
135099
135254
  let line = rawLine.trim();
135100
135255
  if (!line || line.startsWith("#")) continue;