指南

Vapi 自定义声音:30 行自建 TTS 服务器

请注意:Supertonic 3 目前不支持中文,本文中的语音可朗读其支持的 31 种语言。Vapi 运行面向电话和网页的 AI 语音智能体,并允许你接入自己的文字转语音:使用 custom-voice 提供方时,它会把助手说的每一句发送到你运行的服务器,并播放返回的音频。

本指南用 Python 编写这个服务器。它在你自己的机器上用免费的 SuperClonica 引擎朗读,语音本身不花钱;也可以通过 SuperClonica 语音合成 API 朗读。无论哪种方式,智能体都能使用目录中的任意声音,从沉稳的旁白到游戏角色,或者你根据自己的录音训练的声音。

Vapi 发送什么、期待什么

根据 Vapi 的自定义 TTS 指南,Vapi 会发送一个 JSON POST 请求,其中 message 对象包含 type voice-request、要朗读的文字和所需的 sampleRate。响应必须是不带文件头的原始 PCM:单声道、16 位有符号、小端序,采样率与请求完全一致,内容类型为 application/octet-stream。如果设置了内联 secret,Vapi 会通过 X-VAPI-SECRET 请求头发送它。

服务器代码

安装这些包,并把下面的代码保存为 vapi_voice.py:

pip install fastapi uvicorn scipy superclonica-cpu

import math, os
import numpy as np
from fastapi import FastAPI, Header, HTTPException, Request, Response
from fastapi.concurrency import run_in_threadpool
from scipy.signal import resample_poly
from superclonica import Superclonica

VOICE = os.environ.get("TTS_VOICE", "F1")  # M1..M5, F1..F5 or a voice file from the catalog
LANGUAGE = os.environ.get("TTS_LANGUAGE", "en")
SECRET = os.environ["VAPI_SECRET"]
app = FastAPI()
tts = Superclonica()

def voice_audio(text):
    audio, rate = tts.synthesize(text, voice=VOICE, lang=LANGUAGE)
    return np.asarray(audio, dtype=np.float32).reshape(-1), rate

@app.post("/synthesize")
async def synthesize(request: Request, x_vapi_secret: str = Header(None)):
    if x_vapi_secret != SECRET:
        raise HTTPException(401)
    message = (await request.json())["message"]
    audio, rate = await run_in_threadpool(voice_audio, message["text"])
    target = int(message["sampleRate"])
    if target != rate:
        divisor = math.gcd(target, rate)
        audio = resample_poly(audio, target // divisor, rate // divisor)
    pcm = (np.clip(audio, -1, 1) * 32767).astype(np.int16).tobytes()
    return Response(pcm, media_type="application/octet-stream")

引擎在工作线程中以 44.1 kHz 合成语音,scipy 将其重采样到 Vapi 要求的采样率,服务器返回 16 位 PCM。没有正确 secret 的请求会得到 401。用 uvicorn 启动:

uvicorn vapi_voice:app --host 0.0.0.0 --port 8000

连接到你的助手

Vapi 需要通过公网 HTTPS 访问服务器:把它放在你常用的反向代理后面,测试时也可以用隧道。然后设置助手的声音,例如在通过 Vapi API 更新助手时:

{
  "voice": {
    "provider": "custom-voice",
    "server": {
      "url": "https://your-server.example.com/synthesize",
      "secret": "the value of VAPI_SECRET",
      "timeoutSeconds": 30
    }
  }
}

在服务器上把 VAPI_SECRET 设为相同的值。Vapi 的指南还介绍了在控制台创建的凭据和备用声音;建议加上备用声音,这样即使你的服务器宕机,通话也不会中断。

通过 API 实现同一个服务器

如果服务器太小放不下模型,可以通过 SuperClonica 语音合成 API 朗读:把 voice_audio 换成下面的函数,并设置 SUPERCLONICA_KEY 和 VOICE_ID。重采样和响应保持不变。

import io, wave
import requests

API = "https://superclonica.com/api/v1/speech"
VOICE_ID = os.environ["VOICE_ID"]  # the ID on the page of a catalog voice

def voice_audio(text):
    answer = requests.post(API, timeout=60,
                           headers={"Authorization": "Bearer " + os.environ["SUPERCLONICA_KEY"]},
                           json={"text": text, "voice": VOICE_ID, "language": LANGUAGE, "format": "wav"})
    answer.raise_for_status()
    with wave.open(io.BytesIO(answer.content)) as file:
        rate = file.getframerate()
        pcm = np.frombuffer(file.readframes(file.getnframes()), dtype=np.int16)
    return pcm.astype(np.float32) / 32768, rate

目录中的每个声音都能凭其页面上的 ID 在任何套餐中使用。入门套餐每月 5 美元,所有通话合计每个 10 分钟窗口最多 3 分钟语音;Pro 为 20 分钟。

速度与声音

服务器会把每个请求完整合成后再响应。按我们的测量,一句短语的中位耗时在四线程台式机 Ryzen 上为 0.43 秒,在 RTX 5070 Ti 上为 0.12 秒,所以请让助手的句子保持简短。TTS_VOICE 可以是十个内置声音(M1–M5、F1–F5)之一,也可以是目录中的声音文件;Supertonic 3 支持 31 种语言,但暂不支持中文。