@vanillaskyai/video 0.7.0 → 0.8.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 (84) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/PUBLIC-API.md +83 -79
  3. package/README.md +16 -15
  4. package/dist/{builtin-server-KB7FKF6A.js → builtin-server-CUYPIDE7.js} +2 -2
  5. package/dist/check-runtime.js +1 -1
  6. package/dist/{chunk-2PYO6VAC.js → chunk-2DOLF4QN.js} +1 -43
  7. package/dist/{chunk-GAIOHGNR.js → chunk-2OKB27TG.js} +42 -45
  8. package/dist/{chunk-K746NUIP.js → chunk-2USVB3CY.js} +80 -29
  9. package/dist/{chunk-RE4IMWJR.js → chunk-3VRMKFM7.js} +87 -14
  10. package/dist/{chunk-JNN3EYVP.js → chunk-4UNKZEAT.js} +46 -46
  11. package/dist/{chunk-2XT4MZ76.js → chunk-APQKY6P3.js} +36 -3
  12. package/dist/{chunk-RXHW4EP4.js → chunk-L7QGGKJ2.js} +71 -40
  13. package/dist/{chunk-RGF452LL.js → chunk-OWBT45EN.js} +1 -1
  14. package/dist/{chunk-LKBZX7GV.js → chunk-PP3MCPN4.js} +4 -4
  15. package/dist/chunk-ZD2VTUYR.js +28 -0
  16. package/dist/cli.js +131 -24
  17. package/dist/{compose-video-PD6LKKRK.js → compose-video-2NFH4DZY.js} +3 -3
  18. package/dist/{events-B6qS1Lsb.d.ts → events-BQ6z264n.d.ts} +1 -1
  19. package/dist/index.d.ts +5 -67
  20. package/dist/index.js +2 -4
  21. package/dist/{kit-8g46H2RZ.d.ts → kit-tQnCPMOM.d.ts} +1 -1
  22. package/dist/react.d.ts +33 -53
  23. package/dist/react.js +642 -536
  24. package/dist/server.d.ts +11 -13
  25. package/dist/server.js +170 -81
  26. package/dist/template-catalog.js +1 -1
  27. package/dist/templates.d.ts +3 -3
  28. package/dist/test.d.ts +2 -2
  29. package/dist/test.js +3 -3
  30. package/dist/{text-stream-FW4BLBAL.js → text-stream-54LXABJW.js} +1 -1
  31. package/dist/{types-2wHBqtg8.d.ts → types-CEh9eUFu.d.ts} +1 -1
  32. package/dist/{types-DrABlRa7.d.ts → types-DWFO6qca.d.ts} +1 -1
  33. package/docs/agent-integration.md +9 -3
  34. package/docs/architecture.md +8 -10
  35. package/docs/concepts.md +16 -40
  36. package/docs/custom-templates.md +27 -23
  37. package/docs/customization.md +12 -49
  38. package/docs/errors.md +58 -68
  39. package/docs/getting-started.md +21 -14
  40. package/docs/media-and-audio.md +2 -2
  41. package/docs/performance.md +69 -0
  42. package/docs/persistence.md +12 -27
  43. package/docs/production.md +2 -2
  44. package/docs/provider-integration.md +11 -5
  45. package/docs/reference/provider-adapters.md +37 -89
  46. package/docs/security.md +1 -1
  47. package/docs/streaming-protocol.md +2 -2
  48. package/examples/custom-template/README.md +6 -6
  49. package/package.json +6 -7
  50. package/registry/items/barChart.json +2 -2
  51. package/registry/items/bigNumber.json +2 -2
  52. package/registry/items/brandMessage.json +2 -2
  53. package/registry/items/cardList.json +2 -2
  54. package/registry/items/codeEditor.json +2 -2
  55. package/registry/items/ctaLogo.json +2 -2
  56. package/registry/items/ctaMedia.json +2 -2
  57. package/registry/items/incomingCall.json +2 -2
  58. package/registry/items/media.json +2 -2
  59. package/registry/items/milestone.json +2 -2
  60. package/registry/items/notification.json +2 -2
  61. package/registry/items/phoneMockup.json +2 -2
  62. package/registry/items/problemSolution.json +2 -2
  63. package/registry/items/progressRing.json +2 -2
  64. package/registry/items/promptInput.json +2 -2
  65. package/registry/items/reaction.json +2 -2
  66. package/registry/items/reviewStack.json +2 -2
  67. package/registry/items/steps.json +2 -2
  68. package/registry/items/terminal.json +2 -2
  69. package/registry/items/testimonial.json +2 -2
  70. package/registry/items/tripleStats.json +2 -2
  71. package/registry/items/tweet.json +2 -2
  72. package/registry/items/webMockup.json +2 -2
  73. package/starters/video-chat/.env.example +4 -2
  74. package/starters/video-chat/README.md +17 -5
  75. package/starters/video-chat/package.json +2 -4
  76. package/starters/video-chat/providers/speech.ts +20 -0
  77. package/starters/video-chat/providers/video.ts +66 -0
  78. package/starters/video-chat/providers.ts +3 -0
  79. package/starters/video-chat/server.ts +4 -80
  80. package/starters/video-chat/stock.ts +114 -99
  81. package/dist/chunk-SKRGRKHY.js +0 -142
  82. package/dist/state-DZcKuS32.d.ts +0 -3
  83. package/docs/branding-and-personalization.md +0 -87
  84. package/examples/custom-template/supplied-media.tsx +0 -92
