@webex/internal-plugin-voicea 3.12.0-llmrefactor.6 → 3.12.0-llmrefactor.8

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.
@@ -9,15 +9,15 @@ import { VoiceaChannel } from './voicea';
9
9
  export declare class VoiceaPlugin extends WebexPlugin {
10
10
  namespace: string;
11
11
  /**
12
- * Creates a VoiceaChannel bound to the given LLMChannel.
12
+ * Creates a VoiceaChannel, optionally bound to an LLMChannel.
13
13
  *
14
14
  * Note: VoiceaChannel does not take ownership of the LLMChannel.
15
15
  * The caller retains ownership and is responsible for the LLMChannel's
16
16
  * lifecycle (connection, disconnection, cleanup).
17
17
  *
18
- * @param {LLMChannel} llmChannel - The LLM channel to use for voicea
18
+ * @param {LLMChannel} [llmChannel] - The LLM channel to use for voicea (optional)
19
19
  * @returns {VoiceaChannel} A new VoiceaChannel instance
20
20
  */
21
- createChannel(llmChannel: LLMChannel): VoiceaChannel;
21
+ createChannel(llmChannel?: LLMChannel): VoiceaChannel;
22
22
  }
23
23
  export default VoiceaPlugin;
@@ -11,7 +11,7 @@ import { IVoiceaChannel } from './voicea.types';
11
11
  */
12
12
  export declare class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
13
13
  private request;
14
- private llmChannel;
14
+ private llmChannel?;
15
15
  private seqNum;
16
16
  private areCaptionsEnabled;
17
17
  private hasSubscribedToEvents;
@@ -23,16 +23,27 @@ export declare class VoiceaChannel extends EventEmitter implements IVoiceaChanne
23
23
  private currentSpokenLanguage?;
24
24
  private spokenLanguages;
25
25
  private currentCaptionLanguage?;
26
+ private targetLLMChannel?;
27
+ private _pendingOnlineListener?;
28
+ private _pendingCaptionRestore;
26
29
  /**
27
- * Creates a VoiceaChannel bound to the given LLMChannel
28
- * @param {LLMChannel} llmChannel - The LLM channel to use
30
+ * Creates a VoiceaChannel, optionally bound to an LLMChannel.
31
+ * If no llmChannel is provided, call switchLLMChannel() later to attach one.
32
+ * @param {LLMChannel} [llmChannel] - The LLM channel to use (optional)
29
33
  * @param {Function} request - The request function for making API calls (typically webex.request bound to webex)
30
34
  */
