@webex/internal-plugin-voicea 3.12.0-llmrefactor.1 → 3.12.0-llmrefactor.11

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.
@@ -1 +1 @@
1
- {"version":3,"names":[],"sources":["voicea.types.ts"],"sourcesContent":["/* eslint-disable camelcase */\n/* eslint-disable no-undef */\n\n/**\n * Type for payload\n */\ninterface AnnouncementPayload {\n translation: {\n max_languages: number;\n allowed_languages: string[];\n };\n\n ASR: {\n spoken_languages: string[];\n };\n}\n\n// eslint-disable-next-line no-undef\ntype TRANSCRIPTION_TYPES =\n | 'unknown'\n | 'eva_wake'\n | 'eva_thanks'\n | 'eva_cancel'\n | 'highlight_created'\n | 'transcript_interim_results'\n | 'transcript_final_result'\n | 'manual_caption_interim_results'\n | 'manual_caption_interim_result'\n | 'manual_caption_final_result';\n\n/**\n * Class for an Transcription Object\n */\ninterface Transcription {\n start_millis: number;\n end_millis: number;\n text: string;\n transcript_language_code: string;\n translations: {[x: string]: string};\n csis: number[];\n last_packet_timestamp_ms: number;\n timestamp: string;\n}\n\n/**\n * Highlights\n */\ninterface Highlight {\n highlight_id: string;\n transcript: string;\n highlight_label: string;\n highlight_source: string;\n start_millis: number;\n end_millis: number;\n csis: number[];\n}\n/**\n * Type for Transcription message\n */\ninterface TranscriptionResponse {\n type: TRANSCRIPTION_TYPES;\n id: string;\n sender: string;\n data_source: string;\n transcript_id: string;\n translations?: {[x: string]: string};\n transcripts?: Transcription[];\n transcript?: Transcription;\n highlight?: Highlight;\n csis: number[];\n data: string;\n command_response: string;\n language: string;\n}\n/**\n * Type for CaptionLanguageResponse\n */\ninterface CaptionLanguageResponse {\n requestId: string;\n statusCode: number;\n errorCode: number;\n message: string;\n}\n\ninterface IVoiceaChannel {\n setSpokenLanguage: (languageCode: string) => Promise<void>;\n onSpokenLanguageUpdate: (languageCode: string, meetingId: string) => void;\n requestLanguage: (languageCode: string) => void;\n turnOnCaptions: () => undefined | Promise<void>;\n toggleTranscribing: (activate: boolean, spokenLanguage: string) => undefined | Promise<void>;\n deregisterEvents: () => void;\n toggleManualCaption: (enable: boolean) => undefined | Promise<void>;\n sendManualClosedCaption: (\n text: string,\n timeStamp: number,\n csis: number[],\n isFinal: boolean\n ) => void;\n getKeepTranscriptionSubscribed: () => boolean;\n}\n\ntype MeetingTranscripts = {\n start_mills?: number;\n end_mills?: number;\n text: string;\n last_packet_timestamp_ms?: number;\n csis: Array<number>;\n transcript_language_code: string;\n translations?: {\n [key: string]: string;\n };\n timestamp?: string;\n};\n\ntype MeetingTranscriptPayload = {\n isFinal: boolean;\n transcriptId: string;\n transcripts: Array<MeetingTranscripts>;\n};\n\nexport type {\n AnnouncementPayload,\n CaptionLanguageResponse,\n TranscriptionResponse,\n Transcription,\n Highlight,\n IVoiceaChannel,\n MeetingTranscriptPayload,\n};\n"],"mappings":"","ignoreList":[]}
1
+ {"version":3,"names":[],"sources":["voicea.types.ts"],"sourcesContent":["/* eslint-disable camelcase */\n/* eslint-disable no-undef */\n\n/**\n * Type for payload\n */\ninterface AnnouncementPayload {\n translation: {\n max_languages: number;\n allowed_languages: string[];\n };\n\n ASR: {\n spoken_languages: string[];\n };\n}\n\n// eslint-disable-next-line no-undef\ntype TRANSCRIPTION_TYPES =\n | 'unknown'\n | 'eva_wake'\n | 'eva_thanks'\n | 'eva_cancel'\n | 'highlight_created'\n | 'transcript_interim_results'\n | 'transcript_final_result'\n | 'manual_caption_interim_results'\n | 'manual_caption_interim_result'\n | 'manual_caption_final_result';\n\n/**\n * Class for an Transcription Object\n */\ninterface Transcription {\n start_millis: number;\n end_millis: number;\n text: string;\n transcript_language_code: string;\n translations: {[x: string]: string};\n csis: number[];\n last_packet_timestamp_ms: number;\n timestamp: string;\n taggedSpeaker?: {\n speakerId?: string[];\n newName?: string;\n };\n}\n\ninterface TaggedSpeaker {\n csiId: number;\n speakerId: string;\n newName: string;\n}\n\ninterface SpeakerNameUpdatePayload {\n id?: string;\n taggedSpeakers: TaggedSpeaker[];\n}\n\n/**\n * Highlights\n */\ninterface Highlight {\n highlight_id: string;\n transcript: string;\n highlight_label: string;\n highlight_source: string;\n start_millis: number;\n end_millis: number;\n csis: number[];\n}\n/**\n * Type for Transcription message\n */\ninterface TranscriptionResponse {\n type: TRANSCRIPTION_TYPES;\n id: string;\n sender: string;\n data_source: string;\n transcript_id: string;\n translations?: {[x: string]: string};\n transcripts?: Transcription[];\n transcript?: Transcription;\n highlight?: Highlight;\n csis: number[];\n data: string;\n command_response: string;\n language: string;\n}\n/**\n * Type for CaptionLanguageResponse\n */\ninterface CaptionLanguageResponse {\n requestId: string;\n statusCode: number;\n errorCode: number;\n message: string;\n}\n\ninterface IVoiceaChannel {\n setSpokenLanguage: (languageCode: string) => Promise<void>;\n onSpokenLanguageUpdate: (languageCode: string, meetingId: string) => void;\n requestLanguage: (languageCode: string) => void;\n turnOnCaptions: () => undefined | Promise<void>;\n toggleTranscribing: (activate: boolean, spokenLanguage: string) => undefined | Promise<void>;\n deregisterEvents: () => void;\n toggleManualCaption: (enable: boolean) => undefined | Promise<void>;\n sendManualClosedCaption: (\n text: string,\n timeStamp: number,\n csis: number[],\n isFinal: boolean\n ) => void;\n getKeepTranscriptionSubscribed: () => boolean;\n}\n\ntype MeetingTranscripts = {\n start_mills?: number;\n end_mills?: number;\n text: string;\n last_packet_timestamp_ms?: number;\n csis: Array<number>;\n transcript_language_code: string;\n translations?: {\n [key: string]: string;\n };\n timestamp?: string;\n taggedSpeaker?: {\n speakerId?: string[];\n newName?: string;\n };\n};\n\ntype MeetingTranscriptPayload = {\n isFinal: boolean;\n transcriptId: string;\n transcripts: Array<MeetingTranscripts>;\n};\n\nexport type {\n AnnouncementPayload,\n CaptionLanguageResponse,\n TranscriptionResponse,\n Transcription,\n Highlight,\n TaggedSpeaker,\n SpeakerNameUpdatePayload,\n IVoiceaChannel,\n MeetingTranscriptPayload,\n};\n"],"mappings":"","ignoreList":[]}
package/package.json CHANGED
@@ -14,9 +14,9 @@
14
14
  "node": ">=18"
