YanmHa commited on
Commit
86105cd
·
verified ·
1 Parent(s): fe05cd2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +194 -198
app.py CHANGED
@@ -7,7 +7,7 @@ from datetime import datetime
7
  from functools import partial
8
  import json
9
  import io
10
- from huggingface_hub import HfApi # HfFolder 未使用
11
  from huggingface_hub.hf_api import HfHubHTTPError
12
  import traceback
13
 
@@ -31,13 +31,13 @@ if os.path.exists(BASE_IMAGE_DIR):
31
  except Exception as e: print(f"错误:在扫描 '{BASE_IMAGE_DIR}' 时发生错误: {e}"); METHOD_ROOTS = []
32
  else: print(f"警告:基础目录 '{BASE_IMAGE_DIR}' 不存在。将无法加载候选图片。")
33
 
34
- SUBJECTS = ["subj01", "subj02", "subj05", "subj07"]
35
  SENTINEL_TRIAL_INTERVAL = 20
36
  NUM_TRIALS_PER_RUN = 100
37
 
38
  DATASET_REPO_ID = "YanmHa/image-aligned-experiment-data"
39
- INDIVIDUAL_LOGS_FOLDER = "individual_choice_logs"
40
- BATCH_LOG_FOLDER = "run_logs_batch"
41
  CSS = ".gr-block {margin-top: 4px !important; margin-bottom: 4px !important;} .compact_button { padding: 4px 8px; min-width: auto; }"
42
 
43
  # ==== 加载所有可用的目标图片 ====
