# Talking actor ads (/guides/talking-actor)



A talking actor ad is two generations, in order:

1. `tts` turns your script into audio.
2. `actor_ultra` animates the actor to that audio. It needs the finished voice's `generation_id`.

Have your own recording? Skip the voice. [Jump down](#use-your-own-audio).

## Pick an actor and a voice [#pick-an-actor-and-a-voice]

List them with `GET /actors` and `GET /voices` (MCP: `list_actors`, `list_voices`). Actor ids start `act_`, voice ids `voc_`. If the actor has a default voice, you can leave out `voice_id`.

## Make the voice, then the video [#make-the-voice-then-the-video]

<Tabs items="['REST','MCP','CLI']">
  <Tab value="REST">
    ```bash title="Terminal"
    BASE=https://app.riffads.com/api/v1
    AUTH="Authorization: Bearer $RIFFADS_API_KEY"
    SCRIPT="Two weeks of battery, in a case this small."

    # 1. The voice
    VOICE=$(curl -s -X POST "$BASE/generations" -H "$AUTH" -H "Content-Type: application/json" -d "{
      \"capability_id\": \"tts\",
      \"config\": { \"script\": \"$SCRIPT\" },
      \"actor_id\": \"act_0193c8f0a1b24e7f9d3c5a6b7e8f0011\",
      \"voice_id\": \"voc_0193c8f0a1b24e7f9d3c5a6b7e8f0022\",
      \"max_credits\": 10
    }" | jq -r .generation_id)

    # 2. Call until still_running is false and status is completed
    curl -s "$BASE/generations/$VOICE/wait" -H "$AUTH"

    # 3. The video, driven by that voice
    curl -s -X POST "$BASE/generations" -H "$AUTH" -H "Content-Type: application/json" -d "{
      \"capability_id\": \"actor_ultra\",
      \"config\": { \"script\": \"$SCRIPT\" },
      \"actor_id\": \"act_0193c8f0a1b24e7f9d3c5a6b7e8f0011\",
      \"voice_id\": \"voc_0193c8f0a1b24e7f9d3c5a6b7e8f0022\",
      \"approved_voice_generation_id\": \"$VOICE\",
      \"max_credits\": 700
    }"
    ```
  </Tab>

  <Tab value="MCP">
    ```text
    submit_generation      { capability_id: "tts", config: { script: "..." },
                             actor_id: "act_...", voice_id: "voc_...", max_credits: 10 }
    wait_for_generation    { generation_id: "gen_..." }   // until still_running is false

    generate_talking_actor { script: "...", actor_id: "act_...", voice_id: "voc_...",
                             approved_voice_generation_id: "gen_...", max_credits: 700 }
    wait_for_generation    { generation_id: "gen_..." }
    ```

    Other `actor_ultra` settings go under `settings`.
  </Tab>

  <Tab value="CLI">
    <SurfaceStatus id="cli" />

    ```bash title="Terminal"
    SCRIPT="Two weeks of battery, in a case this small."

    VOICE=$(riffads generate -c tts --set-string script="$SCRIPT" \
      --actor act_0193c8f0a1b24e7f9d3c5a6b7e8f0011 \
      --voice voc_0193c8f0a1b24e7f9d3c5a6b7e8f0022 -m 10 --no-wait)
    riffads status "$VOICE" --wait

    riffads generate -c actor_ultra --set-string script="$SCRIPT" \
      --actor act_0193c8f0a1b24e7f9d3c5a6b7e8f0011 \
      --voice voc_0193c8f0a1b24e7f9d3c5a6b7e8f0022 \
      --approved-voice-generation "$VOICE" -m 700 --output ./out
    ```
  </Tab>
</Tabs>

`max_credits` values above are placeholders. Get the real number from the estimate endpoint (`estimate_generation`) for each step, then send at least `ceil(estimate * 1.1)`.

<Callout type="warn" title="Both calls must match">
  Same script, same `actor_id`, same `voice_id`. Build the script once and reuse the variable. Any change: `invalid_config`, `The approved voice no longer matches this generation.`
</Callout>

## What actor\_ultra takes [#what-actor_ultra-takes]

| Field                                | Where  | Notes                                                                 |
| ------------------------------------ | ------ | --------------------------------------------------------------------- |
| `actor_id` or `actor_image_asset_id` | body   | One is required. An uploaded image works as the face                  |
| `voice_id`                           | body   | Optional if the actor has a default voice. Config spells it `voiceId` |
| `approved_voice_generation_id`       | body   | The completed `tts` generation                                        |
| `script`                             | config | Same words as the voice                                               |
| `voice_audio`                        | config | Your own recording instead of a voice                                 |

* **No `aspect_ratio`.** Refused. The video takes the actor image's shape.
* **No duration.** Audio sets the length. It must be under 60 seconds.
* **No `variants`.** Want 3 takes? Run the pair 3 times.
* The id is `actor_ultra`. `talking_actor` is refused.

## Use your own audio [#use-your-own-audio]

One generation. No `tts`, no script, no voice. [Upload](/api/uploads) the file first and wait for `usable: true`.

```json title="Request body"
{
  "capability_id": "actor_ultra",
  "config": { "voice_audio": ["ast_0193c8f0a1b24e7f9d3c5a6b7e8f0044"] },
  "actor_id": "act_0193c8f0a1b24e7f9d3c5a6b7e8f0011",
  "max_credits": 700
}
```

Send it with `submit_generation` on MCP, not `generate_talking_actor`. If both a recording and a script arrive, the recording wins.

## Common refusals [#common-refusals]

All are `invalid_config`, and nothing is charged.

| Message                                                     | Fix                                   |
| ----------------------------------------------------------- | ------------------------------------- |
| `Generate and approve the voice before creating the video.` | Make the `tts` step first             |
| `The approved voice no longer matches this generation.`     | Send the same script, actor and voice |
| `Select a voice first.`                                     | Send `voice_id`                       |

Video sent while the voice still runs: `submission_in_flight`. Wait, then resend.

Getting the file: [results](/guides/results).
