Browse Source
Extracted from nanoBananaExecutor's 3D handling code. Handles 3D model generation via /api/generate with mediaType: "3d", cost tracking, and auto-save to generations folder. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>handoff-20260429-1057
1 changed files with 168 additions and 0 deletions
@ -0,0 +1,168 @@ |
|||
/** |
|||
* Generate3D Executor |
|||
* |
|||
* Executor for generate3d (3D model generation) nodes. |
|||
* Extracted from nanoBananaExecutor's 3D handling code. |
|||
*/ |
|||
|
|||
import type { Generate3DNodeData } from "@/types"; |
|||
import { buildGenerateHeaders } from "@/store/utils/buildApiHeaders"; |
|||
import type { NodeExecutionContext } from "./types"; |
|||
|
|||
export interface Generate3DOptions { |
|||
/** When true, falls back to stored inputImages/inputPrompt if no connections provide them. */ |
|||
useStoredFallback?: boolean; |
|||
} |
|||
|
|||
export async function executeGenerate3D( |
|||
ctx: NodeExecutionContext, |
|||
options: Generate3DOptions = {} |
|||
): Promise<void> { |
|||
const { |
|||
node, |
|||
getConnectedInputs, |
|||
updateNodeData, |
|||
getFreshNode, |
|||
signal, |
|||
providerSettings, |
|||
addIncurredCost, |
|||
generationsPath, |
|||
trackSaveGeneration, |
|||
} = ctx; |
|||
|
|||
const { useStoredFallback = false } = options; |
|||
|
|||
const { images: connectedImages, text: connectedText, dynamicInputs } = getConnectedInputs(node.id); |
|||
|
|||
// Get fresh node data from store
|
|||
const freshNode = getFreshNode(node.id); |
|||
const nodeData = (freshNode?.data || node.data) as Generate3DNodeData; |
|||
|
|||
// Determine images and text (with optional fallback to stored values)
|
|||
let images: string[]; |
|||
let promptText: string | null; |
|||
|
|||
if (useStoredFallback) { |
|||
images = connectedImages.length > 0 ? connectedImages : nodeData.inputImages; |
|||
promptText = connectedText ?? nodeData.inputPrompt; |
|||
} else { |
|||
images = connectedImages; |
|||
const promptFromDynamic = Array.isArray(dynamicInputs.prompt) |
|||
? dynamicInputs.prompt[0] |
|||
: dynamicInputs.prompt; |
|||
promptText = connectedText || promptFromDynamic || null; |
|||
} |
|||
|
|||
// 3D models may work with just images (image-to-3d) or just text (text-to-3d)
|
|||
if (!promptText && images.length === 0) { |
|||
updateNodeData(node.id, { |
|||
status: "error", |
|||
error: "Missing text or image input", |
|||
}); |
|||
throw new Error("Missing text or image input"); |
|||
} |
|||
|
|||
updateNodeData(node.id, { |
|||
inputImages: images, |
|||
inputPrompt: promptText, |
|||
status: "loading", |
|||
error: null, |
|||
}); |
|||
|
|||
const provider = nodeData.selectedModel?.provider || "fal"; |
|||
const headers = buildGenerateHeaders(provider, providerSettings); |
|||
|
|||
const requestPayload = { |
|||
images, |
|||
prompt: promptText || "", |
|||
selectedModel: nodeData.selectedModel, |
|||
parameters: nodeData.parameters, |
|||
dynamicInputs, |
|||
mediaType: "3d" as const, |
|||
}; |
|||
|
|||
try { |
|||
const response = await fetch("/api/generate", { |
|||
method: "POST", |
|||
headers, |
|||
body: JSON.stringify(requestPayload), |
|||
...(signal ? { signal } : {}), |
|||
}); |
|||
|
|||
if (!response.ok) { |
|||
const errorText = await response.text(); |
|||
let errorMessage = `HTTP ${response.status}`; |
|||
try { |
|||
const errorJson = JSON.parse(errorText); |
|||
errorMessage = errorJson.error || errorMessage; |
|||
} catch { |
|||
if (errorText) errorMessage += ` - ${errorText.substring(0, 200)}`; |
|||
} |
|||
|
|||
updateNodeData(node.id, { |
|||
status: "error", |
|||
error: errorMessage, |
|||
}); |
|||
throw new Error(errorMessage); |
|||
} |
|||
|
|||
const result = await response.json(); |
|||
|
|||
if (result.success && result.model3dUrl) { |
|||
updateNodeData(node.id, { |
|||
output3dUrl: result.model3dUrl, |
|||
status: "complete", |
|||
error: null, |
|||
}); |
|||
|
|||
// Track cost if applicable
|
|||
if (nodeData.selectedModel?.pricing) { |
|||
addIncurredCost(nodeData.selectedModel.pricing.amount); |
|||
} |
|||
|
|||
// Auto-save 3D model to generations folder if configured
|
|||
if (generationsPath) { |
|||
const savePromise = fetch("/api/save-generation", { |
|||
method: "POST", |
|||
headers: { "Content-Type": "application/json" }, |
|||
body: JSON.stringify({ |
|||
directoryPath: generationsPath, |
|||
model3d: result.model3dUrl, |
|||
prompt: promptText, |
|||
}), |
|||
}) |
|||
.then((res) => res.json()) |
|||
.catch((err) => { |
|||
console.error("Failed to save 3D model:", err); |
|||
}); |
|||
|
|||
trackSaveGeneration(`3d-${Date.now()}`, savePromise); |
|||
} |
|||
} else { |
|||
updateNodeData(node.id, { |
|||
status: "error", |
|||
error: result.error || "3D generation failed", |
|||
}); |
|||
throw new Error(result.error || "3D generation failed"); |
|||
} |
|||
} catch (error) { |
|||
if (error instanceof DOMException && error.name === "AbortError") { |
|||
throw error; |
|||
} |
|||
|
|||
let errorMessage = "3D generation failed"; |
|||
if (error instanceof TypeError && error.message.includes("NetworkError")) { |
|||
errorMessage = "Network error. Check your connection and try again."; |
|||
} else if (error instanceof TypeError) { |
|||
errorMessage = `Network error: ${error.message}`; |
|||
} else if (error instanceof Error) { |
|||
errorMessage = error.message; |
|||
} |
|||
|
|||
updateNodeData(node.id, { |
|||
status: "error", |
|||
error: errorMessage, |
|||
}); |
|||
throw new Error(errorMessage); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue