@keyframelabs/sdk 0.1.0 → 0.1.1

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.
@@ -0,0 +1,89 @@
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
+ import type { PersonaSessionOptions, SessionState } from './types.js';
12
+ /**
13
+ * PersonaSession manages the connection between your app and a Persona avatar.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const session = new PersonaSession({
18
+ * // WebRTC URL, access token, and agent identity returned from KeyframeLab API (called from your server code)
19
+ * serverUrl: websocket-url,
20
+ * participantToken: webrtc-participant-token,
21
+ * agentIdentity: agent_identity,
22
+ * onVideoTrack: (track) => {
23
+ * videoElement.srcObject = new MediaStream([track]);
24
+ * },
25
+ * });
26
+ *
27
+ * await session.connect();
28
+ * session.sendAudio(audioBytes);
29
+ * await session.close();
30
+ * ```
31
+ */
32
+ export declare class PersonaSession {
33
+ private options;
34
+ private room;
35
+ private _state;
36
+ private byteStreamWriter;
37
+ private writeQueue;
38
+ private autoFlushTimer;
39
+ constructor(options: PersonaSessionOptions);
40
+ /** Current session state */
41
+ get state(): SessionState;
42
+ private setState;
43
+ /**
44
+ * Connect to a Persona avatar session.
45
+ *
46
+ * Creates a session via Central API and connects to the LiveKit room.
47
+ */
48
+ connect(): Promise<void>;
49
+ private setupRoomListeners;
50
+ /**
51
+ * Send audio data to the persona for video synthesis.
52
+ *
53
+ * @param pcmData - 16-bit PCM audio at 24kHz
54
+ */
55
+ sendAudio(pcmData: Uint8Array): void;
56
+ /**
57
+ * Schedule auto-flush after a timeout.
58
+ * Called on each sendAudio() to reset the timer.
59
+ */
60
+ private scheduleAutoFlush;
61
+ /**
62
+ * Flush all pending audio writes.
63
+ * Call before close to ensure all audio is sent.
64
+ */
65
+ flush(): Promise<void>;
66
+ /**
67
+ * End the current audio turn and flush any buffered data.
68
+ *
69
+ * This is called automatically 300ms after the last sendAudio() call.
70
+ * You typically don't need to call this manually, but it's available
71
+ * if you need immediate flushing.
72
+ */
73
+ endAudioTurn(): Promise<void>;
74
+ /**
75
+ * Signal an interruption to the avatar.
76
+ *
77
+ * Clears any pending frames in the GPU node's publish queue,
78
+ * allowing the avatar to quickly respond to new audio.
79
+ *
80
+ * Call this when the user interrupts the agent (e.g., starts speaking
81
+ * while the agent is still talking).
82
+ */
83
+ interrupt(): Promise<void>;
84
+ /**
85
+ * Close the session and clean up resources.
86
+ */
87
+ close(): Promise<void>;
88
+ }
89
+ //# sourceMappingURL=PersonaSession.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PersonaSession.d.ts","sourceRoot":"","sources":["../src/PersonaSession.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAaH,OAAO,KAAK,EAEV,qBAAqB,EACrB,YAAY,EACb,MAAM,YAAY,CAAC;AA2BpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,OAAO,CACS;IACxB,OAAO,CAAC,IAAI,CAAqB;IACjC,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,gBAAgB,CAAiC;IAGzD,OAAO,CAAC,UAAU,CAAoC;IAGtD,OAAO,CAAC,cAAc,CAA8C;gBAExD,OAAO,EAAE,qBAAqB;IAI1C,4BAA4B;IAC5B,IAAI,KAAK,IAAI,YAAY,CAExB;IAED,OAAO,CAAC,QAAQ;IAOhB;;;;OAIG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA8B9B,OAAO,CAAC,kBAAkB;IAiC1B;;;;OAIG;IACH,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IA2BpC;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAazB;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B;;;;;;OAMG;IACG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAuBnC;;;;;;;;OAQG;IACG,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAyBhC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CA4B7B"}
@@ -0,0 +1,277 @@
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
+ import { Room, RoomEvent, Track, DisconnectReason, } from 'livekit-client';
12
+ /** Map LiveKit DisconnectReason to our CloseReason */
13
+ function mapDisconnectReason(reason) {
14
+ switch (reason) {
15
+ case DisconnectReason.CLIENT_INITIATED:
16
+ return 'client_initiated';
17
+ case DisconnectReason.ROOM_DELETED:
18
+ return 'room_deleted';
19
+ case DisconnectReason.SERVER_SHUTDOWN:
20
+ return 'server_shutdown';
21
+ case DisconnectReason.PARTICIPANT_REMOVED:
22
+ return 'participant_removed';
23
+ default:
24
+ return 'unknown';
25
+ }
26
+ }
27
+ /** DataStream topic for audio (must match GPU node) */
28
+ const AUDIO_STREAM_TOPIC = 'lk.audio_stream';
29
+ /** DataStream topic for control messages (interrupt, etc.) */
30
+ const CONTROL_STREAM_TOPIC = 'lk.control';
31
+ /** Auto-flush timeout in ms after last audio chunk */
32
+ const AUTO_FLUSH_TIMEOUT_MS = 300;
33
+ /**
34
+ * PersonaSession manages the connection between your app and a Persona avatar.
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const session = new PersonaSession({
39
+ * // WebRTC URL, access token, and agent identity returned from KeyframeLab API (called from your server code)
40
+ * serverUrl: websocket-url,
41
+ * participantToken: webrtc-participant-token,
42
+ * agentIdentity: agent_identity,
43
+ * onVideoTrack: (track) => {
44
+ * videoElement.srcObject = new MediaStream([track]);
45
+ * },
46
+ * });
47
+ *
48
+ * await session.connect();
49
+ * session.sendAudio(audioBytes);
50
+ * await session.close();
51
+ * ```
52
+ */
53
+ export class PersonaSession {
54
+ options;
55
+ room = null;
56
+ _state = 'disconnected';
57
+ byteStreamWriter = null;
58
+ // Write queue for proper ordering and completion tracking
59
+ writeQueue = Promise.resolve();
60
+ // Auto-flush timer for turn end detection
61
+ autoFlushTimer = null;
62
+ constructor(options) {
63
+ this.options = { ...options };
64
+ }
65
+ /** Current session state */
66
+ get state() {
67
+ return this._state;
68
+ }
69
+ setState(state) {
70
+ if (this._state !== state) {
71
+ this._state = state;
72
+ this.options.onStateChange?.(state);
73
+ }
74
+ }
75
+ /**
76
+ * Connect to a Persona avatar session.
77
+ *
78
+ * Creates a session via Central API and connects to the LiveKit room.
79
+ */
80
+ async connect() {
81
+ if (this._state !== 'disconnected') {
82
+ throw new Error('Session already connected or connecting');
83
+ }
84
+ this.setState('connecting');
85
+ try {
86
+ // 1. Connect to LiveKit room
87
+ this.room = new Room();
88
+ this.setupRoomListeners();
89
+ await this.room.connect(this.options.serverUrl, this.options.participantToken);
90
+ // Open ByteStream for audio
91
+ this.byteStreamWriter = await this.room.localParticipant.streamBytes({
92
+ topic: AUDIO_STREAM_TOPIC,
93
+ destinationIdentities: [this.options.agentIdentity],
94
+ });
95
+ console.log('[PersonaSession] Opened audio stream');
96
+ this.setState('connected');
97
+ console.log('[PersonaSession] Connected to room');
98
+ }
99
+ catch (error) {
100
+ this.setState('error');
101
+ this.options.onError?.(error instanceof Error ? error : new Error(String(error)));
102
+ throw error;
103
+ }
104
+ }
105
+ setupRoomListeners() {
106
+ if (!this.room)
107
+ return;
108
+ // Handle tracks from persona
109
+ this.room.on(RoomEvent.TrackSubscribed, (track, _pub, participant) => {
110
+ if (participant.identity !== this.options.agentIdentity)
111
+ return;
112
+ const mediaTrack = track.mediaStreamTrack;
113
+ if (track.kind === Track.Kind.Video) {
114
+ console.log('[PersonaSession] Got video track from persona');
115
+ this.options.onVideoTrack?.(mediaTrack);
116
+ }
117
+ else if (track.kind === Track.Kind.Audio) {
118
+ console.log('[PersonaSession] Got audio track from persona');
119
+ this.options.onAudioTrack?.(mediaTrack);
120
+ }
121
+ });
122
+ // Handle disconnection
123
+ this.room.on(RoomEvent.Disconnected, (reason) => {
124
+ const closeReason = mapDisconnectReason(reason);
125
+ console.log('[PersonaSession] Disconnected from room:', closeReason);
126
+ this.setState('disconnected');
127
+ this.options.onClose?.(closeReason);
128
+ });
129
+ // Handle errors
130
+ this.room.on(RoomEvent.MediaDevicesError, (error) => {
131
+ console.error('[PersonaSession] Media devices error:', error);
132
+ this.options.onError?.(error);
133
+ });
134
+ }
135
+ /**
136
+ * Send audio data to the persona for video synthesis.
137
+ *
138
+ * @param pcmData - 16-bit PCM audio at 24kHz
139
+ */
140
+ sendAudio(pcmData) {
141
+ if (!this.room || this._state !== 'connected') {
142
+ console.warn('[PersonaSession] sendAudio dropped - not connected');
143
+ return;
144
+ }
145
+ // Chain writes to ensure ordering and track completion
146
+ // Lazily create ByteStreamWriter if needed (e.g., after endAudioTurn closed it)
147
+ console.log(`[PersonaSession] Writing ${pcmData.length} bytes to ByteStream`);
148
+ this.writeQueue = this.writeQueue
149
+ .then(async () => {
150
+ if (!this.byteStreamWriter && this.room) {
151
+ this.byteStreamWriter = await this.room.localParticipant.streamBytes({
152
+ topic: AUDIO_STREAM_TOPIC,
153
+ destinationIdentities: [this.options.agentIdentity],
154
+ });
155
+ }
156
+ await this.byteStreamWriter?.write(pcmData);
157
+ })
158
+ .catch((err) => {
159
+ console.warn('[PersonaSession] Failed to write audio:', err);
160
+ });
161
+ // Schedule auto-flush after timeout (reset on each audio chunk)
162
+ this.scheduleAutoFlush();
163
+ }
164
+ /**
165
+ * Schedule auto-flush after a timeout.
166
+ * Called on each sendAudio() to reset the timer.
167
+ */
168
+ scheduleAutoFlush() {
169
+ if (this.autoFlushTimer) {
170
+ clearTimeout(this.autoFlushTimer);
171
+ }
172
+ this.autoFlushTimer = setTimeout(() => {
173
+ if (this.byteStreamWriter) {
174
+ console.log('[PersonaSession] Auto-flush: no audio for', AUTO_FLUSH_TIMEOUT_MS, 'ms');
175
+ this.endAudioTurn();
176
+ }
177
+ }, AUTO_FLUSH_TIMEOUT_MS);
178
+ }
179
+ /**
180
+ * Flush all pending audio writes.
181
+ * Call before close to ensure all audio is sent.
182
+ */
183
+ async flush() {
184
+ await this.writeQueue;
185
+ }
186
+ /**
187
+ * End the current audio turn and flush any buffered data.
188
+ *
189
+ * This is called automatically 300ms after the last sendAudio() call.
190
+ * You typically don't need to call this manually, but it's available
191
+ * if you need immediate flushing.
192
+ */
193
+ async endAudioTurn() {
194
+ // Clear auto-flush timer to prevent double-flush
195
+ if (this.autoFlushTimer) {
196
+ clearTimeout(this.autoFlushTimer);
197
+ this.autoFlushTimer = null;
198
+ }
199
+ console.debug('[PersonaSession] endAudioTurn() called, awaiting writeQueue...');
200
+ await this.writeQueue;
201
+ console.debug('[PersonaSession] writeQueue complete, closing stream...');
202
+ if (this.byteStreamWriter) {
203
+ try {
204
+ await this.byteStreamWriter.close();
205
+ console.debug('[PersonaSession] stream closed successfully');
206
+ }
207
+ catch (err) {
208
+ console.warn('[PersonaSession] stream close error:', err);
209
+ }
210
+ this.byteStreamWriter = null;
211
+ }
212
+ else {
213
+ console.debug('[PersonaSession] no stream to close');
214
+ }
215
+ }
216
+ /**
217
+ * Signal an interruption to the avatar.
218
+ *
219
+ * Clears any pending frames in the GPU node's publish queue,
220
+ * allowing the avatar to quickly respond to new audio.
221
+ *
222
+ * Call this when the user interrupts the agent (e.g., starts speaking
223
+ * while the agent is still talking).
224
+ */
225
+ async interrupt() {
226
+ // Clear auto-flush timer - we're interrupting, don't need to flush
227
+ if (this.autoFlushTimer) {
228
+ clearTimeout(this.autoFlushTimer);
229
+ this.autoFlushTimer = null;
230
+ }
231
+ if (!this.room || this._state !== 'connected') {
232
+ console.warn('[PersonaSession] interrupt() called but not connected');
233
+ return;
234
+ }
235
+ try {
236
+ console.debug('[PersonaSession] Sending interrupt');
237
+ const writer = await this.room.localParticipant.streamBytes({
238
+ topic: CONTROL_STREAM_TOPIC,
239
+ destinationIdentities: [this.options.agentIdentity],
240
+ });
241
+ await writer.write(new TextEncoder().encode('interrupt'));
242
+ await writer.close();
243
+ }
244
+ catch (error) {
245
+ console.warn('[PersonaSession] Failed to send interrupt:', error);
246
+ }
247
+ }
248
+ /**
249
+ * Close the session and clean up resources.
250
+ */
251
+ async close() {
252
+ // Clear auto-flush timer
253
+ if (this.autoFlushTimer) {
254
+ clearTimeout(this.autoFlushTimer);
255
+ this.autoFlushTimer = null;
256
+ }
257
+ // Wait for pending writes to complete before closing
258
+ await this.flush();
259
+ // Close byte stream
260
+ if (this.byteStreamWriter) {
261
+ try {
262
+ await this.byteStreamWriter.close();
263
+ }
264
+ catch {
265
+ // Ignore close errors
266
+ }
267
+ this.byteStreamWriter = null;
268
+ }
269
+ // Disconnect from room
270
+ if (this.room) {
271
+ this.room.disconnect();
272
+ this.room = null;
273
+ }
274
+ this.setState('disconnected');
275
+ }
276
+ }
277
+ //# sourceMappingURL=PersonaSession.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PersonaSession.js","sourceRoot":"","sources":["../src/PersonaSession.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Audio utilities for PCM processing and silence generation.
3
+ */
4
+ /** Sample rate for audio sent to Persona */
5
+ export declare const SAMPLE_RATE = 24000;
6
+ /** Bytes per sample (16-bit PCM) */
7
+ export declare const BYTES_PER_SAMPLE = 2;
8
+ /** Number of channels (mono) */
9
+ export declare const CHANNELS = 1;
10
+ /**
11
+ * Convert base64-encoded audio to Uint8Array.
12
+ *
13
+ * @param base64 - Base64 encoded audio data
14
+ * @returns Uint8Array of audio bytes
15
+ */
16
+ export declare function base64ToBytes(base64: string): Uint8Array;
17
+ /**
18
+ * Convert Uint8Array to base64 string.
19
+ *
20
+ * @param bytes - Audio bytes
21
+ * @returns Base64 encoded string
22
+ */
23
+ export declare function bytesToBase64(bytes: Uint8Array): string;
24
+ /**
25
+ * Resample PCM audio from one sample rate to another.
26
+ * Simple linear interpolation - not high quality but sufficient for real-time.
27
+ *
28
+ * @param input - Input PCM bytes (16-bit signed)
29
+ * @param fromRate - Source sample rate
30
+ * @param toRate - Target sample rate
31
+ * @returns Resampled PCM bytes
32
+ */
33
+ export declare function resamplePcm(input: Uint8Array, fromRate: number, toRate: number): Uint8Array;
34
+ /**
35
+ * Create a simple typed event emitter.
36
+ */
37
+ export declare function createEventEmitter<T extends Record<string, any>>(): {
38
+ on<K extends keyof T>(event: K, handler: (data: T[K]) => void): void;
39
+ off<K extends keyof T>(event: K, handler: (data: T[K]) => void): void;
40
+ emit<K extends keyof T>(event: K, data: T[K]): void;
41
+ removeAllListeners(): void;
42
+ };
43
+ //# sourceMappingURL=audio-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio-utils.d.ts","sourceRoot":"","sources":["../src/audio-utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,4CAA4C;AAC5C,eAAO,MAAM,WAAW,QAAQ,CAAC;AAEjC,oCAAoC;AACpC,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC,gCAAgC;AAChC,eAAO,MAAM,QAAQ,IAAI,CAAC;AAE1B;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAOxD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAMvD;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,UAAU,CAuBZ;AAED;;GAEG;AAEH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;OAKzD,CAAC,SAAS,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,IAAI;QAQhE,CAAC,SAAS,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,IAAI;SAKhE,CAAC,SAAS,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;0BAI7B,IAAI;EAI7B"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Audio utilities for PCM processing and silence generation.
3
+ */
4
+ /** Sample rate for audio sent to Persona */
5
+ export const SAMPLE_RATE = 24000;
6
+ /** Bytes per sample (16-bit PCM) */
7
+ export const BYTES_PER_SAMPLE = 2;
8
+ /** Number of channels (mono) */
9
+ export const CHANNELS = 1;
10
+ /**
11
+ * Convert base64-encoded audio to Uint8Array.
12
+ *
13
+ * @param base64 - Base64 encoded audio data
14
+ * @returns Uint8Array of audio bytes
15
+ */
16
+ export function base64ToBytes(base64) {
17
+ const binaryString = atob(base64);
18
+ const bytes = new Uint8Array(binaryString.length);
19
+ for (let i = 0; i < binaryString.length; i++) {
20
+ bytes[i] = binaryString.charCodeAt(i);
21
+ }
22
+ return bytes;
23
+ }
24
+ /**
25
+ * Convert Uint8Array to base64 string.
26
+ *
27
+ * @param bytes - Audio bytes
28
+ * @returns Base64 encoded string
29
+ */
30
+ export function bytesToBase64(bytes) {
31
+ let binary = '';
32
+ for (let i = 0; i < bytes.length; i++) {
33
+ binary += String.fromCharCode(bytes[i]);
34
+ }
35
+ return btoa(binary);
36
+ }
37
+ /**
38
+ * Resample PCM audio from one sample rate to another.
39
+ * Simple linear interpolation - not high quality but sufficient for real-time.
40
+ *
41
+ * @param input - Input PCM bytes (16-bit signed)
42
+ * @param fromRate - Source sample rate
43
+ * @param toRate - Target sample rate
44
+ * @returns Resampled PCM bytes
45
+ */
46
+ export function resamplePcm(input, fromRate, toRate) {
47
+ if (fromRate === toRate) {
48
+ return input;
49
+ }
50
+ const inputView = new Int16Array(input.buffer, input.byteOffset, input.length / 2);
51
+ const ratio = fromRate / toRate;
52
+ const outputLength = Math.floor(inputView.length / ratio);
53
+ const output = new Int16Array(outputLength);
54
+ for (let i = 0; i < outputLength; i++) {
55
+ const srcIndex = i * ratio;
56
+ const srcIndexFloor = Math.floor(srcIndex);
57
+ const srcIndexCeil = Math.min(srcIndexFloor + 1, inputView.length - 1);
58
+ const fraction = srcIndex - srcIndexFloor;
59
+ // Linear interpolation
60
+ output[i] = Math.round(inputView[srcIndexFloor] * (1 - fraction) + inputView[srcIndexCeil] * fraction);
61
+ }
62
+ return new Uint8Array(output.buffer);
63
+ }
64
+ /**
65
+ * Create a simple typed event emitter.
66
+ */
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
+ export function createEventEmitter() {
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ const listeners = new Map();
71
+ return {
72
+ on(event, handler) {
73
+ if (!listeners.has(event)) {
74
+ listeners.set(event, new Set());
75
+ }
76
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
+ listeners.get(event).add(handler);
78
+ },
79
+ off(event, handler) {
80
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
+ listeners.get(event)?.delete(handler);
82
+ },
83
+ emit(event, data) {
84
+ listeners.get(event)?.forEach((handler) => handler(data));
85
+ },
86
+ removeAllListeners() {
87
+ listeners.clear();
88
+ },
89
+ };
90
+ }
91
+ //# sourceMappingURL=audio-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio-utils.js","sourceRoot":"","sources":["../src/audio-utils.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * @keyframelabs/sdk - Browser SDK for KeyframeLab's Persona avatar sessions.
3
- *
3
+ *
4
4
  * @example
