- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Transcription: Advanced
This page holds the details for Transcription. Start there for the quick setup.
Provider examples#
Groq#
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:
- responseFormat takes json, text, or verbose_json (the default).
- srt and vtt throw.
- modelOptions takes temperature and timestamp_granularities (['word'], ['segment'], or both).
BytePlus#
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#
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}`)
}Other providers#
ElevenLabs, xAI, Cloudflare, and Lovable use the same generateTranscription() call. Their adapter pages show the setup:
Other transports#
The quick setup uses a streaming route. With TanStack Start, you can call a server function instead. The Generations page explains all three transports.
Server function that returns JSON#
// 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.
}Server function that streams#
// 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.
}Upload a file with FormData#
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.
OpenAI model options#
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 |
Response formats#
| 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.
Speaker diarization#
Use gpt-4o-transcribe-diarize when you need to know who spoke. If you set no response format, the adapter does two things:
- It sets response_format: 'diarized_json'.
- It sends chunking_strategy: 'auto', unless you set your own strategy.
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:
- You can send up to four known speaker references. Send known_speaker_names and known_speaker_references together, with the same length.
- The diarization model does not support prompt, include, or timestamp_granularities. The adapter rejects these options.
Result type#
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
}>
}OpenAI models and audio formats#
| 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.
Errors#
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.
API keys#
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')Tips for better results#
- Audio quality. Less background noise gives more accurate text.
- Language. If you know the language, set language. The result is more accurate and faster.
- File size. Keep files under 25 MB for OpenAI. Split longer recordings into parts.
- Format. MP3 is a good balance of quality and size. WAV and FLAC give the highest quality.
- Vocabulary. Put names and technical terms in prompt.
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.