接入grsai

This commit is contained in:
zhishi 2026-02-28 20:12:04 +08:00
parent 0b1661a940
commit bc36f6599e
15 changed files with 306 additions and 58 deletions

View File

@ -644,6 +644,11 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
{ manufacturer: "vidu", model: "viduq2", grid: 0, type: "ti2i" },
{ manufacturer: "runninghub", model: "nanobanana", grid: 1, type: "ti2i" },
{ manufacturer: "modelScope", model: "Qwen/Qwen-Image", grid: 1, type: "ti2i" },
{ manufacturer: "grsai", model: "nano-banana-fast", grid: 1, type: "ti2i" },
{ manufacturer: "grsai", model: "nano-banana-pro", grid: 1, type: "ti2i" },
{ manufacturer: "grsai", model: "nano-banana", grid: 1, type: "ti2i" },
{ manufacturer: "grsai", model: "nano-banana-2", grid: 1, type: "ti2i" },
]);
},
},
@ -1103,6 +1108,67 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
audio: 0,
type: JSON.stringify(["singleImage", "text"]),
},
{
id: 48,
manufacturer: "grsai",
model: "sora-2",
durationResolutionMap: JSON.stringify([{ duration: [10, 15], resolution: [] }]),
aspectRatio: JSON.stringify(["16:9", "9:16"]),
audio: 0,
type: JSON.stringify(["singleImage", "text"]),
},
{
id: 49,
manufacturer: "grsai",
model: "veo3.1-pro",
durationResolutionMap: JSON.stringify([]),
aspectRatio: JSON.stringify(["16:9", "9:16"]),
audio: 0,
type: JSON.stringify(["startEndRequired", "text"]),
},
{
id: 50,
manufacturer: "grsai",
model: "veo3.1-pro-1080p",
durationResolutionMap: JSON.stringify([]),
aspectRatio: JSON.stringify(["16:9", "9:16"]),
audio: 0,
type: JSON.stringify(["startEndRequired", "text"]),
},{
id: 51,
manufacturer: "grsai",
model: "veo3.1-pro-4k",
durationResolutionMap: JSON.stringify([]),
aspectRatio: JSON.stringify(["16:9", "9:16"]),
audio: 0,
type: JSON.stringify(["startEndRequired", "text"]),
},{
id: 52,
manufacturer: "grsai",
model: "veo3.1-fast",
durationResolutionMap: JSON.stringify([]),
aspectRatio: JSON.stringify(["16:9", "9:16"]),
audio: 0,
type: JSON.stringify(["startEndRequired", "text"]),
},
{
id: 53,
manufacturer: "grsai",
model: "veo3.1-fast-1080p",
durationResolutionMap: JSON.stringify([]),
aspectRatio: JSON.stringify(["16:9", "9:16"]),
audio: 0,
type: JSON.stringify(["startEndRequired", "text"]),
},{
id: 54,
manufacturer: "grsai",
model: "veo3.1-fast-4k",
durationResolutionMap: JSON.stringify([]),
aspectRatio: JSON.stringify(["16:9", "9:16"]),
audio: 0,
type: JSON.stringify(["startEndRequired", "text"]),
},
]);
},
},

View File

