광고 자리 모집 중 · 제휴 문의

개발자가 PlayHT의 스트리밍 API를 연동하여 대화형 에이전트의 실시간 음성 응답을 생성하는 방법

365Finds Editorial
Wiki 문서 11,430편 수록

내 AI에서 365 Wiki 사용하기

아래 프롬프트를 복사해 평소 사용하는 AI에 붙여넣고 바로 질문하세요.

전체 Skill 다운로드

먼저 프롬프트로 체험해 보세요. 365 Wiki를 자주 쓴다면 전체 365Wiki Skill을 설치해 매번 붙여넣는 수고를 줄일 수 있습니다. 평소 쓰는 AI가 없다면 Gemini나 Microsoft Copilot 웹 버전도 사용할 수 있습니다.

대화형 음성 에이전트에 PlayHT의 스트리밍 API를 연동하면 개발자는 인지 대기 시간을 최소화하면서 실시간에 가까운 자연스러운 음성 응답을 제공할 수 있습니다. 고객 지원 봇, 인터랙티브 게임 캐릭터, 음성 비서와 같은 대화형 시스템에서 기존의 배치 방식 텍스트 음성 변환(TTS)은 완전한 문장이나 단락이 합성될 때까지 기다려야 하므로 허용하기 어려운 지연을 초래합니다. WebSocket 또는 청크 방식의 HTTP를 통해 영구 스트리밍 채널을 구축함으로써, 개발자는 대형 언어 모델(LLM)이 생성한 점진적 텍스트 토큰을 PlayHT의 신경망 합성 엔진에 직접 파이프라인으로 전달하여 즉각적인 오디오 재생을 실현할 수 있습니다. 본 가이드에서는 인증 구성, WebSocket 연결 관리, 토큰 청킹 전략 최적화, 대화 중 끼어들기(barge-in) 처리, 그리고 텔레포니 환경의 특수 오디오 요구 사항 충족 방법을 상세히 설명합니다.

실시간 대화형 음성 에이전트 아키텍처

프로덕션 수준의 대화형 음성 에이전트는 서로 분리된 4개의 하위 시스템으로 구성된 연속 전이중(full-duplex) 루프로 동작합니다.

  1. 음성 텍스트 변환(STT): 실시간 전사 엔진(예: Deepgram Nova-2 또는 Whisper Streaming)이 마이크나 전화선에서 사용자 음성을 캡처하고, 음향 프레임을 텍스트 토큰으로 변환하며, 화자가 말을 멈추는 시점을 감지합니다.
  2. 언어 추론(LLM): 대형 언어 모델(예: GPT-4o, Claude 3.5 Sonnet 또는 Groq 호스팅 오픈소스 모델)이 대화 기록과 최신 사용자 질의를 처리하여 서버 전송 이벤트(SSE)를 통해 순차적으로 응답 토큰을 출력합니다.
  3. 텍스트 음성 변환(TTS): 저지연 합성 엔진(예: PlayHT 스트리밍 파이프라인)이 들어오는 텍스트 토큰을 수신하고, 음성학적 구조를 파싱하며, LLM이 전체 문장 생성을 마치기 전에 원시 오디오 청크를 합성합니다.
  4. 전송 및 오디오 재생: 오디오 전송 계층(예: WebRTC, WebSocket 또는 Twilio Media Streams)이 합성된 오디오 패킷을 클라이언트 플레이어나 전화 핸드셋으로 스트리밍하여 즉시 재생합니다.

배치 TTS 아키텍처에서는 LLM이 먼저 완전한 문장(보통 800~2,000밀리초 소요)을 생성해야 하며, 이후 TTS 엔진이 재생을 시작하기 전에 오디오 파일을 렌더링하는 데 500~1,500밀리초가 추가로 필요합니다. 이 1.5~3.5초의 누적 지연은 자연스러운 대화의 몰입감을 해칩니다. 반면 스트리밍 파이프라인은 LLM 생성과 음성 합성을 중첩시킵니다. 음성 엔진은 뒤따르는 어구가 생성되는 동안 처음 몇 단어 합성을 시작하므로 종단 간 대기 시간을 400~800밀리초로 대폭 단축합니다.

PlayHT 스트리밍 인터페이스 및 인증

PlayHT는 초저지연 및 상태 유지 대화 세션을 위해 설계된 양방향 WebSocket API와 더 단순한 단방향 스트리밍을 위한 HTTP 청크 전송 엔드포인트라는 두 가지 주요 스트리밍 인터페이스를 제공했습니다.

1. 인증 및 WebSocket URL 획득

URL 쿼리 문자열로 자격 증명을 받는 공개 WebSocket 엔드포인트와 달리, PlayHT는 소켓을 열기 전에 보안 REST 핸드셰이크로 인증 과정을 분리했습니다. 이를 통해 전송 로그나 프록시 추적에 민감한 API 키가 노출되는 것을 방지했습니다.

인증을 위해 개발자는 인증 게이트웨이로 인증된 HTTP POST 요청을 전송했습니다.

POST https://api.play.ht/api/v4/websocket-auth
Authorization: Bearer <YOUR_API_KEY>
X-User-Id: <YOUR_USER_ID>
Content-Type: application/json

서버는 자격 증명을 검증하고 사용 가능한 합성 엔진에 매핑된 임시 서명된 WebSocket 엔드포인트가 포함된 JSON 페이로드를 반환했습니다.

{
  "websocket_urls": {
    "PlayDialog": "wss://api.play.ht/v1/websocket/playdialog?auth=<SIGNED_TOKEN>",
    "Play3.0-mini": "wss://api.play.ht/v1/websocket/play3.0-mini?auth=<SIGNED_TOKEN>"
  }
}

서명된 URL에는 수명이 짧은 암호화 토큰이 포함되어 있어, 클라이언트 애플리케이션이 마스터 자격 증명을 재전송하지 않고도 보안 WebSocket 연결을 수립할 수 있었습니다.

2. 지원되는 스트리밍 모델

PlayHT는 다양한 대화 워크로드에 최적화된 차별화된 모델을 유지했습니다.

WebSocket 스트리밍 프로토콜 및 메시지 흐름

할당된 WebSocket URL에 연결되면 통신은 정형화된 JSON-over-WebSocket 프로토콜을 따릅니다. 연결 수명 주기는 세션 시작, 증분 텍스트 전송, 바이너리 오디오 수신 및 스트림 완료로 구성됩니다.

1. 페이로드 매개변수

음성 생성을 시작하기 위해 클라이언트는 합성 매개변수가 포함된 JSON 프레임을 전송합니다.

{
  "text": "Hello! I can help you check your account balance today.",
  "voice": "s3://voice-cloning-zero-shot/d9ff78ba-d016-47f6-b0ef-dd630f59414e/narrator/manifest.json",
  "output_format": "mp3",
  "sample_rate": 24000,
  "speed": 1.0,
  "temperature": 0.7
}

주요 구성 옵션은 다음과 같습니다.

2. 서버 메시지 수명 주기

PlayHT WebSocket 서버는 JSON 메타데이터와 바이너리 오디오 페이로드를 번갈아 전송하여 합성 진행 상황을 알렸습니다.

  1. start 이벤트: 유효한 합성 명령을 수신하면 서버는 JSON 메시지를 전송합니다.
    {"type": "start", "request_id": "req_88492048102"}
    
  2. 바이너리 오디오 프레임: 시작 이벤트 직후 서버는 인코딩된 오디오 패킷(원시 PCM 슬라이스 또는 MP3 프레임 등)이 포함된 순차적 바이너리 프레임을 푸시합니다. 클라이언트 애플리케이션은 이 버퍼를 오디오 출력 드라이버나 WebRTC 미디어 트랙에 직접 전달합니다.
  3. end 이벤트: 텍스트 시퀀스의 합성이 완전히 끝나면 서버는 최종 JSON 프레임을 전송합니다.
    {"type": "end", "request_id": "req_88492048102"}
    

구현 예제

1. WebSocket을 사용한 Python 연동

다음 Python 스크립트는 인증을 수행하고, 스트리밍 URL을 가져오며, 합성된 오디오 청크를 로컬 플레이어나 오디오 싱크로 스트리밍하는 방법을 보여줍니다.

import asyncio
import json
import websockets
import httpx

USER_ID = "YOUR_USER_ID"
API_KEY = "YOUR_API_KEY"
VOICE_ID = "s3://voice-cloning-zero-shot/d9ff78ba-d016-47f6-b0ef-dd630f59414e/narrator/manifest.json"

async def get_websocket_url():
    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://api.play.ht/api/v4/websocket-auth",
            headers={
                "Authorization": f"Bearer {API_KEY}",
                "X-User-Id": USER_ID,
                "Content-Type": "application/json"
            }
        )
        response.raise_for_status()
        data = response.json()
        return data["websocket_urls"]["PlayDialog"]

async def stream_speech(text: str, audio_queue: asyncio.Queue):
    ws_url = await get_websocket_url()
    
    async with websockets.connect(ws_url) as ws:
        payload = {
            "text": text,
            "voice": VOICE_ID,
            "output_format": "mp3",
            "sample_rate": 24000,
            "temperature": 0.7
        }
        await ws.send(json.dumps(payload))

        async for message in ws:
            if isinstance(message, bytes):
                # Put binary audio chunk into playback queue
                await audio_queue.put(message)
            else:
                data = json.loads(message)
                if data.get("type") == "end":
                    await audio_queue.put(None) # Signal playback completion
                    break

