Update TTS providers and story markup

This commit is contained in:
2026-05-20 22:13:31 +02:00
parent b911c40d89
commit 8258ea2321
36 changed files with 1482 additions and 197 deletions
+197 -2
View File
@@ -37,6 +37,8 @@ class OptionsUIModule extends BaseModule {
'createModal',
'populateTtsSystems',
'populateVoices',
'ensureSelectedVoiceIsAvailable',
'updateVoiceControlVisibility',
'populateLanguages',
'loadPreferences',
'createVolumeControl',
@@ -233,10 +235,10 @@ class OptionsUIModule extends BaseModule {
this.elements.ttsSpeed = createUIElement('input', {
type: 'range',
min: 50,
max: 150,
max: 200,
value: 100,
'data-pref-bind': 'tts.speed',
'data-pref-transform': 'centered-speed'
'data-pref-transform': 'multiplier-percent'
}, null, speedContainer);
// Update displayed value when slider changes
@@ -301,6 +303,14 @@ class OptionsUIModule extends BaseModule {
this.elements.ttsVoice = createUIElement('select', {
'data-pref-bind': 'tts.voice'
}, null, ttsVoiceContainer);
this.elements.localOpenAiVoice = createUIElement('input', {
id: 'local-openai-voice',
type: 'text',
placeholder: 'alloy',
'data-pref-bind': 'tts.local-openai-tts_voice'
}, null, ttsVoiceContainer);
this.elements.localOpenAiVoice.style.display = 'none';
ttsSection.appendChild(ttsVoiceContainer);
@@ -503,10 +513,108 @@ class OptionsUIModule extends BaseModule {
}, null, openaiApiUrlContainer);
openaiSettings.appendChild(openaiApiUrlContainer);
const openaiModelContainer = document.createElement('div');
openaiModelContainer.className = 'option-item';
const openaiModelLabel = document.createElement('label');
openaiModelLabel.textContent = this.t('options.model') + ':';
openaiModelContainer.appendChild(openaiModelLabel);
this.elements.openaiModel = createUIElement('select', {
id: 'openai-model',
'data-pref-bind': 'tts.openai-tts_model'
}, null, openaiModelContainer);
[
{ id: 'tts-1', name: 'TTS-1' },
{ id: 'tts-1-hd', name: 'TTS-1 HD' },
{ id: 'gpt-4o-mini-tts', name: 'GPT-4o mini TTS' }
].forEach(model => {
const option = document.createElement('option');
option.value = model.id;
option.textContent = model.name;
this.elements.openaiModel.appendChild(option);
});
openaiSettings.appendChild(openaiModelContainer);
// Local OpenAI-compatible API settings
const localOpenAiSettings = document.createElement('div');
localOpenAiSettings.className = 'api-settings local-openai-tts-settings';
localOpenAiSettings.style.display = 'none';
const localOpenAiTitle = document.createElement('h3');
localOpenAiTitle.textContent = this.t('options.localOpenAiSettings');
localOpenAiSettings.appendChild(localOpenAiTitle);
const localOpenAiApiKeyContainer = document.createElement('div');
localOpenAiApiKeyContainer.className = 'option-item';
const localOpenAiApiKeyLabel = document.createElement('label');
localOpenAiApiKeyLabel.textContent = this.t('options.optionalApiKey') + ':';
localOpenAiApiKeyContainer.appendChild(localOpenAiApiKeyLabel);
this.elements.localOpenAiApiKey = createUIElement('input', {
type: 'password',
'data-pref-bind': 'tts.local-openai-tts_api_key'
}, null, localOpenAiApiKeyContainer);
localOpenAiSettings.appendChild(localOpenAiApiKeyContainer);
const localOpenAiApiUrlContainer = document.createElement('div');
localOpenAiApiUrlContainer.className = 'option-item';
const localOpenAiApiUrlLabel = document.createElement('label');
localOpenAiApiUrlLabel.textContent = this.t('options.apiUrl') + ':';
localOpenAiApiUrlContainer.appendChild(localOpenAiApiUrlLabel);
this.elements.localOpenAiApiUrl = createUIElement('input', {
type: 'text',
'data-pref-bind': 'tts.local-openai-tts_api_url'
}, null, localOpenAiApiUrlContainer);
localOpenAiSettings.appendChild(localOpenAiApiUrlContainer);
const localOpenAiModelContainer = document.createElement('div');
localOpenAiModelContainer.className = 'option-item';
const localOpenAiModelLabel = document.createElement('label');
localOpenAiModelLabel.textContent = this.t('options.model') + ':';
localOpenAiModelContainer.appendChild(localOpenAiModelLabel);
this.elements.localOpenAiModel = createUIElement('input', {
id: 'local-openai-model',
type: 'text',
placeholder: 'tts-1',
'data-pref-bind': 'tts.local-openai-tts_model'
}, null, localOpenAiModelContainer);
localOpenAiSettings.appendChild(localOpenAiModelContainer);
const localOpenAiTimeoutContainer = document.createElement('div');
localOpenAiTimeoutContainer.className = 'option-item';
const localOpenAiTimeoutLabel = document.createElement('label');
localOpenAiTimeoutLabel.textContent = this.t('options.requestTimeoutMs') + ':';
localOpenAiTimeoutContainer.appendChild(localOpenAiTimeoutLabel);
this.elements.localOpenAiTimeout = createUIElement('input', {
id: 'local-openai-timeout-ms',
type: 'number',
min: 1000,
max: 600000,
step: 1000,
'data-pref-bind': 'tts.local-openai-tts_timeout_ms',
'data-pref-transform': 'integer:1000,600000'
}, null, localOpenAiTimeoutContainer);
localOpenAiSettings.appendChild(localOpenAiTimeoutContainer);
// Add all API settings to container
apiSettings.appendChild(elevenLabsSettings);
apiSettings.appendChild(openaiSettings);
apiSettings.appendChild(localOpenAiSettings);
return apiSettings;
}
@@ -622,6 +730,15 @@ class OptionsUIModule extends BaseModule {
if (!ttsFactory || !this.elements.ttsVoice) return;
const selectedHandler = this.elements.ttsSystem?.value || this.getPreference('tts', 'preferred_handler', 'none');
this.updateVoiceControlVisibility(selectedHandler);
if (selectedHandler === 'local-openai-tts') {
if (this.elements.localOpenAiVoice) {
this.elements.localOpenAiVoice.value = this.getPreference('tts', 'local-openai-tts_voice', 'alloy');
}
return;
}
const voices = typeof ttsFactory.getVoicesForHandler === 'function'
? await ttsFactory.getVoicesForHandler(selectedHandler) || []
: await ttsFactory.getVoices() || [];
@@ -635,6 +752,34 @@ class OptionsUIModule extends BaseModule {
'name',
this.getPreference('tts', `${selectedHandler}_voice`, this.getPreference('tts', 'voice', ''))
);
this.ensureSelectedVoiceIsAvailable(selectedHandler, voices);
}
ensureSelectedVoiceIsAvailable(selectedHandler, voices = []) {
if (!this.elements.ttsVoice || selectedHandler === 'local-openai-tts') return;
if (!Array.isArray(voices) || voices.length === 0) return;
const available = new Set(voices.map(voice => String(voice.id || '').toLowerCase()));
const current = String(this.elements.ttsVoice.value || '').toLowerCase();
if (current && available.has(current)) return;
const fallback = voices.some(voice => voice.id === 'alloy') ? 'alloy' : voices[0].id;
this.elements.ttsVoice.value = fallback;
this.updatePreference('tts', 'voice', fallback);
if (selectedHandler && selectedHandler !== 'none') {
this.updatePreference('tts', `${selectedHandler}_voice`, fallback);
}
}
updateVoiceControlVisibility(selectedHandler) {
const useTextVoice = selectedHandler === 'local-openai-tts';
if (this.elements.ttsVoice) {
this.elements.ttsVoice.style.display = useTextVoice ? 'none' : '';
}
if (this.elements.localOpenAiVoice) {
this.elements.localOpenAiVoice.style.display = useTextVoice ? '' : 'none';
}
}
renderProviderStatuses() {
@@ -698,6 +843,7 @@ class OptionsUIModule extends BaseModule {
// Update API settings visibility based on current TTS system
if (this.elements.ttsSystem) {
this.updateApiSettingsVisibility(this.elements.ttsSystem.value);
this.updateVoiceControlVisibility(this.elements.ttsSystem.value);
}
}
@@ -753,6 +899,36 @@ class OptionsUIModule extends BaseModule {
if (!this.getPreference('tts', 'openai-tts_api_key')) {
this.updatePreference('tts', 'openai-tts_api_key', '');
}
if (!this.getPreference('tts', 'openai-tts_model')) {
this.updatePreference('tts', 'openai-tts_model', 'tts-1-hd');
}
if (this.elements.localOpenAiApiUrl) {
const savedUrl = this.getPreference('tts', 'local-openai-tts_api_url');
const defaultUrl = 'http://localhost:8000/v1';
if (!savedUrl) {
console.log('Options UI: Setting default local OpenAI-compatible API URL:', defaultUrl);
this.updatePreference('tts', 'local-openai-tts_api_url', defaultUrl);
}
}
if (!this.getPreference('tts', 'local-openai-tts_api_key')) {
this.updatePreference('tts', 'local-openai-tts_api_key', '');
}
if (!this.getPreference('tts', 'local-openai-tts_voice')) {
this.updatePreference('tts', 'local-openai-tts_voice', 'alloy');
}
if (!this.getPreference('tts', 'local-openai-tts_model')) {
this.updatePreference('tts', 'local-openai-tts_model', 'tts-1');
}
if (!this.getPreference('tts', 'local-openai-tts_timeout_ms')) {
this.updatePreference('tts', 'local-openai-tts_timeout_ms', 60000);
}
}
/**
@@ -895,6 +1071,7 @@ class OptionsUIModule extends BaseModule {
this.renderProviderStatuses();
});
this.updateApiSettingsVisibility(value);
this.updateVoiceControlVisibility(value);
} else if (key === 'voice') {
ttsFactory.configure({ voice: value });
} else if (key === 'speed') {
@@ -919,6 +1096,24 @@ class OptionsUIModule extends BaseModule {
const provider = key.replace('_api_url', '');
this.dispatchApiChangeEvent('api:urlChanged', provider, 'url', value);
ttsFactory.refreshHandlerStatus(provider).then(() => this.renderProviderStatuses());
} else if (key.endsWith('_voice')) {
const provider = key.replace('_voice', '');
const handler = typeof ttsFactory.getHandler === 'function' ? ttsFactory.getHandler(provider) : null;
if (handler && typeof handler.setVoiceOptions === 'function') {
handler.setVoiceOptions({ voice: value });
}
if (ttsFactory.activeHandler === provider) {
ttsFactory.voice = value;
}
} else if (key.endsWith('_model')) {
const provider = key.replace('_model', '');
const handler = typeof ttsFactory.getHandler === 'function' ? ttsFactory.getHandler(provider) : null;
if (handler && typeof handler.setVoiceOptions === 'function') {
handler.setVoiceOptions({ model: value });
}
if (provider === 'openai-tts') {
this.populateVoices();
}
}
if (key === 'speed' && this.elements.ttsSpeed) {
this.updateSpeedDisplay();