@vanillaskyai/video 0.5.1 → 0.5.3

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 (63) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/{bg-confetti-KTHLUJPO.js → bg-confetti-572VDSYM.js} +1 -1
  3. package/dist/{bg-emoji-AW7374DZ.js → bg-emoji-T4IEJ6U2.js} +1 -1
  4. package/dist/{bg-media-KGYE6UVS.js → bg-media-VSFT5YC7.js} +2 -2
  5. package/dist/{brand-message-AY43ZQR2.js → brand-message-GSM2LOEP.js} +2 -2
  6. package/dist/{chart-bar-Z42RDRQT.js → chart-bar-ZGGCV3ED.js} +2 -2
  7. package/dist/{chart-counter-X7NAOQ2C.js → chart-counter-T3MXDPP6.js} +2 -2
  8. package/dist/{chart-progress-ring-7NSNNHL4.js → chart-progress-ring-OT5Z2LMK.js} +2 -2
  9. package/dist/check-runtime.js +1 -1
  10. package/dist/{chunk-3ROGEU5A.js → chunk-3OU7HIEB.js} +232 -174
  11. package/dist/{chunk-K6YVQODK.js → chunk-65MRKELL.js} +3 -2
  12. package/dist/{chunk-IJM4B2ZN.js → chunk-MFYFZQHM.js} +1 -1
  13. package/dist/{chunk-4AODC6MS.js → chunk-SNVWTK63.js} +7 -3
  14. package/dist/{compose-video-QJHKTOQ7.js → compose-video-JWIEDTEJ.js} +1 -1
  15. package/dist/{cta-logo-KNBLXUJU.js → cta-logo-CV5NCBHX.js} +2 -2
  16. package/dist/{cta-media-NY4ASBGG.js → cta-media-TJ7F4GN7.js} +2 -2
  17. package/dist/{incoming-call-KWOXLGJN.js → incoming-call-FOD6Z4R2.js} +2 -2
  18. package/dist/{infographic-before-after-OEM4DSTZ.js → infographic-before-after-FQXCQ2CY.js} +1 -1
  19. package/dist/{infographic-feature-list-ZZ25JRYQ.js → infographic-feature-list-Z37COJMT.js} +2 -2
  20. package/dist/{infographic-problem-solution-L3RLFDPE.js → infographic-problem-solution-WAJ76HQV.js} +2 -2
  21. package/dist/{infographic-stat-row-3OL3TNSH.js → infographic-stat-row-ZTS6PO57.js} +2 -2
  22. package/dist/{infographic-steps-LMBSW22P.js → infographic-steps-SNZCO5ZW.js} +2 -2
  23. package/dist/{prompt-input-XCQ6TS2L.js → prompt-input-VQ6WUBRH.js} +2 -2
  24. package/dist/react.js +27 -27
  25. package/dist/{reaction-YL6YPGPB.js → reaction-QECWNLCQ.js} +2 -2
  26. package/dist/server.js +1 -1
  27. package/dist/{showcase-code-EFVHRJUM.js → showcase-code-PVPNSI6U.js} +2 -2
  28. package/dist/{showcase-phone-CTHXHEBT.js → showcase-phone-G32YB25H.js} +2 -2
  29. package/dist/{showcase-terminal-XAMPADHN.js → showcase-terminal-PLZTGUL6.js} +2 -2
  30. package/dist/{showcase-web-LAKHYXSD.js → showcase-web-IRLNVEYD.js} +2 -2
  31. package/dist/{social-milestone-EOBEN7CX.js → social-milestone-T67PZ5RR.js} +2 -2
  32. package/dist/{social-notification-MELRWIB6.js → social-notification-P7ZSTEZ5.js} +2 -2
  33. package/dist/{social-review-stack-H3U437VT.js → social-review-stack-IXAC23MY.js} +2 -2
  34. package/dist/{social-testimonial-GNNMKMDD.js → social-testimonial-AGSFR2IF.js} +2 -2
  35. package/dist/{social-tweet-PBUKKPT3.js → social-tweet-UXTQA5VW.js} +2 -2
  36. package/dist/test.js +1 -1
  37. package/docs/motion-and-effects.md +12 -2
  38. package/examples/nextjs-quickstart/package.json +1 -1
  39. package/package.json +1 -1
  40. package/registry/items/backgrounds.json +1 -1
  41. package/registry/items/barChart.json +1 -1
  42. package/registry/items/bigNumber.json +1 -1
  43. package/registry/items/brandMessage.json +1 -1
  44. package/registry/items/cardList.json +1 -1
  45. package/registry/items/codeEditor.json +1 -1
  46. package/registry/items/ctaLogo.json +1 -1
  47. package/registry/items/ctaMedia.json +1 -1
  48. package/registry/items/incomingCall.json +1 -1
  49. package/registry/items/media.json +1 -1
  50. package/registry/items/milestone.json +1 -1
  51. package/registry/items/notification.json +1 -1
  52. package/registry/items/phoneMockup.json +1 -1
  53. package/registry/items/problemSolution.json +1 -1
  54. package/registry/items/progressRing.json +1 -1
  55. package/registry/items/promptInput.json +1 -1
  56. package/registry/items/reaction.json +1 -1
  57. package/registry/items/reviewStack.json +1 -1
  58. package/registry/items/steps.json +1 -1
  59. package/registry/items/terminal.json +1 -1
  60. package/registry/items/testimonial.json +1 -1
  61. package/registry/items/tripleStats.json +1 -1
  62. package/registry/items/tweet.json +1 -1
  63. package/registry/items/webMockup.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ VanillaSky follows semantic versioning. This changelog begins with the 0.1 beta.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.5.3
