---
name: hubfluencer-create
description: Use this skill when the user wants to generate a video ad, short, image carousel, or social clip with Hubfluencer, OR to run the surrounding content operation — e.g. "use Hubfluencer to make an ad for X", "create a short for my product", "make me a TikTok ad", "turn my product page into a campaign", "plan a month of ads for X". Drives Hubfluencer end-to-end (prompt or product page → finished MP4/carousel; and plan → draft → review → schedule → measure) via the MCP server or the REST API.
user-invocable: true
---

You can make a finished, post-ready video ad with Hubfluencer from a single text prompt. The
whole multi-step pipeline (script → scenes → narration → voice → music → render) runs
server-side; your job is to kick it off, wait, and hand back the MP4.

Read `API.md` in this skill for the exact endpoints, fields, credit costs, and error taxonomy.

## Creative export contract

- Production prompts (`product_prompt`, `scenario_prompt`, and scene `prompt`) are visual directions. Narration scripts are read aloud verbatim, so `set_narration_script.script` must contain only the exact spoken words—never camera notes, prompt syntax, labels, or scene directions. Never copy a creative brief into `script`; use `generate_narration` or Autopilot unless the user supplied final voice-over copy.
- Do not rely on the video model to spell essential brand names, UI, signs, prices, offers, or CTAs. Put exact copy in Hubfluencer overlays, narration, or supplied ending artwork.
- A short logo position is a preferred corner: the renderer moves it away from headline, captions, CTA, offer, and badge areas. Omit `treatment` so transparent marks default to `throughout` and opaque square lockups default to `end_card`. Use a tight transparent wordmark; use square or full-bleed art as the poster instead. If poster artwork already has its logo/CTA, set `poster_includes_lockup: true` and do not add an `end_card` logo.
- Short posters fill the ending frame: phone-shaped portrait art is cover-cropped, while square, classic portrait, and landscape art is contained over a blurred backdrop. Editor closing images are always cover-cropped. Keep important content in the central 9:16 safe area. An Editor closing image gets no generic CTA, so put the CTA in the artwork or state it in the preceding narration/scene.
- Pass `segments_count` (3–10) to `create_editor_ad` when length matters. It is the exact structural target used by the cost quote and Autopilot launch.
- Each Editor AI scene is 8 seconds. Default a concise social ad to 3 scenes (about 24 seconds) unless the user asks for a longer story.

## Generation lifecycle authority

Every Editor and Short product read includes a non-null `generation_summary`. Treat it as the sole
lifecycle authority even when product projections such as `stage`, `autopilot_status`, `batch_generation_status`, child
statuses, or `latest_render` disagree. Poll only while
`generation_summary.timing.poll_after_ms` is a positive integer, at that server cadence; `null`
means stop polling. Read terminality from `generation_summary.status`, invoke only
`generation_summary.available_actions`, and download only when `download_result` is advertised for
a verified delivery artifact. Product projection fields are display metadata or explicit route fences only;
never derive polling, retry, cancel, or download availability from them.

When a stopped summary advertises `edit_required_input` or `review_spend_cap`, call
`provide_generation_input` with that action and its non-empty `input` object or approved
`max_credits`. For a failed `editor.scene.generate` or `editor.scene.regenerate` summary that
advertises `retry_generation`, call `retry_segment_generation { slug, segment_id }`. Both tools
re-read the product and echo the run/revision/step fences; never call them from product projection fields
or guess a fence. Re-read status after the command.

## Connecting (get an access token)

Generation needs a Hubfluencer **access token** (an opaque bearer token — not a JWT). There are
two ways to connect, in order of smoothness:

- **Device-link login (preferred — no copy-paste):** run `npx -y @hubfluencer/mcp@0.18.0 login`.
  It prints a URL + short code; the user opens the URL, approves the
  connection in the signed-in app, and a scoped token is saved locally
  (`~/.hubfluencer/credentials.json`). The MCP server picks it up automatically. If the MCP
  reports "Not connected", tell the user to run `npx -y @hubfluencer/mcp@0.18.0 login`.
- **Access token in the app:** the user buys credits, then opens **Settings → Access tokens**,
  creates a token with the "Generate videos" scope (`video:generate` + `video:read`), and sets it
  as `HUBFLUENCER_API_TOKEN` for the MCP server.
- **Raw fallback (no UI):** with a signed-in session token, `POST /api/tokens {name, scopes}`
  returns a token once. Creating tokens requires a signed-in session — an agent token itself
  cannot create tokens.

Never invent a token, and never echo the token in your output.

## Native macOS Studio

