hyperframes 0.4.39 → 0.4.41

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.39" : "0.0.0-dev";
57
+ VERSION = true ? "0.4.41" : "0.0.0-dev";
58
58
  }
59
59
  });
60
60
 
@@ -10206,8 +10206,8 @@ function trackRenderError(props) {
10206
10206
  memory_free_mb: props.memoryFreeMb
10207
10207
  });
10208
10208
  }
10209
- function trackInitTemplate(templateId) {
10210
- trackEvent("init_template", { template: templateId });
10209
+ function trackInitTemplate(templateId, props) {
10210
+ trackEvent("init_template", { template: templateId, tailwind: props?.tailwind });
10211
10211
  }
10212
10212
  function trackBrowserInstall() {
10213
10213
  trackEvent("browser_install", {});
@@ -25516,6 +25516,13 @@ function resolveConfig(overrides) {
25516
25516
  "PRODUCER_ENABLE_STREAMING_ENCODE",
25517
25517
  DEFAULT_CONFIG2.enableStreamingEncode
25518
25518
  ),
25519
+ streamingEncodeMaxDurationSeconds: Math.max(
25520
+ 0,
25521
+ envNum(
25522
+ "PRODUCER_STREAMING_ENCODE_MAX_DURATION_SECONDS",
25523
+ DEFAULT_CONFIG2.streamingEncodeMaxDurationSeconds
25524
+ )
25525
+ ),
25519
25526
  ffmpegEncodeTimeout: envNum("FFMPEG_ENCODE_TIMEOUT_MS", DEFAULT_CONFIG2.ffmpegEncodeTimeout),
25520
25527
  ffmpegProcessTimeout: envNum("FFMPEG_PROCESS_TIMEOUT_MS", DEFAULT_CONFIG2.ffmpegProcessTimeout),
25521
25528
  ffmpegStreamingTimeout: envNum(
@@ -25573,7 +25580,8 @@ var init_config2 = __esm({
25573
25580
  forceScreenshot: false,
25574
25581
  enableChunkedEncode: false,
25575
25582
  chunkSizeFrames: 360,
25576
- enableStreamingEncode: false,
25583
+ enableStreamingEncode: true,
25584
+ streamingEncodeMaxDurationSeconds: 240,
25577
25585
  ffmpegEncodeTimeout: 6e5,
25578
25586
  ffmpegProcessTimeout: 3e5,
25579
25587
  ffmpegStreamingTimeout: 6e5,
@@ -26182,6 +26190,44 @@ async function pollPageExpression(page, expression, timeoutMs, intervalMs = 100)
26182
26190
  }
26183
26191
  return Boolean(await page.evaluate(expression));
26184
26192
  }
26193
+ async function applyVideoMetadataHints(page, hints) {
26194
+ if (!hints || hints.length === 0) return;
26195
+ await page.evaluate(
26196
+ (metadataHints) => {
26197
+ for (const hint of metadataHints) {
26198
+ if (!hint.id || !Number.isFinite(hint.width) || !Number.isFinite(hint.height) || hint.width <= 0 || hint.height <= 0) {
26199
+ continue;
26200
+ }
26201
+ const video = document.getElementById(hint.id);
26202
+ if (!video) continue;
26203
+ if (!video.hasAttribute("width")) video.setAttribute("width", String(hint.width));
26204
+ if (!video.hasAttribute("height")) video.setAttribute("height", String(hint.height));
26205
+ const computed = window.getComputedStyle(video);
26206
+ if (!video.style.aspectRatio && (!computed.aspectRatio || computed.aspectRatio === "auto")) {
26207
+ video.style.aspectRatio = `${hint.width} / ${hint.height}`;
26208
+ }
26209
+ }
26210
+ },
26211
+ [...hints]
26212
+ );
26213
+ }
26214
+ async function waitForOptionalTailwindReady(page, timeoutMs) {
26215
+ const hasTailwindReady = await page.evaluate(
26216
+ `(() => { const ready = window.__tailwindReady; return !!ready && typeof ready.then === "function"; })()`
26217
+ );
26218
+ if (!hasTailwindReady) return;
26219
+ const ready = await Promise.race([
26220
+ page.evaluate(
26221
+ `Promise.resolve(window.__tailwindReady).then(() => true, () => false)`
26222
+ ),
26223
+ new Promise((resolve39) => setTimeout(() => resolve39(false), timeoutMs))
26224
+ ]);
26225
+ if (!ready) {
26226
+ throw new Error(
26227
+ `[FrameCapture] window.__tailwindReady not resolved after ${timeoutMs}ms. Tailwind browser runtime must finish before frame capture starts.`
26228
+ );
26229
+ }
26230
+ }
26185
26231
  async function initializeSession(session) {
26186
26232
  const { page, serverUrl } = session;
26187
26233
  page.on("console", (msg) => {
@@ -26225,6 +26271,7 @@ async function initializeSession(session) {
26225
26271
  `[FrameCapture] window.__hf not ready after ${pageReadyTimeout2}ms. Page must expose window.__hf = { duration, seek }.`
26226
26272
  );
26227
26273
  }
26274
+ await applyVideoMetadataHints(page, session.options.videoMetadataHints);
26228
26275
  const skipIdsLiteral = JSON.stringify(session.options.skipReadinessVideoIds ?? []);
26229
26276
  const videosReady = await pollPageExpression(
26230
26277
  page,
@@ -26237,6 +26284,7 @@ async function initializeSession(session) {
26237
26284
  );
26238
26285
  }
26239
26286
  await page.evaluate(`document.fonts?.ready`);
26287
+ await waitForOptionalTailwindReady(page, pageReadyTimeout2);
26240
26288
  if (session.options.format === "png") {
26241
26289
  await initTransparentBackground(session.page);
26242
26290
  }
@@ -26291,6 +26339,7 @@ async function initializeSession(session) {
26291
26339
  `[FrameCapture] window.__hf not ready after ${pageReadyTimeout}ms. Page must expose window.__hf = { duration, seek }.`
26292
26340
  );
26293
26341
  }
26342
+ await applyVideoMetadataHints(page, session.options.videoMetadataHints);
26294
26343
  const beginframeSkipIdsLiteral = JSON.stringify(session.options.skipReadinessVideoIds ?? []);
26295
26344
  const videoDeadline = Date.now() + (session.config?.playerReadyTimeout ?? DEFAULT_CONFIG2.playerReadyTimeout);
26296
26345
  while (Date.now() < videoDeadline) {
@@ -26301,6 +26350,7 @@ async function initializeSession(session) {
26301
26350
  await new Promise((r2) => setTimeout(r2, 100));
26302
26351
  }
26303
26352
  await page.evaluate(`document.fonts?.ready`);
26353
+ await waitForOptionalTailwindReady(page, pageReadyTimeout);
26304
26354
  warmupRunning = false;
26305
26355
  session.beginFrameTimeTicks = (warmupTicks + 10) * session.beginFrameIntervalMs;
26306
26356
  if (session.options.format === "png") {
@@ -34108,6 +34158,24 @@ function applyRenderModeHints(cfg, compiled, log2 = defaultLogger) {
34108
34158
  reasons: compiled.renderModeHints.reasons.map((reason) => reason.message)
34109
34159
  });
34110
34160
  }
34161
+ function collectVideoReadinessSkipIds(nativeHdrVideoIds, extractedVideos) {
34162
+ return Array.from(
34163
+ /* @__PURE__ */ new Set([
34164
+ ...nativeHdrVideoIds,
34165
+ ...extractedVideos.filter((video) => hasUsableVideoDimensions(video.metadata)).map((video) => video.videoId)
34166
+ ])
34167
+ ).sort();
34168
+ }
34169
+ function hasUsableVideoDimensions(metadata) {
34170
+ return Number.isFinite(metadata.width) && Number.isFinite(metadata.height) && metadata.width > 0 && metadata.height > 0;
34171
+ }
34172
+ function collectVideoMetadataHints(extractedVideos) {
34173
+ return extractedVideos.filter((video) => hasUsableVideoDimensions(video.metadata)).map((video) => ({
34174
+ id: video.videoId,
34175
+ width: video.metadata.width,
34176
+ height: video.metadata.height
34177
+ })).sort((a, b) => a.id.localeCompare(b.id));
34178
+ }
34111
34179
  function resolveRenderWorkerCount(totalFrames, requestedWorkers, cfg, compiled, composition, log2 = defaultLogger, measuredCaptureCost) {
34112
34180
  const captureCost = combineCaptureCostEstimates(
34113
34181
  estimateCaptureCostMultiplier(compiled, composition),
@@ -34770,6 +34838,27 @@ function createRenderJob(config) {
34770
34838
  function normalizeCompositionSrcPath(srcPath) {
34771
34839
  return srcPath.replace(/\\/g, "/").replace(/^\.\//, "");
34772
34840
  }
34841
+ function createStandaloneEntryRenderClone(root, host) {
34842
+ const hostClone = host.cloneNode(true);
34843
+ hostClone.setAttribute("data-start", "0");
34844
+ if (root === host) return hostClone;
34845
+ const rootClone = root.cloneNode(false);
34846
+ rootClone.appendChild(hostClone);
34847
+ return rootClone;
34848
+ }
34849
+ function replaceBodyWithRenderClone(body, renderClone) {
34850
+ while (body.firstChild) {
34851
+ body.removeChild(body.firstChild);
34852
+ }
34853
+ body.appendChild(renderClone);
34854
+ }
34855
+ function shouldUseStreamingEncode(cfg, outputFormat, workerCount, durationSeconds) {
34856
+ if (!cfg.enableStreamingEncode) return false;
34857
+ if (outputFormat === "png-sequence") return false;
34858
+ if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) return false;
34859
+ if (durationSeconds > cfg.streamingEncodeMaxDurationSeconds) return false;
34860
+ return workerCount === 1;
34861
+ }
34773
34862
  function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
34774
34863
  const normalizedEntryFile = normalizeCompositionSrcPath(entryFile);
34775
34864
  const { document: document2 } = parseHTML(indexHtml);
@@ -34784,16 +34873,8 @@ function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
34784
34873
  (candidate) => candidate.hasAttribute("data-composition-id")
34785
34874
  ) ?? null;
34786
34875
  if (!root) return null;
34787
- const hostClone = host.cloneNode(true);
34788
- hostClone.setAttribute("data-start", "0");
34789
- body.innerHTML = "";
34790
- if (root === host) {
34791
- body.appendChild(hostClone);
34792
- return document2.toString();
34793
- }
34794
- const rootClone = root.cloneNode(false);
34795
- rootClone.appendChild(hostClone);
34796
- body.appendChild(rootClone);
34876
+ const renderClone = createStandaloneEntryRenderClone(root, host);
34877
+ replaceBodyWithRenderClone(body, renderClone);
34797
34878
  return document2.toString();
34798
34879
  }
34799
34880
  async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSignal) {
@@ -34825,7 +34906,6 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
34825
34906
  }
34826
34907
  const enableChunkedEncode = cfg.enableChunkedEncode;
34827
34908
  const chunkedEncodeSize = cfg.chunkSizeFrames;
34828
- const enableStreamingEncode = cfg.enableStreamingEncode && !isPngSequence;
34829
34909
  let peakRssBytes = 0;
34830
34910
  let peakHeapUsedBytes = 0;
34831
34911
  const sampleMemory = () => {
@@ -35119,6 +35199,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35119
35199
  let frameLookup = null;
35120
35200
  const compiledDir = join35(workDir, "compiled");
35121
35201
  let extractionResult = null;
35202
+ let videoReadinessSkipIds = [];
35203
+ let videoMetadataHints = [];
35122
35204
  const nativeHdrVideoIds = /* @__PURE__ */ new Set();
35123
35205
  const videoTransfers = /* @__PURE__ */ new Map();
35124
35206
  if (job.config.hdrMode !== "force-sdr" && composition.videos.length > 0) {
@@ -35175,6 +35257,11 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35175
35257
  if (extractionResult.extracted.length > 0) {
35176
35258
  frameLookup = createFrameLookupTable(composition.videos, extractionResult.extracted);
35177
35259
  }
35260
+ videoReadinessSkipIds = collectVideoReadinessSkipIds(
35261
+ nativeHdrVideoIds,
35262
+ extractionResult.extracted
35263
+ );
35264
+ videoMetadataHints = collectVideoMetadataHints(extractionResult.extracted);
35178
35265
  perfStages.videoExtractMs = Date.now() - stage2Start;
35179
35266
  const existingAudioSrcs = new Set(composition.audios.map((a) => a.src));
35180
35267
  for (const ext of extractionResult.extracted) {
@@ -35288,9 +35375,10 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35288
35375
  format: needsAlpha ? "png" : "jpeg",
35289
35376
  quality: needsAlpha ? void 0 : job.config.quality === "draft" ? 80 : 95
35290
35377
  };
35291
- const buildHdrCaptureOptions = () => ({
35378
+ const buildCaptureOptions = () => ({
35292
35379
  ...captureOptions,
35293
- skipReadinessVideoIds: Array.from(nativeHdrVideoIds)
35380
+ videoMetadataHints,
35381
+ skipReadinessVideoIds: videoReadinessSkipIds
35294
35382
  });
35295
35383
  let captureCalibration;
35296
35384
  let switchedToScreenshotAfterCalibration = false;
@@ -35303,7 +35391,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35303
35391
  calibrationSession = await createCaptureSession(
35304
35392
  fileServer.url,
35305
35393
  calibrationDir,
35306
- buildHdrCaptureOptions(),
35394
+ buildCaptureOptions(),
35307
35395
  videoInjector,
35308
35396
  calibrationCfg
35309
35397
  );
@@ -35386,6 +35474,15 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35386
35474
  await closeCaptureSession(probeSession);
35387
35475
  probeSession = null;
35388
35476
  }
35477
+ let useStreamingEncode = shouldUseStreamingEncode(cfg, outputFormat, workerCount, job.duration);
35478
+ log2.info("streaming-encode gate", {
35479
+ enabled: useStreamingEncode,
35480
+ configFlag: cfg.enableStreamingEncode,
35481
+ outputFormat,
35482
+ workerCount,
35483
+ durationSeconds: job.duration,
35484
+ maxDurationSeconds: cfg.streamingEncodeMaxDurationSeconds
35485
+ });
35389
35486
  const captureAttempts = [];
35390
35487
  const FORMAT_EXT2 = {
35391
35488
  mp4: ".mp4",
@@ -35427,7 +35524,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35427
35524
  const domSession = await createCaptureSession(
35428
35525
  fileServer.url,
35429
35526
  framesDir,
35430
- buildHdrCaptureOptions(),
35527
+ buildCaptureOptions(),
35431
35528
  createVideoFrameInjector(frameLookup),
35432
35529
  cfg
35433
35530
  );
@@ -35925,28 +36022,47 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35925
36022
  } else {
35926
36023
  let streamingEncoder = null;
35927
36024
  let streamingEncoderClosed = false;
35928
- if (enableStreamingEncode) {
35929
- streamingEncoder = await spawnStreamingEncoder(
35930
- videoOnlyPath,
35931
- {
35932
- fps: job.config.fps,
35933
- width,
35934
- height,
35935
- codec: preset.codec,
35936
- preset: preset.preset,
35937
- quality: effectiveQuality,
35938
- bitrate: effectiveBitrate,
35939
- pixelFormat: preset.pixelFormat,
35940
- useGpu: job.config.useGpu,
35941
- imageFormat: captureOptions.format || "jpeg",
35942
- hdr: preset.hdr
35943
- },
35944
- abortSignal
35945
- );
35946
- assertNotAborted();
36025
+ if (useStreamingEncode) {
36026
+ try {
36027
+ streamingEncoder = await spawnStreamingEncoder(
36028
+ videoOnlyPath,
36029
+ {
36030
+ fps: job.config.fps,
36031
+ width,
36032
+ height,
36033
+ codec: preset.codec,
36034
+ preset: preset.preset,
36035
+ quality: effectiveQuality,
36036
+ bitrate: effectiveBitrate,
36037
+ pixelFormat: preset.pixelFormat,
36038
+ useGpu: job.config.useGpu,
36039
+ imageFormat: captureOptions.format || "jpeg",
36040
+ hdr: preset.hdr
36041
+ },
36042
+ abortSignal
36043
+ );
36044
+ assertNotAborted();
36045
+ } catch (err) {
36046
+ if (abortSignal?.aborted) {
36047
+ if (streamingEncoder && !streamingEncoderClosed) {
36048
+ await streamingEncoder.close().catch(() => {
36049
+ });
36050
+ streamingEncoderClosed = true;
36051
+ }
36052
+ throw err;
36053
+ }
36054
+ useStreamingEncode = false;
36055
+ streamingEncoder = null;
36056
+ log2.warn("[Render] Streaming encoder spawn failed; falling back to disk-frame encode.", {
36057
+ error: err instanceof Error ? err.message : String(err),
36058
+ outputFormat,
36059
+ workerCount,
36060
+ durationSeconds: job.duration
36061
+ });
36062
+ }
35947
36063
  }
35948
36064
  try {
35949
- if (enableStreamingEncode && streamingEncoder) {
36065
+ if (useStreamingEncode && streamingEncoder) {
35950
36066
  const reorderBuffer = createFrameReorderBuffer(0, totalFrames);
35951
36067
  const currentEncoder = streamingEncoder;
35952
36068
  if (workerCount > 1) {
@@ -35960,7 +36076,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35960
36076
  fileServer.url,
35961
36077
  workDir,
35962
36078
  tasks,
35963
- buildHdrCaptureOptions(),
36079
+ buildCaptureOptions(),
35964
36080
  () => createVideoFrameInjector(frameLookup),
35965
36081
  abortSignal,
35966
36082
  (progress) => {
@@ -35990,7 +36106,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35990
36106
  const session = probeSession ?? await createCaptureSession(
35991
36107
  fileServer.url,
35992
36108
  framesDir,
35993
- buildHdrCaptureOptions(),
36109
+ buildCaptureOptions(),
35994
36110
  videoInjector,
35995
36111
  cfg
35996
36112
  );
@@ -36045,7 +36161,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
36045
36161
  initialWorkerCount: workerCount,
36046
36162
  allowRetry: job.config.workers === void 0,
36047
36163
  frameExt: needsAlpha ? "png" : "jpg",
36048
- captureOptions: buildHdrCaptureOptions(),
36164
+ captureOptions: buildCaptureOptions(),
36049
36165
  createBeforeCaptureHook: () => createVideoFrameInjector(frameLookup),
36050
36166
  abortSignal,
36051
36167
  onProgress: (progress) => {
@@ -36080,7 +36196,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
36080
36196
  const session = probeSession ?? await createCaptureSession(
36081
36197
  fileServer.url,
36082
36198
  framesDir,
36083
- buildHdrCaptureOptions(),
36199
+ buildCaptureOptions(),
36084
36200
  videoInjector,
36085
36201
  cfg
36086
36202
  );
@@ -37784,7 +37900,8 @@ var init_preview2 = __esm({
37784
37900
  var init_exports = {};
37785
37901
  __export(init_exports, {
37786
37902
  default: () => init_default,
37787
- examples: () => examples2
37903
+ examples: () => examples2,
37904
+ injectTailwindBrowserScript: () => injectTailwindBrowserScript
37788
37905
  });
37789
37906
  import {
37790
37907
  existsSync as existsSync38,
@@ -37875,6 +37992,92 @@ function getStaticTemplateDir(templateId) {
37875
37992
  function getSharedTemplateDir() {
37876
37993
  return resolveAssetDir(["..", "templates", "_shared"], ["templates", "_shared"]);
37877
37994
  }
37995
+ function toPackageName(projectName) {
37996
+ const normalized = basename5(projectName).trim().toLowerCase().replace(/^[._]+/, "").replace(/[^a-z0-9._~-]+/g, "-").replace(/-+/g, "-").replace(/^[-.]+|[-.]+$/g, "");
37997
+ return normalized || "hyperframes-project";
37998
+ }
37999
+ function getHyperframesPackageSpecifier() {
38000
+ return VERSION === "0.0.0-dev" ? "hyperframes" : `hyperframes@${VERSION}`;
38001
+ }
38002
+ function hyperframesScript(command2) {
38003
+ return `npx --yes ${getHyperframesPackageSpecifier()} ${command2}`;
38004
+ }
38005
+ function buildPackageScripts() {
38006
+ return {
38007
+ dev: hyperframesScript("preview"),
38008
+ check: `${hyperframesScript("lint")} && ${hyperframesScript("validate")} && ${hyperframesScript("inspect")}`,
38009
+ render: hyperframesScript("render"),
38010
+ publish: hyperframesScript("publish")
38011
+ };
38012
+ }
38013
+ function writeDefaultPackageJson(destDir, projectName) {
38014
+ const packageJsonPath = resolve22(destDir, "package.json");
38015
+ if (existsSync38(packageJsonPath)) return;
38016
+ writeFileSync18(
38017
+ packageJsonPath,
38018
+ `${JSON.stringify(
38019
+ {
38020
+ name: toPackageName(projectName),
38021
+ private: true,
38022
+ type: "module",
38023
+ scripts: buildPackageScripts()
38024
+ },
38025
+ null,
38026
+ 2
38027
+ )}
38028
+ `,
38029
+ "utf-8"
38030
+ );
38031
+ }
38032
+ function listHtmlFiles(dir) {
38033
+ const files = [];
38034
+ const ignoredDirs = /* @__PURE__ */ new Set([".git", "dist", "node_modules"]);
38035
+ function walk(currentDir) {
38036
+ for (const entry of readdirSync13(currentDir, { withFileTypes: true })) {
38037
+ const entryPath = join40(currentDir, entry.name);
38038
+ if (entry.isDirectory()) {
38039
+ if (!ignoredDirs.has(entry.name)) walk(entryPath);
38040
+ continue;
38041
+ }
38042
+ if (entry.isFile() && entry.name.endsWith(".html")) {
38043
+ files.push(entryPath);
38044
+ }
38045
+ }
38046
+ }
38047
+ walk(dir);
38048
+ return files;
38049
+ }
38050
+ function injectTailwindBrowserScript(html) {
38051
+ if (html.includes(TAILWIND_BROWSER_SRC)) return html;
38052
+ const script = [
38053
+ `<script>`,
38054
+ `window.__tailwindReady=new Promise(function(resolve){`,
38055
+ `var loaded=document.readyState==="complete";`,
38056
+ `var resolved=false;`,
38057
+ `var observer;`,
38058
+ `function readTailwindCss(){var styles=document.querySelectorAll("style");for(var i=styles.length-1;i>=0;i--){var text=styles[i].textContent||"";if(text.indexOf("tailwindcss v")!==-1)return text;}return "";}`,
38059
+ `function finish(){if(resolved||!loaded||!readTailwindCss())return;resolved=true;if(observer)observer.disconnect();resolve(true);}`,
38060
+ `observer=new MutationObserver(finish);`,
38061
+ `observer.observe(document.documentElement,{childList:true,subtree:true,characterData:true});`,
38062
+ `if(loaded){finish();}else{window.addEventListener("load",function(){loaded=true;finish();},{once:true});}`,
38063
+ `});`,
38064
+ `</script>`,
38065
+ `<script src="${TAILWIND_BROWSER_SRC}" integrity="${TAILWIND_BROWSER_INTEGRITY}" crossorigin="anonymous"></script>`
38066
+ ].join("\n");
38067
+ if (/<\/head>/i.test(html)) {
38068
+ return html.replace(/<\/head>/i, (closingHead) => `
38069
+ ${script}
38070
+ ${closingHead}`);
38071
+ }
38072
+ return `${script}
38073
+ ${html}`;
38074
+ }
38075
+ function writeTailwindSupport(destDir) {
38076
+ for (const file of listHtmlFiles(destDir)) {
38077
+ const html = readFileSync27(file, "utf-8");
38078
+ writeFileSync18(file, injectTailwindBrowserScript(html), "utf-8");
38079
+ }
38080
+ }
37878
38081
  function patchVideoSrc(dir, videoFilename, durationSeconds) {
37879
38082
  const htmlFiles = readdirSync13(dir, { withFileTypes: true, recursive: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".html")).map((e2) => join40(e2.parentPath ?? e2.path, e2.name));
37880
38083
  for (const file of htmlFiles) {
@@ -37980,7 +38183,7 @@ async function handleVideoFile(videoPath, destDir, interactive) {
37980
38183
  }
37981
38184
  return { meta, localVideoName };
37982
38185
  }
37983
- async function scaffoldProject(destDir, name, templateId, localVideoName, durationSeconds) {
38186
+ async function scaffoldProject(destDir, name, templateId, localVideoName, durationSeconds, tailwind = false) {
37984
38187
  mkdirSync23(destDir, { recursive: true });
37985
38188
  const templateDir = getStaticTemplateDir(templateId);
37986
38189
  if (existsSync38(templateDir)) {
@@ -37989,6 +38192,7 @@ async function scaffoldProject(destDir, name, templateId, localVideoName, durati
37989
38192
  await fetchRemoteTemplate(templateId, destDir);
37990
38193
  }
37991
38194
  patchVideoSrc(destDir, localVideoName, durationSeconds);
38195
+ if (tailwind) writeTailwindSupport(destDir);
37992
38196
  writeFileSync18(
37993
38197
  resolve22(destDir, "meta.json"),
37994
38198
  JSON.stringify(
@@ -38006,6 +38210,7 @@ async function scaffoldProject(destDir, name, templateId, localVideoName, durati
38006
38210
  const { writeProjectConfig: writeProjectConfig2, DEFAULT_PROJECT_CONFIG: DEFAULT_PROJECT_CONFIG2 } = await Promise.resolve().then(() => (init_projectConfig(), projectConfig_exports));
38007
38211
  writeProjectConfig2(destDir, DEFAULT_PROJECT_CONFIG2);
38008
38212
  }
38213
+ writeDefaultPackageJson(destDir, name);
38009
38214
  const sharedDir = getSharedTemplateDir();
38010
38215
  if (existsSync38(sharedDir)) {
38011
38216
  for (const entry of readdirSync13(sharedDir, { withFileTypes: true })) {
@@ -38017,7 +38222,7 @@ async function scaffoldProject(destDir, name, templateId, localVideoName, durati
38017
38222
  }
38018
38223
  }
38019
38224
  }
38020
- var examples2, WEB_CODECS, DEFAULT_META, init_default;
38225
+ var examples2, WEB_CODECS, DEFAULT_META, TAILWIND_BROWSER_VERSION, TAILWIND_BROWSER_SRC, TAILWIND_BROWSER_INTEGRITY, init_default;
38021
38226
  var init_init = __esm({
38022
38227
  "src/commands/init.ts"() {
38023
38228
  "use strict";
@@ -38029,11 +38234,13 @@ var init_init = __esm({
38029
38234
  init_remote2();
38030
38235
  init_events();
38031
38236
  init_manager();
38237
+ init_version();
38032
38238
  examples2 = [
38033
38239
  ["Create a project with the interactive wizard", "hyperframes init my-video"],
38034
38240
  ["Pick a starter example", "hyperframes init my-video --example warm-grain"],
38035
38241
  ["Start from an existing video file", "hyperframes init my-video --video clip.mp4"],
38036
38242
  ["Start from an audio file", "hyperframes init my-video --audio track.mp3"],
38243
+ ["Scaffold with Tailwind CSS", "hyperframes init my-video --example blank --tailwind"],
38037
38244
  ["Non-interactive mode (for CI or AI agents)", "hyperframes init my-video --non-interactive"],
38038
38245
  ["Skip AI coding skills installation", "hyperframes init my-video --skip-skills"]
38039
38246
  ];
@@ -38046,6 +38253,9 @@ var init_init = __esm({
38046
38253
  hasAudio: false,
38047
38254
  videoCodec: "h264"
38048
38255
  };
38256
+ TAILWIND_BROWSER_VERSION = "4.2.4";
38257
+ TAILWIND_BROWSER_SRC = `https://cdn.jsdelivr.net/npm/@tailwindcss/browser@${TAILWIND_BROWSER_VERSION}/dist/index.global.js`;
38258
+ TAILWIND_BROWSER_INTEGRITY = "sha384-v5YF9xS+gLRWdvrQ0u/WRbCkjSIH0NjHIPe8tBL1ZRrmI7PiSH6LLdzs0aAIMCuh";
38049
38259
  init_default = defineCommand({
38050
38260
  meta: {
38051
38261
  name: "init",
@@ -38098,6 +38308,10 @@ var init_init = __esm({
38098
38308
  "skip-skills": {
38099
38309
  type: "boolean",
38100
38310
  description: "Skip AI coding skills installation"
38311
+ },
38312
+ tailwind: {
38313
+ type: "boolean",
38314
+ description: "Add Tailwind CSS browser-runtime support"
38101
38315
  }
38102
38316
  },
38103
38317
  async run({ args }) {
@@ -38115,6 +38329,7 @@ var init_init = __esm({
38115
38329
  const audioFlag = args.audio;
38116
38330
  const skipTranscribe = args["skip-transcribe"] === true;
38117
38331
  const skipSkills = args["skip-skills"] === true;
38332
+ const tailwind = args.tailwind === true;
38118
38333
  const nonInteractive = args["non-interactive"] === true;
38119
38334
  const modelFlag = args.model;
38120
38335
  const languageFlag = args.language;
@@ -38184,7 +38399,8 @@ var init_init = __esm({
38184
38399
  basename5(destDir2),
38185
38400
  templateId2,
38186
38401
  localVideoName2,
38187
- videoDuration2
38402
+ videoDuration2,
38403
+ tailwind
38188
38404
  );
38189
38405
  } catch (err) {
38190
38406
  console.error(
@@ -38195,7 +38411,7 @@ var init_init = __esm({
38195
38411
  console.error(c.dim("Use --example blank for offline use."));
38196
38412
  process.exit(1);
38197
38413
  }
38198
- trackInitTemplate(templateId2);
38414
+ trackInitTemplate(templateId2, { tailwind });
38199
38415
  const transcriptFile2 = resolve22(destDir2, "transcript.json");
38200
38416
  if (existsSync38(transcriptFile2)) {
38201
38417
  await patchTranscript(destDir2, transcriptFile2);
@@ -38222,10 +38438,13 @@ var init_init = __esm({
38222
38438
  console.log(` ${c.dim("More patterns: hyperframes.heygen.com/guides/prompting")}`);
38223
38439
  console.log();
38224
38440
  console.log(` ${c.accent("4.")} Preview in the browser:`);
38225
- console.log(` ${c.accent(`cd ${name2}`)} && ${c.accent("npx hyperframes preview")}`);
38441
+ console.log(` ${c.accent(`cd ${name2}`)} && ${c.accent("npm run dev")}`);
38442
+ console.log();
38443
+ console.log(` ${c.accent("5.")} Check the composition:`);
38444
+ console.log(` ${c.accent(`cd ${name2}`)} && ${c.accent("npm run check")}`);
38226
38445
  console.log();
38227
- console.log(` ${c.accent("5.")} Render to MP4 when ready:`);
38228
- console.log(` ${c.accent(`cd ${name2}`)} && ${c.accent("npx hyperframes render")}`);
38446
+ console.log(` ${c.accent("6.")} Render to MP4 when ready:`);
38447
+ console.log(` ${c.accent(`cd ${name2}`)} && ${c.accent("npm run render")}`);
38229
38448
  console.log();
38230
38449
  console.log(` ${c.dim("Full docs: hyperframes.heygen.com")}`);
38231
38450
  return;
@@ -38353,7 +38572,7 @@ var init_init = __esm({
38353
38572
  spin.start(`Downloading example ${c.accent(templateId)}...`);
38354
38573
  }
38355
38574
  try {
38356
- await scaffoldProject(destDir, name, templateId, localVideoName, videoDuration);
38575
+ await scaffoldProject(destDir, name, templateId, localVideoName, videoDuration, tailwind);
38357
38576
  if (!isBundled) {
38358
38577
  spin.stop(c.success(`Downloaded ${templateId}`));
38359
38578
  }
@@ -38367,7 +38586,7 @@ ${c.dim("Use --example blank for offline use.")}`
38367
38586
  );
38368
38587
  process.exit(1);
38369
38588
  }
38370
- trackInitTemplate(templateId);
38589
+ trackInitTemplate(templateId, { tailwind });
38371
38590
  const transcriptFile = resolve22(destDir, "transcript.json");
38372
38591
  if (existsSync38(transcriptFile)) {
38373
38592
  await patchTranscript(destDir, transcriptFile);
@@ -5,6 +5,28 @@ description: GSAP animation reference for HyperFrames. Covers gsap.to(), from(),
5
5
 
6
6
  # GSAP
7
7
 
8
+ ## HyperFrames Contract
9
+
10
+ HyperFrames controls GSAP through its `gsap` runtime adapter. Create a paused timeline synchronously, register it on `window.__timelines` with the exact `data-composition-id`, and let HyperFrames seek it.
11
+
12
+ ```html
13
+ <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
14
+ <script>
15
+ window.__timelines = window.__timelines || {};
16
+ const tl = gsap.timeline({ paused: true });
17
+
18
+ tl.from(".title", { y: 48, opacity: 0, duration: 0.6, ease: "power3.out" }, 0);
19
+ tl.to(".accent", { scaleX: 1, duration: 0.5, ease: "power2.out" }, 0.25);
20
+
21
+ window.__timelines["main"] = tl; // key must equal data-composition-id on the composition root
22
+ </script>
23
+ ```
24
+
25
+ - The registry key must match the composition root's `data-composition-id`.
26
+ - Do not call `tl.play()` for render-critical motion.
27
+ - Do not build timelines inside async code, timers, or event handlers.
28
+ - Keep loops finite. HyperFrames renders finite video durations.
29
+
8
30
  ## Core Tween Methods
9
31
 
10
32
  - **gsap.to(targets, vars)** — animate from current state to `vars`. Most common.
@@ -21,7 +43,7 @@ Always use **camelCase** property names (e.g. `backgroundColor`, `rotationX`).
21
43
  - **ease** — `"power1.out"` (default), `"power3.inOut"`, `"back.out(1.7)"`, `"elastic.out(1, 0.3)"`, `"none"`.
22
44
  - **stagger** — number `0.1` or object: `{ amount: 0.3, from: "center" }`, `{ each: 0.1, from: "random" }`.
23
45
  - **overwrite** — `false` (default), `true`, or `"auto"`.
24
- - **repeat** — number or `-1` for infinite. **yoyo** — alternates direction with repeat.
46
+ - **repeat** — finite number; never `-1` in HyperFrames. Compute repeats from the visible duration. **yoyo** — alternates direction with repeat.
25
47
  - **onComplete**, **onStart**, **onUpdate** — callbacks.
26
48
  - **immediateRender** — default `true` for from()/fromTo(). Set `false` on later tweens targeting the same property+element to avoid overwrite.
27
49
 
@@ -209,3 +231,10 @@ Pause or kill off-screen animations.
209
231
  - Chain animations with delay when a timeline can sequence them.
210
232
  - Create tweens before the DOM exists.
211
233
  - Skip cleanup — always kill tweens when no longer needed.
234
+ - Use infinite repeat values in HyperFrames compositions. Use finite repeat counts computed from the visible duration.
235
+
236
+ ## Credits And References
237
+
238
+ - HyperFrames adapter source: `packages/core/src/runtime/adapters/gsap.ts`.
239
+ - GSAP documentation: https://gsap.com/docs/v3/
240
+ - GSAP timeline pause and seek behavior: https://gsap.com/docs/v3/GSAP/Timeline/pause%28%29/
@@ -25,6 +25,7 @@ npx hyperframes init my-video # interactive wizard
25
25
  npx hyperframes init my-video --example warm-grain # pick an example
26
26
  npx hyperframes init my-video --video clip.mp4 # with video file
27
27
  npx hyperframes init my-video --audio track.mp3 # with audio file
28
+ npx hyperframes init my-video --example blank --tailwind # with Tailwind v4 browser runtime
28
29
  npx hyperframes init my-video --non-interactive # skip prompts (CI/agents)
29
30
  ```
30
31
 
@@ -32,6 +33,8 @@ Templates: `blank`, `warm-grain`, `play-mode`, `swiss-grid`, `vignelli`, `decisi
32
33
 
33
34
  `init` creates the right file structure, copies media, transcribes audio with Whisper, and installs AI coding skills. Use it instead of creating files by hand.
34
35
 
36
+ When using `--tailwind`, invoke the `tailwind` skill before editing classes or theme tokens. The scaffold uses Tailwind v4.2 via the browser runtime, not Studio's Tailwind v3 setup.
37
+
35
38
  ## Linting
36
39
 
37
40
  ```bash
@@ -8,15 +8,15 @@ This project uses AI agent skills for framework-specific patterns. Install them
8
8
  npx skills add heygen-com/hyperframes
9
9
  ```
10
10
 
11
- Skills encode patterns like `window.__timelines` registration, `data-*` attribute semantics, and shader-compatible CSS rules that are not in generic web docs. Using them produces correct compositions from the start.
11
+ Skills encode patterns like `window.__timelines` registration, `data-*` attribute semantics, Tailwind v4 browser-runtime styling for `--tailwind` projects, and shader-compatible CSS rules that are not in generic web docs. Using them produces correct compositions from the start.
12
12
 
13
13
  ## Commands
14
14
 
15
15
  ```bash
16
- npx hyperframes preview # preview in browser (studio editor)
17
- npx hyperframes render # render to MP4
18
- npx hyperframes lint # validate compositions (errors + warnings)
19
- npx hyperframes lint --json # machine-readable output for CI
16
+ npm run dev # preview in browser (studio editor)
17
+ npm run check # lint + validate + inspect
18
+ npm run render # render to MP4
19
+ npm run publish # publish and get a shareable link
20
20
  npx hyperframes docs <topic> # reference docs in terminal
21
21
  ```
22
22
 
@@ -30,10 +30,10 @@ npx hyperframes docs <topic> # reference docs in terminal
30
30
 
31
31
  ## Linting — Always Run After Changes
32
32
 
33
- After creating or editing any `.html` composition, run the linter before considering the task complete:
33
+ After creating or editing any `.html` composition, run the full check before considering the task complete:
34
34
 
35
35
  ```bash
36
- npx hyperframes lint
36
+ npm run check
37
37
  ```
38
38
 
39
39
  Fix all errors before presenting the result.
@@ -10,7 +10,13 @@
10
10
  | **hyperframes-cli** | `/hyperframes-cli` | CLI commands: init, lint, preview, render, transcribe, tts |
11
11
  | **hyperframes-registry** | `/hyperframes-registry` | Installing blocks and components via `hyperframes add` |
12
12
  | **website-to-hyperframes** | `/website-to-hyperframes` | Capturing a URL and turning it into a video — full website-to-video pipeline |
13
+ | **tailwind** | `/tailwind` | Tailwind v4 browser-runtime styles for projects created with `hyperframes init --tailwind` |
13
14
  | **gsap** | `/gsap` | GSAP animations for HyperFrames — tweens, timelines, easing, performance |
15
+ | **animejs** | `/animejs` | Anime.js animations registered on `window.__hfAnime` |
16
+ | **css-animations** | `/css-animations` | CSS keyframes that HyperFrames can pause and seek |
17
+ | **lottie** | `/lottie` | `lottie-web` and dotLottie players registered on `window.__hfLottie` |
18
+ | **three** | `/three` | Three.js scenes rendered from HyperFrames `hf-seek` events |
19
+ | **waapi** | `/waapi` | Web Animations API motion driven through `document.getAnimations()` |
14
20
 
15
21
  > **Skills not available?** Ask the user to run `npx hyperframes skills` and restart their
16
22
  > agent session, or install manually: `npx skills add heygen-com/hyperframes`.
@@ -18,9 +24,10 @@
18
24
  ## Commands
19
25
 
20
26
  ```bash
21
- npx hyperframes preview # preview in browser (studio editor)
22
- npx hyperframes render # render to MP4
23
- npx hyperframes lint # validate compositions (errors + warnings)
27
+ npm run dev # preview in browser (studio editor)
28
+ npm run check # lint + validate + inspect
29
+ npm run render # render to MP4
30
+ npm run publish # publish and get a shareable link
24
31
  npx hyperframes lint --verbose # include info-level findings
25
32
  npx hyperframes lint --json # machine-readable output for CI
26
33
  npx hyperframes docs <topic> # reference docs in terminal
@@ -51,13 +58,13 @@ https://hyperframes.heygen.com/llms.txt
51
58
 
52
59
  ## Linting — ALWAYS RUN AFTER CHANGES
53
60
 
54
- After creating or editing any `.html` composition, **always** run the linter before considering the task complete:
61
+ After creating or editing any `.html` composition, **always** run the full check before considering the task complete:
55
62
 
56
63
  ```bash
57
- npx hyperframes lint
64
+ npm run check
58
65
  ```
59
66
 
60
- Fix all errors before presenting the result. Warnings are informational and usually safe to ignore.
67
+ Fix all errors before presenting the result. Inspect warnings should be reviewed before rendering.
61
68
 
62
69
  ## Key Rules
63
70
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperframes",
3
- "version": "0.4.39",
3
+ "version": "0.4.41",
4
4
  "description": "HyperFrames CLI — create, preview, and render HTML video compositions",
5
5
  "repository": {
6
6
  "type": "git",