diff --git a/src/app/api/generate/providers/kie.ts b/src/app/api/generate/providers/kie.ts index 9ed5be40..e73a278e 100644 --- a/src/app/api/generate/providers/kie.ts +++ b/src/app/api/generate/providers/kie.ts @@ -133,6 +133,21 @@ export function getKieModelDefaults(modelId: string): Record { 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 = { 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`); diff --git a/src/app/api/models/[modelId]/route.ts b/src/app/api/models/[modelId]/route.ts index d29679e9..1300fc5b 100644 --- a/src/app/api/models/[modelId]/route.ts +++ b/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: [ diff --git a/src/app/api/models/route.ts b/src/app/api/models/route.ts index c536ebac..35952835 100644 --- a/src/app/api/models/route.ts +++ b/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; }