Pepguy commited on
Commit
3e1a9ed
·
verified ·
1 Parent(s): 3de17d8

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +74 -918
index.html CHANGED
@@ -3,932 +3,88 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Firebase Gemini Live API Chat</title>
7
  <style>
8
- * {
9
- margin: 0;
10
- padding: 0;
11
- box-sizing: border-box;
12
- }
13
-
14
- body {
15
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
16
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
- min-height: 100vh;
18
- display: flex;
19
- justify-content: center;
20
- align-items: center;
21
- padding: 20px;
22
- }
23
-
24
- .container {
25
- width: 100%;
26
- max-width: 800px;
27
- background: white;
28
- border-radius: 20px;
29
- box-shadow: 0 20px 60px rgba(0,0,0,0.3);
30
- overflow: hidden;
31
- display: flex;
32
- flex-direction: column;
33
- height: 90vh;
34
- max-height: 700px;
35
- }
36
-
37
- .header {
38
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
39
- color: white;
40
- padding: 20px;
41
- text-align: center;
42
- }
43
-
44
- .header h1 {
45
- font-size: 24px;
46
- margin-bottom: 5px;
47
- }
48
-
49
- .status {
50
- font-size: 14px;
51
- opacity: 0.9;
52
- display: flex;
53
- align-items: center;
54
- justify-content: center;
55
- gap: 8px;
56
- }
57
-
58
- .status-dot {
59
- width: 8px;
60
- height: 8px;
61
- border-radius: 50%;
62
- background: #ff4444;
63
- animation: pulse 2s infinite;
64
- }
65
-
66
- .status-dot.connected {
67
- background: #44ff44;
68
- }
69
-
70
- @keyframes pulse {
71
- 0%, 100% { opacity: 1; }
72
- 50% { opacity: 0.5; }
73
- }
74
-
75
- .config-panel {
76
- background: #f8f9fa;
77
- padding: 15px;
78
- border-bottom: 1px solid #e0e0e0;
79
- }
80
-
81
- .config-row {
82
- display: flex;
83
- gap: 10px;
84
- margin-bottom: 10px;
85
- }
86
-
87
- .config-row input,
88
- .config-row select,
89
- .config-row button {
90
- padding: 8px 12px;
91
- border: 1px solid #ddd;
92
- border-radius: 8px;
93
- font-size: 14px;
94
- }
95
-
96
- .config-row input {
97
- flex: 1;
98
- }
99
-
100
- .config-row button {
101
- background: #667eea;
102
- color: white;
103
- border: none;
104
- cursor: pointer;
105
- font-weight: 600;
106
- transition: background 0.3s;
107
- }
108
-
109
- .config-row button:hover {
110
- background: #5568d3;
111
- }
112
-
113
- .config-row button:disabled {
114
- background: #ccc;
115
- cursor: not-allowed;
116
- }
117
-
118
- .mode-toggle {
119
- display: flex;
120
- gap: 5px;
121
- }
122
-
123
- .mode-toggle button {
124
- flex: 1;
125
- padding: 8px;
126
- border: 2px solid #667eea;
127
- background: white;
128
- color: #667eea;
129
- border-radius: 8px;
130
- cursor: pointer;
131
- font-size: 11px;
132
- font-weight: 600;
133
- transition: all 0.3s;
134
- }
135
-
136
- .mode-toggle button.active {
137
- background: #667eea;
138
- color: white;
139
- }
140
-
141
- .chat-messages {
142
- flex: 1;
143
- overflow-y: auto;
144
- padding: 20px;
145
- background: #f8f9fa;
146
- }
147
-
148
- .message {
149
- margin-bottom: 15px;
150
- display: flex;
151
- animation: slideIn 0.3s ease-out;
152
- }
153
-
154
- @keyframes slideIn {
155
- from {
156
- opacity: 0;
157
- transform: translateY(10px);
158
- }
159
- to {
160
- opacity: 1;
161
- transform: translateY(0);
162
- }
163
- }
164
-
165
- .message.user {
166
- justify-content: flex-end;
167
- }
168
-
169
- .message-content {
170
- max-width: 70%;
171
- padding: 12px 16px;
172
- border-radius: 18px;
173
- word-wrap: break-word;
174
- }
175
-
176
- .message.user .message-content {
177
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
178
- color: white;
179
- border-bottom-right-radius: 4px;
180
- }
181
-
182
- .message.assistant .message-content {
183
- background: white;
184
- color: #333;
185
- border-bottom-left-radius: 4px;
186
- box-shadow: 0 2px 5px rgba(0,0,0,0.1);
187
- }
188
-
189
- .message.system .message-content {
190
- background: #fff3cd;
191
- color: #856404;
192
- max-width: 100%;
193
- text-align: center;
194
- font-size: 13px;
195
- }
196
-
197
- .input-area {
198
- padding: 20px;
199
- background: white;
200
- border-top: 1px solid #e0e0e0;
201
- }
202
-
203
- .input-row {
204
- display: flex;
205
- gap: 10px;
206
- align-items: center;
207
- }
208
-
209
- .input-row input {
210
- flex: 1;
211
- padding: 12px 16px;
212
- border: 2px solid #e0e0e0;
213
- border-radius: 25px;
214
- font-size: 14px;
215
- outline: none;
216
- transition: border-color 0.3s;
217
- }
218
-
219
- .input-row input:focus {
220
- border-color: #667eea;
221
- }
222
-
223
- .btn {
224
- padding: 12px 24px;
225
- border: none;
226
- border-radius: 25px;
227
- font-size: 14px;
228
- font-weight: 600;
229
- cursor: pointer;
230
- transition: all 0.3s;
231
- display: flex;
232
- align-items: center;
233
- gap: 8px;
234
- }
235
-
236
- .btn-primary {
237
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
238
- color: white;
239
- }
240
-
241
- .btn-primary:hover:not(:disabled) {
242
- transform: translateY(-2px);
243
- box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
244
- }
245
-
246
- .btn-secondary {
247
- background: #6c757d;
248
- color: white;
249
- }
250
-
251
- .btn-secondary:hover:not(:disabled) {
252
- background: #5a6268;
253
- }
254
-
255
- .btn-voice {
256
- width: 50px;
257
- height: 50px;
258
- border-radius: 50%;
259
- padding: 0;
260
- display: flex;
261
- align-items: center;
262
- justify-content: center;
263
- }
264
-
265
- .btn-voice.recording {
266
- background: #ff4444;
267
- animation: recordPulse 1s infinite;
268
- }
269
-
270
- @keyframes recordPulse {
271
- 0%, 100% { transform: scale(1); }
272
- 50% { transform: scale(1.1); }
273
- }
274
-
275
- .btn:disabled {
276
- opacity: 0.5;
277
- cursor: not-allowed;
278
- }
279
-
280
- .typing-indicator {
281
- display: flex;
282
- gap: 4px;
283
- padding: 12px 16px;
284
- }
285
-
286
- .typing-dot {
287
- width: 8px;
288
- height: 8px;
289
- border-radius: 50%;
290
- background: #999;
291
- animation: typing 1.4s infinite;
292
- }
293
-
294
- .typing-dot:nth-child(2) {
295
- animation-delay: 0.2s;
296
- }
297
-
298
- .typing-dot:nth-child(3) {
299
- animation-delay: 0.4s;
300
- }
301
-
302
- @keyframes typing {
303
- 0%, 60%, 100% {
304
- transform: translateY(0);
305
- }
306
- 30% {
307
- transform: translateY(-10px);
308
- }
309
- }
310
-
311
- .audio-visualizer {
312
- display: flex;
313
- gap: 3px;
314
- align-items: center;
315
- justify-content: center;
316
- height: 30px;
317
- }
318
-
319
- .visualizer-bar {
320
- width: 3px;
321
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
322
- border-radius: 2px;
323
- animation: visualize 0.5s ease-in-out infinite alternate;
324
- }
325
-
326
- .visualizer-bar:nth-child(1) { animation-delay: 0s; }
327
- .visualizer-bar:nth-child(2) { animation-delay: 0.1s; }
328
- .visualizer-bar:nth-child(3) { animation-delay: 0.2s; }
329
- .visualizer-bar:nth-child(4) { animation-delay: 0.3s; }
330
- .visualizer-bar:nth-child(5) { animation-delay: 0.2s; }
331
- .visualizer-bar:nth-child(6) { animation-delay: 0.1s; }
332
-
333
- @keyframes visualize {
334
- 0% { height: 5px; }
335
- 100% { height: 25px; }
336
- }
337
  </style>
