Subham9126 commited on
Commit
4aa86e4
Β·
verified Β·
1 Parent(s): b5b8cb3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +49 -220
app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import gradio as gr
2
  import numpy as np
3
  import cv2
@@ -12,9 +13,6 @@ import io
12
 
13
  # Global variables to maintain state
14
  model = None
15
- current_selections = []
16
- next_selection_id = 0
17
- active_selection_id = None
18
 
19
  class SelectionManager:
20
  def __init__(self):
@@ -62,14 +60,12 @@ def load_yolo_model():
62
  """Load YOLO model with error handling"""
63
  global model
64
  try:
65
- # Using YOLOv8n for speed, can change to YOLOv8s, YOLOv8m, YOLOv8l, YOLOv8x for better accuracy
66
- model = YOLO('yolov8n.pt') # This will download the model on first run
67
  return "βœ… YOLO Model loaded successfully"
68
  except Exception as e:
69
  return f"❌ Error loading YOLO model: {str(e)}"
70
 
71
  def detect_objects(image, confidence_threshold=0.5, merge_with_existing=True):
72
- """Detect objects using YOLO model"""
73
  global model, selection_manager
74
 
75
  if model is None:
@@ -79,31 +75,25 @@ def detect_objects(image, confidence_threshold=0.5, merge_with_existing=True):
79
  return None, "❌ No image provided", ""
80
 
81
  try:
82
- # Convert PIL to numpy array for YOLO
83
  img_array = np.array(image)
84
 
85
- # Run YOLO detection
86
  start_time = time.time()
87
  results = model(img_array, conf=confidence_threshold, verbose=False)
88
- detection_time = (time.time() - start_time) * 1000 # Convert to ms
89
 
90
- # Clear existing YOLO detections if not merging
91
  if not merge_with_existing:
92
  selection_manager.clear_type('yolo')
93
 
94
- # Process detections
95
  detections_added = 0
96
  for result in results:
97
  boxes = result.boxes
98
  if boxes is not None:
99
  for box in boxes:
100
- # Get bounding box coordinates
101
  x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
102
  confidence = box.conf[0].cpu().numpy()
103
  class_id = int(box.cls[0].cpu().numpy())
104
  class_name = model.names[class_id]
105
 
106
- # Add selection
107
  selection_manager.add_selection(
108
  x=int(x1),
109
  y=int(y1),
@@ -115,7 +105,6 @@ def detect_objects(image, confidence_threshold=0.5, merge_with_existing=True):
115
  )
116
  detections_added += 1
117
 
118
- # Draw selections on image
119
  annotated_image = draw_selections(image)
120
 
121
  status_msg = f"βœ… Detected {detections_added} objects in {detection_time:.1f}ms"
@@ -127,42 +116,34 @@ def detect_objects(image, confidence_threshold=0.5, merge_with_existing=True):
127
  return image, f"❌ Detection error: {str(e)}", get_analysis_text(image)
128
 
129
  def draw_selections(image):
130
- """Draw all selections on the image"""
131
  if image is None:
132
  return None
133
 
134
- # Create a copy of the image to draw on
135
  img_copy = image.copy()
136
  draw = ImageDraw.Draw(img_copy)
137
 
138
- # Try to load a font, fallback to default if not available
139
  try:
140
  font = ImageFont.truetype("arial.ttf", 12)
141
  except:
142
  font = ImageFont.load_default()
143
 
144
- # Draw each selection
145
  for selection in selection_manager.selections:
146
  x, y, w, h = selection['x'], selection['y'], selection['width'], selection['height']
147
 
148
- # Choose color based on type
149
  if selection['type'] == 'yolo':
150
  outline_color = 'green'
151
- fill_color = (0, 255, 0, 60) # Semi-transparent green
152
  else:
153
  outline_color = 'blue'
154
- fill_color = (0, 0, 255, 60) # Semi-transparent blue
155
 
156
- # Draw rectangle
157
  draw.rectangle([x, y, x + w, y + h], outline=outline_color, width=2)
158
 
159
- # Draw semi-transparent fill
160
  overlay = Image.new('RGBA', img_copy.size, (0, 0, 0, 0))
