108版本
This commit is contained in:
parent
00abf9ae2f
commit
b719b38152
155
.github/workflows/release.yml
vendored
155
.github/workflows/release.yml
vendored
@ -1,155 +0,0 @@
|
||||
name: Build and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version number (e.g., 1.0.0)"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
runs-on: windows-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 Windows installer
|
||||
run: yarn dist:win
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-builds
|
||||
path: |
|
||||
dist/*.exe
|
||||
dist/*.zip
|
||||
retention-days: 30
|
||||
|
||||
build-macos:
|
||||
runs-on: macos-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 macOS installer
|
||||
run: yarn dist:mac
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-builds
|
||||
path: |
|
||||
dist/*.dmg
|
||||
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, build-linux]
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download Windows artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: windows-builds
|
||||
path: dist
|
||||
|
||||
- name: Download macOS artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
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:
|
||||
name: ToonFlow ${{ github.ref_name }}
|
||||
draft: false
|
||||
prerelease: ${{ contains(github.ref_name, 'beta') || contains(github.ref_name, 'alpha') }}
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
dist/*.exe
|
||||
dist/*.zip
|
||||
dist/*.dmg
|
||||
dist/*.AppImage
|
||||
dist/*.deb
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -1,7 +1,7 @@
|
||||
.devtools
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
.vscode
|
||||
# output
|
||||
out
|
||||
dist
|
||||
@ -45,4 +45,8 @@ db.sqlite-wal
|
||||
|
||||
web/*
|
||||
|
||||
.devtools
|
||||
db2.sqlite
|
||||
|
||||
.devtools
|
||||
data/oss/*
|
||||
data/test.sqlite
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
import { ChatOpenAI, ChatOpenAIFields } from "@langchain/openai";
|
||||
|
||||
export const openAI = (config: ChatOpenAIFields = {}) => {
|
||||
return new ChatOpenAI({
|
||||
modelName: "gpt-4.1",
|
||||
temperature: 1,
|
||||
configuration: {
|
||||
apiKey: process.env.AI_OPENAI_KEY,
|
||||
baseURL: process.env.AI_OPENAI_URL,
|
||||
},
|
||||
...config,
|
||||
});
|
||||
};
|
||||
|
||||
export const doubao = (config: ChatOpenAIFields = {}) => {
|
||||
return new ChatOpenAI({
|
||||
model: "doubao-seed-1-6-flash-250828",
|
||||
temperature: 1,
|
||||
configuration: {
|
||||
apiKey: process.env.AI_TIKTOK_KEY,
|
||||
baseURL: process.env.AI_TIKTOK_URL,
|
||||
},
|
||||
...config,
|
||||
});
|
||||
};
|
||||
|
||||
export const deepseek = (config: ChatOpenAIFields = {}) =>
|
||||
new ChatOpenAI({
|
||||
model: "DeepSeek-V3.2",
|
||||
temperature: 1,
|
||||
configuration: {
|
||||
apiKey: process.env.AI_DEEPSEEK_KEY,
|
||||
baseURL: process.env.AI_DEEPSEEK_URL,
|
||||
},
|
||||
...config,
|
||||
});
|
||||
@ -1,769 +0,0 @@
|
||||
// @/agents/outlineScript.ts
|
||||
import u from "@/utils";
|
||||
import { createAgent } from "langchain";
|
||||
import { EventEmitter } from "events";
|
||||
import { openAI } from "@/agents/models";
|
||||
import { z } from "zod";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import type { DB } from "@/types/database";
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
type AgentType = "AI1" | "AI2" | "director";
|
||||
type AssetType = "角色" | "道具" | "场景";
|
||||
type RefreshEvent = "storyline" | "outline" | "assets";
|
||||
|
||||
interface AssetItem {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface EpisodeData {
|
||||
episodeIndex: number;
|
||||
title: string;
|
||||
chapterRange: number[];
|
||||
scenes: AssetItem[]; // 按 outline 出场顺序排列
|
||||
characters: AssetItem[]; // 按 outline 出场顺序排列
|
||||
props: AssetItem[]; // 按 outline 出场顺序排列
|
||||
coreConflict: string;
|
||||
outline: string; // 最高优先级,剧本生成的唯一权威
|
||||
openingHook: string; // outline 第一句话的视觉化,开篇第一个镜头
|
||||
keyEvents: string[]; // 4个元素:[起, 承, 转, 合],严格按 outline 顺序
|
||||
emotionalCurve: string; // 对应 keyEvents 各阶段
|
||||
visualHighlights: string[]; // 按 outline 顺序排列的标志性镜头
|
||||
endingHook: string; // outline 之后的悬念延伸
|
||||
classicQuotes: string[];
|
||||
}
|
||||
|
||||
// ==================== Schema 定义 ====================
|
||||
|
||||
const sceneItemSchema = z.object({
|
||||
name: z.string().describe("场景名称,如'五星酒店宴会厅'、'老旧出租屋'"),
|
||||
description: z.string().describe("环境描写:空间结构、光线氛围、装饰陈设、环境细节"),
|
||||
});
|
||||
|
||||
const characterItemSchema = z.object({
|
||||
name: z.string().describe("角色姓名(必须是具体人名,禁止'众人'、'群众'等集合描述)"),
|
||||
description: z.string().describe("人设样貌:年龄体态、五官特征、发型妆容、服装配饰、气质神态"),
|
||||
});
|
||||
|
||||
const propItemSchema = z.object({
|
||||
name: z.string().describe("道具名称"),
|
||||
description: z.string().describe("样式描写:材质质感、颜色图案、形状尺寸、磨损痕迹、特殊标记"),
|
||||
});
|
||||
|
||||
const episodeSchema = z.object({
|
||||
episodeIndex: z.number().describe("集数索引,从1开始递增"),
|
||||
title: z.string().describe("8字内标题,疑问/感叹句,含情绪爆点"),
|
||||
chapterRange: z.array(z.number()).describe("关联章节号数组"),
|
||||
scenes: z.array(sceneItemSchema).describe("场景列表,按 outline 出场顺序排列"),
|
||||
characters: z.array(characterItemSchema).describe("角色列表,按 outline 出场顺序排列,必须是独立个体"),
|
||||
props: z.array(propItemSchema).describe("道具列表,按 outline 出场顺序排列,至少3个"),
|
||||
coreConflict: z.string().describe("核心矛盾:A想要X vs B阻碍X"),
|
||||
outline: z.string().describe("100-300字剧情主干,最高优先级,剧本生成的唯一权威,按时间顺序完整叙述"),
|
||||
openingHook: z.string().describe("开场镜头:outline 第一句话的视觉化,必须作为剧本第一个镜头"),
|
||||
keyEvents: z.array(z.string()).length(4).describe("4个元素的数组:[起, 承, 转, 合],严格按 outline 顺序从中提取"),
|
||||
emotionalCurve: z.string().describe("情绪曲线,如:2(压抑)→5(反抗)→9(爆发)→3(余波),对应 keyEvents 各阶段"),
|
||||
visualHighlights: z.array(z.string()).describe("3-5个标志性镜头,按 outline 叙事顺序排列"),
|
||||
endingHook: z.string().describe("结尾悬念:outline 之后的延伸,勾引下集"),
|
||||
classicQuotes: z.array(z.string()).describe("1-2句金句,每句≤15字,必须从原文提取"),
|
||||
});
|
||||
|
||||
// ==================== 常量配置 ====================
|
||||
|
||||
// ==================== 主类 ====================
|
||||
|
||||
export default class OutlineScript {
|
||||
private readonly projectId: number;
|
||||
readonly emitter = new EventEmitter();
|
||||
history: Array<[string, string]> = [];
|
||||
novelChapters: DB["t_novel"][] = [];
|
||||
|
||||
modelName = "gpt-4.1";
|
||||
apiKey = "";
|
||||
baseURL = "";
|
||||
|
||||
constructor(projectId: number) {
|
||||
this.projectId = projectId;
|
||||
}
|
||||
|
||||
// ==================== 公共方法 ====================
|
||||
|
||||
get events() {
|
||||
return this.emitter;
|
||||
}
|
||||
|
||||
setNovel(chapters: DB["t_novel"][]) {
|
||||
this.novelChapters = chapters;
|
||||
}
|
||||
|
||||
// ==================== 私有工具方法 ====================
|
||||
|
||||
private emit(event: string, data?: any) {
|
||||
this.emitter.emit(event, data);
|
||||
}
|
||||
|
||||
private refresh(type: RefreshEvent) {
|
||||
this.emit("refresh", type);
|
||||
}
|
||||
|
||||
private log(action: string, detail?: string) {
|
||||
const msg = detail ? `${action}: ${detail}` : action;
|
||||
console.log(`\n[${new Date().toLocaleTimeString()}] ${msg}\n`);
|
||||
}
|
||||
|
||||
private safeParseJson<T>(str: string, fallback: T): T {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private uniqueByName<T extends { name: string }>(items: T[]): T[] {
|
||||
return Array.from(new Map(items.map((item) => [item.name, item])).values());
|
||||
}
|
||||
|
||||
// ==================== 数据库操作 ====================
|
||||
|
||||
private async getProjectInfo(): Promise<any> {
|
||||
return u.db("t_project").where({ id: this.projectId }).first();
|
||||
}
|
||||
|
||||
private async getNovelInfo(asString = false): Promise<any> {
|
||||
const info = await this.getProjectInfo();
|
||||
if (!info) return asString ? "未查询到项目信息" : null;
|
||||
|
||||
if (asString) {
|
||||
const fields = [
|
||||
`小说名称: ${info.name}`,
|
||||
`小说简介: ${info.intro}`,
|
||||
`小说类型: ${info.type}`,
|
||||
`目标短剧类型: ${info.artStyle}`,
|
||||
`短剧画幅: ${info.videoRatio}`,
|
||||
];
|
||||
return fields.join("\n");
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
// ==================== 故事线操作 ====================
|
||||
|
||||
private async findStoryline() {
|
||||
return u.db("t_storyline").where({ projectId: this.projectId }).first();
|
||||
}
|
||||
|
||||
private async upsertStorylineContent(content: string) {
|
||||
const existing = await this.findStoryline();
|
||||
if (existing) {
|
||||
await u.db("t_storyline").where({ projectId: this.projectId }).update({ content });
|
||||
} else {
|
||||
await u.db("t_storyline").insert({ projectId: this.projectId, content });
|
||||
}
|
||||
this.refresh("storyline");
|
||||
}
|
||||
|
||||
private async deleteStorylineContent() {
|
||||
const deleted = await u.db("t_storyline").where({ projectId: this.projectId }).del();
|
||||
this.refresh("storyline");
|
||||
return deleted;
|
||||
}
|
||||
|
||||
// ==================== 大纲操作 ====================
|
||||
|
||||
private async findOutlines() {
|
||||
return u.db("t_outline").where({ projectId: this.projectId }).orderBy("episode", "asc");
|
||||
}
|
||||
|
||||
private async findOutlineById(id: number) {
|
||||
return u.db("t_outline").where({ id, projectId: this.projectId }).first();
|
||||
}
|
||||
|
||||
private async getMaxEpisode(): Promise<number> {
|
||||
const result: any = await u.db("t_outline").where({ projectId: this.projectId }).max("episode as max").first();
|
||||
return result?.max ?? 0;
|
||||
}
|
||||
|
||||
private async clearOutlinesAndScripts() {
|
||||
const outlines = await u.db("t_outline").select("id").where({ projectId: this.projectId });
|
||||
if (outlines.length === 0) return 0;
|
||||
|
||||
const outlineIds = outlines.map((o) => o.id);
|
||||
await u.db("t_script").whereIn("outlineId", outlineIds).del();
|
||||
await u.db("t_outline").where({ projectId: this.projectId }).del();
|
||||
|
||||
return outlines.length;
|
||||
}
|
||||
|
||||
private async insertOutlines(episodes: EpisodeData[], startEpisode: number) {
|
||||
const insertList = episodes.map((ep, idx) => ({
|
||||
projectId: this.projectId,
|
||||
data: JSON.stringify({ ...ep, episodeIndex: startEpisode + idx }),
|
||||
episode: startEpisode + idx,
|
||||
}));
|
||||
|
||||
await u.db("t_outline").insert(insertList);
|
||||
return insertList.length;
|
||||
}
|
||||
|
||||
private async createEmptyScripts(outlineIds: Array<{ id: number; data: string }>) {
|
||||
const scripts = outlineIds.map((item) => {
|
||||
const data = this.safeParseJson<Partial<EpisodeData>>(item.data, {});
|
||||
return {
|
||||
name: `第${data.episodeIndex ?? ""}集`,
|
||||
content: "",
|
||||
projectId: this.projectId,
|
||||
outlineId: item.id,
|
||||
};
|
||||
});
|
||||
|
||||
if (scripts.length > 0) {
|
||||
await u.db("t_script").insert(scripts);
|
||||
}
|
||||
return scripts.length;
|
||||
}
|
||||
|
||||
private async saveOutlineData(episodes: EpisodeData[], overwrite: boolean, startEpisode?: number) {
|
||||
if (overwrite) {
|
||||
const cleared = await this.clearOutlinesAndScripts();
|
||||
if (cleared > 0) {
|
||||
this.log("清理旧数据", `删除了 ${cleared} 条大纲及关联剧本`);
|
||||
}
|
||||
}
|
||||
|
||||
const actualStart = overwrite ? 1 : startEpisode ?? (await this.getMaxEpisode()) + 1;
|
||||
const insertedCount = await this.insertOutlines(episodes, actualStart);
|
||||
|
||||
const newOutlines = await u
|
||||
.db("t_outline")
|
||||
.select("id", "data")
|
||||
.where({ projectId: this.projectId })
|
||||
.orderBy("episode", "desc")
|
||||
.limit(insertedCount);
|
||||
|
||||
const scriptCount = await this.createEmptyScripts(newOutlines as Array<{ id: number; data: string }>);
|
||||
|
||||
this.refresh("outline");
|
||||
return { insertedCount, scriptCount };
|
||||
}
|
||||
|
||||
private async updateOutlineData(id: number, data: EpisodeData) {
|
||||
const existing = await this.findOutlineById(id);
|
||||
if (!existing) return false;
|
||||
|
||||
await u
|
||||
.db("t_outline")
|
||||
.where({ id })
|
||||
.update({ data: JSON.stringify(data) });
|
||||
this.refresh("outline");
|
||||
return true;
|
||||
}
|
||||
|
||||
private async deleteOutlineData(ids: number[]) {
|
||||
const results = await Promise.allSettled(ids.map((id) => u.deleteOutline(id, this.projectId)));
|
||||
this.refresh("outline");
|
||||
return results;
|
||||
}
|
||||
|
||||
private formatOutlineDetail(ep: any): string {
|
||||
const formatList = (items: any[], formatter: (item: any) => string) =>
|
||||
items?.map((item, i) => ` ${i + 1}. ${formatter(item)}`).join("\n") || " 无";
|
||||
|
||||
// keyEvents 按顺序显示:起、承、转、合
|
||||
const keyEventsLabels = ["起", "承", "转", "合"];
|
||||
const formatKeyEvents = (events: string[]) => events?.map((e, i) => ` 【${keyEventsLabels[i] || i + 1}】${e}`).join("\n") || " 无";
|
||||
|
||||
return `
|
||||
大纲ID: ${ep.id}
|
||||
第 ${ep.episodeIndex} 集: ${ep.title || ""}
|
||||
${"=".repeat(50)}
|
||||
章节范围: ${ep.chapterRange?.join(", ") || ""}
|
||||
核心矛盾: ${ep.coreConflict || ""}
|
||||
|
||||
【剧情主干】(最高优先级,剧本生成的唯一权威):
|
||||
${ep.outline || "无"}
|
||||
|
||||
【开场镜头】(必须作为剧本第一个镜头):
|
||||
${ep.openingHook || "无"}
|
||||
|
||||
【剧情节点】(严格按顺序:起→承→转→合):
|
||||
${formatKeyEvents(ep.keyEvents)}
|
||||
|
||||
情绪曲线: ${ep.emotionalCurve || ""}
|
||||
|
||||
【视觉重点】(按剧情主干顺序排列):
|
||||
${formatList(ep.visualHighlights, (v) => v)}
|
||||
|
||||
【结尾悬念】:
|
||||
${ep.endingHook || "无"}
|
||||
|
||||
【经典台词】:
|
||||
${formatList(ep.classicQuotes, (q) => q)}
|
||||
|
||||
角色(按出场顺序): ${ep.characters?.map((c: AssetItem) => `${c.name}(${c.description})`).join("; ") || "无"}
|
||||
场景(按出场顺序): ${ep.scenes?.map((s: AssetItem) => `${s.name}(${s.description})`).join("; ") || "无"}
|
||||
道具(按出场顺序): ${ep.props?.map((p: AssetItem) => `${p.name}(${p.description})`).join("; ") || "无"}`;
|
||||
}
|
||||
|
||||
private async getOutlineText(simplified: boolean): Promise<string> {
|
||||
const records = await this.findOutlines();
|
||||
|
||||
if (!records.length) return "当前项目暂无大纲";
|
||||
|
||||
const episodes = records.map((r) => ({
|
||||
id: r.id,
|
||||
episode: r.episode,
|
||||
...this.safeParseJson<Partial<EpisodeData>>(r.data ?? "{}", {}),
|
||||
}));
|
||||
|
||||
if (simplified) {
|
||||
const list = episodes.map((ep) => `第 ${ep.episodeIndex ?? ep.episode} 集 (id=${ep.id})`).join("\n");
|
||||
return `项目大纲 (共 ${episodes.length} 集):\n${list}`;
|
||||
}
|
||||
|
||||
const details = episodes.map((ep) => this.formatOutlineDetail(ep)).join("\n");
|
||||
return `项目大纲 (共 ${episodes.length} 集)\n\n${details}`;
|
||||
}
|
||||
|
||||
// ==================== 资产操作 ====================
|
||||
|
||||
private async findAssetByTypeAndName(type: AssetType, name: string) {
|
||||
return u.db("t_assets").where({ projectId: this.projectId, type, name }).first();
|
||||
}
|
||||
|
||||
private async upsertAsset(type: AssetType, item: AssetItem): Promise<"inserted" | "updated" | "skipped"> {
|
||||
const existing = await this.findAssetByTypeAndName(type, item.name);
|
||||
|
||||
if (!existing) {
|
||||
await u.db("t_assets").insert({
|
||||
projectId: this.projectId,
|
||||
type,
|
||||
name: item.name,
|
||||
intro: item.description,
|
||||
prompt: item.description,
|
||||
});
|
||||
return "inserted";
|
||||
}
|
||||
|
||||
if (existing.intro !== item.description) {
|
||||
await u.db("t_assets").where({ id: existing.id }).update({
|
||||
intro: item.description,
|
||||
prompt: item.description,
|
||||
});
|
||||
return "updated";
|
||||
}
|
||||
|
||||
return "skipped";
|
||||
}
|
||||
|
||||
private extractAssetsFromOutlines(outlines: Array<{ data?: string | null | undefined }>): {
|
||||
characters: AssetItem[];
|
||||
props: AssetItem[];
|
||||
scenes: AssetItem[];
|
||||
} {
|
||||
const result = { characters: [] as AssetItem[], props: [] as AssetItem[], scenes: [] as AssetItem[] };
|
||||
|
||||
for (const outline of outlines) {
|
||||
const data = this.safeParseJson<Partial<EpisodeData>>(outline.data ?? "{}", {});
|
||||
if (data.characters) result.characters.push(...data.characters);
|
||||
if (data.props) result.props.push(...data.props);
|
||||
if (data.scenes) result.scenes.push(...data.scenes);
|
||||
}
|
||||
|
||||
return {
|
||||
characters: this.uniqueByName(result.characters),
|
||||
props: this.uniqueByName(result.props),
|
||||
scenes: this.uniqueByName(result.scenes),
|
||||
};
|
||||
}
|
||||
|
||||
private async generateAssetsFromOutlines() {
|
||||
const outlines = await u.db("t_outline").select("data").where({ projectId: this.projectId });
|
||||
|
||||
if (!outlines.length) return { inserted: 0, updated: 0, skipped: 0 };
|
||||
|
||||
const { characters, props, scenes } = this.extractAssetsFromOutlines(outlines);
|
||||
|
||||
// 只做新增和更新,不做删除
|
||||
const stats = { inserted: 0, updated: 0, skipped: 0 };
|
||||
|
||||
const processItems = async (items: AssetItem[], type: AssetType) => {
|
||||
for (const item of items) {
|
||||
const result = await this.upsertAsset(type, item);
|
||||
stats[result]++;
|
||||
}
|
||||
};
|
||||
|
||||
await processItems(characters, "角色");
|
||||
await processItems(props, "道具");
|
||||
await processItems(scenes, "场景");
|
||||
|
||||
this.refresh("assets");
|
||||
return { ...stats };
|
||||
}
|
||||
|
||||
// ==================== Tool 定义:故事线 ====================
|
||||
|
||||
getStoryline = tool(
|
||||
async () => {
|
||||
this.log("获取故事线");
|
||||
const storyline = await this.findStoryline();
|
||||
return storyline?.content ?? "当前项目暂无故事线";
|
||||
},
|
||||
{
|
||||
name: "getStoryline",
|
||||
description: "获取当前项目的故事线内容",
|
||||
schema: z.object({}),
|
||||
verboseParsingErrors: true,
|
||||
},
|
||||
);
|
||||
|
||||
saveStoryline = tool(
|
||||
async ({ content }) => {
|
||||
this.log("保存故事线");
|
||||
await this.upsertStorylineContent(content);
|
||||
return "故事线保存成功";
|
||||
},
|
||||
{
|
||||
name: "saveStoryline",
|
||||
description: "保存或更新当前项目的故事线,会覆盖已有内容",
|
||||
schema: z.object({
|
||||
content: z.string().describe("故事线完整内容"),
|
||||
}),
|
||||
verboseParsingErrors: true,
|
||||
},
|
||||
);
|
||||
|
||||
deleteStoryline = tool(
|
||||
async () => {
|
||||
this.log("删除故事线");
|
||||
const deleted = await this.deleteStorylineContent();
|
||||
return deleted > 0 ? "故事线删除成功" : "当前项目没有故事线";
|
||||
},
|
||||
{
|
||||
name: "deleteStoryline",
|
||||
description: "删除当前项目的故事线",
|
||||
schema: z.object({}),
|
||||
verboseParsingErrors: true,
|
||||
},
|
||||
);
|
||||
|
||||
// ==================== Tool 定义:大纲 ====================
|
||||
|
||||
getOutline = tool(
|
||||
async ({ simplified = false }) => {
|
||||
this.log("获取大纲", `简化模式: ${simplified}`);
|
||||
return this.getOutlineText(simplified);
|
||||
},
|
||||
{
|
||||
name: "getOutline",
|
||||
description: "获取项目大纲。simplified=true返回简化列表,false返回完整内容",
|
||||
schema: z.object({
|
||||
simplified: z.boolean().default(false).describe("是否返回简化版本"),
|
||||
}),
|
||||
verboseParsingErrors: true,
|
||||
},
|
||||
);
|
||||
|
||||
saveOutline = tool(
|
||||
async ({ episodes, overwrite = true, startEpisode }) => {
|
||||
this.log("保存大纲", `覆盖模式: ${overwrite}, 集数: ${episodes.length}`);
|
||||
const { insertedCount, scriptCount } = await this.saveOutlineData(episodes as EpisodeData[], overwrite, startEpisode);
|
||||
return `大纲保存成功:插入 ${insertedCount} 集大纲,创建 ${scriptCount} 个剧本记录`;
|
||||
},
|
||||
{
|
||||
name: "saveOutline",
|
||||
description: "保存大纲数据。overwrite=true会清空现有大纲后写入,false则追加到末尾",
|
||||
schema: z.object({
|
||||
episodes: z.array(episodeSchema).min(1).describe("大纲数据数组"),
|
||||
overwrite: z.boolean().default(true).describe("是否覆盖现有大纲"),
|
||||
startEpisode: z.number().optional().describe("追加模式下的起始集数(不填则自动递增)"),
|
||||
}),
|
||||
verboseParsingErrors: true,
|
||||
},
|
||||
);
|
||||
|
||||
updateOutline = tool(
|
||||
async ({ id, data }) => {
|
||||
this.log("更新大纲", `ID: ${id}`);
|
||||
const success = await this.updateOutlineData(id, data as EpisodeData);
|
||||
return success ? `大纲ID ${id} 更新成功` : `未找到大纲ID: ${id}`;
|
||||
},
|
||||
{
|
||||
name: "updateOutline",
|
||||
description: "更新指定ID的单集大纲内容",
|
||||
schema: z.object({
|
||||
id: z.number().describe("大纲ID"),
|
||||
data: episodeSchema.describe("更新后的大纲数据"),
|
||||
}),
|
||||
verboseParsingErrors: true,
|
||||
},
|
||||
);
|
||||
|
||||
deleteOutline = tool(
|
||||
async ({ ids }) => {
|
||||
this.log("删除大纲", `IDs: ${ids.join(", ")}`);
|
||||
const results = await this.deleteOutlineData(ids);
|
||||
const summary = results.map((r, i) => `ID ${ids[i]}: ${r.status === "fulfilled" ? "成功" : "失败"}`).join(", ");
|
||||
return `删除结果: ${summary}`;
|
||||
},
|
||||
{
|
||||
name: "deleteOutline",
|
||||
description: "根据大纲ID删除指定大纲及关联数据",
|
||||
schema: z.object({
|
||||
ids: z.array(z.number()).min(1).describe("要删除的大纲ID数组"),
|
||||
}),
|
||||
verboseParsingErrors: true,
|
||||
},
|
||||
);
|
||||
|
||||
// ==================== Tool 定义:章节 ====================
|
||||
|
||||
getChapter = tool(
|
||||
async ({ chapterNumbers }) => {
|
||||
this.log("获取章节", `章节号: ${chapterNumbers.join(", ")}`);
|
||||
|
||||
const results = await Promise.all(
|
||||
chapterNumbers.map(async (num) => {
|
||||
const chapter = await u
|
||||
.db("t_novel")
|
||||
.where({ projectId: this.projectId, chapterIndex: num })
|
||||
.select("chapterData", "chapterIndex", "chapter")
|
||||
.first();
|
||||
|
||||
if (chapter) {
|
||||
return `\n【第${chapter.chapterIndex}章 ${chapter.chapter || ""}】\n${chapter.chapterData}`;
|
||||
}
|
||||
return `\n【第${num}章】未找到`;
|
||||
}),
|
||||
);
|
||||
|
||||
return results.join("\n\n---\n");
|
||||
},
|
||||
{
|
||||
name: "getChapter",
|
||||
description: "根据章节编号获取小说章节的完整原文内容,支持批量获取",
|
||||
schema: z.object({
|
||||
chapterNumbers: z.array(z.number()).min(1).describe("章节编号数组"),
|
||||
}),
|
||||
verboseParsingErrors: true,
|
||||
},
|
||||
);
|
||||
|
||||
// ==================== Tool 定义:资产 ====================
|
||||
|
||||
generateAssets = tool(
|
||||
async () => {
|
||||
this.log("生成资产");
|
||||
const stats = await this.generateAssetsFromOutlines();
|
||||
|
||||
if (stats.inserted === 0 && stats.updated === 0 && stats.skipped === 0) {
|
||||
return "当前项目没有大纲数据,无法生成资产";
|
||||
}
|
||||
|
||||
return `资产生成完成:新增 ${stats.inserted},更新 ${stats.updated},保持 ${stats.skipped}`;
|
||||
},
|
||||
{
|
||||
name: "generateAssets",
|
||||
description: "从当前项目的所有大纲中提取并生成角色、道具、场景资产,自动去重并清理冗余",
|
||||
schema: z.object({}),
|
||||
verboseParsingErrors: true,
|
||||
},
|
||||
);
|
||||
|
||||
// ==================== 上下文构建 ====================
|
||||
|
||||
private getChapterContext(): string {
|
||||
if (!this.novelChapters.length) return "无章节数据";
|
||||
return this.novelChapters.map((c) => `章节号:${c.chapterIndex},分卷:${c.reel},章节名:${c.chapter}`).join("\n");
|
||||
}
|
||||
|
||||
private async buildEnvironmentContext(): Promise<string> {
|
||||
const [novelInfo, storyline, outlineCount] = await Promise.all([
|
||||
this.getNovelInfo(true),
|
||||
this.findStoryline(),
|
||||
u.db("t_outline").where({ projectId: this.projectId }).count("id as count").first() as any,
|
||||
]);
|
||||
|
||||
return `<环境信息>
|
||||
项目ID: ${this.projectId}
|
||||
系统时间: ${new Date().toLocaleString()}
|
||||
|
||||
${novelInfo}
|
||||
|
||||
已加载章节列表:
|
||||
${this.getChapterContext()}
|
||||
|
||||
故事线状态: ${storyline ? "已生成" : "未生成"}
|
||||
大纲状态: 共 ${outlineCount?.count ?? 0} 集
|
||||
|
||||
可用工具:
|
||||
- getChapter: 获取章节原文
|
||||
- getStoryline/saveStoryline/deleteStoryline: 故事线操作
|
||||
- getOutline/saveOutline/updateOutline/deleteOutline: 大纲操作
|
||||
- generateAssets: 从大纲生成资产
|
||||
</环境信息>`;
|
||||
}
|
||||
|
||||
private buildConversationHistory(): string {
|
||||
if (!this.history.length) return "无对话历史";
|
||||
return this.history.map(([role, content]) => `${role}: ${content}`).join("\n\n");
|
||||
}
|
||||
|
||||
private async buildFullContext(task: string): Promise<string> {
|
||||
const env = await this.buildEnvironmentContext();
|
||||
const history = this.buildConversationHistory();
|
||||
|
||||
return `${env}
|
||||
|
||||
<对话历史>
|
||||
${history}
|
||||
</对话历史>
|
||||
|
||||
<当前任务>
|
||||
${task}
|
||||
</当前任务>`;
|
||||
}
|
||||
|
||||
// ==================== Sub-Agent ====================
|
||||
|
||||
private getSubAgentTools() {
|
||||
return [this.getChapter, this.getStoryline, this.saveStoryline, this.getOutline, this.saveOutline, this.updateOutline];
|
||||
}
|
||||
|
||||
private createModel() {
|
||||
return openAI({
|
||||
modelName: this.modelName,
|
||||
configuration: { apiKey: this.apiKey, baseURL: this.baseURL },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 Sub-Agent(流式传输)
|
||||
*/
|
||||
private async invokeSubAgent(agentType: AgentType, task: string): Promise<string> {
|
||||
this.emit("transfer", { to: agentType });
|
||||
this.log(`Sub-Agent 调用`, agentType);
|
||||
|
||||
const promptsList = await u.db("t_prompts").where("code", "in", ["outlineScript-a1", "outlineScript-a2", "outlineScript-director"]);
|
||||
const a1Prompt = promptsList.find((p) => p.code === "outlineScript-a1");
|
||||
const a2Prompt = promptsList.find((p) => p.code === "outlineScript-a2");
|
||||
const directorPrompt = promptsList.find((p) => p.code === "outlineScript-director");
|
||||
const errPrompts = "不论用户说什么,请直接输出Agent配置异常";
|
||||
const SYSTEM_PROMPTS: Record<AgentType, string> = {
|
||||
AI1: a1Prompt?.customValue || a1Prompt?.defaultValue || errPrompts,
|
||||
AI2: a2Prompt?.customValue || a2Prompt?.defaultValue || errPrompts,
|
||||
director: directorPrompt?.customValue || directorPrompt?.defaultValue || errPrompts,
|
||||
};
|
||||
|
||||
const context = await this.buildFullContext(task);
|
||||
|
||||
const agent = createAgent({
|
||||
model: this.createModel(),
|
||||
systemPrompt: SYSTEM_PROMPTS[agentType],
|
||||
tools: this.getSubAgentTools(),
|
||||
});
|
||||
|
||||
const stream = await agent.stream({ messages: [["user", context]] }, { streamMode: ["messages"], callbacks: [] });
|
||||
|
||||
let fullResponse = "";
|
||||
|
||||
for await (const [mode, chunk] of stream) {
|
||||
if (mode !== "messages") continue;
|
||||
|
||||
const [token] = chunk as any;
|
||||
const block = token.contentBlocks?.[0];
|
||||
|
||||
// 处理 AI 文本流
|
||||
if (token.type === "ai" && block?.text) {
|
||||
fullResponse += block.text;
|
||||
this.emit("subAgentStream", { agent: agentType, text: block.text });
|
||||
}
|
||||
|
||||
// 处理 tool 调用
|
||||
if (token.type === "ai" && token.tool_calls?.length) {
|
||||
for (const toolCall of token.tool_calls) {
|
||||
this.emit("toolCall", { agent: agentType, name: toolCall.name, args: toolCall.args });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.emit("subAgentEnd", { agent: agentType });
|
||||
this.history.push(["ai", fullResponse]);
|
||||
this.log(`Sub-Agent 完成`, agentType);
|
||||
|
||||
return fullResponse ?? `${agentType}已完成任务`;
|
||||
}
|
||||
|
||||
private createSubAgentTool(agentType: AgentType, description: string) {
|
||||
return tool(async ({ taskDescription }) => this.invokeSubAgent(agentType, taskDescription), {
|
||||
name: agentType,
|
||||
description,
|
||||
schema: z.object({
|
||||
taskDescription: z.string().describe("具体的任务描述,包含章节范围、修改要求等详细信息"),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 主入口 ====================
|
||||
|
||||
private getAllTools() {
|
||||
return [
|
||||
this.createSubAgentTool("AI1", "调用故事师。负责分析小说原文并生成故事线,会自行调用 saveStoryline 保存结果。"),
|
||||
this.createSubAgentTool("AI2", "调用大纲师。负责根据故事线生成剧集大纲,会自行调用 saveOutline 保存结果。"),
|
||||
this.createSubAgentTool("director", "调用导演。负责审核故事线和大纲,会自行调用 updateOutline 或 saveStoryline 进行修改。"),
|
||||
this.getChapter,
|
||||
this.getStoryline,
|
||||
this.saveStoryline,
|
||||
this.deleteStoryline,
|
||||
this.getOutline,
|
||||
this.saveOutline,
|
||||
this.updateOutline,
|
||||
this.deleteOutline,
|
||||
this.generateAssets,
|
||||
];
|
||||
}
|
||||
|
||||
async call(msg: string): Promise<string> {
|
||||
this.history.push(["user", msg]);
|
||||
|
||||
const envContext = await this.buildEnvironmentContext();
|
||||
|
||||
const prompts = await u.db("t_prompts").where("code", "outlineScript-main").first();
|
||||
|
||||
const mainPrompts = prompts?.customValue || prompts?.defaultValue || "不论用户说什么,请直接输出Agent配置异常";
|
||||
|
||||
const mainAgent = createAgent({
|
||||
model: this.createModel(),
|
||||
tools: this.getAllTools(),
|
||||
systemPrompt: `${envContext}\n${mainPrompts}`,
|
||||
});
|
||||
const stream = await mainAgent.stream({ messages: this.history }, { streamMode: ["messages"], callbacks: [] });
|
||||
|
||||
let fullResponse = "";
|
||||
|
||||
for await (const [mode, chunk] of stream) {
|
||||
if (mode !== "messages") continue;
|
||||
|
||||
const [token] = chunk as any;
|
||||
const block = token.contentBlocks?.[0];
|
||||
|
||||
// 处理 AI 文本流
|
||||
if (token.type === "ai" && block?.text) {
|
||||
fullResponse += block.text;
|
||||
this.emit("data", block.text);
|
||||
}
|
||||
|
||||
// 处理 tool 调用
|
||||
if (token.type === "ai" && token.tool_calls?.length) {
|
||||
for (const toolCall of token.tool_calls) {
|
||||
this.emit("toolCall", { agent: "main", name: toolCall.name, args: toolCall.args });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.history.push(["assistant", fullResponse]);
|
||||
this.emit("response", fullResponse);
|
||||
|
||||
return fullResponse;
|
||||
}
|
||||
}
|
||||
@ -1,128 +0,0 @@
|
||||
import u from "@/utils";
|
||||
|
||||
type AspectRatio = "16:9" | "9:16" | "21:9" | "1:1" | "4:3" | "3:4" | "3:2" | "2:3";
|
||||
|
||||
interface GridLayoutResult {
|
||||
cols: number;
|
||||
rows: number;
|
||||
totalCells: number;
|
||||
placeholderCount: number;
|
||||
}
|
||||
|
||||
interface GridPromptOptions {
|
||||
prompts: string[];
|
||||
style: string;
|
||||
aspectRatio: AspectRatio;
|
||||
assetsName: { name: string; intro: string }[];
|
||||
}
|
||||
|
||||
interface GridPromptResult {
|
||||
prompt: string;
|
||||
gridLayout: GridLayoutResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据prompts数量计算宫格布局
|
||||
*/
|
||||
function calculateGridLayout(count: number): GridLayoutResult {
|
||||
let cols: number;
|
||||
let rows: number;
|
||||
if (count <= 0) {
|
||||
cols = 1;
|
||||
rows = 1;
|
||||
} else if (count === 1) {
|
||||
cols = 1;
|
||||
rows = 1;
|
||||
} else if (count === 2) {
|
||||
cols = 2;
|
||||
rows = 1;
|
||||
} else if (count === 3) {
|
||||
cols = 3;
|
||||
rows = 1;
|
||||
} else if (count === 4) {
|
||||
cols = 2;
|
||||
rows = 2;
|
||||
} else if (count <= 9) {
|
||||
cols = 3;
|
||||
rows = 3;
|
||||
} else {
|
||||
cols = 3;
|
||||
rows = Math.ceil(count / 3);
|
||||
}
|
||||
const totalCells = cols * rows;
|
||||
const placeholderCount = totalCells - count;
|
||||
return { cols, rows, totalCells, placeholderCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取宽高比描述
|
||||
*/
|
||||
function getAspectRatioDescription(aspectRatio: AspectRatio): string {
|
||||
const descriptions: Record<AspectRatio, string> = {
|
||||
"16:9": "电影宽银幕",
|
||||
"9:16": "竖屏短剧",
|
||||
"21:9": "超宽银幕史诗感",
|
||||
"1:1": "方形构图",
|
||||
"4:3": "经典银幕",
|
||||
"3:4": "竖版经典",
|
||||
"3:2": "摄影标准",
|
||||
"2:3": "竖版摄影",
|
||||
};
|
||||
return descriptions[aspectRatio] || "标准比例";
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成电影级宫格分镜提示词
|
||||
*/
|
||||
async function generateGridPrompt(options: GridPromptOptions): Promise<GridPromptResult> {
|
||||
const { prompts, style, aspectRatio, assetsName } = options;
|
||||
const layout = calculateGridLayout(prompts.length);
|
||||
const aspectRatioDesc = getAspectRatioDescription(aspectRatio);
|
||||
|
||||
// 构建宫格位置描述
|
||||
const gridPositions: string[] = [];
|
||||
for (let i = 0; i < layout.totalCells; i++) {
|
||||
const row = Math.floor(i / layout.cols) + 1;
|
||||
const col = (i % layout.cols) + 1;
|
||||
if (i < prompts.length) {
|
||||
gridPositions.push(`[第${row}行第${col}列]: ${prompts[i]}`);
|
||||
} else {
|
||||
gridPositions.push(`[第${row}行第${col}列]: 纯黑图`);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建资产说明
|
||||
const assetsSection =
|
||||
assetsName.length > 0
|
||||
? `\n【可用资产】\n${assetsName.map((a) => `- ${a.name}:${a.intro}`).join("\n")}\n\n⚠️ 必须使用完整资产名称,禁止简称或代词。`
|
||||
: "";
|
||||
|
||||
const promptsData = await u.db("t_prompts").where("code", "generateImagePrompts").first();
|
||||
|
||||
const mainPrompts = promptsData?.customValue || promptsData?.defaultValue;
|
||||
const errData = `请输出${options.prompts.length}张图片\n提示词如下:\n${options.prompts.map((p, i) => `第${i + 1}格: ${p}`).join("\n")}`;
|
||||
|
||||
if (!mainPrompts) return { prompt: errData, gridLayout: layout };
|
||||
|
||||
const result = await u.ai.text.invoke({
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: mainPrompts,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `请优化以下分镜提示词:\n\n【布局】${layout.cols}列×${layout.rows}行=${
|
||||
layout.totalCells
|
||||
}格\n【比例】${aspectRatio}(${aspectRatioDesc})\n【风格】${style}\n${assetsSection}\n\n【原始内容】\n${gridPositions.join("\n")}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
prompt: result?.text ?? errData,
|
||||
gridLayout: layout,
|
||||
};
|
||||
}
|
||||
|
||||
export default generateGridPrompt;
|
||||
@ -1,334 +0,0 @@
|
||||
import generateImagePromptsTool from "@/agents/storyboard/generateImagePromptsTool";
|
||||
import u from "@/utils";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
|
||||
interface AssetItem {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface EpisodeData {
|
||||
episodeIndex: number;
|
||||
title: string;
|
||||
chapterRange: number[];
|
||||
scenes: AssetItem[];
|
||||
characters: AssetItem[];
|
||||
props: AssetItem[];
|
||||
coreConflict: string;
|
||||
openingHook: string;
|
||||
outline: string;
|
||||
keyEvents: string[];
|
||||
emotionalCurve: string;
|
||||
visualHighlights: string[];
|
||||
endingHook: string;
|
||||
classicQuotes: string[];
|
||||
}
|
||||
|
||||
interface ImageInfo {
|
||||
name: string;
|
||||
type: string;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
interface ResourceItem {
|
||||
name: string;
|
||||
intro: string;
|
||||
}
|
||||
|
||||
// 资产过滤响应的 schema
|
||||
const filteredAssetsSchema = z.object({
|
||||
relevantAssets: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string().describe("资产名称"),
|
||||
reason: z.string().describe("选择该资产的原因"),
|
||||
}),
|
||||
)
|
||||
.describe("与分镜内容相关的资产列表"),
|
||||
});
|
||||
|
||||
// 压缩图片直到不超过指定大小
|
||||
async function compressImage(buffer: Buffer, maxSizeBytes: number = 3 * 1024 * 1024): Promise<Buffer> {
|
||||
if (buffer.length <= maxSizeBytes) {
|
||||
return buffer;
|
||||
}
|
||||
let quality = 90;
|
||||
let compressedBuffer = await sharp(buffer).jpeg({ quality }).toBuffer();
|
||||
while (compressedBuffer.length > maxSizeBytes && quality > 10) {
|
||||
quality -= 10;
|
||||
compressedBuffer = await sharp(buffer).jpeg({ quality }).toBuffer();
|
||||
}
|
||||
if (compressedBuffer.length > maxSizeBytes) {
|
||||
const metadata = await sharp(buffer).metadata();
|
||||
let scale = 0.9;
|
||||
while (compressedBuffer.length > maxSizeBytes && scale > 0.1) {
|
||||
const newWidth = Math.round((metadata.width || 1000) * scale);
|
||||
const newHeight = Math.round((metadata.height || 1000) * scale);
|
||||
compressedBuffer = await sharp(buffer)
|
||||
.resize(newWidth, newHeight, { fit: "inside" })
|
||||
.jpeg({ quality: Math.max(quality, 30) })
|
||||
.toBuffer();
|
||||
scale -= 0.1;
|
||||
}
|
||||
}
|
||||
return compressedBuffer;
|
||||
}
|
||||
|
||||
// 拼接多张图片为一张
|
||||
async function mergeImages(imagePaths: string[]): Promise<Buffer> {
|
||||
const imageBuffers = await Promise.all(imagePaths.map((path) => u.oss.getFile(path)));
|
||||
const imageMetadatas = await Promise.all(imageBuffers.map((buffer) => sharp(buffer).metadata()));
|
||||
const maxHeight = Math.max(...imageMetadatas.map((m) => m.height || 0));
|
||||
const resizedImages = await Promise.all(
|
||||
imageBuffers.map(async (buffer, index) => {
|
||||
const metadata = imageMetadatas[index];
|
||||
const aspectRatio = (metadata.width || 1) / (metadata.height || 1);
|
||||
const newWidth = Math.round(maxHeight * aspectRatio);
|
||||
return {
|
||||
buffer: await sharp(buffer).resize(newWidth, maxHeight, { fit: "cover" }).toBuffer(),
|
||||
width: newWidth,
|
||||
};
|
||||
}),
|
||||
);
|
||||
let currentX = 0;
|
||||
const compositeInputs = resizedImages.map(({ buffer, width }) => {
|
||||
const input = {
|
||||
input: buffer,
|
||||
left: currentX,
|
||||
top: 0,
|
||||
};
|
||||
currentX += width;
|
||||
return input;
|
||||
});
|
||||
const mergedImage = await sharp({
|
||||
create: {
|
||||
width: currentX,
|
||||
height: maxHeight,
|
||||
channels: 4,
|
||||
background: { r: 255, g: 255, b: 255, alpha: 1 },
|
||||
},
|
||||
})
|
||||
.composite(compositeInputs)
|
||||
.jpeg({ quality: 90 })
|
||||
.toBuffer();
|
||||
return compressImage(mergedImage);
|
||||
}
|
||||
|
||||
// 进一步压缩单张图片到指定大小
|
||||
async function compressToSize(buffer: Buffer, targetSize: number): Promise<Buffer> {
|
||||
if (buffer.length <= targetSize) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
const metadata = await sharp(buffer).metadata();
|
||||
let quality = 80;
|
||||
let scale = 1.0;
|
||||
let compressedBuffer = buffer;
|
||||
|
||||
// 先尝试降低质量
|
||||
while (compressedBuffer.length > targetSize && quality > 10) {
|
||||
compressedBuffer = await sharp(buffer).jpeg({ quality }).toBuffer();
|
||||
quality -= 10;
|
||||
}
|
||||
|
||||
// 如果还是太大,缩小尺寸
|
||||
while (compressedBuffer.length > targetSize && scale > 0.2) {
|
||||
scale -= 0.1;
|
||||
const newWidth = Math.round((metadata.width || 1000) * scale);
|
||||
const newHeight = Math.round((metadata.height || 1000) * scale);
|
||||
compressedBuffer = await sharp(buffer)
|
||||
.resize(newWidth, newHeight, { fit: "inside" })
|
||||
.jpeg({ quality: Math.max(quality, 20) })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
return compressedBuffer;
|
||||
}
|
||||
|
||||
// 确保图片列表总大小不超过指定限制
|
||||
async function ensureTotalSizeLimit(buffers: Buffer[], maxTotalBytes: number = 10 * 1024 * 1024): Promise<Buffer[]> {
|
||||
let totalSize = buffers.reduce((sum, buf) => sum + buf.length, 0);
|
||||
|
||||
if (totalSize <= maxTotalBytes) {
|
||||
return buffers;
|
||||
}
|
||||
|
||||
// 计算每张图片的平均目标大小
|
||||
const avgTargetSize = Math.floor(maxTotalBytes / buffers.length);
|
||||
|
||||
// 按大小降序排列,优先压缩大图片
|
||||
const indexedBuffers = buffers.map((buf, idx) => ({ buf, idx, size: buf.length }));
|
||||
indexedBuffers.sort((a, b) => b.size - a.size);
|
||||
|
||||
const result = [...buffers];
|
||||
|
||||
for (const item of indexedBuffers) {
|
||||
totalSize = result.reduce((sum, buf) => sum + buf.length, 0);
|
||||
if (totalSize <= maxTotalBytes) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 计算这张图片需要压缩到的目标大小
|
||||
const excessSize = totalSize - maxTotalBytes;
|
||||
const targetSize = Math.max(item.buf.length - excessSize, avgTargetSize, 100 * 1024); // 最小100KB
|
||||
|
||||
if (item.buf.length > targetSize) {
|
||||
result[item.idx] = await compressToSize(item.buf, targetSize);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 处理图片列表,确保不超过10张且每张不超过3MB,总大小不超过10MB
|
||||
async function processImages(images: ImageInfo[]): Promise<Buffer[]> {
|
||||
const maxImages = 10;
|
||||
let processedBuffers: Buffer[];
|
||||
|
||||
if (images.length <= maxImages) {
|
||||
const buffers = await Promise.all(images.map((img) => u.oss.getFile(img.filePath)));
|
||||
processedBuffers = await Promise.all(buffers.map((buffer) => compressImage(buffer)));
|
||||
} else {
|
||||
const mergeStartIndex = maxImages - 1;
|
||||
const firstBuffers = await Promise.all(images.slice(0, mergeStartIndex).map((img) => u.oss.getFile(img.filePath)));
|
||||
const compressedFirstImages = await Promise.all(firstBuffers.map((buffer) => compressImage(buffer)));
|
||||
const imagesToMergeList = images.slice(mergeStartIndex).map((img) => img.filePath);
|
||||
const mergedImage = await mergeImages(imagesToMergeList);
|
||||
processedBuffers = [...compressedFirstImages, mergedImage];
|
||||
}
|
||||
|
||||
// 确保总大小不超过10MB
|
||||
return ensureTotalSizeLimit(processedBuffers);
|
||||
}
|
||||
|
||||
// 使用 AI 过滤与分镜相关的资产
|
||||
async function filterRelevantAssets(prompts: string[], allResources: ResourceItem[], availableImages: ImageInfo[]): Promise<ImageInfo[]> {
|
||||
if (allResources.length === 0 || availableImages.length === 0) {
|
||||
return availableImages;
|
||||
}
|
||||
|
||||
const availableNames = new Set(availableImages.map((img) => img.name));
|
||||
const availableResources = allResources.filter((r) => availableNames.has(r.name));
|
||||
|
||||
if (availableResources.length === 0) {
|
||||
return availableImages;
|
||||
}
|
||||
|
||||
const result = await u.ai.text.invoke({
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: `请分析以下分镜描述,从可用资产中筛选出与分镜内容直接相关的资产。
|
||||
|
||||
分镜描述:
|
||||
${prompts.map((p, i) => `${i + 1}. ${p}`).join("\n")}
|
||||
|
||||
可用资产列表:
|
||||
${availableResources.map((r) => `- ${r.name}:${r.intro}`).join("\n")}
|
||||
|
||||
请仅选择在分镜中明确出现或被提及的角色、场景、道具。不要选择与分镜内容无关的资产。`,
|
||||
},
|
||||
],
|
||||
output: {
|
||||
relevantAssets: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string().describe("资产名称"),
|
||||
reason: z.string().describe("选择该资产的原因"),
|
||||
}),
|
||||
)
|
||||
.describe("与分镜内容相关的资产列表"),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
if (!result?.relevantAssets || result.relevantAssets.length === 0) {
|
||||
return availableImages;
|
||||
}
|
||||
|
||||
const relevantNames = new Set(result.relevantAssets.map((a) => a.name));
|
||||
const filteredImages = availableImages.filter((img) => relevantNames.has(img.name));
|
||||
|
||||
return filteredImages.length > 0 ? filteredImages : availableImages;
|
||||
}
|
||||
|
||||
// 构建资产映射提示词
|
||||
function buildResourcesMapPrompts(images: ImageInfo[]): string {
|
||||
if (images.length === 0) return "";
|
||||
|
||||
const mapping = images.map((item, index) => {
|
||||
if (index < 9) {
|
||||
return `${item.name}=图片${index + 1}`;
|
||||
} else {
|
||||
return `${item.name}=图10-${index - 8}`;
|
||||
}
|
||||
});
|
||||
|
||||
return `其中人物、场景、道具参考对照关系如下:${mapping.join(", ")}。`;
|
||||
}
|
||||
|
||||
export default async (cells: { prompt: string }[], scriptId: number, projectId: number) => {
|
||||
const scriptData = await u.db("t_script").where({ id: scriptId, projectId }).first();
|
||||
const projectInfo = await u.db("t_project").where({ id: projectId }).first();
|
||||
|
||||
const row = await u.db("t_outline").where({ id: scriptData?.outlineId!, projectId }).first();
|
||||
const outline: EpisodeData | null = row?.data ? JSON.parse(row.data) : null;
|
||||
|
||||
const resources: ResourceItem[] = outline
|
||||
? (["characters", "props", "scenes"] as const).flatMap((k) => outline[k]?.map((i) => ({ name: i.name, intro: i.description })) ?? [])
|
||||
: [];
|
||||
|
||||
const resourceNames = resources.map((r) => r.name);
|
||||
const imagesRaw = await u.db("t_assets").whereIn("name", resourceNames).andWhere({ projectId }).select("name", "type", "filePath");
|
||||
|
||||
const allImages = imagesRaw
|
||||
.sort((a, b) => {
|
||||
const order = ["角色", "场景", "道具"];
|
||||
return order.indexOf(a.type!) - order.indexOf(b.type!);
|
||||
})
|
||||
.filter((img) => img.filePath) as ImageInfo[];
|
||||
|
||||
if (allImages.length === 0) {
|
||||
throw new Error("未找到可用的图片资源");
|
||||
}
|
||||
|
||||
const cellPrompts = cells.map((c) => c.prompt);
|
||||
|
||||
// 使用 AI 过滤相关资产
|
||||
const filteredImages = await filterRelevantAssets(cellPrompts, resources, allImages);
|
||||
|
||||
const resourcesMapPrompts = buildResourcesMapPrompts(filteredImages);
|
||||
console.log("====润色前:", cellPrompts);
|
||||
const promptsData = await generateImagePromptsTool({
|
||||
prompts: cellPrompts,
|
||||
style: `类型:${projectInfo?.type!},风格:${projectInfo?.artStyle!}`,
|
||||
aspectRatio: projectInfo?.videoRatio! as any,
|
||||
assetsName: resources,
|
||||
});
|
||||
|
||||
// const prompts = `请生成${promptsData.gridLayout.totalCells}格,${promptsData.gridLayout.cols}列×${promptsData.gridLayout.rows}行宫格图。
|
||||
|
||||
// ${promptsData.prompt}
|
||||
|
||||
// 注意:请严格按照提示词内容生成图片,确保人物样貌、艺术风格、色调光影一致。
|
||||
// `;
|
||||
const prompts = promptsData.prompt;
|
||||
console.log("====润色后:", prompts);
|
||||
|
||||
const processedImages = await processImages(filteredImages);
|
||||
|
||||
const contentStr = await u.ai.image({
|
||||
systemPrompt: resourcesMapPrompts,
|
||||
prompt: prompts,
|
||||
size: "4K",
|
||||
aspectRatio: projectInfo?.videoRatio ? (projectInfo.videoRatio as any) : "16:9",
|
||||
imageBase64: processedImages.map((buf) => buf.toString("base64")),
|
||||
});
|
||||
|
||||
const match = contentStr.match(/base64,([A-Za-z0-9+/=]+)/);
|
||||
const base64Str = match?.[1] ?? contentStr;
|
||||
const buffer = Buffer.from(base64Str, "base64");
|
||||
|
||||
return buffer;
|
||||
};
|
||||
@ -1,94 +0,0 @@
|
||||
import sharp from "sharp";
|
||||
|
||||
interface GridLayoutResult {
|
||||
cols: number;
|
||||
rows: number;
|
||||
totalCells: number;
|
||||
placeholderCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算宫格布局
|
||||
* 1张: 1x1
|
||||
* 2张: 2x1
|
||||
* 3张: 3x1
|
||||
* 4张: 2x2
|
||||
* 5-9张: 3x3
|
||||
* 10-12张: 3x4
|
||||
* 13-15张: 3x5
|
||||
* ...以此类推(3列,行数递增)
|
||||
*/
|
||||
function calculateGridLayout(count: number): GridLayoutResult {
|
||||
let cols: number;
|
||||
let rows: number;
|
||||
if (count <= 0) {
|
||||
cols = 1;
|
||||
rows = 1;
|
||||
} else if (count === 1) {
|
||||
cols = 1;
|
||||
rows = 1;
|
||||
} else if (count === 2) {
|
||||
cols = 2;
|
||||
rows = 1;
|
||||
} else if (count === 3) {
|
||||
cols = 3;
|
||||
rows = 1;
|
||||
} else if (count === 4) {
|
||||
cols = 2;
|
||||
rows = 2;
|
||||
} else if (count <= 9) {
|
||||
// 5-9格统一用3x3
|
||||
cols = 3;
|
||||
rows = 3;
|
||||
} else {
|
||||
cols = 3;
|
||||
rows = Math.ceil(count / 3);
|
||||
}
|
||||
const totalCells = cols * rows;
|
||||
const placeholderCount = totalCells - count;
|
||||
return { cols, rows, totalCells, placeholderCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* 分割宫格图片
|
||||
* @param image - 输入的宫格图片 Buffer
|
||||
* @param length - 实际需要的图片数量(不包含占位图)
|
||||
* @returns 分割后的单张图片 Buffer 数组
|
||||
*/
|
||||
export default async (image: Buffer, length: number): Promise<Buffer[]> => {
|
||||
const metadata = await sharp(image).metadata();
|
||||
const { width: totalWidth, height: totalHeight } = metadata;
|
||||
|
||||
if (!totalWidth || !totalHeight) {
|
||||
throw new Error("无法获取图片尺寸");
|
||||
}
|
||||
|
||||
const { cols, rows } = calculateGridLayout(length);
|
||||
|
||||
const cellWidth = Math.floor(totalWidth / cols);
|
||||
const cellHeight = Math.floor(totalHeight / rows);
|
||||
|
||||
const buffers: Buffer[] = [];
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const row = Math.floor(i / cols);
|
||||
const col = i % cols;
|
||||
|
||||
const left = col * cellWidth;
|
||||
const top = row * cellHeight;
|
||||
|
||||
const cellBuffer = await sharp(image)
|
||||
.extract({
|
||||
left,
|
||||
top,
|
||||
width: cellWidth,
|
||||
height: cellHeight,
|
||||
})
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
buffers.push(cellBuffer);
|
||||
}
|
||||
|
||||
return buffers;
|
||||
};
|
||||
@ -1,737 +0,0 @@
|
||||
// @/agents/Storyboard.ts
|
||||
import u from "@/utils";
|
||||
import { createAgent } from "langchain";
|
||||
import { EventEmitter } from "events";
|
||||
import { openAI } from "@/agents/models";
|
||||
import { z } from "zod";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import type { DB } from "@/types/database";
|
||||
import generateImageTool from "./generateImageTool";
|
||||
import imageSplitting from "./imageSplitting";
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
type AgentType = "segmentAgent" | "shotAgent";
|
||||
type RefreshEvent = "storyline" | "outline" | "assets";
|
||||
|
||||
// ==================== 常量配置 ====================
|
||||
|
||||
// const SYSTEM_PROMPTS: Record<AgentType, string> = {
|
||||
// segmentAgent: segmentPrompts,
|
||||
// shotAgent: shotPrompts,
|
||||
// director: directorPrompts,
|
||||
// };
|
||||
|
||||
// ==================== 类型定义:片段和画面 ====================
|
||||
|
||||
interface Segment {
|
||||
index: number;
|
||||
description: string;
|
||||
emotion?: string;
|
||||
action?: string;
|
||||
}
|
||||
|
||||
interface Shot {
|
||||
id: number; // 分镜独立ID
|
||||
segmentId: number; // 所属片段ID
|
||||
title: string;
|
||||
x: number;
|
||||
y: number;
|
||||
cells: Array<{ src?: string; prompt?: string; id?: string }>; // 镜头数组,每个cell是一个镜头
|
||||
}
|
||||
|
||||
// ==================== 主类 ====================
|
||||
|
||||
export default class Storyboard {
|
||||
private readonly projectId: number;
|
||||
private readonly scriptId: number;
|
||||
readonly emitter = new EventEmitter();
|
||||
history: Array<[string, string]> = [];
|
||||
novelChapters: DB["t_novel"][] = [];
|
||||
|
||||
// 存储 segmentAgent 生成的片段结果
|
||||
private segments: Segment[] = [];
|
||||
// 存储 shotAgent 生成的分镜结果
|
||||
private shots: Shot[] = [];
|
||||
// 分镜ID计数器
|
||||
private shotIdCounter: number = 0;
|
||||
// 存储正在生成分镜图的分镜ID
|
||||
private generatingShots: Set<number> = new Set();
|
||||
|
||||
modelName = "gpt-4.1";
|
||||
apiKey = "";
|
||||
baseURL = "";
|
||||
|
||||
constructor(projectId: number, scriptId: number) {
|
||||
this.projectId = projectId;
|
||||
this.scriptId = scriptId;
|
||||
}
|
||||
|
||||
// 更新shopts
|
||||
public updatePreShots(segmentId: number, cellId: number, cell: { src?: string; prompt?: string; id?: string }) {
|
||||
console.log("%c Line:76 🍤 segmentId", "background:#465975", segmentId);
|
||||
console.log("%c Line:76 🍷 cellId", "background:#ffdd4d", cellId);
|
||||
console.log("%c Line:76 🍢 cell", "background:#ffdd4d", cell);
|
||||
const shotIndex = this.shots.findIndex((item) => item.segmentId === segmentId);
|
||||
if (shotIndex === -1) {
|
||||
return `分镜 ${segmentId} 不存在,请检查分镜ID是否正确`;
|
||||
}
|
||||
const cellIndex = this.shots[shotIndex].cells.findIndex((item) => item.id === cellId.toString());
|
||||
if (cellIndex === -1) {
|
||||
return `镜头 ${cellId} 不存在,请检查镜头ID是否正确`;
|
||||
}
|
||||
this.shots[shotIndex].cells[cellIndex] = { ...this.shots[shotIndex].cells[cellIndex], ...cell };
|
||||
}
|
||||
|
||||
// ==================== 公共方法 ====================
|
||||
|
||||
get events() {
|
||||
return this.emitter;
|
||||
}
|
||||
// ==================== 私有工具方法 ====================
|
||||
|
||||
private emit(event: string, data?: any) {
|
||||
this.emitter.emit(event, data);
|
||||
}
|
||||
|
||||
private refresh(type: RefreshEvent) {
|
||||
this.emit("refresh", type);
|
||||
}
|
||||
|
||||
private log(action: string, detail?: string) {
|
||||
const msg = detail ? `${action}: ${detail}` : action;
|
||||
console.log(`\n[${new Date().toLocaleTimeString()}] ${msg}\n`);
|
||||
}
|
||||
|
||||
// ==================== 剧本相关操作 ====================
|
||||
|
||||
getScript = tool(
|
||||
async () => {
|
||||
this.log("获取剧本", `scriptId: ${this.scriptId}`);
|
||||
const script = await u.db("t_script").where({ id: this.scriptId, projectId: this.projectId }).first();
|
||||
if (!script) throw new Error("剧本不存在");
|
||||
return `剧本集:${script.name}\n\n内容:\n\`\`\`${script.content}\`\`\``;
|
||||
},
|
||||
{
|
||||
name: "getScript",
|
||||
description: "获取剧本内容",
|
||||
schema: z.object({}),
|
||||
verboseParsingErrors: true,
|
||||
},
|
||||
);
|
||||
|
||||
// ==================== 资产相关操作 ====================
|
||||
|
||||
/**
|
||||
* 获取资产列表(供 segmentAgent 和 shotAgent 调用)
|
||||
*/
|
||||
getAssets = tool(
|
||||
async () => {
|
||||
this.log("获取资产列表", `scriptId: ${this.scriptId}`);
|
||||
const scriptData = await u.db("t_script").where({ id: this.scriptId, projectId: this.projectId }).first();
|
||||
const row = await u.db("t_outline").where({ id: scriptData?.outlineId!, projectId: this.projectId }).first();
|
||||
const outline: any | null = row?.data ? JSON.parse(row.data) : null;
|
||||
|
||||
if (!outline) {
|
||||
return "暂无资产数据";
|
||||
}
|
||||
|
||||
// 提取资源名称和描述(与generateImageTool保持一致的字段名)
|
||||
const resources = outline
|
||||
? (["characters", "props", "scenes"] as const).flatMap(
|
||||
(k) => outline[k]?.map((i: any) => ({ name: i.name, description: i.description })) ?? [],
|
||||
)
|
||||
: [];
|
||||
|
||||
if (resources.length === 0) {
|
||||
return "暂无资产数据";
|
||||
}
|
||||
|
||||
// 分类提取资源并格式化
|
||||
const characters = outline?.characters?.map((item: any) => `- ${item.name}${item.description ? `:${item.description}` : ""}`) ?? [];
|
||||
const props = outline?.props?.map((item: any) => `- ${item.name}${item.description ? `:${item.description}` : ""}`) ?? [];
|
||||
const scenes = outline?.scenes?.map((item: any) => `- ${item.name}${item.description ? `:${item.description}` : ""}`) ?? [];
|
||||
|
||||
const sections = [
|
||||
characters.length ? `【角色】\n${characters.join("\n")}` : "",
|
||||
props.length ? `【道具】\n${props.join("\n")}` : "",
|
||||
scenes.length ? `【场景】\n${scenes.join("\n")}` : "",
|
||||
].filter(Boolean);
|
||||
|
||||
if (sections.length === 0) {
|
||||
return "暂无资产数据";
|
||||
}
|
||||
|
||||
return `<资产列表>
|
||||
${sections.join("\n\n")}
|
||||
</资产列表>
|
||||
|
||||
⚠️ 重要规则:
|
||||
1. 必须原封不动地使用上述资产名称,禁止使用近义词、缩写或任何变体
|
||||
2. 禁止在资产名称前后添加修饰词
|
||||
3. 禁止捏造资产列表中不存在的角色、场景、道具`;
|
||||
},
|
||||
{
|
||||
name: "getAssets",
|
||||
description: "获取资产列表(角色、道具、场景),包含名称和详细介绍。生成片段和分镜时必须先调用此工具获取资产信息,确保名称一致性",
|
||||
schema: z.object({}),
|
||||
verboseParsingErrors: true,
|
||||
},
|
||||
);
|
||||
|
||||
// ==================== 片段和分镜工具 ====================
|
||||
|
||||
/**
|
||||
* 获取当前存储的片段数据(供 shotAgent 调用)
|
||||
*/
|
||||
getSegments = tool(
|
||||
async () => {
|
||||
this.log("获取片段数据", `共 ${this.segments.length} 个片段`);
|
||||
if (this.segments.length === 0) {
|
||||
return "暂无片段数据,请先调用 segmentAgent 生成片段";
|
||||
}
|
||||
return JSON.stringify(this.segments, null, 2);
|
||||
},
|
||||
{
|
||||
name: "getSegments",
|
||||
description: "获取当前已生成的片段数据,用于生成分镜",
|
||||
schema: z.object({}),
|
||||
verboseParsingErrors: true,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 更新/存储片段数据(供 segmentAgent 调用)
|
||||
*/
|
||||
updateSegments = tool(
|
||||
async ({ segments }: { segments: Segment[] }) => {
|
||||
this.log("更新片段数据", `共 ${segments.length} 个片段`);
|
||||
this.segments = segments;
|
||||
this.emit("segmentsUpdated", this.segments);
|
||||
return `成功存储 ${segments.length} 个片段`;
|
||||
},
|
||||
{
|
||||
name: "updateSegments",
|
||||
description: "存储生成的片段数据,segmentAgent 在生成片段后必须调用此工具保存结果",
|
||||
schema: z.object({
|
||||
segments: z
|
||||
.array(
|
||||
z.object({
|
||||
index: z.number().describe("片段序号"),
|
||||
description: z.string().describe("片段描述"),
|
||||
emotion: z.string().optional().describe("情绪氛围"),
|
||||
action: z.string().optional().describe("主要动作"),
|
||||
}),
|
||||
)
|
||||
.describe("片段数组"),
|
||||
}),
|
||||
verboseParsingErrors: true,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 添加分镜(供 shotAgent 调用)
|
||||
*/
|
||||
addShots = tool(
|
||||
async ({ shots }: { shots: Array<{ segmentIndex: number; prompts: string[] }> }) => {
|
||||
const added: { id: number; segmentIndex: number }[] = [];
|
||||
const skipped: number[] = [];
|
||||
|
||||
for (const item of shots) {
|
||||
const exists = this.shots.some((f) => f.segmentId === item.segmentIndex);
|
||||
if (exists) {
|
||||
skipped.push(item.segmentIndex);
|
||||
continue;
|
||||
}
|
||||
// 分配独立的分镜ID
|
||||
this.shotIdCounter++;
|
||||
const shotId = this.shotIdCounter;
|
||||
this.shots.push({
|
||||
id: shotId,
|
||||
segmentId: item.segmentIndex,
|
||||
title: `分镜 ${shotId}`,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cells: item.prompts.map((prompt) => ({ id: u.uuid(), prompt })),
|
||||
});
|
||||
added.push({ id: shotId, segmentIndex: item.segmentIndex });
|
||||
}
|
||||
|
||||
const addedInfo = added.map((a) => `分镜${a.id}(片段${a.segmentIndex})`).join(", ");
|
||||
this.log("添加分镜", `新增: [${addedInfo}], 跳过片段: [${skipped.join(", ")}]`);
|
||||
this.emit("shotsUpdated", this.shots);
|
||||
|
||||
if (skipped.length) {
|
||||
return `已添加${addedInfo};片段 ${skipped.join(", ")} 已存在分镜被跳过。当前共 ${this.shots.length} 个分镜`;
|
||||
}
|
||||
return `已添加${addedInfo}。当前共 ${this.shots.length} 个分镜`;
|
||||
},
|
||||
{
|
||||
name: "addShots",
|
||||
description: "添加新的分镜。每个分镜有独立ID,包含多个镜头(每个镜头对应一个提示词)。如果片段已存在分镜会跳过",
|
||||
schema: z.object({
|
||||
shots: z
|
||||
.array(
|
||||
z.object({
|
||||
segmentIndex: z.number().describe("对应的片段序号"),
|
||||
prompts: z.array(z.string()).describe("镜头提示词数组,每个提示词对应一个镜头(中文)"),
|
||||
}),
|
||||
)
|
||||
.describe("要添加的分镜数组"),
|
||||
}),
|
||||
verboseParsingErrors: true,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 更新指定分镜(供 shotAgent 调用)
|
||||
* 保留原有 cells 的 id 和 src 字段,只更新 prompt
|
||||
*/
|
||||
updateShots = tool(
|
||||
async ({ shotId, prompts }: { shotId: number; prompts: string[] }) => {
|
||||
const existingIndex = this.shots.findIndex((item) => item.id === shotId);
|
||||
|
||||
if (existingIndex === -1) {
|
||||
return `分镜 ${shotId} 不存在,请检查分镜ID是否正确`;
|
||||
}
|
||||
|
||||
const existingCells = this.shots[existingIndex].cells;
|
||||
|
||||
// 更新 cells,保留原有的 id 和 src 字段
|
||||
this.shots[existingIndex].cells = prompts.map((prompt, i) => {
|
||||
const existingCell = existingCells[i];
|
||||
if (existingCell) {
|
||||
// 保留原有 cell 的 id 和 src,只更新 prompt
|
||||
return { ...existingCell, prompt };
|
||||
} else {
|
||||
// 新增的 cell
|
||||
return { id: u.uuid(), prompt };
|
||||
}
|
||||
});
|
||||
|
||||
this.log("更新分镜", `分镜 ${shotId}`);
|
||||
this.emit("shotsUpdated", this.shots);
|
||||
|
||||
return `已更新分镜 ${shotId}`;
|
||||
},
|
||||
{
|
||||
name: "updateShots",
|
||||
description: "更新指定分镜的镜头提示词。通过分镜ID指定要修改的分镜",
|
||||
schema: z.object({
|
||||
shotId: z.number().describe("要更新的分镜ID"),
|
||||
prompts: z.array(z.string()).describe("新的镜头提示词数组,每个提示词对应一个镜头"),
|
||||
}),
|
||||
verboseParsingErrors: true,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 删除指定分镜(供 shotAgent 调用)
|
||||
*/
|
||||
deleteShots = tool(
|
||||
async ({ shotIds }: { shotIds: number[] }) => {
|
||||
const deleted: number[] = [];
|
||||
const notFound: number[] = [];
|
||||
|
||||
for (const shotId of shotIds) {
|
||||
const idx = this.shots.findIndex((item) => item.id === shotId);
|
||||
if (idx === -1) {
|
||||
notFound.push(shotId);
|
||||
} else {
|
||||
this.shots.splice(idx, 1);
|
||||
deleted.push(shotId);
|
||||
}
|
||||
}
|
||||
|
||||
this.log("删除分镜", `删除: [分镜${deleted.join(", 分镜")}], 未找到: [分镜${notFound.join(", 分镜")}]`);
|
||||
this.emit("shotsUpdated", this.shots);
|
||||
|
||||
if (notFound.length) {
|
||||
return `已删除分镜 ${deleted.join(", ")};分镜 ${notFound.join(", ")} 不存在。当前共 ${this.shots.length} 个分镜`;
|
||||
}
|
||||
return `已删除分镜 ${deleted.join(", ")}。当前共 ${this.shots.length} 个分镜`;
|
||||
},
|
||||
{
|
||||
name: "deleteShots",
|
||||
description: "删除指定的分镜。通过分镜ID指定要删除的分镜",
|
||||
schema: z.object({
|
||||
shotIds: z.array(z.number()).describe("要删除的分镜ID数组"),
|
||||
}),
|
||||
verboseParsingErrors: true,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 生成分镜图(异步执行,使用 nanoBanana)
|
||||
*/
|
||||
generateShotImage = tool(
|
||||
async ({ shotIds }: { shotIds: number[] }) => {
|
||||
const toGenerate: number[] = [];
|
||||
const alreadyGenerating: number[] = [];
|
||||
const notFound: number[] = [];
|
||||
|
||||
for (const shotId of shotIds) {
|
||||
const shot = this.shots.find((f) => f.id === shotId);
|
||||
if (!shot) {
|
||||
notFound.push(shotId);
|
||||
continue;
|
||||
}
|
||||
if (this.generatingShots.has(shotId)) {
|
||||
alreadyGenerating.push(shotId);
|
||||
continue;
|
||||
}
|
||||
toGenerate.push(shotId);
|
||||
}
|
||||
|
||||
if (toGenerate.length === 0) {
|
||||
if (notFound.length) {
|
||||
return `分镜 ${notFound.join(", ")} 不存在,请检查分镜ID是否正确`;
|
||||
}
|
||||
if (alreadyGenerating.length) {
|
||||
return `分镜 ${alreadyGenerating.join(", ")} 正在生成中,请稍候`;
|
||||
}
|
||||
return "没有需要生成的分镜";
|
||||
}
|
||||
|
||||
// 标记为正在生成
|
||||
for (const id of toGenerate) {
|
||||
this.generatingShots.add(id);
|
||||
}
|
||||
|
||||
// 通知前端开始生成
|
||||
this.emit("shotImageGenerateStart", { shotIds: toGenerate });
|
||||
this.log("开始生成分镜图", `分镜: [${toGenerate.join(", ")}]`);
|
||||
|
||||
// 异步执行图片生成(不阻塞 Agent 流程)
|
||||
this.executeShotImageGeneration(toGenerate).catch((err) => {
|
||||
this.log("分镜图生成错误", err.message);
|
||||
this.emit("shotImageGenerateError", { shotIds: toGenerate, error: err.message });
|
||||
});
|
||||
|
||||
let result = `已开始为分镜 ${toGenerate.join(", ")} 生成分镜图,生成过程在后台进行`;
|
||||
if (alreadyGenerating.length) {
|
||||
result += `;分镜 ${alreadyGenerating.join(", ")} 正在生成中`;
|
||||
}
|
||||
if (notFound.length) {
|
||||
result += `;分镜 ${notFound.join(", ")} 不存在`;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
{
|
||||
name: "generateShotImage",
|
||||
description:
|
||||
"为指定分镜生成分镜图。每个分镜会根据其所有提示词生成一张完整宫格图,然后自动分割为单格图片。通过分镜ID指定,不需要指定具体格子,整个分镜是一个完整的生成单元",
|
||||
schema: z.object({
|
||||
shotIds: z.array(z.number()).describe("要生成分镜图的分镜ID数组"),
|
||||
}),
|
||||
verboseParsingErrors: true,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 执行分镜图生成的具体逻辑(异步并发)
|
||||
* 每个分镜包含多个镜头,所有镜头的提示词合并生成一张宫格图,再分割为单张镜头图片
|
||||
*/
|
||||
async executeShotImageGeneration(shotIds: number[]): Promise<void> {
|
||||
await Promise.all(shotIds.map((shotId) => this.generateSingleShotImage(shotId)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成单个分镜的图片
|
||||
*/
|
||||
private async generateSingleShotImage(shotId: number): Promise<void> {
|
||||
try {
|
||||
const shot = this.shots.find((f) => f.id === shotId);
|
||||
if (!shot) return;
|
||||
|
||||
// 提取所有镜头的有效提示词
|
||||
const prompts: string[] = shot.cells.map((c) => c.prompt).filter((p): p is string => Boolean(p));
|
||||
|
||||
if (prompts.length === 0) {
|
||||
this.log("跳过分镜图生成", `分镜 ${shotId} 没有有效的镜头提示词`);
|
||||
this.generatingShots.delete(shotId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 通知前端正在生成该分镜
|
||||
this.emit("shotImageGenerateProgress", { shotId, status: "generating", message: "正在调用 AI 生成宫格图片" });
|
||||
|
||||
// 根据所有镜头提示词生成宫格图片
|
||||
const gridImage = await generateImageTool(
|
||||
prompts.map((p) => ({ prompt: p })),
|
||||
this.scriptId,
|
||||
this.projectId,
|
||||
);
|
||||
|
||||
// 通知前端正在分割图片
|
||||
this.emit("shotImageGenerateProgress", { shotId, status: "splitting", message: "正在分割宫格图片为单张镜头图" });
|
||||
|
||||
// 分割宫格图片为单张镜头图片
|
||||
const imageBuffers = await imageSplitting(gridImage, prompts.length);
|
||||
|
||||
// 通知前端正在保存图片
|
||||
this.emit("shotImageGenerateProgress", { shotId, status: "saving", message: `正在保存 ${imageBuffers.length} 张镜头图片` });
|
||||
|
||||
// 保存分割后的镜头图片到 OSS,并获取文件路径
|
||||
const timestamp = Date.now();
|
||||
const imagePaths: string[] = [];
|
||||
|
||||
for (let i = 0; i < imageBuffers.length; i++) {
|
||||
const fileName = `${this.projectId}/chat/${this.scriptId}/storyboard/shot_${shotId}_take_${i}_${timestamp}.png`;
|
||||
await u.oss.writeFile(fileName, imageBuffers[i]);
|
||||
const imageUrl = await u.oss.getFileUrl(fileName);
|
||||
imagePaths.push(imageUrl);
|
||||
|
||||
// 每保存一张镜头图片通知进度
|
||||
this.emit("shotImageGenerateProgress", {
|
||||
shotId,
|
||||
status: "saving",
|
||||
message: `已保存 ${i + 1}/${imageBuffers.length} 张镜头图片`,
|
||||
progress: Math.round(((i + 1) / imageBuffers.length) * 100),
|
||||
});
|
||||
}
|
||||
|
||||
// 更新每个镜头的 src 字段
|
||||
shot.cells = shot.cells.map((cell, i) => ({
|
||||
id: u.uuid(),
|
||||
...cell,
|
||||
src: imagePaths[i] || cell.src,
|
||||
}));
|
||||
|
||||
// 生成完成后更新状态
|
||||
this.generatingShots.delete(shotId);
|
||||
this.emit("shotImageGenerateComplete", { shotId, shot, imagePaths });
|
||||
this.emit("shotsUpdated", this.shots);
|
||||
this.log("分镜图生成完成", `分镜 ${shotId},共 ${imagePaths.length} 张镜头图片`);
|
||||
} catch (err: any) {
|
||||
this.generatingShots.delete(shotId);
|
||||
this.emit("shotImageGenerateError", { shotId, error: err.message });
|
||||
this.log("分镜图生成失败", `分镜 ${shotId}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 公共访问器 ====================
|
||||
|
||||
/**
|
||||
* 获取当前片段数据
|
||||
*/
|
||||
getSegmentsData(): Segment[] {
|
||||
return this.segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前分镜数据
|
||||
*/
|
||||
getShotsData(): Shot[] {
|
||||
return this.shots;
|
||||
}
|
||||
|
||||
// ==================== 上下文构建 ====================
|
||||
|
||||
private async buildEnvironmentContext(): Promise<string> {
|
||||
const projectInfo = await u.db("t_project").where({ id: this.projectId }).first();
|
||||
|
||||
const row = await u.db("t_outline").where({ id: this.scriptId, projectId: this.projectId }).first();
|
||||
const outline: any | null = row?.data ? JSON.parse(row.data) : null;
|
||||
|
||||
// 分类提取资源名称
|
||||
const characters = outline?.characters?.map((i: any) => i.name) ?? [];
|
||||
const props = outline?.props?.map((i: any) => i.name) ?? [];
|
||||
const scenes = outline?.scenes?.map((i: any) => i.name) ?? [];
|
||||
|
||||
const assetList =
|
||||
[
|
||||
characters.length ? `【角色】${characters.join("、")}` : "",
|
||||
props.length ? `【道具】${props.join("、")}` : "",
|
||||
scenes.length ? `【场景】${scenes.join("、")}` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n") || "无";
|
||||
|
||||
return `<环境信息>
|
||||
项目ID: ${this.projectId}
|
||||
系统时间: ${new Date().toLocaleString()}
|
||||
|
||||
项目名称: ${projectInfo?.name || "未知"}
|
||||
项目简介: ${projectInfo?.intro || "无"}
|
||||
类型: ${projectInfo?.type || "未知"}
|
||||
风格: ${projectInfo?.artStyle || "未知"}
|
||||
视频比例: ${projectInfo?.videoRatio || "未知"}
|
||||
|
||||
资产列表:
|
||||
${assetList}
|
||||
|
||||
</环境信息>`;
|
||||
}
|
||||
|
||||
private buildConversationHistory(): string {
|
||||
if (!this.history.length) return "无对话历史";
|
||||
return this.history.map(([role, content]) => `${role}: ${content}`).join("\n\n");
|
||||
}
|
||||
|
||||
private async buildFullContext(task: string): Promise<string> {
|
||||
const env = await this.buildEnvironmentContext();
|
||||
const history = this.buildConversationHistory();
|
||||
|
||||
return `${env}
|
||||
|
||||
<对话历史>
|
||||
${history}
|
||||
</对话历史>
|
||||
|
||||
<当前任务>
|
||||
${task}
|
||||
</当前任务>`;
|
||||
}
|
||||
|
||||
// ==================== Sub-Agent ====================
|
||||
|
||||
private createModel() {
|
||||
return openAI({
|
||||
modelName: this.modelName,
|
||||
configuration: { apiKey: this.apiKey, baseURL: this.baseURL },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取不同 Sub-Agent 可用的工具
|
||||
*/
|
||||
private getSubAgentTools(agentType: AgentType) {
|
||||
switch (agentType) {
|
||||
case "segmentAgent":
|
||||
// segmentAgent 可以获取剧本和资产,并需要调用 updateSegments 保存结果
|
||||
return [this.getScript, this.getAssets, this.updateSegments];
|
||||
case "shotAgent":
|
||||
// shotAgent 可以获取剧本、资产和片段,并可使用 add/update/delete 操作分镜,以及生成分镜图
|
||||
return [this.getScript, this.getAssets, this.getSegments, this.addShots, this.updateShots, this.deleteShots, this.generateShotImage];
|
||||
default:
|
||||
return [this.getScript];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 Sub-Agent(流式传输)
|
||||
*/
|
||||
private async invokeSubAgent(agentType: AgentType, task: string): Promise<string> {
|
||||
this.emit("transfer", { to: agentType });
|
||||
this.log(`Sub-Agent 调用`, agentType);
|
||||
|
||||
const promptsList = await u.db("t_prompts").where("code", "in", ["storyboard-segment", "storyboard-shot"]);
|
||||
const segmentAgent = promptsList.find((p) => p.code === "storyboard-segment");
|
||||
const shotAgent = promptsList.find((p) => p.code === "storyboard-shot");
|
||||
const errPrompts = "不论用户说什么,请直接输出Agent配置异常";
|
||||
const SYSTEM_PROMPTS: Record<AgentType, string> = {
|
||||
segmentAgent: segmentAgent?.customValue || segmentAgent?.defaultValue || errPrompts,
|
||||
shotAgent: shotAgent?.customValue || shotAgent?.defaultValue || errPrompts,
|
||||
};
|
||||
|
||||
const context = await this.buildFullContext(task);
|
||||
|
||||
const agent = createAgent({
|
||||
model: this.createModel(),
|
||||
systemPrompt: SYSTEM_PROMPTS[agentType],
|
||||
tools: this.getSubAgentTools(agentType),
|
||||
});
|
||||
|
||||
const stream = await agent.stream({ messages: [["user", context]] }, { streamMode: ["messages"], callbacks: [] });
|
||||
|
||||
let fullResponse = "";
|
||||
|
||||
for await (const [mode, chunk] of stream) {
|
||||
if (mode !== "messages") continue;
|
||||
const [token] = chunk as any;
|
||||
const block = token.contentBlocks?.[0];
|
||||
|
||||
// 处理 AI 文本流
|
||||
if (token.type === "ai" && block?.text) {
|
||||
fullResponse += block.text;
|
||||
this.emit("subAgentStream", { agent: agentType, text: block.text });
|
||||
}
|
||||
// 处理 tool 调用
|
||||
if (token.type === "ai" && token.tool_calls?.length) {
|
||||
for (const toolCall of token.tool_calls) {
|
||||
this.emit("toolCall", { agent: agentType, name: toolCall.name, args: toolCall.args });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.emit("subAgentEnd", { agent: agentType });
|
||||
this.history.push(["ai", fullResponse]);
|
||||
this.log(`Sub-Agent 完成`, agentType);
|
||||
return fullResponse;
|
||||
}
|
||||
|
||||
private createSubAgentTool(agentType: AgentType, description: string) {
|
||||
return tool(async ({ taskDescription }) => this.invokeSubAgent(agentType, taskDescription), {
|
||||
name: agentType,
|
||||
description,
|
||||
schema: z.object({
|
||||
taskDescription: z.string().describe("具体的任务描述,包含章节范围、修改要求等详细信息"),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 主入口 ====================
|
||||
|
||||
private getAllTools() {
|
||||
return [
|
||||
this.createSubAgentTool(
|
||||
"segmentAgent",
|
||||
"调用片段师。负责根据剧本生成片段,会自行调用 getScript 获取剧本内容,并调用 updateSegments 保存片段结果。",
|
||||
),
|
||||
this.createSubAgentTool(
|
||||
"shotAgent",
|
||||
"调用分镜师。负责根据片段生成分镜提示词,会自行调用 getSegments 获取片段数据,并调用 addShots/updateShots 保存分镜结果。",
|
||||
),
|
||||
// this.createSubAgentTool("director", "调用导演。负责审核故事线和大纲,会自行调用 updateOutline 或 saveStoryline 进行修改。"),
|
||||
this.getScript,
|
||||
this.getSegments,
|
||||
this.generateShotImage,
|
||||
...this.getSubAgentTools("segmentAgent"),
|
||||
...this.getSubAgentTools("shotAgent"),
|
||||
];
|
||||
}
|
||||
|
||||
async call(msg: string): Promise<string> {
|
||||
console.log("模型名称:", this.modelName);
|
||||
this.history.push(["user", msg]);
|
||||
|
||||
const envContext = await this.buildEnvironmentContext();
|
||||
|
||||
const prompts = await u.db("t_prompts").where("code", "storyboard-main").first();
|
||||
|
||||
const mainPrompts = prompts?.customValue || prompts?.defaultValue || "不论用户说什么,请直接输出Agent配置异常";
|
||||
|
||||
const mainAgent = createAgent({
|
||||
model: this.createModel(),
|
||||
tools: this.getAllTools(),
|
||||
systemPrompt: `${envContext}\n${mainPrompts}`,
|
||||
});
|
||||
const stream = await mainAgent.stream({ messages: this.history }, { streamMode: ["messages"], callbacks: [] });
|
||||
|
||||
let fullResponse = "";
|
||||
|
||||
for await (const [mode, chunk] of stream) {
|
||||
if (mode !== "messages") continue;
|
||||
const [token] = chunk as any;
|
||||
const block = token.contentBlocks?.[0];
|
||||
// 处理 AI 文本流
|
||||
if (token.type === "ai" && block?.text) {
|
||||
fullResponse += block.text;
|
||||
this.emit("data", block.text);
|
||||
}
|
||||
|
||||
// 处理 tool 调用
|
||||
if (token.type === "ai" && token.tool_calls?.length) {
|
||||
for (const toolCall of token.tool_calls) {
|
||||
this.emit("toolCall", { agent: "main", name: toolCall.name, args: toolCall.args });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.history.push(["assistant", fullResponse]);
|
||||
this.emit("response", fullResponse);
|
||||
|
||||
return fullResponse;
|
||||
}
|
||||
}
|
||||
1411
output.json
1411
output.json
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "toonflow-app",
|
||||
"version": "1.0.7-patch3",
|
||||
"version": "1.0.7",
|
||||
"description": "Toonflow 是一款 AI 短剧漫剧工具,能够利用 AI 技术将小说自动转化为剧本,并结合 AI 生成的图片和视频,实现高效的短剧创作。",
|
||||
"author": "HBAI-Ltd <ltlctools@outlook.com>",
|
||||
"homepage": "https://github.com/HBAI-Ltd/Toonflow-app#readme",
|
||||
@ -34,13 +34,15 @@
|
||||
"license": "bun run scripts/license.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ag-ui/core": "^0.0.47",
|
||||
"@ai-sdk/anthropic": "^3.0.35",
|
||||
"@ai-sdk/deepseek": "^2.0.17",
|
||||
"@ai-sdk/devtools": "^0.0.11",
|
||||
"@ai-sdk/google": "^3.0.43",
|
||||
"@ai-sdk/google": "^3.0.20",
|
||||
"@ai-sdk/openai": "^3.0.25",
|
||||
"@ai-sdk/openai-compatible": "^2.0.27",
|
||||
"@ai-sdk/xai": "^3.0.47",
|
||||
"@huggingface/transformers": "^3.8.1",
|
||||
"@rmp135/sql-ts": "^2.2.0",
|
||||
"ai": "^6.0.67",
|
||||
"axios": "^1.13.2",
|
||||
@ -61,8 +63,11 @@
|
||||
"qwen-ai-provider-v5": "^2.1.0",
|
||||
"serialize-error": "^13.0.1",
|
||||
"sharp": "^0.34.5",
|
||||
"socket.io": "^4.8.3",
|
||||
"sqlite3": "^5.1.7",
|
||||
"sucrase": "^3.35.1",
|
||||
"uuid": "^13.0.0",
|
||||
"vm2": "^3.10.5",
|
||||
"zhipu-ai-provider": "^0.2.2",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,736 +0,0 @@
|
||||
// @/agents/outlineScript.ts
|
||||
import u from "@/utils";
|
||||
import { EventEmitter } from "events";
|
||||
import { tool, ModelMessage } from "ai";
|
||||
import { z } from "zod";
|
||||
import type { DB } from "@/types/database";
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
type AgentType = "AI1" | "AI2" | "director";
|
||||
type AssetType = "角色" | "道具" | "场景";
|
||||
type RefreshEvent = "storyline" | "outline" | "assets";
|
||||
|
||||
interface AssetItem {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface EpisodeData {
|
||||
episodeIndex: number;
|
||||
title: string;
|
||||
chapterRange: number[];
|
||||
scenes: AssetItem[]; // 按 outline 出场顺序排列
|
||||
characters: AssetItem[]; // 按 outline 出场顺序排列
|
||||
props: AssetItem[]; // 按 outline 出场顺序排列
|
||||
coreConflict: string;
|
||||
outline: string; // 最高优先级,剧本生成的唯一权威
|
||||
openingHook: string; // outline 第一句话的视觉化,开篇第一个镜头
|
||||
keyEvents: string[]; // 4个元素:[起, 承, 转, 合],严格按 outline 顺序
|
||||
emotionalCurve: string; // 对应 keyEvents 各阶段
|
||||
visualHighlights: string[]; // 按 outline 顺序排列的标志性镜头
|
||||
endingHook: string; // outline 之后的悬念延伸
|
||||
classicQuotes: string[];
|
||||
}
|
||||
|
||||
// ==================== Schema 定义 ====================
|
||||
|
||||
const sceneItemSchema = z.object({
|
||||
name: z.string().describe("场景名称,如'五星酒店宴会厅'、'老旧出租屋'"),
|
||||
description: z.string().describe("环境描写:空间结构、光线氛围、装饰陈设、环境细节"),
|
||||
});
|
||||
|
||||
const characterItemSchema = z.object({
|
||||
name: z.string().describe("角色姓名(必须是具体人名,禁止'众人'、'群众'等集合描述)"),
|
||||
description: z.string().describe("人设样貌:年龄体态、五官特征、发型妆容、服装配饰、气质神态"),
|
||||
});
|
||||
|
||||
const propItemSchema = z.object({
|
||||
name: z.string().describe("道具名称"),
|
||||
description: z.string().describe("样式描写:材质质感、颜色图案、形状尺寸、磨损痕迹、特殊标记"),
|
||||
});
|
||||
|
||||
const episodeSchema = z.object({
|
||||
episodeIndex: z.number().describe("集数索引,从1开始递增"),
|
||||
title: z.string().describe("8字内标题,疑问/感叹句,含情绪爆点"),
|
||||
chapterRange: z.array(z.number()).describe("关联章节号数组"),
|
||||
scenes: z.array(sceneItemSchema).describe("场景列表,按 outline 出场顺序排列"),
|
||||
characters: z.array(characterItemSchema).describe("角色列表,按 outline 出场顺序排列,必须是独立个体"),
|
||||
props: z.array(propItemSchema).describe("道具列表,按 outline 出场顺序排列,至少3个"),
|
||||
coreConflict: z.string().describe("核心矛盾:A想要X vs B阻碍X"),
|
||||
outline: z.string().describe("100-300字剧情主干,最高优先级,剧本生成的唯一权威,按时间顺序完整叙述"),
|
||||
openingHook: z.string().describe("开场镜头:outline 第一句话的视觉化,必须作为剧本第一个镜头"),
|
||||
keyEvents: z.array(z.string()).length(4).describe("4个元素的数组:[起, 承, 转, 合],严格按 outline 顺序从中提取"),
|
||||
emotionalCurve: z.string().describe("情绪曲线,如:2(压抑)→5(反抗)→9(爆发)→3(余波),对应 keyEvents 各阶段"),
|
||||
visualHighlights: z.array(z.string()).describe("3-5个标志性镜头,按 outline 叙事顺序排列"),
|
||||
endingHook: z.string().describe("结尾悬念:outline 之后的延伸,勾引下集"),
|
||||
classicQuotes: z.array(z.string()).describe("1-2句金句,每句≤15字,必须从原文提取"),
|
||||
});
|
||||
|
||||
// ==================== 常量配置 ====================
|
||||
|
||||
// ==================== 主类 ====================
|
||||
|
||||
export default class OutlineScript {
|
||||
private readonly projectId: number;
|
||||
readonly emitter = new EventEmitter();
|
||||
history: Array<ModelMessage> = [];
|
||||
novelChapters: DB["t_novel"][] = [];
|
||||
|
||||
constructor(projectId: number) {
|
||||
this.projectId = projectId;
|
||||
}
|
||||
|
||||
// ==================== 公共方法 ====================
|
||||
|
||||
get events() {
|
||||
return this.emitter;
|
||||
}
|
||||
|
||||
setNovel(chapters: DB["t_novel"][]) {
|
||||
this.novelChapters = chapters;
|
||||
}
|
||||
|
||||
// ==================== 私有工具方法 ====================
|
||||
|
||||
private emit(event: string, data?: any) {
|
||||
this.emitter.emit(event, data);
|
||||
}
|
||||
|
||||
private refresh(type: RefreshEvent) {
|
||||
this.emit("refresh", type);
|
||||
}
|
||||
|
||||
private log(action: string, detail?: string) {
|
||||
const msg = detail ? `${action}: ${detail}` : action;
|
||||
console.log(`\n[${new Date().toLocaleTimeString()}] ${msg}\n`);
|
||||
}
|
||||
|
||||
private safeParseJson<T>(str: string, fallback: T): T {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private uniqueByName<T extends { name: string }>(items: T[]): T[] {
|
||||
return Array.from(new Map(items.map((item) => [item.name, item])).values());
|
||||
}
|
||||
|
||||
// ==================== 数据库操作 ====================
|
||||
|
||||
private async getProjectInfo(): Promise<any> {
|
||||
return u.db("t_project").where({ id: this.projectId }).first();
|
||||
}
|
||||
|
||||
private async getNovelInfo(asString = false): Promise<any> {
|
||||
const info = await this.getProjectInfo();
|
||||
if (!info) return asString ? "未查询到项目信息" : null;
|
||||
|
||||
if (asString) {
|
||||
const fields = [
|
||||
`小说名称: ${info.name}`,
|
||||
`小说简介: ${info.intro}`,
|
||||
`小说类型: ${info.type}`,
|
||||
`目标短剧类型: ${info.artStyle}`,
|
||||
`短剧画幅: ${info.videoRatio}`,
|
||||
];
|
||||
return fields.join("\n");
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
// ==================== 故事线操作 ====================
|
||||
|
||||
private async findStoryline() {
|
||||
return u.db("t_storyline").where({ projectId: this.projectId }).first();
|
||||
}
|
||||
|
||||
private async upsertStorylineContent(content: string) {
|
||||
const existing = await this.findStoryline();
|
||||
if (existing) {
|
||||
await u.db("t_storyline").where({ projectId: this.projectId }).update({ content });
|
||||
} else {
|
||||
await u.db("t_storyline").insert({ projectId: this.projectId, content });
|
||||
}
|
||||
this.refresh("storyline");
|
||||
}
|
||||
|
||||
private async deleteStorylineContent() {
|
||||
const deleted = await u.db("t_storyline").where({ projectId: this.projectId }).del();
|
||||
this.refresh("storyline");
|
||||
return deleted;
|
||||
}
|
||||
|
||||
// ==================== 大纲操作 ====================
|
||||
|
||||
private async findOutlines() {
|
||||
return u.db("t_outline").where({ projectId: this.projectId }).orderBy("episode", "asc");
|
||||
}
|
||||
|
||||
private async findOutlineById(id: number) {
|
||||
return u.db("t_outline").where({ id, projectId: this.projectId }).first();
|
||||
}
|
||||
|
||||
private async getMaxEpisode(): Promise<number> {
|
||||
const result: any = await u.db("t_outline").where({ projectId: this.projectId }).max("episode as max").first();
|
||||
return result?.max ?? 0;
|
||||
}
|
||||
|
||||
private async clearOutlinesAndScripts() {
|
||||
const outlines = await u.db("t_outline").select("id").where({ projectId: this.projectId });
|
||||
if (outlines.length === 0) return 0;
|
||||
|
||||
const outlineIds = outlines.map((o) => o.id);
|
||||
await u.db("t_script").whereIn("outlineId", outlineIds).del();
|
||||
await u.db("t_outline").where({ projectId: this.projectId }).del();
|
||||
|
||||
return outlines.length;
|
||||
}
|
||||
|
||||
private async insertOutlines(episodes: EpisodeData[], startEpisode: number) {
|
||||
const insertList = episodes.map((ep, idx) => ({
|
||||
projectId: this.projectId,
|
||||
data: JSON.stringify({ ...ep, episodeIndex: startEpisode + idx }),
|
||||
episode: startEpisode + idx,
|
||||
}));
|
||||
|
||||
await u.db("t_outline").insert(insertList);
|
||||
return insertList.length;
|
||||
}
|
||||
|
||||
private async createEmptyScripts(outlineIds: Array<{ id: number; data: string }>) {
|
||||
const scripts = outlineIds.map((item) => {
|
||||
const data = this.safeParseJson<Partial<EpisodeData>>(item.data, {});
|
||||
return {
|
||||
name: `第${data.episodeIndex ?? ""}集`,
|
||||
content: "",
|
||||
projectId: this.projectId,
|
||||
outlineId: item.id,
|
||||
};
|
||||
});
|
||||
|
||||
if (scripts.length > 0) {
|
||||
await u.db("t_script").insert(scripts);
|
||||
}
|
||||
return scripts.length;
|
||||
}
|
||||
|
||||
private async saveOutlineData(episodes: EpisodeData[], overwrite: boolean, startEpisode?: number) {
|
||||
if (overwrite) {
|
||||
const cleared = await this.clearOutlinesAndScripts();
|
||||
if (cleared > 0) {
|
||||
this.log("清理旧数据", `删除了 ${cleared} 条大纲及关联剧本`);
|
||||
}
|
||||
}
|
||||
|
||||
const actualStart = overwrite ? 1 : (startEpisode ?? (await this.getMaxEpisode()) + 1);
|
||||
const insertedCount = await this.insertOutlines(episodes, actualStart);
|
||||
|
||||
const newOutlines = await u
|
||||
.db("t_outline")
|
||||
.select("id", "data")
|
||||
.where({ projectId: this.projectId })
|
||||
.orderBy("episode", "desc")
|
||||
.limit(insertedCount);
|
||||
|
||||
const scriptCount = await this.createEmptyScripts(newOutlines as Array<{ id: number; data: string }>);
|
||||
|
||||
this.refresh("outline");
|
||||
return { insertedCount, scriptCount };
|
||||
}
|
||||
|
||||
private async updateOutlineData(id: number, data: EpisodeData) {
|
||||
const existing = await this.findOutlineById(id);
|
||||
if (!existing) return false;
|
||||
|
||||
await u
|
||||
.db("t_outline")
|
||||
.where({ id })
|
||||
.update({ data: JSON.stringify(data) });
|
||||
this.refresh("outline");
|
||||
return true;
|
||||
}
|
||||
|
||||
private async deleteOutlineData(ids: number[]) {
|
||||
const results = await Promise.allSettled(ids.map((id) => u.deleteOutline(id, this.projectId)));
|
||||
this.refresh("outline");
|
||||
return results;
|
||||
}
|
||||
|
||||
private formatOutlineDetail(ep: any): string {
|
||||
const formatList = (items: any[], formatter: (item: any) => string) =>
|
||||
items?.map((item, i) => ` ${i + 1}. ${formatter(item)}`).join("\n") || " 无";
|
||||
|
||||
// keyEvents 按顺序显示:起、承、转、合
|
||||
const keyEventsLabels = ["起", "承", "转", "合"];
|
||||
const formatKeyEvents = (events: string[]) => events?.map((e, i) => ` 【${keyEventsLabels[i] || i + 1}】${e}`).join("\n") || " 无";
|
||||
|
||||
return `
|
||||
大纲ID: ${ep.id}
|
||||
第 ${ep.episodeIndex} 集: ${ep.title || ""}
|
||||
${"=".repeat(50)}
|
||||
章节范围: ${ep.chapterRange?.join(", ") || ""}
|
||||
核心矛盾: ${ep.coreConflict || ""}
|
||||
|
||||
【剧情主干】(最高优先级,剧本生成的唯一权威):
|
||||
${ep.outline || "无"}
|
||||
|
||||
【开场镜头】(必须作为剧本第一个镜头):
|
||||
${ep.openingHook || "无"}
|
||||
|
||||
【剧情节点】(严格按顺序:起→承→转→合):
|
||||
${formatKeyEvents(ep.keyEvents)}
|
||||
|
||||
情绪曲线: ${ep.emotionalCurve || ""}
|
||||
|
||||
【视觉重点】(按剧情主干顺序排列):
|
||||
${formatList(ep.visualHighlights, (v) => v)}
|
||||
|
||||
【结尾悬念】:
|
||||
${ep.endingHook || "无"}
|
||||
|
||||
【经典台词】:
|
||||
${formatList(ep.classicQuotes, (q) => q)}
|
||||
|
||||
角色(按出场顺序): ${ep.characters?.map((c: AssetItem) => `${c.name}(${c.description})`).join("; ") || "无"}
|
||||
场景(按出场顺序): ${ep.scenes?.map((s: AssetItem) => `${s.name}(${s.description})`).join("; ") || "无"}
|
||||
道具(按出场顺序): ${ep.props?.map((p: AssetItem) => `${p.name}(${p.description})`).join("; ") || "无"}`;
|
||||
}
|
||||
|
||||
private async getOutlineText(simplified: boolean): Promise<string> {
|
||||
const records = await this.findOutlines();
|
||||
|
||||
if (!records.length) return "当前项目暂无大纲";
|
||||
|
||||
const episodes = records.map((r) => ({
|
||||
id: r.id,
|
||||
episode: r.episode,
|
||||
...this.safeParseJson<Partial<EpisodeData>>(r.data ?? "{}", {}),
|
||||
}));
|
||||
|
||||
if (simplified) {
|
||||
const list = episodes.map((ep) => `第 ${ep.episodeIndex ?? ep.episode} 集 (id=${ep.id})`).join("\n");
|
||||
return `项目大纲 (共 ${episodes.length} 集):\n${list}`;
|
||||
}
|
||||
|
||||
const details = episodes.map((ep) => this.formatOutlineDetail(ep)).join("\n");
|
||||
return `项目大纲 (共 ${episodes.length} 集)\n\n${details}`;
|
||||
}
|
||||
|
||||
// ==================== 资产操作 ====================
|
||||
|
||||
private async findAssetByTypeAndName(type: AssetType, name: string) {
|
||||
return u.db("t_assets").where({ projectId: this.projectId, type, name }).first();
|
||||
}
|
||||
|
||||
private async upsertAsset(type: AssetType, item: AssetItem): Promise<"inserted" | "updated" | "skipped"> {
|
||||
const existing = await this.findAssetByTypeAndName(type, item.name);
|
||||
|
||||
if (!existing) {
|
||||
await u.db("t_assets").insert({
|
||||
projectId: this.projectId,
|
||||
type,
|
||||
name: item.name,
|
||||
intro: item.description,
|
||||
prompt: item.description,
|
||||
});
|
||||
return "inserted";
|
||||
}
|
||||
|
||||
if (existing.intro !== item.description) {
|
||||
await u.db("t_assets").where({ id: existing.id }).update({
|
||||
intro: item.description,
|
||||
prompt: item.description,
|
||||
});
|
||||
return "updated";
|
||||
}
|
||||
|
||||
return "skipped";
|
||||
}
|
||||
|
||||
private extractAssetsFromOutlines(outlines: Array<{ data?: string | null | undefined }>): {
|
||||
characters: AssetItem[];
|
||||
props: AssetItem[];
|
||||
scenes: AssetItem[];
|
||||
} {
|
||||
const result = { characters: [] as AssetItem[], props: [] as AssetItem[], scenes: [] as AssetItem[] };
|
||||
|
||||
for (const outline of outlines) {
|
||||
const data = this.safeParseJson<Partial<EpisodeData>>(outline.data ?? "{}", {});
|
||||
if (data.characters) result.characters.push(...data.characters);
|
||||
if (data.props) result.props.push(...data.props);
|
||||
if (data.scenes) result.scenes.push(...data.scenes);
|
||||
}
|
||||
|
||||
return {
|
||||
characters: this.uniqueByName(result.characters),
|
||||
props: this.uniqueByName(result.props),
|
||||
scenes: this.uniqueByName(result.scenes),
|
||||
};
|
||||
}
|
||||
|
||||
private async generateAssetsFromOutlines() {
|
||||
const outlines = await u.db("t_outline").select("data").where({ projectId: this.projectId });
|
||||
|
||||
if (!outlines.length) return { inserted: 0, updated: 0, skipped: 0 };
|
||||
|
||||
const { characters, props, scenes } = this.extractAssetsFromOutlines(outlines);
|
||||
|
||||
// 只做新增和更新,不做删除
|
||||
const stats = { inserted: 0, updated: 0, skipped: 0 };
|
||||
|
||||
const processItems = async (items: AssetItem[], type: AssetType) => {
|
||||
for (const item of items) {
|
||||
const result = await this.upsertAsset(type, item);
|
||||
stats[result]++;
|
||||
}
|
||||
};
|
||||
|
||||
await processItems(characters, "角色");
|
||||
await processItems(props, "道具");
|
||||
await processItems(scenes, "场景");
|
||||
|
||||
this.refresh("assets");
|
||||
return { ...stats };
|
||||
}
|
||||
|
||||
// ==================== Tool 定义:故事线 ====================
|
||||
|
||||
getStoryline = tool({
|
||||
title: "getStoryline",
|
||||
description: "Get the weather in a location",
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => {
|
||||
this.log("获取故事线");
|
||||
const storyline = await this.findStoryline();
|
||||
return storyline?.content ?? "当前项目暂无故事线";
|
||||
},
|
||||
});
|
||||
|
||||
saveStoryline = tool({
|
||||
title: "saveStoryline",
|
||||
description: "保存或更新当前项目的故事线,会覆盖已有内容",
|
||||
inputSchema: z.object({
|
||||
content: z.string().describe("故事线完整内容"),
|
||||
}),
|
||||
execute: async ({ content }) => {
|
||||
this.log("保存故事线");
|
||||
await this.upsertStorylineContent(content);
|
||||
return "故事线保存成功";
|
||||
},
|
||||
});
|
||||
|
||||
deleteStoryline = tool({
|
||||
title: "deleteStoryline",
|
||||
description: "删除当前项目的故事线",
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => {
|
||||
this.log("删除故事线");
|
||||
const deleted = await this.deleteStorylineContent();
|
||||
return deleted > 0 ? "故事线删除成功" : "当前项目没有故事线";
|
||||
},
|
||||
});
|
||||
|
||||
// ==================== Tool 定义:大纲 ====================
|
||||
|
||||
getOutline = tool({
|
||||
title: "getOutline",
|
||||
description: "获取项目大纲。simplified=true返回简化列表,false返回完整内容",
|
||||
inputSchema: z.object({
|
||||
simplified: z.boolean().default(false).describe("是否返回简化版本"),
|
||||
}),
|
||||
execute: async ({ simplified }) => {
|
||||
this.log("获取大纲", `简化模式: ${simplified}`);
|
||||
return this.getOutlineText(simplified);
|
||||
},
|
||||
});
|
||||
|
||||
saveOutline = tool({
|
||||
title: "saveOutline",
|
||||
description: "保存大纲数据。overwrite=true会清空现有大纲后写入,false则追加到末尾",
|
||||
inputSchema: z.object({
|
||||
episodes: z.array(episodeSchema).min(1).describe("大纲数据数组"),
|
||||
overwrite: z.boolean().default(true).describe("是否覆盖现有大纲"),
|
||||
startEpisode: z.number().optional().describe("追加模式下的起始集数(不填则自动递增)"),
|
||||
}),
|
||||
execute: async ({ episodes, overwrite = true, startEpisode }) => {
|
||||
this.log("保存大纲", `覆盖模式: ${overwrite}, 集数: ${episodes.length}`);
|
||||
const { insertedCount, scriptCount } = await this.saveOutlineData(episodes as EpisodeData[], overwrite, startEpisode);
|
||||
return `大纲保存成功:插入 ${insertedCount} 集大纲,创建 ${scriptCount} 个剧本记录`;
|
||||
},
|
||||
});
|
||||
|
||||
updateOutline = tool({
|
||||
title: "updateOutline",
|
||||
description: "更新指定ID的单集大纲内容",
|
||||
inputSchema: z.object({
|
||||
id: z.number().describe("大纲ID"),
|
||||
data: episodeSchema.describe("更新后的大纲数据"),
|
||||
}),
|
||||
execute: async ({ id, data }) => {
|
||||
this.log("更新大纲", `ID: ${id}`);
|
||||
const success = await this.updateOutlineData(id, data as EpisodeData);
|
||||
return success ? `大纲ID ${id} 更新成功` : `未找到大纲ID: ${id}`;
|
||||
},
|
||||
});
|
||||
|
||||
deleteOutline = tool({
|
||||
title: "deleteOutline",
|
||||
description: "根据大纲ID删除指定大纲及关联数据",
|
||||
inputSchema: z.object({
|
||||
ids: z.array(z.number()).min(1).describe("要删除的大纲ID数组"),
|
||||
}),
|
||||
execute: async ({ ids }) => {
|
||||
this.log("删除大纲", `IDs: ${ids.join(", ")}`);
|
||||
const results = await this.deleteOutlineData(ids);
|
||||
const summary = results.map((r, i) => `ID ${ids[i]}: ${r.status === "fulfilled" ? "成功" : "失败"}`).join(", ");
|
||||
return `删除结果: ${summary}`;
|
||||
},
|
||||
});
|
||||
|
||||
// ==================== Tool 定义:章节 ====================
|
||||
|
||||
getChapter = tool({
|
||||
title: "getChapter",
|
||||
description: "根据章节编号获取小说章节的完整原文内容,支持批量获取",
|
||||
inputSchema: z.object({
|
||||
chapterNumbers: z.array(z.number()).min(1).describe("章节编号数组"),
|
||||
}),
|
||||
execute: async ({ chapterNumbers }) => {
|
||||
this.log("获取章节", `章节号: ${chapterNumbers.join(", ")}`);
|
||||
|
||||
const results = await Promise.all(
|
||||
chapterNumbers.map(async (num) => {
|
||||
const chapter = await u
|
||||
.db("t_novel")
|
||||
.where({ projectId: this.projectId, chapterIndex: num })
|
||||
.select("chapterData", "chapterIndex", "chapter")
|
||||
.first();
|
||||
|
||||
if (chapter) {
|
||||
return `\n【第${chapter.chapterIndex}章 ${chapter.chapter || ""}】\n${chapter.chapterData}`;
|
||||
}
|
||||
return `\n【第${num}章】未找到`;
|
||||
}),
|
||||
);
|
||||
|
||||
return results.join("\n\n---\n");
|
||||
},
|
||||
});
|
||||
|
||||
// ==================== Tool 定义:资产 ====================
|
||||
|
||||
generateAssets = tool({
|
||||
title: "generateAssets",
|
||||
description: "从当前项目的所有大纲中提取并生成角色、道具、场景资产,自动去重并清理冗余",
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => {
|
||||
this.log("生成资产");
|
||||
const stats = await this.generateAssetsFromOutlines();
|
||||
|
||||
if (stats.inserted === 0 && stats.updated === 0 && stats.skipped === 0) {
|
||||
return "当前项目没有大纲数据,无法生成资产";
|
||||
}
|
||||
return `资产生成完成:新增 ${stats.inserted},更新 ${stats.updated},保持 ${stats.skipped}`;
|
||||
},
|
||||
});
|
||||
|
||||
// ==================== 上下文构建 ====================
|
||||
|
||||
private getChapterContext(): string {
|
||||
if (!this.novelChapters.length) return "无章节数据";
|
||||
return this.novelChapters.map((c) => `章节号:${c.chapterIndex},分卷:${c.reel},章节名:${c.chapter}`).join("\n");
|
||||
}
|
||||
|
||||
private async buildEnvironmentContext(): Promise<string> {
|
||||
const [novelInfo, storyline, outlineCount] = await Promise.all([
|
||||
this.getNovelInfo(true),
|
||||
this.findStoryline(),
|
||||
u.db("t_outline").where({ projectId: this.projectId }).count("id as count").first() as any,
|
||||
]);
|
||||
|
||||
return `<环境信息>
|
||||
项目ID: ${this.projectId}
|
||||
系统时间: ${new Date().toLocaleString()}
|
||||
|
||||
${novelInfo}
|
||||
|
||||
已加载章节列表:
|
||||
${this.getChapterContext()}
|
||||
|
||||
故事线状态: ${storyline ? "已生成" : "未生成"}
|
||||
大纲状态: 共 ${outlineCount?.count ?? 0} 集
|
||||
|
||||
可用工具:
|
||||
- getChapter: 获取章节原文
|
||||
- getStoryline/saveStoryline/deleteStoryline: 故事线操作
|
||||
- getOutline/saveOutline/updateOutline/deleteOutline: 大纲操作
|
||||
- generateAssets: 从大纲生成资产
|
||||
</环境信息>`;
|
||||
}
|
||||
|
||||
private buildConversationHistory(): string {
|
||||
if (!this.history.length) return "无对话历史";
|
||||
return this.history.map(({ role, content }) => `${role}: ${content}`).join("\n\n");
|
||||
}
|
||||
|
||||
private async buildFullContext(task: string): Promise<string> {
|
||||
const env = await this.buildEnvironmentContext();
|
||||
const history = this.buildConversationHistory();
|
||||
|
||||
return `${env}
|
||||
|
||||
<对话历史>
|
||||
${history}
|
||||
</对话历史>
|
||||
|
||||
<当前任务>
|
||||
${task}
|
||||
</当前任务>`;
|
||||
}
|
||||
|
||||
// ==================== Sub-Agent ====================
|
||||
|
||||
private getSubAgentTools() {
|
||||
return {
|
||||
getChapter: this.getChapter,
|
||||
getStoryline: this.getStoryline,
|
||||
saveStoryline: this.saveStoryline,
|
||||
getOutline: this.getOutline,
|
||||
saveOutline: this.saveOutline,
|
||||
updateOutline: this.updateOutline,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 Sub-Agent(流式传输)
|
||||
*/
|
||||
private async invokeSubAgent(agentType: AgentType, task: string): Promise<string> {
|
||||
this.emit("transfer", { to: agentType });
|
||||
this.log(`Sub-Agent 调用`, agentType);
|
||||
|
||||
const promptsList = await u.db("t_prompts").where("code", "in", ["outlineScript-a1", "outlineScript-a2", "outlineScript-director"]);
|
||||
const promptConfig = await u.getPromptAi("outlineScriptAgent");
|
||||
|
||||
const errPrompts = "不论用户说什么,请直接输出Agent配置异常";
|
||||
|
||||
const getAiPromptConfig = (code: string) => {
|
||||
const item = promptsList.find((p) => p.code === code);
|
||||
return item?.customValue || item?.defaultValue || errPrompts;
|
||||
};
|
||||
const a1Prompt = getAiPromptConfig("outlineScript-a1");
|
||||
const a2Prompt = getAiPromptConfig("outlineScript-a2");
|
||||
const directorPrompt = getAiPromptConfig("outlineScript-director");
|
||||
const SYSTEM_PROMPTS = {
|
||||
AI1: a1Prompt,
|
||||
AI2: a2Prompt,
|
||||
director: directorPrompt,
|
||||
};
|
||||
|
||||
const context = await this.buildFullContext(task);
|
||||
|
||||
const { fullStream } = await u.ai.text.stream(
|
||||
{
|
||||
system: SYSTEM_PROMPTS[agentType],
|
||||
tools: this.getSubAgentTools(),
|
||||
messages: [{ role: "user", content: context }],
|
||||
maxStep: 100,
|
||||
},
|
||||
promptConfig,
|
||||
);
|
||||
|
||||
let fullResponse = "";
|
||||
for await (const item of fullStream) {
|
||||
if (item.type == "tool-call") {
|
||||
this.emit("toolCall", { agent: "main", name: item.title, args: null });
|
||||
}
|
||||
if (item.type == "text-delta") {
|
||||
fullResponse += item.text;
|
||||
this.emit("subAgentStream", { agent: agentType, text: item.text });
|
||||
}
|
||||
}
|
||||
|
||||
this.emit("subAgentEnd", { agent: agentType });
|
||||
this.history.push({
|
||||
role: "assistant",
|
||||
content: fullResponse,
|
||||
});
|
||||
this.log(`Sub-Agent 完成`, agentType);
|
||||
|
||||
return fullResponse ?? `${agentType}已完成任务`;
|
||||
}
|
||||
|
||||
private createSubAgentTool(agentType: AgentType, description: string) {
|
||||
return tool({
|
||||
title: agentType,
|
||||
description,
|
||||
inputSchema: z.object({
|
||||
taskDescription: z.string().describe("具体的任务描述,包含章节范围、修改要求等详细信息"),
|
||||
}),
|
||||
execute: async ({ taskDescription }) => this.invokeSubAgent(agentType, taskDescription),
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 主入口 ====================
|
||||
|
||||
private getAllTools() {
|
||||
return {
|
||||
AI1: this.createSubAgentTool("AI1", "调用故事师。负责分析小说原文并生成故事线,会自行调用 saveStoryline 保存结果。"),
|
||||
AI2: this.createSubAgentTool("AI2", "调用大纲师。负责根据故事线生成剧集大纲,会自行调用 saveOutline 保存结果。"),
|
||||
director: this.createSubAgentTool("director", "调用导演。负责审核故事线和大纲,会自行调用 updateOutline 或 saveStoryline 进行修改。"),
|
||||
getChapter: this.getChapter,
|
||||
getStoryline: this.getStoryline,
|
||||
saveStoryline: this.saveStoryline,
|
||||
deleteStoryline: this.deleteStoryline,
|
||||
getOutline: this.getOutline,
|
||||
saveOutline: this.saveOutline,
|
||||
updateOutline: this.updateOutline,
|
||||
deleteOutline: this.deleteOutline,
|
||||
generateAssets: this.generateAssets,
|
||||
};
|
||||
}
|
||||
|
||||
async call(msg: string): Promise<string> {
|
||||
this.history.push({
|
||||
role: "user",
|
||||
content: msg,
|
||||
});
|
||||
|
||||
const envContext = await this.buildEnvironmentContext();
|
||||
|
||||
const prompts = await u.db("t_prompts").where("code", "outlineScript-main").first();
|
||||
const promptConfig = await u.getPromptAi("outlineScriptAgent");
|
||||
|
||||
const mainPrompts = prompts?.customValue || prompts?.defaultValue || "不论用户说什么,请直接输出Agent配置异常";
|
||||
|
||||
const { fullStream } = await u.ai.text.stream(
|
||||
{
|
||||
system: `${envContext}\n${mainPrompts}`,
|
||||
tools: this.getAllTools(),
|
||||
messages: this.history,
|
||||
maxStep: 100,
|
||||
},
|
||||
promptConfig,
|
||||
);
|
||||
|
||||
let fullResponse = "";
|
||||
for await (const item of fullStream) {
|
||||
if (item.type == "tool-call") {
|
||||
this.emit("toolCall", { agent: "main", name: item.title, args: null });
|
||||
}
|
||||
if (item.type == "text-delta") {
|
||||
fullResponse += item.text;
|
||||
this.emit("data", item.text);
|
||||
}
|
||||
}
|
||||
this.history.push({
|
||||
role: "assistant",
|
||||
content: fullResponse,
|
||||
});
|
||||
|
||||
this.emit("response", fullResponse);
|
||||
|
||||
return fullResponse;
|
||||
}
|
||||
}
|
||||
@ -1,146 +0,0 @@
|
||||
import u from "@/utils";
|
||||
|
||||
type AspectRatio = "16:9" | "9:16" | "21:9" | "1:1" | "4:3" | "3:4" | "3:2" | "2:3";
|
||||
|
||||
interface GridLayoutResult {
|
||||
cols: number;
|
||||
rows: number;
|
||||
totalCells: number;
|
||||
placeholderCount: number;
|
||||
}
|
||||
|
||||
interface GridPromptOptions {
|
||||
prompts: string[];
|
||||
style: string;
|
||||
aspectRatio: AspectRatio;
|
||||
assetsName: { name: string; intro: string }[];
|
||||
}
|
||||
|
||||
interface GridPromptResult {
|
||||
prompt: string;
|
||||
gridLayout: GridLayoutResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据prompts数量计算宫格布局
|
||||
*/
|
||||
function calculateGridLayout(count: number): GridLayoutResult {
|
||||
let cols: number;
|
||||
let rows: number;
|
||||
if (count <= 0) {
|
||||
cols = 1;
|
||||
rows = 1;
|
||||
} else if (count === 1) {
|
||||
cols = 1;
|
||||
rows = 1;
|
||||
} else if (count === 2) {
|
||||
cols = 2;
|
||||
rows = 1;
|
||||
} else if (count === 3) {
|
||||
cols = 3;
|
||||
rows = 1;
|
||||
} else if (count === 4) {
|
||||
cols = 2;
|
||||
rows = 2;
|
||||
} else if (count <= 9) {
|
||||
cols = 3;
|
||||
rows = 3;
|
||||
} else {
|
||||
cols = 3;
|
||||
rows = Math.ceil(count / 3);
|
||||
}
|
||||
const totalCells = cols * rows;
|
||||
const placeholderCount = totalCells - count;
|
||||
return { cols, rows, totalCells, placeholderCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取宽高比描述
|
||||
*/
|
||||
function getAspectRatioDescription(aspectRatio: AspectRatio): string {
|
||||
const descriptions: Record<AspectRatio, string> = {
|
||||
"16:9": "电影宽银幕",
|
||||
"9:16": "竖屏短剧",
|
||||
"21:9": "超宽银幕史诗感",
|
||||
"1:1": "方形构图",
|
||||
"4:3": "经典银幕",
|
||||
"3:4": "竖版经典",
|
||||
"3:2": "摄影标准",
|
||||
"2:3": "竖版摄影",
|
||||
};
|
||||
return descriptions[aspectRatio] || "标准比例";
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成电影级宫格分镜提示词
|
||||
*/
|
||||
async function generateGridPrompt(options: GridPromptOptions): Promise<GridPromptResult> {
|
||||
const { prompts, style, aspectRatio, assetsName } = options;
|
||||
const layout = calculateGridLayout(prompts.length);
|
||||
const aspectRatioDesc = getAspectRatioDescription(aspectRatio);
|
||||
|
||||
// 构建宫格位置描述
|
||||
const gridPositions: string[] = [];
|
||||
for (let i = 0; i < layout.totalCells; i++) {
|
||||
const row = Math.floor(i / layout.cols) + 1;
|
||||
const col = (i % layout.cols) + 1;
|
||||
if (i < prompts.length) {
|
||||
gridPositions.push(`[第${row}行第${col}列]: ${prompts[i]}`);
|
||||
} else {
|
||||
gridPositions.push(`[第${row}行第${col}列]: 纯黑图`);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建资产说明
|
||||
const assetsSection =
|
||||
assetsName.length > 0
|
||||
? `\n【可用资产】\n${assetsName.map((a) => `- ${a.name}:${a.intro}`).join("\n")}\n\n⚠️ 必须使用完整资产名称,禁止简称或代词。`
|
||||
: "";
|
||||
|
||||
const promptsData = await u.db("t_prompts").where("code", "generateImagePrompts").first();
|
||||
const promptAiConfig = await u.getPromptAi("storyboardAgent");
|
||||
const mainPrompts = promptsData?.customValue || promptsData?.defaultValue;
|
||||
const errData = `请输出${options.prompts.length}张图片\n提示词如下:\n${options.prompts.map((p, i) => `第${i + 1}格: ${p}`).join("\n")}`;
|
||||
|
||||
if (!mainPrompts) return { prompt: errData, gridLayout: layout };
|
||||
|
||||
const result = await u.ai.text.invoke(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: mainPrompts,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `请优化以下分镜提示词:\n\n【布局】${layout.cols}列×${layout.rows}行=${
|
||||
layout.totalCells
|
||||
}格\n【比例】${aspectRatio}(${aspectRatioDesc})\n【风格】${style}\n${assetsSection}\n\n【原始内容】\n${gridPositions.join("\n")}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
promptAiConfig,
|
||||
);
|
||||
|
||||
// const result = await chatModel!.invoke({
|
||||
// messages: [
|
||||
// {
|
||||
// role: "system",
|
||||
// content: mainPrompts,
|
||||
// },
|
||||
// {
|
||||
// role: "user",
|
||||
// content: `请优化以下分镜提示词:\n\n【布局】${layout.cols}列×${layout.rows}行=${
|
||||
// layout.totalCells
|
||||
// }格\n【比例】${aspectRatio}(${aspectRatioDesc})\n【风格】${style}\n${assetsSection}\n\n【原始内容】\n${gridPositions.join("\n")}`,
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
|
||||
return {
|
||||
prompt: result.text ?? errData,
|
||||
gridLayout: layout,
|
||||
};
|
||||
}
|
||||
|
||||
export default generateGridPrompt;
|
||||
@ -1,337 +0,0 @@
|
||||
import generateImagePromptsTool from "@/agents/storyboard/generateImagePromptsTool";
|
||||
import u from "@/utils";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
|
||||
interface AssetItem {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface EpisodeData {
|
||||
episodeIndex: number;
|
||||
title: string;
|
||||
chapterRange: number[];
|
||||
scenes: AssetItem[];
|
||||
characters: AssetItem[];
|
||||
props: AssetItem[];
|
||||
coreConflict: string;
|
||||
openingHook: string;
|
||||
outline: string;
|
||||
keyEvents: string[];
|
||||
emotionalCurve: string;
|
||||
visualHighlights: string[];
|
||||
endingHook: string;
|
||||
classicQuotes: string[];
|
||||
}
|
||||
|
||||
interface ImageInfo {
|
||||
name: string;
|
||||
type: string;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
interface ResourceItem {
|
||||
name: string;
|
||||
intro: string;
|
||||
}
|
||||
|
||||
// 压缩图片直到不超过指定大小
|
||||
async function compressImage(buffer: Buffer, maxSizeBytes: number = 3 * 1024 * 1024): Promise<Buffer> {
|
||||
if (buffer.length <= maxSizeBytes) {
|
||||
return buffer;
|
||||
}
|
||||
let quality = 90;
|
||||
let compressedBuffer = await sharp(buffer).jpeg({ quality }).toBuffer();
|
||||
while (compressedBuffer.length > maxSizeBytes && quality > 10) {
|
||||
quality -= 10;
|
||||
compressedBuffer = await sharp(buffer).jpeg({ quality }).toBuffer();
|
||||
}
|
||||
if (compressedBuffer.length > maxSizeBytes) {
|
||||
const metadata = await sharp(buffer).metadata();
|
||||
let scale = 0.9;
|
||||
while (compressedBuffer.length > maxSizeBytes && scale > 0.1) {
|
||||
const newWidth = Math.round((metadata.width || 1000) * scale);
|
||||
const newHeight = Math.round((metadata.height || 1000) * scale);
|
||||
compressedBuffer = await sharp(buffer)
|
||||
.resize(newWidth, newHeight, { fit: "inside" })
|
||||
.jpeg({ quality: Math.max(quality, 30) })
|
||||
.toBuffer();
|
||||
scale -= 0.1;
|
||||
}
|
||||
}
|
||||
return compressedBuffer;
|
||||
}
|
||||
|
||||
// 拼接多张图片为一张
|
||||
async function mergeImages(imagePaths: string[]): Promise<Buffer> {
|
||||
const imageBuffers = await Promise.all(imagePaths.map((path) => u.oss.getFile(path)));
|
||||
const imageMetadatas = await Promise.all(imageBuffers.map((buffer) => sharp(buffer).metadata()));
|
||||
const maxHeight = Math.max(...imageMetadatas.map((m) => m.height || 0));
|
||||
const resizedImages = await Promise.all(
|
||||
imageBuffers.map(async (buffer, index) => {
|
||||
const metadata = imageMetadatas[index];
|
||||
const aspectRatio = (metadata.width || 1) / (metadata.height || 1);
|
||||
const newWidth = Math.round(maxHeight * aspectRatio);
|
||||
return {
|
||||
buffer: await sharp(buffer).resize(newWidth, maxHeight, { fit: "cover" }).toBuffer(),
|
||||
width: newWidth,
|
||||
};
|
||||
}),
|
||||
);
|
||||
let currentX = 0;
|
||||
const compositeInputs = resizedImages.map(({ buffer, width }) => {
|
||||
const input = {
|
||||
input: buffer,
|
||||
left: currentX,
|
||||
top: 0,
|
||||
};
|
||||
currentX += width;
|
||||
return input;
|
||||
});
|
||||
const mergedImage = await sharp({
|
||||
create: {
|
||||
width: currentX,
|
||||
height: maxHeight,
|
||||
channels: 4,
|
||||
background: { r: 255, g: 255, b: 255, alpha: 1 },
|
||||
},
|
||||
})
|
||||
.composite(compositeInputs)
|
||||
.jpeg({ quality: 90 })
|
||||
.toBuffer();
|
||||
return compressImage(mergedImage);
|
||||
}
|
||||
|
||||
// 进一步压缩单张图片到指定大小
|
||||
async function compressToSize(buffer: Buffer, targetSize: number): Promise<Buffer> {
|
||||
if (buffer.length <= targetSize) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
const metadata = await sharp(buffer).metadata();
|
||||
let quality = 80;
|
||||
let scale = 1.0;
|
||||
let compressedBuffer = buffer;
|
||||
|
||||
// 先尝试降低质量
|
||||
while (compressedBuffer.length > targetSize && quality > 10) {
|
||||
compressedBuffer = await sharp(buffer).jpeg({ quality }).toBuffer();
|
||||
quality -= 10;
|
||||
}
|
||||
|
||||
// 如果还是太大,缩小尺寸
|
||||
while (compressedBuffer.length > targetSize && scale > 0.2) {
|
||||
scale -= 0.1;
|
||||
const newWidth = Math.round((metadata.width || 1000) * scale);
|
||||
const newHeight = Math.round((metadata.height || 1000) * scale);
|
||||
compressedBuffer = await sharp(buffer)
|
||||
.resize(newWidth, newHeight, { fit: "inside" })
|
||||
.jpeg({ quality: Math.max(quality, 20) })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
return compressedBuffer;
|
||||
}
|
||||
|
||||
// 确保图片列表总大小不超过指定限制
|
||||
async function ensureTotalSizeLimit(buffers: Buffer[], maxTotalBytes: number = 10 * 1024 * 1024): Promise<Buffer[]> {
|
||||
let totalSize = buffers.reduce((sum, buf) => sum + buf.length, 0);
|
||||
|
||||
if (totalSize <= maxTotalBytes) {
|
||||
return buffers;
|
||||
}
|
||||
|
||||
// 计算每张图片的平均目标大小
|
||||
const avgTargetSize = Math.floor(maxTotalBytes / buffers.length);
|
||||
|
||||
// 按大小降序排列,优先压缩大图片
|
||||
const indexedBuffers = buffers.map((buf, idx) => ({ buf, idx, size: buf.length }));
|
||||
indexedBuffers.sort((a, b) => b.size - a.size);
|
||||
|
||||
const result = [...buffers];
|
||||
|
||||
for (const item of indexedBuffers) {
|
||||
totalSize = result.reduce((sum, buf) => sum + buf.length, 0);
|
||||
if (totalSize <= maxTotalBytes) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 计算这张图片需要压缩到的目标大小
|
||||
const excessSize = totalSize - maxTotalBytes;
|
||||
const targetSize = Math.max(item.buf.length - excessSize, avgTargetSize, 100 * 1024); // 最小100KB
|
||||
|
||||
if (item.buf.length > targetSize) {
|
||||
result[item.idx] = await compressToSize(item.buf, targetSize);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 处理图片列表,确保不超过10张且每张不超过3MB,总大小不超过10MB
|
||||
async function processImages(images: ImageInfo[]): Promise<Buffer[]> {
|
||||
const maxImages = 10;
|
||||
let processedBuffers: Buffer[];
|
||||
|
||||
if (images.length <= maxImages) {
|
||||
const buffers = await Promise.all(images.map((img) => u.oss.getFile(img.filePath)));
|
||||
|
||||
processedBuffers = await Promise.all(buffers.map((buffer) => compressImage(buffer)));
|
||||
} else {
|
||||
const mergeStartIndex = maxImages - 1;
|
||||
|
||||
const firstBuffers = await Promise.all(images.slice(0, mergeStartIndex).map((img) => u.oss.getFile(img.filePath)));
|
||||
|
||||
const compressedFirstImages = await Promise.all(firstBuffers.map((buffer) => compressImage(buffer)));
|
||||
|
||||
const imagesToMergeList = images.slice(mergeStartIndex).map((img) => img.filePath);
|
||||
|
||||
const mergedImage = await mergeImages(imagesToMergeList);
|
||||
|
||||
processedBuffers = [...compressedFirstImages, mergedImage];
|
||||
}
|
||||
|
||||
// 确保总大小不超过10MB
|
||||
return ensureTotalSizeLimit(processedBuffers);
|
||||
}
|
||||
|
||||
// 使用 AI 过滤与分镜相关的资产
|
||||
async function filterRelevantAssets(prompts: string[], allResources: ResourceItem[], availableImages: ImageInfo[]): Promise<ImageInfo[]> {
|
||||
if (allResources.length === 0 || availableImages.length === 0) {
|
||||
return availableImages;
|
||||
}
|
||||
|
||||
const availableNames = new Set(availableImages.map((img) => img.name));
|
||||
const availableResources = allResources.filter((r) => availableNames.has(r.name));
|
||||
|
||||
if (availableResources.length === 0) {
|
||||
return availableImages;
|
||||
}
|
||||
|
||||
const apiConfig = await u.getPromptAi("storyboardAgent");
|
||||
const { relevantAssets } = await u.ai.text.invoke(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: `请分析以下分镜描述,从可用资产中筛选出与分镜内容直接相关的资产。
|
||||
|
||||
分镜描述:
|
||||
${prompts.map((p, i) => `${i + 1}. ${p}`).join("\n")}
|
||||
|
||||
可用资产列表:
|
||||
${availableResources.map((r) => `- ${r.name}:${r.intro}`).join("\n")}
|
||||
|
||||
请仅选择在分镜中明确出现或被提及的角色、场景、道具。不要选择与分镜内容无关的资产。`,
|
||||
},
|
||||
],
|
||||
output: {
|
||||
relevantAssets: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string().describe("资产名称"),
|
||||
reason: z.string().describe("选择该资产的原因"),
|
||||
}),
|
||||
)
|
||||
.describe("与分镜内容相关的资产列表"),
|
||||
},
|
||||
},
|
||||
apiConfig,
|
||||
);
|
||||
|
||||
if (!relevantAssets || relevantAssets.length === 0) {
|
||||
return availableImages;
|
||||
}
|
||||
|
||||
const relevantNames = new Set(relevantAssets.map((a) => a.name));
|
||||
const filteredImages = availableImages.filter((img) => relevantNames.has(img.name));
|
||||
|
||||
return filteredImages.length > 0 ? filteredImages : availableImages;
|
||||
}
|
||||
|
||||
// 构建资产映射提示词
|
||||
function buildResourcesMapPrompts(images: ImageInfo[]): string {
|
||||
if (images.length === 0) return "";
|
||||
|
||||
const mapping = images.map((item, index) => {
|
||||
if (index < 9) {
|
||||
return `${item.name}=图片${index + 1}`;
|
||||
} else {
|
||||
return `${item.name}=图10-${index - 8}`;
|
||||
}
|
||||
});
|
||||
|
||||
return `其中人物、场景、道具参考对照关系如下:${mapping.join(", ")}。`;
|
||||
}
|
||||
|
||||
export default async (cells: { prompt: string }[], scriptId: number, projectId: number) => {
|
||||
const scriptData = await u.db("t_script").where({ id: scriptId, projectId }).first();
|
||||
const projectInfo = await u.db("t_project").where({ id: projectId }).first();
|
||||
|
||||
const row = await u.db("t_outline").where({ id: scriptData?.outlineId!, projectId }).first();
|
||||
const outline: EpisodeData | null = row?.data ? JSON.parse(row.data) : null;
|
||||
|
||||
const resources: ResourceItem[] = outline
|
||||
? (["characters", "props", "scenes"] as const).flatMap((k) => outline[k]?.map((i) => ({ name: i.name, intro: i.description })) ?? [])
|
||||
: [];
|
||||
|
||||
const resourceNames = resources.map((r) => r.name);
|
||||
const imagesRaw = await u.db("t_assets").whereIn("name", resourceNames).andWhere({ projectId }).select("name", "type", "filePath");
|
||||
|
||||
const allImages = imagesRaw
|
||||
.sort((a, b) => {
|
||||
const order = ["角色", "场景", "道具"];
|
||||
return order.indexOf(a.type!) - order.indexOf(b.type!);
|
||||
})
|
||||
.filter((img) => img.filePath) as ImageInfo[];
|
||||
|
||||
if (allImages.length === 0) {
|
||||
throw new Error("未找到可用的图片资源");
|
||||
}
|
||||
|
||||
const cellPrompts = cells.map((c) => c.prompt);
|
||||
|
||||
// 使用 AI 过滤相关资产
|
||||
const filteredImages = await filterRelevantAssets(cellPrompts, resources, allImages);
|
||||
|
||||
const resourcesMapPrompts = buildResourcesMapPrompts(filteredImages);
|
||||
|
||||
const promptsData = await generateImagePromptsTool({
|
||||
prompts: cellPrompts,
|
||||
style: `类型:${projectInfo?.type!},风格:${projectInfo?.artStyle!}`,
|
||||
aspectRatio: projectInfo?.videoRatio! as any,
|
||||
assetsName: resources,
|
||||
});
|
||||
|
||||
// const prompts = `请生成${promptsData.gridLayout.totalCells}格,${promptsData.gridLayout.cols}列×${promptsData.gridLayout.rows}行宫格图。
|
||||
|
||||
// ${promptsData.prompt}
|
||||
|
||||
// 注意:请严格按照提示词内容生成图片,确保人物样貌、艺术风格、色调光影一致。
|
||||
// `;
|
||||
const prompts = promptsData.prompt;
|
||||
|
||||
const processedImages = await processImages(filteredImages);
|
||||
const apiConfig = await u.getPromptAi("storyboardImage");
|
||||
|
||||
const contentStr = await u.ai.image(
|
||||
{
|
||||
systemPrompt: resourcesMapPrompts,
|
||||
prompt: prompts,
|
||||
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,
|
||||
);
|
||||
const match = contentStr.match(/base64,([A-Za-z0-9+/=]+)/);
|
||||
const base64Str = match?.[1] ?? contentStr;
|
||||
const buffer = Buffer.from(base64Str, "base64");
|
||||
|
||||
return buffer;
|
||||
};
|
||||
@ -1,94 +0,0 @@
|
||||
import sharp from "sharp";
|
||||
|
||||
interface GridLayoutResult {
|
||||
cols: number;
|
||||
rows: number;
|
||||
totalCells: number;
|
||||
placeholderCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算宫格布局
|
||||
* 1张: 1x1
|
||||
* 2张: 2x1
|
||||
* 3张: 3x1
|
||||
* 4张: 2x2
|
||||
* 5-9张: 3x3
|
||||
* 10-12张: 3x4
|
||||
* 13-15张: 3x5
|
||||
* ...以此类推(3列,行数递增)
|
||||
*/
|
||||
function calculateGridLayout(count: number): GridLayoutResult {
|
||||
let cols: number;
|
||||
let rows: number;
|
||||
if (count <= 0) {
|
||||
cols = 1;
|
||||
rows = 1;
|
||||
} else if (count === 1) {
|
||||
cols = 1;
|
||||
rows = 1;
|
||||
} else if (count === 2) {
|
||||
cols = 2;
|
||||
rows = 1;
|
||||
} else if (count === 3) {
|
||||
cols = 3;
|
||||
rows = 1;
|
||||
} else if (count === 4) {
|
||||
cols = 2;
|
||||
rows = 2;
|
||||
} else if (count <= 9) {
|
||||
// 5-9格统一用3x3
|
||||
cols = 3;
|
||||
rows = 3;
|
||||
} else {
|
||||
cols = 3;
|
||||
rows = Math.ceil(count / 3);
|
||||
}
|
||||
const totalCells = cols * rows;
|
||||
const placeholderCount = totalCells - count;
|
||||
return { cols, rows, totalCells, placeholderCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* 分割宫格图片
|
||||
* @param image - 输入的宫格图片 Buffer
|
||||
* @param length - 实际需要的图片数量(不包含占位图)
|
||||
* @returns 分割后的单张图片 Buffer 数组
|
||||
*/
|
||||
export default async (image: Buffer, length: number): Promise<Buffer[]> => {
|
||||
const metadata = await sharp(image).metadata();
|
||||
const { width: totalWidth, height: totalHeight } = metadata;
|
||||
|
||||
if (!totalWidth || !totalHeight) {
|
||||
throw new Error("无法获取图片尺寸");
|
||||
}
|
||||
|
||||
const { cols, rows } = calculateGridLayout(length);
|
||||
|
||||
const cellWidth = Math.floor(totalWidth / cols);
|
||||
const cellHeight = Math.floor(totalHeight / rows);
|
||||
|
||||
const buffers: Buffer[] = [];
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const row = Math.floor(i / cols);
|
||||
const col = i % cols;
|
||||
|
||||
const left = col * cellWidth;
|
||||
const top = row * cellHeight;
|
||||
|
||||
const cellBuffer = await sharp(image)
|
||||
.extract({
|
||||
left,
|
||||
top,
|
||||
width: cellWidth,
|
||||
height: cellHeight,
|
||||
})
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
buffers.push(cellBuffer);
|
||||
}
|
||||
|
||||
return buffers;
|
||||
};
|
||||
@ -1,734 +0,0 @@
|
||||
// @/agents/Storyboard.ts
|
||||
import u from "@/utils";
|
||||
import { tool, ModelMessage, Tool } from "ai";
|
||||
import { EventEmitter } from "events";
|
||||
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";
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
type AgentType = "segmentAgent" | "shotAgent";
|
||||
type RefreshEvent = "storyline" | "outline" | "assets";
|
||||
|
||||
// ==================== 常量配置 ====================
|
||||
|
||||
// const SYSTEM_PROMPTS: Record<AgentType, string> = {
|
||||
// segmentAgent: segmentPrompts,
|
||||
// shotAgent: shotPrompts,
|
||||
// director: directorPrompts,
|
||||
// };
|
||||
|
||||
// ==================== 类型定义:片段和画面 ====================
|
||||
|
||||
interface Segment {
|
||||
index: number;
|
||||
description: string;
|
||||
emotion?: string;
|
||||
action?: string;
|
||||
}
|
||||
|
||||
interface Shot {
|
||||
id: number; // 分镜独立ID
|
||||
segmentId: number; // 所属片段ID
|
||||
title: string;
|
||||
x: number;
|
||||
y: number;
|
||||
cells: Array<{ src?: string; prompt?: string; id?: string }>; // 镜头数组,每个cell是一个镜头
|
||||
fragmentContent: string;
|
||||
assetsTags: AssetsType[];
|
||||
}
|
||||
interface AssetsType {
|
||||
type: "role" | "props" | "scene";
|
||||
text: string;
|
||||
}
|
||||
// ==================== 主类 ====================
|
||||
|
||||
export default class Storyboard {
|
||||
private readonly projectId: number;
|
||||
private readonly scriptId: number;
|
||||
readonly emitter = new EventEmitter();
|
||||
history: ModelMessage[] = [];
|
||||
novelChapters: DB["t_novel"][] = [];
|
||||
|
||||
// 存储 segmentAgent 生成的片段结果
|
||||
private segments: Segment[] = [];
|
||||
// 存储 shotAgent 生成的分镜结果
|
||||
private shots: Shot[] = [];
|
||||
// 分镜ID计数器
|
||||
private shotIdCounter: number = 0;
|
||||
// 存储正在生成分镜图的分镜ID
|
||||
private generatingShots: Set<number> = new Set();
|
||||
|
||||
constructor(projectId: number, scriptId: number) {
|
||||
this.projectId = projectId;
|
||||
this.scriptId = scriptId;
|
||||
}
|
||||
|
||||
// 更新shopts
|
||||
public updatePreShots(segmentId: number, cellId: number, cell: { src?: string; prompt?: string; id?: string }) {
|
||||
const shotIndex = this.shots.findIndex((item) => item.segmentId === segmentId);
|
||||
if (shotIndex === -1) {
|
||||
return `分镜 ${segmentId} 不存在,请检查分镜ID是否正确`;
|
||||
}
|
||||
const cellIndex = this.shots[shotIndex].cells.findIndex((item) => item.id === cellId.toString());
|
||||
if (cellIndex === -1) {
|
||||
return `镜头 ${cellId} 不存在,请检查镜头ID是否正确`;
|
||||
}
|
||||
this.shots[shotIndex].cells[cellIndex] = { ...this.shots[shotIndex].cells[cellIndex], ...cell };
|
||||
}
|
||||
|
||||
// ==================== 公共方法 ====================
|
||||
|
||||
get events() {
|
||||
return this.emitter;
|
||||
}
|
||||
// ==================== 私有工具方法 ====================
|
||||
|
||||
private emit(event: string, data?: any) {
|
||||
this.emitter.emit(event, data);
|
||||
}
|
||||
|
||||
private refresh(type: RefreshEvent) {
|
||||
this.emit("refresh", type);
|
||||
}
|
||||
|
||||
private log(action: string, detail?: string) {
|
||||
const msg = detail ? `${action}: ${detail}` : action;
|
||||
}
|
||||
|
||||
// ==================== 剧本相关操作 ====================
|
||||
|
||||
getScript = tool({
|
||||
title: "getScript",
|
||||
description: "用于获取剧本内容",
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => {
|
||||
this.log("获取剧本", `scriptId: ${this.scriptId}`);
|
||||
const script = await u.db("t_script").where({ id: this.scriptId, projectId: this.projectId }).first();
|
||||
if (!script) throw new Error("剧本不存在");
|
||||
return `剧本集:${script.name}\n\n内容:\n\`\`\`${script.content}\`\`\``;
|
||||
},
|
||||
});
|
||||
|
||||
// ==================== 资产相关操作 ====================
|
||||
|
||||
/**
|
||||
* 获取资产列表(供 segmentAgent 和 shotAgent 调用)
|
||||
*/
|
||||
getAssets = tool({
|
||||
title: "getAssets",
|
||||
description: "获取资产列表(角色、道具、场景),包含名称和详细介绍。生成片段和分镜时必须先调用此工具获取资产信息,确保名称一致性",
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => {
|
||||
this.log("获取资产列表", `scriptId: ${this.scriptId}`);
|
||||
const scriptData = await u.db("t_script").where({ id: this.scriptId, projectId: this.projectId }).first();
|
||||
const row = await u.db("t_outline").where({ id: scriptData?.outlineId!, projectId: this.projectId }).first();
|
||||
const outline: any | null = row?.data ? JSON.parse(row.data) : null;
|
||||
|
||||
if (!outline) {
|
||||
return "暂无资产数据";
|
||||
}
|
||||
|
||||
// 提取资源名称和描述(与generateImageTool保持一致的字段名)
|
||||
const resources = outline
|
||||
? (["characters", "props", "scenes"] as const).flatMap(
|
||||
(k) => outline[k]?.map((i: any) => ({ name: i.name, description: i.description })) ?? [],
|
||||
)
|
||||
: [];
|
||||
|
||||
if (resources.length === 0) {
|
||||
return "暂无资产数据";
|
||||
}
|
||||
|
||||
// 分类提取资源并格式化
|
||||
const characters = outline?.characters?.map((item: any) => `- ${item.name}${item.description ? `:${item.description}` : ""}`) ?? [];
|
||||
const props = outline?.props?.map((item: any) => `- ${item.name}${item.description ? `:${item.description}` : ""}`) ?? [];
|
||||
const scenes = outline?.scenes?.map((item: any) => `- ${item.name}${item.description ? `:${item.description}` : ""}`) ?? [];
|
||||
|
||||
const sections = [
|
||||
characters.length ? `【角色】\n${characters.join("\n")}` : "",
|
||||
props.length ? `【道具】\n${props.join("\n")}` : "",
|
||||
scenes.length ? `【场景】\n${scenes.join("\n")}` : "",
|
||||
].filter(Boolean);
|
||||
|
||||
if (sections.length === 0) {
|
||||
return "暂无资产数据";
|
||||
}
|
||||
|
||||
return `<资产列表>
|
||||
${sections.join("\n\n")}
|
||||
</资产列表>
|
||||
|
||||
⚠️ 重要规则:
|
||||
1. 必须原封不动地使用上述资产名称,禁止使用近义词、缩写或任何变体
|
||||
2. 禁止在资产名称前后添加修饰词
|
||||
3. 禁止捏造资产列表中不存在的角色、场景、道具`;
|
||||
},
|
||||
});
|
||||
|
||||
// ==================== 片段和分镜工具 ====================
|
||||
|
||||
/**
|
||||
* 获取当前存储的片段数据(供 shotAgent 调用)
|
||||
*/
|
||||
getSegments = tool({
|
||||
title: "getSegments",
|
||||
description: "获取当前已生成的片段数据,用于生成分镜",
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => {
|
||||
this.log("获取片段数据", `共 ${this.segments.length} 个片段`);
|
||||
if (this.segments.length === 0) {
|
||||
return "暂无片段数据,请先调用 segmentAgent 生成片段";
|
||||
}
|
||||
return JSON.stringify(this.segments, null, 2);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新/存储片段数据(供 segmentAgent 调用)
|
||||
*/
|
||||
updateSegments = tool({
|
||||
title: "updateSegments",
|
||||
description: "存储生成的片段数据,segmentAgent 在生成片段后必须调用此工具保存结果",
|
||||
inputSchema: z.object({
|
||||
segments: z
|
||||
.array(
|
||||
z.object({
|
||||
index: z.number().describe("片段序号"),
|
||||
description: z.string().describe("片段描述"),
|
||||
emotion: z.string().optional().describe("情绪氛围"),
|
||||
action: z.string().optional().describe("主要动作"),
|
||||
}),
|
||||
)
|
||||
.describe("片段数组"),
|
||||
}),
|
||||
execute: async ({ segments }: { segments: Segment[] }) => {
|
||||
this.log("更新片段数据", `共 ${segments.length} 个片段`);
|
||||
this.segments = segments;
|
||||
this.emit("segmentsUpdated", this.segments);
|
||||
return `成功存储 ${segments.length} 个片段`;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 添加分镜(供 shotAgent 调用)
|
||||
*/
|
||||
addShots = tool({
|
||||
title: "addShots",
|
||||
description: "添加新的分镜。每个分镜有独立ID,包含多个镜头(每个镜头对应一个提示词)。如果片段已存在分镜会跳过",
|
||||
inputSchema: z.object({
|
||||
shots: z
|
||||
.array(
|
||||
z.object({
|
||||
segmentIndex: z.number().describe("对应的片段序号"),
|
||||
prompts: z.array(z.string()).describe("镜头提示词数组,每个提示词对应一个镜头(中文)"),
|
||||
assetsTags: z.array(
|
||||
z.object({
|
||||
type: z.enum(["role", "props", "scene"]).describe("资源类型"),
|
||||
text: z.string().describe("资源名称"),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
)
|
||||
.describe("要添加的分镜数组"),
|
||||
}),
|
||||
execute: async ({ shots }: { shots: Array<{ segmentIndex: number; prompts: string[]; assetsTags: AssetsType[] }> }) => {
|
||||
const added: { id: number; segmentIndex: number }[] = [];
|
||||
const skipped: number[] = [];
|
||||
|
||||
for (const item of shots) {
|
||||
const exists = this.shots.some((f) => f.segmentId === item.segmentIndex);
|
||||
if (exists) {
|
||||
skipped.push(item.segmentIndex);
|
||||
continue;
|
||||
}
|
||||
// 分配独立的分镜ID
|
||||
this.shotIdCounter++;
|
||||
const shotId = this.shotIdCounter;
|
||||
this.shots.push({
|
||||
id: shotId,
|
||||
segmentId: item.segmentIndex,
|
||||
title: `分镜 ${shotId}`,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cells: item.prompts.map((prompt) => ({ id: u.uuid(), prompt })),
|
||||
fragmentContent: this.segments[item.segmentIndex - 1]?.description,
|
||||
assetsTags: item.assetsTags,
|
||||
});
|
||||
added.push({ id: shotId, segmentIndex: item.segmentIndex });
|
||||
}
|
||||
|
||||
const addedInfo = added.map((a) => `分镜${a.id}(片段${a.segmentIndex})`).join(", ");
|
||||
this.log("添加分镜", `新增: [${addedInfo}], 跳过片段: [${skipped.join(", ")}]`);
|
||||
this.emit("shotsUpdated", this.shots);
|
||||
|
||||
if (skipped.length) {
|
||||
return `已添加${addedInfo};片段 ${skipped.join(", ")} 已存在分镜被跳过。当前共 ${this.shots.length} 个分镜`;
|
||||
}
|
||||
return `已添加${addedInfo}。当前共 ${this.shots.length} 个分镜`;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新指定分镜(供 shotAgent 调用)
|
||||
* 保留原有 cells 的 id 和 src 字段,只更新 prompt
|
||||
*/
|
||||
updateShots = tool({
|
||||
title: "updateShots",
|
||||
description: "更新指定分镜的镜头提示词。通过分镜ID指定要修改的分镜",
|
||||
inputSchema: z.object({
|
||||
shotId: z.number().describe("要更新的分镜ID"),
|
||||
prompts: z.array(z.string()).describe("新的镜头提示词数组,每个提示词对应一个镜头"),
|
||||
}),
|
||||
execute: async ({ shotId, prompts }: { shotId: number; prompts: string[] }) => {
|
||||
const existingIndex = this.shots.findIndex((item) => item.id === shotId);
|
||||
|
||||
if (existingIndex === -1) {
|
||||
return `分镜 ${shotId} 不存在,请检查分镜ID是否正确`;
|
||||
}
|
||||
|
||||
const existingCells = this.shots[existingIndex].cells;
|
||||
|
||||
// 更新 cells,保留原有的 id 和 src 字段
|
||||
this.shots[existingIndex].cells = prompts.map((prompt, i) => {
|
||||
const existingCell = existingCells[i];
|
||||
if (existingCell) {
|
||||
// 保留原有 cell 的 id 和 src,只更新 prompt
|
||||
return { ...existingCell, prompt };
|
||||
} else {
|
||||
// 新增的 cell
|
||||
return { id: u.uuid(), prompt };
|
||||
}
|
||||
});
|
||||
|
||||
this.log("更新分镜", `分镜 ${shotId}`);
|
||||
this.emit("shotsUpdated", this.shots);
|
||||
|
||||
return `已更新分镜 ${shotId}`;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 删除指定分镜(供 shotAgent 调用)
|
||||
*/
|
||||
deleteShots = tool({
|
||||
title: "deleteShots",
|
||||
description: "删除指定的分镜。通过分镜ID指定要删除的分镜",
|
||||
inputSchema: z.object({
|
||||
shotIds: z.array(z.number()).describe("要删除的分镜ID数组"),
|
||||
}),
|
||||
execute: async ({ shotIds }: { shotIds: number[] }) => {
|
||||
const deleted: number[] = [];
|
||||
const notFound: number[] = [];
|
||||
|
||||
for (const shotId of shotIds) {
|
||||
const idx = this.shots.findIndex((item) => item.id === shotId);
|
||||
if (idx === -1) {
|
||||
notFound.push(shotId);
|
||||
} else {
|
||||
this.shots.splice(idx, 1);
|
||||
deleted.push(shotId);
|
||||
}
|
||||
}
|
||||
|
||||
this.log("删除分镜", `删除: [分镜${deleted.join(", 分镜")}], 未找到: [分镜${notFound.join(", 分镜")}]`);
|
||||
this.emit("shotsUpdated", this.shots);
|
||||
|
||||
if (notFound.length) {
|
||||
return `已删除分镜 ${deleted.join(", ")};分镜 ${notFound.join(", ")} 不存在。当前共 ${this.shots.length} 个分镜`;
|
||||
}
|
||||
return `已删除分镜 ${deleted.join(", ")}。当前共 ${this.shots.length} 个分镜`;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 生成分镜图(异步执行,使用 nanoBanana)
|
||||
*/
|
||||
generateShotImage = tool({
|
||||
title: "generateShotImage",
|
||||
description:
|
||||
"为指定分镜生成分镜图。每个分镜会根据其所有提示词生成一张完整宫格图,然后自动分割为单格图片。通过分镜ID指定,不需要指定具体格子,整个分镜是一个完整的生成单元",
|
||||
inputSchema: z.object({
|
||||
shotIds: z.array(z.number()).describe("要生成分镜图的分镜ID数组"),
|
||||
}),
|
||||
execute: async ({ shotIds }: { shotIds: number[] }) => {
|
||||
const toGenerate: number[] = [];
|
||||
const alreadyGenerating: number[] = [];
|
||||
const notFound: number[] = [];
|
||||
|
||||
for (const shotId of shotIds) {
|
||||
const shot = this.shots.find((f) => f.id === shotId);
|
||||
if (!shot) {
|
||||
notFound.push(shotId);
|
||||
continue;
|
||||
}
|
||||
if (this.generatingShots.has(shotId)) {
|
||||
alreadyGenerating.push(shotId);
|
||||
continue;
|
||||
}
|
||||
toGenerate.push(shotId);
|
||||
}
|
||||
|
||||
if (toGenerate.length === 0) {
|
||||
if (notFound.length) {
|
||||
return `分镜 ${notFound.join(", ")} 不存在,请检查分镜ID是否正确`;
|
||||
}
|
||||
if (alreadyGenerating.length) {
|
||||
return `分镜 ${alreadyGenerating.join(", ")} 正在生成中,请稍候`;
|
||||
}
|
||||
return "没有需要生成的分镜";
|
||||
}
|
||||
|
||||
// 标记为正在生成
|
||||
for (const id of toGenerate) {
|
||||
this.generatingShots.add(id);
|
||||
}
|
||||
|
||||
// 通知前端开始生成
|
||||
this.emit("shotImageGenerateStart", { shotIds: toGenerate });
|
||||
this.log("开始生成分镜图", `分镜: [${toGenerate.join(", ")}]`);
|
||||
|
||||
// 异步执行图片生成(不阻塞 Agent 流程)
|
||||
this.executeShotImageGeneration(toGenerate).catch((err) => {
|
||||
this.log("分镜图生成错误", err.message);
|
||||
this.emit("shotImageGenerateError", { shotIds: toGenerate, error: err.message });
|
||||
});
|
||||
|
||||
let result = `已开始为分镜 ${toGenerate.join(", ")} 生成分镜图,生成过程在后台进行`;
|
||||
if (alreadyGenerating.length) {
|
||||
result += `;分镜 ${alreadyGenerating.join(", ")} 正在生成中`;
|
||||
}
|
||||
if (notFound.length) {
|
||||
result += `;分镜 ${notFound.join(", ")} 不存在`;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 执行分镜图生成的具体逻辑(异步并发)
|
||||
* 每个分镜包含多个镜头,所有镜头的提示词合并生成一张宫格图,再分割为单张镜头图片
|
||||
*/
|
||||
async executeShotImageGeneration(shotIds: number[]): Promise<void> {
|
||||
await Promise.all(shotIds.map((shotId) => this.generateSingleShotImage(shotId)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成单个分镜的图片
|
||||
*/
|
||||
private async generateSingleShotImage(shotId: number): Promise<void> {
|
||||
try {
|
||||
const shot = this.shots.find((f) => f.id === shotId);
|
||||
if (!shot) return;
|
||||
|
||||
// 提取所有镜头的有效提示词
|
||||
const prompts: string[] = shot.cells.map((c) => c.prompt).filter((p): p is string => Boolean(p));
|
||||
|
||||
if (prompts.length === 0) {
|
||||
this.log("跳过分镜图生成", `分镜 ${shotId} 没有有效的镜头提示词`);
|
||||
this.generatingShots.delete(shotId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 通知前端正在生成该分镜
|
||||
this.emit("shotImageGenerateProgress", { shotId, status: "generating", message: "正在调用 AI 生成宫格图片" });
|
||||
|
||||
// 根据所有镜头提示词生成宫格图片
|
||||
const gridImage = await generateImageTool(
|
||||
prompts.map((p) => ({ prompt: p })),
|
||||
this.scriptId,
|
||||
this.projectId,
|
||||
);
|
||||
|
||||
// 通知前端正在分割图片
|
||||
this.emit("shotImageGenerateProgress", { shotId, status: "splitting", message: "正在分割宫格图片为单张镜头图" });
|
||||
|
||||
// 分割宫格图片为单张镜头图片
|
||||
const imageBuffers = await imageSplitting(gridImage, prompts.length);
|
||||
|
||||
// 通知前端正在保存图片
|
||||
this.emit("shotImageGenerateProgress", { shotId, status: "saving", message: `正在保存 ${imageBuffers.length} 张镜头图片` });
|
||||
|
||||
// 保存分割后的镜头图片到 OSS,并获取文件路径
|
||||
const timestamp = Date.now();
|
||||
const imagePaths: string[] = [];
|
||||
|
||||
for (let i = 0; i < imageBuffers.length; i++) {
|
||||
const fileName = `${this.projectId}/chat/${this.scriptId}/storyboard/shot_${shotId}_take_${i}_${timestamp}.png`;
|
||||
|
||||
await u.oss.writeFile(fileName, imageBuffers[i]);
|
||||
const imageUrl = await u.oss.getFileUrl(fileName);
|
||||
|
||||
imagePaths.push(imageUrl);
|
||||
|
||||
// 每保存一张镜头图片通知进度
|
||||
this.emit("shotImageGenerateProgress", {
|
||||
shotId,
|
||||
status: "saving",
|
||||
message: `已保存 ${i + 1}/${imageBuffers.length} 张镜头图片`,
|
||||
progress: Math.round(((i + 1) / imageBuffers.length) * 100),
|
||||
});
|
||||
}
|
||||
|
||||
// 更新每个镜头的 src 字段
|
||||
shot.cells = shot.cells.map((cell, i) => ({
|
||||
id: u.uuid(),
|
||||
...cell,
|
||||
src: imagePaths[i] || cell.src,
|
||||
}));
|
||||
|
||||
// 生成完成后更新状态
|
||||
this.generatingShots.delete(shotId);
|
||||
this.emit("shotImageGenerateComplete", { shotId, shot, imagePaths });
|
||||
this.emit("shotsUpdated", this.shots);
|
||||
this.log("分镜图生成完成", `分镜 ${shotId},共 ${imagePaths.length} 张镜头图片`);
|
||||
} catch (err: any) {
|
||||
this.generatingShots.delete(shotId);
|
||||
this.emit("shotImageGenerateError", { shotId, error: err.message });
|
||||
this.log("分镜图生成失败", `分镜 ${shotId}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 公共访问器 ====================
|
||||
|
||||
/**
|
||||
* 获取当前片段数据
|
||||
*/
|
||||
getSegmentsData(): Segment[] {
|
||||
return this.segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前分镜数据
|
||||
*/
|
||||
getShotsData(): Shot[] {
|
||||
return this.shots;
|
||||
}
|
||||
|
||||
// ==================== 上下文构建 ====================
|
||||
|
||||
private async buildEnvironmentContext(): Promise<string> {
|
||||
const projectInfo = await u.db("t_project").where({ id: this.projectId }).first();
|
||||
|
||||
const row = await u.db("t_outline").where({ id: this.scriptId, projectId: this.projectId }).first();
|
||||
const outline: any | null = row?.data ? JSON.parse(row.data) : null;
|
||||
|
||||
// 分类提取资源名称
|
||||
const characters = outline?.characters?.map((i: any) => i.name) ?? [];
|
||||
const props = outline?.props?.map((i: any) => i.name) ?? [];
|
||||
const scenes = outline?.scenes?.map((i: any) => i.name) ?? [];
|
||||
|
||||
const assetList =
|
||||
[
|
||||
characters.length ? `【角色】${characters.join("、")}` : "",
|
||||
props.length ? `【道具】${props.join("、")}` : "",
|
||||
scenes.length ? `【场景】${scenes.join("、")}` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n") || "无";
|
||||
|
||||
return `<环境信息>
|
||||
项目ID: ${this.projectId}
|
||||
系统时间: ${new Date().toLocaleString()}
|
||||
|
||||
项目名称: ${projectInfo?.name || "未知"}
|
||||
项目简介: ${projectInfo?.intro || "无"}
|
||||
类型: ${projectInfo?.type || "未知"}
|
||||
风格: ${projectInfo?.artStyle || "未知"}
|
||||
视频比例: ${projectInfo?.videoRatio || "未知"}
|
||||
|
||||
资产列表:
|
||||
${assetList}
|
||||
|
||||
</环境信息>`;
|
||||
}
|
||||
|
||||
private buildConversationHistory(): string {
|
||||
if (!this.history.length) return "无对话历史";
|
||||
return this.history.map(({ role, content }) => `${role}: ${content}`).join("\n\n");
|
||||
}
|
||||
|
||||
private async buildFullContext(task: string): Promise<string> {
|
||||
const env = await this.buildEnvironmentContext();
|
||||
const history = this.buildConversationHistory();
|
||||
|
||||
return `${env}
|
||||
|
||||
<对话历史>
|
||||
${history}
|
||||
</对话历史>
|
||||
|
||||
<当前任务>
|
||||
${task}
|
||||
</当前任务>`;
|
||||
}
|
||||
|
||||
// ==================== Sub-Agent ====================
|
||||
|
||||
/**
|
||||
* 获取不同 Sub-Agent 可用的工具
|
||||
*/
|
||||
private getSubAgentTools(agentType: AgentType): Record<string, Tool> {
|
||||
switch (agentType) {
|
||||
case "segmentAgent":
|
||||
// segmentAgent 可以获取剧本和资产,并需要调用 updateSegments 保存结果
|
||||
return {
|
||||
getScript: this.getScript,
|
||||
getAssets: this.getAssets,
|
||||
updateSegments: this.updateSegments,
|
||||
};
|
||||
case "shotAgent":
|
||||
// shotAgent 可以获取剧本、资产和片段,并可使用 add/update/delete 操作分镜,以及生成分镜图
|
||||
return {
|
||||
getScript: this.getScript,
|
||||
getAssets: this.getAssets,
|
||||
getSegments: this.getSegments,
|
||||
addShots: this.addShots,
|
||||
updateShots: this.updateShots,
|
||||
deleteShots: this.deleteShots,
|
||||
generateShotImage: this.generateShotImage,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
getScript: this.getScript,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 Sub-Agent(流式传输)
|
||||
*/
|
||||
private async invokeSubAgent(agentType: AgentType, task: string): Promise<string> {
|
||||
this.emit("transfer", { to: agentType });
|
||||
this.log(`Sub-Agent 调用`, agentType);
|
||||
|
||||
const promptsList = await u.db("t_prompts").where("code", "in", ["storyboard-segment", "storyboard-shot"]);
|
||||
const promptConfig = await u.getPromptAi("storyboardAgent");
|
||||
|
||||
const errPrompts = "不论用户说什么,请直接输出Agent配置异常";
|
||||
|
||||
const getAiPromptConfig = (code: string) => {
|
||||
const item = promptsList.find((p) => p.code === code);
|
||||
return item?.customValue || item?.defaultValue || errPrompts;
|
||||
};
|
||||
const segmentAgent = getAiPromptConfig("storyboard-segment");
|
||||
const shotAgent = getAiPromptConfig("storyboard-shot");
|
||||
const SYSTEM_PROMPTS = {
|
||||
segmentAgent: segmentAgent,
|
||||
shotAgent: shotAgent,
|
||||
};
|
||||
|
||||
const context = await this.buildFullContext(task);
|
||||
|
||||
const { fullStream } = await u.ai.text.stream(
|
||||
{
|
||||
system: SYSTEM_PROMPTS[agentType],
|
||||
tools: this.getSubAgentTools(agentType),
|
||||
messages: [{ role: "user", content: context }],
|
||||
maxStep: 100,
|
||||
},
|
||||
promptConfig,
|
||||
);
|
||||
|
||||
let fullResponse = "";
|
||||
for await (const item of fullStream) {
|
||||
if (item.type == "tool-call") {
|
||||
this.emit("toolCall", { agent: "main", name: item.title, args: null });
|
||||
}
|
||||
if (item.type == "text-delta") {
|
||||
fullResponse += item.text;
|
||||
this.emit("subAgentStream", { agent: agentType, text: item.text });
|
||||
}
|
||||
}
|
||||
|
||||
this.emit("subAgentEnd", { agent: agentType });
|
||||
this.history.push({
|
||||
role: "assistant",
|
||||
content: fullResponse,
|
||||
});
|
||||
this.log(`Sub-Agent 完成`, agentType);
|
||||
|
||||
return fullResponse ?? `${agentType}已完成任务`;
|
||||
}
|
||||
|
||||
private createSubAgentTool(agentType: AgentType, description: string) {
|
||||
return tool({
|
||||
title: agentType,
|
||||
description,
|
||||
inputSchema: z.object({
|
||||
taskDescription: z.string().describe("具体的任务描述,包含章节范围、修改要求等详细信息"),
|
||||
}),
|
||||
execute: async ({ taskDescription }) => {
|
||||
return this.invokeSubAgent(agentType, taskDescription);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 主入口 ====================
|
||||
|
||||
private getAllTools() {
|
||||
return {
|
||||
segmentAgent: this.createSubAgentTool(
|
||||
"segmentAgent",
|
||||
"调用片段师。负责根据剧本生成片段,必须调用 getScript工具 获取剧本内容,并调用 updateSegments 保存片段结果。",
|
||||
),
|
||||
shotAgent: this.createSubAgentTool(
|
||||
"shotAgent",
|
||||
"调用分镜师。负责根据片段生成分镜提示词,会自行调用 getSegments 获取片段数据,并调用 addShots/updateShots 保存分镜结果。",
|
||||
),
|
||||
// this.createSubAgentTool("director", "调用导演。负责审核故事线和大纲,会自行调用 updateOutline 或 saveStoryline 进行修改。"),
|
||||
getScript: this.getScript,
|
||||
getSegments: this.getSegments,
|
||||
generateShotImage: this.generateShotImage,
|
||||
...this.getSubAgentTools("segmentAgent"),
|
||||
...this.getSubAgentTools("shotAgent"),
|
||||
};
|
||||
}
|
||||
|
||||
async call(msg: string): Promise<string> {
|
||||
this.history.push({
|
||||
role: "user",
|
||||
content: msg,
|
||||
});
|
||||
|
||||
const envContext = await this.buildEnvironmentContext();
|
||||
|
||||
const prompts = await u.db("t_prompts").where("code", "storyboard-main").first();
|
||||
const promptConfig = await u.getPromptAi("storyboardAgent");
|
||||
|
||||
const mainPrompts = prompts?.customValue || prompts?.defaultValue || "不论用户说什么,请直接输出Agent配置异常";
|
||||
|
||||
const { fullStream } = await u.ai.text.stream(
|
||||
{
|
||||
system: `${envContext}\n${mainPrompts}`,
|
||||
tools: this.getAllTools(),
|
||||
messages: this.history,
|
||||
maxStep: 100,
|
||||
},
|
||||
promptConfig,
|
||||
);
|
||||
|
||||
let fullResponse = "";
|
||||
for await (const item of fullStream) {
|
||||
if (item.type == "tool-call") {
|
||||
this.emit("toolCall", { agent: "main", name: item.title, args: null });
|
||||
}
|
||||
if (item.type == "text-delta") {
|
||||
fullResponse += item.text;
|
||||
this.emit("data", item.text);
|
||||
}
|
||||
}
|
||||
this.history.push({
|
||||
role: "assistant",
|
||||
content: fullResponse,
|
||||
});
|
||||
|
||||
this.emit("response", fullResponse);
|
||||
|
||||
return fullResponse;
|
||||
}
|
||||
}
|
||||
33
src/app.ts
33
src/app.ts
@ -2,6 +2,8 @@ import "./logger";
|
||||
import "./err";
|
||||
import "./env";
|
||||
import express, { Request, Response, NextFunction } from "express";
|
||||
import { Server } from "socket.io";
|
||||
import http from "node:http";
|
||||
import expressWs from "express-ws";
|
||||
import logger from "morgan";
|
||||
import cors from "cors";
|
||||
@ -10,11 +12,15 @@ import fs from "fs";
|
||||
import path from "path";
|
||||
import u from "@/utils";
|
||||
import jwt from "jsonwebtoken";
|
||||
import socketInit from "@/socket/index";
|
||||
|
||||
const app = express();
|
||||
let server: ReturnType<typeof app.listen> | null = null;
|
||||
const server = http.createServer(app);
|
||||
|
||||
export default async function startServe(randomPort: Boolean = false) {
|
||||
const io = new Server(server, { cors: { origin: "*" } });
|
||||
socketInit(io);
|
||||
|
||||
if (process.env.NODE_ENV == "dev") await buildRoute();
|
||||
|
||||
expressWs(app);
|
||||
@ -24,14 +30,7 @@ export default async function startServe(randomPort: Boolean = false) {
|
||||
app.use(express.json({ limit: "100mb" }));
|
||||
app.use(express.urlencoded({ extended: true, limit: "100mb" }));
|
||||
|
||||
let rootDir: string;
|
||||
if (typeof process.versions?.electron !== "undefined") {
|
||||
const { app } = require("electron");
|
||||
const userDataDir: string = app.getPath("userData");
|
||||
rootDir = path.join(userDataDir, "uploads");
|
||||
} else {
|
||||
rootDir = path.join(process.cwd(), "uploads");
|
||||
}
|
||||
const rootDir = u.getPath("oss");
|
||||
|
||||
// 确保 uploads 目录存在
|
||||
if (!fs.existsSync(rootDir)) {
|
||||
@ -42,14 +41,14 @@ export default async function startServe(randomPort: Boolean = false) {
|
||||
app.use(express.static(rootDir));
|
||||
|
||||
app.use(async (req, res, next) => {
|
||||
const setting = await u.db("t_setting").where("id", 1).select("tokenKey").first();
|
||||
if (!setting) return res.status(500).send({ message: "服务器未配置,请联系管理员" });
|
||||
const { tokenKey } = setting;
|
||||
const setting = await u.db("o_setting").where("key", "tokenKey").select("value").first();
|
||||
if (!setting) return res.status(444).send({ message: "服务器秘钥未配置,请联系管理员" });
|
||||
const { value: tokenKey } = setting;
|
||||
// 从 header 或 query 参数获取 token
|
||||
const rawToken = req.headers.authorization || (req.query.token as string) || "";
|
||||
const token = rawToken.replace("Bearer ", "");
|
||||
// 白名单路径
|
||||
if (req.path === "/other/login") return next();
|
||||
if (req.path === "/api/login/login") return next();
|
||||
|
||||
if (!token) return res.status(401).send({ message: "未提供token" });
|
||||
try {
|
||||
@ -66,7 +65,7 @@ export default async function startServe(randomPort: Boolean = false) {
|
||||
|
||||
// 404 处理
|
||||
app.use((_, res, next: NextFunction) => {
|
||||
return res.status(404).send({ message: "Not Found" });
|
||||
return res.status(404).send({ message: "API 404 Not Found" });
|
||||
});
|
||||
|
||||
// 错误处理
|
||||
@ -78,9 +77,9 @@ export default async function startServe(randomPort: Boolean = false) {
|
||||
});
|
||||
|
||||
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();
|
||||
return await new Promise((resolve) => {
|
||||
server.listen(port, async () => {
|
||||
const address = server.address();
|
||||
const realPort = typeof address === "string" ? address : address?.port;
|
||||
console.log(`[服务启动成功]: http://localhost:${realPort}`);
|
||||
resolve(realPort);
|
||||
|
||||
@ -41,7 +41,7 @@ export default async function generateRouter(): Promise<void> {
|
||||
content += `${importLines.join("\n")}\n\n`;
|
||||
content += `export default async (app: Express) => {\n`;
|
||||
for (const { routePath, varName } of routeModulePairs) {
|
||||
content += ` app.use("${routePath}", ${varName});\n`;
|
||||
content += ` app.use("/api${routePath}", ${varName});\n`;
|
||||
}
|
||||
content += `}\n`;
|
||||
|
||||
|
||||
1411
src/lib/artStyle.ts
1411
src/lib/artStyle.ts
File diff suppressed because it is too large
Load Diff
197
src/lib/fixDB.ts
197
src/lib/fixDB.ts
File diff suppressed because one or more lines are too long
1273
src/lib/initDB.ts
1273
src/lib/initDB.ts
File diff suppressed because one or more lines are too long
@ -1,19 +1,11 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import getPath from "@/utils/getPath";
|
||||
|
||||
type LogLevel = "log" | "info" | "warn" | "error" | "debug";
|
||||
type ConsoleMethod = (...args: unknown[]) => void;
|
||||
|
||||
function getLogDir(): string {
|
||||
const isElectron = typeof process.versions?.electron !== "undefined";
|
||||
if (isElectron) {
|
||||
const { app } = require("electron");
|
||||
return path.join(app.getPath("userData"), "logs");
|
||||
}
|
||||
return path.join(process.cwd(), "logs");
|
||||
}
|
||||
|
||||
const LOG_DIR = getLogDir();
|
||||
const LOG_DIR = getPath("logs");
|
||||
const LOG_FILE = path.join(LOG_DIR, "app.log");
|
||||
const MAX_SIZE = 1000 * 1024 * 1024;
|
||||
const LEVELS: LogLevel[] = ["log", "info", "warn", "error", "debug"];
|
||||
@ -89,7 +81,6 @@ class Logger {
|
||||
|
||||
private hijack(): void {
|
||||
if (this.isHijacked) return;
|
||||
|
||||
// 劫持 console 方法
|
||||
for (const level of LEVELS) {
|
||||
const original = console[level];
|
||||
@ -97,7 +88,12 @@ class Logger {
|
||||
this.originalConsole[level] = original.bind(console);
|
||||
(console as any)[level] = (...args: unknown[]) => {
|
||||
this.writing = true;
|
||||
this.write(level, args);
|
||||
try {
|
||||
// this.write(level, args);
|
||||
} catch (err) {
|
||||
this.originalConsole.error?.("[Logger Error]", err);
|
||||
}
|
||||
|
||||
this.originalConsole[level]!(...args);
|
||||
this.writing = false;
|
||||
};
|
||||
|
||||
280
src/router.ts
280
src/router.ts
@ -1,170 +1,124 @@
|
||||
// @routes-hash d8b95db972bd0ab01243d87d89a004f0
|
||||
// @routes-hash 4149c7e96379bfdba20853678db5c921
|
||||
import { Express } from "express";
|
||||
|
||||
import route1 from "./routes/artStyle/getArtStyle";
|
||||
import route2 from "./routes/assets/addAssets";
|
||||
import route3 from "./routes/assets/delAssets";
|
||||
import route4 from "./routes/assets/delAssetsImage";
|
||||
import route5 from "./routes/assets/generateAssets";
|
||||
import route6 from "./routes/assets/getAssets";
|
||||
import route7 from "./routes/assets/getImage";
|
||||
import route8 from "./routes/assets/getScriptList";
|
||||
import route9 from "./routes/assets/polishAssetsPrompt";
|
||||
import route10 from "./routes/assets/saveAssets";
|
||||
import route11 from "./routes/assets/updateAssets";
|
||||
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 route1 from "./routes/agents/clearMemory";
|
||||
import route2 from "./routes/agents/getMemory";
|
||||
import route3 from "./routes/agents/productionAgent";
|
||||
import route4 from "./routes/artStyle/getArtStyle";
|
||||
import route5 from "./routes/assets/addAssets";
|
||||
import route6 from "./routes/assets/batchDelete";
|
||||
import route7 from "./routes/assets/batchGenerationData";
|
||||
import route8 from "./routes/assets/delAssets";
|
||||
import route9 from "./routes/assets/getAssetsApi";
|
||||
import route10 from "./routes/assets/getImage";
|
||||
import route11 from "./routes/assets/saveAssets";
|
||||
import route12 from "./routes/assets/updateAssets";
|
||||
import route13 from "./routes/assets/uploadClip";
|
||||
import route14 from "./routes/assetsGenerate/generateAssets";
|
||||
import route15 from "./routes/assetsGenerate/polishAssetsPrompt";
|
||||
import route16 from "./routes/general/generalStatistics";
|
||||
import route17 from "./routes/general/getSingleProject";
|
||||
import route18 from "./routes/general/updateProject";
|
||||
import route19 from "./routes/login/login";
|
||||
import route20 from "./routes/migrate/migrateData";
|
||||
import route21 from "./routes/modelSelect/getModelList";
|
||||
import route22 from "./routes/novel/addNovel";
|
||||
import route23 from "./routes/novel/batchDeleteNovel";
|
||||
import route24 from "./routes/novel/delNovel";
|
||||
import route25 from "./routes/novel/event/batchDeleteEvent";
|
||||
import route26 from "./routes/novel/event/deletEvent";
|
||||
import route27 from "./routes/novel/event/generateEvents";
|
||||
import route28 from "./routes/novel/event/getEvent";
|
||||
import route29 from "./routes/novel/getNovel";
|
||||
import route30 from "./routes/novel/updateNovel";
|
||||
import route31 from "./routes/other/deleteAllData";
|
||||
import route32 from "./routes/other/getCaptcha";
|
||||
import route33 from "./routes/production/getProductionData";
|
||||
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/storyboard/batchSuperScoreImage";
|
||||
import route56 from "./routes/storyboard/chatStoryboard";
|
||||
import route57 from "./routes/storyboard/delStoryboard";
|
||||
import route58 from "./routes/storyboard/generateShotImage";
|
||||
import route59 from "./routes/storyboard/generateVideoPrompt";
|
||||
import route60 from "./routes/storyboard/getStoryboard";
|
||||
import route61 from "./routes/storyboard/keepStoryboard";
|
||||
import route62 from "./routes/storyboard/saveStoryboard";
|
||||
import route63 from "./routes/storyboard/storyboardImageEdit";
|
||||
import route64 from "./routes/storyboard/uploadImage";
|
||||
import route65 from "./routes/task/getMyTaskApi";
|
||||
import route66 from "./routes/task/getTaskCategories";
|
||||
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";
|
||||
import route36 from "./routes/project/editProject";
|
||||
import route37 from "./routes/project/getProject";
|
||||
import route38 from "./routes/script/addScript";
|
||||
import route39 from "./routes/script/delScript";
|
||||
import route40 from "./routes/script/getScrptApi";
|
||||
import route41 from "./routes/script/updateScript";
|
||||
import route42 from "./routes/setting/agentDeploy/deployAgentModel";
|
||||
import route43 from "./routes/setting/agentDeploy/getAgentDeploy";
|
||||
import route44 from "./routes/setting/agentDeploy/updateKey";
|
||||
import route45 from "./routes/setting/dbConfig/clearData";
|
||||
import route46 from "./routes/setting/getTextModel";
|
||||
import route47 from "./routes/setting/loginConfig/getUser";
|
||||
import route48 from "./routes/setting/loginConfig/updateUserPwd";
|
||||
import route49 from "./routes/setting/memoryConfig/getMemory";
|
||||
import route50 from "./routes/setting/memoryConfig/sureMemory";
|
||||
import route51 from "./routes/setting/vendorConfig/addVendor";
|
||||
import route52 from "./routes/setting/vendorConfig/deleteVendor";
|
||||
import route53 from "./routes/setting/vendorConfig/getVendorList";
|
||||
import route54 from "./routes/setting/vendorConfig/modelTest";
|
||||
import route55 from "./routes/setting/vendorConfig/updateVendor";
|
||||
import route56 from "./routes/task/getMyTaskApi";
|
||||
import route57 from "./routes/task/getTaskCategories";
|
||||
import route58 from "./routes/task/taskDetails";
|
||||
import route59 from "./routes/test/test";
|
||||
|
||||
export default async (app: Express) => {
|
||||
app.use("/artStyle/getArtStyle", route1);
|
||||
app.use("/assets/addAssets", route2);
|
||||
app.use("/assets/delAssets", route3);
|
||||
app.use("/assets/delAssetsImage", route4);
|
||||
app.use("/assets/generateAssets", route5);
|
||||
app.use("/assets/getAssets", route6);
|
||||
app.use("/assets/getImage", route7);
|
||||
app.use("/assets/getScriptList", route8);
|
||||
app.use("/assets/polishAssetsPrompt", route9);
|
||||
app.use("/assets/saveAssets", route10);
|
||||
app.use("/assets/updateAssets", 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("/storyboard/batchSuperScoreImage", route55);
|
||||
app.use("/storyboard/chatStoryboard", route56);
|
||||
app.use("/storyboard/delStoryboard", route57);
|
||||
app.use("/storyboard/generateShotImage", route58);
|
||||
app.use("/storyboard/generateVideoPrompt", route59);
|
||||
app.use("/storyboard/getStoryboard", route60);
|
||||
app.use("/storyboard/keepStoryboard", route61);
|
||||
app.use("/storyboard/saveStoryboard", route62);
|
||||
app.use("/storyboard/storyboardImageEdit", route63);
|
||||
app.use("/storyboard/uploadImage", route64);
|
||||
app.use("/task/getMyTaskApi", route65);
|
||||
app.use("/task/getTaskCategories", 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);
|
||||
app.use("/api/agents/clearMemory", route1);
|
||||
app.use("/api/agents/getMemory", route2);
|
||||
app.use("/api/agents/productionAgent", route3);
|
||||
app.use("/api/artStyle/getArtStyle", route4);
|
||||
app.use("/api/assets/addAssets", route5);
|
||||
app.use("/api/assets/batchDelete", route6);
|
||||
app.use("/api/assets/batchGenerationData", route7);
|
||||
app.use("/api/assets/delAssets", route8);
|
||||
app.use("/api/assets/getAssetsApi", route9);
|
||||
app.use("/api/assets/getImage", route10);
|
||||
app.use("/api/assets/saveAssets", route11);
|
||||
app.use("/api/assets/updateAssets", route12);
|
||||
app.use("/api/assets/uploadClip", route13);
|
||||
app.use("/api/assetsGenerate/generateAssets", route14);
|
||||
app.use("/api/assetsGenerate/polishAssetsPrompt", route15);
|
||||
app.use("/api/general/generalStatistics", route16);
|
||||
app.use("/api/general/getSingleProject", route17);
|
||||
app.use("/api/general/updateProject", route18);
|
||||
app.use("/api/login/login", route19);
|
||||
app.use("/api/migrate/migrateData", route20);
|
||||
app.use("/api/modelSelect/getModelList", route21);
|
||||
app.use("/api/novel/addNovel", route22);
|
||||
app.use("/api/novel/batchDeleteNovel", route23);
|
||||
app.use("/api/novel/delNovel", route24);
|
||||
app.use("/api/novel/event/batchDeleteEvent", route25);
|
||||
app.use("/api/novel/event/deletEvent", route26);
|
||||
app.use("/api/novel/event/generateEvents", route27);
|
||||
app.use("/api/novel/event/getEvent", route28);
|
||||
app.use("/api/novel/getNovel", route29);
|
||||
app.use("/api/novel/updateNovel", route30);
|
||||
app.use("/api/other/deleteAllData", route31);
|
||||
app.use("/api/other/getCaptcha", route32);
|
||||
app.use("/api/production/getProductionData", route33);
|
||||
app.use("/api/project/addProject", route34);
|
||||
app.use("/api/project/delProject", route35);
|
||||
app.use("/api/project/editProject", route36);
|
||||
app.use("/api/project/getProject", route37);
|
||||
app.use("/api/script/addScript", route38);
|
||||
app.use("/api/script/delScript", route39);
|
||||
app.use("/api/script/getScrptApi", route40);
|
||||
app.use("/api/script/updateScript", route41);
|
||||
app.use("/api/setting/agentDeploy/deployAgentModel", route42);
|
||||
app.use("/api/setting/agentDeploy/getAgentDeploy", route43);
|
||||
app.use("/api/setting/agentDeploy/updateKey", route44);
|
||||
app.use("/api/setting/dbConfig/clearData", route45);
|
||||
app.use("/api/setting/getTextModel", route46);
|
||||
app.use("/api/setting/loginConfig/getUser", route47);
|
||||
app.use("/api/setting/loginConfig/updateUserPwd", route48);
|
||||
app.use("/api/setting/memoryConfig/getMemory", route49);
|
||||
app.use("/api/setting/memoryConfig/sureMemory", route50);
|
||||
app.use("/api/setting/vendorConfig/addVendor", route51);
|
||||
app.use("/api/setting/vendorConfig/deleteVendor", route52);
|
||||
app.use("/api/setting/vendorConfig/getVendorList", route53);
|
||||
app.use("/api/setting/vendorConfig/modelTest", route54);
|
||||
app.use("/api/setting/vendorConfig/updateVendor", route55);
|
||||
app.use("/api/task/getMyTaskApi", route56);
|
||||
app.use("/api/task/getTaskCategories", route57);
|
||||
app.use("/api/task/taskDetails", route58);
|
||||
app.use("/api/test/test", route59);
|
||||
}
|
||||
|
||||
26
src/routes/agents/clearMemory.ts
Normal file
26
src/routes/agents/clearMemory.ts
Normal file
@ -0,0 +1,26 @@
|
||||
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({
|
||||
projectId: z.number(),
|
||||
episodesId: z.number().optional(),
|
||||
type: z.enum(["message", "summary", "all"]).optional(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId, episodesId, type = "all" } = req.body;
|
||||
const isolationKey = `${projectId}:${episodesId ?? ""}`;
|
||||
|
||||
const query = u.db("memories").where({ isolationKey });
|
||||
if (type !== "all") query.where("type", type);
|
||||
|
||||
await query.del();
|
||||
|
||||
res.status(200).send(success(null));
|
||||
},
|
||||
);
|
||||
33
src/routes/agents/getMemory.ts
Normal file
33
src/routes/agents/getMemory.ts
Normal 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({
|
||||
projectId: z.number(),
|
||||
episodesId: z.number().optional(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId, episodesId } = req.body;
|
||||
const isolationKey = `${projectId}:${episodesId ?? ""}`;
|
||||
|
||||
const rows = await u
|
||||
.db("memories")
|
||||
.where({ isolationKey, type: "message" })
|
||||
.orderBy("createdAt", "asc")
|
||||
.select("id", "content", "createdAt");
|
||||
|
||||
const history = rows.map((row) => ({
|
||||
id: row.id,
|
||||
role: "user",
|
||||
content: [{ type: "text", status: "complete", data: row.content }],
|
||||
createdAt: row.createdAt,
|
||||
}));
|
||||
|
||||
res.status(200).send(success({ history }));
|
||||
},
|
||||
);
|
||||
85
src/routes/agents/productionAgent.ts
Normal file
85
src/routes/agents/productionAgent.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
import express from "express";
|
||||
import { createAGUIStream } from "@/utils/agent/aguiTools";
|
||||
import u from "@/utils";
|
||||
import Memory from "@/utils/agent/memory";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
function delay(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export default router.post("/", async (req, res) => {
|
||||
const { prompt: text, projectId, episodesId } = req.body;
|
||||
const isolationKey = `${projectId}:${episodesId}`;
|
||||
const memory = new Memory("productionAgent", isolationKey);
|
||||
|
||||
const agui = createAGUIStream(res);
|
||||
agui.runStarted();
|
||||
|
||||
// 存入用户消息
|
||||
await memory.add( "user",text);
|
||||
|
||||
// 获取记忆上下文
|
||||
const mem = await memory.get(text);
|
||||
|
||||
console.log("======================================================");
|
||||
// 构建记忆上下文文本(顺序:历史摘要 → 相关记忆 → 近期对话)
|
||||
const memoryContext = [
|
||||
mem.rag.length > 0 && `[相关记忆]\n${mem.rag.map((r) => r.content).join("\n")}`,
|
||||
mem.summaries.length > 0 && `[历史摘要]\n${mem.summaries.map((s, i) => `${i + 1}. ${s.content}`).join("\n")}`,
|
||||
mem.shortTerm.length > 0 && `[近期对话]\n${mem.shortTerm.map((m) => `${m.role}: ${m.content}`).join("\n")}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
|
||||
console.log("%c Line:27 🍏 memoryContext", "background:#3f7cff", memoryContext);
|
||||
|
||||
const systemPrompt = `You are a helpful assistant.${memoryContext ? `\n\n以下是你对用户的记忆,可作为参考:\n${memoryContext}` : ""}`;
|
||||
|
||||
const messages = [
|
||||
{
|
||||
role: "user" as const,
|
||||
content: text,
|
||||
},
|
||||
];
|
||||
|
||||
const { textStream } = await u.Ai.Text("productionAgent").stream({
|
||||
system: systemPrompt,
|
||||
messages,
|
||||
tools: {
|
||||
deepRetrieve: tool({
|
||||
description: "深度检索记忆:当你需要回忆与某个关键词相关的详细历史信息时使用此工具",
|
||||
inputSchema: z.object({
|
||||
keyword: z.string().describe("要检索的关键词"),
|
||||
}),
|
||||
execute: async ({ keyword }) => {
|
||||
const results = await memory.deepRetrieve(keyword);
|
||||
if (results.length === 0) return { found: false, message: "未找到相关记忆" };
|
||||
return { found: true, memories: results.map((r) => r.content) };
|
||||
},
|
||||
}),
|
||||
},
|
||||
onFinish: async (completion) => {
|
||||
// 存入助手回复
|
||||
await memory.add( "assistant",completion.text);
|
||||
},
|
||||
});
|
||||
|
||||
let msg: ReturnType<typeof agui.textMessage> | null = null;
|
||||
let fullResponse = "";
|
||||
|
||||
for await (const chunk of textStream) {
|
||||
if (!msg) msg = agui.textMessage();
|
||||
msg.content(chunk);
|
||||
fullResponse += chunk;
|
||||
await delay(1);
|
||||
}
|
||||
|
||||
msg?.end();
|
||||
|
||||
agui.runFinished();
|
||||
agui.end();
|
||||
});
|
||||
@ -12,7 +12,7 @@ export default router.post(
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { name } = req.body;
|
||||
const data = await u.db("t_artStyle").where("name", name).select("styles").first();
|
||||
const data = await u.db("o_artStyle").where("name", name).select("styles").first();
|
||||
const styles = data?.styles ? JSON.parse(data.styles) : [];
|
||||
res.status(200).send(success(styles));
|
||||
},
|
||||
|
||||
@ -7,31 +7,26 @@ const router = express.Router();
|
||||
|
||||
// 新增资产
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
projectId: z.number(),
|
||||
scriptId: z.number().optional().nullable(),
|
||||
name: z.string(),
|
||||
intro: z.string(),
|
||||
type: z.string(),
|
||||
prompt: z.string(),
|
||||
remark: z.string().optional().nullable(),
|
||||
episode: z.string().optional().nullable(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId, name, intro, type, prompt, remark, episode, scriptId } = req.body;
|
||||
|
||||
await u.db("t_assets").insert({
|
||||
projectId,
|
||||
name,
|
||||
intro,
|
||||
type,
|
||||
prompt,
|
||||
remark,
|
||||
episode,
|
||||
scriptId,
|
||||
});
|
||||
|
||||
res.status(200).send(success({ message: "新增资产成功" }));
|
||||
}
|
||||
"/",
|
||||
validateFields({
|
||||
name: z.string(),
|
||||
describe: z.string(),
|
||||
type: z.string(),
|
||||
projectId: z.number(),
|
||||
remark: z.string(),
|
||||
prompt: z.string().optional().nullable(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { name, describe, type, projectId, remark, prompt } = req.body;
|
||||
await u.db("o_assets").insert({
|
||||
name,
|
||||
describe,
|
||||
type,
|
||||
projectId,
|
||||
remark,
|
||||
prompt,
|
||||
startTime: Date.now(),
|
||||
});
|
||||
res.status(200).send(success({ message: "新增资产成功" }));
|
||||
},
|
||||
);
|
||||
|
||||
20
src/routes/assets/batchDelete.ts
Normal file
20
src/routes/assets/batchDelete.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { z } from "zod";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
import { id } from "zod/locales";
|
||||
const router = express.Router();
|
||||
|
||||
// 批量删除资产
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
id: z.array(z.number()),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id } = req.body;
|
||||
await u.db("o_assets").whereIn("id", id).delete();
|
||||
res.status(200).send(success({ message: "删除资产成功" }));
|
||||
},
|
||||
);
|
||||
40
src/routes/assets/batchGenerationData.ts
Normal file
40
src/routes/assets/batchGenerationData.ts
Normal file
@ -0,0 +1,40 @@
|
||||
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({
|
||||
projectId: z.number(),
|
||||
type: z.string(),
|
||||
name: z.string().optional(),
|
||||
page: z.number(),
|
||||
limit: z.number(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId, type, name, page = 1, limit = 10, } = req.body;
|
||||
const offset = (page - 1) * limit;
|
||||
let query = u.db("o_assets").select("*").where("projectId", projectId).andWhere("type", type);
|
||||
if (name) {
|
||||
query = query.andWhere("name", "like", `%${name}%`);
|
||||
}
|
||||
// 分页查询
|
||||
const parentAssets = await query.offset(offset).limit(limit);
|
||||
|
||||
// 统计总数
|
||||
const totalQuery = (await u
|
||||
.db("o_assets")
|
||||
.where("projectId", projectId)
|
||||
.andWhere("type", type)
|
||||
.andWhere((qb) => {
|
||||
if (name) {
|
||||
qb.andWhere("name", "like", `%${name}%`);
|
||||
}
|
||||
})
|
||||
.count("* as total")
|
||||
.first()) as any;
|
||||
res.status(200).send(success({ data: parentAssets, total: totalQuery?.total }));
|
||||
});
|
||||
@ -5,7 +5,6 @@ import { success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
const router = express.Router();
|
||||
|
||||
// 删除资产
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
@ -13,9 +12,9 @@ export default router.post(
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id } = req.body;
|
||||
|
||||
await u.db("t_assets").where("id", id).del();
|
||||
|
||||
const assetsData = await u.db("o_image").where("assetsId", id);
|
||||
await Promise.all(assetsData.map((i) => i.filePath && u.oss.deleteFile(i.filePath)));
|
||||
await u.db("o_assets").where({ id }).delete();
|
||||
res.status(200).send(success({ message: "删除资产成功" }));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
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: "删除资产图片成功" }));
|
||||
},
|
||||
);
|
||||
@ -1,237 +0,0 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { z } from "zod";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { error, success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
import sharp from "sharp";
|
||||
const router = express.Router();
|
||||
interface OutlineItem {
|
||||
description: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface OutlineData {
|
||||
chapterRange: number[];
|
||||
characters?: OutlineItem[];
|
||||
props?: OutlineItem[];
|
||||
scenes?: OutlineItem[];
|
||||
}
|
||||
|
||||
type ItemType = "characters" | "props" | "scenes";
|
||||
|
||||
interface ResultItem {
|
||||
type: ItemType;
|
||||
name: string;
|
||||
chapterRange: number[];
|
||||
}
|
||||
// 生成资产图片
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
id: z.number(),
|
||||
type: z.enum(["role", "scene", "props", "storyboard"]),
|
||||
projectId: z.number(),
|
||||
name: z.string(),
|
||||
base64: z.string().optional().nullable(),
|
||||
prompt: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id, type, projectId, base64, prompt, name } = req.body;
|
||||
|
||||
//获取风格
|
||||
const project = await u.db("t_project").where("id", projectId).select("artStyle", "type", "intro", "videoRatio").first();
|
||||
if (!project) return res.status(500).send(success({ message: "项目为空" }));
|
||||
|
||||
const promptsList = await u
|
||||
.db("t_prompts")
|
||||
.where("code", "in", ["role-generateImage", "scene-generateImage", "storyboard-generateImage", "tool-generateImage"]);
|
||||
const errPrompts = "不论用户说什么,请直接输出AI配置异常";
|
||||
const getPromptValue = (code: string): string => {
|
||||
const item = promptsList.find((p) => p.code === code);
|
||||
return item?.customValue ?? item?.defaultValue ?? errPrompts;
|
||||
};
|
||||
const role = getPromptValue("role-generateImage");
|
||||
const scene = getPromptValue("scene-generateImage");
|
||||
const tool = getPromptValue("tool-generateImage");
|
||||
const storyboard = getPromptValue("storyboard-generateImage");
|
||||
|
||||
let systemPrompt = "";
|
||||
let userPrompt = "";
|
||||
if (type == "role") {
|
||||
systemPrompt = role;
|
||||
userPrompt = `
|
||||
请根据以下参数生成角色标准四视图:
|
||||
|
||||
**基础参数:**
|
||||
- 画风风格: ${project?.artStyle || "未指定"}
|
||||
|
||||
**角色设定:**
|
||||
- 名称:${name},
|
||||
- 提示词:${prompt},
|
||||
|
||||
请严格按照系统规范生成人物角色四视图。
|
||||
`;
|
||||
}
|
||||
if (type == "scene") {
|
||||
systemPrompt = scene;
|
||||
userPrompt = `
|
||||
请根据以下参数生成标准场景图:
|
||||
|
||||
**基础参数:**
|
||||
- 画风风格: ${project?.artStyle || "未指定"}
|
||||
|
||||
**场景设定:**
|
||||
- 名称:${name},
|
||||
- 提示词:${prompt},
|
||||
|
||||
请严格按照系统规范生成标准场景图。
|
||||
`;
|
||||
}
|
||||
if (type == "props") {
|
||||
systemPrompt = tool;
|
||||
userPrompt = `
|
||||
请根据以下参数生成标准道具图:
|
||||
|
||||
**基础参数:**
|
||||
- 画风风格: ${project?.artStyle || "未指定"}
|
||||
|
||||
**道具设定:**
|
||||
- 名称:${name},
|
||||
- 提示词:${prompt},
|
||||
|
||||
请严格按照系统规范生成标准道具图。
|
||||
`;
|
||||
}
|
||||
if (type == "storyboard") {
|
||||
systemPrompt = storyboard;
|
||||
userPrompt = `
|
||||
请根据以下参数生成标准分镜图:
|
||||
|
||||
**基础参数:**
|
||||
- 画风风格: ${project?.artStyle || "未指定"}
|
||||
|
||||
**分镜设定:**
|
||||
- 名称:${name},
|
||||
- 提示词:${prompt},
|
||||
|
||||
请严格按照系统规范生成标准分镜图。
|
||||
`;
|
||||
}
|
||||
|
||||
const [imageId] = await u.db("t_image").insert({
|
||||
state: "生成中",
|
||||
assetsId: id,
|
||||
});
|
||||
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(
|
||||
{
|
||||
systemPrompt,
|
||||
prompt: userPrompt,
|
||||
imageBase64: base64 ? [base64] : [],
|
||||
size: "2K",
|
||||
aspectRatio: project.videoRatio ?? "16:9",
|
||||
taskClass: taskClass,
|
||||
name: name,
|
||||
describe: prompt,
|
||||
projectId: projectId,
|
||||
},
|
||||
apiConfig,
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
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));
|
||||
}
|
||||
},
|
||||
);
|
||||
async function imageAddText(name: string, imageBuffer: Buffer) {
|
||||
const meta = await sharp(imageBuffer).metadata();
|
||||
const width = meta.width ?? 1000;
|
||||
const height = meta.height ?? 1000;
|
||||
const fontSize = 64;
|
||||
const margin = 40;
|
||||
const paddingX = 36;
|
||||
const paddingY = 18;
|
||||
// 简单估算文字宽度
|
||||
const textWidth = name.length * fontSize * 0.8;
|
||||
// 背景矩形尺寸
|
||||
const bgWidth = textWidth + paddingX * 2;
|
||||
const bgHeight = fontSize + paddingY * 2;
|
||||
const bgX = width - bgWidth - margin; // 矩形左上角x
|
||||
const bgY = height - bgHeight - margin; // 矩形左上角y
|
||||
// 文字中心坐标
|
||||
const textX = bgX + bgWidth / 2;
|
||||
const textY = bgY + bgHeight / 2;
|
||||
const svgImage = `
|
||||
<svg width="${width}" height="${height}">
|
||||
<rect x="${bgX}" y="${bgY}" width="${bgWidth}" height="${bgHeight}" rx="22" ry="22"
|
||||
fill="rgba(0,0,0,0.6)" />
|
||||
<text x="${textX}" y="${textY}"
|
||||
fill="#fff"
|
||||
font-size="${fontSize}"
|
||||
font-family="Arial, 'Microsoft YaHei', sans-serif"
|
||||
text-anchor="middle"
|
||||
dominant-baseline="middle">
|
||||
${name}
|
||||
</text>
|
||||
</svg>
|
||||
`;
|
||||
const outputBuffer = await sharp(imageBuffer)
|
||||
.composite([{ input: Buffer.from(svgImage), blend: "over" }])
|
||||
.jpeg()
|
||||
.toBuffer();
|
||||
return outputBuffer as Buffer<ArrayBuffer>;
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
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({
|
||||
projectId: z.number(),
|
||||
type: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId, type } = req.body;
|
||||
|
||||
const data = await u.db("t_assets").where("projectId", projectId).where("type", type).select("*");
|
||||
|
||||
for (const item of data) {
|
||||
if (item.filePath) {
|
||||
item.filePath = await u.oss.getFileUrl(item.filePath);
|
||||
} else {
|
||||
item.filePath = "";
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).send(success(data));
|
||||
}
|
||||
);
|
||||
70
src/routes/assets/getAssetsApi.ts
Normal file
70
src/routes/assets/getAssetsApi.ts
Normal file
@ -0,0 +1,70 @@
|
||||
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({
|
||||
projectId: z.number(),
|
||||
type: z.string(),
|
||||
name: z.string().optional(),
|
||||
page: z.number(),
|
||||
limit: z.number(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId, type, name, page = 1, limit = 10 } = req.body;
|
||||
const offset = (page - 1) * limit;
|
||||
let query = u
|
||||
.db("o_assets")
|
||||
.leftJoin("o_image", "o_assets.imageId", "o_image.id")
|
||||
.select("o_assets.*", "o_image.filePath")
|
||||
.where("o_assets.projectId", projectId)
|
||||
.andWhere("o_assets.type", type);
|
||||
if (name) {
|
||||
query = query.andWhere("name", "like", `%${name}%`);
|
||||
}
|
||||
// 分页查询
|
||||
const parentAssets = await query.where("o_assets.sonId", null).offset(offset).limit(limit);
|
||||
|
||||
// 获取所有子资产供关联使用
|
||||
let childQuery = u
|
||||
.db("o_assets")
|
||||
.leftJoin("o_image", "o_assets.imageId", "o_image.id")
|
||||
.select("o_assets.*", "o_image.filePath")
|
||||
.where("o_assets.projectId", projectId)
|
||||
.andWhere("o_assets.type", type)
|
||||
.whereNotNull("o_assets.sonId");
|
||||
if (name) {
|
||||
childQuery = childQuery.andWhere("o_assets.name", "like", `%${name}%`);
|
||||
}
|
||||
const childAssets = await childQuery;
|
||||
|
||||
// 为每个父资产添加子资产
|
||||
const result = await Promise.all(
|
||||
parentAssets.map(async (parent) => ({
|
||||
...parent,
|
||||
sonAssets: childAssets.filter((child) => child.sonId === parent.id),
|
||||
filePath: parent.filePath && (await u.oss.getFileUrl(parent.filePath!)),
|
||||
})),
|
||||
);
|
||||
|
||||
// 统计总数
|
||||
const totalQuery = (await u
|
||||
.db("o_assets")
|
||||
.where("projectId", projectId)
|
||||
.andWhere("type", type)
|
||||
.andWhere("sonId", null)
|
||||
.andWhere((qb) => {
|
||||
if (name) {
|
||||
qb.andWhere("name", "like", `%${name}%`);
|
||||
}
|
||||
})
|
||||
.count("* as total")
|
||||
.first()) as any;
|
||||
res.status(200).send(success({ data: result, total: totalQuery?.total }));
|
||||
},
|
||||
);
|
||||
@ -14,26 +14,24 @@ export default router.post(
|
||||
async (req, res) => {
|
||||
const { assetsId } = req.body;
|
||||
|
||||
const assets = await u.db("t_assets").where("id", assetsId).select("id", "filePath", "scriptId", "type", "state").first();
|
||||
const assets = await u.db("o_assets").where("id", assetsId).select("id", "imageId", "type", "state").first();
|
||||
|
||||
const tempAssets = await u.db("t_image").where("assetsId", assetsId).select("id", "filePath", "assetsId", "type", "state");
|
||||
const rawTempAssets = await u.db("o_image").where("assetsId", assetsId).select("id", "filePath", "assetsId", "type", "state");
|
||||
|
||||
for (const item of tempAssets) {
|
||||
if (item.filePath) {
|
||||
item.filePath = await u.oss.getFileUrl(item.filePath);
|
||||
} else {
|
||||
item.filePath = "";
|
||||
}
|
||||
}
|
||||
const tempAssets = await Promise.all(
|
||||
rawTempAssets.map(async (item) => ({
|
||||
...item,
|
||||
filePath: item.filePath ? await u.oss.getFileUrl(item.filePath) : "",
|
||||
selected: assets?.imageId != null && Number(item.id) === Number(assets.imageId),
|
||||
})),
|
||||
);
|
||||
|
||||
const data = {
|
||||
id: assets!.id,
|
||||
state: assets!.state,
|
||||
filePath: assets!.filePath ? await u.oss.getFileUrl(assets!.filePath) : "",
|
||||
scriptId: assets!.scriptId,
|
||||
imageId: assets!.imageId ?? null,
|
||||
tempAssets,
|
||||
};
|
||||
|
||||
res.status(200).send(success(data));
|
||||
},
|
||||
);
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
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({
|
||||
projectId: z.number(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId } = req.body;
|
||||
|
||||
const data = await u.db("t_script").where("projectId", projectId).select("name", "id").distinct("id", "name").orderBy("name", "asc");
|
||||
|
||||
res.status(200).send(success(data));
|
||||
},
|
||||
);
|
||||
@ -4,6 +4,7 @@ import { z } from "zod";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
import { stat } from "original-fs";
|
||||
const router = express.Router();
|
||||
|
||||
// 保存资产图片
|
||||
@ -13,75 +14,38 @@ export default router.post(
|
||||
id: z.number(),
|
||||
projectId: z.number(),
|
||||
base64: z.string().optional().nullable(),
|
||||
filePath: z.string().optional().nullable(),
|
||||
type: z.enum(["role", "scene", "tool"]),
|
||||
prompt: z.string().optional().nullable(),
|
||||
imageId: z.number().optional().nullable(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id, base64, filePath, prompt, projectId } = req.body;
|
||||
|
||||
let savePath: string | undefined;
|
||||
let imageUrl: string | undefined;
|
||||
|
||||
const { id, base64, type, prompt, projectId, imageId } = req.body;
|
||||
if (base64) {
|
||||
// base64图片上传逻辑
|
||||
//自定义上传选择的图片
|
||||
const matches = base64.match(/^data:image\/\w+;base64,(.+)$/);
|
||||
const realBase64 = matches ? matches[1] : base64;
|
||||
// 生成新的图片路径
|
||||
savePath = `/${projectId}/assets/${uuidv4()}.png`;
|
||||
const savePath = `/${projectId}/${type}/${uuidv4()}.png`;
|
||||
// 写入文件
|
||||
await u.oss.writeFile(savePath, Buffer.from(realBase64, "base64"));
|
||||
// 插入图片表
|
||||
await u.db("t_image").insert({
|
||||
const [idData] = await u.db("o_image").insert({
|
||||
assetsId: id,
|
||||
filePath: savePath,
|
||||
type: "image/png",
|
||||
type: type,
|
||||
state: "生成成功",
|
||||
});
|
||||
imageUrl = savePath; // 新图片路径
|
||||
} else if (filePath) {
|
||||
// 前端传入已存在图片路径
|
||||
try {
|
||||
savePath = new URL(filePath).pathname;
|
||||
} catch {
|
||||
savePath = filePath;
|
||||
}
|
||||
|
||||
// 检查图片表里是否有这条图片
|
||||
// const selectedImage = await u.db("t_image").where("filePath", savePath).first();
|
||||
// if (!selectedImage) {
|
||||
// return res.status(500).send({ success: false, message: "所选图片不存在,请重新生成或选定图片" });
|
||||
// }
|
||||
imageUrl = savePath;
|
||||
}
|
||||
|
||||
// 查旧资产图片
|
||||
const oldAsset = await u.db("t_assets").where("id", id).select("filePath", "type").first();
|
||||
|
||||
// 保存新旧图片差异和插临时表逻辑
|
||||
if (imageUrl && ((oldAsset?.filePath && oldAsset.filePath !== imageUrl) || (!oldAsset?.filePath && imageUrl))) {
|
||||
// 新图片保存,移除 t_image 表
|
||||
await u.db("t_image").where("filePath", imageUrl).delete();
|
||||
|
||||
// 原图片如果存在、且不在 t_image 表,插入临时表
|
||||
if (oldAsset?.filePath) {
|
||||
const oldInTemp = await u.db("t_image").where("filePath", oldAsset.filePath).first();
|
||||
if (!oldInTemp) {
|
||||
await u.db("t_image").insert({
|
||||
assetsId: id,
|
||||
filePath: oldAsset.filePath,
|
||||
type: oldAsset.type,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新资产表图片为新图片
|
||||
await u.db("t_assets").where("id", id).update({ filePath: imageUrl });
|
||||
await u.db("o_assets").where("id", id).update({
|
||||
prompt: prompt ?? "",
|
||||
imageId: idData,
|
||||
});
|
||||
} else {
|
||||
await u.db("o_assets").where("id", id).update({
|
||||
prompt: prompt ?? "",
|
||||
imageId: imageId,
|
||||
});
|
||||
}
|
||||
|
||||
// 更新提示信息
|
||||
if (prompt !== undefined && prompt !== null && prompt !== "") {
|
||||
await u.db("t_assets").where("id", id).update({ prompt });
|
||||
}
|
||||
|
||||
res.status(200).send(success({ message: "保存资产图片成功" }));
|
||||
},
|
||||
);
|
||||
|
||||
@ -3,37 +3,27 @@ import u from "@/utils";
|
||||
import { z } from "zod";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
import { id } from "zod/locales";
|
||||
const router = express.Router();
|
||||
|
||||
// 更新资产
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
intro: z.string(),
|
||||
type: z.string(),
|
||||
prompt: z.string(),
|
||||
videoPrompt: z.string().optional().nullable(),
|
||||
remark: z.string().optional().nullable(),
|
||||
duration: z.number().optional().nullable(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id, name, intro, type, prompt, remark, duration, videoPrompt } = req.body;
|
||||
|
||||
await u
|
||||
.db("t_assets")
|
||||
.where("id", id)
|
||||
.update({
|
||||
name,
|
||||
intro,
|
||||
type,
|
||||
prompt,
|
||||
remark,
|
||||
videoPrompt,
|
||||
duration: String(duration),
|
||||
});
|
||||
|
||||
res.status(200).send(success({ message: "更新资产成功" }));
|
||||
}
|
||||
"/",
|
||||
validateFields({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
describe: z.string(),
|
||||
remark: z.string(),
|
||||
prompt: z.string().optional().nullable(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id, name, describe, remark, prompt } = req.body;
|
||||
await u.db("o_assets").where({ id }).update({
|
||||
name,
|
||||
describe,
|
||||
remark,
|
||||
prompt,
|
||||
});
|
||||
res.status(200).send(success({ message: "更新资产成功" }));
|
||||
},
|
||||
);
|
||||
|
||||
61
src/routes/assets/uploadClip.ts
Normal file
61
src/routes/assets/uploadClip.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
import { z } from "zod";
|
||||
import { v4 as uuid } from "uuid";
|
||||
const router = express.Router();
|
||||
|
||||
// 根据 base64 头部获取文件扩展名
|
||||
function getExtFromBase64(base64Data: string): string {
|
||||
const mime = base64Data.match(/^data:([^;]+);base64,/)?.[1] ?? "";
|
||||
const mimeMap: Record<string, string> = {
|
||||
// 图片
|
||||
"image/jpeg": "jpeg",
|
||||
"image/jpg": "jpg",
|
||||
"image/png": "png",
|
||||
// 音频
|
||||
"audio/mpeg": "mp3",
|
||||
"audio/mp3": "mp3",
|
||||
"audio/wav": "wav",
|
||||
// 视频
|
||||
"video/mp4": "mp4",
|
||||
"video/webm": "webm",
|
||||
};
|
||||
return mimeMap[mime] ?? "bin";
|
||||
}
|
||||
|
||||
// 文件上传(支持图片、音频、视频)
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
projectId: z.number(),
|
||||
base64Data: z.string(),
|
||||
type: z.string().optional().default("clip"),
|
||||
name: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { base64Data, projectId, type = "clip", name } = req.body;
|
||||
const ext = getExtFromBase64(base64Data);
|
||||
const savePath = `/${projectId}/assets/${uuid()}.${ext}`;
|
||||
|
||||
await u.oss.writeFile(savePath, Buffer.from(base64Data.match(/base64,([A-Za-z0-9+/=]+)/)[1] ?? "", "base64"));
|
||||
const [id] = await u.db("o_assets").insert({
|
||||
type: type,
|
||||
projectId: projectId,
|
||||
name,
|
||||
startTime: Date.now(),
|
||||
});
|
||||
const [imageId] = await u.db("o_image").insert({
|
||||
filePath: savePath,
|
||||
type,
|
||||
assetsId: id,
|
||||
projectId,
|
||||
state: "1",
|
||||
});
|
||||
await u.db("o_assets").where("id", id).update({
|
||||
imageId: imageId,
|
||||
});
|
||||
res.status(200).send(success("上传成功"));
|
||||
},
|
||||
);
|
||||
136
src/routes/assetsGenerate/generateAssets.ts
Normal file
136
src/routes/assetsGenerate/generateAssets.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { z } from "zod";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { error, success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
const router = express.Router();
|
||||
// 生成资产图片
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
id: z.number(),
|
||||
type: z.enum(["role", "scene", "tool", "storyboard"]),
|
||||
projectId: z.number(),
|
||||
name: z.string(),
|
||||
base64: z.string().optional().nullable(),
|
||||
prompt: z.string(),
|
||||
model: z.string(),
|
||||
resolution: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id, type, projectId, base64, prompt, name, model, resolution } = req.body;
|
||||
//获取风格
|
||||
const project = await u.db("o_project").where("id", projectId).select("artStyle", "type", "intro").first();
|
||||
if (!project) return res.status(500).send(success({ message: "项目为空" }));
|
||||
const role = await u.getPrompts("role-generateImage") ?? "";
|
||||
const scene = await u.getPrompts("scene-generateImage") ?? "";
|
||||
const tool = await u.getPrompts("tool-generateImage") ?? "";
|
||||
|
||||
let systemPrompt = "";
|
||||
let userPrompt = "";
|
||||
if (type == "role") {
|
||||
systemPrompt = role;
|
||||
userPrompt = `
|
||||
请根据以下参数生成角色标准四视图:
|
||||
|
||||
**基础参数:**
|
||||
- 画风风格: ${project?.artStyle || "未指定"}
|
||||
|
||||
**角色设定:**
|
||||
- 名称:${name},
|
||||
- 提示词:${prompt},
|
||||
|
||||
请严格按照系统规范生成人物角色四视图。
|
||||
`;
|
||||
}
|
||||
if (type == "scene") {
|
||||
systemPrompt = scene;
|
||||
userPrompt = `
|
||||
请根据以下参数生成标准场景图:
|
||||
|
||||
**基础参数:**
|
||||
- 画风风格: ${project?.artStyle || "未指定"}
|
||||
|
||||
**场景设定:**
|
||||
- 名称:${name},
|
||||
- 提示词:${prompt},
|
||||
|
||||
请严格按照系统规范生成标准场景图。
|
||||
`;
|
||||
}
|
||||
if (type == "tool") {
|
||||
systemPrompt = tool;
|
||||
userPrompt = `
|
||||
请根据以下参数生成标准道具图:
|
||||
|
||||
**基础参数:**
|
||||
- 画风风格: ${project?.artStyle || "未指定"}
|
||||
|
||||
**道具设定:**
|
||||
- 名称:${name},
|
||||
- 提示词:${prompt},
|
||||
|
||||
请严格按照系统规范生成标准道具图。
|
||||
`;
|
||||
}
|
||||
const [imageId] = await u.db("o_image").insert({
|
||||
type: type,
|
||||
state: "生成中",
|
||||
assetsId: id,
|
||||
});
|
||||
let taskClass = "";
|
||||
if (type == "role") taskClass = "角色图生成";
|
||||
if (type == "scene") taskClass = "场景图生成";
|
||||
if (type == "tool") taskClass = "道具图生成";
|
||||
|
||||
try {
|
||||
let imagePath;
|
||||
let insertType;
|
||||
|
||||
if (type == "role") {
|
||||
insertType = "role";
|
||||
imagePath = `/${projectId}/role/${uuidv4()}.jpg`;
|
||||
}
|
||||
if (type == "scene") {
|
||||
insertType = "scene";
|
||||
imagePath = `/${projectId}/scene/${uuidv4()}.jpg`;
|
||||
}
|
||||
if (type == "tool") {
|
||||
insertType = "tool";
|
||||
imagePath = `/${projectId}/props/${uuidv4()}.jpg`;
|
||||
}
|
||||
|
||||
const aiImage = u.Ai.Image(model);
|
||||
await aiImage.run({
|
||||
systemPrompt,
|
||||
prompt: userPrompt,
|
||||
imageBase64: base64 ? [base64] : [],
|
||||
size: resolution,
|
||||
aspectRatio: "16:9",
|
||||
});
|
||||
aiImage.save(imagePath!);
|
||||
const imageData = await u.db("o_image").where("id", imageId).select("*").first();
|
||||
if (imageData) {
|
||||
await u.db("o_image").where("id", imageId).update({
|
||||
state: "生成成功",
|
||||
filePath: imagePath,
|
||||
type: insertType,
|
||||
});
|
||||
const path = await u.oss.getFileUrl(imagePath!);
|
||||
await u.db("o_assets").where("id", id).update({
|
||||
imageId: imageId,
|
||||
});
|
||||
return res.status(200).send(success({ path, assetsId: id }));
|
||||
} else {
|
||||
return res.status(500).send("资产已被删除");
|
||||
}
|
||||
} catch (e) {
|
||||
await u.db("o_image").where("id", imageId).update({
|
||||
state: "生成失败",
|
||||
});
|
||||
const msg = u.error(e).message || "图片生成失败";
|
||||
return res.status(400).send(error(msg));
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -54,12 +54,12 @@ export default router.post(
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { assetsId, projectId, type, name, describe } = req.body;
|
||||
|
||||
//获取风格
|
||||
const project = await u.db("t_project").where("id", projectId).select("artStyle", "type", "intro").first();
|
||||
const project = await u.db("o_project").where("id", projectId).select("artStyle", "type", "intro").first();
|
||||
//如果没有找到对应的项目,返回错误
|
||||
if (!project) return res.status(500).send(success({ message: "项目为空" }));
|
||||
|
||||
const allOutlineDataList: { data: string }[] = await u.db("t_outline").where("projectId", projectId).select("data");
|
||||
const allOutlineDataList: { data: string }[] = await u.db("o_outline").where("projectId", projectId).select("data");
|
||||
|
||||
const itemMap: Record<string, ResultItem> = {};
|
||||
|
||||
@ -84,23 +84,15 @@ export default router.post(
|
||||
|
||||
const result: ResultItem[] = Object.values(itemMap);
|
||||
|
||||
const promptsList = await u.db("t_prompts").where("code", "in", ["role-polish", "scene-polish", "storyboard-polish", "tool-polish"]);
|
||||
const apiConfigData = await u.getPromptAi("assetsPrompt");
|
||||
const errPrompts = "不论用户说什么,请直接输出AI配置异常";
|
||||
const getPromptValue = (code: string) => {
|
||||
const item = promptsList.find((p) => p.code === code);
|
||||
return item?.customValue ?? item?.defaultValue ?? errPrompts;
|
||||
};
|
||||
const role = getPromptValue("role-polish");
|
||||
const scene = getPromptValue("scene-polish");
|
||||
const tool = getPromptValue("tool-polish");
|
||||
const storyboard = getPromptValue("storyboard-polish");
|
||||
const role = (await u.getPrompts("role-polish")) ?? "";
|
||||
const scene = (await u.getPrompts("scene-polish")) ?? "";
|
||||
const tool = (await u.getPrompts("tool-polish")) ?? "";
|
||||
let systemPrompt = "";
|
||||
let userPrompt = "";
|
||||
if (type == "role") {
|
||||
const data = findItemByName(result, name, "characters");
|
||||
const chapterRange = Array.isArray(data?.chapterRange) ? data.chapterRange : [data?.chapterRange];
|
||||
const novelData = (await u.db("t_novel").whereIn("chapterIndex", chapterRange).select("*")) as NovelChapter[];
|
||||
const novelData = (await u.db("o_novel").whereIn("chapterIndex", [1]).select("*")) as NovelChapter[];
|
||||
const results: string = mergeNovelText(novelData);
|
||||
systemPrompt = role;
|
||||
userPrompt = `
|
||||
@ -124,7 +116,7 @@ export default router.post(
|
||||
const data = findItemByName(result, name, "scenes");
|
||||
|
||||
const chapterRange = Array.isArray(data?.chapterRange) ? data.chapterRange : [data?.chapterRange];
|
||||
const novelData = (await u.db("t_novel").whereIn("chapterIndex", chapterRange).select("*")) as NovelChapter[];
|
||||
const novelData = (await u.db("o_novel").whereIn("chapterIndex", [1]).select("*")) as NovelChapter[];
|
||||
const results: string = mergeNovelText(novelData);
|
||||
systemPrompt = scene;
|
||||
userPrompt = `
|
||||
@ -144,10 +136,10 @@ export default router.post(
|
||||
|
||||
`;
|
||||
}
|
||||
if (type == "props") {
|
||||
if (type == "tool") {
|
||||
const data = findItemByName(result, name, "props");
|
||||
const chapterRange = Array.isArray(data?.chapterRange) ? data.chapterRange : [data?.chapterRange];
|
||||
const novelData = (await u.db("t_novel").whereIn("chapterIndex", chapterRange).select("*")) as NovelChapter[];
|
||||
const novelData = (await u.db("o_novel").whereIn("chapterIndex", [1]).select("*")) as NovelChapter[];
|
||||
const results: string = mergeNovelText(novelData);
|
||||
systemPrompt = tool;
|
||||
userPrompt = `
|
||||
@ -167,26 +159,8 @@ export default router.post(
|
||||
|
||||
`;
|
||||
}
|
||||
if (type == "storyboard") {
|
||||
systemPrompt = storyboard;
|
||||
userPrompt = `
|
||||
请根据以下参数生成分镜图提示词:
|
||||
|
||||
**基础参数:**
|
||||
- 风格: ${project?.artStyle || "未指定"}
|
||||
- 小说类型: ${project?.type || "未指定"}
|
||||
- 小说背景: ${project?.intro || "未指定"}
|
||||
|
||||
**分镜设定:**
|
||||
- 分镜名称:${name},
|
||||
- 分镜描述:${describe},
|
||||
|
||||
请严格按照系统规范生成分镜图提示词。
|
||||
|
||||
`;
|
||||
}
|
||||
async function generatePrompt() {
|
||||
const result = await u.ai.text.invoke(
|
||||
const result = await u.Ai.Text("assetsAi").invoke(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
@ -198,19 +172,22 @@ export default router.post(
|
||||
content: userPrompt,
|
||||
},
|
||||
],
|
||||
output: {
|
||||
prompt: zod.string().describe("提示词"),
|
||||
},
|
||||
},
|
||||
apiConfigData,
|
||||
);
|
||||
return result.prompt;
|
||||
)
|
||||
return result;
|
||||
|
||||
}
|
||||
try {
|
||||
const prompt = (await generatePrompt()) as any;
|
||||
if (!prompt) return res.status(500).send("失败");
|
||||
//添加到任务
|
||||
const { _output } = (await generatePrompt()) as any;
|
||||
if (_output) {
|
||||
await u.db("o_assets").where("id", assetsId).update({
|
||||
prompt: _output,
|
||||
});
|
||||
}
|
||||
if (!_output) return res.status(500).send("失败");
|
||||
|
||||
res.status(200).send(success({ prompt: prompt, assetsId }));
|
||||
res.status(200).send(success({ prompt: _output, assetsId }));
|
||||
} catch (e: any) {
|
||||
return res.status(500).send(error(e?.data?.error?.message ?? e?.message ?? "生成失败"));
|
||||
}
|
||||
@ -5,7 +5,7 @@ import { success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
const router = express.Router();
|
||||
|
||||
// 获取项目统计
|
||||
// 获取项目概览统计
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
@ -14,13 +14,13 @@ export default router.post(
|
||||
async (req, res) => {
|
||||
const { projectId } = req.body;
|
||||
|
||||
const scripts = await u.db("t_script").where("projectId", projectId).select("id");
|
||||
const scripts = await u.db("o_script").where("projectId", projectId).select("id");
|
||||
const scriptIds = scripts.map((item: any) => item.id);
|
||||
|
||||
const roleCount: any = await u.db("t_assets").where("projectId", projectId).where("type", "角色").count("* as total").first();
|
||||
const scriptCount: any = await u.db("t_script").where("projectId", projectId).count("* as total").first();
|
||||
const videoCount: any = await u.db("t_video").whereIn("scriptId", scriptIds).count("* as total").first();
|
||||
const storyboardCount: any = await u.db("t_assets").whereIn("scriptId", scriptIds).where("type", "分镜").count("* as total").first();
|
||||
const roleCount: any = await u.db("o_assets").where("projectId", projectId).where("type", "角色").count("* as total").first();
|
||||
const scriptCount: any = await u.db("o_script").where("projectId", projectId).count("* as total").first();
|
||||
const videoCount: any = await u.db("o_video").whereIn("scriptId", scriptIds).count("* as total").first();
|
||||
const storyboardCount: any = await u.db("o_assets").whereIn("scriptId", scriptIds).where("type", "分镜").count("* as total").first();
|
||||
|
||||
const data = {
|
||||
roleCount: roleCount?.total || 0,
|
||||
@ -14,7 +14,7 @@ export default router.post(
|
||||
async (req, res) => {
|
||||
const { id } = req.body;
|
||||
|
||||
const data = await u.db("t_project").where("id", id).select("*");
|
||||
const data = await u.db("o_project").where("id", id).select("*");
|
||||
|
||||
res.status(200).send(success(data));
|
||||
}
|
||||
@ -19,11 +19,12 @@ export default router.post(
|
||||
async (req, res) => {
|
||||
const { id, intro, type, artStyle, videoRatio, projectType } = req.body;
|
||||
|
||||
await u.db("t_project").where("id", id).update({
|
||||
await u.db("o_project").where("id", id).update({
|
||||
intro,
|
||||
type,
|
||||
artStyle,
|
||||
videoRatio,
|
||||
projectType,
|
||||
});
|
||||
|
||||
res.status(200).send(success({ message: "修改成功" }));
|
||||
@ -23,19 +23,19 @@ export default router.post(
|
||||
async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
const data = await u.db("t_user").where("name", "=", username).first();
|
||||
const data = await u.db("o_user").where("name", "=", username).first();
|
||||
if (!data) return res.status(400).send(error("登录失败"));
|
||||
|
||||
if (data!.password == password && data!.name == username) {
|
||||
const tokenSecret = await u.db("t_setting").where("userId", data.id).select("tokenKey").first();
|
||||
|
||||
const tokenData = await u.db("o_setting").where("key", "tokenKey").first();
|
||||
if (!tokenData) return res.status(400).send(error("未找到tokenKey"));
|
||||
const token = setToken(
|
||||
{
|
||||
id: data!.id,
|
||||
name: data!.name,
|
||||
},
|
||||
"180Days",
|
||||
tokenSecret?.tokenKey as string,
|
||||
tokenData?.value as string,
|
||||
);
|
||||
|
||||
return res.status(200).send(success({ token: "Bearer " + token, name: data!.name, id: data!.id }, "登录成功"));
|
||||
133
src/routes/migrate/migrateData.ts
Normal file
133
src/routes/migrate/migrateData.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import express from "express";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
import db from "@/utils/db";
|
||||
import type { DB } from "@/types/database";
|
||||
import knex from "knex";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { tr } from "zod/locales";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 迁移数据
|
||||
export default router.post(
|
||||
"/",
|
||||
async (req, res) => {
|
||||
// return res.status(200).send({
|
||||
// success: true,
|
||||
// message: '数据迁移功能已关闭,建议手动迁移数据后删除旧数据库文件'
|
||||
// });
|
||||
//连接旧数据库,读取数据
|
||||
try {
|
||||
let db2: knex.Knex | null = null;
|
||||
//读取旧数据库路径
|
||||
let db2Path: string;
|
||||
if (typeof process.versions?.electron !== "undefined") {
|
||||
const { app } = require("electron");
|
||||
const userDataDir: string = app.getPath("userData");
|
||||
db2Path = path.join(userDataDir, "db2.sqlite");
|
||||
} else {
|
||||
db2Path = path.join(process.cwd(), "db2.sqlite");
|
||||
}
|
||||
const dbDir = path.dirname(db2Path);
|
||||
// 确保数据库目录存在
|
||||
if (!fs.existsSync(dbDir)) {
|
||||
fs.mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(db2Path)) {
|
||||
return res.status(404).send({
|
||||
success: false,
|
||||
message: `源数据库文件不存在: ${db2Path}`
|
||||
});
|
||||
}
|
||||
//连接旧数据库
|
||||
db2 = knex({
|
||||
client: "sqlite3",
|
||||
connection: {
|
||||
filename: db2Path,
|
||||
},
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
//需要迁移的旧数据表
|
||||
const db2TableNames = [
|
||||
't_project',
|
||||
't_assets',
|
||||
't_event',
|
||||
't_image',
|
||||
't_novel',
|
||||
't_outline',
|
||||
't_script',
|
||||
't_storyboard',
|
||||
't_video',
|
||||
]
|
||||
//新数据库的表
|
||||
const dbTableNames = [
|
||||
'o_project',
|
||||
'o_assets',
|
||||
'o_event',
|
||||
'o_eventChapter',
|
||||
'o_image',
|
||||
'o_novel',
|
||||
'o_outline',
|
||||
'o_outlineNovel',
|
||||
'o_script',
|
||||
'o_scriptAssets',
|
||||
'o_scriptOutline',
|
||||
'o_storyboard',
|
||||
'o_storyboardScript',
|
||||
'o_video',
|
||||
]
|
||||
|
||||
for (const tableName of db2TableNames) {
|
||||
try {
|
||||
// 从 db2 读取数据
|
||||
const sourceData = await db2(tableName).select('*');
|
||||
for (const item of sourceData) {
|
||||
//迁移项目表
|
||||
if (tableName === 't_project') {
|
||||
// await db("o_project").insert({
|
||||
// name: item.name,
|
||||
// intro: item.intro,
|
||||
// type: item.type,
|
||||
// artStyle: item.artStyle,
|
||||
// videoRatio: item.videoRatio,
|
||||
// createTime: item.createTime,
|
||||
// userId: item.userId,
|
||||
// projectType: "基于小说原文"
|
||||
// })
|
||||
}
|
||||
//迁移资产表
|
||||
if (tableName === 't_assets') {
|
||||
}
|
||||
//迁移事件表
|
||||
if (tableName === 't_event') { }
|
||||
//迁移图片表
|
||||
if (tableName === 't_image') { }
|
||||
//迁移小说表
|
||||
if (tableName === 't_novel') { }
|
||||
//迁移大纲表
|
||||
if (tableName === 't_outline') { }
|
||||
//迁移脚本表
|
||||
if (tableName === 't_script') { }
|
||||
//迁移分镜表
|
||||
if (tableName === 't_storyboard') { }
|
||||
//迁移视频表
|
||||
if (tableName === 't_video') { }
|
||||
}
|
||||
// // 将数据插入到 db 中
|
||||
// const targetTableName = dbTableNames[db2TableNames.indexOf(tableName)];
|
||||
// await db(targetTableName).insert(sourceData);
|
||||
// console.log(`成功迁移表 ${tableName} 的数据到 ${targetTableName}`);
|
||||
} catch (error) {
|
||||
console.error(`连接旧数据库失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('连接旧数据库失败:', error);
|
||||
}
|
||||
return res.status(200).send({
|
||||
success: true,
|
||||
message: '数据迁移功能已关闭,建议手动迁移数据后删除旧数据库文件'
|
||||
});
|
||||
}
|
||||
);
|
||||
39
src/routes/modelSelect/getModelList.ts
Normal file
39
src/routes/modelSelect/getModelList.ts
Normal file
@ -0,0 +1,39 @@
|
||||
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", "all"]),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { type } = req.body;
|
||||
const data = await u.db("o_vendorConfig").select("id", "models", "name").first();
|
||||
if (!data) {
|
||||
return res.status(404).send({ error: "模型未找到" });
|
||||
}
|
||||
const models = JSON.parse(data.models!);
|
||||
if (type === "all") {
|
||||
const allData = models.filter((item: { type: string }) => item.type !== "video").map((item: { name: string; modelName: string; type: string }) => ({
|
||||
id: data.id,
|
||||
label: item.name,
|
||||
value: item.modelName,
|
||||
type: item.type,
|
||||
name: data.name,
|
||||
}));
|
||||
return res.status(200).send(success(allData));
|
||||
}
|
||||
const filteredData = models.filter((item: { type: string }) => item.type === type).map((item: { name: string; modelName: string; type: string }) => ({
|
||||
id: data.id,
|
||||
label: item.name,
|
||||
value: item.modelName,
|
||||
type: item.type,
|
||||
name: data.name,
|
||||
}));
|
||||
res.status(200).send(success(filteredData));
|
||||
}
|
||||
);
|
||||
@ -23,7 +23,7 @@ export default router.post(
|
||||
const { projectId, data } = req.body;
|
||||
|
||||
for (const item of data) {
|
||||
await u.db("t_novel").insert({
|
||||
await u.db("o_novel").insert({
|
||||
projectId,
|
||||
chapterIndex: item.index,
|
||||
reel: item.reel,
|
||||
|
||||
27
src/routes/novel/batchDeleteNovel.ts
Normal file
27
src/routes/novel/batchDeleteNovel.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { z } from "zod";
|
||||
import { error, success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
const router = express.Router();
|
||||
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
ids: z.array(z.number()),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { ids } = req.body;
|
||||
if (!ids.length) {
|
||||
return res.status(400).send(error("请先选择需要删除的内容"));
|
||||
}
|
||||
const chapterData = await u.db("o_eventChapter").whereIn("novelId", ids);
|
||||
await u.db("o_eventChapter").whereIn("novelId", ids).delete();
|
||||
const eventIds = chapterData.map((i) => i.id);
|
||||
if (eventIds.length) await u.db("o_event").whereIn("id", eventIds).delete();
|
||||
|
||||
await u.db("o_novel").whereIn("id", ids).del();
|
||||
|
||||
res.status(200).send(success({ message: "删除原文成功" }));
|
||||
},
|
||||
);
|
||||
@ -14,8 +14,12 @@ export default router.post(
|
||||
async (req, res) => {
|
||||
const { id } = req.body;
|
||||
|
||||
await u.db("t_novel").where("id", id).del();
|
||||
const chapterData = await u.db("o_eventChapter").where("novelId", id);
|
||||
await u.db("o_eventChapter").where("novelId", id).delete();
|
||||
const eventIds = chapterData.map((i) => i.id);
|
||||
if (eventIds.length) await u.db("o_event").whereIn("id", eventIds).delete();
|
||||
await u.db("o_novel").where("id", id).del();
|
||||
|
||||
res.status(200).send(success({ message: "删除原文成功" }));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@ -5,20 +5,17 @@ import { success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
const router = express.Router();
|
||||
|
||||
// 更新前要
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
id: z.number(),
|
||||
content: z.string(),
|
||||
ids: z.array(z.number()),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id, content } = req.body;
|
||||
const { ids } = req.body;
|
||||
|
||||
await u.db("t_script").where("id", id).update({
|
||||
content,
|
||||
});
|
||||
await u.db("o_event").whereIn("id", ids).del();
|
||||
await u.db("o_eventChapter").whereIn("eventId", ids).del();
|
||||
|
||||
res.status(200).send(success({ message: "更新前要成功" }));
|
||||
}
|
||||
res.status(200).send(success({ message: "删除事件成功" }));
|
||||
},
|
||||
);
|
||||
@ -5,7 +5,6 @@ import { success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
const router = express.Router();
|
||||
|
||||
//删除分镜
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
@ -13,7 +12,10 @@ export default router.post(
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id } = req.body;
|
||||
await u.db("t_assets").where("id", id).delete();
|
||||
res.status(200).send(success("分镜删除成功"));
|
||||
|
||||
await u.db("o_event").where("id", id).del();
|
||||
await u.db("o_eventChapter").where("eventId", id).del();
|
||||
|
||||
res.status(200).send(success({ message: "删除事件成功" }));
|
||||
},
|
||||
);
|
||||
89
src/routes/novel/event/generateEvents.ts
Normal file
89
src/routes/novel/event/generateEvents.ts
Normal file
@ -0,0 +1,89 @@
|
||||
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();
|
||||
|
||||
// 解析章节字符串,支持逗号分隔的多段(如 "1-3,5,7-9")
|
||||
function parseChapters(str: string): number[] {
|
||||
const result: number[] = [];
|
||||
// 逗号和空格之间加以划分
|
||||
const segments = str
|
||||
.split(",")
|
||||
.map((s) => s.replace(/[^\d\-]/g, "").trim())
|
||||
.filter(Boolean);
|
||||
for (const seg of segments) {
|
||||
// 匹配区间
|
||||
if (/^\d+\-\d+$/.test(seg)) {
|
||||
const [start, end] = seg.split("-").map(Number);
|
||||
if (start <= end) {
|
||||
for (let i = start; i <= end; i++) result.push(i);
|
||||
}
|
||||
} else if (/^\d+$/.test(seg)) {
|
||||
result.push(Number(seg));
|
||||
}
|
||||
// 其它格式自动忽略
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
parseChapters("7-8章");
|
||||
// 清洗小说原文,生成事件列表
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
projectId: z.number(),
|
||||
windowSize: z.number().optional().default(5), // 每组数量,默认 5
|
||||
overlap: z.number().optional().default(1), // 交叠数量,默认 1
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId, windowSize, overlap } = req.body;
|
||||
//删除之前的事件
|
||||
const [allChapters, novel] = await Promise.all([
|
||||
u.db("o_novel").where("projectId", projectId),
|
||||
Promise.resolve(new u.cleanNovel(windowSize, overlap)),
|
||||
]);
|
||||
const novelIds = allChapters.map((i) => i.id);
|
||||
await u
|
||||
.db("o_eventChapter")
|
||||
.whereIn("novelId", novelIds as number[])
|
||||
.delete();
|
||||
|
||||
const eventIds = await u.db("o_eventChapter").whereIn("novelId", novelIds).select("eventId").pluck("eventId");
|
||||
|
||||
await u
|
||||
.db("o_event")
|
||||
.whereIn("id", eventIds as number[])
|
||||
.delete();
|
||||
|
||||
const data = await novel.start(allChapters, projectId);
|
||||
|
||||
const chapterMap = new Map(allChapters.map((c) => [c.chapterIndex, c]));
|
||||
|
||||
const novelEvent: { eventId: number; novelId: number }[] = [];
|
||||
const now = Date.now();
|
||||
|
||||
for (const item of data) {
|
||||
const [id] = await u.db("o_event").insert({
|
||||
name: item.name,
|
||||
detail: item.detail,
|
||||
createTime: now,
|
||||
});
|
||||
|
||||
parseChapters(item.chapter).forEach((chapterIndex) => {
|
||||
const chapter = chapterMap.get(chapterIndex);
|
||||
if (chapter) {
|
||||
novelEvent.push({ eventId: id, novelId: chapter.id! });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (novelEvent.length > 0) {
|
||||
await u.db("o_eventChapter").insert(novelEvent);
|
||||
}
|
||||
|
||||
return res.status(200).send(success(data));
|
||||
},
|
||||
);
|
||||
67
src/routes/novel/event/getEvent.ts
Normal file
67
src/routes/novel/event/getEvent.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { db } from "@/utils/db";
|
||||
import { z } from "zod";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
const router = express.Router();
|
||||
|
||||
// CREATE TABLE `o_event` (`id` integer not null, `name` varchar(255), `detail` varchar(255), `createTime` integer, primary key (`id`));
|
||||
// CREATE TABLE `o_eventChapter` (`id` integer not null, `eventId` integer, `novelId` integer, foreign key(`eventId`) references `o_event`(`id`), foreign key(`novelId`) references `o_novel`(`id`), primary key (`id`));
|
||||
// CREATE TABLE `o_novel` (`id` integer not null, `chapterIndex` integer, `reel` text, `chapter` text, `chapterData` text, `projectId` integer, `createTime` integer, primary key (`id`));
|
||||
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
projectId: z.number(),
|
||||
page: z.number(),
|
||||
limit: z.number(),
|
||||
search: z.string().optional(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId, page, limit, search } = req.body;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// 构造基础查询:通过 o_eventChapter -> o_novel 过滤 projectId,再 join o_event 取名称和内容
|
||||
const baseQuery = u
|
||||
.db("o_event as e")
|
||||
.join("o_eventChapter as ec", "ec.eventId", "e.id")
|
||||
.join("o_novel as n", "n.id", "ec.novelId")
|
||||
.where("n.projectId", projectId);
|
||||
|
||||
if (search) {
|
||||
baseQuery.where("e.name", "like", `%${search}%`);
|
||||
}
|
||||
|
||||
// 统计去重后的事件总数
|
||||
const [{ total }] = await baseQuery.clone().countDistinct("e.id as total");
|
||||
|
||||
if (!Number(total)) {
|
||||
return res.status(200).send(success({ list: [], total: 0 }));
|
||||
}
|
||||
|
||||
// 分页查询:每个事件对应多个 chapterIndex,用 GROUP_CONCAT 聚合
|
||||
const rows = await baseQuery
|
||||
.clone()
|
||||
.select(
|
||||
"e.id",
|
||||
"e.name as eventName",
|
||||
"e.detail",
|
||||
"e.createTime",
|
||||
db.raw("GROUP_CONCAT(n.chapterIndex) as chapterIndexes"),
|
||||
)
|
||||
.groupBy("e.id")
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
|
||||
const list = rows.map((e: { id: number; eventName: string; detail: string; createTime: number; chapterIndexes: string | null }) => ({
|
||||
id: e.id,
|
||||
eventName: e.eventName,
|
||||
detail: e.detail,
|
||||
createTime: e.createTime,
|
||||
chapters: e.chapterIndexes ? e.chapterIndexes.split(",").map(Number) : [],
|
||||
}));
|
||||
|
||||
res.status(200).send(success({ list, total: Number(total) }));
|
||||
},
|
||||
);
|
||||
@ -10,16 +10,38 @@ export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
projectId: z.number(),
|
||||
page: z.number(),
|
||||
limit: z.number(),
|
||||
search: z.string().optional(),
|
||||
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId } = req.body;
|
||||
|
||||
const { projectId, page, limit, search } = req.body;
|
||||
const offset = (page - 1) * limit;
|
||||
const data = await u
|
||||
.db("t_novel")
|
||||
.db("o_novel")
|
||||
.where("projectId", projectId)
|
||||
.select("id", "chapterIndex as index", "reel", "chapter", "chapterData")
|
||||
.orderBy("chapterIndex", "asc");
|
||||
.andWhere((qb) => {
|
||||
if (search) {
|
||||
qb.where("chapter", "like", `%${search}%`);
|
||||
}
|
||||
})
|
||||
.orderBy("chapterIndex", "asc")
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
// 统计总数
|
||||
const totalQuery = (await u
|
||||
.db("o_novel")
|
||||
.where("projectId", projectId)
|
||||
.andWhere((qb) => {
|
||||
if (search) {
|
||||
qb.where("chapter", "like", `%${search}%`);
|
||||
}
|
||||
})
|
||||
.count("* as total")
|
||||
.first()) as any;
|
||||
|
||||
res.status(200).send(success(data));
|
||||
res.status(200).send(success({ data, total: totalQuery.total }));
|
||||
}
|
||||
);
|
||||
|
||||
@ -18,7 +18,7 @@ export default router.post(
|
||||
async (req, res) => {
|
||||
const { id, index, reel, chapter, chapterData } = req.body;
|
||||
|
||||
await u.db("t_novel").where("id", id).update({
|
||||
await u.db("o_novel").where("id", id).update({
|
||||
chapterIndex: index,
|
||||
reel,
|
||||
chapter,
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
import initDB from "@/lib/initDB";
|
||||
|
||||
import { db } from "@/utils/db";
|
||||
import express from "express";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
const router = express.Router();
|
||||
|
||||
// 清空所有表 (sqlite)
|
||||
export default router.post("/", async (req, res) => {
|
||||
await initDB(db, true);
|
||||
res.status(200).send(success("清空数据库成功"));
|
||||
});
|
||||
@ -1,25 +1,14 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import initDB from "@/lib/initDB";
|
||||
import { db } from "@/utils/db";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
const router = express.Router();
|
||||
|
||||
// 删除数据库表数据
|
||||
export default router.post("/", async (req, res) => {
|
||||
const projects = await u.db("t_project").select("id");
|
||||
|
||||
const projectIds = projects.map((project) => project.id);
|
||||
|
||||
await Promise.all(
|
||||
projectIds.map(async (id) => {
|
||||
try {
|
||||
await u.oss.deleteDirectory(String(id));
|
||||
} catch (error) {
|
||||
console.error(`删除OSS文件失败,项目ID: ${id}`, error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// await initDB(db, true);
|
||||
|
||||
res.status(200).send(success("清空数据库成功"));
|
||||
});
|
||||
// 清空数据表
|
||||
export default router.post(
|
||||
"/",
|
||||
async (req, res) => {
|
||||
await initDB(db, true);
|
||||
res.status(200).send(success({ message: "清空数据表成功" }));
|
||||
},
|
||||
);
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
import express from "express";
|
||||
import { success, error } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
import u from "@/utils";
|
||||
import { z } from "zod";
|
||||
import { tool } from "ai";
|
||||
const router = express.Router();
|
||||
|
||||
// 检查语言模型
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
modelName: z.string(),
|
||||
apiKey: z.string(),
|
||||
baseURL: z.string().optional(),
|
||||
manufacturer: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { modelName, apiKey, baseURL, manufacturer } = req.body;
|
||||
|
||||
const getWeatherTool = tool({
|
||||
description: "Get the weather in a location",
|
||||
inputSchema: z.object({
|
||||
location: z.string().describe("The location to get the weather for"),
|
||||
}),
|
||||
execute: async ({ location }) => {
|
||||
return {
|
||||
location,
|
||||
temperature: 72 + Math.floor(Math.random() * 21) - 10,
|
||||
};
|
||||
},
|
||||
});
|
||||
try {
|
||||
const { reply } = await u.ai.text.invoke(
|
||||
{
|
||||
prompt: "请调用工具获取北京的天气,并回答我多少气温",
|
||||
tools: { getWeatherTool },
|
||||
output: {
|
||||
reply: z.string().describe("回复内容"),
|
||||
},
|
||||
},
|
||||
{
|
||||
model: modelName,
|
||||
apiKey,
|
||||
baseURL,
|
||||
manufacturer,
|
||||
},
|
||||
);
|
||||
res.status(200).send(success(reply));
|
||||
} catch (err) {
|
||||
console.log("%c Line:51 🍬 err", "background:#465975", err);
|
||||
const msg = u.error(err).message;
|
||||
console.error(msg);
|
||||
res.status(500).send(error(msg));
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -1,47 +0,0 @@
|
||||
import express from "express";
|
||||
import { success, error } from "@/lib/responseFormat";
|
||||
import u from "@/utils";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
import { z } from "zod";
|
||||
const router = express.Router();
|
||||
|
||||
// 检查语言模型
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
modelName: z.string().optional(),
|
||||
apiKey: z.string(),
|
||||
baseURL: z.string().optional(),
|
||||
manufacturer: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { modelName, apiKey, baseURL, manufacturer } = req.body;
|
||||
try {
|
||||
const image = await u.ai.image(
|
||||
{
|
||||
prompt:
|
||||
"一张16:9比例的图片,完美等分为2x2四宫格布局,各区域无缝衔接:\n左上宫格:一只可爱的猫,毛发蓬松,眼睛明亮,姿态俏皮\n右上宫格:一只友善的狗,金毛犬,表情愉悦,摇着尾巴\n左下宫格:一头健壮的牛,田园背景,目光温和,皮毛光泽\n右下宫格:一匹骏马,姿态优雅,鬃毛飘逸,肌肉健美\n风格要求:四个宫格风格统一,色彩鲜艳饱和,高清画质,细节清晰锐利,专业插画风格,线条干净,统一的左上方光源,柔和阴影,和谐配色,卡通/半写实风格,宫格间用白色或浅灰细线分隔",
|
||||
imageBase64: [],
|
||||
aspectRatio: "9:16",
|
||||
size: "4K",
|
||||
taskClass: "测试任务",
|
||||
name: "测试图片生成",
|
||||
describe: "测试语言模型生成图片",
|
||||
projectId: 0,
|
||||
},
|
||||
{
|
||||
model: modelName,
|
||||
apiKey,
|
||||
baseURL,
|
||||
manufacturer,
|
||||
},
|
||||
);
|
||||
res.status(200).send(success(image));
|
||||
} catch (err) {
|
||||
console.log("%c Line:41 🍖 err", "background:#fca650", err);
|
||||
const msg = u.error(err).message;
|
||||
console.error(msg);
|
||||
res.status(500).send(error(msg));
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -1,51 +0,0 @@
|
||||
import express from "express";
|
||||
import { success, error } from "@/lib/responseFormat";
|
||||
import u from "@/utils";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
import { z } from "zod";
|
||||
const router = express.Router();
|
||||
|
||||
// 检查语言模型
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
modelName: z.string().optional(),
|
||||
apiKey: z.string(),
|
||||
baseURL: z.string().optional(),
|
||||
manufacturer: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { modelName, apiKey, baseURL, manufacturer } = req.body;
|
||||
try {
|
||||
const duration = manufacturer == "gemini" ? 4 : 5;
|
||||
const videoPath = await u.ai.video(
|
||||
{
|
||||
imageBase64: [],
|
||||
savePath: "test.mp4",
|
||||
prompt: "stickman Dances",
|
||||
duration: duration,
|
||||
resolution: "720p",
|
||||
aspectRatio: "16:9",
|
||||
audio: false,
|
||||
mode: "single",
|
||||
taskClass: "测试视频生成",
|
||||
name: "测试视频生成",
|
||||
describe: "测试视频生成",
|
||||
projectId: 0,
|
||||
},
|
||||
{
|
||||
model: modelName,
|
||||
apiKey,
|
||||
baseURL,
|
||||
manufacturer,
|
||||
},
|
||||
);
|
||||
const url = await u.oss.getFileUrl(videoPath);
|
||||
res.status(200).send(success(url));
|
||||
} catch (err: any) {
|
||||
const msg = u.error(err).message;
|
||||
console.error(msg);
|
||||
res.status(500).send(error(msg));
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -1,25 +0,0 @@
|
||||
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({
|
||||
projectId: z.number(),
|
||||
data: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId, data } = req.body;
|
||||
|
||||
await u.db("t_outline").insert({
|
||||
data,
|
||||
projectId,
|
||||
});
|
||||
|
||||
res.status(200).send(success({ message: "新增大纲成功" }));
|
||||
}
|
||||
);
|
||||
@ -1,149 +0,0 @@
|
||||
import express from "express";
|
||||
import expressWs, { Application } from "express-ws";
|
||||
import u from "@/utils";
|
||||
import OutlineScript from "@/agents/outlineScript";
|
||||
const router = express.Router();
|
||||
expressWs(router as unknown as Application);
|
||||
|
||||
router.ws("/", async (ws, req) => {
|
||||
let agent: OutlineScript;
|
||||
|
||||
|
||||
const projectId = req.query.projectId;
|
||||
if (!projectId || typeof projectId !== "string") {
|
||||
ws.send(JSON.stringify({ type: "error", data: "项目ID缺失" }));
|
||||
ws.close(500, "项目ID缺失");
|
||||
return;
|
||||
}
|
||||
|
||||
agent = new OutlineScript(Number(projectId));
|
||||
|
||||
// const existing = await u
|
||||
// .db("t_chatHistory")
|
||||
// .where({ projectId: Number(projectId) })
|
||||
// .first();
|
||||
// if (existing) {
|
||||
// try {
|
||||
// const historyData = JSON.parse(existing.data!);
|
||||
// agent.history = [];
|
||||
// agent.novelChapters = existing.novel ? JSON.parse(existing.novel) : [];
|
||||
// } catch (error) {
|
||||
// ws.send(JSON.stringify({ type: "error", data: "历史记录解析异常,将清空历史记录" }));
|
||||
// agent.history = [];
|
||||
// }
|
||||
// }
|
||||
// 监听各类事件
|
||||
// 流式传输:每个token
|
||||
agent.emitter.on("data", (text) => {
|
||||
ws.send(JSON.stringify({ type: "stream", data: text }));
|
||||
});
|
||||
|
||||
// 完整响应结束
|
||||
agent.emitter.on("response", async (text) => {
|
||||
ws.send(JSON.stringify({ type: "response_end", data: text }));
|
||||
await saveHistory();
|
||||
});
|
||||
|
||||
// Sub-Agent 流式数据
|
||||
agent.emitter.on("subAgentStream", (data) => {
|
||||
ws.send(JSON.stringify({ type: "subAgentStream", data }));
|
||||
});
|
||||
|
||||
// Sub-Agent 结束
|
||||
agent.emitter.on("subAgentEnd", (data) => {
|
||||
ws.send(JSON.stringify({ type: "subAgentEnd", data }));
|
||||
});
|
||||
|
||||
// Tool 调用
|
||||
agent.emitter.on("toolCall", (data) => {
|
||||
ws.send(JSON.stringify({ type: "toolCall", data }));
|
||||
});
|
||||
|
||||
agent.emitter.on("transfer", (data) => {
|
||||
ws.send(JSON.stringify({ type: "transfer", data }));
|
||||
});
|
||||
|
||||
agent.emitter.on("refresh", (data) => {
|
||||
ws.send(JSON.stringify({ type: "refresh", data }));
|
||||
});
|
||||
|
||||
agent.emitter.on("error", (err) => {
|
||||
ws.send(JSON.stringify({ type: "error", data: err.toString() }));
|
||||
});
|
||||
|
||||
// 发送初始化完成消息,通知前端可以开始发送消息
|
||||
ws.send(JSON.stringify({ type: "init", data: { projectId } }));
|
||||
|
||||
type DataTyype = "msg" | "setNovel" | "cleanHistory";
|
||||
ws.on("message", async function (rawData: string) {
|
||||
let data: { type: DataTyype; data: any } | null = null;
|
||||
try {
|
||||
data = JSON.parse(rawData);
|
||||
} catch (error) {
|
||||
ws.send(JSON.stringify({ type: "error", data: "数据解析异常" }));
|
||||
ws.close(500, "数据解析异常");
|
||||
return;
|
||||
}
|
||||
if (!data) {
|
||||
ws.send(JSON.stringify({ type: "error", data: "数据格式错误" }));
|
||||
ws.close(500, "数据格式错误");
|
||||
return;
|
||||
}
|
||||
const novelData = await u
|
||||
.db("t_novel")
|
||||
.where({ projectId: Number(projectId) })
|
||||
.orderBy("chapterIndex", "asc");
|
||||
agent.setNovel(novelData);
|
||||
const msg = data.data;
|
||||
try {
|
||||
switch (data?.type) {
|
||||
case "msg":
|
||||
let prompt = msg.data;
|
||||
if (msg.type == "user") await agent.call(prompt);
|
||||
break;
|
||||
case "cleanHistory":
|
||||
agent.history = [];
|
||||
await u
|
||||
.db("t_chatHistory")
|
||||
.where({ projectId: Number(projectId) })
|
||||
.del();
|
||||
ws.send(JSON.stringify({ type: "notice", data: "历史记录已清空" }));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
ws.send(JSON.stringify({ type: "error", data: "数据解析/脚本生成异常" }));
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on("close", async () => {
|
||||
agent?.emitter?.removeAllListeners();
|
||||
await saveHistory();
|
||||
});
|
||||
|
||||
async function saveHistory() {
|
||||
const history = agent?.history || [];
|
||||
//保存对话记录
|
||||
const existing = await u
|
||||
.db("t_chatHistory")
|
||||
.where({ projectId: Number(projectId), type: "outlineAgent" })
|
||||
.first();
|
||||
if (existing) {
|
||||
await u
|
||||
.db("t_chatHistory")
|
||||
.where({ projectId: Number(projectId), type: "outlineAgent" })
|
||||
.update({ data: JSON.stringify(history), novel: agent?.novelChapters ? JSON.stringify(agent.novelChapters) : "" });
|
||||
} else {
|
||||
await u.db("t_chatHistory").insert({
|
||||
projectId: Number(projectId),
|
||||
data: JSON.stringify(history),
|
||||
novel: agent?.novelChapters ? JSON.stringify(agent.novelChapters) : "",
|
||||
type: "outlineAgent",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@ -1,22 +0,0 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { z } from "zod";
|
||||
import { error, success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
const router = express.Router();
|
||||
|
||||
// 删除大纲
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
id: z.number(),
|
||||
projectId: z.number(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id, projectId } = req.body;
|
||||
|
||||
await u.deleteOutline(id, projectId);
|
||||
|
||||
res.status(200).send(success({ message: "删除大纲成功" }));
|
||||
}
|
||||
);
|
||||
@ -1,31 +0,0 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { z } from "zod";
|
||||
import { error, success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
const router = express.Router();
|
||||
|
||||
// 获取历史消息记录
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
projectId: z.number(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId } = req.body;
|
||||
|
||||
const history = await u
|
||||
.db("t_chatHistory")
|
||||
.where({ projectId: Number(projectId), type: "outlineWebChat" })
|
||||
.first();
|
||||
if (!history) {
|
||||
await u.db("t_chatHistory").insert({
|
||||
projectId: Number(projectId),
|
||||
type: "outlineWebChat",
|
||||
data: "[]",
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send(success({ data: JSON.parse(history?.data || "[]") }));
|
||||
},
|
||||
);
|
||||
@ -1,21 +0,0 @@
|
||||
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({
|
||||
projectId: z.number(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId } = req.body;
|
||||
|
||||
const data = await u.db("t_outline").where("projectId", projectId).select("*");
|
||||
|
||||
res.status(200).send(success(data));
|
||||
}
|
||||
);
|
||||
@ -1,21 +0,0 @@
|
||||
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({
|
||||
projectId: z.number(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId } = req.body;
|
||||
|
||||
const data = await u.db("t_script").where("projectId", projectId).select("*");
|
||||
|
||||
res.status(200).send(success(data));
|
||||
}
|
||||
);
|
||||
@ -1,19 +0,0 @@
|
||||
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({
|
||||
projectId: z.number(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId } = req.body;
|
||||
const data = await u.db("t_storyline").where("projectId", projectId).first();
|
||||
res.status(200).send(success(data));
|
||||
}
|
||||
);
|
||||
@ -1,39 +0,0 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { z } from "zod";
|
||||
import { error, success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
const router = express.Router();
|
||||
|
||||
// 保存历史消息记录
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
projectId: z.number(),
|
||||
data: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId, data } = req.body;
|
||||
|
||||
const history = await u
|
||||
.db("t_chatHistory")
|
||||
.where({ projectId: Number(projectId), type: "outlineWebChat" })
|
||||
.first();
|
||||
if (!history) {
|
||||
await u.db("t_chatHistory").insert({
|
||||
projectId: Number(projectId),
|
||||
type: "outlineWebChat",
|
||||
data: data,
|
||||
});
|
||||
} else {
|
||||
await u
|
||||
.db("t_chatHistory")
|
||||
.where({ projectId: Number(projectId), type: "outlineWebChat" })
|
||||
.update({
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send(success("保存成功"));
|
||||
},
|
||||
);
|
||||
@ -1,24 +0,0 @@
|
||||
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(),
|
||||
data: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id, data } = req.body;
|
||||
|
||||
await u.db("t_outline").where("id", id).update({
|
||||
data,
|
||||
});
|
||||
|
||||
res.status(200).send(success({ message: "更新大纲成功" }));
|
||||
}
|
||||
);
|
||||
@ -1,27 +0,0 @@
|
||||
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({
|
||||
projectId: z.number(),
|
||||
content: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId, content } = req.body;
|
||||
|
||||
const existing = await u.db("t_storyline").where({ projectId }).first();
|
||||
if (existing) {
|
||||
await u.db("t_storyline").where({ projectId }).update({ content });
|
||||
} else {
|
||||
await u.db("t_storyline").insert({ projectId: projectId, content: content });
|
||||
}
|
||||
|
||||
res.status(200).send(success({ message: "更新故事线成功" }));
|
||||
}
|
||||
);
|
||||
18
src/routes/production/getProductionData.ts
Normal file
18
src/routes/production/getProductionData.ts
Normal 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({
|
||||
projectId: z.number(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId } = req.body;
|
||||
res.status(200).send(success("123"));
|
||||
}
|
||||
);
|
||||
@ -9,7 +9,7 @@ const router = express.Router();
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
projectType: z.string().optional(),
|
||||
projectType: z.string(),
|
||||
name: z.string(),
|
||||
intro: z.string(),
|
||||
type: z.string(),
|
||||
@ -19,7 +19,8 @@ export default router.post(
|
||||
async (req, res) => {
|
||||
const { projectType, name, intro, type, artStyle, videoRatio } = req.body;
|
||||
|
||||
await u.db("t_project").insert({
|
||||
await u.db("o_project").insert({
|
||||
projectType,
|
||||
name,
|
||||
intro,
|
||||
type,
|
||||
|
||||
@ -14,25 +14,24 @@ export default router.post(
|
||||
async (req, res) => {
|
||||
const { id } = req.body;
|
||||
|
||||
const scriptData = await u.db("t_script").where("projectId", id).select("id");
|
||||
const scriptData = await u.db("o_script").where("projectId", id).select("id");
|
||||
const scriptIds = scriptData.map((item: any) => item.id);
|
||||
|
||||
const assetsData = await u.db("t_assets").where("projectId", id).select("id");
|
||||
const assetsData = await u.db("o_assets").where("projectId", id).select("id");
|
||||
const assetsIds = assetsData.map((item: any) => item.id);
|
||||
|
||||
const videoData = await u.db("t_video").whereIn("scriptId", scriptIds).select("id");
|
||||
const videoData = await u.db("o_video").whereIn("scriptId", scriptIds).select("id");
|
||||
const videoIds = videoData.map((item: any) => item.id);
|
||||
|
||||
await u.db("t_project").where("id", id).delete();
|
||||
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("o_project").where("id", id).delete();
|
||||
await u.db("o_novel").where("projectId", id).delete();
|
||||
await u.db("o_outline").where("projectId", id).delete();
|
||||
await u.db("o_myTasks").where("projectId", id).delete();
|
||||
|
||||
await u.db("t_script").where("projectId", id).delete();
|
||||
await u.db("t_assets").where("projectId", id).delete();
|
||||
await u.db("o_script").where("projectId", id).delete();
|
||||
await u.db("o_assets").where("projectId", id).delete();
|
||||
|
||||
const tempAssetsQuery = u.db("t_image").where("projectId", id);
|
||||
const tempAssetsQuery = u.db("o_image").where("projectId", id);
|
||||
if (assetsIds.length > 0) {
|
||||
tempAssetsQuery.orWhereIn("assetsId", assetsIds);
|
||||
}
|
||||
@ -44,9 +43,9 @@ export default router.post(
|
||||
}
|
||||
await tempAssetsQuery.delete();
|
||||
|
||||
await u.db("t_video").whereIn("scriptId", scriptIds).delete();
|
||||
await u.db("o_video").whereIn("scriptId", scriptIds).delete();
|
||||
|
||||
await u.db("t_chatHistory").where("projectId", id).delete();
|
||||
await u.db("o_chatHistory").where("projectId", id).delete();
|
||||
|
||||
try {
|
||||
await u.oss.deleteDirectory(`${id}/`);
|
||||
|
||||
32
src/routes/project/editProject.ts
Normal file
32
src/routes/project/editProject.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(),
|
||||
name: z.string(),
|
||||
intro: z.string(),
|
||||
type: z.string(),
|
||||
artStyle: z.string(),
|
||||
videoRatio: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id, name, intro, type, artStyle, videoRatio } = req.body;
|
||||
|
||||
await u.db("o_project").where("id", id).update({
|
||||
name,
|
||||
intro,
|
||||
type,
|
||||
artStyle,
|
||||
videoRatio,
|
||||
});
|
||||
|
||||
res.status(200).send(success({ message: "新增项目成功" }));
|
||||
},
|
||||
);
|
||||
@ -1,12 +1,10 @@
|
||||
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("/", async (req, res) => {
|
||||
const data = await u.db("t_project").select("*");
|
||||
const data = await u.db("o_project").select("*");
|
||||
res.status(200).send(success(data));
|
||||
});
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
import { z } from "zod";
|
||||
import { v4 as uuid } from "uuid";
|
||||
const router = express.Router();
|
||||
|
||||
// 获取提示词
|
||||
export default router.get("/", async (req, res) => {
|
||||
const data = await u.db("t_prompts");
|
||||
res.status(200).send(success(data));
|
||||
});
|
||||
@ -1,28 +0,0 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
import { z } from "zod";
|
||||
const router = express.Router();
|
||||
|
||||
// 更新提示词
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
id: z.number(),
|
||||
customValue: z.string(),
|
||||
code: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id, customValue, code } = req.body;
|
||||
|
||||
await u
|
||||
.db("t_prompts")
|
||||
.update({
|
||||
customValue: customValue,
|
||||
})
|
||||
.where("id", id);
|
||||
|
||||
res.status(200).send(success({ message: "更新提示词成功" }));
|
||||
},
|
||||
);
|
||||
26
src/routes/script/addScript.ts
Normal file
26
src/routes/script/addScript.ts
Normal file
@ -0,0 +1,26 @@
|
||||
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(),
|
||||
content: z.string(),
|
||||
projectId: z.number(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { name, content, projectId } = req.body;
|
||||
await u.db("o_script").insert({
|
||||
name,
|
||||
content,
|
||||
projectId,
|
||||
createTime: Date.now(),
|
||||
});
|
||||
res.status(200).send(success({ message: "添加剧本成功" }));
|
||||
},
|
||||
);
|
||||
19
src/routes/script/delScript.ts
Normal file
19
src/routes/script/delScript.ts
Normal 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({
|
||||
id: z.number(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id } = req.body;
|
||||
await u.db("o_script").where({ id }).delete();
|
||||
res.status(200).send(success({ message: "删除剧本成功" }));
|
||||
},
|
||||
);
|
||||
@ -1,72 +0,0 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
import { z } from "zod";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
const router = express.Router();
|
||||
|
||||
interface Asset {
|
||||
id: number;
|
||||
type: string; // "角色" 或其他
|
||||
name: string;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
interface ScriptRow {
|
||||
id: number;
|
||||
name: string;
|
||||
content: string;
|
||||
outlineId: number;
|
||||
projectId: number;
|
||||
data: string;
|
||||
}
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
projectId: z.number(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId } = req.body;
|
||||
|
||||
//查询剧本和大纲数据
|
||||
const rows: ScriptRow[] = await u
|
||||
.db("t_outline")
|
||||
.leftJoin("t_script", "t_outline.id", "t_script.outlineId")
|
||||
.where("t_outline.projectId", projectId)
|
||||
.select("t_script.id", "t_script.name", "t_script.content", "t_script.outlineId", "t_script.projectId", "t_outline.data");
|
||||
|
||||
// 查询所有的资产
|
||||
const assets: Asset[] = await u
|
||||
.db("t_assets")
|
||||
.where("projectId", projectId)
|
||||
.andWhere("type", "<>", "分镜")
|
||||
.select("id", "type", "name", "filePath", "intro", "prompt");
|
||||
|
||||
const data = rows.map((item) => {
|
||||
const parseData = JSON.parse(item.data);
|
||||
const charData = parseData.characters.map((i: Asset) => i.name);
|
||||
const propsData = parseData.props.map((i: Asset) => i.name);
|
||||
const sceneData = parseData.scenes.map((i: Asset) => i.name);
|
||||
return {
|
||||
...item,
|
||||
element: [
|
||||
...assets.filter((i) => i.type == "道具" && propsData.includes(i.name)),
|
||||
...assets.filter((i) => i.type == "角色" && charData.includes(i.name)),
|
||||
...assets.filter((i) => i.type == "场景" && sceneData.includes(i.name)),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
data.map(async (script) => {
|
||||
await Promise.all(
|
||||
script.element.map(async (el) => {
|
||||
el.filePath = el.filePath ? await u.oss.getFileUrl(el.filePath) : "";
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
res.status(200).send(success(data));
|
||||
}
|
||||
);
|
||||
@ -1,60 +0,0 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { z } from "zod";
|
||||
import { error, success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
import { generateScript } from "@/utils/generateScript";
|
||||
const router = express.Router();
|
||||
interface NovelChapter {
|
||||
id: number;
|
||||
reel: string;
|
||||
chapter: string;
|
||||
chapterData: string;
|
||||
projectId: number;
|
||||
}
|
||||
function mergeNovelText(novelData: NovelChapter[]): string {
|
||||
if (!Array.isArray(novelData)) return "";
|
||||
return novelData
|
||||
.map((chap) => {
|
||||
return `${chap.chapter.trim()}\n\n${chap.chapterData.trim().replace(/\r?\n/g, "\n")}\n`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
// 生成剧本
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
outlineId: z.number(),
|
||||
scriptId: z.number(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { outlineId, scriptId } = req.body;
|
||||
const outlineData = await u.db("t_outline").where("id", outlineId).select("*").first();
|
||||
if (!outlineData) return res.status(500).send(success({ message: "大纲为空" }));
|
||||
const parameter = JSON.parse(outlineData.data!);
|
||||
|
||||
const novelData = (await u
|
||||
.db("t_novel")
|
||||
.whereIn("chapterIndex", parameter.chapterRange)
|
||||
.where("projectId", outlineData.projectId)
|
||||
.select("*")) as NovelChapter[];
|
||||
|
||||
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: "生成剧本失败" });
|
||||
|
||||
await u.db("t_script").where("id", scriptId).update({
|
||||
content: data,
|
||||
});
|
||||
|
||||
res.status(200).send(success({ message: "生成剧本成功" }));
|
||||
} catch (e) {
|
||||
const errMsg = u.error(e).message || "生成剧本失败";
|
||||
res.status(500).send(error(errMsg));
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -1,26 +0,0 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { z } from "zod";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
import { generateScript } from "@/utils/generateScript";
|
||||
const router = express.Router();
|
||||
|
||||
// 生成剧本
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
outlineId: z.number(),
|
||||
scriptId: z.number(),
|
||||
content: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { outlineId, scriptId, content } = req.body;
|
||||
|
||||
await u.db("t_script").where("id", scriptId).update({
|
||||
content: content,
|
||||
});
|
||||
|
||||
res.status(200).send(success({ message: "保存成功" }));
|
||||
},
|
||||
);
|
||||
23
src/routes/script/getScrptApi.ts
Normal file
23
src/routes/script/getScrptApi.ts
Normal file
@ -0,0 +1,23 @@
|
||||
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({
|
||||
projectId: z.number(),
|
||||
name: z.string().optional(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId, name } = req.body;
|
||||
let query = u.db("o_script").where("projectId", projectId).select("*");
|
||||
if (name) {
|
||||
query = query.andWhere("name", "like", `%${name}%`);
|
||||
}
|
||||
const data = await query;
|
||||
res.status(200).send(success(data));
|
||||
},
|
||||
);
|
||||
24
src/routes/script/updateScript.ts
Normal file
24
src/routes/script/updateScript.ts
Normal 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({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
content: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id, name, content } = req.body;
|
||||
await u.db("o_script").where({ id }).update({
|
||||
name,
|
||||
content,
|
||||
});
|
||||
res.status(200).send(success({ message: "编辑剧本成功" }));
|
||||
},
|
||||
);
|
||||
@ -1,33 +0,0 @@
|
||||
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", "video", "image"]),
|
||||
model: z.string(),
|
||||
baseUrl: z.string(),
|
||||
apiKey: z.string(),
|
||||
modelType: z.string(),
|
||||
manufacturer: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { type, model, baseUrl, apiKey, manufacturer, modelType } = req.body;
|
||||
|
||||
await u.db("t_config").insert({
|
||||
type,
|
||||
model,
|
||||
baseUrl,
|
||||
apiKey,
|
||||
manufacturer,
|
||||
modelType,
|
||||
createTime: Date.now(),
|
||||
userId: 1,
|
||||
});
|
||||
res.status(200).send(success("新增成功"));
|
||||
},
|
||||
);
|
||||
19
src/routes/setting/agentDeploy/deployAgentModel.ts
Normal file
19
src/routes/setting/agentDeploy/deployAgentModel.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import express from "express";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
import u from "@/utils";
|
||||
import { z } from "zod";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
const router = express.Router();
|
||||
|
||||
export default router.post("/", validateFields({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
model: z.string(),
|
||||
modelName: z.string(),
|
||||
vendorId: z.number().nullable(),
|
||||
desc: z.string(),
|
||||
}), async (req, res) => {
|
||||
const { id, name, model, modelName, vendorId, desc } = req.body;
|
||||
await u.db("o_agentDeploy").where({ id }).update({ id, name, model, modelName, vendorId, desc });
|
||||
res.status(200).send(success("配置成功"));
|
||||
});
|
||||
9
src/routes/setting/agentDeploy/getAgentDeploy.ts
Normal file
9
src/routes/setting/agentDeploy/getAgentDeploy.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import express from "express";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
import u from "@/utils";
|
||||
const router = express.Router();
|
||||
|
||||
export default router.post("/", async (req, res) => {
|
||||
const data = await u.db("o_agentDeploy").leftJoin("o_vendorConfig", "o_vendorConfig.id", "o_agentDeploy.vendorId").select("o_agentDeploy.*", "o_vendorConfig.icon");
|
||||
res.status(200).send(success(data));
|
||||
});
|
||||
116
src/routes/setting/agentDeploy/updateKey.ts
Normal file
116
src/routes/setting/agentDeploy/updateKey.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import express from "express";
|
||||
import { success, error } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
import u from "@/utils";
|
||||
import { z } from "zod";
|
||||
import { transform } from "sucrase";
|
||||
const router = express.Router();
|
||||
const vendorConfigSchema = z.object({
|
||||
version: z.number(),
|
||||
icon: z.string().optional(),
|
||||
name: z.string(),
|
||||
inputs: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
label: z.string(),
|
||||
type: z.enum(["text", "password", "url"]),
|
||||
required: z.boolean(),
|
||||
placeholder: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
inputValues: z.record(z.string(), z.string()),
|
||||
models: z.array(
|
||||
z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
name: z.string(),
|
||||
modelName: z.string(),
|
||||
type: z.literal("text"),
|
||||
multimodal: z.boolean(),
|
||||
tool: z.boolean(),
|
||||
}),
|
||||
z.object({
|
||||
name: z.string(),
|
||||
modelName: z.string(),
|
||||
type: z.literal("image"),
|
||||
mode: z.array(z.enum(["text", "singleImage", "multiReference"])),
|
||||
}),
|
||||
z.object({
|
||||
name: z.string(),
|
||||
modelName: z.string(),
|
||||
type: z.literal("video"),
|
||||
mode: z.array(
|
||||
z.enum([
|
||||
"singleImage",
|
||||
"multiImage",
|
||||
"gridImage",
|
||||
"startEndRequired",
|
||||
"endFrameOptional",
|
||||
"startFrameOptional",
|
||||
"text",
|
||||
"audioReference",
|
||||
"videoReference",
|
||||
]),
|
||||
),
|
||||
audio: z.union([z.literal("optional"), z.boolean()]),
|
||||
durationResolutionMap: z.array(
|
||||
z.object({
|
||||
duration: z.array(z.number()),
|
||||
resolution: z.array(z.string()),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
]),
|
||||
),
|
||||
});
|
||||
export default router.post("/",
|
||||
validateFields({
|
||||
id: z.number(),
|
||||
key: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id, key } = req.body;
|
||||
const data = await u.db("o_vendorConfig").where("id", id).select("code", "inputValues").first();
|
||||
let inputValues = JSON.parse(data?.inputValues || "{}");
|
||||
inputValues.apiKey = key;
|
||||
const jsCode = transform(data?.code!, { transforms: ["typescript"] }).code;
|
||||
const exports = u.vm(jsCode);
|
||||
if (!exports) return res.status(400).send(success("脚本文件必须导出对象"));
|
||||
if (!exports.textRequest) return res.status(400).send(success("脚本文件必须导出文本请求对象"));
|
||||
if (!exports.imageRequest) return res.status(400).send(success("脚本文件必须导出图像请求对象"));
|
||||
if (!exports.videoRequest) return res.status(400).send(success("脚本文件必须导出视频请求对象"));
|
||||
if (!exports.vendor) return res.status(400).send(success("脚本文件必须导出vendor对象"));
|
||||
const vendor = exports.vendor;
|
||||
const result = vendorConfigSchema.safeParse(vendor);
|
||||
if (!result.success) {
|
||||
const errorMsg = result.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
|
||||
return res.status(400).send(error(`vendor配置校验失败: ${errorMsg}`));
|
||||
}
|
||||
const replaceBlockValue = (code: string, key: string, newValue: string): string => {
|
||||
const open = newValue.trimStart()[0] as "[" | "{";
|
||||
const close = open === "[" ? "]" : "}";
|
||||
const keyMatch = code.match(new RegExp(`\\b${key}\\s*:\\s*[\\[{]`));
|
||||
if (!keyMatch || keyMatch.index === undefined) return code;
|
||||
const valueStart = keyMatch.index + keyMatch[0].length - 1;
|
||||
let depth = 0;
|
||||
let valueEnd = -1;
|
||||
for (let i = valueStart; i < code.length; i++) {
|
||||
if (code[i] === open) depth++;
|
||||
else if (code[i] === close) {
|
||||
depth--;
|
||||
if (depth === 0) {
|
||||
valueEnd = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (valueEnd === -1) return code;
|
||||
return code.slice(0, valueStart) + newValue + code.slice(valueEnd + 1);
|
||||
};
|
||||
let updatedTsCode = data?.code!;
|
||||
updatedTsCode = replaceBlockValue(updatedTsCode, "inputValues", JSON.stringify(inputValues ?? vendor.inputValues, null, 2));
|
||||
await u.db("o_vendorConfig").where("id", id).update({
|
||||
inputValues: inputValues ? JSON.stringify(inputValues) : JSON.stringify(vendor.inputValues),
|
||||
code: updatedTsCode,
|
||||
});
|
||||
res.status(200).send(success("保存成功"));
|
||||
});
|
||||
@ -1,23 +0,0 @@
|
||||
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(),
|
||||
configId: z.number(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id, configId } = req.body;
|
||||
if (id) {
|
||||
await u.db("t_aiModelMap").where("id", id).update({
|
||||
configId,
|
||||
});
|
||||
}
|
||||
res.status(200).send(success("配置成功"));
|
||||
},
|
||||
);
|
||||
29
src/routes/setting/dbConfig/clearData.ts
Normal file
29
src/routes/setting/dbConfig/clearData.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import express from "express";
|
||||
import { success, error } from "@/lib/responseFormat";
|
||||
import { db } from "@/utils/db";
|
||||
import initDB from "@/lib/initDB";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
export default router.get("/", async (req, res) => {
|
||||
try {
|
||||
// 获取所有表名
|
||||
const tables: { name: string }[] = await db.raw(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE 'knex_%'`,
|
||||
);
|
||||
|
||||
// 禁用外键约束,逐一删除所有表
|
||||
await db.raw("PRAGMA foreign_keys = OFF");
|
||||
for (const table of tables) {
|
||||
await db.schema.dropTableIfExists(table.name);
|
||||
}
|
||||
await db.raw("PRAGMA foreign_keys = ON");
|
||||
|
||||
// 重新初始化数据库
|
||||
await initDB(db as any);
|
||||
|
||||
res.status(200).send(success("数据库已清空并重新初始化"));
|
||||
} catch (err: any) {
|
||||
res.status(500).send(error(err?.message || "清除失败"));
|
||||
}
|
||||
});
|
||||
@ -1,19 +0,0 @@
|
||||
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_config").where("id", id).delete();
|
||||
await u.db("t_aiModelMap").where("configId", id).update("configId",null);
|
||||
res.status(200).send(success("删除成功"));
|
||||
},
|
||||
);
|
||||
@ -1,42 +0,0 @@
|
||||
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[]> = {};
|
||||
const modelCache: Record<string, Set<string>> = {};
|
||||
|
||||
for (const row of modelLists) {
|
||||
if (!result[row.manufacturer]) {
|
||||
result[row.manufacturer] = [];
|
||||
modelCache[row.manufacturer] = new Set();
|
||||
}
|
||||
if (!modelCache[row.manufacturer].has(row.model)) {
|
||||
result[row.manufacturer].push({ label: row.model, value: row.model });
|
||||
modelCache[row.manufacturer].add(row.model);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).send(success(result));
|
||||
},
|
||||
);
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
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 configData = await u
|
||||
.db("t_aiModelMap")
|
||||
.leftJoin("t_config", "t_aiModelMap.configId", "t_config.id")
|
||||
.select("t_aiModelMap.name", "t_config.model", "t_aiModelMap.id", "t_aiModelMap.key", "t_config.manufacturer");
|
||||
res.status(200).send(success(configData));
|
||||
});
|
||||
@ -1,17 +0,0 @@
|
||||
import logger from "@/logger";
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { z } from "zod";
|
||||
import { success, error } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
const router = express.Router();
|
||||
|
||||
export default router.post("/", async (req, res) => {
|
||||
const { id } = (req as any).user;
|
||||
|
||||
if (id !== 1) return res.status(400).send(error("无权限查看,仅管理员USERID=1可见"));
|
||||
|
||||
const logs = logger.exportLogs();
|
||||
|
||||
res.status(200).send(success(logs));
|
||||
});
|
||||
@ -1,11 +0,0 @@
|
||||
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));
|
||||
});
|
||||
10
src/routes/setting/getTextModel.ts
Normal file
10
src/routes/setting/getTextModel.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import express from "express";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
const router = express.Router();
|
||||
|
||||
export default router.post(
|
||||
"/",
|
||||
async (req, res) => {
|
||||
res.status(200).send(success("123"));
|
||||
},
|
||||
);
|
||||
@ -1,31 +0,0 @@
|
||||
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));
|
||||
});
|
||||
@ -1,11 +0,0 @@
|
||||
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));
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user