> ## Documentation Index
> Fetch the complete documentation index at: https://docs.venice.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Narración de artículos con texto a voz

> Convierte cualquier artículo web en un archivo de audio narrado con el texto a voz de Venice, cubriendo la selección de voz, el límite de 4096 caracteres, la unión de audio sin cortes y el streaming.

Hacer una llamada a `/audio/speech` es fácil. Narrar un artículo real es donde aparecen los problemas interesantes: el endpoint acepta como máximo 4096 caracteres por solicitud, cada voz pertenece a un modelo específico, los formatos de audio varían de un modelo a otro y el texto escrito para leerse no se parece en nada al texto escrito para escucharse.

En este tutorial abordamos las cuatro cuestiones. El resultado es un script que convierte una URL en un único archivo de audio:

```bash theme={"system"}
python article_to_audio.py https://docs.venice.ai/overview/privacy
```

Haremos lo siguiente:

1. Elegir un modelo y una voz adecuados para narración larga
2. Hacer una única solicitud de voz y guardar el audio
3. Dividir un artículo largo en fragmentos que quepan en el límite de caracteres
4. Unir los fragmentos sintetizados en un solo archivo sin costuras audibles
5. Reescribir el artículo en algo que valga la pena escuchar
6. Combinar las piezas y, después, ver el streaming para uso interactivo

## Configuración

Necesitas Python 3.9 o posterior, el paquete `requests` y una clave de API de Venice.

```bash theme={"system"}
pip install requests
export VENICE_API_KEY="your-api-key-here"
```

## 1. Elige un modelo y una voz

Las voces pertenecen a modelos. Enviar una voz de una familia a un modelo de otra es el primer error más común, así que empieza por listar lo que cada modelo acepta realmente:

```bash theme={"system"}
curl "https://api.venice.ai/api/v1/models?type=tts" \
  -H "Authorization: Bearer $VENICE_API_KEY" |
  jq -r '.data[] | "\(.id)  formats=\(.model_spec.supported_formats)  voices=\(.model_spec.voices | length)"'
```

```
tts-kokoro  formats=["mp3","opus","aac","flac","wav","pcm"]  voices=54
tts-qwen3-1-7b  formats=["mp3"]  voices=9
tts-xai-v1  formats=["mp3","wav","pcm"]  voices=26
tts-inworld-1-5-max  formats=["wav"]  voices=14
tts-chatterbox-hd  formats=["wav"]  voices=9
tts-orpheus  formats=["wav"]  voices=8
tts-elevenlabs-turbo-v2-5  formats=["mp3"]  voices=21
tts-minimax-speech-02-hd  formats=["mp3","pcm","flac"]  voices=15
tts-gemini-3-1-flash  formats=["mp3","opus","wav"]  voices=30
tts-gradium-v1  formats=["wav","pcm","opus"]  voices=12
```

`model_spec.voices` es la lista de voces autorizada de un modelo, y `supported_formats` te indica qué valores de `response_format` acepta. Quita el `| length` de la consulta para imprimir los nombres de las voces.

Usaremos `tts-xai-v1` con la voz `eve`. Admite `pcm`, que es lo que facilita la unión de fragmentos en la sección 4.

<Note>
  La velocidad de síntesis varía mucho más entre modelos de TTS que la calidad de la salida, y la diferencia es lo bastante grande como para cambiar tu arquitectura. Mide una solicitud realista con dos o tres candidatos antes de comprometerte con uno. Un fragmento que un modelo devuelve en unos segundos puede tardar varios minutos en otro.
</Note>

## 2. Haz una única solicitud

El cuerpo de la respuesta es audio en bruto en lugar de JSON, así que escribe los bytes directamente en un archivo.

