huzey commited on
Commit
8d0781a
·
1 Parent(s): 8e6ffd5
Files changed (2) hide show
  1. app.py +54 -14
  2. feedback_viewer.py +121 -48
app.py CHANGED
@@ -52,6 +52,38 @@ if USE_HUGGINGFACE_ZEROGPU:
52
  logging.warning("Could not import download_models")
53
 
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  def load_gradio_images_helper(pil_images: Union[List, Image.Image, str]) -> List[Image.Image]:
56
  """
57
  Convert various image input formats to a list of PIL Images.
@@ -120,15 +152,18 @@ def create_vibe_blending_tab():
120
  with gr.Group():
121
  gr.Markdown("**Step 3:** Submit your feedback")
122
  rating = gr.Radio(label="How do you like the results?", choices=["1", "2", "3", "4", "5"])
123
- feedback_form = gr.TextArea(label="Feedback", show_label=False, lines=1)
 
124
  feedback_button = gr.Button("Submit Feedback", variant="secondary", size="sm")
125
 
126
  with gr.Column():
127
  with gr.Group():
128
  gr.Markdown("**Step 2:** Run Vibe Blending")
129
- with gr.Accordion("Vibe Blending Results", open=False):
130
- blending_results_grid = gr.Image(label="Grid View", show_label=True, format="png")
131
  blending_results = gr.Gallery(label="Gallery View", show_label=False, columns=4, rows=3, interactive=False)
 
 
 
 
132
  blend_button = gr.Button("🔴 Run Vibe Blending", variant="primary")
133
 
134
  def _process_input_images(input1, input2, extra_images, negative_images):
@@ -156,28 +191,32 @@ def create_vibe_blending_tab():
156
  alpha_weights = np.linspace(alpha_start, alpha_end, n_steps+2)[1:-1].tolist()
157
  blended_images_list = run_vibe_blend_not_safe(input1, input2, extra_images, negative_images, DEFAULT_CONFIG_PATH, alpha_weights)
158
  blended_images_grid = create_image_grid(blended_images_list, rows=np.ceil(len(blended_images_list)/4).astype(int), cols=4)
159
- return blended_images_grid, blended_images_list # Return grid and list for display
 
 
 
 
160
 
161
- blend_button.click(blend_button_click, inputs=[input1, input2, extra_images, negative_images, alpha_start, alpha_end, n_steps], outputs=[blending_results_grid, blending_results])
162
 
163
- def feedback_button_click(rating, feedback_form, input1, input2, extra_images, negative_images, alpha_start, alpha_end, n_steps, blending_results):
164
  """Handle feedback submission and store to Hugging Face Dataset."""
165
  if not rating:
166
  gr.Warning("Please select a rating before submitting feedback.")
167
- return gr.update(value=None), gr.update(value="")
168
 
169
  # Validate that required images exist
170
  if input1 is None:
171
  gr.Warning("Please upload Input 1 image before submitting feedback.")
172
- return gr.update(value=rating), gr.update(value=feedback_form)
173
 
174
  if input2 is None:
175
  gr.Warning("Please upload Input 2 image before submitting feedback.")
176
- return gr.update(value=rating), gr.update(value=feedback_form)
177
 
178
  if blending_results is None or len(blending_results) == 0:
179
  gr.Warning("Please run vibe blending first to generate results before submitting feedback.")
180
- return gr.update(value=rating), gr.update(value=feedback_form)
181
 
182
  # Process images to check if they exist
183
  input1_img, input2_img, extra_images_processed, negative_images_processed = _process_input_images(
@@ -205,11 +244,12 @@ def create_vibe_blending_tab():
205
  extra_images=extra_images_processed if extra_images_processed else None,
206
  negative_images=negative_images_processed if negative_images_processed else None,
207
  blending_result_images=blending_result_images, # Upload list of images
 
208
  )
209
 
210
  if success:
211
  gr.Info("Thank you! Your feedback has been submitted successfully.")
212
- return gr.update(value=None), gr.update(value="") # Reset rating and feedback form
213
  else:
214
  error_msg = "Feedback could not be stored. "
215
  if not HF_FEEDBACK_DATASET_REPO:
@@ -217,12 +257,12 @@ def create_vibe_blending_tab():
217
  else:
218
  error_msg += "Please check the logs for details."
219
  gr.Warning(error_msg)
220
- return gr.update(value=rating), gr.update(value=feedback_form) # Keep rating and feedback form
221
 
222
  feedback_button.click(
223
  feedback_button_click,
224
- inputs=[rating, feedback_form, input1, input2, extra_images, negative_images, alpha_start, alpha_end, n_steps, blending_results],
225
- outputs=[rating, feedback_form] # Reset rating and feedback form
226
  )
227
 
228
  example_cases = [
 
52
  logging.warning("Could not import download_models")
53
 
54
 
55
+ def create_gif_from_images(images: List[Image.Image], fps: float = 3.0) -> str:
56
+ """
57
+ Create a GIF from a list of PIL Images.
58
+
59
+ Args:
60
+ images: List of PIL Images to combine into a GIF
61
+ fps: Frames per second for the GIF (default: 3.0)
62
+
63
+ Returns:
64
+ Path to the temporary GIF file
65
+ """
66
+ if not images:
67
+ return None
68
+
69
+ # Calculate duration in milliseconds (1000ms / fps)
70
+ duration_ms = int(1000 / fps)
71
+
72
+ # Create a temporary file for the GIF
73
+ gif_path = os.path.join(tempfile.gettempdir(), f"vibe_blend_{uuid.uuid4().hex}.gif")
74
+
75
+ # Save as GIF with loop
76
+ images[0].save(
77
+ gif_path,
78
+ save_all=True,
79
+ append_images=images[1:],
80
+ duration=duration_ms,
81
+ loop=0 # 0 = infinite loop
82
+ )
83
+
84
+ return gif_path
85
+
86
+
87
  def load_gradio_images_helper(pil_images: Union[List, Image.Image, str]) -> List[Image.Image]:
88
  """
