@vanillaskyai/video 0.1.1 → 0.3.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 (52) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/PUBLIC-API.md +20 -2
  3. package/README.md +14 -6
  4. package/dist/{builtin-server-F7YXRVCH.js → builtin-server-BRTZN4Q7.js} +2 -2
  5. package/dist/check-runtime.js +1 -1
  6. package/dist/{chunk-EVG2ZYIY.js → chunk-3O7OMMMF.js} +7 -4
  7. package/dist/{chunk-GRMUV7QY.js → chunk-BKF3A357.js} +1 -1
  8. package/dist/{chunk-YC5MFXA5.js → chunk-GR4I3MN2.js} +37 -15
  9. package/dist/{chunk-A2X45FET.js → chunk-I5YWVQ5R.js} +1 -1
  10. package/dist/{chunk-ERY4S6QV.js → chunk-JW47XCRL.js} +6 -1
  11. package/dist/{chunk-XB7LH4J2.js → chunk-QTMHS7JD.js} +2 -1
  12. package/dist/{chunk-W2XJKG6C.js → chunk-RBQN3VSY.js} +2 -2
  13. package/dist/{chunk-OOPIETLW.js → chunk-S5ZU5WUU.js} +1 -1
  14. package/dist/{chunk-ABPKM4GK.js → chunk-STIFILQG.js} +1 -1
  15. package/dist/chunk-WHPZED7X.js +57 -0
  16. package/dist/{chunk-CHLGW22F.js → chunk-XAMUOSX7.js} +3 -3
  17. package/dist/{chunk-IXCUY3B3.js → chunk-XYZOJ5NZ.js} +195 -52
  18. package/dist/cli.js +5 -5
  19. package/dist/{compose-video-CN3VSWST.js → compose-video-TQOHDXNW.js} +4 -4
  20. package/dist/{events-_wfjbmEp.d.ts → events-CTIsANzz.d.ts} +2 -2
  21. package/dist/index.d.ts +2 -2
  22. package/dist/{kit-BMNcHqa9.d.ts → kit-DA2cfJ96.d.ts} +1 -1
  23. package/dist/react.d.ts +7 -4
  24. package/dist/react.js +226 -30
  25. package/dist/server.d.ts +21 -4
  26. package/dist/server.js +205 -18
  27. package/dist/system-prompt-NMYHLAMJ.js +10 -0
  28. package/dist/template-catalog.js +1 -1
  29. package/dist/templates.d.ts +3 -3
  30. package/dist/test.d.ts +3 -2
  31. package/dist/test.js +3 -3
  32. package/dist/{text-stream-6XHC7J47.js → text-stream-UPGUD2TD.js} +2 -2
  33. package/dist/{types-B-PZR6G_.d.ts → types-CkO2EYr4.d.ts} +8 -4
  34. package/docs/concepts.md +11 -1
  35. package/docs/custom-templates.md +34 -6
  36. package/docs/customization.md +2 -1
  37. package/docs/errors.md +3 -1
  38. package/docs/getting-started.md +13 -10
  39. package/docs/input-and-first-scene.md +17 -6
  40. package/docs/integrate-nextjs.md +6 -3
  41. package/docs/media-and-audio.md +73 -14
  42. package/docs/prompt-and-input.md +27 -5
  43. package/docs/provider-integration.md +45 -2
  44. package/docs/reference/protocol.md +9 -0
  45. package/examples/nextjs-quickstart/README.md +3 -1
  46. package/examples/nextjs-quickstart/package.json +3 -2
  47. package/examples/nextjs-quickstart/src/app/api/video/route.ts +1 -1
  48. package/package.json +1 -1
  49. package/registry/items/brandMessage.json +1 -2
  50. package/registry/items/media.json +2 -1
  51. package/dist/chunk-FQ47WXAJ.js +0 -38
  52. package/dist/system-prompt-A6UXNTPK.js +0 -8