15
15
  },
16
16
  "dependencies": {
17
- "@webex/internal-plugin-llm": "3.12.0-llmrefactor.1",
18
- "@webex/internal-plugin-mercury": "3.12.0-llmrefactor.1",
19
- "@webex/webex-core": "3.12.0-llmrefactor.1",
17
+ "@webex/internal-plugin-llm": "3.12.0-llmrefactor.9",
18
+ "@webex/internal-plugin-mercury": "3.12.0-llmrefactor.9",
19
+ "@webex/webex-core": "3.12.0-llmrefactor.7",
20
20
  "uuid": "^3.3.2"
21
21
  },
22
22
  "browserify": {
@@ -31,11 +31,11 @@
31
31
  "@webex/eslint-config-legacy": "0.0.0",
32
32
  "@webex/jest-config-legacy": "0.0.0",
33
33
  "@webex/legacy-tools": "0.0.0",
34
- "@webex/test-helper-chai": "3.12.0-llmrefactor.1",
35
- "@webex/test-helper-mocha": "3.12.0-llmrefactor.1",
36
- "@webex/test-helper-mock-web-socket": "3.12.0-llmrefactor.1",
34
+ "@webex/test-helper-chai": "3.12.0-llmrefactor.2",
35
+ "@webex/test-helper-mocha": "3.12.0-llmrefactor.2",
36
+ "@webex/test-helper-mock-web-socket": "3.12.0-llmrefactor.2",
37
37
  "@webex/test-helper-mock-webex": "3.12.0-llmrefactor.1",
38
- "@webex/test-helper-test-users": "3.12.0-llmrefactor.1",
38
+ "@webex/test-helper-test-users": "3.12.0-llmrefactor.0",
39
39
  "eslint": "^8.24.0",
40
40
  "prettier": "^2.7.1",
41
41
  "sinon": "^9.2.4"
@@ -49,5 +49,5 @@
49
49
  "test:style": "eslint ./src/**/*.*",
50
50
  "test:unit": "webex-legacy-tools test --unit --runner jest"
51
51
  },
