russ4stall commited on
Commit
9197931
·
1 Parent(s): 346f25c
Files changed (2) hide show
  1. app.py +528 -0
  2. requirements-no-version.txt +1 -1
app.py ADDED
@@ -0,0 +1,528 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import torch
3
+ from PIL import Image
4
+ import numpy as np
5
+ import uuid
6
+ import cv2
7
+ import sys
8
+ import os
9
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
10
+
11
+ from core.processing import get_dino_boxes_from_prompt, embed_image_dino_large, embed_text, expand_coords_shape
12
+ from core.models import get_sam_predictor
13
+ from core.image_processing import crop_to_mask_size, apply_mask, resize_image
14
+ from core.storage import upload_image_to_s3, add_vector_to_qdrant, add_object_to_neo4j
15
+ from core.storage import query_vector_db_by_mask, get_object_details, query_vector_db_by_text_embedding
16
+ from core.storage import get_all_locations_for_house, set_object_primary_location_hierarchy
17
+
18
+ #HOUSE_ID='c8c5fdea-7138-44ea-9f02-7fdcd47ff8cf' #office
19
+ HOUSE_ID='fc2e081a-2b17-4b2e-a1bb-woodward' #woodward
20
+
21
+
22
+ # ------------------------------
23
+ # Helper functions
24
+ # ------------------------------
25
+
26
+ def extract_image_and_stroke_mask(editor_output):
27
+ """
28
+ Extracts the image and stroke mask from the editor output.
29
+
30
+ Parameters:
31
+ editor_output: either a dict with 'background' and 'layers' or an HxWx3/4 array
32
+
33
+ Returns:
34
+ A tuple (image, stroke_mask) where:
35
+ - image is the RGB image (HxWx3 array)
36
+ - stroke_mask is a binary mask (HxW array)
37
+ """
38
+ if isinstance(editor_output, dict):
39
+ bg = editor_output.get('background')
40
+ if bg is None:
41
+ return None, None
42
+ image = bg[..., :3]
43
+ stroke_mask = np.zeros(image.shape[:2], dtype=np.uint8)
44
+ for layer in editor_output.get('layers', []):
45
+ stroke_mask |= (layer[..., 3] > 0).astype(np.uint8)
46
+ else:
47
+ arr = editor_output
48
+ if arr.shape[2] == 4:
49
+ image = arr[..., :3]
50
+ stroke_mask = (arr[..., 3] > 0).astype(np.uint8)
51
+ else:
52
+ image = arr
53
+ stroke_mask = np.zeros(arr.shape[:2], dtype=np.uint8)
54
+ return image, stroke_mask
55
+
56
+ def apply_sam(editor_output, background_mode="remove", crop_result=True) -> np.ndarray:
57
+ """
58
+ Uses SAM to generate a segmentation mask based on the sketch (stroke_mask),
59
+ then either removes or extremely blurs the background. Optionally crops to
60
+ the foreground bbox.
61
+
62
+ Parameters:
63
+ editor_output: either a dict with 'background' and 'layers' or an HxWx3/4 array
64
+ background_mode: "remove" or "extreme_blur"
65
+ crop_result: whether to crop output to fg bbox
66
+
67
+ Returns:
68
+ HxWx3 uint8 array
69
+ """
70
+ # --- 1) pull RGB + sketch mask ---
71
+ image, stroke_mask = extract_image_and_stroke_mask(editor_output)
72
+
73
+ # if no sketch, just return original
74
+ if stroke_mask.sum() == 0:
75
+ return image
76
+
77
+ # preprocess & set image
78
+ image = resize_image(image)
79
+ get_sam_predictor().set_image(image)
80
+
81
+ # downscale stroke mask to predictor size
82
+ h, w = image.shape[:2]
83
+ stroke_small = cv2.resize(stroke_mask, (w, h), interpolation=cv2.INTER_NEAREST)
84
+ point_coords, point_labels = stroke_to_coords(stroke_small)
85
+
86
+ # now actually predict using the strokes
87
+ with torch.inference_mode(), torch.autocast("cuda", dtype=torch.bfloat16):
88
+ masks, scores, logits = get_sam_predictor().predict(
89
+ point_coords=point_coords,
90
+ point_labels=point_labels,
91
+ box=None,
92
+ multimask_output=False
93
+ )
94
+
95
+ # pick the highest-score mask and binarize
96
+ best_idx = int(np.argmax(scores))
97
+ mask = masks[best_idx] > 0.5
98
+
99
+ # composite
100
+ output = apply_mask(image, mask, background_mode)
101
+
102
+ # optional crop
103
+ if crop_result:
104
+ output = crop_to_mask_size(output, mask)
105
+
106
+ return output
107
+
108
+ def apply_grounded_sam(editor_output, prompt: str, crop_result=True) -> np.ndarray:
109
+ # 1) pull RGB out
110
+ image, stroke_mask = extract_image_and_stroke_mask(editor_output)
111
+
112
+ sam_boxes = get_dino_boxes_from_prompt(image, prompt)
113
+
114
+ point_coords = None
115
+ point_labels = None
116
+
117
+ if stroke_mask.sum() > 0:
118
+ point_coords, point_labels = stroke_to_coords(stroke_mask)
119
+ point_coords, point_labels = expand_coords_shape(point_coords, point_labels, sam_boxes.shape[0])
120
+
121
+ # 5) feed those boxes into SAM2
122
+ get_sam_predictor().set_image(image)
123
+ masks, scores_sam, _ = get_sam_predictor().predict(
124
+ point_coords=point_coords,
125
+ point_labels=point_labels,
126
+ box=sam_boxes,
127
+ multimask_output=False
128
+ )
129
+
130
+ # 6) pick the best SAM proposal, composite & crop
131
+ best = int(np.argmax(scores_sam))
132
+ # 1) pick the best mask and remove any leading batch‐dim
133
+ mask = masks[best] > 0.5 # masks[best] should give you shape (H, W)
134
+
135
+ output = apply_mask(image, mask, background_mode)
136
+
137
+ if crop_result:
138
+ output = crop_to_mask_size(output, mask)
139
+
140
+ return output
141
+
142
+ def add_item(image, description, object_id, background_mode, click_points):
143
+ """
144
+ Processes the image for memorization:
145
+ - Resizes it.
146
+ - Optionally applies SAM processing (background removal or extreme blur) based on background_mode.
147
+ - Generates a caption if needed.
148
+ - Computes the CLIP embedding and stores it in Qdrant.
149
+ """
150
+
151
+ #apply clip embeddings
152
+ image_features = embed_image_dino_large(image)
153
+
154
+ #generate id's
155
+ if not object_id or object_id.strip() == "":
156
+ object_id = str(uuid.uuid4())
157
+ view_id = str(uuid.uuid4())
158
+
159
+ #upload original full-res to S3
160
+ key = f"object_collection/{object_id}/{view_id}.png"
161
+ image_url = upload_image_to_s3(image, key)
162
+
163
+ store_image_in_qdrant(view_id, vector=image_features, object_id=object_id, house_id=HOUSE_ID, image_url=image_url)
164
+
165
+ if not (description is None or description.strip() == ""):
166
+ desc_features = embed_text(description)
167
+ store_text_in_qdrant(vector=desc_features, object_id=object_id, house_id=HOUSE_ID, description=description)
168
+
169
+ store_in_neo4j(object_id, HOUSE_ID, description, object_id)
170
+
171
+ return f"Item added under object ID: {object_id}\nDescription: {description}"
172
+
173
+ def query_item(query_image, background_mode, click_points, k=5):
174
+ """
175
+ Processes the query image:
176
+ - Resizes it.
177
+ - Optionally applies SAM processing based on background_mode and click points.
178
+ - Computes the CLIP embedding and queries Qdrant.
179
+ - Returns matching objects.
180
+ """
181
+ search_results = query_vector_db_by_mask(query_image, k)
182
+
183
+ object_scores = {}
184
+ object_views = {}
185
+ for result in search_results:
186
+ obj_id = result.payload.get("object_id")
187
+ score = result.score
188
+ if obj_id in object_scores:
189
+ object_scores[obj_id] = max(object_scores[obj_id], score)
190
+ object_views[obj_id].append(result.payload.get("description"))
191
+ else:
192
+ object_scores[obj_id] = score
193
+ object_views[obj_id] = [result.payload.get("description")]
194
+ all_scores = np.array(list(object_scores.values()))
195
+ exp_scores = np.exp(all_scores)
196
+ probabilities = exp_scores / np.sum(exp_scores) if np.sum(exp_scores) > 0 else np.zeros_like(exp_scores)
197
+ results = []
198
+ for i, (obj_id, score) in enumerate(object_scores.items()):
199
+ results.append({
200
+ "object_id": obj_id,
201
+ "aggregated_similarity": float(score),
202
+ "probability": float(probabilities[i]),
203
+ "descriptions": object_views[obj_id]
204
+ })
205
+ return results
206
+
207
+ def query_by_text(description, k=5):
208
+ """
209
+ Embeds the provided text and queries the vector DB.
210
+ Returns top k matches in the usual object result format.
211
+ """
212
+ if not description.strip():
213
+ return {"error": "Description cannot be empty."}
214
+
215
+ query_features = embed_text(description)
216
+
217
+ # Note: assuming you have or can implement a `query_vector_db_by_text` similar to `query_vector_db_by_mask`
218
+ search_results = query_vector_db_by_text_embedding(query_features, k)
219
+
220
+ object_scores = {}
221
+ object_views = {}
222
+ for result in search_results:
223
+ obj_id = result.payload.get("object_id")
224
+ score = result.score
225
+ if obj_id in object_scores:
226
+ object_scores[obj_id] = max(object_scores[obj_id], score)
227
+ object_views[obj_id].append(result.payload.get("description"))
228
+ else:
229
+ object_scores[obj_id] = score
230
+ object_views[obj_id] = [result.payload.get("description")]
231
+ all_scores = np.array(list(object_scores.values()))
232
+ exp_scores = np.exp(all_scores)
233
+ probabilities = exp_scores / np.sum(exp_scores) if np.sum(exp_scores) > 0 else np.zeros_like(exp_scores)
234
+ results = []
235
+ for i, (obj_id, score) in enumerate(object_scores.items()):
236
+ results.append({
237
+ "object_id": obj_id,
238
+ "aggregated_similarity": float(score),
239
+ "probability": float(probabilities[i]),
240
+ "descriptions": object_views[obj_id]
241
+ })
242
+ return results
243
+
244
+
245
+ def store_image_in_qdrant(view_id, vector : np.ndarray, object_id, house_id, image_url : str):
246
+ if object_id is None:
247
+ object_id = str(uuid.uuid4())
248
+
249
+ payload = {"object_id": object_id, "image_url": image_url, "house_id": house_id, "type": "image", "embedding_model": "dino_large"}
250
+ view_id = add_vector_to_qdrant(view_id=view_id,
251
+ vectors={"dinov2_embedding": vector},
252
+ payload=payload)
253
+
254
+ return view_id
255
+
256
+ def store_text_in_qdrant(vector : np.ndarray, house_id: str, object_id: str = None, description: str = None):
257
+ if object_id is None:
258
+ object_id = str(uuid.uuid4())
259
+
260
+ # Add to Qdrant as "text_embedding"
261
+ view_id = add_vector_to_qdrant(
262
+ vectors={"clip_text_embedding": vector},
263
+ payload={"object_id": object_id, "house_id": house_id, "description": description, "type": "text", "embedding_model": "clip"}
264
+ )
265
+
266
+ return view_id
267
+
268
+ def store_in_neo4j(object_id, house_id, description, qdrant_object_id):
269
+ add_object_to_neo4j(object_id, house_id, description, qdrant_object_id)
270
+
271
+ def stroke_to_coords(stroke_mask, max_points=10):
272
+ """
273
+ Converts a stroke mask into sampled point coordinates and labels.
274
+
275
+ Parameters:
276
+ stroke_mask: Binary mask (HxW array) representing the stroke.
277
+ max_points: Maximum number of points to sample.
278
+
279
+ Returns:
280
+ A tuple (point_coords, point_labels) where:
281
+ - point_coords is an Nx2 array of sampled [x, y] coordinates.
282
+ - point_labels is an N array of labels (1 for foreground).
283
+ """
284
+ ys, xs = np.nonzero(stroke_mask)
285
+ coords = np.stack([xs, ys], axis=1)
286
+
287
+ # Sample up to max_points
288
+ N = min(max_points, len(coords))
289
+ if N == 0:
290
+ raise ValueError("No stroke pixels found")
291
+ idxs = np.linspace(0, len(coords) - 1, num=N, dtype=int)
292
+ point_coords = coords[idxs]
293
+ point_labels = np.ones(N, dtype=int)
294
+
295
+ return point_coords, point_labels
296
+
297
+
298
+ def get_locations_overview():
299
+ """
300
+ Fetches all existing locations and their details.
301
+ """
302
+ locations = get_all_locations_for_house(HOUSE_ID, include_images=True)
303
+ # Example response structure expected from `get_all_locations`:
304
+ # [{"name": "Kitchen", "image": <np.ndarray>, "parents": ["Home"]}, ...]
305
+
306
+ overview = []
307
+ for loc in locations:
308
+ overview.append({
309
+ "name": loc["name"],
310
+ "parents": loc.get("parents", []),
311
+ "image": loc.get("image") # Expected to be np.ndarray or PIL.Image
312
+ })
313
+ return overview
314
+
315
+ # Remove location function
316
+ def remove_location(name):
317
+ #from core.storage import remove_location
318
+ #remove_location(house_id=HOUSE_ID, name=name)
319
+ return f"Location '{name}' removed."
320
+
321
+ def add_update_location(name, parent_str, image):
322
+ parents = [p.strip() for p in parent_str.split(",")] if parent_str else []
323
+ # Example function you'd define in core.storage
324
+ #from core.storage import add_or_update_location
325
+ #add_or_update_location(house_id=HOUSE_ID, name=name, parents=parents, image=image)
326
+ return f"Location '{name}' added or updated with parents {parents}."
327
+ # ------------------------------
328
+ # Gradio Interface
329
+ # ------------------------------
330
+
331
+ with gr.Blocks() as demo:
332
+ with gr.Tab("Add Item"):
333
+ image_input = gr.ImageEditor(label="Upload & Sketch", type="numpy")
334
+ seg_prompt_input = gr.Textbox(label="Segmentation Prompt", placeholder="e.g. ‘red apple’")
335
+ description_input = gr.Textbox(label="Description", lines=3)
336
+ object_id_input = gr.Textbox(label="Object ID (optional)")
337
+ background_mode = gr.Radio(choices=["remove","extreme_blur"], value="remove")
338
+ preview_button = gr.Button("Preview")
339
+ preview_output = gr.Image(label="Preview Processed Image", type="numpy")
340
+ submit_button = gr.Button("Submit")
341
+ output_text = gr.Textbox(label="Result")
342
+
343
+ preview_button.click(
344
+ fn=lambda img,mode,prompt: (
345
+ apply_grounded_sam(img, prompt)
346
+ if prompt else
347
+ apply_sam(img, mode)
348
+ ),
349
+ inputs=[image_input, background_mode, seg_prompt_input],
350
+ outputs=[preview_output]
351
+ )
352
+ submit_button.click(fn=add_item,
353
+ inputs=[preview_output, description_input, object_id_input, background_mode, image_input],
354
+ outputs=[output_text])
355
+
356
+ with gr.Tab("Query By Text"):
357
+ text_query_input = gr.Textbox(label="Describe Object", lines=3, placeholder="e.g., 'red ceramic mug'")
358
+ k_text_slider = gr.Slider(1, 10, 5, label="Results k")
359
+ text_query_button = gr.Button("Search by Text")
360
+ text_query_output = gr.JSON(label="Query Results")
361
+
362
+ text_query_button.click(query_by_text,
363
+ inputs=[text_query_input, k_text_slider],
364
+ outputs=[text_query_output])
365
+
366
+ with gr.Tab("Query By Image"):
367
+ query_input = gr.ImageEditor(label="Query & Sketch", type="numpy")
368
+ query_prompt = gr.Textbox(label="Segmentation Prompt", placeholder="optional text-based mask")
369
+ query_mode = gr.Radio(choices=["remove","extreme_blur"], value="remove")
370
+ query_preview_button = gr.Button("Refresh Preview")
371
+ query_preview= gr.Image(label="Query Preview", type="numpy")
372
+ k_slider = gr.Slider(1,10,1, label="Results k")
373
+ query_button = gr.Button("Search")
374
+ query_output = gr.JSON(label="Query Results")
375
+
376
+ # Manual preview refresh
377
+ query_preview_button.click(fn=lambda img,mode,prompt: (
378
+ apply_grounded_sam(img, prompt)
379
+ if prompt else
380
+ apply_sam(img, mode)
381
+ ),
382
+ inputs=[query_input, query_mode, query_prompt],
383
+ outputs=[query_preview])
384
+
385
+ query_button.click(fn=query_item,
386
+ inputs=[query_preview, query_mode, query_input, k_slider],
387
+ outputs=[query_output])
388
+
389
+ with gr.Tab("View Object"):
390
+ view_object_id_input = gr.Textbox(label="Object ID", placeholder="Enter Object ID")
391
+ view_button = gr.Button("View Object")
392
+
393
+ add_image_button = gr.Button("Add Image to This Object")
394
+ add_description_button = gr.Button("Add Text Description")
395
+ add_location_button = gr.Button("Add Location")
396
+
397
+ view_description_output = gr.Textbox(label="Description")
398
+ view_images_output = gr.Gallery(label="Images", columns=3, height="auto")
399
+ view_texts_output = gr.JSON(label="Text Descriptions")
400
+ view_locations_output = gr.JSON(label="Location Chain")
401
+ view_location_images_output = gr.Gallery(label="Location Images", columns=3, height="auto")
402
+
403
+ view_owners_output = gr.JSON(label="Owners")
404
+
405
+ desc_object_id_input = 0 #placeholder
406
+
407
+ def view_object(object_id):
408
+ data = get_object_details(HOUSE_ID, object_id)
409
+ images_display = [Image.fromarray(img_dict["image"]) for img_dict in data["images"]]
410
+ location_images_display = [Image.fromarray(img) for img in data.get("location_images", [])]
411
+ return (
412
+ data["description"] or "No description found.",
413
+ images_display,
414
+ data["texts"],
415
+ data["locations"],
416
+ location_images_display,
417
+ data["owners"]
418
+ )
419
+
420
+ view_button.click(
421
+ view_object,
422
+ inputs=[view_object_id_input],
423
+ outputs=[
424
+ view_description_output,
425
+ view_images_output,
426
+ view_texts_output,
427
+ view_locations_output,
428
+ view_location_images_output,
429
+ view_owners_output
430
+ ]
431
+ )
432
+
433
+ # Reference your existing Add Item tab's object_id_input
434
+ #add_image_button.click(
435
+ # lambda object_id: gr.update(value=object_id),
436
+ # inputs=[view_object_id_input],
437
+ # outputs=[object_id_input]
438
+ #)
439
+
440
+ # Navigation from View Object
441
+ #add_description_button.click(
442
+ # lambda object_id: gr.update(value=object_id),
443
+ # inputs=[view_object_id_input],
444
+ # outputs=[desc_object_id_input]
445
+ #)
446
+
447
+
448
+ with gr.Tab("Add Description"):
449
+ desc_object_id_input = gr.Textbox(label="Object ID")
450
+ desc_text_input = gr.Textbox(label="Description", lines=3)
451
+ submit_desc_button = gr.Button("Submit Description")
452
+ desc_output = gr.Textbox(label="Result")
453
+
454
+ def submit_description(object_id, description):
455
+ desc_features = embed_text(description)
456
+ store_text_in_qdrant(vector=desc_features, object_id=object_id, house_id=HOUSE_ID, description=description)
457
+ return f"Added description to object {object_id}"
458
+
459
+ submit_desc_button.click(submit_description,
460
+ inputs=[desc_object_id_input, desc_text_input],
461
+ outputs=[desc_output])
462
+
463
+
464
+
465
+ with gr.Tab("Manage Locations"):
466
+ with gr.Row():
467
+ refresh_locations_button = gr.Button("Refresh Locations List")
468
+ locations_json_output = gr.JSON(label="Locations Overview (Names and Parents)")
469
+ locations_gallery_output = gr.Gallery(label="Location Images", columns=3, height="auto")
470
+
471
+ # Controls to Add/Remove locations
472
+ location_name_input = gr.Textbox(label="Location Name")
473
+ location_parent_input = gr.Textbox(label="Parent Location(s)", placeholder="Comma-separated, e.g. 'Home, Kitchen'")
474
+ location_image_input = gr.Image(label="Upload Location Image", type="numpy")
475
+
476
+ add_location_button = gr.Button("Add / Update Location")
477
+ remove_location_button = gr.Button("Remove Location")
478
+
479
+ location_manage_output = gr.Textbox(label="Result")
480
+
481
+ # Backend processor to return both JSON summary and Gallery
482
+ def refresh_locations_ui():
483
+ raw_locations = get_all_locations_for_house(HOUSE_ID, include_images=True)
484
+
485
+ # Prepare JSON summary
486
+ summary = [
487
+ {"name": loc["name"], "parents": loc.get("parents", [])}
488
+ for loc in raw_locations
489
+ ]
490
+
491
+ # Prepare images for gallery
492
+ images = []
493
+ for loc in raw_locations:
494
+ img_base64 = loc.get("image_base64")
495
+ if img_base64:
496
+ from PIL import Image
497
+ import io, base64
498
+ img_data = base64.b64decode(img_base64)
499
+ img_pil = Image.open(io.BytesIO(img_data))
500
+ images.append(img_pil)
501
+
502
+ return summary, images
503
+
504
+ refresh_locations_button.click(
505
+ refresh_locations_ui,
506
+ inputs=[],
507
+ outputs=[locations_json_output, locations_gallery_output]
508
+ )
509
+
510
+
511
+
512
+ # Add/Update and Remove functions stay unchanged
513
+ add_location_button.click(
514
+ add_update_location,
515
+ inputs=[location_name_input, location_parent_input, location_image_input],
516
+ outputs=[location_manage_output]
517
+ )
518
+
519
+ remove_location_button.click(
520
+ remove_location,
521
+ inputs=[location_name_input],
522
+ outputs=[location_manage_output]
523
+ )
524
+
525
+
526
+ import os
527
+ os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
528
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=False, debug=True, root_path="/", show_api=False)
requirements-no-version.txt CHANGED
@@ -1,6 +1,6 @@
1
  backports.tarfile
2
  boto3
3
- clip
4
  dotenv
5
  gradio==5.29.1
6
  groundingdino-py
 
1
  backports.tarfile
2
  boto3
3
+ clip @ git+https://github.com/openai/CLIP.git@dcba3cb2e2827b402d2701e7e1c7d9fed8a20ef1
4
  dotenv
5
  gradio==5.29.1
6
  groundingdino-py