@vanillaskyai/video 0.1.1-beta.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 +31 -13
- package/PUBLIC-API.md +27 -18
- 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 +2 -12
- package/registry/items/brandMessage.json +1 -2
- package/registry/items/media.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,19 +4,37 @@ VanillaSky follows semantic versioning. This changelog begins with the 0.1 beta.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
-
<!-- Add release notes
|
|
8
|
-
|
|
9
|
-
## 0.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
7
|
+
<!-- Add release notes for the next version here. -->
|
|
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
|
+
|
|
28
|
+
## 0.1.1
|
|
29
|
+
|
|
30
|
+
No runtime changes: the public API, behavior, and dependencies are identical to
|
|
31
|
+
0.1.0. This release replaces the 0.1.1-beta line and moves `latest` onto a
|
|
32
|
+
single, current version so the repository, npm, and vanillasky.ai agree.
|
|
33
|
+
|
|
34
|
+
- Simplified the release process to a version bump, an annotated tag on `main`,
|
|
35
|
+
and an OIDC publish. Changesets, the generated Version Packages branch, and
|
|
36
|
+
the npm-latest compatibility gate are removed; the tag job now packs a commit
|
|
37
|
+
CI has already verified instead of re-running the suite.
|
|
20
38
|
|
|
21
39
|
## 0.1.0
|
|
22
40
|
|
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
|
|
@@ -19,17 +19,9 @@ public API review.
|
|
|
19
19
|
- `0.1.x` patch releases do not make breaking changes to documented APIs or the
|
|
20
20
|
serialized `Video` schema.
|
|
21
21
|
- A compatible later `0.x` minor needs no migration ceremony. A breaking
|
|
22
|
-
pre-1.0 minor
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
Those sections must include concrete fenced before and after code examples,
|
|
26
|
-
respectively. Start a breaking Changeset with its one-line summary, followed
|
|
27
|
-
by those headings, so Changesets preserves both as real headings in the
|
|
28
|
-
generated changelog. The verifier accepts the configured Changesets
|
|
29
|
-
changelog's canonical two-space continuation indentation, but not headings
|
|
30
|
-
hidden inside code fences. On a feature PR, only a Changeset added since the
|
|
31
|
-
pull request's exact base commit can provide this evidence. A `patch`
|
|
32
|
-
Changeset never authorizes a breaking change.
|
|
22
|
+
pre-1.0 minor must document both `### Breaking changes` and `### Adoption` in
|
|
23
|
+
its changelog section, with concrete fenced before and after code examples
|
|
24
|
+
respectively.
|
|
33
25
|
- An API prefixed with `experimental_` may change in a patch. Canonical examples
|
|
34
26
|
must pin an exact package version when they use one.
|
|
35
27
|
- Deprecated APIs remain usable until the next minor release. The `0.1`
|
|
@@ -40,11 +32,10 @@ public API review.
|
|
|
40
32
|
- Framework adapters are examples, not separate public APIs. The release suite
|
|
41
33
|
verifies current Next.js and Vite production builds.
|
|
42
34
|
|
|
43
|
-
The
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
exactly equal because the report cannot safely distinguish input and output positions.
|
|
35
|
+
The frozen public API surface report is compared on every pull request. It is
|
|
36
|
+
intentionally conservative: every existing normalized declaration and reachable
|
|
37
|
+
support declaration must remain exactly equal because the report cannot safely
|
|
38
|
+
distinguish input and output positions.
|
|
48
39
|
As a result, even optional field additions to an existing public
|
|
49
40
|
type fail a patch gate; use a documented pre-1.0 minor unless a separately
|
|
50
41
|
reviewed, direction-aware compatibility check can prove the change safe. New
|
|
@@ -106,6 +97,9 @@ provider-neutral text-delta escape hatch.
|
|
|
106
97
|
### Types
|
|
107
98
|
|
|
108
99
|
- `VideoHandlerOptions`
|
|
100
|
+
- `MediaResolver`
|
|
101
|
+
- `MediaResolverContext`
|
|
102
|
+
- `ResolvedMedia`
|
|
109
103
|
- `ServerTemplateRegistry`
|
|
110
104
|
- `ServerTemplateMetadata`
|
|
111
105
|
- `VideoFinishReason`
|
|
@@ -120,8 +114,16 @@ provider-neutral text-delta escape hatch.
|
|
|
120
114
|
an intentionally non-public in-process/test handler.
|
|
121
115
|
- `streamText` receives the generated system prompt, grounded user prompt, and
|
|
122
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.
|
|
123
122
|
- `invalidPartBehavior` is `"drop" | "fail"`. A behavior selector is never
|
|
124
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.
|
|
125
127
|
- `onWarning` receives safe typed warnings.
|
|
126
128
|
- `onComplete` receives one server-only `VideoGenerationSummary` after an
|
|
127
129
|
actual `response.complete`; errors and aborts do not invoke it.
|
|
@@ -150,6 +152,7 @@ provider-neutral text-delta escape hatch.
|
|
|
150
152
|
|
|
151
153
|
- `UseVideoOptions`
|
|
152
154
|
- `UseVideoResult`
|
|
155
|
+
- `VideoPlaybackMode`
|
|
153
156
|
- `VideoPlayerProps`
|
|
154
157
|
- `VideoErrorOptions`
|
|
155
158
|
|
|
@@ -174,6 +177,12 @@ interface UseVideoResult {
|
|
|
174
177
|
<VideoPlayer video={savedVideo} />
|
|
175
178
|
```
|
|
176
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
|
+
|
|
177
186
|
Saved-video playback performs no generation request. `VideoPlayerBinding` and
|
|
178
187
|
the internal reducer state are not public types.
|
|
179
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.",
|