Skip to Content
Server IntegrationAvatar only (your own audio)

Avatar only (your own audio)

你有自己的语音链路——自己的语音识别、语言模型和声音,或一个语音到语音的模型?那就只用数字人:你的服务器发送音频,数字人跟着对口型。 不需要角色,控制台里也什么都不用设置;有一个脸的 id(GET /v1/avatars)就够了。

  1. 你的服务器为一张脸创建会话,要求 Avatar only。
  2. 页面照常加入,不用麦克风。
  3. 你的服务器拿到发送音频的凭据,加入会话的房间,等数字人准备好,然后把每一段话以 PCM 发过去。

你的服务器,用 Node 或 Python:

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 里返回。发送速度至少要跟上实时;更快也可以,会排队。 数字人说完(或丢弃)排队的内容时,会调用你服务器上的 RPC lk.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 和其他会话一样。