File size: 7,995 Bytes
d3b96a3
133c124
 
d3b96a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133c124
d3b96a3
 
133c124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d3b96a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133c124
 
 
 
d3b96a3
133c124
 
 
 
 
 
d3b96a3
133c124
 
d3b96a3
133c124
8906f9a
 
133c124
 
 
 
 
 
 
 
 
 
d3b96a3
133c124
d3b96a3
133c124
 
 
d3b96a3
133c124
 
d3b96a3
 
133c124
 
 
 
d3b96a3
133c124
d3b96a3
133c124
 
d3b96a3
133c124
 
 
 
 
 
d3b96a3
133c124
d3b96a3
133c124
 
 
 
 
 
 
 
 
 
 
 
 
d3b96a3
 
133c124
d3b96a3
 
133c124
 
d3b96a3
133c124
 
 
d3b96a3
133c124
 
 
d3b96a3
133c124
 
 
 
d3b96a3
133c124
 
 
 
 
d3b96a3
133c124
 
d3b96a3
133c124
d3b96a3
133c124
 
d3b96a3
 
 
133c124
 
 
 
 
 
 
 
 
 
 
 
 
 
d3b96a3
133c124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d3b96a3
 
 
133c124
d3b96a3
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
"""
BPY MCP Server - Blender Chat Interface with Glass Theme
CPU-only 3D generation with SmolLM3
"""
import os
import tempfile
import uuid
from pathlib import Path

import gradio as gr
import numpy as np
from huggingface_hub import snapshot_download

# OpenVINO imports
import openvino_genai as ov_genai

# Blender Python API
import bpy

# Global model
SMOLLM3_PIPE = None

# Glassmorphism CSS
GLASS_CSS = """
/* Fullscreen Render als Background */
#render-bg {
    position: fixed !important;
    top: 0 !important;
    left: 0 !important;
    width: 100vw !important;
    height: 100vh !important;
    z-index: 0 !important;
    object-fit: cover !important;
    pointer-events: none !important;
}

#render-bg img {
    width: 100% !important;
    height: 100% !important;
    object-fit: cover !important;
}

/* Container transparent */
.gradio-container {
    background: transparent !important;
    position: relative;
    z-index: 1;
}

/* Chat Overlay mit Glass-Effekt */
.glass-chat {
    position: relative !important;
    z-index: 10 !important;
    background: rgba(0, 0, 0, 0.3) !important;
    backdrop-filter: blur(20px) !important;
    -webkit-backdrop-filter: blur(20px) !important;
    border-radius: 20px !important;
    border: 1px solid rgba(255, 255, 255, 0.1) !important;
}

.glass-chat .bubble-wrap {
    background: transparent !important;
}

.glass-chat .message {
    background: rgba(255, 255, 255, 0.1) !important;
    backdrop-filter: blur(10px) !important;
    -webkit-backdrop-filter: blur(10px) !important;
    border: 1px solid rgba(255, 255, 255, 0.15) !important;
    border-radius: 16px !important;
}

/* User message */
.glass-chat .message.user {
    background: rgba(100, 150, 255, 0.2) !important;
}

/* Bot message */
.glass-chat .message.bot {
    background: rgba(255, 255, 255, 0.1) !important;
}

/* Input textbox glass */
.glass-input textarea {
    background: rgba(255, 255, 255, 0.1) !important;
    backdrop-filter: blur(10px) !important;
    border: 1px solid rgba(255, 255, 255, 0.2) !important;
    border-radius: 12px !important;
    color: white !important;
}

/* Buttons glass */
button.primary {
    background: rgba(100, 150, 255, 0.3) !important;
    backdrop-filter: blur(10px) !important;
    border: 1px solid rgba(255, 255, 255, 0.2) !important;
}

/* Header transparent */
.app-header, header {
    background: transparent !important;
}

/* Dark text on glass */
.glass-chat .message p, .glass-chat .message code {
    color: white !important;
}

/* Hide default background */
.main, .contain, .wrap {
    background: transparent !important;
}

body {
    background: #1a1a2e !important;
}
"""


