@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.
- package/CHANGELOG.md +19 -0
- package/PUBLIC-API.md +20 -2
- package/README.md +7 -3
- package/dist/{builtin-server-F7YXRVCH.js → builtin-server-BRTZN4Q7.js} +2 -2
- package/dist/check-runtime.js +1 -1
- package/dist/{chunk-EVG2ZYIY.js → chunk-3O7OMMMF.js} +7 -4
- package/dist/{chunk-GRMUV7QY.js → chunk-BKF3A357.js} +1 -1
- package/dist/{chunk-A2X45FET.js → chunk-I5YWVQ5R.js} +1 -1
- package/dist/{chunk-ERY4S6QV.js → chunk-JW47XCRL.js} +6 -1
- package/dist/{chunk-FQ47WXAJ.js → chunk-K5SRF7D3.js} +4 -0
- package/dist/{chunk-YC5MFXA5.js → chunk-Q2DDQKEG.js} +34 -12
- package/dist/{chunk-XB7LH4J2.js → chunk-QTMHS7JD.js} +2 -1
- package/dist/{chunk-W2XJKG6C.js → chunk-RBQN3VSY.js} +2 -2
- package/dist/{chunk-OOPIETLW.js → chunk-S5ZU5WUU.js} +1 -1
- package/dist/{chunk-ABPKM4GK.js → chunk-STIFILQG.js} +1 -1
- package/dist/{chunk-IXCUY3B3.js → chunk-TGFO454Q.js} +169 -45
- package/dist/{chunk-CHLGW22F.js → chunk-XAMUOSX7.js} +3 -3
- package/dist/cli.js +5 -5
- package/dist/{compose-video-CN3VSWST.js → compose-video-34GRZIF2.js} +4 -4
- package/dist/{events-_wfjbmEp.d.ts → events-B-28kERX.d.ts} +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/{kit-BMNcHqa9.d.ts → kit-CBedbj1n.d.ts} +1 -1
- package/dist/react.d.ts +7 -4
- package/dist/react.js +226 -30
- package/dist/server.d.ts +21 -4
- package/dist/server.js +193 -14
- package/dist/{system-prompt-A6UXNTPK.js → system-prompt-GYTYGWGO.js} +1 -1
- package/dist/template-catalog.js +1 -1
- package/dist/templates.d.ts +3 -3
- package/dist/test.d.ts +3 -2
- package/dist/test.js +3 -3
- package/dist/{text-stream-6XHC7J47.js → text-stream-UPGUD2TD.js} +2 -2
- package/dist/{types-B-PZR6G_.d.ts → types-_t4bduwX.d.ts} +1 -0
- package/docs/concepts.md +7 -0
- package/docs/custom-templates.md +3 -3
- package/docs/errors.md +3 -1
- package/docs/getting-started.md +8 -6
- package/docs/input-and-first-scene.md +7 -4
- package/docs/integrate-nextjs.md +2 -2
- package/docs/media-and-audio.md +73 -14
- package/docs/prompt-and-input.md +7 -0
- package/docs/provider-integration.md +7 -0
- package/docs/reference/protocol.md +9 -0
- package/examples/nextjs-quickstart/package.json +1 -1
- package/package.json +1 -1
- package/registry/items/brandMessage.json +1 -2
- 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
|
+
# VanillaSky Video 0.2 public API
|
|
2
2
|
|
|
3
|
-
Status: frozen public beta contract for `0.
|
|
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
|
-

|
|
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.
|
|
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.
|
|
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
|
|
package/dist/check-runtime.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
@@ -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-
|
|
3
|
+
} from "./chunk-K5SRF7D3.js";
|
|
4
4
|
import {
|
|
5
5
|
PACING_PLANNER_RULES
|
|
6
|
-
} from "./chunk-
|
|
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(
|
|
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
|
|
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"
|
|
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.",
|