@vanillaskyai/video 0.1.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 +45 -0
- package/LICENSE +201 -0
- package/PUBLIC-API.md +276 -0
- package/README.md +152 -0
- package/SECURITY.md +22 -0
- package/SUPPORT.md +28 -0
- package/bin/vanillasky.js +3 -0
- package/dist/bg-confetti-WNQXS7ZS.js +89 -0
- package/dist/bg-emoji-ETI47GLS.js +150 -0
- package/dist/bg-media-SVUZAMGX.js +99 -0
- package/dist/brand-message-F3RD4P3P.js +253 -0
- package/dist/builtin-server-F7YXRVCH.js +8 -0
- package/dist/catalog-types-BIhSpOWK.d.ts +74 -0
- package/dist/chart-bar-CNSC7BNK.js +304 -0
- package/dist/chart-counter-MCRHKN77.js +299 -0
- package/dist/chart-progress-ring-RMI4A7ZK.js +296 -0
- package/dist/check-runtime.d.ts +15 -0
- package/dist/check-runtime.js +102 -0
- package/dist/chunk-2E6T633S.js +27 -0
- package/dist/chunk-352ZZCET.js +83 -0
- package/dist/chunk-73NTSFFI.js +81 -0
- package/dist/chunk-A2X45FET.js +46 -0
- package/dist/chunk-ABPKM4GK.js +187 -0
- package/dist/chunk-ABZACD4G.js +216 -0
- package/dist/chunk-BCRUUJ6A.js +756 -0
- package/dist/chunk-CHLGW22F.js +3551 -0
- package/dist/chunk-CJ7F2CE6.js +104 -0
- package/dist/chunk-CR7FE7BW.js +109 -0
- package/dist/chunk-E7CL7UPB.js +109 -0
- package/dist/chunk-EE6PK6XC.js +18 -0
- package/dist/chunk-ERY4S6QV.js +385 -0
- package/dist/chunk-EVG2ZYIY.js +125 -0
- package/dist/chunk-FNAEQ7QO.js +155 -0
- package/dist/chunk-FQ47WXAJ.js +38 -0
- package/dist/chunk-FSG2PYWG.js +336 -0
- package/dist/chunk-GHJEAP4O.js +504 -0
- package/dist/chunk-GRMUV7QY.js +262 -0
- package/dist/chunk-IFOW65Z6.js +205 -0
- package/dist/chunk-IXCUY3B3.js +786 -0
- package/dist/chunk-L5BTTCZV.js +97 -0
- package/dist/chunk-LMG3GHLL.js +107 -0
- package/dist/chunk-OOPIETLW.js +101 -0
- package/dist/chunk-P22HHFDK.js +43 -0
- package/dist/chunk-QAAO7OAY.js +19 -0
- package/dist/chunk-S7HFLYNN.js +35 -0
- package/dist/chunk-SPVTJH3F.js +24 -0
- package/dist/chunk-W2XJKG6C.js +3177 -0
- package/dist/chunk-XB7LH4J2.js +301 -0
- package/dist/chunk-XGRA2MUV.js +242 -0
- package/dist/chunk-XTOXFMLM.js +105 -0
- package/dist/chunk-YC5MFXA5.js +132 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1681 -0
- package/dist/compose-video-CN3VSWST.js +13 -0
- package/dist/cta-logo-SVA5GP2A.js +209 -0
- package/dist/cta-media-E7VLLK42.js +215 -0
- package/dist/events-_wfjbmEp.d.ts +81 -0
- package/dist/incoming-call-62MFZL3Z.js +320 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +14 -0
- package/dist/infographic-before-after-AXCDAWND.js +460 -0
- package/dist/infographic-feature-list-GW5GUE6O.js +266 -0
- package/dist/infographic-problem-solution-7NURJANA.js +340 -0
- package/dist/infographic-stat-row-J7IZR6ZN.js +241 -0
- package/dist/infographic-steps-VSZM6F5M.js +400 -0
- package/dist/kit-BMNcHqa9.d.ts +79 -0
- package/dist/prompt-input-LOUCRXCY.js +236 -0
- package/dist/react.d.ts +81 -0
- package/dist/react.js +835 -0
- package/dist/reaction-QG4CYZLQ.js +119 -0
- package/dist/server.d.ts +141 -0
- package/dist/server.js +498 -0
- package/dist/showcase-code-RUTFZR3H.js +351 -0
- package/dist/showcase-phone-6CP6XEU5.js +272 -0
- package/dist/showcase-terminal-MGAEQWPR.js +306 -0
- package/dist/showcase-web-Y4W2HVRJ.js +390 -0
- package/dist/social-conversation-75SD2637.js +695 -0
- package/dist/social-milestone-XI2QE5BO.js +292 -0
- package/dist/social-notification-2UOHQCUY.js +244 -0
- package/dist/social-review-stack-FDYZGZTJ.js +239 -0
- package/dist/social-testimonial-OQ7N52GJ.js +284 -0
- package/dist/social-tweet-5OLCNPPC.js +295 -0
- package/dist/state-DZcKuS32.d.ts +3 -0
- package/dist/system-prompt-A6UXNTPK.js +8 -0
- package/dist/template-catalog.d.ts +24 -0
- package/dist/template-catalog.js +6 -0
- package/dist/templates.d.ts +25 -0
- package/dist/templates.js +19 -0
- package/dist/test.d.ts +149 -0
- package/dist/test.js +272 -0
- package/dist/text-stream-6XHC7J47.js +10 -0
- package/dist/types-B-PZR6G_.d.ts +208 -0
- package/dist/validate-T7GBU2YF.js +8 -0
- package/docs/agent-integration.md +39 -0
- package/docs/architecture.md +84 -0
- package/docs/branding-and-personalization.md +87 -0
- package/docs/concepts.md +106 -0
- package/docs/custom-templates.md +329 -0
- package/docs/customization.md +141 -0
- package/docs/errors.md +70 -0
- package/docs/getting-started.md +111 -0
- package/docs/input-and-first-scene.md +41 -0
- package/docs/integrate-nextjs.md +79 -0
- package/docs/media-and-audio.md +69 -0
- package/docs/motion-and-effects.md +107 -0
- package/docs/persistence.md +95 -0
- package/docs/production.md +122 -0
- package/docs/prompt-and-input.md +185 -0
- package/docs/provider-integration.md +115 -0
- package/docs/reference/protocol.md +96 -0
- package/docs/reference/provider-adapters.md +158 -0
- package/docs/responsive-orientation.md +37 -0
- package/docs/security.md +38 -0
- package/docs/streaming-protocol.md +15 -0
- package/docs/testing.md +137 -0
- package/docs/use-cases.md +59 -0
- package/examples/custom-template/README.md +19 -0
- package/examples/custom-template/minimal-text.tsx +82 -0
- package/examples/custom-template/structured-data.tsx +104 -0
- package/examples/custom-template/supplied-media.tsx +92 -0
- package/examples/nextjs-quickstart/.env.example +2 -0
- package/examples/nextjs-quickstart/README.md +22 -0
- package/examples/nextjs-quickstart/next-env.d.ts +4 -0
- package/examples/nextjs-quickstart/package.json +24 -0
- package/examples/nextjs-quickstart/src/app/api/video/route.ts +22 -0
- package/examples/nextjs-quickstart/src/app/layout.tsx +5 -0
- package/examples/nextjs-quickstart/src/app/page.tsx +31 -0
- package/examples/nextjs-quickstart/tsconfig.json +26 -0
- package/package.json +167 -0
- package/registry/items/backgrounds.json +52 -0
- package/registry/items/barChart.json +237 -0
- package/registry/items/beforeAfter.json +180 -0
- package/registry/items/bigNumber.json +225 -0
- package/registry/items/brandMessage.json +158 -0
- package/registry/items/cardList.json +206 -0
- package/registry/items/chatMessenger.json +151 -0
- package/registry/items/chatWhatsapp.json +151 -0
- package/registry/items/codeEditor.json +184 -0
- package/registry/items/confetti.json +107 -0
- package/registry/items/ctaLogo.json +160 -0
- package/registry/items/ctaMedia.json +193 -0
- package/registry/items/emojiBurst.json +101 -0
- package/registry/items/incomingCall.json +170 -0
- package/registry/items/media.json +179 -0
- package/registry/items/milestone.json +205 -0
- package/registry/items/motion.json +45 -0
- package/registry/items/notification.json +182 -0
- package/registry/items/phoneMockup.json +254 -0
- package/registry/items/problemSolution.json +186 -0
- package/registry/items/progressRing.json +199 -0
- package/registry/items/promptInput.json +148 -0
- package/registry/items/reaction.json +170 -0
- package/registry/items/reviewStack.json +224 -0
- package/registry/items/steps.json +207 -0
- package/registry/items/template-context.json +31 -0
- package/registry/items/terminal.json +212 -0
- package/registry/items/testimonial.json +164 -0
- package/registry/items/theme.json +47 -0
- package/registry/items/tripleStats.json +228 -0
- package/registry/items/tweet.json +203 -0
- package/registry/items/typography.json +47 -0
- package/registry/items/webMockup.json +273 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[← Documentation home](../README.md) · [Previous: Input and first scene](input-and-first-scene.md)
|
|
2
|
+
|
|
3
|
+
# Responsive orientation
|
|
4
|
+
|
|
5
|
+
Orientation is a composition constraint, not a fixed pixel size.
|
|
6
|
+
|
|
7
|
+
- `portrait` uses a 9:16 design space for phones, stories, and feeds.
|
|
8
|
+
- `landscape` uses 16:9 for embedded product surfaces, presentations, and desktop playback.
|
|
9
|
+
|
|
10
|
+
Templates render on a canonical design canvas: 1080×1920 for portrait and
|
|
11
|
+
1920×1080 for landscape. `VideoPlayer` preserves that canvas and scales it
|
|
12
|
+
uniformly to its viewport. When the viewport has a different aspect ratio, the
|
|
13
|
+
canvas is centered with letterboxing instead of being stretched.
|
|
14
|
+
|
|
15
|
+
Template authors should still use relative layout, safe edges, clamped typography, and content-aware wrapping so both orientations and localized content remain readable.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<div className="video-shell">
|
|
19
|
+
<VideoPlayer {...video.playerProps} orientation="auto" />
|
|
20
|
+
</div>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```css
|
|
24
|
+
.video-shell { width: min(100%, 48rem); margin-inline: auto; }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`orientation="auto"` uses landscape above 520 CSS pixels and portrait at or
|
|
28
|
+
below 520, based on the player's container rather than the browser window. Set
|
|
29
|
+
`responsiveBreakpoint` to match an existing host breakpoint. Set
|
|
30
|
+
`orientation="portrait"` or `"landscape"` when a surface must remain fixed.
|
|
31
|
+
Omitting the prop preserves the orientation recorded in the response config.
|
|
32
|
+
|
|
33
|
+
This display override does not mutate the completed response, checksum, or export
|
|
34
|
+
orientation. It reflows the same trusted templates and scene variables without
|
|
35
|
+
another provider call. Test every template at narrow mobile widths, large embeds,
|
|
36
|
+
200% zoom, both orientations, reduced motion, and with long localized copy.
|
|
37
|
+
Meaning must remain available when motion is reduced.
|
package/docs/security.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[← Documentation home](../README.md) · [Previous: Streaming protocol](streaming-protocol.md) · [Next: Errors and recovery →](errors.md)
|
|
2
|
+
|
|
3
|
+
# Security
|
|
4
|
+
|
|
5
|
+
The SDK validates protocol shape; your application still owns identity, authorization, data policy, and infrastructure controls.
|
|
6
|
+
|
|
7
|
+
## Required server controls
|
|
8
|
+
|
|
9
|
+
- Authenticate the user and tenant before reading the prompt body.
|
|
10
|
+
- `createVideoHandler` requires an explicit `authorize` policy. The
|
|
11
|
+
`authorize: "none"` escape hatch is for intentionally non-public in-process
|
|
12
|
+
tests only; do not use it on a billable generation route.
|
|
13
|
+
- Allowlist browser origins; CORS is not authentication.
|
|
14
|
+
- Bound request bytes, media count, scene count, duration, tokens, concurrency, and cost.
|
|
15
|
+
- Keep provider keys, system prompts, tools, signed-URL credentials, and admin tokens server-side.
|
|
16
|
+
- Use the same generated template registry on the server and in React; the handler infers validation.
|
|
17
|
+
- Treat every project-owned template as trusted application build code and
|
|
18
|
+
review it before using the CLI. Normal `vanillasky list`,
|
|
19
|
+
`vanillasky describe`, `vanillasky add`, `vanillasky sync`, and
|
|
20
|
+
`vanillasky check` commands execute project template modules locally. This
|
|
21
|
+
includes `vanillasky add` previews with `--dry-run` or `--diff`, because the
|
|
22
|
+
CLI must derive the proposed browser and server registries. Resource and
|
|
23
|
+
environment boundaries reduce accidental damage but are not a portable
|
|
24
|
+
JavaScript sandbox.
|
|
25
|
+
- Use `--builtin` with `list` or `describe` when you need the packaged catalog
|
|
26
|
+
only. That view does not execute project template modules. Project template
|
|
27
|
+
execution requires macOS, Linux, or WSL because Windows cannot provide the
|
|
28
|
+
process-group cleanup guarantee used by these commands.
|
|
29
|
+
- Restrict media domains, types, dimensions, bytes, redirects, and fetch timeouts.
|
|
30
|
+
- Propagate cancellation and use timeouts for provider, media, persistence, and export work.
|
|
31
|
+
- Return safe typed errors while logging private causes only in protected observability.
|
|
32
|
+
|
|
33
|
+
Do not log raw source, personalization, authorization headers, provider deltas, or signed media URLs by default. Record request ID, tenant-safe metrics, model ID, timing, event counts, error codes, and token usage.
|
|
34
|
+
|
|
35
|
+
Treat final configs as customer data. Apply tenant isolation, retention,
|
|
36
|
+
encryption, and deletion policy to snapshots and event logs. Report suspected
|
|
37
|
+
SDK vulnerabilities through the repository's private process in
|
|
38
|
+
[SECURITY.md](../SECURITY.md).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[← Documentation home](../README.md) · [Previous: Core concepts](concepts.md) · [Next: Security →](security.md)
|
|
2
|
+
|
|
3
|
+
# Streaming protocol
|
|
4
|
+
|
|
5
|
+
`createVideoHandler` returns validated server-sent events and `useVideo` consumes
|
|
6
|
+
them. Normal applications do not parse SSE or reduce events themselves.
|
|
7
|
+
|
|
8
|
+
The internal protocol is deterministic and versioned. A run starts once, adds or
|
|
9
|
+
patches only complete validated scenes, and terminates exactly once as complete,
|
|
10
|
+
error, or aborted. Every event has a run ID and sequence. Completion carries the
|
|
11
|
+
authoritative snapshot and drift-detection checksum.
|
|
12
|
+
|
|
13
|
+
Persist `video.video` when replay, editing, or export is required. Played scenes
|
|
14
|
+
are immutable; patches may affect only future scenes. See the [protocol reference](reference/protocol.md)
|
|
15
|
+
when implementing infrastructure rather than an ordinary SDK integration.
|
package/docs/testing.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Test integrations without a model
|
|
2
|
+
|
|
3
|
+
`@vanillaskyai/video/test` provides deterministic provider streams and in-process
|
|
4
|
+
protocol events. It has no React or provider-SDK dependency, makes no network
|
|
5
|
+
request, and does not require a model key.
|
|
6
|
+
|
|
7
|
+
## Test a route handler with Vitest
|
|
8
|
+
|
|
9
|
+
Pass `createMockVideoPlanner()` directly to the same `createVideoHandler()`
|
|
10
|
+
used by the application. Call the handler with a standard `Request`; no live
|
|
11
|
+
HTTP server is involved.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { describe, expect, it } from "vitest";
|
|
15
|
+
import { createVideoHandler } from "@vanillaskyai/video/server";
|
|
16
|
+
import {
|
|
17
|
+
createMockVideoPlanner,
|
|
18
|
+
videoFixtures,
|
|
19
|
+
} from "@vanillaskyai/video/test";
|
|
20
|
+
|
|
21
|
+
describe("POST /api/video", () => {
|
|
22
|
+
it("returns a completed video stream", async () => {
|
|
23
|
+
const handle = createVideoHandler({
|
|
24
|
+
authorize: "none", // Explicit because this in-process test route is never public.
|
|
25
|
+
heartbeatMs: false,
|
|
26
|
+
streamText: createMockVideoPlanner(),
|
|
27
|
+
});
|
|
28
|
+
const response = await handle(new Request("https://app.test/api/video", {
|
|
29
|
+
method: "POST",
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
protocolVersion: "0.4",
|
|
32
|
+
requestId: "route-test",
|
|
33
|
+
input: videoFixtures.portrait.input,
|
|
34
|
+
}),
|
|
35
|
+
}));
|
|
36
|
+
const body = await response.text();
|
|
37
|
+
|
|
38
|
+
expect(response.status).toBe(200);
|
|
39
|
+
expect(body).toContain('"type":"response.complete"');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The mock emits provider-style newline-delimited JSON. The route still parses,
|
|
45
|
+
validates, paces, and converts those plan parts into SSE exactly as it does with
|
|
46
|
+
a live provider.
|
|
47
|
+
|
|
48
|
+
## Test protocol events in process
|
|
49
|
+
|
|
50
|
+
`simulateVideoStream(parts, options?)` runs the same composition and validation
|
|
51
|
+
pipeline without an HTTP boundary. It yields structural, discriminated event
|
|
52
|
+
objects. Default IDs are `test-request` and `test-run`; override `requestId` and
|
|
53
|
+
`runId` when a test needs different fixed values.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { expect, it } from "vitest";
|
|
57
|
+
import {
|
|
58
|
+
simulateVideoStream,
|
|
59
|
+
videoFixtures,
|
|
60
|
+
} from "@vanillaskyai/video/test";
|
|
61
|
+
|
|
62
|
+
it("keeps a truncated result playable", async () => {
|
|
63
|
+
const events = [];
|
|
64
|
+
for await (const event of simulateVideoStream(
|
|
65
|
+
videoFixtures.scenarios.truncated,
|
|
66
|
+
)) {
|
|
67
|
+
events.push(event);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
expect(events.at(-1)).toMatchObject({
|
|
71
|
+
type: "response.complete",
|
|
72
|
+
data: { finishReason: "length" },
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The portrait and landscape fixtures each contain `{ input, parts }`. All public
|
|
78
|
+
fixture values are deeply frozen, and each helper clones parts before a run so
|
|
79
|
+
one test cannot mutate another test's input.
|
|
80
|
+
|
|
81
|
+
## Delays, aborts, and timeouts
|
|
82
|
+
|
|
83
|
+
Delays and `timeoutMs` use ordinary timers and work with Vitest fake timers.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { expect, it, vi } from "vitest";
|
|
87
|
+
import {
|
|
88
|
+
simulateVideoStream,
|
|
89
|
+
videoFixtures,
|
|
90
|
+
} from "@vanillaskyai/video/test";
|
|
91
|
+
|
|
92
|
+
it("times out deterministically", async () => {
|
|
93
|
+
vi.useFakeTimers();
|
|
94
|
+
try {
|
|
95
|
+
const result = (async () => {
|
|
96
|
+
const events = [];
|
|
97
|
+
for await (const event of simulateVideoStream(
|
|
98
|
+
videoFixtures.scenarios.timeout,
|
|
99
|
+
{ timeoutMs: 50 },
|
|
100
|
+
)) events.push(event);
|
|
101
|
+
return events;
|
|
102
|
+
})();
|
|
103
|
+
|
|
104
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
105
|
+
await expect(result).resolves.toMatchObject([
|
|
106
|
+
{ type: "response.start" },
|
|
107
|
+
{ type: "scene.add" },
|
|
108
|
+
{ type: "response.abort", data: { reason: "Request timed out" } },
|
|
109
|
+
]);
|
|
110
|
+
} finally {
|
|
111
|
+
vi.useRealTimers();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
For a host cancellation, pass an `AbortController` signal and abort it after the
|
|
117
|
+
desired partial event. Timeouts are opt-in and local to the simulator; at the
|
|
118
|
+
route boundary the host remains responsible for aborting the request signal.
|
|
119
|
+
|
|
120
|
+
## Fixed scenarios
|
|
121
|
+
|
|
122
|
+
`createMockVideoPlanner({ scenario })` accepts:
|
|
123
|
+
|
|
124
|
+
| Scenario | Expected terminal behavior |
|
|
125
|
+
| --- | --- |
|
|
126
|
+
| `success` | Completed portrait fixture |
|
|
127
|
+
| `delayed` | Success after a 25 ms fake-timer-safe delay |
|
|
128
|
+
| `truncated` | Partial playable result completed with `length` |
|
|
129
|
+
| `invalidScene` | Invalid scene dropped with a recoverable diagnostic, then completion |
|
|
130
|
+
| `providerFailure` | Redacted terminal generation failure |
|
|
131
|
+
| `contentFilter` | Partial playable result completed with `content-filter` |
|
|
132
|
+
| `abort` | Waits for the supplied request signal to abort |
|
|
133
|
+
| `timeout` | Waits for the host signal or simulator `timeoutMs` |
|
|
134
|
+
|
|
135
|
+
Use `parts` in `MockVideoPlannerOptions` to supply custom structural plan parts,
|
|
136
|
+
and `delayMs` to delay every emitted provider chunk. Provider selection,
|
|
137
|
+
credentials, retries, and real-provider acceptance remain application-owned.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[← Documentation home](../README.md)
|
|
2
|
+
|
|
3
|
+
# Video response use cases
|
|
4
|
+
|
|
5
|
+
These four inputs show the primary product shape: AI answers and live
|
|
6
|
+
application context become embedded video responses. The application decides
|
|
7
|
+
which context matters and keeps it current. VanillaSky grounds, validates,
|
|
8
|
+
composes, and plays the visual response.
|
|
9
|
+
|
|
10
|
+
## News summary
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
video.generate({
|
|
14
|
+
input: `Headline: North Sea wind output reached a record on August 17.
|
|
15
|
+
Source summary: Grid operators reported 18.2 GW at 14:00 CET, 12% above the
|
|
16
|
+
previous record. Operators said lower demand helped reduce wholesale prices.`,
|
|
17
|
+
instructions: "Lead with the record, explain the comparison, then the impact.",
|
|
18
|
+
maxDurationSec: 24,
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Contextual help
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
video.generate({
|
|
26
|
+
input: `The user is configuring an alert. The current threshold is 80%.
|
|
27
|
+
Alerts evaluate every 15 minutes. Email and Slack are enabled. The Save button
|
|
28
|
+
applies the rule to 42 selected accounts.`,
|
|
29
|
+
instructions: "Explain what will happen if the user saves this configuration.",
|
|
30
|
+
opening: "Here is what this alert will do.",
|
|
31
|
+
maxDurationSec: 24,
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Personalized briefing
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
video.generate({
|
|
39
|
+
input: `The Acme account has 1,240 weekly active users, up 18% month over
|
|
40
|
+
month. Three renewals are due in September. Support response time improved from
|
|
41
|
+
4.2 hours to 2.8 hours.`,
|
|
42
|
+
personalization: { firstName: "Maya", accountName: "Acme" },
|
|
43
|
+
instructions: "Create a concise morning account briefing.",
|
|
44
|
+
maxDurationSec: 30,
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Dynamic personalized page content
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
video.generate({
|
|
52
|
+
input: `The page shows the Q2 onboarding funnel. 8,420 users started setup,
|
|
53
|
+
6,315 connected a data source, and 4,884 invited a teammate. The largest drop
|
|
54
|
+
is between connecting a data source and inviting a teammate.`,
|
|
55
|
+
instructions: "Respond to the visible funnel and focus on the largest drop.",
|
|
56
|
+
orientation: "landscape",
|
|
57
|
+
maxDurationSec: 24,
|
|
58
|
+
});
|
|
59
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Custom template references
|
|
2
|
+
|
|
3
|
+
These files are deliberately complete, one-file examples. Copy the closest
|
|
4
|
+
shape into `vanillasky/templates/`, rename its ID, then run `npx vanillasky
|
|
5
|
+
sync` and `npx vanillasky check`.
|
|
6
|
+
|
|
7
|
+
| Reference | Use it when |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| [Minimal text](minimal-text.tsx) | One short, grounded idea should fill the scene. |
|
|
10
|
+
| [Structured data](structured-data.tsx) | An exact metric and its change are the central proof. |
|
|
11
|
+
| [Supplied media](supplied-media.tsx) | An image already supplied by the application is the strongest evidence. |
|
|
12
|
+
|
|
13
|
+
All three use only `defineTemplate` from the public
|
|
14
|
+
`@vanillaskyai/video/templates` entry point. They include selection guidance,
|
|
15
|
+
JSON Schema defaults, a named example, deterministic progress-based motion,
|
|
16
|
+
safe-zone layout, and portrait/landscape handling. The media reference uses
|
|
17
|
+
`format: "supplied-image"` for an image that must appear in
|
|
18
|
+
`VideoInput.suppliedMedia`; use `format: "uri"` for another approved media or
|
|
19
|
+
link URL governed by the same server media policy.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { defineTemplate } from "@vanillaskyai/video/templates";
|
|
2
|
+
|
|
3
|
+
export const template = defineTemplate({
|
|
4
|
+
id: "minimal-text",
|
|
5
|
+
label: "Minimal text",
|
|
6
|
+
description: "A single grounded statement with a short supporting line.",
|
|
7
|
+
useWhen: "Use when one concise idea should be the entire visual response.",
|
|
8
|
+
avoidWhen: "Avoid when exact metrics, comparisons, or supplied media are the main proof.",
|
|
9
|
+
schema: {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {
|
|
12
|
+
headline: {
|
|
13
|
+
type: "string",
|
|
14
|
+
title: "Headline",
|
|
15
|
+
description: "The grounded idea the viewer should remember.",
|
|
16
|
+
minLength: 1,
|
|
17
|
+
maxLength: 72,
|
|
18
|
+
default: "The answer, made visual.",
|
|
19
|
+
},
|
|
20
|
+
detail: {
|
|
21
|
+
type: "string",
|
|
22
|
+
title: "Supporting detail",
|
|
23
|
+
description: "One short line that explains the headline.",
|
|
24
|
+
minLength: 1,
|
|
25
|
+
maxLength: 140,
|
|
26
|
+
default: "Grounded in the context your application already has.",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
required: ["headline", "detail"],
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
} as const,
|
|
32
|
+
examples: [{
|
|
33
|
+
name: "Visual answer",
|
|
34
|
+
variables: {
|
|
35
|
+
headline: "The answer, made visual.",
|
|
36
|
+
detail: "Grounded in the context your application already has.",
|
|
37
|
+
},
|
|
38
|
+
}],
|
|
39
|
+
minDuration: 3,
|
|
40
|
+
preferredDuration: 5,
|
|
41
|
+
component: ({ variables, progress, width, height, safeZone }) => {
|
|
42
|
+
const isPortrait = height >= width;
|
|
43
|
+
const reveal = Math.max(0, Math.min(1, progress * 1.6));
|
|
44
|
+
const contentWidth = isPortrait
|
|
45
|
+
? width - safeZone.left - safeZone.right
|
|
46
|
+
: Math.min(width * 0.7, 980);
|
|
47
|
+
|
|
48
|
+
return <section style={{
|
|
49
|
+
boxSizing: "border-box",
|
|
50
|
+
width,
|
|
51
|
+
height,
|
|
52
|
+
padding: `${safeZone.top}px ${safeZone.right}px ${safeZone.bottom}px ${safeZone.left}px`,
|
|
53
|
+
display: "grid",
|
|
54
|
+
placeItems: "center",
|
|
55
|
+
overflow: "hidden",
|
|
56
|
+
color: "#ffffff",
|
|
57
|
+
background: "linear-gradient(145deg, #11152c, #34357d 58%, #7867dd)",
|
|
58
|
+
fontFamily: "Inter, ui-sans-serif, system-ui, sans-serif",
|
|
59
|
+
}}>
|
|
60
|
+
<div style={{
|
|
61
|
+
width: contentWidth,
|
|
62
|
+
opacity: reveal,
|
|
63
|
+
transform: `translateY(${Math.round((1 - reveal) * 28)}px)`,
|
|
64
|
+
textAlign: isPortrait ? "left" : "center",
|
|
65
|
+
}}>
|
|
66
|
+
<h1 style={{
|
|
67
|
+
margin: 0,
|
|
68
|
+
fontSize: Math.max(38, Math.min(width * (isPortrait ? 0.105 : 0.064), 92)),
|
|
69
|
+
lineHeight: 0.98,
|
|
70
|
+
letterSpacing: "-0.05em",
|
|
71
|
+
}}>{variables.headline}</h1>
|
|
72
|
+
<p style={{
|
|
73
|
+
margin: `${Math.max(20, height * 0.035)}px auto 0`,
|
|
74
|
+
maxWidth: 760,
|
|
75
|
+
fontSize: Math.max(18, Math.min(width * 0.028, 34)),
|
|
76
|
+
lineHeight: 1.35,
|
|
77
|
+
color: "rgba(255,255,255,0.76)",
|
|
78
|
+
}}>{variables.detail}</p>
|
|
79
|
+
</div>
|
|
80
|
+
</section>;
|
|
81
|
+
},
|
|
82
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { defineTemplate } from "@vanillaskyai/video/templates";
|
|
2
|
+
|
|
3
|
+
export const template = defineTemplate({
|
|
4
|
+
id: "structured-data",
|
|
5
|
+
label: "Structured data",
|
|
6
|
+
description: "A named metric with its previous value and grounded explanation.",
|
|
7
|
+
useWhen: "Use when one exact metric and its change are the central proof.",
|
|
8
|
+
avoidWhen: "Avoid when several peer metrics matter or the source contains no exact number.",
|
|
9
|
+
schema: {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {
|
|
12
|
+
label: {
|
|
13
|
+
type: "string",
|
|
14
|
+
title: "Metric",
|
|
15
|
+
minLength: 1,
|
|
16
|
+
maxLength: 48,
|
|
17
|
+
default: "Activation",
|
|
18
|
+
},
|
|
19
|
+
current: {
|
|
20
|
+
type: "number",
|
|
21
|
+
format: "grounded-stat",
|
|
22
|
+
title: "Current value",
|
|
23
|
+
default: 58,
|
|
24
|
+
},
|
|
25
|
+
previous: {
|
|
26
|
+
type: "number",
|
|
27
|
+
format: "grounded-stat",
|
|
28
|
+
title: "Previous value",
|
|
29
|
+
default: 41,
|
|
30
|
+
},
|
|
31
|
+
unit: {
|
|
32
|
+
type: "string",
|
|
33
|
+
title: "Unit",
|
|
34
|
+
maxLength: 12,
|
|
35
|
+
default: "%",
|
|
36
|
+
},
|
|
37
|
+
explanation: {
|
|
38
|
+
type: "string",
|
|
39
|
+
title: "Explanation",
|
|
40
|
+
minLength: 1,
|
|
41
|
+
maxLength: 120,
|
|
42
|
+
default: "Guided onboarding helped more users reach value.",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: ["label", "current", "previous", "unit", "explanation"],
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
"x-vanillasky": { requiresStat: true },
|
|
48
|
+
} as const,
|
|
49
|
+
examples: [{
|
|
50
|
+
name: "Activation improved",
|
|
51
|
+
variables: {
|
|
52
|
+
label: "Activation",
|
|
53
|
+
current: 58,
|
|
54
|
+
previous: 41,
|
|
55
|
+
unit: "%",
|
|
56
|
+
explanation: "Guided onboarding helped more users reach value.",
|
|
57
|
+
},
|
|
58
|
+
}],
|
|
59
|
+
minDuration: 4,
|
|
60
|
+
preferredDuration: 6,
|
|
61
|
+
component: ({ variables, progress, width, height, safeZone }) => {
|
|
62
|
+
const isPortrait = height >= width;
|
|
63
|
+
const reveal = Math.max(0, Math.min(1, progress * 1.5));
|
|
64
|
+
const delta = variables.current - variables.previous;
|
|
65
|
+
|
|
66
|
+
return <section style={{
|
|
67
|
+
boxSizing: "border-box",
|
|
68
|
+
width,
|
|
69
|
+
height,
|
|
70
|
+
padding: `${safeZone.top}px ${safeZone.right}px ${safeZone.bottom}px ${safeZone.left}px`,
|
|
71
|
+
display: "grid",
|
|
72
|
+
alignContent: "center",
|
|
73
|
+
gap: isPortrait ? 42 : 28,
|
|
74
|
+
overflow: "hidden",
|
|
75
|
+
color: "#f7f7ff",
|
|
76
|
+
background: "linear-gradient(155deg, #0c1624, #173b4b 58%, #287b77)",
|
|
77
|
+
fontFamily: "Inter, ui-sans-serif, system-ui, sans-serif",
|
|
78
|
+
}}>
|
|
79
|
+
<p style={{ margin: 0, opacity: 0.72, fontSize: isPortrait ? 30 : 28 }}>
|
|
80
|
+
{variables.label}
|
|
81
|
+
</p>
|
|
82
|
+
<div style={{
|
|
83
|
+
display: "flex",
|
|
84
|
+
flexDirection: isPortrait ? "column" : "row",
|
|
85
|
+
alignItems: isPortrait ? "flex-start" : "baseline",
|
|
86
|
+
gap: isPortrait ? 12 : 36,
|
|
87
|
+
opacity: reveal,
|
|
88
|
+
transform: `translateY(${Math.round((1 - reveal) * 24)}px)`,
|
|
89
|
+
}}>
|
|
90
|
+
<strong style={{
|
|
91
|
+
fontSize: Math.max(76, Math.min(width * (isPortrait ? 0.23 : 0.14), 220)),
|
|
92
|
+
lineHeight: 0.85,
|
|
93
|
+
letterSpacing: "-0.07em",
|
|
94
|
+
}}>{variables.current}{variables.unit}</strong>
|
|
95
|
+
<span style={{ fontSize: isPortrait ? 28 : 34, color: "#83f3ca" }}>
|
|
96
|
+
{delta >= 0 ? "+" : ""}{delta}{variables.unit} from {variables.previous}{variables.unit}
|
|
97
|
+
</span>
|
|
98
|
+
</div>
|
|
99
|
+
<p style={{ margin: 0, maxWidth: 900, fontSize: isPortrait ? 27 : 30, lineHeight: 1.35 }}>
|
|
100
|
+
{variables.explanation}
|
|
101
|
+
</p>
|
|
102
|
+
</section>;
|
|
103
|
+
},
|
|
104
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { defineTemplate } from "@vanillaskyai/video/templates";
|
|
2
|
+
|
|
3
|
+
const previewImageUrl = "data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%221200%22%20height=%22720%22%20viewBox=%220%200%201200%20720%22%3E%3Crect%20width=%221200%22%20height=%22720%22%20rx=%2248%22%20fill=%22%23181733%22/%3E%3Crect%20x=%2280%22%20y=%2280%22%20width=%221040%22%20height=%22560%22%20rx=%2232%22%20fill=%22%23f4f1ff%22/%3E%3Ccircle%20cx=%22220%22%20cy=%22230%22%20r=%2270%22%20fill=%22%237867dd%22/%3E%3Cpath%20d=%22M160%20520L380%20360L580%20430L820%20220L1040%20300%22%20fill=%22none%22%20stroke=%22%23287b77%22%20stroke-width=%2232%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22/%3E%3C/svg%3E";
|
|
4
|
+
|
|
5
|
+
export const template = defineTemplate({
|
|
6
|
+
id: "supplied-media",
|
|
7
|
+
label: "Supplied media",
|
|
8
|
+
description: "An application-supplied screenshot with a grounded caption.",
|
|
9
|
+
useWhen: "Use when a supplied product image is the strongest evidence for the answer.",
|
|
10
|
+
avoidWhen: "Avoid when no approved image was supplied or text and data communicate the point more clearly.",
|
|
11
|
+
schema: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
imageUrl: {
|
|
15
|
+
type: "string",
|
|
16
|
+
format: "supplied-image",
|
|
17
|
+
title: "Supplied image",
|
|
18
|
+
description: "The URL of an image present in VideoInput.suppliedMedia.",
|
|
19
|
+
default: previewImageUrl,
|
|
20
|
+
},
|
|
21
|
+
headline: {
|
|
22
|
+
type: "string",
|
|
23
|
+
title: "Headline",
|
|
24
|
+
minLength: 1,
|
|
25
|
+
maxLength: 72,
|
|
26
|
+
default: "See the change in context.",
|
|
27
|
+
},
|
|
28
|
+
caption: {
|
|
29
|
+
type: "string",
|
|
30
|
+
title: "Caption",
|
|
31
|
+
minLength: 1,
|
|
32
|
+
maxLength: 120,
|
|
33
|
+
default: "The dashboard reflects the grounded result described in the answer.",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
required: ["imageUrl", "headline", "caption"],
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
} as const,
|
|
39
|
+
examples: [{
|
|
40
|
+
name: "Product dashboard",
|
|
41
|
+
variables: {
|
|
42
|
+
imageUrl: previewImageUrl,
|
|
43
|
+
headline: "See the change in context.",
|
|
44
|
+
caption: "The dashboard reflects the grounded result described in the answer.",
|
|
45
|
+
},
|
|
46
|
+
}],
|
|
47
|
+
minDuration: 4,
|
|
48
|
+
preferredDuration: 6,
|
|
49
|
+
component: ({ variables, progress, width, height, safeZone }) => {
|
|
50
|
+
const isPortrait = height >= width;
|
|
51
|
+
const reveal = Math.max(0, Math.min(1, progress * 1.7));
|
|
52
|
+
|
|
53
|
+
return <section style={{
|
|
54
|
+
boxSizing: "border-box",
|
|
55
|
+
width,
|
|
56
|
+
height,
|
|
57
|
+
padding: `${safeZone.top}px ${safeZone.right}px ${safeZone.bottom}px ${safeZone.left}px`,
|
|
58
|
+
display: "grid",
|
|
59
|
+
gridTemplateColumns: isPortrait ? "1fr" : "minmax(0, 1.1fr) minmax(320px, 0.9fr)",
|
|
60
|
+
alignContent: "center",
|
|
61
|
+
alignItems: "center",
|
|
62
|
+
gap: isPortrait ? 44 : 64,
|
|
63
|
+
overflow: "hidden",
|
|
64
|
+
color: "#15152b",
|
|
65
|
+
background: "linear-gradient(145deg, #f4f1ff, #dfe7ff)",
|
|
66
|
+
fontFamily: "Inter, ui-sans-serif, system-ui, sans-serif",
|
|
67
|
+
}}>
|
|
68
|
+
<img
|
|
69
|
+
src={variables.imageUrl}
|
|
70
|
+
alt=""
|
|
71
|
+
style={{
|
|
72
|
+
display: "block",
|
|
73
|
+
width: "100%",
|
|
74
|
+
maxHeight: isPortrait ? height * 0.48 : height * 0.68,
|
|
75
|
+
objectFit: "cover",
|
|
76
|
+
borderRadius: 28,
|
|
77
|
+
boxShadow: "0 28px 80px rgba(28, 34, 75, 0.24)",
|
|
78
|
+
opacity: reveal,
|
|
79
|
+
transform: `scale(${0.96 + reveal * 0.04})`,
|
|
80
|
+
}}
|
|
81
|
+
/>
|
|
82
|
+
<div style={{ opacity: reveal }}>
|
|
83
|
+
<h1 style={{ margin: 0, fontSize: isPortrait ? 58 : 70, lineHeight: 0.98, letterSpacing: "-0.045em" }}>
|
|
84
|
+
{variables.headline}
|
|
85
|
+
</h1>
|
|
86
|
+
<p style={{ margin: "24px 0 0", fontSize: isPortrait ? 25 : 28, lineHeight: 1.4 }}>
|
|
87
|
+
{variables.caption}
|
|
88
|
+
</p>
|
|
89
|
+
</div>
|
|
90
|
+
</section>;
|
|
91
|
+
},
|
|
92
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Next.js quickstart
|
|
2
|
+
|
|
3
|
+
This is the smallest complete VanillaSky app: one server route, one React page,
|
|
4
|
+
and the built-in templates.
|
|
5
|
+
|
|
6
|
+
<!-- verify:start -->
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
cp .env.example .env.local
|
|
10
|
+
npm run build
|
|
11
|
+
npm run dev
|
|
12
|
+
```
|
|
13
|
+
<!-- verify:end -->
|
|
14
|
+
|
|
15
|
+
Add your OpenAI API key to `.env.local` before selecting **Generate video**.
|
|
16
|
+
|
|
17
|
+
Open <http://localhost:3000> and select **Generate video**.
|
|
18
|
+
|
|
19
|
+
The example authorizes local development only and denies production requests.
|
|
20
|
+
Replace the local-only authorization before deploying. See the
|
|
21
|
+
[Next.js guide](../../docs/integrate-nextjs.md) for production and optional
|
|
22
|
+
configuration.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vanillasky-nextjs-quickstart",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@ai-sdk/openai": "^4.0.42",
|
|
12
|
+
"@vanillaskyai/video": "0.1.0",
|
|
13
|
+
"ai": "^7.0.66",
|
|
14
|
+
"next": "16.3.1",
|
|
15
|
+
"react": "19.2.8",
|
|
16
|
+
"react-dom": "19.2.8"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^24.10.1",
|
|
20
|
+
"@types/react": "^19",
|
|
21
|
+
"@types/react-dom": "^19",
|
|
22
|
+
"typescript": "^5.9.3"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { openai } from "@ai-sdk/openai";
|
|
2
|
+
import { streamText } from "ai";
|
|
3
|
+
import { createVideoHandler } from "@vanillaskyai/video/server";
|
|
4
|
+
|
|
5
|
+
const model = openai(process.env.OPENAI_MODEL ?? "gpt-4.1");
|
|
6
|
+
|
|
7
|
+
const handle = createVideoHandler({
|
|
8
|
+
authorize: (request) => {
|
|
9
|
+
if (process.env.NODE_ENV !== "development") return false;
|
|
10
|
+
const hostname = new URL(request.url).hostname;
|
|
11
|
+
return hostname === "localhost" || hostname === "127.0.0.1";
|
|
12
|
+
},
|
|
13
|
+
streamText: ({ systemPrompt, userPrompt, signal }) => streamText({
|
|
14
|
+
model,
|
|
15
|
+
system: systemPrompt,
|
|
16
|
+
prompt: userPrompt,
|
|
17
|
+
abortSignal: signal,
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const POST = handle;
|
|
22
|
+
export const OPTIONS = handle;
|