def load_smollm3():
    """Load SmolLM3 OpenVINO model for text generation"""
    global SMOLLM3_PIPE

    if SMOLLM3_PIPE is not None:
        return SMOLLM3_PIPE

    print("Loading SmolLM3 INT4 OpenVINO...")
    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 current Blender scene to image"""
    output_dir = tempfile.mkdtemp()
    render_path = f"{output_dir}/render_{uuid.uuid4().hex[:8]}.png"

    # Setup render settings
    bpy.context.scene.render.filepath = render_path
    bpy.context.scene.render.image_settings.file_format = 'PNG'
    bpy.context.scene.render.resolution_x = 1920
    bpy.context.scene.render.resolution_y = 1080
    bpy.context.scene.render.resolution_percentage = 50

    # Render
    bpy.ops.render.render(write_still=True)

    return render_path


def execute_bpy_code(code: str) -> bool:
    """Execute bpy Python code"""
    try:
        # Clean code
        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", "# import bpy (already loaded)")

        # Execute
        exec(code, {"bpy": bpy, "math": __import__("math")})
        return True
    except Exception as e:
        print(f"Exec error: {e}")
        return False


def chat_with_blender(message: str, history: list):
    """
    Chat mit SmolLM3 - generiert bpy Script und fuehrt aus
    """
    try:
        pipe = load_smollm3()

        # Prompt fuer bpy Script Generierung
        prompt = f"""Du bist ein Blender Python (bpy) Experte. Schreibe ein kurzes bpy Script fuer: {message}

Regeln:
1. Starte mit: bpy.ops.object.select_all(action='SELECT'); bpy.ops.object.delete()
2. Nutze bpy.ops.mesh.primitive_* fuer Objekte
3. Fuege Kamera hinzu: bpy.ops.object.camera_add(location=(x,y,z))
4. Fuege Licht hinzu: bpy.ops.object.light_add(type='SUN')
5. Setze einfache Materialien mit bpy.data.materials.new()

Nur Python Code, keine Erklaerungen. Starte mit import bpy."""

        # Generate script
        result = pipe.generate(prompt, max_new_tokens=512)

        # Execute the script
        success = execute_bpy_code(result)

        if success:
            # Render scene
            render_path = render_scene()
            response = f"Scene erstellt!\n\n```python\n{result}\n```"
            return response, render_path
        else:
            return f"Fehler beim Ausfuehren:\n```python\n{result}\n```", None

    except Exception as e:
        return f"Error: {e}", None


def create_initial_scene():
    """Create a default scene for startup"""
    try:
        # Clear
        bpy.ops.object.select_all(action='SELECT')
        bpy.ops.object.delete()

        # Add cube
        bpy.ops.mesh.primitive_cube_add(location=(0, 0, 0))
        cube = bpy.context.active_object

        # Material
        mat = bpy.data.materials.new(name="BlueMat")
        mat.diffuse_color = (0.2, 0.4, 0.8, 1.0)
        cube.data.materials.append(mat)

        # Camera
        bpy.ops.object.camera_add(location=(5, -5, 4))
        cam = bpy.context.active_object
        cam.rotation_euler = (1.1, 0, 0.8)
        bpy.context.scene.camera = cam

        # Light
        bpy.ops.object.light_add(type='SUN', location=(5, 5, 10))

        return render_scene()
    except Exception as e:
        print(f"Initial scene error: {e}")
        return None


# Gradio Interface
with gr.Blocks(css=GLASS_CSS, theme=gr.themes.Glass(), title="BPY Chat") as demo:

    # State fuer Render
    render_state = gr.State(value=None)

    # Fullscreen Render Background
    with gr.Column(elem_id="render-bg"):
        render_output = gr.Image(
            value=create_initial_scene,
            label="",
            show_label=False,
            interactive=False,
            show_download_button=False
        )

    # Chat Interface Overlay
    gr.Markdown("## Blender Chat", elem_classes="glass-title")

    chatbot = gr.Chatbot(
        elem_classes="glass-chat",
        height=400,
        placeholder="Beschreibe eine 3D Szene..."
    )

    with gr.Row():
        msg = gr.Textbox(
            placeholder="z.B. 'Erstelle eine Pyramide mit rotem Material'",
            show_label=False,
            elem_classes="glass-input",
            scale=9
        )
        submit_btn = gr.Button("Senden", variant="primary", scale=1)

    # Chat logic
    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((message, response))
        return "", chat_history, render_path

    submit_btn.click(
        respond,
        [msg, chatbot],
        [msg, chatbot, render_output]
    )
    msg.submit(
        respond,
        [msg, chatbot],
        [msg, chatbot, render_output]
    )

    gr.Markdown("""
    ---
    **MCP Server:** `https://dev-bjoern-bpy-mcp.hf.space/gradio_api/mcp/sse`
    """)


if __name__ == "__main__":
    demo.launch(server_name="0.0.0.0", server_port=7860, mcp_server=True)