가이드

Vapi 사용자 목소리: 30줄로 만드는 나만의 TTS 서버

Vapi는 전화와 웹용 AI 음성 에이전트를 돌리는 서비스로, 직접 만든 음성 합성을 연결할 수 있습니다. custom-voice 제공업체를 쓰면 어시스턴트가 하는 말을 한 줄씩 내 서버로 보내고, 돌아온 오디오를 재생합니다.

이 가이드는 그 서버를 Python으로 만듭니다. 내 컴퓨터에서 무료 SuperClonica 엔진으로 읽으면 음성 자체는 비용이 들지 않고, SuperClonica 음성 합성 API를 쓸 수도 있습니다. 어느 쪽이든 차분한 내레이터부터 게임 캐릭터까지 카탈로그의 모든 목소리나 내 녹음으로 학습시킨 목소리를 쓸 수 있습니다.

Vapi가 보내는 것과 기대하는 것

Vapi의 사용자 TTS 가이드에 따르면, Vapi는 JSON POST를 보내며 그 message 객체에는 type voice-request, 텍스트, 원하는 sampleRate가 들어 있습니다. 응답은 헤더 없는 원시 PCM이어야 합니다. 모노, 16비트 부호 있는 정수, 리틀 엔디언, 정확히 그 샘플레이트, Content-Type은 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 kHz로 음성을 만들고, scipy가 Vapi가 요청한 레이트로 리샘플링하며, 서버는 16비트 PCM을 돌려줍니다. 올바른 secret이 없는 요청은 401을 받습니다. uvicorn으로 실행하세요:

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

어시스턴트에 연결하기

Vapi가 공개 HTTPS로 서버에 닿아야 합니다. 평소 쓰는 리버스 프록시 뒤에 두거나, 테스트라면 터널을 쓰세요. 그다음 예를 들어 Vapi API로 어시스턴트를 업데이트할 때 어시스턴트 목소리를 설정합니다:

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

서버의 VAPI_SECRET에도 같은 값을 넣으세요. Vapi 가이드는 대시보드에서 만드는 자격 증명과 예비 목소리도 설명합니다. 서버가 멈춰도 통화가 계속 말하도록 예비 목소리를 추가해 두는 것이 좋습니다.

API로 같은 서버 만들기

서버가 작아 모델을 둘 수 없다면 SuperClonica 음성 합성 API로 읽으세요. 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달러로 모든 통화를 합쳐 10분 창마다 최대 3분, Pro는 20분입니다.

속도와 목소리

서버는 요청마다 끝까지 음성을 만든 뒤 응답합니다. 저희 측정으로 짧은 문장의 중앙값은 4스레드 데스크톱 Ryzen에서 0.43초, RTX 5070 Ti에서 0.12초였으니 어시스턴트 문장은 짧게 유지하세요. TTS_VOICE에는 기본 목소리 10개(M1~M5, F1~F5) 중 하나나 카탈로그 목소리 파일을 넣습니다. Supertonic 3는 한국어(TTS_LANGUAGE=ko)를 포함해 31개 언어를 말하지만 중국어는 아직 지원하지 않습니다.