dev-bjoern commited on
Commit
133c124
·
1 Parent(s): 8906f9a

Chat Interface mit Glass Theme - fullscreen render background, glassmorphism overlay

Browse files
Files changed (1) hide show
  1. app.py +229 -199
app.py CHANGED
@@ -1,12 +1,11 @@
1
  """
2
- BPY MCP Server - Blender Python API with OpenVINO SmolVLM/SmolLM3
3
- CPU-only 3D generation
4
  """
5
  import os
6
  import tempfile
7
  import uuid
8
  from pathlib import Path
9
- from typing import Optional
10
 
11
  import gradio as gr
12
  import numpy as np
@@ -18,23 +17,104 @@ import openvino_genai as ov_genai
18
  # Blender Python API
19
  import bpy
20
 
21
- # Global models
22
- SMOLVLM_PIPE = None
23
  SMOLLM3_PIPE = None
24
 
25
-
26
- def load_smolvlm():
27
- """Load SmolVLM OpenVINO model for image understanding"""
28
- global SMOLVLM_PIPE
29
-
30
- if SMOLVLM_PIPE is not None:
31
- return SMOLVLM_PIPE
32
-
33
- print("Loading SmolVLM INT4 OpenVINO...")
34
- model_path = snapshot_download("dev-bjoern/smolvlm-int4-ov")
35
- SMOLVLM_PIPE = ov_genai.VLMPipeline(model_path, device="CPU")
36
- print("SmolVLM loaded")
37
- return SMOLVLM_PIPE
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
 
40
  def load_smollm3():
@@ -51,220 +131,170 @@ def load_smollm3():
51
  return SMOLLM3_PIPE
52
 
53
 
54
- def analyze_image(image: np.ndarray, prompt: str = "Describe this image for 3D scene creation") -> str:
55
- """Analyze image with SmolVLM"""
56
- if image is None:
57
- return "No image provided"
58
-
59
- try:
60
- pipe = load_smolvlm()
61
-
62
- # Save image temporarily using bpy
63
- temp_path = f"{tempfile.mkdtemp()}/input_{uuid.uuid4().hex[:8]}.png"
64
-
65
- # Use bpy to save image
66
- img = bpy.data.images.new("temp_input", width=image.shape[1], height=image.shape[0])
67
- img.pixels = (image / 255.0).flatten().tolist()
68
- img.filepath_raw = temp_path
69
- img.file_format = 'PNG'
70
- img.save()
71
- bpy.data.images.remove(img)
72
-
73
- # Generate description
74
- result = pipe.generate(
75
- prompt,
76
- image=temp_path,
77
- max_new_tokens=256
78
- )
79
-
80
- return result
81
- except Exception as e:
82
- return f"Error: {e}"
83
 
 
 
 
 
 
 
84
 
85
- def generate_bpy_script(scene_type: str, style: str = "realistic") -> str:
86
- """Generate a bpy Python script with SmolLM3"""
87
- try:
88
- pipe = load_smollm3()
89
 
90
- prompt = f"""Write a Python script using bpy (Blender Python API) to create a {style} {scene_type} 3D scene.
91
 
92
- The script must:
93
- 1. Clear existing objects with bpy.ops.object.select_all(action='SELECT') and bpy.ops.object.delete()
94
- 2. Create mesh objects using bpy.ops.mesh.primitive_* functions
95
- 3. Position objects with location parameter
96
- 4. Add a camera with bpy.ops.object.camera_add()
97
- 5. Add lighting with bpy.ops.object.light_add()
98
- 6. Set materials using bpy.data.materials.new()
99
 
100
- Only output valid Python code, no explanations. Start with import bpy."""
 
 
 
 
 
 
 
 
 
101
 
102
- result = pipe.generate(
103
- prompt,
104
- max_new_tokens=1024
105
- )
106
 
107
- return result
 
 
108
  except Exception as e:
109
- return f"# Error: {e}"
 
110
 
111
 
112
- def create_primitive(primitive_type: str, name: str = "Object", location: tuple = (0, 0, 0)) -> str:
113
- """Create a Blender primitive object"""
 
 
114
  try:
