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

# 使用文本转语音朗读文章

> 使用 Venice 文本转语音将任意网页文章转换为朗读音频文件，涵盖音色选择、4096 字符上限、无缝拼接音频及流式传输。

发起一次 `/audio/speech` 调用很简单。朗读一篇真实文章则会带出各种有趣的问题：该端点每次请求最多接受 4096 个字符，每个音色都属于特定的模型，音频格式因模型而异，而写来阅读的文本与写来朗读的文本相差甚远。

在本教程中，我们会一一解决这四个问题。最终得到的脚本可以将一个 URL 转换为单个音频文件：

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

我们会：

1. 选择适合长篇朗读的模型和音色
2. 发起一次语音请求并保存音频
3. 将长文章切分为符合字符上限的片段
4. 将合成的片段无缝拼接为一个文件
5. 将文章改写为值得朗读的内容
6. 组合各部分，然后了解适用于交互场景的流式传输

## 环境准备

你需要 Python 3.9 或更新版本、`requests` 包，以及一个 Venice API 密钥。

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

## 1. 选择模型和音色

音色隶属于模型。将某个家族的音色发送给另一家族的模型，是最常见的初次犯错。所以先列出每个模型实际支持的音色：

```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` 是某个模型音色列表的权威来源，而 `supported_formats` 会告诉你它接受哪些 `response_format` 值。去掉查询中的 `| length` 就可以打印出音色名称本身。

我们将使用 `tts-xai-v1` 搭配音色 `eve`。它支持 `pcm`，这正是让第 4 节中拼接片段变得简单的关键。

<Note>
  不同 TTS 模型之间的合成速度差距远大于输出质量的差距，而这个差距足以改变你的架构。在决定选用之前，用一次真实请求对两三个候选者进行计时。同一段片段在一个模型里几秒钟返回，在另一个模型里可能要几分钟。
</Note>

## 2. 发起一次请求

响应体是原始音频而非 JSON，因此直接将字节写入文件。

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

不匹配的组合会在生成任何音频之前就被拒绝，错误信息会告诉你哪些值本可以使用：

```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. 在 4096 字符上限处切分文本

`input` 字段最多接受 4096 个字符。更长的文本会被直接拒绝，而不会静默截断：

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

因此我们要先切分文章。按句子边界切分很重要，因为在句子中间结束的片段会在拼接处产生明显的顿挫。创建 `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
```

对一段 5362 字符的脚本运行后会产生四个片段，每个都以句子结尾：

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

`max_chars` 的默认值是 1500，而不是接近 4096 的上限，这是有意为之。合成时间随输入长度增加，因此较小的片段返回更快，而由于它们是并行的，整个任务也完成得更快。它们还让失败时的重试成本更低。

## 4. 将片段拼接为一个文件

拼接已编码音频（例如 MP3）并不可靠，因为每个片段都自带帧头。请求 `pcm` 可以完全避免这个问题。PCM 是没有容器的原始采样，因此拼接就只是追加字节，Python 标准库的 `wave` 模块会替我们写入文件头。

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

相较于生成的音频时长，单个片段的返回速度很快：

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

这次请求大约用 13 秒生成了 89 秒的语音。并发运行这四个片段是让总耗时保持合理的关键：下面完整朗读的实际耗时仅为 15 秒。

`ThreadPoolExecutor.map` 会按输入的提交顺序返回结果，因此即便这些片段是同时合成的，也会按阅读顺序落位。

<Warning>
  原始 PCM 不携带采样率，因此在写入 WAV 头时必须自行提供正确的采样率，且该值因模型而异。`tts-xai-v1` 返回 24 kHz，而 `tts-gradium-v1` 返回 48 kHz。如果猜错，朗读的速度和音高都会错。
</Warning>

要查找任意模型的采样率，只需请求一小段 `wav` 音频并读取其文件头：

```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. 准备听起来自然的文本

抓取到的 Markdown 若逐字朗读几乎无法听。URL 就是最典型的例子。语音模型会一个字符一个字符地把它们念出来，因此 `https://docs.venice.ai/llms.txt` 会被念成：

> h t t p s 冒号 斜杠 斜杠 docs 点 venice 点 a i l l m s 点 t x t

标题、项目符号、表格和代码块都会引发类似但程度较轻的问题。与其用正则表达式与 Markdown 搏斗，不如让聊天模型将文章改写成适合朗读的内容。创建 `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()
```

