| <!DOCTYPE html> |
| <html lang="fa" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <title>Gemini Real-time TTS</title> |
| <style> |
| body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: #f0f2f5; margin: 0; padding: 20px; display: flex; justify-content: center; align-items: center; min-height: 100vh; } |
| .container { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); width: 100%; max-width: 600px; } |
| h1 { text-align: center; color: #333; } |
| textarea { width: 100%; padding: 10px; font-size: 16px; border-radius: 5px; border: 1px solid #ccc; margin-bottom: 15px; box-sizing: border-box; resize: vertical; } |
| .button-container { display: flex; gap: 10px; } |
| button { flex-grow: 1; padding: 12px; font-size: 18px; border: none; border-radius: 5px; color: white; cursor: pointer; transition: background-color 0.2s; } |
| #speak-button { background-color: #007bff; } |
| #stop-button { background-color: #dc3545; } |
| button:disabled { background-color: #a0cfff; cursor: not-allowed; } |
| #stop-button:disabled { background-color: #f5c6cb; } |
| #status { margin-top: 15px; text-align: center; color: #555; font-style: italic; } |
| #audio-player-container { margin-top: 20px; } |
| audio { width: 100%; } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <h1>🎙️ پخش صدای آنی Gemini</h1> |
| <textarea id="text-input" rows="4" placeholder="متن خود را اینجا وارد کنید..."></textarea> |
| <div class="button-container"> |
| <button id="speak-button">صحبت کن</button> |
| <button id="stop-button" disabled>توقف</button> |
| </div> |
| <div id="status">در حال اتصال به سرور...</div> |
| <div id="audio-player-container" style="display: none;"> |
| <p>پخش مجدد:</p> |
| <audio id="audio-player" controls></audio> |
| </div> |
| </div> |
|
|
| <script> |
| const textInput = document.getElementById('text-input'); |
| const speakButton = document.getElementById('speak-button'); |
| const stopButton = document.getElementById('stop-button'); |
| const statusDiv = document.getElementById('status'); |
| const audioPlayerContainer = document.getElementById('audio-player-container'); |
| const audioPlayer = document.getElementById('audio-player'); |
| |
| let audioContext; |
| let audioQueue = []; |
| let sourceNodes = []; |
| let isPlaying = false; |
| let isStopped = false; |
| let nextStartTime = 0; |
| let socket; |
| |
| function initializeAudio() { |
| if (!audioContext || audioContext.state === 'suspended') { |
| audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 24000 }); |
| } |
| nextStartTime = audioContext.currentTime; |
| } |
| |
| function getWebSocketURL() { |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; |
| return `${protocol}//${window.location.host}/ws`; |
| } |
| |
| function connectWebSocket() { |
| const wsURL = getWebSocketURL(); |
| socket = new WebSocket(wsURL); |
| |
| socket.onopen = () => { |
| statusDiv.textContent = "آماده دریافت متن"; |
| speakButton.disabled = false; |
| }; |
| |
| socket.onmessage = async (event) => { |
| if (typeof event.data === 'string') { |
| const message = JSON.parse(event.data); |
| if (message.event === "STREAM_ENDED") { |
| handleStreamEnd(message.url); |
| } else if (message.event === "ERROR") { |
| statusDiv.textContent = `خطا: ${message.message}`; |
| resetUI(); |
| } |
| } else { |
| if (isStopped) return; |
| |
| const arrayBuffer = await event.data.arrayBuffer(); |
| const pcmData = new Int16Array(arrayBuffer); |
| |
| audioQueue.push(pcmData); |
| |
| if (!isPlaying) { |
| playFromQueue(); |
| } |
| } |
| }; |
| |
| socket.onclose = () => { |
| statusDiv.textContent = "اتصال قطع شد. تلاش مجدد..."; |
| resetUI(true); |
| setTimeout(connectWebSocket, 3000); |
| }; |
| } |
| |
| async function playFromQueue() { |
| if (audioQueue.length === 0 || isStopped) { |
| isPlaying = false; |
| return; |
| } |
| |
| isPlaying = true; |
| |
| while (audioQueue.length > 0) { |
| const pcmData = audioQueue.shift(); |
| |
| const float32Data = new Float32Array(pcmData.length); |
| for (let i = 0; i < pcmData.length; i++) { |
| float32Data[i] = pcmData[i] / 32768.0; |
| } |
| |
| const audioBuffer = audioContext.createBuffer(1, float32Data.length, audioContext.sampleRate); |
| audioBuffer.getChannelData(0).set(float32Data); |
| |
| const source = audioContext.createBufferSource(); |
| source.buffer = audioBuffer; |
| source.connect(audioContext.destination); |
| |
| const currentTime = audioContext.currentTime; |
| if (nextStartTime < currentTime) { |
| nextStartTime = currentTime; |
| } |
| |
| source.start(nextStartTime); |
| sourceNodes.push(source); |
| |
| nextStartTime += audioBuffer.duration; |
| } |
| |
| isPlaying = false; |
| } |
| |
| function handleStreamEnd(audioUrl) { |
| if (audioUrl) { |
| audioPlayer.src = audioUrl; |
| audioPlayerContainer.style.display = 'block'; |
| } |
| const checkPlaybackEnd = setInterval(() => { |
| if (audioQueue.length === 0 && audioContext.currentTime > nextStartTime) { |
| if(!isStopped) { |
| statusDiv.textContent = "پخش تمام شد."; |
| resetUI(); |
| } |
| clearInterval(checkPlaybackEnd); |
| } |
| }, 100); |
| } |
| |
| function resetUI(isConnectionError = false) { |
| speakButton.disabled = isConnectionError; |
| stopButton.disabled = true; |
| isPlaying = false; |
| } |
| |
| speakButton.addEventListener('click', () => { |
| const text = textInput.value.trim(); |
| if (!text || !socket || socket.readyState !== WebSocket.OPEN) return; |
| |
| initializeAudio(); |
| isStopped = false; |
| |
| audioQueue = []; |
| sourceNodes.forEach(source => source.stop()); |
| sourceNodes = []; |
| |
| socket.send(text); |
| |
| speakButton.disabled = true; |
| stopButton.disabled = false; |
| statusDiv.textContent = "در حال دریافت و پخش صدا..."; |
| |
| audioPlayerContainer.style.display = 'none'; |
| audioPlayer.src = ""; |
| }); |
| |
| stopButton.addEventListener('click', () => { |
| isStopped = true; |
| audioQueue = []; |
| sourceNodes.forEach(source => source.stop()); |
| sourceNodes = []; |
| statusDiv.textContent = "پخش متوقف شد."; |
| resetUI(); |
| }); |
| |
| window.addEventListener('load', connectWebSocket); |
| </script> |
|
|
| </body> |
| </html> |