@webex/internal-plugin-voicea 3.12.0-auth-prejoin-fetch.1 → 3.12.0-llmrefactor.10
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/constants.js +4 -10
- package/dist/constants.js.map +1 -1
- package/dist/index.js +13 -4
- package/dist/index.js.map +1 -1
- package/dist/types/constants.d.ts +2 -1
- package/dist/types/index.d.ts +6 -3
- package/dist/types/voicea-plugin.d.ts +23 -0
- package/dist/types/voicea.d.ts +82 -31
- package/dist/types/voicea.types.d.ts +19 -1
- package/dist/voicea-plugin.js +59 -0
- package/dist/voicea-plugin.js.map +1 -0
- package/dist/voicea.js +257 -156
- package/dist/voicea.js.map +1 -1
- package/dist/voicea.types.js.map +1 -1
- package/package.json +9 -9
- package/src/constants.ts +2 -2
- package/src/index.ts +7 -5
- package/src/voicea-plugin.ts +33 -0
- package/src/voicea.ts +250 -159
- package/src/voicea.types.ts +22 -0
- package/test/unit/spec/voicea-plugin.js +83 -0
- package/test/unit/spec/voicea.js +837 -1126
package/src/voicea.ts
CHANGED
|
@@ -1,63 +1,111 @@
|
|
|
1
|
+
import EventEmitter from 'events';
|
|
1
2
|
import uuid from 'uuid';
|
|
2
|
-
|
|
3
|
+
// @ts-ignore - webex-core types
|
|
4
|
+
import {config} from '@webex/webex-core';
|
|
3
5
|
|
|
6
|
+
// @ts-ignore - internal-plugin-llm types
|
|
7
|
+
import type LLMChannel from '@webex/internal-plugin-llm';
|
|
4
8
|
import {
|
|
5
9
|
EVENT_TRIGGERS,
|
|
6
10
|
AIBRIDGE_RELAY_TYPES,
|
|
7
11
|
TRANSCRIPTION_TYPE,
|
|
8
|
-
VOICEA,
|
|
9
|
-
LLM_PRACTICE_SESSION,
|
|
10
12
|
ANNOUNCE_STATUS,
|
|
11
13
|
TURN_ON_CAPTION_STATUS,
|
|
12
14
|
TOGGLE_MANUAL_CAPTION_STATUS,
|
|
13
15
|
DEFAULT_SPOKEN_LANGUAGE,
|
|
14
|
-
LANGUAGE_ASSIGNMENT,
|
|
15
16
|
} from './constants';
|
|
16
|
-
// eslint-disable-next-line no-unused-vars
|
|
17
17
|
import {
|
|
18
18
|
AnnouncementPayload,
|
|
19
19
|
CaptionLanguageResponse,
|
|
20
|
+
SpeakerNameUpdatePayload,
|
|
20
21
|
TranscriptionResponse,
|
|
21
22
|
IVoiceaChannel,
|
|
22
23
|
} from './voicea.types';
|
|
23
24
|
import {millisToMinutesAndSeconds} from './utils';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
|
-
* @description VoiceaChannel
|
|
27
|
+
* @description VoiceaChannel — handles voicea/transcription functionality for a single LLM connection.
|
|
28
|
+
* Created via `webex.internal.voicea.createChannel(llmChannel)`. The caller owns the
|
|
29
|
+
* channel and is responsible for its lifecycle.
|
|
27
30
|
* @export
|
|
28
31
|
* @class VoiceaChannel
|
|
29
32
|
*/
|
|
30
|
-
export class VoiceaChannel extends
|
|
31
|
-
|
|
33
|
+
export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
34
|
+
private request: (options: {method: string; url: string; body?: object}) => Promise<any>;
|
|
35
|
+
private llmChannel?: LLMChannel;
|
|
32
36
|
|
|
33
37
|
private seqNum: number;
|
|
34
|
-
|
|
35
38
|
private areCaptionsEnabled: boolean;
|
|
36
|
-
|
|
37
39
|
private hasSubscribedToEvents = false;
|
|
38
|
-
|
|
39
40
|
private captionServiceId?: string;
|
|
40
|
-
|
|
41
41
|
private announceStatus: string;
|
|
42
|
-
|
|
43
42
|
private captionStatus: string;
|
|
44
43
|
|
|
45
44
|
private keepTranscriptionSubscribed: boolean;
|
|
46
45
|
|
|
47
46
|
private toggleManualCaptionStatus: string;
|
|
48
|
-
|
|
49
47
|
private currentSpokenLanguage?: string;
|
|
50
|
-
|
|
51
48
|
private spokenLanguages: string[] = [];
|
|
52
|
-
|
|
53
49
|
private currentCaptionLanguage?: string;
|
|
54
50
|
|
|
51
|
+
// Target channel for reconciler pattern - the channel voicea WANTS to be bound to
|
|
52
|
+
private targetLLMChannel?: LLMChannel;
|
|
53
|
+
// Single pending 'online' listener for deferred reconciliation
|
|
54
|
+
private _pendingOnlineListener?: () => void;
|
|
55
|
+
// Tracks whether caption restoration is pending (set on switch, cleared on restore)
|
|
56
|
+
private _pendingCaptionRestore = false;
|
|
57
|
+
|
|
55
58
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
59
|
+
* Creates a VoiceaChannel, optionally bound to an LLMChannel.
|
|
60
|
+
* If no llmChannel is provided, call switchLLMChannel() later to attach one.
|
|
61
|
+
* @param {LLMChannel} [llmChannel] - The LLM channel to use (optional)
|
|
62
|
+
* @param {Function} request - The request function for making API calls (typically webex.request bound to webex)
|
|
58
63
|
*/
|
|
64
|
+
constructor(
|
|
65
|
+
llmChannel: LLMChannel | undefined,
|
|
66
|
+
request: (options: {method: string; url: string; body?: object}) => Promise<any>
|
|
67
|
+
) {
|
|
68
|
+
super();
|
|
69
|
+
this.llmChannel = llmChannel;
|
|
70
|
+
this.request = request;
|
|
59
71
|
|
|
60
|
-
|
|
72
|
+
this.seqNum = 1;
|
|
73
|
+
this.areCaptionsEnabled = false;
|
|
74
|
+
this.captionServiceId = undefined;
|
|
75
|
+
this.announceStatus = ANNOUNCE_STATUS.IDLE;
|
|
76
|
+
this.captionStatus = TURN_ON_CAPTION_STATUS.IDLE;
|
|
77
|
+
this.toggleManualCaptionStatus = TOGGLE_MANUAL_CAPTION_STATUS.IDLE;
|
|
78
|
+
this.currentSpokenLanguage = DEFAULT_SPOKEN_LANGUAGE;
|
|
79
|
+
this.currentCaptionLanguage = undefined;
|
|
80
|
+
this.keepTranscriptionSubscribed = false;
|
|
81
|
+
|
|
82
|
+
// Subscribe to relay events from the LLM channel if provided
|
|
83
|
+
if (this.llmChannel) {
|
|
84
|
+
this.llmChannel.on('event:relay.event', this.eventProcessor);
|
|
85
|
+
this.hasSubscribedToEvents = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Returns the LLM channel, throwing if not available.
|
|
91
|
+
* @private
|
|
92
|
+
* @returns {LLMChannel}
|
|
93
|
+
* @throws {Error} If LLM channel is not available
|
|
94
|
+
*/
|
|
95
|
+
private requireLLMChannel(): LLMChannel {
|
|
96
|
+
if (!this.llmChannel) {
|
|
97
|
+
throw new Error('VoiceaChannel: LLM channel not available');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return this.llmChannel;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Process events from LLM channel
|
|
105
|
+
* @param {Object} e - Event data
|
|
106
|
+
* @returns {void}
|
|
107
|
+
*/
|
|
108
|
+
private eventProcessor = (e: any): void => {
|
|
61
109
|
this.seqNum = e.sequenceNumber + 1;
|
|
62
110
|
switch (e.data.relayType) {
|
|
63
111
|
case AIBRIDGE_RELAY_TYPES.VOICEA.ANNOUNCEMENT:
|
|
@@ -71,6 +119,9 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
71
119
|
case AIBRIDGE_RELAY_TYPES.VOICEA.TRANSCRIPTION:
|
|
72
120
|
this.processTranscription(e.data.voiceaPayload);
|
|
73
121
|
break;
|
|
122
|
+
case AIBRIDGE_RELAY_TYPES.VOICEA.SPEAKER_NAME_UPDATE:
|
|
123
|
+
this.processSpeakerNameUpdate(e.data.voiceaPayload);
|
|
124
|
+
break;
|
|
74
125
|
case AIBRIDGE_RELAY_TYPES.MANUAL.TRANSCRIPTION:
|
|
75
126
|
case AIBRIDGE_RELAY_TYPES.MANUAL.CAPTIONER:
|
|
76
127
|
this.processManualTranscription({
|
|
@@ -85,32 +136,24 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
85
136
|
};
|
|
86
137
|
|
|
87
138
|
/**
|
|
88
|
-
*
|
|
89
|
-
* @returns {undefined}
|
|
90
|
-
*/
|
|
91
|
-
private listenToEvents() {
|
|
92
|
-
if (!this.hasSubscribedToEvents) {
|
|
93
|
-
// @ts-ignore
|
|
94
|
-
this.webex.internal.llm.on('event:relay.event', this.eventProcessor);
|
|
95
|
-
// @ts-ignore
|
|
96
|
-
this.webex.internal.llm.on(`event:relay.event:${LLM_PRACTICE_SESSION}`, this.eventProcessor);
|
|
97
|
-
this.hasSubscribedToEvents = true;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Listen to websocket messages
|
|
139
|
+
* Deregister events and clean up
|
|
103
140
|
* @returns {void}
|
|
104
141
|
*/
|
|
105
|
-
public deregisterEvents() {
|
|
142
|
+
public deregisterEvents(): void {
|
|
106
143
|
this.areCaptionsEnabled = false;
|
|
107
144
|
this.keepTranscriptionSubscribed = false;
|
|
108
145
|
this.captionServiceId = undefined;
|
|
109
|
-
|
|
110
|
-
this.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
this.
|
|
146
|
+
this.targetLLMChannel = undefined;
|
|
147
|
+
this._pendingCaptionRestore = false;
|
|
148
|
+
|
|
149
|
+
// Remove any pending online listener
|
|
150
|
+
this.detachPendingOnline();
|
|
151
|
+
|
|
152
|
+
if (this.hasSubscribedToEvents) {
|
|
153
|
+
this.llmChannel?.off('event:relay.event', this.eventProcessor);
|
|
154
|
+
this.hasSubscribedToEvents = false;
|
|
155
|
+
}
|
|
156
|
+
|
|
114
157
|
this.announceStatus = ANNOUNCE_STATUS.IDLE;
|
|
115
158
|
this.captionStatus = TURN_ON_CAPTION_STATUS.IDLE;
|
|
116
159
|
this.toggleManualCaptionStatus = TOGGLE_MANUAL_CAPTION_STATUS.IDLE;
|
|
@@ -119,20 +162,101 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
119
162
|
}
|
|
120
163
|
|
|
121
164
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
165
|
+
* Attach a pending 'online' listener to a channel for deferred reconciliation.
|
|
166
|
+
* Detaches any existing listener first.
|
|
167
|
+
* @param {LLMChannel} channel - The channel to attach to
|
|
168
|
+
* @param {Function} callback - The callback to run when 'online' fires
|
|
169
|
+
* @private
|
|
170
|
+
* @returns {void}
|
|
124
171
|
*/
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
this.
|
|
128
|
-
this.
|
|
129
|
-
|
|
130
|
-
|
|
172
|
+
private attachPendingOnline(channel: LLMChannel, callback: () => void): void {
|
|
173
|
+
this.detachPendingOnline();
|
|
174
|
+
this._pendingOnlineListener = callback;
|
|
175
|
+
channel.once('online', this._pendingOnlineListener);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Detach any pending 'online' listener.
|
|
180
|
+
* @private
|
|
181
|
+
* @returns {void}
|
|
182
|
+
*/
|
|
183
|
+
private detachPendingOnline(): void {
|
|
184
|
+
if (this._pendingOnlineListener && this.llmChannel) {
|
|
185
|
+
this.llmChannel.off('online', this._pendingOnlineListener);
|
|
186
|
+
}
|
|
187
|
+
this._pendingOnlineListener = undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Reset announcement state for a new connection.
|
|
192
|
+
* @private
|
|
193
|
+
* @returns {void}
|
|
194
|
+
*/
|
|
195
|
+
private resetAnnounceState(): void {
|
|
131
196
|
this.announceStatus = ANNOUNCE_STATUS.IDLE;
|
|
132
197
|
this.captionStatus = TURN_ON_CAPTION_STATUS.IDLE;
|
|
133
|
-
this.
|
|
134
|
-
this.
|
|
135
|
-
|
|
198
|
+
this.captionServiceId = undefined;
|
|
199
|
+
this.areCaptionsEnabled = false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Reconcile voicea binding and caption state to match the desired target.
|
|
204
|
+
* This is idempotent - always re-reads targetLLMChannel and current state.
|
|
205
|
+
* A deferred 'online' callback that fires late self-corrects automatically.
|
|
206
|
+
* @private
|
|
207
|
+
* @returns {Promise<void>}
|
|
208
|
+
*/
|
|
209
|
+
private reconcile(): Promise<void> {
|
|
210
|
+
const target = this.targetLLMChannel;
|
|
211
|
+
if (!target) return Promise.resolve();
|
|
212
|
+
|
|
213
|
+
// 1. Rebind relay-event subscription if actual != desired
|
|
214
|
+
if (this.llmChannel !== target) {
|
|
215
|
+
if (this.hasSubscribedToEvents && this.llmChannel) {
|
|
216
|
+
this.llmChannel.off('event:relay.event', this.eventProcessor);
|
|
217
|
+
}
|
|
218
|
+
this.detachPendingOnline();
|
|
219
|
+
this.llmChannel = target;
|
|
220
|
+
this.llmChannel.on('event:relay.event', this.eventProcessor);
|
|
221
|
+
this.hasSubscribedToEvents = true;
|
|
222
|
+
this.resetAnnounceState();
|
|
223
|
+
// Mark caption restoration as pending if captions were subscribed
|
|
224
|
+
this._pendingCaptionRestore = this.keepTranscriptionSubscribed;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 2. Restore captions if pending
|
|
228
|
+
if (!this._pendingCaptionRestore) return Promise.resolve();
|
|
229
|
+
|
|
230
|
+
if (this.isLLMConnected()) {
|
|
231
|
+
// Channel is connected, restore captions immediately
|
|
232
|
+
this._pendingCaptionRestore = false;
|
|
233
|
+
|
|
234
|
+
return this.turnOnCaptions(this.currentSpokenLanguage)
|
|
235
|
+
.then(() => undefined)
|
|
236
|
+
.catch(() => {
|
|
237
|
+
// Best-effort restoration
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
// Channel not yet connected - defer until 'online' then re-reconcile.
|
|
241
|
+
// If target changes meanwhile, reconcile() self-corrects.
|
|
242
|
+
this.attachPendingOnline(target, () => {
|
|
243
|
+
this.reconcile();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return Promise.resolve();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Switch to a different LLM channel while preserving caption state.
|
|
251
|
+
* Used when transitioning between main meeting and practice session.
|
|
252
|
+
* This is a thin intent-setter - actual binding happens in reconcile().
|
|
253
|
+
* @param {LLMChannel} newLLMChannel - The new LLM channel to switch to
|
|
254
|
+
* @returns {Promise<void>}
|
|
255
|
+
*/
|
|
256
|
+
public switchLLMChannel(newLLMChannel: LLMChannel): Promise<void> {
|
|
257
|
+
this.targetLLMChannel = newLLMChannel;
|
|
258
|
+
|
|
259
|
+
return this.reconcile();
|
|
136
260
|
}
|
|
137
261
|
|
|
138
262
|
/**
|
|
@@ -145,8 +269,7 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
145
269
|
transcriptPayload.type === TRANSCRIPTION_TYPE.MANUAL_CAPTION_FINAL_RESULT ||
|
|
146
270
|
transcriptPayload.type === TRANSCRIPTION_TYPE.MANUAL_CAPTION_INTERIM_RESULT
|
|
147
271
|
) {
|
|
148
|
-
|
|
149
|
-
this.trigger(EVENT_TRIGGERS.NEW_MANUAL_CAPTION, {
|
|
272
|
+
this.emit(EVENT_TRIGGERS.NEW_MANUAL_CAPTION, {
|
|
150
273
|
isFinal: transcriptPayload.type === TRANSCRIPTION_TYPE.MANUAL_CAPTION_FINAL_RESULT,
|
|
151
274
|
transcriptId: transcriptPayload.id,
|
|
152
275
|
transcripts: transcriptPayload.transcripts,
|
|
@@ -156,6 +279,16 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
156
279
|
}
|
|
157
280
|
};
|
|
158
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Process speaker name update and send alert
|
|
284
|
+
* @param {SpeakerNameUpdatePayload} voiceaPayload
|
|
285
|
+
* @returns {void}
|
|
286
|
+
*/
|
|
287
|
+
private processSpeakerNameUpdate = (voiceaPayload: SpeakerNameUpdatePayload): void => {
|
|
288
|
+
// @ts-ignore
|
|
289
|
+
this.emit(EVENT_TRIGGERS.SPEAKER_NAME_UPDATED, voiceaPayload);
|
|
290
|
+
};
|
|
291
|
+
|
|
159
292
|
/**
|
|
160
293
|
* Process Transcript and send alert
|
|
161
294
|
* @param {TranscriptionResponse} voiceaPayload
|
|
@@ -164,8 +297,7 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
164
297
|
private processTranscription = (voiceaPayload: TranscriptionResponse): void => {
|
|
165
298
|
switch (voiceaPayload.type) {
|
|
166
299
|
case TRANSCRIPTION_TYPE.TRANSCRIPT_INTERIM_RESULTS:
|
|
167
|
-
|
|
168
|
-
this.trigger(EVENT_TRIGGERS.NEW_CAPTION, {
|
|
300
|
+
this.emit(EVENT_TRIGGERS.NEW_CAPTION, {
|
|
169
301
|
isFinal: false,
|
|
170
302
|
transcriptId: voiceaPayload.transcript_id,
|
|
171
303
|
transcripts: voiceaPayload.transcripts,
|
|
@@ -173,11 +305,10 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
173
305
|
break;
|
|
174
306
|
|
|
175
307
|
case TRANSCRIPTION_TYPE.TRANSCRIPT_FINAL_RESULT:
|
|
176
|
-
|
|
177
|
-
this.trigger(EVENT_TRIGGERS.NEW_CAPTION, {
|
|
308
|
+
this.emit(EVENT_TRIGGERS.NEW_CAPTION, {
|
|
178
309
|
isFinal: true,
|
|
179
310
|
transcriptId: voiceaPayload.transcript_id,
|
|
180
|
-
transcripts: voiceaPayload.transcripts
|
|
311
|
+
transcripts: voiceaPayload.transcripts?.map((transcript) => {
|
|
181
312
|
transcript.timestamp = millisToMinutesAndSeconds(transcript.end_millis);
|
|
182
313
|
|
|
183
314
|
return transcript;
|
|
@@ -186,20 +317,18 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
186
317
|
break;
|
|
187
318
|
|
|
188
319
|
case TRANSCRIPTION_TYPE.HIGHLIGHT_CREATED:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
timestamp: millisToMinutesAndSeconds(voiceaPayload.highlight.end_millis),
|
|
320
|
+
this.emit(EVENT_TRIGGERS.HIGHLIGHT_CREATED, {
|
|
321
|
+
csis: voiceaPayload.highlight?.csis,
|
|
322
|
+
highlightId: voiceaPayload.highlight?.highlight_id,
|
|
323
|
+
text: voiceaPayload.highlight?.transcript,
|
|
324
|
+
highlightLabel: voiceaPayload.highlight?.highlight_label,
|
|
325
|
+
highlightSource: voiceaPayload.highlight?.highlight_source,
|
|
326
|
+
timestamp: millisToMinutesAndSeconds(voiceaPayload.highlight?.end_millis ?? 0),
|
|
197
327
|
});
|
|
198
328
|
break;
|
|
199
329
|
|
|
200
330
|
case TRANSCRIPTION_TYPE.EVA_THANKS:
|
|
201
|
-
|
|
202
|
-
this.trigger(EVENT_TRIGGERS.EVA_COMMAND, {
|
|
331
|
+
this.emit(EVENT_TRIGGERS.EVA_COMMAND, {
|
|
203
332
|
isListening: false,
|
|
204
333
|
text: voiceaPayload.command_response,
|
|
205
334
|
});
|
|
@@ -207,22 +336,18 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
207
336
|
|
|
208
337
|
case TRANSCRIPTION_TYPE.EVA_WAKE:
|
|
209
338
|
case TRANSCRIPTION_TYPE.EVA_CANCEL:
|
|
210
|
-
|
|
211
|
-
this.trigger(EVENT_TRIGGERS.EVA_COMMAND, {
|
|
339
|
+
this.emit(EVENT_TRIGGERS.EVA_COMMAND, {
|
|
212
340
|
isListening: voiceaPayload.type === TRANSCRIPTION_TYPE.EVA_WAKE,
|
|
213
341
|
});
|
|
214
342
|
break;
|
|
215
343
|
|
|
216
344
|
case TRANSCRIPTION_TYPE.LANGUAGE_DETECTED: {
|
|
217
345
|
const isInSpokenLanguages = this.spokenLanguages.includes(voiceaPayload.language);
|
|
218
|
-
|
|
219
346
|
if (isInSpokenLanguages) {
|
|
220
|
-
|
|
221
|
-
this.trigger(EVENT_TRIGGERS.LANGUAGE_DETECTED, {
|
|
347
|
+
this.emit(EVENT_TRIGGERS.LANGUAGE_DETECTED, {
|
|
222
348
|
languageCode: voiceaPayload.language,
|
|
223
349
|
});
|
|
224
350
|
}
|
|
225
|
-
|
|
226
351
|
break;
|
|
227
352
|
}
|
|
228
353
|
default:
|
|
@@ -237,11 +362,9 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
237
362
|
*/
|
|
238
363
|
private processCaptionLanguageResponse = (voiceaPayload: CaptionLanguageResponse): void => {
|
|
239
364
|
if (voiceaPayload.statusCode === 200) {
|
|
240
|
-
|
|
241
|
-
this.trigger(EVENT_TRIGGERS.CAPTION_LANGUAGE_UPDATE, {statusCode: 200});
|
|
365
|
+
this.emit(EVENT_TRIGGERS.CAPTION_LANGUAGE_UPDATE, {statusCode: 200});
|
|
242
366
|
} else {
|
|
243
|
-
|
|
244
|
-
this.trigger(EVENT_TRIGGERS.CAPTION_LANGUAGE_UPDATE, {
|
|
367
|
+
this.emit(EVENT_TRIGGERS.CAPTION_LANGUAGE_UPDATE, {
|
|
245
368
|
statusCode: voiceaPayload.errorCode,
|
|
246
369
|
errorMessage: voiceaPayload.message,
|
|
247
370
|
});
|
|
@@ -262,51 +385,28 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
262
385
|
};
|
|
263
386
|
|
|
264
387
|
this.spokenLanguages = voiceaPayload?.ASR?.spoken_languages ?? [];
|
|
265
|
-
|
|
266
|
-
this.trigger(EVENT_TRIGGERS.VOICEA_ANNOUNCEMENT, voiceaLanguageOptions);
|
|
388
|
+
this.emit(EVENT_TRIGGERS.VOICEA_ANNOUNCEMENT, voiceaLanguageOptions);
|
|
267
389
|
};
|
|
268
390
|
|
|
269
391
|
/**
|
|
270
|
-
* Indicates whether the
|
|
392
|
+
* Indicates whether the LLM channel is connected.
|
|
271
393
|
* @returns {boolean}
|
|
272
394
|
*/
|
|
273
|
-
|
|
274
|
-
// @ts-ignore
|
|
275
|
-
this.webex.internal.llm.isConnected() ||
|
|
276
|
-
// @ts-ignore
|
|
277
|
-
this.webex.internal.llm.isConnected(LLM_PRACTICE_SESSION);
|
|
395
|
+
public isLLMConnected = (): boolean => this.llmChannel?.isConnected() ?? false;
|
|
278
396
|
|
|
279
397
|
public getKeepTranscriptionSubscribed = (): boolean => this.keepTranscriptionSubscribed;
|
|
280
398
|
|
|
281
|
-
/**
|
|
282
|
-
* Resolves the active LLM publish transport, preferring the practice-session
|
|
283
|
-
* connection only when that session is fully connected.
|
|
284
|
-
* @returns {Object}
|
|
285
|
-
*/
|
|
286
|
-
private getPublishTransport = () => {
|
|
287
|
-
// @ts-ignore
|
|
288
|
-
const {llm} = this.webex.internal;
|
|
289
|
-
const isPracticeSessionConnected = llm.isConnected(LLM_PRACTICE_SESSION);
|
|
290
|
-
|
|
291
|
-
return {
|
|
292
|
-
socket: (isPracticeSessionConnected && llm.getSocket(LLM_PRACTICE_SESSION)) || llm.socket,
|
|
293
|
-
binding:
|
|
294
|
-
(isPracticeSessionConnected && llm.getBinding(LLM_PRACTICE_SESSION)) || llm.getBinding(),
|
|
295
|
-
datachannelUrl:
|
|
296
|
-
(isPracticeSessionConnected && llm.getDatachannelUrl(LLM_PRACTICE_SESSION)) ||
|
|
297
|
-
llm.getDatachannelUrl(),
|
|
298
|
-
};
|
|
299
|
-
};
|
|
300
|
-
|
|
301
399
|
/**
|
|
302
400
|
* Sends Announcement to add voicea to the meeting
|
|
303
401
|
* @returns {void}
|
|
304
402
|
*/
|
|
305
|
-
|
|
403
|
+
public sendAnnouncement = (): void => {
|
|
404
|
+
const llm = this.requireLLMChannel();
|
|
306
405
|
this.announceStatus = ANNOUNCE_STATUS.JOINING;
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
|
|
406
|
+
const socket = llm.getSocket();
|
|
407
|
+
const binding = llm.getBinding();
|
|
408
|
+
|
|
409
|
+
const payload = {
|
|
310
410
|
id: `${this.seqNum}`,
|
|
311
411
|
type: 'publishRequest',
|
|
312
412
|
recipients: [
|
|
@@ -324,7 +424,8 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
324
424
|
relayType: AIBRIDGE_RELAY_TYPES.VOICEA.CLIENT_ANNOUNCEMENT,
|
|
325
425
|
},
|
|
326
426
|
trackingId: `${config.trackingIdPrefix}_${uuid.v4().toString()}`,
|
|
327
|
-
}
|
|
427
|
+
};
|
|
428
|
+
socket.send(payload);
|
|
328
429
|
this.seqNum += 1;
|
|
329
430
|
};
|
|
330
431
|
|
|
@@ -338,11 +439,9 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
338
439
|
languageCode: string,
|
|
339
440
|
languageAssignment?: 'DEFAULT' | 'AUTO' | 'MANUAL'
|
|
340
441
|
): Promise<void> =>
|
|
341
|
-
// @ts-ignore
|
|
342
442
|
this.request({
|
|
343
443
|
method: 'PUT',
|
|
344
|
-
|
|
345
|
-
url: `${this.webex.internal.llm.getLocusUrl()}/controls/`,
|
|
444
|
+
url: `${this.requireLLMChannel().getLocusUrl()}/controls/`,
|
|
346
445
|
body: {
|
|
347
446
|
transcribe: {
|
|
348
447
|
spokenLanguage: languageCode,
|
|
@@ -350,8 +449,7 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
350
449
|
},
|
|
351
450
|
},
|
|
352
451
|
}).then(() => {
|
|
353
|
-
|
|
354
|
-
this.trigger(EVENT_TRIGGERS.SPOKEN_LANGUAGE_UPDATE, {languageCode});
|
|
452
|
+
this.emit(EVENT_TRIGGERS.SPOKEN_LANGUAGE_UPDATE, {languageCode});
|
|
355
453
|
});
|
|
356
454
|
|
|
357
455
|
/**
|
|
@@ -364,7 +462,10 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
364
462
|
return;
|
|
365
463
|
}
|
|
366
464
|
|
|
367
|
-
const
|
|
465
|
+
const llm = this.requireLLMChannel();
|
|
466
|
+
const socket = llm.getSocket();
|
|
467
|
+
const binding = llm.getBinding();
|
|
468
|
+
|
|
368
469
|
socket.send({
|
|
369
470
|
id: `${this.seqNum}`,
|
|
370
471
|
type: 'publishRequest',
|
|
@@ -387,7 +488,6 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
387
488
|
trackingId: `${config.trackingIdPrefix}_${uuid.v4().toString()}`,
|
|
388
489
|
});
|
|
389
490
|
this.currentCaptionLanguage = languageCode;
|
|
390
|
-
|
|
391
491
|
this.seqNum += 1;
|
|
392
492
|
};
|
|
393
493
|
|
|
@@ -409,7 +509,9 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
409
509
|
return;
|
|
410
510
|
}
|
|
411
511
|
|
|
412
|
-
const
|
|
512
|
+
const llm = this.requireLLMChannel();
|
|
513
|
+
const socket = llm.getSocket();
|
|
514
|
+
const binding = llm.getBinding();
|
|
413
515
|
|
|
414
516
|
socket?.send({
|
|
415
517
|
id: `${this.seqNum}`,
|
|
@@ -446,38 +548,35 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
446
548
|
|
|
447
549
|
/**
|
|
448
550
|
* request turn on Captions
|
|
449
|
-
* @param {string} [languageCode] - Optional Parameter for spoken language code
|
|
551
|
+
* @param {string} [languageCode] - Optional Parameter for spoken language code
|
|
450
552
|
* @returns {Promise}
|
|
451
553
|
*/
|
|
452
|
-
private requestTurnOnCaptions = (languageCode
|
|
554
|
+
private requestTurnOnCaptions = (languageCode?: string): undefined | Promise<void> => {
|
|
453
555
|
this.captionStatus = TURN_ON_CAPTION_STATUS.SENDING;
|
|
454
556
|
|
|
455
|
-
|
|
557
|
+
const locusUrl = this.requireLLMChannel().getLocusUrl();
|
|
558
|
+
|
|
456
559
|
const body = {
|
|
457
560
|
transcribe: {caption: true},
|
|
458
561
|
languageCode,
|
|
459
562
|
};
|
|
460
563
|
|
|
461
|
-
// @ts-ignore
|
|
462
|
-
// eslint-disable-next-line newline-before-return
|
|
463
564
|
return this.request({
|
|
464
565
|
method: 'PUT',
|
|
465
|
-
|
|
466
|
-
url: `${this.webex.internal.llm.getLocusUrl()}/controls/`,
|
|
566
|
+
url: `${locusUrl}/controls/`,
|
|
467
567
|
body,
|
|
468
568
|
})
|
|
469
569
|
.then(() => {
|
|
470
|
-
|
|
471
|
-
this.trigger(EVENT_TRIGGERS.CAPTIONS_TURNED_ON);
|
|
570
|
+
this.emit(EVENT_TRIGGERS.CAPTIONS_TURNED_ON);
|
|
472
571
|
|
|
473
572
|
this.areCaptionsEnabled = true;
|
|
474
573
|
this.captionStatus = TURN_ON_CAPTION_STATUS.ENABLED;
|
|
475
574
|
this.announce();
|
|
476
575
|
this.updateSubchannelSubscriptionsAndSyncCaptionState({subscribe: ['transcription']}, true);
|
|
477
576
|
})
|
|
478
|
-
.catch(() => {
|
|
577
|
+
.catch((error) => {
|
|
479
578
|
this.captionStatus = TURN_ON_CAPTION_STATUS.IDLE;
|
|
480
|
-
throw new Error('turn on captions fail');
|
|
579
|
+
throw new Error('turn on captions fail', {cause: error});
|
|
481
580
|
});
|
|
482
581
|
};
|
|
483
582
|
|
|
@@ -485,20 +584,20 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
485
584
|
* is announce processing
|
|
486
585
|
* @returns {boolean}
|
|
487
586
|
*/
|
|
488
|
-
private isAnnounceProcessing = () =>
|
|
587
|
+
private isAnnounceProcessing = (): boolean =>
|
|
489
588
|
[ANNOUNCE_STATUS.JOINING, ANNOUNCE_STATUS.JOINED].includes(this.announceStatus);
|
|
490
589
|
|
|
491
590
|
/**
|
|
492
591
|
* is announce processed
|
|
493
592
|
* @returns {boolean}
|
|
494
593
|
*/
|
|
495
|
-
private isAnnounceProcessed = () => this.announceStatus === ANNOUNCE_STATUS.JOINED;
|
|
594
|
+
private isAnnounceProcessed = (): boolean => this.announceStatus === ANNOUNCE_STATUS.JOINED;
|
|
496
595
|
|
|
497
596
|
/**
|
|
498
|
-
* announce to voicea data
|
|
597
|
+
* announce to voicea data channel
|
|
499
598
|
* @returns {void}
|
|
500
599
|
*/
|
|
501
|
-
public announce = () => {
|
|
600
|
+
public announce = (): void => {
|
|
502
601
|
if (this.isAnnounceProcessed()) {
|
|
503
602
|
return;
|
|
504
603
|
}
|
|
@@ -512,7 +611,7 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
512
611
|
* is turn on caption processing
|
|
513
612
|
* @returns {boolean}
|
|
514
613
|
*/
|
|
515
|
-
private isCaptionProcessing = () =>
|
|
614
|
+
private isCaptionProcessing = (): boolean =>
|
|
516
615
|
[TURN_ON_CAPTION_STATUS.SENDING, TURN_ON_CAPTION_STATUS.ENABLED].includes(this.captionStatus);
|
|
517
616
|
|
|
518
617
|
/**
|
|
@@ -520,8 +619,8 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
520
619
|
* @param {string} [spokenLanguage] - Optional Spoken language code
|
|
521
620
|
* @returns {Promise}
|
|
522
621
|
*/
|
|
523
|
-
public turnOnCaptions = async (spokenLanguage
|
|
524
|
-
if (this.
|
|
622
|
+
public turnOnCaptions = async (spokenLanguage?: string): Promise<void | undefined> => {
|
|
623
|
+
if (this.isCaptionProcessing()) return undefined;
|
|
525
624
|
|
|
526
625
|
if (!this.isLLMConnected()) {
|
|
527
626
|
throw new Error('can not turn on captions before llm connected');
|
|
@@ -540,11 +639,9 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
540
639
|
activate: boolean,
|
|
541
640
|
spokenLanguage?: string
|
|
542
641
|
): undefined | Promise<void> => {
|
|
543
|
-
// @ts-ignore
|
|
544
642
|
return this.request({
|
|
545
643
|
method: 'PUT',
|
|
546
|
-
|
|
547
|
-
url: `${this.webex.internal.llm.getLocusUrl()}/controls/`,
|
|
644
|
+
url: `${this.requireLLMChannel().getLocusUrl()}/controls/`,
|
|
548
645
|
body: {
|
|
549
646
|
transcribe: {
|
|
550
647
|
transcribing: activate,
|
|
@@ -570,11 +667,9 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
570
667
|
|
|
571
668
|
this.toggleManualCaptionStatus = TOGGLE_MANUAL_CAPTION_STATUS.SENDING;
|
|
572
669
|
|
|
573
|
-
// @ts-ignore
|
|
574
670
|
return this.request({
|
|
575
671
|
method: 'PUT',
|
|
576
|
-
|
|
577
|
-
url: `${this.webex.internal.llm.getLocusUrl()}/controls/`,
|
|
672
|
+
url: `${this.requireLLMChannel().getLocusUrl()}/controls/`,
|
|
578
673
|
body: {
|
|
579
674
|
manualCaption: {
|
|
580
675
|
enable,
|
|
@@ -598,9 +693,8 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
598
693
|
* @param {string} meetingId
|
|
599
694
|
* @returns {void}
|
|
600
695
|
*/
|
|
601
|
-
public onSpokenLanguageUpdate = (languageCode: string, meetingId): void => {
|
|
602
|
-
|
|
603
|
-
this.trigger(EVENT_TRIGGERS.SPOKEN_LANGUAGE_UPDATE, {languageCode, meetingId});
|
|
696
|
+
public onSpokenLanguageUpdate = (languageCode: string, meetingId: string): void => {
|
|
697
|
+
this.emit(EVENT_TRIGGERS.SPOKEN_LANGUAGE_UPDATE, {languageCode, meetingId});
|
|
604
698
|
this.currentSpokenLanguage = languageCode;
|
|
605
699
|
};
|
|
606
700
|
|
|
@@ -615,7 +709,6 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
615
709
|
}
|
|
616
710
|
if (this.captionServiceId !== serviceId) {
|
|
617
711
|
this.captionServiceId = serviceId;
|
|
618
|
-
// if service id value has changed and the translation language has been set, client needs to resend the translator language message to the LLM.
|
|
619
712
|
if (this.currentCaptionLanguage) {
|
|
620
713
|
this.requestLanguage(this.currentCaptionLanguage);
|
|
621
714
|
}
|
|
@@ -626,19 +719,17 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
626
719
|
* get caption status
|
|
627
720
|
* @returns {string}
|
|
628
721
|
*/
|
|
629
|
-
public getCaptionStatus = () => this.captionStatus;
|
|
722
|
+
public getCaptionStatus = (): string => this.captionStatus;
|
|
630
723
|
|
|
631
724
|
/**
|
|
632
725
|
* get announce status
|
|
633
726
|
* @returns {string}
|
|
634
727
|
*/
|
|
635
|
-
public getAnnounceStatus = () => this.announceStatus;
|
|
728
|
+
public getAnnounceStatus = (): string => this.announceStatus;
|
|
729
|
+
|
|
636
730
|
/**
|
|
637
731
|
* update LLM sub‑channel subscriptions.
|
|
638
732
|
*
|
|
639
|
-
* sends a single `subchannelSubscriptionRequest` to LLM,
|
|
640
|
-
* allowing subscribe and unsubscribe subchannel.
|
|
641
|
-
*
|
|
642
733
|
* @param {string[]} options.subscribe Sub‑channels to subscribe to.
|
|
643
734
|
* @param {string[]} options.unsubscribe Sub‑channels to unsubscribe from.
|
|
644
735
|
* @returns {Promise}
|
|
@@ -650,19 +741,19 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
650
741
|
subscribe?: string[];
|
|
651
742
|
unsubscribe?: string[];
|
|
652
743
|
} = {}): Promise<void> => {
|
|
653
|
-
|
|
654
|
-
const isDataChannelTokenEnabled = await this.webex.internal.llm.isDataChannelTokenEnabled();
|
|
655
|
-
// @ts-ignore
|
|
656
|
-
if (!this.isLLMConnected() || !isDataChannelTokenEnabled) return;
|
|
744
|
+
if (!this.isLLMConnected()) return;
|
|
657
745
|
|
|
658
|
-
const
|
|
746
|
+
const llm = this.requireLLMChannel();
|
|
747
|
+
const isDataChannelTokenEnabled = await llm.isDataChannelTokenEnabled();
|
|
748
|
+
if (!isDataChannelTokenEnabled) return;
|
|
749
|
+
|
|
750
|
+
const socket = llm.getSocket();
|
|
751
|
+
const datachannelUrl = llm.getDatachannelUrl();
|
|
659
752
|
|
|
660
|
-
// @ts-ignore
|
|
661
753
|
socket.send({
|
|
662
754
|
id: `${this.seqNum}`,
|
|
663
755
|
type: 'subchannelSubscriptionRequest',
|
|
664
756
|
data: {
|
|
665
|
-
// @ts-ignore
|
|
666
757
|
datachannelUri: datachannelUrl,
|
|
667
758
|
subscribe,
|
|
668
759
|
unsubscribe,
|