KingNish commited on
Commit
5e2ef45
1 Parent(s): de313f3

Update script1.js

Browse files
Files changed (1) hide show
  1. script1.js +527 -1
script1.js CHANGED
@@ -1 +1,527 @@
1
- eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('9 1h=W.X(\'1h\');9 2A=W.X(\'4r\');9 1i=W.X(\'4s\');9 4t=W.X(\'4u\');9 2B=W.X(\'4v\');9 2C=W.X(\'4w\');9 R=W.X(\'4x\');9 3r=W.X(\'1b\');b E;b 26=F;b 27=0;b Y=\'\';b Z=G;b M=G;b 1j=G;b 1c=F;b 4y=\'\';b 28=F;b 29="";b 1k=F;9 3s=1d;9 3t="4z://4A.4B.4C/4D/4E/4F";9 3u=S;b u=F;b 11=[];b 1S=[];9 1e=o 2D();9 1f=o 2D();9 3v=10;9 3w=4G;b v=[];9 1T=o 2D();9 3x=4H;9 1U=h=>h.1l().4I().2E(/[^\\w\\s]/g,\'\');9 2F=(13,l,2a,3y)=>`${13}-${l}-${2G.2H(2a)}-${3y}`;9 3A=(h,l)=>{9 13=1U(h);9 e=2F(13,l,v,1i.H);a(1f.2b(e)||1e.2b(e))1g;1S.1m({h:h.1l(),l,e});2I()};9 2I=1n()=>{2J(1S.m>0&&1f.4J<3v){9{h,l,e}=1S.3B();9 1o=o 3C();1f.2K(e,1o);1V{9 t=p 2L(`/3D?h=${2M(h)}&2a=${2G.2H(v)}&3E=${1i.H}`,{2N:\'2O\',3F:{\'3G\':\'z/T-2c\',\'1W-3H\':\'3I/3J\'},1p:1o.1p});a(!t.1q)1X o 15(\'2P t 2Q 2d 1q\');9 2R=p 2S(t.3K,l,1o.1p);a(2R)1e.2K(e,{I:2R,2e:N.O()})}1Y(f){a(f.3L!==\'2f\')J.f("15 4K U:",f)}2g{1f.2h(e);2I()}}};9 2S=1n(2i,l,2j)=>{9 1r=2i.3M();9 2k=o 3N("3O-8");b 17="";1V{2J(K){9{2l,H}=p 1r.3P();a(2l)2T;a(2j.2m)1X o 3Q(\'3R 2m\',\'2f\');9 2n=2k.3S(H,{2c:K});17+=2n;9 P=17.3T(\'\\n\');2o(b i=0;i<P.m-1;i++){9 j=P[i];a(j.2U(\'3U: \')){9 k=j.3V(6).1l();a(k){9 x=p 1Z(k,l);1g x}}}17=P[P.m-1]}}1Y(f){J.f("15 2p 2S:",f)}2g{1r.3W()}1g F};9 1s=1n()=>{a(11.m>0){9 3X=11.3B();9 U=o 4L(3X.I);Q();9 3Y=o 3Z(20=>{U.4M=20;U.40=20});a(u){u.41();u.42=0}u=U;p U.4N();p 3Y;1s()}L{Q()}};9 1Z=1n(z,l)=>{9 43=1U(z);9 e=`${43}-${l}`;a(1T.2b(e)){9 1t=1T.44(e);a(N.O()-1t.2e<3x){1g 1t.I}L{1T.2h(e)}}1V{9 t=p 2L(`${3t}?l=${l}&z=${2M(z)}`,{2N:\'2O\'});a(!t.1q)1X o 15(\'2P t 2Q 2d 1q\');9 45=p t.4O();9 x=4P.4Q(45);1T.2K(e,{I:x,2e:N.O()});1g x}1Y(f){J.f("15 4R 4S U:",f);1g F}};9 2V=1n(h)=>{J.21("4T h c 1u:",h);Z=K;Q();27=N.O();1k=F;9 13=1U(h);9 e=2F(13,1i.H,v,1i.H);a(1e.2b(e)){9 1t=1e.44(e);a(N.O()-1t.2e<3w){9 46=1t.I;11.1m({I:46,2q:K});1s()}L{1e.2h(e)}}1c=o 3C();9 I=`/3D?h=${2M(h)}&3E=${1i.H}&2a=${2G.2H(v)}`;1V{9 t=p 2L(I,{2N:\'2O\',3F:{\'3G\':\'z/T-2c\',\'1W-3H\':\'3I/3J\'},1p:1c.1p});a(!t.1q){a(t.47===4U){J.21("4V 4W 4X, 4Y 2p 1 4Z...");p o 3Z(20=>50(20,51));p 2V(h);1g}1X o 15(`2P t 2Q 2d 1q:${t.47}`)}J.21("52 U t 53");p 2W(t.3K,2A.H,1c.1p)}1Y(f){a(f.3L!==\'2f\'){J.f("15 54 h c 1u:",f)}}2g{Z=G;Q()}};9 2W=1n(2i,l,2j)=>{9 1r=2i.3M();9 2k=o 3N("3O-8");b 17="";b 2X=0;b 1v="";b 1w="";b 22="";b 2r="";1V{2J(K){9{2l,H}=p 1r.3P();a(2l)2T;a(2j.2m)1X o 3Q(\'3R 2m\',\'2f\');a(M){2s(\'48 55 56\');2T}9 2n=2k.3S(H,{2c:K});17+=2n;9 P=17.3T(\'\\n\');2o(b i=0;i<P.m-1;i++){9 j=P[i];a(j.2U(\'3U: \')){9 k=j.3V(6).1l();a(k){a(!1k)1k=N.O();1v+=k+" ";1w+=k+" ";22+=k+" ";3r.k=1w;a(2X<2){9 x=p 1Z(k,l);a(x){11.1m({I:x,2q:G});a(!u)1s()}2r+=k+" ";2X++}L{b 1x=22.2E(2r,\'\').1l();a(1x.m>=3u){9 x=p 1Z(1x,l);a(x){11.1m({I:x,2q:G});a(!u)1s()}22=""}}a(1v!==\'\'){1v=\'\'}}}}17=P[P.m-1]}}1Y(f){J.f("15 2p 2W:",f)}2g{1r.3W();b 1x=22.2E(2r,\'\').1l();a(1x!==""){9 x=p 1Z(1x,l);a(x){11.1m({I:x,2q:G});a(!u)1s()}}a(1v!==\'\'){1v=\'\'}a(1w!==\'\'){2Y(\'49\',1w);1w=\'\'}}};9 Q=(4a=F)=>{2C.k=M?"4b: 4c":"4b: 4d";2C.25=M?"1y 1z-1A 1B-4 1C-2 z-1D 1E 1F-1G 1H-1I 1J-S A-B-c-r y-2t-18 c-2t-2u 1K:A-B-c-r y-2t-1d c-2t-1L":"1y 1z-1A 1B-4 1C-2 z-1D 1E 1F-1G 1H-1I 1J-S A-B-c-r y-C-S c-C-18 2v:y-C-1L 2v:c-C-4e 1K:A-B-c-r y-C-18 c-C-1d";a(Z&&!u){R.k="1u: 57...";R.25="1y 1z-1A 1B-4 1C-2 z-1D 1E 1F-1G 1H-1I 1J-S A-B-c-r y-2w-18 c-2w-2u 1K:A-B-c-r y-2w-1d c-2w-1L"}L a(u&&!M){R.k=4a||"1u: 4c";R.25="1y 1z-1A 1B-4 1C-2 z-1D 1E 1F-1G 1H-1I 1J-S A-B-c-r y-2x-18 c-2x-2u 1K:A-B-c-r y-2x-1d c-2x-1L"}L a(M){R.k="1u: 2y";R.25="1y 1z-1A 1B-4 1C-2 z-1D 1E 1F-1G 1H-1I 1J-S A-B-c-r y-2z-18 c-2z-2u 1K:A-B-c-r y-2z-1d c-2z-1L"}L{R.k="1u: 4d";R.25="1y 1z-1A 1B-4 1C-2 z-1D 1E 1F-1G 1H-1I 1J-S A-B-c-r y-C-S c-C-18 2v:y-C-1L 2v:c-C-4e 1K:A-B-c-r y-C-18 c-C-1d"}};a(\'4f\'2p 58){E=o 4f();59.5a(E,{5b:K,5c:K,5d:\'5e-5f\',5g:3});E.5h=()=>{J.21("2Z 4g 5i");Y=\'\';M=K;28=N.O();Q();1h.30=\'<V 31="32://33.34.35/36/V" 1M="24" 37="24" 38="0 0 24 24" 39="3a" D="3b" D-1M="2" D-3c="1N" D-3d="1N"><q d="4h 4i-4j"></q><q d="3e 3f 3 0 0 0-3 3g 3 0 0 0 6 3h 3 0 0 0-3-3z"></q><q d="3i 3j 7 0 0 1-14 3k-2"></q><j 1O="12" 1P="19" 1Q="12" 1R="23"></j><j 1O="8" 1P="23" 1Q="16" 1R="23"></j></V> 4k 2y\'};E.5j=(T)=>{b 1a=\'\';2o(b i=T.5k;i<T.3l.m;i++){9 1b=T.3l[i][0].1b;a(T.3l[i].5l){Y+=1b;2s(\'5m\');3m(Y);Y=\'\';M=G;Q();27=N.O()}L{1a+=1b;M=K;28=N.O();Q();a(1a.m>29.m+5){4l(29)}29=1a;3A(1a,2A.H);a(Z&&4m(1a)){2s(\'5n\')}}}};E.40=(T)=>{J.f(\'2Z 4g f:\',T.f);a(1j)E.3n()};E.5o=()=>{M=G;Q();a(!Z&&Y!==\'\'){3m(Y);Y=\'\'}a(1j)E.3n()};1h.5p(\'5q\',()=>{a(1j){E.5r();1j=G;1h.30=\'<V 5s="5t" 31="32://33.34.35/36/V" 1M="24" 37="24" 38="0 0 24 24" 39="3a" D="3b" D-1M="2" D-3c="1N" D-3d="1N"><q d="3e 3f 3 0 0 0-3 3g 3 0 0 0 6 3h 3 0 0 0-3-3z"></q><q d="3i 3j 7 0 0 1-14 3k-2"></q><j 1O="12" 1P="19" 1Q="12" 1R="23"></j><j 1O="8" 1P="23" 1Q="16" 1R="23"></j></V> 5u 2y\'}L{E.3n();1j=K;1h.30=\'<V 31="32://33.34.35/36/V" 1M="24" 37="24" 38="0 0 24 24" 39="3a" D="3b" D-1M="2" D-3c="1N" D-3d="1N"><q d="4h 4i-4j"></q><q d="3e 3f 3 0 0 0-3 3g 3 0 0 0 6 3h 3 0 0 0-3-3z"></q><q d="3i 3j 7 0 0 1-14 3k-2"></q><j 1O="12" 1P="19" 1Q="12" 1R="23"></j><j 1O="8" 1P="23" 1Q="16" 1R="23"></j></V> 4k 2y\'}})}L{5v(\'5w 5x 5y 2d 5z 5A 5B 2Z 5C.\')}9 2Y=(3o,1W)=>{a(v.m>0&&v[v.m-1].3o===\'49\'&&v[v.m-1].1W===""){v.5D()}v.1m({3o,1W});a(v.m>6)v.5E(0,2)};9 3m=(1b)=>{9 3p=1b.5F();a(3p!==\'\'&&!Z){26=3p;2V(26);2Y(\'48\',26)}};9 4m=(1a)=>N.O()-28>3s||1a.m>5;9 2s=(3q=\'5G\')=>{J.21(`5H U(3q:${3q})...`);a(u){u.41();u.42=0;u=F}11.m=0;Z=G;a(1c){1c.4n();1c=F}1e.5I();1S.m=0;Q()};9 4l=(h)=>{9 13=1U(h);2o(9[e,1o]5J 1f){a(e.2U(13)){1o.4n();1f.2h(e)}}};9 4o=()=>{a(1k){9 4p=1k-27;2B.k=`4q:${4p}5K`}L{2B.k="4q: 5L"}};5M(4o,5N);',62,360,'|||||||||const|if|let|to||cacheKey|error||query||line|textContent|voice|length||new|await|path|||response|currentAudio|conversationHistory||audioUrl|from|text|bg|gradient|gray|stroke|speechRecognizer|null|false|value|url|console|true|else|isUserSpeaking|Date|now|lines|updateActivityIndicators|aiActivityIndicator|300|event|audio|svg|document|getElementById|completeTranscript|isRequestInProgress||audioPlaybackQueue||normalizedQuery||Error||buffer|400||interimTranscript|transcript|requestAbortController|500|prefetchCache|pendingPrefetchRequests|return|startStopButton|modelSelectionDropdown|isSpeechRecognitionActive|firstResponseTextTimestamp|trim|push|async|abortController|signal|ok|reader|playNextAudio|cachedData|AI|fullResponseText|fullResponseText2|unsentTextChunk|indicator|rounded|full|px|py|white|flex|items|center|transition|colors|duration|hover|700|width|round|x1|y1|x2|y2|prefetchQueue|audioCache|normalizeQueryText|try|content|throw|catch|generateTextToSpeechAudio|resolve|log|textChunk|||className|activeQuery|queryStartTime|lastUserSpeechTimestamp|prefetchTextQuery|history|has|stream|not|timestamp|AbortError|finally|delete|responseStream|abortSignal|decoder|done|aborted|chunk|for|in|isPrefetched|sentText|interruptAudioPlayback|blue|600|dark|purple|green|Listening|yellow|voiceSelectionDropdown|responseTimeDisplay|userActivityIndicator|Map|replace|generateCacheKey|JSON|stringify|processPrefetchQueue|while|set|fetch|encodeURIComponent|method|GET|Network|was|firstAudioUrl|handleStreamingResponseForPrefetch|break|startsWith|sendQueryToAI|handleStreamingResponse|initialChunksSent|addToConversationHistory|Speech|innerHTML|xmlns|http|www|w3|org|2000|height|viewBox|fill|none|currentColor|linecap|linejoin|M12|1a3|3v8a3|0V4a3|M19|10v2a7|0v|results|processSpeechTranscript|start|role|trimmedTranscript|reason|transcriptDiv|USER_SPEECH_INTERRUPT_DELAY|TEXT_TO_SPEECH_API_ENDPOINT|CHUNK_SIZE|MAX_PREFETCH_REQUESTS|prefetchCacheExpiration|audioCacheExpiration|modelName||prefetchFirstAudioChunk|shift|AbortController|stream_audio|model|headers|accept|type|application|json|body|name|getReader|TextDecoder|utf|read|DOMException|Request|decode|split|data|substring|releaseLock|audioData|audioPromise|Promise|onerror|pause|currentTime|normalizedText|get|audioBlob|prefetchedAudioUrl|status|user|assistant|state|User|Speaking|Idle|800|webkitSpeechRecognition|recognition|M9|9h6v6h|6z|Stop|cancelPrefetchRequests|shouldInterruptAudioPlayback|abort|updateLatency|latency|Latency|voiceSelect|modelSelect|noiseSuppressionCheckbox|noiseSuppression|responseTime|userIndicator|aiIndicator|partialTranscript|https|api|streamelements|com|kappa|v2|speech|60000|3600000|toLowerCase|size|prefetching|Audio|onended|play|blob|URL|createObjectURL|generating|TTS|Sending|429|Rate|limit|hit|retrying|second|setTimeout|1000|Streaming|received|sending|is|speaking|Processing|window|Object|assign|continuous|interimResults|language|en|US|maxAlternatives|onstart|started|onresult|resultIndex|isFinal|final|interim|onend|addEventListener|click|stop|id|microphoneIcon|Start|alert|Your|browser|does|support|the|Web|API|pop|splice|trimStart|unknown|Interrupting|clear|of|ms|0ms|setInterval|200'.split('|'),0,{}))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const startStopButton = document.getElementById('startStopButton');
2
+ const voiceSelectionDropdown = document.getElementById('voiceSelect');
3
+ const modelSelectionDropdown = document.getElementById('modelSelect');
4
+ const noiseSuppressionCheckbox = document.getElementById('noiseSuppression');
5
+ const responseTimeDisplay = document.getElementById('responseTime');
6
+ const userActivityIndicator = document.getElementById('userIndicator');
7
+ const aiActivityIndicator = document.getElementById('aiIndicator');
8
+ const transcriptDiv = document.getElementById('transcript');
9
+
10
+ let speechRecognizer;
11
+ let activeQuery = null;
12
+ let queryStartTime = 0;
13
+ let completeTranscript = '';
14
+ let isRequestInProgress = false;
15
+ let isUserSpeaking = false;
16
+ let isSpeechRecognitionActive = false;
17
+ let requestAbortController = null;
18
+ let partialTranscript = '';
19
+ let lastUserSpeechTimestamp = null;
20
+ let prefetchTextQuery = "";
21
+ let firstResponseTextTimestamp = null;
22
+
23
+ // Configuration
24
+ const USER_SPEECH_INTERRUPT_DELAY = 500;
25
+ const TEXT_TO_SPEECH_API_ENDPOINT = "https://api.streamelements.com/kappa/v2/speech";
26
+ const CHUNK_SIZE = 300;
27
+
28
+ // Audio Management
29
+ let currentAudio = null;
30
+ let audioPlaybackQueue = [];
31
+ let prefetchQueue = [];
32
+
33
+ // Enhanced Prefetching and Caching
34
+ const prefetchCache = new Map();
35
+ const pendingPrefetchRequests = new Map();
36
+ const MAX_PREFETCH_REQUESTS = 10;
37
+ const prefetchCacheExpiration = 60000; // 1 minute
38
+
39
+ // Global Conversation History
40
+ let conversationHistory = [];
41
+
42
+ // Audio Caching
43
+ const audioCache = new Map();
44
+ const audioCacheExpiration = 3600000; // 1 hour
45
+
46
+ // Normalize query text
47
+ const normalizeQueryText = query => query.trim().toLowerCase().replace(/[^\w\s]/g, '');
48
+
49
+ // Generate a cache key
50
+ const generateCacheKey = (normalizedQuery, voice, history, modelName) =>
51
+ `${normalizedQuery}-${voice}-${JSON.stringify(history)}-${modelName}`;
52
+
53
+ // Prefetch and cache the first TTS audio chunk
54
+ const prefetchFirstAudioChunk = (query, voice) => {
55
+ const normalizedQuery = normalizeQueryText(query);
56
+ const cacheKey = generateCacheKey(normalizedQuery, voice, conversationHistory, modelSelectionDropdown.value);
57
+
58
+ if (pendingPrefetchRequests.has(cacheKey) || prefetchCache.has(cacheKey)) return;
59
+
60
+ prefetchQueue.push({ query:query.trim(), voice, cacheKey });
61
+ processPrefetchQueue();
62
+ };
63
+
64
+ // Process the prefetch queue
65
+ const processPrefetchQueue = async () => {
66
+ while (prefetchQueue.length > 0 && pendingPrefetchRequests.size < MAX_PREFETCH_REQUESTS) {
67
+ const { query, voice, cacheKey } = prefetchQueue.shift();
68
+ const abortController = new AbortController();
69
+ pendingPrefetchRequests.set(cacheKey, abortController);
70
+
71
+ const url = '/stream_text';
72
+ const requestBody = {
73
+ query: query,
74
+ history: JSON.stringify(conversationHistory),
75
+ model: modelSelectionDropdown.value
76
+ };
77
+
78
+ try {
79
+ const response = await fetch(url, {
80
+ method: 'POST',
81
+ headers: {
82
+ 'Accept': 'text/event-stream',
83
+ 'Content-Type': 'application/json'
84
+ },
85
+ body: JSON.stringify(requestBody),
86
+ signal: abortController.signal
87
+ });
88
+
89
+ if (!response.ok) throw new Error('Network response was not ok');
90
+
91
+ const firstAudioUrl = await handleStreamingResponseForPrefetch(response.body, voice, abortController.signal);
92
+
93
+ if (firstAudioUrl) prefetchCache.set(cacheKey, { url: firstAudioUrl, timestamp: Date.now() });
94
+
95
+ } catch (error) {
96
+ if (error.name !== 'AbortError') console.error("Error prefetching audio:", error);
97
+ } finally {
98
+ pendingPrefetchRequests.delete(cacheKey);
99
+ processPrefetchQueue();
100
+ }
101
+ }
102
+ };
103
+
104
+ // Handle the streaming response for prefetching
105
+ const handleStreamingResponseForPrefetch = async (responseStream, voice, abortSignal) => {
106
+ const reader = responseStream.getReader();
107
+ const decoder = new TextDecoder("utf-8");
108
+ let buffer = "";
109
+
110
+ try {
111
+ while (true) {
112
+ const { done, value } = await reader.read();
113
+ if (done) break;
114
+ if (abortSignal.aborted) throw new DOMException('Request aborted', 'AbortError');
115
+
116
+ const chunk = decoder.decode(value, { stream: true });
117
+ buffer += chunk;
118
+ const lines = buffer.split('\n');
119
+
120
+ for (let i = 0; i < lines.length - 1; i++) {
121
+ const line = lines[i];
122
+ if (line.startsWith('data: ')) {
123
+ const textContent = line.substring(6).trim();
124
+ if (textContent) {
125
+ const audioUrl = await generateTextToSpeechAudio(textContent, voice);
126
+ return audioUrl;
127
+ }
128
+ }
129
+ }
130
+
131
+ buffer = lines[lines.length - 1];
132
+ }
133
+ } catch (error) {
134
+ console.error("Error in handleStreamingResponseForPrefetch:", error);
135
+ } finally {
136
+ reader.releaseLock();
137
+ }
138
+
139
+ return null;
140
+ };
141
+
142
+ // Play audio from the queue
143
+ const playNextAudio = async () => {
144
+ if (audioPlaybackQueue.length > 0) {
145
+ const audioData = audioPlaybackQueue.shift();
146
+ const audio = new Audio(audioData.url);
147
+ updateActivityIndicators();
148
+
149
+ const audioPromise = new Promise(resolve => {
150
+ audio.onended = resolve;
151
+ audio.onerror = resolve;
152
+ });
153
+ if (currentAudio) {
154
+ currentAudio.pause();
155
+ currentAudio.currentTime = 0;
156
+ }
157
+
158
+ currentAudio = audio;
159
+ await audio.play();
160
+ await audioPromise;
161
+ playNextAudio();
162
+ } else {
163
+ updateActivityIndicators();
164
+ }
165
+ };
166
+
167
+ // Generate Text-to-Speech audio with caching
168
+ const generateTextToSpeechAudio = async (text, voice) => {
169
+ const normalizedText = normalizeQueryText(text);
170
+ const cacheKey = `${normalizedText}-${voice}`;
171
+
172
+ if (audioCache.has(cacheKey)) {
173
+ const cachedData = audioCache.get(cacheKey);
174
+ if (Date.now() - cachedData.timestamp < audioCacheExpiration) {
175
+ return cachedData.url;
176
+ } else {
177
+ audioCache.delete(cacheKey);
178
+ }
179
+ }
180
+
181
+ try {
182
+ const response = await fetch(`${TEXT_TO_SPEECH_API_ENDPOINT}?voice=${voice}&text=${encodeURIComponent(text)}`, { method: 'GET' });
183
+ if (!response.ok) throw new Error('Network response was not ok');
184
+ const audioBlob = await response.blob();
185
+ const audioUrl = URL.createObjectURL(audioBlob);
186
+
187
+ audioCache.set(cacheKey, { url: audioUrl, timestamp: Date.now() });
188
+ return audioUrl;
189
+ } catch (error) {
190
+ console.error("Error generating TTS audio:", error);
191
+ return null;
192
+ }
193
+ };
194
+
195
+ // Send a query to the AI
196
+ const sendQueryToAI = async (query) => {
197
+ console.log("Sending query to AI:", query);
198
+ isRequestInProgress = true;
199
+ updateActivityIndicators();
200
+ queryStartTime = Date.now();
201
+ firstResponseTextTimestamp = null;
202
+
203
+ const normalizedQuery = normalizeQueryText(query);
204
+ const cacheKey = generateCacheKey(normalizedQuery, modelSelectionDropdown.value, conversationHistory, modelSelectionDropdown.value);
205
+
206
+ if (prefetchCache.has(cacheKey)) {
207
+ const cachedData = prefetchCache.get(cacheKey);
208
+ if (Date.now() - cachedData.timestamp < prefetchCacheExpiration) {
209
+ const prefetchedAudioUrl = cachedData.url;
210
+ audioPlaybackQueue.push({ url: prefetchedAudioUrl, isPrefetched: true });
211
+ playNextAudio();
212
+ } else {
213
+ prefetchCache.delete(cacheKey);
214
+ }
215
+ }
216
+
217
+ requestAbortController = new AbortController();
218
+
219
+ const url = '/stream_text';
220
+ const requestBody = {
221
+ query: query,
222
+ history: JSON.stringify(conversationHistory),
223
+ model: modelSelectionDropdown.value
224
+ };
225
+
226
+ try {
227
+ const response = await fetch(url, {
228
+ method: 'POST',
229
+ headers: {
230
+ 'Accept': 'text/event-stream',
231
+ 'Content-Type': 'application/json'
232
+ },
233
+ body: JSON.stringify(requestBody),
234
+ signal: requestAbortController.signal
235
+ });
236
+
237
+ if (!response.ok) {
238
+ if (response.status === 429) {
239
+ console.log("Rate limit hit, retrying in 1 second...");
240
+ await new Promise(resolve => setTimeout(resolve, 1000));
241
+ await sendQueryToAI(query);
242
+ return;
243
+ }
244
+ throw new Error(`Network response was not ok: ${response.status}`);
245
+ }
246
+
247
+ console.log("Streaming audio response received");
248
+ await handleStreamingResponse(response.body, voiceSelectionDropdown.value, requestAbortController.signal);
249
+ } catch (error) {
250
+ if (error.name !== 'AbortError') {
251
+ console.error("Error sending query to AI:", error);
252
+ }
253
+ } finally {
254
+ isRequestInProgress = false;
255
+ updateActivityIndicators();
256
+ }
257
+ };
258
+
259
+ // Handle the streaming audio response
260
+ const handleStreamingResponse = async (responseStream, voice, abortSignal) => {
261
+ const reader = responseStream.getReader();
262
+ const decoder = new TextDecoder("utf-8");
263
+ let buffer = "";
264
+ let initialChunksSent = 0;
265
+ let fullResponseText = "";
266
+ let fullResponseText2 = "";
267
+ let textChunk = "";
268
+ let sentText = "";
269
+
270
+ try {
271
+ while (true) {
272
+ const { done, value } = await reader.read();
273
+ if (done) break;
274
+ if (abortSignal.aborted) throw new DOMException('Request aborted', 'AbortError');
275
+
276
+ if (isUserSpeaking) {
277
+ interruptAudioPlayback('user is speaking');
278
+ break;
279
+ }
280
+
281
+ const chunk = decoder.decode(value, { stream: true });
282
+ buffer += chunk;
283
+ const lines = buffer.split('\n');
284
+
285
+ for (let i = 0; i < lines.length - 1; i++) {
286
+ const line = lines[i];
287
+ if (line.startsWith('data: ')) {
288
+ const textContent = line.substring(6).trim();
289
+ if (textContent) {
290
+ if (!firstResponseTextTimestamp) firstResponseTextTimestamp = Date.now();
291
+
292
+ fullResponseText += textContent + " ";
293
+ fullResponseText2 += textContent + " ";
294
+ textChunk += textContent + " ";
295
+ transcriptDiv.textContent = fullResponseText2;
296
+
297
+ if (initialChunksSent < 2) {
298
+ const audioUrl = await generateTextToSpeechAudio(textContent, voice);
299
+ if (audioUrl) {
300
+ audioPlaybackQueue.push({ url: audioUrl, isPrefetched: false });
301
+ if (!currentAudio) playNextAudio();
302
+ }
303
+ sentText += textContent + " ";
304
+ initialChunksSent++;
305
+ } else {
306
+ let unsentTextChunk = textChunk.replace(sentText, '').trim();
307
+
308
+ if (unsentTextChunk.length >= CHUNK_SIZE) {
309
+ const audioUrl = await generateTextToSpeechAudio(unsentTextChunk, voice);
310
+ if (audioUrl) {
311
+ audioPlaybackQueue.push({ url: audioUrl, isPrefetched: false });
312
+ if (!currentAudio) playNextAudio();
313
+ }
314
+ textChunk = "";
315
+ }
316
+ }
317
+
318
+ if (fullResponseText !== '') {
319
+ fullResponseText = '';
320
+ }
321
+ }
322
+ }
323
+ }
324
+
325
+ buffer = lines[lines.length - 1];
326
+ }
327
+ } catch (error) {
328
+ console.error("Error in handleStreamingResponse:", error);
329
+ } finally {
330
+ reader.releaseLock();
331
+
332
+ let unsentTextChunk = textChunk.replace(sentText, '').trim();
333
+ if (unsentTextChunk !== "") {
334
+ const audioUrl = await generateTextToSpeechAudio(unsentTextChunk, voice);
335
+ if (audioUrl) {
336
+ audioPlaybackQueue.push({ url: audioUrl, isPrefetched: false });
337
+ if (!currentAudio) playNextAudio();
338
+ }
339
+ }
340
+
341
+ if (fullResponseText !== '') {
342
+ fullResponseText = '';
343
+ }
344
+ if (fullResponseText2 !== '') {
345
+ addToConversationHistory('assistant', fullResponseText2);
346
+ fullResponseText2 = '';
347
+ }
348
+ }
349
+ };
350
+
351
+ // Update activity indicators
352
+ const updateActivityIndicators = (state = null) => {
353
+ userActivityIndicator.textContent = isUserSpeaking ? "User: Speaking" : "User: Idle";
354
+ userActivityIndicator.className = isUserSpeaking
355
+ ? "indicator rounded-full px-4 py-2 text-white flex items-center transition-colors duration-300 bg-gradient-to-r from-blue-400 to-blue-600 hover:bg-gradient-to-r from-blue-500 to-blue-700"
356
+ : "indicator rounded-full px-4 py-2 text-white flex items-center transition-colors duration-300 bg-gradient-to-r from-gray-300 to-gray-400 dark:from-gray-700 dark:to-gray-800 hover:bg-gradient-to-r from-gray-400 to-gray-500"; // Tailwind classes
357
+
358
+ if (isRequestInProgress && !currentAudio) {
359
+ aiActivityIndicator.textContent = "AI: Processing...";
360
+ aiActivityIndicator.className = "indicator rounded-full px-4 py-2 text-white flex items-center transition-colors duration-300 bg-gradient-to-r from-purple-400 to-purple-600 hover:bg-gradient-to-r from-purple-500 to-purple-700"; // Tailwind class for thinking
361
+ } else if (currentAudio && !isUserSpeaking) {
362
+ aiActivityIndicator.textContent = state || "AI: Speaking";
363
+ aiActivityIndicator.className = "indicator rounded-full px-4 py-2 text-white flex items-center transition-colors duration-300 bg-gradient-to-r from-green-400 to-green-600 hover:bg-gradient-to-r from-green-500 to-green-700"; // Tailwind class for speaking
364
+ } else if (isUserSpeaking) {
365
+ aiActivityIndicator.textContent = "AI: Listening";
366
+ aiActivityIndicator.className = "indicator rounded-full px-4 py-2 text-white flex items-center transition-colors duration-300 bg-gradient-to-r from-yellow-400 to-yellow-600 hover:bg-gradient-to-r from-yellow-500 to-yellow-700"; // Tailwind class for listening
367
+ } else {
368
+ aiActivityIndicator.textContent = "AI: Idle";
369
+ aiActivityIndicator.className = "indicator rounded-full px-4 py-2 text-white flex items-center transition-colors duration-300 bg-gradient-to-r from-gray-300 to-gray-400 dark:from-gray-700 dark:to-gray-800 hover:bg-gradient-to-r from-gray-400 to-gray-500"; // Tailwind classes
370
+ }
371
+ };
372
+
373
+
374
+ // Initialize speech recognition
375
+ if ('webkitSpeechRecognition' in window) {
376
+ speechRecognizer = new webkitSpeechRecognition();
377
+ Object.assign(speechRecognizer, {
378
+ continuous: true,
379
+ interimResults: true,
380
+ language: 'en-US',
381
+ maxAlternatives: 3
382
+ });
383
+
384
+ speechRecognizer.onstart = () => {
385
+ console.log("Speech recognition started");
386
+ completeTranscript = '';
387
+ isUserSpeaking = true;
388
+ lastUserSpeechTimestamp = Date.now();
389
+ updateActivityIndicators();
390
+ startStopButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 9h6v6h-6z"></path><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg> Stop Listening';
391
+ };
392
+
393
+ speechRecognizer.onresult = (event) => {
394
+ let interimTranscript = '';
395
+ for (let i = event.resultIndex; i < event.results.length; i++) {
396
+ const transcript = event.results[i][0].transcript;
397
+ if (event.results[i].isFinal) {
398
+ completeTranscript += transcript;
399
+ interruptAudioPlayback('final');
400
+ processSpeechTranscript(completeTranscript);
401
+ completeTranscript = '';
402
+ isUserSpeaking = false;
403
+ updateActivityIndicators();
404
+ queryStartTime = Date.now();
405
+ } else {
406
+ interimTranscript += transcript;
407
+ isUserSpeaking = true;
408
+ lastUserSpeechTimestamp = Date.now();
409
+ updateActivityIndicators();
410
+
411
+ if (interimTranscript.length > prefetchTextQuery.length + 5) {
412
+ cancelPrefetchRequests(prefetchTextQuery);
413
+ }
414
+ prefetchTextQuery = interimTranscript;
415
+ prefetchFirstAudioChunk(interimTranscript, voiceSelectionDropdown.value);
416
+
417
+ if (isRequestInProgress && shouldInterruptAudioPlayback(interimTranscript)) {
418
+ interruptAudioPlayback('interim');
419
+ }
420
+ }
421
+ }
422
+ };
423
+
424
+ speechRecognizer.onerror = (event) => {
425
+ console.error('Speech recognition error:', event.error);
426
+ if (isSpeechRecognitionActive) speechRecognizer.start();
427
+ };
428
+
429
+ speechRecognizer.onend = () => {
430
+ isUserSpeaking = false;
431
+ updateActivityIndicators();
432
+
433
+ if (!isRequestInProgress && completeTranscript !== '') {
434
+ processSpeechTranscript(completeTranscript);
435
+ completeTranscript = '';
436
+ }
437
+
438
+ if (isSpeechRecognitionActive) speechRecognizer.start();
439
+ };
440
+
441
+ startStopButton.addEventListener('click', () => {
442
+ if (isSpeechRecognitionActive) {
443
+ speechRecognizer.stop();
444
+ isSpeechRecognitionActive = false;
445
+ startStopButton.innerHTML = '<svg id="microphoneIcon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg> Start Listening';
446
+ } else {
447
+ speechRecognizer.start();
448
+ isSpeechRecognitionActive = true;
449
+ startStopButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 9h6v6h-6z"></path><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg> Stop Listening';
450
+ }
451
+ });
452
+ } else {
453
+ alert('Your browser does not support the Web Speech API.');
454
+ }
455
+
456
+ // Add to conversation history
457
+ const addToConversationHistory = (role, content) => {
458
+ if (conversationHistory.length > 0 &&
459
+ conversationHistory[conversationHistory.length - 1].role === 'assistant' &&
460
+ conversationHistory[conversationHistory.length - 1].content === "") {
461
+ conversationHistory.pop();
462
+ }
463
+
464
+ conversationHistory.push({ role, content });
465
+
466
+ if (conversationHistory.length > 6) conversationHistory.splice(0, 2);
467
+ };
468
+
469
+ // Process the final speech transcript
470
+ const processSpeechTranscript = (transcript) => {
471
+ const trimmedTranscript = transcript.trimStart();
472
+ if (trimmedTranscript !== '' && !isRequestInProgress) {
473
+ activeQuery = trimmedTranscript;
474
+ sendQueryToAI(activeQuery);
475
+ addToConversationHistory('user', activeQuery);
476
+ }
477
+ };
478
+
479
+ // Check if audio playback should be interrupted
480
+ const shouldInterruptAudioPlayback = (interimTranscript) =>
481
+ Date.now() - lastUserSpeechTimestamp > USER_SPEECH_INTERRUPT_DELAY || interimTranscript.length > 5;
482
+
483
+ // Interrupt audio playback
484
+ const interruptAudioPlayback = (reason = 'unknown') => {
485
+ console.log(`Interrupting audio (reason: ${reason})...`);
486
+ if (currentAudio) {
487
+ currentAudio.pause();
488
+ currentAudio.currentTime = 0;
489
+ currentAudio = null;
490
+ }
491
+
492
+ audioPlaybackQueue.length = 0;
493
+ isRequestInProgress = false;
494
+
495
+ if (requestAbortController) {
496
+ requestAbortController.abort();
497
+ requestAbortController = null;
498
+ }
499
+
500
+ prefetchCache.clear();
501
+ prefetchQueue.length = 0;
502
+ updateActivityIndicators();
503
+ };
504
+
505
+ // Cancel pending prefetch requests
506
+ const cancelPrefetchRequests = (query) => {
507
+ const normalizedQuery = normalizeQueryText(query);
508
+
509
+ for (const [cacheKey, abortController] of pendingPrefetchRequests) {
510
+ if (cacheKey.startsWith(normalizedQuery)) {
511
+ abortController.abort();
512
+ pendingPrefetchRequests.delete(cacheKey);pendingPrefetchRequests.delete(cacheKey);
513
+ }
514
+ }
515
+ };
516
+
517
+ // Update latency display
518
+ const updateLatency = () => {
519
+ if (firstResponseTextTimestamp) {
520
+ const latency = firstResponseTextTimestamp - queryStartTime;
521
+ responseTimeDisplay.textContent = `Latency: ${latency}ms`;
522
+ } else {
523
+ responseTimeDisplay.textContent = "Latency: 0ms";
524
+ }
525
+ };
526
+
527
+ setInterval(updateLatency, 200);