@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
package/CHANGELOG.md CHANGED
@@ -4,6 +4,25 @@ VanillaSky follows semantic versioning. This changelog begins with the 0.1 beta.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.10.1
8
+
9
+ - 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.
10
+
11
+ - 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.
12
+ - Ask follow-up generation to exclude questions already answered by the full narration, including paraphrases, and to preserve the conversation’s intent.
13
+
14
+ ## 0.10.0
15
+
16
+ ### Breaking
17
+
18
+ - Remove `focusCards` from the built-in catalog and generated source templates, as requested. Existing callers must replace it with narration over `cinemaMedia`, or an appropriate comparison/timeline when supported by their content. Persisted videos using that ID must be regenerated before playback with the new catalog.
19
+
20
+ ### Changed
21
+
22
+ - Opening narration advances into the first shot and body. Exact repeated introductory sentences are removed without additional model calls or startup waits.
23
+ - Prefer relevant footage over repeated graphic scenes; use no more than one explanatory overlay in a typical thirty-second answer. Comparison, key figure, quote and timeline can now use optional host-resolved media with a contrast scrim and soft text shadows. Black remains the fallback; chapter titles stay black.
24
+
25
+
7
26
  ## 0.9.0
8
27
 
9
28
  - Continue video playback with subtitles when native browser speech cannot start, without weakening generated-audio readiness or counting paused time toward the fallback.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  BUILTIN_SERVER_TEMPLATE_KIT
3
- } from "./chunk-MQILEEXC.js";
4
- import "./chunk-LH4BPALA.js";
3
+ } from "./chunk-IR44XKBI.js";
4
+ import "./chunk-IIN5M5HW.js";
5
5
  import "./chunk-2E6T633S.js";
6
6
  export {
7
7
  BUILTIN_SERVER_TEMPLATE_KIT
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  VideoFrame
3
- } from "./chunk-3ZTEFJIR.js";
4
- import "./chunk-UDIODEDK.js";
3
+ } from "./chunk-6KZGF63O.js";
4
+ import "./chunk-224QNWRA.js";
5
5
  import "./chunk-TVIU23OM.js";
6
6
  import "./chunk-SPVTJH3F.js";
7
7
  import "./chunk-R3XAOMKP.js";
@@ -19,8 +19,12 @@ function resolveMediaType(mediaType, mediaUrl) {
19
19
  if (mediaType === "photo") return "photo";
20
20
  return mediaUrl && isVideoUrl(mediaUrl) ? "video" : "photo";
21
21
  }
22
+ function hasSceneMedia(variables) {
23
+ return String(variables.mediaUrl || "").trim() !== "" && String(variables.mediaType || "auto") !== "gradient";
24
+ }
22
25
 
23
26
  export {
24
27
  limitsConcurrentVideoDecoders,
25
- resolveMediaType
28
+ resolveMediaType,
29
+ hasSceneMedia
26
30
  };
@@ -80,31 +80,6 @@ var BUILTIN_TEMPLATE_SCHEMAS = {
80
80
  ],
81
81
  "additionalProperties": false
82
82
  },
