@slatesvideo/shared 0.5.0 → 0.5.1
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.js +98 -75
- package/dist/skills/content.js +8 -8
- package/package.json +1 -1
- package/skills/slates-cost-discipline.md +13 -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 +2 -2
- package/skills/slates-prompting-lip-sync.md +12 -12
- package/skills/slates-prompting-motion-transfer.md +11 -11
- package/skills/slates-storyboard-from-script.md +1 -1
- package/skills/slates-vision-feedback-loop.md +1 -1
package/dist/clients/cloud.d.ts
CHANGED
|
@@ -15,23 +15,23 @@ export interface SlatesUserInfo {
|
|
|
15
15
|
name: string;
|
|
16
16
|
license_status: string;
|
|
17
17
|
tier: string;
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
credit_balance?: number;
|
|
19
|
+
credit_balance_cents?: number;
|
|
20
20
|
};
|
|
21
21
|
scopes: string[];
|
|
22
22
|
}
|
|
23
23
|
export interface CreditsBalance {
|
|
24
24
|
success: boolean;
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
credit_balance?: number;
|
|
26
|
+
credit_balance_cents?: number;
|
|
27
27
|
}
|
|
28
28
|
export interface ModelRegistryResponse {
|
|
29
29
|
success: boolean;
|
|
30
|
-
credit_markup
|
|
30
|
+
credit_markup?: number;
|
|
31
31
|
models: Array<{
|
|
32
32
|
model: string;
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
cost_credits?: number;
|
|
34
|
+
cost_cents?: number;
|
|
35
35
|
}>;
|
|
36
36
|
}
|
|
37
37
|
//# sourceMappingURL=cloud.d.ts.map
|
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 ────────────────────────────────────────────────────
|
|
@@ -573,7 +596,7 @@ export const generateCharacterSheets = {
|
|
|
573
596
|
};
|
|
574
597
|
export const generateEnvironmentPlate = {
|
|
575
598
|
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
|
|
599
|
+
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
600
|
input: z.object({
|
|
578
601
|
environmentId: z.string().uuid(),
|
|
579
602
|
projectId: z.string().uuid(),
|
|
@@ -765,7 +788,7 @@ export const generateImage = {
|
|
|
765
788
|
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
789
|
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
790
|
background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
|
|
768
|
-
confirm: z.boolean().optional().describe('Set true to bypass the
|
|
791
|
+
confirm: z.boolean().optional().describe('Set true to bypass the confirm gate.'),
|
|
769
792
|
}),
|
|
770
793
|
async run(input, ctx) {
|
|
771
794
|
// Clarification gate: aspectRatio + resolution must be deliberate.
|
|
@@ -852,26 +875,26 @@ export const generateImage = {
|
|
|
852
875
|
const entry = registry.models.find((m) => m.model === costKey);
|
|
853
876
|
if (!entry)
|
|
854
877
|
throw new Error(`Model not in registry: ${costKey}`);
|
|
855
|
-
const totalCents = entry
|
|
878
|
+
const totalCents = creditCost(entry) * (input.count ?? 1);
|
|
856
879
|
// Confirm gate. Fires on cost > $0.50, AND (look-first, mirroring
|
|
857
880
|
// slates_generate_video) whenever reference assets are involved
|
|
858
881
|
// regardless of cost — the LLM must see what it's referencing before
|
|
859
882
|
// committing spend.
|
|
860
|
-
if ((totalCents >
|
|
883
|
+
if ((totalCents > CONFIRM_CREDITS || referenceAssetIds.length > 0) && !input.confirm) {
|
|
861
884
|
if (referenceAssetIds.length === 0) {
|
|
862
885
|
return ok({
|
|
863
886
|
requires_confirm: true,
|
|
864
887
|
model: costKey,
|
|
865
888
|
estimated_cents: totalCents,
|
|
866
|
-
|
|
867
|
-
message:
|
|
889
|
+
estimated_credits: totalCents,
|
|
890
|
+
message: `Cost exceeds ${CONFIRM_CREDITS} credits. Re-call with confirm=true to proceed, or pick a smaller resolution / count.`,
|
|
868
891
|
});
|
|
869
892
|
}
|
|
870
893
|
const previews = await previewAssets(ctx, referenceAssetIds.map((id) => ({ id, type: 'image', role: 'reference' })));
|
|
871
894
|
const refLines = previews.map((p) => ` - ${p.role}: ${p.ref}`).join('\n');
|
|
872
895
|
return {
|
|
873
896
|
text: `Pre-flight for ${imageModel} (${costKey}): ` +
|
|
874
|
-
|
|
897
|
+
`${fmtCredits(totalCents)}.` +
|
|
875
898
|
`\n\nReference images attached above:\n${refLines}\n\n` +
|
|
876
899
|
`Review them against your prompt — every reference's role must be labeled in the prompt text. ` +
|
|
877
900
|
`If the references suggest a different composition / style than the current prompt captures, REVISE the prompt before confirming. ` +
|
|
@@ -883,7 +906,7 @@ export const generateImage = {
|
|
|
883
906
|
model: imageModel,
|
|
884
907
|
variant: costKey,
|
|
885
908
|
estimated_cents: totalCents,
|
|
886
|
-
|
|
909
|
+
estimated_credits: totalCents,
|
|
887
910
|
references: previews.map((p) => ({
|
|
888
911
|
role: p.role,
|
|
889
912
|
ref: p.ref,
|
|
@@ -931,7 +954,7 @@ export const generateImage = {
|
|
|
931
954
|
costKey,
|
|
932
955
|
projectId: input.projectId,
|
|
933
956
|
cost_cents: totalCents,
|
|
934
|
-
|
|
957
|
+
cost_credits: totalCents,
|
|
935
958
|
}, refEcho);
|
|
936
959
|
}
|
|
937
960
|
const assetList = result.assets
|
|
@@ -957,7 +980,7 @@ export const generateImage = {
|
|
|
957
980
|
`The saved assets are in data.assets — re-generate only the missing count, don't redo the whole batch. ` +
|
|
958
981
|
`Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"`
|
|
959
982
|
: `Generated ${assetList.length} image(s) into project ${input.projectId} ` +
|
|
960
|
-
`for
|
|
983
|
+
`for ${fmtCredits(totalCents)}. ` +
|
|
961
984
|
`Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"` +
|
|
962
985
|
(refEcho ? ` ${refEcho}` : ''),
|
|
963
986
|
images,
|
|
@@ -968,7 +991,7 @@ export const generateImage = {
|
|
|
968
991
|
aspectRatio: input.aspectRatio,
|
|
969
992
|
resolution,
|
|
970
993
|
cost_cents: totalCents,
|
|
971
|
-
|
|
994
|
+
cost_credits: totalCents,
|
|
972
995
|
// Compact refs only — the full rows (prompt/settings/paths) were a
|
|
973
996
|
// multi-KB leak per generation and everything needed downstream is
|
|
974
997
|
// the id/code/label.
|
|
@@ -1027,7 +1050,7 @@ export const generateImage = {
|
|
|
1027
1050
|
images.push({ data: buf.toString('base64'), mimeType: mt });
|
|
1028
1051
|
}
|
|
1029
1052
|
return {
|
|
1030
|
-
text: `Generated ${urls.length} image(s) for
|
|
1053
|
+
text: `Generated ${urls.length} image(s) for ${fmtCredits(totalCents)}. ` +
|
|
1031
1054
|
`Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"`,
|
|
1032
1055
|
images,
|
|
1033
1056
|
data: {
|
|
@@ -1037,7 +1060,7 @@ export const generateImage = {
|
|
|
1037
1060
|
aspectRatio: input.aspectRatio,
|
|
1038
1061
|
resolution,
|
|
1039
1062
|
cost_cents: totalCents,
|
|
1040
|
-
|
|
1063
|
+
cost_credits: totalCents,
|
|
1041
1064
|
},
|
|
1042
1065
|
};
|
|
1043
1066
|
},
|
|
@@ -1076,7 +1099,7 @@ export const editImage = {
|
|
|
1076
1099
|
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
1100
|
quality: z.enum(['medium', 'high']).optional().describe('gpt-image-2 only — quality tier (default medium).'),
|
|
1078
1101
|
aspectRatio: z.string().optional(),
|
|
1079
|
-
confirm: z.boolean().optional().describe('Set true to bypass the
|
|
1102
|
+
confirm: z.boolean().optional().describe('Set true to bypass the confirm gate.'),
|
|
1080
1103
|
background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
|
|
1081
1104
|
}),
|
|
1082
1105
|
async run(input, ctx) {
|
|
@@ -1100,16 +1123,16 @@ export const editImage = {
|
|
|
1100
1123
|
const entry = registry.models.find((m) => m.model === costKey);
|
|
1101
1124
|
if (!entry)
|
|
1102
1125
|
throw new Error(`Model not in registry: ${costKey}`);
|
|
1103
|
-
const totalCents = entry
|
|
1104
|
-
if (totalCents >
|
|
1126
|
+
const totalCents = creditCost(entry);
|
|
1127
|
+
if (totalCents > CONFIRM_CREDITS && !input.confirm) {
|
|
1105
1128
|
const sourceRef = await lookupAssetRef(desktop, input.sourceAssetId);
|
|
1106
1129
|
return ok({
|
|
1107
1130
|
requires_confirm: true,
|
|
1108
1131
|
variant: costKey,
|
|
1109
1132
|
estimated_cents: totalCents,
|
|
1110
|
-
|
|
1133
|
+
estimated_credits: totalCents,
|
|
1111
1134
|
source_ref: sourceRef,
|
|
1112
|
-
message: `Cost:
|
|
1135
|
+
message: `Cost: ${fmtCredits(totalCents)} to edit ${sourceRef} with ${editModel} (${costKey}). ` +
|
|
1113
1136
|
`Re-call with confirm=true after the user explicitly OKs the spend. ` +
|
|
1114
1137
|
`When discussing with the user, refer to the source by its code (matches the gallery badge).`,
|
|
1115
1138
|
});
|
|
@@ -1135,7 +1158,7 @@ export const editImage = {
|
|
|
1135
1158
|
projectId: input.projectId,
|
|
1136
1159
|
sourceAssetId: input.sourceAssetId,
|
|
1137
1160
|
cost_cents: totalCents,
|
|
1138
|
-
|
|
1161
|
+
cost_credits: totalCents,
|
|
1139
1162
|
});
|
|
1140
1163
|
}
|
|
1141
1164
|
// Inline the edited result so the LLM sees whether the surgery landed —
|
|
@@ -1153,7 +1176,7 @@ export const editImage = {
|
|
|
1153
1176
|
}
|
|
1154
1177
|
return {
|
|
1155
1178
|
text: `Edited image saved as a new asset in project ${input.projectId} ` +
|
|
1156
|
-
`for
|
|
1179
|
+
`for ${fmtCredits(totalCents)} via ${editModel}. ` +
|
|
1157
1180
|
`Edit: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"`,
|
|
1158
1181
|
images,
|
|
1159
1182
|
data: {
|
|
@@ -1163,7 +1186,7 @@ export const editImage = {
|
|
|
1163
1186
|
sourceAssetId: input.sourceAssetId,
|
|
1164
1187
|
resolution,
|
|
1165
1188
|
cost_cents: totalCents,
|
|
1166
|
-
|
|
1189
|
+
cost_credits: totalCents,
|
|
1167
1190
|
asset: result.asset,
|
|
1168
1191
|
generationId: result.generationId,
|
|
1169
1192
|
},
|
|
@@ -1345,7 +1368,7 @@ export const generateVideo = {
|
|
|
1345
1368
|
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
1369
|
negativePrompt: z.string().optional(),
|
|
1347
1370
|
background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
|
|
1348
|
-
confirm: z.boolean().optional().describe('Set true after explicit user OK to bypass the
|
|
1371
|
+
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
1372
|
}),
|
|
1350
1373
|
async run(input, ctx) {
|
|
1351
1374
|
// Resolve the model FIRST — forgiving normalization (cost keys, alias
|
|
@@ -1375,7 +1398,7 @@ export const generateVideo = {
|
|
|
1375
1398
|
return ok({
|
|
1376
1399
|
requires_clarification: true,
|
|
1377
1400
|
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
|
|
1401
|
+
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
1402
|
});
|
|
1380
1403
|
}
|
|
1381
1404
|
if (!input.aspectRatio || !input.duration) {
|
|
@@ -1482,7 +1505,7 @@ export const generateVideo = {
|
|
|
1482
1505
|
throw new Error(`Model variant not in registry: ${costKey}. ` +
|
|
1483
1506
|
`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(', ')}`);
|
|
1484
1507
|
}
|
|
1485
|
-
const totalCents = entry
|
|
1508
|
+
const totalCents = creditCost(entry);
|
|
1486
1509
|
// Pre-flight confirm gate. Fires when:
|
|
1487
1510
|
// (a) cost > $0.50 (the cost gate), OR
|
|
1488
1511
|
// (b) any reference assets are involved (the look-first gate)
|
|
@@ -1511,7 +1534,7 @@ export const generateVideo = {
|
|
|
1511
1534
|
referenceRefs.push({ id, type: 'image', role: 'style' });
|
|
1512
1535
|
}
|
|
1513
1536
|
const hasReferences = referenceRefs.length > 0;
|
|
1514
|
-
if ((totalCents >
|
|
1537
|
+
if ((totalCents > CONFIRM_CREDITS || hasReferences) && !input.confirm) {
|
|
1515
1538
|
const previews = hasReferences ? await previewAssets(ctx, referenceRefs) : [];
|
|
1516
1539
|
const refLines = previews.map((p) => ` - ${p.role}: ${p.ref}`).join('\n');
|
|
1517
1540
|
const refSummary = hasReferences
|
|
@@ -1521,7 +1544,7 @@ export const generateVideo = {
|
|
|
1521
1544
|
const refImages = previews.flatMap((p) => p.images);
|
|
1522
1545
|
return {
|
|
1523
1546
|
text: `Pre-flight for ${input.duration}s ${input.model} (${costKey}): ` +
|
|
1524
|
-
|
|
1547
|
+
`${fmtCredits(totalCents)}.` +
|
|
1525
1548
|
refSummary +
|
|
1526
1549
|
`\n\nWhen ready, re-call slates_generate_video with confirm=true and the (possibly revised) prompt.`,
|
|
1527
1550
|
images: refImages,
|
|
@@ -1530,7 +1553,7 @@ export const generateVideo = {
|
|
|
1530
1553
|
model: input.model,
|
|
1531
1554
|
variant: costKey,
|
|
1532
1555
|
estimated_cents: totalCents,
|
|
1533
|
-
|
|
1556
|
+
estimated_credits: totalCents,
|
|
1534
1557
|
references: previews.map((p) => ({
|
|
1535
1558
|
role: p.role,
|
|
1536
1559
|
ref: p.ref,
|
|
@@ -1581,7 +1604,7 @@ export const generateVideo = {
|
|
|
1581
1604
|
variant: costKey,
|
|
1582
1605
|
projectId: input.projectId,
|
|
1583
1606
|
cost_cents: totalCents,
|
|
1584
|
-
|
|
1607
|
+
cost_credits: totalCents,
|
|
1585
1608
|
references: refInputs.map(({ ref, role }) => ({
|
|
1586
1609
|
role,
|
|
1587
1610
|
...(resolvedRefs.get(ref) ?? { id: ref, code: null, label: null }),
|
|
@@ -1590,7 +1613,7 @@ export const generateVideo = {
|
|
|
1590
1613
|
}
|
|
1591
1614
|
return {
|
|
1592
1615
|
text: `Generated ${input.duration}s ${input.model} video into project ${input.projectId} ` +
|
|
1593
|
-
`for
|
|
1616
|
+
`for ${fmtCredits(totalCents)}. ` +
|
|
1594
1617
|
`Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"` +
|
|
1595
1618
|
(refEcho ? ` ${refEcho}` : ''),
|
|
1596
1619
|
data: {
|
|
@@ -1600,7 +1623,7 @@ export const generateVideo = {
|
|
|
1600
1623
|
aspectRatio: input.aspectRatio,
|
|
1601
1624
|
duration: input.duration,
|
|
1602
1625
|
cost_cents: totalCents,
|
|
1603
|
-
|
|
1626
|
+
cost_credits: totalCents,
|
|
1604
1627
|
asset: result.asset,
|
|
1605
1628
|
generationId: result.generationId,
|
|
1606
1629
|
},
|
|
@@ -1621,8 +1644,8 @@ export const generateLipSync = {
|
|
|
1621
1644
|
ttsLanguage: z.enum(['EN', 'ZH', 'JA', 'KO', 'ES']).optional().describe('Kling engine only — TTS language. Default EN.'),
|
|
1622
1645
|
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
1646
|
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.
|
|
1647
|
+
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.'),
|
|
1648
|
+
klingProvider: z.enum(['fal', 'kling']).optional().describe('Kling engine only — provider routing. Leave unset: all agent generations bill Slates credits (BYOK is retired).'),
|
|
1626
1649
|
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
1650
|
videoResolution: z.enum(['480p', '720p', '1080p', '4k']).optional().describe('Seedance engine only. Default 1080p.'),
|
|
1628
1651
|
aspectRatio: z.string().optional().describe('Seedance engine only. Default 16:9.'),
|
|
@@ -1632,7 +1655,7 @@ export const generateLipSync = {
|
|
|
1632
1655
|
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
1656
|
audioSeconds: z.number().optional().describe('Seedance engine + audioMethod=upload: the audio file\'s duration in seconds — sets the output length (4-15s).'),
|
|
1634
1657
|
background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
|
|
1635
|
-
confirm: z.boolean().optional().describe('Set true to bypass the
|
|
1658
|
+
confirm: z.boolean().optional().describe('Set true to bypass the confirm gate. Required for avatar-pro.'),
|
|
1636
1659
|
}),
|
|
1637
1660
|
async run(input, ctx) {
|
|
1638
1661
|
if (input.audioMethod === 'tts' && !input.ttsText) {
|
|
@@ -1696,12 +1719,12 @@ export const generateLipSync = {
|
|
|
1696
1719
|
const entry = registry.models.find((m) => m.model === costKey);
|
|
1697
1720
|
if (!entry)
|
|
1698
1721
|
throw new Error(`Model variant not in registry: ${costKey}`);
|
|
1699
|
-
const totalCents = entry
|
|
1722
|
+
const totalCents = creditCost(entry);
|
|
1700
1723
|
// Cost confirm gate. Lip-sync is mechanical — the model re-syncs the
|
|
1701
1724
|
// user-chosen source to the user-chosen audio. The agent doesn't
|
|
1702
1725
|
// write a prompt that depends on what the source looks like, so we
|
|
1703
1726
|
// skip the inline preview and just announce the source code in text.
|
|
1704
|
-
if (totalCents >
|
|
1727
|
+
if (totalCents > CONFIRM_CREDITS && !input.confirm) {
|
|
1705
1728
|
const sourceRef = await lookupAssetRef(ctx.desktop(), input.sourceAssetId);
|
|
1706
1729
|
const audioPreview = input.audioMethod === 'tts'
|
|
1707
1730
|
? `Audio: TTS — "${(input.ttsText ?? '').slice(0, 120)}"`
|
|
@@ -1710,9 +1733,9 @@ export const generateLipSync = {
|
|
|
1710
1733
|
requires_confirm: true,
|
|
1711
1734
|
variant: costKey,
|
|
1712
1735
|
estimated_cents: totalCents,
|
|
1713
|
-
|
|
1736
|
+
estimated_credits: totalCents,
|
|
1714
1737
|
source_ref: sourceRef,
|
|
1715
|
-
message: `Cost:
|
|
1738
|
+
message: `Cost: ${fmtCredits(totalCents)} for ${isSeedance ? `${seedanceDuration}s Seedance` : '5s'} lip-sync (${costKey}). ` +
|
|
1716
1739
|
`Source: ${sourceRef}. ${audioPreview}. ` +
|
|
1717
1740
|
`Re-call with confirm=true after the user explicitly OKs the spend. ` +
|
|
1718
1741
|
`When discussing with the user, refer to the source by its code (matches the gallery badge).`,
|
|
@@ -1761,12 +1784,12 @@ export const generateLipSync = {
|
|
|
1761
1784
|
projectId: input.projectId,
|
|
1762
1785
|
sourceAssetId: input.sourceAssetId,
|
|
1763
1786
|
cost_cents: totalCents,
|
|
1764
|
-
|
|
1787
|
+
cost_credits: totalCents,
|
|
1765
1788
|
});
|
|
1766
1789
|
}
|
|
1767
1790
|
return {
|
|
1768
1791
|
text: `Generated ${isSeedance ? `${seedanceDuration}s` : '5s'} lip-sync (${costKey}) into project ${input.projectId} ` +
|
|
1769
|
-
`for
|
|
1792
|
+
`for ${fmtCredits(totalCents)}. ` +
|
|
1770
1793
|
(input.audioMethod === 'tts'
|
|
1771
1794
|
? `Spoken: "${(input.ttsText ?? '').slice(0, 60)}${(input.ttsText ?? '').length > 60 ? '...' : ''}"`
|
|
1772
1795
|
: `Audio: ${input.audioFilePath}`),
|
|
@@ -1776,7 +1799,7 @@ export const generateLipSync = {
|
|
|
1776
1799
|
sourceType: input.sourceType,
|
|
1777
1800
|
sourceAssetId: input.sourceAssetId,
|
|
1778
1801
|
cost_cents: totalCents,
|
|
1779
|
-
|
|
1802
|
+
cost_credits: totalCents,
|
|
1780
1803
|
asset: result.asset,
|
|
1781
1804
|
generationId: result.generationId,
|
|
1782
1805
|
},
|
|
@@ -1791,7 +1814,7 @@ export const generateMotionTransfer = {
|
|
|
1791
1814
|
projectId: z.string().uuid().describe('Slates project. Both source and target assets must live here.'),
|
|
1792
1815
|
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
1816
|
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 (
|
|
1817
|
+
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
1818
|
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
1819
|
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
1820
|
klingProvider: z.enum(['fal', 'kling']).optional().describe('Kling engine only — provider routing. "fal" (default) uses Slates credits.'),
|
|
@@ -1803,7 +1826,7 @@ export const generateMotionTransfer = {
|
|
|
1803
1826
|
realFaceConsent: z.boolean().optional().describe('MANDATORY with seedanceRealFace — set true only after the user explicitly confirms they hold rights/consent to the likeness.'),
|
|
1804
1827
|
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
1828
|
background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
|
|
1806
|
-
confirm: z.boolean().optional().describe('Set true to bypass the
|
|
1829
|
+
confirm: z.boolean().optional().describe('Set true to bypass the confirm gate. Required — both tiers exceed.'),
|
|
1807
1830
|
}),
|
|
1808
1831
|
async run(input, ctx) {
|
|
1809
1832
|
const motionModel = input.motionModel ?? 'kling-mc-pro';
|
|
@@ -1843,12 +1866,12 @@ export const generateMotionTransfer = {
|
|
|
1843
1866
|
const entry = registry.models.find((m) => m.model === costKey);
|
|
1844
1867
|
if (!entry)
|
|
1845
1868
|
throw new Error(`Model variant not in registry: ${costKey}`);
|
|
1846
|
-
const totalCents = entry
|
|
1869
|
+
const totalCents = creditCost(entry);
|
|
1847
1870
|
// Cost confirm gate. Motion transfer is mechanical — the model
|
|
1848
1871
|
// applies source motion to target image deterministically. We don't
|
|
1849
1872
|
// burn tokens previewing assets the user already chose; codes in the
|
|
1850
1873
|
// text are enough to keep the chat unambiguous.
|
|
1851
|
-
if (totalCents >
|
|
1874
|
+
if (totalCents > CONFIRM_CREDITS && !input.confirm) {
|
|
1852
1875
|
const desktop = ctx.desktop();
|
|
1853
1876
|
const [source, target] = await Promise.all([
|
|
1854
1877
|
lookupAssetRef(desktop, input.sourceVideoAssetId),
|
|
@@ -1858,12 +1881,12 @@ export const generateMotionTransfer = {
|
|
|
1858
1881
|
requires_confirm: true,
|
|
1859
1882
|
variant: costKey,
|
|
1860
1883
|
estimated_cents: totalCents,
|
|
1861
|
-
|
|
1884
|
+
estimated_credits: totalCents,
|
|
1862
1885
|
source_ref: source,
|
|
1863
1886
|
target_ref: target,
|
|
1864
|
-
message: `Cost:
|
|
1887
|
+
message: `Cost: ${fmtCredits(totalCents)} for ${isSeedance ? `${seedanceDuration}s Seedance motion transfer` : `5s ${motionModel}`} (${costKey}). ` +
|
|
1865
1888
|
`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
|
|
1889
|
+
`Re-call with confirm=true after the user explicitly OKs the spend${isSeedance ? '' : ', or pick kling-mc-std to save ~10 credits'}. ` +
|
|
1867
1890
|
`When discussing with the user, refer to the assets by those codes — they'll match the gallery badges.`,
|
|
1868
1891
|
});
|
|
1869
1892
|
}
|
|
@@ -1908,12 +1931,12 @@ export const generateMotionTransfer = {
|
|
|
1908
1931
|
sourceVideoAssetId: input.sourceVideoAssetId,
|
|
1909
1932
|
targetImageAssetId: input.targetImageAssetId,
|
|
1910
1933
|
cost_cents: totalCents,
|
|
1911
|
-
|
|
1934
|
+
cost_credits: totalCents,
|
|
1912
1935
|
});
|
|
1913
1936
|
}
|
|
1914
1937
|
return {
|
|
1915
1938
|
text: `Generated ${isSeedance ? `${seedanceDuration}s` : '5s'} motion transfer (${motionModel}) into project ${input.projectId} ` +
|
|
1916
|
-
`for
|
|
1939
|
+
`for ${fmtCredits(totalCents)}.` +
|
|
1917
1940
|
(input.prompt ? ` Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"` : ''),
|
|
1918
1941
|
data: {
|
|
1919
1942
|
variant: costKey,
|
|
@@ -1922,7 +1945,7 @@ export const generateMotionTransfer = {
|
|
|
1922
1945
|
sourceVideoAssetId: input.sourceVideoAssetId,
|
|
1923
1946
|
targetImageAssetId: input.targetImageAssetId,
|
|
1924
1947
|
cost_cents: totalCents,
|
|
1925
|
-
|
|
1948
|
+
cost_credits: totalCents,
|
|
1926
1949
|
asset: result.asset,
|
|
1927
1950
|
generationId: result.generationId,
|
|
1928
1951
|
},
|
|
@@ -1988,17 +2011,17 @@ export const editVideo = {
|
|
|
1988
2011
|
const entry = registry.models.find((m) => m.model === costKey);
|
|
1989
2012
|
if (!entry)
|
|
1990
2013
|
throw new Error(`Model variant not in registry: ${costKey}`);
|
|
1991
|
-
const totalCents = entry
|
|
2014
|
+
const totalCents = creditCost(entry);
|
|
1992
2015
|
// Confirm gate — look-first: preview the source clip + refs so the LLM
|
|
1993
2016
|
// sees what it's editing before committing spend (mirrors generateVideo).
|
|
1994
|
-
if ((totalCents >
|
|
2017
|
+
if ((totalCents > CONFIRM_CREDITS || refInputs.length > 1) && !input.confirm) {
|
|
1995
2018
|
const previews = await previewAssets(ctx, [
|
|
1996
2019
|
{ id: sourceId, type: 'video', role: 'source clip' },
|
|
1997
2020
|
...characterAssetIds.map((id) => ({ id, type: 'image', role: 'subject element' })),
|
|
1998
2021
|
...styleAssetIds.map((id) => ({ id, type: 'image', role: 'style' })),
|
|
1999
2022
|
]);
|
|
2000
2023
|
return {
|
|
2001
|
-
text: `Cost:
|
|
2024
|
+
text: `Cost: ${fmtCredits(totalCents)} to edit a ${billedSeconds}s clip with ${model} (${costKey}). ` +
|
|
2002
2025
|
`${refEcho} Re-call with confirm=true after the user explicitly OKs the spend.`,
|
|
2003
2026
|
images: previews.flatMap((p) => p.images),
|
|
2004
2027
|
data: {
|
|
@@ -2008,7 +2031,7 @@ export const editVideo = {
|
|
|
2008
2031
|
billed_seconds: billedSeconds,
|
|
2009
2032
|
clip_seconds: clipSeconds,
|
|
2010
2033
|
estimated_cents: totalCents,
|
|
2011
|
-
|
|
2034
|
+
estimated_credits: totalCents,
|
|
2012
2035
|
references: previews.map((p) => ({ ref: p.ref, role: p.role, ...p.meta })),
|
|
2013
2036
|
},
|
|
2014
2037
|
};
|
|
@@ -2033,12 +2056,12 @@ export const editVideo = {
|
|
|
2033
2056
|
projectId: input.projectId,
|
|
2034
2057
|
sourceVideoAssetId: sourceId,
|
|
2035
2058
|
cost_cents: totalCents,
|
|
2036
|
-
|
|
2059
|
+
cost_credits: totalCents,
|
|
2037
2060
|
}, refEcho);
|
|
2038
2061
|
}
|
|
2039
2062
|
return {
|
|
2040
2063
|
text: `Edited ${billedSeconds}s clip saved as a new asset in project ${input.projectId} ` +
|
|
2041
|
-
`for
|
|
2064
|
+
`for ${fmtCredits(totalCents)} via ${model}. ` +
|
|
2042
2065
|
`Edit: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"` +
|
|
2043
2066
|
(refEcho ? ` ${refEcho}` : ''),
|
|
2044
2067
|
data: {
|
|
@@ -2048,7 +2071,7 @@ export const editVideo = {
|
|
|
2048
2071
|
sourceVideoAssetId: sourceId,
|
|
2049
2072
|
billed_seconds: billedSeconds,
|
|
2050
2073
|
cost_cents: totalCents,
|
|
2051
|
-
|
|
2074
|
+
cost_credits: totalCents,
|
|
2052
2075
|
asset: result.asset,
|
|
2053
2076
|
generationId: result.generationId,
|
|
2054
2077
|
},
|
|
@@ -2086,7 +2109,7 @@ export const getGenerationStatus = {
|
|
|
2086
2109
|
// agent acts on, with the EXACT billed cost front and center.
|
|
2087
2110
|
return ok({
|
|
2088
2111
|
status,
|
|
2089
|
-
|
|
2112
|
+
cost_credits: g.cost != null ? creditsFromDollars(g.cost) : null,
|
|
2090
2113
|
error: g.error ?? null,
|
|
2091
2114
|
model: g.model,
|
|
2092
2115
|
completed_at: g.completedAt ?? null,
|