@vanillaskyai/video 0.10.2 → 0.10.4
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.
- package/CHANGELOG.md +17 -0
- package/PUBLIC-API.md +43 -31
- package/README.md +18 -9
- package/dist/check-runtime.js +2 -2
- package/dist/{chunk-KHLO5OHT.js → chunk-G5TYLTL5.js} +1 -1
- package/dist/{chunk-77VY4O7A.js → chunk-O6FBIB4L.js} +58 -10
- package/dist/{chunk-TVIU23OM.js → chunk-OOBT4X46.js} +13 -3
- package/dist/{chunk-5C6HZNHY.js → chunk-QZAZT44G.js} +3 -2
- package/dist/{chunk-6KZGF63O.js → chunk-WZFLEPLM.js} +58 -35
- package/dist/{chunk-VQH3JTQC.js → chunk-Z2ZI5G7O.js} +1 -1
- package/dist/{cinema-media-APDJS7SC.js → cinema-media-3SIGVVCY.js} +4 -4
- package/dist/{comparison-ZVE2DE7U.js → comparison-VDGRWPB5.js} +4 -4
- package/dist/{editorial-timeline-YIXXVJUV.js → editorial-timeline-QJU4JJWB.js} +4 -4
- package/dist/{key-figure-I2C37FWV.js → key-figure-YUAQW6OE.js} +4 -4
- package/dist/{mobile-message-JSGBU6EX.js → mobile-message-ZMMTUDUQ.js} +4 -4
- package/dist/{quote-XH54G3FS.js → quote-BB6UJT6L.js} +4 -4
- package/dist/react.d.ts +3 -1
- package/dist/react.js +94 -84
- package/dist/{scene-video-backdrop-PCTARAAJ.js → scene-video-backdrop-4EZVVQY7.js} +2 -2
- package/dist/server.d.ts +15 -3
- package/dist/server.js +416 -275
- package/dist/{types-CdBmNTGD.d.ts → types-BqB8zC9u.d.ts} +1 -1
- package/docs/agent-integration.md +5 -4
- package/docs/concepts.md +2 -2
- package/docs/development.md +23 -0
- package/docs/getting-started.md +7 -5
- package/docs/media-and-audio.md +37 -18
- package/docs/performance.md +21 -10
- package/docs/production.md +11 -10
- package/docs/prompt-and-input.md +24 -15
- package/docs/provider-integration.md +11 -13
- package/docs/reference/protocol.md +7 -1
- package/docs/reference/provider-adapters.md +7 -4
- package/docs/testing.md +7 -2
- package/package.json +5 -2
- package/registry/items/backgrounds.json +2 -2
- package/registry/items/cinemaMedia.json +1 -1
- package/registry/items/comparison.json +1 -1
- package/registry/items/editorialTimeline.json +1 -1
- package/registry/items/keyFigure.json +1 -1
- package/registry/items/mobileMessage.json +1 -1
- package/registry/items/quote.json +1 -1
- package/starters/video-chat/README.md +25 -26
- package/starters/video-chat/package.json +1 -1
- package/starters/video-chat/providers/video.ts +10 -6
- package/starters/video-chat/server.ts +1 -1
- package/starters/video-chat/stock.ts +59 -31
- package/styles/video-chat.css +6 -61
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,268 @@ 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, clipDurationSec) {
|
|
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
|
+
title: text(item?.title, 65) || text(item?.subject, 65) || "The next step",
|
|
905
|
+
subject: text(item?.subject, 80),
|
|
906
|
+
action: text(item?.action, 600),
|
|
907
|
+
durationSec: typeof item?.durationSec === "number" && Number.isFinite(item.durationSec) ? Math.min(clipDurationSec, Math.max(2, item.durationSec)) : clipDurationSec,
|
|
908
|
+
continuity: item?.continuity === "continue" ? "continue" : "cut"
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
function replaceStream(source, textStream) {
|
|
912
|
+
if (!(typeof source === "object" && source != null && "textStream" in source)) return textStream;
|
|
913
|
+
return new Proxy({ textStream }, {
|
|
914
|
+
get(target, key, receiver) {
|
|
915
|
+
return key === "textStream" ? Reflect.get(target, key, receiver) : Reflect.get(source, key, source);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
function createChatShotPlanner(options) {
|
|
920
|
+
const clipDurationSec = options.generatedClipDurationSec ?? 5;
|
|
921
|
+
const incomplete = /* @__PURE__ */ new WeakSet();
|
|
922
|
+
const planner = createTextDeltaVideoPlanner({
|
|
923
|
+
includeRawProviderData: options.includeRawProviderData,
|
|
924
|
+
streamText(context) {
|
|
925
|
+
const providerContext = { ...context, userPrompt: [
|
|
926
|
+
`Create a complete answer within ${context.request.input.maxDurationSec ?? 40} seconds. Each generated clip has at most ${clipDurationSec} seconds; give each spoken beat room to finish.`,
|
|
927
|
+
`Orientation: ${context.request.input.orientation ?? "landscape"}.`,
|
|
928
|
+
"USER REQUEST AND CONVERSATION",
|
|
929
|
+
context.request.input.input
|
|
930
|
+
].join("\n") };
|
|
931
|
+
const sink = getGenerationLifecycleSink(context);
|
|
932
|
+
if (sink) attachGenerationLifecycleSink(providerContext, sink);
|
|
933
|
+
let source;
|
|
934
|
+
try {
|
|
935
|
+
source = options.streamText(providerContext);
|
|
936
|
+
} catch (cause) {
|
|
937
|
+
options.publishOpening(void 0);
|
|
938
|
+
throw cause;
|
|
939
|
+
}
|
|
940
|
+
const upstream = typeof source === "object" && source != null && "textStream" in source ? source.textStream : source;
|
|
941
|
+
const translated = (async function* () {
|
|
942
|
+
let brief, buffer = "", index = 0, bodyDuration = 0, lastNarration = "";
|
|
943
|
+
let firstBody = true;
|
|
944
|
+
const reject = (cause) => {
|
|
945
|
+
incomplete.add(context);
|
|
946
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
947
|
+
if (!getGenerationLifecycleSink(context)?.rejectPart?.(error)) throw error;
|
|
948
|
+
};
|
|
949
|
+
const scenePart = (shot, closer = false) => {
|
|
950
|
+
let narration = shot.narration;
|
|
951
|
+
if (firstBody && !closer) narration = continueAfterOpening(narration, [options.openingLine ?? brief?.opening ?? ""]);
|
|
952
|
+
firstBody = false;
|
|
953
|
+
lastNarration = narration;
|
|
954
|
+
return { type: "scene.add", ...closer ? { placement: "closer" } : {}, scene: {
|
|
955
|
+
id: `${context.request.requestId}-shot-${++index}`,
|
|
956
|
+
templateId: "cinemaMedia",
|
|
957
|
+
variables: { fallbackText: shot.title, mediaType: "video", mediaKeyword: shot.subject, shotDirection: [
|
|
958
|
+
brief?.visualDirection,
|
|
959
|
+
shot.action,
|
|
960
|
+
shot.continuity === "continue" ? "Continue the established subject, setting and action consistently." : "A deliberate new shot; choose framing that reveals this beat.",
|
|
961
|
+
"Silent illustration. No spoken dialogue, voiceover, written words or subtitles in the generated footage."
|
|
962
|
+
].filter(Boolean).join("\n") },
|
|
963
|
+
narration,
|
|
964
|
+
timing: { fixedDuration: shot.durationSec }
|
|
965
|
+
} };
|
|
966
|
+
};
|
|
967
|
+
const line = (raw) => {
|
|
968
|
+
const trimmed = raw.trim();
|
|
969
|
+
if (!trimmed || /^```(?:json|ndjson)?$/i.test(trimmed)) return;
|
|
970
|
+
const part = object(JSON.parse(trimmed));
|
|
971
|
+
if (part?.type === "answer") {
|
|
972
|
+
if (brief) throw new Error("Chat answer brief was emitted more than once");
|
|
973
|
+
brief = { opening: text(part.opening, 300), subject: text(part.subject, 80), visualDirection: text(part.visualDirection, 600), development: text(part.development, 2e3) };
|
|
974
|
+
if (part.ending) {
|
|
975
|
+
try {
|
|
976
|
+
brief.ending = readShot(part.ending, clipDurationSec);
|
|
977
|
+
} catch (cause) {
|
|
978
|
+
reject(cause);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
options.publishOpening(brief.opening ? { line: brief.opening, keyword: brief.subject } : void 0);
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
if (part?.type !== "shot") throw new Error("Chat plan requires an answer brief followed by shots");
|
|
985
|
+
if (!brief) throw new Error("Chat shot arrived before its answer brief");
|
|
986
|
+
const shot = readShot(part, clipDurationSec);
|
|
987
|
+
if (shot.narration === brief.ending?.narration) return;
|
|
988
|
+
if (firstBody && !continueAfterOpening(shot.narration, [options.openingLine ?? brief.opening])) return;
|
|
989
|
+
const budget = (context.request.input.maxDurationSec ?? 40) - (brief.ending?.durationSec ?? clipDurationSec);
|
|
990
|
+
if (bodyDuration + shot.durationSec > budget) throw new Error("Chat shot exceeds the answer duration budget");
|
|
991
|
+
bodyDuration += shot.durationSec;
|
|
992
|
+
return scenePart(shot);
|
|
993
|
+
};
|
|
994
|
+
try {
|
|
995
|
+
for await (const delta of upstream) {
|
|
996
|
+
context.signal.throwIfAborted();
|
|
997
|
+
if (typeof delta !== "string") throw new Error("The LLM adapter returned a non-text delta");
|
|
998
|
+
buffer += delta;
|
|
999
|
+
if (buffer.length > 32768) throw new Error("Chat plan line exceeds the bounded stream limit");
|
|
1000
|
+
let newline = buffer.indexOf("\n");
|
|
1001
|
+
while (newline >= 0) {
|
|
1002
|
+
const raw = buffer.slice(0, newline);
|
|
1003
|
+
buffer = buffer.slice(newline + 1);
|
|
1004
|
+
try {
|
|
1005
|
+
const part = line(raw);
|
|
1006
|
+
if (part) yield JSON.stringify(part) + "\n";
|
|
1007
|
+
} catch (cause) {
|
|
1008
|
+
reject(cause);
|
|
1009
|
+
}
|
|
1010
|
+
newline = buffer.indexOf("\n");
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (buffer.trim()) {
|
|
1014
|
+
try {
|
|
1015
|
+
const part = line(buffer);
|
|
1016
|
+
if (part) yield JSON.stringify(part) + "\n";
|
|
1017
|
+
} catch (cause) {
|
|
1018
|
+
reject(cause);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
if (brief?.development && bodyDuration === 0) incomplete.add(context);
|
|
1022
|
+
if (brief?.ending && brief.ending.narration !== lastNarration) yield JSON.stringify(scenePart(brief.ending, true)) + "\n";
|
|
1023
|
+
else if (!brief?.ending) incomplete.add(context);
|
|
1024
|
+
} catch (cause) {
|
|
1025
|
+
if (!context.signal.aborted && brief?.ending && brief.ending.narration !== lastNarration) {
|
|
1026
|
+
yield JSON.stringify(scenePart(brief.ending, true)) + "\n";
|
|
1027
|
+
}
|
|
1028
|
+
throw cause;
|
|
1029
|
+
} finally {
|
|
1030
|
+
options.publishOpening(void 0);
|
|
1031
|
+
}
|
|
1032
|
+
})();
|
|
1033
|
+
return replaceStream(source, translated);
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
return async function* (context) {
|
|
1037
|
+
let completed = false;
|
|
1038
|
+
for await (const part of resolveShots(planner(context), context, options)) {
|
|
1039
|
+
if (part.type === "plan.complete") completed = true;
|
|
1040
|
+
yield part;
|
|
1041
|
+
}
|
|
1042
|
+
if (!completed) {
|
|
1043
|
+
if (incomplete.has(context)) getGenerationLifecycleSink(context)?.reportWarning?.({ code: "plan_incomplete", category: "provider", message: "Some authored answer content could not be completed.", recoverable: true });
|
|
1044
|
+
yield { type: "plan.complete", ...incomplete.has(context) ? { finishReason: "other" } : {} };
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
async function* resolveShots(parts, context, options) {
|
|
1049
|
+
const queue = [];
|
|
1050
|
+
const iterator = parts[Symbol.asyncIterator]();
|
|
1051
|
+
const limit = Number.isFinite(options.mediaConcurrency) ? Math.min(5, Math.max(1, Math.floor(options.mediaConcurrency))) : 1;
|
|
1052
|
+
let done = false, closed = false, producerError;
|
|
1053
|
+
let notify, space;
|
|
1054
|
+
const resolve = async (part) => {
|
|
1055
|
+
if (part.type !== "scene.add") return part;
|
|
1056
|
+
options.prepareScene?.({ sceneId: part.scene.id, narration: part.scene.narration ?? "" });
|
|
1057
|
+
const { mediaKeyword } = part.scene.variables;
|
|
1058
|
+
let media;
|
|
1059
|
+
if (typeof mediaKeyword === "string" && mediaKeyword && options.resolveMedia) media = await options.resolveMedia(mediaKeyword, {
|
|
1060
|
+
input: context.request.input,
|
|
1061
|
+
requestId: context.request.requestId,
|
|
1062
|
+
scene: part.scene,
|
|
1063
|
+
templateId: "cinemaMedia",
|
|
1064
|
+
preferredType: "video",
|
|
1065
|
+
generatedLook: context.request.input.style?.generatedLook,
|
|
1066
|
+
signal: context.signal
|
|
1067
|
+
});
|
|
1068
|
+
context.signal.throwIfAborted();
|
|
1069
|
+
if (!media) getGenerationLifecycleSink(context)?.reportWarning?.({ code: "provider_warning", category: "provider", message: MEDIA_RECOVERY_NOTICE, recoverable: true });
|
|
1070
|
+
const title = part.scene.variables.fallbackText;
|
|
1071
|
+
const scene = media ? { ...part.scene, variables: { fallbackText: title, mediaType: media.type === "image" ? "photo" : "video", mediaUrl: media.url, ...media.posterUrl ? { mediaPoster: media.posterUrl } : {} } } : { ...part.scene, templateId: "chapterTitle", variables: { title } };
|
|
1072
|
+
return { ...part, scene };
|
|
1073
|
+
};
|
|
1074
|
+
const producer = (async () => {
|
|
1075
|
+
try {
|
|
1076
|
+
while (!closed) {
|
|
1077
|
+
if (queue.length >= limit) await new Promise((r) => {
|
|
1078
|
+
space = r;
|
|
1079
|
+
});
|
|
1080
|
+
if (closed) break;
|
|
1081
|
+
const next = await iterator.next();
|
|
1082
|
+
if (next.done) break;
|
|
1083
|
+
queue.push(resolve(next.value).then((part) => ({ part }), (error) => ({ error })));
|
|
1084
|
+
notify?.();
|
|
1085
|
+
notify = void 0;
|
|
1086
|
+
}
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
producerError = error;
|
|
1089
|
+
} finally {
|
|
1090
|
+
done = true;
|
|
1091
|
+
notify?.();
|
|
1092
|
+
notify = void 0;
|
|
1093
|
+
}
|
|
1094
|
+
})();
|
|
1095
|
+
try {
|
|
1096
|
+
while (!done || queue.length) {
|
|
1097
|
+
if (!queue.length) await new Promise((r) => {
|
|
1098
|
+
notify = r;
|
|
1099
|
+
});
|
|
1100
|
+
const item = queue[0];
|
|
1101
|
+
if (!item) continue;
|
|
1102
|
+
const result = await item;
|
|
1103
|
+
queue.shift();
|
|
1104
|
+
space?.();
|
|
1105
|
+
space = void 0;
|
|
1106
|
+
if ("error" in result) throw result.error;
|
|
1107
|
+
yield result.part;
|
|
1108
|
+
}
|
|
1109
|
+
if (producerError) throw producerError;
|
|
1110
|
+
} finally {
|
|
1111
|
+
closed = true;
|
|
1112
|
+
space?.();
|
|
1113
|
+
void iterator.return?.().catch(() => void 0);
|
|
1114
|
+
void producer;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
938
1118
|
// src/server/video-chat-prompts.ts
|
|
939
|
-
function createVideoChatResponseInstructions(generatedVideoAvailable, openingAlreadyProvided = false, maxGeneratedVideos = 5) {
|
|
1119
|
+
function createVideoChatResponseInstructions(generatedVideoAvailable, openingAlreadyProvided = false, maxGeneratedVideos = 5, clipDurationSec = 5, mode = "cinematic") {
|
|
940
1120
|
return [
|
|
941
|
-
"
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
"
|
|
946
|
-
"
|
|
947
|
-
"
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
"
|
|
951
|
-
"
|
|
952
|
-
|
|
953
|
-
"
|
|
954
|
-
|
|
1121
|
+
"Write a complete, intentful video answer as newline-delimited JSON. Match the user's form and tone; mixed intents can combine directions.",
|
|
1122
|
+
`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":{"title":"short meaningful chapter title, at most 65 characters","narration":"the authored payoff","subject":"literal subject","action":"visible action or change","durationSec":${clipDurationSec},"continuity":"cut|continue"}}.`,
|
|
1123
|
+
`Then stream each developing shot on its own line: {"type":"shot","title":"short meaningful chapter title, at most 65 characters","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":${clipDurationSec},"continuity":"cut|continue"}.`,
|
|
1124
|
+
`The selected footage mode is ${mode === "pexels" ? "Pexels stock search: use literal filmable subjects; never imply stock proves a mechanism or depicts fictional events exactly" : `AI video, with at most ${generatedVideoAvailable ? maxGeneratedVideos : 0} generation attempts`}. Missing footage becomes the authored chapter title, with complete narration. Preserve the full answer rather than shortening it to fit credits. The host selects providers; do not make source choices.`,
|
|
1125
|
+
"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.",
|
|
1126
|
+
"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.",
|
|
1127
|
+
"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.",
|
|
1128
|
+
`Each clip has at most ${clipDurationSec} seconds. Write spoken beats that fit naturally, usually ${Math.floor(clipDurationSec * 1.6)}\u2013${Math.floor(clipDurationSec * 2)} words per 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.`,
|
|
1129
|
+
"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.",
|
|
1130
|
+
"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.",
|
|
1131
|
+
"Stories: portray characters making choices and experiencing consequences; use consistent character descriptions and an earned resolution, not a promised next scene.",
|
|
1132
|
+
"Comedy: establish the premise, time the visual or spoken reveal, allow a reaction beat, and stop on the payoff without explaining the joke.",
|
|
1133
|
+
"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.",
|
|
1134
|
+
"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.",
|
|
1135
|
+
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.",
|
|
1136
|
+
"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."
|
|
1137
|
+
].join("\n");
|
|
955
1138
|
}
|
|
956
1139
|
var VIDEO_CHAT_NARRATION_PROMPT = [
|
|
957
1140
|
"You narrate a short video response, one scene at a time.",
|
|
@@ -995,9 +1178,7 @@ var DEFAULT_MAX_AUDIO_BYTES = 8 * 1024 * 1024;
|
|
|
995
1178
|
var MAX_PROMPT_CHARACTERS = 8e3;
|
|
996
1179
|
var MAX_CONVERSATION_TURNS = 12;
|
|
997
1180
|
var MAX_CONVERSATION_RESPONSE_CHARACTERS = 8e3;
|
|
998
|
-
var VIDEO_CHAT_OPENING_PLAN_TYPE = "video-chat.opening";
|
|
999
1181
|
var VIDEO_CHAT_OPENING_EVENT_TYPE = "data.video-chat-opening";
|
|
1000
|
-
var PROVIDER_CODE_FENCE = /^```(?:json|ndjson)?$/i;
|
|
1001
1182
|
var DEFAULT_WELCOME_PROMPTS = [
|
|
1002
1183
|
{
|
|
1003
1184
|
prompt: "Why does the Moon always show one face?",
|
|
@@ -1044,8 +1225,8 @@ function parseResponseRequest(value) {
|
|
|
1044
1225
|
const body = record2(value, "request");
|
|
1045
1226
|
allowedKeys2(body, ["prompt", "opening", "mode", "orientation", "conversation", "style"], "request");
|
|
1046
1227
|
const mode = body.mode ?? "cinematic";
|
|
1047
|
-
if (mode !== "cinematic") {
|
|
1048
|
-
throw new Error("request.mode must be cinematic");
|
|
1228
|
+
if (mode !== "cinematic" && mode !== "pexels") {
|
|
1229
|
+
throw new Error("request.mode must be cinematic or pexels");
|
|
1049
1230
|
}
|
|
1050
1231
|
const orientation = body.orientation ?? "landscape";
|
|
1051
1232
|
if (orientation !== "portrait" && orientation !== "landscape") {
|
|
@@ -1108,64 +1289,6 @@ async function readJson(request, maximum) {
|
|
|
1108
1289
|
function cleanGeneratedText(value) {
|
|
1109
1290
|
return value.trim().replace(/^["']|["']$/g, "");
|
|
1110
1291
|
}
|
|
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
1292
|
function createOpeningChannel(initial) {
|
|
1170
1293
|
if (initial) return { ready: Promise.resolve(initial), publish: () => void 0 };
|
|
1171
1294
|
let published = false;
|
|
@@ -1182,77 +1305,6 @@ function createOpeningChannel(initial) {
|
|
|
1182
1305
|
}
|
|
1183
1306
|
};
|
|
1184
1307
|
}
|
|
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
1308
|
function resequenceEvent(event, sequence) {
|
|
1257
1309
|
return {
|
|
1258
1310
|
...event,
|
|
@@ -1260,7 +1312,25 @@ function resequenceEvent(event, sequence) {
|
|
|
1260
1312
|
eventId: `${event.runId}:${sequence}`
|
|
1261
1313
|
};
|
|
1262
1314
|
}
|
|
1263
|
-
|
|
1315
|
+
var VIDEO_CHAT_PREPARATION_EVENT_TYPE = "data.video-chat-preparation";
|
|
1316
|
+
function createPreparationChannel() {
|
|
1317
|
+
let wake;
|
|
1318
|
+
const channel = {
|
|
1319
|
+
queue: [],
|
|
1320
|
+
changed: new Promise((resolve) => {
|
|
1321
|
+
wake = resolve;
|
|
1322
|
+
}),
|
|
1323
|
+
publish(value) {
|
|
1324
|
+
channel.queue.push(value);
|
|
1325
|
+
wake();
|
|
1326
|
+
channel.changed = new Promise((resolve) => {
|
|
1327
|
+
wake = resolve;
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
return channel;
|
|
1332
|
+
}
|
|
1333
|
+
function streamVideoChatOpening(response, openingReady, preparations, cancel) {
|
|
1264
1334
|
if (!response.body || !response.headers.get("content-type")?.includes("text/event-stream")) return response;
|
|
1265
1335
|
const events = decodeVideoSse(response.body)[Symbol.asyncIterator]();
|
|
1266
1336
|
const encoded = (async function* () {
|
|
@@ -1276,7 +1346,8 @@ function streamVideoChatOpening(response, openingReady) {
|
|
|
1276
1346
|
...first.value.data.capabilities,
|
|
1277
1347
|
extensions: Array.from(/* @__PURE__ */ new Set([
|
|
1278
1348
|
...first.value.data.capabilities?.extensions ?? [],
|
|
1279
|
-
VIDEO_CHAT_OPENING_EVENT_TYPE
|
|
1349
|
+
VIDEO_CHAT_OPENING_EVENT_TYPE,
|
|
1350
|
+
VIDEO_CHAT_PREPARATION_EVENT_TYPE
|
|
1280
1351
|
]))
|
|
1281
1352
|
}
|
|
1282
1353
|
}
|
|
@@ -1299,10 +1370,28 @@ function streamVideoChatOpening(response, openingReady) {
|
|
|
1299
1370
|
});
|
|
1300
1371
|
sequence += 1;
|
|
1301
1372
|
}
|
|
1302
|
-
let
|
|
1303
|
-
while (
|
|
1304
|
-
|
|
1305
|
-
|
|
1373
|
+
let pending = nextEvent;
|
|
1374
|
+
while (true) {
|
|
1375
|
+
while (preparations.queue.length) {
|
|
1376
|
+
const data = preparations.queue.shift();
|
|
1377
|
+
yield encodeVideoSseEvent({
|
|
1378
|
+
protocolVersion: first.value.protocolVersion,
|
|
1379
|
+
runId: first.value.runId,
|
|
1380
|
+
sequence,
|
|
1381
|
+
eventId: `${first.value.runId}:${sequence}`,
|
|
1382
|
+
type: VIDEO_CHAT_PREPARATION_EVENT_TYPE,
|
|
1383
|
+
data
|
|
1384
|
+
});
|
|
1385
|
+
sequence += 1;
|
|
1386
|
+
}
|
|
1387
|
+
const next = await Promise.race([
|
|
1388
|
+
pending.then((value) => ({ value })),
|
|
1389
|
+
preparations.changed.then(() => void 0)
|
|
1390
|
+
]);
|
|
1391
|
+
if (!next || preparations.queue.length) continue;
|
|
1392
|
+
if (next.value.done) break;
|
|
1393
|
+
yield encodeVideoSseEvent(resequenceEvent(next.value.value, sequence++));
|
|
1394
|
+
pending = events.next();
|
|
1306
1395
|
}
|
|
1307
1396
|
} finally {
|
|
1308
1397
|
await events.return?.(void 0);
|
|
@@ -1328,6 +1417,7 @@ function streamVideoChatOpening(response, openingReady) {
|
|
|
1328
1417
|
}
|
|
1329
1418
|
},
|
|
1330
1419
|
async cancel() {
|
|
1420
|
+
cancel();
|
|
1331
1421
|
completed = true;
|
|
1332
1422
|
await iterator.return?.();
|
|
1333
1423
|
}
|
|
@@ -1338,11 +1428,11 @@ function streamVideoChatOpening(response, openingReady) {
|
|
|
1338
1428
|
headers: response.headers
|
|
1339
1429
|
});
|
|
1340
1430
|
}
|
|
1341
|
-
function readSuggestionSubjects(
|
|
1342
|
-
const opening =
|
|
1343
|
-
const closing =
|
|
1431
|
+
function readSuggestionSubjects(text2) {
|
|
1432
|
+
const opening = text2.indexOf("{");
|
|
1433
|
+
const closing = text2.lastIndexOf("}");
|
|
1344
1434
|
if (opening < 0 || closing <= opening) return [];
|
|
1345
|
-
const parsed = JSON.parse(
|
|
1435
|
+
const parsed = JSON.parse(text2.slice(opening, closing + 1));
|
|
1346
1436
|
return (Array.isArray(parsed.suggestions) ? parsed.suggestions : []).flatMap((entry) => {
|
|
1347
1437
|
if (!entry || typeof entry !== "object") return [];
|
|
1348
1438
|
const item = entry;
|
|
@@ -1392,7 +1482,8 @@ function createVideoChatHandler(options) {
|
|
|
1392
1482
|
allowCredentials,
|
|
1393
1483
|
mediaConcurrency = 5,
|
|
1394
1484
|
maxGeneratedVideos = 5,
|
|
1395
|
-
generateVideoTimeoutMs = 15e3
|
|
1485
|
+
generateVideoTimeoutMs = 15e3,
|
|
1486
|
+
generatedClipDurationSec = 5
|
|
1396
1487
|
} = options;
|
|
1397
1488
|
const videoOptions = {
|
|
1398
1489
|
templates: options.templates,
|
|
@@ -1408,6 +1499,7 @@ function createVideoChatHandler(options) {
|
|
|
1408
1499
|
if (!Number.isFinite(maxAudioBytes) || maxAudioBytes <= 0) throw new Error("maxAudioBytes must be positive");
|
|
1409
1500
|
if (!Number.isFinite(maxBodyBytes) || maxBodyBytes <= 0) throw new Error("maxBodyBytes must be positive");
|
|
1410
1501
|
if (!Number.isSafeInteger(generateVideoTimeoutMs) || generateVideoTimeoutMs < 1 || generateVideoTimeoutMs > 12e4) throw new Error("generateVideoTimeoutMs must be an integer from 1 to 120000");
|
|
1502
|
+
if (!Number.isFinite(generatedClipDurationSec) || generatedClipDurationSec < 2 || generatedClipDurationSec > 20) throw new Error("generatedClipDurationSec must be from 2 to 20");
|
|
1411
1503
|
if (!Number.isSafeInteger(maxGeneratedVideos) || maxGeneratedVideos < 0) throw new Error("maxGeneratedVideos must be a nonnegative safe integer");
|
|
1412
1504
|
const capabilities2 = {
|
|
1413
1505
|
templates: true,
|
|
@@ -1415,24 +1507,41 @@ function createVideoChatHandler(options) {
|
|
|
1415
1507
|
generatedVideo: generateVideo != null,
|
|
1416
1508
|
stockMedia: searchMedia != null,
|
|
1417
1509
|
transcription: transcribe != null,
|
|
1418
|
-
modes: ["cinematic"]
|
|
1510
|
+
modes: searchMedia ? ["cinematic", "pexels"] : ["cinematic"]
|
|
1419
1511
|
};
|
|
1420
1512
|
const welcomePrompts = (welcomeOptions?.prompts ?? DEFAULT_WELCOME_PROMPTS).slice(0, 4);
|
|
1421
1513
|
const heroQuery = welcomeOptions?.heroQuery;
|
|
1422
1514
|
let welcomeResponse;
|
|
1423
1515
|
let requestSequence = 0;
|
|
1424
|
-
const responseHandler = (requestId, openingProvided, openingChannel, openingLine) => {
|
|
1425
|
-
const
|
|
1516
|
+
const responseHandler = (requestId, openingProvided, openingChannel, openingLine, mode, preparations) => {
|
|
1517
|
+
const startedAt = Date.now();
|
|
1518
|
+
const diagnose = (event) => {
|
|
1519
|
+
try {
|
|
1520
|
+
void Promise.resolve(options.onDiagnostic?.({ requestId, mode, elapsedMs: Math.max(0, Date.now() - startedAt), ...event })).catch(() => void 0);
|
|
1521
|
+
} catch {
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
diagnose({ phase: "request-accepted" });
|
|
1525
|
+
const generatedVideoAvailable = mode === "cinematic" && generateVideo != null && maxGeneratedVideos > 0;
|
|
1426
1526
|
let lifecycle;
|
|
1427
1527
|
let generatedAttempts = 0;
|
|
1428
|
-
|
|
1429
|
-
|
|
1528
|
+
let mediaStartedAt;
|
|
1529
|
+
let mediaIndex = 0;
|
|
1430
1530
|
const resolveSelected = generateVideo || searchMedia ? async (query, context) => {
|
|
1431
|
-
|
|
1432
|
-
const
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1531
|
+
mediaStartedAt ??= Date.now();
|
|
1532
|
+
const remainingMs = mediaStartedAt + generateVideoTimeoutMs + mediaIndex++ * generatedClipDurationSec * 1e3 - Date.now();
|
|
1533
|
+
if (remainingMs <= 0) {
|
|
1534
|
+
diagnose({ phase: "media-skipped", sceneId: context.scene.id, reason: "deadline" });
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1537
|
+
if (mode === "cinematic" && (!generateVideo || generatedAttempts >= maxGeneratedVideos)) {
|
|
1538
|
+
diagnose({ phase: "media-skipped", sceneId: context.scene.id, reason: !generateVideo ? "not-configured" : "allowance" });
|
|
1539
|
+
return null;
|
|
1540
|
+
}
|
|
1541
|
+
if (mode === "pexels" && !searchMedia) {
|
|
1542
|
+
diagnose({ phase: "media-skipped", sceneId: context.scene.id, reason: "not-configured" });
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1436
1545
|
const mediaContext = {
|
|
1437
1546
|
purpose: "response",
|
|
1438
1547
|
orientation: context.input.orientation ?? "landscape",
|
|
@@ -1446,6 +1555,8 @@ function createVideoChatHandler(options) {
|
|
|
1446
1555
|
const attempt = async (resolver, timeoutMs) => {
|
|
1447
1556
|
context.signal.throwIfAborted();
|
|
1448
1557
|
if (!resolver) return null;
|
|
1558
|
+
const mediaStart = Date.now();
|
|
1559
|
+
diagnose({ phase: "media-start", sceneId: context.scene.id });
|
|
1449
1560
|
try {
|
|
1450
1561
|
const result = sanitizeVideoChatMedia(await withDeadline(
|
|
1451
1562
|
(signal) => resolver(query, { ...mediaContext, signal }),
|
|
@@ -1453,16 +1564,28 @@ function createVideoChatHandler(options) {
|
|
|
1453
1564
|
context.signal
|
|
1454
1565
|
));
|
|
1455
1566
|
context.signal.throwIfAborted();
|
|
1567
|
+
diagnose({
|
|
1568
|
+
phase: "media-end",
|
|
1569
|
+
sceneId: context.scene.id,
|
|
1570
|
+
durationMs: Math.max(0, Date.now() - mediaStart),
|
|
1571
|
+
reason: result && (result.type === "video" || options.templates && resolver === searchMedia) ? "ready" : "empty"
|
|
1572
|
+
});
|
|
1456
1573
|
return result;
|
|
1457
1574
|
} catch (cause) {
|
|
1575
|
+
diagnose({
|
|
1576
|
+
phase: "media-end",
|
|
1577
|
+
sceneId: context.scene.id,
|
|
1578
|
+
durationMs: Math.max(0, Date.now() - mediaStart),
|
|
1579
|
+
reason: context.signal.aborted ? "cancelled" : cause instanceof DOMException && cause.name === "TimeoutError" ? "timeout" : "provider-error"
|
|
1580
|
+
});
|
|
1458
1581
|
context.signal.throwIfAborted();
|
|
1459
1582
|
reportError(cause);
|
|
1460
1583
|
return null;
|
|
1461
1584
|
}
|
|
1462
1585
|
};
|
|
1463
|
-
if (generatedVideoAvailable && context.scene.variables.mediaSource === "generate") {
|
|
1464
|
-
const generated = generatedAttempts < maxGeneratedVideos ? (generatedAttempts++, await attempt(generateVideo,
|
|
1465
|
-
if (generated) return
|
|
1586
|
+
if (generatedVideoAvailable && (!options.templates || context.scene.variables.mediaSource === "generate")) {
|
|
1587
|
+
const generated = generatedAttempts < maxGeneratedVideos ? (generatedAttempts++, await attempt(generateVideo, remainingMs)) : null;
|
|
1588
|
+
if (generated?.type === "video") return generated;
|
|
1466
1589
|
lifecycle?.reportWarning?.({
|
|
1467
1590
|
code: "provider_warning",
|
|
1468
1591
|
category: "provider",
|
|
@@ -1470,43 +1593,57 @@ function createVideoChatHandler(options) {
|
|
|
1470
1593
|
recoverable: true
|
|
1471
1594
|
});
|
|
1472
1595
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
if (
|
|
1476
|
-
const previous = completedVideos.get(reuseKey);
|
|
1477
|
-
if (previous && !reusedUrls.has(previous.url)) {
|
|
1478
|
-
reusedUrls.add(previous.url);
|
|
1479
|
-
return previous;
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1596
|
+
if (mode !== "pexels") return null;
|
|
1597
|
+
const stock = await attempt(searchMedia, Math.min(3e3, remainingMs));
|
|
1598
|
+
if (stock && (options.templates || stock.type === "video")) return stock;
|
|
1482
1599
|
return null;
|
|
1483
1600
|
} : void 0;
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1601
|
+
if (options.templates) {
|
|
1602
|
+
openingChannel.publish(void 0);
|
|
1603
|
+
return createVideoHandler({
|
|
1604
|
+
...videoOptions,
|
|
1605
|
+
authorize: "none",
|
|
1606
|
+
allowedOrigins,
|
|
1607
|
+
allowCredentials,
|
|
1608
|
+
maxBodyBytes,
|
|
1609
|
+
mediaConcurrency,
|
|
1610
|
+
resolveMedia: resolveSelected,
|
|
1611
|
+
narrate: true,
|
|
1612
|
+
basePrompt: instructions
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
const handler = createVideoStreamHandler({
|
|
1616
|
+
heartbeatMs: videoOptions.heartbeatMs,
|
|
1617
|
+
onError: videoOptions.onError,
|
|
1618
|
+
onWarning: videoOptions.onWarning,
|
|
1619
|
+
onComplete: videoOptions.onComplete,
|
|
1620
|
+
invalidPartBehavior: videoOptions.invalidPartBehavior,
|
|
1621
|
+
requireCloser: options.requireCloser ?? true,
|
|
1622
|
+
generate: createChatShotPlanner({
|
|
1623
|
+
streamText: (context) => {
|
|
1624
|
+
lifecycle = getGenerationLifecycleSink(context);
|
|
1625
|
+
return videoOptions.streamText(context);
|
|
1626
|
+
},
|
|
1627
|
+
includeRawProviderData: videoOptions.includeRawProviderData,
|
|
1628
|
+
openingLine,
|
|
1629
|
+
publishOpening: (opening) => {
|
|
1630
|
+
if (opening) diagnose({ phase: "opening-authored" });
|
|
1631
|
+
openingChannel.publish(opening);
|
|
1632
|
+
},
|
|
1633
|
+
prepareScene: (scene) => {
|
|
1634
|
+
diagnose({ phase: "shot-authored", sceneId: scene.sceneId });
|
|
1635
|
+
if (!resolveSelected) diagnose({ phase: "media-skipped", sceneId: scene.sceneId, reason: "not-configured" });
|
|
1636
|
+
preparations.publish(scene);
|
|
1637
|
+
},
|
|
1638
|
+
generatedClipDurationSec,
|
|
1639
|
+
resolveMedia: resolveSelected,
|
|
1640
|
+
mediaConcurrency
|
|
1641
|
+
}),
|
|
1502
1642
|
authorize: "none",
|
|
1503
1643
|
allowedOrigins,
|
|
1504
1644
|
allowCredentials,
|
|
1505
1645
|
maxBodyBytes,
|
|
1506
|
-
|
|
1507
|
-
basePrompt: [createVideoChatResponseInstructions(generatedVideoAvailable, openingProvided, maxGeneratedVideos), instructions?.trim()].filter(Boolean).join("\n\nAPPLICATION GUIDANCE\n"),
|
|
1508
|
-
narrate: true,
|
|
1509
|
-
resolveMedia: resolveSelected
|
|
1646
|
+
systemPrompt: [createVideoChatResponseInstructions(generatedVideoAvailable, openingProvided, maxGeneratedVideos, generatedClipDurationSec, mode), instructions?.trim()].filter(Boolean).join("\n\nAPPLICATION GUIDANCE\n")
|
|
1510
1647
|
});
|
|
1511
1648
|
return handler;
|
|
1512
1649
|
};
|
|
@@ -1601,12 +1738,12 @@ function createVideoChatHandler(options) {
|
|
|
1601
1738
|
if (audio.byteLength === 0) return jsonError2(400, "empty_audio", "No audio was provided", headers);
|
|
1602
1739
|
if (audio.byteLength > maxAudioBytes) return jsonError2(413, "body_too_large", "The recording is too large", headers);
|
|
1603
1740
|
try {
|
|
1604
|
-
const
|
|
1741
|
+
const text2 = await transcribe({
|
|
1605
1742
|
audio,
|
|
1606
1743
|
mediaType: request.headers.get("content-type") || "audio/webm",
|
|
1607
1744
|
signal: request.signal
|
|
1608
1745
|
});
|
|
1609
|
-
return Response.json({ text:
|
|
1746
|
+
return Response.json({ text: text2.trim() }, { headers });
|
|
1610
1747
|
} catch (cause) {
|
|
1611
1748
|
reportError(cause);
|
|
1612
1749
|
return jsonError2(502, "transcription_failed", "The recording could not be transcribed", headers);
|
|
@@ -1632,12 +1769,14 @@ function createVideoChatHandler(options) {
|
|
|
1632
1769
|
}
|
|
1633
1770
|
const requestId = `video-chat-${Date.now()}-${requestSequence += 1}`;
|
|
1634
1771
|
const openingChannel = createOpeningChannel(input.opening ? { line: input.opening, keyword: "" } : void 0);
|
|
1772
|
+
const preparations = createPreparationChannel();
|
|
1773
|
+
const cancellation = new AbortController();
|
|
1635
1774
|
const forwardedHeaders = new Headers(request.headers);
|
|
1636
1775
|
forwardedHeaders.delete("content-length");
|
|
1637
1776
|
const videoRequest = new Request(request.url, {
|
|
1638
1777
|
method: "POST",
|
|
1639
1778
|
headers: forwardedHeaders,
|
|
1640
|
-
signal: request.signal,
|
|
1779
|
+
signal: AbortSignal.any([request.signal, cancellation.signal]),
|
|
1641
1780
|
body: JSON.stringify({
|
|
1642
1781
|
protocolVersion: VIDEO_PROTOCOL_VERSION,
|
|
1643
1782
|
requestId,
|
|
@@ -1660,9 +1799,11 @@ function createVideoChatHandler(options) {
|
|
|
1660
1799
|
requestId,
|
|
1661
1800
|
input.opening != null,
|
|
1662
1801
|
openingChannel,
|
|
1663
|
-
input.opening
|
|
1802
|
+
input.opening,
|
|
1803
|
+
input.mode,
|
|
1804
|
+
preparations
|
|
1664
1805
|
)(videoRequest);
|
|
1665
|
-
return streamVideoChatOpening(response, openingChannel.ready);
|
|
1806
|
+
return streamVideoChatOpening(response, openingChannel.ready, preparations, () => cancellation.abort());
|
|
1666
1807
|
}
|
|
1667
1808
|
try {
|
|
1668
1809
|
if (action === "opening-media") {
|
|
@@ -1712,7 +1853,7 @@ function createVideoChatHandler(options) {
|
|
|
1712
1853
|
const prompt = boundedString2(value.prompt, "request.prompt");
|
|
1713
1854
|
const lines = Array.isArray(value.lines) ? value.lines.slice(-8).map((line, index) => boundedString2(line, `request.lines[${index}]`, 2e3)) : [];
|
|
1714
1855
|
try {
|
|
1715
|
-
const
|
|
1856
|
+
const text2 = await withDeadline((signal) => callText(
|
|
1716
1857
|
"suggestions",
|
|
1717
1858
|
VIDEO_CHAT_SUGGESTIONS_PROMPT,
|
|
1718
1859
|
`USER PROMPT: ${prompt}
|
|
@@ -1722,7 +1863,7 @@ ${lines.join("\n")}`,
|
|
|
1722
1863
|
512,
|
|
1723
1864
|
signal
|
|
1724
1865
|
), 3e3, request.signal);
|
|
1725
|
-
const subjects = readSuggestionSubjects(
|
|
1866
|
+
const subjects = readSuggestionSubjects(text2);
|
|
1726
1867
|
const mediaResolver = searchMedia;
|
|
1727
1868
|
const media = await Promise.all(subjects.map(async (subject) => {
|
|
1728
1869
|
if (!mediaResolver || !subject.keyword) return null;
|
|
@@ -1753,9 +1894,9 @@ ${lines.join("\n")}`,
|
|
|
1753
1894
|
if (!generateSpeech) return new Response(null, { status: 204, headers });
|
|
1754
1895
|
const value = record2(body, "request");
|
|
1755
1896
|
allowedKeys2(value, ["text"], "request");
|
|
1756
|
-
const
|
|
1897
|
+
const text2 = boundedString2(value.text, "request.text", 4e3);
|
|
1757
1898
|
try {
|
|
1758
|
-
const result = await withDeadline((signal) => generateSpeech({ text, signal }), 3e3, request.signal);
|
|
1899
|
+
const result = await withDeadline((signal) => generateSpeech({ text: text2, signal }), 3e3, request.signal);
|
|
1759
1900
|
return new Response(audioBody(result.audio), {
|
|
1760
1901
|
headers: {
|
|
1761
1902
|
...Object.fromEntries(headers),
|