89
  Convert various image input formats to a list of PIL Images.
 
152
  with gr.Group():
153
  gr.Markdown("**Step 3:** Submit your feedback")
154
  rating = gr.Radio(label="How do you like the results?", choices=["1", "2", "3", "4", "5"])
155
+ feedback_form = gr.TextArea(label="Feedback (optional)", show_label=True, lines=1, placeholder="Enter your feedback here...")
156
+ make_public = gr.Checkbox(label="Make feedback public", value=True, info="Allow this feedback to be visible in the public feedback viewer")
157
  feedback_button = gr.Button("Submit Feedback", variant="secondary", size="sm")
158
 
159
  with gr.Column():
160
  with gr.Group():
161
  gr.Markdown("**Step 2:** Run Vibe Blending")
 
 
162
  blending_results = gr.Gallery(label="Gallery View", show_label=False, columns=4, rows=3, interactive=False)
163
+ with gr.Accordion("Vibe Blending Results (grid view)", open=False):
164
+ blending_results_grid = gr.Image(label="Grid View", show_label=True, format="png", interactive=False)
165
+ with gr.Accordion("Vibe Blending Results (GIF view)", open=False):
166
+ blending_results_gif = gr.Image(label="GIF View", show_label=True, format="gif", interactive=False)
167
  blend_button = gr.Button("🔴 Run Vibe Blending", variant="primary")
168
 
169
  def _process_input_images(input1, input2, extra_images, negative_images):
 
191
  alpha_weights = np.linspace(alpha_start, alpha_end, n_steps+2)[1:-1].tolist()
192
  blended_images_list = run_vibe_blend_not_safe(input1, input2, extra_images, negative_images, DEFAULT_CONFIG_PATH, alpha_weights)
193
  blended_images_grid = create_image_grid(blended_images_list, rows=np.ceil(len(blended_images_list)/4).astype(int), cols=4)
194
+
195
+ # Create GIF at 3 frames per second
196
+ gif_path = create_gif_from_images(blended_images_list, fps=3.0)
197
+
198
+ return blended_images_grid, blended_images_list, gif_path # Return grid, list, and GIF for display
199
 
200
+ blend_button.click(blend_button_click, inputs=[input1, input2, extra_images, negative_images, alpha_start, alpha_end, n_steps], outputs=[blending_results_grid, blending_results, blending_results_gif])
201
 
202
+ def feedback_button_click(rating, feedback_form, make_public, input1, input2, extra_images, negative_images, alpha_start, alpha_end, n_steps, blending_results):
203
  """Handle feedback submission and store to Hugging Face Dataset."""
204
  if not rating:
205
  gr.Warning("Please select a rating before submitting feedback.")
206
+ return gr.update(value=None), gr.update(value=""), gr.update(value=True)
207
 
208
  # Validate that required images exist
209
  if input1 is None:
