@vanillaskyai/video 0.7.0 → 0.8.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 +66 -0
- package/PUBLIC-API.md +83 -79
- package/README.md +16 -15
- package/dist/{builtin-server-KB7FKF6A.js → builtin-server-CUYPIDE7.js} +2 -2
- package/dist/check-runtime.js +1 -1
- package/dist/{chunk-2PYO6VAC.js → chunk-2DOLF4QN.js} +1 -43
- package/dist/{chunk-GAIOHGNR.js → chunk-2OKB27TG.js} +42 -45
- package/dist/{chunk-K746NUIP.js → chunk-2USVB3CY.js} +80 -29
- package/dist/{chunk-RE4IMWJR.js → chunk-3VRMKFM7.js} +87 -14
- package/dist/{chunk-JNN3EYVP.js → chunk-4UNKZEAT.js} +46 -46
- package/dist/{chunk-2XT4MZ76.js → chunk-APQKY6P3.js} +36 -3
- package/dist/{chunk-RXHW4EP4.js → chunk-L7QGGKJ2.js} +71 -40
- package/dist/{chunk-RGF452LL.js → chunk-OWBT45EN.js} +1 -1
- package/dist/{chunk-LKBZX7GV.js → chunk-PP3MCPN4.js} +4 -4
- package/dist/chunk-ZD2VTUYR.js +28 -0
- package/dist/cli.js +131 -24
- package/dist/{compose-video-PD6LKKRK.js → compose-video-2NFH4DZY.js} +3 -3
- package/dist/{events-B6qS1Lsb.d.ts → events-BQ6z264n.d.ts} +1 -1
- package/dist/index.d.ts +5 -67
- package/dist/index.js +2 -4
- package/dist/{kit-8g46H2RZ.d.ts → kit-tQnCPMOM.d.ts} +1 -1
- package/dist/react.d.ts +33 -53
- package/dist/react.js +642 -536
- package/dist/server.d.ts +11 -13
- package/dist/server.js +170 -81
- 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-FW4BLBAL.js → text-stream-54LXABJW.js} +1 -1
- package/dist/{types-2wHBqtg8.d.ts → types-CEh9eUFu.d.ts} +1 -1
- package/dist/{types-DrABlRa7.d.ts → types-DWFO6qca.d.ts} +1 -1
- package/docs/agent-integration.md +9 -3
- package/docs/architecture.md +8 -10
- package/docs/concepts.md +16 -40
- package/docs/custom-templates.md +27 -23
- package/docs/customization.md +12 -49
- package/docs/errors.md +58 -68
- package/docs/getting-started.md +21 -14
- package/docs/media-and-audio.md +2 -2
- package/docs/performance.md +69 -0
- package/docs/persistence.md +12 -27
- package/docs/production.md +2 -2
- package/docs/provider-integration.md +11 -5
- package/docs/reference/provider-adapters.md +37 -89
- package/docs/security.md +1 -1
- package/docs/streaming-protocol.md +2 -2
- package/examples/custom-template/README.md +6 -6
- package/package.json +6 -7
- package/registry/items/barChart.json +2 -2
- package/registry/items/bigNumber.json +2 -2
- package/registry/items/brandMessage.json +2 -2
- package/registry/items/cardList.json +2 -2
- package/registry/items/codeEditor.json +2 -2
- package/registry/items/ctaLogo.json +2 -2
- package/registry/items/ctaMedia.json +2 -2
- package/registry/items/incomingCall.json +2 -2
- package/registry/items/media.json +2 -2
- package/registry/items/milestone.json +2 -2
- package/registry/items/notification.json +2 -2
- package/registry/items/phoneMockup.json +2 -2
- package/registry/items/problemSolution.json +2 -2
- package/registry/items/progressRing.json +2 -2
- package/registry/items/promptInput.json +2 -2
- package/registry/items/reaction.json +2 -2
- package/registry/items/reviewStack.json +2 -2
- package/registry/items/steps.json +2 -2
- package/registry/items/terminal.json +2 -2
- package/registry/items/testimonial.json +2 -2
- package/registry/items/tripleStats.json +2 -2
- package/registry/items/tweet.json +2 -2
- package/registry/items/webMockup.json +2 -2
- package/starters/video-chat/.env.example +4 -2
- package/starters/video-chat/README.md +17 -5
- package/starters/video-chat/package.json +2 -4
- package/starters/video-chat/providers/speech.ts +20 -0
- package/starters/video-chat/providers/video.ts +66 -0
- package/starters/video-chat/providers.ts +3 -0
- package/starters/video-chat/server.ts +4 -80
- package/starters/video-chat/stock.ts +114 -99
- package/dist/chunk-SKRGRKHY.js +0 -142
- package/dist/state-DZcKuS32.d.ts +0 -3
- package/docs/branding-and-personalization.md +0 -87
- package/examples/custom-template/supplied-media.tsx +0 -92
|
@@ -1,26 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stock footage for the modes that do not generate any.
|
|
3
|
-
*
|
|
4
|
-
* A rendered card on a brand gradient is the least a template can look like,
|
|
5
|
-
* and templates-only mode was every scene that way. A stock search costs a few
|
|
6
|
-
* hundred milliseconds and nothing per request, so unlike a generated clip it
|
|
7
|
-
* never lands on the critical path - the difference between cards on gradients
|
|
8
|
-
* and cards over real footage is close to free.
|
|
9
|
-
*
|
|
10
|
-
* Ported from the site's playground, trimmed to what a video chat needs. Every URL
|
|
11
|
-
* that comes back is checked against the host it must come from: the planner
|
|
12
|
-
* asks for a subject, and what a search returns is not something to trust into
|
|
13
|
-
* a page unexamined.
|
|
14
|
-
*/
|
|
15
1
|
import type { VideoOrientation } from "@vanillaskyai/video";
|
|
16
2
|
|
|
17
|
-
const VIDEO_SEARCH = "https://api.pexels.com/videos/search";
|
|
3
|
+
const VIDEO_SEARCH = "https://api.pexels.com/v1/videos/search";
|
|
18
4
|
const PHOTO_SEARCH = "https://api.pexels.com/v1/search";
|
|
19
5
|
const MAX_KEYWORD_LENGTH = 80;
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
// than a response that waits.
|
|
23
|
-
const LOOKUP_TIMEOUT_MS = 4_000;
|
|
6
|
+
// One budget covers specific/broader video searches and photo recovery.
|
|
7
|
+
const LOOKUP_TIMEOUT_MS = 3_000;
|
|
24
8
|
|
|
25
9
|
interface StockMedia {
|
|
26
10
|
url: string;
|
|
@@ -28,12 +12,6 @@ interface StockMedia {
|
|
|
28
12
|
posterUrl?: string;
|
|
29
13
|
}
|
|
30
14
|
|
|
31
|
-
interface VideoFile {
|
|
32
|
-
link?: unknown;
|
|
33
|
-
width?: number;
|
|
34
|
-
height?: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
15
|
/** One hour, keyed by query. The same response asked twice searches once. */
|
|
38
16
|
const cache = new Map<string, { payload: unknown; expiresAt: number }>();
|
|
39
17
|
|
|
@@ -45,95 +23,132 @@ function normalizeKeyword(value: string): string {
|
|
|
45
23
|
.slice(0, MAX_KEYWORD_LENGTH);
|
|
46
24
|
}
|
|
47
25
|
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
26
|
+
function record(value: unknown): Record<string, unknown> {
|
|
27
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
28
|
+
? value as Record<string, unknown>
|
|
29
|
+
: {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function entries(value: unknown): unknown[] {
|
|
33
|
+
return Array.isArray(value) ? value : [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function trustedUrl(value: unknown, host: string): string | undefined {
|
|
37
|
+
if (typeof value !== "string") return undefined;
|
|
38
|
+
try {
|
|
39
|
+
const url = new URL(value);
|
|
40
|
+
if (url.protocol !== "https:" || url.hostname !== host || url.username || url.password || url.port) return undefined;
|
|
41
|
+
return url.toString();
|
|
42
|
+
} catch {
|
|
43
|
+
return undefined;
|
|
52
44
|
}
|
|
53
|
-
return url.toString();
|
|
54
45
|
}
|
|
55
46
|
|
|
56
|
-
function pickVideoFile(files:
|
|
57
|
-
const usable = files
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// frame is a column of water with both banks cut off.
|
|
72
|
-
const wanted = orientation === "portrait"
|
|
73
|
-
? (file: VideoFile) => (file.height ?? 0) >= (file.width ?? 0)
|
|
74
|
-
: (file: VideoFile) => (file.width ?? 0) >= (file.height ?? 0);
|
|
75
|
-
return usable.find(wanted) ?? usable[0] ?? null;
|
|
47
|
+
function pickVideoFile(files: unknown, orientation: VideoOrientation): string | undefined {
|
|
48
|
+
const usable = entries(files).flatMap((value) => {
|
|
49
|
+
const file = record(value);
|
|
50
|
+
const direct = trustedUrl(file.link, "videos.pexels.com");
|
|
51
|
+
const external = trustedUrl(file.link, "player.vimeo.com");
|
|
52
|
+
const link = direct ?? (external && /^\/external\/[a-zA-Z0-9._-]+\.mp4$/.test(new URL(external).pathname) ? external : undefined);
|
|
53
|
+
const { width, height } = file;
|
|
54
|
+
if (!link || typeof width !== "number" || typeof height !== "number"
|
|
55
|
+
|| !Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return [];
|
|
56
|
+
return [{ link, width, height }];
|
|
57
|
+
});
|
|
58
|
+
// Prefer the stage's shape without discarding other safe renditions.
|
|
59
|
+
return (usable.find((file) => orientation === "portrait"
|
|
60
|
+
? file.height >= file.width
|
|
61
|
+
: file.width >= file.height) ?? usable[0])?.link;
|
|
76
62
|
}
|
|
77
63
|
|
|
78
64
|
async function search(url: string, apiKey: string, signal: AbortSignal): Promise<unknown> {
|
|
79
65
|
const cached = cache.get(url);
|
|
80
66
|
if (cached && cached.expiresAt > Date.now()) return cached.payload;
|
|
81
67
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
68
|
+
if (signal.aborted) return null;
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(url, {
|
|
71
|
+
headers: { Authorization: apiKey },
|
|
72
|
+
signal,
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok || signal.aborted) return null;
|
|
75
|
+
const payload: unknown = await response.json();
|
|
76
|
+
if (signal.aborted) return null;
|
|
77
|
+
cache.set(url, { payload, expiresAt: Date.now() + 60 * 60 * 1_000 });
|
|
78
|
+
if (cache.size > 100) cache.delete(cache.keys().next().value!);
|
|
79
|
+
return payload;
|
|
80
|
+
} catch {
|
|
81
|
+
// A failed video search must still allow the independent photo fallback.
|
|
82
|
+
if (!signal.aborted) console.warn("[video-chat] Stock lookup unavailable; trying a fallback.");
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Metadata is a weak mismatch filter, never visual verification. Numeric URLs
|
|
88
|
+
* and missing descriptions are unknown and retain the provider's search ranking. */
|
|
89
|
+
function matchesMetadata(item: Record<string, unknown>, keyword: string): boolean {
|
|
90
|
+
let description = typeof item.alt === "string" ? item.alt : "";
|
|
91
|
+
if (!description) {
|
|
92
|
+
const page = trustedUrl(item.url, "www.pexels.com");
|
|
93
|
+
if (page) description = new URL(page).pathname.replace(/^\/(?:video|photo)\//, "");
|
|
94
|
+
}
|
|
95
|
+
const words = (value: string) => normalizeKeyword(value).toLowerCase().split(/[\s-]+/)
|
|
96
|
+
.filter(word => /\p{L}/u.test(word) && !["a", "an", "the", "of", "in", "on", "with", "and", "video", "photo", "free", "footage"].includes(word));
|
|
97
|
+
const labels = words(description);
|
|
98
|
+
const requested = words(keyword);
|
|
99
|
+
return labels.length === 0 || requested.length === 0 || requested.some(word => labels.includes(word));
|
|
94
100
|
}
|
|
95
101
|
|
|
96
|
-
/**
|
|
97
|
-
*
|
|
98
|
-
|
|
99
|
-
* Returns null rather than throwing when there is nothing to show: the scene
|
|
100
|
-
* falls back to the brand gradient, which is what it looked like before.
|
|
101
|
-
*/
|
|
102
|
-
export async function findStockFootage(subject: string, orientation: VideoOrientation, signal: AbortSignal): Promise<StockMedia | null> {
|
|
102
|
+
/** Video queries precede photos. All work shares one deadline, including body
|
|
103
|
+
* parsing, even if a custom fetch implementation ignores its abort signal. */
|
|
104
|
+
export async function findStockFootage(subject: string, orientation: VideoOrientation, signal: AbortSignal, fallbackSubject?: string): Promise<StockMedia | null> {
|
|
103
105
|
const apiKey = process.env.PEXELS_API_KEY;
|
|
104
106
|
const keyword = normalizeKeyword(subject);
|
|
105
|
-
if (!apiKey || !keyword) return null;
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
107
|
+
if (!apiKey || !keyword || signal.aborted) return null;
|
|
108
|
+
const keywords = [keyword];
|
|
109
|
+
const fallback = normalizeKeyword(fallbackSubject ?? "");
|
|
110
|
+
if (fallback && fallback.toLowerCase() !== keyword.toLowerCase()) keywords.push(fallback);
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
let finish!: (value: null) => void;
|
|
113
|
+
const stopped = new Promise<null>(resolve => { finish = resolve; });
|
|
114
|
+
const abort = () => { controller.abort(); finish(null); };
|
|
115
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
116
|
+
const timer = setTimeout(abort, LOOKUP_TIMEOUT_MS);
|
|
117
|
+
const active = controller.signal;
|
|
118
|
+
const lookup = async (): Promise<StockMedia | null> => {
|
|
119
|
+
for (const query of keywords) {
|
|
120
|
+
const params = new URLSearchParams({ query, orientation, per_page: "3", page: "1" });
|
|
121
|
+
const videos = record(await search(`${VIDEO_SEARCH}?${params}`, apiKey, active));
|
|
122
|
+
if (active.aborted) return null;
|
|
123
|
+
for (const value of entries(videos.videos)) {
|
|
124
|
+
const video = record(value);
|
|
125
|
+
if (!matchesMetadata(video, query)) continue;
|
|
126
|
+
const url = pickVideoFile(video.video_files, orientation);
|
|
127
|
+
if (!url) continue;
|
|
128
|
+
const posterUrl = trustedUrl(video.image, "images.pexels.com");
|
|
129
|
+
return { url, type: "video", ...(posterUrl ? { posterUrl } : {}) };
|
|
130
|
+
}
|
|
123
131
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
for (const query of keywords) {
|
|
133
|
+
const params = new URLSearchParams({ query, orientation, per_page: "3", page: "1" });
|
|
134
|
+
const photos = record(await search(`${PHOTO_SEARCH}?${params}`, apiKey, active));
|
|
135
|
+
if (active.aborted) return null;
|
|
136
|
+
for (const value of entries(photos.photos)) {
|
|
137
|
+
const photo = record(value);
|
|
138
|
+
if (!matchesMetadata(photo, query)) continue;
|
|
139
|
+
const source = record(photo.src);
|
|
140
|
+
for (const candidate of [source.large2x, source.large]) {
|
|
141
|
+
const url = trustedUrl(candidate, "images.pexels.com");
|
|
142
|
+
if (url) return { url, type: "image" };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
136
145
|
}
|
|
137
146
|
return null;
|
|
147
|
+
};
|
|
148
|
+
try {
|
|
149
|
+
return await Promise.race([lookup(), stopped]);
|
|
150
|
+
} finally {
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
signal.removeEventListener("abort", abort);
|
|
138
153
|
}
|
|
139
154
|
}
|
package/dist/chunk-SKRGRKHY.js
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
checksumVideo,
|
|
3
|
-
createVideoEventFactory
|
|
4
|
-
} from "./chunk-2PYO6VAC.js";
|
|
5
|
-
import {
|
|
6
|
-
VIDEO_SCHEMA_VERSION
|
|
7
|
-
} from "./chunk-AOWSXU2K.js";
|
|
8
|
-
import {
|
|
9
|
-
getReadableSceneDuration
|
|
10
|
-
} from "./chunk-3O7OMMMF.js";
|
|
11
|
-
|
|
12
|
-
// src/protocol/scene-timeline.ts
|
|
13
|
-
function createQueue() {
|
|
14
|
-
const buffered = [];
|
|
15
|
-
const waiting = [];
|
|
16
|
-
let closed = false;
|
|
17
|
-
return {
|
|
18
|
-
push(value) {
|
|
19
|
-
const next = waiting.shift();
|
|
20
|
-
if (next) next({ value, done: false });
|
|
21
|
-
else buffered.push(value);
|
|
22
|
-
},
|
|
23
|
-
close() {
|
|
24
|
-
closed = true;
|
|
25
|
-
let next = waiting.shift();
|
|
26
|
-
while (next) {
|
|
27
|
-
next({ value: void 0, done: true });
|
|
28
|
-
next = waiting.shift();
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
iterable: {
|
|
32
|
-
[Symbol.asyncIterator]() {
|
|
33
|
-
return {
|
|
34
|
-
next() {
|
|
35
|
-
const buffer = buffered.shift();
|
|
36
|
-
if (buffer !== void 0) return Promise.resolve({ value: buffer, done: false });
|
|
37
|
-
if (closed) return Promise.resolve({ value: void 0, done: true });
|
|
38
|
-
return new Promise((resolve) => waiting.push(resolve));
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
var runCounter = 0;
|
|
46
|
-
function createSceneTimeline(options) {
|
|
47
|
-
const queue = createQueue();
|
|
48
|
-
const runId = options.runId ?? `composed-${runCounter += 1}`;
|
|
49
|
-
const events = createVideoEventFactory({ runId });
|
|
50
|
-
const orientation = options.orientation ?? "landscape";
|
|
51
|
-
const snapshot = {
|
|
52
|
-
schemaVersion: VIDEO_SCHEMA_VERSION,
|
|
53
|
-
orientation,
|
|
54
|
-
scenes: [],
|
|
55
|
-
style: options.style
|
|
56
|
-
};
|
|
57
|
-
let position = 0;
|
|
58
|
-
let finished = false;
|
|
59
|
-
const held = [];
|
|
60
|
-
let openForScenes = !options.awaitAudio;
|
|
61
|
-
const emit = (event) => {
|
|
62
|
-
if (finished) return;
|
|
63
|
-
queue.push(event);
|
|
64
|
-
};
|
|
65
|
-
emit(events.create("response.start", {
|
|
66
|
-
requestId: options.requestId ?? runId,
|
|
67
|
-
format: { orientation },
|
|
68
|
-
style: options.style
|
|
69
|
-
}));
|
|
70
|
-
if (options.audio) {
|
|
71
|
-
snapshot.audio = options.audio;
|
|
72
|
-
emit(events.create("audio.set", { audio: options.audio }));
|
|
73
|
-
}
|
|
74
|
-
const release = () => {
|
|
75
|
-
const pending = held.splice(0, held.length);
|
|
76
|
-
for (const scene of pending) {
|
|
77
|
-
snapshot.scenes.push(scene);
|
|
78
|
-
emit(events.create("scene.add", { scene, position: position++ }));
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
return {
|
|
82
|
-
stream: queue.iterable,
|
|
83
|
-
add(scene) {
|
|
84
|
-
if (finished) return;
|
|
85
|
-
if (!openForScenes) {
|
|
86
|
-
held.push(scene);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
snapshot.scenes.push(scene);
|
|
90
|
-
emit(events.create("scene.add", { scene, position: position++ }));
|
|
91
|
-
},
|
|
92
|
-
setAudio(audio) {
|
|
93
|
-
if (finished || openForScenes) return;
|
|
94
|
-
if (audio) {
|
|
95
|
-
snapshot.audio = audio;
|
|
96
|
-
emit(events.create("audio.set", { audio }));
|
|
97
|
-
}
|
|
98
|
-
openForScenes = true;
|
|
99
|
-
release();
|
|
100
|
-
},
|
|
101
|
-
complete() {
|
|
102
|
-
if (finished) return;
|
|
103
|
-
if (!openForScenes) {
|
|
104
|
-
openForScenes = true;
|
|
105
|
-
release();
|
|
106
|
-
}
|
|
107
|
-
emit(events.create("response.complete", {
|
|
108
|
-
finishReason: "stop",
|
|
109
|
-
snapshot,
|
|
110
|
-
checksum: checksumVideo(snapshot)
|
|
111
|
-
}));
|
|
112
|
-
finished = true;
|
|
113
|
-
queue.close();
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// src/protocol/scene-duration.ts
|
|
119
|
-
var NARRATION_WORDS_PER_SECOND = 2.6;
|
|
120
|
-
var NARRATION_TAIL_SECONDS = 0.6;
|
|
121
|
-
function getSceneDurationBounds(scene, metadata) {
|
|
122
|
-
return {
|
|
123
|
-
minimum: Math.max(1e-3, metadata?.minDuration ?? 1),
|
|
124
|
-
readable: getReadableSceneDuration(scene, metadata)
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
function getSpokenDuration(narration) {
|
|
128
|
-
const words = narration.trim().split(/\s+/u).filter(Boolean).length;
|
|
129
|
-
if (words === 0) return 0;
|
|
130
|
-
return words / NARRATION_WORDS_PER_SECOND + NARRATION_TAIL_SECONDS;
|
|
131
|
-
}
|
|
132
|
-
function getSceneDuration(scene, metadata) {
|
|
133
|
-
const { readable } = getSceneDurationBounds(scene, metadata);
|
|
134
|
-
return Math.max(readable, scene.narration ? getSpokenDuration(scene.narration) : 0);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export {
|
|
138
|
-
createSceneTimeline,
|
|
139
|
-
getSceneDurationBounds,
|
|
140
|
-
getSpokenDuration,
|
|
141
|
-
getSceneDuration
|
|
142
|
-
};
|
package/dist/state-DZcKuS32.d.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
[← Documentation home](../README.md) · [Previous: Prompt and conversation input](prompt-and-input.md) · [Next: Media and audio →](media-and-audio.md)
|
|
2
|
-
|
|
3
|
-
# Branding and personalization
|
|
4
|
-
|
|
5
|
-
Brand values style the whole response. Personalization shapes who the story is for.
|
|
6
|
-
|
|
7
|
-
## Background: standard, preset, or custom
|
|
8
|
-
|
|
9
|
-
Do nothing to use the standard `cosmic` background:
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
video.generate({ input });
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Choose a curated gradient or solid by name when the standard does not fit:
|
|
16
|
-
|
|
17
|
-
```ts
|
|
18
|
-
video.generate({
|
|
19
|
-
input,
|
|
20
|
-
brand: { background: "twilight" },
|
|
21
|
-
});
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Curated gradients are `cosmic`, `horizon`, `twilight`, `meadow`, `velvet`,
|
|
25
|
-
`flamingo`, `peach`, and `saffron`. Curated solids are `black`, `midnight`,
|
|
26
|
-
`aubergine`, `coal`, and `navy`.
|
|
27
|
-
|
|
28
|
-
Use exact colors only when a real brand or campaign requires them:
|
|
29
|
-
|
|
30
|
-
```ts
|
|
31
|
-
brand: { background: { colors: ["#112233", "#334455"] } } // gradient
|
|
32
|
-
brand: { background: { color: "#070B20" } } // solid
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
These choices set the background colors used by every trusted gradient-backed
|
|
36
|
-
template and by the automatic generation cover. Templates own their
|
|
37
|
-
composition and type treatment, so there is no separate visual-preset choice.
|
|
38
|
-
|
|
39
|
-
## Semantic brand tokens
|
|
40
|
-
|
|
41
|
-
Most applications can stop at `brand.background` and `brand.font`. Supply
|
|
42
|
-
semantic colors only when a real design system requires exact values:
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
const brand = {
|
|
46
|
-
name: "Acme Cloud",
|
|
47
|
-
logoUrl: approvedLogoUrl,
|
|
48
|
-
font: "Inter",
|
|
49
|
-
scriptFont: "Caveat",
|
|
50
|
-
background: "twilight",
|
|
51
|
-
colors: {
|
|
52
|
-
primary: "#7C5CFC",
|
|
53
|
-
secondary: "#3D2A78",
|
|
54
|
-
foreground: "#FFFFFF",
|
|
55
|
-
surface: "#161229",
|
|
56
|
-
surfaceElevated: "#211A38",
|
|
57
|
-
muted: "#AAA5B8",
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Every generated video contains a fully resolved brand. `colors` may be partial
|
|
63
|
-
on input; omitted values receive deterministic defaults. When
|
|
64
|
-
`colors.foreground` is omitted, every named or custom background is paired
|
|
65
|
-
deterministically with black or white. The resolver checks the full rendered
|
|
66
|
-
sRGB gradient ramp, including interior blends rather than only its endpoints.
|
|
67
|
-
If neither foreground reaches 4.5:1 everywhere, resolution rejects the
|
|
68
|
-
background and asks for a consistently readable choice.
|
|
69
|
-
|
|
70
|
-
An explicitly supplied foreground is preserved, never silently replaced. It
|
|
71
|
-
must reach 4.5:1 across the solid background or entire gradient ramp; otherwise
|
|
72
|
-
input resolution and replay validation reject it with the failing brand path
|
|
73
|
-
and minimum contrast. Surface tokens remain independent. Templates that put
|
|
74
|
-
text on `surface` or `surfaceElevated` internally keep an accessible preferred
|
|
75
|
-
semantic text color or derive black/white for that surface.
|
|
76
|
-
|
|
77
|
-
Use an approved public or signed URL for logos. Fonts must be licensed and
|
|
78
|
-
loaded by the host application before playback. Always test both orientations
|
|
79
|
-
and forced browser zoom.
|
|
80
|
-
|
|
81
|
-
`personalization` accepts JSON-safe viewer or account context such as
|
|
82
|
-
`firstName`, `role`, `accountName`, `locale`, `goal`, or `onboardingPartner`.
|
|
83
|
-
These values may appear verbatim in the response, but are always data, never
|
|
84
|
-
instructions. Put product, news, metric, and quoted claims in `input`; do not
|
|
85
|
-
duplicate ordinary viewer context there.
|
|
86
|
-
|
|
87
|
-
Only include information the authenticated viewer is allowed to see. Avoid sensitive HR, health, payment, or customer attributes in event logs and analytics. If a locale changes number/date formatting, format facts server-side so the planner cannot guess.
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { defineTemplate } from "@vanillaskyai/video/templates";
|
|
2
|
-
|
|
3
|
-
const previewImageUrl = "data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%221200%22%20height=%22720%22%20viewBox=%220%200%201200%20720%22%3E%3Crect%20width=%221200%22%20height=%22720%22%20rx=%2248%22%20fill=%22%23181733%22/%3E%3Crect%20x=%2280%22%20y=%2280%22%20width=%221040%22%20height=%22560%22%20rx=%2232%22%20fill=%22%23f4f1ff%22/%3E%3Ccircle%20cx=%22220%22%20cy=%22230%22%20r=%2270%22%20fill=%22%237867dd%22/%3E%3Cpath%20d=%22M160%20520L380%20360L580%20430L820%20220L1040%20300%22%20fill=%22none%22%20stroke=%22%23287b77%22%20stroke-width=%2232%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22/%3E%3C/svg%3E";
|
|
4
|
-
|
|
5
|
-
export const template = defineTemplate({
|
|
6
|
-
id: "supplied-media",
|
|
7
|
-
label: "Supplied media",
|
|
8
|
-
description: "An application-supplied screenshot with a grounded caption.",
|
|
9
|
-
useWhen: "Use when a supplied product image is the strongest evidence for the answer.",
|
|
10
|
-
avoidWhen: "Avoid when no approved image was supplied or text and data communicate the point more clearly.",
|
|
11
|
-
schema: {
|
|
12
|
-
type: "object",
|
|
13
|
-
properties: {
|
|
14
|
-
imageUrl: {
|
|
15
|
-
type: "string",
|
|
16
|
-
format: "supplied-image",
|
|
17
|
-
title: "Supplied image",
|
|
18
|
-
description: "The URL of an image present in VideoInput.suppliedMedia.",
|
|
19
|
-
default: previewImageUrl,
|
|
20
|
-
},
|
|
21
|
-
headline: {
|
|
22
|
-
type: "string",
|
|
23
|
-
title: "Headline",
|
|
24
|
-
minLength: 1,
|
|
25
|
-
maxLength: 72,
|
|
26
|
-
default: "See the change in context.",
|
|
27
|
-
},
|
|
28
|
-
caption: {
|
|
29
|
-
type: "string",
|
|
30
|
-
title: "Caption",
|
|
31
|
-
minLength: 1,
|
|
32
|
-
maxLength: 120,
|
|
33
|
-
default: "The dashboard reflects the grounded result described in the answer.",
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
required: ["imageUrl", "headline", "caption"],
|
|
37
|
-
additionalProperties: false,
|
|
38
|
-
} as const,
|
|
39
|
-
examples: [{
|
|
40
|
-
name: "Product dashboard",
|
|
41
|
-
variables: {
|
|
42
|
-
imageUrl: previewImageUrl,
|
|
43
|
-
headline: "See the change in context.",
|
|
44
|
-
caption: "The dashboard reflects the grounded result described in the answer.",
|
|
45
|
-
},
|
|
46
|
-
}],
|
|
47
|
-
minDuration: 4,
|
|
48
|
-
preferredDuration: 6,
|
|
49
|
-
component: ({ variables, progress, width, height, safeZone }) => {
|
|
50
|
-
const isPortrait = height >= width;
|
|
51
|
-
const reveal = Math.max(0, Math.min(1, progress * 1.7));
|
|
52
|
-
|
|
53
|
-
return <section style={{
|
|
54
|
-
boxSizing: "border-box",
|
|
55
|
-
width,
|
|
56
|
-
height,
|
|
57
|
-
padding: `${safeZone.top}px ${safeZone.right}px ${safeZone.bottom}px ${safeZone.left}px`,
|
|
58
|
-
display: "grid",
|
|
59
|
-
gridTemplateColumns: isPortrait ? "1fr" : "minmax(0, 1.1fr) minmax(320px, 0.9fr)",
|
|
60
|
-
alignContent: "center",
|
|
61
|
-
alignItems: "center",
|
|
62
|
-
gap: isPortrait ? 44 : 64,
|
|
63
|
-
overflow: "hidden",
|
|
64
|
-
color: "#15152b",
|
|
65
|
-
background: "linear-gradient(145deg, #f4f1ff, #dfe7ff)",
|
|
66
|
-
fontFamily: "Inter, ui-sans-serif, system-ui, sans-serif",
|
|
67
|
-
}}>
|
|
68
|
-
<img
|
|
69
|
-
src={variables.imageUrl}
|
|
70
|
-
alt=""
|
|
71
|
-
style={{
|
|
72
|
-
display: "block",
|
|
73
|
-
width: "100%",
|
|
74
|
-
maxHeight: isPortrait ? height * 0.48 : height * 0.68,
|
|
75
|
-
objectFit: "cover",
|
|
76
|
-
borderRadius: 28,
|
|
77
|
-
boxShadow: "0 28px 80px rgba(28, 34, 75, 0.24)",
|
|
78
|
-
opacity: reveal,
|
|
79
|
-
transform: `scale(${0.96 + reveal * 0.04})`,
|
|
80
|
-
}}
|
|
81
|
-
/>
|
|
82
|
-
<div style={{ opacity: reveal }}>
|
|
83
|
-
<h1 style={{ margin: 0, fontSize: isPortrait ? 58 : 70, lineHeight: 0.98, letterSpacing: "-0.045em" }}>
|
|
84
|
-
{variables.headline}
|
|
85
|
-
</h1>
|
|
86
|
-
<p style={{ margin: "24px 0 0", fontSize: isPortrait ? 25 : 28, lineHeight: 1.4 }}>
|
|
87
|
-
{variables.caption}
|
|
88
|
-
</p>
|
|
89
|
-
</div>
|
|
90
|
-
</section>;
|
|
91
|
-
},
|
|
92
|
-
});
|