glove-voice-livekit
v0.1.1
Published
LiveKit adapters for the Glove voice stack — room transport for realtime agents, and avatars that join the room as participants
Maintainers
Readme
glove-voice-livekit
LiveKit as an adapter in the Glove voice stack — two halves that share one room connection:
LiveKitTransport— the room leg every LiveKit-backed voice host otherwise hand-rolls: join, publish the agent's voice as a paced WebRTC track, feed remote mic tracks back out as PCM events, carry JSON on the data channel. Barge-in is server-authoritative:clear()flushes the outboundAudioSourcequeue, so there is no client playback buffer to chase.attachRealtime(rt, transport)binds it to aglove-voice-s2sRealtimeAgentin one call.LiveKit avatars —
TavusLiveKitAvatarandAnamLiveKitAvatarimplement theglove-voice-avatarAvatarAdaptercontract (and pass its conformance suite), so a face over LiveKit is interchangeable with the Daily-based Tavus echo adapter:attachAvatar(rt, avatar)and done. Under the hood they speak LiveKit's published avatar protocol — the provider's worker joins your room as a second participant (token kindagent,lk.publish_on_behalfpointing at your agent) and publishes synchronized voice+face itself; agent PCM reaches it over thelk.audio_streambyte stream, and barge-in is thelk.clear_bufferRPC. A glove agent is indistinguishable from a LiveKit Agents worker as far as the avatar can tell.
Voice only
import { LiveKitTransport, attachRealtime, mintParticipantToken } from "glove-voice-livekit";
const transport = new LiveKitTransport({
url: process.env.LIVEKIT_URL!,
token: await mintParticipantToken(
{ apiKey: process.env.LIVEKIT_API_KEY!, apiSecret: process.env.LIVEKIT_API_SECRET! },
{ roomName: "call-42", identity: "agent" },
),
});
await transport.connect();
attachRealtime(rt, transport); // mics → model, model → track, interrupt → flush
await rt.start();With a face
import {
TavusLiveKitAvatar, TAVUS_AVATAR_IDENTITY, mintAvatarToken,
} from "glove-voice-livekit";
import { attachAvatar } from "glove-voice-avatar";
// The avatar publishes the voice on the agent's behalf — don't double it.
const transport = new LiveKitTransport({ url, token, publishAgentAudio: false });
await transport.connect();
attachRealtime(rt, transport, { agentAudio: false });
const avatar = new TavusLiveKitAvatar({
apiKey: process.env.TAVUS_API_KEY!,
faceId: process.env.TAVUS_FACE_ID!, // minimal echo PAL ensured automatically
livekitUrl: url,
avatarToken: await mintAvatarToken(creds, {
roomName: "call-42",
identity: TAVUS_AVATAR_IDENTITY,
onBehalfOf: "agent",
}),
wire: transport.avatarWire(TAVUS_AVATAR_IDENTITY),
});
await attachAvatar(rt, avatar); // connects + bridges speech/end/interruptAnamLiveKitAvatar is the same shape (avatarId instead of faceId,
ANAM_AVATAR_IDENTITY); written against Anam's documented API and the
conformance suite, live verification pending a key
(#71).
Session renewal: providers can end an avatar session under you — Anam's
plan cap force-ends conversations at 3/5/10 minutes below Growth tier. The
worker leaving the room (participant_disconnected on its identity) is the
signal; disconnect() + connect() on the adapter mints a fresh session
into the same room. examples/livekit-rooms does this automatically with a
debounce.
Browsers see the avatar as an ordinary room participant: attach its video track and you have the face — no provider SDK on the client.
See examples/livekit-rooms for the full
layered setup (front agent + mesh worker + station rooms) on this transport,
with the avatar as an env toggle.
Status
Wire protocol validated against LiveKit Agents' published plugins and
@livekit/rtc-node type declarations; conformance + unit tests green
(pnpm test). Live verification tracked on
#72.
