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; }; 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 { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } const legacyFunctionPrompts: Record = { "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 = { "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 = {}; 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 ): 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: "沿用站点统一图片生成与功能积分规则,不在视频画布单独改价。", }; }