---
name: hubfluencer-create
description: Use this skill when the user wants to generate a video ad, short, or social clip with Hubfluencer — e.g. "use Hubfluencer to make an ad for X", "create a short for my product", "make me a TikTok ad". Drives Hubfluencer end-to-end (prompt → finished, post-ready MP4) 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.

## 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 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 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.

## Credits

Generation spends prepaid **credits** (bought in the app). `create_*` is free (0 credits); only
generation/render charges — a short is **15 credits**, editor ads cost more. 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.

## 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, charged }`.
- **Don't ship a bare video.** For a short, pass `headline` (the on-screen title), `subheadline`,
  and a fitting `music_vibe`/`theme` right in the `make_video` call — these are SHORTS fields
  (`theme` applies to both kinds). If the user wants their own product image, brand logo, or closing
  card, the one-shot call can't attach files — drive the granular path instead (`create_short` +
  `set_short_product`/`set_short_poster`, or `create_editor_draft` + `set_product`/`set_logo`/
  `set_closing_image`, then `generate_short`/`start_autopilot`). 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 only starts generation if it's affordable. If it can't afford
  it (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. To **preview without spending**, call
  `make_video({ prompt, dry_run: true })` first, then run it with `generate_short`/`start_autopilot`.
- `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.
- `save_path` downloads are confined to `HUBFLUENCER_OUTPUT_DIR` (or cwd) and must end in `.mp4`.

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

**Short:** `get_credits` (≥15) → `create_short { product_prompt, …creative fields }` (≥10 chars) →
(optional branding: `set_short_product { slug, file_path }`, `set_short_poster { slug, file_path }`) →
`generate_short { slug }` (send an `Idempotency-Key` on raw REST) → poll
`get_status { slug, kind: "short" }` until `stage` is `video_ready`/`failed` → return
`latest_render.video_url`.

**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 (≤160). `subheadline` — the **secondary title** (≤200).
- `music_vibe` — Upbeat (default), Cinematic, Minimal, Luxury, Playful, Jazz.
- `theme` — none / realistic (default) / cinematic / anime / sci_fi / fantasy / noir / superhero /
  horror / mockumentary / sports / gaming / retro_80s / minimalist / cyberpunk.
- `text_position` (top/center/bottom), `text_animation` (reveal/typewriter/fade_in/pop/bounce),
  `font_family`, `music_instruments` — fine styling, usually fine to leave default.

For images (0 credits, jpeg/png ≤ 20 MB): `set_short_product { slug, file_path, description? }` weaves a
product photo into the footage; `set_short_poster { slug, file_path }` sets an end-card still (extends
the render to 14s). Shorts have **no logo overlay** — that's editor-only (see below).

**Editor (multi-scene):** `create_editor_ad { product_prompt, language }` (creates **and** starts
autopilot; raw REST = `POST /api/editor` then `POST /api/editor/:slug/autopilot` with an
`Idempotency-Key`; optional cost preflight `GET /api/editor/:slug/autopilot/cost`) → poll
`get_status { slug, kind: "editor" }`. To start autopilot on an **existing** draft (e.g. one returned
by a `make_video` `dry_run`, or after a top-up), use `start_autopilot { slug }`. **Note:**
`autopilot_status == "completed"` is necessary but not sufficient — wait until
`latest_render.status == "completed"` with a `video_url` before returning it.

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 one-shot path; 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 }` — **the REVIEW step.** Read back `scenario_prompt`, `segments[]`,
   `narration_script`/`narration_status`, `music`, `latest_render`, `segments_count`,
   `ai_assist_quota`. Decide what to change before spending any credits.
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 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, **≤12000** chars) **or**
   `generate_narration { slug }` (1 AI 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`) and `generate_music { slug, description, mood?, genre?, tempo?, instruments? }`
   (5 credits).
9. `render { slug }` (0 credits — only auto-charges any still-ungenerated 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 and an end-card poster via `set_short_product` / `set_short_poster` — see the Short
section above. Shorts have no logo overlay.) 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? }` (then `set_product_placement { slug, mode:
  "throughout"|"end" }`), `set_closing_image { slug, file_path }`, `set_logo { slug, file_path }`.
- **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). Large videos (≳ 50 MB) use the multipart init → sign-part →
  complete flow; small ones use a single presigned PUT.

Video: mp4 / mov / webm / mkv, **≤ 500 MB, ≤ 5 min**. Image: jpeg / png, **≤ 20 MB** (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.

## 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` / `regenerate_segment_narration` |
| **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` (enum `{0,3,5,7,10}`, not a range) | — | — |
| 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`) | — | 12000 |
| 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) | — | 160 |
| short `subheadline` (secondary title) | — | 200 |
| 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 only ungenerated segments) |
| short generate | 15 |
| unlock AI assists | 1 credit → +10 assists |

## Failure handling

- **402 / "credits_insufficient"** → tell the user the required vs available credits 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"** → the brief is too short; set a ≥10-char `product_prompt`.
- **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).
Don't attempt to auto-publish — return the finished MP4 and a ready-to-paste caption so the user
can post it themselves.
