@vanillaskyai/video 0.2.0 → 0.3.1

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/dist/react.js CHANGED
@@ -152,6 +152,51 @@ function savedVideoState(video) {
152
152
  config: video
153
153
  };
154
154
  }
155
+ function PlayerIcon({ name, size = 22 }) {
156
+ const shared = {
157
+ width: size,
158
+ height: size,
159
+ viewBox: "0 0 24 24",
160
+ fill: "none",
161
+ stroke: "currentColor",
162
+ strokeWidth: 2,
163
+ strokeLinecap: "round",
164
+ strokeLinejoin: "round",
165
+ "aria-hidden": true,
166
+ focusable: false
167
+ };
168
+ if (name === "play") {
169
+ return /* @__PURE__ */ jsx("svg", { ...shared, children: /* @__PURE__ */ jsx("path", { d: "M8 5v14l11-7z", fill: "currentColor", stroke: "none" }) });
170
+ }
171
+ if (name === "pause") {
172
+ return /* @__PURE__ */ jsxs("svg", { ...shared, children: [
173
+ /* @__PURE__ */ jsx("rect", { x: "7", y: "5", width: "3.5", height: "14", rx: "1", fill: "currentColor", stroke: "none" }),
174
+ /* @__PURE__ */ jsx("rect", { x: "13.5", y: "5", width: "3.5", height: "14", rx: "1", fill: "currentColor", stroke: "none" })
175
+ ] });
176
+ }
177
+ if (name === "replay") {
178
+ return /* @__PURE__ */ jsxs("svg", { ...shared, children: [
179
+ /* @__PURE__ */ jsx("path", { d: "M4.5 9A8 8 0 1 1 5 16" }),
180
+ /* @__PURE__ */ jsx("path", { d: "M4.5 4.5V9H9" })
181
+ ] });
182
+ }
183
+ if (name === "volume-off") {
184
+ return /* @__PURE__ */ jsxs("svg", { ...shared, children: [
185
+ /* @__PURE__ */ jsx("path", { d: "M11 5 6.5 9H3v6h3.5L11 19z" }),
186
+ /* @__PURE__ */ jsx("path", { d: "m16 9 5 5M21 9l-5 5" })
187
+ ] });
188
+ }
189
+ if (name === "volume") {
190
+ return /* @__PURE__ */ jsxs("svg", { ...shared, children: [
191
+ /* @__PURE__ */ jsx("path", { d: "M11 5 6.5 9H3v6h3.5L11 19z" }),
192
+ /* @__PURE__ */ jsx("path", { d: "M15 9.5a4 4 0 0 1 0 5M17.8 7a7.5 7.5 0 0 1 0 10" })
193
+ ] });
194
+ }
195
+ if (name === "exit-fullscreen") {
196
+ return /* @__PURE__ */ jsx("svg", { ...shared, children: /* @__PURE__ */ jsx("path", { d: "M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5" }) });
197
+ }
198
+ return /* @__PURE__ */ jsx("svg", { ...shared, children: /* @__PURE__ */ jsx("path", { d: "M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5" }) });
199
+ }
155
200
  function VideoPlayerRuntime({
156
201
  kit,
157
202
  stream,
@@ -469,33 +514,38 @@ function VideoPlayerRuntime({
469
514
  if (document.fullscreenElement === container) await document.exitFullscreen?.();
470
515
  else await container.requestFullscreen?.();
471
516
  };
517
+ const controlSize = Math.max(40, Math.min(52, Math.round(displayWidth * 0.15)));
518
+ const controlInset = Math.max(10, Math.min(20, Math.round(displayWidth * 0.056)));
472
519
  const controlButtonStyle = {
473
520
  display: "inline-grid",
474
521
  placeItems: "center",
475
522
  flex: "0 0 auto",
476
- minWidth: 44,
477
- minHeight: 44,
478
- padding: "6px 9px",
479
- border: 0,
480
- borderRadius: 8,
481
- backgroundColor: "rgba(9, 7, 18, 0.88)",
523
+ width: controlSize,
524
+ height: controlSize,
525
+ minWidth: controlSize,
526
+ minHeight: controlSize,
527
+ padding: 0,
528
+ border: "1px solid rgba(255, 255, 255, 0.08)",
529
+ borderRadius: 999,
530
+ backgroundColor: "rgba(255, 255, 255, 0.16)",
482
531
  color: "#ffffff",
483
- font: "600 13px/1 system-ui, sans-serif",
532
+ boxShadow: "0 5px 18px rgba(0, 0, 0, 0.16)",
533
+ backdropFilter: "blur(12px)",
484
534
  cursor: "pointer"
485
535
  };
486
536
  const startButtonStyle = {
487
537
  display: "inline-flex",
488
538
  alignItems: "center",
489
539
  gap: 10,
490
- minHeight: 54,
491
- padding: "12px 18px",
492
- border: "1px solid rgba(255, 255, 255, 0.3)",
540
+ minHeight: 60,
541
+ padding: "14px 22px",
542
+ border: "1px solid rgba(9, 7, 18, 0.08)",
493
543
  borderRadius: 999,
494
- backgroundColor: "rgba(9, 7, 18, 0.88)",
495
- color: "#ffffff",
496
- boxShadow: "0 8px 28px rgba(0, 0, 0, 0.4)",
497
- backdropFilter: "blur(12px)",
498
- font: "700 15px/1 system-ui, sans-serif",
544
+ backgroundColor: "#ffffff",
545
+ color: "#090712",
546
+ boxShadow: "0 8px 28px rgba(0, 0, 0, 0.24)",
547
+ font: "700 16px/1 system-ui, sans-serif",
548
+ whiteSpace: "nowrap",
499
549
  cursor: "pointer"
500
550
  };
501
551
  const startButtonPositionStyle = {
@@ -579,7 +629,7 @@ function VideoPlayerRuntime({
579
629
  onClick: armPlayback,
580
630
  style: { ...startButtonStyle, ...startButtonPositionStyle },
581
631
  children: [
582
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u25B6" }),
632
+ /* @__PURE__ */ jsx(PlayerIcon, { name: "play", size: 20 }),
583
633
  config?.audio && !isMuted ? "Play with sound" : "Play video"
584
634
  ]
585
635
  }
@@ -615,7 +665,7 @@ function VideoPlayerRuntime({
615
665
  ...startButtonPositionStyle
616
666
  },
617
667
  children: [
618
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u25B6" }),
668
+ /* @__PURE__ */ jsx(PlayerIcon, { name: "play", size: 20 }),
619
669
  config?.audio && !isMuted ? "Play with sound" : "Play video"
620
670
  ]
621
671
  }
@@ -631,60 +681,99 @@ function VideoPlayerRuntime({
631
681
  loop: state.status === "streaming" || introPlaying
632
682
  }
633
683
  ) : null,
634
- !generationCoverVisible ? /* @__PURE__ */ jsxs(
684
+ ended ? /* @__PURE__ */ jsx(
685
+ "div",
686
+ {
687
+ "data-testid": "video-ended-scrim",
688
+ "aria-hidden": "true",
689
+ style: {
690
+ position: "absolute",
691
+ inset: 0,
692
+ zIndex: 1,
693
+ pointerEvents: "none",
694
+ background: "rgba(4, 3, 18, 0.52)",
695
+ backdropFilter: "blur(4px)"
696
+ }
697
+ }
698
+ ) : null,
699
+ ended ? /* @__PURE__ */ jsxs(
700
+ "button",
701
+ {
702
+ type: "button",
703
+ "data-testid": "video-replay-button",
704
+ "aria-label": "Replay video response",
705
+ onClick: togglePlayback,
706
+ style: {
707
+ ...startButtonStyle,
708
+ ...startButtonPositionStyle,
709
+ zIndex: 3
710
+ },
711
+ children: [
712
+ /* @__PURE__ */ jsx(PlayerIcon, { name: "replay", size: 21 }),
713
+ "Replay"
714
+ ]
715
+ }
716
+ ) : null,
717
+ !generationCoverVisible && !showStartPoster && config?.scenes.length ? /* @__PURE__ */ jsxs(
635
718
  "div",
636
719
  {
637
720
  "data-testid": "video-controls",
721
+ "data-layout": "split",
638
722
  style: {
639
723
  position: "absolute",
640
- right: 10,
641
- bottom: 10,
642
- zIndex: 2,
643
- display: "flex",
644
- alignItems: "center",
645
- gap: 6,
646
- minHeight: 48,
647
- padding: "6px 8px",
648
- border: "1px solid rgba(255, 255, 255, 0.2)",
649
- borderRadius: 12,
650
- backgroundColor: "rgba(9, 7, 18, 0.82)",
651
- backdropFilter: "blur(12px)",
652
- color: "#ffffff",
653
- boxShadow: "0 4px 20px rgba(0, 0, 0, 0.35)"
724
+ inset: 0,
725
+ zIndex: 4,
726
+ pointerEvents: "none"
654
727
  },
655
728
  children: [
656
- !showStartPoster ? /* @__PURE__ */ jsx(
657
- "button",
658
- {
659
- type: "button",
660
- "aria-label": ended ? "Replay video response" : isPlaying ? "Pause video response" : "Play video response",
661
- onClick: togglePlayback,
662
- style: controlButtonStyle,
663
- children: ended ? "\u21BB Replay" : isPlaying ? "\u2161" : "\u25B6"
664
- }
665
- ) : null,
666
- config?.audio ? /* @__PURE__ */ jsx(
667
- "button",
729
+ /* @__PURE__ */ jsx(
730
+ "div",
668
731
  {
669
- type: "button",
670
- "aria-label": isMuted ? "Unmute video response" : "Mute video response",
671
- "aria-pressed": !isMuted,
672
- onClick: () => setIsMuted((muted) => {
673
- if (muted) setAudioUnlocked(true);
674
- return !muted;
675
- }),
676
- style: controlButtonStyle,
677
- children: isMuted ? "\u{1F507}" : "\u{1F50A}"
732
+ "data-testid": "video-primary-controls",
733
+ style: { position: "absolute", left: controlInset, bottom: controlInset, display: "flex", pointerEvents: "auto" },
734
+ children: /* @__PURE__ */ jsx(
735
+ "button",
736
+ {
737
+ type: "button",
738
+ "aria-label": ended ? "Play video response from beginning" : isPlaying ? "Pause video response" : "Play video response",
739
+ onClick: togglePlayback,
740
+ style: controlButtonStyle,
741
+ children: /* @__PURE__ */ jsx(PlayerIcon, { name: isPlaying ? "pause" : "play" })
742
+ }
743
+ )
678
744
  }
679
- ) : null,
680
- /* @__PURE__ */ jsx(
681
- "button",
745
+ ),
746
+ /* @__PURE__ */ jsxs(
747
+ "div",
682
748
  {
683
- type: "button",
684
- "aria-label": isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
685
- onClick: () => void toggleFullscreen(),
686
- style: controlButtonStyle,
687
- children: isFullscreen ? "\u2199" : "\u26F6"
749
+ "data-testid": "video-secondary-controls",
750
+ style: { position: "absolute", right: controlInset, bottom: controlInset, display: "flex", gap: 10, pointerEvents: "auto" },
751
+ children: [
752
+ config?.audio ? /* @__PURE__ */ jsx(
753
+ "button",
754
+ {
755
+ type: "button",
756
+ "aria-label": isMuted ? "Unmute video response" : "Mute video response",
757
+ "aria-pressed": !isMuted,
758
+ onClick: () => setIsMuted((muted) => {
759
+ if (muted) setAudioUnlocked(true);
760
+ return !muted;
761
+ }),
762
+ style: controlButtonStyle,
763
+ children: /* @__PURE__ */ jsx(PlayerIcon, { name: isMuted ? "volume-off" : "volume" })
764
+ }
765
+ ) : null,
766
+ /* @__PURE__ */ jsx(
767
+ "button",
768
+ {
769
+ type: "button",
770
+ "aria-label": isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
771
+ onClick: () => void toggleFullscreen(),
772
+ style: controlButtonStyle,
773
+ children: /* @__PURE__ */ jsx(PlayerIcon, { name: isFullscreen ? "exit-fullscreen" : "enter-fullscreen" })
774
+ }
775
+ )
776
+ ]
688
777
  }
689
778
  )
690
779
  ]
package/dist/server.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { l as VideoGenerationContext, m as VideoPlanner, n as VideoRequest, j as VideoCapabilities, d as VideoInput, a as VideoAudio, o as VideoSceneValidator, p as VideoTemplatePacing, q as VideoSnapshotRetention, r as VideoResumeCursor, s as VideoSceneValidationContext, f as VideoScene } from './types-_t4bduwX.js';
2
- import { b as VideoFinishReason, a as VideoWarning, V as VideoEvent } from './events-B-28kERX.js';
3
- export { c as VideoWarningCategory } from './events-B-28kERX.js';
1
+ import { m as VideoGenerationContext, n as VideoPlanner, o as VideoRequest, k as VideoCapabilities, d as VideoInput, a as VideoAudio, p as VideoSceneValidator, q as VideoTemplatePacing, r as VideoSnapshotRetention, s as VideoResumeCursor, t as VideoSceneValidationContext, g as VideoScene } from './types-CVMb6QEq.js';
2
+ import { b as VideoFinishReason, a as VideoWarning, V as VideoEvent } from './events-tQ0x-VaL.js';
3
+ export { c as VideoWarningCategory } from './events-tQ0x-VaL.js';
4
4
  import { S as SceneTemplateMetadata } from './catalog-types-BIhSpOWK.js';
5
5
 
6
6
  interface VideoProviderUsage {
package/dist/server.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-BKF3A357.js";
4
4
  import {
5
5
  createVideo
6
- } from "./chunk-TGFO454Q.js";
6
+ } from "./chunk-LHFADNWJ.js";
7
7
  import "./chunk-E7CL7UPB.js";
8
8
  import {
9
9
  BUILTIN_SERVER_TEMPLATE_KIT,
@@ -31,10 +31,8 @@ import {
31
31
  import {
32
32
  createTemplateSystemPrompt,
33
33
  getStandardMediaResolverContract
34
- } from "./chunk-Q2DDQKEG.js";
35
- import {
36
- DEFAULT_VIDEO_SYSTEM_PROMPT
37
- } from "./chunk-K5SRF7D3.js";
34
+ } from "./chunk-GR4I3MN2.js";
35
+ import "./chunk-WHPZED7X.js";
38
36
  import "./chunk-3O7OMMMF.js";
39
37
  import "./chunk-352ZZCET.js";
40
38
  import "./chunk-XAMUOSX7.js";
@@ -131,6 +129,7 @@ function parseVideoRequest(value) {
131
129
  const input = record(request.input, "request.input");
132
130
  allowedKeys(input, [
133
131
  "input",
132
+ "knowledgeMode",
134
133
  "instructions",
135
134
  "maxDurationSec",
136
135
  "orientation",
@@ -142,6 +141,14 @@ function parseVideoRequest(value) {
142
141
  "audio"
143
142
  ], "request.input");
144
143
  string(input.input, "request.input.input");
144
+ if (input.knowledgeMode != null) {
145
+ enumValue(
146
+ input.knowledgeMode,
147
+ ["input-only", "general"],
148
+ "input-only or general",
149
+ "request.input.knowledgeMode"
150
+ );
151
+ }
145
152
  if (input.instructions != null) string(input.instructions, "request.input.instructions");
146
153
  if (input.maxDurationSec != null) {
147
154
  numberBetween(input.maxDurationSec, 5, 120, "request.input.maxDurationSec");
@@ -154,7 +161,7 @@ function parseVideoRequest(value) {
154
161
  "request.input.orientation"
155
162
  );
156
163
  }
157
- if (input.opening != null) string(input.opening, "request.input.opening");
164
+ if (input.opening != null && input.opening !== false) string(input.opening, "request.input.opening");
158
165
  if (input.audio != null && input.audio !== false) {
159
166
  const soundtrack = record(input.audio, "request.input.audio");
160
167
  allowedKeys(soundtrack, ["src"], "request.input.audio");
@@ -402,7 +409,7 @@ function createVideoStreamHandler(options) {
402
409
  onError: options.onError,
403
410
  onWarning: options.onWarning,
404
411
  onComplete: options.onComplete,
405
- systemPrompt: systemPrompt ?? DEFAULT_VIDEO_SYSTEM_PROMPT,
412
+ systemPrompt,
406
413
  selectAudio: options.selectAudio,
407
414
  snapshotRetention: options.snapshotRetention,
408
415
  signal: request.signal,
@@ -651,12 +658,13 @@ function createVideoHandler(options) {
651
658
  approveUrl,
652
659
  isOpeningReady: (input) => openingReadyInputs.has(input)
653
660
  }),
654
- systemPrompt: ({ capabilities: capabilities2 }) => {
661
+ systemPrompt: ({ request, capabilities: capabilities2 }) => {
655
662
  const selectedIds = capabilities2?.templates == null ? void 0 : new Set(capabilities2.templates);
656
663
  const selectedTemplates = selectedIds == null ? templates.listTemplateMetadata() : templates.listTemplateMetadata().filter(({ id }) => selectedIds.has(id));
657
664
  return createTemplateSystemPrompt({
658
665
  kit: { listTemplateMetadata: () => selectedTemplates },
659
666
  basePrompt,
667
+ knowledgeMode: request.input.knowledgeMode,
660
668
  mediaResolverAvailable: resolveMedia != null
661
669
  });
662
670
  },
@@ -0,0 +1,10 @@
1
+ import {
2
+ DEFAULT_VIDEO_SYSTEM_PROMPT,
3
+ VIDEO_PLAN_INSTRUCTION,
4
+ createVideoSystemPrompt
5
+ } from "./chunk-WHPZED7X.js";
6
+ export {
7
+ DEFAULT_VIDEO_SYSTEM_PROMPT,
8
+ VIDEO_PLAN_INSTRUCTION,
9
+ createVideoSystemPrompt
10
+ };
@@ -1,9 +1,9 @@
1
- import { I as InferTemplateJsonSchema, S as SceneTemplateProps, a as SceneTemplate } from './kit-CBedbj1n.js';
2
- export { T as TemplateRegistry, c as createTemplateRegistry } from './kit-CBedbj1n.js';
1
+ import { I as InferTemplateJsonSchema, S as SceneTemplateProps, a as SceneTemplate } from './kit-DlUSg8lA.js';
2
+ export { T as TemplateRegistry, c as createTemplateRegistry } from './kit-DlUSg8lA.js';
3
3
  import { ComponentType } from 'react';
4
4
  import { d as TemplateJsonSchema, S as SceneTemplateMetadata } from './catalog-types-BIhSpOWK.js';
5
5
  export { T as TemplateFamily, c as TemplateTimingMetadata, e as TemplateTransitionTiming } from './catalog-types-BIhSpOWK.js';
6
- import './types-_t4bduwX.js';
6
+ import './types-CVMb6QEq.js';
7
7
 
8
8
  interface TemplateExample<Variables extends Record<string, unknown> = Record<string, unknown>> {
9
9
  name: string;
package/dist/test.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { f as VideoScene, e as VideoOrientation, g as VideoStyle, V as Video, a as VideoAudio, d as VideoInput } from './types-_t4bduwX.js';
2
- import { a as VideoWarning, b as VideoFinishReason } from './events-B-28kERX.js';
1
+ import { g as VideoScene, f as VideoOrientation, h as VideoStyle, V as Video, a as VideoAudio, d as VideoInput } from './types-CVMb6QEq.js';
2
+ import { a as VideoWarning, b as VideoFinishReason } from './events-tQ0x-VaL.js';
3
3
 
4
4
  type MockVideoStreamPart = {
5
5
  type: "scene.add";
package/dist/test.js CHANGED
@@ -233,7 +233,7 @@ async function* simulateVideoStream(parts, options = {}) {
233
233
  if (timeoutMs != null && (!Number.isFinite(timeoutMs) || timeoutMs < 0)) {
234
234
  throw new Error("Simulation timeoutMs must be a non-negative finite number");
235
235
  }
236
- const { createVideo } = await import("./compose-video-34GRZIF2.js");
236
+ const { createVideo } = await import("./compose-video-6UQ33DOO.js");
237
237
  const { createTextDeltaVideoPlanner } = await import("./text-stream-UPGUD2TD.js");
238
238
  const { BUILTIN_SERVER_TEMPLATE_KIT } = await import("./builtin-server-BRTZN4Q7.js");
239
239
  const { createTemplateSceneValidator } = await import("./validate-T7GBU2YF.js");
@@ -1,6 +1,7 @@
1
1
  declare const VIDEO_PROTOCOL_VERSION: "0.4";
2
2
  declare const VIDEO_SCHEMA_VERSION: "0.1";
3
3
  type VideoOrientation = "portrait" | "landscape";
4
+ type VideoKnowledgeMode = "input-only" | "general";
4
5
  interface VideoBrandColors {
5
6
  primary: string;
6
7
  secondary: string;
@@ -114,16 +115,18 @@ interface VideoSuppliedMedia {
114
115
  role?: "product" | "proof" | "background" | "logo";
115
116
  }
116
117
  interface VideoInput {
117
- /** Raw factual boundary: news, product updates, metrics, or an assistant answer. */
118
+ /** Raw source, request, or question for the video. */
118
119
  input: string;
119
- /** Optional creative direction. It never expands the factual boundary. */
120
+ /** Keep claims source-grounded by default, or allow stable model knowledge. */
121
+ knowledgeMode?: VideoKnowledgeMode;
122
+ /** Optional creative direction. It never changes the knowledge mode. */
120
123
  instructions?: string;
121
124
  maxDurationSec?: number;
122
125
  orientation?: VideoOrientation;
123
126
  /** Optional global visual direction. Omit for VanillaSky defaults. */
124
127
  style?: VideoStyleOptions;
125
- /** Optional deterministic opening copy. Scene details are inferred. */
126
- opening?: string;
128
+ /** Custom opening copy, or false to let the host show loading UI instead. Omit for the deterministic fallback. */
129
+ opening?: string | false;
127
130
  brand?: VideoBrandInput;
128
131
  /** Viewer or account context that may appear verbatim. It is data, never instructions. */
129
132
  personalization?: Record<string, unknown>;
@@ -206,4 +209,4 @@ interface VideoSnapshotRetention {
206
209
  suppliedMediaUrls?: boolean;
207
210
  }
208
211
 
209
- export { type Video as V, type VideoAudio as a, type VideoBackground as b, type VideoBrand as c, type VideoInput as d, type VideoOrientation as e, type VideoScene as f, type VideoStyle as g, type VideoStyleOptions as h, type VideoSuppliedMedia as i, type VideoCapabilities as j, VIDEO_PROTOCOL_VERSION as k, type VideoGenerationContext as l, type VideoPlanner as m, type VideoRequest as n, type VideoSceneValidator as o, type VideoTemplatePacing as p, type VideoSnapshotRetention as q, type VideoResumeCursor as r, type VideoSceneValidationContext as s };
212
+ export { type Video as V, type VideoAudio as a, type VideoBackground as b, type VideoBrand as c, type VideoInput as d, type VideoKnowledgeMode as e, type VideoOrientation as f, type VideoScene as g, type VideoStyle as h, type VideoStyleOptions as i, type VideoSuppliedMedia as j, type VideoCapabilities as k, VIDEO_PROTOCOL_VERSION as l, type VideoGenerationContext as m, type VideoPlanner as n, type VideoRequest as o, type VideoSceneValidator as p, type VideoTemplatePacing as q, type VideoSnapshotRetention as r, type VideoResumeCursor as s, type VideoSceneValidationContext as t };
package/docs/concepts.md CHANGED
@@ -21,8 +21,12 @@ or export pipeline when an encoded file is required.
21
21
  curated set of articles, metrics, events, notes, or an AI answer. It may be
22
22
  short, but the planner is designed to distill larger sources into a concise
23
23
  video rather than represent every fact;
24
+ - `knowledgeMode`: `input-only` by default, or `general` when stable model
25
+ knowledge may supplement the request;
24
26
  - `instructions`: optional creative direction that cannot override facts;
25
- - `opening`: optional copy for a deterministic opening that can play immediately;
27
+ - `opening`: optional custom copy for the deterministic opening; omission uses
28
+ `Creating your video...`, while `false` lets the host render loading UI
29
+ without adding an opening scene to the video;
26
30
  - `personalization`: application-defined fields such as name, role, account,
27
31
  period, goal, or onboarding partner;
28
32
  - `brand`: an optional background preset plus name, logo, font, surfaces, and
@@ -83,6 +83,8 @@ The file created by `vanillasky create` is a complete working template. Keep
83
83
  these concerns together:
84
84
 
85
85
  - `useWhen` and `avoidWhen` tell the AI when the visual is appropriate;
86
+ - `jobs` declares the narrative roles the template can fill, and decides
87
+ whether it may close a video;
86
88
  - `schema` defines allowed variables, validation, defaults, labels, and
87
89
  grounding formats;
88
90
  - named `examples` provide complete deterministic preview values;
@@ -90,6 +92,31 @@ these concerns together:
90
92
  - raw `progress`, dimensions, and `safeZone` make semantic state deterministic
91
93
  and layout safe in portrait and landscape.
92
94
 
95
+ ### Which templates may close a video
96
+
97
+ Every complete plan ends on exactly one closer, and the runtime only accepts a
98
+ closer whose `jobs` include `"ask"` or `"payoff"`. Marking any other template as
99
+ the closer rejects that scene with `Scene template <id> cannot be used as a
100
+ closer`, and the reserved closing time is given to a template that qualifies.
101
+
102
+ Declare `jobs: ["ask"]` on a call to action and `jobs: ["payoff"]` on a
103
+ celebration or resolution beat. A template that only presents evidence keeps
104
+ `jobs: ["proof"]` and is never asked to close.
105
+
106
+ The same rule reads the built-in catalog, so an application that wants to
107
+ constrain how its videos end can compute the eligible IDs instead of guessing:
108
+
109
+ ```ts
110
+ import { builtinTemplates } from "@vanillaskyai/video/templates/catalog";
111
+
112
+ const closerTemplateIds = builtinTemplates
113
+ .filter((template) => template.jobs.some((job) => job === "ask" || job === "payoff"))
114
+ .map((template) => template.id);
115
+ ```
116
+
117
+ Pacing uses the same signal: the planner reserves time for the closer whenever
118
+ the selected catalog contains a template with one of those two jobs.
119
+
93
120
  Customer templates hard-cut by default. Opt into renderer-owned fades only
94
121
  for scenes that use the standard media-background variables, and only after
95
122
  both timing points are visually tested in portrait and landscape. Shared brand
@@ -194,7 +221,7 @@ import { templates } from "../vanillasky/server";
194
221
  export const handleVideo = createVideoHandler({
195
222
  templates,
196
223
  authorize: (request) => {
197
- if (process.env.NODE_ENV !== "development") return false;
224
+ if (process.env.VANILLASKY_LOCAL_DEMO !== "1") return false;
198
225
  const hostname = new URL(request.url).hostname;
199
226
  return hostname === "localhost" || hostname === "127.0.0.1";
200
227
  },
@@ -207,8 +234,9 @@ export const handleVideo = createVideoHandler({
207
234
  });
208
235
  ```
209
236
 
210
- The local-only authorization above is intentionally narrow. Replace it with
211
- your application's session check before deployment, as shown in
237
+ The local-only authorization above is intentionally narrow. Supply its marker
238
+ only from the development command; replace it with your application's session
239
+ check before deployment, as shown in
212
240
  [Getting started](getting-started.md).
213
241
 
214
242
  Use the browser registry for generation and playback:
@@ -76,15 +76,19 @@ appear in the story.
76
76
  first validated scene arrives. The cover is player state, not video content: it
77
77
  is never written to the event log, replay, or export.
78
78
 
79
- An optional opening is deterministic and should not wait for an LLM or remote media:
79
+ The opening is deterministic and should not wait for an LLM or remote media.
80
+ Omit it to use `Creating your video...`, or supply one concise custom sentence:
80
81
 
81
82
  ```ts
82
- opening: "Joris, your Q2 recap is ready."
83
+ opening: "Your Q2 customer impact recap is ready."
83
84
  ```
84
85
 
86
+ Pass `opening: false` to omit the persisted opening and show application-owned
87
+ loading UI until the first generated scene arrives.
88
+
85
89
  Use `opening` only for a genuine opening that should remain in the completed
86
- response. VanillaSky infers the scene ID, `notification` template, variables,
87
- and five-second timing. You do not need to create a generic loading scene.
90
+ response. VanillaSky infers the scene ID, `media` template, gradient variables,
91
+ and three-second timing. Keep generic loading state in the host UI instead.
88
92
 
89
93
  ## Aspect ratio and responsive layout
90
94
 
@@ -5,7 +5,7 @@
5
5
  Install VanillaSky:
6
6
 
7
7
  ```bash
8
- npm install @vanillaskyai/video@0.2.0 ai @ai-sdk/openai
8
+ npm install @vanillaskyai/video@0.3.1 ai @ai-sdk/openai
9
9
  ```
10
10
 
11
11
  Set your provider key in `.env.local` (never commit it):
@@ -29,7 +29,7 @@ const model = openai(process.env.OPENAI_MODEL ?? "gpt-4.1");
29
29
  const handle = createVideoHandler({
30
30
  // Local development only. Replace with your session check before deploying.
31
31
  authorize: (request) => {
32
- if (process.env.NODE_ENV !== "development") return false;
32
+ if (process.env.VANILLASKY_LOCAL_DEMO !== "1") return false;
33
33
  const hostname = new URL(request.url).hostname;
34
34
  return hostname === "localhost" || hostname === "127.0.0.1";
35
35
  },
@@ -45,11 +45,12 @@ export const POST = handle;
45
45
  export const OPTIONS = handle;
46
46
  ```
47
47
 
48
- The local bypass is intentionally fail-closed: it accepts only localhost while
49
- Next.js is in development and denies every production request. Replace it with
50
- your real session validation before deploying. For literal files and commands,
48
+ The local bypass is intentionally fail-closed: the packaged development command
49
+ sets its marker only for `next dev`, and it accepts only localhost. Every
50
+ production request is denied. Replace it with your real session validation
51
+ before deploying. For literal files and commands,
51
52
  use the tested
52
- [`examples/nextjs-quickstart` directory](https://github.com/VanillaSkyAi/video/tree/v0.2.0/examples/nextjs-quickstart).
53
+ [`examples/nextjs-quickstart` directory](https://github.com/VanillaSkyAi/video/tree/v0.3.1/examples/nextjs-quickstart).
53
54
 
54
55
  `model` can come from any AI SDK provider, registry, gateway, compatible API,
55
56
  or custom implementation. The application can choose a cheaper or faster model
@@ -8,10 +8,11 @@
8
8
  import type { VideoInput } from "@vanillaskyai/video";
9
9
 
10
10
  const input: VideoInput = {
11
- input: "Joris completed 142 customer conversations in Q2.",
11
+ input: "Maya completed 142 customer conversations in Q2.",
12
+ knowledgeMode: "input-only",
12
13
  instructions: "Celebrate the result. Never alter a metric.",
13
- opening: "Joris, your Q2 recap is ready.",
14
- personalization: { firstName: "Joris", period: "Q2" },
14
+ opening: "Maya, your Q2 customer impact recap is ready.",
15
+ personalization: { firstName: "Maya", period: "Q2", role: "Product leader" },
15
16
  brand,
16
17
  suppliedMedia,
17
18
  audio: { src: "/audio/calm.mp3" },
@@ -22,18 +23,24 @@ const input: VideoInput = {
22
23
 
23
24
  ## Raw source
24
25
 
25
- Put every fact the response may claim in `input`. Use plain text, compact JSON, or a server-produced digest. Include units, periods, comparison bases, and provenance identifiers where ambiguity is possible. Keep creative direction in `instructions`; it may change emphasis and tone but cannot expand the factual boundary.
26
+ In the default `input-only` mode, put every fact the response may claim in
27
+ `input`. Use plain text, compact JSON, or a server-produced digest. Include
28
+ units, periods, comparison bases, and provenance identifiers where ambiguity
29
+ is possible. Choose `knowledgeMode: "general"` only when the model should use
30
+ stable general knowledge to answer a question or develop content. Creative
31
+ direction in `instructions` never changes that setting.
26
32
 
27
33
  Bound request bytes and reject secret-shaped fields on the server. Do not pass provider keys, authorization headers, internal prompt fragments, or storage credentials as source material.
28
34
 
29
35
  ## Opening
30
36
 
31
- When supplied, `opening` becomes a deterministic `media` scene emitted before
32
- provider work. It contains only the supplied sentence and explicitly uses the
33
- brand gradient without stock media. The SDK requests a three-second opening,
34
- then applies its normal readability and overall-duration budget. It owns the
35
- scene ID, template, variables, and timing so callers only provide the copy. It
36
- should:
37
+ `opening` becomes a deterministic `media` scene emitted before provider work.
38
+ When callers omit it, the SDK uses `Creating your video...`; a supplied value
39
+ replaces that fallback. The scene contains only that sentence and explicitly
40
+ uses the brand gradient without stock media. The SDK requests a three-second
41
+ opening, then applies its normal readability and overall-duration budget. It
42
+ owns the scene ID, template, variables, and timing so callers only provide
43
+ optional custom copy. That copy should:
37
44
 
38
45
  - be personal or situational enough to feel intentional;
39
46
  - require no network media lookup;
@@ -41,4 +48,17 @@ should:
41
48
  - be one concise sentence that fits comfortably in both supported orientations;
42
49
  - be part of the final story, not a spinner disguised as a scene.
43
50
 
44
- Omit `opening` when the generated story should begin with its first planned scene.
51
+ Pass `opening: false` when the application should own the waiting experience:
52
+
53
+ ```ts
54
+ opening: false
55
+ ```
56
+
57
+ The SDK then starts with an empty timeline and emits the first validated,
58
+ generated scene as soon as it is ready. Render a transient loading state in the
59
+ host application until `video.video?.scenes.length` is non-zero. That loading
60
+ state is not persisted in the event stream or completed video. A generated
61
+ scene is still required; a planner that completes without one fails the run.
62
+
63
+ The deterministic opening is runtime-owned, so it remains available even when `templateIds`
64
+ does not let the planner select `media` for generated body scenes.