integrate-characters

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Integrate Characters (GWM-1 Avatars)

集成Characters(GWM-1 数字人)

PREREQUISITES:
  • +check-compatibility
    — Project must have a server-side component (API key must NEVER be exposed to the client)
  • +fetch-api-reference
    — Load the latest API reference from https://docs.dev.runwayml.com/api/ before integrating
  • +setup-api-key
    — API credentials must be configured
OPTIONAL DEPENDENCIES:
  • +integrate-documents
    — Add a knowledge base to your character
  • +integrate-character-embed
    — Use the React SDK to embed the avatar call UI
Help users create Runway Characters — real-time conversational AI avatars powered by GWM-1.
Characters are generated from a single image (any visual style — photorealistic, animated, non-human) with full control over voice, personality, knowledge, and actions. No fine-tuning or training required.
前置要求:
可选依赖:
  • +integrate-documents
    — 为你的数字人添加知识库
  • +integrate-character-embed
    — 使用React SDK嵌入数字人通话UI
帮助用户创建Runway Characters——由GWM-1提供支持的实时对话AI数字人。
数字人可通过单张图片生成(支持任意视觉风格:写实风、动画风、非人类形象),你可以完全控制其声音、性格、知识储备和动作,无需微调或训练。

Key Concepts

核心概念

Avatars vs Sessions

数字人 vs 会话

ConceptDescription
AvatarA persistent persona with a defined appearance, voice, and personality. Created once, used many times.
SessionA live WebRTC connection for real-time conversation. Connects one user to one avatar. Max duration: 5 minutes.
概念描述
Avatar(数字人)具备固定外貌、声音和性格的持久化角色,一次创建可多次使用。
Session(会话)用于实时对话的WebRTC长连接,实现单个用户与单个数字人的连接。最长持续时间:5分钟。

Session Lifecycle

会话生命周期

                    ┌───────────┐
         ┌──────────┤ NOT_READY ├──────────┐
         │          └─────┬─────┘          │
         │                │                │
         ▼                ▼                ▼
     CANCELLED          READY           FAILED
                       ┌──┴──┐
                       │     │
                       ▼     ▼
                    RUNNING FAILED
                    ┌──┴──┐
                    │     │
                    ▼     ▼
                COMPLETED CANCELLED
StatusDescription
NOT_READY
Session is being provisioned. Poll until ready.
READY
Session is ready. The
sessionKey
is available.
RUNNING
WebRTC connection is active. Conversation in progress.
COMPLETED
Session ended normally.
FAILED
Error occurred. Check the
failure
field.
CANCELLED
Explicitly cancelled before completion.
Important: Session credentials can only be consumed once. If the WebRTC connection fails after credentials are consumed, you must create a new Session.
                    ┌───────────┐
         ┌──────────┤ NOT_READY ├──────────┐
         │          └─────┬─────┘          │
         │                │                │
         ▼                ▼                ▼
     CANCELLED          READY           FAILED
                       ┌──┴──┐
                       │     │
                       ▼     ▼
                    RUNNING FAILED
                    ┌──┴──┐
                    │     │
                    ▼     ▼
                COMPLETED CANCELLED
状态描述
NOT_READY
会话正在配置中,请轮询直至就绪。
READY
会话已准备就绪,可获取
sessionKey
RUNNING
WebRTC连接已激活,对话正在进行中。
COMPLETED
会话正常结束。
FAILED
发生错误,请查看
failure
字段。
CANCELLED
会话在完成前被主动取消。
重要提示: 会话凭证仅可使用一次。如果凭证被消费后WebRTC连接失败,你必须创建新的会话。

Architecture

架构

The API key must stay server-side. The flow is:
Client (React)  →  Your Server  →  Runway API
Client (React)  ←─── WebRTC ───← Runway (realtime)
  1. Client requests a session from your server
  2. Your server calls Runway API to create a session (
    POST /v1/realtime_sessions
    )
  3. Your server polls until session is
    READY
    (
    GET /v1/realtime_sessions/:id
    )
  4. Your server consumes credentials (
    POST /v1/realtime_sessions/:id/consume
    )
  5. Your server returns credentials to the client
  6. Client establishes a direct WebRTC connection to Runway
