@hubfluencer/mcp 0.7.1 → 0.8.1

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 CHANGED
@@ -68,7 +68,7 @@ claude mcp add hubfluencer --env HUBFLUENCER_API_TOKEN=YOUR_TOKEN -- npx -y @hub
68
68
 
69
69
  ## Tools
70
70
 
71
- 89 tools, grouped by workflow. Every tool prices its action in its own `description` — costs
71
+ 91 tools, grouped by workflow. Every tool prices its action in its own `description` — costs
72
72
  below are the ground truth. **"$0" means it spends no video credits**; planning/AI-helper tools
73
73
  instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_assists`: 1 credit
74
74
  → +10). Reads need the `video:read` scope; writes need `video:generate`. Nothing here needs
@@ -83,7 +83,7 @@ instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_ass
83
83
  | `update_short` | Patch an existing short draft — brief (`product_prompt`), copy, styling, or the opt-in conversion graphics; only the fields you pass change. Clear one with `""` (`[]` for `text_beats`, `0` for `star_rating`). Re-run `generate_short` to apply. | **0** |
84
84
  | `generate_short` | Render an existing short. Safe to re-call (in-flight = reported, no double charge; a failed short can re-generate). | **15** |
85
85
  | `generate_short_text` | AI-draft editable headline / subheadline / caption beats for a short. | 1 assist |
86
- | `create_editor_ad` | Create an editor project **and** run autopilot end-to-end. Style it with `creative_format` + `visual_language` + `theme`. | credits (est. first) |
86
+ | `create_editor_ad` | Create an editor project **and** run autopilot end-to-end. Style it with `creative_format` + `visual_language` + `theme`. Cast modes need prerequisite assets, so configure them through `create_editor_draft` instead. | credits (est. first) |
87
87
  | `start_autopilot` | Run autopilot on an **existing** editor draft (resume a `make_video` `dry_run`, or after a top-up). | credits |
88
88
  | `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** |
89
89
  | `generate_slider` | Render a carousel (copy → AI backgrounds → composite). Poll `get_slider` until `completed`. | **1 / slide** (3–10) |
@@ -96,12 +96,14 @@ instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_ass
96
96
 
97
97
  | Tool | What it does | Cost |
98
98
  |---|---|---|
99
- | `create_editor_draft` | Editor project, **no** autopilot. Accepts `creative_format` / `visual_language` / `theme`. | **0** |
99
+ | `create_editor_draft` | Editor project, **no** autopilot. Accepts `creative_format` / `visual_language` / `theme` / `cast_mode` (`product_only` needs `set_product` before generation; `presenter_led` needs a configured presenter). | **0** |
100
100
  | `get_editor` | Full project state (scenario, segments, narration, music, latest render, quota) — the review step. | **0** |
101
101
  | `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 |
102
+ | `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** |
102
103
  | `set_scene_count` | Grow/shrink to N scenes (1–20); deletes only trailing un-generated scenes. Each AI scene is a fixed 8s. | **0** |
103
104
  | `set_segment_prompt` | Write one scene's prompt (1–2000 chars), identified by segment id. | **0** |
104
- | `generate_segment` | Render one scene (re-call to retry a failed one). | **5** |
105
+ | `generate_segment` | Render one **pending/failed** scene (re-call to retry a failed one). | **5** |
106
+ | `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** |
105
107
  | `generate_all_segments` | Render every not-yet-completed scene in order (continuity preserved); capped ~280s → re-run to continue. | **5 / segment** |
106
108
  | `set_narration_script` / `generate_narration` | Write your own voiceover script (free) **or** AI-draft it. | free / 1 assist |
107
109
  | `generate_voice` | Synthesize the voiceover. `voice_id` required (see `list_voices`). | **3** |
@@ -220,6 +222,7 @@ edit are **0 credits** — you only pay to generate/render.
220
222
  | Any draft create / edit / reorder | 0 |
221
223
  | Short render | **15** |
222
224
  | Editor AI scene (`generate_segment`) | **5** (4 each in a batch of ≥3) |
225
+ | Editor scene re-roll (`regenerate_segment`, new version of a completed scene) | **4** |
223
226
  | Editor voice | **3** |
224
227
  | Editor music | **5** |
225
228
  | Editor render | **0** (auto-charges only still-ungenerated scenes) |
package/dist/index.js CHANGED
@@ -30120,7 +30120,7 @@ function readStoredCredentials() {
30120
30120
  // package.json
30121
30121
  var package_default = {
30122
30122
  name: "@hubfluencer/mcp",
30123
- version: "0.7.1",
30123
+ version: "0.8.1",
30124
30124
  description: "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
30125
30125
  license: "MIT",
30126
30126
  author: "Monocursive <contact@monocursive.com>",
@@ -31454,7 +31454,7 @@ var SERVER_INSTRUCTIONS = `Hubfluencer turns a prompt — or a whole product pag
31454
31454
 
