@hubfluencer/mcp 0.9.1 → 0.11.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 +19 -11
- package/dist/index.js +283 -53
- package/package.json +1 -1
- package/src/core.ts +111 -37
- package/src/index.ts +393 -66
- package/src/output-schemas.ts +57 -2
- package/src/uploads.ts +2 -1
package/src/index.ts
CHANGED
|
@@ -121,6 +121,7 @@ import {
|
|
|
121
121
|
MAX_CLOSING_IMAGE_BYTES,
|
|
122
122
|
MAX_LOGO_BYTES,
|
|
123
123
|
MAX_PRODUCT_IMAGE_BYTES,
|
|
124
|
+
MAX_SHORT_LOGO_BYTES,
|
|
124
125
|
type PreparedUploadFile,
|
|
125
126
|
preparedUploadIdentity,
|
|
126
127
|
prepareImageUpload,
|
|
@@ -614,12 +615,12 @@ DELEGATE THE CONTENT OPERATION (the full loop — steps 1-3 are $0)
|
|
|
614
615
|
8. RECURRING: create_series + plan_series_episodes → materialize_episode drafts each episode; get_series_dashboard shows upcoming/drafted/published lanes; mark_episode_posted closes a recurring show's loop.
|
|
615
616
|
|
|
616
617
|
CREDITS (spent server-side; each tool prices before charging — see its description)
|
|
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.
|
|
618
|
+
- Free (0 credits): every draft create/edit, reorder, all slider re-composites, short re-renders (rerender_short — applies text/style/CTA/end-card/logo edits over a short's already-paid footage), and the whole onboard/plan/pack/materialize path (steps 1-3). get_credits shows the balance.
|
|
618
619
|
- 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.
|
|
619
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.
|
|
620
621
|
|
|
621
622
|
WAITING & RESUMING
|
|
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.
|
|
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.
|
|
623
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.
|
|
624
625
|
|
|
625
626
|
LOCAL FILES (sandboxed)
|
|
@@ -682,10 +683,9 @@ async function pollSegmentToTerminal(
|
|
|
682
683
|
slug: string,
|
|
683
684
|
segmentId: string | number,
|
|
684
685
|
extra: ToolExtra,
|
|
685
|
-
|
|
686
|
+
deadline: number,
|
|
686
687
|
intervalMs: number,
|
|
687
688
|
): Promise<{ status: string; error: string | null; timed_out: boolean }> {
|
|
688
|
-
const deadline = Date.now() + budgetMs;
|
|
689
689
|
const sid = String(segmentId);
|
|
690
690
|
const read = async (): Promise<{ status: string; error: string | null }> => {
|
|
691
691
|
// Bound each poll GET's retries by the loop deadline (see fetchStatus).
|
|
@@ -1134,6 +1134,7 @@ registerTool(
|
|
|
1134
1134
|
: `/editor/${slug}/autopilot/cost`;
|
|
1135
1135
|
let estimated_credits: number | null = null;
|
|
1136
1136
|
let available_credits: number | null = null;
|
|
1137
|
+
let authorizedCredits: number | undefined;
|
|
1137
1138
|
try {
|
|
1138
1139
|
const cost = asRecord(
|
|
1139
1140
|
asRecord(await client.get<{ data: unknown }>(costPath)).data,
|
|
@@ -1237,6 +1238,7 @@ registerTool(
|
|
|
1237
1238
|
"Editor autopilot launch requires max_credits or a live quote",
|
|
1238
1239
|
);
|
|
1239
1240
|
}
|
|
1241
|
+
authorizedCredits = approvedCap;
|
|
1240
1242
|
await client.post(
|
|
1241
1243
|
`/editor/${slug}/autopilot`,
|
|
1242
1244
|
// Use the caller's explicit cap when given, otherwise the live
|
|
@@ -1271,12 +1273,50 @@ registerTool(
|
|
|
1271
1273
|
.saved_to;
|
|
1272
1274
|
}
|
|
1273
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
|
+
|
|
1274
1314
|
const result = {
|
|
1275
1315
|
...status,
|
|
1276
1316
|
kind_inferred: requestedKind === undefined,
|
|
1277
1317
|
estimated_credits,
|
|
1278
1318
|
available_credits,
|
|
1279
|
-
|
|
1319
|
+
...billing,
|
|
1280
1320
|
saved_to,
|
|
1281
1321
|
timed_out: !status.terminal,
|
|
1282
1322
|
};
|
|
@@ -1296,13 +1336,15 @@ registerTool(
|
|
|
1296
1336
|
note: status.ready
|
|
1297
1337
|
? "Done. Presigned ~24h URL — download promptly."
|
|
1298
1338
|
: status.terminal
|
|
1299
|
-
?
|
|
1300
|
-
|
|
1301
|
-
//
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
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}.`
|
|
1306
1348
|
: `Still rendering — call wait_for_completion(${resumeArgs}).`,
|
|
1307
1349
|
},
|
|
1308
1350
|
links,
|
|
@@ -1436,7 +1478,7 @@ registerTool(
|
|
|
1436
1478
|
"Creates a short draft from a product prompt (min 10 chars). A short is a 12s vertical (two 6s AI " +
|
|
1437
1479
|
"segments + an on-screen title overlay + music; 14s when it ends on a poster or brand-lockup end card). " +
|
|
1438
1480
|
"Returns the slug, costs 0 credits. Follow with generate_short to render. Iteration is FREE after that " +
|
|
1439
|
-
"first generate: the 15 credits bought the footage + music, and every later text/style/CTA/end-card " +
|
|
1481
|
+
"first generate: the 15 credits bought the footage + music, and every later text/style/CTA/end-card/logo " +
|
|
1440
1482
|
"tweak re-renders over them for 0 credits (update_short → rerender_short). " +
|
|
1441
1483
|
"BE PROACTIVE: don't ship a bare clip — set a headline (the on-screen TITLE) and subheadline (secondary " +
|
|
1442
1484
|
"title), and pick a music_vibe plus visual_language that fit the brand. " +
|
|
@@ -1447,8 +1489,8 @@ registerTool(
|
|
|
1447
1489
|
"default). If headline/subheadline/text_beats are left blank, the server auto-writes the headline + " +
|
|
1448
1490
|
"caption beats at generate time (pass skip_auto_text:true to generate_short for a deliberately bare clip). " +
|
|
1449
1491
|
"To brand it further, attach a product image " +
|
|
1450
|
-
"(set_short_product)
|
|
1451
|
-
"
|
|
1492
|
+
"(set_short_product), a deterministic brand logo (set_short_logo), or an end-card poster " +
|
|
1493
|
+
"(set_short_poster) before generate_short.",
|
|
1452
1494
|
inputSchema: {
|
|
1453
1495
|
product_prompt: z
|
|
1454
1496
|
.string()
|
|
@@ -1736,9 +1778,10 @@ registerTool(
|
|
|
1736
1778
|
title: "Update a short draft (0 credits)",
|
|
1737
1779
|
description:
|
|
1738
1780
|
"Patches an existing short draft — only the fields you pass change; omitted fields are left untouched. " +
|
|
1739
|
-
"0 credits, no re-render by itself. APPLYING the edit: changed only text/style/CTA/end-card fields " +
|
|
1781
|
+
"0 credits, no re-render by itself. APPLYING the edit: changed only text/style/CTA/end-card/logo fields " +
|
|
1740
1782
|
"(headline, subheadline, text_beats, cta_text, offer_text, badge_text, star_rating, colors, font, " +
|
|
1741
|
-
"position, animation, closing_claim, brand_name, poster_includes_lockup, end_card
|
|
1783
|
+
"position, animation, closing_claim, brand_name, poster_includes_lockup, end_card, short_logo_position, " +
|
|
1784
|
+
"short_logo_treatment) on an " +
|
|
1742
1785
|
"already-generated short? " +
|
|
1743
1786
|
"rerender_short applies them for 0 credits. Footage/music-affecting edits (product_prompt, product " +
|
|
1744
1787
|
"image, creative_format, visual_language, theme, music_vibe, music_instruments, language) need " +
|
|
@@ -1922,6 +1965,20 @@ registerTool(
|
|
|
1922
1965
|
"Set true if your poster already contains brand name/CTA/badges — suppresses the overlaid " +
|
|
1923
1966
|
"claim/CTA so they don't collide (default false).",
|
|
1924
1967
|
),
|
|
1968
|
+
short_logo_position: z
|
|
1969
|
+
.enum(["top-left", "top-right", "bottom-left", "bottom-right"])
|
|
1970
|
+
.optional()
|
|
1971
|
+
.describe(
|
|
1972
|
+
"Corner for the deterministic brand-logo overlay (set the image with set_short_logo). Moving it is " +
|
|
1973
|
+
"a FREE re-render — logo fields never cost credits.",
|
|
1974
|
+
),
|
|
1975
|
+
short_logo_treatment: z
|
|
1976
|
+
.enum(["throughout", "end_card", "none"])
|
|
1977
|
+
.optional()
|
|
1978
|
+
.describe(
|
|
1979
|
+
'When the brand logo shows: "throughout" the footage, only on the "end_card", or "none" to hide it. ' +
|
|
1980
|
+
"FREE re-render.",
|
|
1981
|
+
),
|
|
1925
1982
|
brand_profile_id: z
|
|
1926
1983
|
.number()
|
|
1927
1984
|
.int()
|
|
@@ -1967,6 +2024,12 @@ registerTool(
|
|
|
1967
2024
|
brand_name?: string;
|
|
1968
2025
|
end_card?: string;
|
|
1969
2026
|
poster_includes_lockup?: boolean;
|
|
2027
|
+
short_logo_position?:
|
|
2028
|
+
| "top-left"
|
|
2029
|
+
| "top-right"
|
|
2030
|
+
| "bottom-left"
|
|
2031
|
+
| "bottom-right";
|
|
2032
|
+
short_logo_treatment?: "throughout" | "end_card" | "none";
|
|
1970
2033
|
brand_profile_id?: number;
|
|
1971
2034
|
},
|
|
1972
2035
|
client,
|
|
@@ -2013,6 +2076,13 @@ registerTool(
|
|
|
2013
2076
|
if (args.end_card !== undefined) body.end_card = args.end_card;
|
|
2014
2077
|
if (args.poster_includes_lockup !== undefined)
|
|
2015
2078
|
body.poster_includes_lockup = args.poster_includes_lockup;
|
|
2079
|
+
// Logo overlay placement — the API maps these body keys straight onto the
|
|
2080
|
+
// factory (short_logo_position/short_logo_treatment). They're excluded from
|
|
2081
|
+
// the paid-inputs snapshot, so a corner/treatment change is a FREE re-render.
|
|
2082
|
+
if (args.short_logo_position !== undefined)
|
|
2083
|
+
body.short_logo_position = args.short_logo_position;
|
|
2084
|
+
if (args.short_logo_treatment !== undefined)
|
|
2085
|
+
body.short_logo_treatment = args.short_logo_treatment;
|
|
2016
2086
|
if (args.brand_profile_id !== undefined)
|
|
2017
2087
|
body.brand_profile_id = args.brand_profile_id;
|
|
2018
2088
|
|
|
@@ -2024,7 +2094,7 @@ registerTool(
|
|
|
2024
2094
|
return ok({
|
|
2025
2095
|
...asRecord(data),
|
|
2026
2096
|
next:
|
|
2027
|
-
"Saved. Only text/style/CTA/end-card changed on an already-generated short? " +
|
|
2097
|
+
"Saved. Only text/style/CTA/end-card/logo changed on an already-generated short? " +
|
|
2028
2098
|
`rerender_short({ slug: "${args.slug}" }) applies it for 0 credits. Footage/music edits ` +
|
|
2029
2099
|
"(prompt, product image, format, visual_language, theme, music_*, language) need " +
|
|
2030
2100
|
"generate_short (15 credits).",
|
|
@@ -2464,8 +2534,9 @@ registerTool(
|
|
|
2464
2534
|
{
|
|
2465
2535
|
title: "Re-render a short (FREE — 0 credits)",
|
|
2466
2536
|
description:
|
|
2467
|
-
"FREE re-render: applies the short's current text/style/CTA/end-card state over the already-paid " +
|
|
2468
|
-
"footage and music — 0 credits. Use after update_short for copy/style/CTA tweaks
|
|
2537
|
+
"FREE re-render: applies the short's current text/style/CTA/end-card/logo state over the already-paid " +
|
|
2538
|
+
"footage and music — 0 credits. Use after update_short for copy/style/CTA tweaks, or after a logo change " +
|
|
2539
|
+
"(set_short_logo, or short_logo_position/short_logo_treatment via update_short). Footage or music " +
|
|
2469
2540
|
"changes need generate_short (15 credits) instead. Edits that add/remove the end card (poster, " +
|
|
2470
2541
|
"end_card) can shift the video between 12s and 14s; when the pinned music bed is shorter than the " +
|
|
2471
2542
|
"export it crossfades into a repeated continuation, then fades at the export end, without regeneration. " +
|
|
@@ -3030,7 +3101,7 @@ registerTool(
|
|
|
3030
3101
|
logo_path: z
|
|
3031
3102
|
.string()
|
|
3032
3103
|
.optional()
|
|
3033
|
-
.describe("Local logo (.jpg/.jpeg/.png, ≤
|
|
3104
|
+
.describe("Local logo (.jpg/.jpeg/.png, ≤5 MiB) to overlay."),
|
|
3034
3105
|
logo_treatment: z
|
|
3035
3106
|
.enum(["intro", "outro", "both", "none"])
|
|
3036
3107
|
.optional()
|
|
@@ -3288,7 +3359,10 @@ registerTool(
|
|
|
3288
3359
|
kind: "editor",
|
|
3289
3360
|
status,
|
|
3290
3361
|
...quote,
|
|
3291
|
-
|
|
3362
|
+
launched: true,
|
|
3363
|
+
authorized_credits: args.max_credits,
|
|
3364
|
+
credits_spent: 0,
|
|
3365
|
+
charged: false,
|
|
3292
3366
|
next: "wait_for_completion",
|
|
3293
3367
|
});
|
|
3294
3368
|
} finally {
|
|
@@ -3308,7 +3382,9 @@ registerTool(
|
|
|
3308
3382
|
"explicit max_credits cap to authorize launch; the tool refuses if pricing is unavailable, the estimate exceeds " +
|
|
3309
3383
|
"the cap, or the balance is insufficient. Each authorized call is a genuine start attempt (like tapping Launch in the app): " +
|
|
3310
3384
|
"if a run is already in progress for this slug the server returns already_running — it never double-starts " +
|
|
3311
|
-
"or double-charges. Failed or cancelled runs resume normally
|
|
3385
|
+
"or double-charges. Failed or cancelled runs resume normally, except when status is batch_retry_required: " +
|
|
3386
|
+
"call retry_editor_batch (then wait for that prepaid batch) or cancel_editor_batch first. For a completed project, " +
|
|
3387
|
+
"set restart:true to quote and build a fresh creative run; omitting it only resumes pending edits. Then poll with " +
|
|
3312
3388
|
'wait_for_completion({ slug, kind: "editor" }).',
|
|
3313
3389
|
inputSchema: {
|
|
3314
3390
|
slug: z.string().describe("Editor project slug"),
|
|
@@ -3425,13 +3501,79 @@ registerTool(
|
|
|
3425
3501
|
kind: "editor",
|
|
3426
3502
|
status,
|
|
3427
3503
|
...quote,
|
|
3428
|
-
|
|
3504
|
+
launched: true,
|
|
3505
|
+
authorized_credits: args.max_credits,
|
|
3506
|
+
credits_spent: 0,
|
|
3507
|
+
charged: false,
|
|
3429
3508
|
next: "wait_for_completion",
|
|
3430
3509
|
});
|
|
3431
3510
|
},
|
|
3432
3511
|
),
|
|
3433
3512
|
);
|
|
3434
3513
|
|
|
3514
|
+
registerTool(
|
|
3515
|
+
"retry_editor_batch",
|
|
3516
|
+
{
|
|
3517
|
+
title: "Retry a paused editor batch",
|
|
3518
|
+
description:
|
|
3519
|
+
"Recovers an editor whose normalized status is batch_retry_required by atomically re-queueing the failed " +
|
|
3520
|
+
"scene and resuming its prepaid batch. This spends 0 additional credits: the batch generation was already " +
|
|
3521
|
+
"paid. It does not authorize or restart a failed parent Autopilot run; after retrying, wait for the batch " +
|
|
3522
|
+
"to settle. If the parent is then terminal, call start_autopilot without max_credits for a fresh quote and " +
|
|
3523
|
+
"relaunch only with an approved cap. A batch parked on " +
|
|
3524
|
+
"insufficient_credits needs a top-up/scope change instead. Scope: video:generate.",
|
|
3525
|
+
inputSchema: { slug: z.string().describe("Editor project slug") },
|
|
3526
|
+
annotations: {
|
|
3527
|
+
title: "Retry editor batch",
|
|
3528
|
+
...WRITE,
|
|
3529
|
+
idempotentHint: false,
|
|
3530
|
+
},
|
|
3531
|
+
},
|
|
3532
|
+
tool(async (args: { slug: string }, client) => {
|
|
3533
|
+
const res = await client.post<{ data: unknown }>(
|
|
3534
|
+
`/video-factories/${args.slug}/retry-batch-segment`,
|
|
3535
|
+
);
|
|
3536
|
+
return ok({
|
|
3537
|
+
...asRecord(asRecord(res).data ?? res),
|
|
3538
|
+
slug: args.slug,
|
|
3539
|
+
charged: false,
|
|
3540
|
+
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.",
|
|
3541
|
+
next: "wait_for_completion",
|
|
3542
|
+
});
|
|
3543
|
+
}),
|
|
3544
|
+
);
|
|
3545
|
+
|
|
3546
|
+
registerTool(
|
|
3547
|
+
"cancel_editor_batch",
|
|
3548
|
+
{
|
|
3549
|
+
title: "Cancel an editor batch",
|
|
3550
|
+
description:
|
|
3551
|
+
"Cancels an editor batch in awaiting_previews, running, or paused_for_retry so the project can be edited or " +
|
|
3552
|
+
"Autopilot can be started again. Cancellation is destructive: dispatched scene work is stopped and its prepaid " +
|
|
3553
|
+
"credits are not refunded; only an awaiting_previews batch (before dispatch) is refunded by the server. Spends " +
|
|
3554
|
+
"0 additional credits. Scope: video:generate.",
|
|
3555
|
+
inputSchema: { slug: z.string().describe("Editor project slug") },
|
|
3556
|
+
annotations: {
|
|
3557
|
+
title: "Cancel editor batch",
|
|
3558
|
+
readOnlyHint: false,
|
|
3559
|
+
destructiveHint: true,
|
|
3560
|
+
openWorldHint: true,
|
|
3561
|
+
idempotentHint: false,
|
|
3562
|
+
},
|
|
3563
|
+
},
|
|
3564
|
+
tool(async (args: { slug: string }, client) => {
|
|
3565
|
+
const res = await client.post<{ data: unknown }>(
|
|
3566
|
+
`/video-factories/${args.slug}/cancel-batch`,
|
|
3567
|
+
);
|
|
3568
|
+
return ok({
|
|
3569
|
+
...asRecord(asRecord(res).data ?? res),
|
|
3570
|
+
slug: args.slug,
|
|
3571
|
+
charged: false,
|
|
3572
|
+
next: "get_status",
|
|
3573
|
+
});
|
|
3574
|
+
}),
|
|
3575
|
+
);
|
|
3576
|
+
|
|
3435
3577
|
// ── AI assists (free daily quota) ───────────────────────────────────────────
|
|
3436
3578
|
|
|
3437
3579
|
registerTool(
|
|
@@ -3630,21 +3772,81 @@ registerTool(
|
|
|
3630
3772
|
),
|
|
3631
3773
|
);
|
|
3632
3774
|
|
|
3775
|
+
function conciseEditorState(value: unknown): Record<string, unknown> {
|
|
3776
|
+
const data = asRecord(value);
|
|
3777
|
+
const keys = [
|
|
3778
|
+
"slug",
|
|
3779
|
+
"status",
|
|
3780
|
+
"autopilot_status",
|
|
3781
|
+
"autopilot_current_step",
|
|
3782
|
+
"autopilot_error_step",
|
|
3783
|
+
"autopilot_error_code",
|
|
3784
|
+
"autopilot_error_message",
|
|
3785
|
+
"autopilot_max_credits",
|
|
3786
|
+
"autopilot_credits_spent",
|
|
3787
|
+
"scenario_status",
|
|
3788
|
+
"scenario_apply_status",
|
|
3789
|
+
"scenario_prompt",
|
|
3790
|
+
"batch_generation_status",
|
|
3791
|
+
"narration_status",
|
|
3792
|
+
"narration_script",
|
|
3793
|
+
"narration_stale",
|
|
3794
|
+
"voice_stale",
|
|
3795
|
+
"music_stale",
|
|
3796
|
+
"segments_count",
|
|
3797
|
+
"current_audio",
|
|
3798
|
+
"current_music",
|
|
3799
|
+
"latest_render",
|
|
3800
|
+
"project_spend",
|
|
3801
|
+
"ai_assist_quota",
|
|
3802
|
+
] as const;
|
|
3803
|
+
const result: Record<string, unknown> = {};
|
|
3804
|
+
for (const key of keys) {
|
|
3805
|
+
if (key in data) result[key] = data[key];
|
|
3806
|
+
}
|
|
3807
|
+
result.segments = Array.isArray(data.segments)
|
|
3808
|
+
? (data.segments as Record<string, unknown>[]).map((segment) => ({
|
|
3809
|
+
id: segment.id,
|
|
3810
|
+
position: segment.position,
|
|
3811
|
+
prompt: segment.prompt,
|
|
3812
|
+
status: segment.status,
|
|
3813
|
+
source_type: segment.source_type,
|
|
3814
|
+
use_previous_frame: segment.use_previous_frame,
|
|
3815
|
+
protagonist_presence: segment.protagonist_presence,
|
|
3816
|
+
cast_mode_snapshot: segment.cast_mode_snapshot,
|
|
3817
|
+
video_stale: segment.video_stale,
|
|
3818
|
+
error_code: segment.error_code,
|
|
3819
|
+
error_message: segment.error_message,
|
|
3820
|
+
narration_text: segment.narration_text,
|
|
3821
|
+
}))
|
|
3822
|
+
: [];
|
|
3823
|
+
return result;
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3633
3826
|
registerTool(
|
|
3634
3827
|
"get_editor",
|
|
3635
3828
|
{
|
|
3636
|
-
title: "Get
|
|
3829
|
+
title: "Get editor state (review step)",
|
|
3637
3830
|
description:
|
|
3638
|
-
"Returns the full editor
|
|
3639
|
-
|
|
3640
|
-
"
|
|
3641
|
-
|
|
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, narration, audio, render, spend, and quota.",
|
|
3832
|
+
inputSchema: {
|
|
3833
|
+
slug: z.string().describe("Editor project slug"),
|
|
3834
|
+
response_format: z.enum(["concise", "full"]).default("full"),
|
|
3835
|
+
},
|
|
3642
3836
|
annotations: { title: "Get editor", ...RO },
|
|
3643
3837
|
},
|
|
3644
|
-
tool(
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3838
|
+
tool(
|
|
3839
|
+
async (
|
|
3840
|
+
args: { slug: string; response_format: "concise" | "full" },
|
|
3841
|
+
client,
|
|
3842
|
+
) => {
|
|
3843
|
+
const res = await client.get<{ data: unknown }>(`/editor/${args.slug}`);
|
|
3844
|
+
const data = asRecord(res).data;
|
|
3845
|
+
return ok(
|
|
3846
|
+
args.response_format === "full" ? data : conciseEditorState(data),
|
|
3847
|
+
);
|
|
3848
|
+
},
|
|
3849
|
+
),
|
|
3648
3850
|
);
|
|
3649
3851
|
|
|
3650
3852
|
registerTool(
|
|
@@ -4093,20 +4295,30 @@ registerTool(
|
|
|
4093
4295
|
},
|
|
4094
4296
|
},
|
|
4095
4297
|
tool(async (args: { slug: string }, client, extra) => {
|
|
4096
|
-
|
|
4298
|
+
// One absolute deadline owns the entire read/submit/poll/reconcile path.
|
|
4299
|
+
// Start it before the initial retrying GET so no preparatory request sits
|
|
4300
|
+
// outside the advertised ~280s tool budget.
|
|
4301
|
+
const overallDeadline = Date.now() + 280_000;
|
|
4302
|
+
const res = await client.get<{ data: unknown }>(
|
|
4303
|
+
`/editor/${args.slug}`,
|
|
4304
|
+
undefined,
|
|
4305
|
+
overallDeadline,
|
|
4306
|
+
);
|
|
4097
4307
|
const data = asRecord(asRecord(res).data);
|
|
4098
4308
|
const segments = Array.isArray(data.segments)
|
|
4099
4309
|
? (data.segments as Record<string, unknown>[])
|
|
4100
4310
|
: [];
|
|
4101
|
-
const
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
st
|
|
4105
|
-
st
|
|
4106
|
-
|
|
4107
|
-
|
|
4311
|
+
const unfinished = segments
|
|
4312
|
+
.filter((s) => {
|
|
4313
|
+
if (s.source_type === "upload") return false;
|
|
4314
|
+
const st = (s.status as string) ?? (s.generation_status as string);
|
|
4315
|
+
return st !== "completed";
|
|
4316
|
+
})
|
|
4317
|
+
.sort(
|
|
4318
|
+
(a, b) =>
|
|
4319
|
+
Number(a.position ?? Number.MAX_SAFE_INTEGER) -
|
|
4320
|
+
Number(b.position ?? Number.MAX_SAFE_INTEGER),
|
|
4108
4321
|
);
|
|
4109
|
-
});
|
|
4110
4322
|
|
|
4111
4323
|
// Overall wall-clock budget: cap the whole call at the same 280s ceiling the
|
|
4112
4324
|
// other wait loops use (under a typical 300s client timeout). Without it a
|
|
@@ -4114,20 +4326,23 @@ registerTool(
|
|
|
4114
4326
|
// hours) in a single call. When the budget runs out mid-batch we stop and
|
|
4115
4327
|
// return the standard resume contract — call again and it naturally continues
|
|
4116
4328
|
// with whatever scenes are still pending (completed ones are skipped).
|
|
4117
|
-
|
|
4118
|
-
//
|
|
4119
|
-
//
|
|
4120
|
-
|
|
4329
|
+
// Every scene poll receives this same absolute deadline, so time already spent
|
|
4330
|
+
// on earlier reads, submissions, and scenes is never granted again.
|
|
4331
|
+
// Reserve a full non-idempotent POST timeout plus one final status read before
|
|
4332
|
+
// starting another paid scene. POSTs deliberately aren't aborted/retried, so
|
|
4333
|
+
// beginning one in the last seconds of the budget would make a client timeout
|
|
4334
|
+
// ambiguous after the server may already have accepted the charge.
|
|
4335
|
+
const SUBMIT_HEADROOM_MS = 61_000;
|
|
4121
4336
|
|
|
4122
4337
|
const generated: Array<string | number> = [];
|
|
4123
4338
|
let n = 0;
|
|
4124
|
-
for (const seg of
|
|
4339
|
+
for (const seg of unfinished) {
|
|
4125
4340
|
const id = seg.id;
|
|
4126
4341
|
if (id === undefined) continue;
|
|
4127
4342
|
const sid = id as string | number;
|
|
4128
4343
|
// Stop before submitting another scene once the overall budget is spent, so
|
|
4129
4344
|
// the call returns instead of charging + waiting past the deadline.
|
|
4130
|
-
|
|
4345
|
+
let remainingMs = overallDeadline - Date.now();
|
|
4131
4346
|
if (remainingMs <= 0) {
|
|
4132
4347
|
return ok({
|
|
4133
4348
|
slug: args.slug,
|
|
@@ -4139,22 +4354,45 @@ registerTool(
|
|
|
4139
4354
|
await reportProgress(
|
|
4140
4355
|
extra,
|
|
4141
4356
|
++n,
|
|
4142
|
-
`generating segment ${String(sid)} (${n}/${
|
|
4143
|
-
|
|
4357
|
+
`generating segment ${String(sid)} (${n}/${unfinished.length})`,
|
|
4358
|
+
unfinished.length,
|
|
4144
4359
|
);
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4360
|
+
const initialStatus =
|
|
4361
|
+
(seg.status as string) ?? (seg.generation_status as string);
|
|
4362
|
+
if (initialStatus !== "processing") {
|
|
4363
|
+
remainingMs = overallDeadline - Date.now();
|
|
4364
|
+
if (remainingMs < SUBMIT_HEADROOM_MS) {
|
|
4365
|
+
return ok({
|
|
4366
|
+
slug: args.slug,
|
|
4367
|
+
generated,
|
|
4368
|
+
timed_out: true,
|
|
4369
|
+
note: "Wall-clock budget is too low to safely start another paid scene. Re-run generate_all_segments to continue; completed scenes are skipped.",
|
|
4370
|
+
});
|
|
4371
|
+
}
|
|
4372
|
+
try {
|
|
4373
|
+
await client.post(
|
|
4374
|
+
`/editor/${args.slug}/segments/${String(sid)}/generate`,
|
|
4375
|
+
undefined,
|
|
4376
|
+
undefined,
|
|
4377
|
+
);
|
|
4378
|
+
} catch (e) {
|
|
4379
|
+
return ok({
|
|
4380
|
+
slug: args.slug,
|
|
4381
|
+
generated,
|
|
4382
|
+
stopped_at: sid,
|
|
4383
|
+
error: errMessage(e),
|
|
4384
|
+
note: "Stopped on first error — fix the cause (often credits) and re-run; completed scenes are skipped, failed/pending are retried.",
|
|
4385
|
+
});
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
remainingMs = overallDeadline - Date.now();
|
|
4389
|
+
if (remainingMs <= 0) {
|
|
4152
4390
|
return ok({
|
|
4153
4391
|
slug: args.slug,
|
|
4154
4392
|
generated,
|
|
4155
4393
|
stopped_at: sid,
|
|
4156
|
-
|
|
4157
|
-
note: "
|
|
4394
|
+
timed_out: true,
|
|
4395
|
+
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.",
|
|
4158
4396
|
});
|
|
4159
4397
|
}
|
|
4160
4398
|
// Gate on THIS segment finishing before starting the next, so the next
|
|
@@ -4166,7 +4404,7 @@ registerTool(
|
|
|
4166
4404
|
args.slug,
|
|
4167
4405
|
sid,
|
|
4168
4406
|
extra,
|
|
4169
|
-
|
|
4407
|
+
overallDeadline,
|
|
4170
4408
|
15000,
|
|
4171
4409
|
);
|
|
4172
4410
|
if (seg_result.status === "failed") {
|
|
@@ -4187,7 +4425,45 @@ registerTool(
|
|
|
4187
4425
|
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.",
|
|
4188
4426
|
});
|
|
4189
4427
|
}
|
|
4190
|
-
generated.push(sid);
|
|
4428
|
+
if (initialStatus !== "processing") generated.push(sid);
|
|
4429
|
+
}
|
|
4430
|
+
|
|
4431
|
+
// Reconcile against a fresh authoritative snapshot. A scene may have been
|
|
4432
|
+
// processing when this call started, or a completion webhook may still be
|
|
4433
|
+
// landing; never claim "all generated" while any AI scene is unfinished.
|
|
4434
|
+
if (overallDeadline - Date.now() < 1_000) {
|
|
4435
|
+
return ok({
|
|
4436
|
+
slug: args.slug,
|
|
4437
|
+
generated,
|
|
4438
|
+
timed_out: true,
|
|
4439
|
+
note: "Wall-clock budget reached before the final reconciliation read. Re-run generate_all_segments to confirm completion; completed scenes are skipped.",
|
|
4440
|
+
});
|
|
4441
|
+
}
|
|
4442
|
+
const finalRes = await client.get<{ data: unknown }>(
|
|
4443
|
+
`/editor/${args.slug}`,
|
|
4444
|
+
undefined,
|
|
4445
|
+
overallDeadline,
|
|
4446
|
+
);
|
|
4447
|
+
const finalData = asRecord(asRecord(finalRes).data);
|
|
4448
|
+
const finalSegments = Array.isArray(finalData.segments)
|
|
4449
|
+
? (finalData.segments as Record<string, unknown>[])
|
|
4450
|
+
: [];
|
|
4451
|
+
const stillUnfinished = finalSegments.filter((segment) => {
|
|
4452
|
+
if (segment.source_type === "upload") return false;
|
|
4453
|
+
const status =
|
|
4454
|
+
(segment.status as string) ?? (segment.generation_status as string);
|
|
4455
|
+
return status !== "completed";
|
|
4456
|
+
});
|
|
4457
|
+
if (stillUnfinished.length > 0) {
|
|
4458
|
+
return ok({
|
|
4459
|
+
slug: args.slug,
|
|
4460
|
+
generated,
|
|
4461
|
+
timed_out: true,
|
|
4462
|
+
in_flight_segment_ids: stillUnfinished
|
|
4463
|
+
.map((segment) => segment.id)
|
|
4464
|
+
.filter((id) => id !== undefined),
|
|
4465
|
+
note: "Some scenes are still processing. Re-run generate_all_segments later; it will wait for in-flight scenes and generate only unfinished ones.",
|
|
4466
|
+
});
|
|
4191
4467
|
}
|
|
4192
4468
|
return ok({
|
|
4193
4469
|
slug: args.slug,
|
|
@@ -4988,7 +5264,7 @@ registerTool(
|
|
|
4988
5264
|
{
|
|
4989
5265
|
title: "Set a brand logo overlay (0 credits)",
|
|
4990
5266
|
description:
|
|
4991
|
-
"Uploads a local brand logo and overlays it on the video. Accepted: .png/.jpg/.jpeg (≤
|
|
5267
|
+
"Uploads a local brand logo and overlays it on the video. Accepted: .png/.jpg/.jpeg (≤5 MiB; PNG keeps " +
|
|
4992
5268
|
"transparency), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. Optional placement controls: treatment, " +
|
|
4993
5269
|
"position, duration_seconds.",
|
|
4994
5270
|
inputSchema: {
|
|
@@ -5043,11 +5319,11 @@ registerTool(
|
|
|
5043
5319
|
),
|
|
5044
5320
|
);
|
|
5045
5321
|
|
|
5046
|
-
// ── Shorts: branding images (product + end-card poster)
|
|
5322
|
+
// ── Shorts: branding images (product + logo + end-card poster) ───────────────
|
|
5047
5323
|
//
|
|
5048
5324
|
// The editor branding tools above hit /editor/...; these hit /shorts/... A short
|
|
5049
|
-
//
|
|
5050
|
-
//
|
|
5325
|
+
// Product images are woven into AI footage; logos are deterministic Forge
|
|
5326
|
+
// overlays; posters are closing cards. All are 0 credits.
|
|
5051
5327
|
|
|
5052
5328
|
registerTool(
|
|
5053
5329
|
"set_short_product",
|
|
@@ -5096,6 +5372,57 @@ registerTool(
|
|
|
5096
5372
|
),
|
|
5097
5373
|
);
|
|
5098
5374
|
|
|
5375
|
+
registerTool(
|
|
5376
|
+
"set_short_logo",
|
|
5377
|
+
{
|
|
5378
|
+
title: "Set a Short brand logo overlay (0 credits)",
|
|
5379
|
+
description:
|
|
5380
|
+
"Uploads an exact brand logo for deterministic compositing over a Short. The image is never sent to the " +
|
|
5381
|
+
"AI video provider, so lettering is preserved. PNG/JPEG, ≤5 MiB, 0 credits. Choose a corner and whether " +
|
|
5382
|
+
"it appears throughout the footage or only on the end card.",
|
|
5383
|
+
inputSchema: {
|
|
5384
|
+
slug: z.string().describe("Short slug"),
|
|
5385
|
+
file_path: z.string().describe("Local logo image (.png/.jpg/.jpeg)"),
|
|
5386
|
+
position: z
|
|
5387
|
+
.enum(["top-left", "top-right", "bottom-left", "bottom-right"])
|
|
5388
|
+
.optional()
|
|
5389
|
+
.describe("Logo corner (default top-right)"),
|
|
5390
|
+
treatment: z
|
|
5391
|
+
.enum(["throughout", "end_card", "none"])
|
|
5392
|
+
.optional()
|
|
5393
|
+
.describe("When the logo appears (default throughout)"),
|
|
5394
|
+
},
|
|
5395
|
+
annotations: { title: "Set Short logo", ...WRITE, idempotentHint: false },
|
|
5396
|
+
},
|
|
5397
|
+
tool(
|
|
5398
|
+
async (
|
|
5399
|
+
args: {
|
|
5400
|
+
slug: string;
|
|
5401
|
+
file_path: string;
|
|
5402
|
+
position?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
5403
|
+
treatment?: "throughout" | "end_card" | "none";
|
|
5404
|
+
},
|
|
5405
|
+
client,
|
|
5406
|
+
) => {
|
|
5407
|
+
const { s3_key } = await uploadImageFile(
|
|
5408
|
+
client,
|
|
5409
|
+
`/shorts/${args.slug}/logo/presign`,
|
|
5410
|
+
args.file_path,
|
|
5411
|
+
MAX_SHORT_LOGO_BYTES,
|
|
5412
|
+
);
|
|
5413
|
+
const res = await client.post<{ data: unknown }>(
|
|
5414
|
+
`/shorts/${args.slug}/logo/confirm`,
|
|
5415
|
+
{
|
|
5416
|
+
s3_key,
|
|
5417
|
+
position: args.position ?? "top-right",
|
|
5418
|
+
treatment: args.treatment ?? "throughout",
|
|
5419
|
+
},
|
|
5420
|
+
);
|
|
5421
|
+
return ok(asRecord(res).data ?? res);
|
|
5422
|
+
},
|
|
5423
|
+
),
|
|
5424
|
+
);
|
|
5425
|
+
|
|
5099
5426
|
registerTool(
|
|
5100
5427
|
"set_short_poster",
|
|
5101
5428
|
{
|
|
@@ -5105,7 +5432,7 @@ registerTool(
|
|
|
5105
5432
|
"render from 12s to 14s). Accepted: " +
|
|
5106
5433
|
IMAGE_EXTS.map((e) => `.${e}`).join(", ") +
|
|
5107
5434
|
" (≤20 MiB = 20,971,520 bytes), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. There is one poster per short; calling " +
|
|
5108
|
-
"again replaces it.
|
|
5435
|
+
"again replaces it. Use set_short_logo for an exact, non-generative brand overlay.",
|
|
5109
5436
|
inputSchema: {
|
|
5110
5437
|
slug: z.string().describe("Short slug (from create_short)"),
|
|
5111
5438
|
file_path: z.string().describe("Local poster image (.jpg/.jpeg/.png)"),
|