Merge branch 'develop' of https://github.com/HBAI-Ltd/Toonflow-app into develop

This commit is contained in:
ACT丶流星雨 2026-02-28 22:25:43 +08:00
commit 2b13b3e55c
44 changed files with 3577 additions and 1077 deletions

View File

@ -78,8 +78,41 @@ jobs:
dist/*.zip
retention-days: 30
build-linux:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
cache: "yarn"
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build application
run: yarn build
- name: Build Linux installer
run: yarn dist:linux
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: linux-builds
path: |
dist/*.AppImage
dist/*.deb
retention-days: 30
release:
needs: [build-windows, build-macos]
needs: [build-windows, build-macos, build-linux]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
@ -99,6 +132,12 @@ jobs:
name: macos-builds
path: dist
- name: Download Linux artifacts
uses: actions/download-artifact@v4
with:
name: linux-builds
path: dist
- name: Create Release
uses: softprops/action-gh-release@v2
with:
@ -110,5 +149,7 @@ jobs:
dist/*.exe
dist/*.zip
dist/*.dmg
dist/*.AppImage
dist/*.deb
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -24,8 +24,14 @@ asar: true
win:
target:
- nsis
- portable
- target: nsis
arch:
- x64
- arm64
- target: portable
arch:
- x64
- arm64
icon: ./scripts/logo.ico
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
@ -34,22 +40,34 @@ nsis:
allowToChangeInstallationDirectory: true
perMachine: true
shortcutName: ${productName}
artifactName: ${productName}-Setup-${version}.${ext}
artifactName: ${productName}-Setup-${version}-${arch}.${ext}
installerIcon: './scripts/logo.ico'
uninstallerIcon: './scripts/logo.ico'
mac:
target:
- dmg
- zip
- target: dmg
arch:
- x64
- arm64
- target: zip
arch:
- x64
- arm64
icon: ./scripts/logo.icns
category: public.app-category.developer-tools
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
linux:
target:
- AppImage
- deb
- target: AppImage
arch:
- x64
- arm64
- target: deb
arch:
- x64
- arm64
icon: ./scripts/logo.png
category: Development
artifactName: ${productName}-${version}-${os}-${arch}.${ext}

View File

@ -1,6 +1,6 @@
{
"name": "toonflow-app",
"version": "1.0.6",
"version": "1.0.7",
"description": "Toonflow 是一款 AI 短剧漫剧工具,能够利用 AI 技术将小说自动转化为剧本,并结合 AI 生成的图片和视频,实现高效的短剧创作。",
"author": "HBAI-Ltd <ltlctools@outlook.com>",
"homepage": "https://github.com/HBAI-Ltd/Toonflow-app#readme",
@ -58,7 +58,7 @@
"jsonwebtoken": "^9.0.3",
"knex": "^3.1.0",
"morgan": "^1.10.1",
"qwen-ai-provider": "^0.1.1",
"qwen-ai-provider-v5": "^2.1.0",
"serialize-error": "^13.0.1",
"sharp": "^0.34.5",
"sqlite3": "^5.1.7",

View File

@ -1,8 +1,12 @@
import { app, BrowserWindow } from "electron";
import path from "path";
import startServe, { closeServe } from "src/app";
import { number } from "zod";
function createMainWindow(): void {
// 默认端口配置
const defaultPort = 60000;
function createMainWindow(port: any): void {
const win = new BrowserWindow({
width: 900,
height: 600,
@ -14,14 +18,28 @@ function createMainWindow(): void {
const htmlPath = isDev
? path.join(process.cwd(), "scripts", "web", "index.html")
: path.join(app.getAppPath(), "scripts", "web", "index.html");
void win.loadFile(htmlPath);
// 使用实际端口构建地址
const baseUrl = `http://localhost:${port}`;
const wsBaseUrl = `ws://localhost:${port}`;
// 构建带有 query 参数的 URL
const url = new URL(`file://${htmlPath}`);
url.searchParams.set("baseUrl", baseUrl);
url.searchParams.set("wsBaseUrl", wsBaseUrl);
console.log("%c Line:30 🥓 url", "background:#33a5ff", url.toString());
void win.loadURL(url.toString());
}
app.whenReady().then(async () => {
createMainWindow();
try {
await startServe();
const port = await startServe(false);
createMainWindow(60000);
} catch (err) {
console.error("[服务启动失败]:", err);
// 如果服务启动失败,使用默认端口创建窗口
createMainWindow(defaultPort);
}
});
@ -30,7 +48,10 @@ app.on("window-all-closed", () => {
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createMainWindow();
if (BrowserWindow.getAllWindows().length === 0) {
// 重新激活时使用默认端口
createMainWindow(defaultPort);
}
});
app.on("before-quit", async (event) => {

File diff suppressed because one or more lines are too long

View File

@ -242,11 +242,10 @@ ${sections.join("\n\n")}
const skipped: number[] = [];
for (const item of shots) {
const resultIndex = item.segmentIndex - 1;
const exists = this.shots.some((f) => f.segmentId === resultIndex);
const exists = this.shots.some((f) => f.segmentId === item.segmentIndex);
if (exists) {
skipped.push(resultIndex);
skipped.push(item.segmentIndex);
continue;
}
// 分配独立的分镜ID
@ -254,15 +253,15 @@ ${sections.join("\n\n")}
const shotId = this.shotIdCounter;
this.shots.push({
id: shotId,
segmentId: resultIndex,
segmentId: item.segmentIndex,
title: `分镜 ${shotId}`,
x: 0,
y: 0,
cells: item.prompts.map((prompt) => ({ id: u.uuid(), prompt })),
fragmentContent: this.segments[resultIndex]?.description,
fragmentContent: this.segments[item.segmentIndex - 1]?.description,
assetsTags: item.assetsTags,
});
added.push({ id: shotId, segmentIndex: resultIndex });
added.push({ id: shotId, segmentIndex: item.segmentIndex });
}
const addedInfo = added.map((a) => `分镜${a.id}(片段${a.segmentIndex})`).join(", ");

View File

@ -14,7 +14,7 @@ import jwt from "jsonwebtoken";
const app = express();
let server: ReturnType<typeof app.listen> | null = null;
export default async function startServe() {
export default async function startServe(randomPort: Boolean = false) {
if (process.env.NODE_ENV == "dev") await buildRoute();
expressWs(app);
@ -77,11 +77,14 @@ export default async function startServe() {
res.status(err.status || 500).send(err);
});
const port = parseInt(process.env.PORT || "60000");
server = app.listen(port, async () => {
const address = server?.address();
const realPort = typeof address === "string" ? address : address?.port;
console.log(`[服务启动成功]: http://localhost:${realPort}`);
const port = randomPort ? 0 : parseInt(process.env.PORT || "60000");
return await new Promise((resolve, reject) => {
server = app.listen(port, async (v) => {
const address = server?.address();
const realPort = typeof address === "string" ? address : address?.port;
console.log(`[服务启动成功]: http://localhost:${realPort}`);
resolve(realPort);
});
});
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,160 +1,170 @@
// @routes-hash 3cfad40b3c8658b442ab766a9323d740
// @routes-hash c97cf72361299980ea4b0c43549a0de8
import { Express } from "express";
import route1 from "./routes/assets/addAssets";
import route2 from "./routes/assets/delAssets";
import route3 from "./routes/assets/generateAssets";
import route4 from "./routes/assets/getAssets";
import route5 from "./routes/assets/getImage";
import route6 from "./routes/assets/getStoryboard";
import route7 from "./routes/assets/polishPrompt";
import route8 from "./routes/assets/saveAssets";
import route9 from "./routes/assets/updateAssets";
import route10 from "./routes/index/index";
import route11 from "./routes/novel/addNovel";
import route12 from "./routes/novel/delNovel";
import route13 from "./routes/novel/getNovel";
import route14 from "./routes/novel/updateNovel";
import route15 from "./routes/other/clearDatabase";
import route16 from "./routes/other/deleteAllData";
import route17 from "./routes/other/getCaptcha";
import route18 from "./routes/other/login";
import route19 from "./routes/other/testAI";
import route20 from "./routes/other/testImage";
import route21 from "./routes/other/testVideo";
import route22 from "./routes/outline/addOutline";
import route23 from "./routes/outline/agentsOutline";
import route24 from "./routes/outline/delOutline";
import route25 from "./routes/outline/getHistory";
import route26 from "./routes/outline/getOutline";
import route27 from "./routes/outline/getPartScript";
import route28 from "./routes/outline/getStoryline";
import route29 from "./routes/outline/setHistory";
import route30 from "./routes/outline/updateOutline";
import route31 from "./routes/outline/updateScript";
import route32 from "./routes/outline/updateStoryline";
import route33 from "./routes/project/addProject";
import route34 from "./routes/project/delProject";
import route35 from "./routes/project/getProject";
import route36 from "./routes/project/getProjectCount";
import route37 from "./routes/project/getSingleProject";
import route38 from "./routes/project/updateProject";
import route39 from "./routes/prompt/getPrompts";
import route40 from "./routes/prompt/updatePrompt";
import route41 from "./routes/script/generateScriptApi";
import route42 from "./routes/script/generateScriptSave";
import route43 from "./routes/script/geScriptApi";
import route44 from "./routes/setting/addModel";
import route45 from "./routes/setting/configurationModel";
import route46 from "./routes/setting/delModel";
import route47 from "./routes/setting/getAiModelMap";
import route48 from "./routes/setting/getLog";
import route49 from "./routes/setting/getSetting";
import route50 from "./routes/setting/getVideoModelList";
import route51 from "./routes/setting/updateModel";
import route52 from "./routes/setting/updeteModel";
import route53 from "./routes/storyboard/batchSuperScoreImage";
import route54 from "./routes/storyboard/chatStoryboard";
import route55 from "./routes/storyboard/generateShotImage";
import route56 from "./routes/storyboard/generateStoryboardApi";
import route57 from "./routes/storyboard/generateVideoPrompt";
import route58 from "./routes/storyboard/getStoryboard";
import route59 from "./routes/storyboard/keepStoryboard";
import route60 from "./routes/storyboard/saveStoryboard";
import route61 from "./routes/storyboard/uploadImage";
import route62 from "./routes/task/getTaskApi";
import route63 from "./routes/task/taskDetails";
import route64 from "./routes/user/getUser";
import route65 from "./routes/video/addVideo";
import route66 from "./routes/video/addVideoConfig";
import route67 from "./routes/video/deleteVideoConfig";
import route68 from "./routes/video/generatePrompt";
import route69 from "./routes/video/generateVideo";
import route70 from "./routes/video/getManufacturer";
import route71 from "./routes/video/getVideo";
import route72 from "./routes/video/getVideoConfigs";
import route73 from "./routes/video/getVideoModel";
import route74 from "./routes/video/getVideoStoryboards";
import route75 from "./routes/video/reviseVideoStoryboards";
import route76 from "./routes/video/saveVideo";
import route77 from "./routes/video/upDateVideoConfig";
import route3 from "./routes/assets/delAssetsImage";
import route4 from "./routes/assets/generateAssets";
import route5 from "./routes/assets/getAssets";
import route6 from "./routes/assets/getImage";
import route7 from "./routes/assets/getStoryboard";
import route8 from "./routes/assets/polishPrompt";
import route9 from "./routes/assets/saveAssets";
import route10 from "./routes/assets/updateAssets";
import route11 from "./routes/index/index";
import route12 from "./routes/novel/addNovel";
import route13 from "./routes/novel/delNovel";
import route14 from "./routes/novel/getNovel";
import route15 from "./routes/novel/updateNovel";
import route16 from "./routes/other/clearDatabase";
import route17 from "./routes/other/deleteAllData";
import route18 from "./routes/other/getCaptcha";
import route19 from "./routes/other/login";
import route20 from "./routes/other/testAI";
import route21 from "./routes/other/testImage";
import route22 from "./routes/other/testVideo";
import route23 from "./routes/outline/addOutline";
import route24 from "./routes/outline/agentsOutline";
import route25 from "./routes/outline/delOutline";
import route26 from "./routes/outline/getHistory";
import route27 from "./routes/outline/getOutline";
import route28 from "./routes/outline/getPartScript";
import route29 from "./routes/outline/getStoryline";
import route30 from "./routes/outline/setHistory";
import route31 from "./routes/outline/updateOutline";
import route32 from "./routes/outline/updateScript";
import route33 from "./routes/outline/updateStoryline";
import route34 from "./routes/project/addProject";
import route35 from "./routes/project/delProject";
import route36 from "./routes/project/getProject";
import route37 from "./routes/project/getProjectCount";
import route38 from "./routes/project/getSingleProject";
import route39 from "./routes/project/updateProject";
import route40 from "./routes/prompt/getPrompts";
import route41 from "./routes/prompt/updatePrompt";
import route42 from "./routes/script/generateScriptApi";
import route43 from "./routes/script/generateScriptSave";
import route44 from "./routes/script/geScriptApi";
import route45 from "./routes/setting/addModel";
import route46 from "./routes/setting/configurationModel";
import route47 from "./routes/setting/delModel";
import route48 from "./routes/setting/getAiModelList";
import route49 from "./routes/setting/getAiModelMap";
import route50 from "./routes/setting/getLog";
import route51 from "./routes/setting/getSetting";
import route52 from "./routes/setting/getVideoModelDetail";
import route53 from "./routes/setting/getVideoModelList";
import route54 from "./routes/setting/updateModel";
import route55 from "./routes/setting/updeteModel";
import route56 from "./routes/storyboard/batchSuperScoreImage";
import route57 from "./routes/storyboard/chatStoryboard";
import route58 from "./routes/storyboard/delStoryboard";
import route59 from "./routes/storyboard/generateShotImage";
import route60 from "./routes/storyboard/generateStoryboardApi";
import route61 from "./routes/storyboard/generateVideoPrompt";
import route62 from "./routes/storyboard/getStoryboard";
import route63 from "./routes/storyboard/keepStoryboard";
import route64 from "./routes/storyboard/saveStoryboard";
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/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);
app.use("/assets/delAssets", route2);
app.use("/assets/generateAssets", route3);
app.use("/assets/getAssets", route4);
app.use("/assets/getImage", route5);
app.use("/assets/getStoryboard", route6);
app.use("/assets/polishPrompt", route7);
app.use("/assets/saveAssets", route8);
app.use("/assets/updateAssets", route9);
app.use("/index", route10);
app.use("/novel/addNovel", route11);
app.use("/novel/delNovel", route12);
app.use("/novel/getNovel", route13);
app.use("/novel/updateNovel", route14);
app.use("/other/clearDatabase", route15);
app.use("/other/deleteAllData", route16);
app.use("/other/getCaptcha", route17);
app.use("/other/login", route18);
app.use("/other/testAI", route19);
app.use("/other/testImage", route20);
app.use("/other/testVideo", route21);
app.use("/outline/addOutline", route22);
app.use("/outline/agentsOutline", route23);
app.use("/outline/delOutline", route24);
app.use("/outline/getHistory", route25);
app.use("/outline/getOutline", route26);
app.use("/outline/getPartScript", route27);
app.use("/outline/getStoryline", route28);
app.use("/outline/setHistory", route29);
app.use("/outline/updateOutline", route30);
app.use("/outline/updateScript", route31);
app.use("/outline/updateStoryline", route32);
app.use("/project/addProject", route33);
app.use("/project/delProject", route34);
app.use("/project/getProject", route35);
app.use("/project/getProjectCount", route36);
app.use("/project/getSingleProject", route37);
app.use("/project/updateProject", route38);
app.use("/prompt/getPrompts", route39);
app.use("/prompt/updatePrompt", route40);
app.use("/script/generateScriptApi", route41);
app.use("/script/generateScriptSave", route42);
app.use("/script/geScriptApi", route43);
app.use("/setting/addModel", route44);
app.use("/setting/configurationModel", route45);
app.use("/setting/delModel", route46);
app.use("/setting/getAiModelMap", route47);
app.use("/setting/getLog", route48);
app.use("/setting/getSetting", route49);
app.use("/setting/getVideoModelList", route50);
app.use("/setting/updateModel", route51);
app.use("/setting/updeteModel", route52);
app.use("/storyboard/batchSuperScoreImage", route53);
app.use("/storyboard/chatStoryboard", route54);
app.use("/storyboard/generateShotImage", route55);
app.use("/storyboard/generateStoryboardApi", route56);
app.use("/storyboard/generateVideoPrompt", route57);
app.use("/storyboard/getStoryboard", route58);
app.use("/storyboard/keepStoryboard", route59);
app.use("/storyboard/saveStoryboard", route60);
app.use("/storyboard/uploadImage", route61);
app.use("/task/getTaskApi", route62);
app.use("/task/taskDetails", route63);
app.use("/user/getUser", route64);
app.use("/video/addVideo", route65);
app.use("/video/addVideoConfig", route66);
app.use("/video/deleteVideoConfig", route67);
app.use("/video/generatePrompt", route68);
app.use("/video/generateVideo", route69);
app.use("/video/getManufacturer", route70);
app.use("/video/getVideo", route71);
app.use("/video/getVideoConfigs", route72);
app.use("/video/getVideoModel", route73);
app.use("/video/getVideoStoryboards", route74);
app.use("/video/reviseVideoStoryboards", route75);
app.use("/video/saveVideo", route76);
app.use("/video/upDateVideoConfig", route77);
app.use("/assets/delAssetsImage", route3);
app.use("/assets/generateAssets", route4);
app.use("/assets/getAssets", route5);
app.use("/assets/getImage", route6);
app.use("/assets/getStoryboard", route7);
app.use("/assets/polishPrompt", route8);
app.use("/assets/saveAssets", route9);
app.use("/assets/updateAssets", route10);
app.use("/index", route11);
app.use("/novel/addNovel", route12);
app.use("/novel/delNovel", route13);
app.use("/novel/getNovel", route14);
app.use("/novel/updateNovel", route15);
app.use("/other/clearDatabase", route16);
app.use("/other/deleteAllData", route17);
app.use("/other/getCaptcha", route18);
app.use("/other/login", route19);
app.use("/other/testAI", route20);
app.use("/other/testImage", route21);
app.use("/other/testVideo", route22);
app.use("/outline/addOutline", route23);
app.use("/outline/agentsOutline", route24);
app.use("/outline/delOutline", route25);
app.use("/outline/getHistory", route26);
app.use("/outline/getOutline", route27);
app.use("/outline/getPartScript", route28);
app.use("/outline/getStoryline", route29);
app.use("/outline/setHistory", route30);
app.use("/outline/updateOutline", route31);
app.use("/outline/updateScript", route32);
app.use("/outline/updateStoryline", route33);
app.use("/project/addProject", route34);
app.use("/project/delProject", route35);
app.use("/project/getProject", route36);
app.use("/project/getProjectCount", route37);
app.use("/project/getSingleProject", route38);
app.use("/project/updateProject", route39);
app.use("/prompt/getPrompts", route40);
app.use("/prompt/updatePrompt", route41);
app.use("/script/generateScriptApi", route42);
app.use("/script/generateScriptSave", route43);
app.use("/script/geScriptApi", route44);
app.use("/setting/addModel", route45);
app.use("/setting/configurationModel", route46);
app.use("/setting/delModel", route47);
app.use("/setting/getAiModelList", route48);
app.use("/setting/getAiModelMap", route49);
app.use("/setting/getLog", route50);
app.use("/setting/getSetting", route51);
app.use("/setting/getVideoModelDetail", route52);
app.use("/setting/getVideoModelList", route53);
app.use("/setting/updateModel", route54);
app.use("/setting/updeteModel", route55);
app.use("/storyboard/batchSuperScoreImage", route56);
app.use("/storyboard/chatStoryboard", route57);
app.use("/storyboard/delStoryboard", route58);
app.use("/storyboard/generateShotImage", route59);
app.use("/storyboard/generateStoryboardApi", route60);
app.use("/storyboard/generateVideoPrompt", route61);
app.use("/storyboard/getStoryboard", route62);
app.use("/storyboard/keepStoryboard", route63);
app.use("/storyboard/saveStoryboard", route64);
app.use("/storyboard/uploadImage", route65);
app.use("/task/getTaskApi", route66);
app.use("/task/taskDetails", route67);
app.use("/user/getUser", route68);
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

@ -0,0 +1,27 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 删除资产图片
export default router.post(
"/",
validateFields({
imageId: z.number().optional(),
assetsId: z.number().optional(),
}),
async (req, res) => {
const { imageId, assetsId } = req.body;
if (assetsId) {
await u.db("t_assets").where("id", assetsId).update({
filePath: null,
});
}
if (imageId) {
await u.db("t_image").where("id", imageId).delete();
}
res.status(200).send(success({ message: "删除资产图片成功" }));
},
);

View File

@ -2,7 +2,7 @@ import express from "express";
import u from "@/utils";
import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { success } from "@/lib/responseFormat";
import { error, success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import sharp from "sharp";
const router = express.Router();
@ -125,56 +125,68 @@ export default router.post(
});
const apiConfig = await u.getPromptAi("assetsImage");
const contentStr = await u.ai.image(
{
systemPrompt,
prompt: userPrompt,
imageBase64: base64 ? [base64] : [],
size: "2K",
aspectRatio: "16:9",
},
apiConfig,
);
try {
const contentStr = await u.ai.image(
{
systemPrompt,
prompt: userPrompt,
imageBase64: base64 ? [base64] : [],
size: "2K",
aspectRatio: "16:9",
},
apiConfig,
);
let insertType;
const match = contentStr.match(/base64,([A-Za-z0-9+/=]+)/);
let buffer = Buffer.from(match && match.length >= 2 ? match[1]! : contentStr!, "base64");
let insertType;
const match = contentStr.match(/base64,([A-Za-z0-9+/=]+)/);
let buffer = Buffer.from(match && match.length >= 2 ? match[1]! : contentStr!, "base64");
if (type != "storyboard") {
//添加文本
// buffer = await imageAddText(name, buffer);
if (type != "storyboard") {
//添加文本
// buffer = await imageAddText(name, buffer);
}
let imagePath;
if (type == "role") {
insertType = "角色";
imagePath = `/${projectId}/role/${uuidv4()}.jpg`;
}
if (type == "scene") {
insertType = "场景";
imagePath = `/${projectId}/scene/${uuidv4()}.jpg`;
}
if (type == "props") {
insertType = "道具";
imagePath = `/${projectId}/props/${uuidv4()}.jpg`;
}
if (type == "storyboard") {
insertType = "分镜";
imagePath = `/${projectId}/storyboard/${uuidv4()}.jpg`;
}
await u.oss.writeFile(imagePath!, buffer);
const imageData = await u.db("t_image").where("id", imageId).select("*").first();
if (imageData) {
await u.db("t_image").where("id", imageId).update({
state: "生成成功",
filePath: imagePath,
type: insertType,
});
const path = await u.oss.getFileUrl(imagePath!);
// const state = await u.db("t_assets").where("id", id).select("state").first();
return res.status(200).send(success({ path, assetsId: id }));
} else {
return res.status(500).send("资产已被删除");
}
} catch (e) {
await u.db("t_image").where("id", imageId).update({
state: "生成失败",
});
const msg = u.error(e).message || "图片生成失败";
return res.status(400).send(error(msg));
}
let imagePath;
if (type == "role") {
insertType = "角色";
imagePath = `/${projectId}/role/${uuidv4()}.jpg`;
}
if (type == "scene") {
insertType = "场景";
imagePath = `/${projectId}/scene/${uuidv4()}.jpg`;
}
if (type == "props") {
insertType = "道具";
imagePath = `/${projectId}/props/${uuidv4()}.jpg`;
}
if (type == "storyboard") {
insertType = "分镜";
imagePath = `/${projectId}/storyboard/${uuidv4()}.jpg`;
}
await u.oss.writeFile(imagePath!, buffer);
await u.db("t_image").where("id", imageId).update({
state: "生成成功",
filePath: imagePath,
type: insertType,
});
const path = await u.oss.getFileUrl(imagePath!);
// const state = await u.db("t_assets").where("id", id).select("state").first();
res.status(200).send(success({ path, assetsId: id }));
},
);
async function imageAddText(name: string, imageBuffer: Buffer) {

View File

@ -27,6 +27,7 @@ export default router.post(
resolution: "720p",
aspectRatio: "16:9",
audio: false,
mode: "single",
},
{
model: modelName,

View File

@ -1,7 +1,7 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { error, success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { generateScript } from "@/utils/generateScript";
const router = express.Router();
@ -43,14 +43,18 @@ export default router.post(
if (novelData.length == 0) return res.status(500).send(success({ message: "原文为空" }));
const result: string = mergeNovelText(novelData);
try {
const data = await generateScript(parameter ?? "", result ?? "");
if (!data) return res.status(500).send({ message: "生成剧本失败" });
const data = await generateScript(parameter ?? "", result ?? "");
if (!data) return res.status(500).send({ message: "生成剧本失败" });
await u.db("t_script").where("id", scriptId).update({
content: data,
});
await u.db("t_script").where("id", scriptId).update({
content: data,
});
res.status(200).send(success({ message: "生成剧本成功" }));
res.status(200).send(success({ message: "生成剧本成功" }));
} catch (e) {
const errMsg = u.error(e).message || "生成剧本失败";
res.status(500).send(error(errMsg));
}
},
);

View File

@ -0,0 +1,33 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
type: z.enum(["text", "image", "video"]),
}),
async (req, res) => {
const { type } = req.body;
const sqlTableMap = {
text: "t_textModel",
image: "t_imageModel",
video: "t_videoModel",
};
const modelLists = await u
.db(sqlTableMap[type as "image" | "text" | "video"])
.whereNot("manufacturer", "other")
.select("id", "manufacturer", "model");
const result: Record<string, any[]> = {};
for (const row of modelLists) {
if (!result[row.manufacturer]) {
result[row.manufacturer] = [];
}
result[row.manufacturer].push({ label: row.model, value: row.model });
}
res.status(200).send(success(result));
},
);

View File

@ -0,0 +1,31 @@
import express from "express";
import u from "@/utils";
import { success } from "@/lib/responseFormat";
const router = express.Router();
export default router.post("/", async (req, res) => {
const videoData = await u.db("t_videoModel").select("*");
const allData = videoData.map((i) => {
const durationResolutionMap = JSON.parse(i.durationResolutionMap ?? "[]");
const aspectRatio = JSON.parse(i.aspectRatio ?? "[]");
const type = JSON.parse(i.type ?? "[]");
return {
...i,
durationResolutionMap,
aspectRatio,
type,
audio: i.audio === 1,
};
});
const otherConfig = {
manufacturer: "other",
model: "",
durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["480p", "720p", "1080p"] }],
aspectRatio: ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9"],
type: ["text", "endFrameOptional", "singleImage", "multiImage"],
audio: true,
};
const returnData = [otherConfig, ...allData];
res.status(200).send(success(returnData));
});

View File

@ -0,0 +1,18 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
id: z.number(),
}),
async (req, res) => {
const { id } = req.body;
await u.db("t_assets").where("id", id).delete();
res.status(200).send(success("分镜删除成功"));
},
);

View File

@ -16,8 +16,10 @@ export default router.post(
}),
async (req, res) => {
const { filePath, prompt, projectId, assetsId } = req.body;
//拿到图片尺寸
const projectInfo = await u.db("t_project").where({ id: projectId }).first();
let data = await u.editImage(filePath, prompt, projectId);
let data = await u.editImage(filePath, prompt, projectId,projectInfo?.videoRatio!);
const returnData: {
id: number | null;
url: string | null;

View File

@ -0,0 +1,24 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 获取用户
export default router.post(
"/",
validateFields({
name: z.string(),
password: z.string(),
id: z.number(),
}),
async (req, res) => {
const { name, password, id } = req.body;
await u.db("t_user").where("id", id).update({
name,
password,
});
res.status(200).send(success("保存设置成功"));
},
);

View File

@ -33,9 +33,9 @@ export default router.post(
if (result.filePath) {
filesToDelete.push(result.filePath);
}
if (result.firstFrame) {
filesToDelete.push(result.firstFrame);
}
// if (result.firstFrame) {
// filesToDelete.push(result.firstFrame);
// }
}
// 删除文件

View File

@ -119,7 +119,7 @@ export default router.post(
res.status(200).send(success({ id: videoId, configId: configId || null }));
// 异步生成视频
generateVideoAsync(videoId, projectId, fileUrl, savePath, prompt, duration, resolution, audioEnabled, aiConfigData);
generateVideoAsync(videoId, projectId, fileUrl, savePath, prompt, duration, resolution, audioEnabled, aiConfigData, mode);
},
);
@ -134,6 +134,7 @@ async function generateVideoAsync(
resolution: string,
audioEnabled: boolean,
aiConfigData: t_config,
mode: string,
) {
try {
const projectData = await u.db("t_project").where("id", projectId).select("artStyle", "videoRatio").first();
@ -175,6 +176,7 @@ ${prompt}
aspectRatio: projectData?.videoRatio as any,
resolution: resolution as any,
audio: audioEnabled,
mode: mode as any,
},
{
baseURL: aiConfigData?.baseUrl!,

View File

@ -1,4 +1,4 @@
// @db-hash 945540586ca016b1b9a42042fc5ccbf3
// @db-hash 0f9789bd5ad2eebd79bd502988efcb4e
//该文件由脚本自动生成,请勿手动修改
export interface t_aiModelMap {
@ -52,6 +52,13 @@ export interface t_image {
'type'?: string | null;
'videoId'?: number | null;
}
export interface t_imageModel {
'grid'?: number | null;
'id'?: number;
'manufacturer'?: string | null;
'model'?: string | null;
'type'?: string | null;
}
export interface t_novel {
'chapter'?: string | null;
'chapterData'?: string | null;
@ -117,6 +124,15 @@ export interface t_taskList {
'startTime'?: string | null;
'state'?: string | null;
}
export interface t_textModel {
'id'?: number;
'image'?: number | null;
'manufacturer'?: string | null;
'model'?: string | null;
'responseFormat'?: string | null;
'think'?: number | null;
'tool'?: number | null;
}
export interface t_user {
'id'?: number;
'name'?: string | null;
@ -143,7 +159,6 @@ export interface t_videoConfig {
'createTime'?: number | null;
'duration'?: number | null;
'endFrame'?: string | null;
'errorReason'?: string | null;
'id'?: number;
'images'?: string | null;
'manufacturer'?: string | null;
@ -156,6 +171,15 @@ export interface t_videoConfig {
'startFrame'?: string | null;
'updateTime'?: number | null;
}
export interface t_videoModel {
'aspectRatio'?: string | null;
'audio'?: number | null;
'durationResolutionMap'?: string | null;
'id'?: number;
'manufacturer'?: string | null;
'model'?: string | null;
'type'?: string | null;
}
export interface DB {
"t_aiModelMap": t_aiModelMap;
@ -163,6 +187,7 @@ export interface DB {
"t_chatHistory": t_chatHistory;
"t_config": t_config;
"t_image": t_image;
"t_imageModel": t_imageModel;
"t_novel": t_novel;
"t_outline": t_outline;
"t_project": t_project;
@ -171,7 +196,9 @@ export interface DB {
"t_setting": t_setting;
"t_storyline": t_storyline;
"t_taskList": t_taskList;
"t_textModel": t_textModel;
"t_user": t_user;
"t_video": t_video;
"t_videoConfig": t_videoConfig;
"t_videoModel": t_videoModel;
}

View File

@ -10,6 +10,8 @@ import runninghub from "./owned/runninghub";
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" });
@ -25,20 +27,21 @@ const modelInstance = {
vidu: vidu,
runninghub: runninghub,
// apimart: apimart,
modelScope,
other,
grsai
} as const;
export default async (input: ImageConfig, config: AIConfig) => {
console.log("%c Line:32 🥪 config", "background:#33a5ff", config);
const { model, apiKey, baseURL, manufacturer } = { ...config };
if (!config || !config?.model || !config?.apiKey || !config?.manufacturer) throw new Error("请检查模型配置是否正确");
const manufacturerFn = modelInstance[manufacturer as keyof typeof modelInstance];
if (!manufacturerFn) if (!manufacturerFn) throw new Error("不支持的图片厂商");
if (manufacturer !== "other") {
const owned = modelList.find((m) => m.model === model);
if (!owned) throw new Error("不支持的模型");
}
// if (manufacturer !== "other") {
// const owned = modelList.find((m) => m.model === model);
// if (!owned) throw new Error("不支持的模型");
// }
// 补充图片的 base64 内容类型字符串
if (input.imageBase64 && input.imageBase64.length > 0) {
@ -65,7 +68,6 @@ export default async (input: ImageConfig, config: AIConfig) => {
}
let imageUrl = await manufacturerFn(input, { model, apiKey, baseURL });
console.log("%c Line:68 🍷 imageUrl", "background:#4fff4B", imageUrl);
if (!input.resType) input.resType = "b64";
if (input.resType === "b64" && imageUrl.startsWith("http")) imageUrl = await urlToBase64(imageUrl);
return imageUrl;

View File

@ -59,13 +59,6 @@ const modelList: Owned[] = [
grid: true,
type: "ti2i",
},
//ApiMart
{
manufacturer: "apimart",
model: "nanobanana",
grid: true,
type: "ti2i",
},
];
export default modelList;

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

@ -0,0 +1,132 @@
import "../type";
import { generateImage, generateText, ModelMessage } from "ai";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { pollTask } from "@/utils/ai/utils";
import u from "@/utils";
import axios from "axios";
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 compressionPrompt(prompt: string) {
const apiConfigData = await u.getPromptAi("assetsPrompt");
const result = await u.ai.text.invoke(
{
messages: [
{
role: "system",
content: `
Prompt工程师和文本摘要专家2000
1.
2. 2000
3. 2000
4.
5.
2000
`,
},
{
role: "user",
content: prompt,
},
],
},
apiConfigData,
);
return result.text;
}
export default async (input: ImageConfig, config: AIConfig): Promise<string> => {
if (!config.model) throw new Error("缺少Model名称");
if (!config.apiKey) throw new Error("缺少API Key");
const defaultBaseURL = "https://api-inference.modelscope.cn/v1/images/generations|https://api-inference.modelscope.cn/v1/tasks/{id}";
const { requestUrl, queryUrl } = getApiUrl(config.baseURL! ?? defaultBaseURL);
// 根据 size 配置映射到具体尺寸
const sizeMap: Record<string, Record<string, string>> = {
"1K": {
"16:9": "1664x928",
"9:16": "928x1664",
},
"2K": {
"16:9": "2048x1152",
"9:16": "1152x2048",
},
"4K": {
"16:9": "2048x1152",
"9:16": "1152x2048",
},
};
// 构建完整的提示词
const fullPrompt = input.systemPrompt ? `${input.systemPrompt}\n\n${input.prompt}` : input.prompt;
let newPrompt = fullPrompt;
if (fullPrompt.length > 2000) {
let compressed = await compressionPrompt(fullPrompt);
newPrompt = compressed;
}
let mergedImage = input.imageBase64;
if (mergedImage && mergedImage.length) {
const smallImage = await u.imageTools.mergeImages(mergedImage, "5mb");
mergedImage = [smallImage];
}
const size = sizeMap[input.size]?.[input.aspectRatio] ?? "1024x1024";
const taskBody: Record<string, any> = {
model: config.model,
prompt: newPrompt,
negative_prompt: "",
size,
...(mergedImage && mergedImage.length ? { image_url: mergedImage } : {}),
};
const apiKey = config.apiKey.replace("Bearer ", "");
try {
const { data } = await axios.post(requestUrl, taskBody, { headers: { Authorization: `Bearer ${apiKey}`, "X-ModelScope-Async-Mode": "true" } });
if (data.task_status != "SUCCEED") throw new Error(`任务提交失败: ${data || "未知错误"}`);
const taskId = data.task_id;
return await pollTask(async () => {
const { data: queryData } = await axios.get(template({ id: taskId }, queryUrl), {
headers: { Authorization: `Bearer ${apiKey}`, "X-ModelScope-Task-Type": "image_generation" },
});
const { task_status, output_images } = queryData || {};
if (task_status === "FAILED") {
return { completed: false, error: "图片生成失败" };
}
if (task_status === "SUCCEED") {
return { completed: true, url: output_images?.[0] };
}
return { completed: false };
});
} catch (error: any) {
const msg = u.error(error).message || "图片生成失败";
throw new Error(msg);
}
};
async function urlToBase64(url: string): Promise<string> {
const res = await axios.get(url, { responseType: "arraybuffer" });
const base64 = Buffer.from(res.data).toString("base64");
const mimeType = res.headers["content-type"] || "image/png";
return `data:${mimeType};base64,${base64}`;
}

View File

@ -69,26 +69,28 @@ export default async (input: ImageConfig, config: AIConfig): Promise<string> =>
console.error(JSON.stringify(result.response, null, 2));
throw new Error("图片生成失败");
}
const mdMatch = result.text.match(/^!\[.*?\]\((.+?)\)$/);
if (mdMatch) {
const imgInfo = mdMatch[1];
const base64InMd = imgInfo.match(/data:image\/[a-z]+;base64,(.+)/);
// 匹配所有 markdown 图片 ![...](url)
const mdImgPattern = /!\[.*?\]\((.+?)\)/g;
const matches = [...result.text.matchAll(mdImgPattern)];
for (const match of matches) {
const imgInfo = match[1];
// 检查是否已是 base64
const base64InMd = imgInfo.match(/data:image\/[a-z]+;base64,.+/);
if (base64InMd) {
return imgInfo;
return imgInfo; // 已经是base64直接返回
} else {
return await urlToBase64(imgInfo);
return await urlToBase64(imgInfo); // 否则尝试转base64
}
}
// 检查纯base64字符串
const base64Match = result.text.match(/base64,([A-Za-z0-9+/=]+)/);
if (base64Match) {
return "data:image/jpeg;base64," + base64Match[1];
}
// 检查是否为图片直链 url
// 检查是否为图片直链
if (/^https?:\/\/.*\.(png|jpg|jpeg|gif|webp|bmp)$/i.test(result.text)) {
return await urlToBase64(result.text);
}
// 默认情况
return result.text;
}

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

@ -8,18 +8,28 @@ export default async (input: ImageConfig, config: AIConfig): Promise<string> =>
const apiKey = "Bearer " + config.apiKey.replace(/Bearer\s+/g, "").trim();
const size = input.size === "1K" ? "2K" : input.size;
const sizeMap: Record<string, Record<string, string>> = {
"16:9": {
"2K": "2848x1600",
"4K": "4096x2304",
},
"9:16": {
"2K": "1600x2848",
"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,
size,
prompt: fullPrompt,
size: sizeMap[input.aspectRatio][size],
response_format: "url",
sequential_image_generation: "disabled",
stream: false,
watermark: false,
...(input.imageBase64 && { image: input.imageBase64 }),
};
const url = config.baseURL ?? "https://ark.cn-beijing.volces.com/api/v3/images/generations";
try {
const { data } = await axios.post(url, body, { headers: { Authorization: apiKey } });
@ -28,4 +38,4 @@ export default async (input: ImageConfig, config: AIConfig): Promise<string> =>
const msg = u.error(error).message || "Volcengine 图片生成失败";
throw new Error(msg);
}
}
};

View File

@ -3,7 +3,7 @@ import { generateText, streamText, Output, stepCountIs, ModelMessage, LanguageMo
import { wrapLanguageModel } from "ai";
import { devToolsMiddleware } from "@ai-sdk/devtools";
import { parse } from "best-effort-json-parser";
import modelList from "./modelList";
import { getModelList } from "./modelList";
import { z } from "zod";
import { OpenAIProvider } from "@ai-sdk/openai";
interface AIInput<T extends Record<string, z.ZodTypeAny> | undefined = undefined> {
@ -26,12 +26,14 @@ const buildOptions = async (input: AIInput<any>, config: AIConfig = {}) => {
if (!config || !config?.model || !config?.apiKey || !config?.manufacturer) throw new Error("请检查模型配置是否正确");
const { model, apiKey, baseURL, manufacturer } = { ...config };
let owned;
const modelList = await getModelList();
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("不支持的模型或厂商");
if (!owned) throw new Error("不支持的厂商");
const modelInstance = owned.instance({ apiKey, baseURL: baseURL!, name: "xixixi" });
@ -52,8 +54,9 @@ const buildOptions = async (input: AIInput<any>, config: AIConfig = {}) => {
};
const output = input.output ? (outputBuilders[owned.responseFormat]?.(input.output) ?? null) : null;
const chatModelManufacturer = ["doubao", "other", "openai"];
const chatModelManufacturer = ["volcengine", "other", "openai", "modelScope","grsai"];
const modelFn = chatModelManufacturer.includes(owned.manufacturer) ? (modelInstance as OpenAIProvider).chat(model!) : modelInstance(model!);
return {
config: {
model: modelFn as LanguageModel,
@ -76,6 +79,7 @@ const ai = Object.create({}) as {
ai.invoke = async (input: AIInput<any>, config: AIConfig) => {
const options = await buildOptions(input, config);
const result = await generateText(options.config);
if (options.responseFormat === "object" && input.output) {
const pattern = /{[^{}]*}|{(?:[^{}]*|{[^{}]*})*}/g;
@ -92,6 +96,7 @@ ai.invoke = async (input: AIInput<any>, config: AIConfig) => {
ai.stream = async (input: AIInput, config: AIConfig) => {
const options = await buildOptions(input, config);
return streamText(options.config);
};

View File

@ -1,11 +1,12 @@
import { createOpenAI } from "@ai-sdk/openai";
import { createOpenAI, OpenAIProviderSettings } from "@ai-sdk/openai";
import { createDeepSeek } from "@ai-sdk/deepseek";
import { createZhipu } from "zhipu-ai-provider";
import { createQwen } from "qwen-ai-provider";
import { createQwen } from "qwen-ai-provider-v5";
import { createGoogleGenerativeAI } from "@ai-sdk/google";
import { createAnthropic } from "@ai-sdk/anthropic";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { createXai } from '@ai-sdk/xai';
import { createXai } from "@ai-sdk/xai";
import db from "@/utils/db";
interface Owned {
manufacturer: string;
@ -23,11 +24,23 @@ interface Owned {
| typeof createAnthropic
| typeof createOpenAICompatible;
}
const instanceMap = {
deepSeek: createDeepSeek,
volcengine: createOpenAI,
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
{
manufacturer: "deepseek",
manufacturer: "deepSeek",
model: "deepseek-chat",
responseFormat: "schema",
image: false,
@ -36,7 +49,7 @@ const modelList: Owned[] = [
tool: true,
},
{
manufacturer: "deepseek",
manufacturer: "deepSeek",
model: "deepseek-reasoner",
responseFormat: "schema",
image: false,
@ -47,7 +60,16 @@ const modelList: Owned[] = [
// 豆包
{
manufacturer: "doubao",
manufacturer: "volcengine",
model: "doubao-seed-2-0-mini-260215",
responseFormat: "object",
image: true,
think: false,
instance: createOpenAI,
tool: true,
},
{
manufacturer: "volcengine",
model: "doubao-seed-1-8-251228",
responseFormat: "schema",
image: true,
@ -56,7 +78,7 @@ const modelList: Owned[] = [
tool: true,
},
{
manufacturer: "doubao",
manufacturer: "volcengine",
model: "doubao-seed-1-6-251015",
responseFormat: "schema",
image: true,
@ -65,7 +87,7 @@ const modelList: Owned[] = [
tool: true,
},
{
manufacturer: "doubao",
manufacturer: "volcengine",
model: "doubao-seed-1-6-lite-251015",
responseFormat: "schema",
image: true,
@ -74,7 +96,7 @@ const modelList: Owned[] = [
tool: true,
},
{
manufacturer: "doubao",
manufacturer: "volcengine",
model: "doubao-seed-1-6-flash-250828",
responseFormat: "schema",
image: true,
@ -413,7 +435,7 @@ const modelList: Owned[] = [
tool: true,
},
//xai
{
{
manufacturer: "xai",
model: "grok-3",
responseFormat: "schema",
@ -422,7 +444,7 @@ const modelList: Owned[] = [
instance: createXai,
tool: true,
},
{
{
manufacturer: "xai",
model: "grok-4",
responseFormat: "schema",
@ -431,7 +453,7 @@ const modelList: Owned[] = [
instance: createXai,
tool: true,
},
{
{
manufacturer: "xai",
model: "grok-4.1",
responseFormat: "schema",
@ -444,12 +466,24 @@ const modelList: Owned[] = [
{
manufacturer: "other",
model: "gpt-4.1",
responseFormat: "schema",
responseFormat: "object",
image: true,
think: false,
instance: createOpenAI,
tool: true,
},
];
export const getModelList = async () => {
const modelLists = await db("t_textModel").select("*");
const resultInstaceList = modelLists.map((model) => {
return {
...model,
tool: model.tool == 1 ? true : false,
think: model.think == 1 ? true : false,
image: model.image == 1 ? true : false,
instance: instanceMap[model.manufacturer as keyof typeof instanceMap],
};
});
return resultInstaceList as Owned[];
};
export default modelList;

View File

@ -1,5 +1,5 @@
import modelList from "./video/modelList";
import { db } from "../db";
interface ValidateResult {
owned: (typeof modelList)[number];
images: string[];
@ -80,3 +80,5 @@ export const pollTask = async (
}
throw new Error(`任务轮询超时,已尝试 ${maxAttempts}`);
};

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) => {
@ -29,8 +30,8 @@ export default async (input: VideoConfig, config?: AIConfig) => {
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("不支持的模型");
// 补充图片的 base64 内容类型字符串
if (input.imageBase64 && input.imageBase64.length > 0) {

View File

@ -10,7 +10,7 @@ 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 { owned, images, hasStartEndType } = validateVideoConfig(input, config);
const defaultBaseUrl = [
"https://generativelanguage.googleapis.com/v1beta/models/{model}:predictLongRunning",
@ -18,8 +18,7 @@ export default async (input: VideoConfig, config: AIConfig) => {
].join("|");
const [submitUrl, queryUrl] = (config.baseURL || defaultBaseUrl).split("|");
const headers = { "x-goog-api-key": config.apiKey };
const instance: Record<string, any> = { prompt: input.prompt };
@ -30,17 +29,17 @@ export default async (input: VideoConfig, config: AIConfig) => {
};
// 根据图片数量和模型能力决定图片用法
const len = images.length;
const hasRef = owned.type.includes("reference");
const hasSingle = owned.type.includes("singleImage");
const len = input.imageBase64 ? input.imageBase64.length : 0;
const hasRef = input.mode == "multi";
const hasSingle = input.mode == "single";
const hasStartEndType = input.mode === "startEnd";
if (len === 2 && hasStartEndType) {
instance.image = buildInlineImage(images[0]);
parameters.lastFrame = buildInlineImage(images[1]);
instance.image = buildInlineImage(input.imageBase64![0]);
parameters.lastFrame = buildInlineImage(input.imageBase64![1]);
} else if (len === 1 && (hasSingle || hasStartEndType)) {
instance.image = buildInlineImage(images[0]);
instance.image = buildInlineImage(input.imageBase64![0]);
} else if (len >= 1 && len <= 3 && hasRef) {
parameters.referenceImages = images.map((img) => ({ image: buildInlineImage(img), referenceType: "asset" }));
parameters.referenceImages = input.imageBase64!.map((img) => ({ image: buildInlineImage(img), referenceType: "asset" }));
}
const { data } = await axios.post(
@ -53,15 +52,14 @@ export default async (input: VideoConfig, config: AIConfig) => {
return pollTask(async () => {
const { data: status } = await axios.get(queryUrl.replace("{name}", data.name), { headers });
const { done, response, error } = status;
if (!done) return { completed: false };
if (error) return { completed: false, error: `任务失败: ${error.message || JSON.stringify(error)}` };
const videoUri = response?.generateVideoResponse?.generatedSamples?.[0]?.video?.uri;
if (!videoUri) return { completed: false, error: "未获取到视频下载地址" };
const videoRes = await axios.get(videoUri, { headers, responseType: "arraybuffer", maxRedirects: 5 });

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

@ -6,7 +6,7 @@ export default async (input: VideoConfig, config: AIConfig) => {
if (!config.apiKey) throw new Error("缺少API Key");
if (!config.baseURL) throw new Error("缺少baseURL配置");
const { images } = validateVideoConfig(input, config);
// const { images } = validateVideoConfig(input, config);
// 解析URL配置图生视频|文生视频|查询地址
const defaultBaseUrl =
@ -24,7 +24,7 @@ export default async (input: VideoConfig, config: AIConfig) => {
const mode = modelMatch ? (modelMatch[2].toLowerCase() as "std" | "pro") : "std";
// 判断是图生视频还是文生视频
const hasImage = images.length > 0;
const hasImage = input.imageBase64 ? input.imageBase64.length > 0 : false;
const createUrl = hasImage ? image2videoUrl : text2videoUrl;
// 去除图片的内容类型前缀kling要求纯base64
@ -41,9 +41,9 @@ export default async (input: VideoConfig, config: AIConfig) => {
if (hasImage) {
// 图生视频:首帧和尾帧
body.image = stripDataUrl(images[0]);
if (images.length > 1) {
body.image_tail = stripDataUrl(images[1]);
body.image = stripDataUrl(input.imageBase64![0]);
if (input.imageBase64!.length > 1) {
body.image_tail = stripDataUrl(input.imageBase64![1]);
}
}

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 || "未知错误"}`);
};

View File

@ -7,7 +7,7 @@ import { pollTask, validateVideoConfig } from "@/utils/ai/utils";
export default async (input: VideoConfig, config: AIConfig) => {
if (!config.apiKey) throw new Error("缺少API Key");
const { owned, images, hasTextType } = validateVideoConfig(input, config);
// const { owned, images, hasTextType } = validateVideoConfig(input, config);
const defaultBaseUrl = [
"https://www.runninghub.cn/openapi/v2/rhart-video-s/image-to-video",
@ -19,8 +19,8 @@ export default async (input: VideoConfig, config: AIConfig) => {
].join("|");
const [image2videoUrl, image2videoProUrl, text2videoUrl, text2videoProUrl, queryUrl, uploadUrl] = (config.baseURL || defaultBaseUrl).split("|");
const isPro = owned.model === "sora-2-pro";
const hasTextType = input.mode == "text";
const isPro = config.model === "sora-2-pro";
const authorization = `Bearer ${config.apiKey}`;
// 上传 base64 图片
@ -65,20 +65,19 @@ export default async (input: VideoConfig, config: AIConfig) => {
return { taskId: data.taskId, status: data.status, url: data.results?.[0]?.url };
};
const isTextToVideo = images.length === 0 && hasTextType;
const isTextToVideo = (!input.imageBase64 || input.imageBase64.length === 0) && hasTextType;
const submitUrl = isTextToVideo ? (isPro ? text2videoProUrl : text2videoUrl) : isPro ? image2videoProUrl : image2videoUrl;
const requestBody: Record<string, unknown> = {
prompt: input.prompt,
duration: String(input.duration),
aspectRatio: input.aspectRatio,
...(isTextToVideo ? {} : { imageUrl: await uploadImage(images[0]) }),
...(isTextToVideo ? {} : { imageUrl: await uploadImage(input.imageBase64![0]) }),
};
const { taskId } = await submitTask(submitUrl, requestBody);
return await pollTask(async () => {
const { data } = await axios.post(
queryUrl,
{

View File

@ -20,30 +20,30 @@ export default async (input: VideoConfig, config: AIConfig) => {
const hasImages = input.imageBase64 && input.imageBase64.length > 0;
// 根据是否有图片,查找匹配的模型配置
const customOwned = modelList.find((m) => {
if (m.manufacturer !== "vidu") return false;
if (m.model !== config.model) return false;
if (hasImages) {
return m.type.some((t) => t !== "text");
} else {
return m.type.includes("text");
}
});
// const customOwned = modelList.find((m) => {
// if (m.manufacturer !== "vidu") return false;
// if (m.model !== config.model) return false;
// if (hasImages) {
// return m.type.some((t) => t !== "text");
// } else {
// return m.type.includes("text");
// }
// });
if (!customOwned) {
throw new Error(`未找到匹配的模型配置: ${config.model}`);
}
// if (!customOwned) {
// throw new Error(`未找到匹配的模型配置: ${config.model}`);
// }
// 使用统一校验函数
const { owned, images } = validateVideoConfig(input, config, customOwned);
// const { owned, images } = validateVideoConfig(input, config, customOwned);
// 判断生成类型
const genType: "text" | "image" = images.length === 0 ? "text" : "image";
const genType: "text" | "image" = input.imageBase64 && input.imageBase64.length === 0 ? "text" : "image";
// 校验宽高比(仅文生视频需要)
if (genType === "text" && owned.aspectRatio.length > 0 && !owned.aspectRatio.includes(input.aspectRatio as `${number}:${number}`)) {
throw new Error(`模型 ${owned.model} 不支持宽高比 ${input.aspectRatio},支持的宽高比:${owned.aspectRatio.join("、")}`);
}
// // 校验宽高比(仅文生视频需要)
// if (genType === "text" && owned.aspectRatio.length > 0 && !owned.aspectRatio.includes(input.aspectRatio as `${number}:${number}`)) {
// throw new Error(`模型 ${owned.model} 不支持宽高比 ${input.aspectRatio},支持的宽高比:${owned.aspectRatio.join("、")}`);
// }
// 创建任务
let taskId: string;
@ -51,13 +51,13 @@ export default async (input: VideoConfig, config: AIConfig) => {
if (genType === "text") {
// 文生视频
const requestBody: Record<string, unknown> = {
model: owned.model,
model: config.model,
prompt: input.prompt,
duration: input.duration,
resolution: input.resolution,
aspect_ratio: input.aspectRatio,
};
if (owned.audio && input.audio !== undefined) {
if (input.audio && input.audio !== undefined) {
requestBody.audio = input.audio;
}
@ -71,15 +71,15 @@ export default async (input: VideoConfig, config: AIConfig) => {
} else {
// 图生视频
const requestBody: Record<string, unknown> = {
model: owned.model,
images: images,
model: config.model,
images: input.imageBase64,
duration: input.duration,
resolution: input.resolution,
};
if (input.prompt) {
requestBody.prompt = input.prompt;
}
if (owned.audio && input.audio !== undefined) {
if (input.audio && input.audio !== undefined) {
requestBody.audio = input.audio;
}

View File

@ -5,11 +5,11 @@ import { pollTask, validateVideoConfig } from "@/utils/ai/utils";
export default async (input: VideoConfig, config: AIConfig) => {
if (!config.apiKey) throw new Error("缺少API Key");
const { owned, images, hasStartEndType } = validateVideoConfig(input, config);
// const { owned, images, hasStartEndType } = validateVideoConfig(input, config);
const hasStartEndType = input.mode === "startEnd";
const authorization = "Bearer " + config.apiKey.replace(/^Bearer\s*/i, "").trim();
const baseUrl = config.baseURL ?? "https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks";
const images = input.imageBase64 || [];
// 判断是否为首尾帧模式(需要两张图且类型支持首尾帧)
const isStartEndMode = images.length === 2 && hasStartEndType;
@ -35,7 +35,7 @@ export default async (input: VideoConfig, config: AIConfig) => {
};
// 仅当模型支持音频时才添加 generate_audio 字段
if (owned.audio) {
if (input?.audio) {
requestBody.generate_audio = input.audio ?? false;
}
// 创建视频生成任务
@ -52,11 +52,11 @@ export default async (input: VideoConfig, config: AIConfig) => {
// 轮询任务状态
return await pollTask(async () => {
const { status, content } = (
await axios.get(`${baseUrl}/${taskId}`, {
headers: { Authorization: authorization },
})
).data;
const data = await axios.get(`${baseUrl}/${taskId}`, {
headers: { Authorization: authorization },
});
const { status, content } = data.data;
switch (status) {
case "succeeded":

View File

@ -39,8 +39,10 @@ const getSizeFromConfig = (resolution: string, aspectRatio: string): string => {
export default async (input: VideoConfig, config: AIConfig) => {
if (!config.apiKey) throw new Error("缺少API Key");
const { owned, images, hasStartEndType, hasTextType } = validateVideoConfig(input, config);
// const { owned, images, hasStartEndType, hasTextType } = validateVideoConfig(input, config);
const hasStartEndType = input.mode === "startEnd";
const hasTextType = input.mode === "text";
const images = input.imageBase64 || [];
const defaultBaseUrl = [
"https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation/video-synthesis",
"https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/video-synthesis",
@ -49,7 +51,7 @@ export default async (input: VideoConfig, config: AIConfig) => {
const [i2vUrl, kf2vUrl, queryUrl] = (config.baseURL || defaultBaseUrl).split("|");
const types = owned.type;
// const types = owned.type;
const authorization = `Bearer ${config.apiKey}`;
// 确定端点和构建请求体
@ -69,7 +71,7 @@ export default async (input: VideoConfig, config: AIConfig) => {
duration: input.duration,
},
};
} else if (types.includes("singleImage")) {
} else if (input.mode == 'single' && images.length === 1) {
// 图生视频
submitUrl = i2vUrl;
body = {
@ -84,7 +86,7 @@ export default async (input: VideoConfig, config: AIConfig) => {
},
};
// audio参数仅部分模型支持
if (owned.audio && input.audio !== undefined) {
if (input.audio && input.audio !== undefined) {
body.parameters.audio = input.audio;
}
} else if (hasStartEndType) {
@ -95,9 +97,7 @@ export default async (input: VideoConfig, config: AIConfig) => {
first_frame_url: images[0],
};
// 尾帧处理
if (types.includes("startEndRequired")) {
inputObj.last_frame_url = images[1];
} else if ((types.includes("endFrameOptional") || types.includes("startFrameOptional")) && images.length >= 2) {
if (hasStartEndType && images.length >= 2) {
inputObj.last_frame_url = images[1];
}
body = {
@ -109,7 +109,7 @@ export default async (input: VideoConfig, config: AIConfig) => {
},
};
} else {
throw new Error(`不支持的视频生成类型: ${types.join(", ")}`);
throw new Error(`不支持的视频生成类型: ${hasStartEndType ? "startEnd" : hasTextType ? "text" : "single"}`);
}
// 提交任务

View File

@ -6,6 +6,7 @@ interface VideoConfig {
savePath: string;
imageBase64?: string[];
audio?: boolean;
mode: "startEnd" | "multi" | "single" | "text";
}
interface AIConfig {

View File

@ -77,7 +77,7 @@ async function convertDirectiveAndImages(images: Record<string, string>, directi
* "将@图10中圈起来的部分换成@图8"
* );
*/
export default async (images: Record<string, string>, directive: string, projectId: number) => {
export default async (images: Record<string, string>, directive: string, projectId: number, aspectRatio: string | null) => {
const { prompt, images: base64Images } = await convertDirectiveAndImages(images, directive);
const apiConfig = await u.getPromptAi("editImage");
@ -86,7 +86,7 @@ export default async (images: Record<string, string>, directive: string, project
systemPrompt: "根据用户提供的具体修改指令,对上传的图片进行智能编辑。",
prompt: prompt,
imageBase64: base64Images,
aspectRatio: "16:9",
aspectRatio: aspectRatio ? aspectRatio : "16:9",
size: "1K",
},
apiConfig,

View File

@ -74,15 +74,6 @@
"@standard-schema/spec" "^1.1.0"
eventsource-parser "^3.0.6"
"@ai-sdk/provider-utils@^2.1.6":
version "2.2.8"
resolved "https://registry.npmmirror.com/@ai-sdk/provider-utils/-/provider-utils-2.2.8.tgz#ad11b92d5a1763ab34ba7b5fc42494bfe08b76d1"
integrity sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==
dependencies:
"@ai-sdk/provider" "1.1.3"
nanoid "^3.3.8"
secure-json-parse "^2.7.0"
"@ai-sdk/provider-utils@^3.0.0":
version "3.0.20"
resolved "https://registry.npmmirror.com/@ai-sdk/provider-utils/-/provider-utils-3.0.20.tgz#61d7741065550833eae3ac6440d943e9d3d25120"
@ -92,12 +83,14 @@
"@standard-schema/spec" "^1.0.0"
eventsource-parser "^3.0.6"
"@ai-sdk/provider@1.1.3", "@ai-sdk/provider@^1.0.7":
version "1.1.3"
resolved "https://registry.npmmirror.com/@ai-sdk/provider/-/provider-1.1.3.tgz#ebdda8077b8d2b3f290dcba32c45ad19b2704681"
integrity sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==
"@ai-sdk/provider-utils@^4.0.0":
version "4.0.15"
resolved "https://registry.npmmirror.com/@ai-sdk/provider-utils/-/provider-utils-4.0.15.tgz#d585c7c89cfdf13697a40be5768ecd907a251585"
integrity sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w==
dependencies:
json-schema "^0.4.0"
"@ai-sdk/provider" "3.0.8"
"@standard-schema/spec" "^1.1.0"
eventsource-parser "^3.0.6"
"@ai-sdk/provider@2.0.1", "@ai-sdk/provider@^2.0.0":
version "2.0.1"
@ -113,6 +106,13 @@
dependencies:
json-schema "^0.4.0"
"@ai-sdk/provider@3.0.8", "@ai-sdk/provider@^3.0.0":
version "3.0.8"
resolved "https://registry.npmmirror.com/@ai-sdk/provider/-/provider-3.0.8.tgz#fd7fac7533c03534ac1d3fb710a6b96e2aa00263"
integrity sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==
dependencies:
json-schema "^0.4.0"
"@ai-sdk/xai@^3.0.47":
version "3.0.47"
resolved "https://registry.npmmirror.com/@ai-sdk/xai/-/xai-3.0.47.tgz#a8d3e08603865c5e401e19c801c7a80c3f31b890"
@ -3160,11 +3160,6 @@ ms@^2.0.0, ms@^2.1.1, ms@^2.1.3:
resolved "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
nanoid@^3.3.8:
version "3.3.11"
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
napi-build-utils@^2.0.0:
version "2.0.0"
resolved "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz#13c22c0187fcfccce1461844136372a47ddc027e"
@ -3614,13 +3609,13 @@ quick-lru@^5.1.1:
resolved "https://registry.npmmirror.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
qwen-ai-provider@^0.1.1:
version "0.1.1"
resolved "https://registry.npmmirror.com/qwen-ai-provider/-/qwen-ai-provider-0.1.1.tgz#f854379514eed919fe01de20007f6238a8ad2b41"
integrity sha512-7dVu97U7fbOGgCYdaOunC4NQqC+7Or3/Gsbx+P16+Ny4VxST7WJxfUCogQl6D2EDxIJdHGz4akHm+5fyEulmyw==
qwen-ai-provider-v5@^2.1.0:
version "2.1.0"
resolved "https://registry.npmmirror.com/qwen-ai-provider-v5/-/qwen-ai-provider-v5-2.1.0.tgz#8672871135bb4a5fda32409c00b70d10637f8a50"
integrity sha512-I+Iv45ymrez1wieZFu0n/lc/lSkbAQMlujWBCfUWBUOf6DizYfvPKaydsojXM7CU8TcqJbYJuN3ofnaxFIwBZA==
dependencies:
"@ai-sdk/provider" "^1.0.7"
"@ai-sdk/provider-utils" "^2.1.6"
"@ai-sdk/provider" "^3.0.0"
"@ai-sdk/provider-utils" "^4.0.0"
range-parser@^1.2.1:
version "1.2.1"
@ -3846,11 +3841,6 @@ sax@^1.2.4:
resolved "https://registry.npmmirror.com/sax/-/sax-1.4.4.tgz#f29c2bba80ce5b86f4343b4c2be9f2b96627cf8b"
integrity sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==
secure-json-parse@^2.7.0:
version "2.7.0"
resolved "https://registry.npmmirror.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862"
integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==
semver-compare@^1.0.0:
version "1.0.0"
resolved "https://registry.npmmirror.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"