野生流动开发者文档
语音合成调用(TTS)

VoxCPM2 语音合成

使用耐久 Job API 稳定生成 VoxCPM2 语音,并正确处理长文本。

VoxCPM2 是野生流动的首方中文语音合成模型,精确运行版本为 openbmb/VoxCPM2。请求以文本作为 输入。当前生产 Artifact 为 MP3(audio/mpeg);当前目录价格为 ¥0.8 / 万字符,实时可用状态和最终 账单以模型广场与控制台为准。

语音生成可能经历排队、模型准备和生成,因此使用耐久 Job,而不是要求客户端一直保持同步连接。 下面的示例包含提交、等待和下载步骤。需要了解重试时,可查看任务与结果文件。

快速调用

以下命令适用于 macOS / Linux,需要 Python 3 和 cURL。每次新任务生成新的幂等键,重试同一任务时保留原键。

先获取 API Key,并为这次业务请求生成一个稳定且唯一的幂等键:

export WILDFLOW_API_KEY="替换为控制台生成的密钥"
export WILDFLOW_IDEMPOTENCY_KEY="$(python3 -c 'import uuid; print(uuid.uuid4())')"

提交任务:

curl --fail-with-body --silent --show-error -D response-headers.txt -X POST https://api.wildflow.cn/v1/jobs \
  -H "Authorization: Bearer $WILDFLOW_API_KEY" \
  -H "Idempotency-Key: $WILDFLOW_IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "VoxCPM2",
    "parameters": {
      "input": "你好,欢迎使用野生流动。",
      "voice": "default",
      "speed": 1.0
    }
  }' \
  --output operation.json

首次接收成功返回 HTTP 202,响应头 Location 指向 /v1/jobs/{operation_id}。重复提交相同 API Key、 幂等键和请求正文会返回原 Operation;同一幂等键配合不同请求会返回 409。

Windows CMD

Windows CMD 不要把 input、voice、speed 放在 JSON 顶层,也不要把提交 Job 的 JSON 响应直接保存为 speech.wav 或 speech.mp3。先把下面内容保存为 UTF-8 编码的 request.json:

{
  "model": "VoxCPM2",
  "parameters": {
    "input": "你好,欢迎使用野生流动。",
    "voice": "default",
    "speed": 1.0
  }
}

在 CMD 中提交任务。--data-binary 会原样上传 UTF-8 文件;-D 把响应头单独保存,避免 -i 将响应头 混入 JSON 文件:

set "WILDFLOW_API_KEY=替换为控制台新生成的密钥"
set "WILDFLOW_IDEMPOTENCY_KEY=voxcpm2-request-001"

curl.exe --fail-with-body -sS -D response-headers.txt -X POST "https://api.wildflow.cn/v1/jobs" ^
  -H "Authorization: Bearer %WILDFLOW_API_KEY%" ^
  -H "Idempotency-Key: %WILDFLOW_IDEMPOTENCY_KEY%" ^
  -H "Content-Type: application/json" ^
  --data-binary "@request.json" ^
  --output operation.json

用 type operation.json 查看响应并复制 id。首次成功应为 202;后续用同一个幂等键和完全相同的 request.json 重放时可能返回 200,两者都不是音频响应:

set "OPERATION_ID=把 operation.json 中的 id 粘贴到这里"

curl.exe --fail-with-body -sS ^
  -H "Authorization: Bearer %WILDFLOW_API_KEY%" ^
  "https://api.wildflow.cn/v1/jobs/%OPERATION_ID%" ^
  --output status.json

当 status.json 中的 state 变为 succeeded 后,复制 artifacts[0].id 再下载真正的 MP3:

set "ARTIFACT_ID=把 status.json 中 artifacts[0].id 粘贴到这里"

curl.exe --fail-with-body -sS ^
  -H "Authorization: Bearer %WILDFLOW_API_KEY%" ^
  "https://api.wildflow.cn/v1/artifacts/%ARTIFACT_ID%/content" ^
  --output result.mp3

参数

参数必需允许值说明
input 或 text是1–20,000 个 Unicode 字符二选一,不能同时传入;服务端自动按语义边界分段
voice是见下表标准模型名不会自动补默认音色
speed否0.25–4保持音高的语速倍率,默认 1.0

可用音色

voice名称说明
shuoshuren说书人沉稳男声,适合叙事内容
dabin大斌磁性男声,适合故事讲述
tingting婷婷温柔女声,适合自然表达
default默认VoxCPM2 无条件合成音色
wangliqun王立群已配置的自有克隆音色

音色是同一个 VoxCPM2 模型的请求参数,不是独立模型。使用真人或克隆音色前,应取得相应授权,不得 用于冒充、欺诈或误导他人。

轮询与下载

先完成上面的提交步骤,确认已生成 operation.json。然后设置下载文件名:

export RESULT_FILE="result.mp3"
export RESULT_MEDIA_TYPE="audio/mpeg"

下面的命令使用 Python 3,读取上一步保存的 operation.json,等待任务完成后下载结果。等待超过两小时会停止;再次执行本段会继续查询原任务,不会重新提交。

python3 - <<'PY'
import hashlib
import json
import os
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from pathlib import Path
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen

