@vanillaskyai/video 0.10.0 → 0.10.2

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 (29) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/check-runtime.js +1 -1
  3. package/dist/{chunk-ZDJNFA52.js → chunk-5BSJLT6H.js} +6 -6
  4. package/dist/{chunk-NMFDC2W2.js → chunk-5C6HZNHY.js} +9 -4
  5. package/dist/{chunk-ELVOT2FQ.js → chunk-6KZGF63O.js} +46 -11
  6. package/dist/{chunk-2HFO22K2.js → chunk-JKVOBTRO.js} +3 -3
  7. package/dist/{chunk-XX4GXKTX.js → chunk-KHLO5OHT.js} +1 -1
  8. package/dist/{chunk-PAJ44TFW.js → chunk-LVM5Q2DL.js} +8 -6
  9. package/dist/{chunk-Z67Q5KUW.js → chunk-VQH3JTQC.js} +1 -1
  10. package/dist/{cinema-media-C7OUPNSH.js → cinema-media-APDJS7SC.js} +2 -2
  11. package/dist/cli.js +2 -2
  12. package/dist/{comparison-GNZOR7MF.js → comparison-ZVE2DE7U.js} +2 -2
  13. package/dist/{compose-video-TBFTONUC.js → compose-video-BH5K6XWT.js} +1 -1
  14. package/dist/{editorial-timeline-EMV5335S.js → editorial-timeline-YIXXVJUV.js} +2 -2
  15. package/dist/{key-figure-P37VK425.js → key-figure-I2C37FWV.js} +2 -2
  16. package/dist/{mobile-message-OF7T62B7.js → mobile-message-JSGBU6EX.js} +2 -2
  17. package/dist/{quote-T6WYLN7A.js → quote-XH54G3FS.js} +2 -2
  18. package/dist/react.js +63 -19
  19. package/dist/server.js +45 -32
  20. package/dist/{system-prompt-SYRT5DWM.js → system-prompt-AG26KJAA.js} +1 -1
  21. package/dist/test.js +1 -1
  22. package/package.json +1 -1
  23. package/registry/items/cinemaMedia.json +1 -1
  24. package/registry/items/comparison.json +1 -1
  25. package/registry/items/editorialTimeline.json +1 -1
  26. package/registry/items/keyFigure.json +1 -1
  27. package/registry/items/mobileMessage.json +1 -1
  28. package/registry/items/quote.json +1 -1
  29. package/starters/video-chat/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ VanillaSky follows semantic versioning. This changelog begins with the 0.1 beta.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.10.2
8
+
9
+ - Keep generated narration playing in Safari after a delayed opening-to-body handoff by reusing the permitted audio element; isolate late playback failures from subsequent lines.
10
+ - In drop mode, a media miss without usable authored fallback skips only that scene and preserves later valid scenes and the ending. Strict mode still fails. The skipped beat is reported; its content cannot be reconstructed without a usable asset or fallback.
11
+ - Guide first-shot narration to fit its five-second footage budget and develop the answer in later scenes, without imposing a fixed sentence length on the rest of the video.
12
+
13
+ ## 0.10.1
14
+
15
+ - Continue narration through failed built-in media using the authored fallback or the existing graphic on black. Keep the recovery anchor after URL resolution, and observe actual mounted images for photo readiness.
16
+
17
+ - Match planning, narration, visual progression and endings to explanations, fiction, comedy, imaginative requests and practical answers. Remove conflicting instructions that forced a factual arc, fixed sentence length or a different template for every beat.
18
+ - Ask follow-up generation to exclude questions already answered by the full narration, including paraphrases, and to preserve the conversation’s intent.
19
+
7
20
  ## 0.10.0
8
21
 
9
22
  ### Breaking
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VideoFrame
3
- } from "./chunk-ELVOT2FQ.js";
3
+ } from "./chunk-6KZGF63O.js";
4
4
  import "./chunk-224QNWRA.js";
5
5
  import "./chunk-TVIU23OM.js";
6
6
  import "./chunk-SPVTJH3F.js";
@@ -114,15 +114,15 @@ function buildVideoUserPrompt(input, openingDurationSec = 0) {
114
114
  "Add that scene before resolving any stock or supplied asset. Resolve media as part of each complete later body scene."
115
115
  ],
116
116
  "Use only claims supported by the factual basis permitted by the trusted system prompt.",
117
- "Select the most decision-relevant grounded takeaways that fit the duration; represent each selected takeaway once before completing the response.",
117
+ "Develop the requested answer within the duration: preserve its mechanism, essential actions, story resolution or punchline. A short prompt can need several distinct visual beats; input length is not the answer length.",
118
118
  "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.",
119
119
  "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.",
