@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/CHANGELOG.md CHANGED
@@ -4,7 +4,41 @@ VanillaSky follows semantic versioning. This changelog begins with the 0.1 beta.
4
4
 
5
5
  ## Unreleased
6
6
 
7
- <!-- Add release notes for the next version here. -->
7
+ ## 0.3.0
8
+
9
+ - Documents closer eligibility: a template may close a video only when its
10
+ `jobs` include `"ask"` or `"payoff"`, with the catalog filter an application
11
+ can use to constrain how its videos end.
12
+ - Documents provider reasoning and effort controls for planning: models that
13
+ reason by default add that time directly to the first generated scene, so
14
+ hosts that want a video to start quickly should disable extended reasoning
15
+ and tune effort against `timeToFirstSceneMs` and `rejectedSceneCount`.
16
+ - Uses a three-second, gradient-backed `media` opening with
17
+ `Creating your video...` whenever `VideoInput.opening` is omitted, while
18
+ preserving supplied opening copy and keeping body-template selection
19
+ independent from the runtime-owned opening.
20
+ - Adds an explicit `knowledgeMode` input: source-grounded `input-only` remains
21
+ the default, while `general` lets chat-style video responses use stable model
22
+ knowledge under bounded safety and factuality rules.
23
+
24
+ ## 0.2.0
25
+
26
+ - Adds an application-owned `resolveMedia` hook that turns bounded semantic
27
+ media intent into approved image or video backgrounds without exposing
28
+ provider credentials, unresolved queries, or untrusted URLs to clients.
29
+ - Requires one grounded closer by default, holds it while body scenes stream,
30
+ and emits it last so complete videos finish on a payoff or supplied call to
31
+ action instead of an arbitrary body scene.
32
+ - Improves planning for rich inputs with adaptive scene counts, coherent
33
+ multi-entry sequencing, reusable best-fit templates, and explicit partial
34
+ completion warnings when provider or duration limits truncate the plan.
35
+ - Adds `VideoPlaybackMode` with sound-first interaction, repeat-stream
36
+ autoplay, manual, muted-autoplay, and immediate-autoplay policies.
37
+ - Renders `VideoInput.opening` as a deterministic, asset-free gradient media
38
+ scene and preserves it as the visible start poster before playback.
39
+ - Resets replacement streams as fresh playback sessions, keeps completed end
40
+ frames stable, and provides a replay control instead of replaying exit
41
+ animation at the terminal boundary.
8
42
 
9
43
  ## 0.1.1
10
44
 
package/PUBLIC-API.md CHANGED
@@ -1,6 +1,6 @@
1
- # VanillaSky Video 0.1 public API
1
+ # VanillaSky Video 0.2 public API
2
2
 
3
- Status: frozen public beta contract for `0.1.1`.
3
+ Status: frozen public beta contract for `0.3.0`.
4
4
 
5
5
  This document defines the API that may enter the fresh
6
6
  `@vanillaskyai/video` package. An export not listed here is internal. Tests and
@@ -97,6 +97,9 @@ provider-neutral text-delta escape hatch.
97
97
  ### Types
98
98
 
99
99
  - `VideoHandlerOptions`
100
+ - `MediaResolver`
101
+ - `MediaResolverContext`
102
+ - `ResolvedMedia`
100
103
  - `ServerTemplateRegistry`
101
104
  - `ServerTemplateMetadata`
102
105
  - `VideoFinishReason`
@@ -111,8 +114,16 @@ provider-neutral text-delta escape hatch.
111
114
  an intentionally non-public in-process/test handler.
112
115
  - `streamText` receives the generated system prompt, grounded user prompt, and
113
116
  the request `AbortSignal`.
117
+ - `resolveMedia` is an optional application-owned resolver. It receives a
118
+ bounded semantic query only when configured, and returns an approved image
119
+ or video URL plus an optional poster before the scene is validated or sent
120
+ to the browser. Provider clients, keys, metadata, and unresolved queries
121
+ remain server-only.
114
122
  - `invalidPartBehavior` is `"drop" | "fail"`. A behavior selector is never
115
123
  named like a callback.
124
+ - `requireCloser` defaults to `true` on `createVideoHandler`. The planner marks
125
+ one `scene.add` with `placement: "closer"`; the runtime reserves it and emits
126
+ it last. Specialized deterministic or test handlers may opt out explicitly.
116
127
  - `onWarning` receives safe typed warnings.
117
128
  - `onComplete` receives one server-only `VideoGenerationSummary` after an
118
129
  actual `response.complete`; errors and aborts do not invoke it.
