mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-22 17:16:19 -07:00
- Add backend selection, setup gating, Anki integration, and external host support - Add launcher flags, documentation, packaging, and focused tests - Open on-demand overlay modals on the first attempt
49 lines
2.2 KiB
JavaScript
49 lines
2.2 KiB
JavaScript
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
// Chrome may return an empty list until its first voiceschanged event. Keep
|
|
// this wait inside the caller's existing deadline and cancellation.
|
|
function waitForSpeechVoices(speech, signal) {
|
|
signal.throwIfAborted();
|
|
return new Promise((resolve, reject) => {
|
|
function clean() {
|
|
speech.removeEventListener("voiceschanged", changed);
|
|
signal.removeEventListener("abort", aborted);
|
|
}
|
|
function changed() { clean(); resolve(speech.getVoices()); }
|
|
function aborted() { clean(); reject(signal.reason); }
|
|
speech.addEventListener("voiceschanged", changed, { once: true });
|
|
signal.addEventListener("abort", aborted, { once: true });
|
|
// The voice service can finish loading between the initial query and the
|
|
// listener registration; checking again avoids missing that transition.
|
|
if (speech.getVoices().length) changed();
|
|
});
|
|
}
|
|
|
|
export async function resolveSpeech(window, source, term, signal) {
|
|
const speech = window.speechSynthesis;
|
|
let voices = speech.getVoices();
|
|
if (!voices.length) voices = await waitForSpeechVoices(speech, signal);
|
|
signal.throwIfAborted();
|
|
if (voices.length === 0) throw new Error("No speech voices are available in this browser.");
|
|
const text = source.type === "text-to-speech-reading" ? term.reading || term.expression : term.expression;
|
|
const utterance = new window.SpeechSynthesisUtterance(text);
|
|
utterance.lang = "ja-JP";
|
|
const japanese = voices.filter(voice => /^ja(?:[-_]|$)/i.test(voice.lang));
|
|
const voice = source.voice
|
|
? voices.find(value => value.voiceURI === source.voice || value.name === source.voice)
|
|
: japanese.find(value => /^Google\b/i.test(value.name))
|
|
|| japanese.find(value => value.default) || japanese[0];
|
|
if (source.voice && !voice) {
|
|
throw new Error("The selected speech voice is no longer available. Choose another voice in Audio Settings.");
|
|
}
|
|
if (!voice) {
|
|
throw new Error("No Japanese speech voice is available. Install a Japanese voice or add an audio provider in Audio Settings.");
|
|
}
|
|
utterance.voice = voice;
|
|
return {
|
|
speech,
|
|
utterance,
|
|
candidate: { name: voice.name, text, voice: source.voice, index: 0 },
|
|
};
|
|
}
|