8
+
9
+ - Makes `crossfade` the default for generated videos and mounts changed media
10
+ invisibly before every contiguous cut, including templates that own their
11
+ transition. This extends the existing media warm-up window so the browser can
12
+ decode the incoming backdrop and replace its stock poster with the real first
13
+ frame while still hidden, instead of flashing the gradient or visibly swapping
14
+ crops after the cut. Making camera motion opt-in also removes the scale reset
15
+ that could look like a quick pullback during the fade.
16
+
17
+ ## 0.5.2
18
+
19
+ - Refactors composition state transitions into a deterministic, directly
20
+ tested session engine without changing the public API, event stream,
21
+ checksums, or runtime behavior.
22
+
7
23
  ## 0.5.1
8
24
 
9
25
  - Removes the redundant README version badge. Package registries and release
@@ -8,7 +8,7 @@ import {
8
8
  import "./chunk-L5BTTCZV.js";
9
9
  import {
10
10
  BrandGradientOverlay
11
- } from "./chunk-IJM4B2ZN.js";
11
+ } from "./chunk-MFYFZQHM.js";
12
12
  import "./chunk-XTOXFMLM.js";
13
13
  import {
14
14
  resolveTokens
@@ -9,7 +9,7 @@ import {
9
9
  } from "./chunk-L5BTTCZV.js";
10
10
  import {
11
11
  BrandGradientOverlay
12
- } from "./chunk-IJM4B2ZN.js";
12
+ } from "./chunk-MFYFZQHM.js";
13
13
  import "./chunk-XTOXFMLM.js";
14
14
  import {
15
15
  resolveTokens
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  SceneBackground,
3
3
  getMediaBackgroundProps
4
- } from "./chunk-4AODC6MS.js";
4
+ } from "./chunk-SNVWTK63.js";
5
5
  import {
6
6
  hasSceneMedia
7
7
  } from "./chunk-35LIYILY.js";
@@ -13,7 +13,7 @@ import {
13
13
  TemplateText
14
14
  } from "./chunk-TCEHK2BW.js";
15
15
  import "./chunk-L5BTTCZV.js";
16
- import "./chunk-IJM4B2ZN.js";
16
+ import "./chunk-MFYFZQHM.js";
17
17
  import "./chunk-XTOXFMLM.js";
18
18
  import {
19
19
  resolveTokens
@@ -4,12 +4,12 @@ import {
4
4
  import {
5
5
  SceneBackground,
6
6
  getMediaBackgroundProps
7
- } from "./chunk-4AODC6MS.js";
7
+ } from "./chunk-SNVWTK63.js";
8
8
  import "./chunk-35LIYILY.js";
9
9
  import {
10
10
  renderWithEmoji
11
11
  } from "./chunk-L5BTTCZV.js";
12
- import "./chunk-IJM4B2ZN.js";
12
+ import "./chunk-MFYFZQHM.js";
13
13
  import {
14
14
  interpolate
15
15
  } from "./chunk-XTOXFMLM.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  SceneBackground,
3
3
  getMediaBackgroundProps
4
- } from "./chunk-4AODC6MS.js";
4
+ } from "./chunk-SNVWTK63.js";
5
5
  import {
6
6
  hasSceneMedia
7
7
  } from "./chunk-35LIYILY.js";
