@hubfluencer/mcp 0.14.0 → 0.15.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 +13 -5
- package/dist/index.js +43 -12
- package/package.json +1 -1
- package/src/index.ts +75 -14
package/README.md
CHANGED
|
@@ -113,7 +113,7 @@ instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_ass
|
|
|
113
113
|
| `generate_all_segments` | Render every not-yet-completed scene in order (continuity preserved); capped ~280s → re-run to continue. | **5 / segment** |
|
|
114
114
|
| `set_narration_script` / `generate_narration` | Write your own voiceover script (free) **or** AI-draft one coherent script after every scene has completed with a known positive duration. AI drafting requires at least one scene without retained original audio and consumes an assist only when a fresh job is queued (exact retries dedupe for free). | free / 1 assist |
|
|
115
115
|
| `generate_voice` | Synthesize the voiceover. `voice_id` required (see `list_voices`). | **3** |
|
|
116
|
-
| `generate_music` | Generate background music (each call is a fresh take). | **5** |
|
|
116
|
+
| `generate_music` | Generate background music (each call is a fresh take). Optional `instruments` is a string array (≤10 non-empty hints, ≤80 chars each), not a comma-separated string. | **5** |
|
|
117
117
|
| `render` | Final MP4. Auto-charges and generates any pending or failed scenes first (4/scene in a batch of ≥3). | **0** |
|
|
118
118
|
| `list_renders` | List every render version for an editor project (`id`, `version`, `status`, `video_url`, `thumbnail_url`, `duration_seconds`, `error_message`, `retryable`, `combination_hash`, `is_stale`, `inserted_at`) — find a failed render or recover a finished URL. | **0** |
|
|
119
119
|
| `retry_render` | Re-run a **failed** render from its exact saved snapshot (recovery without re-driving the pipeline). | **0** |
|
|
@@ -137,19 +137,27 @@ images at **8 MiB (8,388,608 bytes)**; closing/short/catalog images at **20 MiB
|
|
|
137
137
|
| `set_closing_image` | Set the ~2s end-card image (JPEG/PNG, ≤20 MiB; upload a file, or reuse the product image with `from_product:true`). |
|
|
138
138
|
| `set_logo` | Upload a brand logo overlay (JPEG/PNG, ≤5 MiB; editor only; optional treatment/position/duration). |
|
|
139
139
|
| `set_short_product` | Attach a product image (JPEG/PNG, ≤20 MiB) to a **short**. |
|
|
140
|
-
| `set_short_logo` | Attach an exact deterministic logo overlay (JPEG/PNG, ≤5 MiB) to a **short**; choose its corner and `throughout`/`end_card`/`none` (hide) treatment. The logo is never sent to the video model. |
|
|
141
|
-
| `set_short_poster` | Set a short's end-card poster (JPEG/PNG, ≤20 MiB; extends the render 12s → 14s). Pass `poster_includes_lockup:true` if the poster already carries brand name/CTA/badges — suppresses the overlaid claim/CTA so they don't collide. |
|
|
140
|
+
| `set_short_logo` | Attach an exact deterministic logo overlay (JPEG/PNG, ≤5 MiB) to a **short**; choose its preferred corner and `throughout`/`end_card`/`none` (hide) treatment. Forge moves it away from text/CTA/chips. The logo is never sent to the video model. |
|
|
141
|
+
| `set_short_poster` | Set a short's end-card poster (JPEG/PNG, ≤20 MiB; extends the render 12s → 14s). Phone-shaped portrait art is cover-cropped to 9:16; square, classic portrait, and landscape art is contained over a blurred backdrop. Pass `poster_includes_lockup:true` if the poster already carries brand name/CTA/badges — suppresses the overlaid claim/CTA and an end-card-only logo so they don't collide. |
|
|
142
142
|
|
|
143
143
|
### Status & delivery
|
|
144
144
|
|
|
145
145
|
| Tool | What it does |
|
|
146
146
|
|---|---|
|
|
147
|
-
| `get_status` | Normalized `{stage, terminal, ready, video_url, error}` for a short / editor / tracking / slider. |
|
|
147
|
+
| `get_status` | Normalized `{stage, terminal, ready, video_url, error}` for a short / editor / tracking / slider. While a paid short is rendering, also exposes `segments_completed`, `segments_total`, and `active_segment_positions`; `generation_attempt` is the cancellation fence. |
|
|
148
148
|
| `wait_for_completion` | Block-poll until terminal (bounded ≤280s; returns `terminal:false` to re-call). `save_path` downloads the MP4 when ready. |
|
|
149
149
|
| `download_result` | Get (or save, to `HUBFLUENCER_OUTPUT_DIR`) the finished MP4 URL. Not for sliders — use `get_slider`. |
|
|
150
|
-
| `get_credits` / `list_voices` / `list_projects` | Credit balance; narration voices; recent projects (`include_completed:true` to recover a finished short's slug). |
|
|
150
|
+
| `get_credits` / `list_voices` / `list_projects` | Credit balance; narration voices (including normalized verified `languages`); recent projects (`include_completed:true` to recover a finished short's slug). |
|
|
151
151
|
| `get_ai_assists` / `unlock_ai_assists` | The free daily assist quota / spend 1 credit for +10. |
|
|
152
152
|
|
|
153
|
+
### Creative export contract
|
|
154
|
+
|
|
155
|
+
- Production prompts (`product_prompt`, `scenario_prompt`, and scene `prompt`) are visual directions. Narration scripts are read aloud verbatim, so `set_narration_script.script` must contain only the exact spoken words—never camera notes, prompt syntax, labels, or scene directions. Prefer `generate_narration` or Autopilot when exact wording is unnecessary.
|
|
156
|
+
- Do not rely on the video model to spell essential brand names, UI, signs, prices, offers, or CTAs. Put exact copy in Hubfluencer overlays, narration, or supplied ending artwork.
|
|
157
|
+
- `short_logo_position` is a preferred corner. Forge moves the logo away from headline, captions, CTA, offer, and badge areas. Use a tight transparent wordmark; use square/full-bleed art as the poster. `end_card` logos are suppressed when `poster_includes_lockup:true` to avoid double branding.
|
|
158
|
+
- Short posters fill the ending frame: phone-shaped portrait art is cover-cropped, while square, classic portrait, and landscape art is contained over a blurred backdrop. Editor closing images are always cover-cropped. Keep important content in the central 9:16 safe area. An Editor closing image gets no generic CTA, so put the CTA in the artwork or state it in the preceding narration/scene.
|
|
159
|
+
- Each Editor AI scene is 8 seconds. Request 3 scenes (about 24 seconds) for a concise social ad unless the brief needs a longer story.
|
|
160
|
+
|
|
153
161
|
For editor projects, `video_url` may remain the last delivered MP4 after the timeline changes.
|
|
154
162
|
It is current only when `ready:true` and `stale:false`. `stale:true` means the delivered render
|
|
155
163
|
does not match the live timeline; regenerate the reported stale scenes/audio and render again.
|
package/dist/index.js
CHANGED
|
@@ -30266,7 +30266,7 @@ function readStoredCredentials() {
|
|
|
30266
30266
|
// package.json
|
|
30267
30267
|
var package_default = {
|
|
30268
30268
|
name: "@hubfluencer/mcp",
|
|
30269
|
-
version: "0.
|
|
30269
|
+
version: "0.15.0",
|
|
30270
30270
|
description: "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
|
|
30271
30271
|
license: "MIT",
|
|
30272
30272
|
author: "Monocursive <contact@monocursive.com>",
|
|
@@ -32140,7 +32140,7 @@ registerTool("get_credits", {
|
|
|
32140
32140
|
}));
|
|
32141
32141
|
registerTool("list_voices", {
|
|
32142
32142
|
title: "List voices",
|
|
32143
|
-
description: "Lists available narration voices
|
|
32143
|
+
description: "Lists available narration voices with voice_id, preview metadata, and verified language codes. " + "Choose a voice whose languages includes the editor project's language, then pass its voice_id to " + "create_editor_ad / make_video / generate_voice. Shorts have no voiceover.",
|
|
32144
32144
|
inputSchema: {},
|
|
32145
32145
|
annotations: { title: "List voices", ...RO }
|
|
32146
32146
|
}, tool(async (_args, client) => {
|
|
@@ -32337,7 +32337,7 @@ registerTool("update_short", {
|
|
|
32337
32337
|
brand_name: exports_external.string().max(40).optional().describe("Brand/product display name (≤40 chars) shown big on the closing brand-lockup slate when no " + `poster is set. Server falls back to your brand profile's name when unset. Pass "" to remove it.`),
|
|
32338
32338
|
end_card: exports_external.enum(["auto", "none"]).optional().describe('"auto" (default): end on the poster, or on a brand-lockup slate (brand_name + claim + CTA pill) ' + 'when no poster is set — a 14s render. "none": no end card at all (12s, for deliberate ' + "loop-style shorts)."),
|
|
32339
32339
|
poster_includes_lockup: exports_external.boolean().optional().describe("Set true if your poster already contains brand name/CTA/badges — suppresses the overlaid " + "claim/CTA so they don't collide (default false)."),
|
|
32340
|
-
short_logo_position: exports_external.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).optional().describe("
|
|
32340
|
+
short_logo_position: exports_external.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).optional().describe("Preferred corner for the deterministic brand-logo overlay (set the image with set_short_logo). " + "Forge moves it to a clear rail when text/CTA/chips occupy that corner. Moving it is a FREE " + "re-render — logo fields never cost credits."),
|
|
32341
32341
|
short_logo_treatment: exports_external.enum(["throughout", "end_card", "none"]).optional().describe('When the brand logo shows: "throughout" the footage, only on the "end_card", or "none" to hide it. ' + "FREE re-render."),
|
|
32342
32342
|
brand_profile_id: exports_external.number().int().optional().describe("Seed brand defaults — colors/font/CTA — from a saved brand profile (id from " + "list_brand_profiles). Best set at create_short time (that path also freezes a brand snapshot); " + "on update it sets the linked profile. Must be one of your own profiles.")
|
|
32343
32343
|
},
|
|
@@ -32553,19 +32553,21 @@ registerTool("create_tracking_video", {
|
|
|
32553
32553
|
}));
|
|
32554
32554
|
registerTool("generate_short", {
|
|
32555
32555
|
title: "Generate (render) a short",
|
|
32556
|
-
description: "Deducts 15 credits and starts the render pipeline for an existing short — this RE-ROLLS the footage " + "and music (a fresh AI generation). For text/style/CTA/end-card tweaks on an already-generated short, " + "use rerender_short instead: it re-applies the overlay over the existing footage for 0 credits. " + "Safe to call again: a " + "duplicate while a render is in flight is reported as in-progress (no double charge), and a failed " + "short can be re-generated. If the draft's headline/subheadline/text_beats are all blank the server " + "auto-writes the headline + caption beats before rendering (pass skip_auto_text:true for a deliberately " + "bare clip). Then poll with get_status or wait_for_completion (kind=short).",
|
|
32556
|
+
description: "Deducts 15 credits and starts the render pipeline for an existing short — this RE-ROLLS the footage " + "and music (a fresh AI generation). For text/style/CTA/end-card tweaks on an already-generated short, " + "use rerender_short instead: it re-applies the overlay over the existing footage for 0 credits. " + "Safe to call again: a " + "duplicate while a render is in flight is reported as in-progress (no double charge), and a failed " + "short can be re-generated. If the draft's headline/subheadline/text_beats are all blank the server " + "auto-writes the headline + caption beats before rendering (pass skip_auto_text:true for a deliberately " + "bare clip). A transient 5xx is reconciled against the project state: accepted work is returned as " + "in-progress, while an unchanged draft is retried once. Then poll with get_status or " + "wait_for_completion (kind=short).",
|
|
32557
32557
|
inputSchema: {
|
|
32558
32558
|
slug: exports_external.string().describe("Short slug from create_short"),
|
|
32559
32559
|
skip_auto_text: exports_external.boolean().optional().describe("When the draft has no headline/subheadline/text_beats the server auto-writes the copy; pass " + "true to skip that and render a deliberately bare clip (default false).")
|
|
32560
32560
|
},
|
|
32561
32561
|
annotations: { title: "Generate short", ...WRITE }
|
|
32562
32562
|
}, tool(async (args, client) => {
|
|
32563
|
+
const submit = () => client.post(`/shorts/${args.slug}/generate`, args.skip_auto_text ? { skip_auto_text: true } : undefined);
|
|
32563
32564
|
try {
|
|
32564
|
-
const res = await
|
|
32565
|
+
const res = await submit();
|
|
32565
32566
|
const status = normalizeStatus("short", args.slug, asRecord(res).data);
|
|
32566
32567
|
return ok(status);
|
|
32567
32568
|
} catch (e) {
|
|
32568
|
-
|
|
32569
|
+
const statusCode = e.status;
|
|
32570
|
+
if (statusCode === 409) {
|
|
32569
32571
|
return ok({
|
|
32570
32572
|
kind: "short",
|
|
32571
32573
|
slug: args.slug,
|
|
@@ -32576,6 +32578,35 @@ registerTool("generate_short", {
|
|
|
32576
32578
|
error: null
|
|
32577
32579
|
});
|
|
32578
32580
|
}
|
|
32581
|
+
if (statusCode !== undefined && statusCode >= 500) {
|
|
32582
|
+
let reconciled;
|
|
32583
|
+
try {
|
|
32584
|
+
reconciled = await fetchStatus(client, "short", args.slug);
|
|
32585
|
+
} catch (_reconcileError) {
|
|
32586
|
+
throw e;
|
|
32587
|
+
}
|
|
32588
|
+
if (reconciled.stage === "rendering")
|
|
32589
|
+
return ok(reconciled);
|
|
32590
|
+
if (reconciled.stage !== "draft")
|
|
32591
|
+
throw e;
|
|
32592
|
+
try {
|
|
32593
|
+
const retried = await submit();
|
|
32594
|
+
return ok(normalizeStatus("short", args.slug, asRecord(retried).data));
|
|
32595
|
+
} catch (retryError) {
|
|
32596
|
+
if (retryError.status === 409) {
|
|
32597
|
+
return ok({
|
|
32598
|
+
kind: "short",
|
|
32599
|
+
slug: args.slug,
|
|
32600
|
+
stage: "processing",
|
|
32601
|
+
terminal: false,
|
|
32602
|
+
ready: false,
|
|
32603
|
+
video_url: null,
|
|
32604
|
+
error: null
|
|
32605
|
+
});
|
|
32606
|
+
}
|
|
32607
|
+
throw retryError;
|
|
32608
|
+
}
|
|
32609
|
+
}
|
|
32579
32610
|
throw e;
|
|
32580
32611
|
}
|
|
32581
32612
|
}));
|
|
@@ -32847,9 +32878,9 @@ registerTool("edit_slider_slide", {
|
|
|
32847
32878
|
}));
|
|
32848
32879
|
registerTool("create_editor_ad", {
|
|
32849
32880
|
title: "Create, configure, and price a multi-scene editor ad",
|
|
32850
|
-
description: "Creates an editor project, attaches any supplied local product/logo/closing assets BEFORE generation, " + "and prices autopilot (server-orchestrated " + "scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " + "Without max_credits it STOPS after the free draft/configuration and returns the live estimate. It starts " + "the paid pipeline only when max_credits is supplied and covers the estimate. Then poll with get_status / " + "wait_for_completion (kind=editor). Prefer make_video for a prompt-only one-shot. " + "A product image is analyzed into semantic project context and sent as a product-identity reference to every " + "generated Editor scene. The exact photo is also linked as the closing image unless a separate closing image " + "was authored; generated fine label text can still vary. Scene prompts are the complete content contract: describe any people, " + "products, visual continuity, absences, and composition directly in the brief. (Editor ads carry their copy " + "in-scene/narration, not as a title overlay; for an on-screen title/subtitle use a short with " + "headline/subheadline instead.)",
|
|
32881
|
+
description: "Creates an editor project, attaches any supplied local product/logo/closing assets BEFORE generation, " + "and prices autopilot (server-orchestrated " + "scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " + "Without max_credits it STOPS after the free draft/configuration and returns the live estimate. It starts " + "the paid pipeline only when max_credits is supplied and covers the estimate. Then poll with get_status / " + "wait_for_completion (kind=editor). Prefer make_video for a prompt-only one-shot. " + "A product image is analyzed into semantic project context and sent as a product-identity reference to every " + "generated Editor scene. The exact photo is also linked as the closing image unless a separate closing image " + "was authored; generated fine label text can still vary. Do not depend on the video model for exact readable " + "brand names, UI, signs, prices, or CTA copy; put essential copy in narration or deterministic Short/poster overlays. " + "Scene prompts are the complete content contract: describe any people, " + "products, visual continuity, absences, and composition directly in the brief. (Editor ads carry their copy " + "in-scene/narration, not as a title overlay; for an on-screen title/subtitle use a short with " + "headline/subheadline instead.)",
|
|
32851
32882
|
inputSchema: {
|
|
32852
|
-
product_prompt: exports_external.string().min(10).describe("
|
|
32883
|
+
product_prompt: exports_external.string().min(10).describe("Creative brief for the ad (min 10 chars), not literal voiceover copy. Autopilot uses it to draft " + "narration. For exact spoken words, use the granular editor path and pass only read-aloud text to " + "set_narration_script."),
|
|
32853
32884
|
product_subject: exports_external.string().max(300).optional().describe('The concrete product or main subject the video must be about (e.g. "Bordeaux red wine, dark green ' + 'bottle with a gold label"). Distinct from the freeform brief: hard-grounds the AI scenario + narration ' + "so it cannot invent an unrelated product. Strongly recommended for ads."),
|
|
32854
32885
|
project_intent: exports_external.enum(["social_ad", "creative_story"]).optional().describe("social_ad for promotional/ad content (product focus + CTA), creative_story for brand narratives " + "(default)."),
|
|
32855
32886
|
language: exports_external.enum(LANGUAGES).optional().describe('Language (default "en"). Bare ISO-639-1 code only — no region/script subtag (use "es", not "es-MX"). ' + "Drives the WHOLE video: scenario, narration script, narration voice, and captions. " + "Ask the user / infer from their locale."),
|
|
@@ -33628,7 +33659,7 @@ registerTool("generate_voice", {
|
|
|
33628
33659
|
}));
|
|
33629
33660
|
registerTool("generate_music", {
|
|
33630
33661
|
title: "Generate the background music (5 credits)",
|
|
33631
|
-
description: "Generates background music. Costs 5 credits. Each call produces a fresh take (music is non-deterministic) — call again for another. `description` is an optional" + "music-direction prompt (≤1200 chars; longer is silently truncated server-side). 402 fails cleanly.",
|
|
33662
|
+
description: "Generates background music. Costs 5 credits. Each call produces a fresh take (music is non-deterministic) — call again for another. `description` is an optional" + " music-direction prompt (≤1200 chars; longer is silently truncated server-side). `instruments` is an " + "optional array of up to 10 non-empty hints (≤80 chars each), never a comma-separated string. 402 fails cleanly.",
|
|
33632
33663
|
inputSchema: {
|
|
33633
33664
|
slug: exports_external.string().describe("Editor project slug"),
|
|
33634
33665
|
description: exports_external.string().max(1200).optional().describe("Music direction prompt (≤1200 chars, truncated if longer)"),
|
|
@@ -33984,11 +34015,11 @@ registerTool("set_short_product", {
|
|
|
33984
34015
|
}));
|
|
33985
34016
|
registerTool("set_short_logo", {
|
|
33986
34017
|
title: "Set a Short brand logo overlay (0 credits)",
|
|
33987
|
-
description: "Uploads an exact brand logo for deterministic compositing over a Short. The image is never sent to the " + "AI video provider, so lettering is preserved. PNG/JPEG, ≤5 MiB, 0 credits. Choose a corner and whether " + "it appears throughout the footage or only on the end card.",
|
|
34018
|
+
description: "Uploads an exact brand logo for deterministic compositing over a Short. The image is never sent to the " + "AI video provider, so lettering is preserved. PNG/JPEG, ≤5 MiB, 0 credits. Choose a corner and whether " + "it appears throughout the footage or only on the end card. The corner is a preference: Forge moves the " + "logo to a clear rail when headline/caption/CTA/chip overlays occupy it. Prefer a tight transparent PNG " + "wordmark; use square/full-bleed artwork as a poster instead of a watermark.",
|
|
33988
34019
|
inputSchema: {
|
|
33989
34020
|
slug: exports_external.string().describe("Short slug"),
|
|
33990
34021
|
file_path: exports_external.string().describe("Local logo image (.png/.jpg/.jpeg)"),
|
|
33991
|
-
position: exports_external.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).optional().describe("
|
|
34022
|
+
position: exports_external.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).optional().describe("Preferred logo corner (default top-right; auto-relocated to avoid text)"),
|
|
33992
34023
|
treatment: exports_external.enum(["throughout", "end_card", "none"]).optional().describe("When the logo appears (default throughout)")
|
|
33993
34024
|
},
|
|
33994
34025
|
annotations: { title: "Set Short logo", ...WRITE, idempotentHint: false }
|
|
@@ -34020,7 +34051,7 @@ registerTool("set_short_poster", {
|
|
|
34020
34051
|
}));
|
|
34021
34052
|
registerTool("get_status", {
|
|
34022
34053
|
title: "Get generation status",
|
|
34023
|
-
description: "Returns a normalized status {stage, terminal, ready, video_url, error} for a short, editor, " + "tracking, or slider project. ready:true means the post-ready MP4 is at video_url — except for a " + "slider (a carousel of stills, not a video): its video_url is always null, ready:true means every " + "slide is composited, and you read the per-slide image URLs with get_slider.",
|
|
34054
|
+
description: "Returns a normalized status {stage, terminal, ready, video_url, error} for a short, editor, " + "tracking, or slider project. ready:true means the post-ready MP4 is at video_url — except for a " + "slider (a carousel of stills, not a video): its video_url is always null, ready:true means every " + "slide is composited, and you read the per-slide image URLs with get_slider. During paid short " + "generation, segments_completed / segments_total and active_segment_positions report real clip progress.",
|
|
34024
34055
|
inputSchema: { slug: exports_external.string(), kind: kindSchema },
|
|
34025
34056
|
outputSchema: getStatusOutput,
|
|
34026
34057
|
annotations: { title: "Get status", ...RO }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubfluencer/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Monocursive <contact@monocursive.com>",
|
package/src/index.ts
CHANGED
|
@@ -1412,8 +1412,9 @@ registerTool(
|
|
|
1412
1412
|
{
|
|
1413
1413
|
title: "List voices",
|
|
1414
1414
|
description:
|
|
1415
|
-
"Lists available narration voices
|
|
1416
|
-
"
|
|
1415
|
+
"Lists available narration voices with voice_id, preview metadata, and verified language codes. " +
|
|
1416
|
+
"Choose a voice whose languages includes the editor project's language, then pass its voice_id to " +
|
|
1417
|
+
"create_editor_ad / make_video / generate_voice. Shorts have no voiceover.",
|
|
1417
1418
|
inputSchema: {},
|
|
1418
1419
|
annotations: { title: "List voices", ...RO },
|
|
1419
1420
|
},
|
|
@@ -2023,8 +2024,9 @@ registerTool(
|
|
|
2023
2024
|
.enum(["top-left", "top-right", "bottom-left", "bottom-right"])
|
|
2024
2025
|
.optional()
|
|
2025
2026
|
.describe(
|
|
2026
|
-
"
|
|
2027
|
-
"a
|
|
2027
|
+
"Preferred corner for the deterministic brand-logo overlay (set the image with set_short_logo). " +
|
|
2028
|
+
"Forge moves it to a clear rail when text/CTA/chips occupy that corner. Moving it is a FREE " +
|
|
2029
|
+
"re-render — logo fields never cost credits.",
|
|
2028
2030
|
),
|
|
2029
2031
|
short_logo_treatment: z
|
|
2030
2032
|
.enum(["throughout", "end_card", "none"])
|
|
@@ -2541,7 +2543,9 @@ registerTool(
|
|
|
2541
2543
|
"duplicate while a render is in flight is reported as in-progress (no double charge), and a failed " +
|
|
2542
2544
|
"short can be re-generated. If the draft's headline/subheadline/text_beats are all blank the server " +
|
|
2543
2545
|
"auto-writes the headline + caption beats before rendering (pass skip_auto_text:true for a deliberately " +
|
|
2544
|
-
"bare clip).
|
|
2546
|
+
"bare clip). A transient 5xx is reconciled against the project state: accepted work is returned as " +
|
|
2547
|
+
"in-progress, while an unchanged draft is retried once. Then poll with get_status or " +
|
|
2548
|
+
"wait_for_completion (kind=short).",
|
|
2545
2549
|
inputSchema: {
|
|
2546
2550
|
slug: z.string().describe("Short slug from create_short"),
|
|
2547
2551
|
skip_auto_text: z
|
|
@@ -2563,17 +2567,21 @@ registerTool(
|
|
|
2563
2567
|
// of a failed short. Double-charge is prevented server-side — the batch
|
|
2564
2568
|
// claim's conditional update rolls back the losing transaction's credit
|
|
2565
2569
|
// deduction — and a 409 means a render is already running.
|
|
2566
|
-
|
|
2567
|
-
|
|
2570
|
+
const submit = () =>
|
|
2571
|
+
client.post<{ data: unknown }>(
|
|
2568
2572
|
`/shorts/${args.slug}/generate`,
|
|
2569
2573
|
// Only send a body when the caller opts out of server auto-copy —
|
|
2570
2574
|
// the bare POST stays byte-identical to the pre-0.8.2 wire shape.
|
|
2571
2575
|
args.skip_auto_text ? { skip_auto_text: true } : undefined,
|
|
2572
2576
|
);
|
|
2577
|
+
|
|
2578
|
+
try {
|
|
2579
|
+
const res = await submit();
|
|
2573
2580
|
const status = normalizeStatus("short", args.slug, asRecord(res).data);
|
|
2574
2581
|
return ok(status);
|
|
2575
2582
|
} catch (e) {
|
|
2576
|
-
|
|
2583
|
+
const statusCode = (e as { status?: number }).status;
|
|
2584
|
+
if (statusCode === 409) {
|
|
2577
2585
|
return ok({
|
|
2578
2586
|
kind: "short",
|
|
2579
2587
|
slug: args.slug,
|
|
@@ -2584,6 +2592,47 @@ registerTool(
|
|
|
2584
2592
|
error: null,
|
|
2585
2593
|
});
|
|
2586
2594
|
}
|
|
2595
|
+
|
|
2596
|
+
if (statusCode !== undefined && statusCode >= 500) {
|
|
2597
|
+
let reconciled: NormalizedStatus;
|
|
2598
|
+
try {
|
|
2599
|
+
reconciled = await fetchStatus(client, "short", args.slug);
|
|
2600
|
+
} catch (_reconcileError) {
|
|
2601
|
+
// Preserve the original POST failure when the read-side check also
|
|
2602
|
+
// fails; the client must never blind-retry a possibly accepted debit.
|
|
2603
|
+
throw e;
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
// Only the active render stage proves this POST crossed the paid
|
|
2607
|
+
// launch transaction. A previous completed/failed result can still be
|
|
2608
|
+
// visible after an early 5xx; returning it here would masquerade stale
|
|
2609
|
+
// work as the newly accepted generation.
|
|
2610
|
+
if (reconciled.stage === "rendering") return ok(reconciled);
|
|
2611
|
+
if (reconciled.stage !== "draft") throw e;
|
|
2612
|
+
|
|
2613
|
+
// A Short launch changes the durable factory out of draft in the
|
|
2614
|
+
// same transaction as the debit. Still-draft therefore proves the
|
|
2615
|
+
// failed response accepted no paid work, so one retry is safe.
|
|
2616
|
+
try {
|
|
2617
|
+
const retried = await submit();
|
|
2618
|
+
return ok(
|
|
2619
|
+
normalizeStatus("short", args.slug, asRecord(retried).data),
|
|
2620
|
+
);
|
|
2621
|
+
} catch (retryError) {
|
|
2622
|
+
if ((retryError as { status?: number }).status === 409) {
|
|
2623
|
+
return ok({
|
|
2624
|
+
kind: "short",
|
|
2625
|
+
slug: args.slug,
|
|
2626
|
+
stage: "processing",
|
|
2627
|
+
terminal: false,
|
|
2628
|
+
ready: false,
|
|
2629
|
+
video_url: null,
|
|
2630
|
+
error: null,
|
|
2631
|
+
});
|
|
2632
|
+
}
|
|
2633
|
+
throw retryError;
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2587
2636
|
throw e;
|
|
2588
2637
|
}
|
|
2589
2638
|
}),
|
|
@@ -3131,7 +3180,9 @@ registerTool(
|
|
|
3131
3180
|
"wait_for_completion (kind=editor). Prefer make_video for a prompt-only one-shot. " +
|
|
3132
3181
|
"A product image is analyzed into semantic project context and sent as a product-identity reference to every " +
|
|
3133
3182
|
"generated Editor scene. The exact photo is also linked as the closing image unless a separate closing image " +
|
|
3134
|
-
"was authored; generated fine label text can still vary.
|
|
3183
|
+
"was authored; generated fine label text can still vary. Do not depend on the video model for exact readable " +
|
|
3184
|
+
"brand names, UI, signs, prices, or CTA copy; put essential copy in narration or deterministic Short/poster overlays. " +
|
|
3185
|
+
"Scene prompts are the complete content contract: describe any people, " +
|
|
3135
3186
|
"products, visual continuity, absences, and composition directly in the brief. (Editor ads carry their copy " +
|
|
3136
3187
|
"in-scene/narration, not as a title overlay; for an on-screen title/subtitle use a short with " +
|
|
3137
3188
|
"headline/subheadline instead.)",
|
|
@@ -3139,7 +3190,11 @@ registerTool(
|
|
|
3139
3190
|
product_prompt: z
|
|
3140
3191
|
.string()
|
|
3141
3192
|
.min(10)
|
|
3142
|
-
.describe(
|
|
3193
|
+
.describe(
|
|
3194
|
+
"Creative brief for the ad (min 10 chars), not literal voiceover copy. Autopilot uses it to draft " +
|
|
3195
|
+
"narration. For exact spoken words, use the granular editor path and pass only read-aloud text to " +
|
|
3196
|
+
"set_narration_script.",
|
|
3197
|
+
),
|
|
3143
3198
|
product_subject: z
|
|
3144
3199
|
.string()
|
|
3145
3200
|
.max(300)
|
|
@@ -4806,7 +4861,8 @@ registerTool(
|
|
|
4806
4861
|
title: "Generate the background music (5 credits)",
|
|
4807
4862
|
description:
|
|
4808
4863
|
"Generates background music. Costs 5 credits. Each call produces a fresh take (music is non-deterministic) — call again for another. `description` is an optional" +
|
|
4809
|
-
"music-direction prompt (≤1200 chars; longer is silently truncated server-side).
|
|
4864
|
+
" music-direction prompt (≤1200 chars; longer is silently truncated server-side). `instruments` is an " +
|
|
4865
|
+
"optional array of up to 10 non-empty hints (≤80 chars each), never a comma-separated string. 402 fails cleanly.",
|
|
4810
4866
|
inputSchema: {
|
|
4811
4867
|
slug: z.string().describe("Editor project slug"),
|
|
4812
4868
|
description: z
|
|
@@ -5594,14 +5650,18 @@ registerTool(
|
|
|
5594
5650
|
description:
|
|
5595
5651
|
"Uploads an exact brand logo for deterministic compositing over a Short. The image is never sent to the " +
|
|
5596
5652
|
"AI video provider, so lettering is preserved. PNG/JPEG, ≤5 MiB, 0 credits. Choose a corner and whether " +
|
|
5597
|
-
"it appears throughout the footage or only on the end card."
|
|
5653
|
+
"it appears throughout the footage or only on the end card. The corner is a preference: Forge moves the " +
|
|
5654
|
+
"logo to a clear rail when headline/caption/CTA/chip overlays occupy it. Prefer a tight transparent PNG " +
|
|
5655
|
+
"wordmark; use square/full-bleed artwork as a poster instead of a watermark.",
|
|
5598
5656
|
inputSchema: {
|
|
5599
5657
|
slug: z.string().describe("Short slug"),
|
|
5600
5658
|
file_path: z.string().describe("Local logo image (.png/.jpg/.jpeg)"),
|
|
5601
5659
|
position: z
|
|
5602
5660
|
.enum(["top-left", "top-right", "bottom-left", "bottom-right"])
|
|
5603
5661
|
.optional()
|
|
5604
|
-
.describe(
|
|
5662
|
+
.describe(
|
|
5663
|
+
"Preferred logo corner (default top-right; auto-relocated to avoid text)",
|
|
5664
|
+
),
|
|
5605
5665
|
treatment: z
|
|
5606
5666
|
.enum(["throughout", "end_card", "none"])
|
|
5607
5667
|
.optional()
|
|
@@ -5700,7 +5760,8 @@ registerTool(
|
|
|
5700
5760
|
"Returns a normalized status {stage, terminal, ready, video_url, error} for a short, editor, " +
|
|
5701
5761
|
"tracking, or slider project. ready:true means the post-ready MP4 is at video_url — except for a " +
|
|
5702
5762
|
"slider (a carousel of stills, not a video): its video_url is always null, ready:true means every " +
|
|
5703
|
-
"slide is composited, and you read the per-slide image URLs with get_slider."
|
|
5763
|
+
"slide is composited, and you read the per-slide image URLs with get_slider. During paid short " +
|
|
5764
|
+
"generation, segments_completed / segments_total and active_segment_positions report real clip progress.",
|
|
5704
5765
|
inputSchema: { slug: z.string(), kind: kindSchema },
|
|
5705
5766
|
outputSchema: getStatusOutput,
|
|
5706
5767
|
annotations: { title: "Get status", ...RO },
|