feat(voice): add admin-configurable elevenlabs tts
This commit is contained in:
+178
@@ -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(),
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user