161
  overlay_draw = ImageDraw.Draw(overlay)
162
  overlay_draw.rectangle([x, y, x + w, y + h], fill=fill_color)
163
  img_copy = Image.alpha_composite(img_copy.convert('RGBA'), overlay).convert('RGB')
164
 
165
- # Draw label
166
  if selection['label']:
167
  label_y = y - 15 if y > 15 else y + h + 5
168
  draw.text((x, label_y), selection['label'], fill=outline_color, font=font)
@@ -170,7 +151,6 @@ def draw_selections(image):
170
  return img_copy
171
 
172
  def get_analysis_text(image):
173
- """Generate analysis text for current selections"""
174
  if image is None:
175
  return "No image loaded"
176
 
@@ -187,11 +167,11 @@ def get_analysis_text(image):
187
  analysis = f"""
188
  ## πŸ“Š Space Analysis
189
 
190
- **Total Image Area:** {total_image_area:,} pxΒ²
191
- **Total Selected Area:** {total_selected_area:,} pxΒ²
192
- **Selected Region:** {selected_percentage:.1f}% of total
193
- **Extra Space:** {extra_percentage:.1f}% of total
194
- **Number of Selections:** {len(selection_manager.selections)}
195
 
196
  ### πŸ“‹ Selection Details:
197
  """
@@ -199,10 +179,10 @@ def get_analysis_text(image):
199
  for i, selection in enumerate(selection_manager.selections, 1):
200
  area = selection['width'] * selection['height']
201
  analysis += f"""
202
- **{i}.** {selection['label'] or f"Selection {selection['id']}"}
203
- - Type: {selection['type'].upper()}
204
- - Area: {area:,} pxΒ²
205
- - Dimensions: {selection['width']}Γ—{selection['height']}
206
  """
207
  if selection['confidence']:
208
  analysis += f"- Confidence: {selection['confidence']:.2f}\n"
@@ -210,27 +190,22 @@ def get_analysis_text(image):
210
  return analysis
211
 
212
  def add_manual_selection(image, selection_data):
213
- """Add manual selection from user input"""
214
  if image is None:
215
  return image, "❌ No image loaded", ""
216
 
217
  try:
218
- # Parse selection data (format: "x,y,width,height")
219
  coords = [int(x.strip()) for x in selection_data.split(',')]
220
  if len(coords) != 4:
221
  raise ValueError("Invalid format")
222
 
223
  x, y, width, height = coords
224
 
225
- # Validate coordinates
226
  img_width, img_height = image.size
227
  if x < 0 or y < 0 or x + width > img_width or y + height > img_height:
228
  return image, "❌ Selection coordinates out of bounds", get_analysis_text(image)
229
 
230
- # Add selection
231
- sel_id = selection_manager.add_selection(x, y, width, height, "manual", f"Manual {sel_id}")
232
 
233
- # Redraw image
234
  annotated_image = draw_selections(image)
235
  analysis_text = get_analysis_text(image)
236
 
@@ -240,14 +215,12 @@ def add_manual_selection(image, selection_data):
240
  return image, f"❌ Error adding selection: {str(e)}", get_analysis_text(image)
241
 
242
  def clear_all_selections(image):
243
- """Clear all selections"""
244
  selection_manager.clear_all()
245
  if image is not None:
246
  return image.copy(), "βœ… All selections cleared", get_analysis_text(image)
247
  return None, "βœ… All selections cleared", ""
248
 
249
  def clear_yolo_selections(image):
250
- """Clear only YOLO detections"""
251
  selection_manager.clear_type('yolo')
252
  if image is not None:
253
  annotated_image = draw_selections(image)
@@ -255,7 +228,6 @@ def clear_yolo_selections(image):
255
  return None, "βœ… YOLO detections cleared", ""
256
 
257
  def clear_manual_selections(image):
258
- """Clear only manual selections"""
259
  selection_manager.clear_type('manual')
260
  if image is not None:
261
  annotated_image = draw_selections(image)
@@ -263,205 +235,62 @@ def clear_manual_selections(image):
263
  return None, "βœ… Manual selections cleared", ""
264
 
265
  def process_image_upload(image):
266
- """Process uploaded image"""
267
  if image is None:
