mirror of
https://github.com/remsky/Kokoro-FastAPI.git
synced 2025-04-13 09:39:17 +00:00
230 lines
No EOL
8.2 KiB
JavaScript
230 lines
No EOL
8.2 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';
|
|
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.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.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();
|
|
}); |