@@ -57,7 +57,7 @@ if not master_image_list: print(f"关键错误:由于 '{TARGET_DIR}' 问题,
57
 
58
  # ==== 辅助函数 ====
59
  def get_next_trial_info(current_trial_idx_in_run, current_run_image_list_for_trial, num_trials_in_this_run_for_trial):
60
- # ... (此函数与上一版几乎一致,确保 left_display_label 和 right_display_label 是匿名的) ...
61
  global TARGET_DIR, METHOD_ROOTS, SUBJECTS, SENTINEL_TRIAL_INTERVAL
62
  if not current_run_image_list_for_trial or current_trial_idx_in_run >= num_trials_in_this_run_for_trial: return None, current_trial_idx_in_run
63
  img_filename_original = current_run_image_list_for_trial[current_trial_idx_in_run]
@@ -81,8 +81,6 @@ def get_next_trial_info(current_trial_idx_in_run, current_run_image_list_for_tri
81
  trial_info["is_sentinel"] = True
82
  sentinel_candidate_target_tuple = ("目标图像", target_full_path)
83
  random_reconstruction_candidate_tuple = random.choice(pool)
84
- # candidates_for_sentinel 的每个元素是 ((display_label, path_for_display), (internal_label, path_for_internal))
85
- # 但为了简化,display_label 可以简单,internal_label 存详细信息
86
  candidates_for_sentinel = [
87
  (("目标图像", target_full_path), sentinel_candidate_target_tuple[0]),
88
  (("重建图", random_reconstruction_candidate_tuple[1]), random_reconstruction_candidate_tuple[0])
@@ -102,38 +100,79 @@ def get_next_trial_info(current_trial_idx_in_run, current_run_image_list_for_tri
102
  })
103
  return trial_info, current_trial_idx_in_run + 1
104
 
105
- def save_single_log_to_hf_dataset(log_entry, user_identifier_str):
106
- # ... (此函数与您上一版代码完全一致) ...
107
- global DATASET_REPO_ID, INDIVIDUAL_LOGS_FOLDER
108
- if not isinstance(log_entry, dict): print(f"错误:单个日志条目不是字典格式,无法保存:{log_entry}"); return
109
- current_user_id = user_identifier_str if user_identifier_str else "unknown_user_session"
110
- identifier_safe = str(current_user_id).replace('.', '_').replace(':', '_').replace('/', '_').replace(' ', '_') # 添加替换空格
111
- print(f"用户 {identifier_safe} - 保存日志 for image {log_entry.get('image_id', 'Unknown')}...")
 
 
 
 
 
 
 
112
  try:
113
  token = os.getenv("HF_TOKEN")
114
- if not token: print("错误:HF_TOKEN 未设置。无法保存日志。"); return
115
- if not DATASET_REPO_ID: print("错误:DATASET_REPO_ID 未配置。"); return
 
 
 
 
 
116
  api = HfApi(token=token)
117
- image_id_safe = os.path.splitext(log_entry.get("image_id", "unk_img"))[0].replace('.', '_').replace(':', '_').replace('/', '_')
118
- ts = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
119
- fname = f"run{log_entry.get('run_no','X')}_trial{log_entry.get('trial_sequence_in_run','Y')}_img{image_id_safe}_{ts}.json"
120
- path_in_repo = f"{INDIVIDUAL_LOGS_FOLDER}/{identifier_safe}/{fname}"
121
- json_content = json.dumps(log_entry, ensure_ascii=False, indent=2)
122
- log_bytes = json_content.encode('utf-8')
123
- f_obj = io.BytesIO(log_bytes)
124
- print(f"准备上传: {path_in_repo} ({len(log_bytes)} bytes)")
125
- api.upload_file(path_or_fileobj=f_obj,path_in_repo=path_in_repo,repo_id=DATASET_REPO_ID,repo_type="dataset",
126
- commit_message=f"Log: img {log_entry.get('image_id','N/A')}, run {log_entry.get('run_no','N/A')} by {identifier_safe}")
127
- print(f"日志成功保存到: {DATASET_REPO_ID}/{path_in_repo}")
128
- except Exception as e: print(f"保存日志失败 (img {log_entry.get('image_id','Unk')}, user {identifier_safe}): {e}"); traceback.print_exc()
 
 
 
 
 
 
 
 
 
 
 
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  def process_experiment_step(
131
  s_trial_idx_val, s_run_no_val, s_user_logs_val, s_current_trial_data_val, s_user_session_id_val,
132
  s_current_run_image_list_val, s_num_trials_this_run_val,
133
- action_type=None, choice_value=None, request: gr.Request = None):
134
- # ... (此函数与上一版几乎一致,确保所有yield/return都返回正确数量的值) ...
135
- # ... (并且 chosen_raw_label 现在使用 chosen_internal_label, left/right_method_subject_filename 使用 internal_label) ...
136
- global master_image_list, NUM_TRIALS_PER_RUN, outputs_ui_components_definition
137
  output_s_trial_idx = s_trial_idx_val; output_s_run_no = s_run_no_val
138
  output_s_user_logs = list(s_user_logs_val); output_s_current_trial_data = dict(s_current_trial_data_val) if s_current_trial_data_val else {}
139
  output_s_user_session_id = s_user_session_id_val; output_s_current_run_image_list = list(s_current_run_image_list_val)
@@ -155,42 +194,91 @@ def process_experiment_step(
155
  if len(parts) == 3: parsed_chosen_method, parsed_chosen_subject, parsed_chosen_filename = parts[0].strip(), parts[1].strip(), parts[2].strip()
156
  elif len(parts) == 2: parsed_chosen_method, parsed_chosen_subject = parts[0].strip(), parts[1].strip()
157
  elif len(parts) == 1: parsed_chosen_method = parts[0].strip()
158
- log_entry = {"timestamp": datetime.now().isoformat(), "user_identifier": user_identifier_for_logging, "run_no": output_s_run_no, "image_id": output_s_current_trial_data["data"]["image_id"],
159
- "left_internal_label": output_s_current_trial_data["data"]["left_internal_label"], "right_internal_label": output_s_current_trial_data["data"]["right_internal_label"],
160
- "chosen_side": choice_value, "chosen_internal_label": chosen_internal_label, "chosen_method": parsed_chosen_method, "chosen_subject": parsed_chosen_subject, "chosen_filename": parsed_chosen_filename,
161
- "trial_sequence_in_run": output_s_current_trial_data["data"]["cur_no"], "is_sentinel": output_s_current_trial_data["data"]["is_sentinel"]}
162
- output_s_user_logs.append(log_entry)
163
- print(f"用户 {user_identifier_for_logging} 记录选择...准备立即保存...")
164
- save_single_log_to_hf_dataset(log_entry, user_identifier_for_logging)
 
 
 
 
 
 
165
  else:
 
166
  print(f"用户 {user_identifier_for_logging} 错误:记录选择时数据为空!")
167
  error_ui_updates = create_ui_error_tuple("记录选择时内部错误。", f"用户ID: {user_id_display_text} | 进度:{output_s_trial_idx}/{output_s_num_trials_this_run}")
168
  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
 
169
  if action_type == "start_experiment":
170
  is_first = (output_s_num_trials_this_run == 0 and output_s_trial_idx == 0 and output_s_run_no == 1)
171
- is_completed = (output_s_num_trials_this_run > 0 and output_s_trial_idx >= output_s_num_trials_this_run)
172
- if is_first or is_completed:
173
- if not master_image_list: error_ui = create_ui_error_tuple("错误: 无可用目标图片!", f"用户ID: {user_id_display_text} | 进度: 0/0"); return 0, output_s_run_no, [], {}, output_s_user_session_id, [], 0, *error_ui
174
- if is_completed: output_s_run_no += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  num_avail = len(master_image_list); run_size = min(num_avail, NUM_TRIALS_PER_RUN)
176
- if run_size == 0: error_ui = create_ui_error_tuple("错误: 采样图片数为0!", f"用户ID: {user_id_display_text} | 进度: 0/0"); return 0, output_s_run_no, [], {}, output_s_user_session_id, [], 0, *error_ui
 
177
  output_s_current_run_image_list = random.sample(master_image_list, run_size)
178
- output_s_num_trials_this_run = run_size; output_s_trial_idx = 0; output_s_user_logs = []; output_s_current_trial_data = {}
179
- # 新的 user_session_id (即 participant_id) 在欢迎页已收集并传入 s_user_session_id_val
180
- # 这里不再重新生成基于时间戳的 output_s_user_session_id,而是使用已有的。
181
- # user_identifier_for_logging 已经使用了 output_s_user_session_id
 
 
 
 
 
182
  print(f"开始/继续轮次 {output_s_run_no} (用户ID: {output_s_user_session_id}). 随机选择 {output_s_num_trials_this_run} 张图片.")
183
- else:
 
184
  print(f"用户 {user_identifier_for_logging} 在第 {output_s_run_no} 轮,试验 {output_s_trial_idx} 点击开始,但轮次未完成。忽略。")
185
  no_change_ui = create_no_change_tuple()
186
  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
 
 
187
  if output_s_trial_idx >= output_s_num_trials_this_run and output_s_num_trials_this_run > 0:
188
- if output_s_user_logs: print(f"用户 {output_s_user_session_id} 完成第 {output_s_run_no} 轮。")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  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} 轮 🎉"
190
- ui_updates = list(create_ui_error_tuple(f"🎉 第 {output_s_run_no} 轮完成!请点击“开始试验 / 下一轮”。", prog_text))
191
  ui_updates[7]=gr.update(interactive=True); ui_updates[8]=gr.update(interactive=False); ui_updates[9]=gr.update(interactive=False)
192
  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)
193
  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_updates; return
 
 
 
194
  if not output_s_current_run_image_list or output_s_num_trials_this_run == 0:
195
  error_ui = create_ui_error_tuple("错误: 无法加载试验图片 (列表为空)", f"用户ID: {user_id_display_text} | 进度: N/A")
196
  return output_s_trial_idx, output_s_run_no, output_s_user_logs, {"data": None}, output_s_user_session_id, [], 0, *error_ui
@@ -209,20 +297,17 @@ def process_experiment_step(
209
  time.sleep(3)
210
  ui_show_candidates_updates = list(create_no_change_tuple())
211
  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)
212
- ui_show_candidates_updates[3]=gr.update(value=trial_info["left_display_label"], visible=False) # 隐藏标签
213
- ui_show_candidates_updates[4]=gr.update(value=trial_info["right_display_label"], visible=False) # 隐藏标签
214
  ui_show_candidates_updates[5]="请选择更像原图的一张"; ui_show_candidates_updates[6]=prog_text
215
  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)
216
  yield next_s_trial_idx_for_state, 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
217
 
218
- # ==== Gradio UI 定义 ====
219
  welcome_page_markdown = """
220
  ## 欢迎加入实验!
221
  您好!非常感谢您抽出宝贵时间参与我们的视觉偏好评估实验。您的选择将帮助我们改进重建算法,让机器生成的图像更贴近人类视觉体验!
222
-
223
  1. **实验目的**
224
  通过比较两幅 重建图像 与原始 目标图像 的相似度。
225
-
226
  2. **操作流程**
227
  * 点击下方的「我已阅读并同意开始实验」按钮。
228
  * 然后点击主实验界面的「开始试验 / 下一轮」按钮。
@@ -230,212 +315,123 @@ welcome_page_markdown = """
230
  * 随后自动切换到 **两张重建图像**。