31455
31455
  MAKE ONE ASSET (simplest)
31456
31456
  - make_video({ prompt }) creates, generates, and returns a finished MP4 — it picks a single-clip short or a multi-scene editor ad from the prompt (override with kind:"short"|"editor"). create_short / create_editor_ad are the per-product one-shots; create_slider makes an image carousel; create_tracking_video burns a CV overlay onto a local clip.
31457
- - Granular (full control): create a draft, then drive the pipeline step by step (create_editor_draft → generate_scenario → set_scene_count → set_segment_prompt → generate_segment(s) → generate_voice + generate_music → render). Poll with get_status / wait_for_completion; fetch with download_result (a slider has no MP4 — read its slides with get_slider).
31457
+ - Granular (full control): create a draft, then drive the pipeline step by step (create_editor_draft → generate_scenario → apply_scenarioreview/edit prompts (set_segment_prompt) → generate_segment(s) → generate_voice + generate_music → render; regenerate_segment re-rolls a completed scene as a new version). Poll with get_status / wait_for_completion; fetch with download_result (a slider has no MP4 — read its slides with get_slider).
31458
31458
 
31459
31459
  DELEGATE THE CONTENT OPERATION (the full loop — steps 1-3 are $0)
31460
31460
  1. ONBOARD a product: extract_product_page(url) pulls facts + imagery (SSRF-hardened, 0cr); import_product_image saves its picture/logo. Persist identity once with create_product_profile + create_brand_profile so every later draft inherits it instead of re-describing the brand.
@@ -32294,7 +32294,7 @@ registerTool("edit_slider_slide", {
32294
32294
  }));
32295
32295
  registerTool("create_editor_ad", {
32296
32296
  title: "Create a multi-scene editor ad (autopilot)",
32297
- description: "Creates an editor project from a product prompt and starts autopilot (server-orchestrated " + "scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " + "Returns the slug. Costs credits. Then poll with get_status / wait_for_completion (kind=editor). Prefer " + "make_video for the one-shot path. " + "BRANDING: to attach the user's own product image, brand logo, or closing card, create the draft with " + "create_editor_draft first, call set_product / set_logo / set_closing_image (all 0 credits), then " + "start_autopilot — autopilot weaves them in. (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.)",
32297
+ description: "Creates an editor project from a product prompt and starts autopilot (server-orchestrated " + "scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " + "Returns the slug. Costs credits. Then poll with get_status / wait_for_completion (kind=editor). Prefer " + "make_video for the one-shot path. " + "BRANDING: to attach the user's own product image, brand logo, or closing card, create the draft with " + "create_editor_draft first, call set_product / set_logo / set_closing_image (all 0 credits), then " + "start_autopilot — autopilot weaves them in. (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.) " + "For product_only or presenter_led cast modes, use create_editor_draft and configure the required " + "product/presenter before start_autopilot; this one-shot tool intentionally uses automatic casting.",
32298
32298
  inputSchema: {
32299
32299
  product_prompt: exports_external.string().min(10).describe("Brief for the ad (min 10 chars)"),
32300
32300
  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."),
@@ -32391,6 +32391,7 @@ registerTool("create_editor_draft", {
32391
32391
  creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Narrative arc / segment structure. Omit for Auto/generic."),
32392
32392
  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."),
32393
32393
  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.'),
32394
+ 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."),
32394
32395
  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"),
32395
32396
  export_aspect_ratio: exports_external.enum(["9:16", "16:9", "1:1"]).optional().describe('Aspect ratio (default "9:16")'),
32396
32397
  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).")
@@ -32409,17 +32410,18 @@ registerTool("create_editor_draft", {
32409
32410
  creative_format: args.creative_format,
32410
32411
  visual_language: args.visual_language,
32411
32412
  theme: args.theme,
32413
+ cast_mode: args.cast_mode,
32412
32414
  voice_id: args.voice_id,
32413
32415
  export_aspect_ratio: args.export_aspect_ratio,
32414
32416
  project_intent: args.project_intent
32415
32417
  };
32416
32418
  if (prompt)
32417
32419
  body.product_prompt = prompt;
32418
- 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 ?? ""));
32420
+ 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 ?? "", args.cast_mode ?? ""));
32419
32421
  return ok({
32420
32422
  slug: created.data.slug,
32421
32423
  kind: "editor",
32422
- next: "generate_scenario | set_scenario, then get_editor to review"
32424
+ next: "generate_scenario | set_scenario, then get_editor to review, then apply_scenario"
32423
32425
  });
32424
32426
  }));
