@hubfluencer/mcp 0.8.2 → 0.9.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/README.md +23 -12
- package/dist/index.js +650 -179
- package/package.json +1 -1
- package/src/campaign.ts +1 -1
- package/src/client.ts +4 -1
- package/src/core.ts +309 -15
- package/src/index.ts +580 -89
- package/src/output-schemas.ts +13 -2
- package/src/uploads.ts +401 -163
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* One-shot : make_video({ prompt }) → finished MP4 (create→start→poll→download)
|
|
11
11
|
* Shorts : create_short → generate_short → wait_for_completion → download_result
|
|
12
12
|
* (then iterate free: update_short → rerender_short, 0 credits)
|
|
13
|
-
* Editor : create_editor_ad (
|
|
13
|
+
* Editor : create_editor_ad (create + assets + quote; max_credits launches) → wait_for_completion → download_result
|
|
14
14
|
* Granular : create_editor_draft → generate_scenario|set_scenario → get_editor
|
|
15
15
|
* → apply_scenario (scenario → scene prompts) | set_scene_count + set_segment_prompt
|
|
16
16
|
* → generate_segment(s) (+ regenerate_segment to re-roll a completed scene)
|
|
@@ -116,7 +116,14 @@ import {
|
|
|
116
116
|
} from "./series-calendar.js";
|
|
117
117
|
import {
|
|
118
118
|
CATALOG_ASSET_EXTS,
|
|
119
|
+
closePreparedUploadFile,
|
|
119
120
|
IMAGE_EXTS,
|
|
121
|
+
MAX_CLOSING_IMAGE_BYTES,
|
|
122
|
+
MAX_LOGO_BYTES,
|
|
123
|
+
MAX_PRODUCT_IMAGE_BYTES,
|
|
124
|
+
type PreparedUploadFile,
|
|
125
|
+
preparedUploadIdentity,
|
|
126
|
+
prepareImageUpload,
|
|
120
127
|
resolveVideoReadPath,
|
|
121
128
|
uploadCatalogAsset,
|
|
122
129
|
uploadImageFile,
|
|
@@ -146,6 +153,98 @@ async function fetchStatus(
|
|
|
146
153
|
return normalizeStatus(kind, slug, asRecord(res).data);
|
|
147
154
|
}
|
|
148
155
|
|
|
156
|
+
async function editorAutopilotQuote(
|
|
157
|
+
client: HubfluencerClient,
|
|
158
|
+
slug: string,
|
|
159
|
+
overrides: {
|
|
160
|
+
language?: string;
|
|
161
|
+
product_subject?: string;
|
|
162
|
+
product_prompt?: string;
|
|
163
|
+
restart?: boolean;
|
|
164
|
+
} = {},
|
|
165
|
+
): Promise<{
|
|
166
|
+
estimated_credits: number | null;
|
|
167
|
+
available_credits: number | null;
|
|
168
|
+
affordable: boolean;
|
|
169
|
+
}> {
|
|
170
|
+
try {
|
|
171
|
+
const params = new URLSearchParams();
|
|
172
|
+
if (overrides.language) params.set("language", overrides.language);
|
|
173
|
+
if (overrides.product_subject)
|
|
174
|
+
params.set("product_subject", overrides.product_subject);
|
|
175
|
+
if (overrides.product_prompt)
|
|
176
|
+
params.set("product_prompt", overrides.product_prompt);
|
|
177
|
+
if (overrides.restart) params.set("restart", "true");
|
|
178
|
+
const query = params.toString();
|
|
179
|
+
const response = await client.get<{ data: unknown }>(
|
|
180
|
+
`/editor/${slug}/autopilot/cost${query ? `?${query}` : ""}`,
|
|
181
|
+
);
|
|
182
|
+
const cost = asRecord(asRecord(response).data);
|
|
183
|
+
const estimated_credits =
|
|
184
|
+
typeof cost.total === "number" ? cost.total : null;
|
|
185
|
+
const available_credits =
|
|
186
|
+
typeof cost.available_credits === "number"
|
|
187
|
+
? cost.available_credits
|
|
188
|
+
: null;
|
|
189
|
+
return {
|
|
190
|
+
estimated_credits,
|
|
191
|
+
available_credits,
|
|
192
|
+
affordable:
|
|
193
|
+
estimated_credits !== null &&
|
|
194
|
+
available_credits !== null &&
|
|
195
|
+
available_credits >= estimated_credits,
|
|
196
|
+
};
|
|
197
|
+
} catch (error) {
|
|
198
|
+
const apiError = error as Partial<HubfluencerError>;
|
|
199
|
+
if (
|
|
200
|
+
typeof apiError.status === "number" &&
|
|
201
|
+
apiError.status >= 400 &&
|
|
202
|
+
apiError.status < 500 &&
|
|
203
|
+
apiError.status !== 429
|
|
204
|
+
) {
|
|
205
|
+
// Authentication, scope, validation, and unknown-slug errors are stable
|
|
206
|
+
// and actionable. Preserve them rather than disguising them as a pricing
|
|
207
|
+
// outage. Only transient/network/server failures use the fail-closed quote.
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
// A spend cap cannot be enforced without a live price. Fail closed: callers
|
|
211
|
+
// receive a free draft/quote response and can retry the preflight later.
|
|
212
|
+
return {
|
|
213
|
+
estimated_credits: null,
|
|
214
|
+
available_credits: null,
|
|
215
|
+
affordable: false,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function editorChargeReceipt(
|
|
221
|
+
client: HubfluencerClient,
|
|
222
|
+
slug: string,
|
|
223
|
+
action: "scene_regeneration" | "voice_generation" | "music_generation",
|
|
224
|
+
listPriceCredits: number,
|
|
225
|
+
): Promise<Record<string, unknown>> {
|
|
226
|
+
try {
|
|
227
|
+
const response = await client.get<{ data: unknown }>(`/editor/${slug}`);
|
|
228
|
+
const editor = asRecord(asRecord(response).data);
|
|
229
|
+
return {
|
|
230
|
+
action,
|
|
231
|
+
list_price_credits: listPriceCredits,
|
|
232
|
+
project_spend: editor.project_spend ?? null,
|
|
233
|
+
approved_cap: editor.autopilot_max_credits ?? null,
|
|
234
|
+
approved_run_spend: editor.autopilot_credits_spent ?? null,
|
|
235
|
+
};
|
|
236
|
+
} catch {
|
|
237
|
+
// The paid mutation already succeeded. Never make a receipt-enrichment GET
|
|
238
|
+
// look like a failed charge that an agent should retry.
|
|
239
|
+
return {
|
|
240
|
+
action,
|
|
241
|
+
list_price_credits: listPriceCredits,
|
|
242
|
+
project_spend: null,
|
|
243
|
+
enrichment_unavailable: true,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
149
248
|
function isRecompositeInProgressConflict(e: unknown): boolean {
|
|
150
249
|
const err = e as Partial<HubfluencerError>;
|
|
151
250
|
if (err?.status !== 409) return false;
|
|
@@ -501,14 +600,14 @@ const WRITE = {
|
|
|
501
600
|
const SERVER_INSTRUCTIONS = `Hubfluencer turns a prompt — or a whole product page — into finished, post-ready vertical videos and image carousels, and can run the surrounding content operation (plan → draft → review → schedule → measure → repeat).
|
|
502
601
|
|
|
503
602
|
MAKE ONE ASSET (simplest)
|
|
504
|
-
- make_video({ prompt }) creates, generates, and returns a finished MP4 — it picks a single-clip short or a multi-scene editor ad from the prompt (override with kind:"short"|"editor").
|
|
603
|
+
- make_video({ prompt }) creates, generates, and returns a finished MP4 — it picks a single-clip short or a multi-scene editor ad from the prompt (override with kind:"short"|"editor"). create_editor_ad creates/configures/prices an editor and launches only with max_credits; create_short creates a short draft; create_slider makes an image carousel; create_tracking_video burns a CV overlay onto a local clip.
|
|
505
604
|
- Granular (full control): create a draft, then drive the pipeline step by step (create_editor_draft → generate_scenario → apply_scenario → review/edit prompts (set_segment_prompt) → generate_segment(s) → generate_voice + generate_music → render; regenerate_segment re-rolls a completed scene as a new version). Poll with get_status / wait_for_completion; fetch with download_result (a slider has no MP4 — read its slides with get_slider).
|
|
506
605
|
|
|
507
606
|
DELEGATE THE CONTENT OPERATION (the full loop — steps 1-3 are $0)
|
|
508
607
|
1. ONBOARD a product: extract_product_page(url) pulls facts + imagery (SSRF-hardened, 0cr); import_product_image saves its picture/logo. Persist identity once with create_product_profile + create_brand_profile so every later draft inherits it instead of re-describing the brand.
|
|
509
608
|
2. PLAN ($0, free quota — not credits): plan_campaign proposes angles/formats + a per-item credit estimate; generate_hook_variations writes hook options. create_campaign(url | product_profile_id) does 1-2 end-to-end and returns a pack of editable DRAFTS.
|
|
510
609
|
3. PACK & MATERIALIZE ($0): create_campaign_pack / add_pack_variations assemble items; materialize_pack_item turns each into a 0-credit draft (editor_ad/short → a video project, carousel → a slider). list_campaign_packs lists packs; get_campaign_pack({ id }) shows every item's draft slug + credit estimate.
|
|
511
|
-
4. GENERATE per draft (CREDITS — a separate, explicit step):
|
|
610
|
+
4. GENERATE per draft (CREDITS — a separate, explicit step): start_autopilot({slug}) quotes free; after approval, start_autopilot({slug,max_credits:approved_cap}) launches. Run generate_short/generate_slider for approved short/carousel drafts. Nothing above this line spends video credits.
|
|
512
611
|
5. REVIEW: create_review_link mints a no-login page where a human approves/comments (list_review_links / revoke_review_link manage them). Gate spend or posting on approval.
|
|
513
612
|
6. CALENDAR (reminder-only): schedule_post / list_scheduled_posts / reschedule_post / cancel_scheduled_post stage a calendar that pushes REMINDERS to the user — it does NOT auto-publish (posting to TikTok/IG stays a human step in the app). After the user posts the presigned export, call mark_scheduled_posted.
|
|
514
613
|
7. MEASURE → DOUBLE DOWN: record_performance logs views/engagement (tag creatives via set_item_attributes); get_recommendations gives a CAUTIOUS, non-causal read of your own best hook/format (present its confidence + note as-is, never upgrade them); make_more_like_winner spins fresh hooks from a proven item — feed a winner back into add_pack_variations → step 4.
|
|
@@ -516,11 +615,11 @@ DELEGATE THE CONTENT OPERATION (the full loop — steps 1-3 are $0)
|
|
|
516
615
|
|
|
517
616
|
CREDITS (spent server-side; each tool prices before charging — see its description)
|
|
518
617
|
- Free (0 credits): every draft create/edit, reorder, all slider re-composites, short re-renders (rerender_short — applies text/style/CTA/end-card edits over a short's already-paid footage), and the whole onboard/plan/pack/materialize path (steps 1-3). get_credits shows the balance.
|
|
519
|
-
- Charged: short render 15; editor AI scene 5 (4 in a batch of ≥3); voice 3; music 5; editor render 0 (auto-charges ungenerated scenes); slider 1/slide; tracking render 1 (refunded on failure). make_video
|
|
618
|
+
- Charged: short render 15; editor AI scene 5 (4 in a batch of ≥3); voice 3; music 5; editor render 0 (auto-charges ungenerated scenes); slider 1/slide; tracking render 1 (refunded on failure). make_video supports dry_run/max_credits; create_editor_ad/start_autopilot require max_credits before launching.
|
|
520
619
|
- AI ASSISTS (all generate_*/enhance/suggest/plan/hook tools) draw a FREE daily quota (20/day), NOT credits. On 429, set auto_unlock:true to spend 1 credit for +10 assists (retried once), or write the copy yourself and use the set_* tools.
|
|
521
620
|
|
|
522
621
|
WAITING & RESUMING
|
|
523
|
-
- Renders run async and can take minutes. wait_for_completion polls a budget capped at 280s; if it returns terminal=false / timed_out, just CALL IT AGAIN with the same slug. make_video returns the slug + a resume hint on timeout.
|
|
622
|
+
- Renders run async and can take minutes. wait_for_completion polls a budget capped at 280s; if it returns terminal=false / timed_out, just CALL IT AGAIN with the same slug. make_video returns the slug + a resume hint on timeout. A terminal result is not always ready: an editor can go terminal in a needs-action stage (editing_required = stale after edits, regenerate the flagged scenes/audio; insufficient_credits = batch parked, top up / reduce scope; idle = draft with nothing generating, start generation first) — act on those instead of re-polling.
|
|
524
623
|
- Creates and charged actions are idempotency-keyed: a transport retry is safe; a genuine re-run after a terminal FAILURE restarts rather than replaying the dead attempt.
|
|
525
624
|
|
|
526
625
|
LOCAL FILES (sandboxed)
|
|
@@ -843,6 +942,8 @@ registerTool(
|
|
|
843
942
|
),
|
|
844
943
|
max_credits: z
|
|
845
944
|
.number()
|
|
945
|
+
.int()
|
|
946
|
+
.nonnegative()
|
|
846
947
|
.optional()
|
|
847
948
|
.describe(
|
|
848
949
|
"Spend cap: refuse to start (no charge) if the priced estimate exceeds this. Returns the estimate.",
|
|
@@ -1024,9 +1125,9 @@ registerTool(
|
|
|
1024
1125
|
slug = created.data.slug;
|
|
1025
1126
|
}
|
|
1026
1127
|
|
|
1027
|
-
// 2) Preflight cost vs the live balance BEFORE charging.
|
|
1028
|
-
//
|
|
1029
|
-
//
|
|
1128
|
+
// 2) Preflight cost vs the live balance BEFORE charging. Shorts retain
|
|
1129
|
+
// their fixed-price compatibility behavior, but an editor without a
|
|
1130
|
+
// readable quote must never fall through to the API's uncapped launch.
|
|
1030
1131
|
const costPath =
|
|
1031
1132
|
kind === "short"
|
|
1032
1133
|
? `/shorts/${slug}/cost`
|
|
@@ -1043,13 +1144,14 @@ registerTool(
|
|
|
1043
1144
|
? cost.available_credits
|
|
1044
1145
|
: null;
|
|
1045
1146
|
} catch {
|
|
1046
|
-
//
|
|
1147
|
+
// The editor branch below fails closed unless the caller supplied an
|
|
1148
|
+
// explicit cap that the launch endpoint can enforce independently.
|
|
1047
1149
|
}
|
|
1048
1150
|
|
|
1049
1151
|
const resume =
|
|
1050
1152
|
kind === "short"
|
|
1051
1153
|
? `generate_short({ slug: "${slug}"${args.skip_auto_text ? ", skip_auto_text: true" : ""} })`
|
|
1052
|
-
: `start_autopilot({ slug: "${slug}" })`;
|
|
1154
|
+
: `start_autopilot({ slug: "${slug}", max_credits: ${estimated_credits ?? "<approved cap>"} })`;
|
|
1053
1155
|
const affordable =
|
|
1054
1156
|
available_credits === null ||
|
|
1055
1157
|
estimated_credits === null ||
|
|
@@ -1058,17 +1160,28 @@ registerTool(
|
|
|
1058
1160
|
args.max_credits != null &&
|
|
1059
1161
|
estimated_credits != null &&
|
|
1060
1162
|
estimated_credits > args.max_credits;
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1163
|
+
const editorPriceUnavailableWithoutCap =
|
|
1164
|
+
kind === "editor" &&
|
|
1165
|
+
estimated_credits === null &&
|
|
1166
|
+
args.max_credits == null;
|
|
1167
|
+
|
|
1168
|
+
// 3) Stop BEFORE charging on dry_run, an uncapped editor with no live
|
|
1169
|
+
// quote, an over-cap estimate, or a balance we already know is too
|
|
1170
|
+
// low. The free draft is returned so the caller can retry the quote,
|
|
1171
|
+
// top up, or raise the cap without an orphan or surprise charge.
|
|
1172
|
+
if (
|
|
1173
|
+
args.dry_run ||
|
|
1174
|
+
editorPriceUnavailableWithoutCap ||
|
|
1175
|
+
overCap ||
|
|
1176
|
+
!affordable
|
|
1177
|
+
) {
|
|
1067
1178
|
const note = args.dry_run
|
|
1068
1179
|
? `Dry run — created a free draft and priced it (${estimated_credits ?? "?"} credits, have ${available_credits ?? "?"}). To run it: ${resume}.`
|
|
1069
|
-
:
|
|
1070
|
-
? `
|
|
1071
|
-
:
|
|
1180
|
+
: editorPriceUnavailableWithoutCap
|
|
1181
|
+
? `Pricing is temporarily unavailable, so the editor was left as a free draft and nothing was charged. Retry the quote, then run start_autopilot({ slug: "${slug}", max_credits: <approved cap> }).`
|
|
1182
|
+
: overCap
|
|
1183
|
+
? `Estimated ${estimated_credits} credits exceeds max_credits ${args.max_credits}; nothing charged. Raise max_credits, or run ${resume}.`
|
|
1184
|
+
: `Not enough credits (needs ${estimated_credits ?? "?"}, have ${available_credits ?? "?"}); nothing charged. Top up, then run ${resume}.`;
|
|
1072
1185
|
return ok({
|
|
1073
1186
|
slug,
|
|
1074
1187
|
kind,
|
|
@@ -1116,9 +1229,20 @@ registerTool(
|
|
|
1116
1229
|
`gen-short:${slug}:${startState}${args.skip_auto_text ? ":bare" : ""}`,
|
|
1117
1230
|
);
|
|
1118
1231
|
} else {
|
|
1232
|
+
const approvedCap = args.max_credits ?? estimated_credits;
|
|
1233
|
+
if (approvedCap === null) {
|
|
1234
|
+
// Belt-and-suspenders invariant behind the fail-closed branch above:
|
|
1235
|
+
// never let a future condition refactor revive an uncapped editor POST.
|
|
1236
|
+
throw new Error(
|
|
1237
|
+
"Editor autopilot launch requires max_credits or a live quote",
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1119
1240
|
await client.post(
|
|
1120
1241
|
`/editor/${slug}/autopilot`,
|
|
1121
|
-
|
|
1242
|
+
// Use the caller's explicit cap when given, otherwise the live
|
|
1243
|
+
// estimate. The API recomputes under its launch lock and rejects a
|
|
1244
|
+
// price increase before any state change or charge.
|
|
1245
|
+
{ max_credits: approvedCap },
|
|
1122
1246
|
// Fold the create-affecting params AND the failure-state discriminator
|
|
1123
1247
|
// into the start key so it tracks the inputs + current state, not just
|
|
1124
1248
|
// the slug. This key is only compared against a retry of *this* tool on
|
|
@@ -2343,8 +2467,8 @@ registerTool(
|
|
|
2343
2467
|
"FREE re-render: applies the short's current text/style/CTA/end-card state over the already-paid " +
|
|
2344
2468
|
"footage and music — 0 credits. Use after update_short for copy/style/CTA tweaks. Footage or music " +
|
|
2345
2469
|
"changes need generate_short (15 credits) instead. Edits that add/remove the end card (poster, " +
|
|
2346
|
-
"end_card) can shift the video between 12s and 14s; the pinned music bed is
|
|
2347
|
-
"
|
|
2470
|
+
"end_card) can shift the video between 12s and 14s; when the pinned music bed is shorter than the " +
|
|
2471
|
+
"export it crossfades into a repeated continuation, then fades at the export end, without regeneration. " +
|
|
2348
2472
|
"Requires a completed generation to re-render over " +
|
|
2349
2473
|
"(422 short_not_ready otherwise — run generate_short once first); while any generation/render is in " +
|
|
2350
2474
|
"flight it reports in-progress (409) rather than duplicating. Then poll with get_status or " +
|
|
@@ -2817,18 +2941,20 @@ registerTool(
|
|
|
2817
2941
|
registerTool(
|
|
2818
2942
|
"create_editor_ad",
|
|
2819
2943
|
{
|
|
2820
|
-
title: "Create a multi-scene editor ad
|
|
2944
|
+
title: "Create, configure, and price a multi-scene editor ad",
|
|
2821
2945
|
description:
|
|
2822
|
-
"Creates an editor project
|
|
2946
|
+
"Creates an editor project, attaches any supplied local product/logo/closing assets BEFORE generation, " +
|
|
2947
|
+
"and prices autopilot (server-orchestrated " +
|
|
2823
2948
|
"scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " +
|
|
2824
|
-
"
|
|
2825
|
-
"
|
|
2826
|
-
"
|
|
2827
|
-
"
|
|
2828
|
-
"
|
|
2949
|
+
"Without max_credits it STOPS after the free draft/configuration and returns the live estimate. It starts " +
|
|
2950
|
+
"the paid pipeline only when max_credits is supplied and covers the estimate. Then poll with get_status / " +
|
|
2951
|
+
"wait_for_completion (kind=editor). Prefer make_video for a prompt-only one-shot. " +
|
|
2952
|
+
"Supplying assets here is safer than adding them after autopilot because the scenario and scenes are grounded " +
|
|
2953
|
+
"in the real product from the start. (Editor ads carry their copy in-scene/narration, not as a " +
|
|
2829
2954
|
"title overlay; for an on-screen title/subtitle use a short with headline/subheadline instead.) " +
|
|
2830
|
-
"For
|
|
2831
|
-
"
|
|
2955
|
+
"For a mascot or hero product that must stay visually identical with no human presenter, set " +
|
|
2956
|
+
"cast_mode:'product_only' together with product_image_path. Presenter-led casting still belongs on " +
|
|
2957
|
+
"create_editor_draft because it requires a separately configured presenter.",
|
|
2832
2958
|
inputSchema: {
|
|
2833
2959
|
product_prompt: z
|
|
2834
2960
|
.string()
|
|
@@ -2884,6 +3010,61 @@ registerTool(
|
|
|
2884
3010
|
.enum(["9:16", "16:9", "1:1"])
|
|
2885
3011
|
.optional()
|
|
2886
3012
|
.describe('Aspect ratio (default "9:16")'),
|
|
3013
|
+
product_image_path: z
|
|
3014
|
+
.string()
|
|
3015
|
+
.optional()
|
|
3016
|
+
.describe(
|
|
3017
|
+
"Local product image (.jpg/.jpeg/.png, ≤8 MiB) to attach before autopilot.",
|
|
3018
|
+
),
|
|
3019
|
+
product_description: z
|
|
3020
|
+
.string()
|
|
3021
|
+
.max(500)
|
|
3022
|
+
.optional()
|
|
3023
|
+
.describe(
|
|
3024
|
+
"Exact product/character description used with product_image_path.",
|
|
3025
|
+
),
|
|
3026
|
+
closing_image_path: z
|
|
3027
|
+
.string()
|
|
3028
|
+
.optional()
|
|
3029
|
+
.describe("Local closing-card image (.jpg/.jpeg/.png, ≤20 MiB)."),
|
|
3030
|
+
logo_path: z
|
|
3031
|
+
.string()
|
|
3032
|
+
.optional()
|
|
3033
|
+
.describe("Local logo (.jpg/.jpeg/.png, ≤1 MiB) to overlay."),
|
|
3034
|
+
logo_treatment: z
|
|
3035
|
+
.enum(["intro", "outro", "both", "none"])
|
|
3036
|
+
.optional()
|
|
3037
|
+
.describe(
|
|
3038
|
+
'When the logo bumper plays: "intro", "outro", "both" (default), or "none". Requires logo_path.',
|
|
3039
|
+
),
|
|
3040
|
+
logo_position: z
|
|
3041
|
+
.enum(["top-left", "top-right", "bottom-left", "bottom-right"])
|
|
3042
|
+
.optional()
|
|
3043
|
+
.describe(
|
|
3044
|
+
'Logo corner: "top-left", "top-right" (default), "bottom-left", or "bottom-right". Requires logo_path.',
|
|
3045
|
+
),
|
|
3046
|
+
logo_duration_seconds: z
|
|
3047
|
+
.number()
|
|
3048
|
+
.min(0.5)
|
|
3049
|
+
.max(3)
|
|
3050
|
+
.optional()
|
|
3051
|
+
.describe(
|
|
3052
|
+
"Logo bumper duration in seconds, 0.5–3 (default 1.5). Requires logo_path.",
|
|
3053
|
+
),
|
|
3054
|
+
cast_mode: z
|
|
3055
|
+
.literal("product_only")
|
|
3056
|
+
.optional()
|
|
3057
|
+
.describe(
|
|
3058
|
+
"Lock every AI scene to the supplied product/mascot reference with no people. Requires product_image_path.",
|
|
3059
|
+
),
|
|
3060
|
+
max_credits: z
|
|
3061
|
+
.number()
|
|
3062
|
+
.int()
|
|
3063
|
+
.nonnegative()
|
|
3064
|
+
.optional()
|
|
3065
|
+
.describe(
|
|
3066
|
+
"Explicit spend cap. Omit to create/configure/price only; no credits are charged.",
|
|
3067
|
+
),
|
|
2887
3068
|
},
|
|
2888
3069
|
outputSchema: createEditorOutput,
|
|
2889
3070
|
annotations: { title: "Create editor ad", ...WRITE, idempotentHint: true },
|
|
@@ -2900,47 +3081,219 @@ registerTool(
|
|
|
2900
3081
|
theme?: string;
|
|
2901
3082
|
voice_id?: string;
|
|
2902
3083
|
export_aspect_ratio?: string;
|
|
3084
|
+
product_image_path?: string;
|
|
3085
|
+
product_description?: string;
|
|
3086
|
+
closing_image_path?: string;
|
|
3087
|
+
logo_path?: string;
|
|
3088
|
+
logo_treatment?: "intro" | "outro" | "both" | "none";
|
|
3089
|
+
logo_position?:
|
|
3090
|
+
| "top-left"
|
|
3091
|
+
| "top-right"
|
|
3092
|
+
| "bottom-left"
|
|
3093
|
+
| "bottom-right";
|
|
3094
|
+
logo_duration_seconds?: number;
|
|
3095
|
+
cast_mode?: "product_only";
|
|
3096
|
+
max_credits?: number;
|
|
2903
3097
|
},
|
|
2904
3098
|
client,
|
|
2905
3099
|
) => {
|
|
2906
|
-
const
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
3100
|
+
const prepared: PreparedUploadFile[] = [];
|
|
3101
|
+
let productImage: PreparedUploadFile | undefined;
|
|
3102
|
+
let closingImage: PreparedUploadFile | undefined;
|
|
3103
|
+
let logo: PreparedUploadFile | undefined;
|
|
3104
|
+
try {
|
|
3105
|
+
const langErr = validateLanguage(args.language);
|
|
3106
|
+
if (langErr) return fail(langErr);
|
|
3107
|
+
const promptErr = validateProductPrompt(args.product_prompt);
|
|
3108
|
+
if (promptErr) return fail(promptErr);
|
|
3109
|
+
if (args.cast_mode === "product_only" && !args.product_image_path) {
|
|
3110
|
+
return fail(
|
|
3111
|
+
"cast_mode product_only requires product_image_path so identity can be locked before generation.",
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
// Fail fast on settings that would otherwise be silently dropped: the
|
|
3115
|
+
// logo PATCH is gated on logo_path and the product_description is only
|
|
3116
|
+
// sent with product_image_path, so a logo_* / product_description passed
|
|
3117
|
+
// without its asset is a caller mistake. Reject BEFORE creating even a
|
|
3118
|
+
// free draft (consistent with the asset validation just below), rather
|
|
3119
|
+
// than accepting the call and ignoring the field.
|
|
3120
|
+
if (
|
|
3121
|
+
(args.logo_treatment !== undefined ||
|
|
3122
|
+
args.logo_position !== undefined ||
|
|
3123
|
+
args.logo_duration_seconds !== undefined) &&
|
|
3124
|
+
!args.logo_path
|
|
3125
|
+
) {
|
|
3126
|
+
return fail(
|
|
3127
|
+
"logo_treatment/logo_position/logo_duration_seconds require logo_path (the logo image they style). Provide logo_path or drop these settings.",
|
|
3128
|
+
);
|
|
3129
|
+
}
|
|
3130
|
+
if (
|
|
3131
|
+
args.product_description !== undefined &&
|
|
3132
|
+
!args.product_image_path
|
|
3133
|
+
) {
|
|
3134
|
+
return fail(
|
|
3135
|
+
"product_description requires product_image_path (the product image it describes). Provide product_image_path or drop product_description.",
|
|
3136
|
+
);
|
|
3137
|
+
}
|
|
2910
3138
|
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
3139
|
+
// Validate every local asset before creating even a free draft, so a typo,
|
|
3140
|
+
// traversal attempt, bad extension, or oversized logo cannot leave an
|
|
3141
|
+
// orphaned project behind.
|
|
3142
|
+
if (args.product_image_path) {
|
|
3143
|
+
productImage = await prepareImageUpload(
|
|
3144
|
+
args.product_image_path,
|
|
3145
|
+
MAX_PRODUCT_IMAGE_BYTES,
|
|
3146
|
+
);
|
|
3147
|
+
prepared.push(productImage);
|
|
3148
|
+
}
|
|
3149
|
+
if (args.closing_image_path) {
|
|
3150
|
+
closingImage = await prepareImageUpload(
|
|
3151
|
+
args.closing_image_path,
|
|
3152
|
+
MAX_CLOSING_IMAGE_BYTES,
|
|
3153
|
+
);
|
|
3154
|
+
prepared.push(closingImage);
|
|
3155
|
+
}
|
|
3156
|
+
if (args.logo_path) {
|
|
3157
|
+
logo = await prepareImageUpload(args.logo_path, MAX_LOGO_BYTES);
|
|
3158
|
+
prepared.push(logo);
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
const created = await client.post<{ data: { slug: string } }>(
|
|
3162
|
+
"/editor",
|
|
3163
|
+
{
|
|
3164
|
+
language: args.language ?? "en",
|
|
3165
|
+
product_prompt: args.product_prompt,
|
|
3166
|
+
product_subject: args.product_subject,
|
|
3167
|
+
project_intent: args.project_intent,
|
|
3168
|
+
creative_format: args.creative_format,
|
|
3169
|
+
visual_language: args.visual_language,
|
|
3170
|
+
theme: args.theme,
|
|
3171
|
+
voice_id: args.voice_id,
|
|
3172
|
+
export_aspect_ratio: args.export_aspect_ratio,
|
|
3173
|
+
cast_mode: args.cast_mode,
|
|
3174
|
+
},
|
|
3175
|
+
// Fold every create-affecting param into the key so two calls that
|
|
3176
|
+
// differ only by style/theme/voice/aspect get distinct drafts.
|
|
3177
|
+
// Composition lives in core.ts, shared with the golden-pin test.
|
|
3178
|
+
editorAdCreateKey({
|
|
3179
|
+
...args,
|
|
3180
|
+
product_image_identity: productImage
|
|
3181
|
+
? preparedUploadIdentity(productImage)
|
|
3182
|
+
: undefined,
|
|
3183
|
+
closing_image_identity: closingImage
|
|
3184
|
+
? preparedUploadIdentity(closingImage)
|
|
3185
|
+
: undefined,
|
|
3186
|
+
logo_identity: logo ? preparedUploadIdentity(logo) : undefined,
|
|
3187
|
+
}),
|
|
3188
|
+
);
|
|
3189
|
+
const slug = created.data.slug;
|
|
3190
|
+
let configured = {} as Record<string, unknown>;
|
|
3191
|
+
if (
|
|
3192
|
+
args.product_image_path ||
|
|
3193
|
+
args.closing_image_path ||
|
|
3194
|
+
args.logo_path
|
|
3195
|
+
) {
|
|
3196
|
+
const current = await client.get<{ data: unknown }>(
|
|
3197
|
+
`/editor/${slug}`,
|
|
3198
|
+
);
|
|
3199
|
+
configured = asRecord(asRecord(current).data);
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
if (productImage && !configured.product_url) {
|
|
3203
|
+
const { s3_key } = await uploadImageFile(
|
|
3204
|
+
client,
|
|
3205
|
+
`/editor/${slug}/product/presign`,
|
|
3206
|
+
productImage,
|
|
3207
|
+
MAX_PRODUCT_IMAGE_BYTES,
|
|
3208
|
+
);
|
|
3209
|
+
await client.post(`/editor/${slug}/product/confirm`, {
|
|
3210
|
+
s3_key,
|
|
3211
|
+
product_description: args.product_description,
|
|
3212
|
+
});
|
|
3213
|
+
}
|
|
3214
|
+
if (closingImage && !configured.closing_image_url) {
|
|
3215
|
+
const { s3_key } = await uploadImageFile(
|
|
3216
|
+
client,
|
|
3217
|
+
`/editor/${slug}/closing-image/presign`,
|
|
3218
|
+
closingImage,
|
|
3219
|
+
MAX_CLOSING_IMAGE_BYTES,
|
|
3220
|
+
);
|
|
3221
|
+
await client.post(`/editor/${slug}/closing-image/confirm`, {
|
|
3222
|
+
s3_key,
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
3225
|
+
if (logo && !configured.logo_url) {
|
|
3226
|
+
const { s3_key } = await uploadImageFile(
|
|
3227
|
+
client,
|
|
3228
|
+
`/editor/${slug}/logo/presign`,
|
|
3229
|
+
logo,
|
|
3230
|
+
MAX_LOGO_BYTES,
|
|
3231
|
+
);
|
|
3232
|
+
await client.post(`/editor/${slug}/logo/confirm`, { s3_key });
|
|
3233
|
+
}
|
|
3234
|
+
const logoSettings: Record<string, unknown> = {};
|
|
3235
|
+
if (args.logo_treatment !== undefined)
|
|
3236
|
+
logoSettings.logo_treatment = args.logo_treatment;
|
|
3237
|
+
if (args.logo_position !== undefined)
|
|
3238
|
+
logoSettings.logo_position = args.logo_position;
|
|
3239
|
+
if (args.logo_duration_seconds !== undefined)
|
|
3240
|
+
logoSettings.logo_duration_seconds = args.logo_duration_seconds;
|
|
3241
|
+
if (args.logo_path && Object.keys(logoSettings).length > 0)
|
|
3242
|
+
await client.patch(`/editor/${slug}/logo`, logoSettings);
|
|
3243
|
+
|
|
3244
|
+
const quote = await editorAutopilotQuote(client, slug);
|
|
3245
|
+
const overCap =
|
|
3246
|
+
args.max_credits !== undefined &&
|
|
3247
|
+
quote.estimated_credits !== null &&
|
|
3248
|
+
quote.estimated_credits > args.max_credits;
|
|
3249
|
+
if (
|
|
3250
|
+
args.max_credits === undefined ||
|
|
3251
|
+
quote.estimated_credits === null ||
|
|
3252
|
+
overCap ||
|
|
3253
|
+
!quote.affordable
|
|
3254
|
+
) {
|
|
3255
|
+
const note =
|
|
3256
|
+
args.max_credits === undefined
|
|
3257
|
+
? `Free draft configured. Estimated ${quote.estimated_credits ?? "?"} credits (have ${quote.available_credits ?? "?"}); nothing charged. Relaunch with start_autopilot({ slug: "${slug}", max_credits: ${quote.estimated_credits ?? "<approved cap>"} }).`
|
|
3258
|
+
: quote.estimated_credits === null
|
|
3259
|
+
? "Pricing is temporarily unavailable, so the paid pipeline was not started. Retry the quote later."
|
|
3260
|
+
: overCap
|
|
3261
|
+
? `Estimated ${quote.estimated_credits} credits exceeds max_credits ${args.max_credits}; nothing charged.`
|
|
3262
|
+
: `Not enough credits (needs ${quote.estimated_credits}, have ${quote.available_credits ?? "?"}); nothing charged.`;
|
|
3263
|
+
return ok({
|
|
3264
|
+
slug,
|
|
3265
|
+
kind: "editor",
|
|
3266
|
+
...quote,
|
|
3267
|
+
charged: false,
|
|
3268
|
+
note,
|
|
3269
|
+
next: "start_autopilot",
|
|
3270
|
+
});
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
const started = await client.post<{ data: unknown }>(
|
|
3274
|
+
`/editor/${slug}/autopilot`,
|
|
3275
|
+
{ max_credits: args.max_credits },
|
|
3276
|
+
// Fold the create-affecting params into the start key so it tracks
|
|
3277
|
+
// inputs, not just the slug. Self-consistent per-tool only: this key
|
|
3278
|
+
// is compared against retries of *this* tool on *this* slug. The
|
|
3279
|
+
// field set/order differs from make_video / start_autopilot on
|
|
3280
|
+
// purpose (server cache is path+key scoped, slug is unique per
|
|
3281
|
+
// draft), so the keys are not interchangeable across tools.
|
|
3282
|
+
// Composition lives in core.ts, shared with the golden-pin test.
|
|
3283
|
+
editorAdAutopilotKey(slug, args),
|
|
3284
|
+
);
|
|
3285
|
+
const status = normalizeStatus("editor", slug, asRecord(started).data);
|
|
3286
|
+
return ok({
|
|
3287
|
+
slug,
|
|
3288
|
+
kind: "editor",
|
|
3289
|
+
status,
|
|
3290
|
+
...quote,
|
|
3291
|
+
charged: true,
|
|
3292
|
+
next: "wait_for_completion",
|
|
3293
|
+
});
|
|
3294
|
+
} finally {
|
|
3295
|
+
await Promise.all(prepared.map(closePreparedUploadFile));
|
|
3296
|
+
}
|
|
2944
3297
|
},
|
|
2945
3298
|
),
|
|
2946
3299
|
);
|
|
@@ -2951,10 +3304,11 @@ registerTool(
|
|
|
2951
3304
|
title: "Start autopilot on an existing editor draft",
|
|
2952
3305
|
description:
|
|
2953
3306
|
"Starts server-orchestrated autopilot (scenario → segments → narration → voice → music → render) on an " +
|
|
2954
|
-
"EXISTING editor project/draft.
|
|
2955
|
-
"
|
|
3307
|
+
"EXISTING editor project/draft. Without max_credits it returns the live quote and charges nothing. Supply an " +
|
|
3308
|
+
"explicit max_credits cap to authorize launch; the tool refuses if pricing is unavailable, the estimate exceeds " +
|
|
3309
|
+
"the cap, or the balance is insufficient. Each authorized call is a genuine start attempt (like tapping Launch in the app): " +
|
|
2956
3310
|
"if a run is already in progress for this slug the server returns already_running — it never double-starts " +
|
|
2957
|
-
"or double-charges.
|
|
3311
|
+
"or double-charges. Failed or cancelled runs resume normally. For a completed project, set restart:true to quote and build a fresh creative run; omitting it only resumes pending edits. Then poll with " +
|
|
2958
3312
|
'wait_for_completion({ slug, kind: "editor" }).',
|
|
2959
3313
|
inputSchema: {
|
|
2960
3314
|
slug: z.string().describe("Editor project slug"),
|
|
@@ -2977,7 +3331,22 @@ registerTool(
|
|
|
2977
3331
|
.string()
|
|
2978
3332
|
.optional()
|
|
2979
3333
|
.describe("Optional freeform brief to set/replace before launch."),
|
|
3334
|
+
max_credits: z
|
|
3335
|
+
.number()
|
|
3336
|
+
.int()
|
|
3337
|
+
.nonnegative()
|
|
3338
|
+
.optional()
|
|
3339
|
+
.describe(
|
|
3340
|
+
"Explicit spend cap. Omit to quote only; no credits are charged.",
|
|
3341
|
+
),
|
|
3342
|
+
restart: z
|
|
3343
|
+
.boolean()
|
|
3344
|
+
.optional()
|
|
3345
|
+
.describe(
|
|
3346
|
+
"Set true only on a completed project to quote/build a fresh creative run. The delivered timeline stays available until replacement apply.",
|
|
3347
|
+
),
|
|
2980
3348
|
},
|
|
3349
|
+
outputSchema: createEditorOutput,
|
|
2981
3350
|
annotations: { title: "Start autopilot", ...WRITE, idempotentHint: false },
|
|
2982
3351
|
},
|
|
2983
3352
|
tool(
|
|
@@ -2987,6 +3356,8 @@ registerTool(
|
|
|
2987
3356
|
language?: string;
|
|
2988
3357
|
product_subject?: string;
|
|
2989
3358
|
product_prompt?: string;
|
|
3359
|
+
max_credits?: number;
|
|
3360
|
+
restart?: boolean;
|
|
2990
3361
|
},
|
|
2991
3362
|
client,
|
|
2992
3363
|
) => {
|
|
@@ -2996,16 +3367,49 @@ registerTool(
|
|
|
2996
3367
|
if (promptErr) return fail(promptErr);
|
|
2997
3368
|
|
|
2998
3369
|
const slug = args.slug;
|
|
2999
|
-
const overrides:
|
|
3370
|
+
const overrides: {
|
|
3371
|
+
language?: string;
|
|
3372
|
+
product_subject?: string;
|
|
3373
|
+
product_prompt?: string;
|
|
3374
|
+
restart?: boolean;
|
|
3375
|
+
} = {};
|
|
3000
3376
|
if (args.language !== undefined) overrides.language = args.language;
|
|
3001
3377
|
if (args.product_subject !== undefined)
|
|
3002
3378
|
overrides.product_subject = args.product_subject;
|
|
3003
3379
|
if (args.product_prompt !== undefined)
|
|
3004
3380
|
overrides.product_prompt = args.product_prompt;
|
|
3381
|
+
if (args.restart) overrides.restart = true;
|
|
3382
|
+
|
|
3383
|
+
const quote = await editorAutopilotQuote(client, slug, overrides);
|
|
3384
|
+
const overCap =
|
|
3385
|
+
args.max_credits !== undefined &&
|
|
3386
|
+
quote.estimated_credits !== null &&
|
|
3387
|
+
quote.estimated_credits > args.max_credits;
|
|
3388
|
+
if (
|
|
3389
|
+
args.max_credits === undefined ||
|
|
3390
|
+
quote.estimated_credits === null ||
|
|
3391
|
+
overCap ||
|
|
3392
|
+
!quote.affordable
|
|
3393
|
+
) {
|
|
3394
|
+
return ok({
|
|
3395
|
+
slug,
|
|
3396
|
+
kind: "editor",
|
|
3397
|
+
...quote,
|
|
3398
|
+
charged: false,
|
|
3399
|
+
note:
|
|
3400
|
+
args.max_credits === undefined
|
|
3401
|
+
? `Quote only: estimated ${quote.estimated_credits ?? "?"} credits (have ${quote.available_credits ?? "?"}). Pass max_credits to authorize launch.`
|
|
3402
|
+
: quote.estimated_credits === null
|
|
3403
|
+
? "Pricing is unavailable; launch refused because the spend cap cannot be enforced."
|
|
3404
|
+
: overCap
|
|
3405
|
+
? `Estimated ${quote.estimated_credits} credits exceeds max_credits ${args.max_credits}; nothing charged.`
|
|
3406
|
+
: `Not enough credits (needs ${quote.estimated_credits}, have ${quote.available_credits ?? "?"}); nothing charged.`,
|
|
3407
|
+
});
|
|
3408
|
+
}
|
|
3005
3409
|
|
|
3006
3410
|
const started = await client.post<{ data: unknown }>(
|
|
3007
3411
|
`/editor/${slug}/autopilot`,
|
|
3008
|
-
|
|
3412
|
+
{ ...overrides, max_credits: args.max_credits },
|
|
3009
3413
|
// Each call is a genuine new start attempt (matching the in-app Launch
|
|
3010
3414
|
// button). A per-invocation nonce makes the idempotency key unique so the
|
|
3011
3415
|
// server's 24h idempotency cache can't replay a PRIOR start's response on
|
|
@@ -3020,6 +3424,8 @@ registerTool(
|
|
|
3020
3424
|
slug,
|
|
3021
3425
|
kind: "editor",
|
|
3022
3426
|
status,
|
|
3427
|
+
...quote,
|
|
3428
|
+
charged: true,
|
|
3023
3429
|
next: "wait_for_completion",
|
|
3024
3430
|
});
|
|
3025
3431
|
},
|
|
@@ -3087,7 +3493,7 @@ registerTool(
|
|
|
3087
3493
|
title: "Create an editor draft (no autopilot)",
|
|
3088
3494
|
description:
|
|
3089
3495
|
"Creates an editor project from a product prompt and stops (NO autopilot). Costs 0 credits. Returns the " +
|
|
3090
|
-
"slug. Use this to drive the pipeline step by step (vs create_editor_ad which
|
|
3496
|
+
"slug. Use this to drive the pipeline step by step (vs create_editor_ad which configures, quotes, and can launch autopilot with a spend cap). " +
|
|
3091
3497
|
"Each AI-generated scene renders to a fixed 8 seconds (uploaded clips keep their own length). " +
|
|
3092
3498
|
"product_prompt is optional content: send 10–5000 chars, or omit it entirely (empty is treated as omit).",
|
|
3093
3499
|
inputSchema: {
|
|
@@ -3654,7 +4060,15 @@ registerTool(
|
|
|
3654
4060
|
args.prompt !== undefined ? { prompt: args.prompt } : undefined,
|
|
3655
4061
|
undefined,
|
|
3656
4062
|
);
|
|
3657
|
-
return ok(
|
|
4063
|
+
return ok({
|
|
4064
|
+
...asRecord(asRecord(res).data ?? res),
|
|
4065
|
+
charge_receipt: await editorChargeReceipt(
|
|
4066
|
+
client,
|
|
4067
|
+
args.slug,
|
|
4068
|
+
"scene_regeneration",
|
|
4069
|
+
4,
|
|
4070
|
+
),
|
|
4071
|
+
});
|
|
3658
4072
|
},
|
|
3659
4073
|
),
|
|
3660
4074
|
);
|
|
@@ -3863,7 +4277,15 @@ registerTool(
|
|
|
3863
4277
|
{ voice_id: args.voice_id },
|
|
3864
4278
|
await voiceKey(client, args.slug, args.voice_id),
|
|
3865
4279
|
);
|
|
3866
|
-
return ok(
|
|
4280
|
+
return ok({
|
|
4281
|
+
...asRecord(asRecord(res).data ?? res),
|
|
4282
|
+
charge_receipt: await editorChargeReceipt(
|
|
4283
|
+
client,
|
|
4284
|
+
args.slug,
|
|
4285
|
+
"voice_generation",
|
|
4286
|
+
3,
|
|
4287
|
+
),
|
|
4288
|
+
});
|
|
3867
4289
|
}),
|
|
3868
4290
|
);
|
|
3869
4291
|
|
|
@@ -3911,7 +4333,15 @@ registerTool(
|
|
|
3911
4333
|
body,
|
|
3912
4334
|
undefined,
|
|
3913
4335
|
);
|
|
3914
|
-
return ok(
|
|
4336
|
+
return ok({
|
|
4337
|
+
...asRecord(asRecord(res).data ?? res),
|
|
4338
|
+
charge_receipt: await editorChargeReceipt(
|
|
4339
|
+
client,
|
|
4340
|
+
args.slug,
|
|
4341
|
+
"music_generation",
|
|
4342
|
+
5,
|
|
4343
|
+
),
|
|
4344
|
+
});
|
|
3915
4345
|
},
|
|
3916
4346
|
),
|
|
3917
4347
|
);
|
|
@@ -3922,7 +4352,8 @@ registerTool(
|
|
|
3922
4352
|
title: "Render the final editor video (0 credits)",
|
|
3923
4353
|
description:
|
|
3924
4354
|
"Renders the final MP4 from the generated segments + voice + music. Costs 0 credits (any still-ungenerated " +
|
|
3925
|
-
"segments are auto-charged at generation cost first).
|
|
4355
|
+
"segments are auto-charged at generation cost first). Render NEVER generates or charges for voice/music; a new " +
|
|
4356
|
+
"music charge comes only from generate_music or an already-running autopilot child job. Re-rendering reflects the current segments/voice/music (it does not replay a prior render). Returns the latest_render; " +
|
|
3926
4357
|
'pair with wait_for_completion({ slug, kind: "editor" }) then download_result. ' +
|
|
3927
4358
|
"STALENESS: editing the scenario or narration AFTER generating voice/music marks those audio tracks stale, and render then 422s with editor_narration_stale / editor_voice_stale / editor_music_stale — regenerate the named track (generate_narration → generate_voice, and/or generate_music) before rendering.",
|
|
3928
4359
|
inputSchema: { slug: z.string().describe("Editor project slug") },
|
|
@@ -4168,7 +4599,7 @@ registerTool(
|
|
|
4168
4599
|
"duration. Runs presign → PUT (resumable multipart for large files) → confirm, then polls until the " +
|
|
4169
4600
|
"upload is processed (ready). file_path is a LOCAL path confined to HUBFLUENCER_INPUT_DIR (or cwd). Accepted: " +
|
|
4170
4601
|
VIDEO_EXTS.map((e) => `.${e}`).join(", ") +
|
|
4171
|
-
" (≤500 MB, ≤5 min). Costs 0 credits. If add_to_timeline is false (default true) the clip is uploaded but not " +
|
|
4602
|
+
" (≤500 MB decimal = 500,000,000 bytes, ≤5 min). Costs 0 credits. If add_to_timeline is false (default true) the clip is uploaded but not " +
|
|
4172
4603
|
"placed — call add_segment_from_upload later.",
|
|
4173
4604
|
inputSchema: {
|
|
4174
4605
|
slug: z.string().describe("Editor project slug"),
|
|
@@ -4315,7 +4746,8 @@ registerTool(
|
|
|
4315
4746
|
description:
|
|
4316
4747
|
"Sets a specific scene's video from an existing asset — either a ready upload (upload_id) OR a completed scene " +
|
|
4317
4748
|
"in the same project (source_segment_id). Provide EXACTLY ONE. 0 credits. Use it to reuse one uploaded clip " +
|
|
4318
|
-
"across scenes, or to swap a generated scene for your own footage."
|
|
4749
|
+
"across scenes, or to swap a generated scene for your own footage. The response includes continuity impact and " +
|
|
4750
|
+
"current narration/voice/music staleness. If the replacement duration differs, regenerate stale audio before render.",
|
|
4319
4751
|
inputSchema: {
|
|
4320
4752
|
slug: z.string().describe("Editor project slug"),
|
|
4321
4753
|
segment_id: z
|
|
@@ -4358,7 +4790,58 @@ registerTool(
|
|
|
4358
4790
|
`/editor/${args.slug}/segments/${String(args.segment_id)}/use-asset`,
|
|
4359
4791
|
body,
|
|
4360
4792
|
);
|
|
4361
|
-
|
|
4793
|
+
const segment = asRecord(asRecord(res).data ?? res);
|
|
4794
|
+
const impact = asRecord(segment.impact);
|
|
4795
|
+
const staleSegmentIds = Array.isArray(impact.stale_video_segment_ids)
|
|
4796
|
+
? impact.stale_video_segment_ids
|
|
4797
|
+
: [];
|
|
4798
|
+
const requiresRegeneration =
|
|
4799
|
+
impact.requires_regeneration === true || staleSegmentIds.length > 0;
|
|
4800
|
+
// The swap has already SUCCEEDED (the POST above returned). The follow-up
|
|
4801
|
+
// GET only enriches the response with project-level audio staleness — a
|
|
4802
|
+
// transient failure on that read must NOT turn a successful swap into a
|
|
4803
|
+
// thrown error, or the agent would believe the swap failed and retry it,
|
|
4804
|
+
// minting a redundant segment version. Guard it: on failure return success
|
|
4805
|
+
// with the mutation's own impact data and audio-staleness marked unknown
|
|
4806
|
+
// (null) rather than propagating.
|
|
4807
|
+
let timeline: Record<string, unknown>;
|
|
4808
|
+
try {
|
|
4809
|
+
const current = await client.get<{ data: unknown }>(
|
|
4810
|
+
`/editor/${args.slug}`,
|
|
4811
|
+
);
|
|
4812
|
+
const editor = asRecord(asRecord(current).data);
|
|
4813
|
+
timeline = {
|
|
4814
|
+
narration_stale: editor.narration_stale === true,
|
|
4815
|
+
voice_stale: editor.voice_stale === true,
|
|
4816
|
+
music_stale: editor.music_stale === true,
|
|
4817
|
+
stale_segment_ids: staleSegmentIds,
|
|
4818
|
+
requires_regeneration: requiresRegeneration,
|
|
4819
|
+
};
|
|
4820
|
+
} catch {
|
|
4821
|
+
timeline = {
|
|
4822
|
+
// Enrichment GET failed after a successful swap — audio staleness is
|
|
4823
|
+
// unknown (null, not false) so the agent doesn't assume "not stale".
|
|
4824
|
+
narration_stale: null,
|
|
4825
|
+
voice_stale: null,
|
|
4826
|
+
music_stale: null,
|
|
4827
|
+
stale_segment_ids: staleSegmentIds,
|
|
4828
|
+
requires_regeneration: requiresRegeneration,
|
|
4829
|
+
enrichment_unavailable: true,
|
|
4830
|
+
};
|
|
4831
|
+
}
|
|
4832
|
+
return ok({
|
|
4833
|
+
...segment,
|
|
4834
|
+
timeline,
|
|
4835
|
+
next:
|
|
4836
|
+
timeline.requires_regeneration ||
|
|
4837
|
+
timeline.narration_stale ||
|
|
4838
|
+
timeline.voice_stale ||
|
|
4839
|
+
timeline.music_stale
|
|
4840
|
+
? "Regenerate the reported stale scenes/audio before render."
|
|
4841
|
+
: timeline.enrichment_unavailable
|
|
4842
|
+
? "Scene swap succeeded, but audio staleness could not be verified (enrichment fetch failed) — call get_status before render to confirm."
|
|
4843
|
+
: "Timeline is current and can be rendered.",
|
|
4844
|
+
});
|
|
4362
4845
|
},
|
|
4363
4846
|
),
|
|
4364
4847
|
);
|
|
@@ -4370,7 +4853,7 @@ registerTool(
|
|
|
4370
4853
|
description:
|
|
4371
4854
|
"Uploads a local product image and attaches it to the project as the product. Accepted: " +
|
|
4372
4855
|
IMAGE_EXTS.map((e) => `.${e}`).join(", ") +
|
|
4373
|
-
" (≤
|
|
4856
|
+
" (≤8 MiB), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits. The FIRST product added " +
|
|
4374
4857
|
"defaults every pending AI scene to feature it 'throughout' (change with set_product_placement). Optional " +
|
|
4375
4858
|
"description (≤500 chars) guides how it's woven into scenes.",
|
|
4376
4859
|
inputSchema: {
|
|
@@ -4395,6 +4878,7 @@ registerTool(
|
|
|
4395
4878
|
client,
|
|
4396
4879
|
`/editor/${args.slug}/product/presign`,
|
|
4397
4880
|
args.file_path,
|
|
4881
|
+
MAX_PRODUCT_IMAGE_BYTES,
|
|
4398
4882
|
);
|
|
4399
4883
|
const res = await client.post<{ data: unknown }>(
|
|
4400
4884
|
`/editor/${args.slug}/product/confirm`,
|
|
@@ -4437,7 +4921,7 @@ registerTool(
|
|
|
4437
4921
|
{
|
|
4438
4922
|
title: "Set the closing-card image (0 credits)",
|
|
4439
4923
|
description:
|
|
4440
|
-
"Sets the optional ~2s end-card image. Either upload a local file (file_path; .jpg/.jpeg/.png ≤20
|
|
4924
|
+
"Sets the optional ~2s end-card image. Either upload a local file (file_path; .jpg/.jpeg/.png ≤20 MiB, confined " +
|
|
4441
4925
|
"to HUBFLUENCER_INPUT_DIR/cwd) OR reuse the project's product image (from_product:true — a 0-credit server-side " +
|
|
4442
4926
|
"copy, no upload). If a closing image already exists, pass overwrite:true to replace it (else " +
|
|
4443
4927
|
"editor_closing_image_exists).",
|
|
@@ -4488,6 +4972,7 @@ registerTool(
|
|
|
4488
4972
|
client,
|
|
4489
4973
|
`/editor/${args.slug}/closing-image/presign`,
|
|
4490
4974
|
args.file_path,
|
|
4975
|
+
MAX_CLOSING_IMAGE_BYTES,
|
|
4491
4976
|
);
|
|
4492
4977
|
const res = await client.post<{ data: unknown }>(
|
|
4493
4978
|
`/editor/${args.slug}/closing-image/confirm`,
|
|
@@ -4503,7 +4988,7 @@ registerTool(
|
|
|
4503
4988
|
{
|
|
4504
4989
|
title: "Set a brand logo overlay (0 credits)",
|
|
4505
4990
|
description:
|
|
4506
|
-
"Uploads a local brand logo and overlays it on the video. Accepted: .png/.jpg/.jpeg (≤
|
|
4991
|
+
"Uploads a local brand logo and overlays it on the video. Accepted: .png/.jpg/.jpeg (≤1 MiB; PNG keeps " +
|
|
4507
4992
|
"transparency), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. Optional placement controls: treatment, " +
|
|
4508
4993
|
"position, duration_seconds.",
|
|
4509
4994
|
inputSchema: {
|
|
@@ -4533,6 +5018,7 @@ registerTool(
|
|
|
4533
5018
|
client,
|
|
4534
5019
|
`/editor/${args.slug}/logo/presign`,
|
|
4535
5020
|
args.file_path,
|
|
5021
|
+
MAX_LOGO_BYTES,
|
|
4536
5022
|
);
|
|
4537
5023
|
const confirmed = await client.post<{ data: unknown }>(
|
|
4538
5024
|
`/editor/${args.slug}/logo/confirm`,
|
|
@@ -4571,7 +5057,7 @@ registerTool(
|
|
|
4571
5057
|
"Uploads a local product image and attaches it to a SHORT so the product is woven into the footage. " +
|
|
4572
5058
|
"Accepted: " +
|
|
4573
5059
|
IMAGE_EXTS.map((e) => `.${e}`).join(", ") +
|
|
4574
|
-
" (≤20
|
|
5060
|
+
" (≤20 MiB = 20,971,520 bytes), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits. Optional description " +
|
|
4575
5061
|
"(≤500 chars) guides how it's featured. Attach BEFORE generate_short so the product is part of the scenes. " +
|
|
4576
5062
|
"(This is the shorts equivalent of set_product for editor projects.)",
|
|
4577
5063
|
inputSchema: {
|
|
@@ -4618,7 +5104,7 @@ registerTool(
|
|
|
4618
5104
|
"Uploads a local image as the SHORT's end-card poster — a closing still shown at the end (extends the " +
|
|
4619
5105
|
"render from 12s to 14s). Accepted: " +
|
|
4620
5106
|
IMAGE_EXTS.map((e) => `.${e}`).join(", ") +
|
|
4621
|
-
" (≤20
|
|
5107
|
+
" (≤20 MiB = 20,971,520 bytes), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. There is one poster per short; calling " +
|
|
4622
5108
|
"again replaces it. (Shorts have no logo overlay — for a brand logo use an editor project + set_logo.)",
|
|
4623
5109
|
inputSchema: {
|
|
4624
5110
|
slug: z.string().describe("Short slug (from create_short)"),
|
|
@@ -4688,8 +5174,13 @@ registerTool(
|
|
|
4688
5174
|
{
|
|
4689
5175
|
title: "Wait for a render to finish",
|
|
4690
5176
|
description:
|
|
4691
|
-
"Polls status (emitting progress) until terminal
|
|
5177
|
+
"Polls status (emitting progress) until terminal or the wait budget is exhausted. " +
|
|
4692
5178
|
"Generation can take several minutes; if it returns terminal=false, call again to keep waiting. " +
|
|
5179
|
+
"terminal=true does NOT always mean ready=true: an editor can go terminal in a needs-action stage " +
|
|
5180
|
+
'(stage "editing_required" — a delivered render is stale after edits, regenerate the reported scenes/audio; ' +
|
|
5181
|
+
'stage "insufficient_credits" — a batch is parked, top up or reduce scope; ' +
|
|
5182
|
+
'stage "idle" — a draft with nothing generating, start generation first) that only a further tool call ' +
|
|
5183
|
+
"can advance — do NOT just re-poll those. " +
|
|
4693
5184
|
"Pass save_path to download the finished MP4 when it's ready (e.g. to resume a make_video that timed out mid-render). " +
|
|
4694
5185
|
'Works for kind:"slider" too, but a carousel has no MP4 — it just goes terminal when composited; read the slide ' +
|
|
4695
5186
|
"images with get_slider (save_path is ignored for sliders).",
|
|
@@ -6065,7 +6556,7 @@ registerTool(
|
|
|
6065
6556
|
return ok({
|
|
6066
6557
|
...episode,
|
|
6067
6558
|
next: episode.draft_slug
|
|
6068
|
-
? "
|
|
6559
|
+
? "Price this draft with start_autopilot({ slug }); after approval launch it with start_autopilot({ slug, max_credits: approved_cap }). Carousel generation remains a separate paid generate_slider call."
|
|
6069
6560
|
: "Draft created — review it with list_series_episodes.",
|
|
6070
6561
|
});
|
|
6071
6562
|
}),
|
|
@@ -6872,7 +7363,7 @@ registerTool(
|
|
|
6872
7363
|
title: "Upload a local file to your reusable asset catalog",
|
|
6873
7364
|
description:
|
|
6874
7365
|
"Uploads a local IMAGE or VIDEO into your reusable asset catalog so it can be reused across projects. " +
|
|
6875
|
-
`Allowed types: ${CATALOG_ASSET_EXTS.map((e) => `.${e}`).join(", ")} — images ≤20 MiB, videos ≤500 MB. ` +
|
|
7366
|
+
`Allowed types: ${CATALOG_ASSET_EXTS.map((e) => `.${e}`).join(", ")} — images ≤20 MiB (20,971,520 bytes), videos ≤500 MB decimal (500,000,000 bytes). ` +
|
|
6876
7367
|
"Runs presign → PUT → confirm; the confirm HEADs the object and validates size/type, so a mismatch 422s. " +
|
|
6877
7368
|
"For a VIDEO you MUST pass duration_seconds (≤60; the server rejects a video with no/over-long duration). " +
|
|
6878
7369
|
"Spends 0 credits. Scope: video:generate. Rate limit: 30/min. Requires an ACTIVE SUBSCRIPTION (402 " +
|