텍스트 음성 변환 (TTS)
TanStack AI는 전용 TTS 어댑터를 통해 텍스트 음성 변환 생성을 지원합니다. 이 가이드에서는 OpenAI 및 Gemini 프로바이더를 사용해 텍스트를 음성 오디오로 변환하는 방법을 설명합니다.
개요
텍스트 음성 변환(TTS)은 TanStack AI의 다른 어댑터와 동일한 트리 셰이킹 가능한 아키텍처를 따르는 TTS 어댑터로 처리됩니다. TTS 어댑터는 다음을 지원합니다.
- OpenAI: TTS-1, TTS-1-HD, 그리고 오디오 기능을 갖춘 GPT-4o 모델
- Gemini: Gemini 2.5 Flash TTS (실험적)
- BytePlus: Seed Speech (
seed-audio-1.0) - fal.ai: Kokoro, ElevenLabs, MiniMax, Chatterbox, Dia, Orpheus, F5-TTS, VibeVoice, 그리고 더 많은 것들
기본 사용법
OpenAI 텍스트 음성 변환
import { generateSpeech } from '@tanstack/ai'
import { openaiSpeech } from '@tanstack/ai-openai'
// Generate speech from text (uses OPENAI_API_KEY from environment)
const result = await generateSpeech({
adapter: openaiSpeech('tts-1'),
text: 'Hello, welcome to TanStack AI!',
voice: 'alloy',
})
// result.audio contains base64-encoded audio data
console.log(result.format) // 'mp3'
console.log(result.contentType) // 'audio/mpeg'
Gemini 텍스트 음성 변환 (실험적)
import { generateSpeech } from '@tanstack/ai'
import { geminiSpeech } from '@tanstack/ai-gemini'
// Generate speech from text (uses GOOGLE_API_KEY or GEMINI_API_KEY from environment)
const result = await generateSpeech({
adapter: geminiSpeech('gemini-3.1-flash-tts-preview'),
text: 'Hello from Gemini TTS!',
})
console.log(result.audio) // Base64 encoded audio
fal.ai 텍스트 음성 변환
fal.ai는 다양한 TTS 모델과 Google의 최신 gemini-3.1-flash-tts, ElevenLabs v3, MiniMax 2.6 HD, Kokoro의 다국어 음성 등을 제공합니다. 타입이 완전히 지정된 modelOptions를 사용하려면 모델 ID를 문자열 리터럴로 전달합니다.
import { generateSpeech } from '@tanstack/ai'
import { falSpeech } from '@tanstack/ai-fal'
// Google Gemini 3.1 Flash TTS, 80+ languages, expressive audio tags
const result = await generateSpeech({
adapter: falSpeech('fal-ai/gemini-3.1-flash-tts'),
text: '[warm, enthusiastic] Welcome to TanStack AI!',
voice: 'Kore',
})
import { generateSpeech } from '@tanstack/ai'
import { falSpeech } from '@tanstack/ai-fal'
// Kokoro multilingual
const result = await generateSpeech({
adapter: falSpeech('fal-ai/kokoro/american-english'),
text: 'Hello from fal!',
voice: 'af_heart',
speed: 1.0,
})
console.log(result.audio) // Base64 encoded audio
console.log(result.format) // e.g. "wav"
import { generateSpeech } from '@tanstack/ai'
import { falSpeech } from '@tanstack/ai-fal'
// ElevenLabs v3 with model-specific options
const result = await generateSpeech({
adapter: falSpeech('fal-ai/elevenlabs/tts/eleven-v3'),
text: 'Welcome to TanStack AI.',
// The fal adapter maps top-level `voice`/`speed` into the model input;
// `modelOptions` is reserved for model-specific keys.
voice: 'Rachel',
modelOptions: {
stability: 0.5,
},
})
BytePlus Seed Speech
Seed Speech는 ModelArk와 별개의 BytePlus 제품이므로 채팅, 이미지 및 비디오 어댑터가 읽는 ARK_API_KEY가 아니라 자체 키(BYTEPLUS_VOICE_API_KEY)를 사용합니다. 음성 ID는 Seed Speech의 speaker로 전송됩니다.
import { generateSpeech } from '@tanstack/ai'
import { byteplusSpeech } from '@tanstack/ai-byteplus'
const result = await generateSpeech({
adapter: byteplusSpeech('seed-audio-1.0'),
text: 'Welcome to TanStack AI!',
voice: 'en_female_stokie_uranus_bigtts',
format: 'mp3',
})
console.log(result.contentType) // "audio/mpeg"
형식은 wav, mp3, pcm, ogg_opus이며 합성 출력은 최대 120초로 제한됩니다. 문장 및 단어 수준 타이밍을 사용하려면 modelOptions.enable_subtitle을 설정합니다. 타이밍은 밀리초 단위이고 duration은 초 단위라는 점에 유의합니다.
Seed Speech에는 최상위 speaker 필드가 없습니다. 어댑터는 voice를 references: [{ speaker }]로 전송합니다. modelOptions.references는 해당 배열에 병합되지 않고 대체하므로 음성 복제를 위해 references를 전달하면 voice가 조용히 제거됩니다. 기본 음성을 계속 사용하려면 speaker 멤버를 직접 포함합니다. 음성 ID 명명 규칙은 BytePlus 어댑터를 참조합니다.
옵션
공통 옵션
모든 TTS 어댑터는 다음 공통 옵션을 지원합니다.
| 옵션 | 타입 | 설명 |
|---|---|---|
text | string | 음성으로 변환할 텍스트(필수) |
voice | string | 생성에 사용할 음성 |
format | string | 출력 오디오 형식(예: "mp3", "wav") |
OpenAI 음성 옵션
OpenAI는 여러 음성을 제공합니다.
| Voice | 설명 |
|---|---|
alloy | 중립적이고 균형 잡힌 음성 |
echo | 따뜻하고 대화하듯 자연스러운 음성 |
fable | 표현력이 풍부하고 이야기를 들려주는 음성 |
onyx | 깊이 있고 권위적인 음성 |
nova | 친근하고 활기찬 음성 |
shimmer | 명확하고 부드러운 음성 |
ash | 차분하고 절제된 음성 |
ballad | 선율적이고 유려한 음성 |
coral | 밝고 활기찬 음성 |
sage | 현명하고 사려 깊은 음성 |
verse | 시적이고 리듬감 있는 음성 |
OpenAI 형식 옵션
| Format | 설명 |
|---|---|
mp3 | MP3 오디오 (기본값) |
opus | Opus 오디오 (스트리밍에 적합) |
aac | AAC 오디오 |
flac | FLAC 오디오 (무손실) |
wav | WAV 오디오 (비압축) |
pcm | 원시 PCM 오디오 |
브라우저에서 오디오 재생
// Convert base64 to audio and play
function playAudio(result: TTSResult) {
const audioData = atob(result.audio)
const bytes = new Uint8Array(audioData.length)
for (let i = 0; i < audioData.length; i++) {
bytes[i] = audioData.charCodeAt(i)
}
const blob = new Blob([bytes], { type: result.contentType })
const url = URL.createObjectURL(blob)
const audio = new Audio(url)
audio.play()
// Clean up when done
audio.onended = () => URL.revokeObjectURL(url)
}
파일에 오디오 저장 (Node.js)
import { generateSpeech } from '@tanstack/ai'
import { openaiSpeech } from '@tanstack/ai-openai'
import { writeFile } from 'fs/promises'
async function saveAudio(result: TTSResult, filename: string) {
const audioBuffer = Buffer.from(result.audio, 'base64')
await writeFile(filename, audioBuffer)
console.log(`Saved to ${filename}`)
}
// Usage
const result = await generateSpeech({
adapter: openaiSpeech('tts-1'),
text: 'Hello world!',
})
await saveAudio(result, 'output.mp3')
풀스택 사용
TanStack AI는 최소한의 보일러플레이트로 풀스택 텍스트 음성 변환을 구축할 수 있도록 React 훅과 서버 측 스트리밍 헬퍼를 제공합니다.
스트리밍 모드 (서버 라우트 + 클라이언트 훅)
서버: generateSpeech를 스트리밍 응답으로 래핑하는 API 라우트를 생성합니다.
// routes/api/generate/speech.ts
import { generateSpeech, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiSpeech } from '@tanstack/ai-openai'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/api/generate/speech')({
server: {
handlers: {
POST: async ({ request }) => {
const body = await request.json()
const { text, voice, format, model } = body.data
const stream = generateSpeech({
adapter: openaiSpeech(model ?? 'tts-1'),
text,
voice,
format,
stream: true,
})
return toServerSentEventsResponse(stream)
},
},
},
})
클라이언트: 연결 어댑터와 함께 useGenerateSpeech 훅을 사용합니다.
import { useGenerateSpeech, fetchServerSentEvents } from '@tanstack/ai-react'
function SpeechGenerator() {
const { generate, result, isLoading, error } = useGenerateSpeech({
connection: fetchServerSentEvents('/api/generate/speech'),
})
const playAudio = () => {
if (!result) return
const audioData = atob(result.audio)
const bytes = new Uint8Array(audioData.length)
for (let i = 0; i < audioData.length; i++) {
bytes[i] = audioData.charCodeAt(i)
}
const blob = new Blob([bytes], { type: result.contentType })
const url = URL.createObjectURL(blob)
const audio = new Audio(url)
audio.play()
audio.onended = () => URL.revokeObjectURL(url)
}
return (
<div>
<button
onClick={() => generate({ text: 'Hello, welcome to TanStack AI!' })}
disabled={isLoading}
>
{isLoading ? 'Generating...' : 'Generate Speech'}
</button>
{error && <p>Error: {error.message}</p>}
{result && <button onClick={playAudio}>Play Audio</button>}
</div>
)
}
나머지 두 전송 방식(JSON을 반환하는 서버 함수 또는 SSE Response를 반환하는 함수)도 여기서 동일하게 작동합니다. 자세한 내용은
고급: 기타 전송 방식에 설명되어 있으며, Generations에서 한 번 더 설명합니다.
훅 API
useGenerateSpeech 훅은 다음을 받습니다.
| 옵션 | 타입 | 설명 |
|---|---|---|
connection | ConnectionAdapter | 스트리밍 전송 방식 (SSE, HTTP 스트림, 사용자 지정) |
fetcher | (input) => Promise<TTSResult | Response> | 직접 비동기 함수 또는 SSE Response를 반환하는 서버 함수 |
onResult | (result) => TOutput | null | void | 오디오가 생성될 때 호출되는 콜백입니다. 변환된 값을 선택적으로 반환할 수 있습니다(결과 변환 참조). |
onError | (error) => void | 오류 발생 시 호출되는 콜백 |
onProgress | (progress, message?) => void | 진행률 업데이트 (0~100) |
반환값은 다음과 같습니다.
| 속성 | 타입 | 설명 |
|---|---|---|
generate | (input: SpeechGenerateInput) => Promise<void> | 생성 시작 |
result | TOutput | null | 결과(또는 변환된 결과)이며, 없으면 null |
isLoading | boolean | 생성 진행 중 여부 |
error | Error | undefined | 현재 오류(있는 경우) |
status | GenerationClientState | 'idle' | 'generating' | 'success' | 'error' |
stop | () => void | 현재 생성 중단 |
reset | () => void | 결과와 오류를 지우고 idle 상태로 돌아감 |
결과 변환
onResult 콜백은 저장된 result를 대체하는 변환된 값을 선택적으로 반환할 수 있습니다. 원시 API 응답을 컴포넌트에서 더 편리한 형식으로 변환할 때 유용합니다.
변환 동작:
- 저장된 result를 변환된 값으로 대체하려면 null이 아닌 값을 반환합니다.
- 이전 result를 변경하지 않으려면 **
null**을 반환합니다(필터링에 유용함). - 원시 result를 그대로 저장하려면 아무것도 반환하지 않습니다(
void, 이전 버전과 호환됨).
예시: base64 오디오를 재생 가능한 Audio 요소로 변환
import { useGenerateSpeech, fetchServerSentEvents } from '@tanstack/ai-react'
import type { TTSResult } from '@tanstack/ai'
function SpeechPlayer() {
const { generate, result, isLoading } = useGenerateSpeech({
connection: fetchServerSentEvents('/api/generate/speech'),
onResult: (raw: TTSResult) => {
const audioData = atob(raw.audio)
const bytes = new Uint8Array(audioData.length)
for (let i = 0; i < audioData.length; i++) {
bytes[i] = audioData.charCodeAt(i)
}
const blob = new Blob([bytes], { type: raw.contentType })
const url = URL.createObjectURL(blob)
return {
audio: new Audio(url),
duration: raw.duration,
}
},
})
return (
<div>
<button
onClick={() => generate({ text: 'Hello world!', voice: 'alloy' })}
disabled={isLoading}
>
Generate
</button>
{result && (
<button onClick={() => result.audio.play()}>
Play Audio
</button>
)}
</div>
)
}
TypeScript는 onResult 반환값에서 result 타입을 자동으로 추론하므로 명시적인 제네릭 매개변수가 필요하지 않습니다. 이 예제에서 result는 { audio: HTMLAudioElement; duration?: number } | null로 추론되므로 result.audio.play()는 완전히 타입 안전합니다.
고급
이 기능을 사용하기 위해 반드시 알아야 하는 내용은 아닙니다.
기타 전송 방식
직접 모드 (서버 함수 + Fetcher)
TanStack Start 서버 함수를 스트리밍 없이 사용하는 경우:
// lib/server-functions.ts
import { createServerFn } from '@tanstack/react-start'
import { generateSpeech } from '@tanstack/ai'
import { openaiSpeech } from '@tanstack/ai-openai'
export const generateSpeechFn = createServerFn({ method: 'POST' })
.inputValidator((data: { text: string; voice?: string }) => data)
.handler(async ({ data }) => {
return generateSpeech({
adapter: openaiSpeech('tts-1'),
text: data.text,
voice: data.voice,
})
})
import { useGenerateSpeech } from '@tanstack/ai-react'
import { generateSpeechFn } from '../lib/server-functions'
function SpeechGenerator() {
const { generate, result, isLoading } = useGenerateSpeech({
fetcher: (input) => generateSpeechFn({ data: input }),
})
// ... same UI as above
}
서버 함수 스트리밍 (Fetcher + Response)
결과를 스트리밍하는 TanStack Start 서버 함수의 경우입니다. fetcher는 타입이 안전한 입력을 받고 SSE Response를 반환하며, 클라이언트가 이를 자동으로 파싱합니다.
// lib/server-functions.ts
import { createServerFn } from '@tanstack/react-start'
import { generateSpeech, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiSpeech } from '@tanstack/ai-openai'
export const generateSpeechStreamFn = createServerFn({ method: 'POST' })
.inputValidator((data: { text: string; voice?: string }) => data)
.handler(({ data }) => {
return toServerSentEventsResponse(
generateSpeech({
adapter: openaiSpeech('tts-1'),
text: data.text,
voice: data.voice,
stream: true,
}),
)
})
import { useGenerateSpeech } from '@tanstack/ai-react'
import { generateSpeechStreamFn } from '../lib/server-functions'
function SpeechGenerator() {
const { generate, result, isLoading } = useGenerateSpeech({
fetcher: (input) => generateSpeechStreamFn({ data: input }),
})
// ... same UI as above
}
모델 옵션
OpenAI 모델 옵션
import { generateSpeech } from '@tanstack/ai'
import { openaiSpeech } from '@tanstack/ai-openai'
const result = await generateSpeech({
adapter: openaiSpeech('tts-1-hd'),
text: 'High quality speech synthesis',
voice: 'nova',
format: 'mp3',
speed: 1.0, // top-level option, 0.25 to 4.0
modelOptions: {
instructions: 'Speak in a calm, measured tone', // GPT-4o audio models only
},
})
참고:
voice,format,speed는 최상위generateSpeech옵션이며modelOptions키가 아닙니다.
| 옵션 | 타입 | 설명 |
|---|---|---|
instructions | string | 음성 스타일 지침 (GPT-4o 오디오 모델만 해당) |
참고:
instructions및stream_format옵션은tts-1또는tts-1-hd가 아닌gpt-4o-audio-preview모델에서만 사용할 수 있습니다.
응답 형식
TTS 결과에는 다음이 포함됩니다.
interface TTSResult {
id: string // Unique identifier for this generation
model: string // The model used
audio: string // Base64-encoded audio data
format: string // Audio format (e.g., "mp3")
contentType: string // MIME type (e.g., "audio/mpeg")
duration?: number // Duration in seconds (if available)
}
모델 사용 가능 여부
OpenAI 모델
| 모델 | 품질 | 속도 | 사용 사례 |
|---|---|---|---|
tts-1 | 표준 | 빠름 | 실시간 애플리케이션 |
tts-1-hd | 높음 | 느림 | 프로덕션 오디오 |
gpt-4o-audio-preview | 최고 | 가변적 | 고급 음성 제어 |
Gemini 모델
| 모델 | Status | Notes |
|---|---|---|
gemini-2.5-flash-preview-tts | 실험적 | 전체 기능에 Live API가 필요할 수 있음 |
오류 처리
import { generateSpeech } from '@tanstack/ai'
import { openaiSpeech } from '@tanstack/ai-openai'
try {
const result = await generateSpeech({
adapter: openaiSpeech('tts-1'),
text: 'Hello!',
})
} catch (error) {
if (error instanceof Error) {
if (error.message.includes('exceeds maximum length')) {
console.error('Text is too long (max 4096 characters)')
} else if (error.message.includes('Speed must be between')) {
console.error('Invalid speed value')
} else {
console.error('TTS error:', error.message)
}
}
}
팁: 로딩 상태와 함께 프론트엔드에서 음성 생성을 시작하려면 Generation Hooks를 참조합니다.
디버깅: TTS 요청이 실패하거나 예상치 못한 출력을 생성하면
generateSpeech({...})에debug: true를 전달해 전송 요청, 모든 원시 프로바이더 청크 및 캡처된 오류를 로그로 기록합니다. 디버그 로깅을 참조합니다.
환경 변수
TTS 어댑터는 다른 어댑터와 동일한 환경 변수를 사용합니다.
- OpenAI:
OPENAI_API_KEY - Gemini:
GOOGLE_API_KEY또는GEMINI_API_KEY - BytePlus:
BYTEPLUS_VOICE_API_KEY(Seed Speech는 ModelArk와 다른 제품이므로 채팅/이미지/비디오 어댑터에서 사용하는ARK_API_KEY는 여기서 허용되지 않음)
명시적 API 키
프로덕션에서 사용하거나 명시적으로 제어해야 하는 경우:
import { createOpenaiSpeech } from '@tanstack/ai-openai'
import { createGeminiSpeech } from '@tanstack/ai-gemini'
// OpenAI
const openaiAdapter = createOpenaiSpeech('tts-1', 'your-openai-api-key')
// Gemini
const geminiAdapter = createGeminiSpeech('gemini-3.1-flash-tts-preview', 'your-google-api-key')
모범 사례
-
텍스트 길이: OpenAI TTS는 요청당 최대 4096자를 지원합니다. 더 긴 콘텐츠는 청크로 나눕니다.
-
음성 선택: 콘텐츠에 적합한 음성을 선택합니다. 권위적인 콘텐츠에는
onyx, 친근한 상호작용에는nova를 사용합니다. -
형식 선택: 일반적인 용도에는
mp3, 스트리밍에는opus, 추가 처리에는wav를 사용합니다. -
캐싱: 동일한 콘텐츠를 다시 생성하지 않도록 생성된 오디오를 캐시합니다.
-
오류 처리: 특히 사용자 대상 애플리케이션에서는 항상 오류를 적절히 처리합니다.