@vanillaskyai/video 0.10.2 → 0.10.3

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 (38) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +12 -7
  3. package/dist/check-runtime.js +2 -2
  4. package/dist/{chunk-6KZGF63O.js → chunk-3U2F7C7C.js} +53 -34
  5. package/dist/{chunk-5C6HZNHY.js → chunk-LJR5VLEI.js} +3 -2
  6. package/dist/{chunk-77VY4O7A.js → chunk-SI6CFFAA.js} +57 -10
  7. package/dist/{chunk-KHLO5OHT.js → chunk-UCXQORDZ.js} +1 -1
  8. package/dist/{chunk-VQH3JTQC.js → chunk-VVNZCY2U.js} +1 -1
  9. package/dist/{chunk-TVIU23OM.js → chunk-YZZSWGC3.js} +8 -3
  10. package/dist/{cinema-media-APDJS7SC.js → cinema-media-4JUYKPQI.js} +4 -4
  11. package/dist/{comparison-ZVE2DE7U.js → comparison-3QLMUC5V.js} +4 -4
  12. package/dist/{editorial-timeline-YIXXVJUV.js → editorial-timeline-IVE2P25N.js} +4 -4
  13. package/dist/{key-figure-I2C37FWV.js → key-figure-APD56LCO.js} +4 -4
  14. package/dist/{mobile-message-JSGBU6EX.js → mobile-message-WLK3WY5B.js} +4 -4
  15. package/dist/{quote-XH54G3FS.js → quote-Z6YET5BO.js} +4 -4
  16. package/dist/react.js +25 -13
  17. package/dist/{scene-video-backdrop-PCTARAAJ.js → scene-video-backdrop-OWUXPXHZ.js} +2 -2
  18. package/dist/server.js +302 -257
  19. package/docs/agent-integration.md +5 -4
  20. package/docs/getting-started.md +7 -5
  21. package/docs/media-and-audio.md +23 -17
  22. package/docs/production.md +7 -4
  23. package/docs/prompt-and-input.md +24 -15
  24. package/docs/provider-integration.md +2 -2
  25. package/docs/reference/protocol.md +7 -1
  26. package/docs/reference/provider-adapters.md +5 -3
  27. package/docs/testing.md +7 -2
  28. package/package.json +1 -1
  29. package/registry/items/backgrounds.json +2 -2
  30. package/registry/items/cinemaMedia.json +1 -1
  31. package/registry/items/comparison.json +1 -1
  32. package/registry/items/editorialTimeline.json +1 -1
  33. package/registry/items/keyFigure.json +1 -1
  34. package/registry/items/mobileMessage.json +1 -1
  35. package/registry/items/quote.json +1 -1
  36. package/starters/video-chat/README.md +18 -17
  37. package/starters/video-chat/package.json +1 -1
  38. package/starters/video-chat/providers/video.ts +5 -4
package/dist/server.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  invokeIsolated
7
7
  } from "./chunk-5BSJLT6H.js";
8
8
  import {
9
+ attachGenerationLifecycleSink,
9
10
  getGenerationLifecycleSink
10
11
  } from "./chunk-E7CL7UPB.js";
