@slatesvideo/shared 0.5.0 → 0.5.2
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/clients/cloud.d.ts +7 -7
- package/dist/operations/index.d.ts +10 -2
- package/dist/operations/index.js +217 -94
- package/dist/prompts/model-facts.js +18 -2
- package/dist/skills/content.js +13 -12
- package/package.json +1 -1
- package/skills/slates-content-policy.md +14 -1
- package/skills/slates-cost-discipline.md +15 -11
- package/skills/slates-direct-response-ad.md +2 -2
- package/skills/slates-edit-and-iterate.md +1 -1
- package/skills/slates-model-selection.md +11 -7
- package/skills/slates-one-prompt-film.md +1 -1
- package/skills/slates-prompting-lip-sync.md +12 -12
- package/skills/slates-prompting-motion-transfer.md +11 -11
- package/skills/slates-prompting-omni-flash.md +44 -0
- package/skills/slates-prompting-seedance.md +1 -1
- package/skills/slates-prompting-veo-3.md +1 -1
- package/skills/slates-storyboard-from-script.md +1 -1
- package/skills/slates-vision-feedback-loop.md +1 -1
package/dist/operations/index.js
CHANGED
|
@@ -28,6 +28,29 @@ function ok(data, text) {
|
|
|
28
28
|
data,
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
+
// ── Credits (2026-07-07 re-denomination) ────────────────────────
|
|
32
|
+
// Costs are ABSTRACT CREDITS now, not dollars. The API's model registry
|
|
33
|
+
// returns `cost_credits` (with `cost_cents` kept as a legacy alias carrying
|
|
34
|
+
// the same credit value); balances the same. Read via creditCost(); display
|
|
35
|
+
// via fmtCredits(). The confirm gate fires above CONFIRM_CREDITS (≈ the old
|
|
36
|
+
// $0.50 gate at the 3¢/credit peg).
|
|
37
|
+
const CONFIRM_CREDITS = 17;
|
|
38
|
+
const CENTS_PER_CREDIT = 3; // peg: 1 credit = 3¢ billed = 2¢ COGS (mirror of slates-api)
|
|
39
|
+
function creditCost(m) {
|
|
40
|
+
if (!m)
|
|
41
|
+
return 0;
|
|
42
|
+
return m.cost_credits ?? m.cost_cents ?? 0;
|
|
43
|
+
}
|
|
44
|
+
function fmtCredits(credits) {
|
|
45
|
+
return `${Math.round(credits).toLocaleString('en-US')} credits`;
|
|
46
|
+
}
|
|
47
|
+
/** Convert a legacy desktop dollar estimate (COGS × markup) to billed credits,
|
|
48
|
+
* mirroring the server's toCredits(round(dollars × 100)). The desktop's
|
|
49
|
+
* generations.cost column still stores dollars, so status reads convert. */
|
|
50
|
+
function creditsFromDollars(dollars) {
|
|
51
|
+
const cents = Math.round(dollars * 100);
|
|
52
|
+
return cents <= 0 ? 0 : Math.max(1, Math.ceil(cents / CENTS_PER_CREDIT));
|
|
53
|
+
}
|
|
31
54
|
// Shared describe-text for the background flag on every generate_* op.
|
|
32
55
|
const BACKGROUND_DESCRIBE = 'Submit and return immediately with generationId(s) instead of blocking until the file is saved. ' +
|
|
33
56
|
'Poll with slates_get_generation_status. Recommended for video (1-5 min renders).';
|
|
@@ -71,16 +94,17 @@ export const getMe = {
|
|
|
71
94
|
};
|
|
72
95
|
export const getCreditBalance = {
|
|
73
96
|
id: 'slates_get_credit_balance',
|
|
74
|
-
description: 'Current Slates credit balance
|
|
97
|
+
description: 'Current Slates credit balance (abstract credits — never expire). Call before any generation that costs credits.',
|
|
75
98
|
input: z.object({}).strict(),
|
|
76
99
|
async run(_input, ctx) {
|
|
77
100
|
const r = await ctx.cloud().get('/api/agent/credits/balance');
|
|
78
|
-
|
|
101
|
+
const credits = r.credit_balance ?? r.credit_balance_cents ?? 0;
|
|
102
|
+
return ok({ success: r.success, credit_balance: credits }, `Balance: ${fmtCredits(credits)}.`);
|
|
79
103
|
},
|
|
80
104
|
};
|
|
81
105
|
export const listAvailableModels = {
|
|
82
106
|
id: 'slates_list_available_models',
|
|
83
|
-
description: 'Registry of generation model cost keys with per-call credit cost
|
|
107
|
+
description: 'Registry of generation model cost keys with per-call credit cost, as a compact "key credits" table. Optional `filter` substring (e.g. "kling" or "nano-banana") keeps the result small — prefer it. For a single known model, slates_estimate_generation_cost is cheaper still.',
|
|
84
108
|
input: z.object({
|
|
85
109
|
filter: z.string().optional().describe('Substring match on the model key, e.g. "kling-v3" or "seedance"'),
|
|
86
110
|
}),
|
|
@@ -91,21 +115,21 @@ export const listAvailableModels = {
|
|
|
91
115
|
const q = input.filter.toLowerCase();
|
|
92
116
|
models = models.filter((m) => m.model.toLowerCase().includes(q));
|
|
93
117
|
}
|
|
94
|
-
// Compact text table — "key
|
|
118
|
+
// Compact text table — "key credits" lines are ~5x denser than the raw
|
|
95
119
|
// JSON registry (the full pretty-printed dump was an ~8k-token leak).
|
|
96
|
-
const table = models.map((m) => `${m.model} ${m
|
|
120
|
+
const table = models.map((m) => `${m.model} ${creditCost(m)}`).join('\n');
|
|
97
121
|
return {
|
|
98
|
-
text: `${models.length} COST keys (
|
|
122
|
+
text: `${models.length} COST keys (credits per generation)${input.filter ? ` matching "${input.filter}"` : ''}. ` +
|
|
99
123
|
`NOTE: these are billing keys for cost lookup ONLY — the \`model\` param on slates_generate_video takes a BASE id ` +
|
|
100
124
|
`(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` +
|
|
101
125
|
table,
|
|
102
|
-
data: { count: models.length
|
|
126
|
+
data: { count: models.length },
|
|
103
127
|
};
|
|
104
128
|
},
|
|
105
129
|
};
|
|
106
130
|
export const estimateGenerationCost = {
|
|
107
131
|
id: 'slates_estimate_generation_cost',
|
|
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
|
|
132
|
+
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 confirm gate.',
|
|
109
133
|
input: z.object({
|
|
110
134
|
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")'),
|
|
111
135
|
quantity: z.number().int().min(1).max(10).optional().describe('Number of generations (default 1)'),
|
|
@@ -119,7 +143,7 @@ export const estimateGenerationCost = {
|
|
|
119
143
|
}),
|
|
120
144
|
async run(input, ctx) {
|
|
121
145
|
const registry = await ctx.cloud().get('/api/agent/models');
|
|
122
|
-
const byKey = new Map(registry.models.map((m) => [m.model, m
|
|
146
|
+
const byKey = new Map(registry.models.map((m) => [m.model, creditCost(m)]));
|
|
123
147
|
// 1) exact registry cost key
|
|
124
148
|
let key = byKey.has(input.model) ? input.model : null;
|
|
125
149
|
// 2) image base id + resolution (+ quality for gpt-image-2)
|
|
@@ -164,21 +188,20 @@ export const estimateGenerationCost = {
|
|
|
164
188
|
});
|
|
165
189
|
}
|
|
166
190
|
}
|
|
167
|
-
const
|
|
168
|
-
if (key == null ||
|
|
191
|
+
const perCredits = key != null ? byKey.get(key) : undefined;
|
|
192
|
+
if (key == null || perCredits == null) {
|
|
169
193
|
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.`);
|
|
170
194
|
}
|
|
171
195
|
const qty = input.quantity ?? 1;
|
|
172
|
-
const
|
|
196
|
+
const totalCredits = perCredits * qty;
|
|
173
197
|
return ok({
|
|
174
198
|
model: input.model,
|
|
175
199
|
cost_key: key,
|
|
176
200
|
quantity: qty,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
});
|
|
201
|
+
cost_per_credits: perCredits,
|
|
202
|
+
total_credits: totalCredits,
|
|
203
|
+
requires_confirm: totalCredits > CONFIRM_CREDITS,
|
|
204
|
+
}, `${input.model}${qty > 1 ? ` ×${qty}` : ''}: ${fmtCredits(totalCredits)} (${key}).`);
|
|
182
205
|
},
|
|
183
206
|
};
|
|
184
207
|
// ── Projects ────────────────────────────────────────────────────
|
|
@@ -416,12 +439,16 @@ export const getAssetVideoFrames = {
|
|
|
416
439
|
};
|
|
417
440
|
export const uploadReferenceImage = {
|
|
418
441
|
id: 'slates_upload_reference_image',
|
|
419
|
-
description: 'Add a reference image to a Slates project. Pass either filePath (absolute path to a local file) or dataUrl (base64 data: URL) — exactly one.',
|
|
442
|
+
description: 'Add a reference image OR video clip to a Slates project. Pass either filePath (absolute path to a local file) or dataUrl (base64 data: URL) — exactly one. Set type:"video" on a filePath import to bring in a clip (the user\'s own footage to edit/relocate/trim); imported videos are probed on ingest, so duration + dimensions are available immediately. Default type is "image". dataUrl is image-only.',
|
|
420
443
|
input: z
|
|
421
444
|
.object({
|
|
422
445
|
projectId: z.string().uuid(),
|
|
423
446
|
filePath: z.string().optional(),
|
|
424
447
|
dataUrl: z.string().optional(),
|
|
448
|
+
type: z
|
|
449
|
+
.enum(['image', 'video'])
|
|
450
|
+
.optional()
|
|
451
|
+
.describe('Asset kind for a filePath import — "image" (default) or "video". A dataUrl is always an image.'),
|
|
425
452
|
})
|
|
426
453
|
.refine((d) => !!d.filePath !== !!d.dataUrl, {
|
|
427
454
|
message: 'Pass exactly one of filePath or dataUrl',
|
|
@@ -432,10 +459,13 @@ export const uploadReferenceImage = {
|
|
|
432
459
|
const r = await desktop.post('/agent/assets/upload', {
|
|
433
460
|
projectId: input.projectId,
|
|
434
461
|
filePath: input.filePath,
|
|
435
|
-
type: 'image',
|
|
462
|
+
type: input.type ?? 'image',
|
|
436
463
|
});
|
|
437
464
|
return ok(r);
|
|
438
465
|
}
|
|
466
|
+
if (input.type === 'video') {
|
|
467
|
+
throw new Error('dataUrl uploads are image-only — pass a filePath to import a video clip.');
|
|
468
|
+
}
|
|
439
469
|
const r = await desktop.post('/agent/assets/upload-base64', {
|
|
440
470
|
projectId: input.projectId,
|
|
441
471
|
dataUrl: input.dataUrl,
|
|
@@ -573,7 +603,7 @@ export const generateCharacterSheets = {
|
|
|
573
603
|
};
|
|
574
604
|
export const generateEnvironmentPlate = {
|
|
575
605
|
id: 'slates_generate_environment_plate',
|
|
576
|
-
description: "Generate an environment's single clean establishing plate (Nano Banana 2, 2K) from an optional base image, and bind it as the environment's reference. THE real environment-building workflow — call right after slates_create_environment. Afterward, pass the plate asset id as environmentAssetIds to slates_generate_video (or referenceAssetIds to slates_generate_image) so the location stays consistent across shots. Cost
|
|
606
|
+
description: "Generate an environment's single clean establishing plate (Nano Banana 2, 2K) from an optional base image, and bind it as the environment's reference. THE real environment-building workflow — call right after slates_create_environment. Afterward, pass the plate asset id as environmentAssetIds to slates_generate_video (or referenceAssetIds to slates_generate_image) so the location stays consistent across shots. Cost ~6 credits.",
|
|
577
607
|
input: z.object({
|
|
578
608
|
environmentId: z.string().uuid(),
|
|
579
609
|
projectId: z.string().uuid(),
|
|
@@ -765,7 +795,7 @@ export const generateImage = {
|
|
|
765
795
|
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.'),
|
|
766
796
|
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."),
|
|
767
797
|
background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
|
|
768
|
-
confirm: z.boolean().optional().describe('Set true to bypass the
|
|
798
|
+
confirm: z.boolean().optional().describe('Set true to bypass the confirm gate.'),
|
|
769
799
|
}),
|
|
770
800
|
async run(input, ctx) {
|
|
771
801
|
// Clarification gate: aspectRatio + resolution must be deliberate.
|
|
@@ -852,26 +882,26 @@ export const generateImage = {
|
|
|
852
882
|
const entry = registry.models.find((m) => m.model === costKey);
|
|
853
883
|
if (!entry)
|
|
854
884
|
throw new Error(`Model not in registry: ${costKey}`);
|
|
855
|
-
const totalCents = entry
|
|
885
|
+
const totalCents = creditCost(entry) * (input.count ?? 1);
|
|
856
886
|
// Confirm gate. Fires on cost > $0.50, AND (look-first, mirroring
|
|
857
887
|
// slates_generate_video) whenever reference assets are involved
|
|
858
888
|
// regardless of cost — the LLM must see what it's referencing before
|
|
859
889
|
// committing spend.
|
|
860
|
-
if ((totalCents >
|
|
890
|
+
if ((totalCents > CONFIRM_CREDITS || referenceAssetIds.length > 0) && !input.confirm) {
|
|
861
891
|
if (referenceAssetIds.length === 0) {
|
|
862
892
|
return ok({
|
|
863
893
|
requires_confirm: true,
|
|
864
894
|
model: costKey,
|
|
865
895
|
estimated_cents: totalCents,
|
|
866
|
-
|
|
867
|
-
message:
|
|
896
|
+
estimated_credits: totalCents,
|
|
897
|
+
message: `Cost exceeds ${CONFIRM_CREDITS} credits. Re-call with confirm=true to proceed, or pick a smaller resolution / count.`,
|
|
868
898
|
});
|
|
869
899
|
}
|
|
870
900
|
const previews = await previewAssets(ctx, referenceAssetIds.map((id) => ({ id, type: 'image', role: 'reference' })));
|
|
871
901
|
const refLines = previews.map((p) => ` - ${p.role}: ${p.ref}`).join('\n');
|
|
872
902
|
return {
|
|
873
903
|
text: `Pre-flight for ${imageModel} (${costKey}): ` +
|
|
874
|
-
|
|
904
|
+
`${fmtCredits(totalCents)}.` +
|
|
875
905
|
`\n\nReference images attached above:\n${refLines}\n\n` +
|
|
876
906
|
`Review them against your prompt — every reference's role must be labeled in the prompt text. ` +
|
|
877
907
|
`If the references suggest a different composition / style than the current prompt captures, REVISE the prompt before confirming. ` +
|
|
@@ -883,7 +913,7 @@ export const generateImage = {
|
|
|
883
913
|
model: imageModel,
|
|
884
914
|
variant: costKey,
|
|
885
915
|
estimated_cents: totalCents,
|
|
886
|
-
|
|
916
|
+
estimated_credits: totalCents,
|
|
887
917
|
references: previews.map((p) => ({
|
|
888
918
|
role: p.role,
|
|
889
919
|
ref: p.ref,
|
|
@@ -931,7 +961,7 @@ export const generateImage = {
|
|
|
931
961
|
costKey,
|
|
932
962
|
projectId: input.projectId,
|
|
933
963
|
cost_cents: totalCents,
|
|
934
|
-
|
|
964
|
+
cost_credits: totalCents,
|
|
935
965
|
}, refEcho);
|
|
936
966
|
}
|
|
937
967
|
const assetList = result.assets
|
|
@@ -957,7 +987,7 @@ export const generateImage = {
|
|
|
957
987
|
`The saved assets are in data.assets — re-generate only the missing count, don't redo the whole batch. ` +
|
|
958
988
|
`Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"`
|
|
959
989
|
: `Generated ${assetList.length} image(s) into project ${input.projectId} ` +
|
|
960
|
-
`for
|
|
990
|
+
`for ${fmtCredits(totalCents)}. ` +
|
|
961
991
|
`Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"` +
|
|
962
992
|
(refEcho ? ` ${refEcho}` : ''),
|
|
963
993
|
images,
|
|
@@ -968,7 +998,7 @@ export const generateImage = {
|
|
|
968
998
|
aspectRatio: input.aspectRatio,
|
|
969
999
|
resolution,
|
|
970
1000
|
cost_cents: totalCents,
|
|
971
|
-
|
|
1001
|
+
cost_credits: totalCents,
|
|
972
1002
|
// Compact refs only — the full rows (prompt/settings/paths) were a
|
|
973
1003
|
// multi-KB leak per generation and everything needed downstream is
|
|
974
1004
|
// the id/code/label.
|
|
@@ -1027,7 +1057,7 @@ export const generateImage = {
|
|
|
1027
1057
|
images.push({ data: buf.toString('base64'), mimeType: mt });
|
|
1028
1058
|
}
|
|
1029
1059
|
return {
|
|
1030
|
-
text: `Generated ${urls.length} image(s) for
|
|
1060
|
+
text: `Generated ${urls.length} image(s) for ${fmtCredits(totalCents)}. ` +
|
|
1031
1061
|
`Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"`,
|
|
1032
1062
|
images,
|
|
1033
1063
|
data: {
|
|
@@ -1037,7 +1067,7 @@ export const generateImage = {
|
|
|
1037
1067
|
aspectRatio: input.aspectRatio,
|
|
1038
1068
|
resolution,
|
|
1039
1069
|
cost_cents: totalCents,
|
|
1040
|
-
|
|
1070
|
+
cost_credits: totalCents,
|
|
1041
1071
|
},
|
|
1042
1072
|
};
|
|
1043
1073
|
},
|
|
@@ -1076,7 +1106,7 @@ export const editImage = {
|
|
|
1076
1106
|
resolution: z.enum(['1k', '2k', '3k', '4k']).optional().describe('3k (1440p class) is gpt-image-2 only; nano-banana-2-lite is 1k only.'),
|
|
1077
1107
|
quality: z.enum(['medium', 'high']).optional().describe('gpt-image-2 only — quality tier (default medium).'),
|
|
1078
1108
|
aspectRatio: z.string().optional(),
|
|
1079
|
-
confirm: z.boolean().optional().describe('Set true to bypass the
|
|
1109
|
+
confirm: z.boolean().optional().describe('Set true to bypass the confirm gate.'),
|
|
1080
1110
|
background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
|
|
1081
1111
|
}),
|
|
1082
1112
|
async run(input, ctx) {
|
|
@@ -1100,16 +1130,16 @@ export const editImage = {
|
|
|
1100
1130
|
const entry = registry.models.find((m) => m.model === costKey);
|
|
1101
1131
|
if (!entry)
|
|
1102
1132
|
throw new Error(`Model not in registry: ${costKey}`);
|
|
1103
|
-
const totalCents = entry
|
|
1104
|
-
if (totalCents >
|
|
1133
|
+
const totalCents = creditCost(entry);
|
|
1134
|
+
if (totalCents > CONFIRM_CREDITS && !input.confirm) {
|
|
1105
1135
|
const sourceRef = await lookupAssetRef(desktop, input.sourceAssetId);
|
|
1106
1136
|
return ok({
|
|
1107
1137
|
requires_confirm: true,
|
|
1108
1138
|
variant: costKey,
|
|
1109
1139
|
estimated_cents: totalCents,
|
|
1110
|
-
|
|
1140
|
+
estimated_credits: totalCents,
|
|
1111
1141
|
source_ref: sourceRef,
|
|
1112
|
-
message: `Cost:
|
|
1142
|
+
message: `Cost: ${fmtCredits(totalCents)} to edit ${sourceRef} with ${editModel} (${costKey}). ` +
|
|
1113
1143
|
`Re-call with confirm=true after the user explicitly OKs the spend. ` +
|
|
1114
1144
|
`When discussing with the user, refer to the source by its code (matches the gallery badge).`,
|
|
1115
1145
|
});
|
|
@@ -1135,7 +1165,7 @@ export const editImage = {
|
|
|
1135
1165
|
projectId: input.projectId,
|
|
1136
1166
|
sourceAssetId: input.sourceAssetId,
|
|
1137
1167
|
cost_cents: totalCents,
|
|
1138
|
-
|
|
1168
|
+
cost_credits: totalCents,
|
|
1139
1169
|
});
|
|
1140
1170
|
}
|
|
1141
1171
|
// Inline the edited result so the LLM sees whether the surgery landed —
|
|
@@ -1153,7 +1183,7 @@ export const editImage = {
|
|
|
1153
1183
|
}
|
|
1154
1184
|
return {
|
|
1155
1185
|
text: `Edited image saved as a new asset in project ${input.projectId} ` +
|
|
1156
|
-
`for
|
|
1186
|
+
`for ${fmtCredits(totalCents)} via ${editModel}. ` +
|
|
1157
1187
|
`Edit: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"`,
|
|
1158
1188
|
images,
|
|
1159
1189
|
data: {
|
|
@@ -1163,7 +1193,7 @@ export const editImage = {
|
|
|
1163
1193
|
sourceAssetId: input.sourceAssetId,
|
|
1164
1194
|
resolution,
|
|
1165
1195
|
cost_cents: totalCents,
|
|
1166
|
-
|
|
1196
|
+
cost_credits: totalCents,
|
|
1167
1197
|
asset: result.asset,
|
|
1168
1198
|
generationId: result.generationId,
|
|
1169
1199
|
},
|
|
@@ -1181,6 +1211,7 @@ export const VIDEO_MODELS = [
|
|
|
1181
1211
|
'veo-3.1-fast',
|
|
1182
1212
|
'veo-3.1-standard',
|
|
1183
1213
|
'seedance-2',
|
|
1214
|
+
'omni-flash',
|
|
1184
1215
|
];
|
|
1185
1216
|
// Model → registry cost-key. Each provider's keys ship with their own
|
|
1186
1217
|
// shape (verified against /api/agent/models):
|
|
@@ -1241,6 +1272,11 @@ export function videoCostKey(input) {
|
|
|
1241
1272
|
}
|
|
1242
1273
|
return `${tier}-${input.duration}s${input.sound === true ? '-audio' : ''}`;
|
|
1243
1274
|
}
|
|
1275
|
+
if (input.model === 'omni-flash') {
|
|
1276
|
+
// Mirrors omniFlashCreditKey() in slate/src/shared/pricing.ts — flat 720p
|
|
1277
|
+
// rate, audio native + included, no resolution/audio key dimension.
|
|
1278
|
+
return `omni-flash-${input.duration}s`;
|
|
1279
|
+
}
|
|
1244
1280
|
throw new Error(`Unknown video model: ${input.model}`);
|
|
1245
1281
|
}
|
|
1246
1282
|
// Kling O3 video-to-video edit cost key — mirrors klingEditCreditKey() in
|
|
@@ -1251,6 +1287,12 @@ export function klingEditCostKey(model, duration) {
|
|
|
1251
1287
|
const tier = model === 'kling-v3.0-omni-pro-edit' ? 'kling-v3-omni-pro-edit' : 'kling-v3-omni-edit';
|
|
1252
1288
|
return `${tier}-${duration}s`;
|
|
1253
1289
|
}
|
|
1290
|
+
// Omni Flash video-edit cost key — mirrors omniFlashCreditKey() in
|
|
1291
|
+
// slate/src/shared/pricing.ts (must byte-match; checked by the slates-api
|
|
1292
|
+
// pricing-consistency script). Duration is the CEILED source-clip length.
|
|
1293
|
+
export function omniFlashEditCostKey(duration) {
|
|
1294
|
+
return `omni-flash-edit-${duration}s`;
|
|
1295
|
+
}
|
|
1254
1296
|
/**
|
|
1255
1297
|
* Forgiving model-id resolver. Agents routinely paste registry COST keys
|
|
1256
1298
|
* ("kling-v3-standard-8s", "seedance-2-1080p-8s") into the `model` param —
|
|
@@ -1298,6 +1340,9 @@ function resolveVideoModel(raw) {
|
|
|
1298
1340
|
seedance: 'seedance-2',
|
|
1299
1341
|
'veo-3.1': 'veo-3.1-fast',
|
|
1300
1342
|
'veo-3': 'veo-3.1-fast',
|
|
1343
|
+
'gemini-omni-flash': 'omni-flash',
|
|
1344
|
+
'gemini-omni-flash-preview': 'omni-flash',
|
|
1345
|
+
'omni-flash-preview': 'omni-flash',
|
|
1301
1346
|
};
|
|
1302
1347
|
if (aliases[s]) {
|
|
1303
1348
|
out.model = aliases[s];
|
|
@@ -1316,6 +1361,8 @@ function promptingSkillFor(model) {
|
|
|
1316
1361
|
return 'slates-prompting-veo-3';
|
|
1317
1362
|
if (model.startsWith('seedance'))
|
|
1318
1363
|
return 'slates-prompting-seedance';
|
|
1364
|
+
if (model.startsWith('omni-flash'))
|
|
1365
|
+
return 'slates-prompting-omni-flash';
|
|
1319
1366
|
return 'slates-cost-discipline';
|
|
1320
1367
|
}
|
|
1321
1368
|
export const generateVideo = {
|
|
@@ -1323,14 +1370,14 @@ export const generateVideo = {
|
|
|
1323
1370
|
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").',
|
|
1324
1371
|
input: z.object({
|
|
1325
1372
|
prompt: z.string().min(1).max(4000),
|
|
1326
|
-
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 id — duration 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.'),
|
|
1373
|
+
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 | omni-flash. Pass the BASE id — duration 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, omni-flash = cheap 720p tier with audio included (3-10s, 16:9/9:16; t2v, single-start-frame i2v, or up to 7 reference images; no last frame / video / audio refs). All are VIDEO-only. For per-call cost, call slates_estimate_generation_cost — never quote prices from memory.'),
|
|
1327
1374
|
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.'),
|
|
1328
1375
|
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.'),
|
|
1329
|
-
duration: z.number().int().min(
|
|
1376
|
+
duration: z.number().int().min(3).max(15).optional().describe('Seconds. Kling: 5-15. Veo: 4, 6, or 8 only (4K only at 8s). Seedance: 4-15. Omni Flash: 3-10. Default 5 if omitted but always be explicit (cost scales linearly).'),
|
|
1330
1377
|
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).'),
|
|
1331
1378
|
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.'),
|
|
1332
1379
|
lastFrameAssetId: z.string().optional().describe('Ending frame (UUID or badge code). Veo and Seedance only. Pairs with firstFrameAssetId for guided transitions.'),
|
|
1333
|
-
ingredientAssetIds: z.array(z.string()).max(9).optional().describe('Visual reference / ingredient assets (UUIDs or badge codes) for Kling Omni or
|
|
1380
|
+
ingredientAssetIds: z.array(z.string()).max(9).optional().describe('Visual reference / ingredient assets (UUIDs or badge codes) for Kling Omni, Seedance, or Omni Flash. Up to 9 (Seedance), 4 (Kling), or 7 (Omni Flash, combined across all ref params).'),
|
|
1334
1381
|
characterAssetIds: z.array(z.string()).optional().describe('Character sheet assets (UUIDs or badge codes) — keeps a character consistent across the shot.'),
|
|
1335
1382
|
environmentAssetIds: z.array(z.string()).optional().describe('Environment grid assets (UUIDs or badge codes) — keeps a location/setting consistent across the shot.'),
|
|
1336
1383
|
styleAssetIds: z.array(z.string()).optional().describe('Style reference assets (UUIDs or badge codes) — locks the visual style of the shot.'),
|
|
@@ -1345,7 +1392,7 @@ export const generateVideo = {
|
|
|
1345
1392
|
realFaceConsent: z.boolean().optional().describe('MANDATORY with seedanceRealFace: set true ONLY after the user has explicitly confirmed they hold the rights/consent to this person\'s likeness and it doesn\'t impersonate or misrepresent them. The generation is refused without it. Public figures/celebrities fail on every route.'),
|
|
1346
1393
|
negativePrompt: z.string().optional(),
|
|
1347
1394
|
background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
|
|
1348
|
-
confirm: z.boolean().optional().describe('Set true after explicit user OK to bypass the
|
|
1395
|
+
confirm: z.boolean().optional().describe('Set true after explicit user OK to bypass the confirm gate (which fires for almost every video gen since they\'re expensive).'),
|
|
1349
1396
|
}),
|
|
1350
1397
|
async run(input, ctx) {
|
|
1351
1398
|
// Resolve the model FIRST — forgiving normalization (cost keys, alias
|
|
@@ -1375,7 +1422,7 @@ export const generateVideo = {
|
|
|
1375
1422
|
return ok({
|
|
1376
1423
|
requires_clarification: true,
|
|
1377
1424
|
missing: ['projectId'],
|
|
1378
|
-
message: 'projectId is required for video generation. Use slates_list_projects to find one or slates_create_project to make a new one. Video gens cost
|
|
1425
|
+
message: 'projectId is required for video generation. Use slates_list_projects to find one or slates_create_project to make a new one. Video gens cost tens to a few hundred credits per call — they need to land in a project so the user sees the progress card and the result.',
|
|
1379
1426
|
});
|
|
1380
1427
|
}
|
|
1381
1428
|
if (!input.aspectRatio || !input.duration) {
|
|
@@ -1389,10 +1436,41 @@ export const generateVideo = {
|
|
|
1389
1436
|
missing,
|
|
1390
1437
|
message: `Missing required field(s): ${missing.join(', ')}. ` +
|
|
1391
1438
|
`Read the slates-cost-discipline + ${promptingSkillFor(input.model)} skills, ` +
|
|
1392
|
-
`or ask the user. Veo locks to 16:9. Kling/Seedance support 1:1 16:9 9:16 4:3 3:4 21:9. ` +
|
|
1393
|
-
`Duration: Kling 5-15s, Veo 4/6/8s (4K only at 8s), Seedance 4-15s. Cost scales linearly with duration.`,
|
|
1439
|
+
`or ask the user. Veo locks to 16:9. Kling/Seedance support 1:1 16:9 9:16 4:3 3:4 21:9. Omni Flash: 16:9/9:16 only. ` +
|
|
1440
|
+
`Duration: Kling 5-15s, Veo 4/6/8s (4K only at 8s), Seedance 4-15s, Omni Flash 3-10s. Cost scales linearly with duration.`,
|
|
1394
1441
|
});
|
|
1395
1442
|
}
|
|
1443
|
+
// Omni Flash: 3-10s, 720p only — t2v, single-start-frame i2v, or ref2v
|
|
1444
|
+
// with up to 7 reference IMAGES. No last frame, no video/audio refs.
|
|
1445
|
+
// Validate up front so the agent gets an actionable message instead of
|
|
1446
|
+
// a registry throw.
|
|
1447
|
+
if (input.model === 'omni-flash') {
|
|
1448
|
+
if (input.duration < 3 || input.duration > 10) {
|
|
1449
|
+
return ok({
|
|
1450
|
+
requires_clarification: true,
|
|
1451
|
+
missing: ['duration'],
|
|
1452
|
+
message: `Omni Flash supports 3-10 seconds (you passed ${input.duration}s). Pick a duration in that range.`,
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
if (input.lastFrameAssetId || input.videoReferenceAssetId || input.audioReferenceAssetId) {
|
|
1456
|
+
return ok({
|
|
1457
|
+
requires_clarification: true,
|
|
1458
|
+
missing: [],
|
|
1459
|
+
message: 'Omni Flash takes a prompt, an optional start frame, and up to 7 reference IMAGES — last frames and video/audio references are not supported. Drop those, or switch to seedance-2 (video/audio refs, last frame) or veo (last frame).',
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
const refCount = (input.ingredientAssetIds?.length ?? 0) +
|
|
1463
|
+
(input.characterAssetIds?.length ?? 0) +
|
|
1464
|
+
(input.environmentAssetIds?.length ?? 0) +
|
|
1465
|
+
(input.styleAssetIds?.length ?? 0);
|
|
1466
|
+
if (refCount > 7) {
|
|
1467
|
+
return ok({
|
|
1468
|
+
requires_clarification: true,
|
|
1469
|
+
missing: [],
|
|
1470
|
+
message: `Omni Flash takes at most 7 reference images combined (you passed ${refCount}). Trim the list.`,
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1396
1474
|
// Veo exists only at discrete durations 4/6/8s, and 4K only at 8s.
|
|
1397
1475
|
// Validate up front so the agent gets an actionable message instead of
|
|
1398
1476
|
// a generic "Model variant not in registry" throw from the cost lookup.
|
|
@@ -1480,9 +1558,9 @@ export const generateVideo = {
|
|
|
1480
1558
|
const entry = registry.models.find((m) => m.model === costKey);
|
|
1481
1559
|
if (!entry) {
|
|
1482
1560
|
throw new Error(`Model variant not in registry: ${costKey}. ` +
|
|
1483
|
-
`Available video models: ${registry.models.filter((m) => m.model.startsWith('kling') || m.model.startsWith('veo') || m.model.startsWith('seedance')).map((m) => m.model).slice(0, 20).join(', ')}`);
|
|
1561
|
+
`Available video models: ${registry.models.filter((m) => m.model.startsWith('kling') || m.model.startsWith('veo') || m.model.startsWith('seedance') || m.model.startsWith('omni-flash')).map((m) => m.model).slice(0, 20).join(', ')}`);
|
|
1484
1562
|
}
|
|
1485
|
-
const totalCents = entry
|
|
1563
|
+
const totalCents = creditCost(entry);
|
|
1486
1564
|
// Pre-flight confirm gate. Fires when:
|
|
1487
1565
|
// (a) cost > $0.50 (the cost gate), OR
|
|
1488
1566
|
// (b) any reference assets are involved (the look-first gate)
|
|
@@ -1511,7 +1589,7 @@ export const generateVideo = {
|
|
|
1511
1589
|
referenceRefs.push({ id, type: 'image', role: 'style' });
|
|
1512
1590
|
}
|
|
1513
1591
|
const hasReferences = referenceRefs.length > 0;
|
|
1514
|
-
if ((totalCents >
|
|
1592
|
+
if ((totalCents > CONFIRM_CREDITS || hasReferences) && !input.confirm) {
|
|
1515
1593
|
const previews = hasReferences ? await previewAssets(ctx, referenceRefs) : [];
|
|
1516
1594
|
const refLines = previews.map((p) => ` - ${p.role}: ${p.ref}`).join('\n');
|
|
1517
1595
|
const refSummary = hasReferences
|
|
@@ -1521,7 +1599,7 @@ export const generateVideo = {
|
|
|
1521
1599
|
const refImages = previews.flatMap((p) => p.images);
|
|
1522
1600
|
return {
|
|
1523
1601
|
text: `Pre-flight for ${input.duration}s ${input.model} (${costKey}): ` +
|
|
1524
|
-
|
|
1602
|
+
`${fmtCredits(totalCents)}.` +
|
|
1525
1603
|
refSummary +
|
|
1526
1604
|
`\n\nWhen ready, re-call slates_generate_video with confirm=true and the (possibly revised) prompt.`,
|
|
1527
1605
|
images: refImages,
|
|
@@ -1530,7 +1608,7 @@ export const generateVideo = {
|
|
|
1530
1608
|
model: input.model,
|
|
1531
1609
|
variant: costKey,
|
|
1532
1610
|
estimated_cents: totalCents,
|
|
1533
|
-
|
|
1611
|
+
estimated_credits: totalCents,
|
|
1534
1612
|
references: previews.map((p) => ({
|
|
1535
1613
|
role: p.role,
|
|
1536
1614
|
ref: p.ref,
|
|
@@ -1581,7 +1659,7 @@ export const generateVideo = {
|
|
|
1581
1659
|
variant: costKey,
|
|
1582
1660
|
projectId: input.projectId,
|
|
1583
1661
|
cost_cents: totalCents,
|
|
1584
|
-
|
|
1662
|
+
cost_credits: totalCents,
|
|
1585
1663
|
references: refInputs.map(({ ref, role }) => ({
|
|
1586
1664
|
role,
|
|
1587
1665
|
...(resolvedRefs.get(ref) ?? { id: ref, code: null, label: null }),
|
|
@@ -1590,7 +1668,7 @@ export const generateVideo = {
|
|
|
1590
1668
|
}
|
|
1591
1669
|
return {
|
|
1592
1670
|
text: `Generated ${input.duration}s ${input.model} video into project ${input.projectId} ` +
|
|
1593
|
-
`for
|
|
1671
|
+
`for ${fmtCredits(totalCents)}. ` +
|
|
1594
1672
|
`Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"` +
|
|
1595
1673
|
(refEcho ? ` ${refEcho}` : ''),
|
|
1596
1674
|
data: {
|
|
@@ -1600,7 +1678,7 @@ export const generateVideo = {
|
|
|
1600
1678
|
aspectRatio: input.aspectRatio,
|
|
1601
1679
|
duration: input.duration,
|
|
1602
1680
|
cost_cents: totalCents,
|
|
1603
|
-
|
|
1681
|
+
cost_credits: totalCents,
|
|
1604
1682
|
asset: result.asset,
|
|
1605
1683
|
generationId: result.generationId,
|
|
1606
1684
|
},
|
|
@@ -1621,8 +1699,8 @@ export const generateLipSync = {
|
|
|
1621
1699
|
ttsLanguage: z.enum(['EN', 'ZH', 'JA', 'KO', 'ES']).optional().describe('Kling engine only — TTS language. Default EN.'),
|
|
1622
1700
|
ttsSpeed: z.number().min(0.5).max(2).optional().describe('Kling engine only — TTS speech rate. Default 1.0. Range 0.5-2.0.'),
|
|
1623
1701
|
audioFilePath: z.string().optional().describe('Required when audioMethod=upload. Absolute path to the audio file on the user\'s machine (mp3, wav, m4a).'),
|
|
1624
|
-
avatarModel: z.enum(['avatar-standard', 'avatar-pro']).optional().describe('Kling engine, image-source only. avatar-standard (
|
|
1625
|
-
klingProvider: z.enum(['fal', 'kling']).optional().describe('Kling engine only — provider routing.
|
|
1702
|
+
avatarModel: z.enum(['avatar-standard', 'avatar-pro']).optional().describe('Kling engine, image-source only. avatar-standard (~14 credits/5s) for general use. avatar-pro (~29 credits/5s) for sharper face fidelity.'),
|
|
1703
|
+
klingProvider: z.enum(['fal', 'kling']).optional().describe('Kling engine only — provider routing. Leave unset: all agent generations bill Slates credits (BYOK is retired).'),
|
|
1626
1704
|
engine: z.enum(['kling', 'seedance-2']).optional().describe('Default kling (cheap utility). seedance-2 = premium single-pass: natural speech generated in the video, voice cloned from a video source, audio included. Credits only.'),
|
|
1627
1705
|
videoResolution: z.enum(['480p', '720p', '1080p', '4k']).optional().describe('Seedance engine only. Default 1080p.'),
|
|
1628
1706
|
aspectRatio: z.string().optional().describe('Seedance engine only. Default 16:9.'),
|
|
@@ -1632,7 +1710,7 @@ export const generateLipSync = {
|
|
|
1632
1710
|
sourceSeconds: z.number().optional().describe('Seedance engine + sourceType=video: the source clip\'s duration in seconds (from the asset listing). Feeds the vref cost key (input+output billing).'),
|
|
1633
1711
|
audioSeconds: z.number().optional().describe('Seedance engine + audioMethod=upload: the audio file\'s duration in seconds — sets the output length (4-15s).'),
|
|
1634
1712
|
background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
|
|
1635
|
-
confirm: z.boolean().optional().describe('Set true to bypass the
|
|
1713
|
+
confirm: z.boolean().optional().describe('Set true to bypass the confirm gate. Required for avatar-pro.'),
|
|
1636
1714
|
}),
|
|
1637
1715
|
async run(input, ctx) {
|
|
1638
1716
|
if (input.audioMethod === 'tts' && !input.ttsText) {
|
|
@@ -1696,12 +1774,12 @@ export const generateLipSync = {
|
|
|
1696
1774
|
const entry = registry.models.find((m) => m.model === costKey);
|
|
1697
1775
|
if (!entry)
|
|
1698
1776
|
throw new Error(`Model variant not in registry: ${costKey}`);
|
|
1699
|
-
const totalCents = entry
|
|
1777
|
+
const totalCents = creditCost(entry);
|
|
1700
1778
|
// Cost confirm gate. Lip-sync is mechanical — the model re-syncs the
|
|
1701
1779
|
// user-chosen source to the user-chosen audio. The agent doesn't
|
|
1702
1780
|
// write a prompt that depends on what the source looks like, so we
|
|
1703
1781
|
// skip the inline preview and just announce the source code in text.
|
|
1704
|
-
if (totalCents >
|
|
1782
|
+
if (totalCents > CONFIRM_CREDITS && !input.confirm) {
|
|
1705
1783
|
const sourceRef = await lookupAssetRef(ctx.desktop(), input.sourceAssetId);
|
|
1706
1784
|
const audioPreview = input.audioMethod === 'tts'
|
|
1707
1785
|
? `Audio: TTS — "${(input.ttsText ?? '').slice(0, 120)}"`
|
|
@@ -1710,9 +1788,9 @@ export const generateLipSync = {
|
|
|
1710
1788
|
requires_confirm: true,
|
|
1711
1789
|
variant: costKey,
|
|
1712
1790
|
estimated_cents: totalCents,
|
|
1713
|
-
|
|
1791
|
+
estimated_credits: totalCents,
|
|
1714
1792
|
source_ref: sourceRef,
|
|
1715
|
-
message: `Cost:
|
|
1793
|
+
message: `Cost: ${fmtCredits(totalCents)} for ${isSeedance ? `${seedanceDuration}s Seedance` : '5s'} lip-sync (${costKey}). ` +
|
|
1716
1794
|
`Source: ${sourceRef}. ${audioPreview}. ` +
|
|
1717
1795
|
`Re-call with confirm=true after the user explicitly OKs the spend. ` +
|
|
1718
1796
|
`When discussing with the user, refer to the source by its code (matches the gallery badge).`,
|
|
@@ -1761,12 +1839,12 @@ export const generateLipSync = {
|
|
|
1761
1839
|
projectId: input.projectId,
|
|
1762
1840
|
sourceAssetId: input.sourceAssetId,
|
|
1763
1841
|
cost_cents: totalCents,
|
|
1764
|
-
|
|
1842
|
+
cost_credits: totalCents,
|
|
1765
1843
|
});
|
|
1766
1844
|
}
|
|
1767
1845
|
return {
|
|
1768
1846
|
text: `Generated ${isSeedance ? `${seedanceDuration}s` : '5s'} lip-sync (${costKey}) into project ${input.projectId} ` +
|
|
1769
|
-
`for
|
|
1847
|
+
`for ${fmtCredits(totalCents)}. ` +
|
|
1770
1848
|
(input.audioMethod === 'tts'
|
|
1771
1849
|
? `Spoken: "${(input.ttsText ?? '').slice(0, 60)}${(input.ttsText ?? '').length > 60 ? '...' : ''}"`
|
|
1772
1850
|
: `Audio: ${input.audioFilePath}`),
|
|
@@ -1776,7 +1854,7 @@ export const generateLipSync = {
|
|
|
1776
1854
|
sourceType: input.sourceType,
|
|
1777
1855
|
sourceAssetId: input.sourceAssetId,
|
|
1778
1856
|
cost_cents: totalCents,
|
|
1779
|
-
|
|
1857
|
+
cost_credits: totalCents,
|
|
1780
1858
|
asset: result.asset,
|
|
1781
1859
|
generationId: result.generationId,
|
|
1782
1860
|
},
|
|
@@ -1791,7 +1869,7 @@ export const generateMotionTransfer = {
|
|
|
1791
1869
|
projectId: z.string().uuid().describe('Slates project. Both source and target assets must live here.'),
|
|
1792
1870
|
sourceVideoAssetId: z.string().uuid().describe('Asset id of the reference video — its motion will be retargeted onto the target image. Must already exist in the project. Seedance engine: 2-15s clips only.'),
|
|
1793
1871
|
targetImageAssetId: z.string().uuid().describe('Asset id of the target image (the character that will perform the motion). Must already exist in the project.'),
|
|
1794
|
-
motionModel: z.enum(['kling-mc-std', 'kling-mc-pro', 'seedance-2']).optional().describe('kling-mc-std (
|
|
1872
|
+
motionModel: z.enum(['kling-mc-std', 'kling-mc-pro', 'seedance-2']).optional().describe('kling-mc-std (~32 credits) general motion; kling-mc-pro (~42 credits) cleaner anatomy — default. seedance-2 = premium single-pass lane (prompt-driven, native audio, input+output-second billing) — pick when motion fidelity or audio matters.'),
|
|
1795
1873
|
characterOrientation: z.enum(['video', 'image']).optional().describe('Kling only. "video" = use the source video\'s framing. "image" = use the target image\'s framing. Default video.'),
|
|
1796
1874
|
prompt: z.string().optional().describe('Kling: optional refinement. Seedance: THE driver — describe what the character does with the motion from the clip (ordinal references: "the character from image 1 performs the motion from video 1"). A sensible default recipe is used if omitted. Read slates-prompting-motion-transfer.'),
|
|
1797
1875
|
klingProvider: z.enum(['fal', 'kling']).optional().describe('Kling engine only — provider routing. "fal" (default) uses Slates credits.'),
|
|
@@ -1803,7 +1881,7 @@ export const generateMotionTransfer = {
|
|
|
1803
1881
|
realFaceConsent: z.boolean().optional().describe('MANDATORY with seedanceRealFace — set true only after the user explicitly confirms they hold rights/consent to the likeness.'),
|
|
1804
1882
|
sourceVideoSeconds: z.number().optional().describe('Seedance engine: the driving clip\'s duration in seconds (from the asset listing, 2-15s). Feeds the vref cost key (input+output billing).'),
|
|
1805
1883
|
background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
|
|
1806
|
-
confirm: z.boolean().optional().describe('Set true to bypass the
|
|
1884
|
+
confirm: z.boolean().optional().describe('Set true to bypass the confirm gate. Required — both tiers exceed.'),
|
|
1807
1885
|
}),
|
|
1808
1886
|
async run(input, ctx) {
|
|
1809
1887
|
const motionModel = input.motionModel ?? 'kling-mc-pro';
|
|
@@ -1843,12 +1921,12 @@ export const generateMotionTransfer = {
|
|
|
1843
1921
|
const entry = registry.models.find((m) => m.model === costKey);
|
|
1844
1922
|
if (!entry)
|
|
1845
1923
|
throw new Error(`Model variant not in registry: ${costKey}`);
|
|
1846
|
-
const totalCents = entry
|
|
1924
|
+
const totalCents = creditCost(entry);
|
|
1847
1925
|
// Cost confirm gate. Motion transfer is mechanical — the model
|
|
1848
1926
|
// applies source motion to target image deterministically. We don't
|
|
1849
1927
|
// burn tokens previewing assets the user already chose; codes in the
|
|
1850
1928
|
// text are enough to keep the chat unambiguous.
|
|
1851
|
-
if (totalCents >
|
|
1929
|
+
if (totalCents > CONFIRM_CREDITS && !input.confirm) {
|
|
1852
1930
|
const desktop = ctx.desktop();
|
|
1853
1931
|
const [source, target] = await Promise.all([
|
|
1854
1932
|
lookupAssetRef(desktop, input.sourceVideoAssetId),
|
|
@@ -1858,12 +1936,12 @@ export const generateMotionTransfer = {
|
|
|
1858
1936
|
requires_confirm: true,
|
|
1859
1937
|
variant: costKey,
|
|
1860
1938
|
estimated_cents: totalCents,
|
|
1861
|
-
|
|
1939
|
+
estimated_credits: totalCents,
|
|
1862
1940
|
source_ref: source,
|
|
1863
1941
|
target_ref: target,
|
|
1864
|
-
message: `Cost:
|
|
1942
|
+
message: `Cost: ${fmtCredits(totalCents)} for ${isSeedance ? `${seedanceDuration}s Seedance motion transfer` : `5s ${motionModel}`} (${costKey}). ` +
|
|
1865
1943
|
`Transferring motion from ${source} onto ${target}. ` +
|
|
1866
|
-
`Re-call with confirm=true after the user explicitly OKs the spend${isSeedance ? '' : ', or pick kling-mc-std to save
|
|
1944
|
+
`Re-call with confirm=true after the user explicitly OKs the spend${isSeedance ? '' : ', or pick kling-mc-std to save ~10 credits'}. ` +
|
|
1867
1945
|
`When discussing with the user, refer to the assets by those codes — they'll match the gallery badges.`,
|
|
1868
1946
|
});
|
|
1869
1947
|
}
|
|
@@ -1908,12 +1986,12 @@ export const generateMotionTransfer = {
|
|
|
1908
1986
|
sourceVideoAssetId: input.sourceVideoAssetId,
|
|
1909
1987
|
targetImageAssetId: input.targetImageAssetId,
|
|
1910
1988
|
cost_cents: totalCents,
|
|
1911
|
-
|
|
1989
|
+
cost_credits: totalCents,
|
|
1912
1990
|
});
|
|
1913
1991
|
}
|
|
1914
1992
|
return {
|
|
1915
1993
|
text: `Generated ${isSeedance ? `${seedanceDuration}s` : '5s'} motion transfer (${motionModel}) into project ${input.projectId} ` +
|
|
1916
|
-
`for
|
|
1994
|
+
`for ${fmtCredits(totalCents)}.` +
|
|
1917
1995
|
(input.prompt ? ` Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"` : ''),
|
|
1918
1996
|
data: {
|
|
1919
1997
|
variant: costKey,
|
|
@@ -1922,7 +2000,7 @@ export const generateMotionTransfer = {
|
|
|
1922
2000
|
sourceVideoAssetId: input.sourceVideoAssetId,
|
|
1923
2001
|
targetImageAssetId: input.targetImageAssetId,
|
|
1924
2002
|
cost_cents: totalCents,
|
|
1925
|
-
|
|
2003
|
+
cost_credits: totalCents,
|
|
1926
2004
|
asset: result.asset,
|
|
1927
2005
|
generationId: result.generationId,
|
|
1928
2006
|
},
|
|
@@ -1932,15 +2010,15 @@ export const generateMotionTransfer = {
|
|
|
1932
2010
|
// ── Edit video (Kling O3 video-to-video) ────────────────────────
|
|
1933
2011
|
export const editVideo = {
|
|
1934
2012
|
id: 'slates_edit_video',
|
|
1935
|
-
description: 'Edit an EXISTING video clip with one instruction
|
|
2013
|
+
description: 'Edit an EXISTING video clip with one instruction — character swap, environment change, style transfer — in one pass, no masking. Original motion, camera, and audio are preserved; only what the prompt names changes. Use when a clip is ~90% right (fix it, don\'t re-roll it) or to AI-edit the user\'s own footage. Engines: Kling O3 edit (default; 3–15s clips, 720–3840px, subject/style refs via elements) or omni-flash-edit (Gemini Omni Flash; 3–10s clips, 720p output, PROMPT-ONLY — no refs, cheapest seat). Cost = per second of OUTPUT (≈ clip length, rounded UP to the next second): omni-flash-edit ≈ 19¢/s ≈ kling-v3.0-omni-edit ≈ 19¢/s, kling-v3.0-omni-pro-edit ≈ 25¢/s. Subjects to swap IN go as characterAssetIds (frontal + angle images become Kling elements — Kling models only); style refs as styleAssetIds; max 4 combined. The edited clip saves as a NEW asset linked to its parent (chain edits freely). Routing: Kling edit is the default edit tool (element lock + audio intact); omni-flash-edit for cheap prompt-only footage-synced swaps; prefer Seedance edit/relocate only for style-transfer-heavy jobs — see slates-model-selection. Prompting: slates-prompting-kling-v3 §Edit / slates-prompting-omni-flash.',
|
|
1936
2014
|
input: z.object({
|
|
1937
2015
|
projectId: z.string().uuid().describe('Project the source clip lives in.'),
|
|
1938
|
-
sourceVideoAssetId: z.string().describe('The VIDEO asset to edit — UUID or badge code ("VID-V3", bare "V3"); codes resolve against the project at call time. 3–15s clips
|
|
1939
|
-
prompt: z.string().min(1).max(2500).describe('The change, not the whole scene — e.g. "replace the man with @marcus", "make it a rainy night", "turn the street into a neon Tokyo alley". Mention subjects with @name; the transport compiles them to Kling\'s @ElementN notation.'),
|
|
1940
|
-
model: z.enum(['kling-v3.0-omni-edit', 'kling-v3.0-omni-pro-edit']).optional().describe('Default kling-v3.0-omni-edit. Pro (~25¢/s vs ~19¢/s) only for hero shots where fidelity matters.'),
|
|
1941
|
-
characterAssetIds: z.array(z.string()).max(4).optional().describe('Subject/element image assets to swap IN (UUIDs or badge codes). Each becomes a Kling element (@ElementN).'),
|
|
1942
|
-
styleAssetIds: z.array(z.string()).max(4).optional().describe('Style/appearance reference images (@ImageN). Max 4 combined with characterAssetIds.'),
|
|
1943
|
-
keepAudio: z.boolean().optional().describe('Preserve the original audio track (default true).'),
|
|
2016
|
+
sourceVideoAssetId: z.string().describe('The VIDEO asset to edit — UUID or badge code ("VID-V3", bare "V3"); codes resolve against the project at call time. Kling: 3–15s clips; omni-flash-edit: 3–10s.'),
|
|
2017
|
+
prompt: z.string().min(1).max(2500).describe('The change, not the whole scene — e.g. "replace the man with @marcus", "make it a rainy night", "turn the street into a neon Tokyo alley". Mention subjects with @name; the transport compiles them to Kling\'s @ElementN notation (Kling models). For omni-flash-edit keep it simple and add "Keep everything else the same."'),
|
|
2018
|
+
model: z.enum(['kling-v3.0-omni-edit', 'kling-v3.0-omni-pro-edit', 'omni-flash-edit']).optional().describe('Default kling-v3.0-omni-edit. Pro (~25¢/s vs ~19¢/s) only for hero shots where fidelity matters. omni-flash-edit (~19¢/s, 720p, 3–10s) for prompt-only edits — it takes NO character/style refs.'),
|
|
2019
|
+
characterAssetIds: z.array(z.string()).max(4).optional().describe('Subject/element image assets to swap IN (UUIDs or badge codes). Each becomes a Kling element (@ElementN). KLING MODELS ONLY — rejected on omni-flash-edit.'),
|
|
2020
|
+
styleAssetIds: z.array(z.string()).max(4).optional().describe('Style/appearance reference images (@ImageN). Max 4 combined with characterAssetIds. KLING MODELS ONLY — rejected on omni-flash-edit.'),
|
|
2021
|
+
keepAudio: z.boolean().optional().describe('Preserve the original audio track (default true; Kling models only — omni-flash-edit output carries its own audio).'),
|
|
1944
2022
|
background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
|
|
1945
2023
|
confirm: z.boolean().optional().describe('Set true to bypass the cost confirm gate after the user OKs the spend.'),
|
|
1946
2024
|
}),
|
|
@@ -1951,6 +2029,12 @@ export const editVideo = {
|
|
|
1951
2029
|
await desktop.requireCapability('background-generation', 'background generation');
|
|
1952
2030
|
}
|
|
1953
2031
|
const model = input.model ?? 'kling-v3.0-omni-edit';
|
|
2032
|
+
const isOmniFlashEdit = model === 'omni-flash-edit';
|
|
2033
|
+
// Kling edit: 3–15s source clips; Omni Flash edit: 3–10s.
|
|
2034
|
+
const maxClipSeconds = isOmniFlashEdit ? 10 : 15;
|
|
2035
|
+
if (isOmniFlashEdit && ((input.characterAssetIds?.length ?? 0) > 0 || (input.styleAssetIds?.length ?? 0) > 0)) {
|
|
2036
|
+
throw new Error('omni-flash-edit is prompt-only — it takes no character/style reference images. Drop the refs, or switch to kling-v3.0-omni-edit which supports elements.');
|
|
2037
|
+
}
|
|
1954
2038
|
// Resolve refs (UUIDs or badge codes) against the project AT CALL TIME.
|
|
1955
2039
|
const refInputs = [
|
|
1956
2040
|
{ ref: input.sourceVideoAssetId, role: 'source clip' },
|
|
@@ -1978,27 +2062,28 @@ export const editVideo = {
|
|
|
1978
2062
|
if (!Number.isFinite(clipSeconds) || clipSeconds <= 0) {
|
|
1979
2063
|
throw new Error('Source clip has no recorded duration — cannot quote the edit. Re-import the clip or pick another.');
|
|
1980
2064
|
}
|
|
1981
|
-
if (clipSeconds >
|
|
1982
|
-
throw new Error(`Source clip is ${clipSeconds.toFixed(1)}s —
|
|
2065
|
+
if (clipSeconds > maxClipSeconds + 0.05 || clipSeconds < 2.95) {
|
|
2066
|
+
throw new Error(`Source clip is ${clipSeconds.toFixed(1)}s — ${model} accepts 3–${maxClipSeconds}s. ` +
|
|
2067
|
+
`Trim it first with slates_trim_video (e.g. inSec 0, outSec ${maxClipSeconds}), then edit the trimmed clip.`);
|
|
1983
2068
|
}
|
|
1984
|
-
const billedSeconds = Math.min(
|
|
1985
|
-
const costKey = klingEditCostKey(model, billedSeconds);
|
|
2069
|
+
const billedSeconds = Math.min(maxClipSeconds, Math.max(3, Math.ceil(clipSeconds - 0.05)));
|
|
2070
|
+
const costKey = isOmniFlashEdit ? omniFlashEditCostKey(billedSeconds) : klingEditCostKey(model, billedSeconds);
|
|
1986
2071
|
const cloud = ctx.cloud();
|
|
1987
2072
|
const registry = await cloud.get('/api/agent/models');
|
|
1988
2073
|
const entry = registry.models.find((m) => m.model === costKey);
|
|
1989
2074
|
if (!entry)
|
|
1990
2075
|
throw new Error(`Model variant not in registry: ${costKey}`);
|
|
1991
|
-
const totalCents = entry
|
|
2076
|
+
const totalCents = creditCost(entry);
|
|
1992
2077
|
// Confirm gate — look-first: preview the source clip + refs so the LLM
|
|
1993
2078
|
// sees what it's editing before committing spend (mirrors generateVideo).
|
|
1994
|
-
if ((totalCents >
|
|
2079
|
+
if ((totalCents > CONFIRM_CREDITS || refInputs.length > 1) && !input.confirm) {
|
|
1995
2080
|
const previews = await previewAssets(ctx, [
|
|
1996
2081
|
{ id: sourceId, type: 'video', role: 'source clip' },
|
|
1997
2082
|
...characterAssetIds.map((id) => ({ id, type: 'image', role: 'subject element' })),
|
|
1998
2083
|
...styleAssetIds.map((id) => ({ id, type: 'image', role: 'style' })),
|
|
1999
2084
|
]);
|
|
2000
2085
|
return {
|
|
2001
|
-
text: `Cost:
|
|
2086
|
+
text: `Cost: ${fmtCredits(totalCents)} to edit a ${billedSeconds}s clip with ${model} (${costKey}). ` +
|
|
2002
2087
|
`${refEcho} Re-call with confirm=true after the user explicitly OKs the spend.`,
|
|
2003
2088
|
images: previews.flatMap((p) => p.images),
|
|
2004
2089
|
data: {
|
|
@@ -2008,7 +2093,7 @@ export const editVideo = {
|
|
|
2008
2093
|
billed_seconds: billedSeconds,
|
|
2009
2094
|
clip_seconds: clipSeconds,
|
|
2010
2095
|
estimated_cents: totalCents,
|
|
2011
|
-
|
|
2096
|
+
estimated_credits: totalCents,
|
|
2012
2097
|
references: previews.map((p) => ({ ref: p.ref, role: p.role, ...p.meta })),
|
|
2013
2098
|
},
|
|
2014
2099
|
};
|
|
@@ -2033,12 +2118,12 @@ export const editVideo = {
|
|
|
2033
2118
|
projectId: input.projectId,
|
|
2034
2119
|
sourceVideoAssetId: sourceId,
|
|
2035
2120
|
cost_cents: totalCents,
|
|
2036
|
-
|
|
2121
|
+
cost_credits: totalCents,
|
|
2037
2122
|
}, refEcho);
|
|
2038
2123
|
}
|
|
2039
2124
|
return {
|
|
2040
2125
|
text: `Edited ${billedSeconds}s clip saved as a new asset in project ${input.projectId} ` +
|
|
2041
|
-
`for
|
|
2126
|
+
`for ${fmtCredits(totalCents)} via ${model}. ` +
|
|
2042
2127
|
`Edit: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"` +
|
|
2043
2128
|
(refEcho ? ` ${refEcho}` : ''),
|
|
2044
2129
|
data: {
|
|
@@ -2048,13 +2133,48 @@ export const editVideo = {
|
|
|
2048
2133
|
sourceVideoAssetId: sourceId,
|
|
2049
2134
|
billed_seconds: billedSeconds,
|
|
2050
2135
|
cost_cents: totalCents,
|
|
2051
|
-
|
|
2136
|
+
cost_credits: totalCents,
|
|
2052
2137
|
asset: result.asset,
|
|
2053
2138
|
generationId: result.generationId,
|
|
2054
2139
|
},
|
|
2055
2140
|
};
|
|
2056
2141
|
},
|
|
2057
2142
|
};
|
|
2143
|
+
// ── Trim a video to an exact window (fit-to-model primitive) ────
|
|
2144
|
+
export const trimVideo = {
|
|
2145
|
+
id: 'slates_trim_video',
|
|
2146
|
+
description: 'Trim a video asset to an exact [inSec, outSec] window and save the result as a NEW clip linked to the original (the original is untouched). This is the fit-to-model primitive: an 11s clip will not run on omni-flash-edit (3–10s) or Kling edit (3–15s), and a Seedance video reference must be 2–15s — trim it first, then edit/relocate the trimmed clip. EXACT re-encode (not a keyframe-snapped cut) so the result honors hard duration caps to the frame; any phone rotation flag is baked into the pixels in the same pass. The new clip lands with correct duration/width/height immediately. inSec defaults to 0.',
|
|
2147
|
+
input: z.object({
|
|
2148
|
+
projectId: z.string().uuid().describe('Project the clip lives in.'),
|
|
2149
|
+
assetId: z
|
|
2150
|
+
.string()
|
|
2151
|
+
.describe('The VIDEO asset to trim — UUID or badge code ("VID-V3", bare "V3"); resolves against the project at call time.'),
|
|
2152
|
+
inSec: z.number().min(0).optional().describe('Trim start in seconds (default 0).'),
|
|
2153
|
+
outSec: z.number().positive().describe('Trim end in seconds. Must be greater than inSec.'),
|
|
2154
|
+
}),
|
|
2155
|
+
async run(input, ctx) {
|
|
2156
|
+
const desktop = ctx.desktop();
|
|
2157
|
+
const resolved = await resolveAssetRefs(ctx, input.projectId, [input.assetId]);
|
|
2158
|
+
const assetId = resolved.get(input.assetId)?.id ?? input.assetId;
|
|
2159
|
+
const inSec = input.inSec ?? 0;
|
|
2160
|
+
if (input.outSec - inSec < 0.05) {
|
|
2161
|
+
throw new Error('outSec must be at least ~0.1s after inSec.');
|
|
2162
|
+
}
|
|
2163
|
+
const r = await desktop.post('/agent/assets/trim-video', {
|
|
2164
|
+
assetId,
|
|
2165
|
+
inSec,
|
|
2166
|
+
outSec: input.outSec,
|
|
2167
|
+
});
|
|
2168
|
+
const a = r.asset;
|
|
2169
|
+
const name = a?.code ?? a?.id ?? 'new clip';
|
|
2170
|
+
return {
|
|
2171
|
+
text: `Trimmed clip saved as ${name}${a?.label ? ` — ${a.label}` : ''} ` +
|
|
2172
|
+
`(${(input.outSec - inSec).toFixed(1)}s, ${inSec.toFixed(1)}–${input.outSec.toFixed(1)}s). ` +
|
|
2173
|
+
`Edit or generate from it by its new id/code.`,
|
|
2174
|
+
data: { asset: r.asset },
|
|
2175
|
+
};
|
|
2176
|
+
},
|
|
2177
|
+
};
|
|
2058
2178
|
// ── Generation status (background mode) ─────────────────────────
|
|
2059
2179
|
export const getGenerationStatus = {
|
|
2060
2180
|
id: 'slates_get_generation_status',
|
|
@@ -2086,7 +2206,7 @@ export const getGenerationStatus = {
|
|
|
2086
2206
|
// agent acts on, with the EXACT billed cost front and center.
|
|
2087
2207
|
return ok({
|
|
2088
2208
|
status,
|
|
2089
|
-
|
|
2209
|
+
cost_credits: g.cost != null ? creditsFromDollars(g.cost) : null,
|
|
2090
2210
|
error: g.error ?? null,
|
|
2091
2211
|
model: g.model,
|
|
2092
2212
|
completed_at: g.completedAt ?? null,
|
|
@@ -2561,6 +2681,8 @@ function resolveGuideTopic(topic) {
|
|
|
2561
2681
|
return 'slates-prompting-seedream-5-lite';
|
|
2562
2682
|
if (t.startsWith('veo'))
|
|
2563
2683
|
return 'slates-prompting-veo-3';
|
|
2684
|
+
if (t.startsWith('omni-flash') || t.startsWith('gemini-omni') || t === 'omni flash')
|
|
2685
|
+
return 'slates-prompting-omni-flash';
|
|
2564
2686
|
if (t.startsWith('kling-mc'))
|
|
2565
2687
|
return 'slates-prompting-motion-transfer';
|
|
2566
2688
|
if (t === 'edit-video' || t === 'video-edit' || t === 'edit video' || t === 'video edit')
|
|
@@ -2643,6 +2765,7 @@ export const ALL_OPERATIONS = [
|
|
|
2643
2765
|
generateLipSync,
|
|
2644
2766
|
generateMotionTransfer,
|
|
2645
2767
|
editVideo,
|
|
2768
|
+
trimVideo,
|
|
2646
2769
|
editImage,
|
|
2647
2770
|
getGenerationStatus,
|
|
2648
2771
|
listGenerations,
|