115
- # Clear existing objects
116
- bpy.ops.object.select_all(action='SELECT')
117
- bpy.ops.object.delete()
118
 
119
- # Create primitive
120
- if primitive_type == "cube":
121
- bpy.ops.mesh.primitive_cube_add(location=location)
122
- elif primitive_type == "sphere":
123
- bpy.ops.mesh.primitive_uv_sphere_add(location=location)
124
- elif primitive_type == "cylinder":
125
- bpy.ops.mesh.primitive_cylinder_add(location=location)
126
- elif primitive_type == "cone":
127
- bpy.ops.mesh.primitive_cone_add(location=location)
128
- elif primitive_type == "torus":
129
- bpy.ops.mesh.primitive_torus_add(location=location)
130
- elif primitive_type == "plane":
131
- bpy.ops.mesh.primitive_plane_add(location=location)
132
- elif primitive_type == "monkey":
133
- bpy.ops.mesh.primitive_monkey_add(location=location)
134
- else:
135
- return f"Unknown primitive: {primitive_type}"
136
 
137
- # Rename object
138
- obj = bpy.context.active_object
139
- obj.name = name
 
 
 
140
 
141
- # Export to GLB
142
- output_dir = tempfile.mkdtemp()
143
- glb_path = f"{output_dir}/{name}_{uuid.uuid4().hex[:8]}.glb"
144
 
145
- bpy.ops.export_scene.gltf(
146
- filepath=glb_path,
147
- export_format='GLB'
148
- )
 
 
 
 
 
 
 
 
 
149
 
150
- return glb_path
151
  except Exception as e:
152
- return f"Error: {e}"
153
 
154
 
155
- def execute_bpy_script(script: str) -> str:
156
- """Execute a bpy script and export to GLB"""
157
  try:
158
- # Extract just the Python code (remove markdown if present)
159
- code = script
160
- if "```python" in code:
161
- code = code.split("```python")[1].split("```")[0]
162
- elif "```" in code:
163
- parts = code.split("```")
164
- if len(parts) > 1:
165
- code = parts[1]
166
 
167
- # Remove import bpy if present (already imported)
168
- code = code.replace("import bpy", "# import bpy (already loaded)")
 
169
 
170
- # Execute the script
171
- exec(code, {"bpy": bpy, "math": __import__("math")})
 
 
172
 
173
- # Export to GLB
174
- output_dir = tempfile.mkdtemp()
175
- glb_path = f"{output_dir}/scene_{uuid.uuid4().hex[:8]}.glb"
 
 
176
 
177
- bpy.ops.export_scene.gltf(
178
- filepath=glb_path,
179
- export_format='GLB'
180
- )
181
 
182
- return glb_path
183
  except Exception as e:
184
- return f"Error executing script: {e}"
 
185
 
186
 
187
  # Gradio Interface
188
- with gr.Blocks(title="BPY MCP") as demo:
189
- gr.Markdown("""
190
- # 🎨 BPY MCP Server
191
- **Blender Python API + OpenVINO AI** - CPU-only
192
-
193
- Using SmolVLM for image understanding and SmolLM3 for text generation
194
- """)
 
 
 
 
 
 
 
195
 
