La API Web Speech: Reconocimiento y Síntesis de Voz en el Navegador
12 min read

La API Web Speech: Reconocimiento y Síntesis de Voz en el Navegador

Resumen

Las interfaces web han evolucionado más allá de los clics y las pulsaciones de teclado. La API Web Speech, desarrollada por el W3C, proporciona dos subsistemas independientes para integrar voz en cualquier aplicación web: SpeechRecognition (speech-to-text) para capturar y transcribir la voz del usuario, y SpeechSynthesis (text-to-speech) para leer texto en voz alta usando las voces disponibles del sistema.

Este artículo cubre la arquitectura de ambos subsistemas, muestra implementaciones prácticas con código y aborda problemas críticos como la fragmentación entre navegadores, el bug de los 15 segundos en Chrome y las restricciones de iOS.


1. Dos Subsistemas Independientes

La API se divide en dos módulos completamente separados:

SubsistemaDirecciónInterfazSoporte
SpeechRecognitionUsuario → App (entrada de micrófono)SpeechRecognition / webkitSpeechRecognitionLimitado (principalmente Chromium)
SpeechSynthesisApp → Usuario (salida de audio)window.speechSynthesis + SpeechSynthesisUtteranceUniversal (Baseline)

Nota: SpeechSynthesis tiene adopción universal en todos los navegadores modernos. SpeechRecognition, sin embargo, sigue siendo experimental con soporte fragmentado. Siempre implementa detección de características antes de usar cualquiera de los dos.


2. Reconocimiento de Voz (Speech-to-Text)

2.1 Detección de Soporte e Instanciación

Debido al uso de prefijos de vendors, el constructor debe resolverse de forma defensiva:

const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;

if (!SpeechRecognition) {
  console.error('SpeechRecognition no está soportado en este navegador.');
}

2.2 Propiedades de Configuración

Una vez instanciado, el motor de reconocimiento se configura a través de cuatro propiedades clave:

PropiedadTipoDescripción
langstringEtiqueta BCP 47 del idioma (ej. "en-US", "es-ES"). Por defecto usa el atributo lang del documento.
continuousbooleanSi es true, sigue escuchando tras devolver resultados. Por defecto: false (se detiene tras un resultado).
interimResultsbooleanSi es true, emite transcripciones provisionales en tiempo real antes de la finalización.
maxAlternativesnumberNúmero de transcripciones alternativas por segmento de resultado. Por defecto: 1.
const recognition = new SpeechRecognition();
recognition.lang = 'es-ES';
recognition.continuous = true;
recognition.interimResults = true;
recognition.maxAlternatives = 1;

2.3 La Jerarquía de Resultados

Los resultados no son strings planos. Llegan como un árbol de objetos con tres niveles:

  1. SpeechRecognitionResultList — una colección tipo lista accesible mediante event.results
  2. SpeechRecognitionResult — cada elemento contiene un booleano isFinal y una o más alternativas
  3. SpeechRecognitionAlternative — contiene el string transcript y un puntaje de confidence (0 a 1)
recognition.onresult = (event) => {
  for (let i = event.resultIndex; i < event.results.length; i++) {
    const result = event.results[i];
    const transcript = result[0].transcript;
    const confidence = result[0].confidence;

    if (result.isFinal) {
      console.log(`Final: "${transcript}" (${(confidence * 100).toFixed(1)}%)`);
    } else {
      console.log(`Provisional: "${transcript}"`);
    }
  }
};

2.4 Ciclo de Vida de Eventos

El motor de reconocimiento dispara eventos en una secuencia determinista:

start → audiostart → soundstart → speechstart

  result (se repite mientras se habla)

speechend → soundend → audioend → end

Eventos clave a manejar:

recognition.onspeechstart = () => {
  console.log('Voz detectada — transcripción en progreso...');
};

recognition.onerror = (event) => {
  switch (event.error) {
    case 'not-allowed':
      console.error('Acceso al micrófono denegado por el usuario o política.');
      break;
    case 'network':
      console.error('Servicio de reconocimiento en la nube inalcanzable.');
      break;
    case 'no-speech':
      console.warn('No se detectó voz — solo silencio o ruido de fondo.');
      break;
    case 'aborted':
      console.warn('El reconocimiento fue abortado.');
      break;
  }
};