API密钥必须保存在服务端,交互流程为:
Client (React)  →  你的服务端  →  Runway API
Client (React)  ←─── WebRTC ───← Runway (realtime)
  1. 客户端向你的服务端请求会话
  2. 你的服务端调用Runway API创建会话(
    POST /v1/realtime_sessions
  3. 你的服务端轮询会话状态直到变为
    READY
    GET /v1/realtime_sessions/:id
  4. 你的服务端消费凭证(
    POST /v1/realtime_sessions/:id/consume
  5. 你的服务端将凭证返回给客户端
  6. 客户端与Runway建立直接的WebRTC连接

Step 1: Install Dependencies

步骤1:安装依赖

bash
npm install @runwayml/sdk @runwayml/avatars-react
  • @runwayml/sdk
    — Server-side SDK (session creation, avatar management)
  • @runwayml/avatars-react
    — Client-side React components (WebRTC, UI)
bash
npm install @runwayml/sdk @runwayml/avatars-react
  • @runwayml/sdk
    — 服务端SDK(用于会话创建、数字人管理)
  • @runwayml/avatars-react
    — 客户端React组件(用于WebRTC连接、UI渲染)

Step 2: Create an Avatar

步骤2:创建数字人

Avatars can be created via the Developer Portal (UI) or the API (programmatic).
你可以通过**开发者门户(UI)API(编程方式)**创建数字人。

Option A: Developer Portal (Recommended for first time)

选项A:开发者门户(首次使用推荐)

  1. Go to https://dev.runwayml.com/Characters tab
  2. Click Create a Character
  3. Upload a reference image (tips below)
  4. Choose a voice preset
  5. Write personality instructions (e.g., "You are a helpful customer support agent for Acme Corp...")
  6. Optionally add a starting script (what the character says first)
  7. Optionally upload knowledge documents (
    .txt
    files)
  8. Click Create Character
  9. Copy the Avatar ID (a UUID like
    8be4df61-93ca-11d2-aa0d-00e098032b8c
    )
  1. 访问 https://dev.runwayml.com/ → 进入 Characters 标签页
  2. 点击 Create a Character
  3. 上传参考图片(提示见下文)
  4. 选择语音预设
  5. 编写性格说明(例如:“你是Acme公司的客服代理,乐于助人...”)
  6. 可选添加开场脚本(数字人首次发言的内容)
  7. 可选上传知识文档(
    .txt
    格式)
  8. 点击 Create Character
  9. 复制 Avatar ID(UUID格式,例如
    8be4df61-93ca-11d2-aa0d-00e098032b8c

Option B: API (Programmatic)

选项B:API(编程方式)

javascript
// Node.js
import RunwayML from '@runwayml/sdk';

const client = new RunwayML();

const avatar = await client.avatars.create({
  name: 'Support Agent',
  referenceImage: 'https://example.com/avatar.png',
  voice: {
    type: 'runway-live-preset',
    presetId: 'clara',
  },
  personality: 'You are a helpful customer support agent for Acme Corp. You help users with billing questions and technical issues.',
});

console.log('Avatar ID:', avatar.id);
python
undefined
javascript
// Node.js
import RunwayML from '@runwayml/sdk';

const client = new RunwayML();

const avatar = await client.avatars.create({
  name: 'Support Agent',
  referenceImage: 'https://example.com/avatar.png',
  voice: {
    type: 'runway-live-preset',
    presetId: 'clara',
  },
  personality: 'You are a helpful customer support agent for Acme Corp. You help users with billing questions and technical issues.',
});

console.log('Avatar ID:', avatar.id);
python
undefined

Python

Python

from runwayml import RunwayML
client = RunwayML()
avatar = client.avatars.create( name='Support Agent', reference_image='https://example.com/avatar.png', voice={ 'type': 'runway-live-preset', 'preset_id': 'clara', }, personality='You are a helpful customer support agent for Acme Corp.', )
print('Avatar ID:', avatar.id)

**If the reference image is a local file**, upload it first using `+integrate-uploads`:

```javascript
import fs from 'fs';

const upload = await client.uploads.createEphemeral(
  fs.createReadStream('/path/to/avatar-image.png')
);

const avatar = await client.avatars.create({
  name: 'Support Agent',
  referenceImage: upload.runwayUri,
  voice: { type: 'runway-live-preset', presetId: 'clara' },
  personality: 'You are a helpful customer support agent...',
});
from runwayml import RunwayML
client = RunwayML()
avatar = client.avatars.create( name='Support Agent', reference_image='https://example.com/avatar.png', voice={ 'type': 'runway-live-preset', 'preset_id': 'clara', }, personality='You are a helpful customer support agent for Acme Corp.', )
print('Avatar ID:', avatar.id)

**如果参考图片是本地文件**,请先使用`+integrate-uploads`上传:

```javascript
import fs from 'fs';

const upload = await client.uploads.createEphemeral(
  fs.createReadStream('/path/to/avatar-image.png')
);

const avatar = await client.avatars.create({
  name: 'Support Agent',
  referenceImage: upload.runwayUri,
  voice: { type: 'runway-live-preset', presetId: 'clara' },
  personality: 'You are a helpful customer support agent...',
});

Reference Image Guidelines

参考图片规范

  • Any visual style works: photorealistic humans, animated mascots, stylized brand characters
  • Use high-quality images with good lighting
  • Face should be clearly visible and centered
  • Avoid images with multiple people or obstructions
  • Recommended aspect ratio: 1088×704
  • 支持任意视觉风格:写实人像、动画吉祥物、风格化品牌角色均可
  • 使用光线良好的高清图片
  • 面部应当清晰可见且居中
  • 避免包含多人或有遮挡的图片
  • 推荐宽高比:1088×704

Voice Presets

语音预设

Preset IDNameStyle
clara
ClaraSoft, approachable
victoria
VictoriaFirm, professional
vincent
VincentKnowledgeable, authoritative
Preview all voices in the Developer Portal.
Preset ID名称风格
clara
Clara柔和、亲切
victoria
Victoria稳重、专业
vincent
Vincent博学、权威
你可以在开发者门户预览所有语音效果。

Step 3: Create a Session (Server-Side)

步骤3:创建会话(服务端)

This is the server-side API route that your client will call. It creates a session, polls until ready, consumes credentials, and returns them.
这是供客户端调用的服务端API接口,它会创建会话、轮询直至就绪、消费凭证并返回结果。

Next.js App Router

Next.js App Router

typescript
// app/api/avatar/session/route.ts
import RunwayML from '@runwayml/sdk';

const client = new RunwayML();

export async function POST(request: Request) {
  const { avatarId } = await request.json();

  // 1. Create session
  const { id: sessionId } = await client.realtimeSessions.create({
    model: 'gwm1_avatars',
    avatar: { type: 'custom', avatarId },
  });

  // 2. Poll until ready
  let sessionKey: string | undefined;
  for (let i = 0; i < 60; i++) {
    const session = await client.realtimeSessions.retrieve(sessionId);

    if (session.status === 'READY') {
      sessionKey = session.sessionKey;
      break;
    }
    if (session.status === 'FAILED') {
      return Response.json({ error: session.failure }, { status: 500 });
    }

    await new Promise(r => setTimeout(r, 1000));
  }

  if (!sessionKey) {
    return Response.json({ error: 'Session timed out' }, { status: 504 });
  }

  // 3. Consume session to get WebRTC credentials
  const consumeResponse = await fetch(
    `${client.baseURL}/v1/realtime_sessions/${sessionId}/consume`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${sessionKey}`,
        'X-Runway-Version': '2024-11-06',
      },
    }
  );
  const credentials = await consumeResponse.json();

  return Response.json({
    sessionId,
    serverUrl: credentials.url,
    token: credentials.token,
    roomName: credentials.roomName,
  });
}
typescript
// app/api/avatar/session/route.ts
import RunwayML from '@runwayml/sdk';

const client = new RunwayML();

export async function POST(request: Request) {
  const { avatarId } = await request.json();

  // 1. 创建会话
  const { id: sessionId } = await client.realtimeSessions.create({
    model: 'gwm1_avatars',
    avatar: { type: 'custom', avatarId },
  });

  // 2. 轮询直到会话就绪
  let sessionKey: string | undefined;
  for (let i = 0; i < 60; i++) {
    const session = await client.realtimeSessions.retrieve(sessionId);

    if (session.status === 'READY') {
      sessionKey = session.sessionKey;
      break;
    }
    if (session.status === 'FAILED') {
      return Response.json({ error: session.failure }, { status: 500 });
    }

    await new Promise(r => setTimeout(r, 1000));
  }

  if (!sessionKey) {
    return Response.json({ error: 'Session timed out' }, { status: 504 });
  }

  // 3. 消费会话获取WebRTC凭证
  const consumeResponse = await fetch(
    `${client.baseURL}/v1/realtime_sessions/${sessionId}/consume`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${sessionKey}`,
        'X-Runway-Version': '2024-11-06',
      },
    }
  );
  const credentials = await consumeResponse.json();

  return Response.json({
    sessionId,
    serverUrl: credentials.url,
    token: credentials.token,
    roomName: credentials.roomName,
  });
}

Express.js

Express.js

typescript
import RunwayML from '@runwayml/sdk';
import express from 'express';

const client = new RunwayML();
const app = express();
app.use(express.json());

app.post('/api/avatar/session', async (req, res) => {
  const { avatarId } = req.body;

  try {
    // 1. Create session
    const { id: sessionId } = await client.realtimeSessions.create({
      model: 'gwm1_avatars',
      avatar: { type: 'custom', avatarId },
    });

    // 2. Poll until ready
    let sessionKey: string | undefined;
    for (let i = 0; i < 60; i++) {
      const session = await client.realtimeSessions.retrieve(sessionId);

      if (session.status === 'READY') {
        sessionKey = session.sessionKey;
        break;
      }
      if (session.status === 'FAILED') {
        return res.status(500).json({ error: session.failure });
      }

      await new Promise(r => setTimeout(r, 1000));
    }

    if (!sessionKey) {
      return res.status(504).json({ error: 'Session timed out' });
    }

    // 3. Consume credentials
    const consumeResponse = await fetch(
      `${client.baseURL}/v1/realtime_sessions/${sessionId}/consume`,
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${sessionKey}`,
          'X-Runway-Version': '2024-11-06',
        },
      }
    );
    const credentials = await consumeResponse.json();

    res.json({
      sessionId,
      serverUrl: credentials.url,
      token: credentials.token,
      roomName: credentials.roomName,
    });
  } catch (error) {
    console.error('Session creation failed:', error);
    res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
  }
});
typescript
import RunwayML from '@runwayml/sdk';
import express from 'express';

const client = new RunwayML();
const app = express();
app.use(express.json());

app.post('/api/avatar/session', async (req, res) => {
  const { avatarId } = req.body;

  try {
    // 1. 创建会话
    const { id: sessionId } = await client.realtimeSessions.create({
      model: 'gwm1_avatars',
      avatar: { type: 'custom', avatarId },
    });

    // 2. 轮询直到会话就绪
    let sessionKey: string | undefined;
    for (let i = 0; i < 60; i++) {
      const session = await client.realtimeSessions.retrieve(sessionId);

      if (session.status === 'READY') {
        sessionKey = session.sessionKey;
        break;
      }
      if (session.status === 'FAILED') {
        return res.status(500).json({ error: session.failure });
      }

      await new Promise(r => setTimeout(r, 1000));
    }

    if (!sessionKey) {
      return res.status(504).json({ error: 'Session timed out' });
    }

    // 3. 消费凭证
    const consumeResponse = await fetch(
      `${client.baseURL}/v1/realtime_sessions/${sessionId}/consume`,
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${sessionKey}`,
          'X-Runway-Version': '2024-11-06',
        },
      }
    );
    const credentials = await consumeResponse.json();

    res.json({
      sessionId,
      serverUrl: credentials.url,
      token: credentials.token,
      roomName: credentials.roomName,
    });
  } catch (error) {
    console.error('Session creation failed:', error);
    res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
  }
});

