|
|
|
@ -11,8 +11,10 @@ import { |
|
|
|
import { useI18n } from "@/i18n"; |
|
|
|
import { useModelStore } from "@/store/modelStore"; |
|
|
|
import { useWorkflowStore } from "@/store/workflowStore"; |
|
|
|
import type { NanoBananaNodeData, SelectedModel, WorkflowNode } from "@/types"; |
|
|
|
import { SINGLE_INPUT_HANDLE_ID, SINGLE_OUTPUT_HANDLE_ID } from "@/utils/nodeHandles"; |
|
|
|
import type { GenerateResponse, SelectedModel, WorkflowNode } from "@/types"; |
|
|
|
import { buildGenerateHeaders } from "@/store/utils/buildApiHeaders"; |
|
|
|
import { executePopiartTask } from "@/store/execution/popiartTaskClient"; |
|
|
|
import { createDerivedImageNode } from "@/utils/derivedImageNodes"; |
|
|
|
import { toSelectedModel } from "@/utils/selectedModel"; |
|
|
|
|
|
|
|
interface NodeMultiAnglePanelProps { |
|
|
|
@ -21,38 +23,38 @@ interface NodeMultiAnglePanelProps { |
|
|
|
onClose: () => void; |
|
|
|
} |
|
|
|
|
|
|
|
function buildMultiAngleNodeData( |
|
|
|
sourceImage: string, |
|
|
|
settings: MultiAngleSettings, |
|
|
|
selectedModel: SelectedModel |
|
|
|
): Partial<NanoBananaNodeData> { |
|
|
|
type GeneratedImageResult = { |
|
|
|
image: string; |
|
|
|
previewImage?: string; |
|
|
|
}; |
|
|
|
|
|
|
|
function normalizeGeneratedImage(value: unknown): GeneratedImageResult | null { |
|
|
|
if (typeof value === "string" && value.length > 0) { |
|
|
|
return { image: value }; |
|
|
|
} |
|
|
|
if (!value || typeof value !== "object") return null; |
|
|
|
|
|
|
|
const record = value as { url?: unknown; previewImg?: unknown }; |
|
|
|
if (typeof record.url !== "string" || record.url.length === 0) return null; |
|
|
|
|
|
|
|
return { |
|
|
|
inputImages: [sourceImage], |
|
|
|
inputPrompt: "multi-angle image generation", |
|
|
|
aspectRatio: "16:9", |
|
|
|
resolution: "2K", |
|
|
|
model: "nano-banana-pro", |
|
|
|
selectedModel, |
|
|
|
imageCount: 1, |
|
|
|
parameters: { batchSize: 1 }, |
|
|
|
extraTaskParams: { |
|
|
|
horizontal_angle: settings.horizontal, |
|
|
|
vertical_angle: settings.vertical, |
|
|
|
zoom: settings.zoom, |
|
|
|
}, |
|
|
|
useGoogleSearch: false, |
|
|
|
useImageSearch: false, |
|
|
|
status: "idle", |
|
|
|
error: null, |
|
|
|
image: record.url, |
|
|
|
previewImage: typeof record.previewImg === "string" ? record.previewImg : undefined, |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
function readFirstGeneratedImage(result: GenerateResponse): GeneratedImageResult | null { |
|
|
|
if (Array.isArray(result.images) && result.images.length > 0) { |
|
|
|
return normalizeGeneratedImage(result.images[0]); |
|
|
|
} |
|
|
|
return normalizeGeneratedImage(result.image); |
|
|
|
} |
|
|
|
|
|
|
|
export function NodeMultiAnglePanel({ node, imageSource, onClose }: NodeMultiAnglePanelProps) { |
|
|
|
const { t } = useI18n(); |
|
|
|
const addNode = useWorkflowStore((state) => state.addNode); |
|
|
|
const getNodeById = useWorkflowStore((state) => state.getNodeById); |
|
|
|
const onConnect = useWorkflowStore((state) => state.onConnect); |
|
|
|
const regenerateNode = useWorkflowStore((state) => state.regenerateNode); |
|
|
|
const providerSettings = useWorkflowStore((state) => state.providerSettings); |
|
|
|
const selectSingleNode = useWorkflowStore((state) => state.selectSingleNode); |
|
|
|
const multiAngleModel = useModelStore((state) => state.byKind.multiAngle[0] ?? null); |
|
|
|
const [settings, setSettings] = useState<MultiAngleSettings>({ ...DEFAULT_MULTI_ANGLE_SETTINGS }); |
|
|
|
@ -99,66 +101,56 @@ export function NodeMultiAnglePanel({ node, imageSource, onClose }: NodeMultiAng |
|
|
|
: typeof node.style?.width === "number" |
|
|
|
? node.style.width |
|
|
|
: 300; |
|
|
|
const newNodeId = addNode( |
|
|
|
"nanoBanana", |
|
|
|
const result = await executePopiartTask( |
|
|
|
{ |
|
|
|
images: [imageSource], |
|
|
|
prompt: "multi-angle image generation", |
|
|
|
selectedModel, |
|
|
|
parameters: { |
|
|
|
batchSize: 1, |
|
|
|
horizontal_angle: settings.horizontal, |
|
|
|
vertical_angle: settings.vertical, |
|
|
|
zoom: settings.zoom, |
|
|
|
}, |
|
|
|
mediaType: "image", |
|
|
|
}, |
|
|
|
{ |
|
|
|
headers: buildGenerateHeaders("popiserver", providerSettings), |
|
|
|
} |
|
|
|
); |
|
|
|
const generatedImage = readFirstGeneratedImage(result); |
|
|
|
if (!generatedImage) { |
|
|
|
throw new Error(result.error || "Generation failed"); |
|
|
|
} |
|
|
|
|
|
|
|
createDerivedImageNode({ |
|
|
|
sourceNodeId: node.id, |
|
|
|
position: { |
|
|
|
x: node.position.x + currentWidth + 220, |
|
|
|
y: node.position.y, |
|
|
|
}, |
|
|
|
buildMultiAngleNodeData(imageSource, settings, selectedModel) |
|
|
|
); |
|
|
|
onConnect({ |
|
|
|
source: node.id, |
|
|
|
sourceHandle: SINGLE_OUTPUT_HANDLE_ID, |
|
|
|
target: newNodeId, |
|
|
|
targetHandle: SINGLE_INPUT_HANDLE_ID, |
|
|
|
image: generatedImage.image, |
|
|
|
previewImage: generatedImage.previewImage, |
|
|
|
filename: `multi-angle-${Date.now()}.png`, |
|
|
|
dimensions: null, |
|
|
|
label: t("imageInput.multiAngle"), |
|
|
|
operation: "multiAngle", |
|
|
|
addNode, |
|
|
|
onConnect, |
|
|
|
selectSingleNode, |
|
|
|
}); |
|
|
|
selectSingleNode(newNodeId); |
|
|
|
onClose(); |
|
|
|
await regenerateNode(newNodeId); |
|
|
|
const generatedNode = getNodeById(newNodeId); |
|
|
|
const generatedData = generatedNode?.data as NanoBananaNodeData | undefined; |
|
|
|
if (generatedData?.outputImage) { |
|
|
|
const resultNodeId = addNode( |
|
|
|
"smartImage", |
|
|
|
{ |
|
|
|
x: node.position.x + currentWidth + 560, |
|
|
|
y: node.position.y, |
|
|
|
}, |
|
|
|
{ |
|
|
|
image: generatedData.outputImage, |
|
|
|
imageRef: undefined, |
|
|
|
previewImage: generatedData.previewImg, |
|
|
|
filename: `multi-angle-${Date.now()}.png`, |
|
|
|
dimensions: null, |
|
|
|
label: t("imageInput.multiAngle"), |
|
|
|
derivedImage: { |
|
|
|
sourceNodeId: newNodeId, |
|
|
|
operation: "multiAngle", |
|
|
|
}, |
|
|
|
disablePreviewModal: true, |
|
|
|
} |
|
|
|
); |
|
|
|
onConnect({ |
|
|
|
source: newNodeId, |
|
|
|
sourceHandle: SINGLE_OUTPUT_HANDLE_ID, |
|
|
|
target: resultNodeId, |
|
|
|
targetHandle: SINGLE_INPUT_HANDLE_ID, |
|
|
|
}); |
|
|
|
selectSingleNode(resultNodeId); |
|
|
|
} |
|
|
|
} finally { |
|
|
|
setIsGenerating(false); |
|
|
|
} |
|
|
|
}, [ |
|
|
|
addNode, |
|
|
|
getNodeById, |
|
|
|
imageSource, |
|
|
|
isGenerating, |
|
|
|
node, |
|
|
|
onClose, |
|
|
|
onConnect, |
|
|
|
regenerateNode, |
|
|
|
providerSettings, |
|
|
|
selectSingleNode, |
|
|
|
selectedModel, |
|
|
|
settings, |
|
|
|
|