@hubfluencer/mcp 0.10.0 → 0.12.0
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 +36 -12
- package/dist/index.js +276 -89
- package/package.json +1 -1
- package/src/core.ts +103 -44
- package/src/index.ts +381 -133
- package/src/output-schemas.ts +6 -0
- package/src/uploads.ts +1 -1
package/src/index.ts
CHANGED
|
@@ -171,9 +171,9 @@ async function editorAutopilotQuote(
|
|
|
171
171
|
try {
|
|
172
172
|
const params = new URLSearchParams();
|
|
173
173
|
if (overrides.language) params.set("language", overrides.language);
|
|
174
|
-
if (overrides.product_subject)
|
|
174
|
+
if (overrides.product_subject !== undefined)
|
|
175
175
|
params.set("product_subject", overrides.product_subject);
|
|
176
|
-
if (overrides.product_prompt)
|
|
176
|
+
if (overrides.product_prompt !== undefined)
|
|
177
177
|
params.set("product_prompt", overrides.product_prompt);
|
|
178
178
|
if (overrides.restart) params.set("restart", "true");
|
|
179
179
|
const query = params.toString();
|
|
@@ -620,7 +620,7 @@ CREDITS (spent server-side; each tool prices before charging — see its descrip
|
|
|
620
620
|
- 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.
|
|
621
621
|
|
|
622
622
|
WAITING & RESUMING
|
|
623
|
-
- 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.
|
|
623
|
+
- 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; batch_retry_required = call retry_editor_batch or cancel_editor_batch before starting Autopilot again; insufficient_credits = batch parked, top up / reduce scope; idle = draft with nothing generating, start generation first) — act on those instead of re-polling.
|
|
624
624
|
- 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.
|
|
625
625
|
|
|
626
626
|
LOCAL FILES (sandboxed)
|
|
@@ -683,10 +683,9 @@ async function pollSegmentToTerminal(
|
|
|
683
683
|
slug: string,
|
|
684
684
|
segmentId: string | number,
|
|
685
685
|
extra: ToolExtra,
|
|
686
|
-
|
|
686
|
+
deadline: number,
|
|
687
687
|
intervalMs: number,
|
|
688
688
|
): Promise<{ status: string; error: string | null; timed_out: boolean }> {
|
|
689
|
-
const deadline = Date.now() + budgetMs;
|
|
690
689
|
const sid = String(segmentId);
|
|
691
690
|
const read = async (): Promise<{ status: string; error: string | null }> => {
|
|
692
691
|
// Bound each poll GET's retries by the loop deadline (see fetchStatus).
|
|
@@ -1135,6 +1134,7 @@ registerTool(
|
|
|
1135
1134
|
: `/editor/${slug}/autopilot/cost`;
|
|
1136
1135
|
let estimated_credits: number | null = null;
|
|
1137
1136
|
let available_credits: number | null = null;
|
|
1137
|
+
let authorizedCredits: number | undefined;
|
|
1138
1138
|
try {
|
|
1139
1139
|
const cost = asRecord(
|
|
1140
1140
|
asRecord(await client.get<{ data: unknown }>(costPath)).data,
|
|
@@ -1238,6 +1238,7 @@ registerTool(
|
|
|
1238
1238
|
"Editor autopilot launch requires max_credits or a live quote",
|
|
1239
1239
|
);
|
|
1240
1240
|
}
|
|
1241
|
+
authorizedCredits = approvedCap;
|
|
1241
1242
|
await client.post(
|
|
1242
1243
|
`/editor/${slug}/autopilot`,
|
|
1243
1244
|
// Use the caller's explicit cap when given, otherwise the live
|
|
@@ -1272,12 +1273,50 @@ registerTool(
|
|
|
1272
1273
|
.saved_to;
|
|
1273
1274
|
}
|
|
1274
1275
|
|
|
1276
|
+
// Shorts charge atomically when generation starts. Editor autopilot only
|
|
1277
|
+
// authorizes a cap at launch and spends incrementally as child work starts,
|
|
1278
|
+
// so report observed run spend instead of claiming the whole launch charged.
|
|
1279
|
+
// If this receipt-enrichment read fails, omit the charge claim entirely.
|
|
1280
|
+
let editorCreditsSpent: number | undefined;
|
|
1281
|
+
if (kind === "editor") {
|
|
1282
|
+
try {
|
|
1283
|
+
const current = asRecord(
|
|
1284
|
+
asRecord(
|
|
1285
|
+
await client.get<{ data: unknown }>(
|
|
1286
|
+
`/editor/${slug}`,
|
|
1287
|
+
undefined,
|
|
1288
|
+
Date.now() + 5_000,
|
|
1289
|
+
),
|
|
1290
|
+
).data,
|
|
1291
|
+
);
|
|
1292
|
+
if (typeof current.autopilot_credits_spent === "number") {
|
|
1293
|
+
editorCreditsSpent = current.autopilot_credits_spent;
|
|
1294
|
+
}
|
|
1295
|
+
} catch {
|
|
1296
|
+
// The launch/poll result remains valid without receipt enrichment.
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
const billing =
|
|
1301
|
+
kind === "short"
|
|
1302
|
+
? { charged: true }
|
|
1303
|
+
: {
|
|
1304
|
+
launched: true,
|
|
1305
|
+
authorized_credits: authorizedCredits,
|
|
1306
|
+
...(editorCreditsSpent === undefined
|
|
1307
|
+
? {}
|
|
1308
|
+
: {
|
|
1309
|
+
credits_spent: editorCreditsSpent,
|
|
1310
|
+
charged: editorCreditsSpent > 0,
|
|
1311
|
+
}),
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1275
1314
|
const result = {
|
|
1276
1315
|
...status,
|
|
1277
1316
|
kind_inferred: requestedKind === undefined,
|
|
1278
1317
|
estimated_credits,
|
|
1279
1318
|
available_credits,
|
|
1280
|
-
|
|
1319
|
+
...billing,
|
|
1281
1320
|
saved_to,
|
|
1282
1321
|
timed_out: !status.terminal,
|
|
1283
1322
|
};
|
|
@@ -1297,13 +1336,15 @@ registerTool(
|
|
|
1297
1336
|
note: status.ready
|
|
1298
1337
|
? "Done. Presigned ~24h URL — download promptly."
|
|
1299
1338
|
: status.terminal
|
|
1300
|
-
?
|
|
1301
|
-
|
|
1302
|
-
//
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1339
|
+
? status.stage === "batch_retry_required"
|
|
1340
|
+
? `Terminal (${status.stage}). ${status.error ?? ""} Resolve the paused batch before restarting Autopilot.`.trim()
|
|
1341
|
+
: // A terminal-but-not-ready make_video is normally a failed/cancelled run.
|
|
1342
|
+
// Point at a restart: re-running make_video now genuinely restarts
|
|
1343
|
+
// (the start key folds the failure state, so it no longer replays the
|
|
1344
|
+
// dead 202), or resume the pipeline directly. Fix the cause (often
|
|
1345
|
+
// credits) first.
|
|
1346
|
+
`Terminal (${status.stage}). ${status.error ?? ""}`.trim() +
|
|
1347
|
+
` To restart, fix the cause (often credits) then re-run make_video, or resume with ${resume}.`
|
|
1307
1348
|
: `Still rendering — call wait_for_completion(${resumeArgs}).`,
|
|
1308
1349
|
},
|
|
1309
1350
|
links,
|
|
@@ -2979,12 +3020,12 @@ registerTool(
|
|
|
2979
3020
|
"Without max_credits it STOPS after the free draft/configuration and returns the live estimate. It starts " +
|
|
2980
3021
|
"the paid pipeline only when max_credits is supplied and covers the estimate. Then poll with get_status / " +
|
|
2981
3022
|
"wait_for_completion (kind=editor). Prefer make_video for a prompt-only one-shot. " +
|
|
2982
|
-
"
|
|
2983
|
-
"
|
|
2984
|
-
"
|
|
2985
|
-
"
|
|
2986
|
-
"
|
|
2987
|
-
"
|
|
3023
|
+
"A product image is analyzed into semantic project context and sent as a product-identity reference to every " +
|
|
3024
|
+
"generated Editor scene. The exact photo is also linked as the closing image unless a separate closing image " +
|
|
3025
|
+
"was authored; generated fine label text can still vary. Scene prompts are the complete content contract: describe any people, " +
|
|
3026
|
+
"products, visual continuity, absences, and composition directly in the brief. (Editor ads carry their copy " +
|
|
3027
|
+
"in-scene/narration, not as a title overlay; for an on-screen title/subtitle use a short with " +
|
|
3028
|
+
"headline/subheadline instead.)",
|
|
2988
3029
|
inputSchema: {
|
|
2989
3030
|
product_prompt: z
|
|
2990
3031
|
.string()
|
|
@@ -3044,14 +3085,14 @@ registerTool(
|
|
|
3044
3085
|
.string()
|
|
3045
3086
|
.optional()
|
|
3046
3087
|
.describe(
|
|
3047
|
-
"Local product image (.jpg/.jpeg/.png, ≤8 MiB)
|
|
3088
|
+
"Local product image (.jpg/.jpeg/.png, ≤8 MiB) used as the product-identity reference for every generated Editor scene and linked as the exact closing image unless one was already authored.",
|
|
3048
3089
|
),
|
|
3049
3090
|
product_description: z
|
|
3050
3091
|
.string()
|
|
3051
3092
|
.max(500)
|
|
3052
3093
|
.optional()
|
|
3053
3094
|
.describe(
|
|
3054
|
-
"
|
|
3095
|
+
"Product description used to ground the scenario and narration when product_image_path is supplied.",
|
|
3055
3096
|
),
|
|
3056
3097
|
closing_image_path: z
|
|
3057
3098
|
.string()
|
|
@@ -3060,7 +3101,7 @@ registerTool(
|
|
|
3060
3101
|
logo_path: z
|
|
3061
3102
|
.string()
|
|
3062
3103
|
.optional()
|
|
3063
|
-
.describe("Local logo (.jpg/.jpeg/.png, ≤
|
|
3104
|
+
.describe("Local logo (.jpg/.jpeg/.png, ≤5 MiB) to overlay."),
|
|
3064
3105
|
logo_treatment: z
|
|
3065
3106
|
.enum(["intro", "outro", "both", "none"])
|
|
3066
3107
|
.optional()
|
|
@@ -3081,12 +3122,6 @@ registerTool(
|
|
|
3081
3122
|
.describe(
|
|
3082
3123
|
"Logo bumper duration in seconds, 0.5–3 (default 1.5). Requires logo_path.",
|
|
3083
3124
|
),
|
|
3084
|
-
cast_mode: z
|
|
3085
|
-
.literal("product_only")
|
|
3086
|
-
.optional()
|
|
3087
|
-
.describe(
|
|
3088
|
-
"Lock every AI scene to the supplied product/mascot reference with no people. Requires product_image_path.",
|
|
3089
|
-
),
|
|
3090
3125
|
max_credits: z
|
|
3091
3126
|
.number()
|
|
3092
3127
|
.int()
|
|
@@ -3122,7 +3157,6 @@ registerTool(
|
|
|
3122
3157
|
| "bottom-left"
|
|
3123
3158
|
| "bottom-right";
|
|
3124
3159
|
logo_duration_seconds?: number;
|
|
3125
|
-
cast_mode?: "product_only";
|
|
3126
3160
|
max_credits?: number;
|
|
3127
3161
|
},
|
|
3128
3162
|
client,
|
|
@@ -3136,11 +3170,6 @@ registerTool(
|
|
|
3136
3170
|
if (langErr) return fail(langErr);
|
|
3137
3171
|
const promptErr = validateProductPrompt(args.product_prompt);
|
|
3138
3172
|
if (promptErr) return fail(promptErr);
|
|
3139
|
-
if (args.cast_mode === "product_only" && !args.product_image_path) {
|
|
3140
|
-
return fail(
|
|
3141
|
-
"cast_mode product_only requires product_image_path so identity can be locked before generation.",
|
|
3142
|
-
);
|
|
3143
|
-
}
|
|
3144
3173
|
// Fail fast on settings that would otherwise be silently dropped: the
|
|
3145
3174
|
// logo PATCH is gated on logo_path and the product_description is only
|
|
3146
3175
|
// sent with product_image_path, so a logo_* / product_description passed
|
|
@@ -3200,7 +3229,6 @@ registerTool(
|
|
|
3200
3229
|
theme: args.theme,
|
|
3201
3230
|
voice_id: args.voice_id,
|
|
3202
3231
|
export_aspect_ratio: args.export_aspect_ratio,
|
|
3203
|
-
cast_mode: args.cast_mode,
|
|
3204
3232
|
},
|
|
3205
3233
|
// Fold every create-affecting param into the key so two calls that
|
|
3206
3234
|
// differ only by style/theme/voice/aspect get distinct drafts.
|
|
@@ -3318,7 +3346,10 @@ registerTool(
|
|
|
3318
3346
|
kind: "editor",
|
|
3319
3347
|
status,
|
|
3320
3348
|
...quote,
|
|
3321
|
-
|
|
3349
|
+
launched: true,
|
|
3350
|
+
authorized_credits: args.max_credits,
|
|
3351
|
+
credits_spent: 0,
|
|
3352
|
+
charged: false,
|
|
3322
3353
|
next: "wait_for_completion",
|
|
3323
3354
|
});
|
|
3324
3355
|
} finally {
|
|
@@ -3338,7 +3369,9 @@ registerTool(
|
|
|
3338
3369
|
"explicit max_credits cap to authorize launch; the tool refuses if pricing is unavailable, the estimate exceeds " +
|
|
3339
3370
|
"the cap, or the balance is insufficient. Each authorized call is a genuine start attempt (like tapping Launch in the app): " +
|
|
3340
3371
|
"if a run is already in progress for this slug the server returns already_running — it never double-starts " +
|
|
3341
|
-
"or double-charges. Failed or cancelled runs resume normally
|
|
3372
|
+
"or double-charges. Failed or cancelled runs resume normally, except when status is batch_retry_required: " +
|
|
3373
|
+
"call retry_editor_batch (then wait for that prepaid batch) or cancel_editor_batch first. For a completed project, " +
|
|
3374
|
+
"set restart:true to quote and build a fresh creative run; omitting it only resumes pending edits. Then poll with " +
|
|
3342
3375
|
'wait_for_completion({ slug, kind: "editor" }).',
|
|
3343
3376
|
inputSchema: {
|
|
3344
3377
|
slug: z.string().describe("Editor project slug"),
|
|
@@ -3355,12 +3388,14 @@ registerTool(
|
|
|
3355
3388
|
.optional()
|
|
3356
3389
|
.describe(
|
|
3357
3390
|
"Optional locked product / main subject applied before launch so the AI cannot invent an " +
|
|
3358
|
-
"unrelated product.",
|
|
3391
|
+
"unrelated product. Omit to keep the stored subject; send blank to clear it.",
|
|
3359
3392
|
),
|
|
3360
3393
|
product_prompt: z
|
|
3361
3394
|
.string()
|
|
3362
3395
|
.optional()
|
|
3363
|
-
.describe(
|
|
3396
|
+
.describe(
|
|
3397
|
+
"Optional freeform brief to set/replace before launch. Omit to keep the stored brief; send blank to clear it.",
|
|
3398
|
+
),
|
|
3364
3399
|
max_credits: z
|
|
3365
3400
|
.number()
|
|
3366
3401
|
.int()
|
|
@@ -3455,13 +3490,132 @@ registerTool(
|
|
|
3455
3490
|
kind: "editor",
|
|
3456
3491
|
status,
|
|
3457
3492
|
...quote,
|
|
3458
|
-
|
|
3493
|
+
launched: true,
|
|
3494
|
+
authorized_credits: args.max_credits,
|
|
3495
|
+
credits_spent: 0,
|
|
3496
|
+
charged: false,
|
|
3459
3497
|
next: "wait_for_completion",
|
|
3460
3498
|
});
|
|
3461
3499
|
},
|
|
3462
3500
|
),
|
|
3463
3501
|
);
|
|
3464
3502
|
|
|
3503
|
+
registerTool(
|
|
3504
|
+
"retry_editor_batch",
|
|
3505
|
+
{
|
|
3506
|
+
title: "Retry a paused editor batch",
|
|
3507
|
+
description:
|
|
3508
|
+
"Recovers an editor whose normalized status is batch_retry_required by atomically re-queueing the failed " +
|
|
3509
|
+
"scene and resuming its prepaid batch. This spends 0 additional credits: the batch generation was already " +
|
|
3510
|
+
"paid. It does not authorize or restart a failed parent Autopilot run; after retrying, wait for the batch " +
|
|
3511
|
+
"to settle. If the parent is then terminal, call start_autopilot without max_credits for a fresh quote and " +
|
|
3512
|
+
"relaunch only with an approved cap. A batch parked on " +
|
|
3513
|
+
"insufficient_credits needs a top-up/scope change instead. Scope: video:generate.",
|
|
3514
|
+
inputSchema: { slug: z.string().describe("Editor project slug") },
|
|
3515
|
+
annotations: {
|
|
3516
|
+
title: "Retry editor batch",
|
|
3517
|
+
...WRITE,
|
|
3518
|
+
idempotentHint: false,
|
|
3519
|
+
},
|
|
3520
|
+
},
|
|
3521
|
+
tool(async (args: { slug: string }, client) => {
|
|
3522
|
+
const command = await observedEditorBatchCommand(client, args.slug, [
|
|
3523
|
+
"paused_for_retry",
|
|
3524
|
+
]);
|
|
3525
|
+
const res = await client.post<{ data: unknown }>(
|
|
3526
|
+
`/video-factories/${args.slug}/retry-batch-segment`,
|
|
3527
|
+
command,
|
|
3528
|
+
);
|
|
3529
|
+
return ok({
|
|
3530
|
+
...asRecord(asRecord(res).data ?? res),
|
|
3531
|
+
slug: args.slug,
|
|
3532
|
+
charged: false,
|
|
3533
|
+
note: "The failed scene retry is queued and the prepaid batch is running. Wait for it to settle; if the parent Autopilot run is then terminal, quote start_autopilot again and relaunch only with an approved max_credits cap.",
|
|
3534
|
+
next: "wait_for_completion",
|
|
3535
|
+
});
|
|
3536
|
+
}),
|
|
3537
|
+
);
|
|
3538
|
+
|
|
3539
|
+
registerTool(
|
|
3540
|
+
"cancel_editor_batch",
|
|
3541
|
+
{
|
|
3542
|
+
title: "Cancel an editor batch",
|
|
3543
|
+
description:
|
|
3544
|
+
"Cancels an editor batch only while it is preparing or paused_for_retry so the project can be edited or " +
|
|
3545
|
+
"Autopilot can be started again. Preparing cancellation releases the uncharged preflight. Cancelling a paused " +
|
|
3546
|
+
"batch is destructive: dispatched scene work is stopped and its prepaid credits are not refunded. Spends " +
|
|
3547
|
+
"0 additional credits. Scope: video:generate.",
|
|
3548
|
+
inputSchema: { slug: z.string().describe("Editor project slug") },
|
|
3549
|
+
annotations: {
|
|
3550
|
+
title: "Cancel editor batch",
|
|
3551
|
+
readOnlyHint: false,
|
|
3552
|
+
destructiveHint: true,
|
|
3553
|
+
openWorldHint: true,
|
|
3554
|
+
idempotentHint: false,
|
|
3555
|
+
},
|
|
3556
|
+
},
|
|
3557
|
+
tool(async (args: { slug: string }, client) => {
|
|
3558
|
+
const command = await observedEditorBatchCommand(client, args.slug, [
|
|
3559
|
+
"preparing",
|
|
3560
|
+
"paused_for_retry",
|
|
3561
|
+
]);
|
|
3562
|
+
const res = await client.post<{ data: unknown }>(
|
|
3563
|
+
`/video-factories/${args.slug}/cancel-batch`,
|
|
3564
|
+
command,
|
|
3565
|
+
);
|
|
3566
|
+
return ok({
|
|
3567
|
+
...asRecord(asRecord(res).data ?? res),
|
|
3568
|
+
slug: args.slug,
|
|
3569
|
+
charged: false,
|
|
3570
|
+
next: "get_status",
|
|
3571
|
+
});
|
|
3572
|
+
}),
|
|
3573
|
+
);
|
|
3574
|
+
|
|
3575
|
+
type EditorBatchCommandStatus = "preparing" | "paused_for_retry";
|
|
3576
|
+
|
|
3577
|
+
/**
|
|
3578
|
+
* Reads the exact Editor batch ownership token immediately before issuing a
|
|
3579
|
+
* recovery command. The API validates both fields under the factory row lock,
|
|
3580
|
+
* so a replacement run or phase transition between this GET and the POST is a
|
|
3581
|
+
* safe `batch_command_stale` conflict rather than acting on newer work.
|
|
3582
|
+
*/
|
|
3583
|
+
async function observedEditorBatchCommand(
|
|
3584
|
+
client: HubfluencerClient,
|
|
3585
|
+
slug: string,
|
|
3586
|
+
allowedStatuses: readonly EditorBatchCommandStatus[],
|
|
3587
|
+
): Promise<{
|
|
3588
|
+
expected_batch_run_id: string | null;
|
|
3589
|
+
expected_batch_status: EditorBatchCommandStatus;
|
|
3590
|
+
}> {
|
|
3591
|
+
const response = await client.get<{ data: unknown }>(`/editor/${slug}`);
|
|
3592
|
+
const state = asRecord(asRecord(response).data);
|
|
3593
|
+
const status = state.batch_generation_status;
|
|
3594
|
+
if (
|
|
3595
|
+
typeof status !== "string" ||
|
|
3596
|
+
!allowedStatuses.includes(status as EditorBatchCommandStatus)
|
|
3597
|
+
) {
|
|
3598
|
+
throw new Error(
|
|
3599
|
+
`Editor batch is not in an allowed phase (${allowedStatuses.join(" or ")}); refresh status before retrying.`,
|
|
3600
|
+
);
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
const runId = state.batch_generation_run_id;
|
|
3604
|
+
if (
|
|
3605
|
+
runId !== null &&
|
|
3606
|
+
(typeof runId !== "string" || runId.trim().length === 0)
|
|
3607
|
+
) {
|
|
3608
|
+
throw new Error(
|
|
3609
|
+
"Editor state did not include a valid batch_generation_run_id; refresh status before retrying.",
|
|
3610
|
+
);
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
return {
|
|
3614
|
+
expected_batch_run_id: runId,
|
|
3615
|
+
expected_batch_status: status as EditorBatchCommandStatus,
|
|
3616
|
+
};
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3465
3619
|
// ── AI assists (free daily quota) ───────────────────────────────────────────
|
|
3466
3620
|
|
|
3467
3621
|
registerTool(
|
|
@@ -3564,17 +3718,6 @@ registerTool(
|
|
|
3564
3718
|
.describe(
|
|
3565
3719
|
'Visual theme / genre overlay (default "realistic"). One of: none (no imposed style — segments follow your prompts literally), realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.',
|
|
3566
3720
|
),
|
|
3567
|
-
cast_mode: z
|
|
3568
|
-
.enum(["product_only", "presenter_led"])
|
|
3569
|
-
.optional()
|
|
3570
|
-
.describe(
|
|
3571
|
-
"Who/what anchors every AI scene. product_only = a product-led ad with NO people; requires a " +
|
|
3572
|
-
"product image (set_product) before generation and generates every scene from persistent product " +
|
|
3573
|
-
"reference images — the best product fidelity. presenter_led = one locked presenter appears in " +
|
|
3574
|
-
"every scene; requires a presenter to be configured first (in the app / REST presenter endpoints " +
|
|
3575
|
-
"— no MCP tool yet), else generation fails with presenter_required. Omit for the default auto " +
|
|
3576
|
-
"behavior.",
|
|
3577
|
-
),
|
|
3578
3721
|
voice_id: z
|
|
3579
3722
|
.string()
|
|
3580
3723
|
.regex(/^[A-Za-z0-9_-]+$/)
|
|
@@ -3611,7 +3754,6 @@ registerTool(
|
|
|
3611
3754
|
creative_format?: string;
|
|
3612
3755
|
visual_language?: string;
|
|
3613
3756
|
theme?: string;
|
|
3614
|
-
cast_mode?: string;
|
|
3615
3757
|
voice_id?: string;
|
|
3616
3758
|
export_aspect_ratio?: string;
|
|
3617
3759
|
project_intent?: string;
|
|
@@ -3625,7 +3767,6 @@ registerTool(
|
|
|
3625
3767
|
creative_format: args.creative_format,
|
|
3626
3768
|
visual_language: args.visual_language,
|
|
3627
3769
|
theme: args.theme,
|
|
3628
|
-
cast_mode: args.cast_mode,
|
|
3629
3770
|
voice_id: args.voice_id,
|
|
3630
3771
|
export_aspect_ratio: args.export_aspect_ratio,
|
|
3631
3772
|
project_intent: args.project_intent,
|
|
@@ -3648,7 +3789,6 @@ registerTool(
|
|
|
3648
3789
|
args.theme ?? "",
|
|
3649
3790
|
args.voice_id ?? "",
|
|
3650
3791
|
args.export_aspect_ratio ?? "",
|
|
3651
|
-
args.cast_mode ?? "",
|
|
3652
3792
|
),
|
|
3653
3793
|
);
|
|
3654
3794
|
return ok({
|
|
@@ -3660,21 +3800,81 @@ registerTool(
|
|
|
3660
3800
|
),
|
|
3661
3801
|
);
|
|
3662
3802
|
|
|
3803
|
+
function conciseEditorState(value: unknown): Record<string, unknown> {
|
|
3804
|
+
const data = asRecord(value);
|
|
3805
|
+
const keys = [
|
|
3806
|
+
"slug",
|
|
3807
|
+
"status",
|
|
3808
|
+
"autopilot_status",
|
|
3809
|
+
"autopilot_current_step",
|
|
3810
|
+
"autopilot_error_step",
|
|
3811
|
+
"autopilot_error_code",
|
|
3812
|
+
"autopilot_error_message",
|
|
3813
|
+
"autopilot_max_credits",
|
|
3814
|
+
"autopilot_credits_spent",
|
|
3815
|
+
"scenario_status",
|
|
3816
|
+
"scenario_apply_status",
|
|
3817
|
+
"scenario_prompt",
|
|
3818
|
+
"batch_generation_status",
|
|
3819
|
+
"batch_generation_run_id",
|
|
3820
|
+
"auto_render_after_batch",
|
|
3821
|
+
"narration_status",
|
|
3822
|
+
"narration_script",
|
|
3823
|
+
"narration_stale",
|
|
3824
|
+
"voice_stale",
|
|
3825
|
+
"music_stale",
|
|
3826
|
+
"segments_count",
|
|
3827
|
+
"current_audio",
|
|
3828
|
+
"current_music",
|
|
3829
|
+
"latest_render",
|
|
3830
|
+
"project_spend",
|
|
3831
|
+
"ai_assist_quota",
|
|
3832
|
+
] as const;
|
|
3833
|
+
const result: Record<string, unknown> = {};
|
|
3834
|
+
for (const key of keys) {
|
|
3835
|
+
if (key in data) result[key] = data[key];
|
|
3836
|
+
}
|
|
3837
|
+
result.segments = Array.isArray(data.segments)
|
|
3838
|
+
? (data.segments as Record<string, unknown>[]).map((segment) => ({
|
|
3839
|
+
id: segment.id,
|
|
3840
|
+
position: segment.position,
|
|
3841
|
+
prompt: segment.prompt,
|
|
3842
|
+
status: segment.status,
|
|
3843
|
+
source_type: segment.source_type,
|
|
3844
|
+
use_previous_frame: segment.use_previous_frame,
|
|
3845
|
+
video_stale: segment.video_stale,
|
|
3846
|
+
error_code: segment.error_code,
|
|
3847
|
+
error_message: segment.error_message,
|
|
3848
|
+
narration_text: segment.narration_text,
|
|
3849
|
+
}))
|
|
3850
|
+
: [];
|
|
3851
|
+
return result;
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3663
3854
|
registerTool(
|
|
3664
3855
|
"get_editor",
|
|
3665
3856
|
{
|
|
3666
|
-
title: "Get
|
|
3857
|
+
title: "Get editor state (review step)",
|
|
3667
3858
|
description:
|
|
3668
|
-
"Returns the full editor
|
|
3669
|
-
|
|
3670
|
-
"
|
|
3671
|
-
|
|
3859
|
+
"Returns the full editor state by default for backward compatibility. Pass response_format:'concise' for a smaller review projection containing orchestration, scenario, scene prompts/status, narration_status, current_audio, current_music, latest_render, batch_generation_status, spend, and quota.",
|
|
3860
|
+
inputSchema: {
|
|
3861
|
+
slug: z.string().describe("Editor project slug"),
|
|
3862
|
+
response_format: z.enum(["concise", "full"]).default("full"),
|
|
3863
|
+
},
|
|
3672
3864
|
annotations: { title: "Get editor", ...RO },
|
|
3673
3865
|
},
|
|
3674
|
-
tool(
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3866
|
+
tool(
|
|
3867
|
+
async (
|
|
3868
|
+
args: { slug: string; response_format: "concise" | "full" },
|
|
3869
|
+
client,
|
|
3870
|
+
) => {
|
|
3871
|
+
const res = await client.get<{ data: unknown }>(`/editor/${args.slug}`);
|
|
3872
|
+
const data = asRecord(res).data;
|
|
3873
|
+
return ok(
|
|
3874
|
+
args.response_format === "full" ? data : conciseEditorState(data),
|
|
3875
|
+
);
|
|
3876
|
+
},
|
|
3877
|
+
),
|
|
3678
3878
|
);
|
|
3679
3879
|
|
|
3680
3880
|
registerTool(
|
|
@@ -3726,16 +3926,16 @@ registerTool(
|
|
|
3726
3926
|
.optional()
|
|
3727
3927
|
.describe("How many scenes (3–10, server default 5)"),
|
|
3728
3928
|
creative_format: z
|
|
3729
|
-
.enum(CREATIVE_FORMATS)
|
|
3929
|
+
.union([z.enum(CREATIVE_FORMATS), z.literal("")])
|
|
3730
3930
|
.optional()
|
|
3731
3931
|
.describe(
|
|
3732
3932
|
"Persist the narrative arc / segment structure before generating. Omit to leave it unchanged; pass an empty string to clear.",
|
|
3733
3933
|
),
|
|
3734
3934
|
visual_language: z
|
|
3735
|
-
.enum(VISUAL_LANGUAGES)
|
|
3935
|
+
.union([z.enum(VISUAL_LANGUAGES), z.literal("")])
|
|
3736
3936
|
.optional()
|
|
3737
3937
|
.describe(
|
|
3738
|
-
"Persist the visual style / render look before generating. When set it drives the look and a theme of 'none' is ignored.",
|
|
3938
|
+
"Persist the visual style / render look before generating. Omit to leave it unchanged; pass an empty string to clear. When set it drives the look and a theme of 'none' is ignored.",
|
|
3739
3939
|
),
|
|
3740
3940
|
theme: z
|
|
3741
3941
|
.string()
|
|
@@ -4123,20 +4323,30 @@ registerTool(
|
|
|
4123
4323
|
},
|
|
4124
4324
|
},
|
|
4125
4325
|
tool(async (args: { slug: string }, client, extra) => {
|
|
4126
|
-
|
|
4326
|
+
// One absolute deadline owns the entire read/submit/poll/reconcile path.
|
|
4327
|
+
// Start it before the initial retrying GET so no preparatory request sits
|
|
4328
|
+
// outside the advertised ~280s tool budget.
|
|
4329
|
+
const overallDeadline = Date.now() + 280_000;
|
|
4330
|
+
const res = await client.get<{ data: unknown }>(
|
|
4331
|
+
`/editor/${args.slug}`,
|
|
4332
|
+
undefined,
|
|
4333
|
+
overallDeadline,
|
|
4334
|
+
);
|
|
4127
4335
|
const data = asRecord(asRecord(res).data);
|
|
4128
4336
|
const segments = Array.isArray(data.segments)
|
|
4129
4337
|
? (data.segments as Record<string, unknown>[])
|
|
4130
4338
|
: [];
|
|
4131
|
-
const
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
st
|
|
4135
|
-
st
|
|
4136
|
-
|
|
4137
|
-
|
|
4339
|
+
const unfinished = segments
|
|
4340
|
+
.filter((s) => {
|
|
4341
|
+
if (s.source_type === "upload") return false;
|
|
4342
|
+
const st = (s.status as string) ?? (s.generation_status as string);
|
|
4343
|
+
return st !== "completed";
|
|
4344
|
+
})
|
|
4345
|
+
.sort(
|
|
4346
|
+
(a, b) =>
|
|
4347
|
+
Number(a.position ?? Number.MAX_SAFE_INTEGER) -
|
|
4348
|
+
Number(b.position ?? Number.MAX_SAFE_INTEGER),
|
|
4138
4349
|
);
|
|
4139
|
-
});
|
|
4140
4350
|
|
|
4141
4351
|
// Overall wall-clock budget: cap the whole call at the same 280s ceiling the
|
|
4142
4352
|
// other wait loops use (under a typical 300s client timeout). Without it a
|
|
@@ -4144,20 +4354,23 @@ registerTool(
|
|
|
4144
4354
|
// hours) in a single call. When the budget runs out mid-batch we stop and
|
|
4145
4355
|
// return the standard resume contract — call again and it naturally continues
|
|
4146
4356
|
// with whatever scenes are still pending (completed ones are skipped).
|
|
4147
|
-
|
|
4148
|
-
//
|
|
4149
|
-
//
|
|
4150
|
-
|
|
4357
|
+
// Every scene poll receives this same absolute deadline, so time already spent
|
|
4358
|
+
// on earlier reads, submissions, and scenes is never granted again.
|
|
4359
|
+
// Reserve a full non-idempotent POST timeout plus one final status read before
|
|
4360
|
+
// starting another paid scene. POSTs deliberately aren't aborted/retried, so
|
|
4361
|
+
// beginning one in the last seconds of the budget would make a client timeout
|
|
4362
|
+
// ambiguous after the server may already have accepted the charge.
|
|
4363
|
+
const SUBMIT_HEADROOM_MS = 61_000;
|
|
4151
4364
|
|
|
4152
4365
|
const generated: Array<string | number> = [];
|
|
4153
4366
|
let n = 0;
|
|
4154
|
-
for (const seg of
|
|
4367
|
+
for (const seg of unfinished) {
|
|
4155
4368
|
const id = seg.id;
|
|
4156
4369
|
if (id === undefined) continue;
|
|
4157
4370
|
const sid = id as string | number;
|
|
4158
4371
|
// Stop before submitting another scene once the overall budget is spent, so
|
|
4159
4372
|
// the call returns instead of charging + waiting past the deadline.
|
|
4160
|
-
|
|
4373
|
+
let remainingMs = overallDeadline - Date.now();
|
|
4161
4374
|
if (remainingMs <= 0) {
|
|
4162
4375
|
return ok({
|
|
4163
4376
|
slug: args.slug,
|
|
@@ -4169,22 +4382,45 @@ registerTool(
|
|
|
4169
4382
|
await reportProgress(
|
|
4170
4383
|
extra,
|
|
4171
4384
|
++n,
|
|
4172
|
-
`generating segment ${String(sid)} (${n}/${
|
|
4173
|
-
|
|
4385
|
+
`generating segment ${String(sid)} (${n}/${unfinished.length})`,
|
|
4386
|
+
unfinished.length,
|
|
4174
4387
|
);
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4388
|
+
const initialStatus =
|
|
4389
|
+
(seg.status as string) ?? (seg.generation_status as string);
|
|
4390
|
+
if (initialStatus !== "processing") {
|
|
4391
|
+
remainingMs = overallDeadline - Date.now();
|
|
4392
|
+
if (remainingMs < SUBMIT_HEADROOM_MS) {
|
|
4393
|
+
return ok({
|
|
4394
|
+
slug: args.slug,
|
|
4395
|
+
generated,
|
|
4396
|
+
timed_out: true,
|
|
4397
|
+
note: "Wall-clock budget is too low to safely start another paid scene. Re-run generate_all_segments to continue; completed scenes are skipped.",
|
|
4398
|
+
});
|
|
4399
|
+
}
|
|
4400
|
+
try {
|
|
4401
|
+
await client.post(
|
|
4402
|
+
`/editor/${args.slug}/segments/${String(sid)}/generate`,
|
|
4403
|
+
undefined,
|
|
4404
|
+
undefined,
|
|
4405
|
+
);
|
|
4406
|
+
} catch (e) {
|
|
4407
|
+
return ok({
|
|
4408
|
+
slug: args.slug,
|
|
4409
|
+
generated,
|
|
4410
|
+
stopped_at: sid,
|
|
4411
|
+
error: errMessage(e),
|
|
4412
|
+
note: "Stopped on first error — fix the cause (often credits) and re-run; completed scenes are skipped, failed/pending are retried.",
|
|
4413
|
+
});
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
remainingMs = overallDeadline - Date.now();
|
|
4417
|
+
if (remainingMs <= 0) {
|
|
4182
4418
|
return ok({
|
|
4183
4419
|
slug: args.slug,
|
|
4184
4420
|
generated,
|
|
4185
4421
|
stopped_at: sid,
|
|
4186
|
-
|
|
4187
|
-
note: "
|
|
4422
|
+
timed_out: true,
|
|
4423
|
+
note: "The scene was submitted, but the wall-clock budget ended before its status could be checked. Re-run generate_all_segments later to continue safely.",
|
|
4188
4424
|
});
|
|
4189
4425
|
}
|
|
4190
4426
|
// Gate on THIS segment finishing before starting the next, so the next
|
|
@@ -4196,7 +4432,7 @@ registerTool(
|
|
|
4196
4432
|
args.slug,
|
|
4197
4433
|
sid,
|
|
4198
4434
|
extra,
|
|
4199
|
-
|
|
4435
|
+
overallDeadline,
|
|
4200
4436
|
15000,
|
|
4201
4437
|
);
|
|
4202
4438
|
if (seg_result.status === "failed") {
|
|
@@ -4217,7 +4453,45 @@ registerTool(
|
|
|
4217
4453
|
note: "A scene is still rendering after the poll budget. Re-run generate_all_segments later to continue (it retries failed/pending, skips completed), or watch it with get_editor.",
|
|
4218
4454
|
});
|
|
4219
4455
|
}
|
|
4220
|
-
generated.push(sid);
|
|
4456
|
+
if (initialStatus !== "processing") generated.push(sid);
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
// Reconcile against a fresh authoritative snapshot. A scene may have been
|
|
4460
|
+
// processing when this call started, or a completion webhook may still be
|
|
4461
|
+
// landing; never claim "all generated" while any AI scene is unfinished.
|
|
4462
|
+
if (overallDeadline - Date.now() < 1_000) {
|
|
4463
|
+
return ok({
|
|
4464
|
+
slug: args.slug,
|
|
4465
|
+
generated,
|
|
4466
|
+
timed_out: true,
|
|
4467
|
+
note: "Wall-clock budget reached before the final reconciliation read. Re-run generate_all_segments to confirm completion; completed scenes are skipped.",
|
|
4468
|
+
});
|
|
4469
|
+
}
|
|
4470
|
+
const finalRes = await client.get<{ data: unknown }>(
|
|
4471
|
+
`/editor/${args.slug}`,
|
|
4472
|
+
undefined,
|
|
4473
|
+
overallDeadline,
|
|
4474
|
+
);
|
|
4475
|
+
const finalData = asRecord(asRecord(finalRes).data);
|
|
4476
|
+
const finalSegments = Array.isArray(finalData.segments)
|
|
4477
|
+
? (finalData.segments as Record<string, unknown>[])
|
|
4478
|
+
: [];
|
|
4479
|
+
const stillUnfinished = finalSegments.filter((segment) => {
|
|
4480
|
+
if (segment.source_type === "upload") return false;
|
|
4481
|
+
const status =
|
|
4482
|
+
(segment.status as string) ?? (segment.generation_status as string);
|
|
4483
|
+
return status !== "completed";
|
|
4484
|
+
});
|
|
4485
|
+
if (stillUnfinished.length > 0) {
|
|
4486
|
+
return ok({
|
|
4487
|
+
slug: args.slug,
|
|
4488
|
+
generated,
|
|
4489
|
+
timed_out: true,
|
|
4490
|
+
in_flight_segment_ids: stillUnfinished
|
|
4491
|
+
.map((segment) => segment.id)
|
|
4492
|
+
.filter((id) => id !== undefined),
|
|
4493
|
+
note: "Some scenes are still processing. Re-run generate_all_segments later; it will wait for in-flight scenes and generate only unfinished ones.",
|
|
4494
|
+
});
|
|
4221
4495
|
}
|
|
4222
4496
|
return ok({
|
|
4223
4497
|
slug: args.slug,
|
|
@@ -4233,15 +4507,15 @@ registerTool(
|
|
|
4233
4507
|
{
|
|
4234
4508
|
title: "Set the narration script",
|
|
4235
4509
|
description:
|
|
4236
|
-
"Writes your own narration script (1–
|
|
4510
|
+
"Writes your own narration script (1–10000 chars) instead of generating one. Free — no credits, no AI " +
|
|
4237
4511
|
"assist. Use this in place of generate_narration when you want to control the voiceover text. Changing the narration after generating voice/music marks them stale — regenerate (generate_voice / generate_music) before render.",
|
|
4238
4512
|
inputSchema: {
|
|
4239
4513
|
slug: z.string().describe("Editor project slug"),
|
|
4240
4514
|
script: z
|
|
4241
4515
|
.string()
|
|
4242
4516
|
.min(1)
|
|
4243
|
-
.max(
|
|
4244
|
-
.describe("Narration / voiceover text (1–
|
|
4517
|
+
.max(10000)
|
|
4518
|
+
.describe("Narration / voiceover text (1–10000 chars)"),
|
|
4245
4519
|
},
|
|
4246
4520
|
annotations: { title: "Set narration", ...WRITE, idempotentHint: true },
|
|
4247
4521
|
},
|
|
@@ -4258,7 +4532,7 @@ registerTool(
|
|
|
4258
4532
|
{
|
|
4259
4533
|
title: "Generate the narration (AI assist)",
|
|
4260
4534
|
description:
|
|
4261
|
-
"Generates
|
|
4535
|
+
"Generates one coherent narration script with AI from the complete timeline. Every current scene must be completed with a known positive duration, and at least one scene must not keep original upload audio. CONSUMES 1 AI ASSIST (free daily quota) only when a fresh job is queued; an exact retry is deduplicated without consuming another assist — " +
|
|
4262
4536
|
"it is free of CREDITS but NOT free of quota. On 429 set auto_unlock:true (1 credit → +10 assists, " +
|
|
4263
4537
|
"retried once) or write the script yourself with set_narration_script.",
|
|
4264
4538
|
inputSchema: {
|
|
@@ -4290,7 +4564,7 @@ registerTool(
|
|
|
4290
4564
|
title: "Generate the voiceover (3 credits)",
|
|
4291
4565
|
description:
|
|
4292
4566
|
"Synthesizes the voiceover from the narration script. Costs 3 credits. Safe to retry; re-call after changing the narration or voice to regenerate." +
|
|
4293
|
-
"voice_id is REQUIRED — pick one from list_voices (≤64 chars, [A-Za-z0-9_-] only). 402 fails cleanly. Editing the scenario/narration/timeline after this marks the voice stale, and render will 422 (editor_voice_stale) until you re-run generate_voice.",
|
|
4567
|
+
" All current scenes must be completed with known durations first. voice_id is REQUIRED — pick one from list_voices (≤64 chars, [A-Za-z0-9_-] only). 402 fails cleanly. Editing the scenario/narration/timeline after this marks the voice stale, and render will 422 (editor_voice_stale) until you re-run generate_voice.",
|
|
4294
4568
|
inputSchema: {
|
|
4295
4569
|
slug: z.string().describe("Editor project slug"),
|
|
4296
4570
|
voice_id: z
|
|
@@ -4529,9 +4803,10 @@ registerTool(
|
|
|
4529
4803
|
{
|
|
4530
4804
|
title: "List renders for an editor project",
|
|
4531
4805
|
description:
|
|
4532
|
-
"Lists every render version for an editor project: {id, version, status, video_url,
|
|
4533
|
-
"error_message, inserted_at}. Use it to find a
|
|
4534
|
-
"(presigned, ~24h).
|
|
4806
|
+
"Lists every render version for an editor project: {id, version, status, video_url, thumbnail_url, " +
|
|
4807
|
+
"duration_seconds, error_message, retryable, combination_hash, is_stale, inserted_at}. Use it to find a " +
|
|
4808
|
+
"FAILED render to retry, or to recover a finished video_url (presigned, ~24h). status is pending | " +
|
|
4809
|
+
"processing | completed | failed; is_stale null means unknown, not current. Read-only.",
|
|
4535
4810
|
inputSchema: { slug: z.string().describe("Editor project slug") },
|
|
4536
4811
|
annotations: { title: "List renders", ...RO },
|
|
4537
4812
|
},
|
|
@@ -4883,9 +5158,9 @@ registerTool(
|
|
|
4883
5158
|
description:
|
|
4884
5159
|
"Uploads a local product image and attaches it to the project as the product. Accepted: " +
|
|
4885
5160
|
IMAGE_EXTS.map((e) => `.${e}`).join(", ") +
|
|
4886
|
-
" (≤8 MiB), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits.
|
|
4887
|
-
"
|
|
4888
|
-
"
|
|
5161
|
+
" (≤8 MiB), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits. Its pixels guide product identity " +
|
|
5162
|
+
"in every generated Editor scene and its confirmed description grounds scenario and narration. The exact photo " +
|
|
5163
|
+
"is linked as the closing image unless a separate closing image was authored; generated fine label text can still vary.",
|
|
4889
5164
|
inputSchema: {
|
|
4890
5165
|
slug: z.string().describe("Editor project slug"),
|
|
4891
5166
|
file_path: z
|
|
@@ -4919,33 +5194,6 @@ registerTool(
|
|
|
4919
5194
|
),
|
|
4920
5195
|
);
|
|
4921
5196
|
|
|
4922
|
-
registerTool(
|
|
4923
|
-
"set_product_placement",
|
|
4924
|
-
{
|
|
4925
|
-
title: "Set default product placement on pending scenes (0 credits)",
|
|
4926
|
-
description:
|
|
4927
|
-
"Sets where the product appears across pending (never-generated) AI scenes. mode is 'throughout' (every scene) " +
|
|
4928
|
-
"or 'end' (closing scene only; requires a first+last-frame capable model). 0 credits. Add a product first with " +
|
|
4929
|
-
"set_product. (There is no 'none' — to remove the product, that's a separate action.)",
|
|
4930
|
-
inputSchema: {
|
|
4931
|
-
slug: z.string().describe("Editor project slug"),
|
|
4932
|
-
mode: z.enum(["throughout", "end"]).describe("'throughout' or 'end'"),
|
|
4933
|
-
},
|
|
4934
|
-
annotations: {
|
|
4935
|
-
title: "Set product placement",
|
|
4936
|
-
...WRITE,
|
|
4937
|
-
idempotentHint: true,
|
|
4938
|
-
},
|
|
4939
|
-
},
|
|
4940
|
-
tool(async (args: { slug: string; mode: "throughout" | "end" }, client) => {
|
|
4941
|
-
const res = await client.post<{ data: unknown }>(
|
|
4942
|
-
`/editor/${args.slug}/product/apply-default-placement`,
|
|
4943
|
-
{ placement: args.mode },
|
|
4944
|
-
);
|
|
4945
|
-
return ok(asRecord(res).data ?? res);
|
|
4946
|
-
}),
|
|
4947
|
-
);
|
|
4948
|
-
|
|
4949
5197
|
registerTool(
|
|
4950
5198
|
"set_closing_image",
|
|
4951
5199
|
{
|
|
@@ -5018,7 +5266,7 @@ registerTool(
|
|
|
5018
5266
|
{
|
|
5019
5267
|
title: "Set a brand logo overlay (0 credits)",
|
|
5020
5268
|
description:
|
|
5021
|
-
"Uploads a local brand logo and overlays it on the video. Accepted: .png/.jpg/.jpeg (≤
|
|
5269
|
+
"Uploads a local brand logo and overlays it on the video. Accepted: .png/.jpg/.jpeg (≤5 MiB; PNG keeps " +
|
|
5022
5270
|
"transparency), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. Optional placement controls: treatment, " +
|
|
5023
5271
|
"position, duration_seconds.",
|
|
5024
5272
|
inputSchema: {
|