Свой голос для Vapi: собственный TTS-сервер в 30 строк
Vapi запускает голосовых AI-агентов для телефона и веба и позволяет подключить свою озвучку текста: с провайдером custom-voice он отправляет каждую реплику ассистента на ваш сервер и проигрывает звук, который тот вернёт.
В этом руководстве мы соберём такой сервер на Python. Он озвучивает текст бесплатным движком SuperClonica на вашей машине, так что сама речь ничего не стоит, или через API озвучки SuperClonica. В обоих случаях агенту доступны любые голоса каталога — от спокойных дикторов до игровых персонажей — или голос, обученный по вашей записи.
Что Vapi присылает и чего ждёт
Согласно руководству Vapi по своему TTS, Vapi отправляет POST с JSON, в котором объект message содержит type voice-request, текст и нужный sampleRate. Ответ должен быть сырым PCM без заголовка: моно, 16 бит со знаком, little-endian, ровно с этой частотой и с типом содержимого 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 кГц в рабочем потоке, scipy пересэмплирует его под частоту, которую попросил Vapi, и сервер возвращает 16-битный PCM. Запросы без правильного секрета получают 401. Запустите его через uvicorn:
uvicorn vapi_voice:app --host 0.0.0.0 --port 8000
Подключение к ассистенту
Vapi должен достучаться до сервера по публичному HTTPS: поставьте его за ваш обычный обратный прокси или, для проверки, за туннель. Затем задайте голос ассистента, например при обновлении ассистента через API Vapi:
{
"voice": {
"provider": "custom-voice",
"server": {
"url": "https://your-server.example.com/synthesize",
"secret": "the value of VAPI_SECRET",
"timeoutSeconds": 30
}
}
}На сервере задайте в VAPI_SECRET то же значение. Руководство Vapi описывает и учётные данные, созданные в его панели, и запасной голос — его стоит добавить, чтобы звонок не замолкал, если ваш сервер недоступен.
Тот же сервер через API
Если сервер слишком мал для модели, озвучивайте текст через API озвучки SuperClonica: замените 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 в месяц и даёт до 3 минут речи в каждом 10-минутном окне на все звонки вместе, Pro — 20 минут.
Скорость и голоса
Сервер озвучивает каждый запрос целиком, прежде чем ответить. По нашим замерам короткая фраза озвучивалась в среднем за 0,43 с на настольном Ryzen с четырьмя потоками и за 0,12 с на RTX 5070 Ti, так что держите реплики ассистента короткими. TTS_VOICE — один из десяти встроенных голосов, M1–M5 и F1–F5, или файл голоса из каталога; Supertonic 3 говорит на 31 языке, в том числе по-русски (TTS_LANGUAGE=ru), но пока не по-китайски.