AI Twin Player Integration
Integrate @streamoji/aitwin (^0.2.7) — render an AI twin with TTS lipsync in your React app.
Prerequisites
You need a React 18 or 19 project with Node.js and npm (or pnpm/yarn). The package renders a canvas-based AI twin face with TTS lipsync — it runs in the browser only.
Peer dependencies: react and react-dom. If you use Next.js or enforce a strict Content Security Policy, see Integration notes below.
Install the package
Add @streamoji/aitwin and its peer dependencies to your project:
npm install @streamoji/aitwinRender your twin
Provide id (cloud twin slug, e.g. olivia). Use stable useCallback handlers for onReady and onError:
function TwinDemo() {
const twinRef = useRef<AiTwinHandle>(null);
return (
<AiTwin
ref={twinRef}
id="olivia"
onReady={() => console.log("face ready")}
onStatusChange={(status) => console.log("status", status)}
onError={(message) => console.error(message)}
/>
);
}Speak on demand
Control speech imperatively via the ref handle. Call speakText with any string; use stop() to interrupt playback:
<button
type="button"
onClick={() => void twinRef.current?.speakText("Hi, how are you?")}
>
Speak
</button>
<button
type="button"
onClick={() => twinRef.current?.stop()}
>
Stop
</button>Realtime voice (connect)
For twins with a knowledge base, open a mic + WebSocket session to wss://<api-host>/ws/voice. The avatar lip-syncs from server avatar_audio_chunk messages. Mount <AiTwin>, wait for onReady (or twinRef.current?.isReady()), then call connect() from a user gesture (button click) — it unlocks AudioContext and requests mic access.
Use the same authToken JWT as speakText (client_… Bearer token). In production, generate tokens from your backend via getAiTwinAuthToken — do not rely on fetchDevAuthToken outside dev.
When using <AiTwin id="…" />, the SDK calls fetchAiTwin internally and stores voiceId and personaId (from knowledgeContextId). You can omit them in connect() and the SDK will use those values. If you use avatarId or assets without getAiTwin, pass voiceId and personaId yourself in connect() when needed.
WebSocket URL (built automatically when wsUrl is omitted): wss://ai.aitwin.me/ws/voice?authToken=…&tenant=aiTwin&voiceId=…&personaId=…&speaking_rate=0.85&tts_stream=true. Query params: authToken (required), tenant (always aiTwin), voiceId (optional Cartesia UUID), personaId (optional, from getAiTwin knowledgeContextId), speaking_rate (0.5–1.5, default 0.85), tts_stream (default true).
import { useRef } from "react";
import {
AiTwin,
fetchAiTwin,
type AiTwinHandle,
} from "@streamoji/aitwin";
function VoiceTwin() {
const twinRef = useRef<AiTwinHandle>(null);
const handleConnect = async () => {
const authToken = "client_…"; // from your backend
const twin = await fetchAiTwin("modi");
await twinRef.current?.connect({
authToken,
voiceId: twin.voiceId, // optional
personaId: twin.knowledgeContextId, // optional, when present
speakingRate: 0.85, // optional, default 0.85
});
};
const handleDisconnect = async () => {
await twinRef.current?.disconnect();
};
return (
<>
<AiTwin
ref={twinRef}
id="modi"
authToken="client_…"
onUserTranscript={(text, final) => {
if (final) console.log("user:", text);
}}
onBotOutput={(text) => console.log("bot:", text)}
onStatusChange={(status) => console.log("status:", status)}
onError={(msg) => console.error(msg)}
/>
<button type="button" onClick={() => void handleConnect()}>
Connect mic
</button>
<button type="button" onClick={() => void handleDisconnect()}>
Disconnect
</button>
</>
);
}Production auth (Proxy)
For production TTS and encrypted twin assets, authenticate each session with an authToken — a short-lived Bearer credential you pass via the authToken prop. It authorizes the twin to stream TTS without ever exposing your long-lived secret in the browser.
Generate tokens from your backend by calling getAiTwinAuthToken with your Client-Id and Client-Secret in request headers (not the body). Create and view these credentials on the API Keys page in your aitwin dashboard. Pass userId and userName so usage is attributed to the right end customer in your account.
Your backend calls getAiTwinAuthToken, then your frontend receives only the returned authToken — never the Client Secret.
curl -X POST "https://us-central1-streamoji-265f4.cloudfunctions.net/getAiTwinAuthToken" \
-H "Content-Type: application/json" \
-H "Client-Id: YOUR_CLIENT_ID" \
-H "Client-Secret: YOUR_64_CHAR_API_KEY" \
-d '{
"userId": "end-user-123",
"userName": "Jane Doe"
}'// Backend only — never run this in the browser
const CLIENT_ID = process.env.AITWIN_CLIENT_ID;
const CLIENT_SECRET = process.env.AITWIN_CLIENT_SECRET;
async function getAiTwinAuthToken({
userId,
userName,
maxAvatarCreations,
maxCreditsUtilization,
expiresIn,
}) {
const body = { userId, userName };
if (typeof maxAvatarCreations === "number") {
body.maxAvatarCreations = maxAvatarCreations;
}
if (typeof maxCreditsUtilization === "number") {
body.maxCreditsUtilization = maxCreditsUtilization;
}
if (typeof expiresIn === "number") {
body.expiresIn = expiresIn; // seconds; -1 = no expiry; omit = 30 min default
}
const response = await fetch(
"https://us-central1-streamoji-265f4.cloudfunctions.net/getAiTwinAuthToken",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Client-Id": CLIENT_ID,
"Client-Secret": CLIENT_SECRET,
},
body: JSON.stringify(body),
},
);
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error ?? "Auth token generation failed");
}
return data.authToken; // e.g. "client_eyJ..."
}// Frontend — pass the token from your backend API
<AiTwin
ref={twinRef}
id="olivia"
authToken={authToken}
/>Integration notes
Framework and deployment guidance that is not part of the core install flow.
Next.js
Skip this if you are not using Next.js. AiTwin is browser-only (canvas and related APIs) and must not be server-rendered.
Add the package to transpilePackages in next.config.ts, then import AiTwin with next/dynamic and ssr: false in a client component. The viseme worker is loaded from the Streamoji CDN at runtime — you do not need to copy worker files into your app.
// next.config.ts
const nextConfig = {
transpilePackages: ["@streamoji/aitwin"],
};
export default nextConfig;"use client";
import dynamic from "next/dynamic";
import { useRef } from "react";
import type { AiTwinHandle } from "@streamoji/aitwin";
const AiTwin = dynamic(
() => import("@streamoji/aitwin").then((m) => m.AiTwin),
{ ssr: false },
);Content Security Policy (CSP)
If your site sends a Content-Security-Policy header (or meta tag), allow the AiTwin API, CDN, and WebSocket endpoints below. The SDK fetches encrypted face assets and the lipsync worker from the CDN, streams TTS from the API, and uses blob: URLs for the worker script.
Voice sessions (connect()) also require wss:// on the API host. Backend-only calls such as getAiTwinAuthToken run on your server and are not part of the browser CSP.
When embedding the Create AI Twin iframe, add https://aitwin.me to frame-src. Set allow="microphone; camera" on the iframe element so users can record a voice and capture a photo.
API (connect-src, media-src)
- https://ai.aitwin.me
- wss://ai.aitwin.me
CDN — face assets, thumbnails, worker script (connect-src, img-src, worker-src)
- https://pub-607ad1fc22e2400eb57d17240aab857c.r2.dev
Dashboard & Create AI Twin iframe (frame-src when embedding)
- https://aitwin.me
# Example CSP additions (merge with your existing policy)
connect-src 'self' https://ai.aitwin.me wss://ai.aitwin.me https://pub-607ad1fc22e2400eb57d17240aab857c.r2.dev;
img-src 'self' https://pub-607ad1fc22e2400eb57d17240aab857c.r2.dev data: blob:;
media-src 'self' blob: https://ai.aitwin.me;
worker-src blob: https://pub-607ad1fc22e2400eb57d17240aab857c.r2.dev;
script-src 'self' blob:;
frame-src 'self' https://aitwin.me;Props reference
| Name | Description |
|---|---|
| id | Twin slug for cloud lookup (e.g. olivia). |
| authToken | Short-lived Bearer auth token for TTS and encrypted assets. Generate via getAiTwinAuthToken on your backend. |
| ttsEngineId | TTS engine override (default Cartesia). |
| voiceId | Override the default TTS voice. |
| speakingRate | Default speaking rate (default 0.85). |
| onReady | Called when face assets are loaded and canvas is ready. |
| onStatusChange | TTS status: idle, loading, speaking, done, error. |
| onError | Load or runtime errors. |
| onUserTranscript(text, final) | STT from mic during a voice session; final=true on finalized utterance. |
| onBotOutput(text) | Bot/LLM text from the voice session. |
Ref methods
| Name | Description |
|---|---|
| speakText(text, options?) | Run TTS + lipsync. Optional per-call tts / voiceId overrides. |
| connect(options?) | Open mic + WebSocket voice session. Options: authToken, voiceId, personaId (knowledgeContextId), speakingRate, ttsStream, apiBase, wsUrl. Type: VoiceConnectOptions. |
| disconnect() | End the voice WebSocket session and release the mic. |
| isConnected() | Whether a voice WebSocket session is active. |
| stop() | Stop playback and return toward idle. |
| isReady() | Whether face assets are loaded. |
| getStatus() | Lipsync status: idle, loading, speaking, done, error. |