base = "https://api.wildflow.cn"
headers = {"Authorization": "Bearer " + os.environ["WILDFLOW_API_KEY"]}
operation = json.loads(Path("operation.json").read_text())
operation_id = operation["id"]
status_url = base + "/v1/jobs/" + quote(operation_id, safe="")
output = Path(os.environ["RESULT_FILE"])
expected_media_type = os.environ["RESULT_MEDIA_TYPE"]
deadline = time.monotonic() + 7200

def pause(value):
    try:
        seconds = float(value)
    except (TypeError, ValueError):
        try:
            seconds = (parsedate_to_datetime(value) - datetime.now(timezone.utc)).total_seconds()
        except (TypeError, ValueError, OverflowError):
            seconds = 3
    seconds = max(1, seconds)
    if time.monotonic() + seconds >= deadline:
        raise SystemExit("等待超时。保留 operation.json,稍后重新执行本段继续查询。")
    time.sleep(seconds)

while time.monotonic() < deadline:
    try:
        with urlopen(Request(status_url, headers=headers), timeout=60) as response:
            status = json.load(response)
            retry_after = response.headers.get("Retry-After", "3")
    except HTTPError as error:
        if error.code in (429, 503):
            pause(error.headers.get("Retry-After", "3"))
            continue
        raise SystemExit(f"查询失败:HTTP {error.code}。保留原任务,检查错误后继续查询。")

    state = status["state"]
    print("任务状态:", state, flush=True)
    if state == "succeeded":
        artifacts = status.get("artifacts", [])
        if not artifacts:
            raise SystemExit("任务成功但缺少结果文件,请保留任务 ID 并联系支持。")
        artifact = artifacts[0]
        if artifact["media_type"] != expected_media_type:
            raise SystemExit("结果格式与预期不同,请检查任务和模型。")
        download_url = base + "/v1/artifacts/" + quote(artifact["id"], safe="") + "/content"
        temporary = output.with_name(output.name + ".part")
        digest = hashlib.sha256()
        size = 0
        with urlopen(Request(download_url, headers=headers), timeout=60) as response:
            with temporary.open("wb") as file:
                while chunk := response.read(1024 * 1024):
                    file.write(chunk)
                    digest.update(chunk)
                    size += len(chunk)
        if size != artifact["size_bytes"] or digest.hexdigest() != artifact["sha256"]:
            raise SystemExit("文件校验失败,保留原任务并重新执行本段下载。")
        temporary.replace(output)
        print("已保存:", output)
        break
    if state not in ("queued", "running", "submitting"):
        raise SystemExit(f"任务已停止:{state}。请保留任务 ID;不要换幂等键重复提交。")
    pause(retry_after)
else:
    raise SystemExit("等待超时。保留 operation.json,稍后继续查询原任务。")
PY

网络中断时可重新执行本段。failed、cancelled、recovery_required 或未知状态会停止下载;需要帮助时查看任务恢复说明。

下载后可核对 media_type=audio/mpeg、size_bytes 和 sha256。Artifact 的 metadata 还会提供 input_characters、completed_characters、segment_count、completed_segment_count、duration_ms、 codec、bitrate、sample_rate 和 channels。如果任务仍在排队或执行,请继续轮询, 不要更换幂等键重复扣费。

长文本 MP3

单个 Job 最多接受 20,000 个 Unicode 字符。服务端会在句号、问号、感叹号、逗号和换行等语义边界 自动拆成约 300 字的小段,在同一 Job 内按原文顺序生成、缓存和拼接,最后只编码一次 96 kbps、48 kHz、 单声道 MP3。调用方无需再把约 3,000 字的正文拆成多个请求。

每篇正文使用一个稳定且唯一的 Idempotency-Key。网络中断或轮询超时时,应以相同请求正文和相同键 恢复原 Operation;不要换键创建第二个付费任务。服务端只有在字符数、片段数、文件大小和 SHA-256 全部自洽后才将 Job 标记为 succeeded,不完整或结果不确定时进入失败或 recovery_required。

当前生产已经验收 36 字和 3,200 字请求;3,200 字结果为 12/12 片段完整、约 13.16 分钟 MP3。 20,000 字是接口硬上限,不代表所有内容、音色和速度组合都具有同样耗时。接近上限的任务应预留更长 轮询时间,并在正式批量生产前先用代表性文稿验证听感。

可用 ffprobe 检查实际产物:

ffprobe -v error \
  -show_entries stream=codec_name,sample_rate,channels,bit_rate:format=duration \
  -of json result.mp3

预期为 MP3、48 kHz、单声道、96 kbps。自动完整性门禁用于检测技术性截断,不能代替内容验收;长文本 仍建议抽听开头、中间和结尾,并检查时长是否与文字量大致相符。

错误与恢复

统一错误和恢复原则见任务与结果文件。VoxCPM2 还应重点检查:

  • 400 invalid_request:检查 input、voice、speed 是否放在 parameters 内,并检查文本、必传音色和速度范围;
  • 401 / 403:检查 API Key、额度和令牌允许的模型范围;
  • 409 idempotency_conflict:同一幂等键已经绑定了不同请求;
  • 429 / 503:按响应中的 Retry-After 等待后有限重试;
  • recovery_required:结果或账务状态需要平台恢复,不要自动创建第二个付费任务。

不要在日志、前端代码或问题截图中记录完整 API Key,也不要记录不必要的敏感文本。

相关文档

获取 API Key · 价格与计费 · 常见问题