Listening beats reading when you're away from the screen. OmniBrief lets you play any brief as audio - and because the browser already ships a speech engine, that costs nothing: no audio files, no cloud TTS API, no per-character billing.
The engine is the Web Speech API's SpeechSynthesis interface. It's delightfully simple to start with and has two sharp edges worth knowing: the voice list loads asynchronously, and the pause/resume state is unreliable to read back. Here's how OmniBrief handles both.
Key Technical Concepts Covered
- SpeechSynthesis basics: speak an utterance with a chosen voice and rate.
- The async voice list:
getVoices()is often empty on first call. - Voice ranking: prefer higher-quality voices per language.
- Tracking state yourself: don't trust
speechSynthesis.paused.
Part of the engineering behind OmniBrief.
The Async Voice List
The first surprise: speechSynthesis.getVoices() frequently returns an empty array on the first call because voices load asynchronously. You must also listen for the voiceschanged event and re-read the list. OmniBrief sorts the result so higher-quality voices surface first:
export function getVoices() {
if (typeof speechSynthesis === 'undefined') return [];
return [...speechSynthesis.getVoices()].sort((a, b) => {
// Prefer Google / natural voices, then alphabetical
const score = (v) => (/google|natural|premium/i.test(v.name) ? 0 : 1);
return score(a) - score(b) || a.name.localeCompare(b.name);
});
}
export function onVoicesChanged(fn) {
if (typeof speechSynthesis === 'undefined') return () => {};
speechSynthesis.addEventListener('voiceschanged', fn);
return () => speechSynthesis.removeEventListener('voiceschanged', fn);
}
Speaking an Utterance
With a voice chosen, playback is a SpeechSynthesisUtterance handed to speechSynthesis.speak(). Rate, pitch, and voice are all set on the utterance:
function speak(text, voice, rate = 1) {
speechSynthesis.cancel(); // clear any queued speech
const u = new SpeechSynthesisUtterance(text);
if (voice) u.voice = voice;
u.rate = rate;
u.onend = () => setPlaying(false);
speechSynthesis.speak(u);
}
Track Pause State Yourself
The second sharp edge: speechSynthesis.paused and .speaking are inconsistent across engines, so driving a play/pause button off them produces flicker and wrong states. OmniBrief keeps its own boolean and toggles it alongside the API calls:
let isPaused = false;
function togglePause() {
if (isPaused) {
speechSynthesis.resume();
isPaused = false;
} else {
speechSynthesis.pause();
isPaused = true;
}
return isPaused; // source of truth for the UI
}
Conclusion: Free, Local, and Instant
The Web Speech API turns any brief into audio with a few lines and no infrastructure. Handle the asynchronous voice list, rank voices for quality, and own your pause state rather than reading it back - and you get a reliable read-aloud feature that costs nothing and works offline.