120
- "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.",
120
+ "Choose the scene count from the requested intent, visual progression and duration budget. A joke can be brief; a practical sequence or imaginative story may need more development. Do not stop while an essential step or payoff is missing.",
121
121
  "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.",
122
122
  "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.",
123
- "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.",
124
- "Emit exactly one final 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.",
125
- "Use a different suitable template for each body scene when the catalog supports it.",
123
+ "Before plan.complete, check that the answer fulfills the actual request. Do not mistake a hook and a single supporting statement for a complete explanation, or explain a joke after its punchline.",
124
+ "Emit exactly one final scene.add with placement closer. Land the requested ending without repeating the opening or implying another scene follows.",
125
+ "Reuse the same suitable template when it serves successive beats. Vary the subject action, shot scale, perspective or purposeful detail, not the template merely for variety.",
126
126
  "Never add filler to satisfy a count or diversity target.",
127
127
  input.suppliedMedia?.length ? input.opening === false ? "Select zero or more relevant opaque supplied-media references for visible scenes. Never invent or transform a reference. Only emit mediaKeyword when the trusted system catalog explicitly exposes it. Never invent mediaUrl or mediaPoster." : "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." : input.opening === false ? "No supplied media URL is available. Use asset-free templates unless the trusted system catalog explicitly permits host-resolved media intent, which may begin on the first scene. Only emit mediaKeyword when the trusted system catalog explicitly exposes it. 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.",
128
128
  "",
@@ -691,7 +691,7 @@ function createVideo(rawInput, options) {
691
691
  position
692
692
  }));
693
693
  }
694
- const systemPrompt = options.systemPrompt ?? (await import("./system-prompt-SYRT5DWM.js")).createVideoSystemPrompt(input.knowledgeMode);
694
+ const systemPrompt = options.systemPrompt ?? (await import("./system-prompt-AG26KJAA.js")).createVideoSystemPrompt(input.knowledgeMode);
695
695
  const context = { request, systemPrompt, userPrompt, initialConfig, signal: controller.signal };
696
696
  attachGenerationLifecycleSink(context, lifecycle);
697
697
  for await (const untrustedPart of options.generate(context)) {
@@ -163,17 +163,22 @@ var SceneBackground = ({
163
163
  onError: () => setMediaPaint("failed")
164
164
  }
165
165
  ) : /* @__PURE__ */ jsx(
166
- "div",
166
+ "img",
167
167
  {
168
+ src: mediaUrl,
169
+ alt: "",
170
+ "aria-hidden": "true",
171
+ draggable: false,
168
172
  "data-media-position": mediaPosition,
169
173
  style: {
170
174
  position: "absolute",
171
175
  inset: 0,
172
176
  transform: bgTransform.transform,
173
177
  transformOrigin: bgTransform.transformOrigin,
174
- backgroundImage: `url(${mediaUrl})`,
175
- backgroundSize: "cover",
176
- backgroundPosition: resolvedPosition
178
+ width: "100%",
179
+ height: "100%",
180
+ objectFit: "cover",
181
+ objectPosition: resolvedPosition
177
182
  }
178
183
  }
179
184
  )),
@@ -21,10 +21,12 @@ import { createContext, useContext, useEffect, useRef } from "react";
21
21
  import { jsx } from "react/jsx-runtime";
22
22
  var MountedReadinessContext = createContext(void 0);
23
23
  var sceneReadinessKey = (scene) => `${scene.id}\0${String(scene.variables.mediaUrl || "")}`;
