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

> ## Agent Instructions
> To build and serve your own model, start at /deploy/development/quickstart and /deploy/development/overview. Deploying is the default path: reactor init scaffolds a workspace, reactor auth login authenticates, and reactor model deploy registers the model, publishes the release with the weights/ folder, and activates it on Reactor's GPUs, in one command from that workspace. Docker must be running, because the publish step builds the image locally. Bump model.version in reactor.yaml before redeploying a change, because a release that already has an image is reactivated as it is. Deployment access is granted per account, so contact team@reactor.inc if a deploy is refused. Every key in reactor.yaml is documented at /deploy/platform/reactor-yaml. Model code imports reactor_runtime; Python client code imports reactor_sdk. The runtime overview explains the model interface. Running the model on your own machine with reactor run is optional and needs a GPU you attach with --gpus; /deploy/development/local-testing covers that loop and pairs a complete brightness model with a Python client test in a separate brightness-test workspace.
> Reactor hosts multiple models, each with its own connect slug (modelName) and command/event schema. The catalog of every model — slug, typed SDK package, and links to its schema — is at /model-api-reference/overview. Some models expose one slug per experience (e.g. HappyOyster); always take the slug from the model's own pages, never guess it.
> Fastest path to a working app: `npx create-reactor-app my-app --model=<slug>` scaffolds a complete app with secure auth wired up. Typed TypeScript SDKs are published as @reactor-models/<model>; Python uses the base reactor-sdk package.
> Auth: exchange an API key (rk_...) for a JWT via POST https://api.reactor.inc/tokens from your server. Never put the API key in client-side code.
> Append .md to any docs URL for clean Markdown. Search these docs via the MCP server at https://docs.reactor.inc/mcp.

# Vidu S2-Avatar overview

> Create an avatar from one photo and talk to it by voice or text.

export const ModelRate = ({model, fallback = "View pricing"}) => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(false);
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    const ctrl = new AbortController();
    fetch("https://api.reactor.inc/pricing", {
      signal: ctrl.signal
    }).then(r => {
      if (!r.ok) throw new Error(`HTTP ${r.status}`);
      return r.json();
    }).then(json => {
      setData(json);
      setLoading(false);
    }).catch(err => {
      if (err.name === "AbortError") return;
      setError(true);
      setLoading(false);
    });
    return () => ctrl.abort();
  }, []);
  if (loading) {
    return <span aria-label="loading pricing" className="inline-block h-4 w-28 rounded bg-zinc-950/10 dark:bg-white/10 animate-pulse align-middle" />;
  }
  const creditsPerDollar = data?.settings?.credits_per_dollar;
  const rate = data?.models?.find(m => m.name === model)?.rate;
  const amountPerMin = typeof rate?.amount_per_min === "number" ? rate.amount_per_min : typeof rate?.amount_per_sec === "number" ? rate.amount_per_sec * 60 : undefined;
  const canRender = !error && creditsPerDollar && typeof amountPerMin === "number";
  if (!canRender) {
    return fallback === "View pricing" ? <a href="/resources/billing">View pricing</a> : <span className="text-zinc-950/60 dark:text-white/60">{fallback}</span>;
  }
  const perMin = (amountPerMin / creditsPerDollar).toFixed(3);
  return <span>
      <strong>${perMin}/min</strong>
    </span>;
};

**Vidu S2-Avatar** turns a photo of one person into a character you can talk to. You can define its
role and behavior and optionally choose a voice. It responds with synchronized video and speech. It
accepts voice or text input and can use the caller's camera during a video call.

Avatars are saved for up to 90 days and can be reused across sessions. Each session holds one avatar
and one active call at a time.

<Note>
  Billing starts when the session reaches `ready`, including time between calls. Disconnect when the
  caller is done. See [Billing](/resources/billing) for how session time maps to rates.
</Note>

## At a glance

| Spec | Value |
| - | - |
| **Model name** | Vidu S2-Avatar |
| **Pricing** | <ModelRate model="vidu-s2-avatar" fallback="Pricing pending" /> |
| **Input** | One photo, a character description, and voice or text; optional camera video |
| **Output** | Synchronized character video and speech |
| **Frame rate** | 25 fps |
| **Resolution** | 864 × 1152 |
| **Audio** | 48 kHz mono, in sync with the video |

## What you can do

