# UGC Swarm content pipeline: guide for agents

This is the whole content pipeline, open to you through `remixctl`, a one-file Python 3 command-line tool with no
dependencies. Use any part of it for whatever your person asks. Everything you start shows up in the web app
(https://hermes-content.tail7d831b.ts.net), in your person's workspace, where people can watch it and approve steps.

## The parts

| Part | What it is | Commands |
|---|---|---|
| Channels | The social media channels we post through, above the personas: each has its accounts (Instagram, TikTok, YouTube, ...) and its host, a persona or an animated character made for it. Start from the channel you're making for. | `GET`/`PUT /api/v1/channels` (below) |
| Personas | AI characters, each with a consistent face, a voice and a profile: industry and focus, format, theme, personality, channels. Every video is made with one, and a persona can make any number of videos. Make a new one any time. | `personas list`, `persona create`, `persona remix`, `persona profile` |
| Video remix | A persona performs a new script in a source video's style. The source is a video link, an upload, a video picked from a creator's profile, or a saved template. | `remix`, `sources list`, `sources pick`, `templates list` |
| What a video is about | A news story, a prompt, or the exact words. With none of these, the persona's theme picks the subject; news, review and commentary personas pick from today's news. | `--news ID`, `--prompt`, `--script-file` on `remix` |
| Copycat | Monitor up to 50 YouTube, Instagram, TikTok and X profiles, cluster specific stories over 48 hours, and prepare original scripts and storyboard stills with an available persona. | REST `/api/v1/copycat`; see Copycat below |
| News | The news aggregator's ranked stories per industry and subcategory, refreshed about every 3 hours, and what's trending on Google right now. Stories trending on Google are marked. | `news list`, `news show`, `news feeds` |
| Pages and carousels | A separate flow, with no persona. A page is a client's social media account: its brand, voice, look and channels. A carousel for it is image slides plus an MP4 slideshow. | `pages list`, `pages create`, `pages studies`, `carousel` |
| Jobs | Every flow above runs as a job. It stops at approvals and keeps everything it makes. | `jobs wait`, `jobs approve`, `jobs download`, `jobs list` |
| Downloads | The server downloads an Instagram, X, YouTube or TikTok video for you (through Apify, which works from a server's address) and hands you the file. | `download`, `downloads get` |
| Edit workspaces | A workspace on the server with your files, where you run Python (Pillow, numpy) and ffmpeg yourself to cut, join, caption and render. You make the edit; the server runs your commands. | `workspace new`, `workspace run`, `workspace get` |
| Video generation | MiniMax H3, the model every shoot uses, makes a video from your prompt and your images, video or audio: via MiniMax's API (the default), or via Comfy, where a still and a voice line become a talking take. Seedance when you ask for it, on BytePlus or through Trioma. Openjutsu, Comfy's character swap, puts a character into a clip in place of the person in it. The server holds the keys; you get the file. Paid. | `generate`, `generate --model h3-comfy`, `generate --model seedance-2.5`, `generate --model seedance-2.5-trioma`, `swap` (or `generate --model openjutsu`), `generations list`, `generations get` |

Each part has its own section below. `./remixctl --help` and `./remixctl COMMAND --help` list every option.

## Real runs and test runs

Every flow runs either way:
- **Real run** (`--real`): the real thing. Paid models make the images, the voice, the video and the words. It needs
  the `runs:real` permission. Before anything expensive is made (a video's shoot), the job stops and says what it costs.
- **Test run** (without `--real`): free. Every step and approval runs with placeholders, to check a flow end to end.

When your person wants the actual video or carousel, use `--real`.

## Set up

```
curl -fsSL https://hermes-content.tail7d831b.ts.net/cli/remixctl.py -o remixctl && chmod +x remixctl
./remixctl login --name "<who you are, e.g. Lena's Claude Code>"
```

The tool grows with the site. When this guide names a command or option your copy refuses, fetch it again with the same
`curl`; copies from 2026-11-08 on say so themselves.

`login` prints a link and a code, then waits up to 10 minutes. Give the link to your person: they open it, log in to the
web app, check the code matches what you show and pick what you may do. Then you're signed in, and you work in their
workspace. Someone without an account asks for one at https://hermes-content.tail7d831b.ts.net/register first; an account from outside the team
can log in once it's approved. On Windows, run the tool as `python remixctl` (in PowerShell, download it with
`curl.exe`). `./remixctl whoami` shows what you may do:

- **Always:** test runs, uploads, reading results and the news, making personas' and pages' profiles and keeping them
  up to date (every save is a new version; the old one is kept).
- **`runs:real`:** real runs, which call Nano Banana, Claude, ElevenLabs, MiniMax and Lightreel, and AI drafts of a
  profile.
- **`jobs:approve`:** technical permission to submit decisions; human review requirements and explicit generation authorization still apply.
- **`voices:shared`:** choose characters' voices from the team's ElevenLabs library (the server's account) while your
  person's account hasn't connected its own. Every agent has it, on the team or not, with no box to tick (since
  2026-11-09).

To get a permission you don't have, run `./remixctl login` again and ask your person to tick it when they approve you.
Paid real runs also have to be on for your person's account: a new account from outside the team starts with test runs.

Put `--json` before the command for machine-readable output (`./remixctl --json jobs wait JOB`). Never print or share
your key: remixctl keeps it in `~/.config/remixctl/config.json` (mode 600).

## How a job works

A job runs step by step and stops at approval steps ("gates"). `./remixctl jobs wait JOB` waits until the job needs a
decision or is finished. It then says what is waiting, what to look at and which decisions it takes, plus the job's
web link. Long text to review, such as the script, is in `pending_approval.options.previews` in the `--json` output.
Images and videos are easiest to review in the web app.

If you don't have `jobs:approve`, give your person the web link: they approve in the web app, and you keep waiting.
Approve with `./remixctl jobs approve JOB -d DECISION [-c "notes"]`. A `redo` always needs notes saying what to change.

## Channels: where we post, and their characters

A channel is the top of everything (Andrew, 2026-11-02). It's one brand we post through, with:
- what it posts about: `topic`, `industry`, `focus` and `format`;
- its social accounts;
- its characters (`hosts`): one of the personas, or an animated character that exists only for that channel;
- an asset library (its files);
- a video library.

Every script for a channel is written from its topic, industry and format, in its character's own personality. A
character is a persona in the API: `/api/v1/personas`, a job's `persona`, and the persona profile keep that name. Use
your key as `Authorization: Bearer $TOKEN`, with `https://hermes-content.tail7d831b.ts.net` as the server.

- `GET /api/v1/channels` lists the channels. Each has:
  - `topic`, `industry`, `focus` and `format`;
  - `accounts` (platform, handle, url, and an id like `instagram:handle`);
  - `hosts`: each with its `cid`, and an animated one with the `folder` its files are in;
  - `status`: `planned` until it has an account.
  `GET /api/v1/channels/<id>` adds the channel's `files`.
- `PUT /api/v1/channels/<id>` makes or replaces one of your workspace's channels (`personas:write`). The id is lowercase
  letters, digits and `-`. The body:
  `{"name": "Fresh Commits", "about": "repo and idea news", "industry": "tech_ai", "focus": ["developer"],
  "format": "news", "topic": "What it posts about: new open-source repos\nStays away from: hype",
  "accounts": ["https://www.instagram.com/freshcommits/", "tiktok:freshcommits"],
  "hosts": [{"animation": "Patch", "about": "a white robot anchor with a CRT head",
  "image": "characters/patch/cover.png", "voice": "Liam"}]}`.
  - The industries, focus and formats are the persona profile's (`GET /api/v1/profile-options`).
  - A persona character is `{"persona": "<slug from GET /api/v1/personas>"}`.
  - An account is a profile link or `platform:handle`.
  - Fields the app doesn't use are kept in CHANNEL.json when it's saved.
- An animated character's files go in its own folder in the channel:
  - `characters/<id>/CHARACTER.md`;
  - its sheets (turnaround, expressions, mouth shapes, poses);
  - `refs/`, and `set/` for backdrop photos.
  `GET /api/v1/channels/<id>/characters/<cid>` returns the character with its Markdown, `sheets`, `refs` and `set`. A
  persona's set photos for a channel go in `characters/<persona slug>/set/`.
- `PUT /api/v1/channels/<id>/files/<path>` stores a file in the channel (`uploads:write`). The raw bytes are the body,
  e.g. `curl -T reel1.mp4 -H "Authorization: Bearer $TOKEN" https://hermes-content.tail7d831b.ts.net/api/v1/channels/fresh-commits/files/reels/reel1.mp4`.
  - A path is up to four parts, like `reels/reel1.mp4` or `characters/patch/refs/wave.png`.
  - Files can be images, video, audio, JSON, Markdown and text.
  - `GET` the same path fetches the file.
  - Upload a character's picture first, then name it as its `image`.
- A remix is for a channel: `"channel": "<id>"` in `POST /api/v1/remix` or a `video_remix` job's inputs. Without one, it's
  the persona's own channel when the persona is on just one. `"account": "instagram:handle"` makes it for one of the
  channel's accounts.
- **Templates** (Andrew, 2026-11-03: every character is a template): a channel's `templates` are its characters and how
  each one is filmed. `GET /api/v1/channels/<id>` gives them, each with a `title` and what it does (`about`). `PUT` the
  channel with `templates` to change them; what GET adds to one may be sent back as it is.
  - A human one: `{"kind": "human", "persona": "<slug>", "set": "characters/<slug>/set/studio.jpg", "format":
    "talking_head" or "broll", "broll": "stills", "video" or "mixed", "name": "…"}`. `set` is the background photo
    they're filmed in (made in the Set Builder); without one, the source video's room.
  - An animated one: `{"kind": "animated", "character": "<cid>", "style": "templates/<name>", "name": "…"}`; without a
    style, the one that fits best.
  - A video from a template takes its parts: a `video_remix` job (or `POST /api/v1/remix`) with `persona`, `channel`,
    `set`, `content_format` and `broll_mode`, plus the link to remix; or an `animated_remix` job with `channel`,
    `character` and `template` (its style).
  - A remix's `set` is a photo in its channel's folder. Every frame puts the persona in that room; the source video gives
    only the pose, framing, expression and clothing. The job keeps its own copy of the photo.
- **The video library:** `GET /api/v1/channels/<id>/videos` lists the channel's finished videos, newest first, with
  where each was posted. Every job in `GET /api/v1/jobs` has its `channel`.
- **For the posting app (one set of endpoints per social account):**
  - `GET /api/v1/channels/<id>/accounts` lists the accounts and their ids.
  - `GET /api/v1/channels/<id>/accounts/<account>/queue` lists the approved real videos not posted there yet, oldest
    first, each with its `download` path (`jobs:read`).
  - `POST /api/v1/channels/<id>/accounts/<account>/posts` with `{"job_id": "…", "url": "https://…", "external_id": "…",
    "posted_at": "2026-11-03T14:05:00Z"}` says one went out (`posts:write`, granted on the approval page).
- The team's library channels come from `channels/<id>/CHANNEL.json` next to the persona library, in the same format.
  They're read-only through the app.
- **Where a channel posts, and its warm-up:** a channel also has:
  - `platforms`, among instagram, tiktok, x, threads, facebook, reddit, linkedin and youtube;
  - `keywords`, as `{"Tech": ["ai agents", "gadgets"], "Gaming": ["indie games"]}`, at least 3 in all;
  - `schedule`, as `{"instagram": {"per_day": 2, "times": ["09:00", "18:30"]}}`;
  - `devices`: the farm's phone that runs each platform's account.

  For the farm:
  - `GET /api/v1/channels/<id>/warmup` gives the keywords the warm-up follows.
  - `GET /api/v1/farm/devices` lists the phones and which channel each one runs.
  - `GET /api/v1/channels/<id>/accounts` gives `deployments`, each with its status: `setting_up`, `warming_up`,
    `ready`, `live`, `paused` or `blocked`.
  - `POST /api/v1/channels/<id>/accounts/<account or platform>/status` with `{"status": "live", "note": "…"}` reports
    a status (`posts:write`).
  - Statuses and phones are sample data until the farm reports its own.
- **Automations** make a channel's content on a schedule, as free test runs unless one is switched to real.
  - `GET /api/v1/channels/<id>/automations` lists them.
  - `POST` the same path makes one: `{"name": "…", "kind": "template_remix" or "animated_remix", "character": "<cid>",
    "template": "<saved template id, or templates/<name> for animated>", "subject": "news" | "keyword" | "topic",
    "days": ["mon", …], "time": "09:00", "tz": "Europe/Berlin", "real": false}`.
  - `PATCH /api/v1/channels/<id>/automations/<aid>` changes one, e.g. `{"enabled": false}`.
  - `POST /api/v1/channels/<id>/automations/<aid>/run` runs it now.

### Putting a channel up for approval

A drafted channel waits for a yes before it goes live (the Review page). Put it up by saving it with a `review`:

```json
"review": {"state": "pending", "brief": "what it is and why, in a paragraph", "verdict": "can AI make it, and how",
           "cast": "who hosts it, to approve", "references": [{"url": "https://www.instagram.com/reel/...", "title": "...", "note": "why it matters"}],
           "clips": [{"title": "Rack cam v3d", "path": "review/rack_v3d.mp4", "note": "the keeper"}],
           "checklist": [{"text": "Rack photos from Andrew", "done": false}]}
```

The clips are files in the channel's folder (`PUT /api/v1/channels/{id}/files/review/<name>.mp4` first). A channel with a pending
review has `status: review`; after the decision it's `approved` or `returned` (with `review.note`), and `active` once it has
accounts. `POST /api/v1/channels/{id}/review` with `{"action": "submit" | "approve" | "return", "note": "..."}` makes the decision
through the API; the note is required when sending it back. Approving and sending back need Can approve on your person's account
and the approve permission on you; a send-back's note also becomes an open request.

### Work on a channel with the team

Everyone on the team and their agents work on a channel together on its collab page, `/channels/<id>/review`: its test clips and
their versions, references, comments and requests, and a log of everything. These commands need a current `remixctl`: if it says
`review` isn't a command, fetch it again (`curl -fsSL https://hermes-content.tail7d831b.ts.net/cli/remixctl.py -o remixctl`).

What you may do: anyone reads, comments, asks, adds references and uploads versions. `regenerate` is paid, so it needs paid runs on
your person's account and the paid runs permission on you. Without them, leave the request for someone who has them.

**Start here when your person asks you to look at the review:**

```
./remixctl review inbox                  # what the team wants made, on every channel
./remixctl review show rack-cam          # its clips (with their ids), references and open requests
./remixctl review log rack-cam           # what's been happening, and what's been made before
./remixctl review claim rack-cam 42      # you're on request 42: two agents can't both take it
./remixctl review recipe rack-cam 8f3a1b2c   # the nodes that made a clip
```

**A clip's recipe** is a Markdown file next to it in the channel's folder (`review/x.md` for `review/x.mp4`). It says who made the clip,
what was asked for, each node in order (model, full prompt, references, length, ratio, seed, cost) and how to make it again. Read it
before you remake anything, and keep whatever nobody asked to change.

**Then make the new version one of two ways:**

- **The server makes it (paid, one generation only):** say what should change, and Claude rewrites the prompt and leaves the rest:
  ```
  ./remixctl review regenerate rack-cam 8f3a1b2c --change "slower pull-back, keep the phones in focus" --request 42
  ```
  Or send the whole prompt with `--prompt-file new.txt`. `--ref PATH` (repeat) replaces the references, and `--duration`, `--ratio`,
  `--resolution`, `--no-audio`, `--seed` and `--model` change those. It waits until the version is ready; `--no-wait` doesn't. A failed one stays in
  the history with the reason, and its request opens again.
- **You make it with any tools** (a workspace with ffmpeg or Python, `generate`, your own machine). Write a recipe with the nodes you
  ran: each tool, command, input and output, in order, so someone else can make it again. Then:
  ```
  ./remixctl review add rack-cam out.mp4 --from 8f3a1b2c --recipe RECIPE.md --request 42 --note "new rack, same move"
  ```
  If you used `generate`, `./remixctl review add rack-cam --generation ID --from 8f3a1b2c --request 42` is enough: the server copies the
  video in and writes the recipe. A finished job works the same way with `--job ID`.

A new version leads its clip from then on, and `--request` marks the request done (`./remixctl review done rack-cam 42` otherwise).
Leave `--from` out for a new clip. `./remixctl review use rack-cam ID` goes back to an older version; nothing is deleted.

**Approve and Reject.** Each clip has its own verdict, shown in `review show`. `./remixctl review approve rack-cam 8f3a1b2c` says this version
is good (it needs the approve tick, and it doesn't decide the channel). A rejection says what's wrong and how to go on, and costs nothing:

```bash
./remixctl review reject rack-cam 8f3a1b2c --issue slop --issue character --comment "the pull-back is too fast" --model h3
./remixctl review reject rack-cam 8f3a1b2c --scratch --issue character --character human --character-name Mikkel \
  --character-about "a calm Danish engineer" --remix-url https://www.instagram.com/reel/xyz/ --ref look.png
```

Issues: `slop`, `character`, `body`, `motion`, `text`, `camera`, `scene`, `sound`, `reference`, `story`, `other`. It's an open request, so it shows
in `review inbox`, with what was ticked in its `data`. The clip's comments and open requests fold into it, so it says everything. Editing means making a
new version from it: `./remixctl review regenerate rack-cam 8f3a1b2c --request 42` turns all of it into the new prompt and uses the model it names.
With `--scratch`, read its `data`: `character` (keep, human or animated, a name and a description), `remix` (a link and what it should be) and `references`.
Make those, add the video with `review add ... --request 42`, and the request is done. `review reject rack-cam channel --comment "..."` rejects the
whole channel, which sends it back.

**Discard.** `./remixctl review reject rack-cam 8f3a1b2c --discard [--comment "a duplicate"]` says this version isn't wanted and nobody should remake it.
It's a line in the log, not a request: the open requests on the version close, and its file stays. If the clip has another version, the newest of those
leads again. If none is left, the clip has `discarded: true` in `./remixctl --json review show rack-cam`, and people see it under Discarded. Never remake a
discarded version. `./remixctl review restore rack-cam 8f3a1b2c` brings it back as it was.

**Stills.** A clip whose file is a picture has `kind: "image"`. People decide stills in a grid on the channel's page, ten at a time: Approve, Reject (which
discards the still) or a line saying what should change. That line reaches you as an edit rejection in `review inbox`, with `kind: "image"` in its `data`. The
server can't remake a still, so you do: make the new one, then `./remixctl review add henry-aldridge new.png --from CLIP --request 42 --recipe RECIPE.md`.
The new version leads with no verdict, and the person decides again. To upload a batch of new stills, add each as its own clip (leave `--from` out).

**The rest:** `review comment rack-cam "text" [--clip ID]`, `review ask rack-cam "what should change" [--clip ID]` (an open request),
`review ref rack-cam FILE_OR_LINK --note "why"` (a file goes in the channel's `references/`), `review close|reopen rack-cam 42`,
`review decide rack-cam approve|return|submit --note "..."`, and `jobs recipe ID` for a job's steps.

The same through the API (`GET` needs `personas:read`):
- `GET /api/v1/channels/<id>/review`, `GET /api/v1/channels/<id>/log` (`?format=md`), `GET /api/v1/review/requests`,
  `GET /api/v1/channels/<id>/clips/<clip>/recipe`, `GET /api/v1/jobs/<id>/recipe`.
- `POST /api/v1/channels/<id>/comments` with `{"body": "...", "clip": "...", "ask": true}` (`jobs:write`).
- `POST /api/v1/channels/<id>/requests/<request>` with `{"action": "claim" | "done" | "close" | "reopen", "note": "..."}` (`jobs:write`).
  Claiming one somebody took answers 409.
- `POST /api/v1/channels/<id>/references` with `{"url": "..."}` or `{"path": "references/x.jpg"}`, plus `title` and `note` (`uploads:write`).
  The file goes up first with `PUT /api/v1/channels/<id>/files/references/x.jpg`.
- `POST /api/v1/channels/<id>/clips` with `{"generation": "..."}`, `{"job": "..."}`, or `{"path": "review/x.mp4", "recipe": "# ..."}`, plus
  `from`, `request`, `title` and `note` (`uploads:write`). A file's recipe is required.
- `POST /api/v1/channels/<id>/clips/<clip>/regenerate` with `{"change": "..."}` or `{"prompt": "..."}`, plus `references`, `duration`,
  `ratio`, `resolution`, `audio`, `seed`, `model` and `request` (`jobs:write`, `runs:real`).
- `POST /api/v1/channels/<id>/clips/<clip>/show` makes that version lead (`jobs:write`).
- `POST /api/v1/channels/<id>/clips/<clip>/approve` says this version is good (`jobs:write`, `jobs:approve`).
- `POST /api/v1/channels/<id>/clips/<clip>/reject` with `{"mode": "edit" | "scratch", "issues": [...], "details": {"slop": "..."}, "comments": [...], "model": "...",
  "references": [{"url": "..."}, {"path": "references/x.jpg"}], "character": {"kind": "keep" | "human" | "animated", "name": "...", "about": "..."},
  "remix": {"url": "...", "about": "..."}}` (`jobs:write`). `POST /api/v1/channels/<id>/reject` does the same for the whole channel, with mode `scratch`, and sends it
  back (`jobs:approve`). `{"mode": "discard", "comments": ["why"]}` discards the version instead: nothing is remade.
- `POST /api/v1/channels/<id>/clips/<clip>/restore` brings a discarded version back (`jobs:write`).

A generation can also take a channel's file as an input: `{"channel": "rack-cam", "path": "references/x.jpg", "role": "reference_image"}`.

## Animated remixes: render and deliver

An animated remix copies an original reel with one of a channel's animated characters. Only the topic changes, and a
little of the character. Anyone can start one from the character's page, or with a `POST /api/v1/jobs` like
`{"pipeline": "animated_remix", "inputs": {"channel": "<id>", "character": "<cid>", "template": "templates/<name>",
"source_url": "…", "topic": "…", "real_run": false}}`. The job writes a brief, then waits at `agent_render` for you to
render the reel on your own machine. Hermes has no Node or Chromium.

1. Find waiting work with `GET /api/v1/approvals`, looking for kind `agent_delivery`. Its `options.previews[0].text` is
   BRIEF.md: the copy rule, the topic, the template's and style's notes, the character's CHARACTER.md, and every file
   to fetch with `GET /api/v1/channels/<id>/files/<path>`.
2. Render it at 1080x1920.
3. Deliver it as multipart:
   `curl -F video=@reel.mp4 -F files=@beats.json -F notes="…" -H "Authorization: Bearer $TOKEN" https://hermes-content.tail7d831b.ts.net/api/v1/jobs/<job>/deliver`
   (`jobs:write`).
4. The person who ordered it approves it, or asks for a change. A change comes back as a new brief with the change in
   it, so render and deliver again.

Once approved, the reel is the job's final video, in the channel's video library and its accounts' posting queues.

## Make a persona

**Your own character**, from a description and photos of one person (20 or more photos is best):

```
./remixctl persona create --name "Nova Lind" --prompt "late-20s baker in Copenhagen, warm, direct" \
    --photo a.jpg --photo b.jpg --md persona.md \
    --industry food --format education --channel https://www.instagram.com/novabakes/ \
    --theme "What they talk about: sourdough for busy people" --rights [--real]
```

`--industry`, `--format`, `--channel`, `--theme` and `--personality` are optional: they start the persona's profile
(below). A real run drafts the rest with the character, and keeps what you gave as you gave it.

**Remix a persona** from a creator's profile. Persona Scout studies the profile and drafts 10 original characters for
the same niche. They are never lookalikes of the creator.

```
./remixctl persona remix https://www.instagram.com/handle/ --rights [--real]
./remixctl jobs wait JOB                                  # waits at phase1_lock: 10 options
./remixctl persona pick JOB --option C --name "Remy Cole" [--changes "shorter hair"]
./remixctl persona pick JOB --none                        # none fit: 10 new ones
```

Both kinds then stop at:
- `sheet_check`: approve the character from every angle (`-d pass`), or `-d redo -c "what to change"`.
- `voice_pick`: list the voices with `./remixctl voices` (the team's ElevenLabs library, unless your person connected
  their own), then `./remixctl persona voice JOB --voice-id ID`. A voice from the team's library is always spoken with
  the team's key, so nobody needs an ElevenLabs account of their own, on the team or not.

The finished persona appears in `./remixctl personas list` (its slug is what remixes use).

## Persona profiles: industry, format, theme, personality, channels

Every persona has a profile, and every script it says is written from it, whether the video remixes a source or starts
from scratch. The words change from video to video; the persona and its way of talking don't. A profile is drafted
once, when the persona is made, and stays as it is until someone edits it. An edit applies from the next script.

- **Industry:** the broad industry the persona works in. It sits on top of the format: each industry offers its own
  formats, so changing the industry changes the formats to choose from.
- **Focus:** the industry's subcategories the persona covers most, strongest first (`focus`, a list of ids). Content
  overlaps: its news is its focus plus whatever is hot in the rest of the industry.
- **Format:** the kind of content the persona makes within its industry. It stands apart from the theme: it decides
  which content templates automations can use for the persona, how each script is built, and whether its facts are
  researched first.
- **Theme:** a prompt, in your own words, for what the persona talks about.
- **Personality:** a prompt for how the persona comes across and talks on camera.
- **Channels:** the accounts the persona posts to: a platform and a handle each. A video is made for one of them.

| Format | What it is | Researched |
|---|---|---|
| `news` | News: what's new in the niche, every fact from a cited source | yes, every fact cites its source |
| `education` | Education: explainers and how-tos | yes, every fact cites its source |
| `review` | Reviews: products and tools, tried and rated | yes, every fact cites its source |
| `commentary` | Commentary: takes and reactions on what's happening | yes, every fact cites its source |
| `entertainment` | Entertainment: stories, skits and humour | no |
| `lifestyle` | Lifestyle: everyday life, routines and personal stories | no |

| Industry | Formats it offers | Subcategories (focus) |
|---|---|---|
| `tech_ai` (Tech & AI) | all of them | `ai` (AI: everything AI in one bucket, from models and labs to tools, tips, creative generators, regulation and safety); `developer` (Developer: coding, dev tools, GitHub trends, security and vulnerabilities, including cybersecurity for regular people); `business` (Business & money: funding, IPOs, earnings, stock moves, startups, Big Tech strategy); `hardware` (Hardware & devices: phones, chips, robotics, EVs, wearables); `science` (Research & science: breakthroughs, biotech, space) |
| `business` (Business & marketing) | all of them | none yet |
| `finance` (Finance & money) | all of them | none yet |
| `health_fitness` (Health & fitness) | all of them | none yet |
| `beauty_fashion` (Beauty & fashion) | all of them | none yet |
| `food` (Food & cooking) | all of them | none yet |
| `travel` (Travel) | all of them | none yet |
| `gaming` (Gaming) | all of them | `releases` (Releases & reviews: new games, reveals and trailers, release dates, reviews, updates, DLC, leaks and rumours about upcoming games); `business` (Industry & business: studios and publishers as companies: deals, earnings, sales, layoffs, executives, lawsuits and regulation); `esports` (Esports & competitive: tournaments and results, leagues, teams, rosters, pro players); `community` (Community & culture: players and fan reactions, streamers and creators, mods, viral moments, awards shows, film and TV adaptations); `platforms` (Hardware & platforms: consoles, handhelds, PC gaming hardware, stores, Game Pass and PS Plus, cloud gaming) |
| `pop_culture` (Entertainment & pop culture) | all of them | none yet |
| `education_careers` (Education & careers) | all of them | none yet |
| `home_family` (Home, family & lifestyle) | all of them | none yet |
| `sports` (Sports) | all of them | none yet |
| `other` (Something else) | all of them | none yet |

The prompts are free text. These are what helps a script most, written as `Label: text` lines (a remix with no prompt
picks its subject from the theme's `Topics:` line, among others):

- Theme: `What they talk about` (the niche, in one line), `The angle` (what makes their take theirs), `Topics` (recurring subjects, separated by ;), `Stays away from` (subjects they avoid), `Audience` (who watches), `Language` (the language of their videos).
- Personality: `Who they are on camera` (two or three sentences), `Tone` (in a few words), `How they talk` (sentence length, vocabulary, pace, slang, how they talk to the viewer), `On camera` (energy, gestures, expressions), `Catchphrases` (an opener, a sign-off), `Opinions` (stances that colour their takes), `Always` (rules every script follows), `Never` (things they never say or do), `Lines they'd say` (a few, in their own voice).

```
./remixctl persona profile SLUG                       # read it (the same text as the persona's PERSONALITY.md)
./remixctl --json persona profile SLUG > p.json       # the full shape; edit it, then:
./remixctl persona profile SLUG --set p.json          # replace it (a new version; the old one is kept)
./remixctl persona profile SLUG --patch '{"industry": "tech_ai", "format": "news"}'   # change only the fields given
./remixctl persona channel add SLUG https://www.tiktok.com/@handle     # or platform:handle
./remixctl persona channel remove SLUG tiktok:handle
./remixctl persona profile SLUG --draft [--notes "more playful"] [--save]   # Claude drafts it from the persona's files
```

Platforms: `instagram`, `tiktok`, `youtube`, `x`, `facebook`, `threads`, `linkedin`, `snapchat`, `reddit`. Change a persona's profile only when your person
asks: it changes every video that persona makes from then on.

## Remix a video

The persona performs a new script in the source video's style: same structure, pacing and look, in the persona's own
personality. A persona is its channel: the video is for the persona's own channels, so there's no channel to pick.

```
./remixctl personas list
./remixctl remix --persona SLUG --url https://www.instagram.com/reel/XYZ/ \
    --prompt "this week's biggest AI launches, for people who build with AI" [--broll] --rights [--real]
```

- **The source** is one of: a video link with `--url`, your own file with `--video clip.mp4`, or a saved template with
  `--template ID` (see `./remixctl templates list`).
- **From a profile:** `./remixctl sources list https://www.instagram.com/handle/` lists its latest videos. Then run
  `./remixctl sources pick LIST N --persona SLUG [--prompt "..."] [--broll] --rights`.
- **The words:** `--prompt` is what the new video is about. `--news ID` makes it about a story from the news (see
  below), and `--prompt` is then your angle on it. Leave both out and the persona's theme picks the subject (a different
  one from its recent videos). Personas whose format is news, education, review or commentary are researched on the
  web, and every fact cites its source. `--script-file words.txt` uses your exact words instead.
- **The format:** a talking head by default: the persona on camera the whole time. `--broll` makes a B-roll video: the
  persona on camera, with the story's pictures and headlines (news cards and the cited pages' own pictures) cut in.
- **Moving B-roll:** add `--broll-mode video` to collect direct MP4/WebM footage from cited pages, or `--broll-mode mixed`
  to also plan up to two labelled AI illustrations. API field: `broll_mode` (`stills`, `video`, `mixed`; default `stills`).
  These modes build clean presenter frames from the persona's approved hero; the source guides the edit, without copying
  its presenter, captions or product overlays into those frames. Read `options.broll_review` at frames approval: it lists
  the source videos, acquisition gaps and proposed illustration prompts. Inspect source footage for relevance.
  Generated images and H3 clips wait for shoot confirmation. The packet's estimate includes generated video (up to $0.96
  at the harness rate assumption), but excludes image and editing charges. Preserve the visible AI illustration label.
  Embedded players/HLS can fall back to stills. Mixed mode requires the agent edit and no `edit_from` replay.
- **What happens:** the source is analysed, then its frames are made with the persona in them while, at the same time,
  the script is written in three mildly different versions (the same story and facts; a different hook, wording and
  emphasis) and each is recorded in the persona's voice. For a B-roll video the pictures and headlines are gathered then
  too. The video is as long as the voiceover you pick: at most 3 minutes, and the video runs at most 10 s past it.

The steps it stops at, in order:

| Gate | What it is | Decisions |
|---|---|---|
| `frames_approval` | Everything the video is made from: the persona in each keyframe, the three voiceovers (`options.versions`: each one's script, length and audio), and a B-roll video's pictures (`options.broll_artifact_ids`) | `pass --version N` (the voiceover to film), `redo --redo frames\|voice\|both -c` |
| `shoot_confirm` | **Paid** filming from the picked voiceover; `options.packet.est_usd` is the cost | `confirm`, `hold` |
| `clip_approval` | The filmed clips | `pass`, `redo -c` |
| `final_approval` | The edited video | `pass`, `redo -c` |

Listen to the voiceovers before you pick one: `./remixctl jobs download JOB --kind voiceover --to takes/`, or give your
person the web link. A version marked `blocked` can't be filmed (it runs over 3 minutes, or a chunk's line over the video
model's 15 s): pick another, or redo the voiceovers. A redo says what to make again: `--redo frames` keeps the
voiceovers, `--redo voice` keeps the frames (and rewrites all three versions with your notes).

When it's finished, download the video with `./remixctl jobs download JOB --to out/`. Save a remix you like as a template
with `./remixctl jobs template JOB`, to make it again with another persona.

## News: what's happening in a persona's industry

The news aggregator ranks each day's stories by how widely they're covered (major media count more than posts) and
sends its runs here. A video can be about a story: the script is written from the story's coverage, every fact cites a
page, and a B-roll video gets news cards (each outlet's own headline and date) and the articles' own pictures.

```
./remixctl news list --persona SLUG                     # its focus plus what's hot in its industry, last 72 hours
./remixctl news list --industry tech_ai --sub developer --window daily -n 20   # one subcategory
./remixctl news list --industry gaming                  # another industry's news
./remixctl news list --industry trending                # what's trending on Google right now, across topics
./remixctl news show ID                                 # every outlet that covered it, with links
./remixctl remix --persona SLUG --url https://www.instagram.com/reel/XYZ/ --news ID [--prompt "your angle"] --rights
```

- **Industries with news:** `./remixctl news feeds`: Tech & AI and Gaming so far, plus `trending`. A persona's list
  starts in its own industry, but a video can be about a story from any of them.
- **Trending on Google:** a story that's trending on Google Trends right now carries `trend` (the search term, its
  searches such as `500K+`, and since when); `news list` shows its searches in the `google` column, and the web app
  marks it. `trending` isn't an industry: it's what's trending on Google across topics outside the industries the
  aggregator covers, each story with a research brief (`news show ID`) whose facts quote their articles. The script is
  written from that brief.
- **Subcategories:** each story is sorted into the subcategories of its industry it touches, often several: an AI-found
  security flaw is both `ai` and `developer`. A persona's or a page's list is its focus plus the industry's top 3 and any
  major story, marked hot when they're outside the focus: content overlaps. Stories that fit no subcategory (games,
  apps, politics) are only in the industry's whole list. Windows: `72h` (the aggregator's main view, the default) or
  `daily`. The aggregator's own segments are still there for the API (`segment=`).
- **With `--news`,** `--prompt` is your angle, for example "what it means for small creators". One video can be about
  up to 3 stories (repeat `--news`). `sources pick` takes `--news` too.
- **With no `--news`, `--prompt` or `--script-file`,** news, review and commentary personas pick from their focus plus
  what's hot in their industry, skipping the stories they already made videos from (`news list --persona` marks those
  `made`). News, review and commentary pages do the same for a carousel with no subject.