@ -1,4 +1,4 @@
// @routes-hash e13311bd461bd8de8701383106557048
// @routes-hash c97cf72361299980ea4b0c43549a0de8
import { Express } from "express";
import route1 from "./routes/assets/addAssets";
@ -69,19 +69,20 @@ import route65 from "./routes/storyboard/uploadImage";
import route66 from "./routes/task/getTaskApi";
import route67 from "./routes/task/taskDetails";
import route68 from "./routes/user/getUser";
import route69 from "./routes/video/addVideo";
import route70 from "./routes/video/addVideoConfig";
import route71 from "./routes/video/deleteVideoConfig";
import route72 from "./routes/video/generatePrompt";
import route73 from "./routes/video/generateVideo";
import route74 from "./routes/video/getManufacturer";
import route75 from "./routes/video/getVideo";
import route76 from "./routes/video/getVideoConfigs";
import route77 from "./routes/video/getVideoModel";
import route78 from "./routes/video/getVideoStoryboards";
import route79 from "./routes/video/reviseVideoStoryboards";
import route80 from "./routes/video/saveVideo";
import route81 from "./routes/video/upDateVideoConfig";
import route69 from "./routes/user/saveUser";
import route70 from "./routes/video/addVideo";
import route71 from "./routes/video/addVideoConfig";
import route72 from "./routes/video/deleteVideoConfig";
import route73 from "./routes/video/generatePrompt";
import route74 from "./routes/video/generateVideo";
import route75 from "./routes/video/getManufacturer";
import route76 from "./routes/video/getVideo";
import route77 from "./routes/video/getVideoConfigs";
import route78 from "./routes/video/getVideoModel";
import route79 from "./routes/video/getVideoStoryboards";
import route80 from "./routes/video/reviseVideoStoryboards";
import route81 from "./routes/video/saveVideo";
import route82 from "./routes/video/upDateVideoConfig";
export default async (app: Express) => {
app.use("/assets/addAssets", route1);
@ -152,17 +153,18 @@ export default async (app: Express) => {
app.use("/task/getTaskApi", route66);
app.use("/task/taskDetails", route67);
app.use("/user/getUser", route68);
app.use("/video/addVideo", route69);
app.use("/video/addVideoConfig", route70);
app.use("/video/deleteVideoConfig", route71);
app.use("/video/generatePrompt", route72);
app.use("/video/generateVideo", route73);
app.use("/video/getManufacturer", route74);
app.use("/video/getVideo", route75);
app.use("/video/getVideoConfigs", route76);
app.use("/video/getVideoModel", route77);
app.use("/video/getVideoStoryboards", route78);
app.use("/video/reviseVideoStoryboards", route79);
app.use("/video/saveVideo", route80);
app.use("/video/upDateVideoConfig", route81);
app.use("/user/saveUser", route69);
app.use("/video/addVideo", route70);
app.use("/video/addVideoConfig", route71);
app.use("/video/deleteVideoConfig", route72);
app.use("/video/generatePrompt", route73);
app.use("/video/generateVideo", route74);
app.use("/video/getManufacturer", route75);
app.use("/video/getVideo", route76);
app.use("/video/getVideoConfigs", route77);
app.use("/video/getVideoModel", route78);
app.use("/video/getVideoStoryboards", route79);
app.use("/video/reviseVideoStoryboards", route80);
app.use("/video/saveVideo", route81);
app.use("/video/upDateVideoConfig", route82);
}

View File

@ -124,6 +124,7 @@ export default router.post(
assetsId: id,
});
const apiConfig = await u.getPromptAi("assetsImage");
try {
const contentStr = await u.ai.image(
{

View File

@ -48,7 +48,6 @@ export default router.post(
);
res.status(200).send(success(reply));
} catch (err) {
console.log("%c Line:51 🥟 err", "background:#e41a6a", err);
const msg = u.error(err).message;
console.error(msg);
res.status(500).send(error(msg));

View File

@ -12,7 +12,6 @@ export default router.post(
}),
async (req, res) => {
const { id } = req.body;
console.log("%c Line:15 🍕 id", "background:#f5ce50", id);
await u.db("t_assets").where("id", id).delete();
res.status(200).send(success("分镜删除成功"));
},

View File

@ -11,6 +11,7 @@ import apimart from "./owned/apimart";
import other from "./owned/other";
import gemini from "./owned/gemini";
import modelScope from "./owned/modelScope";
import grsai from "./owned/grsai";
const urlToBase64 = async (url: string): Promise<string> => {
const res = await axios.get(url, { responseType: "arraybuffer" });
@ -28,6 +29,7 @@ const modelInstance = {
// apimart: apimart,
modelScope,
other,
grsai
} as const;
export default async (input: ImageConfig, config: AIConfig) => {

View File

@ -0,0 +1,92 @@
import axios from "axios";
import u from "@/utils";
import { pollTask } from "@/utils/ai/utils";
function getApiUrl(apiUrl: string) {
if (apiUrl.includes("|")) {
const parts = apiUrl.split("|");
if (parts.length !== 2 || !parts[0].trim() || !parts[1].trim()) {
throw new Error("url 格式错误,请使用 url1|url2 格式");
}
return { requestUrl: parts[0].trim(), queryUrl: parts[1].trim() };
}
throw new Error("请填写正确的url");
}
function template(replaceObj: Record<string, any>, url: string) {
return url.replace(/\{(\w+)\}/g, (match, varName) => {
return replaceObj.hasOwnProperty(varName) ? replaceObj[varName] : match;
});
}
async function processImages(imageBase64: string[]): Promise<Array<string>> {
let images = imageBase64.filter((img) => img?.trim());
if (images.length === 0) return [];
// 压缩所有图片到10MB以内
images = await Promise.all(images.map((img) => u.imageTools.compressImage(img, "10mb")));
// 参考主体数量和参考图片数量之和不得超过10
if (images.length > 6) {
const mergeImageList = images.splice(5);
const mergedImage = await u.imageTools.mergeImages(mergeImageList, "10mb");
images.push(mergedImage);
}
return images;
}
export default async (input: ImageConfig, config: AIConfig): Promise<string> => {
if (!config.apiKey) throw new Error("缺少API Key");
const apiKey = config.apiKey.replace("Bearer ", "");
const defaultBaseURL = "https://grsai.dakka.com.cn/v1/draw/nano-banana|https://grsai.dakka.com.cn/v1/draw/result";
const { requestUrl, queryUrl } = getApiUrl(config.baseURL! || defaultBaseURL);
// 构建完整的提示词
const fullPrompt = input.systemPrompt ? `${input.systemPrompt}\n\n${input.prompt}` : input.prompt;
let mergedImage = await processImages(input.imageBase64 || []);
const taskBody: Record<string, any> = {
model: config.model,
prompt: fullPrompt,
imageSize: input.size,
aspectRatio: input.aspectRatio,
...(mergedImage && mergedImage.length ? { urls: mergedImage } : {}),
webHook: "-1",
};
try {
const { data } = await axios.post(requestUrl, taskBody, { headers: { Authorization: `Bearer ${apiKey}` } });
if (data.code != 0) throw new Error(`任务提交失败: ${data ? JSON.stringify(data, null, 2) : "未知错误"}`);
return await pollTask(async () => {
const { data: queryData } = await axios.post(
queryUrl,
{
id: data.data.id,
},
{
headers: { Authorization: `Bearer ${apiKey}` },
},
);
if (queryData.code != 0) throw new Error(`查询任务失败: ${queryData ? JSON.stringify(queryData, null, 2) : "未知错误"}`);
const { status, results, error, failure_reason } = queryData.data || {};
if (status === "failed") {
return { completed: false, error: failure_reason + "\n" + error || "图片生成失败" };
}
if (status === "succeeded") {
return { completed: true, url: results?.[0].url };
}
return { completed: false };
});
} catch (error: any) {
const msg = u.error(error).message || "图片生成失败";
throw new Error(msg);
}
};

View File

@ -119,7 +119,6 @@ export default async (input: ImageConfig, config: AIConfig): Promise<string> =>
return { completed: false };
});
} catch (error: any) {
console.error("%c Line:90 🥪 error", "background:#93c0a4", error.response?.data?.errors?.message);
const msg = u.error(error).message || "图片生成失败";
throw new Error(msg);
}

View File

@ -68,11 +68,12 @@ export default async (input: ImageConfig, config: AIConfig): Promise<string> =>
const apiKey = config.apiKey.replace("Bearer ", "");
const baseURL = "https://www.runninghub.cn";
const imageUrls = await Promise.all(input.imageBase64.map((base64Image) => uploadBase64ToRunninghub(base64Image, apiKey, baseURL)));
const fullPrompt = input.systemPrompt ? `${input.systemPrompt}\n\n${input.prompt}` : input.prompt;
const endpoint = input.imageBase64.length === 0 ? "/openapi/v2/rhart-image-n-pro/text-to-image" : "/openapi/v2/rhart-image-n-pro/edit";
const taskRes = await axios.post(
`https://www.runninghub.cn${endpoint}`,
{ prompt: input.prompt, resolution: input.size, aspectRatio: input.aspectRatio, ...(imageUrls.length > 0 && { imageUrls }) },
{ prompt: fullPrompt, resolution: input.size, aspectRatio: input.aspectRatio, ...(imageUrls.length > 0 && { imageUrls }) },
{ headers: { Authorization: "Bearer " + apiKey } },
);
const taskId = taskRes.data.taskId;

View File

@ -18,9 +18,11 @@ export default async (input: ImageConfig, config: AIConfig): Promise<string> =>
"4K": "2304x4096",
},
};
const fullPrompt = input.systemPrompt ? `${input.systemPrompt}\n\n${input.prompt}` : input.prompt;
const body: Record<string, any> = {
model: config.model,
prompt: input.prompt,
prompt: fullPrompt,
size: sizeMap[input.aspectRatio][size],
response_format: "url",
sequential_image_generation: "disabled",

View File

@ -30,7 +30,7 @@ const buildOptions = async (input: AIInput<any>, config: AIConfig = {}) => {
if (manufacturer == "other") {
owned = modelList.find((m) => m.manufacturer === manufacturer);
} else {
owned = modelList.find((m) => m.model === model);
owned = modelList.find((m) => m.model === model && m.manufacturer === manufacturer);
if (!owned) owned = modelList.find((m) => m.manufacturer === manufacturer);
}
if (!owned) throw new Error("不支持的厂商");
@ -54,7 +54,7 @@ const buildOptions = async (input: AIInput<any>, config: AIConfig = {}) => {
};
const output = input.output ? (outputBuilders[owned.responseFormat]?.(input.output) ?? null) : null;
const chatModelManufacturer = ["volcengine", "other", "openai", "modelScope"];
const chatModelManufacturer = ["volcengine", "other", "openai", "modelScope","grsai"];
const modelFn = chatModelManufacturer.includes(owned.manufacturer) ? (modelInstance as OpenAIProvider).chat(model!) : modelInstance(model!);
return {

View File

@ -30,13 +30,12 @@ const instanceMap = {
openai: createOpenAI,
zhipu: createZhipu,
qwen: createQwen,
gemini: createGoogleGenerativeAI,
anthropic: createAnthropic,
modelScope: (options: OpenAIProviderSettings) => createOpenAI({ ...options, headers: { ...options?.headers, "X-ModelScope-Async-Mode": "true" } }),
xai: createXai,
other: createOpenAI,
grsai:createOpenAI
};
const modelList: Owned[] = [
// DeepSeek

View File

@ -11,7 +11,7 @@ import runninghub from "./owned/runninghub";
import gemini from "./owned/gemini";
import apimart from "./owned/apimart";
import other from "./owned/other";
import grsai from "./owned/grsai";
const modelInstance = {
volcengine: volcengine,
kling: kling,
@ -20,7 +20,8 @@ const modelInstance = {
gemini: gemini,
runninghub: runninghub,
apimart: apimart,
// other: other,
other: other,
grsai:grsai
} as const;
export default async (input: VideoConfig, config?: AIConfig) => {

View File

@ -0,0 +1,76 @@
import "../type";
import fs from "fs";
import path from "path";
import axios from "axios";
import { pollTask, validateVideoConfig } from "@/utils/ai/utils";
const buildInlineImage = (data: string) => ({ inlineData: { mimeType: "image/png", data } });
export default async (input: VideoConfig, config: AIConfig) => {
if (!config.model) throw new Error("缺少Model名称");
if (!config.apiKey) throw new Error("缺少API Key");
// const { owned, images, hasStartEndType } = validateVideoConfig(input, config);
const defaultBaseUrl = ["https://grsai.dakka.com.cn/v1/video/{model}", "https://grsai.dakka.com.cn/v1/draw/result"].join("|");
const [submitUrl, queryUrl] = (config.baseURL || defaultBaseUrl).split("|");
const headers = { Authorization: "Bearer " + config.apiKey };
let inputObj: Record<string, any> = {};
let taskUrl = submitUrl.replace("{model}", config.model);
if (config.model.includes("veo")) {
inputObj = {
model: config.model,
prompt: input.prompt,
firstFrameUrl: "",
lastFrameUrl: "",
aspectRatio: input.aspectRatio,
webHook: "-1",
};
inputObj.firstFrameUrl = input.imageBase64?.[0] ?? "";
inputObj.lastFrameUrl = input.imageBase64?.[1] ?? "";
taskUrl = submitUrl.replace("{model}", "veo");
} else {
inputObj = {
model: config.model,
prompt: input.prompt,
url: "",
aspectRatio: input.aspectRatio,
duration: +input.duration,
webHook: "-1",
};
inputObj.url = input.imageBase64?.[0] ?? "";
taskUrl = submitUrl.replace("{model}", "sora-video");
}
const { data } = await axios.post(taskUrl, { ...inputObj }, { headers: { ...headers, "Content-Type": "application/json" } });
if (data.code != 0) throw new Error(`任务提交失败: ${data ? JSON.stringify(data, null, 2) : "未知错误"}`);
return await pollTask(async () => {
const { data: queryData } = await axios.post(
queryUrl,
{
id: data.data.id,
},
{
headers: { Authorization: `Bearer ${config.apiKey}` },
},
);
if (queryData.code != 0) throw new Error(`查询任务失败: ${queryData ? JSON.stringify(queryData, null, 2) : "未知错误"}`);
const { status, error, failure_reason, url } = queryData.data || {};
if (status === "failed") {
return { completed: false, error: failure_reason + "\n" + error || "图片生成失败" };
}
if (status === "succeeded") {
return { completed: true, url: url };
}
return { completed: false };
});
};

View File

@ -4,12 +4,11 @@ import sharp from "sharp";
import FormData from "form-data";
import { pollTask, validateVideoConfig } from "@/utils/ai/utils";
import { createOpenAI } from "@ai-sdk/openai";
import { experimental_generateVideo as generateVideo } from "ai";
export default async (input: VideoConfig, config: AIConfig) => {
if (!config.apiKey) throw new Error("缺少API Key");
if (!config.baseURL) throw new Error("缺少baseURL");
// const { owned, images, hasTextType } = validateVideoConfig(input, config);
const [requestUrl, queryUrl] = config.baseURL.split("|");
const authorization = `Bearer ${config.apiKey}`;
@ -21,30 +20,40 @@ export default async (input: VideoConfig, config: AIConfig) => {
// 根据 aspectRatio 设置 size
const sizeMap: Record<string, string> = {
"16:9": "1920x1080",
"9:16": "1080x1920",
"16:9": "1280x720",
"9:16": "720x1280",
};
formData.append("size", sizeMap[input.aspectRatio] || "1920x1080");
if (input.imageBase64 && input.imageBase64.length) {
const base64Data = input.imageBase64[0]!.replace(/^data:image\/\w+;base64,/, "");
const buffer = Buffer.from(base64Data, "base64");
formData.append("input_reference", buffer, { filename: "image.jpg", contentType: "image/jpeg" });
}
const { data } = await axios.post(requestUrl, formData, {
headers: { "Content-Type": "application/json", Authorization: authorization, ...formData.getHeaders() },
});
if (data.status === "FAILED") throw new Error(`任务提交失败: ${data.errorMessage || "未知错误"}`);
const taskId = data.id;
return await pollTask(async () => {
const { data } = await axios.get(`${queryUrl.replace("{id}", taskId)}`, {
headers: { Authorization: authorization },
});
if (data.status === "SUCCESS") {
return data.results?.length ? { completed: true, url: data.results[0].url } : { completed: false, error: "任务成功但未返回视频链接" };
}
if (data.status === "FAILED") return { completed: false, error: `任务失败: ${data.errorMessage || "未知错误"}` };
if (data.status === "QUEUED" || data.status === "RUNNING") return { completed: false };
return { completed: false, error: `未知状态: ${data.status}` };
});
const body = {
model: config.model,
messages: [
{
role: "user",
content: [
{
type: "text",
text: input.prompt,
},
],
},
],
};
const { data } = await axios.post(
config.baseURL,
{ ...body },
{
headers: { "Content-Type": "application/json", Authorization: authorization },
},
);
console.log("%c Line:49 🥓 data", "background:#ffdd4d", data);
if (data.status === "FAILED") throw new Error(`任务提交失败: ${data.errorMessage || "未知错误"}`);
};