@hivegpt/hiveai-angular 0.0.458 → 0.0.460

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.
@@ -1264,13 +1264,10 @@
1264
1264
  * - Create and manage Daily CallObject
1265
1265
  * - Join Daily room using room_url
1266
1266
  * - Handle mic capture + speaker playback
1267
- * - Provide real-time speaking detection via active-speaker-change (primary)
1268
- * and track-started/track-stopped (fallback for immediate feedback)
1267
+ * - Bot speaking detection via AnalyserNode on remote track (instant)
1268
+ * - User speaking detection via active-speaker-change
1269
1269
  * - Expose speaking$ (bot speaking), userSpeaking$ (user speaking), micMuted$
1270
1270
  * - Expose localStream$ for waveform visualization (AudioAnalyzerService)
1271
- *
1272
- * Speaking state flips immediately when agent audio starts playing.
1273
- * If user speaks while bot is talking, state switches to listening.
1274
1271
  */
1275
1272
  var DailyVoiceClientService = /** @class */ (function () {
1276
1273
  function DailyVoiceClientService(ngZone) {
@@ -1280,6 +1277,9 @@
1280
1277
  this.localSessionId = null;
1281
1278
  /** Explicit playback of remote (bot) audio; required in some browsers. */
1282
1279
  this.remoteAudioElement = null;
1280
+ /** AnalyserNode-based remote audio monitor for instant bot speaking detection. */
1281
+ this.remoteAudioContext = null;
1282
+ this.remoteSpeakingRAF = null;
1283
1283
  this.speakingSubject = new rxjs.BehaviorSubject(false);
1284
1284
  this.userSpeakingSubject = new rxjs.BehaviorSubject(false);
1285
1285
  this.micMutedSubject = new rxjs.BehaviorSubject(false);
@@ -1353,24 +1353,21 @@
1353
1353
  };
1354
1354
  DailyVoiceClientService.prototype.setupEventHandlers = function (call) {
1355
1355
  var _this = this;
1356
- // active-speaker-change: primary source for real-time speaking detection.
1357
- // Emits when the loudest participant changes; bot speaking = remote is active.
1356
+ // active-speaker-change: used ONLY for user speaking detection.
1357
+ // Bot speaking is detected by our own AnalyserNode (instant, no debounce).
1358
1358
  call.on('active-speaker-change', function (event) {
1359
1359
  _this.ngZone.run(function () {
1360
1360
  var _a;
1361
1361
  var peerId = (_a = event === null || event === void 0 ? void 0 : event.activeSpeaker) === null || _a === void 0 ? void 0 : _a.peerId;
1362
1362
  if (!peerId || !_this.localSessionId) {
1363
- _this.speakingSubject.next(false);
1364
1363
  _this.userSpeakingSubject.next(false);
1365
1364
  return;
1366
1365
  }
1367
1366
  var isLocal = peerId === _this.localSessionId;
1368
1367
  _this.userSpeakingSubject.next(isLocal);
1369
- _this.speakingSubject.next(!isLocal);
1370
1368
  });
1371
1369
  });
1372
- // track-started / track-stopped: set up remote audio playback only.
1373
- // Speaking state is driven by active-speaker-change (actual audio energy), NOT track lifecycle.
1370
+ // track-started / track-stopped: set up remote audio playback + AnalyserNode monitor.
1374
1371
  call.on('track-started', function (event) {
1375
1372
  _this.ngZone.run(function () {
1376
1373
  var _a, _b, _c, _d;
@@ -1382,6 +1379,7 @@
1382
1379
  var audioTrack = track !== null && track !== void 0 ? track : (_d = (_c = p.tracks) === null || _c === void 0 ? void 0 : _c.audio) === null || _d === void 0 ? void 0 : _d.track;
1383
1380
  if (audioTrack && typeof audioTrack === 'object') {
1384
1381
  _this.playRemoteTrack(audioTrack);
1382
+ _this.monitorRemoteAudio(audioTrack);
1385
1383
  }
1386
1384
  }
1387
1385
  });
@@ -1392,6 +1390,7 @@
1392
1390
  var p = event === null || event === void 0 ? void 0 : event.participant;
1393
1391
  var type = (_a = event === null || event === void 0 ? void 0 : event.type) !== null && _a !== void 0 ? _a : (_b = event === null || event === void 0 ? void 0 : event.track) === null || _b === void 0 ? void 0 : _b.kind;
1394
1392
  if (p && !p.local && type === 'audio') {
1393
+ _this.stopRemoteAudioMonitor();
1395
1394
  _this.stopRemoteAudio();
1396
1395
  }
1397
1396
  });
@@ -1446,6 +1445,69 @@
1446
1445
  console.warn('DailyVoiceClient: failed to create remote audio element', err);
1447
1446
  }
1448
1447
  };
