Spaces:
Runtime error
Runtime error
| {% extends "base.html" %} | |
| {% block title %}{{ _('websocket.title', 'WebSocket Streaming Demo') }} - TTSFM{% endblock %} | |
| {% block content %} | |
| <div class="container mt-5"> | |
| <div class="row"> | |
| <div class="col-lg-10 mx-auto"> | |
| <h1 class="text-center mb-4"> | |
| <i class="fas fa-bolt text-warning"></i> | |
| {{ _('websocket.title', 'WebSocket Streaming Demo') }} | |
| </h1> | |
| <!-- Connection Status --> | |
| <div class="alert alert-info" id="connection-status"> | |
| <i class="fas fa-plug me-2"></i> | |
| <span id="status-text">Connecting to WebSocket server...</span> | |
| </div> | |
| <!-- Input Form --> | |
| <div class="card shadow-sm mb-4"> | |
| <div class="card-body"> | |
| <h5 class="card-title">{{ _('playground.generate_speech', 'Generate Speech') }}</h5> | |
| <form id="streaming-form"> | |
| <div class="mb-3"> | |
| <label for="text-input" class="form-label"> | |
| {{ _('playground.text_input', 'Text to Convert') }} | |
| </label> | |
| <textarea | |
| class="form-control" | |
| id="text-input" | |
| rows="4" | |
| maxlength="4096" | |
| placeholder="{{ _('playground.text_placeholder', 'Enter your text here...') }}" | |
| >Experience the future of text-to-speech with real-time WebSocket streaming! This innovative feature delivers audio chunks as they're generated, providing a more responsive and engaging user experience.</textarea> | |
| <div class="form-text"> | |
| <i class="fas fa-info-circle me-1"></i> | |
| Streaming will split text into chunks for real-time delivery | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <div class="col-md-6 mb-3"> | |
| <label for="voice-select" class="form-label"> | |
| {{ _('playground.voice', 'Voice') }} | |
| </label> | |
| <select class="form-select" id="voice-select"> | |
| <option value="alloy">Alloy</option> | |
| <option value="echo">Echo</option> | |
| <option value="fable">Fable</option> | |
| <option value="onyx">Onyx</option> | |
| <option value="nova">Nova</option> | |
| <option value="shimmer">Shimmer</option> | |
| </select> | |
| </div> | |
| <div class="col-md-6 mb-3"> | |
| <label for="format-select" class="form-label"> | |
| {{ _('playground.format', 'Audio Format') }} | |
| </label> | |
| <select class="form-select" id="format-select"> | |
| <option value="mp3">MP3</option> | |
| <option value="wav">WAV</option> | |
| <option value="opus">OPUS</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="d-grid gap-2 d-md-flex justify-content-md-end"> | |
| <button type="submit" class="btn btn-primary" id="stream-btn"> | |
| <i class="fas fa-bolt me-2"></i> | |
| Start Streaming | |
| </button> | |
| <button type="button" class="btn btn-danger" id="cancel-btn" style="display: none;"> | |
| <i class="fas fa-stop me-2"></i> | |
| Cancel | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| <!-- Progress Section --> | |
| <div class="card shadow-sm mb-4" id="progress-section" style="display: none;"> | |
| <div class="card-body"> | |
| <h5 class="card-title">Streaming Progress</h5> | |
| <div class="progress mb-3" style="height: 25px;"> | |
| <div | |
| class="progress-bar progress-bar-striped progress-bar-animated" | |
| id="progress-bar" | |
| role="progressbar" | |
| style="width: 0%" | |
| > | |
| <span id="progress-text">0%</span> | |
| </div> | |
| </div> | |
| <div class="row text-center"> | |
| <div class="col-md-4"> | |
| <h6>Chunks Received</h6> | |
| <p class="h4"><span id="chunks-received">0</span> / <span id="total-chunks">0</span></p> | |
| </div> | |
| <div class="col-md-4"> | |
| <h6>Data Transferred</h6> | |
| <p class="h4" id="data-transferred">0 KB</p> | |
| </div> | |
| <div class="col-md-4"> | |
| <h6>Generation Time</h6> | |
| <p class="h4" id="generation-time">0.0s</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Audio Chunks Display --> | |
| <div class="card shadow-sm mb-4" id="chunks-section" style="display: none;"> | |
| <div class="card-body"> | |
| <h5 class="card-title">Audio Chunks</h5> | |
| <div id="chunks-container" class="row g-2"> | |
| <!-- Chunks will be added here dynamically --> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Final Audio Player --> | |
| <div class="card shadow-sm" id="audio-section" style="display: none;"> | |
| <div class="card-body"> | |
| <h5 class="card-title">Generated Audio</h5> | |
| <audio id="audio-player" controls class="w-100"></audio> | |
| <div class="mt-2"> | |
| <button class="btn btn-success" id="download-btn"> | |
| <i class="fas fa-download me-2"></i> | |
| Download Audio | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Info Section --> | |
| <div class="card shadow-sm mt-4"> | |
| <div class="card-body"> | |
| <h5 class="card-title"> | |
| <i class="fas fa-info-circle text-info me-2"></i> | |
| About WebSocket Streaming | |
| </h5> | |
| <p> | |
| This demo showcases real-time audio streaming using WebSockets. Instead of waiting | |
| for the entire audio to be generated, you receive chunks as they're processed, | |
| providing immediate feedback and a more responsive experience. | |
| </p> | |
| <ul> | |
| <li><strong>Lower Perceived Latency:</strong> Start receiving audio before generation completes</li> | |
| <li><strong>Progress Tracking:</strong> Real-time updates on generation progress</li> | |
| <li><strong>Cancellable:</strong> Stop generation mid-stream if needed</li> | |
| <li><strong>Efficient:</strong> Stream chunks as they're ready, no waiting</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Include Socket.IO --> | |
| <script src="https://cdn.socket.io/4.6.0/socket.io.min.js"></script> | |
| <!-- Include our WebSocket client --> | |
| <script src="{{ url_for('static', filename='js/websocket-tts.js') }}"></script> | |
| <script> | |
| // Initialize WebSocket client | |
| let wsClient = null; | |
| let currentRequestId = null; | |
| let startTime = null; | |
| // Initialize on page load | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Create WebSocket client | |
| wsClient = new WebSocketTTSClient({ | |
| debug: true, | |
| onConnect: () => { | |
| updateConnectionStatus('connected'); | |
| }, | |
| onDisconnect: () => { | |
| updateConnectionStatus('disconnected'); | |
| }, | |
| onError: (error) => { | |
| updateConnectionStatus('error'); | |
| showError(`Connection error: ${error.message}`); | |
| } | |
| }); | |
| // Form submission | |
| document.getElementById('streaming-form').addEventListener('submit', handleStreamingSubmit); | |
| // Cancel button | |
| document.getElementById('cancel-btn').addEventListener('click', handleCancel); | |
| }); | |
| function updateConnectionStatus(status) { | |
| const statusEl = document.getElementById('connection-status'); | |
| const statusText = document.getElementById('status-text'); | |
| statusEl.className = 'alert'; | |
| switch(status) { | |
| case 'connected': | |
| statusEl.classList.add('alert-success'); | |
| statusText.innerHTML = '<i class="fas fa-check-circle me-2"></i>Connected to WebSocket server'; | |
| break; | |
| case 'disconnected': | |
| statusEl.classList.add('alert-warning'); | |
| statusText.innerHTML = '<i class="fas fa-exclamation-triangle me-2"></i>Disconnected from server'; | |
| break; | |
| case 'error': | |
| statusEl.classList.add('alert-danger'); | |
| statusText.innerHTML = '<i class="fas fa-times-circle me-2"></i>Connection error'; | |
| break; | |
| default: | |
| statusEl.classList.add('alert-info'); | |
| statusText.innerHTML = '<i class="fas fa-plug me-2"></i>Connecting...'; | |
| } | |
| } | |
| async function handleStreamingSubmit(e) { | |
| e.preventDefault(); | |
| if (!wsClient || !wsClient.isConnected()) { | |
| showError('WebSocket not connected. Please refresh the page.'); | |
| return; | |
| } | |
| // Get form values | |
| const text = document.getElementById('text-input').value.trim(); | |
| const voice = document.getElementById('voice-select').value; | |
| const format = document.getElementById('format-select').value; | |
| if (!text) { | |
| showError('Please enter some text to convert.'); | |
| return; | |
| } | |
| // Reset UI | |
| resetUI(); | |
| // Show progress section | |
| document.getElementById('progress-section').style.display = 'block'; | |
| document.getElementById('chunks-section').style.display = 'block'; | |
| document.getElementById('stream-btn').disabled = true; | |
| document.getElementById('cancel-btn').style.display = 'inline-block'; | |
| startTime = Date.now(); | |
| try { | |
| const result = await wsClient.generateSpeech(text, { | |
| voice: voice, | |
| format: format, | |
| chunkSize: 512, // Smaller chunks for more updates | |
| onStart: (data) => { | |
| currentRequestId = data.request_id; | |
| console.log('Stream started:', data); | |
| }, | |
| onProgress: (progress) => { | |
| updateProgress(progress); | |
| }, | |
| onChunk: (chunk) => { | |
| handleAudioChunk(chunk); | |
| }, | |
| onComplete: (result) => { | |
| handleStreamComplete(result); | |
| }, | |
| onError: (error) => { | |
| showError(`Streaming error: ${error.message}`); | |
| } | |
| }); | |
| console.log('Streaming completed:', result); | |
| } catch (error) { | |
| showError(`Failed to generate speech: ${error.message}`); | |
| resetUI(); | |
| } | |
| } | |
| function updateProgress(progress) { | |
| const progressBar = document.getElementById('progress-bar'); | |
| const progressText = document.getElementById('progress-text'); | |
| const chunksReceived = document.getElementById('chunks-received'); | |
| const totalChunks = document.getElementById('total-chunks'); | |
| const generationTime = document.getElementById('generation-time'); | |
| progressBar.style.width = `${progress.progress}%`; | |
| progressText.textContent = `${progress.progress}%`; | |
| chunksReceived.textContent = progress.chunksCompleted; | |
| totalChunks.textContent = progress.totalChunks; | |
| if (startTime) { | |
| const elapsed = (Date.now() - startTime) / 1000; | |
| generationTime.textContent = `${elapsed.toFixed(1)}s`; | |
| } | |
| } | |
| function handleAudioChunk(chunk) { | |
| const container = document.getElementById('chunks-container'); | |
| // Create chunk visualization | |
| const chunkEl = document.createElement('div'); | |
| chunkEl.className = 'col-auto'; | |
| chunkEl.innerHTML = ` | |
| <div class="badge bg-primary p-2" title="Chunk ${chunk.chunkIndex + 1}"> | |
| <i class="fas fa-music me-1"></i> | |
| ${chunk.chunkIndex + 1} | |
| <small class="d-block">${(chunk.audioData.byteLength / 1024).toFixed(1)}KB</small> | |
| </div> | |
| `; | |
| container.appendChild(chunkEl); | |
| // Update data transferred | |
| const currentData = parseFloat(document.getElementById('data-transferred').textContent); | |
| const newData = currentData + (chunk.audioData.byteLength / 1024); | |
| document.getElementById('data-transferred').textContent = `${newData.toFixed(1)} KB`; | |
| } | |
| function handleStreamComplete(result) { | |
| // Create blob from combined audio | |
| const blob = new Blob([result.audioData], { type: `audio/${result.format}` }); | |
| const url = URL.createObjectURL(blob); | |
| // Set up audio player | |
| const audioPlayer = document.getElementById('audio-player'); | |
| audioPlayer.src = url; | |
| // Show audio section | |
| document.getElementById('audio-section').style.display = 'block'; | |
| // Set up download button | |
| document.getElementById('download-btn').onclick = () => { | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `tts_stream_${Date.now()}.${result.format}`; | |
| a.click(); | |
| }; | |
| // Update final stats | |
| document.getElementById('generation-time').textContent = `${(result.generationTime / 1000).toFixed(2)}s`; | |
| // Reset buttons | |
| document.getElementById('stream-btn').disabled = false; | |
| document.getElementById('cancel-btn').style.display = 'none'; | |
| // Update progress bar to success | |
| const progressBar = document.getElementById('progress-bar'); | |
| progressBar.classList.remove('progress-bar-animated'); | |
| progressBar.classList.add('bg-success'); | |
| } | |
| function handleCancel() { | |
| if (currentRequestId) { | |
| wsClient.cancelStream(currentRequestId); | |
| showInfo('Stream cancelled'); | |
| resetUI(); | |
| } | |
| } | |
| function resetUI() { | |
| document.getElementById('progress-section').style.display = 'none'; | |
| document.getElementById('chunks-section').style.display = 'none'; | |
| document.getElementById('audio-section').style.display = 'none'; | |
| document.getElementById('stream-btn').disabled = false; | |
| document.getElementById('cancel-btn').style.display = 'none'; | |
| document.getElementById('chunks-container').innerHTML = ''; | |
| document.getElementById('progress-bar').style.width = '0%'; | |
| document.getElementById('progress-bar').className = 'progress-bar progress-bar-striped progress-bar-animated'; | |
| document.getElementById('data-transferred').textContent = '0 KB'; | |
| currentRequestId = null; | |
| startTime = null; | |
| } | |
| function showError(message) { | |
| console.error(message); | |
| // You could add a toast notification here | |
| } | |
| function showInfo(message) { | |
| console.info(message); | |
| // You could add a toast notification here | |
| } | |
| </script> | |
| {% endblock %} |