Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
// 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:
SynthesizeSpeechCommand devuelve un AudioStream que se puede canalizar directamente a la respuestamp3, ogg_vorbis, pcm y json (para marcas de voz)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");
}VoiceId es un tipo de unión de todos los IDs de voz disponibles (p. ej., "Joanna", "Matthew")Engine es "neural" o "standard"OutputFormat es "mp3" u "ogg_vorbis" u "pcm" o "json"AudioStream varía según el entorno; conviértelo a ReadableStream en serverlessimport 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,
};Motor neural no disponible para todas las voces - No todas las voces admiten el motor neural. Solución: Verifica DescribeVoicesCommand con Engine: "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 SynthesizeSpeechCommand lance una excepción. Solución: Establece TextType: "ssml" y envuelve el contenido en etiquetas <speak>. Escapa caracteres especiales (&, <).
Compatibilidad de formato de audio - La salida pcm es datos de audio sin procesar, no reproducible en un elemento de audio del navegador. Solución: Usa mp3 u ogg_vorbis para reproducción en navegador. Usa pcm solo 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-Control en las respuestas.
El tipo de cuerpo de streaming varía - AudioStream se escribe de manera diferente en Node.js vs. tiempos de ejecución de edge. Solución: En rutas API de Node.js, conviértelo a ReadableStream. En tiempo de ejecución de edge, ya podría ser un stream web.
| 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 |
DescribeVoicesCommandconst 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>.
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 audiomp3 para reproducción en navegador en la mayoría de casosconst 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",
});<speak>TextType: "ssml" en el comando -- omitir esto trata las etiquetas SSML como texto sin formato& para &, < para <SynthesizeSpeechCommand lance una excepciónimport { 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,
}));OutputFormat: "json" y SpeechMarkTypes: ["word", "sentence"]time, start, end y valueAudioStream es un stream Readable de Node.jsReadableStream webReadableStream al devolverlo desde un constructor Responsefor await y recopila en un Bufferimport type {
SynthesizeSpeechCommandInput,
VoiceId,
Engine,
} from "@aws-sdk/client-polly";
const input: SynthesizeSpeechCommandInput = {
Text: "Hola",
OutputFormat: "mp3",
VoiceId: "Joanna" satisfies VoiceId,
Engine: "neural" satisfies Engine,
};Cache-Control en respuestas API para almacenamiento en caché del navegador"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>.
Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