- **How fresh:** every list says when its run was made, and a run older than 36 hours is marked stale. Check it before
  you call something today's news.
- **Where it comes from:** the app pulls the aggregator's newest run from GitHub every 15 minutes; `news feeds` says
  when it last checked. Sending a run by hand (`./remixctl news push top.json`) is only for the aggregator, and needs
  the `news:write` permission, which a teammate grants on the Agents page.

## Carousels for a page

A page is a client's social media account that posts image carousels: like a persona, without a person. Make one per
client, then make carousels for it: slides for TikTok photo posts and Instagram carousels, plus an MP4 slideshow of
them for Reels and Shorts.

```
./remixctl pages create --name "Peak Peptides" --industry health_fitness --format education [--focus SUB ...] \
    --about "What it sells: research peptides
What it posts about: recovery and training science
Topics: sleep; protein timing; deloads
Audience: lifters over 30" --voice "Tone: calm expert, never hype
Call to action: Save this for your next block" --look "Colours: charcoal, cream and lime" \
    --channel https://www.tiktok.com/@peakpeptides
./remixctl carousel --page peak-peptides --prompt "5 sleep habits that speed up recovery" --style grid --aspect 9:16 \
    --slides 7 --research --real
./remixctl jobs wait JOB                                # stops at slides_approval
./remixctl jobs download JOB --kind slide --to slides/   # read every slide, then: jobs approve JOB -d pass
./remixctl jobs download JOB --kind archive --to out/    # after approval: the ZIP (slides, caption, plan)
./remixctl jobs download JOB --kind video --to out/      # and the MP4
```

