@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.
- package/dist/PersonaSession.d.ts +89 -0
- package/dist/PersonaSession.d.ts.map +1 -0
- package/dist/PersonaSession.js +277 -0
- package/dist/PersonaSession.js.map +1 -0
- package/dist/audio-utils.d.ts +43 -0
- package/dist/audio-utils.d.ts.map +1 -0
- package/dist/audio-utils.js +91 -0
- package/dist/audio-utils.js.map +1 -0
- package/{src/index.ts → dist/index.d.ts} +8 -27
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +2 -3
- package/src/PersonaSession.ts +0 -324
- package/src/audio-utils.ts +0 -112
- package/src/types.ts +0 -43
|
@@ -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
|
|
34
|
-
return new PersonaSession(options);
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
// Core
|
|
31
|
+
declare const createClient: (options: PersonaSessionOptions) => PersonaSession;
|
|
38
32
|
export { createClient, PersonaSession };
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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.
|
|
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"
|
package/src/PersonaSession.ts
DELETED
|
@@ -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
|
-
|
package/src/audio-utils.ts
DELETED
|
@@ -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
|
-
}
|