MCPcopy Create free account
hub / github.com/TanStack/ai / transformResponse

Method transformResponse

packages/ai-fal/src/adapters/speech.ts:89–148  ·  view source on GitHub ↗
(
    response: Result<OutputType<TModel>>,
  )

Source from the content-addressed store, hash-verified

87 }
88
89 private async transformResponse(
90 response: Result<OutputType<TModel>>,
91 ): Promise<TTSResult> {
92 const data = response.data as Record<string, unknown>
93
94 // fal returns { audio: { url, content_type } } or { audio_url: string }
95 let audioUrl: string | undefined
96 let contentType: string | undefined
97
98 if (
99 data['audio'] &&
100 typeof data['audio'] === 'object' &&
101 'url' in data['audio']
102 ) {
103 const audioObj = data['audio'] as { url: string; content_type?: string }
104 audioUrl = audioObj.url
105 contentType = audioObj.content_type
106 } else if (typeof data.audio_url === 'string') {
107 audioUrl = data.audio_url
108 }
109
110 if (!audioUrl) {
111 throw new Error('Audio URL not found in fal TTS response')
112 }
113
114 // Fetch the audio and convert to base64 to match TTSResult contract.
115 // Using a chunked helper here — spreading Uint8Array into btoa exceeds
116 // V8's argument limit (~65k) for any realistic TTS clip.
117 const audioResponse = await fetch(audioUrl)
118 if (!audioResponse.ok) {
119 throw new Error(
120 `Failed to fetch audio from ${audioUrl}: ${audioResponse.status} ${audioResponse.statusText}`,
121 )
122 }
123 const arrayBuffer = await audioResponse.arrayBuffer()
124 const base64 = arrayBufferToBase64(arrayBuffer)
125
126 // Strip parameters like `; charset=...` from contentType, and only use
127 // the URL extension as a fallback when it looks like a real extension.
128 const contentTypeMime = contentType?.split(';')[0]?.trim()
129 const safeUrlExtension = extractUrlExtension(audioUrl)
130 // Prefer URL-derived extension when available (more canonical for file
131 // consumers), otherwise derive from the content-type mime subtype, then
132 // fall back to `wav`. Normalize `mpeg` → `mp3` so the format field is a
133 // usable file extension rather than the IANA subtype.
134 const rawFormat =
135 safeUrlExtension || contentTypeMime?.split('/')[1] || 'wav'
136 const format = rawFormat === 'mpeg' ? 'mp3' : rawFormat
137
138 const usage = buildFalUsage(takeBillableUnits(response.requestId))
139
140 return {
141 id: response.requestId || this.generateId(),
142 model: this.model,
143 audio: base64,
144 format,
145 contentType: contentTypeMime || `audio/${format}`,
146 ...(usage ? { usage } : {}),

Callers 1

generateSpeechMethod · 0.95

Calls 7

generateIdMethod · 0.95
arrayBufferToBase64Function · 0.90
extractUrlExtensionFunction · 0.90
buildFalUsageFunction · 0.90
takeBillableUnitsFunction · 0.90
arrayBufferMethod · 0.80
fetchFunction · 0.50

Tested by

no test coverage detected