<CodeGroup>
  ```python Python theme={"system"}
  import os
  from pathlib import Path

  import requests

  response = requests.post(
      "https://api.venice.ai/api/v1/audio/speech",
      headers={
          "Authorization": f"Bearer {os.environ['VENICE_API_KEY']}",
          "Content-Type": "application/json",
      },
      json={
          "model": "tts-xai-v1",
          "voice": "eve",
          "input": "Hello from Venice.",
          "response_format": "mp3",
      },
      timeout=300,
  )

  response.raise_for_status()
  Path("hello.mp3").write_bytes(response.content)
  ```

  ```javascript Node.js theme={"system"}
  import { writeFile } from "node:fs/promises";

  const response = await fetch("https://api.venice.ai/api/v1/audio/speech", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.VENICE_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "tts-xai-v1",
      voice: "eve",
      input: "Hello from Venice.",
      response_format: "mp3",
    }),
  });

  if (!response.ok) {
    throw new Error(`${response.status}: ${await response.text()}`);
  }

  await writeFile("hello.mp3", Buffer.from(await response.arrayBuffer()));
  ```

  ```bash cURL theme={"system"}
  curl https://api.venice.ai/api/v1/audio/speech \
    -H "Authorization: Bearer $VENICE_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "tts-xai-v1",
      "voice": "eve",
      "input": "Hello from Venice.",
      "response_format": "mp3"
    }' \
    --output hello.mp3
  ```
</CodeGroup>

Las combinaciones no coincidentes se rechazan antes de generar audio, y el error te indica qué habría funcionado en su lugar:

```json theme={"system"}
{"error":"Voice \"eve\" is not supported by model \"tts-kokoro\". Try using a supported voice: af_alloy, af_aoede, af_bella, af_heart, af_jadzia, ...."}
```

```json theme={"system"}
{"error":"response_format \"wav\" is not supported by model \"tts-elevenlabs-turbo-v2-5\". Supported formats: mp3."}
```

## 3. Divide el texto en el límite de 4096 caracteres

El campo `input` acepta como máximo 4096 caracteres. El texto más largo se rechaza directamente en vez de truncarse en silencio:

```json theme={"system"}
{"error":"Invalid request parameters","issues":[{"code":"too_big","maximum":4096,"path":["input"]}]}
```

Así que dividimos el artículo primero. Cortar por los límites de las frases es importante, porque un fragmento que termina a mitad de frase produce un tropiezo audible en la unión. Crea `narrate.py`:

```python theme={"system"}
from __future__ import annotations

import os
import re
import sys
import wave
from concurrent.futures import ThreadPoolExecutor

import requests

BASE_URL = "https://api.venice.ai/api/v1"
HEADERS = {
    "Authorization": f"Bearer {os.environ['VENICE_API_KEY']}",
    "Content-Type": "application/json",
}

MODEL = "tts-xai-v1"
VOICE = "eve"
SAMPLE_RATE = 24000  # tts-xai-v1 returns 24 kHz mono signed 16-bit PCM.

SENTENCE_END = re.compile(r"(?<=[.!?])\s+")


def split_into_chunks(text: str, max_chars: int = 1500) -> list[str]:
    """Split text on sentence boundaries into chunks under the 4096-character cap."""
    chunks: list[str] = []
    current = ""

    for sentence in SENTENCE_END.split(text.strip()):
        if not sentence:
            continue
        if len(sentence) > max_chars:
            raise ValueError(f"Sentence longer than {max_chars} characters: {sentence[:80]}...")
        if len(current) + len(sentence) + 1 > max_chars:
            chunks.append(current)
            current = sentence
        else:
            current = f"{current} {sentence}" if current else sentence

    if current:
        chunks.append(current)
    return chunks
```

Con un guion de 5362 caracteres, esto produce cuatro fragmentos, cada uno terminado en una frase:

```python theme={"system"}
chunks = split_into_chunks(script)
print(f"len(chunks) = {len(chunks)}")
for index, chunk in enumerate(chunks):
    print(f"chunk {index}: {len(chunk)} chars")
```

```
len(chunks) = 4
chunk 0: 1428 chars
chunk 1: 1410 chars
chunk 2: 1489 chars
chunk 3: 1015 chars
```

El valor predeterminado de `max_chars` es 1500 en lugar de algo cercano al tope de 4096, y es intencional. El tiempo de síntesis crece con la longitud de la entrada, así que los fragmentos más pequeños vuelven antes y, como se ejecutan en paralelo, terminan todo el trabajo más rápido. Además abaratan los reintentos cuando falla una solicitud.

