ttp-agent-sdk 2.34.3 → 2.34.5

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.
@@ -12744,6 +12744,7 @@ var TTPChatWidget = /*#__PURE__*/function () {
12744
12744
  onAudioStartPlaying: this.config.onAudioStartPlaying,
12745
12745
  onAudioStoppedPlaying: this.config.onAudioStoppedPlaying,
12746
12746
  onSubtitleDisplay: this.config.onSubtitleDisplay,
12747
+ onVoiceCallButtonClick: this.config.onVoiceCallButtonClick,
12747
12748
  // Callback when call ends - show landing screen (only in unified mode)
12748
12749
  // BUT: Don't return to landing if we're showing a domain error
12749
12750
  onCallEnd: function onCallEnd() {
@@ -13185,6 +13186,7 @@ var TTPChatWidget = /*#__PURE__*/function () {
13185
13186
  onAudioStartPlaying: userConfig.onAudioStartPlaying,
13186
13187
  onAudioStoppedPlaying: userConfig.onAudioStoppedPlaying,
13187
13188
  onSubtitleDisplay: userConfig.onSubtitleDisplay,
13189
+ onVoiceCallButtonClick: userConfig.onVoiceCallButtonClick,
13188
13190
  // Legacy support (for backward compatibility)
13189
13191
  primaryColor: primaryColor
13190
13192
  }, typeof userConfig.position === 'string' ? {
@@ -13669,6 +13671,17 @@ var TTPChatWidget = /*#__PURE__*/function () {
13669
13671
  console.log('⚠️ Call already starting, ignoring duplicate click');
13670
13672
  return _context.a(2);
13671
13673
  case 1:
13674
+ // Call the callback if provided
13675
+ if (typeof _this6.config.onVoiceCallButtonClick === 'function') {
13676
+ try {
13677
+ _this6.config.onVoiceCallButtonClick({
13678
+ timestamp: Date.now(),
13679
+ widgetState: 'start'
13680
+ });
13681
+ } catch (error) {
13682
+ console.error('Error in onVoiceCallButtonClick callback:', error);
13683
+ }
13684
+ }
13672
13685
  _this6.isStartingCall = true;
13673
13686
  _context.p = 2;
13674
13687
  // Show voice interface first (needed for UI state)
@@ -14533,6 +14546,7 @@ var TTPChatWidget = /*#__PURE__*/function () {
14533
14546
  mergedConfig.onAudioStartPlaying = newConfig.onAudioStartPlaying !== undefined ? newConfig.onAudioStartPlaying : this.config.onAudioStartPlaying;
14534
14547
  mergedConfig.onAudioStoppedPlaying = newConfig.onAudioStoppedPlaying !== undefined ? newConfig.onAudioStoppedPlaying : this.config.onAudioStoppedPlaying;
14535
14548
  mergedConfig.onSubtitleDisplay = newConfig.onSubtitleDisplay !== undefined ? newConfig.onSubtitleDisplay : this.config.onSubtitleDisplay;
14549
+ mergedConfig.onVoiceCallButtonClick = newConfig.onVoiceCallButtonClick !== undefined ? newConfig.onVoiceCallButtonClick : this.config.onVoiceCallButtonClick;
14536
14550
 
14537
14551
  // Merge useShadowDOM if provided
14538
14552
  if (newConfig.useShadowDOM !== undefined) {
@@ -14541,7 +14555,7 @@ var TTPChatWidget = /*#__PURE__*/function () {
14541
14555
 
14542
14556
  // Merge any other top-level properties
14543
14557
  Object.keys(newConfig).forEach(function (key) {
14544
- if (!['panel', 'button', 'header', 'footer', 'icon', 'messages', 'direction', 'voice', 'text', 'animation', 'behavior', 'accessibility', 'language', 'tooltips', 'landing', 'primaryColor', 'useShadowDOM', 'onConversationStart', 'onConversationEnd', 'onBargeIn', 'onAudioStartPlaying', 'onAudioStoppedPlaying', 'onSubtitleDisplay'].includes(key)) {
14558
+ if (!['panel', 'button', 'header', 'footer', 'icon', 'messages', 'direction', 'voice', 'text', 'animation', 'behavior', 'accessibility', 'language', 'tooltips', 'landing', 'primaryColor', 'useShadowDOM', 'onConversationStart', 'onConversationEnd', 'onBargeIn', 'onAudioStartPlaying', 'onAudioStoppedPlaying', 'onSubtitleDisplay', 'onVoiceCallButtonClick'].includes(key)) {
14545
14559
  mergedConfig[key] = newConfig[key];
14546
14560
  }
14547
14561
  });
@@ -17119,7 +17133,19 @@ var VoiceInterface = /*#__PURE__*/function () {
17119
17133
  var activeState = this.shadowRoot.getElementById('voiceActiveState');
17120
17134
  if (activeState) activeState.style.display = 'none';
17121
17135
 
17122
- // Call the callback if provided (for unified mode - shows landing screen)
17136
+ // Call the public callback if provided (for user tracking/analytics)
17137
+ if (typeof this.config.onVoiceCallButtonClick === 'function') {
17138
+ try {
17139
+ this.config.onVoiceCallButtonClick({
17140
+ timestamp: Date.now(),
17141
+ widgetState: 'end'
17142
+ });
17143
+ } catch (error) {
17144
+ console.error('Error in onVoiceCallButtonClick callback:', error);
17145
+ }
17146
+ }
17147
+
17148
+ // Call the internal callback if provided (for unified mode - shows landing screen)
17123
17149
  // Otherwise show idle state (for voice-only mode or backward compatibility)
17124
17150
  if (this.config.onCallEnd) {
17125
17151
  this.config.onCallEnd();
@@ -23031,9 +23057,18 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
23031
23057
 
23032
23058
  // Queue for prepared AudioBuffers (ready to schedule)
23033
23059
  _this.preparedBuffer = [];
23060
+
23061
+ // Maximum buffer sizes to prevent unbounded memory growth
23062
+ // If backend sends sentences faster than playback, oldest frames are dropped
23063
+ _this.MAX_PREPARED_BUFFER_SIZE = 200; // Max prepared frames (~10-12 seconds at 600ms per frame)
23064
+ _this.MAX_PCM_CHUNK_QUEUE_SIZE = 50; // Max raw PCM chunks
23065
+
23034
23066
  _this.isProcessingPcmQueue = false;
23035
23067
  _this.isSchedulingFrames = false;
23036
23068
 
23069
+ // Timeout to detect empty sentences (audio_start but no chunks)
23070
+ _this._emptySentenceTimeout = null;
23071
+
23037
23072
  // Minimal scheduling delay to avoid scheduling audio in the past
23038
23073
  // REMOVED: Lookahead buffering was causing quality degradation due to browser resampling/timing issues
23039
23074
  // Now we only schedule with minimal delay (20ms) just enough to avoid gaps
@@ -23049,6 +23084,10 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
23049
23084
  // Cleared when markNewSentence() is called (signals new audio is starting)
23050
23085
  _this._isStopped = false;
23051
23086
 
23087
+ // Track current sentence ID to reject chunks from previous sentences
23088
+ // Incremented each time markNewSentence() is called
23089
+ _this._currentSentenceId = 0;
23090
+
23052
23091
  // Codec registry
23053
23092
 
23054
23093
  _this.codecs = {
@@ -23185,7 +23224,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
23185
23224
  value: (function () {
23186
23225
  var _playChunk = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(pcmData) {
23187
23226
  var _this3 = this;
23188
- var preparedFrame, _this$audioContext;
23227
+ var preparedFrame, dropped, _this$audioContext;
23189
23228
  return _regenerator().w(function (_context) {
23190
23229
  while (1) switch (_context.n) {
23191
23230
  case 0:
@@ -23199,6 +23238,20 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
23199
23238
  // Pre-process frame immediately (convert to AudioBuffer)
23200
23239
  preparedFrame = this.prepareChunk(pcmData);
23201
23240
  if (preparedFrame) {
23241
+ // CRITICAL: Clear empty sentence timeout since chunks are arriving
23242
+ // This resets the timer for the current sentence
23243
+ if (this._emptySentenceTimeout) {
23244
+ clearTimeout(this._emptySentenceTimeout);
23245
+ this._emptySentenceTimeout = null;
23246
+ }
23247
+
23248
+ // CRITICAL: Prevent unbounded buffer growth
23249
+ // If backend sends sentences faster than playback, drop oldest frames
23250
+ if (this.preparedBuffer.length >= this.MAX_PREPARED_BUFFER_SIZE) {
23251
+ dropped = this.preparedBuffer.shift(); // Drop oldest frame
23252
+ console.warn("\u26A0\uFE0F AudioPlayer: preparedBuffer at max size (".concat(this.MAX_PREPARED_BUFFER_SIZE, "), dropped oldest frame"));
23253
+ }
23254
+
23202
23255
  // Add prepared frame to buffer
23203
23256
  this.preparedBuffer.push(preparedFrame);
23204
23257
 
@@ -23206,7 +23259,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
23206
23259
 
23207
23260
  // Use requestAnimationFrame to avoid blocking, but ensure scheduling happens
23208
23261
 
23209
- if (!this.isSchedulingFrames) {
23262
+ if (!this.isSchedulingFrames && !this._isStopped) {
23210
23263
  // Schedule immediately if not already scheduling
23211
23264
 
23212
23265
  this.schedulePreparedFrames();
@@ -23218,7 +23271,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
23218
23271
  // Use a short timeout to ensure we check again after current scheduling completes
23219
23272
 
23220
23273
  setTimeout(function () {
23221
- if (_this3.preparedBuffer.length > 0 && !_this3.isSchedulingFrames) {
23274
+ if (_this3.preparedBuffer.length > 0 && !_this3.isSchedulingFrames && !_this3._isStopped) {
23222
23275
  _this3.schedulePreparedFrames();
23223
23276
  }
23224
23277
  }, 5); // Very short delay to check after current scheduling completes
@@ -23357,7 +23410,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
23357
23410
  value: (function () {
23358
23411
  var _schedulePreparedFrames = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
23359
23412
  var _this4 = this;
23360
- var queuedFrames, targetLookaheadFrames, _this$audioContext2, _this$audioContext3, _this$audioContext4, _this$audioContext5, scheduledCount, _loop, _t;
23413
+ var queuedFrames, targetLookaheadFrames, _this$audioContext2, _this$audioContext3, _this$audioContext4, _this$audioContext5, scheduledCount, _loop, _ret, _t;
23361
23414
  return _regenerator().w(function (_context3) {
23362
23415
  while (1) switch (_context3.p = _context3.n) {
23363
23416
  case 0:
@@ -23409,14 +23462,21 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
23409
23462
  return _regenerator().w(function (_context2) {
23410
23463
  while (1) switch (_context2.n) {
23411
23464
  case 0:
23465
+ if (!_this4._isStopped) {
23466
+ _context2.n = 1;
23467
+ break;
23468
+ }
23469
+ console.log('🛑 AudioPlayer: Stopping frame scheduling - playback was stopped');
23470
+ return _context2.a(2, 0);
23471
+ case 1:
23412
23472
  // Get next prepared frame
23413
23473
  preparedFrame = _this4.preparedBuffer.shift();
23414
23474
  if (preparedFrame) {
23415
- _context2.n = 1;
23475
+ _context2.n = 2;
23416
23476
  break;
23417
23477
  }
23418
- return _context2.a(2, 1);
23419
- case 1:
23478
+ return _context2.a(2, 0);
23479
+ case 2:
23420
23480
  // Create source and schedule playback
23421
23481
  source = _this4.audioContext.createBufferSource();
23422
23482
  source.buffer = preparedFrame.buffer;
@@ -23493,29 +23553,46 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
23493
23553
  // Track when this buffer finishes (for cleanup only)
23494
23554
 
23495
23555
  source.onended = function () {
23496
- // Remove from tracked sources
23556
+ // CRITICAL: Check if playback was stopped before processing cleanup
23557
+ // This prevents race conditions where onended fires after stopImmediate() was called
23558
+ if (_this4._isStopped) {
23559
+ // Playback was stopped, ignore this callback
23560
+ return;
23561
+ }
23562
+
23563
+ // CRITICAL: Only process if source is still in scheduledSources set
23564
+ // This prevents race conditions where stopImmediate() cleared the set but callback fires later
23565
+ if (!_this4.scheduledSources.has(source)) {
23566
+ // Source was already removed (probably by stopImmediate), ignore this callback
23567
+ return;
23568
+ }
23497
23569
 
23570
+ // Remove from tracked sources
23498
23571
  _this4.scheduledSources.delete(source);
23499
- _this4.scheduledBuffers--;
23572
+
23573
+ // Only decrement if we're still playing and buffer count is positive
23574
+ if (!_this4._isStopped && _this4.scheduledBuffers > 0) {
23575
+ _this4.scheduledBuffers--;
23576
+ }
23500
23577
 
23501
23578
  // If no more scheduled buffers and no prepared frames, playback is complete
23502
23579
 
23503
- if (_this4.scheduledBuffers === 0 && _this4.preparedBuffer.length === 0 && _this4.pcmChunkQueue.length === 0) {
23580
+ if (_this4.scheduledBuffers === 0 && _this4.preparedBuffer.length === 0 && _this4.pcmChunkQueue.length === 0 && !_this4._isStopped) {
23504
23581
  _this4.isPlaying = false;
23505
23582
  _this4.isSchedulingFrames = false;
23506
23583
  console.log('🛑 AudioPlayer: Emitting playbackStopped event (all buffers finished)');
23507
23584
  _this4.emit('playbackStopped');
23508
- } else if (_this4.preparedBuffer.length > 0) {
23585
+ } else if (_this4.preparedBuffer.length > 0 && !_this4._isStopped) {
23509
23586
  // More frames available, schedule them immediately
23510
23587
 
23511
23588
  // Use setTimeout to avoid blocking, but schedule quickly
23512
23589
 
23513
23590
  setTimeout(function () {
23514
- if (_this4.preparedBuffer.length > 0 && !_this4.isSchedulingFrames) {
23591
+ if (_this4.preparedBuffer.length > 0 && !_this4.isSchedulingFrames && !_this4._isStopped) {
23515
23592
  _this4.schedulePreparedFrames();
23516
23593
  }
23517
23594
  }, 0);
23518
- } else if (_this4.scheduledBuffers > 0) {
23595
+ } else if (_this4.scheduledBuffers > 0 && !_this4._isStopped) {
23519
23596
  // No more prepared frames but still have scheduled buffers playing
23520
23597
 
23521
23598
  // Set up a check to schedule new frames when they arrive
@@ -23523,9 +23600,13 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
23523
23600
  // Keep checking periodically until we have no more scheduled buffers
23524
23601
 
23525
23602
  var _checkForMoreFrames = function checkForMoreFrames() {
23603
+ // CRITICAL: Check if stopped before scheduling
23604
+ if (_this4._isStopped) {
23605
+ return;
23606
+ }
23526
23607
  if (_this4.preparedBuffer.length > 0 && !_this4.isSchedulingFrames && _this4.scheduledBuffers > 0) {
23527
23608
  _this4.schedulePreparedFrames();
23528
- } else if (_this4.scheduledBuffers > 0) {
23609
+ } else if (_this4.scheduledBuffers > 0 && !_this4._isStopped) {
23529
23610
  // Keep checking - frames might arrive soon
23530
23611
 
23531
23612
  setTimeout(_checkForMoreFrames, 10);
@@ -23539,7 +23620,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
23539
23620
  console.log('🎵 AudioPlayer: Emitting playbackStarted event');
23540
23621
  _this4.emit('playbackStarted');
23541
23622
  }
23542
- case 2:
23623
+ case 3:
23543
23624
  return _context2.a(2);
23544
23625
  }
23545
23626
  }, _loop);
@@ -23551,7 +23632,8 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
23551
23632
  }
23552
23633
  return _context3.d(_regeneratorValues(_loop()), 6);
23553
23634
  case 6:
23554
- if (!_context3.v) {
23635
+ _ret = _context3.v;
23636
+ if (!(_ret === 0)) {
23555
23637
  _context3.n = 7;
23556
23638
  break;
23557
23639
  }
@@ -23568,11 +23650,11 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
23568
23650
 
23569
23651
  // Use requestAnimationFrame for smooth scheduling without blocking
23570
23652
 
23571
- if (this.preparedBuffer.length > 0) {
23653
+ if (this.preparedBuffer.length > 0 && !this._isStopped) {
23572
23654
  // More frames arrived, schedule them immediately
23573
23655
 
23574
23656
  requestAnimationFrame(function () {
23575
- if (_this4.preparedBuffer.length > 0 && !_this4.isSchedulingFrames) {
23657
+ if (_this4.preparedBuffer.length > 0 && !_this4.isSchedulingFrames && !_this4._isStopped) {
23576
23658
  _this4.schedulePreparedFrames();
23577
23659
  }
23578
23660
  });
@@ -23580,19 +23662,19 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
23580
23662
 
23581
23663
  // Always set up a periodic check if we have scheduled buffers playing
23582
23664
  // This ensures continuous playback even if frames arrive slowly
23583
- if (this.scheduledBuffers > 0) {
23665
+ if (this.scheduledBuffers > 0 && !this._isStopped) {
23584
23666
  // Set up a periodic check to schedule new frames as they arrive
23585
23667
  // Use a shorter interval to catch new frames quickly
23586
23668
  setTimeout(function () {
23587
- if (_this4.preparedBuffer.length > 0 && !_this4.isSchedulingFrames && _this4.scheduledBuffers > 0) {
23669
+ if (_this4.preparedBuffer.length > 0 && !_this4.isSchedulingFrames && _this4.scheduledBuffers > 0 && !_this4._isStopped) {
23588
23670
  _this4.schedulePreparedFrames();
23589
- } else if (_this4.scheduledBuffers > 0) {
23671
+ } else if (_this4.scheduledBuffers > 0 && !_this4._isStopped) {
23590
23672
  // Keep checking even if no frames yet - they might arrive soon
23591
23673
 
23592
23674
  // Recursively check until we have no more scheduled buffers
23593
23675
 
23594
23676
  setTimeout(function () {
23595
- if (_this4.preparedBuffer.length > 0 && !_this4.isSchedulingFrames && _this4.scheduledBuffers > 0) {
23677
+ if (_this4.preparedBuffer.length > 0 && !_this4.isSchedulingFrames && _this4.scheduledBuffers > 0 && !_this4._isStopped) {
23596
23678
  _this4.schedulePreparedFrames();
23597
23679
  }
23598
23680
  }, 10);
@@ -23659,16 +23741,23 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
23659
23741
  return this.waitForAudioContextReady();
23660
23742
  case 4:
23661
23743
  if (!(this.pcmChunkQueue.length > 0)) {
23662
- _context4.n = 6;
23744
+ _context4.n = 7;
23663
23745
  break;
23664
23746
  }
23747
+ if (!this._isStopped) {
23748
+ _context4.n = 5;
23749
+ break;
23750
+ }
23751
+ console.log('🛑 AudioPlayer: Stopping PCM queue processing - playback was stopped');
23752
+ return _context4.a(3, 7);
23753
+ case 5:
23665
23754
  pcmData = this.pcmChunkQueue.shift();
23666
23755
  if (pcmData) {
23667
- _context4.n = 5;
23756
+ _context4.n = 6;
23668
23757
  break;
23669
23758
  }
23670
23759
  return _context4.a(3, 4);
23671
- case 5:
23760
+ case 6:
23672
23761
  // Ensure even byte count for 16-bit PCM
23673
23762
  processedData = pcmData;
23674
23763
  if (pcmData.byteLength % 2 !== 0) {
@@ -23762,23 +23851,23 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
23762
23851
  }
23763
23852
  _context4.n = 4;
23764
23853
  break;
23765
- case 6:
23854
+ case 7:
23766
23855
  // end while loop
23767
23856
 
23768
23857
  // All chunks scheduled, reset processing flag
23769
23858
  this.isProcessingPcmQueue = false;
23770
- _context4.n = 8;
23859
+ _context4.n = 9;
23771
23860
  break;
23772
- case 7:
23773
- _context4.p = 7;
23861
+ case 8:
23862
+ _context4.p = 8;
23774
23863
  _t2 = _context4.v;
23775
23864
  console.error('❌ AudioPlayer v2: Error playing chunk:', _t2);
23776
23865
  this.emit('playbackError', _t2);
23777
23866
  this.isProcessingPcmQueue = false;
23778
- case 8:
23867
+ case 9:
23779
23868
  return _context4.a(2);
23780
23869
  }
23781
- }, _callee3, this, [[3, 7]]);
23870
+ }, _callee3, this, [[3, 8]]);
23782
23871
  }));
23783
23872
  function processPcmQueue() {
23784
23873
  return _processPcmQueue.apply(this, arguments);
@@ -24638,6 +24727,15 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
24638
24727
  console.log(' scheduledSources.size:', this.scheduledSources.size);
24639
24728
  console.log(' scheduledBuffers:', this.scheduledBuffers);
24640
24729
 
24730
+ // CRITICAL: Set stopped flag FIRST to prevent new audio from being queued/scheduled
24731
+ // This prevents race conditions where audio chunks arrive after stop but before sources are stopped
24732
+ // The flag will be cleared when markNewSentence() is called (signals new audio is starting)
24733
+ this._isStopped = true;
24734
+
24735
+ // CRITICAL: Stop scheduling immediately to prevent frames from being scheduled
24736
+ // This ensures schedulePreparedFrames() loop will exit on next _isStopped check
24737
+ this.isSchedulingFrames = false;
24738
+
24641
24739
  // Stop current source (legacy queue-based system)
24642
24740
 
24643
24741
  if (this.currentSource) {
@@ -24659,27 +24757,31 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
24659
24757
  if (this.scheduledSources.size > 0) {
24660
24758
  console.log(" Stopping ".concat(this.scheduledSources.size, " scheduled sources..."));
24661
24759
  var stoppedCount = 0;
24662
- var _iterator = _createForOfIteratorHelper(this.scheduledSources),
24663
- _step;
24664
- try {
24665
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
24666
- var source = _step.value;
24667
- try {
24668
- source.stop();
24669
- stoppedCount++;
24670
- } catch (e) {
24671
- // Ignore if already stopped or not started yet
24672
24760
 
24673
- console.log(' Source already stopped or not started:', e.message);
24674
- }
24761
+ // Store sources to stop and count before clearing the set
24762
+ var sourcesToStop = Array.from(this.scheduledSources);
24763
+ var sourcesCount = sourcesToStop.length;
24764
+
24765
+ // Clear the set BEFORE stopping sources to prevent onended callbacks from modifying it
24766
+ // This ensures onended callbacks will see empty set and return early
24767
+ this.scheduledSources.clear();
24768
+
24769
+ // CRITICAL: Reset scheduledBuffers to 0 BEFORE stopping sources
24770
+ // This prevents onended callbacks from decrementing it below 0
24771
+ // Any onended callbacks that fire will see scheduledSources is empty and return early
24772
+ this.scheduledBuffers = 0;
24773
+ for (var _i = 0, _sourcesToStop = sourcesToStop; _i < _sourcesToStop.length; _i++) {
24774
+ var source = _sourcesToStop[_i];
24775
+ try {
24776
+ source.stop();
24777
+ stoppedCount++;
24778
+ } catch (e) {
24779
+ // Ignore if already stopped or not started yet
24780
+
24781
+ console.log(' Source already stopped or not started:', e.message);
24675
24782
  }
24676
- } catch (err) {
24677
- _iterator.e(err);
24678
- } finally {
24679
- _iterator.f();
24680
24783
  }
24681
- console.log(" Stopped ".concat(stoppedCount, " sources"));
24682
- this.scheduledSources.clear();
24784
+ console.log(" Stopped ".concat(stoppedCount, " sources (cleared ").concat(sourcesCount, " from scheduledSources)"));
24683
24785
  }
24684
24786
 
24685
24787
  // Clear state
@@ -24695,15 +24797,15 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
24695
24797
  this.isProcessingPcmQueue = false;
24696
24798
  this.isSchedulingFrames = false;
24697
24799
 
24698
- // CRITICAL: Set stopped flag to prevent new audio from being queued
24699
- // This prevents race conditions where audio chunks arrive after stop but before sources are stopped
24700
- // The flag will be cleared when markNewSentence() is called (signals new audio is starting)
24701
- this._isStopped = true;
24800
+ // Clear empty sentence timeout (barge-in means sentence was interrupted, not empty)
24801
+ if (this._emptySentenceTimeout) {
24802
+ clearTimeout(this._emptySentenceTimeout);
24803
+ this._emptySentenceTimeout = null;
24804
+ }
24702
24805
 
24703
24806
  // Reset scheduling properties
24704
-
24807
+ // Note: scheduledBuffers was already reset to 0 above when clearing scheduledSources
24705
24808
  this.nextStartTime = 0;
24706
- this.scheduledBuffers = 0;
24707
24809
 
24708
24810
  // Clear transcript state
24709
24811
  this.clearTranscriptState();
@@ -24723,14 +24825,77 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
24723
24825
  }, {
24724
24826
  key: "markNewSentence",
24725
24827
  value: function markNewSentence(text) {
24726
- // CRITICAL: Clear stopped flag when new audio starts
24828
+ var _this9 = this;
24829
+ var wasStopped = this._isStopped;
24830
+ var isCurrentlyPlaying = this.isPlaying || this.scheduledSources.size > 0;
24831
+
24832
+ // CRITICAL: Clear stopped flag when new audio starts (after barge-in)
24727
24833
  // This allows new audio chunks to be queued after barge-in
24728
24834
  if (this._isStopped) {
24729
- console.log('🛑 AudioPlayer: Clearing stopped flag - new audio starting');
24835
+ console.log('🛑 AudioPlayer: Clearing stopped flag - new audio starting after barge-in');
24730
24836
  this._isStopped = false;
24837
+
24838
+ // CRITICAL: Reset scheduling state when starting a new sentence after a stop
24839
+ // This ensures the new sentence starts immediately without delay
24840
+ // Reset nextStartTime so first chunk schedules immediately (not in the past)
24841
+ this.nextStartTime = 0;
24842
+
24843
+ // CRITICAL: Reset scheduledBuffers to 0, but ensure it's not negative
24844
+ // This accounts for any onended callbacks that might fire from stopped sources
24845
+ // If scheduledBuffers is negative, it means onended callbacks fired after stopImmediate()
24846
+ // In that case, we should reset to 0 to start fresh
24847
+ if (this.scheduledBuffers < 0) {
24848
+ console.log("\uD83D\uDD04 AudioPlayer: scheduledBuffers was negative (".concat(this.scheduledBuffers, "), resetting to 0"));
24849
+ }
24850
+ this.scheduledBuffers = 0;
24851
+
24852
+ // CRITICAL: ALWAYS clear preparedBuffer after barge-in - any remaining chunks are from previous sentence
24853
+ // This prevents old audio from playing when new sentence starts after interruption
24854
+ if (this.preparedBuffer.length > 0) {
24855
+ console.log("\uD83D\uDD04 AudioPlayer: Clearing ".concat(this.preparedBuffer.length, " prepared frames from previous sentence (after barge-in)"));
24856
+ }
24857
+ this.preparedBuffer = [];
24858
+
24859
+ // CRITICAL: Also clear pcmChunkQueue to prevent raw chunks from previous sentence being processed
24860
+ if (this.pcmChunkQueue.length > 0) {
24861
+ console.log("\uD83D\uDD04 AudioPlayer: Clearing ".concat(this.pcmChunkQueue.length, " raw PCM chunks from previous sentence (after barge-in)"));
24862
+ }
24863
+ this.pcmChunkQueue = [];
24864
+ this.isProcessingPcmQueue = false;
24865
+ console.log('🔄 AudioPlayer: Reset scheduling state for new sentence (after barge-in)');
24866
+ } else if (isCurrentlyPlaying) {
24867
+ // New sentence received while audio is currently playing (no barge-in)
24868
+ // Don't clear buffers or reset state - let current sentence finish, then new one will play
24869
+ console.log("\uD83D\uDCDD AudioPlayer: New sentence queued while audio playing - will start after current sentence finishes");
24731
24870
  }
24871
+
24872
+ // Always update pending sentence text (for transcript display)
24732
24873
  this.pendingSentenceText = text;
24733
- console.log("\uD83D\uDCDD AudioPlayer: New sentence marked: \"".concat(text.substring(0, 40), "...\""));
24874
+ console.log("\uD83D\uDCDD AudioPlayer: New sentence marked: \"".concat(text.substring(0, 40), "...\" (wasStopped: ").concat(wasStopped, ", isPlaying: ").concat(isCurrentlyPlaying, ")"));
24875
+
24876
+ // CRITICAL: Set timeout to detect empty sentences (audio_start but no chunks)
24877
+ // This prevents queue blocking if backend sends audio_start but no audio chunks follow
24878
+ if (this._emptySentenceTimeout) {
24879
+ clearTimeout(this._emptySentenceTimeout);
24880
+ }
24881
+ var sentenceText = text; // Capture for timeout callback
24882
+ this._emptySentenceTimeout = setTimeout(function () {
24883
+ // Check if this sentence still has no chunks after timeout
24884
+ if (_this9.pendingSentenceText === sentenceText && _this9.scheduledBuffers === 0 && _this9.preparedBuffer.length === 0 && _this9.pcmChunkQueue.length === 0 && !_this9._isStopped) {
24885
+ console.warn("\u26A0\uFE0F AudioPlayer: Empty sentence detected after 5s timeout - no chunks received for: \"".concat(sentenceText.substring(0, 40), "...\""));
24886
+ // Clear pending sentence to unblock next sentence
24887
+ if (_this9.pendingSentenceText === sentenceText) {
24888
+ _this9.pendingSentenceText = null;
24889
+ }
24890
+ // Emit playbackStopped to allow next sentence to start
24891
+ // Only if we're not currently playing (to avoid interrupting real playback)
24892
+ if (!_this9.isPlaying && _this9.scheduledSources.size === 0) {
24893
+ console.log('🛑 AudioPlayer: Emitting playbackStopped for empty sentence timeout');
24894
+ _this9.emit('playbackStopped');
24895
+ }
24896
+ }
24897
+ _this9._emptySentenceTimeout = null;
24898
+ }, 5000); // 5 second timeout - adjust based on expected chunk arrival rate
24734
24899
  }
24735
24900
 
24736
24901
  /**
@@ -24739,35 +24904,35 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
24739
24904
  }, {
24740
24905
  key: "startTranscriptChecker",
24741
24906
  value: function startTranscriptChecker() {
24742
- var _this9 = this;
24907
+ var _this0 = this;
24743
24908
  if (this.isCheckingTranscripts) return;
24744
24909
  this.isCheckingTranscripts = true;
24745
24910
  console.log('📝 AudioPlayer: Transcript checker started');
24746
24911
  var _checkLoop = function checkLoop() {
24747
- if (!_this9.isCheckingTranscripts || !_this9.audioContext) return;
24748
- var currentTime = _this9.audioContext.currentTime;
24749
- var _iterator2 = _createForOfIteratorHelper(_this9.sentenceTimings),
24750
- _step2;
24912
+ if (!_this0.isCheckingTranscripts || !_this0.audioContext) return;
24913
+ var currentTime = _this0.audioContext.currentTime;
24914
+ var _iterator = _createForOfIteratorHelper(_this0.sentenceTimings),
24915
+ _step;
24751
24916
  try {
24752
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
24753
- var timing = _step2.value;
24917
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
24918
+ var timing = _step.value;
24754
24919
  if (!timing.displayed && currentTime >= timing.startTime) {
24755
24920
  timing.displayed = true;
24756
24921
  console.log("\uD83D\uDCDD AudioPlayer: Display transcript at ".concat(currentTime.toFixed(3), "s: \"").concat(timing.text.substring(0, 40), "...\""));
24757
- _this9.emit('transcriptDisplay', {
24922
+ _this0.emit('transcriptDisplay', {
24758
24923
  text: timing.text
24759
24924
  });
24760
24925
  }
24761
24926
  }
24762
24927
  } catch (err) {
24763
- _iterator2.e(err);
24928
+ _iterator.e(err);
24764
24929
  } finally {
24765
- _iterator2.f();
24930
+ _iterator.f();
24766
24931
  }
24767
- if (_this9.isPlaying || _this9.scheduledBuffers > 0) {
24932
+ if (_this0.isPlaying || _this0.scheduledBuffers > 0) {
24768
24933
  requestAnimationFrame(_checkLoop);
24769
24934
  } else {
24770
- _this9.isCheckingTranscripts = false;
24935
+ _this0.isCheckingTranscripts = false;
24771
24936
  console.log('📝 AudioPlayer: Transcript checker stopped');
24772
24937
  }
24773
24938
  };
@@ -26129,15 +26294,16 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
26129
26294
  // Store the text in AudioPlayer for synced display when audio actually starts playing
26130
26295
  console.log('📝 VoiceSDK v2: Received audio_start with text:', message.text);
26131
26296
 
26132
- // CRITICAL: Stop any currently playing audio when new sentence starts
26133
- // This prevents overlapping audio from multiple sentences playing simultaneously
26297
+ // NOTE: We do NOT stop current audio here - that only happens on user barge-in (stop_playing)
26298
+ // If audio is already playing, the new sentence will queue and wait for current one to finish
26299
+ // This allows sentences to play sequentially without interruption
26134
26300
  if (this.audioPlayer && (this.audioPlayer.isPlaying || ((_this$audioPlayer$sch = this.audioPlayer.scheduledSources) === null || _this$audioPlayer$sch === void 0 ? void 0 : _this$audioPlayer$sch.size) > 0)) {
26135
- console.log('🛑 VoiceSDK v2: Stopping current audio playback - new sentence starting');
26136
- this.audioPlayer.stopImmediate();
26301
+ console.log('📝 VoiceSDK v2: New sentence received while audio playing - will queue and wait for current sentence to finish');
26137
26302
  }
26138
26303
  if (message.text && this.audioPlayer) {
26139
26304
  // Use AudioPlayer's transcript sync mechanism
26140
26305
  // AudioPlayer will emit transcriptDisplay when audio actually starts playing (synced with audioContext.currentTime)
26306
+ // If audio is currently playing, markNewSentence will queue this sentence
26141
26307
  this.audioPlayer.markNewSentence(message.text);
26142
26308
  }
26143
26309
  // Also emit as message for other listeners