FastAPI (Python)

FastAPI (Python)

python
import time
import httpx
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from runwayml import RunwayML

app = FastAPI()
client = RunwayML()

class SessionRequest(BaseModel):
    avatar_id: str

@app.post("/api/avatar/session")
async def create_session(req: SessionRequest):
    # 1. Create session
    result = client.realtime_sessions.create(
        model='gwm1_avatars',
        avatar={'type': 'custom', 'avatar_id': req.avatar_id},
    )
    session_id = result.id

    # 2. Poll until ready
    session_key = None
    for _ in range(60):
        session = client.realtime_sessions.retrieve(session_id)

        if session.status == 'READY':
            session_key = session.session_key
            break
        if session.status == 'FAILED':
            raise HTTPException(status_code=500, detail=str(session.failure))

        time.sleep(1)

    if not session_key:
        raise HTTPException(status_code=504, detail='Session timed out')

    # 3. Consume credentials
    async with httpx.AsyncClient() as http:
        resp = await http.post(
            f"{client.base_url}/v1/realtime_sessions/{session_id}/consume",
            headers={
                "Authorization": f"Bearer {session_key}",
                "X-Runway-Version": "2024-11-06",
            },
        )
    credentials = resp.json()

    return {
        "session_id": session_id,
        "server_url": credentials["url"],
        "token": credentials["token"],
        "room_name": credentials["roomName"],
    }