async def main():
    audio_queue = asyncio.Queue()
    await stream_speech("Connecting to your server now. How may I direct your call?", audio_queue)

if __name__ == "__main__":
    asyncio.run(main())

2. 공식 SDK를 사용한 Node.js 연동

PlayHT는 WebSocket 핸드셰이크와 연결 재사용을 추상화한 공식 클라이언트 라이브러리(npm의 playht)도 제공했습니다.

import * as PlayHT from "playht";
import { Writable } from "node:stream";

PlayHT.init({
  userId: process.env.PLAYHT_USER_ID,
  apiKey: process.env.PLAYHT_API_KEY,
});

async function synthesizeTextStream(textPrompt, audioDestinationStream) {
  const stream = await PlayHT.stream(textPrompt, {
    voiceEngine: "PlayDialog",
    voiceId: "s3://voice-cloning-zero-shot/d9ff78ba-d016-47f6-b0ef-dd630f59414e/narrator/manifest.json",
    outputFormat: "mp3",
    sampleRate: 24000,
  });

  stream.on("data", (chunk) => {
    audioDestinationStream.write(chunk);
  });

  stream.on("end", () => {
    audioDestinationStream.end();
  });

  stream.on("error", (err) => {
    console.error("PlayHT streaming error:", err);
  });
}

LLM 토큰 스트림과 오디오 합성의 연계

대화형 에이전트를 구축할 때의 핵심 과제는 LLM의 토큰 출력을 TTS 입력에 연결하는 것입니다. LLM은 단어 또는 서브워드 단위로 응답을 출력합니다. 토큰을 한 단어씩 TTS 엔진에 입력하면 발음, 억양 및 음성 연음이 손상됩니다. 반대로 LLM이 전체 단락 생성을 마칠 때까지 기다리면 심각한 대기 시간이 발생합니다.

개발자는 적응형 문장 버퍼링 및 청킹을 통해 이 문제를 해결합니다.

[LLM Token Stream]
       │
       ▼
[Token Buffer & Boundary Detector]
  - Chunk 1: First 4-6 words (Quick start) ──► PlayHT Stream ──► Instant Audio (TTFB < 500ms)
  - Chunk 2: First complete clause           ──► PlayHT Stream ──► Seamless Playback
  - Chunk 3: Next full sentence              ──► PlayHT Stream ──► Continuous Queue

문장 청킹 전략

  1. 초기 버스트 청크(3~6개 단어): 버퍼에 3~6개 단어가 모이거나 초기에 쉼표가 감지되면 즉시 첫 번째 청크를 전송합니다. 사용자는 첫 오디오 청취 시간(TTFA)을 기준으로 응답성을 체감하므로, 이 작은 초기 어구를 즉시 전달하면 인지 대기 시간을 600밀리초 미만으로 유지할 수 있습니다.
  2. 절 및 구두점 경계: 후속 청크는 자연스러운 구두점 경계(., ?, !, ; 또는 공백이 뒤따르는 ,)를 만날 때까지 버퍼링됩니다. 이는 음향 모델이 적절한 음조 윤곽과 음절 길이를 예측하는 데 충분한 언어적 문맥을 제공합니다.
  3. 최대 버퍼 안전 한계치: LLM이 구두점 없이 긴 문장을 생성하는 경우, 오디오 버퍼 고갈을 방지하기 위해 15~20단어 후 적응형 버퍼를 강제로 비웁니다.
// Example: Minimal regex-based token buffer for streaming TTS
class TextStreamBuffer {
  constructor(onChunkReady) {
    this.buffer = "";
    this.isFirstChunk = true;
    this.onChunkReady = onChunkReady;
    this.punctuationRegex = /([.?!;:]\s+|,[\s]+)/;
  }

  addToken(token) {
    this.buffer += token;

    if (this.isFirstChunk) {
      const words = this.buffer.trim().split(/\s+/);
      if (words.length >= 4 || this.punctuationRegex.test(this.buffer)) {
        this.flush();
        this.isFirstChunk = false;
      }
      return;
    }

    const match = this.buffer.match(this.punctuationRegex);
    if (match) {
      const splitIndex = match.index + match[0].length;
      const readyChunk = this.buffer.slice(0, splitIndex).trim();
      this.buffer = this.buffer.slice(splitIndex);
      if (readyChunk) {
        this.onChunkReady(readyChunk);
      }
    }
  }

