@vanillaskyai/video 0.9.0 → 0.10.1

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 (45) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/{builtin-server-HW6HGXCZ.js → builtin-server-W4PLJUZ5.js} +2 -2
  3. package/dist/check-runtime.js +2 -2
  4. package/dist/{chunk-UDIODEDK.js → chunk-224QNWRA.js} +5 -1
  5. package/dist/{chunk-MXEYPK7M.js → chunk-4ZJLPHBV.js} +194 -62
  6. package/dist/{chunk-ZDJNFA52.js → chunk-5BSJLT6H.js} +6 -6
  7. package/dist/{chunk-G7IZBBWF.js → chunk-5C6HZNHY.js} +12 -14
  8. package/dist/{chunk-3ZTEFJIR.js → chunk-6KZGF63O.js} +47 -12
  9. package/dist/{chunk-LH4BPALA.js → chunk-IIN5M5HW.js} +194 -61
  10. package/dist/{chunk-MQILEEXC.js → chunk-IR44XKBI.js} +1 -1
  11. package/dist/{chunk-OR52UMSF.js → chunk-JKVOBTRO.js} +3 -2
  12. package/dist/chunk-KHLO5OHT.js +35 -0
  13. package/dist/{chunk-PAJ44TFW.js → chunk-LVM5Q2DL.js} +8 -6
  14. package/dist/chunk-VQH3JTQC.js +16 -0
  15. package/dist/{cinema-media-YQHSIFDA.js → cinema-media-APDJS7SC.js} +3 -2
  16. package/dist/cli.js +4 -4
  17. package/dist/{comparison-RZMMHZR2.js → comparison-ZVE2DE7U.js} +14 -4
  18. package/dist/{compose-video-TBFTONUC.js → compose-video-BH5K6XWT.js} +1 -1
  19. package/dist/{editorial-timeline-OZX7OS6G.js → editorial-timeline-YIXXVJUV.js} +10 -3
  20. package/dist/key-figure-I2C37FWV.js +30 -0
  21. package/dist/{mobile-message-UZUPIKHD.js → mobile-message-JSGBU6EX.js} +3 -2
  22. package/dist/{preload-media-CFTZIMZL.js → preload-media-LJXWKTWG.js} +1 -1
  23. package/dist/quote-XH54G3FS.js +33 -0
  24. package/dist/react.js +58 -33
  25. package/dist/server.js +106 -27
  26. package/dist/{system-prompt-SYRT5DWM.js → system-prompt-AG26KJAA.js} +1 -1
  27. package/dist/template-catalog.d.ts +154 -19
  28. package/dist/template-catalog.js +1 -1
  29. package/dist/test.js +2 -2
  30. package/docs/customization.md +1 -1
  31. package/docs/maintainers/cinematic-migration.md +4 -0
  32. package/docs/media-and-audio.md +3 -3
  33. package/docs/provider-integration.md +1 -1
  34. package/package.json +1 -1
  35. package/registry/items/cinemaMedia.json +1 -1
  36. package/registry/items/comparison.json +71 -4
  37. package/registry/items/editorialTimeline.json +71 -4
  38. package/registry/items/keyFigure.json +69 -4
  39. package/registry/items/mobileMessage.json +1 -1
  40. package/registry/items/quote.json +71 -4
  41. package/starters/video-chat/package.json +1 -1
  42. package/dist/focus-cards-TJ72BT7U.js +0 -20
  43. package/dist/key-figure-GYENXKH3.js +0 -20
  44. package/dist/quote-EW5HQA6H.js +0 -23
  45. package/registry/items/focusCards.json +0 -90