196
- with gr.Tab("Image Analysis (SmolVLM)"):
197
- with gr.Row():
198
- with gr.Column():
199
- img_input = gr.Image(label="Input Image", type="numpy")
200
- img_prompt = gr.Textbox(
201
- label="Prompt",
202
- value="Describe this image for 3D scene creation"
203
- )
204
- img_btn = gr.Button("🔍 Analyze", variant="primary")
205
- with gr.Column():
206
- img_output = gr.Textbox(label="Description", lines=10)
207
-
208
- img_btn.click(analyze_image, [img_input, img_prompt], img_output)
209
-
210
- with gr.Tab("Scene Generator (SmolLM3)"):
211
- with gr.Row():
212
- with gr.Column():
213
- scene_type = gr.Dropdown(
214
- ["forest", "desert", "city", "interior", "space", "underwater"],
215
- label="Scene Type",
216
- value="forest"
217
- )
218
- scene_style = gr.Dropdown(
219
- ["realistic", "stylized", "low-poly", "fantasy"],
220
- label="Style",
221
- value="realistic"
222
- )
223
- scene_btn = gr.Button("📝 Generate BPY Script", variant="primary")
224
- with gr.Column():
225
- scene_desc = gr.Textbox(label="Generated BPY Script", lines=10)
226
-
227
- scene_btn.click(generate_bpy_script, [scene_type, scene_style], scene_desc)
228
-
229
- with gr.Row():
230
- create_btn = gr.Button("🎬 Execute Script & Create 3D", variant="secondary")
231
- scene_model = gr.Model3D(label="3D Scene")
232
-
233
- create_btn.click(execute_bpy_script, scene_desc, scene_model)
234
-
235
- with gr.Tab("Primitives"):
236
- with gr.Row():
237
- with gr.Column():
238
- prim_type = gr.Dropdown(
239
- ["cube", "sphere", "cylinder", "cone", "torus", "plane", "monkey"],
240
- label="Primitive Type",
241
- value="cube"
242
- )
243
- prim_name = gr.Textbox(label="Name", value="MyObject")
244
- prim_btn = gr.Button("🔲 Create Primitive", variant="primary")
245
- with gr.Column():
246
- prim_model = gr.Model3D(label="3D Preview")
247
- prim_file = gr.File(label="Download GLB")
248
-
249
- prim_btn.click(create_primitive, [prim_type, prim_name], prim_model)
250
- prim_model.change(lambda x: x, inputs=[prim_model], outputs=[prim_file])
251
 
252
  gr.Markdown("""
253
  ---
254
- ### MCP Server
255
- ```json
256
- {
257
- "mcpServers": {
258
- "bpy-mcp": {
259
- "url": "https://dev-bjoern-bpy-mcp.hf.space/gradio_api/mcp/sse"
260
- }
261
- }
262
- }
263
- ```
264
-
265
- ### Models
266
- - **SmolVLM**: `dev-bjoern/smolvlm-int4-ov` (Image-to-Text)
267
- - **SmolLM3**: `dev-bjoern/smollm3-int4-ov` (Text Generation)
268
  """)
269
 
270
 
 
1
  """
2
+ BPY MCP Server - Blender Chat Interface with Glass Theme
3
+ CPU-only 3D generation with SmolLM3
4
  """
5
  import os
6
  import tempfile
7
  import uuid
8
  from pathlib import Path
 
9
 
10
  import gradio as gr
11
  import numpy as np
 
17
  # Blender Python API
18
  import bpy
19
 
20
+ # Global model
 
21
  SMOLLM3_PIPE = None
22
 