11
12
  import {
@@ -55,69 +56,6 @@ import "./chunk-3O7OMMMF.js";
55
56
  import "./chunk-2E6T633S.js";
56
57
  import "./chunk-73NTSFFI.js";
57
58
 
58
- // src/server/opening-continuity.ts
59
- function continueAfterOpening(narration, earlier) {
60
- const words = (value) => Array.from(value.matchAll(/[\p{L}\p{N}]+(?:['’][\p{L}\p{N}]+)*/gu));
61
- let remaining = narration;
62
- for (const previous of [...earlier].reverse()) {
63
- const known = words(previous), current = words(remaining);
64
- if (known.length < 5 || known.length > current.length) continue;
65
- if (!known.every((word, index) => word[0].toLowerCase() === current[index][0].toLowerCase())) continue;
66
- if (known.length === current.length) return "";
67
- const last = current[known.length - 1];
68
- const suffix = remaining.slice(last.index + last[0].length);
69
- const boundary = suffix.match(/^[”"'’\])]*[.!?…]+[\s”"'’\])]*/u);
70
- if (boundary) remaining = suffix.slice(boundary[0].length).trim();
71
- }
72
- return remaining;
73
- }
74
- function createOpeningContinuation(initialOpening) {
75
- const earlier = initialOpening ? [initialOpening] : [];
76
- let bodyStarted = false;
77
- const copy = (authored, narration) => {
78
- if (continueAfterOpening(authored, earlier) !== "") return authored;
79
- const next = continueAfterOpening(narration, earlier).trim();
80
- return next && [...next].length <= 65 && /[.!?…][”"’')\]]*$/u.test(next) ? next : authored;
81
- };
82
- return {
83
- copy,
84
- remember(line) {
85
- if (line.trim()) earlier.push(line);
86
- },
87
- narration(line) {
88
- return continueAfterOpening(line, earlier);
89
- },
90
- line(rawLine) {
91
- if (bodyStarted || earlier.length === 0) return rawLine;
92
- let part;
93
- try {
94
- part = JSON.parse(rawLine);
95
- } catch {
96
- return rawLine;
97
- }
98
- if (!part || part.type !== "scene.add" || part.placement === "closer") return rawLine;
99
- const scene = part.scene;
100
- if (!scene) return rawLine;
101
- if (typeof scene.narration !== "string") {
102
- bodyStarted = true;
103
- return rawLine;
104
- }
105
- const narration = continueAfterOpening(scene.narration, earlier);
106
- if (!narration && scene.templateId === "cinemaMedia") return null;
107
- const variables = scene.variables;
108
- if (!narration && scene.templateId === "chapterTitle" && typeof variables?.title === "string" && !continueAfterOpening(variables.title, earlier)) return null;
109
- bodyStarted = true;
110
- if (!narration) return rawLine;
111
- const field = scene.templateId === "cinemaMedia" ? "fallbackText" : scene.templateId === "chapterTitle" ? "title" : void 0;
112
- const authored = field && variables?.[field];
113
- const updated = typeof authored === "string" ? copy(authored, narration) : void 0;
114
- const copyChanged = typeof authored === "string" && updated !== authored;
115
- if (narration === scene.narration && !copyChanged) return rawLine;
116
- return JSON.stringify({ ...part, scene: { ...scene, narration, ...copyChanged ? { variables: { ...variables, [field]: updated } } : {} } });
117
- }
118
- };
119
- }
120
-
121
59
  // src/server/request-validation.ts
122
60
  var MAX_GENERATED_LOOK_LENGTH = 1e3;
123
61
  function boundedString(value, path) {
@@ -935,23 +873,264 @@ function createVideoHandler(options) {
935
873
  });
936
874
  }
937
875
 
876
+ // src/server/opening-continuity.ts
877
+ function continueAfterOpening(narration, earlier) {
878
+ const words = (value) => Array.from(value.matchAll(/[\p{L}\p{N}]+(?:['’][\p{L}\p{N}]+)*/gu));
879
+ let remaining = narration;
880
+ for (const previous of [...earlier].reverse()) {
881
+ const known = words(previous), current = words(remaining);
882
+ if (known.length < 5 || known.length > current.length) continue;
883
+ if (!known.every((word, index) => word[0].toLowerCase() === current[index][0].toLowerCase())) continue;
884
+ if (known.length === current.length) return "";
885
+ const last = current[known.length - 1];
886
+ const suffix = remaining.slice(last.index + last[0].length);
887
+ const boundary = suffix.match(/^[”"'’\])]*[.!?…]+[\s”"'’\])]*/u);
888
+ if (boundary) remaining = suffix.slice(boundary[0].length).trim();
889
+ }
890
+ return remaining;
891
+ }
892
+
893
+ // src/server/chat-shot-planner.ts
894
+ var object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
895
+ function text(value, maximum) {
896
+ return typeof value === "string" && value.trim().length <= maximum ? value.trim() : "";
897
+ }
898
+ function readShot(value) {
899
+ const item = object(value);
900
+ const narration = text(item?.narration, 2e3);
901
+ if (!narration) throw new Error("Chat shot requires bounded authored narration");
902
+ return {
903
+ narration,
904
+ subject: text(item?.subject, 80),
905
+ action: text(item?.action, 600),
906
+ durationSec: typeof item?.durationSec === "number" && Number.isFinite(item.durationSec) ? Math.min(5, Math.max(2, item.durationSec)) : 5,
907
+ continuity: item?.continuity === "continue" ? "continue" : "cut"
908
+ };
909
+ }
910
+ function replaceStream(source, textStream) {
911
+ if (!(typeof source === "object" && source != null && "textStream" in source)) return textStream;
912
+ return new Proxy({ textStream }, {
913
+ get(target, key, receiver) {
914
+ return key === "textStream" ? Reflect.get(target, key, receiver) : Reflect.get(source, key, source);
915
+ }
916
+ });
917
+ }
918
+ function createChatShotPlanner(options) {
919
+ const incomplete = /* @__PURE__ */ new WeakSet();
920
+ const planner = createTextDeltaVideoPlanner({
921
+ includeRawProviderData: options.includeRawProviderData,
922
+ streamText(context) {
923
+ const providerContext = { ...context, userPrompt: [
924
+ `Create a complete answer within ${context.request.input.maxDurationSec ?? 40} seconds. Each generated clip has at most five seconds; give each spoken beat room to finish.`,
925
+ `Orientation: ${context.request.input.orientation ?? "landscape"}.`,
926
+ "USER REQUEST AND CONVERSATION",
927
+ context.request.input.input
928
+ ].join("\n") };
929
+ const sink = getGenerationLifecycleSink(context);
930
+ if (sink) attachGenerationLifecycleSink(providerContext, sink);
931
+ let source;
932
+ try {
933
+ source = options.streamText(providerContext);
934
+ } catch (cause) {
935
+ options.publishOpening(void 0);
936
+ throw cause;
937
+ }
938
+ const upstream = typeof source === "object" && source != null && "textStream" in source ? source.textStream : source;
939
+ const translated = (async function* () {
940
+ let brief, buffer = "", index = 0, bodyDuration = 0, lastNarration = "";
941
+ let firstBody = true;
942
+ const reject = (cause) => {
943
+ incomplete.add(context);
944
+ const error = cause instanceof Error ? cause : new Error(String(cause));
945
+ if (!getGenerationLifecycleSink(context)?.rejectPart?.(error)) throw error;
946
+ };
947
+ const scenePart = (shot, closer = false) => {
948
+ let narration = shot.narration;
949
+ if (firstBody && !closer) narration = continueAfterOpening(narration, [options.openingLine ?? brief?.opening ?? ""]);
950
+ firstBody = false;
951
+ lastNarration = narration;
952
+ return { type: "scene.add", ...closer ? { placement: "closer" } : {}, scene: {
953
+ id: `${context.request.requestId}-shot-${++index}`,
954
+ templateId: "cinemaMedia",
955
+ variables: { mediaType: "video", mediaKeyword: shot.subject, shotDirection: [
956
+ brief?.visualDirection,
957
+ shot.action,
958
+ shot.continuity === "continue" ? "Continue the established subject, setting and action consistently." : "A deliberate new shot; choose framing that reveals this beat.",
959
+ "Silent illustration. No spoken dialogue, voiceover, written words or subtitles in the generated footage."
960
+ ].filter(Boolean).join("\n") },
961
+ narration,
962
+ timing: { fixedDuration: shot.durationSec }
963
+ } };
964
+ };
965
+ const line = (raw) => {
966
+ const trimmed = raw.trim();
967
+ if (!trimmed || /^```(?:json|ndjson)?$/i.test(trimmed)) return;
968
+ const part = object(JSON.parse(trimmed));
969
+ if (part?.type === "answer") {
970
+ if (brief) throw new Error("Chat answer brief was emitted more than once");
971
+ brief = { opening: text(part.opening, 300), subject: text(part.subject, 80), visualDirection: text(part.visualDirection, 600), development: text(part.development, 2e3) };
972
+ if (part.ending) {
973
+ try {
974
+ brief.ending = readShot(part.ending);
975
+ } catch (cause) {
976
+ reject(cause);
977
+ }
978
+ }
979
+ options.publishOpening(brief.opening ? { line: brief.opening, keyword: brief.subject } : void 0);
980
+ return;
981
+ }
982
+ if (part?.type !== "shot") throw new Error("Chat plan requires an answer brief followed by shots");
983
+ if (!brief) throw new Error("Chat shot arrived before its answer brief");
984
+ const shot = readShot(part);
985
+ if (shot.narration === brief.ending?.narration) return;
986
+ if (firstBody && !continueAfterOpening(shot.narration, [options.openingLine ?? brief.opening])) return;
987
+ const budget = (context.request.input.maxDurationSec ?? 40) - (brief.ending?.durationSec ?? 5);
988
+ if (bodyDuration + shot.durationSec > budget) throw new Error("Chat shot exceeds the answer duration budget");
989
+ bodyDuration += shot.durationSec;
990
+ return scenePart(shot);
991
+ };
992
+ try {
993
+ for await (const delta of upstream) {
994
+ context.signal.throwIfAborted();
995
+ if (typeof delta !== "string") throw new Error("The LLM adapter returned a non-text delta");
996
+ buffer += delta;
997
+ if (buffer.length > 32768) throw new Error("Chat plan line exceeds the bounded stream limit");
998
+ let newline = buffer.indexOf("\n");
999
+ while (newline >= 0) {
1000
+ const raw = buffer.slice(0, newline);
1001
+ buffer = buffer.slice(newline + 1);
1002
+ try {
1003
+ const part = line(raw);
1004
+ if (part) yield JSON.stringify(part) + "\n";
1005
+ } catch (cause) {
1006
+ reject(cause);
1007
+ }
1008
+ newline = buffer.indexOf("\n");
1009
+ }
1010
+ }
1011
+ if (buffer.trim()) {
1012
+ try {
1013
+ const part = line(buffer);
1014
+ if (part) yield JSON.stringify(part) + "\n";
1015
+ } catch (cause) {
1016
+ reject(cause);
1017
+ }
1018
+ }
1019
+ if (brief?.development && bodyDuration === 0) incomplete.add(context);
1020
+ if (brief?.ending && brief.ending.narration !== lastNarration) yield JSON.stringify(scenePart(brief.ending, true)) + "\n";
1021
+ else if (!brief?.ending) incomplete.add(context);
1022
+ } catch (cause) {
1023
+ if (!context.signal.aborted && brief?.ending && brief.ending.narration !== lastNarration) {
1024
+ yield JSON.stringify(scenePart(brief.ending, true)) + "\n";
1025
+ }
1026
+ throw cause;
1027
+ } finally {
1028
+ options.publishOpening(void 0);
1029
+ }
1030
+ })();
1031
+ return replaceStream(source, translated);
1032
+ }
1033
+ });
1034
+ return async function* (context) {
1035
+ let completed = false;
1036
+ for await (const part of resolveShots(planner(context), context, options)) {
1037
+ if (part.type === "plan.complete") completed = true;
1038
+ yield part;
1039
+ }
1040
+ if (!completed) {
1041
+ if (incomplete.has(context)) getGenerationLifecycleSink(context)?.reportWarning?.({ code: "plan_incomplete", category: "provider", message: "Some authored answer content could not be completed.", recoverable: true });
1042
+ yield { type: "plan.complete", ...incomplete.has(context) ? { finishReason: "other" } : {} };
1043
+ }
1044
+ };
1045
+ }
1046
+ async function* resolveShots(parts, context, options) {
1047
+ const queue = [];
1048
+ const iterator = parts[Symbol.asyncIterator]();
1049
+ const limit = Number.isFinite(options.mediaConcurrency) ? Math.min(5, Math.max(1, Math.floor(options.mediaConcurrency))) : 1;
1050
+ let done = false, closed = false, producerError;
1051
+ let notify, space;
1052
+ const resolve = async (part) => {
1053
+ if (part.type !== "scene.add") return part;
1054
+ const { mediaKeyword } = part.scene.variables;
1055
+ let media;
1056
+ if (typeof mediaKeyword === "string" && mediaKeyword && options.resolveMedia) media = await options.resolveMedia(mediaKeyword, {
1057
+ input: context.request.input,
1058
+ requestId: context.request.requestId,
1059
+ scene: part.scene,
1060
+ templateId: "cinemaMedia",
1061
+ preferredType: "video",
1062
+ generatedLook: context.request.input.style?.generatedLook,
1063
+ signal: context.signal
1064
+ });
1065
+ context.signal.throwIfAborted();
1066
+ if (!media) getGenerationLifecycleSink(context)?.reportWarning?.({ code: "provider_warning", category: "provider", message: "Some visuals are unavailable; narration continues.", recoverable: true });
1067
+ const scene = { ...part.scene, variables: { mediaType: media?.type === "image" ? "photo" : "video", mediaUrl: media?.url ?? "", ...media?.posterUrl ? { mediaPoster: media.posterUrl } : {} } };
1068
+ return { ...part, scene };
1069
+ };
1070
+ const producer = (async () => {
1071
+ try {
1072
+ while (!closed) {
1073
+ if (queue.length >= limit) await new Promise((r) => {
1074
+ space = r;
1075
+ });
1076
+ if (closed) break;
1077
+ const next = await iterator.next();
1078
+ if (next.done) break;
1079
+ queue.push(resolve(next.value).then((part) => ({ part }), (error) => ({ error })));
1080
+ notify?.();
1081
+ notify = void 0;
1082
+ }
1083
+ } catch (error) {
1084
+ producerError = error;
1085
+ } finally {
1086
+ done = true;
1087
+ notify?.();
1088
+ notify = void 0;
1089
+ }
1090
+ })();
1091
+ try {
1092
+ while (!done || queue.length) {
1093
+ if (!queue.length) await new Promise((r) => {
1094
+ notify = r;
1095
+ });
1096
+ const item = queue[0];
1097
+ if (!item) continue;
1098
+ const result = await item;
1099
+ queue.shift();
1100
+ space?.();
1101
+ space = void 0;
1102
+ if ("error" in result) throw result.error;
1103
+ yield result.part;
1104
+ }
1105
+ if (producerError) throw producerError;
1106
+ } finally {
1107
+ closed = true;
1108
+ space?.();
1109
+ void iterator.return?.().catch(() => void 0);
1110
+ void producer;
1111
+ }
1112
+ }
1113
+
938
1114
  // src/server/video-chat-prompts.ts
939
1115
  function createVideoChatResponseInstructions(generatedVideoAvailable, openingAlreadyProvided = false, maxGeneratedVideos = 5) {
940
1116
  return [
941
- "Respond as a coherent short film: establish, develop, then land a useful or emotional payoff. Match the user's requested form.",
942
- generatedVideoAvailable ? 'First emit one host-consumed opening JSON object: {"type":"video-chat.opening","spokenHook":"6-9 words","mediaKeyword":"literal subject","firstShot":{"text":"short grounded fallback","narration":"a natural spoken beat advancing the hook","mediaKeyword":"2-8 concrete words","shotDirection":"optional framing and action"}}. Include firstShot only when a distinctive illustrative shot serves the story; omit it for an abstract explanation best opened with graphics. When present, the host inserts it once and begins generation immediately; do not emit it again.' : openingAlreadyProvided ? "Start directly with scene.add; the opening has already been supplied." : 'First emit {"type":"video-chat.opening","spokenHook":"6-9 words","mediaKeyword":"literal subject"}. This is host-consumed, not a scene.',
943
- openingAlreadyProvided ? "Preserve the supplied opening exactly; continue it without repeating its words or claim." : void 0,
944
- "The spokenHook is already heard before the first scene. Start firstShot.narration with the next fact, cause, action or consequence; never restate the hook. The first ordinary scene must then advance beyond firstShot rather than repeat its narration or opening claim. If firstShot is omitted, that first scene must advance the hook directly. firstShot.text, fallbackText and chapter titles must also express that new beat, not reuse the hook or prior shot copy; write a concise visual anchor of roughly 2\u20136 words within the schema budget, never a full narration sentence, clipped statistic or qualifying clause.",
945
- "The optional firstShot has five seconds of footage: give its narration one brief sentence of roughly 8\u201310 spoken words that fits that budget. Carry further explanation, actions or dialogue in later scenes instead of squeezing the answer into the first shot. This limit applies only to firstShot; pace later narration to its own scene.",
946
- "Continue with only as many scenes as the story and duration need. Never pad to a fixed count. Every emitted scene carries narration beside variables and timing; no additional narration request should be needed.",
947
- "Make most of the film relevant footage: use full-bleed media for action, subjects and atmosphere, with graphics only to clarify a specific relationship or piece of evidence. Maintain consistent setting, lighting and subject while varying shot scale. Avoid unrelated cinematic montages.",
948
- "Use the installed catalog: ordered events, comparison, exact quote, one key figure, chapter or a message only when its narrative job fits. For an ordinary explanation, roughly one graphic-led explanatory beat per 30 seconds is a starting point, plus a brief chapter opening when needed. Let other intents use the visual form they need. Do not place graphic beats consecutively unless no honest relevant media is available. Give media-capable graphics a relevant background when it supports the meaning; keep black when no matching media exists. Do not force every template into a video or use lists of bullets.",
949
- `At most ${generatedVideoAvailable ? maxGeneratedVideos : 0} generated-video attempts are available. Media scenes choose mediaSource=generate for distinctive illustrative shots or mediaSource=stock for generic verified imagery. A stock miss is not permission to broaden essential details. Do not spend generation on every scene.`,
950
- "Each mediaKeyword is a literal filmable subject/action, 2\u20138 words, maximum 80 characters. Use optional shotDirection for action, camera framing and continuity; it does not change the literal stock subject. Supply a short grounded fallbackText when the schema declares it: one useful visual anchor, not a second explanation competing with narration or a promise of footage. It must still make sense when the media is absent. Do not put visible headline text over full-bleed footage.",
951
- "No invented quotations, attribution, statistics, personal evidence or URLs. Creative stories may be invented when requested; do not present generated illustration as historical evidence.",
952
- 'Emit exactly one final placement:"closer" scene using a catalog template with a suitable payoff or ask job. It should land the meaning, not recap the whole answer. Prefer the final relevant action or visual reveal with narration; a closer does not require a black chapter card.',
953
- "Every scene needs timing, even an empty object. Narration preserves the requested form and tone, uses natural sentence lengths, and does not read every on-screen word back. Finish with plan.complete."
954
- ].filter((line) => line != null).join("\n");
1117
+ "Write a complete, intentful video answer as newline-delimited JSON. Match the user's form and tone; mixed intents can combine directions.",
1118
+ 'First write one brief: {"type":"answer","intent":"explanation|story|comedy|imagination|practical","opening":"a short inviting spoken introduction of 6\u20139 words","subject":"literal visual subject","development":"the essential development of this answer","visualDirection":"consistent subjects, appearance and visual approach","ending":{"narration":"the authored payoff","subject":"literal subject","action":"visible action or change","durationSec":5,"continuity":"cut|continue"}}.',
1119
+ 'Then stream each developing shot on its own line: {"type":"shot","narration":"the exact spoken beat","subject":"2\u20138 literal filmable words, at most 80 characters","action":"concrete subject, action or visible change and useful framing","durationSec":5,"continuity":"cut|continue"}.',
1120
+ `The host permits at most ${generatedVideoAvailable ? maxGeneratedVideos : 0} generated-video attempts for this response. ${generatedVideoAvailable ? "Generated footage is preferred." : "Generated footage is unavailable."} Relevant stock is the fallback and may only depict a literal filmable subject; some requested visuals may be unavailable. Preserve the complete answer rather than shortening it to fit credits. The host selects providers; do not make source choices.`,
1121
+ "For a very short answer whose ending alone fulfills the request, development may be empty and no developing shots are needed. Otherwise, develop the essential content before the ending.",
1122
+ "The brief's ending is saved and played after your developing shots. Do not repeat it as a shot. Stop writing after the last developing shot. No technical events, identifiers, template choices, media providers, URLs or extra fields.",
1123
+ "Every shot uses moving footage with separate narration and subtitles. Generated footage is silent: do not ask its subjects to speak or render words. No headline cards or on-screen explanatory text.",
1124
+ "Each clip has at most five seconds. Write spoken beats that fit naturally, usually 8\u201310 words per five-second shot. Split longer ideas across purposeful shots, preserving facts and qualifiers. Never truncate a claim to meet a word target. Use only the shots needed within the total duration, including the ending; do not pad to a fixed count.",
1125
+ "Identify the full answer and its ending before developing shots. Each action must support what is said: camera movement alone is not progression. Vary scale, viewpoint and meaningful details while keeping subjects consistent.",
1126
+ "Explanations: clarify the actual causal mechanism, separating physical cause from a metaphor. Generated cutaways and animation illustrate ideas; they are not factual evidence. Preserve uncertainty, quantities and conditions; never invent evidence or quotations.",
1127
+ "Stories: portray characters making choices and experiencing consequences; use consistent character descriptions and an earned resolution, not a promised next scene.",
1128
+ "Comedy: establish the premise, time the visual or spoken reveal, allow a reaction beat, and stop on the payoff without explaining the joke.",
1129
+ "Imagination: make the impossible action concrete, establish the world's internal rules and keep its imagery consistent. Do not replace imagination with an explanation of it.",
1130
+ "Practical answers: show usable actions in their necessary order, with framing that makes the method and result visible. Preserve essential steps and relevant safety conditions.",
1131
+ openingAlreadyProvided ? "The supplied opening has already been spoken. Preserve it and begin the body with new content." : "The brief opening is spoken during preparation. The first body shot must develop it rather than repeat its words or claim.",
1132
+ "Use continuity=continue when the same subject/action should remain coherent; choose cut for a purposeful new view. Describe recurring subjects consistently. Never assume a different angle or generated depiction proves a factual claim."
1133
+ ].join("\n");
955
1134
  }
956
1135
  var VIDEO_CHAT_NARRATION_PROMPT = [
957
1136
  "You narrate a short video response, one scene at a time.",
@@ -995,9 +1174,7 @@ var DEFAULT_MAX_AUDIO_BYTES = 8 * 1024 * 1024;
995
1174
  var MAX_PROMPT_CHARACTERS = 8e3;
996
1175
  var MAX_CONVERSATION_TURNS = 12;
997
1176
  var MAX_CONVERSATION_RESPONSE_CHARACTERS = 8e3;
998
- var VIDEO_CHAT_OPENING_PLAN_TYPE = "video-chat.opening";
999
1177
  var VIDEO_CHAT_OPENING_EVENT_TYPE = "data.video-chat-opening";
1000
- var PROVIDER_CODE_FENCE = /^```(?:json|ndjson)?$/i;
1001
1178
  var DEFAULT_WELCOME_PROMPTS = [
1002
1179
  {
1003
1180
  prompt: "Why does the Moon always show one face?",
@@ -1108,64 +1285,6 @@ async function readJson(request, maximum) {
1108
1285
  function cleanGeneratedText(value) {
1109
1286
  return value.trim().replace(/^["']|["']$/g, "");
1110
1287
  }
1111
- function readGeneratedFirstShot(value) {
1112
- if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1113
- const shot = value;
1114
- const bounded = (field, maximum) => typeof field === "string" ? [...field.trim()].slice(0, maximum).join("").trim() : "";
1115
- const text = bounded(shot.text, 65);
1116
- const narration = bounded(shot.narration, 300);
1117
- const mediaKeyword = bounded(shot.mediaKeyword, 80).match(/\S+/gu)?.slice(0, 8).join(" ") ?? "";
1118
- const shotDirection = bounded(shot.shotDirection, 220);
1119
- return text && narration && mediaKeyword ? { text, narration, mediaKeyword, ...shotDirection ? { shotDirection } : {} } : void 0;
1120
- }
1121
- function boundedWords(value, maximum, characters) {
1122
- if (typeof value !== "string") return "";
1123
- return value.trim().match(/\S+/gu)?.slice(0, maximum).join(" ").slice(0, characters).trim() ?? "";
1124
- }
1125
- function readOpeningPlanLine(line) {
1126
- try {
1127
- const parsed = JSON.parse(line);
1128
- if (!parsed || parsed.type !== VIDEO_CHAT_OPENING_PLAN_TYPE) return void 0;
1129
- const firstShot = readGeneratedFirstShot(parsed.firstShot);
1130
- return {
1131
- line: boundedWords(parsed.spokenHook, 9, 300),
1132
- keyword: boundedWords(parsed.mediaKeyword, 4, 80),
1133
- ...boundedWords(parsed.fallbackKeyword, 4, 80) ? { fallbackKeyword: boundedWords(parsed.fallbackKeyword, 4, 80) } : {},
1134
- ...firstShot ? { firstShot } : {}
1135
- };
1136
- } catch {
1137
- return void 0;
1138
- }
1139
- }
1140
- function reservedFirstScene(requestId, firstShot) {
1141
- return {
1142
- type: "scene.add",
1143
- scene: {
1144
- id: `${requestId}-first-shot`,
1145
- templateId: "cinemaMedia",
1146
- variables: {
1147
- fallbackText: firstShot.text,
1148
- mediaSource: "generate",
1149
- ...firstShot.shotDirection ? { shotDirection: firstShot.shotDirection } : {},
1150
- mediaType: "video",
1151
- mediaKeyword: firstShot.mediaKeyword
1152
- },
1153
- timing: { fixedDuration: 5 },
1154
- narration: firstShot.narration
1155
- }
1156
- };
1157
- }
1158
- function replaceTextStream(source, textStream) {
1159
- const enriched = typeof source === "object" && source != null && "textStream" in source ? source : void 0;
1160
- if (!enriched) return textStream;
1161
- const wrapper = { textStream };
1162
- return new Proxy(wrapper, {
1163
- get(target, property, receiver) {
1164
- if (property === "textStream") return Reflect.get(target, property, receiver);
1165
- return Reflect.get(enriched, property, enriched);
1166
- }
1167
- });
1168
- }
1169
1288
  function createOpeningChannel(initial) {
1170
1289
  if (initial) return { ready: Promise.resolve(initial), publish: () => void 0 };
1171
1290
  let published = false;
@@ -1182,77 +1301,6 @@ function createOpeningChannel(initial) {
1182
1301
  }
1183
1302
  };
1184
1303
  }
1185
- function interceptOpeningPlan(source, options) {
1186
- const enriched = typeof source === "object" && source != null && "textStream" in source ? source : void 0;
1187
- const upstream = enriched?.textStream ?? source;
1188
- const textStream = (async function* () {
1189
- let buffer = "";
1190
- let decided = !options.expectOpening;
1191
- const continuation = createOpeningContinuation(options.openingLine);
1192
- const acceptOpening = (opening) => {
1193
- if (!options.openingProvided) {
1194
- options.publish(opening.line ? opening : void 0);
1195
- continuation.remember(opening.line);
1196
- }
1197
- if (!options.generatedVideoAvailable || !opening.firstShot) return void 0;
1198
- const narration = continuation.narration(opening.firstShot.narration);
1199
- if (!narration) return void 0;
1200
- const text = continuation.copy(opening.firstShot.text, narration);
1201
- continuation.remember(narration);
1202
- return JSON.stringify(reservedFirstScene(options.requestId, { ...opening.firstShot, text, narration }));
1203
- };
1204
- try {
1205
- for await (const delta of upstream) {
1206
- if (typeof delta !== "string") throw new Error("The LLM adapter returned a non-text delta");
1207
- buffer += delta;
1208
- let newline = buffer.indexOf("\n");
1209
- while (newline >= 0) {
1210
- const rawLine = buffer.slice(0, newline);
1211
- buffer = buffer.slice(newline + 1);
1212
- const line = rawLine.trim();
1213
- if (!decided && line && !PROVIDER_CODE_FENCE.test(line)) {
1214
- const opening = readOpeningPlanLine(line);
1215
- decided = true;
1216
- if (opening) {
1217
- const firstScene = acceptOpening(opening);
1218
- if (firstScene) yield `${firstScene}
1219
- `;
1220
- newline = buffer.indexOf("\n");
1221
- continue;
1222
- }
1223
- options.publish(void 0);
1224
- }
1225
- const continued = continuation.line(rawLine);
1226
- if (continued != null) yield `${continued}
1227
- `;
1228
- newline = buffer.indexOf("\n");
1229
- }
1230
- }
1231
- if (buffer) {
1232
- const line = buffer.trim();
1233
- if (!decided && line && !PROVIDER_CODE_FENCE.test(line)) {
1234
- const opening = readOpeningPlanLine(line);
1235
- decided = true;
1236
- if (opening) {
1237
- const firstScene = acceptOpening(opening);
1238
- if (firstScene) yield `${firstScene}
1239
- `;
1240
- } else {
1241
- options.publish(void 0);
1242
- const continued = continuation.line(buffer);
1243
- if (continued != null) yield continued;
1244
- }
1245
- } else {
1246
- const continued = continuation.line(buffer);
1247
- if (continued != null) yield continued;
1248
- }
1249
- }
1250
- } finally {
1251
- options.publish(void 0);
1252
- }
1253
- })();
1254
- return replaceTextStream(source, textStream);
1255
- }
1256
1304
  function resequenceEvent(event, sequence) {
1257
1305
  return {
1258
1306
  ...event,
@@ -1338,11 +1386,11 @@ function streamVideoChatOpening(response, openingReady) {
1338
1386
  headers: response.headers
1339
1387
  });
1340
1388
  }
1341
- function readSuggestionSubjects(text) {
1342
- const opening = text.indexOf("{");
1343
- const closing = text.lastIndexOf("}");
1389
+ function readSuggestionSubjects(text2) {
1390
+ const opening = text2.indexOf("{");
1391
+ const closing = text2.lastIndexOf("}");
1344
1392
  if (opening < 0 || closing <= opening) return [];
1345
- const parsed = JSON.parse(text.slice(opening, closing + 1));
1393
+ const parsed = JSON.parse(text2.slice(opening, closing + 1));
1346
1394
  return (Array.isArray(parsed.suggestions) ? parsed.suggestions : []).flatMap((entry) => {
1347
1395
  if (!entry || typeof entry !== "object") return [];
1348
1396
  const item = entry;
@@ -1425,14 +1473,7 @@ function createVideoChatHandler(options) {
1425
1473
  const generatedVideoAvailable = generateVideo != null && maxGeneratedVideos > 0;
1426
1474
  let lifecycle;
1427
1475
  let generatedAttempts = 0;
1428
- const completedVideos = /* @__PURE__ */ new Map();
1429
- const reusedUrls = /* @__PURE__ */ new Set();
1430
1476
  const resolveSelected = generateVideo || searchMedia ? async (query, context) => {
1431
- const reuseKey = JSON.stringify([query, context.generatedLook ?? "", context.scene.variables.shotDirection ?? "", context.input.orientation ?? "landscape"]);
1432
- const remember = (media) => {
1433
- if (media?.type === "video") completedVideos.set(reuseKey, media);
1434
- return media;
1435
- };
1436
1477
  const mediaContext = {
1437
1478
  purpose: "response",
1438
1479
  orientation: context.input.orientation ?? "landscape",
@@ -1460,9 +1501,9 @@ function createVideoChatHandler(options) {
1460
1501
  return null;
1461
1502
  }
1462
1503
  };
1463
- if (generatedVideoAvailable && context.scene.variables.mediaSource === "generate") {
1504
+ if (generatedVideoAvailable && (!options.templates || context.scene.variables.mediaSource === "generate")) {
1464
1505
  const generated = generatedAttempts < maxGeneratedVideos ? (generatedAttempts++, await attempt(generateVideo, generateVideoTimeoutMs)) : null;
1465
- if (generated) return remember(generated);
1506
+ if (generated?.type === "video") return generated;
1466
1507
  lifecycle?.reportWarning?.({
1467
1508
  code: "provider_warning",
1468
1509
  category: "provider",
@@ -1471,42 +1512,46 @@ function createVideoChatHandler(options) {
1471
1512
  });
1472
1513
  }
1473
1514
  const stock = await attempt(searchMedia, 3e3);
1474
- if (stock) return remember(stock);
1475
- if (generatedVideoAvailable) {
1476
- const previous = completedVideos.get(reuseKey);
1477
- if (previous && !reusedUrls.has(previous.url)) {
1478
- reusedUrls.add(previous.url);
1479
- return previous;
1480
- }
1481
- }
1515
+ if (stock && (options.templates || stock.type === "video")) return stock;
1482
1516
  return null;
1483
1517
  } : void 0;
1484
- const handler = createVideoHandler({
1485
- ...videoOptions,
1486
- streamText: (context) => {
1487
- lifecycle = getGenerationLifecycleSink(context);
1488
- try {
1489
- return interceptOpeningPlan(videoOptions.streamText(context), {
1490
- expectOpening: generatedVideoAvailable || !openingProvided,
1491
- openingProvided,
1492
- openingLine,
1493
- requestId,
1494
- generatedVideoAvailable,
1495
- publish: openingChannel.publish
1496
- });
1497
- } catch (cause) {
1498
- openingChannel.publish(void 0);
1499
- throw cause;
1500
- }
1501
- },
1518
+ if (options.templates) {
1519
+ openingChannel.publish(void 0);
1520
+ return createVideoHandler({
1521
+ ...videoOptions,
1522
+ authorize: "none",
1523
+ allowedOrigins,
1524
+ allowCredentials,
1525
+ maxBodyBytes,
1526
+ mediaConcurrency,
1527
+ resolveMedia: resolveSelected,
1528
+ narrate: true,
1529
+ basePrompt: instructions
1530
+ });
1531
+ }
1532
+ const handler = createVideoStreamHandler({
1533
+ heartbeatMs: videoOptions.heartbeatMs,
1534
+ onError: videoOptions.onError,
1535
+ onWarning: videoOptions.onWarning,
1536
+ onComplete: videoOptions.onComplete,
1537
+ invalidPartBehavior: videoOptions.invalidPartBehavior,
1538
+ requireCloser: options.requireCloser ?? true,
1539
+ generate: createChatShotPlanner({
1540
+ streamText: (context) => {
1541
+ lifecycle = getGenerationLifecycleSink(context);
1542
+ return videoOptions.streamText(context);
1543
+ },
1544
+ includeRawProviderData: videoOptions.includeRawProviderData,
1545
+ openingLine,
1546
+ publishOpening: openingChannel.publish,
1547
+ resolveMedia: resolveSelected,
1548
+ mediaConcurrency
1549
+ }),
1502
1550
  authorize: "none",
1503
1551
  allowedOrigins,
1504
1552
  allowCredentials,
1505
1553
  maxBodyBytes,
1506
- mediaConcurrency,
1507
- basePrompt: [createVideoChatResponseInstructions(generatedVideoAvailable, openingProvided, maxGeneratedVideos), instructions?.trim()].filter(Boolean).join("\n\nAPPLICATION GUIDANCE\n"),
1508
- narrate: true,
1509
- resolveMedia: resolveSelected
1554
+ systemPrompt: [createVideoChatResponseInstructions(generatedVideoAvailable, openingProvided, maxGeneratedVideos), instructions?.trim()].filter(Boolean).join("\n\nAPPLICATION GUIDANCE\n")
1510
1555
  });
1511
1556
  return handler;
1512
1557
  };
@@ -1601,12 +1646,12 @@ function createVideoChatHandler(options) {
1601
1646
  if (audio.byteLength === 0) return jsonError2(400, "empty_audio", "No audio was provided", headers);
1602
1647
  if (audio.byteLength > maxAudioBytes) return jsonError2(413, "body_too_large", "The recording is too large", headers);
1603
1648
  try {
1604
- const text = await transcribe({
1649
+ const text2 = await transcribe({
1605
1650
  audio,
1606
1651
  mediaType: request.headers.get("content-type") || "audio/webm",
1607
1652
  signal: request.signal
1608
1653
  });
1609
- return Response.json({ text: text.trim() }, { headers });
1654
+ return Response.json({ text: text2.trim() }, { headers });
1610
1655
  } catch (cause) {
1611
1656
  reportError(cause);
1612
1657
  return jsonError2(502, "transcription_failed", "The recording could not be transcribed", headers);
@@ -1712,7 +1757,7 @@ function createVideoChatHandler(options) {
1712
1757
  const prompt = boundedString2(value.prompt, "request.prompt");
1713
1758
  const lines = Array.isArray(value.lines) ? value.lines.slice(-8).map((line, index) => boundedString2(line, `request.lines[${index}]`, 2e3)) : [];
1714
1759
  try {
1715
- const text = await withDeadline((signal) => callText(
1760
+ const text2 = await withDeadline((signal) => callText(
1716
1761
  "suggestions",
1717
1762
  VIDEO_CHAT_SUGGESTIONS_PROMPT,
1718
1763
  `USER PROMPT: ${prompt}
@@ -1722,7 +1767,7 @@ ${lines.join("\n")}`,
1722
1767
  512,
1723
1768
  signal
1724
1769
  ), 3e3, request.signal);
1725
- const subjects = readSuggestionSubjects(text);
1770
+ const subjects = readSuggestionSubjects(text2);
1726
1771
  const mediaResolver = searchMedia;
1727
1772
  const media = await Promise.all(subjects.map(async (subject) => {
1728
1773
  if (!mediaResolver || !subject.keyword) return null;
@@ -1753,9 +1798,9 @@ ${lines.join("\n")}`,
1753
1798
  if (!generateSpeech) return new Response(null, { status: 204, headers });
1754
1799
  const value = record2(body, "request");
1755
1800
  allowedKeys2(value, ["text"], "request");
1756
- const text = boundedString2(value.text, "request.text", 4e3);
1801
+ const text2 = boundedString2(value.text, "request.text", 4e3);
1757
1802
  try {
1758
- const result = await withDeadline((signal) => generateSpeech({ text, signal }), 3e3, request.signal);
1803
+ const result = await withDeadline((signal) => generateSpeech({ text: text2, signal }), 3e3, request.signal);
1759
1804
  return new Response(audioBody(result.audio), {
1760
1805
  headers: {
1761
1806
  ...Object.fromEntries(headers),