@@ -123,7 +123,7 @@
123
123
  "mediaKeyword": {
124
124
  "type": "string",
125
125
  "title": "Background search keyword",
126
- "description": "2-4 word English term for Pexels stock-footage search (auto-fills mediaUrl).",
126
+ "description": "2-4 word English term for stock-media search (auto-fills mediaUrl).",
127
127
  "format": "stock-media-keyword",
128
128
  "default": ""
129
129
  },
@@ -142,7 +142,7 @@
142
142
  "mediaPoster": {
143
143
  "type": "string",
144
144
  "title": "Background poster image",
145
- "description": "Still image URL shown while a video backdrop is decoding its first frame. Auto-filled from Pexels' thumbnail when fillPexelsUrls sets a video mediaUrl. Hides the gradient flash that would otherwise appear in the ~50–400ms gap between a <video> mounting and decoding its first frame.",
145
+ "description": "Still image URL shown while a video backdrop is decoding its first frame. A media resolver can supply a poster alongside a video mediaUrl. Hides the gradient flash that would otherwise appear in the ~50–400ms gap between a <video> mounting and decoding its first frame.",
146
146
  "format": "uri",
147
147
  "default": ""
148
148
  },
@@ -80,7 +80,7 @@
80
80
  "mediaKeyword": {
81
81
  "type": "string",
82
82
  "title": "Background search keyword",
83
- "description": "2-4 word English term for Pexels stock-footage search (auto-fills mediaUrl).",
83
+ "description": "2-4 word English term for stock-media search (auto-fills mediaUrl).",
84
84
  "format": "stock-media-keyword",
85
85
  "default": ""
86
86
  },
@@ -99,7 +99,7 @@
99
99
  "mediaPoster": {
100
100
  "type": "string",
101
101
  "title": "Background poster image",
102
- "description": "Still image URL shown while a video backdrop is decoding its first frame. Auto-filled from Pexels' thumbnail when fillPexelsUrls sets a video mediaUrl. Hides the gradient flash that would otherwise appear in the ~50–400ms gap between a <video> mounting and decoding its first frame.",
102
+ "description": "Still image URL shown while a video backdrop is decoding its first frame. A media resolver can supply a poster alongside a video mediaUrl. Hides the gradient flash that would otherwise appear in the ~50–400ms gap between a <video> mounting and decoding its first frame.",
103
103
  "format": "uri",
104
104
  "default": ""
105
105
  },
