@vanillaskyai/video 0.10.23 → 0.11.1
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 +69 -0
- package/PUBLIC-API.md +47 -438
- package/README.md +53 -97
- package/dist/builtin-metadata-OT6V7TB4.js +8 -0
- package/dist/{chapter-title-2JVDU62E.js → chapter-title-2RCXX7SN.js} +1 -2
- package/dist/{chunk-5BSJLT6H.js → chunk-2WIETEL7.js} +16 -29
- package/dist/{chunk-3O7OMMMF.js → chunk-35K6IKB2.js} +3 -3
- package/dist/{chunk-PDQFQIQW.js → chunk-3EQ6PVWL.js} +1 -1
- package/dist/chunk-4G4JBMCM.js +37 -0
- package/dist/{chunk-7AA2JWHZ.js → chunk-5DOQTIMD.js} +6 -6
- package/dist/chunk-666HGTVZ.js +41 -0
- package/dist/{chunk-7M56IUUX.js → chunk-6Z3ID54H.js} +0 -18
- package/dist/chunk-AUN4S3YW.js +50 -0
- package/dist/chunk-HIQM4J3U.js +30 -0
- package/dist/chunk-ISWQ6T5X.js +36 -0
- package/dist/{chunk-224QNWRA.js → chunk-K5J7ESRO.js} +1 -10
- package/dist/chunk-NM4CXXZY.js +25 -0
- package/dist/{chunk-44WND2VP.js → chunk-RFXHLLYU.js} +34 -62
- package/dist/{chunk-RXTN2CW6.js → chunk-Z3DLSLAJ.js} +1 -1
- package/dist/cinema-media-YZ2UANJG.js +365 -0
- package/dist/cli.js +154 -1748
- package/dist/{compose-video-BH5K6XWT.js → compose-video-RSLI4CVP.js} +4 -4
- package/dist/{events-B4YCc4vc.d.ts → events-BZAfl0Dm.d.ts} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -6
- package/dist/preload-media-23JXUFCF.js +77 -0
- package/dist/react.d.ts +39 -15
- package/dist/react.js +1355 -740
- package/dist/scene-validation-SGLLLFY5.js +9 -0
- package/dist/server.d.ts +67 -117
- package/dist/server.js +296 -626
- package/dist/test.d.ts +2 -2
- package/dist/test.js +21 -21
- package/dist/{text-stream-LD364KBI.js → text-stream-SOVLYR2L.js} +2 -2
- package/dist/{types-BqB8zC9u.d.ts → types-CG-kPI81.d.ts} +3 -1
- package/dist/{types-DdZw4GRQ.d.ts → types-ht-Zw3Wv.d.ts} +1 -1
- package/docs/agent-integration.md +19 -64
- package/docs/architecture.md +80 -106
- package/docs/customization.md +49 -66
- package/docs/development.md +18 -17
- package/docs/errors.md +4 -4
- package/docs/getting-started.md +74 -93
- package/docs/media-and-audio.md +9 -7
- package/docs/performance.md +11 -6
- package/docs/persistence.md +3 -7
- package/docs/production.md +4 -15
- package/docs/prompt-and-input.md +1 -2
- package/docs/provider-integration.md +156 -181
- package/docs/reference/protocol.md +5 -14
- package/docs/reference/provider-adapters.md +21 -13
- package/docs/security.md +1 -14
- package/docs/testing.md +56 -123
- package/package.json +4 -25
- package/starters/video-chat/.env.example +12 -4
- package/starters/video-chat/.env.native.example +13 -0
- package/starters/video-chat/README.md +72 -11
- package/starters/video-chat/package.json +1 -1
- package/starters/video-chat/providers/text-native.ts +94 -0
- package/starters/video-chat/providers/text.ts +19 -0
- package/starters/video-chat/providers/transcription.ts +32 -0
- package/starters/video-chat/providers/video-custom.ts +45 -0
- package/starters/video-chat/providers/video-delivery.ts +57 -0
- package/starters/video-chat/providers/video-google.ts +48 -0
- package/starters/video-chat/providers/video-job.ts +103 -0
- package/starters/video-chat/providers/video-runway.ts +45 -0
- package/starters/video-chat/providers/video.ts +39 -63
- package/starters/video-chat/server.ts +3 -29
- package/starters/video-chat/vite.config.ts +2 -8
- package/styles/video-chat.css +8 -105
- package/dist/builtin-server-W4PLJUZ5.js +0 -8
- package/dist/catalog-types-WTbLP6Jh.d.ts +0 -78
- package/dist/check-runtime.d.ts +0 -15
- package/dist/check-runtime.js +0 -96
- package/dist/chunk-2E6T633S.js +0 -27
- package/dist/chunk-4YM2M62S.js +0 -13
- package/dist/chunk-4ZJLPHBV.js +0 -684
- package/dist/chunk-5JBMYQP6.js +0 -156
- package/dist/chunk-73NTSFFI.js +0 -81
- package/dist/chunk-EGVQODKU.js +0 -83
- package/dist/chunk-HFVNAPHZ.js +0 -35
- package/dist/chunk-IIN5M5HW.js +0 -697
- package/dist/chunk-IQMYK5DX.js +0 -133
- package/dist/chunk-IR44XKBI.js +0 -46
- package/dist/chunk-JKVOBTRO.js +0 -145
- package/dist/chunk-LVM5Q2DL.js +0 -68
- package/dist/chunk-M4QTEJTK.js +0 -709
- package/dist/chunk-QSBDB4J2.js +0 -16
- package/dist/chunk-R3XAOMKP.js +0 -29
- package/dist/chunk-SPVTJH3F.js +0 -24
- package/dist/chunk-YAHT3LST.js +0 -663
- package/dist/chunk-ZD2VTUYR.js +0 -28
- package/dist/cinema-media-HYCDG65Z.js +0 -11
- package/dist/comparison-XXAP4S4J.js +0 -36
- package/dist/editorial-timeline-VUQC6KPA.js +0 -42
- package/dist/key-figure-4TJK7HLT.js +0 -29
- package/dist/kit-DrRpdn0p.d.ts +0 -79
- package/dist/mobile-message-FIHAO6XC.js +0 -49
- package/dist/preload-media-LJXWKTWG.js +0 -57
- package/dist/quote-GRPIYKQJ.js +0 -32
- package/dist/system-prompt-AG26KJAA.js +0 -12
- package/dist/template-catalog.d.ts +0 -400
- package/dist/template-catalog.js +0 -6
- package/dist/templates.d.ts +0 -25
- package/dist/templates.js +0 -102
- package/dist/validate-4OUD2CLX.js +0 -10
- package/docs/concepts.md +0 -107
- package/docs/custom-templates.md +0 -346
- package/docs/immersive-interface.md +0 -86
- package/docs/motion-and-effects.md +0 -109
- package/docs/reference/design-system.html +0 -125
- package/docs/responsive-orientation.md +0 -37
- package/docs/streaming-protocol.md +0 -15
- package/examples/custom-template/README.md +0 -19
- package/examples/custom-template/minimal-text.tsx +0 -82
- package/examples/custom-template/structured-data.tsx +0 -104
- package/registry/items/backgrounds.json +0 -70
- package/registry/items/chapterTitle.json +0 -80
- package/registry/items/cinemaMedia.json +0 -136
- package/registry/items/comparison.json +0 -168
- package/registry/items/editorialTimeline.json +0 -172
- package/registry/items/keyFigure.json +0 -162
- package/registry/items/mobileMessage.json +0 -153
- package/registry/items/motion.json +0 -45
- package/registry/items/quote.json +0 -161
- package/registry/items/template-context.json +0 -31
- package/registry/items/theme.json +0 -47
- package/registry/items/typography.json +0 -47
package/docs/testing.md
CHANGED
|
@@ -1,150 +1,83 @@
|
|
|
1
1
|
# Test video chat without a model
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Use deterministic callbacks for your route and the React-free
|
|
4
|
+
`@vanillaskyai/video/test` helpers for protocol cases. These tests need no keys,
|
|
5
|
+
provider SDKs, or real model requests.
|
|
6
6
|
|
|
7
|
-
## Test the
|
|
7
|
+
## Test the default route
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
fixtures. Default AI-first chat tests should instead return an authored answer
|
|
12
|
-
brief and shot descriptions matching the supplied planning prompt. A standard
|
|
13
|
-
`Request` exercises parsing, validation, pacing, the
|
|
14
|
-
opening extension, and SSE without starting an HTTP server.
|
|
9
|
+
Return the answer-brief shape requested by the handler's planning prompt, not
|
|
10
|
+
a generic template plan:
|
|
15
11
|
|
|
16
12
|
```ts
|
|
17
|
-
import {
|
|
18
|
-
import { createVideoChatHandler
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
prompt: "Explain why the Moon shows one face",
|
|
40
|
-
opening: "The Moon turns, perfectly matching its orbit.",
|
|
41
|
-
mode: "cinematic",
|
|
42
|
-
orientation: "landscape",
|
|
43
|
-
}),
|
|
44
|
-
},
|
|
45
|
-
));
|
|
46
|
-
const body = await response.text();
|
|
47
|
-
|
|
48
|
-
expect(response.status).toBe(200);
|
|
49
|
-
expect(body).toContain('"type":"data.video-chat-opening"');
|
|
50
|
-
expect(body).toContain('"type":"scene.add"');
|
|
51
|
-
expect(body).toContain('"type":"response.complete"');
|
|
13
|
+
import { expect, it } from "vitest";
|
|
14
|
+
import { createVideoChatHandler } from "@vanillaskyai/video/server";
|
|
15
|
+
|
|
16
|
+
it("completes a narrated chapter when footage is unavailable", async () => {
|
|
17
|
+
const handle = createVideoChatHandler({
|
|
18
|
+
authorize: "none", // In-process test only.
|
|
19
|
+
heartbeatMs: false,
|
|
20
|
+
streamText: async function* () {
|
|
21
|
+
yield JSON.stringify({
|
|
22
|
+
type: "answer", intent: "informational",
|
|
23
|
+
opening: "Waves move toward the shore.",
|
|
24
|
+
subject: "ocean waves", development: "",
|
|
25
|
+
visualDirection: "Natural ocean footage.",
|
|
26
|
+
ending: {
|
|
27
|
+
title: "Waves carry energy",
|
|
28
|
+
narration: "Ocean waves carry energy toward the shore.",
|
|
29
|
+
subject: "ocean waves", action: "Follow waves toward the shore.",
|
|
30
|
+
durationSec: 4, continuity: "cut",
|
|
31
|
+
},
|
|
32
|
+
}) + "\n";
|
|
33
|
+
},
|
|
34
|
+
generateText: async () => "[]",
|
|
52
35
|
});
|
|
36
|
+
const response = await handle(new Request(
|
|
37
|
+
"https://app.test/api/video-chat?action=response",
|
|
38
|
+
{ method: "POST", body: JSON.stringify({ prompt: "Explain waves" }) },
|
|
39
|
+
));
|
|
40
|
+
const body = await response.text();
|
|
41
|
+
expect(response.status).toBe(200);
|
|
42
|
+
expect(body).toContain('"type":"scene.add"');
|
|
43
|
+
expect(body).toContain('"type":"response.complete"');
|
|
53
44
|
});
|
|
54
45
|
```
|
|
55
46
|
|
|
56
|
-
Also
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
and wait for a known template rather than reaching into hook internals.
|
|
60
|
-
|
|
61
|
-
## Test protocol events in process
|
|
47
|
+
Also test authorization and capability discovery. In the browser, submit through
|
|
48
|
+
the visible composer and verify a completed answer and recovery. Do not couple
|
|
49
|
+
application tests to private hook state or exact prompt wording.
|
|
62
50
|
|
|
63
|
-
|
|
64
|
-
pipeline without an HTTP boundary. It yields typed event objects and remains
|
|
65
|
-
useful for focused planner and protocol cases beneath the chat handler.
|
|
51
|
+
## Protocol fixtures
|
|
66
52
|
|
|
67
53
|
```ts
|
|
68
54
|
import { expect, it } from "vitest";
|
|
69
|
-
import {
|
|
70
|
-
simulateVideoStream,
|
|
71
|
-
videoFixtures,
|
|
72
|
-
} from "@vanillaskyai/video/test";
|
|
55
|
+
import { simulateVideoStream, videoFixtures } from "@vanillaskyai/video/test";
|
|
73
56
|
|
|
74
57
|
it("keeps a truncated result playable", async () => {
|
|
75
58
|
const events = [];
|
|
76
|
-
for await (const event of simulateVideoStream(
|
|
77
|
-
videoFixtures.scenarios.truncated,
|
|
78
|
-
)) {
|
|
59
|
+
for await (const event of simulateVideoStream(videoFixtures.scenarios.truncated)) {
|
|
79
60
|
events.push(event);
|
|
80
61
|
}
|
|
81
|
-
|
|
82
62
|
expect(events.at(-1)).toMatchObject({
|
|
83
|
-
type: "response.complete",
|
|
84
|
-
data: { finishReason: "length" },
|
|
63
|
+
type: "response.complete", data: { finishReason: "length" },
|
|
85
64
|
});
|
|
86
65
|
});
|
|
87
66
|
```
|
|
88
67
|
|
|
89
|
-
The portrait and landscape fixtures
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
Delays and `timeoutMs` use ordinary timers and work with Vitest fake timers.
|
|
95
|
-
|
|
96
|
-
```ts
|
|
97
|
-
import { expect, it, vi } from "vitest";
|
|
98
|
-
import {
|
|
99
|
-
simulateVideoStream,
|
|
100
|
-
videoFixtures,
|
|
101
|
-
} from "@vanillaskyai/video/test";
|
|
102
|
-
|
|
103
|
-
it("times out deterministically", async () => {
|
|
104
|
-
vi.useFakeTimers();
|
|
105
|
-
try {
|
|
106
|
-
const result = (async () => {
|
|
107
|
-
const events = [];
|
|
108
|
-
for await (const event of simulateVideoStream(
|
|
109
|
-
videoFixtures.scenarios.timeout,
|
|
110
|
-
{ timeoutMs: 50 },
|
|
111
|
-
)) events.push(event);
|
|
112
|
-
return events;
|
|
113
|
-
})();
|
|
114
|
-
|
|
115
|
-
await vi.advanceTimersByTimeAsync(50);
|
|
116
|
-
await expect(result).resolves.toMatchObject([
|
|
117
|
-
{ type: "response.start" },
|
|
118
|
-
{ type: "scene.add" },
|
|
119
|
-
{ type: "response.abort", data: { reason: "Request timed out" } },
|
|
120
|
-
]);
|
|
121
|
-
} finally {
|
|
122
|
-
vi.useRealTimers();
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
For host cancellation, pass an `AbortController` signal and abort after the
|
|
128
|
-
desired partial event. At the route boundary, the host remains responsible for
|
|
129
|
-
aborting the request signal and each provider callback must honour it.
|
|
130
|
-
|
|
131
|
-
## Fixed planner scenarios
|
|
132
|
-
|
|
133
|
-
`createMockVideoPlanner({ scenario })` accepts:
|
|
68
|
+
The portrait and landscape fixtures contain frozen `{ input, parts }`.
|
|
69
|
+
Helpers clone inputs for each run. `createMockVideoPlanner({ scenario })`
|
|
70
|
+
provides success, delayed, truncated, invalidScene, providerFailure,
|
|
71
|
+
contentFilter, abort, and timeout scenarios for structural protocol tests.
|
|
72
|
+
Those structural parts are not the default chat model's answer-brief format.
|
|
134
73
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
| `truncated` | Partial playable result completed with `length` |
|
|
140
|
-
| `invalidScene` | Invalid scene dropped with a recoverable warning, then completion |
|
|
141
|
-
| `providerFailure` | Redacted terminal generation failure |
|
|
142
|
-
| `contentFilter` | Partial playable result completed with `content-filter` |
|
|
143
|
-
| `abort` | Waits for the supplied request signal to abort |
|
|
144
|
-
| `timeout` | Waits for the host signal or simulator `timeoutMs` |
|
|
74
|
+
`simulateVideoStream(parts, { signal, timeoutMs })` handles abort and timeout
|
|
75
|
+
without a server. Delays use ordinary timers and work with Vitest fake timers.
|
|
76
|
+
Test cancellation at your route boundary too: every provider must honor the
|
|
77
|
+
request signal.
|
|
145
78
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
79
|
+
Keep fast tests focused on behavior. Run the real packed consumer when public
|
|
80
|
+
types or installation change and browser media tests when playback changes.
|
|
81
|
+
A deterministic fixture proves integration, not live-video quality or latency.
|
|
149
82
|
|
|
150
|
-
[
|
|
83
|
+
[Documentation home](../README.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanillaskyai/video",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Open-source voice-and-video chat SDK for AI applications.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"video-chat",
|
|
@@ -34,19 +34,17 @@
|
|
|
34
34
|
"styles/video-chat.css",
|
|
35
35
|
"styles/fonts",
|
|
36
36
|
"starters/video-chat/.env.example",
|
|
37
|
+
"starters/video-chat/.env.native.example",
|
|
37
38
|
"starters/video-chat/README.md",
|
|
38
39
|
"starters/video-chat/index.html",
|
|
39
40
|
"starters/video-chat/package.json",
|
|
40
41
|
"starters/video-chat/server.ts",
|
|
41
42
|
"starters/video-chat/providers.ts",
|
|
42
|
-
"starters/video-chat/providers
|
|
43
|
-
"starters/video-chat/providers/video.ts",
|
|
43
|
+
"starters/video-chat/providers",
|
|
44
44
|
"starters/video-chat/src/main.tsx",
|
|
45
45
|
"starters/video-chat/stock.ts",
|
|
46
46
|
"starters/video-chat/tsconfig.json",
|
|
47
47
|
"starters/video-chat/vite.config.ts",
|
|
48
|
-
"registry/items",
|
|
49
|
-
"examples/custom-template",
|
|
50
48
|
"README.md",
|
|
51
49
|
"LICENSE",
|
|
52
50
|
"SECURITY.md",
|
|
@@ -77,16 +75,6 @@
|
|
|
77
75
|
"import": "./dist/react.js",
|
|
78
76
|
"default": "./dist/react.js"
|
|
79
77
|
},
|
|
80
|
-
"./templates": {
|
|
81
|
-
"types": "./dist/templates.d.ts",
|
|
82
|
-
"import": "./dist/templates.js",
|
|
83
|
-
"default": "./dist/templates.js"
|
|
84
|
-
},
|
|
85
|
-
"./templates/catalog": {
|
|
86
|
-
"types": "./dist/template-catalog.d.ts",
|
|
87
|
-
"import": "./dist/template-catalog.js",
|
|
88
|
-
"default": "./dist/template-catalog.js"
|
|
89
|
-
},
|
|
90
78
|
"./test": {
|
|
91
79
|
"types": "./dist/test.d.ts",
|
|
92
80
|
"import": "./dist/test.js",
|
|
@@ -99,11 +87,6 @@
|
|
|
99
87
|
"browser:test": "playwright test",
|
|
100
88
|
"build": "tsup",
|
|
101
89
|
"lint": "eslint .",
|
|
102
|
-
"catalog:sync": "tsx scripts/generate-builtin-catalog.ts",
|
|
103
|
-
"catalog:check": "tsx scripts/generate-builtin-catalog.ts --check",
|
|
104
|
-
"registry:sync": "npm run catalog:sync && node scripts/sync-template-registry.mjs",
|
|
105
|
-
"registry:check": "node scripts/sync-template-registry.mjs --check && npm run catalog:check",
|
|
106
|
-
"template:check": "tsx scripts/template-check.ts",
|
|
107
90
|
"test": "vitest run",
|
|
108
91
|
"test:watch": "vitest",
|
|
109
92
|
"typecheck": "tsc --noEmit",
|
|
@@ -122,8 +105,7 @@
|
|
|
122
105
|
},
|
|
123
106
|
"peerDependencies": {
|
|
124
107
|
"react": ">=18 <20",
|
|
125
|
-
"react-dom": ">=18 <20"
|
|
126
|
-
"tsx": ">=4.19 <5"
|
|
108
|
+
"react-dom": ">=18 <20"
|
|
127
109
|
},
|
|
128
110
|
"peerDependenciesMeta": {
|
|
129
111
|
"react": {
|
|
@@ -131,9 +113,6 @@
|
|
|
131
113
|
},
|
|
132
114
|
"react-dom": {
|
|
133
115
|
"optional": true
|
|
134
|
-
},
|
|
135
|
-
"tsx": {
|
|
136
|
-
"optional": true
|
|
137
116
|
}
|
|
138
117
|
},
|
|
139
118
|
"devDependencies": {
|
|
@@ -5,14 +5,22 @@ ANTHROPIC_API_KEY=
|
|
|
5
5
|
# Generated speech; browser voice remains the fallback.
|
|
6
6
|
XAI_API_KEY=
|
|
7
7
|
|
|
8
|
-
# Optional:
|
|
9
|
-
#
|
|
8
|
+
# Optional: `providers add video fal` and/or `providers add transcription`.
|
|
9
|
+
# These are independent capabilities.
|
|
10
10
|
FAL_KEY=
|
|
11
|
+
GEMINI_API_KEY=
|
|
12
|
+
RUNWAY_API_KEY=
|
|
11
13
|
|
|
12
|
-
#
|
|
14
|
+
# Required by the example video-delivery callback, or replace it with your storage.
|
|
15
|
+
VIDEO_UPLOAD_URL=
|
|
16
|
+
VIDEO_STORAGE_TOKEN=
|
|
17
|
+
|
|
18
|
+
# Optional: stock video and photography for scenes and suggestion cards.
|
|
13
19
|
PEXELS_API_KEY=
|
|
14
20
|
|
|
15
|
-
# Optional model overrides.
|
|
21
|
+
# Optional model overrides. Check model duration/resolution policy when changing.
|
|
16
22
|
# ANTHROPIC_PLANNER_MODEL=
|
|
17
23
|
# ANTHROPIC_NARRATION_MODEL=
|
|
18
24
|
# FAL_VIDEO_MODEL=
|
|
25
|
+
# GOOGLE_VIDEO_MODEL=
|
|
26
|
+
# RUNWAY_VIDEO_MODEL=
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Required for the native Gemini REST text adapter (no Vercel AI SDK packages).
|
|
2
|
+
GEMINI_API_KEY=
|
|
3
|
+
# GEMINI_TEXT_MODEL=gemini-2.5-flash
|
|
4
|
+
|
|
5
|
+
# Optional independent capabilities; install each with providers add.
|
|
6
|
+
XAI_API_KEY=
|
|
7
|
+
FAL_KEY=
|
|
8
|
+
RUNWAY_API_KEY=
|
|
9
|
+
PEXELS_API_KEY=
|
|
10
|
+
|
|
11
|
+
# Required by the example video-delivery callback, or replace it with your storage.
|
|
12
|
+
VIDEO_UPLOAD_URL=
|
|
13
|
+
VIDEO_STORAGE_TOKEN=
|
|
@@ -13,31 +13,44 @@ npm run dev
|
|
|
13
13
|
```
|
|
14
14
|
<!-- verify:end -->
|
|
15
15
|
|
|
16
|
-
Add
|
|
16
|
+
The default text adapter uses the optional Vercel AI SDK. Add its key to `.env.local`:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
ANTHROPIC_API_KEY=...
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
Prefer native callbacks? In a new folder run `npx vanillasky init --native` and
|
|
23
|
+
set `GEMINI_API_KEY`. This installs editable Gemini REST text callbacks without
|
|
24
|
+
`ai` or `@ai-sdk/anthropic`. The SDK itself requires neither Vercel nor Google.
|
|
25
|
+
Change `providers/text.ts` to use any text model or your own service.
|
|
26
|
+
|
|
22
27
|
That enables the introduction and browser voice. Add the video adapter for
|
|
23
28
|
generated footage; without media, narration and subtitles remain available.
|
|
24
29
|
The starter installs only the text provider. Enable adapters as needed:
|
|
25
30
|
|
|
26
31
|
```bash
|
|
27
32
|
npx vanillasky providers add speech
|
|
28
|
-
npx vanillasky providers add video
|
|
33
|
+
npx vanillasky providers add video fal
|
|
34
|
+
npx vanillasky providers add transcription
|
|
29
35
|
```
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
Video choices are `fal`, `google`, `runway`, or `custom`; omitting a name selects
|
|
38
|
+
fal. Pick one. These are editable reference integrations, not an SDK vendor
|
|
39
|
+
registry. Video and transcription use native `fetch` with no provider packages.
|
|
40
|
+
Only the optional speech upgrade installs `ai` and `@ai-sdk/xai`.
|
|
33
41
|
|
|
34
42
|
- Speech: add `XAI_API_KEY` to replace browser voice with generated speech.
|
|
35
|
-
- Video:
|
|
43
|
+
- Video: `FAL_KEY`, `GEMINI_API_KEY`, or `RUNWAY_API_KEY` for the selected vendor,
|
|
44
|
+
plus your app-owned video delivery callback described below.
|
|
45
|
+
- Transcription: add `FAL_KEY` for Whisper independently of your video vendor.
|
|
36
46
|
- Stock: add `PEXELS_API_KEY` for footage and suggestion images; no installation
|
|
37
47
|
is needed.
|
|
38
48
|
|
|
39
49
|
The adapters live in `providers/` and remain application-owned. Running an
|
|
40
50
|
upgrade again can finish an interrupted dependency installation.
|
|
51
|
+
To switch an already-installed video vendor, replace `providers/video.ts` with
|
|
52
|
+
the corresponding packaged reference (or your implementation) and update
|
|
53
|
+
`vanillasky.videoVendor` in package.json. The CLI will not overwrite edited code.
|
|
41
54
|
|
|
42
55
|
Restart the development server after changing keys. Open
|
|
43
56
|
<http://localhost:5173> and prompt anything.
|
|
@@ -57,16 +70,64 @@ The Pexels adapter searches the full catalog with bounded subject matching,
|
|
|
57
70
|
orientation selection and caching; it no longer requires a reviewed index.
|
|
58
71
|
Custom interfaces must display a prominent [Pexels](https://www.pexels.com) credit.
|
|
59
72
|
|
|
60
|
-
Speech and video prepare together.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
Speech and video prepare together. Each adapter owns its model, duration,
|
|
74
|
+
resolution, concurrency and deadline; its exported preset forwards those values
|
|
75
|
+
to the handler. Do not override the preset's `generatedClipDurationSec` without
|
|
76
|
+
changing the model request, too. It returns `durationSec` with the delivered
|
|
77
|
+
video. The SDK budgets narration to the clip and holds its last frame if speech
|
|
78
|
+
still overruns; failed media becomes a simple authored chapter.
|
|
79
|
+
|
|
80
|
+
The fal reference requests five seconds at 480P, concurrency 3, deadline 120s.
|
|
81
|
+
Google Veo requests six seconds at 720p, concurrency 2, deadline 360s.
|
|
82
|
+
Runway Gen-4.5 requests five seconds at 720p, concurrency 2, deadline 180s.
|
|
83
|
+
These are explicit starting policies, not measured performance promises.
|
|
84
|
+
Google and Runway may take minutes. Preparing scenes early and playing them in
|
|
85
|
+
order does not make a provider's job API a real-time video stream.
|
|
86
|
+
|
|
87
|
+
Each request submits paid generation once and retains its job ID while polling.
|
|
88
|
+
An ambiguous network failure is never automatically resubmitted. Inspect the
|
|
89
|
+
provider dashboard before retrying. To survive process restarts, add an
|
|
90
|
+
`onSubmitted` callback to `runVideoJob` that persists the ID in your job ledger.
|
|
91
|
+
Abort/deadline stops polling and requests best-effort cancellation on fal/Runway.
|
|
92
|
+
Google Veo has no documented cancellation operation; accepted work can still
|
|
93
|
+
complete and be billed. Cancellation never promises a refund.
|
|
94
|
+
|
|
95
|
+
### App-owned storage and delivery
|
|
96
|
+
|
|
97
|
+
Every direct video adapter requires a delivery callback. `video-delivery.ts`
|
|
98
|
+
includes an example for **your own** upload endpoint: configure `VIDEO_UPLOAD_URL`
|
|
99
|
+
and `VIDEO_STORAGE_TOKEN`; the endpoint accepts `PUT` MP4 bytes with a `jobId`
|
|
100
|
+
query parameter and returns `{ "url": "https://your-cdn.example/clip.mp4" }`.
|
|
101
|
+
This endpoint is not supplied by VanillaSky. Replace the callback with your
|
|
102
|
+
existing S3/R2/storage code and update `videoDeliveryConfigured` if preferred.
|
|
103
|
+
Until delivery is configured, the starter does not submit video jobs.
|
|
104
|
+
|
|
105
|
+
The callback receives server-side bytes, the job ID and clip duration. Google
|
|
106
|
+
downloads require a private API key; that key and its private video URI must
|
|
107
|
+
never be sent to the player. Store all vendors' temporary results long enough
|
|
108
|
+
for playback/replay. Deliver H.264 MP4 with the moov atom at the front, correct
|
|
109
|
+
Content-Type/Content-Length, byte-range support and appropriate CORS. The app
|
|
110
|
+
owns storage, access rules, retention and deletion; the core SDK owns none of it.
|
|
111
|
+
|
|
112
|
+
`providers add video custom` supplies a callback skeleton for any native API or
|
|
113
|
+
vendor SDK. Fill in submit, poll, optional cancel and deliver, then wire the
|
|
114
|
+
factory into `videoProvider.generateVideo`. There is no vendor allowlist in the
|
|
115
|
+
core API. Update `vanillasky.requiredEnv` for custom text key requirements;
|
|
116
|
+
`doctor` checks configuration locally and never submits a paid test job.
|
|
65
117
|
|
|
66
118
|
The provider names its own model. Override the tested defaults with
|
|
67
|
-
`ANTHROPIC_PLANNER_MODEL`, `ANTHROPIC_NARRATION_MODEL`,
|
|
119
|
+
`ANTHROPIC_PLANNER_MODEL`, `ANTHROPIC_NARRATION_MODEL`, `GEMINI_TEXT_MODEL`,
|
|
120
|
+
`FAL_VIDEO_MODEL`, `GOOGLE_VIDEO_MODEL`, or `RUNWAY_VIDEO_MODEL`
|
|
68
121
|
when needed.
|
|
69
122
|
|
|
123
|
+
API references checked for these examples: [fal queue](https://fal.ai/docs/documentation/model-apis/inference/queue),
|
|
124
|
+
[fal H3](https://fal.ai/models/minimax/h3-max-turbo/text-to-video/api),
|
|
125
|
+
[Google Veo](https://ai.google.dev/gemini-api/docs/video),
|
|
126
|
+
[Runway](https://docs.dev.runwayml.com/api/),
|
|
127
|
+
[native Gemini text](https://ai.google.dev/gemini-api/docs/generate-content/text-generation).
|
|
128
|
+
The offline tests check request/response contracts; they do not establish live
|
|
129
|
+
model quality, account access, provider availability or paid generation speed.
|
|
130
|
+
|
|
70
131
|
### Stock selection hints
|
|
71
132
|
|
|
72
133
|
In Pexels mode, the same planning stream can supply an optional
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { VideoChatHandlerOptions } from "@vanillaskyai/video/server";
|
|
2
|
+
|
|
3
|
+
// Native Gemini REST example; the SDK only needs strings/AsyncIterable<string>.
|
|
4
|
+
// https://ai.google.dev/gemini-api/docs/generate-content/text-generation
|
|
5
|
+
const MODEL = process.env.GEMINI_TEXT_MODEL ?? "gemini-2.5-flash";
|
|
6
|
+
const BASE = "https://generativelanguage.googleapis.com/v1beta";
|
|
7
|
+
type TextInput = { systemPrompt: string; userPrompt: string; signal: AbortSignal; maxOutputTokens?: number };
|
|
8
|
+
type GeminiOutput = {
|
|
9
|
+
candidates?: { content?: { parts?: { text?: string; thought?: boolean }[] }; finishReason?: string }[];
|
|
10
|
+
promptFeedback?: { blockReason?: string };
|
|
11
|
+
error?: unknown;
|
|
12
|
+
};
|
|
13
|
+
type Completion = { finishReason: "stop" | "length" | "content-filter" | "error"; rawFinishReason?: string };
|
|
14
|
+
|
|
15
|
+
function completion(output: GeminiOutput): Completion | undefined {
|
|
16
|
+
const raw = output.promptFeedback?.blockReason ?? output.candidates?.[0]?.finishReason;
|
|
17
|
+
if (!raw) return undefined;
|
|
18
|
+
const filtered = output.promptFeedback?.blockReason || ["SAFETY", "RECITATION", "BLOCKLIST", "PROHIBITED_CONTENT", "SPII", "IMAGE_SAFETY"].includes(raw);
|
|
19
|
+
return { rawFinishReason: raw, finishReason: filtered ? "content-filter" : raw === "STOP" ? "stop" : raw === "MAX_TOKENS" ? "length" : "error" };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function outputText(output: GeminiOutput): string {
|
|
23
|
+
if (output.error) throw new Error("Text provider failed");
|
|
24
|
+
return (output.candidates?.[0]?.content?.parts ?? []).filter((part) => !part.thought).map((part) => part.text ?? "").join("");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function request(input: TextInput, streaming: boolean): Promise<Response> {
|
|
28
|
+
if (!process.env.GEMINI_API_KEY) throw new Error("Configure GEMINI_API_KEY for the native text adapter");
|
|
29
|
+
const response = await fetch(`${BASE}/models/${MODEL}:${streaming ? "streamGenerateContent?alt=sse" : "generateContent"}`, {
|
|
30
|
+
method: "POST", signal: input.signal,
|
|
31
|
+
headers: { "x-goog-api-key": process.env.GEMINI_API_KEY, "content-type": "application/json" },
|
|
32
|
+
body: JSON.stringify({ systemInstruction: { parts: [{ text: input.systemPrompt }] },
|
|
33
|
+
contents: [{ role: "user", parts: [{ text: input.userPrompt }] }],
|
|
34
|
+
generationConfig: { maxOutputTokens: input.maxOutputTokens ?? 8192, thinkingConfig: { thinkingBudget: 0 } } }),
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) throw new Error(`Text provider HTTP ${response.status}`);
|
|
37
|
+
return response;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function* stream(input: TextInput, settle: (value: Completion) => void): AsyncGenerator<string> {
|
|
41
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
42
|
+
const decoder = new TextDecoder();
|
|
43
|
+
let pending = "";
|
|
44
|
+
try {
|
|
45
|
+
const response = await request(input, true);
|
|
46
|
+
if (!response.body) throw new Error("Text provider returned no stream");
|
|
47
|
+
reader = response.body.getReader();
|
|
48
|
+
while (true) {
|
|
49
|
+
const { done, value } = await reader.read();
|
|
50
|
+
pending += decoder.decode(value, { stream: !done });
|
|
51
|
+
const lines = pending.split(/\r?\n/);
|
|
52
|
+
pending = done ? "" : lines.pop()!;
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
if (!line.startsWith("data:")) continue;
|
|
55
|
+
const data = line.slice(5).trim();
|
|
56
|
+
if (!data || data === "[DONE]") continue;
|
|
57
|
+
const output = JSON.parse(data) as GeminiOutput;
|
|
58
|
+
const text = outputText(output);
|
|
59
|
+
const finished = completion(output);
|
|
60
|
+
if (finished) settle(finished);
|
|
61
|
+
if (text) yield text;
|
|
62
|
+
if (finished) return;
|
|
63
|
+
}
|
|
64
|
+
if (done) break;
|
|
65
|
+
}
|
|
66
|
+
} finally {
|
|
67
|
+
// Also settle transport errors, missing terminal events and iterator close.
|
|
68
|
+
// Promises resolve (never reject together) so diagnostics cannot hang or
|
|
69
|
+
// create unhandled rejections when the text iterator itself fails.
|
|
70
|
+
settle({ finishReason: "error" });
|
|
71
|
+
void reader?.cancel().catch(() => undefined);
|
|
72
|
+
reader?.releaseLock();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const textProvider: Pick<VideoChatHandlerOptions, "streamText" | "generateText"> = {
|
|
77
|
+
streamText: (input) => {
|
|
78
|
+
let settle!: (value: Completion) => void;
|
|
79
|
+
const finished = new Promise<Completion>(resolve => { settle = resolve; });
|
|
80
|
+
const iterator = stream(input, settle);
|
|
81
|
+
// An async generator's finally does not run when closed before first next().
|
|
82
|
+
const textStream: AsyncIterable<string> = { [Symbol.asyncIterator]: () => ({
|
|
83
|
+
next: () => iterator.next(),
|
|
84
|
+
return: () => { settle({ finishReason: "error" }); return iterator.return(undefined); },
|
|
85
|
+
}) };
|
|
86
|
+
return { textStream, finishReason: finished.then(value => value.finishReason),
|
|
87
|
+
rawFinishReason: finished.then(value => value.rawFinishReason) };
|
|
88
|
+
},
|
|
89
|
+
generateText: async (input) => {
|
|
90
|
+
const output = await (await request(input, false)).json() as GeminiOutput;
|
|
91
|
+
if (completion(output)?.finishReason !== "stop") throw new Error("Text provider did not return a complete unblocked answer");
|
|
92
|
+
return outputText(output);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
2
|
+
import { generateText, streamText } from "ai";
|
|
3
|
+
import type { VideoChatHandlerOptions } from "@vanillaskyai/video/server";
|
|
4
|
+
|
|
5
|
+
// Optional Vercel AI SDK integration. Replace just this file to change text vendors.
|
|
6
|
+
const PLANNER_MODEL = process.env.ANTHROPIC_PLANNER_MODEL ?? "claude-sonnet-5";
|
|
7
|
+
const NARRATION_MODEL = process.env.ANTHROPIC_NARRATION_MODEL ?? "claude-haiku-4-5";
|
|
8
|
+
export const textProvider: Pick<VideoChatHandlerOptions, "streamText" | "generateText"> = {
|
|
9
|
+
streamText: ({ systemPrompt, userPrompt, signal }) => streamText({
|
|
10
|
+
model: anthropic(PLANNER_MODEL), system: systemPrompt, prompt: userPrompt,
|
|
11
|
+
abortSignal: signal, maxOutputTokens: 8192,
|
|
12
|
+
providerOptions: { anthropic: { thinking: { type: "disabled" }, output_config: { effort: "medium" } } },
|
|
13
|
+
}),
|
|
14
|
+
generateText: async ({ systemPrompt, userPrompt, maxOutputTokens, signal }) => {
|
|
15
|
+
const { text } = await generateText({ model: anthropic(NARRATION_MODEL), system: systemPrompt,
|
|
16
|
+
prompt: userPrompt, maxOutputTokens, abortSignal: signal });
|
|
17
|
+
return text;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { VideoChatHandlerOptions } from "@vanillaskyai/video/server";
|
|
2
|
+
import { jsonResponse, providerUrl, runVideoJob } from "./video-job";
|
|
3
|
+
|
|
4
|
+
// Separate capability: using fal Whisper does not select fal for video.
|
|
5
|
+
// https://fal.ai/models/fal-ai/whisper/api — accepts a base64 data URI.
|
|
6
|
+
export const transcriptionProvider: Pick<VideoChatHandlerOptions, "transcribe"> = {
|
|
7
|
+
transcribe: process.env.FAL_KEY ? async ({ audio, mediaType, signal }) => {
|
|
8
|
+
signal.throwIfAborted();
|
|
9
|
+
const headers = { Authorization: `Key ${process.env.FAL_KEY}`, "content-type": "application/json" };
|
|
10
|
+
const { value } = await runVideoJob({
|
|
11
|
+
signal, deadlineAt: Date.now() + 60_000, pollIntervalMs: 1000,
|
|
12
|
+
submit: async (signal) => {
|
|
13
|
+
const result = await jsonResponse<{ request_id: string; status_url: string; response_url: string; cancel_url: string }>(await fetch("https://queue.fal.run/fal-ai/whisper", {
|
|
14
|
+
method: "POST", headers, signal,
|
|
15
|
+
body: JSON.stringify({ audio_url: `data:${mediaType};base64,${Buffer.from(audio).toString("base64")}`, task: "transcribe" }),
|
|
16
|
+
}));
|
|
17
|
+
return { id: result.request_id, statusUrl: providerUrl(result.status_url, "https://queue.fal.run"),
|
|
18
|
+
resultUrl: providerUrl(result.response_url, "https://queue.fal.run"), cancelUrl: providerUrl(result.cancel_url, "https://queue.fal.run") };
|
|
19
|
+
},
|
|
20
|
+
poll: async (job, signal) => {
|
|
21
|
+
const status = await jsonResponse<{ status: string; error?: unknown }>(await fetch(job.statusUrl, { headers, signal }));
|
|
22
|
+
if (status.error) throw new Error("Transcription failed");
|
|
23
|
+
if (["IN_QUEUE", "IN_PROGRESS"].includes(status.status)) return null;
|
|
24
|
+
if (status.status !== "COMPLETED") throw new Error("Unexpected transcription status");
|
|
25
|
+
const result = await jsonResponse<{ text: string }>(await fetch(job.resultUrl, { headers, signal }));
|
|
26
|
+
return result.text;
|
|
27
|
+
},
|
|
28
|
+
cancel: async (job, signal) => { await fetch(job.cancelUrl, { method: "PUT", headers, signal }); },
|
|
29
|
+
});
|
|
30
|
+
return value;
|
|
31
|
+
} : undefined,
|
|
32
|
+
};
|