@keyframelabs/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # @keyframelabs/sdk
2
+
3
+ Browser SDK for KeyframeLab's Persona avatar sessions. Connect any voice AI agent to a real-time avatar.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @keyframelabs/sdk
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ On your node / python backend, create a session using your KeyframeLabs API key.
14
+
15
+ ```typescript
16
+ const response = await fetch('https://api.keyframelabs.com/v1/session'
17
+ method: 'POST',
18
+ headers: {
19
+ 'Content-Type': 'application/json',
20
+ Authorization: `Bearer ${KFL_API_KEY}`,
21
+ },
22
+ body: JSON.stringify({
23
+ persona_id: "luna", // or cosmo or astro, etc.
24
+ model_id: "persona-1-live"
25
+ }),
26
+ );
27
+
28
+ if (!response.ok) {
29
+ throw new Error(`HTTP error! status: ${response.status}`);
30
+ }
31
+
32
+ // Contains your serverUrl and participantToken
33
+ const result = await response.json();
34
+ ```
35
+
36
+ Then, using the client on the browser:
37
+
38
+ ```typescript
39
+ import { createClient } from '@keyframelabs/sdk';
40
+
41
+ // Create a Persona client
42
+ const persona = createClient({
43
+ serverUrl: "wss://...",
44
+ participantToken: "A6gB...",
45
+ onVideoTrack: (track) => {
46
+ // Some HTML video element
47
+ videoElement.srcObject = new MediaStream([track]);
48
+ },
49
+ onAudioTrack: (track) => {
50
+ // Some HTML audio element
51
+ audioElement.srcObject = new MediaStream([track]);
52
+ },
53
+ });
54
+
55
+ // Connect to the avatar
56
+ await persona.connect();
57
+
58
+ // Send audio from your voice AI agent (24kHz 16-bit PCM)
59
+ persona.sendAudio(pcmAudioBytes);
60
+
61
+ // Signal an interruption (clears pending frames)
62
+ persona.interrupt();
63
+
64
+ // Close when done
65
+ await persona.close();
66
+ ```
67
+
68
+ ## API
69
+
70
+ ### `createClient(options)`
71
+
72
+ Create a new Persona client.
73
+
74
+ **Options:**
75
+ - `personaId` - Persona ID (e.g., 'luna', 'cosmo')
76
+ - `apiUrl` - Central API URL (optional)
77
+ - `apiKey` - API key for authentication (optional)
78
+ - `onVideoTrack` - Callback when video track is available
79
+ - `onAudioTrack` - Callback when audio track is available
80
+ - `onStateChange` - Callback when session state changes
81
+ - `onError` - Callback on error
82
+
83
+ ### `PersonaSession`
84
+
85
+ The client instance returned by `createClient()`.
86
+
87
+ **Methods:**
88
+ - `connect()` - Connect to the avatar session
89
+ - `sendAudio(pcmData)` - Send 24kHz 16-bit PCM audio
90
+ - `interrupt()` - Signal an interruption (clears pending frames)
91
+ - `close()` - Close the session
92
+
93
+ **Properties:**
94
+ - `state` - Current session state ('disconnected', 'connecting', 'connected', 'error')
95
+
96
+ ## Integrating Voice AI Agents
97
+
98
+ The SDK is intentionally minimal - it only handles the avatar connection. You bring your own voice AI agent (Gemini, ElevenLabs, OpenAI, etc.).
99
+
100
+ Example integration pattern:
101
+
102
+ ```typescript
103
+ import { createClient } from '@keyframelabs/sdk';
104
+ // Your agent implementation (copy from experiments/src/agents/)
105
+ import { GeminiLiveAgent } from './agents/gemini-live';
106
+
107
+ const persona = createClient({
108
+ personaId: 'luna',
109
+ onVideoTrack: (track) => {
110
+ videoElement.srcObject = new MediaStream([track]);
111
+ },
112
+ });
113
+
114
+ const agent = new GeminiLiveAgent();
115
+
116
+ // Wire agent audio to persona
117
+ agent.on('audio', (pcmData) => persona.sendAudio(pcmData));
118
+
119
+ // Handle interruptions
120
+ agent.on('interrupted', () => persona.interrupt());
121
+
122
+ // Connect both
123
+ await persona.connect();
124
+ await agent.connect({ apiKey: 'your-gemini-key' });
125
+
126
+ // Send microphone audio to agent
127
+ // (capture and send PCM to agent.sendAudio())
128
+ ```
129
+
130
+ ## Architecture
131
+
132
+ ```
133
+ Browser GPU Node
134
+ ┌─────────────────┐ ┌─────────────────┐
135
+ │ Microphone │──PCM 16kHz──▶│ │
136
+ │ ↓ │ │ │
137
+ │ Voice AI Agent │ │ AvatarSession │
138
+ │ ↓ │ │ │
139
+ │ PersonaSession │──DataStream─▶│ ↓ │
140
+ │ ↑ │ │ Inference │
141
+ │ Video Element │◀──WebRTC────│ ↓ │
142
+ └─────────────────┘ │ Video │
143
+ └─────────────────┘
144
+ ```
145
+
146
+ ## License
147
+
148
+ MIT
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@keyframelabs/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Browser SDK for KeyframeLab's Persona avatar sessions",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "dependencies": {
22
+ "livekit-client": "^2.15.14"
23
+ },
24
+ "devDependencies": {
25
+ "typescript": "^5.9.3"
26
+ },
27
+ "peerDependencies": {
28
+ "@google/genai": "^1.0.0"
29
+ },
30
+ "peerDependenciesMeta": {
31
+ "@google/genai": {
32
+ "optional": true
33
+ }
34
+ },
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "dev": "tsc --watch"
38
+ }
39
+ }
@@ -0,0 +1,324 @@
1
+ /**
2
+ * PersonaSession - Core SDK class for connecting to Persona avatars.
3
+ *
4
+ * Handles:
5
+ * - Creating sessions via Central API
6
+ * - Connecting to LiveKit room as "user" participant
7
+ * - Sending audio to "persona" via DataStream
8
+ * - Receiving video from "persona"
9
+ * - Interrupt signaling
10
+ */
11
+
12
+ import {
13
+ Room,
14
+ RoomEvent,
15
+ Track,
16
+ RemoteTrack,
17
+ RemoteParticipant,
18
+ DisconnectReason,
19
+ } from 'livekit-client';
20
+
21
+ // ByteStreamWriter type from livekit-client (not exported directly)
22
+ type ByteStreamWriter = Awaited<ReturnType<Room['localParticipant']['streamBytes']>>;
23
+ import type {
24
+ CloseReason,
25
+ PersonaSessionOptions,
26
+ SessionState
27
+ } from './types.js';
28
+
29
+ /** Map LiveKit DisconnectReason to our CloseReason */
30
+ function mapDisconnectReason(reason?: DisconnectReason): CloseReason {
31
+ switch (reason) {
32
+ case DisconnectReason.CLIENT_INITIATED:
33
+ return 'client_initiated';
34
+ case DisconnectReason.ROOM_DELETED:
35
+ return 'room_deleted';
36
+ case DisconnectReason.SERVER_SHUTDOWN:
37
+ return 'server_shutdown';
38
+ case DisconnectReason.PARTICIPANT_REMOVED:
39
+ return 'participant_removed';
40
+ default:
41
+ return 'unknown';
42
+ }
43
+ }
44
+
45
+ /** DataStream topic for audio (must match GPU node) */
46
+ const AUDIO_STREAM_TOPIC = 'lk.audio_stream';
47
+
48
+ /** DataStream topic for control messages (interrupt, etc.) */
49
+ const CONTROL_STREAM_TOPIC = 'lk.control';
50
+
51
+ /** Auto-flush timeout in ms after last audio chunk */
52
+ const AUTO_FLUSH_TIMEOUT_MS = 300;
53
+
54
+ /**
55
+ * PersonaSession manages the connection between your app and a Persona avatar.
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * const session = new PersonaSession({
60
+ * // WebRTC URL, access token, and agent identity returned from KeyframeLab API (called from your server code)
61
+ * serverUrl: websocket-url,
62
+ * participantToken: webrtc-participant-token,
63
+ * agentIdentity: agent_identity,
64
+ * onVideoTrack: (track) => {
65
+ * videoElement.srcObject = new MediaStream([track]);
66
+ * },
67
+ * });
68
+ *
69
+ * await session.connect();
70
+ * session.sendAudio(audioBytes);
71
+ * await session.close();
72
+ * ```
73
+ */
74
+ export class PersonaSession {
75
+ private options: Required<Pick<PersonaSessionOptions, 'serverUrl' | 'participantToken' | 'agentIdentity'>> &
76
+ PersonaSessionOptions;
77
+ private room: Room | null = null;
78
+ private _state: SessionState = 'disconnected';
79
+ private byteStreamWriter: ByteStreamWriter | null = null;
80
+
81
+ // Write queue for proper ordering and completion tracking
82
+ private writeQueue: Promise<void> = Promise.resolve();
83
+
84
+ // Auto-flush timer for turn end detection
85
+ private autoFlushTimer: ReturnType<typeof setTimeout> | null = null;
86
+
87
+ constructor(options: PersonaSessionOptions) {
88
+ this.options = { ...options };
89
+ }
90
+
91
+ /** Current session state */
92
+ get state(): SessionState {
93
+ return this._state;
94
+ }
95
+
96
+ private setState(state: SessionState): void {
97
+ if (this._state !== state) {
98
+ this._state = state;
99
+ this.options.onStateChange?.(state);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Connect to a Persona avatar session.
105
+ *
106
+ * Creates a session via Central API and connects to the LiveKit room.
107
+ */
108
+ async connect(): Promise<void> {
109
+ if (this._state !== 'disconnected') {
110
+ throw new Error('Session already connected or connecting');
111
+ }
112
+
113
+ this.setState('connecting');
114
+
115
+ try {
116
+ // 1. Connect to LiveKit room
117
+ this.room = new Room();
118
+ this.setupRoomListeners();
119
+
120
+ await this.room.connect(this.options.serverUrl, this.options.participantToken);
121
+
122
+ // Open ByteStream for audio
123
+ this.byteStreamWriter = await this.room.localParticipant.streamBytes({
124
+ topic: AUDIO_STREAM_TOPIC,
125
+ destinationIdentities: [this.options.agentIdentity],
126
+ });
127
+ console.log('[PersonaSession] Opened audio stream');
128
+
129
+ this.setState('connected');
130
+ console.log('[PersonaSession] Connected to room');
131
+ } catch (error) {
132
+ this.setState('error');
133
+ this.options.onError?.(error instanceof Error ? error : new Error(String(error)));
134
+ throw error;
135
+ }
136
+ }
137
+
138
+ private setupRoomListeners(): void {
139
+ if (!this.room) return;
140
+
141
+ // Handle tracks from persona
142
+ this.room.on(RoomEvent.TrackSubscribed, (track: RemoteTrack, _pub, participant: RemoteParticipant) => {
143
+ if (participant.identity !== this.options.agentIdentity) return;
144
+
145
+ const mediaTrack = track.mediaStreamTrack;
146
+
147
+ if (track.kind === Track.Kind.Video) {
148
+ console.log('[PersonaSession] Got video track from persona');
149
+ this.options.onVideoTrack?.(mediaTrack);
150
+ } else if (track.kind === Track.Kind.Audio) {
151
+ console.log('[PersonaSession] Got audio track from persona');
152
+ this.options.onAudioTrack?.(mediaTrack);
153
+ }
154
+ });
155
+
156
+ // Handle disconnection
157
+ this.room.on(RoomEvent.Disconnected, (reason?: DisconnectReason) => {
158
+ const closeReason = mapDisconnectReason(reason);
159
+ console.log('[PersonaSession] Disconnected from room:', closeReason);
160
+ this.setState('disconnected');
161
+ this.options.onClose?.(closeReason);
162
+ });
163
+
164
+ // Handle errors
165
+ this.room.on(RoomEvent.MediaDevicesError, (error: Error) => {
166
+ console.error('[PersonaSession] Media devices error:', error);
167
+ this.options.onError?.(error);
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Send audio data to the persona for video synthesis.
173
+ *
174
+ * @param pcmData - 16-bit PCM audio at 24kHz
175
+ */
176
+ sendAudio(pcmData: Uint8Array): void {
177
+ if (!this.room || this._state !== 'connected') {
178
+ console.warn('[PersonaSession] sendAudio dropped - not connected');
179
+ return;
180
+ }
181
+
182
+ // Chain writes to ensure ordering and track completion
183
+ // Lazily create ByteStreamWriter if needed (e.g., after endAudioTurn closed it)
184
+ console.log(`[PersonaSession] Writing ${pcmData.length} bytes to ByteStream`);
185
+ this.writeQueue = this.writeQueue
186
+ .then(async () => {
187
+ if (!this.byteStreamWriter && this.room) {
188
+ this.byteStreamWriter = await this.room.localParticipant.streamBytes({
189
+ topic: AUDIO_STREAM_TOPIC,
190
+ destinationIdentities: [this.options.agentIdentity],
191
+ });
192
+ }
193
+ await this.byteStreamWriter?.write(pcmData);
194
+ })
195
+ .catch((err) => {
196
+ console.warn('[PersonaSession] Failed to write audio:', err);
197
+ });
198
+
199
+ // Schedule auto-flush after timeout (reset on each audio chunk)
200
+ this.scheduleAutoFlush();
201
+ }
202
+
203
+ /**
204
+ * Schedule auto-flush after a timeout.
205
+ * Called on each sendAudio() to reset the timer.
206
+ */
207
+ private scheduleAutoFlush(): void {
208
+ if (this.autoFlushTimer) {
209
+ clearTimeout(this.autoFlushTimer);
210
+ }
211
+
212
+ this.autoFlushTimer = setTimeout(() => {
213
+ if (this.byteStreamWriter) {
214
+ console.log('[PersonaSession] Auto-flush: no audio for', AUTO_FLUSH_TIMEOUT_MS, 'ms');
215
+ this.endAudioTurn();
216
+ }
217
+ }, AUTO_FLUSH_TIMEOUT_MS);
218
+ }
219
+
220
+ /**
221
+ * Flush all pending audio writes.
222
+ * Call before close to ensure all audio is sent.
223
+ */
224
+ async flush(): Promise<void> {
225
+ await this.writeQueue;
226
+ }
227
+
228
+ /**
229
+ * End the current audio turn and flush any buffered data.
230
+ *
231
+ * This is called automatically 300ms after the last sendAudio() call.
232
+ * You typically don't need to call this manually, but it's available
233
+ * if you need immediate flushing.
234
+ */
235
+ async endAudioTurn(): Promise<void> {
236
+ // Clear auto-flush timer to prevent double-flush
237
+ if (this.autoFlushTimer) {
238
+ clearTimeout(this.autoFlushTimer);
239
+ this.autoFlushTimer = null;
240
+ }
241
+
242
+ console.debug('[PersonaSession] endAudioTurn() called, awaiting writeQueue...');
243
+ await this.writeQueue;
244
+ console.debug('[PersonaSession] writeQueue complete, closing stream...');
245
+ if (this.byteStreamWriter) {
246
+ try {
247
+ await this.byteStreamWriter.close();
248
+ console.debug('[PersonaSession] stream closed successfully');
249
+ } catch (err) {
250
+ console.warn('[PersonaSession] stream close error:', err);
251
+ }
252
+ this.byteStreamWriter = null;
253
+ } else {
254
+ console.debug('[PersonaSession] no stream to close');
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Signal an interruption to the avatar.
260
+ *
261
+ * Clears any pending frames in the GPU node's publish queue,
262
+ * allowing the avatar to quickly respond to new audio.
263
+ *
264
+ * Call this when the user interrupts the agent (e.g., starts speaking
265
+ * while the agent is still talking).
266
+ */
267
+ async interrupt(): Promise<void> {
268
+ // Clear auto-flush timer - we're interrupting, don't need to flush
269
+ if (this.autoFlushTimer) {
270
+ clearTimeout(this.autoFlushTimer);
271
+ this.autoFlushTimer = null;
272
+ }
273
+
274
+ if (!this.room || this._state !== 'connected') {
275
+ console.warn('[PersonaSession] interrupt() called but not connected');
276
+ return;
277
+ }
278
+
279
+ try {
280
+ console.debug('[PersonaSession] Sending interrupt');
281
+ const writer = await this.room.localParticipant.streamBytes({
282
+ topic: CONTROL_STREAM_TOPIC,
283
+ destinationIdentities: [this.options.agentIdentity],
284
+ });
285
+ await writer.write(new TextEncoder().encode('interrupt'));
286
+ await writer.close();
287
+ } catch (error) {
288
+ console.warn('[PersonaSession] Failed to send interrupt:', error);
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Close the session and clean up resources.
294
+ */
295
+ async close(): Promise<void> {
296
+ // Clear auto-flush timer
297
+ if (this.autoFlushTimer) {
298
+ clearTimeout(this.autoFlushTimer);
299
+ this.autoFlushTimer = null;
300
+ }
301
+
302
+ // Wait for pending writes to complete before closing
303
+ await this.flush();
304
+
305
+ // Close byte stream
306
+ if (this.byteStreamWriter) {
307
+ try {
308
+ await this.byteStreamWriter.close();
309
+ } catch {
310
+ // Ignore close errors
311
+ }
312
+ this.byteStreamWriter = null;
313
+ }
314
+
315
+ // Disconnect from room
316
+ if (this.room) {
317
+ this.room.disconnect();
318
+ this.room = null;
319
+ }
320
+
321
+ this.setState('disconnected');
322
+ }
323
+ }
324
+
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Audio utilities for PCM processing and silence generation.
3
+ */
4
+
5
+ /** Sample rate for audio sent to Persona */
6
+ export const SAMPLE_RATE = 24000;
7
+
8
+ /** Bytes per sample (16-bit PCM) */
9
+ export const BYTES_PER_SAMPLE = 2;
10
+
11
+ /** Number of channels (mono) */
12
+ export const CHANNELS = 1;
13
+
14
+ /**
15
+ * Convert base64-encoded audio to Uint8Array.
16
+ *
17
+ * @param base64 - Base64 encoded audio data
18
+ * @returns Uint8Array of audio bytes
19
+ */
20
+ export function base64ToBytes(base64: string): Uint8Array {
21
+ const binaryString = atob(base64);
22
+ const bytes = new Uint8Array(binaryString.length);
23
+ for (let i = 0; i < binaryString.length; i++) {
24
+ bytes[i] = binaryString.charCodeAt(i);
25
+ }
26
+ return bytes;
27
+ }
28
+
29
+ /**
30
+ * Convert Uint8Array to base64 string.
31
+ *
32
+ * @param bytes - Audio bytes
33
+ * @returns Base64 encoded string
34
+ */
35
+ export function bytesToBase64(bytes: Uint8Array): string {
36
+ let binary = '';
37
+ for (let i = 0; i < bytes.length; i++) {
38
+ binary += String.fromCharCode(bytes[i]);
39
+ }
40
+ return btoa(binary);
41
+ }
42
+
43
+ /**
44
+ * Resample PCM audio from one sample rate to another.
45
+ * Simple linear interpolation - not high quality but sufficient for real-time.
46
+ *
47
+ * @param input - Input PCM bytes (16-bit signed)
48
+ * @param fromRate - Source sample rate
49
+ * @param toRate - Target sample rate
50
+ * @returns Resampled PCM bytes
51
+ */
52
+ export function resamplePcm(
53
+ input: Uint8Array,
54
+ fromRate: number,
55
+ toRate: number
56
+ ): Uint8Array {
57
+ if (fromRate === toRate) {
58
+ return input;
59
+ }
60
+
61
+ const inputView = new Int16Array(input.buffer, input.byteOffset, input.length / 2);
62
+ const ratio = fromRate / toRate;
63
+ const outputLength = Math.floor(inputView.length / ratio);
64
+ const output = new Int16Array(outputLength);
65
+
66
+ for (let i = 0; i < outputLength; i++) {
67
+ const srcIndex = i * ratio;
68
+ const srcIndexFloor = Math.floor(srcIndex);
69
+ const srcIndexCeil = Math.min(srcIndexFloor + 1, inputView.length - 1);
70
+ const fraction = srcIndex - srcIndexFloor;
71
+
72
+ // Linear interpolation
73
+ output[i] = Math.round(
74
+ inputView[srcIndexFloor] * (1 - fraction) + inputView[srcIndexCeil] * fraction
75
+ );
76
+ }
77
+
78
+ return new Uint8Array(output.buffer);
79
+ }
80
+
81
+ /**
82
+ * Create a simple typed event emitter.
83
+ */
84
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
+ export function createEventEmitter<T extends Record<string, any>>() {
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
+ const listeners = new Map<keyof T, Set<(data: any) => void>>();
88
+
89
+ return {
90
+ on<K extends keyof T>(event: K, handler: (data: T[K]) => void): void {
91
+ if (!listeners.has(event)) {
92
+ listeners.set(event, new Set());
93
+ }
94
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
+ listeners.get(event)!.add(handler as (data: any) => void);
96
+ },
97
+
98
+ off<K extends keyof T>(event: K, handler: (data: T[K]) => void): void {
99
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
+ listeners.get(event)?.delete(handler as (data: any) => void);
101
+ },
102
+
103
+ emit<K extends keyof T>(event: K, data: T[K]): void {
104
+ listeners.get(event)?.forEach((handler) => handler(data));
105
+ },
106
+
107
+ removeAllListeners(): void {
108
+ listeners.clear();
109
+ },
110
+ };
111
+ }
112
+
package/src/index.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @keyframelabs/sdk - Browser SDK for KeyframeLab's Persona avatar sessions.
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { createClient } from '@keyframelabs/sdk';
7
+ *
8
+ * const client = createClient({
9
+ * personaId: 'luna',
10
+ * // WebRTC URL and access token returned from KeyframeLab API (called from your server code)
11
+ * serverUrl: 'wss://webrtc-server-url.com',
12
+ * participantToken: 'webrtc-participant-token',
13
+ * onVideoTrack: (track) => {
14
+ * videoElement.srcObject = new MediaStream([track]);
15
+ * },
16
+ * });
17
+ *
18
+ * await client.connect();
19
+ * client.sendAudio(audioBytes);
20
+ * await client.close();
21
+ * ```
22
+ */
23
+
24
+ import { PersonaSession } from './PersonaSession.js';
25
+ import type { PersonaSessionOptions } from './types.js';
26
+
27
+ /**
28
+ * Create a new Persona client.
29
+ *
30
+ * @param options - Session configuration options
31
+ * @returns A new PersonaSession instance
32
+ */
33
+ const createClient = (options: PersonaSessionOptions): PersonaSession => {
34
+ return new PersonaSession(options);
35
+ };
36
+
37
+ // Core
38
+ export { createClient, PersonaSession };
39
+
40
+ // Types
41
+ export type {
42
+ CloseReason,
43
+ PersonaSessionOptions,
44
+ SessionState,
45
+ } from './types.js';
46
+
47
+ // Utilities
48
+ export {
49
+ SAMPLE_RATE,
50
+ base64ToBytes,
51
+ bytesToBase64,
52
+ createEventEmitter,
53
+ resamplePcm,
54
+ } from './audio-utils.js';
package/src/types.ts ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Shared types for Persona SDK.
3
+ */
4
+
5
+ /** Session state machine */
6
+ export type SessionState =
7
+ | 'disconnected'
8
+ | 'connecting'
9
+ | 'connected'
10
+ | 'error';
11
+
12
+ /** Reason for session close */
13
+ export type CloseReason =
14
+ | 'client_initiated' // User called close()
15
+ | 'room_deleted' // Backend deleted the room (max duration, etc.)
16
+ | 'server_shutdown' // Server is shutting down
17
+ | 'participant_removed' // Participant was kicked
18
+ | 'unknown'; // Unknown reason
19
+
20
+ /** Options for creating a PersonaSession */
21
+ export interface PersonaSessionOptions {
22
+ /** WebRTC URL (returned from KeyframeLab API) */
23
+ serverUrl: string;
24
+ /** WebRTC access token (returned from KeyframeLab API) */
25
+ participantToken: string;
26
+ /** Identity of the agent participant */
27
+ agentIdentity: string;
28
+
29
+ /** Called when video track is available */
30
+ onVideoTrack?: (track: MediaStreamTrack) => void;
31
+
32
+ /** Called when audio track is available */
33
+ onAudioTrack?: (track: MediaStreamTrack) => void;
34
+
35
+ /** Called when session state changes */
36
+ onStateChange?: (state: SessionState) => void;
37
+
38
+ /** Called on error */
39
+ onError?: (error: Error) => void;
40
+
41
+ /** Called when session is closed (by server or client) */
42
+ onClose?: (reason: CloseReason) => void;
43
+ }