- **What it's about:** `--prompt`, or `--news ID` (a story from `news list --industry` the page's industry; `--prompt`
  is then the angle), or neither: what the page posts about picks the subject.
- **`--research`** has Lightreel find the best-performing carousels on the subject first (free; it takes a few
  minutes). The slides follow what the best ones have in common, in fresh words: never their text, claims or images.
- **The look:** `--style text` (big words on a designed slide), `grid` (a labeled grid of objects on each slide), `card`
  (the words above a picture, like the NEXTFRAME posts; needs `--ai-images`) or `photo` (a full picture with the words on
  it; needs `--ai-images`). `--aspect 4:5` for an Instagram post, `9:16` for
  TikTok, Reels and Shorts. `--slides` counts every slide, from 3 to 10: the hook, the middle and the call to action.
- **Pictures:** graphic design by default (free). `--ai-images` has Nano Banana make one picture per slide, or one per
  item on a grid, in a real run.
- **Test run first:** without `--real` it's free: the real layout and brand with placeholder words. A real run has
  Claude write the slides (about $0.05) and runs the research and the AI images.
- **The review:** `slides_approval` shows every slide and the words. `-d redo -c "notes"` rewrites them with your notes.
- `pages show SLUG`, `pages set SLUG --voice "..."` (only what changes), `pages list`.
- **Researched pages:** `./remixctl pages studies` lists the content studies in the repository, and `pages add-study
  nextframe` adds ChatGPT's NEXTFRAME study: the page, its 10 researched series, and its 3 finished carousels.
  `pages show nextframe` lists the series with their test priorities; `carousel --page nextframe --series 2` makes the
  next post in series 2, following its hook, slide sequence and limits.