  flush() {
    const content = this.buffer.trim();
    if (content) {
      this.onChunkReady(content);
      this.buffer = "";
    }
  }
}

끼어들기(Barge-In) 및 대화 중단 관리

사람 간의 대화에서는 상대방이 말하는 도중에도 자주 끼어들거나, 맞장구를 치거나, 이해했음을 나타냅니다. 사용자가 말하고 있음에도 일방적으로 말을 이어가는 음성 에이전트는 부자연스럽고 불편함을 줍니다.

신속한 끼어들기를 구현하려면 음성 활동 감지(VAD)와 오디오 재생 파이프라인의 긴밀한 조정이 필요합니다.

  1. 지속적인 오디오 수음: 에이전트가 발화하는 동안에도 클라이언트 마이크는 열린 상태로 모니터링됩니다(전이중 동작).
  2. 음향 음향 에코 캔슬링(AEC): 클라이언트나 오디오 서버는 하드웨어 또는 소프트웨어 AEC를 적용하여 수신 마이크 신호에서 에이전트의 스피커 출력을 차감해야 합니다. AEC가 없으면 에이전트가 자신이 합성한 음성을 듣고 잘못된 자체 중단을 유발하게 됩니다.
  3. 중단 트리거: VAD가 에너지 및 신뢰도 임계값을 초과하는 인간의 음성을 감지하면(예: Silero VAD가 150밀리초 동안 음성 프레임을 감지):
    • 재생 중지: 로컬 오디오 플레이어는 즉시 재생을 중지하고 대기 중인 모든 오디오 프레임을 폐기합니다.
    • TTS 파이프라인 중단: 애플리케이션은 활성 WebSocket 합성 세션을 취소하거나 대기 중인 PlayHT 스트림을 닫아 불필요한 연산과 네트워크 전송을 중단합니다.
    • LLM 생성 취소: AbortController가 진행 중인 LLM 토큰 생성을 종료합니다.
    • 상태 전이: 상태 머신이 듣기 상태로 리셋되고 새로운 사용자 음성을 STT 엔진으로 전달합니다.

텔레포니 및 엔터프라이즈 오디오 고려 사항

실시간 음성 에이전트를 공중 교환 전화망(PSTN)—Twilio, Plivo, Vonage 등—에 통합하려면 특수한 오디오 형식 변환이 필요합니다.

매개변수표준 웹 / 데스크톱 오디오텔레포니 (Twilio / SIP)
오디오 형식MP3 / WAV / OGGG.711 mu-law (mulaw) / 선형 PCM
샘플링 레이트24,000 Hz – 48,000 Hz8,000 Hz
비트 심도16비트 부호 있음8비트 비선형
패킷 크기동적 / 가변 비트레이트20ms 청크 (8 kHz에서 160바이트)
전송WebRTC / WebSocket양방향 Media Stream WebSocket

전화 통신을 위해 PlayHT를 구성할 때 개발자는 "output_format": "mulaw" 및 "sample_rate": 8000을 지정했습니다. 이를 통해 애플리케이션 서버에서 고샘플링 레이트 MP3 스트림을 전화 통신 형식으로 트랜스코딩하는 CPU 오버헤드를 제거하고, 합성된 오디오 패킷을 Twilio Media Streams로 직접 전달할 수 있었습니다.

최신 에이전트 오케스트레이션 프레임워크

WebSocket 수명 주기, 버퍼링, 끼어들기 관리를 위한 맞춤형 상태 머신을 처음부터 작성하는 대신, 음성 에이전트를 개발하는 엔지니어들은 오픈소스 대화 오케스트레이션 프레임워크를 널리 채택하고 있습니다.

플랫폼 수명 주기 및 마이그레이션 경로

PlayHT는 대화형 AI를 위한 저지연 스트리밍 TTS의 대중화에 중추적인 역할을 했습니다. 2025년 7월 Meta가 PlayHT 팀을 인수한 이후, 해당 플랫폼은 상용 호스팅 서비스를 공식 종료했으며 모든 공개 API 엔드포인트는 2025년 12월 31일부로 영구 중단되었습니다.

기존에 PlayHT를 기반으로 구축된 실시간 대화 아키텍처를 유지 관리하거나 업그레이드하는 개발자는 호환 가능한 저지연 아키텍처를 제공하는 활성 스트리밍 TTS 제공업체로 마이그레이션해야 합니다.

WebSocket 세션 관리, 적응형 토큰 버퍼링, 끼어들기 동기화, 오디오 포맷팅과 같은 스트리밍 합성의 아키텍처 원리를 이해함으로써, 개발자는 다양한 합성 제공업체 간을 유연하게 전환할 수 있는 견고한 저지연 대화형 음성 에이전트를 설계할 수 있습니다.