python
import time
import httpx
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from runwayml import RunwayML

app = FastAPI()
client = RunwayML()

class SessionRequest(BaseModel):
    avatar_id: str

@app.post("/api/avatar/session")
async def create_session(req: SessionRequest):
    # 1. 创建会话
    result = client.realtime_sessions.create(
        model='gwm1_avatars',
        avatar={'type': 'custom', 'avatar_id': req.avatar_id},
    )
    session_id = result.id

    # 2. 轮询直到会话就绪
    session_key = None
    for _ in range(60):
        session = client.realtime_sessions.retrieve(session_id)

        if session.status == 'READY':
            session_key = session.session_key
            break
        if session.status == 'FAILED':
            raise HTTPException(status_code=500, detail=str(session.failure))

        time.sleep(1)

    if not session_key:
        raise HTTPException(status_code=504, detail='Session timed out')

    # 3. 消费凭证
    async with httpx.AsyncClient() as http:
        resp = await http.post(
            f"{client.base_url}/v1/realtime_sessions/{session_id}/consume",
            headers={
                "Authorization": f"Bearer {session_key}",
                "X-Runway-Version": "2024-11-06",
            },
        )
    credentials = resp.json()

    return {
        "session_id": session_id,
        "server_url": credentials["url"],
        "token": credentials["token"],
        "room_name": credentials["roomName"],
    }

