> ## Documentation Index
> Fetch the complete documentation index at: https://docs.percify.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Make a talking avatar video with the Percify API

> Build a talking avatar video through the Percify API in three calls: gpt-image-2 for the face, zonos2 for the cloned voice, infinitetalk for lip-sync.

The Percify API makes a talking avatar video in three generations: an image model creates the face, a voice model speaks your script in a cloned voice, and a lip-sync model animates the face to that audio. Each step is a `POST /v1/run`, and each output URL is the input of the next. You can skip the first two steps if you already have a portrait photo or a finished voiceover.

| Step     | Model id                              | Input                                             | Output    |
| -------- | ------------------------------------- | ------------------------------------------------- | --------- |
| 1. Face  | `gpt-image-2`                         | `prompt`                                          | Image URL |
| 2. Voice | `zonos2`                              | `text` + `audio` (a sample of the voice to clone) | Audio URL |
| 3. Video | `infinitetalk-fast` or `infinitetalk` | `image` + `audio`                                 | Video URL |

## What you need

* A Percify API key (Scale or Ultra plan). See [Authentication](/percify/api-auth).
* For step 2, a short, clean speech recording of the voice to clone, at a public `https` URL, as `.mp3` or `.wav`. Only clone a voice you have permission to use.
* Or skip steps 1 and 2 with your own portrait (`.jpg` or `.png` with a clearly visible face) and your own voiceover (`.mp3` or `.wav`), both at public `https` URLs.

## Run the pipeline

<CodeGroup>
  ```javascript Node.js theme={"system"}
  const API = "https://api.percify.io/v3/playground/v1";
  const headers = {
    Authorization: `Bearer ${process.env.PERCIFY_API_TOKEN}`,
    "Content-Type": "application/json",
  };

  async function generate(modelId, input, key) {
    let res = await fetch(`${API}/run`, {
      method: "POST",
      headers: { ...headers, "Idempotency-Key": key },
      body: JSON.stringify({ modelId, input }),
    });
    const body = await res.json();
    if (!res.ok) throw new Error(typeof body === "string" ? body : body.message);
    let data = body.data;
    while (data.status !== "succeeded" && data.status !== "failed") {
      res = await fetch(`${API}/generations/${data.id}?wait=45`, { headers });
      ({ data } = await res.json());
    }
    if (data.status === "failed") throw new Error(data.error);
    return data.output.urls[0];
  }

  // 1. Face
  const image = await generate("gpt-image-2", {
    prompt: "Head-and-shoulders photo of a friendly presenter facing the camera, soft studio light, plain background",
    aspect_ratio: "2:3",
  }, "episode-12-face");

  // 2. Voice
  const audio = await generate("zonos2", {
    text: "Welcome back. Today I will show you how we cut our onboarding time in half.",
    audio: "https://example.com/my-voice-sample.wav",
  }, "episode-12-voice");

  // 3. Video
  const video = await generate("infinitetalk-fast", { image, audio }, "episode-12-video");
  console.log(video);
  ```

  ```python Python theme={"system"}
  import os, requests

  API = "https://api.percify.io/v3/playground/v1"
  S = requests.Session()
  S.headers["Authorization"] = f"Bearer {os.environ['PERCIFY_API_TOKEN']}"

  def generate(model_id, input, key):
      r = S.post(f"{API}/run", json={"modelId": model_id, "input": input},
                 headers={"Idempotency-Key": key}, timeout=70)
      r.raise_for_status()
      data = r.json()["data"]
      while data["status"] not in ("succeeded", "failed"):
          data = S.get(f"{API}/generations/{data['id']}", params={"wait": 45}, timeout=70).json()["data"]
      if data["status"] == "failed":
          raise RuntimeError(data["error"])
      return data["output"]["urls"][0]

  image = generate("gpt-image-2", {
      "prompt": "Head-and-shoulders photo of a friendly presenter facing the camera, soft studio light, plain background",
      "aspect_ratio": "2:3",
  }, "episode-12-face")

  audio = generate("zonos2", {
      "text": "Welcome back. Today I will show you how we cut our onboarding time in half.",
      "audio": "https://example.com/my-voice-sample.wav",
  }, "episode-12-voice")

  video = generate("infinitetalk-fast", {"image": image, "audio": audio}, "episode-12-video")
  print(video)
  ```
</CodeGroup>

The idempotency keys make the script safe to rerun: a step that already ran returns its existing generation instead of charging again.

## How much does a talking avatar video cost?

Each step is charged separately, from your plan's credits:

| Step                                | How it is billed                                                                  |
| ----------------------------------- | --------------------------------------------------------------------------------- |
| `gpt-image-2`                       | Per image. The `quality` input changes the price.                                 |
| `zonos2`                            | By the length of the voice sample you pass in `audio`.                            |
| `infinitetalk-fast`, `infinitetalk` | Per second of the audio. On `infinitetalk`, `720p` costs twice as much as `480p`. |

Get exact numbers before you run: call [`POST /v1/estimate`](/api-reference/generations/estimate) for each step with the inputs you plan to send. For step 3, estimate with a real audio URL, because Percify prices it from the audio's length.

## Tips for better results

* **Frame the portrait the way you want the video.** The video keeps the input image's aspect ratio, so crop it first.
* **One face, facing the camera.** A clear, front-facing face gives the lip-sync model the most to work with.
* **Reuse the voice.** There is no saved voice id in the REST API: send the same sample URL to `zonos2` each time to keep a consistent voice.
* **Start with `infinitetalk-fast`** while you test scripts, then switch to `infinitetalk` at `720p` for final renders.
* **Long scripts take longer.** Use `?wait=45` in a loop or a [webhook](/guides/webhooks) instead of a tight poll.

<Tip>
  AI agents can do all three steps for you. Through the [MCP server](/mcp-server), `create_avatar` saves a face and a voice once, and `avatar_say` makes that avatar speak any script.
</Tip>

## Related

<CardGroup cols={2}>
  <Card title="Lip-sync video (InfiniteTalk)" href="/api-reference/avatars/generate">
    Every input of step 3.
  </Card>

  <Card title="Voice cloning and speech" href="/api-reference/audio/overview">
    zonos2 and the text to speech models.
  </Card>

  <Card title="Estimate cost" href="/api-reference/generations/estimate">
    Price each step before running it.
  </Card>

  <Card title="Lip-sync in the app" href="/create/lip-sync">
    The same models without code.
  </Card>
</CardGroup>
