@slatesvideo/shared 0.4.2 → 0.4.3

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.
@@ -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
  }>;
@@ -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
- return `${input.model}-${res}-${input.duration}s`;
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,
@@ -1,4 +1,5 @@
1
1
  export * from './reference-rules.js';
2
+ export * from './reference-composer.js';
2
3
  export * from './content-policy.js';
3
4
  export * from './style-library.js';
4
5
  export * from './model-facts.js';
@@ -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 label them, 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). Label both as identity references and tell the model to render the SCENE's expression (default neutral). An *unlabeled* expression sheet hurts (the model copies the varied expressions \u2192 midpoint face); *labeled*, the close-ups are a fidelity win. The trend is MORE references \u2014 role-labeling 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.";
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-label-roles',
29
- title: 'Attach both sheets — label them, 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). Label them as identity references and tell the model to render the SCENE\'s expression (default neutral). The label not gating is what stops the multiple expressions from averaging the face.',
31
- why: 'An UNLABELED expression sheet hurts: the model copies its varied expressions and the face drifts to a midpoint. Labeled ("use for identity; render the scene\'s expression"), the close-ups are a fidelity win they carry far more facial signal (eyes, skin, teeth, bone structure) than the postage-stamp faces in a full-body turnaround. The trend is MORE references (video/audio/3D into Seedance-class models); role-labeling is what makes many refs work, so lean into attaching rich refs and labeling every role.',
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 — label them, 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). Label both as identity references and tell the model to render the SCENE's expression (default neutral). An *unlabeled* expression sheet hurts (the model copies the varied expressions midpoint face); *labeled*, the close-ups are a fidelity win. The trend is MORE references — role-labeling is what makes many refs work.
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.