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

# Artikel mit Text-to-Speech vertonen

> Verwandle jeden Webartikel in eine vertonte Audiodatei mit Venice Text-to-Speech – inklusive Stimmenauswahl, dem 4096-Zeichen-Limit, lückenlosem Zusammenfügen von Audio und Streaming.

Einen einzelnen Aufruf an `/audio/speech` zu machen ist einfach. Einen echten Artikel zu vertonen ist der Punkt, an dem die interessanten Probleme auftauchen: Der Endpunkt akzeptiert höchstens 4096 Zeichen pro Anfrage, jede Stimme gehört zu einem bestimmten Modell, Audioformate unterscheiden sich von Modell zu Modell, und Text, der zum Lesen geschrieben wurde, sieht ganz anders aus als Text, der zum Hören geschrieben wurde.

In diesem Tutorial arbeiten wir alle vier durch. Das Ergebnis ist ein Skript, das eine URL in eine einzige Audiodatei verwandelt:

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

Wir werden:

1. Ein Modell und eine Stimme wählen, die zu langen Vertonungen passen
2. Eine einzelne Speech-Anfrage stellen und das Audio speichern
3. Einen langen Artikel in Chunks aufteilen, die in das Zeichenlimit passen
4. Die synthetisierten Chunks zu einer Datei ohne hörbare Nähte zusammenfügen
5. Den Artikel in etwas umschreiben, das sich anzuhören lohnt
6. Die Teile kombinieren und uns dann Streaming für den interaktiven Einsatz ansehen

## Einrichtung

Du brauchst Python 3.9 oder neuer, das Paket `requests` und einen Venice-API-Schlüssel.

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

## 1. Ein Modell und eine Stimme wählen

Stimmen gehören zu Modellen. Eine Stimme aus einer Familie an ein Modell einer anderen zu senden ist der häufigste Anfängerfehler, beginne also damit aufzulisten, was jedes Modell tatsächlich akzeptiert:

```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` ist die maßgebliche Stimmenliste für ein Modell, und `supported_formats` sagt dir, welche `response_format`-Werte es akzeptiert. Lasse das `| length` aus der Abfrage weg, um die Stimmennamen selbst auszugeben.

Wir verwenden `tts-xai-v1` mit der Stimme `eve`. Es unterstützt `pcm`, was das Zusammenfügen von Chunks in Abschnitt 4 einfach macht.

<Note>
  Die Synthesegeschwindigkeit variiert zwischen TTS-Modellen weit stärker als die Ausgabequalität, und der Unterschied ist groß genug, um deine Architektur zu ändern. Miss eine realistische Anfrage gegen zwei oder drei Kandidaten, bevor du dich festlegst. Ein Chunk, den ein Modell in wenigen Sekunden zurückgibt, kann bei einem anderen mehrere Minuten dauern.
</Note>

## 2. Eine einzelne Anfrage stellen

Der Response-Body ist rohes Audio und kein JSON, also schreibe die Bytes direkt in eine Datei.

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

Nicht passende Kombinationen werden abgelehnt, bevor irgendein Audio erzeugt wird, und der Fehler sagt dir, was stattdessen funktioniert hätte:

