feat(voice): add admin-configurable elevenlabs tts

This commit is contained in:
Yera All
2026-04-11 16:46:43 +05:00
parent 9d0b76deca
commit fc976804e4
14 changed files with 1342 additions and 32 deletions
+178
View File
@@ -11,6 +11,7 @@ const state = {
providerLabel: 'Keycloak',
},
voiceNameConfig: null,
voiceTtsConfig: null,
};
const $ = (id) => document.getElementById(id);
@@ -650,6 +651,172 @@ function resetVoiceNameConfigForm() {
log('Форма voice name collection сброшена к текущим настройкам');
}
function voiceTtsProviderConfig(config, provider) {
return config?.[provider] || { ru: {}, kz: {} };
}
function voiceTtsOptionsFor(provider, language) {
return state.voiceTtsConfig?.voice_options?.[provider]?.[language] || [];
}
function fillVoiceTtsPresetSelect(selectId, options, selectedValue) {
const select = $(selectId);
if (!select) {
return;
}
const rows = ['<option value="">Свой вариант</option>'].concat(
(options || []).map((item) => `<option value="${item.value}">${item.label}</option>`),
);
select.innerHTML = rows.join('');
const hasSelectedValue = (options || []).some((item) => item.value === selectedValue);
select.value = hasSelectedValue ? selectedValue : '';
}
function syncVoiceTtsPresetOptions(payload) {
const config = payload?.config || payload || state.voiceTtsConfig?.config;
if (!config) {
return;
}
fillVoiceTtsPresetSelect('voiceTtsYandexRuVoicePreset', voiceTtsOptionsFor('yandex', 'ru'), config.yandex?.ru?.voice || '');
fillVoiceTtsPresetSelect('voiceTtsYandexKzVoicePreset', voiceTtsOptionsFor('yandex', 'kz'), config.yandex?.kz?.voice || '');
fillVoiceTtsPresetSelect('voiceTtsElevenRuVoicePreset', voiceTtsOptionsFor('elevenlabs', 'ru'), config.elevenlabs?.ru?.voice || '');
fillVoiceTtsPresetSelect('voiceTtsElevenKzVoicePreset', voiceTtsOptionsFor('elevenlabs', 'kz'), config.elevenlabs?.kz?.voice || '');
fillVoiceTtsPresetSelect('voiceTtsOpenAiRuVoicePreset', voiceTtsOptionsFor('openai', 'ru'), config.openai?.ru?.voice || '');
fillVoiceTtsPresetSelect('voiceTtsOpenAiKzVoicePreset', voiceTtsOptionsFor('openai', 'kz'), config.openai?.kz?.voice || '');
}
function serializeVoiceTtsConfigForm() {
return {
provider: $('voiceTtsProvider').value || 'yandex',
yandex: {
ru: { voice: $('voiceTtsYandexRuVoice').value.trim() || null },
kz: { voice: $('voiceTtsYandexKzVoice').value.trim() || null },
},
elevenlabs: {
ru: {
voice: $('voiceTtsElevenRuVoice').value.trim() || null,
model_id: $('voiceTtsElevenRuModel').value.trim() || null,
language_code: $('voiceTtsElevenRuLanguageCode').value.trim() || null,
},
kz: {
voice: $('voiceTtsElevenKzVoice').value.trim() || null,
model_id: $('voiceTtsElevenKzModel').value.trim() || null,
language_code: $('voiceTtsElevenKzLanguageCode').value.trim() || null,
},
},
openai: {
ru: {
voice: $('voiceTtsOpenAiRuVoice').value.trim() || null,
model_id: $('voiceTtsOpenAiRuModel').value.trim() || null,
language_code: $('voiceTtsOpenAiRuLanguageCode').value.trim() || null,
},
kz: {
voice: $('voiceTtsOpenAiKzVoice').value.trim() || null,
model_id: $('voiceTtsOpenAiKzModel').value.trim() || null,
language_code: $('voiceTtsOpenAiKzLanguageCode').value.trim() || null,
},
},
};
}
function syncVoiceTtsConfigFields(payload) {
const config = payload?.config || payload;
if (!config) {
return;
}
$('voiceTtsProvider').value = config.provider || 'yandex';
$('voiceTtsYandexRuVoice').value = config.yandex?.ru?.voice || '';
$('voiceTtsYandexKzVoice').value = config.yandex?.kz?.voice || '';
$('voiceTtsElevenRuVoice').value = config.elevenlabs?.ru?.voice || '';
$('voiceTtsElevenRuModel').value = config.elevenlabs?.ru?.model_id || '';
$('voiceTtsElevenRuLanguageCode').value = config.elevenlabs?.ru?.language_code || '';
$('voiceTtsElevenKzVoice').value = config.elevenlabs?.kz?.voice || '';
$('voiceTtsElevenKzModel').value = config.elevenlabs?.kz?.model_id || '';
$('voiceTtsElevenKzLanguageCode').value = config.elevenlabs?.kz?.language_code || '';
$('voiceTtsOpenAiRuVoice').value = config.openai?.ru?.voice || '';
$('voiceTtsOpenAiRuModel').value = config.openai?.ru?.model_id || '';
$('voiceTtsOpenAiRuLanguageCode').value = config.openai?.ru?.language_code || '';
$('voiceTtsOpenAiKzVoice').value = config.openai?.kz?.voice || '';
$('voiceTtsOpenAiKzModel').value = config.openai?.kz?.model_id || '';
$('voiceTtsOpenAiKzLanguageCode').value = config.openai?.kz?.language_code || '';
syncVoiceTtsPresetOptions(payload);
}
function renderVoiceTtsConfigSummary(payload) {
const config = payload?.config || payload;
const source = payload?.source || 'defaults';
const updatedAt = payload?.updated_at || 'не сохранялось';
const activeProvider = config?.provider || 'yandex';
const activeSettings = voiceTtsProviderConfig(config, activeProvider);
$('voiceTtsSummary').textContent = [
`Активный провайдер: ${activeProvider}`,
`RU голос: ${activeSettings?.ru?.voice || 'не задан'}`,
`RU модель: ${activeSettings?.ru?.model_id || 'по умолчанию'}`,
`RU language_code: ${activeSettings?.ru?.language_code || 'по умолчанию'}`,
`KZ голос: ${activeSettings?.kz?.voice || 'не задан'}`,
`KZ модель: ${activeSettings?.kz?.model_id || 'по умолчанию'}`,
`KZ language_code: ${activeSettings?.kz?.language_code || 'по умолчанию'}`,
`Источник конфигурации: ${source}`,
`Последнее обновление: ${updatedAt}`,
].join('\n');
}
async function loadVoiceTtsConfig() {
try {
const data = await api('ai', 'ai/voice/config/tts');
state.voiceTtsConfig = data;
syncVoiceTtsConfigFields(data);
renderVoiceTtsConfigSummary(data);
$('voiceTtsConfigOutput').textContent = JSON.stringify(data, null, 2);
log('Настройки voice TTS загружены', { source: data.source, updated_at: data.updated_at });
} catch (err) {
$('voiceTtsConfigOutput').textContent = err.message;
log('Не удалось загрузить настройки voice TTS', { error: err.message });
}
}
async function saveVoiceTtsConfig() {
try {
const payload = serializeVoiceTtsConfigForm();
const data = await api('ai', 'ai/voice/config/tts', {
method: 'PUT',
body: JSON.stringify(payload),
});
state.voiceTtsConfig = data;
syncVoiceTtsConfigFields(data);
renderVoiceTtsConfigSummary(data);
$('voiceTtsConfigOutput').textContent = JSON.stringify(data, null, 2);
log('Настройки voice TTS сохранены', { provider: data?.config?.provider, updated_at: data.updated_at });
} catch (err) {
$('voiceTtsConfigOutput').textContent = err.message;
log('Не удалось сохранить настройки voice TTS', { error: err.message });
}
}
function resetVoiceTtsConfigForm() {
const snapshot = state.voiceTtsConfig;
if (!snapshot) {
$('voiceTtsConfigOutput').textContent = 'Сначала загрузите текущие настройки voice TTS.';
return;
}
syncVoiceTtsConfigFields(snapshot);
renderVoiceTtsConfigSummary(snapshot);
$('voiceTtsConfigOutput').textContent = JSON.stringify(snapshot, null, 2);
log('Форма voice TTS сброшена к текущим настройкам');
}
function bindVoiceTtsPreset(selectId, inputId) {
const select = $(selectId);
if (!select) {
return;
}
select.addEventListener('change', () => {
if (select.value) {
$(inputId).value = select.value;
}
});
}
function parseIvrFlowJson() {
ensureDefaultIvrFlowJson();
return JSON.parse($('ivrFlowJson').value || '{}');
@@ -1177,6 +1344,7 @@ function wire() {
loadQueues(),
loadIvrFlows(),
loadVoiceNameConfig(),
loadVoiceTtsConfig(),
loadAsteriskStatus(),
loadAsteriskEvents(),
]);
@@ -1204,6 +1372,15 @@ function wire() {
$('loadVoiceNameConfigBtn').addEventListener('click', loadVoiceNameConfig);
$('saveVoiceNameConfigBtn').addEventListener('click', saveVoiceNameConfig);
$('resetVoiceNameConfigBtn').addEventListener('click', resetVoiceNameConfigForm);
$('loadVoiceTtsConfigBtn').addEventListener('click', loadVoiceTtsConfig);
$('saveVoiceTtsConfigBtn').addEventListener('click', saveVoiceTtsConfig);
$('resetVoiceTtsConfigBtn').addEventListener('click', resetVoiceTtsConfigForm);
bindVoiceTtsPreset('voiceTtsYandexRuVoicePreset', 'voiceTtsYandexRuVoice');
bindVoiceTtsPreset('voiceTtsYandexKzVoicePreset', 'voiceTtsYandexKzVoice');
bindVoiceTtsPreset('voiceTtsElevenRuVoicePreset', 'voiceTtsElevenRuVoice');
bindVoiceTtsPreset('voiceTtsElevenKzVoicePreset', 'voiceTtsElevenKzVoice');
bindVoiceTtsPreset('voiceTtsOpenAiRuVoicePreset', 'voiceTtsOpenAiRuVoice');
bindVoiceTtsPreset('voiceTtsOpenAiKzVoicePreset', 'voiceTtsOpenAiKzVoice');
$('loadAsteriskStatusBtn').addEventListener('click', loadAsteriskStatus);
$('loadAsteriskEventsBtn').addEventListener('click', loadAsteriskEvents);
$('loadAsteriskEventDetailBtn').addEventListener('click', loadAsteriskEventDetail);
@@ -1231,6 +1408,7 @@ async function init() {
loadQueues(),
loadIvrFlows(),
loadVoiceNameConfig(),
loadVoiceTtsConfig(),
loadAsteriskStatus(),
loadAsteriskEvents(),
]);