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) => ( `` )).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 = ` `; const card2 = document.createElement('div'); card2.className = 'card stack'; card2.innerHTML = `

Для офлайн-озвучки через Piper используйте локальный HTTP-обёртчик. Кнопка «шаблон» подставляет базовые значения.

`; const actions = document.createElement('div'); actions.className = 'auth-footer-actions'; actions.innerHTML = ` `; 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; }