package/dist/server.js CHANGED
@@ -1,27 +1,27 @@
1
1
  import {
2
2
  createTextDeltaVideoPlanner
3
- } from "./chunk-GRMUV7QY.js";
3
+ } from "./chunk-BKF3A357.js";
4
4
  import {
5
5
  createVideo
6
- } from "./chunk-IXCUY3B3.js";
6
+ } from "./chunk-XYZOJ5NZ.js";
7
7
  import "./chunk-E7CL7UPB.js";
8
8
  import {
9
9
  BUILTIN_SERVER_TEMPLATE_KIT,
10
10
  createServerTemplateRegistry,
11
11
  overlayServerTemplateRegistry
12
- } from "./chunk-A2X45FET.js";
12
+ } from "./chunk-I5YWVQ5R.js";
13
13
  import {
14
14
  createTemplateSceneValidator
15
15
  } from "./chunk-LMG3GHLL.js";
16
16
  import {
17
17
  encodeVideoSseEvent,
18
18
  videoSseHeaders
19
- } from "./chunk-OOPIETLW.js";
20
- import "./chunk-ABPKM4GK.js";
19
+ } from "./chunk-S5ZU5WUU.js";
20
+ import "./chunk-STIFILQG.js";
21
21
  import {
22
22
  parseVideoEvent,
23
23
  safePublicDiagnostic
24
- } from "./chunk-ERY4S6QV.js";
24
+ } from "./chunk-JW47XCRL.js";
25
25
  import {
26
26
  MAX_RETAINED_MEDIA_URL_LENGTH
27
27
  } from "./chunk-GHJEAP4O.js";
@@ -29,14 +29,13 @@ import {
29
29
  VIDEO_PROTOCOL_VERSION
30
30
  } from "./chunk-EE6PK6XC.js";
31
31
  import {
32
- createTemplateSystemPrompt
33
- } from "./chunk-YC5MFXA5.js";
34
- import {
35
- DEFAULT_VIDEO_SYSTEM_PROMPT
36
- } from "./chunk-FQ47WXAJ.js";
37
- import "./chunk-EVG2ZYIY.js";
32
+ createTemplateSystemPrompt,
33
+ getStandardMediaResolverContract
34
+ } from "./chunk-GR4I3MN2.js";
35
+ import "./chunk-WHPZED7X.js";
36
+ import "./chunk-3O7OMMMF.js";
38
37
  import "./chunk-352ZZCET.js";
39
- import "./chunk-CHLGW22F.js";
38
+ import "./chunk-XAMUOSX7.js";
40
39
  import "./chunk-2E6T633S.js";