<CardGroup cols={3}>
  <Card title="Create and reuse an avatar" icon="user-round">
    Create an avatar from one image. Save its `avatar_id` and attach it again in later sessions.
  </Card>

  <Card title="Talk or type" icon="messages-square">
    Publish a microphone or send text with `say`. Read `transcript` messages to show both sides of
    the conversation.
  </Card>

  <Card title="Steer a live call" icon="shirt">
    Use `interrupt` to stop speech. Use `update_call` to change the persona or voice. Send reference
    images to change what the character holds, wears, or stands in front of.
  </Card>
</CardGroup>

## Quick start

These examples use the base SDK to create an avatar and start a call. The
[tutorial](/model-api-reference/vidu-s2-avatar/tutorial) shows how to publish a microphone and build
the rest of the call UI.

<CodeGroup>
  ```typescript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
  import { Reactor } from "@reactor-team/js-sdk";

  async function startAvatarCall(jwtToken, file, video, audio) {
    const reactor = new Reactor({
      modelName: "reactor/vidu-s2-avatar",
      modelTracks: [
        { name: "mic", kind: "audio", direction: "sendonly" },
        { name: "webcam", kind: "video", direction: "sendonly" },
        { name: "main_video", kind: "video", direction: "recvonly" },
        { name: "main_audio", kind: "audio", direction: "recvonly" },
      ],
    });

    reactor.on("trackReceived", (name, track) => {
      if (name === "main_video") video.srcObject = new MediaStream([track]);
      if (name === "main_audio") audio.srcObject = new MediaStream([track]);
    });

    function waitForPhase(phase) {
      return new Promise((resolve) => {
        const onMessage = (msg) => {
          if (msg.type !== "session_state" || msg.data.phase !== phase) return;
          reactor.off("message", onMessage);
          resolve(msg.data);
        };
        reactor.on("message", onMessage);
      });
    }

    await reactor.connect(jwtToken);
    await reactor.resumeTrack("main_video");
    await reactor.resumeTrack("main_audio");

    const avatarReady = waitForPhase("avatar_ready");
    const image = await reactor.uploadFile(file);
    await reactor.sendCommand("create_avatar", { image, name: "Tina" });
    await avatarReady;

    const live = waitForPhase("live");
    await reactor.sendCommand("start_call", {
      persona: "You are Tina, a museum guide. Answer in two short sentences.",
      greeting: "Say hello and wave.",
    });
    await live;
    await reactor.resumeTrack("main_video");
    await reactor.resumeTrack("main_audio");

    return reactor;
  }
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
  import asyncio
  from reactor_sdk import Reactor

  async with Reactor(model_name="reactor/vidu-s2-avatar", api_key="rk_...") as reactor:
      phases = {"avatar_ready": asyncio.Event(), "live": asyncio.Event()}

      @reactor.on_message
      def on_message(msg):
          if msg["type"] == "session_state" and msg["data"]["phase"] in phases:
              phases[msg["data"]["phase"]].set()

      await reactor.connect()
      await reactor.send_command("create_avatar", {"image_url": "https://YOUR_PUBLIC_HOST/tina.jpg"})
      await phases["avatar_ready"].wait()

      await reactor.send_command("start_call", {
          "persona": "You are Tina, a museum guide. Answer in two short sentences.",
      })
      await phases["live"].wait()
      await reactor.send_command("say", {"text": "What is in this room?"})
  ```
</CodeGroup>

<Note>
  In a browser, start `startAvatarCall` from a click or tap so the browser can play audio.
</Note>

## How a call works

1. **Connect.** The first `session_state` has phase `idle`. All four tracks exist from connect, so
   the app can prepare its player and microphone before a call starts.
2. **Create or reuse an avatar.** Send `create_avatar` with an upload or a public image URL.
   The phase changes to `preparing_avatar`, then `avatar_ready`. Save the `avatar_id`. In a later session, send
   `attach_avatar` with that id instead.
3. **Start a call.** Send `start_call` with a persona. You can choose a voice from `list_voices` or
   use the default. The phase changes through `starting` and `warming_up` to `live`.
4. **Talk and steer.** Publish `mic` or send `say`. Use `interrupt` and `update_call` to steer a
   live conversation. Use `set_reference_images` to change what the avatar wears, holds, or stands
   in front of.
5. **End or start again.** Send `end_call`, or let the call end at its time limit or idle timeout.
   The avatar stays available, so the app can start another call in the same session.

Use `session_state` to drive the UI. The snapshot reports the phase, current avatar, call timers, and
media state. A refused command or failure arrives as `command_error` with a stable `code`.

See the [schema](/model-api-reference/vidu-s2-avatar/schema) for commands, messages, and tracks. The
[prompt guide](/model-api-reference/vidu-s2-avatar/prompt-guide) covers personas, greetings, and
turn-taking.