24
- function MountedSceneReadiness({ scene, playing, fallback = false }) {
24
+ function MountedSceneReadiness({ scene, playing, fallback = false, onFailure }) {
25
25
  const marker = useRef(null);
26
26
  const report = useContext(MountedReadinessContext);
27
27
  const key = sceneReadinessKey(scene);
28
+ const onFailureRef = useRef(onFailure);
29
+ onFailureRef.current = onFailure;
28
30
  useEffect(() => {
29
31
  if (!report || !playing) return;
30
32
  let stopped = false;
@@ -35,13 +37,14 @@ function MountedSceneReadiness({ scene, playing, fallback = false }) {
35
37
  const finish = (error, actualVideoFrame = false) => {
36
38
  if (!stopped) {
37
39
  stopped = true;
38
- report(key, error, actualVideoFrame);
40
+ if (error && onFailureRef.current) onFailureRef.current();
41
+ else report(key, error, actualVideoFrame);
39
42
  }
40
43
  };
41
44
  const check = () => {
42
45
  if (stopped) return;
43
46
  const root = marker.current?.closest("[data-video-frame]");
44
- if (fallback && root?.querySelector("[data-scene-fallback]")) {
47
+ if (fallback && root?.querySelector("[data-scene-fallback]") && !root.querySelector("[data-template-loading]") && document.fonts?.status !== "loading") {
45
48
  finish();
46
49
  return;
47
50
  }
@@ -132,6 +135,7 @@ import {
132
135
  lazy,
133
136
  Suspense,
134
137
  useState,
138
+ useCallback,
135
139
  useEffect as useEffect2,
136
140
  useSyncExternalStore
137
141
  } from "react";
@@ -269,7 +273,9 @@ function SceneLayer({
269
273
  zIndex,
270
274
  externalVideoBackdrop
271
275
  }) {
272
- const template = kit.getTemplate(range.scene.templateId);
276
+ const mediaFailed = externalVideoBackdrop === "fallback";
277
+ const recoveryTitle = mediaFailed && range.scene.templateId === "cinemaMedia" && typeof range.scene.variables.fallbackText === "string" ? range.scene.variables.fallbackText : void 0;
278
+ const template = kit.getTemplate(recoveryTitle ? "chapterTitle" : range.scene.templateId);
273
279
  const duration = range.end - range.start;
274
280
  return /* @__PURE__ */ jsx2(
275
281
  ExternalVideoBackdropProvider,
@@ -281,6 +287,7 @@ function SceneLayer({
281
287
  "div",
282
288
  {
283
289
  "data-scene-layer": layer,
290
+ "data-scene-fallback": mediaFailed ? "true" : void 0,
284
291
  "data-layer-scene-id": range.scene.id,
285
292
  "data-layer-template-id": range.scene.templateId,
286
293
  "aria-hidden": interactive ? void 0 : true,
@@ -308,7 +315,9 @@ function SceneLayer({
308
315
  createElement(template.component, {
309
316
  variables: {
310
317
  ...template.defaults ?? getTemplateDefaults(template.schema),
311
- ...range.scene.variables
318
+ ...range.scene.variables,
319
+ ...mediaFailed ? { mediaUrl: "", mediaPoster: "", mediaType: "gradient" } : {},
320
+ ...recoveryTitle ? { title: recoveryTitle } : {}
312
321
  },
313
322
  style: config.style,
314
323
  progress,
@@ -344,7 +353,18 @@ function VideoFrame({
344
353
  className,
345
354
  style
346
355
  }) {
347
- const [failedPersistentVideo, setFailedPersistentVideo] = useState();
356
+ const [failedMedia, setFailedMedia] = useState(() => /* @__PURE__ */ new Set());
357
+ const markMediaFailed = useCallback((key) => {
358
+ if (!key || !config.scenes.some((scene) => sceneReadinessKey(scene) === key)) return;
359
+ setFailedMedia((previous) => previous.has(key) ? previous : /* @__PURE__ */ new Set([...previous, key]));
360
+ }, [config.scenes]);
361
+ useEffect2(() => {
362
+ const currentKeys = new Set(config.scenes.map(sceneReadinessKey));
363
+ setFailedMedia((previous) => {
364
+ const retained = new Set([...previous].filter((key) => currentKeys.has(key)));
365
+ return retained.size === previous.size ? previous : retained;
366
+ });
367
+ }, [config.scenes]);
348
368
  const [readyPersistentVideo, setReadyPersistentVideo] = useState();
349
369
  const decoderConstrainedDevice = useDecoderConstraint();
350
370
  const timeline = resolveVideoTimeline(config);
@@ -422,7 +442,8 @@ function VideoFrame({
422
442
  mediaPosition: String(posterPreparationRange.scene.variables.mediaPosition || "center"),
423
443
  backgroundEffect: posterPreparationRange.scene.backgroundEffect ?? config.style.defaultBackgroundEffect
424
444
  } : void 0;
425
- const persistentVideoFailed = persistentVideoKey !== void 0 && failedPersistentVideo === persistentVideoKey;
445
+ const activeMediaFailed = failedMedia.has(sceneReadinessKey(active.scene));
446
+ const persistentVideoFailed = persistentVideoKey !== void 0 && failedMedia.has(persistentVideoKey);
426
447
  const persistentVideoReady = persistentVideoRange !== void 0 && (String(persistentVideoRange.scene.variables.mediaPoster || "") !== "" || readyPersistentVideo === persistentVideoKey);
427
448
  const persistentVideoMode = persistentVideoFailed ? "fallback" : persistentVideoReady ? "ready" : "pending";
428
449
  const mountingNext = Boolean(
@@ -442,6 +463,12 @@ function VideoFrame({
442
463
  return /* @__PURE__ */ jsxs(
443
464
  "div",
444
465
  {
466
+ onErrorCapture: (event) => {
467
+ const target = event.target;
468
+ if ((target instanceof HTMLVideoElement || target instanceof HTMLImageElement) && target.getAttribute("src") === active.scene.variables.mediaUrl && supportsExternalVideoBackdrop(activeTemplate) && (target.closest("[data-layer-scene-id]")?.getAttribute("data-layer-scene-id") === active.scene.id || target.closest("[data-persistent-video-scene-id]")?.getAttribute("data-persistent-video-scene-id") === active.scene.id)) {
469
+ markMediaFailed(sceneReadinessKey(active.scene));
470
+ }
471
+ },
445
472
  "data-video-frame": "ready",
446
473
  "data-scene-id": active.scene.id,
447
474
  "data-template-id": active.scene.templateId,
@@ -455,7 +482,15 @@ function VideoFrame({
455
482
  ...style
456
483
  },
457
484
  children: [
458
- /* @__PURE__ */ jsx2(MountedSceneReadiness, { scene: active.scene, playing }),
485
+ /* @__PURE__ */ jsx2(
486
+ MountedSceneReadiness,
487
+ {
488
+ scene: active.scene,
489
+ playing,
490
+ fallback: activeMediaFailed,
491
+ onFailure: sceneHasBackdrop(active) && supportsExternalVideoBackdrop(activeTemplate) && !activeMediaFailed ? () => markMediaFailed(sceneReadinessKey(active.scene)) : void 0
492
+ }
493
+ ),
459
494
  preparedReadinessRange && /* @__PURE__ */ jsx2(PreparedSceneReadiness, { scene: preparedReadinessRange.scene }),
460
495
  /* @__PURE__ */ jsxs(
461
496
  "div",
@@ -530,7 +565,7 @@ function VideoFrame({
530
565
  opacity: decoderConstrainedTransition ? blendProgress : 0
531
566
  } : void 0,
532
567
  onReady: () => setReadyPersistentVideo(persistentVideoKey),
533
- onError: () => setFailedPersistentVideo(persistentVideoKey)
568
+ onError: () => markMediaFailed(persistentVideoKey)
534
569
  }
535
570
  ) })
536
571
  }
@@ -554,7 +589,7 @@ function VideoFrame({
554
589
  opacity: 1 - blendProgress,
555
590
  interactive: true,
556
591
  zIndex: 1,
557
- externalVideoBackdrop: persistentVideoRange?.scene.id === active.scene.id ? persistentVideoMode : false
592
+ externalVideoBackdrop: activeMediaFailed ? "fallback" : persistentVideoRange?.scene.id === active.scene.id ? persistentVideoMode : false
558
593
  },
559
594
  active.scene.id
560
595
  ),
@@ -598,7 +633,7 @@ function VideoFrame({
598
633
  opacity: 1,
599
634
  interactive: true,
600
635
  zIndex: 1,
601
- externalVideoBackdrop: persistentVideoRange?.scene.id === active.scene.id ? persistentVideoMode : false
636
+ externalVideoBackdrop: activeMediaFailed ? "fallback" : persistentVideoRange?.scene.id === active.scene.id ? persistentVideoMode : false
602
637
  },
603
638
  active.scene.id
604
639
  )
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createVideoSystemPrompt
3
- } from "./chunk-PAJ44TFW.js";
3
+ } from "./chunk-LVM5Q2DL.js";
4
4
  import {
5
5
  PACING_PLANNER_RULES
6
6
  } from "./chunk-3O7OMMMF.js";
@@ -118,7 +118,7 @@ ${options.basePrompt.trim()}` : void 0,
118
118
  "Variable notation is type[count]{characters}(options)! where count is list cardinality, characters is the inclusive character count for a string or each string-array item, and ! means required. Omitted ! means optional.",
119
119
  "Choose a template only when the permitted factual basis contains every fact it needs. Never invent peer values to complete a chart, comparison, stat set, timeline, or list.",
120
120
  ...PACING_PLANNER_RULES,
121
- "Let narration carry ordinary lists. Use a structured overlay only when a comparison, sequence, exact quote or key figure adds understanding beyond the voice. Prefer relevant full-bleed footage for the rest of the explanation.",
121
+ "Let narration carry ordinary lists. Use a structured overlay only when a comparison, sequence, exact quote or key figure adds understanding beyond the voice. Prefer relevant full-bleed footage for the remaining visual beats, with varied purposeful action and framing.",
122
122
  "If the catalog includes a suitable ask template and the input supplies a grounded CTA or URL, keep that concise action closer as its own final scene instead of folding it into preceding content.",
123
123
  "When a grounded CTA or URL is supplied and the catalog contains jobs:[ask], emit that final closer. A brand name may accompany the action but never qualifies as an ask by itself.",
124
124
  terminalPayoffs.length > 0 ? `When there is no grounded action, end with a concise supported payoff using a suitable template from ${terminalPayoffs.join(", ")}. Do not repeat the hook or invent a CTA.` : void 0,
@@ -127,7 +127,7 @@ ${options.basePrompt.trim()}` : void 0,
127
127
  mediaTemplates.length > 0 && options.mediaOnFirstScene !== true ? `The host owns immediate startup; reserve external media templates (${mediaTemplates.join(", ")}) for later beats when an asset-free opening is required.` : void 0,
128
128
  "For list and event variables emit actual JSON arrays. Follow each field's content budget; never use pipe-delimited strings or invent entries.",
129
129
  resolverMediaAvailable ? "Media-capable scenes may carry mediaKeyword: a literal 2\u20138 word shot description, at most 80 characters. Choose mediaSource=generate for distinctive illustrative shots and mediaSource=stock for common subjects a verified asset can depict. For generated footage, optional shotDirection gives action, framing and continuity separately from the stock query. Keep people, actions and essential details precise. Never broaden an exact identity or action into unrelated atmosphere. Supply a grounded fallbackText where declared. Only the host fills mediaUrl and mediaPoster; never invent them. If no media fits an abstract beat, choose an appropriate graphic template instead." : "Use media only when a supplied asset satisfies the scene. Never invent asset URLs. Otherwise choose a grounded graphic template.",
130
- "When the catalog offers media, make relevant footage the default. For a typical 30-second answer use at most one structured explanatory overlay plus an optional brief chapter opening. Do not chain graphic-only beats when honest media is available. Reuse suitable supplied media behind overlays with optional media fields; retain black when no relevant asset exists. Never invent a stock match or spend beyond host generation limits.",
130
+ "When the catalog offers media, make relevant footage the default. For a typical 30-second explanation start with roughly one structured explanatory overlay plus an optional brief chapter opening; other intents may need different pacing and visual emphasis. Do not chain graphic-only beats when honest media is available. Reuse suitable supplied media behind overlays with optional media fields; retain black when no relevant asset exists. Never invent a stock match or spend beyond host generation limits.",
131
131
  "Catalog guidance describes composition; it is not a factual source and must never replace the permitted factual basis.",
132
132
  JSON.stringify(plannerCatalog(templates, resolverMediaAvailable)),
133
133
  // Last, because the catalogue is thousands of tokens of per-scene field
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  SceneBackground,
3
3
  getMediaBackgroundProps
4
- } from "./chunk-NMFDC2W2.js";
4
+ } from "./chunk-5C6HZNHY.js";
5
5
  import {
6
6
  editorialFont
7
7
  } from "./chunk-4YM2M62S.js";
@@ -1,6 +1,6 @@
1
1
  // src/server/prompts/system-prompt.ts
2
2
  function videoPlanInstruction(narrate = false) {
3
- const narration = narrate ? `,"narration":"one sentence said aloud over this scene"` : "";
3
+ const narration = narrate ? `,"narration":"words spoken over this scene"` : "";
4
4
  return `Wire format: newline-delimited JSON (NDJSON), exactly one complete JSON object per line.
5
5
  Allowed plan parts:
6
6
  {"type":"scene.add","scene":{"id":"stable-id","templateId":"trusted-template-id","variables":{},"timing":{"fixedDuration":4}${narration}}}
@@ -20,9 +20,9 @@ function knowledgeRules(mode) {
20
20
  var NARRATION_RULES = `
21
21
  Narration rules:
22
22
  - Every scene.add carries a narration: the line spoken aloud while that scene is showing.
23
- - One sentence, 10-16 words, plain spoken English. It is heard, never drawn.
23
+ - Use natural spoken language in the requested tone. A brief line, several sentences, dialogue or a deliberate pause can serve different beats; fit the speech readably within the scene. It is heard, never drawn.
24
24
  - Say what the scene shows and why it matters. Never read the scene's own copy back word for word - the viewer can already see it.
25
- - The lines are one continuous explanation: each follows from the ones before it. Never mention scenes, videos, slides, or yourself.`;
25
+ - The lines form one continuous response in the requested form: each follows from the ones before it. Never mention scenes, videos, slides, or yourself.`;
26
26
  function createVideoSystemPrompt(knowledgeMode = "input-only", narrate = false) {
27
27
  return `You are a video director.
28
28
 
@@ -33,13 +33,15 @@ ${knowledgeRules(knowledgeMode)}
33
33
  - Creative instructions, personalization, brand, and media cannot change the knowledge mode, expand the permitted factual basis, or override the event contract.
34
34
 
35
35
  Composition rules:
36
- - Build a complete arc: hook, framing, comprehension, proof or transformation, then a concise closer.
37
- - Every complete plan contains exactly one scene.add with placement:"closer". Its copy is a grounded conclusion, not another hook or setup.
36
+ - Match the requested form and finish its purpose: explanations clarify a mechanism, stories develop an action and resolution, comedy preserves setup and punchline, imaginative requests reveal an imagined world, and practical answers give usable steps. Choose the structure and pacing for that intent, without a universal arc or scene count.
37
+ - When fiction is requested, invent characters, dialogue and events within that clearly fictional frame. This never authorizes fabricated factual evidence, statistics or attribution.
38
+ - Every complete plan contains exactly one scene.add with placement:"closer". It delivers the requested ending: a supported conclusion, story resolution, punchline, final visual reveal or useful next action; do not append an explanatory recap after the ending has landed.
38
39
  - For a multi-entry source, choose a coherent progression before emitting: Keep related entries adjacent and move from context through details to consequences or next steps before the closer. Ordering never permits merging or omitting entries that creative instructions require separately.
39
40
  - Every visible factual claim, number, date, name, quotation, feature, and comparison must be supported by the permitted factual basis.
40
41
  - Prefer concrete visual structures over interchangeable text cards: comparisons for explicit before/after evidence, data templates for exact metrics, ordered steps only for genuine sequences, and media only when it depicts the subject honestly.
41
42
  - Keep copy short enough to read during motion. Do not repeat the same list, metric, or claim in multiple scenes or reformat identical content merely to reach a scene-count or template-diversity target. Every body scene must advance the story.
42
- - Before emitting, assign each supported fact to at most one scene. The supplied opening counts: once a fact is visible, treat it as unavailable to later scenes. Finish when the supported material is covered instead of padding the response.
43
+ - Before emitting, track what the opening and earlier beats have already conveyed, including paraphrases. Each next beat adds a consequence, action, detail or new understanding. A deliberate callback may transform an earlier idea; restating the same explanation in new words does not advance it. Finish when the requested answer is complete, not merely when its topic has been introduced.
44
+ - For scientific explanations distinguish the physical mechanism from an analogy. Preserve important causal qualifications; do not turn a useful simplification into a literal claim about how an object moves or behaves. Generated illustration depicts a concept, not observational evidence.
43
45
  - Do not infer that something is scheduled, ready, triggered, enabled, automatic, causal, or available unless the permitted factual basis supports it.
44
46
  - The first generated body scene must be asset-free and fully playable before any external media resolves.
45
47
  - Use only media URLs present in the supplied input or already resolved by the host. Never expose a loading placeholder or unresolved media keyword. Audio is optional and must never delay the first scene.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  SceneBackground,
3
3
  getMediaBackgroundProps
4
- } from "./chunk-NMFDC2W2.js";
4
+ } from "./chunk-5C6HZNHY.js";
5
5
 
6
6
  // src/visual-system/scene-templates/cinema-media.tsx
7
7
  import { jsx } from "react/jsx-runtime";
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  MediaScene,
3
3
  MediaSceneTemplate
4
- } from "./chunk-Z67Q5KUW.js";
5
- import "./chunk-NMFDC2W2.js";
4
+ } from "./chunk-VQH3JTQC.js";
5
+ import "./chunk-5C6HZNHY.js";
6
6
  import "./chunk-77VY4O7A.js";
7
7
  import "./chunk-224QNWRA.js";
8
8
  import "./chunk-TVIU23OM.js";
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-2HFO22K2.js";
3
- import "./chunk-PAJ44TFW.js";
2
+ import "./chunk-JKVOBTRO.js";
3
+ import "./chunk-LVM5Q2DL.js";
4
4
  import {
5
5
  isTemplatePropertyFormatSupported,
6
6
  validateTemplateSchemaValue
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  EditorialSurface
3
- } from "./chunk-XX4GXKTX.js";
4
- import "./chunk-NMFDC2W2.js";
3
+ } from "./chunk-KHLO5OHT.js";
4
+ import "./chunk-5C6HZNHY.js";
5
5
  import "./chunk-77VY4O7A.js";
6
6
  import {
7
7
  editorialLabel,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createVideo
3
- } from "./chunk-ZDJNFA52.js";
3
+ } from "./chunk-5BSJLT6H.js";
4
4
  import "./chunk-E7CL7UPB.js";
5
5
  import "./chunk-RXTN2CW6.js";
6
6
  import "./chunk-7M56IUUX.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  EditorialSurface
3
- } from "./chunk-XX4GXKTX.js";
4
- import "./chunk-NMFDC2W2.js";
3
+ } from "./chunk-KHLO5OHT.js";
4
+ import "./chunk-5C6HZNHY.js";
5
5
  import "./chunk-77VY4O7A.js";
6
6
  import {
7
7
  fade
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  EditorialSurface
3
- } from "./chunk-XX4GXKTX.js";
4
- import "./chunk-NMFDC2W2.js";
3
+ } from "./chunk-KHLO5OHT.js";
4
+ import "./chunk-5C6HZNHY.js";
5
5
  import "./chunk-77VY4O7A.js";
6
6
  import {
7
7
  editorialLabel,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  MediaScene
3
- } from "./chunk-Z67Q5KUW.js";
4
- import "./chunk-NMFDC2W2.js";
3
+ } from "./chunk-VQH3JTQC.js";
4
+ import "./chunk-5C6HZNHY.js";
5
5
  import {
6
6
  spring
7
7
  } from "./chunk-77VY4O7A.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  EditorialSurface
3
- } from "./chunk-XX4GXKTX.js";
4
- import "./chunk-NMFDC2W2.js";
3
+ } from "./chunk-KHLO5OHT.js";
4
+ import "./chunk-5C6HZNHY.js";
5
5
  import "./chunk-77VY4O7A.js";
6
6
  import {
7
7
  fade
package/dist/react.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  VideoFrame,
41
41
  getDimensions,
42
42
  sceneReadinessKey
43
- } from "./chunk-ELVOT2FQ.js";
43
+ } from "./chunk-6KZGF63O.js";
44
44
  import "./chunk-224QNWRA.js";
45
45
  import "./chunk-TVIU23OM.js";
46
46
  import {
@@ -48,7 +48,8 @@ import {
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,13 +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-C7OUPNSH.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
- "editorialTimeline": () => import("./editorial-timeline-EMV5335S.js").then((module) => ({ default: module.TimelineSceneTemplate })),
99
- "mobileMessage": () => import("./mobile-message-OF7T62B7.js").then((module) => ({ default: module.NotificationSceneTemplate })),
100
- "comparison": () => import("./comparison-GNZOR7MF.js").then((module) => ({ default: module.ComparisonSceneTemplate })),
101
- "quote": () => import("./quote-T6WYLN7A.js").then((module) => ({ default: module.QuoteSceneTemplate })),
102
- "keyFigure": () => import("./key-figure-P37VK425.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 }))
103
104
  };
104
105
 
105
106
  // src/visual-system/catalog/builtin-player.generated.ts
@@ -1238,6 +1239,19 @@ var VideoError = class extends Error {
1238
1239
  }
1239
1240
  };
1240
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
+
1241
1255
  // src/video-chat/use-video-chat.ts
1242
1256
  import { useCallback as useCallback2, useEffect as useEffect4, useReducer, useRef as useRef3 } from "react";
1243
1257
 
@@ -1536,6 +1550,8 @@ function createVideoChatVoice(options = {}) {
1536
1550
  const lines = /* @__PURE__ */ new Map();
1537
1551
  const pendingLoads = /* @__PURE__ */ new Set();
1538
1552
  let sounding;
1553
+ let generatedElement;
1554
+ let stopGenerated;
1539
1555
  let browserFinish;
1540
1556
  let held = false;
1541
1557
  let silent = false;
@@ -1660,7 +1676,12 @@ function createVideoChatVoice(options = {}) {
1660
1676
  },
1661
1677
  resume() {
1662
1678
  held = false;
1663
- if (sounding) void sounding.play().catch(() => playbackFailure?.());
1679
+ if (sounding) {
1680
+ const fail = playbackFailure;
1681
+ void sounding.play().catch(() => {
1682
+ if (!held) fail?.();
1683
+ });
1684
+ }
1664
1685
  if (!silent) globalThis.speechSynthesis?.resume();
1665
1686
  },
1666
1687
  setMuted(muted) {
@@ -1739,9 +1760,11 @@ function createVideoChatVoice(options = {}) {
1739
1760
  }
1740
1761
  let playbackFailed = false;
1741
1762
  try {
1742
- const element = new Audio(line.src);
1763
+ stopGenerated?.();
1764
+ const element = generatedElement ??= new Audio();
1765
+ element.src = line.src;
1743
1766
  element.muted = silent;
1744
- if (offsetSeconds !== void 0) element.currentTime = offsetSeconds;
1767
+ element.currentTime = offsetSeconds ?? 0;
1745
1768
  sounding = element;
1746
1769
  await new Promise((resolve) => {
1747
1770
  let finished = false;
@@ -1757,6 +1780,7 @@ function createVideoChatVoice(options = {}) {
1757
1780
  element.onplaying = null;
1758
1781
  element.onended = null;
1759
1782
  element.onerror = null;
1783
+ if (stopGenerated === stop) stopGenerated = void 0;
1760
1784
  if (sounding === element) {
1761
1785
  sounding = void 0;
1762
1786
  playbackFailure = void 0;
@@ -1764,13 +1788,17 @@ function createVideoChatVoice(options = {}) {
1764
1788
  resolve();
1765
1789
  };
1766
1790
  const stop = () => {
1791
+ if (finished) return;
1767
1792
  element.pause();
1768
1793
  finish();
1769
1794
  };
1770
1795
  const fail = () => {
1771
- playbackFailed = true;
1772
- stop();
1796
+ if (!finished) {
1797
+ playbackFailed = true;
1798
+ stop();
1799
+ }
1773
1800
  };
1801
+ stopGenerated = stop;
1774
1802
  const clearWatchdog = watchSpeech(Math.max(line.seconds, estimatedBrowserSeconds(text)), fail);
1775
1803
  speechStops.add(stop);
1776
1804
  playbackFailure = fail;
@@ -1785,7 +1813,9 @@ function createVideoChatVoice(options = {}) {
1785
1813
  element.onerror = fail;
1786
1814
  signal.addEventListener("abort", stop, { once: true });
1787
1815
  try {
1788
- if (!held) void element.play().catch(fail);
1816
+ if (!held) void element.play().catch(() => {
1817
+ if (!held) fail();
1818
+ });
1789
1819
  } catch {
1790
1820
  fail();
1791
1821
  }
@@ -1811,6 +1841,11 @@ function createVideoChatVoice(options = {}) {
1811
1841
  speechStops.clear();
1812
1842
  sounding?.pause();
1813
1843
  sounding = void 0;
1844
+ generatedElement?.pause();
1845
+ generatedElement?.removeAttribute?.("src");
1846
+ generatedElement?.load?.();
1847
+ generatedElement = void 0;
1848
+ stopGenerated = void 0;
1814
1849
  stopBrowser();
1815
1850
  for (const line of lines.values()) {
1816
1851
  if (line.source === "generated") URL.revokeObjectURL(line.src);
@@ -2423,8 +2458,18 @@ function useVideoChatSession(options = {}) {
2423
2458
  planned[position] = plannedScene;
2424
2459
  received[position] = plannedScene;
2425
2460
  const rendererReady = currentOptions.templates?.getTemplate(plannedScene.templateId) ? Promise.resolve() : Promise.resolve(preloadBuiltinTemplate(plannedScene.templateId));
2426
- const visualPreparation = Promise.all([rendererReady, prepareSceneMedia(plannedScene.variables, controller.signal)]).then(() => void 0, () => {
2427
- throw new VideoError("Scene could not prepare its visual", { code: "media_not_ready" });
2461
+ const visualPreparation = rendererReady.then(async () => {
2462
+ try {
2463
+ await prepareSceneMedia(plannedScene.variables, controller.signal);
2464
+ return plannedScene;
2465
+ } catch (cause) {
2466
+ if (controller.signal.aborted) throw cause;
2467
+ const override = currentOptions.templates?.getTemplate(plannedScene.templateId);
2468
+ const fallback = !override || supportsExternalVideoBackdrop(override) ? recoverSceneMedia(plannedScene) : void 0;
2469
+ if (!fallback) throw new VideoError("Scene could not prepare its visual", { code: "media_not_ready" });
2470
+ warn(MEDIA_RECOVERY_NOTICE);
2471
+ return fallback;
2472
+ }
2428
2473
  });
2429
2474
  void visualPreparation.catch(() => void 0);
2430
2475
  warmSceneMedia(plannedScene.variables);
@@ -2453,15 +2498,14 @@ function useVideoChatSession(options = {}) {
2453
2498
  narrating = narrated.catch(() => "");
2454
2499
  pending.push(narrated.then(async (line) => {
2455
2500
  if (!isCurrent() || currentAttempt !== attempt) return;
2456
- const visual = plannedScene;
2457
- const withNarration = line ? { ...visual, narration: line } : visual;
2458
2501
  const spoken = line ? await prepareSpeech(plannedScene.narrationGroup?.text ?? line, controller.signal).catch((cause) => {
2459
2502
  if (controller.signal.aborted) throw cause;
2460
2503
  warn("Some narration is unavailable; the response will continue.");
2461
2504
  return void 0;
2462
2505
  }) : void 0;
2463
2506
  if (!isCurrent() || currentAttempt !== attempt) return;
2464
- await visualPreparation;
2507
+ const visual = await visualPreparation;
2508
+ const withNarration = line ? { ...visual, narration: line } : visual;
2465
2509
  const group = plannedScene.narrationGroup;
2466
2510
  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" });
2467
2511
  ready[position] = group ? withNarration : pacedScene(withNarration, spoken?.seconds, currentOptions.templates);