When the user connects the bearer-gated `hubfluencer-studio` localhost MCP server, use its
advertised local document tools instead of treating the project as a remote render. Read
`get_project_digest` first and pass its exact required `doc_rev` on every call. Read+edit is the
default surface; generation is opt-in and still creates a visible in-app approval card. The native
app owns cloud-context disclosure, so do not call its Director REST endpoint behind the user's back.

## Credits — real money, so confirm before you spend

Generation spends prepaid **credits** (bought in the app), and credits cost real money:
**$0.99 each** bought singly, less in bulk (credit packs and subscriptions bring the per-credit
price down). So a 15-credit short runs up to ~$15 and a ~28-credit autopilot ad up to ~$28.
`create_*` is free (0 credits); only generation/render charges. Check the balance with
`get_credits` (`GET /api/studio/credits`) before generating; if the user is out, tell them to top
up in the app and stop.

**Ask before the first paid call.** Unless the user has already approved that specific spend (they
saw the price, or gave you an explicit credit budget — carry it into `max_credits`), quote the
estimated credits **and** the approximate dollar cost (at up to $0.99/credit) and wait for a clear
yes before `generate_short` / `start_autopilot` / `generate_slider` / per-scene `generate_*` /
`generate_voice` / `generate_music` / `create_tracking_video` / `unlock_ai_assists` / a
non-`dry_run` `make_video`. For a batch (several drafts, a campaign), confirm the **total** once
up front instead of asking per item.

## Two ways to drive it

1. **MCP (preferred).** Use the `hubfluencer` tools. The simplest is **`make_video`** — one call:
   prompt in, finished MP4 out (it creates, starts, polls, and downloads). See `@hubfluencer/mcp`.
2. **Direct REST** — `curl`/`fetch` per `API.md`.

## The one-shot path (recommended)

`make_video({ prompt: "an ad for…", save_path: "ad.mp4" })` → returns
`{ ready, video_url, saved_to, kind_inferred, estimated_credits, available_credits, ...billing }`.
For shorts, `billing` is `charged`; for editors it reports `launched`, `authorized_credits`, and the
observed `credits_spent`/`charged` when the final receipt read is available.
- **Don't ship a bare video.** For a short, pass `headline` (the on-screen title, ≈4 words / ≤40
  chars), `subheadline`, and a fitting `music_vibe` right in the `make_video` call — and **always
  `cta_text`** (a short without a CTA doesn't convert; add `offer_text` only if there's a real deal).
  Set `visual_language` explicitly (`kinetic_creator` is a strong default). These are SHORTS fields
  (`theme` applies to both kinds). If copy is left blank the server auto-writes the headline +
  caption beats at generate time (`skip_auto_text: true` renders a deliberately bare clip). If the
  user wants their own product image, brand logo, or closing
  card, `make_video` can't attach files — use `create_editor_ad`'s asset paths for an editor, or
  drive the granular short path (`create_short` + `set_short_product`/`set_short_logo`/`set_short_poster`, then
  `generate_short`). When the user hasn't given you a
  title or brand assets, infer a sensible headline/subheadline from the prompt — or ask.
- **It prices the job before charging.** `make_video` creates the project (free), reads the real cost
  against the live balance, and starts generation if it's affordable. Shorts charge atomically at
  launch; editor autopilot authorizes `max_credits` and spends incrementally as work starts.
  So unless the user already approved the spend, default to `make_video({ prompt, dry_run: true })`
  first — it's free and returns `estimated_credits` plus a draft `slug`. Quote the credits and the
  ~dollar cost (up to $0.99/credit), get the yes, then run `generate_short`/`start_autopilot` on
  that slug. If a charging call can't afford the job (or the estimate exceeds `max_credits`), it
  returns `charged: false` with the estimate and a free draft `slug` — surface the numbers and
  stop; don't loop.
- `kind` defaults to `auto` — a multi-scene **editor** ad for ad/promo/story briefs, a fast single-clip
  **short** for simple/short ones. The chosen kind comes back as `kind_inferred`; override with
  `kind: "short" | "editor"`. An editor ad is built from **8-second AI scenes** (≈ `scene_count × 8s`);
  autopilot defaults to 5 scenes (~40s).
- If it returns `terminal: false`, the render is still going — call
  `wait_for_completion({ slug, kind })` again. Generation takes a few minutes; that's normal.
- For a paid short, `get_status` / `wait_for_completion` exposes
  `segments_completed`, `segments_total`, and `active_segment_positions`. Report that clip progress;
  do not treat a long `rendering` stage as a hang while the counts or active positions are moving.