52
- "version": "3.12.0-llmrefactor.1"
52
+ "version": "3.12.0-llmrefactor.11"
53
53
  }
package/src/constants.ts CHANGED
@@ -7,6 +7,7 @@ export const EVENT_TRIGGERS = {
7
7
  TRANSCRIBING_OFF: 'voicea:transcribingOff',
8
8
 
9
9
  NEW_CAPTION: 'voicea:newCaption',
10
+ SPEAKER_NAME_UPDATED: 'voicea:speakerNameUpdated',
10
11
  EVA_COMMAND: 'voicea:wxa',
11
12
  HIGHLIGHT_CREATED: 'voicea:highlightCreated',
12
13
  NEW_MANUAL_CAPTION: 'aibridge:newManualCaption',
@@ -21,6 +22,7 @@ export const AIBRIDGE_RELAY_TYPES = {
21
22
  TRANSLATION_REQUEST: 'voicea.transl.req',
22
23
  TRANSLATION_RESPONSE: 'voicea.transl.rsp',
23
24
  TRANSCRIPTION: 'voicea.transcription',
25
+ SPEAKER_NAME_UPDATE: 'voicea.update_speakername',
24
26
  },
25
27
  MANUAL: {
26
28
  TRANSCRIPTION: 'aibridge.manual_transcription',
package/src/index.ts CHANGED
@@ -2,11 +2,11 @@ import * as WebexCore from '@webex/webex-core';
2
2
 
3
3
  import Voicea from './voicea-plugin';
4
4
  import {VoiceaChannel} from './voicea';
5
- import type {MeetingTranscriptPayload} from './voicea.types';
5
+ import type {MeetingTranscriptPayload, SpeakerNameUpdatePayload} from './voicea.types';
6
6
 
7
7
  WebexCore.registerInternalPlugin('voicea', Voicea, {});
8
8
 
9
9
  export {Voicea as VoiceaPlugin, VoiceaChannel};
10
10
  export default Voicea;
11
- export {type MeetingTranscriptPayload};
11
+ export {type MeetingTranscriptPayload, type SpeakerNameUpdatePayload};
12
12
  export {EVENT_TRIGGERS, TURN_ON_CAPTION_STATUS} from './constants';
@@ -13,16 +13,16 @@ export class VoiceaPlugin extends WebexPlugin {
13
13
  namespace = VOICEA;
14
14
 
15
15
  /**
16
- * Creates a VoiceaChannel bound to the given LLMChannel.
16
+ * Creates a VoiceaChannel, optionally bound to an LLMChannel.
17
17
  *
18
18
  * Note: VoiceaChannel does not take ownership of the LLMChannel.
19
19
  * The caller retains ownership and is responsible for the LLMChannel's
20
20
  * lifecycle (connection, disconnection, cleanup).
21
21
  *
22
- * @param {LLMChannel} llmChannel - The LLM channel to use for voicea
22
+ * @param {LLMChannel} [llmChannel] - The LLM channel to use for voicea (optional)
23
23
  * @returns {VoiceaChannel} A new VoiceaChannel instance
24
24
  */
25
- public createChannel(llmChannel: LLMChannel): VoiceaChannel {
25
+ public createChannel(llmChannel?: LLMChannel): VoiceaChannel {
26
26
  // @ts-ignore - webex is available on WebexPlugin
27
27
  const channel = new VoiceaChannel(llmChannel, this.webex.request.bind(this.webex));
28
28
 
package/src/voicea.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  import {
18
18
  AnnouncementPayload,
19
19
  CaptionLanguageResponse,
20
+ SpeakerNameUpdatePayload,
20
21
  TranscriptionResponse,
21
22
  IVoiceaChannel,
22
23
  } from './voicea.types';
@@ -31,7 +32,7 @@ import {millisToMinutesAndSeconds} from './utils';
31
32
  */
32
33
  export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
33
34
  private request: (options: {method: string; url: string; body?: object}) => Promise<any>;
34
- private llmChannel: LLMChannel;
35
+ private llmChannel?: LLMChannel;
35
36
 
36
37
  private seqNum: number;
37
38
  private areCaptionsEnabled: boolean;
@@ -47,13 +48,21 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
47
48
  private spokenLanguages: string[] = [];
48
49
  private currentCaptionLanguage?: string;
49
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
+
50
58
  /**
51
- * Creates a VoiceaChannel bound to the given LLMChannel
52
- * @param {LLMChannel} llmChannel - The LLM channel to use
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)
53
62
  * @param {Function} request - The request function for making API calls (typically webex.request bound to webex)
54
63
  */
55
64
  constructor(
56
- llmChannel: LLMChannel,
65
+ llmChannel: LLMChannel | undefined,
57
66
  request: (options: {method: string; url: string; body?: object}) => Promise<any>
58
67
  ) {
59
68
  super();
@@ -70,9 +79,25 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
70
79
  this.currentCaptionLanguage = undefined;
71
80
  this.keepTranscriptionSubscribed = false;
72
81
 
73
- // Subscribe to relay events from the LLM channel
74
- this.llmChannel.on('event:relay.event', this.eventProcessor);
75
- this.hasSubscribedToEvents = true;
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;
76
101
  }
77
102
 
78
103
  /**
@@ -94,6 +119,9 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
94
119
  case AIBRIDGE_RELAY_TYPES.VOICEA.TRANSCRIPTION:
95
120
  this.processTranscription(e.data.voiceaPayload);
96
121
  break;
122
+ case AIBRIDGE_RELAY_TYPES.VOICEA.SPEAKER_NAME_UPDATE:
123
+ this.processSpeakerNameUpdate(e.data.voiceaPayload);
124
+ break;
97
125
  case AIBRIDGE_RELAY_TYPES.MANUAL.TRANSCRIPTION:
98
126
  case AIBRIDGE_RELAY_TYPES.MANUAL.CAPTIONER:
99
127
  this.processManualTranscription({
@@ -115,9 +143,14 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
115
143
  this.areCaptionsEnabled = false;
116
144
  this.keepTranscriptionSubscribed = false;
117
145
  this.captionServiceId = undefined;
146
+ this.targetLLMChannel = undefined;
147
+ this._pendingCaptionRestore = false;
148
+
149
+ // Remove any pending online listener
150
+ this.detachPendingOnline();
118
151
 
119
152
  if (this.hasSubscribedToEvents) {
120
- this.llmChannel.off('event:relay.event', this.eventProcessor);
153
+ this.llmChannel?.off('event:relay.event', this.eventProcessor);
121
154
  this.hasSubscribedToEvents = false;
122
155
  }
123
156
 
@@ -129,42 +162,101 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
129
162
  }
130
163
 
131
164
  /**
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>}
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}
139
171
  */
140
- public async switchLLMChannel(newLLMChannel: LLMChannel): Promise<void> {
141
- // Save current state
142
- const captionsWereOn = this.keepTranscriptionSubscribed;
143
- const spokenLanguage = this.currentSpokenLanguage;
172
+ private attachPendingOnline(channel: LLMChannel, callback: () => void): void {
173
+ this.detachPendingOnline();
174
+ this._pendingOnlineListener = callback;
175
+ channel.once('online', this._pendingOnlineListener);
176
+ }
144
177
 
145
- // Unsubscribe from old channel
146
- if (this.hasSubscribedToEvents && this.llmChannel) {
147
- this.llmChannel.off('event:relay.event', this.eventProcessor);
148
- this.hasSubscribedToEvents = false;
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);
149
186
  }
187
+ this._pendingOnlineListener = undefined;
188
+ }
150
189
 
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
190
+ /**
191
+ * Reset announcement state for a new connection.
192
+ * @private
193
+ * @returns {void}
194
+ */
195
+ private resetAnnounceState(): void {
159
196
  this.announceStatus = ANNOUNCE_STATUS.IDLE;
160
197
  this.captionStatus = TURN_ON_CAPTION_STATUS.IDLE;
161
198
  this.captionServiceId = undefined;
162
199
  this.areCaptionsEnabled = false;
200
+ }
163
201
 
164
- // Re-announce and re-enable captions if they were on
165
- if (captionsWereOn) {
166
- await this.turnOnCaptions(spokenLanguage);
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;
167
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();
168
260
  }
169
261
 
170
262
  /**
@@ -187,6 +279,16 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
187
279
  }
188
280
  };
189
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
+
190
292
  /**
191
293
  * Process Transcript and send alert
192
294
  * @param {TranscriptionResponse} voiceaPayload
@@ -290,7 +392,7 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
290
392
  * Indicates whether the LLM channel is connected.
291
393
  * @returns {boolean}
292
394
  */
293
- public isLLMConnected = (): boolean => this.llmChannel.isConnected();
395
+ public isLLMConnected = (): boolean => this.llmChannel?.isConnected() ?? false;
294
396
 
295
397
  public getKeepTranscriptionSubscribed = (): boolean => this.keepTranscriptionSubscribed;
296
398
 
@@ -299,9 +401,10 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
299
401
  * @returns {void}
300
402
  */
301
403
  public sendAnnouncement = (): void => {
404
+ const llm = this.requireLLMChannel();
302
405
  this.announceStatus = ANNOUNCE_STATUS.JOINING;
303
- const socket = this.llmChannel.getSocket();
304
- const binding = this.llmChannel.getBinding();
406
+ const socket = llm.getSocket();
407
+ const binding = llm.getBinding();
305
408
 
306
409
  const payload = {
307
410
  id: `${this.seqNum}`,
@@ -338,7 +441,7 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
338
441
  ): Promise<void> =>
339
442
  this.request({
340
443
  method: 'PUT',
341
- url: `${this.llmChannel.getLocusUrl()}/controls/`,
444
+ url: `${this.requireLLMChannel().getLocusUrl()}/controls/`,
342
445
  body: {
343
446
  transcribe: {
344
447
  spokenLanguage: languageCode,
@@ -359,8 +462,9 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
359
462
  return;
360
463
  }
361
464
 
362
- const socket = this.llmChannel.getSocket();
363
- const binding = this.llmChannel.getBinding();
465
+ const llm = this.requireLLMChannel();
466
+ const socket = llm.getSocket();
467
+ const binding = llm.getBinding();
364
468
 
365
469
  socket.send({
366
470
  id: `${this.seqNum}`,
@@ -405,8 +509,9 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
405
509
  return;
406
510
  }
407
511
 
408
- const socket = this.llmChannel.getSocket();
409
- const binding = this.llmChannel.getBinding();
512
+ const llm = this.requireLLMChannel();
513
+ const socket = llm.getSocket();
514
+ const binding = llm.getBinding();
410
515
 
411
516
  socket?.send({
412
517
  id: `${this.seqNum}`,
@@ -449,7 +554,7 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
449
554
  private requestTurnOnCaptions = (languageCode?: string): undefined | Promise<void> => {
450
555
  this.captionStatus = TURN_ON_CAPTION_STATUS.SENDING;
451
556
 
452
- const locusUrl = this.llmChannel.getLocusUrl();
557
+ const locusUrl = this.requireLLMChannel().getLocusUrl();
453
558
 
454
559
  const body = {
455
560
  transcribe: {caption: true},
@@ -471,7 +576,7 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
471
576
  })
472
577
  .catch((error) => {
473
578
  this.captionStatus = TURN_ON_CAPTION_STATUS.IDLE;
474
- throw new Error('turn on captions fail');
579
+ throw new Error('turn on captions fail', {cause: error});
475
580
  });
476
581
  };
477
582
 
@@ -515,7 +620,7 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
515
620
  * @returns {Promise}
516
621
  */
517
622
  public turnOnCaptions = async (spokenLanguage?: string): Promise<void | undefined> => {
518
- if (this.captionStatus === TURN_ON_CAPTION_STATUS.SENDING) return undefined;
623
+ if (this.isCaptionProcessing()) return undefined;
519
624
 
520
625
  if (!this.isLLMConnected()) {
521
626
  throw new Error('can not turn on captions before llm connected');
@@ -536,7 +641,7 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
536
641
  ): undefined | Promise<void> => {
537
642
  return this.request({
538
643
  method: 'PUT',
539
- url: `${this.llmChannel.getLocusUrl()}/controls/`,
644
+ url: `${this.requireLLMChannel().getLocusUrl()}/controls/`,
540
645
  body: {
541
646
  transcribe: {
542
647
  transcribing: activate,
@@ -564,7 +669,7 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
564
669
 
565
670
  return this.request({
566
671
  method: 'PUT',
567
- url: `${this.llmChannel.getLocusUrl()}/controls/`,
672
+ url: `${this.requireLLMChannel().getLocusUrl()}/controls/`,
568
673
  body: {
569
674
  manualCaption: {
570
675
  enable,
@@ -638,11 +743,12 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
638
743
  } = {}): Promise<void> => {
639
744
  if (!this.isLLMConnected()) return;
640
745
 
641
- const isDataChannelTokenEnabled = await this.llmChannel.isDataChannelTokenEnabled();
746
+ const llm = this.requireLLMChannel();
747
+ const isDataChannelTokenEnabled = await llm.isDataChannelTokenEnabled();
642
748
  if (!isDataChannelTokenEnabled) return;
643
749
 
644
- const socket = this.llmChannel.getSocket();
645
- const datachannelUrl = this.llmChannel.getDatachannelUrl();
750
+ const socket = llm.getSocket();
751
+ const datachannelUrl = llm.getDatachannelUrl();
646
752
 
647
753
  socket.send({
648
754
  id: `${this.seqNum}`,
@@ -40,6 +40,21 @@ interface Transcription {
40
40
  csis: number[];
41
41
  last_packet_timestamp_ms: number;
42
42
  timestamp: string;
43
+ taggedSpeaker?: {
44
+ speakerId?: string[];
45
+ newName?: string;
46
+ };
47
+ }
48
+
49
+ interface TaggedSpeaker {
50
+ csiId: number;
51
+ speakerId: string;
52
+ newName: string;
53
+ }
54
+
55
+ interface SpeakerNameUpdatePayload {
56
+ id?: string;
57
+ taggedSpeakers: TaggedSpeaker[];
43
58
  }
44
59
 
45
60
  /**
@@ -110,6 +125,10 @@ type MeetingTranscripts = {
110
125
  [key: string]: string;
111
126
  };
112
127
  timestamp?: string;
128
+ taggedSpeaker?: {
129
+ speakerId?: string[];
130
+ newName?: string;
131
+ };
113
132
  };
114
133
 
115
134
  type MeetingTranscriptPayload = {
@@ -124,6 +143,8 @@ export type {
124
143
  TranscriptionResponse,
125
144
  Transcription,
126
145
  Highlight,
146
+ TaggedSpeaker,
147
+ SpeakerNameUpdatePayload,
127
148
  IVoiceaChannel,
128
149
  MeetingTranscriptPayload,
129
150
  };
@@ -51,6 +51,18 @@ describe('plugin-voicea', () => {
51
51
  assert.instanceOf(channel, VoiceaChannel);
52
52
  });
53
53
 
54
+ it('creates a new VoiceaChannel instance without llmChannel', () => {
55
+ const channel = plugin.createChannel();
56
+
57
+ assert.instanceOf(channel, VoiceaChannel);
58
+ });
59
+
60
+ it('creates a new VoiceaChannel instance with undefined llmChannel', () => {
61
+ const channel = plugin.createChannel(undefined);
62
+
63
+ assert.instanceOf(channel, VoiceaChannel);
64
+ });
65
+
54
66
  it('creates independent channels for multiple calls', () => {
55
67
  const mockLLMChannel1 = createMockLLMChannel();
56
68
  const mockLLMChannel2 = createMockLLMChannel();