268
  return None, "❌ No image uploaded", ""
269
 
270
- # Clear previous selections
271
  selection_manager.clear_all()
272
-
273
- # Return original image and analysis
274
  analysis_text = get_analysis_text(image)
275
  return image, "βœ… Image loaded successfully", analysis_text
276
 
277
- # Custom CSS for better styling
278
- css = """
279
- .gradio-container {
280
- max-width: 1400px !important;
281
- }
282
- .analysis-text {
283
- font-family: 'Courier New', monospace;
284
- background-color: #f8f9fa;
285
- padding: 15px;
286
- border-radius: 8px;
287
- border-left: 4px solid #007bff;
288
- }
289
- """
290
-
291
- # Create Gradio interface
292
  def create_interface():
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  with gr.Blocks(css=css, title="Image Space Analyzer with YOLO") as demo:
294
- gr.Markdown("""
295
- # πŸ” Image Space Analyzer with YOLO
296
-
297
- **Fast object detection and space analysis tool**
298
-
299
- Upload an image to start analyzing space usage with automatic YOLO detection or manual selections.
300
- """)
301
 
302
  with gr.Row():
303
  with gr.Column(scale=2):
304
- # Image input/output
305
- image_input = gr.Image(
306
- type="pil",
307
- label="πŸ“Έ Upload Image",
308
- height=500
309
- )
310
-
311
- # Status display
312
- status_output = gr.Textbox(
313
- label="πŸ“‹ Status",
314
- interactive=False,
315
- max_lines=2
316
- )
317
-
318
  with gr.Column(scale=1):
319
- # Model status
320
- model_status = gr.Textbox(
321
- label="πŸ€– Model Status",
322
- value="Loading YOLO model...",
323
- interactive=False
324
- )
325
-
326
- # YOLO Detection Controls
327
  gr.Markdown("### 🎯 YOLO Object Detection")
328
-
329
- with gr.Row():
330
- confidence_slider = gr.Slider(
331
- minimum=0.1,
332
- maximum=1.0,
333
- value=0.5,
334
- step=0.1,
335
- label="Confidence Threshold"
336
- )
337
-
338
- merge_checkbox = gr.Checkbox(
339
- label="Merge with existing selections",
340
- value=True
341
- )
342
-
343
  detect_btn = gr.Button("πŸ” Detect Objects", variant="primary")
344
-
345
- # Manual Selection Controls
346
  gr.Markdown("### ✏️ Manual Selection")
347
-
348
- manual_input = gr.Textbox(
349
- label="Selection (x,y,width,height)",
350
- placeholder="100,100,200,150",
351
- info="Enter coordinates separated by commas"
352
- )
353
-
354
  add_manual_btn = gr.Button("βž• Add Manual Selection")
355
-
356
- # Selection Management
357
  gr.Markdown("### πŸ—‚οΈ Selection Management")
358
-
359
- with gr.Row():
360
- clear_all_btn = gr.Button("πŸ—‘οΈ Clear All", variant="secondary")
361
- clear_yolo_btn = gr.Button("πŸ—‘οΈ Clear YOLO")
362
- clear_manual_btn = gr.Button("πŸ—‘οΈ Clear Manual")
363
 
364
- # Analysis output
365
  with gr.Row():
366
- analysis_output = gr.Markdown(
367
- label="πŸ“Š Analysis Results",
368
- elem_classes=["analysis-text"]
369
- )
370
-
371
- # Event handlers
372
- image_input.upload(
373
- fn=process_image_upload,
374
- inputs=[image_input],
375
- outputs=[image_input, status_output, analysis_output]
376
- )
377
-
378
- detect_btn.click(
379
- fn=detect_objects,
380
- inputs=[image_input, confidence_slider, merge_checkbox],
381
- outputs=[image_input, status_output, analysis_output]
382
- )
383
-
384
- add_manual_btn.click(
385
- fn=add_manual_selection,
386
- inputs=[image_input, manual_input],
387
- outputs=[image_input, status_output, analysis_output]
388
- )
389
 
390
- clear_all_btn.click(
391
- fn=clear_all_selections,
392
- inputs=[image_input],
393
- outputs=[image_input, status_output, analysis_output]
394
- )
 
