DOCS
接入文档
把 AI 数字人嵌入你的网站,3 种方式任选,最少 2 行代码即可上线。
工作原理:平台负责呈现,大脑由你接入
平台提供数字人的形象渲染、语音合成(TTS)与口型驱动;对话大脑(LLM)由你的自有系统接入。平台不绑定任何模型,也不代管你的对话逻辑。
「自带 LLM」(BYO-LLM)是实时数字人领域的通行架构,Anam、Tavus、D-ID 等均采用同一模式:平台专注实时呈现,对话智能由接入方自主掌控。用你自己的输入 UI 收集用户问题,调你的 LLM 后用 speak() / lipsync() 驱动数字人开口即可。
查看数字人实时对话效果 —— 前往官网体验三步快速接入
怎么选?
先看这张对比表,再挑最适合自己的方式。
| 方式 | 适用场景 | 可控性 | 样式隔离 |
|---|---|---|---|
| JS SDK | 单页应用、需要事件订阅与程序化控制 | ⭐⭐⭐ | 中 |
| Web Component | Vue / React / 原生 HTML,要求简洁 | ⭐⭐ | 高(Shadow DOM) |
| iframe | 静态网页、CMS、公众号文章、零依赖 | ⭐ | 最高(沙箱) |
选哪条接入路线?
根据你的 LLM 输出什么决定 — 只出文本走 A,自带音频走 B。两条路线可在同一个 widget 实例里混用。
你的 LLM 只输出文本
你的后端只需把 LLM 文字回复发给我们,音色合成和数字人口型由我们完成。接入最简单,3 行代码上线。
- ✓接入最快:只需返回文本
- ✓自动多语言(Azure TTS 全语种)
- ✓流式输出,首句响应 <800ms
- ✓音色由控制台一键切换,无需重新发版
- ✗音色限我们 Azure 音色库,无法用自家定制音色
- ✗不能复用你已有的 TTS 投入
widget.speak(text) // 你的 LLM 文本 → 平台合成 TTS + 口型- •通用客服 / FAQ 问答
- •多语言场景(英 / 日 / 韩 / 阿语等)
- •MVP 快速验证,后续可平滑切到路线 B
你的 LLM 输出文本 + 音频
你的后端用自己的 TTS 生成音频,我们只负责把音频对齐成口型。完全掌控音色和发音,适合品牌定制场景。
- ✓音色完全可控,自家明星 / IP / 品牌音
- ✓可用方言 / 定制发音 / 情感语气
- ✓复用你已有的 TTS 平台投入
- ✓走 audioUrl 时不消耗我们 TTS 字符额度
- ✗需要自备 TTS(成本和延迟你自己控制)
- ✗音频必须 WAV 容器、单文件 ≤ 10MB
- ✗走 audioUrl 时音频域名需对我们放行 CORS
widget.lipsync(audioUrl) // 你的音频(URL) → 平台只做口型widget.lipsync(audioBlob) // 你的音频(Blob) → 平台只做口型- •品牌 IP / 明星定制音色数字人
- •方言播报(粤语 / 闽南语 / 特色英语等)
- •已有 TTS 平台用户,只想加口型
- →第一次接入 → 选 A,只要回个字符串就能让数字人开口,3 行代码跑通
- →必须用自家音色 / 明星 IP → 选 B,你的 TTS 出音频,我们对口型
- →没想清楚 → 先 A 上线,后面想换 B 不用改架构,同一个 widget 实例换 API 即可
在 React / Vue 里怎么用?
用我们的 npm 包 —— 真正的 React / Vue <AIAvatar> 组件,带 ref 句柄调 speak() / lipsync()。安装:npm i @ai-avatar/embed-sdk
// 路线 A:你的 LLM 返回文本,平台做 TTS + 口型
// npm i @ai-avatar/embed-sdk
import { useRef, useState } from 'react';
import { AIAvatar, type AvatarInstance } from '@ai-avatar/embed-sdk/react';
export function AvatarCanvas() {
const avatar = useRef<AvatarInstance>(null);
const [ready, setReady] = useState(false);
// 你自己的输入框提交 → 调你的 LLM → 驱动数字人开口
async function onUserSend(text: string) {
const reply = await myLLM(text); // 你自己的 LLM
const r = await avatar.current?.speak(reply); // 平台 TTS + 口型
// r?.usage?.chars, r?.interrupted ...
}
return (
<>
{/* 你的容器 —— 数字人 100% 撑满它;尺寸 / 位置 / 圆角用你自己的 CSS */}
<div id="avatar-box" style={{ width: 360, height: 480, borderRadius: 16, overflow: 'hidden' }} />
<AIAvatar
ref={avatar}
apiKey="YOUR_API_KEY"
mount="#avatar-box" // 纯展示画布(传了 mount 即自动 inline)
onAvatarLoaded={() => setReady(true)} // 就绪后再放开你的输入框
onSpeakingChange={(speaking) => {/* 同步你的 UI */}}
/>
{/* ...你自己的输入框;提交时调 onUserSend,就绪前禁用... */}
</>
);
}
// ↓ 换成你自己的 LLM 调用
async function myLLM(text: string) {
const res = await fetch('/api/my-llm', { method: 'POST', body: JSON.stringify({ text }) });
return (await res.json()).reply as string;
}完整参数参考
所有可用参数 + 类型 + 默认值 + 说明,按接入方式切换查看。
apiKeystring必填用于鉴权、授权本次请求的密钥。
格式 'sk-<48位>'API Key,随数字人自动生成,在该数字人的「嵌入代码」页复制。建议一站点一把 Key 顺便加 Origin 白名单,泄漏时方便定向回收
mountstring | HTMLElement可选默认: —把数字人挂成一块撑满你自己容器的画布。
CSS 选择器或 DOM 元素,如 '#ai-avatar'mount: '#ai-avatar'(或传入元素)。displayMode 为 'inline' 时必填
appearance.displayModeenum可选默认: 'bubble'选择撑满容器的画布,还是悬浮气泡。
'inline'— 会说话的画布:数字人撑满你的 mount 容器(推荐的纯展示范式),常驻画面、嵌进你自己的布局'bubble'— 固定在视口角落的悬浮气泡(默认),点击展开数字人在页面上的放置方式。'inline' 需要配 mount 容器;'bubble' 是自管理的悬浮 widget
appearance.positionenum可选默认: 'bottom-right'将组件固定在视口的某个角落。
'bottom-right'— 右下角(默认)'bottom-left'— 左下角'top-right'— 右上角'top-left'— 左上角传入 appearance.position, SDK 自动应用到 wrapper 的 fixed left/right + top/bottom
appearance.sizeenum可选默认: 'normal'设置数字人的展示尺寸。
'compact'— 340 × 55vh,移动端友好'normal'— 400 × 66vh(默认)'large'— 480 × 80vh,大屏 demo传入 appearance.size, SDK 展开时把对应像素值套到 wrapper width/height
appearance.scalenumber可选默认: 1等比缩放数字人。
推荐范围 0.5 ~ 1.5传入 appearance.scale: 0.9, SDK 展开 panel 时给 iframe 加 transform: scale(0.9)
appearance.zIndexnumber可选默认: 2147483647控制数字人相对页面元素的层叠顺序。
任意整数;你的 UI 被遮挡时调低 (如 1) 让你的元素盖在数字人之上传入 appearance.zIndex: 1, SDK 应用到 wrapper
appearance.interactiveboolean可选默认: false(指针事件默认穿透)决定数字人捕获鼠标事件,还是让事件穿透。
true— iframe 接收鼠标 / 触摸事件false— 整片区域点击穿透到下层你的 UI传入 appearance.interactive: false, SDK 给 wrapper 加 pointer-events: none
appearance.left / right / top / bottomnumber | string可选默认: —以像素级偏移精确定位组件。
number 自动加 px (24 → '24px'); string 原样透传 ('10vw' / '5%' / '50px')传入 appearance.left: 24 等, SDK 把对应字段写到 wrapper inline style (未指定的边保持 auto)
appearance.width / heightnumber | string可选默认: —以显式像素尺寸覆盖 size 预设。
number 自动加 px (320 → '320px'); string 原样透传 ('40vw' / '60%' / '500px')传入 appearance.width / height, SDK 展开时写到 wrapper inline style
behavior.lazyLoadboolean可选默认: false延迟加载数字人至用户打开时,加快首屏速度。
false— init 立即创建 iframe + 加载 GLB + 建 session (默认)true— init 不创建, 等你调 widget.show() 才触发传入 behavior.lazyLoad: true, init 只占位 wrapper, widget.show() 调用时才真正下载 GLB + 建 session
运行时控制 API
加载完成后,通过实例方法以编程方式驱动数字人:SDK 调用 init() 返回的实例,Web Component 直接调用 <ai-avatar> 元素。speak() 与 lipsync() 为异步方法,返回 Promise<SpeakResult>。
const widget = AIAvatar.init({...}); await widget.speak('你好')await document.querySelector('ai-avatar').speak('你好')audioDurationnumber- 本次播报的音频时长,单位为秒。
usage{ chars?: number; seconds?: number }- 本次调用的计量用量:合成字符数(chars)与音频秒数(seconds)。
interruptedboolean- 为 true 时表示本次播报被新的驱动调用或 stop() 中断。
| 方法 | 签名 | 说明 |
|---|---|---|
speak() | speak(text, options?): Promise<SpeakResult> | A 路线:传文本,平台合成 TTS 并驱动口型。Promise 在整段播完(或被打断)时 resolve、失败时 reject。await 它读 SpeakResult(时长 + 用量)。 |
lipsync() | lipsync(audio, text?): Promise<SpeakResult> | B 路线:传音频(WAV 链接,或 Blob / File ≤ 10MB),平台只驱动口型(不做 TTS)。Promise<SpeakResult> 语义同 speak()。 |
waitAvatarLoaded() | waitAvatarLoaded(): Promise<void> | 数字人 GLB + 口型引擎就绪时 resolve(已加载则立即 resolve)。纯展示客户在放开自己的输入框前 await 它。 |
isAvatarLoaded() | isAvatarLoaded(): boolean | 同步检查数字人 GLB 是否已加载完。 |
纯展示范式 —— 输入框和 LLM 都由你自带,数字人只是一块画布。speak() / lipsync() 在数字人说完时 resolve;读 SpeakResult 拿用量,被新调用打断时 interrupted 为 true。
// HTML: <div id="ai-avatar" style="width:360px;height:480px;border-radius:16px;overflow:hidden"></div>
const avatar = AIAvatar.init({
apiKey: 'sk-...',
mount: '#ai-avatar', // 你的容器; 数字人 100% 撑满它
appearance: { displayMode: 'inline' }, // 纯展示画布
onAvatarLoaded() { /* 数字人就绪 — 放开你的输入框 */ },
onSpeakingChange(speaking) { /* 说话时同步你自己的 UI */ },
});
// 也可以用 await 等就绪, 替代上面的回调
await avatar.waitAvatarLoaded();
// 你自己的输入框提交 → 调你的 LLM → 驱动数字人
async function onUserSend(text) {
const reply = await myLLM(text); // 你自己的 LLM
const result = await avatar.speak(reply); // 平台 TTS + 口型
if (!result.interrupted) {
console.log('说完了, TTS 用了字符:', result.usage?.chars);
}
}回调 / 事件
在 init() 里作为回调传入(SDK),或作为事件监听 —— Web Component 的 avatar-* CustomEvent / iframe 的 postMessage AVATAR_*。
onAvatarLoaded() => void数字人 GLB + 口型引擎就绪时触发。纯展示客户在这里放开自己的输入框。
SDK: onAvatarLoaded() · WC: avatar-loaded · iframe: AVATAR_LOADEDonSpeakingChange(speaking: boolean) => void数字人开始 / 停止说话时触发。同步你自己的 UI —— 比如说话时禁用输入框。
SDK: onSpeakingChange(speaking) · WC: avatar-speaking (e.detail.speaking) · iframe: AVATAR_SPEAKINGonError(err: { code: string; message: string }) => void出错时触发 —— 加载失败、驱动(speak / lipsync)失败、鉴权 / 配额问题。
SDK: onError(err) · WC: avatar-error (e.detail) · iframe: ERROR组件控制
控制组件的显隐、形态与生命周期。show() / hide() 控制整体显隐,expand() / collapse() 在气泡与展开形态间切换 —— 二者为相互独立的维度。
| 方法 | 签名 | 说明 |
|---|---|---|
show() | show(): void | 显示 widget。lazyLoad 模式下第一次调用会触发 iframe 创建 + GLB 下载 + session 创建;之后调用只切换可见性,不重新下载。 |
hide() | hide(): void | 隐藏整个 widget(数字人 + 气泡),状态保留,再调 show() 可恢复。 |
expand() | expand(): void | bubble → full 展开。仅当组件以气泡形态启动时有意义。 |
collapse() | collapse(): void | full → bubble 折叠。仅当组件有气泡形态时有意义。 |
toggle() | toggle(): void | expand / collapse 之间切换。 |
reloadConfig() | reloadConfig(): void | 你在控制台改完配置后,通知嵌入端重拉(无需刷新页面)。 |
destroy() | destroy(): void | 彻底销毁(移除 DOM + 解绑事件),不可恢复。Web Component 同步把 <ai-avatar> 元素自身从 DOM 移除;SDK 要重新调 AIAvatar.init()。 |
- •speak (文本→TTS+口型): postMessage `{ __avatar: true, id: String(Date.now()), type: 'SPEAK_TEXT', payload: { text } }` 给 iframe.contentWindow
- •lipsync (音频→口型): postMessage `{ __avatar: true, id, type: 'SPEAK_AUDIO', payload: { audioUrl, text } }`(URL 需 CORS 放行嵌入域)或 `payload: { audio, text }`(audio 为 Blob/File, 跨 origin 透传)
- •两者都监听回传的 `SPEAK_RESULT` 消息(按发出时的 id 关联)拿 SpeakResult
- •show / hide: 父页改 iframe.style.display = 'block' / 'none' 即可
- •expand / collapse: postMessage `{ __avatar: true, type: 'EXPAND' / 'COLLAPSE' }` 给 iframe.contentWindow, 配合 HEIGHT_CHANGE bridge 接收 PureJS 回的尺寸
- •destroy: iframe.remove()
常见错误码
| 状态码 | 含义 | 排查建议 |
|---|---|---|
400 | 请求参数错误 | 对照接口文档检查必填字段(如 text 长度、voiceId 是否合法)。 |
401 | Token 无效或已禁用 | 检查 API Key 是否拼错、是否已在控制台禁用。 |
402 | 额度耗尽 | 前往「计费与用量」升级套餐或购买流量包。 |
403 | 域名未在白名单 | 到项目的「嵌入代码」页,把当前域名加进域名白名单。 |
404 | 资源不存在 | 核对 sessionId 拼写,以及是不是别的用户的资源。 |
422 | 文本与音色语言不匹配 / 合成音频为空 | 比如给中文文本配了非中文音色。到项目设置把音色换成与你内容语言一致的。 |
429 | 速率超限 | 客户端降低调用频率,或升级套餐提升上限。 |
LLM 失败兜底:别让数字人卡在转圈
数字人只会说你回传的文本。你的 LLM 超时或挂了时必须在你自己的 UI 里兜底,否则数字人没有回复、用户一直干等。
- 超时 / 出错时,调 speak('抱歉,出错了') 让数字人说句兜底,别冷场。
- 用户连续发问:speak / lipsync 会自动中断上一条播放,但你自己的 UI 要自己去重 / 排队。
- 必须等 onAvatarLoaded / avatar-loaded 信号后再放行用户输入,否则首句会「听到声音却看不到数字人、且没口型」。
// 你自己的输入处理 → 你的 LLM → 驱动数字人
async function onUserSend(text) {
try {
const reply = await myCustomerLLM(text);
widget.speak(reply);
} catch (e) {
widget.speak('抱歉,我这边出了点问题,请稍后再试'); // 兜底回复
}
}安全:务必配置域名白名单
前端嵌入场景 API Key 不可避免会暴露给浏览器,白名单是最有效的防盗用手段。
在项目的 「嵌入代码」 页找到「域名白名单」,添加允许的来源域名(如 https://your-site.com)。后端会根据请求的 Origin 头校验,非白名单直接拒绝。
- 支持精确匹配(含协议和端口),也支持 *.example.com 通配子域;一行一个
- 开发环境单独加 http://localhost:3000,不要用通配符
- 一把 Key 只服务一个站点,泄漏时方便精准回收