""" BPY MCP Server - Blender Chat Interface CPU-only 3D generation with SmolLM3 """ import os import tempfile import uuid import gradio as gr from huggingface_hub import snapshot_download import openvino_genai as ov_genai import bpy SMOLLM3_PIPE = None def load_smollm3(): global SMOLLM3_PIPE if SMOLLM3_PIPE is not None: return SMOLLM3_PIPE print("Loading SmolLM3...") model_path = snapshot_download("dev-bjoern/smollm3-int4-ov") SMOLLM3_PIPE = ov_genai.LLMPipeline(model_path, device="CPU") print("SmolLM3 loaded") return SMOLLM3_PIPE def render_scene() -> str: """Render with Cycles CPU (works headless)""" output_dir = tempfile.mkdtemp() render_path = f"{output_dir}/render_{uuid.uuid4().hex[:8]}.png" # Cycles CPU for headless bpy.context.scene.render.engine = 'CYCLES' bpy.context.scene.cycles.device = 'CPU' bpy.context.scene.cycles.samples = 32 # Render settings bpy.context.scene.render.filepath = render_path bpy.context.scene.render.image_settings.file_format = 'PNG' bpy.context.scene.render.resolution_x = 800 bpy.context.scene.render.resolution_y = 600 bpy.context.scene.render.resolution_percentage = 100 # Render bpy.ops.render.render(write_still=True) return render_path def export_glb() -> str: output_dir = tempfile.mkdtemp() glb_path = f"{output_dir}/scene_{uuid.uuid4().hex[:8]}.glb" bpy.ops.export_scene.gltf(filepath=glb_path, export_format='GLB') return glb_path def execute_bpy_code(code: str) -> bool: try: if "```python" in code: code = code.split("```python")[1].split("```")[0] elif "```" in code: parts = code.split("```") if len(parts) > 1: code = parts[1] code = code.replace("import bpy", "") exec(code, {"bpy": bpy, "math": __import__("math")}) return True except Exception as e: print(f"Error: {e}") return False def chat_with_blender(message: str, history: list): try: pipe = load_smollm3() prompt = f"""Write bpy Python code for: {message} Rules: 1. Clear scene: bpy.ops.object.select_all(action='SELECT'); bpy.ops.object.delete() 2. Use bpy.ops.mesh.primitive_* for objects 3. Add camera: bpy.ops.object.camera_add() 4. Add light: bpy.ops.object.light_add(type='SUN') Only Python code, no explanations.""" result = pipe.generate(prompt, max_new_tokens=512) success = execute_bpy_code(result) if success: render_path = render_scene() return f"Done!\n```python\n{result}\n```", render_path else: return f"Error:\n```python\n{result}\n```", None except Exception as e: return f"Error: {e}", None with gr.Blocks(title="BPY Chat") as demo: gr.Markdown("## Blender Chat") chatbot = gr.Chatbot(height=300) render_output = gr.Image(label="Render Preview") with gr.Row(): msg = gr.Textbox(placeholder="Describe a 3D scene...", show_label=False, scale=9) btn = gr.Button("Send", variant="primary", scale=1) def respond(message, chat_history): if not message.strip(): return "", chat_history, None response, render_path = chat_with_blender(message, chat_history) chat_history.append({"role": "user", "content": message}) chat_history.append({"role": "assistant", "content": response}) return "", chat_history, render_path btn.click(respond, [msg, chatbot], [msg, chatbot, render_output]) msg.submit(respond, [msg, chatbot], [msg, chatbot, render_output]) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860, mcp_server=True)