`URL_PATTERN` 替换作为兜底保留，以防模型偶尔遗留下链接。

## 6. 把它们串联起来

入口会执行抓取、写脚本、保存并朗读：

```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` 与音频一起保存下来非常值得多写这两行代码。当朗读听起来有问题时，脚本几乎总能告诉你原因，你可以在无需再次付费合成的情况下修复它。

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

近六分钟的朗读，大约在十五秒内生成。脚本现在以正文开头，而不是导航元素：

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

<Tip>
  想在不完整听完的情况下核对一段朗读，可以将音频通过 [`/audio/transcriptions`](/guides/media/speech-to-text) 再发送回去，然后将转录结果与 `script.txt` 对照。转录最后二十秒是快速确认片段拼接顺序是否正确的方式，也能在几秒内发现丢失的片段或被念出来的 URL。
</Tip>

## 用于交互场景的流式传输

批量朗读优化的是总耗时。语音交互界面的优先级正好相反：越早发出第一段音频越好。将 `streaming` 设为 `true`，响应体会随生成过程逐句返回，因此播放大约可以在一秒内开始，而不必等待完整片段。

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

当你将音频直接送入浏览器的 Web Audio API 或音频设备时，优先选择 `pcm` 而非 `mp3`，因为它不需要解码。

## 值得了解的请求参数

| 参数            | 说明                                                                                                            |
| ------------- | ------------------------------------------------------------------------------------------------------------- |
| `speed`       | 取值范围 `0.25` 到 `4.0`，默认 `1.0`。用于朗读时大致保持在 `0.8` 到 `1.3`。超出后语气就不再自然。                                             |
| `language`    | 可选提示，其接受的形式因模型而异。xAI 和 ElevenLabs 接受 ISO 639-1 代码（例如 `en`），而 Qwen 3 和 MiniMax 接受完整名称（例如 `English`）。不支持的值会被忽略。 |
| `prompt`      | 风格与情感提示，最多 500 个字符，目前仅 Qwen 3 系列模型采用。对于其他家族，音色的选择本身就传达了语调。                                                    |
| `temperature` | 取值范围 `0` 到 `2`，Qwen 3、Orpheus 和 Chatterbox HD 支持。提高数值可让不同次生成之间的变化更大。                                          |

## 错误

| 状态            | 原因                  | 修复方法                              |
| ------------- | ------------------- | --------------------------------- |
| `400`         | `input` 超过 4096 个字符 | 如第 3 节所述切分文本                      |
| `400`         | 音色对该模型无效            | 使用该模型 `model_spec.voices` 中的音色    |
| `400`         | 模型不支持该格式            | 检查 `model_spec.supported_formats` |
| `401`         | 密钥缺失或无效             | 确认 `Authorization: Bearer` 请求头    |
| `402`         | 余额不足                | 为账户充值                             |
| `429`         | 触发限速                | 降低 `max_workers` 并使用退避重试          |
| `500` 或 `503` | 容量或推理失败             | 对受影响的片段带抖动重试                      |

由于各片段相互独立，一次失败最多只会损失其中一个，对该片段重试 `synthesize` 总是安全的。

## 后续步骤

从这里出发的一些自然扩展：

* 以文本、音色和模型的哈希为键缓存音频，这样未更改的段落就不会被反复合成。
* 借助 [语音克隆](/guides/media/voice-cloning) 换用克隆的音色，让朗读使用你自己的声音。
* 使用 [基于网页搜索的带引用回答](/guides/tools/cited-web-answers) 生成源文本，而不是抓取。
* 使用 [音乐与音效](/guides/media/music-and-sound-effects) 加入片头或背景音。

<CardGroup cols={2}>
  <Card title="文本转语音" icon="volume-2" href="/guides/media/text-to-speech">
    语音端点及其参数的参考文档。
  </Card>

  <Card title="语音克隆" icon="user" href="/guides/media/voice-cloning">
    使用自定义音色而非预设音色进行朗读。
  </Card>

  <Card title="基于网页搜索的带引用回答" icon="search" href="/guides/tools/cited-web-answers">
    生成本工具要朗读的文本。
  </Card>

  <Card title="语音转文本" icon="microphone" href="/guides/media/speech-to-text">
    对音频进行转录，用于端到端验证朗读效果。
  </Card>
</CardGroup>
