@slatesvideo/shared 0.4.6 → 0.4.8

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/index.d.ts CHANGED
@@ -3,5 +3,6 @@ export { SlatesCloudClient, type SlatesUserInfo, type CreditsBalance, type Model
3
3
  export { SlatesDesktopClient, type DesktopHealth } from './clients/desktop.js';
4
4
  export { SKILLS } from './skills/content.js';
5
5
  export * as operations from './operations/index.js';
6
- export { ALL_OPERATIONS, defaultContext, type Operation, type OperationContext, type OperationResult } from './operations/index.js';
6
+ export { ALL_OPERATIONS, VIDEO_MODELS, defaultContext, type Operation, type OperationContext, type OperationResult } from './operations/index.js';
7
+ export { MODEL_FACTS, getModelFact, type ModelFact } from './prompts/model-facts.js';
7
8
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -3,5 +3,9 @@ export { SlatesCloudClient } from './clients/cloud.js';
3
3
  export { SlatesDesktopClient } from './clients/desktop.js';
4
4
  export { SKILLS } from './skills/content.js';
5
5
  export * as operations from './operations/index.js';
6
- export { ALL_OPERATIONS, defaultContext } from './operations/index.js';
6
+ export { ALL_OPERATIONS, VIDEO_MODELS, defaultContext } from './operations/index.js';
7
+ // Model routing/prompting facts — the SSOT the desktop Studio Agent system
8
+ // prompt derives its MODEL ROUTING doctrine from (kind: image vs video,
9
+ // default/premium/niche notes). Edit model-facts.ts, never prose copies.
10
+ export { MODEL_FACTS, getModelFact } from './prompts/model-facts.js';
7
11
  //# sourceMappingURL=index.js.map
@@ -31,6 +31,12 @@ export declare const listAvailableModels: Operation<{
31
31
  export declare const estimateGenerationCost: Operation<{
32
32
  model: string;
33
33
  quantity?: number;
34
+ duration?: number;
35
+ videoResolution?: '480p' | '720p' | '1080p' | '4k';
36
+ resolution?: '1k' | '2k' | '4k';
37
+ sound?: boolean;
38
+ seedanceFace?: boolean;
39
+ seedanceRealFace?: boolean;
34
40
  }>;
35
41
  export declare const listProjects: Operation<Record<string, never>>;
36
42
  export declare const createProject: Operation<{
@@ -159,7 +165,7 @@ export declare const editImage: Operation<{
159
165
  confirm?: boolean;
160
166
  background?: boolean;
161
167
  }>;
162
- declare const VIDEO_MODELS: readonly ["kling-v3.0-std", "kling-v3.0-pro", "kling-v3.0-omni", "veo-3.1-fast", "veo-3.1-standard", "seedance-2"];
168
+ export declare const VIDEO_MODELS: readonly ["kling-v3.0-std", "kling-v3.0-pro", "kling-v3.0-omni", "veo-3.1-fast", "veo-3.1-standard", "seedance-2"];
163
169
  type VideoModel = (typeof VIDEO_MODELS)[number];
164
170
  export declare function videoCostKey(input: {
165
171
  model: VideoModel;
@@ -171,7 +177,7 @@ export declare function videoCostKey(input: {
171
177
  }): string;
172
178
  export declare const generateVideo: Operation<{
173
179
  prompt: string;
174
- model: VideoModel;
180
+ model: string;
175
181
  projectId?: string;
176
182
  aspectRatio?: '1:1' | '16:9' | '9:16' | '4:3' | '3:4' | '21:9' | '9:21' | '4:5' | '5:4' | '2:3' | '3:2';
177
183
  duration?: number;
@@ -34,12 +34,13 @@ const BACKGROUND_DESCRIBE = 'Submit and return immediately with generationId(s)
34
34
  // Early-return shape when a generation route accepted the job in background
35
35
  // mode ({ background: true } in the response). No inline-image fetch — the
36
36
  // asset doesn't exist yet; the poller delivers it on completion.
37
- function backgroundSubmitted(kind, ids, extra) {
37
+ function backgroundSubmitted(kind, ids, extra, note) {
38
38
  const idText = ids.length > 0 ? ids.join(', ') : '(no id returned)';
39
39
  return {
40
40
  text: `Submitted ${kind} in the background — generationId(s): ${idText}. ` +
41
- `Poll slates_get_generation_status with each id every 5-15s until status is 'completed' ` +
42
- `(video generations commonly take 1-5 minutes). Generations survive app restarts.`,
41
+ `Call slates_get_generation_status with waitSeconds: 45 (it long-polls and returns on completion ` +
42
+ `never a rapid loop; video renders commonly take 1-5 minutes). Generations survive app restarts.` +
43
+ (note ? ` ${note}` : ''),
43
44
  data: { generationIds: ids, status: 'processing', ...extra },
44
45
  };
45
46
  }
@@ -94,7 +95,9 @@ export const listAvailableModels = {
94
95
  // JSON registry (the full pretty-printed dump was an ~8k-token leak).
95
96
  const table = models.map((m) => `${m.model} ${m.cost_cents}`).join('\n');
96
97
  return {
97
- text: `${models.length} model cost keys (cents per generation)${input.filter ? ` matching "${input.filter}"` : ''}:\n` +
98
+ text: `${models.length} COST keys (cents per generation)${input.filter ? ` matching "${input.filter}"` : ''}. ` +
99
+ `NOTE: these are billing keys for cost lookup ONLY — the \`model\` param on slates_generate_video takes a BASE id ` +
100
+ `(kling-v3.0-std | kling-v3.0-pro | kling-v3.0-omni | seedance-2 | veo-3.1-fast | veo-3.1-standard) with duration/videoResolution as separate params:\n` +
98
101
  table,
99
102
  data: { count: models.length, credit_markup: r.credit_markup },
100
103
  };
@@ -102,23 +105,64 @@ export const listAvailableModels = {
102
105
  };
103
106
  export const estimateGenerationCost = {
104
107
  id: 'slates_estimate_generation_cost',
105
- description: 'Pre-flight cost estimate. Call before any generate_* op so the user sees "this will cost N credits" up front. Pairs with the >$0.50 confirm gate.',
108
+ description: 'Pre-flight cost estimate. Call before any generate_* op so the user sees "this will cost N credits" up front. Takes the SAME base model ids as the generate ops (video: "seedance-2" + duration + videoResolution; image: "nano-banana-2" + resolution) — exact registry cost keys also work. Pairs with the >$0.50 confirm gate.',
106
109
  input: z.object({
107
- model: z.string().describe('Model id, e.g. "nano-banana-2-2k" or "veo-3.1-fast-8s"'),
110
+ model: z.string().describe('Base model id as passed to the generate op (e.g. "seedance-2", "kling-v3.0-std", "nano-banana-2") or an exact registry cost key ("nano-banana-2-2k", "seedance-2-1080p-8s")'),
108
111
  quantity: z.number().int().min(1).max(10).optional().describe('Number of generations (default 1)'),
112
+ duration: z.number().int().min(3).max(15).optional().describe('Video only — seconds. Cost scales linearly; required with a video base id.'),
113
+ videoResolution: z.enum(['480p', '720p', '1080p', '4k']).optional().describe('Video only. Seedance defaults to 1080p.'),
114
+ resolution: z.enum(['1k', '2k', '4k']).optional().describe('Image only (default 2k).'),
115
+ sound: z.boolean().optional().describe('Veo only — audio flag changes the cost key.'),
116
+ seedanceFace: z.boolean().optional().describe('Seedance AI-face route (pricier key).'),
117
+ seedanceRealFace: z.boolean().optional().describe('Seedance consented real-face route (premium key).'),
109
118
  }),
110
119
  async run(input, ctx) {
111
120
  const registry = await ctx.cloud().get('/api/agent/models');
112
- const entry = registry.models.find((m) => m.model === input.model);
113
- if (!entry) {
114
- throw new Error(`Unknown model: ${input.model}. Use slates_list_available_models to see options.`);
121
+ const byKey = new Map(registry.models.map((m) => [m.model, m.cost_cents]));
122
+ // 1) exact registry cost key
123
+ let key = byKey.has(input.model) ? input.model : null;
124
+ // 2) image base id + resolution
125
+ if (!key) {
126
+ const img = ['nano-banana-2', 'flux-2-max', 'seedream-5-lite'].find((m) => m === input.model);
127
+ if (img)
128
+ key = imageCostKey(img, input.resolution ?? '2k');
129
+ }
130
+ // 3) video base id (or cost-key spelling) → the same forgiving resolver
131
+ // the generate op uses, so the two can never disagree about a model.
132
+ if (!key) {
133
+ const resolved = resolveVideoModel(input.model);
134
+ if (resolved) {
135
+ const duration = input.duration ?? resolved.duration;
136
+ if (!duration) {
137
+ return ok({
138
+ requires_clarification: true,
139
+ missing: ['duration'],
140
+ message: `"${resolved.model}" cost scales with duration — pass duration (seconds) to estimate.`,
141
+ });
142
+ }
143
+ key = videoCostKey({
144
+ model: resolved.model,
145
+ duration,
146
+ videoResolution: input.videoResolution ??
147
+ resolved.videoResolution ??
148
+ (resolved.model.startsWith('seedance') ? '1080p' : undefined),
149
+ sound: input.sound ?? resolved.sound,
150
+ seedanceFace: input.seedanceFace ?? resolved.seedanceFace,
151
+ seedanceRealFace: input.seedanceRealFace,
152
+ });
153
+ }
154
+ }
155
+ const cents = key != null ? byKey.get(key) : undefined;
156
+ if (key == null || cents == null) {
157
+ throw new Error(`Unknown model: ${input.model}. Pass a base id (${VIDEO_MODELS.join(' | ')} | nano-banana-2 | flux-2-max | seedream-5-lite) plus duration/resolution params, or use slates_list_available_models with a filter.`);
115
158
  }
116
159
  const qty = input.quantity ?? 1;
117
- const totalCents = entry.cost_cents * qty;
160
+ const totalCents = cents * qty;
118
161
  return ok({
119
162
  model: input.model,
163
+ cost_key: key,
120
164
  quantity: qty,
121
- cost_per_cents: entry.cost_cents,
165
+ cost_per_cents: cents,
122
166
  total_cents: totalCents,
123
167
  total_dollars: (totalCents / 100).toFixed(2),
124
168
  requires_confirm: totalCents > 50,
@@ -169,6 +213,69 @@ function compactAsset(a) {
169
213
  created_at: r.createdAt ?? r.created_at ?? undefined,
170
214
  };
171
215
  }
216
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
217
+ /**
218
+ * Resolve asset references that may be UUIDs OR badge codes ("IMG-A8",
219
+ * "vid-v3", bare "A8"). Codes resolve against the project's asset list AT
220
+ * CALL TIME — never a stale mapping from earlier in a conversation. The
221
+ * user creates assets in the Slates UI mid-chat; an agent that guesses or
222
+ * reuses a nearby UUID for a code it never looked up burns real dollars on
223
+ * the wrong start frame (observed live: "use A8" generated from A5).
224
+ * Unknown codes throw a teaching error; unknown UUIDs pass through for the
225
+ * desktop route to validate.
226
+ */
227
+ async function resolveAssetRefs(ctx, projectId, refs) {
228
+ const out = new Map();
229
+ const pending = [...new Set(refs.filter((r) => typeof r === 'string' && r.trim().length > 0))];
230
+ if (pending.length === 0)
231
+ return out;
232
+ const { assets } = await ctx.desktop().get('/agent/assets', { projectId });
233
+ const rows = (assets ?? []).map(compactAsset);
234
+ const byId = new Map(rows.map((a) => [String(a.id).toLowerCase(), a]));
235
+ const byCode = new Map(rows.filter((a) => a.code).map((a) => [String(a.code).toUpperCase(), a]));
236
+ for (const ref of pending) {
237
+ const raw = ref.trim();
238
+ if (UUID_RE.test(raw)) {
239
+ const row = byId.get(raw.toLowerCase());
240
+ out.set(ref, {
241
+ id: raw,
242
+ code: row ? (row.code ?? null) : null,
243
+ label: row ? (row.label ?? null) : null,
244
+ });
245
+ continue;
246
+ }
247
+ let row = byCode.get(raw.toUpperCase());
248
+ if (!row && /^[AVS]\d+$/i.test(raw)) {
249
+ // Bare "A8" / "V3" / "S1" — expand to the full badge family.
250
+ const norm = raw.toUpperCase();
251
+ const prefixed = norm.startsWith('A') ? `IMG-${norm}` : norm.startsWith('V') ? `VID-${norm}` : `AUD-${norm}`;
252
+ row = byCode.get(prefixed);
253
+ }
254
+ if (!row) {
255
+ throw new Error(`No asset matching "${ref}" in this project. Codes resolve at call time — ` +
256
+ `call slates_list_assets (search: "${ref}") to see what actually exists, then pass the exact code or id. Never guess.`);
257
+ }
258
+ out.set(ref, {
259
+ id: String(row.id),
260
+ code: row.code ?? null,
261
+ label: row.label ?? null,
262
+ });
263
+ }
264
+ return out;
265
+ }
266
+ /** "first frame: IMG-A8 — Untitled" echo lines for generation results. */
267
+ function describeResolvedRefs(refInputs, resolved) {
268
+ if (refInputs.length === 0)
269
+ return '';
270
+ const lines = refInputs.map(({ ref, role }) => {
271
+ const r = resolved.get(ref);
272
+ if (!r)
273
+ return `${role}: ${ref}`;
274
+ const name = r.code ?? r.id;
275
+ return `${role}: ${name}${r.label ? ` — ${r.label}` : ''}`;
276
+ });
277
+ return `References used: ${lines.join('; ')}.`;
278
+ }
172
279
  export const listAssets = {
173
280
  id: 'slates_list_assets',
174
281
  description: 'List assets in a Slates project as COMPACT rows (id, code, label, type) — newest first, default limit 50. Each asset carries its short code (IMG-A12 / VID-V3 / AUD-S1 — the badge the user sees on the gallery card) and label. When the user names an asset by code ("use IMG-A36 as the reference"), pass it as `search` to resolve the assetId. Always speak about assets by code + label, never by UUID. NOTE: generate_* results already return the new asset ids — do NOT call this to find an asset you just created.',
@@ -636,7 +743,7 @@ export const generateImage = {
636
743
  aspectRatio: z.enum(['1:1', '16:9', '9:16', '4:3', '3:4', '21:9', '9:21', '4:5', '5:4', '2:3', '3:2']).optional().describe('Pick deliberately from the use case. Cinematic → 16:9. TikTok/Reels/Story → 9:16. IG square → 1:1. Ultra-wide → 21:9. Ask the user when ambiguous.'),
637
744
  count: z.number().int().min(1).max(4).optional(),
638
745
  referenceImageUrls: z.array(z.string().url()).max(14).optional().describe('Headless (no-projectId) nano-banana-2 only: up to 14 ref URLs. For projectId runs, upload refs with slates_upload_reference_image first. Always label each image\'s role in the prompt text.'),
639
- referenceAssetIds: z.array(z.string().uuid()).max(14).optional().describe("Project asset ids to use as reference/ingredient images (resolved on the desktop). Requires projectId. For nano-banana-2 up to 14 refs; FLUX/Seedream route to their edit endpoints with lower per-model caps. Label each reference's role in the prompt text."),
746
+ referenceAssetIds: z.array(z.string()).max(14).optional().describe("Project assets to use as reference/ingredient images — asset UUIDs or badge codes (\"IMG-A8\"); codes resolve against the project at call time. Requires projectId. For nano-banana-2 up to 14 refs; FLUX/Seedream route to their edit endpoints with lower per-model caps. Label each reference's role in the prompt text."),
640
747
  background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
641
748
  confirm: z.boolean().optional().describe('Set true to bypass the >$0.50 cost confirm gate.'),
642
749
  }),
@@ -675,7 +782,7 @@ export const generateImage = {
675
782
  // disk, so a headless run has nowhere to look them up. Same for
676
783
  // background mode: the poller (slates_get_generation_status) reads the
677
784
  // desktop's generation records.
678
- const referenceAssetIds = input.referenceAssetIds ?? [];
785
+ let referenceAssetIds = input.referenceAssetIds ?? [];
679
786
  if (referenceAssetIds.length > 0 && !input.projectId) {
680
787
  return ok({
681
788
  requires_clarification: true,
@@ -690,8 +797,15 @@ export const generateImage = {
690
797
  message: 'background=true routes through the desktop generation pipeline (so slates_get_generation_status can poll it) — pass a projectId, or drop background for a blocking headless run.',
691
798
  });
692
799
  }
800
+ let refEcho = '';
693
801
  if (referenceAssetIds.length > 0) {
694
802
  await ctx.desktop().requireCapability('image-references', 'reference images on image generation');
803
+ // UUIDs or badge codes — resolved against the project at call time
804
+ // (stale-code guessing is a real, observed failure mode).
805
+ const resolved = await resolveAssetRefs(ctx, input.projectId, referenceAssetIds);
806
+ const refInputs = referenceAssetIds.map((ref) => ({ ref, role: 'reference' }));
807
+ referenceAssetIds = referenceAssetIds.map((ref) => resolved.get(ref)?.id ?? ref);
808
+ refEcho = describeResolvedRefs(refInputs, resolved);
695
809
  }
696
810
  if (input.background) {
697
811
  await ctx.desktop().requireCapability('background-generation', 'background generation');
@@ -781,7 +895,7 @@ export const generateImage = {
781
895
  projectId: input.projectId,
782
896
  cost_cents: totalCents,
783
897
  cost_dollars: (totalCents / 100).toFixed(2),
784
- });
898
+ }, refEcho);
785
899
  }
786
900
  const assetList = result.assets
787
901
  ?? (result.asset ? [result.asset] : []);
@@ -807,7 +921,8 @@ export const generateImage = {
807
921
  `Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"`
808
922
  : `Generated ${assetList.length} image(s) into project ${input.projectId} ` +
809
923
  `for $${(totalCents / 100).toFixed(2)} (${totalCents}¢). ` +
810
- `Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"`,
924
+ `Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"` +
925
+ (refEcho ? ` ${refEcho}` : ''),
811
926
  images,
812
927
  data: {
813
928
  model: imageModel,
@@ -1014,7 +1129,10 @@ export const editImage = {
1014
1129
  },
1015
1130
  };
1016
1131
  // ── Generate video ──────────────────────────────────────────────
1017
- const VIDEO_MODELS = [
1132
+ // Exported: the exact `model` ids slates_generate_video accepts — consumed
1133
+ // by the desktop Studio Agent system prompt (SSOT; never restate these ids
1134
+ // in prose that can drift).
1135
+ export const VIDEO_MODELS = [
1018
1136
  'kling-v3.0-std',
1019
1137
  'kling-v3.0-pro',
1020
1138
  'kling-v3.0-omni',
@@ -1070,6 +1188,60 @@ export function videoCostKey(input) {
1070
1188
  }
1071
1189
  throw new Error(`Unknown video model: ${input.model}`);
1072
1190
  }
1191
+ /**
1192
+ * Forgiving model-id resolver. Agents routinely paste registry COST keys
1193
+ * ("kling-v3-standard-8s", "seedance-2-1080p-8s") into the `model` param —
1194
+ * an observed 15-turn error-retry spiral in a live session. Instead of
1195
+ * rejecting, extract the intent: base model + any duration / resolution /
1196
+ * audio the key encodes. Returns null only when nothing matches.
1197
+ */
1198
+ function resolveVideoModel(raw) {
1199
+ let s = raw.trim().toLowerCase();
1200
+ const out = { model: 'kling-v3.0-std' };
1201
+ const dur = /-(\d+)s\b/.exec(s);
1202
+ if (dur) {
1203
+ out.duration = parseInt(dur[1], 10);
1204
+ s = s.replace(/-(\d+)s\b/, '');
1205
+ }
1206
+ const res = /-(480p|720p|1080p|4k)\b/.exec(s);
1207
+ if (res) {
1208
+ out.videoResolution = res[1];
1209
+ s = s.replace(/-(480p|720p|1080p|4k)\b/, '');
1210
+ }
1211
+ if (/-audio\b/.test(s)) {
1212
+ out.sound = true;
1213
+ s = s.replace(/-audio\b/, '');
1214
+ }
1215
+ if (/-(realface|face)\b/.test(s)) {
1216
+ out.seedanceFace = true;
1217
+ s = s.replace(/-(realface|face)\b/, '');
1218
+ }
1219
+ const direct = VIDEO_MODELS.find((m) => m === s);
1220
+ if (direct) {
1221
+ out.model = direct;
1222
+ return out;
1223
+ }
1224
+ const aliases = {
1225
+ 'kling-v3-standard': 'kling-v3.0-std',
1226
+ 'kling-v3-std': 'kling-v3.0-std',
1227
+ 'kling-v3.0-standard': 'kling-v3.0-std',
1228
+ 'kling-v3': 'kling-v3.0-std',
1229
+ 'kling-v3-pro': 'kling-v3.0-pro',
1230
+ 'kling-v3-omni': 'kling-v3.0-omni',
1231
+ 'kling-v3-omni-pro': 'kling-v3.0-omni',
1232
+ 'kling-v3.0-omni-pro': 'kling-v3.0-omni',
1233
+ 'seedance-2.0': 'seedance-2',
1234
+ 'seedance-2-0': 'seedance-2',
1235
+ seedance: 'seedance-2',
1236
+ 'veo-3.1': 'veo-3.1-fast',
1237
+ 'veo-3': 'veo-3.1-fast',
1238
+ };
1239
+ if (aliases[s]) {
1240
+ out.model = aliases[s];
1241
+ return out;
1242
+ }
1243
+ return null;
1244
+ }
1073
1245
  // Maps a video model id to its bundled prompting skill (frontmatter `name:`),
1074
1246
  // so guidance text points at a skill that actually exists. Deriving the name
1075
1247
  // via model.split('-')[0] produced 'slates-prompting-kling' / '...-veo', which
@@ -1085,21 +1257,21 @@ function promptingSkillFor(model) {
1085
1257
  }
1086
1258
  export const generateVideo = {
1087
1259
  id: 'slates_generate_video',
1088
- description: 'Generate video via Slates credits. REQUIRED before calling: read slates-model-selection (routing: Kling = default, Seedance = premium/physics, Veo = audio-only niche), the slates-cost-discipline skill, plus the per-model prompting skill (slates-prompting-seedance / slates-prompting-kling-v3 / slates-prompting-veo-3) — video models prompt very differently. Also read slates-content-policy when the scene involves conflict, creatures, crowds, destruction, weapons, or young characters (build it safe-by-construction so the filter doesn\'t reject or degrade it). projectId is REQUIRED for UI integration (the user sees a progress card and the asset lands in the project — without it the call fails). aspectRatio + duration are required (server returns requires_clarification when missing). Cost > $0.50 returns requires_confirm — pass confirm=true after explicit user OK. Veo locks to 16:9 and to 4/6/8s durations (4K only at 8s). Image-to-video via firstFrameAssetId. Frames-to-video via firstFrameAssetId + lastFrameAssetId (Veo / Seedance only). Ingredients via ingredientAssetIds (Kling Omni / Seedance). No skill files installed? Call slates_get_prompting_guide with the per-model guide (\'slates-prompting-veo-3\' / \'slates-prompting-kling-v3\' / \'slates-prompting-seedance\') and \'slates-cost-discipline\' before first use.',
1260
+ description: 'Generate video via Slates credits. REQUIRED before calling: read slates-model-selection (the routing doctrine), slates-cost-discipline, and the matching per-model prompting skill (slates-prompting-seedance / slates-prompting-kling-v3 / slates-prompting-veo-3) — video models prompt very differently; load them via slates_get_prompting_guide if no skill files are installed. Read slates-content-policy when the scene involves conflict, creatures, crowds, destruction, weapons, or young characters. projectId, aspectRatio, and duration are required (requires_clarification otherwise). Cost > $0.50 returns requires_confirm — pass confirm=true after explicit user OK. Image-to-video via firstFrameAssetId; first+last frames = Veo/Seedance only; ingredients via ingredientAssetIds (Kling Omni / Seedance). Asset params take UUIDs or badge codes ("IMG-A8").',
1089
1261
  input: z.object({
1090
1262
  prompt: z.string().min(1).max(4000),
1091
- model: z.enum(VIDEO_MODELS).describe('Route per the slates-model-selection skill. DEFAULT = Kling V3.0 std (general-purpose, cost-effective, strong start-frame adherence; pro = higher polish; omni = multi-char dialogue + audio). Seedance 2 = the PREMIUM tierpick it whenever physics/effects/scale remotely matter or for hero shots; audio included, first+last frame + up to 9 reference images, full 480p–4K ladder (native 4K) via videoResolution. Veo 3.1 = niche, never the default — only when native synced audio must generate WITH the video in one gen; locks 16:9, 4/6/8s. For exact per-call credit cost, call slates_estimate_generation_cost or slates_list_available_models — never quote prices from memory (they change).'),
1263
+ model: z.string().describe('One of: kling-v3.0-std | kling-v3.0-pro | kling-v3.0-omni | seedance-2 | veo-3.1-fast | veo-3.1-standard. Pass the BASE idduration and videoResolution are separate params (registry cost keys like "kling-v3-standard-8s" auto-resolve). Route per the slates-model-selection skill: Kling std = general-purpose DEFAULT, Seedance 2 = premium physics/effects/hero tier, Veo = native-synced-audio niche only (16:9, 4/6/8s) — never the default. All are VIDEO-only. For per-call cost, call slates_estimate_generation_cost — never quote prices from memory.'),
1092
1264
  projectId: z.string().uuid().optional().describe('Save into this Slates project. Strongly recommended — the desktop UI shows a progress card live and the asset appears when complete.'),
1093
1265
  aspectRatio: z.enum(['1:1', '16:9', '9:16', '4:3', '3:4', '21:9', '9:21', '4:5', '5:4', '2:3', '3:2']).optional().describe('Veo locks to 16:9 — passing anything else will be ignored or fail. Kling/Seedance support all.'),
1094
1266
  duration: z.number().int().min(4).max(15).optional().describe('Seconds. Kling: 5-15. Veo: 4, 6, or 8 only (4K only at 8s). Seedance: 4-15. Default 5 if omitted but always be explicit (cost scales linearly).'),
1095
1267
  videoResolution: z.enum(['480p', '720p', '1080p', '4k']).optional().describe('Veo + Seedance. Seedance: 480p/720p/1080p/4K (default 1080p; 4K is native, the most expensive). Veo: 720p/1080p same price, 4K more (8s only).'),
1096
- firstFrameAssetId: z.string().uuid().optional().describe('Asset id from the projectused as the starting frame for image-to-video. Must already exist in the project.'),
1097
- 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.'),
1098
- 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).'),
1099
- characterAssetIds: z.array(z.string().uuid()).optional().describe('Asset ids of character sheets — keeps a character consistent across the shot. From a Slates project.'),
1100
- 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.'),
1101
- styleAssetIds: z.array(z.string().uuid()).optional().describe('Asset ids of style references — locks the visual style of the shot. From a Slates project.'),
1102
- 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.'),
1268
+ firstFrameAssetId: z.string().optional().describe('Starting frame for image-to-video: asset UUID or badge code ("IMG-A8") codes resolve against the project at call time, so a code the user just spoke is always safe to pass.'),
1269
+ lastFrameAssetId: z.string().optional().describe('Ending frame (UUID or badge code). Veo and Seedance only. Pairs with firstFrameAssetId for guided transitions.'),
1270
+ ingredientAssetIds: z.array(z.string()).max(9).optional().describe('Visual reference / ingredient assets (UUIDs or badge codes) for Kling Omni or Seedance. Up to 9 (Seedance) or 4 (Kling).'),
1271
+ characterAssetIds: z.array(z.string()).optional().describe('Character sheet assets (UUIDs or badge codes) — keeps a character consistent across the shot.'),
1272
+ environmentAssetIds: z.array(z.string()).optional().describe('Environment grid assets (UUIDs or badge codes) — keeps a location/setting consistent across the shot.'),
1273
+ styleAssetIds: z.array(z.string()).optional().describe('Style reference assets (UUIDs or badge codes) — locks the visual style of the shot.'),
1274
+ videoReferenceAssetId: z.string().optional().describe('Seedance ONLY: an existing VIDEO asset (UUID or badge code) 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.'),
1103
1275
  sound: z.boolean().optional().describe('Kling Omni / Veo / Seedance: enable audio generation. Default true.'),
1104
1276
  audioLanguage: z.enum(['EN', 'ZH', 'JA', 'KO', 'ES']).optional().describe('Kling Omni only — language for dialogue.'),
1105
1277
  generateMusic: z.boolean().optional().describe('Kling Omni only — auto-generate background music.'),
@@ -1111,6 +1283,25 @@ export const generateVideo = {
1111
1283
  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).'),
1112
1284
  }),
1113
1285
  async run(input, ctx) {
1286
+ // Resolve the model FIRST — forgiving normalization (cost keys, alias
1287
+ // spellings) with a teaching error, so a wrong id never costs the agent
1288
+ // a retry spiral. Anything the key encoded (duration/res/audio) fills
1289
+ // params the caller left blank; explicit params always win.
1290
+ const resolved = resolveVideoModel(input.model);
1291
+ if (!resolved) {
1292
+ throw new Error(`Unknown video model "${input.model}". Valid model ids: ${VIDEO_MODELS.join(' | ')}. ` +
1293
+ `Registry entries like "kling-v3-standard-8s" or "seedance-2-1080p-8s" are COST keys, not model ids — ` +
1294
+ `pass the base id and set duration / videoResolution as separate params.`);
1295
+ }
1296
+ input.model = resolved.model;
1297
+ if (input.duration == null && resolved.duration != null)
1298
+ input.duration = resolved.duration;
1299
+ if (input.videoResolution == null && resolved.videoResolution != null)
1300
+ input.videoResolution = resolved.videoResolution;
1301
+ if (input.sound == null && resolved.sound != null)
1302
+ input.sound = resolved.sound;
1303
+ if (input.seedanceFace == null && resolved.seedanceFace != null)
1304
+ input.seedanceFace = resolved.seedanceFace;
1114
1305
  // projectId is required for video — without it there's no UI feedback,
1115
1306
  // no asset to reference later, and a failed gen leaves the user with
1116
1307
  // nothing. The MCP-only headless path that exists for image gen is
@@ -1156,6 +1347,38 @@ export const generateVideo = {
1156
1347
  });
1157
1348
  }
1158
1349
  }
1350
+ // Resolve every asset reference (UUID or badge code) against the
1351
+ // project AT CALL TIME. Codes the user just spoke ("a8 is the one")
1352
+ // resolve to whatever exists NOW — including assets created in the UI
1353
+ // after the agent's last list call. Unknown codes throw before any
1354
+ // spend. The resolved code+label is echoed in every result so a wrong
1355
+ // start frame is visible immediately, not after $4 of render.
1356
+ const refInputs = [];
1357
+ if (input.firstFrameAssetId)
1358
+ refInputs.push({ ref: input.firstFrameAssetId, role: 'first frame' });
1359
+ if (input.lastFrameAssetId)
1360
+ refInputs.push({ ref: input.lastFrameAssetId, role: 'last frame' });
1361
+ for (const r of input.ingredientAssetIds ?? [])
1362
+ refInputs.push({ ref: r, role: 'ingredient' });
1363
+ for (const r of input.characterAssetIds ?? [])
1364
+ refInputs.push({ ref: r, role: 'character' });
1365
+ for (const r of input.environmentAssetIds ?? [])
1366
+ refInputs.push({ ref: r, role: 'environment' });
1367
+ for (const r of input.styleAssetIds ?? [])
1368
+ refInputs.push({ ref: r, role: 'style' });
1369
+ if (input.videoReferenceAssetId)
1370
+ refInputs.push({ ref: input.videoReferenceAssetId, role: 'video reference' });
1371
+ const resolvedRefs = await resolveAssetRefs(ctx, input.projectId, refInputs.map((r) => r.ref));
1372
+ const rid = (v) => v ? (resolvedRefs.get(v)?.id ?? v) : v;
1373
+ const rids = (a) => a?.map((v) => resolvedRefs.get(v)?.id ?? v);
1374
+ input.firstFrameAssetId = rid(input.firstFrameAssetId);
1375
+ input.lastFrameAssetId = rid(input.lastFrameAssetId);
1376
+ input.videoReferenceAssetId = rid(input.videoReferenceAssetId);
1377
+ input.ingredientAssetIds = rids(input.ingredientAssetIds);
1378
+ input.characterAssetIds = rids(input.characterAssetIds);
1379
+ input.environmentAssetIds = rids(input.environmentAssetIds);
1380
+ input.styleAssetIds = rids(input.styleAssetIds);
1381
+ const refEcho = describeResolvedRefs(refInputs, resolvedRefs);
1159
1382
  const cloud = ctx.cloud();
1160
1383
  const registry = await cloud.get('/api/agent/models');
1161
1384
  const costKey = videoCostKey({
@@ -1279,12 +1502,17 @@ export const generateVideo = {
1279
1502
  projectId: input.projectId,
1280
1503
  cost_cents: totalCents,
1281
1504
  cost_dollars: (totalCents / 100).toFixed(2),
1282
- });
1505
+ references: refInputs.map(({ ref, role }) => ({
1506
+ role,
1507
+ ...(resolvedRefs.get(ref) ?? { id: ref, code: null, label: null }),
1508
+ })),
1509
+ }, refEcho);
1283
1510
  }
1284
1511
  return {
1285
1512
  text: `Generated ${input.duration}s ${input.model} video into project ${input.projectId} ` +
1286
1513
  `for $${(totalCents / 100).toFixed(2)} (${totalCents}¢). ` +
1287
- `Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"`,
1514
+ `Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"` +
1515
+ (refEcho ? ` ${refEcho}` : ''),
1288
1516
  data: {
1289
1517
  model: input.model,
1290
1518
  variant: costKey,
@@ -1531,13 +1759,27 @@ export const getGenerationStatus = {
1531
1759
  await desktop.requireCapability('background-generation', 'background generation');
1532
1760
  const deadline = Date.now() + Math.min(Math.max(input.waitSeconds ?? 0, 0), 50) * 1000;
1533
1761
  for (;;) {
1534
- const r = await desktop.get('/agent/generation/status', {
1535
- id: input.generationId,
1536
- });
1537
- const status = r.status;
1762
+ const r = await desktop.get('/agent/generation/status', { id: input.generationId });
1763
+ const g = r.generation;
1764
+ const status = g?.status ?? r.status;
1538
1765
  const terminal = status === 'completed' || status === 'failed' || status === 'cancelled';
1539
- if (terminal || Date.now() >= deadline)
1540
- return ok(r);
1766
+ if (terminal || Date.now() >= deadline) {
1767
+ if (!g)
1768
+ return ok(r);
1769
+ // Slim the payload: the raw generation row (full prompt/settings)
1770
+ // and full asset row are context bloat — return the fields the
1771
+ // agent acts on, with the EXACT billed cost front and center.
1772
+ return ok({
1773
+ status,
1774
+ cost_cents: g.cost ?? null,
1775
+ error: g.error ?? null,
1776
+ model: g.model,
1777
+ completed_at: g.completedAt ?? null,
1778
+ asset: r.asset
1779
+ ? { ...compactAsset(r.asset), file_path: r.asset.filePath }
1780
+ : null,
1781
+ });
1782
+ }
1541
1783
  await new Promise((resolve) => setTimeout(resolve, Math.min(3000, deadline - Date.now())));
1542
1784
  }
1543
1785
  },
@@ -34,7 +34,7 @@ export const MODEL_FACTS = [
34
34
  kind: 'video',
35
35
  maxRefImages: null,
36
36
  maxIngredients: 9, // ingredient images per video gen
37
- notes: 'PREMIUM video tier — route here the moment physics, effects, destruction, or scale matter, and for hero shots. Up to 9 ingredient images. Strong I2V / own-footage restyle. Native 4K.',
37
+ notes: 'PREMIUM video tier — route here the moment physics, effects, destruction, or scale matter, and for hero shots. VIDEO-ONLY: cannot generate standalone images (use NB2/FLUX.2/Seedream for those). Up to 9 ingredient images. Strong I2V / own-footage restyle. Native 4K.',
38
38
  },
39
39
  {
40
40
  id: 'kling-v3',
@@ -6,7 +6,7 @@ export const SKILLS = {
6
6
  "slates-cost-discipline": "---\nname: slates-cost-discipline\ndescription: Mandatory pre-flight discipline before ANY generation call (image or video) — estimate cost, announce in dollars, get confirmation, aggregate batches. Read this every time before calling slates_generate_image or any future slates_generate_* op. Skipping this risks burning the user's credits on guesses.\n---\n\n# Slates cost discipline — read before every generation\n\nGeneration costs real money. Every call is on the user's credits. The user can't see what you're about to spend until you tell them. **Tell them first, generate second.**\n\n## The 4 rules\n\n### 1. Pre-flight estimate — never call generate without one\n\nBefore ANY `slates_generate_*` call, run `slates_estimate_generation_cost` first. Inputs you must lock before estimating:\n\n- **Model** — derived from the op (`slates_generate_image` → `nano-banana-2-{resolution}`)\n- **Resolution** — never let the op default. Pick deliberately. Drafts → 1k. Hero → 2k. Print → 4k.\n- **Aspect ratio** — never let the op default to 1:1. Pick from the use case (cinematic → 16:9, mobile vertical → 9:16, square feed → 1:1).\n- **Count** — explicit. Don't generate 4 when 1 will tell you if the prompt works.\n\nIf aspect ratio or resolution isn't obvious from the user's request, **ask before estimating**. Don't guess.\n\n### 2. Announce in dollars, plainly, before spending\n\nFormat: `About to spend $X.XX on N image(s) at [resolution] [aspect ratio]. Proceed?`\n\nExamples:\n- `About to spend $0.10 on 1 image at 1k 16:9. Proceed?`\n- `About to spend $0.40 on 4 images at 2k 9:16 (variants). Proceed?`\n\nBelow ~$0.20 you can proceed silently after announcing once. Above $0.20 wait for explicit confirmation. Above $0.50 the server itself will gate with `requires_confirm` — pass `confirm: true` only after the user explicitly OKs.\n\n### 3. Aggregate batches into ONE upfront announcement\n\nIf you're planning a multi-call workflow (5 storyboard frames, 3 character variants, a grid of options), **announce the total before the first call**, not five $0.10 announcements after the fact.\n\nFormat: `Plan: N generations totaling $X.XX. [Brief description of the sequence.] Proceed with the batch?`\n\nExample: `Plan: 6 frame generations at 1k 16:9 totaling $0.60 — establishing wide, push-in, two-shot, reverse, OTS, insert. Proceed?`\n\n### 3b. Batch authorization — one approval covers the enumerated batch\n\nWhen the user approves a batch plan with one aggregated cost total up front (\"8 scenes, ~$X total — go\"), that single approval authorizes `confirm=true` on **each enumerated call in that batch** — and nothing beyond it. You do not need to re-ask per call; that's the point of the upfront announcement. Hands-off multi-scene runs depend on this.\n\nBoundaries that re-trigger confirmation:\n\n- Any call's actual estimate exceeds what the announced plan implied for it by **>25%** → stop, surface the delta, get a fresh OK.\n- New calls are added that weren't in the enumerated plan (extra variants, retries beyond the plan, a new scene) → those are NOT covered. Announce and confirm separately.\n- The batch scope changes (different model, resolution, or duration than announced) → re-announce, re-confirm.\n\nOne approval = that plan, as enumerated, at those prices. Nothing else.\n\n### 4. Track the running total\n\nAfter each generation completes, the response includes `cost_cents` (when available). Keep a running tally in your context. Surface it every 3 generations or whenever the user asks \"how much have we spent?\"\n\n## Resolution decision rules\n\n| Use case | Resolution |\n|---|---|\n| First draft of a new prompt | 1k |\n| Storyboard frame (will likely regenerate) | 1k |\n| Hero shot, locked composition | 2k |\n| Print, marketing asset, final delivery | 4k |\n| Iterating to refine | match the previous resolution |\n\nResolution is a price lever, not a free choice: on Nano Banana 2 and FLUX.2 Max, 4k costs roughly 2x 1k (Seedream 5 Lite is flat-priced regardless of resolution). Prices change — call `slates_estimate_generation_cost` or `slates_list_available_models` for current numbers instead of assuming. Pick the cheapest resolution that serves the use case.\n\n## Aspect ratio decision rules\n\nAsk the user when ambiguous. Otherwise:\n\n| Context cue | Aspect ratio |\n|---|---|\n| \"cinematic\", \"film\", \"movie\", \"wide\" | 16:9 |\n| \"TikTok\", \"Reels\", \"Story\", \"mobile vertical\", \"phone\" | 9:16 |\n| \"square\", \"Instagram feed\", \"thumbnail\" | 1:1 |\n| \"ultra-wide\", \"anamorphic\", \"cinemascope\" | 21:9 |\n| \"portrait\", \"magazine cover\", \"vertical\" | 4:5 or 2:3 |\n| \"landscape photo\", \"horizontal\" | 3:2 or 4:3 |\n\nIf the user prompt mixes signals (e.g. \"cinematic Instagram post\"), ask. Don't guess.\n\n## When the gate fires\n\nThe server returns `requires_clarification` when aspect ratio or resolution is missing. The server returns `requires_confirm` when total spend exceeds $0.50. In both cases:\n\n1. Surface the gate response to the user\n2. Get a clean answer\n3. Re-call with the explicit values + `confirm: true` if applicable\n\nDon't bypass the gate by silently filling in defaults. The gates exist because defaults waste money.\n\n## Video is slow + async — a timeout is NOT a failure\n\nVideo gens take minutes (Seedance 4K can run far longer). A client/CLI timeout or a slow, empty-looking response is **not** a failed generation — the job is still running on the provider.\n\n- **Never re-submit a video gen because it \"timed out.\"** That double-charges the user for one video. Re-rolling a slow gen is the single most expensive mistake here.\n- **Poll, don't re-roll.** Use `background: true` on `slates_generate_video`, then poll `slates_get_generation_status` (free, read-only) until it reports `completed` or `failed`. In-flight jobs survive app restarts and are recovered.\n- A gen has only failed when the status comes back `failed` — and a provider *rejection* **refunds** the credits, so failed isolation tests are ~free. Until you see a terminal status, the job is in flight. Wait.\n\n## The 3-strike rule\n\nStop after 3 iterations on the same prompt. Hand back to the user with what you tried and what's not working. The slot machine doesn't converge — if it's not landing, the prompt structure is wrong, not the seed.\n",
7
7
  "slates-direct-response-ad": "---\nname: slates-direct-response-ad\ndescription: Build a 30-second hyper-motion direct-response ad in Slates from a product image and brief. Composes upload → storyboard → frame gen → motion gen → timeline → export. Use when the user drops a product image and asks for \"an ad\", \"a promo video\", \"a TikTok ad\", \"an Instagram ad\", a launch video, or any short-form direct-response video built around a product.\n---\n\n# Direct-response ad — Slates workflow\n\nYou are building a 30-second hyper-motion direct-response ad. The user has handed you a product image (or product URL) and a short brief. Slates desktop is open on the second monitor; the user watches it populate as you work.\n\n**Hard rules**\n\n- Always estimate cost before generating. Use `slates_estimate_generation_cost` and surface the total.\n- All MCP/CLI generation routes through Slates Credits, period. BYOK is desktop-UI only by design — don't suggest \"use your own keys\" workarounds.\n- Default model: `nano-banana-2-2k`. For close-up product hero frames step up to `4k` only if the user asks.\n- Hyper-motion = punchy cuts, 4 frames in 30 seconds, ~7s each. Don't over-storyboard.\n\n## Workflow\n\n### 1. Set up the project\n- Create a project named for the product (`slates_create_project`).\n- If the user gave a product image as a file path, upload it (`slates_upload_reference_image`).\n- If they pasted base64 / a data URL, use the same op with `dataUrl`.\n\n### 2. Generate the storyboard frames\nBuild exactly 4 frames in this order:\n\n| # | Beat | Visual goal |\n|---|------|-------------|\n| 1 | Hook | Hyper-close-up of the product, dramatic light, motion blur edge |\n| 2 | Lifestyle | Real person using/wearing/holding the product, eye contact |\n| 3 | Problem→solution | The before/after moment that justifies the buy |\n| 4 | CTA | Clean product hero with mental room for an overlaid CTA in editing |\n\nFor each frame:\n1. Draft a tight 1-2 sentence prompt (visual only — no copy text in the image).\n2. Reference the product upload's URL or asset ID for visual fidelity.\n3. Call `slates_generate_image` with that prompt + reference. **You see the result inline — evaluate it.**\n4. If it's wrong: refine prompt, regenerate. If it's right: bind it as a frame in the storyboard (`slates_add_frame`).\n\n### 3. Build the storyboard\n- `slates_create_storyboard` named \"30s ad — v1\".\n- Default scene already exists. Add 3 more scenes (\"Hook\", \"Lifestyle\", \"Problem-Solution\", \"CTA\") via `slates_add_scene`, or just add all 4 frames to the default scene.\n- For each generated image, add a frame referencing the asset id (`slates_add_frame`).\n\n### 4. Hand back to the user\n- Surface estimated total credits spent.\n- Tell the user the storyboard is ready and they can either:\n - **In Slates desktop:** click each frame to generate motion (the existing UI handles motion generation).\n - **Continue here:** ask you to keep going.\n\n### 5. If they say keep going — motion, assembly, export\n- Generate motion per frame with `slates_generate_video` (`firstFrameAssetId` = the frame's asset, `background: true`), routed per `slates-model-selection` (Kling 3.0 std 8s by default; Seedance 2 for any physics-heavy beat like the hook). Submit all four, then poll `slates_get_generation_status` until each completes (1-5 min).\n- Assemble: `slates_add_clip_to_timeline` for each completed clip in beat order (Hook → Lifestyle → Problem-Solution → CTA). Verify with `slates_get_timeline`; fix order with `slates_reorder_clips`.\n- Export: `slates_export_video` to an absolute `.mp4` path (default `<slates_get_project_directory>/exports/<product>-ad.mp4`), then `slates_reveal_file` so the user sees the file.\n- Full pipeline doctrine (batch cost authorization, model mixing, multi-take selection): `slates-one-prompt-film`.\n\n## Anti-patterns\n\n- **Don't** generate text overlays in the image. Slates renders captions/CTAs at the editor stage.\n- **Don't** burn credits on slot-machine prompting. If the first generation is off, refine the prompt; don't just regenerate.\n- **Don't** skip the cost estimate. Confirm with the user above $0.50.\n- **Don't** invent visual specifics about the product (colors, textures, angles) that aren't in the reference image. Reference-anchored prompts only.\n\n## Voice\n\nThe ad lives or dies on the hook frame. Tight, sensory, no fluff. Match the user's brand. Default tone is \"scroll-stopping\" not \"informative.\"\n",
8
8
  "slates-edit-and-iterate": "---\nname: slates-edit-and-iterate\ndescription: Iterate on an existing Slates asset — re-evaluate, refine prompt, regenerate or edit. Use when the user has an existing generated image in Slates and wants to \"tweak it\", \"change one thing\", \"make it warmer\", \"remove the second person\", or any other surgical refinement instead of full regeneration.\n---\n\n# Edit and iterate — Slates workflow\n\nThe user already has a generated image in Slates and wants to refine it. The vision-feedback-loop skill defines the general pattern; this skill is the specific recipe for \"I have asset X, here's what's wrong with it.\"\n\n## Workflow\n\n### 1. Pull the current asset back into context\n- The user references an asset by id, frame number, or \"the latest one.\"\n- Resolve to an asset id (`slates_list_assets` if needed).\n- `slates_get_asset_image` with that id to load it inline. **You see the image.**\n\n### 2. Identify the delta\nThe user's request is one of:\n- **Surgical** — \"remove the second figure\", \"make the sword red\", \"swap the background to a bamboo forest\".\n- **Aesthetic** — \"warmer light\", \"more dramatic\", \"softer focus\".\n- **Compositional** — \"wider shot\", \"lower angle\", \"centered subject\".\n- **Wholesale** — \"actually let's try a totally different look.\"\n\n### 3. Pick the right tool\n| Delta type | Approach |\n|---|---|\n| Surgical | `slates_edit_image` — `sourceAssetId` = the original, `prompt` = the change only (\"remove the second figure\"), not a re-description of the whole image. |\n| Aesthetic / compositional | `slates_generate_image` with the original in `referenceAssetIds` + a refined prompt. Don't re-roll from scratch. |\n| Wholesale | New prompt, no reference, fresh generation. Treat as a new brief. |\n\n**`slates_edit_image` shape:** `projectId` + `sourceAssetId` + `prompt` (the edit instruction). Default model `nano-banana-2` — the only edit model that also takes extra `referenceAssetIds`; `flux-2-max` / `seedream-5-lite` use their own edit endpoints and ignore references. The result lands as a NEW asset (prompt prefixed `[Edit]`); the source is untouched. Cost > $0.50 gates on `confirm=true`.\n\n### 4. Generate, evaluate, decide\n- Estimate cost first.\n- After generation, the result is inline. Compare side-by-side with the original (`slates_get_asset_image` again).\n- If the delta is correct: bind to the same slot (frame, character turnaround, etc.) the original was bound to.\n- If the delta missed: one focused refinement, then regenerate. Cap at 3 tries.\n\n### 5. Hand back\n- \"Asset updated. Frame 3 now uses {new_asset_id}.\"\n- Always note what changed and what didn't, so the user can see the surgery worked: \"Lighting shifted to warmer, composition unchanged.\"\n\n## Anti-patterns\n\n- **Don't** delete the original asset until the user confirms the new one. Slates keeps both; the user picks.\n- **Don't** mix surgical and wholesale changes in one regeneration. The user said \"make it warmer\" — don't also reframe the shot.\n- **Don't** re-generate when `slates_edit_image` would work. Edits preserve composition and identity; full regen rolls the dice.\n- **Don't** chain >3 iterations without checking in. If three tries didn't land, the brief is wrong, not the model.\n",
9
- "slates-model-selection": "---\nname: slates-model-selection\ndescription: Which model to pick for a given job — the routing doctrine. Read BEFORE choosing any video or image model, before quoting a plan, and before defaulting anywhere. Kling 3.0 is the general-purpose video default; Seedance 2.0 is the premium tier for anything where physics, effects, or scale remotely matter; Veo 3.1 is a narrow niche (native synced audio in one gen, 16:9 only) and never the default.\n---\n\n# Model selection — the routing doctrine\n\nPick the model FIRST, deliberately, before writing a prompt or quoting a plan. Model routing is a core part of the intelligence users are paying for: the agent knows what each model is good at and which ones underperform for a job — defaulting to the wrong model burns the user's credits on a weaker result.\n\n## Video routing\n\n| Job | Model | Why |\n|---|---|---|\n| **General-purpose — the default for most shots** | **Kling 3.0 std** | Cost-effective workhorse. Strong image-to-video: preserves identity, layout, and text from the start frame. Any aspect ratio, 5–15s. |\n| Higher visual polish, no physics demands | Kling 3.0 pro | Mid-price fidelity bump on the same strengths. |\n| Multi-character dialogue / audio co-generation | Kling 3.0 omni | Dialogue syntax, voice direction, language codes, `@element` refs. |\n| **Anything with remotely important physics** — effects, destruction, water/fire/smoke/cloth, creature motion, scale, complex simultaneous action | **Seedance 2.0** | The premium tier. Physics and effects are its whole edge; up to 9 ingredient refs, first+last frame, native 4K. |\n| The premium hero shot a piece hangs on | Seedance 2.0 | Spend where it shows. |\n| Native synchronized audio (dialogue + SFX generated WITH the video in one gen), 16:9, ≤8s | Veo 3.1 | The only job Veo wins. |\n\n**Rules:**\n\n- **Default video = Kling 3.0 std.** Escalate to Seedance the moment the shot has physics/effects weight or is the hero moment — and say why in the plan (\"physics-heavy, routing to Seedance\").\n- **Veo is never the default.** 16:9 only, 4/6/8s only, and it is not the quality pick — treat it as a single-purpose tool for native-synced-audio shots. If audio can be added after (Kling lip-sync, edit stage), prefer Kling or Seedance + audio in post.\n- **9:16 vertical → Kling or Seedance.** Veo can't.\n- **Image-to-video from an NB2 start frame** (the standard pipeline) → Kling by default, Seedance when the motion is physics-heavy. Not Veo.\n- **User names a model explicitly → use it.** But if it's a mismatch for the job (crazy physics on Kling std, vertical on Veo), say so in one line and offer the right route before generating.\n\n## Image routing\n\n- **Default: Nano Banana 2** — best reference handling (14 refs), best legible text, the standard start-frame generator.\n- **FLUX.2 Max** — photoreal texture, hex-color binding, typography, less censored.\n- **Seedream 5 Lite** — cheapest; flat-priced drafts and volume exploration.\n\n## Cost is a tiebreaker, not the router\n\nRoute by capability first, then pick the cheapest tier that serves the job (per `slates-cost-discipline`). Never pick a model because its per-second price looked lowest — a cheap clip that has to be regenerated on the right model costs more than routing correctly once.\n",
9
+ "slates-model-selection": "---\nname: slates-model-selection\ndescription: Which model to pick for a given job — the routing doctrine. Read BEFORE choosing any video or image model, before quoting a plan, and before defaulting anywhere. Kling 3.0 is the general-purpose video default; Seedance 2.0 is the premium tier for anything where physics, effects, or scale remotely matter; Veo 3.1 is a narrow niche (native synced audio in one gen, 16:9 only) and never the default.\n---\n\n# Model selection — the routing doctrine\n\nPick the model FIRST, deliberately, before writing a prompt or quoting a plan. Model routing is a core part of the intelligence users are paying for: the agent knows what each model is good at and which ones underperform for a job — defaulting to the wrong model burns the user's credits on a weaker result.\n\n## Video routing\n\n| Job | Model | Why |\n|---|---|---|\n| **General-purpose — the default for most shots** | **Kling 3.0 std** | Cost-effective workhorse. Strong image-to-video: preserves identity, layout, and text from the start frame. Any aspect ratio, 5–15s. |\n| Higher visual polish, no physics demands | Kling 3.0 pro | Mid-price fidelity bump on the same strengths. |\n| Multi-character dialogue / audio co-generation | Kling 3.0 omni | Dialogue syntax, voice direction, language codes, `@element` refs. |\n| **Anything with remotely important physics** — effects, destruction, water/fire/smoke/cloth, creature motion, scale, complex simultaneous action | **Seedance 2.0** | The premium tier. Physics and effects are its whole edge; up to 9 ingredient refs, first+last frame, native 4K. |\n| The premium hero shot a piece hangs on | Seedance 2.0 | Spend where it shows. |\n| Native synchronized audio (dialogue + SFX generated WITH the video in one gen), 16:9, ≤8s | Veo 3.1 | The only job Veo wins. |\n\n**Rules:**\n\n- **Default video = Kling 3.0 std.** Escalate to Seedance the moment the shot has physics/effects weight or is the hero moment — and say why in the plan (\"physics-heavy, routing to Seedance\").\n- **Veo is never the default.** 16:9 only, 4/6/8s only, and it is not the quality pick — treat it as a single-purpose tool for native-synced-audio shots. If audio can be added after (Kling lip-sync, edit stage), prefer Kling or Seedance + audio in post.\n- **9:16 vertical → Kling or Seedance.** Veo can't.\n- **Image-to-video from an NB2 start frame** (the standard pipeline) → Kling by default, Seedance when the motion is physics-heavy. Not Veo.\n- **User names a model explicitly → use it.** But if it's a mismatch for the job (crazy physics on Kling std, vertical on Veo), say so in one line and offer the right route before generating.\n\n## Image routing\n\n**Video models (Kling, Seedance, Veo) cannot generate standalone images — ever.** A \"premium hero reference image\" is still an image job: it routes to an image model below, never to Seedance.\n\n- **Default: Nano Banana 2** — best reference handling (14 refs), best legible text, the standard start-frame generator.\n- **FLUX.2 Max** — photoreal texture, hex-color binding, typography, less censored.\n- **Seedream 5 Lite** — cheapest; flat-priced drafts and volume exploration.\n\n## Cost is a tiebreaker, not the router\n\nRoute by capability first, then pick the cheapest tier that serves the job (per `slates-cost-discipline`). Never pick a model because its per-second price looked lowest — a cheap clip that has to be regenerated on the right model costs more than routing correctly once.\n",
10
10
  "slates-one-prompt-film": "---\nname: slates-one-prompt-film\ndescription: The full one-prompt-to-finished-film pipeline in Slates — script, project, characters, storyboard, frame images, video generation, timeline assembly, MP4 export. Use when the user gives one idea and wants a finished video out the other end - \"make me a video about X\", \"turn this idea into an ad\", \"make a short film from this\", \"one prompt, finished film\". This is the master recipe; other Slates skills are its sub-steps.\n---\n\n# One prompt → finished film — Slates master pipeline\n\nThe user gives an idea. You hand back an MP4 on disk. Everything in between is yours, with exactly TWO mandatory user checkpoints: the creative plan, and ONE aggregated cost approval.\n\n## The pipeline\n\n### 1. Script the beats\nTurn the idea into a beat-level script: 4-10 shots, each with subject, action, setting, camera, and duration (4-8s per shot). Surface it as a tight table. Get the user's nod on the plan, format (aspect ratio — 16:9 vs 9:16 decides everything downstream), and rough budget appetite before touching any op.\n\n### 2. Set up the project\n- `slates_create_project` named for the piece.\n- Recurring character? Build it properly — `slates_create_character` + the `slates-character-turnaround` recipe — so every frame references the same turnaround.\n- Recurring location? `slates_create_environment`.\n- One-off shots don't need character/environment records; skip the ceremony.\n\n### 3. Storyboard skeleton (no generation yet)\n- `slates_create_storyboard`, `slates_add_scene` per script scene.\n- Structure first, spend second — the user catches script problems on the free skeleton, not on burned credits.\n\n### 4. ONE aggregated cost approval — then hands-off\nPrice the whole batch before the first generation: frame images (count × model — `slates_estimate_generation_cost`) + video gens (count × model × duration). Present a single total:\n\n> Plan: 6 frames at 1k 16:9 + 5 × 8s Kling 3.0 std + 1 × 8s Seedance 2 hero shot ≈ $X.XX total. Proceed with the batch?\n\nPer `slates-cost-discipline` 3b: that single OK authorizes `confirm=true` for **every enumerated call in the batch** — no per-call re-asking. Re-confirm only if a call's price overruns the plan >25% or new calls get added (extra retakes, new shots).\n\n### 5. Generate frame images\nPer shot: `slates_generate_image` with `referenceAssetIds` pointing at the character turnaround / environment / prior frames for consistency (Slates names each reference inline as \"image N\" — you don't hand-write role labels; reuse the same subject name across shots). Evaluate every result inline against the beat. Bind keepers via `slates_add_frame`.\n\n**Multi-take where it matters:** for the hook shot and any shot the whole film hangs on, generate 2-4 variants (cheap model or 1k), pull them back with `slates_get_assets_batch`, pick the strongest on composition + identity, discard the rest. Don't multi-take filler shots.\n\n### 6. Generate video per frame — background mode\n`slates_generate_video` with `firstFrameAssetId` = the bound frame, `background: true`. Submit ALL shots, collect the generationIds, then poll `slates_get_generation_status` every 10-15s (1-5 min per gen; they survive app restarts). This parallelizes a 6-shot film into one wait instead of six.\n\n**Model mixing — route per `slates-model-selection`** (details in the per-model guides):\n- **Kling V3** (`slates-prompting-kling-v3`): the DEFAULT for most shots — any aspect ratio, 5-15s, strong start-frame adherence; std is the workhorse, Omni for multi-character dialogue.\n- **Seedance 2** (`slates-prompting-seedance`): the PREMIUM tier — any shot where physics/effects/scale remotely matter, plus the hero shot; audio included, first+last frame guidance, native 4K.\n- **Veo 3.1** (`slates-prompting-veo-3`): niche, never the default — only when native synced audio must generate WITH the video in one gen; 16:9 only, 4/6/8s.\n\nFailed gen? Check the error via `slates_get_generation_status`, fix the prompt, resubmit that one shot (a retry beyond the plan = announce the delta cost).\n\n### 7. Assemble the timeline\n- `slates_get_timeline` once to get the lay of the land.\n- `slates_add_clip_to_timeline` for each completed video asset **in story order** — defaults append back-to-back on the first video track, which is exactly an assembly cut.\n- Order wrong? `slates_reorder_clips` with the full clip-id list. Dropped a shot? `slates_remove_clip`, then reorder to close the gap.\n\n### 8. Export + deliver\n- Output path: ask the user, or default to `<slates_get_project_directory>/exports/<name>.mp4`.\n- `slates_export_video` (absolute path, `.mp4`; blocks while ffmpeg renders — minutes for long timelines).\n- `slates_reveal_file` so the file is literally in front of them.\n- Offer the finishing path: `slates_export_timeline_xml` → DaVinci Resolve (File → Import → Timeline) for grading, sound, and titles.\n\n### 9. Report\nShots delivered, total spent vs. approved plan, the export path, and the single best next lever (\"re-take shot 3 with a tighter prompt\" / \"add a CTA end-card\").\n\n## Hard rules\n\n- **Two checkpoints only.** Creative plan (step 1) and total cost (step 4). Everything else runs without asking — that's the product promise.\n- **Skeleton before spend.** Project + storyboard structure are free; generation isn't.\n- **Look at everything.** Every image inline, every video via `slates_get_asset_video_frames` if a clip seems off. Never assemble a timeline from clips you haven't evaluated.\n- **3-strike rule per shot.** Three failed takes on one shot = stop, show the user what you tried, ask.\n- **Consistency comes from references, not luck.** Same turnaround asset on every character frame; same environment refs across a location's shots.\n",
11
11
  "slates-project-organization": "---\nname: slates-project-organization\ndescription: How a Slates project's assets are organized AND named — the asset short-code system (IMG-A12 / VID-V3 / AUD-S1 badges on every gallery card), folders for film STRUCTURE, the typed tabs for reusable references. Read when the user refers to an asset by code, asks what a code like IMG-A36 means, or when organizing/navigating a project.\n---\n\n# Organizing a Slates project\n\nSlates already gives each REUSABLE reference type its own home — the **Characters**, **Environments**, and **Styles** tabs, each with its own generation + `@mention`/`#ref` behavior. Do NOT recreate those as folders. Folders are for **structure**, never type.\n\n**Folders = where an asset sits in the FILM**, and they mirror to real subfolders on disk (`projects/<id>/…`), so a human can open the project in Resolve/Finder and navigate it like an edit. Use them for work product, not references.\n\nCreate with `slates_create_folder`; file assets with `slates_move_assets_to_folder`. Generations land in the project's active folder, so set it before a batch.\n\nConventions by project type:\n- **Short film / narrative:** `Shots` (scene stills) · `Clips` (generated video) · `Final` (the export). Use one folder per scene (`Scene 1`, `Scene 2`, …) instead when the piece has distinct locations/beats.\n- **Ad / UGC:** `Hooks` · `B-roll` · `Talking-head` · `Final`.\n\nRules of thumb:\n- Reusable cast / sets / look → leave in the Characters/Environments/Styles tabs. Don't fold them.\n- Scene stills, clips, and the final cut → file into the structural folder they belong to, as you make them.\n- One folder per asset (folders are structure). Cross-cutting status (hero take, reject, variant) is a tag concern, not a folder.\n- Keep the gallery legible: work product lives in folders; the reference scaffolding (sheets, plates, style images) stays in its tabs.\n\n## Asset codes — the shared vocabulary (IMG-A12 / VID-V3 / AUD-S1)\n\nEvery asset gets a short, stable code the moment it lands in a project, and the user sees it as the badge in the **top-left corner of every image and video card** in the gallery. This is the shared vocabulary between you and the user — it exists so neither of you ever has to quote a UUID.\n\n**The scheme:**\n- `IMG-A{n}` = images · `VID-V{n}` = videos · `AUD-S{n}` = audio.\n- Numbering is **per project, per type**, counts up from 1, and **numbers are never reused** — deleting IMG-A12 doesn't renumber anything, so a code always means the same asset forever.\n- Each asset also carries a **label**: the first ~4 meaningful words of its prompt, title-cased. Chat format is code + label: `IMG-A12 — Beach Sunset`.\n\n**How to use it:**\n- **User names a code** (\"use IMG-A36 as the reference\", \"animate VID-V3's last frame\") → resolve it via `slates_list_assets` (match the `code` field) to get the assetId, confirm back in the same vocabulary: \"Got it — IMG-A36 — Marcus Rooftop Close-Up as the first frame.\"\n- **You name assets** → ALWAYS code + label, never UUID, never \"the beach one\" (which of three?). The user matches your words to the badge by eye.\n- **User seems confused** about what a code is or how to point you at an image → explain it in one line: \"Every image and video in your gallery has a code badge in its top-left corner — like IMG-A36. Just say that code and I'll know exactly which one you mean.\"\n- **Ambiguity** (\"the sunset image\" when several exist) → pull candidates with `slates_get_assets_batch` and offer the codes: \"I see IMG-A12, IMG-A19, and IMG-A24 with sunsets — which one?\"\n",
12
12
  "slates-prompting-flux-2-max": "---\nname: slates-prompting-flux-2-max\ndescription: How to prompt FLUX.2 Max (Black Forest Labs image model). Read before calling slates_generate_image with model flux-2-max, or slates_edit_image with editModel flux-2-max. FLUX.2 wants front-loaded structure, real camera vocabulary, and positive-only phrasing — no negative prompts, no tag soup.\n---\n\n# FLUX.2 Max — prompting\n\nBlack Forest Labs' top image model, routed via fal.ai. In Slates: `slates_generate_image` with `model: flux-2-max` (REQUIRES projectId — no headless path), priced per resolution (1k/2k/4k — call `slates_estimate_generation_cost` for current numbers, never quote from memory). Strengths vs Nano Banana 2: photoreal texture, less censored, precise hex-color control, strong typography. Reference images route through FLUX's edit endpoint and carry a lower per-model cap than NB2's 14.\n\n## Core structure — front-load what matters\n\n```\nSubject + Action + Style + Context\n```\n\nWord order is weight. FLUX.2 attends hardest to the start of the prompt: main subject → key action → critical style → essential context → secondary details.\n\n**Length:** 10-30 words for concept tests, 30-80 words for most work, 80+ only for genuinely complex scenes.\n\n## Photorealism: name real gear, not \"professional photo\"\n\nThe single biggest realism lever is concrete camera vocabulary:\n\n```\nShot on Hasselblad X2D, 80mm lens, f/2.8, natural lighting\nShot on Sony A7IV, 35mm, golden hour, shallow depth of field\nKodak Portra 400, natural grain, organic colors\n```\n\nEra cues work the same way: \"early digital camera, slight noise, flash photography, candid\" reads 2000s digicam; \"film grain, warm color cast, soft focus\" reads 80s.\n\nFor portraits add: natural skin texture, realistic pores, subtle imperfections, soft diffused lighting.\n\n## No negative prompts — reframe positively\n\nFLUX.2 has no negative prompt support. Describe the presence you want, not the absence:\n\n- ❌ \"no blur\" → ✅ \"sharp focus throughout\"\n- ❌ \"no people\" → ✅ \"empty scene\"\n- ❌ \"no harsh shadows\" → ✅ \"soft, diffused lighting\"\n\n## Hex colors — bind them to objects\n\nFLUX.2 matches hex codes, but only when each code is attached to a specific object:\n\n```\nwalls in hex #C4725A, sofa in #1B6B6F, accent pillows #E8A847\ngradient starting with color #02eb3c and finishing with color #edfa3c\n```\n\n❌ \"use #FF0000 somewhere\" — unbound colors land inconsistently.\n\n## Text rendering\n\nQuote the exact text, then place and style it:\n\n```\nThe text 'OPEN' appears in red neon letters above the door\nLogo text 'ACME' in color #FF5733, ultra-bold decorative serif, centered\n```\n\nSpecify placement relative to other elements, font family feel (serif / sans / script), and relative size (\"large headline,\" \"small body copy\").\n\n## JSON prompting for production work\n\nFor multi-element scenes that must come out exactly right (product shots, infographics, brand work), FLUX.2 parses structured JSON prompts:\n\n```json\n{\n \"scene\": \"Professional studio product photography on polished concrete\",\n \"subjects\": [{ \"description\": \"matte black ceramic mug with steam\", \"position\": \"center foreground\" }],\n \"style\": \"commercial product photography\",\n \"color_palette\": [\"#1B1B1B\", \"#E8A847\"],\n \"lighting\": \"three-point softbox, soft diffused highlights\",\n \"camera\": { \"lens-mm\": 85, \"f-number\": \"f/5.6\" }\n}\n```\n\nUse natural language for exploration, JSON when the layout is locked and you're matching a spec.\n\n## Reference images (edit path)\n\nIn Slates, pass `referenceAssetIds` on `slates_generate_image` — FLUX routes them through its edit endpoint. Slates names each reference inline in the prompt (\"the subject (image 1), the style (image 2)\") in the order it sends them, so you don't hand-write role labels; the name carries the role and unnamed-by-position blending is avoided. For surgical changes to one existing image use `slates_edit_image` with `editModel: flux-2-max` (note: FLUX edits ignore extra referenceAssetIds — that's NB2-only).\n\nReference discipline (FLUX caps refs lower than NB2's 14, so be deliberate):\n- **2-4 strong refs**, one per role, named — not 1 (warps), not many (blends).\n- **Flat-lit identity refs** — a studio-lit / scene-lit character sheet bleeds its lighting into the output.\n- **Attach both character sheets, named as one entity** — turnaround (body/proportion/outfit) + close-up expression sheet (face detail), cited under the same name; the shared name keeps the expressions from averaging the face. Don't write a role essay or \"render neutral\" instruction — the user's prompt owns the expression, wardrobe, and lighting.\n- **Environment: describe it, don't feed a multi-panel grid** — reserve a ref for a hard exact-match, then use ONE clean establishing image.\n\n## Character consistency across a series\n\nDefine the character exhaustively once, then repeat those exact descriptors verbatim in every subsequent prompt. FLUX has no memory between generations — the repeated description IS the consistency mechanism.\n\n## Common failure modes + fixes\n\n| Failure | Fix |\n|---|---|\n| Generic \"AI look\" on photoreal | Name a camera body + lens + f-stop instead of \"professional photo\" |\n| Colors drift from brand spec | Bind each hex code to a named object |\n| Text garbled | Quote the exact string, specify font feel + placement + size |\n| Multi-reference blend chaos | Name each reference inline (Slates does this from your @mentions/referenceAssetIds) — the same name for one entity, distinct names per role |\n| Wanted element missing | Move it earlier in the prompt — order is weight |\n\n## Pre-flight: references arrive inline, refer by code\n\nWhen you pass `referenceAssetIds`, the first call returns the references **inline as image content blocks** with a cost estimate and `requires_confirm: true`. Look at them — revise the prompt if they suggest a different composition or style — then re-call with `confirm=true`. Refer to each asset by its short code (`IMG-A12 — Beach Sunset`) when talking to the user; it matches the badge on their gallery thumbnail.\n\n## Sources\n\n- [Black Forest Labs — FLUX.2 Prompting Guide](https://docs.bfl.ml/guides/prompting_guide_flux2)\n- [fal.ai — FLUX.2 [max] Prompt Guide](https://fal.ai/learn/devs/flux-2-max-prompt-guide)\n",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slatesvideo/shared",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "Shared operations layer for the Slates MCP server and CLI: auth, cloud/desktop clients, and the single tool surface both consume. Most users want @slatesvideo/mcp-server or @slatesvideo/cli instead.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,6 +28,8 @@ Pick the model FIRST, deliberately, before writing a prompt or quoting a plan. M
28
28
 
29
29
  ## Image routing
30
30
 
31
+ **Video models (Kling, Seedance, Veo) cannot generate standalone images — ever.** A "premium hero reference image" is still an image job: it routes to an image model below, never to Seedance.
32
+
31
33
  - **Default: Nano Banana 2** — best reference handling (14 refs), best legible text, the standard start-frame generator.
32
34
  - **FLUX.2 Max** — photoreal texture, hex-color binding, typography, less censored.
33
35
  - **Seedream 5 Lite** — cheapest; flat-priced drafts and volume exploration.