@vanillaskyai/video 0.1.1 → 0.3.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 (52) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/PUBLIC-API.md +20 -2
  3. package/README.md +14 -6
  4. package/dist/{builtin-server-F7YXRVCH.js → builtin-server-BRTZN4Q7.js} +2 -2
  5. package/dist/check-runtime.js +1 -1
  6. package/dist/{chunk-EVG2ZYIY.js → chunk-3O7OMMMF.js} +7 -4
  7. package/dist/{chunk-GRMUV7QY.js → chunk-BKF3A357.js} +1 -1
  8. package/dist/{chunk-YC5MFXA5.js → chunk-GR4I3MN2.js} +37 -15
  9. package/dist/{chunk-A2X45FET.js → chunk-I5YWVQ5R.js} +1 -1
  10. package/dist/{chunk-ERY4S6QV.js → chunk-JW47XCRL.js} +6 -1
  11. package/dist/{chunk-XB7LH4J2.js → chunk-QTMHS7JD.js} +2 -1
  12. package/dist/{chunk-W2XJKG6C.js → chunk-RBQN3VSY.js} +2 -2
  13. package/dist/{chunk-OOPIETLW.js → chunk-S5ZU5WUU.js} +1 -1
  14. package/dist/{chunk-ABPKM4GK.js → chunk-STIFILQG.js} +1 -1
  15. package/dist/chunk-WHPZED7X.js +57 -0
  16. package/dist/{chunk-CHLGW22F.js → chunk-XAMUOSX7.js} +3 -3
  17. package/dist/{chunk-IXCUY3B3.js → chunk-XYZOJ5NZ.js} +195 -52
  18. package/dist/cli.js +5 -5
  19. package/dist/{compose-video-CN3VSWST.js → compose-video-TQOHDXNW.js} +4 -4
  20. package/dist/{events-_wfjbmEp.d.ts → events-CTIsANzz.d.ts} +2 -2
  21. package/dist/index.d.ts +2 -2
  22. package/dist/{kit-BMNcHqa9.d.ts → kit-DA2cfJ96.d.ts} +1 -1
  23. package/dist/react.d.ts +7 -4
  24. package/dist/react.js +226 -30
  25. package/dist/server.d.ts +21 -4
  26. package/dist/server.js +205 -18
  27. package/dist/system-prompt-NMYHLAMJ.js +10 -0
  28. package/dist/template-catalog.js +1 -1
  29. package/dist/templates.d.ts +3 -3
  30. package/dist/test.d.ts +3 -2
  31. package/dist/test.js +3 -3
  32. package/dist/{text-stream-6XHC7J47.js → text-stream-UPGUD2TD.js} +2 -2
  33. package/dist/{types-B-PZR6G_.d.ts → types-CkO2EYr4.d.ts} +8 -4
  34. package/docs/concepts.md +11 -1
  35. package/docs/custom-templates.md +34 -6
  36. package/docs/customization.md +2 -1
  37. package/docs/errors.md +3 -1
  38. package/docs/getting-started.md +13 -10
  39. package/docs/input-and-first-scene.md +17 -6
  40. package/docs/integrate-nextjs.md +6 -3
  41. package/docs/media-and-audio.md +73 -14
  42. package/docs/prompt-and-input.md +27 -5
  43. package/docs/provider-integration.md +45 -2
  44. package/docs/reference/protocol.md +9 -0
  45. package/examples/nextjs-quickstart/README.md +3 -1
  46. package/examples/nextjs-quickstart/package.json +3 -2
  47. package/examples/nextjs-quickstart/src/app/api/video/route.ts +1 -1
  48. package/package.json +1 -1
  49. package/registry/items/brandMessage.json +1 -2
  50. package/registry/items/media.json +2 -1
  51. package/dist/chunk-FQ47WXAJ.js +0 -38
  52. package/dist/system-prompt-A6UXNTPK.js +0 -8
