실시간 음성 채팅
TanStack AI는 음성 대 음성 AI 상호작용을 구축할 수 있는 완전한 실시간 음성 채팅 시스템을 제공합니다. 실시간 API는 여러 provider(OpenAI, ElevenLabs), 자동 도구 실행, 오디오 시각화, 이미지가 포함된 멀티모달 입력을 지원합니다.
개요
실시간 음성 채팅은 텍스트 기반 채팅과 다음과 같은 몇 가지 중요한 차이가 있습니다.
- 양방향 오디오 - 사용자가 마이크에 대고 말하면 AI가 합성 음성으로 응답합니다.
- 음성 활동 감지(VAD) - 사용자가 말하기 시작하고 멈추는 시점을 자동으로 감지합니다.
- 인터럽트 - 사용자가 AI의 응답 중간에 인터럽트를 발생시킬 수 있습니다.
- 낮은 지연 시간 - WebRTC 또는 WebSocket 연결을 사용해 거의 즉각적으로 통신합니다.
- 멀티모달 - 음성과 함께 텍스트 입력, 이미지 입력, 도구 호출을 지원합니다.
실시간 시스템은 다른 TanStack AI 구성 요소와 동일한 어댑터 아키텍처를 따릅니다.
- 서버는 provider별 토큰 어댑터와 함께
realtimeToken()을 사용해 임시 토큰을 생성합니다. - 클라이언트는 provider별 연결 어댑터와 함께
RealtimeClient(React에서는useRealtimeChat)를 사용해 연결합니다. - Provider 어댑터는 OpenAI WebRTC, ElevenLabs WebSocket 등의 프로토콜 차이를 처리합니다.
빠른 시작
1. 서버 토큰 엔드포인트 설정
서버는 API 키가 클라이언트에 절대 전달되지 않도록 수명이 짧은 토큰을 생성합니다.
import { realtimeToken } from '@tanstack/ai'
import { openaiRealtimeToken } from '@tanstack/ai-openai'
import { createServerFn } from '@tanstack/react-start'
const getRealtimeToken = createServerFn({ method: 'POST' })
.handler(async () => {
return realtimeToken({
adapter: openaiRealtimeToken({
model: 'gpt-realtime',
}),
})
})
참고:
realtimeToken()함수는 모든 서버 프레임워크에서 작동합니다. 위 예제에서는 TanStack Start를 사용하지만, HTTP 요청을 처리할 수 있는 Express, Hono, Fastify 또는 다른 프레임워크를 사용할 수 있습니다.
2. 클라이언트에서 연결(React)
import { useRealtimeChat } from '@tanstack/ai-react'
import { openaiRealtime } from '@tanstack/ai-openai'
function VoiceChat() {
const {
status,
mode,
messages,
connect,
disconnect,
pendingUserTranscript,
pendingAssistantTranscript,
inputLevel,
outputLevel,
} = useRealtimeChat({
getToken: () => fetch('/api/realtime-token', { method: 'POST' }).then(r => r.json()),
adapter: openaiRealtime(),
instructions: 'You are a helpful voice assistant.',
voice: 'alloy',
})
return (
<div>
<p>Status: {status}</p>
<p>Mode: {mode}</p>
<button onClick={status === 'idle' ? connect : disconnect}>
{status === 'idle' ? 'Start Conversation' : 'End Conversation'}
</button>
{pendingUserTranscript && <p>You: {pendingUserTranscript}...</p>}
{pendingAssistantTranscript && <p>AI: {pendingAssistantTranscript}...</p>}
{messages.map((msg) => (
<div key={msg.id}>
<strong>{msg.role}:</strong>
{msg.parts.map((part, i) => (
<span key={i}>
{part.type === 'text' ? part.content : null}
{part.type === 'audio' ? part.transcript : null}
</span>
))}
</div>
))}
</div>
)
}
프로바이더
OpenAI Realtime
OpenAI의 실시간 API는 지연 시간이 짧은 음성 통신을 위해 WebRTC를 사용합니다.
서버(토큰 생성):
import { realtimeToken } from '@tanstack/ai'
import { openaiRealtimeToken } from '@tanstack/ai-openai'
const token = await realtimeToken({
adapter: openaiRealtimeToken({
model: 'gpt-realtime',
}),
})
클라이언트(연결):
import { openaiRealtime } from '@tanstack/ai-openai'
const adapter = openaiRealtime()
환경 변수: OPENAI_API_KEY
사용 가능한 모델:
| 모델 | 설명 |
|---|---|
gpt-realtime | 완전한 실시간 모델 |
gpt-realtime-mini | 더 작고 빠른 실시간 모델 |
사용 가능한 음성: alloy, ash, ballad, coral, echo, sage, shimmer, verse, marin, cedar
ElevenLabs Realtime
ElevenLabs는 WebSocket 연결을 사용하며 대시보드에 에이전트를 구성해야 합니다.
서버(토큰 생성):
import { realtimeToken } from '@tanstack/ai'
import { elevenlabsRealtimeToken } from '@tanstack/ai-elevenlabs'
const token = await realtimeToken({
adapter: elevenlabsRealtimeToken({
agentId: 'your-agent-id',
}),
})
클라이언트(연결):
import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs'
const adapter = elevenlabsRealtime()
환경 변수: ELEVENLABS_API_KEY, ELEVENLABS_AGENT_ID(선택 사항)
음성 활동 감지(VAD)
VAD는 시스템이 말하기 시작하고 멈췄다고 감지하는 시점을 제어합니다. 세 가지 모드를 사용할 수 있습니다.
| 모드 | 작동 방식 | 적합한 경우 |
|---|---|---|
server | provider가 오디오 에너지 수준을 사용해 서버 측에서 음성을 감지합니다. | 기본값 — 간단하며 클라이언트 복잡도가 낮습니다. |
semantic | 일시 중지와 문장 완성 같은 의미적 단서를 사용해 발화 종료를 감지합니다. | 자연스러운 대화 — 문장 중간에 끊는 것을 방지합니다. |
manual | startListening() / stopListening()을 명시적으로 호출합니다. | 푸시 투 토크 인터페이스 |
훅을 생성할 때 VAD 모드를 설정합니다.
import { useRealtimeChat } from '@tanstack/ai-react'
import { openaiRealtime } from '@tanstack/ai-openai'
import { getToken } from './token'
const { startListening, stopListening, updateSession } = useRealtimeChat({
getToken,
adapter: openaiRealtime(),
vadMode: 'manual', // or 'server' or 'semantic'
})
manual VAD 모드에서는 푸시 투 토크 방식의 상호작용을 사용합니다.
<button onMouseDown={startListening} onMouseUp={stopListening}>
Hold to talk
</button>
updateSession을 사용하면 다시 연결하지 않고 런타임에 VAD 모드를 전환할 수 있습니다.
updateSession({ vadMode: 'semantic' })
semantic VAD에서는 eagerness를 구성해 모델이 발화를 마쳤다고 판단하기 전에 기다리는 시간을 제어합니다.
import { useRealtimeChat } from '@tanstack/ai-react'
import { openaiRealtime } from '@tanstack/ai-openai'
import { getToken } from './token'
const chat = useRealtimeChat({
getToken,
adapter: openaiRealtime(),
vadMode: 'semantic',
semanticEagerness: 'low', // waits longer before detecting end-of-speech
})
도구
실시간 세션은 클라이언트 측 도구를 지원합니다. 표준 toolDefinition() API를 사용해 도구를 정의하고 클라이언트 구현을 전달합니다.
import { toolDefinition } from '@tanstack/ai'
import { useRealtimeChat } from '@tanstack/ai-react'
import { openaiRealtime } from '@tanstack/ai-openai'
import { getToken } from './token'
import { z } from 'zod'
const getWeatherDef = toolDefinition({
name: 'getWeather',
description: 'Get weather for a location',
inputSchema: z.object({
location: z.string().meta({ description: 'City name' }),
}),
outputSchema: z.object({
temperature: z.number(),
conditions: z.string(),
}),
})
const getWeather = getWeatherDef.client(async ({ location }) => {
const res = await fetch(`/api/weather?location=${location}`)
return res.json()
})
// Pass tools to the hook
const chat = useRealtimeChat({
getToken,
adapter: openaiRealtime(),
tools: [getWeather],
})
실시간 클라이언트는 도구 호출을 자동으로 실행하고 결과를 provider에 다시 전송합니다. 도구 호출은 메시지에서 tool-call 및 tool-result 파트로 표시됩니다.
텍스트 및 이미지 입력
음성 외에도 텍스트 메시지와 이미지를 보낼 수 있습니다.
import { useRealtimeChat } from '@tanstack/ai-react'
import { openaiRealtime } from '@tanstack/ai-openai'
import { getToken } from './token'
import { base64ImageData } from './assets'
const { sendText, sendImage } = useRealtimeChat({ getToken, adapter: openaiRealtime() })
// Send a text message
sendText('What is the weather like today?')
// Send an image (base64 data or URL)
sendImage(base64ImageData, 'image/png')
오디오 시각화
useRealtimeChat은 레벨 미터, 파형, 스펙트럼 분석기를 구축할 수 있도록 오디오 분석 데이터를 제공합니다.
import { useRealtimeChat } from '@tanstack/ai-react'
import { openaiRealtime } from '@tanstack/ai-openai'
import { getToken } from './token'
const {
inputLevel, // 0–1 normalized microphone level
outputLevel, // 0–1 normalized speaker level
getInputFrequencyData, // Uint8Array — FFT bins for spectrum analyzer
getOutputFrequencyData,
getInputTimeDomainData, // Uint8Array — waveform samples for oscilloscope
getOutputTimeDomainData,
} = useRealtimeChat({ getToken, adapter: openaiRealtime() })
연결된 동안 inputLevel 및 outputLevel 값은 모든 애니메이션 프레임에서 업데이트되므로 CSS 애니메이션이나 canvas 시각화를 구동하는 데 적합합니다.
간단한 레벨 미터:
<div style={{ width: `${inputLevel * 100}%`, height: 4, background: 'green' }} />
맥동하는 오디오 표시기:
function AudioIndicator({ level }: { level: number }) {
return (
<div
style={{
width: 40,
height: 40,
borderRadius: '50%',
transform: `scale(${1 + level * 0.5})`,
backgroundColor: `rgba(59, 130, 246, ${0.3 + level * 0.7})`,
transition: 'transform 0.1s ease',
}}
/>
)
}
canvas를 사용하는 스펙트럼 분석기:
function drawSpectrum(canvas: HTMLCanvasElement) {
const ctx = canvas.getContext('2d')!
const draw = () => {
const data = getInputFrequencyData()
ctx.clearRect(0, 0, canvas.width, canvas.height)
const barWidth = canvas.width / data.length
data.forEach((value, i) => {
const height = (value / 255) * canvas.height
ctx.fillRect(i * barWidth, canvas.height - height, barWidth - 1, height)
})
requestAnimationFrame(draw)
}
draw()
}
인터럽트
사용자는 AI가 말하는 동안 인터럽트를 발생시킬 수 있습니다.
import { useRealtimeChat } from '@tanstack/ai-react'
import { openaiRealtime } from '@tanstack/ai-openai'
import { getToken } from './token'
const { interrupt, mode } = useRealtimeChat({ getToken, adapter: openaiRealtime() })
// Programmatically interrupt
if (mode === 'speaking') {
interrupt()
}
server 또는 semantic VAD에서는 사용자가 말하기 시작할 때 인터럽트가 자동으로 발생합니다. 인터럽트된 메시지는 메시지 배열에서 interrupted: true로 표시됩니다.
RealtimeClient 직접 사용
React를 사용하지 않는 애플리케이션이나 더 세밀한 제어가 필요한 경우 RealtimeClient를 직접 사용합니다.
import { RealtimeClient } from '@tanstack/ai-client'
import { openaiRealtime } from '@tanstack/ai-openai'
const client = new RealtimeClient({
getToken: () => fetch('/api/realtime-token', { method: 'POST' }).then(r => r.json()),
adapter: openaiRealtime(),
instructions: 'You are a helpful assistant.',
voice: 'alloy',
onMessage: (message) => {
console.log(`${message.role}:`, message.parts)
},
onStatusChange: (status) => {
console.log('Status:', status)
},
onModeChange: (mode) => {
console.log('Mode:', mode)
},
})
// Connect
await client.connect()
// Send text
client.sendText('Hello!')
// Subscribe to state changes
const unsub = client.onStateChange((state) => {
console.log('Messages:', state.messages.length)
})
// Disconnect when done
await client.disconnect()
// Clean up
client.destroy()
ElevenLabs 사용
TanStack AI는 대체 실시간 음성 provider로 ElevenLabs를 지원합니다. 클라이언트 API는 동일하므로 어댑터와 토큰 함수만 교체하면 됩니다.
import { useRealtimeChat } from '@tanstack/ai-react'
import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs'
const { status, messages, connect, disconnect } = useRealtimeChat({
getToken: () => fetch('/api/elevenlabs-token').then(r => r.json()),
adapter: elevenlabsRealtime(),
})
참고: ElevenLabs는 에이전트 기반 구성을 사용하므로 음성과 시스템 프롬프트는 ElevenLabs 대시보드 또는 토큰 재정의를 통해 설정합니다. 설정 방법은 ElevenLabs 어댑터 페이지를 참고합니다.
다음 단계
- 도구 - 동형 도구 시스템을 알아봅니다.
- Text-to-Speech - 비실시간 음성 생성
- 멀티모달 콘텐츠 - 이미지, 오디오, 동영상 작업
- ElevenLabs 어댑터 - ElevenLabs 실시간 음성 provider 설정 및 구성
고급 사용법
이 기능을 작동시키는 데 필요하지 않은 참고 세부 정보입니다.
세션 구성
훅 옵션을 통해 실시간 세션을 구성합니다.
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
getToken | () => Promise<RealtimeToken> | 필수 | 서버에서 토큰을 가져오는 함수 |
adapter | RealtimeAdapter | 필수 | Provider 어댑터(openaiRealtime(), elevenlabsRealtime()) |
instructions | string | — | 어시스턴트를 위한 시스템 지침 |
voice | string | — | 오디오 출력에 사용할 음성 |
tools | AnyClientTool[] | — | 실행 로직이 포함된 클라이언트 측 도구 |
vadMode | 'server' | 'semantic' | 'manual' | 'server' | 음성 활동 감지 모드 |
semanticEagerness | 'low' | 'medium' | 'high' | — | semantic VAD의 eagerness |
autoPlayback | boolean | true | 어시스턴트 오디오 자동 재생 |
autoCapture | boolean | true | 연결할 때 마이크 요청 |
outputModalities | Array<'audio' | 'text'> | — | 응답 모달리티 |
temperature | number | — | 생성 temperature |
maxOutputTokens | number | 'inf' | — | 응답의 최대 토큰 수 |
연결 수명 주기
실시간 클라이언트는 다음 상태로 연결 수명 주기를 관리합니다.
| 상태 | 설명 |
|---|---|
idle | 연결되지 않음 |
connecting | 연결 설정 중 |
connected | 활성 세션 |
reconnecting | 인터럽트 후 다시 연결 중 |
error | 연결 오류 발생 |
연결 중에는 다음 모드를 사용합니다.
| 모드 | 설명 |
|---|---|
idle | 연결되었지만 상호작용하지 않음 |
listening | 사용자 오디오 입력 캡처 중 |
thinking | 사용자 입력 처리 중 |
speaking | AI가 응답 생성 중 |
import { useRealtimeChat } from '@tanstack/ai-react'
import { openaiRealtime } from '@tanstack/ai-openai'
import { getToken } from './token'
import { useEffect } from 'react'
const { status, mode, error, connect, disconnect } = useRealtimeChat({ getToken, adapter: openaiRealtime() })
// Handle connection
useEffect(() => {
if (status === 'error' && error) {
console.error('Connection error:', error.message)
}
}, [status, error])
메시지 구조
실시간 메시지는 UIMessage와 유사한 parts 기반 구조를 사용합니다.
import type { RealtimeMessagePart } from '@tanstack/ai'
interface RealtimeMessage {
id: string
role: 'user' | 'assistant'
timestamp: number
parts: Array<RealtimeMessagePart>
interrupted?: boolean
}
각 파트는 다음 중 하나일 수 있습니다.
| 파트 타입 | 필드 | 설명 |
|---|---|---|
text | content | sendText()로 보낸 텍스트 콘텐츠 |
audio | transcript, durationMs | 전사된 음성 콘텐츠 |
tool-call | id, name, arguments, input, output | 도구 호출 |
tool-result | toolCallId, content | 도구 실행 결과 |
image | data, mimeType | sendImage()로 보낸 이미지 |
오류 처리
onError 콜백 또는 error 상태를 통해 오류를 처리합니다.
import { useRealtimeChat } from '@tanstack/ai-react'
import { openaiRealtime } from '@tanstack/ai-openai'
import { getToken } from './token'
const { error } = useRealtimeChat({
getToken,
adapter: openaiRealtime(),
onError: (err: Error) => {
if (err.message.includes('Permission denied')) {
alert('Microphone access is required for voice chat.')
} else {
console.error('Realtime error:', err)
}
},
})
모범 사례
- 토큰 보안 - 항상 서버 측에서 토큰을 생성합니다. API 키를 클라이언트에 절대 노출하지 않습니다.
- 마이크 권한 - 사용자가 마이크 액세스를 거부하는 경우를 적절히 처리합니다.
- 정리 - 컴포넌트를 마운트 해제할 때 항상 연결을 해제합니다.
useRealtimeChat훅이 이를 자동으로 처리합니다. - 지침 - 음성 어시스턴트 지침을 간결하게 유지합니다. 응답이 대화형으로 유지되도록 모델에 음성 인터페이스라는 점을 상기시킵니다.
- 도구 설계 - 결과가 실시간으로 처리되므로 도구 설명은 명확하게, 도구 출력은 작게 유지합니다.
- 오류 복구 - 일시적인 연결 실패에 대한 재시도 로직을 구현합니다.