@slatesvideo/shared 0.5.5 → 0.5.7

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.
Files changed (29) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.js +6 -3
  3. package/dist/operations/index.d.ts +106 -19
  4. package/dist/operations/index.js +624 -172
  5. package/dist/prompts/character-sheet.d.ts +2 -1
  6. package/dist/prompts/character-sheet.js +82 -13
  7. package/dist/prompts/model-facts.d.ts +43 -1
  8. package/dist/prompts/model-facts.js +104 -2
  9. package/dist/prompts/prompting-tips.d.ts +1 -1
  10. package/dist/prompts/prompting-tips.js +208 -5
  11. package/dist/prompts/reference-composer.d.ts +21 -3
  12. package/dist/prompts/reference-composer.js +80 -10
  13. package/dist/prompts/reference-rules.d.ts +19 -2
  14. package/dist/prompts/reference-rules.js +18 -1
  15. package/dist/skills/content.js +8 -5
  16. package/exports/slates-prompt-builder/generated/SKILL.md +2 -2
  17. package/exports/slates-prompt-builder/generated/reference-character.md +9 -4
  18. package/exports/slates-prompt-builder/generated/reference-seedance.md +12 -1
  19. package/exports/slates-prompt-builder/generated/slates-prompt-builder-manifest.json +11 -11
  20. package/exports/slates-prompt-builder/generated/slates-prompt-builder.skill +0 -0
  21. package/package.json +1 -1
  22. package/skills/slates-character-identity.md +9 -4
  23. package/skills/slates-model-selection.md +39 -11
  24. package/skills/slates-prompting-elevenlabs.md +69 -0
  25. package/skills/slates-prompting-lip-sync.md +12 -14
  26. package/skills/slates-prompting-motion-transfer.md +18 -14
  27. package/skills/slates-prompting-seed-audio.md +110 -0
  28. package/skills/slates-prompting-seedance-2-5.md +215 -0
  29. package/skills/slates-prompting-seedance.md +17 -1
@@ -1,7 +1,7 @@
1
- export type ReferenceKind = 'character' | 'environment' | 'style' | 'pinned' | 'first-frame' | 'last-frame' | 'video';
1
+ export type ReferenceKind = 'character' | 'environment' | 'style' | 'pinned' | 'first-frame' | 'last-frame' | 'video' | 'video-ref' | 'audio-ref';
2
2
  export interface ReferenceMedia {
3
3
  path: string;
4
- mediaKind: 'image' | 'video';
4
+ mediaKind: 'image' | 'video' | 'audio';
5
5
  }
6
6
  /**
7
7
  * A named bucket of reference media that can be @mentioned. The whole definition
@@ -22,8 +22,25 @@ export interface ComposedReferences {
22
22
  prompt: string;
23
23
  /** Free-reference image paths in cited order — flatten yields "image 1..N". */
24
24
  orderedImagePaths: string[];
25
- /** Video reference paths in cited order — "video 1..M". */
25
+ /** Video paths in cited order — "video 1..M". Edit sources and reference
26
+ * clips SHARE this list and one counter, so a mixed state can never emit
27
+ * two "Video 1"s. */
26
28
  orderedVideoPaths: string[];
29
+ /** Reference audio paths in cited order — "audio 1..K". */
30
+ orderedAudioPaths: string[];
31
+ /**
32
+ * Tokens written in the prompt that matched NO reference group, as authored
33
+ * (`'#noir'`, `'@bob'`), first-appearance order, deduped case-insensitively.
34
+ *
35
+ * 🚨 THIS FIELD EXISTS BECAUSE THE ALTERNATIVE IS A SILENT EDIT. A `#tag` with
36
+ * no matching style is DELETED from the text — a raw tag confuses every model,
37
+ * so removing it is right, but removing it without saying so changes what the
38
+ * user asked for behind their back. Prompt-transparency doctrine (slate
39
+ * `CLAUDE.md`) requires the surface to be able to say "this went nowhere", and
40
+ * this is the only record that the token was ever there. Callers that render a
41
+ * composed-prompt preview MUST surface it.
42
+ */
43
+ unresolvedTokens: string[];
27
44
  }
