@hubfluencer/mcp 0.9.0 → 0.10.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 +9 -7
- package/dist/index.js +101 -20
- package/package.json +1 -1
- package/src/core.ts +48 -26
- package/src/index.ts +163 -18
- package/src/output-schemas.ts +51 -2
- package/src/uploads.ts +1 -0
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ claude mcp add hubfluencer --env HUBFLUENCER_API_TOKEN=YOUR_TOKEN -- npx -y @hub
|
|
|
68
68
|
|
|
69
69
|
## Tools
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
93 tools, grouped by workflow. Every tool prices its action in its own `description` — costs
|
|
72
72
|
below are the ground truth. **"$0" means it spends no video credits**; planning/AI-helper tools
|
|
73
73
|
instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_assists`: 1 credit
|
|
74
74
|
→ +10). Reads need the `video:read` scope; writes need `video:generate`. Nothing here needs
|
|
@@ -80,12 +80,12 @@ instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_ass
|
|
|
80
80
|
|---|---|---|
|
|
81
81
|
| **`make_video`** | **One shot: prompt → finished MP4** (create → **price** → start → poll → download). `kind:"auto"` picks a multi-scene editor ad for ad/promo/story briefs, a short for simple ones (reported as `kind_inferred`). Pass `dry_run:true` to preview the cost without spending, or `max_credits` to cap it. | short **15**, editor ~**28** (`dry_run`/`max_credits` gate it) |
|
|
82
82
|
| `create_short` | Create a short draft (12s vertical: two 6s AI segments + title overlay + music; 14s when it ends on a poster or brand-lockup end card). Set `headline` (≈4 words, ≤40 chars), `subheadline`, `music_vibe`, and an explicit `visual_language` (`kinetic_creator` is a strong default) — and **always `cta_text`** (a short without a CTA doesn't convert; blank `cta_text` → the server renders a neutral localized ask, e.g. "Learn more" — a truly CTA-less short is not currently supported). End-card fields: `closing_claim` (≤80), `brand_name` (≤40), `end_card` (`"auto"`\|`"none"`). Other opt-in conversion graphics: `offer_text`/`badge_text`/`star_rating` — only what you can substantiate. | **0** |
|
|
83
|
-
| `update_short` | Patch an existing short draft — brief (`product_prompt`), copy, styling, end-card fields (`closing_claim`/`brand_name`/`poster_includes_lockup`/`end_card`), or the opt-in conversion graphics; only the fields you pass change. Clear one with `""` (`[]` for `text_beats`, `0` for `star_rating`). Apply text/style/CTA/end-card edits to a generated short free with `rerender_short`; footage/music edits need `generate_short`. | **0** |
|
|
83
|
+
| `update_short` | Patch an existing short draft — brief (`product_prompt`), copy, styling, end-card fields (`closing_claim`/`brand_name`/`poster_includes_lockup`/`end_card`), logo overlay placement (`short_logo_position`/`short_logo_treatment`), or the opt-in conversion graphics; only the fields you pass change. Clear one with `""` (`[]` for `text_beats`, `0` for `star_rating`). Apply text/style/CTA/end-card/logo edits to a generated short free with `rerender_short`; footage/music edits need `generate_short`. | **0** |
|
|
84
84
|
| `generate_short` | Render an existing short — re-rolls the footage + music (fresh AI generation). If headline/subheadline/beats are all blank the server auto-writes the copy first (`skip_auto_text:true` for a deliberately bare clip). Safe to re-call (in-flight = reported, no double charge; a failed short can re-generate). | **15** |
|
|
85
|
-
| `rerender_short` | **FREE re-render** — applies the short's current text/style/CTA/end-card state over the already-paid footage + music. Use after `update_short` for copy/style/CTA tweaks; footage or music changes need `generate_short` instead. Needs one completed generation first (422 `short_not_ready` otherwise); changed footage/music inputs return 422 `short_paid_regeneration_required`. A failed free re-render leaves the delivered video intact — retry `rerender_short`, it stays free. | **0** |
|
|
85
|
+
| `rerender_short` | **FREE re-render** — applies the short's current text/style/CTA/end-card/logo state over the already-paid footage + music. Use after `update_short` for copy/style/CTA tweaks, or after a logo change (`set_short_logo`, or `short_logo_position`/`short_logo_treatment`); footage or music changes need `generate_short` instead. Needs one completed generation first (422 `short_not_ready` otherwise); changed footage/music inputs return 422 `short_paid_regeneration_required`. A failed free re-render leaves the delivered video intact — retry `rerender_short`, it stays free. | **0** |
|
|
86
86
|
| `generate_short_text` | AI-draft editable headline / subheadline / caption beats for a short. | 1 assist |
|
|
87
87
|
| `create_editor_ad` | Create an editor project, attach optional local product/logo/closing assets, and return the live autopilot quote. Omit `max_credits` to stop free; provide an approved cap to launch. For a mascot/hero product that must remain identical with no humans, pass `cast_mode:"product_only"` plus `product_image_path`. | **0** until capped launch |
|
|
88
|
-
| `start_autopilot` | Quote autopilot on an **existing** editor draft when called with only `slug`; launch only with an explicit `max_credits` that covers the estimate and an adequate balance. | **0** quote; credits on capped launch |
|
|
88
|
+
| `start_autopilot` | Quote autopilot on an **existing** editor draft when called with only `slug`; launch only with an explicit `max_credits` that covers the estimate and an adequate balance. On a completed project, pass `restart:true` to quote and build a fresh creative run; omit it to resume pending edits. | **0** quote; credits on capped launch |
|
|
89
89
|
| `create_slider` | Create an image-carousel draft: one prompt → N still slides + a ready-to-post caption + hashtags. `mode` `creative` (story) or `ad_driven` (facts). | **0** |
|
|
90
90
|
| `generate_slider` | Render a carousel (copy → AI backgrounds → composite). Poll `get_slider` until `completed`. | **1 / slide** (3–10) |
|
|
91
91
|
| `get_slider` | Read a carousel's status and, when completed, the per-slide image URLs + caption + hashtags. | **0** |
|
|
@@ -133,6 +133,7 @@ images at **8 MiB (8,388,608 bytes)**; closing/short/catalog images at **20 MiB
|
|
|
133
133
|
| `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`). |
|
|
134
134
|
| `set_logo` | Upload a brand logo overlay (JPEG/PNG, ≤1 MiB; editor only; optional treatment/position/duration). |
|
|
135
135
|
| `set_short_product` | Attach a product image (JPEG/PNG, ≤20 MiB) to a **short**. |
|
|
136
|
+
| `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. |
|
|
136
137
|
| `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. |
|
|
137
138
|
|
|
138
139
|
### Status & delivery
|
|
@@ -233,7 +234,7 @@ edit are **0 credits** — you only pay to generate/render.
|
|
|
233
234
|
|---|---|
|
|
234
235
|
| Any draft create / edit / reorder | 0 |
|
|
235
236
|
| Short render | **15** |
|
|
236
|
-
| Short re-render (`rerender_short` — text/style/CTA/end-card over existing footage) | **0** |
|
|
237
|
+
| Short re-render (`rerender_short` — text/style/CTA/end-card/logo over existing footage) | **0** |
|
|
237
238
|
| Editor AI scene (`generate_segment`) | **5** (4 each in a batch of ≥3) |
|
|
238
239
|
| Editor scene re-roll (`regenerate_segment`, new version of a completed scene) | **4** |
|
|
239
240
|
| Editor voice | **3** |
|
|
@@ -259,8 +260,9 @@ If it returns `terminal:false`, the render is still going — call `wait_for_com
|
|
|
259
260
|
`create_editor_ad` (free configure + quote) → obtain spend approval → `start_autopilot {slug, max_credits}` → `wait_for_completion {kind:"editor"}` → `download_result`.
|
|
260
261
|
|
|
261
262
|
**Iterate a short for free (never re-roll):** the first `generate_short` (15 credits) buys the footage +
|
|
262
|
-
music; after that, every text/style/CTA/end-card tweak re-renders over them for **0 credits** —
|
|
263
|
-
`update_short` (new hook/CTA/style) → `rerender_short` → compare → repeat → keep the winner.
|
|
263
|
+
music; after that, every text/style/CTA/end-card/logo tweak re-renders over them for **0 credits** —
|
|
264
|
+
`update_short` (new hook/CTA/style) → `rerender_short` → compare → repeat → keep the winner. Logo
|
|
265
|
+
placement (`set_short_logo`, or `short_logo_position`/`short_logo_treatment`) is free too. Only
|
|
264
266
|
footage/music-affecting edits (`product_prompt`, product image, `creative_format`, `visual_language`,
|
|
265
267
|
`theme`, `music_vibe`, `music_instruments`, `language`) need another `generate_short`. Edits that
|
|
266
268
|
add/remove the end card (poster, `end_card`) can shift the video between 12s and 14s; the pinned music
|
package/dist/index.js
CHANGED
|
@@ -29498,6 +29498,16 @@ function normalizeStatus(kind, slug, data) {
|
|
|
29498
29498
|
video_url: videoUrl,
|
|
29499
29499
|
error: d.error_message ?? null
|
|
29500
29500
|
};
|
|
29501
|
+
if (typeof d.failed_stage === "string")
|
|
29502
|
+
status.failed_stage = d.failed_stage;
|
|
29503
|
+
if (typeof d.failure_code === "string")
|
|
29504
|
+
status.failure_code = d.failure_code;
|
|
29505
|
+
if (d.failure_details && typeof d.failure_details === "object")
|
|
29506
|
+
status.failure_details = d.failure_details;
|
|
29507
|
+
if (typeof d.failure_source === "string")
|
|
29508
|
+
status.failure_source = d.failure_source;
|
|
29509
|
+
if (typeof d.failure_attempt === "number")
|
|
29510
|
+
status.failure_attempt = d.failure_attempt;
|
|
29501
29511
|
if (typeof d.latest_render_free === "boolean") {
|
|
29502
29512
|
status.latest_render_free = d.latest_render_free;
|
|
29503
29513
|
}
|
|
@@ -29558,7 +29568,7 @@ function normalizeStatus(kind, slug, data) {
|
|
|
29558
29568
|
const renderFresh = latest.is_stale === false;
|
|
29559
29569
|
const stale = renderStale || staleSegments.length > 0 || audioStale;
|
|
29560
29570
|
const renderActive = renderStatus === "pending" || renderStatus === "rendering" || renderStatus === "processing";
|
|
29561
|
-
const scenarioActive = d.scenario_status === "generating" || d.scenario_apply_status === "applying";
|
|
29571
|
+
const scenarioActive = d.scenario_status === "generating" || d.scenario_apply_status === "pending" || d.scenario_apply_status === "applying";
|
|
29562
29572
|
const narrationActive = d.narration_status === "generating";
|
|
29563
29573
|
const scenarioFailed = d.scenario_status === "failed";
|
|
29564
29574
|
const scenarioApplyFailed = d.scenario_apply_status === "failed";
|
|
@@ -29589,13 +29599,16 @@ function normalizeStatus(kind, slug, data) {
|
|
|
29589
29599
|
].find((value) => typeof value === "string" && value.trim().length > 0);
|
|
29590
29600
|
const audioActive = currentAudio.status === "pending" || currentAudio.status === "processing" || currentMusic.status === "pending" || currentMusic.status === "processing" || newerAudioVersions.some(versionActive) || newerMusicVersions.some(versionActive);
|
|
29591
29601
|
const workActive = autopilotActive || batchActive || renderActive || scenarioActive || narrationActive || audioActive || activeSegments.length > 0;
|
|
29592
|
-
const
|
|
29602
|
+
const deliveredArtifactSupersedesOrchestrationFailure = renderStatus === "completed" && Boolean(videoUrl) && renderFresh && !stale && !workActive && failedSegments.length === 0 && !audioFailed;
|
|
29603
|
+
const ready = deliveredArtifactSupersedesOrchestrationFailure;
|
|
29593
29604
|
const autopilotFailed = autopilot === "failed" || autopilot === "cancelled";
|
|
29594
29605
|
const renderFailed = renderStatus === "failed";
|
|
29595
|
-
const failed = autopilotFailed || renderFailed || scenarioApplyFailed || scenarioFailed || narrationFailed || batchFailed || failedSegments.length > 0 || audioFailed;
|
|
29606
|
+
const failed = autopilotFailed && !deliveredArtifactSupersedesOrchestrationFailure || renderFailed || scenarioApplyFailed && !deliveredArtifactSupersedesOrchestrationFailure || scenarioFailed && !deliveredArtifactSupersedesOrchestrationFailure || narrationFailed && !deliveredArtifactSupersedesOrchestrationFailure || batchFailed && !deliveredArtifactSupersedesOrchestrationFailure || failedSegments.length > 0 || audioFailed;
|
|
29596
29607
|
const terminal = ready || failed || batchInsufficientCredits || !workActive;
|
|
29597
29608
|
let stage = autopilot;
|
|
29598
|
-
if (
|
|
29609
|
+
if (ready)
|
|
29610
|
+
stage = "completed";
|
|
29611
|
+
else if (autopilot === "failed")
|
|
29599
29612
|
stage = "failed";
|
|
29600
29613
|
else if (autopilot === "cancelled")
|
|
29601
29614
|
stage = "cancelled";
|
|
@@ -29629,8 +29642,6 @@ function normalizeStatus(kind, slug, data) {
|
|
|
29629
29642
|
stage = "audio_processing";
|
|
29630
29643
|
else if (!workActive && stale)
|
|
29631
29644
|
stage = "editing_required";
|
|
29632
|
-
else if (ready)
|
|
29633
|
-
stage = "completed";
|
|
29634
29645
|
const segmentError = failedSegments.map((segment) => segment.error_message).find((value) => typeof value === "string" && value.length > 0);
|
|
29635
29646
|
return {
|
|
29636
29647
|
kind,
|
|
@@ -29639,7 +29650,7 @@ function normalizeStatus(kind, slug, data) {
|
|
|
29639
29650
|
terminal,
|
|
29640
29651
|
ready,
|
|
29641
29652
|
video_url: videoUrl,
|
|
29642
|
-
error: d.autopilot_error_message ?? latest.error_message ?? d.scenario_apply_error_message ?? d.scenario_error_message ?? d.narration_error_message ?? d.batch_generation_error_message ?? segmentError ?? audioError ?? (batchInsufficientCredits ? "Batch generation paused: not enough credits to finish. Top up or reduce scope, then resume." : null),
|
|
29653
|
+
error: ready ? null : d.autopilot_error_message ?? latest.error_message ?? d.scenario_apply_error_message ?? d.scenario_error_message ?? d.narration_error_message ?? d.batch_generation_error_message ?? segmentError ?? audioError ?? (batchInsufficientCredits ? "Batch generation paused: not enough credits to finish. Top up or reduce scope, then resume." : null),
|
|
29643
29654
|
stale,
|
|
29644
29655
|
active_segment_ids: activeSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string"),
|
|
29645
29656
|
stale_segment_ids: staleSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string")
|
|
@@ -30232,7 +30243,7 @@ function readStoredCredentials() {
|
|
|
30232
30243
|
// package.json
|
|
30233
30244
|
var package_default = {
|
|
30234
30245
|
name: "@hubfluencer/mcp",
|
|
30235
|
-
version: "0.
|
|
30246
|
+
version: "0.10.0",
|
|
30236
30247
|
description: "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
|
|
30237
30248
|
license: "MIT",
|
|
30238
30249
|
author: "Monocursive <contact@monocursive.com>",
|
|
@@ -30545,6 +30556,11 @@ var getStatusOutput = exports_external.object({
|
|
|
30545
30556
|
stale: exports_external.boolean().optional(),
|
|
30546
30557
|
active_segment_ids: exports_external.array(exports_external.union([exports_external.number(), exports_external.string()])).optional(),
|
|
30547
30558
|
stale_segment_ids: exports_external.array(exports_external.union([exports_external.number(), exports_external.string()])).optional(),
|
|
30559
|
+
failed_stage: exports_external.string().optional().describe("Short only: exact API failure phase behind a terminal failure."),
|
|
30560
|
+
failure_code: exports_external.string().optional().describe("Short only: stable machine-readable failure classification."),
|
|
30561
|
+
failure_details: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Short only: structured provider/QC details for recovery."),
|
|
30562
|
+
failure_source: exports_external.string().optional().describe("Short only: worker/recovery layer that finalized the failure."),
|
|
30563
|
+
failure_attempt: exports_external.number().optional().describe("Short only: paid generation attempt number that failed."),
|
|
30548
30564
|
latest_render_free: exports_external.boolean().optional(),
|
|
30549
30565
|
last_free_rerender_failed: exports_external.boolean().optional()
|
|
30550
30566
|
}).passthrough();
|
|
@@ -30559,6 +30575,11 @@ var waitForCompletionOutput = exports_external.object({
|
|
|
30559
30575
|
stale: exports_external.boolean().optional(),
|
|
30560
30576
|
active_segment_ids: exports_external.array(exports_external.union([exports_external.number(), exports_external.string()])).optional(),
|
|
30561
30577
|
stale_segment_ids: exports_external.array(exports_external.union([exports_external.number(), exports_external.string()])).optional(),
|
|
30578
|
+
failed_stage: exports_external.string().optional().describe("Short only: exact API failure phase behind a terminal failure."),
|
|
30579
|
+
failure_code: exports_external.string().optional().describe("Short only: stable machine-readable failure classification."),
|
|
30580
|
+
failure_details: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Short only: structured provider/QC details for recovery."),
|
|
30581
|
+
failure_source: exports_external.string().optional().describe("Short only: worker/recovery layer that finalized the failure."),
|
|
30582
|
+
failure_attempt: exports_external.number().optional().describe("Short only: paid generation attempt number that failed."),
|
|
30562
30583
|
latest_render_free: exports_external.boolean().optional(),
|
|
30563
30584
|
last_free_rerender_failed: exports_external.boolean().optional(),
|
|
30564
30585
|
saved_to: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
|
|
@@ -31102,6 +31123,7 @@ var MAX_VIDEO_BYTES = 500000000;
|
|
|
31102
31123
|
var MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
31103
31124
|
var MAX_PRODUCT_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
31104
31125
|
var MAX_LOGO_BYTES = 1 * 1024 * 1024;
|
|
31126
|
+
var MAX_SHORT_LOGO_BYTES = 5 * 1024 * 1024;
|
|
31105
31127
|
var MAX_CLOSING_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
31106
31128
|
var MULTIPART_THRESHOLD = 50 * 1024 * 1024;
|
|
31107
31129
|
var MAX_CATALOG_VIDEO_DURATION_SECONDS = 60;
|
|
@@ -31531,6 +31553,8 @@ async function editorAutopilotQuote(client, slug, overrides = {}) {
|
|
|
31531
31553
|
params.set("product_subject", overrides.product_subject);
|
|
31532
31554
|
if (overrides.product_prompt)
|
|
31533
31555
|
params.set("product_prompt", overrides.product_prompt);
|
|
31556
|
+
if (overrides.restart)
|
|
31557
|
+
params.set("restart", "true");
|
|
31534
31558
|
const query = params.toString();
|
|
31535
31559
|
const response = await client.get(`/editor/${slug}/autopilot/cost${query ? `?${query}` : ""}`);
|
|
31536
31560
|
const cost = asRecord(asRecord(response).data);
|
|
@@ -31553,6 +31577,26 @@ async function editorAutopilotQuote(client, slug, overrides = {}) {
|
|
|
31553
31577
|
};
|
|
31554
31578
|
}
|
|
31555
31579
|
}
|
|
31580
|
+
async function editorChargeReceipt(client, slug, action, listPriceCredits) {
|
|
31581
|
+
try {
|
|
31582
|
+
const response = await client.get(`/editor/${slug}`);
|
|
31583
|
+
const editor = asRecord(asRecord(response).data);
|
|
31584
|
+
return {
|
|
31585
|
+
action,
|
|
31586
|
+
list_price_credits: listPriceCredits,
|
|
31587
|
+
project_spend: editor.project_spend ?? null,
|
|
31588
|
+
approved_cap: editor.autopilot_max_credits ?? null,
|
|
31589
|
+
approved_run_spend: editor.autopilot_credits_spent ?? null
|
|
31590
|
+
};
|
|
31591
|
+
} catch {
|
|
31592
|
+
return {
|
|
31593
|
+
action,
|
|
31594
|
+
list_price_credits: listPriceCredits,
|
|
31595
|
+
project_spend: null,
|
|
31596
|
+
enrichment_unavailable: true
|
|
31597
|
+
};
|
|
31598
|
+
}
|
|
31599
|
+
}
|
|
31556
31600
|
function isRecompositeInProgressConflict(e) {
|
|
31557
31601
|
const err = e;
|
|
31558
31602
|
if (err?.status !== 409)
|
|
@@ -31772,7 +31816,7 @@ DELEGATE THE CONTENT OPERATION (the full loop — steps 1-3 are $0)
|
|
|
31772
31816
|
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.
|
|
31773
31817
|
|
|
31774
31818
|
CREDITS (spent server-side; each tool prices before charging — see its description)
|
|
31775
|
-
- 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.
|
|
31819
|
+
- 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.
|
|
31776
31820
|
- 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.
|
|
31777
31821
|
- 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.
|
|
31778
31822
|
|
|
@@ -32065,7 +32109,7 @@ registerTool("list_projects", {
|
|
|
32065
32109
|
}));
|
|
32066
32110
|
registerTool("create_short", {
|
|
32067
32111
|
title: "Create a short (draft)",
|
|
32068
|
-
description: "Creates a short draft from a product prompt (min 10 chars). A short is a 12s vertical (two 6s AI " + "segments + an on-screen title overlay + music; 14s when it ends on a poster or brand-lockup end card). " + "Returns the slug, costs 0 credits. Follow with generate_short to render. Iteration is FREE after that " + "first generate: the 15 credits bought the footage + music, and every later text/style/CTA/end-card " + "tweak re-renders over them for 0 credits (update_short → rerender_short). " + "BE PROACTIVE: don't ship a bare clip — set a headline (the on-screen TITLE) and subheadline (secondary " + "title), and pick a music_vibe plus visual_language that fit the brand. " + "Always set cta_text — a short without a CTA doesn't convert (plus offer_text only if there's a real " + "deal). Note: blank cta_text → the server renders a neutral localized ask (e.g. 'Learn more'); a truly " + "CTA-less short is not currently supported. " + "Keep headline ≈4 words (≤40 chars). Set visual_language explicitly (kinetic_creator is a strong " + "default). If headline/subheadline/text_beats are left blank, the server auto-writes the headline + " + "caption beats at generate time (pass skip_auto_text:true to generate_short for a deliberately bare clip). " + "To brand it further, attach a product image " + "(set_short_product)
|
|
32112
|
+
description: "Creates a short draft from a product prompt (min 10 chars). A short is a 12s vertical (two 6s AI " + "segments + an on-screen title overlay + music; 14s when it ends on a poster or brand-lockup end card). " + "Returns the slug, costs 0 credits. Follow with generate_short to render. Iteration is FREE after that " + "first generate: the 15 credits bought the footage + music, and every later text/style/CTA/end-card/logo " + "tweak re-renders over them for 0 credits (update_short → rerender_short). " + "BE PROACTIVE: don't ship a bare clip — set a headline (the on-screen TITLE) and subheadline (secondary " + "title), and pick a music_vibe plus visual_language that fit the brand. " + "Always set cta_text — a short without a CTA doesn't convert (plus offer_text only if there's a real " + "deal). Note: blank cta_text → the server renders a neutral localized ask (e.g. 'Learn more'); a truly " + "CTA-less short is not currently supported. " + "Keep headline ≈4 words (≤40 chars). Set visual_language explicitly (kinetic_creator is a strong " + "default). If headline/subheadline/text_beats are left blank, the server auto-writes the headline + " + "caption beats at generate time (pass skip_auto_text:true to generate_short for a deliberately bare clip). " + "To brand it further, attach a product image " + "(set_short_product), a deterministic brand logo (set_short_logo), or an end-card poster " + "(set_short_poster) before generate_short.",
|
|
32069
32113
|
inputSchema: {
|
|
32070
32114
|
product_prompt: exports_external.string().min(10).describe("What the short should be about"),
|
|
32071
32115
|
language: exports_external.enum(LANGUAGES).optional().describe('Language code, e.g. "en" (default). Bare ISO-639-1 code only — no region/script subtag like "en-US".'),
|
|
@@ -32162,7 +32206,7 @@ registerTool("create_short", {
|
|
|
32162
32206
|
}));
|
|
32163
32207
|
registerTool("update_short", {
|
|
32164
32208
|
title: "Update a short draft (0 credits)",
|
|
32165
|
-
description: "Patches an existing short draft — only the fields you pass change; omitted fields are left untouched. " + "0 credits, no re-render by itself. APPLYING the edit: changed only text/style/CTA/end-card fields " + "(headline, subheadline, text_beats, cta_text, offer_text, badge_text, star_rating, colors, font, " + "position, animation, closing_claim, brand_name, poster_includes_lockup, end_card) on an " + "already-generated short? " + "rerender_short applies them for 0 credits. Footage/music-affecting edits (product_prompt, product " + "image, creative_format, visual_language, theme, music_vibe, music_instruments, language) need " + "generate_short (15 credits) again. Use this to edit the brief, copy, " + "styling, or the optional conversion graphics after create_short. " + "Always set cta_text — a short without a CTA doesn't convert (plus offer_text only if there's a real " + "deal). Note: blank cta_text → the server renders a neutral localized ask (e.g. 'Learn more'); a truly " + "CTA-less short is not currently supported. " + "Keep headline ≈4 words (≤40 chars). Set visual_language explicitly (kinetic_creator is a strong " + "default). " + "CLEARING a field: the conversion graphics (offer_text, cta_text, badge_text, star_rating) and " + 'text_beats are OPT-IN and OFF by default — pass "" (or 0 for star_rating, [] for text_beats) to REMOVE ' + "one that was set. The four conversion graphics are user-supplied only and never written by the AI copy " + "assist; text_beats (like headline/subheadline) IS rewritten by generate_short_text, so hand-crafted " + "beats don't survive a later assist call. " + "404 for an unknown/foreign slug.",
|
|
32209
|
+
description: "Patches an existing short draft — only the fields you pass change; omitted fields are left untouched. " + "0 credits, no re-render by itself. APPLYING the edit: changed only text/style/CTA/end-card/logo fields " + "(headline, subheadline, text_beats, cta_text, offer_text, badge_text, star_rating, colors, font, " + "position, animation, closing_claim, brand_name, poster_includes_lockup, end_card, short_logo_position, " + "short_logo_treatment) on an " + "already-generated short? " + "rerender_short applies them for 0 credits. Footage/music-affecting edits (product_prompt, product " + "image, creative_format, visual_language, theme, music_vibe, music_instruments, language) need " + "generate_short (15 credits) again. Use this to edit the brief, copy, " + "styling, or the optional conversion graphics after create_short. " + "Always set cta_text — a short without a CTA doesn't convert (plus offer_text only if there's a real " + "deal). Note: blank cta_text → the server renders a neutral localized ask (e.g. 'Learn more'); a truly " + "CTA-less short is not currently supported. " + "Keep headline ≈4 words (≤40 chars). Set visual_language explicitly (kinetic_creator is a strong " + "default). " + "CLEARING a field: the conversion graphics (offer_text, cta_text, badge_text, star_rating) and " + 'text_beats are OPT-IN and OFF by default — pass "" (or 0 for star_rating, [] for text_beats) to REMOVE ' + "one that was set. The four conversion graphics are user-supplied only and never written by the AI copy " + "assist; text_beats (like headline/subheadline) IS rewritten by generate_short_text, so hand-crafted " + "beats don't survive a later assist call. " + "404 for an unknown/foreign slug.",
|
|
32166
32210
|
inputSchema: {
|
|
32167
32211
|
slug: exports_external.string().describe("Short slug (from create_short)"),
|
|
32168
32212
|
product_prompt: exports_external.string().min(10).optional().describe("Rewrite the brief (what the short is about, ≥10 chars); drives the next generate_short."),
|
|
@@ -32198,6 +32242,8 @@ registerTool("update_short", {
|
|
|
32198
32242
|
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.`),
|
|
32199
32243
|
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)."),
|
|
32200
32244
|
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)."),
|
|
32245
|
+
short_logo_position: exports_external.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).optional().describe("Corner for the deterministic brand-logo overlay (set the image with set_short_logo). Moving it is " + "a FREE re-render — logo fields never cost credits."),
|
|
32246
|
+
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."),
|
|
32201
32247
|
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.")
|
|
32202
32248
|
},
|
|
32203
32249
|
annotations: {
|
|
@@ -32255,13 +32301,17 @@ registerTool("update_short", {
|
|
|
32255
32301
|
body.end_card = args.end_card;
|
|
32256
32302
|
if (args.poster_includes_lockup !== undefined)
|
|
32257
32303
|
body.poster_includes_lockup = args.poster_includes_lockup;
|
|
32304
|
+
if (args.short_logo_position !== undefined)
|
|
32305
|
+
body.short_logo_position = args.short_logo_position;
|
|
32306
|
+
if (args.short_logo_treatment !== undefined)
|
|
32307
|
+
body.short_logo_treatment = args.short_logo_treatment;
|
|
32258
32308
|
if (args.brand_profile_id !== undefined)
|
|
32259
32309
|
body.brand_profile_id = args.brand_profile_id;
|
|
32260
32310
|
const res = await client.patch(`/shorts/${args.slug}`, body);
|
|
32261
32311
|
const data = asRecord(res).data ?? res;
|
|
32262
32312
|
return ok({
|
|
32263
32313
|
...asRecord(data),
|
|
32264
|
-
next: "Saved. Only text/style/CTA/end-card changed on an already-generated short? " + `rerender_short({ slug: "${args.slug}" }) applies it for 0 credits. Footage/music edits ` + "(prompt, product image, format, visual_language, theme, music_*, language) need " + "generate_short (15 credits)."
|
|
32314
|
+
next: "Saved. Only text/style/CTA/end-card/logo changed on an already-generated short? " + `rerender_short({ slug: "${args.slug}" }) applies it for 0 credits. Footage/music edits ` + "(prompt, product image, format, visual_language, theme, music_*, language) need " + "generate_short (15 credits)."
|
|
32265
32315
|
});
|
|
32266
32316
|
}));
|
|
32267
32317
|
async function getTracking(client, slug, deadline) {
|
|
@@ -32430,7 +32480,7 @@ registerTool("generate_short", {
|
|
|
32430
32480
|
}));
|
|
32431
32481
|
registerTool("rerender_short", {
|
|
32432
32482
|
title: "Re-render a short (FREE — 0 credits)",
|
|
32433
|
-
description: "FREE re-render: applies the short's current text/style/CTA/end-card state over the already-paid " + "footage and music — 0 credits. Use after update_short for copy/style/CTA tweaks. Footage or music " + "changes need generate_short (15 credits) instead. Edits that add/remove the end card (poster, " + "end_card) can shift the video between 12s and 14s; when the pinned music bed is shorter than the " + "export it crossfades into a repeated continuation, then fades at the export end, without regeneration. " + "Requires a completed generation to re-render over " + "(422 short_not_ready otherwise — run generate_short once first); while any generation/render is in " + "flight it reports in-progress (409) rather than duplicating. Then poll with get_status or " + "wait_for_completion (kind=short). A failed free re-render leaves the delivered video intact — retry " + "rerender_short, it stays free (the status fields latest_render_free / last_free_rerender_failed " + "report this when the server sends them).",
|
|
32483
|
+
description: "FREE re-render: applies the short's current text/style/CTA/end-card/logo state over the already-paid " + "footage and music — 0 credits. Use after update_short for copy/style/CTA tweaks, or after a logo change " + "(set_short_logo, or short_logo_position/short_logo_treatment via update_short). Footage or music " + "changes need generate_short (15 credits) instead. Edits that add/remove the end card (poster, " + "end_card) can shift the video between 12s and 14s; when the pinned music bed is shorter than the " + "export it crossfades into a repeated continuation, then fades at the export end, without regeneration. " + "Requires a completed generation to re-render over " + "(422 short_not_ready otherwise — run generate_short once first); while any generation/render is in " + "flight it reports in-progress (409) rather than duplicating. Then poll with get_status or " + "wait_for_completion (kind=short). A failed free re-render leaves the delivered video intact — retry " + "rerender_short, it stays free (the status fields latest_render_free / last_free_rerender_failed " + "report this when the server sends them).",
|
|
32434
32484
|
inputSchema: {
|
|
32435
32485
|
slug: exports_external.string().describe("Short slug (from create_short)")
|
|
32436
32486
|
},
|
|
@@ -32805,13 +32855,14 @@ registerTool("create_editor_ad", {
|
|
|
32805
32855
|
}));
|
|
32806
32856
|
registerTool("start_autopilot", {
|
|
32807
32857
|
title: "Start autopilot on an existing editor draft",
|
|
32808
|
-
description: "Starts server-orchestrated autopilot (scenario → segments → narration → voice → music → render) on an " + "EXISTING editor project/draft. Without max_credits it returns the live quote and charges nothing. Supply an " + "explicit max_credits cap to authorize launch; the tool refuses if pricing is unavailable, the estimate exceeds " + "the cap, or the balance is insufficient. Each authorized call is a genuine start attempt (like tapping Launch in the app): " + "if a run is already in progress for this slug the server returns already_running — it never double-starts " + "or double-charges.
|
|
32858
|
+
description: "Starts server-orchestrated autopilot (scenario → segments → narration → voice → music → render) on an " + "EXISTING editor project/draft. Without max_credits it returns the live quote and charges nothing. Supply an " + "explicit max_credits cap to authorize launch; the tool refuses if pricing is unavailable, the estimate exceeds " + "the cap, or the balance is insufficient. Each authorized call is a genuine start attempt (like tapping Launch in the app): " + "if a run is already in progress for this slug the server returns already_running — it never double-starts " + "or double-charges. Failed or cancelled runs resume normally. For a completed project, set restart:true to quote and build a fresh creative run; omitting it only resumes pending edits. Then poll with " + 'wait_for_completion({ slug, kind: "editor" }).',
|
|
32809
32859
|
inputSchema: {
|
|
32810
32860
|
slug: exports_external.string().describe("Editor project slug"),
|
|
32811
32861
|
language: exports_external.enum(LANGUAGES).optional().describe('Optional language override applied before launch. Bare ISO-639-1 code only, e.g. "fr" — no ' + "region/script subtag. Drives scenario, narration, voice, and captions."),
|
|
32812
32862
|
product_subject: exports_external.string().max(300).optional().describe("Optional locked product / main subject applied before launch so the AI cannot invent an " + "unrelated product."),
|
|
32813
32863
|
product_prompt: exports_external.string().optional().describe("Optional freeform brief to set/replace before launch."),
|
|
32814
|
-
max_credits: exports_external.number().int().nonnegative().optional().describe("Explicit spend cap. Omit to quote only; no credits are charged.")
|
|
32864
|
+
max_credits: exports_external.number().int().nonnegative().optional().describe("Explicit spend cap. Omit to quote only; no credits are charged."),
|
|
32865
|
+
restart: exports_external.boolean().optional().describe("Set true only on a completed project to quote/build a fresh creative run. The delivered timeline stays available until replacement apply.")
|
|
32815
32866
|
},
|
|
32816
32867
|
outputSchema: createEditorOutput,
|
|
32817
32868
|
annotations: { title: "Start autopilot", ...WRITE, idempotentHint: false }
|
|
@@ -32830,6 +32881,8 @@ registerTool("start_autopilot", {
|
|
|
32830
32881
|
overrides.product_subject = args.product_subject;
|
|
32831
32882
|
if (args.product_prompt !== undefined)
|
|
32832
32883
|
overrides.product_prompt = args.product_prompt;
|
|
32884
|
+
if (args.restart)
|
|
32885
|
+
overrides.restart = true;
|
|
32833
32886
|
const quote = await editorAutopilotQuote(client, slug, overrides);
|
|
32834
32887
|
const overCap = args.max_credits !== undefined && quote.estimated_credits !== null && quote.estimated_credits > args.max_credits;
|
|
32835
32888
|
if (args.max_credits === undefined || quote.estimated_credits === null || overCap || !quote.affordable) {
|
|
@@ -33110,7 +33163,10 @@ registerTool("regenerate_segment", {
|
|
|
33110
33163
|
}
|
|
33111
33164
|
}, tool(async (args, client) => {
|
|
33112
33165
|
const res = await client.post(`/editor/${args.slug}/segments/${String(args.segment_id)}/regenerate`, args.prompt !== undefined ? { prompt: args.prompt } : undefined, undefined);
|
|
33113
|
-
return ok(
|
|
33166
|
+
return ok({
|
|
33167
|
+
...asRecord(asRecord(res).data ?? res),
|
|
33168
|
+
charge_receipt: await editorChargeReceipt(client, args.slug, "scene_regeneration", 4)
|
|
33169
|
+
});
|
|
33114
33170
|
}));
|
|
33115
33171
|
registerTool("generate_all_segments", {
|
|
33116
33172
|
title: "Generate all pending segments (sequential)",
|
|
@@ -33227,7 +33283,10 @@ registerTool("generate_voice", {
|
|
|
33227
33283
|
annotations: { title: "Generate voice", ...WRITE, idempotentHint: true }
|
|
33228
33284
|
}, tool(async (args, client) => {
|
|
33229
33285
|
const res = await client.post(`/editor/${args.slug}/generate-voice`, { voice_id: args.voice_id }, await voiceKey(client, args.slug, args.voice_id));
|
|
33230
|
-
return ok(
|
|
33286
|
+
return ok({
|
|
33287
|
+
...asRecord(asRecord(res).data ?? res),
|
|
33288
|
+
charge_receipt: await editorChargeReceipt(client, args.slug, "voice_generation", 3)
|
|
33289
|
+
});
|
|
33231
33290
|
}));
|
|
33232
33291
|
registerTool("generate_music", {
|
|
33233
33292
|
title: "Generate the background music (5 credits)",
|
|
@@ -33254,11 +33313,14 @@ registerTool("generate_music", {
|
|
|
33254
33313
|
if (args.instruments !== undefined)
|
|
33255
33314
|
body.instruments = args.instruments;
|
|
33256
33315
|
const res = await client.post(`/editor/${args.slug}/generate-music`, body, undefined);
|
|
33257
|
-
return ok(
|
|
33316
|
+
return ok({
|
|
33317
|
+
...asRecord(asRecord(res).data ?? res),
|
|
33318
|
+
charge_receipt: await editorChargeReceipt(client, args.slug, "music_generation", 5)
|
|
33319
|
+
});
|
|
33258
33320
|
}));
|
|
33259
33321
|
registerTool("render", {
|
|
33260
33322
|
title: "Render the final editor video (0 credits)",
|
|
33261
|
-
description: "Renders the final MP4 from the generated segments + voice + music. Costs 0 credits (any still-ungenerated " + "segments are auto-charged at generation cost first). Re-rendering reflects the current segments/voice/music (it does not replay a prior render). Returns the latest_render; " + 'pair with wait_for_completion({ slug, kind: "editor" }) then download_result. ' + "STALENESS: editing the scenario or narration AFTER generating voice/music marks those audio tracks stale, and render then 422s with editor_narration_stale / editor_voice_stale / editor_music_stale — regenerate the named track (generate_narration → generate_voice, and/or generate_music) before rendering.",
|
|
33323
|
+
description: "Renders the final MP4 from the generated segments + voice + music. Costs 0 credits (any still-ungenerated " + "segments are auto-charged at generation cost first). Render NEVER generates or charges for voice/music; a new " + "music charge comes only from generate_music or an already-running autopilot child job. Re-rendering reflects the current segments/voice/music (it does not replay a prior render). Returns the latest_render; " + 'pair with wait_for_completion({ slug, kind: "editor" }) then download_result. ' + "STALENESS: editing the scenario or narration AFTER generating voice/music marks those audio tracks stale, and render then 422s with editor_narration_stale / editor_voice_stale / editor_music_stale — regenerate the named track (generate_narration → generate_voice, and/or generate_music) before rendering.",
|
|
33262
33324
|
inputSchema: { slug: exports_external.string().describe("Editor project slug") },
|
|
33263
33325
|
annotations: { title: "Render", ...WRITE, idempotentHint: false }
|
|
33264
33326
|
}, tool(async (args, client) => {
|
|
@@ -33598,9 +33660,28 @@ registerTool("set_short_product", {
|
|
|
33598
33660
|
const res = await client.post(`/shorts/${args.slug}/product/confirm`, { s3_key, product_description: args.description });
|
|
33599
33661
|
return ok(asRecord(res).data ?? res);
|
|
33600
33662
|
}));
|
|
33663
|
+
registerTool("set_short_logo", {
|
|
33664
|
+
title: "Set a Short brand logo overlay (0 credits)",
|
|
33665
|
+
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.",
|
|
33666
|
+
inputSchema: {
|
|
33667
|
+
slug: exports_external.string().describe("Short slug"),
|
|
33668
|
+
file_path: exports_external.string().describe("Local logo image (.png/.jpg/.jpeg)"),
|
|
33669
|
+
position: exports_external.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).optional().describe("Logo corner (default top-right)"),
|
|
33670
|
+
treatment: exports_external.enum(["throughout", "end_card", "none"]).optional().describe("When the logo appears (default throughout)")
|
|
33671
|
+
},
|
|
33672
|
+
annotations: { title: "Set Short logo", ...WRITE, idempotentHint: false }
|
|
33673
|
+
}, tool(async (args, client) => {
|
|
33674
|
+
const { s3_key } = await uploadImageFile(client, `/shorts/${args.slug}/logo/presign`, args.file_path, MAX_SHORT_LOGO_BYTES);
|
|
33675
|
+
const res = await client.post(`/shorts/${args.slug}/logo/confirm`, {
|
|
33676
|
+
s3_key,
|
|
33677
|
+
position: args.position ?? "top-right",
|
|
33678
|
+
treatment: args.treatment ?? "throughout"
|
|
33679
|
+
});
|
|
33680
|
+
return ok(asRecord(res).data ?? res);
|
|
33681
|
+
}));
|
|
33601
33682
|
registerTool("set_short_poster", {
|
|
33602
33683
|
title: "Set a short's end-card poster image (0 credits)",
|
|
33603
|
-
description: "Uploads a local image as the SHORT's end-card poster — a closing still shown at the end (extends the " + "render from 12s to 14s). Accepted: " + IMAGE_EXTS.map((e) => `.${e}`).join(", ") + " (≤20 MiB = 20,971,520 bytes), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. There is one poster per short; calling " + "again replaces it.
|
|
33684
|
+
description: "Uploads a local image as the SHORT's end-card poster — a closing still shown at the end (extends the " + "render from 12s to 14s). Accepted: " + IMAGE_EXTS.map((e) => `.${e}`).join(", ") + " (≤20 MiB = 20,971,520 bytes), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. There is one poster per short; calling " + "again replaces it. Use set_short_logo for an exact, non-generative brand overlay.",
|
|
33604
33685
|
inputSchema: {
|
|
33605
33686
|
slug: exports_external.string().describe("Short slug (from create_short)"),
|
|
33606
33687
|
file_path: exports_external.string().describe("Local poster image (.jpg/.jpeg/.png)"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubfluencer/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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/core.ts
CHANGED
|
@@ -265,6 +265,16 @@ export interface NormalizedStatus {
|
|
|
265
265
|
ready: boolean;
|
|
266
266
|
video_url: string | null;
|
|
267
267
|
error: string | null;
|
|
268
|
+
/** Short only: exact API failure phase instead of a collapsed terminal state. */
|
|
269
|
+
failed_stage?: string;
|
|
270
|
+
/** Short only: stable machine-readable terminal failure classification. */
|
|
271
|
+
failure_code?: string;
|
|
272
|
+
/** Short only: structured provider/QC details for recovery decisions. */
|
|
273
|
+
failure_details?: Record<string, unknown>;
|
|
274
|
+
/** Short only: worker/recovery layer that finalized the failed attempt. */
|
|
275
|
+
failure_source?: string;
|
|
276
|
+
/** Short only: paid generation attempt number that failed. */
|
|
277
|
+
failure_attempt?: number;
|
|
268
278
|
/** Editor only: the newest delivered render no longer matches the live project. */
|
|
269
279
|
stale?: boolean;
|
|
270
280
|
/** Editor only: current scene ids that are still generating. */
|
|
@@ -341,6 +351,16 @@ export function normalizeStatus(
|
|
|
341
351
|
video_url: videoUrl,
|
|
342
352
|
error: (d.error_message as string) ?? null,
|
|
343
353
|
};
|
|
354
|
+
if (typeof d.failed_stage === "string")
|
|
355
|
+
status.failed_stage = d.failed_stage;
|
|
356
|
+
if (typeof d.failure_code === "string")
|
|
357
|
+
status.failure_code = d.failure_code;
|
|
358
|
+
if (d.failure_details && typeof d.failure_details === "object")
|
|
359
|
+
status.failure_details = d.failure_details as Record<string, unknown>;
|
|
360
|
+
if (typeof d.failure_source === "string")
|
|
361
|
+
status.failure_source = d.failure_source;
|
|
362
|
+
if (typeof d.failure_attempt === "number")
|
|
363
|
+
status.failure_attempt = d.failure_attempt;
|
|
344
364
|
// Additive free-re-render bookkeeping (2026-07 API): surfaced only when
|
|
345
365
|
// the server actually sends the booleans, so older payloads normalize
|
|
346
366
|
// byte-identically. A failed free re-render does NOT flip stage to
|
|
@@ -477,10 +497,11 @@ export function normalizeStatus(
|
|
|
477
497
|
renderStatus === "processing";
|
|
478
498
|
// Scenario text generation / scenario apply run as async workers too
|
|
479
499
|
// (scenario_status: none|generating|ready|failed; scenario_apply_status:
|
|
480
|
-
// idle|applying|failed) — count them as active so a wait right after
|
|
500
|
+
// idle|pending|applying|failed) — count them as active so a wait right after
|
|
481
501
|
// generate_scenario/apply_scenario doesn't bail out as an "idle" draft.
|
|
482
502
|
const scenarioActive =
|
|
483
503
|
d.scenario_status === "generating" ||
|
|
504
|
+
d.scenario_apply_status === "pending" ||
|
|
484
505
|
d.scenario_apply_status === "applying";
|
|
485
506
|
// Narration generation is a distinct async worker that runs before an
|
|
486
507
|
// AudioVersion exists. Without this gate, a project with an older completed
|
|
@@ -553,27 +574,27 @@ export function normalizeStatus(
|
|
|
553
574
|
narrationActive ||
|
|
554
575
|
audioActive ||
|
|
555
576
|
activeSegments.length > 0;
|
|
556
|
-
|
|
577
|
+
// Orchestration failures are historical once a repaired timeline has a fresh,
|
|
578
|
+
// completed MP4. Keeping them terminal would permanently block download_result
|
|
579
|
+
// after a successful granular recovery.
|
|
580
|
+
const deliveredArtifactSupersedesOrchestrationFailure =
|
|
557
581
|
renderStatus === "completed" &&
|
|
558
582
|
Boolean(videoUrl) &&
|
|
559
583
|
renderFresh &&
|
|
560
584
|
!stale &&
|
|
561
585
|
!workActive &&
|
|
562
586
|
failedSegments.length === 0 &&
|
|
563
|
-
!audioFailed
|
|
564
|
-
|
|
565
|
-
!scenarioApplyFailed &&
|
|
566
|
-
!narrationFailed &&
|
|
567
|
-
!batchFailed;
|
|
587
|
+
!audioFailed;
|
|
588
|
+
const ready = deliveredArtifactSupersedesOrchestrationFailure;
|
|
568
589
|
const autopilotFailed = autopilot === "failed" || autopilot === "cancelled";
|
|
569
590
|
const renderFailed = renderStatus === "failed";
|
|
570
591
|
const failed =
|
|
571
|
-
autopilotFailed ||
|
|
592
|
+
(autopilotFailed && !deliveredArtifactSupersedesOrchestrationFailure) ||
|
|
572
593
|
renderFailed ||
|
|
573
|
-
scenarioApplyFailed ||
|
|
574
|
-
scenarioFailed ||
|
|
575
|
-
narrationFailed ||
|
|
576
|
-
batchFailed ||
|
|
594
|
+
(scenarioApplyFailed && !deliveredArtifactSupersedesOrchestrationFailure) ||
|
|
595
|
+
(scenarioFailed && !deliveredArtifactSupersedesOrchestrationFailure) ||
|
|
596
|
+
(narrationFailed && !deliveredArtifactSupersedesOrchestrationFailure) ||
|
|
597
|
+
(batchFailed && !deliveredArtifactSupersedesOrchestrationFailure) ||
|
|
577
598
|
failedSegments.length > 0 ||
|
|
578
599
|
audioFailed;
|
|
579
600
|
// Terminal = control should return to the agent because nothing will progress
|
|
@@ -589,7 +610,8 @@ export function normalizeStatus(
|
|
|
589
610
|
// Failure always wins over an unrelated active flag. Workers can leave an
|
|
590
611
|
// outer autopilot/batch state active after a child stage has failed; reporting
|
|
591
612
|
// the active stage would make agents poll forever instead of taking action.
|
|
592
|
-
if (
|
|
613
|
+
if (ready) stage = "completed";
|
|
614
|
+
else if (autopilot === "failed") stage = "failed";
|
|
593
615
|
else if (autopilot === "cancelled") stage = "cancelled";
|
|
594
616
|
else if (renderFailed) stage = "render_failed";
|
|
595
617
|
else if (scenarioApplyFailed) stage = "scenario_apply_failed";
|
|
@@ -606,7 +628,6 @@ export function normalizeStatus(
|
|
|
606
628
|
else if (narrationActive) stage = "narration_processing";
|
|
607
629
|
else if (audioActive) stage = "audio_processing";
|
|
608
630
|
else if (!workActive && stale) stage = "editing_required";
|
|
609
|
-
else if (ready) stage = "completed";
|
|
610
631
|
const segmentError = failedSegments
|
|
611
632
|
.map((segment) => segment.error_message)
|
|
612
633
|
.find(
|
|
@@ -619,18 +640,19 @@ export function normalizeStatus(
|
|
|
619
640
|
terminal,
|
|
620
641
|
ready,
|
|
621
642
|
video_url: videoUrl,
|
|
622
|
-
error:
|
|
623
|
-
|
|
624
|
-
(
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
643
|
+
error: ready
|
|
644
|
+
? null
|
|
645
|
+
: ((d.autopilot_error_message as string) ??
|
|
646
|
+
(latest.error_message as string) ??
|
|
647
|
+
(d.scenario_apply_error_message as string) ??
|
|
648
|
+
(d.scenario_error_message as string) ??
|
|
649
|
+
(d.narration_error_message as string) ??
|
|
650
|
+
(d.batch_generation_error_message as string) ??
|
|
651
|
+
segmentError ??
|
|
652
|
+
audioError ??
|
|
653
|
+
(batchInsufficientCredits
|
|
654
|
+
? "Batch generation paused: not enough credits to finish. Top up or reduce scope, then resume."
|
|
655
|
+
: null)),
|
|
634
656
|
stale,
|
|
635
657
|
active_segment_ids: activeSegments
|
|
636
658
|
.map((segment) => segment.id)
|
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,
|
|
@@ -160,6 +161,7 @@ async function editorAutopilotQuote(
|
|
|
160
161
|
language?: string;
|
|
161
162
|
product_subject?: string;
|
|
162
163
|
product_prompt?: string;
|
|
164
|
+
restart?: boolean;
|
|
163
165
|
} = {},
|
|
164
166
|
): Promise<{
|
|
165
167
|
estimated_credits: number | null;
|
|
@@ -173,6 +175,7 @@ async function editorAutopilotQuote(
|
|
|
173
175
|
params.set("product_subject", overrides.product_subject);
|
|
174
176
|
if (overrides.product_prompt)
|
|
175
177
|
params.set("product_prompt", overrides.product_prompt);
|
|
178
|
+
if (overrides.restart) params.set("restart", "true");
|
|
176
179
|
const query = params.toString();
|
|
177
180
|
const response = await client.get<{ data: unknown }>(
|
|
178
181
|
`/editor/${slug}/autopilot/cost${query ? `?${query}` : ""}`,
|
|
@@ -215,6 +218,34 @@ async function editorAutopilotQuote(
|
|
|
215
218
|
}
|
|
216
219
|
}
|
|
217
220
|
|
|
221
|
+
async function editorChargeReceipt(
|
|
222
|
+
client: HubfluencerClient,
|
|
223
|
+
slug: string,
|
|
224
|
+
action: "scene_regeneration" | "voice_generation" | "music_generation",
|
|
225
|
+
listPriceCredits: number,
|
|
226
|
+
): Promise<Record<string, unknown>> {
|
|
227
|
+
try {
|
|
228
|
+
const response = await client.get<{ data: unknown }>(`/editor/${slug}`);
|
|
229
|
+
const editor = asRecord(asRecord(response).data);
|
|
230
|
+
return {
|
|
231
|
+
action,
|
|
232
|
+
list_price_credits: listPriceCredits,
|
|
233
|
+
project_spend: editor.project_spend ?? null,
|
|
234
|
+
approved_cap: editor.autopilot_max_credits ?? null,
|
|
235
|
+
approved_run_spend: editor.autopilot_credits_spent ?? null,
|
|
236
|
+
};
|
|
237
|
+
} catch {
|
|
238
|
+
// The paid mutation already succeeded. Never make a receipt-enrichment GET
|
|
239
|
+
// look like a failed charge that an agent should retry.
|
|
240
|
+
return {
|
|
241
|
+
action,
|
|
242
|
+
list_price_credits: listPriceCredits,
|
|
243
|
+
project_spend: null,
|
|
244
|
+
enrichment_unavailable: true,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
218
249
|
function isRecompositeInProgressConflict(e: unknown): boolean {
|
|
219
250
|
const err = e as Partial<HubfluencerError>;
|
|
220
251
|
if (err?.status !== 409) return false;
|
|
@@ -584,7 +615,7 @@ DELEGATE THE CONTENT OPERATION (the full loop — steps 1-3 are $0)
|
|
|
584
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.
|
|
585
616
|
|
|
586
617
|
CREDITS (spent server-side; each tool prices before charging — see its description)
|
|
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.
|
|
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.
|
|
588
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.
|
|
589
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.
|
|
590
621
|
|
|
@@ -1406,7 +1437,7 @@ registerTool(
|
|
|
1406
1437
|
"Creates a short draft from a product prompt (min 10 chars). A short is a 12s vertical (two 6s AI " +
|
|
1407
1438
|
"segments + an on-screen title overlay + music; 14s when it ends on a poster or brand-lockup end card). " +
|
|
1408
1439
|
"Returns the slug, costs 0 credits. Follow with generate_short to render. Iteration is FREE after that " +
|
|
1409
|
-
"first generate: the 15 credits bought the footage + music, and every later text/style/CTA/end-card " +
|
|
1440
|
+
"first generate: the 15 credits bought the footage + music, and every later text/style/CTA/end-card/logo " +
|
|
1410
1441
|
"tweak re-renders over them for 0 credits (update_short → rerender_short). " +
|
|
1411
1442
|
"BE PROACTIVE: don't ship a bare clip — set a headline (the on-screen TITLE) and subheadline (secondary " +
|
|
1412
1443
|
"title), and pick a music_vibe plus visual_language that fit the brand. " +
|
|
@@ -1417,8 +1448,8 @@ registerTool(
|
|
|
1417
1448
|
"default). If headline/subheadline/text_beats are left blank, the server auto-writes the headline + " +
|
|
1418
1449
|
"caption beats at generate time (pass skip_auto_text:true to generate_short for a deliberately bare clip). " +
|
|
1419
1450
|
"To brand it further, attach a product image " +
|
|
1420
|
-
"(set_short_product)
|
|
1421
|
-
"
|
|
1451
|
+
"(set_short_product), a deterministic brand logo (set_short_logo), or an end-card poster " +
|
|
1452
|
+
"(set_short_poster) before generate_short.",
|
|
1422
1453
|
inputSchema: {
|
|
1423
1454
|
product_prompt: z
|
|
1424
1455
|
.string()
|
|
@@ -1706,9 +1737,10 @@ registerTool(
|
|
|
1706
1737
|
title: "Update a short draft (0 credits)",
|
|
1707
1738
|
description:
|
|
1708
1739
|
"Patches an existing short draft — only the fields you pass change; omitted fields are left untouched. " +
|
|
1709
|
-
"0 credits, no re-render by itself. APPLYING the edit: changed only text/style/CTA/end-card fields " +
|
|
1740
|
+
"0 credits, no re-render by itself. APPLYING the edit: changed only text/style/CTA/end-card/logo fields " +
|
|
1710
1741
|
"(headline, subheadline, text_beats, cta_text, offer_text, badge_text, star_rating, colors, font, " +
|
|
1711
|
-
"position, animation, closing_claim, brand_name, poster_includes_lockup, end_card
|
|
1742
|
+
"position, animation, closing_claim, brand_name, poster_includes_lockup, end_card, short_logo_position, " +
|
|
1743
|
+
"short_logo_treatment) on an " +
|
|
1712
1744
|
"already-generated short? " +
|
|
1713
1745
|
"rerender_short applies them for 0 credits. Footage/music-affecting edits (product_prompt, product " +
|
|
1714
1746
|
"image, creative_format, visual_language, theme, music_vibe, music_instruments, language) need " +
|
|
@@ -1892,6 +1924,20 @@ registerTool(
|
|
|
1892
1924
|
"Set true if your poster already contains brand name/CTA/badges — suppresses the overlaid " +
|
|
1893
1925
|
"claim/CTA so they don't collide (default false).",
|
|
1894
1926
|
),
|
|
1927
|
+
short_logo_position: z
|
|
1928
|
+
.enum(["top-left", "top-right", "bottom-left", "bottom-right"])
|
|
1929
|
+
.optional()
|
|
1930
|
+
.describe(
|
|
1931
|
+
"Corner for the deterministic brand-logo overlay (set the image with set_short_logo). Moving it is " +
|
|
1932
|
+
"a FREE re-render — logo fields never cost credits.",
|
|
1933
|
+
),
|
|
1934
|
+
short_logo_treatment: z
|
|
1935
|
+
.enum(["throughout", "end_card", "none"])
|
|
1936
|
+
.optional()
|
|
1937
|
+
.describe(
|
|
1938
|
+
'When the brand logo shows: "throughout" the footage, only on the "end_card", or "none" to hide it. ' +
|
|
1939
|
+
"FREE re-render.",
|
|
1940
|
+
),
|
|
1895
1941
|
brand_profile_id: z
|
|
1896
1942
|
.number()
|
|
1897
1943
|
.int()
|
|
@@ -1937,6 +1983,12 @@ registerTool(
|
|
|
1937
1983
|
brand_name?: string;
|
|
1938
1984
|
end_card?: string;
|
|
1939
1985
|
poster_includes_lockup?: boolean;
|
|
1986
|
+
short_logo_position?:
|
|
1987
|
+
| "top-left"
|
|
1988
|
+
| "top-right"
|
|
1989
|
+
| "bottom-left"
|
|
1990
|
+
| "bottom-right";
|
|
1991
|
+
short_logo_treatment?: "throughout" | "end_card" | "none";
|
|
1940
1992
|
brand_profile_id?: number;
|
|
1941
1993
|
},
|
|
1942
1994
|
client,
|
|
@@ -1983,6 +2035,13 @@ registerTool(
|
|
|
1983
2035
|
if (args.end_card !== undefined) body.end_card = args.end_card;
|
|
1984
2036
|
if (args.poster_includes_lockup !== undefined)
|
|
1985
2037
|
body.poster_includes_lockup = args.poster_includes_lockup;
|
|
2038
|
+
// Logo overlay placement — the API maps these body keys straight onto the
|
|
2039
|
+
// factory (short_logo_position/short_logo_treatment). They're excluded from
|
|
2040
|
+
// the paid-inputs snapshot, so a corner/treatment change is a FREE re-render.
|
|
2041
|
+
if (args.short_logo_position !== undefined)
|
|
2042
|
+
body.short_logo_position = args.short_logo_position;
|
|
2043
|
+
if (args.short_logo_treatment !== undefined)
|
|
2044
|
+
body.short_logo_treatment = args.short_logo_treatment;
|
|
1986
2045
|
if (args.brand_profile_id !== undefined)
|
|
1987
2046
|
body.brand_profile_id = args.brand_profile_id;
|
|
1988
2047
|
|
|
@@ -1994,7 +2053,7 @@ registerTool(
|
|
|
1994
2053
|
return ok({
|
|
1995
2054
|
...asRecord(data),
|
|
1996
2055
|
next:
|
|
1997
|
-
"Saved. Only text/style/CTA/end-card changed on an already-generated short? " +
|
|
2056
|
+
"Saved. Only text/style/CTA/end-card/logo changed on an already-generated short? " +
|
|
1998
2057
|
`rerender_short({ slug: "${args.slug}" }) applies it for 0 credits. Footage/music edits ` +
|
|
1999
2058
|
"(prompt, product image, format, visual_language, theme, music_*, language) need " +
|
|
2000
2059
|
"generate_short (15 credits).",
|
|
@@ -2434,8 +2493,9 @@ registerTool(
|
|
|
2434
2493
|
{
|
|
2435
2494
|
title: "Re-render a short (FREE — 0 credits)",
|
|
2436
2495
|
description:
|
|
2437
|
-
"FREE re-render: applies the short's current text/style/CTA/end-card state over the already-paid " +
|
|
2438
|
-
"footage and music — 0 credits. Use after update_short for copy/style/CTA tweaks
|
|
2496
|
+
"FREE re-render: applies the short's current text/style/CTA/end-card/logo state over the already-paid " +
|
|
2497
|
+
"footage and music — 0 credits. Use after update_short for copy/style/CTA tweaks, or after a logo change " +
|
|
2498
|
+
"(set_short_logo, or short_logo_position/short_logo_treatment via update_short). Footage or music " +
|
|
2439
2499
|
"changes need generate_short (15 credits) instead. Edits that add/remove the end card (poster, " +
|
|
2440
2500
|
"end_card) can shift the video between 12s and 14s; when the pinned music bed is shorter than the " +
|
|
2441
2501
|
"export it crossfades into a repeated continuation, then fades at the export end, without regeneration. " +
|
|
@@ -3278,7 +3338,7 @@ registerTool(
|
|
|
3278
3338
|
"explicit max_credits cap to authorize launch; the tool refuses if pricing is unavailable, the estimate exceeds " +
|
|
3279
3339
|
"the cap, or the balance is insufficient. Each authorized call is a genuine start attempt (like tapping Launch in the app): " +
|
|
3280
3340
|
"if a run is already in progress for this slug the server returns already_running — it never double-starts " +
|
|
3281
|
-
"or double-charges.
|
|
3341
|
+
"or double-charges. Failed or cancelled runs resume normally. For a completed project, set restart:true to quote and build a fresh creative run; omitting it only resumes pending edits. Then poll with " +
|
|
3282
3342
|
'wait_for_completion({ slug, kind: "editor" }).',
|
|
3283
3343
|
inputSchema: {
|
|
3284
3344
|
slug: z.string().describe("Editor project slug"),
|
|
@@ -3309,6 +3369,12 @@ registerTool(
|
|
|
3309
3369
|
.describe(
|
|
3310
3370
|
"Explicit spend cap. Omit to quote only; no credits are charged.",
|
|
3311
3371
|
),
|
|
3372
|
+
restart: z
|
|
3373
|
+
.boolean()
|
|
3374
|
+
.optional()
|
|
3375
|
+
.describe(
|
|
3376
|
+
"Set true only on a completed project to quote/build a fresh creative run. The delivered timeline stays available until replacement apply.",
|
|
3377
|
+
),
|
|
3312
3378
|
},
|
|
3313
3379
|
outputSchema: createEditorOutput,
|
|
3314
3380
|
annotations: { title: "Start autopilot", ...WRITE, idempotentHint: false },
|
|
@@ -3321,6 +3387,7 @@ registerTool(
|
|
|
3321
3387
|
product_subject?: string;
|
|
3322
3388
|
product_prompt?: string;
|
|
3323
3389
|
max_credits?: number;
|
|
3390
|
+
restart?: boolean;
|
|
3324
3391
|
},
|
|
3325
3392
|
client,
|
|
3326
3393
|
) => {
|
|
@@ -3334,12 +3401,14 @@ registerTool(
|
|
|
3334
3401
|
language?: string;
|
|
3335
3402
|
product_subject?: string;
|
|
3336
3403
|
product_prompt?: string;
|
|
3404
|
+
restart?: boolean;
|
|
3337
3405
|
} = {};
|
|
3338
3406
|
if (args.language !== undefined) overrides.language = args.language;
|
|
3339
3407
|
if (args.product_subject !== undefined)
|
|
3340
3408
|
overrides.product_subject = args.product_subject;
|
|
3341
3409
|
if (args.product_prompt !== undefined)
|
|
3342
3410
|
overrides.product_prompt = args.product_prompt;
|
|
3411
|
+
if (args.restart) overrides.restart = true;
|
|
3343
3412
|
|
|
3344
3413
|
const quote = await editorAutopilotQuote(client, slug, overrides);
|
|
3345
3414
|
const overCap =
|
|
@@ -4021,7 +4090,15 @@ registerTool(
|
|
|
4021
4090
|
args.prompt !== undefined ? { prompt: args.prompt } : undefined,
|
|
4022
4091
|
undefined,
|
|
4023
4092
|
);
|
|
4024
|
-
return ok(
|
|
4093
|
+
return ok({
|
|
4094
|
+
...asRecord(asRecord(res).data ?? res),
|
|
4095
|
+
charge_receipt: await editorChargeReceipt(
|
|
4096
|
+
client,
|
|
4097
|
+
args.slug,
|
|
4098
|
+
"scene_regeneration",
|
|
4099
|
+
4,
|
|
4100
|
+
),
|
|
4101
|
+
});
|
|
4025
4102
|
},
|
|
4026
4103
|
),
|
|
4027
4104
|
);
|
|
@@ -4230,7 +4307,15 @@ registerTool(
|
|
|
4230
4307
|
{ voice_id: args.voice_id },
|
|
4231
4308
|
await voiceKey(client, args.slug, args.voice_id),
|
|
4232
4309
|
);
|
|
4233
|
-
return ok(
|
|
4310
|
+
return ok({
|
|
4311
|
+
...asRecord(asRecord(res).data ?? res),
|
|
4312
|
+
charge_receipt: await editorChargeReceipt(
|
|
4313
|
+
client,
|
|
4314
|
+
args.slug,
|
|
4315
|
+
"voice_generation",
|
|
4316
|
+
3,
|
|
4317
|
+
),
|
|
4318
|
+
});
|
|
4234
4319
|
}),
|
|
4235
4320
|
);
|
|
4236
4321
|
|
|
@@ -4278,7 +4363,15 @@ registerTool(
|
|
|
4278
4363
|
body,
|
|
4279
4364
|
undefined,
|
|
4280
4365
|
);
|
|
4281
|
-
return ok(
|
|
4366
|
+
return ok({
|
|
4367
|
+
...asRecord(asRecord(res).data ?? res),
|
|
4368
|
+
charge_receipt: await editorChargeReceipt(
|
|
4369
|
+
client,
|
|
4370
|
+
args.slug,
|
|
4371
|
+
"music_generation",
|
|
4372
|
+
5,
|
|
4373
|
+
),
|
|
4374
|
+
});
|
|
4282
4375
|
},
|
|
4283
4376
|
),
|
|
4284
4377
|
);
|
|
@@ -4289,7 +4382,8 @@ registerTool(
|
|
|
4289
4382
|
title: "Render the final editor video (0 credits)",
|
|
4290
4383
|
description:
|
|
4291
4384
|
"Renders the final MP4 from the generated segments + voice + music. Costs 0 credits (any still-ungenerated " +
|
|
4292
|
-
"segments are auto-charged at generation cost first).
|
|
4385
|
+
"segments are auto-charged at generation cost first). Render NEVER generates or charges for voice/music; a new " +
|
|
4386
|
+
"music charge comes only from generate_music or an already-running autopilot child job. Re-rendering reflects the current segments/voice/music (it does not replay a prior render). Returns the latest_render; " +
|
|
4293
4387
|
'pair with wait_for_completion({ slug, kind: "editor" }) then download_result. ' +
|
|
4294
4388
|
"STALENESS: editing the scenario or narration AFTER generating voice/music marks those audio tracks stale, and render then 422s with editor_narration_stale / editor_voice_stale / editor_music_stale — regenerate the named track (generate_narration → generate_voice, and/or generate_music) before rendering.",
|
|
4295
4389
|
inputSchema: { slug: z.string().describe("Editor project slug") },
|
|
@@ -4979,11 +5073,11 @@ registerTool(
|
|
|
4979
5073
|
),
|
|
4980
5074
|
);
|
|
4981
5075
|
|
|
4982
|
-
// ── Shorts: branding images (product + end-card poster)
|
|
5076
|
+
// ── Shorts: branding images (product + logo + end-card poster) ───────────────
|
|
4983
5077
|
//
|
|
4984
5078
|
// The editor branding tools above hit /editor/...; these hit /shorts/... A short
|
|
4985
|
-
//
|
|
4986
|
-
//
|
|
5079
|
+
// Product images are woven into AI footage; logos are deterministic Forge
|
|
5080
|
+
// overlays; posters are closing cards. All are 0 credits.
|
|
4987
5081
|
|
|
4988
5082
|
registerTool(
|
|
4989
5083
|
"set_short_product",
|
|
@@ -5032,6 +5126,57 @@ registerTool(
|
|
|
5032
5126
|
),
|
|
5033
5127
|
);
|
|
5034
5128
|
|
|
5129
|
+
registerTool(
|
|
5130
|
+
"set_short_logo",
|
|
5131
|
+
{
|
|
5132
|
+
title: "Set a Short brand logo overlay (0 credits)",
|
|
5133
|
+
description:
|
|
5134
|
+
"Uploads an exact brand logo for deterministic compositing over a Short. The image is never sent to the " +
|
|
5135
|
+
"AI video provider, so lettering is preserved. PNG/JPEG, ≤5 MiB, 0 credits. Choose a corner and whether " +
|
|
5136
|
+
"it appears throughout the footage or only on the end card.",
|
|
5137
|
+
inputSchema: {
|
|
5138
|
+
slug: z.string().describe("Short slug"),
|
|
5139
|
+
file_path: z.string().describe("Local logo image (.png/.jpg/.jpeg)"),
|
|
5140
|
+
position: z
|
|
5141
|
+
.enum(["top-left", "top-right", "bottom-left", "bottom-right"])
|
|
5142
|
+
.optional()
|
|
5143
|
+
.describe("Logo corner (default top-right)"),
|
|
5144
|
+
treatment: z
|
|
5145
|
+
.enum(["throughout", "end_card", "none"])
|
|
5146
|
+
.optional()
|
|
5147
|
+
.describe("When the logo appears (default throughout)"),
|
|
5148
|
+
},
|
|
5149
|
+
annotations: { title: "Set Short logo", ...WRITE, idempotentHint: false },
|
|
5150
|
+
},
|
|
5151
|
+
tool(
|
|
5152
|
+
async (
|
|
5153
|
+
args: {
|
|
5154
|
+
slug: string;
|
|
5155
|
+
file_path: string;
|
|
5156
|
+
position?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
5157
|
+
treatment?: "throughout" | "end_card" | "none";
|
|
5158
|
+
},
|
|
5159
|
+
client,
|
|
5160
|
+
) => {
|
|
5161
|
+
const { s3_key } = await uploadImageFile(
|
|
5162
|
+
client,
|
|
5163
|
+
`/shorts/${args.slug}/logo/presign`,
|
|
5164
|
+
args.file_path,
|
|
5165
|
+
MAX_SHORT_LOGO_BYTES,
|
|
5166
|
+
);
|
|
5167
|
+
const res = await client.post<{ data: unknown }>(
|
|
5168
|
+
`/shorts/${args.slug}/logo/confirm`,
|
|
5169
|
+
{
|
|
5170
|
+
s3_key,
|
|
5171
|
+
position: args.position ?? "top-right",
|
|
5172
|
+
treatment: args.treatment ?? "throughout",
|
|
5173
|
+
},
|
|
5174
|
+
);
|
|
5175
|
+
return ok(asRecord(res).data ?? res);
|
|
5176
|
+
},
|
|
5177
|
+
),
|
|
5178
|
+
);
|
|
5179
|
+
|
|
5035
5180
|
registerTool(
|
|
5036
5181
|
"set_short_poster",
|
|
5037
5182
|
{
|
|
@@ -5041,7 +5186,7 @@ registerTool(
|
|
|
5041
5186
|
"render from 12s to 14s). Accepted: " +
|
|
5042
5187
|
IMAGE_EXTS.map((e) => `.${e}`).join(", ") +
|
|
5043
5188
|
" (≤20 MiB = 20,971,520 bytes), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. There is one poster per short; calling " +
|
|
5044
|
-
"again replaces it.
|
|
5189
|
+
"again replaces it. Use set_short_logo for an exact, non-generative brand overlay.",
|
|
5045
5190
|
inputSchema: {
|
|
5046
5191
|
slug: z.string().describe("Short slug (from create_short)"),
|
|
5047
5192
|
file_path: z.string().describe("Local poster image (.jpg/.jpeg/.png)"),
|
package/src/output-schemas.ts
CHANGED
|
@@ -65,8 +65,32 @@ export const getStatusOutput = z
|
|
|
65
65
|
stale: z.boolean().optional(),
|
|
66
66
|
active_segment_ids: z.array(z.union([z.number(), z.string()])).optional(),
|
|
67
67
|
stale_segment_ids: z.array(z.union([z.number(), z.string()])).optional(),
|
|
68
|
-
// Short only, additive (normalizeStatus forwards
|
|
69
|
-
//
|
|
68
|
+
// Short only, additive (normalizeStatus forwards each gated on its typeof,
|
|
69
|
+
// so the precise type can never reject a payload).
|
|
70
|
+
failed_stage: z
|
|
71
|
+
.string()
|
|
72
|
+
.optional()
|
|
73
|
+
.describe(
|
|
74
|
+
"Short only: exact API failure phase behind a terminal failure.",
|
|
75
|
+
),
|
|
76
|
+
failure_code: z
|
|
77
|
+
.string()
|
|
78
|
+
.optional()
|
|
79
|
+
.describe("Short only: stable machine-readable failure classification."),
|
|
80
|
+
failure_details: z
|
|
81
|
+
.record(z.string(), z.unknown())
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("Short only: structured provider/QC details for recovery."),
|
|
84
|
+
failure_source: z
|
|
85
|
+
.string()
|
|
86
|
+
.optional()
|
|
87
|
+
.describe(
|
|
88
|
+
"Short only: worker/recovery layer that finalized the failure.",
|
|
89
|
+
),
|
|
90
|
+
failure_attempt: z
|
|
91
|
+
.number()
|
|
92
|
+
.optional()
|
|
93
|
+
.describe("Short only: paid generation attempt number that failed."),
|
|
70
94
|
latest_render_free: z.boolean().optional(),
|
|
71
95
|
last_free_rerender_failed: z.boolean().optional(),
|
|
72
96
|
})
|
|
@@ -85,6 +109,31 @@ export const waitForCompletionOutput = z
|
|
|
85
109
|
stale: z.boolean().optional(),
|
|
86
110
|
active_segment_ids: z.array(z.union([z.number(), z.string()])).optional(),
|
|
87
111
|
stale_segment_ids: z.array(z.union([z.number(), z.string()])).optional(),
|
|
112
|
+
// Short only, additive — mirrors getStatusOutput (see there).
|
|
113
|
+
failed_stage: z
|
|
114
|
+
.string()
|
|
115
|
+
.optional()
|
|
116
|
+
.describe(
|
|
117
|
+
"Short only: exact API failure phase behind a terminal failure.",
|
|
118
|
+
),
|
|
119
|
+
failure_code: z
|
|
120
|
+
.string()
|
|
121
|
+
.optional()
|
|
122
|
+
.describe("Short only: stable machine-readable failure classification."),
|
|
123
|
+
failure_details: z
|
|
124
|
+
.record(z.string(), z.unknown())
|
|
125
|
+
.optional()
|
|
126
|
+
.describe("Short only: structured provider/QC details for recovery."),
|
|
127
|
+
failure_source: z
|
|
128
|
+
.string()
|
|
129
|
+
.optional()
|
|
130
|
+
.describe(
|
|
131
|
+
"Short only: worker/recovery layer that finalized the failure.",
|
|
132
|
+
),
|
|
133
|
+
failure_attempt: z
|
|
134
|
+
.number()
|
|
135
|
+
.optional()
|
|
136
|
+
.describe("Short only: paid generation attempt number that failed."),
|
|
88
137
|
latest_render_free: z.boolean().optional(),
|
|
89
138
|
last_free_rerender_failed: z.boolean().optional(),
|
|
90
139
|
saved_to: z.union([z.string(), z.null()]).optional(),
|
package/src/uploads.ts
CHANGED
|
@@ -64,6 +64,7 @@ const MAX_VIDEO_BYTES = 500_000_000; // EditorUpload.@max_upload_bytes
|
|
|
64
64
|
const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
65
65
|
export const MAX_PRODUCT_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
66
66
|
export const MAX_LOGO_BYTES = 1 * 1024 * 1024; // Editor.Logo.@max_logo_bytes
|
|
67
|
+
export const MAX_SHORT_LOGO_BYTES = 5 * 1024 * 1024;
|
|
67
68
|
export const MAX_CLOSING_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
68
69
|
const MULTIPART_THRESHOLD = 50 * 1024 * 1024; // @multipart_min_size — at/above this, use multipart
|
|
69
70
|
const MAX_CATALOG_VIDEO_DURATION_SECONDS = 60; // CatalogAsset video validator
|