@vanillaskyai/video 0.5.7 → 0.6.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 +27 -0
- package/PUBLIC-API.md +6 -0
- package/README.md +8 -0
- package/dist/{bg-media-FEBATBT2.js → bg-media-G4XOQEAS.js} +3 -3
- package/dist/{brand-message-P56WEJPG.js → brand-message-XHONEDNW.js} +3 -3
- package/dist/{chart-bar-ZB45ZRKH.js → chart-bar-HRHD57ML.js} +3 -3
- package/dist/{chart-counter-HFYNE4HT.js → chart-counter-KFPV44RB.js} +3 -3
- package/dist/{chart-progress-ring-ZKMLRGSB.js → chart-progress-ring-OR2VOIHV.js} +3 -3
- package/dist/check-runtime.js +2 -2
- package/dist/chunk-5CDAM24P.js +31 -0
- package/dist/{chunk-6M6DATLW.js → chunk-7BGAU6C3.js} +4 -4
- package/dist/{chunk-HT6BGERF.js → chunk-FRN6WKHA.js} +28 -10
- package/dist/{chunk-NGY3TNND.js → chunk-K746NUIP.js} +70 -50
- package/dist/{cta-logo-S4OMTNXD.js → cta-logo-JJOGK4UC.js} +3 -3
- package/dist/{cta-media-EROTHDRR.js → cta-media-IQ3MAWEH.js} +3 -3
- package/dist/{incoming-call-5IWJ2TAS.js → incoming-call-KXF67OT2.js} +3 -3
- package/dist/{infographic-feature-list-7SK65U3K.js → infographic-feature-list-QDM4I7XE.js} +3 -3
- package/dist/{infographic-problem-solution-IW35S6BR.js → infographic-problem-solution-FMLJCPG6.js} +3 -3
- package/dist/{infographic-stat-row-XHUZZPDM.js → infographic-stat-row-VFTIXS5F.js} +3 -3
- package/dist/{infographic-steps-JDDEDI7X.js → infographic-steps-L7W24Z35.js} +3 -3
- package/dist/{prompt-input-6ZWXEL3V.js → prompt-input-F3CYU2XH.js} +3 -3
- package/dist/react.d.ts +6 -0
- package/dist/react.js +33 -26
- package/dist/{reaction-Z3JIM3MQ.js → reaction-2XCLOJOK.js} +3 -3
- package/dist/{scene-video-backdrop-IL4F2SVD.js → scene-video-backdrop-XFPX4O3I.js} +2 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +5 -0
- package/dist/{showcase-code-NWQAAJIA.js → showcase-code-KHSMIPG6.js} +3 -3
- package/dist/{showcase-phone-5UW45ATT.js → showcase-phone-M4CEANIW.js} +3 -3
- package/dist/{showcase-terminal-UVOD25UF.js → showcase-terminal-26I3DYF7.js} +3 -3
- package/dist/{showcase-web-3AW6VHIG.js → showcase-web-TY6CZV2Z.js} +3 -3
- package/dist/{social-milestone-KPKHFOB5.js → social-milestone-TF6MX7ET.js} +3 -3
- package/dist/{social-notification-EDOU6TZB.js → social-notification-2RJDAZSF.js} +3 -3
- package/dist/{social-review-stack-EZOO33SV.js → social-review-stack-2ICKJQG7.js} +3 -3
- package/dist/{social-testimonial-DLOINMMY.js → social-testimonial-X7OGADA3.js} +3 -3
- package/dist/{social-tweet-6JUM323G.js → social-tweet-TK7SH2F6.js} +3 -3
- package/docs/media-and-audio.md +59 -0
- package/examples/nextjs-quickstart/package.json +1 -1
- package/examples/nextjs-quickstart/src/app/api/video/route.ts +13 -4
- package/examples/server-integrations/README.md +20 -0
- package/examples/server-integrations/src/ai-sdk-media.ts +90 -0
- package/package.json +2 -1
- package/registry/items/backgrounds.json +2 -2
- package/dist/chunk-C6WVCZRW.js +0 -19
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// App-owned media generation for VanillaSky scenes.
|
|
2
|
+
//
|
|
3
|
+
// VanillaSky never generates media and never selects a provider. It calls
|
|
4
|
+
// `resolveMedia` with a bounded query and the scene being resolved; the
|
|
5
|
+
// application decides what to generate, where to store it, and what it costs.
|
|
6
|
+
//
|
|
7
|
+
// This adapter is provider-neutral: it takes AI SDK model instances rather
|
|
8
|
+
// than naming a vendor, so the same code runs against any image or video
|
|
9
|
+
// model the AI SDK supports. Choose the provider where you build the models,
|
|
10
|
+
// not here.
|
|
11
|
+
import { experimental_generateVideo, generateImage, type ImageModel } from "ai";
|
|
12
|
+
import type { MediaResolver, MediaResolverContext, ResolvedMedia } from "@vanillaskyai/video/server";
|
|
13
|
+
|
|
14
|
+
// `ai` declares its video model and provider-option types internally without
|
|
15
|
+
// exporting them, so derive them from the call signatures instead of naming
|
|
16
|
+
// them. This keeps the adapter correct across AI SDK releases.
|
|
17
|
+
type VideoModel = Parameters<typeof experimental_generateVideo>[0]["model"];
|
|
18
|
+
type ProviderOptions = Parameters<typeof generateImage>[0]["providerOptions"];
|
|
19
|
+
|
|
20
|
+
/** Persist generated bytes and return a URL the player can load. */
|
|
21
|
+
export type StoreGeneratedMedia = (media: {
|
|
22
|
+
data: Uint8Array;
|
|
23
|
+
mediaType: string;
|
|
24
|
+
kind: "image" | "video";
|
|
25
|
+
/** Stable per scene, so a retried request reuses the stored object. */
|
|
26
|
+
idempotencyKey: string;
|
|
27
|
+
}) => Promise<{ url: string }>;
|
|
28
|
+
|
|
29
|
+
export interface GeneratedMediaOptions {
|
|
30
|
+
imageModel: ImageModel;
|
|
31
|
+
/** Omit to resolve every scene as an image. */
|
|
32
|
+
videoModel?: VideoModel;
|
|
33
|
+
store: StoreGeneratedMedia;
|
|
34
|
+
/** Provider-specific settings, passed straight through to the AI SDK. */
|
|
35
|
+
providerOptions?: ProviderOptions;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function aspectRatio(context: MediaResolverContext): "16:9" | "9:16" {
|
|
39
|
+
return context.input.orientation === "portrait" ? "9:16" : "16:9";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// One generated asset per scene, keyed so a retry of the same request cannot
|
|
43
|
+
// be billed twice.
|
|
44
|
+
function idempotencyKey(context: MediaResolverContext): string {
|
|
45
|
+
return `${context.requestId}:${context.scene.id}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createGeneratedMediaResolver(options: GeneratedMediaOptions): MediaResolver {
|
|
49
|
+
return async (query, context): Promise<ResolvedMedia | null> => {
|
|
50
|
+
const wantsVideo = context.preferredType === "video" && options.videoModel !== undefined;
|
|
51
|
+
|
|
52
|
+
if (wantsVideo && options.videoModel) {
|
|
53
|
+
const result = await experimental_generateVideo({
|
|
54
|
+
model: options.videoModel,
|
|
55
|
+
prompt: query,
|
|
56
|
+
aspectRatio: aspectRatio(context),
|
|
57
|
+
// Generated media is billable. A silent retry can produce a second
|
|
58
|
+
// charge for one scene, so retries stay an explicit, budget-aware
|
|
59
|
+
// decision in the application rather than an SDK default.
|
|
60
|
+
maxRetries: 0,
|
|
61
|
+
abortSignal: context.signal,
|
|
62
|
+
providerOptions: options.providerOptions,
|
|
63
|
+
});
|
|
64
|
+
const stored = await options.store({
|
|
65
|
+
data: result.video.uint8Array,
|
|
66
|
+
mediaType: result.video.mediaType,
|
|
67
|
+
kind: "video",
|
|
68
|
+
idempotencyKey: idempotencyKey(context),
|
|
69
|
+
});
|
|
70
|
+
return { url: stored.url, type: "video" };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const result = await generateImage({
|
|
74
|
+
model: options.imageModel,
|
|
75
|
+
prompt: query,
|
|
76
|
+
aspectRatio: aspectRatio(context),
|
|
77
|
+
n: 1,
|
|
78
|
+
maxRetries: 0,
|
|
79
|
+
abortSignal: context.signal,
|
|
80
|
+
providerOptions: options.providerOptions,
|
|
81
|
+
});
|
|
82
|
+
const stored = await options.store({
|
|
83
|
+
data: result.image.uint8Array,
|
|
84
|
+
mediaType: result.image.mediaType,
|
|
85
|
+
kind: "image",
|
|
86
|
+
idempotencyKey: idempotencyKey(context),
|
|
87
|
+
});
|
|
88
|
+
return { url: stored.url, type: "image" };
|
|
89
|
+
};
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanillaskyai/video",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Open-source video response SDK for personalized AI applications.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"generative-video",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"examples/nextjs-quickstart/package.json",
|
|
36
36
|
"examples/nextjs-quickstart/src",
|
|
37
37
|
"examples/nextjs-quickstart/tsconfig.json",
|
|
38
|
+
"examples/server-integrations/src/ai-sdk-media.ts",
|
|
38
39
|
"README.md",
|
|
39
40
|
"LICENSE",
|
|
40
41
|
"SECURITY.md",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"path": "src/visual-system/scene-templates/external-video-backdrop.tsx",
|
|
29
29
|
"type": "registry:lib",
|
|
30
30
|
"target": "vanillasky/scene-templates/external-video-backdrop.tsx",
|
|
31
|
-
"content": "import React from \"react\";\n\n// Source-owned templates may live in a consumer's tree while VideoFrame comes\n// from the package. Both copies must observe the same internal context or the\n// consumer template would mount a second video over the player-owned plane.\nexport type ExternalVideoBackdropMode = false | \"pending\" | \"ready\" | \"fallback\";\n\nconst sharedContext = globalThis as typeof globalThis & {\n
|
|
31
|
+
"content": "import React from \"react\";\n\n// Source-owned templates may live in a consumer's tree while VideoFrame comes\n// from the package. Both copies must observe the same internal context or the\n// consumer template would mount a second video over the player-owned plane,\n// and would never inherit the player's native media audio state.\nexport type ExternalVideoBackdropMode = false | \"pending\" | \"ready\" | \"fallback\";\n\ninterface BackdropContextValue {\n mode: ExternalVideoBackdropMode;\n audioMuted: boolean;\n audioVolume: number;\n}\n\nconst DEFAULT: BackdropContextValue = { mode: false, audioMuted: true, audioVolume: 1 };\n\nconst sharedContext = globalThis as typeof globalThis & {\n __vanillaskyVideoBackdropContext?: React.Context<BackdropContextValue>;\n};\nconst BackdropContext = sharedContext.__vanillaskyVideoBackdropContext\n ??= React.createContext<BackdropContextValue>(DEFAULT);\n\nexport function ExternalVideoBackdropProvider({\n mode,\n audioMuted = true,\n audioVolume = 1,\n children,\n}: {\n mode: ExternalVideoBackdropMode;\n audioMuted?: boolean;\n audioVolume?: number;\n children: React.ReactNode;\n}) {\n const value = React.useMemo(\n () => ({ mode, audioMuted, audioVolume }),\n [mode, audioMuted, audioVolume],\n );\n return (\n <BackdropContext.Provider value={value}>\n {children}\n </BackdropContext.Provider>\n );\n}\n\nexport function useExternalVideoBackdrop(): ExternalVideoBackdropMode {\n return React.useContext(BackdropContext).mode;\n}\n\nexport function useMediaAudio(): { muted: boolean; volume: number } {\n const { audioMuted, audioVolume } = React.useContext(BackdropContext);\n return { muted: audioMuted, volume: audioVolume };\n}\n"
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
"path": "src/visual-system/scene-templates/media-position.ts",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"path": "src/visual-system/scene-templates/scene-video-backdrop.tsx",
|
|
41
41
|
"type": "registry:lib",
|
|
42
42
|
"target": "vanillasky/scene-templates/scene-video-backdrop.tsx",
|
|
43
|
-
"content": "import React, { useEffect, useRef, useState } from \"react\";\nimport { getBackgroundTransform } from \"../backgrounds\";\nimport { resolveMediaPosition } from \"./media-position\";\n\nexport interface SceneVideoBackdropProps {\n mediaUrl: string;\n mediaPoster?: string;\n mediaPosition?: string;\n backgroundEffect?: string;\n progress: number;\n beatIntensity?: number;\n isPlaying: boolean;\n playbackId?: string;\n retainPoster?: boolean;\n persistent?: boolean;\n preparedPoster?: {\n presentationKey: string;\n mediaPoster: string;\n mediaPosition: string;\n backgroundEffect?: string;\n /** Existing global transition progress. On decoder-constrained Safari,\n * this fades the decoded incoming still above the outgoing video before\n * the single video element changes source. */\n opacity?: number;\n };\n onReady?: () => void;\n onError?: () => void;\n}\n\nexport const SceneVideoBackdrop: React.FC<SceneVideoBackdropProps> = ({\n mediaUrl,\n mediaPoster,\n mediaPosition = \"center\",\n backgroundEffect,\n progress,\n beatIntensity = 0,\n isPlaying,\n playbackId = mediaUrl,\n retainPoster = false,\n persistent = false,\n preparedPoster,\n onReady,\n onError,\n}) => {\n const resolvedPosition = resolveMediaPosition(mediaPosition);\n const bgTransform = getBackgroundTransform(backgroundEffect, progress, beatIntensity);\n const [decodedVideoUrl, setDecodedVideoUrl] = useState<string>();\n const videoRef = useRef<HTMLVideoElement>(null);\n const startedVideoUrl = useRef<string | undefined>(undefined);\n const startedPlaybackId = useRef<string | undefined>(undefined);\n const videoPresentationKey = `${playbackId}\\0${mediaUrl}`;\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n if (!isPlaying) {\n video.pause();\n startedVideoUrl.current = undefined;\n startedPlaybackId.current = undefined;\n return;\n }\n if (startedPlaybackId.current === playbackId) return;\n const changingSource = startedVideoUrl.current !== undefined && startedVideoUrl.current !== mediaUrl;\n video.playbackRate = 1;\n if (!changingSource && video.currentTime > 0) video.currentTime = 0;\n video.play().catch(() => {});\n startedVideoUrl.current = mediaUrl;\n startedPlaybackId.current = playbackId;\n }, [isPlaying, mediaUrl, playbackId]);\n\n
|
|
43
|
+
"content": "import React, { useEffect, useRef, useState } from \"react\";\nimport { getBackgroundTransform } from \"../backgrounds\";\nimport { useMediaAudio } from \"./external-video-backdrop\";\nimport { resolveMediaPosition } from \"./media-position\";\n\nexport interface SceneVideoBackdropProps {\n mediaUrl: string;\n mediaPoster?: string;\n mediaPosition?: string;\n backgroundEffect?: string;\n progress: number;\n beatIntensity?: number;\n isPlaying: boolean;\n muted?: boolean;\n volume?: number;\n playbackId?: string;\n retainPoster?: boolean;\n persistent?: boolean;\n preparedPoster?: {\n presentationKey: string;\n mediaPoster: string;\n mediaPosition: string;\n backgroundEffect?: string;\n /** Existing global transition progress. On decoder-constrained Safari,\n * this fades the decoded incoming still above the outgoing video before\n * the single video element changes source. */\n opacity?: number;\n };\n onReady?: () => void;\n onError?: () => void;\n}\n\nexport const SceneVideoBackdrop: React.FC<SceneVideoBackdropProps> = ({\n mediaUrl,\n mediaPoster,\n mediaPosition = \"center\",\n backgroundEffect,\n progress,\n beatIntensity = 0,\n isPlaying,\n muted,\n volume,\n playbackId = mediaUrl,\n retainPoster = false,\n persistent = false,\n preparedPoster,\n onReady,\n onError,\n}) => {\n const inheritedAudio = useMediaAudio();\n const resolvedMuted = muted ?? inheritedAudio.muted;\n const resolvedVolume = volume ?? inheritedAudio.volume;\n const resolvedPosition = resolveMediaPosition(mediaPosition);\n const bgTransform = getBackgroundTransform(backgroundEffect, progress, beatIntensity);\n const [decodedVideoUrl, setDecodedVideoUrl] = useState<string>();\n const videoRef = useRef<HTMLVideoElement>(null);\n const startedVideoUrl = useRef<string | undefined>(undefined);\n const startedPlaybackId = useRef<string | undefined>(undefined);\n const videoPresentationKey = `${playbackId}\\0${mediaUrl}`;\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n // React Strict Mode rehearses setup → cleanup → setup in development.\n // The cleanup deliberately releases the decoder, so the repeated setup\n // must restore the declarative source before the playback effect runs.\n if (video.getAttribute(\"src\") !== mediaUrl) {\n video.setAttribute(\"src\", mediaUrl);\n video.load();\n }\n return () => {\n video.pause();\n video.removeAttribute(\"src\");\n video.load();\n startedVideoUrl.current = undefined;\n startedPlaybackId.current = undefined;\n };\n }, [mediaUrl]);\n\n useEffect(() => {\n const video = videoRef.current;\n if (video) video.volume = resolvedVolume;\n }, [resolvedVolume]);\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n if (!isPlaying) {\n video.pause();\n startedVideoUrl.current = undefined;\n startedPlaybackId.current = undefined;\n return;\n }\n if (startedPlaybackId.current === playbackId) return;\n const changingSource = startedVideoUrl.current !== undefined && startedVideoUrl.current !== mediaUrl;\n video.playbackRate = 1;\n if (!changingSource && video.currentTime > 0) video.currentTime = 0;\n video.play().catch(() => {});\n startedVideoUrl.current = mediaUrl;\n startedPlaybackId.current = playbackId;\n }, [isPlaying, mediaUrl, playbackId]);\n\n const mediaStyle: React.CSSProperties = {\n position: \"absolute\",\n inset: 0,\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n objectPosition: resolvedPosition,\n transform: bgTransform.transform,\n transformOrigin: bgTransform.transformOrigin,\n zIndex: persistent ? 1 : undefined,\n };\n const preparedPosition = preparedPoster\n ? resolveMediaPosition(preparedPoster.mediaPosition)\n : resolvedPosition;\n const preparedTransform = getBackgroundTransform(preparedPoster?.backgroundEffect, 0, 0);\n const posterPlanes = [\n ...(persistent && mediaPoster ? [{\n presentationKey: videoPresentationKey,\n mediaPoster,\n mediaPosition: resolvedPosition,\n transform: bgTransform.transform,\n transformOrigin: bgTransform.transformOrigin,\n opacity: 1,\n zIndex: 0,\n role: \"current\",\n }] : []),\n ...(preparedPoster && preparedPoster.presentationKey !== videoPresentationKey ? [{\n presentationKey: preparedPoster.presentationKey,\n mediaPoster: preparedPoster.mediaPoster,\n mediaPosition: preparedPosition,\n transform: preparedTransform.transform,\n transformOrigin: preparedTransform.transformOrigin,\n opacity: preparedPoster.opacity ?? 0,\n zIndex: 2,\n role: \"prepared\",\n }] : []),\n ];\n\n return (\n <>\n {posterPlanes.map((posterPlane) => (\n <img\n key={posterPlane.presentationKey}\n src={posterPlane.mediaPoster}\n alt=\"\"\n aria-hidden=\"true\"\n draggable={false}\n data-video-poster-plane={posterPlane.role}\n data-video-poster-visible={posterPlane.opacity > 0 ? \"true\" : \"false\"}\n style={{\n position: \"absolute\",\n inset: 0,\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n objectPosition: posterPlane.mediaPosition,\n transform: posterPlane.transform,\n transformOrigin: posterPlane.transformOrigin,\n zIndex: posterPlane.zIndex,\n opacity: posterPlane.opacity,\n pointerEvents: \"none\",\n }}\n />\n ))}\n <video\n ref={videoRef}\n src={mediaUrl}\n poster={retainPoster || decodedVideoUrl !== mediaUrl ? mediaPoster || undefined : undefined}\n muted={resolvedMuted}\n loop\n playsInline\n preload=\"auto\"\n onLoadedData={(event) => {\n const video = event.currentTarget;\n const markPresented = () => {\n if (!video.isConnected) return;\n onReady?.();\n if (!retainPoster) setDecodedVideoUrl(mediaUrl);\n };\n if (video.requestVideoFrameCallback) {\n video.requestVideoFrameCallback(markPresented);\n return;\n }\n markPresented();\n }}\n onError={onError}\n data-media-position={mediaPosition}\n data-video-backdrop={persistent ? \"persistent\" : \"scene\"}\n style={mediaStyle}\n />\n </>\n );\n};\n"
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
"path": "src/visual-system/scene-templates/color-utils.ts",
|
package/dist/chunk-C6WVCZRW.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// src/visual-system/scene-templates/external-video-backdrop.tsx
|
|
2
|
-
import React from "react";
|
|
3
|
-
import { jsx } from "react/jsx-runtime";
|
|
4
|
-
var sharedContext = globalThis;
|
|
5
|
-
var ExternalVideoBackdropContext = sharedContext.__vanillaskyExternalVideoBackdropContext ??= React.createContext(false);
|
|
6
|
-
function ExternalVideoBackdropProvider({
|
|
7
|
-
mode,
|
|
8
|
-
children
|
|
9
|
-
}) {
|
|
10
|
-
return /* @__PURE__ */ jsx(ExternalVideoBackdropContext.Provider, { value: mode, children });
|
|
11
|
-
}
|
|
12
|
-
function useExternalVideoBackdrop() {
|
|
13
|
-
return React.useContext(ExternalVideoBackdropContext);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export {
|
|
17
|
-
ExternalVideoBackdropProvider,
|
|
18
|
-
useExternalVideoBackdrop
|
|
19
|
-
};
|