@hubfluencer/mcp 0.15.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 +6 -6
- package/dist/index.js +80 -21
- package/package.json +1 -1
- package/src/core.ts +42 -1
- package/src/index.ts +73 -13
- 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,7 +111,7 @@ 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
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** |
|
|
@@ -137,7 +137,7 @@ 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
|
|
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
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
|
|
@@ -152,9 +152,9 @@ images at **8 MiB (8,388,608 bytes)**; closing/short/catalog images at **20 MiB
|
|
|
152
152
|
|
|
153
153
|
### Creative export contract
|
|
154
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.
|
|
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
156
|
- Do not rely on the video model to spell essential brand names, UI, signs, prices, offers, or CTAs. Put exact copy in Hubfluencer overlays, narration, or supplied ending artwork.
|
|
157
|
-
- `short_logo_position` is a preferred corner. Forge moves the logo away from headline, captions, CTA, offer, and badge areas. Use a tight transparent wordmark; use square/full-bleed art as the poster. `end_card` logos are suppressed when `poster_includes_lockup:true` to avoid double branding.
|
|
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
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
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
160
|
|
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,
|
|
@@ -32889,6 +32938,7 @@ registerTool("create_editor_ad", {
|
|
|
32889
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.'),
|
|
32890
32939
|
voice_id: exports_external.string().optional().describe("Preferred narration voice id (see list_voices); omit for the default voice"),
|
|
32891
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."),
|
|
32892
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."),
|
|
32893
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."),
|
|
32894
32944
|
closing_image_path: exports_external.string().optional().describe("Local closing-card image (.jpg/.jpeg/.png, ≤20 MiB)."),
|
|
@@ -32933,6 +32983,7 @@ registerTool("create_editor_ad", {
|
|
|
32933
32983
|
const created = await client.post("/editor", {
|
|
32934
32984
|
language: args.language ?? "en",
|
|
32935
32985
|
product_prompt: args.product_prompt,
|
|
32986
|
+
segments_count: args.segments_count,
|
|
32936
32987
|
product_subject: args.product_subject,
|
|
32937
32988
|
project_intent: args.project_intent,
|
|
32938
32989
|
creative_format: args.creative_format,
|
|
@@ -32979,7 +33030,9 @@ registerTool("create_editor_ad", {
|
|
|
32979
33030
|
logoSettings.logo_duration_seconds = args.logo_duration_seconds;
|
|
32980
33031
|
if (args.logo_path && Object.keys(logoSettings).length > 0)
|
|
32981
33032
|
await client.patch(`/editor/${slug}/logo`, logoSettings);
|
|
32982
|
-
const quote = await editorAutopilotQuote(client, slug
|
|
33033
|
+
const quote = await editorAutopilotQuote(client, slug, {
|
|
33034
|
+
segments_count: args.segments_count
|
|
33035
|
+
});
|
|
32983
33036
|
const overCap = args.max_credits !== undefined && quote.estimated_credits !== null && quote.estimated_credits > args.max_credits;
|
|
32984
33037
|
if (args.max_credits === undefined || quote.estimated_credits === null || overCap || !quote.affordable) {
|
|
32985
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.`;
|
|
@@ -32992,7 +33045,10 @@ registerTool("create_editor_ad", {
|
|
|
32992
33045
|
next: "start_autopilot"
|
|
32993
33046
|
});
|
|
32994
33047
|
}
|
|
32995
|
-
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));
|
|
32996
33052
|
const status = normalizeStatus("editor", slug, asRecord(started).data);
|
|
32997
33053
|
return ok({
|
|
32998
33054
|
slug,
|
|
@@ -33017,6 +33073,7 @@ registerTool("start_autopilot", {
|
|
|
33017
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."),
|
|
33018
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."),
|
|
33019
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."),
|
|
33020
33077
|
max_credits: exports_external.number().int().nonnegative().optional().describe("Explicit spend cap. Omit to quote only; no credits are charged."),
|
|
33021
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.")
|
|
33022
33079
|
},
|
|
@@ -33037,6 +33094,8 @@ registerTool("start_autopilot", {
|
|
|
33037
33094
|
overrides.product_subject = args.product_subject;
|
|
33038
33095
|
if (args.product_prompt !== undefined)
|
|
33039
33096
|
overrides.product_prompt = args.product_prompt;
|
|
33097
|
+
if (args.segments_count !== undefined)
|
|
33098
|
+
overrides.segments_count = args.segments_count;
|
|
33040
33099
|
if (args.restart)
|
|
33041
33100
|
overrides.restart = true;
|
|
33042
33101
|
const quote = await editorAutopilotQuote(client, slug, overrides);
|
|
@@ -34020,7 +34079,7 @@ registerTool("set_short_logo", {
|
|
|
34020
34079
|
slug: exports_external.string().describe("Short slug"),
|
|
34021
34080
|
file_path: exports_external.string().describe("Local logo image (.png/.jpg/.jpeg)"),
|
|
34022
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)"),
|
|
34023
|
-
treatment: exports_external.enum(["throughout", "end_card", "none"]).optional().describe("When the logo appears
|
|
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.")
|
|
34024
34083
|
},
|
|
34025
34084
|
annotations: { title: "Set Short logo", ...WRITE, idempotentHint: false }
|
|
34026
34085
|
}, tool(async (args, client) => {
|
|
@@ -34028,7 +34087,7 @@ registerTool("set_short_logo", {
|
|
|
34028
34087
|
const res = await client.post(`/shorts/${args.slug}/logo/confirm`, {
|
|
34029
34088
|
s3_key,
|
|
34030
34089
|
position: args.position ?? "top-right",
|
|
34031
|
-
treatment: args.treatment
|
|
34090
|
+
...args.treatment ? { treatment: args.treatment } : {}
|
|
34032
34091
|
});
|
|
34033
34092
|
return ok(asRecord(res).data ?? res);
|
|
34034
34093
|
}));
|
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,
|
|
@@ -3245,6 +3274,15 @@ registerTool(
|
|
|
3245
3274
|
.enum(["9:16", "16:9", "1:1"])
|
|
3246
3275
|
.optional()
|
|
3247
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
|
+
),
|
|
3248
3286
|
product_image_path: z
|
|
3249
3287
|
.string()
|
|
3250
3288
|
.optional()
|
|
@@ -3310,6 +3348,7 @@ registerTool(
|
|
|
3310
3348
|
theme?: string;
|
|
3311
3349
|
voice_id?: string;
|
|
3312
3350
|
export_aspect_ratio?: string;
|
|
3351
|
+
segments_count?: number;
|
|
3313
3352
|
product_image_path?: string;
|
|
3314
3353
|
product_description?: string;
|
|
3315
3354
|
closing_image_path?: string;
|
|
@@ -3386,6 +3425,7 @@ registerTool(
|
|
|
3386
3425
|
{
|
|
3387
3426
|
language: args.language ?? "en",
|
|
3388
3427
|
product_prompt: args.product_prompt,
|
|
3428
|
+
segments_count: args.segments_count,
|
|
3389
3429
|
product_subject: args.product_subject,
|
|
3390
3430
|
project_intent: args.project_intent,
|
|
3391
3431
|
creative_format: args.creative_format,
|
|
@@ -3464,7 +3504,9 @@ registerTool(
|
|
|
3464
3504
|
if (args.logo_path && Object.keys(logoSettings).length > 0)
|
|
3465
3505
|
await client.patch(`/editor/${slug}/logo`, logoSettings);
|
|
3466
3506
|
|
|
3467
|
-
const quote = await editorAutopilotQuote(client, slug
|
|
3507
|
+
const quote = await editorAutopilotQuote(client, slug, {
|
|
3508
|
+
segments_count: args.segments_count,
|
|
3509
|
+
});
|
|
3468
3510
|
const overCap =
|
|
3469
3511
|
args.max_credits !== undefined &&
|
|
3470
3512
|
quote.estimated_credits !== null &&
|
|
@@ -3495,7 +3537,10 @@ registerTool(
|
|
|
3495
3537
|
|
|
3496
3538
|
const started = await client.post<{ data: unknown }>(
|
|
3497
3539
|
`/editor/${slug}/autopilot`,
|
|
3498
|
-
{
|
|
3540
|
+
{
|
|
3541
|
+
max_credits: args.max_credits,
|
|
3542
|
+
segments_count: args.segments_count,
|
|
3543
|
+
},
|
|
3499
3544
|
// Fold the create-affecting params into the start key so it tracks
|
|
3500
3545
|
// inputs, not just the slug. Self-consistent per-tool only: this key
|
|
3501
3546
|
// is compared against retries of *this* tool on *this* slug. The
|
|
@@ -3561,6 +3606,15 @@ registerTool(
|
|
|
3561
3606
|
.describe(
|
|
3562
3607
|
"Optional freeform brief to set/replace before launch. Omit to keep the stored brief; send blank to clear it.",
|
|
3563
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
|
+
),
|
|
3564
3618
|
max_credits: z
|
|
3565
3619
|
.number()
|
|
3566
3620
|
.int()
|
|
@@ -3586,6 +3640,7 @@ registerTool(
|
|
|
3586
3640
|
language?: string;
|
|
3587
3641
|
product_subject?: string;
|
|
3588
3642
|
product_prompt?: string;
|
|
3643
|
+
segments_count?: number;
|
|
3589
3644
|
max_credits?: number;
|
|
3590
3645
|
restart?: boolean;
|
|
3591
3646
|
},
|
|
@@ -3601,6 +3656,7 @@ registerTool(
|
|
|
3601
3656
|
language?: string;
|
|
3602
3657
|
product_subject?: string;
|
|
3603
3658
|
product_prompt?: string;
|
|
3659
|
+
segments_count?: number;
|
|
3604
3660
|
restart?: boolean;
|
|
3605
3661
|
} = {};
|
|
3606
3662
|
if (args.language !== undefined) overrides.language = args.language;
|
|
@@ -3608,6 +3664,8 @@ registerTool(
|
|
|
3608
3664
|
overrides.product_subject = args.product_subject;
|
|
3609
3665
|
if (args.product_prompt !== undefined)
|
|
3610
3666
|
overrides.product_prompt = args.product_prompt;
|
|
3667
|
+
if (args.segments_count !== undefined)
|
|
3668
|
+
overrides.segments_count = args.segments_count;
|
|
3611
3669
|
if (args.restart) overrides.restart = true;
|
|
3612
3670
|
|
|
3613
3671
|
const quote = await editorAutopilotQuote(client, slug, overrides);
|
|
@@ -5665,7 +5723,9 @@ registerTool(
|
|
|
5665
5723
|
treatment: z
|
|
5666
5724
|
.enum(["throughout", "end_card", "none"])
|
|
5667
5725
|
.optional()
|
|
5668
|
-
.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
|
+
),
|
|
5669
5729
|
},
|
|
5670
5730
|
annotations: { title: "Set Short logo", ...WRITE, idempotentHint: false },
|
|
5671
5731
|
},
|
|
@@ -5690,7 +5750,7 @@ registerTool(
|
|
|
5690
5750
|
{
|
|
5691
5751
|
s3_key,
|
|
5692
5752
|
position: args.position ?? "top-right",
|
|
5693
|
-
treatment: args.treatment
|
|
5753
|
+
...(args.treatment ? { treatment: args.treatment } : {}),
|
|
5694
5754
|
},
|
|
5695
5755
|
);
|
|
5696
5756
|
return ok(asRecord(res).data ?? res);
|
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
|
+
}
|