@vanillaskyai/video 0.7.1 → 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.
- package/CHANGELOG.md +59 -0
- package/PUBLIC-API.md +83 -79
- package/README.md +16 -15
- package/dist/{builtin-server-KB7FKF6A.js → builtin-server-CUYPIDE7.js} +2 -2
- package/dist/check-runtime.js +1 -1
- package/dist/{chunk-2PYO6VAC.js → chunk-2DOLF4QN.js} +1 -43
- package/dist/{chunk-GAIOHGNR.js → chunk-2OKB27TG.js} +42 -45
- package/dist/{chunk-K746NUIP.js → chunk-2USVB3CY.js} +80 -29
- package/dist/{chunk-RE4IMWJR.js → chunk-3VRMKFM7.js} +87 -14
- package/dist/{chunk-JNN3EYVP.js → chunk-4UNKZEAT.js} +46 -46
- package/dist/{chunk-2XT4MZ76.js → chunk-APQKY6P3.js} +36 -3
- package/dist/{chunk-LLPMVDJZ.js → chunk-L7QGGKJ2.js} +69 -38
- package/dist/{chunk-RGF452LL.js → chunk-OWBT45EN.js} +1 -1
- package/dist/{chunk-LKBZX7GV.js → chunk-PP3MCPN4.js} +4 -4
- package/dist/chunk-ZD2VTUYR.js +28 -0
- package/dist/cli.js +131 -24
- package/dist/{compose-video-PD6LKKRK.js → compose-video-2NFH4DZY.js} +3 -3
- package/dist/{events-B6qS1Lsb.d.ts → events-BQ6z264n.d.ts} +1 -1
- package/dist/index.d.ts +5 -67
- package/dist/index.js +2 -4
- package/dist/{kit-8g46H2RZ.d.ts → kit-tQnCPMOM.d.ts} +1 -1
- package/dist/react.d.ts +33 -53
- package/dist/react.js +642 -536
- package/dist/server.d.ts +11 -13
- package/dist/server.js +170 -81
- package/dist/template-catalog.js +1 -1
- package/dist/templates.d.ts +3 -3
- package/dist/test.d.ts +2 -2
- package/dist/test.js +3 -3
- package/dist/{text-stream-ROTNGSD4.js → text-stream-54LXABJW.js} +1 -1
- package/dist/{types-2wHBqtg8.d.ts → types-CEh9eUFu.d.ts} +1 -1
- package/dist/{types-DrABlRa7.d.ts → types-DWFO6qca.d.ts} +1 -1
- package/docs/agent-integration.md +9 -3
- package/docs/architecture.md +8 -10
- package/docs/concepts.md +16 -40
- package/docs/custom-templates.md +27 -23
- package/docs/customization.md +12 -49
- package/docs/errors.md +58 -68
- package/docs/getting-started.md +21 -14
- package/docs/media-and-audio.md +2 -2
- package/docs/performance.md +69 -0
- package/docs/persistence.md +12 -27
- package/docs/production.md +2 -2
- package/docs/provider-integration.md +11 -5
- package/docs/reference/provider-adapters.md +37 -89
- package/docs/security.md +1 -1
- package/docs/streaming-protocol.md +2 -2
- package/examples/custom-template/README.md +6 -6
- package/package.json +6 -7
- package/registry/items/barChart.json +2 -2
- package/registry/items/bigNumber.json +2 -2
- package/registry/items/brandMessage.json +2 -2
- package/registry/items/cardList.json +2 -2
- package/registry/items/codeEditor.json +2 -2
- package/registry/items/ctaLogo.json +2 -2
- package/registry/items/ctaMedia.json +2 -2
- package/registry/items/incomingCall.json +2 -2
- package/registry/items/media.json +2 -2
- package/registry/items/milestone.json +2 -2
- package/registry/items/notification.json +2 -2
- package/registry/items/phoneMockup.json +2 -2
- package/registry/items/problemSolution.json +2 -2
- package/registry/items/progressRing.json +2 -2
- package/registry/items/promptInput.json +2 -2
- package/registry/items/reaction.json +2 -2
- package/registry/items/reviewStack.json +2 -2
- package/registry/items/steps.json +2 -2
- package/registry/items/terminal.json +2 -2
- package/registry/items/testimonial.json +2 -2
- package/registry/items/tripleStats.json +2 -2
- package/registry/items/tweet.json +2 -2
- package/registry/items/webMockup.json +2 -2
- package/starters/video-chat/.env.example +4 -2
- package/starters/video-chat/README.md +17 -5
- package/starters/video-chat/package.json +2 -4
- package/starters/video-chat/providers/speech.ts +20 -0
- package/starters/video-chat/providers/video.ts +66 -0
- package/starters/video-chat/providers.ts +3 -0
- package/starters/video-chat/server.ts +4 -80
- package/starters/video-chat/stock.ts +114 -99
- package/dist/chunk-SKRGRKHY.js +0 -142
- package/dist/state-DZcKuS32.d.ts +0 -3
- package/docs/branding-and-personalization.md +0 -87
- 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
|
|
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.
|
|
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
|
|
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.
|
|
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": "
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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:
|
|
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:
|
|
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
|
-
|
|
23
|
+
starter installs only the text provider. Enable optional adapters when needed:
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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.
|
|
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
|
+
};
|
|
@@ -1,57 +1,11 @@
|
|
|
1
1
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
2
|
-
import {
|
|
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: [
|