```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. Text am 4096-Zeichen-Limit aufteilen

Das Feld `input` akzeptiert höchstens 4096 Zeichen. Längerer Text wird direkt abgelehnt statt stillschweigend abgeschnitten:

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

Also teilen wir den Artikel zuerst auf. Das Aufteilen an Satzgrenzen ist wichtig, denn ein Chunk, der mitten im Satz endet, erzeugt an der Verbindungsstelle ein hörbares Stolpern. Erstelle `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
```

Bei einem 5362 Zeichen langen Skript ergibt das vier Chunks, die jeweils an einem Satzende schließen:

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

Der Standardwert für `max_chars` ist 1500 statt eines Werts nahe der 4096er-Obergrenze, und das ist Absicht. Die Synthesezeit wächst mit der Eingabelänge, kleinere Chunks kommen also schneller zurück und beenden – weil sie parallel laufen – die gesamte Aufgabe insgesamt schneller. Sie machen außerdem Wiederholungsversuche günstig, wenn eine Anfrage fehlschlägt.

## 4. Die Chunks zu einer Datei zusammenfügen

Kodierte Audioformate wie MP3 zu verketten ist unzuverlässig, weil jeder Chunk seine eigenen Frame-Header trägt. `pcm` anzufordern vermeidet das Problem vollständig. PCM sind rohe Samples ohne Container, das Zusammenfügen ist also nur ein Anhängen von Bytes, und das `wave`-Modul aus Pythons Standardbibliothek schreibt den Header für uns.

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

Ein einzelner Chunk kommt schnell zurück, gemessen daran, wie viel Audio er enthält:

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

Diese Anfrage brauchte etwa 13 Sekunden, um 89 Sekunden Sprache zu erzeugen. Die vier Chunks nebenläufig auszuführen, hält die Gesamtdauer im Rahmen: Die vollständige Vertonung unten brauchte 15 Sekunden Wanduhrzeit.

`ThreadPoolExecutor.map` gibt Ergebnisse in der Reihenfolge zurück, in der die Eingaben übergeben wurden, sodass die Chunks in Leserichtung landen, obwohl sie gleichzeitig synthetisiert wurden.

<Warning>
  Rohes PCM trägt keine Sample-Rate, du musst also beim Schreiben des WAV-Headers die richtige selbst angeben, und sie ist modellspezifisch. `tts-xai-v1` liefert 24 kHz, während `tts-gradium-v1` 48 kHz liefert. Rate falsch, und die Vertonung spielt mit falscher Geschwindigkeit und Tonhöhe.
</Warning>

Um die Rate für ein beliebiges Modell zu finden, fordere einen kurzen Clip als `wav` an und lies den Header, den es zurückliefert:

```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. Text so aufbereiten, dass er sich richtig anhört

Gescrapte Markdown-Inhalte wörtlich vorzulesen ist nahezu unerträglich anzuhören. URLs sind das deutlichste Beispiel. Ein Sprachmodell buchstabiert sie Zeichen für Zeichen aus, sodass `https://docs.venice.ai/llms.txt` so klingt:

> h t t p s Doppelpunkt Schrägstrich Schrägstrich docs Punkt venice Punkt a i l l m s Punkt t x t

Überschriften, Aufzählungszeichen, Tabellen und Codeblöcke verursachen kleinere Versionen desselben Problems. Statt Markdown mit regulären Ausdrücken zu bekämpfen, können wir ein Chat-Modell bitten, den Artikel als etwas umzuschreiben, das zum Sprechen gedacht ist. Erstelle `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()
```

Die `URL_PATTERN`-Ersetzung bleibt als Sicherheitsnetz für den gelegentlichen Link erhalten, den das Modell übrig lässt.

## 6. Alles zusammenfügen

Der Einstiegspunkt scrapt, schreibt das Skript, speichert es und vertont:

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

`script.txt` neben dem Audio abzuspeichern ist die zwei Zeilen wert. Wenn eine Vertonung falsch klingt, zeigt das Skript fast immer, warum, und du kannst es beheben, ohne für eine erneute Synthese zu zahlen.

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

Knapp unter sechs Minuten Vertonung, erzeugt in etwa fünfzehn Sekunden. Das Skript beginnt jetzt mit Prosa statt mit Navigationsbeiwerk:

> Venice is built on a simple but powerful principle. User privacy comes first. The platform's entire architecture flows from this philosophical commitment.

<Tip>
  Um eine Vertonung zu prüfen, ohne sie ganz anhören zu müssen, schicke das Audio zurück durch [`/audio/transcriptions`](/guides/media/speech-to-text) und vergleiche das Transkript mit `script.txt`. Die letzten zwanzig Sekunden zu transkribieren ist eine schnelle Möglichkeit, um zu bestätigen, dass die Chunks in der richtigen Reihenfolge zusammengefügt wurden, und findet in Sekunden fehlende Chunks und ausbuchstabierte URLs.
</Tip>

## Streaming für den interaktiven Einsatz