- `save_path` downloads are confined to `HUBFLUENCER_OUTPUT_DIR` (or cwd) and must end in `.mp4`.

## Granular path (control / recovery / raw REST)

Act on Editor batch recovery only when `generation_summary.available_actions` advertises
`retry_generation` or `cancel_generation`; then call `retry_editor_batch { slug }` or
`cancel_editor_batch { slug }` respectively. The MCP tools re-read the canonical summary before writing.
Raw REST retry callers send `generation_run_id` and `generation_revision` from that summary; cancel
callers send `generation_run_id`. Product batch fields are display projections, never command fences or
action availability. A stale 409 means re-read and issue a new command only if the action is still
advertised; never guess fences. Retry targets the exact failed canonical step. Only a scene named by
`current_step` may be repaired before retry. Resolve the child batch before starting Autopilot again;
retrying it does not authorize a new parent run. For an insufficient-credit blocker, top up or reduce
scope and follow the action in the newly read summary.

Cancellation is fenced to the exact work the agent observed. A paid short is the
exception: once `generate_short` starts, the run cannot be cancelled and the draft
cannot be deleted until it is terminal (a failure still refunds its 15 credits).
For an individually generated editor scene, call
`cancel_segment_generation { slug, segment_id, generation_claim_token }` with the current token from
`get_editor`; it refunds any scene charge. Batch/Autopilot scenes use their workflow controls instead.
Use `cancel_autopilot { slug }` to re-read and forward the summary run/start fence and stop only that
running parent chain; completed work and already charged
scenes remain. A stale fence is a safety error: re-read state; never guess a token, run,
or start time.

**Short:** `get_credits` (≥15) → `create_short { product_prompt, …creative fields }` (≥10 chars) →
(optional branding: `set_short_product { slug, file_path }`, `set_short_logo { slug, file_path, position?, treatment? }`, `set_short_poster { slug, file_path }`) →
`generate_short { slug }` (send an `Idempotency-Key` on raw REST) → poll
`get_status { slug, kind: "short" }` only while its validated
`generation_summary.timing.poll_after_ms` is positive → stop at `null` and return the verified
delivery only when `generation_summary.available_actions` advertises `download_result`.

**Minimum sellable short** — check all seven before you spend:

