@webex/internal-plugin-voicea 3.11.0 → 3.12.0-llmrefactor.2
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/index.js +13 -4
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/types/voicea-plugin.d.ts +23 -0
- package/dist/types/voicea.d.ts +74 -19
- package/dist/types/voicea.types.d.ts +1 -0
- package/dist/voicea-plugin.js +59 -0
- package/dist/voicea-plugin.js.map +1 -0
- package/dist/voicea.js +259 -133
- package/dist/voicea.js.map +1 -1
- package/dist/voicea.types.js.map +1 -1
- package/package.json +9 -9
- package/src/index.ts +5 -3
- package/src/voicea-plugin.ts +33 -0
- package/src/voicea.ts +241 -137
- package/src/voicea.types.ts +1 -0
- package/test/unit/spec/voicea-plugin.js +71 -0
- package/test/unit/spec/voicea.js +649 -842
package/src/voicea.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
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
12
|
ANNOUNCE_STATUS,
|
|
10
13
|
TURN_ON_CAPTION_STATUS,
|
|
11
14
|
TOGGLE_MANUAL_CAPTION_STATUS,
|
|
12
15
|
DEFAULT_SPOKEN_LANGUAGE,
|
|
13
|
-
LANGUAGE_ASSIGNMENT,
|
|
14
16
|
} from './constants';
|
|
15
|
-
// eslint-disable-next-line no-unused-vars
|
|
16
17
|
import {
|
|
17
18
|
AnnouncementPayload,
|
|
18
19
|
CaptionLanguageResponse,
|
|
@@ -22,39 +23,64 @@ import {
|
|
|
22
23
|
import {millisToMinutesAndSeconds} from './utils';
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
* @description VoiceaChannel
|
|
26
|
+
* @description VoiceaChannel — handles voicea/transcription functionality for a single LLM connection.
|
|
27
|
+
* Created via `webex.internal.voicea.createChannel(llmChannel)`. The caller owns the
|
|
28
|
+
* channel and is responsible for its lifecycle.
|
|
26
29
|
* @export
|
|
27
30
|
* @class VoiceaChannel
|
|
28
31
|
*/
|
|
29
|
-
export class VoiceaChannel extends
|
|
30
|
-
|
|
32
|
+
export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
33
|
+
private request: (options: {method: string; url: string; body?: object}) => Promise<any>;
|
|
34
|
+
private llmChannel: LLMChannel;
|
|
31
35
|
|
|
32
36
|
private seqNum: number;
|
|
33
|
-
|
|
34
37
|
private areCaptionsEnabled: boolean;
|
|
35
|
-
|
|
36
38
|
private hasSubscribedToEvents = false;
|
|
37
|
-
|
|
38
39
|
private captionServiceId?: string;
|
|
39
|
-
|
|
40
40
|
private announceStatus: string;
|
|
41
|
-
|
|
42
41
|
private captionStatus: string;
|
|
43
42
|
|
|
44
|
-
private
|
|
43
|
+
private keepTranscriptionSubscribed: boolean;
|
|
45
44
|
|
|
45
|
+
private toggleManualCaptionStatus: string;
|
|
46
46
|
private currentSpokenLanguage?: string;
|
|
47
|
-
|
|
48
47
|
private spokenLanguages: string[] = [];
|
|
49
|
-
|
|
50
48
|
private currentCaptionLanguage?: string;
|
|
51
49
|
|
|
52
50
|
/**
|
|
53
|
-
*
|
|
54
|
-
* @
|
|
51
|
+
* Creates a VoiceaChannel bound to the given LLMChannel
|
|
52
|
+
* @param {LLMChannel} llmChannel - The LLM channel to use
|
|
53
|
+
* @param {Function} request - The request function for making API calls (typically webex.request bound to webex)
|
|
55
54
|
*/
|
|
55
|
+
constructor(
|
|
56
|
+
llmChannel: LLMChannel,
|
|
57
|
+
request: (options: {method: string; url: string; body?: object}) => Promise<any>
|
|
58
|
+
) {
|
|
59
|
+
super();
|
|
60
|
+
this.llmChannel = llmChannel;
|
|
61
|
+
this.request = request;
|
|
62
|
+
|
|
63
|
+
this.seqNum = 1;
|
|
64
|
+
this.areCaptionsEnabled = false;
|
|
65
|
+
this.captionServiceId = undefined;
|
|
66
|
+
this.announceStatus = ANNOUNCE_STATUS.IDLE;
|
|
67
|
+
this.captionStatus = TURN_ON_CAPTION_STATUS.IDLE;
|
|
68
|
+
this.toggleManualCaptionStatus = TOGGLE_MANUAL_CAPTION_STATUS.IDLE;
|
|
69
|
+
this.currentSpokenLanguage = DEFAULT_SPOKEN_LANGUAGE;
|
|
70
|
+
this.currentCaptionLanguage = undefined;
|
|
71
|
+
this.keepTranscriptionSubscribed = false;
|
|
56
72
|
|
|
57
|
-
|
|
73
|
+
// Subscribe to relay events from the LLM channel
|
|
74
|
+
this.llmChannel.on('event:relay.event', this.eventProcessor);
|
|
75
|
+
this.hasSubscribedToEvents = true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Process events from LLM channel
|
|
80
|
+
* @param {Object} e - Event data
|
|
81
|
+
* @returns {void}
|
|
82
|
+
*/
|
|
83
|
+
private eventProcessor = (e: any): void => {
|
|
58
84
|
this.seqNum = e.sequenceNumber + 1;
|
|
59
85
|
switch (e.data.relayType) {
|
|
60
86
|
case AIBRIDGE_RELAY_TYPES.VOICEA.ANNOUNCEMENT:
|
|
@@ -82,27 +108,19 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
82
108
|
};
|
|
83
109
|
|
|
84
110
|
/**
|
|
85
|
-
*
|
|
86
|
-
* @returns {undefined}
|
|
87
|
-
*/
|
|
88
|
-
private listenToEvents() {
|
|
89
|
-
if (!this.hasSubscribedToEvents) {
|
|
90
|
-
// @ts-ignore
|
|
91
|
-
this.webex.internal.llm.on('event:relay.event', this.eventProcessor);
|
|
92
|
-
this.hasSubscribedToEvents = true;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Listen to websocket messages
|
|
111
|
+
* Deregister events and clean up
|
|
98
112
|
* @returns {void}
|
|
99
113
|
*/
|
|
100
|
-
public deregisterEvents() {
|
|
114
|
+
public deregisterEvents(): void {
|
|
101
115
|
this.areCaptionsEnabled = false;
|
|
116
|
+
this.keepTranscriptionSubscribed = false;
|
|
102
117
|
this.captionServiceId = undefined;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
118
|
+
|
|
119
|
+
if (this.hasSubscribedToEvents) {
|
|
120
|
+
this.llmChannel.off('event:relay.event', this.eventProcessor);
|
|
121
|
+
this.hasSubscribedToEvents = false;
|
|
122
|
+
}
|
|
123
|
+
|
|
106
124
|
this.announceStatus = ANNOUNCE_STATUS.IDLE;
|
|
107
125
|
this.captionStatus = TURN_ON_CAPTION_STATUS.IDLE;
|
|
108
126
|
this.toggleManualCaptionStatus = TOGGLE_MANUAL_CAPTION_STATUS.IDLE;
|
|
@@ -111,19 +129,42 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
111
129
|
}
|
|
112
130
|
|
|
113
131
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
132
|
+
* Switch to a different LLM channel while preserving caption state.
|
|
133
|
+
* Used when transitioning between main meeting and practice session.
|
|
134
|
+
* - Preserves keepTranscriptionSubscribed and spokenLanguage state
|
|
135
|
+
* - Unsubscribes from old channel, subscribes to new channel
|
|
136
|
+
* - Re-announces and re-enables captions if they were on
|
|
137
|
+
* @param {LLMChannel} newLLMChannel - The new LLM channel to switch to
|
|
138
|
+
* @returns {Promise<void>}
|
|
116
139
|
*/
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
140
|
+
public async switchLLMChannel(newLLMChannel: LLMChannel): Promise<void> {
|
|
141
|
+
// Save current state
|
|
142
|
+
const captionsWereOn = this.keepTranscriptionSubscribed;
|
|
143
|
+
const spokenLanguage = this.currentSpokenLanguage;
|
|
144
|
+
|
|
145
|
+
// Unsubscribe from old channel
|
|
146
|
+
if (this.hasSubscribedToEvents && this.llmChannel) {
|
|
147
|
+
this.llmChannel.off('event:relay.event', this.eventProcessor);
|
|
148
|
+
this.hasSubscribedToEvents = false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Switch to new channel
|
|
152
|
+
this.llmChannel = newLLMChannel;
|
|
153
|
+
|
|
154
|
+
// Subscribe to new channel
|
|
155
|
+
this.llmChannel.on('event:relay.event', this.eventProcessor);
|
|
156
|
+
this.hasSubscribedToEvents = true;
|
|
157
|
+
|
|
158
|
+
// Reset announcement state for new connection
|
|
122
159
|
this.announceStatus = ANNOUNCE_STATUS.IDLE;
|
|
123
160
|
this.captionStatus = TURN_ON_CAPTION_STATUS.IDLE;
|
|
124
|
-
this.
|
|
125
|
-
this.
|
|
126
|
-
|
|
161
|
+
this.captionServiceId = undefined;
|
|
162
|
+
this.areCaptionsEnabled = false;
|
|
163
|
+
|
|
164
|
+
// Re-announce and re-enable captions if they were on
|
|
165
|
+
if (captionsWereOn) {
|
|
166
|
+
await this.turnOnCaptions(spokenLanguage);
|
|
167
|
+
}
|
|
127
168
|
}
|
|
128
169
|
|
|
129
170
|
/**
|
|
@@ -136,8 +177,7 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
136
177
|
transcriptPayload.type === TRANSCRIPTION_TYPE.MANUAL_CAPTION_FINAL_RESULT ||
|
|
137
178
|
transcriptPayload.type === TRANSCRIPTION_TYPE.MANUAL_CAPTION_INTERIM_RESULT
|
|
138
179
|
) {
|
|
139
|
-
|
|
140
|
-
this.trigger(EVENT_TRIGGERS.NEW_MANUAL_CAPTION, {
|
|
180
|
+
this.emit(EVENT_TRIGGERS.NEW_MANUAL_CAPTION, {
|
|
141
181
|
isFinal: transcriptPayload.type === TRANSCRIPTION_TYPE.MANUAL_CAPTION_FINAL_RESULT,
|
|
142
182
|
transcriptId: transcriptPayload.id,
|
|
143
183
|
transcripts: transcriptPayload.transcripts,
|
|
@@ -155,8 +195,7 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
155
195
|
private processTranscription = (voiceaPayload: TranscriptionResponse): void => {
|
|
156
196
|
switch (voiceaPayload.type) {
|
|
157
197
|
case TRANSCRIPTION_TYPE.TRANSCRIPT_INTERIM_RESULTS:
|
|
158
|
-
|
|
159
|
-
this.trigger(EVENT_TRIGGERS.NEW_CAPTION, {
|
|
198
|
+
this.emit(EVENT_TRIGGERS.NEW_CAPTION, {
|
|
160
199
|
isFinal: false,
|
|
161
200
|
transcriptId: voiceaPayload.transcript_id,
|
|
162
201
|
transcripts: voiceaPayload.transcripts,
|
|
@@ -164,11 +203,10 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
164
203
|
break;
|
|
165
204
|
|
|
166
205
|
case TRANSCRIPTION_TYPE.TRANSCRIPT_FINAL_RESULT:
|
|
167
|
-
|
|
168
|
-
this.trigger(EVENT_TRIGGERS.NEW_CAPTION, {
|
|
206
|
+
this.emit(EVENT_TRIGGERS.NEW_CAPTION, {
|
|
169
207
|
isFinal: true,
|
|
170
208
|
transcriptId: voiceaPayload.transcript_id,
|
|
171
|
-
transcripts: voiceaPayload.transcripts
|
|
209
|
+
transcripts: voiceaPayload.transcripts?.map((transcript) => {
|
|
172
210
|
transcript.timestamp = millisToMinutesAndSeconds(transcript.end_millis);
|
|
173
211
|
|
|
174
212
|
return transcript;
|
|
@@ -177,20 +215,18 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
177
215
|
break;
|
|
178
216
|
|
|
179
217
|
case TRANSCRIPTION_TYPE.HIGHLIGHT_CREATED:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
timestamp: millisToMinutesAndSeconds(voiceaPayload.highlight.end_millis),
|
|
218
|
+
this.emit(EVENT_TRIGGERS.HIGHLIGHT_CREATED, {
|
|
219
|
+
csis: voiceaPayload.highlight?.csis,
|
|
220
|
+
highlightId: voiceaPayload.highlight?.highlight_id,
|
|
221
|
+
text: voiceaPayload.highlight?.transcript,
|
|
222
|
+
highlightLabel: voiceaPayload.highlight?.highlight_label,
|
|
223
|
+
highlightSource: voiceaPayload.highlight?.highlight_source,
|
|
224
|
+
timestamp: millisToMinutesAndSeconds(voiceaPayload.highlight?.end_millis ?? 0),
|
|
188
225
|
});
|
|
189
226
|
break;
|
|
190
227
|
|
|
191
228
|
case TRANSCRIPTION_TYPE.EVA_THANKS:
|
|
192
|
-
|
|
193
|
-
this.trigger(EVENT_TRIGGERS.EVA_COMMAND, {
|
|
229
|
+
this.emit(EVENT_TRIGGERS.EVA_COMMAND, {
|
|
194
230
|
isListening: false,
|
|
195
231
|
text: voiceaPayload.command_response,
|
|
196
232
|
});
|
|
@@ -198,22 +234,18 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
198
234
|
|
|
199
235
|
case TRANSCRIPTION_TYPE.EVA_WAKE:
|
|
200
236
|
case TRANSCRIPTION_TYPE.EVA_CANCEL:
|
|
201
|
-
|
|
202
|
-
this.trigger(EVENT_TRIGGERS.EVA_COMMAND, {
|
|
237
|
+
this.emit(EVENT_TRIGGERS.EVA_COMMAND, {
|
|
203
238
|
isListening: voiceaPayload.type === TRANSCRIPTION_TYPE.EVA_WAKE,
|
|
204
239
|
});
|
|
205
240
|
break;
|
|
206
241
|
|
|
207
242
|
case TRANSCRIPTION_TYPE.LANGUAGE_DETECTED: {
|
|
208
243
|
const isInSpokenLanguages = this.spokenLanguages.includes(voiceaPayload.language);
|
|
209
|
-
|
|
210
244
|
if (isInSpokenLanguages) {
|
|
211
|
-
|
|
212
|
-
this.trigger(EVENT_TRIGGERS.LANGUAGE_DETECTED, {
|
|
245
|
+
this.emit(EVENT_TRIGGERS.LANGUAGE_DETECTED, {
|
|
213
246
|
languageCode: voiceaPayload.language,
|
|
214
247
|
});
|
|
215
248
|
}
|
|
216
|
-
|
|
217
249
|
break;
|
|
218
250
|
}
|
|
219
251
|
default:
|
|
@@ -228,11 +260,9 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
228
260
|
*/
|
|
229
261
|
private processCaptionLanguageResponse = (voiceaPayload: CaptionLanguageResponse): void => {
|
|
230
262
|
if (voiceaPayload.statusCode === 200) {
|
|
231
|
-
|
|
232
|
-
this.trigger(EVENT_TRIGGERS.CAPTION_LANGUAGE_UPDATE, {statusCode: 200});
|
|
263
|
+
this.emit(EVENT_TRIGGERS.CAPTION_LANGUAGE_UPDATE, {statusCode: 200});
|
|
233
264
|
} else {
|
|
234
|
-
|
|
235
|
-
this.trigger(EVENT_TRIGGERS.CAPTION_LANGUAGE_UPDATE, {
|
|
265
|
+
this.emit(EVENT_TRIGGERS.CAPTION_LANGUAGE_UPDATE, {
|
|
236
266
|
statusCode: voiceaPayload.errorCode,
|
|
237
267
|
errorMessage: voiceaPayload.message,
|
|
238
268
|
});
|
|
@@ -253,25 +283,34 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
253
283
|
};
|
|
254
284
|
|
|
255
285
|
this.spokenLanguages = voiceaPayload?.ASR?.spoken_languages ?? [];
|
|
256
|
-
|
|
257
|
-
this.trigger(EVENT_TRIGGERS.VOICEA_ANNOUNCEMENT, voiceaLanguageOptions);
|
|
286
|
+
this.emit(EVENT_TRIGGERS.VOICEA_ANNOUNCEMENT, voiceaLanguageOptions);
|
|
258
287
|
};
|
|
259
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Indicates whether the LLM channel is connected.
|
|
291
|
+
* @returns {boolean}
|
|
292
|
+
*/
|
|
293
|
+
public isLLMConnected = (): boolean => this.llmChannel.isConnected();
|
|
294
|
+
|
|
295
|
+
public getKeepTranscriptionSubscribed = (): boolean => this.keepTranscriptionSubscribed;
|
|
296
|
+
|
|
260
297
|
/**
|
|
261
298
|
* Sends Announcement to add voicea to the meeting
|
|
262
299
|
* @returns {void}
|
|
263
300
|
*/
|
|
264
|
-
|
|
301
|
+
public sendAnnouncement = (): void => {
|
|
265
302
|
this.announceStatus = ANNOUNCE_STATUS.JOINING;
|
|
266
|
-
this.
|
|
267
|
-
|
|
268
|
-
|
|
303
|
+
const socket = this.llmChannel.getSocket();
|
|
304
|
+
const binding = this.llmChannel.getBinding();
|
|
305
|
+
|
|
306
|
+
const payload = {
|
|
269
307
|
id: `${this.seqNum}`,
|
|
270
308
|
type: 'publishRequest',
|
|
271
|
-
recipients:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
309
|
+
recipients: [
|
|
310
|
+
{
|
|
311
|
+
route: binding,
|
|
312
|
+
},
|
|
313
|
+
],
|
|
275
314
|
// If captionServiceId exists, send it as the 'to' header; otherwise keep headers empty.
|
|
276
315
|
headers: this.captionServiceId ? {to: this.captionServiceId} : {},
|
|
277
316
|
data: {
|
|
@@ -282,7 +321,8 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
282
321
|
relayType: AIBRIDGE_RELAY_TYPES.VOICEA.CLIENT_ANNOUNCEMENT,
|
|
283
322
|
},
|
|
284
323
|
trackingId: `${config.trackingIdPrefix}_${uuid.v4().toString()}`,
|
|
285
|
-
}
|
|
324
|
+
};
|
|
325
|
+
socket.send(payload);
|
|
286
326
|
this.seqNum += 1;
|
|
287
327
|
};
|
|
288
328
|
|
|
@@ -296,11 +336,9 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
296
336
|
languageCode: string,
|
|
297
337
|
languageAssignment?: 'DEFAULT' | 'AUTO' | 'MANUAL'
|
|
298
338
|
): Promise<void> =>
|
|
299
|
-
// @ts-ignore
|
|
300
339
|
this.request({
|
|
301
340
|
method: 'PUT',
|
|
302
|
-
|
|
303
|
-
url: `${this.webex.internal.llm.getLocusUrl()}/controls/`,
|
|
341
|
+
url: `${this.llmChannel.getLocusUrl()}/controls/`,
|
|
304
342
|
body: {
|
|
305
343
|
transcribe: {
|
|
306
344
|
spokenLanguage: languageCode,
|
|
@@ -308,8 +346,7 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
308
346
|
},
|
|
309
347
|
},
|
|
310
348
|
}).then(() => {
|
|
311
|
-
|
|
312
|
-
this.trigger(EVENT_TRIGGERS.SPOKEN_LANGUAGE_UPDATE, {languageCode});
|
|
349
|
+
this.emit(EVENT_TRIGGERS.SPOKEN_LANGUAGE_UPDATE, {languageCode});
|
|
313
350
|
});
|
|
314
351
|
|
|
315
352
|
/**
|
|
@@ -318,16 +355,21 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
318
355
|
* @returns {void}
|
|
319
356
|
*/
|
|
320
357
|
public requestLanguage = (languageCode: string): void => {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
358
|
+
if (!this.isLLMConnected()) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const socket = this.llmChannel.getSocket();
|
|
363
|
+
const binding = this.llmChannel.getBinding();
|
|
364
|
+
|
|
365
|
+
socket.send({
|
|
325
366
|
id: `${this.seqNum}`,
|
|
326
367
|
type: 'publishRequest',
|
|
327
|
-
recipients:
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
368
|
+
recipients: [
|
|
369
|
+
{
|
|
370
|
+
route: binding,
|
|
371
|
+
},
|
|
372
|
+
],
|
|
331
373
|
headers: {
|
|
332
374
|
to: this.captionServiceId,
|
|
333
375
|
},
|
|
@@ -342,7 +384,6 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
342
384
|
trackingId: `${config.trackingIdPrefix}_${uuid.v4().toString()}`,
|
|
343
385
|
});
|
|
344
386
|
this.currentCaptionLanguage = languageCode;
|
|
345
|
-
|
|
346
387
|
this.seqNum += 1;
|
|
347
388
|
};
|
|
348
389
|
|
|
@@ -360,17 +401,21 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
360
401
|
csis: number[],
|
|
361
402
|
isFinal: boolean
|
|
362
403
|
): void => {
|
|
363
|
-
|
|
364
|
-
|
|
404
|
+
if (!this.isLLMConnected()) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const socket = this.llmChannel.getSocket();
|
|
409
|
+
const binding = this.llmChannel.getBinding();
|
|
365
410
|
|
|
366
|
-
|
|
367
|
-
this.webex.internal.llm.socket.send({
|
|
411
|
+
socket?.send({
|
|
368
412
|
id: `${this.seqNum}`,
|
|
369
413
|
type: 'publishRequest',
|
|
370
|
-
recipients:
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
414
|
+
recipients: [
|
|
415
|
+
{
|
|
416
|
+
route: binding,
|
|
417
|
+
},
|
|
418
|
+
],
|
|
374
419
|
headers: {},
|
|
375
420
|
data: {
|
|
376
421
|
eventType: 'relay.event',
|
|
@@ -398,35 +443,33 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
398
443
|
|
|
399
444
|
/**
|
|
400
445
|
* request turn on Captions
|
|
401
|
-
* @param {string} [languageCode] - Optional Parameter for spoken language code
|
|
446
|
+
* @param {string} [languageCode] - Optional Parameter for spoken language code
|
|
402
447
|
* @returns {Promise}
|
|
403
448
|
*/
|
|
404
|
-
private requestTurnOnCaptions = (languageCode
|
|
449
|
+
private requestTurnOnCaptions = (languageCode?: string): undefined | Promise<void> => {
|
|
405
450
|
this.captionStatus = TURN_ON_CAPTION_STATUS.SENDING;
|
|
406
451
|
|
|
407
|
-
|
|
452
|
+
const locusUrl = this.llmChannel.getLocusUrl();
|
|
453
|
+
|
|
408
454
|
const body = {
|
|
409
455
|
transcribe: {caption: true},
|
|
410
456
|
languageCode,
|
|
411
457
|
};
|
|
412
458
|
|
|
413
|
-
// @ts-ignore
|
|
414
|
-
// eslint-disable-next-line newline-before-return
|
|
415
459
|
return this.request({
|
|
416
460
|
method: 'PUT',
|
|
417
|
-
|
|
418
|
-
url: `${this.webex.internal.llm.getLocusUrl()}/controls/`,
|
|
461
|
+
url: `${locusUrl}/controls/`,
|
|
419
462
|
body,
|
|
420
463
|
})
|
|
421
464
|
.then(() => {
|
|
422
|
-
|
|
423
|
-
this.trigger(EVENT_TRIGGERS.CAPTIONS_TURNED_ON);
|
|
465
|
+
this.emit(EVENT_TRIGGERS.CAPTIONS_TURNED_ON);
|
|
424
466
|
|
|
425
467
|
this.areCaptionsEnabled = true;
|
|
426
468
|
this.captionStatus = TURN_ON_CAPTION_STATUS.ENABLED;
|
|
427
469
|
this.announce();
|
|
470
|
+
this.updateSubchannelSubscriptionsAndSyncCaptionState({subscribe: ['transcription']}, true);
|
|
428
471
|
})
|
|
429
|
-
.catch(() => {
|
|
472
|
+
.catch((error) => {
|
|
430
473
|
this.captionStatus = TURN_ON_CAPTION_STATUS.IDLE;
|
|
431
474
|
throw new Error('turn on captions fail');
|
|
432
475
|
});
|
|
@@ -436,17 +479,24 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
436
479
|
* is announce processing
|
|
437
480
|
* @returns {boolean}
|
|
438
481
|
*/
|
|
439
|
-
private isAnnounceProcessing = () =>
|
|
482
|
+
private isAnnounceProcessing = (): boolean =>
|
|
440
483
|
[ANNOUNCE_STATUS.JOINING, ANNOUNCE_STATUS.JOINED].includes(this.announceStatus);
|
|
441
484
|
|
|
442
485
|
/**
|
|
443
|
-
* announce
|
|
486
|
+
* is announce processed
|
|
487
|
+
* @returns {boolean}
|
|
488
|
+
*/
|
|
489
|
+
private isAnnounceProcessed = (): boolean => this.announceStatus === ANNOUNCE_STATUS.JOINED;
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* announce to voicea data channel
|
|
444
493
|
* @returns {void}
|
|
445
494
|
*/
|
|
446
|
-
public announce = () => {
|
|
447
|
-
if (this.
|
|
448
|
-
|
|
449
|
-
|
|
495
|
+
public announce = (): void => {
|
|
496
|
+
if (this.isAnnounceProcessed()) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (!this.isLLMConnected()) {
|
|
450
500
|
throw new Error('voicea can not announce before llm connected');
|
|
451
501
|
}
|
|
452
502
|
this.sendAnnouncement();
|
|
@@ -456,7 +506,7 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
456
506
|
* is turn on caption processing
|
|
457
507
|
* @returns {boolean}
|
|
458
508
|
*/
|
|
459
|
-
private isCaptionProcessing = () =>
|
|
509
|
+
private isCaptionProcessing = (): boolean =>
|
|
460
510
|
[TURN_ON_CAPTION_STATUS.SENDING, TURN_ON_CAPTION_STATUS.ENABLED].includes(this.captionStatus);
|
|
461
511
|
|
|
462
512
|
/**
|
|
@@ -464,10 +514,10 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
464
514
|
* @param {string} [spokenLanguage] - Optional Spoken language code
|
|
465
515
|
* @returns {Promise}
|
|
466
516
|
*/
|
|
467
|
-
public turnOnCaptions = async (spokenLanguage
|
|
517
|
+
public turnOnCaptions = async (spokenLanguage?: string): Promise<void | undefined> => {
|
|
468
518
|
if (this.captionStatus === TURN_ON_CAPTION_STATUS.SENDING) return undefined;
|
|
469
|
-
|
|
470
|
-
if (!this.
|
|
519
|
+
|
|
520
|
+
if (!this.isLLMConnected()) {
|
|
471
521
|
throw new Error('can not turn on captions before llm connected');
|
|
472
522
|
}
|
|
473
523
|
|
|
@@ -484,11 +534,9 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
484
534
|
activate: boolean,
|
|
485
535
|
spokenLanguage?: string
|
|
486
536
|
): undefined | Promise<void> => {
|
|
487
|
-
// @ts-ignore
|
|
488
537
|
return this.request({
|
|
489
538
|
method: 'PUT',
|
|
490
|
-
|
|
491
|
-
url: `${this.webex.internal.llm.getLocusUrl()}/controls/`,
|
|
539
|
+
url: `${this.llmChannel.getLocusUrl()}/controls/`,
|
|
492
540
|
body: {
|
|
493
541
|
transcribe: {
|
|
494
542
|
transcribing: activate,
|
|
@@ -514,11 +562,9 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
514
562
|
|
|
515
563
|
this.toggleManualCaptionStatus = TOGGLE_MANUAL_CAPTION_STATUS.SENDING;
|
|
516
564
|
|
|
517
|
-
// @ts-ignore
|
|
518
565
|
return this.request({
|
|
519
566
|
method: 'PUT',
|
|
520
|
-
|
|
521
|
-
url: `${this.webex.internal.llm.getLocusUrl()}/controls/`,
|
|
567
|
+
url: `${this.llmChannel.getLocusUrl()}/controls/`,
|
|
522
568
|
body: {
|
|
523
569
|
manualCaption: {
|
|
524
570
|
enable,
|
|
@@ -542,9 +588,8 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
542
588
|
* @param {string} meetingId
|
|
543
589
|
* @returns {void}
|
|
544
590
|
*/
|
|
545
|
-
public onSpokenLanguageUpdate = (languageCode: string, meetingId): void => {
|
|
546
|
-
|
|
547
|
-
this.trigger(EVENT_TRIGGERS.SPOKEN_LANGUAGE_UPDATE, {languageCode, meetingId});
|
|
591
|
+
public onSpokenLanguageUpdate = (languageCode: string, meetingId: string): void => {
|
|
592
|
+
this.emit(EVENT_TRIGGERS.SPOKEN_LANGUAGE_UPDATE, {languageCode, meetingId});
|
|
548
593
|
this.currentSpokenLanguage = languageCode;
|
|
549
594
|
};
|
|
550
595
|
|
|
@@ -559,7 +604,6 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
559
604
|
}
|
|
560
605
|
if (this.captionServiceId !== serviceId) {
|
|
561
606
|
this.captionServiceId = serviceId;
|
|
562
|
-
// if service id value has changed and the translation language has been set, client needs to resend the translator language message to the LLM.
|
|
563
607
|
if (this.currentCaptionLanguage) {
|
|
564
608
|
this.requestLanguage(this.currentCaptionLanguage);
|
|
565
609
|
}
|
|
@@ -570,13 +614,73 @@ export class VoiceaChannel extends WebexPlugin implements IVoiceaChannel {
|
|
|
570
614
|
* get caption status
|
|
571
615
|
* @returns {string}
|
|
572
616
|
*/
|
|
573
|
-
public getCaptionStatus = () => this.captionStatus;
|
|
617
|
+
public getCaptionStatus = (): string => this.captionStatus;
|
|
574
618
|
|
|
575
619
|
/**
|
|
576
620
|
* get announce status
|
|
577
621
|
* @returns {string}
|
|
578
622
|
*/
|
|
579
|
-
public getAnnounceStatus = () => this.announceStatus;
|
|
623
|
+
public getAnnounceStatus = (): string => this.announceStatus;
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* update LLM sub‑channel subscriptions.
|
|
627
|
+
*
|
|
628
|
+
* @param {string[]} options.subscribe Sub‑channels to subscribe to.
|
|
629
|
+
* @param {string[]} options.unsubscribe Sub‑channels to unsubscribe from.
|
|
630
|
+
* @returns {Promise}
|
|
631
|
+
*/
|
|
632
|
+
public updateSubchannelSubscriptions = async ({
|
|
633
|
+
subscribe = [],
|
|
634
|
+
unsubscribe = [],
|
|
635
|
+
}: {
|
|
636
|
+
subscribe?: string[];
|
|
637
|
+
unsubscribe?: string[];
|
|
638
|
+
} = {}): Promise<void> => {
|
|
639
|
+
if (!this.isLLMConnected()) return;
|
|
640
|
+
|
|
641
|
+
const isDataChannelTokenEnabled = await this.llmChannel.isDataChannelTokenEnabled();
|
|
642
|
+
if (!isDataChannelTokenEnabled) return;
|
|
643
|
+
|
|
644
|
+
const socket = this.llmChannel.getSocket();
|
|
645
|
+
const datachannelUrl = this.llmChannel.getDatachannelUrl();
|
|
646
|
+
|
|
647
|
+
socket.send({
|
|
648
|
+
id: `${this.seqNum}`,
|
|
649
|
+
type: 'subchannelSubscriptionRequest',
|
|
650
|
+
data: {
|
|
651
|
+
datachannelUri: datachannelUrl,
|
|
652
|
+
subscribe,
|
|
653
|
+
unsubscribe,
|
|
654
|
+
},
|
|
655
|
+
trackingId: `${config.trackingIdPrefix}_${uuid.v4().toString()}`,
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
this.seqNum += 1;
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Updates transcription subchannel subscriptions and records whether the
|
|
663
|
+
* transcription subscription should be kept (and restored on reconnect).
|
|
664
|
+
*
|
|
665
|
+
* @param {Object} [options] - Subscription options.
|
|
666
|
+
* @param {string[]} [options.subscribe] - Subchannels to subscribe to.
|
|
667
|
+
* @param {string[]} [options.unsubscribe] - Subchannels to unsubscribe from.
|
|
668
|
+
* @param {boolean} [keepSubscribed=false] - Whether the transcription
|
|
669
|
+
* subscription should be kept and restored on reconnect.
|
|
670
|
+
*
|
|
671
|
+
* @returns {Promise<void>}
|
|
672
|
+
*/
|
|
673
|
+
public updateSubchannelSubscriptionsAndSyncCaptionState = (
|
|
674
|
+
options: {
|
|
675
|
+
subscribe?: string[];
|
|
676
|
+
unsubscribe?: string[];
|
|
677
|
+
} = {},
|
|
678
|
+
keepSubscribed = false
|
|
679
|
+
): Promise<void> => {
|
|
680
|
+
this.keepTranscriptionSubscribed = keepSubscribed;
|
|
681
|
+
|
|
682
|
+
return this.updateSubchannelSubscriptions(options);
|
|
683
|
+
};
|
|
580
684
|
}
|
|
581
685
|
|
|
582
686
|
export default VoiceaChannel;
|