**A finished plan instead:** an agent that writes its own slides can send a `slideshow` job whose inputs contain
`carousel_plan`, an inline `carousel.plan.v1` object, rendered as free graphics with the same review and export:

```bash
./remixctl jobs create slideshow --json-file inputs.json --wait
```

`inputs.json` contains `{"carousel_plan": {...}}`. Plan fields: `schema_version` (`carousel.plan.v1`), `title`,
`audience`, `brand`, `mode` (`listicle|story|comparison|themed_set`), `aspect` (`4:5|9:16|1:1`), `style`
(`editorial|object_grid`), `caption`, optional `fixture`, and 3–10 ordered `slides`. Each slide has `title`, `text`,
`role` (`hook|setup|payoff|item|comparison|cta`), `visual_prompt`, `seconds` (1–10), and optional `source_urls`.
An object-grid slide also has `items` (up to nine objects, each with `label` and `visual_prompt`). Agents with a repo
checkout can run `python -m carousel_pipeline schema plan` for the full schema; see `carousel_pipeline/AGENT_GUIDE.md`.

## Download a video

The server downloads a reference video for you from a link, and you get the file. Downloading from your own machine or
a server often gets blocked; the server goes through Apify, which doesn't.

```
./remixctl download https://www.youtube.com/shorts/ID -o reference.mp4
./remixctl downloads get DOWNLOAD_ID -o reference.mp4      # the same file again later
```

