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

# Conflicts:
#	src/router.ts
#	src/types/database.d.ts
This commit is contained in:
zhishi 2026-03-03 17:21:17 +08:00
commit 2a15c144ae
27 changed files with 5494 additions and 74 deletions

View File

@ -103,8 +103,8 @@ https://www.bilibili.com/video/BV1na6wB6Ea2
| 操作系统 | GitHub 下载 | 夸克网盘下载 | 说明 |
| :------: | :----------------------------------------------------------- | :---------------------------------------------- | :------------- |
| Windows | [Release](https://github.com/HBAI-Ltd/Toonflow-app/releases) | [夸克网盘](https://pan.quark.cn/s/94ef07509df0) | 官方发布安装包 |
| Linux | ⚙️ 敬请期待 | ⚙️ 敬请期待 | 即将发布 |
| macOS | ⚙️ 敬请期待 | ⚙️ 敬请期待 | 即将发布 |
| Linux | [Release](https://github.com/HBAI-Ltd/Toonflow-app/releases) | [夸克网盘](https://pan.quark.cn/s/94ef07509df0) | 官方发布安装包 |
| macOS | [Release](https://github.com/HBAI-Ltd/Toonflow-app/releases) | [夸克网盘](https://pan.quark.cn/s/94ef07509df0) | 官方发布安装包 |
> 目前仅支持 Windows 版本,其他系统将陆续开放。
@ -530,10 +530,11 @@ pm2 monit # 监控面板
~~交流群 12~~
交流群 13:
~~交流群 13~~
<img src="./docs/chat13QR.jpg?r=2" alt="Toonflow Logo" height="400"/>
<p>使用微信扫码添加,二维码过期可提交 Issues 提醒更新</p>
拉群小助手:
<img src="./docs/QR.png" alt="Toonflow Logo" height="400"/>
---
@ -555,7 +556,7 @@ Toonflow 基于 AGPL-3.0 协议开源发布许可证详情https://www.gnu.
# ⭐️ 星标历史
[![Star History Chart](https://api.star-history.com/svg?repos=HBAI-Ltd/Toonflow-app&type=date&legend=top-left)](https://www.star-history.com/#HBAI-Ltd/Toonflow-app&type=date&legend=top-left)
[![Star History Chart](https://api.star-history.com/svg?repos=HBAI-Ltd/Toonflow-app&type=timeline&legend=top-left)](https://www.star-history.com/#HBAI-Ltd/Toonflow-app&type=timeline&legend=top-left)
---

BIN
docs/QR.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
docs/chat12QR.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

BIN
docs/chat13QR.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

1411
output.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -317,6 +317,10 @@ export default async (cells: { prompt: string }[], scriptId: number, projectId:
size: "4K",
aspectRatio: projectInfo?.videoRatio ? (projectInfo.videoRatio as any) : "16:9",
imageBase64: processedImages.map((buf) => buf.toString("base64")),
taskClass: "分镜图生成",
name: `分镜图-${outline?.title || "未知剧集"}`,
describe: prompts,
projectId,
},
apiConfig,
);

1411
src/lib/artStyle.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
import { Knex } from "knex";
import { v4 as uuid } from "uuid";
import { artStyle } from "./artStyle";
interface TableSchema {
name: string;
builder: (table: Knex.CreateTableBuilder) => void;
@ -96,6 +97,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
name: "t_project",
builder: (table) => {
table.integer("id");
table.string("projectType");
table.text("name");
table.text("intro");
table.text("type");
@ -160,20 +162,6 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
table.unique(["id"]);
},
},
{
name: "t_taskList",
builder: (table) => {
table.integer("id").notNullable();
table.integer("projectName");
table.text("name");
table.text("prompt");
table.text("state");
table.text("startTime");
table.text("endTime");
table.primary(["id"]);
table.unique(["id"]);
},
},
{
name: "t_image",
builder: (table) => {
@ -207,6 +195,36 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
},
initData: async (knex) => {},
},
{
name: "t_myTasks",
builder: (table) => {
table.integer("id").notNullable();
table.integer("projectId");
table.string("taskClass");
table.string("relatedObjects");
table.string("model");
table.text("describe");
table.string("state");
table.integer("startTime");
table.string("reason");
table.primary(["id"]);
table.unique(["id"]);
},
initData: async (knex) => {},
},
{
name: "t_artStyle",
builder: (table) => {
table.integer("id").notNullable();
table.string("name");
table.text("styles");
table.primary(["id"]);
table.unique(["id"]);
},
initData: async (knex) => {
await knex("t_artStyle").insert(artStyle.map((item, index) => ({ id: index + 1, name: item.name, styles: JSON.stringify(item.styles) })));
},
},
{
name: "t_videoConfig",
builder: (table) => {

View File

@ -0,0 +1,19 @@
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(),
}),
async (req, res) => {
const { name } = req.body;
const data = await u.db("t_artStyle").where("name", name).select("styles").first();
const styles = data?.styles ? JSON.parse(data.styles) : [];
res.status(200).send(success(styles));
},
);

View File

@ -123,8 +123,13 @@ export default router.post(
state: "生成中",
assetsId: id,
});
const apiConfig = await u.getPromptAi("assetsImage");
let taskClass = "";
if (type == "role") taskClass = "角色图生成";
if (type == "scene") taskClass = "场景图生成";
if (type == "props") taskClass = "道具图生成";
if (type == "storyboard") taskClass = "分镜图生成";
const apiConfig = await u.getPromptAi("assetsImage");
try {
const contentStr = await u.ai.image(
{
@ -133,6 +138,10 @@ export default router.post(
imageBase64: base64 ? [base64] : [],
size: "2K",
aspectRatio: "16:9",
taskClass: taskClass,
name: name,
describe: prompt,
projectId: projectId,
},
apiConfig,
);
@ -171,7 +180,6 @@ export default router.post(
filePath: imagePath,
type: insertType,
});
const path = await u.oss.getFileUrl(imagePath!);
// const state = await u.db("t_assets").where("id", id).select("state").first();

View File

@ -24,6 +24,10 @@ export default router.post(
imageBase64: [],
aspectRatio: "16:9",
size: "1K",
taskClass: "测试任务",
name: "测试图片生成",
describe: "测试语言模型生成图片",
projectId: 0,
},
{
model: modelName,

View File

@ -28,6 +28,10 @@ export default router.post(
aspectRatio: "16:9",
audio: false,
mode: "single",
taskClass: "测试视频生成",
name: "测试视频生成",
describe: "测试视频生成",
projectId: 0,
},
{
model: modelName,

View File

@ -9,6 +9,7 @@ const router = express.Router();
export default router.post(
"/",
validateFields({
projectType: z.string(),
name: z.string(),
intro: z.string(),
type: z.string(),
@ -16,9 +17,10 @@ export default router.post(
videoRatio: z.string(),
}),
async (req, res) => {
const { name, intro, type, artStyle, videoRatio } = req.body;
const { projectType, name, intro, type, artStyle, videoRatio } = req.body;
await u.db("t_project").insert({
projectType,
name,
intro,
type,
@ -29,5 +31,5 @@ export default router.post(
});
res.status(200).send(success({ message: "新增项目成功" }));
}
},
);

View File

@ -27,6 +27,7 @@ export default router.post(
await u.db("t_novel").where("projectId", id).delete();
await u.db("t_storyline").where("projectId", id).delete();
await u.db("t_outline").where("projectId", id).delete();
await u.db("t_myTasks").where("projectId", id).delete();
await u.db("t_script").where("projectId", id).delete();
await u.db("t_assets").where("projectId", id).delete();
@ -55,5 +56,5 @@ export default router.post(
}
res.status(200).send(success({ message: "删除项目成功" }));
}
},
);

View File

@ -14,17 +14,19 @@ export default router.post(
type: z.string().optional().nullable(),
artStyle: z.string().optional().nullable(),
videoRatio: z.string().optional().nullable(),
projectType: z.string().optional().nullable(),
}),
async (req, res) => {
const { id, intro, type, artStyle, videoRatio } = req.body;
const { id, intro, type, artStyle, videoRatio, projectType } = req.body;
await u.db("t_project").where("id", id).update({
intro,
type,
artStyle,
videoRatio,
projectType,
});
res.status(200).send(success({ message: "修改成功" }));
}
},
);

View File

@ -27,6 +27,10 @@ async function superResolutionAndSave(src: string, projectId: number, videoRatio
systemPrompt: "你的核心任务是将所给的图片超分到 1K ,不改变图片任何内容,仅改变分辨率",
prompt: "你的核心任务是将所给的图片超分到 1K ,不改变图片任何内容,仅改变分辨率",
imageBase64: [await urlToBase64(src)],
taskClass: "分镜图超分",
name: `分镜图超分-${v4()}`,
describe: `原始图片链接: ${src}`,
projectId,
},
apiConfig,
);

View File

@ -4,50 +4,45 @@ import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { number, z } from "zod";
const router = express.Router();
export default router.get(
export default router.post(
"/",
validateFields({
projectName: z.string(),
taskName: z.string(),
state: z.string(),
state: z.string().optional().nullable(),
taskClass: z.string().optional().nullable(),
page: z.number(),
limit: z.number(),
projectId: z.number(),
}),
async (req, res) => {
const { projectName, taskName, state, page = 1, limit = 10 }: any = req.query;
const { taskClass, state, page = 1, limit = 10, projectId }: any = req.body;
const offset = (page - 1) * limit;
const data = await u
.db("t_taskList")
.db("t_myTasks")
.where("projectId", projectId)
.andWhere((qb) => {
if (projectName) {
qb.andWhere("t_taskList.projectName", projectName);
}
if (taskName) {
qb.andWhere("t_taskList.name", taskName);
if (taskClass) {
qb.andWhere("t_myTasks.taskClass", taskClass);
}
if (state) {
qb.andWhere("t_taskList.state", state);
qb.andWhere("t_myTasks.state", state);
}
})
.select("*")
.offset(offset)
.limit(limit);
const totalQuery = (await u
.db("t_taskList")
.db("t_myTasks")
.where("projectId", projectId)
.andWhere((qb) => {
if (projectName) {
qb.andWhere("t_taskList.projectName", projectName);
}
if (taskName) {
qb.andWhere("t_taskList.name", taskName);
if (taskClass) {
qb.andWhere("t_myTasks.taskClass", taskClass);
}
if (state) {
qb.andWhere("t_taskList.state", state);
qb.andWhere("t_myTasks.state", state);
}
})
.count("* as total")
.first()) as any;
res.status(200).send(success({ data, total: totalQuery?.total }));
}
},
);

View File

@ -0,0 +1,17 @@
import express from "express";
import u from "@/utils";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { number, z } from "zod";
const router = express.Router();
export default router.post(
"/",
validateFields({
projectId: z.number(),
}),
async (req, res) => {
const data = await u.db("t_myTasks").where("projectId", req.body.projectId).select("taskClass").groupBy("taskClass");
res.status(200).send(success(data));
},
);

View File

@ -177,6 +177,10 @@ ${prompt}
resolution: resolution as any,
audio: audioEnabled,
mode: mode as any,
taskClass: "视频生成",
name: `视频生成-${videoId}`,
describe: `视频生成,时长${duration}秒,分辨率${resolution}`,
projectId,
},
{
baseURL: aiConfigData?.baseUrl!,

View File

@ -1,4 +1,4 @@
// @db-hash bdfbb3a599198f1e91b2e5d7930ccd96
// @db-hash 8171d26b6ac1f411a6ec46a0381b821a
//该文件由脚本自动生成,请勿手动修改
export interface t_aiModelMap {
@ -7,8 +7,12 @@ export interface t_aiModelMap {
'key'?: string | null;
'name'?: string | null;
}
export interface t_artStyle {
'id'?: number;
'name'?: string | null;
'styles'?: string | null;
}
export interface t_assets {
'dialogue'?: string | null;
'duration'?: string | null;
'episode'?: string | null;
'filePath'?: string | null;
@ -60,6 +64,17 @@ export interface t_imageModel {
'model'?: string | null;
'type'?: string | null;
}
export interface t_myTasks {
'describe'?: string | null;
'id'?: number;
'model'?: string | null;
'projectId'?: number | null;
'reason'?: string | null;
'relatedObjects'?: string | null;
'startTime'?: number | null;
'state'?: string | null;
'taskClass'?: string | null;
}
export interface t_novel {
'chapter'?: string | null;
'chapterData'?: string | null;
@ -81,6 +96,7 @@ export interface t_project {
'id'?: number | null;
'intro'?: string | null;
'name'?: string | null;
'projectType'?: string | null;
'type'?: string | null;
'userId'?: number | null;
'videoRatio'?: string | null;
@ -116,15 +132,6 @@ export interface t_storyline {
'novelIds'?: string | null;
'projectId'?: number | null;
}
export interface t_taskList {
'endTime'?: string | null;
'id'?: number;
'name'?: string | null;
'projectName'?: number | null;
'prompt'?: string | null;
'startTime'?: string | null;
'state'?: string | null;
}
export interface t_textModel {
'id'?: number;
'image'?: number | null;
@ -158,7 +165,6 @@ export interface t_videoConfig {
'aiConfigId'?: number | null;
'audioEnabled'?: number | null;
'createTime'?: number | null;
'dialogue'?: string | null;
'duration'?: number | null;
'endFrame'?: string | null;
'id'?: number;
@ -185,11 +191,13 @@ export interface t_videoModel {
export interface DB {
"t_aiModelMap": t_aiModelMap;
"t_artStyle": t_artStyle;
"t_assets": t_assets;
"t_chatHistory": t_chatHistory;
"t_config": t_config;
"t_image": t_image;
"t_imageModel": t_imageModel;
"t_myTasks": t_myTasks;
"t_novel": t_novel;
"t_outline": t_outline;
"t_project": t_project;
@ -197,7 +205,6 @@ export interface DB {
"t_script": t_script;
"t_setting": t_setting;
"t_storyline": t_storyline;
"t_taskList": t_taskList;
"t_textModel": t_textModel;
"t_user": t_user;
"t_video": t_video;

View File

@ -12,6 +12,7 @@ import other from "./owned/other";
import gemini from "./owned/gemini";
import modelScope from "./owned/modelScope";
import grsai from "./owned/grsai";
import { tr } from "zod/locales";
const urlToBase64 = async (url: string): Promise<string> => {
const res = await axios.get(url, { responseType: "arraybuffer" });
@ -29,20 +30,31 @@ const modelInstance = {
// apimart: apimart,
modelScope,
other,
grsai
grsai,
} as const;
export default async (input: ImageConfig, config: AIConfig) => {
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("不支持的模型");
// }
//添加到任务中心
const [taskId] = await u.db("t_myTasks").insert({
taskClass: input.taskClass,
relatedObjects: input.name,
model: config?.model ? config.model : "未知模型",
describe: input.describe ? input.describe : "无",
state: "进行中",
startTime: Date.now(),
projectId: input.projectId,
});
// 补充图片的 base64 内容类型字符串
if (input.imageBase64 && input.imageBase64.length > 0) {
input.imageBase64 = input.imageBase64.map((img) => {
@ -66,9 +78,19 @@ export default async (input: ImageConfig, config: AIConfig) => {
return `data:image/png;base64,${img}`;
});
}
let imageUrl = await manufacturerFn(input, { model, apiKey, baseURL });
if (!input.resType) input.resType = "b64";
if (input.resType === "b64" && imageUrl.startsWith("http")) imageUrl = await urlToBase64(imageUrl);
return imageUrl;
try {
let imageUrl = await manufacturerFn(input, { model, apiKey, baseURL });
if (!input.resType) input.resType = "b64";
if (input.resType === "b64" && imageUrl.startsWith("http")) imageUrl = await urlToBase64(imageUrl);
await u.db("t_myTasks").where("id", taskId).update({
state: "已完成",
});
return imageUrl;
} catch (error: any) {
await u.db("t_myTasks").where("id", taskId).update({
state: "生成失败",
reason: error.message,
});
throw error;
}
};

View File

@ -5,6 +5,10 @@ interface ImageConfig {
size: "1K" | "2K" | "4K";
aspectRatio: string;
resType?: "url" | "b64";
taskClass: string;
name: string;
describe: string;
projectId: number;
}
interface AIConfig {

View File

@ -79,7 +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;

View File

@ -21,7 +21,7 @@ const modelInstance = {
runninghub: runninghub,
apimart: apimart,
other: other,
grsai:grsai
grsai: grsai,
} as const;
export default async (input: VideoConfig, config?: AIConfig) => {
@ -32,7 +32,16 @@ export default async (input: VideoConfig, config?: AIConfig) => {
if (!manufacturerFn) if (!manufacturerFn) throw new Error("不支持的视频厂商");
// const owned = modelList.find((m) => m.model === model);
// if (!owned) throw new Error("不支持的模型");
//添加到任务中心
const [taskId] = await u.db("t_myTasks").insert({
taskClass: input.taskClass,
relatedObjects: input.name,
model: config?.model ? config.model : "未知模型",
describe: input.describe ? input.describe : "无",
state: "进行中",
startTime: Date.now(),
projectId: input.projectId,
});
// 补充图片的 base64 内容类型字符串
if (input.imageBase64 && input.imageBase64.length > 0) {
input.imageBase64 = input.imageBase64.map((img) => {
@ -59,9 +68,20 @@ export default async (input: VideoConfig, config?: AIConfig) => {
let videoUrl = await manufacturerFn(input, { model, apiKey, baseURL });
if (videoUrl) {
const response = await axios.get(videoUrl, { responseType: "stream" });
await u.oss.writeFile(input.savePath, response.data);
return input.savePath;
try {
const response = await axios.get(videoUrl, { responseType: "stream" });
await u.oss.writeFile(input.savePath, response.data);
await u.db("t_myTasks").where("id", taskId).update({
state: "已完成",
});
return input.savePath;
} catch (err: any) {
await u.db("t_myTasks").where("id", taskId).update({
state: "生成失败",
reason: err.message,
});
return videoUrl;
}
}
return videoUrl;
};

View File

@ -7,6 +7,10 @@ interface VideoConfig {
imageBase64?: string[];
audio?: boolean;
mode: "startEnd" | "multi" | "single" | "text";
taskClass: string;
name: string;
projectId: number;
describe?: string;
}
interface AIConfig {

View File

@ -88,6 +88,10 @@ export default async (images: Record<string, string>, directive: string, project
imageBase64: base64Images,
aspectRatio: aspectRatio ? aspectRatio : "16:9",
size: "1K",
taskClass: "图片编辑",
name: `图片编辑-${uuid()}`,
describe: `编辑指令: ${directive}`,
projectId,
},
apiConfig,
);

File diff suppressed because it is too large Load Diff