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