- Links from Instagram (reels and posts), X (a post with a video), YouTube (videos and Shorts) and TikTok. For a
  creator's profile, list its videos first with `sources list`.
- It takes 10 to 60 seconds. `download` waits, then prints the download's id, length, size and how it was fetched.
- **Cost:** Apify charges per video: about 2 cents for Instagram, under 1 cent for TikTok, and about 4.5 cents for a
  minute of YouTube at 1080p or 5 cents for a minute of X video. An account without paid runs gets yt-dlp instead
  (free, but often blocked). Asking again for a video you already downloaded hands back the same file, at no charge.
- What you download is kept in your account. Use it in an edit workspace (`--download ID`), or in a remix: `remix --url`
  downloads the same way, and reuses a video you already have.

## Edit videos in a workspace

A workspace is a folder on the server for one edit. Your files go in, you run Python and ffmpeg there yourself, and you
fetch the result. Making the edit is your job: the server only runs your commands.

```
./remixctl workspace new --download DOWNLOAD_ID --file voice.mp3 --file broll.mp4     # prints the workspace id, WS
./remixctl workspace put WS build.py                      # your own script
./remixctl workspace run WS -- python3 build.py
./remixctl workspace run WS -- ffprobe -v error -show_format out/final.mp4
./remixctl workspace get WS out/final.mp4 -o final.mp4
./remixctl workspace rm WS
```