recognition.onnomatch = () => {
  console.warn('Se detectó habla pero no pudo ser reconocida.');
};

recognition.onend = () => {
  console.log('Sesión de reconocimiento finalizada.');
};

2.5 Ejemplo Completo: Dictado en Vivo

const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;

if (!SpeechRecognition) {
  document.getElementById('status').textContent = 'Reconocimiento de voz no soportado.';
} else {
  const recognition = new SpeechRecognition();
  recognition.lang = 'es-ES';
  recognition.continuous = true;
  recognition.interimResults = true;

  const finalOutput = document.getElementById('final');
  const interimOutput = document.getElementById('interim');
  let finalTranscript = '';

  recognition.onresult = (event) => {
    let interim = '';

    for (let i = event.resultIndex; i < event.results.length; i++) {
      const transcript = event.results[i][0].transcript;

      if (event.results[i].isFinal) {
        finalTranscript += transcript;
      } else {
        interim += transcript;
      }
    }

    finalOutput.textContent = finalTranscript;
    interimOutput.textContent = interim;
  };

  recognition.onerror = (event) => {
    document.getElementById('status').textContent = `Error: ${event.error}`;
  };

  document.getElementById('start-btn').addEventListener('click', () => {
    finalTranscript = '';
    recognition.start();
    document.getElementById('status').textContent = 'Escuchando...';
  });

  document.getElementById('stop-btn').addEventListener('click', () => {
    recognition.stop();
    document.getElementById('status').textContent = 'Detenido.';
  });
}

3. Síntesis de Voz (Text-to-Speech)

3.1 El Controlador Singleton

A diferencia de SpeechRecognition, el motor de síntesis se accede a través de una única instancia global:

const synth = window.speechSynthesis;

Este controlador gestiona una cola FIFO de locuciones y expone tres flags de estado de solo lectura:

PropiedadDescripción
synth.pendingtrue si hay locuciones esperando en la cola
synth.speakingtrue si se está produciendo audio actualmente
synth.pausedtrue si la reproducción ha sido pausada

Métodos de control:

synth.speak(utterance);
synth.pause();
synth.resume();
synth.cancel();

3.2 Creando Locuciones

El objeto SpeechSynthesisUtterance es la unidad de habla. Contiene el texto y toda la configuración prosódica:

PropiedadRangoDescripción
textStringEl contenido de texto a pronunciar
voiceSpeechSynthesisVoiceEl perfil de voz a utilizar
langString (BCP 47)Sobreescritura del idioma
pitch0 – 2Tono de voz (1 = normal)
rate0.1 – 10Velocidad de habla (1 = normal)
volume0 – 1Volumen de audio (1 = máximo)
const utterance = new SpeechSynthesisUtterance('Hola, bienvenido a la aplicación.');
utterance.lang = 'es-ES';
utterance.pitch = 1;
utterance.rate = 1;
utterance.volume = 0.8;

synth.speak(utterance);

3.3 Cargando Voces de Forma Asíncrona

Las voces se cargan asincrónicamente por el navegador (especialmente en Chromium). Una llamada síncrona a getVoices() durante la carga de la página a menudo devuelve un array vacío. Debes escuchar el evento voiceschanged:

const synth = window.speechSynthesis;
let voices = [];

const loadVoices = () => {
  voices = synth.getVoices();
  console.log(`${voices.length} voces cargadas.`);
};

loadVoices();

if (synth.onvoiceschanged !== undefined) {
  synth.onvoiceschanged = loadVoices;
}

Cada objeto SpeechSynthesisVoice contiene:

voices.forEach((voice) => {
  console.log(`${voice.name} (${voice.lang}) ${voice.default ? '— POR DEFECTO' : ''}`);
});

3.4 Ejemplo Completo: Lector de Texto con Selección de Voz

const synth = window.speechSynthesis;
let voices = [];

const voiceSelect = document.getElementById('voice-select');
const textInput = document.getElementById('text-input');

