(params: GenerateFalVideoParams)
| 135 | } |
| 136 | |
| 137 | export async function generateFalVideo(params: GenerateFalVideoParams): Promise<GeneratedVideo> { |
| 138 | const model = params.model || DEFAULT_VIDEO_MODEL |
| 139 | const config = VIDEO_MODELS[model] |
| 140 | if (!config) { |
| 141 | throw new Error( |
| 142 | `Unknown video model: ${model}. Supported: ${Object.keys(VIDEO_MODELS).join(', ')}` |
| 143 | ) |
| 144 | } |
| 145 | |
| 146 | const apiKey = getFalApiKey() |
| 147 | |
| 148 | let endpoint = config.endpoint |
| 149 | const input: Record<string, unknown> = { prompt: params.prompt } |
| 150 | |
| 151 | if (params.imageDataUri) { |
| 152 | if (!config.i2vEndpoint) { |
| 153 | throw new Error( |
| 154 | `Image-to-video is not supported for model ${model}. Try veo-3.1, veo-3.1-fast, seedance-2.0, or kling-v3-pro.` |
| 155 | ) |
| 156 | } |
| 157 | endpoint = config.i2vEndpoint |
| 158 | input.image_url = params.imageDataUri |
| 159 | } |
| 160 | |
| 161 | const duration = formatDuration(config.durationFormat, params.duration) |
| 162 | if (duration !== undefined) input.duration = duration |
| 163 | if (config.supportsAspectRatio && params.aspectRatio) input.aspect_ratio = params.aspectRatio |
| 164 | if (config.supportsResolution && params.resolution) input.resolution = params.resolution |
| 165 | if (config.supportsGenerateAudio && params.generateAudio !== undefined) { |
| 166 | input.generate_audio = params.generateAudio |
| 167 | } |
| 168 | if (config.supportsNegativePrompt && params.negativePrompt) { |
| 169 | input.negative_prompt = params.negativePrompt |
| 170 | } |
| 171 | if (config.supportsPromptOptimizer && params.promptOptimizer !== undefined) { |
| 172 | input.prompt_optimizer = params.promptOptimizer |
| 173 | } |
| 174 | |
| 175 | const { requestId, data } = await runFalQueue(endpoint, input, apiKey) |
| 176 | const url = extractFalMediaUrl(data, ['video', 'output']) |
| 177 | if (!url) throw new Error('No video URL in Fal.ai response') |
| 178 | |
| 179 | const videoNode = isRecordLike(data.video) ? data.video : undefined |
| 180 | const { buffer, contentType } = await downloadFalMedia(url) |
| 181 | const cost = await getFalAICostMetadata({ apiKey, endpointId: endpoint, requestId }) |
| 182 | |
| 183 | return { |
| 184 | buffer, |
| 185 | contentType: contentType.startsWith('video/') ? contentType : 'video/mp4', |
| 186 | width: getNumberProp(videoNode, 'width'), |
| 187 | height: getNumberProp(videoNode, 'height'), |
| 188 | model, |
| 189 | endpoint, |
| 190 | jobId: requestId, |
| 191 | cost, |
| 192 | } |
| 193 | } |
no test coverage detected