@slatesvideo/shared 0.4.2 → 0.4.4
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/dist/operations/index.d.ts +20 -0
- package/dist/operations/index.js +76 -2
- package/dist/prompts/index.d.ts +1 -0
- package/dist/prompts/index.js +1 -0
- package/dist/prompts/reference-composer.d.ts +46 -0
- package/dist/prompts/reference-composer.js +175 -0
- package/dist/prompts/reference-rules.d.ts +1 -1
- package/dist/prompts/reference-rules.js +5 -5
- package/dist/skills/content.js +9 -8
- package/package.json +7 -2
- package/skills/slates-character-turnaround.md +2 -2
- package/skills/slates-cost-discipline.md +8 -0
- package/skills/slates-one-prompt-film.md +1 -1
- package/skills/slates-project-organization.md +22 -0
- package/skills/slates-prompting-flux-2-max.md +4 -4
- package/skills/slates-prompting-kling-v3.md +2 -2
- package/skills/slates-prompting-nano-banana-2.md +4 -12
- package/skills/slates-prompting-seedance.md +15 -2
- package/skills/slates-prompting-veo-3.md +2 -2
|
@@ -93,6 +93,19 @@ export declare const createEnvironment: Operation<{
|
|
|
93
93
|
description?: string;
|
|
94
94
|
style?: string;
|
|
95
95
|
}>;
|
|
96
|
+
export declare const generateCharacterSheets: Operation<{
|
|
97
|
+
characterId: string;
|
|
98
|
+
projectId: string;
|
|
99
|
+
baseAssetId: string;
|
|
100
|
+
sheetTypes?: ('turnaround' | 'expression')[];
|
|
101
|
+
userNotes?: string;
|
|
102
|
+
}>;
|
|
103
|
+
export declare const generateEnvironmentPlate: Operation<{
|
|
104
|
+
environmentId: string;
|
|
105
|
+
projectId: string;
|
|
106
|
+
baseAssetId?: string;
|
|
107
|
+
userNotes?: string;
|
|
108
|
+
}>;
|
|
96
109
|
export declare const listStoryboards: Operation<{
|
|
97
110
|
projectId: string;
|
|
98
111
|
}>;
|
|
@@ -153,9 +166,14 @@ export declare const generateVideo: Operation<{
|
|
|
153
166
|
firstFrameAssetId?: string;
|
|
154
167
|
lastFrameAssetId?: string;
|
|
155
168
|
ingredientAssetIds?: string[];
|
|
169
|
+
characterAssetIds?: string[];
|
|
170
|
+
environmentAssetIds?: string[];
|
|
171
|
+
styleAssetIds?: string[];
|
|
172
|
+
videoReferenceAssetId?: string;
|
|
156
173
|
sound?: boolean;
|
|
157
174
|
audioLanguage?: 'EN' | 'ZH' | 'JA' | 'KO' | 'ES';
|
|
158
175
|
generateMusic?: boolean;
|
|
176
|
+
seedanceFace?: boolean;
|
|
159
177
|
negativePrompt?: string;
|
|
160
178
|
background?: boolean;
|
|
161
179
|
confirm?: boolean;
|
|
@@ -171,6 +189,7 @@ export declare const generateLipSync: Operation<{
|
|
|
171
189
|
ttsSpeed?: number;
|
|
172
190
|
audioFilePath?: string;
|
|
173
191
|
avatarModel?: 'avatar-standard' | 'avatar-pro';
|
|
192
|
+
klingProvider?: 'fal' | 'kling';
|
|
174
193
|
background?: boolean;
|
|
175
194
|
confirm?: boolean;
|
|
176
195
|
}>;
|
|
@@ -181,6 +200,7 @@ export declare const generateMotionTransfer: Operation<{
|
|
|
181
200
|
motionModel?: 'kling-mc-std' | 'kling-mc-pro';
|
|
182
201
|
characterOrientation?: 'video' | 'image';
|
|
183
202
|
prompt?: string;
|
|
203
|
+
klingProvider?: 'fal' | 'kling';
|
|
184
204
|
background?: boolean;
|
|
185
205
|
confirm?: boolean;
|
|
186
206
|
}>;
|
package/dist/operations/index.js
CHANGED
|
@@ -372,6 +372,51 @@ export const createEnvironment = {
|
|
|
372
372
|
return ok(await ctx.desktop().post('/agent/environments', input));
|
|
373
373
|
},
|
|
374
374
|
};
|
|
375
|
+
export const generateCharacterSheets = {
|
|
376
|
+
id: 'slates_generate_character_sheets',
|
|
377
|
+
description: "Generate a character's turnaround sheet (4 neutral full-body angles — identity: body, proportions, outfit) AND expression sheet (close-up facial detail) from a base portrait asset, on Nano Banana 2 at 2K, and bind both to the character. THE real character-building workflow — call right after slates_create_character. baseAssetId is the source portrait (a project asset). Both sheets generate by default. Afterward, carry the character into a scene by passing its turnaround + expression asset ids as characterAssetIds to slates_generate_video (or referenceAssetIds to slates_generate_image) so identity stays consistent across shots. Cost ~$0.36 for both sheets.",
|
|
378
|
+
input: z.object({
|
|
379
|
+
characterId: z.string().uuid(),
|
|
380
|
+
projectId: z.string().uuid(),
|
|
381
|
+
baseAssetId: z.string().uuid().describe('The base portrait asset the sheets are generated from.'),
|
|
382
|
+
sheetTypes: z
|
|
383
|
+
.array(z.enum(['turnaround', 'expression']))
|
|
384
|
+
.optional()
|
|
385
|
+
.describe('Which sheets to generate. Default both.'),
|
|
386
|
+
userNotes: z.string().optional().describe('Extra instruction, e.g. "use the woman on the left".'),
|
|
387
|
+
}),
|
|
388
|
+
async run(input, ctx) {
|
|
389
|
+
return ok(await ctx.desktop().post('/agent/characters/generate-sheets', {
|
|
390
|
+
characterId: input.characterId,
|
|
391
|
+
projectId: input.projectId,
|
|
392
|
+
baseAssetIds: [input.baseAssetId],
|
|
393
|
+
sheetTypes: input.sheetTypes,
|
|
394
|
+
userNotes: input.userNotes,
|
|
395
|
+
}));
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
export const generateEnvironmentPlate = {
|
|
399
|
+
id: 'slates_generate_environment_plate',
|
|
400
|
+
description: "Generate an environment's single clean establishing plate (Nano Banana 2, 2K) from an optional base image, and bind it as the environment's reference. THE real environment-building workflow — call right after slates_create_environment. Afterward, pass the plate asset id as environmentAssetIds to slates_generate_video (or referenceAssetIds to slates_generate_image) so the location stays consistent across shots. Cost ~$0.18.",
|
|
401
|
+
input: z.object({
|
|
402
|
+
environmentId: z.string().uuid(),
|
|
403
|
+
projectId: z.string().uuid(),
|
|
404
|
+
baseAssetId: z
|
|
405
|
+
.string()
|
|
406
|
+
.uuid()
|
|
407
|
+
.optional()
|
|
408
|
+
.describe('Optional base image to establish from (e.g. a rough location shot).'),
|
|
409
|
+
userNotes: z.string().optional(),
|
|
410
|
+
}),
|
|
411
|
+
async run(input, ctx) {
|
|
412
|
+
return ok(await ctx.desktop().post('/agent/environments/generate-plate', {
|
|
413
|
+
environmentId: input.environmentId,
|
|
414
|
+
projectId: input.projectId,
|
|
415
|
+
baseAssetIds: input.baseAssetId ? [input.baseAssetId] : [],
|
|
416
|
+
userNotes: input.userNotes,
|
|
417
|
+
}));
|
|
418
|
+
},
|
|
419
|
+
};
|
|
375
420
|
// ── Storyboards ─────────────────────────────────────────────────
|
|
376
421
|
export const listStoryboards = {
|
|
377
422
|
id: 'slates_list_storyboards',
|
|
@@ -932,9 +977,12 @@ const KLING_TIER_MAP = {
|
|
|
932
977
|
};
|
|
933
978
|
function videoCostKey(input) {
|
|
934
979
|
if (input.model.startsWith('seedance')) {
|
|
935
|
-
// Mirrors seedanceCreditKey() in slate/src/shared/pricing.ts (res × duration).
|
|
980
|
+
// Mirrors seedanceCreditKey() in slate/src/shared/pricing.ts (face × res × duration).
|
|
981
|
+
// Face route (AI-character face ref) bills the EvoLink relaxed rate (+10%) via
|
|
982
|
+
// the `-face-` key; faceless uses the cheaper BytePlus key.
|
|
936
983
|
const res = input.videoResolution ?? '1080p';
|
|
937
|
-
|
|
984
|
+
const face = input.seedanceFace ? '-face' : '';
|
|
985
|
+
return `${input.model}${face}-${res}-${input.duration}s`;
|
|
938
986
|
}
|
|
939
987
|
if (input.model.startsWith('veo')) {
|
|
940
988
|
const is4k = input.videoResolution === '4k';
|
|
@@ -979,9 +1027,14 @@ export const generateVideo = {
|
|
|
979
1027
|
firstFrameAssetId: z.string().uuid().optional().describe('Asset id from the project — used as the starting frame for image-to-video. Must already exist in the project.'),
|
|
980
1028
|
lastFrameAssetId: z.string().uuid().optional().describe('Asset id from the project — used as the ending frame. Veo and Seedance only. Pairs with firstFrameAssetId for guided transitions.'),
|
|
981
1029
|
ingredientAssetIds: z.array(z.string().uuid()).max(9).optional().describe('Asset ids used as visual reference / ingredients for Kling Omni or Seedance. Up to 9 (Seedance) or 4 (Kling).'),
|
|
1030
|
+
characterAssetIds: z.array(z.string().uuid()).optional().describe('Asset ids of character sheets — keeps a character consistent across the shot. From a Slates project.'),
|
|
1031
|
+
environmentAssetIds: z.array(z.string().uuid()).optional().describe('Asset ids of environment grids — keeps a location/setting consistent across the shot. From a Slates project.'),
|
|
1032
|
+
styleAssetIds: z.array(z.string().uuid()).optional().describe('Asset ids of style references — locks the visual style of the shot. From a Slates project.'),
|
|
1033
|
+
videoReferenceAssetId: z.string().uuid().optional().describe('Seedance ONLY: an existing VIDEO asset to use as a reference (edit / relocate an existing clip — Seedance ref-to-video). 2-15s. If the clip contains a human/AI character, pair with seedanceFace=true so it routes to the face-capable provider (the default Seedance route blocks people). Ignored by Kling/Veo.'),
|
|
982
1034
|
sound: z.boolean().optional().describe('Kling Omni / Veo / Seedance: enable audio generation. Default true.'),
|
|
983
1035
|
audioLanguage: z.enum(['EN', 'ZH', 'JA', 'KO', 'ES']).optional().describe('Kling Omni only — language for dialogue.'),
|
|
984
1036
|
generateMusic: z.boolean().optional().describe('Kling Omni only — auto-generate background music.'),
|
|
1037
|
+
seedanceFace: z.boolean().optional().describe('Seedance ONLY: set true when a reference/ingredient shows an AI-character\'s FACE. Faces are blocked on the default (cheaper) Seedance route, so this reroutes the gen to a face-capable provider and costs ~10% more (the cost key becomes seedance-2-face-*). Leave false/unset for faceless or object-only references. No effect on Kling/Veo. Real-person faces are not supported on any route.'),
|
|
985
1038
|
negativePrompt: z.string().optional(),
|
|
986
1039
|
background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
|
|
987
1040
|
confirm: z.boolean().optional().describe('Set true after explicit user OK to bypass the >$0.50 cost confirm gate (which fires for almost every video gen since they\'re expensive).'),
|
|
@@ -1039,6 +1092,7 @@ export const generateVideo = {
|
|
|
1039
1092
|
duration: input.duration,
|
|
1040
1093
|
videoResolution: input.videoResolution,
|
|
1041
1094
|
sound: input.sound,
|
|
1095
|
+
seedanceFace: input.seedanceFace,
|
|
1042
1096
|
});
|
|
1043
1097
|
const entry = registry.models.find((m) => m.model === costKey);
|
|
1044
1098
|
if (!entry) {
|
|
@@ -1064,6 +1118,15 @@ export const generateVideo = {
|
|
|
1064
1118
|
for (const id of input.ingredientAssetIds ?? []) {
|
|
1065
1119
|
referenceRefs.push({ id, type: 'image', role: 'ingredient' });
|
|
1066
1120
|
}
|
|
1121
|
+
for (const id of input.characterAssetIds ?? []) {
|
|
1122
|
+
referenceRefs.push({ id, type: 'image', role: 'character' });
|
|
1123
|
+
}
|
|
1124
|
+
for (const id of input.environmentAssetIds ?? []) {
|
|
1125
|
+
referenceRefs.push({ id, type: 'image', role: 'environment' });
|
|
1126
|
+
}
|
|
1127
|
+
for (const id of input.styleAssetIds ?? []) {
|
|
1128
|
+
referenceRefs.push({ id, type: 'image', role: 'style' });
|
|
1129
|
+
}
|
|
1067
1130
|
const hasReferences = referenceRefs.length > 0;
|
|
1068
1131
|
if ((totalCents > 50 || hasReferences) && !input.confirm) {
|
|
1069
1132
|
const previews = hasReferences ? await previewAssets(ctx, referenceRefs) : [];
|
|
@@ -1111,9 +1174,14 @@ export const generateVideo = {
|
|
|
1111
1174
|
firstFrameAssetId: input.firstFrameAssetId,
|
|
1112
1175
|
lastFrameAssetId: input.lastFrameAssetId,
|
|
1113
1176
|
ingredientAssetIds: input.ingredientAssetIds ?? [],
|
|
1177
|
+
characterAssetIds: input.characterAssetIds ?? [],
|
|
1178
|
+
environmentAssetIds: input.environmentAssetIds ?? [],
|
|
1179
|
+
styleAssetIds: input.styleAssetIds ?? [],
|
|
1180
|
+
videoReferenceAssetId: input.videoReferenceAssetId,
|
|
1114
1181
|
sound: input.sound,
|
|
1115
1182
|
audioLanguage: input.audioLanguage,
|
|
1116
1183
|
generateMusic: input.generateMusic,
|
|
1184
|
+
seedanceFace: input.seedanceFace,
|
|
1117
1185
|
negativePrompt: input.negativePrompt,
|
|
1118
1186
|
background: input.background,
|
|
1119
1187
|
});
|
|
@@ -1163,6 +1231,7 @@ export const generateLipSync = {
|
|
|
1163
1231
|
ttsSpeed: z.number().min(0.5).max(2).optional().describe('TTS speech rate. Default 1.0. Range 0.5-2.0.'),
|
|
1164
1232
|
audioFilePath: z.string().optional().describe('Required when audioMethod=upload. Absolute path to the audio file on the user\'s machine (mp3, wav, m4a).'),
|
|
1165
1233
|
avatarModel: z.enum(['avatar-standard', 'avatar-pro']).optional().describe('Image-source only. avatar-standard ($0.42/5s) for general use. avatar-pro ($0.86/5s) for sharper face fidelity. Ignored when sourceType=video.'),
|
|
1234
|
+
klingProvider: z.enum(['fal', 'kling']).optional().describe('Provider routing for the Kling call. "fal" (default) uses Slates credits. "kling" uses the user\'s own BYOK Kling key if configured — omit unless the user explicitly wants BYOK.'),
|
|
1166
1235
|
background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
|
|
1167
1236
|
confirm: z.boolean().optional().describe('Set true to bypass the >$0.50 cost confirm gate. Required for avatar-pro.'),
|
|
1168
1237
|
}),
|
|
@@ -1231,6 +1300,7 @@ export const generateLipSync = {
|
|
|
1231
1300
|
ttsSpeed: input.ttsSpeed,
|
|
1232
1301
|
audioFilePath: input.audioFilePath,
|
|
1233
1302
|
avatarModel: input.avatarModel,
|
|
1303
|
+
klingProvider: input.klingProvider,
|
|
1234
1304
|
estimatedCost: totalCents,
|
|
1235
1305
|
background: input.background,
|
|
1236
1306
|
});
|
|
@@ -1276,6 +1346,7 @@ export const generateMotionTransfer = {
|
|
|
1276
1346
|
motionModel: z.enum(['kling-mc-std', 'kling-mc-pro']).optional().describe('std ($0.95) for general motion. pro ($1.26) for cleaner anatomy + identity preservation. Default pro — pick std deliberately for cost savings.'),
|
|
1277
1347
|
characterOrientation: z.enum(['video', 'image']).optional().describe('"video" = use the source video\'s framing. "image" = use the target image\'s framing. Default video.'),
|
|
1278
1348
|
prompt: z.string().optional().describe('Optional refinement prompt. Read slates-prompting-motion-transfer for guidance.'),
|
|
1349
|
+
klingProvider: z.enum(['fal', 'kling']).optional().describe('Provider routing for the Kling call. "fal" (default) uses Slates credits. "kling" uses the user\'s own BYOK Kling key if configured — omit unless the user explicitly wants BYOK.'),
|
|
1279
1350
|
background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
|
|
1280
1351
|
confirm: z.boolean().optional().describe('Set true to bypass the >$0.50 confirm gate. Required — both tiers exceed.'),
|
|
1281
1352
|
}),
|
|
@@ -1322,6 +1393,7 @@ export const generateMotionTransfer = {
|
|
|
1322
1393
|
motionModel,
|
|
1323
1394
|
characterOrientation: input.characterOrientation ?? 'video',
|
|
1324
1395
|
prompt: input.prompt,
|
|
1396
|
+
klingProvider: input.klingProvider,
|
|
1325
1397
|
estimatedCost: totalCents,
|
|
1326
1398
|
background: input.background,
|
|
1327
1399
|
});
|
|
@@ -1874,6 +1946,8 @@ export const ALL_OPERATIONS = [
|
|
|
1874
1946
|
setCharacterExpression,
|
|
1875
1947
|
listEnvironments,
|
|
1876
1948
|
createEnvironment,
|
|
1949
|
+
generateCharacterSheets,
|
|
1950
|
+
generateEnvironmentPlate,
|
|
1877
1951
|
listStoryboards,
|
|
1878
1952
|
createStoryboard,
|
|
1879
1953
|
getStoryboardWithFrames,
|
package/dist/prompts/index.d.ts
CHANGED
package/dist/prompts/index.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// plan). So a change here must be applied to those copies in the same pass for
|
|
11
11
|
// now. SSOT map: second-brain business/projects/slates/product/prompting-ssot.md
|
|
12
12
|
export * from './reference-rules.js';
|
|
13
|
+
export * from './reference-composer.js';
|
|
13
14
|
export * from './content-policy.js';
|
|
14
15
|
export * from './style-library.js';
|
|
15
16
|
export * from './model-facts.js';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type ReferenceKind = 'character' | 'environment' | 'style' | 'pinned' | 'first-frame' | 'last-frame' | 'video';
|
|
2
|
+
export interface ReferenceMedia {
|
|
3
|
+
path: string;
|
|
4
|
+
mediaKind: 'image' | 'video';
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A named bucket of reference media that can be @mentioned. The whole definition
|
|
8
|
+
* of a reference. It does NOT mandate a structure — one photo or eight angles,
|
|
9
|
+
* the composer treats them all as "this is one named thing".
|
|
10
|
+
*/
|
|
11
|
+
export interface ReferenceGroup {
|
|
12
|
+
/** '@Marcus' | '@Cafe' | '#noir' | null (pinned / frames / picked video) */
|
|
13
|
+
token: string | null;
|
|
14
|
+
/** Display + citation name: 'Marcus' | 'the cafe' | 'noir'. Used verbatim. */
|
|
15
|
+
name: string;
|
|
16
|
+
kind: ReferenceKind;
|
|
17
|
+
/** A group can carry several images (e.g. turnaround + expression). */
|
|
18
|
+
media: ReferenceMedia[];
|
|
19
|
+
}
|
|
20
|
+
export interface ComposedReferences {
|
|
21
|
+
/** The composed prompt: user's words lead, references cited inline as "image N". */
|
|
22
|
+
prompt: string;
|
|
23
|
+
/** Free-reference image paths in cited order — flatten yields "image 1..N". */
|
|
24
|
+
orderedImagePaths: string[];
|
|
25
|
+
/** Video reference paths in cited order — "video 1..M". */
|
|
26
|
+
orderedVideoPaths: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Compose the raw prompt (mentions intact) + an ORDERED list of reference groups
|
|
30
|
+
* into the named prompt text + ordered media the API receives. The group order
|
|
31
|
+
* IS the send order — image numbers are assigned by walking the list, so the
|
|
32
|
+
* caller controls numbering by ordering the list (default: pinned/base first,
|
|
33
|
+
* then @mentions in first-appearance order, then #style last).
|
|
34
|
+
*
|
|
35
|
+
* @param rawPrompt the user's prompt with @mentions / #tags still in it
|
|
36
|
+
* @param groups the single ordered ReferenceGroup[] (rail + prompt read this)
|
|
37
|
+
* @param opts.startImageNumber images already attached AHEAD of these groups
|
|
38
|
+
* (e.g. a grid-cell base that is "image 1" before the free-refs). Free-ref
|
|
39
|
+
* numbering and orderedImagePaths begin after it. Default 0.
|
|
40
|
+
*/
|
|
41
|
+
export interface ComposeOptions {
|
|
42
|
+
startImageNumber?: number;
|
|
43
|
+
startVideoNumber?: number;
|
|
44
|
+
}
|
|
45
|
+
export declare function composeReferences(rawPrompt: string, groups: ReferenceGroup[], opts?: ComposeOptions): ComposedReferences;
|
|
46
|
+
//# sourceMappingURL=reference-composer.d.ts.map
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Reference composition — the prompt-as-SSOT composer.
|
|
2
|
+
//
|
|
3
|
+
// THE PRINCIPLE: the prompt box is the single source of truth. The app's ENTIRE
|
|
4
|
+
// contribution to the model prompt is (a) translate @mentions/#tags into the
|
|
5
|
+
// lightweight "image N" naming the models actually parse, and (b) name
|
|
6
|
+
// token-less references (pinned images, frames, videos) by number. Nothing
|
|
7
|
+
// else — no role essays, no "ignore the outfit", no injected lighting/expression
|
|
8
|
+
// rules. What the user writes is what leads.
|
|
9
|
+
//
|
|
10
|
+
// This is the canonical implementation. It is mirrored byte-for-byte into the
|
|
11
|
+
// desktop app's `slate/src/shared/promptComposition.ts` (the desktop installs
|
|
12
|
+
// the published @slatesvideo/shared from npm and cannot file-import this source,
|
|
13
|
+
// so the mirror carries a header pointing here). Both the agent/MCP paths and
|
|
14
|
+
// the desktop generation + rail read from this one function, so the rail's badge
|
|
15
|
+
// numbers and the prompt's "image N" citations can never desync.
|
|
16
|
+
//
|
|
17
|
+
// Naming is the ONLY identity signal. Citing both of a subject's images as the
|
|
18
|
+
// SAME name ("Marcus (images 1 and 2)") tells the model they are ONE entity —
|
|
19
|
+
// which is what prevents a multi-image bucket from averaging into a blended
|
|
20
|
+
// face. This IS each model's own official consistency lever (NB2 "assign a
|
|
21
|
+
// distinct name", Seedance "Reference Subject_N in Image_N", Kling "reuse a fixed
|
|
22
|
+
// label verbatim"); the heavy role-essay block was the off-doctrine part.
|
|
23
|
+
// Normalize a name/token for matching: drop the sigil, lowercase, strip
|
|
24
|
+
// spaces/underscores/hyphens. "@big_red" / "@Big Red" / "#Big-Red" all collapse
|
|
25
|
+
// to the same key. Identical to the agent-side resolver's `norm`.
|
|
26
|
+
function normToken(s) {
|
|
27
|
+
return s.toLowerCase().replace(/[@#]/g, '').replace(/[\s_-]+/g, '');
|
|
28
|
+
}
|
|
29
|
+
// Free-reference IMAGE kinds get an "image N" number. Frames are transported in
|
|
30
|
+
// their own dedicated slots (start/last frame) by the per-model adapter and are
|
|
31
|
+
// NOT part of the free-reference numbering — see the reference-rules note that
|
|
32
|
+
// first/last-frame can't mix with free refs on models like Seedance.
|
|
33
|
+
function isFreeRefImageKind(kind) {
|
|
34
|
+
return kind === 'character' || kind === 'environment' || kind === 'style' || kind === 'pinned';
|
|
35
|
+
}
|
|
36
|
+
/** "image 5" / "images 5 and 6" / "images 5, 6 and 7" (lowercase, for inline use). */
|
|
37
|
+
function citeImages(nums) {
|
|
38
|
+
const noun = nums.length === 1 ? 'image' : 'images';
|
|
39
|
+
return `${noun} ${joinNums(nums)}`;
|
|
40
|
+
}
|
|
41
|
+
function joinNums(nums) {
|
|
42
|
+
if (nums.length === 1)
|
|
43
|
+
return String(nums[0]);
|
|
44
|
+
if (nums.length === 2)
|
|
45
|
+
return `${nums[0]} and ${nums[1]}`;
|
|
46
|
+
return `${nums.slice(0, -1).join(', ')} and ${nums[nums.length - 1]}`;
|
|
47
|
+
}
|
|
48
|
+
// Humanize an unresolved @token to plain words (preserve the legacy cleanPrompt
|
|
49
|
+
// fallback: @big_red → "Big Red", @forest3 → "Forest3"). Never send a raw token.
|
|
50
|
+
function humanizeToken(raw) {
|
|
51
|
+
return raw
|
|
52
|
+
.split(/[_-]/)
|
|
53
|
+
.map((w) => (w ? w.charAt(0).toUpperCase() + w.slice(1) : w))
|
|
54
|
+
.join(' ');
|
|
55
|
+
}
|
|
56
|
+
export function composeReferences(rawPrompt, groups, opts = {}) {
|
|
57
|
+
// ── 1. Assign global numbers by walking the list in order ──
|
|
58
|
+
let imageNum = opts.startImageNumber ?? 0;
|
|
59
|
+
let videoNum = opts.startVideoNumber ?? 0;
|
|
60
|
+
const orderedImagePaths = [];
|
|
61
|
+
const orderedVideoPaths = [];
|
|
62
|
+
const numbered = groups.map((g) => {
|
|
63
|
+
const imageNums = [];
|
|
64
|
+
const videoNums = [];
|
|
65
|
+
for (const m of g.media) {
|
|
66
|
+
if (m.mediaKind === 'video' && g.kind === 'video') {
|
|
67
|
+
videoNum += 1;
|
|
68
|
+
videoNums.push(videoNum);
|
|
69
|
+
orderedVideoPaths.push(m.path);
|
|
70
|
+
}
|
|
71
|
+
else if (m.mediaKind === 'image' && isFreeRefImageKind(g.kind)) {
|
|
72
|
+
imageNum += 1;
|
|
73
|
+
imageNums.push(imageNum);
|
|
74
|
+
orderedImagePaths.push(m.path);
|
|
75
|
+
}
|
|
76
|
+
// first-frame / last-frame media: not numbered, not in the free-ref pool.
|
|
77
|
+
}
|
|
78
|
+
return { ...g, imageNums, videoNums };
|
|
79
|
+
});
|
|
80
|
+
// ── 2. Inline-name token groups in the prompt body ──
|
|
81
|
+
// For each character/environment group whose token appears in the prompt, the
|
|
82
|
+
// FIRST occurrence becomes "Name (image N)"; later ones become just "Name".
|
|
83
|
+
// Style tokens are removed (a single trailing clause carries the style). Token
|
|
84
|
+
// groups NOT found in the prompt fall through to a key line in step 3.
|
|
85
|
+
const tokenGroups = numbered.filter((g) => g.token && (g.kind === 'character' || g.kind === 'environment' || g.kind === 'style'));
|
|
86
|
+
const byNorm = new Map();
|
|
87
|
+
for (const g of tokenGroups)
|
|
88
|
+
byNorm.set(normToken(g.token), g);
|
|
89
|
+
const seenFirst = new Set();
|
|
90
|
+
const matchedInPrompt = new Set();
|
|
91
|
+
// First strip "in/with the style of #tag" phrases so the style reads as a
|
|
92
|
+
// clean trailing clause, not a dangling preposition (legacy cleanPrompt behaviour).
|
|
93
|
+
let body = rawPrompt.replace(/\s+(with|in)\s+the\s+style\s+of\s+([@#])([\w-]+)/gi, (full, _prep, sigil, tok) => {
|
|
94
|
+
const g = byNorm.get(normToken(`${sigil}${tok}`));
|
|
95
|
+
if (g && g.kind === 'style') {
|
|
96
|
+
matchedInPrompt.add(normToken(`${sigil}${tok}`));
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
return full;
|
|
100
|
+
});
|
|
101
|
+
body = body.replace(/([@#])([\w-]+)/g, (_full, _sigil, tok) => {
|
|
102
|
+
const key = normToken(`${_sigil}${tok}`);
|
|
103
|
+
const g = byNorm.get(key);
|
|
104
|
+
if (!g) {
|
|
105
|
+
// Unresolved token. A #unknown vanishes; an @unknown humanizes to words.
|
|
106
|
+
return _sigil === '#' ? '' : humanizeToken(tok);
|
|
107
|
+
}
|
|
108
|
+
matchedInPrompt.add(key);
|
|
109
|
+
if (g.kind === 'style')
|
|
110
|
+
return ''; // styles never inline — trailing clause only
|
|
111
|
+
if (!seenFirst.has(key)) {
|
|
112
|
+
seenFirst.add(key);
|
|
113
|
+
return `${g.name} (${citeImages(g.imageNums)})`;
|
|
114
|
+
}
|
|
115
|
+
return g.name;
|
|
116
|
+
});
|
|
117
|
+
// Collapse the whitespace the token removals left behind.
|
|
118
|
+
body = body.replace(/[ \t]{2,}/g, ' ').replace(/\s+([,.;:!?])/g, '$1').trim();
|
|
119
|
+
// ── 3. Build the key lines for token-less / unmatched-token groups ──
|
|
120
|
+
// Video sources, pinned/base canvases, and picked subjects that have no token
|
|
121
|
+
// in the prompt each get ONE short neutral key line (never an essay). The user's
|
|
122
|
+
// own prompt supplies the intent ("this exact frame", "the man becomes…").
|
|
123
|
+
const topKeys = [];
|
|
124
|
+
// Video sources first ("Video 1 is the motion source.").
|
|
125
|
+
for (const g of numbered) {
|
|
126
|
+
if (g.kind === 'video' && g.videoNums.length > 0) {
|
|
127
|
+
const noun = g.videoNums.length === 1 ? 'Video' : 'Videos';
|
|
128
|
+
const verb = g.videoNums.length === 1 ? 'is' : 'are';
|
|
129
|
+
topKeys.push(`${noun} ${joinNums(g.videoNums)} ${verb} the motion source.`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Pinned/base references ("Image 1 is a provided reference.").
|
|
133
|
+
for (const g of numbered) {
|
|
134
|
+
if (g.kind === 'pinned' && g.imageNums.length > 0) {
|
|
135
|
+
const noun = g.imageNums.length === 1 ? 'Image' : 'Images';
|
|
136
|
+
const tail = g.imageNums.length === 1 ? 'is a provided reference.' : 'are provided references.';
|
|
137
|
+
topKeys.push(`${noun} ${joinNums(g.imageNums)} ${tail}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Token-less or not-in-prompt subjects/environments ("Image 1 is Marcus.").
|
|
141
|
+
for (const g of numbered) {
|
|
142
|
+
if ((g.kind === 'character' || g.kind === 'environment') && g.imageNums.length > 0) {
|
|
143
|
+
const tokenWasMatched = g.token && matchedInPrompt.has(normToken(g.token));
|
|
144
|
+
if (!tokenWasMatched) {
|
|
145
|
+
const noun = g.imageNums.length === 1 ? 'Image' : 'Images';
|
|
146
|
+
const verb = g.imageNums.length === 1 ? 'is' : 'are';
|
|
147
|
+
topKeys.push(`${noun} ${joinNums(g.imageNums)} ${verb} ${g.name}.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// ── 4. Style trailing clause (one, at the end — style reads best last) ──
|
|
152
|
+
const styleNums = [];
|
|
153
|
+
for (const g of numbered) {
|
|
154
|
+
if (g.kind === 'style')
|
|
155
|
+
styleNums.push(...g.imageNums);
|
|
156
|
+
}
|
|
157
|
+
const styleClauses = [];
|
|
158
|
+
if (styleNums.length > 0) {
|
|
159
|
+
styleClauses.push(`Render in the visual style of ${citeImages(styleNums)}.`);
|
|
160
|
+
}
|
|
161
|
+
// ── 5. Assemble: [top key line] · [body] · [style clause], blank-line separated ──
|
|
162
|
+
const parts = [];
|
|
163
|
+
if (topKeys.length > 0)
|
|
164
|
+
parts.push(topKeys.join(' '));
|
|
165
|
+
if (body)
|
|
166
|
+
parts.push(body);
|
|
167
|
+
if (styleClauses.length > 0)
|
|
168
|
+
parts.push(styleClauses.join(' '));
|
|
169
|
+
return {
|
|
170
|
+
prompt: parts.join('\n\n'),
|
|
171
|
+
orderedImagePaths,
|
|
172
|
+
orderedVideoPaths,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=reference-composer.js.map
|
|
@@ -30,5 +30,5 @@ export declare const REFERENCE_RULES_HEADLINE = "Identity = a few flat-lit neutr
|
|
|
30
30
|
* plans/2026-06-25-slates-prompting-system-overhaul.md). The TARGET is to
|
|
31
31
|
* inject this text so it can't drift; today it's reconciled manually.
|
|
32
32
|
*/
|
|
33
|
-
export declare const REFERENCE_RULES_TEXT = "## Reference rules (how to use reference images)\n\nIdentity = a few flat-lit neutral angles; one reference per role, labeled; 2-4 refs not 12; describe environments instead of feeding a grid.\n\n1. **2-4 strong references beat both extremes.** Not 1 (warps toward itself), not 12 (averages worse). Start with 2-3 focused refs.\n2. **One reference per ROLE, labeled in the prompt.** Identity / style-grade / environment. The model does not infer roles from order \u2014 name each role in the prompt text. Same-role competitors drift.\n3. **Attach both sheets \u2014
|
|
33
|
+
export declare const REFERENCE_RULES_TEXT = "## Reference rules (how to use reference images)\n\nIdentity = a few flat-lit neutral angles; one reference per role, labeled; 2-4 refs not 12; describe environments instead of feeding a grid.\n\n1. **2-4 strong references beat both extremes.** Not 1 (warps toward itself), not 12 (averages worse). Start with 2-3 focused refs.\n2. **One reference per ROLE, labeled in the prompt.** Identity / style-grade / environment. The model does not infer roles from order \u2014 name each role in the prompt text. Same-role competitors drift.\n3. **Attach both sheets \u2014 NAME them as one entity, don't gate them.** Attach the full-body turnaround (body/proportion/outfit) AND the close-up expression sheet (high-res facial detail: eyes, skin, teeth, bone structure). NAME both inline as the same subject (\"Marcus (images 1 and 2)\") \u2014 the shared name tells the model they are ONE person, which is what stops the varied expressions from averaging the face. Do **not** inject a role essay (\"use for identity, ignore the outfit/lighting, render neutral\") \u2014 that drags the studio-lit sheet's wardrobe + lighting into the scene; the user's prompt owns wardrobe, expression, and lighting. Naming-as-one-entity IS each model's official lever (NB2 \"assign a distinct name\"; Seedance \"Reference Subject_N in Image_N\"; Kling \"reuse a fixed label verbatim\"). The trend is MORE references \u2014 addressing each by name is what makes many refs work.\n4. **Flat-light identity refs.** Prep identity refs with flat, even, shadowless lighting on a plain neutral background. Studio-lit / scene-lit sheets bleed their lighting into every generation (the studio-lit sheet \u2192 \"green-screen-pasted in front of mountains\" failure). Reference prep beats prompting here.\n5. **Environment: describe it, don't feed a grid.** Default to describing the location in words. Reserve an environment reference for a mandatory exact-match, and then use ONE clean establishing image with natural, even ambient lighting that reads as the location's real light, not a studio setup \u2014 never a multi-panel grid fed whole.\n6. **Grids: explore, don't input.** Use grids to explore compositions cheaply, then pick a cell. Never feed a grid back in as a reference \u2014 cells share a split detail budget and generate jointly, so flaws propagate.\n7. **Reuse the same refs across all shots.** Lock a set and reuse it; swapping refs mid-sequence causes drift.\n8. **Legible in-shot text \u2192 bake it into an image start frame, never trust text-to-video.** Animate from the locked frame.\n9. **I2V / own-footage superpower.** Restyle your own clip keeping the performance; delayed-VFX on \"video one\"; marker-object insertion; video-as-ref for a series. Describe ONLY what changes.\n10. **Style transform by natural language.** Default keeps the source's art style; an optional plain-text instruction transforms it (\"anime \u2192 real person\"). No preset pickers.";
|
|
34
34
|
//# sourceMappingURL=reference-rules.d.ts.map
|
|
@@ -25,10 +25,10 @@ export const REFERENCE_RULES = [
|
|
|
25
25
|
grade: 'community',
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
|
-
id: 'identity-
|
|
29
|
-
title: 'Attach both sheets —
|
|
30
|
-
rule: 'Attach BOTH the full-body turnaround (body/proportion/outfit) AND the close-up expression sheet (high-res facial detail).
|
|
31
|
-
why: '
|
|
28
|
+
id: 'identity-name-as-one-entity',
|
|
29
|
+
title: 'Attach both sheets — NAME them as one entity, don\'t gate them',
|
|
30
|
+
rule: 'Attach BOTH the full-body turnaround (body/proportion/outfit) AND the close-up expression sheet (high-res facial detail). NAME both inline as the SAME subject ("Marcus (images 1 and 2)") — that shared name is what tells the model they are ONE person and stops the varied expressions from averaging the face. Do NOT inject a role essay ("use for identity, ignore the outfit/lighting, render neutral"): the user\'s prompt owns wardrobe, expression, and lighting.',
|
|
31
|
+
why: 'Naming both images as one entity IS each model\'s OWN official consistency lever — NB2 "assign a distinct name to each character/object"; Seedance "Reference <Subject_N> in <Image_N>"; Kling "reuse a fixed label verbatim". The old heavy role-essay was the OFF-doctrine part: telling the model to "use for identity" while injecting "ignore the outfit" dragged the studio-lit sheet\'s wardrobe + lighting into scenes that explicitly wanted otherwise (the movie-still injection failure). The close-ups still carry far more facial signal (eyes, skin, teeth, bone structure) than a turnaround\'s postage-stamp faces, so attach both — the NAME, not an instruction, is what makes many refs work. The trend is MORE references (video/audio/3D into Seedance-class models), all addressed by name.',
|
|
32
32
|
grade: 'Eric-test',
|
|
33
33
|
},
|
|
34
34
|
{
|
|
@@ -109,7 +109,7 @@ ${REFERENCE_RULES_HEADLINE}
|
|
|
109
109
|
|
|
110
110
|
1. **2-4 strong references beat both extremes.** Not 1 (warps toward itself), not 12 (averages worse). Start with 2-3 focused refs.
|
|
111
111
|
2. **One reference per ROLE, labeled in the prompt.** Identity / style-grade / environment. The model does not infer roles from order — name each role in the prompt text. Same-role competitors drift.
|
|
112
|
-
3. **Attach both sheets —
|
|
112
|
+
3. **Attach both sheets — NAME them as one entity, don't gate them.** Attach the full-body turnaround (body/proportion/outfit) AND the close-up expression sheet (high-res facial detail: eyes, skin, teeth, bone structure). NAME both inline as the same subject ("Marcus (images 1 and 2)") — the shared name tells the model they are ONE person, which is what stops the varied expressions from averaging the face. Do **not** inject a role essay ("use for identity, ignore the outfit/lighting, render neutral") — that drags the studio-lit sheet's wardrobe + lighting into the scene; the user's prompt owns wardrobe, expression, and lighting. Naming-as-one-entity IS each model's official lever (NB2 "assign a distinct name"; Seedance "Reference Subject_N in Image_N"; Kling "reuse a fixed label verbatim"). The trend is MORE references — addressing each by name is what makes many refs work.
|
|
113
113
|
4. **Flat-light identity refs.** Prep identity refs with ${IDENTITY_LIGHTING} on a plain neutral background. Studio-lit / scene-lit sheets bleed their lighting into every generation (the studio-lit sheet → "green-screen-pasted in front of mountains" failure). Reference prep beats prompting here.
|
|
114
114
|
5. **Environment: describe it, don't feed a grid.** Default to describing the location in words. Reserve an environment reference for a mandatory exact-match, and then use ONE clean establishing image with ${ENVIRONMENT_NATURAL_LIGHT} — never a multi-panel grid fed whole.
|
|
115
115
|
6. **Grids: explore, don't input.** Use grids to explore compositions cheaply, then pick a cell. Never feed a grid back in as a reference — cells share a split detail budget and generate jointly, so flaws propagate.
|