GUIA

Uma voz própria no Vapi: seu servidor de TTS em 30 linhas

O Vapi roda agentes de voz com IA para telefone e web e deixa você trazer seu próprio texto para fala: com o provedor custom-voice ele envia cada fala do assistente a um servidor seu e toca o áudio que volta.

Este guia monta esse servidor em Python. Ele gera a voz com o motor gratuito da SuperClonica na sua própria máquina, então a fala em si não custa nada, ou pela API de voz da SuperClonica. De qualquer forma seu agente pode usar qualquer voz do catálogo, de narradores calmos a personagens de jogo, ou uma voz treinada a partir da sua gravação.

O que o Vapi envia e espera

Segundo o guia de TTS próprio do Vapi, ele envia um POST com JSON cujo objeto message traz type voice-request, o texto e o sampleRate desejado. A resposta deve ser PCM bruto sem cabeçalho: mono, 16 bits com sinal, little-endian, exatamente nessa taxa, com o tipo de conteúdo application/octet-stream. Com um secret embutido, o Vapi o envia no cabeçalho X-VAPI-SECRET.

O servidor

Instale os pacotes e salve isto como 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")

O motor gera a voz a 44,1 kHz numa thread de trabalho, o scipy reamostra para a taxa que o Vapi pediu, e o servidor devolve PCM de 16 bits. Requisições sem o segredo certo recebem 401. Inicie com uvicorn:

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

Conecte ao seu assistente

O Vapi precisa alcançar o servidor por HTTPS público: coloque-o atrás do seu proxy reverso habitual ou, para testar, de um túnel. Depois defina a voz do assistente, por exemplo ao atualizar o assistente pela API do Vapi:

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

No servidor, defina VAPI_SECRET com o mesmo valor. O guia do Vapi também descreve credenciais criadas no painel e uma voz reserva, que vale adicionar para a ligação continuar falando se seu servidor cair.

O mesmo servidor pela API

Se o servidor for pequeno demais para o modelo, gere a voz pela API da SuperClonica: troque voice_audio por esta função e defina SUPERCLONICA_KEY e VOICE_ID. A reamostragem e a resposta continuam iguais.

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

Toda voz do catálogo funciona pelo ID da página dela, em qualquer plano. O Inicial custa US$ 5 por mês por até 3 minutos de fala em cada janela de 10 minutos para todas as chamadas juntas; o Pro, 20 minutos.

Velocidade e vozes

O servidor gera cada requisição inteira antes de responder. Nas nossas medições, uma frase curta levou em mediana 0,43 s num Ryzen de mesa com quatro threads e 0,12 s numa RTX 5070 Ti, então mantenha curtas as frases do assistente. TTS_VOICE aceita uma das dez vozes padrão, M1 a M5 e F1 a F5, ou um arquivo de voz do catálogo; o Supertonic 3 fala 31 idiomas, incluindo português (TTS_LANGUAGE=pt), mas ainda não chinês.