@@ -5,11 +5,11 @@ import {
5
5
  applyVideoEvent,
6
6
  checksumVideo,
7
7
  createVideoState
8
- } from "./chunk-ABPKM4GK.js";
8
+ } from "./chunk-STIFILQG.js";
9
9
  import {
10
10
  parseVideoPlanPart,
11
11
  safePublicDiagnostic
12
- } from "./chunk-ERY4S6QV.js";
12
+ } from "./chunk-JW47XCRL.js";
13
13
  import {
14
14
  MAX_RETAINED_INSTRUCTIONS_LENGTH,
15
15
  MAX_RETAINED_MEDIA_URLS,
@@ -26,7 +26,7 @@ import {
26
26
  getCloserReserve,
27
27
  getReadableSceneDuration,
28
28
  paceScene
29
- } from "./chunk-EVG2ZYIY.js";
29
+ } from "./chunk-3O7OMMMF.js";
30
30
  import {
31
31
  resolveVideoBrand
32
32
  } from "./chunk-XGRA2MUV.js";
@@ -95,19 +95,24 @@ function resolveSuppliedMediaPlanPart(part, input) {
95
95
  function buildVideoUserPrompt(input, openingDurationSec = 0) {
96
96
  return [
97
97
  "Compose a video response from the structured customer input below.",
98
+ `Knowledge mode: ${input.knowledgeMode ?? "input-only"}.`,
98
99
  `Maximum duration: ${input.maxDurationSec ?? 30} seconds, including the supplied opening.`,
99
100
  input.opening?.trim() ? `The host has already added the opening scene, which consumes ${openingDurationSec} seconds. Continue after it and do not repeat or rewrite it.` : "Add the first grounded scene as soon as it is complete.",
100
101
  "The first generated body scene must be fully playable without external media. Use a content-fit text, data, comparison, list, or device-free template with no media URL or keyword.",
101
102
  "Never use media, ctaMedia, or reaction as the first generated body template, even with mediaType=gradient. Choose a non-media template first.",
102
103
  "Add that scene before resolving any stock or supplied asset. Media belongs on later body scenes and must arrive without blocking scene additions.",
103
- "The raw input is the complete factual boundary. Do not add outside claims.",
104
+ "Use only claims supported by the factual basis permitted by the trusted system prompt.",
104
105
  "Select the most decision-relevant grounded takeaways that fit the duration; represent each selected takeaway once before completing the response.",
105
106
  "For a long source, summarize instead of attempting to represent every fact, unless the creative instructions explicitly request complete fact coverage that fits the duration.",
106
- "Preserve exact wording and numbers for the takeaways you select. Preserve qualifiers, units, denominators, ranges, and comparison direction; for example, do not shorten 4.8 out of 5 to 4.8.",
107
+ "When selecting claims from the raw input, preserve their exact wording and numbers. Preserve qualifiers, units, denominators, ranges, and comparison direction; for example, do not shorten 4.8 out of 5 to 4.8.",
107
108
  "Choose the scene count from the distinct grounded material and the duration budget. Use fewer scenes for sparse input; continue beyond five when rich input warrants it and timing allows.",
109
+ "If the creative instructions explicitly require one separate scene per named item, release, section, or list entry, do not merge, group, or omit those required items. Keep related required scenes adjacent in a coherent progression while preserving each item as its own scene.",
110
+ "Before emitting, verify that the explicitly requested structure can fit readably within the maximum duration. If it cannot, preserve readability and the requested separation for the scenes that fit, then finish with plan.complete using finishReason length rather than silently changing the structure.",
111
+ "For ordinary multi-fact input, form at least three distinct beats: hook, comprehension, and payoff. Use only one or two beats when the source genuinely contains no more than two independent grounded takeaways. This is a narrative guideline, never permission to repeat facts or add filler.",
112
+ "After the first playable body scene, emit exactly one scene.add with placement closer. Generate a short grounded conclusion that answers the story's so-what; it must not repeat hook language or imply another scene follows.",
108
113
  "Use a different suitable template for each body scene when the catalog supports it.",
109
114
  "Never add filler to satisfy a count or diversity target.",
110
- input.suppliedMedia?.length ? "Select zero or more relevant opaque supplied-media references for visible later scenes. Never invent or transform a reference, and never emit mediaKeyword without a resolved mediaUrl." : "Use asset-free templates. No media URL has been supplied or resolved. Never emit mediaKeyword without a resolved mediaUrl.",
115
+ input.suppliedMedia?.length ? "Select zero or more relevant opaque supplied-media references for visible later scenes. Never invent or transform a reference. Only emit mediaKeyword when the trusted system catalog explicitly exposes it, and only on later scenes. Never invent mediaUrl or mediaPoster." : "No supplied media URL is available. Use asset-free templates unless the trusted system catalog explicitly permits host-resolved media intent. Only emit mediaKeyword when the trusted system catalog explicitly exposes it, and only on later scenes. Never invent mediaUrl or mediaPoster.",
111
116
  "",
112
117
  "RAW INPUT",
113
118
  input.input.trim(),
@@ -130,6 +135,20 @@ function buildVideoUserPrompt(input, openingDurationSec = 0) {
130
135
  }
131
136
 
132
137
  // src/server/compose-video.ts
138
+ var DEFAULT_OPENING_TEXT = "Creating your video...";
139
+ function resolveVideoInput(input) {
140
+ return {
141
+ ...input,
142
+ opening: input.opening?.trim() || DEFAULT_OPENING_TEXT
143
+ };
144
+ }
145
+ function resolveStreamCapabilities(capabilities) {
146
+ if (capabilities?.templates == null) return capabilities;
147
+ return {
148
+ ...capabilities,
149
+ templates: [.../* @__PURE__ */ new Set(["media", ...capabilities.templates])]
150
+ };
151
+ }
133
152
  function invokeIsolated(callback, value) {
134
153
  if (!callback) return;
135
154
  try {
@@ -158,12 +177,12 @@ function buildInitialConfig(input, audio, snapshotRetention, closerReserveSec, g
158
177
  const openingText = input.opening?.trim();
159
178
  const rawScenes = openingText ? [{
160
179
  id: "supplied-opening",
161
- templateId: "notification",
180
+ templateId: "media",
162
181
  variables: {
163
- appName: input.brand?.name ?? "",
164
- message: openingText
182
+ texts: openingText,
183
+ mediaType: "gradient"
165
184
  },
166
- timing: { fixedDuration: 5 }
185
+ timing: { fixedDuration: 3 }
167
186
  }] : [];
168
187
  const scenes = [];
169
188
  const warnings = [];
@@ -213,6 +232,9 @@ function buildInitialConfig(input, audio, snapshotRetention, closerReserveSec, g
213
232
  }
214
233
  function validateInput(input) {
215
234
  if (!input.input.trim()) throw new Error("Video response input is required");
235
+ if (input.knowledgeMode != null && input.knowledgeMode !== "input-only" && input.knowledgeMode !== "general") {
236
+ throw new Error("Video response knowledge mode must be input-only or general");
237
+ }
216
238
  if (input.maxDurationSec != null && (!Number.isFinite(input.maxDurationSec) || input.maxDurationSec < 5 || input.maxDurationSec > 120)) {
217
239
  throw new Error("Video response maximum duration must be between 5 and 120 seconds");
218
240
  }
@@ -252,11 +274,42 @@ function createOmittedForCloserWarning(sceneId) {
252
274
  return {
253
275
  code: "scene_omitted_for_closer",
254
276
  category: "readability",
255
- message: "Scene was omitted because the final action must remain the last scene.",
277
+ message: "Scene was omitted because the reserved closer must remain the last scene.",
256
278
  sceneId,
257
279
  recoverable: true
258
280
  };
259
281
  }
282
+ function createDuplicateCloserWarning() {
283
+ return {
284
+ code: "scene_omitted_for_closer",
285
+ category: "readability",
286
+ message: "An additional closer was omitted because the first valid closer is already reserved.",
287
+ recoverable: true
288
+ };
289
+ }
290
+ function createIncompletePlanWarning() {
291
+ return {
292
+ code: "plan_incomplete",
293
+ category: "provider",
294
+ message: "The planner stopped at a length limit; some requested scenes or the ending may be missing.",
295
+ recoverable: true
296
+ };
297
+ }
298
+ function createMissingCloserWarning() {
299
+ return {
300
+ code: "plan_missing_closer",
301
+ category: "provider",
302
+ message: "The planner completed without a valid closer; the video may end on a body scene.",
303
+ recoverable: true
304
+ };
305
+ }
306
+ function pendingCloserReserve(scene, getTemplatePacing) {
307
+ const metadata = getTemplatePacing?.(scene.templateId);
308
+ const readableMinimum = getReadableSceneDuration(scene, metadata);
309
+ const explicitRange = scene.timing.startTime != null && scene.timing.endTime != null ? Math.max(0, scene.timing.endTime - scene.timing.startTime) : void 0;
310
+ const requested = explicitRange ?? scene.timing.fixedDuration ?? metadata?.preferredDuration ?? readableMinimum;
311
+ return Math.max(readableMinimum, requested);
312
+ }
260
313
  function sceneSlotDuration(scene) {
261
314
  return Math.max(0, (scene.timing.endTime ?? 0) - (scene.timing.startTime ?? 0));
262
315
  }
@@ -276,11 +329,10 @@ function createSceneQualityWarnings(scene) {
276
329
  recoverable: true
277
330
  }];
278
331
  }
279
- function createVideo(input, options) {
280
- validateInput(input);
281
- if (input.opening?.trim() && options.capabilities?.templates != null && !options.capabilities.templates.includes("notification")) {
282
- throw new Error("Scene template notification was not negotiated");
283
- }
332
+ function createVideo(rawInput, options) {
333
+ validateInput(rawInput);
334
+ const usesDefaultOpening = rawInput.opening == null;
335
+ const input = resolveVideoInput(rawInput);
284
336
  const requestId = options.requestId ?? createId("request");
285
337
  const runId = options.runId ?? createId("run");
286
338
  const request = createVideoRequest(input, {
@@ -379,13 +431,22 @@ function createVideo(input, options) {
379
431
  resolveResult(finalState);
380
432
  }
381
433
  };
434
+ let planCompleted = false;
435
+ let generatedSceneCount = 0;
436
+ let runtimeDurationLimited = false;
437
+ let pendingCloser;
438
+ const deferredForCloser = [];
439
+ let plannerReportedLength = false;
440
+ let closerCommitted = false;
441
+ let missingCloser = false;
442
+ let finishReason = "stop";
382
443
  try {
383
444
  yield emit(events.create("response.start", {
384
445
  requestId,
385
446
  format: { orientation: initialConfig.orientation ?? "portrait" },
386
447
  style: initialConfig.style,
387
448
  meta: initialConfig.meta,
388
- capabilities: options.capabilities
449
+ capabilities: resolveStreamCapabilities(options.capabilities)
389
450
  }));
390
451
  if (initialConfig.audio) yield emit(events.create("audio.set", { audio: initialConfig.audio }));
391
452
  for (const warning of initial.warnings) {
@@ -398,14 +459,7 @@ function createVideo(input, options) {
398
459
  revision: 0
399
460
  }));
400
461
  }
401
- let planCompleted = false;
402
- let generatedSceneCount = 0;
403
- let runtimeDurationLimited = false;
404
- let askAccepted = false;
405
- const deferredForCloser = [];
406
- let plannerReportedLength = false;
407
- let finishReason = "stop";
408
- const systemPrompt = options.systemPrompt ?? (await import("./system-prompt-A6UXNTPK.js")).DEFAULT_VIDEO_SYSTEM_PROMPT;
462
+ const systemPrompt = options.systemPrompt ?? (await import("./system-prompt-NMYHLAMJ.js")).createVideoSystemPrompt(input.knowledgeMode);
409
463
  const context = { request, systemPrompt, userPrompt, initialConfig, signal: controller.signal };
410
464
  attachGenerationLifecycleSink(context, lifecycle);
411
465
  for await (const untrustedPart of options.generate(context)) {
@@ -421,21 +475,29 @@ function createVideo(input, options) {
421
475
  if (options.capabilities?.templates != null && !options.capabilities.templates.includes(part.scene.templateId)) {
422
476
  throw new Error(`Scene template ${part.scene.templateId} was not negotiated`);
423
477
  }
424
- if (askAccepted) {
425
- runtimeDurationLimited = true;
426
- rejectedSceneCount += 1;
427
- yield emit(events.create("response.warning", {
428
- warning: createOmittedForCloserWarning(part.scene.id)
429
- }));
430
- continue;
431
- }
432
478
  options.validateScene?.(part.scene, {
433
479
  input,
434
480
  previousScenes: state.config?.scenes ?? []
435
481
  });
436
482
  const templatePacing = options.getTemplatePacing?.(part.scene.templateId);
437
483
  const isAskCandidate = templatePacing?.jobs?.includes("ask") === true;
438
- if (deferredForCloser.length && !isAskCandidate) {
484
+ const isPayoffCandidate = templatePacing?.jobs?.includes("payoff") === true;
485
+ if (part.placement === "closer" && templatePacing && !isAskCandidate && !isPayoffCandidate) {
486
+ throw new Error(`Scene template ${part.scene.templateId} cannot be used as a closer`);
487
+ }
488
+ const isCloserCandidate = part.placement === "closer" || isAskCandidate;
489
+ if (isCloserCandidate) {
490
+ if (pendingCloser) {
491
+ rejectedSceneCount += 1;
492
+ yield emit(events.create("response.warning", {
493
+ warning: createDuplicateCloserWarning()
494
+ }));
495
+ } else {
496
+ pendingCloser = part.scene;
497
+ }
498
+ continue;
499
+ }
500
+ if (deferredForCloser.length) {
439
501
  deferredForCloser.push(part.scene);
440
502
  continue;
441
503
  }
@@ -443,7 +505,7 @@ function createVideo(input, options) {
443
505
  previousScenes: state.config?.scenes ?? [],
444
506
  audio: state.config?.audio,
445
507
  maxDurationSec: input.maxDurationSec ?? 30,
446
- closerReserveSec,
508
+ closerReserveSec: pendingCloser ? pendingCloserReserve(pendingCloser, options.getTemplatePacing) : closerReserveSec,
447
509
  getTemplatePacing: options.getTemplatePacing
448
510
  });
449
511
  if (!paced.scene) {
@@ -459,16 +521,6 @@ function createVideo(input, options) {
459
521
  continue;
460
522
  }
461
523
  const scene = paced.scene;
462
- const isAsk = isAskCandidate;
463
- if (isAsk && deferredForCloser.length) {
464
- runtimeDurationLimited = true;
465
- for (const deferred of deferredForCloser.splice(0)) {
466
- rejectedSceneCount += 1;
467
- yield emit(events.create("response.warning", {
468
- warning: createOmittedForCloserWarning(deferred.id)
469
- }));
470
- }
471
- }
472
524
  for (const warning of paced.warnings) {
473
525
  yield emit(events.create("response.warning", { warning }));
474
526
  }
@@ -480,13 +532,24 @@ function createVideo(input, options) {
480
532
  position: state.config?.scenes.length ?? 0,
481
533
  revision: 0
482
534
  }));
483
- if (isAsk) {
484
- askAccepted = true;
485
- }
486
535
  generatedSceneCount += 1;
487
536
  acceptedSceneCount += 1;
488
537
  timeToFirstSceneMs ??= Math.max(0, now() - startedAt);
489
538
  } else if (part.type === "scene.patch") {
539
+ if (pendingCloser?.id === part.sceneId) {
540
+ const nextPendingCloser = {
541
+ ...pendingCloser,
542
+ ...part.patch,
543
+ variables: part.patch.variables ? { ...pendingCloser.variables, ...part.patch.variables } : pendingCloser.variables,
544
+ timing: part.patch.timing ? { ...pendingCloser.timing, ...part.patch.timing } : pendingCloser.timing
545
+ };
546
+ options.validateScene?.(nextPendingCloser, {
547
+ input,
548
+ previousScenes: state.config?.scenes ?? []
549
+ });
550
+ pendingCloser = nextPendingCloser;
551
+ continue;
552
+ }
490
553
  const scenes = state.config?.scenes ?? [];
491
554
  const sceneIndex = scenes.findIndex((scene) => scene.id === part.sceneId);
492
555
  if (sceneIndex < 0) throw new Error(`Scene ${part.sceneId} does not exist`);
@@ -521,7 +584,7 @@ function createVideo(input, options) {
521
584
  previousScenes: scenes.slice(0, sceneIndex),
522
585
  audio: state.config?.audio,
523
586
  maxDurationSec: input.maxDurationSec ?? 30,
524
- closerReserveSec,
587
+ closerReserveSec: pendingCloser ? pendingCloserReserve(pendingCloser, options.getTemplatePacing) : closerReserveSec,
525
588
  getTemplatePacing: options.getTemplatePacing
526
589
  });
527
590
  if (!pacedPatch.scene) {
@@ -561,6 +624,18 @@ function createVideo(input, options) {
561
624
  patch
562
625
  }));
563
626
  } else if (part.type === "asset.patch") {
627
+ if (pendingCloser?.id === part.sceneId) {
628
+ const nextPendingCloser = {
629
+ ...pendingCloser,
630
+ variables: { ...pendingCloser.variables, ...part.variables }
631
+ };
632
+ options.validateScene?.(nextPendingCloser, {
633
+ input,
634
+ previousScenes: state.config?.scenes ?? []
635
+ });
636
+ pendingCloser = nextPendingCloser;
637
+ continue;
638
+ }
564
639
  const scenes = state.config?.scenes ?? [];
565
640
  const scene = scenes.find((item) => item.id === part.sceneId);
566
641
  if (!scene) throw new Error(`Scene ${part.sceneId} does not exist`);
@@ -604,7 +679,39 @@ function createVideo(input, options) {
604
679
  yield errorEvent;
605
680
  } else if (part.type === "plan.complete") {
606
681
  if (planCompleted) throw new Error("The planner emitted plan.complete more than once");
607
- if (!askAccepted && deferredForCloser.length) {
682
+ if (pendingCloser) {
683
+ if (deferredForCloser.length) runtimeDurationLimited = true;
684
+ for (const deferred of deferredForCloser.splice(0)) {
685
+ rejectedSceneCount += 1;
686
+ yield emit(events.create("response.warning", {
687
+ warning: createOmittedForCloserWarning(deferred.id)
688
+ }));
689
+ }
690
+ const pacedCloser = paceScene(pendingCloser, {
691
+ previousScenes: state.config?.scenes ?? [],
692
+ audio: state.config?.audio,
693
+ maxDurationSec: input.maxDurationSec ?? 30,
694
+ closerReserveSec: 0,
695
+ getTemplatePacing: options.getTemplatePacing
696
+ });
697
+ for (const warning of pacedCloser.warnings) {
698
+ yield emit(events.create("response.warning", { warning }));
699
+ }
700
+ if (pacedCloser.scene) {
701
+ yield emit(events.create("scene.add", {
702
+ scene: pacedCloser.scene,
703
+ position: state.config?.scenes.length ?? 0,
704
+ revision: 0
705
+ }));
706
+ generatedSceneCount += 1;
707
+ acceptedSceneCount += 1;
708
+ timeToFirstSceneMs ??= Math.max(0, now() - startedAt);
709
+ closerCommitted = true;
710
+ } else {
711
+ runtimeDurationLimited = true;
712
+ rejectedSceneCount += 1;
713
+ }
714
+ } else if (deferredForCloser.length) {
608
715
  for (const deferred of deferredForCloser.splice(0)) {
609
716
  const recovered = paceScene(deferred, {
610
717
  previousScenes: state.config?.scenes ?? [],
@@ -634,8 +741,15 @@ function createVideo(input, options) {
634
741
  timeToFirstSceneMs ??= Math.max(0, now() - startedAt);
635
742
  }
636
743
  }
744
+ if (options.requireCloser && !closerCommitted) {
745
+ missingCloser = true;
746
+ yield emit(events.create("response.warning", {
747
+ warning: createMissingCloserWarning()
748
+ }));
749
+ }
637
750
  planCompleted = true;
638
- finishReason = runtimeDurationLimited ? "length" : part.finishReason ?? "stop";
751
+ const reportedFinishReason = part.finishReason ?? "stop";
752
+ finishReason = runtimeDurationLimited ? "length" : reportedFinishReason !== "stop" ? reportedFinishReason : missingCloser ? "other" : "stop";
639
753
  plannerReportedLength = part.finishReason === "length";
640
754
  continue;
641
755
  }
@@ -665,7 +779,14 @@ function createVideo(input, options) {
665
779
  if (plannerReportedLength && generatedSceneCount === 0) {
666
780
  throw new Error("The planner was truncated before adding a generated scene");
667
781
  }
668
- if (!state.config?.scenes.length) throw new Error("The planner completed without adding a scene");
782
+ if (plannerReportedLength) {
783
+ yield emit(events.create("response.warning", {
784
+ warning: createIncompletePlanWarning()
785
+ }));
786
+ }
787
+ if (generatedSceneCount === 0 && usesDefaultOpening) {
788
+ throw new Error("The planner completed without adding a scene");
789
+ }
669
790
  const snapshot = parseVideo(state.config);
670
791
  const completeEvent = emit(events.create("response.complete", {
671
792
  finishReason,
@@ -699,6 +820,28 @@ function createVideo(input, options) {
699
820
  return;
700
821
  }
701
822
  try {
823
+ if (pendingCloser && !closerCommitted && state.config?.scenes.length) {
824
+ const pacedCloser = paceScene(pendingCloser, {
825
+ previousScenes: state.config.scenes,
826
+ audio: state.config.audio,
827
+ maxDurationSec: input.maxDurationSec ?? 30,
828
+ closerReserveSec: 0,
829
+ getTemplatePacing: options.getTemplatePacing
830
+ });
831
+ for (const warning of pacedCloser.warnings) {
832
+ yield emit(events.create("response.warning", { warning }));
833
+ }
834
+ if (pacedCloser.scene) {
835
+ yield emit(events.create("scene.add", {
836
+ scene: pacedCloser.scene,
837
+ position: state.config.scenes.length,
838
+ revision: 0
839
+ }));
840
+ generatedSceneCount += 1;
841
+ acceptedSceneCount += 1;
842
+ closerCommitted = true;
843
+ }
844
+ }
702
845
  const provider = await settleProviderLifecycle();
703
846
  for (const warning of provider.warnings) {
704
847
  if (!state.warnings.some((existing) => existing.code === warning.code && existing.message === warning.message)) {
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-YC5MFXA5.js";
3
- import "./chunk-FQ47WXAJ.js";
4
- import "./chunk-EVG2ZYIY.js";
2
+ import "./chunk-GR4I3MN2.js";
3
+ import "./chunk-WHPZED7X.js";
4
+ import "./chunk-3O7OMMMF.js";
5
5
  import {
6
6
  isTemplatePropertyFormatSupported,
7
7
  validateTemplateSchemaValue
@@ -9,13 +9,13 @@ import {
9
9
  import {
10
10
  getBuiltinTemplateMetadata,
11
11
  listBuiltinTemplateMetadata
12
- } from "./chunk-CHLGW22F.js";
12
+ } from "./chunk-XAMUOSX7.js";
13
13
  import {
14
14
  assertTemplateTransitionMetadata
15
15
  } from "./chunk-2E6T633S.js";
16
16
  import {
17
17
  BUILTIN_TEMPLATE_MANIFEST
18
- } from "./chunk-W2XJKG6C.js";
18
+ } from "./chunk-RBQN3VSY.js";
19
19
  import {
20
20
  getTemplateDefaults,
21
21
  isMeaningfullyPresentTemplateValue,
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  createVideo
3
- } from "./chunk-IXCUY3B3.js";
3
+ } from "./chunk-XYZOJ5NZ.js";
4
4
  import "./chunk-E7CL7UPB.js";
5
- import "./chunk-ABPKM4GK.js";
6
- import "./chunk-ERY4S6QV.js";
5
+ import "./chunk-STIFILQG.js";
6
+ import "./chunk-JW47XCRL.js";
7
7
  import "./chunk-GHJEAP4O.js";
8
8
  import "./chunk-EE6PK6XC.js";
9
- import "./chunk-EVG2ZYIY.js";
9
+ import "./chunk-3O7OMMMF.js";
10
10
  import "./chunk-XGRA2MUV.js";
11
11
  export {
12
12
  createVideo
@@ -1,7 +1,7 @@
1
- import { e as VideoOrientation, g as VideoStyle, V as Video, j as VideoCapabilities, a as VideoAudio, f as VideoScene, k as VIDEO_PROTOCOL_VERSION } from './types-B-PZR6G_.js';
1
+ import { f as VideoOrientation, h as VideoStyle, V as Video, k as VideoCapabilities, a as VideoAudio, g as VideoScene, l as VIDEO_PROTOCOL_VERSION } from './types-CkO2EYr4.js';
2
2
 
3
3
  type VideoWarningCategory = "validation" | "readability" | "grounding" | "provider" | "media" | "protocol";
4
- type VideoWarningCode = "scene_duration_adjusted" | "scene_omitted_unreadable" | "scene_omitted_for_closer" | "scene_patch_rejected_readability" | "chart_scale_imbalance" | "provider_warning" | "provider_diagnostics_unavailable";
4
+ type VideoWarningCode = "scene_duration_adjusted" | "scene_omitted_unreadable" | "scene_omitted_for_closer" | "scene_patch_rejected_readability" | "chart_scale_imbalance" | "plan_incomplete" | "plan_missing_closer" | "provider_warning" | "provider_diagnostics_unavailable";
5
5
  /** A bounded, client-safe diagnostic that never includes provider payloads. */
6
6
  interface VideoWarning {
7
7
  code: VideoWarningCode;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { V as Video } from './types-B-PZR6G_.js';
2
- export { a as VideoAudio, b as VideoBackground, c as VideoBrand, d as VideoInput, e as VideoOrientation, f as VideoScene, g as VideoStyle, h as VideoStyleOptions, i as VideoSuppliedMedia } from './types-B-PZR6G_.js';
1
+ import { V as Video } from './types-CkO2EYr4.js';
2
+ export { a as VideoAudio, b as VideoBackground, c as VideoBrand, d as VideoInput, e as VideoKnowledgeMode, f as VideoOrientation, g as VideoScene, h as VideoStyle, i as VideoStyleOptions, j as VideoSuppliedMedia } from './types-CkO2EYr4.js';
3
3
  export { V as VideoStatus } from './state-DZcKuS32.js';
4
4
 
5
5
  type VideoValidationErrorCode = "invalid_video" | "unsupported_video_version";
@@ -1,4 +1,4 @@
1
- import { g as VideoStyle, j as VideoCapabilities } from './types-B-PZR6G_.js';
1
+ import { h as VideoStyle, k as VideoCapabilities } from './types-CkO2EYr4.js';
2
2
  import { ComponentType } from 'react';
3
3
  import { S as SceneTemplateMetadata, d as TemplateJsonSchema, f as TemplateJsonSchemaProperty } from './catalog-types-BIhSpOWK.js';
4
4
 
package/dist/react.d.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  import { CSSProperties, ReactElement } from 'react';
2
- import { V as VideoEvent, a as VideoWarning } from './events-_wfjbmEp.js';
3
- import { e as VideoOrientation, V as Video, d as VideoInput } from './types-B-PZR6G_.js';
4
- import { T as TemplateRegistry } from './kit-BMNcHqa9.js';
2
+ import { V as VideoEvent, a as VideoWarning } from './events-CTIsANzz.js';
3
+ import { f as VideoOrientation, V as Video, d as VideoInput } from './types-CkO2EYr4.js';
4
+ import { T as TemplateRegistry } from './kit-DA2cfJ96.js';
5
5
  import { V as VideoStatus } from './state-DZcKuS32.js';
6
6
  import './catalog-types-BIhSpOWK.js';
7
7
 
8
+ type VideoPlaybackMode = "manual" | "muted-autoplay" | "autoplay-with-sound" | "autoplay-after-interaction";
8
9
  interface VideoPlayerSharedProps {
10
+ /** High-level startup policy. When set, this overrides autoPlay and startMuted. */
11
+ playbackMode?: VideoPlaybackMode;
9
12
  /** Initial playback state. Reduced-motion preferences take precedence. */
10
13
  autoPlay?: boolean;
11
14
  startMuted?: boolean;
@@ -78,4 +81,4 @@ interface UseVideoResult {
78
81
  }
79
82
  declare function useVideo(options?: UseVideoOptions): UseVideoResult;
80
83
 
81
- export { type UseVideoOptions, type UseVideoResult, VideoError, type VideoErrorOptions, VideoPlayer, type VideoPlayerProps, useVideo };
84
+ export { type UseVideoOptions, type UseVideoResult, VideoError, type VideoErrorOptions, type VideoPlaybackMode, VideoPlayer, type VideoPlayerProps, useVideo };