@vanillaskyai/video 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/PUBLIC-API.md +83 -79
  3. package/README.md +16 -15
  4. package/dist/{builtin-server-KB7FKF6A.js → builtin-server-CUYPIDE7.js} +2 -2
  5. package/dist/check-runtime.js +1 -1
  6. package/dist/{chunk-2PYO6VAC.js → chunk-2DOLF4QN.js} +1 -43
  7. package/dist/{chunk-GAIOHGNR.js → chunk-2OKB27TG.js} +42 -45
  8. package/dist/{chunk-K746NUIP.js → chunk-2USVB3CY.js} +80 -29
  9. package/dist/{chunk-RE4IMWJR.js → chunk-3VRMKFM7.js} +87 -14
  10. package/dist/{chunk-JNN3EYVP.js → chunk-4UNKZEAT.js} +46 -46
  11. package/dist/{chunk-2XT4MZ76.js → chunk-APQKY6P3.js} +36 -3
  12. package/dist/{chunk-LLPMVDJZ.js → chunk-L7QGGKJ2.js} +69 -38
  13. package/dist/{chunk-RGF452LL.js → chunk-OWBT45EN.js} +1 -1
  14. package/dist/{chunk-LKBZX7GV.js → chunk-PP3MCPN4.js} +4 -4
  15. package/dist/chunk-ZD2VTUYR.js +28 -0
  16. package/dist/cli.js +131 -24
  17. package/dist/{compose-video-PD6LKKRK.js → compose-video-2NFH4DZY.js} +3 -3
  18. package/dist/{events-B6qS1Lsb.d.ts → events-BQ6z264n.d.ts} +1 -1
  19. package/dist/index.d.ts +5 -67
  20. package/dist/index.js +2 -4
  21. package/dist/{kit-8g46H2RZ.d.ts → kit-tQnCPMOM.d.ts} +1 -1
  22. package/dist/react.d.ts +33 -53
  23. package/dist/react.js +642 -536
  24. package/dist/server.d.ts +11 -13
  25. package/dist/server.js +170 -81
  26. package/dist/template-catalog.js +1 -1
  27. package/dist/templates.d.ts +3 -3
  28. package/dist/test.d.ts +2 -2
  29. package/dist/test.js +3 -3
  30. package/dist/{text-stream-ROTNGSD4.js → text-stream-54LXABJW.js} +1 -1
  31. package/dist/{types-2wHBqtg8.d.ts → types-CEh9eUFu.d.ts} +1 -1
  32. package/dist/{types-DrABlRa7.d.ts → types-DWFO6qca.d.ts} +1 -1
  33. package/docs/agent-integration.md +9 -3
  34. package/docs/architecture.md +8 -10
  35. package/docs/concepts.md +16 -40
  36. package/docs/custom-templates.md +27 -23
  37. package/docs/customization.md +12 -49
  38. package/docs/errors.md +58 -68
  39. package/docs/getting-started.md +21 -14
  40. package/docs/media-and-audio.md +2 -2
  41. package/docs/performance.md +69 -0
  42. package/docs/persistence.md +12 -27
  43. package/docs/production.md +2 -2
  44. package/docs/provider-integration.md +11 -5
  45. package/docs/reference/provider-adapters.md +37 -89
  46. package/docs/security.md +1 -1
  47. package/docs/streaming-protocol.md +2 -2
  48. package/examples/custom-template/README.md +6 -6
  49. package/package.json +6 -7
  50. package/registry/items/barChart.json +2 -2
  51. package/registry/items/bigNumber.json +2 -2
  52. package/registry/items/brandMessage.json +2 -2
  53. package/registry/items/cardList.json +2 -2
  54. package/registry/items/codeEditor.json +2 -2
  55. package/registry/items/ctaLogo.json +2 -2
  56. package/registry/items/ctaMedia.json +2 -2
  57. package/registry/items/incomingCall.json +2 -2
  58. package/registry/items/media.json +2 -2
  59. package/registry/items/milestone.json +2 -2
  60. package/registry/items/notification.json +2 -2
  61. package/registry/items/phoneMockup.json +2 -2
  62. package/registry/items/problemSolution.json +2 -2
  63. package/registry/items/progressRing.json +2 -2
  64. package/registry/items/promptInput.json +2 -2
  65. package/registry/items/reaction.json +2 -2
  66. package/registry/items/reviewStack.json +2 -2
  67. package/registry/items/steps.json +2 -2
  68. package/registry/items/terminal.json +2 -2
  69. package/registry/items/testimonial.json +2 -2
  70. package/registry/items/tripleStats.json +2 -2
  71. package/registry/items/tweet.json +2 -2
  72. package/registry/items/webMockup.json +2 -2
  73. package/starters/video-chat/.env.example +4 -2
  74. package/starters/video-chat/README.md +17 -5
  75. package/starters/video-chat/package.json +2 -4
  76. package/starters/video-chat/providers/speech.ts +20 -0
  77. package/starters/video-chat/providers/video.ts +66 -0
  78. package/starters/video-chat/providers.ts +3 -0
  79. package/starters/video-chat/server.ts +4 -80
  80. package/starters/video-chat/stock.ts +114 -99
  81. package/dist/chunk-SKRGRKHY.js +0 -142
  82. package/dist/state-DZcKuS32.d.ts +0 -3
  83. package/docs/branding-and-personalization.md +0 -87
  84. package/examples/custom-template/supplied-media.tsx +0 -92
