diff --git a/data/skills/art_skills/chinese_sweet_romance/README.md b/data/skills/art_skills/chinese_sweet_romance/README.md index 7c3c2c7..c597d72 100644 --- a/data/skills/art_skills/chinese_sweet_romance/README.md +++ b/data/skills/art_skills/chinese_sweet_romance/README.md @@ -1,3 +1,6 @@ +123水电费水电费 +123 +123 123 1212121212的王师傅水电费第三方水电费 1212121212 diff --git a/data/skills/story_skills/director_manual/README.md b/data/skills/story_skills/director_manual/README.md new file mode 100644 index 0000000..307d654 --- /dev/null +++ b/data/skills/story_skills/director_manual/README.md @@ -0,0 +1,2 @@ +123实打实地方 +123 \ No newline at end of file diff --git a/data/skills/story_skills/director_manual/art_prompt/narrative_sweet_romance.md b/data/skills/story_skills/director_manual/art_prompt/narrative_sweet_romance.md new file mode 100644 index 0000000..be6e764 --- /dev/null +++ b/data/skills/story_skills/director_manual/art_prompt/narrative_sweet_romance.md @@ -0,0 +1,110 @@ +--- +name: narrative_sweet_romance +description: 叙事手法技法 · 甜宠言情 — 定义甜宠言情类型在主题立意、情感节奏、场景情绪设计与声音方向上的叙事规划方法。适用于任何视觉风格。 +metaData: director_skills +--- + +# 叙事手法 · 甜宠言情 · 技法参考 + +--- + +## 一、主题立意与情感内核 + +### 甜宠言情叙事要点 + +- **含蓄内敛优先** — 情感表达不靠台词铺陈,靠留白与微妙反应。主题立意应偏向克制含蓄,避免直白煽情 +- **甜的克制** — "差一点就碰到"比"黏在一起"更有效。情感主线应设计"欲说还休"的推拉节奏,甜度来自观众自行脑补 +- **以小博大** — 不追求大场面的情绪冲击,用细节打动人:一个眼神、一次欲言又止、一个被风吹乱的衣角 +- **离场感受建议方向** — 心疼 / 意难平 / 怦然心动 / 治愈。避免"爽感""热血"等与甜宠气质不匹配的方向 +- **冷中带暖、疏中见密** — 甜宠不等于甜腻。整体基调可以偏冷、偏疏,但在关键节点释放暖意,反差才是最大的甜 + +--- + +## 二、叙事结构与节奏规划 + +### 甜宠言情叙事要点 + +- **慢是基本功** — 甜宠言情的情感信息密度高(眼神、微表情、肢体距离),需要给观众"感受"的时间。整体节奏偏慢,但不等于拖沓——每个段落都有情感增量 +- **情绪曲线宜缓坡** — 避免"平平平→突然爆发"。用渐进式情绪递进,每个段落比上一个段落情绪浓度高一级 +- **转折点用行动而非台词** — 关键转折点的处理方式应优先考虑行动手段(目光突变、身体距离变化、沉默、道具传递),而非依赖对白解释 +- **段落间用情绪缓冲过渡** — 段落衔接需要情绪缓冲,不要硬切。可用环境空镜、独处片段或日常碎片做呼吸空间 +- **高潮段落的"快"不是剪辑快** — 是情绪密度高。可以用更紧密的景别切换(全身→近景→特写→大特写)制造心跳加速感,而非缩短停留时间 +- **推拉节奏模型** — 甜宠言情的核心引擎是"推拉":靠近→退缩→再靠近→误会→分离→重逢。每一轮推拉都应比上一轮更深入、更痛苦、更甜蜜 + +--- + +## 三、分场景情绪设计 + +### 甜宠言情叙事要点 + +- **情绪目标用具象词** — 不说"开心",说"偷偷心动后的嘴角压不住"。具象的情绪描述能更好地指导景别选择和表演细节 +- **典型情绪段落与设计** — + +| 段落类型 | 情绪方向 | 叙事手法 | 音乐建议 | +|---|---|---|---| +| 初见/亮相 | 惊艳 + 好奇 | 以旁观者视角"发现"对方,先远后近 | 留白,只用环境音制造"屏息"感 | +| 日常暗恋 | 暗涌 + 克制 | 偷看、欲言又止、刻意保持距离 | 轻柔器乐,低音量,衬底 | +| 误会/分离 | 心痛 + 隐忍 | 不解释、转身、独处落泪 | 悲戚独奏,或纯环境音 | +| 坦白/和解 | 释然 + 心动 | 沉默后开口、眼神先于语言 | 从安静到温暖器乐渐入 | +| 升温/暧昧 | 紧张 + 甜蜜 | 物理距离缩短、肢体轻触、呼吸可闻 | 节奏感轻起,暗示心跳 | +| 高甜/大婚 | 幸福 + 庄重 | 仪式感、郑重的对视、承诺 | 丰满器乐,庄重但温柔 | + +- **"距离感"是叙事核心工具** — 用人物间的物理距离映射关系进展: + - **初期**:远景/半身,物理距离大,言语客套 + - **中期**:近景,距离缩短但有阻隔(物件/人群/犹豫) + - **后期**:特写/大特写,零距离,心理防线全部放下 +- **空间元素即情绪隐喻** — 善用场景元素传递情绪,减少对台词的依赖。例如:隔着帘子的模糊身影 = 隔阂;推开门看到满庭花开 = 释然;独坐雨中 = 孤寂 +- **镜头意图写"为什么"而非"怎么拍"** — "用特写是为了让观众看到她眼里的犹豫"优于"用特写拍她的脸"。意图清晰了,分镜自然能选对景别和角度 + +--- + +## 四、声音与音乐方向 + +### 甜宠言情叙事要点 + +- **沉默比配乐更有力** — 关键情感瞬间(对视、泪落、转身离去)优先考虑去掉配乐,只留环境音。甜宠的"甜"往往在沉默后观众自己脑补出来 +- **配乐情绪跟着段落走** — 不逐场配乐,按段落划分给每段定一个音乐情绪基调。同段落内场景切换靠环境音变化过渡,不频繁换曲 +- **避免满配** — 全片配乐覆盖率建议不超过 60%。留白段落的"无声"与配乐段落形成呼吸感 +- **环境音是氛围一半** — 每场戏标注 1-2 个核心环境音,帮助后续音效设计。环境音层次越丰富,场景越有沉浸感 +- **音乐情绪递进模型** — + +| 情绪阶段 | 音乐策略 | 覆盖率 | +|---|---|---| +| 平稳/日常 | 轻柔器乐衬底 | 低 | +| 暗涌/酝酿 | 单一乐器独奏,极低音量 | 中低 | +| 情感爆发 | 器乐渐满或突然静默 | 中高 | +| 命运转折 | 强烈器乐或全场静默 | 极端 | +| 回暖/治愈 | 温暖器乐缓入 | 中 | + +- **甜宠的"心跳感"** — 暧昧升温段落可用轻节奏打击(手鼓、木鱼、拨弦)暗示心跳加速,比直接用甜蜜旋律更高级 + +--- + +## 五、构图与景别叙事 + +### 甜宠言情叙事要点 + +- **三大核心构图的叙事功能** — + - **大量留白** — 孤独/意境/诗意空间,传递角色的心理孤立感或情感留白 + - **框架式构图** — 纱帘/门框/窗棂/屏风后的人影,制造"偷偷看"的暗恋视角与隔阂感 + - **三分法** — 对话/日常/双人互动,稳定均衡,适合日常甜蜜段落 +- **中心构图的限定使用** — 中心构图留给正式亮相、仪式感场景(如大婚、正式告白)。日常不用,否则丧失仪式感的冲击力 +- **空间纵深即叙事** — 前景遮挡(帘/花枝/烟雾)+ 中景主体 + 远景环境,层次越多隔阂感越强;层次越少越亲密 +- **竖构图与横构图** — 单人特写/亮相偏竖构图(强调孤独感与身形气质);双人/场景偏横构图(强调关系与共处空间) +- **甜宠景别递进** — 同场戏内景别应随情感升温递进:半身→近景→特写→大特写。不要一上来就怼特写,留出情绪上升空间 +- **大特写要有理由** — 大特写(眼/唇/手)是情绪核弹,一集用 2-3 次足够。滥用会让观众疲劳 +- **远景不是过场** — 远景镜头本身就有叙事价值(孤独感、空间压迫、季节氛围)。给远景足够时长(4-6s),别急着切走 + +--- + +## 六、镜头运动与节奏 + +### 甜宠言情叙事要点 + +- **以静制动为主** — 60% 以上镜头应为静止机位,让画面细节和情绪自己说话 +- **缓推 = 靠近/心动** — "观众靠近角色"的心理暗示,适合心动、发现、窥视 +- **缓拉 = 抽离/孤独** — "观众退开"的心理暗示,适合离别、孤独、揭示全貌 +- **快切碎剪不兼容** — 快速剪辑与甜宠言情的气质不兼容。即使在高潮段落,也应通过景别递进而非快切来制造节奏感 +- **摇镜与跟镜** — 慢摇适合展示场景全貌或追随角色行走;跟镜适合仪式/行走场景。速度均应克制 +- **运镜即情绪** — 镜头运动不是技术选择,是情绪选择。静止 = 沉稳/压抑;缓推 = 靠近/心动;缓拉 = 抽离/孤独;缓摇 = 展示/庄重 +- **甜宠"心跳运镜"** — 暧昧升温段落可用微幅缓推配合景别递进(半身→近景→特写),模拟心跳加速时"注意力收窄"的生理感受 diff --git a/data/skills/story_skills/director_manual/art_prompt/storyboard_table_narrative.md b/data/skills/story_skills/director_manual/art_prompt/storyboard_table_narrative.md new file mode 100644 index 0000000..e768f6e --- /dev/null +++ b/data/skills/story_skills/director_manual/art_prompt/storyboard_table_narrative.md @@ -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)做情绪缓冲 +- **段落切换可用叠化/淡入淡出** — 大段落间的情绪跳跃用柔性转场,避免观众出戏 diff --git a/data/skills/story_skills/director_manual/images/ed2fcc56-0069-4666-beea-6b50d9648896.jpg b/data/skills/story_skills/director_manual/images/ed2fcc56-0069-4666-beea-6b50d9648896.jpg new file mode 100644 index 0000000..1d0cd85 Binary files /dev/null and b/data/skills/story_skills/director_manual/images/ed2fcc56-0069-4666-beea-6b50d9648896.jpg differ diff --git a/src/routes/assetsGenerate/batchPolishAssetsPrompt.ts b/src/routes/assetsGenerate/batchPolishAssetsPrompt.ts index 79a375a..898f3b5 100644 --- a/src/routes/assetsGenerate/batchPolishAssetsPrompt.ts +++ b/src/routes/assetsGenerate/batchPolishAssetsPrompt.ts @@ -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; diff --git a/src/routes/assetsGenerate/polishAssetsPrompt.ts b/src/routes/assetsGenerate/polishAssetsPrompt.ts index ee96a7d..e1914bb 100644 --- a/src/routes/assetsGenerate/polishAssetsPrompt.ts +++ b/src/routes/assetsGenerate/polishAssetsPrompt.ts @@ -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; diff --git a/src/routes/production/assets/batchGenerateAssetsImage.ts b/src/routes/production/assets/batchGenerateAssetsImage.ts index dd54ac8..40b13da 100644 --- a/src/routes/production/assets/batchGenerateAssetsImage.ts +++ b/src/routes/production/assets/batchGenerateAssetsImage.ts @@ -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 = { role: { prompt: rolePrompt, diff --git a/src/routes/production/workbench/generateVideo.ts b/src/routes/production/workbench/generateVideo.ts index aae2273..c24c12b 100644 --- a/src/routes/production/workbench/generateVideo.ts +++ b/src/routes/production/workbench/generateVideo.ts @@ -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`; //视频保存路径 diff --git a/src/routes/production/workbench/generateVideoPrompt.ts b/src/routes/production/workbench/generateVideoPrompt.ts index f1990f6..ab9b3b1 100644 --- a/src/routes/production/workbench/generateVideoPrompt.ts +++ b/src/routes/production/workbench/generateVideoPrompt.ts @@ -19,9 +19,11 @@ 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},${visualManual},${directorManual}`, messages: [ { role: "user", diff --git a/src/routes/production/workbench/getGenerateData.ts b/src/routes/production/workbench/getGenerateData.ts index 2139de2..4a35c7c 100644 --- a/src/routes/production/workbench/getGenerateData.ts +++ b/src/routes/production/workbench/getGenerateData.ts @@ -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) diff --git a/src/routes/production/workbench/selectVideo.ts b/src/routes/production/workbench/selectVideo.ts index e23e06c..fa94e52 100644 --- a/src/routes/production/workbench/selectVideo.ts +++ b/src/routes/production/workbench/selectVideo.ts @@ -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: "视频选择成功" })); }, diff --git a/src/routes/project/addDirectorManual.ts b/src/routes/project/addDirectorManual.ts index 1d55b4e..a9f6a40 100644 --- a/src/routes/project/addDirectorManual.ts +++ b/src/routes/project/addDirectorManual.ts @@ -30,15 +30,9 @@ export default router.post( data: { label: string; value: string; data: string }[]; directorManual: string; }; - // 不允许纯数字 - if (/^\d+$/.test(directorManual)) { - res.status(400).send(error("文件名称不能为纯数字")); - return; - } - - // 不允许有符号(只允许中文、字母和数字) - if (!/^[\u4e00-\u9fa5a-zA-Z0-9]+$/.test(directorManual)) { - res.status(400).send(error("文件名称不能包含符号")); + // 安全校验:不允许包含路径分隔符、纯数字,防止越级删除或误删项目目录 + if (name.includes("/") || name.includes("\\") || name === "." || name === ".." || /^\d+$/.test(name)) { + res.status(400).send(error("名称不能包含路径分隔符或为纯数字")); return; } @@ -49,8 +43,8 @@ export default router.post( // 字段映射表(与 getVisualManual 保持一致) const DATA_MAP: { value: string; subDir?: string }[] = [ { value: "README" }, - { value: "art_directorPlanning", subDir: "art_prompt" }, - { value: "art_storyboard", subDir: "art_prompt" }, + { 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 ?? ""])); diff --git a/src/routes/project/addProject.ts b/src/routes/project/addProject.ts index c1a8748..9d50ac7 100644 --- a/src/routes/project/addProject.ts +++ b/src/routes/project/addProject.ts @@ -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, diff --git a/src/routes/project/addVisualManual.ts b/src/routes/project/addVisualManual.ts index ead3b97..b185614 100644 --- a/src/routes/project/addVisualManual.ts +++ b/src/routes/project/addVisualManual.ts @@ -31,11 +31,11 @@ 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_skills", stylePath]); if (fs.existsSync(mainPath)) { return res.status(400).send(error("请勿填写重复名称的视觉手册")); diff --git a/src/routes/project/editDirectorlManual.ts b/src/routes/project/editDirectorlManual.ts index 22062ce..46fc5b0 100644 --- a/src/routes/project/editDirectorlManual.ts +++ b/src/routes/project/editDirectorlManual.ts @@ -31,15 +31,9 @@ export default router.post( data: { label: string; value: string; data: string }[]; }; - // 不允许纯数字 - if (/^\d+$/.test(directorManual)) { - res.status(400).send(error("文件名称不能为纯数字")); - return; - } - - // 不允许有符号(只允许中文、字母和数字) - if (!/^[\u4e00-\u9fa5a-zA-Z0-9]+$/.test(directorManual)) { - res.status(400).send(error("文件名称不能包含符号")); + // 安全校验:不允许包含路径分隔符、纯数字,防止越级删除或误删项目目录 + if (name.includes("/") || name.includes("\\") || name === "." || name === ".." || /^\d+$/.test(name)) { + res.status(400).send(error("名称不能包含路径分隔符或为纯数字")); return; } @@ -50,8 +44,8 @@ export default router.post( // 字段映射表(与 getVisualManual 保持一致) const DATA_MAP: { value: string; subDir?: string }[] = [ { value: "README" }, - { value: "art_directorPlanning", subDir: "art_prompt" }, - { value: "art_storyboard", subDir: "art_prompt" }, + { 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 ?? ""])); diff --git a/src/routes/project/editProject.ts b/src/routes/project/editProject.ts index e1ec46e..f28bb4e 100644 --- a/src/routes/project/editProject.ts +++ b/src/routes/project/editProject.ts @@ -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, diff --git a/src/routes/project/editVisualManual.ts b/src/routes/project/editVisualManual.ts index 589599f..efbaa09 100644 --- a/src/routes/project/editVisualManual.ts +++ b/src/routes/project/editVisualManual.ts @@ -31,8 +31,9 @@ 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; } diff --git a/src/routes/project/queryDirectorManual.ts b/src/routes/project/queryDirectorManual.ts index e64c9a9..d4cdbff 100644 --- a/src/routes/project/queryDirectorManual.ts +++ b/src/routes/project/queryDirectorManual.ts @@ -8,8 +8,8 @@ const router = express.Router(); // 字段映射表 const DATA_MAP: { label: string; value: string; subDir?: string }[] = [ { label: "README", value: "README" }, - { label: "导演规划", value: "art_directorPlanning", subDir: "art_prompt" }, - { label: "分镜表", value: "art_storyboard", subDir: "art_prompt" }, + { label: "导演规划", value: "narrative_sweet_romance", subDir: "art_prompt" }, + { label: "分镜表", value: "storyboard_table_narrative", subDir: "art_prompt" }, ]; // 读取 md 文件内容,文件不存在时返回空字符串 diff --git a/src/utils/getArtPrompt.ts b/src/utils/getArtPrompt.ts index 1b6d679..da0af90 100644 --- a/src/utils/getArtPrompt.ts +++ b/src/utils/getArtPrompt.ts @@ -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 { - const baseDir = getPath(["skills", "art_prompts", styleName]); +export function getAllArtPrompts(styleName: string, source: string): Record { + const baseDir = getPath(["skills", source, styleName]); if (!fs.existsSync(baseDir)) { return {}; diff --git a/src/utils/oss.ts b/src/utils/oss.ts index 8ae4927..370bac7 100644 --- a/src/utils/oss.ts +++ b/src/utils/oss.ts @@ -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];