@@ -12,7 +12,7 @@ import "./chunk-L5BTTCZV.js";
12
12
  import {
13
13
  TOP_TEXT_AREA_RATIO,
14
14
  withOpacity
15
- } from "./chunk-IJM4B2ZN.js";
15
+ } from "./chunk-MFYFZQHM.js";
16
16
  import {
17
17
  SPRING_SNAPPY,
18
18
  interpolate,
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  SceneBackground,
6
6
  getMediaBackgroundProps
7
- } from "./chunk-4AODC6MS.js";
7
+ } from "./chunk-SNVWTK63.js";
8
8
  import {
9
9
  hasSceneMedia
10
10
  } from "./chunk-35LIYILY.js";
@@ -19,7 +19,7 @@ import "./chunk-L5BTTCZV.js";
19
19
  import {
20
20
  TOP_TEXT_AREA_RATIO,
21
21
  withOpacity
22
- } from "./chunk-IJM4B2ZN.js";
22
+ } from "./chunk-MFYFZQHM.js";
23
23
  import {
24
24
  interpolate,
25
25
  spring
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  SceneBackground,
7
7
  getMediaBackgroundProps
8
- } from "./chunk-4AODC6MS.js";
8
+ } from "./chunk-SNVWTK63.js";
9
9
  import {
10
10
  hasSceneMedia
11
11
  } from "./chunk-35LIYILY.js";
@@ -16,7 +16,7 @@ import "./chunk-L5BTTCZV.js";
16
16
  import {
17
17
  TOP_TEXT_AREA_RATIO,
18
18
  withOpacity
19
- } from "./chunk-IJM4B2ZN.js";
19
+ } from "./chunk-MFYFZQHM.js";
20
20
  import {
21
21
  interpolate
22
22
  } from "./chunk-XTOXFMLM.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VideoFrame
3
- } from "./chunk-K6YVQODK.js";
3
+ } from "./chunk-65MRKELL.js";
4
4
  import "./chunk-XWWLKRNU.js";
5
5
  import "./chunk-SPVTJH3F.js";
6
6
  import {
@@ -223,8 +223,9 @@ function buildInitialComposition(input, audio, snapshotRetention, closerReserveS
223
223
  brand: resolveVideoBrand(input.brand),
224
224
  density: input.style?.density ?? "normal",
225
225
  motion: input.style?.motion ?? "normal",
226
- defaultBackgroundEffect: input.style?.backgroundEffect ?? "slow-zoom-in",
227
- defaultTextArchetype: input.style?.textArchetype ?? "subtle"
226
+ defaultBackgroundEffect: input.style?.backgroundEffect ?? "static",
227
+ defaultTextArchetype: input.style?.textArchetype ?? "subtle",
228
+ defaultTransition: "crossfade"
228
229
  },
229
230
  meta
230
231
  },
@@ -349,6 +350,193 @@ function createSceneQualityWarnings(scene) {
349
350
  }];
350
351
  }
351
352
 