package/dist/server.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { o as VideoGenerationContext, p as VideoPlanner, q as VideoRequest, a as VideoCapabilities, g as VideoInput, d as VideoAudio, r as VideoSceneValidator, j as VideoTemplatePacing, s as VideoSnapshotRetention, t as VideoResumeCursor, u as VideoSceneValidationContext, e as VideoScene, b as VideoOrientation } from './types-2wHBqtg8.js';
2
- import { a as VideoFinishReason, V as VideoWarning, b as VideoEvent } from './events-B6qS1Lsb.js';
3
- export { c as VideoWarningCategory } from './events-B6qS1Lsb.js';
1
+ import { m as VideoGenerationContext, n as VideoPlanner, o as VideoRequest, a as VideoCapabilities, g as VideoInput, d as VideoAudio, p as VideoSceneValidator, j as VideoTemplatePacing, q as VideoSnapshotRetention, r as VideoResumeCursor, s as VideoSceneValidationContext, e as VideoScene, b as VideoOrientation } from './types-CEh9eUFu.js';
2
+ import { a as VideoFinishReason, V as VideoWarning, b as VideoEvent } from './events-BQ6z264n.js';
3
+ export { c as VideoWarningCategory } from './events-BQ6z264n.js';
4
4
  import { S as SceneTemplateMetadata } from './catalog-types-BIhSpOWK.js';
5
- import { V as VideoChatWelcomeOptions } from './types-DrABlRa7.js';
6
- export { a as VideoChatCapabilities, b as VideoChatConversationTurn, c as VideoChatMode, d as VideoChatWelcomePrompt } from './types-DrABlRa7.js';
5
+ import { f as VideoChatWelcomeOptions } from './types-DWFO6qca.js';
6
+ export { d as VideoChatCapabilities, g as VideoChatConversationTurn, V as VideoChatMode, h as VideoChatWelcomePrompt } from './types-DWFO6qca.js';
7
7
 
8
8
  interface VideoProviderUsage {
9
9
  inputTokens?: number;
@@ -104,7 +104,6 @@ interface VideoStreamHandlerOptions {
104
104
  signal: AbortSignal;
105
105
  }) => AsyncIterable<VideoEvent>;
106
106
  }
107
- type VideoStreamHandler = (request: Request) => Promise<Response>;
108
107
 
109
108
  interface ServerTemplateRegistry {
110
109
  readonly capabilities: VideoCapabilities;
@@ -185,11 +184,6 @@ interface VideoHandlerOptions extends Omit<VideoStreamHandlerOptions, "generate"
185
184
  */
186
185
  narrate?: boolean;
187
186
  }
188
- /**
189
- * Build the secure route for a template kit while leaving provider, model,
190
- * credentials, authentication, and deployment ownership with the app.
191
- */
192
- declare function createVideoHandler(options: VideoHandlerOptions): VideoStreamHandler;
193
187
 
194
188
  type VideoChatTextTask = "narration" | "suggestions";
195
189
  interface VideoChatTextContext {
@@ -224,9 +218,11 @@ interface VideoChatMediaContext {
224
218
  scene?: Readonly<VideoScene>;
225
219
  templateId?: string;
226
220
  preferredType?: "image" | "video" | "any";
221
+ /** Optional broader subject for atmospheric opening stock, not instructional footage. */
222
+ fallbackQuery?: string;
227
223
  }
228
224
  type VideoChatMediaResolver = (query: string, context: VideoChatMediaContext) => ResolvedMedia | null | Promise<ResolvedMedia | null>;
229
- interface VideoChatHandlerOptions extends Omit<VideoHandlerOptions, "allowMediaUrl" | "basePrompt" | "maxResolvedMedia" | "narrate" | "resolveMedia"> {
225
+ interface VideoChatHandlerOptions extends Pick<VideoHandlerOptions, "templates" | "streamText" | "includeRawProviderData" | "mediaConcurrency" | "allowedOrigins" | "authorize" | "maxBodyBytes" | "heartbeatMs" | "onError" | "onWarning" | "onComplete" | "invalidPartBehavior" | "requireCloser" | "allowCredentials"> {
230
226
  /** Generate the small non-streaming text tasks around the visual plan. */
231
227
  generateText: VideoChatTextGenerator;
232
228
  /** Optional generated speech. Browsers can speak locally when absent. */
@@ -237,6 +233,8 @@ interface VideoChatHandlerOptions extends Omit<VideoHandlerOptions, "allowMediaU
237
233
  searchMedia?: VideoChatMediaResolver;
238
234
  /** Optional generated-video provider. Its presence enables paid visual modes. */
239
235
  generateVideo?: VideoChatMediaResolver;
236
+ /** Maximum generated-video attempts per response, including failures. Defaults to 5. */
237
+ maxGeneratedVideos?: number;
240
238
  /** Trusted application guidance appended to the general-purpose response brief. */
241
239
  instructions?: string;
242
240
  /** Application-owned prompts and visual searches shown before the first turn. */
@@ -255,4 +253,4 @@ type VideoChatHandler = (request: Request) => Promise<Response>;
255
253
  */
256
254
  declare function createVideoChatHandler(options: VideoChatHandlerOptions): VideoChatHandler;
257
255
 
258
- export { type MediaResolver, type MediaResolverContext, type ResolvedMedia, SceneTemplateMetadata as ServerTemplateMetadata, type ServerTemplateRegistry, type VideoChatHandlerOptions, VideoChatWelcomeOptions, VideoFinishReason, type VideoGenerationSummary, type VideoHandlerOptions, type VideoProviderUsage, VideoWarning, createServerTemplateRegistry, createVideoChatHandler, createVideoHandler };
256
+ export { SceneTemplateMetadata as ServerTemplateMetadata, type ServerTemplateRegistry, type VideoChatHandlerOptions, VideoChatWelcomeOptions, VideoFinishReason, type VideoGenerationSummary, type VideoProviderUsage, VideoWarning, createServerTemplateRegistry, createVideoChatHandler };
package/dist/server.js CHANGED
@@ -1,15 +1,18 @@
1
1
  import {
2
2
  createTextDeltaVideoPlanner
3
- } from "./chunk-LLPMVDJZ.js";
3
+ } from "./chunk-L7QGGKJ2.js";
4
4
  import {
5
- createVideo
6
- } from "./chunk-RE4IMWJR.js";
7
- import "./chunk-E7CL7UPB.js";
5
+ createVideo,
6
+ invokeIsolated
7
+ } from "./chunk-3VRMKFM7.js";
8
+ import {
9
+ getGenerationLifecycleSink
10
+ } from "./chunk-E7CL7UPB.js";
8
11
  import {
9
12
  BUILTIN_SERVER_TEMPLATE_KIT,
10
13
  createServerTemplateRegistry,
11
14
  overlayServerTemplateRegistry
12
- } from "./chunk-RGF452LL.js";
15
+ } from "./chunk-OWBT45EN.js";
13
16
  import {
14
17
  createTemplateSceneValidator
15
18
  } from "./chunk-LMG3GHLL.js";