## 4. Une los fragmentos en un solo archivo

Concatenar audio codificado como MP3 no es fiable, porque cada fragmento lleva sus propios encabezados de trama. Solicitar `pcm` evita el problema por completo. PCM son muestras en bruto sin contenedor, así que unir es simplemente añadir bytes, y el módulo estándar `wave` de Python escribe el encabezado por nosotros.

```python theme={"system"}
def synthesize(text: str, speed: float = 1.0) -> bytes:
    """Return raw PCM audio for one chunk."""
    response = requests.post(
        f"{BASE_URL}/audio/speech",
        headers=HEADERS,
        json={
            "model": MODEL,
            "voice": VOICE,
            "input": text,
            "response_format": "pcm",
            "speed": speed,
        },
        timeout=300,
    )
    if response.status_code != 200:
        raise RuntimeError(f"TTS failed ({response.status_code}): {response.text}")
    return response.content


def narrate(text: str, out_path: str) -> str:
    chunks = split_into_chunks(text)
    print(f"Synthesizing {len(chunks)} chunks", file=sys.stderr)

    with ThreadPoolExecutor(max_workers=4) as pool:
        audio = list(pool.map(synthesize, chunks))

    with wave.open(out_path, "wb") as output:
        output.setnchannels(1)
        output.setsampwidth(2)
        output.setframerate(SAMPLE_RATE)
        for part in audio:
            output.writeframes(part)

    seconds = sum(len(part) for part in audio) / 2 / SAMPLE_RATE
    print(f"Wrote {out_path} ({seconds:.1f}s of audio)", file=sys.stderr)
    return out_path
```

Un solo fragmento se devuelve rápido en relación con la cantidad de audio que contiene:

```python theme={"system"}
pcm = synthesize(chunks[0])
print(f"bytes   = {len(pcm)}")
print(f"audio   = {len(pcm) / 2 / SAMPLE_RATE:.1f}s")
```

```
bytes   = 4269376
audio   = 88.9s
```

Esa solicitud tardó unos 13 segundos en producir 89 segundos de voz. Ejecutar los cuatro fragmentos de forma concurrente es lo que mantiene el total en un tiempo razonable: la narración completa de abajo tardó 15 segundos de reloj.

`ThreadPoolExecutor.map` devuelve los resultados en el orden en que se enviaron las entradas, así que los fragmentos aterrizan en orden de lectura aunque se hayan sintetizado al mismo tiempo.

<Warning>
  El PCM en bruto no lleva la tasa de muestreo, así que tienes que proporcionar la correcta al escribir el encabezado WAV, y es específica de cada modelo. `tts-xai-v1` devuelve 24 kHz mientras que `tts-gradium-v1` devuelve 48 kHz. Si te equivocas, la narración se reproduce a velocidad y tono incorrectos.
</Warning>

Para averiguar la tasa de cualquier modelo, pide un clip corto como `wav` y lee el encabezado con el que vuelve:

```python theme={"system"}
import wave

response = requests.post(
    f"{BASE_URL}/audio/speech",
    headers=HEADERS,
    json={"model": MODEL, "voice": VOICE, "input": "Probe.", "response_format": "wav"},
    timeout=300,
)
response.raise_for_status()
with open("probe.wav", "wb") as handle:
    handle.write(response.content)

with wave.open("probe.wav") as probe:
    print(probe.getframerate(), probe.getnchannels(), probe.getsampwidth())
```

```
24000 1 2
```

## 5. Prepara texto que suene bien

El Markdown extraído leído en voz alta al pie de la letra es prácticamente inescuchable. Las URLs son el ejemplo más claro. Un modelo de voz las deletrea carácter por carácter, así que `https://docs.venice.ai/llms.txt` sale como:

> h t t p s dos puntos barra barra docs punto venice punto a i l l m s punto t x t