83
- "focusCards": {
84
- "type": "object",
85
- "properties": {
86
- "items": {
87
- "type": "array",
88
- "minItems": 2,
89
- "maxItems": 4,
90
- "items": {
91
- "type": "string",
92
- "minLength": 1,
93
- "maxLength": 55
94
- },
95
- "default": [
96
- "Listen closely",
97
- "Notice the pattern",
98
- "Make room for change"
99
- ],
100
- "description": "2\u20134 parallel phrases, about 2\u20136 words each; no heading or numbering."
101
- }
102
- },
103
- "required": [
104
- "items"
105
- ],
106
- "additionalProperties": false
107
- },
108
83
  "editorialTimeline": {
109
84
  "type": "object",
110
85
  "properties": {
@@ -138,12 +113,59 @@ var BUILTIN_TEMPLATE_SCHEMAS = {
138
113
  }
139
114
  ],
140
115
  "description": "3\u20135 ordered events or actions, each with one short label; no dates."
116
+ },
117
+ "mediaKeyword": {
118
+ "type": "string",
119
+ "format": "stock-media-keyword",
120
+ "minLength": 1,
121
+ "maxLength": 80,
122
+ "description": "2\u20138 word literal subject/action search intent, maximum 80 characters. Host resolves URLs.",
123
+ "examples": [
124
+ "Ocean waves breaking on a rocky shore"
125
+ ]
126
+ },
127
+ "mediaUrl": {
128
+ "type": "string",
129
+ "format": "uri",
130
+ "description": "Host-only approved photo or video URL.",
131
+ "default": ""
132
+ },
133
+ "mediaType": {
134
+ "type": "string",
135
+ "enum": [
136
+ "photo",
137
+ "video"
138
+ ],
139
+ "default": "video",
140
+ "description": "Host-resolved asset kind."
141
+ },
142
+ "mediaPoster": {
143
+ "type": "string",
144
+ "format": "uri",
145
+ "description": "Host-only approved poster URL for video decoding.",
146
+ "default": ""
147
+ },
148
+ "shotDirection": {
149
+ "type": "string",
150
+ "maxLength": 220,
151
+ "description": "Optional action, framing and continuity for generation. Preserve the subject; do not request rendered text."
152
+ },
153
+ "mediaSource": {
154
+ "type": "string",
155
+ "enum": [
156
+ "generate",
157
+ "stock"
158
+ ],
159
+ "description": "Generate distinctive illustrative shots; use approved stock for familiar observable subjects."
141
160
  }
142
161
  },
143
162
  "required": [
144
163
  "events"
145
164
  ],
146
- "additionalProperties": false
165
+ "additionalProperties": false,
166
+ "x-vanillasky": {
167
+ "allowsStockMedia": true
168
+ }
147
169
  },
148
170
  "mobileMessage": {
149
171
  "type": "object",
@@ -239,13 +261,60 @@ var BUILTIN_TEMPLATE_SCHEMAS = {
239
261
  "maxLength": 60,
240
262
  "default": "More room to think",
241
263
  "description": "Second real alternative, parallel to the first."
264
+ },
265
+ "mediaKeyword": {
266
+ "type": "string",
267
+ "format": "stock-media-keyword",
268
+ "minLength": 1,
269
+ "maxLength": 80,
270
+ "description": "2\u20138 word literal subject/action search intent, maximum 80 characters. Host resolves URLs.",
271
+ "examples": [
272
+ "Ocean waves breaking on a rocky shore"
273
+ ]
274
+ },
275
+ "mediaUrl": {
276
+ "type": "string",
277
+ "format": "uri",
278
+ "description": "Host-only approved photo or video URL.",
279
+ "default": ""
280
+ },
281
+ "mediaType": {
282
+ "type": "string",
283
+ "enum": [
284
+ "photo",
285
+ "video"
286
+ ],
287
+ "default": "video",
288
+ "description": "Host-resolved asset kind."
289
+ },
290
+ "mediaPoster": {
291
+ "type": "string",
292
+ "format": "uri",
293
+ "description": "Host-only approved poster URL for video decoding.",
294
+ "default": ""
295
+ },
296
+ "shotDirection": {
297
+ "type": "string",
298
+ "maxLength": 220,
299
+ "description": "Optional action, framing and continuity for generation. Preserve the subject; do not request rendered text."
300
+ },
301
+ "mediaSource": {
302
+ "type": "string",
303
+ "enum": [
304
+ "generate",
305
+ "stock"
306
+ ],
307
+ "description": "Generate distinctive illustrative shots; use approved stock for familiar observable subjects."
242
308
  }
243
309
  },
244
310
  "required": [
245
311
  "leftText",
246
312
  "rightText"
247
313
  ],
248
- "additionalProperties": false
314
+ "additionalProperties": false,
315
+ "x-vanillasky": {
316
+ "allowsStockMedia": true
317
+ }
249
318
  },
