@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.
- package/CHANGELOG.md +35 -1
- package/PUBLIC-API.md +20 -2
- package/README.md +14 -6
- 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-YC5MFXA5.js → chunk-GR4I3MN2.js} +37 -15
- package/dist/{chunk-A2X45FET.js → chunk-I5YWVQ5R.js} +1 -1
- package/dist/{chunk-ERY4S6QV.js → chunk-JW47XCRL.js} +6 -1
- 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-WHPZED7X.js +57 -0
- package/dist/{chunk-CHLGW22F.js → chunk-XAMUOSX7.js} +3 -3
- package/dist/{chunk-IXCUY3B3.js → chunk-XYZOJ5NZ.js} +195 -52
- package/dist/cli.js +5 -5
- package/dist/{compose-video-CN3VSWST.js → compose-video-TQOHDXNW.js} +4 -4
- package/dist/{events-_wfjbmEp.d.ts → events-CTIsANzz.d.ts} +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/{kit-BMNcHqa9.d.ts → kit-DA2cfJ96.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 +205 -18
- package/dist/system-prompt-NMYHLAMJ.js +10 -0
- 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-CkO2EYr4.d.ts} +8 -4
- package/docs/concepts.md +11 -1
- package/docs/custom-templates.md +34 -6
- package/docs/customization.md +2 -1
- package/docs/errors.md +3 -1
- package/docs/getting-started.md +13 -10
- package/docs/input-and-first-scene.md +17 -6
- package/docs/integrate-nextjs.md +6 -3
- package/docs/media-and-audio.md +73 -14
- package/docs/prompt-and-input.md +27 -5
- package/docs/provider-integration.md +45 -2
- package/docs/reference/protocol.md +9 -0
- package/examples/nextjs-quickstart/README.md +3 -1
- package/examples/nextjs-quickstart/package.json +3 -2
- package/examples/nextjs-quickstart/src/app/api/video/route.ts +1 -1
- package/package.json +1 -1
- package/registry/items/brandMessage.json +1 -2
- package/registry/items/media.json +2 -1
- package/dist/chunk-FQ47WXAJ.js +0 -38
- 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
|
-
|
|
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
|
+
# VanillaSky Video 0.2 public API
|
|
2
2
|
|
|
3
|
-
Status: frozen public beta contract for `0.
|
|
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
|
-

|
|
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.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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
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 {
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "./chunk-
|
|
2
|
+
createVideoSystemPrompt
|
|
3
|
+
} from "./chunk-WHPZED7X.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 =
|
|
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
|
|
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
|
|
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
|
};
|
|
@@ -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
|
|
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.",
|
|
@@ -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.",
|