Los encabezados, los marcadores de viñeta, las tablas y los bloques de código causan versiones más pequeñas del mismo problema. En lugar de pelearnos con Markdown a base de expresiones regulares, podemos pedirle a un modelo de chat que reescriba el artículo como algo pensado para hablarse. Crea `article_to_audio.py`:

```python theme={"system"}
from __future__ import annotations

import os
import re
import sys

import requests

from narrate import narrate

BASE_URL = "https://api.venice.ai/api/v1"
HEADERS = {
    "Authorization": f"Bearer {os.environ['VENICE_API_KEY']}",
    "Content-Type": "application/json",
}

URL_PATTERN = re.compile(r"https?://\S+|www\.\S+")


def scrape(url: str) -> str:
    response = requests.post(
        f"{BASE_URL}/augment/scrape", headers=HEADERS, json={"url": url}, timeout=120
    )
    response.raise_for_status()
    return response.json()["content"]


def write_script(markdown: str, minutes: int = 6) -> str:
    response = requests.post(
        f"{BASE_URL}/chat/completions",
        headers=HEADERS,
        json={
            "model": "zai-org-glm-5-1",
            "messages": [
                {
                    "role": "system",
                    "content": (
                        "You rewrite articles as scripts to be read aloud. Output plain prose only: "
                        "no Markdown, no headings, no bullet points, no URLs, no code, no emoji. "
                        "Spell out abbreviations and numbers the way a narrator would say them. "
                        "Use short sentences with clear punctuation so speech synthesis paces well."
                    ),
                },
                {
                    "role": "user",
                    "content": f"Rewrite this article as a {minutes}-minute spoken summary.\n\n{markdown[:20000]}",
                },
            ],
            "temperature": 0.4,
        },
        timeout=300,
    )
    response.raise_for_status()
    script = response.json()["choices"][0]["message"]["content"]
    return URL_PATTERN.sub("", script).strip()
```

La sustitución `URL_PATTERN` se queda como red de seguridad para el enlace ocasional que el modelo deje suelto.

## 6. Únelo todo

El punto de entrada extrae, escribe el guion, lo guarda y narra:

```python theme={"system"}
if __name__ == "__main__":
    url = sys.argv[1]

    print("Scraping", url, file=sys.stderr)
    markdown = scrape(url)

    print(f"Writing script from {len(markdown)} characters of Markdown", file=sys.stderr)
    script = write_script(markdown)

    with open("script.txt", "w") as handle:
        handle.write(script)

    narrate(script, "article.wav")
```

Guardar `script.txt` junto al audio vale las dos líneas. Cuando una narración suena mal, el guion casi siempre muestra por qué, y puedes arreglarlo sin pagar para sintetizar de nuevo.

```bash theme={"system"}
python article_to_audio.py https://docs.venice.ai/overview/privacy
```

```
Scraping https://docs.venice.ai/overview/privacy
Writing script from 7582 characters of Markdown
Synthesizing 4 chunks
Wrote article.wav (355.0s of audio)
```

Poco menos de seis minutos de narración, producidos en unos quince segundos. El guion ahora abre con prosa en lugar de con adornos de navegación:

> Venice se construye sobre un principio sencillo pero poderoso. La privacidad del usuario está primero. Toda la arquitectura de la plataforma parte de ese compromiso filosófico.

<Tip>
  Para revisar una narración sin escucharla entera, envía el audio de vuelta a través de [`/audio/transcriptions`](/guides/media/speech-to-text) y compara la transcripción con `script.txt`. Transcribir los últimos veinte segundos es una forma rápida de confirmar que los fragmentos se unieron en el orden correcto, y detecta fragmentos perdidos o URLs deletreadas en segundos.
</Tip>

## Streaming para uso interactivo

La narración por lotes optimiza el tiempo total. Una interfaz de voz tiene la prioridad contraria, que es sacar el primer audio lo antes posible. Establecer `streaming: true` devuelve el cuerpo frase por frase a medida que se genera, así que la reproducción puede empezar en aproximadamente un segundo en vez de esperar al clip completo.

