@vanillaskyai/video 0.10.2 → 0.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/PUBLIC-API.md +43 -31
  3. package/README.md +18 -9
  4. package/dist/check-runtime.js +2 -2
  5. package/dist/{chunk-KHLO5OHT.js → chunk-G5TYLTL5.js} +1 -1
  6. package/dist/{chunk-77VY4O7A.js → chunk-O6FBIB4L.js} +58 -10
  7. package/dist/{chunk-TVIU23OM.js → chunk-OOBT4X46.js} +13 -3
  8. package/dist/{chunk-5C6HZNHY.js → chunk-QZAZT44G.js} +3 -2
  9. package/dist/{chunk-6KZGF63O.js → chunk-WZFLEPLM.js} +58 -35
  10. package/dist/{chunk-VQH3JTQC.js → chunk-Z2ZI5G7O.js} +1 -1
  11. package/dist/{cinema-media-APDJS7SC.js → cinema-media-3SIGVVCY.js} +4 -4
  12. package/dist/{comparison-ZVE2DE7U.js → comparison-VDGRWPB5.js} +4 -4
  13. package/dist/{editorial-timeline-YIXXVJUV.js → editorial-timeline-QJU4JJWB.js} +4 -4
  14. package/dist/{key-figure-I2C37FWV.js → key-figure-YUAQW6OE.js} +4 -4
  15. package/dist/{mobile-message-JSGBU6EX.js → mobile-message-ZMMTUDUQ.js} +4 -4
  16. package/dist/{quote-XH54G3FS.js → quote-BB6UJT6L.js} +4 -4
  17. package/dist/react.d.ts +3 -1
  18. package/dist/react.js +94 -84
  19. package/dist/{scene-video-backdrop-PCTARAAJ.js → scene-video-backdrop-4EZVVQY7.js} +2 -2
  20. package/dist/server.d.ts +15 -3
  21. package/dist/server.js +416 -275
  22. package/dist/{types-CdBmNTGD.d.ts → types-BqB8zC9u.d.ts} +1 -1
  23. package/docs/agent-integration.md +5 -4
  24. package/docs/concepts.md +2 -2
  25. package/docs/development.md +23 -0
  26. package/docs/getting-started.md +7 -5
  27. package/docs/media-and-audio.md +37 -18
  28. package/docs/performance.md +21 -10
  29. package/docs/production.md +11 -10
  30. package/docs/prompt-and-input.md +24 -15
  31. package/docs/provider-integration.md +11 -13
  32. package/docs/reference/protocol.md +7 -1
  33. package/docs/reference/provider-adapters.md +7 -4
  34. package/docs/testing.md +7 -2
  35. package/package.json +5 -2
  36. package/registry/items/backgrounds.json +2 -2
  37. package/registry/items/cinemaMedia.json +1 -1
  38. package/registry/items/comparison.json +1 -1
  39. package/registry/items/editorialTimeline.json +1 -1
  40. package/registry/items/keyFigure.json +1 -1
  41. package/registry/items/mobileMessage.json +1 -1
  42. package/registry/items/quote.json +1 -1
  43. package/starters/video-chat/README.md +25 -26
  44. package/starters/video-chat/package.json +1 -1
  45. package/starters/video-chat/providers/video.ts +10 -6
  46. package/starters/video-chat/server.ts +1 -1
  47. package/starters/video-chat/stock.ts +59 -31
  48. package/styles/video-chat.css +6 -61