338
  </head>
339
  <body>
340
- <div class="container">
341
- <div class="header">
342
- <h1>🤖 Gemini Live Chat</h1>
343
- <div class="status">
344
- <div class="status-dot" id="statusDot"></div>
345
- <span id="statusText">Disconnected</span>
346
- </div>
347
- </div>
348
-
349
- <div class="config-panel">
350
- <div class="config-row">
351
- <input type="text" id="apiKey" placeholder="Enter your Firebase API Key" />
352
- <input type="text" id="projectId" placeholder="Firebase Project ID" />
353
- <button id="connectBtn" onclick="connect()">Connect</button>
354
- </div>
355
- <div class="config-row">
356
- <select id="modelSelect">
357
- <option value="gemini-2.0-flash-live-preview-04-09">gemini-2.0-flash-live-preview-04-09</option>
358
- <option value="gemini-live-2.5-flash">gemini-live-2.5-flash</option>
359
- </select>
360
- <div class="mode-toggle">
361
- <button class="active" onclick="setMode('text')">Text → Text</button>
362
- <button onclick="setMode('audio-audio')">Audio → Audio</button>
363
- <button onclick="setMode('speech-audio')">Speech → Audio</button>
364
- </div>
365
- </div>
366
- </div>
367
-
368
- <div class="chat-messages" id="chatMessages">
369
- <div class="message system">
370
- <div class="message-content">
371
- 👋 Welcome! Configure your Firebase credentials and click Connect to start chatting.
372
- <br><br>
373
- <strong>Modes:</strong><br>
374
- • Text → Text: Type and receive text<br>
375
- • Audio → Audio: TTS input, Gemini audio output<br>
376
- • Speech → Audio: Browser speech recognition, Gemini audio output
377
- </div>
378
- </div>
379
- </div>
380
-
381
- <div class="input-area">
382
- <div class="input-row" id="textInputRow">
383
- <input
384
- type="text"
385
- id="messageInput"
386
- placeholder="Type your message..."
387
- disabled
388
- onkeypress="if(event.key === 'Enter') sendMessage()"
389
- />
390
- <button class="btn btn-primary" id="sendBtn" onclick="sendMessage()" disabled>
391
- Send
392
- </button>
393
- <button class="btn btn-secondary" id="disconnectBtn" onclick="disconnect()" disabled>
394
- Disconnect
395
- </button>
396
- </div>
397
- <div class="input-row" id="voiceInputRow" style="display: none;">
398
- <button class="btn btn-voice btn-primary" id="voiceBtn" onclick="toggleVoiceInput()" disabled>
399
- 🎤
400
- </button>
401
- <div id="audioVisualizer" style="display: none;">
402
- <div class="audio-visualizer">
403
- <div class="visualizer-bar"></div>
404
- <div class="visualizer-bar"></div>
405
- <div class="visualizer-bar"></div>
406
- <div class="visualizer-bar"></div>
407
- <div class="visualizer-bar"></div>
408
- <div class="visualizer-bar"></div>
409
- </div>
410
- </div>
411
- <button class="btn btn-secondary" id="disconnectBtn2" onclick="disconnect()" disabled>
412
- Disconnect
413
- </button>
414
- </div>
415
- </div>
416
- </div>
417
-
418
- <script type="module">
419
- import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-app.js';
420
- import { getAI, getLiveGenerativeModel } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-ai-logic.js';
421
- import { VertexAIBackend } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-ai-logic.js';
422
-
423
- let firebaseApp = null;
424
- let session = null;
425
- let currentMode = 'text';
426
- let isReceiving = false;
427
- let recognition = null;
428
- let audioContext = null;
429
- let isRecording = false;
430
- let mediaRecorder = null;
431
- let audioChunks = [];
432
-
433
- // Make functions globally available
434
- window.connect = connect;
435
- window.disconnect = disconnect;
436
- window.sendMessage = sendMessage;
437
- window.setMode = setMode;
438
- window.toggleVoiceInput = toggleVoiceInput;
439
-
440
- const ResponseModality = {
441
- TEXT: 'text',
442
- AUDIO: 'audio'
443
- };
444
-
445
- // Initialize Web Speech API
446
- if ('webkitSpeechRecognition' in window) {
447
- recognition = new webkitSpeechRecognition();
448
- recognition.continuous = false;
449
- recognition.interimResults = false;
450
- recognition.lang = 'en-US';
451
-
452
- recognition.onresult = (event) => {
453
- const transcript = event.results[0][0].transcript;
454
- addMessage('user', transcript);
455
- sendTextMessage(transcript);
456
- };
457
-
458
- recognition.onerror = (event) => {
459
- console.error('Speech recognition error:', event.error);
460
- addMessage('system', `❌ Speech recognition error: ${event.error}`);
461
- stopRecording();
462
- };
463
 
464
- recognition.onend = () => {
465
- stopRecording();
466
- };
467
- }
468
 
