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.
You need a React app with a server route, and a fal.ai API key. This takes about 5 minutes.
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-falFAL_KEY=your-fal-api-keyimport {
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)
}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.
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.
| 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. |
| 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 |
Lyria makes full songs with vocals and instruments:
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"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"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',
},
})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,
})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]',
},
})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())
}| 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) |
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.
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.