- hook (`headline`) ≤4 words
- `text_beats` = hook → proof → payoff
- `cta_text` set (a short without a CTA doesn't convert)
- product image attached (`set_short_product`) AND described
- poster (`set_short_poster`) or closing lockup (`brand_name` / `closing_claim`) carries the ask
- `visual_language` set explicitly (`kinetic_creator` is a strong default)
- iterate: hooks are free to re-render (`update_short` → `rerender_short`, 0 credits after the
  first generate)

Iterate the hook before you spend: `generate_hook_variations` writes 3–6 hook options on the free
assist quota. If `headline`/`subheadline`/`text_beats` are all blank, the server auto-writes the
headline + caption beats at generate time (pass `skip_auto_text: true` to `generate_short` for a
deliberately bare clip).

**Never re-roll — iteration is free after the first generate.** The first `generate_short`
(15 credits) buys the footage + music; after that, every overlay edit re-renders over them for
**0 credits**: `update_short` the fields, then `rerender_short { slug }` (REST:
`POST /api/shorts/:slug/rerender`, no body, `Idempotency-Key` honored, rate limit 10/min).

- **Free to re-render:** `headline`, `subheadline`, `text_beats`, `cta_text`, `offer_text`,
  `badge_text`, `star_rating`, headline/subheadline/accent colors, `font_family`, `text_position`,
  `text_animation`, `closing_claim`, `brand_name`, poster (+ `poster_includes_lockup`), `end_card`,
  logo overlay (`set_short_logo` / `short_logo_position` / `short_logo_treatment`).
- **Needs `generate_short` again (15 credits):** `product_prompt`, product image,
  `creative_format`, `visual_language`, `theme`, `music_vibe`, `music_instruments`, `language`.

Edits that add/remove the end card (poster, `end_card`) can shift the video between 12s and 14s;
the pinned music bed is reused. When it is shorter than the export it crossfades into a repeated
continuation, then fades at the export end, without charging for regenerated music.

Iteration recipe: generate once → `update_short` a hook/CTA variant → `rerender_short` (free) →
compare → repeat → keep the winner. **409** = a render is already running (keep polling);
**422 `short_not_ready`** = nothing generated yet — run `generate_short` first. A re-render is
never a credit error. **422 `short_paid_regeneration_required`** = a footage/music input changed;
run `generate_short` again before free iteration resumes.

`update_short` returns a compact mutation receipt (`updated`, `end_card_mode`,
`resolved_cta_text`, and `next`), not the complete Short. Use `get_status` or the REST GET when you
need authoritative project state.

**Brand the short — don't ship a bare clip.** `create_short` (and `make_video` for the one-shot path)
take creative fields you should populate up front:

- `headline` — the on-screen **title** overlay (≈4 words; ≤40 chars hits hardest; hard cap 80).
  `subheadline` — the **secondary title** (≤200).
- `visual_language` — kinetic_creator / premium_editorial / cinematic_product / ugc_realism /
  startup_explainer / luxury_minimal. Drives Veo direction AND the render's typographic identity
  (fonts, casing, caption treatment, animation) — the highest-leverage styling field.
- `creative_format` — problem_solution / mistake_fix / myth_vs_reality / before_after /
  proof_demo / product_reveal.
- `music_vibe` — Upbeat (default), Cinematic, Minimal, Luxury, Playful, Jazz.
- `theme` — **deprecated for shorts**; legacy fallback only when `visual_language` is unset.
- `text_position` (top/center/bottom), `text_animation` (auto / reveal / typewriter / fade_in / pop /
  bounce / word_stagger / word_spotlight), `font_family` (`auto` or one of 15 ShortFont* faces),
  `music_instruments` (≤ 10 hints, each ≤ 40 chars) — fine styling, usually fine to leave default:
  `text_animation` and `font_family` default to **`auto`**, which follows the visual_language's own
  typography. Every concrete value (including `fade_in` / `ShortFontSpaceGrotesk`) is an explicit pick
  that always wins.
- `headline_color` / `subheadline_color` / `accent_color` — hex overlay colors; `text_beats` —
  ≤8 caption lines shown one at a time in place of the static subheadline.
- Conversion graphics: **always set `cta_text`** (≤24, e.g. "Shop now") — plus `offer_text` (≤16)
  only if there's a real deal. Note: blank `cta_text` → the server renders a neutral localized ask
  (e.g. "Learn more"); a truly CTA-less short is not currently supported.
  `badge_text` (≤24) and `star_rating` (0–5) are opt-in and
  user-supplied only, never AI-written — **only ratings/badges you can substantiate; fabricated
  social proof is deceptive advertising**. Edit or clear any of these later with `update_short`
  (`""` / `0` / `[]` clears) — on a generated short, `rerender_short` then applies them for
  0 credits (see "Never re-roll" above).
- End card: `closing_claim` (≤80 — the end-card claim; falls back to the headline), `brand_name`
  (≤40 — shown big on the closing brand-lockup slate when no poster is set; falls back to your
  brand profile's name), `end_card` (`"auto"` default — poster or brand-lockup slate, 14s;
  `"none"` — no end card, 12s, for deliberate loop-style shorts).
- `brand_profile_id` — if you've saved a brand profile (`list_brand_profiles`), pass its id on
  `create_short`: it seeds any blank fields (format, visual_language, font, CTA, accent) and freezes a
  brand snapshot — the fastest way to brand. `language` is a closed set: `en fr es de it nl pl pt ru zh
  ja ko ar` (no region subtags like `en-US`).

For images (0 credits, jpeg/png): `set_short_product { slug, file_path, description? }` weaves a
product photo into the footage (≤20 MiB); `set_short_logo { slug, file_path, position?, treatment? }` adds an exact deterministic overlay (≤5 MiB; never sent to the video model); `set_short_poster { slug, file_path, poster_includes_lockup? }` sets an
end-card still (extends the render to 14s) — set `poster_includes_lockup: true` if the poster already
contains brand name/CTA/badges, so the overlaid claim/CTA is suppressed and doesn't collide.

**Editor (multi-scene):** `create_editor_ad { product_prompt, language }` creates + configures the
project and returns a **free quote** (`charged: false`, with `estimated_credits`) — it does **not**
start generation. To launch, call it again with `max_credits` set to at least the quoted total; the
paid pipeline runs only when `max_credits` covers the estimate. (Raw REST = `POST /api/editor` then
`POST /api/editor/:slug/autopilot` + an `Idempotency-Key`; reuse a key only for transport retries of
that same logical launch, and mint a fresh one for a deliberate resume/restart/cap change. REST accepts optional `{max_credits}` as a
server-enforced cap, while the MCP tool requires the cap as its spending-safety policy. Cost preflight:
`GET /api/editor/:slug/autopilot/cost`.) Then poll `get_status { slug, kind: "editor" }`. To launch
autopilot on an **existing** draft (e.g. one returned by a `make_video` `dry_run`, or after a
top-up): `start_autopilot { slug }` returns the free quote, `start_autopilot { slug, max_credits }`
launches. Follow `generation_summary.status` and its server-provided polling cadence; return a video
only when `generation_summary.available_actions` advertises `download_result`. Legacy
`autopilot_status`, `autopilot_contract_satisfied`, and `latest_render` are display or route-fence
details and do not establish deliverability.

Result URLs are presigned (~24h TTL) — tell the user to download promptly. Offer a suggested
caption/hashtags.

## Granular control (prompt → full ad, no human steps)

When you want to direct the ad yourself — write the scenario, set the exact scene count, hand-write
each scene prompt and the narration — drive the editor step by step instead of autopilot. Autopilot
(`create_editor_ad`) stays the default path (quote, then launch with `max_credits`); this is the
controlled path. The whole loop is agent-only — no human approval step:

> **Scene length:** every **AI-generated** scene renders to a **fixed 8 seconds** — you can't set a
> per-scene duration, so an all-AI ad is `scene_count × 8s` (e.g. 5 scenes ≈ 40s). Need a different
> total? Change the **number of scenes**, not their length. An **uploaded** clip keeps its own native
> duration instead.

1. `create_editor_draft { product_prompt, language?, theme?, voice_id?, export_aspect_ratio?, project_intent? }`
   — creates an editor project **without** starting autopilot (0 credits). Returns a `slug`.
2. `generate_scenario { slug, segments_count? }` **or** `set_scenario { slug, scenario_prompt }` —
   let the model draft a scenario (1 AI assist) **or** write your own (free). `segments_count` is the
   continuous range **3..10** (server default 5).
3. `get_editor { slug, response_format: "concise" }` — **the REVIEW step.** Read back `scenario_prompt`, `segments[]`,
   `narration_script`/`narration_status`, `current_audio`, `current_music`, `latest_render`, `segments_count`,
   `ai_assist_quota`. Decide what to change before spending any credits. Omit `response_format` only
   when you truly need the backward-compatible full payload.
4. `set_scene_count { slug, count }` — grow/shrink the storyboard (range **1..20**). Each AI scene is a
   fixed **8s**, so this is how you set the ad's length (`count × 8s`). It only deletes **trailing
   un-generated** scenes; it never touches a completed or in-progress scene.
5. `set_segment_prompt { slug, segment_id, prompt }` per scene — hand-write each scene (free, **1..2000**
   chars).
6. `generate_segment { slug, segment_id }` one at a time, **or** `generate_all_segments { slug }` to run
   every not-yet-completed scene **sequentially in position order** (each finishes before the next so
   visual continuity carries forward; re-running retries failed scenes and skips completed ones).
   5 credits per scene; stops on the first 402 and reports. **Cheapest path:** to just generate
   everything, skip this and call `render` directly — it auto-generates any ungenerated (or previously
   failed) scenes at the batch rate (4 credits/scene for ≥3). Use per-scene generation only when you want to review or stop
   between scenes.
7. `set_narration_script { slug, script }` (write your own, free, **≤10000** chars) **or**
   `generate_narration { slug }` (1 AI assist). AI narration requires every current scene to be
   completed with a known positive duration and at least one scene without retained original audio;
   an exact retry deduplicates without consuming another assist. Then write a music **`description`**
   (the ≤1200-char direction).
8. `generate_voice { slug, voice_id }` (3 credits; `voice_id` **required** — pick one from
   `list_voices`; choose one whose normalized `languages` contains the project language) and
   `generate_music { slug, description, mood?, genre?, tempo?, instruments? }` (5 credits).
   `instruments` is an array of up to 10 non-empty strings (≤80 chars each), never a comma-separated string.
9. `render { slug }` (0 credits — only auto-charges any still-ungenerated or previously failed scenes) →
   `wait_for_completion { slug, kind: "editor" }` → `download_result`.

Optional AI helpers along the way (each costs 1 assist): `enhance_prompt`, `suggest_next_scene`,
`suggest_music_prompt`.

## Bring your own media (local uploads)

An **editor** project can use the user's **own local files** — video clips on the timeline, or a
product / closing / logo image — all **0 credits**. (A **short** can't take video clips, but it can take
a product image, exact deterministic logo overlay, and end-card poster via `set_short_product` /
`set_short_logo` / `set_short_poster` — see the Short section above.) Same "two ways to drive it" split:

- **MCP (easy):** `upload_video { slug, file_path, add_to_timeline? }` does the whole dance in one call
  (presign → PUT → poll until processed → optionally append the clip). Then `add_segment_from_upload
  { slug, upload_id }` to append a ready clip, or `use_asset_for_segment { slug, segment_id, upload_id }`
  to set a specific scene (it also takes `source_segment_id` to reuse a finished scene). Images:
  `set_product { slug, file_path, description? }`, `set_closing_image { slug, file_path }`,
  `set_logo { slug, file_path }`. For ordinary/new projects, the Editor product photo is a required
  product-identity reference for every generated AI scene; its confirmed description also grounds
  scenario/narration, and the exact photo becomes the closing image unless a separate closing image
  was authored. Small generated label text can still vary. Replacing/removing the photo or changing
  its identity description invalidates completed AI scenes; a raced generation/Autopilot start can
  return `editor_references_changed`, so re-read and retry.
  Editor product images accept jpeg/png ≤8 MiB (8,388,608 bytes), closing images ≤20 MiB
  (20,971,520 bytes), and editor logos ≤5 MiB (5,242,880 bytes; no WebP).
- **Raw REST:** follow the **Uploads & local assets** section of `API.md`. The shape is always
  `presign → PUT raw bytes → confirm`; then poll `GET /api/editor/:slug/uploads` until your upload is
  `status: "ready"`, then place it with `POST …/segments/from-upload` or `…/segments/:id/use-asset`.
  **The gotcha that breaks `curl`:** a single-object PUT (small video + every image) **must** send
  `Content-Type: <the presigned mime>`, but a multipart **part** PUT must send **no** `Content-Type` at
  all (the part URL doesn't sign one). Videos at ≥50 MiB (52,428,800 bytes) use multipart init → sign-part →
  complete flow; small ones use a single presigned PUT.

Video: mp4 / mov / webm / mkv, **≤500 MB decimal (500,000,000 bytes), ≤5 min**. Editor product images are jpeg/png **≤8 MiB (8,388,608 bytes)**,
closing images **≤20 MiB (20,971,520 bytes)**, and logos **≤5 MiB (5,242,880 bytes)** (no WebP). A
just-uploaded clip must finish processing (`status: ready`) before it can be placed — `upload_video`
waits for you; on raw REST you poll. **Only upload files the user explicitly gave you** — never read or
exfiltrate arbitrary local paths.

An editor `video_url` may remain available after the timeline changes. Treat it as current only when
normalized status says `ready:true` and `stale:false`; unknown/absent staleness is not evidence that
the delivered MP4 matches the live timeline.

## Recover a lost or failed render (editor)

An editor render is versioned, so you can recover it without re-driving the pipeline:

- `list_renders { slug }` (`GET /api/editor/:slug/renders`) — every render version with its
  `status`, `video_url`, `thumbnail_url`, `duration_seconds`, `error_message`, `retryable`,
  `combination_hash`, `is_stale`, and `inserted_at`. Use it to recover a finished URL
  after losing it, or to find the render that failed.
- `retry_render { slug, render_id }` — re-run a **failed** render from its exact saved snapshot
  (segments/voice/music as they were). **0 credits** (those scenes were already charged). Only a
  failed render is retryable (`editor_render_not_retryable` otherwise).

## Delegate the whole content operation (plan → draft → review → schedule → measure)

Beyond one asset, you can run the surrounding content operation. Everything through the drafting
stage is **$0** — the planning tools draw the free AI-assist quota (not credits), and materializing
a plan produces **0-credit drafts**. **Generating/rendering each draft is a separate, explicit,
paid step** — never auto-spend a month of content; create the drafts, then generate the ones the
user approves. The whole loop is agent-scoped (`video:read` + `video:generate`); nothing needs
`account:admin`.

1. **Onboard a product.** `extract_product_page { url }` pulls facts + candidate image/logo URLs
   from a public page (SSRF-guarded server-side, **0 credits**). Persist the identity once so every
   later draft inherits it instead of re-describing the brand: `create_product_profile { name, ... }`
   and `create_brand_profile { name, ... }`. Pull the primary image + logo into the profile with
   `import_product_image { product_profile_id, url, link_as }` (`primary_image` default, or `logo`).
2. **Plan (free quota, not credits).** `plan_campaign { product_profile_id | product }` proposes
   angles/formats + a per-item credit **estimate**; `generate_hook_variations { ..., brand_profile_id? }`
   writes 3–6 draftable hook options (each with a script, caption, hashtags, suggested format). On
   **429 `ai_assist_quota_exceeded`**, pass `auto_unlock:true` (1 credit → +10 assists) or write the
   copy yourself.
3. **Pack & materialize ($0).** `create_campaign_pack { title?, product_profile_id?, brand_profile_id? }`
   is the container; `add_pack_variations { pack_id, variations, materialize:true }` persists the
   variations you picked and turns each into an editable **draft** (an editor/short video project or
   an image slider) — ≤12 per call. Use `materialize_pack_item { pack_id, item_id }` to (re)try one.
   `get_campaign_pack { id }` / `list_campaign_packs` show every item's status + **draft slug** +
   credit estimate. Or do steps 1–3 in one call: `create_campaign { url | product_profile_id,
   brand_profile_id?, item_count?, formats? }` — it returns a pack full of drafts.
4. **Generate per draft (credits — a deliberate step).** Present each draft's credit estimate and
   the ~dollar total (up to $0.99/credit), then generate only the drafts the user approves:
   `start_autopilot { slug, max_credits }` (a bare `start_autopilot { slug }` only re-quotes; pass
   `max_credits` ≥ the estimate to actually launch — or use the granular `generate_*` calls) for a
   video, `generate_slider { slug }` for a carousel. Nothing above this line spent video credits.
5. **Review (no-login approval).** `create_review_link { video_result_id | slider_id }` mints an
   expiring page where a human approves / requests changes / comments with **no account**. The
   plaintext token + URL come back **once** — surface them immediately. `list_review_links` shows
   approval activity; `revoke_review_link { id }` kills a link. Gate spend or posting on approval.
6. **Calendar (reminder-only).** `schedule_post { video_result_id, platform, scheduled_at }` stages
   a calendar entry that pushes a **reminder to the user** at the due time — it does **not**
   auto-publish (posting to TikTok/Instagram stays a human step in the app). `list_scheduled_posts`
   is the calendar; `reschedule_post` / `cancel_scheduled_post` manage it. After the user posts the
   presigned export, `mark_scheduled_posted { id }`. All **0 credits**, all `video:generate` — an
   agent token runs the reminder calendar end to end. (Attaching a social account to *auto-publish*
   would need `account:admin`, which agent tokens never carry — leave that to the app.)
7. **Measure → double down.** `record_performance { item_id, platform, views, ... }` logs a snapshot
   for a campaign-pack item; `set_item_attributes { item_id, ... }` tags its creative. Once a few
   items have both, `get_recommendations` returns a **cautious**, sample-size-aware read of your own
   best hook/format — **present its `confidence` + `note` verbatim; never claim causation, ROAS, or
   attribution** (they're correlations in the user's own data, a hint to test). `make_more_like_winner
   { item_id }` spins fresh hooks from a proven item — feed a chosen one back into
   `add_pack_variations` → step 4.
8. **Recurring shows.** `create_series { name, template, brand_profile_id?, product_profile_id? }`
   defines a repeating format; `plan_series_episodes { series_id }` drafts upcoming episode ideas
   (free quota); `materialize_episode { series_id, episode_id }` turns one into a 0-credit draft;
   `get_series_dashboard { series_id }` shows the upcoming / drafted / published lanes;
   `mark_episode_posted { series_id, episode_id }` closes the loop after a manual post.

**Reusable assets.** `list_assets` / `get_asset_quota` / `upload_asset { file_path, ... }` manage a
cross-project media catalog (images + videos) so a clip/logo can be reused across projects. These
need an **active subscription** (402 `subscription_required` otherwise) and cost **0 credits**;
`upload_asset` reads are confined to `HUBFLUENCER_INPUT_DIR`.

## AI assists

AI helper calls (the ones where the model writes content for you) draw from a **free daily quota of
20** per account — separate from credits. Check it with `get_ai_assists` (`{used, limit, bonus,
remaining, resets_at, unlock_cost, unlock_batch_size}`). When it runs out, `unlock_ai_assists` buys
more: **1 credit → +10 assists**.

| Bucket | Operations |
|--------|-----------|
| **Consume 1 AI assist** | `generate_scenario`, `enhance_prompt`, `suggest_next_scene`, `suggest_music_prompt`, `generate_narration` (per-segment regeneration is REST-only — `POST /api/editor/:slug/segments/:id/regenerate-narration`, not an MCP tool), `generate_short_text` |
| **Free writes** (nothing consumed) | `set_scenario`, `set_segment_prompt`, `set_narration_script`, add/reorder segments, `apply_scenario`, `render` |
| **Cost credits** (not assists) | segment generate 5, regenerate 4, voice 3, music 5 |

**`generate_narration` consumes a quota assist** even though its endpoint summary says "free" — that
"free" means free of *credits*, not free of *quota*. Don't assume narration generation is unlimited.

**Two distinct signals — don't confuse them:**

- **429 `ai_assist_quota_exceeded`** = the daily assist quota is used up. The body has
  `remaining: 0`, `can_unlock`, `unlock_cost`, `unlock_batch_size`. Either `unlock_ai_assists` **once**
  or just write the content yourself (`set_scenario` / `set_narration_script` / `set_segment_prompt`).
- **402 `credits_insufficient`** = not enough credits for a generation or unlock. Stop and surface it.

**Bounded accounting loop:** check `remaining`; if it's 0 and you still want the assist, unlock **once**
*or* write the content yourself. Never loop unlocks or retries.

## Field limits

| Field | Min | Max |
|-------|-----|-----|
| editor `product_prompt` (create) | 0 or 10 | 5000 |
| factory `product_prompt` (non-editor) | 10 | 5000 |
| `scenario_prompt` (`set_scenario`) | 1 | 50000 |
| `generate_scenario` `segments_count` (continuous) | 3 | 10 |
| `apply-scenario` `segments_count` (`0` or continuous `3..10`) | — | — |
| scene count via `set_scene_count` | 1 | 20 |
| AI-generated scene duration (fixed, not settable; uploads keep their own) | 8s | 8s |
| segment `prompt` (add/update) | 1 | 2000 |
| segment `narration_text` | — | 5000 |
| factory `narration_script` (`set_narration_script`) | — | 10000 |
| music direction `description` (silently truncated, not 422) | — | 1200 |
| `voice_id` (required, regex `^[A-Za-z0-9_-]+$`) | 1 | 64 |
| `language` (create) | 2 | 10 |
| `product_description` (editor/short product upload) | — | 500 |
| short `headline` (on-screen title; keep ≈4 words / ≤40 chars) | — | 80 |
| short `subheadline` (secondary title) | — | 200 |
| short `closing_claim` (end-card claim) | — | 80 |
| short `brand_name` (end-card lockup) | — | 40 |
| short `music_vibe` | — | 80 |
| AI assist daily quota | 0 | 20 |

`product_prompt` on create is **empty OR 10..5000** (empty = omit it). The full assembled music prompt
(5000) and music timeline lines (240) are server-side internals — don't try to set them.

## Costs

| Action | Credits |
|--------|---------|
| create editor draft | 0 |
| generate segment (single) | 5 |
| regenerate segment | 4 |
| batch generate (per segment, batch ≥3) | 4 |
| generate voice | 3 |
| generate music | 5 |
| render | 0 (auto-charges ungenerated or previously failed segments) |
| short generate | 15 |
| unlock AI assists | 1 credit → +10 assists |

A credit is **$0.99** bought singly (bulk packs and subscriptions cost less per credit) — multiply
to give the user the real price: a short is 15 credits ≈ **$15 max**, a 5-scene autopilot ad
≈ 28 credits ≈ **$28 max**.

## Failure handling

- **402 / "credits_insufficient"** → tell the user the required vs available credits — and what
  the top-up costs ($0.99/credit, less in bulk) — and stop; don't loop.
- **429 / "ai_assist_quota_exceeded"** → the daily AI-assist quota is exhausted (NOT a credit
  problem). Body has `remaining: 0` + `unlock_cost`. Either `unlock_ai_assists` once or write the
  content yourself; never loop.
- **409 / "*_in_progress" / "autopilot_already_running"** → it's already running. Keep polling;
  do not re-POST a new generation.
- **422 / "scenario_prompt_required"** → no accepted brief is saved; provide any one of a structured `scenario_brief`, a ≥10-char `product_prompt`, or a ≥3-char `product_subject`.
- **403 / "insufficient_scope"** → the token lacks `video:generate`. Ask the user for a token
  with that scope.
- **Long waits are normal** (autopilot can run several minutes). Treat a still-progressing run
  as healthy; only give up after a clearly stated max wait (e.g. ~10 min) and report status.

## Out of scope

Publishing directly to TikTok/Instagram needs a human-linked social account (interactive OAuth)
and `account:admin`, which agent tokens never carry. Don't attempt to auto-publish — return the
finished MP4 (or carousel images) and a ready-to-paste caption so the user can post it themselves.
You **can** stage a **reminder-only** calendar with `schedule_post` (it pushes a reminder to the
user, never a publish) and close the loop with `mark_scheduled_posted` after they post — that's the
supported "run my calendar" path.

**Label synthetic media.** These renders are AI-generated: TikTok requires an AI-generated content
label (and attaches C2PA content credentials since 2025), and Meta requires disclosure for
AI-generated/altered content in paid ads. Whenever you hand over an export or stage a posting
reminder, tell the user to enable the platform's AI label.