@@ -0,0 +1,33 @@
1
+ import {
2
+ EditorialSurface
3
+ } from "./chunk-KHLO5OHT.js";
4
+ import "./chunk-5C6HZNHY.js";
5
+ import "./chunk-77VY4O7A.js";
6
+ import {
7
+ fade
8
+ } from "./chunk-4YM2M62S.js";
9
+ import {
10
+ hasSceneMedia
11
+ } from "./chunk-224QNWRA.js";
12
+ import "./chunk-TVIU23OM.js";
13
+
14
+ // src/visual-system/scene-templates/quote.tsx
15
+ import { jsx, jsxs } from "react/jsx-runtime";
16
+ function QuoteScene(props) {
17
+ const { variables, width, height, progress, motionProgress = progress, safeZone } = props;
18
+ const overMedia = hasSceneMedia(variables);
19
+ const u = Math.min(width, height), top = Math.max(height * 0.16, safeZone.top), bottom = height - Math.max(height * 0.23, safeZone.bottom);
20
+ const side = Math.max(width * 0.13, safeZone.left, safeZone.right);
21
+ return /* @__PURE__ */ jsx(EditorialSurface, { ...props, template: "quote", children: /* @__PURE__ */ jsxs("div", { style: { position: "absolute", left: side, top: (top + bottom) / 2, width: width - side * 2, transform: "translateY(-50%)", textAlign: "center", opacity: fade((motionProgress - 0.03) / 0.24) }, children: [
22
+ /* @__PURE__ */ jsxs("div", { style: { fontSize: u * 0.067, fontWeight: 500, lineHeight: 1.25, letterSpacing: "-.025em", textWrap: "balance", overflowWrap: "anywhere" }, children: [
23
+ "\u201C",
24
+ String(variables.quote ?? ""),
25
+ "\u201D"
26
+ ] }),
27
+ /* @__PURE__ */ jsx("div", { style: { fontSize: u * 0.041, fontWeight: 400, color: overMedia ? "#fff" : "#b7b7bc", lineHeight: 1.3, marginTop: u * 0.065, textWrap: "balance", overflowWrap: "anywhere" }, children: String(variables.attribution ?? "") })
28
+ ] }) });
29
+ }
30
+ var QuoteSceneTemplate = QuoteScene;
31
+ export {
32
+ QuoteSceneTemplate
33
+ };
package/dist/react.js CHANGED
@@ -30,7 +30,7 @@ import {
30
30
  } from "./chunk-7NZOMTAL.js";
31
31
  import {
32
32
  getBuiltinTemplateMetadata
33
- } from "./chunk-LH4BPALA.js";
33
+ } from "./chunk-IIN5M5HW.js";
34
34
  import "./chunk-3O7OMMMF.js";
35
35
  import {
36
36
  assertTemplateTransitionMetadata
@@ -40,15 +40,16 @@ import {
40
40
  VideoFrame,
41
41
  getDimensions,
42
42
  sceneReadinessKey
43
- } from "./chunk-3ZTEFJIR.js";
44
- import "./chunk-UDIODEDK.js";
43
+ } from "./chunk-6KZGF63O.js";
44
+ import "./chunk-224QNWRA.js";
45
45
  import "./chunk-TVIU23OM.js";
46
46
  import {
47
47
  getVideoDuration,
48
48
  resolveVideoTimeline
49
49
  } from "./chunk-SPVTJH3F.js";
50
50
  import {
51
- markExternalVideoBackdropTemplate
51
+ markExternalVideoBackdropTemplate,
52
+ supportsExternalVideoBackdrop
52
53
  } from "./chunk-R3XAOMKP.js";
53
54
  import "./chunk-73NTSFFI.js";
54
55
 
@@ -93,14 +94,13 @@ import { createElement } from "react";
93
94
 
94
95
  // src/visual-system/catalog/builtin-loaders.generated.ts
95
96
  var GENERATED_BUILTIN_TEMPLATE_LOADERS = {
96
- "cinemaMedia": () => import("./cinema-media-YQHSIFDA.js").then((module) => ({ default: module.MediaSceneTemplate })),
97
+ "cinemaMedia": () => import("./cinema-media-APDJS7SC.js").then((module) => ({ default: module.MediaSceneTemplate })),
97
98
  "chapterTitle": () => import("./chapter-title-YRLIQY3V.js").then((module) => ({ default: module.TitleSceneTemplate })),
98
- "focusCards": () => import("./focus-cards-TJ72BT7U.js").then((module) => ({ default: module.CardsSceneTemplate })),
99
- "editorialTimeline": () => import("./editorial-timeline-OZX7OS6G.js").then((module) => ({ default: module.TimelineSceneTemplate })),
100
- "mobileMessage": () => import("./mobile-message-UZUPIKHD.js").then((module) => ({ default: module.NotificationSceneTemplate })),
101
- "comparison": () => import("./comparison-RZMMHZR2.js").then((module) => ({ default: module.ComparisonSceneTemplate })),
102
- "quote": () => import("./quote-EW5HQA6H.js").then((module) => ({ default: module.QuoteSceneTemplate })),
103
- "keyFigure": () => import("./key-figure-GYENXKH3.js").then((module) => ({ default: module.KeyFigureSceneTemplate }))
99
+ "editorialTimeline": () => import("./editorial-timeline-YIXXVJUV.js").then((module) => ({ default: module.TimelineSceneTemplate })),
100
+ "mobileMessage": () => import("./mobile-message-JSGBU6EX.js").then((module) => ({ default: module.NotificationSceneTemplate })),
101
+ "comparison": () => import("./comparison-ZVE2DE7U.js").then((module) => ({ default: module.ComparisonSceneTemplate })),
102
+ "quote": () => import("./quote-XH54G3FS.js").then((module) => ({ default: module.QuoteSceneTemplate })),
103
+ "keyFigure": () => import("./key-figure-I2C37FWV.js").then((module) => ({ default: module.KeyFigureSceneTemplate }))
104
104
  };
