@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/src/voicea.ts CHANGED
@@ -1,63 +1,111 @@
1
+ import EventEmitter from 'events';
1
2
  import uuid from 'uuid';
2
- import {WebexPlugin, config} from '@webex/webex-core';
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 to hold single instance of LLM
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 WebexPlugin implements IVoiceaChannel {
31
- namespace = VOICEA;
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
- * @param {Object} e
57
- * @returns {undefined}
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
- private eventProcessor = (e) => {
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
- * Listen to websocket messages
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
- // @ts-ignore
110
- this.webex.internal.llm.off('event:relay.event', this.eventProcessor);
111
- // @ts-ignore
112
- this.webex.internal.llm.off(`event:relay.event:${LLM_PRACTICE_SESSION}`, this.eventProcessor);
113
- this.hasSubscribedToEvents = false;
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
- * Initializes Voicea plugin
123
- * @param {any} args
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
- constructor(...args) {
126
- super(...args);
127
- this.seqNum = 1;
128
- this.areCaptionsEnabled = false;
129
- this.keepTranscriptionSubscribed = false;
130
- this.captionServiceId = undefined;
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.toggleManualCaptionStatus = TOGGLE_MANUAL_CAPTION_STATUS.IDLE;
134
- this.currentSpokenLanguage = DEFAULT_SPOKEN_LANGUAGE;
135
- this.currentCaptionLanguage = undefined;
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
- // @ts-ignore
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
- // @ts-ignore
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
- // @ts-ignore
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.map((transcript) => {
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
- // @ts-ignore
190
- this.trigger(EVENT_TRIGGERS.HIGHLIGHT_CREATED, {
191
- csis: voiceaPayload.highlight.csis,
192
- highlightId: voiceaPayload.highlight.highlight_id,
193
- text: voiceaPayload.highlight.transcript,
194
- highlightLabel: voiceaPayload.highlight.highlight_label,
195
- highlightSource: voiceaPayload.highlight.highlight_source,
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
- // @ts-ignore
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
- // @ts-ignore
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
- // @ts-ignore
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
- // @ts-ignore
241
- this.trigger(EVENT_TRIGGERS.CAPTION_LANGUAGE_UPDATE, {statusCode: 200});
365
+ this.emit(EVENT_TRIGGERS.CAPTION_LANGUAGE_UPDATE, {statusCode: 200});
242
366
  } else {
243
- // @ts-ignore
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
- // @ts-ignore
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 default or practice-session LLM connection is active.
392
+ * Indicates whether the LLM channel is connected.
271
393
  * @returns {boolean}
272
394
  */
273
- private isLLMConnected = (): boolean =>
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
- private sendAnnouncement = (): void => {
403
+ public sendAnnouncement = (): void => {
404
+ const llm = this.requireLLMChannel();
306
405
  this.announceStatus = ANNOUNCE_STATUS.JOINING;
307
- this.listenToEvents();
308
- const {socket, binding} = this.getPublishTransport();
309
- socket.send({
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
- // @ts-ignore
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
- // @ts-ignore
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 {socket, binding} = this.getPublishTransport();
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 {socket, binding} = this.getPublishTransport();
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. Defaults to English
551
+ * @param {string} [languageCode] - Optional Parameter for spoken language code
450
552
  * @returns {Promise}
451
553
  */
452
- private requestTurnOnCaptions = (languageCode?): undefined | Promise<void> => {
554
+ private requestTurnOnCaptions = (languageCode?: string): undefined | Promise<void> => {
453
555
  this.captionStatus = TURN_ON_CAPTION_STATUS.SENDING;
454
556
 
455
- // only set the spoken language if it is provided
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
- // @ts-ignore
466
- url: `${this.webex.internal.llm.getLocusUrl()}/controls/`,
566
+ url: `${locusUrl}/controls/`,
467
567
  body,
468
568
  })
469
569
  .then(() => {
470
- // @ts-ignore
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 chanel
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?): undefined | Promise<void> => {
524
- if (this.captionStatus === TURN_ON_CAPTION_STATUS.SENDING) return undefined;
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
- // @ts-ignore
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
- // @ts-ignore
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
- // @ts-ignore
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
- // @ts-ignore
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 {socket, datachannelUrl} = this.getPublishTransport();
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,