231
  * 根据刚才的观察记忆,选出您认为与目标图像最相似的一张。
232
  * 选择后系统会自动进入下一轮比较。
233
-
234
  3. **温馨提示**
235
  * 请勿刷新或关闭页面,以免中断实验。
236
  * 若图片加载稍有延迟,请耐心等待;持续异常可联系邮箱 yangminghan@bupt.edu.cn。
237
  * 本实验将保护您的任何个人隐私信息,所有数据仅用于学术研究,请您认真选择和填写。
238
-
239
  4. **奖励说明**
240
  * 完成全部轮次后,请截图记录您所完成的实验总数(可累积,页面左下角将显示进度,请保证截取到为您分配的ID,轮次)。
241
  * 将截图发送至邮箱 yangminghan@bupt.edu.cn,我们将在核验后发放奖励。
242
-
243
  再次感谢您的参与与支持!您每一次认真选择都对我们的研究意义重大。祝您一切顺利,实验愉快!
244
  """
245
 
246
- # 新增:处理同意并开始实验的函数
247
  def handle_agree_and_start(name, gender, age, education, request: gr.Request):
248
- # 简单验证
249
  error_messages_list = []
250
- if not name or str(name).strip() == "":
251
- error_messages_list.append("姓名 不能为空。")
252
-
253
- # 对于 Radio Dropdown,如果初始没有值且用户未选择,它们可能返回 None
254
- if gender is None or str(gender).strip() == "": # Radio 可能返回None或空字符串
255
- error_messages_list.append("性别 必须选择。")
256
-
257
- # 对于 gr.Number,如果未填写,它可能是 None。age=0 可能也是无效的,取决于您的逻辑
258
- if age is None: # 或者 age == 0 如果0是无效年龄
259
- error_messages_list.append("年龄 不能为空或为0。")
260
- elif not (isinstance(age, (int, float)) and 1 <= age <= 120): # 假设年龄范围1-120
261
- try: # 尝试转换以处理从UI传来的可能是字符串的数字
262
- num_age = float(age)
263
- if not (1 <= num_age <= 120):
264
- error_messages_list.append("年龄必须在 1 到 120 之间。")
265
- except (ValueError, TypeError):
266
- error_messages_list.append("年龄必须是一个有效的数字。")
267
-
268
- if education is None or str(education).strip() == "其他": # Dropdown 可能返回None或空字符串
269
- error_messages_list.append("学历 必须选择。")
270
 
271
  if error_messages_list:
272
  full_error_message = "请修正以下错误:\n" + "\n".join([f"- {msg}" for msg in error_messages_list])
273
  print(f"用户输入验证失败: {full_error_message}")
274
- # 返回更新,以显示错误并保持在欢迎页面
275
- # 对应 outputs: [s_user_session_id, s_show_experiment_ui, welcome_container, experiment_container, welcome_error_msg]
276
- return (
277
- gr.update(), # s_user_session_id (不改变)
278
- False, # s_show_experiment_ui (保持为False,显示欢迎页)
279
- gr.update(visible=True), # welcome_container (保持可见)
280
- gr.update(visible=False), # experiment_container (保持隐藏)
281
- full_error_message # welcome_error_msg (显示错误信息)
282
- )
283
 
284
- # 清理和连接信息作为用户ID
285
  s_name = str(name).strip().replace(" ","_").replace("/","_").replace("\\","_")
286
  s_gender = str(gender).strip().replace(" ","_").replace("/","_").replace("\\","_")
287
- s_age = str(int(float(age))) # 确保是整数再转字符串
288
  s_education = str(education).strip().replace(" ","_").replace("/","_").replace("\\","_")
289
-
290
  user_id_str = f"N-{s_name}_G-{s_gender}_A-{s_age}_E-{s_education}"
291
-
292
  print(f"用户信息收集完毕,生成用户ID: {user_id_str}")
293
-
294
- # 成功,切换界面并清空错误信息
295
  return user_id_str, True, gr.update(visible=False), gr.update(visible=True), ""
296
 
297
-
298
  with gr.Blocks(css=CSS, title="图像重建主观评估") as demo:
299
- # --- 所有 State 变量首先定义 ---
300
  s_show_experiment_ui = gr.State(False)
301
- s_trial_index = gr.State(0)
302
- s_run_no = gr.State(1)
303
- s_user_logs = gr.State([])
304
- s_current_trial_data = gr.State({})
305
- s_user_session_id = gr.State(None) # 将存储由用户信息构成的ID
306
- s_current_run_image_list = gr.State([])
307
- s_num_trials_this_run = gr.State(0)
308
 
309
- # --- UI 容器 ---
310
  welcome_container = gr.Column(visible=True)
311
  experiment_container = gr.Column(visible=False)
312
 
313
- # --- 欢迎页面 UI ---
314
  with welcome_container:
315
- gr.Markdown(welcome_page_markdown) # welcome_page_markdown 在全局定义
316
  with gr.Row():
317
- user_name_input = gr.Textbox(label="请输入您的姓名", placeholder="例如:张三 ")
318
- user_gender_input = gr.Radio(label="性别", choices=["男", "女"])
319
  with gr.Row():
320
- user_age_input = gr.Number(label="年龄 (请输入整数)", minimum=1, maximum=120, step=1)
321
- user_education_input = gr.Dropdown(
322
- label="学历",
323
- choices=["其他","初中及以下","高中(含在读)", "大专(含在读)", "本科(含在读)", "硕士(含在读)", "博士(含在读)"]
324
- )
325
- welcome_error_msg = gr.Markdown(value="") # 用于显示欢迎页的错误信息
326
  btn_agree_and_start = gr.Button("我已阅读上述说明并同意参与实验")
327
 
328
- # --- 实验主界面 UI ---
329
  with experiment_container:
330
  gr.Markdown("## 🧠 图像重建主观评估实验")
331
- gr.Markdown(
332
- f"每轮实验大约有 {NUM_TRIALS_PER_RUN} 次比较。" # 移除了原来对流程的详细描述,因为已在欢迎页说明
333
- )
334
- # 候选图片区域的新布局
335
  with gr.Row():
336
- with gr.Column(scale=1, min_width=300): # 左边列
337
  left_img = gr.Image(label="左候选图", visible=False, height=400, interactive=False)
338
- # left_lbl Textbox 现在将被永久隐藏,但保留以匹配输出数量
339
  left_lbl = gr.Textbox(label="左图信息", visible=False, interactive=False, max_lines=1)
340
  btn_left = gr.Button("选择左图 (更相似)", interactive=False, elem_classes="compact_button")
341
-
342
- with gr.Column(scale=1, min_width=300): # 右边列
343
  right_img = gr.Image(label="右候选图", visible=False, height=400, interactive=False)
344
- # right_lbl Textbox 现在将被永久隐藏
345
  right_lbl = gr.Textbox(label="右图信息", visible=False, interactive=False, max_lines=1)
346
  btn_right = gr.Button("选择右图 (更相似)", interactive=False, elem_classes="compact_button")
347
-
348
- # 目标图像单独一行,跨列显示或居中
349
- with gr.Row():
350
- target_img = gr.Image(label="目标图像 (观察3秒后消失)", visible=False, height=400, interactive=False)
351
-
352
- # 状态和进度文本
353
- with gr.Row():
354
- status_text = gr.Markdown(value="请点击“开始试验 / 下一轮”按钮。")
355
- with gr.Row():
356
- progress_text = gr.Markdown() # 用于显示 用户ID | 进度 | 轮次
357
-
358
- # 开始按钮单独一行
359
- with gr.Row():
360
- btn_start = gr.Button("开始试验 / 下一轮")
361
-
362
- # 隐藏的 File 组件,用于匹配输出数量 (如果仍需要)
363
  file_out_placeholder = gr.File(label=" ", visible=False, interactive=False)
364
 
365
-
366
- # --- 定义UI组件列表,用于简化事件处理函数的输出 ---
367
  outputs_ui_components_definition = [
368
- target_img, left_img, right_img,
369
- left_lbl, right_lbl, # 即使隐藏,也包含在列表以维持原函数的输出结构
370
- status_text, progress_text,
371
- btn_start, btn_left, btn_right,
372
- file_out_placeholder
373
  ]
374
-
375
  click_inputs_base = [
376
  s_trial_index, s_run_no, s_user_logs, s_current_trial_data, s_user_session_id,
377
  s_current_run_image_list, s_num_trials_this_run
378
  ]
379
  event_outputs = [
380
  s_trial_index, s_run_no, s_user_logs, s_current_trial_data, s_user_session_id,
381
- s_current_run_image_list, s_num_trials_this_run,
382
- *outputs_ui_components_definition
383
  ]
384
 
385
- # --- 事件绑定 ---
386
  btn_agree_and_start.click(
387
- fn=handle_agree_and_start, # 新的处理函数
388
- inputs=[user_name_input, user_gender_input, user_age_input, user_education_input], # 添加 request
389
- outputs=[
390
- s_user_session_id, # 更新用户ID状态
391
- s_show_experiment_ui, # 更新控制界面可见性的状态
392
- welcome_container, # 更新欢迎页容器的可见性
393
- experiment_container, # 更新实验页容器的可见性
394
- welcome_error_msg # 用于显示欢迎页的验证错误信息
395
- ]
396
- )
397
-
398
- btn_start.click(
399
- fn=partial(process_experiment_step, action_type="start_experiment"),
400
- inputs=click_inputs_base, # 移除了 gr.Request()
401
- outputs=event_outputs,
402
- queue=True # 保持队列
403
- )
404
- btn_left.click(
405
- fn=partial(process_experiment_step, action_type="record_choice", choice_value="left"),
406
- inputs=click_inputs_base,
407
- outputs=event_outputs,
408
- queue=True
409
- )
410
- btn_right.click(
411
- fn=partial(process_experiment_step, action_type="record_choice", choice_value="right"),
412
- inputs=click_inputs_base,
413
- outputs=event_outputs,
414
- queue=True
415
  )
 
 
 
416
 
417
- # ==== 程序入口 ====
418
  if __name__ == "__main__":
419
- # ... (与上一版相同的启动检查和打印逻辑) ...
420
- if not master_image_list:
421
- print("\n关键错误:程序无法启动,因为 TARGET_DIR 中没有找到图片。"); exit()
422
  else:
423
- print(f"从 '{TARGET_DIR}' 加载了 {len(master_image_list)} 张可用的目标图片。每轮实验将尝试选取 {NUM_TRIALS_PER_RUN} 张。")
424
- if not METHOD_ROOTS: print(f"警告: '{BASE_IMAGE_DIR}' 目录下没有找到候选方法子目录 (除了 '{TARGET_DIR_BASENAME}')。")
425
- else: print(f"已识别的方法根目录: {METHOD_ROOTS}")
426
- if not SUBJECTS: print("警告: SUBJECTS 列表为空。将无法从子目录加载候选图片。")
427
- else: print(f"实验将针对以下 subjects (子目录名): {SUBJECTS}")
428
- print(f"日志将尝试保存到 Hugging Face Dataset: '{DATASET_REPO_ID}'")
429
- if BATCH_LOG_FOLDER: print(f" - 批量运行日志 (如果启用): '{BATCH_LOG_FOLDER}/'")
430
- if INDIVIDUAL_LOGS_FOLDER: print(f" - 单个选择日志: '{INDIVIDUAL_LOGS_FOLDER}/'")
431
- if not os.getenv("HF_TOKEN"): print("警告: 环境变量 HF_TOKEN 未设置。日志将无法保存到 Hugging Face Dataset。\n 请在 Space Secrets 中设置 HF_TOKEN。")
432
- else: print("环境变量 HF_TOKEN 已找到。")
433
  path_to_allow_serving_from = BASE_IMAGE_DIR
434
  allowed_paths_list = []
435
  if os.path.exists(path_to_allow_serving_from) and os.path.isdir(path_to_allow_serving_from):
436
  allowed_paths_list.append(os.path.abspath(path_to_allow_serving_from))
437
- print(f"Gradio `demo.launch()` 将配置 allowed_paths: {allowed_paths_list}")
438
- else: print(f"关键警告:配置的图片基础目录 '{path_to_allow_serving_from}' ({os.path.abspath(path_to_allow_serving_from) if path_to_allow_serving_from else 'N/A'}) 不存在或不是一个目录。")
439
- print("启动 Gradio 应用程序...")
440
  if allowed_paths_list: demo.launch(allowed_paths=allowed_paths_list)
441
  else: demo.launch()
 
7
  from functools import partial
8
  import json
9
  import io
10
+ from huggingface_hub import HfApi
11
  from huggingface_hub.hf_api import HfHubHTTPError
12
  import traceback
13
 
 
31
  except Exception as e: print(f"错误:在扫描 '{BASE_IMAGE_DIR}' 时发生错误: {e}"); METHOD_ROOTS = []
32
  else: print(f"警告:基础目录 '{BASE_IMAGE_DIR}' 不存在。将无法加载候选图片。")
33
 
34
+ SUBJECTS = ["subj01", "subj02", "subj05", "subj07"]
35
  SENTINEL_TRIAL_INTERVAL = 20
36
  NUM_TRIALS_PER_RUN = 100
37
 
38
  DATASET_REPO_ID = "YanmHa/image-aligned-experiment-data"
39
+ INDIVIDUAL_LOGS_FOLDER = "individual_choice_logs" # 单个日志文件夹(如果将来恢复单条保存,此名称仍可用)
40
+ BATCH_LOG_FOLDER = "run_logs_batch" # 用于批量保存的文件夹
41
  CSS = ".gr-block {margin-top: 4px !important; margin-bottom: 4px !important;} .compact_button { padding: 4px 8px; min-width: auto; }"
42
 
43
  # ==== 加载所有可用的目标图片 ====
 
57
 
58
  # ==== 辅助函数 ====
59
  def get_next_trial_info(current_trial_idx_in_run, current_run_image_list_for_trial, num_trials_in_this_run_for_trial):
60
+ # ... (此函数与您上一版代码完全一致, 返回匿名显示标签和内部详细标签) ...
61
  global TARGET_DIR, METHOD_ROOTS, SUBJECTS, SENTINEL_TRIAL_INTERVAL
62
  if not current_run_image_list_for_trial or current_trial_idx_in_run >= num_trials_in_this_run_for_trial: return None, current_trial_idx_in_run
63
  img_filename_original = current_run_image_list_for_trial[current_trial_idx_in_run]
 
81
  trial_info["is_sentinel"] = True
82
  sentinel_candidate_target_tuple = ("目标图像", target_full_path)
83
  random_reconstruction_candidate_tuple = random.choice(pool)
 
 
84
  candidates_for_sentinel = [
85
  (("目标图像", target_full_path), sentinel_candidate_target_tuple[0]),
86
  (("重建图", random_reconstruction_candidate_tuple[1]), random_reconstruction_candidate_tuple[0])
 
100
  })
101
  return trial_info, current_trial_idx_in_run + 1
102
 
103
+ # --- ( save_single_log_to_hf_dataset 函数可以删除或注释掉,因为不再每次选择都调用) ---
104
+ # def save_single_log_to_hf_dataset(log_entry, user_identifier_str):
105
+ # ...
106
+
107
+ # --- 新增/修改:批量保存累积日志的函数 ---
108
+ def save_collected_logs_batch(list_of_log_entries, user_identifier_str, current_run_no_for_filename):
109
+ global DATASET_REPO_ID, BATCH_LOG_FOLDER # 使用 BATCH_LOG_FOLDER
110
+ if not list_of_log_entries:
111
+ print("没有累积的日志可供批量保存。")
112
+ return True # 没有东西要保存,也算“成功”
113
+
114
+ identifier_safe = str(user_identifier_str if user_identifier_str else "unknown_user_session").replace('.', '_').replace(':', '_').replace('/', '_').replace(' ', '_')
115
+ print(f"用户 {identifier_safe} - 准备批量保存截至轮次 {current_run_no_for_filename} 的 {len(list_of_log_entries)} 条日志...")
116
+
117
  try:
118
  token = os.getenv("HF_TOKEN")
119
+ if not token:
120
+ print("错误:HF_TOKEN 未设置。无法批量保存日志。")
121
+ return False # 指示保存失败
122
+ if not DATASET_REPO_ID:
123
+ print("错误:DATASET_REPO_ID 未配置。无法批量保存日志。")
124
+ return False # 指示保存失败
125
+
126
  api = HfApi(token=token)
127
+
128
+ timestamp_str = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
129
+ # 文件名可以指明这是哪一“批”的日志,例如基于当前的轮次号
130
+ batch_filename = f"batch_user-{identifier_safe}_upto-run{current_run_no_for_filename}_{timestamp_str}.jsonl"
131
+ # 路径结构: BATCH_LOG_FOLDER / USER_SESSION_ID_SAFE / FILENAME.jsonl
132
+ path_in_repo = f"{BATCH_LOG_FOLDER}/{identifier_safe}/{batch_filename}"
133
+
134
+ jsonl_content = ""
135
+ for log_entry in list_of_log_entries:
136
+ try:
137
+ if isinstance(log_entry, dict):
138
+ jsonl_content += json.dumps(log_entry, ensure_ascii=False) + "\n"
139
+ else:
140
+ print(f"警告:批量保存时,日志条目不是字典格式,跳过:{log_entry}")
141
+ except Exception as json_err:
142
+ print(f"错误:批量保存时,序列化单条日志时出错: {log_entry}. 错误: {json_err}")
143
+ jsonl_content += json.dumps({"error": "serialization_failed_in_batch",
144
+ "original_data_preview": str(log_entry)[:100],
145
+ "timestamp": datetime.now().isoformat()}, ensure_ascii=False) + "\n"
146
+
147
+ if not jsonl_content.strip():
148
+ print(f"用户 {identifier_safe} 截至轮次 {current_run_no_for_filename} 没有可序列化的日志内容。")
149
+ return True # 没有东西要上传
150
 
151
+ log_bytes = jsonl_content.encode('utf-8')
152
+ file_like_object = io.BytesIO(log_bytes)
153
+
154
+ print(f"准备批量上传日志文件: {path_in_repo} ({len(log_bytes)} bytes)")
155
+ api.upload_file(
156
+ path_or_fileobj=file_like_object,
157
+ path_in_repo=path_in_repo,
158
+ repo_id=DATASET_REPO_ID,
159
+ repo_type="dataset", # 确保与您的DATASET_REPO_ID类型匹配
160
+ commit_message=f"Batch logs for user {identifier_safe} up to run {current_run_no_for_filename}"
161
+ )
162
+ print(f"批量日志已成功保存到 HF Dataset: {DATASET_REPO_ID}/{path_in_repo}")
163
+ return True # 指示保存成功
164
+ except Exception as e:
165
+ print(f"批量保存日志 (user {identifier_safe}, up to run {current_run_no_for_filename}) 到 Hugging Face Dataset 时发生严重错误: {e}")
166
+ traceback.print_exc()
167
+ return False # 指示保存失败
168
+
169
+ # ==== 主要的 Gradio 事件处理函数 ====
170
  def process_experiment_step(
171
  s_trial_idx_val, s_run_no_val, s_user_logs_val, s_current_trial_data_val, s_user_session_id_val,
172
  s_current_run_image_list_val, s_num_trials_this_run_val,
173
+ action_type=None, choice_value=None, request: gr.Request = None
174
+ ):
175
+ global master_image_list, NUM_TRIALS_PER_RUN, outputs_ui_components_definition
 
176
  output_s_trial_idx = s_trial_idx_val; output_s_run_no = s_run_no_val
177
  output_s_user_logs = list(s_user_logs_val); output_s_current_trial_data = dict(s_current_trial_data_val) if s_current_trial_data_val else {}
178
  output_s_user_session_id = s_user_session_id_val; output_s_current_run_image_list = list(s_current_run_image_list_val)
 
194
  if len(parts) == 3: parsed_chosen_method, parsed_chosen_subject, parsed_chosen_filename = parts[0].strip(), parts[1].strip(), parts[2].strip()
195
  elif len(parts) == 2: parsed_chosen_method, parsed_chosen_subject = parts[0].strip(), parts[1].strip()
196
  elif len(parts) == 1: parsed_chosen_method = parts[0].strip()
197
+ log_entry = { # ... (log_entry 创建与之前一致) ...
198
+ "timestamp": datetime.now().isoformat(), "user_identifier": user_identifier_for_logging, "run_no": output_s_run_no,
199
+ "image_id": output_s_current_trial_data["data"]["image_id"],
200
+ "left_internal_label": output_s_current_trial_data["data"]["left_internal_label"],
201
+ "right_internal_label": output_s_current_trial_data["data"]["right_internal_label"],
202
+ "chosen_side": choice_value, "chosen_internal_label": chosen_internal_label,
203
+ "chosen_method": parsed_chosen_method, "chosen_subject": parsed_chosen_subject, "chosen_filename": parsed_chosen_filename,
204
+ "trial_sequence_in_run": output_s_current_trial_data["data"]["cur_no"],
205
+ "is_sentinel": output_s_current_trial_data["data"]["is_sentinel"]
206
+ }
207
+ output_s_user_logs.append(log_entry) # <--- 累积日志到列表
208
+ print(f"用户 {user_identifier_for_logging} 记录选择 (img: {log_entry['image_id']})。当前批次日志数: {len(output_s_user_logs)}")
209
+ # !!! 移除 save_single_log_to_hf_dataset 的调用 !!!
210
  else:
211
+ # ... (错误处理) ...
212
  print(f"用户 {user_identifier_for_logging} 错误:记录选择时数据为空!")
213
  error_ui_updates = create_ui_error_tuple("记录选择时内部错误。", f"用户ID: {user_id_display_text} | 进度:{output_s_trial_idx}/{output_s_num_trials_this_run}")
214
  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
215
+
216
  if action_type == "start_experiment":
217
  is_first = (output_s_num_trials_this_run == 0 and output_s_trial_idx == 0 and output_s_run_no == 1)
218
+ is_completed_for_restart = (output_s_num_trials_this_run > 0 and output_s_trial_idx >= output_s_num_trials_this_run)
219
+
220
+ if is_first or is_completed_for_restart:
221
+ # ... (开始新轮次的逻辑,与之前代码一致,例如:随机选择图片,重置trial_idx等) ...
222
+ # ... (注意:当 is_completed_for_restart 时,output_s_run_no 应该已经增加了) ...
223
+ # output_s_user_logs 不在这里重置,它会累积
224
+ if not master_image_list: error_ui = create_ui_error_tuple("错误: 无可用目标图片!", f"用户ID: {user_id_display_text} | 进度: 0/0"); return 0, output_s_run_no, output_s_user_logs, {}, output_s_user_session_id, [], 0, *error_ui # output_s_user_logs is passed as is
225
+
226
+ # 这段逻辑用于确定是否是真正的“下一轮”并增加轮次号
227
+ # 如果是点击“开始实验”按钮且上一轮刚结束,则轮次号增加
228
+ if is_completed_for_restart:
229
+ # 此时 output_s_trial_idx 指向的是刚完成轮次的总数,output_s_run_no 是刚完成的轮次号
230
+ # 所以,下一轮的轮次号应该是 output_s_run_no + 1
231
+ # 但 process_experiment_step 的状态管理是,它返回的值会成为下一次调用的输入值
232
+ # 所以,如果这里要开始新轮次,应该在这里增加 output_s_run_no
233
+ output_s_run_no += 1 # 增加轮次号为新的一轮
234
+
235
  num_avail = len(master_image_list); run_size = min(num_avail, NUM_TRIALS_PER_RUN)
236
+ if run_size == 0: error_ui = create_ui_error_tuple("错误: 采样图片数为0!", f"用户ID: {user_id_display_text} | 进度: 0/0"); return 0, output_s_run_no, output_s_user_logs, {}, output_s_user_session_id, [], 0, *error_ui
237
+
238
  output_s_current_run_image_list = random.sample(master_image_list, run_size)
239
+ output_s_num_trials_this_run = run_size
240
+ output_s_trial_idx = 0 # 新轮次从0开始
241
+ # output_s_user_logs 不重置
242
+ output_s_current_trial_data = {}
243
+
244
+ # 只有在会话ID为空(即应用刚启动,用户第一次点击“同意”后,再点击“开始实验”)时才生成新的基于时间戳的ID
245
+ # 或者,如果策略是每轮实验都用新的用户ID,则可以在这里(is_first or is_completed_for_restart)重新生成
246
+ # 当前代码中,user_session_id 是在欢迎页的 handle_agree_and_start 中设置的,这里不再修改它
247
+ # user_identifier_for_logging 会使用这个ID
248
  print(f"开始/继续轮次 {output_s_run_no} (用户ID: {output_s_user_session_id}). 随机选择 {output_s_num_trials_this_run} 张图片.")
249
+
250
+ else: # 在轮次中途点击了开始按钮
251
  print(f"用户 {user_identifier_for_logging} 在第 {output_s_run_no} 轮,试验 {output_s_trial_idx} 点击开始,但轮次未完成。忽略。")
252
  no_change_ui = create_no_change_tuple()
253
  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
254
+
255
+ # --- 轮次结束处理 ---
256
  if output_s_trial_idx >= output_s_num_trials_this_run and output_s_num_trials_this_run > 0:
257
+ logs_for_this_run_count = sum(1 for log in output_s_user_logs if log.get("run_no") == output_s_run_no) # 粗略计算本轮日志数
258
+ if logs_for_this_run_count > 0 or not output_s_user_logs: # 如果本轮有日志,或总日志为空(可能刚开始)
259
+ print(f"用户 {output_s_user_session_id} 完成第 {output_s_run_no} 轮。累积日志数: {len(output_s_user_logs)}")
260
+
261
+ # 检查是否达到批量保存的条件
262
+ if output_s_run_no > 0 and output_s_run_no % 10 == 0:
263
+ if output_s_user_logs: # 只有当有日志时才尝试保存
264
+ print(f"达到第 {output_s_run_no} 轮,准备批量保存累积的日志...")
265
+ save_success = save_collected_logs_batch(list(output_s_user_logs), user_identifier_for_logging, output_s_run_no)
266
+ if save_success:
267
+ print("批量日志已成功(或尝试)保存,将清空累积日志列表。")
268
+ output_s_user_logs = [] # 清空已保存的日志
269
+ else:
270
+ print("警告:批量日志保存失败。日志将继续累积。")
271
+ else:
272
+ print(f"第 {output_s_run_no} 轮结束,但累积日志为空,无需批量保存。")
273
+
274
  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} 轮 🎉"
275
+ ui_updates = list(create_ui_error_tuple(f"🎉 第 {output_s_run_no} 轮完成!请点击“开始试验 / 下一轮”开始新的轮次。", prog_text))
276
  ui_updates[7]=gr.update(interactive=True); ui_updates[8]=gr.update(interactive=False); ui_updates[9]=gr.update(interactive=False)
277
  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)
278
  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_updates; return
279
+
280
+ # --- 获取并显示下一个试验 ---
281
+ # ... (与上一版代码相同,确保 prog_text 使用了新的 user_id_display_text 或 output_s_user_session_id) ...
282
  if not output_s_current_run_image_list or output_s_num_trials_this_run == 0:
283
  error_ui = create_ui_error_tuple("错误: 无法加载试验图片 (列表为空)", f"用户ID: {user_id_display_text} | 进度: N/A")
284
  return output_s_trial_idx, output_s_run_no, output_s_user_logs, {"data": None}, output_s_user_session_id, [], 0, *error_ui
 
297
  time.sleep(3)
298
  ui_show_candidates_updates = list(create_no_change_tuple())
299
  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)
300
+ ui_show_candidates_updates[3]=gr.update(value=trial_info["left_display_label"], visible=False); ui_show_candidates_updates[4]=gr.update(value=trial_info["right_display_label"], visible=False)
 
301
  ui_show_candidates_updates[5]="请选择更像原图的一张"; ui_show_candidates_updates[6]=prog_text
302
  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)
303
  yield next_s_trial_idx_for_state, 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
304
 
305
+
306
  welcome_page_markdown = """
