| | <!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> |