210
  gr.Warning("Please upload Input 1 image before submitting feedback.")
211
+ return gr.update(value=rating), gr.update(value=feedback_form), gr.update(value=make_public)
212
 
213
  if input2 is None:
214
  gr.Warning("Please upload Input 2 image before submitting feedback.")
215
+ return gr.update(value=rating), gr.update(value=feedback_form), gr.update(value=make_public)
216
 
217
  if blending_results is None or len(blending_results) == 0:
218
  gr.Warning("Please run vibe blending first to generate results before submitting feedback.")
219
+ return gr.update(value=rating), gr.update(value=feedback_form), gr.update(value=make_public)
220
 
221
  # Process images to check if they exist
222
  input1_img, input2_img, extra_images_processed, negative_images_processed = _process_input_images(
 
244
  extra_images=extra_images_processed if extra_images_processed else None,
245
  negative_images=negative_images_processed if negative_images_processed else None,
246
  blending_result_images=blending_result_images, # Upload list of images
247
+ is_public=make_public, # Pass the public flag
248
  )
249
 
250
  if success:
251
  gr.Info("Thank you! Your feedback has been submitted successfully.")
252
+ return gr.update(value=None), gr.update(value=""), gr.update(value=True) # Reset rating, feedback form, and checkbox
253
  else:
254
  error_msg = "Feedback could not be stored. "
255
  if not HF_FEEDBACK_DATASET_REPO:
 
257
  else:
258
  error_msg += "Please check the logs for details."
259
  gr.Warning(error_msg)
260
+ return gr.update(value=rating), gr.update(value=feedback_form), gr.update(value=make_public) # Keep rating, feedback form, and checkbox
261
 
262
  feedback_button.click(
263
  feedback_button_click,
264
+ inputs=[rating, feedback_form, make_public, input1, input2, extra_images, negative_images, alpha_start, alpha_end, n_steps, blending_results],
265
+ outputs=[rating, feedback_form, make_public] # Reset rating, feedback form, and checkbox
266
  )
267
 
