Kokoro-FastAPI/web/src/components/VoiceSelector.js

166 lines
No EOL
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

export class VoiceSelector {
constructor(voiceService) {
this.voiceService = voiceService;
this.elements = {
voiceSearch: document.getElementById('voice-search'),
voiceDropdown: document.getElementById('voice-dropdown'),
voiceOptions: document.getElementById('voice-options'),
selectedVoices: document.getElementById('selected-voices')
};
this.setupEventListeners();
}
setupEventListeners() {
// Voice search focus
this.elements.voiceSearch.addEventListener('focus', () => {
this.elements.voiceDropdown.classList.add('show');
});
// Voice search
this.elements.voiceSearch.addEventListener('input', (e) => {
const filteredVoices = this.voiceService.filterVoices(e.target.value);
this.renderVoiceOptions(filteredVoices);
});
// Voice selection - handle clicks on the entire voice option
this.elements.voiceOptions.addEventListener('mousedown', (e) => {
e.preventDefault(); // Prevent blur on search input
const voiceOption = e.target.closest('.voice-option');
if (!voiceOption) return;
const voice = voiceOption.dataset.voice;
if (!voice) return;
const isSelected = voiceOption.classList.contains('selected');
if (!isSelected) {
this.voiceService.addVoice(voice);
} else {
this.voiceService.removeVoice(voice);
}
voiceOption.classList.toggle('selected');
this.updateSelectedVoicesDisplay();
// Keep focus on search input
requestAnimationFrame(() => {
this.elements.voiceSearch.focus();
});
});
// Weight adjustment
this.elements.selectedVoices.addEventListener('input', (e) => {
if (e.target.type === 'number') {
const voice = e.target.dataset.voice;
let weight = parseFloat(e.target.value);
// Ensure weight is between 0.1 and 10
weight = Math.max(0.1, Math.min(10, weight));
e.target.value = weight;
this.voiceService.updateWeight(voice, weight);
}
});
// Remove selected voice
this.elements.selectedVoices.addEventListener('click', (e) => {
if (e.target.classList.contains('remove-voice')) {
e.preventDefault();
e.stopPropagation();
const voice = e.target.dataset.voice;
this.voiceService.removeVoice(voice);
this.updateVoiceOptionState(voice, false);
this.updateSelectedVoicesDisplay();
}
});
// Handle clicks outside to close dropdown
document.addEventListener('mousedown', (e) => {
// Don't handle clicks in selected voices area
if (this.elements.selectedVoices.contains(e.target)) {
return;
}
// Don't close if clicking in search or dropdown
if (this.elements.voiceSearch.contains(e.target) ||
this.elements.voiceDropdown.contains(e.target)) {
return;
}
this.elements.voiceDropdown.classList.remove('show');
this.elements.voiceSearch.blur();
});
this.elements.voiceSearch.addEventListener('blur', () => {
if (!this.elements.voiceSearch.value) {
this.updateSearchPlaceholder();
}
});
}
renderVoiceOptions(voices) {
this.elements.voiceOptions.innerHTML = voices
.map(voice => `
<div class="voice-option ${this.voiceService.getSelectedVoices().includes(voice) ? 'selected' : ''}"
data-voice="${voice}">
${voice}
</div>
`)
.join('');
}
updateSelectedVoicesDisplay() {
const selectedVoices = this.voiceService.getSelectedVoiceWeights();
this.elements.selectedVoices.innerHTML = selectedVoices
.map(({voice, weight}) => `
<span class="selected-voice-tag">
<span class="voice-name">${voice}</span>
<span class="voice-weight">
<input type="number"
value="${weight}"
min="0.1"
max="10"
step="0.1"
data-voice="${voice}"
class="weight-input"
title="Voice weight (0.1 to 10)">
</span>
<span class="remove-voice" data-voice="${voice}" title="Remove voice">×</span>
</span>
`)
.join('');
this.updateSearchPlaceholder();
}
updateSearchPlaceholder() {
const hasSelected = this.voiceService.hasSelectedVoices();
this.elements.voiceSearch.placeholder = hasSelected ?
'Search voices...' :
'Search and select voices...';
}
updateVoiceOptionState(voice, selected) {
const voiceOption = this.elements.voiceOptions
.querySelector(`[data-voice="${voice}"]`);
if (voiceOption) {
voiceOption.classList.toggle('selected', selected);
}
}
async initialize() {
try {
await this.voiceService.loadVoices();
this.renderVoiceOptions(this.voiceService.getAvailableVoices());
this.updateSelectedVoicesDisplay();
return true;
} catch (error) {
console.error('Failed to initialize voice selector:', error);
return false;
}
}
}
export default VoiceSelector;