353
+ // src/server/composition-session.ts
354
+ var warningActions = (warnings) => warnings.map((warning) => ({ type: "response.warning", warning }));
355
+ function createCompositionSession() {
356
+ return {
357
+ acceptedSceneCount: 0,
358
+ rejectedSceneCount: 0,
359
+ planCompleted: false,
360
+ runtimeDurationLimited: false,
361
+ deferredForCloser: [],
362
+ closerCommitted: false,
363
+ plannerReportedLength: false,
364
+ finishReason: "stop"
365
+ };
366
+ }
367
+ function pace(scene, video, options, closerReserveSec) {
368
+ return paceScene(scene, {
369
+ previousScenes: video.scenes,
370
+ audio: video.audio,
371
+ maxDurationSec: options.maxDurationSec,
372
+ closerReserveSec,
373
+ getTemplatePacing: options.getTemplatePacing
374
+ });
375
+ }
376
+ function commitScene(session, result, includeQualityWarnings) {
377
+ return {
378
+ session: {
379
+ ...session,
380
+ acceptedSceneCount: session.acceptedSceneCount + 1
381
+ },
382
+ actions: [
383
+ ...warningActions(result.warnings),
384
+ ...includeQualityWarnings ? warningActions(createSceneQualityWarnings(result.scene)) : [],
385
+ { type: "scene.add", scene: result.scene }
386
+ ]
387
+ };
388
+ }
389
+ function completePlan(session, reportedFinishReason, requireCloser) {
390
+ const missingCloser = requireCloser && !session.closerCommitted;
391
+ const finishReason = session.runtimeDurationLimited ? "length" : reportedFinishReason != null && reportedFinishReason !== "stop" ? reportedFinishReason : missingCloser ? "other" : "stop";
392
+ return {
393
+ session: {
394
+ ...session,
395
+ planCompleted: true,
396
+ plannerReportedLength: reportedFinishReason === "length",
397
+ finishReason
398
+ },
399
+ actions: missingCloser ? [{ type: "response.warning", warning: createMissingCloserWarning() }] : []
400
+ };
401
+ }
402
+ function advanceComposition(session, part, video, options) {
403
+ if (session.planCompleted) {
404
+ throw new Error(part.type === "plan.complete" ? "The planner emitted plan.complete more than once" : "The planner emitted content after plan.complete");
405
+ }
406
+ if (part.type === "plan.complete") {
407
+ if (!session.pendingCloser) {
408
+ let next2 = { ...session, deferredForCloser: [] };
409
+ let currentVideo = video;
410
+ const actions2 = [];
411
+ for (const deferred of session.deferredForCloser) {
412
+ const recovered = pace(deferred, currentVideo, options, 0);
413
+ if (!recovered.scene) {
414
+ next2 = {
415
+ ...next2,
416
+ rejectedSceneCount: next2.rejectedSceneCount + 1,
417
+ runtimeDurationLimited: true
418
+ };
419
+ actions2.push(...warningActions(recovered.warnings));
420
+ continue;
421
+ }
422
+ const committed2 = commitScene(next2, { ...recovered, scene: recovered.scene }, true);
423
+ next2 = committed2.session;
424
+ actions2.push(...committed2.actions);
425
+ currentVideo = { ...currentVideo, scenes: [...currentVideo.scenes, recovered.scene] };
426
+ }
427
+ const completed2 = completePlan(next2, part.finishReason, options.requireCloser);
428
+ return {
429
+ session: completed2.session,
430
+ actions: [...actions2, ...completed2.actions]
431
+ };
432
+ }
433
+ let next = {
434
+ ...session,
435
+ rejectedSceneCount: session.rejectedSceneCount + session.deferredForCloser.length,
436
+ runtimeDurationLimited: session.runtimeDurationLimited || session.deferredForCloser.length > 0,
437
+ deferredForCloser: []
438
+ };
439
+ const actions = session.deferredForCloser.map((scene) => ({
440
+ type: "response.warning",
441
+ warning: createOmittedForCloserWarning(scene.id)
442
+ }));
443
+ const closer = pace(session.pendingCloser, video, options, 0);
444
+ if (!closer.scene) {
445
+ next = {
446
+ ...next,
447
+ rejectedSceneCount: next.rejectedSceneCount + 1,
448
+ runtimeDurationLimited: true
449
+ };
450
+ actions.push(...warningActions(closer.warnings));
451
+ const completed2 = completePlan(next, part.finishReason, options.requireCloser);
452
+ return {
453
+ session: completed2.session,
454
+ actions: [...actions, ...completed2.actions]
455
+ };
456
+ }
457
+ const committed = commitScene(next, { ...closer, scene: closer.scene }, false);
458
+ next = { ...committed.session, closerCommitted: true };
459
+ const completed = completePlan(next, part.finishReason, options.requireCloser);
460
+ return {
461
+ session: completed.session,
462
+ actions: [...actions, ...committed.actions, ...completed.actions]
463
+ };
464
+ }
465
+ const metadata = options.getTemplatePacing?.(part.scene.templateId);
466
+ const isAsk = metadata?.jobs?.includes("ask") === true;
467
+ const isPayoff = metadata?.jobs?.includes("payoff") === true;
468
+ if (part.placement === "closer" && metadata && !isAsk && !isPayoff) {
469
+ throw new Error(`Scene template ${part.scene.templateId} cannot be used as a closer`);
470
+ }
471
+ if (part.placement === "closer" || isAsk) {
472
+ if (session.pendingCloser) {
473
+ return {
474
+ session: {
475
+ ...session,
476
+ rejectedSceneCount: session.rejectedSceneCount + 1
477
+ },
478
+ actions: [{
479
+ type: "response.warning",
480
+ warning: createDuplicateCloserWarning()
481
+ }]
482
+ };
483
+ }
484
+ return { session: { ...session, pendingCloser: part.scene }, actions: [] };
485
+ }
486
+ if (session.deferredForCloser.length > 0) {
487
+ return {
488
+ session: { ...session, deferredForCloser: [...session.deferredForCloser, part.scene] },
489
+ actions: []
490
+ };
491
+ }
492
+ const paced = pace(
493
+ part.scene,
494
+ video,
495
+ options,
496
+ session.pendingCloser ? pendingCloserReserve(session.pendingCloser, options.getTemplatePacing) : options.closerReserveSec
497
+ );
498
+ if (!paced.scene) {
499
+ if (paced.warnings.some(({ code }) => code === "scene_omitted_for_closer")) {
500
+ return {
501
+ session: { ...session, deferredForCloser: [...session.deferredForCloser, part.scene] },
502
+ actions: []
503
+ };
504
+ }
505
+ return {
506
+ session: {
507
+ ...session,
508
+ rejectedSceneCount: session.rejectedSceneCount + 1,
509
+ runtimeDurationLimited: true
510
+ },
511
+ actions: warningActions(paced.warnings)
512
+ };
513
+ }
514
+ return commitScene(session, { ...paced, scene: paced.scene }, true);
515
+ }
516
+ function rejectCompositionScene(session) {
517
+ return {
518
+ ...session,
519
+ rejectedSceneCount: session.rejectedSceneCount + 1
520
+ };
521
+ }
522
+ function recoverComposition(session, video, options) {
523
+ if (!session.pendingCloser || session.closerCommitted || video.scenes.length === 0) {
524
+ return { session, actions: [] };
525
+ }
526
+ const closer = pace(session.pendingCloser, video, options, 0);
527
+ if (!closer.scene) {
528
+ return {
529
+ session,
530
+ actions: warningActions(closer.warnings)
531
+ };
532
+ }
533
+ const committed = commitScene(session, { ...closer, scene: closer.scene }, false);
534
+ return {
535
+ session: { ...committed.session, closerCommitted: true },
536
+ actions: committed.actions
537
+ };
538
+ }
539
+
352
540
  // src/server/composition-runtime.ts