const populateVoices = () => {
  voices = synth.getVoices();
  voiceSelect.innerHTML = '';

  voices.forEach((voice, index) => {
    const option = document.createElement('option');
    option.value = index;
    option.textContent = `${voice.name} (${voice.lang})`;
    voiceSelect.appendChild(option);
  });
};

populateVoices();
if (synth.onvoiceschanged !== undefined) {
  synth.onvoiceschanged = populateVoices;
}

document.getElementById('speak-btn').addEventListener('click', () => {
  synth.cancel();

  const utterance = new SpeechSynthesisUtterance(textInput.value);
  utterance.voice = voices[voiceSelect.value];
  utterance.pitch = parseFloat(document.getElementById('pitch').value);
  utterance.rate = parseFloat(document.getElementById('rate').value);

  utterance.onstart = () => {
    document.getElementById('status').textContent = 'Hablando...';
  };

  utterance.onend = () => {
    document.getElementById('status').textContent = 'Finalizado.';
  };

  utterance.onerror = (event) => {
    document.getElementById('status').textContent = `Error: ${event.error}`;
  };

  synth.speak(utterance);
});

document.getElementById('stop-btn').addEventListener('click', () => {
  synth.cancel();
  document.getElementById('status').textContent = 'Cancelado.';
});

4. Procesamiento en la Nube vs. en el Dispositivo

SpeechRecognition puede enrutar el audio a través de dos pipelines radicalmente diferentes:

4.1 Procesamiento en la Nube (Por Defecto)

Por defecto, Chrome serializa el flujo de audio y lo envía a los servidores en la nube de Google para su transcripción. Esto proporciona alta precisión (modelos neuronales profundos, enormes datasets de entrenamiento) pero introduce compromisos:

  • Latencia: los viajes de ida y vuelta por red degradan las experiencias en tiempo real
  • Privacidad: los datos de audio crudos salen del dispositivo
  • Sin conexión: completamente inoperante sin internet

4.2 Procesamiento en el Dispositivo (Experimental)

Los navegadores modernos están introduciendo inferencia local usando modelos en el dispositivo. Esto elimina la latencia de red, funciona sin conexión y mantiene el audio privado:

const recognition = new SpeechRecognition();

const availability = await SpeechRecognition.available();

if (availability === 'available') {
  recognition.start();
} else if (availability === 'downloadable') {
  await SpeechRecognition.install();
  recognition.start();
} else {
  console.warn('Reconocimiento en dispositivo no soportado — usando la nube como fallback.');
  recognition.start();
}

Nota: SpeechRecognition.available() y SpeechRecognition.install() son APIs experimentales. Permiten verificar si los modelos de idioma están cacheados localmente y activar su descarga. El soporte se limita a versiones recientes de Chromium.


5. Modelo de Seguridad

La API Web Speech implementa restricciones de seguridad estrictas para prevenir la interceptación pasiva de audio:

  • Gesto del usuario requerido: recognition.start() debe invocarse dentro de un evento iniciado por el usuario (clic, tap). Invocarlo programáticamente sin un gesto lanza un error.
  • Solo HTTPS: toda la API está restringida a contextos seguros. Las páginas HTTP no pueden acceder al micrófono.
  • Indicadores visuales: los navegadores muestran indicadores de grabación persistentes e inmodificables (íconos en las pestañas, alertas en la barra de estado) mientras el micrófono está activo.
  • Permissions-Policy: los administradores de servidores pueden bloquear el acceso al micrófono a nivel de cabecera HTTP para iframes de origen cruzado:
Permissions-Policy: microphone=()

6. Compatibilidad entre Navegadores (2026)

NavegadorSpeechSynthesis (TTS)SpeechRecognition (STT)Notas
Chrome✅ Completo (v33+)✅ Completo (v33+)Implementador principal. Basado en la nube por defecto.
Edge✅ Completo (v14+)⚠️ No confiableExpone la interfaz pero los eventos pueden no dispararse nunca.
Firefox✅ Completo (v49+)❌ No implementadoObjeción filosófica al enrutamiento de biometría vocal a nubes de terceros.
Safari✅ Completo (v7+)⚠️ Parcial (v14.1+)Requiere prefijo webkit. Depende de que Siri esté habilitado.
Opera✅ Completo (v21+)❌ No implementadoRefleja las limitaciones de Edge/Firefox.
Brave⚠️ Variable❌ BloqueadoBloquea conexiones STT a la nube por privacidad.

