botium-connector-voip 0.0.30 → 0.0.31

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.
@@ -78,6 +78,7 @@ const Capabilities = {
78
78
  VOIP_STT_MESSAGE_HANDLING: 'VOIP_STT_MESSAGE_HANDLING',
79
79
  VOIP_STT_MESSAGE_HANDLING_TIMEOUT: 'VOIP_STT_MESSAGE_HANDLING_TIMEOUT',
80
80
  VOIP_STT_MESSAGE_HANDLING_TIMEOUT_SUBSEQUENT: 'VOIP_STT_MESSAGE_HANDLING_TIMEOUT_SUBSEQUENT',
81
+ VOIP_STT_MESSAGE_HANDLING_LATENCY_GRACE_MS: 'VOIP_STT_MESSAGE_HANDLING_LATENCY_GRACE_MS',
81
82
  VOIP_JOIN_SILENCE_DURATION_BY_SUBSTRING: 'VOIP_JOIN_SILENCE_DURATION_BY_SUBSTRING',
82
83
  VOIP_STT_DICTIONARY_REPLACEMENTS: 'VOIP_STT_DICTIONARY_REPLACEMENTS',
83
84
  VOIP_STT_MESSAGE_HANDLING_DELIMITER: 'VOIP_STT_MESSAGE_HANDLING_DELIMITER',
@@ -137,6 +138,10 @@ const Defaults = {
137
138
  VOIP_TTS_PREFETCH_ENABLE: true,
138
139
  VOIP_STT_MESSAGE_HANDLING: 'ORIGINAL',
139
140
  VOIP_STT_MESSAGE_HANDLING_TIMEOUT: 2500,
141
+ // Extra wall-clock grace added to the JOIN/PSST flush window when the last buffered final was
142
+ // cut mid-utterance (no Azure-detected end-of-speech silence). Absorbs STT delivery latency so a
143
+ // paused-then-resumed prompt is joined instead of split. Set to 0 to disable. See _getPsstLatencyGraceMs.
144
+ VOIP_STT_MESSAGE_HANDLING_LATENCY_GRACE_MS: 1500,
140
145
  VOIP_STT_MESSAGE_HANDLING_DELIMITER: '. ',
141
146
  VOIP_STT_MESSAGE_HANDLING_PUNCTUATION: '.!?',
142
147
  VOIP_WEBSOCKET_CONNECT_TIMEOUT: 4000,
@@ -383,6 +388,15 @@ class BotiumConnectorVoip {
383
388
  const isJoinMethod = sttHandling === 'JOIN' || sttHandling === 'PSST' || sttHandling === 'CONCAT' || this._hasJoinLogicHookOrRule(this.convoStep);
384
389
  if (!isJoinMethod) return;
385
390
  const joinTimeoutMs = this._getEffectiveJoinTimeoutMs(this.convoStep, this.botMsgs);
391
+ // Variant-3 latency grace: when the last buffered final was cut mid-utterance (no
392
+ // Azure-detected end-of-speech silence) a continuation is plausible but its STT delivery
393
+ // lags behind the audio by ~1s. Extend the wall-clock flush window by the grace so the
394
+ // resumed segment's partial/final (or a worker speechResumed event) can re-arm this timer
395
+ // and be joined, instead of flushing the first fragment on its own. A natural-ended final
396
+ // keeps the base window and flushes fast.
397
+ const graceMs = this._getPsstLatencyGraceMs();
398
+ const graceApplied = graceMs > 0 && lodash__default["default"].isFinite(joinTimeoutMs) && joinTimeoutMs > 0 && !this._isLastFinalNaturalEnd();
399
+ const effectiveWindowMs = graceApplied ? joinTimeoutMs + graceMs : joinTimeoutMs || 0;
386
400
  if (this.silenceTimeout) {
387
401
  clearTimeout(this.silenceTimeout);
388
402
  this.silenceTimeout = null;
@@ -391,21 +405,24 @@ class BotiumConnectorVoip {
391
405
  const armedAt = Date.now();
392
406
  this._markReplyTrace({
393
407
  psstTimerArmedAtMs: armedAt,
394
- psstScheduledMs: joinTimeoutMs || 0
408
+ psstScheduledMs: effectiveWindowMs || 0
395
409
  });
396
410
  _info('psst_timer_armed', {
397
411
  sessionId: this.sessionId,
398
- joinTimeoutMs: joinTimeoutMs || 0,
412
+ joinTimeoutMs: effectiveWindowMs || 0,
413
+ baseTimeoutMs: joinTimeoutMs || 0,
414
+ graceMs: graceApplied ? graceMs : 0,
415
+ graceApplied,
399
416
  bufferedChunks: bufferedAtArm,
400
417
  stopCalled: !!this.stopCalled
401
418
  });
402
- // Emit the authoritative join timeout so downstream consumers (e.g.
419
+ // Emit the authoritative flush window so downstream consumers (e.g.
403
420
  // SpeculationBuffer) can size their quiet threshold relative to it.
404
- if (this.eventEmitter && lodash__default["default"].isFinite(joinTimeoutMs) && joinTimeoutMs > 0) {
421
+ if (this.eventEmitter && lodash__default["default"].isFinite(effectiveWindowMs) && effectiveWindowMs > 0) {
405
422
  try {
406
423
  this.eventEmitter.emit('voip.psstTimerArmed', {
407
424
  sessionId: this.sessionId,
408
- joinTimeoutMs,
425
+ joinTimeoutMs: effectiveWindowMs,
409
426
  bufferedChunks: bufferedAtArm,
410
427
  armedAt
411
428
  });
@@ -425,10 +442,11 @@ class BotiumConnectorVoip {
425
442
  sessionId: this.sessionId,
426
443
  bufferedChunks: this.botMsgs.length,
427
444
  actualDelayMs: fireDelay,
428
- scheduledDelayMs: joinTimeoutMs || 0,
445
+ scheduledDelayMs: effectiveWindowMs || 0,
446
+ graceApplied,
429
447
  outcome: 'emit'
430
448
  });
431
- debug$3('Silence Duration Timeout (JOIN/PSST):', joinTimeoutMs, 'ms');
449
+ debug$3('Silence Duration Timeout (JOIN/PSST):', effectiveWindowMs, 'ms');
432
450
  sendBotMsg(joinBotMsg(this.botMsgs, this.joinLastPrevMsg));
433
451
  this.firstMsg = false;
434
452
  this.joinLastPrevMsg = this.botMsgs[this.botMsgs.length - 1];
@@ -441,11 +459,11 @@ class BotiumConnectorVoip {
441
459
  sessionId: this.sessionId,
442
460
  bufferedChunks: 0,
443
461
  actualDelayMs: fireDelay,
444
- scheduledDelayMs: joinTimeoutMs || 0,
462
+ scheduledDelayMs: effectiveWindowMs || 0,
445
463
  outcome: 'noop_empty_buffer'
446
464
  });
447
465
  }
448
- }, joinTimeoutMs || 0);
466
+ }, effectiveWindowMs || 0);
449
467
  };
