hyperframes 0.4.40 → 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.40" : "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,
@@ -26203,6 +26211,23 @@ async function applyVideoMetadataHints(page, hints) {
26203
26211
  [...hints]
26204
26212
  );
26205
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
+ }
26206
26231
  async function initializeSession(session) {
26207
26232
  const { page, serverUrl } = session;
26208
26233
  page.on("console", (msg) => {
@@ -26259,6 +26284,7 @@ async function initializeSession(session) {
26259
26284
  );
26260
26285
  }
26261
26286
  await page.evaluate(`document.fonts?.ready`);
26287
+ await waitForOptionalTailwindReady(page, pageReadyTimeout2);
26262
26288
  if (session.options.format === "png") {
26263
26289
  await initTransparentBackground(session.page);
26264
26290
  }
@@ -26324,6 +26350,7 @@ async function initializeSession(session) {
26324
26350
  await new Promise((r2) => setTimeout(r2, 100));
26325
26351
  }
26326
26352
  await page.evaluate(`document.fonts?.ready`);
26353
+ await waitForOptionalTailwindReady(page, pageReadyTimeout);
26327
26354
  warmupRunning = false;
26328
26355
  session.beginFrameTimeTicks = (warmupTicks + 10) * session.beginFrameIntervalMs;
26329
26356
  if (session.options.format === "png") {
@@ -34811,6 +34838,27 @@ function createRenderJob(config) {
34811
34838
  function normalizeCompositionSrcPath(srcPath) {
34812
34839
  return srcPath.replace(/\\/g, "/").replace(/^\.\//, "");
34813
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
+ }
34814
34862
  function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
34815
34863
  const normalizedEntryFile = normalizeCompositionSrcPath(entryFile);
34816
34864
  const { document: document2 } = parseHTML(indexHtml);
@@ -34825,16 +34873,8 @@ function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
34825
34873
  (candidate) => candidate.hasAttribute("data-composition-id")
34826
34874
  ) ?? null;
34827
34875
  if (!root) return null;
34828
- const hostClone = host.cloneNode(true);
34829
- hostClone.setAttribute("data-start", "0");
34830
- body.innerHTML = "";
34831
- if (root === host) {
34832
- body.appendChild(hostClone);
34833
- return document2.toString();
34834
- }
34835
- const rootClone = root.cloneNode(false);
34836
- rootClone.appendChild(hostClone);
34837
- body.appendChild(rootClone);
34876
+ const renderClone = createStandaloneEntryRenderClone(root, host);
34877
+ replaceBodyWithRenderClone(body, renderClone);
34838
34878
  return document2.toString();
34839
34879
  }
34840
34880
  async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSignal) {
@@ -34866,7 +34906,6 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
34866
34906
  }
34867
34907
  const enableChunkedEncode = cfg.enableChunkedEncode;
34868
34908
  const chunkedEncodeSize = cfg.chunkSizeFrames;
34869
- const enableStreamingEncode = cfg.enableStreamingEncode && !isPngSequence;
34870
34909
  let peakRssBytes = 0;
34871
34910
  let peakHeapUsedBytes = 0;
34872
34911
  const sampleMemory = () => {
@@ -35435,6 +35474,15 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35435
35474
  await closeCaptureSession(probeSession);
35436
35475
  probeSession = null;
35437
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
+ });
35438
35486
  const captureAttempts = [];
35439
35487
  const FORMAT_EXT2 = {
35440
35488
  mp4: ".mp4",
@@ -35974,28 +36022,47 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35974
36022
  } else {
35975
36023
  let streamingEncoder = null;
35976
36024
  let streamingEncoderClosed = false;
35977
- if (enableStreamingEncode) {
35978
- streamingEncoder = await spawnStreamingEncoder(
35979
- videoOnlyPath,
35980
- {
35981
- fps: job.config.fps,
35982
- width,
35983
- height,
35984
- codec: preset.codec,
35985
- preset: preset.preset,
35986
- quality: effectiveQuality,
35987
- bitrate: effectiveBitrate,
35988
- pixelFormat: preset.pixelFormat,
35989
- useGpu: job.config.useGpu,
35990
- imageFormat: captureOptions.format || "jpeg",
35991
- hdr: preset.hdr
35992
- },
35993
- abortSignal
35994
- );
35995
- 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
+ }
35996
36063
  }
35997
36064
  try {
35998
- if (enableStreamingEncode && streamingEncoder) {
36065
+ if (useStreamingEncode && streamingEncoder) {
35999
36066
  const reorderBuffer = createFrameReorderBuffer(0, totalFrames);
36000
36067
  const currentEncoder = streamingEncoder;
36001
36068
  if (workerCount > 1) {
@@ -37833,7 +37900,8 @@ var init_preview2 = __esm({
37833
37900
  var init_exports = {};
37834
37901
  __export(init_exports, {
37835
37902
  default: () => init_default,
37836
- examples: () => examples2
37903
+ examples: () => examples2,
37904
+ injectTailwindBrowserScript: () => injectTailwindBrowserScript
37837
37905
  });
37838
37906
  import {
37839
37907
  existsSync as existsSync38,
@@ -37924,6 +37992,92 @@ function getStaticTemplateDir(templateId) {
37924
37992
  function getSharedTemplateDir() {
37925
37993
  return resolveAssetDir(["..", "templates", "_shared"], ["templates", "_shared"]);
37926
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
+ }
37927
38081
  function patchVideoSrc(dir, videoFilename, durationSeconds) {
37928
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));
37929
38083
  for (const file of htmlFiles) {
@@ -38029,7 +38183,7 @@ async function handleVideoFile(videoPath, destDir, interactive) {
38029
38183
  }
38030
38184
  return { meta, localVideoName };
38031
38185
  }
38032
- async function scaffoldProject(destDir, name, templateId, localVideoName, durationSeconds) {
38186
+ async function scaffoldProject(destDir, name, templateId, localVideoName, durationSeconds, tailwind = false) {
38033
38187
  mkdirSync23(destDir, { recursive: true });
38034
38188
  const templateDir = getStaticTemplateDir(templateId);
38035
38189
  if (existsSync38(templateDir)) {
@@ -38038,6 +38192,7 @@ async function scaffoldProject(destDir, name, templateId, localVideoName, durati
38038
38192
  await fetchRemoteTemplate(templateId, destDir);
38039
38193
  }
38040
38194
  patchVideoSrc(destDir, localVideoName, durationSeconds);
38195
+ if (tailwind) writeTailwindSupport(destDir);
38041
38196
  writeFileSync18(
38042
38197
  resolve22(destDir, "meta.json"),
38043
38198
  JSON.stringify(
@@ -38055,6 +38210,7 @@ async function scaffoldProject(destDir, name, templateId, localVideoName, durati
38055
38210
  const { writeProjectConfig: writeProjectConfig2, DEFAULT_PROJECT_CONFIG: DEFAULT_PROJECT_CONFIG2 } = await Promise.resolve().then(() => (init_projectConfig(), projectConfig_exports));
38056
38211
  writeProjectConfig2(destDir, DEFAULT_PROJECT_CONFIG2);
38057
38212
  }
38213
+ writeDefaultPackageJson(destDir, name);
38058
38214
  const sharedDir = getSharedTemplateDir();
38059
38215
  if (existsSync38(sharedDir)) {
38060
38216
  for (const entry of readdirSync13(sharedDir, { withFileTypes: true })) {
@@ -38066,7 +38222,7 @@ async function scaffoldProject(destDir, name, templateId, localVideoName, durati
38066
38222
  }
38067
38223
  }
38068
38224
  }
38069
- 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;
38070
38226
  var init_init = __esm({
38071
38227
  "src/commands/init.ts"() {
38072
38228
  "use strict";
@@ -38078,11 +38234,13 @@ var init_init = __esm({
38078
38234
  init_remote2();
38079
38235
  init_events();
38080
38236
  init_manager();
38237
+ init_version();
38081
38238
  examples2 = [
38082
38239
  ["Create a project with the interactive wizard", "hyperframes init my-video"],
38083
38240
  ["Pick a starter example", "hyperframes init my-video --example warm-grain"],
38084
38241
  ["Start from an existing video file", "hyperframes init my-video --video clip.mp4"],
38085
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"],
38086
38244
  ["Non-interactive mode (for CI or AI agents)", "hyperframes init my-video --non-interactive"],
38087
38245
  ["Skip AI coding skills installation", "hyperframes init my-video --skip-skills"]
38088
38246
  ];
@@ -38095,6 +38253,9 @@ var init_init = __esm({
38095
38253
  hasAudio: false,
38096
38254
  videoCodec: "h264"
38097
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";
38098
38259
  init_default = defineCommand({
38099
38260
  meta: {
38100
38261
  name: "init",
@@ -38147,6 +38308,10 @@ var init_init = __esm({
38147
38308
  "skip-skills": {
38148
38309
  type: "boolean",
38149
38310
  description: "Skip AI coding skills installation"
38311
+ },
38312
+ tailwind: {
38313
+ type: "boolean",
38314
+ description: "Add Tailwind CSS browser-runtime support"
38150
38315
  }
38151
38316
  },
38152
38317
  async run({ args }) {
@@ -38164,6 +38329,7 @@ var init_init = __esm({
38164
38329
  const audioFlag = args.audio;
38165
38330
  const skipTranscribe = args["skip-transcribe"] === true;
38166
38331
  const skipSkills = args["skip-skills"] === true;
38332
+ const tailwind = args.tailwind === true;
38167
38333
  const nonInteractive = args["non-interactive"] === true;
38168
38334
  const modelFlag = args.model;
38169
38335
  const languageFlag = args.language;
@@ -38233,7 +38399,8 @@ var init_init = __esm({
38233
38399
  basename5(destDir2),
38234
38400
  templateId2,
38235
38401
  localVideoName2,
38236
- videoDuration2
38402
+ videoDuration2,
38403
+ tailwind
38237
38404
  );
38238
38405
  } catch (err) {
38239
38406
  console.error(
@@ -38244,7 +38411,7 @@ var init_init = __esm({
38244
38411
  console.error(c.dim("Use --example blank for offline use."));
38245
38412
  process.exit(1);
38246
38413
  }
38247
- trackInitTemplate(templateId2);
38414
+ trackInitTemplate(templateId2, { tailwind });
38248
38415
  const transcriptFile2 = resolve22(destDir2, "transcript.json");
38249
38416
  if (existsSync38(transcriptFile2)) {
38250
38417
  await patchTranscript(destDir2, transcriptFile2);
@@ -38271,10 +38438,13 @@ var init_init = __esm({
38271
38438
  console.log(` ${c.dim("More patterns: hyperframes.heygen.com/guides/prompting")}`);
38272
38439
  console.log();
38273
38440
  console.log(` ${c.accent("4.")} Preview in the browser:`);
38274
- 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")}`);
38275
38445
  console.log();
38276
- console.log(` ${c.accent("5.")} Render to MP4 when ready:`);
38277
- 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")}`);
38278
38448
  console.log();
38279
38449
  console.log(` ${c.dim("Full docs: hyperframes.heygen.com")}`);
38280
38450
  return;
@@ -38402,7 +38572,7 @@ var init_init = __esm({
38402
38572
  spin.start(`Downloading example ${c.accent(templateId)}...`);
38403
38573
  }
38404
38574
  try {
38405
- await scaffoldProject(destDir, name, templateId, localVideoName, videoDuration);
38575
+ await scaffoldProject(destDir, name, templateId, localVideoName, videoDuration, tailwind);
38406
38576
  if (!isBundled) {
38407
38577
  spin.stop(c.success(`Downloaded ${templateId}`));
38408
38578
  }
@@ -38416,7 +38586,7 @@ ${c.dim("Use --example blank for offline use.")}`
38416
38586
  );
38417
38587
  process.exit(1);
38418
38588
  }
38419
- trackInitTemplate(templateId);
38589
+ trackInitTemplate(templateId, { tailwind });
38420
38590
  const transcriptFile = resolve22(destDir, "transcript.json");
38421
38591
  if (existsSync38(transcriptFile)) {
38422
38592
  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.4.40",
3
+ "version": "0.4.41",
4
4
  "description": "HyperFrames CLI — create, preview, and render HTML video compositions",
5
5
  "repository": {
6
6
  "type": "git",