1+ import { AudioRecordingWorklet } from "$lib/helpers/pcmProcessor" ;
2+
3+ // @ts -ignore
4+ const SpeechRecognition = window . SpeechRecognition || window . webkitSpeechRecognition ;
5+
6+ // @ts -ignore
7+ const AudioContext = window . AudioContext || window . webkitAudioContext ;
8+
9+ export const realtimeChat = {
10+
11+ /** @type {WebSocket | null } */
12+ socket : null ,
13+
14+ /** @type {MediaRecorder | null } */
15+ mediaRecorder : null ,
16+
17+ /** @type {MediaStream | null } */
18+ mediaStream : null ,
19+
20+ /** @type {SpeechRecognition | null } */
21+ recognition : null ,
22+
23+ /**
24+ * @param {string } agentId
25+ * @param {string } conversationId
26+ */
27+ start ( agentId , conversationId ) {
28+ this . socket = new WebSocket ( `ws://localhost:5100/chat/stream/${ agentId } /${ conversationId } ` ) ;
29+
30+ this . socket . onopen = async ( ) => {
31+ console . log ( "WebSocket connected" ) ;
32+
33+ this . socket ?. send ( JSON . stringify ( {
34+ event : "start"
35+ } ) ) ;
36+
37+ this . mediaStream = await navigator . mediaDevices . getUserMedia ( { audio : true } ) ;
38+ const audioCtx = new AudioContext ( { sampleRate : 16000 } ) ;
39+
40+ const workletName = "audio-recorder-worklet" ;
41+ const src = createWorketFromSrc ( workletName , AudioRecordingWorklet ) ;
42+ await audioCtx . audioWorklet . addModule ( src ) ;
43+
44+ const workletNode = new AudioWorkletNode ( audioCtx , workletName ) ;
45+ const micSource = audioCtx . createMediaStreamSource ( this . mediaStream ) ;
46+ micSource . connect ( workletNode ) ;
47+
48+ workletNode . port . onmessage = event => {
49+ const arrayBuffer = event . data . data . int16arrayBuffer ;
50+ if ( arrayBuffer && this . socket ?. readyState === WebSocket . OPEN ) {
51+ const arrayBufferString = arrayBufferToBase64 ( arrayBuffer ) ;
52+ this . socket . send ( JSON . stringify ( {
53+ event : 'media' ,
54+ payload : arrayBufferString
55+ } ) ) ;
56+ }
57+ } ;
58+
59+ // this.recognition = new SpeechRecognition();
60+ // this.recognition.continuous = true;
61+ // this.recognition.interimResults = false;
62+ // this.recognition.lang = "en-US";
63+
64+ // this.recognition.onresult = (/** @type { any } */ event) => {
65+ // const lastResult = event.results[event.results.length - 1];
66+ // const transcript = lastResult[0].transcript.trim();
67+
68+ // console.log("Recognized:", transcript);
69+
70+ // const message = {
71+ // event: "media",
72+ // payload: transcript
73+ // };
74+
75+ // if (this.socket?.readyState === WebSocket.OPEN) {
76+ // this.socket.send(JSON.stringify(message));
77+ // }
78+ // };
79+
80+ // this.recognition.onend = () => {
81+ // console.log('Speech recognition closed.');
82+ // };
83+ // this.recognition.start();
84+
85+ // navigator.mediaDevices.getUserMedia({ audio: true })
86+ // .then(stream => {
87+ // this.mediaStream = stream;
88+ // this.mediaRecorder = new MediaRecorder(stream, { mimeType: "audio/webm" });
89+ // /** @type {any[] } */
90+ // let audioChunks = [];
91+ // this.mediaRecorder.ondataavailable = (/** @type {any } */ event) => {
92+ // if (event.data.size > 0) {
93+ // // audioChunks.push(event.data);
94+ // }
95+ // };
96+
97+ // this.mediaRecorder.onstop = async () => {
98+ // console.log('mediaRecorder stopped');
99+ // // const blob = new Blob(audioChunks, { type: 'audio/webm' });
100+ // // const arrayBuffer = await blob.arrayBuffer();
101+
102+ // // // Decode audio and downsample to PCM16
103+ // // const audioCtx = new AudioContext({ sampleRate: 16000 });
104+ // // const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
105+
106+ // // const channelData = audioBuffer.getChannelData(0); // mono
107+ // // const pcm16 = new Int16Array(channelData.length);
108+
109+ // // for (let i = 0; i < channelData.length; i++) {
110+ // // pcm16[i] = Math.max(-1, Math.min(1, channelData[i])) * 32767;
111+ // // }
112+
113+ // // const pcmBytes = new Uint8Array(pcm16.buffer);
114+ // // const base64 = btoa(String.fromCharCode(...pcmBytes));
115+ // // console.log(base64);
116+ // };
117+
118+ // this.mediaRecorder.start();
119+ // })
120+ // .catch((err) => {
121+ // console.error("Failed to access microphone", err);
122+ // });
123+ } ;
124+
125+ this . socket . onclose = ( ) => {
126+ console . log ( "Websocket closed" ) ;
127+ }
128+
129+ this . socket . onerror = ( /** @type {any } */ e ) => console . error ( 'WebSocket error' , e ) ;
130+ } ,
131+
132+ stop ( ) {
133+ if ( this . mediaRecorder ) {
134+ this . mediaRecorder . stop ( ) ;
135+ }
136+
137+ if ( this . mediaStream ) {
138+ this . mediaStream . getTracks ( ) . forEach ( t => t . stop ( ) ) ;
139+ this . mediaStream = null ;
140+ }
141+
142+ if ( this . recognition ) {
143+ this . recognition . stop ( ) ;
144+ }
145+
146+ if ( this . socket ?. readyState === WebSocket . OPEN ) {
147+ this . socket . send ( JSON . stringify ( {
148+ event : 'disconnect'
149+ } ) ) ;
150+ this . socket . close ( ) ;
151+ }
152+ }
153+ } ;
154+
155+ /**
156+ * @param {ArrayBuffer } buffer
157+ */
158+ function arrayBufferToBase64 ( buffer ) {
159+ var binary = "" ;
160+ var bytes = new Uint8Array ( buffer ) ;
161+ var len = bytes . byteLength ;
162+ for ( var i = 0 ; i < len ; i ++ ) {
163+ binary += String . fromCharCode ( bytes [ i ] ) ;
164+ }
165+ return window . btoa ( binary ) ;
166+ }
167+
168+ /**
169+ * @param {string } workletName
170+ * @param {string } workletSrc
171+ */
172+ function createWorketFromSrc ( workletName , workletSrc ) {
173+ const script = new Blob (
174+ [ `registerProcessor("${ workletName } ", ${ workletSrc } )` ] ,
175+ {
176+ type : "application/javascript" ,
177+ } ,
178+ ) ;
179+
180+ return URL . createObjectURL ( script ) ;
181+ } ;
0 commit comments