307
  ## 欢迎加入实验!
308
  您好!非常感谢您抽出宝贵时间参与我们的视觉偏好评估实验。您的选择将帮助我们改进重建算法,让机器生成的图像更贴近人类视觉体验!
 
309
  1. **实验目的**
310
  通过比较两幅 重建图像 与原始 目标图像 的相似度。
 
311
  2. **操作流程**
312
  * 点击下方的「我已阅读并同意开始实验」按钮。
313
  * 然后点击主实验界面的「开始试验 / 下一轮」按钮。
 
315
  * 随后自动切换到 **两张重建图像**。
316
  * 根据刚才的观察记忆,选出您认为与目标图像最相似的一张。
317
  * 选择后系统会自动进入下一轮比较。
 
318
  3. **温馨提示**
319
  * 请勿刷新或关闭页面,以免中断实验。
320
  * 若图片加载稍有延迟,请耐心等待;持续异常可联系邮箱 yangminghan@bupt.edu.cn。
321
  * 本实验将保护您的任何个人隐私信息,所有数据仅用于学术研究,请您认真选择和填写。
 
322
  4. **奖励说明**
323
  * 完成全部轮次后,请截图记录您所完成的实验总数(可累积,页面左下角将显示进度,请保证截取到为您分配的ID,轮次)。
