refactor(*): 稳定期治理 - 收敛核心架构、错误语义并修复跨项目数据竞态

This commit is contained in:
ddCat committed 2026-09-09 22:02:10 +08:00
1 parent bacd502f5c
commit b5f3413ded
298 files changed
+12681 -26428

No files matched your search

+127
View File
@@ -0,0 +1,127 @@
import type { CanvasGenerationMode } from "@/types/canvas";
import type { SrtEntry } from "@/types/timeline";
export type CanvasPromptPreset = {
id: string;
name: string;
description: string;
prompt: string;
modes: CanvasGenerationMode[];
source: "builtin" | "skill";
};
export const CANVAS_BUILTIN_PRESETS: CanvasPromptPreset[] = [
{
id: "character-sheet",
name: "角色设定图",
description: "正面、侧面、背面与表情参考,锁定角色一致性",
prompt: "生成角色设定图:保持同一角色身份、五官、发型、服装和体态一致,包含正面、侧面、背面和关键表情参考,背景简洁,便于后续镜头复用。",
modes: ["image"],
source: "builtin",
},
{
id: "multi-angle",
name: "多机位视角",
description: "围绕同一主体生成连续、可衔接的机位变化",
prompt: "围绕同一主体设计多机位画面,保持人物、服装、场景和光线一致,分别给出远景、全景、中景、近景、特写、侧面、背面和俯拍视角,镜头之间具有连续性。",
modes: ["image", "video"],
source: "builtin",
},
{
id: "next-shot",
name: "画面推演",
description: "推演当前画面的前后动作与镜头衔接",
prompt: "基于当前画面推演下一个连续镜头:保持角色和场景一致,明确主体接下来的动作、视线、环境变化、镜头运动和自然衔接方式,不要跳变构图或身份。",
modes: ["image", "video"],
source: "builtin",
},
{
id: "story-beats",
name: "连续镜头",
description: "将短剧情拆成可生成的连续镜头节拍",
prompt: "把这段内容拆成连续镜头节拍。每个镜头写清主体动作、景别、构图、机位、运镜、光线、情绪和与前后镜头的衔接,并保持角色、场景和道具一致。",
modes: ["text", "image", "video"],
source: "builtin",
},
{
id: "cinematic-light",
name: "电影光影优化",
description: "保留内容,优化真实光线、层次和融合感",
prompt: "保留主体身份、动作和原始构图,优化为真实电影摄影光线:明确主光方向、环境反射、阴影层次、肤色和背景融合,降低塑料感与过度锐化,不改变画面内容。",
modes: ["image", "video"],
source: "builtin",
},
{
id: "video-prompt",
name: "视频提示词优化",
description: "整理为模型更容易执行的时序化镜头指令",
prompt: "将当前要求改写为结构化视频提示词,按时间顺序描述开场画面、主体动作、镜头运动、环境变化、声音和结束画面;消除冲突指令,保留所有关键约束。",
modes: ["text", "video"],
source: "builtin",
},
];
export const IMAGE_PROMPT_REVERSE = `请根据参考图片反推一段适合用于 AI 生图的提示词。
要求:
1. 只输出提示词正文,不要解释。
2. 覆盖主体、构图、风格、光线、色彩、材质、镜头和氛围。
3. 尽量写成可直接用于生图模型的完整提示词。`;
export function workflowStarterPrompt(kind: "character_cards" | "character_three_view" | "storyboard_video", title: string, workflowTitle: string) {
if (kind === "character_cards") return `请基于「${workflowTitle}」拆分主要角色,并为每个角色生成可用于后续创作的角色图片卡片:外观、服饰、身份、性格和视觉辨识点。`;
if (kind === "character_three_view") return `请基于上游角色卡片生成「${title}」:同一角色的正面、侧面、背面三视图,保持服饰、发型、道具和比例一致。`;
return `请基于上游角色三视图,为「${workflowTitle}」制作分镜剧情视频方案:包含镜头顺序、景别、动作、节奏和画面连续性。`;
}
export function buildTextRewritePrompt(sourceText: string, instruction: string) {
return `请根据要求修改以下文本。\n\n原文:\n${sourceText}\n\n修改要求:\n${instruction}`;
}
export function buildVideoOperationPrompt(operation: string, prompt: string, operationLabel: string) {
if (operation === "compare_versions") return `请对以下视频结果版本做对比分析,输出推荐版本、差异点和修改建议:\n${prompt}`;
return `视频编辑任务:${operationLabel}\n创作要求:${prompt}`;
}
export function buildSubtitleHighlightSystemPrompt() {
return `你是一个视频字幕关键词高亮助手。请输出严格 JSON。
任务目标:
- 为每条字幕判断是否需要高亮
- 每条字幕最多高亮 1 段
- 只返回最值得高亮的一个关键词或短语
- start 和 end 使用 JavaScript 字符串下标
- end 为 exclusive
- 没有明确重点时返回 shouldHighlight=false
高亮标准:
- 优先高亮结论词、数字、身份词、首次/突破词、强反差词
- 不要高亮虚词、口头语、整句长短语
边界约束:
- 不负责颜色、字号、位置、动效
- 只返回 highlights 数组
- highlightText 必须严格等于原字幕文本切片结果`;
}
export function buildSubtitleHighlightUserMessage(entries: SrtEntry[]) {
const lines = entries.map((entry) => {
return `entryIndex=${entry.index} | startMs=${entry.startMs} | endMs=${entry.endMs} | text=${entry.text}`;
});
return `请根据以下字幕和上下文,为每条字幕判断是否需要高亮,并返回 JSON:
{
"highlights": [
{
"entryIndex": 1,
"shouldHighlight": true,
"highlightText": "示例",
"start": 0,
"end": 2
}
]
}
字幕列表:
${lines.join("\n")}`;
}
+22
View File
@@ -0,0 +1,22 @@
export {
PromptTemplateOperation,
promptTemplateTaskPlaceholder,
type PromptTemplateOperationId,
} from "./operations";
export {
CANVAS_BUILTIN_PRESETS,
IMAGE_PROMPT_REVERSE,
buildSubtitleHighlightSystemPrompt,
buildSubtitleHighlightUserMessage,
buildTextRewritePrompt,
buildVideoOperationPrompt,
workflowStarterPrompt,
type CanvasPromptPreset,
} from "./catalog";
export {
parseGeneratedStory,
shortDramaOutlineVariables,
storyTitleFromAnswer,
type GeneratedShortDramaChapter,
type GeneratedShortDramaOutline,
} from "./short-drama-outline";
+17
View File
@@ -0,0 +1,17 @@
export const PromptTemplateOperation = {
ChapterAssetsExtract: "chapter_assets_extract",
CharacterExtract: "character_extract",
CharacterTurnaround: "character_turnaround",
StoryboardPlan: "storyboard_plan",
StoryboardRepair: "storyboard_repair",
StoryboardFirstFrame: "storyboard_first_frame",
StoryboardVideo: "storyboard_video",
ShortDramaOutline: "short_drama_outline",
SkillDraft: "skill_draft",
} as const;
export type PromptTemplateOperationId = (typeof PromptTemplateOperation)[keyof typeof PromptTemplateOperation];
export function promptTemplateTaskPlaceholder(label: string) {
return `使用当前启用的${label}模板。`;
}
@@ -0,0 +1,57 @@
export type GeneratedShortDramaChapter = {
title: string;
content: string;
};
export type GeneratedShortDramaOutline = {
title: string;
synopsis: string;
chapters: GeneratedShortDramaChapter[];
};
export function parseGeneratedStory(answer: string): GeneratedShortDramaOutline {
const cleaned = answer
.replace(/```json/gi, "")
.replace(/```/g, "")
.trim();
const match = cleaned.match(/\{[\s\S]*\}/);
const payload = match ? JSON.parse(match[0]) as Record<string, unknown> : {};
const title = String(payload.title || "").trim();
const synopsis = String(payload.synopsis || "").trim();
const chapters = Array.isArray(payload.chapters)
? payload.chapters
.map((chapter: unknown) => {
const item = typeof chapter === "object" && chapter ? chapter as Record<string, unknown> : {};
return { title: String(item.title || "").trim(), content: String(item.content || "").trim() };
})
.filter((chapter: GeneratedShortDramaChapter) => chapter.title && chapter.content)
: [];
return { title: title || storyTitleFromAnswer(answer), synopsis, chapters };
}
export function storyTitleFromAnswer(answer: string) {
const line = answer.split(/\r?\n/).find((item) => item.trim());
return line ? line.trim().slice(0, 24) : "AI 生成短剧";
}
export function shortDramaOutlineVariables(input: {
story: string;
chapterCount: string;
structure: string;
wordCount: string;
perspective: string;
tone: string;
characterScale: string;
chapterLength: string;
}) {
return {
用户故事: input.story,
章节数量: input.chapterCount,
叙事结构: input.structure,
每章字数: input.wordCount,
叙事视角: input.perspective,
整体基调: input.tone,
角色规模: input.characterScale,
章节篇幅: input.chapterLength,
};
}