5
5
  * ```typescript
6
6
  * import { createClient } from '@keyframelabs/sdk';
7
- *
7
+ *
8
8
  * const client = createClient({
9
9
  * personaId: 'luna',
10
10
  * // WebRTC URL and access token returned from KeyframeLab API (called from your server code)
@@ -14,41 +14,22 @@
14
14
  * videoElement.srcObject = new MediaStream([track]);
15
15
  * },
16
16
  * });
17
- *
17
+ *
18
18
  * await client.connect();
19
19
  * client.sendAudio(audioBytes);
20
20
  * await client.close();
21
21
  * ```
22
22
  */
23
-
24
23
  import { PersonaSession } from './PersonaSession.js';
25
24
  import type { PersonaSessionOptions } from './types.js';
26
-
27
25
  /**
28
26
  * Create a new Persona client.
29
- *
27
+ *
30
28
  * @param options - Session configuration options
31
29
  * @returns A new PersonaSession instance
32
30
  */
33
- const createClient = (options: PersonaSessionOptions): PersonaSession => {
34
- return new PersonaSession(options);
35
- };
36
-
37
- // Core
31
+ declare const createClient: (options: PersonaSessionOptions) => PersonaSession;
38
32
  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';
33
+ export type { CloseReason, PersonaSessionOptions, SessionState, } from './types.js';
34
+ export { SAMPLE_RATE, base64ToBytes, bytesToBase64, createEventEmitter, resamplePcm, } from './audio-utils.js';
35
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAExD;;;;;GAKG;AACH,QAAA,MAAM,YAAY,GAAI,SAAS,qBAAqB,KAAG,cAEtD,CAAC;AAGF,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,CAAC;AAGxC,YAAY,EACV,WAAW,EACX,qBAAqB,EACrB,YAAY,GACb,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,WAAW,EACX,aAAa,EACb,aAAa,EACb,kBAAkB,EAClB,WAAW,GACZ,MAAM,kBAAkB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,37 @@
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
+ import { PersonaSession } from './PersonaSession.js';
24
+ /**
25
+ * Create a new Persona client.
26
+ *
27
+ * @param options - Session configuration options
28
+ * @returns A new PersonaSession instance
29
+ */
30
+ const createClient = (options) => {
31
+ return new PersonaSession(options);
32
+ };
33
+ // Core
34
+ export { createClient, PersonaSession };
35
+ // Utilities
36
+ export { SAMPLE_RATE, base64ToBytes, bytesToBase64, createEventEmitter, resamplePcm, } from './audio-utils.js';
37
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGrD;;;;;GAKG;AACH,MAAM,YAAY,GAAG,CAAC,OAA8B,EAAkB,EAAE;IACtE,OAAO,IAAI,cAAc,CAAC,OAAO,CAAC,CAAC;AACrC,CAAC,CAAC;AAEF,OAAO;AACP,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,CAAC;AASxC,YAAY;AACZ,OAAO,EACL,WAAW,EACX,aAAa,EACb,aAAa,EACb,kBAAkB,EAClB,WAAW,GACZ,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shared types for Persona SDK.
3
+ */
4
+ /** Session state machine */
5
+ export type SessionState = 'disconnected' | 'connecting' | 'connected' | 'error';
6
+ /** Reason for session close */
7
+ export type CloseReason = 'client_initiated' | 'room_deleted' | 'server_shutdown' | 'participant_removed' | 'unknown';
8
+ /** Options for creating a PersonaSession */
9
+ export interface PersonaSessionOptions {
10
+ /** WebRTC URL (returned from KeyframeLab API) */
11
+ serverUrl: string;
12
+ /** WebRTC access token (returned from KeyframeLab API) */
13
+ participantToken: string;
14
+ /** Identity of the agent participant */
15
+ agentIdentity: string;
16
+ /** Called when video track is available */
17
+ onVideoTrack?: (track: MediaStreamTrack) => void;
18
+ /** Called when audio track is available */
19
+ onAudioTrack?: (track: MediaStreamTrack) => void;
20
+ /** Called when session state changes */
21
+ onStateChange?: (state: SessionState) => void;
22
+ /** Called on error */
23
+ onError?: (error: Error) => void;
24
+ /** Called when session is closed (by server or client) */
25
+ onClose?: (reason: CloseReason) => void;
26
+ }
27
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,4BAA4B;AAC5B,MAAM,MAAM,YAAY,GACpB,cAAc,GACd,YAAY,GACZ,WAAW,GACX,OAAO,CAAC;AAEZ,+BAA+B;AAC/B,MAAM,MAAM,WAAW,GACnB,kBAAkB,GAClB,cAAc,GACd,iBAAiB,GACjB,qBAAqB,GACrB,SAAS,CAAC;AAEd,4CAA4C;AAC5C,MAAM,WAAW,qBAAqB;IACpC,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,gBAAgB,EAAE,MAAM,CAAC;IACzB,wCAAwC;IACxC,aAAa,EAAE,MAAM,CAAC;IAEtB,2CAA2C;IAC3C,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAEjD,2CAA2C;IAC3C,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAEjD,wCAAwC;IACxC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAE9C,sBAAsB;IACtB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAEjC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;CACzC"}
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Shared types for Persona SDK.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keyframelabs/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Browser SDK for KeyframeLab's Persona avatar sessions",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,8 +12,7 @@
12
12
  }
13
13
  },
14
14
  "files": [
15
- "dist",
16
- "src"
15
+ "dist"
17
16
  ],
18
17
  "publishConfig": {
19
18
  "access": "public"
@@ -1,324 +0,0 @@
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
-
@@ -1,112 +0,0 @@
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/types.ts DELETED
@@ -1,43 +0,0 @@
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
- }