@vanillaskyai/video 0.10.23 → 0.11.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.
Files changed (127) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/PUBLIC-API.md +45 -438
  3. package/README.md +53 -97
  4. package/dist/builtin-metadata-OT6V7TB4.js +8 -0
  5. package/dist/{chapter-title-2JVDU62E.js → chapter-title-2RCXX7SN.js} +1 -2
  6. package/dist/{chunk-PDQFQIQW.js → chunk-3EQ6PVWL.js} +1 -1
  7. package/dist/chunk-4G4JBMCM.js +37 -0
  8. package/dist/{chunk-7AA2JWHZ.js → chunk-5DOQTIMD.js} +6 -6
  9. package/dist/chunk-666HGTVZ.js +41 -0
  10. package/dist/{chunk-7M56IUUX.js → chunk-6Z3ID54H.js} +0 -18
  11. package/dist/{chunk-5BSJLT6H.js → chunk-E7FHBLAC.js} +16 -29
  12. package/dist/chunk-HIQM4J3U.js +30 -0
  13. package/dist/chunk-ISWQ6T5X.js +36 -0
  14. package/dist/{chunk-224QNWRA.js → chunk-K5J7ESRO.js} +1 -10
  15. package/dist/chunk-KMVRBUL5.js +50 -0
  16. package/dist/chunk-NM4CXXZY.js +25 -0
  17. package/dist/{chunk-44WND2VP.js → chunk-V2CP7PVY.js} +19 -63
  18. package/dist/{chunk-3O7OMMMF.js → chunk-WZESNEPT.js} +0 -1
  19. package/dist/{chunk-RXTN2CW6.js → chunk-Z3DLSLAJ.js} +1 -1
  20. package/dist/cinema-media-YZ2UANJG.js +365 -0
  21. package/dist/cli.js +154 -1748
  22. package/dist/{compose-video-BH5K6XWT.js → compose-video-H2YUQDUY.js} +4 -4
  23. package/dist/{events-B4YCc4vc.d.ts → events-BZAfl0Dm.d.ts} +1 -1
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.js +4 -6
  26. package/dist/preload-media-23JXUFCF.js +77 -0
  27. package/dist/react.d.ts +39 -15
  28. package/dist/react.js +1350 -739
  29. package/dist/scene-validation-SGLLLFY5.js +9 -0
  30. package/dist/server.d.ts +67 -117
  31. package/dist/server.js +262 -614
  32. package/dist/test.d.ts +2 -2
  33. package/dist/test.js +21 -21
  34. package/dist/{text-stream-LD364KBI.js → text-stream-SOVLYR2L.js} +2 -2
  35. package/dist/{types-BqB8zC9u.d.ts → types-CG-kPI81.d.ts} +3 -1
  36. package/dist/{types-DdZw4GRQ.d.ts → types-ht-Zw3Wv.d.ts} +1 -1
  37. package/docs/agent-integration.md +19 -64
  38. package/docs/architecture.md +80 -106
  39. package/docs/customization.md +49 -66
  40. package/docs/development.md +18 -17
  41. package/docs/errors.md +4 -4
  42. package/docs/getting-started.md +74 -93
  43. package/docs/media-and-audio.md +9 -7
  44. package/docs/performance.md +11 -6
  45. package/docs/persistence.md +3 -7
  46. package/docs/production.md +4 -15
  47. package/docs/prompt-and-input.md +1 -2
  48. package/docs/provider-integration.md +134 -181
  49. package/docs/reference/protocol.md +5 -14
  50. package/docs/reference/provider-adapters.md +21 -13
  51. package/docs/security.md +1 -14
  52. package/docs/testing.md +56 -123
  53. package/package.json +4 -25
  54. package/starters/video-chat/.env.example +12 -4
  55. package/starters/video-chat/.env.native.example +13 -0
  56. package/starters/video-chat/README.md +72 -11
  57. package/starters/video-chat/package.json +1 -1
  58. package/starters/video-chat/providers/text-native.ts +94 -0
  59. package/starters/video-chat/providers/text.ts +19 -0
  60. package/starters/video-chat/providers/transcription.ts +32 -0
  61. package/starters/video-chat/providers/video-custom.ts +45 -0
  62. package/starters/video-chat/providers/video-delivery.ts +57 -0
  63. package/starters/video-chat/providers/video-google.ts +48 -0
  64. package/starters/video-chat/providers/video-job.ts +103 -0
  65. package/starters/video-chat/providers/video-runway.ts +45 -0
  66. package/starters/video-chat/providers/video.ts +39 -63
  67. package/starters/video-chat/server.ts +3 -29
  68. package/starters/video-chat/vite.config.ts +2 -8
  69. package/styles/video-chat.css +8 -105
  70. package/dist/builtin-server-W4PLJUZ5.js +0 -8
  71. package/dist/catalog-types-WTbLP6Jh.d.ts +0 -78
  72. package/dist/check-runtime.d.ts +0 -15
  73. package/dist/check-runtime.js +0 -96
  74. package/dist/chunk-2E6T633S.js +0 -27
  75. package/dist/chunk-4YM2M62S.js +0 -13
  76. package/dist/chunk-4ZJLPHBV.js +0 -684
  77. package/dist/chunk-5JBMYQP6.js +0 -156
  78. package/dist/chunk-73NTSFFI.js +0 -81
  79. package/dist/chunk-EGVQODKU.js +0 -83
  80. package/dist/chunk-HFVNAPHZ.js +0 -35
  81. package/dist/chunk-IIN5M5HW.js +0 -697
  82. package/dist/chunk-IQMYK5DX.js +0 -133
  83. package/dist/chunk-IR44XKBI.js +0 -46
  84. package/dist/chunk-JKVOBTRO.js +0 -145
  85. package/dist/chunk-LVM5Q2DL.js +0 -68
  86. package/dist/chunk-M4QTEJTK.js +0 -709
  87. package/dist/chunk-QSBDB4J2.js +0 -16
  88. package/dist/chunk-R3XAOMKP.js +0 -29
  89. package/dist/chunk-SPVTJH3F.js +0 -24
  90. package/dist/chunk-YAHT3LST.js +0 -663
  91. package/dist/chunk-ZD2VTUYR.js +0 -28
  92. package/dist/cinema-media-HYCDG65Z.js +0 -11
  93. package/dist/comparison-XXAP4S4J.js +0 -36
  94. package/dist/editorial-timeline-VUQC6KPA.js +0 -42
  95. package/dist/key-figure-4TJK7HLT.js +0 -29
  96. package/dist/kit-DrRpdn0p.d.ts +0 -79
  97. package/dist/mobile-message-FIHAO6XC.js +0 -49
  98. package/dist/preload-media-LJXWKTWG.js +0 -57
  99. package/dist/quote-GRPIYKQJ.js +0 -32
  100. package/dist/system-prompt-AG26KJAA.js +0 -12
  101. package/dist/template-catalog.d.ts +0 -400
  102. package/dist/template-catalog.js +0 -6
  103. package/dist/templates.d.ts +0 -25
  104. package/dist/templates.js +0 -102
  105. package/dist/validate-4OUD2CLX.js +0 -10
  106. package/docs/concepts.md +0 -107
  107. package/docs/custom-templates.md +0 -346
  108. package/docs/immersive-interface.md +0 -86
  109. package/docs/motion-and-effects.md +0 -109
  110. package/docs/reference/design-system.html +0 -125
  111. package/docs/responsive-orientation.md +0 -37
  112. package/docs/streaming-protocol.md +0 -15
  113. package/examples/custom-template/README.md +0 -19
  114. package/examples/custom-template/minimal-text.tsx +0 -82
  115. package/examples/custom-template/structured-data.tsx +0 -104
  116. package/registry/items/backgrounds.json +0 -70
  117. package/registry/items/chapterTitle.json +0 -80
  118. package/registry/items/cinemaMedia.json +0 -136
  119. package/registry/items/comparison.json +0 -168
  120. package/registry/items/editorialTimeline.json +0 -172
  121. package/registry/items/keyFigure.json +0 -162
  122. package/registry/items/mobileMessage.json +0 -153
  123. package/registry/items/motion.json +0 -45
  124. package/registry/items/quote.json +0 -161
  125. package/registry/items/template-context.json +0 -31
  126. package/registry/items/theme.json +0 -47
  127. 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
