SHA256
96 lines
3.3 KiB
JavaScript
96 lines
3.3 KiB
JavaScript
const PIPER_LENGTH_SCALE_BY_QUALITY = {
|
|
easy: '1.15',
|
|
medium: '1.0',
|
|
hard: '0.9',
|
|
};
|
|
|
|
function normalizeOpenAiBaseUrl(url) {
|
|
const value = String(url || '').trim();
|
|
if (!value) return 'https://api.openai.com/v1';
|
|
return value.replace(/\/+$/, '');
|
|
}
|
|
|
|
export function isTextToSpeechConfigured(entrySettings) {
|
|
const cfg = entrySettings?.tools?.textToSpeech || {};
|
|
const provider = String(cfg.provider || 'browser');
|
|
if (provider === 'browser') return true;
|
|
if (provider === 'piper-http') return !!String(cfg.piperBaseUrl || '').trim();
|
|
if (provider === 'openai') return !!String(cfg.apiKey || '').trim();
|
|
return false;
|
|
}
|
|
|
|
export async function speakTextBySettings(text, entrySettings) {
|
|
const value = String(text || '').trim();
|
|
if (!value) return;
|
|
|
|
const cfg = entrySettings?.tools?.textToSpeech || {};
|
|
const provider = String(cfg.provider || 'browser');
|
|
|
|
if (provider === 'browser') {
|
|
const utt = new SpeechSynthesisUtterance(value);
|
|
utt.lang = 'ru-RU';
|
|
const selected = String(cfg.voice || '').trim();
|
|
if (selected) {
|
|
const voice = window.speechSynthesis.getVoices().find((v) => v.name === selected);
|
|
if (voice) utt.voice = voice;
|
|
}
|
|
window.speechSynthesis.speak(utt);
|
|
return;
|
|
}
|
|
|
|
if (provider === 'piper-http') {
|
|
const baseUrl = String(cfg.piperBaseUrl || '').trim().replace(/\/+$/, '');
|
|
if (!baseUrl) throw new Error('Не указан адрес Piper HTTP.');
|
|
const quality = String(cfg.quality || 'medium').toLowerCase();
|
|
const voice = String(cfg.voice || '').trim();
|
|
const lengthScale = PIPER_LENGTH_SCALE_BY_QUALITY[quality] || PIPER_LENGTH_SCALE_BY_QUALITY.medium;
|
|
const resp = await fetch(`${baseUrl}/api/tts`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
text: value,
|
|
voice,
|
|
quality,
|
|
length_scale: lengthScale,
|
|
}),
|
|
});
|
|
if (!resp.ok) throw new Error(`Piper HTTP недоступен (${resp.status}).`);
|
|
const blob = await resp.blob();
|
|
const audioUrl = URL.createObjectURL(blob);
|
|
const audio = new Audio(audioUrl);
|
|
await audio.play();
|
|
window.setTimeout(() => URL.revokeObjectURL(audioUrl), 30000);
|
|
return;
|
|
}
|
|
|
|
if (provider === 'openai') {
|
|
const apiKey = String(cfg.apiKey || '').trim();
|
|
if (!apiKey) throw new Error('Не заполнен API key для OpenAI TTS.');
|
|
const model = String(cfg.model || '').trim() || 'gpt-4o-mini-tts';
|
|
const baseUrl = normalizeOpenAiBaseUrl(cfg.externalBaseUrl || cfg.baseUrl || 'https://api.openai.com/v1');
|
|
const voice = String(cfg.voice || '').trim() || 'alloy';
|
|
const resp = await fetch(`${baseUrl}/audio/speech`, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${apiKey}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
model,
|
|
voice,
|
|
input: value,
|
|
format: 'mp3',
|
|
}),
|
|
});
|
|
if (!resp.ok) throw new Error(`OpenAI TTS недоступен (${resp.status}).`);
|
|
const blob = await resp.blob();
|
|
const audioUrl = URL.createObjectURL(blob);
|
|
const audio = new Audio(audioUrl);
|
|
await audio.play();
|
|
window.setTimeout(() => URL.revokeObjectURL(audioUrl), 30000);
|
|
return;
|
|
}
|
|
|
|
throw new Error('Неизвестный провайдер озвучки.');
|
|
}
|