Merge branch '108' of https://github.com/HBAI-Ltd/Toonflow-app into 108
@ -1,3 +1,6 @@
|
||||
123水电费水电费
|
||||
123
|
||||
123
|
||||
123
|
||||
1212121212的王师傅水电费第三方水电费
|
||||
1212121212
|
||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 272 KiB After Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 272 KiB After Width: | Height: | Size: 272 KiB |
2
data/skills/story_skills/director_manual/README.md
Normal file
@ -0,0 +1,2 @@
|
||||
123实打实地方
|
||||
123
|
||||
@ -0,0 +1,110 @@
|
||||
---
|
||||
name: narrative_sweet_romance
|
||||
description: 叙事手法技法 · 甜宠言情 — 定义甜宠言情类型在主题立意、情感节奏、场景情绪设计与声音方向上的叙事规划方法。适用于任何视觉风格。
|
||||
metaData: director_skills
|
||||
---
|
||||
|
||||
# 叙事手法 · 甜宠言情 · 技法参考
|
||||
|
||||
---
|
||||
|
||||
## 一、主题立意与情感内核
|
||||
|
||||
### 甜宠言情叙事要点
|
||||
|
||||
- **含蓄内敛优先** — 情感表达不靠台词铺陈,靠留白与微妙反应。主题立意应偏向克制含蓄,避免直白煽情
|
||||
- **甜的克制** — "差一点就碰到"比"黏在一起"更有效。情感主线应设计"欲说还休"的推拉节奏,甜度来自观众自行脑补
|
||||
- **以小博大** — 不追求大场面的情绪冲击,用细节打动人:一个眼神、一次欲言又止、一个被风吹乱的衣角
|
||||
- **离场感受建议方向** — 心疼 / 意难平 / 怦然心动 / 治愈。避免"爽感""热血"等与甜宠气质不匹配的方向
|
||||
- **冷中带暖、疏中见密** — 甜宠不等于甜腻。整体基调可以偏冷、偏疏,但在关键节点释放暖意,反差才是最大的甜
|
||||
|
||||
---
|
||||
|
||||
## 二、叙事结构与节奏规划
|
||||
|
||||
### 甜宠言情叙事要点
|
||||
|
||||
- **慢是基本功** — 甜宠言情的情感信息密度高(眼神、微表情、肢体距离),需要给观众"感受"的时间。整体节奏偏慢,但不等于拖沓——每个段落都有情感增量
|
||||
- **情绪曲线宜缓坡** — 避免"平平平→突然爆发"。用渐进式情绪递进,每个段落比上一个段落情绪浓度高一级
|
||||
- **转折点用行动而非台词** — 关键转折点的处理方式应优先考虑行动手段(目光突变、身体距离变化、沉默、道具传递),而非依赖对白解释
|
||||
- **段落间用情绪缓冲过渡** — 段落衔接需要情绪缓冲,不要硬切。可用环境空镜、独处片段或日常碎片做呼吸空间
|
||||
- **高潮段落的"快"不是剪辑快** — 是情绪密度高。可以用更紧密的景别切换(全身→近景→特写→大特写)制造心跳加速感,而非缩短停留时间
|
||||
- **推拉节奏模型** — 甜宠言情的核心引擎是"推拉":靠近→退缩→再靠近→误会→分离→重逢。每一轮推拉都应比上一轮更深入、更痛苦、更甜蜜
|
||||
|
||||
---
|
||||
|
||||
## 三、分场景情绪设计
|
||||
|
||||
### 甜宠言情叙事要点
|
||||
|
||||
- **情绪目标用具象词** — 不说"开心",说"偷偷心动后的嘴角压不住"。具象的情绪描述能更好地指导景别选择和表演细节
|
||||
- **典型情绪段落与设计** —
|
||||
|
||||
| 段落类型 | 情绪方向 | 叙事手法 | 音乐建议 |
|
||||
|---|---|---|---|
|
||||
| 初见/亮相 | 惊艳 + 好奇 | 以旁观者视角"发现"对方,先远后近 | 留白,只用环境音制造"屏息"感 |
|
||||
| 日常暗恋 | 暗涌 + 克制 | 偷看、欲言又止、刻意保持距离 | 轻柔器乐,低音量,衬底 |
|
||||
| 误会/分离 | 心痛 + 隐忍 | 不解释、转身、独处落泪 | 悲戚独奏,或纯环境音 |
|
||||
| 坦白/和解 | 释然 + 心动 | 沉默后开口、眼神先于语言 | 从安静到温暖器乐渐入 |
|
||||
| 升温/暧昧 | 紧张 + 甜蜜 | 物理距离缩短、肢体轻触、呼吸可闻 | 节奏感轻起,暗示心跳 |
|
||||
| 高甜/大婚 | 幸福 + 庄重 | 仪式感、郑重的对视、承诺 | 丰满器乐,庄重但温柔 |
|
||||
|
||||
- **"距离感"是叙事核心工具** — 用人物间的物理距离映射关系进展:
|
||||
- **初期**:远景/半身,物理距离大,言语客套
|
||||
- **中期**:近景,距离缩短但有阻隔(物件/人群/犹豫)
|
||||
- **后期**:特写/大特写,零距离,心理防线全部放下
|
||||
- **空间元素即情绪隐喻** — 善用场景元素传递情绪,减少对台词的依赖。例如:隔着帘子的模糊身影 = 隔阂;推开门看到满庭花开 = 释然;独坐雨中 = 孤寂
|
||||
- **镜头意图写"为什么"而非"怎么拍"** — "用特写是为了让观众看到她眼里的犹豫"优于"用特写拍她的脸"。意图清晰了,分镜自然能选对景别和角度
|
||||
|
||||
---
|
||||
|
||||
## 四、声音与音乐方向
|
||||
|
||||
### 甜宠言情叙事要点
|
||||
|
||||
- **沉默比配乐更有力** — 关键情感瞬间(对视、泪落、转身离去)优先考虑去掉配乐,只留环境音。甜宠的"甜"往往在沉默后观众自己脑补出来
|
||||
- **配乐情绪跟着段落走** — 不逐场配乐,按段落划分给每段定一个音乐情绪基调。同段落内场景切换靠环境音变化过渡,不频繁换曲
|
||||
- **避免满配** — 全片配乐覆盖率建议不超过 60%。留白段落的"无声"与配乐段落形成呼吸感
|
||||
- **环境音是氛围一半** — 每场戏标注 1-2 个核心环境音,帮助后续音效设计。环境音层次越丰富,场景越有沉浸感
|
||||
- **音乐情绪递进模型** —
|
||||
|
||||
| 情绪阶段 | 音乐策略 | 覆盖率 |
|
||||
|---|---|---|
|
||||
| 平稳/日常 | 轻柔器乐衬底 | 低 |
|
||||
| 暗涌/酝酿 | 单一乐器独奏,极低音量 | 中低 |
|
||||
| 情感爆发 | 器乐渐满或突然静默 | 中高 |
|
||||
| 命运转折 | 强烈器乐或全场静默 | 极端 |
|
||||
| 回暖/治愈 | 温暖器乐缓入 | 中 |
|
||||
|
||||
- **甜宠的"心跳感"** — 暧昧升温段落可用轻节奏打击(手鼓、木鱼、拨弦)暗示心跳加速,比直接用甜蜜旋律更高级
|
||||
|
||||
---
|
||||
|
||||
## 五、构图与景别叙事
|
||||
|
||||
### 甜宠言情叙事要点
|
||||
|
||||
- **三大核心构图的叙事功能** —
|
||||
- **大量留白** — 孤独/意境/诗意空间,传递角色的心理孤立感或情感留白
|
||||
- **框架式构图** — 纱帘/门框/窗棂/屏风后的人影,制造"偷偷看"的暗恋视角与隔阂感
|
||||
- **三分法** — 对话/日常/双人互动,稳定均衡,适合日常甜蜜段落
|
||||
- **中心构图的限定使用** — 中心构图留给正式亮相、仪式感场景(如大婚、正式告白)。日常不用,否则丧失仪式感的冲击力
|
||||
- **空间纵深即叙事** — 前景遮挡(帘/花枝/烟雾)+ 中景主体 + 远景环境,层次越多隔阂感越强;层次越少越亲密
|
||||
- **竖构图与横构图** — 单人特写/亮相偏竖构图(强调孤独感与身形气质);双人/场景偏横构图(强调关系与共处空间)
|
||||
- **甜宠景别递进** — 同场戏内景别应随情感升温递进:半身→近景→特写→大特写。不要一上来就怼特写,留出情绪上升空间
|
||||
- **大特写要有理由** — 大特写(眼/唇/手)是情绪核弹,一集用 2-3 次足够。滥用会让观众疲劳
|
||||
- **远景不是过场** — 远景镜头本身就有叙事价值(孤独感、空间压迫、季节氛围)。给远景足够时长(4-6s),别急着切走
|
||||
|
||||
---
|
||||
|
||||
## 六、镜头运动与节奏
|
||||
|
||||
### 甜宠言情叙事要点
|
||||
|
||||
- **以静制动为主** — 60% 以上镜头应为静止机位,让画面细节和情绪自己说话
|
||||
- **缓推 = 靠近/心动** — "观众靠近角色"的心理暗示,适合心动、发现、窥视
|
||||
- **缓拉 = 抽离/孤独** — "观众退开"的心理暗示,适合离别、孤独、揭示全貌
|
||||
- **快切碎剪不兼容** — 快速剪辑与甜宠言情的气质不兼容。即使在高潮段落,也应通过景别递进而非快切来制造节奏感
|
||||
- **摇镜与跟镜** — 慢摇适合展示场景全貌或追随角色行走;跟镜适合仪式/行走场景。速度均应克制
|
||||
- **运镜即情绪** — 镜头运动不是技术选择,是情绪选择。静止 = 沉稳/压抑;缓推 = 靠近/心动;缓拉 = 抽离/孤独;缓摇 = 展示/庄重
|
||||
- **甜宠"心跳运镜"** — 暧昧升温段落可用微幅缓推配合景别递进(半身→近景→特写),模拟心跳加速时"注意力收窄"的生理感受
|
||||
@ -0,0 +1,85 @@
|
||||
---
|
||||
name: storyboard_table_narrative
|
||||
description: 分镜表叙事手法 · 甜宠言情 — 定义甜宠言情在分镜表中的景别递进、运镜节奏、时长把控、镜头合并、互动设计、台词留白与转场逻辑。适用于任何视觉风格。
|
||||
metaData: director_skills
|
||||
---
|
||||
|
||||
# 分镜表叙事手法 · 甜宠言情 · 技法参考
|
||||
|
||||
---
|
||||
|
||||
## 一、分镜表定位
|
||||
|
||||
分镜表是导演将剧本转化为镜头语言的核心工具。表单字段由导演根据项目需要自行设定(分镜号、景别、运镜、时长、人物、事件、台词、光影、情绪、转场等),以下仅提供甜宠言情叙事类型下的技法参考。
|
||||
|
||||
---
|
||||
|
||||
## 二、景别选择
|
||||
|
||||
- **甜宠戏的景别递进** — 同场戏内景别应随情感升温递进:半身→近景→特写→大特写。不要一上来就怼特写,留出情绪上升空间
|
||||
- **远景不是过场** — 远景镜头本身就有叙事价值(孤独感、空间压迫、季节氛围)。给远景足够时长(4-6s),别急着切走
|
||||
- **大特写要有理由** — 大特写(眼/唇/手)是情绪核弹,一集用 2-3 次足够。滥用会让观众疲劳
|
||||
- **定场镜头要精简** — 定场(建立镜头)最多 1-2 个镜头搞定,不要拆成 3 个以上碎片。典型做法:1 个大远景/远景定场 + 1 个全景引入主体,或直接 1 个带缓推的远景完成定场+引入。避免"先拍环境→再拍局部→再拍人物到达"的冗余三段式
|
||||
|
||||
---
|
||||
|
||||
## 三、运镜节奏
|
||||
|
||||
- **默认静止** — 60% 以上镜头应为静止机位,让画面细节和情绪自己说话
|
||||
- **缓推 = 情绪递进** — "观众靠近角色"的心理暗示,适合心动、发现、窥视
|
||||
- **缓拉 = 情绪抽离** — "观众退开"的心理暗示,适合离别、孤独、揭示全貌
|
||||
- **运镜即情绪** — 镜头运动不是技术选择,是情绪选择。静止 = 沉稳/压抑;缓推 = 靠近/心动;缓拉 = 抽离/孤独;缓摇 = 展示/庄重
|
||||
- **甜宠"心跳运镜"** — 暧昧升温段落可用微幅缓推配合景别递进(半身→近景→特写),模拟心跳加速时"注意力收窄"的生理感受
|
||||
|
||||
---
|
||||
|
||||
## 四、时长把控
|
||||
|
||||
- **特写/表情镜头** — 2-3s,聚焦微表情变化
|
||||
- **对话近景** — 3-4s,稳定出词
|
||||
- **全身亮相** — 3-5s,展示全貌
|
||||
- **远景/空镜** — 4-6s,氛围渲染
|
||||
- **单镜头不超过 6s** — 超过 6s 观众注意力衰减,需要运镜或动态元素维持
|
||||
- **黄金 6 秒规则** — 无台词镜头累计超过 6s 未出现新信息(台词/动作/主体变化),观众注意力断裂。定场+过渡类镜头尤其注意,宁可合并压缩也不要拖沓
|
||||
|
||||
---
|
||||
|
||||
## 五、镜头合并策略(去 AI 感)
|
||||
|
||||
- **能一镜交代的不拆两镜** — 如果一个带运镜的镜头(如缓推从远景到全景)能同时完成定场+主体引入,就不要拆成"先空镜定场→再切主体入画"两个镜头
|
||||
- **连续同类信息合并** — 连续描述同一空间不同局部的镜头(院门→藤蔓→焦黑厢房)应合并为一个镜头,用画面描述涵盖多层空间信息
|
||||
- **叙事密度优先** — 每个镜头必须推进叙事或情绪,纯装饰性镜头(只为展示环境细节)应合并到有叙事功能的镜头中
|
||||
- **导演思维检验** — 写完分镜后自检:如果一个真人导演会把相邻 2-3 个镜头合成 1 个镜头拍,说明拆得过细,应合并
|
||||
|
||||
---
|
||||
|
||||
## 六、一镜到底(长镜头合并)
|
||||
|
||||
- **适用条件** — 相邻镜头之间存在动作连续变化、场景轻度变化(同场景内位移)、或拍摄角度渐变时,优先考虑用一镜到底替代碎切,画面和内容更流畅
|
||||
- **典型场景** — 角色行走穿越空间、跟随动作从A点到B点、环绕角色展示环境、定场缓推到主体特写
|
||||
- **标注方式** — 在运镜字段写明完整路径(如"一镜到底:缓推远景→跟移至院内→落幅全景"),画面描述中交代起幅和落幅
|
||||
- **时长放宽** — 因信息量持续更新,可突破单镜 6s 上限,但不超过 12s
|
||||
- **抽卡风险** — 一镜到底对画面生成的连续性要求高,抽卡难度提升。仅在叙事流畅性收益明显大于碎切时使用,全片不宜超过 2-3 处
|
||||
|
||||
---
|
||||
|
||||
## 七、人物互动设计
|
||||
|
||||
- **单镜头动作不超过两个** — "低头拈花 + 微笑"可以,"低头拈花 + 微笑 + 转身 + 抬手"会崩
|
||||
- **甜宠互动用暗示** — 手指差一点碰到、衣袂擦过、目光追随又移开。不要在分镜表里写"拥抱""接吻"等大幅度双人交互,拆成暗示性的局部镜头
|
||||
|
||||
---
|
||||
|
||||
## 八、台词与留白
|
||||
|
||||
- **台词少的镜头给长时长** — 无台词的情绪镜头往往比有台词的更需要时间。沉默 3 秒比一句台词更有张力
|
||||
- **一句台词对应一个镜头** — 避免在单镜头内塞多句对白,切换说话者时应切镜头
|
||||
- **旁白镜头用远景或空镜** — 内心独白配近景容易显得嘴唇不动很假,配远景或场景空镜更自然
|
||||
|
||||
---
|
||||
|
||||
## 九、转场设计
|
||||
|
||||
- **默认硬切** — 同场戏内镜头间用硬切,干净利落
|
||||
- **场景切换用空镜过渡** — 不同场景间插入 1 个场景空镜(2-3s)做情绪缓冲
|
||||
- **段落切换可用叠化/淡入淡出** — 大段落间的情绪跳跃用柔性转场,避免观众出戏
|
||||
|
After Width: | Height: | Size: 743 KiB |
@ -58,7 +58,7 @@ export async function decisionAI(ctx: AgentContext) {
|
||||
...createSubAgent(ctx),
|
||||
},
|
||||
onFinish: async (completion) => {
|
||||
await memory.add("assistant:decision", completion.text);
|
||||
await memory.add("assistant:decision", removeAllXmlTags(completion.text));
|
||||
},
|
||||
});
|
||||
|
||||
@ -109,7 +109,7 @@ function createSubAgent(parentCtx: AgentContext) {
|
||||
}
|
||||
|
||||
if (fullResponse.trim()) {
|
||||
await memory.add(memoryKey, fullResponse, {
|
||||
await memory.add(memoryKey, removeAllXmlTags(fullResponse), {
|
||||
name,
|
||||
createTime: new Date(subMsg.datetime).getTime(),
|
||||
});
|
||||
@ -193,3 +193,11 @@ async function createArtSkills(artName: string) {
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
function removeAllXmlTags(text: string): string {
|
||||
text = text.replace(/<([a-zA-Z][\w-]*)(\s+[^>]*)?>([\s\S]*?)<\/\1>/g, "");
|
||||
text = text.replace(/<([a-zA-Z][\w-]*)(\s+[^>]*)?\/>/g, "");
|
||||
text = text.replace(/<\/?[a-zA-Z][\w-]*(\s+[^>]*)?>/g, "");
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
@ -37,6 +37,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
|
||||
table.text("intro");
|
||||
table.text("type");
|
||||
table.text("artStyle");
|
||||
table.text("directorManual");
|
||||
table.text("mode");
|
||||
table.text("videoRatio");
|
||||
table.integer("createTime");
|
||||
|
||||
216
src/router.ts
@ -1,4 +1,4 @@
|
||||
// @routes-hash 6b77c26005a9993d80cda7ab95d26702
|
||||
// @routes-hash 8fcf006c33d4705a20117ed4d821cc8d
|
||||
import { Express } from "express";
|
||||
|
||||
import route1 from "./routes/agents/clearMemory";
|
||||
@ -78,59 +78,62 @@ import route74 from "./routes/production/workbench/getGenerateData";
|
||||
import route75 from "./routes/production/workbench/getVideoList";
|
||||
import route76 from "./routes/production/workbench/getVideoModelDetail";
|
||||
import route77 from "./routes/production/workbench/selectVideo";
|
||||
import route78 from "./routes/project/addProject";
|
||||
import route79 from "./routes/project/addVisual";
|
||||
import route78 from "./routes/project/addDirectorManual";
|
||||
import route79 from "./routes/project/addProject";
|
||||
import route80 from "./routes/project/addVisualManual";
|
||||
import route81 from "./routes/project/deleteVisualManual";
|
||||
import route82 from "./routes/project/delProject";
|
||||
import route83 from "./routes/project/editProject";
|
||||
import route84 from "./routes/project/editVisualManual";
|
||||
import route85 from "./routes/project/getProject";
|
||||
import route86 from "./routes/project/getVisualManual";
|
||||
import route87 from "./routes/project/visualManual";
|
||||
import route88 from "./routes/script/addScript";
|
||||
import route89 from "./routes/script/delScript";
|
||||
import route90 from "./routes/script/exportScript";
|
||||
import route91 from "./routes/script/extractAssets";
|
||||
import route92 from "./routes/script/getScrptApi";
|
||||
import route93 from "./routes/script/pollScriptAssets";
|
||||
import route94 from "./routes/script/updateScript";
|
||||
import route95 from "./routes/scriptAgent/getPlanData";
|
||||
import route96 from "./routes/scriptAgent/setPlanData";
|
||||
import route97 from "./routes/scriptAgent/updateData";
|
||||
import route98 from "./routes/setting/about/checkUpdate";
|
||||
import route99 from "./routes/setting/about/downloadApp";
|
||||
import route100 from "./routes/setting/agentDeploy/agentSetKey";
|
||||
import route101 from "./routes/setting/agentDeploy/deployAgentModel";
|
||||
import route102 from "./routes/setting/agentDeploy/getAgentDeploy";
|
||||
import route103 from "./routes/setting/dbConfig/clearData";
|
||||
import route104 from "./routes/setting/dev/getSwitchAiDevTool";
|
||||
import route105 from "./routes/setting/dev/updateSwitchAiDevTool";
|
||||
import route106 from "./routes/setting/fileManagement/openFolder";
|
||||
import route107 from "./routes/setting/getTextModel";
|
||||
import route108 from "./routes/setting/loginConfig/getUser";
|
||||
import route109 from "./routes/setting/loginConfig/updateUserPwd";
|
||||
import route110 from "./routes/setting/memoryConfig/delAllMemory";
|
||||
import route111 from "./routes/setting/memoryConfig/getMemory";
|
||||
import route112 from "./routes/setting/memoryConfig/sureMemory";
|
||||
import route113 from "./routes/setting/promptManage/getPrompt";
|
||||
import route114 from "./routes/setting/promptManage/updatePrompt";
|
||||
import route115 from "./routes/setting/skillManagement/getSkillContent";
|
||||
import route116 from "./routes/setting/skillManagement/getSkillList";
|
||||
import route117 from "./routes/setting/skillManagement/saveSkillContent";
|
||||
import route118 from "./routes/setting/vendorConfig/addVendor";
|
||||
import route119 from "./routes/setting/vendorConfig/deleteVendor";
|
||||
import route120 from "./routes/setting/vendorConfig/enableEnglishVendor";
|
||||
import route121 from "./routes/setting/vendorConfig/getCodeByLink";
|
||||
import route122 from "./routes/setting/vendorConfig/getVendorList";
|
||||
import route123 from "./routes/setting/vendorConfig/modelTest";
|
||||
import route124 from "./routes/setting/vendorConfig/updateCode";
|
||||
import route125 from "./routes/setting/vendorConfig/updateVendor";
|
||||
import route126 from "./routes/task/getProject";
|
||||
import route127 from "./routes/task/getTaskApi";
|
||||
import route128 from "./routes/task/getTaskCategories";
|
||||
import route129 from "./routes/task/taskDetails";
|
||||
import route130 from "./routes/test/test";
|
||||
import route81 from "./routes/project/deleteDirectorManual";
|
||||
import route82 from "./routes/project/deleteVisualManual";
|
||||
import route83 from "./routes/project/delProject";
|
||||
import route84 from "./routes/project/editDirectorlManual";
|
||||
import route85 from "./routes/project/editProject";
|
||||
import route86 from "./routes/project/editVisualManual";
|
||||
import route87 from "./routes/project/getProject";
|
||||
import route88 from "./routes/project/getVisualManual";
|
||||
import route89 from "./routes/project/queryDirectorManual";
|
||||
import route90 from "./routes/project/visualManual";
|
||||
import route91 from "./routes/script/addScript";
|
||||
import route92 from "./routes/script/delScript";
|
||||
import route93 from "./routes/script/exportScript";
|
||||
import route94 from "./routes/script/extractAssets";
|
||||
import route95 from "./routes/script/getScrptApi";
|
||||
import route96 from "./routes/script/pollScriptAssets";
|
||||
import route97 from "./routes/script/updateScript";
|
||||
import route98 from "./routes/scriptAgent/getPlanData";
|
||||
import route99 from "./routes/scriptAgent/setPlanData";
|
||||
import route100 from "./routes/scriptAgent/updateData";
|
||||
import route101 from "./routes/setting/about/checkUpdate";
|
||||
import route102 from "./routes/setting/about/downloadApp";
|
||||
import route103 from "./routes/setting/agentDeploy/agentSetKey";
|
||||
import route104 from "./routes/setting/agentDeploy/deployAgentModel";
|
||||
import route105 from "./routes/setting/agentDeploy/getAgentDeploy";
|
||||
import route106 from "./routes/setting/dbConfig/clearData";
|
||||
import route107 from "./routes/setting/dev/getSwitchAiDevTool";
|
||||
import route108 from "./routes/setting/dev/updateSwitchAiDevTool";
|
||||
import route109 from "./routes/setting/fileManagement/openFolder";
|
||||
import route110 from "./routes/setting/getTextModel";
|
||||
import route111 from "./routes/setting/loginConfig/getUser";
|
||||
import route112 from "./routes/setting/loginConfig/updateUserPwd";
|
||||
import route113 from "./routes/setting/memoryConfig/delAllMemory";
|
||||
import route114 from "./routes/setting/memoryConfig/getMemory";
|
||||
import route115 from "./routes/setting/memoryConfig/sureMemory";
|
||||
import route116 from "./routes/setting/promptManage/getPrompt";
|
||||
import route117 from "./routes/setting/promptManage/updatePrompt";
|
||||
import route118 from "./routes/setting/skillManagement/getSkillContent";
|
||||
import route119 from "./routes/setting/skillManagement/getSkillList";
|
||||
import route120 from "./routes/setting/skillManagement/saveSkillContent";
|
||||
import route121 from "./routes/setting/vendorConfig/addVendor";
|
||||
import route122 from "./routes/setting/vendorConfig/deleteVendor";
|
||||
import route123 from "./routes/setting/vendorConfig/enableVendor";
|
||||
import route124 from "./routes/setting/vendorConfig/getCodeByLink";
|
||||
import route125 from "./routes/setting/vendorConfig/getVendorList";
|
||||
import route126 from "./routes/setting/vendorConfig/modelTest";
|
||||
import route127 from "./routes/setting/vendorConfig/updateCode";
|
||||
import route128 from "./routes/setting/vendorConfig/updateVendor";
|
||||
import route129 from "./routes/task/getProject";
|
||||
import route130 from "./routes/task/getTaskApi";
|
||||
import route131 from "./routes/task/getTaskCategories";
|
||||
import route132 from "./routes/task/taskDetails";
|
||||
import route133 from "./routes/test/test";
|
||||
|
||||
export default async (app: Express) => {
|
||||
app.use("/api/agents/clearMemory", route1);
|
||||
@ -210,57 +213,60 @@ export default async (app: Express) => {
|
||||
app.use("/api/production/workbench/getVideoList", route75);
|
||||
app.use("/api/production/workbench/getVideoModelDetail", route76);
|
||||
app.use("/api/production/workbench/selectVideo", route77);
|
||||
app.use("/api/project/addProject", route78);
|
||||
app.use("/api/project/addVisual", route79);
|
||||
app.use("/api/project/addDirectorManual", route78);
|
||||
app.use("/api/project/addProject", route79);
|
||||
app.use("/api/project/addVisualManual", route80);
|
||||
app.use("/api/project/deleteVisualManual", route81);
|
||||
app.use("/api/project/delProject", route82);
|
||||
app.use("/api/project/editProject", route83);
|
||||
app.use("/api/project/editVisualManual", route84);
|
||||
app.use("/api/project/getProject", route85);
|
||||
app.use("/api/project/getVisualManual", route86);
|
||||
app.use("/api/project/visualManual", route87);
|
||||
app.use("/api/script/addScript", route88);
|
||||
app.use("/api/script/delScript", route89);
|
||||
app.use("/api/script/exportScript", route90);
|
||||
app.use("/api/script/extractAssets", route91);
|
||||
app.use("/api/script/getScrptApi", route92);
|
||||
app.use("/api/script/pollScriptAssets", route93);
|
||||
app.use("/api/script/updateScript", route94);
|
||||
app.use("/api/scriptAgent/getPlanData", route95);
|
||||
app.use("/api/scriptAgent/setPlanData", route96);
|
||||
app.use("/api/scriptAgent/updateData", route97);
|
||||
app.use("/api/setting/about/checkUpdate", route98);
|
||||
app.use("/api/setting/about/downloadApp", route99);
|
||||
app.use("/api/setting/agentDeploy/agentSetKey", route100);
|
||||
app.use("/api/setting/agentDeploy/deployAgentModel", route101);
|
||||
app.use("/api/setting/agentDeploy/getAgentDeploy", route102);
|
||||
app.use("/api/setting/dbConfig/clearData", route103);
|
||||
app.use("/api/setting/dev/getSwitchAiDevTool", route104);
|
||||
app.use("/api/setting/dev/updateSwitchAiDevTool", route105);
|
||||
app.use("/api/setting/fileManagement/openFolder", route106);
|
||||
app.use("/api/setting/getTextModel", route107);
|
||||
app.use("/api/setting/loginConfig/getUser", route108);
|
||||
app.use("/api/setting/loginConfig/updateUserPwd", route109);
|
||||
app.use("/api/setting/memoryConfig/delAllMemory", route110);
|
||||
app.use("/api/setting/memoryConfig/getMemory", route111);
|
||||
app.use("/api/setting/memoryConfig/sureMemory", route112);
|
||||
app.use("/api/setting/promptManage/getPrompt", route113);
|
||||
app.use("/api/setting/promptManage/updatePrompt", route114);
|
||||
app.use("/api/setting/skillManagement/getSkillContent", route115);
|
||||
app.use("/api/setting/skillManagement/getSkillList", route116);
|
||||
app.use("/api/setting/skillManagement/saveSkillContent", route117);
|
||||
app.use("/api/setting/vendorConfig/addVendor", route118);
|
||||
app.use("/api/setting/vendorConfig/deleteVendor", route119);
|
||||
app.use("/api/setting/vendorConfig/enableEnglishVendor", route120);
|
||||
app.use("/api/setting/vendorConfig/getCodeByLink", route121);
|
||||
app.use("/api/setting/vendorConfig/getVendorList", route122);
|
||||
app.use("/api/setting/vendorConfig/modelTest", route123);
|
||||
app.use("/api/setting/vendorConfig/updateCode", route124);
|
||||
app.use("/api/setting/vendorConfig/updateVendor", route125);
|
||||
app.use("/api/task/getProject", route126);
|
||||
app.use("/api/task/getTaskApi", route127);
|
||||
app.use("/api/task/getTaskCategories", route128);
|
||||
app.use("/api/task/taskDetails", route129);
|
||||
app.use("/api/test/test", route130);
|
||||
app.use("/api/project/deleteDirectorManual", route81);
|
||||
app.use("/api/project/deleteVisualManual", route82);
|
||||
app.use("/api/project/delProject", route83);
|
||||
app.use("/api/project/editDirectorlManual", route84);
|
||||
app.use("/api/project/editProject", route85);
|
||||
app.use("/api/project/editVisualManual", route86);
|
||||
app.use("/api/project/getProject", route87);
|
||||
app.use("/api/project/getVisualManual", route88);
|
||||
app.use("/api/project/queryDirectorManual", route89);
|
||||
app.use("/api/project/visualManual", route90);
|
||||
app.use("/api/script/addScript", route91);
|
||||
app.use("/api/script/delScript", route92);
|
||||
app.use("/api/script/exportScript", route93);
|
||||
app.use("/api/script/extractAssets", route94);
|
||||
app.use("/api/script/getScrptApi", route95);
|
||||
app.use("/api/script/pollScriptAssets", route96);
|
||||
app.use("/api/script/updateScript", route97);
|
||||
app.use("/api/scriptAgent/getPlanData", route98);
|
||||
app.use("/api/scriptAgent/setPlanData", route99);
|
||||
app.use("/api/scriptAgent/updateData", route100);
|
||||
app.use("/api/setting/about/checkUpdate", route101);
|
||||
app.use("/api/setting/about/downloadApp", route102);
|
||||
app.use("/api/setting/agentDeploy/agentSetKey", route103);
|
||||
app.use("/api/setting/agentDeploy/deployAgentModel", route104);
|
||||
app.use("/api/setting/agentDeploy/getAgentDeploy", route105);
|
||||
app.use("/api/setting/dbConfig/clearData", route106);
|
||||
app.use("/api/setting/dev/getSwitchAiDevTool", route107);
|
||||
app.use("/api/setting/dev/updateSwitchAiDevTool", route108);
|
||||
app.use("/api/setting/fileManagement/openFolder", route109);
|
||||
app.use("/api/setting/getTextModel", route110);
|
||||
app.use("/api/setting/loginConfig/getUser", route111);
|
||||
app.use("/api/setting/loginConfig/updateUserPwd", route112);
|
||||
app.use("/api/setting/memoryConfig/delAllMemory", route113);
|
||||
app.use("/api/setting/memoryConfig/getMemory", route114);
|
||||
app.use("/api/setting/memoryConfig/sureMemory", route115);
|
||||
app.use("/api/setting/promptManage/getPrompt", route116);
|
||||
app.use("/api/setting/promptManage/updatePrompt", route117);
|
||||
app.use("/api/setting/skillManagement/getSkillContent", route118);
|
||||
app.use("/api/setting/skillManagement/getSkillList", route119);
|
||||
app.use("/api/setting/skillManagement/saveSkillContent", route120);
|
||||
app.use("/api/setting/vendorConfig/addVendor", route121);
|
||||
app.use("/api/setting/vendorConfig/deleteVendor", route122);
|
||||
app.use("/api/setting/vendorConfig/enableVendor", route123);
|
||||
app.use("/api/setting/vendorConfig/getCodeByLink", route124);
|
||||
app.use("/api/setting/vendorConfig/getVendorList", route125);
|
||||
app.use("/api/setting/vendorConfig/modelTest", route126);
|
||||
app.use("/api/setting/vendorConfig/updateCode", route127);
|
||||
app.use("/api/setting/vendorConfig/updateVendor", route128);
|
||||
app.use("/api/task/getProject", route129);
|
||||
app.use("/api/task/getTaskApi", route130);
|
||||
app.use("/api/task/getTaskCategories", route131);
|
||||
app.use("/api/task/taskDetails", route132);
|
||||
app.use("/api/test/test", route133);
|
||||
}
|
||||
|
||||
@ -127,7 +127,7 @@ export default router.post(
|
||||
const config = typeConfig[item.type];
|
||||
if (!config) return;
|
||||
//获取到视觉手册
|
||||
const visualManual = await u.getArtPrompt(project.artStyle as string, config.visualManual);
|
||||
const visualManual = await u.getArtPrompt(project.artStyle as string, "art_skills", config.visualManual);
|
||||
if (!visualManual) return res.status(500).send(error("视觉手册未定义"));
|
||||
findItemByName(result, item.name, config.itemType);
|
||||
const systemPrompt = visualManual;
|
||||
|
||||
@ -107,7 +107,7 @@ export default router.post(
|
||||
if (!config) return res.status(500).send(error("不支持的类型"));
|
||||
if (!config.visualManual) return res.status(500).send(error("视觉手册未定义"));
|
||||
//获取到视觉手册
|
||||
const visualManual = await u.getArtPrompt(project.artStyle as string, config.visualManual);
|
||||
const visualManual = await u.getArtPrompt(project.artStyle as string, "art_skills", config.visualManual);
|
||||
if (!visualManual) return res.status(500).send(error("视觉手册未定义"));
|
||||
findItemByName(result, name, config.itemType);
|
||||
const systemPrompt = visualManual;
|
||||
|
||||
@ -40,9 +40,9 @@ export default router.post(
|
||||
assetsSrcArr.forEach((item) => {
|
||||
imageUrlRecord[item.id] = item.src;
|
||||
});
|
||||
const rolePrompt = u.getArtPrompt(projectSettingData!.artStyle!, "art_character_derivative");
|
||||
const toolPrompt = u.getArtPrompt(projectSettingData!.artStyle!, "art_prop_derivative");
|
||||
const scenePrompt = u.getArtPrompt(projectSettingData!.artStyle!, "art_scene_derivative");
|
||||
const rolePrompt = u.getArtPrompt(projectSettingData!.artStyle!, "art_skills", "art_character_derivative");
|
||||
const toolPrompt = u.getArtPrompt(projectSettingData!.artStyle!, "art_skills", "art_prop_derivative");
|
||||
const scenePrompt = u.getArtPrompt(projectSettingData!.artStyle!, "art_skills", "art_scene_derivative");
|
||||
const promptRecord: Record<string, { prompt: string }> = {
|
||||
role: {
|
||||
prompt: rolePrompt,
|
||||
|
||||
@ -32,7 +32,6 @@ export default router.post(
|
||||
.where("o_assets.id", "in", assetIds)
|
||||
.andWhere("o_assets.assetsId", null)
|
||||
.where("o_assets.projectId", projectId);
|
||||
console.log("%c Line:28 🎂 assetsData", "background:#6ec1c2", assetsData);
|
||||
|
||||
let childAssetsData = await u
|
||||
.db("o_assets")
|
||||
|
||||
@ -37,7 +37,7 @@ export default router.post(
|
||||
trackId: z.number(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { scriptId, projectId, prompt, uploadData, model, duration, resolution, audio, mode, trackId } = req.body;
|
||||
const { scriptId, projectId, prompt, uploadData, model, duration, resolution, audio, mode, trackId } = req.body;
|
||||
//获取生成视频比例
|
||||
const ratio = await u.db("o_project").select("videoRatio").where("id", projectId).first();
|
||||
const videoPath = `/${projectId}/video/${uuidv4()}.mp4`; //视频保存路径
|
||||
|
||||
@ -19,15 +19,15 @@ export default router.post(
|
||||
const projectData = await u.db("o_project").select("*").where({ id: projectId }).first();
|
||||
const videoPrompt = await u.db("o_prompt").where("type", "videoPromptGeneration").first();
|
||||
const artStyle = projectData?.artStyle || "无";
|
||||
const visualManual = u.getArtPrompt(artStyle, "art_storyboard_video");
|
||||
const data = projectData?.directorManual || "无";
|
||||
const visualManual = u.getArtPrompt(artStyle, "art_skills", "art_storyboard_video");
|
||||
const directorManual = u.getArtPrompt(data, "story_skills", "narrative_sweet_romance");
|
||||
const { text } = await u.Ai.Text("universalAi").invoke({
|
||||
system: `${videoPrompt?.data},${visualManual}`,
|
||||
system: `${videoPrompt?.data}\n${visualManual}\n${directorManual}`,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: `你是一个专业的${modelData}视频生成助手。请根据以下提示词,生成一段完整的、可直接用于视频生成模型的中文提示词。${prompt.join(
|
||||
",",
|
||||
)}`,
|
||||
content: `${prompt.join(",")}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@ -58,7 +58,7 @@ export default router.post(
|
||||
prompt: item?.prompt || "",
|
||||
state: (item?.state as "未生成" | "生成中" | "已完成" | "生成失败") ?? "未生成",
|
||||
reason: item?.reason ?? "",
|
||||
selectVideoId: Number(item?.selectVideoId)!,
|
||||
selectVideoId: Number(item?.videoId)!,
|
||||
medias: await Promise.all(
|
||||
storyboardList
|
||||
.filter((s) => s.trackId === trackId)
|
||||
|
||||
@ -14,7 +14,7 @@ export default router.post(
|
||||
async (req, res) => {
|
||||
const { trackId, videoId } = req.body;
|
||||
await u.db("o_videoTrack").where("id", trackId).update({
|
||||
selectVideoId: videoId,
|
||||
videoId: videoId,
|
||||
});
|
||||
res.status(200).send(success({ message: "视频选择成功" }));
|
||||
},
|
||||
|
||||
104
src/routes/project/addDirectorManual.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { error, success } from "@/lib/responseFormat";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
import { z } from "zod";
|
||||
const router = express.Router();
|
||||
|
||||
// 新增导演手册
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
name: z.string(),
|
||||
images: z.array(z.string()),
|
||||
directorManual: z.string(),
|
||||
data: z.array(
|
||||
z.object({
|
||||
label: z.string(),
|
||||
value: z.string(),
|
||||
data: z.string(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { name, images, data, directorManual } = req.body as {
|
||||
name: string;
|
||||
images: string[];
|
||||
data: { label: string; value: string; data: string }[];
|
||||
directorManual: string;
|
||||
};
|
||||
// 安全校验:不允许包含路径分隔符、纯数字,防止越级删除或误删项目目录
|
||||
if (name.includes("/") || name.includes("\\") || name === "." || name === ".." || /^\d+$/.test(name)) {
|
||||
res.status(400).send(error("名称不能包含路径分隔符或为纯数字"));
|
||||
return;
|
||||
}
|
||||
|
||||
const mainPath = u.getPath(["skills", "story_skills", directorManual]);
|
||||
if (fs.existsSync(mainPath)) {
|
||||
return res.status(400).send(error("请勿填写重复名称的视觉手册"));
|
||||
}
|
||||
// 字段映射表(与 getVisualManual 保持一致)
|
||||
const DATA_MAP: { value: string; subDir?: string }[] = [
|
||||
{ value: "README" },
|
||||
{ value: "narrative_sweet_romance", subDir: "art_prompt" },
|
||||
{ value: "storyboard_table_narrative", subDir: "art_prompt" },
|
||||
];
|
||||
// 根据 DATA_MAP 构建 value -> subDir 的映射
|
||||
const SUB_DIR_MAP = new Map(DATA_MAP.map(({ value, subDir }) => [value, subDir ?? ""]));
|
||||
|
||||
// 合法的 value 值集合,用于校验
|
||||
const VALID_KEYS = new Set(DATA_MAP.map(({ value }) => value));
|
||||
|
||||
for (const item of data) {
|
||||
if (!VALID_KEYS.has(item.value)) continue;
|
||||
|
||||
const subDir = SUB_DIR_MAP.get(item.value)!;
|
||||
const dirArr = subDir ? [mainPath, subDir] : [mainPath];
|
||||
const filePath = u.getPath([...dirArr, `${item.value}.md`]);
|
||||
|
||||
const fileDir = path.dirname(filePath);
|
||||
// 目录不存在时递归创建
|
||||
if (!fs.existsSync(fileDir)) {
|
||||
fs.mkdirSync(fileDir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(filePath, item.data, "utf-8");
|
||||
}
|
||||
const imagesDir = path.join(mainPath, "images");
|
||||
|
||||
let existingFiles: string[] = [];
|
||||
try {
|
||||
const allFiles = fs.readdirSync(imagesDir);
|
||||
existingFiles = allFiles.filter((f) => /\.(png|jpe?g|gif|webp|svg)$/i.test(f));
|
||||
} catch {}
|
||||
|
||||
const retainedFileNames = new Set(images.filter((item) => item.startsWith("http")).map((url) => path.basename(new URL(url).pathname)));
|
||||
|
||||
for (const file of existingFiles) {
|
||||
if (!retainedFileNames.has(file)) {
|
||||
const filePath = path.join(imagesDir, file);
|
||||
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(imagesDir)) {
|
||||
fs.mkdirSync(imagesDir, { recursive: true });
|
||||
}
|
||||
|
||||
for (const item of images) {
|
||||
if (!item.startsWith("http")) {
|
||||
const fileName = `${u.uuid()}.jpg`;
|
||||
const targetPath = path.join(imagesDir, fileName);
|
||||
const buffer = Buffer.from(item.replace(/^data:[^;]+;base64,/, ""), "base64");
|
||||
fs.writeFileSync(targetPath, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).send(success());
|
||||
} catch (err) {
|
||||
res.status(500).send({ error: String(err) });
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -14,6 +14,7 @@ export default router.post(
|
||||
intro: z.string(),
|
||||
type: z.string(),
|
||||
artStyle: z.string(),
|
||||
directorManual: z.string(),
|
||||
videoRatio: z.string(),
|
||||
imageModel: z.string(),
|
||||
videoModel: z.string(),
|
||||
@ -21,7 +22,7 @@ export default router.post(
|
||||
mode: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectType, name, intro, type, artStyle, videoRatio, imageModel, videoModel, imageQuality, mode } = req.body;
|
||||
const { projectType, name, intro, type, directorManual, artStyle, videoRatio, imageModel, videoModel, imageQuality, mode } = req.body;
|
||||
|
||||
await u.db("o_project").insert({
|
||||
projectType,
|
||||
@ -30,6 +31,7 @@ export default router.post(
|
||||
type,
|
||||
artStyle,
|
||||
videoRatio,
|
||||
directorManual,
|
||||
userId: 1,
|
||||
imageModel,
|
||||
videoModel,
|
||||
|
||||
@ -1,85 +0,0 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
import { z } from "zod";
|
||||
const router = express.Router();
|
||||
|
||||
// 新增视觉手册
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
name: z.string(),
|
||||
image: z.string(),
|
||||
data: z.array(
|
||||
z.object({
|
||||
label: z.string(),
|
||||
value: z.string(),
|
||||
data: z.string(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { name, image, data } = req.body as {
|
||||
name: string;
|
||||
image: string;
|
||||
data: { label: string; value: string; data: string }[];
|
||||
};
|
||||
|
||||
const mainPath = u.getPath(["skills", "art_prompts", name]);
|
||||
|
||||
// 将 image 写入 mainPath/images/image 文件(无后缀)
|
||||
if (image) {
|
||||
const imagesDir = path.join(mainPath, "images");
|
||||
if (!fs.existsSync(imagesDir)) {
|
||||
fs.mkdirSync(imagesDir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(path.join(imagesDir, "image"), image, "utf-8");
|
||||
}
|
||||
|
||||
// 字段映射表(与 getVisualManual 保持一致)
|
||||
const DATA_MAP: { label: string; value: string; subDir?: string }[] = [
|
||||
{ label: "README", value: "README" },
|
||||
{ label: "前缀", value: "prefix" },
|
||||
{ label: "角色", value: "art_character", subDir: "art_prompt" },
|
||||
{ label: "角色衍生", value: "art_character_derivative", subDir: "art_prompt" },
|
||||
{ label: "道具", value: "art_prop", subDir: "art_prompt" },
|
||||
{ label: "道具衍生", value: "art_prop_derivative", subDir: "art_prompt" },
|
||||
{ label: "场景", value: "art_scene", subDir: "art_prompt" },
|
||||
{ label: "场景衍生", value: "art_scene_derivative", subDir: "art_prompt" },
|
||||
{ label: "分镜", value: "art_storyboard", subDir: "art_prompt" },
|
||||
{ label: "分镜视频", value: "art_storyboard_video", subDir: "art_prompt" },
|
||||
{ label: "技法-导演规划", value: "director_planning", subDir: "driector_skills" },
|
||||
{ label: "技法-分镜表设计", value: "director_storyboard_table", subDir: "driector_skills" },
|
||||
];
|
||||
|
||||
// 根据 DATA_MAP 构建 value -> subDir 的映射
|
||||
const SUB_DIR_MAP = new Map(DATA_MAP.map(({ value, subDir }) => [value, subDir ?? ""]));
|
||||
|
||||
// 合法的 value 值集合,用于校验
|
||||
const VALID_KEYS = new Set(DATA_MAP.map(({ value }) => value));
|
||||
|
||||
for (const item of data) {
|
||||
if (!VALID_KEYS.has(item.value)) continue;
|
||||
|
||||
const subDir = SUB_DIR_MAP.get(item.value)!;
|
||||
const dirArr = subDir ? [mainPath, subDir] : [mainPath];
|
||||
const filePath = u.getPath([...dirArr, `${item.value}.md`]);
|
||||
|
||||
const fileDir = path.dirname(filePath);
|
||||
// 目录不存在时递归创建
|
||||
if (!fs.existsSync(fileDir)) {
|
||||
fs.mkdirSync(fileDir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(filePath, item.data, "utf-8");
|
||||
}
|
||||
|
||||
res.status(200).send(success());
|
||||
} catch (err) {
|
||||
res.status(500).send({ error: String(err) });
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -7,7 +7,7 @@ import { validateFields } from "@/middleware/middleware";
|
||||
import { z } from "zod";
|
||||
const router = express.Router();
|
||||
|
||||
// 编辑视觉手册
|
||||
// 新增视觉手册
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
@ -31,12 +31,12 @@ export default router.post(
|
||||
stylePath: string;
|
||||
};
|
||||
|
||||
if (/^\d+$/.test(stylePath)) {
|
||||
res.status(400).send(error("文件名称不能为纯数字"));
|
||||
// 安全校验:不允许包含路径分隔符、纯数字,防止越级删除或误删项目目录
|
||||
if (name.includes("/") || name.includes("\\") || name === "." || name === ".." || /^\d+$/.test(name)) {
|
||||
res.status(400).send(error("名称不能包含路径分隔符或为纯数字"));
|
||||
return;
|
||||
}
|
||||
|
||||
const mainPath = u.getPath(["skills", "art_prompts", stylePath]);
|
||||
const mainPath = u.getPath(["skills", "art_skills", stylePath]);
|
||||
if (fs.existsSync(mainPath)) {
|
||||
return res.status(400).send(error("请勿填写重复名称的视觉手册"));
|
||||
}
|
||||
|
||||
41
src/routes/project/deleteDirectorManual.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import fs from "node:fs/promises";
|
||||
import { z } from "zod";
|
||||
import { error, success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
const router = express.Router();
|
||||
|
||||
// 删除导演手册
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
name: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { name } = req.body as { name: string };
|
||||
|
||||
// 安全校验:不允许包含路径分隔符、纯数字,防止越级删除或误删项目目录
|
||||
if (name.includes("/") || name.includes("\\") || name === "." || name === ".." || /^\d+$/.test(name)) {
|
||||
res.status(400).send(error("名称不能包含路径分隔符或为纯数字"));
|
||||
return;
|
||||
}
|
||||
|
||||
const artPromptsDir = u.getPath(["skills", "story_skills", name]);
|
||||
|
||||
try {
|
||||
const stat = await fs.stat(artPromptsDir);
|
||||
if (!stat.isDirectory()) {
|
||||
throw new Error(`${artPromptsDir} 不是文件夹`);
|
||||
}
|
||||
await fs.rm(artPromptsDir, { recursive: true, force: true });
|
||||
} catch (e) {
|
||||
console.error("[删除视觉手册] 删除失败:", artPromptsDir, e);
|
||||
}
|
||||
res.status(200).send(success({ message: "删除成功" }));
|
||||
} catch (err) {
|
||||
res.status(500).send(error(u.error(err).message || "删除失败"));
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -22,7 +22,7 @@ export default router.post(
|
||||
return;
|
||||
}
|
||||
|
||||
const artPromptsDir = u.getPath(["skills", "art_prompts", name]);
|
||||
const artPromptsDir = u.getPath(["skills", "art_skills", name]);
|
||||
|
||||
try {
|
||||
const stat = await fs.stat(artPromptsDir);
|
||||
|
||||
106
src/routes/project/editDirectorlManual.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { error, success } from "@/lib/responseFormat";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
import { z } from "zod";
|
||||
const router = express.Router();
|
||||
|
||||
// 编辑导演手册
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
name: z.string(),
|
||||
directorManual: z.string(),
|
||||
images: z.array(z.string()),
|
||||
data: z.array(
|
||||
z.object({
|
||||
label: z.string(),
|
||||
value: z.string(),
|
||||
data: z.string(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { name, directorManual, images, data } = req.body as {
|
||||
name: string;
|
||||
directorManual: string;
|
||||
images: string[];
|
||||
data: { label: string; value: string; data: string }[];
|
||||
};
|
||||
|
||||
// 安全校验:不允许包含路径分隔符、纯数字,防止越级删除或误删项目目录
|
||||
if (name.includes("/") || name.includes("\\") || name === "." || name === ".." || /^\d+$/.test(name)) {
|
||||
res.status(400).send(error("名称不能包含路径分隔符或为纯数字"));
|
||||
return;
|
||||
}
|
||||
|
||||
const mainPath = u.getPath(["skills", "story_skills", directorManual]);
|
||||
if (!fs.existsSync(mainPath)) {
|
||||
return res.status(400).send(error("导演手册不存在"));
|
||||
}
|
||||
// 字段映射表(与 getVisualManual 保持一致)
|
||||
const DATA_MAP: { value: string; subDir?: string }[] = [
|
||||
{ value: "README" },
|
||||
{ value: "narrative_sweet_romance", subDir: "art_prompt" },
|
||||
{ value: "storyboard_table_narrative", subDir: "art_prompt" },
|
||||
];
|
||||
// 根据 DATA_MAP 构建 value -> subDir 的映射
|
||||
const SUB_DIR_MAP = new Map(DATA_MAP.map(({ value, subDir }) => [value, subDir ?? ""]));
|
||||
|
||||
// 合法的 value 值集合,用于校验
|
||||
const VALID_KEYS = new Set(DATA_MAP.map(({ value }) => value));
|
||||
|
||||
for (const item of data) {
|
||||
if (!VALID_KEYS.has(item.value)) continue;
|
||||
|
||||
const subDir = SUB_DIR_MAP.get(item.value)!;
|
||||
const dirArr = subDir ? [mainPath, subDir] : [mainPath];
|
||||
const filePath = u.getPath([...dirArr, `${item.value}.md`]);
|
||||
|
||||
const fileDir = path.dirname(filePath);
|
||||
// 目录不存在时递归创建
|
||||
if (!fs.existsSync(fileDir)) {
|
||||
fs.mkdirSync(fileDir, { recursive: true });
|
||||
}
|
||||
const content = item.value === "README" ? `${name}\n${item.data}` : item.data;
|
||||
fs.writeFileSync(filePath, content, "utf-8");
|
||||
}
|
||||
const imagesDir = path.join(mainPath, "images");
|
||||
|
||||
let existingFiles: string[] = [];
|
||||
try {
|
||||
const allFiles = fs.readdirSync(imagesDir);
|
||||
existingFiles = allFiles.filter((f) => /\.(png|jpe?g|gif|webp|svg)$/i.test(f));
|
||||
} catch {}
|
||||
|
||||
const retainedFileNames = new Set(images.filter((item) => item.startsWith("http")).map((url) => path.basename(new URL(url).pathname)));
|
||||
|
||||
for (const file of existingFiles) {
|
||||
if (!retainedFileNames.has(file)) {
|
||||
const filePath = path.join(imagesDir, file);
|
||||
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(imagesDir)) {
|
||||
fs.mkdirSync(imagesDir, { recursive: true });
|
||||
}
|
||||
|
||||
for (const item of images) {
|
||||
if (!item.startsWith("http")) {
|
||||
const fileName = `${u.uuid()}.jpg`;
|
||||
const targetPath = path.join(imagesDir, fileName);
|
||||
const buffer = Buffer.from(item.replace(/^data:[^;]+;base64,/, ""), "base64");
|
||||
fs.writeFileSync(targetPath, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).send(success());
|
||||
} catch (err) {
|
||||
res.status(500).send({ error: String(err) });
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -14,6 +14,7 @@ export default router.post(
|
||||
intro: z.string(),
|
||||
type: z.string(),
|
||||
artStyle: z.string(),
|
||||
directorManual: z.string(),
|
||||
videoRatio: z.string(),
|
||||
imageModel: z.string(),
|
||||
videoModel: z.string(),
|
||||
@ -22,7 +23,7 @@ export default router.post(
|
||||
mode: z.string(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id, name, intro, type, artStyle, videoRatio, imageModel, videoModel, imageQuality, projectType, mode } = req.body;
|
||||
const { id, name, intro, type, artStyle, videoRatio, directorManual, imageModel, videoModel, imageQuality, projectType, mode } = req.body;
|
||||
|
||||
await u.db("o_project").where("id", id).update({
|
||||
name,
|
||||
@ -30,6 +31,7 @@ export default router.post(
|
||||
type,
|
||||
artStyle,
|
||||
videoRatio,
|
||||
directorManual,
|
||||
imageModel,
|
||||
videoModel,
|
||||
imageQuality,
|
||||
|
||||
@ -31,12 +31,13 @@ export default router.post(
|
||||
data: { label: string; value: string; data: string }[];
|
||||
};
|
||||
|
||||
if (/^\d+$/.test(stylePath)) {
|
||||
res.status(400).send(error("名称不能为纯数字"));
|
||||
// 安全校验:不允许包含路径分隔符、纯数字,防止越级删除或误删项目目录
|
||||
if (name.includes("/") || name.includes("\\") || name === "." || name === ".." || /^\d+$/.test(name)) {
|
||||
res.status(400).send(error("名称不能包含路径分隔符或为纯数字"));
|
||||
return;
|
||||
}
|
||||
|
||||
const mainPath = u.getPath(["skills", "art_prompts", stylePath]);
|
||||
const mainPath = u.getPath(["skills", "art_skills", stylePath]);
|
||||
if (!fs.existsSync(mainPath)) {
|
||||
return res.status(400).send(error("视觉手册不存在"));
|
||||
}
|
||||
|
||||
@ -33,9 +33,9 @@ function readMd(filePath: string): string {
|
||||
// 获取 images 文件夹下所有图片文件路径列表
|
||||
async function readAllImages(imagesDir: string) {
|
||||
try {
|
||||
const ossPath = u.getPath(path.join("skills", "art_prompts", imagesDir, "images"));
|
||||
const ossPath = u.getPath(path.join("skills", "art_skills", imagesDir, "images"));
|
||||
const files = fs.readdirSync(ossPath);
|
||||
const images = files.filter((f) => /\.(png|jpe?g|gif|webp|svg)$/i.test(f)).map((f) => path.join("art_prompts", imagesDir, "images", f));
|
||||
const images = files.filter((f) => /\.(png|jpe?g|gif|webp|svg)$/i.test(f)).map((f) => path.join("art_skills", imagesDir, "images", f));
|
||||
if (images.length) {
|
||||
return Promise.all(images.map(async (i) => await u.oss.getFileUrl(i, "skills")));
|
||||
} else {
|
||||
@ -49,7 +49,7 @@ async function readAllImages(imagesDir: string) {
|
||||
// 获取视觉手册
|
||||
export default router.post("/", async (req, res) => {
|
||||
try {
|
||||
const artPromptsDir = u.getPath(["skills", "art_prompts"]);
|
||||
const artPromptsDir = u.getPath(["skills", "art_skills"]);
|
||||
|
||||
// 读取所有风格文件夹
|
||||
const styleDirs = fs
|
||||
|
||||
84
src/routes/project/queryDirectorManual.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
const router = express.Router();
|
||||
|
||||
// 字段映射表
|
||||
const DATA_MAP: { label: string; value: string; subDir?: string }[] = [
|
||||
{ label: "README", value: "README" },
|
||||
{ label: "导演规划", value: "narrative_sweet_romance", subDir: "art_prompt" },
|
||||
{ label: "分镜表", value: "storyboard_table_narrative", subDir: "art_prompt" },
|
||||
];
|
||||
|
||||
// 读取 md 文件内容,文件不存在时返回空字符串
|
||||
function readMd(filePath: string): string {
|
||||
try {
|
||||
return fs.readFileSync(filePath, "utf-8");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 images 文件夹下所有图片文件路径列表
|
||||
async function readAllImages(imagesDir: string) {
|
||||
try {
|
||||
const ossPath = u.getPath(path.join("skills", "story_skills", imagesDir, "images"));
|
||||
const files = fs.readdirSync(ossPath);
|
||||
const images = files.filter((f) => /\.(png|jpe?g|gif|webp|svg)$/i.test(f)).map((f) => path.join("story_skills", imagesDir, "images", f));
|
||||
if (images.length) {
|
||||
return Promise.all(images.map(async (i) => await u.oss.getFileUrl(i, "skills")));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 获取导演手册
|
||||
export default router.post("/", async (req, res) => {
|
||||
try {
|
||||
const artPromptsDir = u.getPath(["skills", "story_skills"]);
|
||||
|
||||
// 读取所有风格文件夹
|
||||
const styleDirs = fs
|
||||
.readdirSync(artPromptsDir, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory())
|
||||
.map((d) => d.name);
|
||||
|
||||
const result = await Promise.all(
|
||||
styleDirs.map(async (directorManual) => {
|
||||
const styleDir = path.join(artPromptsDir, directorManual);
|
||||
const images = await readAllImages(directorManual);
|
||||
const readmePath = path.join(styleDir, "README.md");
|
||||
const readmeContent = fs.readFileSync(readmePath, "utf-8");
|
||||
const firstLine = readmeContent.split("\n")[0].replace(/--/g, "");
|
||||
const data = DATA_MAP.map(({ label, value, subDir }) => {
|
||||
let mdPath: string;
|
||||
if (subDir) {
|
||||
mdPath = path.join(styleDir, subDir, `${value}.md`);
|
||||
} else {
|
||||
mdPath = path.join(styleDir, `${value}.md`);
|
||||
}
|
||||
return {
|
||||
label,
|
||||
value,
|
||||
data: readMd(mdPath),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
name: firstLine,
|
||||
image: images,
|
||||
directorManual: directorManual,
|
||||
data,
|
||||
};
|
||||
}),
|
||||
);
|
||||
res.status(200).send(success(result));
|
||||
} catch (err) {
|
||||
res.status(500).send({ error: String(err) });
|
||||
}
|
||||
});
|
||||
19
src/types/database.d.ts
vendored
@ -1,6 +1,21 @@
|
||||
// @db-hash 7af86e2bafe5cab7d175eb68cf76ed7a
|
||||
// @db-hash 35cf00f711e9d4df398703de70511684
|
||||
//该文件由脚本自动生成,请勿手动修改
|
||||
|
||||
export interface _o_project_old_20260402 {
|
||||
'artStyle'?: string | null;
|
||||
'createTime'?: number | null;
|
||||
'id'?: number | null;
|
||||
'imageModel'?: string | null;
|
||||
'imageQuality'?: string | null;
|
||||
'intro'?: string | null;
|
||||
'mode'?: string | null;
|
||||
'name'?: string | null;
|
||||
'projectType'?: string | null;
|
||||
'type'?: string | null;
|
||||
'userId'?: number | null;
|
||||
'videoModel'?: string | null;
|
||||
'videoRatio'?: string | null;
|
||||
}
|
||||
export interface _o_storyboard_old_20260402 {
|
||||
'createTime'?: number | null;
|
||||
'duration'?: string | null;
|
||||
@ -165,6 +180,7 @@ export interface o_outlineNovel {
|
||||
export interface o_project {
|
||||
'artStyle'?: string | null;
|
||||
'createTime'?: number | null;
|
||||
'directorManual'?: string | null;
|
||||
'id'?: number | null;
|
||||
'imageModel'?: string | null;
|
||||
'imageQuality'?: string | null;
|
||||
@ -285,6 +301,7 @@ export interface o_videoTrack {
|
||||
}
|
||||
|
||||
export interface DB {
|
||||
"_o_project_old_20260402": _o_project_old_20260402;
|
||||
"_o_storyboard_old_20260402": _o_storyboard_old_20260402;
|
||||
"_o_storyboard_old_20260402_1": _o_storyboard_old_20260402_1;
|
||||
"_o_vendorConfig_old_20260401": _o_vendorConfig_old_20260401;
|
||||
|
||||
@ -8,8 +8,8 @@ import getPath from "./getPath";
|
||||
* @param fileName - 目标文件名(不含 .md 后缀),例如 "art_character"、"prefix"
|
||||
* @returns 文件内容字符串,未找到时返回空字符串
|
||||
*/
|
||||
export function getArtPrompt(styleName: string, fileName: string): string {
|
||||
const baseDir = getPath(["skills", "art_prompts", styleName]);
|
||||
export function getArtPrompt(styleName: string, source: string, fileName: string): string {
|
||||
const baseDir = getPath(["skills", source, styleName]);
|
||||
|
||||
if (!fs.existsSync(baseDir)) {
|
||||
return "";
|
||||
@ -34,8 +34,8 @@ export function getArtPrompt(styleName: string, fileName: string): string {
|
||||
* @param styleName - 风格目录名,例如 "chinese_sweet_romance"
|
||||
* @returns Record<文件名(不含后缀), 文件内容>
|
||||
*/
|
||||
export function getAllArtPrompts(styleName: string): Record<string, string> {
|
||||
const baseDir = getPath(["skills", "art_prompts", styleName]);
|
||||
export function getAllArtPrompts(styleName: string, source: string): Record<string, string> {
|
||||
const baseDir = getPath(["skills", source, styleName]);
|
||||
|
||||
if (!fs.existsSync(baseDir)) {
|
||||
return {};
|
||||
|
||||
@ -95,6 +95,8 @@ class OSS {
|
||||
".ico": "image/x-icon",
|
||||
".tiff": "image/tiff",
|
||||
".tif": "image/tiff",
|
||||
".mp4": "video/mp4",
|
||||
".mp3": "audio/mpeg",
|
||||
};
|
||||
|
||||
const mimeType = mimeTypes[ext];
|
||||
|
||||