Une voix personnalisée pour Vapi : votre serveur TTS en 30 lignes
Vapi fait tourner des agents vocaux IA pour le téléphone et le web et vous laisse apporter votre propre synthèse vocale : avec le fournisseur custom-voice, il envoie chaque réplique de l’assistant à un serveur que vous gérez et joue l’audio renvoyé.
Ce guide construit ce serveur en Python. Il lit le texte avec le moteur gratuit SuperClonica sur votre propre machine, si bien que la parole elle-même ne coûte rien, ou via l’API vocale de SuperClonica. Dans les deux cas, votre agent peut utiliser n’importe quelle voix du catalogue, du narrateur posé au personnage de jeu, ou une voix entraînée à partir de votre propre enregistrement.
Ce que Vapi envoie et attend
Selon le guide TTS personnalisé de Vapi, Vapi envoie un POST avec un JSON dont l’objet message contient type voice-request, le texte et le sampleRate voulu. La réponse doit être du PCM brut sans en-tête : mono, 16 bits signés, little-endian, exactement à ce débit, avec le type de contenu application/octet-stream. Avec un secret en ligne, Vapi l’envoie dans l’en-tête X-VAPI-SECRET.
Le serveur
Installez les paquets et enregistrez ceci sous 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")Le moteur lit le texte à 44,1 kHz dans un thread de travail, scipy le rééchantillonne au débit demandé par Vapi, et le serveur le renvoie en PCM 16 bits. Les requêtes sans le bon secret reçoivent 401. Lancez-le avec uvicorn :
uvicorn vapi_voice:app --host 0.0.0.0 --port 8000
Le relier à votre assistant
Vapi doit joindre le serveur en HTTPS public : placez-le derrière votre proxy inverse habituel ou, pour un essai, derrière un tunnel. Réglez ensuite la voix de l’assistant, par exemple en le mettant à jour via l’API Vapi :
{
"voice": {
"provider": "custom-voice",
"server": {
"url": "https://your-server.example.com/synthesize",
"secret": "the value of VAPI_SECRET",
"timeoutSeconds": 30
}
}
}Sur le serveur, donnez la même valeur à VAPI_SECRET. Le guide de Vapi décrit aussi les identifiants créés dans son tableau de bord et une voix de secours, utile pour que l’appel continue de parler si votre serveur tombe.
Le même serveur via l’API
Si votre serveur est trop petit pour le modèle, faites lire le texte par l’API vocale de SuperClonica : remplacez voice_audio par cette fonction et définissez SUPERCLONICA_KEY et VOICE_ID. Le rééchantillonnage et la réponse ne changent pas.
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, rateChaque voix du catalogue fonctionne avec l’ID de sa page, dans toute offre. Débutant coûte 5 $ par mois pour jusqu’à 3 minutes de parole par fenêtre de 10 minutes pour tous les appels ensemble, Pro 20 minutes.
Vitesse et voix
Le serveur lit chaque requête en entier avant de répondre. Dans nos mesures, une phrase courte prenait en médiane 0,43 s sur un Ryzen de bureau avec quatre threads et 0,12 s sur une RTX 5070 Ti : gardez donc les phrases de l’assistant courtes. TTS_VOICE accepte l’une des dix voix de base, M1 à M5 et F1 à F5, ou un fichier de voix du catalogue ; Supertonic 3 parle 31 langues, dont le français (TTS_LANGUAGE=fr), mais pas encore le chinois.