YanmHa commited on
Commit
346e154
·
verified ·
1 Parent(s): fb336b4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +80 -162
app.py CHANGED
@@ -9,7 +9,7 @@ import io
9
  from huggingface_hub import HfApi
10
  from huggingface_hub.hf_api import HfHubHTTPError
11
  import traceback
12
- from itertools import combinations
13
 
14
  # ==== 全局配置 ====
15
  # ---- 测试模式开关 ----
@@ -93,12 +93,6 @@ if not os.path.exists(full_subdir_path):
93
  else:
94
  print(f"信息:持久化子目录 '{full_subdir_path}' 已存在。")
95
 
96
- # (调试代码可以按需保留或移除)
97
- # print(f"调试:检查 '{PERSISTENT_STORAGE_BASE}' 的内容:")
98
- # try:
99
- # for item in os.listdir(PERSISTENT_STORAGE_BASE): print(f" - Base Storage Item: {item}")
100
- # except Exception as e: print(f" 错误:列出 '{PERSISTENT_STORAGE_BASE}' 内容失败: {e}")
101
-
102
  GLOBAL_HISTORY_FILE = os.path.join(full_subdir_path, "global_experiment_shown_pairs.json")
103
  if not (os.path.isdir(full_subdir_path) and os.access(full_subdir_path, os.W_OK)):
104
  print(f"严重警告:持久化子目录 '{full_subdir_path}' 无效或不可写。")
@@ -106,13 +100,11 @@ print(f"全局历史文件将被加载/保存到: {GLOBAL_HISTORY_FILE}")
106
 
107
  global_shown_pairs_cache = {}
108
  global_history_has_unsaved_changes = False
109
- exhausted_target_images = set() # <--- 新增:存储已耗尽所有图片对的目标图片
110
 
111
  def load_global_shown_pairs():
112
  global global_shown_pairs_cache, global_history_has_unsaved_changes, exhausted_target_images
113
- # exhausted_target_images 在此版本中不从文件加载,每次启动重置。
114
- # 如果需要持久化它,也需要加入到JSON的读写逻辑中。
115
- exhausted_target_images = set() # 确保每次加载时重置(如果它不是持久化的)
116
 
117
  if not GLOBAL_HISTORY_FILE or not os.path.exists(GLOBAL_HISTORY_FILE):
118
  print(f"信息:全局历史文件 '{GLOBAL_HISTORY_FILE}' 未找到或路径无效。将创建新的空历史记录。")
@@ -146,9 +138,7 @@ def save_global_shown_pairs():
146
  if not GLOBAL_HISTORY_FILE:
147
  print("错误:GLOBAL_HISTORY_FILE 未定义。无法保存历史。")
148
  return False
149
- # print(f"--- save_global_shown_pairs --- 当前工作目录: {os.getcwd()}") # 可按需保留
150
  final_save_path = os.path.abspath(GLOBAL_HISTORY_FILE)
151
- # print(f"--- save_global_shown_pairs --- 尝试保存到文件: {final_save_path}") # 可按需保留
152
  try:
153
  parent_dir = os.path.dirname(final_save_path)
154
  if not os.path.exists(parent_dir):
@@ -162,30 +152,16 @@ def save_global_shown_pairs():
162
  target_img: [sorted(list(pair_fset)) for pair_fset in pairs_set]
163
  for target_img, pairs_set in global_shown_pairs_cache.items()
164
  }
165
- # print(f"调试 SAVE_GLOBAL: 准备保存到JSON的完整data_to_save内容:\n{json.dumps(data_to_save, indent=2, ensure_ascii=False)}") # 可按需保留
166
-
167
  temp_file_path = final_save_path + ".tmp"
168
  with open(temp_file_path, 'w', encoding='utf-8') as f:
169
  json.dump(data_to_save, f, ensure_ascii=False, indent=2)
170
  os.replace(temp_file_path, final_save_path)
171
  print(f"已成功将全局已展示图片对历史保存到 '{final_save_path}'。")
172
  global_history_has_unsaved_changes = False
173
-
174
- # (保存后立即读取的调试代码可以按需保留或移除)
175
- # print(f"调试:尝试立即读取已保存的文件 '{final_save_path}'...")
176
- # try:
177
- # with open(final_save_path, 'r', encoding='utf-8') as f_read: content_read = f_read.read()
178
- # print(f"调试:成功读取文件内容(最多显示前500字符):\n--BEGIN FILE CONTENT--\n{content_read[:500]}\n--END FILE CONTENT--")
179
- # if not content_read.strip(): print("调试:警告 - 读取到的文件内容为空或只有空白符。")
180
- # try:
181
- # json.loads(content_read)
182
- # print("调试:读取到的文件内容是有效的JSON。")
183
- # except json.JSONDecodeError as jde: print(f"调试:错误 - 读取到的文件内容不是有效的JSON: {jde}")
184
- # except FileNotFoundError: print(f"调试:严重错误 - 刚刚保存的文件 '{final_save_path}' 在尝试立即读取时未找到!")
185
- # except Exception as e_read: print(f"调试:读取文件 '{final_save_path}' 时发生其他错误: {e_read}")
186
  return True
187
  except Exception as e:
188
- print(f"���误:保存全局历史文件 '{final_save_path}' 失败: {e}")
189
  return False
190
 
191
  load_global_shown_pairs()
@@ -209,21 +185,16 @@ if REPEAT_SINGLE_TARGET_FOR_TESTING:
209
  if not master_image_list:
210
  print(f"测试模式错误:master_image_list 为空,无法进行重复单一目标图测试。")
211
  else:
212
- # test_target_image = "399162.jpg" # 您可以指定一个特定的测试图片
213
- # if test_target_image in master_image_list:
214
- # master_image_list = [test_target_image]
215
- # print(f"测试模式:master_image_list 已成功设置为: {master_image_list}")
216
- # # load_global_shown_pairs_test() # 如果需要,在这里调用
217
- # else:
218
  original_first_image = master_image_list[0]
219
  master_image_list = [original_first_image]
220
- # print(f"测试模式警告:指定测试图片 '{test_target_image}' 不在原始列表中。")
221
  print(f"测试模式:master_image_list 已被缩减为原列表的第一个图像: {master_image_list}")
222
- if not master_image_list: # 最终检查
223
  print(f"关键错误:无目标图片可用 (master_image_list为空)。实验无法进行。")
224
- # exit() # 考虑是否退出
225
 
226
  # ==== 辅助函数 ====
 
 
 
227
  def get_next_trial_info(current_trial_idx_in_run, current_run_image_list_for_trial, num_trials_in_this_run_for_trial):
228
  global TARGET_DIR, METHOD_ROOTS, SUBJECTS, SENTINEL_TRIAL_INTERVAL
229
  global global_shown_pairs_cache, global_history_has_unsaved_changes, exhausted_target_images
