Utility
TEXT TO Speech
Listen to any text using your browser's built-in speech synthesis. Choose voice, speed, pitch, and volume.
Your browser does not support the Web Speech API. Please use Chrome, Edge, or Safari.
0 / 5000 characters
Ready
Powered by the browser's Web Speech API. Available voices depend on your OS and browser. Speech is processed entirely on-device — no data is sent to any server.
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
const MAX_CHARS = 5000;
let synth = window.speechSynthesis;
let voices = [];
let allVoices = [];
let currentUtterance = null;
let isPaused = false;
let voiceFilter = 'all';
// Check support
if (!synth) {
document.getElementById('noSupportAlert').style.display = 'block';
}
// Load voices
function loadVoices() {
allVoices = synth.getVoices();
if (allVoices.length === 0) return;
populateVoiceSelect(voiceFilter);
}
function populateVoiceSelect(filter) {
const sel = document.getElementById('voiceSelect');
let filtered = allVoices;
if (filter === 'en') filtered = allVoices.filter(v => v.lang.startsWith('en'));
if (filter === 'local') filtered = allVoices.filter(v => v.localService);
if (filtered.length === 0) filtered = allVoices;
voices = filtered;
sel.innerHTML = '';
filtered.forEach((v, i) => {
const opt = document.createElement('option');
opt.value = i;
opt.textContent = v.name + ' (' + v.lang + ')' + (v.localService ? '' : ' [online]');
sel.appendChild(opt);
});
// Prefer English default
const engIdx = filtered.findIndex(v => v.lang.startsWith('en'));
if (engIdx >= 0) sel.value = engIdx;
}
function filterVoices(type, btn) {
document.querySelectorAll('.voice-filter .toggle-item').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
voiceFilter = type;
populateVoiceSelect(type);
}
if (synth) {
synth.onvoiceschanged = loadVoices;
loadVoices();
// Some browsers load voices sync
setTimeout(loadVoices, 200);
}
function onVoiceChange() {}
function updateCharCount() {
const len = document.getElementById('textInput').value.length;
const el = document.getElementById('charCount');
el.textContent = len + ' / ' + MAX_CHARS + ' characters';
el.className = 'char-count';
if (len > MAX_CHARS * 0.9) el.classList.add('warn');
if (len >= MAX_CHARS) el.classList.add('danger');
if (len > MAX_CHARS) {
document.getElementById('textInput').value = document.getElementById('textInput').value.slice(0, MAX_CHARS);
updateCharCount();
}
}
function updateSliderVal(type, val) {
const displayVal = parseFloat(val).toFixed(1);
document.getElementById(type + 'Val').textContent = displayVal;
}
function getSelectedVoice() {
const sel = document.getElementById('voiceSelect');
const idx = parseInt(sel.value);
return voices[idx] || null;
}
function setStatus(state) {
const dot = document.getElementById('statusDot');
const text = document.getElementById('statusText');
const fill = document.getElementById('progressFill');
dot.className = 'status-dot ' + state;
if (state === 'speaking') {
text.textContent = 'Speaking...';
fill.classList.add('animating');
fill.style.width = '100%';
} else if (state === 'paused') {
text.textContent = 'Paused';
fill.classList.remove('animating');
fill.style.width = '60%';
} else {
text.textContent = 'Ready';
fill.classList.remove('animating');
fill.style.width = '0%';
}
}
function play() {
if (!synth) return;
const text = document.getElementById('textInput').value.trim();
if (!text) { document.getElementById('textInput').focus(); return; }
synth.cancel();
isPaused = false;
const utter = new SpeechSynthesisUtterance(text);
const voice = getSelectedVoice();
if (voice) utter.voice = voice;
utter.rate = parseFloat(document.getElementById('rateSlider').value);
utter.pitch = parseFloat(document.getElementById('pitchSlider').value);
utter.volume = parseFloat(document.getElementById('volumeSlider').value);
utter.onstart = () => {
setStatus('speaking');
document.getElementById('playBtn').disabled = true;
document.getElementById('pauseBtn').disabled = false;
document.getElementById('stopBtn').disabled = false;
document.getElementById('pauseBtn').textContent = '\u23F8 Pause';
};
utter.onend = () => {
setStatus('stopped');
document.getElementById('playBtn').disabled = false;
document.getElementById('pauseBtn').disabled = true;
document.getElementById('stopBtn').disabled = true;
isPaused = false;
};
utter.onerror = (e) => {
if (e.error !== 'interrupted') {
setStatus('stopped');
document.getElementById('playBtn').disabled = false;
document.getElementById('pauseBtn').disabled = true;
document.getElementById('stopBtn').disabled = true;
}
};
currentUtterance = utter;
synth.speak(utter);
}
function pauseResume() {
if (!synth) return;
if (isPaused) {
synth.resume();
isPaused = false;
setStatus('speaking');
document.getElementById('pauseBtn').textContent = '\u23F8 Pause';
} else {
synth.pause();
isPaused = true;
setStatus('paused');
document.getElementById('pauseBtn').textContent = '\u25B6 Resume';
}
}
function stop() {
if (!synth) return;
synth.cancel();
isPaused = false;
setStatus('stopped');
document.getElementById('playBtn').disabled = false;
document.getElementById('pauseBtn').disabled = true;
document.getElementById('stopBtn').disabled = true;
document.getElementById('pauseBtn').textContent = '\u23F8 Pause';
}