오디오 전사
TanStack AI는 전용 전사 어댑터를 통해 오디오 전사(음성-텍스트)를 지원합니다. 이 가이드에서는 OpenAI의 Whisper 및 GPT-4o 전사 모델, Groq에서 호스팅하는 Whisper 모델, fal.ai STT 모델을 사용해 음성 오디오를 텍스트로 변환하는 방법을 설명합니다.
개요
오디오 전사는 TanStack AI의 다른 어댑터와 동일한 트리 셰이킹 가능한 아키텍처를 따르는 전사 어댑터가 처리합니다.
현재 지원되는 서비스:
- OpenAI: Whisper-1, GPT-4o-transcribe, GPT-4o-mini-transcribe, GPT-4o-transcribe-diarize
- Groq: whisper-large-v3-turbo, whisper-large-v3
- BytePlus: Seed Speech ASR (
seed-asr) - fal.ai: Whisper, Wizper, speech-to-text turbo, ElevenLabs speech-to-text
기본 사용법
OpenAI 전사
import { generateTranscription } from '@tanstack/ai'
import { openaiTranscription } from '@tanstack/ai-openai'
import { audioBuffer } from './audio'
// Transcribe audio from a file (the adapter uses OPENAI_API_KEY from environment)
const audioFile = new File([audioBuffer], 'audio.mp3', { type: 'audio/mpeg' })
const result = await generateTranscription({
adapter: openaiTranscription('whisper-1'),
audio: audioFile,
language: 'en',
})
console.log(result.text) // The transcribed text
Base64 오디오 사용
import { generateTranscription } from '@tanstack/ai'
import { openaiTranscription } from '@tanstack/ai-openai'
import { readFile } from 'fs/promises'
// Read audio file as base64
const audioBuffer = await readFile('recording.mp3')
const base64Audio = audioBuffer.toString('base64')
const result = await generateTranscription({
adapter: openaiTranscription('whisper-1'),
audio: base64Audio,
})
console.log(result.text)
데이터 URL 사용
import { generateTranscription } from '@tanstack/ai'
import { openaiTranscription } from '@tanstack/ai-openai'
import { base64AudioData } from './audio'
const dataUrl = `data:audio/mpeg;base64,${base64AudioData}`
const result = await generateTranscription({
adapter: openaiTranscription('whisper-1'),
audio: dataUrl,
})
Groq 전사
Groq는 빠른 추론 스택에서 Whisper large-v3 및 large-v3-turbo를 호스팅합니다. audio 입력에는 File, Blob, ArrayBuffer, base64 문자열, 데이터 URL 또는 https:// URL을 사용할 수 있으며, URL은 다시 업로드하지 않고 Groq로 전달됩니다.
import { generateTranscription } from '@tanstack/ai'
import { groqTranscription } from '@tanstack/ai-groq'
const result = await generateTranscription({
adapter: groqTranscription('whisper-large-v3-turbo'),
audio: 'https://example.com/recording.mp3',
language: 'en',
})
console.log(result.text)
console.log(result.language)
// verbose_json is the default — segments carry segment-level start/end timestamps
for (const segment of result.segments ?? []) {
console.log(`[${segment.start}s → ${segment.end}s] ${segment.text}`)
}
참고: Groq는
responseFormat값으로json,text,verbose_json(기본값)을 지원합니다.srt와vtt는 지원되지 않으며 전달하면 오류가 발생합니다. 공급자별modelOptions는temperature와timestamp_granularities(['word'],['segment']또는 둘 다)입니다.
BytePlus 전사
BytePlus Seed Speech ASR은 동기식으로 동작합니다. 오디오를 입력하면 전사 결과를 반환하며 폴링이 필요하지 않습니다. Seed Speech 제품에 속하므로 BYTEPLUS_VOICE_API_KEY를 읽습니다. BytePlus 채팅·이미지·비디오 어댑터에서 사용하는 ARK_API_KEY와는 다릅니다. 오디오는 File, base64, 데이터 URL 또는 공개 URL일 수 있으며, 최대 2시간 및 100MB까지 지원합니다.
import { generateTranscription } from '@tanstack/ai'
import { byteplusTranscription } from '@tanstack/ai-byteplus'
const result = await generateTranscription({
adapter: byteplusTranscription('seed-asr'),
audio: 'https://example.com/recording.mp3',
language: 'en',
modelOptions: { enable_punc: true, enable_speaker_info: true },
})
console.log(result.text)
// Speaker labels land on the segment when enable_speaker_info is set
for (const segment of result.segments ?? []) {
console.log(`[${segment.speaker ?? 'unknown'}] ${segment.text}`)
}
참고: 공급자별
modelOptions는enable_itn(말한 숫자와 날짜를 숫자로 표시),enable_punc(구두점),enable_ddc(간투사와 더듬거림 제거),enable_speaker_info(화자 레이블),show_utterances(발화별 세부 분석, 기본적으로 활성화됨)입니다.
fal.ai 전사
fal.ai는 Whisper, Wizper 및 기타 STT 모델을 제공합니다. audio 입력에는 URL, File, Blob 또는 ArrayBuffer를 사용할 수 있으며, ArrayBuffer는 자동으로 Blob으로 래핑됩니다.
import { generateTranscription } from '@tanstack/ai'
import { falTranscription } from '@tanstack/ai-fal'
const result = await generateTranscription({
adapter: falTranscription('fal-ai/whisper'),
audio: 'https://example.com/recording.mp3',
language: 'en',
})
console.log(result.text)
console.log(result.language)
// Models that return word/chunk timestamps populate result.segments
for (const segment of result.segments ?? []) {
console.log(`[${segment.start}s → ${segment.end}s] ${segment.text}`)
}
옵션
공통 옵션
| 옵션 | 타입 | 설명 |
|---|---|---|
audio | File | string | 오디오 데이터(File 객체 또는 base64 문자열) - 필수 |
language | string | 언어 코드(예: "en", "es", "fr") |
prompt | string | 전사 스타일이나 용어를 안내하는 선택적 프롬프트입니다. gpt-4o-transcribe-diarize에서는 지원되지 않습니다. |
responseFormat | 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt' | 공통 출력 형식 |
지원 언어
Whisper는 여러 언어를 지원합니다. 일반적인 코드는 다음과 같습니다.
| 코드 | 언어 |
|---|---|
en | 영어 |
es | 스페인어 |
fr | 프랑스어 |
de | 독일어 |
it | 이탈리아어 |
pt | 포르투갈어 |
ja | 일본어 |
ko | 한국어 |
zh | 중국어 |
ru | 러시아어 |
팁: 올바른 언어 코드를 제공하면 정확도가 향상되고 지연 시간이 줄어듭니다.
전체 예제
import { generateTranscription } from '@tanstack/ai'
import { openaiTranscription } from '@tanstack/ai-openai'
import { readFile } from 'fs/promises'
async function transcribeAudio(filepath: string) {
// Read the audio file
const audioBuffer = await readFile(filepath)
const audioFile = new File(
[audioBuffer],
filepath.split('/').pop()!,
{ type: 'audio/mpeg' }
)
// Transcribe with detailed output
const result = await generateTranscription({
adapter: openaiTranscription('whisper-1'),
audio: audioFile,
language: 'en',
responseFormat: 'verbose_json',
modelOptions: {
timestamp_granularities: ['segment', 'word'],
},
})
console.log('Full text:', result.text)
console.log('Duration:', result.duration, 'seconds')
// Print segments with timestamps
if (result.segments) {
for (const segment of result.segments) {
console.log(`[${segment.start.toFixed(2)}s - ${segment.end.toFixed(2)}s]: ${segment.text}`)
}
}
return result
}
// Usage
await transcribeAudio('./meeting-recording.mp3')
브라우저에서 사용
녹음 및 전사
async function recordAndTranscribe() {
// Request microphone access
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
const mediaRecorder = new MediaRecorder(stream)
const chunks: Blob[] = []
mediaRecorder.ondataavailable = (e) => chunks.push(e.data)
mediaRecorder.onstop = async () => {
const audioBlob = new Blob(chunks, { type: 'audio/webm' })
const audioFile = new File([audioBlob], 'recording.webm', { type: 'audio/webm' })
// Send to your API endpoint for transcription
const formData = new FormData()
formData.append('audio', audioFile)
const response = await fetch('/api/transcribe', {
method: 'POST',
body: formData,
})
const result = await response.json()
console.log('Transcription:', result.text)
}
// Start recording
mediaRecorder.start()
// Stop after 10 seconds
setTimeout(() => mediaRecorder.stop(), 10000)
}
서버 API 엔드포인트
// api/transcribe.ts
import { generateTranscription } from '@tanstack/ai'
import { openaiTranscription } from '@tanstack/ai-openai'
export async function POST(request: Request) {
const formData = await request.formData()
const audioFile = formData.get('audio')
if (!(audioFile instanceof File)) {
throw new Error('Expected an audio file under "audio"')
}
const result = await generateTranscription({
adapter: openaiTranscription('whisper-1'),
audio: audioFile,
})
return Response.json(result)
}
풀스택 사용
TanStack AI는 React 훅과 서버 측 스트리밍 헬퍼를 제공하므로 보일러플레이트를 최소화해 풀스택 오디오 전사 기능을 구축할 수 있습니다.
참고: 큰 파일의 전사는 오래 걸릴 수 있습니다. Generation Persistence를 사용하면 새로고침하거나 연결이 끊긴 경우에도 상태와 결과를 유지할 수 있습니다.
스트리밍 모드(서버 라우트 + 클라이언트 훅)
서버 — generateTranscription을 스트리밍 응답으로 감싸는 API 라우트를 만듭니다.
// routes/api/transcribe.ts
import {
generateTranscription,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiTranscription } from '@tanstack/ai-openai'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/api/transcribe')({
server: {
handlers: {
POST: async ({ request }) => {
const body = await request.json()
const { audio, language, model } = body.data
const stream = generateTranscription({
adapter: openaiTranscription(model ?? 'whisper-1'),
audio,
language,
stream: true,
})
return toServerSentEventsResponse(stream)
},
},
},
})
참고: 브라우저에서 녹음한 오디오는 일반적으로 JSON 본문에 base64 문자열로 전달합니다. 파일 업로드에는 대신 FormData 기반 엔드포인트를 사용합니다(위의 브라우저에서 사용 참고).
클라이언트 — 연결 어댑터와 함께 useTranscription 훅을 사용합니다.
import { useTranscription, fetchServerSentEvents } from '@tanstack/ai-react'
function AudioTranscriber() {
const { generate, result, isLoading, error } = useTranscription({
connection: fetchServerSentEvents('/api/transcribe'),
})
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
// Convert to base64 for JSON transport
const buffer = await file.arrayBuffer()
const base64 = btoa(
new Uint8Array(buffer).reduce((s, b) => s + String.fromCharCode(b), ''),
)
const dataUrl = `data:${file.type};base64,${base64}`
await generate({ audio: dataUrl, language: 'en' })
}
return (
<div>
<input type="file" accept="audio/*" onChange={handleFileUpload} />
{isLoading && <p>Transcribing...</p>}
{error && <p>Error: {error.message}</p>}
{result && (
<div>
<p>{result.text}</p>
{result.duration && <p>Duration: {result.duration}s</p>}
</div>
)}
</div>
)
}
나머지 두 전송 방식(JSON을 반환하는 서버 함수 또는 SSE Response를 반환하는 방식)도 여기서 동일하게 작동합니다. 고급: 기타 전송 방식에 설명되어 있으며, 생성에서 한 번 더 설명합니다.
훅 API
useTranscription 훅은 다음을 받습니다.
| 옵션 | 타입 | 설명 |
|---|---|---|
connection | ConnectionAdapter | 스트리밍 전송(SSE, HTTP 스트림, 사용자 지정) |
fetcher | (input) => Promise<TranscriptionResult | Response> | 직접 호출하는 비동기 함수 또는 SSE Response를 반환하는 서버 함수 |
onResult | (result) => TOutput | null | void | 전사가 완료될 때의 콜백입니다. 변환된 값을 반환해 result로 저장할 수 있습니다. |
onError | (error) => void | 오류 발생 시의 콜백 |
onProgress | (progress, message?) => void | 진행률 업데이트(0-100) |
다음 값을 반환합니다.
| 속성 | 타입 | 설명 |
|---|---|---|
generate | (input: TranscriptionGenerateInput) => Promise<void> | 전사 시작 |
result | TranscriptionResult | null | 텍스트와 세그먼트가 포함된 결과 또는 null |
isLoading | boolean | 전사가 진행 중인지 여부 |
error | Error | undefined | 현재 오류(있는 경우) |
status | GenerationClientState | 'idle' | 'generating' | 'success' | 'error' |
stop | () => void | 현재 전사를 중단합니다. |
reset | () => void | 결과와 오류를 지우고 유휴 상태로 돌아갑니다. |
고급
이 기능을 작동시키는 데 필요하지 않은 참고 세부 정보입니다.
기타 전송 방식
직접 모드(서버 함수 + fetcher)
TanStack Start 서버 함수에서 스트리밍하지 않고 사용하려면 다음과 같이 합니다.
// lib/server-functions.ts
import { createServerFn } from '@tanstack/react-start'
import { generateTranscription } from '@tanstack/ai'
import { openaiTranscription } from '@tanstack/ai-openai'
export const transcribeFn = createServerFn({ method: 'POST' })
.inputValidator((data: { audio: string; language?: string }) => data)
.handler(async ({ data }) => {
return generateTranscription({
adapter: openaiTranscription('whisper-1'),
audio: data.audio,
language: data.language,
})
})
import { useTranscription } from '@tanstack/ai-react'
import { transcribeFn } from '../lib/server-functions'
function AudioTranscriber() {
const { generate, result, isLoading } = useTranscription({
fetcher: (input) => transcribeFn({ data: input }),
})
// ... same UI as above
}
서버 함수 스트리밍(fetcher + Response)
결과를 스트리밍하는 TanStack Start 서버 함수에서는 fetcher가 타입 안전한 입력을 받고 SSE Response를 반환합니다. 클라이언트가 이를 자동으로 파싱합니다.
// lib/server-functions.ts
import { createServerFn } from '@tanstack/react-start'
import { generateTranscription, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiTranscription } from '@tanstack/ai-openai'
export const transcribeStreamFn = createServerFn({ method: 'POST' })
.inputValidator((data: { audio: string; language?: string }) => data)
.handler(({ data }) => {
return toServerSentEventsResponse(
generateTranscription({
adapter: openaiTranscription('whisper-1'),
audio: data.audio,
language: data.language,
stream: true,
}),
)
})
import { useTranscription } from '@tanstack/ai-react'
import { transcribeStreamFn } from '../lib/server-functions'
function AudioTranscriber() {
const { generate, result, isLoading } = useTranscription({
fetcher: (input) => {
if (typeof input.audio !== 'string') {
throw new Error('Expected base64 or data URL audio')
}
return transcribeStreamFn({
data: { ...input, audio: input.audio },
})
},
})
// ... same UI as above
}
모델 옵션
OpenAI 모델 옵션
import { generateTranscription } from '@tanstack/ai'
import { openaiTranscription } from '@tanstack/ai-openai'
import { audioFile } from './audio'
const result = await generateTranscription({
adapter: openaiTranscription('whisper-1'),
audio: audioFile,
responseFormat: 'verbose_json', // Top-level: detailed output with timestamps
prompt: 'Technical terms: API, SDK, CLI', // Top-level: guide transcription
modelOptions: {
temperature: 0, // Lower = more deterministic (provider option)
timestamp_granularities: ['word', 'segment'],
},
})
| 옵션 | 타입 | 설명 |
|---|---|---|
temperature | number | 샘플링 온도(0~1) |
timestamp_granularities | Array<'word' | 'segment'> | 채울 타임스탬프 세분성(whisper-1 전용이며 최상위 responseFormat: 'verbose_json' 필요) |
include | string[] | 응답에 포함할 추가 값(예: logprobs) |
response_format | 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt' | 'diarized_json' | 원시 OpenAI 응답 형식입니다. 화자 레이블이 있는 분리 출력에는 여기서 diarized_json을 사용합니다. |
chunking_strategy | 'auto' | { type: 'server_vad', ... } | null | 오디오 청크 분할 전략입니다(모든 모델에서 사용 가능하며, 설정하지 않으면 오디오를 하나의 블록으로 전사합니다). 30초보다 긴 gpt-4o-transcribe-diarize 입력에는 OpenAI가 요구하며, 어댑터는 이 모델에서 기본값을 'auto'로 설정합니다. |
known_speaker_names | string[] | 화자 분리를 위한 최대 4개의 화자 레이블 |
known_speaker_references | string[] | known_speaker_names와 일치하는 2~10초 길이의 데이터 URL 오디오 샘플 |
responseFormat과prompt는generateTranscription의 최상위 옵션이며modelOptions키가 아닙니다.
응답 형식
| 형식 | 설명 |
|---|---|
json | 텍스트가 포함된 단순 JSON |
text | 일반 텍스트만 포함 |
srt | SubRip 자막 형식 |
verbose_json | 타임스탬프와 세그먼트가 포함된 상세 JSON |
vtt | WebVTT 자막 형식 |
OpenAI의 gpt-4o-transcribe-diarize는 화자 레이블이 있는 세그먼트를 위해 modelOptions.response_format: 'diarized_json'도 지원합니다.
화자 분리
화자 레이블이 필요하면 gpt-4o-transcribe-diarize를 사용합니다. 응답 형식을 지정하지 않으면 TanStack AI는 요청의 기본값을 response_format: 'diarized_json'으로 설정하고, 직접 청크 분할 전략을 제공하지 않는 한 chunking_strategy: 'auto'를 전송합니다. 최상위 responseFormat: 'json' 또는 'text'를 전달하면 화자 세그먼트를 사용하지 않습니다.
import { generateTranscription } from '@tanstack/ai'
import { openaiTranscription } from '@tanstack/ai-openai'
import { meetingAudioFile } from './audio'
const result = await generateTranscription({
adapter: openaiTranscription('gpt-4o-transcribe-diarize'),
audio: meetingAudioFile,
modelOptions: {
known_speaker_names: ['agent', 'customer'],
known_speaker_references: [
'data:audio/wav;base64,...',
'data:audio/wav;base64,...',
],
},
})
for (const segment of result.segments ?? []) {
console.log(segment.speaker, segment.start, segment.end, segment.text)
}
어댑터는 API를 호출하기 전에 다음 두 가지 제약을 적용합니다.
- 알려진 화자 참조는 최대 4개입니다.
known_speaker_names와known_speaker_references는 길이가 일치하도록 함께 제공해야 합니다. - 화자 분리 모델은
prompt,include또는timestamp_granularities를 지원하지 않습니다. 이러한 조합은 거부됩니다.
응답 형식
전사 결과에는 다음이 포함됩니다.
interface TranscriptionResult {
id: string // Unique identifier
model: string // Model used
text: string // Full transcribed text
language?: string // Detected/specified language
duration?: number // Audio duration in seconds
segments?: Array<{ // Timestamped segments
id: number // Segment identifier
start: number // Start time in seconds
end: number // End time in seconds
text: string // Segment text
confidence?: number // Confidence score (0-1), if available
speaker?: string // Speaker identifier, if diarization is enabled
}>
words?: Array<{ // Word-level timestamps (top-level)
word: string
start: number
end: number
}>
}
모델 사용 가능 여부
OpenAI 모델
| 모델 | 설명 | 사용 사례 |
|---|---|---|
whisper-1 | Whisper large-v2 | 일반 전사 |
gpt-4o-transcribe | GPT-4o 기반 전사 | 더 높은 정확도 |
gpt-4o-transcribe-diarize | 화자 분리 지원 | 여러 화자가 있는 오디오 |
gpt-4o-mini-transcribe | 더 빠르고 가벼운 모델 | 비용 효율적 |
지원 오디오 형식
OpenAI는 다음 오디오 형식을 지원합니다.
mp3- MPEG Audio Layer 3mp4- MPEG-4 Audiompeg- MPEG Audiompga- MPEG Audiom4a- MPEG-4 Audiowav- Waveform Audiowebm- WebM Audioflac- Free Lossless Audio Codecogg- Ogg Vorbis
참고: 최대 파일 크기는 25MB입니다.
오류 처리
import { generateTranscription } from '@tanstack/ai'
import { openaiTranscription } from '@tanstack/ai-openai'
import { audioFile } from './audio'
try {
const result = await generateTranscription({
adapter: openaiTranscription('whisper-1'),
audio: audioFile,
})
} catch (error) {
if (error instanceof Error) {
if (error.message.includes('Invalid file format')) {
console.error('Unsupported audio format')
} else if (error.message.includes('File too large')) {
console.error('Audio file exceeds 25 MB limit')
} else if (error.message.includes('Audio file is too short')) {
console.error('Audio must be at least 0.1 seconds')
} else {
console.error('Transcription error:', error.message)
}
}
}
디버깅: 전사 결과가 무의미하거나 세그먼트가 비어 있거나 공급자가 오디오 형식을 거부하면
generateTranscription({...})에debug: true를 전달해 전송 요청과 각 원시 공급자 청크를 기록합니다. 디버그 로깅을 참고합니다.
환경 변수
전사 어댑터는 다음을 사용합니다.
OPENAI_API_KEY: OpenAI API 키BYTEPLUS_VOICE_API_KEY: BytePlus Seed Speech 키(ModelArk 키가 아님)
명시적 API 키
import { createOpenaiTranscription } from '@tanstack/ai-openai'
const adapter = createOpenaiTranscription('whisper-1', 'your-openai-api-key')
모범 사례
-
오디오 품질: 오디오 품질이 높을수록 전사 정확도가 향상됩니다. 가능한 경우 배경 소음을 줄입니다.
-
언어 지정: 알고 있는 경우 항상 언어를 지정합니다. 정확도와 속도가 향상됩니다.
-
파일 크기: 오디오 파일을 25MB 미만으로 유지합니다. 더 긴 녹음은 청크로 나눕니다.
-
형식 선택: MP3는 품질과 크기의 균형이 좋습니다. 최고의 품질이 필요하면 WAV 또는 FLAC을 사용합니다.
-
프롬프트 사용:
prompt옵션으로 맥락이나 예상 어휘(예: 기술 용어, 이름)를 제공합니다. -
타임스탬프: 자막이나 동기화에 시간 정보가 필요하면
responseFormat: 'verbose_json'을 요청하고modelOptions.timestamp_granularities를 설정합니다. -
화자 분리: 여러 화자가 있는 오디오에는
modelOptions.response_format: 'diarized_json'출력을 사용하도록gpt-4o-transcribe-diarize를 사용합니다. 사용자 지정 VAD 조정이 필요하지 않으면chunking_strategy: 'auto'를 유지합니다.