@vanillaskyai/video 0.1.1 → 0.2.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 (47) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/PUBLIC-API.md +20 -2
  3. package/README.md +7 -3
  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-A2X45FET.js → chunk-I5YWVQ5R.js} +1 -1
  9. package/dist/{chunk-ERY4S6QV.js → chunk-JW47XCRL.js} +6 -1
  10. package/dist/{chunk-FQ47WXAJ.js → chunk-K5SRF7D3.js} +4 -0
  11. package/dist/{chunk-YC5MFXA5.js → chunk-Q2DDQKEG.js} +34 -12
  12. package/dist/{chunk-XB7LH4J2.js → chunk-QTMHS7JD.js} +2 -1
  13. package/dist/{chunk-W2XJKG6C.js → chunk-RBQN3VSY.js} +2 -2
  14. package/dist/{chunk-OOPIETLW.js → chunk-S5ZU5WUU.js} +1 -1
  15. package/dist/{chunk-ABPKM4GK.js → chunk-STIFILQG.js} +1 -1
  16. package/dist/{chunk-IXCUY3B3.js → chunk-TGFO454Q.js} +169 -45
  17. package/dist/{chunk-CHLGW22F.js → chunk-XAMUOSX7.js} +3 -3
  18. package/dist/cli.js +5 -5
  19. package/dist/{compose-video-CN3VSWST.js → compose-video-34GRZIF2.js} +4 -4
  20. package/dist/{events-_wfjbmEp.d.ts → events-B-28kERX.d.ts} +2 -2
  21. package/dist/index.d.ts +2 -2
  22. package/dist/{kit-BMNcHqa9.d.ts → kit-CBedbj1n.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 +193 -14
  27. package/dist/{system-prompt-A6UXNTPK.js → system-prompt-GYTYGWGO.js} +1 -1
  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-_t4bduwX.d.ts} +1 -0
  34. package/docs/concepts.md +7 -0
  35. package/docs/custom-templates.md +3 -3
  36. package/docs/errors.md +3 -1
  37. package/docs/getting-started.md +8 -6
  38. package/docs/input-and-first-scene.md +7 -4
  39. package/docs/integrate-nextjs.md +2 -2
  40. package/docs/media-and-audio.md +73 -14
  41. package/docs/prompt-and-input.md +7 -0
  42. package/docs/provider-integration.md +7 -0
  43. package/docs/reference/protocol.md +9 -0
  44. package/examples/nextjs-quickstart/package.json +1 -1
  45. package/package.json +1 -1
  46. package/registry/items/brandMessage.json +1 -2
  47. package/registry/items/media.json +2 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,25 @@ VanillaSky follows semantic versioning. This changelog begins with the 0.1 beta.
6
6
 
7
7
  <!-- Add release notes for the next version here. -->
8
8
 
9
+ ## 0.2.0
10
+
11
+ - Adds an application-owned `resolveMedia` hook that turns bounded semantic
12
+ media intent into approved image or video backgrounds without exposing
13
+ provider credentials, unresolved queries, or untrusted URLs to clients.
14
+ - Requires one grounded closer by default, holds it while body scenes stream,
15
+ and emits it last so complete videos finish on a payoff or supplied call to
16
+ action instead of an arbitrary body scene.
17
+ - Improves planning for rich inputs with adaptive scene counts, coherent
18
+ multi-entry sequencing, reusable best-fit templates, and explicit partial
19
+ completion warnings when provider or duration limits truncate the plan.
20
+ - Adds `VideoPlaybackMode` with sound-first interaction, repeat-stream
21
+ autoplay, manual, muted-autoplay, and immediate-autoplay policies.
22
+ - Renders `VideoInput.opening` as a deterministic, asset-free gradient media
23
+ scene and preserves it as the visible start poster before playback.
24
+ - Resets replacement streams as fresh playback sessions, keeps completed end
25
+ frames stable, and provides a replay control instead of replaying exit
26
+ animation at the terminal boundary.
27
+
9
28
  ## 0.1.1