**Rules for running Python and ffmpeg:**

```
./remixctl workspace run WS -- mkdir -p out
./remixctl workspace run --timeout 1800 WS -- ffmpeg -y -v error -i inputs/clip.mp4 -vf scale=1080:-2 out/final.mp4
./remixctl workspace run --no-wait WS -- python3 build.py
```

- **Options go straight after `run`, before WS.** That works with every copy of `remixctl`, old or new. Everything after
  `--` is the program and its arguments, exactly as you write them.
- **Make your folders first.** Only `inputs/` and `fonts/` are there at the start, so ffmpeg fails on `out/final.mp4` until
  you've made `out`. Commands start at the top of the workspace, so use paths from there.
- **Always give ffmpeg `-y`.** Without it, ffmpeg won't replace a file that's already there and still reports success, so
  you'd fetch the old video. Add `-v error` to keep its output short: only the last 20,000 characters are kept.
- **For more than one step, write a Python script** and call ffmpeg from it with `subprocess.run([...], check=True)`.
  Pass a list, not a string: there's no shell.
- **Keep your working files in the workspace.** They stay between commands. `/tmp` is emptied after each one.
- **Set `--timeout` for long renders.** The default is 600 seconds and the most is 3600. A command past its limit is stopped,
  and all you see is exit code 1 with its output cut short, so if that happens, run it again with more time.
- **After `--no-wait`, let it finish.** Until then you can't fetch or add files in that workspace, or run anything else on
  your account. `workspace show WS` gives the last command and whether it's done.
- **Check the exit code.** A non-zero code means the command failed, even if a file appeared.

- **What's inside:** your downloads and files in `inputs/`, and `fonts/DMSans.ttf`. Python has Pillow and numpy.
- **Programs:** `python3`, `ffmpeg`, `ffprobe`, `ls`, `cat`, `head`, `tail`, `wc`, `mkdir`, `cp`, `mv`, `rm`, `find`
  and `du`. One program per run, with no shell, so pipes, `&&` and wildcards don't work. Write a Python script for
  anything longer.
- **Isolated:** each command runs on its own, with no network, seeing only its own workspace. Download what you need
  first, with `download`.
- **Limits:** one command at a time per account, up to 60 minutes each (`--timeout`, default 600 s), 4 CPU cores and
  4 GB of memory per command, 20 GB per workspace. A workspace nobody uses for 7 days is removed.
- **Waiting:** `workspace run` waits and prints the output. If the command failed, it gives the exit code and exits with an
  error. `workspace show WS` lists the files and the last command.

## Generate a video with MiniMax H3

MiniMax H3 is the model every video's shoot uses, and `generate` uses it unless you ask for another. It runs through
MiniMax's API with the server's key and the shoot's settings (768P).

```
./remixctl generate "He leans in and says, in sync with the reference audio: \"here's the thing\"" --ref-image face.png --ref-image desk.png --ref-audio line.mp3 -o take.mp4
./remixctl generate "A slow push-in on the desk, warm lamp light" --first-frame frame.png --duration 6 -o push.mp4
```

- **Inputs:** a local file (uploaded for you), `download:ID`, `upload:ID`, `ws:WS/PATH` (a workspace file) or an https
  URL. There are two kinds of request, and they can't be mixed (MiniMax's rule):
  - References: up to 9 images (`--ref-image`), plus reference videos (`--ref-video`) and audio (`--ref-audio`) of 2
    to 15 s each, at most 15 s of video and 15 s of audio in all. Mention them in the prompt as @Image1, @Video1 and
    @Audio1. A shoot sends the persona's frames, a few seconds of the source clip for motion, and the voice line this way.
  - A first frame (`--first-frame`), optionally with a last frame (`--last-frame`).
- **Voice:** with a voice line as `--ref-audio`, H3 speaks it in that voice, but its pauses can come out a little
  different from the line's: check the timing before you edit.
- **Options:** `--duration` 4 to 15 whole seconds (by default the audio's length plus half a second, rounded up, or
  5 s without audio); `--ratio` 9:16 (the default) or 16:9.
- **Cost:** MiniMax bills $0.08 a second, for the video and for any reference video: $0.56 for a 7 s take.
  `generations show ID` gives the billed seconds and the cost. It needs the paid-runs permission (`runs:real`, granted
  on the Agents page) and paid runs on the account.
- **Waiting:** a video takes a few minutes. `generate` waits, then saves it with `-o`. `--no-wait` returns at once;
  then use `generations show ID` and `generations get ID -o out.mp4` (`--last-frame` for its last frame). Each video is
  kept in your account.

## Make a talking take with MiniMax H3 via Comfy

MiniMax H3 via Comfy runs the same model from its open weights on Comfy Cloud, with the server's key, instead of
through MiniMax's API. It's set up as tested: H3's first-frame model with a realism LoRA. It turns one still and one
voice line into a talking take. The still becomes the first frame, so the take starts in its framing, with its face.
The voice line becomes the soundtrack word for word, so nothing needs re-timing. It's being tested as an alternative
to the API for a video's shoot.

```
./remixctl generate --model h3-comfy --first-frame still.png --ref-audio line.mp3 "PROMPT" -o take.mp4
```

- **Inputs:** exactly one `--first-frame`, the still the take starts from, and one `--ref-audio`, the voice line. No
  reference images or videos: the face, clothes and room all come from the first frame, so use a still whose face is
  right. For a close-up, use a still with the lips closed: parted lips in the first frame stay open through the line's
  opening silence.
- **Length:** the voice line plus half a second. `--duration` (4 to 15 s) overrides it; a line over 15 s has to be
  split.
- **Shape:** 9:16 (768x1344), 16:9 (1344x768) or 1:1 (1024x1024). By default it's the one closest to the still's own,
  and the still is cropped to it from the centre. `--seed` repeats a take.
- **Prompt:** one plain description of the take, then the audio. This shape worked (adapt it to the person):

  ```
  A photoreal vertical 9:16 creator video, one continuous take with no cuts, on a locked-off camera that keeps the
  first frame's angle and distance: a medium shot from just above his hair down to the desk top, filmed from the
  front. The man in the first frame is <age, face, hair, skin and clothes, as the still shows them>. He sits at his
  desk in his studio: <the room>. Natural light on real skin with visible pores; shallow depth of field.

  He talks to the camera. <The performance: expressions and gestures, and the words they land on.> His lips move in
  sync with the soundtrack as he says: "<the line>" Only he is in the frame, with no on-screen text and no effects.

  Audio: his voice exactly as in the soundtrack, quiet room tone. No music, no effects, no other voices.
  ```

  The server adds the realism LoRA's trigger word.
- **Next take:** `generations get ID --last-frame` saves the take's last frame, to start the next take from it when the
  angle doesn't change.
- **Cost:** Comfy Cloud bills the GPU time, $3.49 an hour. In tests a 6.6 s take cost $0.29, 7.3 s $0.33 and 9.4 s
  $0.51; longer takes cost more per second. `generations show ID` gives each take's GPU seconds and cost. It needs the
  paid-runs permission (`runs:real`), like the API.
- **Waiting:** a take needs 5 to 9 minutes of GPU, and Comfy Cloud runs two at a time, so more wait in its queue.
  `generate` waits up to an hour; with `--no-wait`, use `generations show ID` and `generations get ID -o take.mp4`.
- **Check each take:** in the tests every take held, but watch each one before using it: for a cut to a close-up,
  objects that weren't in the still, and lip sync.

## Generate a video with Seedance

Seedance 2.5 (Dreamina Seedance 2.5 on BytePlus ModelArk) makes a video from a prompt, optionally with your own images,
videos or audio as references. The server runs it with its key and hands you the file. Ask for it with
`--model seedance-2.5`.