28
45
  /**
29
46
  * Compose the raw prompt (mentions intact) + an ORDERED list of reference groups
@@ -41,6 +58,7 @@ export interface ComposedReferences {
41
58
  export interface ComposeOptions {
42
59
  startImageNumber?: number;
43
60
  startVideoNumber?: number;
61
+ startAudioNumber?: number;
44
62
  }
45
63
  export declare function composeReferences(rawPrompt: string, groups: ReferenceGroup[], opts?: ComposeOptions): ComposedReferences;
46
64
  export interface KlingEditElement {
@@ -56,17 +56,28 @@ export function composeReferences(rawPrompt, groups, opts = {}) {
56
56
  // ── 1. Assign global numbers by walking the list in order ──
57
57
  let imageNum = opts.startImageNumber ?? 0;
58
58
  let videoNum = opts.startVideoNumber ?? 0;
59
+ let audioNum = opts.startAudioNumber ?? 0;
59
60
  const orderedImagePaths = [];
60
61
  const orderedVideoPaths = [];
62
+ const orderedAudioPaths = [];
61
63
  const numbered = groups.map((g) => {
62
64
  const imageNums = [];
63
65
  const videoNums = [];
66
+ const audioNums = [];
64
67
  for (const m of g.media) {
65
- if (m.mediaKind === 'video' && g.kind === 'video') {
68
+ // ONE video counter across the edit source and reference clips. They are
69
+ // mutually exclusive in practice, and a shared counter means a mixed
70
+ // state degrades to wrong-but-unambiguous rather than two "Video 1"s.
71
+ if (m.mediaKind === 'video' && (g.kind === 'video' || g.kind === 'video-ref')) {
66
72
  videoNum += 1;
67
73
  videoNums.push(videoNum);
68
74
  orderedVideoPaths.push(m.path);
69
75
  }
76
+ else if (m.mediaKind === 'audio' && g.kind === 'audio-ref') {
77
+ audioNum += 1;
78
+ audioNums.push(audioNum);
79
+ orderedAudioPaths.push(m.path);
80
+ }
70
81
  else if (m.mediaKind === 'image' && isFreeRefImageKind(g.kind)) {
71
82
  imageNum += 1;
72
83
  imageNums.push(imageNum);
@@ -74,7 +85,7 @@ export function composeReferences(rawPrompt, groups, opts = {}) {
74
85
  }
75
86
  // first-frame / last-frame media: not numbered, not in the free-ref pool.
76
87
  }
77
- return { ...g, imageNums, videoNums };
88
+ return { ...g, imageNums, videoNums, audioNums };
78
89
  });
79
90
  // ── 2. Inline-name token groups in the prompt body ──
80
91
  // For each character/environment group whose token appears in the prompt, the
@@ -87,21 +98,40 @@ export function composeReferences(rawPrompt, groups, opts = {}) {
87
98
  byNorm.set(normToken(g.token), g);
88
99
  const seenFirst = new Set();
89
100
  const matchedInPrompt = new Set();
101
+ // Every token that named nothing, recorded as authored and deduped by the
102
+ // same normalisation used for matching. This is what makes the deletion
103
+ // below reportable instead of silent — see ComposedReferences.unresolvedTokens.
104
+ const unresolvedTokens = [];
105
+ const unresolvedSeen = new Set();
106
+ const noteUnresolved = (sigil, tok) => {
107
+ const key = normToken(`${sigil}${tok}`);
108
+ if (unresolvedSeen.has(key))
109
+ return;
110
+ unresolvedSeen.add(key);
111
+ unresolvedTokens.push(`${sigil}${tok}`);
112
+ };
90
113
  // First strip "in/with the style of #tag" phrases so the style reads as a
91
- // clean trailing clause, not a dangling preposition (legacy cleanPrompt behaviour).
92
- let body = rawPrompt.replace(/\s+(with|in)\s+the\s+style\s+of\s+([@#])([\w-]+)/gi, (full, _prep, sigil, tok) => {
114
+ // clean trailing clause, not a dangling preposition (legacy cleanPrompt
115
+ // behaviour). An UNRESOLVED token in that phrase is stripped too: leaving the
116
+ // preposition behind ("…a portrait in the style of") was the worse half of the
117
+ // silent edit — the tag vanished a step later anyway and the sentence was left
118
+ // broken. Stripped or not, the token is reported.
119
+ let body = rawPrompt.replace(/\s+(with|in)\s+the\s+style\s+of\s+([@#])([\w-]+)/gi, (_full, _prep, sigil, tok) => {
93
120
  const g = byNorm.get(normToken(`${sigil}${tok}`));
94
121
  if (g && g.kind === 'style') {
95
122
  matchedInPrompt.add(normToken(`${sigil}${tok}`));
96
123
  return '';
97
124
  }
98
- return full;
125
+ noteUnresolved(sigil, tok);
126
+ return '';
99
127
  });
100
128
  body = body.replace(/([@#])([\w-]+)/g, (_full, _sigil, tok) => {
101
129
  const key = normToken(`${_sigil}${tok}`);
102
130
  const g = byNorm.get(key);
103
131
  if (!g) {
104
132
  // Unresolved token. A #unknown vanishes; an @unknown humanizes to words.
133
+ // Both are edits the user never asked for, so both are reported.
134
+ noteUnresolved(_sigil, tok);
105
135
  return _sigil === '#' ? '' : humanizeToken(tok);
106
136
  }
107
137
  matchedInPrompt.add(key);
@@ -128,14 +158,52 @@ export function composeReferences(rawPrompt, groups, opts = {}) {
128
158
  topKeys.push(`${noun} ${joinNums(g.videoNums)} ${verb} the motion source.`);
129
159
  }
130
160
  }
131
- // Pinned/base references ("Image 1 is a provided reference.").
161
+ // Reference clips ("Video 2 is a provided reference."). This line SURVIVES
162
+ // the 2026-08-10 cull of null-role key lines because it is not one: the
163
+ // "motion source" wording above belongs to the edit path alone, so a video
164
+ // attachment has two possible roles and the sentence says which. The pinned
165
+ // IMAGE line had no such contrast partner and was deleted (see step 3).
166
+ for (const g of numbered) {
167
+ if (g.kind === 'video-ref' && g.videoNums.length > 0) {
168
+ const noun = g.videoNums.length === 1 ? 'Video' : 'Videos';
169
+ const tail = g.videoNums.length === 1 ? 'is a provided reference.' : 'are provided references.';
170
+ topKeys.push(`${noun} ${joinNums(g.videoNums)} ${tail}`);
171
+ }
172
+ }
173
+ // Reference audio ("Audio 1 is a provided reference.").
132
174
  for (const g of numbered) {
133
- if (g.kind === 'pinned' && g.imageNums.length > 0) {
134
- const noun = g.imageNums.length === 1 ? 'Image' : 'Images';
135
- const tail = g.imageNums.length === 1 ? 'is a provided reference.' : 'are provided references.';
136
- topKeys.push(`${noun} ${joinNums(g.imageNums)} ${tail}`);
175
+ if (g.kind === 'audio-ref' && g.audioNums.length > 0) {
176
+ const noun = g.audioNums.length === 1 ? 'Audio' : 'Audios';
177
+ const tail = g.audioNums.length === 1 ? 'is a provided reference.' : 'are provided references.';
178
+ topKeys.push(`${noun} ${joinNums(g.audioNums)} ${tail}`);
137
179
  }
138
180
  }
181
+ // 🚨 A PINNED REFERENCE IMAGE GETS NO KEY LINE, DELIBERATELY (2026-08-10).
182
+ // It used to emit "Image 1 is a provided reference." — the only branch here
183
+ // that assigns NO role, and therefore says nothing: every image in the request
184
+ // IS a provided reference, so the sentence restated the transport. Its
185
+ // siblings all carry information — "is the motion source" contrasts with a
186
+ // reference clip, "is the subject" / "is Marcus" name the thing, the style
187
+ // clause assigns a job. This one was the null member of that set, and unlike
188
+ // video there is no competing numbered image role for it to disambiguate
189
+ // against (frames ride dedicated slots and are never numbered).
190
+ //
191
+ // It was also actively wrong for the case pinned-first exists to serve: the
192
+ // desktop's `referenceGroups.ts` orders these images FIRST for the
193
+ // edit/injection case (the movie still you paint over reads as image 1) —
194
+ // where image 1 is the CANVAS, not a reference, and the line asserted the
195
+ // opposite.
196
+ //
197
+ // Every vendor assigns roles inline in natural language instead. Google's own
198
+ // multi-reference example is "the attached napkin sketch as the structure and
199
+ // the attached fabric sample as the texture"; ByteDance's is "use @Image 2 as
200
+ // the dormitory scene style reference". Nobody declares a null role. Our own
201
+ // skills say the same thing: "the model does NOT infer a reference's role from
202
+ // its position; the NAME carries it" — and this line had no name to carry.
203
+ //
204
+ // Do not add it back. Attaching an image already means "here is an image"; if
205
+ // a reference needs a role, it gets one from a role badge or from the user's
206
+ // own words, both of which compose into real sentences above.
139
207
  // Token-less or not-in-prompt subjects/environments ("Image 1 is Marcus.").
140
208
  for (const g of numbered) {
141
209
  if ((g.kind === 'character' || g.kind === 'environment') && g.imageNums.length > 0) {
@@ -169,6 +237,8 @@ export function composeReferences(rawPrompt, groups, opts = {}) {
169
237
  prompt: parts.join('\n\n'),
170
238
  orderedImagePaths,
171
239
  orderedVideoPaths,
240
+ orderedAudioPaths,
241
+ unresolvedTokens,
172
242
  };
173
243
  }
174
244
  /** fal cap: max 4 combined element + style-image references per edit request. */
