diff --git a/package.json b/package.json index f8e872d..07abbcb 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@ai-sdk/deepseek": "^2.0.17", "@ai-sdk/google": "^3.0.20", "@ai-sdk/openai": "^3.0.25", + "@ai-sdk/openai-compatible": "^2.0.27", "@aigne/core": "^1.72.0", "@aigne/openai": "^0.16.16", "@langchain/core": "^1.1.15", diff --git a/src/routes/other/testImage.ts b/src/routes/other/testImage.ts index 4d42b2b..0d5bbb6 100644 --- a/src/routes/other/testImage.ts +++ b/src/routes/other/testImage.ts @@ -18,13 +18,14 @@ export default router.post( const { modelName, apiKey, baseURL, manufacturer } = req.body; try { const image = await u.ai.image({ - prompt: "生成16:9 四宫格图片,第一宫格是一只猫,第二宫格是一只狗, 第三宫格是一只老虎,第四宫格是猪。保证宫格图片标准等分", + prompt: "生成16:9 四宫格图片,第一宫格是一只猫,第二宫格是一只狗, 第三宫格是一只老虎,第四宫格是猪。保证四宫格图片标准四等分", imageBase64: [], aspectRatio: "16:9", size: "1K", }); res.status(200).send(success(image)); } catch (e: any) { + console.log("%c Line:28 🥒 e", "background:#fca650", e); return res.status(500).send(error(e?.response?.data ?? e?.message ?? "生成失败")); } diff --git a/src/utils/ai/image/index.ts b/src/utils/ai/image/index.ts index 4eac7ca..5f3560b 100644 --- a/src/utils/ai/image/index.ts +++ b/src/utils/ai/image/index.ts @@ -8,6 +8,8 @@ import kling from "./owned/kling"; import gemini from "./owned/gemini"; import vidu from "./owned/vidu"; import runninghub from "./owned/runninghub"; +import apimart from "./owned/apimart"; +import other from "./owned/other"; interface AIConfig { model?: string; apiKey?: string; @@ -27,7 +29,8 @@ const modelInstance = { kling: kling, vidu: vidu, runninghub: runninghub, - apimart: null, + apimart: apimart, + other } as const; export default async (input: ImageConfig, config?: AIConfig) => { @@ -35,11 +38,10 @@ export default async (input: ImageConfig, config?: AIConfig) => { const { model, apiKey, baseURL, manufacturer } = { ...sqlTextModelConfig, ...config }; const manufacturerFn = modelInstance[manufacturer as keyof typeof modelInstance]; if (!manufacturerFn) if (!manufacturerFn) throw new Error("不支持的图片厂商"); - const owned = modelList.find((m) => m.model === model); - if (!owned) throw new Error("不支持的模型"); + // const owned = modelList.find((m) => m.model === model); + // if (!owned) throw new Error("不支持的模型"); let imageUrl = await manufacturerFn(input, { model, apiKey, baseURL }); - console.log("%c Line:41 🍅 imageUrl", "background:#ed9ec7", imageUrl); if (!input.resType) input.resType = "b64"; if (input.resType === "b64" && imageUrl.startsWith("http")) imageUrl = await urlToBase64(imageUrl); return imageUrl; diff --git a/src/utils/ai/image/owned/apimart.ts b/src/utils/ai/image/owned/apimart.ts new file mode 100644 index 0000000..f4ca7cc --- /dev/null +++ b/src/utils/ai/image/owned/apimart.ts @@ -0,0 +1,31 @@ +import axios from "axios"; +import u from "@/utils"; +import FormData from "form-data"; +import axiosRetry from "axios-retry"; +import { OpenAIChatModel, type OpenAIChatModelOptions } from "@aigne/openai"; +import sharp from "sharp"; +import { pollTask } from "@/utils/ai/utils"; + +axiosRetry(axios, { retries: 3, retryDelay: () => 200 }); + +export default async (input: ImageConfig, config: AIConfig): Promise => { + if (!config.apiKey) throw new Error("缺少API Key"); + const apiKey = config.apiKey.replace("Bearer ", ""); + const taskRes = await axios.post( + `https://api.apimart.ai/v1/images/generations`, + { model: "gemini-3-pro-image-preview", prompt: input.prompt, size: input.aspectRatio, n: 1, resolution: input.size }, + { headers: { Authorization: apiKey } }, + ); + + if (taskRes.data.code !== 200 || !taskRes.data.data?.[0]?.task_id) throw new Error("任务创建失败: " + JSON.stringify(taskRes.data)); + + const taskId = taskRes.data.data[0].task_id; + return pollTask(async () => { + const res = await axios.get(`https://api.apimart.ai/v1/tasks/${taskId}`, { headers: { Authorization: apiKey }, params: { language: "en" } }); + if (res.data.code !== 200) return { completed: false, error: `查询失败: ${JSON.stringify(res.data)}` }; + const { status, result } = res.data.data; + if (status === "completed") return { completed: true, imageUrl: result?.images?.[0]?.url?.[0] }; + if (status === "failed" || status === "cancelled") return { completed: false, error: `任务${status}` }; + return { completed: false }; + }); +}; diff --git a/src/utils/ai/image/owned/gemini.ts b/src/utils/ai/image/owned/gemini.ts index f1c4123..c865394 100644 --- a/src/utils/ai/image/owned/gemini.ts +++ b/src/utils/ai/image/owned/gemini.ts @@ -16,13 +16,6 @@ export default async (input: ImageConfig, config: AIConfig): Promise => // 构建完整的提示词 const fullPrompt = input.systemPrompt ? `${input.systemPrompt}\n\n${input.prompt}` : input.prompt; - // 根据 size 配置映射到具体尺寸 - const sizeMap: Record = { - "1K": "1024x1024", - "2K": "2048x2048", - "4K": "4096x4096", - }; - const result = await generateText({ model: google.languageModel(config.model), prompt: fullPrompt + `请直接输出图片`, diff --git a/src/utils/ai/image/owned/other.ts b/src/utils/ai/image/owned/other.ts new file mode 100644 index 0000000..80c7d35 --- /dev/null +++ b/src/utils/ai/image/owned/other.ts @@ -0,0 +1,76 @@ +import "../type"; +import { createGoogleGenerativeAI } from "@ai-sdk/google"; +import { generateImage, generateText } from "ai"; +import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; + +export default async (input: ImageConfig, config: AIConfig): Promise => { + if (!config.model) throw new Error("缺少Model名称"); + if (!config.apiKey) throw new Error("缺少API Key"); + if (!config.baseURL) throw new Error("缺少baseUrl"); + + const apiKey = config.apiKey.replace("Bearer ", ""); + + const otherProvider = createOpenAICompatible({ + name: "xixixi", + baseURL: config.baseURL, + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }); + + // 根据 size 配置映射到具体尺寸 + const sizeMap: Record = { + "1K": "1024x1024", + "2K": "2048x2048", + "4K": "4096x4096", + }; + // 构建完整的提示词 + const fullPrompt = input.systemPrompt ? `${input.systemPrompt}\n\n${input.prompt}` : input.prompt; + const model = config.model; + if (model.includes("gemini") || model.includes("nano")) { + const result = await generateText({ + model: otherProvider.languageModel(model), + prompt: fullPrompt + `请直接输出图片`, + providerOptions: { + google: { + imageConfig: { + ...(config.model == "gemini-2.5-flash-image" + ? { aspectRatio: input.aspectRatio } + : { aspectRatio: input.aspectRatio, imageSize: input.size }), + }, + responseModalities: ["IMAGE"], + }, + }, + }); + if (result.files && result.files.length) { + let imageBase64; + for (const item of result.files) { + imageBase64 = `data:${item.mediaType};base64,${item.base64}`; + } + // 返回生成的图片 base64 + return imageBase64!; + } else { + if (!result.text) { + console.error(JSON.stringify(result.response, null, 2)); + throw new Error("图片生成失败"); + } + const match = result.text.match(/base64,([A-Za-z0-9+/=]+)/); + const base64Str = match && match[1] ? match[1] : result.text; + + // 返回生成的图片 base64 + return "data:image/jpeg;base64," + base64Str!; + } + } else { + const { image } = await generateImage({ + model: otherProvider.imageModel(model), + prompt: + input.imageBase64 && input.imageBase64.length + ? { text: fullPrompt + `请直接输出图片`, images: input.imageBase64 } + : fullPrompt + `请直接输出图片`, + aspectRatio: input.aspectRatio as "1:1" | "3:4" | "4:3" | "9:16" | "16:9", + size: sizeMap[input.size] ?? "1024x1024", + }); + + return image.base64; + } +}; diff --git a/src/utils/ai/image/owned/vidu.ts b/src/utils/ai/image/owned/vidu.ts index 9405117..6ceef26 100644 --- a/src/utils/ai/image/owned/vidu.ts +++ b/src/utils/ai/image/owned/vidu.ts @@ -21,7 +21,7 @@ export default async (input: ImageConfig, config: AIConfig): Promise => if (!config.model) throw new Error("缺少Model名称"); if (!config.apiKey) throw new Error("缺少API Key"); - const apiKey = "Token " + config.apiKey.replace(/Bearer\s+/g, "").trim(); + const apiKey = "Token " + config.apiKey.replace(/Token\s+/g, "").trim(); const viduq2Ratio = ["16:9", "9:16", "1:1", "3:4", "4:3", "21:9", "2:3", "3:2"]; const viduq1Ratio = ["16:9", "9:16", "1:1", "3:4", "4:3"]; let images: string[] = []; @@ -51,7 +51,6 @@ export default async (input: ImageConfig, config: AIConfig): Promise => else size = input.size; if (!viduq2Ratio.includes(input.aspectRatio)) throw new Error("不支持的图片比例:" + input.aspectRatio); } - console.log("%c Line:23 🍔 size", "background:#ffdd4d", size); const body: Record = { model: config.model, @@ -60,16 +59,15 @@ export default async (input: ImageConfig, config: AIConfig): Promise => resolution: size, ...(images.length && { images: images }), }; - console.log("%c Line:27 🍷 body", "background:#6ec1c2", body); + const urlObj = getApiUrl(config.baseURL!); try { const { data } = await axios.post(urlObj.requestUrl, body, { headers: { Authorization: apiKey } }); - console.log("%c Line:35 🥕 data", "background:#93c0a4", data); + const queryUrl = template({ id: data.task_id }, urlObj.queryUrl); - console.log("%c Line:53 🍋 queryUrl", "background:#465975", queryUrl); + return await pollTask(async () => { const { data: queryData } = await axios.get(queryUrl, { headers: { Authorization: apiKey } }); - console.log("%c Line:42 🍐 queryData", "background:#4fff4B", queryData); if (queryData.state !== 0) { return { completed: false, error: queryData.message || "查询任务失败" }; diff --git a/yarn.lock b/yarn.lock index a00a10f..28551f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,6 +40,14 @@ "@ai-sdk/provider" "3.0.7" "@ai-sdk/provider-utils" "4.0.13" +"@ai-sdk/openai-compatible@^2.0.27": + version "2.0.27" + resolved "https://registry.npmmirror.com/@ai-sdk/openai-compatible/-/openai-compatible-2.0.27.tgz#55c6bf3c59d71e71d9c337dbef8b764fa69e7ccd" + integrity sha512-YpAZe7OQuMkYqcM/m1BMX0xFn4QdhuL4qGo8sNaiLq1VjEeU/pPfz51rnlpCfCvYanUL5TjIZEbdclBUwLooSQ== + dependencies: + "@ai-sdk/provider" "3.0.7" + "@ai-sdk/provider-utils" "4.0.13" + "@ai-sdk/openai@^3.0.25": version "3.0.25" resolved "https://registry.npmmirror.com/@ai-sdk/openai/-/openai-3.0.25.tgz#452c8f8ed597468048569ec9476a0b5641888d2a"