@slatesvideo/shared 0.4.1 → 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.
@@ -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.