@vanillaskyai/video 0.1.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.
- package/CHANGELOG.md +45 -0
- package/LICENSE +201 -0
- package/PUBLIC-API.md +276 -0
- package/README.md +152 -0
- package/SECURITY.md +22 -0
- package/SUPPORT.md +28 -0
- package/bin/vanillasky.js +3 -0
- package/dist/bg-confetti-WNQXS7ZS.js +89 -0
- package/dist/bg-emoji-ETI47GLS.js +150 -0
- package/dist/bg-media-SVUZAMGX.js +99 -0
- package/dist/brand-message-F3RD4P3P.js +253 -0
- package/dist/builtin-server-F7YXRVCH.js +8 -0
- package/dist/catalog-types-BIhSpOWK.d.ts +74 -0
- package/dist/chart-bar-CNSC7BNK.js +304 -0
- package/dist/chart-counter-MCRHKN77.js +299 -0
- package/dist/chart-progress-ring-RMI4A7ZK.js +296 -0
- package/dist/check-runtime.d.ts +15 -0
- package/dist/check-runtime.js +102 -0
- package/dist/chunk-2E6T633S.js +27 -0
- package/dist/chunk-352ZZCET.js +83 -0
- package/dist/chunk-73NTSFFI.js +81 -0
- package/dist/chunk-A2X45FET.js +46 -0
- package/dist/chunk-ABPKM4GK.js +187 -0
- package/dist/chunk-ABZACD4G.js +216 -0
- package/dist/chunk-BCRUUJ6A.js +756 -0
- package/dist/chunk-CHLGW22F.js +3551 -0
- package/dist/chunk-CJ7F2CE6.js +104 -0
- package/dist/chunk-CR7FE7BW.js +109 -0
- package/dist/chunk-E7CL7UPB.js +109 -0
- package/dist/chunk-EE6PK6XC.js +18 -0
- package/dist/chunk-ERY4S6QV.js +385 -0
- package/dist/chunk-EVG2ZYIY.js +125 -0
- package/dist/chunk-FNAEQ7QO.js +155 -0
- package/dist/chunk-FQ47WXAJ.js +38 -0
- package/dist/chunk-FSG2PYWG.js +336 -0
- package/dist/chunk-GHJEAP4O.js +504 -0
- package/dist/chunk-GRMUV7QY.js +262 -0
- package/dist/chunk-IFOW65Z6.js +205 -0
- package/dist/chunk-IXCUY3B3.js +786 -0
- package/dist/chunk-L5BTTCZV.js +97 -0
- package/dist/chunk-LMG3GHLL.js +107 -0
- package/dist/chunk-OOPIETLW.js +101 -0
- package/dist/chunk-P22HHFDK.js +43 -0
- package/dist/chunk-QAAO7OAY.js +19 -0
- package/dist/chunk-S7HFLYNN.js +35 -0
- package/dist/chunk-SPVTJH3F.js +24 -0
- package/dist/chunk-W2XJKG6C.js +3177 -0
- package/dist/chunk-XB7LH4J2.js +301 -0
- package/dist/chunk-XGRA2MUV.js +242 -0
- package/dist/chunk-XTOXFMLM.js +105 -0
- package/dist/chunk-YC5MFXA5.js +132 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1681 -0
- package/dist/compose-video-CN3VSWST.js +13 -0
- package/dist/cta-logo-SVA5GP2A.js +209 -0
- package/dist/cta-media-E7VLLK42.js +215 -0
- package/dist/events-_wfjbmEp.d.ts +81 -0
- package/dist/incoming-call-62MFZL3Z.js +320 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +14 -0
- package/dist/infographic-before-after-AXCDAWND.js +460 -0
- package/dist/infographic-feature-list-GW5GUE6O.js +266 -0
- package/dist/infographic-problem-solution-7NURJANA.js +340 -0
- package/dist/infographic-stat-row-J7IZR6ZN.js +241 -0
- package/dist/infographic-steps-VSZM6F5M.js +400 -0
- package/dist/kit-BMNcHqa9.d.ts +79 -0
- package/dist/prompt-input-LOUCRXCY.js +236 -0
- package/dist/react.d.ts +81 -0
- package/dist/react.js +835 -0
- package/dist/reaction-QG4CYZLQ.js +119 -0
- package/dist/server.d.ts +141 -0
- package/dist/server.js +498 -0
- package/dist/showcase-code-RUTFZR3H.js +351 -0
- package/dist/showcase-phone-6CP6XEU5.js +272 -0
- package/dist/showcase-terminal-MGAEQWPR.js +306 -0
- package/dist/showcase-web-Y4W2HVRJ.js +390 -0
- package/dist/social-conversation-75SD2637.js +695 -0
- package/dist/social-milestone-XI2QE5BO.js +292 -0
- package/dist/social-notification-2UOHQCUY.js +244 -0
- package/dist/social-review-stack-FDYZGZTJ.js +239 -0
- package/dist/social-testimonial-OQ7N52GJ.js +284 -0
- package/dist/social-tweet-5OLCNPPC.js +295 -0
- package/dist/state-DZcKuS32.d.ts +3 -0
- package/dist/system-prompt-A6UXNTPK.js +8 -0
- package/dist/template-catalog.d.ts +24 -0
- package/dist/template-catalog.js +6 -0
- package/dist/templates.d.ts +25 -0
- package/dist/templates.js +19 -0
- package/dist/test.d.ts +149 -0
- package/dist/test.js +272 -0
- package/dist/text-stream-6XHC7J47.js +10 -0
- package/dist/types-B-PZR6G_.d.ts +208 -0
- package/dist/validate-T7GBU2YF.js +8 -0
- package/docs/agent-integration.md +39 -0
- package/docs/architecture.md +84 -0
- package/docs/branding-and-personalization.md +87 -0
- package/docs/concepts.md +106 -0
- package/docs/custom-templates.md +329 -0
- package/docs/customization.md +141 -0
- package/docs/errors.md +70 -0
- package/docs/getting-started.md +111 -0
- package/docs/input-and-first-scene.md +41 -0
- package/docs/integrate-nextjs.md +79 -0
- package/docs/media-and-audio.md +69 -0
- package/docs/motion-and-effects.md +107 -0
- package/docs/persistence.md +95 -0
- package/docs/production.md +122 -0
- package/docs/prompt-and-input.md +185 -0
- package/docs/provider-integration.md +115 -0
- package/docs/reference/protocol.md +96 -0
- package/docs/reference/provider-adapters.md +158 -0
- package/docs/responsive-orientation.md +37 -0
- package/docs/security.md +38 -0
- package/docs/streaming-protocol.md +15 -0
- package/docs/testing.md +137 -0
- package/docs/use-cases.md +59 -0
- package/examples/custom-template/README.md +19 -0
- package/examples/custom-template/minimal-text.tsx +82 -0
- package/examples/custom-template/structured-data.tsx +104 -0
- package/examples/custom-template/supplied-media.tsx +92 -0
- package/examples/nextjs-quickstart/.env.example +2 -0
- package/examples/nextjs-quickstart/README.md +22 -0
- package/examples/nextjs-quickstart/next-env.d.ts +4 -0
- package/examples/nextjs-quickstart/package.json +24 -0
- package/examples/nextjs-quickstart/src/app/api/video/route.ts +22 -0
- package/examples/nextjs-quickstart/src/app/layout.tsx +5 -0
- package/examples/nextjs-quickstart/src/app/page.tsx +31 -0
- package/examples/nextjs-quickstart/tsconfig.json +26 -0
- package/package.json +167 -0
- package/registry/items/backgrounds.json +52 -0
- package/registry/items/barChart.json +237 -0
- package/registry/items/beforeAfter.json +180 -0
- package/registry/items/bigNumber.json +225 -0
- package/registry/items/brandMessage.json +158 -0
- package/registry/items/cardList.json +206 -0
- package/registry/items/chatMessenger.json +151 -0
- package/registry/items/chatWhatsapp.json +151 -0
- package/registry/items/codeEditor.json +184 -0
- package/registry/items/confetti.json +107 -0
- package/registry/items/ctaLogo.json +160 -0
- package/registry/items/ctaMedia.json +193 -0
- package/registry/items/emojiBurst.json +101 -0
- package/registry/items/incomingCall.json +170 -0
- package/registry/items/media.json +179 -0
- package/registry/items/milestone.json +205 -0
- package/registry/items/motion.json +45 -0
- package/registry/items/notification.json +182 -0
- package/registry/items/phoneMockup.json +254 -0
- package/registry/items/problemSolution.json +186 -0
- package/registry/items/progressRing.json +199 -0
- package/registry/items/promptInput.json +148 -0
- package/registry/items/reaction.json +170 -0
- package/registry/items/reviewStack.json +224 -0
- package/registry/items/steps.json +207 -0
- package/registry/items/template-context.json +31 -0
- package/registry/items/terminal.json +212 -0
- package/registry/items/testimonial.json +164 -0
- package/registry/items/theme.json +47 -0
- package/registry/items/tripleStats.json +228 -0
- package/registry/items/tweet.json +203 -0
- package/registry/items/typography.json +47 -0
- package/registry/items/webMockup.json +273 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTextDeltaVideoPlanner
|
|
3
|
+
} from "./chunk-GRMUV7QY.js";
|
|
4
|
+
import {
|
|
5
|
+
createVideo
|
|
6
|
+
} from "./chunk-IXCUY3B3.js";
|
|
7
|
+
import "./chunk-E7CL7UPB.js";
|
|
8
|
+
import {
|
|
9
|
+
BUILTIN_SERVER_TEMPLATE_KIT,
|
|
10
|
+
createServerTemplateRegistry,
|
|
11
|
+
overlayServerTemplateRegistry
|
|
12
|
+
} from "./chunk-A2X45FET.js";
|
|
13
|
+
import {
|
|
14
|
+
createTemplateSceneValidator
|
|
15
|
+
} from "./chunk-LMG3GHLL.js";
|
|
16
|
+
import {
|
|
17
|
+
encodeVideoSseEvent,
|
|
18
|
+
videoSseHeaders
|
|
19
|
+
} from "./chunk-OOPIETLW.js";
|
|
20
|
+
import "./chunk-ABPKM4GK.js";
|
|
21
|
+
import {
|
|
22
|
+
parseVideoEvent,
|
|
23
|
+
safePublicDiagnostic
|
|
24
|
+
} from "./chunk-ERY4S6QV.js";
|
|
25
|
+
import {
|
|
26
|
+
MAX_RETAINED_MEDIA_URL_LENGTH
|
|
27
|
+
} from "./chunk-GHJEAP4O.js";
|
|
28
|
+
import {
|
|
29
|
+
VIDEO_PROTOCOL_VERSION
|
|
30
|
+
} from "./chunk-EE6PK6XC.js";
|
|
31
|
+
import {
|
|
32
|
+
createTemplateSystemPrompt
|
|
33
|
+
} from "./chunk-YC5MFXA5.js";
|
|
34
|
+
import {
|
|
35
|
+
DEFAULT_VIDEO_SYSTEM_PROMPT
|
|
36
|
+
} from "./chunk-FQ47WXAJ.js";
|
|
37
|
+
import "./chunk-EVG2ZYIY.js";
|
|
38
|
+
import "./chunk-352ZZCET.js";
|
|
39
|
+
import "./chunk-CHLGW22F.js";
|
|
40
|
+
import "./chunk-2E6T633S.js";
|
|
41
|
+
import {
|
|
42
|
+
validateBrandInput
|
|
43
|
+
} from "./chunk-XGRA2MUV.js";
|
|
44
|
+
import "./chunk-73NTSFFI.js";
|
|
45
|
+
|
|
46
|
+
// src/server/request-validation.ts
|
|
47
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
48
|
+
function record(value, path) {
|
|
49
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
50
|
+
throw new Error(`${path} must be an object`);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
function allowedKeys(value, allowed, path) {
|
|
55
|
+
const permitted = new Set(allowed);
|
|
56
|
+
for (const key of Object.keys(value)) {
|
|
57
|
+
if (!permitted.has(key)) throw new Error(`${path} contains unsupported field ${key}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function string(value, path) {
|
|
61
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`${path} must be a non-empty string`);
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
function boundedString(value, path) {
|
|
65
|
+
const result = string(value, path);
|
|
66
|
+
if (result.length > MAX_RETAINED_MEDIA_URL_LENGTH) {
|
|
67
|
+
throw new Error(`${path} must be at most ${MAX_RETAINED_MEDIA_URL_LENGTH} characters`);
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
function nonNegativeInteger(value, path) {
|
|
72
|
+
if (!Number.isInteger(value) || Number(value) < 0) {
|
|
73
|
+
throw new Error(`${path} must be a non-negative integer`);
|
|
74
|
+
}
|
|
75
|
+
return Number(value);
|
|
76
|
+
}
|
|
77
|
+
function numberBetween(value, minimum, maximum, path) {
|
|
78
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < minimum || value > maximum) {
|
|
79
|
+
throw new Error(`${path} must be a number between ${minimum} and ${maximum}`);
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
function enumValue(value, allowed, description, path) {
|
|
84
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
85
|
+
throw new Error(`${path} must be ${description}`);
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
function jsonValue(value, path, seen = /* @__PURE__ */ new Set()) {
|
|
90
|
+
if (value == null || typeof value === "string" || typeof value === "boolean") return;
|
|
91
|
+
if (typeof value === "number") {
|
|
92
|
+
if (!Number.isFinite(value)) throw new Error(`${path} must be finite`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (typeof value !== "object" || seen.has(value)) throw new Error(`${path} must be JSON-serializable`);
|
|
96
|
+
seen.add(value);
|
|
97
|
+
if (Array.isArray(value)) {
|
|
98
|
+
value.forEach((item, index) => jsonValue(item, `${path}[${index}]`, seen));
|
|
99
|
+
} else {
|
|
100
|
+
for (const [key, child] of Object.entries(value)) {
|
|
101
|
+
if (DANGEROUS_KEYS.has(key)) throw new Error(`${path} contains unsupported field ${key}`);
|
|
102
|
+
jsonValue(child, `${path}.${key}`, seen);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
seen.delete(value);
|
|
106
|
+
}
|
|
107
|
+
function capabilities(value, path) {
|
|
108
|
+
const result = record(value, path);
|
|
109
|
+
allowedKeys(result, ["templates", "extensions"], path);
|
|
110
|
+
for (const key of ["templates", "extensions"]) {
|
|
111
|
+
const entries = result[key];
|
|
112
|
+
if (entries == null) continue;
|
|
113
|
+
if (!Array.isArray(entries)) throw new Error(`${path}.${key} must be an array`);
|
|
114
|
+
entries.forEach((entry, index) => {
|
|
115
|
+
const item = string(entry, `${path}.${key}[${index}]`);
|
|
116
|
+
if (key === "extensions" && (!item.startsWith("data.") || item === "data.")) {
|
|
117
|
+
throw new Error(`${path}.${key}[${index}] must use the data.* namespace`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function parseVideoRequest(value) {
|
|
124
|
+
const request = record(value, "request");
|
|
125
|
+
allowedKeys(request, ["protocolVersion", "requestId", "input", "capabilities", "resume"], "request");
|
|
126
|
+
if (request.protocolVersion !== VIDEO_PROTOCOL_VERSION) {
|
|
127
|
+
throw new Error(`request.protocolVersion must be ${VIDEO_PROTOCOL_VERSION}`);
|
|
128
|
+
}
|
|
129
|
+
string(request.requestId, "request.requestId");
|
|
130
|
+
const input = record(request.input, "request.input");
|
|
131
|
+
allowedKeys(input, [
|
|
132
|
+
"input",
|
|
133
|
+
"instructions",
|
|
134
|
+
"maxDurationSec",
|
|
135
|
+
"orientation",
|
|
136
|
+
"style",
|
|
137
|
+
"opening",
|
|
138
|
+
"brand",
|
|
139
|
+
"personalization",
|
|
140
|
+
"suppliedMedia",
|
|
141
|
+
"audio"
|
|
142
|
+
], "request.input");
|
|
143
|
+
string(input.input, "request.input.input");
|
|
144
|
+
if (input.instructions != null) string(input.instructions, "request.input.instructions");
|
|
145
|
+
if (input.maxDurationSec != null) {
|
|
146
|
+
numberBetween(input.maxDurationSec, 5, 120, "request.input.maxDurationSec");
|
|
147
|
+
}
|
|
148
|
+
if (input.orientation != null) {
|
|
149
|
+
enumValue(
|
|
150
|
+
input.orientation,
|
|
151
|
+
["portrait", "landscape"],
|
|
152
|
+
"portrait or landscape",
|
|
153
|
+
"request.input.orientation"
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (input.opening != null) string(input.opening, "request.input.opening");
|
|
157
|
+
if (input.audio != null && input.audio !== false) {
|
|
158
|
+
const soundtrack = record(input.audio, "request.input.audio");
|
|
159
|
+
allowedKeys(soundtrack, ["src"], "request.input.audio");
|
|
160
|
+
boundedString(soundtrack.src, "request.input.audio.src");
|
|
161
|
+
}
|
|
162
|
+
if (input.suppliedMedia != null) {
|
|
163
|
+
if (!Array.isArray(input.suppliedMedia)) throw new Error("request.input.suppliedMedia must be an array");
|
|
164
|
+
input.suppliedMedia.forEach((entry, index) => {
|
|
165
|
+
const path = `request.input.suppliedMedia[${index}]`;
|
|
166
|
+
const media = record(entry, path);
|
|
167
|
+
allowedKeys(media, [
|
|
168
|
+
"id",
|
|
169
|
+
"url",
|
|
170
|
+
"type",
|
|
171
|
+
"description",
|
|
172
|
+
"mimeType",
|
|
173
|
+
"posterUrl",
|
|
174
|
+
"focalPoint",
|
|
175
|
+
"treatment",
|
|
176
|
+
"role"
|
|
177
|
+
], path);
|
|
178
|
+
string(media.id, `${path}.id`);
|
|
179
|
+
boundedString(media.url, `${path}.url`);
|
|
180
|
+
if (media.posterUrl != null) boundedString(media.posterUrl, `${path}.posterUrl`);
|
|
181
|
+
if (media.type !== "image" && media.type !== "video") {
|
|
182
|
+
throw new Error(`${path}.type must be image or video`);
|
|
183
|
+
}
|
|
184
|
+
for (const field of ["description", "mimeType"]) {
|
|
185
|
+
if (media[field] != null) string(media[field], `${path}.${field}`);
|
|
186
|
+
}
|
|
187
|
+
if (media.focalPoint != null) {
|
|
188
|
+
enumValue(
|
|
189
|
+
media.focalPoint,
|
|
190
|
+
["center", "top", "bottom", "left", "right"],
|
|
191
|
+
"center, top, bottom, left, or right",
|
|
192
|
+
`${path}.focalPoint`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (media.treatment != null) {
|
|
196
|
+
enumValue(
|
|
197
|
+
media.treatment,
|
|
198
|
+
["subtle", "cinematic", "text-safe"],
|
|
199
|
+
"subtle, cinematic, or text-safe",
|
|
200
|
+
`${path}.treatment`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (media.role != null) {
|
|
204
|
+
enumValue(
|
|
205
|
+
media.role,
|
|
206
|
+
["product", "proof", "background", "logo"],
|
|
207
|
+
"product, proof, background, or logo",
|
|
208
|
+
`${path}.role`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (input.style != null) {
|
|
214
|
+
const visual = record(input.style, "request.input.style");
|
|
215
|
+
allowedKeys(visual, ["density", "motion", "textArchetype", "backgroundEffect"], "request.input.style");
|
|
216
|
+
if (visual.density != null) {
|
|
217
|
+
enumValue(visual.density, ["airy", "normal", "packed"], "airy, normal, or packed", "request.input.style.density");
|
|
218
|
+
}
|
|
219
|
+
if (visual.motion != null) {
|
|
220
|
+
enumValue(visual.motion, ["calm", "normal", "punchy"], "calm, normal, or punchy", "request.input.style.motion");
|
|
221
|
+
}
|
|
222
|
+
if (visual.textArchetype != null) {
|
|
223
|
+
enumValue(
|
|
224
|
+
visual.textArchetype,
|
|
225
|
+
["subtle", "typewriter", "wordStagger", "slam", "cinematic", "heroWord"],
|
|
226
|
+
"subtle, typewriter, wordStagger, slam, cinematic, or heroWord",
|
|
227
|
+
"request.input.style.textArchetype"
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (visual.backgroundEffect != null) {
|
|
231
|
+
enumValue(
|
|
232
|
+
visual.backgroundEffect,
|
|
233
|
+
["static", "slow-zoom-in", "slow-zoom-out", "ken-burns", "drift", "pulse", "breathe", "slow-tilt", "camera-shake"],
|
|
234
|
+
"static, slow-zoom-in, slow-zoom-out, ken-burns, drift, pulse, breathe, slow-tilt, or camera-shake",
|
|
235
|
+
"request.input.style.backgroundEffect"
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (input.brand != null) validateBrandInput(input.brand, "request.input.brand");
|
|
240
|
+
jsonValue(input, "request.input");
|
|
241
|
+
if (request.capabilities != null) capabilities(request.capabilities, "request.capabilities");
|
|
242
|
+
if (request.resume != null) {
|
|
243
|
+
const resume = record(request.resume, "request.resume");
|
|
244
|
+
allowedKeys(resume, ["runId", "afterSequence"], "request.resume");
|
|
245
|
+
string(resume.runId, "request.resume.runId");
|
|
246
|
+
nonNegativeInteger(resume.afterSequence, "request.resume.afterSequence");
|
|
247
|
+
}
|
|
248
|
+
return request;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/server/video-stream-handler.ts
|
|
252
|
+
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
253
|
+
var DEFAULT_HEARTBEAT_MS = 15e3;
|
|
254
|
+
var SECRET_FIELD = /(?:api[_-]?key|authorization|admin[_-]?session|secret(?:[_-]?key)?)$/i;
|
|
255
|
+
function jsonError(status, code, message, headers) {
|
|
256
|
+
return Response.json({ error: { code, message } }, { status, headers });
|
|
257
|
+
}
|
|
258
|
+
function corsHeaders(origin, allowedOrigins, allowCredentials = false) {
|
|
259
|
+
const headers = new Headers({ "Cache-Control": "no-store", Vary: "Origin" });
|
|
260
|
+
if (origin && allowedOrigins?.includes(origin)) {
|
|
261
|
+
headers.set("Access-Control-Allow-Origin", origin);
|
|
262
|
+
if (allowCredentials) headers.set("Access-Control-Allow-Credentials", "true");
|
|
263
|
+
}
|
|
264
|
+
return headers;
|
|
265
|
+
}
|
|
266
|
+
function containsSecretField(value, seen = /* @__PURE__ */ new Set()) {
|
|
267
|
+
if (!value || typeof value !== "object" || seen.has(value)) return false;
|
|
268
|
+
seen.add(value);
|
|
269
|
+
if (Array.isArray(value)) return value.some((item) => containsSecretField(item, seen));
|
|
270
|
+
return Object.entries(value).some(([key, child]) => SECRET_FIELD.test(key) || containsSecretField(child, seen));
|
|
271
|
+
}
|
|
272
|
+
function errorFrom(cause) {
|
|
273
|
+
return cause instanceof Error ? cause : new Error(String(cause));
|
|
274
|
+
}
|
|
275
|
+
function defaultRunId(request) {
|
|
276
|
+
return `run-${request.requestId}`;
|
|
277
|
+
}
|
|
278
|
+
function negotiateCapabilities(requested, supported) {
|
|
279
|
+
if (!supported) return requested;
|
|
280
|
+
if (!requested) return supported;
|
|
281
|
+
const intersect = (left, right) => {
|
|
282
|
+
if (right == null) return left;
|
|
283
|
+
if (left == null) return right;
|
|
284
|
+
const permitted = new Set(right);
|
|
285
|
+
return left.filter((value) => permitted.has(value));
|
|
286
|
+
};
|
|
287
|
+
return {
|
|
288
|
+
...requested.templates != null || supported.templates != null ? { templates: intersect(requested.templates, supported.templates) } : {},
|
|
289
|
+
...requested.extensions != null || supported.extensions != null ? { extensions: intersect(requested.extensions, supported.extensions) } : {}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function createVideoStreamHandler(options) {
|
|
293
|
+
if (!options || typeof options.generate !== "function") {
|
|
294
|
+
throw new Error("createVideoStreamHandler requires a planner");
|
|
295
|
+
}
|
|
296
|
+
if (options.authorize !== "none" && typeof options.authorize !== "function") {
|
|
297
|
+
throw new Error('createVideoStreamHandler requires authorize or authorize: "none"');
|
|
298
|
+
}
|
|
299
|
+
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
300
|
+
if (!Number.isFinite(maxBodyBytes) || maxBodyBytes <= 0) throw new Error("maxBodyBytes must be positive");
|
|
301
|
+
const heartbeatMs = options.heartbeatMs === false ? false : options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
|
|
302
|
+
if (heartbeatMs !== false && (!Number.isFinite(heartbeatMs) || heartbeatMs <= 0)) {
|
|
303
|
+
throw new Error("heartbeatMs must be positive or false");
|
|
304
|
+
}
|
|
305
|
+
const reportError = (error) => {
|
|
306
|
+
try {
|
|
307
|
+
void Promise.resolve(options.onError?.(error)).catch(() => void 0);
|
|
308
|
+
} catch {
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
return async function handle(request) {
|
|
312
|
+
const origin = request.headers.get("origin");
|
|
313
|
+
const baseHeaders = corsHeaders(origin, options.allowedOrigins, options.allowCredentials);
|
|
314
|
+
if (request.method === "OPTIONS") {
|
|
315
|
+
if (origin && options.allowedOrigins && !options.allowedOrigins.includes(origin)) {
|
|
316
|
+
return jsonError(403, "origin_forbidden", "Origin is not allowed", baseHeaders);
|
|
317
|
+
}
|
|
318
|
+
baseHeaders.set("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
319
|
+
baseHeaders.set("Access-Control-Allow-Headers", "Authorization, Content-Type, Last-Event-ID");
|
|
320
|
+
return new Response(null, { status: 204, headers: baseHeaders });
|
|
321
|
+
}
|
|
322
|
+
if (request.method !== "POST") {
|
|
323
|
+
baseHeaders.set("Allow", "POST, OPTIONS");
|
|
324
|
+
return jsonError(405, "method_not_allowed", "Use POST", baseHeaders);
|
|
325
|
+
}
|
|
326
|
+
if (origin && options.allowedOrigins && !options.allowedOrigins.includes(origin)) {
|
|
327
|
+
return jsonError(403, "origin_forbidden", "Origin is not allowed", baseHeaders);
|
|
328
|
+
}
|
|
329
|
+
if (options.authorize !== "none") {
|
|
330
|
+
let authorized = false;
|
|
331
|
+
try {
|
|
332
|
+
authorized = await options.authorize(request);
|
|
333
|
+
} catch (cause) {
|
|
334
|
+
reportError(errorFrom(cause));
|
|
335
|
+
}
|
|
336
|
+
if (!authorized) return jsonError(401, "unauthorized", "Authentication required", baseHeaders);
|
|
337
|
+
}
|
|
338
|
+
const declaredLength = Number(request.headers.get("content-length"));
|
|
339
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBodyBytes) {
|
|
340
|
+
return jsonError(413, "body_too_large", "Request body is too large", baseHeaders);
|
|
341
|
+
}
|
|
342
|
+
let rawBody;
|
|
343
|
+
try {
|
|
344
|
+
rawBody = await request.text();
|
|
345
|
+
} catch {
|
|
346
|
+
return jsonError(400, "invalid_body", "Request body could not be read", baseHeaders);
|
|
347
|
+
}
|
|
348
|
+
if (new TextEncoder().encode(rawBody).byteLength > maxBodyBytes) {
|
|
349
|
+
return jsonError(413, "body_too_large", "Request body is too large", baseHeaders);
|
|
350
|
+
}
|
|
351
|
+
let body;
|
|
352
|
+
try {
|
|
353
|
+
body = JSON.parse(rawBody);
|
|
354
|
+
} catch {
|
|
355
|
+
return jsonError(400, "invalid_json", "Request body must be valid JSON", baseHeaders);
|
|
356
|
+
}
|
|
357
|
+
if (containsSecretField(body)) {
|
|
358
|
+
return jsonError(
|
|
359
|
+
400,
|
|
360
|
+
"secret_field",
|
|
361
|
+
"Provider credentials belong in server environment variables, not the request body",
|
|
362
|
+
baseHeaders
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
let motionRequest;
|
|
366
|
+
try {
|
|
367
|
+
motionRequest = parseVideoRequest(body);
|
|
368
|
+
} catch (cause) {
|
|
369
|
+
return jsonError(
|
|
370
|
+
400,
|
|
371
|
+
"invalid_request",
|
|
372
|
+
safePublicDiagnostic(errorFrom(cause).message, "Video request is invalid"),
|
|
373
|
+
baseHeaders
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
if (motionRequest.resume) {
|
|
377
|
+
const expectedLastEventId = `${motionRequest.resume.runId}:${motionRequest.resume.afterSequence}`;
|
|
378
|
+
if (request.headers.get("last-event-id") !== expectedLastEventId) {
|
|
379
|
+
return jsonError(400, "invalid_cursor", "Last-Event-ID does not match the resume cursor", baseHeaders);
|
|
380
|
+
}
|
|
381
|
+
if (!options.replay) {
|
|
382
|
+
return jsonError(409, "resume_unavailable", "This endpoint does not have replay storage configured", baseHeaders);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const negotiatedCapabilities = negotiateCapabilities(
|
|
386
|
+
motionRequest.capabilities,
|
|
387
|
+
options.supportedCapabilities
|
|
388
|
+
);
|
|
389
|
+
const systemPrompt = typeof options.systemPrompt === "function" ? options.systemPrompt({ request: motionRequest, capabilities: negotiatedCapabilities }) : options.systemPrompt;
|
|
390
|
+
let run;
|
|
391
|
+
if (!motionRequest.resume) {
|
|
392
|
+
try {
|
|
393
|
+
run = createVideo(motionRequest.input, {
|
|
394
|
+
requestId: motionRequest.requestId,
|
|
395
|
+
runId: (options.createRunId ?? defaultRunId)(motionRequest),
|
|
396
|
+
capabilities: negotiatedCapabilities,
|
|
397
|
+
validateScene: options.validateScene,
|
|
398
|
+
getTemplatePacing: options.getTemplatePacing,
|
|
399
|
+
invalidPartBehavior: options.invalidPartBehavior ?? "drop",
|
|
400
|
+
onError: options.onError,
|
|
401
|
+
onWarning: options.onWarning,
|
|
402
|
+
onComplete: options.onComplete,
|
|
403
|
+
systemPrompt: systemPrompt ?? DEFAULT_VIDEO_SYSTEM_PROMPT,
|
|
404
|
+
selectAudio: options.selectAudio,
|
|
405
|
+
snapshotRetention: options.snapshotRetention,
|
|
406
|
+
signal: request.signal,
|
|
407
|
+
generate: options.generate
|
|
408
|
+
});
|
|
409
|
+
} catch (cause) {
|
|
410
|
+
reportError(errorFrom(cause));
|
|
411
|
+
return jsonError(400, "invalid_input", "Video input is invalid", baseHeaders);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const source = motionRequest.resume ? options.replay(motionRequest.resume, { request: motionRequest, signal: request.signal }) : run.stream;
|
|
415
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
416
|
+
let replaySequence = motionRequest.resume?.afterSequence;
|
|
417
|
+
const encoder = new TextEncoder();
|
|
418
|
+
let heartbeat;
|
|
419
|
+
let closed = false;
|
|
420
|
+
const stream = new ReadableStream({
|
|
421
|
+
start(controller) {
|
|
422
|
+
if (heartbeatMs !== false) {
|
|
423
|
+
heartbeat = setInterval(() => {
|
|
424
|
+
if (!closed && (controller.desiredSize ?? 0) > 0) {
|
|
425
|
+
controller.enqueue(encoder.encode(": ping\n\n"));
|
|
426
|
+
}
|
|
427
|
+
}, heartbeatMs);
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
async pull(controller) {
|
|
431
|
+
const next = await iterator.next();
|
|
432
|
+
if (next.done) {
|
|
433
|
+
closed = true;
|
|
434
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
435
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
436
|
+
controller.close();
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const event = parseVideoEvent(next.value);
|
|
440
|
+
if (motionRequest.resume) {
|
|
441
|
+
if (event.runId !== motionRequest.resume.runId || event.sequence !== replaySequence + 1) {
|
|
442
|
+
throw new Error("Replay storage returned an event outside the requested cursor");
|
|
443
|
+
}
|
|
444
|
+
replaySequence = event.sequence;
|
|
445
|
+
}
|
|
446
|
+
controller.enqueue(encoder.encode(encodeVideoSseEvent(event)));
|
|
447
|
+
},
|
|
448
|
+
async cancel(reason) {
|
|
449
|
+
closed = true;
|
|
450
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
451
|
+
run?.abort(typeof reason === "string" ? reason : "client disconnected");
|
|
452
|
+
await iterator.return?.();
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
return new Response(stream, {
|
|
456
|
+
status: 200,
|
|
457
|
+
headers: videoSseHeaders(baseHeaders)
|
|
458
|
+
});
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/server/create-video-handler.ts
|
|
463
|
+
function createVideoHandler(options) {
|
|
464
|
+
if (options?.authorize !== "none" && typeof options?.authorize !== "function") {
|
|
465
|
+
throw new Error('createVideoHandler requires authorize or authorize: "none"');
|
|
466
|
+
}
|
|
467
|
+
if (options && "mediaPolicy" in options) {
|
|
468
|
+
throw new Error("mediaPolicy is not supported; resolve media before generation and pass it through VideoInput.suppliedMedia");
|
|
469
|
+
}
|
|
470
|
+
const {
|
|
471
|
+
templates: configuredTemplates,
|
|
472
|
+
streamText,
|
|
473
|
+
includeRawProviderData,
|
|
474
|
+
basePrompt,
|
|
475
|
+
allowMediaUrl,
|
|
476
|
+
...handlerOptions
|
|
477
|
+
} = options;
|
|
478
|
+
const templates = configuredTemplates ? overlayServerTemplateRegistry(BUILTIN_SERVER_TEMPLATE_KIT, configuredTemplates) : BUILTIN_SERVER_TEMPLATE_KIT;
|
|
479
|
+
return createVideoStreamHandler({
|
|
480
|
+
...handlerOptions,
|
|
481
|
+
generate: createTextDeltaVideoPlanner({ streamText, includeRawProviderData }),
|
|
482
|
+
systemPrompt: ({ capabilities: capabilities2 }) => {
|
|
483
|
+
const selectedIds = capabilities2?.templates == null ? void 0 : new Set(capabilities2.templates);
|
|
484
|
+
const selectedTemplates = selectedIds == null ? templates.listTemplateMetadata() : templates.listTemplateMetadata().filter(({ id }) => selectedIds.has(id));
|
|
485
|
+
return createTemplateSystemPrompt({
|
|
486
|
+
kit: { listTemplateMetadata: () => selectedTemplates },
|
|
487
|
+
basePrompt
|
|
488
|
+
});
|
|
489
|
+
},
|
|
490
|
+
supportedCapabilities: templates.capabilities,
|
|
491
|
+
validateScene: createTemplateSceneValidator({ kit: templates, allowMediaUrl }),
|
|
492
|
+
getTemplatePacing: (templateId) => templates.getTemplateMetadata(templateId)
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
export {
|
|
496
|
+
createServerTemplateRegistry,
|
|
497
|
+
createVideoHandler
|
|
498
|
+
};
|