@@ -17,9 +20,10 @@ import {
17
20
  decodeVideoSse,
18
21
  encodeVideoSseEvent,
19
22
  sanitizeVideoChatMedia,
20
- videoSseHeaders
21
- } from "./chunk-2XT4MZ76.js";
22
- import "./chunk-GAIOHGNR.js";
23
+ videoSseHeaders,
24
+ withDeadline
25
+ } from "./chunk-APQKY6P3.js";
26
+ import "./chunk-2OKB27TG.js";
23
27
  import {
24
28
  allowedKeys,
25
29
  createClippedVariableWarning,
@@ -33,7 +37,7 @@ import {
33
37
  } from "./chunk-MMUXVA47.js";
34
38
  import {
35
39
  MAX_RETAINED_MEDIA_URL_LENGTH
36
- } from "./chunk-2PYO6VAC.js";
40
+ } from "./chunk-2DOLF4QN.js";
37
41
  import {
38
42
  VIDEO_PROTOCOL_VERSION
39
43
  } from "./chunk-AOWSXU2K.js";
@@ -43,7 +47,7 @@ import {
43
47
  } from "./chunk-AITKH6QT.js";
44
48
  import "./chunk-5SLENAJW.js";
45
49
  import "./chunk-352ZZCET.js";
46
- import "./chunk-JNN3EYVP.js";
50
+ import "./chunk-4UNKZEAT.js";
47
51
  import "./chunk-3O7OMMMF.js";
48
52
  import "./chunk-2E6T633S.js";
