@vanillaskyai/video 0.10.7 → 0.10.9

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.
@@ -9,7 +9,7 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
12
- "@vanillaskyai/video": "0.10.7",
12
+ "@vanillaskyai/video": "0.10.9",
13
13
  "react": "^19.2.8",
14
14
  "react-dom": "^19.2.8",
15
15
  "@ai-sdk/anthropic": "^3.0.0",
@@ -25,11 +25,6 @@ export const handleVideoChat = createVideoChatHandler({
25
25
  output_config: { effort: "medium" },
26
26
  },
27
27
  },
28
- onFinish: ({ finishReason, text }) => {
29
- if (finishReason !== "stop") console.warn(`[planner] ${finishReason}: ${text.slice(0, 300)}`);
30
- const keyed = (text.match(/"mediaKeyword"/g) ?? []).length;
31
- console.log(`[planner] ${(text.match(/"scene\.add"/g) ?? []).length} scenes, ${keyed} asked for footage`);
32
- },
33
28
  }),
34
29
  generateText: async ({ systemPrompt, userPrompt, maxOutputTokens, signal }) => {
35
30
  const { text } = await generateText({
@@ -42,7 +37,7 @@ export const handleVideoChat = createVideoChatHandler({
42
37
  return text;
43
38
  },
44
39
  searchMedia: process.env.PEXELS_API_KEY
45
- ? (query, { orientation, signal }) => findStockFootage(query, orientation, signal)
40
+ ? (query, { orientation, signal, scene }) => findStockFootage(query, orientation, signal, scene?.variables.stockSelection)
46
41
  : undefined,
47
42
  ...providers,
48
43
  welcome: {
@@ -9,11 +9,27 @@ interface PexelsVideo {
9
9
  url?: string;
10
10
  image?: string;
11
11
  title?: unknown;
12
+ description?: unknown;
12
13
  tags?: unknown;
13
14
  video_files?: { link?: string; width?: number; height?: number; file_type?: string }[];
14
15
  }
15
16
  const cache = new Map<string, { expires: number; media: StockVideo | null }>();
16
17
  const words = (value: string): string[] => value.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
18
+ const ignored = new Set(['a','an','the','in','on','at','of','with','and','to']);
19
+ const terms = (value: string) => words(value).filter(word => !ignored.has(word));
20
+ function selectionHint(value: unknown) {
21
+ const phrase = (input: unknown) => {
22
+ if (typeof input !== 'string') return undefined;
23
+ const normalized = input.trim().toLowerCase().replace(/\s+/gu,' ').replaceAll('’', "'");
24
+ return normalized.length <= 48 && /^[\p{L}\p{N} '-]+$/u.test(normalized) && words(normalized).length <= 4 && terms(normalized).length ? normalized : undefined;
25
+ };
26
+ const raw = value && typeof value === 'object' ? value as Record<string, unknown> : {};
27
+ const subject = phrase(raw.subject);
28
+ if (!subject) return undefined;
29
+ const activity = phrase(raw.activity), equipment = phrase(raw.equipment);
30
+ const exclude = Array.isArray(raw.exclude) && raw.exclude.length <= 3 ? raw.exclude.map(phrase).filter((item): item is string => Boolean(item)) : [];
31
+ return {subject, ...(activity ? {activity} : {}), ...(equipment ? {equipment} : {}), ...(exclude.length ? {exclude} : {})};
32
+ }
17
33
  function pexelsUrl(value: unknown): value is string {
18
34
  if (typeof value !== "string") return false;
19
35
  try {
@@ -27,11 +43,12 @@ function pexelsUrl(value: unknown): value is string {
27
43
  * Applications using this adapter must display a prominent link to Pexels.
28
44
  * https://www.pexels.com/api/documentation/#guidelines
29
45
  */
30
- export async function findStockFootage(query: string, orientation: VideoOrientation, signal: AbortSignal) {
46
+ export async function findStockFootage(query: string, orientation: VideoOrientation, signal: AbortSignal, rawSelection?: unknown) {
31
47
  signal.throwIfAborted();
32
48
  const normalized = query.trim().toLowerCase().replace(/\s+/g, " ");
33
49
  const tokens = words(normalized);
34
- const key = `${orientation}:${normalized}`;
50
+ const selection = selectionHint(rawSelection);
51
+ const key = JSON.stringify({version: 2, orientation, query: normalized, selection});
35
52
  const apiKey = process.env.PEXELS_API_KEY;
36
53
  if (!apiKey || !tokens.length || normalized.length > 80 || tokens.length > 8) return null;
37
54
  const existing = cache.get(key);
@@ -49,8 +66,14 @@ export async function findStockFootage(query: string, orientation: VideoOrientat
49
66
  const slug = new URL(video.url).pathname.replace(/^\/video\//, "");
50
67
  const title = typeof video.title === "string" ? video.title : "";
51
68
  const tags = Array.isArray(video.tags) ? video.tags.filter((tag): tag is string => typeof tag === "string").join(" ") : "";
52
- const subject = words(`${slug} ${title} ${tags}`).filter(token => !/^\d+$/.test(token));
53
- const matches = tokens.filter(token => subject.includes(token)).length;
69
+ const subject = words(`${slug} ${title} ${typeof video.description === "string" ? video.description : ""} ${tags}`).filter(token => !/^\d+$/.test(token));
70
+ let matches = tokens.filter(token => subject.includes(token)).length;
71
+ if (selection && subject.length) {
72
+ const covers = (phrase: string) => terms(phrase).every(word => subject.includes(word));
73
+ if (!covers(selection.subject) || selection.exclude?.some(covers)) continue;
74
+ matches = 2 + Number(Boolean(selection.activity && covers(selection.activity)))
75
+ + Number(Boolean(selection.equipment && covers(selection.equipment)));
76
+ }
54
77
  // The documented Video resource can have only a numeric page URL and no
55
78
  // editorial metadata. Preserve provider search order for unknown relevance;
56
79
  // positive overlap ranks above it, while explicitly unrelated copy is skipped.
@@ -1,207 +0,0 @@
1
- import {
2
- BrandGradientOverlay,
3
- SceneVideoBackdrop,
4
- getBackgroundTransform,
5
- resolveMediaPosition
6
- } from "./chunk-OBRKB7PL.js";
7
- import {
8
- resolveMediaType
9
- } from "./chunk-224QNWRA.js";
10
- import {
11
- useExternalVideoBackdrop
12
- } from "./chunk-OOBT4X46.js";
13
-
14
- // src/visual-system/scene-templates/scene-background.tsx
15
- import { useEffect, useState } from "react";
16
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
17
- function resolveMediaTreatment(value) {
18
- return value === "none" || value === "subtle" || value === "text-safe" ? value : "cinematic";
19
- }
20
- var SCRIM_STOP_COUNT = 7;
21
- function smoothstep(t) {
22
- return t * t * (3 - 2 * t);
23
- }
24
- function easedStops(peakAlpha, start, end, direction) {
25
- const alphaAt = (t) => {
26
- const eased = direction === "fade-out" ? 1 - smoothstep(t) : smoothstep(t);
27
- return `rgba(0,0,0,${Number((peakAlpha * eased).toFixed(3))})`;
28
- };
29
- const stops = [];
30
- if (start > 0) stops.push(`${alphaAt(0)} 0%`);
31
- for (let i = 0; i < SCRIM_STOP_COUNT; i += 1) {
32
- const t = i / (SCRIM_STOP_COUNT - 1);
33
- const position = Number((start + (end - start) * t).toFixed(2));
34
- stops.push(`${alphaAt(t)} ${position}%`);
35
- }
36
- if (end < 100) stops.push(`${alphaAt(1)} 100%`);
37
- return stops.join(", ");
38
- }
39
- function getMediaTreatmentLayers(value, anchor = "full") {
40
- const treatment = resolveMediaTreatment(value);
41
- if (treatment === "none") return [];
42
- const vignette = {
43
- id: "vignette",
44
- background: treatment === "subtle" ? `radial-gradient(ellipse at center, ${easedStops(0.28, 45, 100, "fade-in")})` : `radial-gradient(ellipse at center, ${easedStops(0.72, 32, 100, "fade-in")})`
45
- };
46
- if (treatment === "subtle") return [vignette];
47
- const textSafe = treatment === "text-safe";
48
- const layers = [vignette];
49
- if (anchor !== "bottom") {
50
- layers.push({
51
- id: "center-scrim",
52
- background: textSafe ? `radial-gradient(ellipse 92% 58% at 50% 50%, ${easedStops(0.46, 34, 90, "fade-out")})` : `radial-gradient(ellipse 88% 52% at 50% 50%, ${easedStops(0.26, 30, 88, "fade-out")})`
53
- });
54
- }
55
- if (anchor !== "center") {
56
- layers.push({
57
- id: "bottom-scrim",
58
- background: `linear-gradient(to top, ${easedStops(textSafe ? 0.64 : 0.5, 8, 100, "fade-out")})`,
59
- style: { top: "55%" }
60
- });
61
- }
62
- return layers;
63
- }
64
- function initialMediaPaint(wantsMedia, resolved, mediaUrl, mediaPoster) {
65
- if (typeof window === "undefined") return "ready";
66
- if (!wantsMedia) return "ready";
67
- if (resolved === "video") return mediaPoster ? "ready" : "pending";
68
- if (typeof Image === "undefined") return "ready";
69
- const cached = new Image();
70
- cached.src = mediaUrl;
71
- return cached.complete && cached.naturalWidth > 0 ? "ready" : "pending";
72
- }
73
- function getMediaBackgroundProps(variables) {
74
- return {
75
- mediaUrl: String(variables.mediaUrl || ""),
76
- mediaType: String(variables.mediaType || "auto"),
77
- mediaPoster: String(variables.mediaPoster || ""),
78
- mediaPosition: String(variables.mediaPosition || "center"),
79
- mediaTreatment: String(variables.mediaTreatment || "cinematic")
80
- };
81
- }
82
- var SceneBackground = ({
83
- style,
84
- progress,
85
- sceneDuration,
86
- width: _width,
87
- // accepted for symmetry; not currently used in render
88
- height: _height,
89
- mediaUrl = "",
90
- mediaType = "auto",
91
- mediaPoster,
92
- mediaPosition = "center",
93
- mediaTreatment = "cinematic",
94
- textAnchor = "full",
95
- backgroundEffect,
96
- seed,
97
- isPlaying = true,
98
- beatIntensity = 0
99
- }) => {
100
- void _width;
101
- void _height;
102
- const resolved = resolveMediaType(mediaType, mediaUrl);
103
- const wantsMedia = resolved !== "gradient" && !!mediaUrl;
104
- const externalVideoBackdrop = useExternalVideoBackdrop();
105
- const hasExternalVideoBackdrop = externalVideoBackdrop !== false && resolved === "video";
106
- const externalVideoFailed = externalVideoBackdrop === "fallback" && resolved === "video";
107
- const externalVideoReady = externalVideoBackdrop === "ready" && resolved === "video";
108
- const [mediaPaint, setMediaPaint] = useState(
109
- () => initialMediaPaint(wantsMedia, resolved, mediaUrl, mediaPoster)
110
- );
111
- useEffect(() => {
112
- setMediaPaint(initialMediaPaint(wantsMedia, resolved, mediaUrl, mediaPoster));
113
- if (!wantsMedia || resolved !== "photo") return;
114
- if (typeof Image === "undefined") return;
115
- let cancelled = false;
116
- const probe = new Image();
117
- probe.onload = () => {
118
- if (!cancelled) setMediaPaint("ready");
119
- };
120
- probe.onerror = () => {
121
- if (!cancelled) setMediaPaint("failed");
122
- };
123
- probe.src = mediaUrl;
124
- if (probe.complete) setMediaPaint(probe.naturalWidth > 0 ? "ready" : "failed");
125
- return () => {
126
- cancelled = true;
127
- probe.onload = null;
128
- probe.onerror = null;
129
- };
130
- }, [mediaUrl, mediaPoster, resolved, wantsMedia]);
131
- const showMedia = wantsMedia && mediaPaint !== "failed";
132
- const showTreatment = wantsMedia && mediaPaint === "ready";
133
- const resolvedPosition = resolveMediaPosition(mediaPosition);
134
- const resolvedTreatment = resolveMediaTreatment(mediaTreatment);
135
- const treatmentLayers = getMediaTreatmentLayers(resolvedTreatment, textAnchor);
136
- const gradSeed = typeof seed === "number" ? seed : typeof seed === "string" ? seed.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0) : 0;
137
- const bgTransform = getBackgroundTransform(
138
- backgroundEffect,
139
- progress,
140
- beatIntensity
141
- );
142
- return /* @__PURE__ */ jsxs(Fragment, { children: [
143
- (!hasExternalVideoBackdrop || externalVideoFailed) && /* @__PURE__ */ jsx(
144
- BrandGradientOverlay,
145
- {
146
- style,
147
- progress,
148
- sceneDuration,
149
- seed: gradSeed
150
- }
151
- ),
152
- showMedia && !hasExternalVideoBackdrop && (resolved === "video" ? /* @__PURE__ */ jsx(
153
- SceneVideoBackdrop,
154
- {
155
- mediaUrl,
156
- mediaPoster,
157
- mediaPosition,
158
- backgroundEffect,
159
- progress,
160
- sceneDuration,
161
- beatIntensity,
162
- isPlaying,
163
- onReady: () => setMediaPaint("ready"),
164
- onError: () => setMediaPaint("failed")
165
- }
166
- ) : /* @__PURE__ */ jsx(
167
- "img",
168
- {
169
- src: mediaUrl,
170
- alt: "",
171
- "aria-hidden": "true",
172
- draggable: false,
173
- "data-media-position": mediaPosition,
174
- style: {
175
- position: "absolute",
176
- inset: 0,
177
- transform: bgTransform.transform,
178
- transformOrigin: bgTransform.transformOrigin,
179
- width: "100%",
180
- height: "100%",
181
- objectFit: "cover",
182
- objectPosition: resolvedPosition
183
- }
184
- }
185
- )),
186
- (hasExternalVideoBackdrop ? externalVideoReady : showTreatment) && !externalVideoFailed && treatmentLayers.map((layer) => /* @__PURE__ */ jsx(
187
- "div",
188
- {
189
- "data-media-treatment": resolvedTreatment,
190
- "data-media-overlay": layer.id,
191
- style: {
192
- position: "absolute",
193
- inset: 0,
194
- background: layer.background,
195
- pointerEvents: "none",
196
- ...layer.style
197
- }
198
- },
199
- layer.id
200
- ))
201
- ] });
202
- };
203
-
204
- export {
205
- getMediaBackgroundProps,
206
- SceneBackground
207
- };
@@ -1,7 +0,0 @@
1
- import {
2
- SceneVideoBackdrop
3
- } from "./chunk-OBRKB7PL.js";
4
- import "./chunk-OOBT4X46.js";
5
- export {
6
- SceneVideoBackdrop
7
- };
@@ -1,37 +1,3 @@
1
- // src/visual-system/scene-templates/external-video-backdrop.tsx
2
- import React from "react";
3
- import { jsx } from "react/jsx-runtime";
4
- var DEFAULT = { mode: false, audioMuted: true, audioVolume: 1 };
5
- var sharedContext = globalThis;
6
- var BackdropContext = sharedContext.__vanillaskyVideoBackdropContext ??= React.createContext(DEFAULT);
7
- function ExternalVideoBackdropProvider({
8
- mode,
9
- audioMuted = true,
10
- audioVolume = 1,
11
- preparingNarration = false,
12
- onMediaError,
13
- children
14
- }) {
15
- const value = React.useMemo(
16
- () => ({ mode, audioMuted, audioVolume, preparingNarration, onMediaError }),
17
- [mode, audioMuted, audioVolume, preparingNarration, onMediaError]
18
- );
19
- return /* @__PURE__ */ jsx(BackdropContext.Provider, { value, children });
20
- }
21
- function useExternalVideoBackdrop() {
22
- return React.useContext(BackdropContext).mode;
23
- }
24
- function useMediaAudio() {
25
- const { audioMuted, audioVolume } = React.useContext(BackdropContext);
26
- return { muted: audioMuted, volume: audioVolume };
27
- }
28
- function useNarrationPreroll() {
29
- return React.useContext(BackdropContext).preparingNarration === true;
30
- }
31
- function useMediaFailure() {
32
- return React.useContext(BackdropContext).onMediaError;
33
- }
34
-
35
1
  // src/visual-system/scene-templates/tokens.ts
36
2
  var STYLE_PRESETS = {
37
3
  bold: {
@@ -144,6 +110,40 @@ function darken(hex, factor) {
144
110
  return `#${ch(1)}${ch(3)}${ch(5)}`;
145
111
  }
146
112
 
113
+ // src/visual-system/scene-templates/external-video-backdrop.tsx
114
+ import React from "react";
115
+ import { jsx } from "react/jsx-runtime";
116
+ var DEFAULT = { mode: false, audioMuted: true, audioVolume: 1 };
117
+ var sharedContext = globalThis;
118
+ var BackdropContext = sharedContext.__vanillaskyVideoBackdropContext ??= React.createContext(DEFAULT);
119
+ function ExternalVideoBackdropProvider({
120
+ mode,
121
+ audioMuted = true,
122
+ audioVolume = 1,
123
+ preparingNarration = false,
124
+ onMediaError,
125
+ children
126
+ }) {
127
+ const value = React.useMemo(
128
+ () => ({ mode, audioMuted, audioVolume, preparingNarration, onMediaError }),
129
+ [mode, audioMuted, audioVolume, preparingNarration, onMediaError]
130
+ );
131
+ return /* @__PURE__ */ jsx(BackdropContext.Provider, { value, children });
132
+ }
133
+ function useExternalVideoBackdrop() {
134
+ return React.useContext(BackdropContext).mode;
135
+ }
136
+ function useMediaAudio() {
137
+ const { audioMuted, audioVolume } = React.useContext(BackdropContext);
138
+ return { muted: audioMuted, volume: audioVolume };
139
+ }
140
+ function useNarrationPreroll() {
141
+ return React.useContext(BackdropContext).preparingNarration === true;
142
+ }
143
+ function useMediaFailure() {
144
+ return React.useContext(BackdropContext).onMediaError;
145
+ }
146
+
147
147
  export {
148
148
  resolveDensity,
149
149
  resolveTokens,