Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/liveavatar-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@livekit/agents-plugin-liveavatar': minor
Comment thread
toubatbrian marked this conversation as resolved.
Outdated
'@livekit/agents': minor
Comment thread
toubatbrian marked this conversation as resolved.
Outdated
---

Port the `liveavatar` plugin from the Python `livekit-agents` repo, including the new `videoQuality` parameter from livekit/agents#5552.

The new `@livekit/agents-plugin-liveavatar` package adds a LiveAvatar `AvatarSession` that mirrors the Python plugin: it brings up a LiveAvatar streaming session, opens the realtime websocket, captures the agent's audio output through a queue-based `AudioOutput`, resamples to 24 kHz mono, and forwards base64-encoded chunks (~600 ms first chunk, ~1 s subsequent) to the LiveAvatar service. Inbound websocket events drive playback start/finish notifications back into the `AgentSession`.

Also exports `voice.AudioOutput` (and its companion `AudioOutputCapabilities` / `PlaybackFinishedEvent` / `PlaybackStartedEvent` types) from `@livekit/agents` so plugin authors can subclass the abstract audio sink.
8 changes: 7 additions & 1 deletion agents/src/voice/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ export {
RoomSessionTransport,
} from './remote_session.js';
export * from './events.js';
export { type TimedString } from './io.js';
export {
AudioOutput,
type AudioOutputCapabilities,
type PlaybackFinishedEvent,
type PlaybackStartedEvent,
type TimedString,
} from './io.js';
export * from './report.js';
export * from './room_io/index.js';
export { RunContext } from './run_context.js';
Expand Down
81 changes: 81 additions & 0 deletions plugins/liveavatar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<!--
SPDX-FileCopyrightText: 2026 LiveKit, Inc.

SPDX-License-Identifier: Apache-2.0
-->

# LiveAvatar plugin for LiveKit Agents

