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