324
  * 将截图发送至邮箱 yangminghan@bupt.edu.cn,我们将在核验后发放奖励。
 
325
  再次感谢您的参与与支持!您每一次认真选择都对我们的研究意义重大。祝您一切顺利,实验愉快!
326
  """
327
 
 
328
  def handle_agree_and_start(name, gender, age, education, request: gr.Request):
 
329
  error_messages_list = []
330
+ if not name or str(name).strip() == "": error_messages_list.append("姓名 不能为空。")
331
+ if gender is None or str(gender).strip() == "": error_messages_list.append("性别 必须选择。")
332
+ if age is None: error_messages_list.append("年龄 不能为空。")
333
+ elif not (isinstance(age, (int, float)) and 1 <= age <= 120):
334
+ try: num_age = float(age);
335
+ except (ValueError, TypeError): error_messages_list.append("年龄必须是一个有效的数字。")
336
+ else:
337
+ if not (1 <= num_age <= 120): error_messages_list.append("年龄必须在 1 到 120 之间。")
338
+ if education is None or str(education).strip() == "" or str(education).strip() == "其他" and not name: # 稍微修改逻辑,如果选了“其他”但没填姓名,也可能需要提示
339
+ # 更简单的逻辑:如果education是None或空字符串
340
+ if education is None or str(education).strip() == "":
341
+ error_messages_list.append("学历 必须选择。")
 
 
 
 
 
 
 
 
342
 
343
  if error_messages_list:
344
  full_error_message = "请修正以下错误:\n" + "\n".join([f"- {msg}" for msg in error_messages_list])
345
  print(f"用户输入验证失败: {full_error_message}")
346
+ return gr.update(), False, gr.update(visible=True), gr.update(visible=False), full_error_message
 
 
 
 
 
 
 
 
347
 
 
348
  s_name = str(name).strip().replace(" ","_").replace("/","_").replace("\\","_")
349
  s_gender = str(gender).strip().replace(" ","_").replace("/","_").replace("\\","_")
350
+ s_age = str(int(float(age)))
351
  s_education = str(education).strip().replace(" ","_").replace("/","_").replace("\\","_")
 
352
  user_id_str = f"N-{s_name}_G-{s_gender}_A-{s_age}_E-{s_education}"
 
353
  print(f"用户信息收集完毕,生成用户ID: {user_id_str}")
 
 
354
  return user_id_str, True, gr.update(visible=False), gr.update(visible=True), ""
355
 
 
356
  with gr.Blocks(css=CSS, title="图像重建主观评估") as demo:
 
357
  s_show_experiment_ui = gr.State(False)
358
+ s_trial_index = gr.State(0); s_run_no = gr.State(1); s_user_logs = gr.State([])
359
+ s_current_trial_data = gr.State({}); s_user_session_id = gr.State(None)
360
+ s_current_run_image_list = gr.State([]); s_num_trials_this_run = gr.State(0)
 
 
 
 
361
 
 
362
  welcome_container = gr.Column(visible=True)
363
  experiment_container = gr.Column(visible=False)
364
 
 
365
  with welcome_container:
366
+ gr.Markdown(welcome_page_markdown)
367
  with gr.Row():
368
+ user_name_input = gr.Textbox(label="请输入您的姓名或代号 (例如 ZS 或 User001)", placeholder="例如:张三 -> ZS") # 修改了label
369
+ user_gender_input = gr.Radio(label="性别", choices=["男", "女", "其他", "不愿透露"], value="不愿透露")
370
  with gr.Row():
371
+ user_age_input = gr.Number(label="年龄 (请输入1-120的整数)", minimum=1, maximum=120, step=1, value=25) # 修改了label和value
372
+ user_education_input = gr.Dropdown(label="学历", choices=["其他","初中及以下","高中(含中专)", "大专", "本科", "硕士", "博士(含在读)"], value="本科") # 修改了选项和value
373
+ welcome_error_msg = gr.Markdown(value="")
 
 
 
374
  btn_agree_and_start = gr.Button("我已阅读上述说明并同意参与实验")
375
 
 
376
  with experiment_container:
377
  gr.Markdown("## 🧠 图像重建主观评估实验")
378
+ gr.Markdown(f"每轮实验大约有 {NUM_TRIALS_PER_RUN} 次比较。")
 
 
 
379
  with gr.Row():
380
+ with gr.Column(scale=1, min_width=300):
381
  left_img = gr.Image(label="左候选图", visible=False, height=400, interactive=False)
 
382
  left_lbl = gr.Textbox(label="左图信息", visible=False, interactive=False, max_lines=1)
383
  btn_left = gr.Button("选择左图 (更相似)", interactive=False, elem_classes="compact_button")
384
+ with gr.Column(scale=1, min_width=300):
 
385
  right_img = gr.Image(label="右候选图", visible=False, height=400, interactive=False)
 
386
  right_lbl = gr.Textbox(label="右图信息", visible=False, interactive=False, max_lines=1)
387
  btn_right = gr.Button("选择右图 (更相似)", interactive=False, elem_classes="compact_button")
388
+ with gr.Row(): target_img = gr.Image(label="目标图像 (观察3秒后消失)", visible=False, height=400, interactive=False)
389
+ with gr.Row(): status_text = gr.Markdown(value="请点击“开始试验 / 下一轮”按钮。")
390
+ with gr.Row(): progress_text = gr.Markdown()
391
+ with gr.Row(): btn_start = gr.Button("开始试验 / 下一轮")
 
 
 
 
 
 
 
 
 
 
 
 
392
  file_out_placeholder = gr.File(label=" ", visible=False, interactive=False)
393
 
 
 
394
  outputs_ui_components_definition = [
395
+ target_img, left_img, right_img, left_lbl, right_lbl, status_text, progress_text,
396
+ btn_start, btn_left, btn_right, file_out_placeholder
 
 
 
397
  ]
 
398
  click_inputs_base = [
399
  s_trial_index, s_run_no, s_user_logs, s_current_trial_data, s_user_session_id,
400
  s_current_run_image_list, s_num_trials_this_run
401
  ]
402
  event_outputs = [
403
  s_trial_index, s_run_no, s_user_logs, s_current_trial_data, s_user_session_id,
404
+ s_current_run_image_list, s_num_trials_this_run, *outputs_ui_components_definition
 
405
  ]
406
 
 
407
  btn_agree_and_start.click(
408
+ fn=handle_agree_and_start,
409
+ inputs=[user_name_input, user_gender_input, user_age_input, user_education_input, gr.Request()],
410
+ outputs=[s_user_session_id, s_show_experiment_ui, welcome_container, experiment_container, welcome_error_msg]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  )
412
+ btn_start.click(fn=partial(process_experiment_step, action_type="start_experiment"), inputs=click_inputs_base, outputs=event_outputs, queue=True)
413
+ btn_left.click(fn=partial(process_experiment_step, action_type="record_choice", choice_value="left"), inputs=click_inputs_base, outputs=event_outputs, queue=True)
414
+ btn_right.click(fn=partial(process_experiment_step, action_type="record_choice", choice_value="right"), inputs=click_inputs_base, outputs=event_outputs, queue=True)
415
 
 
416
  if __name__ == "__main__":
417
+ if not master_image_list: print("\n关键错误:程序无法启动,因无目标图片。"); exit()
 
 
418
  else:
419
+ print(f"从 '{TARGET_DIR}' 加载 {len(master_image_list)} 张目标图片。每轮选 {NUM_TRIALS_PER_RUN} 张。")
420
+ if not METHOD_ROOTS: print(f"警告: '{BASE_IMAGE_DIR}' 无候选方法子目录。")
421
+ else: print(f"方法根目录: {METHOD_ROOTS}")
422
+ if not SUBJECTS: print("警告: SUBJECTS 列表为空。")
423
+ else: print(f"Subjects: {SUBJECTS}")
424
+ print(f"日志保存到 Dataset: '{DATASET_REPO_ID}'")
425
+ if BATCH_LOG_FOLDER: print(f" - 批量日志文件夹: '{BATCH_LOG_FOLDER}/'") # 使用 BATCH_LOG_FOLDER
426
+ if INDIVIDUAL_LOGS_FOLDER: print(f" - 单个选择日志文件夹: '{INDIVIDUAL_LOGS_FOLDER}/'")
427
+ if not os.getenv("HF_TOKEN"): print("警告: HF_TOKEN 未设置。日志无法保存。\n 请在 Space Secrets 中设置 HF_TOKEN。")
428
+ else: print("HF_TOKEN 已找到。")
429
  path_to_allow_serving_from = BASE_IMAGE_DIR
430
  allowed_paths_list = []
431
  if os.path.exists(path_to_allow_serving_from) and os.path.isdir(path_to_allow_serving_from):
432
  allowed_paths_list.append(os.path.abspath(path_to_allow_serving_from))
433
+ print(f"Gradio `demo.launch()` 配置 allowed_paths: {allowed_paths_list}")
434
+ else: print(f"关键警告:图片基础目录 '{path_to_allow_serving_from}' 不存在或非目录。")
435
+ print("启动 Gradio 应用...")
436
  if allowed_paths_list: demo.launch(allowed_paths=allowed_paths_list)
437
  else: demo.launch()