450
468
 
451
469
  // Flush buffered STT chunks on teardown so a late final is not lost when
@@ -1076,6 +1094,36 @@ class BotiumConnectorVoip {
1076
1094
  }
1077
1095
  }
1078
1096
  }
1097
+ if (parsedData && parsedData.type === 'speech' && parsedData.event === 'onSpeechResumed') {
1098
+ // Positive VAD signal from the worker: the bot started speaking again after a pause.
1099
+ // It arrives ~1s before the resumed segment's first STT partial, so when we are
1100
+ // buffering finals (PSST/JOIN) we re-arm the flush timer now — keeping the buffered
1101
+ // fragment open to be joined instead of flushed on its own. Bounded by the same
1102
+ // extension budget as partial-driven re-arms to prevent infinite stranding.
1103
+ _info('speech_resumed', {
1104
+ sessionId: this.sessionId
1105
+ });
1106
+ const sttHandling = this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING];
1107
+ const isJoinMethod = sttHandling === 'JOIN' || sttHandling === 'PSST' || sttHandling === 'CONCAT' || this._hasJoinLogicHookOrRule(this.convoStep);
1108
+ if (isJoinMethod && this.botMsgs && this.botMsgs.length > 0) {
1109
+ const MAX_EXTENSION_MS = 60000;
1110
+ const now = Date.now();
1111
+ const withinCap = this.psstFirstRearmAt == null || now - this.psstFirstRearmAt < MAX_EXTENSION_MS;
1112
+ if (withinCap) {
1113
+ if (this.psstFirstRearmAt == null) this.psstFirstRearmAt = now;
1114
+ this.psstRearmCount++;
1115
+ _info('psst_timer_extended', {
1116
+ sessionId: this.sessionId,
1117
+ rearmCount: this.psstRearmCount,
1118
+ msSinceFirstRearm: now - this.psstFirstRearmAt,
1119
+ bufferedChunks: this.botMsgs.length,
1120
+ reason: 'speech_resumed',
1121
+ silenceDurationMs: lodash__default["default"].get(parsedData, 'data.silenceDurationMs', null)
1122
+ });
1123
+ armJoinSilenceTimer();
1124
+ }
1125
+ }
1126
+ }
1079
1127
  if (parsedData && parsedData.type === 'fullRecord') {
1080
1128
  // Non-chunked full record
1081
1129
  const base64 = _extractFullRecordBase64(parsedData);
@@ -2458,6 +2506,21 @@ class BotiumConnectorVoip {
2458
2506
  if (!lodash__default["default"].isFinite(parsed) || parsed <= 0) return null;
2459
2507
  return isPsst ? Math.max(0, parsed - 500) : parsed;
2460
2508
  }
2509
+
2510
+ // Extra grace (ms) added to the JOIN/PSST flush window when the last buffered final was cut
2511
+ // mid-utterance. Returns 0 when unset/invalid (feature off). See _isLastFinalNaturalEnd.
2512
+ _getPsstLatencyGraceMs() {
2513
+ const parsed = parseInt(this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_LATENCY_GRACE_MS], 10);
2514
+ if (!lodash__default["default"].isFinite(parsed) || parsed <= 0) return 0;
2515
+ return parsed;
2516
+ }
2517
+
2518
+ // A final that carries a finite silenceStartedSec ended on Azure-detected end-of-speech silence
2519
+ // (bot genuinely stopped) => flush at the base window, no grace. A final without it was cut
2520
+ // mid-utterance (forced/segmented) => a continuation is plausible => apply the latency grace.
2521
+ _isLastFinalNaturalEnd() {
2522
+ return lodash__default["default"].isFinite(lodash__default["default"].get(this.prevData, 'data.silenceStartedSec'));
2523
+ }
2461
2524
  _getEffectiveJoinTimeoutMs(convoStep, botMsgs) {
2462
2525
  const sttHandling = this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING];
2463
2526
  const isPsst = sttHandling === 'PSST';