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

This commit is contained in:
ACT丶流星雨 2026-02-10 16:39:50 +08:00
commit 8ff115c43e
14 changed files with 537 additions and 1802 deletions

View File

@ -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

View File

@ -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: "正在分割宫格图片为单张镜头图" });

View File

@ -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");

View File

@ -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");

View File

@ -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);
}

View File

@ -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));
});

View 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));
});

View 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("编辑成功"));
},
);

View File

@ -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 });
}
}

View File

@ -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

View File

@ -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;

View File

@ -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}`;
}

View File

@ -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}` };
});
};