23
+ # Glassmorphism CSS
24
+ GLASS_CSS = """
25
+ /* Fullscreen Render als Background */
26
+ #render-bg {
27
+ position: fixed !important;
28
+ top: 0 !important;
29
+ left: 0 !important;
30
+ width: 100vw !important;
31
+ height: 100vh !important;
32
+ z-index: 0 !important;
33
+ object-fit: cover !important;
34
+ pointer-events: none !important;
35
+ }
36
+
37
+ #render-bg img {
38
+ width: 100% !important;
39
+ height: 100% !important;
40
+ object-fit: cover !important;
41
+ }
42
+
43
+ /* Container transparent */
44
+ .gradio-container {
45
+ background: transparent !important;
46
+ position: relative;
47
+ z-index: 1;
48
+ }
49
+
50
+ /* Chat Overlay mit Glass-Effekt */
51
+ .glass-chat {
52
+ position: relative !important;
53
+ z-index: 10 !important;
54
+ background: rgba(0, 0, 0, 0.3) !important;
55
+ backdrop-filter: blur(20px) !important;
56
+ -webkit-backdrop-filter: blur(20px) !important;
57
+ border-radius: 20px !important;
58
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
59
+ }
60
+
61
+ .glass-chat .bubble-wrap {
62
+ background: transparent !important;
63
+ }
64
+
65
+ .glass-chat .message {
66
+ background: rgba(255, 255, 255, 0.1) !important;
67
+ backdrop-filter: blur(10px) !important;
68
+ -webkit-backdrop-filter: blur(10px) !important;
69
+ border: 1px solid rgba(255, 255, 255, 0.15) !important;
70
+ border-radius: 16px !important;
71
+ }
72
+
73
+ /* User message */
74
+ .glass-chat .message.user {
75
+ background: rgba(100, 150, 255, 0.2) !important;
76
+ }
77
+
78
+ /* Bot message */
79
+ .glass-chat .message.bot {
80
+ background: rgba(255, 255, 255, 0.1) !important;
81
+ }
82
+
83
+ /* Input textbox glass */
84
+ .glass-input textarea {
85
+ background: rgba(255, 255, 255, 0.1) !important;
86
+ backdrop-filter: blur(10px) !important;
87
+ border: 1px solid rgba(255, 255, 255, 0.2) !important;
88
+ border-radius: 12px !important;
89
+ color: white !important;
90
+ }
91
+
92
+ /* Buttons glass */
93
+ button.primary {
94
+ background: rgba(100, 150, 255, 0.3) !important;
95
+ backdrop-filter: blur(10px) !important;
96
+ border: 1px solid rgba(255, 255, 255, 0.2) !important;
97
+ }
98
+
99
+ /* Header transparent */
100
+ .app-header, header {
101
+ background: transparent !important;
102
+ }
103
+
104
+ /* Dark text on glass */
105
+ .glass-chat .message p, .glass-chat .message code {
106
+ color: white !important;
107
+ }
108
+
109
+ /* Hide default background */
110
+ .main, .contain, .wrap {
111
+ background: transparent !important;
112
+ }
113
+
114
+ body {
115
+ background: #1a1a2e !important;
116
+ }
117
+ """
118
 
119
 
120
  def load_smollm3():
 
131
  return SMOLLM3_PIPE
132
 
133
 
134
+ def render_scene() -> str:
135
+ """Render current Blender scene to image"""
136
+ output_dir = tempfile.mkdtemp()
137
+ render_path = f"{output_dir}/render_{uuid.uuid4().hex[:8]}.png"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
+ # Setup render settings
140
+ bpy.context.scene.render.filepath = render_path
141
+ bpy.context.scene.render.image_settings.file_format = 'PNG'
142
+ bpy.context.scene.render.resolution_x = 1920
143
+ bpy.context.scene.render.resolution_y = 1080
144
+ bpy.context.scene.render.resolution_percentage = 50
145
 
146
+ # Render
147
+ bpy.ops.render.render(write_still=True)
 
 
148
 
149
+ return render_path
150
 
 
 
 
 
 
 
 
151
 
152
+ def execute_bpy_code(code: str) -> bool:
153
+ """Execute bpy Python code"""
154
+ try:
155
+ # Clean code
156
+ if "```python" in code:
157
+ code = code.split("```python")[1].split("```")[0]
158
+ elif "```" in code:
159
+ parts = code.split("```")
160
+ if len(parts) > 1:
161
+ code = parts[1]
162
 
163
+ code = code.replace("import bpy", "# import bpy (already loaded)")
 
 
 
164
 
165
+ # Execute
166
+ exec(code, {"bpy": bpy, "math": __import__("math")})
167
+ return True
168
  except Exception as e:
169
+ print(f"Exec error: {e}")
170
+ return False
171
 
172
 
173
+ def chat_with_blender(message: str, history: list):
174
+ """
175
+ Chat mit SmolLM3 - generiert bpy Script und fuehrt aus
176
+ """
177
  try:
178
+ pipe = load_smollm3()
 
 
179
 
180
+ # Prompt fuer bpy Script Generierung
181
+ prompt = f"""Du bist ein Blender Python (bpy) Experte. Schreibe ein kurzes bpy Script fuer: {message}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
 
183
+ Regeln:
184
+ 1. Starte mit: bpy.ops.object.select_all(action='SELECT'); bpy.ops.object.delete()
185
+ 2. Nutze bpy.ops.mesh.primitive_* fuer Objekte
186
+ 3. Fuege Kamera hinzu: bpy.ops.object.camera_add(location=(x,y,z))
187
+ 4. Fuege Licht hinzu: bpy.ops.object.light_add(type='SUN')
188
+ 5. Setze einfache Materialien mit bpy.data.materials.new()
189
 
190
+ Nur Python Code, keine Erklaerungen. Starte mit import bpy."""
 
 
191
 
