ANLEITUNG

Eine eigene Stimme für Vapi: Ihr TTS-Server in 30 Zeilen

Vapi betreibt KI-Sprachagenten für Telefon und Web und lässt Sie Ihr eigenes Text-to-Speech einbinden: Mit dem Anbieter custom-voice schickt es jede Zeile des Assistenten an einen Server, den Sie betreiben, und spielt das zurückkommende Audio ab.

Diese Anleitung baut diesen Server in Python. Er vertont den Text mit der kostenlosen SuperClonica-Engine auf Ihrem eigenen Rechner, sodass die Sprache selbst nichts kostet, oder über die Sprach-API von SuperClonica. So oder so kann Ihr Agent jede Katalogstimme nutzen, vom ruhigen Erzähler bis zur Spielfigur, oder eine Stimme, die Sie aus Ihrer eigenen Aufnahme trainieren.

Was Vapi sendet und erwartet

Laut Vapis Anleitung zu eigenem TTS sendet Vapi einen POST mit JSON, dessen message-Objekt type voice-request, den Text und die gewünschte sampleRate enthält. Die Antwort muss rohes PCM ohne Header sein: mono, 16 Bit mit Vorzeichen, Little Endian, mit genau dieser Abtastrate und dem Inhaltstyp application/octet-stream. Mit einem Inline-Secret schickt Vapi es im Header X-VAPI-SECRET.

Der Server

Installieren Sie die Pakete und speichern Sie dies als 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")

Die Engine vertont den Text mit 44,1 kHz in einem Worker-Thread, scipy rechnet ihn auf die von Vapi verlangte Rate um, und der Server gibt 16-Bit-PCM zurück. Anfragen ohne das richtige Secret erhalten 401. Starten Sie ihn mit uvicorn:

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

Mit Ihrem Assistenten verbinden

Vapi muss den Server über öffentliches HTTPS erreichen: Stellen Sie ihn hinter Ihren üblichen Reverse Proxy oder für einen Test hinter einen Tunnel. Dann setzen Sie die Stimme des Assistenten, etwa beim Aktualisieren des Assistenten über die Vapi-API:

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

Setzen Sie VAPI_SECRET auf dem Server auf denselben Wert. Vapis Anleitung beschreibt auch Zugangsdaten aus dem Dashboard und eine Ersatzstimme, die sich lohnt, damit ein Anruf weiterspricht, falls Ihr Server ausfällt.

Derselbe Server über die API

Ist Ihr Server zu klein für das Modell, vertonen Sie den Text über die Sprach-API von SuperClonica: Ersetzen Sie voice_audio durch diese Funktion und setzen Sie SUPERCLONICA_KEY und VOICE_ID. Umrechnung und Antwort bleiben gleich.

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

Jede Katalogstimme funktioniert mit der ID von ihrer Seite, in jedem Tarif. Start kostet 5 $ im Monat für bis zu 3 Minuten Sprache in jedem 10-Minuten-Fenster für alle Anrufe zusammen, Pro 20 Minuten.

Tempo und Stimmen

Der Server vertont jede Anfrage vollständig, bevor er antwortet. In unseren Messungen dauerte ein kurzer Satz im Median 0,43 s auf einem Desktop-Ryzen mit vier Threads und 0,12 s auf einer RTX 5070 Ti, halten Sie die Sätze des Assistenten also kurz. TTS_VOICE nimmt eine der zehn Standardstimmen M1 bis M5 und F1 bis F5 oder eine Stimmdatei aus dem Katalog; Supertonic 3 spricht 31 Sprachen, darunter Deutsch (TTS_LANGUAGE=de), aber noch kein Chinesisch.