1448
+ /**
1449
+ * Monitor remote audio track energy via AnalyserNode.
1450
+ * Polls at ~60fps and flips speakingSubject based on actual audio energy.
1451
+ */
1452
+ DailyVoiceClientService.prototype.monitorRemoteAudio = function (track) {
1453
+ var _this = this;
1454
+ this.stopRemoteAudioMonitor();
1455
+ try {
1456
+ var ctx = new AudioContext();
1457
+ var source = ctx.createMediaStreamSource(new MediaStream([track]));
1458
+ var analyser_1 = ctx.createAnalyser();
1459
+ analyser_1.fftSize = 256;
1460
+ source.connect(analyser_1);
1461
+ this.remoteAudioContext = ctx;
1462
+ var dataArray_1 = new Uint8Array(analyser_1.frequencyBinCount);
1463
+ var THRESHOLD_1 = 5;
1464
+ var SILENCE_MS_1 = 1500;
1465
+ var lastSoundTime_1 = 0;
1466
+ var isSpeaking_1 = false;
1467
+ var poll_1 = function () {
1468
+ if (!_this.remoteAudioContext)
1469
+ return;
1470
+ analyser_1.getByteFrequencyData(dataArray_1);
1471
+ var sum = 0;
1472
+ for (var i = 0; i < dataArray_1.length; i++) {
1473
+ sum += dataArray_1[i];
1474
+ }
1475
+ var avg = sum / dataArray_1.length;
1476
+ var now = Date.now();
1477
+ if (avg > THRESHOLD_1) {
1478
+ lastSoundTime_1 = now;
1479
+ if (!isSpeaking_1) {
1480
+ isSpeaking_1 = true;
1481
+ console.log("[VoiceDebug] Bot audio energy detected (speaking=true) \u2014 avg=" + avg.toFixed(1) + " \u2014 " + new Date().toISOString());
1482
+ _this.ngZone.run(function () {
1483
+ _this.userSpeakingSubject.next(false);
1484
+ _this.speakingSubject.next(true);
1485
+ });
1486
+ }
1487
+ }
1488
+ else if (isSpeaking_1 && now - lastSoundTime_1 > SILENCE_MS_1) {
1489
+ isSpeaking_1 = false;
1490
+ console.log("[VoiceDebug] Bot audio silence detected (speaking=false) \u2014 " + new Date().toISOString());
1491
+ _this.ngZone.run(function () { return _this.speakingSubject.next(false); });
1492
+ }
1493
+ _this.remoteSpeakingRAF = requestAnimationFrame(poll_1);
1494
+ };
1495
+ this.remoteSpeakingRAF = requestAnimationFrame(poll_1);
1496
+ }
1497
+ catch (err) {
1498
+ console.warn('DailyVoiceClient: failed to create remote audio monitor', err);
1499
+ }
1500
+ };
1501
+ DailyVoiceClientService.prototype.stopRemoteAudioMonitor = function () {
1502
+ if (this.remoteSpeakingRAF) {
1503
+ cancelAnimationFrame(this.remoteSpeakingRAF);
1504
+ this.remoteSpeakingRAF = null;
1505
+ }
1506
+ if (this.remoteAudioContext) {
1507
+ this.remoteAudioContext.close().catch(function () { });
1508
+ this.remoteAudioContext = null;
1509
+ }
1510
+ };
1449
1511
  DailyVoiceClientService.prototype.stopRemoteAudio = function () {
1450
1512
  if (this.remoteAudioElement) {
1451
1513
  try {
@@ -1492,6 +1554,7 @@
1492
1554
  });
1493
1555
  };
1494
1556
  DailyVoiceClientService.prototype.cleanup = function () {
1557
+ this.stopRemoteAudioMonitor();
1495
1558
  this.stopRemoteAudio();
1496
1559
  if (this.callObject) {
1497
1560
  this.callObject.destroy().catch(function () { });