egorin Рация: качественный движок чтения — умный выбор голоса (приоритет онлайн/нейронных ru), последовательное чтение очереди через onend-цепочку
This commit is contained in:
+42
-9
@@ -46,18 +46,47 @@ function chime(sev: MonitorEvent['severity']) {
|
||||
}
|
||||
}
|
||||
|
||||
export function speak(text: string) {
|
||||
// Умный выбор голоса: приоритет качества — онлайн/нейронные (Google/Yandex/Microsoft),
|
||||
// затем именованные женские, затем любой ru. Кэш + переизбор на voiceschanged.
|
||||
let cachedVoice: SpeechSynthesisVoice | null = null;
|
||||
let voiceHooked = false;
|
||||
function pickVoice(): SpeechSynthesisVoice | null {
|
||||
if (!('speechSynthesis' in window)) return null;
|
||||
const ru = window.speechSynthesis.getVoices().filter((v) => v.lang.toLowerCase().startsWith('ru'));
|
||||
if (!ru.length) return null;
|
||||
const score = (v: SpeechSynthesisVoice) => {
|
||||
const n = v.name.toLowerCase();
|
||||
let s = 0;
|
||||
if (!v.localService) s += 4; // remote-голоса обычно качественнее
|
||||
if (/google|yandex|neural|natural|online|premium/.test(n)) s += 3;
|
||||
if (/milena|dariya|alena|katya|irina|svetlana|tatyana|elena/.test(n)) s += 2;
|
||||
if (/microsoft/.test(n)) s += 1;
|
||||
return s;
|
||||
};
|
||||
return ru.slice().sort((a, b) => score(b) - score(a))[0];
|
||||
}
|
||||
function bestVoice(): SpeechSynthesisVoice | null {
|
||||
if (!cachedVoice) cachedVoice = pickVoice();
|
||||
if (!voiceHooked && 'speechSynthesis' in window) {
|
||||
voiceHooked = true;
|
||||
window.speechSynthesis.addEventListener('voiceschanged', () => { cachedVoice = pickVoice(); });
|
||||
}
|
||||
return cachedVoice;
|
||||
}
|
||||
|
||||
export function speak(text: string, opts?: { onend?: () => void }) {
|
||||
try {
|
||||
if (!('speechSynthesis' in window)) return;
|
||||
if (!('speechSynthesis' in window) || !text) { opts?.onend?.(); return; }
|
||||
const u = new SpeechSynthesisUtterance(text);
|
||||
u.lang = 'ru-RU';
|
||||
u.rate = 1;
|
||||
u.pitch = 1;
|
||||
const ru = window.speechSynthesis.getVoices().find((v) => v.lang.toLowerCase().startsWith('ru'));
|
||||
if (ru) u.voice = ru;
|
||||
u.rate = 1.0;
|
||||
u.pitch = 1.03; // чуть живее, менее «робот»
|
||||
const v = bestVoice();
|
||||
if (v) u.voice = v;
|
||||
if (opts?.onend) { u.onend = () => opts.onend!(); u.onerror = () => opts.onend!(); }
|
||||
window.speechSynthesis.speak(u);
|
||||
} catch {
|
||||
/* ignore */
|
||||
opts?.onend?.();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,8 +180,12 @@ export const useRadio = create<RadioState>((set, get) => ({
|
||||
clearRead: () => set({ readList: [] }),
|
||||
readAll: () => {
|
||||
ctx(); // прайм аудио на жесте
|
||||
const list = get().readList;
|
||||
list.forEach((t, i) => setTimeout(() => speak(t), i * 200)); // speechSynthesis сам ставит в очередь
|
||||
if (!('speechSynthesis' in window)) return;
|
||||
window.speechSynthesis.cancel(); // сбросить прошлую очередь
|
||||
const list = get().readList.slice();
|
||||
let i = 0;
|
||||
const next = () => { if (i >= list.length) return; speak(list[i++], { onend: next }); }; // последовательно, без наложения
|
||||
next();
|
||||
},
|
||||
toggle: () => {
|
||||
const on = !get().on;
|
||||
|
||||
Reference in New Issue
Block a user