192
+ # Generate script
193
+ result = pipe.generate(prompt, max_new_tokens=512)
194
+
195
+ # Execute the script
196
+ success = execute_bpy_code(result)
197
+
198
+ if success:
199
+ # Render scene
200
+ render_path = render_scene()
201
+ response = f"Scene erstellt!\n\n```python\n{result}\n```"
202
+ return response, render_path
203
+ else:
204
+ return f"Fehler beim Ausfuehren:\n```python\n{result}\n```", None
205
 
 
206
  except Exception as e:
207
+ return f"Error: {e}", None
208
 
209
 
210
+ def create_initial_scene():
211
+ """Create a default scene for startup"""
212
  try:
213
+ # Clear
214
+ bpy.ops.object.select_all(action='SELECT')
215
+ bpy.ops.object.delete()
 
 
 
 
 
216
 
217
+ # Add cube
218
+ bpy.ops.mesh.primitive_cube_add(location=(0, 0, 0))
219
+ cube = bpy.context.active_object
220
 
221
+ # Material
222
+ mat = bpy.data.materials.new(name="BlueMat")
223
+ mat.diffuse_color = (0.2, 0.4, 0.8, 1.0)
224
+ cube.data.materials.append(mat)
225
 
226
+ # Camera
227
+ bpy.ops.object.camera_add(location=(5, -5, 4))
228
+ cam = bpy.context.active_object
229
+ cam.rotation_euler = (1.1, 0, 0.8)
230
+ bpy.context.scene.camera = cam
231
 
232
+ # Light
233
+ bpy.ops.object.light_add(type='SUN', location=(5, 5, 10))
 
 
234
 
235
+ return render_scene()
236
  except Exception as e:
237
+ print(f"Initial scene error: {e}")
238
+ return None
239
 
240
 
241
  # Gradio Interface
242
+ with gr.Blocks(css=GLASS_CSS, theme=gr.themes.Glass(), title="BPY Chat") as demo:
243
+
244
+ # State fuer Render
245
+ render_state = gr.State(value=None)
246
+
247
+ # Fullscreen Render Background
248
+ with gr.Column(elem_id="render-bg"):
249
+ render_output = gr.Image(
250
+ value=create_initial_scene,
251
+ label="",
252
+ show_label=False,
253
+ interactive=False,
254
+ show_download_button=False
255
+ )
256
 
257
+ # Chat Interface Overlay
258
+ gr.Markdown("## Blender Chat", elem_classes="glass-title")
259
+
260
+ chatbot = gr.Chatbot(
261
+ elem_classes="glass-chat",
262
+ height=400,
263
+ placeholder="Beschreibe eine 3D Szene..."
264
+ )
265
+
266
+ with gr.Row():
267
+ msg = gr.Textbox(
268
+ placeholder="z.B. 'Erstelle eine Pyramide mit rotem Material'",
269
+ show_label=False,
270
+ elem_classes="glass-input",
271
+ scale=9
272
+ )
273
+ submit_btn = gr.Button("Senden", variant="primary", scale=1)
274
+
275
+ # Chat logic
276
+ def respond(message, chat_history):
277
+ if not message.strip():
278
+ return "", chat_history, None
279
+
280
+ response, render_path = chat_with_blender(message, chat_history)
281
+ chat_history.append((message, response))
282
+ return "", chat_history, render_path
283
+
284
+ submit_btn.click(
285
+ respond,
286
+ [msg, chatbot],
287
+ [msg, chatbot, render_output]
288
+ )
289
+ msg.submit(
290
+ respond,
291
+ [msg, chatbot],
292
+ [msg, chatbot, render_output]
293
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
 
295
  gr.Markdown("""
296
  ---
297
+ **MCP Server:** `https://dev-bjoern-bpy-mcp.hf.space/gradio_api/mcp/sse`
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  """)
299
 
300