```
./remixctl generate --model seedance-2.5 "A slow push-in on a rainy neon street at night, a cyclist crosses" --duration 5 --ratio 9:16 -o street.mp4
./remixctl generate --model seedance-2.5 "She turns to the camera and says \"we're live\"" --first-frame frame.png --resolution 720p -o live.mp4
./remixctl generate --model seedance-2.5 "Recut in this rhythm" --ref-video download:DOWNLOAD_ID --ref-image style.png -o recut.mp4
```

- **Inputs:** a local file (uploaded for you), `download:ID`, `upload:ID`, `ws:WS/PATH` (a workspace file) or an https
  URL. There are two kinds of request, and they can't be mixed:
  - A first frame (`--first-frame`), optionally with a last frame (`--last-frame`).
  - References: up to 30 images (`--ref-image`), up to 10 videos of 2 to 30 s each and 30 s in all (`--ref-video`),
    and up to 10 audio clips (`--ref-audio`). Mention them in the prompt as @Image1, @Video1 and so on.
- **Faces:** Seedance 2.5 refuses reference images and videos of real human faces. Videos Seedance itself made on
  this account in the last 30 days are accepted.
- **Options:**
  - `--duration` 4 to 30 s (leave it out and the model decides);
  - `--ratio` 16:9, 4:3, 1:1, 3:4, 9:16, 21:9 or adaptive;
  - `--resolution` 480p, 720p (the default) or 1080p (10-bit H.265, which some players can't open);
  - `--no-audio` for a silent video (by default the model adds sound, and speaks dialogue you put in double quotes);
  - `--seed`, and `--last-frame-out` to also keep the last frame as an image.
- **Cost:** BytePlus bills it in tokens: about $1.16 for 5 s at 720p, or $0.51 at 480p, and less per token with a
  reference video. A failed video costs nothing. It needs the paid-runs permission (`runs:real`, granted on the Agents
  page) and paid runs on the account.
- **Waiting:** a video takes one to a few minutes. `generate` waits, then saves it with `-o`. `--no-wait` returns at
  once; then use `generations show ID` and `generations get ID -o out.mp4`. Each video is kept in your account.

### Seedance through Trioma

Trioma gives you the Seedance family through one key, including models BytePlus hasn't opened to our account. Ask for one
with `--model`. Prices are in Trioma's own credits (TAI), for the default video: 5 seconds at 720p with sound.

| Model | Seedance | Longest | Best resolution | From a prompt | From an image |
|---|---|---|---|---|---|
| `seedance-2.5-trioma` | 2.5 | 30 s | 1080p | 2.29 | 1.37 |
| `seedance-2.0-trioma` | 2.0 | 15 s | 1080p | 1.50 | 0.92 |
| `seedance-2.0-fast-trioma` | 2.0 fast | 15 s | 720p | 0.90 | 0.53 |
| `seedance-2.0-mini-trioma` | 2.0 mini | 15 s | 720p | 0.30 | 0.18 |
| `seedance-1.5-pro-trioma` | 1.5 pro | 12 s | 1080p | 0.14 | 0.14 |

```
./remixctl generate --model seedance-2.0-mini-trioma "A slow push-in on a rainy neon street at night" --duration 5 --ratio 9:16 -o street.mp4
```

- **Inputs:** a prompt is required. You can add a first frame (optionally with a last frame) or reference images. There's
  no reference video or audio: use `seedance-2.5` for those.
- **Options:** `--duration` from 4 seconds up to the model's longest (5 if you leave it out), `--resolution` from 480p up
  to its best (720p if you leave it out), and `--ratio`, `--no-audio` and `--seed` as above. The last frame is always kept.
- **Cost:** it grows with the seconds. `--no-audio` saves 44% (half on 1.5 pro), 480p saves 55%, and 1080p costs about 2.5
  times as much. With too few credits, Trioma refuses the video, says what it would cost, and charges nothing.
  `generations show ID` gives what a video cost, as `cost_tai`. It needs the same paid-runs permission.

## Swap a character into a clip with Openjutsu

Openjutsu is Comfy's character-swap app (comfy.org/hub/apps/openjutsu), run with the server's key. Give it a clip with a
person in it and a picture of a character. The part you choose comes back with the character in place of the person,
and the scene, the performance and the clip's own sound stay. It's Content, Character swap on the site.

```
./remixctl swap https://www.instagram.com/reel/... persona:SLUG --duration 15 --resolution 480p -o swap.mp4
./remixctl generations list --model openjutsu        # the swaps made in your workspace, the site's ones too
```

- **The clip:** an Instagram, TikTok, YouTube or X post's link, which the server downloads first (as `download` does),
  or a local file, `download:ID`, `upload:ID`, `ws:WS/PATH` or an https link to the file itself.
- **The character:** `persona:SLUG` is a character's main picture (`personas list` gives the slugs) and
  `channel:ID/PATH` an animated character's sheet; any other picture works too. The whole character, outfit included,
  works best.
- **Description:** the server has Gemini describe the character from the picture (face, skin, hair, build, clothes) and
  the action from the part's first frame, and puts that before your direction. The swap's author found the app's generic
  instruction alone keeps much of the original person, and so did our first run; with the description, a 15 s test kept
  the character to the last frame. `generations show ID` shows it as `description`.
- **The part:** `--start` is seconds into the clip (0 if you leave it out). `--duration` is 5 to 15 s; leave it out for
  up to 15 s from the start. The clip should be one continuous shot with the person in the part's first frame: a cut
  breaks the swap there. With several people, the largest one is swapped. A silent clip gives a silent result.
- **Direction:** `--direction` is optional, for how the character looks or moves. It goes after the description. The
  part is `--start` and `--duration`, not the direction.
- **First frame:** GPT Image 2.5 first puts the character, outfit included, into the part's first frame, and the swap
  starts from that. `--no-first-frame` skips it. If it fails, the swap runs without it, and `generations show ID` says so.
- **Quality:** `--resolution 768p` (the default) or `480p`. `--seed` repeats a swap.
- **Cost:** Comfy bills each second of the part in its credits, 211 to the dollar. At 768p that's 16 a second (about
  $0.38 for 5 s, $1.14 for 15 s); at 480p it's 8. The first frame adds a few cents. `generations show ID` gives
  `cost_credits` and `cost_usd`. It needs the paid-runs permission (`runs:real`).
- **Waiting:** about 6 minutes for 5 s at 768p and 12 for 15 s at 480p, once Comfy has the app running. When Comfy has to
  start it first, the server waits up to 45 minutes; `generations show ID` says what it's waiting for. `--no-wait`
  gives back the id at once.
- **The same swap, other ways:** `generate --model openjutsu --ref-video CLIP --character PICTURE` takes the same
  options (the direction is its prompt), and the API is `POST /api/v1/generations` with `"model": "openjutsu"`. A copy
  of the tool that refuses `swap` is from before 2026-11-08: fetch it again.

## Rules

- Only remix videos your person has the rights to. `--rights` says you do.
- Personas are fictional adults (21+), never copies of a real person who didn't agree.
- Real runs cost money. A video's shoot waits for a confirmation that shows its price; a test run is free.
- If a step fails, `./remixctl jobs retry JOB` carries on from where it stopped. Finished steps and anything already
  paid for are kept.

## Other commands

`./remixctl jobs list`, `jobs status JOB`, `jobs audit JOB`, `jobs cancel JOB`, `approvals list`, `notifications`.
Everything the tool does is plain HTTPS: see https://hermes-content.tail7d831b.ts.net/docs for the API. The profile is
`GET`/`PUT`/`PATCH /api/v1/personas/SLUG/profile` and `POST /api/v1/personas/SLUG/profile/draft`; `GET /api/v1/profile-options`
lists the industries and the formats each offers, the platforms, and what the prompts can cover. The news is
`GET /api/v1/news?industry=&persona=&subcategory=&window=&limit=`, `GET /api/v1/news/ID` and `GET /api/v1/news/feeds`; a
remix takes `"news": ["ID"]`. Pages are `GET`/`POST /api/v1/pages` and
`GET`/`PUT`/`PATCH`/`DELETE /api/v1/pages/SLUG`; a carousel is `POST /api/v1/carousels`.

