@vanillaskyai/video 0.6.0 → 0.7.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 (130) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/PUBLIC-API.md +123 -9
  3. package/README.md +69 -115
  4. package/dist/{bg-confetti-SHQI7ATB.js → bg-confetti-HITCGLGD.js} +1 -1
  5. package/dist/{bg-emoji-XYFRA63Q.js → bg-emoji-VU4UZYZN.js} +1 -1
  6. package/dist/{bg-media-G4XOQEAS.js → bg-media-L34PDQXJ.js} +2 -2
  7. package/dist/{brand-message-XHONEDNW.js → brand-message-FUJ4STFO.js} +1 -1
  8. package/dist/{builtin-server-YHEZ2JRF.js → builtin-server-KB7FKF6A.js} +2 -2
  9. package/dist/{chart-bar-HRHD57ML.js → chart-bar-EGPHJATL.js} +2 -2
  10. package/dist/{chart-counter-KFPV44RB.js → chart-counter-5FFWQNZO.js} +2 -2
  11. package/dist/{chart-progress-ring-OR2VOIHV.js → chart-progress-ring-JMXQLMLC.js} +2 -2
  12. package/dist/{chunk-66MNRUCR.js → chunk-2PYO6VAC.js} +51 -3
  13. package/dist/{chunk-ZQQQAAKP.js → chunk-2XT4MZ76.js} +33 -2
  14. package/dist/{chunk-PNS52FL4.js → chunk-5SLENAJW.js} +19 -6
  15. package/dist/{chunk-EFL34TXF.js → chunk-AITKH6QT.js} +24 -8
  16. package/dist/{chunk-FZHMQFG3.js → chunk-GAIOHGNR.js} +5 -21
  17. package/dist/{chunk-3RV4YKB3.js → chunk-JNN3EYVP.js} +51 -26
  18. package/dist/{chunk-HVCPEAQF.js → chunk-LKBZX7GV.js} +9 -6
  19. package/dist/{chunk-G66Z5CWR.js → chunk-MMUXVA47.js} +27 -3
  20. package/dist/{chunk-7BGAU6C3.js → chunk-MXZSDGZQ.js} +2 -1
  21. package/dist/{chunk-3OU7HIEB.js → chunk-RE4IMWJR.js} +15 -31
  22. package/dist/{chunk-FVTMYS6U.js → chunk-RGF452LL.js} +1 -1
  23. package/dist/{chunk-34O5BY6X.js → chunk-RXHW4EP4.js} +21 -4
  24. package/dist/chunk-SKRGRKHY.js +142 -0
  25. package/dist/{chunk-TCEHK2BW.js → chunk-YJJC4N4D.js} +2 -1
  26. package/dist/cli.js +438 -30
  27. package/dist/{compose-video-JWIEDTEJ.js → compose-video-PD6LKKRK.js} +4 -4
  28. package/dist/{cta-logo-JJOGK4UC.js → cta-logo-YR7LYUR5.js} +1 -1
  29. package/dist/{cta-media-IQ3MAWEH.js → cta-media-DQJBKUO2.js} +2 -2
  30. package/dist/{events-BBP30j3c.d.ts → events-B6qS1Lsb.d.ts} +2 -2
  31. package/dist/{incoming-call-KXF67OT2.js → incoming-call-54KQ7O5A.js} +1 -1
  32. package/dist/index.d.ts +89 -3
  33. package/dist/index.js +12 -1
  34. package/dist/{infographic-before-after-2I7YZVDD.js → infographic-before-after-ROS52GOW.js} +1 -1
  35. package/dist/{infographic-feature-list-QDM4I7XE.js → infographic-feature-list-E7MGUDAN.js} +2 -2
  36. package/dist/{infographic-problem-solution-FMLJCPG6.js → infographic-problem-solution-3S6NGO5N.js} +2 -2
  37. package/dist/{infographic-stat-row-VFTIXS5F.js → infographic-stat-row-SYB7AB7H.js} +2 -2
  38. package/dist/{infographic-steps-L7W24Z35.js → infographic-steps-KCE6JWE2.js} +2 -2
  39. package/dist/{kit-DEG3fcaL.d.ts → kit-8g46H2RZ.d.ts} +1 -1
  40. package/dist/{prompt-input-F3CYU2XH.js → prompt-input-MHX4O42G.js} +1 -1
  41. package/dist/react.d.ts +194 -6
  42. package/dist/react.js +1849 -40
  43. package/dist/{reaction-2XCLOJOK.js → reaction-SLEGR3BE.js} +2 -2
  44. package/dist/server.d.ts +102 -4
  45. package/dist/server.js +972 -27
  46. package/dist/{showcase-code-KHSMIPG6.js → showcase-code-2HLAVKBM.js} +2 -2
  47. package/dist/{showcase-phone-M4CEANIW.js → showcase-phone-Q4HEO4LH.js} +2 -2
  48. package/dist/{showcase-terminal-26I3DYF7.js → showcase-terminal-DL45LUFG.js} +2 -2
  49. package/dist/{showcase-web-TY6CZV2Z.js → showcase-web-2QZO73FZ.js} +2 -2
  50. package/dist/{social-milestone-TF6MX7ET.js → social-milestone-GEFUQSBF.js} +1 -1
  51. package/dist/{social-notification-2RJDAZSF.js → social-notification-FJ5VHVLK.js} +1 -1
  52. package/dist/{social-review-stack-2ICKJQG7.js → social-review-stack-KXQCKFRP.js} +1 -1
  53. package/dist/{social-testimonial-X7OGADA3.js → social-testimonial-JZ7CHC7S.js} +1 -1
  54. package/dist/{social-tweet-TK7SH2F6.js → social-tweet-UCDXM25F.js} +1 -1
  55. package/dist/{system-prompt-4I6Z5HK3.js → system-prompt-RRXIWDDD.js} +5 -3
  56. package/dist/template-catalog.js +1 -1
  57. package/dist/templates.d.ts +3 -3
  58. package/dist/test.d.ts +2 -2
  59. package/dist/test.js +3 -3
  60. package/dist/{text-stream-J6EDDJP4.js → text-stream-FW4BLBAL.js} +2 -2
  61. package/dist/{types-BV9IqExh.d.ts → types-2wHBqtg8.d.ts} +29 -1
  62. package/dist/types-DrABlRa7.d.ts +46 -0
  63. package/docs/agent-integration.md +49 -25
  64. package/docs/architecture.md +37 -22
  65. package/docs/branding-and-personalization.md +1 -1
  66. package/docs/concepts.md +14 -0
  67. package/docs/custom-templates.md +9 -9
  68. package/docs/customization.md +41 -5
  69. package/docs/getting-started.md +81 -84
  70. package/docs/media-and-audio.md +103 -166
  71. package/docs/persistence.md +1 -1
  72. package/docs/production.md +81 -92
  73. package/docs/prompt-and-input.md +71 -161
  74. package/docs/provider-integration.md +91 -49
  75. package/docs/responsive-orientation.md +1 -1
  76. package/docs/security.md +6 -5
  77. package/docs/streaming-protocol.md +1 -1
  78. package/docs/testing.md +51 -43
  79. package/examples/custom-template/README.md +1 -1
  80. package/package.json +20 -22
  81. package/registry/items/barChart.json +6 -4
  82. package/registry/items/beforeAfter.json +1 -1
  83. package/registry/items/bigNumber.json +4 -3
  84. package/registry/items/brandMessage.json +3 -2
  85. package/registry/items/cardList.json +6 -4
  86. package/registry/items/codeEditor.json +4 -3
  87. package/registry/items/confetti.json +1 -1
  88. package/registry/items/ctaLogo.json +3 -2
  89. package/registry/items/ctaMedia.json +4 -3
  90. package/registry/items/emojiBurst.json +1 -1
  91. package/registry/items/incomingCall.json +3 -2
  92. package/registry/items/media.json +4 -3
  93. package/registry/items/milestone.json +3 -2
  94. package/registry/items/notification.json +3 -2
  95. package/registry/items/phoneMockup.json +4 -3
  96. package/registry/items/problemSolution.json +4 -3
  97. package/registry/items/progressRing.json +4 -3
  98. package/registry/items/promptInput.json +3 -2
  99. package/registry/items/reaction.json +2 -2
  100. package/registry/items/reviewStack.json +3 -2
  101. package/registry/items/steps.json +6 -4
  102. package/registry/items/terminal.json +5 -4
  103. package/registry/items/testimonial.json +3 -2
  104. package/registry/items/tripleStats.json +4 -3
  105. package/registry/items/tweet.json +3 -2
  106. package/registry/items/webMockup.json +4 -3
  107. package/starters/video-chat/.env.example +16 -0
  108. package/starters/video-chat/README.md +63 -0
  109. package/starters/video-chat/index.html +12 -0
  110. package/starters/video-chat/package.json +28 -0
  111. package/starters/video-chat/server.ts +152 -0
  112. package/starters/video-chat/src/main.tsx +8 -0
  113. package/starters/video-chat/stock.ts +139 -0
  114. package/starters/video-chat/tsconfig.json +20 -0
  115. package/starters/video-chat/vite.config.ts +81 -0
  116. package/styles/video-chat.css +713 -0
  117. package/docs/input-and-first-scene.md +0 -64
  118. package/docs/integrate-nextjs.md +0 -86
  119. package/docs/live-channels.md +0 -149
  120. package/docs/use-cases.md +0 -59
  121. package/examples/nextjs-quickstart/.env.example +0 -2
  122. package/examples/nextjs-quickstart/README.md +0 -27
  123. package/examples/nextjs-quickstart/next-env.d.ts +0 -4
  124. package/examples/nextjs-quickstart/package.json +0 -25
  125. package/examples/nextjs-quickstart/src/app/api/video/route.ts +0 -33
  126. package/examples/nextjs-quickstart/src/app/layout.tsx +0 -5
  127. package/examples/nextjs-quickstart/src/app/page.tsx +0 -31
  128. package/examples/nextjs-quickstart/tsconfig.json +0 -26
  129. package/examples/server-integrations/README.md +0 -20
  130. package/examples/server-integrations/src/ai-sdk-media.ts +0 -90
