mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat: add kie media provider support
This commit is contained in:
@@ -100,6 +100,18 @@ export const AUDIO_TRANSCRIPTION_PROVIDERS: Record<string, AudioProvider> = {
|
||||
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<string, AudioProvider> = {
|
||||
@@ -246,6 +258,20 @@ export const AUDIO_SPEECH_PROVIDERS: Record<string, AudioProvider> = {
|
||||
{ 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" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<string, ImageProviderConfig> = {
|
||||
|
||||
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"],
|
||||
},
|
||||
|
||||
|
||||
@@ -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<string, MusicProvider> = {
|
||||
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" },
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@@ -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<string, VideoProvider> = {
|
||||
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 },
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
140
open-sse/executors/kie.ts
Normal file
140
open-sse/executors/kie.ts
Normal file
@@ -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<any> {
|
||||
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<KieTaskRecord> {
|
||||
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();
|
||||
@@ -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<string, string> = {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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<string, unknown> = {};
|
||||
try {
|
||||
resultJson =
|
||||
typeof recordData?.data?.resultJson === "string"
|
||||
? JSON.parse(recordData.data.resultJson)
|
||||
: recordData?.data?.resultJson || {};
|
||||
} catch {
|
||||
resultJson = {};
|
||||
}
|
||||
|
||||
const urls = new Set<string>();
|
||||
|
||||
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,
|
||||
|
||||
@@ -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<string, string> = {
|
||||
"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<string, unknown>;
|
||||
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" }));
|
||||
|
||||
@@ -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;
|
||||
|
||||
BIN
public/providers/kie.png
Normal file
BIN
public/providers/kie.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -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<string, { id: string; label: string }[]> = {
|
||||
@@ -287,6 +301,13 @@ const VOICE_PRESETS: Record<string, { id: string; label: string }[]> = {
|
||||
{ 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<string>("");
|
||||
const [selectedModel, setSelectedModel] = useState<string>("");
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>(
|
||||
INITIAL_IMAGE_PROVIDER?.id ?? ""
|
||||
);
|
||||
const [selectedModel, setSelectedModel] = useState<string>(INITIAL_IMAGE_MODEL?.id ?? "");
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [result, setResult] = useState<GenerationResult | null>(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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -153,6 +153,7 @@ const KNOWN_PNGS = new Set([
|
||||
"glmt",
|
||||
"groq",
|
||||
"ironclaw",
|
||||
"kie",
|
||||
"kilo-gateway",
|
||||
"kilocode",
|
||||
"kimi-coding-apikey",
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user