Step 4: Connect from the Client

步骤4:客户端连接

See
+integrate-character-embed
for the React SDK components that handle WebRTC connection and rendering. The simplest approach:
tsx
'use client';
import { AvatarCall } from '@runwayml/avatars-react';
import '@runwayml/avatars-react/styles.css';

export default function CharacterPage() {
  return (
    <AvatarCall
      avatarId="your-avatar-id"
      connectUrl="/api/avatar/session"
      onEnd={() => console.log('Call ended')}
      onError={(error) => console.error('Error:', error)}
    />
  );
}
查看
+integrate-character-embed
了解负责WebRTC连接和渲染的React SDK组件,最简实现方式如下:
tsx
'use client';
import { AvatarCall } from '@runwayml/avatars-react';
import '@runwayml/avatars-react/styles.css';

export default function CharacterPage() {
  return (
    <AvatarCall
      avatarId="your-avatar-id"
      connectUrl="/api/avatar/session"
      onEnd={() => console.log('Call ended')}
      onError={(error) => console.error('Error:', error)}
    />
  );
}

Troubleshooting

问题排查

  • API key errors: Key starts with
    key_
    followed by 128 hex chars. Ensure it's active.
  • No credits: Account must have prepaid credits before starting a call.
  • Session timeout: The 60-iteration poll loop waits ~60 seconds. If sessions consistently time out, check your tier's concurrency limits.
  • Credentials already consumed: Session credentials are one-time use. If WebRTC fails after consume, create a new session.
  • API密钥错误: 密钥以
    key_
    开头,后面跟着128个十六进制字符,请确保密钥处于激活状态。
  • 余额不足: 发起通话前账户必须有预付费余额。
  • 会话超时: 60次轮询循环大约等待60秒,如果会话持续超时,请查看你所在套餐的并发限制。
  • 凭证已被消费: 会话凭证为单次使用,如果消费后WebRTC连接失败,请创建新的会话。

