@vanillaskyai/video 0.10.3 → 0.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/PUBLIC-API.md +43 -31
- package/README.md +11 -7
- package/dist/check-runtime.js +2 -2
- package/dist/{chunk-UCXQORDZ.js → chunk-G5TYLTL5.js} +1 -1
- package/dist/{chunk-SI6CFFAA.js → chunk-O6FBIB4L.js} +6 -5
- package/dist/{chunk-YZZSWGC3.js → chunk-OOBT4X46.js} +8 -3
- package/dist/{chunk-LJR5VLEI.js → chunk-QZAZT44G.js} +2 -2
- package/dist/{chunk-3U2F7C7C.js → chunk-WZFLEPLM.js} +7 -3
- package/dist/{chunk-VVNZCY2U.js → chunk-Z2ZI5G7O.js} +1 -1
- package/dist/{cinema-media-4JUYKPQI.js → cinema-media-3SIGVVCY.js} +4 -4
- package/dist/{comparison-3QLMUC5V.js → comparison-VDGRWPB5.js} +4 -4
- package/dist/{editorial-timeline-IVE2P25N.js → editorial-timeline-QJU4JJWB.js} +4 -4
- package/dist/{key-figure-APD56LCO.js → key-figure-YUAQW6OE.js} +4 -4
- package/dist/{mobile-message-WLK3WY5B.js → mobile-message-ZMMTUDUQ.js} +4 -4
- package/dist/{quote-Z6YET5BO.js → quote-BB6UJT6L.js} +4 -4
- package/dist/react.d.ts +3 -1
- package/dist/react.js +79 -81
- package/dist/{scene-video-backdrop-OWUXPXHZ.js → scene-video-backdrop-4EZVVQY7.js} +2 -2
- package/dist/server.d.ts +15 -3
- package/dist/server.js +129 -33
- package/dist/{types-CdBmNTGD.d.ts → types-BqB8zC9u.d.ts} +1 -1
- package/docs/concepts.md +2 -2
- package/docs/development.md +23 -0
- package/docs/getting-started.md +2 -2
- package/docs/media-and-audio.md +26 -13
- package/docs/performance.md +21 -10
- package/docs/production.md +11 -13
- package/docs/prompt-and-input.md +3 -3
- package/docs/provider-integration.md +9 -11
- package/docs/reference/provider-adapters.md +3 -2
- package/package.json +5 -2
- package/registry/items/backgrounds.json +2 -2
- package/starters/video-chat/README.md +13 -15
- package/starters/video-chat/package.json +1 -1
- package/starters/video-chat/providers/video.ts +5 -2
- package/starters/video-chat/server.ts +1 -1
- package/starters/video-chat/stock.ts +59 -31
- package/styles/video-chat.css +6 -61
package/dist/server.js
CHANGED
|
@@ -895,15 +895,16 @@ var object = (value) => value && typeof value === "object" && !Array.isArray(val
|
|
|
895
895
|
function text(value, maximum) {
|
|
896
896
|
return typeof value === "string" && value.trim().length <= maximum ? value.trim() : "";
|
|
897
897
|
}
|
|
898
|
-
function readShot(value) {
|
|
898
|
+
function readShot(value, clipDurationSec) {
|
|
899
899
|
const item = object(value);
|
|
900
900
|
const narration = text(item?.narration, 2e3);
|
|
901
901
|
if (!narration) throw new Error("Chat shot requires bounded authored narration");
|
|
902
902
|
return {
|
|
903
903
|
narration,
|
|
904
|
+
title: text(item?.title, 65) || text(item?.subject, 65) || "The next step",
|
|
904
905
|
subject: text(item?.subject, 80),
|
|
905
906
|
action: text(item?.action, 600),
|
|
906
|
-
durationSec: typeof item?.durationSec === "number" && Number.isFinite(item.durationSec) ? Math.min(
|
|
907
|
+
durationSec: typeof item?.durationSec === "number" && Number.isFinite(item.durationSec) ? Math.min(clipDurationSec, Math.max(2, item.durationSec)) : clipDurationSec,
|
|
907
908
|
continuity: item?.continuity === "continue" ? "continue" : "cut"
|
|
908
909
|
};
|
|
909
910
|
}
|
|
@@ -916,12 +917,13 @@ function replaceStream(source, textStream) {
|
|
|
916
917
|
});
|
|
917
918
|
}
|
|
918
919
|
function createChatShotPlanner(options) {
|
|
920
|
+
const clipDurationSec = options.generatedClipDurationSec ?? 5;
|
|
919
921
|
const incomplete = /* @__PURE__ */ new WeakSet();
|
|
920
922
|
const planner = createTextDeltaVideoPlanner({
|
|
921
923
|
includeRawProviderData: options.includeRawProviderData,
|
|
922
924
|
streamText(context) {
|
|
923
925
|
const providerContext = { ...context, userPrompt: [
|
|
924
|
-
`Create a complete answer within ${context.request.input.maxDurationSec ?? 40} seconds. Each generated clip has at most
|
|
926
|
+
`Create a complete answer within ${context.request.input.maxDurationSec ?? 40} seconds. Each generated clip has at most ${clipDurationSec} seconds; give each spoken beat room to finish.`,
|
|
925
927
|
`Orientation: ${context.request.input.orientation ?? "landscape"}.`,
|
|
926
928
|
"USER REQUEST AND CONVERSATION",
|
|
927
929
|
context.request.input.input
|
|
@@ -952,7 +954,7 @@ function createChatShotPlanner(options) {
|
|
|
952
954
|
return { type: "scene.add", ...closer ? { placement: "closer" } : {}, scene: {
|
|
953
955
|
id: `${context.request.requestId}-shot-${++index}`,
|
|
954
956
|
templateId: "cinemaMedia",
|
|
955
|
-
variables: { mediaType: "video", mediaKeyword: shot.subject, shotDirection: [
|
|
957
|
+
variables: { fallbackText: shot.title, mediaType: "video", mediaKeyword: shot.subject, shotDirection: [
|
|
956
958
|
brief?.visualDirection,
|
|
957
959
|
shot.action,
|
|
958
960
|
shot.continuity === "continue" ? "Continue the established subject, setting and action consistently." : "A deliberate new shot; choose framing that reveals this beat.",
|
|
@@ -971,7 +973,7 @@ function createChatShotPlanner(options) {
|
|
|
971
973
|
brief = { opening: text(part.opening, 300), subject: text(part.subject, 80), visualDirection: text(part.visualDirection, 600), development: text(part.development, 2e3) };
|
|
972
974
|
if (part.ending) {
|
|
973
975
|
try {
|
|
974
|
-
brief.ending = readShot(part.ending);
|
|
976
|
+
brief.ending = readShot(part.ending, clipDurationSec);
|
|
975
977
|
} catch (cause) {
|
|
976
978
|
reject(cause);
|
|
977
979
|
}
|
|
@@ -981,10 +983,10 @@ function createChatShotPlanner(options) {
|
|
|
981
983
|
}
|
|
982
984
|
if (part?.type !== "shot") throw new Error("Chat plan requires an answer brief followed by shots");
|
|
983
985
|
if (!brief) throw new Error("Chat shot arrived before its answer brief");
|
|
984
|
-
const shot = readShot(part);
|
|
986
|
+
const shot = readShot(part, clipDurationSec);
|
|
985
987
|
if (shot.narration === brief.ending?.narration) return;
|
|
986
988
|
if (firstBody && !continueAfterOpening(shot.narration, [options.openingLine ?? brief.opening])) return;
|
|
987
|
-
const budget = (context.request.input.maxDurationSec ?? 40) - (brief.ending?.durationSec ??
|
|
989
|
+
const budget = (context.request.input.maxDurationSec ?? 40) - (brief.ending?.durationSec ?? clipDurationSec);
|
|
988
990
|
if (bodyDuration + shot.durationSec > budget) throw new Error("Chat shot exceeds the answer duration budget");
|
|
989
991
|
bodyDuration += shot.durationSec;
|
|
990
992
|
return scenePart(shot);
|
|
@@ -1051,6 +1053,7 @@ async function* resolveShots(parts, context, options) {
|
|
|
1051
1053
|
let notify, space;
|
|
1052
1054
|
const resolve = async (part) => {
|
|
1053
1055
|
if (part.type !== "scene.add") return part;
|
|
1056
|
+
options.prepareScene?.({ sceneId: part.scene.id, narration: part.scene.narration ?? "" });
|
|
1054
1057
|
const { mediaKeyword } = part.scene.variables;
|
|
1055
1058
|
let media;
|
|
1056
1059
|
if (typeof mediaKeyword === "string" && mediaKeyword && options.resolveMedia) media = await options.resolveMedia(mediaKeyword, {
|
|
@@ -1063,8 +1066,9 @@ async function* resolveShots(parts, context, options) {
|
|
|
1063
1066
|
signal: context.signal
|
|
1064
1067
|
});
|
|
1065
1068
|
context.signal.throwIfAborted();
|
|
1066
|
-
if (!media) getGenerationLifecycleSink(context)?.reportWarning?.({ code: "provider_warning", category: "provider", message:
|
|
1067
|
-
const
|
|
1069
|
+
if (!media) getGenerationLifecycleSink(context)?.reportWarning?.({ code: "provider_warning", category: "provider", message: MEDIA_RECOVERY_NOTICE, recoverable: true });
|
|
1070
|
+
const title = part.scene.variables.fallbackText;
|
|
1071
|
+
const scene = media ? { ...part.scene, variables: { fallbackText: title, mediaType: media.type === "image" ? "photo" : "video", mediaUrl: media.url, ...media.posterUrl ? { mediaPoster: media.posterUrl } : {} } } : { ...part.scene, templateId: "chapterTitle", variables: { title } };
|
|
1068
1072
|
return { ...part, scene };
|
|
1069
1073
|
};
|
|
1070
1074
|
const producer = (async () => {
|
|
@@ -1112,16 +1116,16 @@ async function* resolveShots(parts, context, options) {
|
|
|
1112
1116
|
}
|
|
1113
1117
|
|
|
1114
1118
|
// src/server/video-chat-prompts.ts
|
|
1115
|
-
function createVideoChatResponseInstructions(generatedVideoAvailable, openingAlreadyProvided = false, maxGeneratedVideos = 5) {
|
|
1119
|
+
function createVideoChatResponseInstructions(generatedVideoAvailable, openingAlreadyProvided = false, maxGeneratedVideos = 5, clipDurationSec = 5, mode = "cinematic") {
|
|
1116
1120
|
return [
|
|
1117
1121
|
"Write a complete, intentful video answer as newline-delimited JSON. Match the user's form and tone; mixed intents can combine directions.",
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
`The
|
|
1122
|
+
`First write one brief: {"type":"answer","intent":"explanation|story|comedy|imagination|practical","opening":"a short inviting spoken introduction of 6\u20139 words","subject":"literal visual subject","development":"the essential development of this answer","visualDirection":"consistent subjects, appearance and visual approach","ending":{"title":"short meaningful chapter title, at most 65 characters","narration":"the authored payoff","subject":"literal subject","action":"visible action or change","durationSec":${clipDurationSec},"continuity":"cut|continue"}}.`,
|
|
1123
|
+
`Then stream each developing shot on its own line: {"type":"shot","title":"short meaningful chapter title, at most 65 characters","narration":"the exact spoken beat","subject":"2\u20138 literal filmable words, at most 80 characters","action":"concrete subject, action or visible change and useful framing","durationSec":${clipDurationSec},"continuity":"cut|continue"}.`,
|
|
1124
|
+
`The selected footage mode is ${mode === "pexels" ? "Pexels stock search: use literal filmable subjects; never imply stock proves a mechanism or depicts fictional events exactly" : `AI video, with at most ${generatedVideoAvailable ? maxGeneratedVideos : 0} generation attempts`}. Missing footage becomes the authored chapter title, with complete narration. Preserve the full answer rather than shortening it to fit credits. The host selects providers; do not make source choices.`,
|
|
1121
1125
|
"For a very short answer whose ending alone fulfills the request, development may be empty and no developing shots are needed. Otherwise, develop the essential content before the ending.",
|
|
1122
1126
|
"The brief's ending is saved and played after your developing shots. Do not repeat it as a shot. Stop writing after the last developing shot. No technical events, identifiers, template choices, media providers, URLs or extra fields.",
|
|
1123
1127
|
"Every shot uses moving footage with separate narration and subtitles. Generated footage is silent: do not ask its subjects to speak or render words. No headline cards or on-screen explanatory text.",
|
|
1124
|
-
|
|
1128
|
+
`Each clip has at most ${clipDurationSec} seconds. Write spoken beats that fit naturally, usually ${Math.floor(clipDurationSec * 1.6)}\u2013${Math.floor(clipDurationSec * 2)} words per shot. Split longer ideas across purposeful shots, preserving facts and qualifiers. Never truncate a claim to meet a word target. Use only the shots needed within the total duration, including the ending; do not pad to a fixed count.`,
|
|
1125
1129
|
"Identify the full answer and its ending before developing shots. Each action must support what is said: camera movement alone is not progression. Vary scale, viewpoint and meaningful details while keeping subjects consistent.",
|
|
1126
1130
|
"Explanations: clarify the actual causal mechanism, separating physical cause from a metaphor. Generated cutaways and animation illustrate ideas; they are not factual evidence. Preserve uncertainty, quantities and conditions; never invent evidence or quotations.",
|
|
1127
1131
|
"Stories: portray characters making choices and experiencing consequences; use consistent character descriptions and an earned resolution, not a promised next scene.",
|
|
@@ -1221,8 +1225,8 @@ function parseResponseRequest(value) {
|
|
|
1221
1225
|
const body = record2(value, "request");
|
|
1222
1226
|
allowedKeys2(body, ["prompt", "opening", "mode", "orientation", "conversation", "style"], "request");
|
|
1223
1227
|
const mode = body.mode ?? "cinematic";
|
|
1224
|
-
if (mode !== "cinematic") {
|
|
1225
|
-
throw new Error("request.mode must be cinematic");
|
|
1228
|
+
if (mode !== "cinematic" && mode !== "pexels") {
|
|
1229
|
+
throw new Error("request.mode must be cinematic or pexels");
|
|
1226
1230
|
}
|
|
1227
1231
|
const orientation = body.orientation ?? "landscape";
|
|
1228
1232
|
if (orientation !== "portrait" && orientation !== "landscape") {
|
|
@@ -1308,7 +1312,25 @@ function resequenceEvent(event, sequence) {
|
|
|
1308
1312
|
eventId: `${event.runId}:${sequence}`
|
|
1309
1313
|
};
|
|
1310
1314
|
}
|
|
1311
|
-
|
|
1315
|
+
var VIDEO_CHAT_PREPARATION_EVENT_TYPE = "data.video-chat-preparation";
|
|
1316
|
+
function createPreparationChannel() {
|
|
1317
|
+
let wake;
|
|
1318
|
+
const channel = {
|
|
1319
|
+
queue: [],
|
|
1320
|
+
changed: new Promise((resolve) => {
|
|
1321
|
+
wake = resolve;
|
|
1322
|
+
}),
|
|
1323
|
+
publish(value) {
|
|
1324
|
+
channel.queue.push(value);
|
|
1325
|
+
wake();
|
|
1326
|
+
channel.changed = new Promise((resolve) => {
|
|
1327
|
+
wake = resolve;
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
return channel;
|
|
1332
|
+
}
|
|
1333
|
+
function streamVideoChatOpening(response, openingReady, preparations, cancel) {
|
|
1312
1334
|
if (!response.body || !response.headers.get("content-type")?.includes("text/event-stream")) return response;
|
|
1313
1335
|
const events = decodeVideoSse(response.body)[Symbol.asyncIterator]();
|
|
1314
1336
|
const encoded = (async function* () {
|
|
@@ -1324,7 +1346,8 @@ function streamVideoChatOpening(response, openingReady) {
|
|
|
1324
1346
|
...first.value.data.capabilities,
|
|
1325
1347
|
extensions: Array.from(/* @__PURE__ */ new Set([
|
|
1326
1348
|
...first.value.data.capabilities?.extensions ?? [],
|
|
1327
|
-
VIDEO_CHAT_OPENING_EVENT_TYPE
|
|
1349
|
+
VIDEO_CHAT_OPENING_EVENT_TYPE,
|
|
1350
|
+
VIDEO_CHAT_PREPARATION_EVENT_TYPE
|
|
1328
1351
|
]))
|
|
1329
1352
|
}
|
|
1330
1353
|
}
|
|
@@ -1347,10 +1370,28 @@ function streamVideoChatOpening(response, openingReady) {
|
|
|
1347
1370
|
});
|
|
1348
1371
|
sequence += 1;
|
|
1349
1372
|
}
|
|
1350
|
-
let
|
|
1351
|
-
while (
|
|
1352
|
-
|
|
1353
|
-
|
|
1373
|
+
let pending = nextEvent;
|
|
1374
|
+
while (true) {
|
|
1375
|
+
while (preparations.queue.length) {
|
|
1376
|
+
const data = preparations.queue.shift();
|
|
1377
|
+
yield encodeVideoSseEvent({
|
|
1378
|
+
protocolVersion: first.value.protocolVersion,
|
|
1379
|
+
runId: first.value.runId,
|
|
1380
|
+
sequence,
|
|
1381
|
+
eventId: `${first.value.runId}:${sequence}`,
|
|
1382
|
+
type: VIDEO_CHAT_PREPARATION_EVENT_TYPE,
|
|
1383
|
+
data
|
|
1384
|
+
});
|
|
1385
|
+
sequence += 1;
|
|
1386
|
+
}
|
|
1387
|
+
const next = await Promise.race([
|
|
1388
|
+
pending.then((value) => ({ value })),
|
|
1389
|
+
preparations.changed.then(() => void 0)
|
|
1390
|
+
]);
|
|
1391
|
+
if (!next || preparations.queue.length) continue;
|
|
1392
|
+
if (next.value.done) break;
|
|
1393
|
+
yield encodeVideoSseEvent(resequenceEvent(next.value.value, sequence++));
|
|
1394
|
+
pending = events.next();
|
|
1354
1395
|
}
|
|
1355
1396
|
} finally {
|
|
1356
1397
|
await events.return?.(void 0);
|
|
@@ -1376,6 +1417,7 @@ function streamVideoChatOpening(response, openingReady) {
|
|
|
1376
1417
|
}
|
|
1377
1418
|
},
|
|
1378
1419
|
async cancel() {
|
|
1420
|
+
cancel();
|
|
1379
1421
|
completed = true;
|
|
1380
1422
|
await iterator.return?.();
|
|
1381
1423
|
}
|
|
@@ -1440,7 +1482,8 @@ function createVideoChatHandler(options) {
|
|
|
1440
1482
|
allowCredentials,
|
|
1441
1483
|
mediaConcurrency = 5,
|
|
1442
1484
|
maxGeneratedVideos = 5,
|
|
1443
|
-
generateVideoTimeoutMs = 15e3
|
|
1485
|
+
generateVideoTimeoutMs = 15e3,
|
|
1486
|
+
generatedClipDurationSec = 5
|
|
1444
1487
|
} = options;
|
|
1445
1488
|
const videoOptions = {
|
|
1446
1489
|
templates: options.templates,
|
|
@@ -1456,6 +1499,7 @@ function createVideoChatHandler(options) {
|
|
|
1456
1499
|
if (!Number.isFinite(maxAudioBytes) || maxAudioBytes <= 0) throw new Error("maxAudioBytes must be positive");
|
|
1457
1500
|
if (!Number.isFinite(maxBodyBytes) || maxBodyBytes <= 0) throw new Error("maxBodyBytes must be positive");
|
|
1458
1501
|
if (!Number.isSafeInteger(generateVideoTimeoutMs) || generateVideoTimeoutMs < 1 || generateVideoTimeoutMs > 12e4) throw new Error("generateVideoTimeoutMs must be an integer from 1 to 120000");
|
|
1502
|
+
if (!Number.isFinite(generatedClipDurationSec) || generatedClipDurationSec < 2 || generatedClipDurationSec > 20) throw new Error("generatedClipDurationSec must be from 2 to 20");
|
|
1459
1503
|
if (!Number.isSafeInteger(maxGeneratedVideos) || maxGeneratedVideos < 0) throw new Error("maxGeneratedVideos must be a nonnegative safe integer");
|
|
1460
1504
|
const capabilities2 = {
|
|
1461
1505
|
templates: true,
|
|
@@ -1463,17 +1507,41 @@ function createVideoChatHandler(options) {
|
|
|
1463
1507
|
generatedVideo: generateVideo != null,
|
|
1464
1508
|
stockMedia: searchMedia != null,
|
|
1465
1509
|
transcription: transcribe != null,
|
|
1466
|
-
modes: ["cinematic"]
|
|
1510
|
+
modes: searchMedia ? ["cinematic", "pexels"] : ["cinematic"]
|
|
1467
1511
|
};
|
|
1468
1512
|
const welcomePrompts = (welcomeOptions?.prompts ?? DEFAULT_WELCOME_PROMPTS).slice(0, 4);
|
|
1469
1513
|
const heroQuery = welcomeOptions?.heroQuery;
|
|
1470
1514
|
let welcomeResponse;
|
|
1471
1515
|
let requestSequence = 0;
|
|
1472
|
-
const responseHandler = (requestId, openingProvided, openingChannel, openingLine) => {
|
|
1473
|
-
const
|
|
1516
|
+
const responseHandler = (requestId, openingProvided, openingChannel, openingLine, mode, preparations) => {
|
|
1517
|
+
const startedAt = Date.now();
|
|
1518
|
+
const diagnose = (event) => {
|
|
1519
|
+
try {
|
|
1520
|
+
void Promise.resolve(options.onDiagnostic?.({ requestId, mode, elapsedMs: Math.max(0, Date.now() - startedAt), ...event })).catch(() => void 0);
|
|
1521
|
+
} catch {
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
diagnose({ phase: "request-accepted" });
|
|
1525
|
+
const generatedVideoAvailable = mode === "cinematic" && generateVideo != null && maxGeneratedVideos > 0;
|
|
1474
1526
|
let lifecycle;
|
|
1475
1527
|
let generatedAttempts = 0;
|
|
1528
|
+
let mediaStartedAt;
|
|
1529
|
+
let mediaIndex = 0;
|
|
1476
1530
|
const resolveSelected = generateVideo || searchMedia ? async (query, context) => {
|
|
1531
|
+
mediaStartedAt ??= Date.now();
|
|
1532
|
+
const remainingMs = mediaStartedAt + generateVideoTimeoutMs + mediaIndex++ * generatedClipDurationSec * 1e3 - Date.now();
|
|
1533
|
+
if (remainingMs <= 0) {
|
|
1534
|
+
diagnose({ phase: "media-skipped", sceneId: context.scene.id, reason: "deadline" });
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1537
|
+
if (mode === "cinematic" && (!generateVideo || generatedAttempts >= maxGeneratedVideos)) {
|
|
1538
|
+
diagnose({ phase: "media-skipped", sceneId: context.scene.id, reason: !generateVideo ? "not-configured" : "allowance" });
|
|
1539
|
+
return null;
|
|
1540
|
+
}
|
|
1541
|
+
if (mode === "pexels" && !searchMedia) {
|
|
1542
|
+
diagnose({ phase: "media-skipped", sceneId: context.scene.id, reason: "not-configured" });
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1477
1545
|
const mediaContext = {
|
|
1478
1546
|
purpose: "response",
|
|
1479
1547
|
orientation: context.input.orientation ?? "landscape",
|
|
@@ -1487,6 +1555,8 @@ function createVideoChatHandler(options) {
|
|
|
1487
1555
|
const attempt = async (resolver, timeoutMs) => {
|
|
1488
1556
|
context.signal.throwIfAborted();
|
|
1489
1557
|
if (!resolver) return null;
|
|
1558
|
+
const mediaStart = Date.now();
|
|
1559
|
+
diagnose({ phase: "media-start", sceneId: context.scene.id });
|
|
1490
1560
|
try {
|
|
1491
1561
|
const result = sanitizeVideoChatMedia(await withDeadline(
|
|
1492
1562
|
(signal) => resolver(query, { ...mediaContext, signal }),
|
|
@@ -1494,15 +1564,27 @@ function createVideoChatHandler(options) {
|
|
|
1494
1564
|
context.signal
|
|
1495
1565
|
));
|
|
1496
1566
|
context.signal.throwIfAborted();
|
|
1567
|
+
diagnose({
|
|
1568
|
+
phase: "media-end",
|
|
1569
|
+
sceneId: context.scene.id,
|
|
1570
|
+
durationMs: Math.max(0, Date.now() - mediaStart),
|
|
1571
|
+
reason: result && (result.type === "video" || options.templates && resolver === searchMedia) ? "ready" : "empty"
|
|
1572
|
+
});
|
|
1497
1573
|
return result;
|
|
1498
1574
|
} catch (cause) {
|
|
1575
|
+
diagnose({
|
|
1576
|
+
phase: "media-end",
|
|
1577
|
+
sceneId: context.scene.id,
|
|
1578
|
+
durationMs: Math.max(0, Date.now() - mediaStart),
|
|
1579
|
+
reason: context.signal.aborted ? "cancelled" : cause instanceof DOMException && cause.name === "TimeoutError" ? "timeout" : "provider-error"
|
|
1580
|
+
});
|
|
1499
1581
|
context.signal.throwIfAborted();
|
|
1500
1582
|
reportError(cause);
|
|
1501
1583
|
return null;
|
|
1502
1584
|
}
|
|
1503
1585
|
};
|
|
1504
1586
|
if (generatedVideoAvailable && (!options.templates || context.scene.variables.mediaSource === "generate")) {
|
|
1505
|
-
const generated = generatedAttempts < maxGeneratedVideos ? (generatedAttempts++, await attempt(generateVideo,
|
|
1587
|
+
const generated = generatedAttempts < maxGeneratedVideos ? (generatedAttempts++, await attempt(generateVideo, remainingMs)) : null;
|
|
1506
1588
|
if (generated?.type === "video") return generated;
|
|
1507
1589
|
lifecycle?.reportWarning?.({
|
|
1508
1590
|
code: "provider_warning",
|
|
@@ -1511,7 +1593,8 @@ function createVideoChatHandler(options) {
|
|
|
1511
1593
|
recoverable: true
|
|
1512
1594
|
});
|
|
1513
1595
|
}
|
|
1514
|
-
|
|
1596
|
+
if (mode !== "pexels") return null;
|
|
1597
|
+
const stock = await attempt(searchMedia, Math.min(3e3, remainingMs));
|
|
1515
1598
|
if (stock && (options.templates || stock.type === "video")) return stock;
|
|
1516
1599
|
return null;
|
|
1517
1600
|
} : void 0;
|
|
@@ -1543,7 +1626,16 @@ function createVideoChatHandler(options) {
|
|
|
1543
1626
|
},
|
|
1544
1627
|
includeRawProviderData: videoOptions.includeRawProviderData,
|
|
1545
1628
|
openingLine,
|
|
1546
|
-
publishOpening:
|
|
1629
|
+
publishOpening: (opening) => {
|
|
1630
|
+
if (opening) diagnose({ phase: "opening-authored" });
|
|
1631
|
+
openingChannel.publish(opening);
|
|
1632
|
+
},
|
|
1633
|
+
prepareScene: (scene) => {
|
|
1634
|
+
diagnose({ phase: "shot-authored", sceneId: scene.sceneId });
|
|
1635
|
+
if (!resolveSelected) diagnose({ phase: "media-skipped", sceneId: scene.sceneId, reason: "not-configured" });
|
|
1636
|
+
preparations.publish(scene);
|
|
1637
|
+
},
|
|
1638
|
+
generatedClipDurationSec,
|
|
1547
1639
|
resolveMedia: resolveSelected,
|
|
1548
1640
|
mediaConcurrency
|
|
1549
1641
|
}),
|
|
@@ -1551,7 +1643,7 @@ function createVideoChatHandler(options) {
|
|
|
1551
1643
|
allowedOrigins,
|
|
1552
1644
|
allowCredentials,
|
|
1553
1645
|
maxBodyBytes,
|
|
1554
|
-
systemPrompt: [createVideoChatResponseInstructions(generatedVideoAvailable, openingProvided, maxGeneratedVideos), instructions?.trim()].filter(Boolean).join("\n\nAPPLICATION GUIDANCE\n")
|
|
1646
|
+
systemPrompt: [createVideoChatResponseInstructions(generatedVideoAvailable, openingProvided, maxGeneratedVideos, generatedClipDurationSec, mode), instructions?.trim()].filter(Boolean).join("\n\nAPPLICATION GUIDANCE\n")
|
|
1555
1647
|
});
|
|
1556
1648
|
return handler;
|
|
1557
1649
|
};
|
|
@@ -1677,12 +1769,14 @@ function createVideoChatHandler(options) {
|
|
|
1677
1769
|
}
|
|
1678
1770
|
const requestId = `video-chat-${Date.now()}-${requestSequence += 1}`;
|
|
1679
1771
|
const openingChannel = createOpeningChannel(input.opening ? { line: input.opening, keyword: "" } : void 0);
|
|
1772
|
+
const preparations = createPreparationChannel();
|
|
1773
|
+
const cancellation = new AbortController();
|
|
1680
1774
|
const forwardedHeaders = new Headers(request.headers);
|
|
1681
1775
|
forwardedHeaders.delete("content-length");
|
|
1682
1776
|
const videoRequest = new Request(request.url, {
|
|
1683
1777
|
method: "POST",
|
|
1684
1778
|
headers: forwardedHeaders,
|
|
1685
|
-
signal: request.signal,
|
|
1779
|
+
signal: AbortSignal.any([request.signal, cancellation.signal]),
|
|
1686
1780
|
body: JSON.stringify({
|
|
1687
1781
|
protocolVersion: VIDEO_PROTOCOL_VERSION,
|
|
1688
1782
|
requestId,
|
|
@@ -1705,9 +1799,11 @@ function createVideoChatHandler(options) {
|
|
|
1705
1799
|
requestId,
|
|
1706
1800
|
input.opening != null,
|
|
1707
1801
|
openingChannel,
|
|
1708
|
-
input.opening
|
|
1802
|
+
input.opening,
|
|
1803
|
+
input.mode,
|
|
1804
|
+
preparations
|
|
1709
1805
|
)(videoRequest);
|
|
1710
|
-
return streamVideoChatOpening(response, openingChannel.ready);
|
|
1806
|
+
return streamVideoChatOpening(response, openingChannel.ready, preparations, () => cancellation.abort());
|
|
1711
1807
|
}
|
|
1712
1808
|
try {
|
|
1713
1809
|
if (action === "opening-media") {
|
package/docs/concepts.md
CHANGED
|
@@ -23,7 +23,7 @@ trusted templates. It begins before the full plan is available and ends as an
|
|
|
23
23
|
editable deterministic configuration.
|
|
24
24
|
|
|
25
25
|
It is not an encoded video stream. The browser renders normal React components
|
|
26
|
-
from validated scene instructions. The
|
|
26
|
+
from validated scene instructions. The SDK does not include MP4 or WebM
|
|
27
27
|
encoding; pass the completed deterministic JSON to an application-owned render
|
|
28
28
|
or export pipeline when an encoded file is required.
|
|
29
29
|
|
|
@@ -31,7 +31,7 @@ or export pipeline when an encoded file is required.
|
|
|
31
31
|
|
|
32
32
|
The viewer sends a prompt with bounded completed conversation turns. The server
|
|
33
33
|
adds trusted application `instructions`, template capabilities, and the selected
|
|
34
|
-
|
|
34
|
+
AI video or Pexels mode. `VideoChat` options control style, orientation, and custom
|
|
35
35
|
templates. Exact facts belong in the authorized prompt or conversation; secrets
|
|
36
36
|
and provider configuration stay on the server.
|
|
37
37
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Develop the chat experience
|
|
2
|
+
|
|
3
|
+
Run `npm ci --no-audit`, then `npm run dev:chat`. The localhost HMR surface renders the actual SDK `VideoChat`, templates and server handler from source. It never loads provider credentials. Select an intent and fault condition in the development toolbar; use the chat's normal media-mode controls. The prompt box remains the real product UI.
|
|
4
|
+
|
|
5
|
+
Offline answers are deterministic for explanation, story, comedy, imagination, practical steps and golf. They use local waterfall footage and a recorded timing cue, **not narration that matches the displayed script**. This harness is for loading, playback, controls and recovery. It cannot prove generated answer quality. Conditions cover ready, delayed footage, missing media, decode failure, speech failure, exhausted video allowance and request throttling. Browser speech may be used in the speech-failure condition.
|
|
6
|
+
|
|
7
|
+
The toolbar labels source and fixture identity. It separates the first body surface from the first decoded moving-footage frame; the former is a renderer paint opportunity and can precede decode. Its bounded safe phase log records browser request/stream arrival phases, speech response completion, first speech, footage and buffer pauses. No prompts, narration, scene IDs or provider bodies are retained. Media start/end/skip reasons come from the handler’s separate host-only `onDiagnostic` callback, shown in the local terminal for offline fixtures; live hosts own that callback themselves. Stream arrival is not the server’s exact authorship timestamp. Local fixtures never fetch external footage or invoke a paid model. Mode boundaries, provider deadlines and host admission remain covered by their dedicated server and host suites.
|
|
8
|
+
|
|
9
|
+
## Optional live host
|
|
10
|
+
|
|
11
|
+
Set `VANILLASKY_CHAT_LIVE_ENDPOINT` to an application-owned video-chat endpoint before starting the harness. The toolbar then offers **Connect live endpoint (uses allowance)**. It stays offline until that explicit click. Live fetches include host cookies, subject to browser cookie policy. The host must allow credentialed requests from the localhost origin and provide its normal authorization; credentials and allowances stay with that application. This tool does not proxy secrets, reset limits, or retry generated answers. Capabilities and welcome requests may run as soon as you connect. Request only the bounded live examples needed to evaluate actual quality.
|
|
12
|
+
|
|
13
|
+
## Fast checks
|
|
14
|
+
|
|
15
|
+
`npm run check:chat` runs the harness/unit recovery cases, checks harness TypeScript and runs one Chromium recording with short footage looping under actual audio. Install Chromium once with `npx playwright install chromium`. The harness smoke additionally records submit-to-visible-chapter paint opportunity with a 200 ms warm-UI target. This is a browser animation-frame opportunity, not a physical display measurement. The target for the complete command is under a minute on a warm machine; the command reports its measured duration. It does not replace release checks or claim browser-wide/live-provider coverage.
|
|
16
|
+
|
|
17
|
+
## One candidate for release verification
|
|
18
|
+
|
|
19
|
+
`npm run verify:release` runs registry, lint, type, unit, acceptance, size, production dependency audit, API, packed-consumer, onboarding, provider and all existing browser checks. It builds and packs once into ignored `artifacts/chat-candidate/`, then sends the exact tarball and integrity to every consumer verifier. Each verifier still uses its independent clean install. Browser and provider compatibility remain independent gates; no paid API calls are enabled by this command.
|
|
20
|
+
|
|
21
|
+
`candidate.json` records version, source commit, dirty-tree status, SHA-256 and npm integrity. A local candidate may include uncommitted work; it is never called a published artifact. `VANILLASKY_CANDIDATE_DIR` selects a different output directory. The command verifies but never publishes, merges or deploys. CI additionally retains the existing Node and React version checks.
|
|
22
|
+
|
|
23
|
+
For one targeted consumer check, run `node scripts/chat-candidate.mjs`, then provide `VANILLASKY_PACKED_TARBALL`, `VANILLASKY_EXPECTED_INTEGRITY` and `VANILLASKY_EXPECTED_SHA256` from that manifest to an existing verifier. Do not repack between checks of the same candidate.
|
package/docs/getting-started.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
The fastest VanillaSky integration is the complete, general-purpose video chat.
|
|
6
6
|
It starts with a template introduction and browser voice. Add the video adapter
|
|
7
|
-
and its server key for generated footage,
|
|
7
|
+
and its server key for generated footage, or choose Pexels for stock footage.
|
|
8
8
|
Without a media provider, the answer retains narration and subtitles with an
|
|
9
|
-
|
|
9
|
+
chapter template. Generated speech is optional.
|
|
10
10
|
|
|
11
11
|
## Create the app
|
|
12
12
|
|
package/docs/media-and-audio.md
CHANGED
|
@@ -8,21 +8,21 @@ all credentials out of React and the browser bundle.
|
|
|
8
8
|
|
|
9
9
|
## AI-first video answers
|
|
10
10
|
|
|
11
|
-
The default chat
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
The default chat displays the real chapter template immediately, then prepares
|
|
12
|
+
speech and footage concurrently. Choose `mode: "cinematic"` for AI video or
|
|
13
|
+
`mode: "pexels"` for stock. The UI labels these choices **AI video** and **Pexels**.
|
|
14
|
+
Each mode uses only its selected media provider. A failure, exhausted video
|
|
15
|
+
allowance, or missed deadline becomes an authored chapter with complete narration.
|
|
16
|
+
The chapter retains narration and subtitles for the whole beat.
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
chat body shots use `cinemaMedia` as the footage renderer, without extra headlines.
|
|
19
|
-
If both media providers miss, the answer retains its narration and subtitles
|
|
20
|
-
with an explicit unavailable-visual state rather than a text-slide replacement.
|
|
18
|
+
## Pexels search
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
The starter performs bounded full-catalog Pexels video search with subject
|
|
21
|
+
matching, orientation-aware renditions and a bounded cache. Add `PEXELS_API_KEY`
|
|
22
|
+
on the server and choose Pexels in Settings. The SDK UI links to Pexels; custom
|
|
23
|
+
interfaces must also display the attribution required by their media provider.
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
cards:
|
|
25
|
+
Applications can replace `searchMedia` with their own licensed catalog:
|
|
26
26
|
|
|
27
27
|
```ts
|
|
28
28
|
createVideoChatHandler({
|
|
@@ -46,7 +46,7 @@ createVideoChatHandler({
|
|
|
46
46
|
The planner emits a short semantic keyword, not a URL. The callback returns an
|
|
47
47
|
application-approved image or video URL, and the SDK validates it before it
|
|
48
48
|
reaches a scene. Return `null` when no licensed, safe, relevant asset exists;
|
|
49
|
-
default chat
|
|
49
|
+
default chat displays an authored chapter while retaining the spoken answer. An explicit custom `templates` registry retains its structured planner,
|
|
50
50
|
validation, and fallback contracts.
|
|
51
51
|
|
|
52
52
|
For Pexels, keep `PEXELS_API_KEY` on the server, enforce a deadline, filter for
|
|
@@ -96,6 +96,19 @@ decision with a known budget.
|
|
|
96
96
|
VanillaSky does not depend on a video model or storage service. The application
|
|
97
97
|
owns the provider key, model, spend, generated bytes, retention, and delivery.
|
|
98
98
|
|
|
99
|
+
### Timing and recovery
|
|
100
|
+
|
|
101
|
+
Set `generatedClipDurationSec` to the duration your video adapter actually
|
|
102
|
+
requests (default 5, supported range 2–20 seconds). Keep it aligned with provider
|
|
103
|
+
settings and host spending limits. The planner fits natural spoken beats to that
|
|
104
|
+
budget; measured audio determines the final scene timing. Silent footage may
|
|
105
|
+
loop until the finite scene ends. Audible footage is not looped.
|
|
106
|
+
|
|
107
|
+
`generateVideoTimeoutMs` sets the first-shot preparation budget (default 15 seconds).
|
|
108
|
+
Later deadlines account for their position in the answer rather than restarting
|
|
109
|
+
an unlimited wait. Hosts must honor cancellation. A missed deadline selects the
|
|
110
|
+
authored chapter instead of a second paid generation or cross-mode stock search.
|
|
111
|
+
|
|
99
112
|
## Voice and transcription
|
|
100
113
|
|
|
101
114
|
Without `generateSpeech`, `VideoChat` uses the browser voice. Add a speech
|
package/docs/performance.md
CHANGED
|
@@ -20,6 +20,13 @@ The SDK sends nothing to a telemetry service. Events contain only an opaque
|
|
|
20
20
|
turn ID, mode, relative timing, and fixed event categories. Keep custom turn IDs
|
|
21
21
|
opaque; do not put prompts or customer information into them.
|
|
22
22
|
|
|
23
|
+
The handler's optional `onDiagnostic(event)` observes accepted requests,
|
|
24
|
+
authored openings and shots, and media start/end/skip timings on the host.
|
|
25
|
+
Fixed reasons distinguish allowance, deadline, timeout, provider error, empty
|
|
26
|
+
results, cancellation and absent configuration. These records contain no
|
|
27
|
+
prompt, narration, query, media URL or provider response. They are never sent
|
|
28
|
+
to the browser automatically, and callback failures cannot stop an answer.
|
|
29
|
+
|
|
23
30
|
## What the measurements mean
|
|
24
31
|
|
|
25
32
|
`first-frame` is the first committed active scene reaching an animation-frame
|
|
@@ -55,15 +62,19 @@ these against an explicitly authorized, bounded live run before treating them
|
|
|
55
62
|
as tuned provider budgets. Compare first-frame/speech times, stalled duration,
|
|
56
63
|
and visual/voice quality together; faster fallback alone does not prove quality.
|
|
57
64
|
|
|
58
|
-
## Opening
|
|
65
|
+
## Opening and media preparation
|
|
66
|
+
|
|
67
|
+
The submitted prompt immediately appears in the chapter template; the streamed
|
|
68
|
+
opening replaces that topic with an authored spoken beat. The default UI makes
|
|
69
|
+
no opening stock request. Each body beat starts speech preparation while its
|
|
70
|
+
selected footage source prepares. At most two speech preparations run together.
|
|
59
71
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
72
|
+
`first-media-frame` reports the first decoded footage frame presented by the
|
|
73
|
+
mounted media surface. It is separate from `first-frame`, which also includes
|
|
74
|
+
chapter scenes. Neither callback measures the immediate opening template;
|
|
75
|
+
measure that surface separately when checking submit-to-template latency.
|
|
63
76
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
obvious mismatches, but missing metadata remains unknown and is accepted. This
|
|
69
|
-
is not visual relevance verification. See the [Pexels API contract](https://www.pexels.com/api/documentation/).
|
|
77
|
+
AI and Pexels modes remain separate. Missing, late, or unplayable footage uses
|
|
78
|
+
the authored chapter and complete narration. Silent clips loop for the finite
|
|
79
|
+
narrated scene. See [the local chat harness](development.md) for fixture timing
|
|
80
|
+
and explicit live-provider checks.
|
package/docs/production.md
CHANGED
|
@@ -22,13 +22,11 @@ player. Read the [security guide](security.md) for the complete controls.
|
|
|
22
22
|
|
|
23
23
|
## Cinematic direction and providers
|
|
24
24
|
|
|
25
|
-
Default chat
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
unavailability state. Explicit custom template registries keep their existing
|
|
31
|
-
composition and fallback contracts.
|
|
25
|
+
Default chat shows an immediate chapter while speech and selected footage prepare.
|
|
26
|
+
Configure `generateVideo` for AI mode and `searchMedia` for Pexels mode. Neither
|
|
27
|
+
mode calls the other footage source. Missing or late footage uses the authored
|
|
28
|
+
chapter with complete narration. Explicit custom template registries keep their
|
|
29
|
+
existing composition and fallback contracts.
|
|
32
30
|
A stock candidate must match the subject, action and permitted crop. Return
|
|
33
31
|
`null` for uncertainty rather than broadening an essential detail.
|
|
34
32
|
|
|
@@ -39,11 +37,11 @@ licensing before use.
|
|
|
39
37
|
|
|
40
38
|
## Fast first response
|
|
41
39
|
|
|
42
|
-
The
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
model round trip.
|
|
40
|
+
The prompt appears immediately in the chapter template. The planner's first
|
|
41
|
+
streamed object supplies an authored spoken opening and reserves the ending.
|
|
42
|
+
Prepare each shot's speech alongside its footage, and keep the opening readable
|
|
43
|
+
until its narration and the contiguous preparation cushion are ready. Welcome
|
|
44
|
+
cards may carry a prepared opening so its speech starts without a model round trip.
|
|
47
45
|
|
|
48
46
|
Do not wait for the complete plan before showing the first validated scene.
|
|
49
47
|
Preload upcoming assets and keep the current visual if the next one is late.
|
|
@@ -106,7 +104,7 @@ npm test
|
|
|
106
104
|
- [ ] Keys exist only in the server secret store.
|
|
107
105
|
- [ ] Authentication, tenant policy, rate limits, and origin allowlist are live.
|
|
108
106
|
- [ ] Cancellation, timeouts, fallbacks, and safe errors are tested.
|
|
109
|
-
- [ ]
|
|
107
|
+
- [ ] Authored chapter recovery works when every optional provider is unavailable.
|
|
110
108
|
- [ ] Per-scene media choices obey provider availability and spending limits.
|
|
111
109
|
- [ ] Both orientations render and narration stays synchronized.
|
|
112
110
|
- [ ] A packed-artifact consumer and deterministic browser chat pass.
|
package/docs/prompt-and-input.md
CHANGED
|
@@ -96,9 +96,9 @@ Every `scene.add` is validated before the browser receives it. The model never
|
|
|
96
96
|
returns React, HTML, CSS, or executable JavaScript. Invalid planning content
|
|
97
97
|
produces safe diagnostics. A media failure does not delete valid narration.
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
AI mode generates footage within the host allowance. Pexels mode searches
|
|
100
|
+
stock without calling the video generator. Each authored beat includes a short
|
|
101
|
+
chapter title; missing footage becomes that chapter with its complete narration. Handlers configured with an explicit custom `templates` registry continue to
|
|
102
102
|
support the trusted catalog and its existing structured planner contract.
|
|
103
103
|
|
|
104
104
|
## Grounding
|