- `@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.
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 chat route with Vitest
7
+ ## Test the default route
8
8
 
9
- Pass `createMockVideoPlanner()` to the same `createVideoChatHandler` used by the
10
- application, using an explicit template registry for the structured composition
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 { describe, expect, it } from "vitest";
18
- import { createVideoChatHandler, createServerTemplateRegistry } from "@vanillaskyai/video/server";
19
- import { createMockVideoPlanner } from "@vanillaskyai/video/test";
20
-
21
- describe("POST /api/video-chat", () => {
22
- it("returns a completed video answer", async () => {
23
- const handle = createVideoChatHandler({
24
- authorize: "none", // Only acceptable because this handler stays in process.
25
- heartbeatMs: false,
26
- templates: createServerTemplateRegistry({ templates: [] }),
27
- streamText: createMockVideoPlanner(),
28
- generateText: async ({ task }) => task === "suggestions"
29
- ? JSON.stringify({ suggestions: [] })
30
- : "A deterministic narration line.",
31
- });
32
-
33
- const response = await handle(new Request(
34
- "https://app.test/api/video-chat?action=response",
35
- {
36
- method: "POST",
37
- headers: { "content-type": "application/json" },
38
- body: JSON.stringify({
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 request `?action=capabilities` and `?action=welcome` in route tests. Assert
57
- that optional modes and controls appear only when their callbacks are present.
58
- For browser coverage, render `<VideoChat />`, submit through the visible prompt,
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
- `simulateVideoStream(parts, options?)` runs the composition and validation
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 each contain `{ input, parts }`. Public
90
- fixture values are deeply frozen and every helper clones parts before a run.
91
-
92
- ## Delays, aborts, and timeouts
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
- | Scenario | Expected terminal behavior |
136
- | --- | --- |
137
- | `success` | Completed portrait fixture |
138
- | `delayed` | Success after a 25 ms fake-timer-safe delay |
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
- Use `parts` for custom structural plan parts and `delayMs` to delay each
147
- provider chunk. Keep provider selection, credentials, retries, and real-model
148
- acceptance outside deterministic CI.
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
- [Documentation home](../README.md)
83
+ [Documentation home](../README.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanillaskyai/video",
3
- "version": "0.10.23",
3
+ "version": "0.11.0",
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/speech.ts",
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: run `npx vanillasky providers add video` first.
9
- # Generated video and server-side speech transcription.
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
- # Optional: stock video and photography for templates and suggestion cards.
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. The starter ships with tested defaults.
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 one required key to `.env.local`:
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
- Each command installs its provider package and connects the adapter through
32
- `providers.ts`. Run both commands to enable both upgrades.
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: add `FAL_KEY` for generated video and server transcription.
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. Keep `generatedClipDurationSec` aligned with
61
- the duration actually requested by the video adapter (five seconds by default).
62
- The planner writes natural short beats; measured speech controls scene timing.
63
- Silent footage loops through any remaining narration. Cancellation stops pending
64
- work and playback, and failed media becomes the authored chapter.
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`, or `FAL_VIDEO_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
@@ -9,7 +9,7 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
12
- "@vanillaskyai/video": "0.10.23",
12
+ "@vanillaskyai/video": "0.11.0",
13
13
  "react": "^19.2.8",
14
14
  "react-dom": "^19.2.8",
15
15
  "@ai-sdk/anthropic": "^3.0.0",
@@ -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
+ };