mirror of
https://github.com/remsky/Kokoro-FastAPI.git
synced 2025-08-05 16:48:53 +00:00
166 lines
No EOL
5.9 KiB
JavaScript
166 lines
No EOL
5.9 KiB
JavaScript
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; |