mirror of
https://github.com/remsky/Kokoro-FastAPI.git
synced 2025-08-05 16:48:53 +00:00
188 lines
6.7 KiB
JavaScript
188 lines
6.7 KiB
JavaScript
![]() |
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';
|
||
|
|
||
|
export class App {
|
||
|
constructor() {
|
||
|
this.elements = {
|
||
|
textInput: document.getElementById('text-input'),
|
||
|
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 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.style.display = 'none';
|
||
|
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.style.display = 'flex';
|
||
|
});
|
||
|
|
||
|
// Handle completion
|
||
|
this.audioService.addEventListener('complete', () => {
|
||
|
this.setGenerating(false);
|
||
|
this.showStatus('Generation complete', 'success');
|
||
|
});
|
||
|
|
||
|
// 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.className = isGenerating ? 'loading' : '';
|
||
|
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.elements.textInput.value.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.elements.textInput.value.trim();
|
||
|
const voice = this.voiceService.getSelectedVoiceString();
|
||
|
const speed = this.playerState.getState().speed;
|
||
|
|
||
|
this.setGenerating(true);
|
||
|
this.elements.downloadBtn.style.display = 'none';
|
||
|
|
||
|
// 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) return;
|
||
|
|
||
|
const format = this.elements.formatSelect.value;
|
||
|
const voice = this.voiceService.getSelectedVoiceString();
|
||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||
|
|
||
|
// Create download link
|
||
|
const a = document.createElement('a');
|
||
|
a.href = downloadUrl;
|
||
|
a.download = `${voice}_${timestamp}.${format}`;
|
||
|
document.body.appendChild(a);
|
||
|
a.click();
|
||
|
document.body.removeChild(a);
|
||
|
URL.revokeObjectURL(downloadUrl);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Initialize app when DOM is loaded
|
||
|
document.addEventListener('DOMContentLoaded', () => {
|
||
|
new App();
|
||
|
});
|