package/dist/server.js CHANGED
@@ -1,46 +1,50 @@
1
1
  import {
2
2
  createTextDeltaVideoPlanner
3
- } from "./chunk-34O5BY6X.js";
3
+ } from "./chunk-RXHW4EP4.js";
4
4
  import {
5
5
  createVideo
6
- } from "./chunk-3OU7HIEB.js";
6
+ } from "./chunk-RE4IMWJR.js";
7
7
  import "./chunk-E7CL7UPB.js";
8
8
  import {
9
9
  BUILTIN_SERVER_TEMPLATE_KIT,
10
10
  createServerTemplateRegistry,
11
11
  overlayServerTemplateRegistry
12
- } from "./chunk-FVTMYS6U.js";
12
+ } from "./chunk-RGF452LL.js";
13
13
  import {
14
14
  createTemplateSceneValidator
15
15
  } from "./chunk-LMG3GHLL.js";
16
16
  import {
17
+ decodeVideoSse,
17
18
  encodeVideoSseEvent,
19
+ sanitizeVideoChatMedia,
18
20
  videoSseHeaders
19
- } from "./chunk-ZQQQAAKP.js";
20
- import "./chunk-FZHMQFG3.js";
21
+ } from "./chunk-2XT4MZ76.js";
22
+ import "./chunk-GAIOHGNR.js";
21
23
  import {
22
24
  allowedKeys,
25
+ createClippedVariableWarning,
26
+ createMediaBudgetWarning,
23
27
  jsonValue,
24
28
  nonNegativeInteger,
25
29
  parseVideoEvent,
26
30
  record,
27
31
  safePublicDiagnostic,
28
32
  string
29
- } from "./chunk-G66Z5CWR.js";
33
+ } from "./chunk-MMUXVA47.js";
30
34
  import {
31
35
  MAX_RETAINED_MEDIA_URL_LENGTH
32
- } from "./chunk-66MNRUCR.js";
36
+ } from "./chunk-2PYO6VAC.js";
33
37
  import {
34
38
  VIDEO_PROTOCOL_VERSION
35
39
  } from "./chunk-AOWSXU2K.js";
36
40
  import {
37
41
  createTemplateSystemPrompt,
38
42
  getStandardMediaResolverContract
39
- } from "./chunk-EFL34TXF.js";
40
- import "./chunk-PNS52FL4.js";
41
- import "./chunk-3O7OMMMF.js";
42
- import "./chunk-3RV4YKB3.js";
43
+ } from "./chunk-AITKH6QT.js";
44
+ import "./chunk-5SLENAJW.js";
43
45
  import "./chunk-352ZZCET.js";
46
+ import "./chunk-JNN3EYVP.js";
47
+ import "./chunk-3O7OMMMF.js";
44
48
  import "./chunk-2E6T633S.js";
