@convai/web-sdk 1.3.1-beta.0 → 1.4.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/AudioManager.d.ts +4 -0
- package/dist/core/AudioManager.d.ts.map +1 -1
- package/dist/core/AudioManager.js +26 -0
- package/dist/core/AudioManager.js.map +1 -1
- package/dist/core/BlendshapeQueue.d.ts +13 -1
- package/dist/core/BlendshapeQueue.d.ts.map +1 -1
- package/dist/core/BlendshapeQueue.js +56 -5
- package/dist/core/BlendshapeQueue.js.map +1 -1
- package/dist/core/ConvaiClient.d.ts +26 -0
- package/dist/core/ConvaiClient.d.ts.map +1 -1
- package/dist/core/ConvaiClient.js +287 -196
- package/dist/core/ConvaiClient.js.map +1 -1
- package/dist/core/MessageHandler.d.ts +6 -0
- package/dist/core/MessageHandler.d.ts.map +1 -1
- package/dist/core/MessageHandler.js +107 -27
- package/dist/core/MessageHandler.js.map +1 -1
- package/dist/core/WebSocketSession.d.ts +45 -0
- package/dist/core/WebSocketSession.d.ts.map +1 -0
- package/dist/core/WebSocketSession.js +133 -0
- package/dist/core/WebSocketSession.js.map +1 -0
- package/dist/core/types.d.ts +15 -51
- package/dist/core/types.d.ts.map +1 -1
- package/dist/react/components/ConvaiWidget.d.ts.map +1 -1
- package/dist/react/components/ConvaiWidget.js +26 -14
- package/dist/react/components/ConvaiWidget.js.map +1 -1
- package/dist/react/hooks/useConvaiClient.d.ts.map +1 -1
- package/dist/react/hooks/useConvaiClient.js +16 -0
- package/dist/react/hooks/useConvaiClient.js.map +1 -1
- package/dist/vanilla/AudioRenderer.d.ts +6 -0
- package/dist/vanilla/AudioRenderer.d.ts.map +1 -1
- package/dist/vanilla/AudioRenderer.js +21 -0
- package/dist/vanilla/AudioRenderer.js.map +1 -1
- package/dist/vanilla/ConvaiWidget.d.ts.map +1 -1
- package/dist/vanilla/ConvaiWidget.js +1 -0
- package/dist/vanilla/ConvaiWidget.js.map +1 -1
- package/dist/vanilla/types.d.ts +1 -1
- package/dist/vanilla/types.d.ts.map +1 -1
- package/package.json +3 -1
|
@@ -6,6 +6,7 @@ import { MessageHandler } from "./MessageHandler";
|
|
|
6
6
|
import { EventEmitter } from "./EventEmitter";
|
|
7
7
|
import { MemoryManager } from "./MemoryManager";
|
|
8
8
|
import { ConnectionStateHandler } from "./ConnectionStateHandler";
|
|
9
|
+
import { WebSocketSession } from "./WebSocketSession";
|
|
9
10
|
/**
|
|
10
11
|
* Custom reconnect policy that disables automatic reconnection
|
|
11
12
|
*/
|
|
@@ -64,6 +65,8 @@ export class ConvaiClient extends EventEmitter {
|
|
|
64
65
|
this._storedConfig = null;
|
|
65
66
|
this._endUserId = null;
|
|
66
67
|
this._endUserMetadata = null;
|
|
68
|
+
this._wsSession = null;
|
|
69
|
+
this._activeTransport = null;
|
|
67
70
|
// Conversation session tracking
|
|
68
71
|
this._conversationSessionId = 0;
|
|
69
72
|
this._conversationStartTime = 0;
|
|
@@ -209,7 +212,25 @@ export class ConvaiClient extends EventEmitter {
|
|
|
209
212
|
this.updateState({ emotion });
|
|
210
213
|
});
|
|
211
214
|
this._messageHandler.on("listeningChange", (isListening) => {
|
|
215
|
+
const wasListening = this._state.isListening;
|
|
212
216
|
this.updateState({ isListening });
|
|
217
|
+
// Voice input path — activate the blendshape queue as soon as the user
|
|
218
|
+
// starts speaking (user-started-speaking) so conversationActive is true
|
|
219
|
+
// immediately, matching the text-input path where it's true on send.
|
|
220
|
+
if (!wasListening && isListening) {
|
|
221
|
+
// Reset blendshape queue if mid-conversation, but don't interrupt the server
|
|
222
|
+
if (this.blendshapeQueue.isConversationActive() ||
|
|
223
|
+
this._state.isSpeaking) {
|
|
224
|
+
this.blendshapeQueue.interrupt();
|
|
225
|
+
}
|
|
226
|
+
this._conversationSessionId++;
|
|
227
|
+
this._conversationStartTime = Date.now();
|
|
228
|
+
this.emit("conversationStart", {
|
|
229
|
+
sessionId: this._conversationSessionId,
|
|
230
|
+
userMessage: "[voice]",
|
|
231
|
+
timestamp: this._conversationStartTime,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
213
234
|
});
|
|
214
235
|
// Handle metrics events
|
|
215
236
|
this._messageHandler.on("metrics", (metricsData) => {
|
|
@@ -240,6 +261,31 @@ export class ConvaiClient extends EventEmitter {
|
|
|
240
261
|
this._messageHandler.on("llmNoResponse", () => {
|
|
241
262
|
this.emit("llmNoResponse");
|
|
242
263
|
});
|
|
264
|
+
// Forward bot-output event (aggregated output with spoken status)
|
|
265
|
+
this._messageHandler.on("botOutput", (data) => {
|
|
266
|
+
this.emit("botOutput", data);
|
|
267
|
+
});
|
|
268
|
+
// Forward bot TTS text (word-by-word as spoken)
|
|
269
|
+
this._messageHandler.on("botTtsText", (data) => {
|
|
270
|
+
this.emit("botTtsText", data);
|
|
271
|
+
});
|
|
272
|
+
// Forward bot TTS lifecycle events
|
|
273
|
+
this._messageHandler.on("botTtsStarted", () => {
|
|
274
|
+
this.emit("botTtsStarted");
|
|
275
|
+
});
|
|
276
|
+
this._messageHandler.on("botTtsStopped", () => {
|
|
277
|
+
this.emit("botTtsStopped");
|
|
278
|
+
});
|
|
279
|
+
// Forward server-side muting events
|
|
280
|
+
this._messageHandler.on("userMuteStarted", () => {
|
|
281
|
+
this.emit("userMuteStarted");
|
|
282
|
+
});
|
|
283
|
+
this._messageHandler.on("userMuteStopped", () => {
|
|
284
|
+
this.emit("userMuteStopped");
|
|
285
|
+
});
|
|
286
|
+
this._messageHandler.on("actionResponse", (data) => {
|
|
287
|
+
this.emit("actionResponse", data);
|
|
288
|
+
});
|
|
243
289
|
}
|
|
244
290
|
/**
|
|
245
291
|
* Setup MessageHandler to respond to conversation start events
|
|
@@ -249,7 +295,6 @@ export class ConvaiClient extends EventEmitter {
|
|
|
249
295
|
setupMessageHandlerEvents() {
|
|
250
296
|
this.on("conversationStart", (data) => {
|
|
251
297
|
this._messageHandler.getBlendshapeQueue().startConversation();
|
|
252
|
-
console.log(`[ConvaiClient] 🎬 Blendshape queue activated for conversation ${data.sessionId}`);
|
|
253
298
|
});
|
|
254
299
|
}
|
|
255
300
|
/**
|
|
@@ -279,6 +324,30 @@ export class ConvaiClient extends EventEmitter {
|
|
|
279
324
|
this.emit("stateChange", this._state);
|
|
280
325
|
}
|
|
281
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Send a data message via the active transport.
|
|
329
|
+
* For LiveKit: publishes via the data channel.
|
|
330
|
+
* For WebSocket: sends via PipecatClient.sendClientMessage (wrapped in {t,d} envelope).
|
|
331
|
+
*/
|
|
332
|
+
publishMessage(type, data) {
|
|
333
|
+
if (this._activeTransport === "websocket" && this._wsSession?.isConnected) {
|
|
334
|
+
this._wsSession.sendMessage(type, data);
|
|
335
|
+
}
|
|
336
|
+
else if (this._room?.localParticipant &&
|
|
337
|
+
this._room.state !== "disconnected") {
|
|
338
|
+
const encodedData = new TextEncoder().encode(JSON.stringify({ type, data }));
|
|
339
|
+
this._room.localParticipant.publishData(encodedData, { reliable: true });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Returns true when a message can be sent on the active transport.
|
|
344
|
+
*/
|
|
345
|
+
isTransportReady() {
|
|
346
|
+
if (this._activeTransport === "websocket") {
|
|
347
|
+
return this._wsSession?.isConnected ?? false;
|
|
348
|
+
}
|
|
349
|
+
return (!!this._room?.localParticipant && this._room.state !== "disconnected");
|
|
350
|
+
}
|
|
282
351
|
/**
|
|
283
352
|
* Connect to a Convai character
|
|
284
353
|
*/
|
|
@@ -315,6 +384,8 @@ export class ConvaiClient extends EventEmitter {
|
|
|
315
384
|
// Determine connection type based on enableVideo
|
|
316
385
|
const connType = configWithDefaults.enableVideo ? "video" : "audio";
|
|
317
386
|
this._connectionType = connType;
|
|
387
|
+
const transportType = configWithDefaults.transport ?? "livekit";
|
|
388
|
+
this._activeTransport = transportType;
|
|
318
389
|
// Prepare request body with required parameters
|
|
319
390
|
const characterSessionIdToSend = configWithDefaults.characterSessionId ?? this._characterSessionId;
|
|
320
391
|
const requestBody = {
|
|
@@ -325,7 +396,7 @@ export class ConvaiClient extends EventEmitter {
|
|
|
325
396
|
...(configWithDefaults.endUserMetadata != null && {
|
|
326
397
|
end_user_metadata: configWithDefaults.endUserMetadata,
|
|
327
398
|
}),
|
|
328
|
-
transport:
|
|
399
|
+
transport: transportType,
|
|
329
400
|
connection_type: connType,
|
|
330
401
|
blendshape_provider: configWithDefaults.enableLipsync
|
|
331
402
|
? "neurosync"
|
|
@@ -338,7 +409,7 @@ export class ConvaiClient extends EventEmitter {
|
|
|
338
409
|
output_fps: 90,
|
|
339
410
|
},
|
|
340
411
|
llm_provider: "dynamic",
|
|
341
|
-
...(configWithDefaults.enableEmotion
|
|
412
|
+
...(configWithDefaults.enableEmotion === true && {
|
|
342
413
|
emotion_config: {
|
|
343
414
|
provider: configWithDefaults.emotionConfig?.provider ?? "llm",
|
|
344
415
|
min_word_threshold: configWithDefaults.emotionConfig?.min_word_threshold ?? 3,
|
|
@@ -356,19 +427,14 @@ export class ConvaiClient extends EventEmitter {
|
|
|
356
427
|
character_session_id: characterSessionIdToSend,
|
|
357
428
|
}),
|
|
358
429
|
...(configWithDefaults.actionConfig && {
|
|
359
|
-
action_config:
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
...(configWithDefaults.currentAttentionObject && {
|
|
368
|
-
current_attention_object: configWithDefaults.currentAttentionObject,
|
|
369
|
-
}),
|
|
370
|
-
...(configWithDefaults.sceneDescription && {
|
|
371
|
-
scene_description: configWithDefaults.sceneDescription,
|
|
430
|
+
action_config: {
|
|
431
|
+
actions: configWithDefaults.actionConfig.actions,
|
|
432
|
+
objects: configWithDefaults.actionConfig.objects,
|
|
433
|
+
characters: configWithDefaults.actionConfig.characters,
|
|
434
|
+
...(configWithDefaults.actionConfig.current_attention_object && {
|
|
435
|
+
current_attention_object: configWithDefaults.actionConfig.current_attention_object,
|
|
436
|
+
}),
|
|
437
|
+
},
|
|
372
438
|
}),
|
|
373
439
|
...(configWithDefaults.dynamicInfo && {
|
|
374
440
|
dynamic_info: {
|
|
@@ -383,24 +449,26 @@ export class ConvaiClient extends EventEmitter {
|
|
|
383
449
|
}),
|
|
384
450
|
invocation_metadata: {
|
|
385
451
|
source: configWithDefaults.invocationMetadata?.source ?? "web_sdk",
|
|
386
|
-
client_version: configWithDefaults.invocationMetadata?.clientVersion ??
|
|
452
|
+
client_version: configWithDefaults.invocationMetadata?.clientVersion ??
|
|
453
|
+
"1.3.2-beta.0",
|
|
387
454
|
...(configWithDefaults.invocationMetadata?.extraMetadata && {
|
|
388
455
|
extra_metadata: configWithDefaults.invocationMetadata.extraMetadata,
|
|
389
456
|
}),
|
|
390
457
|
},
|
|
391
458
|
};
|
|
392
|
-
const
|
|
459
|
+
const connectHeaders = {
|
|
393
460
|
"Content-Type": "application/json",
|
|
394
461
|
};
|
|
395
462
|
if (configWithDefaults.authToken) {
|
|
396
|
-
|
|
463
|
+
connectHeaders["API-AUTH-TOKEN"] = configWithDefaults.authToken;
|
|
397
464
|
}
|
|
398
465
|
else if (configWithDefaults.apiKey) {
|
|
399
|
-
|
|
466
|
+
connectHeaders["X-API-Key"] = configWithDefaults.apiKey;
|
|
400
467
|
}
|
|
468
|
+
// HTTP POST /connect — same request for both transports
|
|
401
469
|
const response = await fetch(`${configWithDefaults.url}/connect`, {
|
|
402
470
|
method: "POST",
|
|
403
|
-
headers,
|
|
471
|
+
headers: connectHeaders,
|
|
404
472
|
body: JSON.stringify(requestBody),
|
|
405
473
|
});
|
|
406
474
|
if (!response.ok) {
|
|
@@ -426,7 +494,7 @@ export class ConvaiClient extends EventEmitter {
|
|
|
426
494
|
throw new Error(errorMessage);
|
|
427
495
|
}
|
|
428
496
|
const connectionData = await response.json();
|
|
429
|
-
// Capture
|
|
497
|
+
// Capture session identifiers from /connect response
|
|
430
498
|
if (connectionData.character_session_id) {
|
|
431
499
|
this._characterSessionId = connectionData.character_session_id;
|
|
432
500
|
this._storedConfig = {
|
|
@@ -434,39 +502,87 @@ export class ConvaiClient extends EventEmitter {
|
|
|
434
502
|
characterSessionId: connectionData.character_session_id,
|
|
435
503
|
};
|
|
436
504
|
}
|
|
437
|
-
// Capture end_user_id and end_user_metadata from connection response
|
|
438
505
|
if (connectionData.end_user_id) {
|
|
439
506
|
this._endUserId = connectionData.end_user_id;
|
|
440
507
|
}
|
|
441
508
|
if (connectionData.end_user_metadata) {
|
|
442
509
|
this._endUserMetadata = connectionData.end_user_metadata;
|
|
443
510
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
channelCount: this._audioSettings.channelCount,
|
|
511
|
+
if (transportType === "websocket") {
|
|
512
|
+
// ── WebSocket (Pipecat) transport path ────────────────────────────
|
|
513
|
+
// Server returns the WebSocket URL in room_url (same field as LiveKit).
|
|
514
|
+
const wsUrl = connectionData.room_url;
|
|
515
|
+
if (!wsUrl) {
|
|
516
|
+
throw new Error("No WebSocket URL returned from /connect endpoint (expected room_url)");
|
|
517
|
+
}
|
|
518
|
+
// Always enable mic at construction so WavMediaManager initializes the
|
|
519
|
+
// audio stream during connect(). If startWithAudioOn is false, we mute
|
|
520
|
+
// after the connection is established (enableMic(false) works once the
|
|
521
|
+
// stream exists, but not before it is initialized).
|
|
522
|
+
this._wsSession = new WebSocketSession((payload) => this._messageHandler.handleDataReceivedPublic(payload), true);
|
|
523
|
+
this._wsSession.on("botAudioTrack", (track) => {
|
|
524
|
+
this.emit("botAudioTrack", track);
|
|
459
525
|
});
|
|
526
|
+
this._wsSession.on("disconnected", () => {
|
|
527
|
+
this._connectionStateHandler.handleDisconnected();
|
|
528
|
+
});
|
|
529
|
+
this._audioManager.setWebSocketSession(this._wsSession);
|
|
530
|
+
// Wait for WebSocket transport to connect (fires before bot-ready)
|
|
531
|
+
await new Promise((resolve, reject) => {
|
|
532
|
+
const onConnected = () => {
|
|
533
|
+
this._wsSession.off("connected", onConnected);
|
|
534
|
+
this._wsSession.off("error", onError);
|
|
535
|
+
resolve();
|
|
536
|
+
};
|
|
537
|
+
const onError = (err) => {
|
|
538
|
+
this._wsSession.off("connected", onConnected);
|
|
539
|
+
this._wsSession.off("error", onError);
|
|
540
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
541
|
+
};
|
|
542
|
+
this._wsSession.on("connected", onConnected);
|
|
543
|
+
this._wsSession.on("error", onError);
|
|
544
|
+
// connectWithUrl calls initDevices() then connect({ wsUrl }) — matches sandbox flow
|
|
545
|
+
this._wsSession.connectWithUrl(wsUrl).catch((err) => {
|
|
546
|
+
this._wsSession.off("connected", onConnected);
|
|
547
|
+
this._wsSession.off("error", onError);
|
|
548
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
// For WebSocket, mic is already streaming after connect.
|
|
552
|
+
// Only mute if the caller explicitly sets startWithAudioOn: false.
|
|
553
|
+
// Defaulting to on matches the sandbox behavior and ensures the bot
|
|
554
|
+
// sees audio when it initializes (avoids server "tap mic to talk" prompt).
|
|
555
|
+
if (configWithDefaults.startWithAudioOn === false) {
|
|
556
|
+
this._wsSession.enableMic(false);
|
|
557
|
+
this._audioManager.syncWsAudioState(false);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
this._audioManager.syncWsAudioState(true);
|
|
561
|
+
}
|
|
460
562
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
563
|
+
else {
|
|
564
|
+
// ── LiveKit transport path (default) ─────────────────────────────
|
|
565
|
+
await this._room.connect(connectionData.room_url, connectionData.token, {
|
|
566
|
+
rtcConfig: {
|
|
567
|
+
iceTransportPolicy: "relay",
|
|
568
|
+
},
|
|
569
|
+
});
|
|
570
|
+
if (configWithDefaults.startWithAudioOn) {
|
|
571
|
+
await this._room.localParticipant.setMicrophoneEnabled(true, {
|
|
572
|
+
echoCancellation: this._audioSettings.echoCancellation,
|
|
573
|
+
noiseSuppression: this._audioSettings.noiseSuppression,
|
|
574
|
+
autoGainControl: this._audioSettings.autoGainControl,
|
|
575
|
+
sampleRate: this._audioSettings.sampleRate,
|
|
576
|
+
channelCount: this._audioSettings.channelCount,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
if (configWithDefaults.enableVideo &&
|
|
580
|
+
configWithDefaults.startWithVideoOn) {
|
|
581
|
+
await this._room.localParticipant.setCameraEnabled(true);
|
|
582
|
+
}
|
|
583
|
+
this._audioManager.syncStateFromRoom({ emit: true });
|
|
584
|
+
this._participantSid = this._room.localParticipant.sid;
|
|
465
585
|
}
|
|
466
|
-
// Ensure audio manager mirrors the actual microphone permission state
|
|
467
|
-
this._audioManager.syncStateFromRoom({ emit: true });
|
|
468
|
-
// Capture participant SID
|
|
469
|
-
this._participantSid = this._room.localParticipant.sid;
|
|
470
586
|
// Apply custom mapper to blendshape queue if provided
|
|
471
587
|
if (configWithDefaults.blendshapeConfig?.customMapper) {
|
|
472
588
|
this.blendshapeQueue.setMapper(configWithDefaults.blendshapeConfig.customMapper);
|
|
@@ -505,7 +621,18 @@ export class ConvaiClient extends EventEmitter {
|
|
|
505
621
|
* Disconnect from the current character session
|
|
506
622
|
*/
|
|
507
623
|
async disconnect() {
|
|
508
|
-
if (this.
|
|
624
|
+
if (this._activeTransport === "websocket" && this._wsSession) {
|
|
625
|
+
try {
|
|
626
|
+
await this._wsSession.disconnect();
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
// ignore disconnect errors
|
|
630
|
+
}
|
|
631
|
+
this._wsSession = null;
|
|
632
|
+
this._connectionStateHandler.resetConnectionState();
|
|
633
|
+
this.clearAllManagers();
|
|
634
|
+
}
|
|
635
|
+
else if (this._room && this._room.state !== "disconnected") {
|
|
509
636
|
try {
|
|
510
637
|
await this._room.disconnect();
|
|
511
638
|
this._connectionStateHandler.resetConnectionState();
|
|
@@ -523,6 +650,8 @@ export class ConvaiClient extends EventEmitter {
|
|
|
523
650
|
*/
|
|
524
651
|
clearAllManagers() {
|
|
525
652
|
this._connectionType = null;
|
|
653
|
+
this._activeTransport = null;
|
|
654
|
+
this._wsSession = null;
|
|
526
655
|
this._apiKey = null;
|
|
527
656
|
this._authToken = null;
|
|
528
657
|
this._characterId = null;
|
|
@@ -530,6 +659,7 @@ export class ConvaiClient extends EventEmitter {
|
|
|
530
659
|
this._endUserId = null;
|
|
531
660
|
this._endUserMetadata = null;
|
|
532
661
|
this._memoryManager = null; // Clear memory manager on disconnect
|
|
662
|
+
this._audioManager.setWebSocketSession(null);
|
|
533
663
|
this._audioManager.reset();
|
|
534
664
|
this._videoManager.reset();
|
|
535
665
|
this._screenShareManager.reset();
|
|
@@ -556,35 +686,28 @@ export class ConvaiClient extends EventEmitter {
|
|
|
556
686
|
* Send a text message to the character
|
|
557
687
|
*/
|
|
558
688
|
sendUserTextMessage(text) {
|
|
559
|
-
if (!this.
|
|
560
|
-
this._room.state === "disconnected" ||
|
|
561
|
-
!this._room.localParticipant) {
|
|
689
|
+
if (!this.isTransportReady())
|
|
562
690
|
return;
|
|
563
|
-
|
|
564
|
-
if (!text || !text.trim()) {
|
|
691
|
+
if (!text || !text.trim())
|
|
565
692
|
return;
|
|
566
|
-
}
|
|
567
693
|
try {
|
|
694
|
+
// Reset blendshape queue if mid-conversation, but don't interrupt the server
|
|
695
|
+
if (this.blendshapeQueue.isConversationActive() ||
|
|
696
|
+
this._state.isSpeaking) {
|
|
697
|
+
this.blendshapeQueue.interrupt();
|
|
698
|
+
}
|
|
568
699
|
// Start new conversation session
|
|
569
700
|
this._conversationSessionId++;
|
|
570
701
|
this._conversationStartTime = Date.now();
|
|
571
|
-
console.log(`------conversation ${this._conversationSessionId}-------`);
|
|
572
702
|
// Emit conversation start event
|
|
573
703
|
this.emit("conversationStart", {
|
|
574
704
|
sessionId: this._conversationSessionId,
|
|
575
705
|
userMessage: text.trim(),
|
|
576
706
|
timestamp: this._conversationStartTime,
|
|
577
707
|
});
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
text: text.trim(),
|
|
582
|
-
participant_sid: this._participantSid || this._room.localParticipant.sid,
|
|
583
|
-
},
|
|
584
|
-
};
|
|
585
|
-
const encodedData = new TextEncoder().encode(JSON.stringify(message));
|
|
586
|
-
this._room.localParticipant.publishData(encodedData, {
|
|
587
|
-
reliable: true,
|
|
708
|
+
this.publishMessage("user_text_message", {
|
|
709
|
+
text: text.trim(),
|
|
710
|
+
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
588
711
|
});
|
|
589
712
|
}
|
|
590
713
|
catch (error) {
|
|
@@ -595,44 +718,61 @@ export class ConvaiClient extends EventEmitter {
|
|
|
595
718
|
* Send a trigger message to invoke specific character actions
|
|
596
719
|
*/
|
|
597
720
|
sendTriggerMessage(triggerName, triggerMessage) {
|
|
598
|
-
if (this.
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
721
|
+
if (!this.isTransportReady())
|
|
722
|
+
return;
|
|
723
|
+
// Reset blendshape queue if mid-conversation, but don't interrupt the server
|
|
724
|
+
if (this.blendshapeQueue.isConversationActive() || this._state.isSpeaking) {
|
|
725
|
+
this.blendshapeQueue.interrupt();
|
|
726
|
+
}
|
|
727
|
+
this._conversationSessionId++;
|
|
728
|
+
this._conversationStartTime = Date.now();
|
|
729
|
+
this.emit("conversationStart", {
|
|
730
|
+
sessionId: this._conversationSessionId,
|
|
731
|
+
userMessage: `[trigger:${triggerName ?? ""}]`,
|
|
732
|
+
timestamp: this._conversationStartTime,
|
|
733
|
+
});
|
|
734
|
+
this.publishMessage("trigger-message", {
|
|
735
|
+
...(triggerName && { trigger_name: triggerName }),
|
|
736
|
+
...(triggerMessage && { trigger_message: triggerMessage }),
|
|
737
|
+
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* If the bot is currently speaking or a conversation is active, send an
|
|
742
|
+
* interrupt to the server and end the blendshape conversation so the queue
|
|
743
|
+
* is clean before a new response starts arriving.
|
|
744
|
+
*/
|
|
745
|
+
endCurrentConversationIfActive() {
|
|
746
|
+
const queueActive = this.blendshapeQueue.isConversationActive();
|
|
747
|
+
const botSpeaking = this._state.isSpeaking;
|
|
748
|
+
if (!queueActive && !botSpeaking)
|
|
749
|
+
return;
|
|
750
|
+
if (this.isTransportReady()) {
|
|
751
|
+
this.publishMessage("interrupt-bot", {
|
|
752
|
+
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
610
753
|
});
|
|
611
754
|
}
|
|
755
|
+
// End the blendshape queue so the lipsync player finishes cleanly
|
|
756
|
+
this.blendshapeQueue.interrupt();
|
|
612
757
|
}
|
|
613
758
|
/**
|
|
614
759
|
* Send an interrupt message to stop the bot's current response
|
|
615
760
|
*/
|
|
616
761
|
sendInterruptMessage() {
|
|
617
|
-
if (!this.
|
|
618
|
-
this._room.state === "disconnected" ||
|
|
619
|
-
!this._room.localParticipant) {
|
|
762
|
+
if (!this.isTransportReady())
|
|
620
763
|
return;
|
|
621
|
-
}
|
|
622
764
|
try {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
data: {
|
|
626
|
-
participant_sid: this._participantSid || this._room.localParticipant.sid,
|
|
627
|
-
},
|
|
628
|
-
};
|
|
629
|
-
const encodedData = new TextEncoder().encode(JSON.stringify(message));
|
|
630
|
-
this._room.localParticipant.publishData(encodedData, {
|
|
631
|
-
reliable: true,
|
|
765
|
+
this.publishMessage("interrupt-bot", {
|
|
766
|
+
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
632
767
|
});
|
|
633
|
-
//
|
|
634
|
-
|
|
635
|
-
|
|
768
|
+
// Blendshape fade-out only for LiveKit (WebSocket transport has no lipsync;
|
|
769
|
+
// calling interrupt() would set conversationEnded=true and block the next
|
|
770
|
+
// bot-started-speaking from ever firing isSpeaking).
|
|
771
|
+
if (this._activeTransport !== "websocket") {
|
|
772
|
+
if (this.blendshapeQueue.isConversationActive() ||
|
|
773
|
+
this._state.isSpeaking) {
|
|
774
|
+
this.blendshapeQueue.interrupt();
|
|
775
|
+
}
|
|
636
776
|
}
|
|
637
777
|
}
|
|
638
778
|
catch (error) {
|
|
@@ -643,39 +783,23 @@ export class ConvaiClient extends EventEmitter {
|
|
|
643
783
|
* Update template keys in the character's context
|
|
644
784
|
*/
|
|
645
785
|
updateTemplateKeys(templateKeys) {
|
|
646
|
-
if (this.
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
template_keys: templateKeys,
|
|
653
|
-
participant_sid: this._participantSid || this._room.localParticipant.sid,
|
|
654
|
-
},
|
|
655
|
-
};
|
|
656
|
-
const encodedData = new TextEncoder().encode(JSON.stringify(message));
|
|
657
|
-
this._room.localParticipant.publishData(encodedData, {
|
|
658
|
-
reliable: true,
|
|
659
|
-
});
|
|
660
|
-
}
|
|
786
|
+
if (!this.isTransportReady() || Object.keys(templateKeys).length === 0)
|
|
787
|
+
return;
|
|
788
|
+
this.publishMessage("update-template-keys", {
|
|
789
|
+
template_keys: templateKeys,
|
|
790
|
+
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
791
|
+
});
|
|
661
792
|
}
|
|
662
793
|
/**
|
|
663
794
|
* Update dynamic information about the current context
|
|
664
795
|
*/
|
|
665
796
|
updateDynamicInfo(dynamicInfo) {
|
|
666
|
-
if (this.
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
},
|
|
673
|
-
};
|
|
674
|
-
const encodedData = new TextEncoder().encode(JSON.stringify(message));
|
|
675
|
-
this._room.localParticipant.publishData(encodedData, {
|
|
676
|
-
reliable: true,
|
|
677
|
-
});
|
|
678
|
-
}
|
|
797
|
+
if (!this.isTransportReady() || !dynamicInfo?.trim())
|
|
798
|
+
return;
|
|
799
|
+
this.publishMessage("update-dynamic-info", {
|
|
800
|
+
dynamic_info: { text: dynamicInfo },
|
|
801
|
+
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
802
|
+
});
|
|
679
803
|
}
|
|
680
804
|
/**
|
|
681
805
|
* Update the bot's temporary runtime context in a unified way.
|
|
@@ -704,79 +828,61 @@ export class ConvaiClient extends EventEmitter {
|
|
|
704
828
|
* ```
|
|
705
829
|
*/
|
|
706
830
|
updateContext(options) {
|
|
707
|
-
if (!this.
|
|
831
|
+
if (!this.isTransportReady())
|
|
708
832
|
return;
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
833
|
+
const hasText = !!options.text?.trim();
|
|
834
|
+
const hasAttention = options.current_attention_object !== undefined;
|
|
835
|
+
// text is required unless mode is "reset" or only updating attention
|
|
836
|
+
if (options.mode !== "reset" && !hasText && !hasAttention) {
|
|
837
|
+
console.warn("[ConvaiClient] updateContext: text or current_attention_object is required unless mode is 'reset'");
|
|
713
838
|
return;
|
|
714
839
|
}
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
840
|
+
const willTriggerLlm = options.run_llm !== "false";
|
|
841
|
+
if (willTriggerLlm) {
|
|
842
|
+
if (this.blendshapeQueue.isConversationActive() ||
|
|
843
|
+
this._state.isSpeaking) {
|
|
844
|
+
this.blendshapeQueue.interrupt();
|
|
845
|
+
}
|
|
846
|
+
this._conversationSessionId++;
|
|
847
|
+
this._conversationStartTime = Date.now();
|
|
848
|
+
this.emit("conversationStart", {
|
|
849
|
+
sessionId: this._conversationSessionId,
|
|
850
|
+
userMessage: `[context-update:${options.mode ?? "append"}]`,
|
|
851
|
+
timestamp: this._conversationStartTime,
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
this.publishMessage("context-update", {
|
|
855
|
+
...(hasText && { text: options.text.trim() }),
|
|
856
|
+
...(options.mode && { mode: options.mode }),
|
|
857
|
+
...(options.run_llm && { run_llm: options.run_llm }),
|
|
858
|
+
...(hasAttention && { current_attention_object: options.current_attention_object }),
|
|
859
|
+
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
727
860
|
});
|
|
728
861
|
}
|
|
862
|
+
/**
|
|
863
|
+
* Send descriptive scene updates. Use when environment description changes
|
|
864
|
+
* and the bot should know what is visible or nearby. Does not modify action_config.
|
|
865
|
+
*/
|
|
866
|
+
updateSceneMetadata(items) {
|
|
867
|
+
if (!this.isTransportReady())
|
|
868
|
+
return;
|
|
869
|
+
this.publishMessage("update-scene-metadata", { scene_metadata: items });
|
|
870
|
+
}
|
|
729
871
|
/**
|
|
730
872
|
* Toggle text-to-speech on or off
|
|
731
873
|
*/
|
|
732
874
|
toggleTts(enabled) {
|
|
733
|
-
if (!this.
|
|
734
|
-
this._room.state === "disconnected" ||
|
|
735
|
-
!this._room.localParticipant) {
|
|
875
|
+
if (!this.isTransportReady())
|
|
736
876
|
return;
|
|
737
|
-
}
|
|
738
|
-
try {
|
|
739
|
-
const message = {
|
|
740
|
-
type: "tts-toggle",
|
|
741
|
-
data: {
|
|
742
|
-
enabled: enabled,
|
|
743
|
-
},
|
|
744
|
-
};
|
|
745
|
-
const encodedData = new TextEncoder().encode(JSON.stringify(message));
|
|
746
|
-
this._room.localParticipant.publishData(encodedData, {
|
|
747
|
-
reliable: true,
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
catch (error) {
|
|
751
|
-
throw error;
|
|
752
|
-
}
|
|
877
|
+
this.publishMessage("tts-toggle", { enabled });
|
|
753
878
|
}
|
|
754
879
|
/**
|
|
755
880
|
* Toggle speech-to-text on or off
|
|
756
881
|
*/
|
|
757
882
|
toggleStt(enabled) {
|
|
758
|
-
if (!this.
|
|
759
|
-
this._room.state === "disconnected" ||
|
|
760
|
-
!this._room.localParticipant) {
|
|
883
|
+
if (!this.isTransportReady())
|
|
761
884
|
return;
|
|
762
|
-
}
|
|
763
|
-
try {
|
|
764
|
-
const message = {
|
|
765
|
-
lable: "rtvi-ai",
|
|
766
|
-
type: "stt-toggle",
|
|
767
|
-
data: {
|
|
768
|
-
muted: !enabled,
|
|
769
|
-
},
|
|
770
|
-
};
|
|
771
|
-
const encodedData = new TextEncoder().encode(JSON.stringify(message));
|
|
772
|
-
console.log("Toggle Stt: ", enabled, encodedData);
|
|
773
|
-
this._room.localParticipant.publishData(encodedData, {
|
|
774
|
-
reliable: true,
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
catch (error) {
|
|
778
|
-
throw error;
|
|
779
|
-
}
|
|
885
|
+
this.publishMessage("stt-toggle", { muted: !enabled });
|
|
780
886
|
}
|
|
781
887
|
/**
|
|
782
888
|
* Reset the server-side idle timer.
|
|
@@ -784,26 +890,11 @@ export class ConvaiClient extends EventEmitter {
|
|
|
784
890
|
* the session from being disconnected due to inactivity.
|
|
785
891
|
*/
|
|
786
892
|
resetIdleTimer() {
|
|
787
|
-
if (!this.
|
|
788
|
-
this._room.state === "disconnected" ||
|
|
789
|
-
!this._room.localParticipant) {
|
|
893
|
+
if (!this.isTransportReady())
|
|
790
894
|
return;
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
type: "reset-idle-timer",
|
|
795
|
-
data: {
|
|
796
|
-
participant_sid: this._participantSid || this._room.localParticipant.sid,
|
|
797
|
-
},
|
|
798
|
-
};
|
|
799
|
-
const encodedData = new TextEncoder().encode(JSON.stringify(message));
|
|
800
|
-
this._room.localParticipant.publishData(encodedData, {
|
|
801
|
-
reliable: true,
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
catch (error) {
|
|
805
|
-
throw error;
|
|
806
|
-
}
|
|
895
|
+
this.publishMessage("reset-idle-timer", {
|
|
896
|
+
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
897
|
+
});
|
|
807
898
|
}
|
|
808
899
|
}
|
|
809
900
|
//# sourceMappingURL=ConvaiClient.js.map
|