GUÍA

Una voz propia para Vapi: tu servidor TTS en 30 líneas

Vapi ejecuta agentes de voz con IA para teléfono y web y te deja traer tu propio texto a voz: con el proveedor custom-voice envía cada frase del asistente a un servidor tuyo y reproduce el audio que vuelve.

Esta guía crea ese servidor en Python. Genera la voz con el motor gratuito de SuperClonica en tu propio equipo, así que la voz en sí no cuesta nada, o mediante la API de voz de SuperClonica. En ambos casos tu agente puede usar cualquier voz del catálogo, desde narradores serenos hasta personajes de videojuego, o una voz que entrenes con tu propia grabación.

Qué envía y qué espera Vapi

Según la guía de TTS propio de Vapi, Vapi envía un POST con un JSON cuyo objeto message lleva type voice-request, el texto y el sampleRate que quiere. La respuesta debe ser PCM sin procesar y sin cabecera: mono, 16 bits con signo, little-endian, exactamente a esa frecuencia y con el tipo de contenido application/octet-stream. Con un secret en línea, Vapi lo envía en la cabecera X-VAPI-SECRET.

El servidor

Instala los paquetes y guarda esto 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")

El motor genera la voz a 44,1 kHz en un hilo de trabajo, scipy la remuestrea a la frecuencia que pidió Vapi y el servidor la devuelve como PCM de 16 bits. Las peticiones sin el secreto correcto reciben 401. Arráncalo con uvicorn:

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

Conéctalo a tu asistente

Vapi tiene que llegar al servidor por HTTPS público: ponlo detrás de tu proxy inverso habitual o, para probar, de un túnel. Después configura la voz del asistente, por ejemplo al actualizar el asistente con la API de Vapi:

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

Define en el servidor VAPI_SECRET con el mismo valor. La guía de Vapi también describe credenciales creadas en su panel y una voz de respaldo, que conviene añadir para que la llamada siga hablando si tu servidor cae.

El mismo servidor por la API

Si tu servidor es demasiado pequeño para el modelo, genera la voz con la API de voz de SuperClonica: sustituye voice_audio por esta función y define SUPERCLONICA_KEY y VOICE_ID. El remuestreo y la respuesta no cambian.

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

Todas las voces del catálogo funcionan con el ID de su página, en cualquier plan. Inicio cuesta 5 $ al mes por hasta 3 minutos de voz en cada ventana de 10 minutos para todas las llamadas juntas; Pro, 20 minutos.

Velocidad y voces

El servidor genera cada petición entera antes de responder. En nuestras mediciones, una frase corta tardó una mediana de 0,43 s en un Ryzen de sobremesa con cuatro hilos y 0,12 s en una RTX 5070 Ti, así que mantén cortas las frases del asistente. TTS_VOICE admite una de las diez voces de serie, M1 a M5 y F1 a F5, o un archivo de voz del catálogo; Supertonic 3 habla 31 idiomas, incluido el español (TTS_LANGUAGE=es), pero aún no chino.