This page holds the details for Transcription. Start there for the quick setup.
Groq runs Whisper large-v3 and large-v3-turbo. The audio input accepts the usual types and also an https:// URL. Groq reads the URL directly, so the audio is not uploaded again.
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',
threadId: 'thread-1',
})
console.log(result.text)
console.log(result.language)
// verbose_json is the default, so segments carry start and end times
for (const segment of result.segments ?? []) {
console.log(`[${segment.start}s - ${segment.end}s] ${segment.text}`)
}Groq details:
BytePlus Seed Speech ASR is synchronous: the audio goes in, the text comes out, with no polling. It reads BYTEPLUS_VOICE_API_KEY, not the ARK_API_KEY that the BytePlus chat, image, and video adapters use.
The audio can be a File, base64, a data URL, or a public URL. The limit is two hours and 100 MB.
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 },
threadId: 'thread-1',
})
console.log(result.text)
// With enable_speaker_info, each segment has a speaker label
for (const segment of result.segments ?? []) {
console.log(`[${segment.speaker ?? 'unknown'}] ${segment.text}`)
}BytePlus modelOptions:
| Option | What it does |
|---|---|
| enable_itn | Writes spoken numbers and dates as digits |
| enable_punc | Adds punctuation |
| enable_ddc | Removes fillers and stutters |
| enable_speaker_info | Adds speaker labels |
| show_utterances | Adds a breakdown per utterance. On by default. |
fal.ai has Whisper, Wizper, and other speech-to-text models. The audio input accepts a URL, a File, a Blob, or an ArrayBuffer. The adapter wraps an ArrayBuffer in a Blob for you.
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',
threadId: 'thread-1',
})
console.log(result.text)
console.log(result.language)
// Models that return word or chunk timestamps fill result.segments
for (const segment of result.segments ?? []) {
console.log(`[${segment.start}s - ${segment.end}s] ${segment.text}`)
}ElevenLabs, xAI, Cloudflare, and Lovable use the same generateTranscription() call. Their adapter pages show the setup:
The quick setup uses a streaming route. With TanStack Start, you can call a server function instead. The Generations page explains all three transports.
// 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; threadId: string }) => data,
)
.handler(async ({ data }) => {
return generateTranscription({
adapter: openaiTranscription('gpt-4o-transcribe'),
audio: data.audio,
language: data.language,
threadId: data.threadId,
})
})import { useTranscription } from '@tanstack/ai-react'
import { transcribeFn } from '../lib/server-functions'
const threadId = 'transcription'
export function Transcriber() {
const { generate, result, isLoading } = useTranscription({
fetcher: (input, { threadId }) =>
transcribeFn({ data: { ...input, threadId } }),
threadId,
})
// Render the same way as in the quick setup.
}// 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; threadId: string }) => data,
)
.handler(({ data }) => {
return toServerSentEventsResponse(
generateTranscription({
adapter: openaiTranscription('gpt-4o-transcribe'),
audio: data.audio,
language: data.language,
stream: true,
threadId: data.threadId,
}),
)
})import { useTranscription } from '@tanstack/ai-react'
import { transcribeStreamFn } from '../lib/server-functions'
const threadId = 'transcription'
export function Transcriber() {
const { generate, result, isLoading } = useTranscription({
fetcher: (input, { threadId }) => {
if (typeof input.audio !== 'string') {
throw new Error('Expected base64 or data URL audio')
}
return transcribeStreamFn({
data: { ...input, audio: input.audio, threadId },
})
},
threadId,
})
// Render the same way as in the quick setup.
}For a large file, you can skip the JSON body and post the file itself. This route does not use the hook. It returns the result as JSON.
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')
const threadId = formData.get('threadId')
if (!(audioFile instanceof File) || typeof threadId !== 'string') {
return new Response('Send an audio file under "audio" and a threadId.', {
status: 400,
})
}
const result = await generateTranscription({
adapter: openaiTranscription('gpt-4o-transcribe'),
audio: audioFile,
threadId,
})
return Response.json(result)
}In the browser, put the file in a FormData under audio. Add the threadId under threadId. Then POST the form to this route.
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',
prompt: 'Technical terms: API, SDK, CLI',
threadId: 'thread-1',
modelOptions: {
temperature: 0,
timestamp_granularities: ['word', 'segment'],
},
})responseFormat and prompt are top-level options on generateTranscription(). They are not modelOptions keys.
| Option | Type | Description |
|---|---|---|
| temperature | number | Sampling temperature, from 0 to 1. A lower value gives more predictable text. |
| timestamp_granularities | Array<'word' | 'segment'> | Which timestamps to fill. Only whisper-1. Needs a top-level responseFormat: 'verbose_json'. |
| include | string[] | Extra values in the response, for example logprobs |
| response_format | 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt' | 'diarized_json' | The raw OpenAI response format. Use diarized_json for speaker labels. |
| chunking_strategy | 'auto' | { type: 'server_vad', ... } | null | How OpenAI splits the audio. Works with any model. If you do not set it, the audio is transcribed as one block. OpenAI requires it for gpt-4o-transcribe-diarize input longer than 30 seconds, so the adapter sets 'auto' for that model. |
| known_speaker_names | string[] | Up to four speaker labels for diarization |
| known_speaker_references | string[] | Data URL samples of 2 to 10 seconds that match known_speaker_names |
| Format | Description |
|---|---|
| json | JSON with the text |
| text | Plain text only |
| srt | SubRip subtitles |
| verbose_json | JSON with timestamps and segments |
| vtt | WebVTT subtitles |
gpt-4o-transcribe-diarize also takes modelOptions.response_format: 'diarized_json' for segments with speaker labels.
Use gpt-4o-transcribe-diarize when you need to know who spoke. If you set no response format, the adapter does two things:
A top-level responseFormat: 'json' or 'text' turns the speaker segments off.
import { generateTranscription } from '@tanstack/ai'
import { openaiTranscription } from '@tanstack/ai-openai'
import { agentSampleUrl, customerSampleUrl, meetingAudioFile } from './audio'
const result = await generateTranscription({
adapter: openaiTranscription('gpt-4o-transcribe-diarize'),
audio: meetingAudioFile,
modelOptions: {
known_speaker_names: ['agent', 'customer'],
known_speaker_references: [agentSampleUrl, customerSampleUrl],
},
threadId: 'thread-1',
})
for (const segment of result.segments ?? []) {
console.log(segment.speaker, segment.start, segment.end, segment.text)
}The adapter checks two rules before it calls the API:
interface TranscriptionResult {
id: string // unique id
model: string // the model used
text: string // the full text
language?: string // detected or given language
duration?: number // audio length in seconds
segments?: Array<{
id: number
start: number // start time in seconds
end: number // end time in seconds
text: string
confidence?: number // 0 to 1, when available
speaker?: string // when diarization is on
}>
words?: Array<{
word: string
start: number
end: number
}>
}| Model | Description | Use it for |
|---|---|---|
| gpt-4o-transcribe | GPT-4o transcription | Higher accuracy |
| gpt-4o-mini-transcribe | A smaller, faster model | Lower cost |
| gpt-4o-transcribe-diarize | Adds speaker labels | Audio with more than one speaker |
| whisper-1 | Whisper large-v2 | General transcription, word timestamps |
OpenAI accepts these audio formats: mp3, mp4, mpeg, mpga, m4a, wav, webm, flac, and ogg. The maximum file size is 25 MB.
import { generateTranscription } from '@tanstack/ai'
import { openaiTranscription } from '@tanstack/ai-openai'
import { audioFile } from './audio'
try {
const result = await generateTranscription({
adapter: openaiTranscription('gpt-4o-transcribe'),
audio: audioFile,
threadId: 'thread-1',
})
} 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)
}
}
}If the text is wrong, the segments are empty, or the provider rejects your audio format, pass debug: true to generateTranscription(). It logs the request and every raw provider chunk. See Debug Logging.
By default, each adapter reads its key from the environment. The variables are in the Providers table.
To pass a key in code, use the create* factory:
import { createOpenaiTranscription } from '@tanstack/ai-openai'
const adapter = createOpenaiTranscription('gpt-4o-transcribe', 'your-openai-api-key')For timestamps, set responseFormat: 'verbose_json' and modelOptions.timestamp_granularities. For several speakers, use gpt-4o-transcribe-diarize and keep chunking_strategy: 'auto', unless you need your own voice activity settings.