Your Name e054dcabcc
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: update canvas and flow board features
2026-04-11 22:28:18 +08:00

762 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { CanvasNode, NodeType, Position } from "@/types/canvas";
const DEFAULT_VIDEO_WORKFLOW_TEXT_COLOR = "#111827";
export type VideoWorkflowSectionId =
| "input"
| "analysis"
| "storyboard"
| "generation"
| "character"
| "tool"
| "output";
export type VideoWorkflowTemplateType =
| "camera-movement"
| "canvas-node"
| "character-description"
| "create-character"
| "create-scene"
| "custom-agent"
| "doodle-canvas"
| "extract-characters-scenes"
| "gen-image"
| "gen-music"
| "gen-video"
| "generate-character-image"
| "generate-character-video"
| "generate-scene-image"
| "generate-scene-video"
| "image-compare"
| "inpaint-crop"
| "inpaint-stitch"
| "input-image"
| "jimeng-super-resolution"
| "local-save"
| "motion-control"
| "novel-input"
| "preview"
| "professional-camera"
| "rh-app"
| "rh-comfy"
| "scene-description"
| "storyboard-chart-node"
| "storyboard-node"
| "table-editor-node"
| "text-node"
| "video-analyze"
| "video-input";
export interface VideoWorkflowQuickConnectOption {
type: VideoWorkflowTemplateType;
label: string;
}
export interface VideoWorkflowNodeDefinition {
type: VideoWorkflowTemplateType;
label: string;
description: string;
group: VideoWorkflowSectionId;
size: {
width: number;
height: number;
};
canvasNodeType: NodeType;
accentColor: string;
quickConnectOutputs: VideoWorkflowQuickConnectOption[];
}
export interface VideoWorkflowSectionDefinition {
id: VideoWorkflowSectionId;
label: string;
description: string;
}
export interface VideoWorkflowStarterTemplate {
id: string;
title: string;
description: string;
nodes: VideoWorkflowTemplateType[];
}
const QUICK_CONNECT_CONFIG: Partial<
Record<VideoWorkflowTemplateType, VideoWorkflowQuickConnectOption[]>
> = {
"text-node": [
{ type: "gen-image", label: "绘画" },
{ type: "gen-video", label: "视频" },
{ type: "custom-agent", label: "Agent" },
],
"input-image": [
{ type: "gen-image", label: "绘画" },
{ type: "gen-video", label: "视频" },
{ type: "storyboard-node", label: "分镜视表" },
{ type: "inpaint-crop", label: "裁剪局部重绘" },
],
"novel-input": [
{ type: "custom-agent", label: "Agent" },
{ type: "text-node", label: "文本" },
{ type: "rh-app", label: "RH 应用" },
],
"video-input": [
{ type: "preview", label: "预览" },
{ type: "rh-app", label: "RH 应用" },
],
"video-analyze": [
{ type: "storyboard-node", label: "分镜视表" },
{ type: "gen-image", label: "绘画" },
],
"table-editor-node": [{ type: "storyboard-node", label: "分镜视表" }],
"storyboard-node": [
{ type: "gen-image", label: "绘画" },
{ type: "gen-video", label: "视频" },
],
"storyboard-chart-node": [{ type: "preview", label: "预览" }],
"inpaint-stitch": [
{ type: "preview", label: "预览" },
{ type: "local-save", label: "保存" },
],
};
const SECTION_DEFINITIONS: VideoWorkflowSectionDefinition[] = [
{ id: "input", label: "输入", description: "脚本、图片、视频和小说素材入口" },
{ id: "analysis", label: "分析", description: "拆解素材、抽取镜头和角色语义" },
{ id: "storyboard", label: "分镜", description: "组织镜头表、图表和镜头语言" },
{ id: "generation", label: "生成", description: "图片、视频、音乐和增强节点" },
{ id: "character", label: "角色场景", description: "角色与场景资产沉淀和派生" },
{ id: "tool", label: "工具", description: "代理、画板、局部编辑和外部应用" },
{ id: "output", label: "输出", description: "预览、对比和保存结果" },
];
const COMPACT_NODE_SIZE_OVERRIDES: Partial<
Record<VideoWorkflowTemplateType, { width: number; height: number }>
> = {
"text-node": { width: 300, height: 170 },
"novel-input": { width: 340, height: 240 },
"input-image": { width: 220, height: 220 },
"video-input": { width: 240, height: 220 },
"video-analyze": { width: 300, height: 210 },
"extract-characters-scenes": { width: 300, height: 210 },
"storyboard-node": { width: 380, height: 240 },
"storyboard-chart-node": { width: 360, height: 220 },
"table-editor-node": { width: 340, height: 220 },
"gen-image": { width: 280, height: 190 },
"gen-video": { width: 280, height: 190 },
"gen-music": { width: 300, height: 200 },
"jimeng-super-resolution": { width: 260, height: 160 },
"motion-control": { width: 300, height: 220 },
"inpaint-crop": { width: 280, height: 180 },
"inpaint-stitch": { width: 280, height: 180 },
"professional-camera": { width: 280, height: 180 },
"camera-movement": { width: 280, height: 180 },
"image-compare": { width: 320, height: 220 },
preview: { width: 320, height: 220 },
"local-save": { width: 260, height: 180 },
"character-description": { width: 300, height: 180 },
"scene-description": { width: 300, height: 180 },
"create-character": { width: 280, height: 160 },
"create-scene": { width: 280, height: 160 },
"generate-character-image": { width: 300, height: 200 },
"generate-character-video": { width: 300, height: 200 },
"generate-scene-image": { width: 300, height: 200 },
"generate-scene-video": { width: 300, height: 200 },
"custom-agent": { width: 340, height: 220 },
"rh-app": { width: 280, height: 180 },
"rh-comfy": { width: 280, height: 180 },
"canvas-node": { width: 360, height: 240 },
"doodle-canvas": { width: 300, height: 180 },
};
function resolveCompactNodeSize(
type: VideoWorkflowTemplateType,
canvasNodeType: NodeType,
size: { width: number; height: number }
) {
const override = COMPACT_NODE_SIZE_OVERRIDES[type];
if (override) {
return override;
}
if (canvasNodeType === "workflow") {
return {
width: Math.min(size.width, 300),
height: Math.min(size.height, 180),
};
}
if (canvasNodeType === "text") {
return {
width: Math.min(size.width, 320),
height: Math.min(size.height, 180),
};
}
if (canvasNodeType === "image") {
return {
width: Math.min(size.width, 220),
height: Math.min(size.height, 220),
};
}
if (canvasNodeType === "video") {
return {
width: Math.min(size.width, 240),
height: Math.min(size.height, 220),
};
}
return size;
}
const createDefinition = (
type: VideoWorkflowTemplateType,
label: string,
description: string,
group: VideoWorkflowSectionId,
size: { width: number; height: number },
canvasNodeType: NodeType,
accentColor: string
): VideoWorkflowNodeDefinition => ({
type,
label,
description,
group,
size: resolveCompactNodeSize(type, canvasNodeType, size),
canvasNodeType,
accentColor,
quickConnectOutputs: QUICK_CONNECT_CONFIG[type] || [],
});
export const VIDEO_WORKFLOW_NODE_DEFINITIONS: Record<
VideoWorkflowTemplateType,
VideoWorkflowNodeDefinition
> = {
"camera-movement": createDefinition(
"camera-movement",
"镜头运动",
"规划推拉摇移、运镜节奏和镜头情绪。",
"storyboard",
{ width: 340, height: 480 },
"workflow",
"#fde68a"
),
"canvas-node": createDefinition(
"canvas-node",
"画板节点",
"把多个素材汇总到一个创意画板里继续编排。",
"tool",
{ width: 600, height: 500 },
"workflow",
"#e2e8f0"
),
"character-description": createDefinition(
"character-description",
"角色描述",
"沉淀角色外观、气质和动作特征。",
"character",
{ width: 400, height: 400 },
"workflow",
"#dbeafe"
),
"create-character": createDefinition(
"create-character",
"创建角色",
"将角色设定固化成可复用的角色资产。",
"character",
{ width: 350, height: 300 },
"workflow",
"#dbeafe"
),
"create-scene": createDefinition(
"create-scene",
"创建场景",
"将场景描述固化成可复用的世界观素材。",
"character",
{ width: 350, height: 300 },
"workflow",
"#dbeafe"
),
"custom-agent": createDefinition(
"custom-agent",
"自定义代理",
"接入一段更复杂的代理链路处理创作任务。",
"tool",
{ width: 500, height: 800 },
"workflow",
"#e2e8f0"
),
"doodle-canvas": createDefinition(
"doodle-canvas",
"涂鸦画板",
"用草图、路径或遮罩表达额外视觉意图。",
"tool",
{ width: 500, height: 300 },
"workflow",
"#e2e8f0"
),
"extract-characters-scenes": createDefinition(
"extract-characters-scenes",
"提取角色场景",
"从长文本里拆出角色、地点和关系线索。",
"analysis",
{ width: 400, height: 500 },
"workflow",
"#bbf7d0"
),
"gen-image": createDefinition(
"gen-image",
"生成图片",
"根据上游脚本、分镜或素材生成关键帧。",
"generation",
{ width: 360, height: 340 },
"workflow",
"#fecaca"
),
"gen-music": createDefinition(
"gen-music",
"生成音乐",
"为视频节奏匹配音乐、氛围和情绪。",
"generation",
{ width: 350, height: 700 },
"workflow",
"#fecaca"
),
"gen-video": createDefinition(
"gen-video",
"生成视频",
"根据文本、图像或分镜节点生成视频镜头。",
"generation",
{ width: 360, height: 340 },
"workflow",
"#fecaca"
),
"generate-character-image": createDefinition(
"generate-character-image",
"生成角色图片",
"围绕角色设定输出稳定的角色图像。",
"character",
{ width: 400, height: 450 },
"workflow",
"#dbeafe"
),
"generate-character-video": createDefinition(
"generate-character-video",
"生成角色视频",
"围绕角色设定输出角色镜头或动作片段。",
"character",
{ width: 400, height: 450 },
"workflow",
"#dbeafe"
),
"generate-scene-image": createDefinition(
"generate-scene-image",
"生成场景图片",
"围绕场景设定生成环境和氛围关键帧。",
"character",
{ width: 400, height: 450 },
"workflow",
"#dbeafe"
),
"generate-scene-video": createDefinition(
"generate-scene-video",
"生成场景视频",
"围绕场景设定生成世界观或转场镜头。",
"character",
{ width: 400, height: 450 },
"workflow",
"#dbeafe"
),
"image-compare": createDefinition(
"image-compare",
"图片对比",
"并排比较不同版本画面与细节变化。",
"output",
{ width: 400, height: 300 },
"workflow",
"#bae6fd"
),
"inpaint-crop": createDefinition(
"inpaint-crop",
"裁剪局部重绘",
"选定区域后做局部改图、补图或定点修改。",
"tool",
{ width: 340, height: 380 },
"workflow",
"#e2e8f0"
),
"inpaint-stitch": createDefinition(
"inpaint-stitch",
"无缝拼回",
"把局部重绘结果回贴到原始画面里。",
"tool",
{ width: 340, height: 380 },
"workflow",
"#e2e8f0"
),
"input-image": createDefinition(
"input-image",
"图片输入",
"上传参考图、角色图、首帧或关键帧素材。",
"input",
{ width: 260, height: 260 },
"image",
"#bfdbfe"
),
"jimeng-super-resolution": createDefinition(
"jimeng-super-resolution",
"智能超清",
"对结果做分辨率增强和画质修复。",
"generation",
{ width: 320, height: 280 },
"workflow",
"#fecaca"
),
"local-save": createDefinition(
"local-save",
"本地保存",
"将流程输出整理并保存到本地项目目录。",
"output",
{ width: 320, height: 380 },
"workflow",
"#bae6fd"
),
"motion-control": createDefinition(
"motion-control",
"动作迁移",
"把动作参考迁移到主体或镜头中。",
"generation",
{ width: 320, height: 580 },
"workflow",
"#fecaca"
),
"novel-input": createDefinition(
"novel-input",
"小说输入",
"粘贴长篇设定、故事梗概或分章节剧情。",
"input",
{ width: 400, height: 500 },
"text",
"#bfdbfe"
),
"preview": createDefinition(
"preview",
"预览节点",
"检查上游视频、图片或分镜输出效果。",
"output",
{ width: 440, height: 310 },
"workflow",
"#bae6fd"
),
"professional-camera": createDefinition(
"professional-camera",
"专业镜头",
"补充焦段、景别和镜头语言规则。",
"storyboard",
{ width: 320, height: 420 },
"workflow",
"#fde68a"
),
"rh-app": createDefinition(
"rh-app",
"RH 应用",
"跳转外部 RH 应用执行专门流程。",
"tool",
{ width: 340, height: 420 },
"workflow",
"#e2e8f0"
),
"rh-comfy": createDefinition(
"rh-comfy",
"RH Comfy",
"挂接 Comfy 风格的外部工作流节点。",
"tool",
{ width: 340, height: 420 },
"workflow",
"#e2e8f0"
),
"scene-description": createDefinition(
"scene-description",
"场景描述",
"沉淀空间氛围、材质和布光信息。",
"character",
{ width: 400, height: 400 },
"workflow",
"#dbeafe"
),
"storyboard-chart-node": createDefinition(
"storyboard-chart-node",
"分镜图表",
"把分镜节点转成更适合检查的图表视图。",
"storyboard",
{ width: 540, height: 480 },
"workflow",
"#fde68a"
),
"storyboard-node": createDefinition(
"storyboard-node",
"分镜节点",
"组织镜头表、时长、画面意图和镜头顺序。",
"storyboard",
{ width: 600, height: 500 },
"workflow",
"#fde68a"
),
"table-editor-node": createDefinition(
"table-editor-node",
"表格编辑器",
"先用结构化表格整理分镜字段再输出。",
"storyboard",
{ width: 520, height: 420 },
"workflow",
"#fde68a"
),
"text-node": createDefinition(
"text-node",
"文本节点",
"输入脚本、镜头、旁白和视频需求。",
"input",
{ width: 360, height: 280 },
"text",
"#bfdbfe"
),
"video-analyze": createDefinition(
"video-analyze",
"视频分析",
"解析视频节奏、镜头结构和可复用画面信息。",
"analysis",
{ width: 400, height: 500 },
"workflow",
"#bbf7d0"
),
"video-input": createDefinition(
"video-input",
"视频输入",
"导入素材视频、参考视频或上游成片。",
"input",
{ width: 360, height: 420 },
"video",
"#bfdbfe"
),
};
export const VIDEO_WORKFLOW_LIBRARY_SECTIONS = SECTION_DEFINITIONS;
export const VIDEO_WORKFLOW_STARTER_TEMPLATES: VideoWorkflowStarterTemplate[] = [
{
id: "script-to-video",
title: "脚本成片",
description: "从脚本文案进入,经过分镜和视频生成,最后预览并保存。",
nodes: ["text-node", "storyboard-node", "gen-video", "preview", "local-save"],
},
{
id: "image-to-video",
title: "图片转视频",
description: "从参考图启动视频生成,适合首帧、角色图和海报成片。",
nodes: ["input-image", "gen-video", "preview", "local-save"],
},
{
id: "video-analysis-remix",
title: "视频拆解再创作",
description: "先解析已有视频,再回到分镜和生图/生视频链路。",
nodes: ["video-input", "video-analyze", "storyboard-node", "gen-video", "preview"],
},
{
id: "novel-character-pipeline",
title: "小说角色拆解",
description: "从长文本提取角色与场景,再衍生角色视频。",
nodes: [
"novel-input",
"extract-characters-scenes",
"create-character",
"generate-character-video",
"preview",
],
},
{
id: "inpaint-repair",
title: "局部重绘回贴",
description: "对参考图局部修改后拼回,再预览和保存。",
nodes: ["input-image", "inpaint-crop", "inpaint-stitch", "preview", "local-save"],
},
];
export function getVideoWorkflowNodeDefinition(
type: VideoWorkflowTemplateType | string | null | undefined
) {
if (!type) return null;
return VIDEO_WORKFLOW_NODE_DEFINITIONS[type as VideoWorkflowTemplateType] || null;
}
export function getVideoWorkflowSectionNodes(sectionId: VideoWorkflowSectionId) {
return Object.values(VIDEO_WORKFLOW_NODE_DEFINITIONS).filter(
(definition) => definition.group === sectionId
);
}
export function getVideoWorkflowQuickConnectOutputs(type: VideoWorkflowTemplateType) {
return VIDEO_WORKFLOW_NODE_DEFINITIONS[type]?.quickConnectOutputs || [];
}
export function inferVideoWorkflowTemplateType(
node: CanvasNode | null | undefined
): VideoWorkflowTemplateType | null {
if (!node) return null;
const rawTemplateType =
typeof (node.data as { templateType?: string; metadata?: Record<string, unknown> })?.templateType ===
"string"
? String((node.data as { templateType?: string }).templateType).trim()
: typeof (node.data as { metadata?: Record<string, unknown> })?.metadata?.templateType ===
"string"
? String((node.data as { metadata?: Record<string, unknown> }).metadata?.templateType).trim()
: "";
if (rawTemplateType && rawTemplateType in VIDEO_WORKFLOW_NODE_DEFINITIONS) {
return rawTemplateType as VideoWorkflowTemplateType;
}
if (node.type === "text") {
return "text-node";
}
if (node.type === "image") {
return "input-image";
}
if (node.type === "video") {
return "video-input";
}
return null;
}
export function buildVideoWorkflowCanvasNode(
type: VideoWorkflowTemplateType,
position: Position
): Omit<CanvasNode, "id" | "createdAt" | "updatedAt"> {
const definition = getVideoWorkflowNodeDefinition(type);
if (!definition) {
throw new Error(`Unknown video workflow node type: ${type}`);
}
const metadata = {
templateType: definition.type,
label: definition.label,
description: definition.description,
};
if (definition.canvasNodeType === "text") {
const isNovelInput = type === "novel-input";
return {
type: "text",
position,
size: definition.size,
data: {
content: isNovelInput
? "在这里粘贴长篇剧情、人物关系、世界观设定或章节梗概"
: "输入脚本、镜头、画面意图或视频需求",
fontSize: isNovelInput ? 16 : 15,
color: DEFAULT_VIDEO_WORKFLOW_TEXT_COLOR,
lineHeight: 1.5,
borderWidth: 1,
borderColor: "#d4d4d8",
borderRadius: 20,
padding: 18,
backgroundColor: "#ffffff",
metadata,
},
connections: [],
};
}
if (definition.canvasNodeType === "image") {
return {
type: "image",
position,
size: definition.size,
data: {
src: "",
status: "pending",
title: definition.label,
prompt: definition.description,
note: "上传参考图、角色图、海报或关键帧",
metadata,
},
connections: [],
};
}
if (definition.canvasNodeType === "video") {
return {
type: "video",
position,
size: definition.size,
data: {
src: "",
status: "pending",
prompt: "上传视频素材、参考视频,或作为上游输出接收节点",
metadata,
},
connections: [],
};
}
return {
type: "workflow",
position,
size: definition.size,
data: {
templateType: definition.type,
title: definition.label,
description: definition.description,
summary:
definition.quickConnectOutputs.length > 0
? `可接:${definition.quickConnectOutputs
.map((item) => item.label)
.join(" / ")}`
: "当前节点用于处理中间结果或输出检查。",
status: "pending",
group: definition.group,
accentColor: definition.accentColor,
metadata,
},
connections: [],
};
}
export function buildVideoWorkflowStarterTemplateGraph(
templateId: string,
origin: Position
) {
const template =
VIDEO_WORKFLOW_STARTER_TEMPLATES.find((item) => item.id === templateId) ||
VIDEO_WORKFLOW_STARTER_TEMPLATES[0];
const laneHeight = 320;
let cursorX = origin.x;
const nodes = template.nodes.map((type) => {
const definition = getVideoWorkflowNodeDefinition(type);
if (!definition) {
throw new Error(`Unknown video workflow starter node type: ${type}`);
}
const position = {
x: cursorX,
y: origin.y + Math.max(0, (laneHeight - definition.size.height) / 2),
};
cursorX += definition.size.width + 96;
return {
type,
node: buildVideoWorkflowCanvasNode(type, position),
};
});
const edges = nodes.slice(1).map((node, index) => ({
sourceIndex: index,
targetIndex: index + 1,
type: "default" as const,
}));
return {
template,
nodes,
edges,
};
}