@hubfluencer/mcp 0.8.2 → 0.9.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 +23 -12
- package/dist/index.js +611 -175
- package/package.json +1 -1
- package/src/campaign.ts +1 -1
- package/src/client.ts +4 -1
- package/src/core.ts +307 -15
- package/src/index.ts +511 -84
- package/src/output-schemas.ts +13 -2
- package/src/uploads.ts +401 -163
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* One-shot : make_video({ prompt }) → finished MP4 (create→start→poll→download)
|
|
11
11
|
* Shorts : create_short → generate_short → wait_for_completion → download_result
|
|
12
12
|
* (then iterate free: update_short → rerender_short, 0 credits)
|
|
13
|
-
* Editor : create_editor_ad (
|
|
13
|
+
* Editor : create_editor_ad (create + assets + quote; max_credits launches) → wait_for_completion → download_result
|
|
14
14
|
* Granular : create_editor_draft → generate_scenario|set_scenario → get_editor
|
|
15
15
|
* → apply_scenario (scenario → scene prompts) | set_scene_count + set_segment_prompt
|
|
16
16
|
* → generate_segment(s) (+ regenerate_segment to re-roll a completed scene)
|
|
@@ -116,7 +116,14 @@ import {
|
|
|
116
116
|
} from "./series-calendar.js";
|
|
117
117
|
import {
|
|
118
118
|
CATALOG_ASSET_EXTS,
|
|
119
|
+
closePreparedUploadFile,
|
|
119
120
|
IMAGE_EXTS,
|
|
121
|
+
MAX_CLOSING_IMAGE_BYTES,
|
|
122
|
+
MAX_LOGO_BYTES,
|
|
123
|
+
MAX_PRODUCT_IMAGE_BYTES,
|
|
124
|
+
type PreparedUploadFile,
|
|
125
|
+
preparedUploadIdentity,
|
|
126
|
+
prepareImageUpload,
|
|
120
127
|
resolveVideoReadPath,
|
|
121
128
|
uploadCatalogAsset,
|
|
122
129
|
uploadImageFile,
|
|
@@ -146,6 +153,68 @@ async function fetchStatus(
|
|
|
146
153
|
return normalizeStatus(kind, slug, asRecord(res).data);
|
|
147
154
|
}
|
|
148
155
|
|
|
156
|
+
async function editorAutopilotQuote(
|
|
157
|
+
client: HubfluencerClient,
|
|
158
|
+
slug: string,
|
|
159
|
+
overrides: {
|
|
160
|
+
language?: string;
|
|
161
|
+
product_subject?: string;
|
|
162
|
+
product_prompt?: string;
|
|
163
|
+
} = {},
|
|
164
|
+
): Promise<{
|
|
165
|
+
estimated_credits: number | null;
|
|
166
|
+
available_credits: number | null;
|
|
167
|
+
affordable: boolean;
|
|
168
|
+
}> {
|
|
169
|
+
try {
|
|
170
|
+
const params = new URLSearchParams();
|
|
171
|
+
if (overrides.language) params.set("language", overrides.language);
|
|
172
|
+
if (overrides.product_subject)
|
|
173
|
+
params.set("product_subject", overrides.product_subject);
|
|
174
|
+
if (overrides.product_prompt)
|
|
175
|
+
params.set("product_prompt", overrides.product_prompt);
|
|
176
|
+
const query = params.toString();
|
|
177
|
+
const response = await client.get<{ data: unknown }>(
|
|
178
|
+
`/editor/${slug}/autopilot/cost${query ? `?${query}` : ""}`,
|
|
179
|
+
);
|
|
180
|
+
const cost = asRecord(asRecord(response).data);
|
|
181
|
+
const estimated_credits =
|
|
182
|
+
typeof cost.total === "number" ? cost.total : null;
|
|
183
|
+
const available_credits =
|
|
184
|
+
typeof cost.available_credits === "number"
|
|
185
|
+
? cost.available_credits
|
|
186
|
+
: null;
|
|
187
|
+
return {
|
|
188
|
+
estimated_credits,
|
|
189
|
+
available_credits,
|
|
190
|
+
affordable:
|
|
191
|
+
estimated_credits !== null &&
|
|
192
|
+
available_credits !== null &&
|
|
193
|
+
available_credits >= estimated_credits,
|
|
194
|
+
};
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const apiError = error as Partial<HubfluencerError>;
|
|
197
|
+
if (
|
|
198
|
+
typeof apiError.status === "number" &&
|
|
199
|
+
apiError.status >= 400 &&
|
|
200
|
+
apiError.status < 500 &&
|
|
201
|
+
apiError.status !== 429
|
|
202
|
+
) {
|
|
203
|
+
// Authentication, scope, validation, and unknown-slug errors are stable
|
|
204
|
+
// and actionable. Preserve them rather than disguising them as a pricing
|
|
205
|
+
// outage. Only transient/network/server failures use the fail-closed quote.
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
// A spend cap cannot be enforced without a live price. Fail closed: callers
|
|
209
|
+
// receive a free draft/quote response and can retry the preflight later.
|
|
210
|
+
return {
|
|
211
|
+
estimated_credits: null,
|
|
212
|
+
available_credits: null,
|
|
213
|
+
affordable: false,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
149
218
|
function isRecompositeInProgressConflict(e: unknown): boolean {
|
|
150
219
|
const err = e as Partial<HubfluencerError>;
|
|
151
220
|
if (err?.status !== 409) return false;
|
|
@@ -501,14 +570,14 @@ const WRITE = {
|
|
|
501
570
|
const SERVER_INSTRUCTIONS = `Hubfluencer turns a prompt — or a whole product page — into finished, post-ready vertical videos and image carousels, and can run the surrounding content operation (plan → draft → review → schedule → measure → repeat).
|
|
502
571
|
|
|
503
572
|
MAKE ONE ASSET (simplest)
|
|
504
|
-
- make_video({ prompt }) creates, generates, and returns a finished MP4 — it picks a single-clip short or a multi-scene editor ad from the prompt (override with kind:"short"|"editor").
|
|
573
|
+
- make_video({ prompt }) creates, generates, and returns a finished MP4 — it picks a single-clip short or a multi-scene editor ad from the prompt (override with kind:"short"|"editor"). create_editor_ad creates/configures/prices an editor and launches only with max_credits; create_short creates a short draft; create_slider makes an image carousel; create_tracking_video burns a CV overlay onto a local clip.
|
|
505
574
|
- Granular (full control): create a draft, then drive the pipeline step by step (create_editor_draft → generate_scenario → apply_scenario → review/edit prompts (set_segment_prompt) → generate_segment(s) → generate_voice + generate_music → render; regenerate_segment re-rolls a completed scene as a new version). Poll with get_status / wait_for_completion; fetch with download_result (a slider has no MP4 — read its slides with get_slider).
|
|
506
575
|
|
|
507
576
|
DELEGATE THE CONTENT OPERATION (the full loop — steps 1-3 are $0)
|
|
508
577
|
1. ONBOARD a product: extract_product_page(url) pulls facts + imagery (SSRF-hardened, 0cr); import_product_image saves its picture/logo. Persist identity once with create_product_profile + create_brand_profile so every later draft inherits it instead of re-describing the brand.
|
|
509
578
|
2. PLAN ($0, free quota — not credits): plan_campaign proposes angles/formats + a per-item credit estimate; generate_hook_variations writes hook options. create_campaign(url | product_profile_id) does 1-2 end-to-end and returns a pack of editable DRAFTS.
|
|
510
579
|
3. PACK & MATERIALIZE ($0): create_campaign_pack / add_pack_variations assemble items; materialize_pack_item turns each into a 0-credit draft (editor_ad/short → a video project, carousel → a slider). list_campaign_packs lists packs; get_campaign_pack({ id }) shows every item's draft slug + credit estimate.
|
|
511
|
-
4. GENERATE per draft (CREDITS — a separate, explicit step):
|
|
580
|
+
4. GENERATE per draft (CREDITS — a separate, explicit step): start_autopilot({slug}) quotes free; after approval, start_autopilot({slug,max_credits:approved_cap}) launches. Run generate_short/generate_slider for approved short/carousel drafts. Nothing above this line spends video credits.
|
|
512
581
|
5. REVIEW: create_review_link mints a no-login page where a human approves/comments (list_review_links / revoke_review_link manage them). Gate spend or posting on approval.
|
|
513
582
|
6. CALENDAR (reminder-only): schedule_post / list_scheduled_posts / reschedule_post / cancel_scheduled_post stage a calendar that pushes REMINDERS to the user — it does NOT auto-publish (posting to TikTok/IG stays a human step in the app). After the user posts the presigned export, call mark_scheduled_posted.
|
|
514
583
|
7. MEASURE → DOUBLE DOWN: record_performance logs views/engagement (tag creatives via set_item_attributes); get_recommendations gives a CAUTIOUS, non-causal read of your own best hook/format (present its confidence + note as-is, never upgrade them); make_more_like_winner spins fresh hooks from a proven item — feed a winner back into add_pack_variations → step 4.
|
|
@@ -516,11 +585,11 @@ DELEGATE THE CONTENT OPERATION (the full loop — steps 1-3 are $0)
|
|
|
516
585
|
|
|
517
586
|
CREDITS (spent server-side; each tool prices before charging — see its description)
|
|
518
587
|
- Free (0 credits): every draft create/edit, reorder, all slider re-composites, short re-renders (rerender_short — applies text/style/CTA/end-card edits over a short's already-paid footage), and the whole onboard/plan/pack/materialize path (steps 1-3). get_credits shows the balance.
|
|
519
|
-
- Charged: short render 15; editor AI scene 5 (4 in a batch of ≥3); voice 3; music 5; editor render 0 (auto-charges ungenerated scenes); slider 1/slide; tracking render 1 (refunded on failure). make_video
|
|
588
|
+
- Charged: short render 15; editor AI scene 5 (4 in a batch of ≥3); voice 3; music 5; editor render 0 (auto-charges ungenerated scenes); slider 1/slide; tracking render 1 (refunded on failure). make_video supports dry_run/max_credits; create_editor_ad/start_autopilot require max_credits before launching.
|
|
520
589
|
- AI ASSISTS (all generate_*/enhance/suggest/plan/hook tools) draw a FREE daily quota (20/day), NOT credits. On 429, set auto_unlock:true to spend 1 credit for +10 assists (retried once), or write the copy yourself and use the set_* tools.
|
|
521
590
|
|
|
522
591
|
WAITING & RESUMING
|
|
523
|
-
- Renders run async and can take minutes. wait_for_completion polls a budget capped at 280s; if it returns terminal=false / timed_out, just CALL IT AGAIN with the same slug. make_video returns the slug + a resume hint on timeout.
|
|
592
|
+
- Renders run async and can take minutes. wait_for_completion polls a budget capped at 280s; if it returns terminal=false / timed_out, just CALL IT AGAIN with the same slug. make_video returns the slug + a resume hint on timeout. A terminal result is not always ready: an editor can go terminal in a needs-action stage (editing_required = stale after edits, regenerate the flagged scenes/audio; insufficient_credits = batch parked, top up / reduce scope; idle = draft with nothing generating, start generation first) — act on those instead of re-polling.
|
|
524
593
|
- Creates and charged actions are idempotency-keyed: a transport retry is safe; a genuine re-run after a terminal FAILURE restarts rather than replaying the dead attempt.
|
|
525
594
|
|
|
526
595
|
LOCAL FILES (sandboxed)
|
|
@@ -843,6 +912,8 @@ registerTool(
|
|
|
843
912
|
),
|
|
844
913
|
max_credits: z
|
|
845
914
|
.number()
|
|
915
|
+
.int()
|
|
916
|
+
.nonnegative()
|
|
846
917
|
.optional()
|
|
847
918
|
.describe(
|
|
848
919
|
"Spend cap: refuse to start (no charge) if the priced estimate exceeds this. Returns the estimate.",
|
|
@@ -1024,9 +1095,9 @@ registerTool(
|
|
|
1024
1095
|
slug = created.data.slug;
|
|
1025
1096
|
}
|
|
1026
1097
|
|
|
1027
|
-
// 2) Preflight cost vs the live balance BEFORE charging.
|
|
1028
|
-
//
|
|
1029
|
-
//
|
|
1098
|
+
// 2) Preflight cost vs the live balance BEFORE charging. Shorts retain
|
|
1099
|
+
// their fixed-price compatibility behavior, but an editor without a
|
|
1100
|
+
// readable quote must never fall through to the API's uncapped launch.
|
|
1030
1101
|
const costPath =
|
|
1031
1102
|
kind === "short"
|
|
1032
1103
|
? `/shorts/${slug}/cost`
|
|
@@ -1043,13 +1114,14 @@ registerTool(
|
|
|
1043
1114
|
? cost.available_credits
|
|
1044
1115
|
: null;
|
|
1045
1116
|
} catch {
|
|
1046
|
-
//
|
|
1117
|
+
// The editor branch below fails closed unless the caller supplied an
|
|
1118
|
+
// explicit cap that the launch endpoint can enforce independently.
|
|
1047
1119
|
}
|
|
1048
1120
|
|
|
1049
1121
|
const resume =
|
|
1050
1122
|
kind === "short"
|
|
1051
1123
|
? `generate_short({ slug: "${slug}"${args.skip_auto_text ? ", skip_auto_text: true" : ""} })`
|
|
1052
|
-
: `start_autopilot({ slug: "${slug}" })`;
|
|
1124
|
+
: `start_autopilot({ slug: "${slug}", max_credits: ${estimated_credits ?? "<approved cap>"} })`;
|
|
1053
1125
|
const affordable =
|
|
1054
1126
|
available_credits === null ||
|
|
1055
1127
|
estimated_credits === null ||
|
|
@@ -1058,17 +1130,28 @@ registerTool(
|
|
|
1058
1130
|
args.max_credits != null &&
|
|
1059
1131
|
estimated_credits != null &&
|
|
1060
1132
|
estimated_credits > args.max_credits;
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1133
|
+
const editorPriceUnavailableWithoutCap =
|
|
1134
|
+
kind === "editor" &&
|
|
1135
|
+
estimated_credits === null &&
|
|
1136
|
+
args.max_credits == null;
|
|
1137
|
+
|
|
1138
|
+
// 3) Stop BEFORE charging on dry_run, an uncapped editor with no live
|
|
1139
|
+
// quote, an over-cap estimate, or a balance we already know is too
|
|
1140
|
+
// low. The free draft is returned so the caller can retry the quote,
|
|
1141
|
+
// top up, or raise the cap without an orphan or surprise charge.
|
|
1142
|
+
if (
|
|
1143
|
+
args.dry_run ||
|
|
1144
|
+
editorPriceUnavailableWithoutCap ||
|
|
1145
|
+
overCap ||
|
|
1146
|
+
!affordable
|
|
1147
|
+
) {
|
|
1067
1148
|
const note = args.dry_run
|
|
1068
1149
|
? `Dry run — created a free draft and priced it (${estimated_credits ?? "?"} credits, have ${available_credits ?? "?"}). To run it: ${resume}.`
|
|
1069
|
-
:
|
|
1070
|
-
? `
|
|
1071
|
-
:
|
|
1150
|
+
: editorPriceUnavailableWithoutCap
|
|
1151
|
+
? `Pricing is temporarily unavailable, so the editor was left as a free draft and nothing was charged. Retry the quote, then run start_autopilot({ slug: "${slug}", max_credits: <approved cap> }).`
|
|
1152
|
+
: overCap
|
|
1153
|
+
? `Estimated ${estimated_credits} credits exceeds max_credits ${args.max_credits}; nothing charged. Raise max_credits, or run ${resume}.`
|
|
1154
|
+
: `Not enough credits (needs ${estimated_credits ?? "?"}, have ${available_credits ?? "?"}); nothing charged. Top up, then run ${resume}.`;
|
|
1072
1155
|
return ok({
|
|
1073
1156
|
slug,
|
|
1074
1157
|
kind,
|
|
@@ -1116,9 +1199,20 @@ registerTool(
|
|
|
1116
1199
|
`gen-short:${slug}:${startState}${args.skip_auto_text ? ":bare" : ""}`,
|
|
1117
1200
|
);
|
|
1118
1201
|
} else {
|
|
1202
|
+
const approvedCap = args.max_credits ?? estimated_credits;
|
|
1203
|
+
if (approvedCap === null) {
|
|
1204
|
+
// Belt-and-suspenders invariant behind the fail-closed branch above:
|
|
1205
|
+
// never let a future condition refactor revive an uncapped editor POST.
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
"Editor autopilot launch requires max_credits or a live quote",
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1119
1210
|
await client.post(
|
|
1120
1211
|
`/editor/${slug}/autopilot`,
|
|
1121
|
-
|
|
1212
|
+
// Use the caller's explicit cap when given, otherwise the live
|
|
1213
|
+
// estimate. The API recomputes under its launch lock and rejects a
|
|
1214
|
+
// price increase before any state change or charge.
|
|
1215
|
+
{ max_credits: approvedCap },
|
|
1122
1216
|
// Fold the create-affecting params AND the failure-state discriminator
|
|
1123
1217
|
// into the start key so it tracks the inputs + current state, not just
|
|
1124
1218
|
// the slug. This key is only compared against a retry of *this* tool on
|
|
@@ -2343,8 +2437,8 @@ registerTool(
|
|
|
2343
2437
|
"FREE re-render: applies the short's current text/style/CTA/end-card state over the already-paid " +
|
|
2344
2438
|
"footage and music — 0 credits. Use after update_short for copy/style/CTA tweaks. Footage or music " +
|
|
2345
2439
|
"changes need generate_short (15 credits) instead. Edits that add/remove the end card (poster, " +
|
|
2346
|
-
"end_card) can shift the video between 12s and 14s; the pinned music bed is
|
|
2347
|
-
"
|
|
2440
|
+
"end_card) can shift the video between 12s and 14s; when the pinned music bed is shorter than the " +
|
|
2441
|
+
"export it crossfades into a repeated continuation, then fades at the export end, without regeneration. " +
|
|
2348
2442
|
"Requires a completed generation to re-render over " +
|
|
2349
2443
|
"(422 short_not_ready otherwise — run generate_short once first); while any generation/render is in " +
|
|
2350
2444
|
"flight it reports in-progress (409) rather than duplicating. Then poll with get_status or " +
|
|
@@ -2817,18 +2911,20 @@ registerTool(
|
|
|
2817
2911
|
registerTool(
|
|
2818
2912
|
"create_editor_ad",
|
|
2819
2913
|
{
|
|
2820
|
-
title: "Create a multi-scene editor ad
|
|
2914
|
+
title: "Create, configure, and price a multi-scene editor ad",
|
|
2821
2915
|
description:
|
|
2822
|
-
"Creates an editor project
|
|
2916
|
+
"Creates an editor project, attaches any supplied local product/logo/closing assets BEFORE generation, " +
|
|
2917
|
+
"and prices autopilot (server-orchestrated " +
|
|
2823
2918
|
"scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " +
|
|
2824
|
-
"
|
|
2825
|
-
"
|
|
2826
|
-
"
|
|
2827
|
-
"
|
|
2828
|
-
"
|
|
2919
|
+
"Without max_credits it STOPS after the free draft/configuration and returns the live estimate. It starts " +
|
|
2920
|
+
"the paid pipeline only when max_credits is supplied and covers the estimate. Then poll with get_status / " +
|
|
2921
|
+
"wait_for_completion (kind=editor). Prefer make_video for a prompt-only one-shot. " +
|
|
2922
|
+
"Supplying assets here is safer than adding them after autopilot because the scenario and scenes are grounded " +
|
|
2923
|
+
"in the real product from the start. (Editor ads carry their copy in-scene/narration, not as a " +
|
|
2829
2924
|
"title overlay; for an on-screen title/subtitle use a short with headline/subheadline instead.) " +
|
|
2830
|
-
"For
|
|
2831
|
-
"
|
|
2925
|
+
"For a mascot or hero product that must stay visually identical with no human presenter, set " +
|
|
2926
|
+
"cast_mode:'product_only' together with product_image_path. Presenter-led casting still belongs on " +
|
|
2927
|
+
"create_editor_draft because it requires a separately configured presenter.",
|
|
2832
2928
|
inputSchema: {
|
|
2833
2929
|
product_prompt: z
|
|
2834
2930
|
.string()
|
|
@@ -2884,6 +2980,61 @@ registerTool(
|
|
|
2884
2980
|
.enum(["9:16", "16:9", "1:1"])
|
|
2885
2981
|
.optional()
|
|
2886
2982
|
.describe('Aspect ratio (default "9:16")'),
|
|
2983
|
+
product_image_path: z
|
|
2984
|
+
.string()
|
|
2985
|
+
.optional()
|
|
2986
|
+
.describe(
|
|
2987
|
+
"Local product image (.jpg/.jpeg/.png, ≤8 MiB) to attach before autopilot.",
|
|
2988
|
+
),
|
|
2989
|
+
product_description: z
|
|
2990
|
+
.string()
|
|
2991
|
+
.max(500)
|
|
2992
|
+
.optional()
|
|
2993
|
+
.describe(
|
|
2994
|
+
"Exact product/character description used with product_image_path.",
|
|
2995
|
+
),
|
|
2996
|
+
closing_image_path: z
|
|
2997
|
+
.string()
|
|
2998
|
+
.optional()
|
|
2999
|
+
.describe("Local closing-card image (.jpg/.jpeg/.png, ≤20 MiB)."),
|
|
3000
|
+
logo_path: z
|
|
3001
|
+
.string()
|
|
3002
|
+
.optional()
|
|
3003
|
+
.describe("Local logo (.jpg/.jpeg/.png, ≤1 MiB) to overlay."),
|
|
3004
|
+
logo_treatment: z
|
|
3005
|
+
.enum(["intro", "outro", "both", "none"])
|
|
3006
|
+
.optional()
|
|
3007
|
+
.describe(
|
|
3008
|
+
'When the logo bumper plays: "intro", "outro", "both" (default), or "none". Requires logo_path.',
|
|
3009
|
+
),
|
|
3010
|
+
logo_position: z
|
|
3011
|
+
.enum(["top-left", "top-right", "bottom-left", "bottom-right"])
|
|
3012
|
+
.optional()
|
|
3013
|
+
.describe(
|
|
3014
|
+
'Logo corner: "top-left", "top-right" (default), "bottom-left", or "bottom-right". Requires logo_path.',
|
|
3015
|
+
),
|
|
3016
|
+
logo_duration_seconds: z
|
|
3017
|
+
.number()
|
|
3018
|
+
.min(0.5)
|
|
3019
|
+
.max(3)
|
|
3020
|
+
.optional()
|
|
3021
|
+
.describe(
|
|
3022
|
+
"Logo bumper duration in seconds, 0.5–3 (default 1.5). Requires logo_path.",
|
|
3023
|
+
),
|
|
3024
|
+
cast_mode: z
|
|
3025
|
+
.literal("product_only")
|
|
3026
|
+
.optional()
|
|
3027
|
+
.describe(
|
|
3028
|
+
"Lock every AI scene to the supplied product/mascot reference with no people. Requires product_image_path.",
|
|
3029
|
+
),
|
|
3030
|
+
max_credits: z
|
|
3031
|
+
.number()
|
|
3032
|
+
.int()
|
|
3033
|
+
.nonnegative()
|
|
3034
|
+
.optional()
|
|
3035
|
+
.describe(
|
|
3036
|
+
"Explicit spend cap. Omit to create/configure/price only; no credits are charged.",
|
|
3037
|
+
),
|
|
2887
3038
|
},
|
|
2888
3039
|
outputSchema: createEditorOutput,
|
|
2889
3040
|
annotations: { title: "Create editor ad", ...WRITE, idempotentHint: true },
|
|
@@ -2900,47 +3051,219 @@ registerTool(
|
|
|
2900
3051
|
theme?: string;
|
|
2901
3052
|
voice_id?: string;
|
|
2902
3053
|
export_aspect_ratio?: string;
|
|
3054
|
+
product_image_path?: string;
|
|
3055
|
+
product_description?: string;
|
|
3056
|
+
closing_image_path?: string;
|
|
3057
|
+
logo_path?: string;
|
|
3058
|
+
logo_treatment?: "intro" | "outro" | "both" | "none";
|
|
3059
|
+
logo_position?:
|
|
3060
|
+
| "top-left"
|
|
3061
|
+
| "top-right"
|
|
3062
|
+
| "bottom-left"
|
|
3063
|
+
| "bottom-right";
|
|
3064
|
+
logo_duration_seconds?: number;
|
|
3065
|
+
cast_mode?: "product_only";
|
|
3066
|
+
max_credits?: number;
|
|
2903
3067
|
},
|
|
2904
3068
|
client,
|
|
2905
3069
|
) => {
|
|
2906
|
-
const
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
3070
|
+
const prepared: PreparedUploadFile[] = [];
|
|
3071
|
+
let productImage: PreparedUploadFile | undefined;
|
|
3072
|
+
let closingImage: PreparedUploadFile | undefined;
|
|
3073
|
+
let logo: PreparedUploadFile | undefined;
|
|
3074
|
+
try {
|
|
3075
|
+
const langErr = validateLanguage(args.language);
|
|
3076
|
+
if (langErr) return fail(langErr);
|
|
3077
|
+
const promptErr = validateProductPrompt(args.product_prompt);
|
|
3078
|
+
if (promptErr) return fail(promptErr);
|
|
3079
|
+
if (args.cast_mode === "product_only" && !args.product_image_path) {
|
|
3080
|
+
return fail(
|
|
3081
|
+
"cast_mode product_only requires product_image_path so identity can be locked before generation.",
|
|
3082
|
+
);
|
|
3083
|
+
}
|
|
3084
|
+
// Fail fast on settings that would otherwise be silently dropped: the
|
|
3085
|
+
// logo PATCH is gated on logo_path and the product_description is only
|
|
3086
|
+
// sent with product_image_path, so a logo_* / product_description passed
|
|
3087
|
+
// without its asset is a caller mistake. Reject BEFORE creating even a
|
|
3088
|
+
// free draft (consistent with the asset validation just below), rather
|
|
3089
|
+
// than accepting the call and ignoring the field.
|
|
3090
|
+
if (
|
|
3091
|
+
(args.logo_treatment !== undefined ||
|
|
3092
|
+
args.logo_position !== undefined ||
|
|
3093
|
+
args.logo_duration_seconds !== undefined) &&
|
|
3094
|
+
!args.logo_path
|
|
3095
|
+
) {
|
|
3096
|
+
return fail(
|
|
3097
|
+
"logo_treatment/logo_position/logo_duration_seconds require logo_path (the logo image they style). Provide logo_path or drop these settings.",
|
|
3098
|
+
);
|
|
3099
|
+
}
|
|
3100
|
+
if (
|
|
3101
|
+
args.product_description !== undefined &&
|
|
3102
|
+
!args.product_image_path
|
|
3103
|
+
) {
|
|
3104
|
+
return fail(
|
|
3105
|
+
"product_description requires product_image_path (the product image it describes). Provide product_image_path or drop product_description.",
|
|
3106
|
+
);
|
|
3107
|
+
}
|
|
2910
3108
|
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
3109
|
+
// Validate every local asset before creating even a free draft, so a typo,
|
|
3110
|
+
// traversal attempt, bad extension, or oversized logo cannot leave an
|
|
3111
|
+
// orphaned project behind.
|
|
3112
|
+
if (args.product_image_path) {
|
|
3113
|
+
productImage = await prepareImageUpload(
|
|
3114
|
+
args.product_image_path,
|
|
3115
|
+
MAX_PRODUCT_IMAGE_BYTES,
|
|
3116
|
+
);
|
|
3117
|
+
prepared.push(productImage);
|
|
3118
|
+
}
|
|
3119
|
+
if (args.closing_image_path) {
|
|
3120
|
+
closingImage = await prepareImageUpload(
|
|
3121
|
+
args.closing_image_path,
|
|
3122
|
+
MAX_CLOSING_IMAGE_BYTES,
|
|
3123
|
+
);
|
|
3124
|
+
prepared.push(closingImage);
|
|
3125
|
+
}
|
|
3126
|
+
if (args.logo_path) {
|
|
3127
|
+
logo = await prepareImageUpload(args.logo_path, MAX_LOGO_BYTES);
|
|
3128
|
+
prepared.push(logo);
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
const created = await client.post<{ data: { slug: string } }>(
|
|
3132
|
+
"/editor",
|
|
3133
|
+
{
|
|
3134
|
+
language: args.language ?? "en",
|
|
3135
|
+
product_prompt: args.product_prompt,
|
|
3136
|
+
product_subject: args.product_subject,
|
|
3137
|
+
project_intent: args.project_intent,
|
|
3138
|
+
creative_format: args.creative_format,
|
|
3139
|
+
visual_language: args.visual_language,
|
|
3140
|
+
theme: args.theme,
|
|
3141
|
+
voice_id: args.voice_id,
|
|
3142
|
+
export_aspect_ratio: args.export_aspect_ratio,
|
|
3143
|
+
cast_mode: args.cast_mode,
|
|
3144
|
+
},
|
|
3145
|
+
// Fold every create-affecting param into the key so two calls that
|
|
3146
|
+
// differ only by style/theme/voice/aspect get distinct drafts.
|
|
3147
|
+
// Composition lives in core.ts, shared with the golden-pin test.
|
|
3148
|
+
editorAdCreateKey({
|
|
3149
|
+
...args,
|
|
3150
|
+
product_image_identity: productImage
|
|
3151
|
+
? preparedUploadIdentity(productImage)
|
|
3152
|
+
: undefined,
|
|
3153
|
+
closing_image_identity: closingImage
|
|
3154
|
+
? preparedUploadIdentity(closingImage)
|
|
3155
|
+
: undefined,
|
|
3156
|
+
logo_identity: logo ? preparedUploadIdentity(logo) : undefined,
|
|
3157
|
+
}),
|
|
3158
|
+
);
|
|
3159
|
+
const slug = created.data.slug;
|
|
3160
|
+
let configured = {} as Record<string, unknown>;
|
|
3161
|
+
if (
|
|
3162
|
+
args.product_image_path ||
|
|
3163
|
+
args.closing_image_path ||
|
|
3164
|
+
args.logo_path
|
|
3165
|
+
) {
|
|
3166
|
+
const current = await client.get<{ data: unknown }>(
|
|
3167
|
+
`/editor/${slug}`,
|
|
3168
|
+
);
|
|
3169
|
+
configured = asRecord(asRecord(current).data);
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
if (productImage && !configured.product_url) {
|
|
3173
|
+
const { s3_key } = await uploadImageFile(
|
|
3174
|
+
client,
|
|
3175
|
+
`/editor/${slug}/product/presign`,
|
|
3176
|
+
productImage,
|
|
3177
|
+
MAX_PRODUCT_IMAGE_BYTES,
|
|
3178
|
+
);
|
|
3179
|
+
await client.post(`/editor/${slug}/product/confirm`, {
|
|
3180
|
+
s3_key,
|
|
3181
|
+
product_description: args.product_description,
|
|
3182
|
+
});
|
|
3183
|
+
}
|
|
3184
|
+
if (closingImage && !configured.closing_image_url) {
|
|
3185
|
+
const { s3_key } = await uploadImageFile(
|
|
3186
|
+
client,
|
|
3187
|
+
`/editor/${slug}/closing-image/presign`,
|
|
3188
|
+
closingImage,
|
|
3189
|
+
MAX_CLOSING_IMAGE_BYTES,
|
|
3190
|
+
);
|
|
3191
|
+
await client.post(`/editor/${slug}/closing-image/confirm`, {
|
|
3192
|
+
s3_key,
|
|
3193
|
+
});
|
|
3194
|
+
}
|
|
3195
|
+
if (logo && !configured.logo_url) {
|
|
3196
|
+
const { s3_key } = await uploadImageFile(
|
|
3197
|
+
client,
|
|
3198
|
+
`/editor/${slug}/logo/presign`,
|
|
3199
|
+
logo,
|
|
3200
|
+
MAX_LOGO_BYTES,
|
|
3201
|
+
);
|
|
3202
|
+
await client.post(`/editor/${slug}/logo/confirm`, { s3_key });
|
|
3203
|
+
}
|
|
3204
|
+
const logoSettings: Record<string, unknown> = {};
|
|
3205
|
+
if (args.logo_treatment !== undefined)
|
|
3206
|
+
logoSettings.logo_treatment = args.logo_treatment;
|
|
3207
|
+
if (args.logo_position !== undefined)
|
|
3208
|
+
logoSettings.logo_position = args.logo_position;
|
|
3209
|
+
if (args.logo_duration_seconds !== undefined)
|
|
3210
|
+
logoSettings.logo_duration_seconds = args.logo_duration_seconds;
|
|
3211
|
+
if (args.logo_path && Object.keys(logoSettings).length > 0)
|
|
3212
|
+
await client.patch(`/editor/${slug}/logo`, logoSettings);
|
|
3213
|
+
|
|
3214
|
+
const quote = await editorAutopilotQuote(client, slug);
|
|
3215
|
+
const overCap =
|
|
3216
|
+
args.max_credits !== undefined &&
|
|
3217
|
+
quote.estimated_credits !== null &&
|
|
3218
|
+
quote.estimated_credits > args.max_credits;
|
|
3219
|
+
if (
|
|
3220
|
+
args.max_credits === undefined ||
|
|
3221
|
+
quote.estimated_credits === null ||
|
|
3222
|
+
overCap ||
|
|
3223
|
+
!quote.affordable
|
|
3224
|
+
) {
|
|
3225
|
+
const note =
|
|
3226
|
+
args.max_credits === undefined
|
|
3227
|
+
? `Free draft configured. Estimated ${quote.estimated_credits ?? "?"} credits (have ${quote.available_credits ?? "?"}); nothing charged. Relaunch with start_autopilot({ slug: "${slug}", max_credits: ${quote.estimated_credits ?? "<approved cap>"} }).`
|
|
3228
|
+
: quote.estimated_credits === null
|
|
3229
|
+
? "Pricing is temporarily unavailable, so the paid pipeline was not started. Retry the quote later."
|
|
3230
|
+
: overCap
|
|
3231
|
+
? `Estimated ${quote.estimated_credits} credits exceeds max_credits ${args.max_credits}; nothing charged.`
|
|
3232
|
+
: `Not enough credits (needs ${quote.estimated_credits}, have ${quote.available_credits ?? "?"}); nothing charged.`;
|
|
3233
|
+
return ok({
|
|
3234
|
+
slug,
|
|
3235
|
+
kind: "editor",
|
|
3236
|
+
...quote,
|
|
3237
|
+
charged: false,
|
|
3238
|
+
note,
|
|
3239
|
+
next: "start_autopilot",
|
|
3240
|
+
});
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
const started = await client.post<{ data: unknown }>(
|
|
3244
|
+
`/editor/${slug}/autopilot`,
|
|
3245
|
+
{ max_credits: args.max_credits },
|
|
3246
|
+
// Fold the create-affecting params into the start key so it tracks
|
|
3247
|
+
// inputs, not just the slug. Self-consistent per-tool only: this key
|
|
3248
|
+
// is compared against retries of *this* tool on *this* slug. The
|
|
3249
|
+
// field set/order differs from make_video / start_autopilot on
|
|
3250
|
+
// purpose (server cache is path+key scoped, slug is unique per
|
|
3251
|
+
// draft), so the keys are not interchangeable across tools.
|
|
3252
|
+
// Composition lives in core.ts, shared with the golden-pin test.
|
|
3253
|
+
editorAdAutopilotKey(slug, args),
|
|
3254
|
+
);
|
|
3255
|
+
const status = normalizeStatus("editor", slug, asRecord(started).data);
|
|
3256
|
+
return ok({
|
|
3257
|
+
slug,
|
|
3258
|
+
kind: "editor",
|
|
3259
|
+
status,
|
|
3260
|
+
...quote,
|
|
3261
|
+
charged: true,
|
|
3262
|
+
next: "wait_for_completion",
|
|
3263
|
+
});
|
|
3264
|
+
} finally {
|
|
3265
|
+
await Promise.all(prepared.map(closePreparedUploadFile));
|
|
3266
|
+
}
|
|
2944
3267
|
},
|
|
2945
3268
|
),
|
|
2946
3269
|
);
|
|
@@ -2951,8 +3274,9 @@ registerTool(
|
|
|
2951
3274
|
title: "Start autopilot on an existing editor draft",
|
|
2952
3275
|
description:
|
|
2953
3276
|
"Starts server-orchestrated autopilot (scenario → segments → narration → voice → music → render) on an " +
|
|
2954
|
-
"EXISTING editor project/draft.
|
|
2955
|
-
"
|
|
3277
|
+
"EXISTING editor project/draft. Without max_credits it returns the live quote and charges nothing. Supply an " +
|
|
3278
|
+
"explicit max_credits cap to authorize launch; the tool refuses if pricing is unavailable, the estimate exceeds " +
|
|
3279
|
+
"the cap, or the balance is insufficient. Each authorized call is a genuine start attempt (like tapping Launch in the app): " +
|
|
2956
3280
|
"if a run is already in progress for this slug the server returns already_running — it never double-starts " +
|
|
2957
3281
|
"or double-charges. Once finished, failed, or cancelled, calling again relaunches it (resume after a top-up, raising your spend cap, or a mid-pipeline failure). Then poll with " +
|
|
2958
3282
|
'wait_for_completion({ slug, kind: "editor" }).',
|
|
@@ -2977,7 +3301,16 @@ registerTool(
|
|
|
2977
3301
|
.string()
|
|
2978
3302
|
.optional()
|
|
2979
3303
|
.describe("Optional freeform brief to set/replace before launch."),
|
|
3304
|
+
max_credits: z
|
|
3305
|
+
.number()
|
|
3306
|
+
.int()
|
|
3307
|
+
.nonnegative()
|
|
3308
|
+
.optional()
|
|
3309
|
+
.describe(
|
|
3310
|
+
"Explicit spend cap. Omit to quote only; no credits are charged.",
|
|
3311
|
+
),
|
|
2980
3312
|
},
|
|
3313
|
+
outputSchema: createEditorOutput,
|
|
2981
3314
|
annotations: { title: "Start autopilot", ...WRITE, idempotentHint: false },
|
|
2982
3315
|
},
|
|
2983
3316
|
tool(
|
|
@@ -2987,6 +3320,7 @@ registerTool(
|
|
|
2987
3320
|
language?: string;
|
|
2988
3321
|
product_subject?: string;
|
|
2989
3322
|
product_prompt?: string;
|
|
3323
|
+
max_credits?: number;
|
|
2990
3324
|
},
|
|
2991
3325
|
client,
|
|
2992
3326
|
) => {
|
|
@@ -2996,16 +3330,47 @@ registerTool(
|
|
|
2996
3330
|
if (promptErr) return fail(promptErr);
|
|
2997
3331
|
|
|
2998
3332
|
const slug = args.slug;
|
|
2999
|
-
const overrides:
|
|
3333
|
+
const overrides: {
|
|
3334
|
+
language?: string;
|
|
3335
|
+
product_subject?: string;
|
|
3336
|
+
product_prompt?: string;
|
|
3337
|
+
} = {};
|
|
3000
3338
|
if (args.language !== undefined) overrides.language = args.language;
|
|
3001
3339
|
if (args.product_subject !== undefined)
|
|
3002
3340
|
overrides.product_subject = args.product_subject;
|
|
3003
3341
|
if (args.product_prompt !== undefined)
|
|
3004
3342
|
overrides.product_prompt = args.product_prompt;
|
|
3005
3343
|
|
|
3344
|
+
const quote = await editorAutopilotQuote(client, slug, overrides);
|
|
3345
|
+
const overCap =
|
|
3346
|
+
args.max_credits !== undefined &&
|
|
3347
|
+
quote.estimated_credits !== null &&
|
|
3348
|
+
quote.estimated_credits > args.max_credits;
|
|
3349
|
+
if (
|
|
3350
|
+
args.max_credits === undefined ||
|
|
3351
|
+
quote.estimated_credits === null ||
|
|
3352
|
+
overCap ||
|
|
3353
|
+
!quote.affordable
|
|
3354
|
+
) {
|
|
3355
|
+
return ok({
|
|
3356
|
+
slug,
|
|
3357
|
+
kind: "editor",
|
|
3358
|
+
...quote,
|
|
3359
|
+
charged: false,
|
|
3360
|
+
note:
|
|
3361
|
+
args.max_credits === undefined
|
|
3362
|
+
? `Quote only: estimated ${quote.estimated_credits ?? "?"} credits (have ${quote.available_credits ?? "?"}). Pass max_credits to authorize launch.`
|
|
3363
|
+
: quote.estimated_credits === null
|
|
3364
|
+
? "Pricing is unavailable; launch refused because the spend cap cannot be enforced."
|
|
3365
|
+
: overCap
|
|
3366
|
+
? `Estimated ${quote.estimated_credits} credits exceeds max_credits ${args.max_credits}; nothing charged.`
|
|
3367
|
+
: `Not enough credits (needs ${quote.estimated_credits}, have ${quote.available_credits ?? "?"}); nothing charged.`,
|
|
3368
|
+
});
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3006
3371
|
const started = await client.post<{ data: unknown }>(
|
|
3007
3372
|
`/editor/${slug}/autopilot`,
|
|
3008
|
-
|
|
3373
|
+
{ ...overrides, max_credits: args.max_credits },
|
|
3009
3374
|
// Each call is a genuine new start attempt (matching the in-app Launch
|
|
3010
3375
|
// button). A per-invocation nonce makes the idempotency key unique so the
|
|
3011
3376
|
// server's 24h idempotency cache can't replay a PRIOR start's response on
|
|
@@ -3020,6 +3385,8 @@ registerTool(
|
|
|
3020
3385
|
slug,
|
|
3021
3386
|
kind: "editor",
|
|
3022
3387
|
status,
|
|
3388
|
+
...quote,
|
|
3389
|
+
charged: true,
|
|
3023
3390
|
next: "wait_for_completion",
|
|
3024
3391
|
});
|
|
3025
3392
|
},
|
|
@@ -3087,7 +3454,7 @@ registerTool(
|
|
|
3087
3454
|
title: "Create an editor draft (no autopilot)",
|
|
3088
3455
|
description:
|
|
3089
3456
|
"Creates an editor project from a product prompt and stops (NO autopilot). Costs 0 credits. Returns the " +
|
|
3090
|
-
"slug. Use this to drive the pipeline step by step (vs create_editor_ad which
|
|
3457
|
+
"slug. Use this to drive the pipeline step by step (vs create_editor_ad which configures, quotes, and can launch autopilot with a spend cap). " +
|
|
3091
3458
|
"Each AI-generated scene renders to a fixed 8 seconds (uploaded clips keep their own length). " +
|
|
3092
3459
|
"product_prompt is optional content: send 10–5000 chars, or omit it entirely (empty is treated as omit).",
|
|
3093
3460
|
inputSchema: {
|
|
@@ -4168,7 +4535,7 @@ registerTool(
|
|
|
4168
4535
|
"duration. Runs presign → PUT (resumable multipart for large files) → confirm, then polls until the " +
|
|
4169
4536
|
"upload is processed (ready). file_path is a LOCAL path confined to HUBFLUENCER_INPUT_DIR (or cwd). Accepted: " +
|
|
4170
4537
|
VIDEO_EXTS.map((e) => `.${e}`).join(", ") +
|
|
4171
|
-
" (≤500 MB, ≤5 min). Costs 0 credits. If add_to_timeline is false (default true) the clip is uploaded but not " +
|
|
4538
|
+
" (≤500 MB decimal = 500,000,000 bytes, ≤5 min). Costs 0 credits. If add_to_timeline is false (default true) the clip is uploaded but not " +
|
|
4172
4539
|
"placed — call add_segment_from_upload later.",
|
|
4173
4540
|
inputSchema: {
|
|
4174
4541
|
slug: z.string().describe("Editor project slug"),
|
|
@@ -4315,7 +4682,8 @@ registerTool(
|
|
|
4315
4682
|
description:
|
|
4316
4683
|
"Sets a specific scene's video from an existing asset — either a ready upload (upload_id) OR a completed scene " +
|
|
4317
4684
|
"in the same project (source_segment_id). Provide EXACTLY ONE. 0 credits. Use it to reuse one uploaded clip " +
|
|
4318
|
-
"across scenes, or to swap a generated scene for your own footage."
|
|
4685
|
+
"across scenes, or to swap a generated scene for your own footage. The response includes continuity impact and " +
|
|
4686
|
+
"current narration/voice/music staleness. If the replacement duration differs, regenerate stale audio before render.",
|
|
4319
4687
|
inputSchema: {
|
|
4320
4688
|
slug: z.string().describe("Editor project slug"),
|
|
4321
4689
|
segment_id: z
|
|
@@ -4358,7 +4726,58 @@ registerTool(
|
|
|
4358
4726
|
`/editor/${args.slug}/segments/${String(args.segment_id)}/use-asset`,
|
|
4359
4727
|
body,
|
|
4360
4728
|
);
|
|
4361
|
-
|
|
4729
|
+
const segment = asRecord(asRecord(res).data ?? res);
|
|
4730
|
+
const impact = asRecord(segment.impact);
|
|
4731
|
+
const staleSegmentIds = Array.isArray(impact.stale_video_segment_ids)
|
|
4732
|
+
? impact.stale_video_segment_ids
|
|
4733
|
+
: [];
|
|
4734
|
+
const requiresRegeneration =
|
|
4735
|
+
impact.requires_regeneration === true || staleSegmentIds.length > 0;
|
|
4736
|
+
// The swap has already SUCCEEDED (the POST above returned). The follow-up
|
|
4737
|
+
// GET only enriches the response with project-level audio staleness — a
|
|
4738
|
+
// transient failure on that read must NOT turn a successful swap into a
|
|
4739
|
+
// thrown error, or the agent would believe the swap failed and retry it,
|
|
4740
|
+
// minting a redundant segment version. Guard it: on failure return success
|
|
4741
|
+
// with the mutation's own impact data and audio-staleness marked unknown
|
|
4742
|
+
// (null) rather than propagating.
|
|
4743
|
+
let timeline: Record<string, unknown>;
|
|
4744
|
+
try {
|
|
4745
|
+
const current = await client.get<{ data: unknown }>(
|
|
4746
|
+
`/editor/${args.slug}`,
|
|
4747
|
+
);
|
|
4748
|
+
const editor = asRecord(asRecord(current).data);
|
|
4749
|
+
timeline = {
|
|
4750
|
+
narration_stale: editor.narration_stale === true,
|
|
4751
|
+
voice_stale: editor.voice_stale === true,
|
|
4752
|
+
music_stale: editor.music_stale === true,
|
|
4753
|
+
stale_segment_ids: staleSegmentIds,
|
|
4754
|
+
requires_regeneration: requiresRegeneration,
|
|
4755
|
+
};
|
|
4756
|
+
} catch {
|
|
4757
|
+
timeline = {
|
|
4758
|
+
// Enrichment GET failed after a successful swap — audio staleness is
|
|
4759
|
+
// unknown (null, not false) so the agent doesn't assume "not stale".
|
|
4760
|
+
narration_stale: null,
|
|
4761
|
+
voice_stale: null,
|
|
4762
|
+
music_stale: null,
|
|
4763
|
+
stale_segment_ids: staleSegmentIds,
|
|
4764
|
+
requires_regeneration: requiresRegeneration,
|
|
4765
|
+
enrichment_unavailable: true,
|
|
4766
|
+
};
|
|
4767
|
+
}
|
|
4768
|
+
return ok({
|
|
4769
|
+
...segment,
|
|
4770
|
+
timeline,
|
|
4771
|
+
next:
|
|
4772
|
+
timeline.requires_regeneration ||
|
|
4773
|
+
timeline.narration_stale ||
|
|
4774
|
+
timeline.voice_stale ||
|
|
4775
|
+
timeline.music_stale
|
|
4776
|
+
? "Regenerate the reported stale scenes/audio before render."
|
|
4777
|
+
: timeline.enrichment_unavailable
|
|
4778
|
+
? "Scene swap succeeded, but audio staleness could not be verified (enrichment fetch failed) — call get_status before render to confirm."
|
|
4779
|
+
: "Timeline is current and can be rendered.",
|
|
4780
|
+
});
|
|
4362
4781
|
},
|
|
4363
4782
|
),
|
|
4364
4783
|
);
|
|
@@ -4370,7 +4789,7 @@ registerTool(
|
|
|
4370
4789
|
description:
|
|
4371
4790
|
"Uploads a local product image and attaches it to the project as the product. Accepted: " +
|
|
4372
4791
|
IMAGE_EXTS.map((e) => `.${e}`).join(", ") +
|
|
4373
|
-
" (≤
|
|
4792
|
+
" (≤8 MiB), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits. The FIRST product added " +
|
|
4374
4793
|
"defaults every pending AI scene to feature it 'throughout' (change with set_product_placement). Optional " +
|
|
4375
4794
|
"description (≤500 chars) guides how it's woven into scenes.",
|
|
4376
4795
|
inputSchema: {
|
|
@@ -4395,6 +4814,7 @@ registerTool(
|
|
|
4395
4814
|
client,
|
|
4396
4815
|
`/editor/${args.slug}/product/presign`,
|
|
4397
4816
|
args.file_path,
|
|
4817
|
+
MAX_PRODUCT_IMAGE_BYTES,
|
|
4398
4818
|
);
|
|
4399
4819
|
const res = await client.post<{ data: unknown }>(
|
|
4400
4820
|
`/editor/${args.slug}/product/confirm`,
|
|
@@ -4437,7 +4857,7 @@ registerTool(
|
|
|
4437
4857
|
{
|
|
4438
4858
|
title: "Set the closing-card image (0 credits)",
|
|
4439
4859
|
description:
|
|
4440
|
-
"Sets the optional ~2s end-card image. Either upload a local file (file_path; .jpg/.jpeg/.png ≤20
|
|
4860
|
+
"Sets the optional ~2s end-card image. Either upload a local file (file_path; .jpg/.jpeg/.png ≤20 MiB, confined " +
|
|
4441
4861
|
"to HUBFLUENCER_INPUT_DIR/cwd) OR reuse the project's product image (from_product:true — a 0-credit server-side " +
|
|
4442
4862
|
"copy, no upload). If a closing image already exists, pass overwrite:true to replace it (else " +
|
|
4443
4863
|
"editor_closing_image_exists).",
|
|
@@ -4488,6 +4908,7 @@ registerTool(
|
|
|
4488
4908
|
client,
|
|
4489
4909
|
`/editor/${args.slug}/closing-image/presign`,
|
|
4490
4910
|
args.file_path,
|
|
4911
|
+
MAX_CLOSING_IMAGE_BYTES,
|
|
4491
4912
|
);
|
|
4492
4913
|
const res = await client.post<{ data: unknown }>(
|
|
4493
4914
|
`/editor/${args.slug}/closing-image/confirm`,
|
|
@@ -4503,7 +4924,7 @@ registerTool(
|
|
|
4503
4924
|
{
|
|
4504
4925
|
title: "Set a brand logo overlay (0 credits)",
|
|
4505
4926
|
description:
|
|
4506
|
-
"Uploads a local brand logo and overlays it on the video. Accepted: .png/.jpg/.jpeg (≤
|
|
4927
|
+
"Uploads a local brand logo and overlays it on the video. Accepted: .png/.jpg/.jpeg (≤1 MiB; PNG keeps " +
|
|
4507
4928
|
"transparency), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. Optional placement controls: treatment, " +
|
|
4508
4929
|
"position, duration_seconds.",
|
|
4509
4930
|
inputSchema: {
|
|
@@ -4533,6 +4954,7 @@ registerTool(
|
|
|
4533
4954
|
client,
|
|
4534
4955
|
`/editor/${args.slug}/logo/presign`,
|
|
4535
4956
|
args.file_path,
|
|
4957
|
+
MAX_LOGO_BYTES,
|
|
4536
4958
|
);
|
|
4537
4959
|
const confirmed = await client.post<{ data: unknown }>(
|
|
4538
4960
|
`/editor/${args.slug}/logo/confirm`,
|
|
@@ -4571,7 +4993,7 @@ registerTool(
|
|
|
4571
4993
|
"Uploads a local product image and attaches it to a SHORT so the product is woven into the footage. " +
|
|
4572
4994
|
"Accepted: " +
|
|
4573
4995
|
IMAGE_EXTS.map((e) => `.${e}`).join(", ") +
|
|
4574
|
-
" (≤20
|
|
4996
|
+
" (≤20 MiB = 20,971,520 bytes), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits. Optional description " +
|
|
4575
4997
|
"(≤500 chars) guides how it's featured. Attach BEFORE generate_short so the product is part of the scenes. " +
|
|
4576
4998
|
"(This is the shorts equivalent of set_product for editor projects.)",
|
|
4577
4999
|
inputSchema: {
|
|
@@ -4618,7 +5040,7 @@ registerTool(
|
|
|
4618
5040
|
"Uploads a local image as the SHORT's end-card poster — a closing still shown at the end (extends the " +
|
|
4619
5041
|
"render from 12s to 14s). Accepted: " +
|
|
4620
5042
|
IMAGE_EXTS.map((e) => `.${e}`).join(", ") +
|
|
4621
|
-
" (≤20
|
|
5043
|
+
" (≤20 MiB = 20,971,520 bytes), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. There is one poster per short; calling " +
|
|
4622
5044
|
"again replaces it. (Shorts have no logo overlay — for a brand logo use an editor project + set_logo.)",
|
|
4623
5045
|
inputSchema: {
|
|
4624
5046
|
slug: z.string().describe("Short slug (from create_short)"),
|
|
@@ -4688,8 +5110,13 @@ registerTool(
|
|
|
4688
5110
|
{
|
|
4689
5111
|
title: "Wait for a render to finish",
|
|
4690
5112
|
description:
|
|
4691
|
-
"Polls status (emitting progress) until terminal
|
|
5113
|
+
"Polls status (emitting progress) until terminal or the wait budget is exhausted. " +
|
|
4692
5114
|
"Generation can take several minutes; if it returns terminal=false, call again to keep waiting. " +
|
|
5115
|
+
"terminal=true does NOT always mean ready=true: an editor can go terminal in a needs-action stage " +
|
|
5116
|
+
'(stage "editing_required" — a delivered render is stale after edits, regenerate the reported scenes/audio; ' +
|
|
5117
|
+
'stage "insufficient_credits" — a batch is parked, top up or reduce scope; ' +
|
|
5118
|
+
'stage "idle" — a draft with nothing generating, start generation first) that only a further tool call ' +
|
|
5119
|
+
"can advance — do NOT just re-poll those. " +
|
|
4693
5120
|
"Pass save_path to download the finished MP4 when it's ready (e.g. to resume a make_video that timed out mid-render). " +
|
|
4694
5121
|
'Works for kind:"slider" too, but a carousel has no MP4 — it just goes terminal when composited; read the slide ' +
|
|
4695
5122
|
"images with get_slider (save_path is ignored for sliders).",
|
|
@@ -6065,7 +6492,7 @@ registerTool(
|
|
|
6065
6492
|
return ok({
|
|
6066
6493
|
...episode,
|
|
6067
6494
|
next: episode.draft_slug
|
|
6068
|
-
? "
|
|
6495
|
+
? "Price this draft with start_autopilot({ slug }); after approval launch it with start_autopilot({ slug, max_credits: approved_cap }). Carousel generation remains a separate paid generate_slider call."
|
|
6069
6496
|
: "Draft created — review it with list_series_episodes.",
|
|
6070
6497
|
});
|
|
6071
6498
|
}),
|
|
@@ -6872,7 +7299,7 @@ registerTool(
|
|
|
6872
7299
|
title: "Upload a local file to your reusable asset catalog",
|
|
6873
7300
|
description:
|
|
6874
7301
|
"Uploads a local IMAGE or VIDEO into your reusable asset catalog so it can be reused across projects. " +
|
|
6875
|
-
`Allowed types: ${CATALOG_ASSET_EXTS.map((e) => `.${e}`).join(", ")} — images ≤20 MiB, videos ≤500 MB. ` +
|
|
7302
|
+
`Allowed types: ${CATALOG_ASSET_EXTS.map((e) => `.${e}`).join(", ")} — images ≤20 MiB (20,971,520 bytes), videos ≤500 MB decimal (500,000,000 bytes). ` +
|
|
6876
7303
|
"Runs presign → PUT → confirm; the confirm HEADs the object and validates size/type, so a mismatch 422s. " +
|
|
6877
7304
|
"For a VIDEO you MUST pass duration_seconds (≤60; the server rejects a video with no/over-long duration). " +
|
|
6878
7305
|
"Spends 0 credits. Scope: video:generate. Rate limit: 30/min. Requires an ACTIVE SUBSCRIPTION (402 " +
|