@@ -235,31 +206,34 @@ def get_next_trial_info(current_trial_idx_in_run, current_run_image_list_for_tri
235
  target_full_path = os.path.join(TARGET_DIR, img_filename_original)
236
  trial_number_for_display = current_trial_idx_in_run + 1
237
 
238
- generated_pool = []
239
- other_pool = []
 
240
 
241
  for m_root_path in METHOD_ROOTS:
242
  method_name = os.path.basename(m_root_path)
243
- is_generated_color = method_name == "generated_images_color"
244
-
245
  subjects_for_method = SUBJECTS
246
  if method_name.lower() == "takagi":
247
  if "subj01" in SUBJECTS:
248
  subjects_for_method = ["subj01"]
249
  else:
250
  continue
251
-
252
  for s_id in subjects_for_method:
253
  base, ext = os.path.splitext(img_filename_original)
254
  reconstructed_filename = f"{base}_0{ext}"
255
  candidate_path = os.path.join(m_root_path, s_id, reconstructed_filename)
256
  if os.path.exists(candidate_path):
257
  internal_label = f"{method_name}/{s_id}/{reconstructed_filename}"
258
- if is_generated_color:
259
- generated_pool.append((internal_label, candidate_path))
 
 
 
260
  else:
261
- other_pool.append((internal_label, candidate_path))
262
-
263
 
264
  trial_info = {"image_id": img_filename_original, "target_path": target_full_path, "cur_no": trial_number_for_display, "is_sentinel": False,
265
  "left_display_label": "N/A", "left_internal_label": "N/A", "left_path": None,
@@ -268,13 +242,15 @@ def get_next_trial_info(current_trial_idx_in_run, current_run_image_list_for_tri
268
  is_potential_sentinel_trial = (trial_number_for_display > 0 and trial_number_for_display % SENTINEL_TRIAL_INTERVAL == 0)
269
 
270
  if is_potential_sentinel_trial:
271
- if not pool: print(f"警告:哨兵图 '{img_filename_original}' (trial {trial_number_for_display}) 无候选。")
 
 
 
272
  else:
273
- # ... (哨兵试验逻辑不变) ...
274
  print(f"生成哨兵试验 for '{img_filename_original}' (trial {trial_number_for_display})")
275
  trial_info["is_sentinel"] = True
276
  sentinel_candidate_target_tuple = ("目标图像", target_full_path)
277
- random_reconstruction_candidate_tuple = random.choice(pool)
278
  candidates_for_sentinel = [
279
  (("目标图像", target_full_path), sentinel_candidate_target_tuple[0]),
280
  (("重建图", random_reconstruction_candidate_tuple[1]), random_reconstruction_candidate_tuple[0])
@@ -285,20 +261,22 @@ def get_next_trial_info(current_trial_idx_in_run, current_run_image_list_for_tri
285
  "right_display_label": candidates_for_sentinel[1][0][0], "right_path": candidates_for_sentinel[1][0][1], "right_internal_label": candidates_for_sentinel[1][1],
286
  })
287
  else: # 常规试验
288
- if not generated_pool or not other_pool:
289
- print(f"警告:常规图 '{img_filename_original}' (trial {trial_number_for_display}) 候选少于2 (找到 {len(pool)})。此试验无法进行。")
290
- # 注意:如果这个目标图片是当前轮次中唯一的图片,或者后续图片也无法生成试验,轮次可能会提前结束。
291
- # 我们可以在这里也将其标记为耗尽,以确保它在下一轮开始时被正确过滤。
292
- # exhausted_target_images.add(img_filename_original) # 可选:立即标记,以防万一
 
293
  return None, current_trial_idx_in_run
294
 
295
  target_global_history_set = global_shown_pairs_cache.setdefault(img_filename_original, set())
 
 
296
  all_possible_pairs_in_pool = []
297
- for c1 in generated_pool:
298
- for c2 in other_pool:
299
- pair_labels_fset = frozenset({c1[0], c2[0]})
300
- all_possible_pairs_in_pool.append(((c1, c2), pair_labels_fset))
301
-
302
 
303
  unseen_globally_pairs_with_data = [
304
  item for item in all_possible_pairs_in_pool if item[1] not in target_global_history_set
@@ -311,14 +289,12 @@ def get_next_trial_info(current_trial_idx_in_run, current_run_image_list_for_tri
311
  chosen_pair_frozenset = chosen_pair_data_and_labels[1]
312
  target_global_history_set.add(chosen_pair_frozenset)
313
  global_history_has_unsaved_changes = True
314
- # print(f"调试:目标 '{img_filename_original}': 新全局唯一对 {chosen_pair_frozenset} 已添加。")
315
- else: # 所有唯一对都已展示过
316
- print(f"警告:目标图 '{img_filename_original}' (trial {trial_number_for_display}): 来自当前池的所有 ({len(all_possible_pairs_in_pool)}) 个候选对均已在全局展示过。")
317
- if all_possible_pairs_in_pool: # 确保池中至少能形成一对
318
  print(f"目标图 '{img_filename_original}' 将被标记为已耗尽,未来轮次中将被跳过。")
319
  exhausted_target_images.add(img_filename_original)
320
- # global_history_has_unsaved_changes = True # 如果 exhausted_target_images 需要持久化,这里也应该标记
321
- return None, current_trial_idx_in_run # 返回 None,表示此图片本次无法生成新试验
322
 
323
  display_order_candidates = list(selected_candidates_tuples)
324
  if random.random() > 0.5:
@@ -334,7 +310,7 @@ def save_single_log_to_hf_dataset(log_entry, user_identifier_str):
334
  global DATASET_REPO_ID, INDIVIDUAL_LOGS_FOLDER
335
  if not isinstance(log_entry, dict):
336
  print(f"错误:单个日志条目不是字典格式,无法保存:{log_entry}")
337
- return False # 返回失败
338
  current_user_id = user_identifier_str if user_identifier_str else "unknown_user_session"
339
  identifier_safe = str(current_user_id).replace('.', '_').replace(':', '_').replace('/', '_')
340
  print(f"用户 {identifier_safe} - 准备保存单条日志 for image {log_entry.get('image_id', 'Unknown')}...")
@@ -342,12 +318,11 @@ def save_single_log_to_hf_dataset(log_entry, user_identifier_str):
342
  token = os.getenv("HF_TOKEN")
343
  if not token:
344
  print("错误:环境变量 HF_TOKEN 未设置。无法保存单条日志到Dataset。")
345
- return False # 返回失败
346
  if not DATASET_REPO_ID:
347
  print("错误:DATASET_REPO_ID 未配置。无法保存单条日志到Dataset。")
348
- return False # 返回失败
349
  api = HfApi(token=token)
350
- # ... (文件名和路径生成逻辑不变) ...
351
  image_id_safe_for_filename = os.path.splitext(log_entry.get("image_id", "unknown_img"))[0].replace('.', '_').replace(':', '_').replace('/', '_')
352
  file_creation_timestamp_str = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
353
  unique_filename = (f"run{log_entry.get('run_no', 'X')}_trial{log_entry.get('trial_sequence_in_run', 'Y')}_img{image_id_safe_for_filename}_{file_creation_timestamp_str}.json")
@@ -357,10 +332,8 @@ def save_single_log_to_hf_dataset(log_entry, user_identifier_str):
357
  json_content = json.dumps(log_entry, ensure_ascii=False, indent=2)
358
  except Exception as json_err:
359
  print(f"错误:序列化单条日志时出错: {log_entry}. 错误: {json_err}")
360
- # 即使序列化失败,也尝试记录一个最小化的错误日志
361
  error_log_content = {"error": "serialization_failed_single", "original_data_keys": list(log_entry.keys()) if isinstance(log_entry, dict) else None, "timestamp": datetime.now().isoformat()}
362
  json_content = json.dumps(error_log_content, ensure_ascii=False, indent=2)
363
- # return False # 序列化失败也认为是一种保存失败
364
 
365
  log_bytes = json_content.encode('utf-8')
366
  file_like_object = io.BytesIO(log_bytes)
@@ -373,22 +346,22 @@ def save_single_log_to_hf_dataset(log_entry, user_identifier_str):
373
  commit_message=(f"Log choice: img {log_entry.get('image_id', 'N/A')}, run {log_entry.get('run_no', 'N/A')}, trial {log_entry.get('trial_sequence_in_run', 'N/A')} by {identifier_safe}")
374
  )
375
  print(f"单条日志已成功保存到 HF Dataset: {DATASET_REPO_ID}/{path_in_repo}")
376
- return True # 返回成功
377
  except HfHubHTTPError as hf_http_error:
378
  print(f"保存单条日志到 Hugging Face Dataset 时发生 HTTP 错误 (可能被限流或权限问题): {hf_http_error}")
379
  traceback.print_exc()
380
- return False # 返回失败
381
  except Exception as e:
382
  print(f"保存单条日志 (image {log_entry.get('image_id', 'Unknown')}, user {identifier_safe}) 到 Hugging Face Dataset 时发生严重错误: {e}")
383
  traceback.print_exc()
384
- return False # 返回失败
385
 
386
  # ==== 批量保存用户选择日志函数 (确保返回 True/False) ====
387
  def save_collected_logs_batch(list_of_log_entries, user_identifier_str, batch_identifier):
388
  global DATASET_REPO_ID, BATCH_LOG_FOLDER
389
  if not list_of_log_entries:
390
  print("批量保存用户日志:没有累积的日志。")
391
- return True # 没有日志也算“成功”完成此操作
392
  identifier_safe = str(user_identifier_str if user_identifier_str else "unknown_user_session").replace('.', '_').replace(':', '_').replace('/', '_').replace(' ', '_')
393
  print(f"用户 {identifier_safe} - 准备批量保存 {len(list_of_log_entries)} 条选择日志 (批次标识: {batch_identifier})...")
394
  try:
@@ -400,7 +373,6 @@ def save_collected_logs_batch(list_of_log_entries, user_identifier_str, batch_id
400
  print("错误:DATASET_REPO_ID 未配置。无法批量保存选择日志。")
401
  return False
402
  api = HfApi(token=token)
403
- # ... (文件名和内容生成逻辑不变) ...
404
  timestamp_str = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
405
  batch_filename = f"batch_user-{identifier_safe}_id-{batch_identifier}_{timestamp_str}_logs-{len(list_of_log_entries)}.jsonl"
406
  path_in_repo = f"{BATCH_LOG_FOLDER}/{identifier_safe}/{batch_filename}"
@@ -415,7 +387,7 @@ def save_collected_logs_batch(list_of_log_entries, user_identifier_str, batch_id
415
 
416
  if not jsonl_content.strip():
417
  print(f"用户 {identifier_safe} (批次 {batch_identifier}) 无可序列化选择日志。")
418
- return True # 没有内容可保存也算“成功”
419
 
420
  log_bytes = jsonl_content.encode('utf-8')
421
  file_like_object = io.BytesIO(log_bytes)
@@ -456,12 +428,10 @@ def process_experiment_step(
456
  user_ip_fallback = request.client.host if request else "unknown_ip"
457
  user_identifier_for_logging = output_s_user_session_id if output_s_user_session_id else user_ip_fallback
458
 
459
- # outputs_ui_components_definition 长度现在是 11
460
- # (target_img, left_img, right_img, left_lbl, right_lbl, status_text, progress_text, btn_start, btn_left, btn_right, file_out_placeholder)
461
  len_ui_outputs = len(outputs_ui_components_definition)
462
 
463
  def create_ui_error_tuple(message, progress_msg_text, stop_experiment=False):
464
- btn_start_interactive = not stop_experiment # 如果停止实验,开始按钮也禁用
465
  btn_choices_interactive = not stop_experiment
466
  return (gr.update(visible=False),) * 3 + \
467
  ("", "") + \
@@ -474,7 +444,6 @@ def process_experiment_step(
474
 
475
  if action_type == "record_choice":
476
  if output_s_current_trial_data.get("data") and output_s_current_trial_data["data"].get("left_internal_label"):
477
- # ... (log_entry 创建逻辑不变) ...
478
  chosen_internal_label = (output_s_current_trial_data["data"]["left_internal_label"] if choice_value == "left" else output_s_current_trial_data["data"]["right_internal_label"])
479
  parsed_chosen_method, parsed_chosen_subject, parsed_chosen_filename = "N/A", "N/A", "N/A"
480
  if chosen_internal_label == "目标图像": parsed_chosen_method, parsed_chosen_subject, parsed_chosen_filename = "TARGET", "GT", output_s_current_trial_data["data"]["image_id"]
@@ -496,7 +465,6 @@ def process_experiment_step(
496
  output_s_user_logs.append(log_entry)
497
  print(f"用户 {user_identifier_for_logging} 记录选择 (img: {log_entry['image_id']})。当前批次日志数: {len(output_s_user_logs)}")
498
 
499
- # 尝试批量保存日志
500
  if len(output_s_user_logs) >= LOG_BATCH_SIZE:
501
  print(f"累积用户选择日志达到 {LOG_BATCH_SIZE} 条,准备批量保存...")
502
  batch_id_for_filename = f"run{output_s_run_no}_trialidx{output_s_trial_idx}_logcount{len(output_s_user_logs)}"
@@ -506,14 +474,12 @@ def process_experiment_step(
506
  output_s_user_logs = []
507
  else:
508
  print("严重错误:批量用户选择日志保存失败。实验无法继续。")
509
- # 更新UI以显示错误并停止实验
510
  error_message_ui = "错误:日志保存失败,可能是网络问题或API限流。实验已停止,请联系管理员。"
511
  progress_message_ui = f"用户ID: {user_id_display_text} | 实验因错误停止在第 {output_s_run_no} 轮,试验 {output_s_trial_idx+1}"
512
  error_ui_updates = create_ui_error_tuple(error_message_ui, progress_message_ui, stop_experiment=True)
513
  return output_s_trial_idx, output_s_run_no, output_s_user_logs, output_s_current_trial_data, \
514
  output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *error_ui_updates
515
 
516
- # 尝试保存全局历史
517
  if global_history_has_unsaved_changes:
518
  print("检测到全局图片对历史自上次保存后有更新,将一并保存...")
519
  if not save_global_shown_pairs():
@@ -525,18 +491,15 @@ def process_experiment_step(
525
  output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *error_ui_updates
526
  else:
527
  print(f"用户 {user_identifier_for_logging} 错误:记录选择时当前试验数据为空或缺少internal_label!")
528
- # ... (这里的错误处理保持不变,因为它不是API保存错误) ...
529
- error_ui_updates = create_ui_error_tuple("记录选择时内部错误。", f"用户ID: {user_id_display_text} | 进度:{output_s_trial_idx}/{output_s_num_trials_this_run}", stop_experiment=False) # 这种错误不一定停止实验
530
  return output_s_trial_idx, output_s_run_no, output_s_user_logs, output_s_current_trial_data, output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *error_ui_updates
531
 
532
- # ... (start_experiment 和后续的试验获取逻辑基本保持不变) ...
533
- # --- 在 start_experiment 开始新轮次前,或在轮次结束时,也需要检查并尝试保存剩余日志和全局历史 ---
534
  if action_type == "start_experiment":
535
  is_first = (output_s_num_trials_this_run == 0 and output_s_trial_idx == 0 and output_s_run_no == 1)
536
  is_completed_for_restart = (output_s_num_trials_this_run > 0 and output_s_trial_idx >= output_s_num_trials_this_run)
537
 
538
- if is_completed_for_restart: # 如果是完成一轮后准备开始下一轮
539
- if output_s_user_logs: # 尝试保存上一轮剩余的日志
540
  print(f"轮次 {output_s_run_no-1} 结束,尝试保存剩余的 {len(output_s_user_logs)} 条用户选择日志...")
541
  batch_id_for_filename = f"run{output_s_run_no-1}_final_logcount{len(output_s_user_logs)}"
542
  if not save_collected_logs_batch(list(output_s_user_logs), user_identifier_for_logging, batch_id_for_filename):
@@ -546,7 +509,7 @@ def process_experiment_step(
546
  error_ui_updates = create_ui_error_tuple(error_message_ui, progress_message_ui, stop_experiment=True)
547
  return output_s_trial_idx, output_s_run_no-1, output_s_user_logs, output_s_current_trial_data, \
548
  output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *error_ui_updates
549
- output_s_user_logs = [] # 清空
550
 
551
  if global_history_has_unsaved_changes:
552
  print("轮次结束,尝试保存全局图片对历史...")
@@ -558,29 +521,25 @@ def process_experiment_step(
558
  return output_s_trial_idx, output_s_run_no-1, output_s_user_logs, output_s_current_trial_data, \
559
  output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *error_ui_updates
560
 
561
- # ... (后续的 start_experiment 逻辑不变,如选择图片,重置状态等) ...
562
  if is_first or is_completed_for_restart:
563
  if is_completed_for_restart: output_s_run_no += 1
564
  available_master_images = [img for img in master_image_list if img not in exhausted_target_images]
565
  print(f"开始轮次 {output_s_run_no}: 从 {len(master_image_list)}个总目标图片中筛选,可用图片 {len(available_master_images)}个 (已排除 {len(exhausted_target_images)}个已耗尽图片).")
566
  if not available_master_images:
567
- # ... (所有图片耗尽的逻辑不变) ...
568
  msg = "所有目标图片的所有唯一图片对均已展示完毕!感谢您的参与。"
569
  prog_text = f"用户ID: {user_id_display_text} | 实验完成!"
570
- # 在这里,如果还有未保存的日志,也应该尝试保存
571
  if output_s_user_logs:
572
  print(f"最终轮次结束,尝试保存剩余的 {len(output_s_user_logs)} 条用户选择日志...")
573
- batch_id_for_filename = f"run{output_s_run_no-1}_final_logcount{len(output_s_user_logs)}" # 使用上一轮的run_no
574
- save_collected_logs_batch(list(output_s_user_logs), user_identifier_for_logging, batch_id_for_filename) # 即使失败,也继续显示完成信息
575
  output_s_user_logs = []
576
  if global_history_has_unsaved_changes:
577
  print("实验最终结束,尝试保存全局图片对历史...")
578
- save_global_shown_pairs() # 即使失败,也继续
579
 
580
- ui_updates = list(create_ui_error_tuple(msg, prog_text, stop_experiment=True)) # 停止实验
581
  return 0, output_s_run_no, [], {}, output_s_user_session_id, [], 0, *tuple(ui_updates)
582
 
583
- # ... (后续 start_experiment 逻辑:选择图片,重置 trial_idx, user_logs, current_trial_data 等)
584
  if REPEAT_SINGLE_TARGET_FOR_TESTING and available_master_images:
585
  print(f"测试模式 (重复单一目标图) 已激活。")
586
  single_image_to_repeat = available_master_images[0]
@@ -592,44 +551,31 @@ def process_experiment_step(
592
  current_run_max_trials = NUM_TRIALS_PER_RUN
593
  run_size = min(num_really_avail, current_run_max_trials)
594
  if run_size == 0:
595
- error_ui = create_ui_error_tuple("错误: 可用图片采样数为0!", f"用户ID: {user_id_display_text} | 进度: 0/0", stop_experiment=False) # 不一定停止
596
  return 0, output_s_run_no, output_s_user_logs, {}, output_s_user_session_id, [], 0, *error_ui
597
  output_s_current_run_image_list = random.sample(available_master_images, run_size)
598
  output_s_num_trials_this_run = run_size
599
 
600
  output_s_trial_idx = 0
601
- output_s_current_trial_data = {} # 新轮次重置当前试验数据
602
- # output_s_user_logs 已经在 is_completed_for_restart 分支中被清空(如果保存成功)
603
- # 如果是 is_first,它本身就是空的
604
- if is_first: # 只有第一次启动时分配新用户ID
605
  timestamp_str = datetime.now().strftime('%Y%m%d%H%M%S%f'); random_val = random.randint(10000, 99999)
606
- if not output_s_user_session_id: # 确保只在没有 session_id 时创建 (例如从 handle_agree_and_start 那里已经有了)
607
  output_s_user_session_id = f"user_{timestamp_str}_{random_val}"; user_identifier_for_logging = output_s_user_session_id
608
  else:
609
- user_identifier_for_logging = output_s_user_session_id # 使用从用户信息处获得的ID
610
  print(f"用户会话ID: {output_s_user_session_id}")
611
  print(f"开始/继续轮次 {output_s_run_no} (用户ID: {output_s_user_session_id}). 本轮共 {output_s_num_trials_this_run} 个试验。")
612
- else: # 中途点击开始
613
  print(f"用户 {user_identifier_for_logging} 在第 {output_s_run_no} 轮,试验 {output_s_trial_idx} 点击开始,但轮次未完成。忽略。")
614
  no_change_ui = create_no_change_tuple()
615
  return output_s_trial_idx, output_s_run_no, output_s_user_logs, output_s_current_trial_data, output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *no_change_ui
616
 
617
-
618
- # --- 获取下一个试验并更新UI的逻辑 ---
619
- # ... (这部分逻辑与您上一版包含while循环尝试获取有效试验的代码基本相同) ...
620
- # ... (确保所有 yield 和 return 都返回7个状态变量 + 11个UI更新) ...
621
- # ... (并且在循环结束仍未找到 trial_info 时,也尝试保存剩余日志和全局历史) ...
622
  current_actual_trial_index_for_get_next = output_s_trial_idx
623
- # (上一段 action_type == "record_choice" 已经处理了日志保存,所以这里主要处理 trial_info 获取)
624
-
625
  if current_actual_trial_index_for_get_next >= output_s_num_trials_this_run and output_s_num_trials_this_run > 0:
626
- # (这个条件实际上在 record_choice 后,或 start_experiment 的 is_completed_for_restart 后,output_s_trial_idx 可能已经等于或超过)
627
- # (这里的逻辑主要是为了在 yield 返回前,如果恰好是轮次结束,则正确显示结束信息)
628
- # (大部分轮次结束逻辑已移到 action_type == "record_choice" 和 "start_experiment" 的 is_completed_for_restart 部分)
629
- print(f"调试: 在获取下一个试验前,检测到轮次 {output_s_run_no} 可能已结束 (idx: {current_actual_trial_index_for_get_next} >= num_trials: {output_s_num_trials_this_run})")
630
- # (如果确实结束,应该已经在前面返回了,这里是一个保险或冗余检查)
631
  prog_text = f"用户ID: {output_s_user_session_id} | 进度:{output_s_num_trials_this_run}/{output_s_num_trials_this_run} | 第 {output_s_run_no} 轮 🎉"
632
- ui_updates = list(create_ui_error_tuple(f"🎉 第 {output_s_run_no} 轮完成!请点击“开始试验 / 下一轮”继续。", prog_text, stop_experiment=False)) # 完成一轮不立即停止
633
  ui_updates[7]=gr.update(interactive=True); ui_updates[8]=gr.update(interactive=False); ui_updates[9]=gr.update(interactive=False)
634
  ui_updates[0]=gr.update(value=None,visible=False); ui_updates[1]=gr.update(value=None,visible=False); ui_updates[2]=gr.update(value=None,visible=False)
635
  yield output_s_trial_idx, output_s_run_no, output_s_user_logs, {"data": None}, output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *ui_updates; return
@@ -654,24 +600,20 @@ def process_experiment_step(
654
 
655
  if _trial_info_candidate is not None:
656
  trial_info = _trial_info_candidate
657
- output_s_trial_idx = _returned_next_idx # 更新主状态的 trial_idx
658
  break
659
  else:
660
  print(f"信息:目标图 '{current_target_image_for_trial}' 无法生成有效试验。尝试列表中的下一个。")
661
  next_s_trial_idx_for_state_loop +=1
662
  output_s_trial_idx = next_s_trial_idx_for_state_loop
663
- # exhausted_target_images 应该在 get_next_trial_info 内部被更新
664
 
665
  if trial_info is None:
666
  print(f"轮次 {output_s_run_no} 中没有更多可用的有效试验了。结束本轮。")
667
- # 尝试保存剩余日志
668
  if output_s_user_logs:
669
  print(f"轮次 {output_s_run_no} 无更多有效试验,尝试保存剩余 {len(output_s_user_logs)} 条日志...")
670
  batch_id_for_filename = f"run{output_s_run_no}_no_more_trials_logcount{len(output_s_user_logs)}"
671
  if not save_collected_logs_batch(list(output_s_user_logs), user_identifier_for_logging, batch_id_for_filename):
672
- # 即使这里保存失败,也应该让用户知道轮次结束了,但可能需要更强的错误提示
673
- print("严重错误:保存剩余日志失败。���验可能需要停止。")
674
- # 可以选择在这里返回一个停止实验的UI更新
675
  output_s_user_logs = []
676
  if global_history_has_unsaved_changes:
677
  print("轮次无更多有效试验,尝试保存全局图片对历史...")
@@ -697,17 +639,13 @@ def process_experiment_step(
697
 
698
  ui_show_candidates_updates = list(create_no_change_tuple())
699
  ui_show_candidates_updates[0]=gr.update(value=None,visible=False); ui_show_candidates_updates[1]=gr.update(value=trial_info["left_path"],visible=True); ui_show_candidates_updates[2]=gr.update(value=trial_info["right_path"],visible=True)
700
- ui_show_candidates_updates[3]=gr.update(value=trial_info["left_display_label"], visible=True); ui_show_candidates_updates[4]=gr.update(value=trial_info["right_display_label"], visible=True) # 改回 True
701
  ui_show_candidates_updates[5]="请选择更像原图的一张"; ui_show_candidates_updates[6]=prog_text
702
  ui_show_candidates_updates[7]=gr.update(interactive=False); ui_show_candidates_updates[8]=gr.update(interactive=True); ui_show_candidates_updates[9]=gr.update(interactive=True)
703
  yield output_s_trial_idx, output_s_run_no, output_s_user_logs, output_s_current_trial_data, output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *ui_show_candidates_updates
704
 
705
- # ==== Gradio UI 定义 和 程序入口 (与您上一版相同,包含下载按钮等) ====
706
- # ... (welcome_page_markdown, handle_agree_and_start, handle_download_history_file) ...
707
- # ... (with gr.Blocks(...) as demo: ...) ...
708
- # ... (if __name__ == "__main__": ...) ...
709
- # (此处省略这部分,因为它们与您包含下载按钮的上一版代码相同)
710
- def handle_download_history_file(): # 确保这个函数在UI定义之前
711
  global GLOBAL_HISTORY_FILE
712
  if os.path.exists(GLOBAL_HISTORY_FILE):
713
  try:
@@ -785,21 +723,21 @@ with gr.Blocks(css=CSS, title="图像重建主观评估") as demo:
785
  with experiment_container:
786
  gr.Markdown("## 🧠 图像重建主观评估实验"); gr.Markdown(f"每轮实验大约有 {NUM_TRIALS_PER_RUN} 次比较。")
787
  with gr.Row():
788
- with gr.Column(scale=1, min_width=300): left_img = gr.Image(label="左候选图", visible=False, height=400, interactive=False); left_lbl = gr.Textbox(label="左图信息", value="", visible=True, interactive=False, max_lines=1); btn_left = gr.Button("选择左图 (更相似)", interactive=False, elem_classes="compact_button") # left_lbl 改回 visible=True
789
- with gr.Column(scale=1, min_width=300): right_img = gr.Image(label="右候选图", visible=False, height=400, interactive=False); right_lbl = gr.Textbox(label="右图信息",value="", visible=True, interactive=False, max_lines=1); btn_right = gr.Button("选择右图 (更相似)", interactive=False, elem_classes="compact_button") # right_lbl 改回 visible=True
790
  with gr.Row(): target_img = gr.Image(label="目标图像 (观察3秒后消失)", visible=False, height=400, interactive=False)
791
  with gr.Row(): status_text = gr.Markdown(value="请点击“开始试验 / 下一轮”按钮。")
792
- with gr.Row(): progress_text = gr.Markdown() # 初始为空
793
  with gr.Row():
794
  btn_start = gr.Button("开始试验 / 下一轮")
795
  btn_download_json = gr.Button("下载JSON历史记录")
796
  json_download_output = gr.File(label="下载的文件会在此处提供", interactive=False)
797
- file_out_placeholder = gr.File(label=" ", visible=False, interactive=False) # 保持这个占位符
798
 
799
  outputs_ui_components_definition = [
800
  target_img, left_img, right_img, left_lbl, right_lbl, status_text, progress_text,
801
  btn_start, btn_left, btn_right, file_out_placeholder
802
- ] # 确保这里是 11 个组件
803
  click_inputs_base = [
804
  s_trial_index, s_run_no, s_user_logs, s_current_trial_data, s_user_session_id,
805
  s_current_run_image_list, s_num_trials_this_run
@@ -815,33 +753,13 @@ with gr.Blocks(css=CSS, title="图像重建主观评估") as demo:
815
  btn_right.click(fn=partial(process_experiment_step, action_type="record_choice", choice_value="right"), inputs=click_inputs_base, outputs=event_outputs, queue=True)
816
  btn_download_json.click(fn=handle_download_history_file, inputs=None, outputs=[json_download_output, status_text])
817
 
818
-
819
-
820
-
821
-
822
-
823
-
824
-
825
-
826
-
827
-
828
-
829
-
830
-
831
-
832
-
833
-
834
-
835
-
836
-
837
  if __name__ == "__main__":
838
- # ... (您的 __main__ 部分代码保持不变,包含所有路径检查和 Gradio 启动) ...
839
  if not master_image_list: print("\n关键错误:程序无法启动,因无目标图片。"); exit()
840
  else:
841
  print(f"从 '{TARGET_DIR}' 加载 {len(master_image_list)} 张目标���片。")
842
  if not METHOD_ROOTS: print(f"警告: '{BASE_IMAGE_DIR}' 无候选方法子目录。")
843
  if not SUBJECTS: print("警告: SUBJECTS 列表为空。")
844
- print(f"用户选择日志保存到 Dataset: '{DATASET_REPO_ID}' 的 '{BATCH_LOG_FOLDER}/ 文件夹") # 修正日志文件夹描述
845
  if not os.getenv("HF_TOKEN"): print("警告: HF_TOKEN 未设置。日志无法保存到Hugging Face Dataset。\n 请在 Space Secrets 中设置 HF_TOKEN。")
846
  else: print("HF_TOKEN 已找到。")
847
  print(f"全局图片对历史将从 '{GLOBAL_HISTORY_FILE}' 加载/保存到此文件。")
@@ -865,9 +783,9 @@ if __name__ == "__main__":
865
  except Exception as e_mkdir_main:
866
  print(f"错误:在 main 中创建目录 '{PERSISTENT_STORAGE_BASE}' 失败: {e_mkdir_main}")
867
 
868
- final_allowed_paths = list(set(allowed_paths_list)) # 去重
869
  if final_allowed_paths:
870
- print(f"Gradio `demo.launch()` 配置最终 allowed_paths: {final_allowed_paths}")
871
  else:
872
  print("警告:没有有效的 allowed_paths 被配置。Gradio文件访问可能受限。")
873
 
 
9
  from huggingface_hub import HfApi
10
  from huggingface_hub.hf_api import HfHubHTTPError
11
  import traceback
12
+ from itertools import combinations, product
13
 
14
  # ==== 全局配置 ====
15
  # ---- 测试模式开关 ----
 
93
  else:
94
  print(f"信息:持久化子目录 '{full_subdir_path}' 已存在。")
95
 
 
 
 
 
 
 
96
  GLOBAL_HISTORY_FILE = os.path.join(full_subdir_path, "global_experiment_shown_pairs.json")
97
  if not (os.path.isdir(full_subdir_path) and os.access(full_subdir_path, os.W_OK)):
98
  print(f"严重警告:持久化子目录 '{full_subdir_path}' 无效或不可写。")
 
100
 
101
  global_shown_pairs_cache = {}
102
  global_history_has_unsaved_changes = False
103
+ exhausted_target_images = set()
104
 
105
  def load_global_shown_pairs():
106
  global global_shown_pairs_cache, global_history_has_unsaved_changes, exhausted_target_images
107
+ exhausted_target_images = set()
 
 
108
 
109
  if not GLOBAL_HISTORY_FILE or not os.path.exists(GLOBAL_HISTORY_FILE):
110
  print(f"信息:全局历史文件 '{GLOBAL_HISTORY_FILE}' 未找到或路径无效。将创建新的空历史记录。")
 
138
  if not GLOBAL_HISTORY_FILE:
139
  print("错误:GLOBAL_HISTORY_FILE 未定义。无法保存历史。")
140
  return False
 
141
  final_save_path = os.path.abspath(GLOBAL_HISTORY_FILE)
 
142
  try:
143
  parent_dir = os.path.dirname(final_save_path)
144
  if not os.path.exists(parent_dir):
 
152
  target_img: [sorted(list(pair_fset)) for pair_fset in pairs_set]
153
  for target_img, pairs_set in global_shown_pairs_cache.items()
154
  }
155
+
 
156
  temp_file_path = final_save_path + ".tmp"
157
  with open(temp_file_path, 'w', encoding='utf-8') as f:
158
  json.dump(data_to_save, f, ensure_ascii=False, indent=2)
159
  os.replace(temp_file_path, final_save_path)
160
  print(f"已成功将全局已展示图片对历史保存到 '{final_save_path}'。")
161
  global_history_has_unsaved_changes = False
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  return True
163
  except Exception as e:
164
+ print(f"错误:保存全局历史文件 '{final_save_path}' 失败: {e}")
165
  return False
166
 
167
  load_global_shown_pairs()
 
185
  if not master_image_list:
186
  print(f"测试模式错误:master_image_list 为空,无法进行重复单一目标图测试。")
187
  else:
 
 
 
 
 
 
188
  original_first_image = master_image_list[0]
189
  master_image_list = [original_first_image]
 
190
  print(f"测试模式:master_image_list 已被缩减为原列表的第一个图像: {master_image_list}")
191
+ if not master_image_list:
192
  print(f"关键错误:无目标图片可用 (master_image_list为空)。实验无法进行。")
 
193
 
194
  # ==== 辅助函数 ====
195
+ # #############################################################################
196
+ # ############# 函数修改点:get_next_trial_info ################################
197
+ # #############################################################################
198
  def get_next_trial_info(current_trial_idx_in_run, current_run_image_list_for_trial, num_trials_in_this_run_for_trial):
199
  global TARGET_DIR, METHOD_ROOTS, SUBJECTS, SENTINEL_TRIAL_INTERVAL
200
  global global_shown_pairs_cache, global_history_has_unsaved_changes, exhausted_target_images
 
206
  target_full_path = os.path.join(TARGET_DIR, img_filename_original)
207
  trial_number_for_display = current_trial_idx_in_run + 1
208
 
209
+ # ---- MODIFICATION START: Create two separate pools for candidates ----
210
+ pool_generated_color = []
211
+ pool_other_methods = []
212
 
213
  for m_root_path in METHOD_ROOTS:
214
  method_name = os.path.basename(m_root_path)
215
+
 
216
  subjects_for_method = SUBJECTS
217
  if method_name.lower() == "takagi":
218
  if "subj01" in SUBJECTS:
219
  subjects_for_method = ["subj01"]
220
  else:
221
  continue
222
+
223
  for s_id in subjects_for_method:
224
  base, ext = os.path.splitext(img_filename_original)
225
  reconstructed_filename = f"{base}_0{ext}"
226
  candidate_path = os.path.join(m_root_path, s_id, reconstructed_filename)
227
  if os.path.exists(candidate_path):
228
  internal_label = f"{method_name}/{s_id}/{reconstructed_filename}"
229
+ candidate_tuple = (internal_label, candidate_path)
230
+
231
+ # Segregate candidates into the two pools
232
+ if method_name == "generated_images_color":
233
+ pool_generated_color.append(candidate_tuple)
234
  else:
235
+ pool_other_methods.append(candidate_tuple)
236
+ # ---- MODIFICATION END: Candidate pools are now populated ----
237
 
238
  trial_info = {"image_id": img_filename_original, "target_path": target_full_path, "cur_no": trial_number_for_display, "is_sentinel": False,
239
  "left_display_label": "N/A", "left_internal_label": "N/A", "left_path": None,
 
242
  is_potential_sentinel_trial = (trial_number_for_display > 0 and trial_number_for_display % SENTINEL_TRIAL_INTERVAL == 0)
243
 
244
  if is_potential_sentinel_trial:
245
+ # For sentinel trials, we just need one random reconstruction. Combine pools to pick one.
246
+ combined_pool = pool_generated_color + pool_other_methods
247
+ if not combined_pool:
248
+ print(f"警告:哨兵图 '{img_filename_original}' (trial {trial_number_for_display}) 无任何候选图。")
249
  else:
 
250
  print(f"生成哨兵试验 for '{img_filename_original}' (trial {trial_number_for_display})")
251
  trial_info["is_sentinel"] = True
252
  sentinel_candidate_target_tuple = ("目标图像", target_full_path)
253
+ random_reconstruction_candidate_tuple = random.choice(combined_pool)
254
  candidates_for_sentinel = [
255
  (("目标图像", target_full_path), sentinel_candidate_target_tuple[0]),
256
  (("重建图", random_reconstruction_candidate_tuple[1]), random_reconstruction_candidate_tuple[0])
 
261
  "right_display_label": candidates_for_sentinel[1][0][0], "right_path": candidates_for_sentinel[1][0][1], "right_internal_label": candidates_for_sentinel[1][1],
262
  })
263
  else: # 常规试验
264
+ # ---- MODIFICATION START: New check and pairing logic ----
265
+ # Check if both pools have at least one candidate
266
+ if not pool_generated_color or not pool_other_methods:
267
+ print(f"警告:常规图 '{img_filename_original}' (trial {trial_number_for_display}) 候选不足以形成指定对。 "
268
+ f"('generated_images_color' 找到 {len(pool_generated_color)} 个, "
269
+ f"其他方法找到 {len(pool_other_methods)} 个)。此试验无法进行。")
270
  return None, current_trial_idx_in_run
271
 
272
  target_global_history_set = global_shown_pairs_cache.setdefault(img_filename_original, set())
273
+
274
+ # Generate all pairs by picking one from each pool
275
  all_possible_pairs_in_pool = []
276
+ for c_color, c_other in product(pool_generated_color, pool_other_methods):
277
+ pair_labels_fset = frozenset({c_color[0], c_other[0]})
278
+ all_possible_pairs_in_pool.append( ((c_color, c_other), pair_labels_fset) )
279
+ # ---- MODIFICATION END: New pairing logic is complete ----
 
280
 
281
  unseen_globally_pairs_with_data = [
282
  item for item in all_possible_pairs_in_pool if item[1] not in target_global_history_set
 
289
  chosen_pair_frozenset = chosen_pair_data_and_labels[1]
290
  target_global_history_set.add(chosen_pair_frozenset)
291
  global_history_has_unsaved_changes = True
292
+ else:
293
+ print(f"警告:目标图 '{img_filename_original}' (trial {trial_number_for_display}): 所有 ({len(all_possible_pairs_in_pool)}) 个 'generated_color' vs 'other' 对均已在全局展示过。")
294
+ if all_possible_pairs_in_pool:
 
295
  print(f"目标图 '{img_filename_original}' 将被标记为已耗尽,未来轮次中将被跳过。")
296
  exhausted_target_images.add(img_filename_original)
297
+ return None, current_trial_idx_in_run
 
298
 
299
  display_order_candidates = list(selected_candidates_tuples)
300
  if random.random() > 0.5:
 
310
  global DATASET_REPO_ID, INDIVIDUAL_LOGS_FOLDER
311
  if not isinstance(log_entry, dict):
312
  print(f"错误:单个日志条目不是字典格式,无法保存:{log_entry}")
313
+ return False
314
  current_user_id = user_identifier_str if user_identifier_str else "unknown_user_session"
315
  identifier_safe = str(current_user_id).replace('.', '_').replace(':', '_').replace('/', '_')
316
  print(f"用户 {identifier_safe} - 准备保存单条日志 for image {log_entry.get('image_id', 'Unknown')}...")
 
318
  token = os.getenv("HF_TOKEN")
319
  if not token:
320
  print("错误:环境变量 HF_TOKEN 未设置。无法保存单条日志到Dataset。")
321
+ return False
322
  if not DATASET_REPO_ID:
323
  print("错误:DATASET_REPO_ID 未配置。无法保存单条日志到Dataset。")
324
+ return False
325
  api = HfApi(token=token)
 
326
  image_id_safe_for_filename = os.path.splitext(log_entry.get("image_id", "unknown_img"))[0].replace('.', '_').replace(':', '_').replace('/', '_')
327
  file_creation_timestamp_str = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
328
  unique_filename = (f"run{log_entry.get('run_no', 'X')}_trial{log_entry.get('trial_sequence_in_run', 'Y')}_img{image_id_safe_for_filename}_{file_creation_timestamp_str}.json")
 
332
  json_content = json.dumps(log_entry, ensure_ascii=False, indent=2)
333
  except Exception as json_err:
334
  print(f"错误:序列化单条日志时出错: {log_entry}. 错误: {json_err}")
 
335
  error_log_content = {"error": "serialization_failed_single", "original_data_keys": list(log_entry.keys()) if isinstance(log_entry, dict) else None, "timestamp": datetime.now().isoformat()}
336
  json_content = json.dumps(error_log_content, ensure_ascii=False, indent=2)
 
337
 
338
  log_bytes = json_content.encode('utf-8')
339
  file_like_object = io.BytesIO(log_bytes)
 
346
  commit_message=(f"Log choice: img {log_entry.get('image_id', 'N/A')}, run {log_entry.get('run_no', 'N/A')}, trial {log_entry.get('trial_sequence_in_run', 'N/A')} by {identifier_safe}")
347
  )
348
  print(f"单条日志已成功保存到 HF Dataset: {DATASET_REPO_ID}/{path_in_repo}")
349
+ return True
350
  except HfHubHTTPError as hf_http_error:
351
  print(f"保存单条日志到 Hugging Face Dataset 时发生 HTTP 错误 (可能被限流或权限问题): {hf_http_error}")
352
  traceback.print_exc()
353
+ return False
354
  except Exception as e:
355
  print(f"保存单条日志 (image {log_entry.get('image_id', 'Unknown')}, user {identifier_safe}) 到 Hugging Face Dataset 时发生严重错误: {e}")
356
  traceback.print_exc()
357
+ return False
358
 
359
  # ==== 批量保存用户选择日志函数 (确保返回 True/False) ====
360
  def save_collected_logs_batch(list_of_log_entries, user_identifier_str, batch_identifier):
361
  global DATASET_REPO_ID, BATCH_LOG_FOLDER
362
  if not list_of_log_entries:
363
  print("批量保存用户日志:没有累积的日志。")
364
+ return True
365
  identifier_safe = str(user_identifier_str if user_identifier_str else "unknown_user_session").replace('.', '_').replace(':', '_').replace('/', '_').replace(' ', '_')
366
  print(f"用户 {identifier_safe} - 准备批量保存 {len(list_of_log_entries)} 条选择日志 (批次标识: {batch_identifier})...")
367
  try:
 
373
  print("错误:DATASET_REPO_ID 未配置。无法批量保存选择日志。")
374
  return False
375
  api = HfApi(token=token)
 
376
  timestamp_str = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
377
  batch_filename = f"batch_user-{identifier_safe}_id-{batch_identifier}_{timestamp_str}_logs-{len(list_of_log_entries)}.jsonl"
378
  path_in_repo = f"{BATCH_LOG_FOLDER}/{identifier_safe}/{batch_filename}"
 
387
 
388
  if not jsonl_content.strip():
389
  print(f"用户 {identifier_safe} (批次 {batch_identifier}) 无可序列化选择日志。")
390
+ return True
391
 
392
  log_bytes = jsonl_content.encode('utf-8')
393
  file_like_object = io.BytesIO(log_bytes)
 
428
  user_ip_fallback = request.client.host if request else "unknown_ip"
429
  user_identifier_for_logging = output_s_user_session_id if output_s_user_session_id else user_ip_fallback
430
 
 
 
431
  len_ui_outputs = len(outputs_ui_components_definition)
432
 
433
  def create_ui_error_tuple(message, progress_msg_text, stop_experiment=False):
434
+ btn_start_interactive = not stop_experiment
435
  btn_choices_interactive = not stop_experiment
436
  return (gr.update(visible=False),) * 3 + \
437
  ("", "") + \
 
444
 
445
  if action_type == "record_choice":
446
  if output_s_current_trial_data.get("data") and output_s_current_trial_data["data"].get("left_internal_label"):
 
447
  chosen_internal_label = (output_s_current_trial_data["data"]["left_internal_label"] if choice_value == "left" else output_s_current_trial_data["data"]["right_internal_label"])
448
  parsed_chosen_method, parsed_chosen_subject, parsed_chosen_filename = "N/A", "N/A", "N/A"
449
  if chosen_internal_label == "目标图像": parsed_chosen_method, parsed_chosen_subject, parsed_chosen_filename = "TARGET", "GT", output_s_current_trial_data["data"]["image_id"]
 
465
  output_s_user_logs.append(log_entry)
466
  print(f"用户 {user_identifier_for_logging} 记录选择 (img: {log_entry['image_id']})。当前批次日志数: {len(output_s_user_logs)}")
467
 
 
468
  if len(output_s_user_logs) >= LOG_BATCH_SIZE:
469
  print(f"累积用户选择日志达到 {LOG_BATCH_SIZE} 条,准备批量保存...")
470
  batch_id_for_filename = f"run{output_s_run_no}_trialidx{output_s_trial_idx}_logcount{len(output_s_user_logs)}"
 
474
  output_s_user_logs = []
475
  else:
476
  print("严重错误:批量用户选择日志保存失败。实验无法继续。")
 
477
  error_message_ui = "错误:日志保存失败,可能是网络问题或API限流。实验已停止,请联系管理员。"
478
  progress_message_ui = f"用户ID: {user_id_display_text} | 实验因错误停止在第 {output_s_run_no} 轮,试验 {output_s_trial_idx+1}"
479
  error_ui_updates = create_ui_error_tuple(error_message_ui, progress_message_ui, stop_experiment=True)
480
  return output_s_trial_idx, output_s_run_no, output_s_user_logs, output_s_current_trial_data, \
481
  output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *error_ui_updates
482
 
 
483
  if global_history_has_unsaved_changes:
484
  print("检测到全局图片对历史自上次保存后有更新,将一并保存...")
485
  if not save_global_shown_pairs():
 
491
  output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *error_ui_updates
492
  else:
493
  print(f"用户 {user_identifier_for_logging} 错误:记录选择时当前试验数据为空或缺少internal_label!")
494
+ error_ui_updates = create_ui_error_tuple("记录选择时内部错误。", f"用户ID: {user_id_display_text} | 进度:{output_s_trial_idx}/{output_s_num_trials_this_run}", stop_experiment=False)
 
495
  return output_s_trial_idx, output_s_run_no, output_s_user_logs, output_s_current_trial_data, output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *error_ui_updates
496
 
 
 
497
  if action_type == "start_experiment":
498
  is_first = (output_s_num_trials_this_run == 0 and output_s_trial_idx == 0 and output_s_run_no == 1)
499
  is_completed_for_restart = (output_s_num_trials_this_run > 0 and output_s_trial_idx >= output_s_num_trials_this_run)
500
 
501
+ if is_completed_for_restart:
502
+ if output_s_user_logs:
503
  print(f"轮次 {output_s_run_no-1} 结束,尝试保存剩余的 {len(output_s_user_logs)} 条用户选择日志...")
504
  batch_id_for_filename = f"run{output_s_run_no-1}_final_logcount{len(output_s_user_logs)}"
505
  if not save_collected_logs_batch(list(output_s_user_logs), user_identifier_for_logging, batch_id_for_filename):
 
509
  error_ui_updates = create_ui_error_tuple(error_message_ui, progress_message_ui, stop_experiment=True)
510
  return output_s_trial_idx, output_s_run_no-1, output_s_user_logs, output_s_current_trial_data, \
511
  output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *error_ui_updates
512
+ output_s_user_logs = []
513
 
514
  if global_history_has_unsaved_changes:
515
  print("轮次结束,尝试保存全局图片对历史...")
 
521
  return output_s_trial_idx, output_s_run_no-1, output_s_user_logs, output_s_current_trial_data, \
522
  output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *error_ui_updates
523
 
 
524
  if is_first or is_completed_for_restart:
525
  if is_completed_for_restart: output_s_run_no += 1
526
  available_master_images = [img for img in master_image_list if img not in exhausted_target_images]
527
  print(f"开始轮次 {output_s_run_no}: 从 {len(master_image_list)}个总目标图片中筛选,可用图片 {len(available_master_images)}个 (已排除 {len(exhausted_target_images)}个已耗尽图片).")
528
  if not available_master_images:
 
529
  msg = "所有目标图片的所有唯一图片对均已展示完毕!感谢您的参与。"
530
  prog_text = f"用户ID: {user_id_display_text} | 实验完成!"
 
531
  if output_s_user_logs:
532
  print(f"最终轮次结束,尝试保存剩余的 {len(output_s_user_logs)} 条用户选择日志...")
533
+ batch_id_for_filename = f"run{output_s_run_no-1}_final_logcount{len(output_s_user_logs)}"
534
+ save_collected_logs_batch(list(output_s_user_logs), user_identifier_for_logging, batch_id_for_filename)
535
  output_s_user_logs = []
536
  if global_history_has_unsaved_changes:
537
  print("实验最终结束,尝试保存全局图片对历史...")
538
+ save_global_shown_pairs()
539
 
540
+ ui_updates = list(create_ui_error_tuple(msg, prog_text, stop_experiment=True))
541
  return 0, output_s_run_no, [], {}, output_s_user_session_id, [], 0, *tuple(ui_updates)
542
 
 
543
  if REPEAT_SINGLE_TARGET_FOR_TESTING and available_master_images:
544
  print(f"测试模式 (重复单一目标图) 已激活。")
545
  single_image_to_repeat = available_master_images[0]
 
551
  current_run_max_trials = NUM_TRIALS_PER_RUN
552
  run_size = min(num_really_avail, current_run_max_trials)
553
  if run_size == 0:
554
+ error_ui = create_ui_error_tuple("错误: 可用图片采样数为0!", f"用户ID: {user_id_display_text} | 进度: 0/0", stop_experiment=False)
555
  return 0, output_s_run_no, output_s_user_logs, {}, output_s_user_session_id, [], 0, *error_ui
556
  output_s_current_run_image_list = random.sample(available_master_images, run_size)
557
  output_s_num_trials_this_run = run_size
558
 
559
  output_s_trial_idx = 0
560
+ output_s_current_trial_data = {}
561
+ if is_first:
 
 
562
  timestamp_str = datetime.now().strftime('%Y%m%d%H%M%S%f'); random_val = random.randint(10000, 99999)
563
+ if not output_s_user_session_id:
564
  output_s_user_session_id = f"user_{timestamp_str}_{random_val}"; user_identifier_for_logging = output_s_user_session_id
565
  else:
566
+ user_identifier_for_logging = output_s_user_session_id
567
  print(f"用户会话ID: {output_s_user_session_id}")
568
  print(f"开始/继续轮次 {output_s_run_no} (用户ID: {output_s_user_session_id}). 本轮共 {output_s_num_trials_this_run} 个试验。")
569
+ else:
570
  print(f"用户 {user_identifier_for_logging} 在第 {output_s_run_no} 轮,试验 {output_s_trial_idx} 点击开始,但轮次未完成。忽略。")
571
  no_change_ui = create_no_change_tuple()
572
  return output_s_trial_idx, output_s_run_no, output_s_user_logs, output_s_current_trial_data, output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *no_change_ui
573
 
 
 
 
 
 
574
  current_actual_trial_index_for_get_next = output_s_trial_idx
575
+
 
576
  if current_actual_trial_index_for_get_next >= output_s_num_trials_this_run and output_s_num_trials_this_run > 0:
 
 
 
 
 
577
  prog_text = f"用户ID: {output_s_user_session_id} | 进度:{output_s_num_trials_this_run}/{output_s_num_trials_this_run} | 第 {output_s_run_no} 轮 🎉"
578
+ ui_updates = list(create_ui_error_tuple(f"🎉 第 {output_s_run_no} 轮完成!请点击“开始试验 / 下一轮”继续。", prog_text, stop_experiment=False))
579
  ui_updates[7]=gr.update(interactive=True); ui_updates[8]=gr.update(interactive=False); ui_updates[9]=gr.update(interactive=False)
580
  ui_updates[0]=gr.update(value=None,visible=False); ui_updates[1]=gr.update(value=None,visible=False); ui_updates[2]=gr.update(value=None,visible=False)
581
  yield output_s_trial_idx, output_s_run_no, output_s_user_logs, {"data": None}, output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *ui_updates; return
 
600
 
601
  if _trial_info_candidate is not None:
602
  trial_info = _trial_info_candidate
603
+ output_s_trial_idx = _returned_next_idx
604
  break
605
  else:
606
  print(f"信息:目标图 '{current_target_image_for_trial}' 无法生成有效试验。尝试列表中的下一个。")
607
  next_s_trial_idx_for_state_loop +=1
608
  output_s_trial_idx = next_s_trial_idx_for_state_loop
 
609
 
610
  if trial_info is None:
611
  print(f"轮次 {output_s_run_no} 中没有更多可用的有效试验了。结束本轮。")
 
612
  if output_s_user_logs:
613
  print(f"轮次 {output_s_run_no} 无更多有效试验,尝试保存剩余 {len(output_s_user_logs)} 条日志...")
614
  batch_id_for_filename = f"run{output_s_run_no}_no_more_trials_logcount{len(output_s_user_logs)}"
615
  if not save_collected_logs_batch(list(output_s_user_logs), user_identifier_for_logging, batch_id_for_filename):
616
+ print("严重错误:保存剩余日志失败。实验可能需要停止。")
 
 
617
  output_s_user_logs = []
618
  if global_history_has_unsaved_changes:
619
  print("轮次无更多有效试验,尝试保存全局图片对历史...")
 
639
 
640
  ui_show_candidates_updates = list(create_no_change_tuple())
641
  ui_show_candidates_updates[0]=gr.update(value=None,visible=False); ui_show_candidates_updates[1]=gr.update(value=trial_info["left_path"],visible=True); ui_show_candidates_updates[2]=gr.update(value=trial_info["right_path"],visible=True)
642
+ ui_show_candidates_updates[3]=gr.update(value=trial_info["left_display_label"], visible=True); ui_show_candidates_updates[4]=gr.update(value=trial_info["right_display_label"], visible=True)
643
  ui_show_candidates_updates[5]="请选择更像原图的一张"; ui_show_candidates_updates[6]=prog_text
644
  ui_show_candidates_updates[7]=gr.update(interactive=False); ui_show_candidates_updates[8]=gr.update(interactive=True); ui_show_candidates_updates[9]=gr.update(interactive=True)
645
  yield output_s_trial_idx, output_s_run_no, output_s_user_logs, output_s_current_trial_data, output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *ui_show_candidates_updates
646
 
647
+ # ==== Gradio UI 定义 和 程序入口 ====
648
+ def handle_download_history_file():
 
 
 
 
649
  global GLOBAL_HISTORY_FILE
650
  if os.path.exists(GLOBAL_HISTORY_FILE):
651
  try:
 
723
  with experiment_container:
724
  gr.Markdown("## 🧠 图像重建主观评估实验"); gr.Markdown(f"每轮实验大约有 {NUM_TRIALS_PER_RUN} 次比较。")
725
  with gr.Row():
726
+ with gr.Column(scale=1, min_width=300): left_img = gr.Image(label="左候选图", visible=False, height=400, interactive=False); left_lbl = gr.Textbox(label="左图信息", value="", visible=True, interactive=False, max_lines=1); btn_left = gr.Button("选择左图 (更相似)", interactive=False, elem_classes="compact_button")
727
+ with gr.Column(scale=1, min_width=300): right_img = gr.Image(label="右候选图", visible=False, height=400, interactive=False); right_lbl = gr.Textbox(label="右图信息",value="", visible=True, interactive=False, max_lines=1); btn_right = gr.Button("选择右图 (更相似)", interactive=False, elem_classes="compact_button")
728
  with gr.Row(): target_img = gr.Image(label="目标图像 (观察3秒后消失)", visible=False, height=400, interactive=False)
729
  with gr.Row(): status_text = gr.Markdown(value="请点击“开始试验 / 下一轮”按钮。")
730
+ with gr.Row(): progress_text = gr.Markdown()
731
  with gr.Row():
732
  btn_start = gr.Button("开始试验 / 下一轮")
733
  btn_download_json = gr.Button("下载JSON历史记录")
734
  json_download_output = gr.File(label="下载的文件会在此处提供", interactive=False)
735
+ file_out_placeholder = gr.File(label=" ", visible=False, interactive=False)
736
 
737
  outputs_ui_components_definition = [
738
  target_img, left_img, right_img, left_lbl, right_lbl, status_text, progress_text,
739
  btn_start, btn_left, btn_right, file_out_placeholder
740
+ ]
741
  click_inputs_base = [
742
  s_trial_index, s_run_no, s_user_logs, s_current_trial_data, s_user_session_id,
743
  s_current_run_image_list, s_num_trials_this_run
 
753
  btn_right.click(fn=partial(process_experiment_step, action_type="record_choice", choice_value="right"), inputs=click_inputs_base, outputs=event_outputs, queue=True)
754
  btn_download_json.click(fn=handle_download_history_file, inputs=None, outputs=[json_download_output, status_text])
755
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
756
  if __name__ == "__main__":
 
757
  if not master_image_list: print("\n关键错误:程序无法启动,因无目标图片。"); exit()
758
  else:
759
  print(f"从 '{TARGET_DIR}' 加载 {len(master_image_list)} 张目标���片。")
760
  if not METHOD_ROOTS: print(f"警告: '{BASE_IMAGE_DIR}' 无候选方法子目录。")
761
  if not SUBJECTS: print("警告: SUBJECTS 列表为空。")
762
+ print(f"用户选择日志保存到 Dataset: '{DATASET_REPO_ID}' 的 '{BATCH_LOG_FOLDER}/ 文件夹")
763
  if not os.getenv("HF_TOKEN"): print("警告: HF_TOKEN 未设置。日志无法保存到Hugging Face Dataset。\n 请在 Space Secrets 中设置 HF_TOKEN。")
764
  else: print("HF_TOKEN 已找到。")
765
  print(f"全局图片对历史将从 '{GLOBAL_HISTORY_FILE}' 加载/保存到此文件。")
 
783
  except Exception as e_mkdir_main:
784
  print(f"错误:在 main 中创建目录 '{PERSISTENT_STORAGE_BASE}' 失败: {e_mkdir_main}")
785
 
786
+ final_allowed_paths = list(set(allowed_paths_list))
787
  if final_allowed_paths:
788
+ print(f"Gradio demo.launch() 配置最终 allowed_paths: {final_allowed_paths}")
789
  else:
790
  print("警告:没有有效的 allowed_paths 被配置。Gradio文件访问可能受限。")
791