@@ -20,8 +20,25 @@ export declare const IDENTITY_LIGHTING = "flat, even, shadowless lighting";
20
20
  * crushes hair and wardrobe silhouettes.** A deep neutral grey holds both.
21
21
  */
22
22
  export declare const IDENTITY_PLATE_HEX = "#3a3a3c";
23
- export declare const IDENTITY_BACKGROUND = "a plain, deep neutral-grey background (#3a3a3c)";
24
- export declare const IDENTITY_LIGHTING_CLAUSE = "Render on a plain, deep neutral-grey background (#3a3a3c) with flat, even, shadowless lighting so the sheet captures the character's identity, not scene lighting.";
23
+ /**
24
+ * 🚨 THE HEX IS EMITTED WITHOUT ITS `#` AND THAT IS LOAD-BEARING (2026-07-30).
25
+ *
26
+ * `#` is a REFERENCE-TOKEN SIGIL in the desktop's prompt composer
27
+ * (`slate/src/shared/promptComposition.ts` — `/([@#])([\w-]+)/g`), and an
28
+ * unresolved `#token` is SILENTLY DELETED. So `#3a3a3c` never survived to any
29
+ * model: fal echoed back `deep neutral-grey background ()` on a real 2026-07-30
30
+ * request. The plate value has been doing nothing since the composer shipped.
31
+ *
32
+ * Writing it bare keeps the value in the prompt. `IDENTITY_PLATE_HEX` keeps its
33
+ * `#` because it is a colour constant and may have non-prompt consumers.
34
+ *
35
+ * GENERAL RULE FOR THIS FILE: never emit `#` or `@` into prompt text. Both are
36
+ * sigils downstream and both mangle silently — no error, no log, just missing
37
+ * words. HOW YOU'D KNOW THIS IS BEATEN: the composer stops treating bare
38
+ * `#hex` as a token, or starts leaving unresolved tokens intact.
39
+ */
40
+ export declare const IDENTITY_BACKGROUND: string;
41
+ export declare const IDENTITY_LIGHTING_CLAUSE: string;
25
42
  /**
26
43
  * Craft clauses every identity reference wants — the eye and skin detail that
27
44
  * survives downstream, plus the two "reads literally" guards. Crushed-black
@@ -96,7 +96,24 @@ export const IDENTITY_LIGHTING = 'flat, even, shadowless lighting';
96
96
  * crushes hair and wardrobe silhouettes.** A deep neutral grey holds both.
97
97
  */
