@convai/web-sdk 1.5.0 → 1.6.0-beta.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/README.md +89 -36
- package/dist/core/BlendshapeQueue.d.ts +75 -4
- package/dist/core/BlendshapeQueue.d.ts.map +1 -1
- package/dist/core/BlendshapeQueue.js +400 -24
- package/dist/core/BlendshapeQueue.js.map +1 -1
- package/dist/core/ConvaiClient.d.ts +10 -2
- package/dist/core/ConvaiClient.d.ts.map +1 -1
- package/dist/core/ConvaiClient.js +208 -57
- package/dist/core/ConvaiClient.js.map +1 -1
- package/dist/core/MessageHandler.d.ts +8 -0
- package/dist/core/MessageHandler.d.ts.map +1 -1
- package/dist/core/MessageHandler.js +63 -12
- package/dist/core/MessageHandler.js.map +1 -1
- package/dist/core/VideoManager.d.ts +40 -5
- package/dist/core/VideoManager.d.ts.map +1 -1
- package/dist/core/VideoManager.js +579 -30
- package/dist/core/VideoManager.js.map +1 -1
- package/dist/core/connectRequest.d.ts +23 -0
- package/dist/core/connectRequest.d.ts.map +1 -0
- package/dist/core/connectRequest.js +31 -0
- package/dist/core/connectRequest.js.map +1 -0
- package/dist/core/contextUpdateRequest.d.ts +4 -0
- package/dist/core/contextUpdateRequest.d.ts.map +1 -0
- package/dist/core/contextUpdateRequest.js +10 -0
- package/dist/core/contextUpdateRequest.js.map +1 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/types.d.ts +172 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/videoSourceState.d.ts +4 -0
- package/dist/core/videoSourceState.d.ts.map +1 -0
- package/dist/core/videoSourceState.js +9 -0
- package/dist/core/videoSourceState.js.map +1 -0
- package/dist/core/visionRequest.d.ts +4 -0
- package/dist/core/visionRequest.d.ts.map +1 -0
- package/dist/core/visionRequest.js +42 -0
- package/dist/core/visionRequest.js.map +1 -0
- package/dist/react/hooks/useConvaiClient.d.ts.map +1 -1
- package/dist/react/hooks/useConvaiClient.js +3 -0
- package/dist/react/hooks/useConvaiClient.js.map +1 -1
- package/dist/vanilla/types.d.ts +6 -0
- package/dist/vanilla/types.d.ts.map +1 -1
- package/package.json +5 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Room } from "livekit-client";
|
|
2
|
-
import { ConvaiConfig, ConvaiClientState, ChatMessage, IConvaiClient, AudioControls, VideoControls, ScreenShareControls, DynamicInfo, ContextUpdateOptions, UploadFileOptions, WebSocketSessionFactory } from "./types";
|
|
2
|
+
import { ConvaiConfig, ConvaiClientState, ChatMessage, IConvaiClient, AudioControls, VideoControls, ScreenShareControls, DynamicInfo, ContextUpdateOptions, UploadFileOptions, WebSocketSessionFactory, RespondModeUpdateOptions, UpdateSceneMetadataOptions, VisionStatusOptions, VisionTriggerOptions } from "./types";
|
|
3
3
|
import { EventEmitter } from "./EventEmitter";
|
|
4
4
|
import { BlendshapeQueue } from "./BlendshapeQueue";
|
|
5
5
|
import { MemoryManager } from "./MemoryManager";
|
|
@@ -57,6 +57,7 @@ export declare class ConvaiClient extends EventEmitter implements IConvaiClient
|
|
|
57
57
|
private _wsSession;
|
|
58
58
|
private _activeTransport;
|
|
59
59
|
private _logRtviMessages;
|
|
60
|
+
private _isDisconnecting;
|
|
60
61
|
private _clientReadyRetryTimer;
|
|
61
62
|
private _clientReadyTimeoutTimer;
|
|
62
63
|
private _clientReadyMsgId;
|
|
@@ -122,6 +123,7 @@ export declare class ConvaiClient extends EventEmitter implements IConvaiClient
|
|
|
122
123
|
* Connect to a Convai character
|
|
123
124
|
*/
|
|
124
125
|
connect(config?: ConvaiConfig): Promise<void>;
|
|
126
|
+
private cleanupWebSocketSessionAfterConnectFailure;
|
|
125
127
|
/**
|
|
126
128
|
* RTVI client-ready handshake. Server-side gates bot-ready emission on
|
|
127
129
|
* receipt of client-ready (and re-emits bot-ready on every client-ready),
|
|
@@ -160,6 +162,7 @@ export declare class ConvaiClient extends EventEmitter implements IConvaiClient
|
|
|
160
162
|
* is clean before a new response starts arriving.
|
|
161
163
|
*/
|
|
162
164
|
private endCurrentConversationIfActive;
|
|
165
|
+
private startTrackedConversation;
|
|
163
166
|
/**
|
|
164
167
|
* Send an interrupt message to stop the bot's current response
|
|
165
168
|
*/
|
|
@@ -201,6 +204,9 @@ export declare class ConvaiClient extends EventEmitter implements IConvaiClient
|
|
|
201
204
|
* ```
|
|
202
205
|
*/
|
|
203
206
|
updateContext(options: ContextUpdateOptions): void;
|
|
207
|
+
visionStatus(options?: VisionStatusOptions): string | null;
|
|
208
|
+
visionTrigger(options?: VisionTriggerOptions): string | null;
|
|
209
|
+
respondModeUpdate(options: RespondModeUpdateOptions): string | null;
|
|
204
210
|
/**
|
|
205
211
|
* Send descriptive scene updates. Use when environment description changes
|
|
206
212
|
* and the bot should know what is visible or nearby. Does not modify action_config.
|
|
@@ -208,7 +214,9 @@ export declare class ConvaiClient extends EventEmitter implements IConvaiClient
|
|
|
208
214
|
updateSceneMetadata(items: Array<{
|
|
209
215
|
name: string;
|
|
210
216
|
description: string;
|
|
211
|
-
}
|
|
217
|
+
}>, options?: UpdateSceneMetadataOptions): void;
|
|
218
|
+
private createUpdateId;
|
|
219
|
+
private serializeRespondModes;
|
|
212
220
|
/**
|
|
213
221
|
* Upload a file to the character via the LiveKit byte stream transport.
|
|
214
222
|
* Only works when connected with the LiveKit transport.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConvaiClient.d.ts","sourceRoot":"","sources":["../../src/core/ConvaiClient.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EAML,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,WAAW,EAEX,aAAa,EACb,aAAa,EACb,aAAa,EACb,mBAAmB,EACnB,WAAW,EACX,oBAAoB,EACpB,iBAAiB,EAIjB,uBAAuB,
|
|
1
|
+
{"version":3,"file":"ConvaiClient.d.ts","sourceRoot":"","sources":["../../src/core/ConvaiClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Room, RoomEvent, DefaultReconnectPolicy, setLogLevel, } from "livekit-client";
|
|
2
2
|
setLogLevel("warn");
|
|
3
|
+
import { DisconnectReasonEnum, } from "./types";
|
|
3
4
|
import { AudioManager } from "./AudioManager";
|
|
4
5
|
import { VideoManager } from "./VideoManager";
|
|
5
6
|
import { ScreenShareManager } from "./ScreenShareManager";
|
|
@@ -7,6 +8,9 @@ import { MessageHandler } from "./MessageHandler";
|
|
|
7
8
|
import { EventEmitter } from "./EventEmitter";
|
|
8
9
|
import { MemoryManager } from "./MemoryManager";
|
|
9
10
|
import { ConnectionStateHandler } from "./ConnectionStateHandler";
|
|
11
|
+
import { buildBlendshapeConnectConfig } from "./connectRequest";
|
|
12
|
+
import { shouldPreemptForContextUpdateResponse, shouldPreemptForExplicitRespondMode, } from "./contextUpdateRequest";
|
|
13
|
+
import { resolveVisionInputConfig, serializeVisionInputConfig, } from "./visionRequest";
|
|
10
14
|
// Module-level registry — populated by importing @convai/web-sdk/vanilla/websocket
|
|
11
15
|
let _wsFactory = null;
|
|
12
16
|
/**
|
|
@@ -78,6 +82,7 @@ export class ConvaiClient extends EventEmitter {
|
|
|
78
82
|
this._wsSession = null;
|
|
79
83
|
this._activeTransport = null;
|
|
80
84
|
this._logRtviMessages = true;
|
|
85
|
+
this._isDisconnecting = false;
|
|
81
86
|
// RTVI client-ready handshake (race-proof bot-ready delivery)
|
|
82
87
|
this._clientReadyRetryTimer = null;
|
|
83
88
|
this._clientReadyTimeoutTimer = null;
|
|
@@ -244,8 +249,9 @@ export class ConvaiClient extends EventEmitter {
|
|
|
244
249
|
if (!wasListening && isListening) {
|
|
245
250
|
// Reset blendshape queue if mid-conversation, but don't interrupt the server
|
|
246
251
|
if (this.blendshapeQueue.isConversationActive() ||
|
|
247
|
-
this._state.isSpeaking
|
|
248
|
-
this.blendshapeQueue.
|
|
252
|
+
this._state.isSpeaking ||
|
|
253
|
+
this.blendshapeQueue.hasBufferedFrames()) {
|
|
254
|
+
this._messageHandler.interruptLipsyncPlayback();
|
|
249
255
|
}
|
|
250
256
|
this._conversationSessionId++;
|
|
251
257
|
this._conversationStartTime = Date.now();
|
|
@@ -277,6 +283,10 @@ export class ConvaiClient extends EventEmitter {
|
|
|
277
283
|
this._messageHandler.on("blendshapeStatsReceived", (stats) => {
|
|
278
284
|
this.emit("blendshapeStatsReceived", stats);
|
|
279
285
|
});
|
|
286
|
+
// Forward blendshape cancel event (owner-scoped visual hard stop)
|
|
287
|
+
this._messageHandler.on("blendshapeCancel", (data) => {
|
|
288
|
+
this.emit("blendshapeCancel", data);
|
|
289
|
+
});
|
|
280
290
|
// Forward idle warning event
|
|
281
291
|
this._messageHandler.on("idleWarning", (data) => {
|
|
282
292
|
this.emit("idleWarning", data);
|
|
@@ -419,6 +429,7 @@ export class ConvaiClient extends EventEmitter {
|
|
|
419
429
|
this._connectionType = connType;
|
|
420
430
|
const transportType = configWithDefaults.transport ?? "livekit";
|
|
421
431
|
this._activeTransport = transportType;
|
|
432
|
+
const effectiveVisionInputConfig = resolveVisionInputConfig(configWithDefaults);
|
|
422
433
|
// Prepare request body with required parameters
|
|
423
434
|
const characterSessionIdToSend = configWithDefaults.characterSessionId ?? this._characterSessionId;
|
|
424
435
|
const requestBody = {
|
|
@@ -431,16 +442,7 @@ export class ConvaiClient extends EventEmitter {
|
|
|
431
442
|
}),
|
|
432
443
|
transport: transportType,
|
|
433
444
|
connection_type: connType,
|
|
434
|
-
|
|
435
|
-
? "neurosync"
|
|
436
|
-
: "not_provided",
|
|
437
|
-
blendshape_config: {
|
|
438
|
-
enable_chunking: true,
|
|
439
|
-
chunk_size: 10,
|
|
440
|
-
format: configWithDefaults.blendshapeConfig?.format || "mha",
|
|
441
|
-
frames_buffer_duration: configWithDefaults.blendshapeConfig?.frames_buffer_duration ?? 0.1,
|
|
442
|
-
output_fps: 90,
|
|
443
|
-
},
|
|
445
|
+
...buildBlendshapeConnectConfig(configWithDefaults),
|
|
444
446
|
llm_provider: "dynamic",
|
|
445
447
|
...(configWithDefaults.enableEmotion === true && {
|
|
446
448
|
emotion_config: (() => {
|
|
@@ -461,6 +463,12 @@ export class ConvaiClient extends EventEmitter {
|
|
|
461
463
|
? configWithDefaults.ttsEnabled
|
|
462
464
|
: true,
|
|
463
465
|
default_stt_enabled: true,
|
|
466
|
+
...(effectiveVisionInputConfig && {
|
|
467
|
+
vision_input_config: serializeVisionInputConfig(effectiveVisionInputConfig),
|
|
468
|
+
}),
|
|
469
|
+
...(configWithDefaults.respondModes && {
|
|
470
|
+
respond_modes: this.serializeRespondModes(configWithDefaults.respondModes),
|
|
471
|
+
}),
|
|
464
472
|
...(characterSessionIdToSend &&
|
|
465
473
|
characterSessionIdToSend !== "-1" && {
|
|
466
474
|
character_session_id: characterSessionIdToSend,
|
|
@@ -489,7 +497,7 @@ export class ConvaiClient extends EventEmitter {
|
|
|
489
497
|
invocation_metadata: {
|
|
490
498
|
source: configWithDefaults.invocationMetadata?.source ?? "web_sdk",
|
|
491
499
|
client_version: configWithDefaults.invocationMetadata?.clientVersion ??
|
|
492
|
-
"1.
|
|
500
|
+
"1.6.0-beta.0",
|
|
493
501
|
...(configWithDefaults.invocationMetadata?.extraMetadata && {
|
|
494
502
|
extra_metadata: configWithDefaults.invocationMetadata.extraMetadata,
|
|
495
503
|
}),
|
|
@@ -569,32 +577,69 @@ export class ConvaiClient extends EventEmitter {
|
|
|
569
577
|
// after the connection is established (enableMic(false) works once the
|
|
570
578
|
// stream exists, but not before it is initialized).
|
|
571
579
|
this._wsSession = _wsFactory((payload) => this._messageHandler.handleDataReceivedPublic(payload), true);
|
|
572
|
-
|
|
580
|
+
const webSocketVisionEnabled = configWithDefaults.enableVideo === true &&
|
|
581
|
+
effectiveVisionInputConfig?.enabled !== false;
|
|
582
|
+
if (webSocketVisionEnabled) {
|
|
583
|
+
this._videoManager.setWebSocketVisionMessageSender((type, data) => {
|
|
584
|
+
if (this._activeTransport !== "websocket" ||
|
|
585
|
+
!this._wsSession?.isConnected) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
this._wsSession.sendMessage(type, data);
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
this._videoManager.setWebSocketVisionDisabled();
|
|
593
|
+
}
|
|
594
|
+
const wsSession = this._wsSession;
|
|
595
|
+
wsSession.on("botAudioTrack", (track) => {
|
|
573
596
|
this.emit("botAudioTrack", track);
|
|
574
597
|
});
|
|
575
|
-
|
|
598
|
+
wsSession.on("disconnected", () => {
|
|
599
|
+
if (this._wsSession !== wsSession || this._isDisconnecting) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
576
602
|
this._connectionStateHandler.handleDisconnected();
|
|
603
|
+
this.clearAllManagers();
|
|
577
604
|
});
|
|
578
605
|
this._audioManager.setWebSocketSession(this._wsSession);
|
|
579
606
|
// Wait for WebSocket transport to connect (fires before bot-ready)
|
|
580
607
|
await new Promise((resolve, reject) => {
|
|
608
|
+
let settled = false;
|
|
609
|
+
const cleanupConnectListeners = () => {
|
|
610
|
+
wsSession.off("connected", onConnected);
|
|
611
|
+
wsSession.off("error", onError);
|
|
612
|
+
wsSession.off("disconnected", onDisconnected);
|
|
613
|
+
};
|
|
614
|
+
const settle = (callback) => {
|
|
615
|
+
if (settled) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
settled = true;
|
|
619
|
+
cleanupConnectListeners();
|
|
620
|
+
callback();
|
|
621
|
+
};
|
|
581
622
|
const onConnected = () => {
|
|
582
|
-
|
|
583
|
-
this._wsSession.off("error", onError);
|
|
584
|
-
resolve();
|
|
623
|
+
settle(resolve);
|
|
585
624
|
};
|
|
586
625
|
const onError = (err) => {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
626
|
+
settle(() => {
|
|
627
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
628
|
+
});
|
|
629
|
+
};
|
|
630
|
+
const onDisconnected = () => {
|
|
631
|
+
settle(() => {
|
|
632
|
+
reject(new Error("WebSocket disconnected before connect completed"));
|
|
633
|
+
});
|
|
590
634
|
};
|
|
591
|
-
|
|
592
|
-
|
|
635
|
+
wsSession.on("connected", onConnected);
|
|
636
|
+
wsSession.on("error", onError);
|
|
637
|
+
wsSession.on("disconnected", onDisconnected);
|
|
593
638
|
// connectWithUrl calls initDevices() then connect({ wsUrl }) — matches sandbox flow
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
639
|
+
wsSession.connectWithUrl(wsUrl).catch((err) => {
|
|
640
|
+
settle(() => {
|
|
641
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
642
|
+
});
|
|
598
643
|
});
|
|
599
644
|
});
|
|
600
645
|
// For WebSocket, mic is already streaming after connect.
|
|
@@ -608,8 +653,14 @@ export class ConvaiClient extends EventEmitter {
|
|
|
608
653
|
else {
|
|
609
654
|
this._audioManager.syncWsAudioState(true);
|
|
610
655
|
}
|
|
656
|
+
if (webSocketVisionEnabled &&
|
|
657
|
+
configWithDefaults.enableVideo &&
|
|
658
|
+
configWithDefaults.startWithVideoOn) {
|
|
659
|
+
await this._videoManager.enableVideo();
|
|
660
|
+
}
|
|
611
661
|
}
|
|
612
662
|
else {
|
|
663
|
+
this._videoManager.setWebSocketVisionMessageSender(null);
|
|
613
664
|
// ── LiveKit transport path (default) ─────────────────────────────
|
|
614
665
|
await this._room.connect(connectionData.room_url, connectionData.token, {
|
|
615
666
|
rtcConfig: {
|
|
@@ -659,6 +710,7 @@ export class ConvaiClient extends EventEmitter {
|
|
|
659
710
|
this._startClientReadyHandshake();
|
|
660
711
|
}
|
|
661
712
|
catch (error) {
|
|
713
|
+
await this.cleanupWebSocketSessionAfterConnectFailure();
|
|
662
714
|
this.updateState({
|
|
663
715
|
isConnected: false,
|
|
664
716
|
isConnecting: false,
|
|
@@ -667,6 +719,22 @@ export class ConvaiClient extends EventEmitter {
|
|
|
667
719
|
throw error;
|
|
668
720
|
}
|
|
669
721
|
}
|
|
722
|
+
async cleanupWebSocketSessionAfterConnectFailure() {
|
|
723
|
+
if (this._activeTransport !== "websocket" || !this._wsSession) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
this._isDisconnecting = true;
|
|
727
|
+
try {
|
|
728
|
+
await this._wsSession.disconnect();
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
// ignore cleanup errors while preserving the original connect failure
|
|
732
|
+
}
|
|
733
|
+
finally {
|
|
734
|
+
this._isDisconnecting = false;
|
|
735
|
+
}
|
|
736
|
+
this.clearAllManagers();
|
|
737
|
+
}
|
|
670
738
|
/**
|
|
671
739
|
* RTVI client-ready handshake. Server-side gates bot-ready emission on
|
|
672
740
|
* receipt of client-ready (and re-emits bot-ready on every client-ready),
|
|
@@ -732,14 +800,18 @@ export class ConvaiClient extends EventEmitter {
|
|
|
732
800
|
async disconnect() {
|
|
733
801
|
this._stopClientReadyHandshake();
|
|
734
802
|
if (this._activeTransport === "websocket" && this._wsSession) {
|
|
803
|
+
this._isDisconnecting = true;
|
|
735
804
|
try {
|
|
736
805
|
await this._wsSession.disconnect();
|
|
737
806
|
}
|
|
738
807
|
catch {
|
|
739
808
|
// ignore disconnect errors
|
|
740
809
|
}
|
|
810
|
+
finally {
|
|
811
|
+
this._isDisconnecting = false;
|
|
812
|
+
}
|
|
741
813
|
this._wsSession = null;
|
|
742
|
-
this._connectionStateHandler.
|
|
814
|
+
this._connectionStateHandler.handleDisconnected(DisconnectReasonEnum.CLIENT_INITIATED);
|
|
743
815
|
this.clearAllManagers();
|
|
744
816
|
}
|
|
745
817
|
else if (this._room && this._room.state !== "disconnected") {
|
|
@@ -771,6 +843,7 @@ export class ConvaiClient extends EventEmitter {
|
|
|
771
843
|
this._memoryManager = null; // Clear memory manager on disconnect
|
|
772
844
|
this._audioManager.setWebSocketSession(null);
|
|
773
845
|
this._audioManager.reset();
|
|
846
|
+
this._videoManager.setWebSocketVisionMessageSender(null);
|
|
774
847
|
this._videoManager.reset();
|
|
775
848
|
this._screenShareManager.reset();
|
|
776
849
|
}
|
|
@@ -803,8 +876,9 @@ export class ConvaiClient extends EventEmitter {
|
|
|
803
876
|
try {
|
|
804
877
|
// Reset blendshape queue if mid-conversation, but don't interrupt the server
|
|
805
878
|
if (this.blendshapeQueue.isConversationActive() ||
|
|
806
|
-
this._state.isSpeaking
|
|
807
|
-
this.blendshapeQueue.
|
|
879
|
+
this._state.isSpeaking ||
|
|
880
|
+
this.blendshapeQueue.hasBufferedFrames()) {
|
|
881
|
+
this._messageHandler.interruptLipsyncPlayback();
|
|
808
882
|
}
|
|
809
883
|
// Start new conversation session
|
|
810
884
|
this._conversationSessionId++;
|
|
@@ -831,8 +905,10 @@ export class ConvaiClient extends EventEmitter {
|
|
|
831
905
|
if (!this.isTransportReady())
|
|
832
906
|
return;
|
|
833
907
|
// Reset blendshape queue if mid-conversation, but don't interrupt the server
|
|
834
|
-
if (this.blendshapeQueue.isConversationActive() ||
|
|
835
|
-
this.
|
|
908
|
+
if (this.blendshapeQueue.isConversationActive() ||
|
|
909
|
+
this._state.isSpeaking ||
|
|
910
|
+
this.blendshapeQueue.hasBufferedFrames()) {
|
|
911
|
+
this._messageHandler.interruptLipsyncPlayback();
|
|
836
912
|
}
|
|
837
913
|
this._conversationSessionId++;
|
|
838
914
|
this._conversationStartTime = Date.now();
|
|
@@ -853,7 +929,8 @@ export class ConvaiClient extends EventEmitter {
|
|
|
853
929
|
* is clean before a new response starts arriving.
|
|
854
930
|
*/
|
|
855
931
|
endCurrentConversationIfActive() {
|
|
856
|
-
const queueActive = this.blendshapeQueue.isConversationActive()
|
|
932
|
+
const queueActive = this.blendshapeQueue.isConversationActive() ||
|
|
933
|
+
this.blendshapeQueue.hasBufferedFrames();
|
|
857
934
|
const botSpeaking = this._state.isSpeaking;
|
|
858
935
|
if (!queueActive && !botSpeaking)
|
|
859
936
|
return;
|
|
@@ -863,7 +940,21 @@ export class ConvaiClient extends EventEmitter {
|
|
|
863
940
|
});
|
|
864
941
|
}
|
|
865
942
|
// End the blendshape queue so the lipsync player finishes cleanly
|
|
866
|
-
this.
|
|
943
|
+
this._messageHandler.interruptLipsyncPlayback();
|
|
944
|
+
}
|
|
945
|
+
startTrackedConversation(userMessage) {
|
|
946
|
+
if (this.blendshapeQueue.isConversationActive() ||
|
|
947
|
+
this._state.isSpeaking ||
|
|
948
|
+
this.blendshapeQueue.hasBufferedFrames()) {
|
|
949
|
+
this._messageHandler.interruptLipsyncPlayback();
|
|
950
|
+
}
|
|
951
|
+
this._conversationSessionId++;
|
|
952
|
+
this._conversationStartTime = Date.now();
|
|
953
|
+
this.emit("conversationStart", {
|
|
954
|
+
sessionId: this._conversationSessionId,
|
|
955
|
+
userMessage,
|
|
956
|
+
timestamp: this._conversationStartTime,
|
|
957
|
+
});
|
|
867
958
|
}
|
|
868
959
|
/**
|
|
869
960
|
* Send an interrupt message to stop the bot's current response
|
|
@@ -875,14 +966,13 @@ export class ConvaiClient extends EventEmitter {
|
|
|
875
966
|
this.publishMessage("interrupt-bot", {
|
|
876
967
|
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
877
968
|
});
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
if (this.
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
}
|
|
969
|
+
// Stop local lipsync immediately for every transport. WebSocket can also
|
|
970
|
+
// buffer ahead-delivered frames, and waiting for a server cancel leaves
|
|
971
|
+
// stale mouth animation visible after the audio stop.
|
|
972
|
+
if (this.blendshapeQueue.isConversationActive() ||
|
|
973
|
+
this._state.isSpeaking ||
|
|
974
|
+
this.blendshapeQueue.hasBufferedFrames()) {
|
|
975
|
+
this._messageHandler.interruptLipsyncPlayback();
|
|
886
976
|
}
|
|
887
977
|
}
|
|
888
978
|
catch (error) {
|
|
@@ -947,36 +1037,97 @@ export class ConvaiClient extends EventEmitter {
|
|
|
947
1037
|
console.warn("[ConvaiClient] updateContext: text or current_attention_object is required unless mode is 'reset'");
|
|
948
1038
|
return;
|
|
949
1039
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
this.
|
|
957
|
-
this._conversationStartTime = Date.now();
|
|
958
|
-
this.emit("conversationStart", {
|
|
959
|
-
sessionId: this._conversationSessionId,
|
|
960
|
-
userMessage: `[context-update:${options.mode ?? "append"}]`,
|
|
961
|
-
timestamp: this._conversationStartTime,
|
|
962
|
-
});
|
|
1040
|
+
// Only guaranteed-response updates should preempt active local lipsync.
|
|
1041
|
+
// Server-scheduled auto/silent updates may be downgraded while the bot is
|
|
1042
|
+
// speaking, so clearing the local queue here can stop lipsync while audio
|
|
1043
|
+
// continues.
|
|
1044
|
+
const shouldPreemptLipsync = shouldPreemptForContextUpdateResponse(options);
|
|
1045
|
+
if (shouldPreemptLipsync) {
|
|
1046
|
+
this.startTrackedConversation(`[context-update:${options.mode ?? "append"}]`);
|
|
963
1047
|
}
|
|
964
1048
|
this.publishMessage("context-update", {
|
|
965
1049
|
...(hasText && { text: options.text.trim() }),
|
|
966
1050
|
...(options.mode && { mode: options.mode }),
|
|
967
1051
|
...(options.run_llm && { run_llm: options.run_llm }),
|
|
1052
|
+
...(options.respond_mode && { respond_mode: options.respond_mode }),
|
|
968
1053
|
...(hasAttention && { current_attention_object: options.current_attention_object }),
|
|
969
1054
|
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
970
1055
|
});
|
|
971
1056
|
}
|
|
1057
|
+
visionStatus(options = {}) {
|
|
1058
|
+
if (!this.isTransportReady())
|
|
1059
|
+
return null;
|
|
1060
|
+
const updateId = options.updateId ?? this.createUpdateId("vision-status");
|
|
1061
|
+
this.publishMessage("vision-status", {
|
|
1062
|
+
update_id: updateId,
|
|
1063
|
+
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
1064
|
+
});
|
|
1065
|
+
return updateId;
|
|
1066
|
+
}
|
|
1067
|
+
visionTrigger(options = {}) {
|
|
1068
|
+
if (!this.isTransportReady())
|
|
1069
|
+
return null;
|
|
1070
|
+
const updateId = options.updateId ?? this.createUpdateId("vision-trigger");
|
|
1071
|
+
const respondMode = options.respondMode;
|
|
1072
|
+
if (shouldPreemptForExplicitRespondMode(respondMode)) {
|
|
1073
|
+
this.startTrackedConversation(`[vision-trigger:${respondMode}]`);
|
|
1074
|
+
}
|
|
1075
|
+
this.publishMessage("vision-trigger", {
|
|
1076
|
+
update_id: updateId,
|
|
1077
|
+
...(options.text && { text: options.text }),
|
|
1078
|
+
...(respondMode && { respond_mode: respondMode }),
|
|
1079
|
+
...(options.frameIndices && { frame_indices: options.frameIndices }),
|
|
1080
|
+
...(options.frameIds && { frame_ids: options.frameIds }),
|
|
1081
|
+
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
1082
|
+
});
|
|
1083
|
+
return updateId;
|
|
1084
|
+
}
|
|
1085
|
+
respondModeUpdate(options) {
|
|
1086
|
+
if (!this.isTransportReady())
|
|
1087
|
+
return null;
|
|
1088
|
+
const updateId = options.updateId ?? this.createUpdateId("respond-mode");
|
|
1089
|
+
this.publishMessage("respond-mode-update", {
|
|
1090
|
+
update_id: updateId,
|
|
1091
|
+
modality: options.modality,
|
|
1092
|
+
mode: options.mode,
|
|
1093
|
+
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
1094
|
+
});
|
|
1095
|
+
return updateId;
|
|
1096
|
+
}
|
|
972
1097
|
/**
|
|
973
1098
|
* Send descriptive scene updates. Use when environment description changes
|
|
974
1099
|
* and the bot should know what is visible or nearby. Does not modify action_config.
|
|
975
1100
|
*/
|
|
976
|
-
updateSceneMetadata(items) {
|
|
1101
|
+
updateSceneMetadata(items, options = {}) {
|
|
977
1102
|
if (!this.isTransportReady())
|
|
978
1103
|
return;
|
|
979
|
-
|
|
1104
|
+
if (shouldPreemptForExplicitRespondMode(options.respondMode)) {
|
|
1105
|
+
this.startTrackedConversation(`[scene-metadata:${options.respondMode}]`);
|
|
1106
|
+
}
|
|
1107
|
+
this.publishMessage("update-scene-metadata", {
|
|
1108
|
+
scene_metadata: items,
|
|
1109
|
+
...(options.respondMode && { respond_mode: options.respondMode }),
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
createUpdateId(prefix) {
|
|
1113
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
|
1114
|
+
return `${prefix}:${crypto.randomUUID()}`;
|
|
1115
|
+
}
|
|
1116
|
+
return `${prefix}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
|
|
1117
|
+
}
|
|
1118
|
+
serializeRespondModes(config) {
|
|
1119
|
+
return {
|
|
1120
|
+
...(config.text !== undefined && { text: config.text }),
|
|
1121
|
+
...(config.audio !== undefined && { audio: config.audio }),
|
|
1122
|
+
...(config.vision !== undefined && { vision: config.vision }),
|
|
1123
|
+
...(config.contextUpdate !== undefined && {
|
|
1124
|
+
context_update: config.contextUpdate,
|
|
1125
|
+
}),
|
|
1126
|
+
...(config.trigger !== undefined && { trigger: config.trigger }),
|
|
1127
|
+
...(config.sceneMetadata !== undefined && {
|
|
1128
|
+
scene_metadata: config.sceneMetadata,
|
|
1129
|
+
}),
|
|
1130
|
+
};
|
|
980
1131
|
}
|
|
981
1132
|
/**
|
|
982
1133
|
* Upload a file to the character via the LiveKit byte stream transport.
|