diff --git a/lib/folk-blender.ts b/lib/folk-blender.ts index ed3b03f..b62927b 100644 --- a/lib/folk-blender.ts +++ b/lib/folk-blender.ts @@ -377,6 +377,8 @@ export class FolkBlender extends FolkShape { if (!h.available && this.#generateBtn) { this.#generateBtn.disabled = true; this.#generateBtn.title = (h.issues || []).join(", ") || "Blender service unavailable"; + } else if (h.warnings?.length && this.#generateBtn) { + this.#generateBtn.title = h.warnings.join(", "); } }).catch(() => { if (this.#generateBtn) { diff --git a/server/index.ts b/server/index.ts index c618af0..fd3aba1 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1569,7 +1569,7 @@ const RUNPOD_API_KEY = process.env.RUNPOD_API_KEY || ""; app.get("/api/blender-gen/health", async (c) => { const issues: string[] = []; - if (!RUNPOD_API_KEY) issues.push("RUNPOD_API_KEY not configured"); + const warnings: string[] = []; const OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434"; try { const res = await fetch(`${OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(3000) }); @@ -1577,12 +1577,11 @@ app.get("/api/blender-gen/health", async (c) => { } catch { issues.push("Ollama unreachable"); } - return c.json({ available: issues.length === 0, issues }); + if (!RUNPOD_API_KEY) warnings.push("RunPod not configured — script-only mode"); + return c.json({ available: issues.length === 0, issues, warnings }); }); app.post("/api/blender-gen", async (c) => { - if (!RUNPOD_API_KEY) return c.json({ error: "RUNPOD_API_KEY not configured" }, 503); - const { prompt } = await c.req.json(); if (!prompt) return c.json({ error: "prompt required" }, 400); @@ -1595,7 +1594,7 @@ app.post("/api/blender-gen", async (c) => { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - model: process.env.OLLAMA_MODEL || "llama3.1", + model: process.env.OLLAMA_MODEL || "qwen2.5:14b", prompt: `Generate a Blender Python script that creates: ${prompt}\n\nThe script should:\n- Import bpy\n- Clear the default scene\n- Create the described objects with materials\n- Set up basic lighting and camera\n- Render to /tmp/render.png at 1024x1024\n\nOnly output the Python code, no explanations.`, stream: false, }), @@ -1605,18 +1604,22 @@ app.post("/api/blender-gen", async (c) => { const llmData = await llmRes.json(); script = llmData.response || ""; // Extract code block if wrapped in markdown - const codeMatch = script.match(/```python\n([\s\S]*?)```/); - if (codeMatch) script = codeMatch[1]; + const codeMatch = script.match(/```(?:python)?\n([\s\S]*?)```/); + if (codeMatch) script = codeMatch[1].trim(); } } catch (e) { console.error("[blender-gen] LLM error:", e); } if (!script) { - return c.json({ error: "Failed to generate Blender script" }, 502); + return c.json({ error: "Failed to generate Blender script — is Ollama running?" }, 502); + } + + // Step 2: Execute on RunPod (headless Blender) — optional + if (!RUNPOD_API_KEY) { + return c.json({ script, render_url: null, blend_url: null }); } - // Step 2: Execute on RunPod (headless Blender) try { const runpodRes = await fetch("https://api.runpod.ai/v2/blender/runsync", { method: "POST", @@ -1633,7 +1636,6 @@ app.post("/api/blender-gen", async (c) => { }); if (!runpodRes.ok) { - // Return just the script if RunPod fails return c.json({ script, error_detail: "RunPod execution failed" }); } @@ -1644,7 +1646,6 @@ app.post("/api/blender-gen", async (c) => { blend_url: runpodData.output?.blend_url || null, }); } catch (e) { - // Return the script even if RunPod is unavailable return c.json({ script, error_detail: "RunPod unavailable" }); } });