250
319
  "quote": {
251
320
  "type": "object",
@@ -268,13 +337,60 @@ var BUILTIN_TEMPLATE_SCHEMAS = {
268
337
  "Illustrative example"
269
338
  ],
270
339
  "description": "Grounded attribution belonging to these exact quoted words."
340
+ },
341
+ "mediaKeyword": {
342
+ "type": "string",
343
+ "format": "stock-media-keyword",
344
+ "minLength": 1,
345
+ "maxLength": 80,
346
+ "description": "2\u20138 word literal subject/action search intent, maximum 80 characters. Host resolves URLs.",
347
+ "examples": [
348
+ "Ocean waves breaking on a rocky shore"
349
+ ]
350
+ },
351
+ "mediaUrl": {
352
+ "type": "string",
353
+ "format": "uri",
354
+ "description": "Host-only approved photo or video URL.",
355
+ "default": ""
356
+ },
357
+ "mediaType": {
358
+ "type": "string",
359
+ "enum": [
360
+ "photo",
361
+ "video"
362
+ ],
363
+ "default": "video",
364
+ "description": "Host-resolved asset kind."
365
+ },
366
+ "mediaPoster": {
367
+ "type": "string",
368
+ "format": "uri",
369
+ "description": "Host-only approved poster URL for video decoding.",
370
+ "default": ""
371
+ },
372
+ "shotDirection": {
373
+ "type": "string",
374
+ "maxLength": 220,
375
+ "description": "Optional action, framing and continuity for generation. Preserve the subject; do not request rendered text."
376
+ },
377
+ "mediaSource": {
378
+ "type": "string",
379
+ "enum": [
380
+ "generate",
381
+ "stock"
382
+ ],
383
+ "description": "Generate distinctive illustrative shots; use approved stock for familiar observable subjects."
271
384
  }
272
385
  },
273
386
  "required": [
274
387
  "quote",
275
388
  "attribution"
276
389
  ],
277
- "additionalProperties": false
390
+ "additionalProperties": false,
391
+ "x-vanillasky": {
392
+ "allowsStockMedia": true
393
+ }
278
394
  },
279
395
  "keyFigure": {
280
396
  "type": "object",
@@ -297,6 +413,50 @@ var BUILTIN_TEMPLATE_SCHEMAS = {
297
413
  "Illustrative example"
298
414
  ],
299
415
  "description": "Exactly one short label identifying the quantity and its context."
416
+ },
417
+ "mediaKeyword": {
418
+ "type": "string",
419
+ "format": "stock-media-keyword",
420
+ "minLength": 1,
421
+ "maxLength": 80,
422
+ "description": "2\u20138 word literal subject/action search intent, maximum 80 characters. Host resolves URLs.",
423
+ "examples": [
424
+ "Ocean waves breaking on a rocky shore"
425
+ ]
426
+ },
427
+ "mediaUrl": {
428
+ "type": "string",
429
+ "format": "uri",
430
+ "description": "Host-only approved photo or video URL.",
431
+ "default": ""
432
+ },
433
+ "mediaType": {
434
+ "type": "string",
435
+ "enum": [
436
+ "photo",
437
+ "video"
438
+ ],
439
+ "default": "video",
440
+ "description": "Host-resolved asset kind."
441
+ },
442
+ "mediaPoster": {
443
+ "type": "string",
444
+ "format": "uri",
445
+ "description": "Host-only approved poster URL for video decoding.",
446
+ "default": ""
447
+ },
448
+ "shotDirection": {
449
+ "type": "string",
450
+ "maxLength": 220,
451
+ "description": "Optional action, framing and continuity for generation. Preserve the subject; do not request rendered text."
452
+ },
453
+ "mediaSource": {
454
+ "type": "string",
455
+ "enum": [
456
+ "generate",
457
+ "stock"
458
+ ],
459
+ "description": "Generate distinctive illustrative shots; use approved stock for familiar observable subjects."
300
460
  }
301
461
  },
302
462
  "required": [
@@ -305,7 +465,8 @@ var BUILTIN_TEMPLATE_SCHEMAS = {
305
465
  ],
306
466
  "additionalProperties": false,
307
467
  "x-vanillasky": {
308
- "requiresStat": true
468
+ "requiresStat": true,
469
+ "allowsStockMedia": true
309
470
  }
310
471
  }
311
472
  };
@@ -369,38 +530,9 @@ var BUILTIN_TEMPLATE_MANIFEST = [
369
530
  },
370
531
  schema: BUILTIN_TEMPLATE_SCHEMAS.chapterTitle
371
532
  },