105
105
 
106
106
  // src/visual-system/catalog/builtin-player.generated.ts
@@ -121,17 +121,6 @@ var GENERATED_BUILTIN_PLAYER_TEMPLATES = [
121
121
  },
122
122
  "usesGlobalTransition": false
123
123
  },
124
- {
125
- "id": "focusCards",
126
- "defaults": {
127
- "items": [
128
- "Listen closely",
129
- "Notice the pattern",
130
- "Make room for change"
131
- ]
132
- },
133
- "usesGlobalTransition": false
134
- },
135
124
  {
136
125
  "id": "editorialTimeline",
137
126
  "defaults": {
@@ -145,7 +134,10 @@ var GENERATED_BUILTIN_PLAYER_TEMPLATES = [
145
134
  {
146
135
  "label": "Act"
147
136
  }
148
- ]
137
+ ],
138
+ "mediaUrl": "",
139
+ "mediaType": "video",
140
+ "mediaPoster": ""
149
141
  },
150
142
  "usesGlobalTransition": false
151
143
  },
@@ -164,22 +156,33 @@ var GENERATED_BUILTIN_PLAYER_TEMPLATES = [
164
156
  "id": "comparison",
165
157
  "defaults": {
166
158
  "leftText": "More distractions",
167
- "rightText": "More room to think"
159
+ "rightText": "More room to think",
160
+ "mediaUrl": "",
161
+ "mediaType": "video",
162
+ "mediaPoster": ""
168
163
  },
169
164
  "usesGlobalTransition": false
170
165
  },
171
166
  {
172
167
  "id": "quote",
173
- "defaults": {},
168
+ "defaults": {
169
+ "mediaUrl": "",
170
+ "mediaType": "video",
171
+ "mediaPoster": ""
172
+ },
174
173
  "usesGlobalTransition": false
175
174
  },
176
175
  {
177
176
  "id": "keyFigure",
178
- "defaults": {},
177
+ "defaults": {
178
+ "mediaUrl": "",
179
+ "mediaType": "video",
180
+ "mediaPoster": ""
181
+ },
179
182
  "usesGlobalTransition": false
180
183
  }
181
184
  ];
182
- var GENERATED_BUILTIN_VIDEO_BACKDROP_IDS = ["cinemaMedia", "mobileMessage"];
185
+ var GENERATED_BUILTIN_VIDEO_BACKDROP_IDS = ["cinemaMedia", "editorialTimeline", "mobileMessage", "comparison", "quote", "keyFigure"];
183
186
 
184
187
  // src/visual-system/catalog/builtin-player.ts