268
  example_cases = [
feedback_viewer.py CHANGED
@@ -47,6 +47,7 @@ def store_feedback_to_hf_dataset(
47
  extra_images: Optional[List[Image.Image]],
48
  negative_images: Optional[List[Image.Image]],
49
  blending_result_images: Optional[List[Image.Image]],
 
50
  dataset_repo: Optional[str] = None,
51
  token: Optional[str] = None
52
  ) -> bool:
@@ -67,6 +68,7 @@ def store_feedback_to_hf_dataset(
67
  extra_images: List of extra images (PIL Images)
68
  negative_images: List of negative images (PIL Images)
69
  blending_result_images: List of blending result images (PIL Images)
 
70
  dataset_repo: Hugging Face dataset repository (username/dataset-name)
71
  token: Hugging Face token (if None, will try to use HF_TOKEN env var)
72
 
@@ -127,6 +129,7 @@ def store_feedback_to_hf_dataset(
127
  "alpha_start": Value("float64"),
128
  "alpha_end": Value("float64"),
129
  "n_steps": Value("int64"),
 
130
  "input1": ImageFeature(),
131
  "input2": ImageFeature(),
132
  "extra_images": Sequence(ImageFeature()),
@@ -156,6 +159,7 @@ def store_feedback_to_hf_dataset(
156
  "alpha_start": float(alpha_start),
157
  "alpha_end": float(alpha_end),
158
  "n_steps": int(n_steps),
 
159
  "input1": prepare_image(input1_image),
160
  "input2": prepare_image(input2_image),
161
  "extra_images": [prepare_image(img) for img in (extra_images or []) if prepare_image(img) is not None],
@@ -180,15 +184,34 @@ def store_feedback_to_hf_dataset(
180
  return example
181
  existing_dataset = existing_dataset.map(add_uuid)
182
 
183
- # Ensure the existing dataset has the UUID field in its schema
184
- # Add UUID to schema if missing, then cast to match new schema
 
 
 
 
 
 
 
 
 
185
  try:
186
  existing_features = existing_dataset.features
 
 
 
 
187
  if "uuid" not in existing_features:
188
  # Create new features dict with UUID added
189
- from datasets import Features as DatasetFeatures
190
- updated_features_dict = dict(existing_features)
191
  updated_features_dict["uuid"] = Value("string")
 
 
 
 
 
 
 
 
192
  updated_features = DatasetFeatures(updated_features_dict)
193
  existing_dataset = existing_dataset.cast(updated_features)
194
 
@@ -515,7 +538,8 @@ def load_feedback_from_hf_dataset(
515
  dataset_repo: Optional[str] = None,
516
  token: Optional[str] = None,
517
  limit: Optional[int] = None,
518
- reverse: bool = False
 
519
  ) -> List[dict]:
520
  """
521
  Load feedback entries from a Hugging Face Dataset.
@@ -525,6 +549,8 @@ def load_feedback_from_hf_dataset(
525
  token: Hugging Face token (if None, will try to use HF_TOKEN env var)
526
  limit: Maximum number of entries to return (None for all)
527
  reverse: If True, reverse the order (newest first). Default False (oldest first).
 
 
528
 
529
  Returns:
530
  List of feedback entries as dictionaries
@@ -621,6 +647,14 @@ def load_feedback_from_hf_dataset(
621
  if "uuid" not in entry_dict or not entry_dict.get("uuid"):
622
  entry_dict["uuid"] = str(uuid.uuid4())
623
 
 
 
 
 
 
 
 
 
624
  # Convert Image objects to PIL Images using the raw accessed values
625
  # ImageFeature objects need to be converted to PIL Images for display
626
  try:
@@ -814,30 +848,61 @@ def create_feedback_viewer_tab():
814
 
815
  selected_details = gr.JSON(label="Full Feedback Details", visible=False)
816
 
817
- # Admin delete section - hidden behind accordion
818
  with gr.Accordion("⋮", open=False):
 
819
  with gr.Row():
820
- delete_uuid_input = gr.Textbox(
821
- label="UUID to Delete",
822
- placeholder="Enter UUID or prefix (e.g. e7132a33)",
823
- value="",
824
- scale=2
825
- )
826
- delete_password_input = gr.Textbox(
827
  label="Admin Password",
828
  placeholder="Enter admin password",
829
  type="password",
830
  value="",
 
 
 
 
 
 
831
  scale=1
832
  )
 
 
 
 
 
 
 
 
 
 
 
833
  delete_button = gr.Button("🗑️ Delete", variant="stop", scale=1)
834
  delete_status = gr.Markdown("")
835
 
836
- def delete_entry_by_uuid(uuid_to_delete: str, password: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
837
  """Delete a feedback entry by UUID after password verification."""
838
  # Check password
839
- if password != "admin":
840
- return "❌ Invalid admin password."
841
 
842
  if not uuid_to_delete or not uuid_to_delete.strip():
843
  return "❌ Please enter a UUID to delete."
@@ -945,7 +1010,7 @@ def create_feedback_viewer_tab():
945
 
946
  delete_button.click(
947
  delete_entry_by_uuid,
948
- inputs=[delete_uuid_input, delete_password_input],
949
  outputs=[delete_status]
950
  )
951
 
@@ -981,12 +1046,13 @@ def create_feedback_viewer_tab():
981
 
982
  return None
983
 
984
- def load_and_display_feedback(items_per_page, page, sort_order, uuid_search="", timestamp_start="", timestamp_end="", rating_filter="All", filter_extra=False, filter_negative=False):
985
  """Load feedback from dataset and format as HTML table with pagination."""
986
  # Convert radio selection to reverse boolean
987
  sort_reverse = (sort_order == "New to Old")
988
  # Load all feedback entries (no limit, we'll paginate ourselves)
989
- all_feedbacks = load_feedback_from_hf_dataset(reverse=sort_reverse)
 
990
 
991
  # Filter by UUID if search term provided
992
  if uuid_search and uuid_search.strip():
@@ -1310,86 +1376,93 @@ def create_feedback_viewer_tab():
1310
  return html_content, page, f"**Total Pages:** {total_pages}"
1311
 
1312
  # Refresh button - loads first page
1313
- def refresh_feedback(items_per_page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative):
1314
- return load_and_display_feedback(items_per_page, 1, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative)
1315
 
1316
  refresh_button.click(
1317
  refresh_feedback,
1318
- inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images],
1319
  outputs=[feedback_html, page_number, total_pages_display]
1320
  )
1321
 
1322
  # Sort order radio - reset to page 1 when sort order changes
1323
- def on_sort_order_change(items_per_page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative):
1324
  # Reset to page 1 when sort order changes
1325
- return load_and_display_feedback(items_per_page, 1, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative)
1326
 
1327
  sort_order_radio.change(
1328
  on_sort_order_change,
1329
- inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images],
1330
  outputs=[feedback_html, page_number, total_pages_display]
1331
  )
1332
 
1333
  # Search button
1334
- def on_search(items_per_page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative):
1335
- return load_and_display_feedback(items_per_page, 1, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative)
1336
 
1337
  search_button.click(
1338
  on_search,
1339
- inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images],
1340
  outputs=[feedback_html, page_number, total_pages_display]
1341
  )
1342
 
1343
  # Rating filter change
1344
  rating_filter.change(
1345
  on_search,
1346
- inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images],
1347
  outputs=[feedback_html, page_number, total_pages_display]
1348
  )
1349
 
1350
  # Checkbox changes
1351
  filter_extra_images.change(
1352
  on_search,
1353
- inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images],
1354
  outputs=[feedback_html, page_number, total_pages_display]
1355
  )
1356
 
1357
  filter_negative_images.change(
1358
  on_search,
1359
- inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images],
 
 
 
 
 
 
 
1360
  outputs=[feedback_html, page_number, total_pages_display]
1361
  )
1362
 
1363
 
1364
  # Pagination controls
1365
- def on_page_change(items_per_page, page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative):
1366
- return load_and_display_feedback(items_per_page, page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative)
1367
 
1368
- def on_items_per_page_change(items_per_page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative):
1369
- return load_and_display_feedback(items_per_page, 1, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative)
1370
 
1371
  page_number.change(
1372
  on_page_change,
1373
- inputs=[items_per_page_slider, page_number, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images],
1374
  outputs=[feedback_html, page_number, total_pages_display]
1375
  )
1376
 
1377
  items_per_page_slider.change(
1378
  on_items_per_page_change,
1379
- inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images],
1380
  outputs=[feedback_html, page_number, total_pages_display]
1381
  )
1382
 
1383
  # Previous/Next page buttons
1384
- def go_to_previous_page(items_per_page, current_page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative):
1385
  new_page = max(1, int(current_page) - 1)
1386
- return load_and_display_feedback(items_per_page, new_page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative)
1387
 
1388
- def go_to_next_page(items_per_page, current_page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative):
1389
  # Convert radio selection to reverse boolean
1390
  sort_reverse = (sort_order == "New to Old")
1391
  # Load all feedbacks to calculate total pages (with filters if applicable)
1392
- all_feedbacks = load_feedback_from_hf_dataset(reverse=sort_reverse)
1393
 
1394
  # Apply UUID filter if search term provided
1395
  if uuid_search and uuid_search.strip():
@@ -1449,42 +1522,42 @@ def create_feedback_viewer_tab():
1449
  ]
1450
 
1451
  if not all_feedbacks:
1452
- return load_and_display_feedback(items_per_page, 1, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative)
1453
 
1454
  total_items = len(all_feedbacks)
1455
  items_per_page = max(1, int(items_per_page))
1456
  total_pages = max(1, (total_items + items_per_page - 1) // items_per_page)
1457
  new_page = min(total_pages, int(current_page) + 1)
1458
- return load_and_display_feedback(items_per_page, new_page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative)
1459
 
1460
  prev_page_button.click(
1461
  go_to_previous_page,
1462
- inputs=[items_per_page_slider, page_number, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images],
1463
  outputs=[feedback_html, page_number, total_pages_display]
1464
  )
1465
 
1466
  next_page_button.click(
1467
  go_to_next_page,
1468
- inputs=[items_per_page_slider, page_number, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images],
1469
  outputs=[feedback_html, page_number, total_pages_display]
1470
  )
1471
 
1472
  # Allow Enter key to trigger search
1473
  uuid_search_input.submit(
1474
  on_search,
1475
- inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images],
1476
  outputs=[feedback_html, page_number, total_pages_display]
1477
  )
1478
 
1479
  timestamp_start_input.submit(
1480
  on_search,
1481
- inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images],
1482
  outputs=[feedback_html, page_number, total_pages_display]
1483
  )
1484
 
1485
  timestamp_end_input.submit(
1486
  on_search,
1487
- inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images],
1488
  outputs=[feedback_html, page_number, total_pages_display]
1489
  )
1490
 
 
47
  extra_images: Optional[List[Image.Image]],
48
  negative_images: Optional[List[Image.Image]],
49
  blending_result_images: Optional[List[Image.Image]],
50
+ is_public: bool = True,
51
  dataset_repo: Optional[str] = None,
52
  token: Optional[str] = None
53
  ) -> bool:
 
68
  extra_images: List of extra images (PIL Images)
69
  negative_images: List of negative images (PIL Images)
70
  blending_result_images: List of blending result images (PIL Images)
71
+ is_public: Whether the feedback should be publicly visible (default True)
72
  dataset_repo: Hugging Face dataset repository (username/dataset-name)
73
  token: Hugging Face token (if None, will try to use HF_TOKEN env var)
74
 
 
129
  "alpha_start": Value("float64"),
130
  "alpha_end": Value("float64"),
131
  "n_steps": Value("int64"),
132
+ "is_public": Value("bool"), # Whether feedback is publicly visible
133
  "input1": ImageFeature(),
134
  "input2": ImageFeature(),
135
  "extra_images": Sequence(ImageFeature()),
 
159
  "alpha_start": float(alpha_start),
160
  "alpha_end": float(alpha_end),
161
  "n_steps": int(n_steps),
162
+ "is_public": bool(is_public), # Store public visibility flag
163
  "input1": prepare_image(input1_image),
164
  "input2": prepare_image(input2_image),
165
  "extra_images": [prepare_image(img) for img in (extra_images or []) if prepare_image(img) is not None],
 
184
  return example
185
  existing_dataset = existing_dataset.map(add_uuid)
186
 
187
+ # Check if existing dataset has is_public field, if not add it (default to True for old entries)
188
+ if "is_public" not in existing_dataset.column_names:
189
+ logging.info("Existing dataset missing is_public field, adding is_public=True to existing entries")
190
+ def add_is_public(example):
191
+ if "is_public" not in example:
192
+ example["is_public"] = True
193
+ return example
194
+ existing_dataset = existing_dataset.map(add_is_public)
195
+
196
+ # Ensure the existing dataset has the UUID and is_public fields in its schema
197
+ # Add fields to schema if missing, then cast to match new schema
198
  try:
199
  existing_features = existing_dataset.features
200
+ from datasets import Features as DatasetFeatures
201
+ updated_features_dict = dict(existing_features)
202
+ schema_updated = False
203
+
204
  if "uuid" not in existing_features:
205
  # Create new features dict with UUID added
 
 
206
  updated_features_dict["uuid"] = Value("string")
207
+ schema_updated = True
208
+
209
+ if "is_public" not in existing_features:
210
+ # Add is_public field to schema
211
+ updated_features_dict["is_public"] = Value("bool")
212
+ schema_updated = True
213
+
214
+ if schema_updated:
215
  updated_features = DatasetFeatures(updated_features_dict)
216
  existing_dataset = existing_dataset.cast(updated_features)
217
 
 
538
  dataset_repo: Optional[str] = None,
539
  token: Optional[str] = None,
540
  limit: Optional[int] = None,
541
+ reverse: bool = False,
542
+ public_only: bool = True
543
  ) -> List[dict]:
544
  """
545
  Load feedback entries from a Hugging Face Dataset.
 
549
  token: Hugging Face token (if None, will try to use HF_TOKEN env var)
550
  limit: Maximum number of entries to return (None for all)
551
  reverse: If True, reverse the order (newest first). Default False (oldest first).
552
+ public_only: If True, only return public feedback entries. Default True.
553
+ Old entries without is_public field are treated as public.
554
 
555
  Returns:
556
  List of feedback entries as dictionaries
 
647
  if "uuid" not in entry_dict or not entry_dict.get("uuid"):
648
  entry_dict["uuid"] = str(uuid.uuid4())
649
 
650
+ # Add is_public if missing (backward compatibility - old entries are public by default)
651
+ if "is_public" not in entry_dict:
652
+ entry_dict["is_public"] = True
653
+
654
+ # Filter by public_only if requested
655
+ if public_only and not entry_dict.get("is_public", True):
656
+ continue
657
+
658
  # Convert Image objects to PIL Images using the raw accessed values
659
  # ImageFeature objects need to be converted to PIL Images for display
660
  try:
 
848
 
849
  selected_details = gr.JSON(label="Full Feedback Details", visible=False)
850
 
851
+ # Admin section - hidden behind accordion
852
  with gr.Accordion("⋮", open=False):
853
+ gr.Markdown("### Admin Options")
854
  with gr.Row():
855
+ admin_password_input = gr.Textbox(
 
 
 
 
 
 
856
  label="Admin Password",
857
  placeholder="Enter admin password",
858
  type="password",
859
  value="",
860
+ scale=2
861
+ )
862
+ include_private_checkbox = gr.Checkbox(
863
+ label="Include private feedbacks",
864
+ value=False,
865
+ interactive=False,
866
  scale=1
867
  )
868
+ verify_password_button = gr.Button("🔓 Verify", variant="secondary", scale=1)
869
+ admin_status = gr.Markdown("")
870
+
871
+ gr.Markdown("### Delete Entry")
872
+ with gr.Row():
873
+ delete_uuid_input = gr.Textbox(
874
+ label="UUID to Delete",
875
+ placeholder="Enter UUID or prefix (e.g. e7132a33)",
876
+ value="",
877
+ scale=3
878
+ )
879
  delete_button = gr.Button("🗑️ Delete", variant="stop", scale=1)
880
  delete_status = gr.Markdown("")
881
 
882
+ def verify_admin_password(password: str, current_include_private: bool):
883
+ """Verify admin password and enable/disable private feedback checkbox."""
884
+ if password == "admin":
885
+ return (
886
+ gr.update(value=True, interactive=True), # Enable and check the checkbox
887
+ "✅ Admin access granted. You can now view private feedbacks."
888
+ )
889
+ else:
890
+ return (
891
+ gr.update(value=False, interactive=False), # Disable and uncheck the checkbox
892
+ "❌ Invalid password. Private feedbacks hidden."
893
+ )
894
+
895
+ verify_password_button.click(
896
+ verify_admin_password,
897
+ inputs=[admin_password_input, include_private_checkbox],
898
+ outputs=[include_private_checkbox, admin_status]
899
+ )
900
+
901
+ def delete_entry_by_uuid(uuid_to_delete: str, admin_password: str):
902
  """Delete a feedback entry by UUID after password verification."""
903
  # Check password
904
+ if admin_password != "admin":
905
+ return "❌ Invalid admin password. Enter password and click Verify first."
906
 
907
  if not uuid_to_delete or not uuid_to_delete.strip():
908
  return "❌ Please enter a UUID to delete."
 
1010
 
1011
  delete_button.click(
1012
  delete_entry_by_uuid,
1013
+ inputs=[delete_uuid_input, admin_password_input],
1014
  outputs=[delete_status]
1015
  )
1016
 
 
1046
 
1047
  return None
1048
 
1049
+ def load_and_display_feedback(items_per_page, page, sort_order, uuid_search="", timestamp_start="", timestamp_end="", rating_filter="All", filter_extra=False, filter_negative=False, include_private=False):
1050
  """Load feedback from dataset and format as HTML table with pagination."""
1051
  # Convert radio selection to reverse boolean
1052
  sort_reverse = (sort_order == "New to Old")
1053
  # Load all feedback entries (no limit, we'll paginate ourselves)
1054
+ # If include_private is True, set public_only to False
1055
+ all_feedbacks = load_feedback_from_hf_dataset(reverse=sort_reverse, public_only=not include_private)
1056
 
1057
  # Filter by UUID if search term provided
1058
  if uuid_search and uuid_search.strip():
 
1376
  return html_content, page, f"**Total Pages:** {total_pages}"
1377
 
1378
  # Refresh button - loads first page
1379
+ def refresh_feedback(items_per_page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative, include_private):
1380
+ return load_and_display_feedback(items_per_page, 1, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative, include_private)
1381
 
1382
  refresh_button.click(
1383
  refresh_feedback,
1384
+ inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images, include_private_checkbox],
1385
  outputs=[feedback_html, page_number, total_pages_display]
1386
  )
1387
 
1388
  # Sort order radio - reset to page 1 when sort order changes
1389
+ def on_sort_order_change(items_per_page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative, include_private):
1390
  # Reset to page 1 when sort order changes
1391
+ return load_and_display_feedback(items_per_page, 1, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative, include_private)
1392
 
1393
  sort_order_radio.change(
1394
  on_sort_order_change,
1395
+ inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images, include_private_checkbox],
1396
  outputs=[feedback_html, page_number, total_pages_display]
1397
  )
1398
 
1399
  # Search button
1400
+ def on_search(items_per_page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative, include_private):
1401
+ return load_and_display_feedback(items_per_page, 1, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative, include_private)
1402
 
1403
  search_button.click(
1404
  on_search,
1405
+ inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images, include_private_checkbox],
1406
  outputs=[feedback_html, page_number, total_pages_display]
1407
  )
1408
 
1409
  # Rating filter change
1410
  rating_filter.change(
1411
  on_search,
1412
+ inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images, include_private_checkbox],
1413
  outputs=[feedback_html, page_number, total_pages_display]
1414
  )
1415
 
1416
  # Checkbox changes
1417
  filter_extra_images.change(
1418
  on_search,
1419
+ inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images, include_private_checkbox],
1420
  outputs=[feedback_html, page_number, total_pages_display]
1421
  )