Support for [LiveAvatar](https://www.liveavatar.com) interactive avatars.

This is the JS/TS port of the Python `livekit-plugins-liveavatar` plugin. See [https://docs.livekit.io/agents/integrations/avatar/](https://docs.livekit.io/agents/integrations/avatar/) for more information.
Comment thread
toubatbrian marked this conversation as resolved.
Outdated

## Installation

```bash
npm install @livekit/agents-plugin-liveavatar
```

or

```bash
pnpm add @livekit/agents-plugin-liveavatar
```

## Pre-requisites

Create a developer API key from the LiveAvatar dashboard and set the `LIVEAVATAR_API_KEY` environment variable with it:

```bash
export LIVEAVATAR_API_KEY=<your-liveavatar-api-key>
```

## Usage

```typescript
import { AvatarSession } from '@livekit/agents-plugin-liveavatar';
import { AgentSession } from '@livekit/agents';

const avatarSession = new AvatarSession({
avatarId: 'your-avatar-id', // or via LIVEAVATAR_AVATAR_ID
apiKey: process.env.LIVEAVATAR_API_KEY,
videoQuality: 'high', // optional: 'very_high' | 'high' | 'medium' | 'low'
});

await avatarSession.start(agentSession, room, {
livekitUrl: process.env.LIVEKIT_URL,
livekitApiKey: process.env.LIVEKIT_API_KEY,
livekitApiSecret: process.env.LIVEKIT_API_SECRET,
});
```

## API

### `AvatarSession`

#### Constructor Options

- `avatarId?: string` — The LiveAvatar avatar id. Falls back to `LIVEAVATAR_AVATAR_ID`.
- `apiUrl?: string` — Override the LiveAvatar API base URL.
- `apiKey?: string` — Your LiveAvatar API key. Falls back to `LIVEAVATAR_API_KEY`.
- `isSandbox?: boolean` — Use the LiveAvatar sandbox (1 minute connection limit). Defaults to `false`.
- `videoQuality?: 'very_high' | 'high' | 'medium' | 'low'` — Avatar video quality requested from the service. When omitted, the LiveAvatar service decides.
- `avatarParticipantIdentity?: string` — Identity for the avatar participant. Defaults to `'liveavatar-avatar-agent'`.
- `avatarParticipantName?: string` — Display name for the avatar participant. Defaults to `'liveavatar-avatar-agent'`.
- `connOptions?: APIConnectOptions` — API retry/timeout options.

#### Methods

##### `start(agentSession, room, options?)`

Starts the avatar session, brings up a LiveAvatar streaming session, opens the realtime websocket, and routes the agent's audio output through to the avatar.

**StartOptions:**

- `livekitUrl?: string` — Falls back to `LIVEKIT_URL`.
- `livekitApiKey?: string` — Falls back to `LIVEKIT_API_KEY`.
- `livekitApiSecret?: string` — Falls back to `LIVEKIT_API_SECRET`.

## License

Apache 2.0
53 changes: 53 additions & 0 deletions plugins/liveavatar/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "@livekit/agents-plugin-liveavatar",
"version": "1.3.0",
"description": "LiveAvatar avatar plugin for LiveKit Node Agents",
"main": "dist/index.js",
"require": "dist/index.cjs",
"types": "dist/index.d.ts",
"exports": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"author": "LiveKit",
"type": "module",
"repository": "git@github.com:livekit/agents-js.git",
"license": "Apache-2.0",
"files": [
"dist",
"src",
"README.md"
],
"scripts": {
"build": "tsup --onSuccess \"pnpm build:types\"",
"build:types": "tsc --declaration --emitDeclarationOnly && node ../../scripts/copyDeclarationOutput.js",
"clean": "rm -rf dist",
"clean:build": "pnpm clean && pnpm build",
"lint": "eslint -f unix \"src/**/*.{ts,js}\"",
"api:check": "api-extractor run --typescript-compiler-folder ../../node_modules/typescript",
"api:update": "api-extractor run --local --typescript-compiler-folder ../../node_modules/typescript --verbose"
},
"devDependencies": {
"@livekit/agents": "workspace:*",
"@livekit/rtc-node": "catalog:",
"@microsoft/api-extractor": "^7.35.0",
"@types/ws": "catalog:",
"pino": "^8.19.0",
"tsup": "^8.3.5",
"typescript": "^5.0.0"
},
"dependencies": {
"livekit-server-sdk": "^2.13.3",
"ws": "catalog:"
},
"peerDependencies": {
"@livekit/agents": "workspace:*",
"@livekit/rtc-node": "catalog:"
}
}
202 changes: 202 additions & 0 deletions plugins/liveavatar/src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// SPDX-FileCopyrightText: 2026 LiveKit, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import {
type APIConnectOptions,
APIConnectionError,
APIStatusError,
DEFAULT_API_CONNECT_OPTIONS,
} from '@livekit/agents';
import { log } from './log.js';

export const DEFAULT_API_URL = 'https://api.liveavatar.com/v1/sessions';

// Ref: python livekit-plugins/livekit-plugins-liveavatar/livekit/plugins/liveavatar/avatar.py - 41 line
export type VideoQuality = 'very_high' | 'high' | 'medium' | 'low';

/**
* Exception thrown when the LiveAvatar plugin or the LiveAvatar service errors.
*/
export class LiveAvatarException extends Error {
constructor(message: string) {
super(message);
this.name = 'LiveAvatarException';
}
}

export interface CreateStreamingSessionOptions {
livekitUrl: string;
livekitToken: string;
roomName: string;
avatarId: string;
isSandbox?: boolean;
// Ref: python livekit-plugins/livekit-plugins-liveavatar/livekit/plugins/liveavatar/api.py - 62 line
videoQuality?: VideoQuality | null;
}

export interface SessionResponse {
data: {
session_id: string;
session_token: string;
[key: string]: unknown;
};
code: number;
[key: string]: unknown;
}

export interface StartSessionResponse {
data: {
ws_url: string;
[key: string]: unknown;
};
code: number;
[key: string]: unknown;
}

export interface StopSessionResponse {
data: Record<string, unknown>;
code: number;
[key: string]: unknown;
}

export interface LiveAvatarAPIOptions {
apiKey?: string;
apiUrl?: string;
connOptions?: APIConnectOptions;
}

