Persistence (Advanced)
- Build your first harness
- Host options
- Send and show media
- Connect clients
- Write a client in any language
- Access and tenants
- Run in the terminal
- Deploy a harness
- Send inputs safely
- Run a tool without a model
- Durable sessions
- Run background work on workers
- List, rename, and fork sessions
- Lifecycle and retention
- Run side effects once
- Wait for a person
- Build on the session log
- Control how a turn ends
- Store settings per thread
- Compact a long session
- Fork and reset a thread
- One log for a session tree
- Write a plugin
- Track usage and cost
- Ask before risky tool calls
- Give your agent coding tools
- Sandbox and extend the coding tools
- Run the coding tools for many threads
- Undo a turn
- Build a coding agent
- Delegate to coding agents
- Switch between agent profiles
- Auth and connectors
- Share a thread between people
- Connect model providers
- Build your own UI
- Code mode in a harness
- Work until a goal is met
- Run agents from a harness
- Hand off a conversation
- Add skills
- Use MCP servers
- Give your agent a computer
- Use from any MCP client
- Use a harness from an editor (ACP)
- Overview
- Quick Start
- ProvidersUpdated
- Harnesses
- Harness Auth
- Workspace
- Tools
- Policy
- Lifecycle & Snapshots
- Portable Snapshots
- Keep Files After Reload
- Save a Named Version
- Branch From a Version
- Send a Frozen File
- Let the Agent Save and Fork
- Pick Which Files to Keep
- What a Snapshot Stores
- Instance Durability
- Durable Runs
- Events
Sandboxes (Advanced)
- Runtime Context
- Bring Your Own Key
- Keep Keys on the Server
- Debug Logging
- Multimodal Content
- Files API
- Per-Model Type Safety
- Runtime Adapter Switching
- Tree-Shaking
- Extend Adapter
- Extend Adapter: Advanced
- Typed Pre-Configured Options
- Test with a Fake Model
- Prompt Caching
- Mid-Conversation Changes
- Approval Flow Processing
Audio Generation
generateAudio() sends a text prompt to an audio model and returns the clip. useGenerateAudio plays it in the browser.
For a spoken voice, use Text-to-Speech instead.
Generate a track#
You need a React app with a server route, and a fal.ai API key. This takes about 5 minutes.
- Install the packages:
npm i @tanstack/ai @tanstack/ai-react @tanstack/ai-falpnpm add @tanstack/ai @tanstack/ai-react @tanstack/ai-falyarn add @tanstack/ai @tanstack/ai-react @tanstack/ai-falbun add @tanstack/ai @tanstack/ai-react @tanstack/ai-fal- Put your key in .env:
FAL_KEY=your-fal-api-key- Add a POST route at /api/generate/audio:
import {
generateAudio,
generationParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { falAudio } from '@tanstack/ai-fal'
export async function POST(request: Request) {
const { input, threadId } = await generationParamsFromRequest('audio', request)
const stream = generateAudio({
adapter: falAudio('fal-ai/minimax-music/v2.6'),
prompt: input.prompt,
threadId,
duration: input.duration,
stream: true,
})
return toServerSentEventsResponse(stream)
}- Call the route from a component:
import { fetchServerSentEvents, useGenerateAudio } from '@tanstack/ai-react'
export function AudioGenerator() {
const { generate, result, isLoading, error, reset } = useGenerateAudio({
connection: fetchServerSentEvents('/api/generate/audio'),
threadId: 'audio',
})
const src = result?.audio.url
? result.audio.url
: result?.audio.b64Json
? `data:${result.audio.contentType ?? 'audio/mpeg'};base64,${result.audio.b64Json}`
: undefined
return (
<div>
<button
onClick={() =>
generate({
prompt: 'City Pop, 80s retro, groovy synth bass, warm female vocal, 104 BPM',
})
}
disabled={isLoading}
>
{isLoading ? 'Generating...' : 'Generate'}
</button>
{error && <p role="alert">{error.message}</p>}
{src && <audio src={src} controls />}
{result && <button onClick={reset}>Clear</button>}
</div>
)
}Click Generate. When the track is ready, an audio player shows up.
How it works#
- generateAudio() sends the prompt to the model that the adapter names.
- The result has one audio object. It holds a url (fal) or base64 bytes in b64Json (Gemini), plus contentType when the provider sends it.
- With stream: true, the route sends the result to the hook as Server-Sent Events. The hook stores it on result.
To call a TanStack Start server function instead of a route, pass fetcher to the hook. See Generations: Transports in full. The hook options and return values are in Generation Hooks.
Note
A long track can outlive a page reload, and a fal URL expires. To keep the status, the result, and the audio file, add Generation Persistence.
Options#
| Option | Type | Description |
|---|---|---|
| adapter | AudioAdapter | The audio adapter and model (required) |
| prompt | string | What the audio sounds like (required) |
| duration | number | Length in seconds. Not every model uses it. |
| modelOptions | object | Model-specific options. Typed when you pass the model id as a string literal. |
| debug | DebugOption | Log every chunk from the provider SDK: true, false, or a DebugConfig. See Debug Logging. |
Pick a provider#
| Provider | Adapter | Package | Models | Key |
|---|---|---|---|---|
| Google Gemini | geminiAudio | @tanstack/ai-gemini | lyria-3-pro-preview, lyria-3-clip-preview | GOOGLE_API_KEY |
| fal.ai | falAudio | @tanstack/ai-fal | MiniMax Music, DiffRhythm, Google Lyria 2, Stable Audio 2.5, MMAudio, ElevenLabs sound effects, Thinksound, and more | FAL_KEY |
Google Lyria (music)#
Lyria makes full songs with vocals and instruments:
- lyria-3-pro-preview makes compositions with several verses.
- lyria-3-clip-preview makes 30-second clips.
import { generateAudio } from '@tanstack/ai'
import { geminiAudio } from '@tanstack/ai-gemini'
const result = await generateAudio({
adapter: geminiAudio('lyria-3-pro-preview'),
prompt: 'Uplifting indie pop with layered vocals and jangly guitars',
threadId: 'thread-1',
})
console.log(result.audio.b64Json) // base64 audio bytes
console.log(result.audio.contentType) // for example "audio/mpeg"fal.ai music (MiniMax Music 2.6)#
MiniMax Music 2.6 makes a full song (vocals, backing music, and arrangement) from one prompt:
import { generateAudio } from '@tanstack/ai'
import { falAudio } from '@tanstack/ai-fal'
const result = await generateAudio({
adapter: falAudio('fal-ai/minimax-music/v2.6'),
prompt: 'City Pop, 80s retro, groovy synth bass, warm female vocal, 104 BPM',
threadId: 'thread-1',
})
console.log(result.audio.url) // URL of the audio file
console.log(result.audio.contentType) // for example "audio/wav"fal.ai music with your own lyrics (DiffRhythm)#
import { generateAudio } from '@tanstack/ai'
import { falAudio } from '@tanstack/ai-fal'
const result = await generateAudio({
adapter: falAudio('fal-ai/diffrhythm'),
prompt: 'An upbeat electronic track with synths',
threadId: 'thread-1',
modelOptions: {
lyrics: '[verse]\nHello world\n[chorus]\nLa la la',
},
})fal.ai sound effects#
import { generateAudio } from '@tanstack/ai'
import { falAudio } from '@tanstack/ai-fal'
const result = await generateAudio({
adapter: falAudio('fal-ai/elevenlabs/sound-effects/v2'),
prompt: 'Thunderclap followed by heavy rain',
threadId: 'thread-1',
duration: 5,
})Advanced#
MiniMax Music v2 lyrics#
MiniMax Music v2 takes lyric guidance in the lyrics_prompt field:
import { generateAudio } from '@tanstack/ai'
import { falAudio } from '@tanstack/ai-fal'
const result = await generateAudio({
adapter: falAudio('fal-ai/minimax-music/v2'),
prompt: 'A dreamy pop ballad in the style of the 80s',
threadId: 'thread-1',
modelOptions: {
lyrics_prompt: '[instrumental]',
},
})Result shape#
import type { TokenUsage } from '@tanstack/ai'
interface AudioGenerationResult {
id: string
model: string
audio: {
url?: string
b64Json?: string
contentType?: string
duration?: number
}
// Same TokenUsage shape as chat. Present when the provider reports it
// (for example Gemini Lyria). fal sets `usage.billed` instead:
// { quantity, unit: 'units' }, read from the `x-fal-billable-units`
// result header. Multiply the quantity by the endpoint unit price
// (from the fal pricing API) to get the exact cost.
usage?: TokenUsage
}An audio object holds either url or b64Json, never both. Gemini returns b64Json. fal returns url. To get raw bytes from a fal result, fetch the URL:
import { generateAudio } from '@tanstack/ai'
import { falAudio } from '@tanstack/ai-fal'
const result = await generateAudio({
adapter: falAudio('fal-ai/diffrhythm'),
prompt: 'An upbeat electronic track',
threadId: 'thread-1',
})
if (result.audio.url) {
const response = await fetch(result.audio.url)
const bytes = new Uint8Array(await response.arrayBuffer())
}Audio generation or text-to-speech#
| generateAudio() | generateSpeech() | |
|---|---|---|
| Purpose | Music, soundscapes, sound effects | Spoken words |
| Result | result.audio.url or result.audio.b64Json | Base64 in result.audio |
| Main input | prompt | text |
| Voice and speed controls | No | Yes (voice, speed) |
Debug a request#
If the audio is not what you expected, pass debug: true. Examples: the model cuts the track short, the provider rejects the prompt, or the response shape looks wrong. The adapter then logs every chunk that the provider SDK sends. See Debug Logging.
API keys#
Each provider reads its key from the environment by default:
GOOGLE_API_KEY=your-google-api-key
FAL_KEY=your-fal-api-keyTo pass the key in code:
import { createGeminiAudio } from '@tanstack/ai-gemini'
import { falAudio } from '@tanstack/ai-fal'
createGeminiAudio('lyria-3-pro-preview', 'your-key')
falAudio('fal-ai/diffrhythm', { apiKey: 'your-key' })To let each user bring their own key, see the useGenerateAudio example in Generation Hooks.