185
188
  function createPreloadableBuiltinTemplate(loader, mediaBackdrop) {
@@ -235,7 +238,7 @@ function preloadBuiltinTemplate(id) {
235
238
 
236
239
  // src/player/warm-scene-media.ts
237
240
  function warmSceneMedia(variables) {
238
- void import("./preload-media-CFTZIMZL.js").then((module) => module.preloadSceneMedia(variables)).catch(() => {
241
+ void import("./preload-media-LJXWKTWG.js").then((module) => module.preloadSceneMedia(variables)).catch(() => {
239
242
  });
240
243
  }
241
244
 
@@ -1236,6 +1239,19 @@ var VideoError = class extends Error {
1236
1239
  }
1237
1240
  };
1238
1241
 
1242
+ // src/player/recover-scene-media.ts
1243
+ function recoverSceneMedia(scene) {
1244
+ if (scene.templateId === "cinemaMedia") {
1245
+ const title = scene.variables.fallbackText;
1246
+ if (typeof title !== "string" || !title.trim() || [...title].length > 65) return void 0;
1247
+ return { ...scene, templateId: "chapterTitle", variables: { title: title.trim() } };
1248
+ }
1249
+ if (!["comparison", "editorialTimeline", "quote", "keyFigure"].includes(scene.templateId)) return void 0;
1250
+ const variables = { ...scene.variables };
1251
+ for (const key of ["mediaUrl", "mediaPoster", "mediaType", "mediaKeyword", "mediaSource"]) delete variables[key];
1252
+ return { ...scene, variables };
1253
+ }
1254
+
1239
1255
  // src/video-chat/use-video-chat.ts
1240
1256
  import { useCallback as useCallback2, useEffect as useEffect4, useReducer, useRef as useRef3 } from "react";
1241
1257
 
@@ -2421,8 +2437,18 @@ function useVideoChatSession(options = {}) {
2421
2437
  planned[position] = plannedScene;
2422
2438
  received[position] = plannedScene;
2423
2439
  const rendererReady = currentOptions.templates?.getTemplate(plannedScene.templateId) ? Promise.resolve() : Promise.resolve(preloadBuiltinTemplate(plannedScene.templateId));
2424
- const visualPreparation = Promise.all([rendererReady, prepareSceneMedia(plannedScene.variables, controller.signal)]).then(() => void 0, () => {
2425
- throw new VideoError("Scene could not prepare its visual", { code: "media_not_ready" });
2440
+ const visualPreparation = rendererReady.then(async () => {
2441
+ try {
2442
+ await prepareSceneMedia(plannedScene.variables, controller.signal);
2443
+ return plannedScene;
2444
+ } catch (cause) {
2445
+ if (controller.signal.aborted) throw cause;
2446
+ const override = currentOptions.templates?.getTemplate(plannedScene.templateId);
2447
+ const fallback = !override || supportsExternalVideoBackdrop(override) ? recoverSceneMedia(plannedScene) : void 0;
2448
+ if (!fallback) throw new VideoError("Scene could not prepare its visual", { code: "media_not_ready" });
2449
+ warn(MEDIA_RECOVERY_NOTICE);
2450
+ return fallback;
2451
+ }
2426
2452
  });
2427
2453
  void visualPreparation.catch(() => void 0);
2428
2454
  warmSceneMedia(plannedScene.variables);
@@ -2451,15 +2477,14 @@ function useVideoChatSession(options = {}) {
2451
2477
  narrating = narrated.catch(() => "");
2452
2478
  pending.push(narrated.then(async (line) => {
2453
2479
  if (!isCurrent() || currentAttempt !== attempt) return;
2454
- const visual = plannedScene;
2455
- const withNarration = line ? { ...visual, narration: line } : visual;
2456
2480
  const spoken = line ? await prepareSpeech(plannedScene.narrationGroup?.text ?? line, controller.signal).catch((cause) => {
2457
2481
  if (controller.signal.aborted) throw cause;
2458
2482
  warn("Some narration is unavailable; the response will continue.");
2459
2483
  return void 0;
2460
2484
  }) : void 0;
2461
2485
  if (!isCurrent() || currentAttempt !== attempt) return;
2462
- await visualPreparation;
2486
+ const visual = await visualPreparation;
2487
+ const withNarration = line ? { ...visual, narration: line } : visual;
2463
2488
  const group = plannedScene.narrationGroup;
2464
2489
  if (group && (spoken?.supportsOffsets !== true || voiceRef.current.supportsOffsets !== true || Math.abs(spoken.seconds - group.totalSeconds) > 0.1)) throw new VideoError("Narration group requires matching measured audio with offset support", { code: "narration_group_invalid" });
2465
2490
  ready[position] = group ? withNarration : pacedScene(withNarration, spoken?.seconds, currentOptions.templates);
package/dist/server.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  createVideo,
6
6
  invokeIsolated
7
- } from "./chunk-ZDJNFA52.js";
7
+ } from "./chunk-5BSJLT6H.js";
8
8
  import {
9
9
  getGenerationLifecycleSink
10
10
  } from "./chunk-E7CL7UPB.js";
@@ -12,7 +12,7 @@ import {
12
12
  BUILTIN_SERVER_TEMPLATE_KIT,
13
13
  createServerTemplateRegistry,
14
14
  overlayServerTemplateRegistry
15
- } from "./chunk-MQILEEXC.js";
15
+ } from "./chunk-IR44XKBI.js";
16
16
  import {
17
17
  createTemplateSceneValidator,
18
18
  validateTemplateSceneStructure
@@ -47,14 +47,77 @@ import {
47
47
  import {
48
48
  createTemplateSystemPrompt,
49
49
  getStandardMediaResolverContract
50
- } from "./chunk-OR52UMSF.js";
51
- import "./chunk-PAJ44TFW.js";
50
+ } from "./chunk-JKVOBTRO.js";
51
+ import "./chunk-LVM5Q2DL.js";
52
52
  import "./chunk-EGVQODKU.js";
53
- import "./chunk-LH4BPALA.js";
53
+ import "./chunk-IIN5M5HW.js";
54
54
  import "./chunk-3O7OMMMF.js";
55
55
  import "./chunk-2E6T633S.js";
56
56
  import "./chunk-73NTSFFI.js";
57
57
 
58
+ // src/server/opening-continuity.ts
59
+ function continueAfterOpening(narration, earlier) {
60
+ const words = (value) => Array.from(value.matchAll(/[\p{L}\p{N}]+(?:['’][\p{L}\p{N}]+)*/gu));
61
+ let remaining = narration;
62
+ for (const previous of [...earlier].reverse()) {
63
+ const known = words(previous), current = words(remaining);
64
+ if (known.length < 5 || known.length > current.length) continue;
65
+ if (!known.every((word, index) => word[0].toLowerCase() === current[index][0].toLowerCase())) continue;
66
+ if (known.length === current.length) return "";
67
+ const last = current[known.length - 1];
68
+ const suffix = remaining.slice(last.index + last[0].length);
69
+ const boundary = suffix.match(/^[”"'’\])]*[.!?…]+[\s”"'’\])]*/u);
70
+ if (boundary) remaining = suffix.slice(boundary[0].length).trim();
71
+ }
72
+ return remaining;
73
+ }
74
+ function createOpeningContinuation(initialOpening) {
75
+ const earlier = initialOpening ? [initialOpening] : [];
76
+ let bodyStarted = false;
77
+ const copy = (authored, narration) => {
78
+ if (continueAfterOpening(authored, earlier) !== "") return authored;
79
+ const next = continueAfterOpening(narration, earlier).trim();
80
+ return next && [...next].length <= 65 && /[.!?…][”"’')\]]*$/u.test(next) ? next : authored;
81
+ };
82
+ return {
83
+ copy,
84
+ remember(line) {
85
+ if (line.trim()) earlier.push(line);
86
+ },
87
+ narration(line) {
88
+ return continueAfterOpening(line, earlier);
89
+ },
90
+ line(rawLine) {
91
+ if (bodyStarted || earlier.length === 0) return rawLine;
92
+ let part;
93
+ try {
94
+ part = JSON.parse(rawLine);
95
+ } catch {
96
+ return rawLine;
97
+ }
98
+ if (!part || part.type !== "scene.add" || part.placement === "closer") return rawLine;
99
+ const scene = part.scene;
100
+ if (!scene) return rawLine;
101
+ if (typeof scene.narration !== "string") {
102
+ bodyStarted = true;
103
+ return rawLine;
104
+ }
105
+ const narration = continueAfterOpening(scene.narration, earlier);
106
+ if (!narration && scene.templateId === "cinemaMedia") return null;
107
+ const variables = scene.variables;
108
+ if (!narration && scene.templateId === "chapterTitle" && typeof variables?.title === "string" && !continueAfterOpening(variables.title, earlier)) return null;
109
+ bodyStarted = true;
110
+ if (!narration) return rawLine;
111
+ const field = scene.templateId === "cinemaMedia" ? "fallbackText" : scene.templateId === "chapterTitle" ? "title" : void 0;
112
+ const authored = field && variables?.[field];
113
+ const updated = typeof authored === "string" ? copy(authored, narration) : void 0;
114
+ const copyChanged = typeof authored === "string" && updated !== authored;
115
+ if (narration === scene.narration && !copyChanged) return rawLine;
116
+ return JSON.stringify({ ...part, scene: { ...scene, narration, ...copyChanged ? { variables: { ...variables, [field]: updated } } : {} } });
117
+ }
118
+ };
119
+ }
120
+
58
121
  // src/server/request-validation.ts
59
122
  var MAX_GENERATED_LOOK_LENGTH = 1e3;
60
123
  function boundedString(value, path) {
@@ -566,7 +629,7 @@ async function resolvePartVariables(options) {
566
629
  }
567
630
  const rendered = { ...variables };
568
631
  delete rendered.mediaSource;
569
- delete rendered.fallbackText;
632
+ if (options.templateId !== "cinemaMedia") delete rendered.fallbackText;
570
633
  delete rendered.shotDirection;
571
634
  return { ...options.part, scene: { ...options.part.scene, variables: rendered } };
572
635
  }
@@ -865,16 +928,17 @@ function createVideoHandler(options) {
865
928
  function createVideoChatResponseInstructions(generatedVideoAvailable, openingAlreadyProvided = false, maxGeneratedVideos = 5) {
866
929
  return [
867
930
  "Respond as a coherent short film: establish, develop, then land a useful or emotional payoff. Match the user's requested form.",
868
- generatedVideoAvailable ? 'First emit one host-consumed opening JSON object: {"type":"video-chat.opening","spokenHook":"6-9 words","mediaKeyword":"literal subject","firstShot":{"text":"short grounded fallback","narration":"one natural spoken sentence","mediaKeyword":"2-8 concrete words","shotDirection":"optional framing and action"}}. Include firstShot only when a distinctive illustrative shot serves the story; omit it for an abstract explanation best opened with graphics. When present, the host inserts it once and begins generation immediately; do not emit it again.' : openingAlreadyProvided ? "Start directly with scene.add; the opening has already been supplied." : 'First emit {"type":"video-chat.opening","spokenHook":"6-9 words","mediaKeyword":"literal subject"}. This is host-consumed, not a scene.',
931
+ generatedVideoAvailable ? 'First emit one host-consumed opening JSON object: {"type":"video-chat.opening","spokenHook":"6-9 words","mediaKeyword":"literal subject","firstShot":{"text":"short grounded fallback","narration":"a natural spoken beat advancing the hook","mediaKeyword":"2-8 concrete words","shotDirection":"optional framing and action"}}. Include firstShot only when a distinctive illustrative shot serves the story; omit it for an abstract explanation best opened with graphics. When present, the host inserts it once and begins generation immediately; do not emit it again.' : openingAlreadyProvided ? "Start directly with scene.add; the opening has already been supplied." : 'First emit {"type":"video-chat.opening","spokenHook":"6-9 words","mediaKeyword":"literal subject"}. This is host-consumed, not a scene.',
869
932
  openingAlreadyProvided ? "Preserve the supplied opening exactly; continue it without repeating its words or claim." : void 0,
933
+ "The spokenHook is already heard before the first scene. Start firstShot.narration with the next fact, cause, action or consequence; never restate the hook. The first ordinary scene must then advance beyond firstShot rather than repeat its narration or opening claim. If firstShot is omitted, that first scene must advance the hook directly. firstShot.text, fallbackText and chapter titles must also express that new beat, not reuse the hook or prior shot copy; write a concise visual anchor of roughly 2\u20136 words within the schema budget, never a full narration sentence, clipped statistic or qualifying clause.",
870
934
  "Continue with only as many scenes as the story and duration need. Never pad to a fixed count. Every emitted scene carries narration beside variables and timing; no additional narration request should be needed.",
871
- "For concrete topics, coherent footage carries the story and graphics clarify particular points. Maintain consistent setting, lighting and subject while varying shot scale. Avoid unrelated cinematic montages.",
872
- "Use the installed catalog: parallel points, ordered events, comparison, exact quote, one key figure, chapter or a message only when its narrative job fits. Do not force every template into a video. Avoid repeating the same graphic or more than two consecutive graphic scenes unless necessary.",
935
+ "Make most of the film relevant footage: use full-bleed media for action, subjects and atmosphere, with graphics only to clarify a specific relationship or piece of evidence. Maintain consistent setting, lighting and subject while varying shot scale. Avoid unrelated cinematic montages.",
936
+ "Use the installed catalog: ordered events, comparison, exact quote, one key figure, chapter or a message only when its narrative job fits. For an ordinary explanation, roughly one graphic-led explanatory beat per 30 seconds is a starting point, plus a brief chapter opening when needed. Let other intents use the visual form they need. Do not place graphic beats consecutively unless no honest relevant media is available. Give media-capable graphics a relevant background when it supports the meaning; keep black when no matching media exists. Do not force every template into a video or use lists of bullets.",
873
937
  `At most ${generatedVideoAvailable ? maxGeneratedVideos : 0} generated-video attempts are available. Media scenes choose mediaSource=generate for distinctive illustrative shots or mediaSource=stock for generic verified imagery. A stock miss is not permission to broaden essential details. Do not spend generation on every scene.`,
874
- "Each mediaKeyword is a literal filmable subject/action, 2\u20138 words, maximum 80 characters. Use optional shotDirection for action, camera framing and continuity; it does not change the literal stock subject. Supply a short grounded fallbackText when the schema declares it. Do not put visible headline text over full-bleed footage.",
938
+ "Each mediaKeyword is a literal filmable subject/action, 2\u20138 words, maximum 80 characters. Use optional shotDirection for action, camera framing and continuity; it does not change the literal stock subject. Supply a short grounded fallbackText when the schema declares it: one useful visual anchor, not a second explanation competing with narration or a promise of footage. It must still make sense when the media is absent. Do not put visible headline text over full-bleed footage.",
875
939
  "No invented quotations, attribution, statistics, personal evidence or URLs. Creative stories may be invented when requested; do not present generated illustration as historical evidence.",
876
- 'Emit exactly one final placement:"closer" scene using a catalog template with a suitable payoff or ask job. It should land the meaning, not recap the whole answer.',
877
- "Every scene needs timing, even an empty object. Narration forms one continuous spoken explanation, uses natural sentence lengths, and does not read every on-screen word back. Finish with plan.complete."
940
+ 'Emit exactly one final placement:"closer" scene using a catalog template with a suitable payoff or ask job. It should land the meaning, not recap the whole answer. Prefer the final relevant action or visual reveal with narration; a closer does not require a black chapter card.',
941
+ "Every scene needs timing, even an empty object. Narration preserves the requested form and tone, uses natural sentence lengths, and does not read every on-screen word back. Finish with plan.complete."
878
942
  ].filter((line) => line != null).join("\n");
879
943
  }
880
944
  var VIDEO_CHAT_NARRATION_PROMPT = [
@@ -882,7 +946,7 @@ var VIDEO_CHAT_NARRATION_PROMPT = [
882
946
  "You are given the scene about to be shown, and the lines already said before it.",
883
947
  "",
884
948
  "Return only the line to say over this scene. No JSON, no quotes, no preamble.",
885
- "One sentence, 10-16 words, plain spoken English.",
949
+ "Use the requested tone and natural spoken language. Keep this beat concise enough for its planned duration; preserve dialogue, a short punchline or practical instructions when appropriate.",
886
950
  "Say only what this scene contributes. Preserve the requested form: narrate a story as a story, give recommendations directly, and explain only when the user asked for an explanation.",
887
951
  "Never jump ahead to a later scene or repeat a line already said.",
888
952
  "Never read the on-screen text back word for word - the viewer can already see it.",
@@ -893,7 +957,8 @@ var VIDEO_CHAT_SUGGESTIONS_PROMPT = [
893
957
  'Return JSON only: {"suggestions": [{"prompt": string, "keyword": string}]} with exactly four entries.',
894
958
  "prompt: one clear, specific follow-up in 4\u20138 words. Never exceed 8 words or 60 characters, including spaces. These are small mobile cards, not full instructions.",
895
959
  "Ask about one idea. Use context from the answer instead of restating it. No combined questions, preambles, lists of examples, or requests to visualize multiple things.",
896
- "Examples: How do atoms gain electrons? Why do atoms bond? What holds the nucleus together? Can an atom split? Suggestions may continue, remix, deepen, compare, or apply the answer; stay relevant to its topic.",
960
+ "Treat the entire VIDEO RESPONSE as already answered, including its causes, mechanisms and conclusions. Reject any candidate whose answer is already given there, even as a paraphrase or a narrower question. Before returning, compare all four candidates with that coverage and with one another; replace overlaps with a new consequence, application, counterfactual or adjacent unanswered idea.",
961
+ "Match the response intent: continue or change a fictional story, try a different comic premise, explore another imagined setting, apply a practical method, or deepen an unanswered part of an explanation. Never add a factual explanation to a joke by default.",
897
962
  "keyword: two to four words naming something filmable that stands for the prompt, for stock footage search. Concrete subjects only - no abstractions, no text on screen."
898
963
  ].join("\n");
899
964
  function createNarrationUserPrompt(prompt, scene, earlier) {
@@ -1111,6 +1176,19 @@ function interceptOpeningPlan(source, options) {
1111
1176
  const textStream = (async function* () {
1112
1177
  let buffer = "";
1113
1178
  let decided = !options.expectOpening;
1179
+ const continuation = createOpeningContinuation(options.openingLine);
1180
+ const acceptOpening = (opening) => {
1181
+ if (!options.openingProvided) {
1182
+ options.publish(opening.line ? opening : void 0);
1183
+ continuation.remember(opening.line);
1184
+ }
1185
+ if (!options.generatedVideoAvailable || !opening.firstShot) return void 0;
1186
+ const narration = continuation.narration(opening.firstShot.narration);
1187
+ if (!narration) return void 0;
1188
+ const text = continuation.copy(opening.firstShot.text, narration);
1189
+ continuation.remember(narration);
1190
+ return JSON.stringify(reservedFirstScene(options.requestId, { ...opening.firstShot, text, narration }));
1191
+ };
1114
1192
  try {
1115
1193
  for await (const delta of upstream) {
1116
1194
  if (typeof delta !== "string") throw new Error("The LLM adapter returned a non-text delta");
@@ -1124,17 +1202,16 @@ function interceptOpeningPlan(source, options) {
1124
1202
  const opening = readOpeningPlanLine(line);
1125
1203
  decided = true;
1126
1204
  if (opening) {
1127
- if (!options.openingProvided) options.publish(opening.line ? opening : void 0);
1128
- if (options.generatedVideoAvailable && opening.firstShot) {
1129
- yield `${JSON.stringify(reservedFirstScene(options.requestId, opening.firstShot))}
1205
+ const firstScene = acceptOpening(opening);
1206
+ if (firstScene) yield `${firstScene}
1130
1207
  `;
1131
- }
1132
1208
  newline = buffer.indexOf("\n");
1133
1209
  continue;
1134
1210
  }
1135
1211
  options.publish(void 0);
1136
1212
  }
1137
- yield `${rawLine}
1213
+ const continued = continuation.line(rawLine);
1214
+ if (continued != null) yield `${continued}
1138
1215
  `;
1139
1216
  newline = buffer.indexOf("\n");
1140
1217
  }
@@ -1145,17 +1222,17 @@ function interceptOpeningPlan(source, options) {
1145
1222
  const opening = readOpeningPlanLine(line);
1146
1223
  decided = true;
1147
1224
  if (opening) {
1148
- if (!options.openingProvided) options.publish(opening.line ? opening : void 0);
1149
- if (options.generatedVideoAvailable && opening.firstShot) {
1150
- yield `${JSON.stringify(reservedFirstScene(options.requestId, opening.firstShot))}
1225
+ const firstScene = acceptOpening(opening);
1226
+ if (firstScene) yield `${firstScene}
1151
1227
  `;
1152
- }
1153
1228
  } else {
1154
1229
  options.publish(void 0);
1155
- yield buffer;
1230
+ const continued = continuation.line(buffer);
1231
+ if (continued != null) yield continued;
1156
1232
  }
1157
1233
  } else {
1158
- yield buffer;
1234
+ const continued = continuation.line(buffer);
1235
+ if (continued != null) yield continued;
1159
1236
  }
1160
1237
  }
1161
1238
  } finally {
@@ -1332,7 +1409,7 @@ function createVideoChatHandler(options) {
1332
1409
  const heroQuery = welcomeOptions?.heroQuery;
1333
1410
  let welcomeResponse;
1334
1411
  let requestSequence = 0;
1335
- const responseHandler = (requestId, openingProvided, openingChannel) => {
1412
+ const responseHandler = (requestId, openingProvided, openingChannel, openingLine) => {
1336
1413
  const generatedVideoAvailable = generateVideo != null && maxGeneratedVideos > 0;
1337
1414
  let lifecycle;
1338
1415
  let generatedAttempts = 0;
@@ -1400,6 +1477,7 @@ function createVideoChatHandler(options) {
1400
1477
  return interceptOpeningPlan(videoOptions.streamText(context), {
1401
1478
  expectOpening: generatedVideoAvailable || !openingProvided,
1402
1479
  openingProvided,
1480
+ openingLine,
1403
1481
  requestId,
1404
1482
  generatedVideoAvailable,
1405
1483
  publish: openingChannel.publish
@@ -1569,7 +1647,8 @@ function createVideoChatHandler(options) {
1569
1647
  const response = await responseHandler(
1570
1648
  requestId,
1571
1649
  input.opening != null,
1572
- openingChannel
1650
+ openingChannel,
1651
+ input.opening
1573
1652
  )(videoRequest);
1574
1653
  return streamVideoChatOpening(response, openingChannel.ready);
1575
1654
  }
@@ -3,7 +3,7 @@ import {
3
3
  VIDEO_PLAN_INSTRUCTION,
4
4
  createVideoSystemPrompt,
5
5
  videoPlanInstruction
6
- } from "./chunk-PAJ44TFW.js";
6
+ } from "./chunk-LVM5Q2DL.js";
7
7
  export {
8
8
  DEFAULT_VIDEO_SYSTEM_PROMPT,
9
9
  VIDEO_PLAN_INSTRUCTION,