dev-bjoern commited on
Commit
0467e53
·
1 Parent(s): 7e7dd3d

Einfach: Chat + GLB Export, kein Rendering, kein Docker

Browse files
Files changed (3) hide show
  1. Dockerfile +0 -23
  2. README.md +3 -1
  3. app.py +28 -235
Dockerfile DELETED
@@ -1,23 +0,0 @@
1
- FROM python:3.11-slim
2
-
3
- # EGL/OpenGL for bpy rendering
4
- RUN apt-get update && apt-get install -y --no-install-recommends \
5
- libegl1 \
6
- libgl1 \
7
- libglib2.0-0 \
8
- libgomp1 \
9
- libsm6 \
10
- libxext6 \
11
- libxrender1 \
12
- && rm -rf /var/lib/apt/lists/*
13
-
14
- WORKDIR /app
15
-
16
- COPY requirements.txt .
17
- RUN pip install --no-cache-dir -r requirements.txt
18
-
19
- COPY . .
20
-
21
- EXPOSE 7860
22
-
23
- CMD ["python", "app.py"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -3,7 +3,9 @@ title: BPY MCP
3
  emoji: 🎨
4
  colorFrom: yellow
5
  colorTo: purple
6
- sdk: docker
 
 
7
  app_file: app.py
8
  pinned: false
9
  license: mit
 
3
  emoji: 🎨
4
  colorFrom: yellow
5
  colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: 6.0.2
8
+ python_version: "3.11"
9
  app_file: app.py
10
  pinned: false
11
  license: mit
app.py CHANGED
@@ -1,308 +1,101 @@
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
12
  from huggingface_hub import snapshot_download
13
 
14
- # OpenVINO imports
15
  import openvino_genai as ov_genai
16
-
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():
121
- """Load SmolLM3 OpenVINO model for text generation"""
122
  global SMOLLM3_PIPE
123
-
124
  if SMOLLM3_PIPE is not None:
125
  return SMOLLM3_PIPE
126
-
127
- print("Loading SmolLM3 INT4 OpenVINO...")
128
  model_path = snapshot_download("dev-bjoern/smollm3-int4-ov")
129
  SMOLLM3_PIPE = ov_genai.LLMPipeline(model_path, device="CPU")
130
  print("SmolLM3 loaded")
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(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
- buttons=[]
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
 
301
  if __name__ == "__main__":
302
- demo.launch(
303
- server_name="0.0.0.0",
304
- server_port=7860,
305
- mcp_server=True,
306
- theme=gr.themes.Glass(),
307
- css=GLASS_CSS
308
- )
 
1
  """
2
+ BPY MCP Server - Blender Chat Interface
3
  CPU-only 3D generation with SmolLM3
4
  """
5
  import os
6
  import tempfile
7
  import uuid
 
8
 
9
  import gradio as gr
 
10
  from huggingface_hub import snapshot_download
11
 
 
12
  import openvino_genai as ov_genai
 
 
13
  import bpy
14
 
 
15
  SMOLLM3_PIPE = None
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  def load_smollm3():
 
19
  global SMOLLM3_PIPE
 
20
  if SMOLLM3_PIPE is not None:
21
  return SMOLLM3_PIPE
22
+ print("Loading SmolLM3...")
 
23
  model_path = snapshot_download("dev-bjoern/smollm3-int4-ov")
24
  SMOLLM3_PIPE = ov_genai.LLMPipeline(model_path, device="CPU")
25
  print("SmolLM3 loaded")
26
  return SMOLLM3_PIPE
27
 
28
 
29
+ def export_glb() -> str:
 
30
  output_dir = tempfile.mkdtemp()
31
+ glb_path = f"{output_dir}/scene_{uuid.uuid4().hex[:8]}.glb"
32
+ bpy.ops.export_scene.gltf(filepath=glb_path, export_format='GLB')
33
+ return glb_path
 
 
 
 
 
 
 
 
 
 
34
 
35
 
36
  def execute_bpy_code(code: str) -> bool:
 
37
  try:
 
38
  if "```python" in code:
39
  code = code.split("```python")[1].split("```")[0]
40
  elif "```" in code:
41
  parts = code.split("```")
42
  if len(parts) > 1:
43
  code = parts[1]
44
+ code = code.replace("import bpy", "")
 
 
 
45
  exec(code, {"bpy": bpy, "math": __import__("math")})
46
  return True
47
  except Exception as e:
48
+ print(f"Error: {e}")
49
  return False
50
 
51
 
52
  def chat_with_blender(message: str, history: list):
 
 
 
53
  try:
54
  pipe = load_smollm3()
55
 
56
+ prompt = f"""Write bpy Python code for: {message}
 
57
 
58
+ Rules:
59
+ 1. Clear scene: bpy.ops.object.select_all(action='SELECT'); bpy.ops.object.delete()
60
+ 2. Use bpy.ops.mesh.primitive_* for objects
61
+ 3. Add camera: bpy.ops.object.camera_add()
62
+ 4. Add light: bpy.ops.object.light_add(type='SUN')
 
63
 
64
+ Only Python code, no explanations."""
65
 
 
66
  result = pipe.generate(prompt, max_new_tokens=512)
 
 
67
  success = execute_bpy_code(result)
68
 
69
  if success:
70
+ glb_path = export_glb()
71
+ return f"Done!\n```python\n{result}\n```", glb_path
 
 
72
  else:
73
+ return f"Error:\n```python\n{result}\n```", None
74
 
75
  except Exception as e:
76
  return f"Error: {e}", None
77
 
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  with gr.Blocks(title="BPY Chat") as demo:
80
+ gr.Markdown("## Blender Chat")
81
 
82
+ chatbot = gr.Chatbot(height=400)
83
+ model_output = gr.Model3D(label="3D Scene")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
  with gr.Row():
86
+ msg = gr.Textbox(placeholder="Describe a 3D scene...", show_label=False, scale=9)
87
+ btn = gr.Button("Send", variant="primary", scale=1)
 
 
 
 
 
88
 
 
89
  def respond(message, chat_history):
90
  if not message.strip():
91
  return "", chat_history, None
92
+ response, glb_path = chat_with_blender(message, chat_history)
 
93
  chat_history.append((message, response))
94
+ return "", chat_history, glb_path
 
 
 
 
 
 
 
 
 
 
 
95
 
96
+ btn.click(respond, [msg, chatbot], [msg, chatbot, model_output])
97
+ msg.submit(respond, [msg, chatbot], [msg, chatbot, model_output])
 
 
98
 
99
 
100
  if __name__ == "__main__":
101
+ demo.launch(server_name="0.0.0.0", server_port=7860, mcp_server=True)