- 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
Audio Recording
useAudioRecorder captures the microphone and returns a clip you can send to chat() or to transcription.
Record and transcribe#
You need the /api/transcribe route from the Transcription quick setup. This takes about 5 minutes.
- Install the packages:
npm i @tanstack/ai @tanstack/ai-react @tanstack/ai-openaipnpm add @tanstack/ai @tanstack/ai-react @tanstack/ai-openaiyarn add @tanstack/ai @tanstack/ai-react @tanstack/ai-openaibun add @tanstack/ai @tanstack/ai-react @tanstack/ai-openaiAdd the transcription route. Copy it from Transcription.
Add a record button that sends the clip to the route:
import {
fetchServerSentEvents,
useAudioRecorder,
useTranscription,
} from '@tanstack/ai-react'
export function VoiceNote() {
const { isRecording, isSupported, start, stop } = useAudioRecorder()
const { generate, result, isLoading } = useTranscription({
connection: fetchServerSentEvents('/api/transcribe'),
threadId: 'transcription',
})
async function toggle() {
try {
if (!isRecording) {
await start()
return
}
const rec = await stop()
// Remove ";codecs=..." so the provider gets a clean audio type.
const mimeType = rec.mimeType.split(';')[0]
await generate({ audio: `data:${mimeType};base64,${rec.base64}` })
} catch (error) {
console.error(error)
}
}
if (!isSupported) return <p>This browser cannot record audio.</p>
return (
<div>
<button onClick={() => void toggle()}>
{isRecording ? 'Stop' : 'Record'}
</button>
{isLoading && <p>Transcribing...</p>}
{result && <p>{result.text}</p>}
</div>
)
}Click Record and allow the microphone. Say something, then click Stop. Your words show up as text.
How it works#
- start() asks for the microphone with getUserMedia and starts a MediaRecorder.
- stop() ends the recording and resolves to an AudioRecording.
- The recording keeps the native format of the browser recorder, audio/webm or audio/mp4. Nothing is transcoded.
The sample sends the clip as a data URL. The data URL carries the real audio type to the provider. With plain base64, the OpenAI and Groq transcription adapters treat the bytes as audio/mpeg. That is the wrong type for webm or mp4 bytes.
An AudioRecording has these fields:
| Field | Type | Description |
|---|---|---|
| part | AudioPart | A content part that is ready for chat: { type: 'audio', source: { type: 'data', value, mimeType } } |
| base64 | string | The recorded bytes as base64 |
| blob | Blob | The recorded bytes as a Blob |
| mimeType | string | The native recorder type, for example audio/webm;codecs=opus |
| durationMs | number | The length of the recording, in milliseconds |
Send a recording in chat#
rec.part is already a chat content part. Put it in sendMessage:
import {
fetchServerSentEvents,
useAudioRecorder,
useChat,
} from '@tanstack/ai-react'
export function VoiceComposer() {
const { isRecording, start, stop } = useAudioRecorder()
const { sendMessage } = useChat({
connection: fetchServerSentEvents('/api/chat'),
threadId: 'support-chat',
})
async function toggle() {
try {
if (!isRecording) {
await start()
return
}
const rec = await stop()
await sendMessage({ content: [rec.part] })
} catch (error) {
console.error(error)
}
}
return (
<button onClick={() => void toggle()}>
{isRecording ? 'Send' : 'Record'}
</button>
)
}The chat model must accept audio input.
Handle errors#
Errors come through two channels. Handle one of them, not both:
- onError(error) runs when the user denies the microphone, or when the recorder fails.
- start() and stop() also reject.
When they reject:
- start() rejects when the user denies the microphone.
- stop() rejects when the recorder fails.
- stop() rejects with Recording cancelled if the recording is cancelled while a stop is in progress. This happens, for example, when the component unmounts during a recording.
If you handle errors through the promises, await start() and await stop() in try/catch. Do not discard the promises with void.
Show the latest recording#
The hook also has a reactive recording value. It holds the same value that stop() returns. You can show a preview from it without storing the return value of stop(). It is null until the first stop().
import { useAudioRecorder } from '@tanstack/ai-react'
export function Preview() {
const { recording, isRecording, start, stop } = useAudioRecorder({
onError: (error) => console.error(error),
})
return (
<div>
<button onClick={() => void (isRecording ? stop() : start())}>
{isRecording ? 'Stop' : 'Record'}
</button>
{recording && (
<audio
src={`data:${recording.mimeType};base64,${recording.base64}`}
controls
/>
)}
</div>
)
}Change the recording before you use it#
Pass onComplete to turn the raw recording into what your app needs: a URL after an upload, an encoded blob, or your own object. stop() and recording then hold your value. The function can be async.
import { useAudioRecorder } from '@tanstack/ai-react'
export function Uploader() {
const { recording, stop } = useAudioRecorder({
onComplete: async (rec) => {
const res = await fetch('/api/upload', { method: 'POST', body: rec.blob })
const data: unknown = await res.json()
if (typeof data === 'object' && data !== null && 'url' in data) {
return String(data.url)
}
return null
},
})
// recording and stop() now hold string | null
}What the return value does:
- Return undefined (or nothing): the hook keeps the raw AudioRecording.
- Return any other value, null included: the hook uses that value, and the types of stop() and recording change to match.
The generation hooks have a similar onResult transform. Two things are different there: onResult is not async, and a null from onResult keeps the previous value.
Other frameworks#
Every framework has the same recorder:
| Framework | Import | Function | Reactive values |
|---|---|---|---|
| React | @tanstack/ai-react | useAudioRecorder | isRecording, recording (values) |
| Octane | @tanstack/ai-octane | useAudioRecorder | Same as React |
| Solid | @tanstack/ai-solid | useAudioRecorder | isRecording(), recording() (accessors) |
| Vue | @tanstack/ai-vue | useAudioRecorder | isRecording.value, recording.value (read-only refs) |
| Svelte | @tanstack/ai-svelte | createAudioRecorder | recorder.isRecording, recorder.recording (getters) |
| Angular | @tanstack/ai-angular | injectAudioRecorder | isRecording(), recording() (signals). Call it in an injection context. |
| Remix | @tanstack/ai-remix | createAudioRecorder(handle, options) | recorder.isRecording, recorder.recording |
Svelte 5 runes cannot register automatic cleanup. If a recording can still run when the component goes away, call recorder.cancel() in your cleanup:
<script lang="ts">
import {
createAudioRecorder,
createChat,
fetchServerSentEvents,
} from '@tanstack/ai-svelte'
const recorder = createAudioRecorder()
const chat = createChat({ connection: fetchServerSentEvents('/api/chat'), threadId: 'support-chat' })
async function toggle() {
if (!recorder.isRecording) {
await recorder.start()
return
}
const rec = await recorder.stop()
await chat.sendMessage({ content: [rec.part] })
}
</script>
<button onclick={toggle}>{recorder.isRecording ? 'Send' : 'Record'}</button>Advanced#
Options#
useAudioRecorder(options?), createAudioRecorder, and injectAudioRecorder take these options:
| Option | Type | Description |
|---|---|---|
| onComplete | (recording: AudioRecording) => T | Promise<T> | Changes the recording. Its awaited return value sets the type of stop() and recording. Return nothing to keep the raw recording. |
| onError | (error: Error) => void | Runs when the user denies the microphone, or when the recorder fails |
| audio | MediaTrackConstraints | boolean | Goes to getUserMedia({ audio }). The default is true. |
| mimeType | string | The recorder type you prefer. If the browser does not support it, the browser default is used. |
The browser can ignore a mimeType it does not support. If a later step needs one exact format, read recording.mimeType.
Return value#
| Property | Type | Description |
|---|---|---|
| recording | T | null | The latest recording, after onComplete. Reactive. |
| isRecording | boolean | true while the recorder runs |
| isSupported | boolean | true if the browser can record |
| start | () => Promise<void> | Gets the microphone and starts recording |
| stop | () => Promise<T> | Stops, then resolves with the recording (after onComplete) |
| cancel | () => void | Discards the recording in progress and releases the microphone |
T is AudioRecording, unless onComplete returns another type. The shape of the reactive values changes by framework. See Other frameworks.