@hivegpt/hiveai-angular 0.0.424 → 0.0.427
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.
- package/bundles/hivegpt-hiveai-angular.umd.js +474 -158
- package/bundles/hivegpt-hiveai-angular.umd.js.map +1 -1
- package/bundles/hivegpt-hiveai-angular.umd.min.js +1 -1
- package/bundles/hivegpt-hiveai-angular.umd.min.js.map +1 -1
- package/esm2015/hivegpt-hiveai-angular.js +6 -4
- package/esm2015/lib/components/voice-agent/services/audio-analyzer.service.js +5 -1
- package/esm2015/lib/components/voice-agent/services/daily-voice-client.service.js +181 -0
- package/esm2015/lib/components/voice-agent/services/voice-agent.service.js +128 -140
- package/esm2015/lib/components/voice-agent/services/websocket-voice-client.service.js +93 -0
- package/esm2015/lib/components/voice-agent/voice-agent.module.js +6 -2
- package/fesm2015/hivegpt-hiveai-angular.js +399 -143
- package/fesm2015/hivegpt-hiveai-angular.js.map +1 -1
- package/hivegpt-hiveai-angular.d.ts +5 -3
- package/hivegpt-hiveai-angular.d.ts.map +1 -1
- package/hivegpt-hiveai-angular.metadata.json +1 -1
- package/lib/components/voice-agent/services/audio-analyzer.service.d.ts +4 -0
- package/lib/components/voice-agent/services/audio-analyzer.service.d.ts.map +1 -1
- package/lib/components/voice-agent/services/daily-voice-client.service.d.ts +48 -0
- package/lib/components/voice-agent/services/daily-voice-client.service.d.ts.map +1 -0
- package/lib/components/voice-agent/services/voice-agent.service.d.ts +19 -8
- package/lib/components/voice-agent/services/voice-agent.service.d.ts.map +1 -1
- package/lib/components/voice-agent/services/websocket-voice-client.service.d.ts +47 -0
- package/lib/components/voice-agent/services/websocket-voice-client.service.d.ts.map +1 -0
- package/lib/components/voice-agent/voice-agent.module.d.ts.map +1 -1
- package/package.json +2 -1
|
@@ -3,17 +3,16 @@ import { ComponentPortal } from '@angular/cdk/portal';
|
|
|
3
3
|
import * as i1 from '@angular/common/http';
|
|
4
4
|
import { HttpHeaders, HttpClient } from '@angular/common/http';
|
|
5
5
|
import * as i0 from '@angular/core';
|
|
6
|
-
import { Injectable, InjectionToken, EventEmitter, Component, Injector, Output, Input, ElementRef, ChangeDetectionStrategy, ChangeDetectorRef, Renderer2, ViewContainerRef, ViewChild, ViewChildren, NgModule, Pipe, Inject } from '@angular/core';
|
|
6
|
+
import { Injectable, NgZone, InjectionToken, EventEmitter, Component, Injector, Output, Input, ElementRef, ChangeDetectionStrategy, ChangeDetectorRef, Renderer2, ViewContainerRef, ViewChild, ViewChildren, NgModule, Pipe, Inject } from '@angular/core';
|
|
7
7
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
8
|
-
import { Subject, BehaviorSubject, of } from 'rxjs';
|
|
9
|
-
import { map, switchMap, catchError } from 'rxjs/operators';
|
|
8
|
+
import { Subject, BehaviorSubject, Subscription, combineLatest, of } from 'rxjs';
|
|
9
|
+
import { map, take, takeUntil, filter, switchMap, catchError } from 'rxjs/operators';
|
|
10
10
|
import { Socket } from 'ngx-socket-io';
|
|
11
11
|
import { Validators, FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
12
12
|
import * as SpeechSDK from 'microsoft-cognitiveservices-speech-sdk';
|
|
13
13
|
import * as marked from 'marked';
|
|
14
14
|
import { __awaiter } from 'tslib';
|
|
15
|
-
import
|
|
16
|
-
import { WebSocketTransport } from '@pipecat-ai/websocket-transport';
|
|
15
|
+
import Daily from '@daily-co/daily-js';
|
|
17
16
|
import { CommonModule, DOCUMENT } from '@angular/common';
|
|
18
17
|
import { MatIconModule } from '@angular/material/icon';
|
|
19
18
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
|
@@ -501,6 +500,10 @@ BotsService.ctorParameters = () => [
|
|
|
501
500
|
{ type: HttpClient }
|
|
502
501
|
];
|
|
503
502
|
|
|
503
|
+
/**
|
|
504
|
+
* Audio analyzer for waveform visualization only.
|
|
505
|
+
* Do NOT use isUserSpeaking$ for call state; speaking state must come from Daily.js.
|
|
506
|
+
*/
|
|
504
507
|
class AudioAnalyzerService {
|
|
505
508
|
constructor() {
|
|
506
509
|
this.audioContext = null;
|
|
@@ -617,13 +620,283 @@ AudioAnalyzerService.decorators = [
|
|
|
617
620
|
},] }
|
|
618
621
|
];
|
|
619
622
|
|
|
623
|
+
/**
|
|
624
|
+
* WebSocket-only client for voice agent signaling.
|
|
625
|
+
* Responsibilities:
|
|
626
|
+
* - Connect to ws_url
|
|
627
|
+
* - Parse JSON messages
|
|
628
|
+
* - Emit roomCreated$, userTranscript$, botTranscript$
|
|
629
|
+
* - NO audio logic, NO mic logic.
|
|
630
|
+
*/
|
|
631
|
+
class WebSocketVoiceClientService {
|
|
632
|
+
constructor() {
|
|
633
|
+
this.ws = null;
|
|
634
|
+
this.roomCreatedSubject = new Subject();
|
|
635
|
+
this.userTranscriptSubject = new Subject();
|
|
636
|
+
this.botTranscriptSubject = new Subject();
|
|
637
|
+
/** Emits room_url when backend sends room_created. */
|
|
638
|
+
this.roomCreated$ = this.roomCreatedSubject.asObservable();
|
|
639
|
+
/** Emits user transcript updates. */
|
|
640
|
+
this.userTranscript$ = this.userTranscriptSubject.asObservable();
|
|
641
|
+
/** Emits bot transcript updates. */
|
|
642
|
+
this.botTranscript$ = this.botTranscriptSubject.asObservable();
|
|
643
|
+
}
|
|
644
|
+
/** Connect to signaling WebSocket. No audio over this connection. */
|
|
645
|
+
connect(wsUrl) {
|
|
646
|
+
var _a;
|
|
647
|
+
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (this.ws) {
|
|
651
|
+
this.ws.close();
|
|
652
|
+
this.ws = null;
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
this.ws = new WebSocket(wsUrl);
|
|
656
|
+
this.ws.onmessage = (event) => {
|
|
657
|
+
var _a;
|
|
658
|
+
try {
|
|
659
|
+
const msg = JSON.parse(event.data);
|
|
660
|
+
if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'room_created') {
|
|
661
|
+
const roomUrl = ((_a = msg.room_url) !== null && _a !== void 0 ? _a : msg.roomUrl);
|
|
662
|
+
if (typeof roomUrl === 'string') {
|
|
663
|
+
this.roomCreatedSubject.next(roomUrl);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
else if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'user_transcript' && typeof msg.text === 'string') {
|
|
667
|
+
this.userTranscriptSubject.next({
|
|
668
|
+
text: msg.text,
|
|
669
|
+
final: msg.final === true,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
else if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'bot_transcript' && typeof msg.text === 'string') {
|
|
673
|
+
this.botTranscriptSubject.next(msg.text);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
catch (_b) {
|
|
677
|
+
// Ignore non-JSON or unknown messages
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
this.ws.onerror = () => {
|
|
681
|
+
this.disconnect();
|
|
682
|
+
};
|
|
683
|
+
this.ws.onclose = () => {
|
|
684
|
+
this.ws = null;
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
catch (err) {
|
|
688
|
+
console.error('WebSocketVoiceClient: connect failed', err);
|
|
689
|
+
this.ws = null;
|
|
690
|
+
throw err;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/** Disconnect and cleanup. */
|
|
694
|
+
disconnect() {
|
|
695
|
+
if (this.ws) {
|
|
696
|
+
this.ws.close();
|
|
697
|
+
this.ws = null;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
/** Whether the WebSocket is open. */
|
|
701
|
+
get isConnected() {
|
|
702
|
+
var _a;
|
|
703
|
+
return ((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
WebSocketVoiceClientService.ɵprov = i0.ɵɵdefineInjectable({ factory: function WebSocketVoiceClientService_Factory() { return new WebSocketVoiceClientService(); }, token: WebSocketVoiceClientService, providedIn: "root" });
|
|
707
|
+
WebSocketVoiceClientService.decorators = [
|
|
708
|
+
{ type: Injectable, args: [{
|
|
709
|
+
providedIn: 'root',
|
|
710
|
+
},] }
|
|
711
|
+
];
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Daily.js WebRTC client for voice agent audio.
|
|
715
|
+
* Responsibilities:
|
|
716
|
+
* - Create and manage Daily CallObject
|
|
717
|
+
* - Join Daily room using room_url
|
|
718
|
+
* - Handle mic capture + speaker playback
|
|
719
|
+
* - Provide real-time speaking detection via active-speaker-change (primary)
|
|
720
|
+
* and track-started/track-stopped (fallback for immediate feedback)
|
|
721
|
+
* - Expose speaking$ (bot speaking), userSpeaking$ (user speaking), micMuted$
|
|
722
|
+
* - Expose localStream$ for waveform visualization (AudioAnalyzerService)
|
|
723
|
+
*
|
|
724
|
+
* Speaking state flips immediately when agent audio starts playing.
|
|
725
|
+
* If user speaks while bot is talking, state switches to listening.
|
|
726
|
+
*/
|
|
727
|
+
class DailyVoiceClientService {
|
|
728
|
+
constructor(ngZone) {
|
|
729
|
+
this.ngZone = ngZone;
|
|
730
|
+
this.callObject = null;
|
|
731
|
+
this.localStream = null;
|
|
732
|
+
this.localSessionId = null;
|
|
733
|
+
this.speakingSubject = new BehaviorSubject(false);
|
|
734
|
+
this.userSpeakingSubject = new BehaviorSubject(false);
|
|
735
|
+
this.micMutedSubject = new BehaviorSubject(false);
|
|
736
|
+
this.localStreamSubject = new BehaviorSubject(null);
|
|
737
|
+
/** True when bot (remote participant) is the active speaker. */
|
|
738
|
+
this.speaking$ = this.speakingSubject.asObservable();
|
|
739
|
+
/** True when user (local participant) is the active speaker. */
|
|
740
|
+
this.userSpeaking$ = this.userSpeakingSubject.asObservable();
|
|
741
|
+
/** True when mic is muted. */
|
|
742
|
+
this.micMuted$ = this.micMutedSubject.asObservable();
|
|
743
|
+
/** Emits local mic stream for waveform visualization. */
|
|
744
|
+
this.localStream$ = this.localStreamSubject.asObservable();
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Connect to Daily room. Acquires mic first for waveform, then joins with audio.
|
|
748
|
+
* @param roomUrl Daily room URL (from room_created)
|
|
749
|
+
* @param token Optional meeting token
|
|
750
|
+
*/
|
|
751
|
+
connect(roomUrl, token) {
|
|
752
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
753
|
+
if (this.callObject) {
|
|
754
|
+
yield this.disconnect();
|
|
755
|
+
}
|
|
756
|
+
try {
|
|
757
|
+
// Get mic stream for both Daily and waveform (single capture)
|
|
758
|
+
const stream = yield navigator.mediaDevices.getUserMedia({ audio: true });
|
|
759
|
+
const audioTrack = stream.getAudioTracks()[0];
|
|
760
|
+
if (!audioTrack) {
|
|
761
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
762
|
+
throw new Error('No audio track');
|
|
763
|
+
}
|
|
764
|
+
this.localStream = stream;
|
|
765
|
+
this.localStreamSubject.next(stream);
|
|
766
|
+
// Create audio-only call object
|
|
767
|
+
// videoSource: false = no camera, audioSource = our mic track
|
|
768
|
+
const callObject = Daily.createCallObject({
|
|
769
|
+
videoSource: false,
|
|
770
|
+
audioSource: audioTrack,
|
|
771
|
+
});
|
|
772
|
+
this.callObject = callObject;
|
|
773
|
+
this.setupEventHandlers(callObject);
|
|
774
|
+
// Join room; Daily handles playback of remote (bot) audio automatically
|
|
775
|
+
yield callObject.join({ url: roomUrl, token });
|
|
776
|
+
const participants = callObject.participants();
|
|
777
|
+
if (participants === null || participants === void 0 ? void 0 : participants.local) {
|
|
778
|
+
this.localSessionId = participants.local.session_id;
|
|
779
|
+
}
|
|
780
|
+
// Initial mute state: Daily starts with audio on
|
|
781
|
+
this.micMutedSubject.next(!callObject.localAudio());
|
|
782
|
+
}
|
|
783
|
+
catch (err) {
|
|
784
|
+
this.cleanup();
|
|
785
|
+
throw err;
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
setupEventHandlers(call) {
|
|
790
|
+
// active-speaker-change: primary source for real-time speaking detection.
|
|
791
|
+
// Emits when the loudest participant changes; bot speaking = remote is active.
|
|
792
|
+
call.on('active-speaker-change', (event) => {
|
|
793
|
+
this.ngZone.run(() => {
|
|
794
|
+
var _a;
|
|
795
|
+
const peerId = (_a = event === null || event === void 0 ? void 0 : event.activeSpeaker) === null || _a === void 0 ? void 0 : _a.peerId;
|
|
796
|
+
if (!peerId || !this.localSessionId) {
|
|
797
|
+
this.speakingSubject.next(false);
|
|
798
|
+
this.userSpeakingSubject.next(false);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const isLocal = peerId === this.localSessionId;
|
|
802
|
+
this.userSpeakingSubject.next(isLocal);
|
|
803
|
+
this.speakingSubject.next(!isLocal);
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
// track-started / track-stopped: fallback for immediate feedback when
|
|
807
|
+
// remote (bot) audio track starts or stops. Ensures talking indicator
|
|
808
|
+
// flips as soon as agent audio begins, without waiting for active-speaker-change.
|
|
809
|
+
call.on('track-started', (event) => {
|
|
810
|
+
this.ngZone.run(() => {
|
|
811
|
+
var _a, _b;
|
|
812
|
+
const p = event === null || event === void 0 ? void 0 : event.participant;
|
|
813
|
+
const 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;
|
|
814
|
+
if (p && !p.local && type === 'audio') {
|
|
815
|
+
this.speakingSubject.next(true);
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
call.on('track-stopped', (event) => {
|
|
820
|
+
this.ngZone.run(() => {
|
|
821
|
+
var _a, _b;
|
|
822
|
+
const p = event === null || event === void 0 ? void 0 : event.participant;
|
|
823
|
+
const 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;
|
|
824
|
+
if (p && !p.local && type === 'audio') {
|
|
825
|
+
this.speakingSubject.next(false);
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
call.on('left-meeting', () => {
|
|
830
|
+
this.ngZone.run(() => this.cleanup());
|
|
831
|
+
});
|
|
832
|
+
call.on('error', (event) => {
|
|
833
|
+
this.ngZone.run(() => {
|
|
834
|
+
var _a;
|
|
835
|
+
console.error('DailyVoiceClient: Daily error', (_a = event === null || event === void 0 ? void 0 : event.errorMsg) !== null && _a !== void 0 ? _a : event);
|
|
836
|
+
this.cleanup();
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
/** Set mic muted state. */
|
|
841
|
+
setMuted(muted) {
|
|
842
|
+
if (!this.callObject)
|
|
843
|
+
return;
|
|
844
|
+
this.callObject.setLocalAudio(!muted);
|
|
845
|
+
this.micMutedSubject.next(muted);
|
|
846
|
+
}
|
|
847
|
+
/** Disconnect and cleanup. */
|
|
848
|
+
disconnect() {
|
|
849
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
850
|
+
if (!this.callObject) {
|
|
851
|
+
this.cleanup();
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
try {
|
|
855
|
+
yield this.callObject.leave();
|
|
856
|
+
}
|
|
857
|
+
catch (e) {
|
|
858
|
+
// ignore
|
|
859
|
+
}
|
|
860
|
+
this.cleanup();
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
cleanup() {
|
|
864
|
+
if (this.callObject) {
|
|
865
|
+
this.callObject.destroy().catch(() => { });
|
|
866
|
+
this.callObject = null;
|
|
867
|
+
}
|
|
868
|
+
if (this.localStream) {
|
|
869
|
+
this.localStream.getTracks().forEach((t) => t.stop());
|
|
870
|
+
this.localStream = null;
|
|
871
|
+
}
|
|
872
|
+
this.localSessionId = null;
|
|
873
|
+
this.speakingSubject.next(false);
|
|
874
|
+
this.userSpeakingSubject.next(false);
|
|
875
|
+
this.localStreamSubject.next(null);
|
|
876
|
+
// Keep last micMuted state; will reset on next connect
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
DailyVoiceClientService.ɵprov = i0.ɵɵdefineInjectable({ factory: function DailyVoiceClientService_Factory() { return new DailyVoiceClientService(i0.ɵɵinject(i0.NgZone)); }, token: DailyVoiceClientService, providedIn: "root" });
|
|
880
|
+
DailyVoiceClientService.decorators = [
|
|
881
|
+
{ type: Injectable, args: [{
|
|
882
|
+
providedIn: 'root',
|
|
883
|
+
},] }
|
|
884
|
+
];
|
|
885
|
+
DailyVoiceClientService.ctorParameters = () => [
|
|
886
|
+
{ type: NgZone }
|
|
887
|
+
];
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Voice agent orchestrator. Coordinates WebSocket (signaling) and Daily.js (WebRTC audio).
|
|
891
|
+
* - Maintains callState, statusText, duration, isMicMuted, isUserSpeaking, audioLevels
|
|
892
|
+
* - Uses WebSocket for room_created and transcripts only (no audio)
|
|
893
|
+
* - Uses Daily.js for all audio, mic, and real-time speaking detection
|
|
894
|
+
*/
|
|
620
895
|
class VoiceAgentService {
|
|
621
|
-
constructor(audioAnalyzer) {
|
|
896
|
+
constructor(audioAnalyzer, wsClient, dailyClient) {
|
|
622
897
|
this.audioAnalyzer = audioAnalyzer;
|
|
623
|
-
this.
|
|
624
|
-
this.
|
|
625
|
-
this.userMediaStream = null;
|
|
626
|
-
this.botAudioElement = null;
|
|
898
|
+
this.wsClient = wsClient;
|
|
899
|
+
this.dailyClient = dailyClient;
|
|
627
900
|
this.callStateSubject = new BehaviorSubject('idle');
|
|
628
901
|
this.statusTextSubject = new BehaviorSubject('');
|
|
629
902
|
this.durationSubject = new BehaviorSubject('00:00');
|
|
@@ -634,6 +907,8 @@ class VoiceAgentService {
|
|
|
634
907
|
this.botTranscriptSubject = new Subject();
|
|
635
908
|
this.callStartTime = 0;
|
|
636
909
|
this.durationInterval = null;
|
|
910
|
+
this.subscriptions = new Subscription();
|
|
911
|
+
this.destroy$ = new Subject();
|
|
637
912
|
this.callState$ = this.callStateSubject.asObservable();
|
|
638
913
|
this.statusText$ = this.statusTextSubject.asObservable();
|
|
639
914
|
this.duration$ = this.durationSubject.asObservable();
|
|
@@ -642,39 +917,23 @@ class VoiceAgentService {
|
|
|
642
917
|
this.audioLevels$ = this.audioLevelsSubject.asObservable();
|
|
643
918
|
this.userTranscript$ = this.userTranscriptSubject.asObservable();
|
|
644
919
|
this.botTranscript$ = this.botTranscriptSubject.asObservable();
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
653
|
-
else if (!isSpeaking && this.callStateSubject.value === 'listening') {
|
|
654
|
-
this.callStateSubject.next('connected');
|
|
655
|
-
}
|
|
656
|
-
});
|
|
920
|
+
// Waveform visualization only - do NOT use for speaking state
|
|
921
|
+
this.subscriptions.add(this.audioAnalyzer.audioLevels$.subscribe((levels) => this.audioLevelsSubject.next(levels)));
|
|
922
|
+
}
|
|
923
|
+
ngOnDestroy() {
|
|
924
|
+
this.destroy$.next();
|
|
925
|
+
this.subscriptions.unsubscribe();
|
|
926
|
+
this.disconnect();
|
|
657
927
|
}
|
|
658
928
|
/** Reset to idle state (e.g. when modal opens so user can click Start Call). */
|
|
659
929
|
resetToIdle() {
|
|
660
930
|
if (this.callStateSubject.value === 'idle')
|
|
661
931
|
return;
|
|
662
|
-
|
|
663
|
-
clearInterval(this.durationInterval);
|
|
664
|
-
this.durationInterval = null;
|
|
665
|
-
}
|
|
932
|
+
this.stopDurationTimer();
|
|
666
933
|
this.audioAnalyzer.stop();
|
|
667
|
-
this.
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
this.userMediaStream.getTracks().forEach(track => track.stop());
|
|
671
|
-
this.userMediaStream = null;
|
|
672
|
-
}
|
|
673
|
-
if (this.botAudioElement) {
|
|
674
|
-
this.botAudioElement.pause();
|
|
675
|
-
this.botAudioElement.srcObject = null;
|
|
676
|
-
this.botAudioElement = null;
|
|
677
|
-
}
|
|
934
|
+
this.wsClient.disconnect();
|
|
935
|
+
// Fire-and-forget: Daily disconnect is async; connect() will await if needed
|
|
936
|
+
void this.dailyClient.disconnect();
|
|
678
937
|
this.callStateSubject.next('idle');
|
|
679
938
|
this.statusTextSubject.next('');
|
|
680
939
|
this.durationSubject.next('0:00');
|
|
@@ -690,44 +949,58 @@ class VoiceAgentService {
|
|
|
690
949
|
this.callStateSubject.next('connecting');
|
|
691
950
|
this.statusTextSubject.next('Connecting...');
|
|
692
951
|
const baseUrl = apiUrl.replace(/\/$/, '');
|
|
693
|
-
const
|
|
694
|
-
const headers =
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
952
|
+
const postUrl = `${baseUrl}/ai/ask-voice`;
|
|
953
|
+
const headers = {
|
|
954
|
+
'Content-Type': 'application/json',
|
|
955
|
+
Authorization: `Bearer ${token}`,
|
|
956
|
+
'domain-authority': domainAuthority,
|
|
957
|
+
'eventtoken': eventToken,
|
|
958
|
+
'eventurl': eventUrl,
|
|
959
|
+
'hive-bot-id': botId,
|
|
960
|
+
'x-api-key': apiKey,
|
|
961
|
+
};
|
|
962
|
+
// POST to get ws_url for signaling
|
|
963
|
+
const res = yield fetch(postUrl, {
|
|
964
|
+
method: 'POST',
|
|
704
965
|
headers,
|
|
705
|
-
|
|
966
|
+
body: JSON.stringify({
|
|
706
967
|
bot_id: botId,
|
|
707
968
|
conversation_id: conversationId,
|
|
708
969
|
voice: 'alloy',
|
|
709
|
-
},
|
|
710
|
-
};
|
|
711
|
-
this.transport = new WebSocketTransport();
|
|
712
|
-
this.client = new PipecatClient({
|
|
713
|
-
transport: this.transport,
|
|
714
|
-
enableMic: true,
|
|
715
|
-
enableCam: false,
|
|
970
|
+
}),
|
|
716
971
|
});
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
this.botAudioElement.autoplay = true;
|
|
720
|
-
yield this.client.startBotAndConnect(startBotParams);
|
|
721
|
-
const tracks = this.client.tracks();
|
|
722
|
-
if ((_a = tracks === null || tracks === void 0 ? void 0 : tracks.local) === null || _a === void 0 ? void 0 : _a.audio) {
|
|
723
|
-
this.userMediaStream = new MediaStream([tracks.local.audio]);
|
|
724
|
-
this.audioAnalyzer.start(this.userMediaStream);
|
|
972
|
+
if (!res.ok) {
|
|
973
|
+
throw new Error(`HTTP ${res.status}`);
|
|
725
974
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
975
|
+
const json = yield res.json();
|
|
976
|
+
const wsUrl = (_a = json === null || json === void 0 ? void 0 : json.ws_url) !== null && _a !== void 0 ? _a : json === null || json === void 0 ? void 0 : json.rn_ws_url;
|
|
977
|
+
if (!wsUrl || typeof wsUrl !== 'string') {
|
|
978
|
+
throw new Error('No ws_url in response');
|
|
979
|
+
}
|
|
980
|
+
// Subscribe to room_created BEFORE connecting to avoid race
|
|
981
|
+
this.wsClient.roomCreated$
|
|
982
|
+
.pipe(take(1), takeUntil(this.destroy$))
|
|
983
|
+
.subscribe((roomUrl) => __awaiter(this, void 0, void 0, function* () {
|
|
984
|
+
try {
|
|
985
|
+
yield this.onRoomCreated(roomUrl);
|
|
986
|
+
}
|
|
987
|
+
catch (err) {
|
|
988
|
+
console.error('Daily join failed:', err);
|
|
989
|
+
this.callStateSubject.next('ended');
|
|
990
|
+
this.statusTextSubject.next('Connection failed');
|
|
991
|
+
yield this.disconnect();
|
|
992
|
+
throw err;
|
|
993
|
+
}
|
|
994
|
+
}));
|
|
995
|
+
// Forward transcripts from WebSocket
|
|
996
|
+
this.subscriptions.add(this.wsClient.userTranscript$
|
|
997
|
+
.pipe(takeUntil(this.destroy$))
|
|
998
|
+
.subscribe((t) => this.userTranscriptSubject.next(t)));
|
|
999
|
+
this.subscriptions.add(this.wsClient.botTranscript$
|
|
1000
|
+
.pipe(takeUntil(this.destroy$))
|
|
1001
|
+
.subscribe((t) => this.botTranscriptSubject.next(t)));
|
|
1002
|
+
// Connect signaling WebSocket (no audio over WS)
|
|
1003
|
+
this.wsClient.connect(wsUrl);
|
|
731
1004
|
}
|
|
732
1005
|
catch (error) {
|
|
733
1006
|
console.error('Error connecting voice agent:', error);
|
|
@@ -738,83 +1011,56 @@ class VoiceAgentService {
|
|
|
738
1011
|
}
|
|
739
1012
|
});
|
|
740
1013
|
}
|
|
741
|
-
|
|
1014
|
+
onRoomCreated(roomUrl) {
|
|
742
1015
|
return __awaiter(this, void 0, void 0, function* () {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1016
|
+
// Connect Daily.js for WebRTC audio
|
|
1017
|
+
yield this.dailyClient.connect(roomUrl);
|
|
1018
|
+
// Waveform: use local mic stream from Daily client
|
|
1019
|
+
this.dailyClient.localStream$
|
|
1020
|
+
.pipe(filter((s) => s != null), take(1))
|
|
1021
|
+
.subscribe((stream) => {
|
|
1022
|
+
this.audioAnalyzer.start(stream);
|
|
1023
|
+
});
|
|
1024
|
+
// Speaking state from Daily only (NOT from AudioAnalyzer).
|
|
1025
|
+
// User speaking overrides bot talking: if user speaks while bot talks, state = listening.
|
|
1026
|
+
this.subscriptions.add(this.dailyClient.userSpeaking$.subscribe((s) => this.isUserSpeakingSubject.next(s)));
|
|
1027
|
+
this.subscriptions.add(combineLatest([
|
|
1028
|
+
this.dailyClient.speaking$,
|
|
1029
|
+
this.dailyClient.userSpeaking$,
|
|
1030
|
+
]).subscribe(([bot, user]) => {
|
|
1031
|
+
if (user) {
|
|
1032
|
+
this.callStateSubject.next('listening');
|
|
752
1033
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
this.userMediaStream.getTracks().forEach(track => track.stop());
|
|
756
|
-
this.userMediaStream = null;
|
|
1034
|
+
else if (bot) {
|
|
1035
|
+
this.callStateSubject.next('talking');
|
|
757
1036
|
}
|
|
758
|
-
if (this.
|
|
759
|
-
this.
|
|
760
|
-
this.
|
|
761
|
-
this.botAudioElement = null;
|
|
1037
|
+
else if (this.callStateSubject.value === 'talking' ||
|
|
1038
|
+
this.callStateSubject.value === 'listening') {
|
|
1039
|
+
this.callStateSubject.next('connected');
|
|
762
1040
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
}
|
|
1041
|
+
}));
|
|
1042
|
+
this.subscriptions.add(this.dailyClient.micMuted$.subscribe((muted) => this.isMicMutedSubject.next(muted)));
|
|
1043
|
+
this.callStateSubject.next('connected');
|
|
1044
|
+
this.statusTextSubject.next('Connected');
|
|
1045
|
+
this.callStartTime = Date.now();
|
|
1046
|
+
this.startDurationTimer();
|
|
770
1047
|
});
|
|
771
1048
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
setupEventHandlers() {
|
|
780
|
-
if (!this.client)
|
|
781
|
-
return;
|
|
782
|
-
this.client.on(RTVIEvent.BotStartedSpeaking, () => {
|
|
783
|
-
this.callStateSubject.next('talking');
|
|
784
|
-
});
|
|
785
|
-
this.client.on(RTVIEvent.BotStoppedSpeaking, () => {
|
|
786
|
-
if (this.callStateSubject.value === 'talking') {
|
|
787
|
-
this.callStateSubject.next('connected');
|
|
788
|
-
}
|
|
789
|
-
});
|
|
790
|
-
this.client.on(RTVIEvent.TrackStarted, (track, participant) => {
|
|
791
|
-
if (this.botAudioElement && participant && !participant.local && track.kind === 'audio') {
|
|
792
|
-
const stream = new MediaStream([track]);
|
|
793
|
-
this.botAudioElement.srcObject = stream;
|
|
794
|
-
this.botAudioElement.play().catch(console.error);
|
|
795
|
-
}
|
|
796
|
-
});
|
|
797
|
-
this.client.on(RTVIEvent.UserTranscript, (data) => {
|
|
798
|
-
var _a;
|
|
799
|
-
this.userTranscriptSubject.next({
|
|
800
|
-
text: data.text,
|
|
801
|
-
final: (_a = data.final) !== null && _a !== void 0 ? _a : false,
|
|
802
|
-
});
|
|
803
|
-
});
|
|
804
|
-
this.client.on(RTVIEvent.BotTranscript, (data) => {
|
|
805
|
-
if (data === null || data === void 0 ? void 0 : data.text) {
|
|
806
|
-
this.botTranscriptSubject.next(data.text);
|
|
807
|
-
}
|
|
808
|
-
});
|
|
809
|
-
this.client.on(RTVIEvent.Error, () => {
|
|
810
|
-
this.statusTextSubject.next('Error occurred');
|
|
811
|
-
});
|
|
812
|
-
this.client.on(RTVIEvent.Disconnected, () => {
|
|
1049
|
+
disconnect() {
|
|
1050
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1051
|
+
this.stopDurationTimer();
|
|
1052
|
+
this.audioAnalyzer.stop();
|
|
1053
|
+
// Daily first, then WebSocket
|
|
1054
|
+
yield this.dailyClient.disconnect();
|
|
1055
|
+
this.wsClient.disconnect();
|
|
813
1056
|
this.callStateSubject.next('ended');
|
|
814
|
-
this.statusTextSubject.next('
|
|
815
|
-
this.disconnect();
|
|
1057
|
+
this.statusTextSubject.next('Call Ended');
|
|
816
1058
|
});
|
|
817
1059
|
}
|
|
1060
|
+
toggleMic() {
|
|
1061
|
+
const current = this.isMicMutedSubject.value;
|
|
1062
|
+
this.dailyClient.setMuted(!current);
|
|
1063
|
+
}
|
|
818
1064
|
startDurationTimer() {
|
|
819
1065
|
const updateDuration = () => {
|
|
820
1066
|
if (this.callStartTime > 0) {
|
|
@@ -824,18 +1070,26 @@ class VoiceAgentService {
|
|
|
824
1070
|
this.durationSubject.next(`${minutes}:${String(seconds).padStart(2, '0')}`);
|
|
825
1071
|
}
|
|
826
1072
|
};
|
|
827
|
-
updateDuration();
|
|
1073
|
+
updateDuration();
|
|
828
1074
|
this.durationInterval = setInterval(updateDuration, 1000);
|
|
829
1075
|
}
|
|
1076
|
+
stopDurationTimer() {
|
|
1077
|
+
if (this.durationInterval) {
|
|
1078
|
+
clearInterval(this.durationInterval);
|
|
1079
|
+
this.durationInterval = null;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
830
1082
|
}
|
|
831
|
-
VoiceAgentService.ɵprov = i0.ɵɵdefineInjectable({ factory: function VoiceAgentService_Factory() { return new VoiceAgentService(i0.ɵɵinject(AudioAnalyzerService)); }, token: VoiceAgentService, providedIn: "root" });
|
|
1083
|
+
VoiceAgentService.ɵprov = i0.ɵɵdefineInjectable({ factory: function VoiceAgentService_Factory() { return new VoiceAgentService(i0.ɵɵinject(AudioAnalyzerService), i0.ɵɵinject(WebSocketVoiceClientService), i0.ɵɵinject(DailyVoiceClientService)); }, token: VoiceAgentService, providedIn: "root" });
|
|
832
1084
|
VoiceAgentService.decorators = [
|
|
833
1085
|
{ type: Injectable, args: [{
|
|
834
|
-
providedIn: 'root'
|
|
1086
|
+
providedIn: 'root',
|
|
835
1087
|
},] }
|
|
836
1088
|
];
|
|
837
1089
|
VoiceAgentService.ctorParameters = () => [
|
|
838
|
-
{ type: AudioAnalyzerService }
|
|
1090
|
+
{ type: AudioAnalyzerService },
|
|
1091
|
+
{ type: WebSocketVoiceClientService },
|
|
1092
|
+
{ type: DailyVoiceClientService }
|
|
839
1093
|
];
|
|
840
1094
|
|
|
841
1095
|
const VOICE_MODAL_CONFIG = new InjectionToken('VOICE_MODAL_CONFIG');
|
|
@@ -2940,7 +3194,9 @@ VoiceAgentModule.decorators = [
|
|
|
2940
3194
|
],
|
|
2941
3195
|
providers: [
|
|
2942
3196
|
VoiceAgentService,
|
|
2943
|
-
AudioAnalyzerService
|
|
3197
|
+
AudioAnalyzerService,
|
|
3198
|
+
WebSocketVoiceClientService,
|
|
3199
|
+
DailyVoiceClientService
|
|
2944
3200
|
],
|
|
2945
3201
|
exports: [
|
|
2946
3202
|
VoiceAgentModalComponent
|
|
@@ -3211,5 +3467,5 @@ HiveGptModule.decorators = [
|
|
|
3211
3467
|
* Generated bundle index. Do not edit.
|
|
3212
3468
|
*/
|
|
3213
3469
|
|
|
3214
|
-
export { AudioAnalyzerService, ChatBotComponent, ChatDrawerComponent, HiveGptModule, VOICE_MODAL_CLOSE_CALLBACK, VOICE_MODAL_CONFIG, VoiceAgentModalComponent, VoiceAgentModule, VoiceAgentService, BotsService as ɵa, SocketService as ɵb, ConversationService as ɵc, NotificationSocket as ɵd, TranslationService as ɵe,
|
|
3470
|
+
export { AudioAnalyzerService, ChatBotComponent, ChatDrawerComponent, HiveGptModule, VOICE_MODAL_CLOSE_CALLBACK, VOICE_MODAL_CONFIG, VoiceAgentModalComponent, VoiceAgentModule, VoiceAgentService, BotsService as ɵa, SocketService as ɵb, ConversationService as ɵc, NotificationSocket as ɵd, TranslationService as ɵe, WebSocketVoiceClientService as ɵf, DailyVoiceClientService as ɵg, VideoPlayerComponent as ɵh, SafeHtmlPipe as ɵi, BotHtmlEditorComponent as ɵj };
|
|
3215
3471
|
//# sourceMappingURL=hivegpt-hiveai-angular.js.map
|