hyperframes 0.5.0-alpha.13 → 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.13" : "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,
@@ -26444,6 +26452,23 @@ async function applyVideoMetadataHints(page, hints) {
26444
26452
  [...hints]
26445
26453
  );
26446
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
+ }
26447
26472
  async function initializeSession(session) {
26448
26473
  const { page, serverUrl } = session;
26449
26474
  page.on("console", (msg) => {
@@ -26500,6 +26525,7 @@ async function initializeSession(session) {
26500
26525
  );
26501
26526
  }
26502
26527
  await page.evaluate(`document.fonts?.ready`);
26528
+ await waitForOptionalTailwindReady(page, pageReadyTimeout2);
26503
26529
  if (session.options.format === "png") {
26504
26530
  await initTransparentBackground(session.page);
26505
26531
  }
@@ -26565,6 +26591,7 @@ async function initializeSession(session) {
26565
26591
  await new Promise((r2) => setTimeout(r2, 100));
26566
26592
  }
26567
26593
  await page.evaluate(`document.fonts?.ready`);
26594
+ await waitForOptionalTailwindReady(page, pageReadyTimeout);
26568
26595
  warmupRunning = false;
26569
26596
  session.beginFrameTimeTicks = (warmupTicks + 10) * session.beginFrameIntervalMs;
26570
26597
  if (session.options.format === "png") {
@@ -35061,6 +35088,27 @@ function createRenderJob(config) {
35061
35088
  function normalizeCompositionSrcPath(srcPath) {
35062
35089
  return srcPath.replace(/\\/g, "/").replace(/^\.\//, "");
35063
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
+ }
35064
35112
  function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
35065
35113
  const normalizedEntryFile = normalizeCompositionSrcPath(entryFile);
35066
35114
  const { document: document2 } = parseHTML(indexHtml);
@@ -35075,16 +35123,8 @@ function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
35075
35123
  (candidate) => candidate.hasAttribute("data-composition-id")
35076
35124
  ) ?? null;
35077
35125
  if (!root) return null;
35078
- const hostClone = host.cloneNode(true);
35079
- hostClone.setAttribute("data-start", "0");
35080
- body.innerHTML = "";
35081
- if (root === host) {
35082
- body.appendChild(hostClone);
35083
- return document2.toString();
35084
- }
35085
- const rootClone = root.cloneNode(false);
35086
- rootClone.appendChild(hostClone);
35087
- body.appendChild(rootClone);
35126
+ const renderClone = createStandaloneEntryRenderClone(root, host);
35127
+ replaceBodyWithRenderClone(body, renderClone);
35088
35128
  return document2.toString();
35089
35129
  }
35090
35130
  async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSignal) {
@@ -35116,7 +35156,6 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35116
35156
  }
35117
35157
  const enableChunkedEncode = cfg.enableChunkedEncode;
35118
35158
  const chunkedEncodeSize = cfg.chunkSizeFrames;
35119
- const enableStreamingEncode = cfg.enableStreamingEncode && !isPngSequence;
35120
35159
  let peakRssBytes = 0;
35121
35160
  let peakHeapUsedBytes = 0;
35122
35161
  const sampleMemory = () => {
@@ -35685,6 +35724,15 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35685
35724
  await closeCaptureSession(probeSession);
35686
35725
  probeSession = null;
35687
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
+ });
35688
35736
  const captureAttempts = [];
35689
35737
  const FORMAT_EXT2 = {
35690
35738
  mp4: ".mp4",
@@ -36224,28 +36272,47 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
36224
36272
  } else {
36225
36273
  let streamingEncoder = null;
36226
36274
  let streamingEncoderClosed = false;
36227
- if (enableStreamingEncode) {
36228
- streamingEncoder = await spawnStreamingEncoder(
36229
- videoOnlyPath,
36230
- {
36231
- fps: job.config.fps,
36232
- width,
36233
- height,
36234
- codec: preset.codec,
36235
- preset: preset.preset,
36236
- quality: effectiveQuality,
36237
- bitrate: effectiveBitrate,
36238
- pixelFormat: preset.pixelFormat,
36239
- useGpu: job.config.useGpu,
36240
- imageFormat: captureOptions.format || "jpeg",
36241
- hdr: preset.hdr
36242
- },
36243
- abortSignal
36244
- );
36245
- 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
+ }
36246
36313
  }
36247
36314
  try {
36248
- if (enableStreamingEncode && streamingEncoder) {
36315
+ if (useStreamingEncode && streamingEncoder) {
36249
36316
  const reorderBuffer = createFrameReorderBuffer(0, totalFrames);
36250
36317
  const currentEncoder = streamingEncoder;
36251
36318
  if (workerCount > 1) {
@@ -38094,7 +38161,8 @@ var init_preview2 = __esm({
38094
38161
  var init_exports = {};
38095
38162
  __export(init_exports, {
38096
38163
  default: () => init_default,
38097
- examples: () => examples2
38164
+ examples: () => examples2,
38165
+ injectTailwindBrowserScript: () => injectTailwindBrowserScript
38098
38166
  });
38099
38167
  import {
38100
38168
  existsSync as existsSync39,
@@ -38185,6 +38253,92 @@ function getStaticTemplateDir(templateId) {
38185
38253
  function getSharedTemplateDir() {
38186
38254
  return resolveAssetDir(["..", "templates", "_shared"], ["templates", "_shared"]);
38187
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
+ }
38188
38342
  function patchVideoSrc(dir, videoFilename, durationSeconds) {
38189
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));
38190
38344
  for (const file of htmlFiles) {
@@ -38290,7 +38444,7 @@ async function handleVideoFile(videoPath, destDir, interactive) {
38290
38444
  }
38291
38445
  return { meta, localVideoName };
38292
38446
  }
38293
- async function scaffoldProject(destDir, name, templateId, localVideoName, durationSeconds) {
38447
+ async function scaffoldProject(destDir, name, templateId, localVideoName, durationSeconds, tailwind = false) {
38294
38448
  mkdirSync23(destDir, { recursive: true });
38295
38449
  const templateDir = getStaticTemplateDir(templateId);
38296
38450
  if (existsSync39(templateDir)) {
@@ -38299,6 +38453,7 @@ async function scaffoldProject(destDir, name, templateId, localVideoName, durati
38299
38453
  await fetchRemoteTemplate(templateId, destDir);
38300
38454
  }
38301
38455
  patchVideoSrc(destDir, localVideoName, durationSeconds);
38456
+ if (tailwind) writeTailwindSupport(destDir);
38302
38457
  writeFileSync18(
38303
38458
  resolve22(destDir, "meta.json"),
38304
38459
  JSON.stringify(
@@ -38316,6 +38471,7 @@ async function scaffoldProject(destDir, name, templateId, localVideoName, durati
38316
38471
  const { writeProjectConfig: writeProjectConfig2, DEFAULT_PROJECT_CONFIG: DEFAULT_PROJECT_CONFIG2 } = await Promise.resolve().then(() => (init_projectConfig(), projectConfig_exports));
38317
38472
  writeProjectConfig2(destDir, DEFAULT_PROJECT_CONFIG2);
38318
38473
  }
38474
+ writeDefaultPackageJson(destDir, name);
38319
38475
  const sharedDir = getSharedTemplateDir();
38320
38476
  if (existsSync39(sharedDir)) {
38321
38477
  for (const entry of readdirSync14(sharedDir, { withFileTypes: true })) {
@@ -38327,7 +38483,7 @@ async function scaffoldProject(destDir, name, templateId, localVideoName, durati
38327
38483
  }
38328
38484
  }
38329
38485
  }
38330
- 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;
38331
38487
  var init_init = __esm({
38332
38488
  "src/commands/init.ts"() {
38333
38489
  "use strict";
@@ -38339,11 +38495,13 @@ var init_init = __esm({
38339
38495
  init_remote2();
38340
38496
  init_events();
38341
38497
  init_manager();
38498
+ init_version();
38342
38499
  examples2 = [
38343
38500
  ["Create a project with the interactive wizard", "hyperframes init my-video"],
38344
38501
  ["Pick a starter example", "hyperframes init my-video --example warm-grain"],
38345
38502
  ["Start from an existing video file", "hyperframes init my-video --video clip.mp4"],
38346
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"],
38347
38505
  ["Non-interactive mode (for CI or AI agents)", "hyperframes init my-video --non-interactive"],
38348
38506
  ["Skip AI coding skills installation", "hyperframes init my-video --skip-skills"]
38349
38507
  ];
@@ -38356,6 +38514,9 @@ var init_init = __esm({
38356
38514
  hasAudio: false,
38357
38515
  videoCodec: "h264"
38358
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";
38359
38520
  init_default = defineCommand({
38360
38521
  meta: {
38361
38522
  name: "init",
@@ -38408,6 +38569,10 @@ var init_init = __esm({
38408
38569
  "skip-skills": {
38409
38570
  type: "boolean",
38410
38571
  description: "Skip AI coding skills installation"
38572
+ },
38573
+ tailwind: {
38574
+ type: "boolean",
38575
+ description: "Add Tailwind CSS browser-runtime support"
38411
38576
  }
38412
38577
  },
38413
38578
  async run({ args }) {
@@ -38425,6 +38590,7 @@ var init_init = __esm({
38425
38590
  const audioFlag = args.audio;
38426
38591
  const skipTranscribe = args["skip-transcribe"] === true;
38427
38592
  const skipSkills = args["skip-skills"] === true;
38593
+ const tailwind = args.tailwind === true;
38428
38594
  const nonInteractive = args["non-interactive"] === true;
38429
38595
  const modelFlag = args.model;
38430
38596
  const languageFlag = args.language;
@@ -38494,7 +38660,8 @@ var init_init = __esm({
38494
38660
  basename5(destDir2),
38495
38661
  templateId2,
38496
38662
  localVideoName2,
38497
- videoDuration2
38663
+ videoDuration2,
38664
+ tailwind
38498
38665
  );
38499
38666
  } catch (err) {
38500
38667
  console.error(
@@ -38505,7 +38672,7 @@ var init_init = __esm({
38505
38672
  console.error(c.dim("Use --example blank for offline use."));
38506
38673
  process.exit(1);
38507
38674
  }
38508
- trackInitTemplate(templateId2);
38675
+ trackInitTemplate(templateId2, { tailwind });
38509
38676
  const transcriptFile2 = resolve22(destDir2, "transcript.json");
38510
38677
  if (existsSync39(transcriptFile2)) {
38511
38678
  await patchTranscript(destDir2, transcriptFile2);
@@ -38532,10 +38699,13 @@ var init_init = __esm({
38532
38699
  console.log(` ${c.dim("More patterns: hyperframes.heygen.com/guides/prompting")}`);
38533
38700
  console.log();
38534
38701
  console.log(` ${c.accent("4.")} Preview in the browser:`);
38535
- 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")}`);
38536
38706
  console.log();
38537
- console.log(` ${c.accent("5.")} Render to MP4 when ready:`);
38538
- 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")}`);
38539
38709
  console.log();
38540
38710
  console.log(` ${c.dim("Full docs: hyperframes.heygen.com")}`);
38541
38711
  return;
@@ -38663,7 +38833,7 @@ var init_init = __esm({
38663
38833
  spin.start(`Downloading example ${c.accent(templateId)}...`);
38664
38834
  }
38665
38835
  try {
38666
- await scaffoldProject(destDir, name, templateId, localVideoName, videoDuration);
38836
+ await scaffoldProject(destDir, name, templateId, localVideoName, videoDuration, tailwind);
38667
38837
  if (!isBundled) {
38668
38838
  spin.stop(c.success(`Downloaded ${templateId}`));
38669
38839
  }
@@ -38677,7 +38847,7 @@ ${c.dim("Use --example blank for offline use.")}`
38677
38847
  );
38678
38848
  process.exit(1);
38679
38849
  }
38680
- trackInitTemplate(templateId);
38850
+ trackInitTemplate(templateId, { tailwind });
38681
38851
  const transcriptFile = resolve22(destDir, "transcript.json");
38682
38852
  if (existsSync39(transcriptFile)) {
38683
38853
  await patchTranscript(destDir, transcriptFile);
@@ -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,6 +10,7 @@
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 |
14
15
  | **animejs** | `/animejs` | Anime.js animations registered on `window.__hfAnime` |
15
16
  | **css-animations** | `/css-animations` | CSS keyframes that HyperFrames can pause and seek |
@@ -23,9 +24,10 @@
23
24
  ## Commands
24
25
 
25
26
  ```bash
26
- npx hyperframes preview # preview in browser (studio editor)
27
- npx hyperframes render # render to MP4
28
- 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
29
31
  npx hyperframes lint --verbose # include info-level findings
30
32
  npx hyperframes lint --json # machine-readable output for CI
31
33
  npx hyperframes docs <topic> # reference docs in terminal
@@ -56,13 +58,13 @@ https://hyperframes.heygen.com/llms.txt
56
58
 
57
59
  ## Linting — ALWAYS RUN AFTER CHANGES
58
60
 
59
- 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:
60
62
 
61
63
  ```bash
62
- npx hyperframes lint
64
+ npm run check
63
65
  ```
64
66
 
65
- 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.
66
68
 
67
69
  ## Key Rules
68
70
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperframes",
3
- "version": "0.5.0-alpha.13",
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",