395
 
396
- clear_yolo_btn.click(
397
- fn=clear_yolo_selections,
398
- inputs=[image_input],
399
- outputs=[image_input, status_output, analysis_output]
400
- )
401
-
402
- clear_manual_btn.click(
403
- fn=clear_manual_selections,
404
- inputs=[image_input],
405
- outputs=[image_input, status_output, analysis_output]
406
- )
407
-
408
- # Load model on startup
409
- demo.load(
410
- fn=load_yolo_model,
411
- outputs=[model_status]
412
- )
413
-
414
- gr.Markdown("""
415
- ## πŸ“– How to Use:
416
-
417
- 1. **Upload an image** using the image upload area
418
- 2. **Adjust confidence threshold** for YOLO detection sensitivity
419
- 3. **Click "Detect Objects"** to automatically detect objects using YOLO
420
- 4. **Add manual selections** by entering coordinates (x,y,width,height)
421
- 5. **View analysis results** showing space usage statistics
422
- 6. **Manage selections** using the clear buttons
423
-
424
- ### πŸ”§ Features:
425
- - ⚑ **Ultra-fast YOLO detection** (millisecond response times)
426
- - 🎯 **High accuracy object detection** with confidence scores
427
- - ✏️ **Manual selection support** for custom regions
428
- - πŸ“Š **Detailed space analysis** with percentages and areas
429
- - πŸ—‚οΈ **Selection management** with type-based filtering
430
- - 🎨 **Visual feedback** with color-coded selections
431
-
432
- ### πŸš€ Performance:
433
- - Uses YOLOv8n for optimal speed/accuracy balance
434
- - GPU acceleration when available
435
- - Real-time analysis updates
436
- """)
437
 
438
  return demo
439
 
440
- # Additional requirements.txt content that should be installed:
441
- requirements = """
442
- gradio>=4.0.0
443
- ultralytics>=8.0.0
444
- torch>=2.0.0
445
- torchvision>=0.15.0
446
- opencv-python>=4.8.0
447
- numpy>=1.24.0
448
- Pillow>=10.0.0
449
- """
450
-
451
- if __name__ == "__main__":
452
- # Print requirements
453
- print("πŸ“‹ Required packages:")
454
- print(requirements)
455
- print("\n" + "="*50)
456
- print("πŸš€ Starting Image Space Analyzer with YOLO...")
457
- print("="*50)
458
-
459
- # Create and launch the interface
460
- demo = create_interface()
461
- demo.launch(
462
- server_name="0.0.0.0", # Allow external access
463
- server_port=7860, # Default Gradio port
464
- share=False, # Set to True to create public link
465
- debug=True,
466
- show_error=True
467
- )
 
1
+ # app.py
2
  import gradio as gr
3
  import numpy as np
4
  import cv2
 
13
 
14
  # Global variables to maintain state
15
  model = None
 
 
 
16
 
17
  class SelectionManager:
18
  def __init__(self):
 
60
  """Load YOLO model with error handling"""
61
  global model
62
  try:
63
+ model = YOLO('yolov8n.pt')
 
64
  return "βœ… YOLO Model loaded successfully"
65
  except Exception as e:
66
  return f"❌ Error loading YOLO model: {str(e)}"
67
 
68
  def detect_objects(image, confidence_threshold=0.5, merge_with_existing=True):
 
69
  global model, selection_manager
70
 
71
  if model is None:
 
75
  return None, "❌ No image provided", ""
76
 
77
  try:
 
78
  img_array = np.array(image)
79
 
 
80
  start_time = time.time()
81
  results = model(img_array, conf=confidence_threshold, verbose=False)
82
+ detection_time = (time.time() - start_time) * 1000
83
 
 
84
  if not merge_with_existing:
85
  selection_manager.clear_type('yolo')
86
 
 
87
  detections_added = 0
88
  for result in results:
89
  boxes = result.boxes
90
  if boxes is not None:
91
  for box in boxes:
 
92
  x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
93
  confidence = box.conf[0].cpu().numpy()
94
  class_id = int(box.cls[0].cpu().numpy())
95
  class_name = model.names[class_id]
