@vibeframe/cli 0.57.3 → 0.59.0

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.
Files changed (2) hide show
  1. package/dist/index.js +2055 -395
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -6746,7 +6746,10 @@ var init_output = __esm({
6746
6746
  "pipeline highlights": { min: 0.05, max: 1, unit: "per analysis" },
6747
6747
  "pipeline auto-shorts": { min: 0.1, max: 2, unit: "per batch" },
6748
6748
  "pipeline animated-caption": { min: 0.01, max: 0.1, unit: "per video" },
6749
- "pipeline regenerate-scene": { min: 0.5, max: 5, unit: "per scene" }
6749
+ "pipeline regenerate-scene": { min: 0.5, max: 5, unit: "per scene" },
6750
+ // Scene composition (v0.59+) — per-beat composer call ≈ $0.06 (PR #111
6751
+ // pre-flight). Range covers a 1-beat preview to a 10-beat long-form.
6752
+ "compose scenes with skills": { min: 0.05, max: 1.5, unit: "per pipeline" }
6750
6753
  };
6751
6754
  }
6752
6755
  });
@@ -14966,7 +14969,7 @@ var init_GeminiProvider = __esm({
14966
14969
  * Sleep helper
14967
14970
  */
14968
14971
  sleep(ms) {
14969
- return new Promise((resolve53) => setTimeout(resolve53, ms));
14972
+ return new Promise((resolve54) => setTimeout(resolve54, ms));
14970
14973
  }
14971
14974
  /**
14972
14975
  * Extend a previously generated Veo video
@@ -19053,7 +19056,7 @@ var init_RunwayProvider = __esm({
19053
19056
  * Sleep helper
19054
19057
  */
19055
19058
  sleep(ms) {
19056
- return new Promise((resolve53) => setTimeout(resolve53, ms));
19059
+ return new Promise((resolve54) => setTimeout(resolve54, ms));
19057
19060
  }
19058
19061
  };
19059
19062
  RunwayProvider.API_VERSION = "2024-11-06";
@@ -19485,7 +19488,7 @@ var init_KlingProvider = __esm({
19485
19488
  * Sleep helper
19486
19489
  */
19487
19490
  sleep(ms) {
19488
- return new Promise((resolve53) => setTimeout(resolve53, ms));
19491
+ return new Promise((resolve54) => setTimeout(resolve54, ms));
19489
19492
  }
19490
19493
  };
19491
19494
  klingProvider = new KlingProvider();
@@ -19794,7 +19797,7 @@ var init_GrokProvider = __esm({
19794
19797
  }
19795
19798
  }
19796
19799
  sleep(ms) {
19797
- return new Promise((resolve53) => setTimeout(resolve53, ms));
19800
+ return new Promise((resolve54) => setTimeout(resolve54, ms));
19798
19801
  }
19799
19802
  };
19800
19803
  grokProvider = new GrokProvider();
@@ -19815,11 +19818,11 @@ var require_middleware = __commonJS({
19815
19818
  "use strict";
19816
19819
  var __awaiter = exports && exports.__awaiter || function(thisArg, _arguments, P, generator) {
19817
19820
  function adopt(value) {
19818
- return value instanceof P ? value : new P(function(resolve53) {
19819
- resolve53(value);
19821
+ return value instanceof P ? value : new P(function(resolve54) {
19822
+ resolve54(value);
19820
19823
  });
19821
19824
  }
19822
- return new (P || (P = Promise))(function(resolve53, reject) {
19825
+ return new (P || (P = Promise))(function(resolve54, reject) {
19823
19826
  function fulfilled(value) {
19824
19827
  try {
19825
19828
  step(generator.next(value));
@@ -19835,7 +19838,7 @@ var require_middleware = __commonJS({
19835
19838
  }
19836
19839
  }
19837
19840
  function step(result) {
19838
- result.done ? resolve53(result.value) : adopt(result.value).then(fulfilled, rejected);
19841
+ result.done ? resolve54(result.value) : adopt(result.value).then(fulfilled, rejected);
19839
19842
  }
19840
19843
  step((generator = generator.apply(thisArg, _arguments || [])).next());
19841
19844
  });
@@ -19920,11 +19923,11 @@ var require_response = __commonJS({
19920
19923
  "use strict";
19921
19924
  var __awaiter = exports && exports.__awaiter || function(thisArg, _arguments, P, generator) {
19922
19925
  function adopt(value) {
19923
- return value instanceof P ? value : new P(function(resolve53) {
19924
- resolve53(value);
19926
+ return value instanceof P ? value : new P(function(resolve54) {
19927
+ resolve54(value);
19925
19928
  });
19926
19929
  }
19927
- return new (P || (P = Promise))(function(resolve53, reject) {
19930
+ return new (P || (P = Promise))(function(resolve54, reject) {
19928
19931
  function fulfilled(value) {
19929
19932
  try {
19930
19933
  step(generator.next(value));
@@ -19940,7 +19943,7 @@ var require_response = __commonJS({
19940
19943
  }
19941
19944
  }
19942
19945
  function step(result) {
19943
- result.done ? resolve53(result.value) : adopt(result.value).then(fulfilled, rejected);
19946
+ result.done ? resolve54(result.value) : adopt(result.value).then(fulfilled, rejected);
19944
19947
  }
19945
19948
  step((generator = generator.apply(thisArg, _arguments || [])).next());
19946
19949
  });
@@ -20047,11 +20050,11 @@ var require_utils = __commonJS({
20047
20050
  "use strict";
20048
20051
  var __awaiter = exports && exports.__awaiter || function(thisArg, _arguments, P, generator) {
20049
20052
  function adopt(value) {
20050
- return value instanceof P ? value : new P(function(resolve53) {
20051
- resolve53(value);
20053
+ return value instanceof P ? value : new P(function(resolve54) {
20054
+ resolve54(value);
20052
20055
  });
20053
20056
  }
20054
- return new (P || (P = Promise))(function(resolve53, reject) {
20057
+ return new (P || (P = Promise))(function(resolve54, reject) {
20055
20058
  function fulfilled(value) {
20056
20059
  try {
20057
20060
  step(generator.next(value));
@@ -20067,7 +20070,7 @@ var require_utils = __commonJS({
20067
20070
  }
20068
20071
  }
20069
20072
  function step(result) {
20070
- result.done ? resolve53(result.value) : adopt(result.value).then(fulfilled, rejected);
20073
+ result.done ? resolve54(result.value) : adopt(result.value).then(fulfilled, rejected);
20071
20074
  }
20072
20075
  step((generator = generator.apply(thisArg, _arguments || [])).next());
20073
20076
  });
@@ -20160,7 +20163,7 @@ var require_utils = __commonJS({
20160
20163
  }
20161
20164
  function sleep2(ms) {
20162
20165
  return __awaiter(this, void 0, void 0, function* () {
20163
- return new Promise((resolve53) => setTimeout(resolve53, ms));
20166
+ return new Promise((resolve54) => setTimeout(resolve54, ms));
20164
20167
  });
20165
20168
  }
20166
20169
  }
@@ -20172,11 +20175,11 @@ var require_retry = __commonJS({
20172
20175
  "use strict";
20173
20176
  var __awaiter = exports && exports.__awaiter || function(thisArg, _arguments, P, generator) {
20174
20177
  function adopt(value) {
20175
- return value instanceof P ? value : new P(function(resolve53) {
20176
- resolve53(value);
20178
+ return value instanceof P ? value : new P(function(resolve54) {
20179
+ resolve54(value);
20177
20180
  });
20178
20181
  }
20179
- return new (P || (P = Promise))(function(resolve53, reject) {
20182
+ return new (P || (P = Promise))(function(resolve54, reject) {
20180
20183
  function fulfilled(value) {
20181
20184
  try {
20182
20185
  step(generator.next(value));
@@ -20192,7 +20195,7 @@ var require_retry = __commonJS({
20192
20195
  }
20193
20196
  }
20194
20197
  function step(result) {
20195
- result.done ? resolve53(result.value) : adopt(result.value).then(fulfilled, rejected);
20198
+ result.done ? resolve54(result.value) : adopt(result.value).then(fulfilled, rejected);
20196
20199
  }
20197
20200
  step((generator = generator.apply(thisArg, _arguments || [])).next());
20198
20201
  });
@@ -20399,11 +20402,11 @@ var require_request = __commonJS({
20399
20402
  "use strict";
20400
20403
  var __awaiter = exports && exports.__awaiter || function(thisArg, _arguments, P, generator) {
20401
20404
  function adopt(value) {
20402
- return value instanceof P ? value : new P(function(resolve53) {
20403
- resolve53(value);
20405
+ return value instanceof P ? value : new P(function(resolve54) {
20406
+ resolve54(value);
20404
20407
  });
20405
20408
  }
20406
- return new (P || (P = Promise))(function(resolve53, reject) {
20409
+ return new (P || (P = Promise))(function(resolve54, reject) {
20407
20410
  function fulfilled(value) {
20408
20411
  try {
20409
20412
  step(generator.next(value));
@@ -20419,7 +20422,7 @@ var require_request = __commonJS({
20419
20422
  }
20420
20423
  }
20421
20424
  function step(result) {
20422
- result.done ? resolve53(result.value) : adopt(result.value).then(fulfilled, rejected);
20425
+ result.done ? resolve54(result.value) : adopt(result.value).then(fulfilled, rejected);
20423
20426
  }
20424
20427
  step((generator = generator.apply(thisArg, _arguments || [])).next());
20425
20428
  });
@@ -20506,11 +20509,11 @@ var require_storage = __commonJS({
20506
20509
  "use strict";
20507
20510
  var __awaiter = exports && exports.__awaiter || function(thisArg, _arguments, P, generator) {
20508
20511
  function adopt(value) {
20509
- return value instanceof P ? value : new P(function(resolve53) {
20510
- resolve53(value);
20512
+ return value instanceof P ? value : new P(function(resolve54) {
20513
+ resolve54(value);
20511
20514
  });
20512
20515
  }
20513
- return new (P || (P = Promise))(function(resolve53, reject) {
20516
+ return new (P || (P = Promise))(function(resolve54, reject) {
20514
20517
  function fulfilled(value) {
20515
20518
  try {
20516
20519
  step(generator.next(value));
@@ -20526,7 +20529,7 @@ var require_storage = __commonJS({
20526
20529
  }
20527
20530
  }
20528
20531
  function step(result) {
20529
- result.done ? resolve53(result.value) : adopt(result.value).then(fulfilled, rejected);
20532
+ result.done ? resolve54(result.value) : adopt(result.value).then(fulfilled, rejected);
20530
20533
  }
20531
20534
  step((generator = generator.apply(thisArg, _arguments || [])).next());
20532
20535
  });
@@ -20854,11 +20857,11 @@ var require_auth = __commonJS({
20854
20857
  "use strict";
20855
20858
  var __awaiter = exports && exports.__awaiter || function(thisArg, _arguments, P, generator) {
20856
20859
  function adopt(value) {
20857
- return value instanceof P ? value : new P(function(resolve53) {
20858
- resolve53(value);
20860
+ return value instanceof P ? value : new P(function(resolve54) {
20861
+ resolve54(value);
20859
20862
  });
20860
20863
  }
20861
- return new (P || (P = Promise))(function(resolve53, reject) {
20864
+ return new (P || (P = Promise))(function(resolve54, reject) {
20862
20865
  function fulfilled(value) {
20863
20866
  try {
20864
20867
  step(generator.next(value));
@@ -20874,7 +20877,7 @@ var require_auth = __commonJS({
20874
20877
  }
20875
20878
  }
20876
20879
  function step(result) {
20877
- result.done ? resolve53(result.value) : adopt(result.value).then(fulfilled, rejected);
20880
+ result.done ? resolve54(result.value) : adopt(result.value).then(fulfilled, rejected);
20878
20881
  }
20879
20882
  step((generator = generator.apply(thisArg, _arguments || [])).next());
20880
20883
  });
@@ -20913,11 +20916,11 @@ var require_streaming = __commonJS({
20913
20916
  "use strict";
20914
20917
  var __awaiter = exports && exports.__awaiter || function(thisArg, _arguments, P, generator) {
20915
20918
  function adopt(value) {
20916
- return value instanceof P ? value : new P(function(resolve53) {
20917
- resolve53(value);
20919
+ return value instanceof P ? value : new P(function(resolve54) {
20920
+ resolve54(value);
20918
20921
  });
20919
20922
  }
20920
- return new (P || (P = Promise))(function(resolve53, reject) {
20923
+ return new (P || (P = Promise))(function(resolve54, reject) {
20921
20924
  function fulfilled(value) {
20922
20925
  try {
20923
20926
  step(generator.next(value));
@@ -20933,7 +20936,7 @@ var require_streaming = __commonJS({
20933
20936
  }
20934
20937
  }
20935
20938
  function step(result) {
20936
- result.done ? resolve53(result.value) : adopt(result.value).then(fulfilled, rejected);
20939
+ result.done ? resolve54(result.value) : adopt(result.value).then(fulfilled, rejected);
20937
20940
  }
20938
20941
  step((generator = generator.apply(thisArg, _arguments || [])).next());
20939
20942
  });
@@ -21166,7 +21169,7 @@ var require_streaming = __commonJS({
21166
21169
  query: options.queryParams
21167
21170
  });
21168
21171
  this.options = options;
21169
- this.donePromise = new Promise((resolve53, reject) => {
21172
+ this.donePromise = new Promise((resolve54, reject) => {
21170
21173
  if (this.streamClosed) {
21171
21174
  reject(new response_1.ApiError({
21172
21175
  message: "Streaming connection is already closed.",
@@ -21177,11 +21180,11 @@ var require_streaming = __commonJS({
21177
21180
  }
21178
21181
  this.signal.addEventListener("abort", () => {
21179
21182
  var _a2;
21180
- resolve53((_a2 = this.currentData) !== null && _a2 !== void 0 ? _a2 : {});
21183
+ resolve54((_a2 = this.currentData) !== null && _a2 !== void 0 ? _a2 : {});
21181
21184
  });
21182
21185
  this.on("done", (data) => {
21183
21186
  this.streamClosed = true;
21184
- resolve53(data);
21187
+ resolve54(data);
21185
21188
  });
21186
21189
  this.on("error", (error) => {
21187
21190
  this.streamClosed = true;
@@ -21206,7 +21209,7 @@ var require_streaming = __commonJS({
21206
21209
  if (data) {
21207
21210
  yield yield __await(data);
21208
21211
  }
21209
- yield __await(new Promise((resolve53) => setTimeout(resolve53, 16)));
21212
+ yield __await(new Promise((resolve54) => setTimeout(resolve54, 16)));
21210
21213
  }
21211
21214
  });
21212
21215
  }
@@ -21251,11 +21254,11 @@ var require_queue = __commonJS({
21251
21254
  "use strict";
21252
21255
  var __awaiter = exports && exports.__awaiter || function(thisArg, _arguments, P, generator) {
21253
21256
  function adopt(value) {
21254
- return value instanceof P ? value : new P(function(resolve53) {
21255
- resolve53(value);
21257
+ return value instanceof P ? value : new P(function(resolve54) {
21258
+ resolve54(value);
21256
21259
  });
21257
21260
  }
21258
- return new (P || (P = Promise))(function(resolve53, reject) {
21261
+ return new (P || (P = Promise))(function(resolve54, reject) {
21259
21262
  function fulfilled(value) {
21260
21263
  try {
21261
21264
  step(generator.next(value));
@@ -21271,7 +21274,7 @@ var require_queue = __commonJS({
21271
21274
  }
21272
21275
  }
21273
21276
  function step(result) {
21274
- result.done ? resolve53(result.value) : adopt(result.value).then(fulfilled, rejected);
21277
+ result.done ? resolve54(result.value) : adopt(result.value).then(fulfilled, rejected);
21275
21278
  }
21276
21279
  step((generator = generator.apply(thisArg, _arguments || [])).next());
21277
21280
  });
@@ -21406,7 +21409,7 @@ var require_queue = __commonJS({
21406
21409
  }
21407
21410
  return doneStatus;
21408
21411
  }
21409
- return new Promise((resolve53, reject) => {
21412
+ return new Promise((resolve54, reject) => {
21410
21413
  var _a;
21411
21414
  let pollingTimeoutId;
21412
21415
  const pollInterval = "pollInterval" in options && typeof options.pollInterval === "number" ? (_a = options.pollInterval) !== null && _a !== void 0 ? _a : DEFAULT_POLL_INTERVAL : DEFAULT_POLL_INTERVAL;
@@ -21438,7 +21441,7 @@ var require_queue = __commonJS({
21438
21441
  }
21439
21442
  if (requestStatus.status === "COMPLETED") {
21440
21443
  clearScheduledTasks();
21441
- resolve53(requestStatus);
21444
+ resolve54(requestStatus);
21442
21445
  return;
21443
21446
  }
21444
21447
  pollingTimeoutId = setTimeout(poll, pollInterval);
@@ -23399,11 +23402,11 @@ var require_realtime = __commonJS({
23399
23402
  "use strict";
23400
23403
  var __awaiter = exports && exports.__awaiter || function(thisArg, _arguments, P, generator) {
23401
23404
  function adopt(value) {
23402
- return value instanceof P ? value : new P(function(resolve53) {
23403
- resolve53(value);
23405
+ return value instanceof P ? value : new P(function(resolve54) {
23406
+ resolve54(value);
23404
23407
  });
23405
23408
  }
23406
- return new (P || (P = Promise))(function(resolve53, reject) {
23409
+ return new (P || (P = Promise))(function(resolve54, reject) {
23407
23410
  function fulfilled(value) {
23408
23411
  try {
23409
23412
  step(generator.next(value));
@@ -23419,7 +23422,7 @@ var require_realtime = __commonJS({
23419
23422
  }
23420
23423
  }
23421
23424
  function step(result) {
23422
- result.done ? resolve53(result.value) : adopt(result.value).then(fulfilled, rejected);
23425
+ result.done ? resolve54(result.value) : adopt(result.value).then(fulfilled, rejected);
23423
23426
  }
23424
23427
  step((generator = generator.apply(thisArg, _arguments || [])).next());
23425
23428
  });
@@ -23744,11 +23747,11 @@ var require_client = __commonJS({
23744
23747
  "use strict";
23745
23748
  var __awaiter = exports && exports.__awaiter || function(thisArg, _arguments, P, generator) {
23746
23749
  function adopt(value) {
23747
- return value instanceof P ? value : new P(function(resolve53) {
23748
- resolve53(value);
23750
+ return value instanceof P ? value : new P(function(resolve54) {
23751
+ resolve54(value);
23749
23752
  });
23750
23753
  }
23751
- return new (P || (P = Promise))(function(resolve53, reject) {
23754
+ return new (P || (P = Promise))(function(resolve54, reject) {
23752
23755
  function fulfilled(value) {
23753
23756
  try {
23754
23757
  step(generator.next(value));
@@ -23764,7 +23767,7 @@ var require_client = __commonJS({
23764
23767
  }
23765
23768
  }
23766
23769
  function step(result) {
23767
- result.done ? resolve53(result.value) : adopt(result.value).then(fulfilled, rejected);
23770
+ result.done ? resolve54(result.value) : adopt(result.value).then(fulfilled, rejected);
23768
23771
  }
23769
23772
  step((generator = generator.apply(thisArg, _arguments || [])).next());
23770
23773
  });
@@ -24329,7 +24332,7 @@ var init_ReplicateProvider = __esm({
24329
24332
  * Sleep helper
24330
24333
  */
24331
24334
  sleep(ms) {
24332
- return new Promise((resolve53) => setTimeout(resolve53, ms));
24335
+ return new Promise((resolve54) => setTimeout(resolve54, ms));
24333
24336
  }
24334
24337
  /**
24335
24338
  * Generate music from text prompt using MusicGen
@@ -25315,7 +25318,7 @@ async function prompt(question, hidden = false) {
25315
25318
  input: process.stdin,
25316
25319
  output: process.stdout
25317
25320
  });
25318
- return new Promise((resolve53) => {
25321
+ return new Promise((resolve54) => {
25319
25322
  if (hidden && process.stdin.isTTY) {
25320
25323
  process.stdout.write(question);
25321
25324
  let input = "";
@@ -25329,7 +25332,7 @@ async function prompt(question, hidden = false) {
25329
25332
  process.stdin.removeListener("data", onData);
25330
25333
  process.stdout.write("\n");
25331
25334
  rl.close();
25332
- resolve53(input);
25335
+ resolve54(input);
25333
25336
  } else if (char === "") {
25334
25337
  process.exit(1);
25335
25338
  } else if (char === "\x7F" || char === "\b") {
@@ -25344,7 +25347,7 @@ async function prompt(question, hidden = false) {
25344
25347
  } else {
25345
25348
  rl.question(question, (answer) => {
25346
25349
  rl.close();
25347
- resolve53(answer);
25350
+ resolve54(answer);
25348
25351
  });
25349
25352
  }
25350
25353
  });
@@ -25611,6 +25614,70 @@ function buildEmptyRootHtml(opts) {
25611
25614
  </html>
25612
25615
  `;
25613
25616
  }
25617
+ function buildDesignMd(opts) {
25618
+ const { name, style } = opts;
25619
+ const intro = style ? `Visual identity for **${name}**, scaffolded from the **${style.name}** style (after ${style.designer}). Customise freely \u2014 this file is the single source of truth for every scene's palette, typography, and motion.` : `Visual identity for **${name}**. Fill the sections below before authoring any scene HTML or generating any backdrop. Pick a named style with \`vibe scene styles\` if you want a credible starting point.`;
25620
+ const moodLine = style ? `**Mood:** ${style.mood} \xB7 **Best for:** ${style.bestFor}` : `**Mood:** _(one line \u2014 what should the viewer FEEL?)_`;
25621
+ const palette = style ? `${style.palette.map((c) => `- \`${c}\``).join("\n")}
25622
+
25623
+ ${style.paletteNotes}` : `- _hex_ \u2014 primary
25624
+ - _hex_ \u2014 accent
25625
+
25626
+ _2\u20133 colours max. Declare explicit hex values; never name colours abstractly._`;
25627
+ const typography = style ? style.typography : `_One family, two weights. State the role of each (headline / label / body)._`;
25628
+ const composition = style ? style.composition : `_Grid? Centered? Layered? How does negative space behave?_`;
25629
+ const motion = style ? `${style.motion}
25630
+
25631
+ **GSAP signature:** ${style.gsapSignature}` : `_How fast? Snappy or fluid? Overshoot or precision?_
25632
+
25633
+ **GSAP signature:** _e.g. \`expo.out\`, \`sine.inOut\`, \`back.out(1.8)\`_`;
25634
+ const transition = style ? style.transition : `_Which Hyperframes shader matches the energy? (Cinematic Zoom, Cross-Warp Morph, Glitch, Domain Warp, \u2026)_`;
25635
+ const avoid = style ? style.avoid.map((a) => `- ${a}`).join("\n") : `- _anti-pattern 1_
25636
+ - _anti-pattern 2_
25637
+ - _anti-pattern 3_`;
25638
+ return `# ${name} \u2014 Design
25639
+
25640
+ > **Hard-gate.** This file defines the visual identity of every scene.
25641
+ > Author it before generating any HTML, backdrop image, or motion.
25642
+ > The Hyperframes \`hyperframes\` skill enforces this: scenes that
25643
+ > contradict DESIGN.md are rejected.
25644
+
25645
+ ${intro}
25646
+
25647
+ ## Style
25648
+
25649
+ ${moodLine}
25650
+
25651
+ ## Palette
25652
+
25653
+ ${palette}
25654
+
25655
+ ## Typography
25656
+
25657
+ ${typography}
25658
+
25659
+ ## Composition
25660
+
25661
+ ${composition}
25662
+
25663
+ ## Motion
25664
+
25665
+ ${motion}
25666
+
25667
+ ## Transition
25668
+
25669
+ ${transition}
25670
+
25671
+ ## What NOT to do
25672
+
25673
+ ${avoid}
25674
+
25675
+ ---
25676
+
25677
+ _Browse other named styles: \`vibe scene styles\`_
25678
+ ${style ? `_This file was seeded by \`vibe scene init --visual-style "${style.name}"\`._` : `_Seed this file from a named style: \`vibe scene init <dir> --visual-style "<name>"\`._`}
25679
+ `;
25680
+ }
25614
25681
  function buildProjectClaudeMd(name) {
25615
25682
  return `# ${name} \u2014 Scene Authoring Project
25616
25683
 
@@ -25618,6 +25685,16 @@ This project is **bilingual**: it works with both VibeFrame (\`vibe\`) and
25618
25685
  HeyGen Hyperframes (\`hyperframes\`). You can run either CLI inside this
25619
25686
  directory.
25620
25687
 
25688
+ ## Visual identity hard-gate
25689
+
25690
+ **Author \`DESIGN.md\` before any scene HTML.** It defines palette,
25691
+ typography, motion, and transition rules. Both the agent-driven path and
25692
+ the fallback emit reference it; scenes that contradict DESIGN.md are
25693
+ rejected by the Hyperframes \`hyperframes\` skill.
25694
+
25695
+ Browse named styles: \`vibe scene styles\`. Re-seed from one with
25696
+ \`vibe scene init . --visual-style "Swiss Pulse"\` (idempotent).
25697
+
25621
25698
  ## Skills \u2014 USE THESE FIRST
25622
25699
 
25623
25700
  **Always invoke the relevant skill before authoring scenes.** Skills encode
@@ -25626,14 +25703,23 @@ semantics, VibeFrame pipeline conventions) that are NOT in generic web docs.
25626
25703
 
25627
25704
  | Skill | Command | When to use |
25628
25705
  | ----------------- | ---------------- | ------------------------------------------------------------------------------------- |
25629
- | **vibe-scene** | \`/vibe-scene\` | Authoring / editing per-scene HTML in this project \u2014 preferred |
25630
- | **hyperframes** | \`/hyperframes\` | Fallback: direct HF authoring (install via \`npx skills add heygen-com/hyperframes\`) |
25706
+ | **hyperframes** | \`/hyperframes\` | Cinematic-quality composition \u2014 DESIGN.md hard-gate, named styles, motion principles |
25707
+ | **vibe-scene** | \`/vibe-scene\` | VibeFrame's authoring loop, AI assets, lint feedback, pipeline integration |
25631
25708
  | **gsap** | \`/gsap\` | GSAP tweens, timelines, easing |
25632
25709
 
25633
- If the skill is not available, follow the **Key Rules** below.
25710
+ Install the Hyperframes skills once per machine:
25711
+
25712
+ \`\`\`bash
25713
+ npx skills add heygen-com/hyperframes
25714
+ \`\`\`
25715
+
25716
+ Restart your agent session (or reload the skill list) after installing.
25717
+ If skills aren't available, follow the **Key Rules** below \u2014 they cover
25718
+ the framework-level minimum, not the cinematic craft layer.
25634
25719
 
25635
25720
  ## Project structure
25636
25721
 
25722
+ - \`DESIGN.md\` \u2014 visual identity contract (palette, type, motion, transitions)
25637
25723
  - \`index.html\` \u2014 root composition (timeline)
25638
25724
  - \`compositions/scene-*.html\` \u2014 per-scene HTML authored by you or the agent
25639
25725
  - \`assets/\` \u2014 shared media (narration audio, images, video)
@@ -25751,6 +25837,17 @@ async function scaffoldSceneProject(opts) {
25751
25837
  await writeFile4(claudePath, buildProjectClaudeMd(name), "utf-8");
25752
25838
  created.push(claudePath);
25753
25839
  }
25840
+ const designPath = resolve5(dir, "DESIGN.md");
25841
+ if (await pathExists(designPath)) {
25842
+ skipped.push(designPath);
25843
+ } else {
25844
+ await writeFile4(
25845
+ designPath,
25846
+ buildDesignMd({ name, style: opts.visualStyle }),
25847
+ "utf-8"
25848
+ );
25849
+ created.push(designPath);
25850
+ }
25754
25851
  const gitignorePath = resolve5(dir, ".gitignore");
25755
25852
  if (await pathExists(gitignorePath)) {
25756
25853
  skipped.push(gitignorePath);
@@ -25774,6 +25871,180 @@ var init_scene_project = __esm({
25774
25871
  }
25775
25872
  });
25776
25873
 
25874
+ // src/commands/_shared/visual-styles.ts
25875
+ function listVisualStyles() {
25876
+ return STYLES;
25877
+ }
25878
+ function getVisualStyle(query) {
25879
+ const q = query.trim().toLowerCase();
25880
+ return STYLES.find(
25881
+ (s) => s.name.toLowerCase() === q || s.slug === q
25882
+ );
25883
+ }
25884
+ function visualStyleNames() {
25885
+ return STYLES.map((s) => `"${s.name}"`).join(", ");
25886
+ }
25887
+ var STYLES;
25888
+ var init_visual_styles = __esm({
25889
+ "src/commands/_shared/visual-styles.ts"() {
25890
+ "use strict";
25891
+ STYLES = [
25892
+ {
25893
+ name: "Swiss Pulse",
25894
+ slug: "swiss-pulse",
25895
+ designer: "Josef M\xFCller-Brockmann",
25896
+ mood: "Clinical, precise",
25897
+ bestFor: "SaaS dashboards, developer tools, APIs, metrics",
25898
+ palette: ["#1a1a1a", "#ffffff", "#0066FF"],
25899
+ paletteNotes: "Black, white, ONE accent \u2014 electric blue (#0066FF) or amber (#FFB300). Never both accents at once.",
25900
+ typography: "Helvetica or Inter Bold for headlines, Regular for labels. Numbers dominate at 80\u2013120px.",
25901
+ composition: "Grid-locked. Every element snaps to an invisible 12-column grid. Hard cuts only \u2014 no decorative transitions.",
25902
+ motion: "Animated counters count up from 0. Entries are fast and snap into place. Nothing floats.",
25903
+ transition: "Cinematic Zoom or SDF Iris (precise, geometric)",
25904
+ gsapSignature: "expo.out, power4.out \u2014 fast arrivals, hard stops",
25905
+ avoid: [
25906
+ "Decorative transitions (fades, dissolves) \u2014 use hard cuts",
25907
+ "Two accent colours competing in one frame",
25908
+ "Off-grid placement or floating elements"
25909
+ ]
25910
+ },
25911
+ {
25912
+ name: "Velvet Standard",
25913
+ slug: "velvet-standard",
25914
+ designer: "Massimo Vignelli",
25915
+ mood: "Premium, timeless",
25916
+ bestFor: "Luxury products, enterprise software, keynotes, investor decks",
25917
+ palette: ["#000000", "#ffffff", "#1a237e"],
25918
+ paletteNotes: "Black, white, ONE rich accent \u2014 deep navy (#1a237e) or gold (#c9a84c).",
25919
+ typography: "Thin sans-serif, ALL CAPS, wide letter-spacing (0.15em+). Sequential reveals only.",
25920
+ composition: "Generous negative space. Symmetrical, centered, architectural precision. Nothing busy.",
25921
+ motion: "Slow, deliberate. Sequential reveals with long holds. No frantic motion.",
25922
+ transition: "Cross-Warp Morph (elegant, organic flow between scenes)",
25923
+ gsapSignature: "sine.inOut, power1 \u2014 nothing snaps, everything glides",
25924
+ avoid: [
25925
+ "Tight letter-spacing \u2014 kills the premium register",
25926
+ "Bouncy or elastic easings \u2014 too playful",
25927
+ "Multiple elements arriving at once \u2014 break sequence"
25928
+ ]
25929
+ },
25930
+ {
25931
+ name: "Deconstructed",
25932
+ slug: "deconstructed",
25933
+ designer: "Neville Brody",
25934
+ mood: "Industrial, raw",
25935
+ bestFor: "Tech news, developer launches, security products, punk-energy reveals",
25936
+ palette: ["#1a1a1a", "#D4501E", "#f0f0f0"],
25937
+ paletteNotes: "Dark grey (#1a1a1a), rust orange (#D4501E), raw white (#f0f0f0).",
25938
+ typography: "Type at angles, overlapping edges, escaping frames. Bold industrial weight.",
25939
+ composition: "Gritty textures \u2014 scan-line effects, glitch artifacts baked into the design.",
25940
+ motion: "Text SLAMS and SHATTERS. Letters scramble then snap to final position.",
25941
+ transition: "Glitch shader or Whip Pan (breaks the rules, feels aggressive)",
25942
+ gsapSignature: "back.out(2.5), steps(8), elastic.out(1.2, 0.4) \u2014 intentional irregularity",
25943
+ avoid: [
25944
+ "Polished, centered compositions \u2014 must feel raw",
25945
+ "Smooth fades \u2014 use glitch / scramble entries",
25946
+ "Soft easings \u2014 every motion lands hard"
25947
+ ]
25948
+ },
25949
+ {
25950
+ name: "Maximalist Type",
25951
+ slug: "maximalist-type",
25952
+ designer: "Paula Scher",
25953
+ mood: "Loud, kinetic",
25954
+ bestFor: "Big product launches, milestone announcements, high-energy hype videos",
25955
+ palette: ["#E63946", "#FFD60A", "#000000", "#ffffff"],
25956
+ paletteNotes: "Bold saturated: red (#E63946), yellow (#FFD60A), black, white \u2014 maximum contrast.",
25957
+ typography: "Text IS the visual. Overlapping type layers at different scales and angles, filling 50\u201380% of frame.",
25958
+ composition: "Text layered OVER footage \u2014 never empty backgrounds. 2\u20133 second rapid-fire scenes.",
25959
+ motion: "Everything is kinetic \u2014 slamming, sliding, scaling. No static moments.",
25960
+ transition: "Ridged Burn (explosive, dramatic, impossible to ignore)",
25961
+ gsapSignature: "expo.out, back.out(1.8) \u2014 fast arrivals, hard stops",
25962
+ avoid: [
25963
+ "Static moments \u2014 every frame must move",
25964
+ "Empty backgrounds \u2014 text and footage must layer",
25965
+ "Single typeface at a single size \u2014 must be layered"
25966
+ ]
25967
+ },
25968
+ {
25969
+ name: "Data Drift",
25970
+ slug: "data-drift",
25971
+ designer: "Refik Anadol",
25972
+ mood: "Futuristic, immersive",
25973
+ bestFor: "AI products, ML platforms, data companies, speculative tech",
25974
+ palette: ["#0a0a0a", "#7c3aed", "#06b6d4"],
25975
+ paletteNotes: "Iridescent \u2014 deep black (#0a0a0a), electric purple (#7c3aed), cyan (#06b6d4).",
25976
+ typography: "Thin futuristic sans-serif \u2014 floating, weightless, minimal text.",
25977
+ composition: "Fluid morphing compositions. Extreme scale shifts (micro \u2192 macro). Particles coalesce into numbers.",
25978
+ motion: "Light traces data paths through the frame. Smooth, continuous, organic \u2014 nothing hard.",
25979
+ transition: "Gravitational Lens or Domain Warp (otherworldly distortion)",
25980
+ gsapSignature: "sine.inOut, power2.out \u2014 smooth, continuous, organic",
25981
+ avoid: [
25982
+ "Hard cuts \u2014 break the immersion",
25983
+ "Heavy/bold typography \u2014 too grounded",
25984
+ "Static compositions \u2014 must feel like flow"
25985
+ ]
25986
+ },
25987
+ {
25988
+ name: "Soft Signal",
25989
+ slug: "soft-signal",
25990
+ designer: "Stefan Sagmeister",
25991
+ mood: "Intimate, warm",
25992
+ bestFor: "Wellness brands, personal stories, lifestyle products, human-centered apps",
25993
+ palette: ["#F5A623", "#FFF8EC", "#C4A3A3", "#8FAF8C"],
25994
+ paletteNotes: "Warm amber (#F5A623), cream (#FFF8EC), dusty rose (#C4A3A3), sage green (#8FAF8C).",
25995
+ typography: "Handwritten-style or humanist serif fonts. Personal, lowercase, delicate.",
25996
+ composition: "Close-up framing feel \u2014 single element fills the frame. Nothing feels corporate.",
25997
+ motion: "Slow drifts and floats, never snaps. Soft organic motion throughout.",
25998
+ transition: "Thermal Distortion (warm, flowing, like heat shimmer)",
25999
+ gsapSignature: "sine.inOut, power1.inOut \u2014 everything breathes",
26000
+ avoid: [
26001
+ "Sharp geometric layouts \u2014 break the warmth",
26002
+ "Hard easings or snaps \u2014 too clinical",
26003
+ "Cool tones (blue/green-blue) without warm balance"
26004
+ ]
26005
+ },
26006
+ {
26007
+ name: "Folk Frequency",
26008
+ slug: "folk-frequency",
26009
+ designer: "Eduardo Terrazas",
26010
+ mood: "Cultural, vivid",
26011
+ bestFor: "Consumer apps, food platforms, community products, festive launches",
26012
+ palette: ["#FF1493", "#0047AB", "#FFE000", "#009B77"],
26013
+ paletteNotes: "Vivid folk: hot pink (#FF1493), cobalt blue (#0047AB), sun yellow (#FFE000), emerald (#009B77).",
26014
+ typography: "Bold warm rounded type. Every frame feels handcrafted.",
26015
+ composition: "Pattern and repetition \u2014 folk art rhythm and density. Layered compositions with rich visual texture.",
26016
+ motion: "Colorful motion \u2014 elements bounce, pop, and spin into place with joy.",
26017
+ transition: "Swirl Vortex or Ripple Waves (hypnotic, celebratory)",
26018
+ gsapSignature: "back.out(1.6), elastic.out(1, 0.5) \u2014 overshoots feel intentional",
26019
+ avoid: [
26020
+ "Muted or monochrome palettes \u2014 kill the celebration",
26021
+ "Pure flat / minimal compositions \u2014 must feel layered",
26022
+ "Linear easings \u2014 motion should feel joyful"
26023
+ ]
26024
+ },
26025
+ {
26026
+ name: "Shadow Cut",
26027
+ slug: "shadow-cut",
26028
+ designer: "Hans Hillmann",
26029
+ mood: "Dark, cinematic",
26030
+ bestFor: "Security products, dramatic reveals, investigative content, intense launches",
26031
+ palette: ["#0a0a0a", "#3a3a3a", "#ffffff", "#C1121F"],
26032
+ paletteNotes: "Near-monochrome \u2014 deep blacks (#0a0a0a), cold greys (#3a3a3a), stark white + ONE accent (blood red #C1121F or toxic green #39FF14).",
26033
+ typography: "Sharp angular text like film noir title cards. Heavy contrast, no softness.",
26034
+ composition: "Heavy shadow \u2014 elements emerge from darkness. The reveal IS the narrative.",
26035
+ motion: "Slow creeping push-ins, dramatic scale reveals. Silence before the hit matters.",
26036
+ transition: "Domain Warp (dissolves reality before revealing the next scene)",
26037
+ gsapSignature: "power4.in for exits, power3.out for dramatic reveals \u2014 pause before the hit",
26038
+ avoid: [
26039
+ "Bright or saturated palettes \u2014 kill the cinematic mood",
26040
+ "Bouncy easings \u2014 break the tension",
26041
+ "Quick cuts \u2014 let the reveal breathe"
26042
+ ]
26043
+ }
26044
+ ];
26045
+ }
26046
+ });
26047
+
25777
26048
  // src/commands/_shared/scene-html-emit.ts
25778
26049
  function esc(text) {
25779
26050
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -25799,6 +26070,27 @@ function buildTranscriptTweens(transcript, targetSelector) {
25799
26070
  return `tl.fromTo('${sel}', { opacity: 0, y: 10 }, { opacity: 1, y: 0, duration: 0.18, ease: 'power2.out' }, ${start});`;
25800
26071
  }).join("\n ");
25801
26072
  }
26073
+ function crossfadeTweens(scope, dur) {
26074
+ return `tl.from('${scope}', { opacity: 0, duration: ${SCENE_OVERLAP_SECONDS}, ease: 'power2.out' }, 0);
26075
+ tl.to('${scope}', { opacity: 0, duration: ${SCENE_OVERLAP_SECONDS}, ease: 'power2.in' }, ${(dur - SCENE_OVERLAP_SECONDS).toFixed(2)});`;
26076
+ }
26077
+ function fitFontSize(text, baseMaxPx, minPx, comfortableChars) {
26078
+ const charCount = text.length;
26079
+ if (charCount <= comfortableChars) return baseMaxPx;
26080
+ const ratio = comfortableChars / charCount;
26081
+ return Math.max(minPx, Math.round(baseMaxPx * ratio));
26082
+ }
26083
+ function fitKineticFontSize(words) {
26084
+ const totalChars = words.join(" ").length;
26085
+ if (totalChars <= 18) return 180;
26086
+ if (totalChars <= 26) return 140;
26087
+ if (totalChars <= 38) return 110;
26088
+ if (totalChars <= 54) return 90;
26089
+ return 72;
26090
+ }
26091
+ function kenBurnsTween(scope, dur, endScale = 1.08) {
26092
+ return `tl.fromTo('${scope} .backdrop', { scale: 1.0 }, { scale: ${endScale}, duration: ${dur.toFixed(2)}, ease: 'none' }, 0);`;
26093
+ }
25802
26094
  function buildPreset(input) {
25803
26095
  const id = input.id;
25804
26096
  const scope = `[data-composition-id="${id}"]`;
@@ -25835,7 +26127,8 @@ function buildPreset(input) {
25835
26127
  const captionInner = useWordSync ? renderTranscriptSpans(transcript) : esc(captionText);
25836
26128
  const wordCss = useWordSync ? `
25837
26129
  ${scope} .caption .word { display: inline-block; opacity: 0; }` : "";
25838
- const timeline = useWordSync ? buildTranscriptTweens(transcript, `${scope} .caption .word`) : `tl.from('${scope} .caption', { opacity: 0, y: 28, duration: 0.45, ease: 'power3.out' }, 0.05);`;
26130
+ const captionFontPx = fitFontSize(captionText, 56, 28, 60);
26131
+ const captionTween = useWordSync ? buildTranscriptTweens(transcript, `${scope} .caption .word`) : `tl.from('${scope} .caption', { opacity: 0, y: 28, duration: 0.45, ease: 'power3.out' }, 0.05);`;
25839
26132
  return {
25840
26133
  css: `${scope} {
25841
26134
  position: absolute; inset: 0; width: 100%; height: 100%;
@@ -25843,21 +26136,26 @@ function buildPreset(input) {
25843
26136
  color: #fff; overflow: hidden; background: #000;
25844
26137
  }
25845
26138
  ${backdrop}
26139
+ ${scope} .backdrop { transform-origin: center; }
25846
26140
  ${scope} .caption {
25847
26141
  position: absolute;
25848
26142
  left: 8%; right: 8%; bottom: 12%;
25849
26143
  text-align: center;
25850
- font-size: 56px;
26144
+ font-size: ${captionFontPx}px;
25851
26145
  font-weight: 700;
25852
26146
  line-height: 1.2;
26147
+ overflow-wrap: break-word;
25853
26148
  text-shadow: 0 4px 20px rgba(0,0,0,0.65);
25854
26149
  }${wordCss}`,
25855
26150
  body: `${backdropMarkup}
25856
26151
  <div class="caption" id="caption">${captionInner}</div>`,
25857
- timeline
26152
+ timeline: `${crossfadeTweens(scope, dur)}
26153
+ ${kenBurnsTween(scope, dur)}
26154
+ ${captionTween}`
25858
26155
  };
25859
26156
  }
25860
26157
  case "announcement": {
26158
+ const fontSize = fitFontSize(headline, 160, 72, 22);
25861
26159
  return {
25862
26160
  css: `${scope} {
25863
26161
  position: absolute; inset: 0; width: 100%; height: 100%;
@@ -25865,6 +26163,7 @@ function buildPreset(input) {
25865
26163
  color: #fff; overflow: hidden; background: #000;
25866
26164
  }
25867
26165
  ${backdrop}
26166
+ ${scope} .backdrop { transform-origin: center; }
25868
26167
  ${scope} .announce {
25869
26168
  position: absolute; inset: 0;
25870
26169
  display: flex; align-items: center; justify-content: center;
@@ -25873,10 +26172,11 @@ function buildPreset(input) {
25873
26172
  }
25874
26173
  ${scope} .announce h1 {
25875
26174
  margin: 0;
25876
- font-size: 160px;
26175
+ font-size: ${fontSize}px;
25877
26176
  font-weight: 900;
25878
26177
  letter-spacing: -4px;
25879
- line-height: 1;
26178
+ line-height: 1.05;
26179
+ overflow-wrap: break-word;
25880
26180
  background: linear-gradient(90deg, #8e2de2, #00c9ff);
25881
26181
  -webkit-background-clip: text; background-clip: text; color: transparent;
25882
26182
  text-shadow: 0 8px 40px rgba(142,45,226,0.35);
@@ -25886,8 +26186,11 @@ function buildPreset(input) {
25886
26186
  // Faster, snappier entrance and no tail fade-out — see the
25887
26187
  // matching note in the `simple` case above. The headline stays
25888
26188
  // on screen until the producer cuts to the next clip; the next
25889
- // scene's own fade-in handles the visual transition.
25890
- timeline: `tl.from('${scope} #headline', { opacity: 0, scale: 0.92, duration: 0.55, ease: 'power3.out' }, 0.05);`
26189
+ // scene's own fade-in handles the visual transition. Backdrop
26190
+ // Ken-Burns fills what used to be 80% dead static frame.
26191
+ timeline: `${crossfadeTweens(scope, dur)}
26192
+ ${kenBurnsTween(scope, dur)}
26193
+ tl.from('${scope} #headline', { opacity: 0, scale: 0.92, duration: 0.55, ease: 'power3.out' }, 0.05);`
25891
26194
  };
25892
26195
  }
25893
26196
  case "explainer": {
@@ -25899,6 +26202,7 @@ function buildPreset(input) {
25899
26202
  const wordCss = useWordSync ? `
25900
26203
  ${scope} #subtitle .word { display: inline-block; opacity: 0; }` : "";
25901
26204
  const subtitleTween = useWordSync ? buildTranscriptTweens(transcript, `${scope} #subtitle .word`) : sub ? `tl.from('${scope} #subtitle', { opacity: 0, y: 30, duration: 0.55, ease: 'power3.out' }, 0.55);` : "";
26205
+ const titleFontPx = fitFontSize(headline, 110, 56, 30);
25902
26206
  return {
25903
26207
  css: `${scope} {
25904
26208
  position: absolute; inset: 0; width: 100%; height: 100%;
@@ -25906,6 +26210,7 @@ function buildPreset(input) {
25906
26210
  color: #fff; overflow: hidden; background: #000;
25907
26211
  }
25908
26212
  ${backdrop}
26213
+ ${scope} .backdrop { transform-origin: center; }
25909
26214
  ${scope} .stage {
25910
26215
  position: absolute; inset: 0;
25911
26216
  display: flex; flex-direction: column; justify-content: center;
@@ -25916,11 +26221,13 @@ function buildPreset(input) {
25916
26221
  color: #00c9ff; font-weight: 600;
25917
26222
  }
25918
26223
  ${scope} .title {
25919
- font-size: 110px; font-weight: 800; letter-spacing: -2px;
26224
+ font-size: ${titleFontPx}px; font-weight: 800; letter-spacing: -2px;
25920
26225
  line-height: 1.05; margin: 0;
26226
+ overflow-wrap: break-word;
25921
26227
  }
25922
26228
  ${scope} .subtitle {
25923
26229
  font-size: 38px; font-weight: 300; color: #c0c0d0; max-width: 80%;
26230
+ overflow-wrap: break-word;
25924
26231
  }${wordCss}`,
25925
26232
  body: `${backdropMarkup}
25926
26233
  <div class="stage">
@@ -25928,7 +26235,9 @@ function buildPreset(input) {
25928
26235
  <h1 class="title" id="title">${esc(headline)}</h1>${sub ? `
25929
26236
  <div class="subtitle" id="subtitle">${subtitleInner}</div>` : ""}
25930
26237
  </div>`,
25931
- timeline: `tl.from('${scope} #kicker', { opacity: 0, y: 16, duration: 0.4, ease: 'power2.out' }, 0.1);
26238
+ timeline: `${crossfadeTweens(scope, dur)}
26239
+ ${kenBurnsTween(scope, dur)}
26240
+ tl.from('${scope} #kicker', { opacity: 0, y: 16, duration: 0.4, ease: 'power2.out' }, 0.1);
25932
26241
  tl.from('${scope} #title', { opacity: 0, y: 60, duration: 0.7, ease: 'power3.out' }, 0.25);
25933
26242
  ${subtitleTween}`
25934
26243
  };
@@ -25946,6 +26255,7 @@ function buildPreset(input) {
25946
26255
  const start = (0.05 + i * stagger).toFixed(2);
25947
26256
  return `tl.from('${scope} #w-${i}', { opacity: 0, y: 80, scale: 0.8, duration: 0.45, ease: 'back.out(1.8)' }, ${start});`;
25948
26257
  }).join("\n ");
26258
+ const kineticFontPx = fitKineticFontSize(words);
25949
26259
  return {
25950
26260
  css: `${scope} {
25951
26261
  position: absolute; inset: 0; width: 100%; height: 100%;
@@ -25953,21 +26263,27 @@ function buildPreset(input) {
25953
26263
  color: #fff; overflow: hidden; background: #000;
25954
26264
  }
25955
26265
  ${backdrop}
26266
+ ${scope} .backdrop { transform-origin: center; }
25956
26267
  ${scope} .kinetic {
25957
26268
  position: absolute; inset: 0;
25958
26269
  display: flex; align-items: center; justify-content: center;
26270
+ flex-wrap: wrap; gap: 8px 16px;
25959
26271
  text-align: center; padding: 0 6%;
25960
- font-size: 180px; font-weight: 900; letter-spacing: -6px;
26272
+ font-size: ${kineticFontPx}px; font-weight: 900; letter-spacing: -6px;
25961
26273
  line-height: 1; text-shadow: 0 6px 30px rgba(0,0,0,0.6);
26274
+ overflow-wrap: break-word;
25962
26275
  }
25963
26276
  ${scope} .kinetic .word { display: inline-block; margin: 0 12px; }`,
25964
26277
  body: `${backdropMarkup}
25965
26278
  <div class="kinetic">${wordSpans}</div>`,
25966
- timeline: tweens
26279
+ timeline: `${crossfadeTweens(scope, dur)}
26280
+ ${kenBurnsTween(scope, dur)}
26281
+ ${tweens}`
25967
26282
  };
25968
26283
  }
25969
26284
  case "product-shot": {
25970
26285
  const label = kicker || humanise(id);
26286
+ const headlineFontPx = fitFontSize(headline, 72, 40, 50);
25971
26287
  return {
25972
26288
  css: `${scope} {
25973
26289
  position: absolute; inset: 0; width: 100%; height: 100%;
@@ -25985,19 +26301,22 @@ function buildPreset(input) {
25985
26301
  }
25986
26302
  ${scope} .product-headline {
25987
26303
  position: absolute; left: 8%; right: 8%; bottom: 14%;
25988
- font-size: 72px; font-weight: 800; letter-spacing: -1px;
25989
- line-height: 1.1; text-shadow: 0 4px 20px rgba(0,0,0,0.7);
26304
+ font-size: ${headlineFontPx}px; font-weight: 800; letter-spacing: -1px;
26305
+ line-height: 1.1; overflow-wrap: break-word;
26306
+ text-shadow: 0 4px 20px rgba(0,0,0,0.7);
25990
26307
  }${subhead ? `
25991
26308
  ${scope} .product-sub {
25992
26309
  position: absolute; left: 8%; right: 8%; bottom: 8%;
25993
26310
  font-size: 28px; font-weight: 400; color: #d0d0e0;
26311
+ overflow-wrap: break-word;
25994
26312
  text-shadow: 0 2px 10px rgba(0,0,0,0.7);
25995
26313
  }` : ""}`,
25996
26314
  body: `${backdropMarkup}
25997
26315
  <div class="label" id="label">${esc(label)}</div>
25998
26316
  <div class="product-headline" id="headline">${esc(headline)}</div>${subhead ? `
25999
26317
  <div class="product-sub" id="subhead">${esc(subhead)}</div>` : ""}`,
26000
- timeline: `tl.fromTo('${scope} .backdrop', { scale: 1.0 }, { scale: 1.08, duration: ${dur.toFixed(2)}, ease: 'none' }, 0);
26318
+ timeline: `${crossfadeTweens(scope, dur)}
26319
+ tl.fromTo('${scope} .backdrop', { scale: 1.0 }, { scale: 1.12, duration: ${dur.toFixed(2)}, ease: 'none' }, 0);
26001
26320
  tl.from('${scope} #label', { opacity: 0, x: -30, duration: 0.5, ease: 'power3.out' }, 0.2);
26002
26321
  tl.from('${scope} #headline', { opacity: 0, y: 40, duration: 0.6, ease: 'power3.out' }, 0.4);${subhead ? `
26003
26322
  tl.from('${scope} #subhead', { opacity: 0, y: 20, duration: 0.5, ease: 'power3.out' }, 0.65);` : ""}`
@@ -26046,7 +26365,7 @@ ${audioBlock}
26046
26365
  </template>
26047
26366
  `;
26048
26367
  }
26049
- function nextSceneStart(rootHtml) {
26368
+ function nextSceneStart(rootHtml, overlap = 0) {
26050
26369
  const clipRegex = /<div\s+class="clip"[^>]*?\sdata-start="([\d.]+)"[^>]*?\sdata-duration="([\d.]+)"/gi;
26051
26370
  let maxEnd = 0;
26052
26371
  let match;
@@ -26054,14 +26373,15 @@ function nextSceneStart(rootHtml) {
26054
26373
  const end = parseFloat(match[1]) + parseFloat(match[2]);
26055
26374
  if (Number.isFinite(end) && end > maxEnd) maxEnd = end;
26056
26375
  }
26057
- return maxEnd;
26376
+ return Math.max(0, maxEnd - overlap);
26058
26377
  }
26059
26378
  function buildClipReference(opts) {
26060
26379
  const start = Number(opts.start.toFixed(3));
26061
26380
  const duration = Number(opts.duration.toFixed(3));
26062
26381
  const track = opts.trackIndex ?? 1;
26063
26382
  const src = opts.src ?? `compositions/scene-${opts.id}.html`;
26064
- return `<div class="clip" data-composition-id="${esc(opts.id)}" data-composition-src="${esc(src)}" data-start="${start}" data-duration="${duration}" data-track-index="${track}"></div>`;
26383
+ const zIndex = Math.max(1, 1e6 - Math.floor(start * 1e3));
26384
+ return `<div class="clip" data-composition-id="${esc(opts.id)}" data-composition-src="${esc(src)}" data-start="${start}" data-duration="${duration}" data-track-index="${track}" style="z-index: ${zIndex};"></div>`;
26065
26385
  }
26066
26386
  function insertClipIntoRoot(rootHtml, clip) {
26067
26387
  const clipDiv = buildClipReference(clip);
@@ -26094,7 +26414,7 @@ function readRootDims(rootHtml) {
26094
26414
  if (!widthMatch || !heightMatch) return null;
26095
26415
  return { width: parseInt(widthMatch[1], 10), height: parseInt(heightMatch[1], 10) };
26096
26416
  }
26097
- var SCENE_PRESETS, GSAP_CDN;
26417
+ var SCENE_PRESETS, GSAP_CDN, SCENE_OVERLAP_SECONDS;
26098
26418
  var init_scene_html_emit = __esm({
26099
26419
  "src/commands/_shared/scene-html-emit.ts"() {
26100
26420
  "use strict";
@@ -26106,6 +26426,7 @@ var init_scene_html_emit = __esm({
26106
26426
  "product-shot"
26107
26427
  ];
26108
26428
  GSAP_CDN = "https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js";
26429
+ SCENE_OVERLAP_SECONDS = 0.4;
26109
26430
  }
26110
26431
  });
26111
26432
 
@@ -26789,6 +27110,18 @@ function validatePreset(value) {
26789
27110
  }
26790
27111
  return value;
26791
27112
  }
27113
+ function validateVisualStyle(value) {
27114
+ const found = getVisualStyle(value);
27115
+ if (!found) {
27116
+ exitWithError(
27117
+ usageError(
27118
+ `Unknown visual style: ${value}`,
27119
+ `Valid: ${visualStyleNames()}. Browse details with \`vibe scene styles\`.`
27120
+ )
27121
+ );
27122
+ }
27123
+ return found;
27124
+ }
26792
27125
  async function pathExists2(p) {
26793
27126
  try {
26794
27127
  await access4(p);
@@ -27002,7 +27335,20 @@ async function executeSceneAdd(opts) {
27002
27335
  }
27003
27336
  const cfg = await loadVibeProjectConfig(projectDir);
27004
27337
  const fallback2 = cfg?.defaultSceneDuration ?? 5;
27005
- const duration = opts.duration ?? narrationDuration ?? fallback2;
27338
+ const NARRATION_TAIL_BUFFER = 0.5;
27339
+ const userDur = opts.duration;
27340
+ const audioMinDur = narrationDuration !== void 0 ? narrationDuration + SCENE_OVERLAP_SECONDS + NARRATION_TAIL_BUFFER : void 0;
27341
+ let duration;
27342
+ if (userDur !== void 0 && audioMinDur !== void 0) {
27343
+ duration = Math.max(userDur, audioMinDur);
27344
+ } else if (audioMinDur !== void 0) {
27345
+ duration = audioMinDur;
27346
+ } else if (userDur !== void 0) {
27347
+ duration = userDur;
27348
+ } else {
27349
+ duration = fallback2;
27350
+ }
27351
+ duration = Number(duration.toFixed(2));
27006
27352
  opts.onProgress?.("Emitting scene HTML...");
27007
27353
  const sceneHtml = emitSceneHtml({
27008
27354
  id,
@@ -27020,8 +27366,10 @@ async function executeSceneAdd(opts) {
27020
27366
  await mkdir4(dirname3(scenePath), { recursive: true });
27021
27367
  await writeFile6(scenePath, sceneHtml, "utf-8");
27022
27368
  opts.onProgress?.("Updating root composition...");
27023
- const start = nextSceneStart(rootHtmlBefore);
27024
- const updated = insertClipIntoRoot(rootHtmlBefore, { id, start, duration });
27369
+ const start = nextSceneStart(rootHtmlBefore, SCENE_OVERLAP_SECONDS);
27370
+ const existingClipCount = (rootHtmlBefore.match(/<div\s+class="clip"/g) || []).length;
27371
+ const trackIndex = existingClipCount % 2 + 1;
27372
+ const updated = insertClipIntoRoot(rootHtmlBefore, { id, start, duration, trackIndex });
27025
27373
  await writeFile6(rootPath, updated, "utf-8");
27026
27374
  const transcriptAbsPath = transcriptRelPath ? resolve10(projectDir, transcriptRelPath) : void 0;
27027
27375
  return {
@@ -27080,6 +27428,7 @@ var init_scene = __esm({
27080
27428
  init_dist2();
27081
27429
  init_tts_resolve();
27082
27430
  init_scene_project();
27431
+ init_visual_styles();
27083
27432
  init_scene_html_emit();
27084
27433
  init_scene_lint();
27085
27434
  init_scene_render();
@@ -27104,21 +27453,28 @@ Examples:
27104
27453
 
27105
27454
  A scene project is bilingual: it works with both \`vibe\` and \`npx hyperframes\`.
27106
27455
  Run 'vibe schema scene.<command>' for structured parameter info.`);
27107
- sceneCommand.command("init").description("Scaffold a new scene project (or safely augment an existing Hyperframes project)").argument("<dir>", "Project directory (created if it doesn't exist)").option("-n, --name <name>", "Project name (defaults to directory basename)").option("-r, --ratio <ratio>", "Aspect ratio: 16:9, 9:16, 1:1, 4:5", "16:9").option("-d, --duration <sec>", "Default root composition duration (seconds)", "10").option("--dry-run", "Preview parameters without writing files").action(async (dir, options) => {
27456
+ sceneCommand.command("init").description("Scaffold a new scene project (or safely augment an existing Hyperframes project)").argument("<dir>", "Project directory (created if it doesn't exist)").option("-n, --name <name>", "Project name (defaults to directory basename)").option("-r, --ratio <ratio>", "Aspect ratio: 16:9, 9:16, 1:1, 4:5", "16:9").option("-d, --duration <sec>", "Default root composition duration (seconds)", "10").option("--visual-style <name>", `Seed DESIGN.md from a named style (browse via \`vibe scene styles\`). E.g. "Swiss Pulse"`).option("--dry-run", "Preview parameters without writing files").action(async (dir, options) => {
27108
27457
  const aspect = validateAspect(options.ratio);
27109
27458
  const duration = validateDuration(options.duration);
27110
27459
  const name = options.name ?? basename4(dir.replace(/\/+$/, ""));
27460
+ const visualStyle = options.visualStyle ? validateVisualStyle(options.visualStyle) : void 0;
27111
27461
  if (options.dryRun) {
27112
27462
  outputResult({
27113
27463
  dryRun: true,
27114
27464
  command: "scene init",
27115
- params: { dir, name, aspect, duration }
27465
+ params: {
27466
+ dir,
27467
+ name,
27468
+ aspect,
27469
+ duration,
27470
+ visualStyle: visualStyle?.name ?? null
27471
+ }
27116
27472
  });
27117
27473
  return;
27118
27474
  }
27119
27475
  const spinner2 = isJsonMode() ? null : ora(`Scaffolding scene project at ${dir}...`).start();
27120
27476
  try {
27121
- const result = await scaffoldSceneProject({ dir, name, aspect, duration });
27477
+ const result = await scaffoldSceneProject({ dir, name, aspect, duration, visualStyle });
27122
27478
  if (isJsonMode()) {
27123
27479
  outputResult({
27124
27480
  success: true,
@@ -27127,6 +27483,7 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
27127
27483
  name,
27128
27484
  aspect,
27129
27485
  duration,
27486
+ visualStyle: visualStyle?.name ?? null,
27130
27487
  created: result.created,
27131
27488
  merged: result.merged,
27132
27489
  skipped: result.skipped
@@ -27143,7 +27500,13 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
27143
27500
  console.log();
27144
27501
  console.log(source_default.bold.cyan("Next steps"));
27145
27502
  console.log(source_default.dim("\u2500".repeat(60)));
27146
- console.log(` ${source_default.cyan("vibe scene add")} <name> ${source_default.dim("# author a scene via AI")}`);
27503
+ if (visualStyle) {
27504
+ console.log(` ${source_default.dim("DESIGN.md seeded with")} ${source_default.bold(visualStyle.name)} ${source_default.dim("\u2014 review and customise.")}`);
27505
+ } else {
27506
+ console.log(` ${source_default.cyan("vibe scene styles")} ${source_default.dim("# pick a named style for DESIGN.md")}`);
27507
+ }
27508
+ console.log(` ${source_default.cyan("npx skills add heygen-com/hyperframes")} ${source_default.dim("# load the cinematic-craft skill set")}`);
27509
+ console.log(` ${source_default.cyan("vibe scene add")} <name> ${source_default.dim("# fallback path: 5-preset emit")}`);
27147
27510
  console.log(` ${source_default.cyan("vibe scene lint")} ${source_default.dim("# validate HTML")}`);
27148
27511
  console.log(` ${source_default.cyan("vibe scene render")} ${source_default.dim("# render to MP4")}`);
27149
27512
  } catch (error) {
@@ -27152,6 +27515,69 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
27152
27515
  exitWithError(generalError(`Failed to scaffold: ${msg}`));
27153
27516
  }
27154
27517
  });
27518
+ sceneCommand.command("styles").description("List vendored visual styles (or show one) for DESIGN.md seeding").argument("[name]", "Style name to inspect (omit to list all)").action((name) => {
27519
+ if (!name) {
27520
+ const all = listVisualStyles();
27521
+ if (isJsonMode()) {
27522
+ outputResult({
27523
+ success: true,
27524
+ command: "scene styles",
27525
+ count: all.length,
27526
+ styles: all.map((s) => ({
27527
+ name: s.name,
27528
+ slug: s.slug,
27529
+ designer: s.designer,
27530
+ mood: s.mood,
27531
+ bestFor: s.bestFor
27532
+ }))
27533
+ });
27534
+ return;
27535
+ }
27536
+ console.log();
27537
+ console.log(source_default.bold.cyan("Visual styles"));
27538
+ console.log(source_default.dim("\u2500".repeat(60)));
27539
+ for (const s of all) {
27540
+ console.log(
27541
+ ` ${source_default.bold(s.name.padEnd(18))} ${source_default.dim(s.mood.padEnd(24))} ${source_default.dim(s.bestFor)}`
27542
+ );
27543
+ }
27544
+ console.log();
27545
+ console.log(source_default.dim("Show details: "), source_default.cyan('vibe scene styles "<name>"'));
27546
+ console.log(source_default.dim("Seed DESIGN.md:"), source_default.cyan('vibe scene init <dir> --visual-style "<name>"'));
27547
+ return;
27548
+ }
27549
+ const style = getVisualStyle(name);
27550
+ if (!style) {
27551
+ exitWithError(
27552
+ usageError(
27553
+ `Unknown visual style: ${name}`,
27554
+ `Valid: ${visualStyleNames()}.`
27555
+ )
27556
+ );
27557
+ return;
27558
+ }
27559
+ if (isJsonMode()) {
27560
+ outputResult({ success: true, command: "scene styles", style });
27561
+ return;
27562
+ }
27563
+ console.log();
27564
+ console.log(source_default.bold.cyan(style.name), source_default.dim(`\u2014 ${style.designer}`));
27565
+ console.log(source_default.dim("\u2500".repeat(60)));
27566
+ console.log(`${source_default.bold("Mood:")} ${style.mood}`);
27567
+ console.log(`${source_default.bold("Best for:")} ${style.bestFor}`);
27568
+ console.log(`${source_default.bold("Palette:")} ${style.palette.join(", ")}`);
27569
+ console.log(source_default.dim(" ") + style.paletteNotes);
27570
+ console.log(`${source_default.bold("Typography:")} ${style.typography}`);
27571
+ console.log(`${source_default.bold("Composition:")} ${style.composition}`);
27572
+ console.log(`${source_default.bold("Motion:")} ${style.motion}`);
27573
+ console.log(`${source_default.bold("GSAP:")} ${style.gsapSignature}`);
27574
+ console.log(`${source_default.bold("Transition:")} ${style.transition}`);
27575
+ console.log();
27576
+ console.log(source_default.bold("Avoid:"));
27577
+ for (const a of style.avoid) console.log(` ${source_default.red("\u2022")} ${a}`);
27578
+ console.log();
27579
+ console.log(source_default.dim("Seed DESIGN.md:"), source_default.cyan(`vibe scene init <dir> --visual-style "${style.name}"`));
27580
+ });
27155
27581
  sceneCommand.command("add").description("Add a new scene to a project: AI narration + image + per-scene HTML").argument("<name>", "Scene name (slugified into the composition id)").option("--style <preset>", `Style preset: ${SCENE_PRESETS.join(", ")}`, "simple").option("--narration <text>", "Narration text (or path to a .txt file). Drives TTS + scene duration.").option("--narration-file <path>", "Existing narration audio file (.wav/.mp3). Skips TTS \u2014 useful with hyperframes tts, Mac say, or other external tools.").option("-d, --duration <sec>", "Explicit scene duration in seconds (overrides narration audio)").option("--visuals <prompt>", "Image prompt \u2014 generates assets/scene-<id>.png via the configured image provider").option("--headline <text>", "Visible headline (defaults to the humanised scene name)").option("--kicker <text>", "Small label above the headline (explainer / product-shot)").option("--insert-into <path>", "Root composition file to update", "index.html").option("--project <dir>", "Project directory", ".").option("--image-provider <name>", "Image provider: gemini, openai", "gemini").option("--tts <provider>", "TTS provider: auto, elevenlabs, kokoro (default auto \u2014 picks ElevenLabs when key set, else Kokoro local)", "auto").option("--voice <id>", "Voice id (ElevenLabs name/id, or Kokoro id like af_heart, am_michael)").option("--no-audio", "Skip TTS even when --narration is provided (useful for tests/agent dry runs)").option("--no-image", "Skip image generation even when --visuals is provided").option("--no-transcribe", "Skip Whisper word-level transcribe step (no transcript-<id>.json emitted)").option("--transcribe-language <code>", "BCP-47 language code passed to Whisper (e.g. en, ko)").option("--force", "Overwrite an existing compositions/scene-<id>.html").option("--dry-run", "Preview parameters without writing files or calling APIs").action(async (name, options) => {
27156
27582
  if (options.style) options.style = validatePreset(options.style);
27157
27583
  if (options.duration !== void 0) options.duration = validateDuration(options.duration);
@@ -28365,16 +28791,16 @@ async function prompt2(question) {
28365
28791
  input,
28366
28792
  output: process.stdout
28367
28793
  });
28368
- return new Promise((resolve53) => {
28794
+ return new Promise((resolve54) => {
28369
28795
  rl.question(question, (answer) => {
28370
28796
  rl.close();
28371
- resolve53(answer);
28797
+ resolve54(answer);
28372
28798
  });
28373
28799
  });
28374
28800
  }
28375
28801
  async function promptHidden(question) {
28376
28802
  const input = getTTYInputStream();
28377
- return new Promise((resolve53) => {
28803
+ return new Promise((resolve54) => {
28378
28804
  process.stdout.write(question);
28379
28805
  let value = "";
28380
28806
  if (typeof input.setRawMode === "function") {
@@ -28386,7 +28812,7 @@ async function promptHidden(question) {
28386
28812
  input.setRawMode(false);
28387
28813
  input.removeListener("data", onData);
28388
28814
  process.stdout.write("\n");
28389
- resolve53(value);
28815
+ resolve54(value);
28390
28816
  } else if (char === "") {
28391
28817
  input.setRawMode(false);
28392
28818
  process.stdout.write("\n");
@@ -28406,7 +28832,7 @@ async function promptHidden(question) {
28406
28832
  const rl = createInterface2({ input, output: process.stdout });
28407
28833
  rl.question("", (answer) => {
28408
28834
  rl.close();
28409
- resolve53(answer);
28835
+ resolve54(answer);
28410
28836
  });
28411
28837
  }
28412
28838
  });
@@ -28414,7 +28840,7 @@ async function promptHidden(question) {
28414
28840
  async function promptSelect(_question, options, defaultIndex = 0) {
28415
28841
  const input = getTTYInputStream();
28416
28842
  if (typeof input.setRawMode === "function") {
28417
- return new Promise((resolve53) => {
28843
+ return new Promise((resolve54) => {
28418
28844
  let selected = defaultIndex;
28419
28845
  const render = () => {
28420
28846
  if (renderCount > 0) {
@@ -28438,7 +28864,7 @@ async function promptSelect(_question, options, defaultIndex = 0) {
28438
28864
  input.setRawMode(false);
28439
28865
  input.removeListener("data", onData);
28440
28866
  process.stdout.write("\n");
28441
- resolve53(selected);
28867
+ resolve54(selected);
28442
28868
  } else if (char === "") {
28443
28869
  input.setRawMode(false);
28444
28870
  process.stdout.write("\n");
@@ -28455,7 +28881,7 @@ async function promptSelect(_question, options, defaultIndex = 0) {
28455
28881
  input.removeListener("data", onData);
28456
28882
  render();
28457
28883
  process.stdout.write("\n");
28458
- resolve53(selected);
28884
+ resolve54(selected);
28459
28885
  }
28460
28886
  };
28461
28887
  input.on("data", onData);
@@ -28625,13 +29051,10 @@ var init_subtitle = __esm({
28625
29051
  // src/utils/remotion.ts
28626
29052
  var remotion_exports = {};
28627
29053
  __export(remotion_exports, {
28628
- compositeOverlay: () => compositeOverlay,
28629
- compositeWithImage: () => compositeWithImage,
28630
29054
  ensureRemotionInstalled: () => ensureRemotionInstalled,
28631
29055
  generateAnimatedCaptionComponent: () => generateAnimatedCaptionComponent,
28632
29056
  generateCaptionComponent: () => generateCaptionComponent,
28633
29057
  generateTextOverlayComponent: () => generateTextOverlayComponent,
28634
- renderAndComposite: () => renderAndComposite,
28635
29058
  renderMotion: () => renderMotion,
28636
29059
  renderWithEmbeddedImage: () => renderWithEmbeddedImage,
28637
29060
  renderWithEmbeddedVideo: () => renderWithEmbeddedVideo,
@@ -29379,92 +29802,6 @@ export const ${name} = () => {
29379
29802
  `;
29380
29803
  return { code, name };
29381
29804
  }
29382
- async function compositeOverlay(options) {
29383
- try {
29384
- await execSafe("ffmpeg", [
29385
- "-y",
29386
- "-i",
29387
- options.baseVideo,
29388
- "-i",
29389
- options.overlayPath,
29390
- "-filter_complex",
29391
- "[0:v][1:v]overlay=0:0:shortest=1[out]",
29392
- "-map",
29393
- "[out]",
29394
- "-map",
29395
- "0:a?",
29396
- "-c:a",
29397
- "copy",
29398
- "-c:v",
29399
- "libx264",
29400
- "-crf",
29401
- "18",
29402
- "-pix_fmt",
29403
- "yuv420p",
29404
- options.outputPath
29405
- ], { timeout: 3e5 });
29406
- return { success: true, outputPath: options.outputPath };
29407
- } catch (error) {
29408
- const msg = error instanceof Error ? error.message : String(error);
29409
- return { success: false, error: `FFmpeg composite failed: ${msg}` };
29410
- }
29411
- }
29412
- async function compositeWithImage(options) {
29413
- try {
29414
- const fps = options.fps || 30;
29415
- await execSafe("ffmpeg", [
29416
- "-y",
29417
- "-loop",
29418
- "1",
29419
- "-framerate",
29420
- String(fps),
29421
- "-i",
29422
- options.baseImage,
29423
- "-i",
29424
- options.overlayPath,
29425
- "-filter_complex",
29426
- "[0:v]scale=iw:ih[base];[base][1:v]overlay=0:0:shortest=1[out]",
29427
- "-map",
29428
- "[out]",
29429
- "-c:v",
29430
- "libx264",
29431
- "-crf",
29432
- "18",
29433
- "-pix_fmt",
29434
- "yuv420p",
29435
- "-t",
29436
- String(options.durationSeconds),
29437
- options.outputPath
29438
- ], { timeout: 3e5 });
29439
- return { success: true, outputPath: options.outputPath };
29440
- } catch (error) {
29441
- const msg = error instanceof Error ? error.message : String(error);
29442
- return { success: false, error: `FFmpeg image composite failed: ${msg}` };
29443
- }
29444
- }
29445
- async function renderAndComposite(motionOpts, baseVideo, finalOutput) {
29446
- const renderOpts = {
29447
- ...motionOpts,
29448
- transparent: !!baseVideo,
29449
- outputPath: baseVideo ? motionOpts.outputPath.replace(/\.\w+$/, "_overlay.webm") : motionOpts.outputPath
29450
- };
29451
- const renderResult = await renderMotion(renderOpts);
29452
- if (!renderResult.success || !renderResult.outputPath) {
29453
- return renderResult;
29454
- }
29455
- if (!baseVideo) {
29456
- return renderResult;
29457
- }
29458
- const output = finalOutput || motionOpts.outputPath;
29459
- const compositeResult = await compositeOverlay({
29460
- baseVideo,
29461
- overlayPath: renderResult.outputPath,
29462
- outputPath: output
29463
- });
29464
- await rm(renderResult.outputPath, { force: true }).catch(() => {
29465
- });
29466
- return compositeResult;
29467
- }
29468
29805
  var REMOTION_VERSION, REMOTION_CACHE_DIR;
29469
29806
  var init_remotion = __esm({
29470
29807
  "src/utils/remotion.ts"() {
@@ -30822,7 +31159,12 @@ async function executeSegmentsToScenes(opts) {
30822
31159
  const dims = aspectToDims(aspect);
30823
31160
  const preset = opts.scenePreset ?? DEFAULT_PRESET;
30824
31161
  const projectName = opts.projectName ?? basename7(outputDir);
30825
- const totalDuration = opts.segments.reduce((sum, s) => sum + (s.duration || 0), 0) || 10;
31162
+ const totalDuration = (() => {
31163
+ const sum = opts.segments.reduce((acc, s) => acc + (s.duration || 0), 0);
31164
+ if (sum <= 0) return 10;
31165
+ const joins = Math.max(0, opts.segments.length - 1);
31166
+ return Math.max(0.1, sum - joins * SCENE_OVERLAP_SECONDS);
31167
+ })();
30826
31168
  const baseError = (error) => ({
30827
31169
  success: false,
30828
31170
  outputDir,
@@ -30895,9 +31237,10 @@ async function executeSegmentsToScenes(opts) {
30895
31237
  id,
30896
31238
  start: clipStart,
30897
31239
  duration,
31240
+ trackIndex: i % 2 + 1,
30898
31241
  src: `compositions/${id}.html`
30899
31242
  });
30900
- clipStart += duration;
31243
+ clipStart += duration - SCENE_OVERLAP_SECONDS;
30901
31244
  }
30902
31245
  await writeFile10(rootAbs, rootHtml, "utf-8");
30903
31246
  let lintResult;
@@ -30957,7 +31300,7 @@ import { readFile as readFile13, writeFile as writeFile11, mkdir as mkdir8, unli
30957
31300
  import { resolve as resolve15, basename as basename8, dirname as dirname6, extname as extname4 } from "node:path";
30958
31301
  import { existsSync as existsSync9 } from "node:fs";
30959
31302
  function sleep(ms) {
30960
- return new Promise((resolve53) => setTimeout(resolve53, ms));
31303
+ return new Promise((resolve54) => setTimeout(resolve54, ms));
30961
31304
  }
30962
31305
  async function uploadToImgbb(imageBuffer, apiKey) {
30963
31306
  try {
@@ -31322,7 +31665,6 @@ async function executeScriptToVideo(options) {
31322
31665
  outputDir: absOutputDir,
31323
31666
  scenes: segments.length,
31324
31667
  storyboardPath,
31325
- narrations: [],
31326
31668
  narrationEntries: [],
31327
31669
  images: [],
31328
31670
  videos: [],
@@ -31383,7 +31725,6 @@ async function executeScriptToVideo(options) {
31383
31725
  );
31384
31726
  }
31385
31727
  segment.duration = actualDuration;
31386
- result.narrations.push(audioPath);
31387
31728
  result.narrationEntries.push({
31388
31729
  path: audioPath,
31389
31730
  duration: actualDuration,
@@ -32366,6 +32707,34 @@ var init_provider_resolver = __esm({
32366
32707
  }
32367
32708
  });
32368
32709
 
32710
+ // src/commands/_shared/openai-image.ts
32711
+ function resolveOpenAIImageModel(modelAlias) {
32712
+ const isGptImage2 = modelAlias === "2" || modelAlias === "gpt-image-2";
32713
+ return {
32714
+ openaiModel: isGptImage2 ? "gpt-image-2" : void 0,
32715
+ modelLabel: isGptImage2 ? "GPT Image 2" : "GPT Image 1.5"
32716
+ };
32717
+ }
32718
+ async function executeOpenAIImageGenerate(prompt3, options, ctx) {
32719
+ const provider = new OpenAIImageProvider();
32720
+ await provider.initialize({ apiKey: ctx.apiKey });
32721
+ const { openaiModel, modelLabel } = resolveOpenAIImageModel(options.model);
32722
+ const result = await provider.generateImage(prompt3, {
32723
+ model: openaiModel,
32724
+ size: options.size,
32725
+ quality: options.quality,
32726
+ style: options.style,
32727
+ n: options.count !== void 0 ? parseInt(options.count, 10) : void 0
32728
+ });
32729
+ return { result, openaiModel, modelLabel };
32730
+ }
32731
+ var init_openai_image2 = __esm({
32732
+ "src/commands/_shared/openai-image.ts"() {
32733
+ "use strict";
32734
+ init_dist2();
32735
+ }
32736
+ });
32737
+
32369
32738
  // src/commands/ai-image.ts
32370
32739
  var ai_image_exports = {};
32371
32740
  __export(ai_image_exports, {
@@ -32418,22 +32787,11 @@ function _registerImageCommands(aiCommand) {
32418
32787
  }
32419
32788
  const spinner2 = ora(`Generating image with ${providerName}...`).start();
32420
32789
  if (provider === "dalle" || provider === "openai") {
32421
- const openaiImage = new OpenAIImageProvider();
32422
- await openaiImage.initialize({ apiKey });
32423
- const modelAlias = options.model;
32424
- const openaiModel = modelAlias === "2" || modelAlias === "gpt-image-2" ? "gpt-image-2" : void 0;
32425
- const result = await openaiImage.generateImage(prompt3, {
32426
- model: openaiModel,
32427
- size: options.size,
32428
- quality: options.quality,
32429
- style: options.style,
32430
- n: parseInt(options.count)
32431
- });
32790
+ const { result, modelLabel } = await executeOpenAIImageGenerate(prompt3, options, { apiKey });
32432
32791
  if (!result.success || !result.images) {
32433
32792
  spinner2.fail("Image generation failed");
32434
32793
  exitWithError(apiError(result.error || "Image generation failed", true));
32435
32794
  }
32436
- const modelLabel = openaiModel === "gpt-image-2" ? "GPT Image 2" : "GPT Image 1.5";
32437
32795
  spinner2.succeed(source_default.green(`Generated ${result.images.length} image(s) with OpenAI ${modelLabel}`));
32438
32796
  console.log();
32439
32797
  console.log(source_default.bold.cyan("Generated Images"));
@@ -33120,6 +33478,7 @@ var init_ai_image = __esm({
33120
33478
  init_exec_safe();
33121
33479
  init_output();
33122
33480
  init_validate();
33481
+ init_openai_image2();
33123
33482
  }
33124
33483
  });
33125
33484
 
@@ -33560,6 +33919,7 @@ var init_generate = __esm({
33560
33919
  init_provider_resolver();
33561
33920
  init_ai_image();
33562
33921
  init_ai_motion();
33922
+ init_openai_image2();
33563
33923
  generateCommand = new Command("generate").alias("gen").description(
33564
33924
  "Generate assets using AI (images, videos, speech, music, motion)"
33565
33925
  ).addHelpText(
@@ -33659,22 +34019,11 @@ Examples:
33659
34019
  const apiKey = await requireApiKey(envKey, providerName, options.apiKey);
33660
34020
  const spinner2 = ora(`Generating image with ${providerName}...`).start();
33661
34021
  if (provider === "dalle" || provider === "openai") {
33662
- const openaiImage = new OpenAIImageProvider();
33663
- await openaiImage.initialize({ apiKey });
33664
- const modelAlias = options.model;
33665
- const openaiModel = modelAlias === "2" || modelAlias === "gpt-image-2" ? "gpt-image-2" : void 0;
33666
- const result = await openaiImage.generateImage(prompt3, {
33667
- model: openaiModel,
33668
- size: options.size,
33669
- quality: options.quality,
33670
- style: options.style,
33671
- n: parseInt(options.count)
33672
- });
34022
+ const { result, modelLabel } = await executeOpenAIImageGenerate(prompt3, options, { apiKey });
33673
34023
  if (!result.success || !result.images) {
33674
34024
  spinner2.fail(result.error || "Image generation failed");
33675
34025
  exitWithError(apiError(result.error || "Image generation failed", true));
33676
34026
  }
33677
- const modelLabel = openaiModel === "gpt-image-2" ? "GPT Image 2" : "GPT Image 1.5";
33678
34027
  spinner2.succeed(source_default.green(`Generated ${result.images.length} image(s) with OpenAI ${modelLabel}`));
33679
34028
  if (isJsonMode()) {
33680
34029
  const outputPath = options.output ? resolve18(process.cwd(), options.output) : void 0;
@@ -48740,7 +49089,7 @@ function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, so
48740
49089
  return args;
48741
49090
  }
48742
49091
  function runFFmpegProcess(ffmpegPath, args, onProgress) {
48743
- return new Promise((resolve53, reject) => {
49092
+ return new Promise((resolve54, reject) => {
48744
49093
  const ffmpeg = spawn(ffmpegPath, args, {
48745
49094
  stdio: ["pipe", "pipe", "pipe"]
48746
49095
  });
@@ -48764,7 +49113,7 @@ function runFFmpegProcess(ffmpegPath, args, onProgress) {
48764
49113
  });
48765
49114
  ffmpeg.on("close", (code) => {
48766
49115
  if (code === 0) {
48767
- resolve53();
49116
+ resolve54();
48768
49117
  } else {
48769
49118
  const errorMatch = stderr.match(/Error.*$/m);
48770
49119
  const errorMsg = errorMatch ? errorMatch[0] : `FFmpeg exited with code ${code}`;
@@ -48900,17 +49249,17 @@ function getPresetSettings(preset, aspectRatio) {
48900
49249
  }
48901
49250
  async function runHyperframesExport(projectPath, options, spinner2) {
48902
49251
  spinner2.text = "Loading project...";
48903
- const { readFile: readFile43 } = await import("node:fs/promises");
48904
- const { resolve: resolve53, basename: basename28 } = await import("node:path");
49252
+ const { readFile: readFile44 } = await import("node:fs/promises");
49253
+ const { resolve: resolve54, basename: basename28 } = await import("node:path");
48905
49254
  const { Project: Project2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
48906
49255
  const { createHyperframesBackend: createHyperframesBackend2 } = await Promise.resolve().then(() => (init_hyperframes(), hyperframes_exports));
48907
49256
  const { exitWithError: exitWithError2, generalError: generalError2, outputResult: outputResult2 } = await Promise.resolve().then(() => (init_output(), output_exports));
48908
49257
  const chalk2 = (await Promise.resolve().then(() => (init_source(), source_exports))).default;
48909
- const filePath = resolve53(process.cwd(), projectPath);
48910
- const content = await readFile43(filePath, "utf-8");
49258
+ const filePath = resolve54(process.cwd(), projectPath);
49259
+ const content = await readFile44(filePath, "utf-8");
48911
49260
  const project = Project2.fromJSON(JSON.parse(content));
48912
49261
  const state = project.getState();
48913
- const outputPath = options.output ? resolve53(process.cwd(), options.output) : resolve53(process.cwd(), `${basename28(projectPath, ".vibe.json")}.${options.format ?? "mp4"}`);
49262
+ const outputPath = options.output ? resolve54(process.cwd(), options.output) : resolve54(process.cwd(), `${basename28(projectPath, ".vibe.json")}.${options.format ?? "mp4"}`);
48914
49263
  const quality = ["draft", "standard", "high"].includes(options.preset ?? "") ? options.preset : "standard";
48915
49264
  const backend = createHyperframesBackend2();
48916
49265
  spinner2.text = "Rendering with Hyperframes...";
@@ -51439,6 +51788,1295 @@ var init_ai_audio = __esm({
51439
51788
  }
51440
51789
  });
51441
51790
 
51791
+ // src/commands/_shared/hf-skill-bundle/bundle-content.ts
51792
+ var SKILL_MD, HOUSE_STYLE_MD, MOTION_PRINCIPLES_MD, TYPOGRAPHY_MD, TRANSITIONS_MD;
51793
+ var init_bundle_content = __esm({
51794
+ "src/commands/_shared/hf-skill-bundle/bundle-content.ts"() {
51795
+ "use strict";
51796
+ SKILL_MD = `---
51797
+ name: hyperframes
51798
+ description: Create video compositions, animations, title cards, overlays, captions, voiceovers, audio-reactive visuals, and scene transitions in HyperFrames HTML. Use when asked to build any HTML-based video content, add captions or subtitles synced to audio, generate text-to-speech narration, create audio-reactive animation (beat sync, glow, pulse driven by music), add animated text highlighting (marker sweeps, hand-drawn circles, burst lines, scribble, sketchout), or add transitions between scenes (crossfades, wipes, reveals, shader transitions). Covers composition authoring, timing, media, and the full video production workflow. For CLI commands (init, lint, preview, render, transcribe, tts) see the hyperframes-cli skill.
51799
+ ---
51800
+
51801
+ # HyperFrames
51802
+
51803
+ HTML is the source of truth for video. A composition is an HTML file with \`data-*\` attributes for timing, a GSAP timeline for animation, and CSS for appearance. The framework handles clip visibility, media playback, and timeline sync.
51804
+
51805
+ ## Approach
51806
+
51807
+ Before writing HTML, think at a high level:
51808
+
51809
+ 1. **What** \u2014 what should the viewer experience? Identify the narrative arc, key moments, and emotional beats.
51810
+ 2. **Structure** \u2014 how many compositions, which are sub-compositions vs inline, what tracks carry what (video, audio, overlays, captions).
51811
+ 3. **Timing** \u2014 which clips drive the duration, where do transitions land, what's the pacing.
51812
+ 4. **Layout** \u2014 build the end-state first. See "Layout Before Animation" below.
51813
+ 5. **Animate** \u2014 then add motion using the rules below.
51814
+
51815
+ For small edits (fix a color, adjust timing, add one element), skip straight to the rules.
51816
+
51817
+ ### Visual Identity Gate
51818
+
51819
+ <HARD-GATE>
51820
+ Before writing ANY composition HTML, you MUST have a visual identity defined. Do NOT write compositions with default or generic colors.
51821
+
51822
+ Check in this order:
51823
+
51824
+ 1. **DESIGN.md exists in the project?** \u2192 Read it. Use its exact colors, fonts, motion rules, and "What NOT to Do" constraints.
51825
+ 2. **visual-style.md exists?** \u2192 Read it. Apply its \`style_prompt_full\` and structured fields. (Note: \`visual-style.md\` is a project-specific file. \`visual-styles.md\` is the style library with 8 named presets \u2014 different files.)
51826
+ 3. **User named a style** (e.g., "Swiss Pulse", "dark and techy", "luxury brand")? \u2192 Read [visual-styles.md](./visual-styles.md) for the 8 named presets. Generate a minimal DESIGN.md with: \`## Style Prompt\` (one paragraph), \`## Colors\` (3-5 hex values with roles), \`## Typography\` (1-2 font families), \`## What NOT to Do\` (3-5 anti-patterns).
51827
+ 4. **None of the above?** \u2192 Ask 3 questions before writing any HTML:
51828
+ - What's the mood? (explosive / cinematic / fluid / technical / chaotic / warm)
51829
+ - Light or dark canvas?
51830
+ - Any specific brand colors, fonts, or visual references?
51831
+ Then generate a minimal DESIGN.md from the answers.
51832
+
51833
+ Every composition must trace its palette and typography back to a DESIGN.md, visual-style.md, or explicit user direction. If you're reaching for \`#333\`, \`#3b82f6\`, or \`Roboto\` \u2014 you skipped this step.
51834
+ </HARD-GATE>
51835
+
51836
+ For motion defaults, sizing, entrance patterns, and easing \u2014 follow [house-style.md](./house-style.md). The house style handles HOW things move. The DESIGN.md handles WHAT things look like.
51837
+
51838
+ ## Layout Before Animation
51839
+
51840
+ Position every element where it should be at its **most visible moment** \u2014 the frame where it's fully entered, correctly placed, and not yet exiting. Write this as static HTML+CSS first. No GSAP yet.
51841
+
51842
+ **Why this matters:** If you position elements at their animated start state (offscreen, scaled to 0, opacity 0) and tween them to where you think they should land, you're guessing the final layout. Overlaps are invisible until the video renders. By building the end state first, you can see and fix layout problems before adding any motion.
51843
+
51844
+ ### The process
51845
+
51846
+ 1. **Identify the hero frame** for each scene \u2014 the moment when the most elements are simultaneously visible. This is the layout you build.
51847
+ 2. **Write static CSS** for that frame. The \`.scene-content\` container MUST fill the full scene using \`width: 100%; height: 100%; padding: Npx;\` with \`display: flex; flex-direction: column; gap: Npx; box-sizing: border-box\`. Use padding to push content inward \u2014 NEVER \`position: absolute; top: Npx\` on a content container. Absolute-positioned content containers overflow when content is taller than the remaining space. Reserve \`position: absolute\` for decoratives only.
51848
+ 3. **Add entrances with \`gsap.from()\`** \u2014 animate FROM offscreen/invisible TO the CSS position. The CSS position is the ground truth; the tween describes the journey to get there.
51849
+ 4. **Add exits with \`gsap.to()\`** \u2014 animate TO offscreen/invisible FROM the CSS position.
51850
+
51851
+ ### Example
51852
+
51853
+ \`\`\`css
51854
+ /* scene-content fills the scene, padding positions content */
51855
+ .scene-content {
51856
+ display: flex;
51857
+ flex-direction: column;
51858
+ justify-content: center;
51859
+ width: 100%;
51860
+ height: 100%;
51861
+ padding: 120px 160px;
51862
+ gap: 24px;
51863
+ box-sizing: border-box;
51864
+ }
51865
+ .title {
51866
+ font-size: 120px;
51867
+ }
51868
+ .subtitle {
51869
+ font-size: 42px;
51870
+ }
51871
+ /* Container fills any scene size (1920x1080, 1080x1920, etc).
51872
+ Padding positions content. Flex + gap handles spacing. */
51873
+ \`\`\`
51874
+
51875
+ **WRONG \u2014 hardcoded dimensions and absolute positioning:**
51876
+
51877
+ \`\`\`css
51878
+ .scene-content {
51879
+ position: absolute;
51880
+ top: 200px;
51881
+ left: 160px;
51882
+ width: 1920px;
51883
+ height: 1080px;
51884
+ display: flex; /* ... */
51885
+ }
51886
+ \`\`\`
51887
+
51888
+ \`\`\`js
51889
+ // Step 3: Animate INTO those positions
51890
+ tl.from(".title", { y: 60, opacity: 0, duration: 0.6, ease: "power3.out" }, 0);
51891
+ tl.from(".subtitle", { y: 40, opacity: 0, duration: 0.5, ease: "power3.out" }, 0.2);
51892
+ tl.from(".logo", { scale: 0.8, opacity: 0, duration: 0.4, ease: "power2.out" }, 0.3);
51893
+
51894
+ // Step 4: Animate OUT from those positions
51895
+ tl.to(".title", { y: -40, opacity: 0, duration: 0.4, ease: "power2.in" }, 3);
51896
+ tl.to(".subtitle", { y: -30, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.1);
51897
+ tl.to(".logo", { scale: 0.9, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.2);
51898
+ \`\`\`
51899
+
51900
+ ### When elements share space across time
51901
+
51902
+ If element A exits before element B enters in the same area, both should have correct CSS positions for their respective hero frames. The timeline ordering guarantees they never visually coexist \u2014 but if you skip the layout step, you won't catch the case where they accidentally overlap due to a timing error.
51903
+
51904
+ ### What counts as intentional overlap
51905
+
51906
+ Layered effects (glow behind text, shadow elements, background patterns) and z-stacked designs (card stacks, depth layers) are intentional. The layout step is about catching **unintentional** overlap \u2014 two headlines landing on top of each other, a stat covering a label, content bleeding off-frame.
51907
+
51908
+ ## Data Attributes
51909
+
51910
+ ### All Clips
51911
+
51912
+ | Attribute | Required | Values |
51913
+ | ------------------ | --------------------------------- | ------------------------------------------------------ |
51914
+ | \`id\` | Yes | Unique identifier |
51915
+ | \`data-start\` | Yes | Seconds or clip ID reference (\`"el-1"\`, \`"intro + 2"\`) |
51916
+ | \`data-duration\` | Required for img/div/compositions | Seconds. Video/audio defaults to media duration. |
51917
+ | \`data-track-index\` | Yes | Integer. Same-track clips cannot overlap. |
51918
+ | \`data-media-start\` | No | Trim offset into source (seconds) |
51919
+ | \`data-volume\` | No | 0-1 (default 1) |
51920
+
51921
+ \`data-track-index\` does **not** affect visual layering \u2014 use CSS \`z-index\`.
51922
+
51923
+ ### Composition Clips
51924
+
51925
+ | Attribute | Required | Values |
51926
+ | ---------------------------- | -------- | -------------------------------------------- |
51927
+ | \`data-composition-id\` | Yes | Unique composition ID |
51928
+ | \`data-start\` | Yes | Start time (root composition: use \`"0"\`) |
51929
+ | \`data-duration\` | Yes | Takes precedence over GSAP timeline duration |
51930
+ | \`data-width\` / \`data-height\` | Yes | Pixel dimensions (1920x1080 or 1080x1920) |
51931
+ | \`data-composition-src\` | No | Path to external HTML file |
51932
+
51933
+ ## Composition Structure
51934
+
51935
+ Sub-compositions loaded via \`data-composition-src\` use a \`<template>\` wrapper. **Standalone compositions (the main index.html) do NOT use \`<template>\`** \u2014 they put the \`data-composition-id\` div directly in \`<body>\`. Using \`<template>\` on a standalone file hides all content from the browser and breaks rendering.
51936
+
51937
+ Sub-composition structure:
51938
+
51939
+ \`\`\`html
51940
+ <template id="my-comp-template">
51941
+ <div data-composition-id="my-comp" data-width="1920" data-height="1080">
51942
+ <!-- content -->
51943
+ <style>
51944
+ [data-composition-id="my-comp"] {
51945
+ /* scoped styles */
51946
+ }
51947
+ </style>
51948
+ <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
51949
+ <script>
51950
+ window.__timelines = window.__timelines || {};
51951
+ const tl = gsap.timeline({ paused: true });
51952
+ // tweens...
51953
+ window.__timelines["my-comp"] = tl;
51954
+ </script>
51955
+ </div>
51956
+ </template>
51957
+ \`\`\`
51958
+
51959
+ Load in root: \`<div id="el-1" data-composition-id="my-comp" data-composition-src="compositions/my-comp.html" data-start="0" data-duration="10" data-track-index="1"></div>\`
51960
+
51961
+ ## Video and Audio
51962
+
51963
+ Video must be \`muted playsinline\`. Audio is always a separate \`<audio>\` element:
51964
+
51965
+ \`\`\`html
51966
+ <video
51967
+ id="el-v"
51968
+ data-start="0"
51969
+ data-duration="30"
51970
+ data-track-index="0"
51971
+ src="video.mp4"
51972
+ muted
51973
+ playsinline
51974
+ ></video>
51975
+ <audio
51976
+ id="el-a"
51977
+ data-start="0"
51978
+ data-duration="30"
51979
+ data-track-index="2"
51980
+ src="video.mp4"
51981
+ data-volume="1"
51982
+ ></audio>
51983
+ \`\`\`
51984
+
51985
+ ## Timeline Contract
51986
+
51987
+ - All timelines start \`{ paused: true }\` \u2014 the player controls playback
51988
+ - Register every timeline: \`window.__timelines["<composition-id>"] = tl\`
51989
+ - Framework auto-nests sub-timelines \u2014 do NOT manually add them
51990
+ - Duration comes from \`data-duration\`, not from GSAP timeline length
51991
+ - Never create empty tweens to set duration
51992
+
51993
+ ## Rules (Non-Negotiable)
51994
+
51995
+ **Deterministic:** No \`Math.random()\`, \`Date.now()\`, or time-based logic. Use a seeded PRNG if you need pseudo-random values (e.g. mulberry32).
51996
+
51997
+ **GSAP:** Only animate visual properties (\`opacity\`, \`x\`, \`y\`, \`scale\`, \`rotation\`, \`color\`, \`backgroundColor\`, \`borderRadius\`, transforms). Do NOT animate \`visibility\`, \`display\`, or call \`video.play()\`/\`audio.play()\`.
51998
+
51999
+ **Animation conflicts:** Never animate the same property on the same element from multiple timelines simultaneously.
52000
+
52001
+ **No \`repeat: -1\`:** Infinite-repeat timelines break the capture engine. Calculate the exact repeat count from composition duration: \`repeat: Math.ceil(duration / cycleDuration) - 1\`.
52002
+
52003
+ **Synchronous timeline construction:** Never build timelines inside \`async\`/\`await\`, \`setTimeout\`, or Promises. The capture engine reads \`window.__timelines\` synchronously after page load. Fonts are embedded by the compiler, so they're available immediately \u2014 no need to wait for font loading.
52004
+
52005
+ **Never do:**
52006
+
52007
+ 1. Forget \`window.__timelines\` registration
52008
+ 2. Use video for audio \u2014 always muted video + separate \`<audio>\`
52009
+ 3. Nest video inside a timed div \u2014 use a non-timed wrapper
52010
+ 4. Use \`data-layer\` (use \`data-track-index\`) or \`data-end\` (use \`data-duration\`)
52011
+ 5. Animate video element dimensions \u2014 animate a wrapper div
52012
+ 6. Call play/pause/seek on media \u2014 framework owns playback
52013
+ 7. Create a top-level container without \`data-composition-id\`
52014
+ 8. Use \`repeat: -1\` on any timeline or tween \u2014 always finite repeats
52015
+ 9. Build timelines asynchronously (inside \`async\`, \`setTimeout\`, \`Promise\`)
52016
+ 10. Use \`gsap.set()\` on clip elements from later scenes \u2014 they don't exist in the DOM at page load. Use \`tl.set(selector, vars, timePosition)\` inside the timeline at or after the clip's \`data-start\` time instead.
52017
+ 11. Use \`<br>\` in content text \u2014 forced line breaks don't account for actual rendered font width. Text that wraps naturally + a \`<br>\` produces an extra unwanted break, causing overlap. Let text wrap via \`max-width\` instead. Exception: short display titles where each word is deliberately on its own line (e.g., "THE\\nIMMORTAL\\nGAME" at 130px).
52018
+
52019
+ ## Scene Transitions (Non-Negotiable)
52020
+
52021
+ Every multi-scene composition MUST follow ALL of these rules. Violating any one of them is a broken composition.
52022
+
52023
+ 1. **ALWAYS use transitions between scenes.** No jump cuts. No exceptions.
52024
+ 2. **ALWAYS use entrance animations on every scene.** Every element animates IN via \`gsap.from()\`. No element may appear fully-formed. If a scene has 5 elements, it needs 5 entrance tweens.
52025
+ 3. **NEVER use exit animations** except on the final scene. This means: NO \`gsap.to()\` that animates opacity to 0, y offscreen, scale to 0, or any other "out" animation before a transition fires. The transition IS the exit. The outgoing scene's content MUST be fully visible at the moment the transition starts.
52026
+ 4. **Final scene only:** The last scene may fade elements out (e.g., fade to black). This is the ONLY scene where \`gsap.to(..., { opacity: 0 })\` is allowed.
52027
+
52028
+ **WRONG \u2014 exit animation before transition:**
52029
+
52030
+ \`\`\`js
52031
+ // BANNED \u2014 this empties the scene before the transition can use it
52032
+ tl.to("#s1-title", { opacity: 0, y: -40, duration: 0.4 }, 6.5);
52033
+ tl.to("#s1-subtitle", { opacity: 0, duration: 0.3 }, 6.7);
52034
+ // transition fires on empty frame
52035
+ \`\`\`
52036
+
52037
+ **RIGHT \u2014 entrance only, transition handles exit:**
52038
+
52039
+ \`\`\`js
52040
+ // Scene 1 entrance animations
52041
+ tl.from("#s1-title", { y: 50, opacity: 0, duration: 0.7, ease: "power3.out" }, 0.3);
52042
+ tl.from("#s1-subtitle", { y: 30, opacity: 0, duration: 0.5, ease: "power2.out" }, 0.6);
52043
+ // NO exit tweens \u2014 transition at 7.2s handles the scene change
52044
+ // Scene 2 entrance animations
52045
+ tl.from("#s2-heading", { x: -40, opacity: 0, duration: 0.6, ease: "expo.out" }, 8.0);
52046
+ \`\`\`
52047
+
52048
+ ## Animation Guardrails
52049
+
52050
+ - Offset first animation 0.1-0.3s (not t=0)
52051
+ - Vary eases across entrance tweens \u2014 use at least 3 different eases per scene
52052
+ - Don't repeat an entrance pattern within a scene
52053
+ - Avoid full-screen linear gradients on dark backgrounds (H.264 banding \u2014 use radial or solid + localized glow)
52054
+ - 60px+ headlines, 20px+ body, 16px+ data labels for rendered video
52055
+ - \`font-variant-numeric: tabular-nums\` on number columns
52056
+
52057
+ When no \`visual-style.md\` or animation direction is provided, follow [house-style.md](./house-style.md) for aesthetic defaults.
52058
+
52059
+ ## Typography and Assets
52060
+
52061
+ - **Fonts:** Just write the \`font-family\` you want in CSS \u2014 the compiler embeds supported fonts automatically. If a font isn't supported, the compiler warns.
52062
+ - Add \`crossorigin="anonymous"\` to external media
52063
+ - For dynamic text overflow, use \`window.__hyperframes.fitTextFontSize(text, { maxWidth, fontFamily, fontWeight })\`
52064
+ - All files live at the project root alongside \`index.html\`; sub-compositions use \`../\`
52065
+
52066
+ ## Editing Existing Compositions
52067
+
52068
+ - Read the full composition first \u2014 match existing fonts, colors, animation patterns
52069
+ - Only change what was requested
52070
+ - Preserve timing of unrelated clips
52071
+
52072
+ ## Output Checklist
52073
+
52074
+ - [ ] \`npx hyperframes lint\` and \`npx hyperframes validate\` both pass
52075
+ - [ ] \`npx hyperframes inspect\` passes, or every reported overflow is intentionally marked
52076
+ - [ ] Contrast warnings addressed (see Quality Checks below)
52077
+ - [ ] Layout issues addressed (see Quality Checks below)
52078
+ - [ ] Animation choreography verified (see Quality Checks below)
52079
+
52080
+ ## Quality Checks
52081
+
52082
+ ### Visual Inspect
52083
+
52084
+ \`hyperframes inspect\` runs the composition in headless Chrome, seeks through the timeline, and maps visual layout issues with timestamps, selectors, bounding boxes, and fix hints. Run it after \`lint\` and \`validate\`:
52085
+
52086
+ \`\`\`bash
52087
+ npx hyperframes inspect
52088
+ npx hyperframes inspect --json
52089
+ \`\`\`
52090
+
52091
+ Failures usually mean text is spilling out of a bubble/card, a fixed-size label is clipping dynamic copy, or text has moved off the canvas. Fix by increasing container size or padding, reducing font size or letter spacing, adding a real \`max-width\` so text wraps inside the container, or using \`window.__hyperframes.fitTextFontSize(...)\` for dynamic copy.
52092
+
52093
+ Use \`--samples 15\` for dense videos and \`--at 1.5,4,7.25\` for specific hero frames. Repeated static issues are collapsed by default to avoid flooding agent context. If overflow is intentional for an entrance/exit animation, mark the element or ancestor with \`data-layout-allow-overflow\`. If a decorative element should never be audited, mark it with \`data-layout-ignore\`.
52094
+
52095
+ \`hyperframes layout\` is the compatibility alias for the same check.
52096
+
52097
+ ### Contrast
52098
+
52099
+ \`hyperframes validate\` runs a WCAG contrast audit by default. It seeks to 5 timestamps, screenshots the page, samples background pixels behind every text element, and computes contrast ratios. Failures appear as warnings:
52100
+
52101
+ \`\`\`
52102
+ \u26A0 WCAG AA contrast warnings (3):
52103
+ \xB7 .subtitle "secondary text" \u2014 2.67:1 (need 4.5:1, t=5.3s)
52104
+ \`\`\`
52105
+
52106
+ If warnings appear:
52107
+
52108
+ - On dark backgrounds: brighten the failing color until it clears 4.5:1 (normal text) or 3:1 (large text, 24px+ or 19px+ bold)
52109
+ - On light backgrounds: darken it
52110
+ - Stay within the palette family \u2014 don't invent a new color, adjust the existing one
52111
+ - Re-run \`hyperframes validate\` until clean
52112
+
52113
+ Use \`--no-contrast\` to skip if iterating rapidly and you'll check later.
52114
+
52115
+ ### Animation Map
52116
+
52117
+ After authoring animations, run the animation map to verify choreography:
52118
+
52119
+ \`\`\`bash
52120
+ node skills/hyperframes/scripts/animation-map.mjs <composition-dir> \\
52121
+ --out <composition-dir>/.hyperframes/anim-map
52122
+ \`\`\`
52123
+
52124
+ Outputs a single \`animation-map.json\` with:
52125
+
52126
+ - **Per-tween summaries**: \`"#card1 animates opacity+y over 0.50s. moves 23px up. fades in. ends at (120, 200)"\`
52127
+ - **ASCII timeline**: Gantt chart of all tweens across the composition duration
52128
+ - **Stagger detection**: reports actual intervals (\`"3 elements stagger at 120ms"\`)
52129
+ - **Dead zones**: periods over 1s with no animation \u2014 intentional hold or missing entrance?
52130
+ - **Element lifecycles**: first/last animation time, final visibility
52131
+ - **Scene snapshots**: visible element state at 5 key timestamps
52132
+ - **Flags**: \`offscreen\`, \`collision\`, \`invisible\`, \`paced-fast\` (under 0.2s), \`paced-slow\` (over 2s)
52133
+
52134
+ Read the JSON. Scan summaries for anything unexpected. Check every flag \u2014 fix or justify. Verify the timeline shows the intended choreography rhythm. Re-run after fixes.
52135
+
52136
+ Skip on small edits (fixing a color, adjusting one duration). Run on new compositions and significant animation changes.
52137
+
52138
+ ---
52139
+
52140
+ ## References (loaded on demand)
52141
+
52142
+ - **[references/captions.md](references/captions.md)** \u2014 Captions, subtitles, lyrics, karaoke synced to audio. Tone-adaptive style detection, per-word styling, text overflow prevention, caption exit guarantees, word grouping. Read when adding any text synced to audio timing.
52143
+ - **[references/tts.md](references/tts.md)** \u2014 Text-to-speech with Kokoro-82M. Voice selection, speed tuning, TTS+captions workflow. Read when generating narration or voiceover.
52144
+ - **[references/audio-reactive.md](references/audio-reactive.md)** \u2014 Audio-reactive animation: map frequency bands and amplitude to GSAP properties. Read when visuals should respond to music, voice, or sound.
52145
+ - **[references/css-patterns.md](references/css-patterns.md)** \u2014 CSS+GSAP marker highlighting: highlight, circle, burst, scribble, sketchout. Deterministic, fully seekable. Read when adding visual emphasis to text.
52146
+ - **[references/typography.md](references/typography.md)** \u2014 Typography: font pairing, OpenType features, dark-background adjustments, font discovery script. **Always read** \u2014 every composition has text.
52147
+ - **[references/motion-principles.md](references/motion-principles.md)** \u2014 Motion design principles: easing as emotion, timing as weight, choreography as hierarchy, scene pacing, ambient motion, anti-patterns. Read when choreographing GSAP animations.
52148
+ - **[visual-styles.md](visual-styles.md)** \u2014 8 named visual styles (Swiss Pulse, Velvet Standard, Deconstructed, Maximalist Type, Data Drift, Soft Signal, Folk Frequency, Shadow Cut) with hex palettes, GSAP easing signatures, and shader pairings. Read when user names a style or when generating DESIGN.md.
52149
+ - **[house-style.md](house-style.md)** \u2014 Default motion, sizing, and color palettes when no style is specified.
52150
+ - **[patterns.md](patterns.md)** \u2014 PiP, title cards, slide show patterns.
52151
+ - **[data-in-motion.md](data-in-motion.md)** \u2014 Data, stats, and infographic patterns.
52152
+ - **[references/transcript-guide.md](references/transcript-guide.md)** \u2014 Transcription commands, whisper models, external APIs, troubleshooting.
52153
+ - **[references/dynamic-techniques.md](references/dynamic-techniques.md)** \u2014 Dynamic caption animation techniques (karaoke, clip-path, slam, scatter, elastic, 3D).
52154
+
52155
+ - **[references/transitions.md](references/transitions.md)** \u2014 Scene transitions: crossfades, wipes, reveals, shader transitions. Energy/mood selection, CSS vs WebGL guidance. **Always read for multi-scene compositions** \u2014 scenes without transitions feel like jump cuts.
52156
+ - [transitions/catalog.md](references/transitions/catalog.md) \u2014 Hard rules, scene template, and routing to per-type implementation code.
52157
+ - Shader transitions are in \`@hyperframes/shader-transitions\` (\`packages/shader-transitions/\`) \u2014 read package source, not skill files.
52158
+
52159
+ GSAP patterns and effects are in the \`/gsap\` skill.
52160
+ `;
52161
+ HOUSE_STYLE_MD = `# House Style
52162
+
52163
+ Creative direction for compositions when no \`visual-style.md\` is provided. These are starting points \u2014 override anything that doesn't serve the content.
52164
+
52165
+ ## Before Writing HTML
52166
+
52167
+ 1. **Interpret the prompt.** Generate real content. A recipe lists real ingredients. A HUD has real readouts.
52168
+ 2. **Pick a palette.** Light or dark? Declare bg, fg, accent before writing code.
52169
+ 3. **Pick typefaces.** Run the font discovery script in [references/typography.md](references/typography.md) \u2014 or pick a font you already know that fits the theme. The script broadens your options; it's not the only source.
52170
+
52171
+ ## Lazy Defaults to Question
52172
+
52173
+ These patterns are AI design tells \u2014 the first thing every LLM reaches for. If you're about to use one, pause and ask: is this a deliberate choice for THIS content, or am I defaulting?
52174
+
52175
+ - Gradient text (\`background-clip: text\` + gradient)
52176
+ - Left-edge accent stripes on cards/callouts
52177
+ - Cyan-on-dark / purple-to-blue gradients / neon accents
52178
+ - Pure \`#000\` or \`#fff\` (tint toward your accent hue instead)
52179
+ - Identical card grids (same-size cards repeated)
52180
+ - Everything centered with equal weight (lead the eye somewhere)
52181
+ - Banned fonts (see [references/typography.md](references/typography.md) for full list)
52182
+
52183
+ If the content genuinely calls for one of these \u2014 centered layout for a solemn closing, cards for a real product UI mockup, a banned font because it's the perfect thematic match \u2014 use it. The goal is intentionality, not avoidance.
52184
+
52185
+ ## Color
52186
+
52187
+ - Match light/dark to content: food, wellness, kids \u2192 light. Tech, cinema, finance \u2192 dark.
52188
+ - One accent hue. Same background across all scenes.
52189
+ - Tint neutrals toward your accent (even subtle warmth/coolness beats dead gray).
52190
+ - **Contrast:** enforced by \`hyperframes validate\` (WCAG AA). Text must be readable with decoratives removed.
52191
+ - Declare palette up front. Don't invent colors per-element.
52192
+
52193
+ ## Background Layer
52194
+
52195
+ Every scene needs visual depth \u2014 persistent decorative elements that stay visible while content animates in. Without these, scenes feel empty during entrance staggering.
52196
+
52197
+ Ideas (mix and match, 2-5 per scene):
52198
+
52199
+ - Radial glows (accent-tinted, low opacity, breathing scale)
52200
+ - Ghost text (theme words at 3-8% opacity, very large, slow drift)
52201
+ - Accent lines (hairline rules, subtle pulse)
52202
+ - Grain/noise overlay, geometric shapes, grid patterns
52203
+ - Thematic decoratives (orbit rings for space, vinyl grooves for music, grid lines for data)
52204
+
52205
+ All decoratives should have slow ambient GSAP animation \u2014 breathing, drift, pulse. Static decoratives feel dead.
52206
+
52207
+ ## Motion
52208
+
52209
+ See [references/motion-principles.md](references/motion-principles.md) for full rules. Quick: 0.3\u20130.6s, vary eases, combine transforms on entrances, overlap entries.
52210
+
52211
+ ## Typography
52212
+
52213
+ See [references/typography.md](references/typography.md) for full rules. Quick: 700-900 headlines / 300-400 body, serif + sans (not two sans), 60px+ headlines / 20px+ body.
52214
+
52215
+ ## Palettes
52216
+
52217
+ Declare one background, one foreground, one accent before writing HTML.
52218
+
52219
+ | Category | Use for | File |
52220
+ | ----------------- | --------------------------------------------- | ---------------------------------------------------------- |
52221
+ | Bold / Energetic | Product launches, social media, announcements | [palettes/bold-energetic.md](palettes/bold-energetic.md) |
52222
+ | Warm / Editorial | Storytelling, documentaries, case studies | [palettes/warm-editorial.md](palettes/warm-editorial.md) |
52223
+ | Dark / Premium | Tech, finance, luxury, cinematic | [palettes/dark-premium.md](palettes/dark-premium.md) |
52224
+ | Clean / Corporate | Explainers, tutorials, presentations | [palettes/clean-corporate.md](palettes/clean-corporate.md) |
52225
+ | Nature / Earth | Sustainability, outdoor, organic | [palettes/nature-earth.md](palettes/nature-earth.md) |
52226
+ | Neon / Electric | Gaming, tech, nightlife | [palettes/neon-electric.md](palettes/neon-electric.md) |
52227
+ | Pastel / Soft | Fashion, beauty, lifestyle, wellness | [palettes/pastel-soft.md](palettes/pastel-soft.md) |
52228
+ | Jewel / Rich | Luxury, events, sophisticated | [palettes/jewel-rich.md](palettes/jewel-rich.md) |
52229
+ | Monochrome | Dramatic, typography-focused | [palettes/monochrome.md](palettes/monochrome.md) |
52230
+
52231
+ Or derive from OKLCH \u2014 pick a hue, build bg/fg/accent at different lightnesses, tint everything toward that hue.
52232
+ `;
52233
+ MOTION_PRINCIPLES_MD = `# Motion Principles
52234
+
52235
+ ## Guardrails
52236
+
52237
+ You know these rules but you violate them. Stop.
52238
+
52239
+ - **Don't use the same ease on every tween.** You default to \`power2.out\` on everything. Vary eases like you vary font weights \u2014 no more than 2 independent tweens with the same ease in a scene.
52240
+ - **Don't use the same speed on everything.** You default to 0.4-0.5s for everything. The slowest scene should be 3\xD7 slower than the fastest. Vary duration deliberately.
52241
+ - **Don't enter everything from the same direction.** You default to \`y: 30, opacity: 0\` on every element. Vary: from left, from right, from scale, opacity-only, letter-spacing.
52242
+ - **Don't use the same stagger on every scene.** Each scene needs its own rhythm.
52243
+ - **Don't use ambient zoom on every scene.** Pick different ambient motion per scene: slow pan, subtle rotation, scale push, color shift, or nothing. Stillness after motion is powerful.
52244
+ - **Don't start at t=0.** Offset the first animation 0.1-0.3s. Zero-delay feels like a jump cut.
52245
+
52246
+ ## What You Don't Do Without Being Told
52247
+
52248
+ ### Easing is emotion, not technique
52249
+
52250
+ The transition is the verb. The easing is the adverb. A slide-in with \`expo.out\` = confident. With \`sine.inOut\` = dreamy. With \`elastic.out\` = playful. Same motion, different meaning. Choose the adverb deliberately.
52251
+
52252
+ **Direction rules \u2014 these are not optional:**
52253
+
52254
+ - \`.out\` for elements entering. Starts fast, decelerates. Feels responsive. This is your default.
52255
+ - \`.in\` for elements leaving. Starts slow, accelerates away. Throws them off.
52256
+ - \`.inOut\` for elements moving between positions.
52257
+
52258
+ You get this backwards constantly. Ease-in for entrances feels sluggish. Ease-out for exits feels reluctant.
52259
+
52260
+ ### Speed communicates weight
52261
+
52262
+ - Fast (0.15-0.3s) \u2014 energy, urgency, confidence
52263
+ - Medium (0.3-0.5s) \u2014 professional, most content
52264
+ - Slow (0.5-0.8s) \u2014 gravity, luxury, contemplation
52265
+ - Very slow (0.8-2.0s) \u2014 cinematic, emotional, atmospheric
52266
+
52267
+ ### Scene structure: build / breathe / resolve
52268
+
52269
+ Every scene has three phases. You dump everything in the build and leave nothing for breathe or resolve.
52270
+
52271
+ - **Build (0-30%)** \u2014 elements enter, staggered. Don't dump everything at once.
52272
+ - **Breathe (30-70%)** \u2014 content visible, alive with ONE ambient motion.
52273
+ - **Resolve (70-100%)** \u2014 exit or decisive end. Exits are faster than entrances.
52274
+
52275
+ ### Transitions are meaning
52276
+
52277
+ - **Crossfade** = "this continues"
52278
+ - **Hard cut** = "wake up" / disruption
52279
+ - **Slow dissolve** = "drift with me"
52280
+
52281
+ You crossfade everything. Use hard cuts for disruption and register shifts.
52282
+
52283
+ ### Choreography is hierarchy
52284
+
52285
+ The element that moves first is perceived as most important. Stagger in order of importance, not DOM order. Don't wait for completion \u2014 overlap entries. Total stagger sequence under 500ms regardless of item count.
52286
+
52287
+ ### Asymmetry
52288
+
52289
+ Entrances need longer than exits. A card takes 0.4s to appear but 0.25s to disappear.
52290
+
52291
+ ## Visual Composition
52292
+
52293
+ You build for the web. Video frames are not pages.
52294
+
52295
+ - **Two focal points minimum per scene.** The eye needs somewhere to travel. Never a single text block floating in empty space.
52296
+ - **Fill the frame.** Hero text: 60-80% of width. You will try to use web-sized elements. Don't.
52297
+ - **Three layers minimum per scene.** Background treatment (glow, oversized faded type, color panel). Foreground content. Accent elements (dividers, labels, data bars).
52298
+ - **Background is not empty.** Radial glows, oversized faded type bleeding off-frame, subtle border panels, hairline rules. Pure solid #000 reads as "nothing loaded."
52299
+ - **Anchor to edges.** Pin content to left/top or right/bottom. Centered-and-floating is a web pattern.
52300
+ - **Split frames.** Data panel on the left, content on the right. Top bar with metadata, full-width below. Zone-based layouts, not centered stacks.
52301
+ - **Use structural elements.** Rules, dividers, border panels. They create paths for the eye and animate well (scaleX from 0).
52302
+ `;
52303
+ TYPOGRAPHY_MD = `# Typography
52304
+
52305
+ The compiler embeds supported fonts \u2014 just write \`font-family\` in CSS.
52306
+
52307
+ ## Banned
52308
+
52309
+ Training-data defaults that every LLM reaches for. These produce monoculture across compositions.
52310
+
52311
+ Inter, Roboto, Open Sans, Noto Sans, Arimo, Lato, Source Sans, PT Sans, Nunito, Poppins, Outfit, Sora, Playfair Display, Cormorant Garamond, Bodoni Moda, EB Garamond, Cinzel, Prata, Syne
52312
+
52313
+ **Syne in particular** is the most overused "distinctive" display font. It is an instant AI design tell.
52314
+
52315
+ ## Guardrails
52316
+
52317
+ You know these rules but you violate them. Stop.
52318
+
52319
+ - **Don't pair two sans-serifs.** You do this constantly \u2014 one for headlines, one for body. Cross the boundary: serif + sans, or sans + mono.
52320
+ - **One expressive font per scene.** You pick two interesting fonts trying to make it "better." One performs, one recedes.
52321
+ - **Weight contrast must be extreme.** You default to 400 vs 700. Video needs 300 vs 900. The difference must be visible in motion at a glance.
52322
+ - **Video sizes, not web sizes.** Body: 20px minimum. Headlines: 60px+. Data labels: 16px. You will try to use 14px. Don't.
52323
+
52324
+ ## What You Don't Do Without Being Told
52325
+
52326
+ - **Tension should mean something.** Don't pattern-match pairings. Ask WHY these two fonts disagree. The pairing should embody the content's contradiction \u2014 mechanical vs human, public vs private, institutional vs personal. If you can't articulate the tension, it's arbitrary.
52327
+ - **Register switching.** Assign different fonts to different communicative modes \u2014 one voice for statements, another for data, another for attribution. Not hierarchy on a page. Voices in a conversation.
52328
+ - **Tension can live inside a single font.** A font that looks familiar but is secretly strange creates tension with the viewer's expectations, not with another font.
52329
+ - **One variable changed = dramatic contrast.** Same letterforms, monospaced vs proportional. Same family at different optical sizes. Changing only rhythm while everything else stays constant.
52330
+ - **Double personality works.** Two expressive fonts can coexist if they share an attitude (both irreverent, both precise) even when their forms are completely different.
52331
+ - **Time is hierarchy.** The first element to appear is the most important. In video, sequence replaces position.
52332
+ - **Motion is typography.** How a word enters carries as much meaning as the font. A 0.1s slam vs a 2s fade \u2014 same font, completely different message.
52333
+ - **Fixed reading time.** 3 seconds on screen = must be readable in 2. Fewer words, larger type.
52334
+ - **Tracking tighter than web.** -0.03em to -0.05em on display sizes. Video encoding compresses letter detail.
52335
+
52336
+ ## Finding Fonts
52337
+
52338
+ Don't default to what you know. If the content is luxury, a grotesque sans might create more tension than the expected Didone serif. Decide the register first, then search.
52339
+
52340
+ Save this script to \`/tmp/fontquery.py\` and run with \`curl -s 'https://fonts.google.com/metadata/fonts' > /tmp/gfonts.json && python3 /tmp/fontquery.py /tmp/gfonts.json\`:
52341
+
52342
+ \`\`\`python
52343
+ import json, sys, random
52344
+ from collections import OrderedDict
52345
+
52346
+ random.seed() # true random each run
52347
+
52348
+ with open(sys.argv[1]) as f:
52349
+ data = json.load(f)
52350
+ fonts = data.get("familyMetadataList", [])
52351
+
52352
+ ban = {"Inter","Roboto","Open Sans","Noto Sans","Lato","Poppins","Source Sans 3",
52353
+ "PT Sans","Nunito","Outfit","Sora","Playfair Display","Cormorant Garamond",
52354
+ "Bodoni Moda","EB Garamond","Cinzel","Prata","Arimo","Source Sans Pro","Syne"}
52355
+ skip_pfx = ("Roboto","Noto ","Google Sans","Bpmf","Playwrite","Anek","BIZ ",
52356
+ "Nanum","Shippori","Sawarabi","Zen ","Kaisei","Kiwi ","Yuji ","Radio ")
52357
+
52358
+ def ok(f):
52359
+ if f["family"] in ban: return False
52360
+ if any(f["family"].startswith(b) for b in skip_pfx): return False
52361
+ if "latin" not in (f.get("subsets") or []): return False
52362
+ return True
52363
+
52364
+ seen = set()
52365
+ R = OrderedDict()
52366
+
52367
+ # Trending Sans \u2014 recent (2022+), popular (<300)
52368
+ R["Trending Sans"] = []
52369
+ for f in fonts:
52370
+ if not ok(f) or f["family"] in seen: continue
52371
+ if f.get("category") in ("Sans Serif","Display") and f.get("dateAdded","") >= "2022-01-01" and f.get("popularity",9999) < 300:
52372
+ R["Trending Sans"].append(f); seen.add(f["family"])
52373
+
52374
+ # Trending Serif \u2014 recent (2018+), popular (<600)
52375
+ R["Trending Serif"] = []
52376
+ for f in fonts:
52377
+ if not ok(f) or f["family"] in seen: continue
52378
+ if f.get("category") == "Serif" and f.get("dateAdded","") >= "2018-01-01" and f.get("popularity",9999) < 600:
52379
+ R["Trending Serif"].append(f); seen.add(f["family"])
52380
+
52381
+ # Monospace \u2014 recent (2018+), popular (<600)
52382
+ R["Monospace"] = []
52383
+ for f in fonts:
52384
+ if not ok(f) or f["family"] in seen: continue
52385
+ if f.get("category") == "Monospace" and f.get("dateAdded","") >= "2018-01-01" and f.get("popularity",9999) < 600:
52386
+ R["Monospace"].append(f); seen.add(f["family"])
52387
+
52388
+ # Impact & Condensed \u2014 heavy display fonts with 800+ weight
52389
+ R["Impact & Condensed"] = []
52390
+ for f in fonts:
52391
+ if not ok(f) or f["family"] in seen: continue
52392
+ has_heavy = any(k in list(f.get("fonts",{}).keys()) for k in ("800","900"))
52393
+ is_display = f.get("category") in ("Sans Serif","Display")
52394
+ if has_heavy and is_display and f.get("popularity",9999) < 400:
52395
+ R["Impact & Condensed"].append(f); seen.add(f["family"])
52396
+
52397
+ # Script & Handwriting \u2014 popular (<300)
52398
+ R["Script & Handwriting"] = []
52399
+ for f in fonts:
52400
+ if not ok(f) or f["family"] in seen: continue
52401
+ if f.get("category") == "Handwriting" and f.get("popularity",9999) < 300:
52402
+ R["Script & Handwriting"].append(f); seen.add(f["family"])
52403
+
52404
+
52405
+ # Randomize the top 5 in each category so the LLM doesn't always pick the same first result
52406
+ for cat in R:
52407
+ R[cat].sort(key=lambda x: x.get("popularity",9999))
52408
+ top5 = R[cat][:5]
52409
+ rest = R[cat][5:]
52410
+ random.shuffle(top5)
52411
+ R[cat] = top5 + rest
52412
+ limits = {"Trending Sans":15,"Trending Serif":12,"Monospace":8,
52413
+ "Impact & Condensed":12,"Script & Handwriting":10}
52414
+ for cat in R:
52415
+ items = R[cat][:limits.get(cat,10)]
52416
+ if not items: continue
52417
+ print(f"--- {cat} ({len(items)}) ---")
52418
+ for ff in items:
52419
+ var = "VAR" if ff.get("axes") else " "
52420
+ print(f' {ff.get("popularity"):4d} | {var} | {ff["family"]}')
52421
+ print()
52422
+ \`\`\`
52423
+
52424
+ Five categories: trending sans, trending serif, monospace, impact/condensed, script/handwriting. All dynamically filtered from Google Fonts metadata \u2014 no hardcoded font names. Cross classification boundaries when pairing.
52425
+
52426
+ ## Selection Thinking
52427
+
52428
+ Don't pick fonts by category reflex (editorial \u2192 serif, tech \u2192 mono, modern \u2192 geometric sans). That's pattern matching, not design.
52429
+
52430
+ 1. **Name the register.** What voice is the content speaking in? Institutional authority? Personal confession? Technical precision? Casual irreverence? The register narrows the field more than the category.
52431
+ 2. **Think physically.** Imagine the font as a physical object the brand could ship \u2014 a museum exhibit caption, a hand-painted shop sign, a 1970s mainframe terminal manual, a fabric label inside a coat, a children's book printed on cheap newsprint, a tax form. Whichever physical object fits the register is pointing at the right _kind_ of typeface.
52432
+ 3. **Reject your first instinct.** The first font that feels right is usually your training-data default for that register. If you picked it last time too, find something else.
52433
+ 4. **Cross-check the assumption.** An editorial brief does NOT need a serif. A technical brief does NOT need a sans. A children's product does NOT need a rounded display font. The most distinctive choice often contradicts the category expectation.
52434
+
52435
+ ## Similar-Font Pairing
52436
+
52437
+ Never pair two fonts that are similar but not identical \u2014 two geometric sans-serifs, two transitional serifs, two humanist sans. They create visual friction without clear hierarchy. The viewer senses something is "off" but can't articulate it. Either use one font at two weights, or pair fonts that contrast on multiple axes: serif + sans, condensed + wide, geometric + humanist.
52438
+
52439
+ ## Dark Backgrounds
52440
+
52441
+ Light text on dark backgrounds creates two optical illusions you need to compensate for:
52442
+
52443
+ - **Increased apparent weight.** Light-on-dark reads heavier than dark-on-light at the same \`font-weight\`. Use 350 instead of 400 for body text. Headlines are less affected because size compensates.
52444
+ - **Tighter apparent spacing.** Light halos around letterforms reduce perceived gaps. Increase \`line-height\` by 0.05-0.1 beyond your light-background value. For display sizes, add 0.01em \`letter-spacing\` to counteract.
52445
+
52446
+ ## OpenType Features for Data
52447
+
52448
+ Most fonts ship with OpenType features that are off by default. Turn them on for data compositions:
52449
+
52450
+ \`\`\`css
52451
+ /* Tabular numbers \u2014 digits align vertically in columns */
52452
+ .stat-value,
52453
+ .timer,
52454
+ .data-column {
52455
+ font-variant-numeric: tabular-nums;
52456
+ }
52457
+
52458
+ /* Diagonal fractions \u2014 renders 1/2 as \xBD */
52459
+ .recipe-amount,
52460
+ .ratio {
52461
+ font-variant-numeric: diagonal-fractions;
52462
+ }
52463
+
52464
+ /* Small caps for abbreviations \u2014 less visual shouting */
52465
+ .abbreviation,
52466
+ .unit {
52467
+ font-variant-caps: all-small-caps;
52468
+ }
52469
+
52470
+ /* Disable ligatures in code \u2014 fi, fl, ffi should stay separate */
52471
+ code,
52472
+ .code {
52473
+ font-variant-ligatures: none;
52474
+ }
52475
+ \`\`\`
52476
+
52477
+ \`tabular-nums\` is essential any time numbers are stacked vertically \u2014 stat callouts, timers, scoreboards, data tables. Without it, digits have proportional widths and columns don't align.
52478
+ `;
52479
+ TRANSITIONS_MD = `# Scene Transitions
52480
+
52481
+ A transition tells the viewer how two scenes relate. A crossfade says "this continues." A push slide says "next point." A blur crossfade says "drift with me." Choose transitions that match what the content is doing emotionally, not just technically.
52482
+
52483
+ ## Animation Rules for Multi-Scene Compositions
52484
+
52485
+ These are non-negotiable for every multi-scene composition:
52486
+
52487
+ 1. **Every composition uses transitions.** No exceptions. Scenes without transitions feel like jump cuts.
52488
+ 2. **Every scene uses entrance animations.** Elements animate IN via \`gsap.from()\` \u2014 opacity, position, scale, etc. No scene should pop fully-formed onto screen.
52489
+ 3. **Exit animations are BANNED** except on the final scene. Do NOT use \`gsap.to()\` to animate elements out before a transition fires. The transition IS the exit. Outgoing scene content must be fully visible when the transition starts \u2014 the transition handles the visual handoff.
52490
+ 4. **Final scene exception:** The last scene MAY fade elements out (e.g., fade to black at the end of the composition). This is the only scene where exit animations are allowed.
52491
+
52492
+ ## Energy \u2192 Primary Transition
52493
+
52494
+ | Energy | CSS Primary | Shader Primary | Accent | Duration | Easing |
52495
+ | ---------------------------------------- | ---------------------------- | ------------------------------------ | ------------------------------ | --------- | ---------------------- |
52496
+ | **Calm** (wellness, brand story, luxury) | Blur crossfade, focus pull | Cross-warp morph, thermal distortion | Light leak, circle iris | 0.5-0.8s | \`sine.inOut\`, \`power1\` |
52497
+ | **Medium** (corporate, SaaS, explainer) | Push slide, staggered blocks | Whip pan, cinematic zoom | Squeeze, vertical push | 0.3-0.5s | \`power2\`, \`power3\` |
52498
+ | **High** (promos, sports, music, launch) | Zoom through, overexposure | Ridged burn, glitch, chromatic split | Staggered blocks, gravity drop | 0.15-0.3s | \`power4\`, \`expo\` |
52499
+
52500
+ Pick ONE primary (60-70% of scene changes) + 1-2 accents. Never use a different transition for every scene.
52501
+
52502
+ ## Mood \u2192 Transition Type
52503
+
52504
+ Think about what the transition _communicates_, not just what it looks like.
52505
+
52506
+ | Mood | Transitions | Why it works |
52507
+ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
52508
+ | **Warm / inviting** | Light leak, blur crossfade, focus pull, film burn \xB7 **Shader:** thermal distortion, light leak, cross-warp morph | Soft edges, warm color washes. Nothing sharp or mechanical. |
52509
+ | **Cold / clinical** | Squeeze, zoom out, blinds, shutter, grid dissolve \xB7 **Shader:** gravitational lens | Content transforms mechanically \u2014 compressed, shrunk, sliced, gridded. |
52510
+ | **Editorial / magazine** | Push slide, vertical push, diagonal split, shutter \xB7 **Shader:** whip pan | Like turning a page or slicing a layout. Clean directional movement. |
52511
+ | **Tech / futuristic** | Grid dissolve, staggered blocks, blinds, chromatic aberration \xB7 **Shader:** glitch, chromatic split | Grid dissolve is the core "data" transition. Shader glitch adds posterization + scan lines. |
52512
+ | **Tense / edgy** | Glitch, VHS, chromatic aberration, ripple \xB7 **Shader:** ridged burn, glitch, domain warp | Instability, distortion, digital breakdown. Ridged burn adds sharp lightning-crack edges. |
52513
+ | **Playful / fun** | Elastic push, 3D flip, circle iris, morph circle, clock wipe \xB7 **Shader:** ripple waves, swirl vortex | Overshoot, bounce, rotation, expansion. Swirl vortex adds organic spiral distortion. |
52514
+ | **Dramatic / cinematic** | Zoom through, zoom out, gravity drop, overexposure, color dip to black \xB7 **Shader:** cinematic zoom, gravitational lens, domain warp | Scale, weight, light extremes. Shader transitions add per-pixel depth. |
52515
+ | **Premium / luxury** | Focus pull, blur crossfade, color dip to black \xB7 **Shader:** cross-warp morph, thermal distortion | Restraint. Cross-warp morph flows both scenes into each other organically. |
52516
+ | **Retro / analog** | Film burn, light leak, VHS, clock wipe \xB7 **Shader:** light leak | Organic imperfection. Warm color bleeds, scan line displacement. |
52517
+
52518
+ ## Narrative Position
52519
+
52520
+ | Position | Use | Why |
52521
+ | -------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------- |
52522
+ | **Opening** | Your most distinctive transition. Match the mood. 0.4-0.6s | Sets the visual language for the entire piece. |
52523
+ | **Between related points** | Your primary transition. Consistent. 0.3s | Don't distract \u2014 the content is continuing. |
52524
+ | **Topic change** | Something different from your primary. Staggered blocks, shutter, squeeze. | Signals "new section" \u2014 the viewer's brain resets. |
52525
+ | **Climax / hero reveal** | Your boldest accent. Fastest or most dramatic. | This is the payoff \u2014 spend your best transition here. |
52526
+ | **Wind-down** | Return to gentle. Blur crossfade, crossfade. 0.5-0.7s | Let the viewer exhale after the climax. |
52527
+ | **Outro** | Slowest, simplest. Crossfade, color dip to black. 0.6-1.0s | Closure. Don't introduce new energy at the end. |
52528
+
52529
+ ## Blur Intensity by Energy
52530
+
52531
+ | Energy | Blur | Duration | Hold at peak |
52532
+ | ---------- | ------- | -------- | ------------ |
52533
+ | **Calm** | 20-30px | 0.8-1.2s | 0.3-0.5s |
52534
+ | **Medium** | 8-15px | 0.4-0.6s | 0.1-0.2s |
52535
+ | **High** | 3-6px | 0.2-0.3s | 0s |
52536
+
52537
+ ## Presets
52538
+
52539
+ | Preset | Duration | Easing |
52540
+ | ---------- | -------- | ----------------- |
52541
+ | \`snappy\` | 0.2s | \`power4.inOut\` |
52542
+ | \`smooth\` | 0.4s | \`power2.inOut\` |
52543
+ | \`gentle\` | 0.6s | \`sine.inOut\` |
52544
+ | \`dramatic\` | 0.5s | \`power3.in\` \u2192 out |
52545
+ | \`instant\` | 0.15s | \`expo.inOut\` |
52546
+ | \`luxe\` | 0.7s | \`power1.inOut\` |
52547
+
52548
+ ## Implementation
52549
+
52550
+ Read [transitions/catalog.md](transitions/catalog.md) for GSAP code and hard rules for every transition type.
52551
+
52552
+ | Category | CSS | Shader (WebGL) |
52553
+ | ----------- | -------------------------------------------------------------- | ------------------------------------------------------------------------- |
52554
+ | Push/slide | Push slide, vertical push, elastic push, squeeze | Whip pan |
52555
+ | Scale/zoom | Zoom through, zoom out, gravity drop, 3D flip | Cinematic zoom, gravitational lens |
52556
+ | Reveal/mask | Circle iris, diamond iris, diagonal split, clock wipe, shutter | SDF iris |
52557
+ | Dissolve | Crossfade, blur crossfade, focus pull, color dip | Cross-warp morph, domain warp |
52558
+ | Cover | Staggered blocks, horizontal blinds, vertical blinds | \u2014 |
52559
+ | Light | Light leak, overexposure burn, film burn | Light leak (shader), thermal distortion |
52560
+ | Distortion | Glitch, chromatic aberration, ripple, VHS tape | Glitch (shader), chromatic split, ridged burn, ripple waves, swirl vortex |
52561
+ | Pattern | Grid dissolve, morph circle | \u2014 |
52562
+
52563
+ ## Transitions That Don't Work in CSS
52564
+
52565
+ Avoid: star iris, tilt-shift, lens flare, hinge/door. See catalog.md for why.
52566
+
52567
+ ## CSS vs Shader
52568
+
52569
+ CSS transitions animate scene containers with opacity, transforms, clip-path, and filters. Shader transitions composite both scene textures per-pixel on a WebGL canvas \u2014 they can warp, dissolve, and morph in ways CSS cannot.
52570
+
52571
+ **Both are first-class options.** Shaders are provided by the \`@hyperframes/shader-transitions\` package \u2014 import from the package instead of writing raw GLSL. CSS transitions are simpler to set up. Choose based on the effect you want, not based on which is easier.
52572
+
52573
+ When a composition uses shader transitions, ALL transitions in that composition should be shader-based (the WebGL canvas replaces DOM-based scene switching). Don't mix CSS and shader transitions in the same composition.
52574
+
52575
+ ## Shader-Compatible CSS Rules
52576
+
52577
+ Shader transitions capture DOM scenes to WebGL textures via html2canvas. The canvas 2D rendering pipeline doesn't match CSS exactly. Follow these rules to avoid visible artifacts at transition boundaries:
52578
+
52579
+ 1. **No \`transparent\` keyword in gradients.** Canvas interpolates \`transparent\` as \`rgba(0,0,0,0)\` (black at zero alpha), creating dark fringes. Always use the target color at zero alpha: \`rgba(200,117,51,0)\` not \`transparent\`.
52580
+ 2. **No gradient backgrounds on elements thinner than 4px.** Canvas can't match CSS gradient rendering on 1-2px elements. Use solid \`background-color\` on thin accent lines.
52581
+ 3. **No CSS variables (\`var()\`) on elements visible during capture.** html2canvas doesn't reliably resolve custom properties. Use literal color values in inline styles.
52582
+ 4. **Mark uncapturable decorative elements with \`data-no-capture\`.** The capture function skips these. They're present on the live DOM but absent from the shader texture. Use for elements that can't follow the rules above.
52583
+ 5. **No gradient opacity below 0.15.** Gradient elements below 10% opacity render differently in canvas vs CSS. Increase to 0.15+ or use a solid color at equivalent brightness.
52584
+ 6. **Every \`.scene\` div must have explicit \`background-color\`, AND pass the same color as \`bgColor\` in the \`init()\` config.** The package captures scene elements via html2canvas. Both the CSS \`background-color\` on \`.scene\` and the \`bgColor\` config must match. Without either, the texture renders as black.
52585
+
52586
+ These rules only apply to shader transition compositions. CSS-only compositions have no restrictions.
52587
+
52588
+ ## Visual Pattern Warning
52589
+
52590
+ Avoid transitions that create visible repeating geometric patterns \u2014 grids of tiles, hexagonal cells, uniform dot arrays, evenly-spaced blob circles. These look cheap and artificial regardless of the math behind them. Organic noise (FBM, domain warping) is good because it's irregular. Geometric repetition is bad because the eye instantly sees the grid.
52591
+ `;
52592
+ }
52593
+ });
52594
+
52595
+ // src/commands/_shared/hf-skill-bundle/bundle.ts
52596
+ import { readFileSync, existsSync as existsSync27, statSync } from "node:fs";
52597
+ import { join as join7 } from "node:path";
52598
+ import { homedir as homedir3 } from "node:os";
52599
+ import { createHash as createHash2 } from "node:crypto";
52600
+ function installedSkillPath() {
52601
+ const candidate = join7(homedir3(), ".claude", "skills", "hyperframes");
52602
+ if (!existsSync27(candidate)) return null;
52603
+ if (!statSync(candidate).isDirectory()) return null;
52604
+ return candidate;
52605
+ }
52606
+ function joinSections(sections) {
52607
+ return sections.map((s) => `
52608
+
52609
+ === ${s.label} ===
52610
+
52611
+ ${s.content}`).join("\n");
52612
+ }
52613
+ function buildVendored() {
52614
+ return {
52615
+ content: joinSections(VENDORED_SECTIONS),
52616
+ hint: `[skill] using vendored Hyperframes snapshot ${BUNDLE_VERSION}. For latest, run \`npx skills add heygen-com/hyperframes\`.`
52617
+ };
52618
+ }
52619
+ function buildInstalled(skillRoot) {
52620
+ const installedPaths = {
52621
+ "hyperframes/SKILL.md": join7(skillRoot, "SKILL.md"),
52622
+ "hyperframes/house-style.md": join7(skillRoot, "house-style.md"),
52623
+ "hyperframes/motion-principles.md": join7(skillRoot, "references", "motion-principles.md"),
52624
+ "hyperframes/typography.md": join7(skillRoot, "references", "typography.md"),
52625
+ "hyperframes/transitions.md": join7(skillRoot, "references", "transitions.md")
52626
+ };
52627
+ const sections = [];
52628
+ for (const [label, path2] of Object.entries(installedPaths)) {
52629
+ if (!existsSync27(path2)) return null;
52630
+ sections.push({ label, content: readFileSync(path2, "utf-8") });
52631
+ }
52632
+ return {
52633
+ content: joinSections(sections),
52634
+ hint: `[skill] using installed Hyperframes skill from ${skillRoot}.`
52635
+ };
52636
+ }
52637
+ function loadHyperframesSkillBundle() {
52638
+ const installedRoot = installedSkillPath();
52639
+ if (installedRoot) {
52640
+ const r = buildInstalled(installedRoot);
52641
+ if (r) {
52642
+ return {
52643
+ content: r.content,
52644
+ source: "installed",
52645
+ hint: r.hint,
52646
+ hash: hashBundle(BUNDLE_VERSION, r.content)
52647
+ };
52648
+ }
52649
+ }
52650
+ const v = buildVendored();
52651
+ return {
52652
+ content: v.content,
52653
+ source: "vendored",
52654
+ hint: v.hint,
52655
+ hash: hashBundle(BUNDLE_VERSION, v.content)
52656
+ };
52657
+ }
52658
+ function hashBundle(version, content) {
52659
+ return createHash2("sha256").update(version).update(" ").update(content).digest("hex");
52660
+ }
52661
+ var BUNDLE_VERSION, VENDORED_SECTIONS;
52662
+ var init_bundle = __esm({
52663
+ "src/commands/_shared/hf-skill-bundle/bundle.ts"() {
52664
+ "use strict";
52665
+ init_bundle_content();
52666
+ BUNDLE_VERSION = "970367f-2026-04-25";
52667
+ VENDORED_SECTIONS = [
52668
+ { label: "hyperframes/SKILL.md", content: SKILL_MD },
52669
+ { label: "hyperframes/house-style.md", content: HOUSE_STYLE_MD },
52670
+ { label: "hyperframes/motion-principles.md", content: MOTION_PRINCIPLES_MD },
52671
+ { label: "hyperframes/typography.md", content: TYPOGRAPHY_MD },
52672
+ { label: "hyperframes/transitions.md", content: TRANSITIONS_MD }
52673
+ ];
52674
+ }
52675
+ });
52676
+
52677
+ // src/commands/_shared/storyboard-parse.ts
52678
+ function parseStoryboard(md) {
52679
+ const text = md.replace(/\r\n/g, "\n");
52680
+ const headings = [];
52681
+ HEADING_RE.lastIndex = 0;
52682
+ let m;
52683
+ while ((m = HEADING_RE.exec(text)) !== null) {
52684
+ headings.push({
52685
+ start: m.index,
52686
+ end: m.index + m[0].length,
52687
+ line: m[1].trim()
52688
+ });
52689
+ }
52690
+ if (headings.length === 0) {
52691
+ return { global: text.trim(), beats: [] };
52692
+ }
52693
+ const global2 = text.slice(0, headings[0].start).trim();
52694
+ const beats = headings.map((h, i) => {
52695
+ const bodyStart = h.end;
52696
+ const bodyEnd = i + 1 < headings.length ? headings[i + 1].start : text.length;
52697
+ const body = text.slice(bodyStart, bodyEnd).trim();
52698
+ const id = deriveBeatId(h.line);
52699
+ const duration = parseBeatDuration(body);
52700
+ return {
52701
+ id,
52702
+ heading: h.line,
52703
+ body,
52704
+ ...duration !== void 0 ? { duration } : {}
52705
+ };
52706
+ });
52707
+ return { global: global2, beats };
52708
+ }
52709
+ function deriveBeatId(headingLine) {
52710
+ const beatPrefix = headingLine.match(BEAT_PREFIX_RE);
52711
+ if (beatPrefix) {
52712
+ return slugify(beatPrefix[1]);
52713
+ }
52714
+ return slugify(headingLine);
52715
+ }
52716
+ function slugify(s) {
52717
+ return s.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/\([^)]*\)/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "beat";
52718
+ }
52719
+ function parseBeatDuration(body) {
52720
+ const sub = body.match(DURATION_SUBSECTION_RE);
52721
+ if (!sub) return void 0;
52722
+ const valueMatch = sub[1].match(DURATION_VALUE_RE);
52723
+ if (!valueMatch) return void 0;
52724
+ const n = parseFloat(valueMatch[1]);
52725
+ return Number.isFinite(n) && n > 0 ? n : void 0;
52726
+ }
52727
+ var HEADING_RE, BEAT_PREFIX_RE, DURATION_SUBSECTION_RE, DURATION_VALUE_RE;
52728
+ var init_storyboard_parse = __esm({
52729
+ "src/commands/_shared/storyboard-parse.ts"() {
52730
+ "use strict";
52731
+ HEADING_RE = /^##\s+(.+?)\s*$/gm;
52732
+ BEAT_PREFIX_RE = /^Beat\s+(.+?)\s+(?:—|:|-)\s+(.+)$/i;
52733
+ DURATION_SUBSECTION_RE = /###\s+Beat\s+duration\s*\n([\s\S]*?)(?=\n###\s|\n##\s|$)/i;
52734
+ DURATION_VALUE_RE = /(-?\d+(?:\.\d+)?)\s*(?:s|sec|seconds?)?/i;
52735
+ }
52736
+ });
52737
+
52738
+ // src/commands/_shared/compose-scenes-skills.ts
52739
+ var compose_scenes_skills_exports = {};
52740
+ __export(compose_scenes_skills_exports, {
52741
+ ComposeBeatError: () => ComposeBeatError,
52742
+ buildSystemPrompt: () => buildSystemPrompt,
52743
+ buildUserPrompt: () => buildUserPrompt,
52744
+ composeBeatHtml: () => composeBeatHtml,
52745
+ composeBeatWithRetry: () => composeBeatWithRetry,
52746
+ computeCacheKey: () => computeCacheKey,
52747
+ defaultCacheDir: () => defaultCacheDir,
52748
+ executeComposeScenesWithSkills: () => executeComposeScenesWithSkills,
52749
+ extractHtml: () => extractHtml,
52750
+ formatLintFeedback: () => formatLintFeedback,
52751
+ lintBeatHtml: () => lintBeatHtml
52752
+ });
52753
+ import Anthropic from "@anthropic-ai/sdk";
52754
+ import { createHash as createHash3 } from "node:crypto";
52755
+ import { existsSync as existsSync28, mkdirSync } from "node:fs";
52756
+ import { mkdir as mkdir19, readFile as readFile35, writeFile as writeFile31 } from "node:fs/promises";
52757
+ import { homedir as homedir4 } from "node:os";
52758
+ import { dirname as dirname20, join as join8, resolve as resolve42 } from "node:path";
52759
+ import {
52760
+ runHyperframeLint as runHyperframeLint2
52761
+ } from "@hyperframes/producer";
52762
+ function buildSystemPrompt(ctx) {
52763
+ return `You are a Hyperframes composition author. The skill content below
52764
+ defines the framework rules, motion principles, and quality standards.
52765
+ Read it thoroughly before writing any HTML.
52766
+
52767
+ ${ctx.skillBundle.content}
52768
+
52769
+ === DESIGN.md (project-specific visual identity \u2014 HARD-GATE, every decision must trace back) ===
52770
+
52771
+ ${ctx.designMd}`;
52772
+ }
52773
+ function buildUserPrompt(ctx) {
52774
+ const compositionId = `scene-${ctx.beat.id}`;
52775
+ const baseRequirements = `Build the Hyperframes sub-composition HTML for this beat. The composition
52776
+ will be loaded into a root index.html via
52777
+ \`data-composition-src="compositions/${compositionId}.html"\`.
52778
+
52779
+ Requirements (non-negotiable):
52780
+ - Use the \`<template>\` wrapper (this is a sub-composition, not standalone)
52781
+ - Composition id: \`${compositionId}\`
52782
+ - One paused GSAP timeline registered on \`window.__timelines["${compositionId}"]\`
52783
+ - All timed elements have \`class="clip"\` and \`data-start\`, \`data-duration\`, \`data-track-index\`
52784
+ - No \`Math.random()\`, \`Date.now()\`, \`repeat: -1\`, or \`<br>\` in content
52785
+ - Layout-before-animation: position elements at hero-frame state in CSS, animate FROM
52786
+ - No exit animations (transitions handle scene exits, except the final beat)
52787
+ - Strictly follow DESIGN.md palette, typography, motion signature
52788
+
52789
+ === Storyboard \u2014 global direction ===
52790
+
52791
+ ${ctx.storyboardGlobal || "(no global direction)"}
52792
+
52793
+ === Beat to build ===
52794
+
52795
+ ## ${ctx.beat.heading}
52796
+
52797
+ ${ctx.beat.body}
52798
+
52799
+ === Output format ===
52800
+
52801
+ Return ONE complete HTML file in a single \`\`\`html\`\`\` fenced code block.
52802
+ No prose, no explanations, no commentary outside the code block. Just the HTML.`;
52803
+ if (ctx.retryFeedback && ctx.retryFeedback.trim().length > 0) {
52804
+ return `${baseRequirements}
52805
+
52806
+ === Previous attempt failed lint with the following findings \u2014 fix them ===
52807
+
52808
+ ${ctx.retryFeedback.trim()}`;
52809
+ }
52810
+ return baseRequirements;
52811
+ }
52812
+ function computeCacheKey(parts) {
52813
+ return createHash3("sha256").update(parts.model).update("\u241E").update(parts.systemPrompt).update("\u241E").update(parts.userPrompt).digest("hex");
52814
+ }
52815
+ function defaultCacheDir() {
52816
+ return join8(homedir4(), ".vibeframe", "cache", "compose-scenes");
52817
+ }
52818
+ function extractHtml(responseText) {
52819
+ const fenced = responseText.match(/```html\s*\n([\s\S]*?)\n```/);
52820
+ if (fenced) return fenced[1].trim();
52821
+ const trimmed = responseText.trim();
52822
+ if (trimmed.startsWith("<")) return trimmed;
52823
+ throw new ComposeBeatError(
52824
+ "no-html-in-response",
52825
+ `Could not extract HTML from response. First 200 chars: ${trimmed.slice(0, 200)}`
52826
+ );
52827
+ }
52828
+ async function composeBeatHtml(ctx, overrides) {
52829
+ const effort = ctx.effort ?? "medium";
52830
+ const settings = MODEL_SETTINGS[effort];
52831
+ const systemPrompt = buildSystemPrompt(ctx);
52832
+ const userPrompt = buildUserPrompt(ctx);
52833
+ const cacheKey = computeCacheKey({
52834
+ systemPrompt,
52835
+ userPrompt,
52836
+ model: settings.model
52837
+ });
52838
+ const cacheDir = ctx.cacheDir ?? defaultCacheDir();
52839
+ const cachePath = join8(cacheDir, `${cacheKey}.html`);
52840
+ if (existsSync28(cachePath)) {
52841
+ const html2 = await readFile35(cachePath, "utf-8");
52842
+ return { html: html2, cached: true, cacheKey, model: settings.model };
52843
+ }
52844
+ if (!ctx.apiKey) {
52845
+ throw new ComposeBeatError(
52846
+ "missing-api-key",
52847
+ "ANTHROPIC_API_KEY required for compose-scenes-with-skills (set it in env or .env)."
52848
+ );
52849
+ }
52850
+ const client = overrides?.client ?? new Anthropic({ apiKey: ctx.apiKey });
52851
+ const now = overrides?.now ?? (() => Date.now());
52852
+ const t0 = now();
52853
+ let response;
52854
+ try {
52855
+ response = await client.messages.create({
52856
+ model: settings.model,
52857
+ max_tokens: settings.maxTokens,
52858
+ system: systemPrompt,
52859
+ messages: [{ role: "user", content: userPrompt }]
52860
+ });
52861
+ } catch (err) {
52862
+ throw new ComposeBeatError(
52863
+ "api-call-failed",
52864
+ `Anthropic API call failed: ${err instanceof Error ? err.message : String(err)}`,
52865
+ err
52866
+ );
52867
+ }
52868
+ const latencyMs = now() - t0;
52869
+ if (!("content" in response) || !Array.isArray(response.content)) {
52870
+ throw new ComposeBeatError("unexpected-response-shape", "Anthropic response missing `content` array.");
52871
+ }
52872
+ const textBlock = response.content.find((b) => b.type === "text");
52873
+ if (!textBlock || textBlock.type !== "text") {
52874
+ throw new ComposeBeatError("no-text-block", "Anthropic response had no text block.");
52875
+ }
52876
+ const html = extractHtml(textBlock.text);
52877
+ try {
52878
+ mkdirSync(cacheDir, { recursive: true });
52879
+ await writeFile31(cachePath, html, "utf-8");
52880
+ } catch {
52881
+ }
52882
+ const inputTokens = response.usage?.input_tokens ?? 0;
52883
+ const outputTokens = response.usage?.output_tokens ?? 0;
52884
+ const costUsd = inputTokens / 1e6 * settings.costPerMTokIn + outputTokens / 1e6 * settings.costPerMTokOut;
52885
+ return {
52886
+ html,
52887
+ cached: false,
52888
+ cacheKey,
52889
+ inputTokens,
52890
+ outputTokens,
52891
+ costUsd: Number(costUsd.toFixed(4)),
52892
+ latencyMs,
52893
+ model: settings.model
52894
+ };
52895
+ }
52896
+ function lintBeatHtml(html, beatId) {
52897
+ const prepared = {
52898
+ html,
52899
+ entryFile: `compositions/scene-${beatId}.html`,
52900
+ source: "projectDir"
52901
+ };
52902
+ const raw = runHyperframeLint2(prepared);
52903
+ const findings = filterSubCompFalsePositives(raw.findings, true);
52904
+ return {
52905
+ errorCount: findings.filter((f) => f.severity === "error").length,
52906
+ warningCount: findings.filter((f) => f.severity === "warning").length,
52907
+ findings
52908
+ };
52909
+ }
52910
+ function formatLintFeedback(findings) {
52911
+ const errors = findings.filter((f) => f.severity === "error");
52912
+ if (errors.length === 0) return "";
52913
+ return errors.map((f) => {
52914
+ const fix = f.fixHint ? `
52915
+ Fix hint: ${f.fixHint}` : "";
52916
+ return `ERROR [${f.code}] ${f.message}${fix}`;
52917
+ }).join("\n");
52918
+ }
52919
+ async function composeBeatWithRetry(ctx, overrides) {
52920
+ const first = await composeBeatHtml(ctx, overrides);
52921
+ const lint1 = lintBeatHtml(first.html, ctx.beat.id);
52922
+ if (lint1.errorCount === 0) {
52923
+ return { ...first, lintAttempts: 1, lint: lint1 };
52924
+ }
52925
+ const feedback = formatLintFeedback(lint1.findings);
52926
+ const second = await composeBeatHtml({ ...ctx, retryFeedback: feedback }, overrides);
52927
+ const lint2 = lintBeatHtml(second.html, ctx.beat.id);
52928
+ if (lint2.errorCount === 0) {
52929
+ return { ...second, lintAttempts: 2, lint: lint2 };
52930
+ }
52931
+ throw new ComposeBeatError(
52932
+ "lint-failed-after-retry",
52933
+ `Beat "${ctx.beat.id}" failed lint after retry. Final findings:
52934
+ ${formatLintFeedback(lint2.findings)}`
52935
+ );
52936
+ }
52937
+ async function executeComposeScenesWithSkills(params, outputDir, overrides) {
52938
+ const projectRoot = params.project ? resolve42(outputDir, params.project) : resolve42(outputDir);
52939
+ const designPath = resolve42(projectRoot, params.design ?? "DESIGN.md");
52940
+ const storyboardPath = resolve42(projectRoot, params.storyboard ?? "STORYBOARD.md");
52941
+ if (!existsSync28(designPath)) {
52942
+ return { success: false, error: `DESIGN.md not found at ${designPath}` };
52943
+ }
52944
+ if (!existsSync28(storyboardPath)) {
52945
+ return { success: false, error: `STORYBOARD.md not found at ${storyboardPath}` };
52946
+ }
52947
+ const designMd = await readFile35(designPath, "utf-8");
52948
+ const storyboardMd = await readFile35(storyboardPath, "utf-8");
52949
+ const { global: storyboardGlobal, beats } = parseStoryboard(storyboardMd);
52950
+ if (beats.length === 0) {
52951
+ return {
52952
+ success: false,
52953
+ error: `STORYBOARD.md at ${storyboardPath} contains no \`## Beat \u2026\` headings.`
52954
+ };
52955
+ }
52956
+ const skillBundleLoaded = loadHyperframesSkillBundle();
52957
+ const skillBundle = { content: skillBundleLoaded.content, hash: skillBundleLoaded.hash };
52958
+ const apiKey = process.env.ANTHROPIC_API_KEY ?? "";
52959
+ const compositionsDir = join8(projectRoot, "compositions");
52960
+ await mkdir19(compositionsDir, { recursive: true });
52961
+ const onProgress = params.onProgress ?? (() => {
52962
+ });
52963
+ const totalBeats = beats.length;
52964
+ const tasks = beats.map(async (beat, beatIndex) => {
52965
+ onProgress({ type: "beat-start", beatId: beat.id, beatIndex, totalBeats });
52966
+ try {
52967
+ const result = await composeBeatWithRetry(
52968
+ {
52969
+ beat,
52970
+ designMd,
52971
+ storyboardGlobal,
52972
+ skillBundle,
52973
+ apiKey,
52974
+ effort: params.effort,
52975
+ cacheDir: params.cacheDir
52976
+ },
52977
+ overrides
52978
+ );
52979
+ const compositionPath = join8(compositionsDir, `scene-${beat.id}.html`);
52980
+ await mkdir19(dirname20(compositionPath), { recursive: true });
52981
+ await writeFile31(compositionPath, result.html, "utf-8");
52982
+ if (result.cached) {
52983
+ onProgress({
52984
+ type: "beat-cached",
52985
+ beatId: beat.id,
52986
+ beatIndex,
52987
+ totalBeats,
52988
+ lintAttempts: result.lintAttempts
52989
+ });
52990
+ } else {
52991
+ onProgress({
52992
+ type: "beat-fresh",
52993
+ beatId: beat.id,
52994
+ beatIndex,
52995
+ totalBeats,
52996
+ lintAttempts: result.lintAttempts,
52997
+ costUsd: result.costUsd,
52998
+ latencyMs: result.latencyMs
52999
+ });
53000
+ }
53001
+ return { ok: true, beatId: beat.id, beatIndex, path: compositionPath, result };
53002
+ } catch (err) {
53003
+ const message = err instanceof Error ? err.message : String(err);
53004
+ onProgress({ type: "beat-failed", beatId: beat.id, beatIndex, totalBeats, error: message });
53005
+ return { ok: false, beatId: beat.id, beatIndex, error: message };
53006
+ }
53007
+ });
53008
+ const outcomes = await Promise.all(tasks);
53009
+ const written = [];
53010
+ const failures = [];
53011
+ let totalCostUsd = 0;
53012
+ let totalTokensIn = 0;
53013
+ let totalTokensOut = 0;
53014
+ let cacheHits = 0;
53015
+ for (const outcome of outcomes) {
53016
+ if (!outcome.ok) {
53017
+ failures.push({ beatId: outcome.beatId, error: outcome.error });
53018
+ continue;
53019
+ }
53020
+ const r = outcome.result;
53021
+ written.push({
53022
+ beatId: outcome.beatId,
53023
+ path: outcome.path,
53024
+ cached: r.cached,
53025
+ lintAttempts: r.lintAttempts,
53026
+ costUsd: r.costUsd
53027
+ });
53028
+ if (r.cached) cacheHits++;
53029
+ if (r.costUsd) totalCostUsd += r.costUsd;
53030
+ if (r.inputTokens) totalTokensIn += r.inputTokens;
53031
+ if (r.outputTokens) totalTokensOut += r.outputTokens;
53032
+ }
53033
+ const aggregateData = {
53034
+ beats: beats.length,
53035
+ written,
53036
+ totalCostUsd: Number(totalCostUsd.toFixed(4)),
53037
+ totalTokensIn,
53038
+ totalTokensOut,
53039
+ cacheHits
53040
+ };
53041
+ if (failures.length > 0) {
53042
+ return {
53043
+ success: false,
53044
+ outputPath: projectRoot,
53045
+ error: failures.length === 1 ? `compose-scenes-with-skills failed at beat "${failures[0].beatId}": ${failures[0].error}` : `compose-scenes-with-skills failed at ${failures.length} beats:
53046
+ ${failures.map((f) => ` - ${f.beatId}: ${f.error}`).join("\n")}`,
53047
+ data: aggregateData
53048
+ };
53049
+ }
53050
+ return {
53051
+ success: true,
53052
+ outputPath: projectRoot,
53053
+ data: aggregateData
53054
+ };
53055
+ }
53056
+ var MODEL_SETTINGS, ComposeBeatError;
53057
+ var init_compose_scenes_skills = __esm({
53058
+ "src/commands/_shared/compose-scenes-skills.ts"() {
53059
+ "use strict";
53060
+ init_bundle();
53061
+ init_scene_lint();
53062
+ init_storyboard_parse();
53063
+ MODEL_SETTINGS = {
53064
+ // Sonnet 4.6 — pre-flight validated default. $3/$15 per MTok.
53065
+ low: { model: "claude-sonnet-4-6", maxTokens: 4e3, costPerMTokIn: 3, costPerMTokOut: 15 },
53066
+ medium: { model: "claude-sonnet-4-6", maxTokens: 6e3, costPerMTokIn: 3, costPerMTokOut: 15 },
53067
+ high: { model: "claude-sonnet-4-6", maxTokens: 8e3, costPerMTokIn: 3, costPerMTokOut: 15 }
53068
+ };
53069
+ ComposeBeatError = class extends Error {
53070
+ constructor(code, message, cause) {
53071
+ super(message);
53072
+ this.code = code;
53073
+ this.cause = cause;
53074
+ this.name = "ComposeBeatError";
53075
+ }
53076
+ };
53077
+ }
53078
+ });
53079
+
51442
53080
  // src/agent/adapters/openai.ts
51443
53081
  var openai_exports = {};
51444
53082
  __export(openai_exports, {
@@ -51540,7 +53178,7 @@ var claude_exports = {};
51540
53178
  __export(claude_exports, {
51541
53179
  ClaudeAdapter: () => ClaudeAdapter
51542
53180
  });
51543
- import Anthropic from "@anthropic-ai/sdk";
53181
+ import Anthropic2 from "@anthropic-ai/sdk";
51544
53182
  var ClaudeAdapter;
51545
53183
  var init_claude2 = __esm({
51546
53184
  "src/agent/adapters/claude.ts"() {
@@ -51550,7 +53188,7 @@ var init_claude2 = __esm({
51550
53188
  client = null;
51551
53189
  model = "claude-sonnet-4-6";
51552
53190
  async initialize(apiKey) {
51553
- this.client = new Anthropic({ apiKey });
53191
+ this.client = new Anthropic2({ apiKey });
51554
53192
  }
51555
53193
  isInitialized() {
51556
53194
  return this.client !== null;
@@ -52169,14 +53807,14 @@ var project_exports = {};
52169
53807
  __export(project_exports, {
52170
53808
  registerProjectTools: () => registerProjectTools
52171
53809
  });
52172
- import { readFile as readFile36, writeFile as writeFile32, stat as stat7 } from "node:fs/promises";
52173
- import { resolve as resolve43 } from "node:path";
53810
+ import { readFile as readFile37, writeFile as writeFile33, stat as stat7 } from "node:fs/promises";
53811
+ import { resolve as resolve44 } from "node:path";
52174
53812
  async function resolveProjectPath3(inputPath, cwd) {
52175
- const filePath = resolve43(cwd, inputPath);
53813
+ const filePath = resolve44(cwd, inputPath);
52176
53814
  try {
52177
53815
  const stats = await stat7(filePath);
52178
53816
  if (stats.isDirectory()) {
52179
- return resolve43(filePath, "project.vibe.json");
53817
+ return resolve44(filePath, "project.vibe.json");
52180
53818
  }
52181
53819
  } catch {
52182
53820
  }
@@ -52299,9 +53937,9 @@ var init_project2 = __esm({
52299
53937
  const project = new Project(name);
52300
53938
  project.setAspectRatio(aspectRatio);
52301
53939
  project.setFrameRate(fps);
52302
- const outputPath = resolve43(context.workingDirectory, output);
53940
+ const outputPath = resolve44(context.workingDirectory, output);
52303
53941
  const data = JSON.stringify(project.toJSON(), null, 2);
52304
- await writeFile32(outputPath, data, "utf-8");
53942
+ await writeFile33(outputPath, data, "utf-8");
52305
53943
  context.projectPath = outputPath;
52306
53944
  return {
52307
53945
  toolCallId: "",
@@ -52324,7 +53962,7 @@ Frame Rate: ${fps} fps`
52324
53962
  const path2 = args.path || args.project;
52325
53963
  try {
52326
53964
  const filePath = await resolveProjectPath3(path2, context.workingDirectory);
52327
- const content = await readFile36(filePath, "utf-8");
53965
+ const content = await readFile37(filePath, "utf-8");
52328
53966
  const data = JSON.parse(content);
52329
53967
  const project = Project.fromJSON(data);
52330
53968
  const summary = project.getSummary();
@@ -52359,7 +53997,7 @@ Frame Rate: ${fps} fps`
52359
53997
  const updates = [];
52360
53998
  try {
52361
53999
  const filePath = await resolveProjectPath3(path2, context.workingDirectory);
52362
- const content = await readFile36(filePath, "utf-8");
54000
+ const content = await readFile37(filePath, "utf-8");
52363
54001
  const data = JSON.parse(content);
52364
54002
  const project = Project.fromJSON(data);
52365
54003
  if (args.name) {
@@ -52374,7 +54012,7 @@ Frame Rate: ${fps} fps`
52374
54012
  project.setFrameRate(args.fps);
52375
54013
  updates.push(`Frame Rate: ${args.fps} fps`);
52376
54014
  }
52377
- await writeFile32(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
54015
+ await writeFile33(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
52378
54016
  return {
52379
54017
  toolCallId: "",
52380
54018
  success: true,
@@ -52394,7 +54032,7 @@ ${updates.join("\n")}`
52394
54032
  const path2 = args.path || args.project;
52395
54033
  try {
52396
54034
  const filePath = await resolveProjectPath3(path2, context.workingDirectory);
52397
- const content = await readFile36(filePath, "utf-8");
54035
+ const content = await readFile37(filePath, "utf-8");
52398
54036
  const data = JSON.parse(content);
52399
54037
  const project = Project.fromJSON(data);
52400
54038
  context.projectPath = filePath;
@@ -52428,10 +54066,10 @@ Duration: ${summary.duration.toFixed(1)}s`
52428
54066
  }
52429
54067
  try {
52430
54068
  const filePath = await resolveProjectPath3(path2, context.workingDirectory);
52431
- const content = await readFile36(filePath, "utf-8");
54069
+ const content = await readFile37(filePath, "utf-8");
52432
54070
  const data = JSON.parse(content);
52433
54071
  const project = Project.fromJSON(data);
52434
- await writeFile32(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
54072
+ await writeFile33(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
52435
54073
  return {
52436
54074
  toolCallId: "",
52437
54075
  success: true,
@@ -52454,8 +54092,8 @@ var timeline_exports = {};
52454
54092
  __export(timeline_exports, {
52455
54093
  registerTimelineTools: () => registerTimelineTools
52456
54094
  });
52457
- import { readFile as readFile37, writeFile as writeFile33, stat as stat8 } from "node:fs/promises";
52458
- import { resolve as resolve44, basename as basename25, extname as extname15 } from "node:path";
54095
+ import { readFile as readFile38, writeFile as writeFile34, stat as stat8 } from "node:fs/promises";
54096
+ import { resolve as resolve45, basename as basename25, extname as extname15 } from "node:path";
52459
54097
  function detectMediaType4(path2) {
52460
54098
  const ext = extname15(path2).toLowerCase();
52461
54099
  const videoExts = [".mp4", ".mov", ".webm", ".avi", ".mkv"];
@@ -52478,11 +54116,11 @@ async function getMediaDuration2(filePath, mediaType) {
52478
54116
  }
52479
54117
  }
52480
54118
  async function resolveProjectPath4(inputPath, cwd) {
52481
- const filePath = resolve44(cwd, inputPath);
54119
+ const filePath = resolve45(cwd, inputPath);
52482
54120
  try {
52483
54121
  const stats = await stat8(filePath);
52484
54122
  if (stats.isDirectory()) {
52485
- return resolve44(filePath, "project.vibe.json");
54123
+ return resolve45(filePath, "project.vibe.json");
52486
54124
  }
52487
54125
  } catch {
52488
54126
  }
@@ -52490,13 +54128,13 @@ async function resolveProjectPath4(inputPath, cwd) {
52490
54128
  }
52491
54129
  async function loadProject(path2, cwd) {
52492
54130
  const filePath = await resolveProjectPath4(path2, cwd);
52493
- const content = await readFile37(filePath, "utf-8");
54131
+ const content = await readFile38(filePath, "utf-8");
52494
54132
  const data = JSON.parse(content);
52495
54133
  const project = Project.fromJSON(data);
52496
54134
  return { project, filePath };
52497
54135
  }
52498
54136
  async function saveProject(project, filePath) {
52499
- await writeFile33(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
54137
+ await writeFile34(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
52500
54138
  }
52501
54139
  function registerTimelineTools(registry) {
52502
54140
  registry.register(addSourceDef, addSource);
@@ -52801,7 +54439,7 @@ var init_timeline = __esm({
52801
54439
  const mediaPath = args.media;
52802
54440
  try {
52803
54441
  const { project, filePath } = await loadProject(projectPath, context.workingDirectory);
52804
- const absMediaPath = resolve44(context.workingDirectory, mediaPath);
54442
+ const absMediaPath = resolve45(context.workingDirectory, mediaPath);
52805
54443
  const mediaName = args.name || basename25(mediaPath);
52806
54444
  const mediaType = args.type || detectMediaType4(mediaPath);
52807
54445
  const duration = args.duration || await getMediaDuration2(absMediaPath, mediaType);
@@ -53319,8 +54957,8 @@ var filesystem_exports = {};
53319
54957
  __export(filesystem_exports, {
53320
54958
  registerFilesystemTools: () => registerFilesystemTools
53321
54959
  });
53322
- import { readFile as readFile38, writeFile as writeFile34, readdir as readdir5, stat as stat9, access as access8 } from "node:fs/promises";
53323
- import { resolve as resolve45, join as join7 } from "node:path";
54960
+ import { readFile as readFile39, writeFile as writeFile35, readdir as readdir5, stat as stat9, access as access8 } from "node:fs/promises";
54961
+ import { resolve as resolve46, join as join9 } from "node:path";
53324
54962
  function matchPattern(filename, pattern) {
53325
54963
  const regex2 = new RegExp(
53326
54964
  "^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".") + "$",
@@ -53416,14 +55054,14 @@ var init_filesystem = __esm({
53416
55054
  const dirPath = args.path || ".";
53417
55055
  const pattern = args.pattern;
53418
55056
  try {
53419
- const absPath = resolve45(context.workingDirectory, dirPath);
55057
+ const absPath = resolve46(context.workingDirectory, dirPath);
53420
55058
  const entries = await readdir5(absPath, { withFileTypes: true });
53421
55059
  const results = [];
53422
55060
  for (const entry of entries) {
53423
55061
  if (pattern && !matchPattern(entry.name, pattern)) {
53424
55062
  continue;
53425
55063
  }
53426
- const fullPath = join7(absPath, entry.name);
55064
+ const fullPath = join9(absPath, entry.name);
53427
55065
  const stats = await stat9(fullPath);
53428
55066
  if (entry.isDirectory()) {
53429
55067
  results.push(`[DIR] ${entry.name}/`);
@@ -53456,8 +55094,8 @@ ${results.join("\n")}`
53456
55094
  read2 = async (args, context) => {
53457
55095
  const filePath = args.path;
53458
55096
  try {
53459
- const absPath = resolve45(context.workingDirectory, filePath);
53460
- const content = await readFile38(absPath, "utf-8");
55097
+ const absPath = resolve46(context.workingDirectory, filePath);
55098
+ const content = await readFile39(absPath, "utf-8");
53461
55099
  const maxLength = 4e3;
53462
55100
  const truncated = content.length > maxLength;
53463
55101
  const output = truncated ? content.substring(0, maxLength) + "\n... (truncated)" : content;
@@ -53480,8 +55118,8 @@ ${output}`
53480
55118
  const filePath = args.path;
53481
55119
  const content = args.content;
53482
55120
  try {
53483
- const absPath = resolve45(context.workingDirectory, filePath);
53484
- await writeFile34(absPath, content, "utf-8");
55121
+ const absPath = resolve46(context.workingDirectory, filePath);
55122
+ await writeFile35(absPath, content, "utf-8");
53485
55123
  return {
53486
55124
  toolCallId: "",
53487
55125
  success: true,
@@ -53499,7 +55137,7 @@ ${output}`
53499
55137
  exists = async (args, context) => {
53500
55138
  const filePath = args.path;
53501
55139
  try {
53502
- const absPath = resolve45(context.workingDirectory, filePath);
55140
+ const absPath = resolve46(context.workingDirectory, filePath);
53503
55141
  await access8(absPath);
53504
55142
  const stats = await stat9(absPath);
53505
55143
  const type = stats.isDirectory() ? "directory" : "file";
@@ -53524,8 +55162,8 @@ var media_exports = {};
53524
55162
  __export(media_exports, {
53525
55163
  registerMediaTools: () => registerMediaTools
53526
55164
  });
53527
- import { readFile as readFile39, writeFile as writeFile35 } from "node:fs/promises";
53528
- import { resolve as resolve46 } from "node:path";
55165
+ import { readFile as readFile40, writeFile as writeFile36 } from "node:fs/promises";
55166
+ import { resolve as resolve47 } from "node:path";
53529
55167
  function formatTimestamp2(seconds) {
53530
55168
  const mins = Math.floor(seconds / 60);
53531
55169
  const secs = (seconds % 60).toFixed(2);
@@ -53664,7 +55302,7 @@ var init_media = __esm({
53664
55302
  mediaInfo = async (args, context) => {
53665
55303
  const mediaPath = args.path;
53666
55304
  try {
53667
- const absPath = resolve46(context.workingDirectory, mediaPath);
55305
+ const absPath = resolve47(context.workingDirectory, mediaPath);
53668
55306
  const { stdout } = await execSafe("ffprobe", ["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", absPath], { maxBuffer: 10 * 1024 * 1024 });
53669
55307
  const info = JSON.parse(stdout);
53670
55308
  const format = info.format || {};
@@ -53718,7 +55356,7 @@ Audio:`);
53718
55356
  const videoPath = args.video;
53719
55357
  const threshold = args.threshold || 0.3;
53720
55358
  try {
53721
- const absPath = resolve46(context.workingDirectory, videoPath);
55359
+ const absPath = resolve47(context.workingDirectory, videoPath);
53722
55360
  const { stdout, stderr } = await execSafe("ffmpeg", ["-i", absPath, "-filter:v", `select='gt(scene,${threshold})',showinfo`, "-f", "null", "-"], { maxBuffer: 50 * 1024 * 1024 });
53723
55361
  const output = stdout + stderr;
53724
55362
  const scenes = [];
@@ -53734,7 +55372,7 @@ Audio:`);
53734
55372
  return `[${i + 1}] ${formatTimestamp2(s.timestamp)} - ${formatTimestamp2(end)} (${(end - s.timestamp).toFixed(1)}s)`;
53735
55373
  }).join("\n");
53736
55374
  if (args.output) {
53737
- const outputPath = resolve46(context.workingDirectory, args.output);
55375
+ const outputPath = resolve47(context.workingDirectory, args.output);
53738
55376
  const result = {
53739
55377
  source: absPath,
53740
55378
  totalDuration,
@@ -53746,7 +55384,7 @@ Audio:`);
53746
55384
  duration: (i < scenes.length - 1 ? scenes[i + 1].timestamp : totalDuration) - s.timestamp
53747
55385
  }))
53748
55386
  };
53749
- await writeFile35(outputPath, JSON.stringify(result, null, 2), "utf-8");
55387
+ await writeFile36(outputPath, JSON.stringify(result, null, 2), "utf-8");
53750
55388
  }
53751
55389
  return {
53752
55390
  toolCallId: "",
@@ -53768,7 +55406,7 @@ ${sceneList}`
53768
55406
  const noise = args.noise || "-30";
53769
55407
  const duration = args.duration || 0.5;
53770
55408
  try {
53771
- const absPath = resolve46(context.workingDirectory, mediaPath);
55409
+ const absPath = resolve47(context.workingDirectory, mediaPath);
53772
55410
  const { stdout, stderr } = await execSafe("ffmpeg", ["-i", absPath, "-af", `silencedetect=noise=${noise}dB:d=${duration}`, "-f", "null", "-"], { maxBuffer: 50 * 1024 * 1024 });
53773
55411
  const output = stdout + stderr;
53774
55412
  const silences = [];
@@ -53794,8 +55432,8 @@ ${sceneList}`
53794
55432
  (s, idx) => `[${idx + 1}] ${formatTimestamp2(s.start)} - ${formatTimestamp2(s.end)} (${s.duration.toFixed(1)}s)`
53795
55433
  ).join("\n");
53796
55434
  if (args.output) {
53797
- const outputPath = resolve46(context.workingDirectory, args.output);
53798
- await writeFile35(
55435
+ const outputPath = resolve47(context.workingDirectory, args.output);
55436
+ await writeFile36(
53799
55437
  outputPath,
53800
55438
  JSON.stringify({ source: absPath, silences }, null, 2),
53801
55439
  "utf-8"
@@ -53819,7 +55457,7 @@ ${silenceList}` : "No silence periods detected."
53819
55457
  detectBeats = async (args, context) => {
53820
55458
  const audioPath = args.audio;
53821
55459
  try {
53822
- const absPath = resolve46(context.workingDirectory, audioPath);
55460
+ const absPath = resolve47(context.workingDirectory, audioPath);
53823
55461
  const totalDuration = await ffprobeDuration(absPath);
53824
55462
  const estimatedBPM = 120;
53825
55463
  const beatInterval = 60 / estimatedBPM;
@@ -53831,8 +55469,8 @@ ${silenceList}` : "No silence periods detected."
53831
55469
  (t, i) => `[${i + 1}] ${formatTimestamp2(t)}`
53832
55470
  ).join("\n");
53833
55471
  if (args.output) {
53834
- const outputPath = resolve46(context.workingDirectory, args.output);
53835
- await writeFile35(
55472
+ const outputPath = resolve47(context.workingDirectory, args.output);
55473
+ await writeFile36(
53836
55474
  outputPath,
53837
55475
  JSON.stringify({ source: absPath, beatCount: beats.length, beats }, null, 2),
53838
55476
  "utf-8"
@@ -53867,8 +55505,8 @@ ${beatList}${beats.length > 20 ? `
53867
55505
  error: "OpenAI API key required for transcription. Set OPENAI_API_KEY or configure via 'vibe setup'."
53868
55506
  };
53869
55507
  }
53870
- const absPath = resolve46(context.workingDirectory, audioPath);
53871
- const audioBuffer = await readFile39(absPath);
55508
+ const absPath = resolve47(context.workingDirectory, audioPath);
55509
+ const audioBuffer = await readFile40(absPath);
53872
55510
  const { WhisperProvider: WhisperProvider2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
53873
55511
  const whisper = new WhisperProvider2();
53874
55512
  await whisper.initialize({ apiKey });
@@ -53896,11 +55534,11 @@ ${result.fullText}`;
53896
55534
  }
53897
55535
  }
53898
55536
  if (args.output) {
53899
- const outputPath = resolve46(context.workingDirectory, args.output);
55537
+ const outputPath = resolve47(context.workingDirectory, args.output);
53900
55538
  const { detectFormat: detectFormat2, formatTranscript: formatTranscript2 } = await Promise.resolve().then(() => (init_subtitle(), subtitle_exports));
53901
55539
  const format = detectFormat2(args.output, void 0);
53902
55540
  const content = formatTranscript2(result, format);
53903
- await writeFile35(outputPath, content, "utf-8");
55541
+ await writeFile36(outputPath, content, "utf-8");
53904
55542
  output += `
53905
55543
 
53906
55544
  Saved to: ${args.output}`;
@@ -53999,10 +55637,10 @@ Saved to: ${args.output}`;
53999
55637
  const inputPath = args.input;
54000
55638
  const quality = args.quality || "medium";
54001
55639
  try {
54002
- const absInput = resolve46(context.workingDirectory, inputPath);
55640
+ const absInput = resolve47(context.workingDirectory, inputPath);
54003
55641
  const ext = inputPath.split(".").pop() || "mp4";
54004
55642
  const baseName = inputPath.replace(/\.[^/.]+$/, "");
54005
- const outputPath = args.output ? resolve46(context.workingDirectory, args.output) : resolve46(context.workingDirectory, `${baseName}-compressed.${ext}`);
55643
+ const outputPath = args.output ? resolve47(context.workingDirectory, args.output) : resolve47(context.workingDirectory, `${baseName}-compressed.${ext}`);
54006
55644
  const crfValues = {
54007
55645
  low: 28,
54008
55646
  medium: 23,
@@ -54010,8 +55648,8 @@ Saved to: ${args.output}`;
54010
55648
  };
54011
55649
  const crf = crfValues[quality] || 23;
54012
55650
  await execSafe("ffmpeg", ["-i", absInput, "-c:v", "libx264", "-crf", String(crf), "-preset", "medium", "-c:a", "aac", "-b:a", "128k", outputPath, "-y"], { maxBuffer: 50 * 1024 * 1024 });
54013
- const inputStats = await readFile39(absInput);
54014
- const outputStats = await readFile39(outputPath);
55651
+ const inputStats = await readFile40(absInput);
55652
+ const outputStats = await readFile40(outputPath);
54015
55653
  const inputSize = inputStats.length;
54016
55654
  const outputSize = outputStats.length;
54017
55655
  const reduction = ((inputSize - outputSize) / inputSize * 100).toFixed(1);
@@ -54036,8 +55674,8 @@ Size: ${formatSize2(inputSize)} \u2192 ${formatSize2(outputSize)} (${reduction}%
54036
55674
  const codec = args.codec;
54037
55675
  const audioCodec = args.audioCodec;
54038
55676
  try {
54039
- const absInput = resolve46(context.workingDirectory, inputPath);
54040
- const absOutput = resolve46(context.workingDirectory, outputPath);
55677
+ const absInput = resolve47(context.workingDirectory, inputPath);
55678
+ const absOutput = resolve47(context.workingDirectory, outputPath);
54041
55679
  let videoCodecName = "copy";
54042
55680
  let audioCodecName = "copy";
54043
55681
  if (codec) {
@@ -54085,19 +55723,19 @@ Size: ${formatSize2(inputSize)} \u2192 ${formatSize2(outputSize)} (${reduction}%
54085
55723
  };
54086
55724
  }
54087
55725
  try {
54088
- const absOutput = resolve46(context.workingDirectory, outputPath);
55726
+ const absOutput = resolve47(context.workingDirectory, outputPath);
54089
55727
  if (reencode) {
54090
55728
  const ffmpegArgs = [];
54091
55729
  for (const i of inputs) {
54092
- ffmpegArgs.push("-i", resolve46(context.workingDirectory, i));
55730
+ ffmpegArgs.push("-i", resolve47(context.workingDirectory, i));
54093
55731
  }
54094
55732
  const filterComplex = inputs.map((_, i) => `[${i}:v][${i}:a]`).join("");
54095
55733
  ffmpegArgs.push("-filter_complex", `${filterComplex}concat=n=${inputs.length}:v=1:a=1[outv][outa]`, "-map", "[outv]", "-map", "[outa]", absOutput, "-y");
54096
55734
  await execSafe("ffmpeg", ffmpegArgs, { maxBuffer: 100 * 1024 * 1024 });
54097
55735
  } else {
54098
- const tempList = resolve46(context.workingDirectory, `concat-list-${Date.now()}.txt`);
54099
- const listContent = inputs.map((i) => `file '${resolve46(context.workingDirectory, i)}'`).join("\n");
54100
- await writeFile35(tempList, listContent, "utf-8");
55736
+ const tempList = resolve47(context.workingDirectory, `concat-list-${Date.now()}.txt`);
55737
+ const listContent = inputs.map((i) => `file '${resolve47(context.workingDirectory, i)}'`).join("\n");
55738
+ await writeFile36(tempList, listContent, "utf-8");
54101
55739
  await execSafe("ffmpeg", ["-f", "concat", "-safe", "0", "-i", tempList, "-c", "copy", absOutput, "-y"], { maxBuffer: 100 * 1024 * 1024 });
54102
55740
  const { unlink: unlink3 } = await import("node:fs/promises");
54103
55741
  await unlink3(tempList).catch(() => {
@@ -54121,8 +55759,8 @@ Size: ${formatSize2(inputSize)} \u2192 ${formatSize2(outputSize)} (${reduction}%
54121
55759
  });
54122
55760
 
54123
55761
  // src/agent/tools/ai-generation.ts
54124
- import { writeFile as writeFile36, readFile as readFile40 } from "node:fs/promises";
54125
- import { resolve as resolve47 } from "node:path";
55762
+ import { writeFile as writeFile37, readFile as readFile41 } from "node:fs/promises";
55763
+ import { resolve as resolve48 } from "node:path";
54126
55764
  function getTimestamp() {
54127
55765
  return Date.now().toString();
54128
55766
  }
@@ -54391,7 +56029,7 @@ var init_ai_generation = __esm({
54391
56029
  error: `API key required for ${provider}. Configure via 'vibe setup'.`
54392
56030
  };
54393
56031
  }
54394
- const outputPath = resolve47(context.workingDirectory, output);
56032
+ const outputPath = resolve48(context.workingDirectory, output);
54395
56033
  if (provider === "dalle" || provider === "openai") {
54396
56034
  const { OpenAIImageProvider: OpenAIImageProvider2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
54397
56035
  const openaiImage = new OpenAIImageProvider2();
@@ -54422,7 +56060,7 @@ var init_ai_generation = __esm({
54422
56060
  error: "Image generated but no URL or base64 data returned"
54423
56061
  };
54424
56062
  }
54425
- await writeFile36(outputPath, buffer);
56063
+ await writeFile37(outputPath, buffer);
54426
56064
  } else if (provider === "gemini") {
54427
56065
  const { GeminiProvider: GeminiProvider2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
54428
56066
  const gemini = new GeminiProvider2();
@@ -54439,7 +56077,7 @@ var init_ai_generation = __esm({
54439
56077
  const image = result.images[0];
54440
56078
  if (image.base64) {
54441
56079
  const buffer = Buffer.from(image.base64, "base64");
54442
- await writeFile36(outputPath, buffer);
56080
+ await writeFile37(outputPath, buffer);
54443
56081
  }
54444
56082
  } else if (provider === "grok") {
54445
56083
  const { GrokProvider: GrokProvider2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
@@ -54458,10 +56096,10 @@ var init_ai_generation = __esm({
54458
56096
  if (image.url) {
54459
56097
  const response = await fetch(image.url);
54460
56098
  const buffer = Buffer.from(await response.arrayBuffer());
54461
- await writeFile36(outputPath, buffer);
56099
+ await writeFile37(outputPath, buffer);
54462
56100
  } else if (image.base64) {
54463
56101
  const buffer = Buffer.from(image.base64, "base64");
54464
- await writeFile36(outputPath, buffer);
56102
+ await writeFile37(outputPath, buffer);
54465
56103
  }
54466
56104
  }
54467
56105
  return {
@@ -54497,8 +56135,8 @@ Size: ${size}`
54497
56135
  };
54498
56136
  }
54499
56137
  async function prepareReferenceImage(imgPath) {
54500
- const absImagePath = resolve47(context.workingDirectory, imgPath);
54501
- const imageBuffer = await readFile40(absImagePath);
56138
+ const absImagePath = resolve48(context.workingDirectory, imgPath);
56139
+ const imageBuffer = await readFile41(absImagePath);
54502
56140
  const base64 = imageBuffer.toString("base64");
54503
56141
  const ext = imgPath.split(".").pop()?.toLowerCase() || "png";
54504
56142
  const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : `image/${ext}`;
@@ -54552,9 +56190,9 @@ Size: ${size}`
54552
56190
  };
54553
56191
  }
54554
56192
  if (finalResult.videoUrl) {
54555
- const outputPath = resolve47(context.workingDirectory, output);
56193
+ const outputPath = resolve48(context.workingDirectory, output);
54556
56194
  const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
54557
- await writeFile36(outputPath, buffer);
56195
+ await writeFile37(outputPath, buffer);
54558
56196
  }
54559
56197
  }
54560
56198
  return {
@@ -54613,9 +56251,9 @@ Duration: ${duration}s`
54613
56251
  };
54614
56252
  }
54615
56253
  if (finalResult.videoUrl) {
54616
- const outputPath = resolve47(context.workingDirectory, output);
56254
+ const outputPath = resolve48(context.workingDirectory, output);
54617
56255
  const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
54618
- await writeFile36(outputPath, buffer);
56256
+ await writeFile37(outputPath, buffer);
54619
56257
  }
54620
56258
  }
54621
56259
  return {
@@ -54669,9 +56307,9 @@ Duration: ${duration}s`
54669
56307
  };
54670
56308
  }
54671
56309
  if (finalResult.videoUrl) {
54672
- const outputPath = resolve47(context.workingDirectory, output);
56310
+ const outputPath = resolve48(context.workingDirectory, output);
54673
56311
  const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
54674
- await writeFile36(outputPath, buffer);
56312
+ await writeFile37(outputPath, buffer);
54675
56313
  }
54676
56314
  }
54677
56315
  return {
@@ -54754,9 +56392,9 @@ Duration: ${duration}s`
54754
56392
  };
54755
56393
  }
54756
56394
  if (finalResult.videoUrl) {
54757
- const outputPath = resolve47(context.workingDirectory, output);
56395
+ const outputPath = resolve48(context.workingDirectory, output);
54758
56396
  const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
54759
- await writeFile36(outputPath, buffer);
56397
+ await writeFile37(outputPath, buffer);
54760
56398
  }
54761
56399
  }
54762
56400
  return {
@@ -54806,8 +56444,8 @@ Mode: ${mode}`
54806
56444
  error: `TTS generation failed: ${result.error || "No audio generated"}`
54807
56445
  };
54808
56446
  }
54809
- const outputPath = resolve47(context.workingDirectory, output);
54810
- await writeFile36(outputPath, result.audioBuffer);
56447
+ const outputPath = resolve48(context.workingDirectory, output);
56448
+ await writeFile37(outputPath, result.audioBuffer);
54811
56449
  return {
54812
56450
  toolCallId: "",
54813
56451
  success: true,
@@ -54851,8 +56489,8 @@ Text: ${text.substring(0, 100)}${text.length > 100 ? "..." : ""}`
54851
56489
  error: `SFX generation failed: ${result.error || "No audio generated"}`
54852
56490
  };
54853
56491
  }
54854
- const outputPath = resolve47(context.workingDirectory, output);
54855
- await writeFile36(outputPath, result.audioBuffer);
56492
+ const outputPath = resolve48(context.workingDirectory, output);
56493
+ await writeFile37(outputPath, result.audioBuffer);
54856
56494
  return {
54857
56495
  toolCallId: "",
54858
56496
  success: true,
@@ -54900,8 +56538,8 @@ Prompt: ${prompt3}`
54900
56538
  error: `Music generation failed: ${result.error || "No audio generated"}`
54901
56539
  };
54902
56540
  }
54903
- const outputPath = resolve47(context.workingDirectory, output);
54904
- await writeFile36(outputPath, result.audioBuffer);
56541
+ const outputPath = resolve48(context.workingDirectory, output);
56542
+ await writeFile37(outputPath, result.audioBuffer);
54905
56543
  return {
54906
56544
  toolCallId: "",
54907
56545
  success: true,
@@ -54953,10 +56591,10 @@ Provider: ElevenLabs${instrumental ? "\nMode: Instrumental" : ""}`
54953
56591
  }
54954
56592
  }
54955
56593
  if (finalResult.audioUrl) {
54956
- const outputPath = resolve47(context.workingDirectory, output);
56594
+ const outputPath = resolve48(context.workingDirectory, output);
54957
56595
  const response = await fetch(finalResult.audioUrl);
54958
56596
  const buffer = Buffer.from(await response.arrayBuffer());
54959
- await writeFile36(outputPath, buffer);
56597
+ await writeFile37(outputPath, buffer);
54960
56598
  } else {
54961
56599
  return {
54962
56600
  toolCallId: "",
@@ -55011,8 +56649,8 @@ Provider: Replicate`
55011
56649
  error: "Failed to generate storyboard"
55012
56650
  };
55013
56651
  }
55014
- const outputPath = resolve47(context.workingDirectory, output);
55015
- await writeFile36(outputPath, JSON.stringify(result, null, 2), "utf-8");
56652
+ const outputPath = resolve48(context.workingDirectory, output);
56653
+ await writeFile37(outputPath, JSON.stringify(result, null, 2), "utf-8");
55016
56654
  const sanitizedResult = sanitizeAIResult(result);
55017
56655
  const summary = sanitizedResult.map(
55018
56656
  (scene, i) => `Scene ${i + 1}: ${scene.description.substring(0, 60)}...`
@@ -55036,8 +56674,8 @@ ${summary}`
55036
56674
  generateMotion3 = async (args, context) => {
55037
56675
  try {
55038
56676
  const { executeMotion: executeMotion2 } = await Promise.resolve().then(() => (init_ai_motion(), ai_motion_exports));
55039
- const video = args.video ? resolve47(context.workingDirectory, args.video) : void 0;
55040
- const output = args.output ? resolve47(context.workingDirectory, args.output) : void 0;
56677
+ const video = args.video ? resolve48(context.workingDirectory, args.video) : void 0;
56678
+ const output = args.output ? resolve48(context.workingDirectory, args.output) : void 0;
55041
56679
  const result = await executeMotion2({
55042
56680
  description: args.description,
55043
56681
  duration: args.duration,
@@ -55079,8 +56717,8 @@ ${summary}`
55079
56717
  });
55080
56718
 
55081
56719
  // src/agent/tools/ai-pipeline.ts
55082
- import { writeFile as writeFile37, readFile as readFile41 } from "node:fs/promises";
55083
- import { resolve as resolve48 } from "node:path";
56720
+ import { writeFile as writeFile38, readFile as readFile42 } from "node:fs/promises";
56721
+ import { resolve as resolve49 } from "node:path";
55084
56722
  function getTimestamp2() {
55085
56723
  return Date.now().toString();
55086
56724
  }
@@ -55493,7 +57131,7 @@ This tool re-creates videos for failed scenes using image-to-video (if ImgBB key
55493
57131
  };
55494
57132
  scriptToVideoHandler = async (args, context) => {
55495
57133
  const script = args.script;
55496
- const outputDir = args.outputDir ? resolve48(context.workingDirectory, args.outputDir) : resolve48(context.workingDirectory, "script-video-output");
57134
+ const outputDir = args.outputDir ? resolve49(context.workingDirectory, args.outputDir) : resolve49(context.workingDirectory, "script-video-output");
55497
57135
  try {
55498
57136
  const result = await executeScriptToVideo({
55499
57137
  script,
@@ -55565,9 +57203,9 @@ This tool re-creates videos for failed scenes using image-to-video (if ImgBB key
55565
57203
  }
55566
57204
  };
55567
57205
  highlightsHandler = async (args, context) => {
55568
- const media = resolve48(context.workingDirectory, args.media);
55569
- const output = args.output ? resolve48(context.workingDirectory, args.output) : void 0;
55570
- const project = args.project ? resolve48(context.workingDirectory, args.project) : void 0;
57206
+ const media = resolve49(context.workingDirectory, args.media);
57207
+ const output = args.output ? resolve49(context.workingDirectory, args.output) : void 0;
57208
+ const project = args.project ? resolve49(context.workingDirectory, args.project) : void 0;
55571
57209
  try {
55572
57210
  const result = await executeHighlights({
55573
57211
  media,
@@ -55629,8 +57267,8 @@ This tool re-creates videos for failed scenes using image-to-video (if ImgBB key
55629
57267
  }
55630
57268
  };
55631
57269
  autoShortsHandler = async (args, context) => {
55632
- const video = resolve48(context.workingDirectory, args.video);
55633
- const outputDir = args.outputDir ? resolve48(context.workingDirectory, args.outputDir) : void 0;
57270
+ const video = resolve49(context.workingDirectory, args.video);
57271
+ const outputDir = args.outputDir ? resolve49(context.workingDirectory, args.outputDir) : void 0;
55634
57272
  try {
55635
57273
  const result = await executeAutoShorts({
55636
57274
  video,
@@ -55694,7 +57332,7 @@ This tool re-creates videos for failed scenes using image-to-video (if ImgBB key
55694
57332
  geminiVideoHandler = async (args, context) => {
55695
57333
  let source = args.source;
55696
57334
  if (!source.includes("youtube.com") && !source.includes("youtu.be")) {
55697
- source = resolve48(context.workingDirectory, source);
57335
+ source = resolve49(context.workingDirectory, source);
55698
57336
  }
55699
57337
  try {
55700
57338
  const result = await executeGeminiVideo({
@@ -55742,7 +57380,7 @@ This tool re-creates videos for failed scenes using image-to-video (if ImgBB key
55742
57380
  analyzeHandler = async (args, context) => {
55743
57381
  let source = args.source;
55744
57382
  if (!source.startsWith("http://") && !source.startsWith("https://")) {
55745
- source = resolve48(context.workingDirectory, source);
57383
+ source = resolve49(context.workingDirectory, source);
55746
57384
  }
55747
57385
  try {
55748
57386
  const result = await executeAnalyze({
@@ -55844,8 +57482,8 @@ This tool re-creates videos for failed scenes using image-to-video (if ImgBB key
55844
57482
  }
55845
57483
  const imageBuffers = [];
55846
57484
  for (const imagePath of images) {
55847
- const absPath = resolve48(context.workingDirectory, imagePath);
55848
- const buffer = await readFile41(absPath);
57485
+ const absPath = resolve49(context.workingDirectory, imagePath);
57486
+ const buffer = await readFile42(absPath);
55849
57487
  imageBuffers.push(buffer);
55850
57488
  }
55851
57489
  let result;
@@ -55882,14 +57520,14 @@ This tool re-creates videos for failed scenes using image-to-video (if ImgBB key
55882
57520
  };
55883
57521
  }
55884
57522
  const img = result.images[0];
55885
- const outputPath = resolve48(context.workingDirectory, output);
57523
+ const outputPath = resolve49(context.workingDirectory, output);
55886
57524
  if (img.base64) {
55887
57525
  const buffer = Buffer.from(img.base64, "base64");
55888
- await writeFile37(outputPath, buffer);
57526
+ await writeFile38(outputPath, buffer);
55889
57527
  } else if (img.url) {
55890
57528
  const resp = await fetch(img.url);
55891
57529
  const arrayBuf = await resp.arrayBuffer();
55892
- await writeFile37(outputPath, Buffer.from(arrayBuf));
57530
+ await writeFile38(outputPath, Buffer.from(arrayBuf));
55893
57531
  }
55894
57532
  return {
55895
57533
  toolCallId: "",
@@ -55956,8 +57594,8 @@ Failed scenes: ${result.failedScenes.join(", ")}`;
55956
57594
  };
55957
57595
  };
55958
57596
  animatedCaptionHandler = async (args, context) => {
55959
- const videoPath = resolve48(context.workingDirectory, args.videoPath);
55960
- const outputPath = resolve48(context.workingDirectory, args.outputPath);
57597
+ const videoPath = resolve49(context.workingDirectory, args.videoPath);
57598
+ const outputPath = resolve49(context.workingDirectory, args.outputPath);
55961
57599
  try {
55962
57600
  const result = await executeAnimatedCaption({
55963
57601
  videoPath,
@@ -56002,7 +57640,7 @@ Tier: ${result.tier}`
56002
57640
  });
56003
57641
 
56004
57642
  // src/agent/tools/ai-editing.ts
56005
- import { resolve as resolve49 } from "node:path";
57643
+ import { resolve as resolve50 } from "node:path";
56006
57644
  function registerEditingTools(registry) {
56007
57645
  registry.register(textOverlayDef, textOverlayHandler);
56008
57646
  registry.register(reviewDef, reviewHandler);
@@ -56431,10 +58069,10 @@ var init_ai_editing = __esm({
56431
58069
  };
56432
58070
  };
56433
58071
  silenceCutHandler = async (args, context) => {
56434
- const videoPath = resolve49(context.workingDirectory, args.videoPath);
58072
+ const videoPath = resolve50(context.workingDirectory, args.videoPath);
56435
58073
  const ext = videoPath.split(".").pop() || "mp4";
56436
58074
  const name = videoPath.replace(/\.[^.]+$/, "");
56437
- const outputPath = args.outputPath ? resolve49(context.workingDirectory, args.outputPath) : `${name}-cut.${ext}`;
58075
+ const outputPath = args.outputPath ? resolve50(context.workingDirectory, args.outputPath) : `${name}-cut.${ext}`;
56438
58076
  try {
56439
58077
  const result = await executeSilenceCut({
56440
58078
  videoPath,
@@ -56479,10 +58117,10 @@ var init_ai_editing = __esm({
56479
58117
  }
56480
58118
  };
56481
58119
  jumpCutHandler = async (args, context) => {
56482
- const videoPath = resolve49(context.workingDirectory, args.videoPath);
58120
+ const videoPath = resolve50(context.workingDirectory, args.videoPath);
56483
58121
  const ext = videoPath.split(".").pop() || "mp4";
56484
58122
  const name = videoPath.replace(/\.[^.]+$/, "");
56485
- const outputPath = args.outputPath ? resolve49(context.workingDirectory, args.outputPath) : `${name}-jumpcut.${ext}`;
58123
+ const outputPath = args.outputPath ? resolve50(context.workingDirectory, args.outputPath) : `${name}-jumpcut.${ext}`;
56486
58124
  try {
56487
58125
  const result = await executeJumpCut({
56488
58126
  videoPath,
@@ -56530,10 +58168,10 @@ var init_ai_editing = __esm({
56530
58168
  }
56531
58169
  };
56532
58170
  captionHandler = async (args, context) => {
56533
- const videoPath = resolve49(context.workingDirectory, args.videoPath);
58171
+ const videoPath = resolve50(context.workingDirectory, args.videoPath);
56534
58172
  const ext = videoPath.split(".").pop() || "mp4";
56535
58173
  const name = videoPath.replace(/\.[^.]+$/, "");
56536
- const outputPath = args.outputPath ? resolve49(context.workingDirectory, args.outputPath) : `${name}-captioned.${ext}`;
58174
+ const outputPath = args.outputPath ? resolve50(context.workingDirectory, args.outputPath) : `${name}-captioned.${ext}`;
56537
58175
  try {
56538
58176
  const result = await executeCaption({
56539
58177
  videoPath,
@@ -56573,10 +58211,10 @@ var init_ai_editing = __esm({
56573
58211
  }
56574
58212
  };
56575
58213
  noiseReduceHandler = async (args, context) => {
56576
- const inputPath = resolve49(context.workingDirectory, args.inputPath);
58214
+ const inputPath = resolve50(context.workingDirectory, args.inputPath);
56577
58215
  const ext = inputPath.split(".").pop() || "mp4";
56578
58216
  const name = inputPath.replace(/\.[^.]+$/, "");
56579
- const outputPath = args.outputPath ? resolve49(context.workingDirectory, args.outputPath) : `${name}-denoised.${ext}`;
58217
+ const outputPath = args.outputPath ? resolve50(context.workingDirectory, args.outputPath) : `${name}-denoised.${ext}`;
56580
58218
  try {
56581
58219
  const result = await executeNoiseReduce({
56582
58220
  inputPath,
@@ -56610,10 +58248,10 @@ var init_ai_editing = __esm({
56610
58248
  }
56611
58249
  };
56612
58250
  fadeHandler = async (args, context) => {
56613
- const videoPath = resolve49(context.workingDirectory, args.videoPath);
58251
+ const videoPath = resolve50(context.workingDirectory, args.videoPath);
56614
58252
  const ext = videoPath.split(".").pop() || "mp4";
56615
58253
  const name = videoPath.replace(/\.[^.]+$/, "");
56616
- const outputPath = args.outputPath ? resolve49(context.workingDirectory, args.outputPath) : `${name}-faded.${ext}`;
58254
+ const outputPath = args.outputPath ? resolve50(context.workingDirectory, args.outputPath) : `${name}-faded.${ext}`;
56617
58255
  try {
56618
58256
  const result = await executeFade({
56619
58257
  videoPath,
@@ -56651,9 +58289,9 @@ var init_ai_editing = __esm({
56651
58289
  }
56652
58290
  };
56653
58291
  thumbnailBestFrameHandler = async (args, context) => {
56654
- const videoPath = resolve49(context.workingDirectory, args.videoPath);
58292
+ const videoPath = resolve50(context.workingDirectory, args.videoPath);
56655
58293
  const name = videoPath.replace(/\.[^.]+$/, "");
56656
- const outputPath = args.outputPath ? resolve49(context.workingDirectory, args.outputPath) : `${name}-thumbnail.png`;
58294
+ const outputPath = args.outputPath ? resolve50(context.workingDirectory, args.outputPath) : `${name}-thumbnail.png`;
56657
58295
  try {
56658
58296
  const result = await executeThumbnailBestFrame({
56659
58297
  videoPath,
@@ -56688,11 +58326,11 @@ var init_ai_editing = __esm({
56688
58326
  }
56689
58327
  };
56690
58328
  translateSrtHandler = async (args, context) => {
56691
- const srtPath = resolve49(context.workingDirectory, args.srtPath);
58329
+ const srtPath = resolve50(context.workingDirectory, args.srtPath);
56692
58330
  const target = args.targetLanguage;
56693
58331
  const ext = srtPath.split(".").pop() || "srt";
56694
58332
  const name = srtPath.replace(/\.[^.]+$/, "");
56695
- const outputPath = args.outputPath ? resolve49(context.workingDirectory, args.outputPath) : `${name}-${target}.${ext}`;
58333
+ const outputPath = args.outputPath ? resolve50(context.workingDirectory, args.outputPath) : `${name}-${target}.${ext}`;
56696
58334
  try {
56697
58335
  const result = await executeTranslateSrt({
56698
58336
  srtPath,
@@ -56754,7 +58392,7 @@ var export_exports = {};
56754
58392
  __export(export_exports, {
56755
58393
  registerExportTools: () => registerExportTools
56756
58394
  });
56757
- import { resolve as resolve50, basename as basename26 } from "node:path";
58395
+ import { resolve as resolve51, basename as basename26 } from "node:path";
56758
58396
  function registerExportTools(registry) {
56759
58397
  registry.register(exportVideoDef, exportVideo);
56760
58398
  registry.register(exportAudioDef, exportAudio);
@@ -56849,8 +58487,8 @@ var init_export2 = __esm({
56849
58487
  const preset = args.preset || "standard";
56850
58488
  const overwrite = args.overwrite || false;
56851
58489
  const output = args.output || `${basename26(projectPath, ".vibe.json")}.${format}`;
56852
- const absProjectPath = resolve50(context.workingDirectory, projectPath);
56853
- const absOutputPath = resolve50(context.workingDirectory, output);
58490
+ const absProjectPath = resolve51(context.workingDirectory, projectPath);
58491
+ const absOutputPath = resolve51(context.workingDirectory, output);
56854
58492
  try {
56855
58493
  const result = await runExport(absProjectPath, absOutputPath, {
56856
58494
  format,
@@ -56923,8 +58561,8 @@ var batch_exports = {};
56923
58561
  __export(batch_exports, {
56924
58562
  registerBatchTools: () => registerBatchTools
56925
58563
  });
56926
- import { readFile as readFile42, writeFile as writeFile38, readdir as readdir6 } from "node:fs/promises";
56927
- import { resolve as resolve51, basename as basename27, extname as extname16, join as join8 } from "node:path";
58564
+ import { readFile as readFile43, writeFile as writeFile39, readdir as readdir6 } from "node:fs/promises";
58565
+ import { resolve as resolve52, basename as basename27, extname as extname16, join as join10 } from "node:path";
56928
58566
  function detectMediaType5(filePath) {
56929
58567
  const ext = extname16(filePath).toLowerCase();
56930
58568
  const videoExts = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
@@ -57085,17 +58723,17 @@ var init_batch = __esm({
57085
58723
  const filterStr = args.filter;
57086
58724
  const imageDuration = args.imageDuration || 5;
57087
58725
  try {
57088
- const filePath = resolve51(context.workingDirectory, projectPath);
57089
- const content = await readFile42(filePath, "utf-8");
58726
+ const filePath = resolve52(context.workingDirectory, projectPath);
58727
+ const content = await readFile43(filePath, "utf-8");
57090
58728
  const data = JSON.parse(content);
57091
58729
  const project = Project.fromJSON(data);
57092
- const dirPath = resolve51(context.workingDirectory, directory);
58730
+ const dirPath = resolve52(context.workingDirectory, directory);
57093
58731
  const filterExts = filterStr ? filterStr.split(",").map((e) => e.trim().toLowerCase()) : null;
57094
58732
  const mediaFiles = [];
57095
58733
  const scanDir = async (dir) => {
57096
58734
  const entries = await readdir6(dir, { withFileTypes: true });
57097
58735
  for (const entry of entries) {
57098
- const entryPath = join8(dir, entry.name);
58736
+ const entryPath = join10(dir, entry.name);
57099
58737
  if (entry.isDirectory() && recursive) {
57100
58738
  await scanDir(entryPath);
57101
58739
  } else if (entry.isFile()) {
@@ -57136,7 +58774,7 @@ var init_batch = __esm({
57136
58774
  });
57137
58775
  addedSources.push({ id: source.id, name: mediaName, type: mediaType });
57138
58776
  }
57139
- await writeFile38(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
58777
+ await writeFile39(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
57140
58778
  const output = [
57141
58779
  `Imported ${addedSources.length} media files:`,
57142
58780
  "",
@@ -57164,8 +58802,8 @@ var init_batch = __esm({
57164
58802
  const startTime = args.startTime || 0;
57165
58803
  const gap = args.gap || 0;
57166
58804
  try {
57167
- const filePath = resolve51(context.workingDirectory, projectPath);
57168
- const content = await readFile42(filePath, "utf-8");
58805
+ const filePath = resolve52(context.workingDirectory, projectPath);
58806
+ const content = await readFile43(filePath, "utf-8");
57169
58807
  const data = JSON.parse(content);
57170
58808
  const project = Project.fromJSON(data);
57171
58809
  const sourcesToConcat = useAll ? project.getSources() : sourceIds.map((id) => project.getSource(id)).filter(Boolean);
@@ -57212,7 +58850,7 @@ var init_batch = __esm({
57212
58850
  });
57213
58851
  currentTime += source.duration + gap;
57214
58852
  }
57215
- await writeFile38(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
58853
+ await writeFile39(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
57216
58854
  const totalDuration = currentTime - gap - startTime;
57217
58855
  const output = [
57218
58856
  `Created ${createdClips.length} clips (total: ${totalDuration.toFixed(1)}s):`,
@@ -57241,8 +58879,8 @@ var init_batch = __esm({
57241
58879
  const duration = args.duration;
57242
58880
  const rawParams = args.params || {};
57243
58881
  try {
57244
- const filePath = resolve51(context.workingDirectory, projectPath);
57245
- const content = await readFile42(filePath, "utf-8");
58882
+ const filePath = resolve52(context.workingDirectory, projectPath);
58883
+ const content = await readFile43(filePath, "utf-8");
57246
58884
  const data = JSON.parse(content);
57247
58885
  const project = Project.fromJSON(data);
57248
58886
  const targetClips = useAll ? project.getClips() : clipIds.map((id) => project.getClip(id)).filter(Boolean);
@@ -57274,7 +58912,7 @@ var init_batch = __esm({
57274
58912
  appliedEffects.push({ clipId: clip.id, effectId: effect.id });
57275
58913
  }
57276
58914
  }
57277
- await writeFile38(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
58915
+ await writeFile39(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
57278
58916
  return {
57279
58917
  toolCallId: "",
57280
58918
  success: true,
@@ -57298,7 +58936,7 @@ __export(scene_exports, {
57298
58936
  registerSceneTools: () => registerSceneTools,
57299
58937
  sceneToolDefinitions: () => sceneToolDefinitions
57300
58938
  });
57301
- import { resolve as resolve52, relative as relative7 } from "node:path";
58939
+ import { resolve as resolve53, relative as relative7 } from "node:path";
57302
58940
  function summariseLint(result) {
57303
58941
  const lines = [
57304
58942
  `${result.ok ? "\u2705" : "\u274C"} Lint ${result.ok ? "clean" : "failed"} \u2014 ${result.errorCount} error(s), ${result.warningCount} warning(s), ${result.infoCount} info`
@@ -57409,7 +59047,7 @@ var init_scene2 = __esm({
57409
59047
  }
57410
59048
  };
57411
59049
  sceneInitHandler = async (args, context) => {
57412
- const dir = resolve52(context.workingDirectory, args.dir);
59050
+ const dir = resolve53(context.workingDirectory, args.dir);
57413
59051
  try {
57414
59052
  const result = await scaffoldSceneProject({
57415
59053
  dir,
@@ -57436,7 +59074,7 @@ var init_scene2 = __esm({
57436
59074
  }
57437
59075
  };
57438
59076
  sceneAddHandler = async (args, context) => {
57439
- const projectDir = args.projectDir ? resolve52(context.workingDirectory, args.projectDir) : context.workingDirectory;
59077
+ const projectDir = args.projectDir ? resolve53(context.workingDirectory, args.projectDir) : context.workingDirectory;
57440
59078
  const result = await executeSceneAdd({
57441
59079
  name: args.name,
57442
59080
  preset: args.preset ?? "simple",
@@ -57473,7 +59111,7 @@ var init_scene2 = __esm({
57473
59111
  return { toolCallId: "", success: true, output: lines.join("\n") };
57474
59112
  };
57475
59113
  sceneLintHandler = async (args, context) => {
57476
- const projectDir = args.projectDir ? resolve52(context.workingDirectory, args.projectDir) : context.workingDirectory;
59114
+ const projectDir = args.projectDir ? resolve53(context.workingDirectory, args.projectDir) : context.workingDirectory;
57477
59115
  try {
57478
59116
  const result = await runProjectLint({
57479
59117
  projectDir,
@@ -57496,7 +59134,7 @@ var init_scene2 = __esm({
57496
59134
  }
57497
59135
  };
57498
59136
  sceneRenderHandler = async (args, context) => {
57499
- const projectDir = args.projectDir ? resolve52(context.workingDirectory, args.projectDir) : context.workingDirectory;
59137
+ const projectDir = args.projectDir ? resolve53(context.workingDirectory, args.projectDir) : context.workingDirectory;
57500
59138
  const result = await executeSceneRender({
57501
59139
  projectDir,
57502
59140
  root: args.root,
@@ -57538,7 +59176,7 @@ init_source();
57538
59176
  // package.json
57539
59177
  var package_default = {
57540
59178
  name: "@vibeframe/cli",
57541
- version: "0.57.3",
59179
+ version: "0.59.0",
57542
59180
  description: "VibeFrame CLI - natural language video editing from the terminal",
57543
59181
  private: false,
57544
59182
  license: "MIT",
@@ -57764,8 +59402,8 @@ projectCommand.command("create").description("Create a new project").argument("<
57764
59402
  outputPath = resolve2(process.cwd(), options.output);
57765
59403
  } else if (name.includes("/")) {
57766
59404
  const dirPath = resolve2(process.cwd(), name);
57767
- const { mkdir: mkdir21 } = await import("node:fs/promises");
57768
- await mkdir21(dirPath, { recursive: true });
59405
+ const { mkdir: mkdir22 } = await import("node:fs/promises");
59406
+ await mkdir22(dirPath, { recursive: true });
57769
59407
  outputPath = resolve2(dirPath, "project.vibe.json");
57770
59408
  } else {
57771
59409
  outputPath = resolve2(process.cwd(), "project.vibe.json");
@@ -59650,9 +61288,9 @@ function registerViralCommand(ai) {
59650
61288
  exitWithError(authError("ANTHROPIC_API_KEY", "Anthropic"));
59651
61289
  }
59652
61290
  let filePath = resolve28(process.cwd(), projectPath);
59653
- const { statSync } = await import("node:fs");
61291
+ const { statSync: statSync2 } = await import("node:fs");
59654
61292
  try {
59655
- if (statSync(filePath).isDirectory()) {
61293
+ if (statSync2(filePath).isDirectory()) {
59656
61294
  const candidates = ["project.vibe.json", ".vibe.json"];
59657
61295
  let found = false;
59658
61296
  for (const candidate of candidates) {
@@ -65776,20 +67414,20 @@ async function runDiagnostics() {
65776
67414
  installed = false;
65777
67415
  }
65778
67416
  } else if (toolName === "chrome") {
65779
- const { existsSync: existsSync28 } = await import("node:fs");
65780
- const { homedir: homedir3 } = await import("node:os");
65781
- const { join: join9 } = await import("node:path");
67417
+ const { existsSync: existsSync30 } = await import("node:fs");
67418
+ const { homedir: homedir5 } = await import("node:os");
67419
+ const { join: join11 } = await import("node:path");
65782
67420
  const chromePaths = [
65783
67421
  process.env.HYPERFRAMES_CHROME_PATH,
65784
67422
  process.env.CHROME_PATH,
65785
- join9(homedir3(), ".cache", "puppeteer", "chrome-headless-shell", "mac_arm-147.0.7727.56", "chrome-headless-shell-mac_arm", "chrome-headless-shell"),
67423
+ join11(homedir5(), ".cache", "puppeteer", "chrome-headless-shell", "mac_arm-147.0.7727.56", "chrome-headless-shell-mac_arm", "chrome-headless-shell"),
65786
67424
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
65787
67425
  "/Applications/Chromium.app/Contents/MacOS/Chromium",
65788
67426
  "/usr/bin/google-chrome",
65789
67427
  "/usr/bin/chromium",
65790
67428
  "/usr/bin/chromium-browser"
65791
67429
  ];
65792
- installed = chromePaths.some((p) => p && existsSync28(p));
67430
+ installed = chromePaths.some((p) => p && existsSync30(p));
65793
67431
  } else {
65794
67432
  installed = commandExists(toolName);
65795
67433
  }
@@ -66170,9 +67808,9 @@ init_ora();
66170
67808
 
66171
67809
  // src/pipeline/executor.ts
66172
67810
  var import_yaml7 = __toESM(require_dist(), 1);
66173
- import { resolve as resolve42 } from "node:path";
66174
- import { readFile as readFile35, writeFile as writeFile31, mkdir as mkdir19 } from "node:fs/promises";
66175
- import { existsSync as existsSync27 } from "node:fs";
67811
+ import { resolve as resolve43 } from "node:path";
67812
+ import { readFile as readFile36, writeFile as writeFile32, mkdir as mkdir20 } from "node:fs/promises";
67813
+ import { existsSync as existsSync29 } from "node:fs";
66176
67814
 
66177
67815
  // src/pipeline/resolver.ts
66178
67816
  function resolveStepParams(params, completedSteps) {
@@ -66258,7 +67896,8 @@ var ACTION_TO_COMMAND = {
66258
67896
  "detect-beats": "detect beats",
66259
67897
  "analyze-media": "analyze media",
66260
67898
  "analyze-video": "analyze video",
66261
- "review-video": "analyze review"
67899
+ "review-video": "analyze review",
67900
+ "compose-scenes-with-skills": "compose scenes with skills"
66262
67901
  };
66263
67902
  function maxCostFor(action) {
66264
67903
  const cmd = ACTION_TO_COMMAND[action];
@@ -66271,9 +67910,9 @@ function registerAction(action, handler) {
66271
67910
  }
66272
67911
  function getOutput(params, outputDir, defaultName) {
66273
67912
  if (params.output && typeof params.output === "string") {
66274
- return resolve42(outputDir, params.output);
67913
+ return resolve43(outputDir, params.output);
66275
67914
  }
66276
- return resolve42(outputDir, defaultName);
67915
+ return resolve43(outputDir, defaultName);
66277
67916
  }
66278
67917
  async function ensureActionsRegistered() {
66279
67918
  if (Object.keys(ACTION_HANDLERS).length > 0) return;
@@ -66393,13 +68032,33 @@ async function ensureActionsRegistered() {
66393
68032
  const r = await executeReview2({ videoPath: params.input, autoApply: params.autoApply, model: params.model });
66394
68033
  return { id: "", action: "review-video", success: r.success, output: r.outputPath, data: { feedback: r.feedback }, error: r.error };
66395
68034
  });
68035
+ registerAction("compose-scenes-with-skills", async (params, outputDir) => {
68036
+ const { executeComposeScenesWithSkills: executeComposeScenesWithSkills2 } = await Promise.resolve().then(() => (init_compose_scenes_skills(), compose_scenes_skills_exports));
68037
+ const r = await executeComposeScenesWithSkills2(
68038
+ {
68039
+ design: params.design,
68040
+ storyboard: params.storyboard,
68041
+ project: params.project,
68042
+ effort: params.effort
68043
+ },
68044
+ outputDir
68045
+ );
68046
+ return {
68047
+ id: "",
68048
+ action: "compose-scenes-with-skills",
68049
+ success: r.success,
68050
+ output: r.outputPath,
68051
+ data: r.data,
68052
+ error: r.error
68053
+ };
68054
+ });
66396
68055
  }
66397
68056
  async function loadPipeline(filePath) {
66398
- const absPath = resolve42(process.cwd(), filePath);
66399
- if (!existsSync27(absPath)) {
68057
+ const absPath = resolve43(process.cwd(), filePath);
68058
+ if (!existsSync29(absPath)) {
66400
68059
  throw new Error(`Pipeline file not found: ${absPath}`);
66401
68060
  }
66402
- const content = await readFile35(absPath, "utf-8");
68061
+ const content = await readFile36(absPath, "utf-8");
66403
68062
  const manifest = (0, import_yaml7.parse)(content);
66404
68063
  if (!manifest.name) throw new Error("Pipeline missing 'name' field");
66405
68064
  if (!manifest.steps || !Array.isArray(manifest.steps)) throw new Error("Pipeline missing 'steps' array");
@@ -66415,15 +68074,15 @@ async function loadPipeline(filePath) {
66415
68074
  var CHECKPOINT_FILE = ".pipeline-state.yaml";
66416
68075
  async function executePipeline(manifest, options = {}) {
66417
68076
  await ensureActionsRegistered();
66418
- const outputDir = resolve42(process.cwd(), options.outputDir || `${manifest.name}-output`);
66419
- await mkdir19(outputDir, { recursive: true });
68077
+ const outputDir = resolve43(process.cwd(), options.outputDir || `${manifest.name}-output`);
68078
+ await mkdir20(outputDir, { recursive: true });
66420
68079
  const completedSteps = /* @__PURE__ */ new Map();
66421
68080
  const results = [];
66422
68081
  const startTime = Date.now();
66423
68082
  if (options.resume) {
66424
- const checkpointPath = resolve42(outputDir, CHECKPOINT_FILE);
66425
- if (existsSync27(checkpointPath)) {
66426
- const checkpointContent = await readFile35(checkpointPath, "utf-8");
68083
+ const checkpointPath = resolve43(outputDir, CHECKPOINT_FILE);
68084
+ if (existsSync29(checkpointPath)) {
68085
+ const checkpointContent = await readFile36(checkpointPath, "utf-8");
66427
68086
  const checkpoint = (0, import_yaml7.parse)(checkpointContent);
66428
68087
  for (const cs of checkpoint.completedSteps) {
66429
68088
  completedSteps.set(cs.id, {
@@ -66541,8 +68200,8 @@ async function executePipeline(manifest, options = {}) {
66541
68200
  data: s.data
66542
68201
  }))
66543
68202
  };
66544
- await writeFile31(
66545
- resolve42(outputDir, CHECKPOINT_FILE),
68203
+ await writeFile32(
68204
+ resolve43(outputDir, CHECKPOINT_FILE),
66546
68205
  (0, import_yaml7.stringify)(checkpoint, { indent: 2 }),
66547
68206
  "utf-8"
66548
68207
  );
@@ -66710,7 +68369,8 @@ Run 'vibe schema run' for structured parameter info.
66710
68369
  success: s.success,
66711
68370
  output: s.output,
66712
68371
  duration: s.duration,
66713
- error: s.error
68372
+ error: s.error,
68373
+ data: s.data
66714
68374
  }))
66715
68375
  });
66716
68376
  } else {
@@ -67299,14 +68959,14 @@ async function promptConfirm2(toolName, args) {
67299
68959
  output: process.stdout,
67300
68960
  terminal: process.stdin.isTTY ?? false
67301
68961
  });
67302
- return new Promise((resolve53) => {
68962
+ return new Promise((resolve54) => {
67303
68963
  const argsStr = JSON.stringify(args, null, 2);
67304
68964
  console.log();
67305
68965
  console.log(source_default.yellow(`Execute ${source_default.bold(toolName)}?`));
67306
68966
  console.log(source_default.dim(argsStr));
67307
68967
  rl.question(source_default.cyan("(y/n): "), (answer) => {
67308
68968
  rl.close();
67309
- resolve53(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
68969
+ resolve54(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
67310
68970
  });
67311
68971
  });
67312
68972
  }
@@ -67395,7 +69055,7 @@ async function startAgent(options = {}) {
67395
69055
  console.log();
67396
69056
  console.log(source_default.dim(" Commands: exit \xB7 reset \xB7 tools \xB7 context"));
67397
69057
  console.log();
67398
- return new Promise((resolve53) => {
69058
+ return new Promise((resolve54) => {
67399
69059
  if (typeof process.stdin.ref === "function") {
67400
69060
  process.stdin.ref();
67401
69061
  }
@@ -67497,7 +69157,7 @@ async function startAgent(options = {}) {
67497
69157
  clearInterval(keepalive);
67498
69158
  console.log();
67499
69159
  console.log(source_default.dim("Goodbye!"));
67500
- resolve53();
69160
+ resolve54();
67501
69161
  });
67502
69162
  rl.prompt();
67503
69163
  });
@@ -67525,8 +69185,8 @@ init_source();
67525
69185
  init_config();
67526
69186
  init_schema();
67527
69187
  init_api_key();
67528
- import { access as access9, writeFile as writeFile39, mkdir as mkdir20 } from "node:fs/promises";
67529
- import { dirname as dirname20 } from "node:path";
69188
+ import { access as access9, writeFile as writeFile40, mkdir as mkdir21 } from "node:fs/promises";
69189
+ import { dirname as dirname21 } from "node:path";
67530
69190
  var BANNER_SHOWN_PATH = CONFIG_PATH.replace(/config\.yaml$/, ".banner-shown");
67531
69191
  async function isFirstRun() {
67532
69192
  try {
@@ -67549,8 +69209,8 @@ async function isFirstRun() {
67549
69209
  }
67550
69210
  async function markBannerShown() {
67551
69211
  try {
67552
- await mkdir20(dirname20(BANNER_SHOWN_PATH), { recursive: true });
67553
- await writeFile39(BANNER_SHOWN_PATH, (/* @__PURE__ */ new Date()).toISOString());
69212
+ await mkdir21(dirname21(BANNER_SHOWN_PATH), { recursive: true });
69213
+ await writeFile40(BANNER_SHOWN_PATH, (/* @__PURE__ */ new Date()).toISOString());
67554
69214
  } catch {
67555
69215
  }
67556
69216
  }