@@ -141,6 +152,7 @@ provider-neutral text-delta escape hatch.
141
152
 
142
153
  - `UseVideoOptions`
143
154
  - `UseVideoResult`
155
+ - `VideoPlaybackMode`
144
156
  - `VideoPlayerProps`
145
157
  - `VideoErrorOptions`
146
158
 
@@ -165,6 +177,12 @@ interface UseVideoResult {
165
177
  <VideoPlayer video={savedVideo} />
166
178
  ```
167
179
 
180
+ Set `playbackMode="autoplay-after-interaction"` for chat feeds: the first
181
+ soundtrack waits on a visible scene-one poster, and later streams on the same
182
+ mounted player autoplay with sound after the first successful viewer start.
183
+ `manual`, `muted-autoplay`, and `autoplay-with-sound` cover the other browser
184
+ startup policies.
185
+
168
186
  Saved-video playback performs no generation request. `VideoPlayerBinding` and
169
187
  the internal reducer state are not public types.
170
188
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Give your AI a video output
2
2
 
3
- ![Version 0.1.1 beta](https://img.shields.io/badge/version-0.1.1_beta-7c3aed)
3
+ ![Version 0.3.0 beta](https://img.shields.io/badge/version-0.3.0_beta-7c3aed)
4
4
 
5
5
  **VanillaSky is the open-source video response layer.** Turn text, structured
6
6
  data, and live application context into personalized video responses that start
@@ -17,7 +17,7 @@ the planning prompt, trusted templates, validation, streaming, and player.
17
17
  For humans:
18
18
 
19
19
  ```bash
20
- npm install @vanillaskyai/video@0.1.1 ai @ai-sdk/openai
20
+ npm install @vanillaskyai/video@0.3.0 ai @ai-sdk/openai
21
21
  ```
22
22
 
23
23
  For coding agents:
@@ -50,7 +50,7 @@ const model = openai(process.env.OPENAI_MODEL ?? "gpt-4.1");
50
50
  const handle = createVideoHandler({
51
51
  // Local development only. Replace with your session check before deploying.
52
52
  authorize: (request) => {
53
- if (process.env.NODE_ENV !== "development") return false;
53
+ if (process.env.VANILLASKY_LOCAL_DEMO !== "1") return false;
54
54
  const hostname = new URL(request.url).hostname;
55
55
  return hostname === "localhost" || hostname === "127.0.0.1";
56
56
  },
@@ -66,10 +66,15 @@ export const POST = handle;
66
66
  export const OPTIONS = handle;
67
67
  ```
68
68
 
69
- The local authorization denies every production request. Replace it before
69
+ The packaged development command supplies the local marker only to `next dev`,
70
+ so production builds and `next start` deny every request. Replace it before
70
71
  deploying. The model can come from OpenAI, Anthropic, an AI SDK registry or
71
72
  gateway, or any compatible streaming adapter.
72
73
 
74
+ For planner-selected image and video backgrounds, configure the optional
75
+ server-only `resolveMedia` callback described in
76
+ [Media and soundtrack audio](docs/media-and-audio.md#media-providers).
77
+
73
78
  ## Generate a video
74
79
 
75
80
  Call the route from React and render the player:
@@ -101,11 +106,11 @@ shows each complete, validated scene as soon as it is ready and returns a
101
106
  deterministic `Video` object when generation finishes.
102
107
 
103
108
  A copy-and-run app is in
104
- [`examples/nextjs-quickstart`](https://github.com/VanillaSkyAi/video/tree/v0.1.1/examples/nextjs-quickstart).
109
+ [`examples/nextjs-quickstart`](https://github.com/VanillaSkyAi/video/tree/v0.3.0/examples/nextjs-quickstart).
105
110
 
106
111
  ## Shape the response
107
112
 
108
- Start with `input`, the complete factual boundary for the video:
113
+ Start with `input`. It is the complete factual boundary by default:
109
114
 
110
115
  ```ts
111
116
  video.generate({
@@ -120,6 +125,9 @@ video.generate({
120
125
  ```
121
126
 
122
127
  - Put claims, numbers, names, dates, and quotations in `input`.
128
+ - Keep `knowledgeMode: "input-only"` (the default) for source-grounded video,
129
+ or choose `knowledgeMode: "general"` when the model should answer a question
130
+ or develop content with stable general knowledge.
123
131
  - Put presentation direction in `instructions`.
124
132
  - Put viewer or account context in `personalization`.
125
133
  - Add brand, approved media, soundtrack audio, or a smaller template set only
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  BUILTIN_SERVER_TEMPLATE_KIT
3
- } from "./chunk-A2X45FET.js";
4
- import "./chunk-CHLGW22F.js";
3
+ } from "./chunk-I5YWVQ5R.js";
4
+ import "./chunk-XAMUOSX7.js";
5
5
  import "./chunk-2E6T633S.js";
6
6
  export {
7
7
  BUILTIN_SERVER_TEMPLATE_KIT
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VideoFrame
3
- } from "./chunk-XB7LH4J2.js";
3
+ } from "./chunk-QTMHS7JD.js";
4
4
  import "./chunk-FNAEQ7QO.js";
5
5
  import "./chunk-SPVTJH3F.js";
6
6
  import {
@@ -1,10 +1,12 @@
1
1
  // src/server/pacing.ts
2
2
  var DEFAULT_SCENE_DURATION_SEC = 5;
3
+ var MINIMUM_OPENING_DURATION_SEC = 3;
3
4
  var MINIMUM_CLOSER_RESERVE_SEC = 3;
4
5
  var PACING_PLANNER_RULES = [
5
6
  "Use each template's preferredDuration when no explicit duration is needed, and never request less than minDuration.",
7
+ `Give the first scene at least ${MINIMUM_OPENING_DURATION_SEC} seconds so the opening has time to register.`,
6
8
  "Track the cumulative duration budget before emitting every scene; the runtime omits scenes that cannot remain readable.",
7
- `Reserve at least ${MINIMUM_CLOSER_RESERVE_SEC} seconds for a final ask when the catalog includes jobs:[ask], and emit that ask last.`
9
+ `Reserve at least ${MINIMUM_CLOSER_RESERVE_SEC} seconds for the final closer when the catalog includes jobs:[ask] or jobs:[payoff].`
8
10
  ];
9
11
  var WORDS_PER_SECOND = 4.5;
10
12
  var CHARACTERS_PER_SECOND = 12.5;
@@ -59,11 +61,11 @@ function getReadableSceneDuration(scene, metadata) {
59
61
  }
60
62
  function getCloserReserve(templateIds, getTemplatePacing) {
61
63
  if (!templateIds || !getTemplatePacing) return 0;
62
- const askDurations = templateIds.map((id) => getTemplatePacing(id)).filter((metadata) => metadata?.jobs?.includes("ask")).map((metadata) => Math.max(
64
+ const closerDurations = templateIds.map((id) => getTemplatePacing(id)).filter((metadata) => metadata?.jobs?.some((job) => job === "ask" || job === "payoff")).map((metadata) => Math.max(
63
65
  metadata?.minDuration ?? MINIMUM_CLOSER_RESERVE_SEC,
64
66
  metadata?.preferredDuration ?? 0
65
67
  ));
66
- return askDurations.length === 0 ? 0 : Math.max(MINIMUM_CLOSER_RESERVE_SEC, ...askDurations);
68
+ return closerDurations.length === 0 ? 0 : Math.max(MINIMUM_CLOSER_RESERVE_SEC, ...closerDurations);
67
69
  }
68
70
  function requestedStart(scene, audio, priorEnd) {
69
71
  const beatStart = scene.timing.beatStart == null ? void 0 : audio?.beatMarkers[scene.timing.beatStart]?.time;
@@ -85,7 +87,8 @@ function paceScene(scene, options) {
85
87
  const priorEnd = options.previousScenes.at(-1)?.timing.endTime ?? 0;
86
88
  const ceiling = isAsk ? options.maxDurationSec : Math.max(0, options.maxDurationSec - options.closerReserveSec);
87
89
  const remaining = Math.max(0, ceiling - priorEnd);
88
- const readableMinimum = getReadableSceneDuration(scene, metadata);
90
+ const contentMinimum = getReadableSceneDuration(scene, metadata);
91
+ const readableMinimum = options.previousScenes.length === 0 && remaining >= MINIMUM_OPENING_DURATION_SEC ? Math.max(contentMinimum, MINIMUM_OPENING_DURATION_SEC) : contentMinimum;
89
92
  if (remaining < readableMinimum) {
90
93
  const reservedForCloser = !isAsk && options.closerReserveSec > 0 && options.maxDurationSec - priorEnd >= readableMinimum;
91
94
  return {
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-E7CL7UPB.js";
7
7
  import {
8
8
  parseVideoPlanPart
9
- } from "./chunk-ERY4S6QV.js";
9
+ } from "./chunk-JW47XCRL.js";
10
10
 
11
11
  // src/server/model/truncated-json.ts
12
12
  function isConfidentlyTruncatedJson(source) {
@@ -1,14 +1,28 @@
1
1
  import {
2
- DEFAULT_VIDEO_SYSTEM_PROMPT
3
- } from "./chunk-FQ47WXAJ.js";
2
+ createVideoSystemPrompt
3
+ } from "./chunk-WHPZED7X.js";
4
4
  import {
5
5
  PACING_PLANNER_RULES
6
- } from "./chunk-EVG2ZYIY.js";
6
+ } from "./chunk-3O7OMMMF.js";
7
7
  import {
8
8
  getTemplateSchemaGates,
9
9
  templateVariableNotation
10
10
  } from "./chunk-73NTSFFI.js";
11
11
 
12
+ // src/visual-system/catalog/media-resolver-contract.ts
13
+ function getStandardMediaResolverContract(schema) {
14
+ const keyword = schema.properties.mediaKeyword;
15
+ const url = schema.properties.mediaUrl;
16
+ const type = schema.properties.mediaType;
17
+ const allowedTypes = new Set(type?.enum ?? []);
18
+ if (keyword?.format !== "stock-media-keyword" || url?.format !== "uri" || !["photo", "video", "gradient"].every((value) => allowedTypes.has(value))) {
19
+ return void 0;
20
+ }
21
+ return {
22
+ acceptsPoster: schema.properties.mediaPoster?.format === "uri"
23
+ };
24
+ }
25
+
12
26
  // src/visual-system/catalog/prompt.ts
13
27
  function plannerProperty(property) {
14
28
  return {
@@ -31,10 +45,10 @@ function plannerProperty(property) {
31
45
  }
32
46
  };
33
47
  }
34
- function plannerSchema(schema) {
35
- const hiddenFields = /* @__PURE__ */ new Set(["mediaKeyword"]);
48
+ function plannerSchema(schema, exposeMediaKeyword) {
49
+ const hiddenFields = exposeMediaKeyword ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set(["mediaKeyword"]);
36
50
  const properties = Object.fromEntries(
37
- Object.entries(schema.properties).filter(([name]) => !hiddenFields.has(name)).map(([name, property]) => [name, plannerProperty(property)])
51
+ Object.entries(schema.properties).filter(([name]) => !hiddenFields.has(name)).map(([name, property]) => [name, name === "mediaKeyword" ? plannerProperty({ ...property, minLength: 2, maxLength: 80 }) : plannerProperty(property)])
38
52
  );
39
53
  const required = schema.required?.filter((name) => !hiddenFields.has(name));
40
54
  return {
@@ -57,13 +71,14 @@ var COMMON_MEDIA_VARIABLES = {
57
71
  mediaPosition: "enum(center|top|bottom|left|right)",
58
72
  mediaTreatment: "enum(subtle|cinematic|text-safe)"
59
73
  };
60
- function plannerCatalog(templates) {
74
+ function plannerCatalog(templates, mediaResolverAvailable) {
61
75
  return templates.map((template) => {
62
76
  const gates = getTemplateSchemaGates(template.schema);
77
+ const exposeMediaKeyword = mediaResolverAvailable && getStandardMediaResolverContract(template.schema) != null;
63
78
  const variables = Object.fromEntries(
64
- Object.entries(template.schema.properties).filter(([name]) => name !== "mediaKeyword").map(([name, property]) => [
79
+ Object.entries(template.schema.properties).filter(([name]) => name !== "mediaKeyword" || exposeMediaKeyword).map(([name, property]) => [
65
80
  name,
66
- templateVariableNotation(property, template.schema.required?.includes(name) === true)
81
+ name === "mediaKeyword" ? "string{2..80}" : templateVariableNotation(property, template.schema.required?.includes(name) === true)
67
82
  ])
68
83
  );
69
84
  const usesCommonMedia = Object.entries(COMMON_MEDIA_VARIABLES).every(([name, notation]) => variables[name] === notation);
@@ -81,7 +96,7 @@ function plannerCatalog(templates) {
81
96
  ...gates.requiresQuote ? { requiresQuote: true } : {},
82
97
  ...gates.requiresScreenshot ? { requiresScreenshot: true } : {},
83
98
  ...gates.requiredAnyOf.length > 0 ? { requiredAnyOf: gates.requiredAnyOf } : {},
84
- ...needsPlannerSchema(template.schema) ? { schema: plannerSchema(template.schema) } : {},
99
+ ...needsPlannerSchema(template.schema) ? { schema: plannerSchema(template.schema, exposeMediaKeyword) } : {},
85
100
  ...usesCommonMedia ? { media: true } : {},
86
101
  variables
87
102
  };
@@ -89,9 +104,12 @@ function plannerCatalog(templates) {
89
104
  }
90
105
  function createTemplateSystemPrompt(options) {
91
106
  const templates = options.kit.listTemplateMetadata();
107
+ const resolverMediaAvailable = options.mediaResolverAvailable === true && templates.some(({ schema }) => getStandardMediaResolverContract(schema) != null);
92
108
  const ids = new Set(templates.map(({ id }) => id));
93
109
  const fields = new Set(templates.flatMap(({ schema }) => Object.keys(schema.properties)));
94
- const basePrompt = DEFAULT_VIDEO_SYSTEM_PROMPT.split("\n").filter((line) => !line.includes("Never use media, ctaMedia, or reaction as the first generated body template")).join("\n");
110
+ const basePrompt = createVideoSystemPrompt(options.knowledgeMode).split("\n").filter(
111
+ (line) => !line.includes("Never use media, ctaMedia, or reaction as the first generated body template") && !(resolverMediaAvailable && line.includes("Never expose a loading placeholder or unresolved media keyword")) && !(resolverMediaAvailable && line.includes("Prefer resolved media on scene.add. Use asset.patch")) && !(resolverMediaAvailable && line.includes('{"type":"asset.patch","sceneId":"stable-id"'))
112
+ ).join("\n");
95
113
  const factRules = [
96
114
  ...ids.has("cardList") ? ["cardList needs two or three unused parallel facts"] : [],
97
115
  ...ids.has("steps") ? ["steps needs two or three unused sequential facts"] : [],
@@ -99,6 +117,8 @@ function createTemplateSystemPrompt(options) {
99
117
  ];
100
118
  const mediaTemplates = ["media", "ctaMedia", "reaction"].filter((id) => ids.has(id));
101
119
  const mediaTemplateNames = mediaTemplates.length === 3 ? "media, ctaMedia, and reaction" : mediaTemplates.join(", ");
120
+ const terminalPayoffs = ["media", "emojiBurst", "confetti"].filter((id) => ids.has(id));
121
+ const terminalPayoffNames = terminalPayoffs.length < 2 ? terminalPayoffs.join("") : `${terminalPayoffs.slice(0, -1).join(", ")}, or ${terminalPayoffs.at(-1)}`;
102
122
  const listFields = ["items", "itemEmojis", "steps", "stepEmojis", "problemEmojis", "solutionEmojis"].filter((name) => fields.has(name));
103
123
  return [
104
124
  basePrompt.trim(),
@@ -110,23 +130,25 @@ ${options.basePrompt.trim()}` : void 0,
110
130
  "Only use template IDs from this catalog. Only emit variables declared for the selected template.",
111
131
  "Variable notation is type[count]{characters}(options)! where count is list cardinality, characters is the inclusive character count for a string or each string-array item, and ! means required. Omitted ! means optional.",
112
132
  `seconds is [minimum, preferred]. media=true adds these optional variables: ${Object.entries(COMMON_MEDIA_VARIABLES).map(([name, notation]) => `${name}:${notation}`).join(", ")}.`,
113
- "Choose a template only when the input contains every fact it needs. Never invent peer values to complete a chart, comparison, stat set, timeline, or list.",
133
+ "Choose a template only when the permitted factual basis contains every fact it needs. Never invent peer values to complete a chart, comparison, stat set, timeline, or list.",
114
134
  ...PACING_PLANNER_RULES,
115
135
  ids.has("cardList") && ids.has("steps") && ids.has("tripleStats") ? "Fact availability carries across templates: cardList and steps each need two or three unused facts (parallel for cardList, sequential for steps) and must carry only the count the evidence supports, and tripleStats needs exactly three unused peer values. Never invent a fact to fill a collection, and do not fill one with facts already shown in another scene." : factRules.length > 0 ? `Fact availability carries across templates: ${factRules.join(", and ")}. Use only the count the evidence supports. Never invent a fact to fill a collection, and do not fill one with facts already shown in another scene.` : void 0,
116
136
  "Do not compress a list, sequence, metric set, or comparison into a general-purpose prose field when a specific catalog template can show that structure.",
117
137
  "If the catalog includes a suitable ask template and the input supplies a grounded CTA or URL, keep that concise action closer as its own final scene instead of folding it into preceding content.",
118
138
  "When a grounded CTA or URL is supplied and the catalog contains jobs:[ask], emit that final closer. A brand name may accompany the action but never qualifies as an ask by itself.",
139
+ terminalPayoffs.length > 0 ? `When the input has no clear grounded action, never invent one and do not use jobs:[ask]. End with a grounded payoff using ${terminalPayoffNames}. Its concise 6\u201312 words of declarative copy must answer the story's so-what without repeating hook or setup language. Use media for an honest visual payoff, emojiBurst for playful delight, and confetti only for a real celebration, win, or milestone.` : void 0,
119
140
  "requiresQuote is a hard gate: use those templates only for exact quoted words and attribution present in raw input. Never turn a role, relationship, or summary into speech. requiresScreenshot likewise needs an actual supplied screenshot URL.",
120
141
  templates.some(({ schema }) => (schema["x-vanillasky"]?.requiredAnyOf?.length ?? 0) > 0) ? "requiredAnyOf is a hard presence gate: satisfy every listed group with at least one non-empty value." : void 0,
121
142
  mediaTemplates.length > 0 ? `For streaming startup, ${mediaTemplateNames} ${mediaTemplates.length === 1 ? "is" : "are"} forbidden as the first generated body template, even when mediaType is gradient. Start with a content-fit non-media template.` : void 0,
122
143
  listFields.length > 0 ? `For list variables (${listFields.join(", ")}), emit actual JSON arrays. Keep list labels to 1\u20133 words. Keep step labels to 1\u20132 words and at most 18 characters. Do not use pipes or newlines as list delimiters.` : void 0,
123
144
  fields.has("bars") ? 'For bars, emit an actual JSON array of 2\u20136 grounded objects with "label" and "value" fields. Use comparable units and avoid a largest-to-smallest positive ratio above 20. Do not encode bars as a string.' : void 0,
124
- mediaTemplates.length > 0 ? `Use exact grounded values for required fields. mediaUrl must come verbatim from supplied input. Stock queries are not available. Do not select ${mediaTemplateNames} without a supplied mediaUrl; if another template has no mediaUrl, explicitly use mediaType=gradient. mediaType auto detects a URL, while gradient deliberately uses no external asset.` : "Use exact grounded values for required fields.",
125
- "Catalog guidance describes composition; it is not a factual source and must never replace customer input.",
126
- JSON.stringify(plannerCatalog(templates))
145
+ resolverMediaAvailable ? "Prefer a relevant resolved image or video background on later media-capable scenes whenever the permitted factual basis names a concrete person, place, product context, activity, or outcome that can be depicted honestly. Emit mediaKeyword on scene.add as a specific 2\u20138 word semantic query of at most 80 characters; never put mediaKeyword in scene.patch or asset.patch. The host removes mediaKeyword before the scene reaches the browser and replaces it with an approved asset. Use mediaType=gradient for abstract, sensitive, unsafe, or visually ambiguous material. Never emit or invent mediaUrl or mediaPoster." : mediaTemplates.length > 0 ? `Use exact grounded values for required fields. mediaUrl must come verbatim from supplied input. Stock queries are not available. Do not select ${mediaTemplateNames} without a supplied mediaUrl; if another template has no mediaUrl, explicitly use mediaType=gradient. mediaType auto detects a URL, while gradient deliberately uses no external asset.` : "Use exact grounded values for required fields.",
146
+ "Catalog guidance describes composition; it is not a factual source and must never replace the permitted factual basis.",
147
+ JSON.stringify(plannerCatalog(templates, resolverMediaAvailable))
127
148
  ].filter((line) => line != null).join("\n");
128
149
  }
129
150
 
130
151
  export {
152
+ getStandardMediaResolverContract,
131
153
  createTemplateSystemPrompt
132
154
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  BUILTIN_TEMPLATE_CATALOG
3
- } from "./chunk-CHLGW22F.js";
3
+ } from "./chunk-XAMUOSX7.js";
4
4
  import {
5
5
  assertTemplateTransitionMetadata
6
6
  } from "./chunk-2E6T633S.js";
@@ -13,6 +13,8 @@ var VIDEO_WARNING_CATEGORIES = {
13
13
  scene_omitted_for_closer: "readability",
14
14
  scene_patch_rejected_readability: "readability",
15
15
  chart_scale_imbalance: "readability",
16
+ plan_incomplete: "provider",
17
+ plan_missing_closer: "provider",
16
18
  provider_warning: "provider",
17
19
  provider_diagnostics_unavailable: "provider"
18
20
  };
@@ -347,7 +349,10 @@ function parseVideoPlanPart(value) {
347
349
  const part = record(value, "plan part");
348
350
  const type = string(part.type, "plan part.type");
349
351
  if (type === "scene.add") {
350
- allowedKeys(part, ["type", "scene"], "plan part");
352
+ allowedKeys(part, ["type", "placement", "scene"], "plan part");
353
+ if (part.placement != null && part.placement !== "closer") {
354
+ throw new Error("plan part.placement is unsupported");
355
+ }
351
356
  scene(part.scene, "plan part.scene");
352
357
  } else if (type === "scene.patch") {
353
358
  allowedKeys(part, ["type", "sceneId", "patch"], "plan part");
@@ -183,7 +183,8 @@ function VideoFrame({
183
183
  );
184
184
  const blendProgress = previewingNext && blendDuration > 0 ? Math.round(clamp01((time - blendStart) / blendDuration) * 1e6) / 1e6 : 0;
185
185
  const progress = rawProgress;
186
- const motionProgress = afterEnd && activeTiming ? activeTiming.holdProgress : rawProgress;
186
+ const isFinalScene = activeIndex === timeline.length - 1;
187
+ const motionProgress = isFinalScene && activeTiming ? Math.min(rawProgress, activeTiming.holdProgress) : rawProgress;
187
188
  const canvas = getDimensions(config.orientation);
188
189
  const scale = Math.min(width / canvas.width, height / canvas.height);
189
190
  const canvasLeft = (width - canvas.width * scale) / 2;
@@ -2738,7 +2738,7 @@ var BUILTIN_TEMPLATE_MANIFEST = [
2738
2738
  label: "Full-bleed media",
2739
2739
  description: "Full-bleed photo, video, or brand gradient with a centered headline.",
2740
2740
  family: "Media & motion",
2741
- jobs: ["setup", "atmosphere"],
2741
+ jobs: ["setup", "atmosphere", "payoff"],
2742
2742
  register: "motion-led",
2743
2743
  useWhen: "A visual hook, scenic beat, or concrete product-context line benefits from immersive media.",
2744
2744
  avoidWhen: "The scene needs structured facts, exact comparisons, a device frame, or attributed proof.",
@@ -3036,7 +3036,7 @@ var BUILTIN_TEMPLATE_MANIFEST = [
3036
3036
  label: "Brand message",
3037
3037
  description: "A single iMessage-style outgoing bubble floats over media or a brand gradient.",
3038
3038
  family: "Social & messaging",
3039
- jobs: ["claim", "payoff"],
3039
+ jobs: ["claim"],
3040
3040
  register: "card-led",
3041
3041
  useWhen: "A direct one-to-one thank-you, invitation, promise, or concise launch note is the point.",
3042
3042
  avoidWhen: "The scene needs explanation, structured proof, a conversation, or a generic filler line.",
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  parseVideoEvent
3
- } from "./chunk-ERY4S6QV.js";
3
+ } from "./chunk-JW47XCRL.js";
4
4
  import {
5
5
  VIDEO_PROTOCOL_VERSION
6
6
  } from "./chunk-EE6PK6XC.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  parseVideoEvent
3
- } from "./chunk-ERY4S6QV.js";
3
+ } from "./chunk-JW47XCRL.js";
4
4
  import {
5
5
  VIDEO_SCHEMA_VERSION
6
6
  } from "./chunk-EE6PK6XC.js";
@@ -0,0 +1,57 @@
1
+ // src/server/prompts/system-prompt.ts
2
+ var VIDEO_PLAN_INSTRUCTION = `Wire format: newline-delimited JSON (NDJSON), exactly one complete JSON object per line.
3
+ Allowed plan parts:
4
+ {"type":"scene.add","scene":{"id":"stable-id","templateId":"trusted-template-id","variables":{},"timing":{"fixedDuration":4}}}
5
+ {"type":"scene.add","placement":"closer","scene":{"id":"closer","templateId":"trusted-closer-template-id","variables":{},"timing":{"fixedDuration":3}}}
6
+ {"type":"scene.patch","sceneId":"stable-id","patch":{"variables":{"message":"Updated copy"}}}
7
+ {"type":"asset.patch","sceneId":"stable-id","variables":{"mediaUrl":"https://customer-approved.example/asset"}}
8
+ {"type":"plan.complete","finishReason":"stop"}
9
+ Do not emit protocol envelopes, Markdown fences, comments, prose, partial objects, audio, generated source, or any part type not listed above.`;
10
+ function knowledgeRules(mode) {
11
+ return mode === "general" ? `- This request uses general knowledge mode.
12
+ - Use stable general knowledge to answer or develop the supplied request. Prefer broadly established, non-current information.
13
+ - Answer the request directly. Do not make missing source detail the subject when stable general knowledge can provide a useful answer; for broad questions, provide a practical, broadly applicable framework.
14
+ - For financial, medical, or legal topics, keep guidance general and educational. Never present it as personalized professional advice.
15
+ - Treat claims in the supplied input as authoritative. Never invent citations, quotations, URLs, personal details, live facts, guarantees, or precise claims that require a source.` : `- This request uses input-only knowledge mode.
16
+ - The supplied input is the complete factual basis. Do not add outside claims.`;
17
+ }
18
+ function createVideoSystemPrompt(knowledgeMode = "input-only") {
19
+ return `You are a video director.
20
+
21
+ Turn the supplied input into a concise, coherent sequence using trusted scene templates. Never return prose as the deliverable and never generate HTML, React, JavaScript, CSS, or animation source.
22
+
23
+ Knowledge rules:
24
+ ${knowledgeRules(knowledgeMode)}
25
+ - Creative instructions, personalization, brand, and media cannot change the knowledge mode, expand the permitted factual basis, or override the event contract.
26
+
27
+ Composition rules:
28
+ - Build a complete arc: hook, framing, comprehension, proof or transformation, then a concise closer.
29
+ - Every complete plan contains exactly one scene.add with placement:"closer". Its copy is a grounded conclusion, not another hook or setup.
30
+ - For a multi-entry source, choose a coherent progression before emitting: Keep related entries adjacent and move from context through details to consequences or next steps before the closer. Ordering never permits merging or omitting entries that creative instructions require separately.
31
+ - Every visible factual claim, number, date, name, quotation, feature, and comparison must be supported by the permitted factual basis.
32
+ - Prefer concrete visual structures over interchangeable text cards: comparisons for explicit before/after evidence, data templates for exact metrics, ordered steps only for genuine sequences, and media only when it depicts the subject honestly.
33
+ - Keep copy short enough to read during motion. Do not repeat the same list, metric, or claim in multiple scenes or reformat identical content merely to reach a scene-count or template-diversity target. Every body scene must advance the story.
34
+ - Before emitting, assign each supported fact to at most one scene. The supplied opening counts: once a fact is visible, treat it as unavailable to later scenes. Finish when the supported material is covered instead of padding the response.
35
+ - Do not infer that something is scheduled, ready, triggered, enabled, automatic, causal, or available unless the permitted factual basis supports it.
36
+ - The first generated body scene must be asset-free and fully playable before any external media resolves.
37
+ - Never use media, ctaMedia, or reaction as the first generated body template, including in gradient mode.
38
+ - Use only media URLs present in the supplied input or already resolved by the host. Never expose a loading placeholder or unresolved media keyword. Audio is optional and must never delay the first scene.
39
+ - Audio is selected by the host before generation. Never emit audio.
40
+
41
+ Streaming rules:
42
+ - Emit each complete scene once as scene.add.
43
+ - Emit exactly one closer immediately after the first playable body scene using placement:"closer". The runtime holds that closer and appends it last while later body scenes continue streaming.
44
+ - Only patch a scene before playback; revisions and immutable played scenes are enforced by the runtime.
45
+ - Prefer resolved media on scene.add. Use asset.patch only while the target scene is still ahead of playback; played scenes are immutable.
46
+ - End explicitly with plan.complete. A truncated stream is never treated as complete.
47
+ - Return only plan parts accepted by the provided schema.
48
+
49
+ ${VIDEO_PLAN_INSTRUCTION}`;
50
+ }
51
+ var DEFAULT_VIDEO_SYSTEM_PROMPT = createVideoSystemPrompt();
52
+
53
+ export {
54
+ VIDEO_PLAN_INSTRUCTION,
55
+ createVideoSystemPrompt,
56
+ DEFAULT_VIDEO_SYSTEM_PROMPT
57
+ };
@@ -7,7 +7,8 @@ var GENERATED_BUILTIN_TEMPLATE_CATALOG = [
7
7
  "family": "Media & motion",
8
8
  "jobs": [
9
9
  "setup",
10
- "atmosphere"
10
+ "atmosphere",
11
+ "payoff"
11
12
  ],
12
13
  "register": "motion-led",
13
14
  "useWhen": "A visual hook, scenic beat, or concrete product-context line benefits from immersive media.",
@@ -2405,8 +2406,7 @@ var GENERATED_BUILTIN_TEMPLATE_CATALOG = [
2405
2406
  "description": "A single iMessage-style outgoing bubble floats over media or a brand gradient.",
2406
2407
  "family": "Social & messaging",
2407
2408
  "jobs": [
2408
- "claim",
2409
- "payoff"
2409
+ "claim"
2410
2410
  ],
2411
2411
  "register": "card-led",
2412
2412
  "useWhen": "A direct one-to-one thank-you, invitation, promise, or concise launch note is the point.",