353
541
  function invokeIsolated(callback, value) {
354
542
  if (!callback) return;
@@ -422,8 +610,6 @@ function createVideo(rawInput, options) {
422
610
  rejectResult = reject;
423
611
  });
424
612
  const startedAt = monotonicNow();
425
- let acceptedSceneCount = 0;
426
- let rejectedSceneCount = 0;
427
613
  let timeToFirstSceneMs;
428
614
  const { sink: lifecycle, settle: settleProviderLifecycle } = createProviderLifecycle();
429
615
  const reportedErrors = /* @__PURE__ */ new WeakSet();
@@ -434,6 +620,13 @@ function createVideo(rawInput, options) {
434
620
  };
435
621
  const eventSource = (async function* () {
436
622
  let state = createVideoState();
623
+ let composition = createCompositionSession();
624
+ const compositionOptions = {
625
+ maxDurationSec: input.maxDurationSec ?? 30,
626
+ closerReserveSec,
627
+ requireCloser: options.requireCloser ?? false,
628
+ getTemplatePacing: options.getTemplatePacing
629
+ };
437
630
  const emit = (event) => {
438
631
  state = applyVideoEvent(state, event);
439
632
  if (event.type === "response.warning") {
@@ -442,21 +635,23 @@ function createVideo(rawInput, options) {
442
635
  options.onEvent?.(event);
443
636
  return event;
444
637
  };
638
+ const emitCompositionAction = (action) => {
639
+ if (action.type === "response.warning") {
640
+ return emit(events.create("response.warning", { warning: action.warning }));
641
+ }
642
+ const event = emit(events.create("scene.add", {
643
+ scene: action.scene,
644
+ position: state.config?.scenes.length ?? 0
645
+ }));
646
+ timeToFirstSceneMs ??= Math.max(0, monotonicNow() - startedAt);
647
+ return event;
648
+ };
445
649
  const finish = (finalState) => {
446
650
  if (!settled) {
447
651
  settled = true;
448
652
  resolveResult(finalState);
449
653
  }
450
654
  };
451
- let planCompleted = false;
452
- let generatedSceneCount = 0;
453
- let runtimeDurationLimited = false;
454
- let pendingCloser;
455
- const deferredForCloser = [];
456
- let plannerReportedLength = false;
457
- let closerCommitted = false;
458
- let missingCloser = false;
459
- let finishReason = "stop";
460
655
  try {
461
656
  yield emit(events.create("response.start", {
462
657
  requestId,
@@ -484,8 +679,8 @@ function createVideo(rawInput, options) {
484
679
  const part = resolveSuppliedMediaPlanPart(parseVideoPlanPart(untrustedPart), input);
485
680
  attemptedScene = part.type === "scene.add";
486
681
  if (controller.signal.aborted) throw controller.signal.reason ?? new Error(abortReason);
487
- if (planCompleted && part.type !== "plan.complete") {
488
- throw new Error("The planner emitted content after plan.complete");
682
+ if (composition.planCompleted) {
683
+ throw new Error(part.type === "plan.complete" ? "The planner emitted plan.complete more than once" : "The planner emitted content after plan.complete");
489
684
  }
490
685
  if (part.type === "scene.add") {
491
686
  if (options.capabilities?.templates != null && !options.capabilities.templates.includes(part.scene.templateId)) {
@@ -495,138 +690,16 @@ function createVideo(rawInput, options) {
495
690
  input,
496
691
  previousScenes: state.config?.scenes ?? []
497
692
  });
498
- const templatePacing = options.getTemplatePacing?.(part.scene.templateId);
499
- const isAskCandidate = templatePacing?.jobs?.includes("ask") === true;
500
- const isPayoffCandidate = templatePacing?.jobs?.includes("payoff") === true;
501
- if (part.placement === "closer" && templatePacing && !isAskCandidate && !isPayoffCandidate) {
502
- throw new Error(`Scene template ${part.scene.templateId} cannot be used as a closer`);
503
- }
504
- const isCloserCandidate = part.placement === "closer" || isAskCandidate;
505
- if (isCloserCandidate) {
506
- if (pendingCloser) {
507
- rejectedSceneCount += 1;
508
- yield emit(events.create("response.warning", {
509
- warning: createDuplicateCloserWarning()
510
- }));
511
- } else {
512
- pendingCloser = part.scene;
513
- }
514
- continue;
515
- }
516
- if (deferredForCloser.length) {
517
- deferredForCloser.push(part.scene);
518
- continue;
519
- }
520
- const paced = paceScene(part.scene, {
521
- previousScenes: state.config?.scenes ?? [],
522
- audio: state.config?.audio,
523
- maxDurationSec: input.maxDurationSec ?? 30,
524
- closerReserveSec: pendingCloser ? pendingCloserReserve(pendingCloser, options.getTemplatePacing) : closerReserveSec,
525
- getTemplatePacing: options.getTemplatePacing
526
- });
527
- if (!paced.scene) {
528
- if (paced.warnings.some(({ code }) => code === "scene_omitted_for_closer")) {
529
- deferredForCloser.push(part.scene);
530
- continue;
531
- }
532
- for (const warning of paced.warnings) {
533
- yield emit(events.create("response.warning", { warning }));
534
- }
535
- runtimeDurationLimited = true;
536
- rejectedSceneCount += 1;
537
- continue;
538
- }
539
- const scene = paced.scene;
540
- for (const warning of paced.warnings) {
541
- yield emit(events.create("response.warning", { warning }));
542
- }
543
- for (const warning of createSceneQualityWarnings(scene)) {
544
- yield emit(events.create("response.warning", { warning }));
545
- }
546
- yield emit(events.create("scene.add", {
547
- scene,
548
- position: state.config?.scenes.length ?? 0
549
- }));
550
- generatedSceneCount += 1;
551
- acceptedSceneCount += 1;
552
- timeToFirstSceneMs ??= Math.max(0, monotonicNow() - startedAt);
553
- } else if (part.type === "plan.complete") {
554
- if (planCompleted) throw new Error("The planner emitted plan.complete more than once");
555
- if (pendingCloser) {
556
- if (deferredForCloser.length) runtimeDurationLimited = true;
557
- for (const deferred of deferredForCloser.splice(0)) {
558
- rejectedSceneCount += 1;
559
- yield emit(events.create("response.warning", {
560
- warning: createOmittedForCloserWarning(deferred.id)
561
- }));
562
- }
563
- const pacedCloser = paceScene(pendingCloser, {
564
- previousScenes: state.config?.scenes ?? [],
565
- audio: state.config?.audio,
566
- maxDurationSec: input.maxDurationSec ?? 30,
567
- closerReserveSec: 0,
568
- getTemplatePacing: options.getTemplatePacing
569
- });
570
- for (const warning of pacedCloser.warnings) {
571
- yield emit(events.create("response.warning", { warning }));
572
- }
573
- if (pacedCloser.scene) {
574
- yield emit(events.create("scene.add", {
575
- scene: pacedCloser.scene,
576
- position: state.config?.scenes.length ?? 0
577
- }));
578
- generatedSceneCount += 1;
579
- acceptedSceneCount += 1;
580
- timeToFirstSceneMs ??= Math.max(0, monotonicNow() - startedAt);
581
- closerCommitted = true;
582
- } else {
583
- runtimeDurationLimited = true;
584
- rejectedSceneCount += 1;
585
- }
586
- } else if (deferredForCloser.length) {
587
- for (const deferred of deferredForCloser.splice(0)) {
588
- const recovered = paceScene(deferred, {
589
- previousScenes: state.config?.scenes ?? [],
590
- audio: state.config?.audio,
591
- maxDurationSec: input.maxDurationSec ?? 30,
592
- closerReserveSec: 0,
593
- getTemplatePacing: options.getTemplatePacing
594
- });
595
- for (const warning of recovered.warnings) {
596
- yield emit(events.create("response.warning", { warning }));
597
- }
598
- if (!recovered.scene) {
599
- runtimeDurationLimited = true;
600
- rejectedSceneCount += 1;
601
- continue;
602
- }
603
- for (const warning of createSceneQualityWarnings(recovered.scene)) {
604
- yield emit(events.create("response.warning", { warning }));
605
- }
606
- yield emit(events.create("scene.add", {
607
- scene: recovered.scene,
608
- position: state.config?.scenes.length ?? 0
609
- }));
610
- generatedSceneCount += 1;
611
- acceptedSceneCount += 1;
612
- timeToFirstSceneMs ??= Math.max(0, monotonicNow() - startedAt);
613
- }
614
- }
615
- if (options.requireCloser && !closerCommitted) {
616
- missingCloser = true;
617
- yield emit(events.create("response.warning", {
618
- warning: createMissingCloserWarning()
619
- }));
620
- }
621
- planCompleted = true;
622
- const reportedFinishReason = part.finishReason ?? "stop";
623
- finishReason = runtimeDurationLimited ? "length" : reportedFinishReason !== "stop" ? reportedFinishReason : missingCloser ? "other" : "stop";
624
- plannerReportedLength = part.finishReason === "length";
625
- continue;
626
693
  }
694
+ if (!state.config) throw new Error("response.start did not initialize composition state");
695
+ const transition = advanceComposition(composition, part, state.config, compositionOptions);
696
+ for (const action of transition.actions) yield emitCompositionAction(action);
697
+ composition = transition.session;
627
698
  } catch (cause) {
628
699
  const error = cause instanceof Error ? cause : new Error(String(cause));
629
- if (attemptedScene || untrustedPart != null && typeof untrustedPart === "object" && untrustedPart.type === "scene.add") rejectedSceneCount += 1;
700
+ if (attemptedScene || untrustedPart != null && typeof untrustedPart === "object" && untrustedPart.type === "scene.add") {
701
+ composition = rejectCompositionScene(composition);
702
+ }
630
703
  reportError(error);
631
704
  if ((options.invalidPartBehavior ?? "fail") === "fail") throw error;
632
705
  yield emit(events.create("response.error", {
@@ -646,35 +719,35 @@ function createVideo(rawInput, options) {
646
719
  for (const warning of provider.warnings) {
647
720
  yield emit(events.create("response.warning", { warning }));
648
721
  }
649
- if (!planCompleted) throw new Error("The planner stream ended before plan.complete");
650
- if (plannerReportedLength && generatedSceneCount === 0) {
722
+ if (!composition.planCompleted) throw new Error("The planner stream ended before plan.complete");
723
+ if (composition.plannerReportedLength && composition.acceptedSceneCount === 0) {
651
724
  throw new Error("The planner was truncated before adding a generated scene");
652
725
  }
653
- if (plannerReportedLength) {
726
+ if (composition.plannerReportedLength) {
654
727
  yield emit(events.create("response.warning", {
655
728
  warning: createIncompletePlanWarning()
656
729
  }));
657
730
  }
658
- if (generatedSceneCount === 0 && requiresGeneratedScene) {
731
+ if (composition.acceptedSceneCount === 0 && requiresGeneratedScene) {
659
732
  throw new Error("The planner completed without adding a scene");
660
733
  }
661
734
  const snapshot = parseVideo(state.config);
662
735
  const completeEvent = emit(events.create("response.complete", {
663
- finishReason,
736
+ finishReason: composition.finishReason,
664
737
  snapshot,
665
738
  checksum: checksumVideo(snapshot)
666
739
  }));
667
740
  finish(state);
668
741
  const summary = {
669
- finishReason,
742
+ finishReason: composition.finishReason,
670
743
  ...provider.usage ? { usage: provider.usage } : {},
671
744
  ...provider.providerMetadata !== void 0 ? { providerMetadata: provider.providerMetadata } : {},
672
745
  ...provider.requestedModelId ? { requestedModelId: provider.requestedModelId } : {},
673
746
  ...provider.resolvedModelId ? { resolvedModelId: provider.resolvedModelId } : {},
674
747
  ...timeToFirstSceneMs != null ? { timeToFirstSceneMs } : {},
675
748
  totalDurationMs: Math.max(0, monotonicNow() - startedAt),
676
- acceptedSceneCount,
677
- rejectedSceneCount,
749
+ acceptedSceneCount: composition.acceptedSceneCount,
750
+ rejectedSceneCount: composition.rejectedSceneCount,
678
751
  videoDurationSec: snapshot.scenes.at(-1)?.timing.endTime ?? 0,
679
752
  warnings: state.warnings.map(cloneWarning)
680
753
  };
@@ -691,30 +764,16 @@ function createVideo(rawInput, options) {
691
764
  return;
692
765
  }
693
766
  try {
694
- if (pendingCloser && !closerCommitted && state.config?.scenes.length) {
695
- const pacedCloser = paceScene(pendingCloser, {
696
- previousScenes: state.config.scenes,
697
- audio: state.config.audio,
698
- maxDurationSec: input.maxDurationSec ?? 30,
699
- closerReserveSec: 0,
700
- getTemplatePacing: options.getTemplatePacing
701
- });
702
- for (const warning of pacedCloser.warnings) {
703
- yield emit(events.create("response.warning", { warning }));
704
- }
705
- if (pacedCloser.scene) {
706
- yield emit(events.create("scene.add", {
707
- scene: pacedCloser.scene,
708
- position: state.config.scenes.length
709
- }));
710
- generatedSceneCount += 1;
711
- acceptedSceneCount += 1;
712
- closerCommitted = true;
713
- }
767
+ if (state.config) {
768
+ const recovery = recoverComposition(composition, state.config, compositionOptions);
769
+ for (const action of recovery.actions) yield emitCompositionAction(action);
770
+ composition = recovery.session;
714
771
  }
715
772
  const provider = await settleProviderLifecycle();
716
773
  for (const warning of provider.warnings) {
717
- if (!state.warnings.some((existing) => existing.code === warning.code && existing.message === warning.message)) {
774
+ if (!state.warnings.some(
775
+ (existing) => existing.code === warning.code && existing.message === warning.message
776
+ )) {
718
777
  yield emit(events.create("response.warning", { warning }));
719
778
  }
720
779
  }
@@ -752,7 +811,6 @@ function createVideo(rawInput, options) {
752
811
  return {
753
812
  request,
754
813
  initialConfig,
755
- initialStyle: initialConfig.style,
756
814
  stream,
757
815
  result,
758
816
  abort(reason = "user cancelled") {
@@ -173,11 +173,12 @@ function VideoFrame({
173
173
  const next = activeIndex < timeline.length - 1 ? timeline[activeIndex + 1] : void 0;
174
174
  const activeTemplate = kit.getTemplate(active.scene.templateId);
175
175
  const nextTemplate = next ? kit.getTemplate(next.scene.templateId) : void 0;
176
- const contiguousNext = next && rangesAreContiguous(active, next) && activeTemplate.usesGlobalTransition && activeTemplate.transitionTiming && nextTemplate?.usesGlobalTransition && nextTemplate.transitionTiming ? next : void 0;
176
+ const contiguousNext = next && rangesAreContiguous(active, next) ? next : void 0;
177
+ const transitionableNext = contiguousNext && activeTemplate.usesGlobalTransition && activeTemplate.transitionTiming && nextTemplate?.usesGlobalTransition && nextTemplate.transitionTiming ? contiguousNext : void 0;
177
178
  const activeTiming = activeTemplate.transitionTiming;
178
179
  const nextTiming = nextTemplate?.transitionTiming;
179
180
  const eligibleNextTransition = Boolean(
180
- transitionEnabled && contiguousNext && activeTiming && nextTiming && sceneBackgroundChanges(active, contiguousNext)
181
+ transitionEnabled && transitionableNext && activeTiming && nextTiming && sceneBackgroundChanges(active, transitionableNext)
181
182
  );
182
183
  const blendDuration = Math.min(
183
184
  SCENE_TRANSITION_SECONDS,
@@ -147,7 +147,7 @@ function getDriftTransform(progress, direction) {
147
147
  }
148
148
  function getBackgroundTransform(effect, progress, beatIntensity = 0, sceneIndex = 0) {
149
149
  const dir = DIRECTIONS[sceneIndex % DIRECTIONS.length];
150
- const resolved = effect ?? "slow-zoom-in";
150
+ const resolved = effect ?? "static";
151
151
  switch (resolved) {
152
152
  case "slow-zoom-in":
153
153
  return {