1422
 
1423
  filter_negative_images.change(
1424
  on_search,
1425
+ inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images, include_private_checkbox],
1426
+ outputs=[feedback_html, page_number, total_pages_display]
1427
+ )
1428
+
1429
+ # Include private checkbox change - refresh when toggled
1430
+ include_private_checkbox.change(
1431
+ on_search,
1432
+ inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images, include_private_checkbox],
1433
  outputs=[feedback_html, page_number, total_pages_display]
1434
  )
1435
 
1436
 
1437
  # Pagination controls
1438
+ def on_page_change(items_per_page, page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative, include_private):
1439
+ return load_and_display_feedback(items_per_page, page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative, include_private)
1440
 
1441
+ def on_items_per_page_change(items_per_page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative, include_private):
1442
+ return load_and_display_feedback(items_per_page, 1, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative, include_private)
1443
 
1444
  page_number.change(
1445
  on_page_change,
1446
+ inputs=[items_per_page_slider, page_number, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images, include_private_checkbox],
1447
  outputs=[feedback_html, page_number, total_pages_display]
1448
  )
1449
 
1450
  items_per_page_slider.change(
1451
  on_items_per_page_change,
1452
+ inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images, include_private_checkbox],
1453
  outputs=[feedback_html, page_number, total_pages_display]