372
- {
373
- "label": "Focus cards",
374
- "description": "Two to four short phrases fade in sequentially on black; no boxes or heading.",
375
- "usesGlobalTextEffect": false,
376
- "usesGlobalTransition": false,
377
- "usesGlobalBackgroundEffect": false,
378
- "textCanvas": "tight",
379
- "id": "focusCards",
380
- "family": "Explainers",
381
- "jobs": [
382
- "setup",
383
- "proof"
384
- ],
385
- "register": "card-led",
386
- "useWhen": "Two to four short parallel points support an explanation. ",
387
- "avoidWhen": "The points require an order, long paragraphs, numerical data or additional images to make sense.",
388
- "minDuration": 4,
389
- "preferredDuration": 5,
390
- "timing": {
391
- "contentFields": [
392
- "items"
393
- ],
394
- "contentUnit": "items",
395
- "revealSeconds": 2.4,
396
- "holdSeconds": 1.8,
397
- "exitSeconds": 0
398
- },
399
- schema: BUILTIN_TEMPLATE_SCHEMAS.focusCards
400
- },
401
533
  {
402
534
  "label": "Timeline",
403
- "description": "Three to five events on a fine continuous line on black; without dates.",
535
+ "description": "Three to five events on a fine continuous line over relevant media or black; without dates.",
404
536
  "usesGlobalTextEffect": false,
405
537
  "usesGlobalTransition": false,
406
538
  "usesGlobalBackgroundEffect": false,
@@ -458,7 +590,7 @@ var BUILTIN_TEMPLATE_MANIFEST = [
458
590
  },
459
591
  {
460
592
  "label": "Comparison",
461
- "description": "Two short statements with equal weight on black, side by side or stacked in portrait.",
593
+ "description": "Two short statements with equal weight over relevant media or black, side by side or stacked in portrait.",
462
594
  "usesGlobalTextEffect": false,
463
595
  "usesGlobalTransition": false,
464
596
  "usesGlobalBackgroundEffect": false,
@@ -489,7 +621,7 @@ var BUILTIN_TEMPLATE_MANIFEST = [
489
621
  },
490
622
  {
491
623
  "label": "Quote",
492
- "description": "A short exact quotation with a legible attribution on black.",
624
+ "description": "A short exact quotation with a legible attribution over relevant media or black.",
493
625
  "usesGlobalTextEffect": false,
494
626
  "usesGlobalTransition": false,
495
627
  "usesGlobalBackgroundEffect": false,
@@ -518,7 +650,7 @@ var BUILTIN_TEMPLATE_MANIFEST = [
518
650
  },
519
651
  {
520
652
  "label": "Key figure",
521
- "description": "One supplied figure with one short label on black.",
653
+ "description": "One supplied figure with one short label over relevant media or black.",
522
654
  "usesGlobalTextEffect": false,
523
655
  "usesGlobalTransition": false,
524
656
  "usesGlobalBackgroundEffect": false,
@@ -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)) {
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-77VY4O7A.js";
7
7
  import {
8
8
  resolveMediaType
9
- } from "./chunk-UDIODEDK.js";
9
+ } from "./chunk-224QNWRA.js";
10
10
  import {
11
11
  useExternalVideoBackdrop
12
12
  } from "./chunk-TVIU23OM.js";
@@ -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
  )),
@@ -195,14 +200,7 @@ var SceneBackground = ({
195
200
  ] });
196
201
  };
197
202
 
198
- // src/visual-system/scene-templates/cinema-media.tsx
199
- import { jsx as jsx2 } from "react/jsx-runtime";
200
- function MediaScene({ variables, style, width, height, progress, sceneDuration, isPlaying }) {
201
- return /* @__PURE__ */ jsx2("div", { "data-template": "cinemaMedia", style: { width, height, position: "relative", overflow: "hidden" }, children: /* @__PURE__ */ jsx2(SceneBackground, { style, width, height, progress, sceneDuration, ...getMediaBackgroundProps(variables), mediaTreatment: "none", backgroundEffect: "none", isPlaying }) });
202
- }
203
- var MediaSceneTemplate = MediaScene;
204
-
205
203
  export {
206
- MediaScene,
207
- MediaSceneTemplate
204
+ getMediaBackgroundProps,
205
+ SceneBackground
208
206
  };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  limitsConcurrentVideoDecoders,
3
3
  resolveMediaType
4
- } from "./chunk-UDIODEDK.js";
4
+ } from "./chunk-224QNWRA.js";
5
5
  import {
6
6
  ExternalVideoBackdropProvider,
7
7
  resolveDensity
@@ -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
  )