@hubfluencer/mcp 0.11.0 → 0.13.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 +37 -10
- package/dist/index.js +160 -64
- package/package.json +1 -1
- package/src/core.ts +17 -8
- package/src/index.ts +277 -105
- package/src/output-schemas.ts +8 -0
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();
|
|
@@ -565,6 +565,25 @@ const VISUAL_LANGUAGES = [
|
|
|
565
565
|
"startup_explainer",
|
|
566
566
|
"luxury_minimal",
|
|
567
567
|
] as const;
|
|
568
|
+
// Closed API enum from VideoFactories.ScenarioThemes. Keeping this explicit in
|
|
569
|
+
// MCP prevents an agent typo from spending a round trip on an avoidable 422.
|
|
570
|
+
const SCENARIO_THEMES = [
|
|
571
|
+
"none",
|
|
572
|
+
"realistic",
|
|
573
|
+
"cinematic",
|
|
574
|
+
"anime",
|
|
575
|
+
"sci_fi",
|
|
576
|
+
"fantasy",
|
|
577
|
+
"noir",
|
|
578
|
+
"superhero",
|
|
579
|
+
"horror",
|
|
580
|
+
"mockumentary",
|
|
581
|
+
"sports",
|
|
582
|
+
"gaming",
|
|
583
|
+
"retro_80s",
|
|
584
|
+
"minimalist",
|
|
585
|
+
"cyberpunk",
|
|
586
|
+
] as const;
|
|
568
587
|
// The languages the API's VideoFactory.language accepts (video_factory.ex
|
|
569
588
|
// @supported_languages, validate_inclusion at create + autopilot). It's a CLOSED
|
|
570
589
|
// set of bare ISO-639-1 codes — the server rejects region/script subtags ("en",
|
|
@@ -914,12 +933,12 @@ registerTool(
|
|
|
914
933
|
"SHORTS + EDITOR: visual style direction and render look. Default app choice is kinetic_creator.",
|
|
915
934
|
),
|
|
916
935
|
theme: z
|
|
917
|
-
.
|
|
936
|
+
.enum(SCENARIO_THEMES)
|
|
918
937
|
.optional()
|
|
919
938
|
.describe(
|
|
920
939
|
"Visual theme: editor genre overlay, and legacy shorts fallback only when visual_language is unset. " +
|
|
921
|
-
"For editor,
|
|
922
|
-
"
|
|
940
|
+
"For editor, visual_language drives the look when present; make_video defaults theme to none " +
|
|
941
|
+
"(literal — no imposed genre style). Options: none, realistic, cinematic, " +
|
|
923
942
|
"anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, " +
|
|
924
943
|
"minimalist, cyberpunk",
|
|
925
944
|
),
|
|
@@ -1115,12 +1134,14 @@ registerTool(
|
|
|
1115
1134
|
voice_id: args.voice_id,
|
|
1116
1135
|
creative_format: args.creative_format,
|
|
1117
1136
|
visual_language: args.visual_language,
|
|
1118
|
-
|
|
1137
|
+
// Prompt-first default: do not silently inject the API's legacy
|
|
1138
|
+
// realistic/neutral art direction when the caller authored a look.
|
|
1139
|
+
theme: args.theme ?? "none",
|
|
1119
1140
|
},
|
|
1120
1141
|
// Fold every create-affecting param into the key so two calls that
|
|
1121
1142
|
// differ only by style/theme/voice/aspect get distinct drafts (shared
|
|
1122
1143
|
// with the golden-pin test via core.ts).
|
|
1123
|
-
makeVideoEditorCreateKey(args),
|
|
1144
|
+
makeVideoEditorCreateKey({ ...args, theme: args.theme ?? "none" }),
|
|
1124
1145
|
);
|
|
1125
1146
|
slug = created.data.slug;
|
|
1126
1147
|
}
|
|
@@ -1530,7 +1551,7 @@ registerTool(
|
|
|
1530
1551
|
"Visual language for Veo direction and render styling. Good default: kinetic_creator.",
|
|
1531
1552
|
),
|
|
1532
1553
|
theme: z
|
|
1533
|
-
.
|
|
1554
|
+
.enum(SCENARIO_THEMES)
|
|
1534
1555
|
.optional()
|
|
1535
1556
|
.describe(
|
|
1536
1557
|
"Deprecated for shorts: legacy visual theme used only when visual_language is unset.",
|
|
@@ -1837,7 +1858,7 @@ registerTool(
|
|
|
1837
1858
|
.optional()
|
|
1838
1859
|
.describe("Visual language for Veo direction and render styling."),
|
|
1839
1860
|
theme: z
|
|
1840
|
-
.
|
|
1861
|
+
.enum(SCENARIO_THEMES)
|
|
1841
1862
|
.optional()
|
|
1842
1863
|
.describe(
|
|
1843
1864
|
"Deprecated for shorts: legacy visual theme used only when visual_language is unset.",
|
|
@@ -2529,6 +2550,55 @@ registerTool(
|
|
|
2529
2550
|
}),
|
|
2530
2551
|
);
|
|
2531
2552
|
|
|
2553
|
+
registerTool(
|
|
2554
|
+
"cancel_short_generation",
|
|
2555
|
+
{
|
|
2556
|
+
title: "Cancel a running short generation",
|
|
2557
|
+
description:
|
|
2558
|
+
"Stops the caller-observed paid Short attempt, fences late provider callbacks, and refunds the 15-credit " +
|
|
2559
|
+
"generation exactly once. Pass generation_attempt from get_status so a delayed command cannot cancel a " +
|
|
2560
|
+
"newer run on the same draft. The draft is preserved for editing and regeneration. Safe to repeat with " +
|
|
2561
|
+
"the same attempt after a successful cancellation. Scope: video:generate.",
|
|
2562
|
+
inputSchema: {
|
|
2563
|
+
slug: z.string().describe("Slug of the processing short"),
|
|
2564
|
+
generation_attempt: z
|
|
2565
|
+
.number()
|
|
2566
|
+
.int()
|
|
2567
|
+
.min(0)
|
|
2568
|
+
.describe("generation_attempt from the latest get_status response"),
|
|
2569
|
+
},
|
|
2570
|
+
annotations: {
|
|
2571
|
+
title: "Cancel short generation",
|
|
2572
|
+
readOnlyHint: false,
|
|
2573
|
+
destructiveHint: true,
|
|
2574
|
+
openWorldHint: true,
|
|
2575
|
+
idempotentHint: true,
|
|
2576
|
+
},
|
|
2577
|
+
},
|
|
2578
|
+
tool(async (args: { slug: string; generation_attempt: number }, client) => {
|
|
2579
|
+
const res = await client.post<{ data: unknown }>(
|
|
2580
|
+
`/shorts/${args.slug}/cancel`,
|
|
2581
|
+
{ generation_attempt: args.generation_attempt },
|
|
2582
|
+
undefined,
|
|
2583
|
+
);
|
|
2584
|
+
const data = asRecord(asRecord(res).data ?? res);
|
|
2585
|
+
return ok({
|
|
2586
|
+
...data,
|
|
2587
|
+
slug: args.slug,
|
|
2588
|
+
charged: false,
|
|
2589
|
+
// The cancel endpoint returns the short's STATE, not a refund receipt,
|
|
2590
|
+
// so don't assert refunded:true on every call — a repeat cancel is a
|
|
2591
|
+
// no-op that mints no new credits. Echo the server's own flag if it ever
|
|
2592
|
+
// sends one; otherwise describe the honest idempotent settlement.
|
|
2593
|
+
refund:
|
|
2594
|
+
"refunded" in data
|
|
2595
|
+
? data.refunded
|
|
2596
|
+
: "settled (idempotent — no new credits minted on repeat)",
|
|
2597
|
+
next: "Edit the preserved draft, then call generate_short when ready.",
|
|
2598
|
+
});
|
|
2599
|
+
}),
|
|
2600
|
+
);
|
|
2601
|
+
|
|
2532
2602
|
registerTool(
|
|
2533
2603
|
"rerender_short",
|
|
2534
2604
|
{
|
|
@@ -3020,12 +3090,12 @@ registerTool(
|
|
|
3020
3090
|
"Without max_credits it STOPS after the free draft/configuration and returns the live estimate. It starts " +
|
|
3021
3091
|
"the paid pipeline only when max_credits is supplied and covers the estimate. Then poll with get_status / " +
|
|
3022
3092
|
"wait_for_completion (kind=editor). Prefer make_video for a prompt-only one-shot. " +
|
|
3023
|
-
"
|
|
3024
|
-
"
|
|
3025
|
-
"
|
|
3026
|
-
"
|
|
3027
|
-
"
|
|
3028
|
-
"
|
|
3093
|
+
"A product image is analyzed into semantic project context and sent as a product-identity reference to every " +
|
|
3094
|
+
"generated Editor scene. The exact photo is also linked as the closing image unless a separate closing image " +
|
|
3095
|
+
"was authored; generated fine label text can still vary. Scene prompts are the complete content contract: describe any people, " +
|
|
3096
|
+
"products, visual continuity, absences, and composition directly in the brief. (Editor ads carry their copy " +
|
|
3097
|
+
"in-scene/narration, not as a title overlay; for an on-screen title/subtitle use a short with " +
|
|
3098
|
+
"headline/subheadline instead.)",
|
|
3029
3099
|
inputSchema: {
|
|
3030
3100
|
product_prompt: z
|
|
3031
3101
|
.string()
|
|
@@ -3066,10 +3136,10 @@ registerTool(
|
|
|
3066
3136
|
"Visual style direction and render look. Good default: kinetic_creator. When set it drives the look and a theme of 'none' is ignored.",
|
|
3067
3137
|
),
|
|
3068
3138
|
theme: z
|
|
3069
|
-
.
|
|
3139
|
+
.enum(SCENARIO_THEMES)
|
|
3070
3140
|
.optional()
|
|
3071
3141
|
.describe(
|
|
3072
|
-
'
|
|
3142
|
+
'Optional visual theme / genre overlay (default "none", so scene prompts drive the look). One of: none, realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.',
|
|
3073
3143
|
),
|
|
3074
3144
|
voice_id: z
|
|
3075
3145
|
.string()
|
|
@@ -3085,14 +3155,14 @@ registerTool(
|
|
|
3085
3155
|
.string()
|
|
3086
3156
|
.optional()
|
|
3087
3157
|
.describe(
|
|
3088
|
-
"Local product image (.jpg/.jpeg/.png, ≤8 MiB)
|
|
3158
|
+
"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.",
|
|
3089
3159
|
),
|
|
3090
3160
|
product_description: z
|
|
3091
3161
|
.string()
|
|
3092
3162
|
.max(500)
|
|
3093
3163
|
.optional()
|
|
3094
3164
|
.describe(
|
|
3095
|
-
"
|
|
3165
|
+
"Product description used to ground the scenario and narration when product_image_path is supplied.",
|
|
3096
3166
|
),
|
|
3097
3167
|
closing_image_path: z
|
|
3098
3168
|
.string()
|
|
@@ -3122,12 +3192,6 @@ registerTool(
|
|
|
3122
3192
|
.describe(
|
|
3123
3193
|
"Logo bumper duration in seconds, 0.5–3 (default 1.5). Requires logo_path.",
|
|
3124
3194
|
),
|
|
3125
|
-
cast_mode: z
|
|
3126
|
-
.literal("product_only")
|
|
3127
|
-
.optional()
|
|
3128
|
-
.describe(
|
|
3129
|
-
"Lock every AI scene to the supplied product/mascot reference with no people. Requires product_image_path.",
|
|
3130
|
-
),
|
|
3131
3195
|
max_credits: z
|
|
3132
3196
|
.number()
|
|
3133
3197
|
.int()
|
|
@@ -3163,7 +3227,6 @@ registerTool(
|
|
|
3163
3227
|
| "bottom-left"
|
|
3164
3228
|
| "bottom-right";
|
|
3165
3229
|
logo_duration_seconds?: number;
|
|
3166
|
-
cast_mode?: "product_only";
|
|
3167
3230
|
max_credits?: number;
|
|
3168
3231
|
},
|
|
3169
3232
|
client,
|
|
@@ -3177,11 +3240,6 @@ registerTool(
|
|
|
3177
3240
|
if (langErr) return fail(langErr);
|
|
3178
3241
|
const promptErr = validateProductPrompt(args.product_prompt);
|
|
3179
3242
|
if (promptErr) return fail(promptErr);
|
|
3180
|
-
if (args.cast_mode === "product_only" && !args.product_image_path) {
|
|
3181
|
-
return fail(
|
|
3182
|
-
"cast_mode product_only requires product_image_path so identity can be locked before generation.",
|
|
3183
|
-
);
|
|
3184
|
-
}
|
|
3185
3243
|
// Fail fast on settings that would otherwise be silently dropped: the
|
|
3186
3244
|
// logo PATCH is gated on logo_path and the product_description is only
|
|
3187
3245
|
// sent with product_image_path, so a logo_* / product_description passed
|
|
@@ -3238,16 +3296,16 @@ registerTool(
|
|
|
3238
3296
|
project_intent: args.project_intent,
|
|
3239
3297
|
creative_format: args.creative_format,
|
|
3240
3298
|
visual_language: args.visual_language,
|
|
3241
|
-
theme: args.theme,
|
|
3299
|
+
theme: args.theme ?? "none",
|
|
3242
3300
|
voice_id: args.voice_id,
|
|
3243
3301
|
export_aspect_ratio: args.export_aspect_ratio,
|
|
3244
|
-
cast_mode: args.cast_mode,
|
|
3245
3302
|
},
|
|
3246
3303
|
// Fold every create-affecting param into the key so two calls that
|
|
3247
3304
|
// differ only by style/theme/voice/aspect get distinct drafts.
|
|
3248
3305
|
// Composition lives in core.ts, shared with the golden-pin test.
|
|
3249
3306
|
editorAdCreateKey({
|
|
3250
3307
|
...args,
|
|
3308
|
+
theme: args.theme ?? "none",
|
|
3251
3309
|
product_image_identity: productImage
|
|
3252
3310
|
? preparedUploadIdentity(productImage)
|
|
3253
3311
|
: undefined,
|
|
@@ -3401,12 +3459,14 @@ registerTool(
|
|
|
3401
3459
|
.optional()
|
|
3402
3460
|
.describe(
|
|
3403
3461
|
"Optional locked product / main subject applied before launch so the AI cannot invent an " +
|
|
3404
|
-
"unrelated product.",
|
|
3462
|
+
"unrelated product. Omit to keep the stored subject; send blank to clear it.",
|
|
3405
3463
|
),
|
|
3406
3464
|
product_prompt: z
|
|
3407
3465
|
.string()
|
|
3408
3466
|
.optional()
|
|
3409
|
-
.describe(
|
|
3467
|
+
.describe(
|
|
3468
|
+
"Optional freeform brief to set/replace before launch. Omit to keep the stored brief; send blank to clear it.",
|
|
3469
|
+
),
|
|
3410
3470
|
max_credits: z
|
|
3411
3471
|
.number()
|
|
3412
3472
|
.int()
|
|
@@ -3518,7 +3578,12 @@ registerTool(
|
|
|
3518
3578
|
description:
|
|
3519
3579
|
"Recovers an editor whose normalized status is batch_retry_required by atomically re-queueing the failed " +
|
|
3520
3580
|
"scene and resuming its prepaid batch. This spends 0 additional credits: the batch generation was already " +
|
|
3521
|
-
"paid.
|
|
3581
|
+
"paid. Before retrying you may REPAIR the one failed scene: while paused_for_retry, set_segment_prompt can " +
|
|
3582
|
+
"edit that scene's prompt (and use_previous_frame) — but only for the exact failed position the server " +
|
|
3583
|
+
"names in batch_generation_failed_position (from get_editor); the rest of the paused batch stays " +
|
|
3584
|
+
"mutation-locked. Explicit paused retries are capped — once the failure code is free_retries_exhausted a " +
|
|
3585
|
+
"further retry is rejected (start a fresh run instead). It does not authorize or restart a failed parent " +
|
|
3586
|
+
"Autopilot run; after retrying, wait for the batch " +
|
|
3522
3587
|
"to settle. If the parent is then terminal, call start_autopilot without max_credits for a fresh quote and " +
|
|
3523
3588
|
"relaunch only with an approved cap. A batch parked on " +
|
|
3524
3589
|
"insufficient_credits needs a top-up/scope change instead. Scope: video:generate.",
|
|
@@ -3530,8 +3595,12 @@ registerTool(
|
|
|
3530
3595
|
},
|
|
3531
3596
|
},
|
|
3532
3597
|
tool(async (args: { slug: string }, client) => {
|
|
3598
|
+
const command = await observedEditorBatchCommand(client, args.slug, [
|
|
3599
|
+
"paused_for_retry",
|
|
3600
|
+
]);
|
|
3533
3601
|
const res = await client.post<{ data: unknown }>(
|
|
3534
3602
|
`/video-factories/${args.slug}/retry-batch-segment`,
|
|
3603
|
+
command,
|
|
3535
3604
|
);
|
|
3536
3605
|
return ok({
|
|
3537
3606
|
...asRecord(asRecord(res).data ?? res),
|
|
@@ -3548,9 +3617,9 @@ registerTool(
|
|
|
3548
3617
|
{
|
|
3549
3618
|
title: "Cancel an editor batch",
|
|
3550
3619
|
description:
|
|
3551
|
-
"Cancels an editor batch
|
|
3552
|
-
"Autopilot can be started again.
|
|
3553
|
-
"
|
|
3620
|
+
"Cancels an editor batch only while it is preparing or paused_for_retry so the project can be edited or " +
|
|
3621
|
+
"Autopilot can be started again. Preparing cancellation releases the uncharged preflight. Cancelling a paused " +
|
|
3622
|
+
"batch is destructive: dispatched scene work is stopped and its prepaid credits are not refunded. Spends " +
|
|
3554
3623
|
"0 additional credits. Scope: video:generate.",
|
|
3555
3624
|
inputSchema: { slug: z.string().describe("Editor project slug") },
|
|
3556
3625
|
annotations: {
|
|
@@ -3562,8 +3631,13 @@ registerTool(
|
|
|
3562
3631
|
},
|
|
3563
3632
|
},
|
|
3564
3633
|
tool(async (args: { slug: string }, client) => {
|
|
3634
|
+
const command = await observedEditorBatchCommand(client, args.slug, [
|
|
3635
|
+
"preparing",
|
|
3636
|
+
"paused_for_retry",
|
|
3637
|
+
]);
|
|
3565
3638
|
const res = await client.post<{ data: unknown }>(
|
|
3566
3639
|
`/video-factories/${args.slug}/cancel-batch`,
|
|
3640
|
+
command,
|
|
3567
3641
|
);
|
|
3568
3642
|
return ok({
|
|
3569
3643
|
...asRecord(asRecord(res).data ?? res),
|
|
@@ -3574,6 +3648,50 @@ registerTool(
|
|
|
3574
3648
|
}),
|
|
3575
3649
|
);
|
|
3576
3650
|
|
|
3651
|
+
type EditorBatchCommandStatus = "preparing" | "paused_for_retry";
|
|
3652
|
+
|
|
3653
|
+
/**
|
|
3654
|
+
* Reads the exact Editor batch ownership token immediately before issuing a
|
|
3655
|
+
* recovery command. The API validates both fields under the factory row lock,
|
|
3656
|
+
* so a replacement run or phase transition between this GET and the POST is a
|
|
3657
|
+
* safe `batch_command_stale` conflict rather than acting on newer work.
|
|
3658
|
+
*/
|
|
3659
|
+
async function observedEditorBatchCommand(
|
|
3660
|
+
client: HubfluencerClient,
|
|
3661
|
+
slug: string,
|
|
3662
|
+
allowedStatuses: readonly EditorBatchCommandStatus[],
|
|
3663
|
+
): Promise<{
|
|
3664
|
+
expected_batch_run_id: string | null;
|
|
3665
|
+
expected_batch_status: EditorBatchCommandStatus;
|
|
3666
|
+
}> {
|
|
3667
|
+
const response = await client.get<{ data: unknown }>(`/editor/${slug}`);
|
|
3668
|
+
const state = asRecord(asRecord(response).data);
|
|
3669
|
+
const status = state.batch_generation_status;
|
|
3670
|
+
if (
|
|
3671
|
+
typeof status !== "string" ||
|
|
3672
|
+
!allowedStatuses.includes(status as EditorBatchCommandStatus)
|
|
3673
|
+
) {
|
|
3674
|
+
throw new Error(
|
|
3675
|
+
`Editor batch is not in an allowed phase (${allowedStatuses.join(" or ")}); refresh status before retrying.`,
|
|
3676
|
+
);
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
const runId = state.batch_generation_run_id;
|
|
3680
|
+
if (
|
|
3681
|
+
runId !== null &&
|
|
3682
|
+
(typeof runId !== "string" || runId.trim().length === 0)
|
|
3683
|
+
) {
|
|
3684
|
+
throw new Error(
|
|
3685
|
+
"Editor state did not include a valid batch_generation_run_id; refresh status before retrying.",
|
|
3686
|
+
);
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
return {
|
|
3690
|
+
expected_batch_run_id: runId,
|
|
3691
|
+
expected_batch_status: status as EditorBatchCommandStatus,
|
|
3692
|
+
};
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3577
3695
|
// ── AI assists (free daily quota) ───────────────────────────────────────────
|
|
3578
3696
|
|
|
3579
3697
|
registerTool(
|
|
@@ -3671,21 +3789,10 @@ registerTool(
|
|
|
3671
3789
|
"Visual style direction and render look. Good default: kinetic_creator. When set it drives the look and a theme of 'none' is ignored.",
|
|
3672
3790
|
),
|
|
3673
3791
|
theme: z
|
|
3674
|
-
.
|
|
3675
|
-
.optional()
|
|
3676
|
-
.describe(
|
|
3677
|
-
'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.',
|
|
3678
|
-
),
|
|
3679
|
-
cast_mode: z
|
|
3680
|
-
.enum(["product_only", "presenter_led"])
|
|
3792
|
+
.enum(SCENARIO_THEMES)
|
|
3681
3793
|
.optional()
|
|
3682
3794
|
.describe(
|
|
3683
|
-
|
|
3684
|
-
"product image (set_product) before generation and generates every scene from persistent product " +
|
|
3685
|
-
"reference images — the best product fidelity. presenter_led = one locked presenter appears in " +
|
|
3686
|
-
"every scene; requires a presenter to be configured first (in the app / REST presenter endpoints " +
|
|
3687
|
-
"— no MCP tool yet), else generation fails with presenter_required. Omit for the default auto " +
|
|
3688
|
-
"behavior.",
|
|
3795
|
+
'Optional visual theme / genre overlay (default "none", so scene prompts drive the look). One of: none, realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.',
|
|
3689
3796
|
),
|
|
3690
3797
|
voice_id: z
|
|
3691
3798
|
.string()
|
|
@@ -3723,7 +3830,6 @@ registerTool(
|
|
|
3723
3830
|
creative_format?: string;
|
|
3724
3831
|
visual_language?: string;
|
|
3725
3832
|
theme?: string;
|
|
3726
|
-
cast_mode?: string;
|
|
3727
3833
|
voice_id?: string;
|
|
3728
3834
|
export_aspect_ratio?: string;
|
|
3729
3835
|
project_intent?: string;
|
|
@@ -3736,8 +3842,7 @@ registerTool(
|
|
|
3736
3842
|
product_subject: args.product_subject,
|
|
3737
3843
|
creative_format: args.creative_format,
|
|
3738
3844
|
visual_language: args.visual_language,
|
|
3739
|
-
theme: args.theme,
|
|
3740
|
-
cast_mode: args.cast_mode,
|
|
3845
|
+
theme: args.theme ?? "none",
|
|
3741
3846
|
voice_id: args.voice_id,
|
|
3742
3847
|
export_aspect_ratio: args.export_aspect_ratio,
|
|
3743
3848
|
project_intent: args.project_intent,
|
|
@@ -3757,10 +3862,9 @@ registerTool(
|
|
|
3757
3862
|
args.language ?? "en",
|
|
3758
3863
|
args.creative_format ?? "",
|
|
3759
3864
|
args.visual_language ?? "",
|
|
3760
|
-
args.theme ?? "",
|
|
3865
|
+
args.theme ?? "none",
|
|
3761
3866
|
args.voice_id ?? "",
|
|
3762
3867
|
args.export_aspect_ratio ?? "",
|
|
3763
|
-
args.cast_mode ?? "",
|
|
3764
3868
|
),
|
|
3765
3869
|
);
|
|
3766
3870
|
return ok({
|
|
@@ -3788,6 +3892,9 @@ function conciseEditorState(value: unknown): Record<string, unknown> {
|
|
|
3788
3892
|
"scenario_apply_status",
|
|
3789
3893
|
"scenario_prompt",
|
|
3790
3894
|
"batch_generation_status",
|
|
3895
|
+
"batch_generation_run_id",
|
|
3896
|
+
"batch_generation_failed_position",
|
|
3897
|
+
"auto_render_after_batch",
|
|
3791
3898
|
"narration_status",
|
|
3792
3899
|
"narration_script",
|
|
3793
3900
|
"narration_stale",
|
|
@@ -3811,9 +3918,8 @@ function conciseEditorState(value: unknown): Record<string, unknown> {
|
|
|
3811
3918
|
prompt: segment.prompt,
|
|
3812
3919
|
status: segment.status,
|
|
3813
3920
|
source_type: segment.source_type,
|
|
3921
|
+
generation_claim_token: segment.generation_claim_token,
|
|
3814
3922
|
use_previous_frame: segment.use_previous_frame,
|
|
3815
|
-
protagonist_presence: segment.protagonist_presence,
|
|
3816
|
-
cast_mode_snapshot: segment.cast_mode_snapshot,
|
|
3817
3923
|
video_stale: segment.video_stale,
|
|
3818
3924
|
error_code: segment.error_code,
|
|
3819
3925
|
error_message: segment.error_message,
|
|
@@ -3828,7 +3934,7 @@ registerTool(
|
|
|
3828
3934
|
{
|
|
3829
3935
|
title: "Get editor state (review step)",
|
|
3830
3936
|
description:
|
|
3831
|
-
"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,
|
|
3937
|
+
"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.",
|
|
3832
3938
|
inputSchema: {
|
|
3833
3939
|
slug: z.string().describe("Editor project slug"),
|
|
3834
3940
|
response_format: z.enum(["concise", "full"]).default("full"),
|
|
@@ -3898,19 +4004,19 @@ registerTool(
|
|
|
3898
4004
|
.optional()
|
|
3899
4005
|
.describe("How many scenes (3–10, server default 5)"),
|
|
3900
4006
|
creative_format: z
|
|
3901
|
-
.enum(CREATIVE_FORMATS)
|
|
4007
|
+
.union([z.enum(CREATIVE_FORMATS), z.literal("")])
|
|
3902
4008
|
.optional()
|
|
3903
4009
|
.describe(
|
|
3904
4010
|
"Persist the narrative arc / segment structure before generating. Omit to leave it unchanged; pass an empty string to clear.",
|
|
3905
4011
|
),
|
|
3906
4012
|
visual_language: z
|
|
3907
|
-
.enum(VISUAL_LANGUAGES)
|
|
4013
|
+
.union([z.enum(VISUAL_LANGUAGES), z.literal("")])
|
|
3908
4014
|
.optional()
|
|
3909
4015
|
.describe(
|
|
3910
|
-
"Persist the visual style / render look before generating. When set it drives the look and a theme of 'none' is ignored.",
|
|
4016
|
+
"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.",
|
|
3911
4017
|
),
|
|
3912
4018
|
theme: z
|
|
3913
|
-
.
|
|
4019
|
+
.enum(SCENARIO_THEMES)
|
|
3914
4020
|
.optional()
|
|
3915
4021
|
.describe(
|
|
3916
4022
|
"Persist the genre overlay / theme before generating (e.g. realistic, cinematic, anime, none).",
|
|
@@ -4219,6 +4325,96 @@ registerTool(
|
|
|
4219
4325
|
}),
|
|
4220
4326
|
);
|
|
4221
4327
|
|
|
4328
|
+
registerTool(
|
|
4329
|
+
"cancel_segment_generation",
|
|
4330
|
+
{
|
|
4331
|
+
title: "Cancel one editor scene generation",
|
|
4332
|
+
description:
|
|
4333
|
+
"Stops one caller-observed, individually processing Editor scene; its generation_claim_token fence " +
|
|
4334
|
+
"prevents a delayed command from cancelling a newer attempt. The token is returned by get_editor. " +
|
|
4335
|
+
"Refunds that scene's charge idempotently (a free retry creates no refund). Use cancel_editor_batch or " +
|
|
4336
|
+
"cancel_autopilot for workflow-owned scenes. After cancellation, edit the prompt if needed and call " +
|
|
4337
|
+
"generate_segment again. Scope: video:generate.",
|
|
4338
|
+
inputSchema: {
|
|
4339
|
+
slug: z.string().describe("Editor project slug"),
|
|
4340
|
+
segment_id: z
|
|
4341
|
+
.union([z.number(), z.string()])
|
|
4342
|
+
.describe("Processing segment id (from get_editor)"),
|
|
4343
|
+
generation_claim_token: z
|
|
4344
|
+
.string()
|
|
4345
|
+
.nullable()
|
|
4346
|
+
.describe("generation_claim_token from that processing segment"),
|
|
4347
|
+
},
|
|
4348
|
+
annotations: {
|
|
4349
|
+
title: "Cancel segment generation",
|
|
4350
|
+
readOnlyHint: false,
|
|
4351
|
+
destructiveHint: true,
|
|
4352
|
+
openWorldHint: true,
|
|
4353
|
+
idempotentHint: true,
|
|
4354
|
+
},
|
|
4355
|
+
},
|
|
4356
|
+
tool(
|
|
4357
|
+
async (
|
|
4358
|
+
args: {
|
|
4359
|
+
slug: string;
|
|
4360
|
+
segment_id: number | string;
|
|
4361
|
+
generation_claim_token: string | null;
|
|
4362
|
+
},
|
|
4363
|
+
client,
|
|
4364
|
+
) => {
|
|
4365
|
+
const res = await client.post<{ data: unknown }>(
|
|
4366
|
+
`/editor/${args.slug}/segments/${String(args.segment_id)}/cancel`,
|
|
4367
|
+
{ expected_generation_claim_token: args.generation_claim_token },
|
|
4368
|
+
undefined,
|
|
4369
|
+
);
|
|
4370
|
+
return ok({
|
|
4371
|
+
...asRecord(asRecord(res).data ?? res),
|
|
4372
|
+
slug: args.slug,
|
|
4373
|
+
charged: false,
|
|
4374
|
+
next: "set_segment_prompt or generate_segment",
|
|
4375
|
+
});
|
|
4376
|
+
},
|
|
4377
|
+
),
|
|
4378
|
+
);
|
|
4379
|
+
|
|
4380
|
+
registerTool(
|
|
4381
|
+
"cancel_autopilot",
|
|
4382
|
+
{
|
|
4383
|
+
title: "Cancel a running Autopilot run",
|
|
4384
|
+
description:
|
|
4385
|
+
"Stops an in-flight Autopilot run on an editor project: sets autopilot_status to cancelled so the " +
|
|
4386
|
+
"orchestration chain (scenario → scenes → narration → voice → music → render) advances no further. Use " +
|
|
4387
|
+
"this when Autopilot is still running and you want to halt it. Any scenes already charged stay charged and " +
|
|
4388
|
+
"all completed work (generated scenes, voice, music, a finished render) is kept — a step already enqueued " +
|
|
4389
|
+
"may still finish, but nothing new is started. Idempotent: a no-op that returns the project unchanged when " +
|
|
4390
|
+
"Autopilot is not running (already completed, failed, cancelled, or never launched), so it is safe to " +
|
|
4391
|
+
"repeat. It does NOT itself refund or re-run anything. For a non-Autopilot prepaid batch " +
|
|
4392
|
+
"(batch_generation_status preparing / paused_for_retry) use cancel_editor_batch; to stop one " +
|
|
4393
|
+
"individually-generated scene use cancel_segment_generation. Scope: video:generate.",
|
|
4394
|
+
inputSchema: { slug: z.string().describe("Editor project slug") },
|
|
4395
|
+
annotations: {
|
|
4396
|
+
title: "Cancel autopilot",
|
|
4397
|
+
readOnlyHint: false,
|
|
4398
|
+
destructiveHint: true,
|
|
4399
|
+
openWorldHint: true,
|
|
4400
|
+
idempotentHint: true,
|
|
4401
|
+
},
|
|
4402
|
+
},
|
|
4403
|
+
tool(async (args: { slug: string }, client) => {
|
|
4404
|
+
const res = await client.post<{ data: unknown }>(
|
|
4405
|
+
`/editor/${args.slug}/autopilot/cancel`,
|
|
4406
|
+
undefined,
|
|
4407
|
+
undefined,
|
|
4408
|
+
);
|
|
4409
|
+
return ok({
|
|
4410
|
+
...asRecord(asRecord(res).data ?? res),
|
|
4411
|
+
slug: args.slug,
|
|
4412
|
+
charged: false,
|
|
4413
|
+
next: "get_status",
|
|
4414
|
+
});
|
|
4415
|
+
}),
|
|
4416
|
+
);
|
|
4417
|
+
|
|
4222
4418
|
registerTool(
|
|
4223
4419
|
"regenerate_segment",
|
|
4224
4420
|
{
|
|
@@ -4280,7 +4476,7 @@ registerTool(
|
|
|
4280
4476
|
{
|
|
4281
4477
|
title: "Generate all pending segments (sequential)",
|
|
4282
4478
|
description:
|
|
4283
|
-
"Generates every not-yet-completed scene (pending or previously failed) in position order, waiting for each to finish before starting" +
|
|
4479
|
+
"Generates every not-yet-completed scene (pending or previously failed) in position order, waiting for each to finish before starting " +
|
|
4284
4480
|
"the next (preserves visual continuity). Each AI-generated scene is a fixed 8 seconds, so N scenes ≈ N×8s of footage. Costs 5 credits per segment generated. Stops and reports what " +
|
|
4285
4481
|
"completed if a scene errors on submit (e.g. 402 credits_insufficient) or fails while rendering; if a " +
|
|
4286
4482
|
"scene is still rendering past the budget it returns timed_out so you can re-run to continue (completed " +
|
|
@@ -4479,15 +4675,15 @@ registerTool(
|
|
|
4479
4675
|
{
|
|
4480
4676
|
title: "Set the narration script",
|
|
4481
4677
|
description:
|
|
4482
|
-
"Writes your own narration script (1–
|
|
4678
|
+
"Writes your own narration script (1–10000 chars) instead of generating one. Free — no credits, no AI " +
|
|
4483
4679
|
"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.",
|
|
4484
4680
|
inputSchema: {
|
|
4485
4681
|
slug: z.string().describe("Editor project slug"),
|
|
4486
4682
|
script: z
|
|
4487
4683
|
.string()
|
|
4488
4684
|
.min(1)
|
|
4489
|
-
.max(
|
|
4490
|
-
.describe("Narration / voiceover text (1–
|
|
4685
|
+
.max(10000)
|
|
4686
|
+
.describe("Narration / voiceover text (1–10000 chars)"),
|
|
4491
4687
|
},
|
|
4492
4688
|
annotations: { title: "Set narration", ...WRITE, idempotentHint: true },
|
|
4493
4689
|
},
|
|
@@ -4504,7 +4700,7 @@ registerTool(
|
|
|
4504
4700
|
{
|
|
4505
4701
|
title: "Generate the narration (AI assist)",
|
|
4506
4702
|
description:
|
|
4507
|
-
"Generates
|
|
4703
|
+
"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 — " +
|
|
4508
4704
|
"it is free of CREDITS but NOT free of quota. On 429 set auto_unlock:true (1 credit → +10 assists, " +
|
|
4509
4705
|
"retried once) or write the script yourself with set_narration_script.",
|
|
4510
4706
|
inputSchema: {
|
|
@@ -4536,7 +4732,7 @@ registerTool(
|
|
|
4536
4732
|
title: "Generate the voiceover (3 credits)",
|
|
4537
4733
|
description:
|
|
4538
4734
|
"Synthesizes the voiceover from the narration script. Costs 3 credits. Safe to retry; re-call after changing the narration or voice to regenerate." +
|
|
4539
|
-
"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.",
|
|
4735
|
+
" 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.",
|
|
4540
4736
|
inputSchema: {
|
|
4541
4737
|
slug: z.string().describe("Editor project slug"),
|
|
4542
4738
|
voice_id: z
|
|
@@ -4627,8 +4823,8 @@ registerTool(
|
|
|
4627
4823
|
{
|
|
4628
4824
|
title: "Render the final editor video (0 credits)",
|
|
4629
4825
|
description:
|
|
4630
|
-
"Renders the final MP4 from the generated segments + voice + music. Costs 0 credits
|
|
4631
|
-
"
|
|
4826
|
+
"Renders the final MP4 from the generated segments + voice + music. Costs 0 credits. Pending or previously " +
|
|
4827
|
+
"failed AI scenes are generated first and charged as a new batch. Render NEVER generates or charges for voice/music; a new " +
|
|
4632
4828
|
"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; " +
|
|
4633
4829
|
'pair with wait_for_completion({ slug, kind: "editor" }) then download_result. ' +
|
|
4634
4830
|
"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.",
|
|
@@ -4775,9 +4971,10 @@ registerTool(
|
|
|
4775
4971
|
{
|
|
4776
4972
|
title: "List renders for an editor project",
|
|
4777
4973
|
description:
|
|
4778
|
-
"Lists every render version for an editor project: {id, version, status, video_url,
|
|
4779
|
-
"error_message, inserted_at}. Use it to find a
|
|
4780
|
-
"(presigned, ~24h).
|
|
4974
|
+
"Lists every render version for an editor project: {id, version, status, video_url, thumbnail_url, " +
|
|
4975
|
+
"duration_seconds, error_message, retryable, combination_hash, is_stale, inserted_at}. Use it to find a " +
|
|
4976
|
+
"FAILED render to retry, or to recover a finished video_url (presigned, ~24h). status is pending | " +
|
|
4977
|
+
"processing | completed | failed; is_stale null means unknown, not current. Read-only.",
|
|
4781
4978
|
inputSchema: { slug: z.string().describe("Editor project slug") },
|
|
4782
4979
|
annotations: { title: "List renders", ...RO },
|
|
4783
4980
|
},
|
|
@@ -5129,9 +5326,11 @@ registerTool(
|
|
|
5129
5326
|
description:
|
|
5130
5327
|
"Uploads a local product image and attaches it to the project as the product. Accepted: " +
|
|
5131
5328
|
IMAGE_EXTS.map((e) => `.${e}`).join(", ") +
|
|
5132
|
-
" (≤8 MiB), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits.
|
|
5133
|
-
"
|
|
5134
|
-
"
|
|
5329
|
+
" (≤8 MiB), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits. Its pixels guide product identity " +
|
|
5330
|
+
"in every generated Editor scene and its confirmed description grounds scenario and narration. The exact photo " +
|
|
5331
|
+
"is linked as the closing image unless a separate closing image was authored; generated fine label text can still vary. " +
|
|
5332
|
+
"WARNING: replacing the product photo on a project that already has generated AI scenes invalidates their product " +
|
|
5333
|
+
"identity — those scenes were built from the old photo and must be regenerated to match the new one.",
|
|
5135
5334
|
inputSchema: {
|
|
5136
5335
|
slug: z.string().describe("Editor project slug"),
|
|
5137
5336
|
file_path: z
|
|
@@ -5165,33 +5364,6 @@ registerTool(
|
|
|
5165
5364
|
),
|
|
5166
5365
|
);
|
|
5167
5366
|
|
|
5168
|
-
registerTool(
|
|
5169
|
-
"set_product_placement",
|
|
5170
|
-
{
|
|
5171
|
-
title: "Set default product placement on pending scenes (0 credits)",
|
|
5172
|
-
description:
|
|
5173
|
-
"Sets where the product appears across pending (never-generated) AI scenes. mode is 'throughout' (every scene) " +
|
|
5174
|
-
"or 'end' (closing scene only; requires a first+last-frame capable model). 0 credits. Add a product first with " +
|
|
5175
|
-
"set_product. (There is no 'none' — to remove the product, that's a separate action.)",
|
|
5176
|
-
inputSchema: {
|
|
5177
|
-
slug: z.string().describe("Editor project slug"),
|
|
5178
|
-
mode: z.enum(["throughout", "end"]).describe("'throughout' or 'end'"),
|
|
5179
|
-
},
|
|
5180
|
-
annotations: {
|
|
5181
|
-
title: "Set product placement",
|
|
5182
|
-
...WRITE,
|
|
5183
|
-
idempotentHint: true,
|
|
5184
|
-
},
|
|
5185
|
-
},
|
|
5186
|
-
tool(async (args: { slug: string; mode: "throughout" | "end" }, client) => {
|
|
5187
|
-
const res = await client.post<{ data: unknown }>(
|
|
5188
|
-
`/editor/${args.slug}/product/apply-default-placement`,
|
|
5189
|
-
{ placement: args.mode },
|
|
5190
|
-
);
|
|
5191
|
-
return ok(asRecord(res).data ?? res);
|
|
5192
|
-
}),
|
|
5193
|
-
);
|
|
5194
|
-
|
|
5195
5367
|
registerTool(
|
|
5196
5368
|
"set_closing_image",
|
|
5197
5369
|
{
|