45
49
  import {
46
50
  validateBrandInput
@@ -48,6 +52,7 @@ import {
48
52
  import "./chunk-73NTSFFI.js";
49
53
 
50
54
  // src/server/request-validation.ts
55
+ var MAX_GENERATED_LOOK_LENGTH = 1e3;
51
56
  function boundedString(value, path) {
52
57
  const result = string(value, path);
53
58
  if (result.length > MAX_RETAINED_MEDIA_URL_LENGTH) {
@@ -184,7 +189,13 @@ function parseVideoRequest(value) {
184
189
  }
185
190
  if (input.style != null) {
186
191
  const visual = record(input.style, "request.input.style");
187
- allowedKeys(visual, ["density", "motion", "textArchetype", "backgroundEffect"], "request.input.style");
192
+ allowedKeys(visual, ["density", "motion", "textArchetype", "backgroundEffect", "generatedLook"], "request.input.style");
193
+ if (visual.generatedLook != null) {
194
+ const look = string(visual.generatedLook, "request.input.style.generatedLook");
195
+ if (look.length > MAX_GENERATED_LOOK_LENGTH) {
196
+ throw new Error(`request.input.style.generatedLook must be at most ${MAX_GENERATED_LOOK_LENGTH} characters`);
197
+ }
198
+ }
188
199
  if (visual.density != null) {
189
200
  enumValue(visual.density, ["airy", "normal", "packed"], "airy, normal, or packed", "request.input.style.density");
190
201
  }
@@ -472,6 +483,7 @@ async function resolveVariables(options) {
472
483
  if (!options.openingReady || !options.resolveMedia || !queryIsBounded(rawQuery)) {
473
484
  return fallbackVariables(variables);
474
485
  }
486
+ if (options.claimBudget && !options.claimBudget()) return fallbackVariables(variables);
475
487
  let resolved;
476
488
  try {
477
489
  resolved = await options.resolveMedia(rawQuery, {
@@ -480,6 +492,7 @@ async function resolveVariables(options) {
480
492
  scene: { ...options.scene, variables },
481
493
  templateId: options.templateId,
482
494
  preferredType: preferredType(variables.mediaType),
495
+ generatedLook: options.input.style?.generatedLook,
483
496
  signal: options.signal
484
497
  });
485
498
  } catch (error) {
@@ -530,7 +543,8 @@ async function resolvePartVariables(options) {
530
543
  signal: options.signal,
531
544
  resolveMedia: options.resolveMedia,
532
545
  approveUrl: options.approveUrl,
533
- openingReady: options.openingReady
546
+ openingReady: options.openingReady,
547
+ claimBudget: options.claimBudget
534
548
  };
535
549
  if (options.part.type === "scene.add") {
536
550
  const variables = await resolveVariables({
@@ -544,19 +558,152 @@ async function resolvePartVariables(options) {
544
558
  return options.part;
545
559
  }
546
560
  function createMediaResolvingPlanner(options) {
561
+ const limit = Math.max(1, Math.floor(options.mediaConcurrency ?? 1));
547
562
  return async function* resolveMediaPlan(context) {
563
+ let remaining = options.maxResolvedMedia;
564
+ let announced = false;
565
+ const claimBudget = remaining === void 0 ? void 0 : () => {
566
+ if (remaining > 0) {
567
+ remaining = remaining - 1;
568
+ return true;
569
+ }
570
+ if (!announced) {
571
+ announced = true;
572
+ options.onBudgetReached?.();
573
+ }
574
+ return false;
575
+ };
576
+ const resolveOne = (part, openingReady2) => resolvePartVariables({
577
+ part,
578
+ requestId: context.request.requestId,
579
+ templateId: part.type === "scene.add" ? part.scene.templateId : void 0,
580
+ input: context.request.input,
581
+ signal: context.signal,
582
+ templates: options.templates,
583
+ resolveMedia: options.resolveMedia,
584
+ approveUrl: options.approveUrl,
585
+ openingReady: openingReady2 ?? options.isOpeningReady(context.request.input),
586
+ claimBudget
587
+ });
588
+ if (limit === 1) {
589
+ for await (const part of options.planner(context)) yield await resolveOne(part);
590
+ return;
591
+ }
592
+ let openingReady = options.isOpeningReady(context.request.input);
593
+ const pending = [];
594
+ const iterator = options.planner(context)[Symbol.asyncIterator]();
595
+ let producerDone = false;
596
+ let producerError;
597
+ let consumerClosed = false;
598
+ let wakeConsumer;
599
+ let wakeProducer;
600
+ const notifyConsumer = () => {
601
+ wakeConsumer?.();
602
+ wakeConsumer = void 0;
603
+ };
604
+ const notifyProducer = () => {
605
+ wakeProducer?.();
606
+ wakeProducer = void 0;
607
+ };
608
+ const waitForItem = () => pending.length > 0 || producerDone ? Promise.resolve() : new Promise((resolve) => {
609
+ wakeConsumer = resolve;
610
+ });
611
+ const waitForSpace = () => pending.length < limit || consumerClosed ? Promise.resolve() : new Promise((resolve) => {
612
+ wakeProducer = resolve;
613
+ });
614
+ const producer = (async () => {
615
+ try {
616
+ while (!consumerClosed) {
617
+ await waitForSpace();
618
+ if (consumerClosed) break;
619
+ const next = await iterator.next();
620
+ if (next.done || consumerClosed) break;
621
+ const part = next.value;
622
+ pending.push(resolveOne(part, openingReady).then(
623
+ (resolved) => ({ part: resolved }),
624
+ (cause) => ({ cause })
625
+ ));
626
+ if (!openingReady && options.marksOpeningReady?.(part) === true) openingReady = true;
627
+ notifyConsumer();
628
+ }
629
+ } catch (cause) {
630
+ producerError = cause;
631
+ } finally {
632
+ producerDone = true;
633
+ notifyConsumer();
634
+ }
635
+ })();
636
+ try {
637
+ while (!producerDone || pending.length > 0) {
638
+ await waitForItem();
639
+ const task = pending[0];
640
+ if (!task) continue;
641
+ const result = await task;
642
+ pending.shift();
643
+ notifyProducer();
644
+ if ("cause" in result) throw result.cause;
645
+ yield result.part;
646
+ }
647
+ await producer;
648
+ if (producerError !== void 0) throw producerError;
649
+ } finally {
650
+ consumerClosed = true;
651
+ notifyProducer();
652
+ if (!producerDone) {
653
+ void Promise.resolve(iterator.return?.()).catch(() => void 0);
654
+ }
655
+ }
656
+ };
657
+ }
658
+
659
+ // src/server/bound-variables.ts
660
+ var ELLIPSIS = "\u2026";
661
+ function clipText(value, maxLength) {
662
+ if (value.length <= maxLength) return value;
663
+ const room = Math.max(1, maxLength - ELLIPSIS.length);
664
+ const clipped = value.slice(0, room);
665
+ const lastSpace = clipped.lastIndexOf(" ");
666
+ const body = lastSpace > room * 0.6 ? clipped.slice(0, lastSpace) : clipped;
667
+ return `${body.trimEnd()}${ELLIPSIS}`;
668
+ }
669
+ function boundValue(value, schema) {
670
+ if (!schema) return value;
671
+ if (typeof value === "string") {
672
+ return typeof schema.maxLength === "number" ? clipText(value, schema.maxLength) : value;
673
+ }
674
+ if (!Array.isArray(value)) return value;
675
+ const items = typeof schema.maxItems === "number" ? value.slice(0, schema.maxItems) : value;
676
+ return schema.items ? items.map((item) => boundValue(item, schema.items)) : items;
677
+ }
678
+ function boundSceneVariables(templates, templateId, variables) {
679
+ const properties = templates.getTemplateMetadata(templateId)?.schema?.properties;
680
+ if (!properties) return { variables, clipped: [] };
681
+ const clipped = [];
682
+ const bounded = Object.fromEntries(Object.entries(variables).map(([name, value]) => {
683
+ const next = boundValue(value, properties[name]);
684
+ if (next !== value && JSON.stringify(next) !== JSON.stringify(value)) clipped.push(name);
685
+ return [name, next];
686
+ }));
687
+ return clipped.length > 0 ? { variables: bounded, clipped } : { variables, clipped };
688
+ }
689
+ function createBoundedVariablePlanner(options) {
690
+ return async function* boundPlan(context) {
548
691
  for await (const part of options.planner(context)) {
549
- yield await resolvePartVariables({
550
- part,
551
- requestId: context.request.requestId,
552
- templateId: part.type === "scene.add" ? part.scene.templateId : void 0,
553
- input: context.request.input,
554
- signal: context.signal,
555
- templates: options.templates,
556
- resolveMedia: options.resolveMedia,
557
- approveUrl: options.approveUrl,
558
- openingReady: options.isOpeningReady(context.request.input)
559
- });
692
+ if (part.type !== "scene.add") {
693
+ yield part;
694
+ continue;
695
+ }
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;
704
+ }
705
+ options.onClipped?.(part.scene.templateId, clipped);
706
+ yield { ...part, scene: { ...part.scene, variables } };
560
707
  }
561
708
  };
562
709
  }
@@ -576,6 +723,9 @@ function createVideoHandler(options) {
576
723
  basePrompt,
577
724
  allowMediaUrl,
578
725
  resolveMedia,
726
+ mediaConcurrency,
727
+ maxResolvedMedia,
728
+ narrate,
579
729
  requireCloser = true,
580
730
  ...handlerOptions
581
731
  } = options;
@@ -587,7 +737,13 @@ function createVideoHandler(options) {
587
737
  approved.add(url);
588
738
  approvedMediaUrls.set(input, approved);
589
739
  };
590
- const planner = createTextDeltaVideoPlanner({ streamText, includeRawProviderData });
740
+ const planner = createBoundedVariablePlanner({
741
+ planner: createTextDeltaVideoPlanner({ streamText, includeRawProviderData }),
742
+ templates,
743
+ onClipped: (templateId, fields) => void handlerOptions.onWarning?.(
744
+ createClippedVariableWarning(templateId, fields)
745
+ )
746
+ });
591
747
  const validateTemplateScene = createTemplateSceneValidator({
592
748
  kit: templates,
593
749
  allowMediaUrl: (url, context) => approvedMediaUrls.get(context.input)?.has(url) === true || allowMediaUrl?.(url, context) === true
@@ -600,7 +756,23 @@ function createVideoHandler(options) {
600
756
  templates,
601
757
  resolveMedia,
602
758
  approveUrl,
603
- isOpeningReady: (input) => openingReadyInputs.has(input)
759
+ // An opening that was never asked for is ready from the start. The gate
760
+ // exists so the runtime's opening card is on screen before anything waits
761
+ // on a provider; `opening: false` says there is no card, the host owns
762
+ // the wait, and the first scene is then an ordinary scene - which is what
763
+ // a filmed answer needs, since otherwise its first beat can never be
764
+ // filmed however much the host is willing to spend.
765
+ isOpeningReady: (input) => input.opening === false || openingReadyInputs.has(input),
766
+ mediaConcurrency,
767
+ maxResolvedMedia,
768
+ // The ceiling is a spend policy, so it is reported to the application
769
+ // that set it rather than to the browser, which cannot act on it.
770
+ onBudgetReached: maxResolvedMedia === void 0 ? void 0 : () => {
771
+ void handlerOptions.onWarning?.(createMediaBudgetWarning(maxResolvedMedia));
772
+ },
773
+ // The same rule the scene validator applies, answered from the part
774
+ // rather than from what the validator has already seen.
775
+ marksOpeningReady: (part) => part.type === "scene.add" && templates.getTemplateMetadata(part.scene.templateId)?.jobs?.includes("ask") !== true
604
776
  }),
605
777
  systemPrompt: ({ request, capabilities: capabilities2 }) => {
606
778
  const selectedIds = capabilities2?.templates == null ? void 0 : new Set(capabilities2.templates);
@@ -609,7 +781,13 @@ function createVideoHandler(options) {
609
781
  kit: { listTemplateMetadata: () => selectedTemplates },
610
782
  basePrompt,
611
783
  knowledgeMode: request.input.knowledgeMode,
612
- mediaResolverAvailable: resolveMedia != null
784
+ mediaResolverAvailable: resolveMedia != null,
785
+ suppliedMediaAvailable: (request.input.suppliedMedia?.length ?? 0) > 0,
786
+ // The same condition the resolver gate uses. The two have to agree:
787
+ // letting the first scene resolve media while still telling the
788
+ // planner not to put any there films nothing.
789
+ mediaOnFirstScene: request.input.opening === false && resolveMedia != null,
790
+ narrate
613
791
  });
614
792
  },
615
793
  supportedCapabilities: templates.capabilities,
@@ -623,7 +801,774 @@ function createVideoHandler(options) {
623
801
  getTemplatePacing: (templateId) => templates.getTemplateMetadata(templateId)
624
802
  });
625
803
  }
804
+
805
+ // src/server/video-chat-prompts.ts
806
+ var RESPONSE_SCENES = 5;
807
+ function createVideoChatResponseInstructions(fullAiVideo, openingAlreadyProvided = false) {
808
+ return [
809
+ "The input is a prompt from a user. Respond as a short, coherent video.",
810
+ "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.',
812
+ fullAiVideo && openingAlreadyProvided ? "Copy the supplied OPENING ALREADY SPOKEN exactly into spokenHook; do not replace or paraphrase it." : void 0,
813
+ 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:`,
814
+ fullAiVideo ? void 0 : "1. Open directly on the strongest image, action, claim, or idea.",
815
+ "2. Develop it with new information or movement.",
816
+ "3. Deepen, complicate, or advance the response.",
817
+ "4. Deliver the turn, consequence, recommendation, or emotional peak.",
818
+ "5. Close on the payoff or the one thing worth carrying forward, never a recap.",
819
+ "No scene may repeat another scene's job. If a scene could be deleted without weakening the response, replace it.",
820
+ "The prompt is already on screen, so never spend the opening scene restating it.",
821
+ "If the input includes an OPENING ALREADY SPOKEN transcript, continue directly from that exact hook. Never repeat, paraphrase, contradict, or render it as the first scene's copy.",
822
+ "For factual requests, distinguish supplied facts from stable general knowledge and never invent statistics, quotations, sources, or personal experience.",
823
+ "For creative requests, create the requested material directly rather than explaining how one might create it.",
824
+ fullAiVideo ? "Use the media template for every scene you emit. Every scene must set mediaType to video and carry a mediaKeyword: a concrete, filmable subject, a real object doing a real thing, never a diagram or an abstraction. Never use mediaType gradient and never omit mediaKeyword - a scene without one has nothing to film. If a beat seems too abstract to film, film the closest real thing that shows it." : "Choose the template that fits each beat honestly - a figure belongs in a number scene, an ordered process in steps, a comparison in a chart - rather than repeating one shape. Give any scene that would be stronger over real footage a mediaKeyword: a concrete, filmable subject, a real object doing a real thing, never a diagram or an abstraction. Use the media template for the one or two beats most worth watching happen.",
825
+ fullAiVideo ? 'Mark the final scene placement:"closer". Use the media template for it, as for every other beat.' : 'Mark the final scene placement:"closer" and use either the milestone or the media template for it. Those are the only two here that may close, so the response must end on one.',
826
+ "Every scene must carry a timing object, even an empty one. A scene without it fails the whole response.",
827
+ "Never mention the video, the scenes, or yourself."
828
+ ].filter((line) => line != null).join("\n");
829
+ }
830
+ var VIDEO_CHAT_NARRATION_PROMPT = [
831
+ "You narrate a short video response, one scene at a time.",
832
+ "You are given the scene about to be shown, and the lines already said before it.",
833
+ "",
834
+ "Return only the line to say over this scene. No JSON, no quotes, no preamble.",
835
+ "One sentence, 10-16 words, plain spoken English.",
836
+ "Say only what this scene contributes. Preserve the requested form: narrate a story as a story, give recommendations directly, and explain only when the user asked for an explanation.",
837
+ "Never jump ahead to a later scene or repeat a line already said.",
838
+ "Never read the on-screen text back word for word - the viewer can already see it.",
839
+ "Continue naturally from the lines before it. Never mention scenes, videos or slides."
840
+ ].join("\n");
841
+ var VIDEO_CHAT_SUGGESTIONS_PROMPT = [
842
+ "You suggest what a user might prompt next after receiving a video response.",
843
+ 'Return JSON only: {"suggestions": [{"prompt": string, "keyword": string}]} with exactly four entries.',
844
+ "prompt: short and specific to what was created or discussed. Suggestions may continue, remix, deepen, compare, or apply it.",
845
+ "keyword: two to four words naming something filmable that stands for the prompt, for stock footage search. Concrete subjects only - no abstractions, no text on screen."
846
+ ].join("\n");
847
+ function createNarrationUserPrompt(prompt, scene, earlier) {
848
+ return [
849
+ `USER PROMPT: ${prompt}`,
850
+ earlier.length > 0 ? `
851
+ SAID SO FAR:
852
+ ${earlier.map((line) => `- ${line}`).join("\n")}` : "",
853
+ `
854
+ THIS SCENE: [${scene.templateId}] ${JSON.stringify(scene.variables).slice(0, 4e3)}`
855
+ ].join("\n");
856
+ }
857
+
858
+ // src/server/create-video-chat-handler.ts
859
+ var DEFAULT_MAX_BODY_BYTES2 = 1024 * 1024;
860
+ var DEFAULT_MAX_AUDIO_BYTES = 8 * 1024 * 1024;
861
+ var MAX_PROMPT_CHARACTERS = 8e3;
862
+ var MAX_CONVERSATION_TURNS = 12;
863
+ var MAX_CONVERSATION_RESPONSE_CHARACTERS = 8e3;
864
+ var VIDEO_CHAT_OPENING_PLAN_TYPE = "video-chat.opening";
865
+ var VIDEO_CHAT_OPENING_EVENT_TYPE = "data.video-chat-opening";
866
+ var PROVIDER_CODE_FENCE = /^```(?:json|ndjson)?$/i;
867
+ var DEFAULT_WELCOME_PROMPTS = [
868
+ {
869
+ prompt: "Why does the Moon always show one face?",
870
+ opening: "The Moon turns, perfectly matching its orbit.",
871
+ mediaQuery: "full moon night sky"
872
+ },
873
+ {
874
+ prompt: "Tell me a tiny story about a robot growing a garden on Mars",
875
+ opening: "One patient robot is about to make Mars bloom.",
876
+ mediaQuery: "robot garden mars"
877
+ },
878
+ {
879
+ prompt: "Recommend a perfect rainy afternoon in Amsterdam",
880
+ opening: "Rain makes Amsterdam's best afternoons feel even warmer.",
881
+ mediaQuery: "Amsterdam rain cafe"
882
+ },
883
+ {
884
+ prompt: "Pitch a playful ad for a coffee mug that never spills",
885
+ opening: "This mug makes gravity look completely optional.",
886
+ mediaQuery: "coffee mug desk"
887
+ }
888
+ ];
889
+ function jsonError2(status, code, message, headers) {
890
+ return Response.json({ error: { code, message } }, { status, headers });
891
+ }
892
+ function allowedKeys2(value, allowed, label) {
893
+ const permitted = new Set(allowed);
894
+ const unexpected = Object.keys(value).find((key) => !permitted.has(key));
895
+ if (unexpected) throw new Error(`${label}.${unexpected} is not supported`);
896
+ }
897
+ function record2(value, label) {
898
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
899
+ throw new Error(`${label} must be an object`);
900
+ }
901
+ return value;
902
+ }
903
+ function boundedString2(value, label, maximum = MAX_PROMPT_CHARACTERS) {
904
+ if (typeof value !== "string" || !value.trim()) throw new Error(`${label} must be a non-empty string`);
905
+ const trimmed = value.trim();
906
+ if ([...trimmed].length > maximum) throw new Error(`${label} is too long`);
907
+ return trimmed;
908
+ }
909
+ function parseResponseRequest(value) {
910
+ const body = record2(value, "request");
911
+ allowedKeys2(body, ["prompt", "opening", "mode", "orientation", "conversation", "brand", "style"], "request");
912
+ const mode = body.mode ?? "templates";
913
+ if (mode !== "templates" && mode !== "full") {
914
+ throw new Error("request.mode must be templates or full");
915
+ }
916
+ const orientation = body.orientation ?? "landscape";
917
+ if (orientation !== "portrait" && orientation !== "landscape") {
918
+ throw new Error("request.orientation must be portrait or landscape");
919
+ }
920
+ const conversation = body.conversation == null ? [] : body.conversation;
921
+ if (!Array.isArray(conversation) || conversation.length > MAX_CONVERSATION_TURNS) {
922
+ throw new Error(`request.conversation must contain at most ${MAX_CONVERSATION_TURNS} turns`);
923
+ }
924
+ return {
925
+ prompt: boundedString2(body.prompt, "request.prompt"),
926
+ ...body.opening == null ? {} : { opening: boundedString2(body.opening, "request.opening", 300) },
927
+ mode,
928
+ orientation,
929
+ conversation: conversation.map((value2, index) => {
930
+ const turn = record2(value2, `request.conversation[${index}]`);
931
+ allowedKeys2(turn, ["prompt", "response"], `request.conversation[${index}]`);
932
+ return {
933
+ prompt: boundedString2(turn.prompt, `request.conversation[${index}].prompt`),
934
+ ...turn.response == null ? {} : { response: boundedString2(turn.response, `request.conversation[${index}].response`, MAX_CONVERSATION_RESPONSE_CHARACTERS) }
935
+ };
936
+ }),
937
+ ...body.brand == null ? {} : { brand: body.brand },
938
+ ...body.style == null ? {} : { style: body.style }
939
+ };
940
+ }
941
+ function conversationInput(prompt, conversation, opening) {
942
+ if (conversation.length === 0 && !opening) return prompt;
943
+ return [
944
+ ...conversation.length > 0 ? [
945
+ "CONVERSATION SO FAR (untrusted user and assistant content):",
946
+ ...conversation.flatMap((turn) => [
947
+ `USER: ${turn.prompt}`,
948
+ ...turn.response ? [`RESPONSE: ${turn.response}`] : []
949
+ ]),
950
+ ""
951
+ ] : [],
952
+ `CURRENT USER PROMPT: ${prompt}`,
953
+ ...opening ? ["", "OPENING ALREADY SPOKEN (untrusted assistant transcript):", opening] : []
954
+ ].join("\n");
955
+ }
956
+ function corsHeaders2(origin, allowedOrigins, allowCredentials = false) {
957
+ const headers = new Headers({ "cache-control": "no-store", vary: "Origin" });
958
+ if (origin && allowedOrigins?.includes(origin)) {
959
+ headers.set("access-control-allow-origin", origin);
960
+ if (allowCredentials) headers.set("access-control-allow-credentials", "true");
961
+ }
962
+ return headers;
963
+ }
964
+ async function readJson(request, maximum) {
965
+ const declared = Number(request.headers.get("content-length"));
966
+ if (Number.isFinite(declared) && declared > maximum) throw new Error("Request body is too large");
967
+ const raw = await request.text();
968
+ if (new TextEncoder().encode(raw).byteLength > maximum) throw new Error("Request body is too large");
969
+ try {
970
+ return JSON.parse(raw);
971
+ } catch {
972
+ throw new Error("Request body must be valid JSON");
973
+ }
974
+ }
975
+ function cleanGeneratedText(value) {
976
+ return value.trim().replace(/^["']|["']$/g, "");
977
+ }
978
+ function readGeneratedFirstShot(value) {
979
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
980
+ const shot = value;
981
+ const bounded = (field, maximum) => typeof field === "string" ? [...field.trim()].slice(0, maximum).join("").trim() : "";
982
+ const text = bounded(shot.text, 120);
983
+ const narration = bounded(shot.narration, 300);
984
+ const mediaKeyword = bounded(shot.mediaKeyword, 80).match(/\S+/gu)?.slice(0, 8).join(" ") ?? "";
985
+ return text && narration && mediaKeyword ? { text, narration, mediaKeyword } : void 0;
986
+ }
987
+ function boundedWords(value, maximum, characters) {
988
+ if (typeof value !== "string") return "";
989
+ return value.trim().match(/\S+/gu)?.slice(0, maximum).join(" ").slice(0, characters).trim() ?? "";
990
+ }
991
+ function readOpeningPlanLine(line) {
992
+ try {
993
+ const parsed = JSON.parse(line);
994
+ if (!parsed || parsed.type !== VIDEO_CHAT_OPENING_PLAN_TYPE) return void 0;
995
+ const firstShot = readGeneratedFirstShot(parsed.firstShot);
996
+ return {
997
+ line: boundedWords(parsed.spokenHook, 9, 300),
998
+ keyword: boundedWords(parsed.mediaKeyword, 4, 80),
999
+ ...firstShot ? { firstShot } : {}
1000
+ };
1001
+ } catch {
1002
+ return void 0;
1003
+ }
1004
+ }
1005
+ function reservedFirstScene(requestId, firstShot) {
1006
+ return {
1007
+ type: "scene.add",
1008
+ scene: {
1009
+ id: `${requestId}-first-shot`,
1010
+ templateId: "media",
1011
+ variables: {
1012
+ texts: firstShot.text,
1013
+ mediaType: "video",
1014
+ mediaKeyword: firstShot.mediaKeyword
1015
+ },
1016
+ timing: { fixedDuration: 5 },
1017
+ narration: firstShot.narration
1018
+ }
1019
+ };
1020
+ }
1021
+ function replaceTextStream(source, textStream) {
1022
+ const enriched = typeof source === "object" && source != null && "textStream" in source ? source : void 0;
1023
+ if (!enriched) return textStream;
1024
+ const wrapper = { textStream };
1025
+ return new Proxy(wrapper, {
1026
+ get(target, property, receiver) {
1027
+ if (property === "textStream") return Reflect.get(target, property, receiver);
1028
+ return Reflect.get(enriched, property, enriched);
1029
+ }
1030
+ });
1031
+ }
1032
+ function createOpeningChannel(initial) {
1033
+ if (initial) return { ready: Promise.resolve(initial), publish: () => void 0 };
1034
+ let published = false;
1035
+ let resolve;
1036
+ const ready = new Promise((settle) => {
1037
+ resolve = settle;
1038
+ });
1039
+ return {
1040
+ ready,
1041
+ publish(value) {
1042
+ if (published) return;
1043
+ published = true;
1044
+ resolve(value);
1045
+ }
1046
+ };
1047
+ }
1048
+ function interceptOpeningPlan(source, options) {
1049
+ const enriched = typeof source === "object" && source != null && "textStream" in source ? source : void 0;
1050
+ const upstream = enriched?.textStream ?? source;
1051
+ const textStream = (async function* () {
1052
+ let buffer = "";
1053
+ let decided = !options.expectOpening;
1054
+ try {
1055
+ for await (const delta of upstream) {
1056
+ if (typeof delta !== "string") throw new Error("The LLM adapter returned a non-text delta");
1057
+ buffer += delta;
1058
+ let newline = buffer.indexOf("\n");
1059
+ while (newline >= 0) {
1060
+ const rawLine = buffer.slice(0, newline);
1061
+ buffer = buffer.slice(newline + 1);
1062
+ const line = rawLine.trim();
1063
+ if (!decided && line && !PROVIDER_CODE_FENCE.test(line)) {
1064
+ const opening = readOpeningPlanLine(line);
1065
+ decided = true;
1066
+ if (opening) {
1067
+ if (!options.openingProvided) options.publish(opening.line ? opening : void 0);
1068
+ if (options.fullAiVideo && opening.firstShot) {
1069
+ yield `${JSON.stringify(reservedFirstScene(options.requestId, opening.firstShot))}
1070
+ `;
1071
+ }
1072
+ newline = buffer.indexOf("\n");
1073
+ continue;
1074
+ }
1075
+ options.publish(void 0);
1076
+ }
1077
+ yield `${rawLine}
1078
+ `;
1079
+ newline = buffer.indexOf("\n");
1080
+ }
1081
+ }
1082
+ if (buffer) {
1083
+ const line = buffer.trim();
1084
+ if (!decided && line && !PROVIDER_CODE_FENCE.test(line)) {
1085
+ const opening = readOpeningPlanLine(line);
1086
+ decided = true;
1087
+ if (opening) {
1088
+ if (!options.openingProvided) options.publish(opening.line ? opening : void 0);
1089
+ if (options.fullAiVideo && opening.firstShot) {
1090
+ yield `${JSON.stringify(reservedFirstScene(options.requestId, opening.firstShot))}
1091
+ `;
1092
+ }
1093
+ } else {
1094
+ options.publish(void 0);
1095
+ yield buffer;
1096
+ }
1097
+ } else {
1098
+ yield buffer;
1099
+ }
1100
+ }
1101
+ } finally {
1102
+ options.publish(void 0);
1103
+ }
1104
+ })();
1105
+ return replaceTextStream(source, textStream);
1106
+ }
1107
+ function resequenceEvent(event, sequence) {
1108
+ return {
1109
+ ...event,
1110
+ sequence,
1111
+ eventId: `${event.runId}:${sequence}`
1112
+ };
1113
+ }
1114
+ function streamVideoChatOpening(response, openingReady) {
1115
+ if (!response.body || !response.headers.get("content-type")?.includes("text/event-stream")) return response;
1116
+ const events = decodeVideoSse(response.body)[Symbol.asyncIterator]();
1117
+ const encoded = (async function* () {
1118
+ try {
1119
+ const first = await events.next();
1120
+ if (first.done) return;
1121
+ let sequence = 0;
1122
+ const started = first.value.type === "response.start" ? {
1123
+ ...first.value,
1124
+ data: {
1125
+ ...first.value.data,
1126
+ capabilities: {
1127
+ ...first.value.data.capabilities,
1128
+ extensions: Array.from(/* @__PURE__ */ new Set([
1129
+ ...first.value.data.capabilities?.extensions ?? [],
1130
+ VIDEO_CHAT_OPENING_EVENT_TYPE
1131
+ ]))
1132
+ }
1133
+ }
1134
+ } : first.value;
1135
+ yield encodeVideoSseEvent(resequenceEvent(started, sequence++));
1136
+ const nextEvent = events.next();
1137
+ const opening = await openingReady;
1138
+ if (opening?.line) {
1139
+ yield encodeVideoSseEvent({
1140
+ protocolVersion: first.value.protocolVersion,
1141
+ runId: first.value.runId,
1142
+ sequence,
1143
+ eventId: `${first.value.runId}:${sequence}`,
1144
+ type: VIDEO_CHAT_OPENING_EVENT_TYPE,
1145
+ data: {
1146
+ line: opening.line,
1147
+ ...opening.keyword ? { keyword: opening.keyword } : {}
1148
+ }
1149
+ });
1150
+ sequence += 1;
1151
+ }
1152
+ let next = await nextEvent;
1153
+ while (!next.done) {
1154
+ yield encodeVideoSseEvent(resequenceEvent(next.value, sequence++));
1155
+ next = await events.next();
1156
+ }
1157
+ } finally {
1158
+ await events.return?.(void 0);
1159
+ }
1160
+ })();
1161
+ const encoder = new TextEncoder();
1162
+ const iterator = encoded[Symbol.asyncIterator]();
1163
+ let completed = false;
1164
+ const body = new ReadableStream({
1165
+ async pull(controller) {
1166
+ try {
1167
+ const next = await iterator.next();
1168
+ if (next.done) {
1169
+ if (!completed) controller.enqueue(encoder.encode("data: [DONE]\n\n"));
1170
+ completed = true;
1171
+ controller.close();
1172
+ return;
1173
+ }
1174
+ controller.enqueue(encoder.encode(next.value));
1175
+ } catch (cause) {
1176
+ completed = true;
1177
+ controller.error(cause);
1178
+ }
1179
+ },
1180
+ async cancel() {
1181
+ completed = true;
1182
+ await iterator.return?.();
1183
+ }
1184
+ });
1185
+ return new Response(body, {
1186
+ status: response.status,
1187
+ statusText: response.statusText,
1188
+ headers: response.headers
1189
+ });
1190
+ }
1191
+ function readSuggestionSubjects(text) {
1192
+ const opening = text.indexOf("{");
1193
+ const closing = text.lastIndexOf("}");
1194
+ if (opening < 0 || closing <= opening) return [];
1195
+ const parsed = JSON.parse(text.slice(opening, closing + 1));
1196
+ return (Array.isArray(parsed.suggestions) ? parsed.suggestions : []).flatMap((entry) => {
1197
+ const item = entry;
1198
+ const prompt = typeof item.prompt === "string" ? item.prompt.trim() : "";
1199
+ const keyword = typeof item.keyword === "string" ? item.keyword.trim() : "";
1200
+ return prompt ? [{ prompt: prompt.slice(0, 300), keyword: keyword.slice(0, 80) }] : [];
1201
+ }).slice(0, 4);
1202
+ }
1203
+ function audioBody(value) {
1204
+ const source = value instanceof Uint8Array ? value : new Uint8Array(value);
1205
+ return Uint8Array.from(source).buffer;
1206
+ }
1207
+ var VideoChatProviderError = class extends Error {
1208
+ constructor(task) {
1209
+ super(`Video chat ${task} provider failed`);
1210
+ this.task = task;
1211
+ }
1212
+ task;
1213
+ };
1214
+ function createVideoChatHandler(options) {
1215
+ if (!options || typeof options.streamText !== "function" || typeof options.generateText !== "function") {
1216
+ throw new Error("createVideoChatHandler requires streamText and generateText");
1217
+ }
1218
+ if (options.authorize !== "none" && typeof options.authorize !== "function") {
1219
+ throw new Error('createVideoChatHandler requires authorize or authorize: "none"');
1220
+ }
1221
+ const reportError = (cause) => {
1222
+ const error = cause instanceof Error ? cause : new Error(String(cause));
1223
+ try {
1224
+ void Promise.resolve(options.onError?.(error)).catch(() => void 0);
1225
+ } catch {
1226
+ }
1227
+ };
1228
+ const {
1229
+ authorize,
1230
+ generateText,
1231
+ generateSpeech,
1232
+ transcribe,
1233
+ searchMedia,
1234
+ generateVideo,
1235
+ instructions,
1236
+ welcome: welcomeOptions,
1237
+ maxAudioBytes = DEFAULT_MAX_AUDIO_BYTES,
1238
+ maxBodyBytes = DEFAULT_MAX_BODY_BYTES2,
1239
+ allowedOrigins,
1240
+ allowCredentials,
1241
+ mediaConcurrency = 5,
1242
+ ...videoOptions
1243
+ } = options;
1244
+ if (!Number.isFinite(maxAudioBytes) || maxAudioBytes <= 0) throw new Error("maxAudioBytes must be positive");
1245
+ if (!Number.isFinite(maxBodyBytes) || maxBodyBytes <= 0) throw new Error("maxBodyBytes must be positive");
1246
+ const capabilities2 = {
1247
+ templates: true,
1248
+ generatedSpeech: generateSpeech != null,
1249
+ generatedVideo: generateVideo != null,
1250
+ stockMedia: searchMedia != null,
1251
+ transcription: transcribe != null,
1252
+ modes: generateVideo ? ["templates", "full"] : ["templates"]
1253
+ };
1254
+ const welcomePrompts = (welcomeOptions?.prompts ?? DEFAULT_WELCOME_PROMPTS).slice(0, 4);
1255
+ const heroQuery = welcomeOptions?.heroQuery ?? "underwater ocean sunlight";
1256
+ let welcomeResponse;
1257
+ let requestSequence = 0;
1258
+ const responseHandler = (requestedMode, requestId, openingProvided, openingChannel) => {
1259
+ const mode = requestedMode !== "templates" && !generateVideo ? "templates" : requestedMode;
1260
+ 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;
1272
+ const handler = createVideoHandler({
1273
+ ...videoOptions,
1274
+ streamText: (context) => {
1275
+ try {
1276
+ return interceptOpeningPlan(videoOptions.streamText(context), {
1277
+ expectOpening: fullAiVideo || !openingProvided,
1278
+ openingProvided,
1279
+ requestId,
1280
+ fullAiVideo,
1281
+ publish: openingChannel.publish
1282
+ });
1283
+ } catch (cause) {
1284
+ openingChannel.publish(void 0);
1285
+ throw cause;
1286
+ }
1287
+ },
1288
+ authorize: "none",
1289
+ allowedOrigins,
1290
+ allowCredentials,
1291
+ maxBodyBytes,
1292
+ mediaConcurrency,
1293
+ basePrompt: [createVideoChatResponseInstructions(fullAiVideo, openingProvided), instructions?.trim()].filter(Boolean).join("\n\nAPPLICATION GUIDANCE\n"),
1294
+ narrate: true,
1295
+ ...fullAiVideo ? { maxResolvedMedia: 5 } : {},
1296
+ resolveMedia: resolveSelected
1297
+ });
1298
+ return handler;
1299
+ };
1300
+ const callText = async (task, systemPrompt, userPrompt, maxOutputTokens, signal) => {
1301
+ try {
1302
+ return cleanGeneratedText(await generateText({ task, systemPrompt, userPrompt, maxOutputTokens, signal }));
1303
+ } catch (cause) {
1304
+ reportError(cause);
1305
+ throw new VideoChatProviderError(task);
1306
+ }
1307
+ };
1308
+ return async function handleVideoChat(request) {
1309
+ const url = new URL(request.url);
1310
+ const action = url.searchParams.get("action");
1311
+ const origin = request.headers.get("origin");
1312
+ const headers = corsHeaders2(origin, allowedOrigins, allowCredentials);
1313
+ if (request.method === "OPTIONS") {
1314
+ if (origin && allowedOrigins && !allowedOrigins.includes(origin)) {
1315
+ return jsonError2(403, "origin_forbidden", "Origin is not allowed", headers);
1316
+ }
1317
+ headers.set("access-control-allow-methods", "GET, POST, OPTIONS");
1318
+ headers.set("access-control-allow-headers", "Authorization, Content-Type");
1319
+ return new Response(null, { status: 204, headers });
1320
+ }
1321
+ if (origin && allowedOrigins && !allowedOrigins.includes(origin)) {
1322
+ return jsonError2(403, "origin_forbidden", "Origin is not allowed", headers);
1323
+ }
1324
+ if (authorize !== "none") {
1325
+ let authorized = false;
1326
+ try {
1327
+ authorized = await authorize(request);
1328
+ } catch (cause) {
1329
+ reportError(cause);
1330
+ authorized = false;
1331
+ }
1332
+ if (!authorized) return jsonError2(401, "unauthorized", "Authentication required", headers);
1333
+ }
1334
+ if (action === "capabilities") {
1335
+ if (request.method !== "GET") return jsonError2(405, "method_not_allowed", "Use GET", headers);
1336
+ return Response.json(capabilities2, { headers });
1337
+ }
1338
+ if (action === "welcome") {
1339
+ if (request.method !== "GET") return jsonError2(405, "method_not_allowed", "Use GET", headers);
1340
+ if (welcomeResponse) return Response.json(welcomeResponse, { headers });
1341
+ const resolvedWelcome = await (async () => {
1342
+ let failed = false;
1343
+ const mediaResolver = searchMedia;
1344
+ const resolve = async (query) => {
1345
+ if (!mediaResolver || !query) return null;
1346
+ const signal = AbortSignal.any([request.signal, AbortSignal.timeout(1e4)]);
1347
+ try {
1348
+ const raw = await Promise.resolve().then(() => mediaResolver(query, {
1349
+ purpose: "welcome",
1350
+ orientation: "landscape",
1351
+ signal
1352
+ }));
1353
+ const media = sanitizeVideoChatMedia(raw);
1354
+ if (signal.aborted || raw != null && !media) failed = true;
1355
+ return media;
1356
+ } catch (cause) {
1357
+ failed = true;
1358
+ if (!request.signal.aborted) reportError(cause);
1359
+ return null;
1360
+ }
1361
+ };
1362
+ const [hero, ...cards] = await Promise.all([
1363
+ resolve(heroQuery),
1364
+ ...welcomePrompts.map((entry) => resolve(entry.mediaQuery))
1365
+ ]);
1366
+ return {
1367
+ cacheable: !failed,
1368
+ body: {
1369
+ hero,
1370
+ cards: welcomePrompts.map((entry, index) => ({
1371
+ prompt: entry.prompt,
1372
+ ...entry.opening ? { opening: entry.opening } : {},
1373
+ media: cards[index] ?? null
1374
+ }))
1375
+ }
1376
+ };
1377
+ })();
1378
+ if (!request.signal.aborted && resolvedWelcome.cacheable) welcomeResponse = resolvedWelcome.body;
1379
+ return Response.json(resolvedWelcome.body, { headers });
1380
+ }
1381
+ if (request.method !== "POST") return jsonError2(405, "method_not_allowed", "Use POST", headers);
1382
+ if (action === "transcription") {
1383
+ if (!transcribe) return jsonError2(404, "capability_unavailable", "Transcription is not configured", headers);
1384
+ const declared = Number(request.headers.get("content-length"));
1385
+ if (Number.isFinite(declared) && declared > maxAudioBytes) {
1386
+ return jsonError2(413, "body_too_large", "The recording is too large", headers);
1387
+ }
1388
+ const audio = new Uint8Array(await request.arrayBuffer());
1389
+ if (audio.byteLength === 0) return jsonError2(400, "empty_audio", "No audio was provided", headers);
1390
+ if (audio.byteLength > maxAudioBytes) return jsonError2(413, "body_too_large", "The recording is too large", headers);
1391
+ try {
1392
+ const text = await transcribe({
1393
+ audio,
1394
+ mediaType: request.headers.get("content-type") || "audio/webm",
1395
+ signal: request.signal
1396
+ });
1397
+ return Response.json({ text: text.trim() }, { headers });
1398
+ } catch (cause) {
1399
+ reportError(cause);
1400
+ return jsonError2(502, "transcription_failed", "The recording could not be transcribed", headers);
1401
+ }
1402
+ }
1403
+ let body;
1404
+ try {
1405
+ body = await readJson(request, maxBodyBytes);
1406
+ } catch (cause) {
1407
+ return jsonError2(
1408
+ cause instanceof Error && cause.message.includes("too large") ? 413 : 400,
1409
+ "invalid_body",
1410
+ cause instanceof Error ? cause.message : "Request body is invalid",
1411
+ headers
1412
+ );
1413
+ }
1414
+ if (action === "response") {
1415
+ let input;
1416
+ try {
1417
+ input = parseResponseRequest(body);
1418
+ } catch (cause) {
1419
+ return jsonError2(400, "invalid_request", cause instanceof Error ? cause.message : "Request is invalid", headers);
1420
+ }
1421
+ const mode = input.mode !== "templates" && !generateVideo ? "templates" : input.mode;
1422
+ const fullAiVideo = mode === "full";
1423
+ const requestId = `video-chat-${Date.now()}-${requestSequence += 1}`;
1424
+ const openingChannel = createOpeningChannel(input.opening ? { line: input.opening, keyword: "" } : void 0);
1425
+ const forwardedHeaders = new Headers(request.headers);
1426
+ forwardedHeaders.delete("content-length");
1427
+ const videoRequest = new Request(request.url, {
1428
+ method: "POST",
1429
+ headers: forwardedHeaders,
1430
+ signal: request.signal,
1431
+ body: JSON.stringify({
1432
+ protocolVersion: "0.5",
1433
+ requestId,
1434
+ ...fullAiVideo ? { capabilities: { templates: ["media"] } } : {},
1435
+ input: {
1436
+ input: conversationInput(input.prompt, input.conversation, input.opening),
1437
+ knowledgeMode: "general",
1438
+ opening: false,
1439
+ orientation: input.orientation,
1440
+ maxDurationSec: 40,
1441
+ ...input.brand ? { brand: input.brand } : {},
1442
+ style: {
1443
+ density: "airy",
1444
+ motion: "calm",
1445
+ textArchetype: "cinematic",
1446
+ ...input.style
1447
+ }
1448
+ }
1449
+ })
1450
+ });
1451
+ const response = await responseHandler(
1452
+ mode,
1453
+ requestId,
1454
+ input.opening != null,
1455
+ openingChannel
1456
+ )(videoRequest);
1457
+ return streamVideoChatOpening(response, openingChannel.ready);
1458
+ }
1459
+ try {
1460
+ if (action === "opening-media") {
1461
+ const value = record2(body, "request");
1462
+ allowedKeys2(value, ["keyword", "orientation"], "request");
1463
+ const keyword = boundedString2(value.keyword, "request.keyword", 80);
1464
+ const orientation = value.orientation ?? "landscape";
1465
+ if (orientation !== "portrait" && orientation !== "landscape") {
1466
+ throw new Error("request.orientation must be portrait or landscape");
1467
+ }
1468
+ if (!searchMedia) return Response.json({ media: null }, { headers });
1469
+ try {
1470
+ const raw = await Promise.resolve().then(() => searchMedia(keyword, {
1471
+ purpose: "response",
1472
+ orientation,
1473
+ signal: request.signal
1474
+ }));
1475
+ return Response.json({ media: sanitizeVideoChatMedia(raw) }, { headers });
1476
+ } catch (cause) {
1477
+ if (!request.signal.aborted) reportError(cause);
1478
+ return Response.json({ media: null }, { headers });
1479
+ }
1480
+ }
1481
+ if (action === "narration") {
1482
+ const value = record2(body, "request");
1483
+ allowedKeys2(value, ["prompt", "scene", "earlier"], "request");
1484
+ const prompt = boundedString2(value.prompt, "request.prompt");
1485
+ const scene = record2(value.scene, "request.scene");
1486
+ if (typeof scene.templateId !== "string" || !scene.variables || typeof scene.variables !== "object") {
1487
+ throw new Error("request.scene is invalid");
1488
+ }
1489
+ const earlier = Array.isArray(value.earlier) ? value.earlier.slice(-4).map((line2, index) => boundedString2(line2, `request.earlier[${index}]`, 2e3)) : [];
1490
+ const line = await callText(
1491
+ "narration",
1492
+ VIDEO_CHAT_NARRATION_PROMPT,
1493
+ createNarrationUserPrompt(prompt, scene, earlier),
1494
+ 256,
1495
+ request.signal
1496
+ );
1497
+ return Response.json({ line }, { headers });
1498
+ }
1499
+ if (action === "suggestions") {
1500
+ const value = record2(body, "request");
1501
+ allowedKeys2(value, ["prompt", "lines"], "request");
1502
+ const prompt = boundedString2(value.prompt, "request.prompt");
1503
+ const lines = Array.isArray(value.lines) ? value.lines.slice(-8).map((line, index) => boundedString2(line, `request.lines[${index}]`, 2e3)) : [];
1504
+ try {
1505
+ const text = await callText(
1506
+ "suggestions",
1507
+ VIDEO_CHAT_SUGGESTIONS_PROMPT,
1508
+ `USER PROMPT: ${prompt}
1509
+
1510
+ VIDEO RESPONSE:
1511
+ ${lines.join("\n")}`,
1512
+ 512,
1513
+ request.signal
1514
+ );
1515
+ const subjects = readSuggestionSubjects(text);
1516
+ const mediaResolver = searchMedia;
1517
+ const media = await Promise.all(subjects.map(async (subject) => {
1518
+ if (!mediaResolver || !subject.keyword) return null;
1519
+ try {
1520
+ const raw = await Promise.resolve().then(() => mediaResolver(subject.keyword, {
1521
+ purpose: "suggestion",
1522
+ orientation: "landscape",
1523
+ signal: request.signal
1524
+ }));
1525
+ return sanitizeVideoChatMedia(raw);
1526
+ } catch (cause) {
1527
+ if (!request.signal.aborted) reportError(cause);
1528
+ return null;
1529
+ }
1530
+ }));
1531
+ return Response.json({
1532
+ suggestions: subjects.map((subject, index) => ({
1533
+ prompt: subject.prompt,
1534
+ media: media[index] ?? null
1535
+ }))
1536
+ }, { headers });
1537
+ } catch (cause) {
1538
+ if (!(cause instanceof VideoChatProviderError)) reportError(cause);
1539
+ return Response.json({ suggestions: [] }, { headers });
1540
+ }
1541
+ }
1542
+ if (action === "speech") {
1543
+ if (!generateSpeech) return new Response(null, { status: 204, headers });
1544
+ const value = record2(body, "request");
1545
+ allowedKeys2(value, ["text"], "request");
1546
+ const text = boundedString2(value.text, "request.text", 4e3);
1547
+ try {
1548
+ const result = await generateSpeech({ text, signal: request.signal });
1549
+ return new Response(audioBody(result.audio), {
1550
+ headers: {
1551
+ ...Object.fromEntries(headers),
1552
+ "content-type": result.mediaType || "audio/mpeg",
1553
+ "cache-control": "no-store"
1554
+ }
1555
+ });
1556
+ } catch (cause) {
1557
+ reportError(cause);
1558
+ return jsonError2(502, "speech_failed", "Speech could not be generated", headers);
1559
+ }
1560
+ }
1561
+ } catch (cause) {
1562
+ if (cause instanceof VideoChatProviderError) {
1563
+ return jsonError2(502, "text_generation_failed", "Text could not be generated", headers);
1564
+ }
1565
+ return jsonError2(400, "invalid_request", cause instanceof Error ? cause.message : "Request is invalid", headers);
1566
+ }
1567
+ return jsonError2(404, "unknown_action", "Video chat action was not found", headers);
1568
+ };
1569
+ }
626
1570
  export {
627
1571
  createServerTemplateRegistry,
1572
+ createVideoChatHandler,
628
1573
  createVideoHandler
629
1574
  };