41
40
  import {
42
41
  validateBrandInput
@@ -130,6 +129,7 @@ function parseVideoRequest(value) {
130
129
  const input = record(request.input, "request.input");
131
130
  allowedKeys(input, [
132
131
  "input",
132
+ "knowledgeMode",
133
133
  "instructions",
134
134
  "maxDurationSec",
135
135
  "orientation",
@@ -141,6 +141,14 @@ function parseVideoRequest(value) {
141
141
  "audio"
142
142
  ], "request.input");
143
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
+ }
144
152
  if (input.instructions != null) string(input.instructions, "request.input.instructions");
145
153
  if (input.maxDurationSec != null) {
146
154
  numberBetween(input.maxDurationSec, 5, 120, "request.input.maxDurationSec");
@@ -397,10 +405,11 @@ function createVideoStreamHandler(options) {
397
405
  validateScene: options.validateScene,
398
406
  getTemplatePacing: options.getTemplatePacing,
399
407
  invalidPartBehavior: options.invalidPartBehavior ?? "drop",
408
+ requireCloser: options.requireCloser,
400
409
  onError: options.onError,
401
410
  onWarning: options.onWarning,
402
411
  onComplete: options.onComplete,
403
- systemPrompt: systemPrompt ?? DEFAULT_VIDEO_SYSTEM_PROMPT,
412
+ systemPrompt,
404
413
  selectAudio: options.selectAudio,
405
414
  snapshotRetention: options.snapshotRetention,
406
415
  signal: request.signal,
@@ -459,6 +468,155 @@ function createVideoStreamHandler(options) {
459
468
  };
460
469
  }
461
470
 
471
+ // src/server/media-resolver.ts
472
+ var MAX_MEDIA_QUERY_CHARACTERS = 80;
473
+ var MAX_MEDIA_QUERY_WORDS = 8;
474
+ function queryIsBounded(query) {
475
+ const length = [...query].length;
476
+ const words = query.match(/\S+/gu)?.length ?? 0;
477
+ return length >= 2 && length <= MAX_MEDIA_QUERY_CHARACTERS && words <= MAX_MEDIA_QUERY_WORDS;
478
+ }
479
+ function preferredType(value) {
480
+ if (value === "photo") return "image";
481
+ if (value === "video") return "video";
482
+ return "any";
483
+ }
484
+ function cleanUrl(value, field) {
485
+ if (value == null || value === "") return void 0;
486
+ if (typeof value !== "string" || !value.trim() || value.length > MAX_RETAINED_MEDIA_URL_LENGTH) {
487
+ throw new Error(`Resolved media ${field} is invalid`);
488
+ }
489
+ return value.trim();
490
+ }
491
+ function fallbackVariables(variables) {
492
+ const fallback = { ...variables };
493
+ delete fallback.mediaKeyword;
494
+ delete fallback.mediaUrl;
495
+ delete fallback.mediaPoster;
496
+ fallback.mediaType = "gradient";
497
+ return fallback;
498
+ }
499
+ function isAbortError(error) {
500
+ return error instanceof Error && error.name === "AbortError";
501
+ }
502
+ async function resolveVariables(options) {
503
+ if (!Object.hasOwn(options.variables, "mediaKeyword")) return options.variables;
504
+ const variables = { ...options.variables };
505
+ const rawQuery = typeof variables.mediaKeyword === "string" ? variables.mediaKeyword.trim() : "";
506
+ delete variables.mediaKeyword;
507
+ if (!options.contract) return variables;
508
+ if (!options.openingReady || !options.resolveMedia || !queryIsBounded(rawQuery)) {
509
+ return fallbackVariables(variables);
510
+ }
511
+ let resolved;
512
+ try {
513
+ resolved = await options.resolveMedia(rawQuery, {
514
+ input: options.input,
515
+ templateId: options.templateId,
516
+ preferredType: preferredType(variables.mediaType),
517
+ signal: options.signal
518
+ });
519
+ } catch (error) {
520
+ if (options.signal.aborted) throw options.signal.reason ?? error;
521
+ if (isAbortError(error)) throw error;
522
+ return fallbackVariables(variables);
523
+ }
524
+ if (options.signal.aborted) {
525
+ throw options.signal.reason ?? new DOMException("The media request was aborted", "AbortError");
526
+ }
527
+ if (!resolved) return fallbackVariables(variables);
528
+ if (resolved.type !== "image" && resolved.type !== "video") {
529
+ throw new Error("Resolved media type is invalid");
530
+ }
531
+ const url = cleanUrl(resolved.url, "URL");
532
+ if (!url) throw new Error("Resolved media URL is required");
533
+ const posterUrl = options.contract.acceptsPoster ? cleanUrl(resolved.posterUrl, "poster URL") : void 0;
534
+ options.approveUrl(options.input, url);
535
+ if (posterUrl) options.approveUrl(options.input, posterUrl);
536
+ variables.mediaUrl = url;
537
+ variables.mediaType = resolved.type === "image" ? "photo" : "video";
538
+ if (posterUrl) variables.mediaPoster = posterUrl;
539
+ else delete variables.mediaPoster;
540
+ return variables;
541
+ }
542
+ function templateContract(templates, templateId) {
543
+ const metadata = templates.getTemplateMetadata(templateId);
544
+ return metadata == null ? void 0 : getStandardMediaResolverContract(metadata.schema);
545
+ }
546
+ function withoutKeyword(variables) {
547
+ if (!Object.hasOwn(variables, "mediaKeyword")) return variables;
548
+ const clean = { ...variables };
549
+ delete clean.mediaKeyword;
550
+ return clean;
551
+ }
552
+ function sanitizeUnknownTemplatePart(part) {
553
+ if (part.type === "scene.add") {
554
+ return { ...part, scene: { ...part.scene, variables: withoutKeyword(part.scene.variables) } };
555
+ }
556
+ if (part.type === "scene.patch" && part.patch.variables) {
557
+ return { ...part, patch: { ...part.patch, variables: withoutKeyword(part.patch.variables) } };
558
+ }
559
+ if (part.type === "asset.patch") {
560
+ return { ...part, variables: withoutKeyword(part.variables) };
561
+ }
562
+ return part;
563
+ }
564
+ async function resolvePartVariables(options) {
565
+ if (!options.templateId) return sanitizeUnknownTemplatePart(options.part);
566
+ const shared = {
567
+ contract: templateContract(options.templates, options.templateId),
568
+ input: options.input,
569
+ templateId: options.templateId,
570
+ signal: options.signal,
571
+ resolveMedia: options.resolveMedia,
572
+ approveUrl: options.approveUrl,
573
+ openingReady: options.openingReady
574
+ };
575
+ if (options.part.type === "scene.add") {
576
+ const variables = await resolveVariables({
577
+ ...shared,
578
+ variables: options.part.scene.variables
579
+ });
580
+ return { ...options.part, scene: { ...options.part.scene, variables } };
581
+ }
582
+ if (options.part.type === "scene.patch" && options.part.patch.variables) {
583
+ const variables = await resolveVariables({
584
+ ...shared,
585
+ variables: options.part.patch.variables
586
+ });
587
+ return { ...options.part, patch: { ...options.part.patch, variables } };
588
+ }
589
+ if (options.part.type === "asset.patch") {
590
+ const variables = await resolveVariables({
591
+ ...shared,
592
+ variables: options.part.variables
593
+ });
594
+ return { ...options.part, variables };
595
+ }
596
+ return options.part;
597
+ }
598
+ function createMediaResolvingPlanner(options) {
599
+ return async function* resolveMediaPlan(context) {
600
+ const sceneTemplates = /* @__PURE__ */ new Map();
601
+ for await (const part of options.planner(context)) {
602
+ const templateId = part.type === "scene.add" ? part.scene.templateId : part.type === "scene.patch" || part.type === "asset.patch" ? sceneTemplates.get(part.sceneId) : void 0;
603
+ if (part.type === "scene.add") {
604
+ sceneTemplates.set(part.scene.id, part.scene.templateId);
605
+ }
606
+ yield await resolvePartVariables({
607
+ part,
608
+ templateId,
609
+ input: context.request.input,
610
+ signal: context.signal,
611
+ templates: options.templates,
612
+ resolveMedia: options.resolveMedia,
613
+ approveUrl: options.approveUrl,
614
+ openingReady: options.isOpeningReady(context.request.input)
615
+ });
616
+ }
617
+ };
618
+ }
619
+
462
620
  // src/server/create-video-handler.ts
463
621
  function createVideoHandler(options) {
464
622
  if (options?.authorize !== "none" && typeof options?.authorize !== "function") {
@@ -473,22 +631,51 @@ function createVideoHandler(options) {
473
631
  includeRawProviderData,
474
632
  basePrompt,
475
633
  allowMediaUrl,
634
+ resolveMedia,
635
+ requireCloser = true,
476
636
  ...handlerOptions
477
637
  } = options;
478
638
  const templates = configuredTemplates ? overlayServerTemplateRegistry(BUILTIN_SERVER_TEMPLATE_KIT, configuredTemplates) : BUILTIN_SERVER_TEMPLATE_KIT;
639
+ const approvedMediaUrls = /* @__PURE__ */ new WeakMap();
640
+ const openingReadyInputs = /* @__PURE__ */ new WeakSet();
641
+ const approveUrl = (input, url) => {
642
+ const approved = approvedMediaUrls.get(input) ?? /* @__PURE__ */ new Set();
643
+ approved.add(url);
644
+ approvedMediaUrls.set(input, approved);
645
+ };
646
+ const planner = createTextDeltaVideoPlanner({ streamText, includeRawProviderData });
647
+ const validateTemplateScene = createTemplateSceneValidator({
648
+ kit: templates,
649
+ allowMediaUrl: (url, context) => approvedMediaUrls.get(context.input)?.has(url) === true || allowMediaUrl?.(url, context) === true
650
+ });
479
651
  return createVideoStreamHandler({
480
652
  ...handlerOptions,
481
- generate: createTextDeltaVideoPlanner({ streamText, includeRawProviderData }),
482
- systemPrompt: ({ capabilities: capabilities2 }) => {
653
+ requireCloser,
654
+ generate: createMediaResolvingPlanner({
655
+ planner,
656
+ templates,
657
+ resolveMedia,
658
+ approveUrl,
659
+ isOpeningReady: (input) => openingReadyInputs.has(input)
660
+ }),
661
+ systemPrompt: ({ request, capabilities: capabilities2 }) => {
483
662
  const selectedIds = capabilities2?.templates == null ? void 0 : new Set(capabilities2.templates);
484
663
  const selectedTemplates = selectedIds == null ? templates.listTemplateMetadata() : templates.listTemplateMetadata().filter(({ id }) => selectedIds.has(id));
485
664
  return createTemplateSystemPrompt({
486
665
  kit: { listTemplateMetadata: () => selectedTemplates },
487
- basePrompt
666
+ basePrompt,
667
+ knowledgeMode: request.input.knowledgeMode,
668
+ mediaResolverAvailable: resolveMedia != null
488
669
  });
489
670
  },
490
671
  supportedCapabilities: templates.capabilities,
491
- validateScene: createTemplateSceneValidator({ kit: templates, allowMediaUrl }),
672
+ validateScene: (scene, context) => {
673
+ validateTemplateScene(scene, context);
674
+ const isAsk = templates.getTemplateMetadata(scene.templateId)?.jobs?.includes("ask") === true;
675
+ if (!isAsk) {
676
+ openingReadyInputs.add(context.input);
677
+ }
678
+ },
492
679
  getTemplatePacing: (templateId) => templates.getTemplateMetadata(templateId)
493
680
  });
494
681
  }
@@ -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,6 +1,6 @@
1
1
  import {
2
2
  BUILTIN_TEMPLATE_MANIFEST
3
- } from "./chunk-W2XJKG6C.js";
3
+ } from "./chunk-RBQN3VSY.js";
4
4
  export {
5
5
  BUILTIN_TEMPLATE_MANIFEST as builtinTemplates
6
6
  };
@@ -1,9 +1,9 @@
1
- import { I as InferTemplateJsonSchema, S as SceneTemplateProps, a as SceneTemplate } from './kit-BMNcHqa9.js';
2
- export { T as TemplateRegistry, c as createTemplateRegistry } from './kit-BMNcHqa9.js';
1
+ import { I as InferTemplateJsonSchema, S as SceneTemplateProps, a as SceneTemplate } from './kit-DA2cfJ96.js';
2
+ export { T as TemplateRegistry, c as createTemplateRegistry } from './kit-DA2cfJ96.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-B-PZR6G_.js';
6
+ import './types-CkO2EYr4.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,8 +1,9 @@
1
- import { f as VideoScene, e as VideoOrientation, g as VideoStyle, V as Video, a as VideoAudio, d as VideoInput } from './types-B-PZR6G_.js';
2
- import { a as VideoWarning, b as VideoFinishReason } from './events-_wfjbmEp.js';
1
+ import { g as VideoScene, f as VideoOrientation, h as VideoStyle, V as Video, a as VideoAudio, d as VideoInput } from './types-CkO2EYr4.js';
2
+ import { a as VideoWarning, b as VideoFinishReason } from './events-CTIsANzz.js';
3
3
 
4
4
  type MockVideoStreamPart = {
5
5
  type: "scene.add";
6
+ placement?: "closer";
6
7
  scene: VideoScene;
7
8
  } | {
8
9
  type: "scene.patch";
package/dist/test.js CHANGED
@@ -233,9 +233,9 @@ 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-CN3VSWST.js");
237
- const { createTextDeltaVideoPlanner } = await import("./text-stream-6XHC7J47.js");
238
- const { BUILTIN_SERVER_TEMPLATE_KIT } = await import("./builtin-server-F7YXRVCH.js");
236
+ const { createVideo } = await import("./compose-video-TQOHDXNW.js");
237
+ const { createTextDeltaVideoPlanner } = await import("./text-stream-UPGUD2TD.js");
238
+ const { BUILTIN_SERVER_TEMPLATE_KIT } = await import("./builtin-server-BRTZN4Q7.js");
239
239
  const { createTemplateSceneValidator } = await import("./validate-T7GBU2YF.js");
240
240
  const controller = new AbortController();
241
241
  const forwardAbort = () => controller.abort(options.signal?.reason);
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createTextDeltaVideoPlanner
3
- } from "./chunk-GRMUV7QY.js";
3
+ } from "./chunk-BKF3A357.js";
4
4
  import "./chunk-E7CL7UPB.js";
5
- import "./chunk-ERY4S6QV.js";
5
+ import "./chunk-JW47XCRL.js";
6
6
  import "./chunk-EE6PK6XC.js";
7
7
  import "./chunk-XGRA2MUV.js";
8
8
  export {
@@ -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,15 +115,17 @@ 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. */
128
+ /** Optional custom opening copy. Omit for the deterministic "Creating your video..." fallback. */
126
129
  opening?: string;
127
130
  brand?: VideoBrandInput;
128
131
  /** Viewer or account context that may appear verbatim. It is data, never instructions. */
@@ -159,6 +162,7 @@ interface VideoGenerationContext {
159
162
  }
160
163
  type VideoPlanPart = {
161
164
  type: "scene.add";
165
+ placement?: "closer";
162
166
  scene: VideoScene;
163
167
  } | {
164
168
  type: "scene.patch";
@@ -205,4 +209,4 @@ interface VideoSnapshotRetention {
205
209
  suppliedMediaUrls?: boolean;
206
210
  }
207
211
 
208
- 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,11 @@ 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...`;
26
29
  - `personalization`: application-defined fields such as name, role, account,
27
30
  period, goal, or onboarding partner;
28
31
  - `brand`: an optional background preset plus name, logo, font, surfaces, and
@@ -54,6 +57,12 @@ another streaming text model.
54
57
  The planner does not create public event IDs, checksums, revisions, or final
55
58
  snapshots. The runtime owns those guarantees.
56
59
 
60
+ `createVideoHandler` asks the planner to emit one early
61
+ `scene.add` with `placement: "closer"` after the first playable body scene.
62
+ That placement is planner-only: the runtime holds the validated closer,
63
+ reserves its readable duration while later body scenes stream, and appends it
64
+ last without persisting `placement` into the completed `Video`.
65
+
57
66
  | Boundary | Type | Owner |
58
67
  | --- | --- | --- |
59
68
  | Planner parts | Internal validated plan data | Server/provider adapter |
@@ -73,6 +82,7 @@ audio.set optional, before any scene
73
82
  scene.add supplied opening
74
83
  scene.add generated body
75
84
  scene.add generated body
85
+ scene.add reserved closer
76
86
  response.complete exact terminal snapshot
77
87
  ```
78
88
 
@@ -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:
@@ -311,9 +339,9 @@ Templates whose core proof needs a real statistic can add
311
339
  `"x-vanillasky": { "requiresStat": true }`. See the structured-data and
312
340
  supplied-media references for complete examples.
313
341
 
314
- The automatic `opening` uses the built-in `notification` variables (`appName`
315
- and `message`). If you replace `notification` while using automatic openings,
316
- keep that variable contract.
342
+ The automatic `opening` uses the built-in `media` variables (`texts` and
343
+ `mediaType: "gradient"`). If you replace `media` while using automatic
344
+ openings, keep that variable contract.
317
345
 
318
346
  ## What belongs where
319
347
 
@@ -76,7 +76,8 @@ 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
83
  opening: "Joris, your Q2 recap is ready."
package/docs/errors.md CHANGED
@@ -35,7 +35,9 @@ try {
35
35
  `response.error` or `response.abort` rejects it. The hook still retains the
36
36
  latest validated `video.video`, and its player stream still receives terminal
37
37
  events, so an application may keep already accepted scenes visible while it
38
- offers a retry.
38
+ offers a retry. If the planner already supplied a validated reserved closer,
39
+ the runtime appends it to that playable terminal snapshot before reporting a
40
+ late provider failure; it never invents or rewrites closer copy during recovery.
39
41
 
40
42
  Both a server `response.abort` and an explicit `video.abort(reason)` keep
41
43
  `video.status` at `aborted`; `video.error` contains the same safe typed abort
@@ -5,7 +5,7 @@
5
5
  Install VanillaSky:
6
6
 
7
7
  ```bash
8
- npm install @vanillaskyai/video@0.1.1 ai @ai-sdk/openai
8
+ npm install @vanillaskyai/video@0.3.0 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.1.1/examples/nextjs-quickstart).
53
+ [`examples/nextjs-quickstart` directory](https://github.com/VanillaSkyAi/video/tree/v0.3.0/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
@@ -90,10 +91,12 @@ reactive UI.
90
91
 
91
92
  `video.status` is `idle`, `streaming`, `complete`, `error`, or `aborted`.
92
93
  `video.video` is the latest deterministic video, and `video.warnings` contains
93
- bounded typed diagnostics safe to show or branch on. Provider limits and
94
- content filters are available to the server through the `onComplete` summary;
95
- surface that server-owned state separately if completeness matters to your
96
- product.
94
+ bounded typed diagnostics safe to show or branch on. A playable response that
95
+ stops at a planner length limit includes a `plan_incomplete` warning because
96
+ requested scenes or the ending may be missing. Provider finish reasons and
97
+ content-filter details remain available to the server through the `onComplete`
98
+ summary; surface that server-owned state separately when completeness matters
99
+ to your product.
97
100
 
98
101
  Persist a completed `Video` as JSON and play it later without another model
99
102
  request:
@@ -9,6 +9,7 @@ import type { VideoInput } from "@vanillaskyai/video";
9
9
 
10
10
  const input: VideoInput = {
11
11
  input: "Joris completed 142 customer conversations in Q2.",
12
+ knowledgeMode: "input-only",
12
13
  instructions: "Celebrate the result. Never alter a metric.",
13
14
  opening: "Joris, your Q2 recap is ready.",
14
15
  personalization: { firstName: "Joris", period: "Q2" },
@@ -22,20 +23,30 @@ 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 five-second `notification`
32
- scene emitted before provider work. The SDK owns its scene ID, template, variables,
33
- and timing so callers only provide the copy. It 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:
34
44
 
35
45
  - be personal or situational enough to feel intentional;
36
46
  - require no network media lookup;
37
47
  - remain true if generation later fails;
38
- - fit comfortably in both supported orientations;
48
+ - be one concise sentence that fits comfortably in both supported orientations;
39
49
  - be part of the final story, not a spinner disguised as a scene.
40
50
 
41
- Omit `opening` when the generated story should begin with its first planned scene.
51
+ The opening is runtime-owned, so it remains available even when `templateIds`
52
+ does not let the planner select `media` for generated body scenes.