1454
  )
1455
 
1456
  # Previous/Next page buttons
1457
+ def go_to_previous_page(items_per_page, current_page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative, include_private):
1458
  new_page = max(1, int(current_page) - 1)
1459
+ return load_and_display_feedback(items_per_page, new_page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative, include_private)
1460
 
1461
+ def go_to_next_page(items_per_page, current_page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative, include_private):
1462
  # Convert radio selection to reverse boolean
1463
  sort_reverse = (sort_order == "New to Old")
1464
  # Load all feedbacks to calculate total pages (with filters if applicable)
1465
+ all_feedbacks = load_feedback_from_hf_dataset(reverse=sort_reverse, public_only=not include_private)
1466
 
1467
  # Apply UUID filter if search term provided
1468
  if uuid_search and uuid_search.strip():
 
1522
  ]
1523
 
1524
  if not all_feedbacks:
1525
+ return load_and_display_feedback(items_per_page, 1, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative, include_private)
1526
 
1527
  total_items = len(all_feedbacks)
1528
  items_per_page = max(1, int(items_per_page))
1529
  total_pages = max(1, (total_items + items_per_page - 1) // items_per_page)
1530
  new_page = min(total_pages, int(current_page) + 1)
1531
+ return load_and_display_feedback(items_per_page, new_page, sort_order, uuid_search, timestamp_start, timestamp_end, rating_filter, filter_extra, filter_negative, include_private)
1532
 
1533
  prev_page_button.click(
1534
  go_to_previous_page,
1535
+ inputs=[items_per_page_slider, page_number, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images, include_private_checkbox],
1536
  outputs=[feedback_html, page_number, total_pages_display]
1537
  )
1538
 
1539
  next_page_button.click(
1540
  go_to_next_page,
1541
+ inputs=[items_per_page_slider, page_number, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images, include_private_checkbox],
1542
  outputs=[feedback_html, page_number, total_pages_display]
1543
  )
1544
 
1545
  # Allow Enter key to trigger search
1546
  uuid_search_input.submit(
1547
  on_search,
1548
+ inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images, include_private_checkbox],
1549
  outputs=[feedback_html, page_number, total_pages_display]
1550
  )
1551
 
1552
  timestamp_start_input.submit(
1553
  on_search,
1554
+ inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images, include_private_checkbox],
1555
  outputs=[feedback_html, page_number, total_pages_display]
1556
  )
1557
 
1558
  timestamp_end_input.submit(
1559
  on_search,
1560
+ inputs=[items_per_page_slider, sort_order_radio, uuid_search_input, timestamp_start_input, timestamp_end_input, rating_filter, filter_extra_images, filter_negative_images, include_private_checkbox],
1561
  outputs=[feedback_html, page_number, total_pages_display]
1562
  )
1563