<!-- AvatarFactory Docs · https://avatarfactory.in/docs/guides/production-voice-ux · Full map: https://avatarfactory.in/llms.txt -->

# Production & Voice UX

Ship a voice avatar that feels good, not just one that works. Symptom-indexed fixes, a latency budget, interruption handling, and reconnect patterns.

Getting an avatar on screen is the quickstart. Getting a voice experience that
feels responsive and reliable is this page. Read the first section by symptom
when something is wrong; read the rest before you launch.

## Read this if you're seeing…

### The avatar is invisible / blank

The Avatar fills its container, so a parent with no size renders nothing.

- Give the parent an explicit width **and** height (not `flex: 1` alone — a flex
  child can collapse to zero height).
- Confirm you imported the stylesheet once at your entry point:
  `import "@avatarfactory/react/styles.css"`.
- Make sure `` is inside ``.

See [Styling](/docs/sdk/react/styling) for the full container recipe.

### No audio / the avatar never speaks

- In `call` mode the browser must grant **microphone** permission when the
  session starts. If the prompt was dismissed, nothing is captured.
- On Safari and iOS, audio playback must be unlocked by a user gesture — start
  the session from a real click/tap, not on page load. See the
  [FAQ](/docs/guides/faq) autoplay note.
- Check that a session token is actually being returned by your
  `getSessionToken` function (a rejected token fails the connection silently to
  the user).

### Echo — the avatar hears itself

Echo happens when the avatar's output audio is picked up by the microphone.

- **Web:** rely on the browser's built-in echo cancellation; test with the
  user on headphones vs speakers, which is the common failure case.
- **React Native:** the SDK ships mobile echo cancellation — make sure you're on
  a real dev build, not Expo Go, and that the config plugin is applied. See the
  React Native [Overview](/docs/sdk/react-native/overview).

### Calls end after the same amount of time every session

That's the idle timeout, not a bug. A `call` session ends after roughly
**2 minutes** of no interaction. If you need the session to persist, keep it
active or reconnect on demand (below). See [Limits & Quotas](/docs/guides/limits).

### The avatar disconnected mid-conversation

Network drops happen. Listen for the error/disconnect events via
[`useAvatarEvent`](/docs/sdk/react/use-avatar-event) and reconnect — see
[Reconnect patterns](#reconnect-patterns).

## Set a latency budget

Perceived responsiveness is the whole product. Decide your target up front —
for a natural back-and-forth, aim for a sub-second gap between the user
finishing speaking and the avatar starting to respond — then treat every
feature as a tradeoff against that budget.

Measure each stage of the pipeline separately so you know where the time goes:

- **Token fetch** — your `getSessionToken` round-trip. Cache/keep-alive your
  backend so this isn't a cold start every call; measure your own p50/p95.
- **Connection setup** — establishing the live stream. **~5–7s on the first
  connect** (the Rive avatar downloads and initializes), then **~2–3s** on later
  connects, because the avatar loads from cache rather than being re-downloaded.
- **Response latency** — in `call` mode, the gap from the user finishing
  speaking to the avatar starting to respond is **~700–900ms** (measured).
- **First frame** — time from response audio to the avatar animating.

> Optimize the stage that's actually largest in **your** measurements. Don't
>   guess — a slow token endpoint is the most common self-inflicted latency and
>   the easiest to fix.

## Perceived latency beats raw latency

You can feel faster without being faster:

- **Show state immediately.** Reflect `isListening` / `isSpeaking` from
  [`useAvatar`](/docs/sdk/react/use-avatar) so the user sees the avatar react
  the instant they speak, even before audio returns.
- **Use a "connecting" affordance.** A ringing/connecting UI during setup makes
  the unavoidable connection time feel intentional rather than broken.
- **Don't block on transcripts.** If you display a live transcript, render it
  progressively; waiting for a final transcript before showing anything adds
  perceived lag.

## Interruption UX

Real conversations interrupt. When the user starts speaking while the avatar is
talking, stop the avatar promptly — read `isSpeaking` and call `stop()` (or your
mode's interrupt path) so the user isn't talking over a monologue. Test the
"barge-in" case explicitly; it's the difference between a demo and a product.

## When a session ends

Treat disconnects as expected, not exceptional — and in most apps, there is
nothing to build.

A fatal error ends the **session**, not the avatar. `stop()` keeps the avatar
painted so restarting is cheap, and `isIdle` flips back to `true`, so your
existing start button reappears on its own and *is* the retry:

```tsx
function CallStage() {
  // No error branch. Idle timeout, dropped connection, rate limit — the avatar
  // stays on screen and the start button comes back. Let the user press it.
  return <Avatar errorFallback={<CouldNotLoad />} />;
}
```

- Say **why** the call ended (a toast off `error.code`), so the user knows the
  button is worth pressing. See [Error Handling](/docs/sdk/react/error-handling).
- `errorFallback` covers the one case with nothing to show: the avatar never
  loaded. Put a retry inside it if your own controls aren't visible there.
- Reconnecting automatically is rarely worth it, and `fatal && retryable` is the
  wrong test for it: `SESSION_IDLE_TIMEOUT` would rebuild a session that idles
  out again minutes later, and `SERVER_CLOSE_REQUESTED` would walk straight back
  into the rate limit that closed you.
- For a fatal error that is **not** retryable (`MIC_PERMISSION_DENIED`), tell the
  user what to change — the start button can't fix it.

## Pre-launch checklist

- Microphone permission flow tested on Chrome, Safari, and mobile.
- Autoplay unlocked from a user gesture (Safari/iOS).
- Latency budget set and each stage measured in production.
- Interruption / barge-in handled.
- Session-ended UX decided: the avatar stays painted and the start button is the
  retry, with a toast saying why the call ended.
- Idle-timeout behavior is intentional (persist or reconnect as needed).