49
53
  import {
@@ -471,9 +475,6 @@ function fallbackVariables(variables) {
471
475
  fallback.mediaType = "gradient";
472
476
  return fallback;
473
477
  }
474
- function isAbortError(error) {
475
- return error instanceof Error && error.name === "AbortError";
476
- }
477
478
  async function resolveVariables(options) {
478
479
  if (!Object.hasOwn(options.variables, "mediaKeyword")) return options.variables;
479
480
  const variables = { ...options.variables };
@@ -484,6 +485,10 @@ async function resolveVariables(options) {
484
485
  return fallbackVariables(variables);
485
486
  }
486
487
  if (options.claimBudget && !options.claimBudget()) return fallbackVariables(variables);
488
+ const fallback = () => {
489
+ options.onFallback?.();
490
+ return fallbackVariables(variables);
491
+ };
487
492
  let resolved;
488
493
  try {
489
494
  resolved = await options.resolveMedia(rawQuery, {
@@ -495,28 +500,27 @@ async function resolveVariables(options) {
495
500
  generatedLook: options.input.style?.generatedLook,
496
501
  signal: options.signal
497
502
  });
503
+ if (options.signal.aborted) {
504
+ throw options.signal.reason ?? new DOMException("The media request was aborted", "AbortError");
505
+ }
506
+ if (!resolved) return fallback();
507
+ if (resolved.type !== "image" && resolved.type !== "video") {
508
+ throw new Error("Resolved media type is invalid");
509
+ }
510
+ const url = cleanUrl(resolved.url, "URL");
511
+ if (!url) throw new Error("Resolved media URL is required");
512
+ const posterUrl = options.contract.acceptsPoster ? cleanUrl(resolved.posterUrl, "poster URL") : void 0;
513
+ options.approveUrl(options.input, url);
514
+ if (posterUrl) options.approveUrl(options.input, posterUrl);
515
+ variables.mediaUrl = url;
516
+ variables.mediaType = resolved.type === "image" ? "photo" : "video";
517
+ if (posterUrl) variables.mediaPoster = posterUrl;
518
+ else delete variables.mediaPoster;
519
+ return variables;
498
520
  } catch (error) {
499
521
  if (options.signal.aborted) throw options.signal.reason ?? error;
500
- if (isAbortError(error)) throw error;
501
- return fallbackVariables(variables);
502
- }
503
- if (options.signal.aborted) {
504
- throw options.signal.reason ?? new DOMException("The media request was aborted", "AbortError");
505
- }
506
- if (!resolved) return fallbackVariables(variables);
507
- if (resolved.type !== "image" && resolved.type !== "video") {
508
- throw new Error("Resolved media type is invalid");
522
+ return fallback();
509
523
  }
510
- const url = cleanUrl(resolved.url, "URL");
511
- if (!url) throw new Error("Resolved media URL is required");
512
- const posterUrl = options.contract.acceptsPoster ? cleanUrl(resolved.posterUrl, "poster URL") : void 0;
513
- options.approveUrl(options.input, url);
514
- if (posterUrl) options.approveUrl(options.input, posterUrl);
515
- variables.mediaUrl = url;
516
- variables.mediaType = resolved.type === "image" ? "photo" : "video";
517
- if (posterUrl) variables.mediaPoster = posterUrl;
518
- else delete variables.mediaPoster;
519
- return variables;
520
524
  }
521
525
  function templateContract(templates, templateId) {
522
526
  const metadata = templates.getTemplateMetadata(templateId);
@@ -544,7 +548,8 @@ async function resolvePartVariables(options) {
544
548
  resolveMedia: options.resolveMedia,
545
549
  approveUrl: options.approveUrl,
546
550
  openingReady: options.openingReady,
547
- claimBudget: options.claimBudget
551
+ claimBudget: options.claimBudget,
552
+ onFallback: options.onFallback
548
553
  };
549
554
  if (options.part.type === "scene.add") {
550
555
  const variables = await resolveVariables({
@@ -583,7 +588,13 @@ function createMediaResolvingPlanner(options) {
583
588
  resolveMedia: options.resolveMedia,
584
589
  approveUrl: options.approveUrl,
585
590
  openingReady: openingReady2 ?? options.isOpeningReady(context.request.input),
586
- claimBudget
591
+ claimBudget,
592
+ onFallback: () => getGenerationLifecycleSink(context)?.reportWarning?.({
593
+ code: "provider_warning",
594
+ category: "provider",
595
+ message: "Some visuals use a simple background.",
596
+ recoverable: true
597
+ })
587
598
  });
588
599
  if (limit === 1) {
589
600
  for await (const part of options.planner(context)) yield await resolveOne(part);
@@ -688,22 +699,36 @@ function boundSceneVariables(templates, templateId, variables) {
688
699
  }
689
700
  function createBoundedVariablePlanner(options) {
690
701
  return async function* boundPlan(context) {
691
- for await (const part of options.planner(context)) {
702
+ for await (let part of options.planner(context)) {
692
703
  if (part.type !== "scene.add") {
693
704
  yield part;
694
705
  continue;
695
706
  }
696
- const { variables, clipped } = boundSceneVariables(
697
- options.templates,
698
- part.scene.templateId,
699
- part.scene.variables
700
- );
701
- if (clipped.length === 0) {
702
- yield part;
703
- continue;
707
+ try {
708
+ const lifecycle = getGenerationLifecycleSink(context);
709
+ if (!options.templates.getTemplateMetadata(part.scene.templateId) && lifecycle?.recoverGeneratedParts && (context.request.capabilities?.templates == null || context.request.capabilities.templates.includes("media"))) {
710
+ const copy = [part.scene.variables.texts, part.scene.variables.title, part.scene.variables.message, part.scene.narration].find((value) => typeof value === "string" && Boolean(value.trim()) && !/https?:\/\/|\b(?:api[_-]?key|authorization|secret|token|password)\s*[:=]/i.test(value));
711
+ if (copy) {
712
+ part = { ...part, scene: { ...part.scene, templateId: "media", variables: { texts: copy, mediaType: "gradient" } } };
713
+ lifecycle.reportWarning?.({ code: "provider_warning", category: "provider", message: "Some scenes use a simpler layout.", recoverable: true });
714
+ }
715
+ }
716
+ const { variables, clipped } = boundSceneVariables(
717
+ options.templates,
718
+ part.scene.templateId,
719
+ part.scene.variables
720
+ );
721
+ if (clipped.length === 0) {
722
+ yield part;
723
+ continue;
724
+ }
725
+ const templateId = part.scene.templateId;
726
+ invokeIsolated(options.onClipped && (() => options.onClipped(templateId, clipped)), void 0);
727
+ yield { ...part, scene: { ...part.scene, variables } };
728
+ } catch (cause) {
729
+ const error = cause instanceof Error ? cause : new Error(String(cause));
730
+ if (!getGenerationLifecycleSink(context)?.rejectPart?.(error)) throw error;
704
731
  }
705
- options.onClipped?.(part.scene.templateId, clipped);
706
- yield { ...part, scene: { ...part.scene, variables } };
707
732
  }
708
733
  };
709
734
  }
@@ -740,7 +765,8 @@ function createVideoHandler(options) {
740
765
  const planner = createBoundedVariablePlanner({
741
766
  planner: createTextDeltaVideoPlanner({ streamText, includeRawProviderData }),
742
767
  templates,
743
- onClipped: (templateId, fields) => void handlerOptions.onWarning?.(
768
+ onClipped: (templateId, fields) => invokeIsolated(
769
+ handlerOptions.onWarning,
744
770
  createClippedVariableWarning(templateId, fields)
745
771
  )
746
772
  });
@@ -768,7 +794,7 @@ function createVideoHandler(options) {
768
794
  // The ceiling is a spend policy, so it is reported to the application
769
795
  // that set it rather than to the browser, which cannot act on it.
770
796
  onBudgetReached: maxResolvedMedia === void 0 ? void 0 : () => {
771
- void handlerOptions.onWarning?.(createMediaBudgetWarning(maxResolvedMedia));
797
+ invokeIsolated(handlerOptions.onWarning, createMediaBudgetWarning(maxResolvedMedia));
772
798
  },
773
799
  // The same rule the scene validator applies, answered from the part
774
800
  // rather than from what the validator has already seen.
@@ -804,13 +830,15 @@ function createVideoHandler(options) {
804
830
 
805
831
  // src/server/video-chat-prompts.ts
806
832
  var RESPONSE_SCENES = 5;
807
- function createVideoChatResponseInstructions(fullAiVideo, openingAlreadyProvided = false) {
833
+ function createVideoChatResponseInstructions(fullAiVideo, openingAlreadyProvided = false, maxGeneratedVideos = 5) {
808
834
  return [
809
835
  "The input is a prompt from a user. Respond as a short, coherent video.",
810
836
  "Honor the requested purpose and tone. A story should feel like a story, a recommendation should be useful, an explanation should be clear, and a creative request should not be turned into a lecture.",
811
- fullAiVideo ? 'Your first output line must be exactly one host-consumed JSON object: {"type":"video-chat.opening","spokenHook":"6-9 words","mediaKeyword":"2-4 concrete words","firstShot":{"text":"2-7 words","narration":"10-16 spoken words","mediaKeyword":"2-8 concrete words"}}. This leading object is the one explicit exception to the standard plan-part list and is consumed before plan validation.' : openingAlreadyProvided ? "The host has already started the supplied OPENING ALREADY SPOKEN. Start directly with scene.add plan parts; do not emit a video-chat.opening object." : 'Your first output line must be exactly one host-consumed JSON object: {"type":"video-chat.opening","spokenHook":"6-9 words","mediaKeyword":"2-4 concrete words"}. This leading object is the one explicit exception to the standard plan-part list and is consumed before plan validation.',
837
+ fullAiVideo ? 'Your first output line must be exactly one host-consumed JSON object: {"type":"video-chat.opening","spokenHook":"6-9 words","mediaKeyword":"2-4 concrete words","fallbackKeyword":"2-4 broader concrete words","firstShot":{"text":"2-7 words","narration":"10-16 spoken words","mediaKeyword":"2-8 concrete words"}}. This leading object is the one explicit exception to the standard plan-part list and is consumed before plan validation.' : openingAlreadyProvided ? "The host has already started the supplied OPENING ALREADY SPOKEN. Start directly with scene.add plan parts; do not emit a video-chat.opening object." : 'Your first output line must be exactly one host-consumed JSON object: {"type":"video-chat.opening","spokenHook":"6-9 words","mediaKeyword":"2-4 concrete words","fallbackKeyword":"2-4 broader concrete words"}. This leading object is the one explicit exception to the standard plan-part list and is consumed before plan validation.',
838
+ "For the opening only, fallbackKeyword names a broader real-world subject that can provide atmosphere if the specific stock search misses. It must not pretend to demonstrate specialized instructions. For example, Minecraft castle can broaden to medieval castle for atmosphere. Do not broaden body-scene subjects into misleading demonstrations.",
812
839
  fullAiVideo && openingAlreadyProvided ? "Copy the supplied OPENING ALREADY SPOKEN exactly into spokenHook; do not replace or paraphrase it." : void 0,
813
840
  fullAiVideo ? "The host inserts firstShot as the first body scene and begins generating it immediately. After the opening object, emit exactly four additional scenes that continue it. Never emit, repeat, paraphrase, or replace firstShot as a scene." : `After the opening instruction above, use exactly ${RESPONSE_SCENES} scenes and build a progression that fits the request:`,
841
+ fullAiVideo ? `The response has a budget of ${maxGeneratedVideos} generated-video attempts, including the host-inserted firstShot. The first ${maxGeneratedVideos} media requests may generate footage; all later requests use stock footage. Plan later subjects that stock footage can honestly illustrate. Keep five total scenes even when the budget is smaller; do not request extra generated scenes.` : void 0,
814
842
  fullAiVideo ? void 0 : "1. Open directly on the strongest image, action, claim, or idea.",
815
843
  "2. Develop it with new information or movement.",
816
844
  "3. Deepen, complicate, or advance the response.",
@@ -996,6 +1024,7 @@ function readOpeningPlanLine(line) {
996
1024
  return {
997
1025
  line: boundedWords(parsed.spokenHook, 9, 300),
998
1026
  keyword: boundedWords(parsed.mediaKeyword, 4, 80),
1027
+ ...boundedWords(parsed.fallbackKeyword, 4, 80) ? { fallbackKeyword: boundedWords(parsed.fallbackKeyword, 4, 80) } : {},
999
1028
  ...firstShot ? { firstShot } : {}
1000
1029
  };
1001
1030
  } catch {
@@ -1144,7 +1173,8 @@ function streamVideoChatOpening(response, openingReady) {
1144
1173
  type: VIDEO_CHAT_OPENING_EVENT_TYPE,
1145
1174
  data: {
1146
1175
  line: opening.line,
1147
- ...opening.keyword ? { keyword: opening.keyword } : {}
1176
+ ...opening.keyword ? { keyword: opening.keyword } : {},
1177
+ ...opening.fallbackKeyword ? { fallbackKeyword: opening.fallbackKeyword } : {}
1148
1178
  }
1149
1179
  });
1150
1180
  sequence += 1;
@@ -1239,10 +1269,22 @@ function createVideoChatHandler(options) {
1239
1269
  allowedOrigins,
1240
1270
  allowCredentials,
1241
1271
  mediaConcurrency = 5,
1242
- ...videoOptions
1272
+ maxGeneratedVideos = 5
1243
1273
  } = options;
1274
+ const videoOptions = {
1275
+ templates: options.templates,
1276
+ streamText: options.streamText,
1277
+ includeRawProviderData: options.includeRawProviderData,
1278
+ heartbeatMs: options.heartbeatMs,
1279
+ onError: options.onError,
1280
+ onWarning: options.onWarning,
1281
+ onComplete: options.onComplete,
1282
+ invalidPartBehavior: options.invalidPartBehavior,
1283
+ requireCloser: options.requireCloser
1284
+ };
1244
1285
  if (!Number.isFinite(maxAudioBytes) || maxAudioBytes <= 0) throw new Error("maxAudioBytes must be positive");
1245
1286
  if (!Number.isFinite(maxBodyBytes) || maxBodyBytes <= 0) throw new Error("maxBodyBytes must be positive");
1287
+ if (!Number.isSafeInteger(maxGeneratedVideos) || maxGeneratedVideos < 0) throw new Error("maxGeneratedVideos must be a nonnegative safe integer");
1246
1288
  const capabilities2 = {
1247
1289
  templates: true,
1248
1290
  generatedSpeech: generateSpeech != null,
@@ -1258,20 +1300,68 @@ function createVideoChatHandler(options) {
1258
1300
  const responseHandler = (requestedMode, requestId, openingProvided, openingChannel) => {
1259
1301
  const mode = requestedMode !== "templates" && !generateVideo ? "templates" : requestedMode;
1260
1302
  const fullAiVideo = mode === "full";
1261
- const selectedResolver = fullAiVideo ? generateVideo : searchMedia;
1262
- const resolveSelected = selectedResolver ? (query, context) => selectedResolver(query, {
1263
- purpose: "response",
1264
- orientation: context.input.orientation ?? "landscape",
1265
- generatedLook: context.generatedLook,
1266
- signal: context.signal,
1267
- requestId: context.requestId,
1268
- scene: context.scene,
1269
- templateId: context.templateId,
1270
- preferredType: context.preferredType
1271
- }) : void 0;
1303
+ let lifecycle;
1304
+ let generatedAttempts = 0;
1305
+ const completedVideos = /* @__PURE__ */ new Map();
1306
+ const reusedUrls = /* @__PURE__ */ new Set();
1307
+ const resolveSelected = generateVideo || searchMedia ? async (query, context) => {
1308
+ const reuseKey = JSON.stringify([query, context.generatedLook ?? ""]);
1309
+ const remember = (media) => {
1310
+ if (media?.type === "video") completedVideos.set(reuseKey, media);
1311
+ return media;
1312
+ };
1313
+ const mediaContext = {
1314
+ purpose: "response",
1315
+ orientation: context.input.orientation ?? "landscape",
1316
+ generatedLook: context.generatedLook,
1317
+ signal: context.signal,
1318
+ requestId: context.requestId,
1319
+ scene: context.scene,
1320
+ templateId: context.templateId,
1321
+ preferredType: context.preferredType
1322
+ };
1323
+ const attempt = async (resolver, timeoutMs) => {
1324
+ context.signal.throwIfAborted();
1325
+ if (!resolver) return null;
1326
+ try {
1327
+ const result = sanitizeVideoChatMedia(await withDeadline(
1328
+ (signal) => resolver(query, { ...mediaContext, signal }),
1329
+ timeoutMs,
1330
+ context.signal
1331
+ ));
1332
+ context.signal.throwIfAborted();
1333
+ return result;
1334
+ } catch (cause) {
1335
+ context.signal.throwIfAborted();
1336
+ reportError(cause);
1337
+ return null;
1338
+ }
1339
+ };
1340
+ if (fullAiVideo) {
1341
+ const generated = generatedAttempts < maxGeneratedVideos ? (generatedAttempts++, await attempt(generateVideo, 15e3)) : null;
1342
+ if (generated) return remember(generated);
1343
+ lifecycle?.reportWarning?.({
1344
+ code: "provider_warning",
1345
+ category: "provider",
1346
+ message: "Some visuals were replaced so your response can continue.",
1347
+ recoverable: true
1348
+ });
1349
+ }
1350
+ const stock = await attempt(searchMedia, 3e3);
1351
+ if (stock) return remember(stock);
1352
+ if (fullAiVideo) {
1353
+ const previous = completedVideos.get(reuseKey);
1354
+ if (previous && !reusedUrls.has(previous.url)) {
1355
+ reusedUrls.add(previous.url);
1356
+ return previous;
1357
+ }
1358
+ }
1359
+ return null;
1360
+ } : void 0;
1272
1361
  const handler = createVideoHandler({
1273
1362
  ...videoOptions,
1274
1363
  streamText: (context) => {
1364
+ lifecycle = getGenerationLifecycleSink(context);
1275
1365
  try {
1276
1366
  return interceptOpeningPlan(videoOptions.streamText(context), {
1277
1367
  expectOpening: fullAiVideo || !openingProvided,
@@ -1290,9 +1380,8 @@ function createVideoChatHandler(options) {
1290
1380
  allowCredentials,
1291
1381
  maxBodyBytes,
1292
1382
  mediaConcurrency,
1293
- basePrompt: [createVideoChatResponseInstructions(fullAiVideo, openingProvided), instructions?.trim()].filter(Boolean).join("\n\nAPPLICATION GUIDANCE\n"),
1383
+ basePrompt: [createVideoChatResponseInstructions(fullAiVideo, openingProvided, maxGeneratedVideos), instructions?.trim()].filter(Boolean).join("\n\nAPPLICATION GUIDANCE\n"),
1294
1384
  narrate: true,
1295
- ...fullAiVideo ? { maxResolvedMedia: 5 } : {},
1296
1385
  resolveMedia: resolveSelected
1297
1386
  });
1298
1387
  return handler;
@@ -1343,15 +1432,14 @@ function createVideoChatHandler(options) {
1343
1432
  const mediaResolver = searchMedia;
1344
1433
  const resolve = async (query) => {
1345
1434
  if (!mediaResolver || !query) return null;
1346
- const signal = AbortSignal.any([request.signal, AbortSignal.timeout(1e4)]);
1347
1435
  try {
1348
- const raw = await Promise.resolve().then(() => mediaResolver(query, {
1436
+ const raw = await withDeadline((signal) => mediaResolver(query, {
1349
1437
  purpose: "welcome",
1350
1438
  orientation: "landscape",
1351
1439
  signal
1352
- }));
1440
+ }), 3e3, request.signal);
1353
1441
  const media = sanitizeVideoChatMedia(raw);
1354
- if (signal.aborted || raw != null && !media) failed = true;
1442
+ if (request.signal.aborted || raw != null && !media) failed = true;
1355
1443
  return media;
1356
1444
  } catch (cause) {
1357
1445
  failed = true;
@@ -1459,19 +1547,21 @@ function createVideoChatHandler(options) {
1459
1547
  try {
1460
1548
  if (action === "opening-media") {
1461
1549
  const value = record2(body, "request");
1462
- allowedKeys2(value, ["keyword", "orientation"], "request");
1550
+ allowedKeys2(value, ["keyword", "fallbackKeyword", "orientation"], "request");
1463
1551
  const keyword = boundedString2(value.keyword, "request.keyword", 80);
1552
+ const fallbackQuery = value.fallbackKeyword == null ? void 0 : boundedString2(value.fallbackKeyword, "request.fallbackKeyword", 80);
1464
1553
  const orientation = value.orientation ?? "landscape";
1465
1554
  if (orientation !== "portrait" && orientation !== "landscape") {
1466
1555
  throw new Error("request.orientation must be portrait or landscape");
1467
1556
  }
1468
1557
  if (!searchMedia) return Response.json({ media: null }, { headers });
1469
1558
  try {
1470
- const raw = await Promise.resolve().then(() => searchMedia(keyword, {
1559
+ const raw = await withDeadline((signal) => searchMedia(keyword, {
1471
1560
  purpose: "response",
1561
+ ...fallbackQuery ? { fallbackQuery } : {},
1472
1562
  orientation,
1473
- signal: request.signal
1474
- }));
1563
+ signal
1564
+ }), 3e3, request.signal);
1475
1565
  return Response.json({ media: sanitizeVideoChatMedia(raw) }, { headers });
1476
1566
  } catch (cause) {
1477
1567
  if (!request.signal.aborted) reportError(cause);
@@ -1502,7 +1592,7 @@ function createVideoChatHandler(options) {
1502
1592
  const prompt = boundedString2(value.prompt, "request.prompt");
1503
1593
  const lines = Array.isArray(value.lines) ? value.lines.slice(-8).map((line, index) => boundedString2(line, `request.lines[${index}]`, 2e3)) : [];
1504
1594
  try {
1505
- const text = await callText(
1595
+ const text = await withDeadline((signal) => callText(
1506
1596
  "suggestions",
1507
1597
  VIDEO_CHAT_SUGGESTIONS_PROMPT,
1508
1598
  `USER PROMPT: ${prompt}
@@ -1510,18 +1600,18 @@ function createVideoChatHandler(options) {
1510
1600
  VIDEO RESPONSE:
1511
1601
  ${lines.join("\n")}`,
1512
1602
  512,
1513
- request.signal
1514
- );
1603
+ signal
1604
+ ), 3e3, request.signal);
1515
1605
  const subjects = readSuggestionSubjects(text);
1516
1606
  const mediaResolver = searchMedia;
1517
1607
  const media = await Promise.all(subjects.map(async (subject) => {
1518
1608
  if (!mediaResolver || !subject.keyword) return null;
1519
1609
  try {
1520
- const raw = await Promise.resolve().then(() => mediaResolver(subject.keyword, {
1610
+ const raw = await withDeadline((signal) => mediaResolver(subject.keyword, {
1521
1611
  purpose: "suggestion",
1522
1612
  orientation: "landscape",
1523
- signal: request.signal
1524
- }));
1613
+ signal
1614
+ }), 3e3, request.signal);
1525
1615
  return sanitizeVideoChatMedia(raw);
1526
1616
  } catch (cause) {
1527
1617
  if (!request.signal.aborted) reportError(cause);
@@ -1545,7 +1635,7 @@ ${lines.join("\n")}`,
1545
1635
  allowedKeys2(value, ["text"], "request");
1546
1636
  const text = boundedString2(value.text, "request.text", 4e3);
1547
1637
  try {
1548
- const result = await generateSpeech({ text, signal: request.signal });
1638
+ const result = await withDeadline((signal) => generateSpeech({ text, signal }), 3e3, request.signal);
1549
1639
  return new Response(audioBody(result.audio), {
1550
1640
  headers: {
1551
1641
  ...Object.fromEntries(headers),
@@ -1569,6 +1659,5 @@ ${lines.join("\n")}`,
1569
1659
  }
1570
1660
  export {
1571
1661
  createServerTemplateRegistry,
1572
- createVideoChatHandler,
1573
- createVideoHandler
1662
+ createVideoChatHandler
1574
1663
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  BUILTIN_TEMPLATE_MANIFEST
3
- } from "./chunk-LKBZX7GV.js";
3
+ } from "./chunk-PP3MCPN4.js";
4
4
  export {
5
5
  BUILTIN_TEMPLATE_MANIFEST as builtinTemplates
6
6
  };
@@ -1,9 +1,9 @@
1
- import { I as InferTemplateJsonSchema, S as SceneTemplateProps, a as SceneTemplate } from './kit-8g46H2RZ.js';
2
- export { T as TemplateRegistry, c as createTemplateRegistry } from './kit-8g46H2RZ.js';
1
+ import { I as InferTemplateJsonSchema, S as SceneTemplateProps, a as SceneTemplate } from './kit-tQnCPMOM.js';
2
+ export { T as TemplateRegistry, c as createTemplateRegistry } from './kit-tQnCPMOM.js';
3
3
  import { ComponentType } from 'react';
4
4
  import { d as TemplateJsonSchema, S as SceneTemplateMetadata } from './catalog-types-BIhSpOWK.js';
5
5
  export { T as TemplateFamily, c as TemplateTimingMetadata, e as TemplateTransitionTiming } from './catalog-types-BIhSpOWK.js';
6
- import './types-2wHBqtg8.js';
6
+ import './types-CEh9eUFu.js';
7
7
 
8
8
  interface TemplateExample<Variables extends Record<string, unknown> = Record<string, unknown>> {
9
9
  name: string;
package/dist/test.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { e as VideoScene, b as VideoOrientation, V as VideoStyle, c as Video, d as VideoAudio, g as VideoInput } from './types-2wHBqtg8.js';
2
- import { V as VideoWarning, a as VideoFinishReason } from './events-B6qS1Lsb.js';
1
+ import { e as VideoScene, b as VideoOrientation, V as VideoStyle, c as Video, d as VideoAudio, g as VideoInput } from './types-CEh9eUFu.js';
2
+ import { V as VideoWarning, a as VideoFinishReason } from './events-BQ6z264n.js';
3
3
 
4
4
  type MockVideoStreamPart = {
5
5
  type: "scene.add";
package/dist/test.js CHANGED
@@ -233,9 +233,9 @@ async function* simulateVideoStream(parts, options = {}) {
233
233
  if (timeoutMs != null && (!Number.isFinite(timeoutMs) || timeoutMs < 0)) {
234
234
  throw new Error("Simulation timeoutMs must be a non-negative finite number");
235
235
  }
236
- const { createVideo } = await import("./compose-video-PD6LKKRK.js");
237
- const { createTextDeltaVideoPlanner } = await import("./text-stream-ROTNGSD4.js");
238
- const { BUILTIN_SERVER_TEMPLATE_KIT } = await import("./builtin-server-KB7FKF6A.js");
236
+ const { createVideo } = await import("./compose-video-2NFH4DZY.js");
237
+ const { createTextDeltaVideoPlanner } = await import("./text-stream-54LXABJW.js");
238
+ const { BUILTIN_SERVER_TEMPLATE_KIT } = await import("./builtin-server-CUYPIDE7.js");
239
239
  const { createTemplateSceneValidator } = await import("./validate-T7GBU2YF.js");
240
240
  const controller = new AbortController();
241
241
  const forwardAbort = () => controller.abort(options.signal?.reason);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createTextDeltaVideoPlanner
3
- } from "./chunk-LLPMVDJZ.js";
3
+ } from "./chunk-L7QGGKJ2.js";
4
4
  import "./chunk-E7CL7UPB.js";
5
5
  import "./chunk-MMUXVA47.js";
6
6
  import "./chunk-AOWSXU2K.js";
@@ -222,4 +222,4 @@ interface VideoSnapshotRetention {
222
222
  suppliedMediaUrls?: boolean;
223
223
  }
224
224
 
225
- export { type VideoStyle as V, type VideoCapabilities as a, type VideoOrientation as b, type Video as c, type VideoAudio as d, type VideoScene as e, VIDEO_PROTOCOL_VERSION as f, type VideoInput as g, type VideoBrandInput as h, type VideoBrand as i, type VideoTemplatePacing as j, type VideoBackground as k, type VideoKnowledgeMode as l, type VideoStyleOptions as m, type VideoSuppliedMedia as n, type VideoGenerationContext as o, type VideoPlanner as p, type VideoRequest as q, type VideoSceneValidator as r, type VideoSnapshotRetention as s, type VideoResumeCursor as t, type VideoSceneValidationContext as u };
225
+ export { type VideoStyle as V, type VideoCapabilities as a, type VideoOrientation as b, type Video as c, type VideoAudio as d, type VideoScene as e, VIDEO_PROTOCOL_VERSION as f, type VideoInput as g, type VideoBrandInput as h, type VideoBrand as i, type VideoTemplatePacing as j, type VideoBackground as k, type VideoStyleOptions as l, type VideoGenerationContext as m, type VideoPlanner as n, type VideoRequest as o, type VideoSceneValidator as p, type VideoSnapshotRetention as q, type VideoResumeCursor as r, type VideoSceneValidationContext as s };
@@ -43,4 +43,4 @@ interface VideoChatWelcomeOptions {
43
43
  prompts?: readonly VideoChatWelcomePrompt[];
44
44
  }
45
45
 
46
- export type { VideoChatWelcomeOptions as V, VideoChatCapabilities as a, VideoChatConversationTurn as b, VideoChatMode as c, VideoChatWelcomePrompt as d, VideoChatAskOptions as e, VideoChatMedia as f, VideoChatSuggestion as g, VideoChatWelcome as h };
46
+ export type { VideoChatMode as V, VideoChatAskOptions as a, VideoChatMedia as b, VideoChatSuggestion as c, VideoChatCapabilities as d, VideoChatWelcome as e, VideoChatWelcomeOptions as f, VideoChatConversationTurn as g, VideoChatWelcomePrompt as h };
@@ -22,14 +22,20 @@ npm run dev
22
22
  ```
23
23
 
24
24
  Init generates the thin application-owned shell around the SDK's complete
25
- `VideoChat` experience. The default uses packaged templates + browser voice.
26
- One text-provider key makes it answer; optional keys enable generated speech,
27
- stock media, transcription, and generated video without client changes.
25
+ `VideoChat` experience and runs doctor automatically. The default uses packaged
26
+ templates + browser voice and installs no optional provider packages.
27
+ `ANTHROPIC_API_KEY` is the only required key. Rerun init after an interrupted
28
+ installation.
28
29
 
29
30
  If doctor reports a missing key, add the named key to the ignored `.env.local`
30
31
  yourself. Never read or print a secret value, and never paste one into the chat.
31
32
  Doctor is the safe capability interface: it reports names and readiness only.
32
33
 
34
+ For requested upgrades, run `npx vanillasky providers add speech` for xAI speech
35
+ or `npx vanillasky providers add video` for FAL video and transcription. Have the
36
+ developer add `XAI_API_KEY` or `FAL_KEY` locally, then restart the server. Stock
37
+ media needs only `PEXELS_API_KEY`. The client does not change.
38
+
33
39
  ## Verify, do not merely scaffold
34
40
 
35
41
  The agent should leave the dev server running and open its reported localhost