Spaces:
Runtime error
Runtime error
import AudioService from './services/AudioService.js'; | |
import VoiceService from './services/VoiceService.js'; | |
import PlayerState from './state/PlayerState.js'; | |
import PlayerControls from './components/PlayerControls.js'; | |
import VoiceSelector from './components/VoiceSelector.js'; | |
import WaveVisualizer from './components/WaveVisualizer.js'; | |
import TextEditor from './components/TextEditor.js'; | |
export class App { | |
constructor() { | |
this.elements = { | |
generateBtn: document.getElementById('generate-btn'), | |
generateBtnText: document.querySelector('#generate-btn .btn-text'), | |
generateBtnLoader: document.querySelector('#generate-btn .loader'), | |
downloadBtn: document.getElementById('download-btn'), | |
autoplayToggle: document.getElementById('autoplay-toggle'), | |
formatSelect: document.getElementById('format-select'), | |
status: document.getElementById('status'), | |
cancelBtn: document.getElementById('cancel-btn') | |
}; | |
this.initialize(); | |
} | |
async initialize() { | |
// Initialize services and state | |
this.playerState = new PlayerState(); | |
this.audioService = new AudioService(); | |
this.voiceService = new VoiceService(); | |
// Initialize components | |
this.playerControls = new PlayerControls(this.audioService, this.playerState); | |
this.voiceSelector = new VoiceSelector(this.voiceService); | |
this.waveVisualizer = new WaveVisualizer(this.playerState); | |
// Initialize text editor | |
const editorContainer = document.getElementById('text-editor'); | |
this.textEditor = new TextEditor(editorContainer, { | |
linesPerPage: 20, | |
onTextChange: (text) => { | |
// Optional: Handle text changes here if needed | |
console.log('Text changed:', text); | |
} | |
}); | |
// Initialize voice selector | |
const voicesLoaded = await this.voiceSelector.initialize(); | |
if (!voicesLoaded) { | |
this.showStatus('Failed to load voices', 'error'); | |
this.elements.generateBtn.disabled = true; | |
return; | |
} | |
this.setupEventListeners(); | |
this.setupAudioEvents(); | |
} | |
setupEventListeners() { | |
// Generate button | |
this.elements.generateBtn.addEventListener('click', () => this.generateSpeech()); | |
// Download button | |
this.elements.downloadBtn.addEventListener('click', () => this.downloadAudio()); | |
// Cancel button | |
this.elements.cancelBtn.addEventListener('click', () => { | |
this.audioService.cancel(); | |
this.setGenerating(false); | |
this.elements.downloadBtn.classList.remove('ready'); | |
this.showStatus('Generation cancelled', 'info'); | |
}); | |
// Handle page unload | |
window.addEventListener('beforeunload', () => { | |
this.audioService.cleanup(); | |
this.playerControls.cleanup(); | |
this.waveVisualizer.cleanup(); | |
}); | |
} | |
setupAudioEvents() { | |
// Handle download button visibility | |
this.audioService.addEventListener('downloadReady', () => { | |
this.elements.downloadBtn.classList.add('ready'); | |
}); | |
// Handle buffer errors | |
this.audioService.addEventListener('bufferError', () => { | |
this.showStatus('Processing... (Download will be available when complete)', 'info'); | |
}); | |
// Handle completion | |
this.audioService.addEventListener('complete', () => { | |
this.setGenerating(false); | |
// Show preparing status | |
this.showStatus('Preparing file...', 'info'); | |
// Trigger coffee steam animation | |
const steamElement = document.querySelector('.cup .steam'); | |
if (steamElement) { | |
// Remove and re-add the element to restart animation | |
const parent = steamElement.parentNode; | |
const clone = steamElement.cloneNode(true); | |
parent.removeChild(steamElement); | |
parent.appendChild(clone); | |
} | |
}); | |
// Handle download ready | |
this.audioService.addEventListener('downloadReady', () => { | |
setTimeout(() => { | |
this.showStatus('Generation complete', 'success'); | |
}, 500); // Small delay to ensure "Preparing file..." is visible | |
}); | |
// Handle audio end | |
this.audioService.addEventListener('ended', () => { | |
this.playerState.setPlaying(false); | |
}); | |
// Handle errors | |
this.audioService.addEventListener('error', (error) => { | |
this.showStatus('Error: ' + error.message, 'error'); | |
this.setGenerating(false); | |
this.elements.downloadBtn.style.display = 'none'; | |
}); | |
} | |
showStatus(message, type = 'info') { | |
this.elements.status.textContent = message; | |
this.elements.status.className = 'status ' + type; | |
setTimeout(() => { | |
this.elements.status.className = 'status'; | |
}, 5000); | |
} | |
setGenerating(isGenerating) { | |
this.playerState.setGenerating(isGenerating); | |
this.elements.generateBtn.disabled = isGenerating; | |
this.elements.generateBtn.classList.toggle('loading', isGenerating); | |
this.elements.generateBtnLoader.style.display = isGenerating ? 'block' : 'none'; | |
this.elements.generateBtnText.style.visibility = isGenerating ? 'hidden' : 'visible'; | |
this.elements.cancelBtn.style.display = isGenerating ? 'block' : 'none'; | |
} | |
validateInput() { | |
const text = this.textEditor.getText().trim(); | |
if (!text) { | |
this.showStatus('Please enter some text', 'error'); | |
return false; | |
} | |
if (!this.voiceService.hasSelectedVoices()) { | |
this.showStatus('Please select a voice', 'error'); | |
return false; | |
} | |
return true; | |
} | |
async generateSpeech() { | |
// Don't check isGenerating state since we want to allow generation after cancel | |
if (!this.validateInput()) { | |
return; | |
} | |
const text = this.textEditor.getText().trim(); | |
const voice = this.voiceService.getSelectedVoiceString(); | |
const speed = this.playerState.getState().speed; | |
this.setGenerating(true); | |
this.elements.downloadBtn.classList.remove('ready'); | |
// Just reset progress bar, don't do full cleanup | |
this.waveVisualizer.updateProgress(0, 1); | |
try { | |
console.log('Starting audio generation...', { text, voice, speed }); | |
// Ensure we have valid input | |
if (!text || !voice) { | |
console.error('Invalid input:', { text, voice, speed }); | |
throw new Error('Invalid input parameters'); | |
} | |
await this.audioService.streamAudio( | |
text, | |
voice, | |
speed, | |
(loaded, total) => { | |
console.log('Progress update:', { loaded, total }); | |
this.waveVisualizer.updateProgress(loaded, total); | |
} | |
); | |
} catch (error) { | |
console.error('Generation error:', error); | |
if (error.name !== 'AbortError') { | |
this.showStatus('Error generating speech: ' + error.message, 'error'); | |
this.setGenerating(false); | |
} | |
} | |
} | |
downloadAudio() { | |
const downloadUrl = this.audioService.getDownloadUrl(); | |
if (!downloadUrl) { | |
console.warn('No download URL available'); | |
return; | |
} | |
console.log('Starting download from:', downloadUrl); | |
const format = this.elements.formatSelect.value; | |
const voice = this.voiceService.getSelectedVoiceString(); | |
const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); | |
const a = document.createElement('a'); | |
a.href = downloadUrl; | |
a.download = `${voice}_${timestamp}.${format}`; | |
document.body.appendChild(a); | |
a.click(); | |
document.body.removeChild(a); | |
} | |
} | |
// Initialize app when DOM is loaded | |
document.addEventListener('DOMContentLoaded', () => { | |
new App(); | |
}); | |