Spaces:
Running
Running
| // index.js | |
| class SupertonicTTS { | |
| constructor() { | |
| this.tts = null; | |
| this.currentAudio = null; | |
| this.isGenerating = false; | |
| this.device = 'cpu'; | |
| this.initElements(); | |
| this.bindEvents(); | |
| this.checkWebGPU(); | |
| } | |
| initElements() { | |
| this.textInput = document.getElementById('textInput'); | |
| this.generateBtn = document.getElementById('generateBtn'); | |
| this.playBtn = document.getElementById('playBtn'); | |
| this.downloadBtn = document.getElementById('downloadBtn'); | |
| this.regenerateBtn = document.getElementById('regenerateBtn'); | |
| this.audioPlayer = document.getElementById('audioPlayer'); | |
| this.deviceToggle = document.getElementById('deviceToggle'); | |
| this.deviceText = document.getElementById('deviceText'); | |
| this.status = document.getElementById('status'); | |
| this.audioSection = document.getElementById('audioSection'); | |
| this.charCount = document.getElementById('charCount'); | |
| } | |
| bindEvents() { | |
| this.textInput.addEventListener('input', () => this.updateCharCount()); | |
| this.generateBtn.addEventListener('click', () => this.generateSpeech()); | |
| this.playBtn.addEventListener('click', () => this.playAudio()); | |
| this.downloadBtn.addEventListener('click', () => this.downloadAudio()); | |
| this.regenerateBtn.addEventListener('click', () => this.regenerate()); | |
| this.deviceToggle.addEventListener('change', (e) => this.toggleDevice(e.target.checked)); | |
| this.audioPlayer.addEventListener('ended', () => this.updatePlayButton(true)); | |
| } | |
| async checkWebGPU() { | |
| if (navigator.gpu) { | |
| this.deviceText.textContent = 'GPU Available'; | |
| this.deviceToggle.disabled = false; | |
| } else { | |
| this.deviceText.textContent = 'GPU Not Supported'; | |
| } | |
| } | |
| toggleDevice(enabled) { | |
| this.device = enabled ? 'webgpu' : 'cpu'; | |
| this.deviceText.textContent = enabled ? 'GPU Mode' : 'CPU Mode'; | |
| if (this.tts) { | |
| this.showStatus('Device changed. Please regenerate audio.', 'info'); | |
| } | |
| } | |
| updateCharCount() { | |
| const length = this.textInput.value.length; | |
| this.charCount.textContent = `${length}/500`; | |
| this.generateBtn.disabled = !this.textInput.value.trim() || length > 500 || this.isGenerating; | |
| } | |
| async initTTS() { | |
| try { | |
| this.showStatus('Loading Supertonic TTS model...', 'loading'); | |
| const options = { | |
| dtype: 'fp32' | |
| }; | |
| if (this.device === 'webgpu') { | |
| options.device = 'webgpu'; | |
| } | |
| // Note: For demo purposes, we'll use speaker_embeddings as a placeholder | |
| // In production, host actual speaker files or use default | |
| this.tts = await window.pipeline('text-to-speech', 'onnx-community/Supertonic-TTS-ONNX', options); | |
| this.showStatus('Model loaded successfully! Ready to generate speech.', 'success'); | |
| this.generateBtn.disabled = false; | |
| } catch (error) { | |
| console.error('Failed to load TTS model:', error); | |
| this.showStatus(`Failed to load model: ${error.message}. Retrying in CPU mode...`, 'error'); | |
| // Fallback to CPU | |
| try { | |
| this.tts = await window.pipeline('text-to-speech', 'onnx-community/Supertonic-TTS-ONNX', { dtype: 'fp32' }); | |
| this.showStatus('Model loaded in CPU mode!', 'success'); | |
| this.generateBtn.disabled = false; | |
| } catch (fallbackError) { | |
| this.showStatus('Failed to load model. Please refresh and try again.', 'error'); | |
| } | |
| } | |
| } | |
| async generateSpeech() { | |
| if (this.isGenerating) return; | |
| const text = this.textInput.value.trim(); | |
| if (!text) { | |
| this.showStatus('Please enter some text to convert to speech.', 'error'); | |
| return; | |
| } | |
| this.isGenerating = true; | |
| this.generateBtn.disabled = true; | |
| this.setButtonLoading(true); | |
| try { | |
| this.showStatus('Generating speech...', 'loading'); | |
| // Generate audio - note: speaker_embeddings path simplified for demo | |
| // In production, host actual speaker embedding files | |
| const audio = await this.tts(text, { | |
| speaker_embeddings: 'https://huggingface.co/datasets/Supertonic/Supertonic-TTS-ONNX/resolve/main/voices/F1.bin' | |
| }); | |
| // Convert to playable audio | |
| this.currentAudio = audio; | |
| // Create blob URL for audio player | |
| const audioBlob = await audio.save(); | |
| const audioUrl = URL.createObjectURL(audioBlob); | |
| this.audioPlayer.src = audioUrl; | |
| this.audioSection.classList.remove('hidden'); | |
| this.downloadBtn.disabled = false; | |
| this.updatePlayButton(true); | |
| this.showStatus('Speech generated successfully! Click play to listen.', 'success'); | |
| } catch (error) { | |
| console.error('TTS generation failed:', error); | |
| this.showStatus(`Generation failed: ${error.message}`, 'error'); | |
| } finally { | |
| this.isGenerating = false; | |
| this.setButtonLoading(false); | |
| this.generateBtn.disabled = false; | |
| } | |
| } | |
| playAudio() { | |
| if (this.audioPlayer.paused) { | |
| this.audioPlayer.play(); | |
| this.updatePlayButton(false); | |
| } else { | |
| this.audioPlayer.pause(); | |
| this.updatePlayButton(true); | |
| } | |
| } | |
| updatePlayButton(isPaused) { | |
| const icon = this.playBtn.querySelector('svg'); | |
| if (isPaused) { | |
| icon.innerHTML = '<path d="M8 5v14l11-7z"/>'; | |
| this.playBtn.title = 'Play audio'; | |
| } else { | |
| icon.innerHTML = '<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>'; | |
| this.playBtn.title = 'Pause audio'; | |
| } | |
| } | |
| async downloadAudio() { | |
| if (this.currentAudio) { | |
| const audioBlob = await this.currentAudio.save(); | |
| const url = URL.createObjectURL(audioBlob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'supertonic-speech.wav'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| } | |
| regenerate() { | |
| this.textInput.value = ''; | |
| this.audioSection.classList.add('hidden'); | |
| this.downloadBtn.disabled = true; | |
| this.currentAudio = null; | |
| this.audioPlayer.src = ''; | |
| this.updateCharCount(); | |
| } | |
| setButtonLoading(loading) { | |
| const spinner = this.generateBtn.querySelector('.spinner'); | |
| const btnText = this.generateBtn.querySelector('.btn-text'); | |
| if (loading) { | |
| spinner.style.display = 'inline-block'; | |
| btnText.textContent = 'Generating...'; | |
| } else { | |
| spinner.style.display = 'none'; | |
| btnText.textContent = 'Generate Speech'; | |
| } | |
| } | |
| showStatus(message, type) { | |
| this.status.textContent = message; | |
| this.status.className = `status ${type}`; | |
| this.status.classList.remove('hidden'); | |
| if (type !== 'loading') { | |
| setTimeout(() => { | |
| this.status.classList.add('hidden'); | |
| }, 5000); | |
| } | |
| } | |
| } | |
| // Initialize app when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const app = new SupertonicTTS(); | |
| // Auto-initialize TTS model | |
| setTimeout(() => app.initTTS(), 100); | |
| }); |