```python theme={"system"}
import time

import requests

start = time.time()
first_byte = None

with requests.post(
    "https://api.venice.ai/api/v1/audio/speech",
    headers=HEADERS,
    json={
        "model": "tts-xai-v1",
        "voice": "eve",
        "input": "Streaming returns audio while the rest is still being generated.",
        "response_format": "mp3",
        "streaming": True,
    },
    stream=True,
    timeout=300,
) as response:
    response.raise_for_status()
    with open("streamed.mp3", "wb") as audio:
        for chunk in response.iter_content(chunk_size=4096):
            if first_byte is None:
                first_byte = time.time() - start
            audio.write(chunk)

print(f"first byte: {first_byte:.2f}s   complete: {time.time() - start:.2f}s")
```

```
first byte: 0.85s   complete: 1.43s
```

Prefiere `pcm` sobre `mp3` cuando alimentes la Web Audio API de un navegador o un dispositivo de audio directamente, ya que no necesita decodificarse.

## Opciones de solicitud que vale la pena conocer

| Parámetro     | Notas                                                                                                                                                                                                                       |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `speed`       | Acepta de `0.25` a `4.0`, valor predeterminado `1.0`. Mantente aproximadamente entre `0.8` y `1.3` para narración. Más allá de eso, la entrega deja de sonar natural.                                                       |
| `language`    | Pista opcional cuya forma aceptada es específica del modelo. xAI y ElevenLabs toman códigos ISO 639-1 como `en`, mientras que Qwen 3 y MiniMax toman nombres completos como `English`. Los valores no admitidos se ignoran. |
| `prompt`      | Indicación de estilo y emoción, de hasta 500 caracteres, actualmente respetada solo por los modelos Qwen 3. Para otras familias, la elección de la voz aporta el tono.                                                      |
| `temperature` | Rango `0` a `2`, admitido por Qwen 3, Orpheus y Chatterbox HD. Súbela para más variación entre tomas.                                                                                                                       |

## Errores

| Estado        | Causa                              | Solución                                         |
| ------------- | ---------------------------------- | ------------------------------------------------ |
| `400`         | `input` supera los 4096 caracteres | Fragmenta el texto como en la sección 3          |
| `400`         | Voz no válida para el modelo       | Usa una voz de `model_spec.voices` de ese modelo |
| `400`         | Formato no admitido por el modelo  | Comprueba `model_spec.supported_formats`         |
| `401`         | Clave ausente o no válida          | Confirma el encabezado `Authorization: Bearer`   |
| `402`         | Saldo insuficiente                 | Recarga la cuenta                                |
| `429`         | Límite de tasa                     | Reduce `max_workers` y reintenta con backoff     |
| `500` o `503` | Fallo de capacidad o de inferencia | Reintenta el fragmento afectado con jitter       |

Como los fragmentos son independientes, un fallo solo te cuesta uno de ellos, y reintentar `synthesize` para ese fragmento siempre es seguro.

## Próximos pasos

Algunas extensiones naturales desde aquí:

* Almacena en caché el audio por un hash del texto, la voz y el modelo para que los párrafos sin cambios nunca se vuelvan a sintetizar.
* Sustituye por una voz clonada con [Clonación de voz](/guides/media/voice-cloning) para que la narración use la tuya propia.
* Genera el texto fuente en lugar de extraerlo, usando [Respuestas citadas con búsqueda web](/guides/tools/cited-web-answers).
* Añade audio de introducción o de fondo con [Música y efectos de sonido](/guides/media/music-and-sound-effects).

<CardGroup cols={2}>
  <Card title="Texto a voz" icon="volume-2" href="/guides/media/text-to-speech">
    Referencia del endpoint de voz y sus parámetros.
  </Card>

  <Card title="Clonación de voz" icon="user" href="/guides/media/voice-cloning">
    Narra con una voz personalizada en lugar de una preestablecida.
  </Card>

  <Card title="Respuestas citadas con búsqueda web" icon="search" href="/guides/tools/cited-web-answers">
    Genera el texto que esta herramienta narra.
  </Card>

  <Card title="Voz a texto" icon="microphone" href="/guides/media/speech-to-text">
    Transcribe audio para verificar una narración de principio a fin.
  </Card>
</CardGroup>
