@vanillaskyai/video 0.5.8 → 0.7.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 +178 -0
- package/PUBLIC-API.md +129 -9
- package/README.md +70 -108
- package/dist/{bg-confetti-SHQI7ATB.js → bg-confetti-HITCGLGD.js} +1 -1
- package/dist/{bg-emoji-XYFRA63Q.js → bg-emoji-VU4UZYZN.js} +1 -1
- package/dist/{bg-media-FEBATBT2.js → bg-media-L34PDQXJ.js} +4 -4
- package/dist/{brand-message-P56WEJPG.js → brand-message-FUJ4STFO.js} +3 -3
- package/dist/{builtin-server-YHEZ2JRF.js → builtin-server-KB7FKF6A.js} +2 -2
- package/dist/{chart-bar-ZB45ZRKH.js → chart-bar-EGPHJATL.js} +4 -4
- package/dist/{chart-counter-HFYNE4HT.js → chart-counter-5FFWQNZO.js} +4 -4
- package/dist/{chart-progress-ring-ZKMLRGSB.js → chart-progress-ring-JMXQLMLC.js} +4 -4
- package/dist/check-runtime.js +2 -2
- package/dist/{chunk-66MNRUCR.js → chunk-2PYO6VAC.js} +51 -3
- package/dist/{chunk-ZQQQAAKP.js → chunk-2XT4MZ76.js} +33 -2
- package/dist/chunk-5CDAM24P.js +31 -0
- package/dist/{chunk-PNS52FL4.js → chunk-5SLENAJW.js} +19 -6
- package/dist/{chunk-EFL34TXF.js → chunk-AITKH6QT.js} +24 -8
- package/dist/{chunk-HT6BGERF.js → chunk-FRN6WKHA.js} +28 -10
- package/dist/{chunk-FZHMQFG3.js → chunk-GAIOHGNR.js} +5 -21
- package/dist/{chunk-3RV4YKB3.js → chunk-JNN3EYVP.js} +51 -26
- package/dist/{chunk-NGY3TNND.js → chunk-K746NUIP.js} +70 -50
- package/dist/{chunk-HVCPEAQF.js → chunk-LKBZX7GV.js} +9 -6
- package/dist/{chunk-G66Z5CWR.js → chunk-MMUXVA47.js} +27 -3
- package/dist/{chunk-6M6DATLW.js → chunk-MXZSDGZQ.js} +6 -5
- package/dist/{chunk-3OU7HIEB.js → chunk-RE4IMWJR.js} +15 -31
- package/dist/{chunk-FVTMYS6U.js → chunk-RGF452LL.js} +1 -1
- package/dist/{chunk-34O5BY6X.js → chunk-RXHW4EP4.js} +21 -4
- package/dist/chunk-SKRGRKHY.js +142 -0
- package/dist/{chunk-TCEHK2BW.js → chunk-YJJC4N4D.js} +2 -1
- package/dist/cli.js +438 -30
- package/dist/{compose-video-JWIEDTEJ.js → compose-video-PD6LKKRK.js} +4 -4
- package/dist/{cta-logo-S4OMTNXD.js → cta-logo-YR7LYUR5.js} +3 -3
- package/dist/{cta-media-EROTHDRR.js → cta-media-DQJBKUO2.js} +4 -4
- package/dist/{events-BBP30j3c.d.ts → events-B6qS1Lsb.d.ts} +2 -2
- package/dist/{incoming-call-5IWJ2TAS.js → incoming-call-54KQ7O5A.js} +3 -3
- package/dist/index.d.ts +89 -3
- package/dist/index.js +12 -1
- package/dist/{infographic-before-after-2I7YZVDD.js → infographic-before-after-ROS52GOW.js} +1 -1
- package/dist/{infographic-feature-list-7SK65U3K.js → infographic-feature-list-E7MGUDAN.js} +4 -4
- package/dist/{infographic-problem-solution-IW35S6BR.js → infographic-problem-solution-3S6NGO5N.js} +4 -4
- package/dist/{infographic-stat-row-XHUZZPDM.js → infographic-stat-row-SYB7AB7H.js} +4 -4
- package/dist/{infographic-steps-JDDEDI7X.js → infographic-steps-KCE6JWE2.js} +4 -4
- package/dist/{kit-DEG3fcaL.d.ts → kit-8g46H2RZ.d.ts} +1 -1
- package/dist/{prompt-input-6ZWXEL3V.js → prompt-input-MHX4O42G.js} +3 -3
- package/dist/react.d.ts +200 -6
- package/dist/react.js +1859 -43
- package/dist/{reaction-Z3JIM3MQ.js → reaction-SLEGR3BE.js} +4 -4
- package/dist/{scene-video-backdrop-IL4F2SVD.js → scene-video-backdrop-XFPX4O3I.js} +2 -1
- package/dist/server.d.ts +104 -4
- package/dist/server.js +976 -26
- package/dist/{showcase-code-NWQAAJIA.js → showcase-code-2HLAVKBM.js} +4 -4
- package/dist/{showcase-phone-5UW45ATT.js → showcase-phone-Q4HEO4LH.js} +4 -4
- package/dist/{showcase-terminal-UVOD25UF.js → showcase-terminal-DL45LUFG.js} +4 -4
- package/dist/{showcase-web-3AW6VHIG.js → showcase-web-2QZO73FZ.js} +4 -4
- package/dist/{social-milestone-KPKHFOB5.js → social-milestone-GEFUQSBF.js} +3 -3
- package/dist/{social-notification-EDOU6TZB.js → social-notification-FJ5VHVLK.js} +3 -3
- package/dist/{social-review-stack-EZOO33SV.js → social-review-stack-KXQCKFRP.js} +3 -3
- package/dist/{social-testimonial-DLOINMMY.js → social-testimonial-JZ7CHC7S.js} +3 -3
- package/dist/{social-tweet-6JUM323G.js → social-tweet-UCDXM25F.js} +3 -3
- package/dist/{system-prompt-4I6Z5HK3.js → system-prompt-RRXIWDDD.js} +5 -3
- 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-J6EDDJP4.js → text-stream-FW4BLBAL.js} +2 -2
- package/dist/{types-BV9IqExh.d.ts → types-2wHBqtg8.d.ts} +29 -1
- package/dist/types-DrABlRa7.d.ts +46 -0
- package/docs/agent-integration.md +49 -25
- package/docs/architecture.md +37 -22
- package/docs/branding-and-personalization.md +1 -1
- package/docs/concepts.md +14 -0
- package/docs/custom-templates.md +9 -9
- package/docs/customization.md +41 -5
- package/docs/getting-started.md +81 -84
- package/docs/media-and-audio.md +113 -117
- package/docs/persistence.md +1 -1
- package/docs/production.md +81 -92
- package/docs/prompt-and-input.md +71 -161
- package/docs/provider-integration.md +91 -49
- package/docs/responsive-orientation.md +1 -1
- package/docs/security.md +6 -5
- package/docs/streaming-protocol.md +1 -1
- package/docs/testing.md +51 -43
- package/examples/custom-template/README.md +1 -1
- package/package.json +20 -21
- package/registry/items/backgrounds.json +2 -2
- package/registry/items/barChart.json +6 -4
- package/registry/items/beforeAfter.json +1 -1
- package/registry/items/bigNumber.json +4 -3
- package/registry/items/brandMessage.json +3 -2
- package/registry/items/cardList.json +6 -4
- package/registry/items/codeEditor.json +4 -3
- package/registry/items/confetti.json +1 -1
- package/registry/items/ctaLogo.json +3 -2
- package/registry/items/ctaMedia.json +4 -3
- package/registry/items/emojiBurst.json +1 -1
- package/registry/items/incomingCall.json +3 -2
- package/registry/items/media.json +4 -3
- package/registry/items/milestone.json +3 -2
- package/registry/items/notification.json +3 -2
- package/registry/items/phoneMockup.json +4 -3
- package/registry/items/problemSolution.json +4 -3
- package/registry/items/progressRing.json +4 -3
- package/registry/items/promptInput.json +3 -2
- package/registry/items/reaction.json +2 -2
- package/registry/items/reviewStack.json +3 -2
- package/registry/items/steps.json +6 -4
- package/registry/items/terminal.json +5 -4
- package/registry/items/testimonial.json +3 -2
- package/registry/items/tripleStats.json +4 -3
- package/registry/items/tweet.json +3 -2
- package/registry/items/webMockup.json +4 -3
- package/starters/video-chat/.env.example +16 -0
- package/starters/video-chat/README.md +63 -0
- package/starters/video-chat/index.html +12 -0
- package/starters/video-chat/package.json +28 -0
- package/starters/video-chat/server.ts +152 -0
- package/starters/video-chat/src/main.tsx +8 -0
- package/starters/video-chat/stock.ts +139 -0
- package/starters/video-chat/tsconfig.json +20 -0
- package/starters/video-chat/vite.config.ts +81 -0
- package/styles/video-chat.css +713 -0
- package/dist/chunk-C6WVCZRW.js +0 -19
- package/docs/input-and-first-scene.md +0 -64
- package/docs/integrate-nextjs.md +0 -86
- package/docs/live-channels.md +0 -149
- package/docs/use-cases.md +0 -59
- package/examples/nextjs-quickstart/.env.example +0 -2
- package/examples/nextjs-quickstart/README.md +0 -27
- package/examples/nextjs-quickstart/next-env.d.ts +0 -4
- package/examples/nextjs-quickstart/package.json +0 -25
- package/examples/nextjs-quickstart/src/app/api/video/route.ts +0 -24
- package/examples/nextjs-quickstart/src/app/layout.tsx +0 -5
- package/examples/nextjs-quickstart/src/app/page.tsx +0 -31
- package/examples/nextjs-quickstart/tsconfig.json +0 -26
|
@@ -1,49 +1,96 @@
|
|
|
1
|
-
[← Documentation home](../README.md) · [Previous:
|
|
1
|
+
[← Documentation home](../README.md) · [Previous: Getting started](getting-started.md) · [Next: Branding and personalization →](branding-and-personalization.md)
|
|
2
2
|
|
|
3
3
|
# Provider integration
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
model; the application passes the generated `systemPrompt` and `userPrompt` to
|
|
8
|
-
its provider here.
|
|
5
|
+
Start from the generated chat so provider work stays confined to the
|
|
6
|
+
application-owned server:
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
```bash
|
|
9
|
+
npx @vanillaskyai/video init
|
|
10
|
+
npx vanillasky doctor
|
|
11
|
+
npm run dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The generated `server.ts` is the canonical provider example. It starts with one
|
|
15
|
+
text key, packaged templates, and browser voice. Adding the optional generated
|
|
16
|
+
speech, stock, transcription, or generated-video key enables its callback and
|
|
17
|
+
advertises the capability automatically; `src/main.tsx` does not change.
|
|
18
|
+
|
|
19
|
+
For the full chat experience, mount one `createVideoChatHandler` and keep every
|
|
20
|
+
provider choice in its callbacks:
|
|
13
21
|
|
|
14
22
|
```ts
|
|
15
23
|
import "server-only";
|
|
16
24
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const provider = process.env.VIDEO_PROVIDER;
|
|
22
|
-
const modelId = process.env.VIDEO_MODEL;
|
|
23
|
-
if (!modelId) throw new Error("Set VIDEO_MODEL in the server environment");
|
|
24
|
-
const model = provider === "openai" ? openai(modelId)
|
|
25
|
-
: provider === "anthropic" ? anthropic(modelId)
|
|
26
|
-
: (() => { throw new Error("Set VIDEO_PROVIDER to openai or anthropic"); })();
|
|
27
|
-
|
|
28
|
-
export const POST = createVideoHandler({
|
|
25
|
+
import { generateText, streamText } from "ai";
|
|
26
|
+
import { createVideoChatHandler } from "@vanillaskyai/video/server";
|
|
27
|
+
|
|
28
|
+
export const handler = createVideoChatHandler({
|
|
29
29
|
authorize: verifySession,
|
|
30
30
|
streamText: ({ systemPrompt, userPrompt, signal }) => streamText({
|
|
31
|
-
model,
|
|
31
|
+
model: anthropic(process.env.TEXT_MODEL ?? "claude-sonnet-5"),
|
|
32
32
|
system: systemPrompt,
|
|
33
33
|
prompt: userPrompt,
|
|
34
34
|
abortSignal: signal,
|
|
35
35
|
}),
|
|
36
|
+
generateText: async ({ systemPrompt, userPrompt, maxOutputTokens, signal }) => {
|
|
37
|
+
const result = await generateText({
|
|
38
|
+
model: anthropic(process.env.TEXT_MODEL ?? "claude-sonnet-5"),
|
|
39
|
+
system: systemPrompt,
|
|
40
|
+
prompt: userPrompt,
|
|
41
|
+
maxOutputTokens,
|
|
42
|
+
abortSignal: signal,
|
|
43
|
+
});
|
|
44
|
+
return result.text;
|
|
45
|
+
},
|
|
36
46
|
});
|
|
37
47
|
```
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
That one text provider gives the browser templated video responses and local
|
|
50
|
+
browser speech. Supplying `generateSpeech`, `transcribe`, `searchMedia`, or
|
|
51
|
+
`generateVideo` enables those capabilities automatically. The callbacks are
|
|
52
|
+
structural and provider-neutral; their SDKs and credentials remain application
|
|
53
|
+
dependencies and never enter the browser bundle.
|
|
54
|
+
|
|
55
|
+
The planner emits a 6-9 word hook first, then continues into the scenes in the
|
|
56
|
+
same stream. The stock lookup is a separate cancellable request, so it cannot
|
|
57
|
+
delay speech or planning. A welcome card can carry a prewritten `opening`, which
|
|
58
|
+
starts immediately with its already-loaded media. For a full AI response, the
|
|
59
|
+
same first streamed object reserves the exact first body scene. Its clip starts
|
|
60
|
+
generating while the planner continues with scenes two through five. Template
|
|
61
|
+
responses stream their first resolved scene without waiting for the rest of the
|
|
62
|
+
plan, and normal narration is written in that same planner call rather than
|
|
63
|
+
through a second model round trip.
|
|
64
|
+
|
|
65
|
+
The matching complete React interface is one component and one scoped style
|
|
66
|
+
import:
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import { VideoChat } from "@vanillaskyai/video/react";
|
|
70
|
+
import "@vanillaskyai/video/video-chat.css";
|
|
71
|
+
|
|
72
|
+
export function App() {
|
|
73
|
+
return <VideoChat />;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Custom interface
|
|
78
|
+
|
|
79
|
+
For a custom interface, use `useVideoChat` and render its `turns`, `welcome`,
|
|
80
|
+
`suggestions`, `caption`, and `status`; the hook owns their network and playback
|
|
81
|
+
lifecycle. Pass a selected card through
|
|
82
|
+
`chat.ask(card.prompt, { opening: card.opening, openingMedia: card.media })` to
|
|
83
|
+
start its hook and reuse its footage immediately. For typed prompts, the hook
|
|
84
|
+
and media keyword arrive through the response stream automatically.
|
|
85
|
+
|
|
86
|
+
Any AI SDK `LanguageModel` works in both `streamText` and `generateText`. Keep
|
|
87
|
+
selection in one server-only module when an application supports several text
|
|
88
|
+
providers. The chat route and React component stay unchanged; only the model
|
|
89
|
+
passed to those callbacks changes. The AI SDK result can be returned directly:
|
|
90
|
+
its text stream, finish reason, usage, warnings, and response metadata match the
|
|
91
|
+
structural callback contract. See the
|
|
92
|
+
[provider adapter reference](reference/provider-adapters.md) for native provider
|
|
93
|
+
alternatives.
|
|
47
94
|
|
|
48
95
|
## Planning effort and reasoning modes
|
|
49
96
|
|
|
@@ -85,7 +132,7 @@ and credentials stay with the application.
|
|
|
85
132
|
Use `onComplete` for server-side cost and completion measurement:
|
|
86
133
|
|
|
87
134
|
```ts
|
|
88
|
-
|
|
135
|
+
createVideoChatHandler({
|
|
89
136
|
authorize: verifySession,
|
|
90
137
|
streamText: ({ systemPrompt, userPrompt, signal }) => streamText({
|
|
91
138
|
model,
|
|
@@ -93,6 +140,7 @@ createVideoHandler({
|
|
|
93
140
|
prompt: userPrompt,
|
|
94
141
|
abortSignal: signal,
|
|
95
142
|
}),
|
|
143
|
+
generateText: runSmallTextTask,
|
|
96
144
|
onWarning: (warning) => logSafeWarning(warning.code, warning.category),
|
|
97
145
|
onComplete: (summary) => recordGeneration({
|
|
98
146
|
finishReason: summary.finishReason,
|
|
@@ -113,23 +161,18 @@ true` only when the host deliberately needs bounded provider-native usage and
|
|
|
113
161
|
metadata and has an appropriate retention policy.
|
|
114
162
|
|
|
115
163
|
`acceptedSceneCount`, `rejectedSceneCount`, and `timeToFirstSceneMs` describe
|
|
116
|
-
model-generated scene additions;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
server-side quality signal without exposing model metadata in the browser.
|
|
164
|
+
model-generated scene additions; the streamed opening hook is not counted.
|
|
165
|
+
Their sum is the proposed scene count. `videoDurationSec` is the duration
|
|
166
|
+
actually committed. These fields provide a server-side quality signal without
|
|
167
|
+
exposing model metadata in the browser.
|
|
121
168
|
Warnings include the same bounded typed warnings emitted to the client.
|
|
122
169
|
`plan_incomplete` identifies a playable partial response whose planner reported
|
|
123
170
|
a length limit; applications should show that result as incomplete and may
|
|
124
171
|
offer a bounded retry with a larger output or duration budget.
|
|
125
|
-
`plan_missing_closer` identifies a playable
|
|
126
|
-
explicit final scene
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
For non-interactive backfills, define an application threshold (for example,
|
|
130
|
-
no rejected scenes and a useful committed-duration ratio) and retry a bounded
|
|
131
|
-
number of times. Keep the best accepted result rather than treating
|
|
132
|
-
`finishReason: "stop"` alone as a quality score.
|
|
172
|
+
`plan_missing_closer` identifies a playable answer that ended without its
|
|
173
|
+
explicit final scene. For non-interactive evaluation, define an application
|
|
174
|
+
threshold and retry a bounded number of times. Keep the best accepted result
|
|
175
|
+
rather than treating `finishReason: "stop"` alone as a quality score.
|
|
133
176
|
|
|
134
177
|
The generated system prompt includes the selected trusted-template catalog and
|
|
135
178
|
is intentionally substantial. It is stable for the same SDK version, template
|
|
@@ -150,9 +193,8 @@ same explicit request budget.
|
|
|
150
193
|
|
|
151
194
|
## Product-level planner guidance
|
|
152
195
|
|
|
153
|
-
`
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
Presentation guidance in `basePrompt` or `instructions` never changes it.
|
|
196
|
+
`createVideoChatHandler` constructs the planner prompt from the trusted template
|
|
197
|
+
registry. Normal integrations do not build prompts or capabilities. Use the
|
|
198
|
+
handler's `instructions` option for durable product-level direction such as a
|
|
199
|
+
character, audience, domain, or answer style. The current user prompt and
|
|
200
|
+
bounded prior turns are supplied separately by the SDK.
|
package/docs/security.md
CHANGED
|
@@ -7,7 +7,8 @@ The SDK validates protocol shape; your application still owns identity, authoriz
|
|
|
7
7
|
## Required server controls
|
|
8
8
|
|
|
9
9
|
- Authenticate the user and tenant before reading the prompt body.
|
|
10
|
-
- `
|
|
10
|
+
- `createVideoChatHandler` and the lower-level `createVideoHandler` require an
|
|
11
|
+
explicit `authorize` policy. The
|
|
11
12
|
`authorize: "none"` escape hatch is for intentionally non-public in-process
|
|
12
13
|
tests only; do not use it on a billable generation route.
|
|
13
14
|
- Allowlist browser origins; CORS is not authentication.
|
|
@@ -15,10 +16,10 @@ The SDK validates protocol shape; your application still owns identity, authoriz
|
|
|
15
16
|
- Keep provider keys, system prompts, tools, signed-URL credentials, and admin tokens server-side.
|
|
16
17
|
- Use the same generated template registry on the server and in React; the handler infers validation.
|
|
17
18
|
- Treat every project-owned template as trusted application build code and
|
|
18
|
-
review it before using the CLI. Normal `vanillasky list`,
|
|
19
|
-
`vanillasky describe`, `vanillasky add`, `vanillasky sync`, and
|
|
20
|
-
`vanillasky check` commands execute project template modules locally. This
|
|
21
|
-
includes `vanillasky add` previews with `--dry-run` or `--diff`, because the
|
|
19
|
+
review it before using the CLI. Normal `vanillasky templates list`,
|
|
20
|
+
`vanillasky templates describe`, `vanillasky templates add`, `vanillasky templates sync`, and
|
|
21
|
+
`vanillasky templates check` commands execute project template modules locally. This
|
|
22
|
+
includes `vanillasky templates add` previews with `--dry-run` or `--diff`, because the
|
|
22
23
|
CLI must derive the proposed browser and server registries. Resource and
|
|
23
24
|
environment boundaries reduce accidental damage but are not a portable
|
|
24
25
|
JavaScript sandbox.
|
package/docs/testing.md
CHANGED
|
@@ -1,56 +1,63 @@
|
|
|
1
|
-
# Test
|
|
1
|
+
# Test video chat without a model
|
|
2
2
|
|
|
3
|
-
`@vanillaskyai/video/test` provides deterministic
|
|
4
|
-
protocol events. It has no React or provider-SDK dependency, makes
|
|
5
|
-
request, and
|
|
3
|
+
`@vanillaskyai/video/test` provides deterministic planner streams and
|
|
4
|
+
in-process protocol events. It has no React or provider-SDK dependency, makes
|
|
5
|
+
no network request, and needs no model key.
|
|
6
6
|
|
|
7
|
-
## Test
|
|
7
|
+
## Test the chat route with Vitest
|
|
8
8
|
|
|
9
|
-
Pass `createMockVideoPlanner()`
|
|
10
|
-
|
|
11
|
-
HTTP server
|
|
9
|
+
Pass `createMockVideoPlanner()` to the same `createVideoChatHandler` used by the
|
|
10
|
+
application. A standard `Request` exercises parsing, validation, pacing, the
|
|
11
|
+
opening extension, and SSE without starting an HTTP server.
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
14
|
import { describe, expect, it } from "vitest";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
createMockVideoPlanner,
|
|
18
|
-
videoFixtures,
|
|
19
|
-
} from "@vanillaskyai/video/test";
|
|
15
|
+
import { createVideoChatHandler } from "@vanillaskyai/video/server";
|
|
16
|
+
import { createMockVideoPlanner } from "@vanillaskyai/video/test";
|
|
20
17
|
|
|
21
|
-
describe("POST /api/video", () => {
|
|
22
|
-
it("returns a completed video
|
|
23
|
-
const handle =
|
|
24
|
-
authorize: "none", //
|
|
18
|
+
describe("POST /api/video-chat", () => {
|
|
19
|
+
it("returns a completed video answer", async () => {
|
|
20
|
+
const handle = createVideoChatHandler({
|
|
21
|
+
authorize: "none", // Only acceptable because this handler stays in process.
|
|
25
22
|
heartbeatMs: false,
|
|
26
23
|
streamText: createMockVideoPlanner(),
|
|
24
|
+
generateText: async ({ task }) => task === "suggestions"
|
|
25
|
+
? JSON.stringify({ suggestions: [] })
|
|
26
|
+
: "A deterministic narration line.",
|
|
27
27
|
});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
|
|
29
|
+
const response = await handle(new Request(
|
|
30
|
+
"https://app.test/api/video-chat?action=response",
|
|
31
|
+
{
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: { "content-type": "application/json" },
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
prompt: "Explain why the Moon shows one face",
|
|
36
|
+
opening: "The Moon turns, perfectly matching its orbit.",
|
|
37
|
+
mode: "templates",
|
|
38
|
+
orientation: "landscape",
|
|
39
|
+
}),
|
|
40
|
+
},
|
|
41
|
+
));
|
|
36
42
|
const body = await response.text();
|
|
37
43
|
|
|
38
44
|
expect(response.status).toBe(200);
|
|
45
|
+
expect(body).toContain('"type":"data.video-chat-opening"');
|
|
39
46
|
expect(body).toContain('"type":"response.complete"');
|
|
40
47
|
});
|
|
41
48
|
});
|
|
42
49
|
```
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
Also request `?action=capabilities` and `?action=welcome` in route tests. Assert
|
|
52
|
+
that optional modes and controls appear only when their callbacks are present.
|
|
53
|
+
For browser coverage, render `<VideoChat />`, submit through the visible prompt,
|
|
54
|
+
and wait for a known template rather than reaching into hook internals.
|
|
47
55
|
|
|
48
56
|
## Test protocol events in process
|
|
49
57
|
|
|
50
|
-
`simulateVideoStream(parts, options?)` runs the
|
|
51
|
-
pipeline without an HTTP boundary. It yields
|
|
52
|
-
|
|
53
|
-
`runId` when a test needs different fixed values.
|
|
58
|
+
`simulateVideoStream(parts, options?)` runs the composition and validation
|
|
59
|
+
pipeline without an HTTP boundary. It yields typed event objects and remains
|
|
60
|
+
useful for focused planner and protocol cases beneath the chat handler.
|
|
54
61
|
|
|
55
62
|
```ts
|
|
56
63
|
import { expect, it } from "vitest";
|
|
@@ -74,9 +81,8 @@ it("keeps a truncated result playable", async () => {
|
|
|
74
81
|
});
|
|
75
82
|
```
|
|
76
83
|
|
|
77
|
-
The portrait and landscape fixtures each contain `{ input, parts }`.
|
|
78
|
-
fixture values are deeply frozen
|
|
79
|
-
one test cannot mutate another test's input.
|
|
84
|
+
The portrait and landscape fixtures each contain `{ input, parts }`. Public
|
|
85
|
+
fixture values are deeply frozen and every helper clones parts before a run.
|
|
80
86
|
|
|
81
87
|
## Delays, aborts, and timeouts
|
|
82
88
|
|
|
@@ -113,11 +119,11 @@ it("times out deterministically", async () => {
|
|
|
113
119
|
});
|
|
114
120
|
```
|
|
115
121
|
|
|
116
|
-
For
|
|
117
|
-
desired partial event.
|
|
118
|
-
|
|
122
|
+
For host cancellation, pass an `AbortController` signal and abort after the
|
|
123
|
+
desired partial event. At the route boundary, the host remains responsible for
|
|
124
|
+
aborting the request signal and each provider callback must honour it.
|
|
119
125
|
|
|
120
|
-
## Fixed scenarios
|
|
126
|
+
## Fixed planner scenarios
|
|
121
127
|
|
|
122
128
|
`createMockVideoPlanner({ scenario })` accepts:
|
|
123
129
|
|
|
@@ -126,12 +132,14 @@ route boundary the host remains responsible for aborting the request signal.
|
|
|
126
132
|
| `success` | Completed portrait fixture |
|
|
127
133
|
| `delayed` | Success after a 25 ms fake-timer-safe delay |
|
|
128
134
|
| `truncated` | Partial playable result completed with `length` |
|
|
129
|
-
| `invalidScene` | Invalid scene dropped with a recoverable
|
|
135
|
+
| `invalidScene` | Invalid scene dropped with a recoverable warning, then completion |
|
|
130
136
|
| `providerFailure` | Redacted terminal generation failure |
|
|
131
137
|
| `contentFilter` | Partial playable result completed with `content-filter` |
|
|
132
138
|
| `abort` | Waits for the supplied request signal to abort |
|
|
133
139
|
| `timeout` | Waits for the host signal or simulator `timeoutMs` |
|
|
134
140
|
|
|
135
|
-
Use `parts`
|
|
136
|
-
|
|
137
|
-
|
|
141
|
+
Use `parts` for custom structural plan parts and `delayMs` to delay each
|
|
142
|
+
provider chunk. Keep provider selection, credentials, retries, and real-model
|
|
143
|
+
acceptance outside deterministic CI.
|
|
144
|
+
|
|
145
|
+
[← Documentation home](../README.md)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
These files are deliberately complete, one-file examples. Copy the closest
|
|
4
4
|
shape into `vanillasky/templates/`, rename its ID, then run `npx vanillasky
|
|
5
|
-
sync` and `npx vanillasky check`.
|
|
5
|
+
templates sync` and `npx vanillasky templates check`.
|
|
6
6
|
|
|
7
7
|
| Reference | Use it when |
|
|
8
8
|
| --- | --- |
|
package/package.json
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanillaskyai/video",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Open-source video
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Open-source voice-and-video chat SDK for AI applications.",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"video-chat",
|
|
7
|
+
"voice-chat",
|
|
8
|
+
"ai-chat",
|
|
6
9
|
"generative-video",
|
|
7
|
-
"personalized-video",
|
|
8
10
|
"video-response",
|
|
9
11
|
"ai-output",
|
|
10
|
-
"contextual-ai",
|
|
11
12
|
"streaming",
|
|
12
13
|
"react",
|
|
13
14
|
"llm",
|
|
14
15
|
"video"
|
|
15
16
|
],
|
|
16
17
|
"type": "module",
|
|
17
|
-
"sideEffects":
|
|
18
|
+
"sideEffects": [
|
|
19
|
+
"./styles/video-chat.css"
|
|
20
|
+
],
|
|
18
21
|
"license": "Apache-2.0",
|
|
19
22
|
"repository": {
|
|
20
23
|
"type": "git",
|
|
@@ -27,14 +30,18 @@
|
|
|
27
30
|
"files": [
|
|
28
31
|
"bin",
|
|
29
32
|
"dist",
|
|
33
|
+
"styles/video-chat.css",
|
|
34
|
+
"starters/video-chat/.env.example",
|
|
35
|
+
"starters/video-chat/README.md",
|
|
36
|
+
"starters/video-chat/index.html",
|
|
37
|
+
"starters/video-chat/package.json",
|
|
38
|
+
"starters/video-chat/server.ts",
|
|
39
|
+
"starters/video-chat/src/main.tsx",
|
|
40
|
+
"starters/video-chat/stock.ts",
|
|
41
|
+
"starters/video-chat/tsconfig.json",
|
|
42
|
+
"starters/video-chat/vite.config.ts",
|
|
30
43
|
"registry/items",
|
|
31
44
|
"examples/custom-template",
|
|
32
|
-
"examples/nextjs-quickstart/.env.example",
|
|
33
|
-
"examples/nextjs-quickstart/README.md",
|
|
34
|
-
"examples/nextjs-quickstart/next-env.d.ts",
|
|
35
|
-
"examples/nextjs-quickstart/package.json",
|
|
36
|
-
"examples/nextjs-quickstart/src",
|
|
37
|
-
"examples/nextjs-quickstart/tsconfig.json",
|
|
38
45
|
"README.md",
|
|
39
46
|
"LICENSE",
|
|
40
47
|
"SECURITY.md",
|
|
@@ -79,7 +86,8 @@
|
|
|
79
86
|
"types": "./dist/test.d.ts",
|
|
80
87
|
"import": "./dist/test.js",
|
|
81
88
|
"default": "./dist/test.js"
|
|
82
|
-
}
|
|
89
|
+
},
|
|
90
|
+
"./video-chat.css": "./styles/video-chat.css"
|
|
83
91
|
},
|
|
84
92
|
"scripts": {
|
|
85
93
|
"acceptance:live": "tsx scripts/acceptance/run-live.ts",
|
|
@@ -88,14 +96,6 @@
|
|
|
88
96
|
"browser:install": "playwright install chromium firefox webkit",
|
|
89
97
|
"browser:test": "playwright test",
|
|
90
98
|
"build": "tsup",
|
|
91
|
-
"example:install": "npm run examples:install-current",
|
|
92
|
-
"example:build": "npm run build --prefix examples/react-vite",
|
|
93
|
-
"example:dev": "npm run dev --prefix examples/react-vite",
|
|
94
|
-
"example:preview": "npm run preview --prefix examples/react-vite -- --host 127.0.0.1 --port 4187 --strictPort",
|
|
95
|
-
"examples:verify-documented": "node scripts/verify-documented-examples.mjs",
|
|
96
|
-
"examples:install-current": "npm run build && node scripts/install-current-examples.mjs",
|
|
97
|
-
"server-examples:typecheck": "npm run typecheck --prefix examples/server-integrations",
|
|
98
|
-
"server-examples:compat": "npm run ai-sdk:compat --prefix examples/server-integrations",
|
|
99
99
|
"lint": "eslint .",
|
|
100
100
|
"catalog:sync": "tsx scripts/generate-builtin-catalog.ts",
|
|
101
101
|
"catalog:check": "tsx scripts/generate-builtin-catalog.ts --check",
|
|
@@ -142,7 +142,6 @@
|
|
|
142
142
|
"eslint": "^9.39.1",
|
|
143
143
|
"globals": "^16.5.0",
|
|
144
144
|
"jsdom": "^26.1.0",
|
|
145
|
-
"mdast-util-from-markdown": "2.0.3",
|
|
146
145
|
"openai": "^7.5.0",
|
|
147
146
|
"react": "^19.2.8",
|
|
148
147
|
"react-dom": "^19.2.8",
|
|
@@ -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",
|