SHA256
UI: голосовой ввод/STT, TTS через OpenAI, настройки инструментов + учёт недопроверенных фич
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import { saveEntrySettings, state } from '../state.js';
|
||||
import { speakTextBySettings } from '../services/speech-tools-service.js';
|
||||
|
||||
export const pageMeta = { id: 'tools-settings-view', title: 'Настройки инструментов' };
|
||||
|
||||
function optionsMarkup(options, selected) {
|
||||
return options.map((opt) => (
|
||||
`<option value="${opt.value}" ${selected === opt.value ? 'selected' : ''}>${opt.label}</option>`
|
||||
)).join('');
|
||||
}
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const stt = state.entrySettings.tools?.speechToText || {};
|
||||
const tts = state.entrySettings.tools?.textToSpeech || {};
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card stack';
|
||||
card.innerHTML = `
|
||||
<h3 class="modal-title">Распознавание речи</h3>
|
||||
<label class="field-label">Провайдер</label>
|
||||
<select class="input" id="stt-provider">
|
||||
${optionsMarkup([{ value: 'openai', label: 'OpenAI (Whisper/Transcribe)' }], String(stt.provider || 'openai'))}
|
||||
</select>
|
||||
<label class="field-label">Уровень</label>
|
||||
<select class="input" id="stt-quality">
|
||||
${optionsMarkup([
|
||||
{ value: 'easy', label: 'Easy (дешевле)' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'hard', label: 'Hard (лучше)' },
|
||||
], String(stt.quality || 'medium'))}
|
||||
</select>
|
||||
<label class="field-label">Адрес OpenAI API</label>
|
||||
<input class="input" id="stt-base-url" type="text" value="${String(stt.baseUrl || 'https://api.openai.com/v1')}" placeholder="https://api.openai.com/v1" />
|
||||
<label class="field-label">Кастомная модель (опционально)</label>
|
||||
<input class="input" id="stt-model" type="text" value="${String(stt.model || '')}" placeholder="например: gpt-4o-mini-transcribe" />
|
||||
<label class="field-label">API key</label>
|
||||
<input class="input" id="stt-api-key" type="password" value="${String(stt.apiKey || '')}" placeholder="sk-..." />
|
||||
`;
|
||||
|
||||
const card2 = document.createElement('div');
|
||||
card2.className = 'card stack';
|
||||
card2.innerHTML = `
|
||||
<h3 class="modal-title">Прочесть вслух (TTS)</h3>
|
||||
<label class="field-label">Провайдер</label>
|
||||
<select class="input" id="tts-provider">
|
||||
${optionsMarkup([
|
||||
{ value: 'browser', label: 'Браузер (SpeechSynthesis)' },
|
||||
{ value: 'piper-http', label: 'Piper (локальный HTTP)' },
|
||||
{ value: 'openai', label: 'OpenAI TTS API' },
|
||||
], String(tts.provider || 'browser'))}
|
||||
</select>
|
||||
<label class="field-label">Уровень</label>
|
||||
<select class="input" id="tts-quality">
|
||||
${optionsMarkup([
|
||||
{ value: 'easy', label: 'Easy' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'hard', label: 'Hard' },
|
||||
], String(tts.quality || 'medium'))}
|
||||
</select>
|
||||
<label class="field-label">Голос</label>
|
||||
<input class="input" id="tts-voice" type="text" value="${String(tts.voice || '')}" placeholder="например: andrey / alloy" />
|
||||
<label class="field-label">Piper HTTP адрес</label>
|
||||
<input class="input" id="tts-piper-url" type="text" value="${String(tts.piperBaseUrl || 'http://127.0.0.1:5000')}" placeholder="http://127.0.0.1:5000" />
|
||||
<label class="field-label">OpenAI/внешний API адрес</label>
|
||||
<input class="input" id="tts-external-url" type="text" value="${String(tts.externalBaseUrl || '')}" placeholder="https://api.openai.com/v1" />
|
||||
<label class="field-label">API key (для внешнего API)</label>
|
||||
<input class="input" id="tts-api-key" type="password" value="${String(tts.apiKey || '')}" placeholder="sk-..." />
|
||||
<label class="field-label">Модель (для внешнего API)</label>
|
||||
<input class="input" id="tts-model" type="text" value="${String(tts.model || '')}" placeholder="например: gpt-4o-mini-tts" />
|
||||
<label class="field-label">Тестовая фраза</label>
|
||||
<input class="input" id="tts-test-text" type="text" value="Привет! Проверка озвучки работает." />
|
||||
<div class="row wrap-row">
|
||||
<button class="ghost-btn" type="button" id="tts-test-btn">Проверить озвучку</button>
|
||||
<button class="ghost-btn" type="button" id="piper-autofill">Загрузить и настроить (шаблон)</button>
|
||||
<button class="ghost-btn" type="button" id="piper-links">Ссылки на Piper/голоса</button>
|
||||
</div>
|
||||
<p class="meta-muted">Для офлайн-озвучки через Piper используйте локальный HTTP-обёртчик. Кнопка «шаблон» подставляет базовые значения.</p>
|
||||
`;
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'auth-footer-actions';
|
||||
actions.innerHTML = `
|
||||
<button class="ghost-btn" type="button" id="tools-cancel">Отмена</button>
|
||||
<button class="primary-btn" type="button" id="tools-save">Сохранить</button>
|
||||
`;
|
||||
|
||||
actions.querySelector('#tools-cancel')?.addEventListener('click', () => navigate('settings-view'));
|
||||
actions.querySelector('#tools-save')?.addEventListener('click', async () => {
|
||||
const next = {
|
||||
...state.entrySettings,
|
||||
tools: {
|
||||
speechToText: {
|
||||
provider: card.querySelector('#stt-provider')?.value || 'openai',
|
||||
quality: card.querySelector('#stt-quality')?.value || 'medium',
|
||||
baseUrl: String(card.querySelector('#stt-base-url')?.value || '').trim(),
|
||||
apiKey: String(card.querySelector('#stt-api-key')?.value || '').trim(),
|
||||
model: String(card.querySelector('#stt-model')?.value || '').trim(),
|
||||
},
|
||||
textToSpeech: {
|
||||
provider: card2.querySelector('#tts-provider')?.value || 'browser',
|
||||
quality: card2.querySelector('#tts-quality')?.value || 'medium',
|
||||
voice: String(card2.querySelector('#tts-voice')?.value || '').trim(),
|
||||
piperBaseUrl: String(card2.querySelector('#tts-piper-url')?.value || '').trim(),
|
||||
externalBaseUrl: String(card2.querySelector('#tts-external-url')?.value || '').trim(),
|
||||
apiKey: String(card2.querySelector('#tts-api-key')?.value || '').trim(),
|
||||
model: String(card2.querySelector('#tts-model')?.value || '').trim(),
|
||||
},
|
||||
},
|
||||
};
|
||||
await saveEntrySettings(next);
|
||||
navigate('settings-view');
|
||||
});
|
||||
|
||||
card2.querySelector('#piper-autofill')?.addEventListener('click', () => {
|
||||
card2.querySelector('#tts-provider').value = 'piper-http';
|
||||
card2.querySelector('#tts-piper-url').value = 'http://127.0.0.1:5000';
|
||||
card2.querySelector('#tts-quality').value = 'medium';
|
||||
if (!String(card2.querySelector('#tts-voice').value || '').trim()) {
|
||||
card2.querySelector('#tts-voice').value = 'ru_RU-irina-medium';
|
||||
}
|
||||
});
|
||||
|
||||
card2.querySelector('#piper-links')?.addEventListener('click', () => {
|
||||
window.open('https://github.com/rhasspy/piper', '_blank', 'noopener,noreferrer');
|
||||
window.open('https://huggingface.co/rhasspy/piper-voices/tree/main', '_blank', 'noopener,noreferrer');
|
||||
});
|
||||
|
||||
card2.querySelector('#tts-test-btn')?.addEventListener('click', async () => {
|
||||
const ttsProvider = card2.querySelector('#tts-provider')?.value || 'openai';
|
||||
const text = String(card2.querySelector('#tts-test-text')?.value || '').trim();
|
||||
const runtimeSettings = {
|
||||
...state.entrySettings,
|
||||
tools: {
|
||||
...state.entrySettings.tools,
|
||||
textToSpeech: {
|
||||
provider: ttsProvider,
|
||||
quality: card2.querySelector('#tts-quality')?.value || 'medium',
|
||||
voice: String(card2.querySelector('#tts-voice')?.value || '').trim(),
|
||||
piperBaseUrl: String(card2.querySelector('#tts-piper-url')?.value || '').trim(),
|
||||
externalBaseUrl: String(card2.querySelector('#tts-external-url')?.value || '').trim(),
|
||||
apiKey: String(card2.querySelector('#tts-api-key')?.value || '').trim(),
|
||||
model: String(card2.querySelector('#tts-model')?.value || '').trim(),
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
await speakTextBySettings(text || 'Проверка озвучки', runtimeSettings);
|
||||
} catch (error) {
|
||||
window.alert(`Ошибка озвучки: ${error?.message || 'unknown'}`);
|
||||
}
|
||||
});
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Настройки инструментов',
|
||||
leftAction: { label: '←', onClick: () => navigate('settings-view') },
|
||||
}),
|
||||
card,
|
||||
card2,
|
||||
actions,
|
||||
);
|
||||
|
||||
return screen;
|
||||
}
|
||||
Reference in New Issue
Block a user