@@ -82,7 +82,7 @@
82
82
  "reactionTag": {
83
83
  "type": "string",
84
84
  "title": "Reaction tag",
85
- "description": "Pexels meme search intent. Pick from the punchline/problem copy first, then the overall video topic.",
85
+ "description": "Stock-media search intent. Pick from the punchline/problem copy first, then the overall video topic.",
86
86
  "enum": [
87
87
  "launch",
88
88
  "productLaunch",
@@ -123,7 +123,7 @@
123
123
  "mediaUrl": {
124
124
  "type": "string",
125
125
  "title": "Reaction clip",
126
- "description": "Resolved Pexels MP4 URL.",
126
+ "description": "Resolved stock-video URL.",
127
127
  "format": "uri",
128
128
  "default": ""
129
129
  },
@@ -149,7 +149,7 @@
149
149
  "mediaKeyword": {
150
150
  "type": "string",
151
151
  "title": "Background search keyword",
152
- "description": "2-4 word English term for Pexels stock-footage search (auto-fills mediaUrl).",
152
+ "description": "2-4 word English term for stock-media search (auto-fills mediaUrl).",
153
153
  "format": "stock-media-keyword",
154
154
  "default": ""
155
155
  },
@@ -168,7 +168,7 @@
168
168
  "mediaPoster": {
169
169
  "type": "string",
170
170
  "title": "Background poster image",
171
- "description": "Still image URL shown while a video backdrop is decoding its first frame. Auto-filled from Pexels' thumbnail when fillPexelsUrls sets a video mediaUrl. Hides the gradient flash that would otherwise appear in the ~50–400ms gap between a <video> mounting and decoding its first frame.",
171
+ "description": "Still image URL shown while a video backdrop is decoding its first frame. A media resolver can supply a poster alongside a video mediaUrl. Hides the gradient flash that would otherwise appear in the ~50–400ms gap between a <video> mounting and decoding its first frame.",
172
172
  "format": "uri",
173
173
  "default": ""
174
174
  },
@@ -135,7 +135,7 @@
135
135
  "mediaKeyword": {
136
136
  "type": "string",
137
137
  "title": "Background search keyword",
138
- "description": "2-4 word English term for Pexels stock-footage search (auto-fills mediaUrl).",
138
+ "description": "2-4 word English term for stock-media search (auto-fills mediaUrl).",
139
139
  "format": "stock-media-keyword",
140
140
  "default": ""
141
141
  },
@@ -154,7 +154,7 @@
154
154
  "mediaPoster": {
155
155
  "type": "string",
156
156
  "title": "Background poster image",
157
- "description": "Still image URL shown while a video backdrop is decoding its first frame. Auto-filled from Pexels' thumbnail when fillPexelsUrls sets a video mediaUrl. Hides the gradient flash that would otherwise appear in the ~50–400ms gap between a <video> mounting and decoding its first frame.",
157
+ "description": "Still image URL shown while a video backdrop is decoding its first frame. A media resolver can supply a poster alongside a video mediaUrl. Hides the gradient flash that would otherwise appear in the ~50–400ms gap between a <video> mounting and decoding its first frame.",
158
158
  "format": "uri",
159
159
  "default": ""
160
160
  },
@@ -137,7 +137,7 @@
137
137
  "mediaKeyword": {
138
138
  "type": "string",
139
139
  "title": "Background search keyword",
140
- "description": "2-4 word English term for Pexels stock-footage search (auto-fills mediaUrl).",
140
+ "description": "2-4 word English term for stock-media search (auto-fills mediaUrl).",
141
141
  "format": "stock-media-keyword",
142
142
  "default": ""
143
143
  },
@@ -156,7 +156,7 @@
156
156
  "mediaPoster": {
157
157
  "type": "string",
158
158
  "title": "Background poster image",
159
- "description": "Still image URL shown while a video backdrop is decoding its first frame. Auto-filled from Pexels' thumbnail when fillPexelsUrls sets a video mediaUrl. Hides the gradient flash that would otherwise appear in the ~50–400ms gap between a <video> mounting and decoding its first frame.",
159
+ "description": "Still image URL shown while a video backdrop is decoding its first frame. A media resolver can supply a poster alongside a video mediaUrl. Hides the gradient flash that would otherwise appear in the ~50–400ms gap between a <video> mounting and decoding its first frame.",
160
160
  "format": "uri",
161
161
  "default": ""
162
162
  },
@@ -93,7 +93,7 @@
93
93
  "mediaKeyword": {
94
94
  "type": "string",
95
95
  "title": "Background search keyword",
96
- "description": "2-4 word English term for Pexels stock-footage search (auto-fills mediaUrl).",
96
+ "description": "2-4 word English term for stock-media search (auto-fills mediaUrl).",
97
97
  "format": "stock-media-keyword",
98
98
  "default": ""
99
99
  },
@@ -112,7 +112,7 @@
112
112
  "mediaPoster": {
113
113
  "type": "string",
114
114
  "title": "Background poster image",
115
- "description": "Still image URL shown while a video backdrop is decoding its first frame. Auto-filled from Pexels' thumbnail when fillPexelsUrls sets a video mediaUrl. Hides the gradient flash that would otherwise appear in the ~50–400ms gap between a <video> mounting and decoding its first frame.",
115
+ "description": "Still image URL shown while a video backdrop is decoding its first frame. A media resolver can supply a poster alongside a video mediaUrl. Hides the gradient flash that would otherwise appear in the ~50–400ms gap between a <video> mounting and decoding its first frame.",
116
116
  "format": "uri",
117
117
  "default": ""
118
118
  },
@@ -146,7 +146,7 @@
146
146
  "mediaKeyword": {
147
147
  "type": "string",
148
148
  "title": "Background search keyword",
149
- "description": "2-4 word English term for Pexels stock-footage search (auto-fills mediaUrl).",
149
+ "description": "2-4 word English term for stock-media search (auto-fills mediaUrl).",
150
150
  "format": "stock-media-keyword",
151
151
  "default": ""
152
152
  },
@@ -165,7 +165,7 @@
165
165
  "mediaPoster": {
166
166
  "type": "string",
167
167
  "title": "Background poster image",
168
- "description": "Still image URL shown while a video backdrop is decoding its first frame. Auto-filled from Pexels' thumbnail when fillPexelsUrls sets a video mediaUrl. Hides the gradient flash that would otherwise appear in the ~50–400ms gap between a <video> mounting and decoding its first frame.",
168
+ "description": "Still image URL shown while a video backdrop is decoding its first frame. A media resolver can supply a poster alongside a video mediaUrl. Hides the gradient flash that would otherwise appear in the ~50–400ms gap between a <video> mounting and decoding its first frame.",
169
169
  "format": "uri",
170
170
  "default": ""
171
171
  },
@@ -129,7 +129,7 @@
129
129
  "mediaKeyword": {
130
130
  "type": "string",
131
131
  "title": "Background search keyword",
132
- "description": "2-4 word English term for Pexels stock-footage search (auto-fills mediaUrl).",
132
+ "description": "2-4 word English term for stock-media search (auto-fills mediaUrl).",
133
133
  "format": "stock-media-keyword",
134
134
  "default": ""
135
135
  },
@@ -148,7 +148,7 @@
148
148
  "mediaPoster": {
149
149
  "type": "string",
150
150
  "title": "Background poster image",
151
- "description": "Still image URL shown while a video backdrop is decoding its first frame. Auto-filled from Pexels' thumbnail when fillPexelsUrls sets a video mediaUrl. Hides the gradient flash that would otherwise appear in the ~50–400ms gap between a <video> mounting and decoding its first frame.",
151
+ "description": "Still image URL shown while a video backdrop is decoding its first frame. A media resolver can supply a poster alongside a video mediaUrl. Hides the gradient flash that would otherwise appear in the ~50–400ms gap between a <video> mounting and decoding its first frame.",
152
152
  "format": "uri",
153
153
  "default": ""
154
154
  },
@@ -204,7 +204,7 @@
204
204
  "mediaKeyword": {
205
205
  "type": "string",
206
206
  "title": "Background search keyword",
207
- "description": "2-4 word English term for Pexels stock-footage search (auto-fills mediaUrl).",
207
+ "description": "2-4 word English term for stock-media search (auto-fills mediaUrl).",
208
208
  "default": ""
209
209
  },
210
210
  "mediaType": {
@@ -222,7 +222,7 @@
222
222
  "mediaPoster": {
223
223
  "type": "string",
224
224
  "title": "Background poster image",
225
- "description": "Still image URL shown while a video backdrop is decoding its first frame. Auto-filled from Pexels' thumbnail when fillPexelsUrls sets a video mediaUrl. Hides the gradient flash that would otherwise appear in the ~50–400ms gap between a <video> mounting and decoding its first frame.",
225
+ "description": "Still image URL shown while a video backdrop is decoding its first frame. A media resolver can supply a poster alongside a video mediaUrl. Hides the gradient flash that would otherwise appear in the ~50–400ms gap between a <video> mounting and decoding its first frame.",
226
226
  "format": "uri",
227
227
  "default": ""
228
228
  },
@@ -1,10 +1,12 @@
1
1
  # Required: plans the response, including its opening and narration, and suggestions.
2
2
  ANTHROPIC_API_KEY=
3
3
 
4
- # Optional: generated speech. The browser voice is used when this is empty.
4
+ # Optional: run `npx vanillasky providers add speech` first.
5
+ # Generated speech; browser voice remains the fallback.
5
6
  XAI_API_KEY=
6
7
 
7
- # Optional: generated video and server-side speech transcription.
8
+ # Optional: run `npx vanillasky providers add video` first.
9
+ # Generated video and server-side speech transcription.
8
10
  FAL_KEY=
9
11
 
10
12
  # Optional: stock video and photography for templates and suggestion cards.
@@ -20,11 +20,23 @@ ANTHROPIC_API_KEY=...
20
20
  ```
21
21
 
22
22
  That is enough for rendered templates and the browser's built-in voice. The
23
- optional keys progressively improve the same experience:
23
+ starter installs only the text provider. Enable optional adapters when needed:
24
24
 
25
- - `XAI_API_KEY` replaces the browser voice with generated speech.
26
- - `FAL_KEY` enables generated-video modes and server transcription.
27
- - `PEXELS_API_KEY` adds stock footage to rendered responses and suggestion cards.
25
+ ```bash
26
+ npx vanillasky providers add speech
27
+ npx vanillasky providers add video
28
+ ```
29
+
30
+ Each command installs its provider package and connects the adapter through
31
+ `providers.ts`. Run both commands to enable both upgrades.
32
+
33
+ - Speech: add `XAI_API_KEY` to replace browser voice with generated speech.
34
+ - Video: add `FAL_KEY` for generated video and server transcription.
35
+ - Stock: add `PEXELS_API_KEY` for footage and suggestion images; no installation
36
+ is needed.
37
+
38
+ The adapters live in `providers/` and remain application-owned. Running an
39
+ upgrade again can finish an interrupted dependency installation.
28
40
 
29
41
  Restart the development server after changing keys. Open
30
42
  <http://localhost:5173> and prompt anything.
@@ -42,7 +54,7 @@ Restart the development server after changing keys. Open
42
54
  4. The opening footage loops or holds until the first planned scene and its
43
55
  voice are ready, then the response timeline takes over without a blank gap.
44
56
  5. The SDK holds every planned scene for its measured or estimated speech time
45
- and prepares four possible next prompts while playback continues.
57
+ and loads possible next prompts independently after the response completes.
46
58
 
47
59
  The text model is the only required provider. Generated speech, transcription,
48
60
  stock media, and generated video are independent upgrades and never prevent a
@@ -9,13 +9,11 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
12
- "@vanillaskyai/video": "0.7.0",
12
+ "@vanillaskyai/video": "0.8.0",
13
13
  "react": "^19.2.8",
14
14
  "react-dom": "^19.2.8",
15
15
  "@ai-sdk/anthropic": "^3.0.0",
16
- "ai": "^7.0.77",
17
- "@ai-sdk/xai": "^4.0.52",
18
- "@fal-ai/client": "1.10.1"
16
+ "ai": "^7.0.77"
19
17
  },
20
18
  "devDependencies": {
21
19
  "@types/react": "^19.2.18",
@@ -0,0 +1,20 @@
1
+ import { createXai } from "@ai-sdk/xai";
2
+ import { experimental_generateSpeech as synthesizeSpeech } from "ai";
3
+ import type { VideoChatHandlerOptions } from "@vanillaskyai/video/server";
4
+
5
+ /** Optional generated speech; the SDK supplies the browser-voice fallback. */
6
+ export const speechProvider: Pick<VideoChatHandlerOptions, "generateSpeech"> = {
7
+ generateSpeech: process.env.XAI_API_KEY
8
+ ? async ({ text, signal }) => {
9
+ const speech = await synthesizeSpeech({
10
+ model: createXai({ apiKey: process.env.XAI_API_KEY }).speech(),
11
+ text,
12
+ abortSignal: signal,
13
+ });
14
+ return {
15
+ audio: speech.audio.uint8Array,
16
+ mediaType: speech.audio.mediaType || "audio/mpeg",
17
+ };
18
+ }
19
+ : undefined,
20
+ };
@@ -0,0 +1,66 @@
1
+ import { fal } from "@fal-ai/client";
2
+ import type { VideoChatHandlerOptions } from "@vanillaskyai/video/server";
3
+
4
+ const VIDEO_MODEL = process.env.FAL_VIDEO_MODEL ?? "minimax/h3-max/text-to-video";
5
+
6
+ async function filmScene(
7
+ subject: string,
8
+ generatedLook: string | undefined,
9
+ orientation: "portrait" | "landscape",
10
+ signal: AbortSignal,
11
+ ) {
12
+ fal.config({ credentials: process.env.FAL_KEY });
13
+ const started = Date.now();
14
+ console.log(`[video-chat] filming: ${subject.slice(0, 70)}`);
15
+ try {
16
+ const result = await fal.subscribe(VIDEO_MODEL, {
17
+ input: {
18
+ prompt: [
19
+ `Locked-off shot, ${orientation === "portrait" ? "9:16 vertical" : "16:9"}. ${subject}.`,
20
+ "One slow continuous camera move. Physically plausible motion.",
21
+ "No on-screen text, captions, subtitles, watermarks or logos.",
22
+ "Diegetic sound only. No music, no voiceover.",
23
+ generatedLook,
24
+ ].filter(Boolean).join("\n\n"),
25
+ duration: 5,
26
+ resolution: "480P",
27
+ aspect_ratio: orientation === "portrait" ? "9:16" : "16:9",
28
+ },
29
+ abortSignal: signal,
30
+ });
31
+ const url = result?.data?.video?.url;
32
+ if (typeof url !== "string") throw new Error("the video provider returned no url");
33
+ console.log(`[video-chat] filmed in ${Date.now() - started}ms`);
34
+ return url;
35
+ } catch (cause) {
36
+ const status = (cause as { status?: unknown })?.status;
37
+ console.error(
38
+ `[video-chat] filming failed after ${Date.now() - started}ms${typeof status === "number" ? ` (status ${status})` : ""}`,
39
+ );
40
+ throw cause;
41
+ }
42
+ }
43
+
44
+ /** Optional generated video and transcription; credentials remain server-only. */
45
+ export const videoProvider: Pick<VideoChatHandlerOptions, "generateVideo" | "transcribe"> = {
46
+ generateVideo: process.env.FAL_KEY
47
+ ? async (query, { generatedLook, orientation, signal }) => ({
48
+ url: await filmScene(query, generatedLook, orientation, signal),
49
+ type: "video" as const,
50
+ })
51
+ : undefined,
52
+ transcribe: process.env.FAL_KEY
53
+ ? async ({ audio, mediaType, signal }) => {
54
+ signal.throwIfAborted();
55
+ fal.config({ credentials: process.env.FAL_KEY });
56
+ const bytes = Uint8Array.from(audio);
57
+ const url = await fal.storage.upload(new Blob([bytes.buffer], { type: mediaType }));
58
+ signal.throwIfAborted();
59
+ const result = await fal.subscribe(process.env.FAL_TRANSCRIBE_MODEL ?? "fal-ai/whisper", {
60
+ input: { audio_url: url, task: "transcribe" },
61
+ abortSignal: AbortSignal.any([signal, AbortSignal.timeout(60_000)]),
62
+ });
63
+ return String((result?.data as { text?: unknown })?.text ?? "");
64
+ }
65
+ : undefined,
66
+ };
@@ -0,0 +1,3 @@
1
+ import type { VideoChatHandlerOptions } from "@vanillaskyai/video/server";
2
+
3
+ export const providers: Pick<VideoChatHandlerOptions, "generateSpeech" | "generateVideo" | "transcribe"> = {};
@@ -1,57 +1,11 @@
1
1
  import { anthropic } from "@ai-sdk/anthropic";
2
- import { fal } from "@fal-ai/client";
3
- import { createXai } from "@ai-sdk/xai";
4
- import {
5
- experimental_generateSpeech as synthesizeSpeech,
6
- generateText,
7
- streamText,
8
- } from "ai";
2
+ import { generateText, streamText } from "ai";
9
3
  import { createVideoChatHandler } from "@vanillaskyai/video/server";
10
4
  import { findStockFootage } from "./stock";
5
+ import { providers } from "./providers";
11
6
 
12
7
  const PLANNER_MODEL = process.env.ANTHROPIC_PLANNER_MODEL ?? "claude-sonnet-5";
13
8
  const NARRATION_MODEL = process.env.ANTHROPIC_NARRATION_MODEL ?? "claude-haiku-4-5";
14
- const VIDEO_MODEL = process.env.FAL_VIDEO_MODEL ?? "minimax/h3-max/text-to-video";
15
- const SHOT_DEADLINE_MS = 180_000;
16
-
17
- async function filmScene(
18
- subject: string,
19
- generatedLook: string | undefined,
20
- orientation: "portrait" | "landscape",
21
- signal: AbortSignal,
22
- ) {
23
- fal.config({ credentials: process.env.FAL_KEY });
24
- const started = Date.now();
25
- console.log(`[video-chat] filming: ${subject.slice(0, 70)}`);
26
- try {
27
- const result = await fal.subscribe(VIDEO_MODEL, {
28
- input: {
29
- prompt: [
30
- `Locked-off shot, ${orientation === "portrait" ? "9:16 vertical" : "16:9"}. ${subject}.`,
31
- "One slow continuous camera move. Physically plausible motion.",
32
- "No on-screen text, captions, subtitles, watermarks or logos.",
33
- "Diegetic sound only. No music, no voiceover.",
34
- generatedLook,
35
- ].filter(Boolean).join("\n\n"),
36
- duration: 5,
37
- resolution: "480P",
38
- aspect_ratio: orientation === "portrait" ? "9:16" : "16:9",
39
- },
40
- abortSignal: AbortSignal.any([signal, AbortSignal.timeout(SHOT_DEADLINE_MS)]),
41
- });
42
- const url = result?.data?.video?.url;
43
- if (typeof url !== "string") throw new Error("the video provider returned no url");
44
- console.log(`[video-chat] filmed in ${Date.now() - started}ms`);
45
- return url;
46
- } catch (cause) {
47
- const status = (cause as { status?: unknown })?.status;
48
- console.error(
49
- `[video-chat] filming failed after ${Date.now() - started}ms${typeof status === "number" ? ` (status ${status})` : ""}`,
50
- );
51
- throw cause;
52
- }
53
- }
54
-
55
9
  /**
56
10
  * The application chooses providers and keeps their credentials here. The SDK
57
11
  * owns the video-chat protocol, prompts, capability negotiation, spend limits,
@@ -87,40 +41,10 @@ export const handleVideoChat = createVideoChatHandler({
87
41
  });
88
42
  return text;
89
43
  },
90
- generateSpeech: process.env.XAI_API_KEY
91
- ? async ({ text, signal }) => {
92
- const speech = await synthesizeSpeech({
93
- model: createXai({ apiKey: process.env.XAI_API_KEY }).speech(),
94
- text,
95
- abortSignal: signal,
96
- });
97
- return {
98
- audio: speech.audio.uint8Array,
99
- mediaType: speech.audio.mediaType || "audio/mpeg",
100
- };
101
- }
102
- : undefined,
103
- transcribe: process.env.FAL_KEY
104
- ? async ({ audio, mediaType, signal }) => {
105
- fal.config({ credentials: process.env.FAL_KEY });
106
- const bytes = Uint8Array.from(audio);
107
- const url = await fal.storage.upload(new Blob([bytes.buffer], { type: mediaType }));
108
- const result = await fal.subscribe(process.env.FAL_TRANSCRIBE_MODEL ?? "fal-ai/whisper", {
109
- input: { audio_url: url, task: "transcribe" },
110
- abortSignal: AbortSignal.any([signal, AbortSignal.timeout(60_000)]),
111
- });
112
- return String((result?.data as { text?: unknown })?.text ?? "");
113
- }
114
- : undefined,
115
44
  searchMedia: process.env.PEXELS_API_KEY
116
- ? (query, { orientation, signal }) => findStockFootage(query, orientation, signal)
117
- : undefined,
118
- generateVideo: process.env.FAL_KEY
119
- ? async (query, { generatedLook, orientation, signal }) => ({
120
- url: await filmScene(query, generatedLook, orientation, signal),
121
- type: "video" as const,
122
- })
45
+ ? (query, { orientation, signal, fallbackQuery }) => findStockFootage(query, orientation, signal, fallbackQuery)
123
46
  : undefined,
47
+ ...providers,
124
48
  welcome: {
125
49
  heroQuery: process.env.VIDEO_CHAT_WELCOME_KEYWORD ?? "underwater ocean sunlight",
126
50
  prompts: [