@sayna-ai/node-sdk 0.0.19 → 0.0.20
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 +10 -0
- package/dist/index.cjs +238 -53
- package/dist/index.cjs.map +3 -3
- package/dist/index.js +238 -53
- package/dist/index.js.map +3 -3
- package/dist/sayna-client.d.ts +39 -1
- package/dist/sayna-client.d.ts.map +1 -1
- package/dist/types.d.ts +38 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -240,14 +240,24 @@ Registers a callback for error messages.
|
|
|
240
240
|
|
|
241
241
|
Registers a callback for participant messages.
|
|
242
242
|
|
|
243
|
+
### `client.registerOnParticipantConnected(callback)`
|
|
244
|
+
|
|
245
|
+
Registers a callback for participant connection events.
|
|
246
|
+
|
|
243
247
|
### `client.registerOnParticipantDisconnected(callback)`
|
|
244
248
|
|
|
245
249
|
Registers a callback for participant disconnection events.
|
|
246
250
|
|
|
251
|
+
### `client.registerOnTrackSubscribed(callback)`
|
|
252
|
+
|
|
253
|
+
Registers a callback for track subscription events.
|
|
254
|
+
|
|
247
255
|
### `client.registerOnTtsPlaybackComplete(callback)`
|
|
248
256
|
|
|
249
257
|
Registers a callback for TTS playback completion events.
|
|
250
258
|
|
|
259
|
+
Unknown or malformed websocket control messages are logged and ignored so protocol drift does not break the client receive loop. Actual server `error` messages still reach `client.registerOnError(callback)`.
|
|
260
|
+
|
|
251
261
|
### `await client.speak(text, flush?, allowInterruption?)`
|
|
252
262
|
|
|
253
263
|
Sends text to be synthesized as speech.
|
package/dist/index.cjs
CHANGED
|
@@ -128,7 +128,9 @@ class SaynaClient {
|
|
|
128
128
|
ttsCallback;
|
|
129
129
|
errorCallback;
|
|
130
130
|
messageCallback;
|
|
131
|
+
participantConnectedCallback;
|
|
131
132
|
participantDisconnectedCallback;
|
|
133
|
+
trackSubscribedCallback;
|
|
132
134
|
ttsPlaybackCompleteCallback;
|
|
133
135
|
sipTransferErrorCallback;
|
|
134
136
|
readyPromiseResolve;
|
|
@@ -157,7 +159,7 @@ class SaynaClient {
|
|
|
157
159
|
if (this.isConnected) {
|
|
158
160
|
return;
|
|
159
161
|
}
|
|
160
|
-
const wsUrl = this.
|
|
162
|
+
const wsUrl = this.getWebSocketUrl();
|
|
161
163
|
return new Promise((resolve, reject) => {
|
|
162
164
|
this.readyPromiseResolve = resolve;
|
|
163
165
|
this.readyPromiseReject = reject;
|
|
@@ -212,27 +214,30 @@ class SaynaClient {
|
|
|
212
214
|
try {
|
|
213
215
|
if (event.data instanceof Blob || event.data instanceof ArrayBuffer) {
|
|
214
216
|
const buffer = event.data instanceof Blob ? await event.data.arrayBuffer() : event.data;
|
|
215
|
-
|
|
216
|
-
await this.ttsCallback(buffer);
|
|
217
|
-
}
|
|
217
|
+
await this.invokeCallback(this.ttsCallback, buffer, "TTS audio");
|
|
218
218
|
} else {
|
|
219
219
|
if (typeof event.data !== "string") {
|
|
220
|
-
|
|
220
|
+
this.logProtocolWarning("Ignoring websocket message with unsupported non-binary payload type", event.data);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
let data;
|
|
224
|
+
try {
|
|
225
|
+
data = JSON.parse(event.data);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
this.logProtocolWarning(`Ignoring invalid websocket JSON: ${this.describeError(error)}`, event.data);
|
|
228
|
+
return;
|
|
221
229
|
}
|
|
222
|
-
const data = JSON.parse(event.data);
|
|
223
230
|
await this.handleJsonMessage(data);
|
|
224
231
|
}
|
|
225
232
|
} catch (error) {
|
|
226
|
-
|
|
227
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
228
|
-
await this.errorCallback({
|
|
229
|
-
type: "error",
|
|
230
|
-
message: `Failed to process message: ${errorMessage}`
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
+
await this.reportHandlerError("WebSocket message", error);
|
|
233
234
|
}
|
|
234
235
|
}
|
|
235
|
-
async handleJsonMessage(
|
|
236
|
+
async handleJsonMessage(raw) {
|
|
237
|
+
const data = this.parseIncomingMessage(raw);
|
|
238
|
+
if (!data) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
236
241
|
const messageType = data.type;
|
|
237
242
|
try {
|
|
238
243
|
switch (messageType) {
|
|
@@ -250,72 +255,246 @@ class SaynaClient {
|
|
|
250
255
|
break;
|
|
251
256
|
}
|
|
252
257
|
case "stt_result": {
|
|
253
|
-
|
|
254
|
-
if (this.sttCallback) {
|
|
255
|
-
await this.sttCallback(sttResult);
|
|
256
|
-
}
|
|
258
|
+
await this.invokeCallback(this.sttCallback, data, "STT result");
|
|
257
259
|
break;
|
|
258
260
|
}
|
|
259
261
|
case "error": {
|
|
260
|
-
|
|
261
|
-
if (this.errorCallback) {
|
|
262
|
-
await this.errorCallback(errorMsg);
|
|
263
|
-
}
|
|
262
|
+
await this.invokeCallback(this.errorCallback, data, "error");
|
|
264
263
|
if (this.readyPromiseReject && !this.isReady) {
|
|
265
|
-
this.readyPromiseReject(new SaynaServerError(
|
|
264
|
+
this.readyPromiseReject(new SaynaServerError(data.message));
|
|
266
265
|
}
|
|
267
266
|
break;
|
|
268
267
|
}
|
|
269
268
|
case "sip_transfer_error": {
|
|
270
|
-
const sipTransferError = data;
|
|
271
269
|
if (this.sipTransferErrorCallback) {
|
|
272
|
-
await this.sipTransferErrorCallback
|
|
270
|
+
await this.invokeCallback(this.sipTransferErrorCallback, data, "SIP transfer error");
|
|
273
271
|
} else if (this.errorCallback) {
|
|
274
272
|
await this.errorCallback({
|
|
275
273
|
type: "error",
|
|
276
|
-
message:
|
|
274
|
+
message: data.message
|
|
277
275
|
});
|
|
278
276
|
}
|
|
279
277
|
break;
|
|
280
278
|
}
|
|
281
279
|
case "message": {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
280
|
+
await this.invokeCallback(this.messageCallback, data.message, "message");
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case "participant_connected": {
|
|
284
|
+
await this.invokeCallback(this.participantConnectedCallback, data.participant, "participant connected");
|
|
286
285
|
break;
|
|
287
286
|
}
|
|
288
287
|
case "participant_disconnected": {
|
|
289
|
-
|
|
290
|
-
if (this.participantDisconnectedCallback) {
|
|
291
|
-
await this.participantDisconnectedCallback(participantMsg.participant);
|
|
292
|
-
}
|
|
288
|
+
await this.invokeCallback(this.participantDisconnectedCallback, data.participant, "participant disconnected");
|
|
293
289
|
break;
|
|
294
290
|
}
|
|
295
|
-
case "
|
|
296
|
-
|
|
297
|
-
if (this.ttsPlaybackCompleteCallback) {
|
|
298
|
-
await this.ttsPlaybackCompleteCallback(ttsPlaybackCompleteMsg.timestamp);
|
|
299
|
-
}
|
|
291
|
+
case "track_subscribed": {
|
|
292
|
+
await this.invokeCallback(this.trackSubscribedCallback, data.track, "track subscribed");
|
|
300
293
|
break;
|
|
301
294
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (this.errorCallback) {
|
|
306
|
-
await this.errorCallback({ type: "error", message: errorMessage });
|
|
307
|
-
} else {
|
|
308
|
-
console.warn(errorMessage);
|
|
309
|
-
}
|
|
295
|
+
case "tts_playback_complete": {
|
|
296
|
+
await this.invokeCallback(this.ttsPlaybackCompleteCallback, data.timestamp, "TTS playback complete");
|
|
297
|
+
break;
|
|
310
298
|
}
|
|
311
299
|
}
|
|
312
300
|
} catch (error) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
301
|
+
await this.reportHandlerError(`"${messageType}"`, error);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
getWebSocketUrl() {
|
|
305
|
+
const wsUrl = this.url.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
|
|
306
|
+
const parsedUrl = new URL(wsUrl);
|
|
307
|
+
if (!parsedUrl.pathname.endsWith("/ws")) {
|
|
308
|
+
parsedUrl.pathname = parsedUrl.pathname.endsWith("/") ? `${parsedUrl.pathname}ws` : `${parsedUrl.pathname}/ws`;
|
|
309
|
+
}
|
|
310
|
+
return parsedUrl.toString();
|
|
311
|
+
}
|
|
312
|
+
parseIncomingMessage(raw) {
|
|
313
|
+
if (!this.isJsonObject(raw)) {
|
|
314
|
+
this.logProtocolWarning("Ignoring websocket payload because it is not a JSON object", raw);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const messageType = raw.type;
|
|
318
|
+
if (typeof messageType !== "string") {
|
|
319
|
+
this.logProtocolWarning('Ignoring websocket payload without a string "type" field', raw);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
switch (messageType) {
|
|
324
|
+
case "ready":
|
|
325
|
+
return {
|
|
326
|
+
type: "ready",
|
|
327
|
+
stream_id: this.getOptionalString(raw, "stream_id"),
|
|
328
|
+
livekit_room_name: this.getOptionalString(raw, "livekit_room_name"),
|
|
329
|
+
livekit_url: this.getOptionalString(raw, "livekit_url"),
|
|
330
|
+
sayna_participant_identity: this.getOptionalString(raw, "sayna_participant_identity"),
|
|
331
|
+
sayna_participant_name: this.getOptionalString(raw, "sayna_participant_name")
|
|
332
|
+
};
|
|
333
|
+
case "stt_result":
|
|
334
|
+
return {
|
|
335
|
+
type: "stt_result",
|
|
336
|
+
transcript: this.getRequiredString(raw, "transcript"),
|
|
337
|
+
is_final: this.getRequiredBoolean(raw, "is_final"),
|
|
338
|
+
is_speech_final: this.getRequiredBoolean(raw, "is_speech_final"),
|
|
339
|
+
confidence: this.getRequiredNumber(raw, "confidence")
|
|
340
|
+
};
|
|
341
|
+
case "error":
|
|
342
|
+
return {
|
|
343
|
+
type: "error",
|
|
344
|
+
message: this.getRequiredString(raw, "message")
|
|
345
|
+
};
|
|
346
|
+
case "sip_transfer_error":
|
|
347
|
+
return {
|
|
348
|
+
type: "sip_transfer_error",
|
|
349
|
+
message: this.getRequiredString(raw, "message")
|
|
350
|
+
};
|
|
351
|
+
case "message":
|
|
352
|
+
return {
|
|
353
|
+
type: "message",
|
|
354
|
+
message: this.parseSaynaMessage(raw, "message")
|
|
355
|
+
};
|
|
356
|
+
case "participant_connected":
|
|
357
|
+
return {
|
|
358
|
+
type: "participant_connected",
|
|
359
|
+
participant: this.parseParticipant(raw, "participant")
|
|
360
|
+
};
|
|
361
|
+
case "participant_disconnected":
|
|
362
|
+
return {
|
|
363
|
+
type: "participant_disconnected",
|
|
364
|
+
participant: this.parseParticipant(raw, "participant")
|
|
365
|
+
};
|
|
366
|
+
case "track_subscribed":
|
|
367
|
+
return {
|
|
368
|
+
type: "track_subscribed",
|
|
369
|
+
track: this.parseTrack(raw, "track")
|
|
370
|
+
};
|
|
371
|
+
case "tts_playback_complete":
|
|
372
|
+
return {
|
|
373
|
+
type: "tts_playback_complete",
|
|
374
|
+
timestamp: this.getRequiredNumber(raw, "timestamp")
|
|
375
|
+
};
|
|
376
|
+
default:
|
|
377
|
+
this.logProtocolWarning(`Ignoring unknown websocket message type "${messageType}"`, raw);
|
|
378
|
+
return;
|
|
318
379
|
}
|
|
380
|
+
} catch (error) {
|
|
381
|
+
this.logProtocolWarning(`Ignoring malformed websocket message "${messageType}": ${this.describeError(error)}`, raw);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
parseSaynaMessage(container, fieldName) {
|
|
386
|
+
const message = this.getRequiredObject(container, fieldName);
|
|
387
|
+
return {
|
|
388
|
+
message: this.getOptionalString(message, "message"),
|
|
389
|
+
data: this.getOptionalString(message, "data"),
|
|
390
|
+
identity: this.getRequiredString(message, "identity"),
|
|
391
|
+
topic: this.getRequiredString(message, "topic"),
|
|
392
|
+
room: this.getRequiredString(message, "room"),
|
|
393
|
+
timestamp: this.getRequiredNumber(message, "timestamp")
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
parseParticipant(container, fieldName) {
|
|
397
|
+
const participant = this.getRequiredObject(container, fieldName);
|
|
398
|
+
return {
|
|
399
|
+
identity: this.getRequiredString(participant, "identity"),
|
|
400
|
+
name: this.getOptionalString(participant, "name"),
|
|
401
|
+
room: this.getRequiredString(participant, "room"),
|
|
402
|
+
timestamp: this.getRequiredNumber(participant, "timestamp")
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
parseTrack(container, fieldName) {
|
|
406
|
+
const track = this.getRequiredObject(container, fieldName);
|
|
407
|
+
const trackKind = this.getRequiredString(track, "track_kind");
|
|
408
|
+
if (trackKind !== "audio" && trackKind !== "video") {
|
|
409
|
+
throw new Error('Field "track_kind" must be either "audio" or "video"');
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
identity: this.getRequiredString(track, "identity"),
|
|
413
|
+
name: this.getOptionalString(track, "name"),
|
|
414
|
+
track_kind: trackKind,
|
|
415
|
+
track_sid: this.getRequiredString(track, "track_sid"),
|
|
416
|
+
room: this.getRequiredString(track, "room"),
|
|
417
|
+
timestamp: this.getRequiredNumber(track, "timestamp")
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
getRequiredObject(container, fieldName) {
|
|
421
|
+
const value = container[fieldName];
|
|
422
|
+
if (!this.isJsonObject(value)) {
|
|
423
|
+
throw new Error(`Field "${fieldName}" must be an object`);
|
|
424
|
+
}
|
|
425
|
+
return value;
|
|
426
|
+
}
|
|
427
|
+
getRequiredString(container, fieldName) {
|
|
428
|
+
const value = container[fieldName];
|
|
429
|
+
if (typeof value !== "string") {
|
|
430
|
+
throw new Error(`Field "${fieldName}" must be a string`);
|
|
431
|
+
}
|
|
432
|
+
return value;
|
|
433
|
+
}
|
|
434
|
+
getOptionalString(container, fieldName) {
|
|
435
|
+
const value = container[fieldName];
|
|
436
|
+
if (typeof value === "undefined" || value === null) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (typeof value !== "string") {
|
|
440
|
+
throw new Error(`Field "${fieldName}" must be a string when present`);
|
|
441
|
+
}
|
|
442
|
+
return value;
|
|
443
|
+
}
|
|
444
|
+
getRequiredBoolean(container, fieldName) {
|
|
445
|
+
const value = container[fieldName];
|
|
446
|
+
if (typeof value !== "boolean") {
|
|
447
|
+
throw new Error(`Field "${fieldName}" must be a boolean`);
|
|
448
|
+
}
|
|
449
|
+
return value;
|
|
450
|
+
}
|
|
451
|
+
getRequiredNumber(container, fieldName) {
|
|
452
|
+
const value = container[fieldName];
|
|
453
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
454
|
+
throw new Error(`Field "${fieldName}" must be a finite number`);
|
|
455
|
+
}
|
|
456
|
+
return value;
|
|
457
|
+
}
|
|
458
|
+
isJsonObject(value) {
|
|
459
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
460
|
+
}
|
|
461
|
+
logProtocolWarning(message, payload) {
|
|
462
|
+
console.warn(`${message}; payload=${this.safeStringify(payload)}`);
|
|
463
|
+
}
|
|
464
|
+
safeStringify(payload) {
|
|
465
|
+
try {
|
|
466
|
+
return JSON.stringify(payload);
|
|
467
|
+
} catch {
|
|
468
|
+
return String(payload);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
describeError(error) {
|
|
472
|
+
return error instanceof Error ? error.message : String(error);
|
|
473
|
+
}
|
|
474
|
+
async invokeCallback(callback, payload, label) {
|
|
475
|
+
if (!callback) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
await callback(payload);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
await this.reportHandlerError(`${label} callback`, error);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async reportHandlerError(label, error) {
|
|
485
|
+
const message = `${label} failed: ${this.describeError(error)}`;
|
|
486
|
+
if (!this.errorCallback) {
|
|
487
|
+
console.error(message);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
await this.errorCallback({
|
|
492
|
+
type: "error",
|
|
493
|
+
message
|
|
494
|
+
});
|
|
495
|
+
} catch (callbackError) {
|
|
496
|
+
console.error(message);
|
|
497
|
+
console.error(`error callback failed: ${this.describeError(callbackError)}`);
|
|
319
498
|
}
|
|
320
499
|
}
|
|
321
500
|
createWebSocket(url) {
|
|
@@ -435,9 +614,15 @@ class SaynaClient {
|
|
|
435
614
|
registerOnMessage(callback) {
|
|
436
615
|
this.messageCallback = callback;
|
|
437
616
|
}
|
|
617
|
+
registerOnParticipantConnected(callback) {
|
|
618
|
+
this.participantConnectedCallback = callback;
|
|
619
|
+
}
|
|
438
620
|
registerOnParticipantDisconnected(callback) {
|
|
439
621
|
this.participantDisconnectedCallback = callback;
|
|
440
622
|
}
|
|
623
|
+
registerOnTrackSubscribed(callback) {
|
|
624
|
+
this.trackSubscribedCallback = callback;
|
|
625
|
+
}
|
|
441
626
|
registerOnTtsPlaybackComplete(callback) {
|
|
442
627
|
this.ttsPlaybackCompleteCallback = callback;
|
|
443
628
|
}
|
|
@@ -879,4 +1064,4 @@ async function saynaConnect(url, sttConfig, ttsConfig, livekitConfig, withoutAud
|
|
|
879
1064
|
return client;
|
|
880
1065
|
}
|
|
881
1066
|
|
|
882
|
-
//# debugId=
|
|
1067
|
+
//# debugId=5E4C5FDAA012925764756E2164756E21
|