From 770aa1b123317b696547ba2e09592b2f2d5931b8 Mon Sep 17 00:00:00 2001 From: wauputr4 <103489788+wauputr4@users.noreply.github.com> Date: Wed, 6 May 2026 23:23:41 +0700 Subject: [PATCH] feat: add kie media provider support --- open-sse/config/audioRegistry.ts | 26 +++ open-sse/config/imageRegistry.ts | 44 +++- open-sse/config/musicRegistry.ts | 8 +- open-sse/config/videoRegistry.ts | 45 ++-- open-sse/executors/index.ts | 3 + open-sse/executors/kie.ts | 140 +++++++++++++ open-sse/handlers/audioSpeech.ts | 197 ++++++++++++++++++ open-sse/handlers/audioTranscription.ts | 89 ++++++++ open-sse/handlers/imageGeneration.ts | 162 ++++++++++---- open-sse/handlers/musicGeneration.ts | 149 ++++++++++--- open-sse/handlers/videoGeneration.ts | 32 ++- public/providers/kie.png | Bin 0 -> 52959 bytes .../dashboard/cache/media/MediaPageClient.tsx | 75 ++++--- src/lib/dataPaths.js | 70 ------- src/lib/providers/validation.ts | 50 +++++ src/shared/components/ProviderIcon.tsx | 1 + src/shared/utils/nodeRuntimeSupport.ts | 1 + 17 files changed, 877 insertions(+), 215 deletions(-) create mode 100644 open-sse/executors/kie.ts create mode 100644 public/providers/kie.png delete mode 100644 src/lib/dataPaths.js diff --git a/open-sse/config/audioRegistry.ts b/open-sse/config/audioRegistry.ts index d95c1483c2..f6ba4ffe27 100644 --- a/open-sse/config/audioRegistry.ts +++ b/open-sse/config/audioRegistry.ts @@ -100,6 +100,18 @@ export const AUDIO_TRANSCRIPTION_PROVIDERS: Record = { format: "openai", models: [{ id: "qwen3-asr", name: "Qwen3 ASR" }], }, + + kie: { + id: "kie", + baseUrl: "https://api.kie.ai", + authType: "apikey", + authHeader: "bearer", + format: "kie-audio", + models: [ + { id: "elevenlabs/speech-to-text", name: "ElevenLabs STT" }, + { id: "elevenlabs/audio-isolation", name: "ElevenLabs Audio Isolation" }, + ], + }, }; export const AUDIO_SPEECH_PROVIDERS: Record = { @@ -246,6 +258,20 @@ export const AUDIO_SPEECH_PROVIDERS: Record = { { id: "Play3.0-mini", name: "Play3.0 Mini" }, ], }, + + kie: { + id: "kie", + baseUrl: "https://api.kie.ai", + authType: "apikey", + authHeader: "bearer", + format: "kie-audio", + models: [ + { id: "elevenlabs/text-to-speech-multilingual-v2", name: "ElevenLabs TTS v2" }, + { id: "elevenlabs/text-to-speech-turbo-2-5", name: "ElevenLabs TTS Turbo 2.5" }, + { id: "elevenlabs/text-to-dialogue-v3", name: "ElevenLabs Text to Dialogue v3" }, + { id: "elevenlabs/sound-effect-v2", name: "ElevenLabs Sound Effect v2" }, + ], + }, }; /** diff --git a/open-sse/config/imageRegistry.ts b/open-sse/config/imageRegistry.ts index 2315608f02..17f9162caa 100644 --- a/open-sse/config/imageRegistry.ts +++ b/open-sse/config/imageRegistry.ts @@ -10,6 +10,7 @@ interface ImageModelEntry { name: string; inputModalities?: string[]; description?: string; + isMarket?: boolean; } interface ImageProviderConfig { @@ -233,12 +234,49 @@ export const IMAGE_PROVIDERS: Record = { kie: { id: "kie", - baseUrl: "https://api.kie.ai/api/v1/gpt4o-image/generate", - statusUrl: "https://api.kie.ai/api/v1/gpt4o-image/record-info", + baseUrl: "https://api.kie.ai", + statusUrl: "https://api.kie.ai/api/v1/jobs/recordInfo", authType: "apikey", authHeader: "bearer", format: "kie-image", - models: [{ id: "gpt4o-image", name: "KIE 4o Image" }], + models: [ + { id: "gpt4o-image", name: "KIE 4o Image" }, + { id: "seedream/4.5-text-to-image", name: "Seedream 4.5", isMarket: true }, + { id: "seedream/4.5-edit", name: "Seedream 4.5 Edit", isMarket: true }, + { id: "seedream/5.0-lite-text-to-image", name: "Seedream 5.0 Lite", isMarket: true }, + { id: "seedream/5.0-lite-image-to-image", name: "Seedream 5.0 Lite I2I", isMarket: true }, + { id: "z-image/4.0-text-to-image", name: "Z-Image v4.0", isMarket: true }, + { id: "z-image/4.5-text-to-image", name: "Z-Image v4.5", isMarket: true }, + { id: "google-imagen/imagen4-fast", name: "Imagen 4 Fast", isMarket: true }, + { id: "google-imagen/imagen4-ultra", name: "Imagen 4 Ultra", isMarket: true }, + { id: "google-imagen/imagen4", name: "Imagen 4", isMarket: true }, + { id: "google-imagen/nano-banana-2", name: "Nano Banana 2", isMarket: true }, + { id: "google-imagen/nano-banana", name: "Nano Banana", isMarket: true }, + { id: "google-imagen/nano-banana-pro", name: "Nano Banana Pro", isMarket: true }, + { id: "google-imagen/nano-banana-edit", name: "Nano Banana Edit", isMarket: true }, + { id: "flux/2-pro-image-to-image", name: "Flux 2 Pro I2I", isMarket: true }, + { id: "flux/2-pro-text-to-image", name: "Flux 2 Pro T2I", isMarket: true }, + { id: "flux/2-image-to-image", name: "Flux 2 I2I", isMarket: true }, + { id: "flux/2-text-to-image", name: "Flux 2 T2I", isMarket: true }, + { id: "flux/kontext", name: "Flux Kontext", isMarket: true }, + { id: "grok-imagine/text-to-image", name: "Grok Imagine T2I", isMarket: true }, + { id: "grok-imagine/image-to-image", name: "Grok Imagine I2I", isMarket: true }, + { id: "gpt/gpt-image-1.5-text-to-image", name: "GPT Image 1.5 T2I", isMarket: true }, + { id: "gpt/gpt-image-1.5-image-to-image", name: "GPT Image 1.5 I2I", isMarket: true }, + { id: "gpt/gpt-image-2-text-to-image", name: "GPT Image 2 T2I", isMarket: true }, + { id: "gpt/gpt-image-2-image-to-image", name: "GPT Image 2 I2I", isMarket: true }, + { id: "ideogram/v3-text-to-image", name: "Ideogram v3", isMarket: true }, + { id: "ideogram/v3-edit", name: "Ideogram v3 Edit", isMarket: true }, + { id: "ideogram/v3-remix", name: "Ideogram v3 Remix", isMarket: true }, + { id: "ideogram/v3-reframe", name: "Ideogram v3 Reframe", isMarket: true }, + { id: "qwen/text-to-image", name: "Qwen T2I", isMarket: true }, + { id: "qwen/image-to-image", name: "Qwen I2I", isMarket: true }, + { id: "qwen/image-edit", name: "Qwen Edit", isMarket: true }, + { id: "qwen2/image-edit", name: "Qwen2 Edit", isMarket: true }, + { id: "qwen2/text-to-image", name: "Qwen2 T2I", isMarket: true }, + { id: "wan/2.7-image", name: "Wan 2.7 Image", isMarket: true }, + { id: "wan/2.7-image-pro", name: "Wan 2.7 Image Pro", isMarket: true }, + ], supportedSizes: ["1:1", "16:9", "9:16", "4:3", "3:4"], }, diff --git a/open-sse/config/musicRegistry.ts b/open-sse/config/musicRegistry.ts index 6cce3b22ee..38ae3c779c 100644 --- a/open-sse/config/musicRegistry.ts +++ b/open-sse/config/musicRegistry.ts @@ -10,6 +10,7 @@ import { parseModelFromRegistry, getAllModelsFromRegistry } from "./registryUtil interface MusicModel { id: string; name: string; + isMarket?: boolean; } interface MusicProvider { @@ -26,14 +27,13 @@ export const MUSIC_PROVIDERS: Record = { kie: { id: "kie", baseUrl: "https://api.kie.ai", - statusUrl: "https://api.kie.ai/api/v1/generate/record-info", + statusUrl: "https://api.kie.ai/api/v1/jobs/recordInfo", authType: "apikey", authHeader: "bearer", format: "kie-music", models: [ - { id: "V4", name: "Suno V4" }, - { id: "V4_5", name: "Suno V4.5" }, - { id: "V5", name: "Suno V5" }, + { id: "suno-v3.5", name: "Suno V3.5" }, + { id: "suno-v4.0", name: "Suno V4.0" }, ], }, diff --git a/open-sse/config/videoRegistry.ts b/open-sse/config/videoRegistry.ts index 2778c3e0b8..1d1fc952dd 100644 --- a/open-sse/config/videoRegistry.ts +++ b/open-sse/config/videoRegistry.ts @@ -10,6 +10,7 @@ import { parseModelFromRegistry, getAllModelsFromRegistry } from "./registryUtil interface VideoModel { id: string; name: string; + isMarket?: boolean; } interface VideoProvider { @@ -31,23 +32,33 @@ export const VIDEO_PROVIDERS: Record = { authHeader: "bearer", format: "kie-video", models: [ - { id: "kling-2.6/text-to-video", name: "Kling 2.6 Text to Video" }, - { id: "kling/v2-1-master-image-to-video", name: "Kling v2.1 Master I2V" }, - { id: "kling/v2-1-master-text-to-video", name: "Kling v2.1 Master T2V" }, - { id: "kling/v25-turbo-image-to-video-pro", name: "Kling v2.5 Turbo I2V Pro" }, - { id: "kling/v25-turbo-text-to-video-pro", name: "Kling v2.5 Turbo T2V Pro" }, - { id: "wan/2-6-text-to-video", name: "Wan 2.6 Text to Video" }, - { id: "wan/2-6-image-to-video", name: "Wan 2.6 Image to Video" }, - { id: "wan/2-7-text-to-video", name: "Wan 2.7 Text to Video" }, - { id: "wan/2-7-image-to-video", name: "Wan 2.7 Image to Video" }, - { id: "sora2/sora-2-text-to-video", name: "Sora 2 Text to Video" }, - { id: "sora2/sora-2-image-to-video", name: "Sora 2 Image to Video" }, - { id: "hailuo/02-text-to-video-pro", name: "Hailuo 02 T2V Pro" }, - { id: "hailuo/02-image-to-video-pro", name: "Hailuo 02 I2V Pro" }, - { id: "grok-imagine/text-to-video", name: "Grok Imagine T2V" }, - { id: "grok-imagine/image-to-video", name: "Grok Imagine I2V" }, - { id: "bytedance/v1-pro-text-to-video", name: "Bytedance v1 Pro T2V" }, - { id: "bytedance/v1-pro-image-to-video", name: "Bytedance v1 Pro I2V" }, + { id: "veo/veo-3-1", name: "Veo 3.1", isMarket: true }, + { id: "veo/veo-3-1-fast", name: "Veo 3.1 Fast", isMarket: true }, + { + id: "kling/kling-v2-1-master-text-to-video", + name: "Kling v2.1 Master T2V", + isMarket: true, + }, + { + id: "kling/kling-v2-1-master-image-to-video", + name: "Kling v2.1 Master I2V", + isMarket: true, + }, + { id: "kling/v2-5-turbo-text-to-video", name: "Kling v2.5 Turbo T2V", isMarket: true }, + { id: "kling/v2-5-turbo-image-to-video", name: "Kling v2.5 Turbo I2V", isMarket: true }, + { id: "kling/v3-0", name: "Kling v3.0", isMarket: true }, + { id: "wan/2-6-text-to-video", name: "Wan 2.6 T2V", isMarket: true }, + { id: "wan/2-6-image-to-video", name: "Wan 2.6 I2V", isMarket: true }, + { id: "wan/2-7-text-to-video", name: "Wan 2.7 T2V", isMarket: true }, + { id: "wan/2-7-image-to-video", name: "Wan 2.7 I2V", isMarket: true }, + { id: "sora2/sora-2-text-to-video", name: "Sora 2 T2V", isMarket: true }, + { id: "sora2/sora-2-image-to-video", name: "Sora 2 I2V", isMarket: true }, + { id: "hailuo/02-text-to-video-pro", name: "Hailuo 02 T2V Pro", isMarket: true }, + { id: "hailuo/02-image-to-video-pro", name: "Hailuo 02 I2V Pro", isMarket: true }, + { id: "grok-imagine/text-to-video", name: "Grok Imagine T2V", isMarket: true }, + { id: "grok-imagine/image-to-video", name: "Grok Imagine I2V", isMarket: true }, + { id: "bytedance/v2-0-text-to-video", name: "Seedance v2.0 T2V", isMarket: true }, + { id: "bytedance/v2-0-fast-text-to-video", name: "Seedance v2.0 Fast T2V", isMarket: true }, ], }, diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index d6386eb066..2153aa5b51 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -14,6 +14,7 @@ import { VertexExecutor } from "./vertex.ts"; import { CliproxyapiExecutor } from "./cliproxyapi.ts"; import { PerplexityWebExecutor } from "./perplexity-web.ts"; import { GrokWebExecutor } from "./grok-web.ts"; +import { KieExecutor } from "./kie.ts"; const executors = { antigravity: new AntigravityExecutor(), @@ -38,6 +39,7 @@ const executors = { "perplexity-web": new PerplexityWebExecutor(), "pplx-web": new PerplexityWebExecutor(), // Alias "grok-web": new GrokWebExecutor(), + kie: new KieExecutor(), }; const defaultCache = new Map(); @@ -69,3 +71,4 @@ export { CliproxyapiExecutor } from "./cliproxyapi.ts"; export { VertexExecutor } from "./vertex.ts"; export { PerplexityWebExecutor } from "./perplexity-web.ts"; export { GrokWebExecutor } from "./grok-web.ts"; +export { KieExecutor } from "./kie.ts"; diff --git a/open-sse/executors/kie.ts b/open-sse/executors/kie.ts new file mode 100644 index 0000000000..fbb2758822 --- /dev/null +++ b/open-sse/executors/kie.ts @@ -0,0 +1,140 @@ +import { BaseExecutor } from "./base.ts"; +import { sleep } from "../utils/sleep.ts"; + +type KieTaskInput = { + baseUrl: string; + token: string; + payload: unknown; + endpoint?: string; +}; + +type KiePollInput = { + statusUrl: string; + taskId: string; + token: string; + timeoutMs: number; + pollIntervalMs: number; +}; + +export type KieTaskState = "success" | "failed" | "pending"; + +export type KieTaskRecord = { + data: any; + state: KieTaskState; +}; + +function normalizeBaseUrl(baseUrl: string): string { + return baseUrl.replace(/\/$/, ""); +} + +export function normalizeKieTaskState(recordData: any): KieTaskState { + const state = String( + recordData?.data?.status ?? + recordData?.data?.state ?? + recordData?.data?.successFlag ?? + recordData?.msg ?? + "PENDING" + ).toUpperCase(); + + if ( + state === "SUCCESS" || + state === "1" || + state === "FINISHED" || + state === "COMPLETE" || + state === "COMPLETED" || + state === "FIRST_SUCCESS" || + state === "ALL_SUCCESS" || + state.includes("SUCCESS") + ) { + return "success"; + } + + if ( + state === "FAIL" || + state === "FAILED" || + state === "ERROR" || + state === "2" || + state === "3" || + state.includes("FAIL") || + state.includes("ERROR") || + state === "CREATE_TASK_FAILED" || + state === "GENERATE_FAILED" + ) { + return "failed"; + } + + return "pending"; +} + +export class KieExecutor extends BaseExecutor { + constructor() { + super("kie", { baseUrl: "https://api.kie.ai" }); + } + + getTaskCreateUrl(baseUrl: string, endpoint = "/api/v1/jobs/createTask"): string { + return `${normalizeBaseUrl(baseUrl)}${endpoint}`; + } + + getTaskStatusUrl(baseUrl: string): string { + return `${normalizeBaseUrl(baseUrl)}/api/v1/jobs/recordInfo`; + } + + async createTask({ baseUrl, token, payload, endpoint }: KieTaskInput): Promise { + const res = await fetch(this.getTaskCreateUrl(baseUrl, endpoint), { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + if (!res.ok) { + const error = await res.text(); + throw Object.assign(new Error(error || `Kie createTask failed with status ${res.status}`), { + status: res.status, + }); + } + + return res.json(); + } + + async pollTask({ + statusUrl, + taskId, + token, + timeoutMs, + pollIntervalMs, + }: KiePollInput): Promise { + const deadline = Date.now() + timeoutMs; + + while (Date.now() < deadline) { + const pollUrl = new URL(statusUrl); + pollUrl.searchParams.set("taskId", String(taskId)); + + const res = await fetch(pollUrl.toString(), { + method: "GET", + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!res.ok) { + const error = await res.text(); + throw Object.assign(new Error(error || `Kie poll failed with status ${res.status}`), { + status: res.status, + }); + } + + const data = await res.json(); + const state = normalizeKieTaskState(data); + if (state !== "pending") { + return { data, state }; + } + + await sleep(pollIntervalMs); + } + + throw Object.assign(new Error("Kie task timed out"), { status: 504 }); + } +} + +export const kieExecutor = new KieExecutor(); diff --git a/open-sse/handlers/audioSpeech.ts b/open-sse/handlers/audioSpeech.ts index 424ef10774..27d0853d6c 100644 --- a/open-sse/handlers/audioSpeech.ts +++ b/open-sse/handlers/audioSpeech.ts @@ -19,6 +19,7 @@ import { getCorsOrigin } from "../utils/cors.ts"; import { getSpeechProvider, parseSpeechModel } from "../config/audioRegistry.ts"; import { buildAuthHeaders } from "../config/registryUtils.ts"; +import { kieExecutor } from "../executors/kie.ts"; import { errorResponse } from "../utils/error.ts"; /** @@ -68,6 +69,104 @@ function audioStreamResponse(res, defaultContentType = "audio/mpeg") { }); } +function getKieCallbackUrl(body: any): string { + return ( + body.callBackUrl || + body.callback_url || + body.callbackUrl || + "https://omniroute.local/api/kie/callback" + ); +} + +function normalizeKieElevenLabsVoice(voice: unknown): string { + const value = typeof voice === "string" ? voice.trim() : ""; + const aliases: Record = { + alloy: "Rachel", + echo: "Adam", + fable: "Brian", + onyx: "Antoni", + nova: "Bella", + shimmer: "Dorothy", + }; + return aliases[value.toLowerCase()] || value || "Rachel"; +} + +function parseKieResultJson(recordData: any): any { + try { + return typeof recordData?.data?.resultJson === "string" + ? JSON.parse(recordData.data.resultJson) + : recordData?.data?.resultJson || {}; + } catch { + return {}; + } +} + +function findAudioUrlDeep(value: any): string | null { + if (!value) return null; + + if (typeof value === "string") { + if (/^https?:\/\//i.test(value) && !/\.(jpg|jpeg|png|webp|gif|svg)(\?|$)/i.test(value)) { + return value; + } + return null; + } + + if (Array.isArray(value)) { + for (const item of value) { + const url = findAudioUrlDeep(item); + if (url) return url; + } + return null; + } + + if (typeof value === "object") { + const preferredKeys = [ + "audio_url", + "audioUrl", + "stream_audio_url", + "streamAudioUrl", + "resultUrl", + "url", + "downloadUrl", + "resultUrls", + ]; + + for (const key of preferredKeys) { + const url = findAudioUrlDeep(value[key]); + if (url) return url; + } + + for (const item of Object.values(value)) { + const url = findAudioUrlDeep(item); + if (url) return url; + } + } + + return null; +} + +function findKieAudioUrl(recordData: any): string | null { + const resultJson = parseKieResultJson(recordData); + const candidates = [ + recordData?.data?.response, + recordData?.data, + resultJson, + ...(Array.isArray(recordData?.data?.response) ? recordData.data.response : []), + ...(Array.isArray(recordData?.data?.data) ? recordData.data.data : []), + ...(Array.isArray(resultJson?.data) ? resultJson.data : []), + ...(Array.isArray(resultJson?.result) ? resultJson.result : []), + ]; + + for (const item of candidates) { + const url = findAudioUrlDeep(item); + if (url) { + return url; + } + } + + return null; +} + /** * Validate a path segment to prevent path traversal / SSRF. * Returns true if safe, false if it contains traversal sequences. @@ -321,6 +420,100 @@ async function handlePlayHtSpeech(providerConfig, body, modelId, token) { return audioStreamResponse(res); } +/** + * Handle Kie.ai TTS + * Kie.ai has model-specific endpoints or uses unified jobs API. + */ +async function handleKieAudioSpeech(providerConfig, body, modelId, token) { + const baseUrl = providerConfig.baseUrl.replace(/\/$/, ""); + + const voice = normalizeKieElevenLabsVoice(body.voice); + + const payload = { + model: modelId, + callBackUrl: getKieCallbackUrl(body), + input: { + text: body.input, + voice, + stability: typeof body.stability === "number" ? body.stability : 0.5, + similarity_boost: typeof body.similarity_boost === "number" ? body.similarity_boost : 0.75, + style: typeof body.style === "number" ? body.style : 0, + speed: typeof body.speed === "number" ? body.speed : 1, + timestamps: body.timestamps === true, + previous_text: body.previous_text || "", + next_text: body.next_text || "", + language_code: body.language_code || "", + }, + }; + + let data; + try { + data = await kieExecutor.createTask({ + baseUrl, + token, + payload, + }); + } catch (err: any) { + return Response.json( + { + error: { message: err?.message || "Kie audio createTask failed", code: err?.status || 502 }, + }, + { + status: Number(err?.status) || 502, + headers: { "Access-Control-Allow-Origin": getCorsOrigin() }, + } + ); + } + const taskId = data?.data?.taskId || data?.taskId; + + if (taskId) { + return pollKieAudioResult(baseUrl, modelId, taskId, token); + } + + const audioUrl = findKieAudioUrl(data); + if (typeof audioUrl === "string" && audioUrl.length > 0) { + const audioRes = await fetch(audioUrl); + return audioStreamResponse(audioRes); + } + + return errorResponse( + 502, + data?.msg || data?.message || "Kie audio generation did not return taskId or audio URL" + ); +} + +/** + * Internal polling for Kie.ai async audio tasks + */ +async function pollKieAudioResult(baseUrl, modelId, taskId, token) { + const statusUrl = kieExecutor.getTaskStatusUrl(baseUrl); + try { + const { data, state } = await kieExecutor.pollTask({ + statusUrl, + taskId: String(taskId), + token, + timeoutMs: 60000, + pollIntervalMs: 2000, + }); + + if (state === "success") { + const url = findKieAudioUrl(data); + if (url) { + const audioRes = await fetch(url); + return audioStreamResponse(audioRes); + } + return errorResponse(502, "Kie audio task completed without audio URL"); + } + } catch (err: any) { + return errorResponse( + Number(err?.status) || 504, + err?.message || "Kie audio generation timed out or failed" + ); + } + + return errorResponse(504, "Kie audio generation timed out or failed"); +} + /** * Handle Coqui TTS (local, no auth) * POST {baseUrl} with { text, speaker_id } → WAV audio @@ -457,6 +650,10 @@ export async function handleAudioSpeech({ return handlePlayHtSpeech(providerConfig, body, modelId, token); } + if (providerConfig.format === "kie-audio") { + return handleKieAudioSpeech(providerConfig, body, modelId, token); + } + if (providerConfig.format === "coqui") { return handleCoquiSpeech(providerConfig, body); } diff --git a/open-sse/handlers/audioTranscription.ts b/open-sse/handlers/audioTranscription.ts index 0ece148e23..9a6a9b2e87 100644 --- a/open-sse/handlers/audioTranscription.ts +++ b/open-sse/handlers/audioTranscription.ts @@ -1,4 +1,5 @@ import { getCorsOrigin } from "../utils/cors.ts"; +import { Buffer } from "node:buffer"; /** * Audio Transcription Handler * @@ -19,6 +20,7 @@ import { type AudioProvider, } from "../config/audioRegistry.ts"; import { buildAuthHeaders } from "../config/registryUtils.ts"; +import { kieExecutor } from "../executors/kie.ts"; import { errorResponse } from "../utils/error.ts"; type TranscriptionCredentials = { @@ -284,6 +286,89 @@ async function handleHuggingFaceTranscription(providerConfig, file, modelId, tok return Response.json({ text }, { headers: { "Access-Control-Allow-Origin": getCorsOrigin() } }); } +/** + * Handle Kie.ai transcription + */ +async function handleKieAudioTranscription(providerConfig, file, modelId, token) { + const baseUrl = providerConfig.baseUrl.replace(/\/$/, ""); + const fileBuffer = await file.arrayBuffer(); + const fileBase64 = Buffer.from(fileBuffer).toString("base64"); + let data; + try { + data = await kieExecutor.createTask({ + baseUrl, + token, + payload: { + model: modelId, + input: { + file_name: getUploadedFileName(file), + file_base64: fileBase64, + }, + }, + }); + } catch (err: any) { + return Response.json( + { + error: { + message: err?.message || "Kie transcription createTask failed", + code: err?.status || 502, + }, + }, + { + status: Number(err?.status) || 502, + headers: { "Access-Control-Allow-Origin": getCorsOrigin() }, + } + ); + } + const taskId = data?.data?.taskId || data?.taskId; + + if (taskId) { + return pollKieTranscriptionResult(baseUrl, modelId, taskId, token); + } + + return Response.json( + { text: data?.data?.text || data?.text || "" }, + { headers: { "Access-Control-Allow-Origin": getCorsOrigin() } } + ); +} + +/** + * Internal polling for Kie.ai async transcription tasks + */ +async function pollKieTranscriptionResult(baseUrl, modelId, taskId, token) { + void modelId; + const statusUrl = kieExecutor.getTaskStatusUrl(baseUrl); + try { + const { data, state } = await kieExecutor.pollTask({ + statusUrl, + taskId: String(taskId), + token, + timeoutMs: 120000, + pollIntervalMs: 2000, + }); + + if (state === "success") { + const text = + data?.data?.response?.text || + data?.data?.resultText || + data?.data?.text || + data?.text || + ""; + return Response.json( + { text }, + { headers: { "Access-Control-Allow-Origin": getCorsOrigin() } } + ); + } + } catch (err: any) { + return errorResponse( + Number(err?.status) || 504, + err?.message || "Kie transcription generation timed out or failed" + ); + } + + return errorResponse(504, "Kie transcription generation timed out or failed"); +} + /** * Handle audio transcription request * @@ -354,6 +439,10 @@ export async function handleAudioTranscription({ return handleHuggingFaceTranscription(providerConfig, file, modelId, token); } + if (providerConfig.format === "kie-audio") { + return handleKieAudioTranscription(providerConfig, file, modelId, token); + } + // Default: OpenAI/Groq/Qwen3-compatible multipart proxy const upstreamForm = new FormData(); upstreamForm.append("file", file, getUploadedFileName(file)); diff --git a/open-sse/handlers/imageGeneration.ts b/open-sse/handlers/imageGeneration.ts index fb14b6540c..03a8e45ecd 100644 --- a/open-sse/handlers/imageGeneration.ts +++ b/open-sse/handlers/imageGeneration.ts @@ -17,6 +17,7 @@ import { randomUUID } from "crypto"; */ import { getImageProvider, parseImageModel } from "../config/imageRegistry.ts"; +import { kieExecutor } from "../executors/kie.ts"; import { mapImageSize } from "../translator/image/sizeMapper.ts"; import { saveCallLog } from "@/lib/usageDb"; import { sleep } from "../utils/sleep.ts"; @@ -299,6 +300,46 @@ export async function handleImageGeneration({ body, credentials, log, resolvedPr return handleOpenAIImageGeneration({ model, provider, providerConfig, body, credentials, log }); } +function normalizeKieImageResult(recordData: any): string[] { + let resultJson: Record = {}; + try { + resultJson = + typeof recordData?.data?.resultJson === "string" + ? JSON.parse(recordData.data.resultJson) + : recordData?.data?.resultJson || {}; + } catch { + resultJson = {}; + } + + const urls = new Set(); + + const add = (val: any) => { + if (typeof val === "string" && val.startsWith("http")) urls.add(val); + if (Array.isArray(val)) { + val.forEach((v) => { + if (typeof v === "string" && v.startsWith("http")) urls.add(v); + }); + } + }; + + // Check resultJson (common in Market API) + add(resultJson?.resultUrls); + add(resultJson?.imageUrls); + add(resultJson?.resultUrl); + add(resultJson?.imageUrl); + + // Check data.response (common in 4o-image API) + add(recordData?.data?.response?.resultUrls); + add(recordData?.data?.response?.resultUrl); + + // Check direct data fields + add(recordData?.data?.resultImageUrls); + add(recordData?.data?.resultImageUrl); + add(recordData?.data?.url); + + return Array.from(urls); +} + async function handleKieImageGeneration({ model, provider, @@ -312,58 +353,88 @@ async function handleKieImageGeneration({ const timeoutMs = normalizePositiveNumber(body.timeout_ms, 300000); const pollIntervalMs = normalizePositiveNumber(body.poll_interval_ms, 2500); - const payload = { - prompt: body.prompt, - image_size: mapImageSize(body.size, "1:1"), - num_images: body.n || 1, - }; + // Check if model is a Market model (unified API) + const fullRegistry = getImageProvider(provider); + const modelEntry = fullRegistry?.models?.find((m: any) => m.id === model); + const isMarket = modelEntry?.isMarket || model.includes("/"); + + const { imageUrl } = extractImageInputs(body); + let baseUrl = ""; + let payload: any = {}; + + if (isMarket) { + // Unified Market API endpoint + baseUrl = `${providerConfig.baseUrl.replace(/\/$/, "")}/api/v1/jobs/createTask`; + // Strip category prefix (e.g., "gpt/gpt-image-2" -> "gpt-image-2") + const marketModelId = model.includes("/") ? model.split("/").pop() : model; + payload = { + model: marketModelId, + input: { + prompt: body.prompt, + aspect_ratio: mapImageSize(body.size, "1:1"), + }, + }; + if (imageUrl) { + payload.input.image_url = imageUrl; + } + } else { + // Legacy/Direct endpoint + const modelPath = model.replace("-t2i", "").replace("-i2i", ""); + baseUrl = providerConfig.baseUrl.includes(model) + ? providerConfig.baseUrl + : `https://api.kie.ai/api/v1/${modelPath}/generate`; + + payload = { + prompt: body.prompt, + image_size: mapImageSize(body.size, "1:1"), + num_images: body.n || 1, + }; + } if (log) { const promptPreview = String(body.prompt ?? "").slice(0, 60); - log.info("IMAGE", `${provider}/${model} (kie-image) | prompt: "${promptPreview}..."`); + log.info( + "IMAGE", + `${provider}/${model} (${isMarket ? "market" : "direct"}) | prompt: "${promptPreview}..."` + ); } try { - const createRes = await fetch(providerConfig.baseUrl, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), + const endpoint = isMarket ? "/api/v1/jobs/createTask" : new URL(baseUrl).pathname; + const createBaseUrl = isMarket ? providerConfig.baseUrl : baseUrl.replace(endpoint, ""); + const createData = await kieExecutor.createTask({ + baseUrl: createBaseUrl, + token, + payload, + endpoint, }); - - if (!createRes.ok) { - const errorText = await createRes.text(); - return saveImageErrorResult({ - provider, - model, - status: createRes.status, - startTime, - error: errorText, - requestBody: payload, - }); - } - - const createData = await createRes.json(); const taskId = createData?.data?.taskId || createData?.taskId; + if (!taskId) { + const errorMessage = + createData?.msg || + createData?.message || + createData?.error || + "KIE image generation did not return taskId"; + if (log) { + log.error("IMAGE", `KIE createTask failed: ${JSON.stringify(createData)}`); + } return saveImageErrorResult({ provider, model, status: 502, startTime, - error: "KIE image generation did not return taskId", + error: errorMessage, requestBody: payload, }); } // Use statusUrl from providerConfig if available, fallback to dynamic derivation - const statusUrl = - providerConfig.statusUrl || - providerConfig.baseUrl - .replace(/\/generate$/, "/record-info") - .replace("/api/v1/gpt4o-image/generate", "/api/v1/gpt4o-image/record-info"); + const statusUrl = isMarket + ? `${providerConfig.baseUrl.replace(/\/$/, "")}/api/v1/jobs/recordInfo` + : providerConfig.statusUrl && !providerConfig.statusUrl.includes("jobs/recordInfo") + ? providerConfig.statusUrl + : baseUrl.replace(/\/generate$/, "/record-info"); const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { @@ -391,18 +462,20 @@ async function handleKieImageGeneration({ const recordData = await recordRes.json(); const state = String( - recordData?.data?.status ?? recordData?.data?.successFlag ?? recordData?.msg ?? "PENDING" + recordData?.data?.status ?? + recordData?.data?.state ?? + recordData?.data?.successFlag ?? + recordData?.msg ?? + "PENDING" ).toUpperCase(); if (state === "SUCCESS" || state === "1" || state === "FINISHED") { - const urls = Array.isArray(recordData?.data?.response?.resultUrls) - ? recordData.data.response.resultUrls - : Array.isArray(recordData?.data?.resultImageUrls) - ? recordData.data.resultImageUrls - : []; - const images = urls - .filter((url: unknown) => typeof url === "string" && url.length > 0) - .map((url: unknown) => ({ url: url as string, revised_prompt: body.prompt })); + if (log) { + log.info("IMAGE", `KIE poll success for task ${taskId}`); + } + const urls = normalizeKieImageResult(recordData); + const images = urls.map((url: string) => ({ url, revised_prompt: body.prompt })); + return saveImageSuccessResult({ provider, model, @@ -430,6 +503,11 @@ async function handleKieImageGeneration({ recordData?.data?.failMsg || recordData?.msg || `KIE image task failed with status: ${state}`; + + if (log) { + log.error("IMAGE", `KIE poll failed for task ${taskId}: ${JSON.stringify(recordData)}`); + } + return saveImageErrorResult({ provider, model, diff --git a/open-sse/handlers/musicGeneration.ts b/open-sse/handlers/musicGeneration.ts index a8092d88c3..0dc197231d 100644 --- a/open-sse/handlers/musicGeneration.ts +++ b/open-sse/handlers/musicGeneration.ts @@ -15,6 +15,7 @@ */ import { getMusicProvider, parseMusicModel } from "../config/musicRegistry.ts"; +import { kieExecutor } from "../executors/kie.ts"; import { submitComfyWorkflow, pollComfyResult, @@ -24,6 +25,63 @@ import { import { saveCallLog } from "@/lib/usageDb"; import { sleep } from "../utils/sleep.ts"; +function getKieCallbackUrl(body: any): string { + return ( + body.callBackUrl || + body.callback_url || + body.callbackUrl || + "https://omniroute.local/api/kie/callback" + ); +} + +function normalizeKieSunoModel(model: string): string { + const map: Record = { + "suno-v3.5": "V3_5", + "suno-v4.0": "V4", + }; + return map[model] || model; +} + +function parseKieResultJson(recordData: any): any { + try { + return typeof recordData?.data?.resultJson === "string" + ? JSON.parse(recordData.data.resultJson) + : recordData?.data?.resultJson || {}; + } catch { + return {}; + } +} + +function normalizeKieMusicTracks(recordData: any): any[] { + const resultJson = parseKieResultJson(recordData); + const candidates = [ + recordData?.data?.response?.sunoData, + recordData?.data?.response?.data, + recordData?.data?.data, + recordData?.data?.sunoData, + resultJson?.sunoData, + resultJson?.data, + resultJson?.result, + ]; + + for (const candidate of candidates) { + if (Array.isArray(candidate) && candidate.length > 0) { + return candidate; + } + } + + const singleUrl = + recordData?.data?.response?.audioUrl || + recordData?.data?.response?.audio_url || + recordData?.data?.resultUrl || + recordData?.data?.audio_url || + resultJson?.audioUrl || + resultJson?.audio_url || + resultJson?.url; + + return typeof singleUrl === "string" && singleUrl.length > 0 ? [{ audioUrl: singleUrl }] : []; +} + /** * Handle music generation request */ @@ -190,41 +248,64 @@ async function handleKieMusicGeneration({ const pollIntervalMs = Number(body.poll_interval_ms) > 0 ? Number(body.poll_interval_ms) : 2500; const token = credentials?.apiKey || credentials?.accessToken; const baseUrl = providerConfig.baseUrl.replace(/\/$/, ""); - const payload = { - prompt: body.prompt, - customMode: false, - instrumental: true, - model, - }; + + // Check if model is a Market model + const fullRegistry = getMusicProvider(provider); + const modelEntry = fullRegistry?.models?.find((m: any) => m.id === model); + const isMarket = modelEntry?.isMarket || model.includes("/"); + + let url = ""; + let payload: any = {}; + + if (isMarket) { + url = `${baseUrl}/api/v1/jobs/createTask`; + payload = { + model: model.includes("/") ? model.split("/").pop() : model, + callBackUrl: getKieCallbackUrl(body), + input: { + prompt: body.prompt, + instrumental: true, + }, + }; + } else { + url = `${baseUrl}/api/v1/generate`; + payload = { + prompt: body.prompt, + customMode: false, + instrumental: true, + model: normalizeKieSunoModel(model), + callBackUrl: getKieCallbackUrl(body), + }; + } if (log) { const promptPreview = String(body.prompt ?? "").slice(0, 60); - log.info("MUSIC", `${provider}/${model} (kie-music) | prompt: "${promptPreview}..."`); + log.info( + "MUSIC", + `${provider}/${model} (${isMarket ? "market" : "direct"}) | prompt: "${promptPreview}..."` + ); } try { - const createRes = await fetch(`${baseUrl}/api/v1/generate`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - }); - - if (!createRes.ok) { - const errorText = await createRes.text(); - return { success: false, status: createRes.status, error: errorText }; - } - - const createData = await createRes.json(); + const endpoint = new URL(url).pathname; + const createData = await kieExecutor.createTask({ baseUrl, token, payload, endpoint }); const taskId = createData?.data?.taskId || createData?.taskId; if (!taskId) { - return { success: false, status: 502, error: "KIE music generation did not return taskId" }; + const errorMessage = + createData?.msg || + createData?.message || + createData?.error || + "KIE music generation did not return taskId"; + if (log) { + log.error("MUSIC", `KIE createTask failed: ${JSON.stringify(createData)}`); + } + return { success: false, status: 502, error: errorMessage }; } const deadline = Date.now() + timeoutMs; - const statusUrl = providerConfig.statusUrl || `${baseUrl}/api/v1/generate/record-info`; + const statusUrl = isMarket + ? `${baseUrl}/api/v1/jobs/recordInfo` + : providerConfig.statusUrl || `${baseUrl}/api/v1/generate/record-info`; while (Date.now() < deadline) { const pollUrl = new URL(statusUrl); @@ -243,16 +324,22 @@ async function handleKieMusicGeneration({ } const recordData = await recordRes.json(); - const state = String(recordData?.data?.status || recordData?.msg || "PENDING").toUpperCase(); + const state = String( + recordData?.data?.status ?? + recordData?.data?.state ?? + recordData?.data?.successFlag ?? + recordData?.msg ?? + "PENDING" + ).toUpperCase(); if (state === "SUCCESS" || state === "1" || state === "FINISHED") { - const tracks = Array.isArray(recordData?.data?.response?.sunoData) - ? recordData.data.response.sunoData - : []; + const tracks = normalizeKieMusicTracks(recordData); + const audioFiles = tracks - .map((track: unknown) => { - const t = track as Record; - return (typeof t?.audioUrl === "string" ? t.audioUrl : t?.url) as string; + .map((track: any) => { + return ( + typeof track?.audioUrl === "string" ? track.audioUrl : track?.audio_url || track?.url + ) as string; }) .filter((url: string) => typeof url === "string" && url.length > 0) .map((url: string) => ({ url, format: "mp3" })); diff --git a/open-sse/handlers/videoGeneration.ts b/open-sse/handlers/videoGeneration.ts index 98b05e9208..6ee997f275 100644 --- a/open-sse/handlers/videoGeneration.ts +++ b/open-sse/handlers/videoGeneration.ts @@ -16,6 +16,7 @@ */ import { getVideoProvider, parseVideoModel } from "../config/videoRegistry.ts"; +import { kieExecutor } from "../executors/kie.ts"; import { submitComfyWorkflow, pollComfyResult, @@ -311,8 +312,11 @@ async function handleKieVideoGeneration({ const token = credentials?.apiKey || credentials?.accessToken; const baseUrl = providerConfig.baseUrl.replace(/\/$/, ""); + // Strip category prefix (e.g., "veo/veo-3-1" -> "veo-3-1") + const marketModelId = model.includes("/") ? model.split("/").pop() : model; + const payload = { - model, + model: marketModelId, input: { prompt: body.prompt, duration: body.duration ? String(body.duration) : "5", @@ -327,24 +331,18 @@ async function handleKieVideoGeneration({ } try { - const createRes = await fetch(`${baseUrl}/api/v1/jobs/createTask`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - }); - - if (!createRes.ok) { - const errorText = await createRes.text(); - return { success: false, status: createRes.status, error: errorText }; - } - - const createData = await createRes.json(); + const createData = await kieExecutor.createTask({ baseUrl, token, payload }); const taskId = createData?.data?.taskId || createData?.taskId; if (!taskId) { - return { success: false, status: 502, error: "KIE video generation did not return taskId" }; + const errorMessage = + createData?.msg || + createData?.message || + createData?.error || + "KIE video generation did not return taskId"; + if (log) { + log.error("VIDEO", `KIE createTask failed: ${JSON.stringify(createData)}`); + } + return { success: false, status: 502, error: errorMessage }; } const deadline = Date.now() + timeoutMs; diff --git a/public/providers/kie.png b/public/providers/kie.png new file mode 100644 index 0000000000000000000000000000000000000000..439f8fbc65acac932765c5c72c347834ecf556db GIT binary patch literal 52959 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 Ygp!7s?rnQ;OVq$B$g0ScNSXNmAId)3X#fBK literal 0 HcmV?d00001 diff --git a/src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx b/src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx index 0a5865479e..8fea6babe8 100644 --- a/src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx +++ b/src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect } from "react"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { IMAGE_PROVIDERS } from "@omniroute/open-sse/config/imageRegistry.ts"; @@ -93,23 +93,18 @@ const PROVIDER_MODELS: Record< id: "kie", name: "KIE.AI", models: [ - { id: "kie/kling-2.6/text-to-video", name: "Kling 2.6 Text to Video" }, - { id: "kie/kling/v2-1-master-image-to-video", name: "Kling v2.1 Master I2V" }, - { id: "kie/kling/v2-1-master-text-to-video", name: "Kling v2.1 Master T2V" }, - { id: "kie/kling/v25-turbo-image-to-video-pro", name: "Kling v2.5 Turbo I2V Pro" }, - { id: "kie/kling/v25-turbo-text-to-video-pro", name: "Kling v2.5 Turbo T2V Pro" }, - { id: "kie/wan/2-6-text-to-video", name: "Wan 2.6 Text to Video" }, - { id: "kie/wan/2-6-image-to-video", name: "Wan 2.6 Image to Video" }, - { id: "kie/wan/2-7-text-to-video", name: "Wan 2.7 Text to Video" }, - { id: "kie/wan/2-7-image-to-video", name: "Wan 2.7 Image to Video" }, - { id: "kie/sora2/sora-2-text-to-video", name: "Sora 2 Text to Video" }, - { id: "kie/sora2/sora-2-image-to-video", name: "Sora 2 Image to Video" }, + { id: "kie/veo/veo-3-1", name: "Veo 3.1" }, + { id: "kie/veo/veo-3-1-fast", name: "Veo 3.1 Fast" }, + { id: "kie/kling/kling-v2-1-master-text-to-video", name: "Kling v2.1 Master T2V" }, + { id: "kie/kling/kling-v2-1-master-image-to-video", name: "Kling v2.1 Master I2V" }, + { id: "kie/kling/v2-5-turbo-text-to-video", name: "Kling v2.5 Turbo T2V" }, + { id: "kie/kling/v2-5-turbo-image-to-video", name: "Kling v2.5 Turbo I2V" }, + { id: "kie/wan/2-7-text-to-video", name: "Wan 2.7 T2V" }, + { id: "kie/wan/2-7-image-to-video", name: "Wan 2.7 I2V" }, + { id: "kie/sora2/sora-2-text-to-video", name: "Sora 2 T2V" }, { id: "kie/hailuo/02-text-to-video-pro", name: "Hailuo 02 T2V Pro" }, - { id: "kie/hailuo/02-image-to-video-pro", name: "Hailuo 02 I2V Pro" }, { id: "kie/grok-imagine/text-to-video", name: "Grok Imagine T2V" }, - { id: "kie/grok-imagine/image-to-video", name: "Grok Imagine I2V" }, - { id: "kie/bytedance/v1-pro-text-to-video", name: "Bytedance v1 Pro T2V" }, - { id: "kie/bytedance/v1-pro-image-to-video", name: "Bytedance v1 Pro I2V" }, + { id: "kie/bytedance/v2-0-text-to-video", name: "Seedance v2.0 T2V" }, ], }, { @@ -131,9 +126,8 @@ const PROVIDER_MODELS: Record< id: "kie", name: "KIE.AI", models: [ - { id: "kie/V4", name: "Suno V4" }, - { id: "kie/V4_5", name: "Suno V4.5" }, - { id: "kie/V5", name: "Suno V5" }, + { id: "kie/suno-v3.5", name: "Suno V3.5" }, + { id: "kie/suno-v4.0", name: "Suno V4.0" }, ], }, { @@ -155,6 +149,16 @@ const PROVIDER_MODELS: Record< { id: "openai/gpt-4o-mini-tts", name: "GPT-4o Mini TTS" }, ], }, + { + id: "kie", + name: "KIE.AI", + models: [ + { id: "kie/elevenlabs/text-to-speech-multilingual-v2", name: "ElevenLabs TTS v2" }, + { id: "kie/elevenlabs/text-to-speech-turbo-2-5", name: "ElevenLabs TTS Turbo 2.5" }, + { id: "kie/elevenlabs/text-to-dialogue-v3", name: "ElevenLabs Text to Dialogue v3" }, + { id: "kie/elevenlabs/sound-effect-v2", name: "ElevenLabs Sound Effect v2" }, + ], + }, { id: "elevenlabs", name: "ElevenLabs", @@ -227,6 +231,14 @@ const PROVIDER_MODELS: Record< { id: "deepgram/base", name: "Base" }, ], }, + { + id: "kie", + name: "KIE.AI", + models: [ + { id: "kie/elevenlabs/speech-to-text", name: "ElevenLabs STT" }, + { id: "kie/elevenlabs/audio-isolation", name: "ElevenLabs Audio Isolation" }, + ], + }, { id: "assemblyai", name: "AssemblyAI ($50 free)", @@ -265,6 +277,8 @@ const PROVIDER_MODELS: Record< { id: "qwen", name: "Qwen", models: [{ id: "qwen/qwen3-asr", name: "Qwen3 ASR" }] }, ], }; +const INITIAL_IMAGE_PROVIDER = PROVIDER_MODELS.image[0]; +const INITIAL_IMAGE_MODEL = INITIAL_IMAGE_PROVIDER?.models[0]; // Voice presets per TTS provider const VOICE_PRESETS: Record = { @@ -287,6 +301,13 @@ const VOICE_PRESETS: Record = { { id: "pNInz6obpgDQGcFmaJgB", label: "Adam (EN)" }, { id: "yoZ06aMxZJJ28mfd3POQ", label: "Sam (EN)" }, ], + kie: [ + { id: "Rachel", label: "Rachel (EN)" }, + { id: "Adam", label: "Adam (EN)" }, + { id: "Brian", label: "Brian (EN)" }, + { id: "Roger", label: "Roger (EN)" }, + { id: "Bella", label: "Bella (EN)" }, + ], cartesia: [ { id: "a0e99841-438c-4a64-b679-ae501e7d6091", label: "Barbershop Man" }, { id: "694f9389-aac1-45b6-b726-9d9369183238", label: "Friendly Reading Man" }, @@ -414,8 +435,10 @@ export default function MediaPageClient() { const [prompt, setPrompt] = useState(""); // Selected provider and model per modality - const [selectedProvider, setSelectedProvider] = useState(""); - const [selectedModel, setSelectedModel] = useState(""); + const [selectedProvider, setSelectedProvider] = useState( + INITIAL_IMAGE_PROVIDER?.id ?? "" + ); + const [selectedModel, setSelectedModel] = useState(INITIAL_IMAGE_MODEL?.id ?? ""); const [loading, setLoading] = useState(false); const [result, setResult] = useState(null); @@ -505,16 +528,6 @@ export default function MediaPageClient() { } }; - // Initialize on mount — pick first provider/model for image tab - const initialized = useRef(false); - if (!initialized.current) { - initialized.current = true; - const providers = PROVIDER_MODELS["image"] ?? []; - const firstProvider = providers[0]; - setSelectedProvider(firstProvider?.id ?? ""); - setSelectedModel(firstProvider?.models[0]?.id ?? ""); - } - const handleGenerate = async () => { setLoading(true); setError(null); diff --git a/src/lib/dataPaths.js b/src/lib/dataPaths.js deleted file mode 100644 index 3595429936..0000000000 --- a/src/lib/dataPaths.js +++ /dev/null @@ -1,70 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.APP_NAME = void 0; -exports.getLegacyDotDataDir = getLegacyDotDataDir; -exports.getDefaultDataDir = getDefaultDataDir; -exports.resolveDataDir = resolveDataDir; -exports.isSamePath = isSamePath; -const path_1 = __importDefault(require("path")); -const os_1 = __importDefault(require("os")); -exports.APP_NAME = "omniroute"; -function fallbackHomeDir() { - const envHome = process.env.HOME || process.env.USERPROFILE; - if (typeof envHome === "string" && envHome.trim().length > 0) { - return path_1.default.resolve(envHome); - } - return os_1.default.tmpdir(); -} -function safeHomeDir() { - try { - return os_1.default.homedir(); - } - catch { - return fallbackHomeDir(); - } -} -function normalizeConfiguredPath(dir) { - if (typeof dir !== "string") - return null; - const trimmed = dir.trim(); - if (!trimmed) - return null; - return path_1.default.resolve(trimmed); -} -function getLegacyDotDataDir() { - return path_1.default.join(safeHomeDir(), `.${exports.APP_NAME}`); -} -function getDefaultDataDir() { - const homeDir = safeHomeDir(); - if (process.platform === "win32") { - const appData = process.env.APPDATA || path_1.default.join(homeDir, "AppData", "Roaming"); - return path_1.default.join(appData, exports.APP_NAME); - } - // Support XDG on Linux/macOS when explicitly configured. - const xdgConfigHome = normalizeConfiguredPath(process.env.XDG_CONFIG_HOME); - if (xdgConfigHome) { - return path_1.default.join(xdgConfigHome, exports.APP_NAME); - } - return getLegacyDotDataDir(); -} -function resolveDataDir({ isCloud = false } = {}) { - if (isCloud) - return "/tmp"; - const configured = normalizeConfiguredPath(process.env.DATA_DIR); - if (configured) - return configured; - return getDefaultDataDir(); -} -function isSamePath(a, b) { - if (!a || !b) - return false; - const normalizedA = path_1.default.resolve(a); - const normalizedB = path_1.default.resolve(b); - if (process.platform === "win32") { - return normalizedA.toLowerCase() === normalizedB.toLowerCase(); - } - return normalizedA === normalizedB; -} diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index d1af78a9b6..67b88470f6 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -533,6 +533,55 @@ async function validateInworldProvider({ apiKey, providerSpecificData = {} }: an } } +async function validateKieProvider({ apiKey, providerSpecificData = {} }: any) { + try { + // Use credit check endpoint as requested by user based on Kie.ai docs. + const response = await validationRead("https://api.kie.ai/api/v1/chat/credit", { + method: "GET", + headers: applyCustomUserAgent( + { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + providerSpecificData + ), + }); + + if (response.ok) { + return { valid: true, error: null }; + } + + if (response.status === 401 || response.status === 403) { + return { valid: false, error: "Invalid Kie.ai API key" }; + } + + // Fallback: if credits endpoint is 404/not supported, try minimal chat probe + const chatRes = await validationWrite("https://api.kie.ai/api/v1/chat/completions", { + method: "POST", + headers: buildBearerHeaders(apiKey, providerSpecificData), + body: JSON.stringify({ + model: providerSpecificData.validationModelId || "gpt-4o-mini", + messages: [{ role: "user", content: "test" }], + max_tokens: 1, + }), + }); + + if ( + chatRes.ok || + (chatRes.status >= 400 && + chatRes.status < 500 && + chatRes.status !== 401 && + chatRes.status !== 403) + ) { + return { valid: true, error: null }; + } + + return { valid: false, error: `Validation failed: ${chatRes.status}` }; + } catch (error: any) { + return toValidationErrorResult(error); + } +} + async function validateBailianCodingPlanProvider({ apiKey, providerSpecificData = {} }: any) { try { const rawBaseUrl = @@ -1285,6 +1334,7 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi nanobanana: validateNanoBananaProvider, elevenlabs: validateElevenLabsProvider, inworld: validateInworldProvider, + kie: validateKieProvider, "bailian-coding-plan": validateBailianCodingPlanProvider, heroku: validateHerokuProvider, databricks: validateDatabricksProvider, diff --git a/src/shared/components/ProviderIcon.tsx b/src/shared/components/ProviderIcon.tsx index ef561fb77d..d15d0aceb9 100644 --- a/src/shared/components/ProviderIcon.tsx +++ b/src/shared/components/ProviderIcon.tsx @@ -153,6 +153,7 @@ const KNOWN_PNGS = new Set([ "glmt", "groq", "ironclaw", + "kie", "kilo-gateway", "kilocode", "kimi-coding-apikey", diff --git a/src/shared/utils/nodeRuntimeSupport.ts b/src/shared/utils/nodeRuntimeSupport.ts index 98daa345d0..21ba8c9640 100644 --- a/src/shared/utils/nodeRuntimeSupport.ts +++ b/src/shared/utils/nodeRuntimeSupport.ts @@ -11,6 +11,7 @@ export const SECURE_NODE_LINES = Object.freeze([ Object.freeze({ major: 20, minor: 20, patch: 2 }), Object.freeze({ major: 22, minor: 22, patch: 2 }), Object.freeze({ major: 24, minor: 0, patch: 0 }), + Object.freeze({ major: 25, minor: 0, patch: 0 }), ]); export const RECOMMENDED_NODE_VERSION = "24.14.1";