hyperframes 0.5.0-alpha.12 → 0.5.0-alpha.14

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.5.0-alpha.12" : "0.0.0-dev";
57
+ VERSION = true ? "0.5.0-alpha.14" : "0.0.0-dev";
58
58
  }
59
59
  });
60
60
 
@@ -10218,8 +10218,8 @@ function trackRenderError(props) {
10218
10218
  memory_free_mb: props.memoryFreeMb
10219
10219
  });
10220
10220
  }
10221
- function trackInitTemplate(templateId) {
10222
- trackEvent("init_template", { template: templateId });
10221
+ function trackInitTemplate(templateId, props) {
10222
+ trackEvent("init_template", { template: templateId, tailwind: props?.tailwind });
10223
10223
  }
10224
10224
  function trackBrowserInstall() {
10225
10225
  trackEvent("browser_install", {});
@@ -25757,6 +25757,13 @@ function resolveConfig(overrides) {
25757
25757
  "PRODUCER_ENABLE_STREAMING_ENCODE",
25758
25758
  DEFAULT_CONFIG2.enableStreamingEncode
25759
25759
  ),
25760
+ streamingEncodeMaxDurationSeconds: Math.max(
25761
+ 0,
25762
+ envNum(
25763
+ "PRODUCER_STREAMING_ENCODE_MAX_DURATION_SECONDS",
25764
+ DEFAULT_CONFIG2.streamingEncodeMaxDurationSeconds
25765
+ )
25766
+ ),
25760
25767
  ffmpegEncodeTimeout: envNum("FFMPEG_ENCODE_TIMEOUT_MS", DEFAULT_CONFIG2.ffmpegEncodeTimeout),
25761
25768
  ffmpegProcessTimeout: envNum("FFMPEG_PROCESS_TIMEOUT_MS", DEFAULT_CONFIG2.ffmpegProcessTimeout),
25762
25769
  ffmpegStreamingTimeout: envNum(
@@ -25814,7 +25821,8 @@ var init_config2 = __esm({
25814
25821
  forceScreenshot: false,
25815
25822
  enableChunkedEncode: false,
25816
25823
  chunkSizeFrames: 360,
25817
- enableStreamingEncode: false,
25824
+ enableStreamingEncode: true,
25825
+ streamingEncodeMaxDurationSeconds: 240,
25818
25826
  ffmpegEncodeTimeout: 6e5,
25819
25827
  ffmpegProcessTimeout: 3e5,
25820
25828
  ffmpegStreamingTimeout: 6e5,
@@ -26423,6 +26431,44 @@ async function pollPageExpression(page, expression, timeoutMs, intervalMs = 100)
26423
26431
  }
26424
26432
  return Boolean(await page.evaluate(expression));
26425
26433
  }
26434
+ async function applyVideoMetadataHints(page, hints) {
26435
+ if (!hints || hints.length === 0) return;
26436
+ await page.evaluate(
26437
+ (metadataHints) => {
26438
+ for (const hint of metadataHints) {
26439
+ if (!hint.id || !Number.isFinite(hint.width) || !Number.isFinite(hint.height) || hint.width <= 0 || hint.height <= 0) {
26440
+ continue;
26441
+ }
26442
+ const video = document.getElementById(hint.id);
26443
+ if (!video) continue;
26444
+ if (!video.hasAttribute("width")) video.setAttribute("width", String(hint.width));
26445
+ if (!video.hasAttribute("height")) video.setAttribute("height", String(hint.height));
26446
+ const computed = window.getComputedStyle(video);
26447
+ if (!video.style.aspectRatio && (!computed.aspectRatio || computed.aspectRatio === "auto")) {
26448
+ video.style.aspectRatio = `${hint.width} / ${hint.height}`;
26449
+ }
26450
+ }
26451
+ },
26452
+ [...hints]
26453
+ );
26454
+ }
26455
+ async function waitForOptionalTailwindReady(page, timeoutMs) {
26456
+ const hasTailwindReady = await page.evaluate(
26457
+ `(() => { const ready = window.__tailwindReady; return !!ready && typeof ready.then === "function"; })()`
26458
+ );
26459
+ if (!hasTailwindReady) return;
26460
+ const ready = await Promise.race([
26461
+ page.evaluate(
26462
+ `Promise.resolve(window.__tailwindReady).then(() => true, () => false)`
26463
+ ),
26464
+ new Promise((resolve39) => setTimeout(() => resolve39(false), timeoutMs))
26465
+ ]);
26466
+ if (!ready) {
26467
+ throw new Error(
26468
+ `[FrameCapture] window.__tailwindReady not resolved after ${timeoutMs}ms. Tailwind browser runtime must finish before frame capture starts.`
26469
+ );
26470
+ }
26471
+ }
26426
26472
  async function initializeSession(session) {
26427
26473
  const { page, serverUrl } = session;
26428
26474
  page.on("console", (msg) => {
@@ -26466,6 +26512,7 @@ async function initializeSession(session) {
26466
26512
  `[FrameCapture] window.__hf not ready after ${pageReadyTimeout2}ms. Page must expose window.__hf = { duration, seek }.`
26467
26513
  );
26468
26514
  }
26515
+ await applyVideoMetadataHints(page, session.options.videoMetadataHints);
26469
26516
  const skipIdsLiteral = JSON.stringify(session.options.skipReadinessVideoIds ?? []);
26470
26517
  const videosReady = await pollPageExpression(
26471
26518
  page,
@@ -26478,6 +26525,7 @@ async function initializeSession(session) {
26478
26525
  );
26479
26526
  }
26480
26527
  await page.evaluate(`document.fonts?.ready`);
26528
+ await waitForOptionalTailwindReady(page, pageReadyTimeout2);
26481
26529
  if (session.options.format === "png") {
26482
26530
  await initTransparentBackground(session.page);
26483
26531
  }
@@ -26532,6 +26580,7 @@ async function initializeSession(session) {
26532
26580
  `[FrameCapture] window.__hf not ready after ${pageReadyTimeout}ms. Page must expose window.__hf = { duration, seek }.`
26533
26581
  );
26534
26582
  }
26583
+ await applyVideoMetadataHints(page, session.options.videoMetadataHints);
26535
26584
  const beginframeSkipIdsLiteral = JSON.stringify(session.options.skipReadinessVideoIds ?? []);
26536
26585
  const videoDeadline = Date.now() + (session.config?.playerReadyTimeout ?? DEFAULT_CONFIG2.playerReadyTimeout);
26537
26586
  while (Date.now() < videoDeadline) {
@@ -26542,6 +26591,7 @@ async function initializeSession(session) {
26542
26591
  await new Promise((r2) => setTimeout(r2, 100));
26543
26592
  }
26544
26593
  await page.evaluate(`document.fonts?.ready`);
26594
+ await waitForOptionalTailwindReady(page, pageReadyTimeout);
26545
26595
  warmupRunning = false;
26546
26596
  session.beginFrameTimeTicks = (warmupTicks + 10) * session.beginFrameIntervalMs;
26547
26597
  if (session.options.format === "png") {
@@ -34358,6 +34408,24 @@ function applyRenderModeHints(cfg, compiled, log2 = defaultLogger) {
34358
34408
  reasons: compiled.renderModeHints.reasons.map((reason) => reason.message)
34359
34409
  });
34360
34410
  }
34411
+ function collectVideoReadinessSkipIds(nativeHdrVideoIds, extractedVideos) {
34412
+ return Array.from(
34413
+ /* @__PURE__ */ new Set([
34414
+ ...nativeHdrVideoIds,
34415
+ ...extractedVideos.filter((video) => hasUsableVideoDimensions(video.metadata)).map((video) => video.videoId)
34416
+ ])
34417
+ ).sort();
34418
+ }
34419
+ function hasUsableVideoDimensions(metadata) {
34420
+ return Number.isFinite(metadata.width) && Number.isFinite(metadata.height) && metadata.width > 0 && metadata.height > 0;
34421
+ }
34422
+ function collectVideoMetadataHints(extractedVideos) {
34423
+ return extractedVideos.filter((video) => hasUsableVideoDimensions(video.metadata)).map((video) => ({
34424
+ id: video.videoId,
34425
+ width: video.metadata.width,
34426
+ height: video.metadata.height
34427
+ })).sort((a, b) => a.id.localeCompare(b.id));
34428
+ }
34361
34429
  function resolveRenderWorkerCount(totalFrames, requestedWorkers, cfg, compiled, composition, log2 = defaultLogger, measuredCaptureCost) {
34362
34430
  const captureCost = combineCaptureCostEstimates(
34363
34431
  estimateCaptureCostMultiplier(compiled, composition),
@@ -35020,6 +35088,27 @@ function createRenderJob(config) {
35020
35088
  function normalizeCompositionSrcPath(srcPath) {
35021
35089
  return srcPath.replace(/\\/g, "/").replace(/^\.\//, "");
35022
35090
  }
35091
+ function createStandaloneEntryRenderClone(root, host) {
35092
+ const hostClone = host.cloneNode(true);
35093
+ hostClone.setAttribute("data-start", "0");
35094
+ if (root === host) return hostClone;
35095
+ const rootClone = root.cloneNode(false);
35096
+ rootClone.appendChild(hostClone);
35097
+ return rootClone;
35098
+ }
35099
+ function replaceBodyWithRenderClone(body, renderClone) {
35100
+ while (body.firstChild) {
35101
+ body.removeChild(body.firstChild);
35102
+ }
35103
+ body.appendChild(renderClone);
35104
+ }
35105
+ function shouldUseStreamingEncode(cfg, outputFormat, workerCount, durationSeconds) {
35106
+ if (!cfg.enableStreamingEncode) return false;
35107
+ if (outputFormat === "png-sequence") return false;
35108
+ if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) return false;
35109
+ if (durationSeconds > cfg.streamingEncodeMaxDurationSeconds) return false;
35110
+ return workerCount === 1;
35111
+ }
35023
35112
  function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
35024
35113
  const normalizedEntryFile = normalizeCompositionSrcPath(entryFile);
35025
35114
  const { document: document2 } = parseHTML(indexHtml);
@@ -35034,16 +35123,8 @@ function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
35034
35123
  (candidate) => candidate.hasAttribute("data-composition-id")
35035
35124
  ) ?? null;
35036
35125
  if (!root) return null;
35037
- const hostClone = host.cloneNode(true);
35038
- hostClone.setAttribute("data-start", "0");
35039
- body.innerHTML = "";
35040
- if (root === host) {
35041
- body.appendChild(hostClone);
35042
- return document2.toString();
35043
- }
35044
- const rootClone = root.cloneNode(false);
35045
- rootClone.appendChild(hostClone);
35046
- body.appendChild(rootClone);
35126
+ const renderClone = createStandaloneEntryRenderClone(root, host);
35127
+ replaceBodyWithRenderClone(body, renderClone);
35047
35128
  return document2.toString();
35048
35129
  }
35049
35130
  async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSignal) {
@@ -35075,7 +35156,6 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35075
35156
  }
35076
35157
  const enableChunkedEncode = cfg.enableChunkedEncode;
35077
35158
  const chunkedEncodeSize = cfg.chunkSizeFrames;
35078
- const enableStreamingEncode = cfg.enableStreamingEncode && !isPngSequence;
35079
35159
  let peakRssBytes = 0;
35080
35160
  let peakHeapUsedBytes = 0;
35081
35161
  const sampleMemory = () => {
@@ -35369,6 +35449,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35369
35449
  let frameLookup = null;
35370
35450
  const compiledDir = join36(workDir, "compiled");
35371
35451
  let extractionResult = null;
35452
+ let videoReadinessSkipIds = [];
35453
+ let videoMetadataHints = [];
35372
35454
  const nativeHdrVideoIds = /* @__PURE__ */ new Set();
35373
35455
  const videoTransfers = /* @__PURE__ */ new Map();
35374
35456
  if (job.config.hdrMode !== "force-sdr" && composition.videos.length > 0) {
@@ -35425,6 +35507,11 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35425
35507
  if (extractionResult.extracted.length > 0) {
35426
35508
  frameLookup = createFrameLookupTable(composition.videos, extractionResult.extracted);
35427
35509
  }
35510
+ videoReadinessSkipIds = collectVideoReadinessSkipIds(
35511
+ nativeHdrVideoIds,
35512
+ extractionResult.extracted
35513
+ );
35514
+ videoMetadataHints = collectVideoMetadataHints(extractionResult.extracted);
35428
35515
  perfStages.videoExtractMs = Date.now() - stage2Start;
35429
35516
  const existingAudioSrcs = new Set(composition.audios.map((a) => a.src));
35430
35517
  for (const ext of extractionResult.extracted) {
@@ -35538,9 +35625,10 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35538
35625
  format: needsAlpha ? "png" : "jpeg",
35539
35626
  quality: needsAlpha ? void 0 : job.config.quality === "draft" ? 80 : 95
35540
35627
  };
35541
- const buildHdrCaptureOptions = () => ({
35628
+ const buildCaptureOptions = () => ({
35542
35629
  ...captureOptions,
35543
- skipReadinessVideoIds: Array.from(nativeHdrVideoIds)
35630
+ videoMetadataHints,
35631
+ skipReadinessVideoIds: videoReadinessSkipIds
35544
35632
  });
35545
35633
  let captureCalibration;
35546
35634
  let switchedToScreenshotAfterCalibration = false;
@@ -35553,7 +35641,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35553
35641
  calibrationSession = await createCaptureSession(
35554
35642
  fileServer.url,
35555
35643
  calibrationDir,
35556
- buildHdrCaptureOptions(),
35644
+ buildCaptureOptions(),
35557
35645
  videoInjector,
35558
35646
  calibrationCfg
35559
35647
  );
@@ -35636,6 +35724,15 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35636
35724
  await closeCaptureSession(probeSession);
35637
35725
  probeSession = null;
35638
35726
  }
35727
+ let useStreamingEncode = shouldUseStreamingEncode(cfg, outputFormat, workerCount, job.duration);
35728
+ log2.info("streaming-encode gate", {
35729
+ enabled: useStreamingEncode,
35730
+ configFlag: cfg.enableStreamingEncode,
35731
+ outputFormat,
35732
+ workerCount,
35733
+ durationSeconds: job.duration,
35734
+ maxDurationSeconds: cfg.streamingEncodeMaxDurationSeconds
35735
+ });
35639
35736
  const captureAttempts = [];
35640
35737
  const FORMAT_EXT2 = {
35641
35738
  mp4: ".mp4",
@@ -35677,7 +35774,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35677
35774
  const domSession = await createCaptureSession(
35678
35775
  fileServer.url,
35679
35776
  framesDir,
35680
- buildHdrCaptureOptions(),
35777
+ buildCaptureOptions(),
35681
35778
  createVideoFrameInjector(frameLookup),
35682
35779
  cfg
35683
35780
  );
@@ -36175,28 +36272,47 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
36175
36272
  } else {
36176
36273
  let streamingEncoder = null;
36177
36274
  let streamingEncoderClosed = false;
36178
- if (enableStreamingEncode) {
36179
- streamingEncoder = await spawnStreamingEncoder(
36180
- videoOnlyPath,
36181
- {
36182
- fps: job.config.fps,
36183
- width,
36184
- height,
36185
- codec: preset.codec,
36186
- preset: preset.preset,
36187
- quality: effectiveQuality,
36188
- bitrate: effectiveBitrate,
36189
- pixelFormat: preset.pixelFormat,
36190
- useGpu: job.config.useGpu,
36191
- imageFormat: captureOptions.format || "jpeg",
36192
- hdr: preset.hdr
36193
- },
36194
- abortSignal
36195
- );
36196
- assertNotAborted();
36275
+ if (useStreamingEncode) {
36276
+ try {
36277
+ streamingEncoder = await spawnStreamingEncoder(
36278
+ videoOnlyPath,
36279
+ {
36280
+ fps: job.config.fps,
36281
+ width,
36282
+ height,
36283
+ codec: preset.codec,
36284
+ preset: preset.preset,
36285
+ quality: effectiveQuality,
36286
+ bitrate: effectiveBitrate,
36287
+ pixelFormat: preset.pixelFormat,
36288
+ useGpu: job.config.useGpu,
36289
+ imageFormat: captureOptions.format || "jpeg",
36290
+ hdr: preset.hdr
36291
+ },
36292
+ abortSignal
36293
+ );
36294
+ assertNotAborted();
36295
+ } catch (err) {
36296
+ if (abortSignal?.aborted) {
36297
+ if (streamingEncoder && !streamingEncoderClosed) {
36298
+ await streamingEncoder.close().catch(() => {
36299
+ });
36300
+ streamingEncoderClosed = true;
36301
+ }
36302
+ throw err;
36303
+ }
36304
+ useStreamingEncode = false;
36305
+ streamingEncoder = null;
36306
+ log2.warn("[Render] Streaming encoder spawn failed; falling back to disk-frame encode.", {
36307
+ error: err instanceof Error ? err.message : String(err),
36308
+ outputFormat,
36309
+ workerCount,
36310
+ durationSeconds: job.duration
36311
+ });
36312
+ }
36197
36313
  }
36198
36314
  try {
36199
- if (enableStreamingEncode && streamingEncoder) {
36315
+ if (useStreamingEncode && streamingEncoder) {
36200
36316
  const reorderBuffer = createFrameReorderBuffer(0, totalFrames);
36201
36317
  const currentEncoder = streamingEncoder;
36202
36318
  if (workerCount > 1) {
@@ -36210,7 +36326,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
36210
36326
  fileServer.url,
36211
36327
  workDir,
36212
36328
  tasks,
36213
- buildHdrCaptureOptions(),
36329
+ buildCaptureOptions(),
36214
36330
  () => createVideoFrameInjector(frameLookup),
36215
36331
  abortSignal,
36216
36332
  (progress) => {
@@ -36240,7 +36356,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
36240
36356
  const session = probeSession ?? await createCaptureSession(
36241
36357
  fileServer.url,
36242
36358
  framesDir,
36243
- buildHdrCaptureOptions(),
36359
+ buildCaptureOptions(),
36244
36360
  videoInjector,
36245
36361
  cfg
36246
36362
  );
@@ -36295,7 +36411,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
36295
36411
  initialWorkerCount: workerCount,
36296
36412
  allowRetry: job.config.workers === void 0,
36297
36413
  frameExt: needsAlpha ? "png" : "jpg",
36298
- captureOptions: buildHdrCaptureOptions(),
36414
+ captureOptions: buildCaptureOptions(),
36299
36415
  createBeforeCaptureHook: () => createVideoFrameInjector(frameLookup),
36300
36416
  abortSignal,
36301
36417
  onProgress: (progress) => {
@@ -36330,7 +36446,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
36330
36446
  const session = probeSession ?? await createCaptureSession(
36331
36447
  fileServer.url,
36332
36448
  framesDir,
36333
- buildHdrCaptureOptions(),
36449
+ buildCaptureOptions(),
36334
36450
  videoInjector,
36335
36451
  cfg
36336
36452
  );
@@ -38045,7 +38161,8 @@ var init_preview2 = __esm({
38045
38161
  var init_exports = {};
38046
38162
  __export(init_exports, {
38047
38163
  default: () => init_default,
38048
- examples: () => examples2
38164
+ examples: () => examples2,
38165
+ injectTailwindBrowserScript: () => injectTailwindBrowserScript
38049
38166
  });
38050
38167
  import {
38051
38168
  existsSync as existsSync39,
@@ -38136,6 +38253,92 @@ function getStaticTemplateDir(templateId) {
38136
38253
  function getSharedTemplateDir() {
38137
38254
  return resolveAssetDir(["..", "templates", "_shared"], ["templates", "_shared"]);
38138
38255
  }
38256
+ function toPackageName(projectName) {
38257
+ const normalized = basename5(projectName).trim().toLowerCase().replace(/^[._]+/, "").replace(/[^a-z0-9._~-]+/g, "-").replace(/-+/g, "-").replace(/^[-.]+|[-.]+$/g, "");
38258
+ return normalized || "hyperframes-project";
38259
+ }
38260
+ function getHyperframesPackageSpecifier() {
38261
+ return VERSION === "0.0.0-dev" ? "hyperframes" : `hyperframes@${VERSION}`;
38262
+ }
38263
+ function hyperframesScript(command2) {
38264
+ return `npx --yes ${getHyperframesPackageSpecifier()} ${command2}`;
38265
+ }
38266
+ function buildPackageScripts() {
38267
+ return {
38268
+ dev: hyperframesScript("preview"),
38269
+ check: `${hyperframesScript("lint")} && ${hyperframesScript("validate")} && ${hyperframesScript("inspect")}`,
38270
+ render: hyperframesScript("render"),
38271
+ publish: hyperframesScript("publish")
38272
+ };
38273
+ }
38274
+ function writeDefaultPackageJson(destDir, projectName) {
38275
+ const packageJsonPath = resolve22(destDir, "package.json");
38276
+ if (existsSync39(packageJsonPath)) return;
38277
+ writeFileSync18(
38278
+ packageJsonPath,
38279
+ `${JSON.stringify(
38280
+ {
38281
+ name: toPackageName(projectName),
38282
+ private: true,
38283
+ type: "module",
38284
+ scripts: buildPackageScripts()
38285
+ },
38286
+ null,
38287
+ 2
38288
+ )}
38289
+ `,
38290
+ "utf-8"
38291
+ );
38292
+ }
38293
+ function listHtmlFiles(dir) {
38294
+ const files = [];
38295
+ const ignoredDirs = /* @__PURE__ */ new Set([".git", "dist", "node_modules"]);
38296
+ function walk(currentDir) {
38297
+ for (const entry of readdirSync14(currentDir, { withFileTypes: true })) {
38298
+ const entryPath = join41(currentDir, entry.name);
38299
+ if (entry.isDirectory()) {
38300
+ if (!ignoredDirs.has(entry.name)) walk(entryPath);
38301
+ continue;
38302
+ }
38303
+ if (entry.isFile() && entry.name.endsWith(".html")) {
38304
+ files.push(entryPath);
38305
+ }
38306
+ }
38307
+ }
38308
+ walk(dir);
38309
+ return files;
38310
+ }
38311
+ function injectTailwindBrowserScript(html) {
38312
+ if (html.includes(TAILWIND_BROWSER_SRC)) return html;
38313
+ const script = [
38314
+ `<script>`,
38315
+ `window.__tailwindReady=new Promise(function(resolve){`,
38316
+ `var loaded=document.readyState==="complete";`,
38317
+ `var resolved=false;`,
38318
+ `var observer;`,
38319
+ `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 "";}`,
38320
+ `function finish(){if(resolved||!loaded||!readTailwindCss())return;resolved=true;if(observer)observer.disconnect();resolve(true);}`,
38321
+ `observer=new MutationObserver(finish);`,
38322
+ `observer.observe(document.documentElement,{childList:true,subtree:true,characterData:true});`,
38323
+ `if(loaded){finish();}else{window.addEventListener("load",function(){loaded=true;finish();},{once:true});}`,
38324
+ `});`,
38325
+ `</script>`,
38326
+ `<script src="${TAILWIND_BROWSER_SRC}" integrity="${TAILWIND_BROWSER_INTEGRITY}" crossorigin="anonymous"></script>`
38327
+ ].join("\n");
38328
+ if (/<\/head>/i.test(html)) {
38329
+ return html.replace(/<\/head>/i, (closingHead) => `
38330
+ ${script}
38331
+ ${closingHead}`);
38332
+ }
38333
+ return `${script}
38334
+ ${html}`;
38335
+ }
38336
+ function writeTailwindSupport(destDir) {
38337
+ for (const file of listHtmlFiles(destDir)) {
38338
+ const html = readFileSync27(file, "utf-8");
38339
+ writeFileSync18(file, injectTailwindBrowserScript(html), "utf-8");
38340
+ }
38341
+ }
38139
38342
  function patchVideoSrc(dir, videoFilename, durationSeconds) {
38140
38343
  const htmlFiles = readdirSync14(dir, { withFileTypes: true, recursive: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".html")).map((e2) => join41(e2.parentPath ?? e2.path, e2.name));
38141
38344
  for (const file of htmlFiles) {
@@ -38241,7 +38444,7 @@ async function handleVideoFile(videoPath, destDir, interactive) {
38241
38444
  }
38242
38445
  return { meta, localVideoName };
38243
38446
  }
38244
- async function scaffoldProject(destDir, name, templateId, localVideoName, durationSeconds) {
38447
+ async function scaffoldProject(destDir, name, templateId, localVideoName, durationSeconds, tailwind = false) {
38245
38448
  mkdirSync23(destDir, { recursive: true });
38246
38449
  const templateDir = getStaticTemplateDir(templateId);
38247
38450
  if (existsSync39(templateDir)) {
@@ -38250,6 +38453,7 @@ async function scaffoldProject(destDir, name, templateId, localVideoName, durati
38250
38453
  await fetchRemoteTemplate(templateId, destDir);
38251
38454
  }
38252
38455
  patchVideoSrc(destDir, localVideoName, durationSeconds);
38456
+ if (tailwind) writeTailwindSupport(destDir);
38253
38457
  writeFileSync18(
38254
38458
  resolve22(destDir, "meta.json"),
38255
38459
  JSON.stringify(
@@ -38267,6 +38471,7 @@ async function scaffoldProject(destDir, name, templateId, localVideoName, durati
38267
38471
  const { writeProjectConfig: writeProjectConfig2, DEFAULT_PROJECT_CONFIG: DEFAULT_PROJECT_CONFIG2 } = await Promise.resolve().then(() => (init_projectConfig(), projectConfig_exports));
38268
38472
  writeProjectConfig2(destDir, DEFAULT_PROJECT_CONFIG2);
38269
38473
  }
38474
+ writeDefaultPackageJson(destDir, name);
38270
38475
  const sharedDir = getSharedTemplateDir();
38271
38476
  if (existsSync39(sharedDir)) {
38272
38477
  for (const entry of readdirSync14(sharedDir, { withFileTypes: true })) {
@@ -38278,7 +38483,7 @@ async function scaffoldProject(destDir, name, templateId, localVideoName, durati
38278
38483
  }
38279
38484
  }
38280
38485
  }
38281
- var examples2, WEB_CODECS, DEFAULT_META, init_default;
38486
+ var examples2, WEB_CODECS, DEFAULT_META, TAILWIND_BROWSER_VERSION, TAILWIND_BROWSER_SRC, TAILWIND_BROWSER_INTEGRITY, init_default;
38282
38487
  var init_init = __esm({
38283
38488
  "src/commands/init.ts"() {
38284
38489
  "use strict";
@@ -38290,11 +38495,13 @@ var init_init = __esm({
38290
38495
  init_remote2();
38291
38496
  init_events();
38292
38497
  init_manager();
38498
+ init_version();
38293
38499
  examples2 = [
38294
38500
  ["Create a project with the interactive wizard", "hyperframes init my-video"],
38295
38501
  ["Pick a starter example", "hyperframes init my-video --example warm-grain"],
38296
38502
  ["Start from an existing video file", "hyperframes init my-video --video clip.mp4"],
38297
38503
  ["Start from an audio file", "hyperframes init my-video --audio track.mp3"],
38504
+ ["Scaffold with Tailwind CSS", "hyperframes init my-video --example blank --tailwind"],
38298
38505
  ["Non-interactive mode (for CI or AI agents)", "hyperframes init my-video --non-interactive"],
38299
38506
  ["Skip AI coding skills installation", "hyperframes init my-video --skip-skills"]
38300
38507
  ];
@@ -38307,6 +38514,9 @@ var init_init = __esm({
38307
38514
  hasAudio: false,
38308
38515
  videoCodec: "h264"
38309
38516
  };
38517
+ TAILWIND_BROWSER_VERSION = "4.2.4";
38518
+ TAILWIND_BROWSER_SRC = `https://cdn.jsdelivr.net/npm/@tailwindcss/browser@${TAILWIND_BROWSER_VERSION}/dist/index.global.js`;
38519
+ TAILWIND_BROWSER_INTEGRITY = "sha384-v5YF9xS+gLRWdvrQ0u/WRbCkjSIH0NjHIPe8tBL1ZRrmI7PiSH6LLdzs0aAIMCuh";
38310
38520
  init_default = defineCommand({
38311
38521
  meta: {
38312
38522
  name: "init",
@@ -38359,6 +38569,10 @@ var init_init = __esm({
38359
38569
  "skip-skills": {
38360
38570
  type: "boolean",
38361
38571
  description: "Skip AI coding skills installation"
38572
+ },
38573
+ tailwind: {
38574
+ type: "boolean",
38575
+ description: "Add Tailwind CSS browser-runtime support"
38362
38576
  }
38363
38577
  },
38364
38578
  async run({ args }) {
@@ -38376,6 +38590,7 @@ var init_init = __esm({
38376
38590
  const audioFlag = args.audio;
38377
38591
  const skipTranscribe = args["skip-transcribe"] === true;
38378
38592
  const skipSkills = args["skip-skills"] === true;
38593
+ const tailwind = args.tailwind === true;
38379
38594
  const nonInteractive = args["non-interactive"] === true;
38380
38595
  const modelFlag = args.model;
38381
38596
  const languageFlag = args.language;
@@ -38445,7 +38660,8 @@ var init_init = __esm({
38445
38660
  basename5(destDir2),
38446
38661
  templateId2,
38447
38662
  localVideoName2,
38448
- videoDuration2
38663
+ videoDuration2,
38664
+ tailwind
38449
38665
  );
38450
38666
  } catch (err) {
38451
38667
  console.error(
@@ -38456,7 +38672,7 @@ var init_init = __esm({
38456
38672
  console.error(c.dim("Use --example blank for offline use."));
38457
38673
  process.exit(1);
38458
38674
  }
38459
- trackInitTemplate(templateId2);
38675
+ trackInitTemplate(templateId2, { tailwind });
38460
38676
  const transcriptFile2 = resolve22(destDir2, "transcript.json");
38461
38677
  if (existsSync39(transcriptFile2)) {
38462
38678
  await patchTranscript(destDir2, transcriptFile2);
@@ -38483,10 +38699,13 @@ var init_init = __esm({
38483
38699
  console.log(` ${c.dim("More patterns: hyperframes.heygen.com/guides/prompting")}`);
38484
38700
  console.log();
38485
38701
  console.log(` ${c.accent("4.")} Preview in the browser:`);
38486
- console.log(` ${c.accent(`cd ${name2}`)} && ${c.accent("npx hyperframes preview")}`);
38702
+ console.log(` ${c.accent(`cd ${name2}`)} && ${c.accent("npm run dev")}`);
38703
+ console.log();
38704
+ console.log(` ${c.accent("5.")} Check the composition:`);
38705
+ console.log(` ${c.accent(`cd ${name2}`)} && ${c.accent("npm run check")}`);
38487
38706
  console.log();
38488
- console.log(` ${c.accent("5.")} Render to MP4 when ready:`);
38489
- console.log(` ${c.accent(`cd ${name2}`)} && ${c.accent("npx hyperframes render")}`);
38707
+ console.log(` ${c.accent("6.")} Render to MP4 when ready:`);
38708
+ console.log(` ${c.accent(`cd ${name2}`)} && ${c.accent("npm run render")}`);
38490
38709
  console.log();
38491
38710
  console.log(` ${c.dim("Full docs: hyperframes.heygen.com")}`);
38492
38711
  return;
@@ -38614,7 +38833,7 @@ var init_init = __esm({
38614
38833
  spin.start(`Downloading example ${c.accent(templateId)}...`);
38615
38834
  }
38616
38835
  try {
38617
- await scaffoldProject(destDir, name, templateId, localVideoName, videoDuration);
38836
+ await scaffoldProject(destDir, name, templateId, localVideoName, videoDuration, tailwind);
38618
38837
  if (!isBundled) {
38619
38838
  spin.stop(c.success(`Downloaded ${templateId}`));
38620
38839
  }
@@ -38628,7 +38847,7 @@ ${c.dim("Use --example blank for offline use.")}`
38628
38847
  );
38629
38848
  process.exit(1);
38630
38849
  }
38631
- trackInitTemplate(templateId);
38850
+ trackInitTemplate(templateId, { tailwind });
38632
38851
  const transcriptFile = resolve22(destDir, "transcript.json");
38633
38852
  if (existsSync39(transcriptFile)) {
38634
38853
  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.5.0-alpha.12",
3
+ "version": "0.5.0-alpha.14",
4
4
  "description": "HyperFrames CLI — create, preview, and render HTML video compositions",
5
5
  "repository": {
6
6
  "type": "git",