@hubfluencer/mcp 0.11.0 → 0.12.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 +25 -7
- package/dist/index.js +54 -51
- package/package.json +1 -1
- package/src/core.ts +13 -8
- package/src/index.ts +88 -86
package/README.md
CHANGED
|
@@ -84,10 +84,10 @@ instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_ass
|
|
|
84
84
|
| `generate_short` | Render an existing short — re-rolls the footage + music (fresh AI generation). If headline/subheadline/beats are all blank the server auto-writes the copy first (`skip_auto_text:true` for a deliberately bare clip). Safe to re-call (in-flight = reported, no double charge; a failed short can re-generate). | **15** |
|
|
85
85
|
| `rerender_short` | **FREE re-render** — applies the short's current text/style/CTA/end-card/logo state over the already-paid footage + music. Use after `update_short` for copy/style/CTA tweaks, or after a logo change (`set_short_logo`, or `short_logo_position`/`short_logo_treatment`); footage or music changes need `generate_short` instead. Needs one completed generation first (422 `short_not_ready` otherwise); changed footage/music inputs return 422 `short_paid_regeneration_required`. A failed free re-render leaves the delivered video intact — retry `rerender_short`, it stays free. | **0** |
|
|
86
86
|
| `generate_short_text` | AI-draft editable headline / subheadline / caption beats for a short. | 1 assist |
|
|
87
|
-
| `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.
|
|
87
|
+
| `create_editor_ad` | Create an editor project, attach optional local product/logo/closing assets, and return the live autopilot quote. Omit `max_credits` to stop free; provide an approved cap to launch. Write every required person, product, object, and continuity decision directly in the prompt. | **0** until capped launch |
|
|
88
88
|
| `start_autopilot` | Quote autopilot on an **existing** editor draft when called with only `slug`; launch only with an explicit `max_credits` that covers the estimate and an adequate balance. On a completed project, pass `restart:true` to quote and build a fresh creative run; omit it to resume pending edits. | **0** quote; credits on capped launch |
|
|
89
89
|
| `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** |
|
|
90
|
-
| `cancel_editor_batch` | Cancel an
|
|
90
|
+
| `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** |
|
|
91
91
|
| `create_slider` | Create an image-carousel draft: one prompt → N still slides + a ready-to-post caption + hashtags. `mode` `creative` (story) or `ad_driven` (facts). | **0** |
|
|
92
92
|
| `generate_slider` | Render a carousel (copy → AI backgrounds → composite). Poll `get_slider` until `completed`. | **1 / slide** (3–10) |
|
|
93
93
|
| `get_slider` | Read a carousel's status and, when completed, the per-slide image URLs + caption + hashtags. | **0** |
|
|
@@ -99,7 +99,7 @@ instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_ass
|
|
|
99
99
|
|
|
100
100
|
| Tool | What it does | Cost |
|
|
101
101
|
|---|---|---|
|
|
102
|
-
| `create_editor_draft` | Editor project, **no** autopilot. Accepts `creative_format` / `visual_language` / `theme
|
|
102
|
+
| `create_editor_draft` | Editor project, **no** autopilot. Accepts `creative_format` / `visual_language` / `theme`; the prompt is the complete scene-content contract. | **0** |
|
|
103
103
|
| `get_editor` | Full project state by default for compatibility; pass `response_format:"concise"` for a smaller review projection. | **0** |
|
|
104
104
|
| `set_scenario` / `generate_scenario` | Write your own scenario (free) **or** AI-draft it. `generate_scenario` can also persist `creative_format` / `visual_language` / `theme`. | free / 1 assist |
|
|
105
105
|
| `apply_scenario` | Compile the **ready** scenario into per-scene prompts on the timeline (`segments_count` 0 or 3..10; omitted uses the generated scenario count, then 5; AI scenes replaced, uploads preserved). Polls until applied (~2 min budget). | **0** |
|
|
@@ -108,11 +108,11 @@ instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_ass
|
|
|
108
108
|
| `generate_segment` | Render one **pending/failed** scene (re-call to retry a failed one). | **5** |
|
|
109
109
|
| `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** |
|
|
110
110
|
| `generate_all_segments` | Render every not-yet-completed scene in order (continuity preserved); capped ~280s → re-run to continue. | **5 / segment** |
|
|
111
|
-
| `set_narration_script` / `generate_narration` | Write your own voiceover script (free) **or** AI-draft
|
|
111
|
+
| `set_narration_script` / `generate_narration` | Write your own voiceover script (free) **or** AI-draft one coherent script after every scene has completed with a known positive duration. AI drafting requires at least one scene without retained original audio and consumes an assist only when a fresh job is queued (exact retries dedupe for free). | free / 1 assist |
|
|
112
112
|
| `generate_voice` | Synthesize the voiceover. `voice_id` required (see `list_voices`). | **3** |
|
|
113
113
|
| `generate_music` | Generate background music (each call is a fresh take). | **5** |
|
|
114
114
|
| `render` | Final MP4. Auto-charges any still-ungenerated scenes first (4/scene in a batch of ≥3). | **0** |
|
|
115
|
-
| `list_renders` | List every render version for an editor project (id
|
|
115
|
+
| `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** |
|
|
116
116
|
| `retry_render` | Re-run a **failed** render from its exact saved snapshot (recovery without re-driving the pipeline). | **0** |
|
|
117
117
|
| `enhance_prompt` / `suggest_next_scene` / `suggest_music_prompt` | AI helpers along the way. | 1 assist each |
|
|
118
118
|
|
|
@@ -130,8 +130,7 @@ images at **8 MiB (8,388,608 bytes)**; closing/short/catalog images at **20 MiB
|
|
|
130
130
|
| `upload_video` | Upload a local clip into an editor project (presign → PUT, resumable multipart for large files → confirm → poll to ready) and, by default, add it to the timeline. An uploaded clip keeps its native duration. |
|
|
131
131
|
| `add_segment_from_upload` | Append a **ready** upload to the timeline as a new scene. |
|
|
132
132
|
| `use_asset_for_segment` | Set a specific scene's video from a ready upload **or** a completed scene in the same project (pass exactly one). Returns downstream continuity impact plus narration/voice/music staleness that must be resolved before render. |
|
|
133
|
-
| `set_product` | Upload an editor product image (JPEG/PNG, ≤8 MiB) and
|
|
134
|
-
| `set_product_placement` | Set where the product appears on pending AI scenes: `throughout` or `end`. |
|
|
133
|
+
| `set_product` | Upload an editor product image (JPEG/PNG, ≤8 MiB). Its pixels guide product identity in every generated AI scene and its confirmed description grounds scenario/narration. The exact photo is also linked as the closing image unless a separate closing image was authored. Small generated label text can still vary. |
|
|
135
134
|
| `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`). |
|
|
136
135
|
| `set_logo` | Upload a brand logo overlay (JPEG/PNG, ≤5 MiB; editor only; optional treatment/position/duration). |
|
|
137
136
|
| `set_short_product` | Attach a product image (JPEG/PNG, ≤20 MiB) to a **short**. |
|
|
@@ -154,6 +153,25 @@ does not match the live timeline; regenerate the reported stale scenes/audio and
|
|
|
154
153
|
An absent/`null` server `is_stale` is **unknown** (for example a legacy render), not proof that
|
|
155
154
|
the render is current. Failure stages take precedence over active stages and are terminal.
|
|
156
155
|
|
|
156
|
+
Editor state uses `narration_status` (`none | generating | ready | failed`),
|
|
157
|
+
`current_audio`, and `current_music` (selected version status `pending | processing |
|
|
158
|
+
completed | failed`). Batch state is `idle | preparing | awaiting_previews | running |
|
|
159
|
+
paused_for_retry | paused_insufficient_credits | completed | cancelled | failed`.
|
|
160
|
+
Only `paused_for_retry` is resumed by `retry_editor_batch`; `cancel_editor_batch` works
|
|
161
|
+
only while the batch is `preparing` or `paused_for_retry` — awaiting-previews/running
|
|
162
|
+
batches are worker-owned and cannot be cancelled. Preparing cancellation releases the
|
|
163
|
+
uncharged preflight (nothing was charged yet); paused cancellation is destructive and
|
|
164
|
+
non-refundable. Both tools re-read the editor state and echo `expected_batch_run_id` +
|
|
165
|
+
`expected_batch_status` with the REST command, so a phase change between poll and command
|
|
166
|
+
is a safe 409 `batch_command_stale` (re-poll and retry) instead of acting on newer work;
|
|
167
|
+
the REST endpoints reject editor commands that omit these fields with 409
|
|
168
|
+
`batch_command_required` (client >= 0.12 sends them automatically). For
|
|
169
|
+
`paused_insufficient_credits`, top up or reduce scope and invoke generation/render again.
|
|
170
|
+
|
|
171
|
+
For raw REST Autopilot calls, reuse one `Idempotency-Key` only for transport retries of the
|
|
172
|
+
same logical launch. Mint a fresh key for a deliberate resume, restart, or changed cap; a
|
|
173
|
+
successful response is replayed for 24 hours under the old key.
|
|
174
|
+
|
|
157
175
|
### Campaign engine — onboard a product, plan, and draft (the delegation surface)
|
|
158
176
|
|
|
159
177
|
Everything here is **$0** (draft creates + planning against the free assist quota). Generation
|
package/dist/index.js
CHANGED
|
@@ -29555,6 +29555,7 @@ function normalizeStatus(kind, slug, data) {
|
|
|
29555
29555
|
const failedSegments = segments.filter((segment) => segment.status === "failed");
|
|
29556
29556
|
const batchStatus = d.batch_generation_status ?? null;
|
|
29557
29557
|
const batchActive = batchStatus === "running" || batchStatus === "preparing" || batchStatus === "awaiting_previews";
|
|
29558
|
+
const autoRenderHandoffActive = batchStatus === "completed" && d.auto_render_after_batch === true;
|
|
29558
29559
|
const batchRetryRequired = batchStatus === "paused_for_retry";
|
|
29559
29560
|
const batchInsufficientCredits = batchStatus === "paused_insufficient_credits";
|
|
29560
29561
|
const batchFailed = batchStatus === "failed";
|
|
@@ -29599,7 +29600,7 @@ function normalizeStatus(kind, slug, data) {
|
|
|
29599
29600
|
currentMusic.error_message
|
|
29600
29601
|
].find((value) => typeof value === "string" && value.trim().length > 0);
|
|
29601
29602
|
const audioActive = currentAudio.status === "pending" || currentAudio.status === "processing" || currentMusic.status === "pending" || currentMusic.status === "processing" || newerAudioVersions.some(versionActive) || newerMusicVersions.some(versionActive);
|
|
29602
|
-
const childWorkActive = batchActive || renderActive || scenarioActive || narrationActive || audioActive || activeSegments.length > 0;
|
|
29603
|
+
const childWorkActive = batchActive || autoRenderHandoffActive || renderActive || scenarioActive || narrationActive || audioActive || activeSegments.length > 0;
|
|
29603
29604
|
const autopilotStartedAt = typeof d.autopilot_started_at === "string" ? Date.parse(d.autopilot_started_at) : Number.NaN;
|
|
29604
29605
|
const now = Date.now();
|
|
29605
29606
|
const autopilotResumeGraceActive = autopilotActive && Number.isFinite(autopilotStartedAt) && autopilotStartedAt <= now && now - autopilotStartedAt <= 120000;
|
|
@@ -29642,6 +29643,8 @@ function normalizeStatus(kind, slug, data) {
|
|
|
29642
29643
|
stage = "segment_processing";
|
|
29643
29644
|
else if (batchActive)
|
|
29644
29645
|
stage = "segment_generation";
|
|
29646
|
+
else if (autoRenderHandoffActive)
|
|
29647
|
+
stage = "rendering";
|
|
29645
29648
|
else if (renderActive)
|
|
29646
29649
|
stage = "rendering";
|
|
29647
29650
|
else if (scenarioActive)
|
|
@@ -29661,7 +29664,7 @@ function normalizeStatus(kind, slug, data) {
|
|
|
29661
29664
|
terminal,
|
|
29662
29665
|
ready,
|
|
29663
29666
|
video_url: videoUrl,
|
|
29664
|
-
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
|
|
29667
|
+
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),
|
|
29665
29668
|
stale,
|
|
29666
29669
|
active_segment_ids: activeSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string"),
|
|
29667
29670
|
stale_segment_ids: staleSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string")
|
|
@@ -29694,10 +29697,10 @@ function makeVideoAutopilotKey(slug, a, startState) {
|
|
|
29694
29697
|
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);
|
|
29695
29698
|
}
|
|
29696
29699
|
function editorAdCreateKey(a) {
|
|
29697
|
-
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)
|
|
29700
|
+
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));
|
|
29698
29701
|
}
|
|
29699
29702
|
function editorAdAutopilotKey(slug, a) {
|
|
29700
|
-
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.
|
|
29703
|
+
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));
|
|
29701
29704
|
}
|
|
29702
29705
|
function sliderCreateKey(a) {
|
|
29703
29706
|
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 ?? "");
|
|
@@ -30254,7 +30257,7 @@ function readStoredCredentials() {
|
|
|
30254
30257
|
// package.json
|
|
30255
30258
|
var package_default = {
|
|
30256
30259
|
name: "@hubfluencer/mcp",
|
|
30257
|
-
version: "0.
|
|
30260
|
+
version: "0.12.0",
|
|
30258
30261
|
description: "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
|
|
30259
30262
|
license: "MIT",
|
|
30260
30263
|
author: "Monocursive <contact@monocursive.com>",
|
|
@@ -31566,9 +31569,9 @@ async function editorAutopilotQuote(client, slug, overrides = {}) {
|
|
|
31566
31569
|
const params = new URLSearchParams;
|
|
31567
31570
|
if (overrides.language)
|
|
31568
31571
|
params.set("language", overrides.language);
|
|
31569
|
-
if (overrides.product_subject)
|
|
31572
|
+
if (overrides.product_subject !== undefined)
|
|
31570
31573
|
params.set("product_subject", overrides.product_subject);
|
|
31571
|
-
if (overrides.product_prompt)
|
|
31574
|
+
if (overrides.product_prompt !== undefined)
|
|
31572
31575
|
params.set("product_prompt", overrides.product_prompt);
|
|
31573
31576
|
if (overrides.restart)
|
|
31574
31577
|
params.set("restart", "true");
|
|
@@ -32756,7 +32759,7 @@ registerTool("edit_slider_slide", {
|
|
|
32756
32759
|
}));
|
|
32757
32760
|
registerTool("create_editor_ad", {
|
|
32758
32761
|
title: "Create, configure, and price a multi-scene editor ad",
|
|
32759
|
-
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. " + "
|
|
32762
|
+
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.)",
|
|
32760
32763
|
inputSchema: {
|
|
32761
32764
|
product_prompt: exports_external.string().min(10).describe("Brief for the ad (min 10 chars)"),
|
|
32762
32765
|
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."),
|
|
@@ -32767,14 +32770,13 @@ registerTool("create_editor_ad", {
|
|
|
32767
32770
|
theme: exports_external.string().optional().describe('Visual theme / genre overlay (default "realistic"). One of: none (no imposed style — segments follow your prompts literally), realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.'),
|
|
32768
32771
|
voice_id: exports_external.string().optional().describe("Preferred narration voice id (see list_voices); omit for the default voice"),
|
|
32769
32772
|
export_aspect_ratio: exports_external.enum(["9:16", "16:9", "1:1"]).optional().describe('Aspect ratio (default "9:16")'),
|
|
32770
|
-
product_image_path: exports_external.string().optional().describe("Local product image (.jpg/.jpeg/.png, ≤8 MiB)
|
|
32771
|
-
product_description: exports_external.string().max(500).optional().describe("
|
|
32773
|
+
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."),
|
|
32774
|
+
product_description: exports_external.string().max(500).optional().describe("Product description used to ground the scenario and narration when product_image_path is supplied."),
|
|
32772
32775
|
closing_image_path: exports_external.string().optional().describe("Local closing-card image (.jpg/.jpeg/.png, ≤20 MiB)."),
|
|
32773
32776
|
logo_path: exports_external.string().optional().describe("Local logo (.jpg/.jpeg/.png, ≤5 MiB) to overlay."),
|
|
32774
32777
|
logo_treatment: exports_external.enum(["intro", "outro", "both", "none"]).optional().describe('When the logo bumper plays: "intro", "outro", "both" (default), or "none". Requires logo_path.'),
|
|
32775
32778
|
logo_position: exports_external.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).optional().describe('Logo corner: "top-left", "top-right" (default), "bottom-left", or "bottom-right". Requires logo_path.'),
|
|
32776
32779
|
logo_duration_seconds: exports_external.number().min(0.5).max(3).optional().describe("Logo bumper duration in seconds, 0.5–3 (default 1.5). Requires logo_path."),
|
|
32777
|
-
cast_mode: exports_external.literal("product_only").optional().describe("Lock every AI scene to the supplied product/mascot reference with no people. Requires product_image_path."),
|
|
32778
32780
|
max_credits: exports_external.number().int().nonnegative().optional().describe("Explicit spend cap. Omit to create/configure/price only; no credits are charged.")
|
|
32779
32781
|
},
|
|
32780
32782
|
outputSchema: createEditorOutput,
|
|
@@ -32791,9 +32793,6 @@ registerTool("create_editor_ad", {
|
|
|
32791
32793
|
const promptErr = validateProductPrompt(args.product_prompt);
|
|
32792
32794
|
if (promptErr)
|
|
32793
32795
|
return fail(promptErr);
|
|
32794
|
-
if (args.cast_mode === "product_only" && !args.product_image_path) {
|
|
32795
|
-
return fail("cast_mode product_only requires product_image_path so identity can be locked before generation.");
|
|
32796
|
-
}
|
|
32797
32796
|
if ((args.logo_treatment !== undefined || args.logo_position !== undefined || args.logo_duration_seconds !== undefined) && !args.logo_path) {
|
|
32798
32797
|
return fail("logo_treatment/logo_position/logo_duration_seconds require logo_path (the logo image they style). Provide logo_path or drop these settings.");
|
|
32799
32798
|
}
|
|
@@ -32821,8 +32820,7 @@ registerTool("create_editor_ad", {
|
|
|
32821
32820
|
visual_language: args.visual_language,
|
|
32822
32821
|
theme: args.theme,
|
|
32823
32822
|
voice_id: args.voice_id,
|
|
32824
|
-
export_aspect_ratio: args.export_aspect_ratio
|
|
32825
|
-
cast_mode: args.cast_mode
|
|
32823
|
+
export_aspect_ratio: args.export_aspect_ratio
|
|
32826
32824
|
}, editorAdCreateKey({
|
|
32827
32825
|
...args,
|
|
32828
32826
|
product_image_identity: productImage ? preparedUploadIdentity(productImage) : undefined,
|
|
@@ -32897,8 +32895,8 @@ registerTool("start_autopilot", {
|
|
|
32897
32895
|
inputSchema: {
|
|
32898
32896
|
slug: exports_external.string().describe("Editor project slug"),
|
|
32899
32897
|
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."),
|
|
32900
|
-
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."),
|
|
32901
|
-
product_prompt: exports_external.string().optional().describe("Optional freeform brief to set/replace before launch."),
|
|
32898
|
+
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."),
|
|
32899
|
+
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."),
|
|
32902
32900
|
max_credits: exports_external.number().int().nonnegative().optional().describe("Explicit spend cap. Omit to quote only; no credits are charged."),
|
|
32903
32901
|
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.")
|
|
32904
32902
|
},
|
|
@@ -32956,7 +32954,10 @@ registerTool("retry_editor_batch", {
|
|
|
32956
32954
|
idempotentHint: false
|
|
32957
32955
|
}
|
|
32958
32956
|
}, tool(async (args, client) => {
|
|
32959
|
-
const
|
|
32957
|
+
const command = await observedEditorBatchCommand(client, args.slug, [
|
|
32958
|
+
"paused_for_retry"
|
|
32959
|
+
]);
|
|
32960
|
+
const res = await client.post(`/video-factories/${args.slug}/retry-batch-segment`, command);
|
|
32960
32961
|
return ok({
|
|
32961
32962
|
...asRecord(asRecord(res).data ?? res),
|
|
32962
32963
|
slug: args.slug,
|
|
@@ -32967,7 +32968,7 @@ registerTool("retry_editor_batch", {
|
|
|
32967
32968
|
}));
|
|
32968
32969
|
registerTool("cancel_editor_batch", {
|
|
32969
32970
|
title: "Cancel an editor batch",
|
|
32970
|
-
description: "Cancels an editor batch
|
|
32971
|
+
description: "Cancels an editor batch only while it is preparing or paused_for_retry so the project can be edited or " + "Autopilot can be started again. Preparing cancellation releases the uncharged preflight. Cancelling a paused " + "batch is destructive: dispatched scene work is stopped and its prepaid credits are not refunded. Spends " + "0 additional credits. Scope: video:generate.",
|
|
32971
32972
|
inputSchema: { slug: exports_external.string().describe("Editor project slug") },
|
|
32972
32973
|
annotations: {
|
|
32973
32974
|
title: "Cancel editor batch",
|
|
@@ -32977,7 +32978,11 @@ registerTool("cancel_editor_batch", {
|
|
|
32977
32978
|
idempotentHint: false
|
|
32978
32979
|
}
|
|
32979
32980
|
}, tool(async (args, client) => {
|
|
32980
|
-
const
|
|
32981
|
+
const command = await observedEditorBatchCommand(client, args.slug, [
|
|
32982
|
+
"preparing",
|
|
32983
|
+
"paused_for_retry"
|
|
32984
|
+
]);
|
|
32985
|
+
const res = await client.post(`/video-factories/${args.slug}/cancel-batch`, command);
|
|
32981
32986
|
return ok({
|
|
32982
32987
|
...asRecord(asRecord(res).data ?? res),
|
|
32983
32988
|
slug: args.slug,
|
|
@@ -32985,6 +32990,22 @@ registerTool("cancel_editor_batch", {
|
|
|
32985
32990
|
next: "get_status"
|
|
32986
32991
|
});
|
|
32987
32992
|
}));
|
|
32993
|
+
async function observedEditorBatchCommand(client, slug, allowedStatuses) {
|
|
32994
|
+
const response = await client.get(`/editor/${slug}`);
|
|
32995
|
+
const state = asRecord(asRecord(response).data);
|
|
32996
|
+
const status = state.batch_generation_status;
|
|
32997
|
+
if (typeof status !== "string" || !allowedStatuses.includes(status)) {
|
|
32998
|
+
throw new Error(`Editor batch is not in an allowed phase (${allowedStatuses.join(" or ")}); refresh status before retrying.`);
|
|
32999
|
+
}
|
|
33000
|
+
const runId = state.batch_generation_run_id;
|
|
33001
|
+
if (runId !== null && (typeof runId !== "string" || runId.trim().length === 0)) {
|
|
33002
|
+
throw new Error("Editor state did not include a valid batch_generation_run_id; refresh status before retrying.");
|
|
33003
|
+
}
|
|
33004
|
+
return {
|
|
33005
|
+
expected_batch_run_id: runId,
|
|
33006
|
+
expected_batch_status: status
|
|
33007
|
+
};
|
|
33008
|
+
}
|
|
32988
33009
|
registerTool("get_ai_assists", {
|
|
32989
33010
|
title: "Get AI assist quota",
|
|
32990
33011
|
description: "Returns the free daily AI-assist quota {used, limit, bonus, remaining, resets_at, unlock_cost, " + "unlock_batch_size}. AI assists power helper calls (generate_scenario, generate_narration, " + "enhance_prompt, suggest_next_scene, suggest_music_prompt). Default limit is 20/day. When remaining " + "hits 0 those helpers return 429 — either unlock_ai_assists (1 credit → +10) or write the content " + "yourself with set_scenario / set_narration_script / set_segment_prompt (those are free, no quota).",
|
|
@@ -33011,7 +33032,6 @@ registerTool("create_editor_draft", {
|
|
|
33011
33032
|
creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Narrative arc / segment structure. Omit for Auto/generic."),
|
|
33012
33033
|
visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("Visual style direction and render look. Good default: kinetic_creator. When set it drives the look and a theme of 'none' is ignored."),
|
|
33013
33034
|
theme: exports_external.string().optional().describe('Visual theme / genre overlay (default "realistic"). One of: none (no imposed style — segments follow your prompts literally), realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.'),
|
|
33014
|
-
cast_mode: exports_external.enum(["product_only", "presenter_led"]).optional().describe("Who/what anchors every AI scene. product_only = a product-led ad with NO people; requires a " + "product image (set_product) before generation and generates every scene from persistent product " + "reference images — the best product fidelity. presenter_led = one locked presenter appears in " + "every scene; requires a presenter to be configured first (in the app / REST presenter endpoints " + "— no MCP tool yet), else generation fails with presenter_required. Omit for the default auto " + "behavior."),
|
|
33015
33035
|
voice_id: exports_external.string().regex(/^[A-Za-z0-9_-]+$/).max(64).optional().describe("Preferred narration voice id (see list_voices); ≤64 chars, [A-Za-z0-9_-] only"),
|
|
33016
33036
|
export_aspect_ratio: exports_external.enum(["9:16", "16:9", "1:1"]).optional().describe('Aspect ratio (default "9:16")'),
|
|
33017
33037
|
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).")
|
|
@@ -33030,14 +33050,13 @@ registerTool("create_editor_draft", {
|
|
|
33030
33050
|
creative_format: args.creative_format,
|
|
33031
33051
|
visual_language: args.visual_language,
|
|
33032
33052
|
theme: args.theme,
|
|
33033
|
-
cast_mode: args.cast_mode,
|
|
33034
33053
|
voice_id: args.voice_id,
|
|
33035
33054
|
export_aspect_ratio: args.export_aspect_ratio,
|
|
33036
33055
|
project_intent: args.project_intent
|
|
33037
33056
|
};
|
|
33038
33057
|
if (prompt)
|
|
33039
33058
|
body.product_prompt = prompt;
|
|
33040
|
-
const created = await client.post("/editor", body, idemKey("create-editor-draft", prompt ?? "", args.product_subject ?? "", args.project_intent ?? "", args.language ?? "en", args.creative_format ?? "", args.visual_language ?? "", args.theme ?? "", args.voice_id ?? "", args.export_aspect_ratio ?? ""
|
|
33059
|
+
const created = await client.post("/editor", body, idemKey("create-editor-draft", prompt ?? "", args.product_subject ?? "", args.project_intent ?? "", args.language ?? "en", args.creative_format ?? "", args.visual_language ?? "", args.theme ?? "", args.voice_id ?? "", args.export_aspect_ratio ?? ""));
|
|
33041
33060
|
return ok({
|
|
33042
33061
|
slug: created.data.slug,
|
|
33043
33062
|
kind: "editor",
|
|
@@ -33060,6 +33079,8 @@ function conciseEditorState(value) {
|
|
|
33060
33079
|
"scenario_apply_status",
|
|
33061
33080
|
"scenario_prompt",
|
|
33062
33081
|
"batch_generation_status",
|
|
33082
|
+
"batch_generation_run_id",
|
|
33083
|
+
"auto_render_after_batch",
|
|
33063
33084
|
"narration_status",
|
|
33064
33085
|
"narration_script",
|
|
33065
33086
|
"narration_stale",
|
|
@@ -33084,8 +33105,6 @@ function conciseEditorState(value) {
|
|
|
33084
33105
|
status: segment.status,
|
|
33085
33106
|
source_type: segment.source_type,
|
|
33086
33107
|
use_previous_frame: segment.use_previous_frame,
|
|
33087
|
-
protagonist_presence: segment.protagonist_presence,
|
|
33088
|
-
cast_mode_snapshot: segment.cast_mode_snapshot,
|
|
33089
33108
|
video_stale: segment.video_stale,
|
|
33090
33109
|
error_code: segment.error_code,
|
|
33091
33110
|
error_message: segment.error_message,
|
|
@@ -33095,7 +33114,7 @@ function conciseEditorState(value) {
|
|
|
33095
33114
|
}
|
|
33096
33115
|
registerTool("get_editor", {
|
|
33097
33116
|
title: "Get editor state (review step)",
|
|
33098
|
-
description: "Returns the full editor state by default for backward compatibility. Pass response_format:'concise' for a smaller review projection containing orchestration, scenario, scene prompts/status,
|
|
33117
|
+
description: "Returns the full editor state by default for backward compatibility. Pass response_format:'concise' for a smaller review projection containing orchestration, scenario, scene prompts/status, narration_status, current_audio, current_music, latest_render, batch_generation_status, spend, and quota.",
|
|
33099
33118
|
inputSchema: {
|
|
33100
33119
|
slug: exports_external.string().describe("Editor project slug"),
|
|
33101
33120
|
response_format: exports_external.enum(["concise", "full"]).default("full")
|
|
@@ -33124,8 +33143,8 @@ registerTool("generate_scenario", {
|
|
|
33124
33143
|
inputSchema: {
|
|
33125
33144
|
slug: exports_external.string().describe("Editor project slug"),
|
|
33126
33145
|
segments_count: exports_external.number().int().min(3).max(10).optional().describe("How many scenes (3–10, server default 5)"),
|
|
33127
|
-
creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Persist the narrative arc / segment structure before generating. Omit to leave it unchanged; pass an empty string to clear."),
|
|
33128
|
-
visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("Persist the visual style / render look before generating. When set it drives the look and a theme of 'none' is ignored."),
|
|
33146
|
+
creative_format: exports_external.union([exports_external.enum(CREATIVE_FORMATS), exports_external.literal("")]).optional().describe("Persist the narrative arc / segment structure before generating. Omit to leave it unchanged; pass an empty string to clear."),
|
|
33147
|
+
visual_language: exports_external.union([exports_external.enum(VISUAL_LANGUAGES), exports_external.literal("")]).optional().describe("Persist the visual style / render look before generating. Omit to leave it unchanged; pass an empty string to clear. When set it drives the look and a theme of 'none' is ignored."),
|
|
33129
33148
|
theme: exports_external.string().optional().describe("Persist the genre overlay / theme before generating (e.g. realistic, cinematic, anime, none)."),
|
|
33130
33149
|
auto_unlock: exports_external.boolean().optional().describe("On 429, spend 1 credit to unlock +10 assists and retry once (default false)")
|
|
33131
33150
|
},
|
|
@@ -33429,10 +33448,10 @@ registerTool("generate_all_segments", {
|
|
|
33429
33448
|
}));
|
|
33430
33449
|
registerTool("set_narration_script", {
|
|
33431
33450
|
title: "Set the narration script",
|
|
33432
|
-
description: "Writes your own narration script (1–
|
|
33451
|
+
description: "Writes your own narration script (1–10000 chars) instead of generating one. Free — no credits, no AI " + "assist. Use this in place of generate_narration when you want to control the voiceover text. Changing the narration after generating voice/music marks them stale — regenerate (generate_voice / generate_music) before render.",
|
|
33433
33452
|
inputSchema: {
|
|
33434
33453
|
slug: exports_external.string().describe("Editor project slug"),
|
|
33435
|
-
script: exports_external.string().min(1).max(
|
|
33454
|
+
script: exports_external.string().min(1).max(1e4).describe("Narration / voiceover text (1–10000 chars)")
|
|
33436
33455
|
},
|
|
33437
33456
|
annotations: { title: "Set narration", ...WRITE, idempotentHint: true }
|
|
33438
33457
|
}, tool(async (args, client) => {
|
|
@@ -33443,7 +33462,7 @@ registerTool("set_narration_script", {
|
|
|
33443
33462
|
}));
|
|
33444
33463
|
registerTool("generate_narration", {
|
|
33445
33464
|
title: "Generate the narration (AI assist)",
|
|
33446
|
-
description: "Generates
|
|
33465
|
+
description: "Generates one coherent narration script with AI from the complete timeline. Every current scene must be completed with a known positive duration, and at least one scene must not keep original upload audio. CONSUMES 1 AI ASSIST (free daily quota) only when a fresh job is queued; an exact retry is deduplicated without consuming another assist — " + "it is free of CREDITS but NOT free of quota. On 429 set auto_unlock:true (1 credit → +10 assists, " + "retried once) or write the script yourself with set_narration_script.",
|
|
33447
33466
|
inputSchema: {
|
|
33448
33467
|
slug: exports_external.string().describe("Editor project slug"),
|
|
33449
33468
|
auto_unlock: exports_external.boolean().optional().describe("On 429, spend 1 credit to unlock +10 assists and retry once (default false)")
|
|
@@ -33459,7 +33478,7 @@ registerTool("generate_narration", {
|
|
|
33459
33478
|
}));
|
|
33460
33479
|
registerTool("generate_voice", {
|
|
33461
33480
|
title: "Generate the voiceover (3 credits)",
|
|
33462
|
-
description: "Synthesizes the voiceover from the narration script. Costs 3 credits. Safe to retry; re-call after changing the narration or voice to regenerate." + "voice_id is REQUIRED — pick one from list_voices (≤64 chars, [A-Za-z0-9_-] only). 402 fails cleanly. Editing the scenario/narration/timeline after this marks the voice stale, and render will 422 (editor_voice_stale) until you re-run generate_voice.",
|
|
33481
|
+
description: "Synthesizes the voiceover from the narration script. Costs 3 credits. Safe to retry; re-call after changing the narration or voice to regenerate." + " All current scenes must be completed with known durations first. voice_id is REQUIRED — pick one from list_voices (≤64 chars, [A-Za-z0-9_-] only). 402 fails cleanly. Editing the scenario/narration/timeline after this marks the voice stale, and render will 422 (editor_voice_stale) until you re-run generate_voice.",
|
|
33463
33482
|
inputSchema: {
|
|
33464
33483
|
slug: exports_external.string().describe("Editor project slug"),
|
|
33465
33484
|
voice_id: exports_external.string().regex(/^[A-Za-z0-9_-]+$/).max(64).describe("Narration voice id from list_voices (required)")
|
|
@@ -33571,7 +33590,7 @@ registerTool("suggest_music_prompt", {
|
|
|
33571
33590
|
}));
|
|
33572
33591
|
registerTool("list_renders", {
|
|
33573
33592
|
title: "List renders for an editor project",
|
|
33574
|
-
description: "Lists every render version for an editor project: {id, version, status, video_url,
|
|
33593
|
+
description: "Lists 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}. Use it to find a " + "FAILED render to retry, or to recover a finished video_url (presigned, ~24h). status is pending | " + "processing | completed | failed; is_stale null means unknown, not current. Read-only.",
|
|
33575
33594
|
inputSchema: { slug: exports_external.string().describe("Editor project slug") },
|
|
33576
33595
|
annotations: { title: "List renders", ...RO }
|
|
33577
33596
|
}, tool(async (args, client) => {
|
|
@@ -33744,7 +33763,7 @@ registerTool("use_asset_for_segment", {
|
|
|
33744
33763
|
}));
|
|
33745
33764
|
registerTool("set_product", {
|
|
33746
33765
|
title: "Attach a product image (from a local file)",
|
|
33747
|
-
description: "Uploads a local product image and attaches it to the project as the product. Accepted: " + IMAGE_EXTS.map((e) => `.${e}`).join(", ") + " (≤8 MiB), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits.
|
|
33766
|
+
description: "Uploads a local product image and attaches it to the project as the product. Accepted: " + IMAGE_EXTS.map((e) => `.${e}`).join(", ") + " (≤8 MiB), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits. Its pixels guide product identity " + "in every generated Editor scene and its confirmed description grounds scenario and narration. The exact photo " + "is linked as the closing image unless a separate closing image was authored; generated fine label text can still vary.",
|
|
33748
33767
|
inputSchema: {
|
|
33749
33768
|
slug: exports_external.string().describe("Editor project slug"),
|
|
33750
33769
|
file_path: exports_external.string().describe("Local product image path (.jpg/.jpeg/.png)"),
|
|
@@ -33756,22 +33775,6 @@ registerTool("set_product", {
|
|
|
33756
33775
|
const res = await client.post(`/editor/${args.slug}/product/confirm`, { s3_key, product_description: args.description });
|
|
33757
33776
|
return ok(asRecord(res).data ?? res);
|
|
33758
33777
|
}));
|
|
33759
|
-
registerTool("set_product_placement", {
|
|
33760
|
-
title: "Set default product placement on pending scenes (0 credits)",
|
|
33761
|
-
description: "Sets where the product appears across pending (never-generated) AI scenes. mode is 'throughout' (every scene) " + "or 'end' (closing scene only; requires a first+last-frame capable model). 0 credits. Add a product first with " + "set_product. (There is no 'none' — to remove the product, that's a separate action.)",
|
|
33762
|
-
inputSchema: {
|
|
33763
|
-
slug: exports_external.string().describe("Editor project slug"),
|
|
33764
|
-
mode: exports_external.enum(["throughout", "end"]).describe("'throughout' or 'end'")
|
|
33765
|
-
},
|
|
33766
|
-
annotations: {
|
|
33767
|
-
title: "Set product placement",
|
|
33768
|
-
...WRITE,
|
|
33769
|
-
idempotentHint: true
|
|
33770
|
-
}
|
|
33771
|
-
}, tool(async (args, client) => {
|
|
33772
|
-
const res = await client.post(`/editor/${args.slug}/product/apply-default-placement`, { placement: args.mode });
|
|
33773
|
-
return ok(asRecord(res).data ?? res);
|
|
33774
|
-
}));
|
|
33775
33778
|
registerTool("set_closing_image", {
|
|
33776
33779
|
title: "Set the closing-card image (0 credits)",
|
|
33777
33780
|
description: "Sets the optional ~2s end-card image. Either upload a local file (file_path; .jpg/.jpeg/.png ≤20 MiB, confined " + "to HUBFLUENCER_INPUT_DIR/cwd) OR reuse the project's product image (from_product:true — a 0-credit server-side " + "copy, no upload). If a closing image already exists, pass overwrite:true to replace it (else " + "editor_closing_image_exists).",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubfluencer/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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
|
@@ -464,6 +464,13 @@ export function normalizeStatus(
|
|
|
464
464
|
batchStatus === "running" ||
|
|
465
465
|
batchStatus === "preparing" ||
|
|
466
466
|
batchStatus === "awaiting_previews";
|
|
467
|
+
// The scene batch commits `completed` before the durable auto-render handoff
|
|
468
|
+
// creates its VideoResult row. While the persisted intent remains armed, the
|
|
469
|
+
// server still owns the project and a reconciler may create the render at any
|
|
470
|
+
// moment. Treat that narrow gap as active work so wait_for_completion cannot
|
|
471
|
+
// return an idle/terminal result just before the render appears.
|
|
472
|
+
const autoRenderHandoffActive =
|
|
473
|
+
batchStatus === "completed" && d.auto_render_after_batch === true;
|
|
467
474
|
const batchRetryRequired = batchStatus === "paused_for_retry";
|
|
468
475
|
// A parked batch that ran out of credits: it cannot progress without the user
|
|
469
476
|
// topping up or reducing scope, so it is a needs-user-action TERMINAL state
|
|
@@ -567,6 +574,7 @@ export function normalizeStatus(
|
|
|
567
574
|
newerMusicVersions.some(versionActive);
|
|
568
575
|
const childWorkActive =
|
|
569
576
|
batchActive ||
|
|
577
|
+
autoRenderHandoffActive ||
|
|
570
578
|
renderActive ||
|
|
571
579
|
scenarioActive ||
|
|
572
580
|
narrationActive ||
|
|
@@ -670,6 +678,7 @@ export function normalizeStatus(
|
|
|
670
678
|
else if (batchInsufficientCredits) stage = "insufficient_credits";
|
|
671
679
|
else if (activeSegments.length > 0) stage = "segment_processing";
|
|
672
680
|
else if (batchActive) stage = "segment_generation";
|
|
681
|
+
else if (autoRenderHandoffActive) stage = "rendering";
|
|
673
682
|
else if (renderActive) stage = "rendering";
|
|
674
683
|
else if (scenarioActive) stage = "scenario_processing";
|
|
675
684
|
else if (narrationActive) stage = "narration_processing";
|
|
@@ -705,7 +714,7 @@ export function normalizeStatus(
|
|
|
705
714
|
: null) ??
|
|
706
715
|
actionableChildError ??
|
|
707
716
|
(batchInsufficientCredits
|
|
708
|
-
? "Batch generation paused: not enough credits to finish. Top up or reduce scope, then
|
|
717
|
+
? "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."
|
|
709
718
|
: null)),
|
|
710
719
|
stale,
|
|
711
720
|
active_segment_ids: activeSegments
|
|
@@ -856,7 +865,6 @@ export interface EditorAdKeyArgs {
|
|
|
856
865
|
logo_treatment?: string;
|
|
857
866
|
logo_position?: string;
|
|
858
867
|
logo_duration_seconds?: number;
|
|
859
|
-
cast_mode?: string;
|
|
860
868
|
/** Identities of the retained, pre-create asset byte snapshots. */
|
|
861
869
|
product_image_identity?: string;
|
|
862
870
|
closing_image_identity?: string;
|
|
@@ -896,7 +904,6 @@ export function editorAdCreateKey(a: EditorAdKeyArgs): string {
|
|
|
896
904
|
a.logo_duration_seconds === undefined
|
|
897
905
|
? ""
|
|
898
906
|
: String(a.logo_duration_seconds),
|
|
899
|
-
a.cast_mode ?? "",
|
|
900
907
|
);
|
|
901
908
|
}
|
|
902
909
|
|
|
@@ -931,7 +938,6 @@ export function editorAdAutopilotKey(slug: string, a: EditorAdKeyArgs): string {
|
|
|
931
938
|
a.logo_duration_seconds === undefined
|
|
932
939
|
? ""
|
|
933
940
|
: String(a.logo_duration_seconds),
|
|
934
|
-
a.cast_mode ?? "",
|
|
935
941
|
a.max_credits === undefined ? "" : String(a.max_credits),
|
|
936
942
|
);
|
|
937
943
|
}
|
|
@@ -1076,10 +1082,9 @@ export function validateLanguage(language: string | undefined): string | null {
|
|
|
1076
1082
|
export function validateProductPrompt(
|
|
1077
1083
|
prompt: string | undefined,
|
|
1078
1084
|
): string | null {
|
|
1079
|
-
//
|
|
1080
|
-
//
|
|
1081
|
-
//
|
|
1082
|
-
// as undefined. Still reject 1–9 non-blank chars (too short to be a brief).
|
|
1085
|
+
// Empty is valid but its meaning is endpoint-specific: draft creation omits
|
|
1086
|
+
// it, while Autopilot treats an explicitly present blank as an atomic clear.
|
|
1087
|
+
// Still reject 1–9 non-blank chars (too short to be a brief).
|
|
1083
1088
|
if (prompt === undefined) return null;
|
|
1084
1089
|
const len = prompt.trim().length;
|
|
1085
1090
|
if (len === 0) return null;
|
package/src/index.ts
CHANGED
|
@@ -171,9 +171,9 @@ async function editorAutopilotQuote(
|
|
|
171
171
|
try {
|
|
172
172
|
const params = new URLSearchParams();
|
|
173
173
|
if (overrides.language) params.set("language", overrides.language);
|
|
174
|
-
if (overrides.product_subject)
|
|
174
|
+
if (overrides.product_subject !== undefined)
|
|
175
175
|
params.set("product_subject", overrides.product_subject);
|
|
176
|
-
if (overrides.product_prompt)
|
|
176
|
+
if (overrides.product_prompt !== undefined)
|
|
177
177
|
params.set("product_prompt", overrides.product_prompt);
|
|
178
178
|
if (overrides.restart) params.set("restart", "true");
|
|
179
179
|
const query = params.toString();
|
|
@@ -3020,12 +3020,12 @@ registerTool(
|
|
|
3020
3020
|
"Without max_credits it STOPS after the free draft/configuration and returns the live estimate. It starts " +
|
|
3021
3021
|
"the paid pipeline only when max_credits is supplied and covers the estimate. Then poll with get_status / " +
|
|
3022
3022
|
"wait_for_completion (kind=editor). Prefer make_video for a prompt-only one-shot. " +
|
|
3023
|
-
"
|
|
3024
|
-
"
|
|
3025
|
-
"
|
|
3026
|
-
"
|
|
3027
|
-
"
|
|
3028
|
-
"
|
|
3023
|
+
"A product image is analyzed into semantic project context and sent as a product-identity reference to every " +
|
|
3024
|
+
"generated Editor scene. The exact photo is also linked as the closing image unless a separate closing image " +
|
|
3025
|
+
"was authored; generated fine label text can still vary. Scene prompts are the complete content contract: describe any people, " +
|
|
3026
|
+
"products, visual continuity, absences, and composition directly in the brief. (Editor ads carry their copy " +
|
|
3027
|
+
"in-scene/narration, not as a title overlay; for an on-screen title/subtitle use a short with " +
|
|
3028
|
+
"headline/subheadline instead.)",
|
|
3029
3029
|
inputSchema: {
|
|
3030
3030
|
product_prompt: z
|
|
3031
3031
|
.string()
|
|
@@ -3085,14 +3085,14 @@ registerTool(
|
|
|
3085
3085
|
.string()
|
|
3086
3086
|
.optional()
|
|
3087
3087
|
.describe(
|
|
3088
|
-
"Local product image (.jpg/.jpeg/.png, ≤8 MiB)
|
|
3088
|
+
"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.",
|
|
3089
3089
|
),
|
|
3090
3090
|
product_description: z
|
|
3091
3091
|
.string()
|
|
3092
3092
|
.max(500)
|
|
3093
3093
|
.optional()
|
|
3094
3094
|
.describe(
|
|
3095
|
-
"
|
|
3095
|
+
"Product description used to ground the scenario and narration when product_image_path is supplied.",
|
|
3096
3096
|
),
|
|
3097
3097
|
closing_image_path: z
|
|
3098
3098
|
.string()
|
|
@@ -3122,12 +3122,6 @@ registerTool(
|
|
|
3122
3122
|
.describe(
|
|
3123
3123
|
"Logo bumper duration in seconds, 0.5–3 (default 1.5). Requires logo_path.",
|
|
3124
3124
|
),
|
|
3125
|
-
cast_mode: z
|
|
3126
|
-
.literal("product_only")
|
|
3127
|
-
.optional()
|
|
3128
|
-
.describe(
|
|
3129
|
-
"Lock every AI scene to the supplied product/mascot reference with no people. Requires product_image_path.",
|
|
3130
|
-
),
|
|
3131
3125
|
max_credits: z
|
|
3132
3126
|
.number()
|
|
3133
3127
|
.int()
|
|
@@ -3163,7 +3157,6 @@ registerTool(
|
|
|
3163
3157
|
| "bottom-left"
|
|
3164
3158
|
| "bottom-right";
|
|
3165
3159
|
logo_duration_seconds?: number;
|
|
3166
|
-
cast_mode?: "product_only";
|
|
3167
3160
|
max_credits?: number;
|
|
3168
3161
|
},
|
|
3169
3162
|
client,
|
|
@@ -3177,11 +3170,6 @@ registerTool(
|
|
|
3177
3170
|
if (langErr) return fail(langErr);
|
|
3178
3171
|
const promptErr = validateProductPrompt(args.product_prompt);
|
|
3179
3172
|
if (promptErr) return fail(promptErr);
|
|
3180
|
-
if (args.cast_mode === "product_only" && !args.product_image_path) {
|
|
3181
|
-
return fail(
|
|
3182
|
-
"cast_mode product_only requires product_image_path so identity can be locked before generation.",
|
|
3183
|
-
);
|
|
3184
|
-
}
|
|
3185
3173
|
// Fail fast on settings that would otherwise be silently dropped: the
|
|
3186
3174
|
// logo PATCH is gated on logo_path and the product_description is only
|
|
3187
3175
|
// sent with product_image_path, so a logo_* / product_description passed
|
|
@@ -3241,7 +3229,6 @@ registerTool(
|
|
|
3241
3229
|
theme: args.theme,
|
|
3242
3230
|
voice_id: args.voice_id,
|
|
3243
3231
|
export_aspect_ratio: args.export_aspect_ratio,
|
|
3244
|
-
cast_mode: args.cast_mode,
|
|
3245
3232
|
},
|
|
3246
3233
|
// Fold every create-affecting param into the key so two calls that
|
|
3247
3234
|
// differ only by style/theme/voice/aspect get distinct drafts.
|
|
@@ -3401,12 +3388,14 @@ registerTool(
|
|
|
3401
3388
|
.optional()
|
|
3402
3389
|
.describe(
|
|
3403
3390
|
"Optional locked product / main subject applied before launch so the AI cannot invent an " +
|
|
3404
|
-
"unrelated product.",
|
|
3391
|
+
"unrelated product. Omit to keep the stored subject; send blank to clear it.",
|
|
3405
3392
|
),
|
|
3406
3393
|
product_prompt: z
|
|
3407
3394
|
.string()
|
|
3408
3395
|
.optional()
|
|
3409
|
-
.describe(
|
|
3396
|
+
.describe(
|
|
3397
|
+
"Optional freeform brief to set/replace before launch. Omit to keep the stored brief; send blank to clear it.",
|
|
3398
|
+
),
|
|
3410
3399
|
max_credits: z
|
|
3411
3400
|
.number()
|
|
3412
3401
|
.int()
|
|
@@ -3530,8 +3519,12 @@ registerTool(
|
|
|
3530
3519
|
},
|
|
3531
3520
|
},
|
|
3532
3521
|
tool(async (args: { slug: string }, client) => {
|
|
3522
|
+
const command = await observedEditorBatchCommand(client, args.slug, [
|
|
3523
|
+
"paused_for_retry",
|
|
3524
|
+
]);
|
|
3533
3525
|
const res = await client.post<{ data: unknown }>(
|
|
3534
3526
|
`/video-factories/${args.slug}/retry-batch-segment`,
|
|
3527
|
+
command,
|
|
3535
3528
|
);
|
|
3536
3529
|
return ok({
|
|
3537
3530
|
...asRecord(asRecord(res).data ?? res),
|
|
@@ -3548,9 +3541,9 @@ registerTool(
|
|
|
3548
3541
|
{
|
|
3549
3542
|
title: "Cancel an editor batch",
|
|
3550
3543
|
description:
|
|
3551
|
-
"Cancels an editor batch
|
|
3552
|
-
"Autopilot can be started again.
|
|
3553
|
-
"
|
|
3544
|
+
"Cancels an editor batch only while it is preparing or paused_for_retry so the project can be edited or " +
|
|
3545
|
+
"Autopilot can be started again. Preparing cancellation releases the uncharged preflight. Cancelling a paused " +
|
|
3546
|
+
"batch is destructive: dispatched scene work is stopped and its prepaid credits are not refunded. Spends " +
|
|
3554
3547
|
"0 additional credits. Scope: video:generate.",
|
|
3555
3548
|
inputSchema: { slug: z.string().describe("Editor project slug") },
|
|
3556
3549
|
annotations: {
|
|
@@ -3562,8 +3555,13 @@ registerTool(
|
|
|
3562
3555
|
},
|
|
3563
3556
|
},
|
|
3564
3557
|
tool(async (args: { slug: string }, client) => {
|
|
3558
|
+
const command = await observedEditorBatchCommand(client, args.slug, [
|
|
3559
|
+
"preparing",
|
|
3560
|
+
"paused_for_retry",
|
|
3561
|
+
]);
|
|
3565
3562
|
const res = await client.post<{ data: unknown }>(
|
|
3566
3563
|
`/video-factories/${args.slug}/cancel-batch`,
|
|
3564
|
+
command,
|
|
3567
3565
|
);
|
|
3568
3566
|
return ok({
|
|
3569
3567
|
...asRecord(asRecord(res).data ?? res),
|
|
@@ -3574,6 +3572,50 @@ registerTool(
|
|
|
3574
3572
|
}),
|
|
3575
3573
|
);
|
|
3576
3574
|
|
|
3575
|
+
type EditorBatchCommandStatus = "preparing" | "paused_for_retry";
|
|
3576
|
+
|
|
3577
|
+
/**
|
|
3578
|
+
* Reads the exact Editor batch ownership token immediately before issuing a
|
|
3579
|
+
* recovery command. The API validates both fields under the factory row lock,
|
|
3580
|
+
* so a replacement run or phase transition between this GET and the POST is a
|
|
3581
|
+
* safe `batch_command_stale` conflict rather than acting on newer work.
|
|
3582
|
+
*/
|
|
3583
|
+
async function observedEditorBatchCommand(
|
|
3584
|
+
client: HubfluencerClient,
|
|
3585
|
+
slug: string,
|
|
3586
|
+
allowedStatuses: readonly EditorBatchCommandStatus[],
|
|
3587
|
+
): Promise<{
|
|
3588
|
+
expected_batch_run_id: string | null;
|
|
3589
|
+
expected_batch_status: EditorBatchCommandStatus;
|
|
3590
|
+
}> {
|
|
3591
|
+
const response = await client.get<{ data: unknown }>(`/editor/${slug}`);
|
|
3592
|
+
const state = asRecord(asRecord(response).data);
|
|
3593
|
+
const status = state.batch_generation_status;
|
|
3594
|
+
if (
|
|
3595
|
+
typeof status !== "string" ||
|
|
3596
|
+
!allowedStatuses.includes(status as EditorBatchCommandStatus)
|
|
3597
|
+
) {
|
|
3598
|
+
throw new Error(
|
|
3599
|
+
`Editor batch is not in an allowed phase (${allowedStatuses.join(" or ")}); refresh status before retrying.`,
|
|
3600
|
+
);
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
const runId = state.batch_generation_run_id;
|
|
3604
|
+
if (
|
|
3605
|
+
runId !== null &&
|
|
3606
|
+
(typeof runId !== "string" || runId.trim().length === 0)
|
|
3607
|
+
) {
|
|
3608
|
+
throw new Error(
|
|
3609
|
+
"Editor state did not include a valid batch_generation_run_id; refresh status before retrying.",
|
|
3610
|
+
);
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
return {
|
|
3614
|
+
expected_batch_run_id: runId,
|
|
3615
|
+
expected_batch_status: status as EditorBatchCommandStatus,
|
|
3616
|
+
};
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3577
3619
|
// ── AI assists (free daily quota) ───────────────────────────────────────────
|
|
3578
3620
|
|
|
3579
3621
|
registerTool(
|
|
@@ -3676,17 +3718,6 @@ registerTool(
|
|
|
3676
3718
|
.describe(
|
|
3677
3719
|
'Visual theme / genre overlay (default "realistic"). One of: none (no imposed style — segments follow your prompts literally), realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.',
|
|
3678
3720
|
),
|
|
3679
|
-
cast_mode: z
|
|
3680
|
-
.enum(["product_only", "presenter_led"])
|
|
3681
|
-
.optional()
|
|
3682
|
-
.describe(
|
|
3683
|
-
"Who/what anchors every AI scene. product_only = a product-led ad with NO people; requires a " +
|
|
3684
|
-
"product image (set_product) before generation and generates every scene from persistent product " +
|
|
3685
|
-
"reference images — the best product fidelity. presenter_led = one locked presenter appears in " +
|
|
3686
|
-
"every scene; requires a presenter to be configured first (in the app / REST presenter endpoints " +
|
|
3687
|
-
"— no MCP tool yet), else generation fails with presenter_required. Omit for the default auto " +
|
|
3688
|
-
"behavior.",
|
|
3689
|
-
),
|
|
3690
3721
|
voice_id: z
|
|
3691
3722
|
.string()
|
|
3692
3723
|
.regex(/^[A-Za-z0-9_-]+$/)
|
|
@@ -3723,7 +3754,6 @@ registerTool(
|
|
|
3723
3754
|
creative_format?: string;
|
|
3724
3755
|
visual_language?: string;
|
|
3725
3756
|
theme?: string;
|
|
3726
|
-
cast_mode?: string;
|
|
3727
3757
|
voice_id?: string;
|
|
3728
3758
|
export_aspect_ratio?: string;
|
|
3729
3759
|
project_intent?: string;
|
|
@@ -3737,7 +3767,6 @@ registerTool(
|
|
|
3737
3767
|
creative_format: args.creative_format,
|
|
3738
3768
|
visual_language: args.visual_language,
|
|
3739
3769
|
theme: args.theme,
|
|
3740
|
-
cast_mode: args.cast_mode,
|
|
3741
3770
|
voice_id: args.voice_id,
|
|
3742
3771
|
export_aspect_ratio: args.export_aspect_ratio,
|
|
3743
3772
|
project_intent: args.project_intent,
|
|
@@ -3760,7 +3789,6 @@ registerTool(
|
|
|
3760
3789
|
args.theme ?? "",
|
|
3761
3790
|
args.voice_id ?? "",
|
|
3762
3791
|
args.export_aspect_ratio ?? "",
|
|
3763
|
-
args.cast_mode ?? "",
|
|
3764
3792
|
),
|
|
3765
3793
|
);
|
|
3766
3794
|
return ok({
|
|
@@ -3788,6 +3816,8 @@ function conciseEditorState(value: unknown): Record<string, unknown> {
|
|
|
3788
3816
|
"scenario_apply_status",
|
|
3789
3817
|
"scenario_prompt",
|
|
3790
3818
|
"batch_generation_status",
|
|
3819
|
+
"batch_generation_run_id",
|
|
3820
|
+
"auto_render_after_batch",
|
|
3791
3821
|
"narration_status",
|
|
3792
3822
|
"narration_script",
|
|
3793
3823
|
"narration_stale",
|
|
@@ -3812,8 +3842,6 @@ function conciseEditorState(value: unknown): Record<string, unknown> {
|
|
|
3812
3842
|
status: segment.status,
|
|
3813
3843
|
source_type: segment.source_type,
|
|
3814
3844
|
use_previous_frame: segment.use_previous_frame,
|
|
3815
|
-
protagonist_presence: segment.protagonist_presence,
|
|
3816
|
-
cast_mode_snapshot: segment.cast_mode_snapshot,
|
|
3817
3845
|
video_stale: segment.video_stale,
|
|
3818
3846
|
error_code: segment.error_code,
|
|
3819
3847
|
error_message: segment.error_message,
|
|
@@ -3828,7 +3856,7 @@ registerTool(
|
|
|
3828
3856
|
{
|
|
3829
3857
|
title: "Get editor state (review step)",
|
|
3830
3858
|
description:
|
|
3831
|
-
"Returns the full editor state by default for backward compatibility. Pass response_format:'concise' for a smaller review projection containing orchestration, scenario, scene prompts/status,
|
|
3859
|
+
"Returns the full editor state by default for backward compatibility. Pass response_format:'concise' for a smaller review projection containing orchestration, scenario, scene prompts/status, narration_status, current_audio, current_music, latest_render, batch_generation_status, spend, and quota.",
|
|
3832
3860
|
inputSchema: {
|
|
3833
3861
|
slug: z.string().describe("Editor project slug"),
|
|
3834
3862
|
response_format: z.enum(["concise", "full"]).default("full"),
|
|
@@ -3898,16 +3926,16 @@ registerTool(
|
|
|
3898
3926
|
.optional()
|
|
3899
3927
|
.describe("How many scenes (3–10, server default 5)"),
|
|
3900
3928
|
creative_format: z
|
|
3901
|
-
.enum(CREATIVE_FORMATS)
|
|
3929
|
+
.union([z.enum(CREATIVE_FORMATS), z.literal("")])
|
|
3902
3930
|
.optional()
|
|
3903
3931
|
.describe(
|
|
3904
3932
|
"Persist the narrative arc / segment structure before generating. Omit to leave it unchanged; pass an empty string to clear.",
|
|
3905
3933
|
),
|
|
3906
3934
|
visual_language: z
|
|
3907
|
-
.enum(VISUAL_LANGUAGES)
|
|
3935
|
+
.union([z.enum(VISUAL_LANGUAGES), z.literal("")])
|
|
3908
3936
|
.optional()
|
|
3909
3937
|
.describe(
|
|
3910
|
-
"Persist the visual style / render look before generating. When set it drives the look and a theme of 'none' is ignored.",
|
|
3938
|
+
"Persist the visual style / render look before generating. Omit to leave it unchanged; pass an empty string to clear. When set it drives the look and a theme of 'none' is ignored.",
|
|
3911
3939
|
),
|
|
3912
3940
|
theme: z
|
|
3913
3941
|
.string()
|
|
@@ -4479,15 +4507,15 @@ registerTool(
|
|
|
4479
4507
|
{
|
|
4480
4508
|
title: "Set the narration script",
|
|
4481
4509
|
description:
|
|
4482
|
-
"Writes your own narration script (1–
|
|
4510
|
+
"Writes your own narration script (1–10000 chars) instead of generating one. Free — no credits, no AI " +
|
|
4483
4511
|
"assist. Use this in place of generate_narration when you want to control the voiceover text. Changing the narration after generating voice/music marks them stale — regenerate (generate_voice / generate_music) before render.",
|
|
4484
4512
|
inputSchema: {
|
|
4485
4513
|
slug: z.string().describe("Editor project slug"),
|
|
4486
4514
|
script: z
|
|
4487
4515
|
.string()
|
|
4488
4516
|
.min(1)
|
|
4489
|
-
.max(
|
|
4490
|
-
.describe("Narration / voiceover text (1–
|
|
4517
|
+
.max(10000)
|
|
4518
|
+
.describe("Narration / voiceover text (1–10000 chars)"),
|
|
4491
4519
|
},
|
|
4492
4520
|
annotations: { title: "Set narration", ...WRITE, idempotentHint: true },
|
|
4493
4521
|
},
|
|
@@ -4504,7 +4532,7 @@ registerTool(
|
|
|
4504
4532
|
{
|
|
4505
4533
|
title: "Generate the narration (AI assist)",
|
|
4506
4534
|
description:
|
|
4507
|
-
"Generates
|
|
4535
|
+
"Generates one coherent narration script with AI from the complete timeline. Every current scene must be completed with a known positive duration, and at least one scene must not keep original upload audio. CONSUMES 1 AI ASSIST (free daily quota) only when a fresh job is queued; an exact retry is deduplicated without consuming another assist — " +
|
|
4508
4536
|
"it is free of CREDITS but NOT free of quota. On 429 set auto_unlock:true (1 credit → +10 assists, " +
|
|
4509
4537
|
"retried once) or write the script yourself with set_narration_script.",
|
|
4510
4538
|
inputSchema: {
|
|
@@ -4536,7 +4564,7 @@ registerTool(
|
|
|
4536
4564
|
title: "Generate the voiceover (3 credits)",
|
|
4537
4565
|
description:
|
|
4538
4566
|
"Synthesizes the voiceover from the narration script. Costs 3 credits. Safe to retry; re-call after changing the narration or voice to regenerate." +
|
|
4539
|
-
"voice_id is REQUIRED — pick one from list_voices (≤64 chars, [A-Za-z0-9_-] only). 402 fails cleanly. Editing the scenario/narration/timeline after this marks the voice stale, and render will 422 (editor_voice_stale) until you re-run generate_voice.",
|
|
4567
|
+
" All current scenes must be completed with known durations first. voice_id is REQUIRED — pick one from list_voices (≤64 chars, [A-Za-z0-9_-] only). 402 fails cleanly. Editing the scenario/narration/timeline after this marks the voice stale, and render will 422 (editor_voice_stale) until you re-run generate_voice.",
|
|
4540
4568
|
inputSchema: {
|
|
4541
4569
|
slug: z.string().describe("Editor project slug"),
|
|
4542
4570
|
voice_id: z
|
|
@@ -4775,9 +4803,10 @@ registerTool(
|
|
|
4775
4803
|
{
|
|
4776
4804
|
title: "List renders for an editor project",
|
|
4777
4805
|
description:
|
|
4778
|
-
"Lists every render version for an editor project: {id, version, status, video_url,
|
|
4779
|
-
"error_message, inserted_at}. Use it to find a
|
|
4780
|
-
"(presigned, ~24h).
|
|
4806
|
+
"Lists every render version for an editor project: {id, version, status, video_url, thumbnail_url, " +
|
|
4807
|
+
"duration_seconds, error_message, retryable, combination_hash, is_stale, inserted_at}. Use it to find a " +
|
|
4808
|
+
"FAILED render to retry, or to recover a finished video_url (presigned, ~24h). status is pending | " +
|
|
4809
|
+
"processing | completed | failed; is_stale null means unknown, not current. Read-only.",
|
|
4781
4810
|
inputSchema: { slug: z.string().describe("Editor project slug") },
|
|
4782
4811
|
annotations: { title: "List renders", ...RO },
|
|
4783
4812
|
},
|
|
@@ -5129,9 +5158,9 @@ registerTool(
|
|
|
5129
5158
|
description:
|
|
5130
5159
|
"Uploads a local product image and attaches it to the project as the product. Accepted: " +
|
|
5131
5160
|
IMAGE_EXTS.map((e) => `.${e}`).join(", ") +
|
|
5132
|
-
" (≤8 MiB), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits.
|
|
5133
|
-
"
|
|
5134
|
-
"
|
|
5161
|
+
" (≤8 MiB), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits. Its pixels guide product identity " +
|
|
5162
|
+
"in every generated Editor scene and its confirmed description grounds scenario and narration. The exact photo " +
|
|
5163
|
+
"is linked as the closing image unless a separate closing image was authored; generated fine label text can still vary.",
|
|
5135
5164
|
inputSchema: {
|
|
5136
5165
|
slug: z.string().describe("Editor project slug"),
|
|
5137
5166
|
file_path: z
|
|
@@ -5165,33 +5194,6 @@ registerTool(
|
|
|
5165
5194
|
),
|
|
5166
5195
|
);
|
|
5167
5196
|
|
|
5168
|
-
registerTool(
|
|
5169
|
-
"set_product_placement",
|
|
5170
|
-
{
|
|
5171
|
-
title: "Set default product placement on pending scenes (0 credits)",
|
|
5172
|
-
description:
|
|
5173
|
-
"Sets where the product appears across pending (never-generated) AI scenes. mode is 'throughout' (every scene) " +
|
|
5174
|
-
"or 'end' (closing scene only; requires a first+last-frame capable model). 0 credits. Add a product first with " +
|
|
5175
|
-
"set_product. (There is no 'none' — to remove the product, that's a separate action.)",
|
|
5176
|
-
inputSchema: {
|
|
5177
|
-
slug: z.string().describe("Editor project slug"),
|
|
5178
|
-
mode: z.enum(["throughout", "end"]).describe("'throughout' or 'end'"),
|
|
5179
|
-
},
|
|
5180
|
-
annotations: {
|
|
5181
|
-
title: "Set product placement",
|
|
5182
|
-
...WRITE,
|
|
5183
|
-
idempotentHint: true,
|
|
5184
|
-
},
|
|
5185
|
-
},
|
|
5186
|
-
tool(async (args: { slug: string; mode: "throughout" | "end" }, client) => {
|
|
5187
|
-
const res = await client.post<{ data: unknown }>(
|
|
5188
|
-
`/editor/${args.slug}/product/apply-default-placement`,
|
|
5189
|
-
{ placement: args.mode },
|
|
5190
|
-
);
|
|
5191
|
-
return ok(asRecord(res).data ?? res);
|
|
5192
|
-
}),
|
|
5193
|
-
);
|
|
5194
|
-
|
|
5195
5197
|
registerTool(
|
|
5196
5198
|
"set_closing_image",
|
|
5197
5199
|
{
|