Avatar only (your own audio)
你有自己的语音链路——自己的语音识别、语言模型和声音,或一个语音到语音的模型?那就只用数字人:你的服务器发送音频,数字人跟着对口型。
不需要角色,控制台里也什么都不用设置;有一个脸的 id(GET /v1/avatars)就够了。
- 你的服务器为一张脸创建会话,要求 Avatar only。
- 页面照常加入,不用麦克风。
- 你的服务器拿到发送音频的凭据,加入会话的房间,等数字人准备好,然后把每一段话以 PCM 发过去。
你的服务器,用 Node 或 Python:
Node
Node 18+;npm install @livekit/rtc-node。
import { Room, TrackKind } from "@livekit/rtc-node";
const API = "https://api.avatargo.ai/v1";
async function call(method, path, body) {
const res = await fetch(API + path, {
method,
headers: { authorization: `Bearer ${process.env.AVATARGO_API_KEY}`, ...(body && { "content-type": "application/json" }) },
body: body && JSON.stringify(body),
});
const json = res.status === 204 ? null : await res.json();
if (!res.ok) throw new Error(`${json.error.code}: ${json.error.message}`);
return json;
}
// 1. 为一张脸创建会话,由你的服务器发送音频。把 sessionToken 交给页面。
export function startSession(avatarId) {
return call("POST", "/auth/session-token", { avatarId, sessionOptions: { audioPassthrough: true } });
}
// 3. 以发送方身份加入房间,等数字人准备好。
export async function joinAsSender(sessionId) {
const ingress = await call("POST", `/sessions/${sessionId}/audio-ingress`);
const room = new Room();
await room.connect(ingress.livekitUrl, ingress.token, { autoSubscribe: false });
// 数字人说完一段话(或被打断)时调用这个方法。
let finished = () => {};
room.localParticipant.registerRpcMethod("lk.playback_finished", async () => (finished(), ""));
// 加入不等于准备好:数字人发布画面之前发送的音频会被丢弃。
const ready = () => [...(room.remoteParticipants.get(ingress.avatarIdentity)?.trackPublications.values() ?? [])]
.some((t) => t.kind === TrackKind.KIND_VIDEO);
while (!ready()) await new Promise((r) => setTimeout(r, 250));
// 每段话一个流:pcm_s16le,24 kHz,单声道。关闭流即结束这段话。
// 数字人说完时才返回:发送比说话快。
const { topic, sampleRate, channels } = ingress.audio;
async function say(pcm) {
const spoken = new Promise((resolve) => (finished = resolve));
const writer = await room.localParticipant.streamBytes({
name: `utterance-${Date.now()}`, topic, destinationIdentities: [ingress.avatarIdentity],
attributes: { sample_rate: String(sampleRate), num_channels: String(channels) },
});
for (let i = 0; i < pcm.length; i += 15_000) await writer.write(pcm.subarray(i, i + 15_000));
await writer.close();
await spoken;
}
return { room, say };
}
// 停下数字人正在说的话;结束会话。
export const interrupt = (sessionId) => call("POST", `/sessions/${sessionId}/interrupt`);
export async function end(sessionId, room) {
await call("POST", `/sessions/${sessionId}/stop`);
await room.disconnect();
}页面——照常用 SDK,不用麦克风:
const client = createClient(sessionToken);
await client.streamToVideoElement("avatar", { microphone: false });在你的服务器上串起来(以 Node 为例;Python 一样,只是写成 snake_case):
const { sessionId, sessionToken } = await startSession("av_…");
// … 把 sessionToken 交给页面,页面按上面的方式加入 …
const { room, say } = await joinAsSender(sessionId);
await say(pcm); // pcm_s16le、24 kHz、单声道的 Buffer:来自你的声音模型或语音到语音模型
// … 随着对话进行,发送更多段话 …
await end(sessionId, room); // 等最后一段说完再结束:提前结束会把它截断需要知道的:
- 音频:
pcm_s16le,24 kHz,单声道——这些值由audio-ingress在audio里返回。发送速度至少要跟上实时;更快也可以,会排队。 数字人说完(或丢弃)排队的内容时,会调用你服务器上的 RPClk.playback_finished,say就在这时返回;在这之前结束会话,会截断剩下的内容。 - 准备好:数字人在你的服务器之后加入,能接收音频时才发布画面——要几秒钟。在那之前发送的音频会被丢弃,所以要像上面那样等待。
- 打断(你的用户开始说话了):
POST /v1/sessions/{id}/interrupt丢弃排队的内容;然后发送下一段话。 - 结束:
POST /v1/sessions/{id}/stop。如果你的服务器直接离开房间,会话会在大约 5 秒后结束(sender_left)。如果在调用audio-ingress后 30 秒内一直没有加入,会话以sender_not_joined失败。 - 没有的东西:没有
message事件(我们既听不到也看不到这些话),没有工具调用,也没有文字记录——这些属于你自己的链路。video_play_started和session_ended照常可用。 - 计费:按会话时长,从数字人接通开始算。
POST /v1/auth/session-token返回的错误:传了脸却没有传audioPassthrough: true时,返回invalid_request,param为sessionOptions.audioPassthrough(这样忘了传characterId不会变成一个不出声的数字人);同时还传了角色时,param为characterId/characterConfig;concurrency_limit和engine_unavailable和其他会话一样。