96
 
 
97
  selection_manager.add_selection(
98
  x=int(x1),
99
  y=int(y1),
 
105
  )
106
  detections_added += 1
107
 
 
108
  annotated_image = draw_selections(image)
109
 
110
  status_msg = f"βœ… Detected {detections_added} objects in {detection_time:.1f}ms"
 
116
  return image, f"❌ Detection error: {str(e)}", get_analysis_text(image)
117
 
118
  def draw_selections(image):
 
119
  if image is None:
120
  return None
121
 
 
122
  img_copy = image.copy()
123
  draw = ImageDraw.Draw(img_copy)
124
 
 
125
  try:
126
  font = ImageFont.truetype("arial.ttf", 12)
127
  except:
128
  font = ImageFont.load_default()
129
 
 
130
  for selection in selection_manager.selections:
131
  x, y, w, h = selection['x'], selection['y'], selection['width'], selection['height']
132
 
 
133
  if selection['type'] == 'yolo':
134
  outline_color = 'green'
135
+ fill_color = (0, 255, 0, 60)
136
  else:
137
  outline_color = 'blue'
138
+ fill_color = (0, 0, 255, 60)
139
 
 
140
  draw.rectangle([x, y, x + w, y + h], outline=outline_color, width=2)
141
 
 
142
  overlay = Image.new('RGBA', img_copy.size, (0, 0, 0, 0))
143
  overlay_draw = ImageDraw.Draw(overlay)
144
  overlay_draw.rectangle([x, y, x + w, y + h], fill=fill_color)
145
  img_copy = Image.alpha_composite(img_copy.convert('RGBA'), overlay).convert('RGB')
146
 
 
147
  if selection['label']:
148
  label_y = y - 15 if y > 15 else y + h + 5
149
  draw.text((x, label_y), selection['label'], fill=outline_color, font=font)
 
151
  return img_copy
152
 
153
  def get_analysis_text(image):
 
154
  if image is None:
155
  return "No image loaded"
156
 
 
167
  analysis = f"""
168
  ## πŸ“Š Space Analysis
169
 
170
+ **Total Image Area:** {total_image_area:,} pxΒ²
171
+ **Total Selected Area:** {total_selected_area:,} pxΒ²
172
+ **Selected Region:** {selected_percentage:.1f}% of total
173
+ **Extra Space:** {extra_percentage:.1f}% of total
174
+ **Number of Selections:** {len(selection_manager.selections)}
175
 
176
  ### πŸ“‹ Selection Details:
177
  """
 
179
  for i, selection in enumerate(selection_manager.selections, 1):
180
  area = selection['width'] * selection['height']
181
  analysis += f"""
182
+ **{i}.** {selection['label'] or f"Selection {selection['id']}"}
183
+ - Type: {selection['type'].upper()}
184
+ - Area: {area:,} pxΒ²
185
+ - Dimensions: {selection['width']}Γ—{selection['height']}
186
  """
187
  if selection['confidence']:
188
  analysis += f"- Confidence: {selection['confidence']:.2f}\n"
 
190
  return analysis
191
 
192
  def add_manual_selection(image, selection_data):
 
193
  if image is None:
194
  return image, "❌ No image loaded", ""
195
 
196
  try:
 
197
  coords = [int(x.strip()) for x in selection_data.split(',')]
198
  if len(coords) != 4:
199
  raise ValueError("Invalid format")
200
 
201
  x, y, width, height = coords
202
 
 
203
  img_width, img_height = image.size
204
  if x < 0 or y < 0 or x + width > img_width or y + height > img_height:
205
  return image, "❌ Selection coordinates out of bounds", get_analysis_text(image)
206
 
207
+ selection_manager.add_selection(x, y, width, height, "manual", "Manual Selection")
 
208
 
 
209
  annotated_image = draw_selections(image)
210
  analysis_text = get_analysis_text(image)
211
 
 
215
  return image, f"❌ Error adding selection: {str(e)}", get_analysis_text(image)
216
 
217
  def clear_all_selections(image):
 
218
  selection_manager.clear_all()
219
  if image is not None:
220
  return image.copy(), "βœ… All selections cleared", get_analysis_text(image)
221
  return None, "βœ… All selections cleared", ""