6.1 Detección Defensiva de Características

Siempre verifica el soporte antes de usar cualquiera de los dos subsistemas:

const hasSpeechRecognition = 'SpeechRecognition' in window || 'webkitSpeechRecognition' in window;
const hasSpeechSynthesis = 'speechSynthesis' in window;

if (!hasSpeechRecognition) {
  console.warn('SpeechRecognition no disponible. Considera un fallback en la nube (Google Cloud STT, Azure, AWS Transcribe).');
}

if (!hasSpeechSynthesis) {
  console.warn('SpeechSynthesis no disponible.');
}

7. Problemas Conocidos y Soluciones

7.1 Bug de 15 Segundos en Chrome (TTS)

Las voces respaldadas por la nube en Chrome abortan locuciones de más de ~15 segundos sin disparar eventos end o error, dejando la interfaz en un estado de “hablando” permanente. La solución es dividir textos largos en locuciones más cortas:

const speakLongText = (text) => {
  const synth = window.speechSynthesis;
  synth.cancel();

  const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];

  sentences.forEach((sentence) => {
    const utterance = new SpeechSynthesisUtterance(sentence.trim());
    utterance.voice = voices[0];
    synth.speak(utterance);
  });
};

7.2 Safari iOS: TTS Requiere un Gesto de “Calentamiento”

Safari en iOS bloquea el audio TTS hasta que el usuario ejecuta un gesto. Para solucionarlo, “calienta” silenciosamente el contexto de audio en la primera interacción del usuario:

document.addEventListener('click', () => {
  const primer = new SpeechSynthesisUtterance('');
  window.speechSynthesis.speak(primer);
}, { once: true });

Después de esto, las llamadas subsecuentes a synth.speak() funcionarán sin requerir gestos adicionales.

7.3 SpeechRecognition en PWAs de iOS

Cuando una página web se agrega a la pantalla de inicio en iOS (“Agregar a inicio”), webkitSpeechRecognition se deshabilita completamente. Todas las llamadas lanzan errores NotAllowed sin siquiera mostrar el cuadro de permisos. No hay solución dentro de la API Web Speech — se debe usar un servicio STT basado en la nube en su lugar.


8. Beneficios de Accesibilidad

La API Web Speech mejora directamente la accesibilidad para:

  • Usuarios con discapacidad motora: los comandos de voz reemplazan las interacciones de teclado y ratón
  • Usuarios con discapacidad visual: TTS lee el contenido en voz alta, complementando los lectores de pantalla
  • Usuarios con baja alfabetización: la retroalimentación auditiva reduce la dependencia de la lectura
  • Escenarios manos libres: cocina, conducción o entornos médicos donde la entrada táctil/escritura es impráctica

Mejores prácticas al integrar voz en interfaces accesibles:

  • Desvincular el teclado virtual llamando a element.blur() cuando inicia el reconocimiento, recuperando espacio del viewport en móvil
  • Mostrar resultados provisionales visualmente para que el usuario vea retroalimentación en tiempo real durante el dictado
  • Usar el evento boundary de TTS para resaltar texto mientras se lee, proporcionando una experiencia visual-auditiva sincronizada

9. Conclusión

La API Web Speech proporciona un puente poderoso entre la voz y la plataforma web. SpeechSynthesis está listo para producción en todos los navegadores, mientras que SpeechRecognition requiere detección cuidadosa de características y estrategias de fallback debido al soporte fragmentado.

Comienza con TTS para mejoras de accesibilidad — funciona en todas partes. Para STT, apunta a navegadores Chromium con reconocimiento basado en la nube, implementa manejo robusto de errores y evalúa servicios de fallback en la nube (Google Cloud Speech-to-Text, Azure Cognitive Services, AWS Transcribe) para cobertura entre navegadores.


Fuentes: Especificación W3C Web Speech API, MDN Web Docs, rastreador de issues de Chromium, datos de compatibilidad (caniuse.com). Los ejemplos corresponden al estado de la API a marzo de 2026.