Files
ai.interactive.fiction/public/kokoro-loader.html
T

276 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kokoro Loader</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
line-height: 1.6;
}
#status {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 10px;
}
#log {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
.error { color: red; }
.info { color: blue; }
.success { color: green; }
</style>
</head>
<body>
<div id="status">Loading Kokoro...</div>
<div id="log"></div>
<script type="module">
// Import the KokoroTTS class from the module
import { KokoroTTS } from '/js/kokoro-js.js';
// Logging function
function log(message, type = 'info') {
console.log(message);
const logElement = document.getElementById('log');
const entry = document.createElement('div');
entry.className = type;
entry.textContent = `[${new Date().toISOString()}] ${message}`;
logElement.appendChild(entry);
logElement.scrollTop = logElement.scrollHeight;
// Also send to parent
if (window.parent !== window) {
window.parent.postMessage({
type: 'kokoro-log',
logType: type,
message: message
}, '*');
}
}
// Create a global object to store Kokoro instance
window.KokoroLoader = {
loaded: false,
error: null,
instance: null,
kokoroTTS: null,
voices: null,
callbacks: [],
progress: 0,
progressMessage: 'Initializing...',
// Register a callback for when Kokoro is loaded
onLoad: function(callback) {
if (this.loaded) {
callback(this.instance);
} else if (this.error) {
callback(null, this.error);
} else {
this.callbacks.push(callback);
}
},
// Update progress
updateProgress: function(progress, message) {
this.progress = progress;
this.progressMessage = message || 'Loading...';
const progressPercent = Math.round(progress * 100);
document.getElementById('status').textContent = `${this.progressMessage} (${isNaN(progressPercent) ? 0 : progressPercent}%)`;
log(`Progress: ${this.progressMessage} (${isNaN(progressPercent) ? 0 : progressPercent}%)`);
// Notify parent window
if (window.parent !== window) {
window.parent.postMessage({
type: 'kokoro-progress',
progress: isNaN(progress) ? 0 : progress,
message: this.progressMessage
}, '*');
}
},
// Get default voices
getDefaultVoices: function() {
return [
// American Female voices
{ id: 'af_heart', name: 'Heart', lang: 'en-US', gender: 'female' },
{ id: 'af_daisy', name: 'Daisy', lang: 'en-US', gender: 'female' },
{ id: 'af_soft', name: 'Soft', lang: 'en-US', gender: 'female' },
{ id: 'af_glados', name: 'GLaDOS', lang: 'en-US', gender: 'female' },
{ id: 'af_southern_belle', name: 'Southern Belle', lang: 'en-US', gender: 'female' },
{ id: 'af_dramatic', name: 'Dramatic', lang: 'en-US', gender: 'female' },
{ id: 'af_valley_girl', name: 'Valley Girl', lang: 'en-US', gender: 'female' },
{ id: 'af_british', name: 'British', lang: 'en-US', gender: 'female' },
{ id: 'af_russian', name: 'Russian', lang: 'en-US', gender: 'female' },
{ id: 'af_german', name: 'German', lang: 'en-US', gender: 'female' },
{ id: 'af_cheeky_cute', name: 'Cheeky Cute', lang: 'en-US', gender: 'female' },
// American Male voices
{ id: 'am_bruce', name: 'Bruce', lang: 'en-US', gender: 'male' },
{ id: 'am_announcer', name: 'Announcer', lang: 'en-US', gender: 'male' },
{ id: 'am_radio_host', name: 'Radio Host', lang: 'en-US', gender: 'male' },
// British Female voices
{ id: 'bf_charlotte', name: 'Charlotte', lang: 'en-GB', gender: 'female' },
{ id: 'bf_elizabeth', name: 'Elizabeth', lang: 'en-GB', gender: 'female' },
{ id: 'bf_lily', name: 'Lily', lang: 'en-GB', gender: 'female' },
{ id: 'bf_olivia', name: 'Olivia', lang: 'en-GB', gender: 'female' },
{ id: 'bf_victoria', name: 'Victoria', lang: 'en-GB', gender: 'female' },
// British Male voices
{ id: 'bm_william', name: 'William', lang: 'en-GB', gender: 'male' },
{ id: 'bm_arthur', name: 'Arthur', lang: 'en-GB', gender: 'male' },
{ id: 'bm_george', name: 'George', lang: 'en-GB', gender: 'male' },
{ id: 'bm_harry', name: 'Harry', lang: 'en-GB', gender: 'male' },
{ id: 'bm_jack', name: 'Jack', lang: 'en-GB', gender: 'male' }
];
},
// Initialize Kokoro
init: async function() {
try {
log('Starting Kokoro initialization');
this.updateProgress(0.1, 'Loading Kokoro library...');
// Store the KokoroTTS class
this.kokoroTTS = KokoroTTS;
log('Kokoro library loaded successfully', 'success');
this.updateProgress(0.3, 'Initializing Kokoro model...');
// Initialize the model
const model_id = "onnx-community/Kokoro-82M-v1.0-ONNX";
this.instance = await this.kokoroTTS.from_pretrained(model_id, {
dtype: "q8", // Use quantized model for better performance
device: "wasm", // Use WebAssembly for compatibility
progress_callback: (progress) => {
// Map progress from 0-1 to 30-90
const mappedProgress = 0.3 + (progress * 0.6);
this.updateProgress(mappedProgress, `Loading Kokoro model: ${Math.round(progress * 100)}%`);
}
});
// Fetch available voices
log('Fetching available voices...');
this.updateProgress(0.8, 'Fetching voices...');
// Use default voices directly since the list_voices method is unreliable
log('Using predefined voice list instead of attempting to fetch from model');
this.voices = this.getDefaultVoices();
log(`Using ${this.voices.length} predefined voices`, 'success');
log('Testing Kokoro with a simple text');
this.updateProgress(0.95, 'Testing Kokoro...');
// Test with a simple text
// Use the first available voice for testing
const testVoice = this.voices && this.voices.length > 0 ? this.voices[0].id : 'af_heart';
await this.instance.generate('Test', { voice: testVoice });
log('Kokoro initialized successfully', 'success');
this.loaded = true;
this.updateProgress(1.0, 'Kokoro ready');
// Notify parent window
if (window.parent !== window) {
log('Notifying parent window of successful initialization');
window.parent.postMessage({
type: 'kokoro-ready',
success: true,
voices: this.voices
}, '*');
}
// Call all callbacks
log(`Calling ${this.callbacks.length} registered callbacks`);
this.callbacks.forEach(callback => callback(this.instance));
document.getElementById('status').textContent = 'Kokoro loaded and ready!';
} catch (error) {
const errorMsg = error.message || 'Unknown error';
log(`Error initializing Kokoro: ${errorMsg}`, 'error');
console.error('Error initializing Kokoro:', error);
this.error = error;
// Notify parent window
if (window.parent !== window) {
log('Notifying parent window of initialization failure');
window.parent.postMessage({
type: 'kokoro-ready',
success: false,
error: errorMsg
}, '*');
}
// Call all callbacks with error
log(`Calling ${this.callbacks.length} registered callbacks with error`);
this.callbacks.forEach(callback => callback(null, error));
document.getElementById('status').textContent = `Error loading Kokoro: ${errorMsg}`;
}
}
};
// Initialize Kokoro when the page loads
window.addEventListener('load', () => {
log('Page loaded, initializing Kokoro');
window.KokoroLoader.init();
});
// Handle messages from parent window
window.addEventListener('message', function(event) {
if (event.source !== window.parent) return;
const data = event.data;
if (data.type === 'kokoro-generate') {
// Generate speech in a non-blocking way
if (!window.KokoroLoader.loaded) {
log(`Cannot process generation request ${data.id}: Kokoro not loaded`, 'error');
window.parent.postMessage({
type: 'kokoro-generated',
id: data.id,
success: false,
error: 'Kokoro not loaded'
}, '*');
} else {
log(`Processing generation request ${data.id}`);
// Make the generation asynchronous to avoid freezing the UI
setTimeout(() => {
window.KokoroLoader.instance.generate(data.text, { voice: data.voice, speed: data.speed })
.then(result => {
log(`Generation successful for request ${data.id}`, 'success');
window.parent.postMessage({
type: 'kokoro-generated',
id: data.id,
success: true,
result: result
}, '*');
})
.catch(error => {
log(`Generation failed for request ${data.id}: ${error.message}`, 'error');
window.parent.postMessage({
type: 'kokoro-generated',
id: data.id,
success: false,
error: error.message
}, '*');
});
}, 0);
}
}
});
</script>
</body>
</html>