Update TTS providers and story markup
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user