@@ -42,7 +42,7 @@ export const handleVideoChat = createVideoChatHandler({
42
42
  return text;
43
43
  },
44
44
  searchMedia: process.env.PEXELS_API_KEY
45
- ? (query, { orientation, signal, fallbackQuery }) => findStockFootage(query, orientation, signal, fallbackQuery)
45
+ ? (query, { orientation, signal }) => findStockFootage(query, orientation, signal)
46
46
  : undefined,
47
47
  ...providers,
48
48
  welcome: {
@@ -1,38 +1,66 @@
1
1
  import type { VideoOrientation } from "@vanillaskyai/video";
2
2
 
3
- export interface ApprovedStock {
4
- /** Literal phrases the inspected clip actually depicts; no broad topic aliases. */
5
- queries: readonly string[];
6
- orientations: readonly VideoOrientation[];
7
- description: string;
8
- reviewedAt: string;
9
- media: { url: string; type: "video" | "image"; posterUrl?: string };
3
+ interface StockVideo {
4
+ url: string;
5
+ type: "video";
6
+ posterUrl?: string;
10
7
  }
11
-
12
- /** Add assets only after watching the clip and checking its crop and poster.
13
- * An empty collection deliberately declines stock instead of guessing from
14
- * search rank. The host can maintain this reviewed index independently of the SDK.
15
- */
16
- export const approvedStock: readonly ApprovedStock[] = [];
17
-
18
- const normalize = (value: string) => value.trim().toLowerCase().replace(/\s+/g, ' ');
19
- function approvedUrl(value: string): boolean {
20
- try { const url=new URL(value);return url.protocol==='https:' && !url.username && !url.password && !url.port; }
21
- catch { return false; }
8
+ interface PexelsVideo {
9
+ url?: string;
10
+ image?: string;
11
+ video_files?: { link?: string; width?: number; height?: number; file_type?: string }[];
12
+ }
13
+ const cache = new Map<string, { expires: number; media: StockVideo | null }>();
14
+ const words = (value: string): string[] => value.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
15
+ function pexelsUrl(value: unknown): value is string {
16
+ if (typeof value !== "string") return false;
17
+ try {
18
+ const url = new URL(value);
19
+ return url.protocol === "https:" && !url.username && !url.password && !url.port
20
+ && (url.hostname === "pexels.com" || url.hostname.endsWith(".pexels.com"));
21
+ } catch { return false; }
22
22
  }
23
23
 
24
- export async function findStockFootage(
25
- query: string,
26
- orientation: VideoOrientation,
27
- signal: AbortSignal,
28
- _fallbackQuery?: string,
29
- index: readonly ApprovedStock[] = approvedStock,
30
- ) {
24
+ /** Full catalog search. Metadata establishes subject relevance, not factual proof.
25
+ * Applications using this adapter must display a prominent link to Pexels.
26
+ * https://www.pexels.com/api/documentation/#guidelines
27
+ */
28
+ export async function findStockFootage(query: string, orientation: VideoOrientation, signal: AbortSignal) {
29
+ signal.throwIfAborted();
30
+ const normalized = query.trim().toLowerCase().replace(/\s+/g, " ");
31
+ const tokens = words(normalized);
32
+ const key = `${orientation}:${normalized}`;
33
+ const apiKey = process.env.PEXELS_API_KEY;
34
+ if (!apiKey || !tokens.length || normalized.length > 80 || tokens.length > 8) return null;
35
+ const existing = cache.get(key);
36
+ if (existing && existing.expires > Date.now()) return existing.media;
37
+ const url = new URL("https://api.pexels.com/v1/videos/search");
38
+ url.search = new URLSearchParams({ query: normalized, orientation, per_page: "12", size: "medium" }).toString();
39
+ const response = await fetch(url, { headers: { Authorization: apiKey }, signal });
40
+ signal.throwIfAborted();
41
+ if (!response.ok) return null;
42
+ const result = await response.json() as { videos?: PexelsVideo[] };
31
43
  signal.throwIfAborted();
32
- const intent=normalize(query);
33
- if(!intent || intent.length>80 || intent.split(' ').length>8)return null;
34
- const entry=index.find(asset=>asset.reviewedAt && asset.description && asset.orientations.includes(orientation)
35
- && asset.queries.some(candidate=>normalize(candidate)===intent)
36
- && approvedUrl(asset.media.url) && (!asset.media.posterUrl || approvedUrl(asset.media.posterUrl)));
37
- return entry ? {...entry.media} : null;
44
+ let selected: StockVideo | null = null, bestScore = 0;
45
+ for (const video of (Array.isArray(result.videos) ? result.videos : []).slice(0, 12)) {
46
+ if (!pexelsUrl(video.url)) continue;
47
+ const subject = words(new URL(video.url).pathname);
48
+ const matches = tokens.filter(token => subject.includes(token)).length;
49
+ // Require a majority of the literal query, never accept search rank alone.
50
+ if (matches < Math.ceil(tokens.length * 0.6)) continue;
51
+ const files = (Array.isArray(video.video_files) ? video.video_files : []).filter(file =>
52
+ file.file_type === "video/mp4" && pexelsUrl(file.link)
53
+ && Number.isFinite(file.width) && Number.isFinite(file.height)
54
+ && Math.min(file.width!, file.height!) >= 360
55
+ && (orientation === "portrait" ? file.height! > file.width! : file.width! >= file.height!),
56
+ ).sort((a, b) => Math.abs(Math.max(a.width!, a.height!) - 1280) - Math.abs(Math.max(b.width!, b.height!) - 1280));
57
+ const file = files[0];
58
+ if (!file || matches <= bestScore) continue;
59
+ bestScore = matches;
60
+ selected = { url: file.link!, type: "video", ...(pexelsUrl(video.image) ? { posterUrl: video.image } : {}) };
61
+ }
62
+ // Bounded process-local cache; no request signal or credentials are retained.
63
+ if (cache.size >= 128) cache.delete(cache.keys().next().value!);
64
+ cache.set(key, { expires: Date.now() + (selected ? 24 * 60 * 60_000 : 60_000), media: selected });
65
+ return selected;
38
66
  }
@@ -193,59 +193,6 @@
193
193
 
194
194
  .vanillasky-video-chat .stage :where(video, canvas, img) { max-width: 100%; max-height: 100%; object-fit: contain; }
195
195
 
196
- .vanillasky-video-chat .ground {
197
- position: absolute;
198
- inset: -30%;
199
- background: radial-gradient(closest-side, oklch(1 0 0 / 28%), transparent 70%);
200
- animation: vanillasky-video-chat-drift 9s ease-in-out infinite alternate;
201
- }
202
-
203
- @keyframes vanillasky-video-chat-drift {
204
- from { transform: translate3d(-12%, -8%, 0) scale(1); }
205
- to { transform: translate3d(14%, 10%, 0) scale(1.25); }
206
- }
207
-
208
- .vanillasky-video-chat .asked {
209
- margin: 0 auto;
210
- padding: 0 1.5rem;
211
- display: grid;
212
- gap: 1.1rem;
213
- justify-items: center;
214
- align-content: center;
215
- text-align: center;
216
- color: var(--vs-on-media);
217
- }
218
-
219
- .vanillasky-video-chat .asked-prompt {
220
- margin: 0;
221
- max-width: 26ch;
222
- font-size: var(--vs-text-display);
223
- line-height: var(--vs-lead-display);
224
- letter-spacing: var(--vs-track-display);
225
- font-weight: 700;
226
- text-wrap: balance;
227
- animation: vanillasky-video-chat-step-in 620ms ease both;
228
- }
229
-
230
- .vanillasky-video-chat .asked-step {
231
- margin: 0;
232
- font-size: var(--vs-text-subhead);
233
- line-height: var(--vs-lead-subhead);
234
- letter-spacing: var(--vs-track-subhead);
235
- font-weight: 500;
236
- background: linear-gradient(100deg, oklch(1 0 0 / 45%) 20%, oklch(1 0 0) 42%, oklch(1 0 0 / 45%) 64%);
237
- background-size: 220% 100%;
238
- -webkit-background-clip: text;
239
- background-clip: text;
240
- color: transparent;
241
- animation: vanillasky-video-chat-sweep 2.4s linear infinite, vanillasky-video-chat-step-in 520ms ease both;
242
- }
243
-
244
- @keyframes vanillasky-video-chat-step-in {
245
- from { opacity: 0; transform: translateY(6px); }
246
- to { opacity: 1; transform: none; }
247
- }
248
-
249
196
  .vanillasky-video-chat .welcome { position: absolute; inset: 0; overflow: hidden; }
250
197
 
251
198
  .vanillasky-video-chat .frame-media { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
@@ -463,7 +410,6 @@
463
410
  animation: vanillasky-video-chat-fade-in 320ms ease both;
464
411
  }
465
412
 
466
- @keyframes vanillasky-video-chat-sweep { from { background-position: 140% 0; } to { background-position: -40% 0; } }
467
413
 
468
414
  @keyframes vanillasky-video-chat-fade-in { from { opacity: 0; } to { opacity: 1; } }
469
415
 
@@ -573,7 +519,6 @@
573
519
  animation-iteration-count: 1 !important;
574
520
  transition-duration: 0.01ms !important;
575
521
  }
576
- .vanillasky-video-chat .asked-step { color: var(--vs-on-media); -webkit-text-fill-color: currentColor; }
577
522
  }
578
523
 
579
524
  .vanillasky-video-chat {
@@ -1053,12 +998,7 @@
1053
998
  @media (max-width: 700px) {
1054
999
  .vanillasky-video-chat .sheet-popover { max-height: calc(100% - 102px); }
1055
1000
  }
1056
- .vanillasky-video-chat .opening-wash {
1057
- position: absolute;
1058
- inset: 0;
1059
- background: linear-gradient(180deg, rgb(8 11 22 / 12%), rgb(8 11 22 / 42%));
1060
- pointer-events: none;
1061
- }
1001
+
1062
1002
  .vanillasky-video-chat .ending { pointer-events: none; }
1063
1003
  .vanillasky-video-chat .ending .cards { pointer-events: auto; }
1064
1004
 
@@ -1138,3 +1078,8 @@
1138
1078
  backdrop-filter: none;
1139
1079
  -webkit-backdrop-filter: none;
1140
1080
  }
1081
+
1082
+ .vanillasky-video-chat .opening-chapter { animation: vanillasky-chapter-enter 450ms ease-out both; }
1083
+ .vanillasky-video-chat .media-credit { color: var(--vs-fg); font-size: 11px; margin-left: 12px; text-underline-offset: 3px; }
1084
+ @keyframes vanillasky-chapter-enter { from { opacity: 0; } to { opacity: 1; } }
1085
+ @media (prefers-reduced-motion: reduce) { .vanillasky-video-chat .opening-chapter { animation: none; } }