AWS SDK Polly - Síntesis de texto a voz con audio en streaming y soporte SSML
Receta
npm install @aws-sdk/client-polly// lib/polly.ts
import { PollyClient } from "@aws-sdk/client-polly";
export const pollyClient = new PollyClient({
region: process.env.AWS_REGION!,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});// app/api/speech/route.ts
import { SynthesizeSpeechCommand } from "@aws-sdk/client-polly";
import { pollyClient } from "@/lib/polly";
export async function POST(req: Request) {
const { text, voiceId = "Joanna" } = await req.json();
const command = new SynthesizeSpeechCommand({
Text: text,
OutputFormat: "mp3",
VoiceId: voiceId,
Engine: "neural",
});
const response = await pollyClient.send(command);
const stream = response.AudioStream as ReadableStream;
return new Response(stream, {
headers: {
"Content-Type": "audio/mpeg",
"Cache-Control": "public, max-age=3600",
},
});
}Cuándo usarlo: Necesitas convertir texto a voz natural en una aplicación web para accesibilidad, asistentes de voz, aprendizaje de idiomas o narración de contenido.
Ejemplo en funcionamiento
// app/components/TextToSpeech.tsx
"use client";
import { useState, useRef } from "react";
const VOICES = [
{ id: "Joanna", name: "Joanna (Inglés estadounidense)", lang: "en-US" },
{ id: "Matthew", name: "Matthew (Inglés estadounidense)", lang: "en-US" },
{ id: "Amy", name: "Amy (Inglés británico)", lang: "en-GB" },
{ id: "Lea", name: "Léa (Francés)", lang: "fr-FR" },
{ id: "Vicki", name: "Vicki (Alemán)", lang: "de-DE" },
{ id: "Lucia", name: "Lucia (Español)", lang: "es-ES" },
];
export default function TextToSpeech() {
const [text, setText] = useState("");
const [voiceId, setVoiceId] = useState("Joanna");
const [loading, setLoading] = useState(false);
const audioRef = useRef<HTMLAudioElement>(null);
async function handleSpeak() {
if (!text.trim()) return;
setLoading(true);
try {
const res = await fetch("/api/speech", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, voiceId }),
});
if (!res.ok) throw new Error("La síntesis de voz falló");
const blob = await res.blob();
const url = URL.createObjectURL(blob);
if (audioRef.current) {
audioRef.current.src = url;
audioRef.current.play();
}
} catch (error) {
console.error("TTS error:", error);
} finally {
setLoading(false);
}
}
return (
<div className="max-w-md mx-auto p-6 space-y-4">
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Ingresa el texto a hablar..."
rows={4}
className="w-full border rounded px-3 py-2"
maxLength={3000}
/>
<div className="flex gap-3">
<select
value={voiceId}
onChange={(e) => setVoiceId(e.target.value)}
className="border rounded px-3 py-2"
>
{VOICES.map((v) => (
<option key={v.id} value={v.id}>
{v.name}
</option>
))}
</select>
<button
onClick={handleSpeak}
disabled={loading || !text.trim()}
className="bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50"
>
{loading ? "Generando..." : "Hablar"}
</button>
</div>
<audio ref={audioRef} controls className="w-full" />
<p className="text-xs text-gray-500">{text.length}/3000 caracteres</p>
</div>
);
}Lo que demuestra esto:
- IU de texto a voz completa con selección de voz
- Audio en streaming desde Polly a través de una ruta API de Next.js
- Creación y reproducción de blob de audio con elemento HTML5 audio
- Manejo del límite de caracteres (límite de Polly es 3000 caracteres para estándar, 6000 para neural)
Profundización
Cómo funciona
- Polly convierte texto a audio usando motores de síntesis de voz neural o estándar
- El motor neural produce voz más natural pero solo está disponible para un subconjunto de voces
SynthesizeSpeechCommanddevuelve unAudioStreamque se puede canalizar directamente a la respuesta- Formatos de salida compatibles:
mp3,ogg_vorbis,pcmyjson(para marcas de voz) - Polly admite tanto texto sin formato como SSML (Lenguaje de Marcado de Síntesis de Voz) para control granular
- Cada voz está vinculada a un idioma específico; no puedes cambiar el idioma de una voz
Variaciones
SSML para control avanzado de voz:
const command = new SynthesizeSpeechCommand({
Text: `<speak>
Bienvenido a nuestra aplicación.
<break time="500ms"/>
<prosody rate="slow" pitch="+10%">
Esta parte se habla lentamente con un tono más alto.
</prosody>
<emphasis level="strong">Esto es importante.</emphasis>
<say-as interpret-as="date" format="mdy">12/25/2025</say-as>
</speak>`,
TextType: "ssml",
OutputFormat: "mp3",
VoiceId: "Joanna",
Engine: "neural",
});Obtener voces disponibles:
import { DescribeVoicesCommand } from "@aws-sdk/client-polly";
export async function GET() {
const command = new DescribeVoicesCommand({
Engine: "neural",
LanguageCode: "en-US",
});
const response = await pollyClient.send(command);
const voices = response.Voices?.map((v) => ({
id: v.Id,
name: v.Name,
gender: v.Gender,
languageName: v.LanguageName,
}));
return Response.json(voices);
}Marcas de voz (datos de tiempo de palabras):
const command = new SynthesizeSpeechCommand({
Text: "Hola, ¿cómo estás hoy?",
OutputFormat: "json",
VoiceId: "Joanna",
Engine: "neural",
SpeechMarkTypes: ["word", "sentence"],
});
const response = await pollyClient.send(command);
// Devuelve JSONL con datos de tiempo para cada palabra/oración
// {"time":0,"type":"sentence","start":0,"end":25,"value":"Hello, how are you today?"}
// {"time":0,"type":"word","start":0,"end":5,"value":"Hello"}Server Action para frases cortas:
"use server";
import { SynthesizeSpeechCommand } from "@aws-sdk/client-polly";
import { pollyClient } from "@/lib/polly";
export async function synthesizeSpeech(text: string, voiceId: string = "Joanna") {
const command = new SynthesizeSpeechCommand({
Text: text.slice(0, 3000),
OutputFormat: "mp3",
VoiceId: voiceId,
Engine: "neural",
});
const response = await pollyClient.send(command);
const chunks: Uint8Array[] = [];
const stream = response.AudioStream as AsyncIterable<Uint8Array>;
for await (const chunk of stream) {
chunks.push(chunk);
}
const buffer = Buffer.concat(chunks);
return buffer.toString("base64");
}Notas de TypeScript
VoiceIdes un tipo de unión de todos los IDs de voz disponibles (p. ej.,"Joanna","Matthew")Enginees"neural"o"standard"OutputFormates"mp3"u"ogg_vorbis"u"pcm"o"json"- El tipo
AudioStreamvaría según el entorno; conviértelo aReadableStreamen serverless
import type {
SynthesizeSpeechCommandInput,
VoiceId,
Engine,
} from "@aws-sdk/client-polly";
const voiceId: VoiceId = "Joanna";
const engine: Engine = "neural";
const input: SynthesizeSpeechCommandInput = {
Text: "Hello",
OutputFormat: "mp3",
VoiceId: voiceId,
Engine: engine,
};Escollos
-
Motor neural no disponible para todas las voces — No todas las voces admiten el motor neural. Solución: Verifica
DescribeVoicesCommandconEngine: "neural"para obtener la lista de voces admitidas. Vuelve a"standard"si no estás seguro. -
Límites de longitud de texto — El motor estándar permite 6000 caracteres, neural permite 3000 por solicitud. Solución: Divide el texto largo en fragmentos en los límites de las oraciones y sintetiza cada uno por separado.
-
SSML debe ser XML bien formado — Las etiquetas SSML inválidas hacen que
SynthesizeSpeechCommandlance una excepción. Solución: EstableceTextType: "ssml"y envuelve el contenido en etiquetas<speak>. Escapa caracteres especiales (&,<). -
Compatibilidad de formato de audio — La salida
pcmes datos de audio sin procesar, no reproducible en un elemento de audio del navegador. Solución: Usamp3uogg_vorbispara reproducción en navegador. Usapcmsolo para canalizaciones de procesamiento de audio. -
Costo a escala — Polly cobra por carácter sintetizado. Solución: Almacena en caché resultados de audio en S3 o un CDN para frases repetidas. Usa el encabezado
Cache-Controlen las respuestas. -
El tipo de cuerpo de streaming varía —
AudioStreamse escribe de manera diferente en Node.js vs. tiempos de ejecución de edge. Solución: En rutas API de Node.js, conviértelo aReadableStream. En tiempo de ejecución de edge, ya podría ser un stream web.
Alternativas
| Biblioteca | Mejor para | Compensación |
|---|---|---|
| AWS Polly | Voces neurales de alta calidad, control SSML | Se requiere cuenta de AWS, costo por carácter |
| API de voz web | TTS nativo del navegador gratuito | Calidad inconsistente, control de voz limitado |
| TTS de Google Cloud | Voces WaveNet, amplio soporte de idiomas | SDK diferente, precios similares |
| ElevenLabs | Clonación de voz ultra realista | Costo más alto, API separada |
| OpenAI TTS | API simple, buena calidad | Personalización de voz limitada |
Preguntas frecuentes
¿Cuál es la diferencia entre los motores neural y estándar de Polly?
- El motor neural produce voz más natural y similar a la humana
- El motor estándar está disponible para más voces pero suena más robótico
- Neural tiene un límite de 3000 caracteres por solicitud; estándar permite 6000
- No todas las voces admiten el motor neural -- verifica con
DescribeVoicesCommand
¿Cómo transmito audio de Polly a través de una ruta API de Next.js al navegador?
const response = await pollyClient.send(command);
const stream = response.AudioStream as ReadableStream;
return new Response(stream, {
headers: { "Content-Type": "audio/mpeg" },
});El navegador recibe la transmisión de audio y puede reproducirla a través de un elemento <audio>.
¿Qué formatos de salida de audio admite Polly y cuál debo usar para reproducción en navegador?
mp3-- ampliamente compatible, bueno para reproducción en navegadorogg_vorbis-- buena calidad, compatible en la mayoría de navegadores modernospcm-- datos de audio sin procesar, no reproducible directamente en un navegadorjson-- devuelve marcas de voz (datos de tiempo), no audio- Usa
mp3para reproducción en navegador en la mayoría de casos
¿Cómo uso SSML para controlar la prosodia del habla, pausas y énfasis?
const command = new SynthesizeSpeechCommand({
Text: `<speak>
Hola. <break time="500ms"/>
<prosody rate="slow">Esto es lento.</prosody>
<emphasis level="strong">¡Importante!</emphasis>
</speak>`,
TextType: "ssml",
OutputFormat: "mp3",
VoiceId: "Joanna",
Engine: "neural",
});Escollo: ¿Por qué mi solicitud SSML lanza un error?
- SSML debe ser XML bien formado envuelto en etiquetas
<speak> - Establece
TextType: "ssml"en el comando -- omitir esto trata las etiquetas SSML como texto sin formato - Escapa caracteres XML especiales: usa
&para&,<para< - Las etiquetas inválidas o no cerradas hacen que
SynthesizeSpeechCommandlance una excepción
¿Cómo listo todas las voces neurales disponibles para un idioma específico?
import { DescribeVoicesCommand } from "@aws-sdk/client-polly";
const command = new DescribeVoicesCommand({
Engine: "neural",
LanguageCode: "en-US",
});
const response = await pollyClient.send(command);
const voices = response.Voices?.map(v => ({
id: v.Id,
name: v.Name,
gender: v.Gender,
}));¿Qué son las marcas de voz y cómo obtengo datos de tiempo a nivel de palabra?
- Las marcas de voz proporcionan información de tiempo para cada palabra, oración o visema
- Establece
OutputFormat: "json"ySpeechMarkTypes: ["word", "sentence"] - La respuesta es JSONL (un objeto JSON por línea) con campos
time,start,endyvalue - Útil para resaltado estilo karaoke o sincronización de labios
Escollo: ¿Cómo manejo el tipo AudioStream que varía entre tiempos de ejecución?
- En rutas API de Node.js,
AudioStreames un streamReadablede Node.js - En tiempo de ejecución de edge, ya podría ser un
ReadableStreamweb - Conviértelo a
ReadableStreamal devolverlo desde un constructorResponse - Para Server Actions, itera con
for awaity recopila en unBuffer
¿Cómo escribo las entradas del comando Polly en TypeScript?
import type {
SynthesizeSpeechCommandInput,
VoiceId,
Engine,
} from "@aws-sdk/client-polly";
const input: SynthesizeSpeechCommandInput = {
Text: "Hola",
OutputFormat: "mp3",
VoiceId: "Joanna" satisfies VoiceId,
Engine: "neural" satisfies Engine,
};¿Cómo debo manejar el límite de 3000 caracteres para solicitudes de voz neural?
- Divide el texto largo en fragmentos en los límites de las oraciones
- Sintetiza cada fragmento por separado y concatena el audio en el cliente
- El motor estándar permite 6000 caracteres por solicitud si neural no es necesario
¿Cómo puedo reducir los costos de Polly para frases repetidas?
- Almacena en caché resultados de audio en S3 para frases que se sintetizan frecuentemente
- Establece encabezados
Cache-Controlen respuestas API para almacenamiento en caché del navegador - Usa un CDN frente a tu ruta de API de speech
- Solo re-sintetiza cuando el texto o la voz cambian
¿Cómo devuelvo audio desde una Server Action como base64 para reproducción en el cliente?
"use server";
const response = await pollyClient.send(command);
const chunks: Uint8Array[] = [];
for await (const chunk of response.AudioStream as AsyncIterable<Uint8Array>) {
chunks.push(chunk);
}
return Buffer.concat(chunks).toString("base64");En el cliente, convierte base64 a una URL de blob para el elemento <audio>.
Relacionado
- AWS SDK S3 — Almacena en caché el audio sintetizado en S3
- AWS SDK Lambda — Procesa audio en Lambda
- Vercel AI SDK — Combina generación de texto de IA con salida de voz