Merge branch 'develop' of https://github.com/HBAI-Ltd/Toonflow-app into develop
This commit is contained in:
commit
8ff115c43e
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "toonflow-app",
|
||||
"version": "1.0.6-dev",
|
||||
"version": "1.0.6-dev2",
|
||||
"description": "Toonflow 是一款 AI 短剧漫剧工具,能够利用 AI 技术将小说自动转化为剧本,并结合 AI 生成的图片和视频,实现高效的短剧创作。",
|
||||
"author": "HBAI-Ltd <ltlctools@outlook.com>",
|
||||
"homepage": "https://github.com/HBAI-Ltd/Toonflow-app#readme",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -6,6 +6,8 @@ import { z } from "zod";
|
||||
import type { DB } from "@/types/database";
|
||||
import generateImageTool from "./generateImageTool";
|
||||
import imageSplitting from "./imageSplitting";
|
||||
import path from "path";
|
||||
import sharp from "sharp";
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
@ -240,9 +242,11 @@ ${sections.join("\n\n")}
|
||||
const skipped: number[] = [];
|
||||
|
||||
for (const item of shots) {
|
||||
const exists = this.shots.some((f) => f.segmentId === item.segmentIndex);
|
||||
const resultIndex = item.segmentIndex - 1;
|
||||
|
||||
const exists = this.shots.some((f) => f.segmentId === resultIndex);
|
||||
if (exists) {
|
||||
skipped.push(item.segmentIndex);
|
||||
skipped.push(resultIndex);
|
||||
continue;
|
||||
}
|
||||
// 分配独立的分镜ID
|
||||
@ -250,15 +254,15 @@ ${sections.join("\n\n")}
|
||||
const shotId = this.shotIdCounter;
|
||||
this.shots.push({
|
||||
id: shotId,
|
||||
segmentId: item.segmentIndex,
|
||||
segmentId: resultIndex,
|
||||
title: `分镜 ${shotId}`,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cells: item.prompts.map((prompt) => ({ id: u.uuid(), prompt })),
|
||||
fragmentContent: this.segments[item.segmentIndex]?.description,
|
||||
fragmentContent: this.segments[resultIndex]?.description,
|
||||
assetsTags: item.assetsTags,
|
||||
});
|
||||
added.push({ id: shotId, segmentIndex: item.segmentIndex });
|
||||
added.push({ id: shotId, segmentIndex: resultIndex });
|
||||
}
|
||||
|
||||
const addedInfo = added.map((a) => `分镜${a.id}(片段${a.segmentIndex})`).join(", ");
|
||||
@ -442,7 +446,6 @@ ${sections.join("\n\n")}
|
||||
this.scriptId,
|
||||
this.projectId,
|
||||
);
|
||||
|
||||
// 通知前端正在分割图片
|
||||
this.emit("shotImageGenerateProgress", { shotId, status: "splitting", message: "正在分割宫格图片为单张镜头图" });
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ export default async (knex: Knex): Promise<void> => {
|
||||
await addColumn("t_video", "aiConfigId", "integer");
|
||||
await addColumn("t_config", "modelType", "text");
|
||||
await addColumn("t_videoConfig", "audioEnabled", "integer");
|
||||
await addColumn("t_videoConfig", "errorReason", "text");
|
||||
|
||||
//更正字段
|
||||
await alterColumnType("t_config", "modelType", "text");
|
||||
|
||||
@ -151,6 +151,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
|
||||
table.text("firstFrame");
|
||||
table.text("storyboardImgs");
|
||||
table.text("model");
|
||||
table.text("errorReason");
|
||||
table.integer("time");
|
||||
table.integer("state");
|
||||
table.integer("scriptId");
|
||||
|
||||
110
src/router.ts
110
src/router.ts
@ -1,4 +1,4 @@
|
||||
// @routes-hash df8fc2bdd69f6bbf900ca75396098390
|
||||
// @routes-hash 3cfad40b3c8658b442ab766a9323d740
|
||||
import { Express } from "express";
|
||||
|
||||
import route1 from "./routes/assets/addAssets";
|
||||
@ -50,32 +50,34 @@ 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/updeteModel";
|
||||
import route51 from "./routes/storyboard/batchSuperScoreImage";
|
||||
import route52 from "./routes/storyboard/chatStoryboard";
|
||||
import route53 from "./routes/storyboard/generateShotImage";
|
||||
import route54 from "./routes/storyboard/generateStoryboardApi";
|
||||
import route55 from "./routes/storyboard/generateVideoPrompt";
|
||||
import route56 from "./routes/storyboard/getStoryboard";
|
||||
import route57 from "./routes/storyboard/keepStoryboard";
|
||||
import route58 from "./routes/storyboard/saveStoryboard";
|
||||
import route59 from "./routes/storyboard/uploadImage";
|
||||
import route60 from "./routes/task/getTaskApi";
|
||||
import route61 from "./routes/task/taskDetails";
|
||||
import route62 from "./routes/user/getUser";
|
||||
import route63 from "./routes/video/addVideo";
|
||||
import route64 from "./routes/video/addVideoConfig";
|
||||
import route65 from "./routes/video/deleteVideoConfig";
|
||||
import route66 from "./routes/video/generatePrompt";
|
||||
import route67 from "./routes/video/generateVideo";
|
||||
import route68 from "./routes/video/getManufacturer";
|
||||
import route69 from "./routes/video/getVideo";
|
||||
import route70 from "./routes/video/getVideoConfigs";
|
||||
import route71 from "./routes/video/getVideoModel";
|
||||
import route72 from "./routes/video/getVideoStoryboards";
|
||||
import route73 from "./routes/video/reviseVideoStoryboards";
|
||||
import route74 from "./routes/video/saveVideo";
|
||||
import route75 from "./routes/video/upDateVideoConfig";
|
||||
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";
|
||||
|
||||
export default async (app: Express) => {
|
||||
app.use("/assets/addAssets", route1);
|
||||
@ -127,30 +129,32 @@ export default async (app: Express) => {
|
||||
app.use("/setting/getAiModelMap", route47);
|
||||
app.use("/setting/getLog", route48);
|
||||
app.use("/setting/getSetting", route49);
|
||||
app.use("/setting/updeteModel", route50);
|
||||
app.use("/storyboard/batchSuperScoreImage", route51);
|
||||
app.use("/storyboard/chatStoryboard", route52);
|
||||
app.use("/storyboard/generateShotImage", route53);
|
||||
app.use("/storyboard/generateStoryboardApi", route54);
|
||||
app.use("/storyboard/generateVideoPrompt", route55);
|
||||
app.use("/storyboard/getStoryboard", route56);
|
||||
app.use("/storyboard/keepStoryboard", route57);
|
||||
app.use("/storyboard/saveStoryboard", route58);
|
||||
app.use("/storyboard/uploadImage", route59);
|
||||
app.use("/task/getTaskApi", route60);
|
||||
app.use("/task/taskDetails", route61);
|
||||
app.use("/user/getUser", route62);
|
||||
app.use("/video/addVideo", route63);
|
||||
app.use("/video/addVideoConfig", route64);
|
||||
app.use("/video/deleteVideoConfig", route65);
|
||||
app.use("/video/generatePrompt", route66);
|
||||
app.use("/video/generateVideo", route67);
|
||||
app.use("/video/getManufacturer", route68);
|
||||
app.use("/video/getVideo", route69);
|
||||
app.use("/video/getVideoConfigs", route70);
|
||||
app.use("/video/getVideoModel", route71);
|
||||
app.use("/video/getVideoStoryboards", route72);
|
||||
app.use("/video/reviseVideoStoryboards", route73);
|
||||
app.use("/video/saveVideo", route74);
|
||||
app.use("/video/upDateVideoConfig", route75);
|
||||
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);
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ const router = express.Router();
|
||||
|
||||
export default router.post("/", async (req, res) => {
|
||||
const userId = 1;
|
||||
const configData = await u.db("t_config").where("userId", userId).select("*");
|
||||
const configData = await u.db("t_config").where("type","<>","video").where("userId", userId).select("*");
|
||||
|
||||
res.status(200).send(success(configData));
|
||||
});
|
||||
|
||||
11
src/routes/setting/getVideoModelList.ts
Normal file
11
src/routes/setting/getVideoModelList.ts
Normal file
@ -0,0 +1,11 @@
|
||||
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 userId = 1;
|
||||
const configData = await u.db("t_config").where("type","video").where("userId", userId).select("*");
|
||||
|
||||
res.status(200).send(success(configData));
|
||||
});
|
||||
32
src/routes/setting/updateModel.ts
Normal file
32
src/routes/setting/updateModel.ts
Normal file
@ -0,0 +1,32 @@
|
||||
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(),
|
||||
type: z.enum(["text", "video", "image"]),
|
||||
model: z.string(),
|
||||
baseUrl: z.string(),
|
||||
modelType: z.string(),
|
||||
apiKey: z.string(),
|
||||
manufacturer: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id, type, model, baseUrl, apiKey, manufacturer, modelType } = req.body;
|
||||
|
||||
await u.db("t_config").where("id", id).update({
|
||||
type,
|
||||
model,
|
||||
baseUrl,
|
||||
apiKey,
|
||||
manufacturer,
|
||||
modelType,
|
||||
});
|
||||
res.status(200).send(success("编辑成功"));
|
||||
},
|
||||
);
|
||||
@ -196,7 +196,10 @@ ${prompt}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`视频生成失败 videoId=${videoId}:`, err);
|
||||
await u.db("t_video").where("id", videoId).update({ state: -1 });
|
||||
await u
|
||||
.db("t_video")
|
||||
.where("id", videoId)
|
||||
.update({ state: -1, errorReason: u.error(err).message });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ export default router.post(
|
||||
qb.whereIn("id", specifyIds);
|
||||
}
|
||||
})
|
||||
.select("id", "configId", "time", "resolution", "prompt", "firstFrame", "filePath", "storyboardImgs", "model", "scriptId", "state");
|
||||
.select("id", "configId", "time", "resolution", "prompt", "firstFrame", "filePath", "storyboardImgs", "model", "scriptId", "state","errorReason");
|
||||
// const videoIds: number[] = videos.map((video: any) => (typeof video.id === "string" ? parseInt(video.id) : video.id));
|
||||
|
||||
// let tempAssets: TempAsset[] = await u
|
||||
|
||||
18
src/types/database.d.ts
vendored
18
src/types/database.d.ts
vendored
@ -1,6 +1,20 @@
|
||||
// @db-hash b175910ce89abacc2636f298095b06c3
|
||||
// @db-hash 5de69fa13b58ac3b447664cb6faa9e8a
|
||||
//该文件由脚本自动生成,请勿手动修改
|
||||
|
||||
export interface _t_video_old_20260210 {
|
||||
'aiConfigId'?: number | null;
|
||||
'configId'?: number | null;
|
||||
'filePath'?: string | null;
|
||||
'firstFrame'?: string | null;
|
||||
'id'?: number;
|
||||
'model'?: string | null;
|
||||
'prompt'?: string | null;
|
||||
'resolution'?: string | null;
|
||||
'scriptId'?: number | null;
|
||||
'state'?: number | null;
|
||||
'storyboardImgs'?: string | null;
|
||||
'time'?: number | null;
|
||||
}
|
||||
export interface t_aiModelMap {
|
||||
'configId'?: number | null;
|
||||
'id'?: number;
|
||||
@ -125,6 +139,7 @@ export interface t_user {
|
||||
export interface t_video {
|
||||
'aiConfigId'?: number | null;
|
||||
'configId'?: number | null;
|
||||
'errorReason'?: string | null;
|
||||
'filePath'?: string | null;
|
||||
'firstFrame'?: string | null;
|
||||
'id'?: number;
|
||||
@ -156,6 +171,7 @@ export interface t_videoConfig {
|
||||
}
|
||||
|
||||
export interface DB {
|
||||
"_t_video_old_20260210": _t_video_old_20260210;
|
||||
"t_aiModelMap": t_aiModelMap;
|
||||
"t_assets": t_assets;
|
||||
"t_chatHistory": t_chatHistory;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import "../type";
|
||||
import { generateImage, generateText, ModelMessage } from "ai";
|
||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
||||
import axios from "axios";
|
||||
|
||||
export default async (input: ImageConfig, config: AIConfig): Promise<string> => {
|
||||
if (!config.model) throw new Error("缺少Model名称");
|
||||
@ -67,11 +68,26 @@ export default async (input: ImageConfig, config: AIConfig): Promise<string> =>
|
||||
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!;
|
||||
const mdMatch = result.text.match(/^!\[.*?\]\((.+?)\)$/);
|
||||
if (mdMatch) {
|
||||
const imgInfo = mdMatch[1];
|
||||
const base64InMd = imgInfo.match(/data:image\/[a-z]+;base64,(.+)/);
|
||||
if (base64InMd) {
|
||||
return imgInfo;
|
||||
} else {
|
||||
return await urlToBase64(imgInfo);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
const { image } = await generateImage({
|
||||
@ -87,3 +103,10 @@ export default async (input: ImageConfig, config: AIConfig): Promise<string> =>
|
||||
return image.base64;
|
||||
}
|
||||
};
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
@ -4,11 +4,12 @@ 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}`;
|
||||
@ -20,39 +21,30 @@ export default async (input: VideoConfig, config: AIConfig) => {
|
||||
|
||||
// 根据 aspectRatio 设置 size
|
||||
const sizeMap: Record<string, string> = {
|
||||
"16:9": "1280x720",
|
||||
"9:16": "720x1280",
|
||||
"16:9": "1920x1080",
|
||||
"9:16": "1080x1920",
|
||||
};
|
||||
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 body = {
|
||||
model: "sora-2-all",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: input.prompt,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const { data } = await axios.post(
|
||||
"https://api2.aigcbest.top/v1/chat/completions",
|
||||
{ ...body },
|
||||
{
|
||||
headers: { "Content-Type": "application/json", Authorization: authorization },
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
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}` };
|
||||
});
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user