Browse Source

feat: add missing audio models (music, TTS schemas, sound effects)

- Add text-to-music and text-to-sound-effects to fal.ai RELEVANT_CATEGORIES
  so music/SFX models (MiniMax Music, Lyria 2, Stable Audio, etc.) appear
- Map new fal categories to text-to-audio capability in mapFalCategory
- Add ElevenLabs Sound Effects v2 model to KIE_MODELS registry
- Add parameter schemas for all 4 ElevenLabs models (turbo-v2.5,
  multilingual-v2, text-to-dialogue-v3, sound-effect-v2) in getKieSchema
- Add model defaults for ElevenLabs TTS and SFX models
- Remap prompt->text field for ElevenLabs models (Kie API requirement)
- Add audio response detection in generateWithKie (capability, URL
  extension, and content-type checks) so audio output is correctly
  returned as type "audio" instead of being misclassified as "image"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
436db12a8c
  1. 56
      src/app/api/generate/providers/kie.ts
  2. 36
      src/app/api/models/[modelId]/route.ts
  3. 20
      src/app/api/models/route.ts

56
src/app/api/generate/providers/kie.ts

@ -133,6 +133,21 @@ export function getKieModelDefaults(modelId: string): Record<string, unknown> {
aspect_ratio: "16:9",
};
// ElevenLabs TTS models
case "elevenlabs/turbo-v2.5":
case "elevenlabs/multilingual-v2":
case "elevenlabs/text-to-dialogue-v3":
return {
output_format: "mp3_44100_128",
};
// ElevenLabs Sound Effects
case "elevenlabs/sound-effect-v2":
return {
output_format: "mp3_44100_128",
prompt_influence: 0.3,
};
default:
return {};
}
@ -600,6 +615,14 @@ export async function generateWithKie(
};
}
// ElevenLabs models use "text" instead of "prompt"
if (modelId.startsWith("elevenlabs/")) {
if (inputParams.prompt) {
inputParams.text = inputParams.prompt;
delete inputParams.prompt;
}
}
// All remaining Kie models use the standard createTask endpoint
const requestBody: Record<string, unknown> = {
model: modelId,
@ -692,6 +715,13 @@ export async function generateWithKie(
const data = pollResult.data;
let mediaUrl: string | null = null;
let isVideo = false;
let isAudio = false;
// Detect audio from model capabilities
const isAudioModel = input.model.capabilities.some(c => c.includes("audio"));
if (isAudioModel) {
isAudio = true;
}
console.log(`[API:${requestId}] Kie poll result data:`, JSON.stringify(data).substring(0, 500));
@ -751,10 +781,15 @@ export async function generateWithKie(
}
// Detect video from URL if not already detected
if (!isVideo && (mediaUrl.includes('.mp4') || mediaUrl.includes('.webm') || mediaUrl.includes('video'))) {
if (!isVideo && !isAudio && (mediaUrl.includes('.mp4') || mediaUrl.includes('.webm') || mediaUrl.includes('video'))) {
isVideo = true;
}
// Detect audio from URL if not already detected
if (!isVideo && !isAudio && (mediaUrl.includes('.mp3') || mediaUrl.includes('.wav') || mediaUrl.includes('.ogg') || mediaUrl.includes('.flac'))) {
isAudio = true;
}
// Validate URL before fetching
const mediaUrlCheck = validateMediaUrl(mediaUrl);
if (!mediaUrlCheck.valid) {
@ -778,10 +813,13 @@ export async function generateWithKie(
return { success: false, error: `Media too large: ${(mediaContentLength / (1024 * 1024)).toFixed(0)}MB > 500MB limit` };
}
const contentType = mediaResponse.headers.get("content-type") || (isVideo ? "video/mp4" : "image/png");
const contentType = mediaResponse.headers.get("content-type") || (isVideo ? "video/mp4" : isAudio ? "audio/mpeg" : "image/png");
if (contentType.startsWith("video/")) {
isVideo = true;
}
if (contentType.startsWith("audio/")) {
isAudio = true;
}
const mediaArrayBuffer = await mediaResponse.arrayBuffer();
if (mediaArrayBuffer.byteLength > MAX_MEDIA_SIZE) {
@ -792,6 +830,20 @@ export async function generateWithKie(
console.log(`[API:${requestId}] Output: ${contentType}, ${mediaSizeMB.toFixed(2)}MB`);
// For audio models, return base64 encoded audio
if (isAudio) {
const audioBase64 = Buffer.from(mediaArrayBuffer).toString("base64");
console.log(`[API:${requestId}] SUCCESS - Returning audio`);
return {
success: true,
outputs: [{
type: "audio",
data: `data:${contentType};base64,${audioBase64}`,
url: mediaUrl,
}],
};
}
// For very large videos (>20MB), return URL only (data left empty for consumers)
if (isVideo && mediaSizeMB > 20) {
console.log(`[API:${requestId}] SUCCESS - Returning URL for large video`);

36
src/app/api/models/[modelId]/route.ts

@ -609,6 +609,42 @@ function getKieSchema(modelId: string): ExtractedSchema {
{ name: "image_urls", type: "image", required: true, label: "Image", isArray: true },
],
},
// ============ Audio/TTS models ============
"elevenlabs/turbo-v2.5": {
parameters: [
{ name: "voice_id", type: "string", description: "Voice ID to use for synthesis" },
{ name: "stability", type: "number", description: "Voice stability (0-1)", default: 0.5, minimum: 0, maximum: 1 },
{ name: "similarity_boost", type: "number", description: "Similarity boost (0-1)", default: 0.75, minimum: 0, maximum: 1 },
{ name: "output_format", type: "string", description: "Audio output format", enum: ["mp3_44100_128", "mp3_44100_192", "pcm_16000", "pcm_22050", "pcm_24000", "pcm_44100"], default: "mp3_44100_128" },
],
inputs: [{ name: "prompt", type: "text", required: true, label: "Text" }],
},
"elevenlabs/multilingual-v2": {
parameters: [
{ name: "voice_id", type: "string", description: "Voice ID to use for synthesis" },
{ name: "stability", type: "number", description: "Voice stability (0-1)", default: 0.5, minimum: 0, maximum: 1 },
{ name: "similarity_boost", type: "number", description: "Similarity boost (0-1)", default: 0.75, minimum: 0, maximum: 1 },
{ name: "output_format", type: "string", description: "Audio output format", enum: ["mp3_44100_128", "mp3_44100_192", "pcm_16000", "pcm_22050", "pcm_24000", "pcm_44100"], default: "mp3_44100_128" },
],
inputs: [{ name: "prompt", type: "text", required: true, label: "Text" }],
},
"elevenlabs/text-to-dialogue-v3": {
parameters: [
{ name: "stability", type: "number", description: "Voice stability (0-1)", default: 0.5, minimum: 0, maximum: 1 },
{ name: "similarity_boost", type: "number", description: "Similarity boost (0-1)", default: 0.75, minimum: 0, maximum: 1 },
{ name: "output_format", type: "string", description: "Audio output format", enum: ["mp3_44100_128", "mp3_44100_192", "pcm_16000", "pcm_22050", "pcm_24000", "pcm_44100"], default: "mp3_44100_128" },
],
inputs: [{ name: "prompt", type: "text", required: true, label: "Text / Dialogue Script" }],
},
"elevenlabs/sound-effect-v2": {
parameters: [
{ name: "duration_seconds", type: "number", description: "Duration in seconds (0.5-22)", minimum: 0.5, maximum: 22 },
{ name: "loop", type: "boolean", description: "Enable smooth looping", default: false },
{ name: "prompt_influence", type: "number", description: "How closely to follow the prompt (0-1)", default: 0.3, minimum: 0, maximum: 1 },
{ name: "output_format", type: "string", description: "Audio output format", enum: ["mp3_44100_128", "mp3_44100_192", "pcm_16000", "pcm_22050", "pcm_24000", "pcm_44100"], default: "mp3_44100_128" },
],
inputs: [{ name: "prompt", type: "text", required: true, label: "Sound Description" }],
},
// ============ Video models ============
"grok-imagine/text-to-video": {
parameters: [

20
src/app/api/models/route.ts

@ -52,6 +52,8 @@ const RELEVANT_CATEGORIES = [
"text-to-3d",
"image-to-3d",
"text-to-speech",
"text-to-music",
"text-to-sound-effects",
];
// Kie.ai models (hardcoded - no discovery API available)
@ -310,7 +312,7 @@ const KIE_MODELS: ProviderModel[] = [
coverImage: undefined,
pageUrl: "https://docs.kie.ai/veo3-api/quickstart",
},
// ============ Audio/TTS Models (3) ============
// ============ Audio/TTS Models (4) ============
{
id: "elevenlabs/turbo-v2.5",
name: "ElevenLabs Turbo v2.5",
@ -341,6 +343,16 @@ const KIE_MODELS: ProviderModel[] = [
pricing: { type: "per-run", amount: 0.06, currency: "USD" },
pageUrl: "https://kie.ai/elevenlabs/text-to-dialogue-v3",
},
{
id: "elevenlabs/sound-effect-v2",
name: "ElevenLabs Sound Effects v2",
description: "Generate sound effects from text descriptions. Supports looping, 0.5-22 second duration, and multiple output formats via Kie.ai.",
provider: "kie",
capabilities: ["text-to-audio"],
coverImage: undefined,
pricing: { type: "per-run", amount: 0.02, currency: "USD" },
pageUrl: "https://kie.ai/elevenlabs-sound-effect",
},
];
// Gemini image models (hardcoded - these don't come from an external API)
@ -759,6 +771,12 @@ function mapFalCategory(category: string): ModelCapability | null {
if (category === "text-to-speech") {
return "text-to-audio";
}
if (category === "text-to-music") {
return "text-to-audio";
}
if (category === "text-to-sound-effects") {
return "text-to-audio";
}
if (RELEVANT_CATEGORIES.includes(category)) {
return category as ModelCapability;
}

Loading…
Cancel
Save