222
 
223
  def clear_yolo_selections(image):
 
224
  selection_manager.clear_type('yolo')
225
  if image is not None:
226
  annotated_image = draw_selections(image)
 
228
  return None, "βœ… YOLO detections cleared", ""
229
 
230
  def clear_manual_selections(image):
 
231
  selection_manager.clear_type('manual')
232
  if image is not None:
233
  annotated_image = draw_selections(image)
 
235
  return None, "βœ… Manual selections cleared", ""
236
 
237
  def process_image_upload(image):
 
238
  if image is None:
239
  return None, "❌ No image uploaded", ""
240
 
 
241
  selection_manager.clear_all()
 
 
242
  analysis_text = get_analysis_text(image)
243
  return image, "βœ… Image loaded successfully", analysis_text
244
 
245
+ # Gradio Interface creation (exported as app)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  def create_interface():
247
+ css = """
248
+ .gradio-container {
249
+ max-width: 1400px !important;
250
+ }
251
+ .analysis-text {
252
+ font-family: 'Courier New', monospace;
253
+ background-color: #f8f9fa;
254
+ padding: 15px;
255
+ border-radius: 8px;
256
+ border-left: 4px solid #007bff;
257
+ }
258
+ """
259
+
260
  with gr.Blocks(css=css, title="Image Space Analyzer with YOLO") as demo:
261
+ gr.Markdown("# πŸ” Image Space Analyzer with YOLO")
 
 
 
 
 
 
262
 
263
  with gr.Row():
264
  with gr.Column(scale=2):
265
+ image_input = gr.Image(type="pil", label="πŸ“Έ Upload Image", height=500)
266
+ status_output = gr.Textbox(label="πŸ“‹ Status", interactive=False, max_lines=2)
 
 
 
 
 
 
 
 
 
 
 
 
267
  with gr.Column(scale=1):
268
+ model_status = gr.Textbox(label="πŸ€– Model Status", value="Loading YOLO model...", interactive=False)
 
 
 
 
 
 
 
269
  gr.Markdown("### 🎯 YOLO Object Detection")
270
+ confidence_slider = gr.Slider(minimum=0.1, maximum=1.0, value=0.5, step=0.1, label="Confidence Threshold")
271
+ merge_checkbox = gr.Checkbox(label="Merge with existing selections", value=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  detect_btn = gr.Button("πŸ” Detect Objects", variant="primary")
 
 
273
  gr.Markdown("### ✏️ Manual Selection")
274
+ manual_input = gr.Textbox(label="Selection (x,y,width,height)", placeholder="100,100,200,150")
 
 
 
 
 
 
275
  add_manual_btn = gr.Button("βž• Add Manual Selection")
 
 
276
  gr.Markdown("### πŸ—‚οΈ Selection Management")
277
+ clear_all_btn = gr.Button("πŸ—‘οΈ Clear All", variant="secondary")
278
+ clear_yolo_btn = gr.Button("πŸ—‘οΈ Clear YOLO")
279
+ clear_manual_btn = gr.Button("πŸ—‘οΈ Clear Manual")
 
 
280
 
 
281
  with gr.Row():
282
+ analysis_output = gr.Markdown(label="πŸ“Š Analysis Results", elem_classes=["analysis-text"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
 
284
+ image_input.upload(process_image_upload, [image_input], [image_input, status_output, analysis_output])
285
+ detect_btn.click(detect_objects, [image_input, confidence_slider, merge_checkbox], [image_input, status_output, analysis_output])
286
+ add_manual_btn.click(add_manual_selection, [image_input, manual_input], [image_input, status_output, analysis_output])
287
+ clear_all_btn.click(clear_all_selections, [image_input], [image_input, status_output, analysis_output])
288
+ clear_yolo_btn.click(clear_yolo_selections, [image_input], [image_input, status_output, analysis_output])
289
+ clear_manual_btn.click(clear_manual_selections, [image_input], [image_input, status_output, analysis_output])
290
 
291
+ demo.load(load_yolo_model, outputs=[model_status])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
 
293
  return demo
294
 
295
+ # Export app (required for Hugging Face Spaces!)
296
+ app = create_interface()