@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.
Files changed (162) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE +201 -0
  3. package/PUBLIC-API.md +276 -0
  4. package/README.md +152 -0
  5. package/SECURITY.md +22 -0
  6. package/SUPPORT.md +28 -0
  7. package/bin/vanillasky.js +3 -0
  8. package/dist/bg-confetti-WNQXS7ZS.js +89 -0
  9. package/dist/bg-emoji-ETI47GLS.js +150 -0
  10. package/dist/bg-media-SVUZAMGX.js +99 -0
  11. package/dist/brand-message-F3RD4P3P.js +253 -0
  12. package/dist/builtin-server-F7YXRVCH.js +8 -0
  13. package/dist/catalog-types-BIhSpOWK.d.ts +74 -0
  14. package/dist/chart-bar-CNSC7BNK.js +304 -0
  15. package/dist/chart-counter-MCRHKN77.js +299 -0
  16. package/dist/chart-progress-ring-RMI4A7ZK.js +296 -0
  17. package/dist/check-runtime.d.ts +15 -0
  18. package/dist/check-runtime.js +102 -0
  19. package/dist/chunk-2E6T633S.js +27 -0
  20. package/dist/chunk-352ZZCET.js +83 -0
  21. package/dist/chunk-73NTSFFI.js +81 -0
  22. package/dist/chunk-A2X45FET.js +46 -0
  23. package/dist/chunk-ABPKM4GK.js +187 -0
  24. package/dist/chunk-ABZACD4G.js +216 -0
  25. package/dist/chunk-BCRUUJ6A.js +756 -0
  26. package/dist/chunk-CHLGW22F.js +3551 -0
  27. package/dist/chunk-CJ7F2CE6.js +104 -0
  28. package/dist/chunk-CR7FE7BW.js +109 -0
  29. package/dist/chunk-E7CL7UPB.js +109 -0
  30. package/dist/chunk-EE6PK6XC.js +18 -0
  31. package/dist/chunk-ERY4S6QV.js +385 -0
  32. package/dist/chunk-EVG2ZYIY.js +125 -0
  33. package/dist/chunk-FNAEQ7QO.js +155 -0
  34. package/dist/chunk-FQ47WXAJ.js +38 -0
  35. package/dist/chunk-FSG2PYWG.js +336 -0
  36. package/dist/chunk-GHJEAP4O.js +504 -0
  37. package/dist/chunk-GRMUV7QY.js +262 -0
  38. package/dist/chunk-IFOW65Z6.js +205 -0
  39. package/dist/chunk-IXCUY3B3.js +786 -0
  40. package/dist/chunk-L5BTTCZV.js +97 -0
  41. package/dist/chunk-LMG3GHLL.js +107 -0
  42. package/dist/chunk-OOPIETLW.js +101 -0
  43. package/dist/chunk-P22HHFDK.js +43 -0
  44. package/dist/chunk-QAAO7OAY.js +19 -0
  45. package/dist/chunk-S7HFLYNN.js +35 -0
  46. package/dist/chunk-SPVTJH3F.js +24 -0
  47. package/dist/chunk-W2XJKG6C.js +3177 -0
  48. package/dist/chunk-XB7LH4J2.js +301 -0
  49. package/dist/chunk-XGRA2MUV.js +242 -0
  50. package/dist/chunk-XTOXFMLM.js +105 -0
  51. package/dist/chunk-YC5MFXA5.js +132 -0
  52. package/dist/cli.d.ts +1 -0
  53. package/dist/cli.js +1681 -0
  54. package/dist/compose-video-CN3VSWST.js +13 -0
  55. package/dist/cta-logo-SVA5GP2A.js +209 -0
  56. package/dist/cta-media-E7VLLK42.js +215 -0
  57. package/dist/events-_wfjbmEp.d.ts +81 -0
  58. package/dist/incoming-call-62MFZL3Z.js +320 -0
  59. package/dist/index.d.ts +15 -0
  60. package/dist/index.js +14 -0
  61. package/dist/infographic-before-after-AXCDAWND.js +460 -0
  62. package/dist/infographic-feature-list-GW5GUE6O.js +266 -0
  63. package/dist/infographic-problem-solution-7NURJANA.js +340 -0
  64. package/dist/infographic-stat-row-J7IZR6ZN.js +241 -0
  65. package/dist/infographic-steps-VSZM6F5M.js +400 -0
  66. package/dist/kit-BMNcHqa9.d.ts +79 -0
  67. package/dist/prompt-input-LOUCRXCY.js +236 -0
  68. package/dist/react.d.ts +81 -0
  69. package/dist/react.js +835 -0
  70. package/dist/reaction-QG4CYZLQ.js +119 -0
  71. package/dist/server.d.ts +141 -0
  72. package/dist/server.js +498 -0
  73. package/dist/showcase-code-RUTFZR3H.js +351 -0
  74. package/dist/showcase-phone-6CP6XEU5.js +272 -0
  75. package/dist/showcase-terminal-MGAEQWPR.js +306 -0
  76. package/dist/showcase-web-Y4W2HVRJ.js +390 -0
  77. package/dist/social-conversation-75SD2637.js +695 -0
  78. package/dist/social-milestone-XI2QE5BO.js +292 -0
  79. package/dist/social-notification-2UOHQCUY.js +244 -0
  80. package/dist/social-review-stack-FDYZGZTJ.js +239 -0
  81. package/dist/social-testimonial-OQ7N52GJ.js +284 -0
  82. package/dist/social-tweet-5OLCNPPC.js +295 -0
  83. package/dist/state-DZcKuS32.d.ts +3 -0
  84. package/dist/system-prompt-A6UXNTPK.js +8 -0
  85. package/dist/template-catalog.d.ts +24 -0
  86. package/dist/template-catalog.js +6 -0
  87. package/dist/templates.d.ts +25 -0
  88. package/dist/templates.js +19 -0
  89. package/dist/test.d.ts +149 -0
  90. package/dist/test.js +272 -0
  91. package/dist/text-stream-6XHC7J47.js +10 -0
  92. package/dist/types-B-PZR6G_.d.ts +208 -0
  93. package/dist/validate-T7GBU2YF.js +8 -0
  94. package/docs/agent-integration.md +39 -0
  95. package/docs/architecture.md +84 -0
  96. package/docs/branding-and-personalization.md +87 -0
  97. package/docs/concepts.md +106 -0
  98. package/docs/custom-templates.md +329 -0
  99. package/docs/customization.md +141 -0
  100. package/docs/errors.md +70 -0
  101. package/docs/getting-started.md +111 -0
  102. package/docs/input-and-first-scene.md +41 -0
  103. package/docs/integrate-nextjs.md +79 -0
  104. package/docs/media-and-audio.md +69 -0
  105. package/docs/motion-and-effects.md +107 -0
  106. package/docs/persistence.md +95 -0
  107. package/docs/production.md +122 -0
  108. package/docs/prompt-and-input.md +185 -0
  109. package/docs/provider-integration.md +115 -0
  110. package/docs/reference/protocol.md +96 -0
  111. package/docs/reference/provider-adapters.md +158 -0
  112. package/docs/responsive-orientation.md +37 -0
  113. package/docs/security.md +38 -0
  114. package/docs/streaming-protocol.md +15 -0
  115. package/docs/testing.md +137 -0
  116. package/docs/use-cases.md +59 -0
  117. package/examples/custom-template/README.md +19 -0
  118. package/examples/custom-template/minimal-text.tsx +82 -0
  119. package/examples/custom-template/structured-data.tsx +104 -0
  120. package/examples/custom-template/supplied-media.tsx +92 -0
  121. package/examples/nextjs-quickstart/.env.example +2 -0
  122. package/examples/nextjs-quickstart/README.md +22 -0
  123. package/examples/nextjs-quickstart/next-env.d.ts +4 -0
  124. package/examples/nextjs-quickstart/package.json +24 -0
  125. package/examples/nextjs-quickstart/src/app/api/video/route.ts +22 -0
  126. package/examples/nextjs-quickstart/src/app/layout.tsx +5 -0
  127. package/examples/nextjs-quickstart/src/app/page.tsx +31 -0
  128. package/examples/nextjs-quickstart/tsconfig.json +26 -0
  129. package/package.json +167 -0
  130. package/registry/items/backgrounds.json +52 -0
  131. package/registry/items/barChart.json +237 -0
  132. package/registry/items/beforeAfter.json +180 -0
  133. package/registry/items/bigNumber.json +225 -0
  134. package/registry/items/brandMessage.json +158 -0
  135. package/registry/items/cardList.json +206 -0
  136. package/registry/items/chatMessenger.json +151 -0
  137. package/registry/items/chatWhatsapp.json +151 -0
  138. package/registry/items/codeEditor.json +184 -0
  139. package/registry/items/confetti.json +107 -0
  140. package/registry/items/ctaLogo.json +160 -0
  141. package/registry/items/ctaMedia.json +193 -0
  142. package/registry/items/emojiBurst.json +101 -0
  143. package/registry/items/incomingCall.json +170 -0
  144. package/registry/items/media.json +179 -0
  145. package/registry/items/milestone.json +205 -0
  146. package/registry/items/motion.json +45 -0
  147. package/registry/items/notification.json +182 -0
  148. package/registry/items/phoneMockup.json +254 -0
  149. package/registry/items/problemSolution.json +186 -0
  150. package/registry/items/progressRing.json +199 -0
  151. package/registry/items/promptInput.json +148 -0
  152. package/registry/items/reaction.json +170 -0
  153. package/registry/items/reviewStack.json +224 -0
  154. package/registry/items/steps.json +207 -0
  155. package/registry/items/template-context.json +31 -0
  156. package/registry/items/terminal.json +212 -0
  157. package/registry/items/testimonial.json +164 -0
  158. package/registry/items/theme.json +47 -0
  159. package/registry/items/tripleStats.json +228 -0
  160. package/registry/items/tweet.json +203 -0
  161. package/registry/items/typography.json +47 -0
  162. 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.
@@ -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.
@@ -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,2 @@
1
+ OPENAI_API_KEY=replace-me
2
+ OPENAI_MODEL=gpt-4.1
@@ -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,4 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // This file is generated and maintained by Next.js.
@@ -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;
@@ -0,0 +1,5 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
4
+ return <html lang="en"><body>{children}</body></html>;
5
+ }