10
29
 
11
30
  No runtime changes: the public API, behavior, and dependencies are identical to
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.2.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.2.0 beta](https://img.shields.io/badge/version-0.2.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.2.0 ai @ai-sdk/openai
21
21
  ```
22
22
 
23
23
  For coding agents:
@@ -70,6 +70,10 @@ The local authorization denies every production request. Replace it before
70
70
  deploying. The model can come from OpenAI, Anthropic, an AI SDK registry or
71
71
  gateway, or any compatible streaming adapter.
72
72
 
73
+ For planner-selected image and video backgrounds, configure the optional
74
+ server-only `resolveMedia` callback described in
75
+ [Media and soundtrack audio](docs/media-and-audio.md#media-providers).
76
+
73
77
  ## Generate a video
74
78
 
75
79
  Call the route from React and render the player:
@@ -101,7 +105,7 @@ shows each complete, validated scene as soon as it is ready and returns a
101
105
  deterministic `Video` object when generation finishes.
102
106
 
103
107
  A copy-and-run app is in
104
- [`examples/nextjs-quickstart`](https://github.com/VanillaSkyAi/video/tree/v0.1.1/examples/nextjs-quickstart).
108
+ [`examples/nextjs-quickstart`](https://github.com/VanillaSkyAi/video/tree/v0.2.0/examples/nextjs-quickstart).
105
109
 
106
110
  ## Shape the response
107
111
 
@@ -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,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");
@@ -2,6 +2,7 @@
2
2
  var VIDEO_PLAN_INSTRUCTION = `Wire format: newline-delimited JSON (NDJSON), exactly one complete JSON object per line.
3
3
  Allowed plan parts:
4
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}}}
5
6
  {"type":"scene.patch","sceneId":"stable-id","patch":{"variables":{"message":"Updated copy"}}}
6
7
  {"type":"asset.patch","sceneId":"stable-id","variables":{"mediaUrl":"https://customer-approved.example/asset"}}
7
8
  {"type":"plan.complete","finishReason":"stop"}
@@ -12,6 +13,8 @@ Turn the supplied factual input into a concise, coherent sequence using trusted
12
13
 
13
14
  Composition rules:
14
15
  - Build a complete arc: hook, framing, comprehension, proof or transformation, then a concise closer.
16
+ - Every complete plan contains exactly one scene.add with placement:"closer". Its copy is a grounded conclusion, not another hook or setup.
17
+ - 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.
15
18
  - Every visible factual claim, number, date, name, quotation, feature, and comparison must be grounded in the supplied input.
16
19
  - 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.
17
20
  - 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.
@@ -25,6 +28,7 @@ Composition rules:
25
28
 
26
29
  Streaming rules:
27
30
  - Emit each complete scene once as scene.add.
31
+ - 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.
28
32
  - Only patch a scene before playback; revisions and immutable played scenes are enforced by the runtime.
29
33
  - Prefer resolved media on scene.add. Use asset.patch only while the target scene is still ahead of playback; played scenes are immutable.
30
34
  - End explicitly with plan.complete. A truncated stream is never treated as complete.
@@ -1,14 +1,28 @@
1
1
  import {
2
2
  DEFAULT_VIDEO_SYSTEM_PROMPT
3
- } from "./chunk-FQ47WXAJ.js";
3
+ } from "./chunk-K5SRF7D3.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 = DEFAULT_VIDEO_SYSTEM_PROMPT.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(),
@@ -116,17 +136,19 @@ ${options.basePrompt.trim()}` : 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.",
145
+ resolverMediaAvailable ? "Prefer a relevant resolved image or video background on later media-capable scenes whenever the input 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.",
125
146
  "Catalog guidance describes composition; it is not a factual source and must never replace customer input.",
126
- JSON.stringify(plannerCatalog(templates))
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
  };
@@ -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";