314 lines
11 KiB
TypeScript
314 lines
11 KiB
TypeScript
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;
|
||
creditCost?: number;
|
||
prompt: string;
|
||
description: string;
|
||
requiresBackend?: boolean;
|
||
};
|
||
|
||
export type VideoCanvasFunctionConfig = {
|
||
enabled: boolean;
|
||
defaultImageModel: string;
|
||
defaultVideoModel: string;
|
||
defaultMultimodalModel: string;
|
||
functions: Record<string, VideoCanvasFunctionDefinition>;
|
||
};
|
||
|
||
const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||
{
|
||
key: "character-three-view",
|
||
label: "角色三视图",
|
||
kind: "image-edit",
|
||
route: "canvas-generate-image",
|
||
enabled: true,
|
||
creditCost: 14,
|
||
prompt:
|
||
"以输入角色图为唯一角色参考,生成正面、侧面、背面三视图角色设定图。保持五官、发型、服装、颜色和体型一致,干净白底,三视图水平排布,适合后续视频角色一致性使用。",
|
||
description: "官方角色三视图预设,使用参考图生图/编辑模型。",
|
||
},
|
||
{
|
||
key: "multi-view-nine-grid",
|
||
label: "多机位九宫格",
|
||
kind: "image-edit",
|
||
route: "canvas-generate-image",
|
||
enabled: true,
|
||
creditCost: 14,
|
||
prompt:
|
||
"以选中图片为角色/主体参考,生成多视角 3x3 九宫格分镜图。保持主体一致,九个镜头包含远景、全身、侧身、背面、半身、近景、局部细节等视角,使用清晰黑色分割线分隔每一格,白色或干净背景。",
|
||
description: "官方九宫格工具:先生成带黑色分割线的 3x3 分镜图,再由宫格切分拆成单张镜头。",
|
||
},
|
||
{
|
||
key: "grid-split",
|
||
label: "宫格切分",
|
||
kind: "utility",
|
||
route: "local",
|
||
enabled: true,
|
||
creditCost: 0,
|
||
prompt: "识别黑色宫格分割线,将合成图切分为可单独调整的镜头图片。",
|
||
description: "浏览器端识别黑色分割线并生成切片节点;后端可后续替换为更稳的图像分割服务。",
|
||
},
|
||
{
|
||
key: "lighting",
|
||
label: "打光",
|
||
kind: "image-edit",
|
||
route: "canvas-generate-image",
|
||
enabled: true,
|
||
creditCost: 14,
|
||
prompt:
|
||
"在保持主体、构图和身份一致的前提下,对图片进行电影级光影重塑,增强轮廓光、层次、对比和空间感,保留自然材质细节。",
|
||
description: "图片编辑生图模型,复刻官方打光工具。",
|
||
},
|
||
{
|
||
key: "hd-upscale",
|
||
label: "高清",
|
||
kind: "utility",
|
||
route: "enhance",
|
||
enabled: true,
|
||
creditCost: 8,
|
||
prompt: "提升图片清晰度、细节和分辨率,保持原图内容不变。",
|
||
description: "接入现有图片增强接口;高清重绘可在模型字段指定增强 Pro。",
|
||
},
|
||
{
|
||
key: "panorama-720",
|
||
label: "720 全景",
|
||
kind: "image-edit",
|
||
route: "extend",
|
||
enabled: true,
|
||
creditCost: 14,
|
||
prompt:
|
||
"基于当前图片向四周扩展为 720 全景视野,保持主体与场景一致,补全自然环境和空间透视,避免变形和重复纹理。",
|
||
description: "优先使用拓图接口,必要时可切到图片编辑生图模型。",
|
||
},
|
||
{
|
||
key: "image-outpaint",
|
||
label: "扩图",
|
||
kind: "utility",
|
||
route: "extend",
|
||
enabled: true,
|
||
creditCost: 10,
|
||
prompt: "在保持主体不变的前提下扩展画面边界,补全自然背景和构图。",
|
||
description: "对应官方高清菜单里的扩图。",
|
||
},
|
||
{
|
||
key: "image-repaint",
|
||
label: "重绘",
|
||
kind: "image-edit",
|
||
route: "canvas-generate-image",
|
||
enabled: true,
|
||
creditCost: 14,
|
||
prompt: "保持主体结构和身份一致,按照用户补充提示重绘画面局部或整体风格。",
|
||
description: "对应官方高清菜单里的重绘。",
|
||
},
|
||
{
|
||
key: "image-erase",
|
||
label: "擦除",
|
||
kind: "image-edit",
|
||
route: "canvas-generate-image",
|
||
enabled: true,
|
||
creditCost: 10,
|
||
prompt: "根据用户标记或文字说明移除指定元素,并自然补全背景纹理。",
|
||
description: "当前以前端提示词占位接图片编辑模型,后续可替换为蒙版编辑接口。",
|
||
},
|
||
{
|
||
key: "remove-bg",
|
||
label: "抠图",
|
||
kind: "utility",
|
||
route: "remove-bg",
|
||
enabled: true,
|
||
creditCost: 4,
|
||
prompt: "移除图片背景,保留主体透明或干净背景结果。",
|
||
description: "接入现有抠图接口。",
|
||
},
|
||
{
|
||
key: "image-crop",
|
||
label: "裁剪",
|
||
kind: "utility",
|
||
route: "local",
|
||
enabled: true,
|
||
creditCost: 0,
|
||
prompt: "按用户选择的裁剪比例或宫格切片范围处理图片。",
|
||
description: "本地画布裁剪/宫格切分能力。",
|
||
},
|
||
{
|
||
key: "angle-edit",
|
||
label: "多角度编辑器",
|
||
kind: "image-edit",
|
||
route: "angle-edit",
|
||
enabled: true,
|
||
creditCost: 1,
|
||
prompt: "根据水平环绕、垂直俯仰和景别缩放参数生成新的角色/主体视角。",
|
||
description: "接现有 /api/angle-edit,多角度编辑器提交时触发。",
|
||
},
|
||
{
|
||
key: "story-script",
|
||
label: "故事脚本生成",
|
||
kind: "multimodal",
|
||
route: "canvas-workflow-analyze",
|
||
enabled: true,
|
||
creditCost: 6,
|
||
prompt: "根据输入文本、图片或视频参考,生成结构化分镜脚本,包含镜号、场景、画面、声音和镜头节奏。",
|
||
description: "多模态大模型/文本模型生成脚本表格。",
|
||
},
|
||
{
|
||
key: "image-to-prompt",
|
||
label: "图片反推提示词",
|
||
kind: "multimodal",
|
||
route: "canvas-workflow-analyze",
|
||
enabled: true,
|
||
creditCost: 3,
|
||
prompt: "分析参考图片,输出结构化中文提示词,包括主体、环境、构图、光影、镜头语言和风格关键词。",
|
||
description: "多模态识图生成提示词。",
|
||
},
|
||
{
|
||
key: "text-to-video",
|
||
label: "文生视频",
|
||
kind: "video",
|
||
route: "canvas-generate-video",
|
||
enabled: true,
|
||
creditCost: 14,
|
||
prompt: "根据文本描述生成短视频,保持镜头运动、节奏和电影级质感。",
|
||
description: "视频生成模型,走现有视频生成接口。",
|
||
},
|
||
{
|
||
key: "first-frame-video",
|
||
label: "首帧图生视频",
|
||
kind: "video",
|
||
route: "canvas-generate-video",
|
||
enabled: true,
|
||
creditCost: 14,
|
||
prompt: "以参考图片作为视频首帧,生成自然流畅的短视频。",
|
||
description: "图片到视频/首帧生视频。",
|
||
},
|
||
{
|
||
key: "first-last-frame-video",
|
||
label: "首尾帧生成视频",
|
||
kind: "video",
|
||
route: "canvas-generate-video",
|
||
enabled: true,
|
||
creditCost: 14,
|
||
prompt: "以两张参考图作为首帧和尾帧,生成首尾连贯的短视频。",
|
||
description: "首尾帧视频生成。",
|
||
},
|
||
{
|
||
key: "audio-to-video",
|
||
label: "音频生视频",
|
||
kind: "video",
|
||
route: "canvas-generate-video",
|
||
enabled: true,
|
||
creditCost: 14,
|
||
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);
|
||
}
|
||
|
||
function normalizeFunctionDefinition(
|
||
key: string,
|
||
value: unknown,
|
||
fallback: VideoCanvasFunctionDefinition,
|
||
): VideoCanvasFunctionDefinition {
|
||
const incoming = isRecord(value) ? value : {};
|
||
const normalized: VideoCanvasFunctionDefinition = {
|
||
...fallback,
|
||
key,
|
||
label: String(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,
|
||
creditCost:
|
||
typeof incoming.creditCost === "number" && Number.isFinite(incoming.creditCost)
|
||
? Math.max(0, incoming.creditCost)
|
||
: fallback.creditCost,
|
||
prompt: String(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,
|
||
creditCost: typeof customValue.creditCost === "number" ? Math.max(0, customValue.creditCost) : 0,
|
||
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,
|
||
};
|
||
}
|