32425
32427
  registerTool("get_editor", {
@@ -32445,7 +32447,7 @@ registerTool("set_scenario", {
32445
32447
  }));
32446
32448
  registerTool("generate_scenario", {
32447
32449
  title: "Generate the editor scenario (AI assist)",
32448
- description: "Generates a scenario with AI. CONSUMES 1 AI ASSIST (free daily quota; check get_ai_assists). On 429 " + "(quota exhausted) either set auto_unlock:true to spend 1 credit for +10 assists and retry once, or " + "write the scenario yourself with set_scenario. Server default segments_count is 5. " + "Optionally pass creative_format / visual_language / theme to persist the project’s creative style before " + "generating (this is the granular path’s way to set/change style; PATCH /scenario does not).",
32450
+ description: "Generates a scenario with AI. CONSUMES 1 AI ASSIST (free daily quota; check get_ai_assists). On 429 " + "(quota exhausted) either set auto_unlock:true to spend 1 credit for +10 assists and retry once, or " + "write the scenario yourself with set_scenario. Server default segments_count is 5. " + "Optionally pass creative_format / visual_language / theme to persist the project’s creative style before " + "generating (this is the granular path’s way to set/change style; PATCH /scenario does not). " + "Generation is ASYNC: get_editor shows scenario_status generating → ready (a few seconds). NEXT STEP: " + "once ready, review the scenario with get_editor, then call apply_scenario to turn it into per-scene " + "segment prompts on the timeline.",
32449
32451
  inputSchema: {
32450
32452
  slug: exports_external.string().describe("Editor project slug"),
32451
32453
  segments_count: exports_external.number().int().min(3).max(10).optional().describe("How many scenes (3–10, server default 5)"),
@@ -32472,6 +32474,63 @@ registerTool("generate_scenario", {
32472
32474
  const res = await withAssist(client, args.auto_unlock ?? false, () => client.post(`/editor/${args.slug}/generate-scenario`, body));
32473
32475
  return ok(asRecord(res).data ?? res);
32474
32476
  }));
32477
+ registerTool("apply_scenario", {
32478
+ title: "Apply the scenario to the timeline (per-scene prompts)",
32479
+ description: "Turns the READY scenario into per-scene segment prompts on the timeline — the missing step between " + "generate_scenario and generate_segment in the granular flow. Existing AI segments are REPLACED with " + "freshly generated scene prompts (uploaded clips are preserved); segments_count 0 removes the AI scenes " + "and keeps the scenario only. Free — no credits, no AI assist (rate limit 15/min). Requires " + 'scenario_status "ready" (422 scenario_not_ready otherwise): after generate_scenario wait until ' + "get_editor shows ready (a few seconds); set_scenario is ready immediately. The apply itself runs async " + "server-side — this tool polls get_editor (bounded ~2 min) until scenario_apply_status leaves " + '"applying" and returns the materialized prompts; if it reports timed_out, keep polling get_editor ' + "yourself. Then review/edit with set_segment_prompt and generate with generate_segment / " + "generate_all_segments (or just render).",
32480
+ inputSchema: {
32481
+ slug: exports_external.string().describe("Editor project slug"),
32482
+ segments_count: exports_external.union([exports_external.literal(0), exports_external.number().int().min(3).max(10)]).optional().describe("How many AI scenes to create on the timeline — 0 or any integer from 3 to 10. Omit to use " + "the scene count saved by generate_scenario (or 5 when none is stored). " + "0 keeps the scenario only and preserves manual timeline control.")
32483
+ },
32484
+ annotations: { title: "Apply scenario", ...WRITE, idempotentHint: false }
32485
+ }, tool(async (args, client, extra) => {
32486
+ const body = {};
32487
+ if (args.segments_count !== undefined)
32488
+ body.segments_count = args.segments_count;
32489
+ const res = await client.post(`/editor/${args.slug}/apply-scenario`, body);
32490
+ const accepted = asRecord(asRecord(res).data);
32491
+ const APPLY_BUDGET_MS = 120000;
32492
+ const APPLY_POLL_MS = 2500;
32493
+ const deadline = Date.now() + APPLY_BUDGET_MS;
32494
+ const readEditor = async () => {
32495
+ const r = await client.get(`/editor/${args.slug}`, undefined, deadline);
32496
+ return asRecord(asRecord(r).data);
32497
+ };
32498
+ let n = 0;
32499
+ let editor = await readEditor();
32500
+ while (editor.scenario_apply_status === "applying" && Date.now() + APPLY_POLL_MS <= deadline) {
32501
+ await reportProgress(extra, ++n, "applying scenario to timeline");
32502
+ await sleep4(APPLY_POLL_MS);
32503
+ editor = await readEditor();
32504
+ }
32505
+ if (editor.scenario_apply_status === "applying") {
32506
+ return ok({
32507
+ slug: args.slug,
32508
+ applied: false,
32509
+ timed_out: true,
32510
+ note: 'The apply is still running server-side. Poll get_editor until scenario_apply_status is no longer "applying", then review the scene prompts.'
32511
+ });
32512
+ }
32513
+ if (editor.scenario_apply_status === "failed") {
32514
+ return fail("Scenario apply failed server-side. Re-run apply_scenario to retry, or build the timeline yourself with set_scene_count + set_segment_prompt.");
32515
+ }
32516
+ const segments = Array.isArray(editor.segments) ? editor.segments : [];
32517
+ return ok({
32518
+ slug: args.slug,
32519
+ applied: true,
32520
+ ai_segments_replaced: accepted.ai_segments_replaced,
32521
+ uploads_preserved: accepted.uploads_preserved,
32522
+ target_ai_segments: accepted.target_ai_segments,
32523
+ scene_count: segments.length,
32524
+ segments: segments.map((s) => ({
32525
+ id: s.id,
32526
+ position: s.position,
32527
+ source_type: s.source_type,
32528
+ status: s.status,
32529
+ prompt: s.prompt
32530
+ })),
32531
+ next: "Review/edit prompts (set_segment_prompt), then generate_segment / generate_all_segments — or just render (it auto-charges ungenerated scenes at the batch rate)"
32532
+ });
32533
+ }));
32475
32534
  registerTool("set_scene_count", {
32476
32535
  title: "Set the number of scenes",
32477
32536
  description: "Adjusts the editor to exactly `count` scenes (1–20) by adding pending segments to grow, or deleting " + "only TRAILING un-generated (pending or failed) segments to shrink. Never deletes completed/processing segments — " + "if a shrink would require that, it stops and reports. Free (no credits, no assist). Each AI-generated " + "scene is a fixed 8 seconds, so `count` scenes ≈ count×8s of AI footage (uploaded clips keep their own length).",
@@ -32539,7 +32598,7 @@ registerTool("set_segment_prompt", {
32539
32598
  }));
32540
32599
  registerTool("generate_segment", {
32541
32600
  title: "Generate one segment (5 credits)",
32542
- description: "Renders the video for one scene. Each AI-generated scene is a fixed 8 seconds long. Costs 5 credits. " + "Re-call to retry a failed scene or regenerate after a prompt edit — the server blocks regenerating one that's still processing, so no double-charge. Visual" + "continuity is handled server-side (this scene uses the previous scene's last frame). 402 " + "credits_insufficient fails cleanly. Generate scenes in position order for correct continuity.",
32601
+ description: "Renders the video for one PENDING or FAILED scene. Each AI-generated scene is a fixed 8 seconds long. " + "Costs 5 credits. Re-call to retry a failed scene or after a prompt edit — the server blocks one that's " + "still processing (editor_segment_busy), so no double-charge; a COMPLETED scene returns " + "editor_segment_completed — re-roll those with regenerate_segment instead. Visual " + "continuity is handled server-side (this scene uses the previous scene's last frame). 402 " + "credits_insufficient fails cleanly. Generate scenes in position order for correct continuity.",
32543
32602
  inputSchema: {
32544
32603
  slug: exports_external.string().describe("Editor project slug"),
32545
32604
  segment_id: exports_external.union([exports_external.number(), exports_external.string()]).describe("Segment id (from get_editor)")
@@ -32549,6 +32608,23 @@ registerTool("generate_segment", {
32549
32608
  const res = await client.post(`/editor/${args.slug}/segments/${String(args.segment_id)}/generate`, undefined, undefined);
32550
32609
  return ok(asRecord(res).data ?? res);
32551
32610
  }));
32611
+ registerTool("regenerate_segment", {
32612
+ title: "Regenerate a completed segment (new version, 4 credits)",
32613
+ description: "Re-rolls a COMPLETED scene as a NEW VERSION of that segment — the previous version is kept (switch back " + "in the app). Costs 4 credits. Use generate_segment for pending/failed scenes; use THIS for a completed " + "scene you want different — optionally with a new prompt (omit it to re-roll the current prompt). The new " + "version starts rendering immediately; watch it with get_editor (status processing → completed|failed). " + "KNOWN LIMITATION: scenes chain visual continuity off the previous scene's last frame, so regenerating a " + "MID-TIMELINE scene can degrade the transition into the FOLLOWING scene (it was seeded from the old " + "version's last frame). A scene still processing returns editor_segment_busy (no double charge); 402 " + "credits_insufficient fails cleanly.",
32614
+ inputSchema: {
32615
+ slug: exports_external.string().describe("Editor project slug"),
32616
+ segment_id: exports_external.union([exports_external.number(), exports_external.string()]).describe("Segment id (from get_editor)"),
32617
+ prompt: exports_external.string().min(1).max(2000).optional().describe("Optional new scene prompt for the regenerated version (1–2000 chars); omit to reuse the current prompt")
32618
+ },
32619
+ annotations: {
32620
+ title: "Regenerate segment",
32621
+ ...WRITE,
32622
+ idempotentHint: false
32623
+ }
32624
+ }, tool(async (args, client) => {
32625
+ const res = await client.post(`/editor/${args.slug}/segments/${String(args.segment_id)}/regenerate`, args.prompt !== undefined ? { prompt: args.prompt } : undefined, undefined);
32626
+ return ok(asRecord(res).data ?? res);
32627
+ }));
32552
32628
  registerTool("generate_all_segments", {
32553
32629
  title: "Generate all pending segments (sequential)",
32554
32630
  description: "Generates every not-yet-completed scene (pending or previously failed) in position order, waiting for each to finish before starting" + "the next (preserves visual continuity). Each AI-generated scene is a fixed 8 seconds, so N scenes ≈ N×8s of footage. Costs 5 credits per segment generated. Stops and reports what " + "completed if a scene errors on submit (e.g. 402 credits_insufficient) or fails while rendering; if a " + "scene is still rendering past the budget it returns timed_out so you can re-run to continue (completed " + "scenes are skipped). The whole call is capped at ~280s: on a project with many scenes it processes as " + "many as fit, returns timed_out, and you re-run to continue (it picks up the remaining pending scenes). " + "Tip: to generate everything at lowest cost, skip this and just call render — it auto-generates any ungenerated scenes at the batch rate (4 credits/scene for ≥3); use this tool when you want to review or stop per scene. May take several minutes.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubfluencer/mcp",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
5
5
  "license": "MIT",
6
6
  "author": "Monocursive <contact@monocursive.com>",
package/src/index.ts CHANGED
@@ -11,7 +11,8 @@
11
11
  * Shorts : create_short → generate_short → wait_for_completion → download_result
12
12
  * Editor : create_editor_ad (creates + starts autopilot) → wait_for_completion → download_result
13
13
  * Granular : create_editor_draft → generate_scenario|set_scenario → get_editor
14
- * → set_scene_countset_segment_prompt generate_segment(s)
14
+ * → apply_scenario (scenario scene prompts) | set_scene_count + set_segment_prompt
15
+ * → generate_segment(s) (+ regenerate_segment to re-roll a completed scene)
15
16
  * → set_narration_script|generate_narration → generate_voice + generate_music
16
17
  * → render → wait_for_completion → download_result
17
18
  */
@@ -500,7 +501,7 @@ const SERVER_INSTRUCTIONS = `Hubfluencer turns a prompt — or a whole product p
500
501
 
501
502
  MAKE ONE ASSET (simplest)
502
503
  - make_video({ prompt }) creates, generates, and returns a finished MP4 — it picks a single-clip short or a multi-scene editor ad from the prompt (override with kind:"short"|"editor"). create_short / create_editor_ad are the per-product one-shots; create_slider makes an image carousel; create_tracking_video burns a CV overlay onto a local clip.
503
- - Granular (full control): create a draft, then drive the pipeline step by step (create_editor_draft → generate_scenario → set_scene_count → set_segment_prompt → generate_segment(s) → generate_voice + generate_music → render). Poll with get_status / wait_for_completion; fetch with download_result (a slider has no MP4 — read its slides with get_slider).
504
+ - Granular (full control): create a draft, then drive the pipeline step by step (create_editor_draft → generate_scenario → apply_scenarioreview/edit prompts (set_segment_prompt) → generate_segment(s) → generate_voice + generate_music → render; regenerate_segment re-rolls a completed scene as a new version). Poll with get_status / wait_for_completion; fetch with download_result (a slider has no MP4 — read its slides with get_slider).
504
505
 
505
506
  DELEGATE THE CONTENT OPERATION (the full loop — steps 1-3 are $0)
506
507
  1. ONBOARD a product: extract_product_page(url) pulls facts + imagery (SSRF-hardened, 0cr); import_product_image saves its picture/logo. Persist identity once with create_product_profile + create_brand_profile so every later draft inherits it instead of re-describing the brand.
@@ -2578,7 +2579,9 @@ registerTool(
2578
2579
  "BRANDING: to attach the user's own product image, brand logo, or closing card, create the draft with " +
2579
2580
  "create_editor_draft first, call set_product / set_logo / set_closing_image (all 0 credits), then " +
2580
2581
  "start_autopilot — autopilot weaves them in. (Editor ads carry their copy in-scene/narration, not as a " +
2581
- "title overlay; for an on-screen title/subtitle use a short with headline/subheadline instead.)",
2582
+ "title overlay; for an on-screen title/subtitle use a short with headline/subheadline instead.) " +
2583
+ "For product_only or presenter_led cast modes, use create_editor_draft and configure the required " +
2584
+ "product/presenter before start_autopilot; this one-shot tool intentionally uses automatic casting.",
2582
2585
  inputSchema: {
2583
2586
  product_prompt: z
2584
2587
  .string()
@@ -2826,7 +2829,8 @@ registerTool(
2826
2829
  // ad with full control (review the scenario, write its own prompts, etc.)
2827
2830
  // instead of one-shot autopilot (create_editor_ad / make_video).
2828
2831
  // create_editor_draft → generate_scenario|set_scenario → get_editor (REVIEW)
2829
- // → set_scene_countset_segment_prompt generate_segment|generate_all_segments
2832
+ // → apply_scenario (scenario per-scene prompts) | set_scene_count + set_segment_prompt
2833
+ // → generate_segment|generate_all_segments (+ regenerate_segment for completed scenes)
2830
2834
  // → set_narration_script|generate_narration → generate_voice + generate_music
2831
2835
  // → render → wait_for_completion(kind:"editor") → download_result.
2832
2836
 
@@ -2877,6 +2881,17 @@ registerTool(
2877
2881
  .describe(
2878
2882
  '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.',
2879
2883
  ),
2884
+ cast_mode: z
2885
+ .enum(["product_only", "presenter_led"])
2886
+ .optional()
2887
+ .describe(
2888
+ "Who/what anchors every AI scene. product_only = a product-led ad with NO people; requires a " +
2889
+ "product image (set_product) before generation and generates every scene from persistent product " +
2890
+ "reference images — the best product fidelity. presenter_led = one locked presenter appears in " +
2891
+ "every scene; requires a presenter to be configured first (in the app / REST presenter endpoints " +
2892
+ "— no MCP tool yet), else generation fails with presenter_required. Omit for the default auto " +
2893
+ "behavior.",
2894
+ ),
2880
2895
  voice_id: z
2881
2896
  .string()
2882
2897
  .regex(/^[A-Za-z0-9_-]+$/)
@@ -2913,6 +2928,7 @@ registerTool(
2913
2928
  creative_format?: string;
2914
2929
  visual_language?: string;
2915
2930
  theme?: string;
2931
+ cast_mode?: string;
2916
2932
  voice_id?: string;
2917
2933
  export_aspect_ratio?: string;
2918
2934
  project_intent?: string;
@@ -2926,6 +2942,7 @@ registerTool(
2926
2942
  creative_format: args.creative_format,
2927
2943
  visual_language: args.visual_language,
2928
2944
  theme: args.theme,
2945
+ cast_mode: args.cast_mode,
2929
2946
  voice_id: args.voice_id,
2930
2947
  export_aspect_ratio: args.export_aspect_ratio,
2931
2948
  project_intent: args.project_intent,
@@ -2948,12 +2965,13 @@ registerTool(
2948
2965
  args.theme ?? "",
2949
2966
  args.voice_id ?? "",
2950
2967
  args.export_aspect_ratio ?? "",
2968
+ args.cast_mode ?? "",
2951
2969
  ),
2952
2970
  );
2953
2971
  return ok({
2954
2972
  slug: created.data.slug,
2955
2973
  kind: "editor",
2956
- next: "generate_scenario | set_scenario, then get_editor to review",
2974
+ next: "generate_scenario | set_scenario, then get_editor to review, then apply_scenario",
2957
2975
  });
2958
2976
  },
2959
2977
  ),
@@ -3011,7 +3029,10 @@ registerTool(
3011
3029
  "(quota exhausted) either set auto_unlock:true to spend 1 credit for +10 assists and retry once, or " +
3012
3030
  "write the scenario yourself with set_scenario. Server default segments_count is 5. " +
3013
3031
  "Optionally pass creative_format / visual_language / theme to persist the project\u2019s creative style before " +
3014
- "generating (this is the granular path\u2019s way to set/change style; PATCH /scenario does not).",
3032
+ "generating (this is the granular path\u2019s way to set/change style; PATCH /scenario does not). " +
3033
+ "Generation is ASYNC: get_editor shows scenario_status generating \u2192 ready (a few seconds). NEXT STEP: " +
3034
+ "once ready, review the scenario with get_editor, then call apply_scenario to turn it into per-scene " +
3035
+ "segment prompts on the timeline.",
3015
3036
  inputSchema: {
3016
3037
  slug: z.string().describe("Editor project slug"),
3017
3038
  segments_count: z
@@ -3083,6 +3104,111 @@ registerTool(
3083
3104
  ),
3084
3105
  );
3085
3106
 
3107
+ registerTool(
3108
+ "apply_scenario",
3109
+ {
3110
+ title: "Apply the scenario to the timeline (per-scene prompts)",
3111
+ description:
3112
+ "Turns the READY scenario into per-scene segment prompts on the timeline — the missing step between " +
3113
+ "generate_scenario and generate_segment in the granular flow. Existing AI segments are REPLACED with " +
3114
+ "freshly generated scene prompts (uploaded clips are preserved); segments_count 0 removes the AI scenes " +
3115
+ "and keeps the scenario only. Free — no credits, no AI assist (rate limit 15/min). Requires " +
3116
+ 'scenario_status "ready" (422 scenario_not_ready otherwise): after generate_scenario wait until ' +
3117
+ "get_editor shows ready (a few seconds); set_scenario is ready immediately. The apply itself runs async " +
3118
+ "server-side — this tool polls get_editor (bounded ~2 min) until scenario_apply_status leaves " +
3119
+ '"applying" and returns the materialized prompts; if it reports timed_out, keep polling get_editor ' +
3120
+ "yourself. Then review/edit with set_segment_prompt and generate with generate_segment / " +
3121
+ "generate_all_segments (or just render).",
3122
+ inputSchema: {
3123
+ slug: z.string().describe("Editor project slug"),
3124
+ segments_count: z
3125
+ .union([z.literal(0), z.number().int().min(3).max(10)])
3126
+ .optional()
3127
+ .describe(
3128
+ "How many AI scenes to create on the timeline — 0 or any integer from 3 to 10. Omit to use " +
3129
+ "the scene count saved by generate_scenario (or 5 when none is stored). " +
3130
+ "0 keeps the scenario only and preserves manual timeline control.",
3131
+ ),
3132
+ },
3133
+ annotations: { title: "Apply scenario", ...WRITE, idempotentHint: false },
3134
+ },
3135
+ tool(
3136
+ async (args: { slug: string; segments_count?: number }, client, extra) => {
3137
+ const body: Record<string, unknown> = {};
3138
+ if (args.segments_count !== undefined)
3139
+ body.segments_count = args.segments_count;
3140
+ const res = await client.post<{ data: unknown }>(
3141
+ `/editor/${args.slug}/apply-scenario`,
3142
+ body,
3143
+ );
3144
+ const accepted = asRecord(asRecord(res).data);
3145
+
3146
+ // The 202 only STARTS the apply — the controller flips
3147
+ // scenario_apply_status to "applying" before responding, and an async
3148
+ // worker compiles the scenario into per-scene prompts. Poll the project
3149
+ // until the status leaves "applying" ("idle" = success, "failed" =
3150
+ // terminal) so the agent gets the materialized prompts back instead of
3151
+ // having to poll itself. The budget is far below the render loops' 280s
3152
+ // ceiling: an apply is a single LLM call (typically seconds).
3153
+ const APPLY_BUDGET_MS = 120_000;
3154
+ const APPLY_POLL_MS = 2_500;
3155
+ const deadline = Date.now() + APPLY_BUDGET_MS;
3156
+ const readEditor = async () => {
3157
+ // Bound each poll GET's retries by the loop deadline (see fetchStatus).
3158
+ const r = await client.get<{ data: unknown }>(
3159
+ `/editor/${args.slug}`,
3160
+ undefined,
3161
+ deadline,
3162
+ );
3163
+ return asRecord(asRecord(r).data);
3164
+ };
3165
+ let n = 0;
3166
+ let editor = await readEditor();
3167
+ while (
3168
+ editor.scenario_apply_status === "applying" &&
3169
+ Date.now() + APPLY_POLL_MS <= deadline
3170
+ ) {
3171
+ await reportProgress(extra, ++n, "applying scenario to timeline");
3172
+ await sleep(APPLY_POLL_MS);
3173
+ editor = await readEditor();
3174
+ }
3175
+
3176
+ if (editor.scenario_apply_status === "applying") {
3177
+ return ok({
3178
+ slug: args.slug,
3179
+ applied: false,
3180
+ timed_out: true,
3181
+ note: 'The apply is still running server-side. Poll get_editor until scenario_apply_status is no longer "applying", then review the scene prompts.',
3182
+ });
3183
+ }
3184
+ if (editor.scenario_apply_status === "failed") {
3185
+ return fail(
3186
+ "Scenario apply failed server-side. Re-run apply_scenario to retry, or build the timeline yourself with set_scene_count + set_segment_prompt.",
3187
+ );
3188
+ }
3189
+ const segments = Array.isArray(editor.segments)
3190
+ ? (editor.segments as Record<string, unknown>[])
3191
+ : [];
3192
+ return ok({
3193
+ slug: args.slug,
3194
+ applied: true,
3195
+ ai_segments_replaced: accepted.ai_segments_replaced,
3196
+ uploads_preserved: accepted.uploads_preserved,
3197
+ target_ai_segments: accepted.target_ai_segments,
3198
+ scene_count: segments.length,
3199
+ segments: segments.map((s) => ({
3200
+ id: s.id,
3201
+ position: s.position,
3202
+ source_type: s.source_type,
3203
+ status: s.status,
3204
+ prompt: s.prompt,
3205
+ })),
3206
+ next: "Review/edit prompts (set_segment_prompt), then generate_segment / generate_all_segments — or just render (it auto-charges ungenerated scenes at the batch rate)",
3207
+ });
3208
+ },
3209
+ ),
3210
+ );
3211
+
3086
3212
  registerTool(
3087
3213
  "set_scene_count",
3088
3214
  {
@@ -3214,8 +3340,10 @@ registerTool(
3214
3340
  {
3215
3341
  title: "Generate one segment (5 credits)",
3216
3342
  description:
3217
- "Renders the video for one scene. Each AI-generated scene is a fixed 8 seconds long. Costs 5 credits. " +
3218
- "Re-call to retry a failed scene or regenerate after a prompt edit — the server blocks regenerating one that's still processing, so no double-charge. Visual" +
3343
+ "Renders the video for one PENDING or FAILED scene. Each AI-generated scene is a fixed 8 seconds long. " +
3344
+ "Costs 5 credits. Re-call to retry a failed scene or after a prompt edit — the server blocks one that's " +
3345
+ "still processing (editor_segment_busy), so no double-charge; a COMPLETED scene returns " +
3346
+ "editor_segment_completed — re-roll those with regenerate_segment instead. Visual " +
3219
3347
  "continuity is handled server-side (this scene uses the previous scene's last frame). 402 " +
3220
3348
  "credits_insufficient fails cleanly. Generate scenes in position order for correct continuity.",
3221
3349
  inputSchema: {
@@ -3236,6 +3364,54 @@ registerTool(
3236
3364
  }),
3237
3365
  );
3238
3366
 
3367
+ registerTool(
3368
+ "regenerate_segment",
3369
+ {
3370
+ title: "Regenerate a completed segment (new version, 4 credits)",
3371
+ description:
3372
+ "Re-rolls a COMPLETED scene as a NEW VERSION of that segment — the previous version is kept (switch back " +
3373
+ "in the app). Costs 4 credits. Use generate_segment for pending/failed scenes; use THIS for a completed " +
3374
+ "scene you want different — optionally with a new prompt (omit it to re-roll the current prompt). The new " +
3375
+ "version starts rendering immediately; watch it with get_editor (status processing → completed|failed). " +
3376
+ "KNOWN LIMITATION: scenes chain visual continuity off the previous scene's last frame, so regenerating a " +
3377
+ "MID-TIMELINE scene can degrade the transition into the FOLLOWING scene (it was seeded from the old " +
3378
+ "version's last frame). A scene still processing returns editor_segment_busy (no double charge); 402 " +
3379
+ "credits_insufficient fails cleanly.",
3380
+ inputSchema: {
3381
+ slug: z.string().describe("Editor project slug"),
3382
+ segment_id: z
3383
+ .union([z.number(), z.string()])
3384
+ .describe("Segment id (from get_editor)"),
3385
+ prompt: z
3386
+ .string()
3387
+ .min(1)
3388
+ .max(2000)
3389
+ .optional()
3390
+ .describe(
3391
+ "Optional new scene prompt for the regenerated version (1–2000 chars); omit to reuse the current prompt",
3392
+ ),
3393
+ },
3394
+ annotations: {
3395
+ title: "Regenerate segment",
3396
+ ...WRITE,
3397
+ idempotentHint: false,
3398
+ },
3399
+ },
3400
+ tool(
3401
+ async (
3402
+ args: { slug: string; segment_id: number | string; prompt?: string },
3403
+ client,
3404
+ ) => {
3405
+ const res = await client.post<{ data: unknown }>(
3406
+ `/editor/${args.slug}/segments/${String(args.segment_id)}/regenerate`,
3407
+ args.prompt !== undefined ? { prompt: args.prompt } : undefined,
3408
+ undefined,
3409
+ );
3410
+ return ok(asRecord(res).data ?? res);
3411
+ },
3412
+ ),
3413
+ );
3414
+
3239
3415
  registerTool(
3240
3416
  "generate_all_segments",
3241
3417
  {