From 7a1a478eafaad945fac75b3f5aa70ea80eaeaab9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 19 Apr 2026 10:52:54 +0800 Subject: [PATCH] Improve nine grid storyboard generation --- .../canvas/LiblibVideoCanvasReplica.tsx | 94 ++++++++++++++++--- lib/video-canvas/config.ts | 2 +- 2 files changed, 80 insertions(+), 16 deletions(-) diff --git a/components/canvas/LiblibVideoCanvasReplica.tsx b/components/canvas/LiblibVideoCanvasReplica.tsx index d0d26e23e..ef86d8a17 100644 --- a/components/canvas/LiblibVideoCanvasReplica.tsx +++ b/components/canvas/LiblibVideoCanvasReplica.tsx @@ -384,11 +384,15 @@ const OFFICIAL_TEXT_RESOURCE_WIDTH = 560; const OFFICIAL_IMAGE_TOOLBAR_SCALE = 0.7; const OFFICIAL_COMPOSER_SCALE = 0.546; const OFFICIAL_ZOOM_DOCK_SCALE = 0.65; +const OFFICIAL_NINE_GRID_WIDTH = 930; +const OFFICIAL_NINE_GRID_HEIGHT = 520; const PROMPT_COMPOSER_BASE_WIDTH = 895; const PROMPT_COMPOSER_BASE_HEIGHT = 286; const PRESET_COMPOSER_BASE_HEIGHT = 238; -const MULTI_VIEW_NINE_GRID_PROMPT = +const LEGACY_MULTI_VIEW_NINE_GRID_PROMPT = "以选中图片为角色/主体参考,生成多视角 3x3 九宫格分镜图。保持主体一致,九个镜头包含远景、全身、侧身、背面、半身、近景、局部细节等视角,使用清晰黑色分割线分隔每一格,白色或干净背景。"; +const MULTI_VIEW_NINE_GRID_PROMPT = + "以选中图片作为角色、服装、发型、场景光线的唯一参考,生成一张 3x3 多机位分镜合成图。必须是 9 个明显不同的镜头画面,不是同一张图裁切或重复人脸。九格分别包含:1 森林环境远景/主体很小,2 全身远景,3 中景正面,4 侧面半身,5 低机位或俯拍,6 面部近景,7 手部/衣袖/动作细节,8 背面或侧背,9 仰视或情绪特写。保持同一角色身份、绿色服装、自然森林光线一致,每格构图、景别、机位和主体大小必须不同。输出为单张 16:9 图片,九格之间使用清晰黑色分割线,不能出现文字、水印、编号,不能白底棚拍。"; const NINE_GRID_CELL_VISUALS: GridCellVisual[] = [ { fit: "contain", objectPosition: "50% 50%", scale: 0.62 }, { fit: "contain", objectPosition: "58% 50%", scale: 0.74, translateX: -10 }, @@ -1043,8 +1047,8 @@ function createNineGridCompositeNode({ inputType: "image", x, y, - width: 560, - height: 347, + width: OFFICIAL_NINE_GRID_WIDTH, + height: OFFICIAL_NINE_GRID_HEIGHT, accent: "#8790ff", draftValues: { action: "IMAGE_GENERATE_NINE_GRID", @@ -1188,6 +1192,27 @@ function composeFunctionPrompt( return `${base}\n\n用户补充:${extra}`; } +function composeMultiViewNineGridPrompt( + definition: VideoCanvasFunctionDefinition, + option?: string, +) { + const configuredPrompt = definition.prompt.trim(); + const selectedOption = String(option || "").trim(); + const parts = [MULTI_VIEW_NINE_GRID_PROMPT]; + const isLegacyPrompt = + configuredPrompt === LEGACY_MULTI_VIEW_NINE_GRID_PROMPT || + (configuredPrompt.includes("以选中图片为角色/主体参考") && configuredPrompt.includes("白色或干净背景")); + + if (configuredPrompt && configuredPrompt !== MULTI_VIEW_NINE_GRID_PROMPT && !isLegacyPrompt) { + parts.push(`后台补充:${configuredPrompt}`); + } + if (selectedOption) { + parts.push(`用户选择:${selectedOption}`); + } + + return parts.join("\n\n"); +} + function getFirstGeneratedImageUrl( response: Awaited>, ) { @@ -1496,10 +1521,29 @@ function normalizeTargetModuleNode(node: CloneNode, kind: AddModuleKind, fallbac function normalizeRestoredNode(node: CloneNode, fallbackIndex: number): CloneNode { const action = getDraftString(node, "action"); + const isNineGridResultNode = + action === "IMAGE_GENERATE_NINE_GRID" || + Boolean(node.draftValues?.nineGridComposite) || + (node.inputType === "image" && /多机位九宫格/.test(node.title || "")); if (action === "TEXT_RESOURCE" && !getPresetMode(node)) { return normalizeTextResourceNode(node, fallbackIndex); } + if (isNineGridResultNode) { + return { + ...node, + width: OFFICIAL_NINE_GRID_WIDTH, + height: OFFICIAL_NINE_GRID_HEIGHT, + draftValues: { + ...(node.draftValues || {}), + action: "IMAGE_GENERATE_NINE_GRID", + gridPrompt: getDraftString(node, "gridPrompt") || MULTI_VIEW_NINE_GRID_PROMPT, + nineGridComposite: true, + outputSizeLabel: getDraftString(node, "outputSizeLabel") || "2752 × 1536", + }, + }; + } + const kind = getTargetModuleKind(node) || inferLegacyTargetModuleKind(node); if (!kind) return node; @@ -2455,8 +2499,8 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib const tileSize = tileCount === 25 ? 142 : tileCount === 16 ? 156 : 180; const gap = 26; const startY = Math.round(anchor.y + anchor.height / 2 - (rows * tileSize + (rows - 1) * gap) / 2); - const isSyntheticGrid = Boolean(anchor.draftValues?.nineGridComposite); const anchorMediaUrl = getNodeMediaUrl(anchor); + const isSyntheticGrid = Boolean(anchor.draftValues?.nineGridComposite) && !anchorMediaUrl; const detectedSplit = !isSyntheticGrid && anchorMediaUrl ? await detectGridSplitFromImage(anchorMediaUrl, rows, columns) @@ -2535,7 +2579,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib rows, sourceUrl, x: rightX, - y: centerY(347), + y: centerY(OFFICIAL_NINE_GRID_HEIGHT), }); const shouldGenerate = videoCanvasFunctionConfig.enabled && gridDefinition?.enabled; const pendingGridNode: CloneNode = { @@ -2562,7 +2606,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib setActivePanel(null); if (videoCanvasFunctionConfig.enabled && gridDefinition?.enabled) { - const prompt = composeFunctionPrompt(gridDefinition, option); + const prompt = composeMultiViewNineGridPrompt(gridDefinition, option); const response = await imageGenerationService.generateImage({ aspectRatio: "16:9", imageCount: 1, @@ -2810,6 +2854,10 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib definition, payload.prompt.trim() || payload.slashScene || "生成角色三视图", ); + const finalPrompt = + functionKey === "multi-view-nine-grid" + ? composeMultiViewNineGridPrompt(definition, payload.prompt.trim() || payload.slashScene || "多机位九宫格") + : prompt; const count = Math.max(1, Math.min(4, Number.parseInt(payload.settings.count, 10) || 1)); let generatedUrl = ""; @@ -2818,7 +2866,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib aspectRatio: payload.settings.ratio, imageCount: count, model: getFunctionModel(videoCanvasFunctionConfig, definition), - prompt, + prompt: finalPrompt, referenceImage: sourceUrl, }); generatedUrl = response.success ? getFirstGeneratedImageUrl(response) : ""; @@ -2830,14 +2878,14 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib const generatedNode = generatedUrl ? createMediaResourceNode({ action: "IMAGE_RESOURCE", - height: functionKey === "multi-view-nine-grid" ? 347 : 280, + height: functionKey === "multi-view-nine-grid" ? OFFICIAL_NINE_GRID_HEIGHT : 280, mediaType: "image", mediaUrl: generatedUrl, - prompt, + prompt: finalPrompt, title: payload.slashScene || definition.label || "图片生成", - width: functionKey === "multi-view-nine-grid" ? 560 : 420, + width: functionKey === "multi-view-nine-grid" ? OFFICIAL_NINE_GRID_WIDTH : 420, x: rightX, - y: Math.round(anchor.y + anchor.height / 2 - (functionKey === "multi-view-nine-grid" ? 347 : 280) / 2), + y: Math.round(anchor.y + anchor.height / 2 - (functionKey === "multi-view-nine-grid" ? OFFICIAL_NINE_GRID_HEIGHT : 280) / 2), }) : null; const created = generatedNode || createTargetModuleNode("image", rightX, 0, nodes.length + 1); @@ -2851,7 +2899,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib ...(created.draftValues || {}), generated: Boolean(generatedUrl) || created.draftValues?.generated, params: { - prompt, + prompt: finalPrompt, references: payload.references, settings: payload.settings, slashScene: payload.slashScene, @@ -2915,7 +2963,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib imageUrls, mode: "agent-plan", nodeTitle: node.title, - prompt, + prompt: finalPrompt, videoUrls, }); if (!response.success) throw new Error(response.error || "故事脚本生成失败"); @@ -7041,8 +7089,17 @@ function PresetMediaCanvasNode({ progress={getDraftNumber(node, "generationProgress", 2)} status={generationStatus} /> + ) : isNineGridComposite && mediaUrl ? ( + {node.title} ) : isNineGridComposite ? ( - + ) : cropTile && mediaUrl && gridCellVisual ? ( ) : cropTile && mediaUrl ? ( @@ -8275,9 +8332,16 @@ function ImageNodePreview({ node }: { node: CloneNode }) {
{mediaType === "video" ? (