@vibeframe/cli 0.58.0 → 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 +1601 -402
  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
  });
@@ -26088,31 +26091,6 @@ function fitKineticFontSize(words) {
26088
26091
  function kenBurnsTween(scope, dur, endScale = 1.08) {
26089
26092
  return `tl.fromTo('${scope} .backdrop', { scale: 1.0 }, { scale: ${endScale}, duration: ${dur.toFixed(2)}, ease: 'none' }, 0);`;
26090
26093
  }
26091
- function idleHeroPulse(scope, selector, idleStart, dur) {
26092
- const cycleDur = 1.5;
26093
- const idleEnd = dur - SCENE_OVERLAP_SECONDS;
26094
- const window2 = idleEnd - idleStart;
26095
- if (window2 < cycleDur) return "";
26096
- const plays = Math.max(1, Math.floor(window2 / cycleDur));
26097
- const repeat = plays - 1;
26098
- return `tl.to('${scope} ${selector}', { scale: 1.04, y: -8, duration: ${cycleDur}, yoyo: true, repeat: ${repeat}, ease: 'sine.inOut' }, ${idleStart.toFixed(2)});`;
26099
- }
26100
- function kineticWordBobs(scope, wordCount, idleStart, dur) {
26101
- const cycleDur = 1.8;
26102
- const idleEnd = dur - SCENE_OVERLAP_SECONDS;
26103
- const window2 = idleEnd - idleStart;
26104
- if (window2 < cycleDur) return "";
26105
- const plays = Math.max(1, Math.floor(window2 / cycleDur));
26106
- const repeat = plays - 1;
26107
- const lines = [];
26108
- for (let i = 0; i < wordCount; i++) {
26109
- const start = (idleStart + i * 0.18).toFixed(2);
26110
- lines.push(
26111
- `tl.to('${scope} #w-${i}', { y: -12, duration: ${cycleDur}, yoyo: true, repeat: ${repeat}, ease: 'sine.inOut' }, ${start});`
26112
- );
26113
- }
26114
- return lines.join("\n ");
26115
- }
26116
26094
  function buildPreset(input) {
26117
26095
  const id = input.id;
26118
26096
  const scope = `[data-composition-id="${id}"]`;
@@ -26173,8 +26151,7 @@ function buildPreset(input) {
26173
26151
  <div class="caption" id="caption">${captionInner}</div>`,
26174
26152
  timeline: `${crossfadeTweens(scope, dur)}
26175
26153
  ${kenBurnsTween(scope, dur)}
26176
- ${captionTween}
26177
- ${idleHeroPulse(scope, "#caption", 1, dur)}`
26154
+ ${captionTween}`
26178
26155
  };
26179
26156
  }
26180
26157
  case "announcement": {
@@ -26213,8 +26190,7 @@ function buildPreset(input) {
26213
26190
  // Ken-Burns fills what used to be 80% dead static frame.
26214
26191
  timeline: `${crossfadeTweens(scope, dur)}
26215
26192
  ${kenBurnsTween(scope, dur)}
26216
- tl.from('${scope} #headline', { opacity: 0, scale: 0.92, duration: 0.55, ease: 'power3.out' }, 0.05);
26217
- ${idleHeroPulse(scope, "#headline", 1, dur)}`
26193
+ tl.from('${scope} #headline', { opacity: 0, scale: 0.92, duration: 0.55, ease: 'power3.out' }, 0.05);`
26218
26194
  };
26219
26195
  }
26220
26196
  case "explainer": {
@@ -26263,8 +26239,7 @@ function buildPreset(input) {
26263
26239
  ${kenBurnsTween(scope, dur)}
26264
26240
  tl.from('${scope} #kicker', { opacity: 0, y: 16, duration: 0.4, ease: 'power2.out' }, 0.1);
26265
26241
  tl.from('${scope} #title', { opacity: 0, y: 60, duration: 0.7, ease: 'power3.out' }, 0.25);
26266
- ${subtitleTween}
26267
- ${idleHeroPulse(scope, "#title", 1.2, dur)}`
26242
+ ${subtitleTween}`
26268
26243
  };
26269
26244
  }
26270
26245
  case "kinetic-type": {
@@ -26281,8 +26256,6 @@ function buildPreset(input) {
26281
26256
  return `tl.from('${scope} #w-${i}', { opacity: 0, y: 80, scale: 0.8, duration: 0.45, ease: 'back.out(1.8)' }, ${start});`;
26282
26257
  }).join("\n ");
26283
26258
  const kineticFontPx = fitKineticFontSize(words);
26284
- const lastWordEntryEnd = useWordSync ? Math.max(...transcript.map((w) => w.end + 0.2)) : 0.05 + (words.length - 1) * stagger + 0.45 + 0.2;
26285
- const idleStart = Math.max(1, Number(lastWordEntryEnd.toFixed(2)));
26286
26259
  return {
26287
26260
  css: `${scope} {
26288
26261
  position: absolute; inset: 0; width: 100%; height: 100%;
@@ -26305,8 +26278,7 @@ function buildPreset(input) {
26305
26278
  <div class="kinetic">${wordSpans}</div>`,
26306
26279
  timeline: `${crossfadeTweens(scope, dur)}
26307
26280
  ${kenBurnsTween(scope, dur)}
26308
- ${tweens}
26309
- ${kineticWordBobs(scope, words.length, idleStart, dur)}`
26281
+ ${tweens}`
26310
26282
  };
26311
26283
  }
26312
26284
  case "product-shot": {
@@ -26347,8 +26319,7 @@ function buildPreset(input) {
26347
26319
  tl.fromTo('${scope} .backdrop', { scale: 1.0 }, { scale: 1.12, duration: ${dur.toFixed(2)}, ease: 'none' }, 0);
26348
26320
  tl.from('${scope} #label', { opacity: 0, x: -30, duration: 0.5, ease: 'power3.out' }, 0.2);
26349
26321
  tl.from('${scope} #headline', { opacity: 0, y: 40, duration: 0.6, ease: 'power3.out' }, 0.4);${subhead ? `
26350
- tl.from('${scope} #subhead', { opacity: 0, y: 20, duration: 0.5, ease: 'power3.out' }, 0.65);` : ""}
26351
- ${idleHeroPulse(scope, "#headline", 1.2, dur)}`
26322
+ tl.from('${scope} #subhead', { opacity: 0, y: 20, duration: 0.5, ease: 'power3.out' }, 0.65);` : ""}`
26352
26323
  };
26353
26324
  }
26354
26325
  }
@@ -28820,16 +28791,16 @@ async function prompt2(question) {
28820
28791
  input,
28821
28792
  output: process.stdout
28822
28793
  });
28823
- return new Promise((resolve53) => {
28794
+ return new Promise((resolve54) => {
28824
28795
  rl.question(question, (answer) => {
28825
28796
  rl.close();
28826
- resolve53(answer);
28797
+ resolve54(answer);
28827
28798
  });
28828
28799
  });
28829
28800
  }
28830
28801
  async function promptHidden(question) {
28831
28802
  const input = getTTYInputStream();
28832
- return new Promise((resolve53) => {
28803
+ return new Promise((resolve54) => {
28833
28804
  process.stdout.write(question);
28834
28805
  let value = "";
28835
28806
  if (typeof input.setRawMode === "function") {
@@ -28841,7 +28812,7 @@ async function promptHidden(question) {
28841
28812
  input.setRawMode(false);
28842
28813
  input.removeListener("data", onData);
28843
28814
  process.stdout.write("\n");
28844
- resolve53(value);
28815
+ resolve54(value);
28845
28816
  } else if (char === "") {
28846
28817
  input.setRawMode(false);
28847
28818
  process.stdout.write("\n");
@@ -28861,7 +28832,7 @@ async function promptHidden(question) {
28861
28832
  const rl = createInterface2({ input, output: process.stdout });
28862
28833
  rl.question("", (answer) => {
28863
28834
  rl.close();
28864
- resolve53(answer);
28835
+ resolve54(answer);
28865
28836
  });
28866
28837
  }
28867
28838
  });
@@ -28869,7 +28840,7 @@ async function promptHidden(question) {
28869
28840
  async function promptSelect(_question, options, defaultIndex = 0) {
28870
28841
  const input = getTTYInputStream();
28871
28842
  if (typeof input.setRawMode === "function") {
28872
- return new Promise((resolve53) => {
28843
+ return new Promise((resolve54) => {
28873
28844
  let selected = defaultIndex;
28874
28845
  const render = () => {
28875
28846
  if (renderCount > 0) {
@@ -28893,7 +28864,7 @@ async function promptSelect(_question, options, defaultIndex = 0) {
28893
28864
  input.setRawMode(false);
28894
28865
  input.removeListener("data", onData);
28895
28866
  process.stdout.write("\n");
28896
- resolve53(selected);
28867
+ resolve54(selected);
28897
28868
  } else if (char === "") {
28898
28869
  input.setRawMode(false);
28899
28870
  process.stdout.write("\n");
@@ -28910,7 +28881,7 @@ async function promptSelect(_question, options, defaultIndex = 0) {
28910
28881
  input.removeListener("data", onData);
28911
28882
  render();
28912
28883
  process.stdout.write("\n");
28913
- resolve53(selected);
28884
+ resolve54(selected);
28914
28885
  }
28915
28886
  };
28916
28887
  input.on("data", onData);
@@ -29080,13 +29051,10 @@ var init_subtitle = __esm({
29080
29051
  // src/utils/remotion.ts
29081
29052
  var remotion_exports = {};
29082
29053
  __export(remotion_exports, {
29083
- compositeOverlay: () => compositeOverlay,
29084
- compositeWithImage: () => compositeWithImage,
29085
29054
  ensureRemotionInstalled: () => ensureRemotionInstalled,
29086
29055
  generateAnimatedCaptionComponent: () => generateAnimatedCaptionComponent,
29087
29056
  generateCaptionComponent: () => generateCaptionComponent,
29088
29057
  generateTextOverlayComponent: () => generateTextOverlayComponent,
29089
- renderAndComposite: () => renderAndComposite,
29090
29058
  renderMotion: () => renderMotion,
29091
29059
  renderWithEmbeddedImage: () => renderWithEmbeddedImage,
29092
29060
  renderWithEmbeddedVideo: () => renderWithEmbeddedVideo,
@@ -29834,92 +29802,6 @@ export const ${name} = () => {
29834
29802
  `;
29835
29803
  return { code, name };
29836
29804
  }
29837
- async function compositeOverlay(options) {
29838
- try {
29839
- await execSafe("ffmpeg", [
29840
- "-y",
29841
- "-i",
29842
- options.baseVideo,
29843
- "-i",
29844
- options.overlayPath,
29845
- "-filter_complex",
29846
- "[0:v][1:v]overlay=0:0:shortest=1[out]",
29847
- "-map",
29848
- "[out]",
29849
- "-map",
29850
- "0:a?",
29851
- "-c:a",
29852
- "copy",
29853
- "-c:v",
29854
- "libx264",
29855
- "-crf",
29856
- "18",
29857
- "-pix_fmt",
29858
- "yuv420p",
29859
- options.outputPath
29860
- ], { timeout: 3e5 });
29861
- return { success: true, outputPath: options.outputPath };
29862
- } catch (error) {
29863
- const msg = error instanceof Error ? error.message : String(error);
29864
- return { success: false, error: `FFmpeg composite failed: ${msg}` };
29865
- }
29866
- }
29867
- async function compositeWithImage(options) {
29868
- try {
29869
- const fps = options.fps || 30;
29870
- await execSafe("ffmpeg", [
29871
- "-y",
29872
- "-loop",
29873
- "1",
29874
- "-framerate",
29875
- String(fps),
29876
- "-i",
29877
- options.baseImage,
29878
- "-i",
29879
- options.overlayPath,
29880
- "-filter_complex",
29881
- "[0:v]scale=iw:ih[base];[base][1:v]overlay=0:0:shortest=1[out]",
29882
- "-map",
29883
- "[out]",
29884
- "-c:v",
29885
- "libx264",
29886
- "-crf",
29887
- "18",
29888
- "-pix_fmt",
29889
- "yuv420p",
29890
- "-t",
29891
- String(options.durationSeconds),
29892
- options.outputPath
29893
- ], { timeout: 3e5 });
29894
- return { success: true, outputPath: options.outputPath };
29895
- } catch (error) {
29896
- const msg = error instanceof Error ? error.message : String(error);
29897
- return { success: false, error: `FFmpeg image composite failed: ${msg}` };
29898
- }
29899
- }
29900
- async function renderAndComposite(motionOpts, baseVideo, finalOutput) {
29901
- const renderOpts = {
29902
- ...motionOpts,
29903
- transparent: !!baseVideo,
29904
- outputPath: baseVideo ? motionOpts.outputPath.replace(/\.\w+$/, "_overlay.webm") : motionOpts.outputPath
29905
- };
29906
- const renderResult = await renderMotion(renderOpts);
29907
- if (!renderResult.success || !renderResult.outputPath) {
29908
- return renderResult;
29909
- }
29910
- if (!baseVideo) {
29911
- return renderResult;
29912
- }
29913
- const output = finalOutput || motionOpts.outputPath;
29914
- const compositeResult = await compositeOverlay({
29915
- baseVideo,
29916
- overlayPath: renderResult.outputPath,
29917
- outputPath: output
29918
- });
29919
- await rm(renderResult.outputPath, { force: true }).catch(() => {
29920
- });
29921
- return compositeResult;
29922
- }
29923
29805
  var REMOTION_VERSION, REMOTION_CACHE_DIR;
29924
29806
  var init_remotion = __esm({
29925
29807
  "src/utils/remotion.ts"() {
@@ -31418,7 +31300,7 @@ import { readFile as readFile13, writeFile as writeFile11, mkdir as mkdir8, unli
31418
31300
  import { resolve as resolve15, basename as basename8, dirname as dirname6, extname as extname4 } from "node:path";
31419
31301
  import { existsSync as existsSync9 } from "node:fs";
31420
31302
  function sleep(ms) {
31421
- return new Promise((resolve53) => setTimeout(resolve53, ms));
31303
+ return new Promise((resolve54) => setTimeout(resolve54, ms));
31422
31304
  }
31423
31305
  async function uploadToImgbb(imageBuffer, apiKey) {
31424
31306
  try {
@@ -31783,7 +31665,6 @@ async function executeScriptToVideo(options) {
31783
31665
  outputDir: absOutputDir,
31784
31666
  scenes: segments.length,
31785
31667
  storyboardPath,
31786
- narrations: [],
31787
31668
  narrationEntries: [],
31788
31669
  images: [],
31789
31670
  videos: [],
@@ -31844,7 +31725,6 @@ async function executeScriptToVideo(options) {
31844
31725
  );
31845
31726
  }
31846
31727
  segment.duration = actualDuration;
31847
- result.narrations.push(audioPath);
31848
31728
  result.narrationEntries.push({
31849
31729
  path: audioPath,
31850
31730
  duration: actualDuration,
@@ -32827,6 +32707,34 @@ var init_provider_resolver = __esm({
32827
32707
  }
32828
32708
  });
32829
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
+
32830
32738
  // src/commands/ai-image.ts
32831
32739
  var ai_image_exports = {};
32832
32740
  __export(ai_image_exports, {
@@ -32879,22 +32787,11 @@ function _registerImageCommands(aiCommand) {
32879
32787
  }
32880
32788
  const spinner2 = ora(`Generating image with ${providerName}...`).start();
32881
32789
  if (provider === "dalle" || provider === "openai") {
32882
- const openaiImage = new OpenAIImageProvider();
32883
- await openaiImage.initialize({ apiKey });
32884
- const modelAlias = options.model;
32885
- const openaiModel = modelAlias === "2" || modelAlias === "gpt-image-2" ? "gpt-image-2" : void 0;
32886
- const result = await openaiImage.generateImage(prompt3, {
32887
- model: openaiModel,
32888
- size: options.size,
32889
- quality: options.quality,
32890
- style: options.style,
32891
- n: parseInt(options.count)
32892
- });
32790
+ const { result, modelLabel } = await executeOpenAIImageGenerate(prompt3, options, { apiKey });
32893
32791
  if (!result.success || !result.images) {
32894
32792
  spinner2.fail("Image generation failed");
32895
32793
  exitWithError(apiError(result.error || "Image generation failed", true));
32896
32794
  }
32897
- const modelLabel = openaiModel === "gpt-image-2" ? "GPT Image 2" : "GPT Image 1.5";
32898
32795
  spinner2.succeed(source_default.green(`Generated ${result.images.length} image(s) with OpenAI ${modelLabel}`));
32899
32796
  console.log();
32900
32797
  console.log(source_default.bold.cyan("Generated Images"));
@@ -33581,6 +33478,7 @@ var init_ai_image = __esm({
33581
33478
  init_exec_safe();
33582
33479
  init_output();
33583
33480
  init_validate();
33481
+ init_openai_image2();
33584
33482
  }
33585
33483
  });
33586
33484
 
@@ -34021,6 +33919,7 @@ var init_generate = __esm({
34021
33919
  init_provider_resolver();
34022
33920
  init_ai_image();
34023
33921
  init_ai_motion();
33922
+ init_openai_image2();
34024
33923
  generateCommand = new Command("generate").alias("gen").description(
34025
33924
  "Generate assets using AI (images, videos, speech, music, motion)"
34026
33925
  ).addHelpText(
@@ -34120,22 +34019,11 @@ Examples:
34120
34019
  const apiKey = await requireApiKey(envKey, providerName, options.apiKey);
34121
34020
  const spinner2 = ora(`Generating image with ${providerName}...`).start();
34122
34021
  if (provider === "dalle" || provider === "openai") {
34123
- const openaiImage = new OpenAIImageProvider();
34124
- await openaiImage.initialize({ apiKey });
34125
- const modelAlias = options.model;
34126
- const openaiModel = modelAlias === "2" || modelAlias === "gpt-image-2" ? "gpt-image-2" : void 0;
34127
- const result = await openaiImage.generateImage(prompt3, {
34128
- model: openaiModel,
34129
- size: options.size,
34130
- quality: options.quality,
34131
- style: options.style,
34132
- n: parseInt(options.count)
34133
- });
34022
+ const { result, modelLabel } = await executeOpenAIImageGenerate(prompt3, options, { apiKey });
34134
34023
  if (!result.success || !result.images) {
34135
34024
  spinner2.fail(result.error || "Image generation failed");
34136
34025
  exitWithError(apiError(result.error || "Image generation failed", true));
34137
34026
  }
34138
- const modelLabel = openaiModel === "gpt-image-2" ? "GPT Image 2" : "GPT Image 1.5";
34139
34027
  spinner2.succeed(source_default.green(`Generated ${result.images.length} image(s) with OpenAI ${modelLabel}`));
34140
34028
  if (isJsonMode()) {
34141
34029
  const outputPath = options.output ? resolve18(process.cwd(), options.output) : void 0;
@@ -49201,7 +49089,7 @@ function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, so
49201
49089
  return args;
49202
49090
  }
49203
49091
  function runFFmpegProcess(ffmpegPath, args, onProgress) {
49204
- return new Promise((resolve53, reject) => {
49092
+ return new Promise((resolve54, reject) => {
49205
49093
  const ffmpeg = spawn(ffmpegPath, args, {
49206
49094
  stdio: ["pipe", "pipe", "pipe"]
49207
49095
  });
@@ -49225,7 +49113,7 @@ function runFFmpegProcess(ffmpegPath, args, onProgress) {
49225
49113
  });
49226
49114
  ffmpeg.on("close", (code) => {
49227
49115
  if (code === 0) {
49228
- resolve53();
49116
+ resolve54();
49229
49117
  } else {
49230
49118
  const errorMatch = stderr.match(/Error.*$/m);
49231
49119
  const errorMsg = errorMatch ? errorMatch[0] : `FFmpeg exited with code ${code}`;
@@ -49361,17 +49249,17 @@ function getPresetSettings(preset, aspectRatio) {
49361
49249
  }
49362
49250
  async function runHyperframesExport(projectPath, options, spinner2) {
49363
49251
  spinner2.text = "Loading project...";
49364
- const { readFile: readFile43 } = await import("node:fs/promises");
49365
- 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");
49366
49254
  const { Project: Project2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
49367
49255
  const { createHyperframesBackend: createHyperframesBackend2 } = await Promise.resolve().then(() => (init_hyperframes(), hyperframes_exports));
49368
49256
  const { exitWithError: exitWithError2, generalError: generalError2, outputResult: outputResult2 } = await Promise.resolve().then(() => (init_output(), output_exports));
49369
49257
  const chalk2 = (await Promise.resolve().then(() => (init_source(), source_exports))).default;
49370
- const filePath = resolve53(process.cwd(), projectPath);
49371
- const content = await readFile43(filePath, "utf-8");
49258
+ const filePath = resolve54(process.cwd(), projectPath);
49259
+ const content = await readFile44(filePath, "utf-8");
49372
49260
  const project = Project2.fromJSON(JSON.parse(content));
49373
49261
  const state = project.getState();
49374
- 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"}`);
49375
49263
  const quality = ["draft", "standard", "high"].includes(options.preset ?? "") ? options.preset : "standard";
49376
49264
  const backend = createHyperframesBackend2();
49377
49265
  spinner2.text = "Rendering with Hyperframes...";
@@ -51900,6 +51788,1295 @@ var init_ai_audio = __esm({
51900
51788
  }
51901
51789
  });
51902
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
+
51903
53080
  // src/agent/adapters/openai.ts
51904
53081
  var openai_exports = {};
51905
53082
  __export(openai_exports, {
@@ -52001,7 +53178,7 @@ var claude_exports = {};
52001
53178
  __export(claude_exports, {
52002
53179
  ClaudeAdapter: () => ClaudeAdapter
52003
53180
  });
52004
- import Anthropic from "@anthropic-ai/sdk";
53181
+ import Anthropic2 from "@anthropic-ai/sdk";
52005
53182
  var ClaudeAdapter;
52006
53183
  var init_claude2 = __esm({
52007
53184
  "src/agent/adapters/claude.ts"() {
@@ -52011,7 +53188,7 @@ var init_claude2 = __esm({
52011
53188
  client = null;
52012
53189
  model = "claude-sonnet-4-6";
52013
53190
  async initialize(apiKey) {
52014
- this.client = new Anthropic({ apiKey });
53191
+ this.client = new Anthropic2({ apiKey });
52015
53192
  }
52016
53193
  isInitialized() {
52017
53194
  return this.client !== null;
@@ -52630,14 +53807,14 @@ var project_exports = {};
52630
53807
  __export(project_exports, {
52631
53808
  registerProjectTools: () => registerProjectTools
52632
53809
  });
52633
- import { readFile as readFile36, writeFile as writeFile32, stat as stat7 } from "node:fs/promises";
52634
- 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";
52635
53812
  async function resolveProjectPath3(inputPath, cwd) {
52636
- const filePath = resolve43(cwd, inputPath);
53813
+ const filePath = resolve44(cwd, inputPath);
52637
53814
  try {
52638
53815
  const stats = await stat7(filePath);
52639
53816
  if (stats.isDirectory()) {
52640
- return resolve43(filePath, "project.vibe.json");
53817
+ return resolve44(filePath, "project.vibe.json");
52641
53818
  }
52642
53819
  } catch {
52643
53820
  }
@@ -52760,9 +53937,9 @@ var init_project2 = __esm({
52760
53937
  const project = new Project(name);
52761
53938
  project.setAspectRatio(aspectRatio);
52762
53939
  project.setFrameRate(fps);
52763
- const outputPath = resolve43(context.workingDirectory, output);
53940
+ const outputPath = resolve44(context.workingDirectory, output);
52764
53941
  const data = JSON.stringify(project.toJSON(), null, 2);
52765
- await writeFile32(outputPath, data, "utf-8");
53942
+ await writeFile33(outputPath, data, "utf-8");
52766
53943
  context.projectPath = outputPath;
52767
53944
  return {
52768
53945
  toolCallId: "",
@@ -52785,7 +53962,7 @@ Frame Rate: ${fps} fps`
52785
53962
  const path2 = args.path || args.project;
52786
53963
  try {
52787
53964
  const filePath = await resolveProjectPath3(path2, context.workingDirectory);
52788
- const content = await readFile36(filePath, "utf-8");
53965
+ const content = await readFile37(filePath, "utf-8");
52789
53966
  const data = JSON.parse(content);
52790
53967
  const project = Project.fromJSON(data);
52791
53968
  const summary = project.getSummary();
@@ -52820,7 +53997,7 @@ Frame Rate: ${fps} fps`
52820
53997
  const updates = [];
52821
53998
  try {
52822
53999
  const filePath = await resolveProjectPath3(path2, context.workingDirectory);
52823
- const content = await readFile36(filePath, "utf-8");
54000
+ const content = await readFile37(filePath, "utf-8");
52824
54001
  const data = JSON.parse(content);
52825
54002
  const project = Project.fromJSON(data);
52826
54003
  if (args.name) {
@@ -52835,7 +54012,7 @@ Frame Rate: ${fps} fps`
52835
54012
  project.setFrameRate(args.fps);
52836
54013
  updates.push(`Frame Rate: ${args.fps} fps`);
52837
54014
  }
52838
- await writeFile32(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
54015
+ await writeFile33(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
52839
54016
  return {
52840
54017
  toolCallId: "",
52841
54018
  success: true,
@@ -52855,7 +54032,7 @@ ${updates.join("\n")}`
52855
54032
  const path2 = args.path || args.project;
52856
54033
  try {
52857
54034
  const filePath = await resolveProjectPath3(path2, context.workingDirectory);
52858
- const content = await readFile36(filePath, "utf-8");
54035
+ const content = await readFile37(filePath, "utf-8");
52859
54036
  const data = JSON.parse(content);
52860
54037
  const project = Project.fromJSON(data);
52861
54038
  context.projectPath = filePath;
@@ -52889,10 +54066,10 @@ Duration: ${summary.duration.toFixed(1)}s`
52889
54066
  }
52890
54067
  try {
52891
54068
  const filePath = await resolveProjectPath3(path2, context.workingDirectory);
52892
- const content = await readFile36(filePath, "utf-8");
54069
+ const content = await readFile37(filePath, "utf-8");
52893
54070
  const data = JSON.parse(content);
52894
54071
  const project = Project.fromJSON(data);
52895
- await writeFile32(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
54072
+ await writeFile33(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
52896
54073
  return {
52897
54074
  toolCallId: "",
52898
54075
  success: true,
@@ -52915,8 +54092,8 @@ var timeline_exports = {};
52915
54092
  __export(timeline_exports, {
52916
54093
  registerTimelineTools: () => registerTimelineTools
52917
54094
  });
52918
- import { readFile as readFile37, writeFile as writeFile33, stat as stat8 } from "node:fs/promises";
52919
- 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";
52920
54097
  function detectMediaType4(path2) {
52921
54098
  const ext = extname15(path2).toLowerCase();
52922
54099
  const videoExts = [".mp4", ".mov", ".webm", ".avi", ".mkv"];
@@ -52939,11 +54116,11 @@ async function getMediaDuration2(filePath, mediaType) {
52939
54116
  }
52940
54117
  }
52941
54118
  async function resolveProjectPath4(inputPath, cwd) {
52942
- const filePath = resolve44(cwd, inputPath);
54119
+ const filePath = resolve45(cwd, inputPath);
52943
54120
  try {
52944
54121
  const stats = await stat8(filePath);
52945
54122
  if (stats.isDirectory()) {
52946
- return resolve44(filePath, "project.vibe.json");
54123
+ return resolve45(filePath, "project.vibe.json");
52947
54124
  }
52948
54125
  } catch {
52949
54126
  }
@@ -52951,13 +54128,13 @@ async function resolveProjectPath4(inputPath, cwd) {
52951
54128
  }
52952
54129
  async function loadProject(path2, cwd) {
52953
54130
  const filePath = await resolveProjectPath4(path2, cwd);
52954
- const content = await readFile37(filePath, "utf-8");
54131
+ const content = await readFile38(filePath, "utf-8");
52955
54132
  const data = JSON.parse(content);
52956
54133
  const project = Project.fromJSON(data);
52957
54134
  return { project, filePath };
52958
54135
  }
52959
54136
  async function saveProject(project, filePath) {
52960
- await writeFile33(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
54137
+ await writeFile34(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
52961
54138
  }
52962
54139
  function registerTimelineTools(registry) {
52963
54140
  registry.register(addSourceDef, addSource);
@@ -53262,7 +54439,7 @@ var init_timeline = __esm({
53262
54439
  const mediaPath = args.media;
53263
54440
  try {
53264
54441
  const { project, filePath } = await loadProject(projectPath, context.workingDirectory);
53265
- const absMediaPath = resolve44(context.workingDirectory, mediaPath);
54442
+ const absMediaPath = resolve45(context.workingDirectory, mediaPath);
53266
54443
  const mediaName = args.name || basename25(mediaPath);
53267
54444
  const mediaType = args.type || detectMediaType4(mediaPath);
53268
54445
  const duration = args.duration || await getMediaDuration2(absMediaPath, mediaType);
@@ -53780,8 +54957,8 @@ var filesystem_exports = {};
53780
54957
  __export(filesystem_exports, {
53781
54958
  registerFilesystemTools: () => registerFilesystemTools
53782
54959
  });
53783
- import { readFile as readFile38, writeFile as writeFile34, readdir as readdir5, stat as stat9, access as access8 } from "node:fs/promises";
53784
- 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";
53785
54962
  function matchPattern(filename, pattern) {
53786
54963
  const regex2 = new RegExp(
53787
54964
  "^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".") + "$",
@@ -53877,14 +55054,14 @@ var init_filesystem = __esm({
53877
55054
  const dirPath = args.path || ".";
53878
55055
  const pattern = args.pattern;
53879
55056
  try {
53880
- const absPath = resolve45(context.workingDirectory, dirPath);
55057
+ const absPath = resolve46(context.workingDirectory, dirPath);
53881
55058
  const entries = await readdir5(absPath, { withFileTypes: true });
53882
55059
  const results = [];
53883
55060
  for (const entry of entries) {
53884
55061
  if (pattern && !matchPattern(entry.name, pattern)) {
53885
55062
  continue;
53886
55063
  }
53887
- const fullPath = join7(absPath, entry.name);
55064
+ const fullPath = join9(absPath, entry.name);
53888
55065
  const stats = await stat9(fullPath);
53889
55066
  if (entry.isDirectory()) {
53890
55067
  results.push(`[DIR] ${entry.name}/`);
@@ -53917,8 +55094,8 @@ ${results.join("\n")}`
53917
55094
  read2 = async (args, context) => {
53918
55095
  const filePath = args.path;
53919
55096
  try {
53920
- const absPath = resolve45(context.workingDirectory, filePath);
53921
- const content = await readFile38(absPath, "utf-8");
55097
+ const absPath = resolve46(context.workingDirectory, filePath);
55098
+ const content = await readFile39(absPath, "utf-8");
53922
55099
  const maxLength = 4e3;
53923
55100
  const truncated = content.length > maxLength;
53924
55101
  const output = truncated ? content.substring(0, maxLength) + "\n... (truncated)" : content;
@@ -53941,8 +55118,8 @@ ${output}`
53941
55118
  const filePath = args.path;
53942
55119
  const content = args.content;
53943
55120
  try {
53944
- const absPath = resolve45(context.workingDirectory, filePath);
53945
- await writeFile34(absPath, content, "utf-8");
55121
+ const absPath = resolve46(context.workingDirectory, filePath);
55122
+ await writeFile35(absPath, content, "utf-8");
53946
55123
  return {
53947
55124
  toolCallId: "",
53948
55125
  success: true,
@@ -53960,7 +55137,7 @@ ${output}`
53960
55137
  exists = async (args, context) => {
53961
55138
  const filePath = args.path;
53962
55139
  try {
53963
- const absPath = resolve45(context.workingDirectory, filePath);
55140
+ const absPath = resolve46(context.workingDirectory, filePath);
53964
55141
  await access8(absPath);
53965
55142
  const stats = await stat9(absPath);
53966
55143
  const type = stats.isDirectory() ? "directory" : "file";
@@ -53985,8 +55162,8 @@ var media_exports = {};
53985
55162
  __export(media_exports, {
53986
55163
  registerMediaTools: () => registerMediaTools
53987
55164
  });
53988
- import { readFile as readFile39, writeFile as writeFile35 } from "node:fs/promises";
53989
- 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";
53990
55167
  function formatTimestamp2(seconds) {
53991
55168
  const mins = Math.floor(seconds / 60);
53992
55169
  const secs = (seconds % 60).toFixed(2);
@@ -54125,7 +55302,7 @@ var init_media = __esm({
54125
55302
  mediaInfo = async (args, context) => {
54126
55303
  const mediaPath = args.path;
54127
55304
  try {
54128
- const absPath = resolve46(context.workingDirectory, mediaPath);
55305
+ const absPath = resolve47(context.workingDirectory, mediaPath);
54129
55306
  const { stdout } = await execSafe("ffprobe", ["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", absPath], { maxBuffer: 10 * 1024 * 1024 });
54130
55307
  const info = JSON.parse(stdout);
54131
55308
  const format = info.format || {};
@@ -54179,7 +55356,7 @@ Audio:`);
54179
55356
  const videoPath = args.video;
54180
55357
  const threshold = args.threshold || 0.3;
54181
55358
  try {
54182
- const absPath = resolve46(context.workingDirectory, videoPath);
55359
+ const absPath = resolve47(context.workingDirectory, videoPath);
54183
55360
  const { stdout, stderr } = await execSafe("ffmpeg", ["-i", absPath, "-filter:v", `select='gt(scene,${threshold})',showinfo`, "-f", "null", "-"], { maxBuffer: 50 * 1024 * 1024 });
54184
55361
  const output = stdout + stderr;
54185
55362
  const scenes = [];
@@ -54195,7 +55372,7 @@ Audio:`);
54195
55372
  return `[${i + 1}] ${formatTimestamp2(s.timestamp)} - ${formatTimestamp2(end)} (${(end - s.timestamp).toFixed(1)}s)`;
54196
55373
  }).join("\n");
54197
55374
  if (args.output) {
54198
- const outputPath = resolve46(context.workingDirectory, args.output);
55375
+ const outputPath = resolve47(context.workingDirectory, args.output);
54199
55376
  const result = {
54200
55377
  source: absPath,
54201
55378
  totalDuration,
@@ -54207,7 +55384,7 @@ Audio:`);
54207
55384
  duration: (i < scenes.length - 1 ? scenes[i + 1].timestamp : totalDuration) - s.timestamp
54208
55385
  }))
54209
55386
  };
54210
- await writeFile35(outputPath, JSON.stringify(result, null, 2), "utf-8");
55387
+ await writeFile36(outputPath, JSON.stringify(result, null, 2), "utf-8");
54211
55388
  }
54212
55389
  return {
54213
55390
  toolCallId: "",
@@ -54229,7 +55406,7 @@ ${sceneList}`
54229
55406
  const noise = args.noise || "-30";
54230
55407
  const duration = args.duration || 0.5;
54231
55408
  try {
54232
- const absPath = resolve46(context.workingDirectory, mediaPath);
55409
+ const absPath = resolve47(context.workingDirectory, mediaPath);
54233
55410
  const { stdout, stderr } = await execSafe("ffmpeg", ["-i", absPath, "-af", `silencedetect=noise=${noise}dB:d=${duration}`, "-f", "null", "-"], { maxBuffer: 50 * 1024 * 1024 });
54234
55411
  const output = stdout + stderr;
54235
55412
  const silences = [];
@@ -54255,8 +55432,8 @@ ${sceneList}`
54255
55432
  (s, idx) => `[${idx + 1}] ${formatTimestamp2(s.start)} - ${formatTimestamp2(s.end)} (${s.duration.toFixed(1)}s)`
54256
55433
  ).join("\n");
54257
55434
  if (args.output) {
54258
- const outputPath = resolve46(context.workingDirectory, args.output);
54259
- await writeFile35(
55435
+ const outputPath = resolve47(context.workingDirectory, args.output);
55436
+ await writeFile36(
54260
55437
  outputPath,
54261
55438
  JSON.stringify({ source: absPath, silences }, null, 2),
54262
55439
  "utf-8"
@@ -54280,7 +55457,7 @@ ${silenceList}` : "No silence periods detected."
54280
55457
  detectBeats = async (args, context) => {
54281
55458
  const audioPath = args.audio;
54282
55459
  try {
54283
- const absPath = resolve46(context.workingDirectory, audioPath);
55460
+ const absPath = resolve47(context.workingDirectory, audioPath);
54284
55461
  const totalDuration = await ffprobeDuration(absPath);
54285
55462
  const estimatedBPM = 120;
54286
55463
  const beatInterval = 60 / estimatedBPM;
@@ -54292,8 +55469,8 @@ ${silenceList}` : "No silence periods detected."
54292
55469
  (t, i) => `[${i + 1}] ${formatTimestamp2(t)}`
54293
55470
  ).join("\n");
54294
55471
  if (args.output) {
54295
- const outputPath = resolve46(context.workingDirectory, args.output);
54296
- await writeFile35(
55472
+ const outputPath = resolve47(context.workingDirectory, args.output);
55473
+ await writeFile36(
54297
55474
  outputPath,
54298
55475
  JSON.stringify({ source: absPath, beatCount: beats.length, beats }, null, 2),
54299
55476
  "utf-8"
@@ -54328,8 +55505,8 @@ ${beatList}${beats.length > 20 ? `
54328
55505
  error: "OpenAI API key required for transcription. Set OPENAI_API_KEY or configure via 'vibe setup'."
54329
55506
  };
54330
55507
  }
54331
- const absPath = resolve46(context.workingDirectory, audioPath);
54332
- const audioBuffer = await readFile39(absPath);
55508
+ const absPath = resolve47(context.workingDirectory, audioPath);
55509
+ const audioBuffer = await readFile40(absPath);
54333
55510
  const { WhisperProvider: WhisperProvider2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
54334
55511
  const whisper = new WhisperProvider2();
54335
55512
  await whisper.initialize({ apiKey });
@@ -54357,11 +55534,11 @@ ${result.fullText}`;
54357
55534
  }
54358
55535
  }
54359
55536
  if (args.output) {
54360
- const outputPath = resolve46(context.workingDirectory, args.output);
55537
+ const outputPath = resolve47(context.workingDirectory, args.output);
54361
55538
  const { detectFormat: detectFormat2, formatTranscript: formatTranscript2 } = await Promise.resolve().then(() => (init_subtitle(), subtitle_exports));
54362
55539
  const format = detectFormat2(args.output, void 0);
54363
55540
  const content = formatTranscript2(result, format);
54364
- await writeFile35(outputPath, content, "utf-8");
55541
+ await writeFile36(outputPath, content, "utf-8");
54365
55542
  output += `
54366
55543
 
54367
55544
  Saved to: ${args.output}`;
@@ -54460,10 +55637,10 @@ Saved to: ${args.output}`;
54460
55637
  const inputPath = args.input;
54461
55638
  const quality = args.quality || "medium";
54462
55639
  try {
54463
- const absInput = resolve46(context.workingDirectory, inputPath);
55640
+ const absInput = resolve47(context.workingDirectory, inputPath);
54464
55641
  const ext = inputPath.split(".").pop() || "mp4";
54465
55642
  const baseName = inputPath.replace(/\.[^/.]+$/, "");
54466
- 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}`);
54467
55644
  const crfValues = {
54468
55645
  low: 28,
54469
55646
  medium: 23,
@@ -54471,8 +55648,8 @@ Saved to: ${args.output}`;
54471
55648
  };
54472
55649
  const crf = crfValues[quality] || 23;
54473
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 });
54474
- const inputStats = await readFile39(absInput);
54475
- const outputStats = await readFile39(outputPath);
55651
+ const inputStats = await readFile40(absInput);
55652
+ const outputStats = await readFile40(outputPath);
54476
55653
  const inputSize = inputStats.length;
54477
55654
  const outputSize = outputStats.length;
54478
55655
  const reduction = ((inputSize - outputSize) / inputSize * 100).toFixed(1);
@@ -54497,8 +55674,8 @@ Size: ${formatSize2(inputSize)} \u2192 ${formatSize2(outputSize)} (${reduction}%
54497
55674
  const codec = args.codec;
54498
55675
  const audioCodec = args.audioCodec;
54499
55676
  try {
54500
- const absInput = resolve46(context.workingDirectory, inputPath);
54501
- const absOutput = resolve46(context.workingDirectory, outputPath);
55677
+ const absInput = resolve47(context.workingDirectory, inputPath);
55678
+ const absOutput = resolve47(context.workingDirectory, outputPath);
54502
55679
  let videoCodecName = "copy";
54503
55680
  let audioCodecName = "copy";
54504
55681
  if (codec) {
@@ -54546,19 +55723,19 @@ Size: ${formatSize2(inputSize)} \u2192 ${formatSize2(outputSize)} (${reduction}%
54546
55723
  };
54547
55724
  }
54548
55725
  try {
54549
- const absOutput = resolve46(context.workingDirectory, outputPath);
55726
+ const absOutput = resolve47(context.workingDirectory, outputPath);
54550
55727
  if (reencode) {
54551
55728
  const ffmpegArgs = [];
54552
55729
  for (const i of inputs) {
54553
- ffmpegArgs.push("-i", resolve46(context.workingDirectory, i));
55730
+ ffmpegArgs.push("-i", resolve47(context.workingDirectory, i));
54554
55731
  }
54555
55732
  const filterComplex = inputs.map((_, i) => `[${i}:v][${i}:a]`).join("");
54556
55733
  ffmpegArgs.push("-filter_complex", `${filterComplex}concat=n=${inputs.length}:v=1:a=1[outv][outa]`, "-map", "[outv]", "-map", "[outa]", absOutput, "-y");
54557
55734
  await execSafe("ffmpeg", ffmpegArgs, { maxBuffer: 100 * 1024 * 1024 });
54558
55735
  } else {
54559
- const tempList = resolve46(context.workingDirectory, `concat-list-${Date.now()}.txt`);
54560
- const listContent = inputs.map((i) => `file '${resolve46(context.workingDirectory, i)}'`).join("\n");
54561
- 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");
54562
55739
  await execSafe("ffmpeg", ["-f", "concat", "-safe", "0", "-i", tempList, "-c", "copy", absOutput, "-y"], { maxBuffer: 100 * 1024 * 1024 });
54563
55740
  const { unlink: unlink3 } = await import("node:fs/promises");
54564
55741
  await unlink3(tempList).catch(() => {
@@ -54582,8 +55759,8 @@ Size: ${formatSize2(inputSize)} \u2192 ${formatSize2(outputSize)} (${reduction}%
54582
55759
  });
54583
55760
 
54584
55761
  // src/agent/tools/ai-generation.ts
54585
- import { writeFile as writeFile36, readFile as readFile40 } from "node:fs/promises";
54586
- 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";
54587
55764
  function getTimestamp() {
54588
55765
  return Date.now().toString();
54589
55766
  }
@@ -54852,7 +56029,7 @@ var init_ai_generation = __esm({
54852
56029
  error: `API key required for ${provider}. Configure via 'vibe setup'.`
54853
56030
  };
54854
56031
  }
54855
- const outputPath = resolve47(context.workingDirectory, output);
56032
+ const outputPath = resolve48(context.workingDirectory, output);
54856
56033
  if (provider === "dalle" || provider === "openai") {
54857
56034
  const { OpenAIImageProvider: OpenAIImageProvider2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
54858
56035
  const openaiImage = new OpenAIImageProvider2();
@@ -54883,7 +56060,7 @@ var init_ai_generation = __esm({
54883
56060
  error: "Image generated but no URL or base64 data returned"
54884
56061
  };
54885
56062
  }
54886
- await writeFile36(outputPath, buffer);
56063
+ await writeFile37(outputPath, buffer);
54887
56064
  } else if (provider === "gemini") {
54888
56065
  const { GeminiProvider: GeminiProvider2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
54889
56066
  const gemini = new GeminiProvider2();
@@ -54900,7 +56077,7 @@ var init_ai_generation = __esm({
54900
56077
  const image = result.images[0];
54901
56078
  if (image.base64) {
54902
56079
  const buffer = Buffer.from(image.base64, "base64");
54903
- await writeFile36(outputPath, buffer);
56080
+ await writeFile37(outputPath, buffer);
54904
56081
  }
54905
56082
  } else if (provider === "grok") {
54906
56083
  const { GrokProvider: GrokProvider2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
@@ -54919,10 +56096,10 @@ var init_ai_generation = __esm({
54919
56096
  if (image.url) {
54920
56097
  const response = await fetch(image.url);
54921
56098
  const buffer = Buffer.from(await response.arrayBuffer());
54922
- await writeFile36(outputPath, buffer);
56099
+ await writeFile37(outputPath, buffer);
54923
56100
  } else if (image.base64) {
54924
56101
  const buffer = Buffer.from(image.base64, "base64");
54925
- await writeFile36(outputPath, buffer);
56102
+ await writeFile37(outputPath, buffer);
54926
56103
  }
54927
56104
  }
54928
56105
  return {
@@ -54958,8 +56135,8 @@ Size: ${size}`
54958
56135
  };
54959
56136
  }
54960
56137
  async function prepareReferenceImage(imgPath) {
54961
- const absImagePath = resolve47(context.workingDirectory, imgPath);
54962
- const imageBuffer = await readFile40(absImagePath);
56138
+ const absImagePath = resolve48(context.workingDirectory, imgPath);
56139
+ const imageBuffer = await readFile41(absImagePath);
54963
56140
  const base64 = imageBuffer.toString("base64");
54964
56141
  const ext = imgPath.split(".").pop()?.toLowerCase() || "png";
54965
56142
  const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : `image/${ext}`;
@@ -55013,9 +56190,9 @@ Size: ${size}`
55013
56190
  };
55014
56191
  }
55015
56192
  if (finalResult.videoUrl) {
55016
- const outputPath = resolve47(context.workingDirectory, output);
56193
+ const outputPath = resolve48(context.workingDirectory, output);
55017
56194
  const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
55018
- await writeFile36(outputPath, buffer);
56195
+ await writeFile37(outputPath, buffer);
55019
56196
  }
55020
56197
  }
55021
56198
  return {
@@ -55074,9 +56251,9 @@ Duration: ${duration}s`
55074
56251
  };
55075
56252
  }
55076
56253
  if (finalResult.videoUrl) {
55077
- const outputPath = resolve47(context.workingDirectory, output);
56254
+ const outputPath = resolve48(context.workingDirectory, output);
55078
56255
  const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
55079
- await writeFile36(outputPath, buffer);
56256
+ await writeFile37(outputPath, buffer);
55080
56257
  }
55081
56258
  }
55082
56259
  return {
@@ -55130,9 +56307,9 @@ Duration: ${duration}s`
55130
56307
  };
55131
56308
  }
55132
56309
  if (finalResult.videoUrl) {
55133
- const outputPath = resolve47(context.workingDirectory, output);
56310
+ const outputPath = resolve48(context.workingDirectory, output);
55134
56311
  const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
55135
- await writeFile36(outputPath, buffer);
56312
+ await writeFile37(outputPath, buffer);
55136
56313
  }
55137
56314
  }
55138
56315
  return {
@@ -55215,9 +56392,9 @@ Duration: ${duration}s`
55215
56392
  };
55216
56393
  }
55217
56394
  if (finalResult.videoUrl) {
55218
- const outputPath = resolve47(context.workingDirectory, output);
56395
+ const outputPath = resolve48(context.workingDirectory, output);
55219
56396
  const buffer = await downloadVideo(finalResult.videoUrl, apiKey);
55220
- await writeFile36(outputPath, buffer);
56397
+ await writeFile37(outputPath, buffer);
55221
56398
  }
55222
56399
  }
55223
56400
  return {
@@ -55267,8 +56444,8 @@ Mode: ${mode}`
55267
56444
  error: `TTS generation failed: ${result.error || "No audio generated"}`
55268
56445
  };
55269
56446
  }
55270
- const outputPath = resolve47(context.workingDirectory, output);
55271
- await writeFile36(outputPath, result.audioBuffer);
56447
+ const outputPath = resolve48(context.workingDirectory, output);
56448
+ await writeFile37(outputPath, result.audioBuffer);
55272
56449
  return {
55273
56450
  toolCallId: "",
55274
56451
  success: true,
@@ -55312,8 +56489,8 @@ Text: ${text.substring(0, 100)}${text.length > 100 ? "..." : ""}`
55312
56489
  error: `SFX generation failed: ${result.error || "No audio generated"}`
55313
56490
  };
55314
56491
  }
55315
- const outputPath = resolve47(context.workingDirectory, output);
55316
- await writeFile36(outputPath, result.audioBuffer);
56492
+ const outputPath = resolve48(context.workingDirectory, output);
56493
+ await writeFile37(outputPath, result.audioBuffer);
55317
56494
  return {
55318
56495
  toolCallId: "",
55319
56496
  success: true,
@@ -55361,8 +56538,8 @@ Prompt: ${prompt3}`
55361
56538
  error: `Music generation failed: ${result.error || "No audio generated"}`
55362
56539
  };
55363
56540
  }
55364
- const outputPath = resolve47(context.workingDirectory, output);
55365
- await writeFile36(outputPath, result.audioBuffer);
56541
+ const outputPath = resolve48(context.workingDirectory, output);
56542
+ await writeFile37(outputPath, result.audioBuffer);
55366
56543
  return {
55367
56544
  toolCallId: "",
55368
56545
  success: true,
@@ -55414,10 +56591,10 @@ Provider: ElevenLabs${instrumental ? "\nMode: Instrumental" : ""}`
55414
56591
  }
55415
56592
  }
55416
56593
  if (finalResult.audioUrl) {
55417
- const outputPath = resolve47(context.workingDirectory, output);
56594
+ const outputPath = resolve48(context.workingDirectory, output);
55418
56595
  const response = await fetch(finalResult.audioUrl);
55419
56596
  const buffer = Buffer.from(await response.arrayBuffer());
55420
- await writeFile36(outputPath, buffer);
56597
+ await writeFile37(outputPath, buffer);
55421
56598
  } else {
55422
56599
  return {
55423
56600
  toolCallId: "",
@@ -55472,8 +56649,8 @@ Provider: Replicate`
55472
56649
  error: "Failed to generate storyboard"
55473
56650
  };
55474
56651
  }
55475
- const outputPath = resolve47(context.workingDirectory, output);
55476
- 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");
55477
56654
  const sanitizedResult = sanitizeAIResult(result);
55478
56655
  const summary = sanitizedResult.map(
55479
56656
  (scene, i) => `Scene ${i + 1}: ${scene.description.substring(0, 60)}...`
@@ -55497,8 +56674,8 @@ ${summary}`
55497
56674
  generateMotion3 = async (args, context) => {
55498
56675
  try {
55499
56676
  const { executeMotion: executeMotion2 } = await Promise.resolve().then(() => (init_ai_motion(), ai_motion_exports));
55500
- const video = args.video ? resolve47(context.workingDirectory, args.video) : void 0;
55501
- 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;
55502
56679
  const result = await executeMotion2({
55503
56680
  description: args.description,
55504
56681
  duration: args.duration,
@@ -55540,8 +56717,8 @@ ${summary}`
55540
56717
  });
55541
56718
 
55542
56719
  // src/agent/tools/ai-pipeline.ts
55543
- import { writeFile as writeFile37, readFile as readFile41 } from "node:fs/promises";
55544
- 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";
55545
56722
  function getTimestamp2() {
55546
56723
  return Date.now().toString();
55547
56724
  }
@@ -55954,7 +57131,7 @@ This tool re-creates videos for failed scenes using image-to-video (if ImgBB key
55954
57131
  };
55955
57132
  scriptToVideoHandler = async (args, context) => {
55956
57133
  const script = args.script;
55957
- 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");
55958
57135
  try {
55959
57136
  const result = await executeScriptToVideo({
55960
57137
  script,
@@ -56026,9 +57203,9 @@ This tool re-creates videos for failed scenes using image-to-video (if ImgBB key
56026
57203
  }
56027
57204
  };
56028
57205
  highlightsHandler = async (args, context) => {
56029
- const media = resolve48(context.workingDirectory, args.media);
56030
- const output = args.output ? resolve48(context.workingDirectory, args.output) : void 0;
56031
- 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;
56032
57209
  try {
56033
57210
  const result = await executeHighlights({
56034
57211
  media,
@@ -56090,8 +57267,8 @@ This tool re-creates videos for failed scenes using image-to-video (if ImgBB key
56090
57267
  }
56091
57268
  };
56092
57269
  autoShortsHandler = async (args, context) => {
56093
- const video = resolve48(context.workingDirectory, args.video);
56094
- 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;
56095
57272
  try {
56096
57273
  const result = await executeAutoShorts({
56097
57274
  video,
@@ -56155,7 +57332,7 @@ This tool re-creates videos for failed scenes using image-to-video (if ImgBB key
56155
57332
  geminiVideoHandler = async (args, context) => {
56156
57333
  let source = args.source;
56157
57334
  if (!source.includes("youtube.com") && !source.includes("youtu.be")) {
56158
- source = resolve48(context.workingDirectory, source);
57335
+ source = resolve49(context.workingDirectory, source);
56159
57336
  }
56160
57337
  try {
56161
57338
  const result = await executeGeminiVideo({
@@ -56203,7 +57380,7 @@ This tool re-creates videos for failed scenes using image-to-video (if ImgBB key
56203
57380
  analyzeHandler = async (args, context) => {
56204
57381
  let source = args.source;
56205
57382
  if (!source.startsWith("http://") && !source.startsWith("https://")) {
56206
- source = resolve48(context.workingDirectory, source);
57383
+ source = resolve49(context.workingDirectory, source);
56207
57384
  }
56208
57385
  try {
56209
57386
  const result = await executeAnalyze({
@@ -56305,8 +57482,8 @@ This tool re-creates videos for failed scenes using image-to-video (if ImgBB key
56305
57482
  }
56306
57483
  const imageBuffers = [];
56307
57484
  for (const imagePath of images) {
56308
- const absPath = resolve48(context.workingDirectory, imagePath);
56309
- const buffer = await readFile41(absPath);
57485
+ const absPath = resolve49(context.workingDirectory, imagePath);
57486
+ const buffer = await readFile42(absPath);
56310
57487
  imageBuffers.push(buffer);
56311
57488
  }
56312
57489
  let result;
@@ -56343,14 +57520,14 @@ This tool re-creates videos for failed scenes using image-to-video (if ImgBB key
56343
57520
  };
56344
57521
  }
56345
57522
  const img = result.images[0];
56346
- const outputPath = resolve48(context.workingDirectory, output);
57523
+ const outputPath = resolve49(context.workingDirectory, output);
56347
57524
  if (img.base64) {
56348
57525
  const buffer = Buffer.from(img.base64, "base64");
56349
- await writeFile37(outputPath, buffer);
57526
+ await writeFile38(outputPath, buffer);
56350
57527
  } else if (img.url) {
56351
57528
  const resp = await fetch(img.url);
56352
57529
  const arrayBuf = await resp.arrayBuffer();
56353
- await writeFile37(outputPath, Buffer.from(arrayBuf));
57530
+ await writeFile38(outputPath, Buffer.from(arrayBuf));
56354
57531
  }
56355
57532
  return {
56356
57533
  toolCallId: "",
@@ -56417,8 +57594,8 @@ Failed scenes: ${result.failedScenes.join(", ")}`;
56417
57594
  };
56418
57595
  };
56419
57596
  animatedCaptionHandler = async (args, context) => {
56420
- const videoPath = resolve48(context.workingDirectory, args.videoPath);
56421
- const outputPath = resolve48(context.workingDirectory, args.outputPath);
57597
+ const videoPath = resolve49(context.workingDirectory, args.videoPath);
57598
+ const outputPath = resolve49(context.workingDirectory, args.outputPath);
56422
57599
  try {
56423
57600
  const result = await executeAnimatedCaption({
56424
57601
  videoPath,
@@ -56463,7 +57640,7 @@ Tier: ${result.tier}`
56463
57640
  });
56464
57641
 
56465
57642
  // src/agent/tools/ai-editing.ts
56466
- import { resolve as resolve49 } from "node:path";
57643
+ import { resolve as resolve50 } from "node:path";
56467
57644
  function registerEditingTools(registry) {
56468
57645
  registry.register(textOverlayDef, textOverlayHandler);
56469
57646
  registry.register(reviewDef, reviewHandler);
@@ -56892,10 +58069,10 @@ var init_ai_editing = __esm({
56892
58069
  };
56893
58070
  };
56894
58071
  silenceCutHandler = async (args, context) => {
56895
- const videoPath = resolve49(context.workingDirectory, args.videoPath);
58072
+ const videoPath = resolve50(context.workingDirectory, args.videoPath);
56896
58073
  const ext = videoPath.split(".").pop() || "mp4";
56897
58074
  const name = videoPath.replace(/\.[^.]+$/, "");
56898
- 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}`;
56899
58076
  try {
56900
58077
  const result = await executeSilenceCut({
56901
58078
  videoPath,
@@ -56940,10 +58117,10 @@ var init_ai_editing = __esm({
56940
58117
  }
56941
58118
  };
56942
58119
  jumpCutHandler = async (args, context) => {
56943
- const videoPath = resolve49(context.workingDirectory, args.videoPath);
58120
+ const videoPath = resolve50(context.workingDirectory, args.videoPath);
56944
58121
  const ext = videoPath.split(".").pop() || "mp4";
56945
58122
  const name = videoPath.replace(/\.[^.]+$/, "");
56946
- 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}`;
56947
58124
  try {
56948
58125
  const result = await executeJumpCut({
56949
58126
  videoPath,
@@ -56991,10 +58168,10 @@ var init_ai_editing = __esm({
56991
58168
  }
56992
58169
  };
56993
58170
  captionHandler = async (args, context) => {
56994
- const videoPath = resolve49(context.workingDirectory, args.videoPath);
58171
+ const videoPath = resolve50(context.workingDirectory, args.videoPath);
56995
58172
  const ext = videoPath.split(".").pop() || "mp4";
56996
58173
  const name = videoPath.replace(/\.[^.]+$/, "");
56997
- 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}`;
56998
58175
  try {
56999
58176
  const result = await executeCaption({
57000
58177
  videoPath,
@@ -57034,10 +58211,10 @@ var init_ai_editing = __esm({
57034
58211
  }
57035
58212
  };
57036
58213
  noiseReduceHandler = async (args, context) => {
57037
- const inputPath = resolve49(context.workingDirectory, args.inputPath);
58214
+ const inputPath = resolve50(context.workingDirectory, args.inputPath);
57038
58215
  const ext = inputPath.split(".").pop() || "mp4";
57039
58216
  const name = inputPath.replace(/\.[^.]+$/, "");
57040
- 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}`;
57041
58218
  try {
57042
58219
  const result = await executeNoiseReduce({
57043
58220
  inputPath,
@@ -57071,10 +58248,10 @@ var init_ai_editing = __esm({
57071
58248
  }
57072
58249
  };
57073
58250
  fadeHandler = async (args, context) => {
57074
- const videoPath = resolve49(context.workingDirectory, args.videoPath);
58251
+ const videoPath = resolve50(context.workingDirectory, args.videoPath);
57075
58252
  const ext = videoPath.split(".").pop() || "mp4";
57076
58253
  const name = videoPath.replace(/\.[^.]+$/, "");
57077
- 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}`;
57078
58255
  try {
57079
58256
  const result = await executeFade({
57080
58257
  videoPath,
@@ -57112,9 +58289,9 @@ var init_ai_editing = __esm({
57112
58289
  }
57113
58290
  };
57114
58291
  thumbnailBestFrameHandler = async (args, context) => {
57115
- const videoPath = resolve49(context.workingDirectory, args.videoPath);
58292
+ const videoPath = resolve50(context.workingDirectory, args.videoPath);
57116
58293
  const name = videoPath.replace(/\.[^.]+$/, "");
57117
- 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`;
57118
58295
  try {
57119
58296
  const result = await executeThumbnailBestFrame({
57120
58297
  videoPath,
@@ -57149,11 +58326,11 @@ var init_ai_editing = __esm({
57149
58326
  }
57150
58327
  };
57151
58328
  translateSrtHandler = async (args, context) => {
57152
- const srtPath = resolve49(context.workingDirectory, args.srtPath);
58329
+ const srtPath = resolve50(context.workingDirectory, args.srtPath);
57153
58330
  const target = args.targetLanguage;
57154
58331
  const ext = srtPath.split(".").pop() || "srt";
57155
58332
  const name = srtPath.replace(/\.[^.]+$/, "");
57156
- 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}`;
57157
58334
  try {
57158
58335
  const result = await executeTranslateSrt({
57159
58336
  srtPath,
@@ -57215,7 +58392,7 @@ var export_exports = {};
57215
58392
  __export(export_exports, {
57216
58393
  registerExportTools: () => registerExportTools
57217
58394
  });
57218
- import { resolve as resolve50, basename as basename26 } from "node:path";
58395
+ import { resolve as resolve51, basename as basename26 } from "node:path";
57219
58396
  function registerExportTools(registry) {
57220
58397
  registry.register(exportVideoDef, exportVideo);
57221
58398
  registry.register(exportAudioDef, exportAudio);
@@ -57310,8 +58487,8 @@ var init_export2 = __esm({
57310
58487
  const preset = args.preset || "standard";
57311
58488
  const overwrite = args.overwrite || false;
57312
58489
  const output = args.output || `${basename26(projectPath, ".vibe.json")}.${format}`;
57313
- const absProjectPath = resolve50(context.workingDirectory, projectPath);
57314
- const absOutputPath = resolve50(context.workingDirectory, output);
58490
+ const absProjectPath = resolve51(context.workingDirectory, projectPath);
58491
+ const absOutputPath = resolve51(context.workingDirectory, output);
57315
58492
  try {
57316
58493
  const result = await runExport(absProjectPath, absOutputPath, {
57317
58494
  format,
@@ -57384,8 +58561,8 @@ var batch_exports = {};
57384
58561
  __export(batch_exports, {
57385
58562
  registerBatchTools: () => registerBatchTools
57386
58563
  });
57387
- import { readFile as readFile42, writeFile as writeFile38, readdir as readdir6 } from "node:fs/promises";
57388
- 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";
57389
58566
  function detectMediaType5(filePath) {
57390
58567
  const ext = extname16(filePath).toLowerCase();
57391
58568
  const videoExts = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
@@ -57546,17 +58723,17 @@ var init_batch = __esm({
57546
58723
  const filterStr = args.filter;
57547
58724
  const imageDuration = args.imageDuration || 5;
57548
58725
  try {
57549
- const filePath = resolve51(context.workingDirectory, projectPath);
57550
- const content = await readFile42(filePath, "utf-8");
58726
+ const filePath = resolve52(context.workingDirectory, projectPath);
58727
+ const content = await readFile43(filePath, "utf-8");
57551
58728
  const data = JSON.parse(content);
57552
58729
  const project = Project.fromJSON(data);
57553
- const dirPath = resolve51(context.workingDirectory, directory);
58730
+ const dirPath = resolve52(context.workingDirectory, directory);
57554
58731
  const filterExts = filterStr ? filterStr.split(",").map((e) => e.trim().toLowerCase()) : null;
57555
58732
  const mediaFiles = [];
57556
58733
  const scanDir = async (dir) => {
57557
58734
  const entries = await readdir6(dir, { withFileTypes: true });
57558
58735
  for (const entry of entries) {
57559
- const entryPath = join8(dir, entry.name);
58736
+ const entryPath = join10(dir, entry.name);
57560
58737
  if (entry.isDirectory() && recursive) {
57561
58738
  await scanDir(entryPath);
57562
58739
  } else if (entry.isFile()) {
@@ -57597,7 +58774,7 @@ var init_batch = __esm({
57597
58774
  });
57598
58775
  addedSources.push({ id: source.id, name: mediaName, type: mediaType });
57599
58776
  }
57600
- await writeFile38(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
58777
+ await writeFile39(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
57601
58778
  const output = [
57602
58779
  `Imported ${addedSources.length} media files:`,
57603
58780
  "",
@@ -57625,8 +58802,8 @@ var init_batch = __esm({
57625
58802
  const startTime = args.startTime || 0;
57626
58803
  const gap = args.gap || 0;
57627
58804
  try {
57628
- const filePath = resolve51(context.workingDirectory, projectPath);
57629
- const content = await readFile42(filePath, "utf-8");
58805
+ const filePath = resolve52(context.workingDirectory, projectPath);
58806
+ const content = await readFile43(filePath, "utf-8");
57630
58807
  const data = JSON.parse(content);
57631
58808
  const project = Project.fromJSON(data);
57632
58809
  const sourcesToConcat = useAll ? project.getSources() : sourceIds.map((id) => project.getSource(id)).filter(Boolean);
@@ -57673,7 +58850,7 @@ var init_batch = __esm({
57673
58850
  });
57674
58851
  currentTime += source.duration + gap;
57675
58852
  }
57676
- await writeFile38(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
58853
+ await writeFile39(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
57677
58854
  const totalDuration = currentTime - gap - startTime;
57678
58855
  const output = [
57679
58856
  `Created ${createdClips.length} clips (total: ${totalDuration.toFixed(1)}s):`,
@@ -57702,8 +58879,8 @@ var init_batch = __esm({
57702
58879
  const duration = args.duration;
57703
58880
  const rawParams = args.params || {};
57704
58881
  try {
57705
- const filePath = resolve51(context.workingDirectory, projectPath);
57706
- const content = await readFile42(filePath, "utf-8");
58882
+ const filePath = resolve52(context.workingDirectory, projectPath);
58883
+ const content = await readFile43(filePath, "utf-8");
57707
58884
  const data = JSON.parse(content);
57708
58885
  const project = Project.fromJSON(data);
57709
58886
  const targetClips = useAll ? project.getClips() : clipIds.map((id) => project.getClip(id)).filter(Boolean);
@@ -57735,7 +58912,7 @@ var init_batch = __esm({
57735
58912
  appliedEffects.push({ clipId: clip.id, effectId: effect.id });
57736
58913
  }
57737
58914
  }
57738
- await writeFile38(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
58915
+ await writeFile39(filePath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
57739
58916
  return {
57740
58917
  toolCallId: "",
57741
58918
  success: true,
@@ -57759,7 +58936,7 @@ __export(scene_exports, {
57759
58936
  registerSceneTools: () => registerSceneTools,
57760
58937
  sceneToolDefinitions: () => sceneToolDefinitions
57761
58938
  });
57762
- import { resolve as resolve52, relative as relative7 } from "node:path";
58939
+ import { resolve as resolve53, relative as relative7 } from "node:path";
57763
58940
  function summariseLint(result) {
57764
58941
  const lines = [
57765
58942
  `${result.ok ? "\u2705" : "\u274C"} Lint ${result.ok ? "clean" : "failed"} \u2014 ${result.errorCount} error(s), ${result.warningCount} warning(s), ${result.infoCount} info`
@@ -57870,7 +59047,7 @@ var init_scene2 = __esm({
57870
59047
  }
57871
59048
  };
57872
59049
  sceneInitHandler = async (args, context) => {
57873
- const dir = resolve52(context.workingDirectory, args.dir);
59050
+ const dir = resolve53(context.workingDirectory, args.dir);
57874
59051
  try {
57875
59052
  const result = await scaffoldSceneProject({
57876
59053
  dir,
@@ -57897,7 +59074,7 @@ var init_scene2 = __esm({
57897
59074
  }
57898
59075
  };
57899
59076
  sceneAddHandler = async (args, context) => {
57900
- const projectDir = args.projectDir ? resolve52(context.workingDirectory, args.projectDir) : context.workingDirectory;
59077
+ const projectDir = args.projectDir ? resolve53(context.workingDirectory, args.projectDir) : context.workingDirectory;
57901
59078
  const result = await executeSceneAdd({
57902
59079
  name: args.name,
57903
59080
  preset: args.preset ?? "simple",
@@ -57934,7 +59111,7 @@ var init_scene2 = __esm({
57934
59111
  return { toolCallId: "", success: true, output: lines.join("\n") };
57935
59112
  };
57936
59113
  sceneLintHandler = async (args, context) => {
57937
- const projectDir = args.projectDir ? resolve52(context.workingDirectory, args.projectDir) : context.workingDirectory;
59114
+ const projectDir = args.projectDir ? resolve53(context.workingDirectory, args.projectDir) : context.workingDirectory;
57938
59115
  try {
57939
59116
  const result = await runProjectLint({
57940
59117
  projectDir,
@@ -57957,7 +59134,7 @@ var init_scene2 = __esm({
57957
59134
  }
57958
59135
  };
57959
59136
  sceneRenderHandler = async (args, context) => {
57960
- const projectDir = args.projectDir ? resolve52(context.workingDirectory, args.projectDir) : context.workingDirectory;
59137
+ const projectDir = args.projectDir ? resolve53(context.workingDirectory, args.projectDir) : context.workingDirectory;
57961
59138
  const result = await executeSceneRender({
57962
59139
  projectDir,
57963
59140
  root: args.root,
@@ -57999,7 +59176,7 @@ init_source();
57999
59176
  // package.json
58000
59177
  var package_default = {
58001
59178
  name: "@vibeframe/cli",
58002
- version: "0.58.0",
59179
+ version: "0.59.0",
58003
59180
  description: "VibeFrame CLI - natural language video editing from the terminal",
58004
59181
  private: false,
58005
59182
  license: "MIT",
@@ -58225,8 +59402,8 @@ projectCommand.command("create").description("Create a new project").argument("<
58225
59402
  outputPath = resolve2(process.cwd(), options.output);
58226
59403
  } else if (name.includes("/")) {
58227
59404
  const dirPath = resolve2(process.cwd(), name);
58228
- const { mkdir: mkdir21 } = await import("node:fs/promises");
58229
- await mkdir21(dirPath, { recursive: true });
59405
+ const { mkdir: mkdir22 } = await import("node:fs/promises");
59406
+ await mkdir22(dirPath, { recursive: true });
58230
59407
  outputPath = resolve2(dirPath, "project.vibe.json");
58231
59408
  } else {
58232
59409
  outputPath = resolve2(process.cwd(), "project.vibe.json");
@@ -60111,9 +61288,9 @@ function registerViralCommand(ai) {
60111
61288
  exitWithError(authError("ANTHROPIC_API_KEY", "Anthropic"));
60112
61289
  }
60113
61290
  let filePath = resolve28(process.cwd(), projectPath);
60114
- const { statSync } = await import("node:fs");
61291
+ const { statSync: statSync2 } = await import("node:fs");
60115
61292
  try {
60116
- if (statSync(filePath).isDirectory()) {
61293
+ if (statSync2(filePath).isDirectory()) {
60117
61294
  const candidates = ["project.vibe.json", ".vibe.json"];
60118
61295
  let found = false;
60119
61296
  for (const candidate of candidates) {
@@ -66237,20 +67414,20 @@ async function runDiagnostics() {
66237
67414
  installed = false;
66238
67415
  }
66239
67416
  } else if (toolName === "chrome") {
66240
- const { existsSync: existsSync28 } = await import("node:fs");
66241
- const { homedir: homedir3 } = await import("node:os");
66242
- 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");
66243
67420
  const chromePaths = [
66244
67421
  process.env.HYPERFRAMES_CHROME_PATH,
66245
67422
  process.env.CHROME_PATH,
66246
- 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"),
66247
67424
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
66248
67425
  "/Applications/Chromium.app/Contents/MacOS/Chromium",
66249
67426
  "/usr/bin/google-chrome",
66250
67427
  "/usr/bin/chromium",
66251
67428
  "/usr/bin/chromium-browser"
66252
67429
  ];
66253
- installed = chromePaths.some((p) => p && existsSync28(p));
67430
+ installed = chromePaths.some((p) => p && existsSync30(p));
66254
67431
  } else {
66255
67432
  installed = commandExists(toolName);
66256
67433
  }
@@ -66631,9 +67808,9 @@ init_ora();
66631
67808
 
66632
67809
  // src/pipeline/executor.ts
66633
67810
  var import_yaml7 = __toESM(require_dist(), 1);
66634
- import { resolve as resolve42 } from "node:path";
66635
- import { readFile as readFile35, writeFile as writeFile31, mkdir as mkdir19 } from "node:fs/promises";
66636
- 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";
66637
67814
 
66638
67815
  // src/pipeline/resolver.ts
66639
67816
  function resolveStepParams(params, completedSteps) {
@@ -66719,7 +67896,8 @@ var ACTION_TO_COMMAND = {
66719
67896
  "detect-beats": "detect beats",
66720
67897
  "analyze-media": "analyze media",
66721
67898
  "analyze-video": "analyze video",
66722
- "review-video": "analyze review"
67899
+ "review-video": "analyze review",
67900
+ "compose-scenes-with-skills": "compose scenes with skills"
66723
67901
  };
66724
67902
  function maxCostFor(action) {
66725
67903
  const cmd = ACTION_TO_COMMAND[action];
@@ -66732,9 +67910,9 @@ function registerAction(action, handler) {
66732
67910
  }
66733
67911
  function getOutput(params, outputDir, defaultName) {
66734
67912
  if (params.output && typeof params.output === "string") {
66735
- return resolve42(outputDir, params.output);
67913
+ return resolve43(outputDir, params.output);
66736
67914
  }
66737
- return resolve42(outputDir, defaultName);
67915
+ return resolve43(outputDir, defaultName);
66738
67916
  }
66739
67917
  async function ensureActionsRegistered() {
66740
67918
  if (Object.keys(ACTION_HANDLERS).length > 0) return;
@@ -66854,13 +68032,33 @@ async function ensureActionsRegistered() {
66854
68032
  const r = await executeReview2({ videoPath: params.input, autoApply: params.autoApply, model: params.model });
66855
68033
  return { id: "", action: "review-video", success: r.success, output: r.outputPath, data: { feedback: r.feedback }, error: r.error };
66856
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
+ });
66857
68055
  }
66858
68056
  async function loadPipeline(filePath) {
66859
- const absPath = resolve42(process.cwd(), filePath);
66860
- if (!existsSync27(absPath)) {
68057
+ const absPath = resolve43(process.cwd(), filePath);
68058
+ if (!existsSync29(absPath)) {
66861
68059
  throw new Error(`Pipeline file not found: ${absPath}`);
66862
68060
  }
66863
- const content = await readFile35(absPath, "utf-8");
68061
+ const content = await readFile36(absPath, "utf-8");
66864
68062
  const manifest = (0, import_yaml7.parse)(content);
66865
68063
  if (!manifest.name) throw new Error("Pipeline missing 'name' field");
66866
68064
  if (!manifest.steps || !Array.isArray(manifest.steps)) throw new Error("Pipeline missing 'steps' array");
@@ -66876,15 +68074,15 @@ async function loadPipeline(filePath) {
66876
68074
  var CHECKPOINT_FILE = ".pipeline-state.yaml";
66877
68075
  async function executePipeline(manifest, options = {}) {
66878
68076
  await ensureActionsRegistered();
66879
- const outputDir = resolve42(process.cwd(), options.outputDir || `${manifest.name}-output`);
66880
- await mkdir19(outputDir, { recursive: true });
68077
+ const outputDir = resolve43(process.cwd(), options.outputDir || `${manifest.name}-output`);
68078
+ await mkdir20(outputDir, { recursive: true });
66881
68079
  const completedSteps = /* @__PURE__ */ new Map();
66882
68080
  const results = [];
66883
68081
  const startTime = Date.now();
66884
68082
  if (options.resume) {
66885
- const checkpointPath = resolve42(outputDir, CHECKPOINT_FILE);
66886
- if (existsSync27(checkpointPath)) {
66887
- 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");
66888
68086
  const checkpoint = (0, import_yaml7.parse)(checkpointContent);
66889
68087
  for (const cs of checkpoint.completedSteps) {
66890
68088
  completedSteps.set(cs.id, {
@@ -67002,8 +68200,8 @@ async function executePipeline(manifest, options = {}) {
67002
68200
  data: s.data
67003
68201
  }))
67004
68202
  };
67005
- await writeFile31(
67006
- resolve42(outputDir, CHECKPOINT_FILE),
68203
+ await writeFile32(
68204
+ resolve43(outputDir, CHECKPOINT_FILE),
67007
68205
  (0, import_yaml7.stringify)(checkpoint, { indent: 2 }),
67008
68206
  "utf-8"
67009
68207
  );
@@ -67171,7 +68369,8 @@ Run 'vibe schema run' for structured parameter info.
67171
68369
  success: s.success,
67172
68370
  output: s.output,
67173
68371
  duration: s.duration,
67174
- error: s.error
68372
+ error: s.error,
68373
+ data: s.data
67175
68374
  }))
67176
68375
  });
67177
68376
  } else {
@@ -67760,14 +68959,14 @@ async function promptConfirm2(toolName, args) {
67760
68959
  output: process.stdout,
67761
68960
  terminal: process.stdin.isTTY ?? false
67762
68961
  });
67763
- return new Promise((resolve53) => {
68962
+ return new Promise((resolve54) => {
67764
68963
  const argsStr = JSON.stringify(args, null, 2);
67765
68964
  console.log();
67766
68965
  console.log(source_default.yellow(`Execute ${source_default.bold(toolName)}?`));
67767
68966
  console.log(source_default.dim(argsStr));
67768
68967
  rl.question(source_default.cyan("(y/n): "), (answer) => {
67769
68968
  rl.close();
67770
- resolve53(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
68969
+ resolve54(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
67771
68970
  });
67772
68971
  });
67773
68972
  }
@@ -67856,7 +69055,7 @@ async function startAgent(options = {}) {
67856
69055
  console.log();
67857
69056
  console.log(source_default.dim(" Commands: exit \xB7 reset \xB7 tools \xB7 context"));
67858
69057
  console.log();
67859
- return new Promise((resolve53) => {
69058
+ return new Promise((resolve54) => {
67860
69059
  if (typeof process.stdin.ref === "function") {
67861
69060
  process.stdin.ref();
67862
69061
  }
@@ -67958,7 +69157,7 @@ async function startAgent(options = {}) {
67958
69157
  clearInterval(keepalive);
67959
69158
  console.log();
67960
69159
  console.log(source_default.dim("Goodbye!"));
67961
- resolve53();
69160
+ resolve54();
67962
69161
  });
67963
69162
  rl.prompt();
67964
69163
  });
@@ -67986,8 +69185,8 @@ init_source();
67986
69185
  init_config();
67987
69186
  init_schema();
67988
69187
  init_api_key();
67989
- import { access as access9, writeFile as writeFile39, mkdir as mkdir20 } from "node:fs/promises";
67990
- 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";
67991
69190
  var BANNER_SHOWN_PATH = CONFIG_PATH.replace(/config\.yaml$/, ".banner-shown");
67992
69191
  async function isFirstRun() {
67993
69192
  try {
@@ -68010,8 +69209,8 @@ async function isFirstRun() {
68010
69209
  }
68011
69210
  async function markBannerShown() {
68012
69211
  try {
68013
- await mkdir20(dirname20(BANNER_SHOWN_PATH), { recursive: true });
68014
- 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());
68015
69214
  } catch {
68016
69215
  }
68017
69216
  }