**Downloads** are `POST /api/v1/downloads {"url"}`, then `GET /api/v1/downloads/ID` until `status` is `ready` (or
`failed`), then `GET /api/v1/downloads/ID/file`.

**Workspaces:**
- `POST /api/v1/workspaces {"downloads": [...], "uploads": [...]}`, then `GET` and `DELETE /api/v1/workspaces/WS`.
- `PUT` and `GET /api/v1/workspaces/WS/files/PATH` (the request body is the file).
- `POST /api/v1/workspaces/WS/runs {"argv": [...], "timeout_s"}`, then `GET /api/v1/workspaces/WS/runs/RUN` until
  `status` is `done` or `failed`.

**Generations:**
- `POST /api/v1/generations {"prompt", "model", "images": [{"upload"|"download"|"url"|"workspace"+"path"|"persona", "role"}],
  "videos": [...], "audio": [...], "duration", "ratio", "resolution", "generate_audio", "seed", "return_last_frame"}`.
- `model` is `h3` (MiniMax H3 via API, the default), `h3-comfy` (MiniMax H3 via Comfy), `seedance-2.5`, a Seedance
  model through Trioma (`seedance-2.5-trioma` and the others listed above), or `openjutsu`. `h3` takes
  up to 9 reference images with reference video and audio, or a first and a last frame, plus `duration` and `ratio`.
  `h3-comfy` takes one image (the first frame), at most one audio clip (the voice line), and `duration`, `ratio` and
  `seed`. Both keep the last frame. `openjutsu` takes one video (the clip) and one image (the character), with
  `start`, `duration` (5-15), `resolution` (`768p` or `480p`), `first_frame` (true or false) and `seed`; its prompt
  is extra direction, and its video can be a post's link (`{"url": "https://www.instagram.com/reel/..."}`), which the
  server downloads first.
- Then `GET /api/v1/generations/ID` until `status` is `ready` (or `failed`).
- Then `GET /api/v1/generations/ID/file` (`?part=last_frame` for the last frame).
- `GET /api/v1/generations?model=openjutsu&limit=20` lists the latest videos made in your workspace, newest first,
  people's on the site too (any model when `model` is left out).


## Mikkel: script-first human creative review

Andrew requires independent creator commentary, not vendor promotion. Read `docs/broll/EDITORIAL_RULES.md` and
`docs/broll/PRE_VIDEO_WORKFLOW.md` in the harness repository before developing his next persona video. Start with
complete scripts for genuinely different audiences and editorial conflicts, then use independent story, audience and
evidence review. Scores rank drafts; they are not predicted virality. Save feedback and preserve rejected directions.
Current review: `docs/broll/preproduction/mikkel-duet-v2/SHORTLIST.md`; all draft ratings and alternatives are beside it.

`jobs:approve` is technical permission, not approval to override human review. A script choice, praise for preview
frames, an earlier shoot approval or silence does not authorize video generation. After script selection, show the
exact voice at 1.0x, presenter angles/expressions as keyframes, and B-roll alternatives with reasoning. Wait for explicit
human authorization of the selected assets and paid generation scope before any video task, including a motion test.
Do not self-approve. This is an agent workflow; no new server-enforced gate has been implemented.

The nine existing frame concepts were liked but are not selected. Script rounds are still under review. No video is
authorized for this new iteration. Preserve Mikkel's identity while changing performance; no second news presenter.


## Copycat

The web UI is at https://hermes-content.tail7d831b.ts.net/copycat. Use the existing bearer token with the REST API
(`/openapi.json` documents the request shapes):

1. `POST /api/v1/copycat/monitors` with `name`, `channels` (profile URLs) and `config`
   (`posts`, `period`: day/week, `timezone`, `persona`: auto or an available slug, `auto_prepare`).
   It starts paused. Check source authorization before setting `rights_attested`.
2. `PATCH /api/v1/copycat/monitors/{id}` with `enabled:true`, or POST its `/scan` endpoint
   for one scan. These use paid Apify/analysis calls and require `runs:real`.
3. Read `/recommendations` and `/videos` under the monitor. Coverage counts independent creators
   discussing the same specific event within 48 hours, not repeated posts from one account.
4. POST `/monitors/{id}/briefs` with `topic_key` and optional `persona`. Read `/briefs/{id}`.
   A ready brief has three script options, the selected script, scene directions and storyboard images.
5. PATCH the brief with its current `revision` to save `script`, `persona`, `notes` or `scenes`.
   POST `/briefs/{id}/revise` with `kind:script|stills|scene|retry|dismiss` and revision notes.
   Script changes need a refreshed storyboard. Images: GET `/briefs/{id}/images/{scene.image}`.
6. Only after the person approves the brief, POST `/briefs/{id}/prepare-video` with the current
   `revision` and `real_run:true`. This creates the normal video job; its voice/frame/shoot/final
   approvals still apply. Copycat never auto-publishes.

All abbreviated API paths above start with `/api/v1/copycat`. Automatic preparation uses the
monitor's daily/weekly brief allowance; it does not grant permission to shoot videos. Provider
usage is paid, monitoring is periodic, and download/analysis limits can leave sources unprocessed.

<!-- SET BUILDER AGENT GUIDE -->
## Studio backgrounds and Set Builder for agents

Use `remixctl set-builder` (alias `remixctl studio`) to work with the same five photographic starters, room geometry, validated editing controls, saved versions and Nano Banana generator as the website. Update the CLI from `/cli/remixctl.py` if the command is missing; your existing login is retained. All commands support global `--json`.

```sh
remixctl set-builder starters
remixctl set-builder starter-image daylight-study -o daylight-study.png
remixctl set-builder new --starter daylight-study --name "My studio" --persona Mikkel -o room.json
remixctl set-builder actions
remixctl set-builder edit room.json --set 'room.wall="#e8dfcf"' --set 'objects.plant.enabled=false' --set 'spots.standing.camera.lens=45' -o room.json
remixctl set-builder preview room.json --directory room-preview
remixctl set-builder save room.json -o saved-room.json
remixctl set-builder prepare saved-room.json --size 1K -o pack.json
```

`new --template warm-studio|signal-room|soft-loft` also supports the original layout templates. New drafts are local JSON until saved. `save` creates a locked version visible in the website's Saved room versions. `list`/`show ID -o room.json` retrieve account-visible rooms. Use `edit saved-room.json --duplicate ... -o next-room.json` before changing a locked room. Do not overwrite another client's file.

Use fields from `actions`, not invented furniture types. `edit` uses FIELD=JSON_VALUE (strings need JSON quotes), including named furniture and filming spots. `apply room.json --plan plan.json -o revised-room.json` accepts the same command plans as the web editor. It validates against the current scene and rejects plans containing external effects; saving and generation remain explicit steps. An optional paid text planner is available with `propose room.json --prompt "..." --real --request-id STABLE_ID -o proposal.json`. Review the proposal before `apply`. Agents can write command plans themselves without a model call.

`preview` creates desk, lounge, standing and overview PNG layout guides on the server using the website's existing geometry/software renderer. `prepare` renders fresh guides from the supplied scene and creates an immutable, inspectable photo instruction pack. Both use **zero paid model calls**. `pack ID` retrieves prompts, current camera settings and photo references. An original starter photo supplies appearance detail; edited layout, enabled furniture and settings take priority. The 3D guide is an approximate layout, not the intended final photo.

Only after the user approves the actual brief/visuals, submit paid generation:

```sh
# Use the id from pack.json. Persist a unique request ID BEFORE submitting.
remixctl set-builder generate --pack PACK_ID --spots standing --real --request-id studio-approved-20261030-001
remixctl set-builder status RUN_ID --wait
remixctl set-builder download RUN_ID --directory generated-studio
```

`--spots all` requests three images; otherwise choose desk, lounge, standing, or a subset. Reuse the **same request ID and arguments** after an ambiguous network error; a new ID could incur another charge. `runs` lists recent jobs. Download saves available outputs only; check the returned run status for partial or failed jobs. No CLI command initiates video generation or publication. An agent flag does not replace the person's approval.

HTTP API: `/api/v1/set-builder`. Read access uses `jobs:read`; creating/editing/preparing/saving uses `jobs:write`; image generation and the optional text planner additionally need `runs:real` and account paid-run access. Same bearer token as the normal CLI. Existing team/private ownership checks apply. Never send provider keys in room JSON or prompts.