98
98
  export const IDENTITY_PLATE_HEX = '#3a3a3c';
99
- export const IDENTITY_BACKGROUND = `a plain, deep neutral-grey background (${IDENTITY_PLATE_HEX})`;
99
+ /**
100
+ * 🚨 THE HEX IS EMITTED WITHOUT ITS `#` AND THAT IS LOAD-BEARING (2026-07-30).
101
+ *
102
+ * `#` is a REFERENCE-TOKEN SIGIL in the desktop's prompt composer
103
+ * (`slate/src/shared/promptComposition.ts` — `/([@#])([\w-]+)/g`), and an
104
+ * unresolved `#token` is SILENTLY DELETED. So `#3a3a3c` never survived to any
105
+ * model: fal echoed back `deep neutral-grey background ()` on a real 2026-07-30
106
+ * request. The plate value has been doing nothing since the composer shipped.
107
+ *
108
+ * Writing it bare keeps the value in the prompt. `IDENTITY_PLATE_HEX` keeps its
109
+ * `#` because it is a colour constant and may have non-prompt consumers.
110
+ *
111
+ * GENERAL RULE FOR THIS FILE: never emit `#` or `@` into prompt text. Both are
112
+ * sigils downstream and both mangle silently — no error, no log, just missing
113
+ * words. HOW YOU'D KNOW THIS IS BEATEN: the composer stops treating bare
114
+ * `#hex` as a token, or starts leaving unresolved tokens intact.
115
+ */
116
+ export const IDENTITY_BACKGROUND = `a plain, deep neutral-grey background (hex ${IDENTITY_PLATE_HEX.replace('#', '')})`;
100
117
  export const IDENTITY_LIGHTING_CLAUSE = `Render on ${IDENTITY_BACKGROUND} with ${IDENTITY_LIGHTING} so the sheet captures the character's identity, not scene lighting.`;
101
118
  /**
102
119
  * Craft clauses every identity reference wants — the eye and skin detail that