Debug logging

调试日志

tsx
<AvatarCall
  avatarId="your-avatar-id"
  connectUrl="/api/avatar/session"
  onError={(error) => {
    console.error('Avatar error:', error);
    console.error('Error name:', error.name);
    console.error('Error message:', error.message);
    if (error.cause) console.error('Cause:', error.cause);
  }}
/>
tsx
<AvatarCall
  avatarId="your-avatar-id"
  connectUrl="/api/avatar/session"
  onError={(error) => {
    console.error('Avatar error:', error);
    console.error('Error name:', error.name);
    console.error('Error message:', error.message);
    if (error.cause) console.error('Cause:', error.cause);
  }}
/>

Monitor session state

监控会话状态

tsx
import { useAvatarSession } from '@runwayml/avatars-react';

function DebugInfo() {
  const { state, sessionId, error } = useAvatarSession();
  return (
    <pre>
      {JSON.stringify({ state, sessionId, error: error?.message }, null, 2)}
    </pre>
  );
}
tsx
import { useAvatarSession } from '@runwayml/avatars-react';

function DebugInfo() {
  const { state, sessionId, error } = useAvatarSession();
  return (
    <pre>
      {JSON.stringify({ state, sessionId, error: error?.message }, null, 2)}
    </pre>
  );
}

Test with minimal setup

最小化测试配置

bash
npx degit runwayml/avatars-sdk-react/examples/nextjs-simple test-app
cd test-app
npm install
bash
npx degit runwayml/avatars-sdk-react/examples/nextjs-simple test-app
cd test-app
npm install

Add your API key to .env.local

将你的API密钥添加到.env.local文件中

npm run dev
undefined
npm run dev
undefined

Browser Support

浏览器支持

BrowserMinimum Version
Chrome74+
Firefox78+
Safari14.1+
Edge79+
Users must grant microphone permissions. Camera permissions needed if user video is enabled.
浏览器最低版本要求
Chrome74+
Firefox78+
Safari14.1+
Edge79+
用户必须授予麦克风权限,如果开启用户视频则需要授予摄像头权限。

Getting Help

获取帮助

ResourceDescription
Developer PortalManage avatars, view logs, access dashboard
SDK RepositoryReport bugs, view examples, check releases
When reporting issues, include: browser/version, SDK version (
npm list @runwayml/avatars-react
), error messages, session ID, and steps to reproduce.
资源描述
开发者门户管理数字人、查看日志、访问控制面板
SDK仓库提交Bug、查看示例、检查版本更新
上报问题时,请提供以下信息:浏览器/版本、SDK版本(执行
npm list @runwayml/avatars-react
查看)、错误信息、会话ID以及复现步骤。