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
1.0
1.0
1.0
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';
  }