/**
* Thin client for the LiveAvatar HTTP API.
*
* Mirrors `livekit-plugins/livekit-plugins-liveavatar/livekit/plugins/liveavatar/api.py`.
*/
export class LiveAvatarAPI {
private apiKey: string;
private apiUrl: string;
private connOptions: APIConnectOptions;

#logger = log();

constructor(options: LiveAvatarAPIOptions = {}) {
const apiKey = options.apiKey ?? process.env.LIVEAVATAR_API_KEY ?? '';
if (!apiKey) {
throw new LiveAvatarException('api_key or LIVEAVATAR_API_KEY must be set');
}
this.apiKey = apiKey;
this.apiUrl = options.apiUrl || DEFAULT_API_URL;
this.connOptions = options.connOptions || DEFAULT_API_CONNECT_OPTIONS;
}

/**
* Create a new streaming session, returning the session id and session token.
*
* Ref: python livekit-plugins/livekit-plugins-liveavatar/livekit/plugins/liveavatar/api.py - 51-87 lines
*/
async createStreamingSession(opts: CreateStreamingSessionOptions): Promise<SessionResponse> {
const livekitConfig = {
livekit_room: opts.roomName,
livekit_url: opts.livekitUrl,
livekit_client_token: opts.livekitToken,
};

const payload: Record<string, unknown> = {
mode: 'LITE',
avatar_id: opts.avatarId,
is_sandbox: opts.isSandbox ?? false,
livekit_config: livekitConfig,
};

// Ref: python livekit-plugins/livekit-plugins-liveavatar/livekit/plugins/liveavatar/api.py - 75-76 lines
if (opts.videoQuality != null) {
payload.video_quality = opts.videoQuality;
}

const headers = {
accept: 'application/json',
'content-type': 'application/json',
'X-API-KEY': this.apiKey,
};
return (await this.post('/token', payload, headers)) as SessionResponse;
}

/**
* Start a previously created streaming session.
*
* Ref: python livekit-plugins/livekit-plugins-liveavatar/livekit/plugins/liveavatar/api.py - 92-97 lines
*/
async startStreamingSession(
sessionId: string,
sessionToken: string,
): Promise<StartSessionResponse> {
const payload = { session_id: sessionId };
const headers = {
'content-type': 'application/json',
Authorization: `Bearer ${sessionToken}`,
};
return (await this.post('/start', payload, headers)) as StartSessionResponse;
}

/**
* Stop a running streaming session.
*
* Ref: python livekit-plugins/livekit-plugins-liveavatar/livekit/plugins/liveavatar/api.py - 99-107 lines
*/
async stopStreamingSession(
sessionId: string,
sessionToken: string,
): Promise<StopSessionResponse> {
const payload = { session_id: sessionId, reason: 'USER_DISCONNECTED' };
const headers = {
'content-type': 'application/json',
Authorization: `Bearer ${sessionToken}`,
};
return (await this.post('/stop', payload, headers)) as StopSessionResponse;
}

/**
* POST helper with the same retry/backoff semantics as the Python plugin.
*
* Ref: python livekit-plugins/livekit-plugins-liveavatar/livekit/plugins/liveavatar/api.py - 109-138 lines
*/
private async post(
endpoint: string,
payload: Record<string, unknown>,
headers: Record<string, string>,
): Promise<unknown> {
const url = this.apiUrl + endpoint;
const maxRetry = this.connOptions.maxRetry;
for (let i = 0; i < maxRetry; i++) {
Comment thread
toubatbrian marked this conversation as resolved.
Outdated
try {
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(payload),
signal: AbortSignal.timeout(this.connOptions.timeoutMs),
});
if (!response.ok) {
const text = await response.text();
throw new APIStatusError({
message: `Server returned an error for ${url}: ${response.status}`,
options: { statusCode: response.status, body: { error: text } },
});
}
return await response.json();
} catch (e) {
if (e instanceof APIStatusError && !e.retryable) {
throw e;
}
this.#logger.warn(
{ error: String(e), url, attempt: i },
`API request to ${url} failed on attempt ${i}`,
);
}

if (i < maxRetry - 1) {
await new Promise((resolve) => setTimeout(resolve, this.connOptions.retryIntervalMs));
}
}
throw new APIConnectionError({
message: `Failed to call LiveAvatar API after ${maxRetry} retries`,
});
}
}
Loading
Loading