> ## 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.

# Narrer des articles avec la synthèse vocale

> Transformez n'importe quel article web en fichier audio narré avec la synthèse vocale Venice, en abordant le choix de la voix, la limite de 4096 caractères, l'assemblage audio sans coupures et le streaming.

Faire un appel à `/audio/speech` est facile. C'est en narrant un vrai article que les problèmes intéressants apparaissent : le point de terminaison accepte au plus 4096 caractères par requête, chaque voix appartient à un modèle spécifique, les formats audio diffèrent d'un modèle à l'autre, et un texte écrit pour être lu ne ressemble en rien à un texte écrit pour être entendu.

Dans ce tutoriel, nous traitons les quatre. Le résultat est un script qui transforme une URL en un seul fichier audio :

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

Nous allons :

1. Choisir un modèle et une voix adaptés à la narration longue
2. Faire une seule requête de synthèse et enregistrer l'audio
3. Découper un long article en morceaux respectant la limite de caractères
4. Assembler les morceaux synthétisés en un seul fichier sans coutures audibles
5. Réécrire l'article en quelque chose qui vaut la peine d'être écouté
6. Combiner les pièces, puis examiner le streaming pour un usage interactif

## Configuration

Vous avez besoin de Python 3.9 ou plus récent, du paquet `requests` et d'une clé API Venice.

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

## 1. Choisir un modèle et une voix

Les voix appartiennent aux modèles. Envoyer une voix d'une famille à un modèle d'une autre famille est la première erreur la plus courante, alors commencez par lister ce que chaque modèle accepte réellement :

```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` est la liste de voix qui fait autorité pour un modèle, et `supported_formats` vous indique les valeurs de `response_format` qu'il accepte. Retirez le `| length` de la requête pour afficher les noms de voix eux-mêmes.

Nous utiliserons `tts-xai-v1` avec la voix `eve`. Elle prend en charge `pcm`, ce qui rend l'assemblage des morceaux simple à la section 4.

<Note>
  La vitesse de synthèse varie bien plus entre modèles TTS que la qualité de sortie, et l'écart est suffisamment important pour modifier votre architecture. Chronométrez une requête réaliste sur deux ou trois candidats avant de vous engager. Un morceau qu'un modèle renvoie en quelques secondes peut prendre à un autre plusieurs minutes.
</Note>

## 2. Faire une seule requête

Le corps de la réponse est de l'audio brut plutôt que du JSON, donc écrivez les octets directement dans un fichier.

<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>

Les combinaisons incompatibles sont rejetées avant qu'aucun audio ne soit généré, et l'erreur vous indique ce qui aurait fonctionné à la place :

```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. Découper le texte à la limite des 4096 caractères

Le champ `input` accepte au plus 4096 caractères. Un texte plus long est rejeté d'emblée plutôt que tronqué silencieusement :

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

Nous découpons donc l'article d'abord. Découper aux frontières de phrases est important, car un morceau qui se termine au milieu d'une phrase produit une hésitation audible à la jointure. Créez `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
```

Sur un script de 5362 caractères, cela produit quatre morceaux, chacun se terminant sur une phrase :

```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
```

La valeur par défaut de `max_chars` est 1500 plutôt que quelque chose proche du plafond de 4096, et c'est délibéré. Le temps de synthèse croît avec la longueur d'entrée, donc les morceaux plus petits reviennent plus tôt et, comme ils s'exécutent en parallèle, terminent le travail global plus vite. Ils rendent aussi les nouvelles tentatives peu coûteuses lorsqu'une requête échoue.

## 4. Assembler les morceaux en un seul fichier

Concaténer de l'audio encodé comme le MP3 n'est pas fiable, car chaque morceau porte ses propres en-têtes de trame. Demander `pcm` évite complètement le problème. PCM est constitué d'échantillons bruts sans conteneur, donc l'assemblage se résume à ajouter des octets bout à bout, et le module `wave` de la bibliothèque standard Python écrit l'en-tête pour nous.

```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 seul morceau revient rapidement par rapport à la quantité d'audio qu'il contient :

```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
```

Cette requête a pris environ 13 secondes pour produire 89 secondes de parole. Exécuter les quatre morceaux en parallèle est ce qui maintient le total raisonnable : la narration complète ci-dessous a pris 15 secondes de temps d'horloge.

`ThreadPoolExecutor.map` renvoie les résultats dans l'ordre où les entrées ont été soumises, de sorte que les morceaux arrivent dans l'ordre de lecture même s'ils ont été synthétisés en même temps.

<Warning>
  Le PCM brut ne transporte aucune fréquence d'échantillonnage, donc vous devez fournir la bonne lors de l'écriture de l'en-tête WAV, et elle est spécifique à chaque modèle. `tts-xai-v1` renvoie 24 kHz tandis que `tts-gradium-v1` renvoie 48 kHz. Trompez-vous et la narration est jouée à la mauvaise vitesse et à la mauvaise hauteur.
</Warning>

Pour trouver la fréquence pour n'importe quel modèle, demandez un court clip en `wav` et lisez l'en-tête qu'il renvoie :

```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. Préparer un texte qui sonne bien

Le Markdown extrait, lu à voix haute tel quel, est proche de l'inécoutable. Les URL en sont l'exemple le plus clair. Un modèle vocal les épelle caractère par caractère, si bien que `https://docs.venice.ai/llms.txt` sort ainsi :

> h t t p s deux points barre oblique barre oblique docs point venice point a i l l m s point t x t