Batch-Vertonung optimiert die Gesamtzeit. Eine Sprachschnittstelle hat die gegenteilige Priorität, nämlich das erste Audio so schnell wie möglich auszugeben. Wenn du `streaming: true` setzt, wird der Body Satz für Satz zurückgegeben, während er erzeugt wird, sodass die Wiedergabe nach etwa einer Sekunde starten kann, statt auf den kompletten Clip zu warten.

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

Bevorzuge `pcm` gegenüber `mp3`, wenn du die Web Audio API eines Browsers oder direkt ein Audiogerät fütterst, da hier kein Dekodierschritt nötig ist.

## Nützliche Anfrageoptionen

| Parameter     | Hinweise                                                                                                                                                                                                                                 |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `speed`       | Akzeptiert `0.25` bis `4.0`, Standard `1.0`. Für Vertonung ungefähr im Bereich `0.8` bis `1.3` bleiben. Darüber hinaus klingt der Vortrag nicht mehr natürlich.                                                                          |
| `language`    | Optionaler Hinweis, dessen akzeptierte Form modellspezifisch ist. xAI und ElevenLabs erwarten ISO-639-1-Codes wie `en`, während Qwen 3 und MiniMax vollständige Namen wie `English` erwarten. Nicht unterstützte Werte werden ignoriert. |
| `prompt`      | Stil- und Emotionshinweis, bis zu 500 Zeichen, wird derzeit nur von den Qwen-3-Modellen berücksichtigt. Bei anderen Familien trägt die Wahl der Stimme den Ton.                                                                          |
| `temperature` | Bereich `0` bis `2`, unterstützt von Qwen 3, Orpheus und Chatterbox HD. Erhöhe sie für mehr Variation zwischen Takes.                                                                                                                    |

## Fehler

| Status           | Ursache                             | Behebung                                                  |
| ---------------- | ----------------------------------- | --------------------------------------------------------- |
| `400`            | `input` über 4096 Zeichen           | Text wie in Abschnitt 3 in Chunks aufteilen               |
| `400`            | Stimme für das Modell ungültig      | Eine Stimme aus `model_spec.voices` des Modells verwenden |
| `400`            | Format vom Modell nicht unterstützt | `model_spec.supported_formats` prüfen                     |
| `401`            | Fehlender oder ungültiger Schlüssel | Header `Authorization: Bearer` bestätigen                 |
| `402`            | Guthaben nicht ausreichend          | Konto aufladen                                            |
| `429`            | Ratenbegrenzung                     | `max_workers` senken und mit Backoff wiederholen          |
| `500` oder `503` | Kapazitäts- oder Inferenzfehler     | Betroffenen Chunk mit Jitter erneut versuchen             |

Weil die Chunks voneinander unabhängig sind, kostet ein Fehlschlag immer nur einen von ihnen, und `synthesize` für diesen Chunk erneut auszuführen ist stets sicher.

## Nächste Schritte

Ein paar natürliche Erweiterungen von hier aus:

* Audio nach einem Hash von Text, Stimme und Modell cachen, damit unveränderte Absätze nie erneut synthetisiert werden.
* Eine geklonte Stimme über [Voice Cloning](/guides/media/voice-cloning) einsetzen, damit die Vertonung deine eigene verwendet.
* Den Quelltext erzeugen, statt ihn zu scrapen, mit [Zitierte Antworten mit Websuche](/guides/tools/cited-web-answers).
* Intro- oder Hintergrund-Audio mit [Musik und Soundeffekte](/guides/media/music-and-sound-effects) hinzufügen.

<CardGroup cols={2}>
  <Card title="Text-to-Speech" icon="volume-2" href="/guides/media/text-to-speech">
    Referenz für den Speech-Endpunkt und seine Parameter.
  </Card>

  <Card title="Voice Cloning" icon="user" href="/guides/media/voice-cloning">
    Mit einer eigenen Stimme statt eines Presets vertonen.
  </Card>

  <Card title="Zitierte Antworten mit Websuche" icon="search" href="/guides/tools/cited-web-answers">
    Den Text erzeugen, den dieses Tool vertont.
  </Card>

  <Card title="Speech-to-Text" icon="microphone" href="/guides/media/speech-to-text">
    Audio transkribieren, um eine Vertonung durchgehend zu verifizieren.
  </Card>
</CardGroup>
