Your Name 432d315bc6
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: align canvas billing and 3d task routing
2026-04-27 16:56:40 +08:00

482 lines
22 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.

export const VIDEO_CANVAS_FUNCTION_CONFIG_KEY = "video_canvas_function_config";
export type VideoCanvasFunctionKind =
| "multimodal"
| "image"
| "image-edit"
| "video"
| "audio"
| "utility"
| "workflow";
export type VideoCanvasBackendRoute =
| "canvas-generate-image"
| "canvas-generate-video"
| "canvas-workflow-analyze"
| "angle-edit"
| "enhance"
| "enhance-pro"
| "extend"
| "remove-bg"
| "layer-split"
| "local";
export type VideoCanvasFunctionDefinition = {
key: string;
label: string;
kind: VideoCanvasFunctionKind;
route: VideoCanvasBackendRoute;
enabled: boolean;
model?: string;
prompt: string;
description: string;
requiresBackend?: boolean;
};
export type VideoCanvasFunctionBillingCopy = {
shortLabel: string;
description: string;
};
export type VideoCanvasFunctionConfig = {
enabled: boolean;
defaultImageModel: string;
defaultVideoModel: string;
defaultMultimodalModel: string;
functions: Record<string, VideoCanvasFunctionDefinition>;
};
const defaultFunctions: VideoCanvasFunctionDefinition[] = [
{
key: "story-beat-grid-4",
label: "剧情推演四宫格",
kind: "image-edit",
route: "canvas-generate-image",
enabled: true,
prompt:
"以选中图片作为角色身份、服装、发型、场景光线、镜头距离和画面质感的唯一参考,生成一张 2x2 剧情推演四宫格合成图。画面中只能有 4 个画面区域,只允许一条竖向分割线和一条横向分割线;严禁出现 3 行 3 列、9 个画面区域或九宫格布局。四格都必须保持参考图的近景/半身近中景人物构图,主体占画面主要面积,不要远景、全景、森林空镜或主体很小的画面。四格是同一角色在同一自然光场景中的连续轻微剧情变化:1 正面安静凝视,2 保留参考图抬手/袖摆动作的延续,3 视线或表情轻微变化,4 情绪收束的近中景反应。保持五官、发型、绿色服装、柔和森林逆光、色调和景深一致;每格角度和动作有轻微差异,但不能像 9 个不同机位分镜。输出单张 16:9 图片,2x2 等分排版,分割线清晰干净,不能出现文字、水印、编号、边框标签或 3x3/5x5 宫格。",
description: "官方剧情推演四宫格:基于参考图生成 2x2 连续剧情分镜合成图。",
},
{
key: "character-three-view",
label: "角色三视图生成",
kind: "image-edit",
route: "canvas-generate-image",
enabled: true,
prompt:
"以选中图片作为唯一角色参考,生成一张角色三视图设定图。只输出正面、侧面、背面 3 个完整站姿人物,三人横向并排在同一张 16:9 画面中,白色或浅灰干净背景。必须保持同一角色身份、五官、发型、绿色服装、材质、体型比例和配饰一致;正面、侧面、背面视角要清晰可辨,人物全身完整入画,站姿自然。禁止生成 3x3 九宫格、多机位分镜、黑色分割线、重复头像、文字、水印、编号、环境场景或额外人物。",
description: "官方角色三视图预设,使用参考图生图/编辑模型。",
},
{
key: "multi-view-nine-grid",
label: "多机位九宫格",
kind: "image-edit",
route: "canvas-generate-image",
enabled: true,
prompt:
"以选中图片作为角色、服装、发型、场景光线的唯一参考,生成一张 3x3 多机位分镜合成图。必须是 9 个明显不同的镜头画面,不是同一张图裁切或重复人脸。九格分别包含:1 森林环境远景/主体很小,2 全身远景,3 中景正面,4 侧面半身,5 低机位或俯拍,6 面部近景,7 手部/衣袖/动作细节,8 背面或侧背,9 仰视或情绪特写。保持同一角色身份、绿色服装、自然森林光线一致,每格构图、景别、机位和主体大小必须不同。输出为单张 16:9 图片,九格之间使用清晰黑色分割线,不能出现文字、水印、编号,不能白底棚拍。",
description: "官方九宫格工具:先生成带黑色分割线的 3x3 分镜图,再由宫格切分拆成单张镜头。",
},
{
key: "continuous-storyboard-grid-25",
label: "25宫格连贯分镜",
kind: "image-edit",
route: "canvas-generate-image",
enabled: true,
prompt:
"以选中图片作为角色、服装、发型、场景光线和电影质感的唯一参考,生成一张 5x5 连贯分镜合成图。必须是 25 个连续镜头,体现从远景建立场景、人物动作推进、情绪变化、细节特写到结尾反应的完整微型叙事,不是同一张图重复裁切。保持同一角色身份和绿色服装一致,森林自然光线统一;每格景别、机位、主体大小或动作都要有明显差异。输出为单张 16:9 图片,必须严格 5x5 等分排版,25 个宫格的宽高一致、间距一致、边界完整,所有宫格之间使用清晰纯黑分割线,分割线等宽且横线竖线都要贯穿整张图;禁止使用白线、浅色线、柔边、渐变线、不规则拼贴、错位排版、跨格大图、留白边距、文字、水印、编号或额外边框说明。",
description: "官方 25 宫格连贯分镜:生成 5x5 连续镜头合成图。",
},
{
key: "cinematic-lighting-correction",
label: "电影级光影校正",
kind: "image-edit",
route: "canvas-generate-image",
enabled: true,
prompt:
"以选中图片为唯一参考,在保持角色身份、发型、服装、姿态和构图一致的前提下,进行电影级光影校正。增强自然逆光、轮廓光、皮肤通透感、背景层次和柔和景深,保留原始角色与场景,不改变人物数量,不生成宫格或分镜。输出单张 16:9 图片,不能出现文字、水印、编号或额外边框。",
description: "官方电影级光影校正:对参考图做单张电影感光影重塑。",
},
{
key: "frame-prediction-plus-3s",
label: "画面推演 - 3秒后",
kind: "image-edit",
route: "canvas-generate-image",
enabled: true,
prompt:
"以选中图片作为当前画面,推演同一镜头时间线 3 秒后的画面。保持角色身份、服装、发型、场景和自然光线一致,只让动作、表情、衣袖/头发摆动、视线或镜头位置发生合理连续变化。输出单张 16:9 图片,不要生成宫格、分镜、文字、水印或编号。",
description: "官方画面推演:根据当前帧生成 3 秒后的单帧结果。",
},
{
key: "frame-prediction-minus-5s",
label: "画面推演 - 5秒前",
kind: "image-edit",
route: "canvas-generate-image",
enabled: true,
prompt:
"以选中图片作为当前画面,反向推演同一镜头时间线 5 秒前的画面。保持角色身份、服装、发型、场景和自然光线一致,只让动作起势、姿态、表情、衣袖/头发位置或镜头位置回到合理的前一状态。输出单张 16:9 图片,不要生成宫格、分镜、文字、水印或编号。",
description: "官方画面推演:根据当前帧生成 5 秒前的单帧结果。",
},
{
key: "grid-split",
label: "宫格切分",
kind: "utility",
route: "local",
enabled: true,
prompt: "按当前图的标准宫格布局进行固定等分切分,将合成图切分为可单独调整的镜头图片。",
description: "对齐官方画布:4/9/16/25 和自定义宫格都按当前图的行列固定等分切分,并支持在当前图上多选宫格后继续创建生图或高清节点。",
},
{
key: "lighting",
label: "打光",
kind: "image-edit",
route: "canvas-generate-image",
enabled: true,
prompt:
"在保持主体、构图和身份一致的前提下,对图片进行电影级光影重塑,增强轮廓光、层次、对比和空间感,保留自然材质细节。",
description: "图片编辑生图模型,复刻官方打光工具。",
},
{
key: "hd-upscale",
label: "高清",
kind: "utility",
route: "enhance",
enabled: true,
prompt: "提升图片清晰度、细节和分辨率,保持原图内容不变。",
description: "接入现有图片增强接口;高清重绘可在模型字段指定增强 Pro。",
},
{
key: "panorama-720",
label: "720 全景",
kind: "image-edit",
route: "extend",
enabled: true,
prompt:
"基于当前图片向四周扩展为 720 全景视野,保持主体与场景一致,补全自然环境和空间透视,避免变形和重复纹理。",
description: "优先使用拓图接口,必要时可切到图片编辑生图模型。",
},
{
key: "image-outpaint",
label: "扩图",
kind: "utility",
route: "extend",
enabled: true,
prompt: "在保持主体不变的前提下扩展画面边界,补全自然背景和构图。",
description: "对应官方高清菜单里的扩图。",
},
{
key: "image-repaint",
label: "重绘",
kind: "image-edit",
route: "canvas-generate-image",
enabled: true,
prompt: "保持主体结构和身份一致,按照用户补充提示重绘画面局部或整体风格。",
description: "对应官方高清菜单里的重绘。",
},
{
key: "image-erase",
label: "擦除",
kind: "image-edit",
route: "canvas-generate-image",
enabled: true,
prompt: "根据用户标记或文字说明移除指定元素,并自然补全背景纹理。",
description: "当前以前端提示词占位接图片编辑模型,后续可替换为蒙版编辑接口。",
},
{
key: "remove-bg",
label: "抠图",
kind: "utility",
route: "remove-bg",
enabled: true,
prompt: "移除图片背景,保留主体透明或干净背景结果。",
description: "接入现有抠图接口。",
},
{
key: "image-crop",
label: "裁剪",
kind: "utility",
route: "local",
enabled: true,
prompt: "按用户选择的裁剪比例或宫格切片范围处理图片。",
description: "本地画布裁剪/宫格切分能力。",
},
{
key: "angle-edit",
label: "多角度编辑器",
kind: "image-edit",
route: "angle-edit",
enabled: true,
prompt: "根据水平环绕、垂直俯仰和景别缩放参数生成新的角色/主体视角。",
description: "接现有 /api/angle-edit,多角度编辑器提交时触发。",
},
{
key: "story-script",
label: "故事脚本生成",
kind: "multimodal",
route: "canvas-workflow-analyze",
enabled: true,
prompt: "根据输入文本、图片或视频参考,生成结构化分镜脚本,包含镜号、场景、画面、声音和镜头节奏。",
description: "多模态大模型/文本模型生成脚本表格。",
},
{
key: "video-analysis",
label: "视频解析",
kind: "multimodal",
route: "canvas-workflow-analyze",
enabled: true,
prompt:
"解析当前视频的镜头节奏、景别变化、主体动作、背景音乐、人声/音效、关键帧要点,并输出可直接衔接后续图像生成与视频生成节点的结构化故事表。",
description: "多模态视频解析:抽取关键帧并输出视频故事表。",
},
{
key: "image-to-prompt",
label: "图片反推提示词",
kind: "multimodal",
route: "canvas-workflow-analyze",
enabled: true,
prompt: "分析参考图片,输出结构化中文提示词,包括主体、环境、构图、光影、镜头语言和风格关键词。",
description: "多模态识图生成提示词。",
},
{
key: "text-to-video",
label: "文生视频",
kind: "video",
route: "canvas-generate-video",
enabled: true,
prompt: "根据文本描述生成短视频,保持镜头运动、节奏和电影级质感。",
description: "视频生成模型,走现有视频生成接口。",
},
{
key: "first-frame-video",
label: "首帧图生视频",
kind: "video",
route: "canvas-generate-video",
enabled: true,
prompt: "以参考图片作为视频首帧,生成自然流畅的短视频。",
description: "图片到视频/首帧生视频。",
},
{
key: "first-last-frame-video",
label: "首尾帧生成视频",
kind: "video",
route: "canvas-generate-video",
enabled: true,
prompt: "以两张参考图作为首帧和尾帧,生成首尾连贯的短视频。",
description: "首尾帧视频生成。",
},
{
key: "audio-to-video",
label: "音频生视频",
kind: "video",
route: "canvas-generate-video",
enabled: true,
prompt: "根据上传音频生成对应场景画面,镜头语言、节奏、音乐匹配情绪变化,电影级质感。",
description: "官方为音频+参考图到视频;当前先接视频生成提示词和参考图,音频节拍解析接口后续补充。",
requiresBackend: true,
},
];
export const defaultVideoCanvasFunctionConfig: VideoCanvasFunctionConfig = {
enabled: true,
defaultImageModel: "auto",
defaultVideoModel: "auto",
defaultMultimodalModel: "auto",
functions: Object.fromEntries(defaultFunctions.map((item) => [item.key, item])),
};
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
const legacyFunctionPrompts: Record<string, string[]> = {
"story-beat-grid-4": [
"以选中图片作为角色身份、服装、发型、场景光线、镜头距离和画面质感的唯一参考,生成一张 2x2 剧情推演四宫格合成图。四格都必须保持参考图的近景/半身近中景人物构图,主体占画面主要面积,不要远景、全景、森林空镜或主体很小的画面。四格是同一角色在同一自然光场景中的连续轻微剧情变化:1 正面安静凝视,2 保留参考图抬手/袖摆动作的延续,3 视线或表情轻微变化,4 情绪收束的近中景反应。保持五官、发型、绿色服装、柔和森林逆光、色调和景深一致;每格角度和动作有轻微差异,但不能像 9 个不同机位分镜。输出单张 16:9 图片,2x2 等分排版,分割线清晰干净,不能出现文字、水印、编号、边框标签或 3x3/5x5 宫格。",
"以选中图片作为角色、服装、发型、场景光线的唯一参考,生成一张 3x3 多机位分镜合成图。必须是 9 个明显不同的镜头画面,不是同一张图裁切或重复人脸。九格分别包含:1 森林环境远景/主体很小,2 全身远景,3 中景正面,4 侧面半身,5 低机位或俯拍,6 面部近景,7 手部/衣袖/动作细节,8 背面或侧背,9 仰视或情绪特写。保持同一角色身份、绿色服装、自然森林光线一致,每格构图、景别、机位和主体大小必须不同。输出为单张 16:9 图片,九格之间使用清晰黑色分割线,不能出现文字、水印、编号,不能白底棚拍。",
],
"continuous-storyboard-grid-25": [
"以选中图片作为角色、服装、发型、场景光线和电影质感的唯一参考,生成一张 5x5 连贯分镜合成图。必须是 25 个连续镜头,体现从远景建立场景、人物动作推进、情绪变化、细节特写到结尾反应的完整微型叙事,不是同一张图重复裁切。保持同一角色身份和绿色服装一致,森林自然光线统一;每格景别、机位、主体大小或动作都要有明显差异。输出为单张 16:9 图片,5x5 等分排版,使用清晰分割线,不能出现文字、水印、编号或多余边框说明。",
],
"character-three-view": [
"以输入角色图为唯一角色参考,生成正面、侧面、背面三视图角色设定图。保持五官、发型、服装、颜色和体型一致,干净白底,三视图水平排布,适合后续视频角色一致性使用。",
],
};
const legacyFunctionLabels: Record<string, string[]> = {
"character-three-view": ["角色三视图"],
};
function normalizeComparableText(value: string) {
return value.replace(/\s+/g, " ").trim();
}
function normalizeFunctionLabel(
key: string,
value: unknown,
fallbackLabel: string,
) {
const label = typeof value === "string" ? value.trim() : "";
if (!label) return fallbackLabel;
const normalizedLabel = normalizeComparableText(label);
const isLegacyLabel = (legacyFunctionLabels[key] || []).some(
(legacyLabel) => normalizeComparableText(legacyLabel) === normalizedLabel,
);
return isLegacyLabel ? fallbackLabel : label;
}
function normalizeFunctionPrompt(
key: string,
value: unknown,
fallbackPrompt: string,
) {
const prompt = typeof value === "string" ? value.trim() : "";
if (!prompt) return fallbackPrompt;
const normalizedPrompt = normalizeComparableText(prompt);
const isStoryPromptClearlyNineGrid =
key === "story-beat-grid-4" &&
/(?:生成一张\s*3\s*[x×]\s*3|生成一张[^。]*九宫格|九格分别|必须是\s*9\s*个)/.test(
normalizedPrompt,
);
if (isStoryPromptClearlyNineGrid) return fallbackPrompt;
const isLegacyPrompt = (legacyFunctionPrompts[key] || []).some(
(legacyPrompt) => normalizeComparableText(legacyPrompt) === normalizedPrompt,
);
return isLegacyPrompt ? fallbackPrompt : prompt;
}
function normalizeFunctionDefinition(
key: string,
value: unknown,
fallback: VideoCanvasFunctionDefinition,
): VideoCanvasFunctionDefinition {
const incoming = isRecord(value) ? value : {};
const normalized: VideoCanvasFunctionDefinition = {
...fallback,
key,
label: normalizeFunctionLabel(key, incoming.label, fallback.label),
kind: (incoming.kind as VideoCanvasFunctionKind) || fallback.kind,
route: (incoming.route as VideoCanvasBackendRoute) || fallback.route,
enabled: typeof incoming.enabled === "boolean" ? incoming.enabled : fallback.enabled,
model: typeof incoming.model === "string" ? incoming.model : fallback.model,
prompt: normalizeFunctionPrompt(key, incoming.prompt, fallback.prompt),
description: String(incoming.description || fallback.description),
requiresBackend:
typeof incoming.requiresBackend === "boolean"
? incoming.requiresBackend
: fallback.requiresBackend,
};
return normalized;
}
export function normalizeVideoCanvasFunctionConfig(value: unknown): VideoCanvasFunctionConfig {
if (!isRecord(value)) return defaultVideoCanvasFunctionConfig;
const incomingFunctions = isRecord(value.functions) ? value.functions : {};
const functions: Record<string, VideoCanvasFunctionDefinition> = {};
for (const [key, fallback] of Object.entries(defaultVideoCanvasFunctionConfig.functions)) {
functions[key] = normalizeFunctionDefinition(key, incomingFunctions[key], fallback);
}
for (const [key, customValue] of Object.entries(incomingFunctions)) {
if (functions[key] || !isRecord(customValue)) continue;
const fallback: VideoCanvasFunctionDefinition = {
key,
label: String(customValue.label || key),
kind: (customValue.kind as VideoCanvasFunctionKind) || "workflow",
route: (customValue.route as VideoCanvasBackendRoute) || "local",
enabled: customValue.enabled !== false,
prompt: String(customValue.prompt || ""),
description: String(customValue.description || "自定义视频画布功能"),
};
functions[key] = normalizeFunctionDefinition(key, customValue, fallback);
}
return {
enabled: value.enabled !== false,
defaultImageModel:
typeof value.defaultImageModel === "string" && value.defaultImageModel.trim()
? value.defaultImageModel.trim()
: defaultVideoCanvasFunctionConfig.defaultImageModel,
defaultVideoModel:
typeof value.defaultVideoModel === "string" && value.defaultVideoModel.trim()
? value.defaultVideoModel.trim()
: defaultVideoCanvasFunctionConfig.defaultVideoModel,
defaultMultimodalModel:
typeof value.defaultMultimodalModel === "string" && value.defaultMultimodalModel.trim()
? value.defaultMultimodalModel.trim()
: defaultVideoCanvasFunctionConfig.defaultMultimodalModel,
functions,
};
}
export function getVideoCanvasFunctionBillingCopy(
definition: Pick<VideoCanvasFunctionDefinition, "key" | "route">
): VideoCanvasFunctionBillingCopy {
if (definition.route === "local") {
return {
shortLabel: "本地",
description: "本地画布工具,不单独扣积分。",
};
}
if (definition.key === "character-three-view" || definition.key === "multi-view-nine-grid") {
return {
shortLabel: "多视角",
description: "跟随全站“图片多视角”积分设置,不在视频画布单独改价。",
};
}
if (definition.key === "image-repaint") {
return {
shortLabel: "重绘",
description: "跟随全站“图片重绘”积分设置,不在视频画布单独改价。",
};
}
if (definition.route === "remove-bg") {
return {
shortLabel: "抠图",
description: "跟随全站“抠图”积分设置,不在视频画布单独改价。",
};
}
if (definition.route === "extend") {
return {
shortLabel: "扩图",
description: "跟随全站“图像拓展”积分设置,不在视频画布单独改价。",
};
}
if (definition.route === "enhance-pro") {
return {
shortLabel: "增强Pro",
description: "跟随全站“图像增强 Pro”积分设置,不在视频画布单独改价。",
};
}
if (definition.route === "enhance") {
return {
shortLabel: "按站点",
description: "沿用站点统一高清/增强能力,不在视频画布单独配置扣点。",
};
}
if (definition.route === "canvas-generate-video") {
return {
shortLabel: "按站点",
description: "沿用站点统一视频模型与积分规则,不在视频画布单独改价。",
};
}
if (definition.route === "canvas-workflow-analyze" || definition.route === "angle-edit") {
return {
shortLabel: "按站点",
description: "沿用站点统一模型与功能规则,不在视频画布单独改价。",
};
}
return {
shortLabel: "按站点",
description: "沿用站点统一图片生成与功能积分规则,不在视频画布单独改价。",
};
}