Les titres, les puces, les tableaux et les blocs de code causent des versions plus discrètes du même problème. Plutôt que de combattre le Markdown avec des expressions régulières, nous pouvons demander à un modèle de chat de réécrire l'article comme quelque chose destiné à être parlé. Créez `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 substitution `URL_PATTERN` reste en place comme filet de sécurité pour le lien occasionnel que le modèle laisserait derrière lui.

## 6. Assembler le tout

Le point d'entrée extrait la page, rédige le script, l'enregistre et le narre :

```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")
```

Enregistrer `script.txt` à côté de l'audio vaut bien deux lignes. Quand une narration sonne mal, le script en montre presque toujours la raison, et vous pouvez la corriger sans payer une nouvelle synthèse.

```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)
```

Juste moins de six minutes de narration, produites en environ quinze secondes. Le script s'ouvre désormais sur de la prose plutôt que du mobilier de navigation :

> Venice est bâti sur un principe simple mais puissant. La confidentialité de l'utilisateur passe avant tout. Toute l'architecture de la plateforme découle de cet engagement philosophique.

<Tip>
  Pour vérifier une narration sans l'écouter entièrement, renvoyez l'audio dans [`/audio/transcriptions`](/guides/media/speech-to-text) et comparez la transcription à `script.txt`. Transcrire les vingt dernières secondes est un moyen rapide de confirmer que les morceaux ont été assemblés dans le bon ordre, et cela repère en quelques secondes les morceaux perdus et les URL épelées.
</Tip>

## Streaming pour un usage interactif

La narration par lots optimise le temps total. Une interface vocale a la priorité inverse, qui est d'obtenir le premier audio aussi vite que possible. Régler `streaming: true` renvoie le corps phrase par phrase au fur et à mesure de la génération, si bien que la lecture peut démarrer en environ une seconde au lieu d'attendre le clip complet.

```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
```

Préférez `pcm` à `mp3` lorsque vous alimentez directement l'API Web Audio d'un navigateur ou un périphérique audio, puisqu'il n'a besoin d'aucune étape de décodage.

## Options de requête à connaître

| Paramètre     | Notes                                                                                                                                                                                                                                               |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `speed`       | Accepte de `0.25` à `4.0`, par défaut `1.0`. Restez à peu près entre `0.8` et `1.3` pour la narration. Au-delà, le rendu cesse de sonner naturel.                                                                                                   |
| `language`    | Indication facultative dont la forme acceptée dépend du modèle. xAI et ElevenLabs prennent des codes ISO 639-1 comme `en`, tandis que Qwen 3 et MiniMax prennent des noms complets comme `English`. Les valeurs non prises en charge sont ignorées. |
| `prompt`      | Indice de style et d'émotion, jusqu'à 500 caractères, actuellement pris en compte uniquement par les modèles Qwen 3. Pour les autres familles, c'est le choix de la voix qui porte le ton.                                                          |
| `temperature` | Plage `0` à `2`, prise en charge par Qwen 3, Orpheus et Chatterbox HD. Augmentez-la pour plus de variation entre les prises.                                                                                                                        |

## Erreurs

| Statut         | Cause                                   | Solution                                                 |
| -------------- | --------------------------------------- | -------------------------------------------------------- |
| `400`          | `input` de plus de 4096 caractères      | Découpez le texte comme à la section 3                   |
| `400`          | Voix non valide pour le modèle          | Utilisez une voix issue de `model_spec.voices` du modèle |
| `400`          | Format non pris en charge par le modèle | Vérifiez `model_spec.supported_formats`                  |
| `401`          | Clé manquante ou invalide               | Vérifiez l'en-tête `Authorization: Bearer`               |
| `402`          | Solde insuffisant                       | Rechargez le compte                                      |
| `429`          | Limite de débit atteinte                | Baissez `max_workers` et réessayez avec un backoff       |
| `500` ou `503` | Capacité ou échec d'inférence           | Réessayez le morceau concerné avec du jitter             |

Comme les morceaux sont indépendants, un échec ne coûte jamais que l'un d'eux, et relancer `synthesize` pour ce morceau est toujours sûr.

## Étapes suivantes

Quelques extensions naturelles à partir d'ici :

* Mettez en cache l'audio par un hash du texte, de la voix et du modèle afin que les paragraphes inchangés ne soient jamais re-synthétisés.
* Substituez une voix clonée avec le [Clonage vocal](/guides/media/voice-cloning) pour que la narration utilise la vôtre.
* Générez le texte source au lieu de l'extraire, en utilisant [Réponses sourcées avec la recherche web](/guides/tools/cited-web-answers).
* Ajoutez un audio d'introduction ou de fond avec [Musique et effets sonores](/guides/media/music-and-sound-effects).

<CardGroup cols={2}>
  <Card title="Synthèse vocale" icon="volume-2" href="/guides/media/text-to-speech">
    Référence du point de terminaison speech et de ses paramètres.
  </Card>

  <Card title="Clonage vocal" icon="user" href="/guides/media/voice-cloning">
    Narrer avec une voix personnalisée plutôt qu'un préréglage.
  </Card>

  <Card title="Réponses sourcées avec la recherche web" icon="search" href="/guides/tools/cited-web-answers">
    Générez le texte que cet outil narre.
  </Card>

  <Card title="Reconnaissance vocale" icon="microphone" href="/guides/media/speech-to-text">
    Transcrivez de l'audio pour vérifier une narration de bout en bout.
  </Card>
</CardGroup>