469
- async function connect() {
470
- /* const apiKey = document.getElementById('apiKey').value.trim();
471
- const projectId = document.getElementById('projectId').value.trim();
472
- const modelName = document.getElementById('modelSelect').value;
473
 
474
- if (!apiKey || !projectId) {
475
- addMessage('system', 'Please enter both API Key and Project ID');
476
- return;
477
- }
478
- */
479
 
480
- try {
481
- document.getElementById('connectBtn').disabled = true;
482
- addMessage('system', 'Connecting to Firebase...');
483
 
484
- const firebaseConfig = {
485
- apiKey: "AIzaSyB6EeMqhn5kvMEDYRzhtdokA9i0idKtUKo",
486
- authDomain: "winternut-pay.firebaseapp.com",
487
- projectId: "winternut-pay",
488
- storageBucket: "winternut-pay.firebasestorage.app",
489
- messagingSenderId: "564557307443",
490
- appId: "1:564557307443:web:a1abf6b0db99ca79560d46",
491
- measurementId: "G-K07BVBE7FW"
492
- };
493
-
494
- firebaseApp = initializeApp(firebaseConfig);
495
- const ai = getAI(firebaseApp, {
496
- backend: new VertexAIBackend("us-central1")
497
- });
498
-
499
- const useAudio = currentMode !== 'text';
500
- const model = getLiveGenerativeModel(ai, {
501
- model: modelName,
502
- generationConfig: {
503
- responseModalities: [useAudio ? ResponseModality.AUDIO : ResponseModality.TEXT],
504
- }
505
- });
506
-
507
- session = await model.connect();
508
-
509
- updateStatus(true);
510
- addMessage('system', `✅ Connected in ${currentMode} mode!`);
511
-
512
- // Enable appropriate inputs
513
- if (currentMode === 'text') {
514
- document.getElementById('messageInput').disabled = false;
515
- document.getElementById('sendBtn').disabled = false;
516
- } else {
517
- document.getElementById('voiceBtn').disabled = false;
518
- }
519
-
520
- document.getElementById('disconnectBtn').disabled = false;
521
- document.getElementById('disconnectBtn2').disabled = false;
522
- document.getElementById('connectBtn').disabled = true;
523
-
524
- // Initialize audio context for audio playback
525
- if (useAudio) {
526
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
527
- }
528
-
529
- startReceiving();
530
-
531
- } catch (error) {
532
- console.error('Connection error:', error);
533
- addMessage('system', `❌ Connection failed: ${error.message}`);
534
- document.getElementById('connectBtn').disabled = false;
535
- }
536
- }
537
-
538
- async function disconnect() {
539
- if (session) {
540
- try {
541
- await session.close();
542
- } catch (e) {
543
- console.error('Error closing session:', e);
544
- }
545
- session = null;
546
- }
547
-
548
- if (audioContext) {
549
- audioContext.close();
550
- audioContext = null;
551
- }
552
-
553
- updateStatus(false);
554
- addMessage('system', 'Disconnected from server');
555
-
556
- document.getElementById('messageInput').disabled = true;
557
- document.getElementById('sendBtn').disabled = true;
558
- document.getElementById('voiceBtn').disabled = true;
559
- document.getElementById('disconnectBtn').disabled = true;
560
- document.getElementById('disconnectBtn2').disabled = true;
561
- document.getElementById('connectBtn').disabled = false;
562
- }
563
-
564
- async function sendMessage() {
565
- const input = document.getElementById('messageInput');
566
- const message = input.value.trim();
567
-
568
- if (!message || !session) return;
569
-
570
- addMessage('user', message);
571
- input.value = '';
572
-
573
- await sendTextMessage(message);
574
- }
575
-
576
- async function sendTextMessage(text) {
577
- showTypingIndicator();
578
-
579
- try {
580
- // For audio-audio mode, use TTS to convert text to audio
581
- if (currentMode === 'audio-audio') {
582
- const audioData = await textToSpeech(text);
583
- await session.send([{
584
- inlineData: {
585
- data: audioData,
586
- mimeType: "audio/pcm"
587
- }
588
- }]);
589
- } else {
590
- await session.send(text);
591
- }
592
- } catch (error) {
593
- console.error('Send error:', error);
594
- hideTypingIndicator();
595
- addMessage('system', `❌ Error sending message: ${error.message}`);
596
- }
597
- }
598
-
599
- async function textToSpeech(text) {
600
- return new Promise((resolve, reject) => {
601
- const utterance = new SpeechSynthesisUtterance(text);
602
-
603
- // Create an audio context to capture the speech
604
- const tempAudioContext = new AudioContext({ sampleRate: 16000 });
605
- const destination = tempAudioContext.createMediaStreamDestination();
606
-
607
- utterance.onend = () => {
608
- // This is a simplified version - in production, you'd need to
609
- // properly capture and convert the audio to PCM format
610
- const buffer = new ArrayBuffer(text.length * 32);
611
- const view = new Uint8Array(buffer);
612
-
613
- // Convert to base64
614
- const base64 = btoa(String.fromCharCode(...view));
615
- resolve(base64);
616
- };
617
-
618
- utterance.onerror = (error) => {
619
- reject(error);
620
- };
621
-
622
- speechSynthesis.speak(utterance);
623
- });
624
- }
625
-
626
- function toggleVoiceInput() {
627
- if (isRecording) {
628
- stopRecording();
629
- } else {
630
- startRecording();
631
- }
632
- }
633
-
634
- function startRecording() {
635
- const voiceBtn = document.getElementById('voiceBtn');
636
- const visualizer = document.getElementById('audioVisualizer');
637
-
638
- if (currentMode === 'speech-audio') {
639
- // Use browser speech recognition
640
- if (recognition) {
641
- recognition.start();
642
- voiceBtn.classList.add('recording');
643
- voiceBtn.textContent = '⏹️';
644
- visualizer.style.display = 'block';
645
- isRecording = true;
646
- addMessage('system', '🎤 Listening...');
647
- } else {
648
- addMessage('system', '❌ Speech recognition not supported in this browser');
649
- }
650
- } else if (currentMode === 'audio-audio') {
651
- // Use microphone for audio recording
652
- navigator.mediaDevices.getUserMedia({ audio: true })
653
- .then(stream => {
654
- mediaRecorder = new MediaRecorder(stream);
655
- audioChunks = [];
656
-
657
- mediaRecorder.ondataavailable = (event) => {
658
- audioChunks.push(event.data);
659
- };
660
-
661
- mediaRecorder.onstop = async () => {
662
- const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
663
- await processAudioBlob(audioBlob);
664
- stream.getTracks().forEach(track => track.stop());
665
- };
666
-
667
- mediaRecorder.start();
668
- voiceBtn.classList.add('recording');
669
- voiceBtn.textContent = '⏹️';
670
- visualizer.style.display = 'block';
671
- isRecording = true;
672
- addMessage('system', '🎤 Recording audio...');
673
- })
674
- .catch(error => {
675
- console.error('Microphone error:', error);
676
- addMessage('system', '❌ Microphone access denied');
677
- });
678
- }
679
- }
680
-
681
- function stopRecording() {
682
- const voiceBtn = document.getElementById('voiceBtn');
683
- const visualizer = document.getElementById('audioVisualizer');
684
-
685
- if (currentMode === 'speech-audio' && recognition) {
686
- recognition.stop();
687
- } else if (currentMode === 'audio-audio' && mediaRecorder) {
688
- mediaRecorder.stop();
689
- }
690
-
691
- voiceBtn.classList.remove('recording');
692
- voiceBtn.textContent = '🎤';
693
- visualizer.style.display = 'none';
694
- isRecording = false;
695
- }
696
-
697
- async function processAudioBlob(blob) {
698
- try {
699
- const arrayBuffer = await blob.arrayBuffer();
700
- const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
701
-
702
- // Convert to PCM 16-bit 16kHz
703
- const pcmData = convertToPCM(audioBuffer);
704
- const base64Data = arrayBufferToBase64(pcmData);
705
-
706
- addMessage('user', '🎤 [Audio message]');
707
- showTypingIndicator();
708
-
709
- await session.send([{
710
- inlineData: {
711
- data: base64Data,
712
- mimeType: "audio/pcm"
713
- }
714
- }]);
715
- } catch (error) {
716
- console.error('Audio processing error:', error);
717
- addMessage('system', `❌ Error processing audio: ${error.message}`);
718
- }
719
- }
720
-
721
- function convertToPCM(audioBuffer) {
722
- const targetSampleRate = 16000;
723
- const channelData = audioBuffer.getChannelData(0);
724
- const samples = resampleAudio(channelData, audioBuffer.sampleRate, targetSampleRate);
725
-
726
- const pcm = new Int16Array(samples.length);
727
- for (let i = 0; i < samples.length; i++) {
728
- const s = Math.max(-1, Math.min(1, samples[i]));
729
- pcm[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
730
- }
731
-
732
- return pcm.buffer;
733
- }
734
-
735
- function resampleAudio(samples, fromRate, toRate) {
736
- const ratio = fromRate / toRate;
737
- const newLength = Math.round(samples.length / ratio);
738
- const result = new Float32Array(newLength);
739
-
740
- for (let i = 0; i < newLength; i++) {
741
- const srcIndex = i * ratio;
742
- const srcIndexFloor = Math.floor(srcIndex);
743
- const srcIndexCeil = Math.min(srcIndexFloor + 1, samples.length - 1);
744
- const t = srcIndex - srcIndexFloor;
745
-
746
- result[i] = samples[srcIndexFloor] * (1 - t) + samples[srcIndexCeil] * t;
747
- }
748
-
749
- return result;
750
- }
751
-
752
- function arrayBufferToBase64(buffer) {
753
- const bytes = new Uint8Array(buffer);
754
- let binary = '';
755
- for (let i = 0; i < bytes.byteLength; i++) {
756
- binary += String.fromCharCode(bytes[i]);
757
- }
758
- return btoa(binary);
759
- }
760
-
761
- async function startReceiving() {
762
- if (isReceiving || !session) return;
763
-
764
- isReceiving = true;
765
- let currentText = '';
766
- let audioQueue = [];
767
-
768
- try {
769
- const messages = session.receive();
770
-
771
- for await (const message of messages) {
772
- hideTypingIndicator();
773
-
774
- switch (message.type) {
775
- case 'serverContent':
776
- if (message.turnComplete) {
777
- if (currentText) {
778
- addMessage('assistant', currentText);
779
- currentText = '';
780
- }
781
- if (audioQueue.length > 0) {
782
- await playAudioChunks(audioQueue);
783
- audioQueue = [];
784
- }
785
- } else if (message.modelTurn) {
786
- const parts = message.modelTurn?.parts;
787
- if (parts) {
788
- parts.forEach(part => {
789
- if (part.text) {
790
- currentText += part.text;
791
- } else if (part.inlineData) {
792
- audioQueue.push(part.inlineData.data);
793
- }
794
- });
795
- }
796
- }
797
- break;
798
-
799
- case 'toolCall':
800
- addMessage('system', '🔧 Model is calling a tool...');
801
- break;
802
-
803
- case 'toolCallCancellation':
804
- addMessage('system', '⚠️ Tool call was cancelled');
805
- break;
806
- }
807
- }
808
- } catch (error) {
809
- console.error('Receive error:', error);
810
- addMessage('system', `❌ Error receiving messages: ${error.message}`);
811
- } finally {
812
- isReceiving = false;
813
- }
814
- }
815
-
816
- async function playAudioChunks(chunks) {
817
- if (!audioContext || chunks.length === 0) return;
818
-
819
- addMessage('assistant', '🔊 [Audio response]');
820
-
821
- for (const base64Data of chunks) {
822
- try {
823
- const binaryData = atob(base64Data);
824
- const arrayBuffer = new ArrayBuffer(binaryData.length);
825
- const view = new Uint8Array(arrayBuffer);
826
-
827
- for (let i = 0; i < binaryData.length; i++) {
828
- view[i] = binaryData.charCodeAt(i);
829
- }
830
-
831
- // Convert PCM to AudioBuffer
832
- const pcmData = new Int16Array(arrayBuffer);
833
- const floatData = new Float32Array(pcmData.length);
834
-
835
- for (let i = 0; i < pcmData.length; i++) {
836
- floatData[i] = pcmData[i] / 32768.0;
837
- }
838
-
839
- const audioBuffer = audioContext.createBuffer(1, floatData.length, 24000);
840
- audioBuffer.getChannelData(0).set(floatData);
841
-
842
- const source = audioContext.createBufferSource();
843
- source.buffer = audioBuffer;
844
- source.connect(audioContext.destination);
845
-
846
- await new Promise((resolve) => {
847
- source.onended = resolve;
848
- source.start();
849
- });
850
- } catch (error) {
851
- console.error('Audio playback error:', error);
852
- }
853
- }
854
- }
855
-
856
- function setMode(mode) {
857
- currentMode = mode;
858
-
859
- const buttons = document.querySelectorAll('.mode-toggle button');
860
- buttons.forEach(btn => {
861
- btn.classList.remove('active');
862
- });
863
- event.target.classList.add('active');
864
-
865
- // Show/hide appropriate input areas
866
- const textInputRow = document.getElementById('textInputRow');
867
- const voiceInputRow = document.getElementById('voiceInputRow');
868
-
869
- if (mode === 'text') {
870
- textInputRow.style.display = 'flex';
871
- voiceInputRow.style.display = 'none';
872
- } else {
873
- textInputRow.style.display = 'none';
874
- voiceInputRow.style.display = 'flex';
875
- }
876
-
877
- if (session) {
878
- addMessage('system', `⚠️ Mode changed to ${mode}. Please disconnect and reconnect to apply changes.`);
879
- }
880
- }
881
-
882
- function addMessage(type, content) {
883
- const messagesDiv = document.getElementById('chatMessages');
884
- const messageDiv = document.createElement('div');
885
- messageDiv.className = `message ${type}`;
886
-
887
- const contentDiv = document.createElement('div');
888
- contentDiv.className = 'message-content';
889
- contentDiv.textContent = content;
890
-
891
- messageDiv.appendChild(contentDiv);
892
- messagesDiv.appendChild(messageDiv);
893
- messagesDiv.scrollTop = messagesDiv.scrollHeight;
894
- }
895
-
896
- function showTypingIndicator() {
897
- hideTypingIndicator();
898
-
899
- const messagesDiv = document.getElementById('chatMessages');
900
- const indicatorDiv = document.createElement('div');
901
- indicatorDiv.className = 'message assistant';
902
- indicatorDiv.id = 'typingIndicator';
903
-
904
- const contentDiv = document.createElement('div');
905
- contentDiv.className = 'message-content typing-indicator';
906
- contentDiv.innerHTML = '<div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div>';
907
-
908
- indicatorDiv.appendChild(contentDiv);
909
- messagesDiv.appendChild(indicatorDiv);
910
- messagesDiv.scrollTop = messagesDiv.scrollHeight;
911
- }
912
-
913
- function hideTypingIndicator() {
914
- const indicator = document.getElementById('typingIndicator');
915
- if (indicator) {
916
- indicator.remove();
917
- }
918
- }
919
-
920
- function updateStatus(connected) {
921
- const statusDot = document.getElementById('statusDot');
922
- const statusText = document.getElementById('statusText');
923
-
924
- if (connected) {
925
- statusDot.classList.add('connected');
926
- statusText.textContent = 'Connected';
927
- } else {
928
- statusDot.classList.remove('connected');
929
- statusText.textContent = 'Disconnected';
930
- }
931
- }
932
  </script>
 
933
  </body>
934
- </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Gemini Audio Generator</title>
7
  <style>
8
+ body { font-family: sans-serif; max-width: 600px; margin: 20px auto; padding: 20px; }
9
+ h1 { text-align: center; }
10
+ textarea { width: 100%; height: 150px; margin-bottom: 10px; padding: 10px; box-sizing: border-box; }
11
+ button { display: block; width: 100%; padding: 10px; font-size: 16px; cursor: pointer; }
12
+ #audio-output { margin-top: 20px; text-align: center; }
13
+ #loading { display: none; text-align: center; margin-top: 10px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  </style>
15
  </head>
16
  <body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
+ <h1>Gemini TTS Web App</h1>
 
 
 
19
 
20
+ <p>Enter text below and click "Generate Audio" to hear it read aloud.</p>
21
+
22
+ <textarea id="text-input" placeholder="Enter your text here..."></textarea>
23
+ <button id="generate-button">Generate Audio</button>
24
 
25
+ <div id="loading">Generating audio...</div>
 
 
 
 
26
 
27
+ <div id="audio-output"></div>
 
 
28
 
29
+ <script type="module">
30
+ import * as GoogleGenAI from 'https://cdn.jsdelivr.net/npm/@google/genai@1.21.0/+esm'
31
+
32
+
33
+ // For development only: Replace with your Gemini API key
34
+ // In a production environment, use a backend server to prevent exposing your key.
35
+ const API_KEY = "AIzaSyDtmPxzbYrXtzt0AHQagk0Y-DG57jUnpk0";
36
+
37
+ const ai = new GoogleGenAI({ apiKey: API_KEY });
38
+
39
+ const textInput = document.getElementById("text-input");
40
+ const generateButton = document.getElementById("generate-button");
41
+ const audioOutputDiv = document.getElementById("audio-output");
42
+ const loadingIndicator = document.getElementById("loading");
43
+
44
+ generateButton.addEventListener("click", async () => {
45
+ const text = textInput.value;
46
+ if (!text) {
47
+ alert("Please enter some text.");
48
+ return;
49
+ }
50
+
51
+ loadingIndicator.style.display = "block";
52
+ generateButton.disabled = true;
53
+ audioOutputDiv.innerHTML = ""; // Clear previous audio
54
+
55
+ try {
56
+ // Use the gemini-2.5-flash-tts model for text-to-speech
57
+ const model = ai.getGenerativeModel({
58
+ model: "gemini-2.5-flash-tts"
59
+ });
60
+
61
+ // Generate audio from text
62
+ const response = await model.generateContent({
63
+ contents: [{ parts: [{ text: text }] }]
64
+ });
65
+
66
+ const audioResponse = response.audio;
67
+ const audioBlob = new Blob([audioResponse.data], { type: audioResponse.mimeType });
68
+ const audioUrl = URL.createObjectURL(audioBlob);
69
+
70
+ // Create and play an audio element
71
+ const audioElement = document.createElement("audio");
72
+ audioElement.controls = true;
73
+ audioElement.src = audioUrl;
74
+ audioOutputDiv.appendChild(audioElement);
75
+ audioElement.play();
76
+
77
+ } catch (error) {
78
+ console.error("Error generating audio:", error);
79
+ audioOutputDiv.innerHTML = `<p>Error: ${error.message}</p>`;
80
+ } finally {
81
+ loadingIndicator.style.display = "none";
82
+ generateButton.disabled = false;
83
+ }
84
+ });
85
+
86
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  </script>
88
+
89
  </body>
90
+ </html>