@hubfluencer/mcp 0.14.0 → 0.16.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 +16 -8
- package/dist/index.js +122 -32
- package/package.json +1 -1
- package/src/core.ts +42 -1
- package/src/index.ts +148 -27
- package/src/polling.ts +51 -0
package/README.md
CHANGED
|
@@ -85,8 +85,8 @@ instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_ass
|
|
|
85
85
|
| `cancel_short_generation` | Stop the caller-observed paid short attempt, fence late workers from publishing, preserve the draft, and restore its 15 credits idempotently. Requires `generation_attempt` from `get_status`; stale commands cannot cancel a newer run. | **full refund** |
|
|
86
86
|
| `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** |
|
|
87
87
|
| `generate_short_text` | AI-draft editable headline / subheadline / caption beats for a short. | 1 assist |
|
|
88
|
-
| `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. Write every required person, product, object, and continuity decision directly in the prompt. | **0** until capped launch |
|
|
89
|
-
| `start_autopilot` | Quote autopilot on an **existing** editor draft when called with only `slug`;
|
|
88
|
+
| `create_editor_ad` | Create an editor project, attach optional local product/logo/closing assets, and return the live autopilot quote. Pass `segments_count` (3–10) as the exact structural target. Omit `max_credits` to stop free; provide an approved cap to launch. Write every required person, product, object, and continuity decision directly in the prompt. | **0** until capped launch |
|
|
89
|
+
| `start_autopilot` | Quote autopilot on an **existing** editor draft when called with only `slug`; `segments_count` (3–10) overrides an older brief and is reflected in the matching quote. 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 |
|
|
90
90
|
| `retry_editor_batch` | Recover `batch_retry_required` by atomically re-queueing the failed scene and resuming its prepaid batch. If the parent Autopilot run is terminal, quote/relaunch it separately afterward. | **0 additional** |
|
|
91
91
|
| `cancel_editor_batch` | Cancel an editor batch only while it is `preparing` or `paused_for_retry` (awaiting-previews/running batches are worker-owned and not cancellable). Preparing cancellation releases the uncharged preflight; paused cancellation is destructive — prepaid credits are not refunded. | **0 additional** |
|
|
92
92
|
| `cancel_autopilot` | Stop a **running** Autopilot run: flips `autopilot_status` to `cancelled` so the chain (scenario → scenes → narration → voice → music → render) advances no further. Charged scenes stay charged and all completed work is kept; a step already enqueued may still finish. Idempotent — a no-op when Autopilot isn't running. (Use `cancel_editor_batch` for a non-Autopilot prepaid batch, `cancel_segment_generation` for one scene.) | **0 additional** |
|
|
@@ -111,9 +111,9 @@ instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_ass
|
|
|
111
111
|
| `cancel_segment_generation` | Stop the caller-observed individually generated scene, fence its provider job from publishing, and restore any charged credits idempotently. Requires that scene's `generation_claim_token` from `get_editor`; stale commands cannot cancel a newer attempt. Batch/Autopilot scenes use their workflow-level controls. | **full refund** |
|
|
112
112
|
| `regenerate_segment` | Re-roll a **completed** scene as a new version (optional new `prompt`). Mid-timeline: the next scene's transition can degrade because it was seeded from the old version's last frame. | **4** |
|
|
113
113
|
| `generate_all_segments` | Render every not-yet-completed scene in order (continuity preserved); capped ~280s → re-run to continue. | **5 / segment** |
|
|
114
|
-
| `set_narration_script` / `generate_narration` | Write
|
|
114
|
+
| `set_narration_script` / `generate_narration` | Write the exact words to read aloud (free)—never pass a production prompt, camera directions, or a creative brief as `script`—**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
|
|
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**. The corner is preferred and Forge moves it away from text/CTA/chips. Omit treatment to auto-select: transparent marks use `throughout`, opaque square lockups use `end_card`; explicit `throughout`/`end_card`/`none` still wins. 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. Never copy a creative brief into `script`; prefer `generate_narration` or Autopilot unless the user supplied final voice-over copy.
|
|
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. Omit treatment so transparent marks default to `throughout` and opaque square lockups default to `end_card`. 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
|
@@ -29578,6 +29578,7 @@ function normalizeStatus(kind, slug, data) {
|
|
|
29578
29578
|
const renderStale = latest.is_stale === true;
|
|
29579
29579
|
const renderFresh = latest.is_stale === false;
|
|
29580
29580
|
const stale = renderStale || staleSegments.length > 0 || audioStale;
|
|
29581
|
+
const autopilotContractIncomplete = autopilot === "completed" && d.autopilot_contract_satisfied === false && !stale;
|
|
29581
29582
|
const renderActive = renderStatus === "pending" || renderStatus === "rendering" || renderStatus === "processing";
|
|
29582
29583
|
const scenarioActive = d.scenario_status === "generating" || d.scenario_apply_status === "pending" || d.scenario_apply_status === "applying";
|
|
29583
29584
|
const narrationActive = d.narration_status === "generating";
|
|
@@ -29617,7 +29618,7 @@ function normalizeStatus(kind, slug, data) {
|
|
|
29617
29618
|
const batchRecoveryActive = autopilotFailed && batchActive;
|
|
29618
29619
|
const childFailureActionable = !batchRecoveryActive && (!autopilotActive || !childWorkActive && !autopilotResumeGraceActive);
|
|
29619
29620
|
const workActive = autopilotActive || childWorkActive;
|
|
29620
|
-
const deliveredArtifactSupersedesOrchestrationFailure = renderStatus === "completed" && Boolean(videoUrl) && renderFresh && !stale && !workActive && failedSegments.length === 0 && !audioFailed;
|
|
29621
|
+
const deliveredArtifactSupersedesOrchestrationFailure = renderStatus === "completed" && Boolean(videoUrl) && renderFresh && !stale && !workActive && failedSegments.length === 0 && !audioFailed && !autopilotContractIncomplete;
|
|
29621
29622
|
const ready = deliveredArtifactSupersedesOrchestrationFailure;
|
|
29622
29623
|
const parentFailureActionable = autopilotFailed && !batchRecoveryActive;
|
|
29623
29624
|
const renderFailed = renderStatus === "failed" && childFailureActionable;
|
|
@@ -29648,6 +29649,8 @@ function normalizeStatus(kind, slug, data) {
|
|
|
29648
29649
|
stage = "audio_failed";
|
|
29649
29650
|
else if (batchInsufficientCredits)
|
|
29650
29651
|
stage = "insufficient_credits";
|
|
29652
|
+
else if (autopilotContractIncomplete)
|
|
29653
|
+
stage = "contract_incomplete";
|
|
29651
29654
|
else if (activeSegments.length > 0)
|
|
29652
29655
|
stage = "segment_processing";
|
|
29653
29656
|
else if (batchActive)
|
|
@@ -29673,10 +29676,13 @@ function normalizeStatus(kind, slug, data) {
|
|
|
29673
29676
|
terminal,
|
|
29674
29677
|
ready,
|
|
29675
29678
|
video_url: videoUrl,
|
|
29676
|
-
error: ready ? null : batchRetryRequired ? "Batch generation is paused after a failed scene. Call retry_editor_batch or cancel_editor_batch before continuing." : (parentFailureActionable ? d.autopilot_error_message : null) ?? actionableChildError ?? (batchInsufficientCredits ? "Batch generation paused: not enough credits to finish. Top up or reduce scope, then invoke the generation/render path again; retry_editor_batch only applies to paused_for_retry." : null),
|
|
29679
|
+
error: ready ? null : batchRetryRequired ? "Batch generation is paused after a failed scene. Call retry_editor_batch or cancel_editor_batch before continuing." : (parentFailureActionable ? d.autopilot_error_message : null) ?? actionableChildError ?? (batchInsufficientCredits ? "Batch generation paused: not enough credits to finish. Top up or reduce scope, then invoke the generation/render path again; retry_editor_batch only applies to paused_for_retry." : null) ?? (autopilotContractIncomplete ? "Autopilot completed without every quoted deliverable. Resume Autopilot to repair the missing component." : null),
|
|
29677
29680
|
stale,
|
|
29678
29681
|
active_segment_ids: activeSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string"),
|
|
29679
|
-
stale_segment_ids: staleSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string")
|
|
29682
|
+
stale_segment_ids: staleSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string"),
|
|
29683
|
+
...typeof d.autopilot_contract_satisfied === "boolean" ? {
|
|
29684
|
+
autopilot_contract_satisfied: d.autopilot_contract_satisfied
|
|
29685
|
+
} : {}
|
|
29680
29686
|
};
|
|
29681
29687
|
}
|
|
29682
29688
|
function startStateDiscriminator(kind, data) {
|
|
@@ -29700,16 +29706,16 @@ function idemKey(...parts) {
|
|
|
29700
29706
|
return createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 32);
|
|
29701
29707
|
}
|
|
29702
29708
|
function makeVideoEditorCreateKey(a) {
|
|
29703
|
-
return idemKey("make-editor", a.prompt, a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.aspect ?? "", a.voice_id ?? "", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "");
|
|
29709
|
+
return idemKey("make-editor", a.prompt, a.segments_count === undefined ? "" : String(a.segments_count), a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.aspect ?? "", a.voice_id ?? "", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "");
|
|
29704
29710
|
}
|
|
29705
29711
|
function makeVideoAutopilotKey(slug, a, startState) {
|
|
29706
|
-
return idemKey("autopilot", slug, a.prompt, a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.aspect ?? "", a.voice_id ?? "", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "", startState);
|
|
29712
|
+
return idemKey("autopilot", slug, a.prompt, a.segments_count === undefined ? "" : String(a.segments_count), a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.aspect ?? "", a.voice_id ?? "", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "", startState);
|
|
29707
29713
|
}
|
|
29708
29714
|
function editorAdCreateKey(a) {
|
|
29709
|
-
return idemKey("create-editor", a.product_prompt ?? "", a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "", a.voice_id ?? "", a.export_aspect_ratio ?? "", a.product_image_path ?? "", a.product_image_identity ?? "", a.product_description ?? "", a.closing_image_path ?? "", a.closing_image_identity ?? "", a.logo_path ?? "", a.logo_identity ?? "", a.logo_treatment ?? "", a.logo_position ?? "", a.logo_duration_seconds === undefined ? "" : String(a.logo_duration_seconds));
|
|
29715
|
+
return idemKey("create-editor", a.product_prompt ?? "", a.segments_count === undefined ? "" : String(a.segments_count), a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "", a.voice_id ?? "", a.export_aspect_ratio ?? "", a.product_image_path ?? "", a.product_image_identity ?? "", a.product_description ?? "", a.closing_image_path ?? "", a.closing_image_identity ?? "", a.logo_path ?? "", a.logo_identity ?? "", a.logo_treatment ?? "", a.logo_position ?? "", a.logo_duration_seconds === undefined ? "" : String(a.logo_duration_seconds));
|
|
29710
29716
|
}
|
|
29711
29717
|
function editorAdAutopilotKey(slug, a) {
|
|
29712
|
-
return idemKey("autopilot", slug, a.product_prompt ?? "", a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "", a.voice_id ?? "", a.export_aspect_ratio ?? "", a.product_image_path ?? "", a.product_description ?? "", a.closing_image_path ?? "", a.logo_path ?? "", a.logo_treatment ?? "", a.logo_position ?? "", a.logo_duration_seconds === undefined ? "" : String(a.logo_duration_seconds), a.max_credits === undefined ? "" : String(a.max_credits));
|
|
29718
|
+
return idemKey("autopilot", slug, a.product_prompt ?? "", a.segments_count === undefined ? "" : String(a.segments_count), a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "", a.voice_id ?? "", a.export_aspect_ratio ?? "", a.product_image_path ?? "", a.product_description ?? "", a.closing_image_path ?? "", a.logo_path ?? "", a.logo_treatment ?? "", a.logo_position ?? "", a.logo_duration_seconds === undefined ? "" : String(a.logo_duration_seconds), a.max_credits === undefined ? "" : String(a.max_credits));
|
|
29713
29719
|
}
|
|
29714
29720
|
function sliderCreateKey(a) {
|
|
29715
29721
|
return idemKey("create-slider", a.prompt, a.mode ?? "", a.template ?? "", a.slide_count === undefined ? "" : String(a.slide_count), a.aspect_ratio ?? "", a.accent_color ?? "", a.text_position ?? "");
|
|
@@ -30266,7 +30272,7 @@ function readStoredCredentials() {
|
|
|
30266
30272
|
// package.json
|
|
30267
30273
|
var package_default = {
|
|
30268
30274
|
name: "@hubfluencer/mcp",
|
|
30269
|
-
version: "0.
|
|
30275
|
+
version: "0.16.0",
|
|
30270
30276
|
description: "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
|
|
30271
30277
|
license: "MIT",
|
|
30272
30278
|
author: "Monocursive <contact@monocursive.com>",
|
|
@@ -30992,6 +30998,38 @@ function assetSubscriptionErrorText(status, code, message) {
|
|
|
30992
30998
|
return `${message} The reusable asset catalog requires an ACTIVE SUBSCRIPTION; ` + "this is a billing gate, not a scope or credit issue, so retrying won't help — " + "subscribe in the app first. (Generating videos/carousels does not require this.)";
|
|
30993
30999
|
}
|
|
30994
31000
|
|
|
31001
|
+
// src/polling.ts
|
|
31002
|
+
async function pollWithFinalReconciliation({
|
|
31003
|
+
read,
|
|
31004
|
+
isTerminal: isTerminal2,
|
|
31005
|
+
budgetMs,
|
|
31006
|
+
intervalMs,
|
|
31007
|
+
onPoll,
|
|
31008
|
+
now = Date.now,
|
|
31009
|
+
sleep: sleep3 = (milliseconds) => new Promise((resolve2) => setTimeout(resolve2, milliseconds))
|
|
31010
|
+
}) {
|
|
31011
|
+
const deadline = now() + budgetMs;
|
|
31012
|
+
let value = await read(deadline);
|
|
31013
|
+
let pollNumber = 0;
|
|
31014
|
+
while (!isTerminal2(value)) {
|
|
31015
|
+
const remainingMs = deadline - now();
|
|
31016
|
+
if (remainingMs <= 0)
|
|
31017
|
+
break;
|
|
31018
|
+
await onPoll?.(value, ++pollNumber);
|
|
31019
|
+
await sleep3(Math.min(intervalMs, remainingMs));
|
|
31020
|
+
if (now() >= deadline)
|
|
31021
|
+
break;
|
|
31022
|
+
value = await read(deadline);
|
|
31023
|
+
}
|
|
31024
|
+
if (!isTerminal2(value)) {
|
|
31025
|
+
const finalReadDeadline = now() + Math.min(5000, Math.max(1000, intervalMs));
|
|
31026
|
+
try {
|
|
31027
|
+
value = await read(finalReadDeadline);
|
|
31028
|
+
} catch {}
|
|
31029
|
+
}
|
|
31030
|
+
return value;
|
|
31031
|
+
}
|
|
31032
|
+
|
|
30995
31033
|
// src/series-calendar.ts
|
|
30996
31034
|
var SERIES_FIELDS = [
|
|
30997
31035
|
"brand_profile_id",
|
|
@@ -31590,6 +31628,8 @@ async function editorAutopilotQuote(client, slug, overrides = {}) {
|
|
|
31590
31628
|
params.set("product_subject", overrides.product_subject);
|
|
31591
31629
|
if (overrides.product_prompt !== undefined)
|
|
31592
31630
|
params.set("product_prompt", overrides.product_prompt);
|
|
31631
|
+
if (overrides.segments_count !== undefined)
|
|
31632
|
+
params.set("segments_count", String(overrides.segments_count));
|
|
31593
31633
|
if (overrides.restart)
|
|
31594
31634
|
params.set("restart", "true");
|
|
31595
31635
|
const query = params.toString();
|
|
@@ -31597,9 +31637,13 @@ async function editorAutopilotQuote(client, slug, overrides = {}) {
|
|
|
31597
31637
|
const cost = asRecord(asRecord(response).data);
|
|
31598
31638
|
const estimated_credits = typeof cost.total === "number" ? cost.total : null;
|
|
31599
31639
|
const available_credits = typeof cost.available_credits === "number" ? cost.available_credits : null;
|
|
31640
|
+
const scene_count = typeof cost.scene_count === "number" ? cost.scene_count : null;
|
|
31641
|
+
const target_duration_seconds = typeof cost.target_duration_seconds === "number" ? cost.target_duration_seconds : null;
|
|
31600
31642
|
return {
|
|
31601
31643
|
estimated_credits,
|
|
31602
31644
|
available_credits,
|
|
31645
|
+
scene_count,
|
|
31646
|
+
target_duration_seconds,
|
|
31603
31647
|
affordable: estimated_credits !== null && available_credits !== null && available_credits >= estimated_credits
|
|
31604
31648
|
};
|
|
31605
31649
|
} catch (error2) {
|
|
@@ -31610,6 +31654,8 @@ async function editorAutopilotQuote(client, slug, overrides = {}) {
|
|
|
31610
31654
|
return {
|
|
31611
31655
|
estimated_credits: null,
|
|
31612
31656
|
available_credits: null,
|
|
31657
|
+
scene_count: null,
|
|
31658
|
+
target_duration_seconds: null,
|
|
31613
31659
|
affordable: false
|
|
31614
31660
|
};
|
|
31615
31661
|
}
|
|
@@ -31885,15 +31931,13 @@ AUTH: set HUBFLUENCER_API_TOKEN (Settings → Access tokens) or run \`npx -y @hu
|
|
|
31885
31931
|
var server = new McpServer({ name: "hubfluencer", version: VERSION }, { instructions: SERVER_INSTRUCTIONS });
|
|
31886
31932
|
var registerTool = server.registerTool.bind(server);
|
|
31887
31933
|
async function pollToTerminal(client, kind, slug, extra, budgetMs, intervalMs) {
|
|
31888
|
-
|
|
31889
|
-
|
|
31890
|
-
|
|
31891
|
-
|
|
31892
|
-
|
|
31893
|
-
|
|
31894
|
-
|
|
31895
|
-
}
|
|
31896
|
-
return status;
|
|
31934
|
+
return pollWithFinalReconciliation({
|
|
31935
|
+
read: (deadline) => fetchStatus(client, kind, slug, deadline),
|
|
31936
|
+
isTerminal: (status) => status.terminal,
|
|
31937
|
+
budgetMs,
|
|
31938
|
+
intervalMs,
|
|
31939
|
+
onPoll: (status, pollNumber) => reportProgress(extra, pollNumber, `stage: ${status.stage}`)
|
|
31940
|
+
});
|
|
31897
31941
|
}
|
|
31898
31942
|
async function pollSegmentToTerminal(client, slug, segmentId, extra, deadline, intervalMs) {
|
|
31899
31943
|
const sid = String(segmentId);
|
|
@@ -31928,6 +31972,7 @@ registerTool("make_video", {
|
|
|
31928
31972
|
kind: exports_external.string().optional().describe("'short' (fast, 1 clip), 'editor' (multi-scene), or 'auto' (default — inferred)"),
|
|
31929
31973
|
product_subject: exports_external.string().max(300).optional().describe('EDITOR only: the concrete product / main subject the video must be about (e.g. "Bordeaux red ' + 'wine, dark green bottle"). Hard-grounds the AI so it cannot invent an unrelated product. ' + "Recommended for ads."),
|
|
31930
31974
|
project_intent: exports_external.enum(["social_ad", "creative_story"]).optional().describe("EDITOR only: social_ad for promotional content (product focus + CTA) or creative_story (default)."),
|
|
31975
|
+
segments_count: exports_external.number().int().min(3).max(10).optional().describe("EDITOR only: exact number of 8-second AI scenes (3–10). Use 3 for a concise ~24s ad."),
|
|
31931
31976
|
language: exports_external.enum(LANGUAGES).optional().describe('Language (default "en"). Bare ISO-639-1 code only — no region/script subtag ' + '(use "pt", never "pt-BR"/"en-US"). Drives scenario, narration, voice, and captions.'),
|
|
31932
31977
|
aspect: exports_external.string().optional().describe("Aspect ratio for editor: 9:16 (default), 16:9, or 1:1"),
|
|
31933
31978
|
voice_id: exports_external.string().optional().describe("Preferred narration voice id for editor ads (see list_voices); shorts have no voiceover"),
|
|
@@ -31979,6 +32024,9 @@ registerTool("make_video", {
|
|
|
31979
32024
|
return fail(kindErr);
|
|
31980
32025
|
const requestedKind = args.kind === "short" || args.kind === "editor" ? args.kind : undefined;
|
|
31981
32026
|
const kind = requestedKind ?? inferKind(args.prompt);
|
|
32027
|
+
if (kind !== "editor" && args.segments_count !== undefined) {
|
|
32028
|
+
return fail("segments_count is only supported for editor videos.");
|
|
32029
|
+
}
|
|
31982
32030
|
const waitSeconds = Math.min(280, Math.max(10, args.max_wait_seconds ?? 240));
|
|
31983
32031
|
const budgetMs = waitSeconds * 1000;
|
|
31984
32032
|
if (args.star_rating !== undefined && (args.star_rating < 0 || args.star_rating > 5)) {
|
|
@@ -32031,6 +32079,7 @@ registerTool("make_video", {
|
|
|
32031
32079
|
const created = await client.post("/editor", {
|
|
32032
32080
|
language: args.language ?? "en",
|
|
32033
32081
|
product_prompt: args.prompt,
|
|
32082
|
+
segments_count: args.segments_count,
|
|
32034
32083
|
product_subject: args.product_subject,
|
|
32035
32084
|
project_intent: args.project_intent,
|
|
32036
32085
|
export_aspect_ratio: args.aspect,
|
|
@@ -32140,7 +32189,7 @@ registerTool("get_credits", {
|
|
|
32140
32189
|
}));
|
|
32141
32190
|
registerTool("list_voices", {
|
|
32142
32191
|
title: "List voices",
|
|
32143
|
-
description: "Lists available narration voices
|
|
32192
|
+
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
32193
|
inputSchema: {},
|
|
32145
32194
|
annotations: { title: "List voices", ...RO }
|
|
32146
32195
|
}, tool(async (_args, client) => {
|
|
@@ -32337,7 +32386,7 @@ registerTool("update_short", {
|
|
|
32337
32386
|
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
32387
|
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
32388
|
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("
|
|
32389
|
+
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
32390
|
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
32391
|
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
32392
|
},
|
|
@@ -32553,19 +32602,21 @@ registerTool("create_tracking_video", {
|
|
|
32553
32602
|
}));
|
|
32554
32603
|
registerTool("generate_short", {
|
|
32555
32604
|
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).",
|
|
32605
|
+
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
32606
|
inputSchema: {
|
|
32558
32607
|
slug: exports_external.string().describe("Short slug from create_short"),
|
|
32559
32608
|
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
32609
|
},
|
|
32561
32610
|
annotations: { title: "Generate short", ...WRITE }
|
|
32562
32611
|
}, tool(async (args, client) => {
|
|
32612
|
+
const submit = () => client.post(`/shorts/${args.slug}/generate`, args.skip_auto_text ? { skip_auto_text: true } : undefined);
|
|
32563
32613
|
try {
|
|
32564
|
-
const res = await
|
|
32614
|
+
const res = await submit();
|
|
32565
32615
|
const status = normalizeStatus("short", args.slug, asRecord(res).data);
|
|
32566
32616
|
return ok(status);
|
|
32567
32617
|
} catch (e) {
|
|
32568
|
-
|
|
32618
|
+
const statusCode = e.status;
|
|
32619
|
+
if (statusCode === 409) {
|
|
32569
32620
|
return ok({
|
|
32570
32621
|
kind: "short",
|
|
32571
32622
|
slug: args.slug,
|
|
@@ -32576,6 +32627,35 @@ registerTool("generate_short", {
|
|
|
32576
32627
|
error: null
|
|
32577
32628
|
});
|
|
32578
32629
|
}
|
|
32630
|
+
if (statusCode !== undefined && statusCode >= 500) {
|
|
32631
|
+
let reconciled;
|
|
32632
|
+
try {
|
|
32633
|
+
reconciled = await fetchStatus(client, "short", args.slug);
|
|
32634
|
+
} catch (_reconcileError) {
|
|
32635
|
+
throw e;
|
|
32636
|
+
}
|
|
32637
|
+
if (reconciled.stage === "rendering")
|
|
32638
|
+
return ok(reconciled);
|
|
32639
|
+
if (reconciled.stage !== "draft")
|
|
32640
|
+
throw e;
|
|
32641
|
+
try {
|
|
32642
|
+
const retried = await submit();
|
|
32643
|
+
return ok(normalizeStatus("short", args.slug, asRecord(retried).data));
|
|
32644
|
+
} catch (retryError) {
|
|
32645
|
+
if (retryError.status === 409) {
|
|
32646
|
+
return ok({
|
|
32647
|
+
kind: "short",
|
|
32648
|
+
slug: args.slug,
|
|
32649
|
+
stage: "processing",
|
|
32650
|
+
terminal: false,
|
|
32651
|
+
ready: false,
|
|
32652
|
+
video_url: null,
|
|
32653
|
+
error: null
|
|
32654
|
+
});
|
|
32655
|
+
}
|
|
32656
|
+
throw retryError;
|
|
32657
|
+
}
|
|
32658
|
+
}
|
|
32579
32659
|
throw e;
|
|
32580
32660
|
}
|
|
32581
32661
|
}));
|
|
@@ -32847,9 +32927,9 @@ registerTool("edit_slider_slide", {
|
|
|
32847
32927
|
}));
|
|
32848
32928
|
registerTool("create_editor_ad", {
|
|
32849
32929
|
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.)",
|
|
32930
|
+
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
32931
|
inputSchema: {
|
|
32852
|
-
product_prompt: exports_external.string().min(10).describe("
|
|
32932
|
+
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
32933
|
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
32934
|
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
32935
|
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."),
|
|
@@ -32858,6 +32938,7 @@ registerTool("create_editor_ad", {
|
|
|
32858
32938
|
theme: exports_external.enum(SCENARIO_THEMES).optional().describe('Optional visual theme / genre overlay (default "none", so scene prompts drive the look). One of: none, realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.'),
|
|
32859
32939
|
voice_id: exports_external.string().optional().describe("Preferred narration voice id (see list_voices); omit for the default voice"),
|
|
32860
32940
|
export_aspect_ratio: exports_external.enum(["9:16", "16:9", "1:1"]).optional().describe('Aspect ratio (default "9:16")'),
|
|
32941
|
+
segments_count: exports_external.number().int().min(3).max(10).optional().describe("Exact number of 8-second AI scenes (3–10). Use 3 for a concise ~24s ad; omit to retain the server default."),
|
|
32861
32942
|
product_image_path: exports_external.string().optional().describe("Local product image (.jpg/.jpeg/.png, ≤8 MiB) used as the product-identity reference for every generated Editor scene and linked as the exact closing image unless one was already authored."),
|
|
32862
32943
|
product_description: exports_external.string().max(500).optional().describe("Product description used to ground the scenario and narration when product_image_path is supplied."),
|
|
32863
32944
|
closing_image_path: exports_external.string().optional().describe("Local closing-card image (.jpg/.jpeg/.png, ≤20 MiB)."),
|
|
@@ -32902,6 +32983,7 @@ registerTool("create_editor_ad", {
|
|
|
32902
32983
|
const created = await client.post("/editor", {
|
|
32903
32984
|
language: args.language ?? "en",
|
|
32904
32985
|
product_prompt: args.product_prompt,
|
|
32986
|
+
segments_count: args.segments_count,
|
|
32905
32987
|
product_subject: args.product_subject,
|
|
32906
32988
|
project_intent: args.project_intent,
|
|
32907
32989
|
creative_format: args.creative_format,
|
|
@@ -32948,7 +33030,9 @@ registerTool("create_editor_ad", {
|
|
|
32948
33030
|
logoSettings.logo_duration_seconds = args.logo_duration_seconds;
|
|
32949
33031
|
if (args.logo_path && Object.keys(logoSettings).length > 0)
|
|
32950
33032
|
await client.patch(`/editor/${slug}/logo`, logoSettings);
|
|
32951
|
-
const quote = await editorAutopilotQuote(client, slug
|
|
33033
|
+
const quote = await editorAutopilotQuote(client, slug, {
|
|
33034
|
+
segments_count: args.segments_count
|
|
33035
|
+
});
|
|
32952
33036
|
const overCap = args.max_credits !== undefined && quote.estimated_credits !== null && quote.estimated_credits > args.max_credits;
|
|
32953
33037
|
if (args.max_credits === undefined || quote.estimated_credits === null || overCap || !quote.affordable) {
|
|
32954
33038
|
const note = args.max_credits === undefined ? `Free draft configured. Estimated ${quote.estimated_credits ?? "?"} credits (have ${quote.available_credits ?? "?"}); nothing charged. Relaunch with start_autopilot({ slug: "${slug}", max_credits: ${quote.estimated_credits ?? "<approved cap>"} }).` : quote.estimated_credits === null ? "Pricing is temporarily unavailable, so the paid pipeline was not started. Retry the quote later." : overCap ? `Estimated ${quote.estimated_credits} credits exceeds max_credits ${args.max_credits}; nothing charged.` : `Not enough credits (needs ${quote.estimated_credits}, have ${quote.available_credits ?? "?"}); nothing charged.`;
|
|
@@ -32961,7 +33045,10 @@ registerTool("create_editor_ad", {
|
|
|
32961
33045
|
next: "start_autopilot"
|
|
32962
33046
|
});
|
|
32963
33047
|
}
|
|
32964
|
-
const started = await client.post(`/editor/${slug}/autopilot`, {
|
|
33048
|
+
const started = await client.post(`/editor/${slug}/autopilot`, {
|
|
33049
|
+
max_credits: args.max_credits,
|
|
33050
|
+
segments_count: args.segments_count
|
|
33051
|
+
}, editorAdAutopilotKey(slug, args));
|
|
32965
33052
|
const status = normalizeStatus("editor", slug, asRecord(started).data);
|
|
32966
33053
|
return ok({
|
|
32967
33054
|
slug,
|
|
@@ -32986,6 +33073,7 @@ registerTool("start_autopilot", {
|
|
|
32986
33073
|
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."),
|
|
32987
33074
|
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. Omit to keep the stored subject; send blank to clear it."),
|
|
32988
33075
|
product_prompt: exports_external.string().optional().describe("Optional freeform brief to set/replace before launch. Omit to keep the stored brief; send blank to clear it."),
|
|
33076
|
+
segments_count: exports_external.number().int().min(3).max(10).optional().describe("Optional exact number of 8-second AI scenes (3–10). Omit to keep the stored structural target."),
|
|
32989
33077
|
max_credits: exports_external.number().int().nonnegative().optional().describe("Explicit spend cap. Omit to quote only; no credits are charged."),
|
|
32990
33078
|
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.")
|
|
32991
33079
|
},
|
|
@@ -33006,6 +33094,8 @@ registerTool("start_autopilot", {
|
|
|
33006
33094
|
overrides.product_subject = args.product_subject;
|
|
33007
33095
|
if (args.product_prompt !== undefined)
|
|
33008
33096
|
overrides.product_prompt = args.product_prompt;
|
|
33097
|
+
if (args.segments_count !== undefined)
|
|
33098
|
+
overrides.segments_count = args.segments_count;
|
|
33009
33099
|
if (args.restart)
|
|
33010
33100
|
overrides.restart = true;
|
|
33011
33101
|
const quote = await editorAutopilotQuote(client, slug, overrides);
|
|
@@ -33628,7 +33718,7 @@ registerTool("generate_voice", {
|
|
|
33628
33718
|
}));
|
|
33629
33719
|
registerTool("generate_music", {
|
|
33630
33720
|
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.",
|
|
33721
|
+
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
33722
|
inputSchema: {
|
|
33633
33723
|
slug: exports_external.string().describe("Editor project slug"),
|
|
33634
33724
|
description: exports_external.string().max(1200).optional().describe("Music direction prompt (≤1200 chars, truncated if longer)"),
|
|
@@ -33984,12 +34074,12 @@ registerTool("set_short_product", {
|
|
|
33984
34074
|
}));
|
|
33985
34075
|
registerTool("set_short_logo", {
|
|
33986
34076
|
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.",
|
|
34077
|
+
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
34078
|
inputSchema: {
|
|
33989
34079
|
slug: exports_external.string().describe("Short slug"),
|
|
33990
34080
|
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("
|
|
33992
|
-
treatment: exports_external.enum(["throughout", "end_card", "none"]).optional().describe("When the logo appears
|
|
34081
|
+
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)"),
|
|
34082
|
+
treatment: exports_external.enum(["throughout", "end_card", "none"]).optional().describe("When the logo appears. Omit for automatic treatment: transparent wordmarks run throughout; opaque square artwork moves to the end card.")
|
|
33993
34083
|
},
|
|
33994
34084
|
annotations: { title: "Set Short logo", ...WRITE, idempotentHint: false }
|
|
33995
34085
|
}, tool(async (args, client) => {
|
|
@@ -33997,7 +34087,7 @@ registerTool("set_short_logo", {
|
|
|
33997
34087
|
const res = await client.post(`/shorts/${args.slug}/logo/confirm`, {
|
|
33998
34088
|
s3_key,
|
|
33999
34089
|
position: args.position ?? "top-right",
|
|
34000
|
-
treatment: args.treatment
|
|
34090
|
+
...args.treatment ? { treatment: args.treatment } : {}
|
|
34001
34091
|
});
|
|
34002
34092
|
return ok(asRecord(res).data ?? res);
|
|
34003
34093
|
}));
|
|
@@ -34020,7 +34110,7 @@ registerTool("set_short_poster", {
|
|
|
34020
34110
|
}));
|
|
34021
34111
|
registerTool("get_status", {
|
|
34022
34112
|
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.",
|
|
34113
|
+
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
34114
|
inputSchema: { slug: exports_external.string(), kind: kindSchema },
|
|
34025
34115
|
outputSchema: getStatusOutput,
|
|
34026
34116
|
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.16.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
|
@@ -289,6 +289,8 @@ export interface NormalizedStatus {
|
|
|
289
289
|
active_segment_ids?: Array<number | string>;
|
|
290
290
|
/** Editor only: completed current scene ids invalidated by continuity edits. */
|
|
291
291
|
stale_segment_ids?: Array<number | string>;
|
|
292
|
+
/** Editor Autopilot only: server-verified quoted delivery contract. */
|
|
293
|
+
autopilot_contract_satisfied?: boolean;
|
|
292
294
|
/** Short only (additive, absent on older APIs): the newest render row is a free re-render. */
|
|
293
295
|
latest_render_free?: boolean;
|
|
294
296
|
/**
|
|
@@ -515,6 +517,21 @@ export function normalizeStatus(
|
|
|
515
517
|
// Legacy/null/omitted freshness is unknown and must never be promoted to ready.
|
|
516
518
|
const renderFresh = latest.is_stale === false;
|
|
517
519
|
const stale = renderStale || staleSegments.length > 0 || audioStale;
|
|
520
|
+
// A completed Autopilot run the API reports as NOT satisfying its delivery
|
|
521
|
+
// contract — gated on a FRESH timeline. The server recomputes
|
|
522
|
+
// autopilot_contract_satisfied LIVE for completed runs, folding in staleness,
|
|
523
|
+
// so any ordinary post-delivery edit (e.g. adding narration makes music stale)
|
|
524
|
+
// flips it false even though the run genuinely delivered. Gating on !stale
|
|
525
|
+
// keeps this signal to the real missing-component case — canonically a legacy
|
|
526
|
+
// "music-only" run where nothing is stale but voice/narration were never
|
|
527
|
+
// produced — and lets the stale/editing_required semantics win everywhere once
|
|
528
|
+
// an edit lands. Declared after `stale` (which it depends on); the raw
|
|
529
|
+
// autopilot_contract_satisfied wire field is still passed through verbatim
|
|
530
|
+
// below, only this derived stage/error signal is freshness-gated.
|
|
531
|
+
const autopilotContractIncomplete =
|
|
532
|
+
autopilot === "completed" &&
|
|
533
|
+
d.autopilot_contract_satisfied === false &&
|
|
534
|
+
!stale;
|
|
518
535
|
// A render in flight is active work too — fold it into workActive so the
|
|
519
536
|
// terminal computation below can never fire while a render is queued/running.
|
|
520
537
|
// "pending" here is NOT an idle default: a video_results row only exists once
|
|
@@ -638,7 +655,8 @@ export function normalizeStatus(
|
|
|
638
655
|
!stale &&
|
|
639
656
|
!workActive &&
|
|
640
657
|
failedSegments.length === 0 &&
|
|
641
|
-
!audioFailed
|
|
658
|
+
!audioFailed &&
|
|
659
|
+
!autopilotContractIncomplete;
|
|
642
660
|
const ready = deliveredArtifactSupersedesOrchestrationFailure;
|
|
643
661
|
const parentFailureActionable = autopilotFailed && !batchRecoveryActive;
|
|
644
662
|
const renderFailed = renderStatus === "failed" && childFailureActionable;
|
|
@@ -698,6 +716,7 @@ export function normalizeStatus(
|
|
|
698
716
|
stage = "segment_failed";
|
|
699
717
|
else if (childFailureActionable && audioFailed) stage = "audio_failed";
|
|
700
718
|
else if (batchInsufficientCredits) stage = "insufficient_credits";
|
|
719
|
+
else if (autopilotContractIncomplete) stage = "contract_incomplete";
|
|
701
720
|
else if (activeSegments.length > 0) stage = "segment_processing";
|
|
702
721
|
else if (batchActive) stage = "segment_generation";
|
|
703
722
|
else if (autoRenderHandoffActive) stage = "rendering";
|
|
@@ -727,6 +746,13 @@ export function normalizeStatus(
|
|
|
727
746
|
terminal,
|
|
728
747
|
ready,
|
|
729
748
|
video_url: videoUrl,
|
|
749
|
+
// Mirror the stage chain's precedence EXACTLY so the error text always
|
|
750
|
+
// matches the stage the caller sees. batch-retry, then the actionable
|
|
751
|
+
// parent/child failures, then insufficient-credits, and only last the
|
|
752
|
+
// contract-incomplete message — because a completed-autopilot editor with a
|
|
753
|
+
// later paused manual batch reports stage "batch_retry_required", so it must
|
|
754
|
+
// read the retry guidance, not "Resume Autopilot" (the one action
|
|
755
|
+
// start_autopilot refuses while a batch is paused).
|
|
730
756
|
error: ready
|
|
731
757
|
? null
|
|
732
758
|
: batchRetryRequired
|
|
@@ -737,6 +763,9 @@ export function normalizeStatus(
|
|
|
737
763
|
actionableChildError ??
|
|
738
764
|
(batchInsufficientCredits
|
|
739
765
|
? "Batch generation paused: not enough credits to finish. Top up or reduce scope, then invoke the generation/render path again; retry_editor_batch only applies to paused_for_retry."
|
|
766
|
+
: null) ??
|
|
767
|
+
(autopilotContractIncomplete
|
|
768
|
+
? "Autopilot completed without every quoted deliverable. Resume Autopilot to repair the missing component."
|
|
740
769
|
: null)),
|
|
741
770
|
stale,
|
|
742
771
|
active_segment_ids: activeSegments
|
|
@@ -751,6 +780,12 @@ export function normalizeStatus(
|
|
|
751
780
|
(id): id is number | string =>
|
|
752
781
|
typeof id === "number" || typeof id === "string",
|
|
753
782
|
),
|
|
783
|
+
...(typeof d.autopilot_contract_satisfied === "boolean"
|
|
784
|
+
? {
|
|
785
|
+
autopilot_contract_satisfied:
|
|
786
|
+
d.autopilot_contract_satisfied as boolean,
|
|
787
|
+
}
|
|
788
|
+
: {}),
|
|
754
789
|
};
|
|
755
790
|
}
|
|
756
791
|
|
|
@@ -817,6 +852,7 @@ export function idemKey(...parts: string[]): string {
|
|
|
817
852
|
/** make_video's editor draft-create key (`POST /editor`). */
|
|
818
853
|
export interface MakeVideoEditorCreateArgs {
|
|
819
854
|
prompt: string;
|
|
855
|
+
segments_count?: number;
|
|
820
856
|
product_subject?: string;
|
|
821
857
|
project_intent?: string;
|
|
822
858
|
language?: string;
|
|
@@ -831,6 +867,7 @@ export function makeVideoEditorCreateKey(a: MakeVideoEditorCreateArgs): string {
|
|
|
831
867
|
return idemKey(
|
|
832
868
|
"make-editor",
|
|
833
869
|
a.prompt,
|
|
870
|
+
a.segments_count === undefined ? "" : String(a.segments_count),
|
|
834
871
|
a.product_subject ?? "",
|
|
835
872
|
a.project_intent ?? "",
|
|
836
873
|
a.language ?? "en",
|
|
@@ -857,6 +894,7 @@ export function makeVideoAutopilotKey(
|
|
|
857
894
|
"autopilot",
|
|
858
895
|
slug,
|
|
859
896
|
a.prompt,
|
|
897
|
+
a.segments_count === undefined ? "" : String(a.segments_count),
|
|
860
898
|
a.product_subject ?? "",
|
|
861
899
|
a.project_intent ?? "",
|
|
862
900
|
a.language ?? "en",
|
|
@@ -872,6 +910,7 @@ export function makeVideoAutopilotKey(
|
|
|
872
910
|
/** create_editor_ad's draft-create and autopilot-start key argument set. */
|
|
873
911
|
export interface EditorAdKeyArgs {
|
|
874
912
|
product_prompt?: string;
|
|
913
|
+
segments_count?: number;
|
|
875
914
|
product_subject?: string;
|
|
876
915
|
project_intent?: string;
|
|
877
916
|
language?: string;
|
|
@@ -906,6 +945,7 @@ export function editorAdCreateKey(a: EditorAdKeyArgs): string {
|
|
|
906
945
|
return idemKey(
|
|
907
946
|
"create-editor",
|
|
908
947
|
a.product_prompt ?? "",
|
|
948
|
+
a.segments_count === undefined ? "" : String(a.segments_count),
|
|
909
949
|
a.product_subject ?? "",
|
|
910
950
|
a.project_intent ?? "",
|
|
911
951
|
a.language ?? "en",
|
|
@@ -943,6 +983,7 @@ export function editorAdAutopilotKey(slug: string, a: EditorAdKeyArgs): string {
|
|
|
943
983
|
"autopilot",
|
|
944
984
|
slug,
|
|
945
985
|
a.product_prompt ?? "",
|
|
986
|
+
a.segments_count === undefined ? "" : String(a.segments_count),
|
|
946
987
|
a.product_subject ?? "",
|
|
947
988
|
a.project_intent ?? "",
|
|
948
989
|
a.language ?? "en",
|
package/src/index.ts
CHANGED
|
@@ -101,6 +101,7 @@ import {
|
|
|
101
101
|
validatePerformanceMetrics,
|
|
102
102
|
validateReviewOutput,
|
|
103
103
|
} from "./perf-review-assets.js";
|
|
104
|
+
import { pollWithFinalReconciliation } from "./polling.js";
|
|
104
105
|
import {
|
|
105
106
|
buildScheduledPostBody,
|
|
106
107
|
calendarScopeErrorText,
|
|
@@ -161,11 +162,14 @@ async function editorAutopilotQuote(
|
|
|
161
162
|
language?: string;
|
|
162
163
|
product_subject?: string;
|
|
163
164
|
product_prompt?: string;
|
|
165
|
+
segments_count?: number;
|
|
164
166
|
restart?: boolean;
|
|
165
167
|
} = {},
|
|
166
168
|
): Promise<{
|
|
167
169
|
estimated_credits: number | null;
|
|
168
170
|
available_credits: number | null;
|
|
171
|
+
scene_count: number | null;
|
|
172
|
+
target_duration_seconds: number | null;
|
|
169
173
|
affordable: boolean;
|
|
170
174
|
}> {
|
|
171
175
|
try {
|
|
@@ -175,6 +179,8 @@ async function editorAutopilotQuote(
|
|
|
175
179
|
params.set("product_subject", overrides.product_subject);
|
|
176
180
|
if (overrides.product_prompt !== undefined)
|
|
177
181
|
params.set("product_prompt", overrides.product_prompt);
|
|
182
|
+
if (overrides.segments_count !== undefined)
|
|
183
|
+
params.set("segments_count", String(overrides.segments_count));
|
|
178
184
|
if (overrides.restart) params.set("restart", "true");
|
|
179
185
|
const query = params.toString();
|
|
180
186
|
const response = await client.get<{ data: unknown }>(
|
|
@@ -187,9 +193,17 @@ async function editorAutopilotQuote(
|
|
|
187
193
|
typeof cost.available_credits === "number"
|
|
188
194
|
? cost.available_credits
|
|
189
195
|
: null;
|
|
196
|
+
const scene_count =
|
|
197
|
+
typeof cost.scene_count === "number" ? cost.scene_count : null;
|
|
198
|
+
const target_duration_seconds =
|
|
199
|
+
typeof cost.target_duration_seconds === "number"
|
|
200
|
+
? cost.target_duration_seconds
|
|
201
|
+
: null;
|
|
190
202
|
return {
|
|
191
203
|
estimated_credits,
|
|
192
204
|
available_credits,
|
|
205
|
+
scene_count,
|
|
206
|
+
target_duration_seconds,
|
|
193
207
|
affordable:
|
|
194
208
|
estimated_credits !== null &&
|
|
195
209
|
available_credits !== null &&
|
|
@@ -213,6 +227,8 @@ async function editorAutopilotQuote(
|
|
|
213
227
|
return {
|
|
214
228
|
estimated_credits: null,
|
|
215
229
|
available_credits: null,
|
|
230
|
+
scene_count: null,
|
|
231
|
+
target_duration_seconds: null,
|
|
216
232
|
affordable: false,
|
|
217
233
|
};
|
|
218
234
|
}
|
|
@@ -674,15 +690,14 @@ async function pollToTerminal(
|
|
|
674
690
|
budgetMs: number,
|
|
675
691
|
intervalMs: number,
|
|
676
692
|
): Promise<NormalizedStatus> {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
685
|
-
return status;
|
|
693
|
+
return pollWithFinalReconciliation({
|
|
694
|
+
read: (deadline) => fetchStatus(client, kind, slug, deadline),
|
|
695
|
+
isTerminal: (status) => status.terminal,
|
|
696
|
+
budgetMs,
|
|
697
|
+
intervalMs,
|
|
698
|
+
onPoll: (status, pollNumber) =>
|
|
699
|
+
reportProgress(extra, pollNumber, `stage: ${status.stage}`),
|
|
700
|
+
});
|
|
686
701
|
}
|
|
687
702
|
|
|
688
703
|
/**
|
|
@@ -799,6 +814,15 @@ registerTool(
|
|
|
799
814
|
.describe(
|
|
800
815
|
"EDITOR only: social_ad for promotional content (product focus + CTA) or creative_story (default).",
|
|
801
816
|
),
|
|
817
|
+
segments_count: z
|
|
818
|
+
.number()
|
|
819
|
+
.int()
|
|
820
|
+
.min(3)
|
|
821
|
+
.max(10)
|
|
822
|
+
.optional()
|
|
823
|
+
.describe(
|
|
824
|
+
"EDITOR only: exact number of 8-second AI scenes (3–10). Use 3 for a concise ~24s ad.",
|
|
825
|
+
),
|
|
802
826
|
language: z
|
|
803
827
|
.enum(LANGUAGES)
|
|
804
828
|
.optional()
|
|
@@ -978,6 +1002,7 @@ registerTool(
|
|
|
978
1002
|
kind?: string;
|
|
979
1003
|
product_subject?: string;
|
|
980
1004
|
project_intent?: string;
|
|
1005
|
+
segments_count?: number;
|
|
981
1006
|
language?: string;
|
|
982
1007
|
aspect?: string;
|
|
983
1008
|
voice_id?: string;
|
|
@@ -1022,6 +1047,9 @@ registerTool(
|
|
|
1022
1047
|
const requestedKind =
|
|
1023
1048
|
args.kind === "short" || args.kind === "editor" ? args.kind : undefined;
|
|
1024
1049
|
const kind: Kind = requestedKind ?? inferKind(args.prompt);
|
|
1050
|
+
if (kind !== "editor" && args.segments_count !== undefined) {
|
|
1051
|
+
return fail("segments_count is only supported for editor videos.");
|
|
1052
|
+
}
|
|
1025
1053
|
const waitSeconds = Math.min(
|
|
1026
1054
|
280,
|
|
1027
1055
|
Math.max(10, args.max_wait_seconds ?? 240),
|
|
@@ -1128,6 +1156,7 @@ registerTool(
|
|
|
1128
1156
|
{
|
|
1129
1157
|
language: args.language ?? "en",
|
|
1130
1158
|
product_prompt: args.prompt,
|
|
1159
|
+
segments_count: args.segments_count,
|
|
1131
1160
|
product_subject: args.product_subject,
|
|
1132
1161
|
project_intent: args.project_intent,
|
|
1133
1162
|
export_aspect_ratio: args.aspect,
|
|
@@ -1412,8 +1441,9 @@ registerTool(
|
|
|
1412
1441
|
{
|
|
1413
1442
|
title: "List voices",
|
|
1414
1443
|
description:
|
|
1415
|
-
"Lists available narration voices
|
|
1416
|
-
"
|
|
1444
|
+
"Lists available narration voices with voice_id, preview metadata, and verified language codes. " +
|
|
1445
|
+
"Choose a voice whose languages includes the editor project's language, then pass its voice_id to " +
|
|
1446
|
+
"create_editor_ad / make_video / generate_voice. Shorts have no voiceover.",
|
|
1417
1447
|
inputSchema: {},
|
|
1418
1448
|
annotations: { title: "List voices", ...RO },
|
|
1419
1449
|
},
|
|
@@ -2023,8 +2053,9 @@ registerTool(
|
|
|
2023
2053
|
.enum(["top-left", "top-right", "bottom-left", "bottom-right"])
|
|
2024
2054
|
.optional()
|
|
2025
2055
|
.describe(
|
|
2026
|
-
"
|
|
2027
|
-
"a
|
|
2056
|
+
"Preferred corner for the deterministic brand-logo overlay (set the image with set_short_logo). " +
|
|
2057
|
+
"Forge moves it to a clear rail when text/CTA/chips occupy that corner. Moving it is a FREE " +
|
|
2058
|
+
"re-render — logo fields never cost credits.",
|
|
2028
2059
|
),
|
|
2029
2060
|
short_logo_treatment: z
|
|
2030
2061
|
.enum(["throughout", "end_card", "none"])
|
|
@@ -2541,7 +2572,9 @@ registerTool(
|
|
|
2541
2572
|
"duplicate while a render is in flight is reported as in-progress (no double charge), and a failed " +
|
|
2542
2573
|
"short can be re-generated. If the draft's headline/subheadline/text_beats are all blank the server " +
|
|
2543
2574
|
"auto-writes the headline + caption beats before rendering (pass skip_auto_text:true for a deliberately " +
|
|
2544
|
-
"bare clip).
|
|
2575
|
+
"bare clip). A transient 5xx is reconciled against the project state: accepted work is returned as " +
|
|
2576
|
+
"in-progress, while an unchanged draft is retried once. Then poll with get_status or " +
|
|
2577
|
+
"wait_for_completion (kind=short).",
|
|
2545
2578
|
inputSchema: {
|
|
2546
2579
|
slug: z.string().describe("Short slug from create_short"),
|
|
2547
2580
|
skip_auto_text: z
|
|
@@ -2563,17 +2596,21 @@ registerTool(
|
|
|
2563
2596
|
// of a failed short. Double-charge is prevented server-side — the batch
|
|
2564
2597
|
// claim's conditional update rolls back the losing transaction's credit
|
|
2565
2598
|
// deduction — and a 409 means a render is already running.
|
|
2566
|
-
|
|
2567
|
-
|
|
2599
|
+
const submit = () =>
|
|
2600
|
+
client.post<{ data: unknown }>(
|
|
2568
2601
|
`/shorts/${args.slug}/generate`,
|
|
2569
2602
|
// Only send a body when the caller opts out of server auto-copy —
|
|
2570
2603
|
// the bare POST stays byte-identical to the pre-0.8.2 wire shape.
|
|
2571
2604
|
args.skip_auto_text ? { skip_auto_text: true } : undefined,
|
|
2572
2605
|
);
|
|
2606
|
+
|
|
2607
|
+
try {
|
|
2608
|
+
const res = await submit();
|
|
2573
2609
|
const status = normalizeStatus("short", args.slug, asRecord(res).data);
|
|
2574
2610
|
return ok(status);
|
|
2575
2611
|
} catch (e) {
|
|
2576
|
-
|
|
2612
|
+
const statusCode = (e as { status?: number }).status;
|
|
2613
|
+
if (statusCode === 409) {
|
|
2577
2614
|
return ok({
|
|
2578
2615
|
kind: "short",
|
|
2579
2616
|
slug: args.slug,
|
|
@@ -2584,6 +2621,47 @@ registerTool(
|
|
|
2584
2621
|
error: null,
|
|
2585
2622
|
});
|
|
2586
2623
|
}
|
|
2624
|
+
|
|
2625
|
+
if (statusCode !== undefined && statusCode >= 500) {
|
|
2626
|
+
let reconciled: NormalizedStatus;
|
|
2627
|
+
try {
|
|
2628
|
+
reconciled = await fetchStatus(client, "short", args.slug);
|
|
2629
|
+
} catch (_reconcileError) {
|
|
2630
|
+
// Preserve the original POST failure when the read-side check also
|
|
2631
|
+
// fails; the client must never blind-retry a possibly accepted debit.
|
|
2632
|
+
throw e;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
// Only the active render stage proves this POST crossed the paid
|
|
2636
|
+
// launch transaction. A previous completed/failed result can still be
|
|
2637
|
+
// visible after an early 5xx; returning it here would masquerade stale
|
|
2638
|
+
// work as the newly accepted generation.
|
|
2639
|
+
if (reconciled.stage === "rendering") return ok(reconciled);
|
|
2640
|
+
if (reconciled.stage !== "draft") throw e;
|
|
2641
|
+
|
|
2642
|
+
// A Short launch changes the durable factory out of draft in the
|
|
2643
|
+
// same transaction as the debit. Still-draft therefore proves the
|
|
2644
|
+
// failed response accepted no paid work, so one retry is safe.
|
|
2645
|
+
try {
|
|
2646
|
+
const retried = await submit();
|
|
2647
|
+
return ok(
|
|
2648
|
+
normalizeStatus("short", args.slug, asRecord(retried).data),
|
|
2649
|
+
);
|
|
2650
|
+
} catch (retryError) {
|
|
2651
|
+
if ((retryError as { status?: number }).status === 409) {
|
|
2652
|
+
return ok({
|
|
2653
|
+
kind: "short",
|
|
2654
|
+
slug: args.slug,
|
|
2655
|
+
stage: "processing",
|
|
2656
|
+
terminal: false,
|
|
2657
|
+
ready: false,
|
|
2658
|
+
video_url: null,
|
|
2659
|
+
error: null,
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
throw retryError;
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2587
2665
|
throw e;
|
|
2588
2666
|
}
|
|
2589
2667
|
}),
|
|
@@ -3131,7 +3209,9 @@ registerTool(
|
|
|
3131
3209
|
"wait_for_completion (kind=editor). Prefer make_video for a prompt-only one-shot. " +
|
|
3132
3210
|
"A product image is analyzed into semantic project context and sent as a product-identity reference to every " +
|
|
3133
3211
|
"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.
|
|
3212
|
+
"was authored; generated fine label text can still vary. Do not depend on the video model for exact readable " +
|
|
3213
|
+
"brand names, UI, signs, prices, or CTA copy; put essential copy in narration or deterministic Short/poster overlays. " +
|
|
3214
|
+
"Scene prompts are the complete content contract: describe any people, " +
|
|
3135
3215
|
"products, visual continuity, absences, and composition directly in the brief. (Editor ads carry their copy " +
|
|
3136
3216
|
"in-scene/narration, not as a title overlay; for an on-screen title/subtitle use a short with " +
|
|
3137
3217
|
"headline/subheadline instead.)",
|
|
@@ -3139,7 +3219,11 @@ registerTool(
|
|
|
3139
3219
|
product_prompt: z
|
|
3140
3220
|
.string()
|
|
3141
3221
|
.min(10)
|
|
3142
|
-
.describe(
|
|
3222
|
+
.describe(
|
|
3223
|
+
"Creative brief for the ad (min 10 chars), not literal voiceover copy. Autopilot uses it to draft " +
|
|
3224
|
+
"narration. For exact spoken words, use the granular editor path and pass only read-aloud text to " +
|
|
3225
|
+
"set_narration_script.",
|
|
3226
|
+
),
|
|
3143
3227
|
product_subject: z
|
|
3144
3228
|
.string()
|
|
3145
3229
|
.max(300)
|
|
@@ -3190,6 +3274,15 @@ registerTool(
|
|
|
3190
3274
|
.enum(["9:16", "16:9", "1:1"])
|
|
3191
3275
|
.optional()
|
|
3192
3276
|
.describe('Aspect ratio (default "9:16")'),
|
|
3277
|
+
segments_count: z
|
|
3278
|
+
.number()
|
|
3279
|
+
.int()
|
|
3280
|
+
.min(3)
|
|
3281
|
+
.max(10)
|
|
3282
|
+
.optional()
|
|
3283
|
+
.describe(
|
|
3284
|
+
"Exact number of 8-second AI scenes (3–10). Use 3 for a concise ~24s ad; omit to retain the server default.",
|
|
3285
|
+
),
|
|
3193
3286
|
product_image_path: z
|
|
3194
3287
|
.string()
|
|
3195
3288
|
.optional()
|
|
@@ -3255,6 +3348,7 @@ registerTool(
|
|
|
3255
3348
|
theme?: string;
|
|
3256
3349
|
voice_id?: string;
|
|
3257
3350
|
export_aspect_ratio?: string;
|
|
3351
|
+
segments_count?: number;
|
|
3258
3352
|
product_image_path?: string;
|
|
3259
3353
|
product_description?: string;
|
|
3260
3354
|
closing_image_path?: string;
|
|
@@ -3331,6 +3425,7 @@ registerTool(
|
|
|
3331
3425
|
{
|
|
3332
3426
|
language: args.language ?? "en",
|
|
3333
3427
|
product_prompt: args.product_prompt,
|
|
3428
|
+
segments_count: args.segments_count,
|
|
3334
3429
|
product_subject: args.product_subject,
|
|
3335
3430
|
project_intent: args.project_intent,
|
|
3336
3431
|
creative_format: args.creative_format,
|
|
@@ -3409,7 +3504,9 @@ registerTool(
|
|
|
3409
3504
|
if (args.logo_path && Object.keys(logoSettings).length > 0)
|
|
3410
3505
|
await client.patch(`/editor/${slug}/logo`, logoSettings);
|
|
3411
3506
|
|
|
3412
|
-
const quote = await editorAutopilotQuote(client, slug
|
|
3507
|
+
const quote = await editorAutopilotQuote(client, slug, {
|
|
3508
|
+
segments_count: args.segments_count,
|
|
3509
|
+
});
|
|
3413
3510
|
const overCap =
|
|
3414
3511
|
args.max_credits !== undefined &&
|
|
3415
3512
|
quote.estimated_credits !== null &&
|
|
@@ -3440,7 +3537,10 @@ registerTool(
|
|
|
3440
3537
|
|
|
3441
3538
|
const started = await client.post<{ data: unknown }>(
|
|
3442
3539
|
`/editor/${slug}/autopilot`,
|
|
3443
|
-
{
|
|
3540
|
+
{
|
|
3541
|
+
max_credits: args.max_credits,
|
|
3542
|
+
segments_count: args.segments_count,
|
|
3543
|
+
},
|
|
3444
3544
|
// Fold the create-affecting params into the start key so it tracks
|
|
3445
3545
|
// inputs, not just the slug. Self-consistent per-tool only: this key
|
|
3446
3546
|
// is compared against retries of *this* tool on *this* slug. The
|
|
@@ -3506,6 +3606,15 @@ registerTool(
|
|
|
3506
3606
|
.describe(
|
|
3507
3607
|
"Optional freeform brief to set/replace before launch. Omit to keep the stored brief; send blank to clear it.",
|
|
3508
3608
|
),
|
|
3609
|
+
segments_count: z
|
|
3610
|
+
.number()
|
|
3611
|
+
.int()
|
|
3612
|
+
.min(3)
|
|
3613
|
+
.max(10)
|
|
3614
|
+
.optional()
|
|
3615
|
+
.describe(
|
|
3616
|
+
"Optional exact number of 8-second AI scenes (3–10). Omit to keep the stored structural target.",
|
|
3617
|
+
),
|
|
3509
3618
|
max_credits: z
|
|
3510
3619
|
.number()
|
|
3511
3620
|
.int()
|
|
@@ -3531,6 +3640,7 @@ registerTool(
|
|
|
3531
3640
|
language?: string;
|
|
3532
3641
|
product_subject?: string;
|
|
3533
3642
|
product_prompt?: string;
|
|
3643
|
+
segments_count?: number;
|
|
3534
3644
|
max_credits?: number;
|
|
3535
3645
|
restart?: boolean;
|
|
3536
3646
|
},
|
|
@@ -3546,6 +3656,7 @@ registerTool(
|
|
|
3546
3656
|
language?: string;
|
|
3547
3657
|
product_subject?: string;
|
|
3548
3658
|
product_prompt?: string;
|
|
3659
|
+
segments_count?: number;
|
|
3549
3660
|
restart?: boolean;
|
|
3550
3661
|
} = {};
|
|
3551
3662
|
if (args.language !== undefined) overrides.language = args.language;
|
|
@@ -3553,6 +3664,8 @@ registerTool(
|
|
|
3553
3664
|
overrides.product_subject = args.product_subject;
|
|
3554
3665
|
if (args.product_prompt !== undefined)
|
|
3555
3666
|
overrides.product_prompt = args.product_prompt;
|
|
3667
|
+
if (args.segments_count !== undefined)
|
|
3668
|
+
overrides.segments_count = args.segments_count;
|
|
3556
3669
|
if (args.restart) overrides.restart = true;
|
|
3557
3670
|
|
|
3558
3671
|
const quote = await editorAutopilotQuote(client, slug, overrides);
|
|
@@ -4806,7 +4919,8 @@ registerTool(
|
|
|
4806
4919
|
title: "Generate the background music (5 credits)",
|
|
4807
4920
|
description:
|
|
4808
4921
|
"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).
|
|
4922
|
+
" music-direction prompt (≤1200 chars; longer is silently truncated server-side). `instruments` is an " +
|
|
4923
|
+
"optional array of up to 10 non-empty hints (≤80 chars each), never a comma-separated string. 402 fails cleanly.",
|
|
4810
4924
|
inputSchema: {
|
|
4811
4925
|
slug: z.string().describe("Editor project slug"),
|
|
4812
4926
|
description: z
|
|
@@ -5594,18 +5708,24 @@ registerTool(
|
|
|
5594
5708
|
description:
|
|
5595
5709
|
"Uploads an exact brand logo for deterministic compositing over a Short. The image is never sent to the " +
|
|
5596
5710
|
"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."
|
|
5711
|
+
"it appears throughout the footage or only on the end card. The corner is a preference: Forge moves the " +
|
|
5712
|
+
"logo to a clear rail when headline/caption/CTA/chip overlays occupy it. Prefer a tight transparent PNG " +
|
|
5713
|
+
"wordmark; use square/full-bleed artwork as a poster instead of a watermark.",
|
|
5598
5714
|
inputSchema: {
|
|
5599
5715
|
slug: z.string().describe("Short slug"),
|
|
5600
5716
|
file_path: z.string().describe("Local logo image (.png/.jpg/.jpeg)"),
|
|
5601
5717
|
position: z
|
|
5602
5718
|
.enum(["top-left", "top-right", "bottom-left", "bottom-right"])
|
|
5603
5719
|
.optional()
|
|
5604
|
-
.describe(
|
|
5720
|
+
.describe(
|
|
5721
|
+
"Preferred logo corner (default top-right; auto-relocated to avoid text)",
|
|
5722
|
+
),
|
|
5605
5723
|
treatment: z
|
|
5606
5724
|
.enum(["throughout", "end_card", "none"])
|
|
5607
5725
|
.optional()
|
|
5608
|
-
.describe(
|
|
5726
|
+
.describe(
|
|
5727
|
+
"When the logo appears. Omit for automatic treatment: transparent wordmarks run throughout; opaque square artwork moves to the end card.",
|
|
5728
|
+
),
|
|
5609
5729
|
},
|
|
5610
5730
|
annotations: { title: "Set Short logo", ...WRITE, idempotentHint: false },
|
|
5611
5731
|
},
|
|
@@ -5630,7 +5750,7 @@ registerTool(
|
|
|
5630
5750
|
{
|
|
5631
5751
|
s3_key,
|
|
5632
5752
|
position: args.position ?? "top-right",
|
|
5633
|
-
treatment: args.treatment
|
|
5753
|
+
...(args.treatment ? { treatment: args.treatment } : {}),
|
|
5634
5754
|
},
|
|
5635
5755
|
);
|
|
5636
5756
|
return ok(asRecord(res).data ?? res);
|
|
@@ -5700,7 +5820,8 @@ registerTool(
|
|
|
5700
5820
|
"Returns a normalized status {stage, terminal, ready, video_url, error} for a short, editor, " +
|
|
5701
5821
|
"tracking, or slider project. ready:true means the post-ready MP4 is at video_url — except for a " +
|
|
5702
5822
|
"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."
|
|
5823
|
+
"slide is composited, and you read the per-slide image URLs with get_slider. During paid short " +
|
|
5824
|
+
"generation, segments_completed / segments_total and active_segment_positions report real clip progress.",
|
|
5704
5825
|
inputSchema: { slug: z.string(), kind: kindSchema },
|
|
5705
5826
|
outputSchema: getStatusOutput,
|
|
5706
5827
|
annotations: { title: "Get status", ...RO },
|
package/src/polling.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface PollOptions<T> {
|
|
2
|
+
read: (deadline: number) => Promise<T>;
|
|
3
|
+
isTerminal: (value: T) => boolean;
|
|
4
|
+
budgetMs: number;
|
|
5
|
+
intervalMs: number;
|
|
6
|
+
onPoll?: (value: T, pollNumber: number) => Promise<void>;
|
|
7
|
+
now?: () => number;
|
|
8
|
+
sleep?: (milliseconds: number) => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Poll within a fixed budget, then perform one separately bounded
|
|
13
|
+
* reconciliation read. The final read closes the common race where work
|
|
14
|
+
* finishes during the last partial interval and the caller otherwise returns
|
|
15
|
+
* the stale pre-completion snapshot.
|
|
16
|
+
*/
|
|
17
|
+
export async function pollWithFinalReconciliation<T>({
|
|
18
|
+
read,
|
|
19
|
+
isTerminal,
|
|
20
|
+
budgetMs,
|
|
21
|
+
intervalMs,
|
|
22
|
+
onPoll,
|
|
23
|
+
now = Date.now,
|
|
24
|
+
sleep = (milliseconds) =>
|
|
25
|
+
new Promise<void>((resolve) => setTimeout(resolve, milliseconds)),
|
|
26
|
+
}: PollOptions<T>): Promise<T> {
|
|
27
|
+
const deadline = now() + budgetMs;
|
|
28
|
+
let value = await read(deadline);
|
|
29
|
+
let pollNumber = 0;
|
|
30
|
+
|
|
31
|
+
while (!isTerminal(value)) {
|
|
32
|
+
const remainingMs = deadline - now();
|
|
33
|
+
if (remainingMs <= 0) break;
|
|
34
|
+
await onPoll?.(value, ++pollNumber);
|
|
35
|
+
await sleep(Math.min(intervalMs, remainingMs));
|
|
36
|
+
if (now() >= deadline) break;
|
|
37
|
+
value = await read(deadline);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!isTerminal(value)) {
|
|
41
|
+
const finalReadDeadline =
|
|
42
|
+
now() + Math.min(5_000, Math.max(1_000, intervalMs));
|
|
43
|
+
try {
|
|
44
|
+
value = await read(finalReadDeadline);
|
|
45
|
+
} catch {
|
|
46
|
+
// The normal timeout/resume contract uses the last successful state.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return value;
|
|
51
|
+
}
|