Improve nine grid storyboard generation
This commit is contained in:
parent
f3869f5e63
commit
7a1a478eaf
@ -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<ReturnType<typeof imageGenerationService.generateImage>>,
|
||||
) {
|
||||
@ -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 ? (
|
||||
<img
|
||||
alt={node.title}
|
||||
className="pointer-events-none h-full w-full object-cover"
|
||||
draggable={false}
|
||||
onDragStart={preventNativeImageDrag}
|
||||
src={mediaUrl}
|
||||
style={{ transform: `rotate(${rotation}deg)` }}
|
||||
/>
|
||||
) : isNineGridComposite ? (
|
||||
<NineGridCompositePreview node={node} sourceUrl={sourceUrl || mediaUrl} />
|
||||
<NineGridCompositePreview node={node} sourceUrl={sourceUrl} />
|
||||
) : cropTile && mediaUrl && gridCellVisual ? (
|
||||
<GridCellImage alt={node.title} src={mediaUrl} visual={gridCellVisual} />
|
||||
) : cropTile && mediaUrl ? (
|
||||
@ -8275,9 +8332,16 @@ function ImageNodePreview({ node }: { node: CloneNode }) {
|
||||
<div className="rounded-[8px] border border-white/8 bg-[#111] p-3">
|
||||
{mediaType === "video" ? (
|
||||
<video className="max-h-[72vh] w-full rounded-[6px] object-contain" controls playsInline src={url} />
|
||||
) : isNineGridComposite && url ? (
|
||||
<img
|
||||
alt={node.title}
|
||||
className="max-h-[72vh] w-full rounded-[6px] object-contain"
|
||||
src={url}
|
||||
style={{ transform: `rotate(${rotation}deg)` }}
|
||||
/>
|
||||
) : isNineGridComposite ? (
|
||||
<div className="relative aspect-video max-h-[72vh] w-full overflow-hidden rounded-[6px] bg-white">
|
||||
<NineGridCompositePreview node={node} sourceUrl={sourceUrl || url} />
|
||||
<NineGridCompositePreview node={node} sourceUrl={sourceUrl} />
|
||||
</div>
|
||||
) : cropTile && url && gridCellVisual ? (
|
||||
<div className="relative aspect-square max-h-[72vh] w-full overflow-hidden rounded-[6px] bg-white">
|
||||
|
||||
@ -62,7 +62,7 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
enabled: true,
|
||||
creditCost: 14,
|
||||
prompt:
|
||||
"以选中图片为角色/主体参考,生成多视角 3x3 九宫格分镜图。保持主体一致,九个镜头包含远景、全身、侧身、背面、半身、近景、局部细节等视角,使用清晰黑色分割线分隔每一格,白色或干净背景。",
|
||||
"以选中图片作为角色、服装、发型、场景光线的唯一参考,生成一张 3x3 多机位分镜合成图。必须是 9 个明显不同的镜头画面,不是同一张图裁切或重复人脸。九格分别包含:1 森林环境远景/主体很小,2 全身远景,3 中景正面,4 侧面半身,5 低机位或俯拍,6 面部近景,7 手部/衣袖/动作细节,8 背面或侧背,9 仰视或情绪特写。保持同一角色身份、绿色服装、自然森林光线一致,每格构图、景别、机位和主体大小必须不同。输出为单张 16:9 图片,九格之间使用清晰黑色分割线,不能出现文字、水印、编号,不能白底棚拍。",
|
||||
description: "官方九宫格工具:先生成带黑色分割线的 3x3 分镜图,再由宫格切分拆成单张镜头。",
|
||||
},
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user