31
- constructor(llmChannel: LLMChannel, request: (options: {
35
+ constructor(llmChannel: LLMChannel | undefined, request: (options: {
32
36
  method: string;
33
37
  url: string;
34
38
  body?: object;
35
39
  }) => Promise<any>);
40
+ /**
41
+ * Returns the LLM channel, throwing if not available.
42
+ * @private
43
+ * @returns {LLMChannel}
44
+ * @throws {Error} If LLM channel is not available
45
+ */
46
+ private requireLLMChannel;
36
47
  /**
37
48
  * Process events from LLM channel
38
49
  * @param {Object} e - Event data
@@ -44,12 +55,39 @@ export declare class VoiceaChannel extends EventEmitter implements IVoiceaChanne
44
55
  * @returns {void}
45
56
  */
46
57
  deregisterEvents(): void;
58
+ /**
59
+ * Attach a pending 'online' listener to a channel for deferred reconciliation.
60
+ * Detaches any existing listener first.
61
+ * @param {LLMChannel} channel - The channel to attach to
62
+ * @param {Function} callback - The callback to run when 'online' fires
63
+ * @private
64
+ * @returns {void}
65
+ */
66
+ private attachPendingOnline;
67
+ /**
68
+ * Detach any pending 'online' listener.
69
+ * @private
70
+ * @returns {void}
71
+ */
72
+ private detachPendingOnline;
73
+ /**
74
+ * Reset announcement state for a new connection.
75
+ * @private
76
+ * @returns {void}
77
+ */
78
+ private resetAnnounceState;
79
+ /**
80
+ * Reconcile voicea binding and caption state to match the desired target.
81
+ * This is idempotent - always re-reads targetLLMChannel and current state.
82
+ * A deferred 'online' callback that fires late self-corrects automatically.
83
+ * @private
84
+ * @returns {Promise<void>}
85
+ */
86
+ private reconcile;
47
87
  /**
48
88
  * Switch to a different LLM channel while preserving caption state.
49
89
  * Used when transitioning between main meeting and practice session.
50
- * - Preserves keepTranscriptionSubscribed and spokenLanguage state
51
- * - Unsubscribes from old channel, subscribes to new channel
52
- * - Re-announces and re-enables captions if they were on
90
+ * This is a thin intent-setter - actual binding happens in reconcile().
53
91
  * @param {LLMChannel} newLLMChannel - The new LLM channel to switch to
54
92
  * @returns {Promise<void>}
55
93
  */
@@ -39,13 +39,13 @@ var VoiceaPlugin = exports.VoiceaPlugin = /*#__PURE__*/function (_WebexPlugin) {
39
39
  key: "createChannel",
40
40
  value:
41
41
  /**
42
- * Creates a VoiceaChannel bound to the given LLMChannel.
42
+ * Creates a VoiceaChannel, optionally bound to an LLMChannel.
43
43
  *
44
44
  * Note: VoiceaChannel does not take ownership of the LLMChannel.
45
45
  * The caller retains ownership and is responsible for the LLMChannel's
46
46
  * lifecycle (connection, disconnection, cleanup).
47
47
  *
48
- * @param {LLMChannel} llmChannel - The LLM channel to use for voicea
48
+ * @param {LLMChannel} [llmChannel] - The LLM channel to use for voicea (optional)
49
49
  * @returns {VoiceaChannel} A new VoiceaChannel instance
50
50
  */
51
51
  function createChannel(llmChannel) {
@@ -1 +1 @@
1
- {"version":3,"names":["_webexCore","require","_voicea","_constants","_callSuper","t","o","e","_getPrototypeOf2","default","_possibleConstructorReturn2","_isNativeReflectConstruct","_Reflect$construct","constructor","apply","Boolean","prototype","valueOf","call","VoiceaPlugin","exports","_WebexPlugin","_this","_classCallCheck2","_len","arguments","length","args","Array","_key","concat","_defineProperty2","VOICEA","_inherits2","_createClass2","key","value","createChannel","llmChannel","channel","VoiceaChannel","webex","request","bind","WebexPlugin","_default"],"sources":["voicea-plugin.ts"],"sourcesContent":["import {WebexPlugin} from '@webex/webex-core';\nimport type LLMChannel from '@webex/internal-plugin-llm';\n\nimport {VoiceaChannel} from './voicea';\nimport {VOICEA} from './constants';\n\n/**\n * VoiceaPlugin - factory for creating VoiceaChannel instances\n * @export\n * @class VoiceaPlugin\n */\nexport class VoiceaPlugin extends WebexPlugin {\n namespace = VOICEA;\n\n /**\n * Creates a VoiceaChannel bound to the given LLMChannel.\n *\n * Note: VoiceaChannel does not take ownership of the LLMChannel.\n * The caller retains ownership and is responsible for the LLMChannel's\n * lifecycle (connection, disconnection, cleanup).\n *\n * @param {LLMChannel} llmChannel - The LLM channel to use for voicea\n * @returns {VoiceaChannel} A new VoiceaChannel instance\n */\n public createChannel(llmChannel: LLMChannel): VoiceaChannel {\n // @ts-ignore - webex is available on WebexPlugin\n const channel = new VoiceaChannel(llmChannel, this.webex.request.bind(this.webex));\n\n return channel;\n }\n}\n\nexport default VoiceaPlugin;\n"],"mappings":";;;;;;;;;;;;;;;AAAA,IAAAA,UAAA,GAAAC,OAAA;AAGA,IAAAC,OAAA,GAAAD,OAAA;AACA,IAAAE,UAAA,GAAAF,OAAA;AAAmC,SAAAG,WAAAC,CAAA,EAAAC,CAAA,EAAAC,CAAA,WAAAD,CAAA,OAAAE,gBAAA,CAAAC,OAAA,EAAAH,CAAA,OAAAI,2BAAA,CAAAD,OAAA,EAAAJ,CAAA,EAAAM,yBAAA,KAAAC,kBAAA,CAAAN,CAAA,EAAAC,CAAA,YAAAC,gBAAA,CAAAC,OAAA,EAAAJ,CAAA,EAAAQ,WAAA,IAAAP,CAAA,CAAAQ,KAAA,CAAAT,CAAA,EAAAE,CAAA;AAAA,SAAAI,0BAAA,cAAAN,CAAA,IAAAU,OAAA,CAAAC,SAAA,CAAAC,OAAA,CAAAC,IAAA,CAAAN,kBAAA,CAAAG,OAAA,iCAAAV,CAAA,aAAAM,yBAAA,YAAAA,0BAAA,aAAAN,CAAA;AAEnC;AACA;AACA;AACA;AACA;AAJA,IAKac,YAAY,GAAAC,OAAA,CAAAD,YAAA,0BAAAE,YAAA;EAAA,SAAAF,aAAA;IAAA,IAAAG,KAAA;IAAA,IAAAC,gBAAA,CAAAd,OAAA,QAAAU,YAAA;IAAA,SAAAK,IAAA,GAAAC,SAAA,CAAAC,MAAA,EAAAC,IAAA,OAAAC,KAAA,CAAAJ,IAAA,GAAAK,IAAA,MAAAA,IAAA,GAAAL,IAAA,EAAAK,IAAA;MAAAF,IAAA,CAAAE,IAAA,IAAAJ,SAAA,CAAAI,IAAA;IAAA;IAAAP,KAAA,GAAAlB,UAAA,OAAAe,YAAA,KAAAW,MAAA,CAAAH,IAAA;IAAA,IAAAI,gBAAA,CAAAtB,OAAA,EAAAa,KAAA,eACXU,iBAAM;IAAA,OAAAV,KAAA;EAAA;EAAA,IAAAW,UAAA,CAAAxB,OAAA,EAAAU,YAAA,EAAAE,YAAA;EAAA,WAAAa,aAAA,CAAAzB,OAAA,EAAAU,YAAA;IAAAgB,GAAA;IAAAC,KAAA;IAElB;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACE,SAAOC,aAAaA,CAACC,UAAsB,EAAiB;MAC1D;MACA,IAAMC,OAAO,GAAG,IAAIC,qBAAa,CAACF,UAAU,EAAE,IAAI,CAACG,KAAK,CAACC,OAAO,CAACC,IAAI,CAAC,IAAI,CAACF,KAAK,CAAC,CAAC;MAElF,OAAOF,OAAO;IAChB;EAAC;AAAA,EAlB+BK,sBAAW;AAAA,IAAAC,QAAA,GAAAzB,OAAA,CAAAX,OAAA,GAqB9BU,YAAY","ignoreList":[]}
1
+ {"version":3,"names":["_webexCore","require","_voicea","_constants","_callSuper","t","o","e","_getPrototypeOf2","default","_possibleConstructorReturn2","_isNativeReflectConstruct","_Reflect$construct","constructor","apply","Boolean","prototype","valueOf","call","VoiceaPlugin","exports","_WebexPlugin","_this","_classCallCheck2","_len","arguments","length","args","Array","_key","concat","_defineProperty2","VOICEA","_inherits2","_createClass2","key","value","createChannel","llmChannel","channel","VoiceaChannel","webex","request","bind","WebexPlugin","_default"],"sources":["voicea-plugin.ts"],"sourcesContent":["import {WebexPlugin} from '@webex/webex-core';\nimport type LLMChannel from '@webex/internal-plugin-llm';\n\nimport {VoiceaChannel} from './voicea';\nimport {VOICEA} from './constants';\n\n/**\n * VoiceaPlugin - factory for creating VoiceaChannel instances\n * @export\n * @class VoiceaPlugin\n */\nexport class VoiceaPlugin extends WebexPlugin {\n namespace = VOICEA;\n\n /**\n * Creates a VoiceaChannel, optionally bound to an LLMChannel.\n *\n * Note: VoiceaChannel does not take ownership of the LLMChannel.\n * The caller retains ownership and is responsible for the LLMChannel's\n * lifecycle (connection, disconnection, cleanup).\n *\n * @param {LLMChannel} [llmChannel] - The LLM channel to use for voicea (optional)\n * @returns {VoiceaChannel} A new VoiceaChannel instance\n */\n public createChannel(llmChannel?: LLMChannel): VoiceaChannel {\n // @ts-ignore - webex is available on WebexPlugin\n const channel = new VoiceaChannel(llmChannel, this.webex.request.bind(this.webex));\n\n return channel;\n }\n}\n\nexport default VoiceaPlugin;\n"],"mappings":";;;;;;;;;;;;;;;AAAA,IAAAA,UAAA,GAAAC,OAAA;AAGA,IAAAC,OAAA,GAAAD,OAAA;AACA,IAAAE,UAAA,GAAAF,OAAA;AAAmC,SAAAG,WAAAC,CAAA,EAAAC,CAAA,EAAAC,CAAA,WAAAD,CAAA,OAAAE,gBAAA,CAAAC,OAAA,EAAAH,CAAA,OAAAI,2BAAA,CAAAD,OAAA,EAAAJ,CAAA,EAAAM,yBAAA,KAAAC,kBAAA,CAAAN,CAAA,EAAAC,CAAA,YAAAC,gBAAA,CAAAC,OAAA,EAAAJ,CAAA,EAAAQ,WAAA,IAAAP,CAAA,CAAAQ,KAAA,CAAAT,CAAA,EAAAE,CAAA;AAAA,SAAAI,0BAAA,cAAAN,CAAA,IAAAU,OAAA,CAAAC,SAAA,CAAAC,OAAA,CAAAC,IAAA,CAAAN,kBAAA,CAAAG,OAAA,iCAAAV,CAAA,aAAAM,yBAAA,YAAAA,0BAAA,aAAAN,CAAA;AAEnC;AACA;AACA;AACA;AACA;AAJA,IAKac,YAAY,GAAAC,OAAA,CAAAD,YAAA,0BAAAE,YAAA;EAAA,SAAAF,aAAA;IAAA,IAAAG,KAAA;IAAA,IAAAC,gBAAA,CAAAd,OAAA,QAAAU,YAAA;IAAA,SAAAK,IAAA,GAAAC,SAAA,CAAAC,MAAA,EAAAC,IAAA,OAAAC,KAAA,CAAAJ,IAAA,GAAAK,IAAA,MAAAA,IAAA,GAAAL,IAAA,EAAAK,IAAA;MAAAF,IAAA,CAAAE,IAAA,IAAAJ,SAAA,CAAAI,IAAA;IAAA;IAAAP,KAAA,GAAAlB,UAAA,OAAAe,YAAA,KAAAW,MAAA,CAAAH,IAAA;IAAA,IAAAI,gBAAA,CAAAtB,OAAA,EAAAa,KAAA,eACXU,iBAAM;IAAA,OAAAV,KAAA;EAAA;EAAA,IAAAW,UAAA,CAAAxB,OAAA,EAAAU,YAAA,EAAAE,YAAA;EAAA,WAAAa,aAAA,CAAAzB,OAAA,EAAAU,YAAA;IAAAgB,GAAA;IAAAC,KAAA;IAElB;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;IACE,SAAOC,aAAaA,CAACC,UAAuB,EAAiB;MAC3D;MACA,IAAMC,OAAO,GAAG,IAAIC,qBAAa,CAACF,UAAU,EAAE,IAAI,CAACG,KAAK,CAACC,OAAO,CAACC,IAAI,CAAC,IAAI,CAACF,KAAK,CAAC,CAAC;MAElF,OAAOF,OAAO;IAChB;EAAC;AAAA,EAlB+BK,sBAAW;AAAA,IAAAC,QAAA,GAAAzB,OAAA,CAAAX,OAAA,GAqB9BU,YAAY","ignoreList":[]}
package/dist/voicea.js CHANGED
@@ -12,6 +12,7 @@ _Object$defineProperty(exports, "__esModule", {
12
12
  value: true
13
13
  });
14
14
  exports.default = exports.VoiceaChannel = void 0;
15
+ var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));
15
16
  var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs2/regenerator"));
16
17
  var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/asyncToGenerator"));
17
18
  var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck"));
@@ -39,8 +40,9 @@ function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.
39
40
  */
40
41
  var VoiceaChannel = exports.VoiceaChannel = /*#__PURE__*/function (_EventEmitter) {
41
42
  /**
42
- * Creates a VoiceaChannel bound to the given LLMChannel
43
- * @param {LLMChannel} llmChannel - The LLM channel to use
43
+ * Creates a VoiceaChannel, optionally bound to an LLMChannel.
44
+ * If no llmChannel is provided, call switchLLMChannel() later to attach one.
45
+ * @param {LLMChannel} [llmChannel] - The LLM channel to use (optional)
44
46
  * @param {Function} request - The request function for making API calls (typically webex.request bound to webex)
45
47
  */
46
48
  function VoiceaChannel(llmChannel, request) {
@@ -60,6 +62,12 @@ var VoiceaChannel = exports.VoiceaChannel = /*#__PURE__*/function (_EventEmitter
60
62
  (0, _defineProperty2.default)(_this, "currentSpokenLanguage", void 0);
61
63
  (0, _defineProperty2.default)(_this, "spokenLanguages", []);
62
64
  (0, _defineProperty2.default)(_this, "currentCaptionLanguage", void 0);
65
+ // Target channel for reconciler pattern - the channel voicea WANTS to be bound to
66
+ (0, _defineProperty2.default)(_this, "targetLLMChannel", void 0);
67
+ // Single pending 'online' listener for deferred reconciliation
68
+ (0, _defineProperty2.default)(_this, "_pendingOnlineListener", void 0);
69
+ // Tracks whether caption restoration is pending (set on switch, cleared on restore)
70
+ (0, _defineProperty2.default)(_this, "_pendingCaptionRestore", false);
63
71
  /**
64
72
  * Process events from LLM channel
65
73
  * @param {Object} e - Event data
@@ -218,7 +226,8 @@ var VoiceaChannel = exports.VoiceaChannel = /*#__PURE__*/function (_EventEmitter
218
226
  * @returns {boolean}
219
227
  */
220
228
  (0, _defineProperty2.default)(_this, "isLLMConnected", function () {
221
- return _this.llmChannel.isConnected();
229
+ var _this$llmChannel$isCo, _this$llmChannel;
230
+ return (_this$llmChannel$isCo = (_this$llmChannel = _this.llmChannel) === null || _this$llmChannel === void 0 ? void 0 : _this$llmChannel.isConnected()) !== null && _this$llmChannel$isCo !== void 0 ? _this$llmChannel$isCo : false;
222
231
  });
223
232
  (0, _defineProperty2.default)(_this, "getKeepTranscriptionSubscribed", function () {
224
233
  return _this.keepTranscriptionSubscribed;
@@ -228,9 +237,10 @@ var VoiceaChannel = exports.VoiceaChannel = /*#__PURE__*/function (_EventEmitter
228
237
  * @returns {void}
229
238
  */
230
239
  (0, _defineProperty2.default)(_this, "sendAnnouncement", function () {
240
+ var llm = _this.requireLLMChannel();
231
241
  _this.announceStatus = _constants.ANNOUNCE_STATUS.JOINING;
232
- var socket = _this.llmChannel.getSocket();
233
- var binding = _this.llmChannel.getBinding();
242
+ var socket = llm.getSocket();
243
+ var binding = llm.getBinding();
234
244
  var payload = {
235
245
  id: "".concat(_this.seqNum),
236
246
  type: 'publishRequest',
@@ -262,7 +272,7 @@ var VoiceaChannel = exports.VoiceaChannel = /*#__PURE__*/function (_EventEmitter
262
272
  (0, _defineProperty2.default)(_this, "setSpokenLanguage", function (languageCode, languageAssignment) {
263
273
  return _this.request({
264
274
  method: 'PUT',
265
- url: "".concat(_this.llmChannel.getLocusUrl(), "/controls/"),
275
+ url: "".concat(_this.requireLLMChannel().getLocusUrl(), "/controls/"),
266
276
  body: {
267
277
  transcribe: _objectSpread({
268
278
  spokenLanguage: languageCode
@@ -285,8 +295,9 @@ var VoiceaChannel = exports.VoiceaChannel = /*#__PURE__*/function (_EventEmitter
285
295
  if (!_this.isLLMConnected()) {
286
296
  return;
287
297
  }
288
- var socket = _this.llmChannel.getSocket();
289
- var binding = _this.llmChannel.getBinding();
298
+ var llm = _this.requireLLMChannel();
299
+ var socket = llm.getSocket();
300
+ var binding = llm.getBinding();
290
301
  socket.send({
291
302
  id: "".concat(_this.seqNum),
292
303
  type: 'publishRequest',
@@ -321,8 +332,9 @@ var VoiceaChannel = exports.VoiceaChannel = /*#__PURE__*/function (_EventEmitter
321
332
  if (!_this.isLLMConnected()) {
322
333
  return;
323
334
  }
324
- var socket = _this.llmChannel.getSocket();
325
- var binding = _this.llmChannel.getBinding();
335
+ var llm = _this.requireLLMChannel();
336
+ var socket = llm.getSocket();
337
+ var binding = llm.getBinding();
326
338
  socket === null || socket === void 0 ? void 0 : socket.send({
327
339
  id: "".concat(_this.seqNum),
328
340
  type: 'publishRequest',
@@ -356,7 +368,7 @@ var VoiceaChannel = exports.VoiceaChannel = /*#__PURE__*/function (_EventEmitter
356
368
  */
357
369
  (0, _defineProperty2.default)(_this, "requestTurnOnCaptions", function (languageCode) {
358
370
  _this.captionStatus = _constants.TURN_ON_CAPTION_STATUS.SENDING;
359
- var locusUrl = _this.llmChannel.getLocusUrl();
371
+ var locusUrl = _this.requireLLMChannel().getLocusUrl();
360
372
  var body = {
361
373
  transcribe: {
362
374
  caption: true
@@ -377,7 +389,9 @@ var VoiceaChannel = exports.VoiceaChannel = /*#__PURE__*/function (_EventEmitter
377
389
  }, true);
378
390
  }).catch(function (error) {
379
391
  _this.captionStatus = _constants.TURN_ON_CAPTION_STATUS.IDLE;
380
- throw new Error('turn on captions fail');
392
+ throw new Error('turn on captions fail', {
393
+ cause: error
394
+ });
381
395
  });
382
396
  });
383
397
  /**
@@ -424,7 +438,7 @@ var VoiceaChannel = exports.VoiceaChannel = /*#__PURE__*/function (_EventEmitter
424
438
  return _regenerator.default.wrap(function (_context) {
425
439
  while (1) switch (_context.prev = _context.next) {
426
440
  case 0:
427
- if (!(_this.captionStatus === _constants.TURN_ON_CAPTION_STATUS.SENDING)) {
441
+ if (!_this.isCaptionProcessing()) {
428
442
  _context.next = 1;
429
443
  break;
430
444
  }
@@ -456,7 +470,7 @@ var VoiceaChannel = exports.VoiceaChannel = /*#__PURE__*/function (_EventEmitter
456
470
  (0, _defineProperty2.default)(_this, "toggleTranscribing", function (activate, spokenLanguage) {
457
471
  return _this.request({
458
472
  method: 'PUT',
459
- url: "".concat(_this.llmChannel.getLocusUrl(), "/controls/"),
473
+ url: "".concat(_this.requireLLMChannel().getLocusUrl(), "/controls/"),
460
474
  body: {
461
475
  transcribe: {
462
476
  transcribing: activate
@@ -480,7 +494,7 @@ var VoiceaChannel = exports.VoiceaChannel = /*#__PURE__*/function (_EventEmitter
480
494
  _this.toggleManualCaptionStatus = _constants.TOGGLE_MANUAL_CAPTION_STATUS.SENDING;
481
495
  return _this.request({
482
496
  method: 'PUT',
483
- url: "".concat(_this.llmChannel.getLocusUrl(), "/controls/"),
497
+ url: "".concat(_this.requireLLMChannel().getLocusUrl(), "/controls/"),
484
498
  body: {
485
499
  manualCaption: {
486
500
  enable: enable
@@ -550,6 +564,7 @@ var VoiceaChannel = exports.VoiceaChannel = /*#__PURE__*/function (_EventEmitter
550
564
  subscribe,
551
565
  _ref3$unsubscribe,
552
566
  unsubscribe,
567
+ llm,
553
568
  isDataChannelTokenEnabled,
554
569
  socket,
555
570
  datachannelUrl,
@@ -564,8 +579,9 @@ var VoiceaChannel = exports.VoiceaChannel = /*#__PURE__*/function (_EventEmitter
564
579
  }
565
580
  return _context2.abrupt("return");
566
581
  case 1:
582
+ llm = _this.requireLLMChannel();
567
583
  _context2.next = 2;
568
- return _this.llmChannel.isDataChannelTokenEnabled();
584
+ return llm.isDataChannelTokenEnabled();
569
585
  case 2:
570
586
  isDataChannelTokenEnabled = _context2.sent;
571
587
  if (isDataChannelTokenEnabled) {
@@ -574,8 +590,8 @@ var VoiceaChannel = exports.VoiceaChannel = /*#__PURE__*/function (_EventEmitter
574
590
  }
575
591
  return _context2.abrupt("return");
576
592
  case 3:
577
- socket = _this.llmChannel.getSocket();
578
- datachannelUrl = _this.llmChannel.getDatachannelUrl();
593
+ socket = llm.getSocket();
594
+ datachannelUrl = llm.getDatachannelUrl();
579
595
  socket.send({
580
596
  id: "".concat(_this.seqNum),
581
597
  type: 'subchannelSubscriptionRequest',
@@ -623,13 +639,30 @@ var VoiceaChannel = exports.VoiceaChannel = /*#__PURE__*/function (_EventEmitter
623
639
  _this.currentCaptionLanguage = undefined;
624
640
  _this.keepTranscriptionSubscribed = false;
625
641
 
626
- // Subscribe to relay events from the LLM channel
627
- _this.llmChannel.on('event:relay.event', _this.eventProcessor);
628
- _this.hasSubscribedToEvents = true;
642
+ // Subscribe to relay events from the LLM channel if provided
643
+ if (_this.llmChannel) {
644
+ _this.llmChannel.on('event:relay.event', _this.eventProcessor);
645
+ _this.hasSubscribedToEvents = true;
646
+ }
629
647
  return _this;
630
648
  }
649
+
650
+ /**
651
+ * Returns the LLM channel, throwing if not available.
652
+ * @private
653
+ * @returns {LLMChannel}
654
+ * @throws {Error} If LLM channel is not available
655
+ */
631
656
  (0, _inherits2.default)(VoiceaChannel, _EventEmitter);
632
657
  return (0, _createClass2.default)(VoiceaChannel, [{
658
+ key: "requireLLMChannel",
659
+ value: function requireLLMChannel() {
660
+ if (!this.llmChannel) {
661
+ throw new Error('VoiceaChannel: LLM channel not available');
662
+ }
663
+ return this.llmChannel;
664
+ }
665
+ }, {
633
666
  key: "deregisterEvents",
634
667
  value:
635
668
  /**
@@ -640,8 +673,14 @@ var VoiceaChannel = exports.VoiceaChannel = /*#__PURE__*/function (_EventEmitter
640
673
  this.areCaptionsEnabled = false;
641
674
  this.keepTranscriptionSubscribed = false;
642
675
  this.captionServiceId = undefined;
676
+ this.targetLLMChannel = undefined;
677
+ this._pendingCaptionRestore = false;
678
+
679
+ // Remove any pending online listener
680
+ this.detachPendingOnline();
643
681
  if (this.hasSubscribedToEvents) {
644
- this.llmChannel.off('event:relay.event', this.eventProcessor);
682
+ var _this$llmChannel2;
683
+ (_this$llmChannel2 = this.llmChannel) === null || _this$llmChannel2 === void 0 ? void 0 : _this$llmChannel2.off('event:relay.event', this.eventProcessor);
645
684
  this.hasSubscribedToEvents = false;
646
685
  }
647
686
  this.announceStatus = _constants.ANNOUNCE_STATUS.IDLE;
@@ -652,61 +691,109 @@ var VoiceaChannel = exports.VoiceaChannel = /*#__PURE__*/function (_EventEmitter
652
691
  }
653
692
 
654
693
  /**
655
- * Switch to a different LLM channel while preserving caption state.
656
- * Used when transitioning between main meeting and practice session.
657
- * - Preserves keepTranscriptionSubscribed and spokenLanguage state
658
- * - Unsubscribes from old channel, subscribes to new channel
659
- * - Re-announces and re-enables captions if they were on
660
- * @param {LLMChannel} newLLMChannel - The new LLM channel to switch to
661
- * @returns {Promise<void>}
694
+ * Attach a pending 'online' listener to a channel for deferred reconciliation.
695
+ * Detaches any existing listener first.
696
+ * @param {LLMChannel} channel - The channel to attach to
697
+ * @param {Function} callback - The callback to run when 'online' fires
698
+ * @private
699
+ * @returns {void}
662
700
  */
663
701
  }, {
664
- key: "switchLLMChannel",
665
- value: (function () {
666
- var _switchLLMChannel = (0, _asyncToGenerator2.default)(/*#__PURE__*/_regenerator.default.mark(function _callee3(newLLMChannel) {
667
- var captionsWereOn, spokenLanguage;
668
- return _regenerator.default.wrap(function (_context3) {
669
- while (1) switch (_context3.prev = _context3.next) {
670
- case 0:
671
- // Save current state
672
- captionsWereOn = this.keepTranscriptionSubscribed;
673
- spokenLanguage = this.currentSpokenLanguage; // Unsubscribe from old channel
674
- if (this.hasSubscribedToEvents && this.llmChannel) {
675
- this.llmChannel.off('event:relay.event', this.eventProcessor);
676
- this.hasSubscribedToEvents = false;
677
- }
702
+ key: "attachPendingOnline",
703
+ value: function attachPendingOnline(channel, callback) {
704
+ this.detachPendingOnline();
705
+ this._pendingOnlineListener = callback;
706
+ channel.once('online', this._pendingOnlineListener);
707
+ }
678
708
 
679
- // Switch to new channel
680
- this.llmChannel = newLLMChannel;
709
+ /**
710
+ * Detach any pending 'online' listener.
711
+ * @private
712
+ * @returns {void}
713
+ */
714
+ }, {
715
+ key: "detachPendingOnline",
716
+ value: function detachPendingOnline() {
717
+ if (this._pendingOnlineListener && this.llmChannel) {
718
+ this.llmChannel.off('online', this._pendingOnlineListener);
719
+ }
720
+ this._pendingOnlineListener = undefined;
721
+ }
681
722
 
682
- // Subscribe to new channel
683
- this.llmChannel.on('event:relay.event', this.eventProcessor);
684
- this.hasSubscribedToEvents = true;
723
+ /**
724
+ * Reset announcement state for a new connection.
725
+ * @private
726
+ * @returns {void}
727
+ */
728
+ }, {
729
+ key: "resetAnnounceState",
730
+ value: function resetAnnounceState() {
731
+ this.announceStatus = _constants.ANNOUNCE_STATUS.IDLE;
732
+ this.captionStatus = _constants.TURN_ON_CAPTION_STATUS.IDLE;
733
+ this.captionServiceId = undefined;
734
+ this.areCaptionsEnabled = false;
735
+ }
685
736
 
686
- // Reset announcement state for new connection
687
- this.announceStatus = _constants.ANNOUNCE_STATUS.IDLE;
688
- this.captionStatus = _constants.TURN_ON_CAPTION_STATUS.IDLE;
689
- this.captionServiceId = undefined;
690
- this.areCaptionsEnabled = false;
737
+ /**
738
+ * Reconcile voicea binding and caption state to match the desired target.
739
+ * This is idempotent - always re-reads targetLLMChannel and current state.
740
+ * A deferred 'online' callback that fires late self-corrects automatically.
741
+ * @private
742
+ * @returns {Promise<void>}
743
+ */
744
+ }, {
745
+ key: "reconcile",
746
+ value: function reconcile() {
747
+ var _this2 = this;
748
+ var target = this.targetLLMChannel;
749
+ if (!target) return _promise.default.resolve();
691
750
 
692
- // Re-announce and re-enable captions if they were on
693
- if (!captionsWereOn) {
694
- _context3.next = 1;
695
- break;
696
- }
697
- _context3.next = 1;
698
- return this.turnOnCaptions(spokenLanguage);
699
- case 1:
700
- case "end":
701
- return _context3.stop();
702
- }
703
- }, _callee3, this);
704
- }));
705
- function switchLLMChannel(_x2) {
706
- return _switchLLMChannel.apply(this, arguments);
751
+ // 1. Rebind relay-event subscription if actual != desired
752
+ if (this.llmChannel !== target) {
753
+ if (this.hasSubscribedToEvents && this.llmChannel) {
754
+ this.llmChannel.off('event:relay.event', this.eventProcessor);
755
+ }
756
+ this.detachPendingOnline();
757
+ this.llmChannel = target;
758
+ this.llmChannel.on('event:relay.event', this.eventProcessor);
759
+ this.hasSubscribedToEvents = true;
760
+ this.resetAnnounceState();
761
+ // Mark caption restoration as pending if captions were subscribed
762
+ this._pendingCaptionRestore = this.keepTranscriptionSubscribed;
707
763
  }
708
- return switchLLMChannel;
709
- }())
764
+
765
+ // 2. Restore captions if pending
766
+ if (!this._pendingCaptionRestore) return _promise.default.resolve();
767
+ if (this.isLLMConnected()) {
768
+ // Channel is connected, restore captions immediately
769
+ this._pendingCaptionRestore = false;
770
+ return this.turnOnCaptions(this.currentSpokenLanguage).then(function () {
771
+ return undefined;
772
+ }).catch(function () {
773
+ // Best-effort restoration
774
+ });
775
+ }
776
+ // Channel not yet connected - defer until 'online' then re-reconcile.
777
+ // If target changes meanwhile, reconcile() self-corrects.
778
+ this.attachPendingOnline(target, function () {
779
+ _this2.reconcile();
780
+ });
781
+ return _promise.default.resolve();
782
+ }
783
+
784
+ /**
785
+ * Switch to a different LLM channel while preserving caption state.
786
+ * Used when transitioning between main meeting and practice session.
787
+ * This is a thin intent-setter - actual binding happens in reconcile().
788
+ * @param {LLMChannel} newLLMChannel - The new LLM channel to switch to
789
+ * @returns {Promise<void>}
790
+ */
791
+ }, {
792
+ key: "switchLLMChannel",
793
+ value: function switchLLMChannel(newLLMChannel) {
794
+ this.targetLLMChannel = newLLMChannel;
795
+ return this.reconcile();
796
+ }
710
797
  }]);
711
798
  }(_events.default);
712
799
  var _default = exports.default = VoiceaChannel;