YanmHa commited on
Commit
a4a89bf
·
verified ·
1 Parent(s): e0a5cab

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +862 -23
app.py CHANGED
@@ -1,32 +1,871 @@
1
- import os
2
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
- ROOT_DIR = "/data/images/images"
5
 
6
- def get_directory_structure(root_path):
7
- if not os.path.exists(root_path):
8
- return f"路径不存在:{root_path}"
9
 
10
- tree_lines = []
11
 
12
- for current_dir, subdirs, files in os.walk(root_path):
13
- level = current_dir.replace(root_path, "").count(os.sep)
14
- indent = " " * level
15
- tree_lines.append(f"{indent}📁 {os.path.basename(current_dir) or '/'}")
 
 
 
 
 
 
 
16
 
17
- sub_indent = " " * (level + 1)
18
- for f in files:
19
- tree_lines.append(f"{sub_indent}📄 {f}")
 
 
 
20
 
21
- return "\n".join(tree_lines)
 
 
 
 
 
 
 
 
 
 
22
 
23
- # Gradio 界面
24
- demo = gr.Interface(
25
- fn=lambda: get_directory_structure(ROOT_DIR),
26
- inputs=[],
27
- outputs="text",
28
- title="查看 /data/images/images 的目录结构",
29
- description="点击按钮以展示该目录下的文件夹和文件结构"
30
- )
31
 
32
- demo.launch()
 
 
 
 
 
 
1
  import gradio as gr
2
+ import os
3
+ import random
4
+ import time
5
+ from datetime import datetime
6
+ from functools import partial
7
+ import json
8
+ 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
+ # ---- 测试模式开关 ----
16
+ REPEAT_SINGLE_TARGET_FOR_TESTING = False # 设置为 True 以启用“重复单一目标图”测试模式
17
+ NUM_REPEATED_TRIALS_FOR_TESTING = 5 # 在该测试模式下,单个目标图片重复的次数 (原为20,改为5方便测试)
18
+ # ---- 常规配置 ----
19
+ BASE_IMAGE_DIR = "/data/images/images"
20
+ TARGET_DIR_BASENAME = "gt"
21
+ TARGET_DIR = os.path.join(BASE_IMAGE_DIR, TARGET_DIR_BASENAME)
22
+
23
+ METHOD_ROOTS = []
24
+ if os.path.exists(BASE_IMAGE_DIR):
25
+ try:
26
+ METHOD_ROOTS = [
27
+ os.path.join(BASE_IMAGE_DIR, d)
28
+ for d in os.listdir(BASE_IMAGE_DIR)
29
+ if os.path.isdir(os.path.join(BASE_IMAGE_DIR, d)) and \
30
+ d != TARGET_DIR_BASENAME and \
31
+ not d.startswith('.')
32
+ ]
33
+ if not METHOD_ROOTS: print(f"警告:在 '{BASE_IMAGE_DIR}' 中没有找到有效的方法目录 (除了 '{TARGET_DIR_BASENAME}')。")
34
+ else: print(f"已识别的方法根目录 (原始): {METHOD_ROOTS}")
35
+ except Exception as e: print(f"错误:在扫描 '{BASE_IMAGE_DIR}' 时发生错误: {e}"); METHOD_ROOTS = []
36
+ else: print(f"警告:基础目录 '{BASE_IMAGE_DIR}' 不存在。将无法加载候选图片。")
37
+
38
+ SUBJECTS = ["subj01", "subj02", "subj05", "subj07"] # 修正了 "subj05," 的拼写
39
+ SENTINEL_TRIAL_INTERVAL = 20
40
+ NUM_TRIALS_PER_RUN = 100 # 正常运行时的每轮试验数
41
+ LOG_BATCH_SIZE = 20
42
+
43
+ DATASET_REPO_ID = "YanmHa/image-aligned-experiment-data"
44
+ BATCH_LOG_FOLDER = "run_logs_batch"
45
+ CSS = ".gr-block {margin-top: 4px !important; margin-bottom: 4px !important;} .compact_button { padding: 4px 8px; min-width: auto; }"
46
+
47
+ # ---- 测试模式的列表缩减逻辑 (仅当 REPEAT_SINGLE_TARGET_FOR_TESTING 为 True 时生效) ----
48
+ if REPEAT_SINGLE_TARGET_FOR_TESTING:
49
+ print(f"--- 特殊测试模式 (重复单一目标图) 已激活 ---")
50
+ NUM_TRIALS_PER_RUN = NUM_REPEATED_TRIALS_FOR_TESTING # 确保UI显示的数字和实际测试一致
51
+ print(f"测试模式:NUM_TRIALS_PER_RUN 已被设置为: {NUM_TRIALS_PER_RUN}")
52
+
53
+ if METHOD_ROOTS:
54
+ original_method_roots_count = len(METHOD_ROOTS)
55
+ METHOD_ROOTS = [METHOD_ROOTS[0]]
56
+ print(f"测试模式:METHOD_ROOTS 已从 {original_method_roots_count} 个缩减为仅包含第一个方法: {METHOD_ROOTS}")
57
+ else:
58
+ print("测试模式警告:METHOD_ROOTS 为空,无法缩减。")
59
+
60
+ if len(METHOD_ROOTS) == 1: # 只有一种方法时,确保至少有两个subject以形成对比
61
+ if len(SUBJECTS) >= 2:
62
+ original_subjects_count = len(SUBJECTS)
63
+ SUBJECTS = [SUBJECTS[0], SUBJECTS[1]]
64
+ print(f"测试模式:由于方法仅1种,SUBJECTS 已从 {original_subjects_count} 个缩减为前两个: {SUBJECTS}")
65
+ elif SUBJECTS:
66
+ print(f"测试模式:SUBJECTS 只有一个元素 ({SUBJECTS}),方法也只有一种。注意:可能无法形成候选对。")
67
+ else:
68
+ print("测试模式警告:SUBJECTS 为空。")
69
+ print(f"--- 特殊测试模式配置结束 ---")
70
+ else:
71
+ print(f"正常模式:使用完整配置。每轮目标试验数: {NUM_TRIALS_PER_RUN}")
72
+ print(f"方法根目录: {METHOD_ROOTS}")
73
+ print(f"Subjects: {SUBJECTS}")
74
+
75
+ # ==== 全局持久化历史记录 ====
76
+ PERSISTENT_STORAGE_BASE = "/data"
77
+ DATA_SUBDIR_NAME = "my_user_study_persistent_history"
78
+
79
+ if not os.path.exists(PERSISTENT_STORAGE_BASE):
80
+ try:
81
+ os.makedirs(PERSISTENT_STORAGE_BASE, exist_ok=True)
82
+ print(f"信息:基础持久化目录 '{PERSISTENT_STORAGE_BASE}' 尝试确保其存在。")
83
+ except Exception as e:
84
+ print(f"警告:操作基础持久化目录 '{PERSISTENT_STORAGE_BASE}' 时出现问题: {e}。")
85
+
86
+ full_subdir_path = os.path.join(PERSISTENT_STORAGE_BASE, DATA_SUBDIR_NAME)
87
+ if not os.path.exists(full_subdir_path):
88
+ try:
89
+ os.makedirs(full_subdir_path)
90
+ print(f"成功创建持久化子目录: {full_subdir_path}")
91
+ except Exception as e:
92
+ print(f"错误:创建持久化子目录 '{full_subdir_path}' 失败: {e}")
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}' 无效或不可写。")
105
+ 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}' 未找到或路径无效。将创建新的空历史记录。")
119
+ global_shown_pairs_cache = {}
120
+ global_history_has_unsaved_changes = False
121
+ return
122
+
123
+ try:
124
+ with open(GLOBAL_HISTORY_FILE, 'r', encoding='utf-8') as f:
125
+ content = f.read()
126
+ if not content.strip():
127
+ print(f"信息:全局历史文件 '{GLOBAL_HISTORY_FILE}' 为空。将使用空历史记录。")
128
+ global_shown_pairs_cache = {}
129
+ else:
130
+ data_from_file = json.loads(content)
131
+ global_shown_pairs_cache = {
132
+ target_img: {frozenset(pair) for pair in pairs_list}
133
+ for target_img, pairs_list in data_from_file.items()
134
+ }
135
+ print(f"已成功从 '{GLOBAL_HISTORY_FILE}' 加载全局已展示图片对历史。")
136
+ except json.JSONDecodeError as jde:
137
+ print(f"错误:加载全局历史文件 '{GLOBAL_HISTORY_FILE}' 失败 (JSON解析错误: {jde})。文件内容可能已损坏。将使用空历史记录。")
138
+ global_shown_pairs_cache = {}
139
+ except Exception as e:
140
+ print(f"错误:加载全局历史文件 '{GLOBAL_HISTORY_FILE}' 时发生其他错误: {e}。将使用空历史记录。")
141
+ global_shown_pairs_cache = {}
142
+ global_history_has_unsaved_changes = False
143
+
144
+ def save_global_shown_pairs():
145
+ global global_shown_pairs_cache, global_history_has_unsaved_changes
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):
155
+ try:
156
+ os.makedirs(parent_dir, exist_ok=True)
157
+ print(f"信息: 为保存历史文件,创建了父目录 {parent_dir}")
158
+ except Exception as e_mkdir:
159
+ print(f"错误: 创建历史文件的父目录 {parent_dir} 失败: {e_mkdir}。保存可能失败。")
160
+ return False
161
+ data_to_save = {
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()
192
+
193
+ # ==== 加载所有可用的目标图片 ====
194
+ master_image_list = []
195
+ if os.path.exists(TARGET_DIR):
196
+ try:
197
+ master_image_list = sorted(
198
+ [f for f in os.listdir(TARGET_DIR) if f.lower().endswith((".jpg", ".png", ".jpeg"))],
199
+ key=lambda x: int(os.path.splitext(x)[0])
200
+ )
201
+ except ValueError:
202
+ master_image_list = sorted([f for f in os.listdir(TARGET_DIR) if f.lower().endswith((".jpg", ".png", ".jpeg"))])
203
+ if master_image_list: print(f"警告: '{TARGET_DIR}' 文件名非纯数字,按字母排序。")
204
+ if not master_image_list: print(f"警告:在 '{TARGET_DIR}' 中无有效图片。")
205
+ elif not os.path.exists(TARGET_DIR) and os.path.exists(BASE_IMAGE_DIR): print(f"错误:目标目录 '{TARGET_DIR}' 未找到。")
206
+
207
+ # ---- 测试模式:缩减 master_image_list (仅当 REPEAT_SINGLE_TARGET_FOR_TESTING 为 True 时生效) ----
208
+ 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
230
+
231
+ if not current_run_image_list_for_trial or current_trial_idx_in_run >= num_trials_in_this_run_for_trial:
232
+ return None, current_trial_idx_in_run
233
+
234
+ img_filename_original = current_run_image_list_for_trial[current_trial_idx_in_run]
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
+ pool = []
239
+ for m_root_path in METHOD_ROOTS:
240
+ method_name = os.path.basename(m_root_path)
241
+
242
+ subjects_for_method = SUBJECTS # 默认使用所有 subjects
243
+ if method_name.lower() == "takagi": # 对 "takagi" 方法的特殊处理 (大小写不敏感)
244
+ if "subj01" in SUBJECTS:
245
+ subjects_for_method = ["subj01"]
246
+ # print(f"调试:方法 'takagi' 将仅使用 'subj01'")
247
+ else:
248
+ # print(f"调试:方法 'takagi' 希望使用 'subj01',但其不在全局 SUBJECTS 列表中。此方法此次不产生候选图。")
249
+ continue # 如果 subj01 不可用,则跳过 takagi 方法的此次循环
250
+
251
+ for s_id in subjects_for_method:
252
+ base, ext = os.path.splitext(img_filename_original)
253
+ reconstructed_filename = f"{base}_0{ext}"
254
+ candidate_path = os.path.join(m_root_path, s_id, reconstructed_filename)
255
+ if os.path.exists(candidate_path):
256
+ internal_label = f"{method_name}/{s_id}/{reconstructed_filename}"
257
+ pool.append((internal_label, candidate_path))
258
+
259
+ trial_info = {"image_id": img_filename_original, "target_path": target_full_path, "cur_no": trial_number_for_display, "is_sentinel": False,
260
+ "left_display_label": "N/A", "left_internal_label": "N/A", "left_path": None,
261
+ "right_display_label": "N/A", "right_internal_label": "N/A", "right_path": None}
262
+
263
+ is_potential_sentinel_trial = (trial_number_for_display > 0 and trial_number_for_display % SENTINEL_TRIAL_INTERVAL == 0)
264
+
265
+ if is_potential_sentinel_trial:
266
+ if not pool: print(f"警告:哨兵图 '{img_filename_original}' (trial {trial_number_for_display}) 无候选。")
267
+ else:
268
+ # ... (哨兵试验逻辑不变) ...
269
+ print(f"生成哨兵试验 for '{img_filename_original}' (trial {trial_number_for_display})")
270
+ trial_info["is_sentinel"] = True
271
+ sentinel_candidate_target_tuple = ("目标图像", target_full_path)
272
+ random_reconstruction_candidate_tuple = random.choice(pool)
273
+ candidates_for_sentinel = [
274
+ (("目标图像", target_full_path), sentinel_candidate_target_tuple[0]),
275
+ (("重建图", random_reconstruction_candidate_tuple[1]), random_reconstruction_candidate_tuple[0])
276
+ ]
277
+ random.shuffle(candidates_for_sentinel)
278
+ trial_info.update({
279
+ "left_display_label": candidates_for_sentinel[0][0][0], "left_path": candidates_for_sentinel[0][0][1], "left_internal_label": candidates_for_sentinel[0][1],
280
+ "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],
281
+ })
282
+ else: # 常规试验
283
+ if len(pool) < 2:
284
+ print(f"警告:常规图 '{img_filename_original}' (trial {trial_number_for_display}) 候选少于2 (找到 {len(pool)})。此试验无法进行。")
285
+ # 注意:如果这个目标图片是当前轮次中唯一的图片,或者后续图片也无法生成试验,轮次可能会提前结束。
286
+ # 我们可以在这里也将其标记为耗尽,以确保它在下一轮开始时被正确过滤。
287
+ # exhausted_target_images.add(img_filename_original) # 可选:立即标记,以防万一
288
+ return None, current_trial_idx_in_run
289
+
290
+ target_global_history_set = global_shown_pairs_cache.setdefault(img_filename_original, set())
291
+ all_possible_pairs_in_pool = []
292
+ for c1, c2 in combinations(pool, 2):
293
+ pair_labels_fset = frozenset({c1[0], c2[0]})
294
+ all_possible_pairs_in_pool.append( ((c1, c2), pair_labels_fset) )
295
+
296
+ unseen_globally_pairs_with_data = [
297
+ item for item in all_possible_pairs_in_pool if item[1] not in target_global_history_set
298
+ ]
299
+ selected_candidates_tuples = None
300
+
301
+ if unseen_globally_pairs_with_data:
302
+ chosen_pair_data_and_labels = random.choice(unseen_globally_pairs_with_data)
303
+ selected_candidates_tuples = chosen_pair_data_and_labels[0]
304
+ chosen_pair_frozenset = chosen_pair_data_and_labels[1]
305
+ target_global_history_set.add(chosen_pair_frozenset)
306
+ global_history_has_unsaved_changes = True
307
+ # print(f"调试:目标 '{img_filename_original}': 新全局唯一对 {chosen_pair_frozenset} 已添加。")
308
+ else: # 所有唯一对都已展示过
309
+ print(f"警告:目标图 '{img_filename_original}' (trial {trial_number_for_display}): 来自当前池的所有 ({len(all_possible_pairs_in_pool)}) 个候选对均已在全局展示过。")
310
+ if all_possible_pairs_in_pool: # 确保池中至少能形成一对
311
+ print(f"目标图 '{img_filename_original}' 将被标记为已耗尽,未来轮次中将被跳过。")
312
+ exhausted_target_images.add(img_filename_original)
313
+ # global_history_has_unsaved_changes = True # 如果 exhausted_target_images 需要持久化,这里也应该标记
314
+ return None, current_trial_idx_in_run # 返回 None,表示此图片本次无法生成新试验
315
+
316
+ display_order_candidates = list(selected_candidates_tuples)
317
+ if random.random() > 0.5:
318
+ display_order_candidates = display_order_candidates[::-1]
319
+ trial_info.update({
320
+ "left_display_label": "候选图 1", "left_path": display_order_candidates[0][1], "left_internal_label": display_order_candidates[0][0],
321
+ "right_display_label": "候选图 2", "right_path": display_order_candidates[1][1], "right_internal_label": display_order_candidates[1][0],
322
+ })
323
+ return trial_info, current_trial_idx_in_run + 1
324
+
325
+ # ==== 批量保存用户选择日志函数 (保持不变) ====
326
+ def save_single_log_to_hf_dataset(log_entry, user_identifier_str):
327
+ global DATASET_REPO_ID, INDIVIDUAL_LOGS_FOLDER
328
+ if not isinstance(log_entry, dict):
329
+ print(f"错误:单个日志条目不是字典格式,无法保存:{log_entry}")
330
+ return False # 返回失败
331
+ current_user_id = user_identifier_str if user_identifier_str else "unknown_user_session"
332
+ identifier_safe = str(current_user_id).replace('.', '_').replace(':', '_').replace('/', '_')
333
+ print(f"用户 {identifier_safe} - 准备保存单条日志 for image {log_entry.get('image_id', 'Unknown')}...")
334
+ try:
335
+ token = os.getenv("HF_TOKEN")
336
+ if not token:
337
+ print("错误:环境变量 HF_TOKEN 未设置。无法保存单条日志到Dataset。")
338
+ return False # 返回失败
339
+ if not DATASET_REPO_ID:
340
+ print("错误:DATASET_REPO_ID 未配置。无法保存单条日志到Dataset。")
341
+ return False # 返回失败
342
+ api = HfApi(token=token)
343
+ # ... (文件名和路径生成逻辑不变) ...
344
+ image_id_safe_for_filename = os.path.splitext(log_entry.get("image_id", "unknown_img"))[0].replace('.', '_').replace(':', '_').replace('/', '_')
345
+ file_creation_timestamp_str = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
346
+ 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")
347
+ path_in_repo = f"{INDIVIDUAL_LOGS_FOLDER}/{identifier_safe}/{unique_filename}"
348
+
349
+ try:
350
+ json_content = json.dumps(log_entry, ensure_ascii=False, indent=2)
351
+ except Exception as json_err:
352
+ print(f"错误:序列化单条日志时出错: {log_entry}. 错误: {json_err}")
353
+ # 即使序列化失败,也尝试记录一个最小化的错误日志
354
+ 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()}
355
+ json_content = json.dumps(error_log_content, ensure_ascii=False, indent=2)
356
+ # return False # 序列化失败也认为是一种保存失败
357
+
358
+ log_bytes = json_content.encode('utf-8')
359
+ file_like_object = io.BytesIO(log_bytes)
360
+ print(f"准备上传单条日志文件: {path_in_repo} ({len(log_bytes)} bytes)")
361
+ api.upload_file(
362
+ path_or_fileobj=file_like_object,
363
+ path_in_repo=path_in_repo,
364
+ repo_id=DATASET_REPO_ID,
365
+ repo_type="dataset",
366
+ 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}")
367
+ )
368
+ print(f"单条日志已成功保存到 HF Dataset: {DATASET_REPO_ID}/{path_in_repo}")
369
+ return True # 返回成功
370
+ except HfHubHTTPError as hf_http_error:
371
+ print(f"保存单条日志到 Hugging Face Dataset 时发生 HTTP 错误 (可能被限流或权限问题): {hf_http_error}")
372
+ traceback.print_exc()
373
+ return False # 返回失败
374
+ except Exception as e:
375
+ print(f"保存单条日志 (image {log_entry.get('image_id', 'Unknown')}, user {identifier_safe}) 到 Hugging Face Dataset 时发生严重错误: {e}")
376
+ traceback.print_exc()
377
+ return False # 返回失败
378
+
379
+ # ==== 批量保存用户选择日志函数 (确保返回 True/False) ====
380
+ def save_collected_logs_batch(list_of_log_entries, user_identifier_str, batch_identifier):
381
+ global DATASET_REPO_ID, BATCH_LOG_FOLDER
382
+ if not list_of_log_entries:
383
+ print("批量保存用户日志:没有累积的日志。")
384
+ return True # 没有日志也算“成功”完成此操作
385
+ identifier_safe = str(user_identifier_str if user_identifier_str else "unknown_user_session").replace('.', '_').replace(':', '_').replace('/', '_').replace(' ', '_')
386
+ print(f"用户 {identifier_safe} - 准备批量保存 {len(list_of_log_entries)} 条选择日志 (批次标识: {batch_identifier})...")
387
+ try:
388
+ token = os.getenv("HF_TOKEN")
389
+ if not token:
390
+ print("错误:HF_TOKEN 未设置。无法批量保存选择日志。")
391
+ return False
392
+ if not DATASET_REPO_ID:
393
+ print("错误:DATASET_REPO_ID 未配置。无法批量保存选择日志。")
394
+ return False
395
+ api = HfApi(token=token)
396
+ # ... (文件名和内容生成逻辑不变) ...
397
+ timestamp_str = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
398
+ batch_filename = f"batch_user-{identifier_safe}_id-{batch_identifier}_{timestamp_str}_logs-{len(list_of_log_entries)}.jsonl"
399
+ path_in_repo = f"{BATCH_LOG_FOLDER}/{identifier_safe}/{batch_filename}"
400
+ jsonl_content = ""
401
+ for log_entry in list_of_log_entries:
402
+ try:
403
+ if isinstance(log_entry, dict): jsonl_content += json.dumps(log_entry, ensure_ascii=False) + "\n"
404
+ else: print(f"警告:批量保存选择日志时,条目非字典:{log_entry}")
405
+ except Exception as json_err:
406
+ print(f"错误:批量保存选择日志序列化单条时出错: {log_entry}. 错误: {json_err}")
407
+ jsonl_content += json.dumps({"error": "serialization_failed_in_batch_user_log", "original_data_preview": str(log_entry)[:100],"timestamp": datetime.now().isoformat()}, ensure_ascii=False) + "\n"
408
+
409
+ if not jsonl_content.strip():
410
+ print(f"用户 {identifier_safe} (批次 {batch_identifier}) 无可序列化选择日志。")
411
+ return True # 没有内容可保存也算“成功”
412
+
413
+ log_bytes = jsonl_content.encode('utf-8')
414
+ file_like_object = io.BytesIO(log_bytes)
415
+ print(f"准备批量上传选择日志文件: {path_in_repo} ({len(log_bytes)} bytes)")
416
+ api.upload_file(
417
+ path_or_fileobj=file_like_object,
418
+ path_in_repo=path_in_repo,
419
+ repo_id=DATASET_REPO_ID,
420
+ repo_type="dataset",
421
+ commit_message=f"Batch user choice logs for {identifier_safe}, batch_id {batch_identifier} ({len(list_of_log_entries)} entries)"
422
+ )
423
+ print(f"批量选择日志已成功保存到 HF Dataset: {DATASET_REPO_ID}/{path_in_repo}")
424
+ return True
425
+ except HfHubHTTPError as hf_http_error:
426
+ print(f"批量保存选��日志到 Hugging Face Dataset 时发生 HTTP 错误 (可能被限流或权限问题): {hf_http_error}")
427
+ traceback.print_exc()
428
+ return False
429
+ except Exception as e:
430
+ print(f"批量保存选择日志 (user {identifier_safe}, batch_id {batch_identifier}) 失败: {e}")
431
+ traceback.print_exc()
432
+ return False
433
+
434
+
435
+ # ==== 主要的 Gradio 事件处理函数 ====
436
+ def process_experiment_step(
437
+ s_trial_idx_val, s_run_no_val, s_user_logs_val, s_current_trial_data_val, s_user_session_id_val,
438
+ s_current_run_image_list_val, s_num_trials_this_run_val,
439
+ action_type=None, choice_value=None, request: gr.Request = None
440
+ ):
441
+ global master_image_list, NUM_TRIALS_PER_RUN, outputs_ui_components_definition, LOG_BATCH_SIZE
442
+ global REPEAT_SINGLE_TARGET_FOR_TESTING, NUM_REPEATED_TRIALS_FOR_TESTING
443
+ global exhausted_target_images, global_history_has_unsaved_changes
444
+
445
+ output_s_trial_idx = s_trial_idx_val; output_s_run_no = s_run_no_val
446
+ 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 {}
447
+ output_s_user_session_id = s_user_session_id_val; output_s_current_run_image_list = list(s_current_run_image_list_val)
448
+ output_s_num_trials_this_run = s_num_trials_this_run_val
449
+ user_ip_fallback = request.client.host if request else "unknown_ip"
450
+ user_identifier_for_logging = output_s_user_session_id if output_s_user_session_id else user_ip_fallback
451
+
452
+ # outputs_ui_components_definition 长度现在是 11
453
+ # (target_img, left_img, right_img, left_lbl, right_lbl, status_text, progress_text, btn_start, btn_left, btn_right, file_out_placeholder)
454
+ len_ui_outputs = len(outputs_ui_components_definition)
455
+
456
+ def create_ui_error_tuple(message, progress_msg_text, stop_experiment=False):
457
+ btn_start_interactive = not stop_experiment # 如果停止实验,开始按钮也禁用
458
+ btn_choices_interactive = not stop_experiment
459
+ return (gr.update(visible=False),) * 3 + \
460
+ ("", "") + \
461
+ (message, progress_msg_text) + \
462
+ (gr.update(interactive=btn_start_interactive), gr.update(interactive=btn_choices_interactive), gr.update(interactive=btn_choices_interactive)) + \
463
+ (gr.update(visible=False),)
464
+
465
+ def create_no_change_tuple(): return (gr.update(),) * len_ui_outputs
466
+ user_id_display_text = output_s_user_session_id if output_s_user_session_id else "用户ID待分配"
467
+
468
+ if action_type == "record_choice":
469
+ if output_s_current_trial_data.get("data") and output_s_current_trial_data["data"].get("left_internal_label"):
470
+ # ... (log_entry 创建逻辑不变) ...
471
+ 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"])
472
+ parsed_chosen_method, parsed_chosen_subject, parsed_chosen_filename = "N/A", "N/A", "N/A"
473
+ if chosen_internal_label == "目标图像": parsed_chosen_method, parsed_chosen_subject, parsed_chosen_filename = "TARGET", "GT", output_s_current_trial_data["data"]["image_id"]
474
+ else:
475
+ parts = chosen_internal_label.split('/');
476
+ if len(parts) == 3: parsed_chosen_method, parsed_chosen_subject, parsed_chosen_filename = parts[0].strip(), parts[1].strip(), parts[2].strip()
477
+ elif len(parts) == 2: parsed_chosen_method, parsed_chosen_subject = parts[0].strip(), parts[1].strip()
478
+ elif len(parts) == 1: parsed_chosen_method = parts[0].strip()
479
+ log_entry = {
480
+ "timestamp": datetime.now().isoformat(), "user_identifier": user_identifier_for_logging, "run_no": output_s_run_no,
481
+ "image_id": output_s_current_trial_data["data"]["image_id"],
482
+ "left_internal_label": output_s_current_trial_data["data"]["left_internal_label"],
483
+ "right_internal_label": output_s_current_trial_data["data"]["right_internal_label"],
484
+ "chosen_side": choice_value, "chosen_internal_label": chosen_internal_label,
485
+ "chosen_method": parsed_chosen_method, "chosen_subject": parsed_chosen_subject, "chosen_filename": parsed_chosen_filename,
486
+ "trial_sequence_in_run": output_s_current_trial_data["data"]["cur_no"],
487
+ "is_sentinel": output_s_current_trial_data["data"]["is_sentinel"]
488
+ }
489
+ output_s_user_logs.append(log_entry)
490
+ print(f"用户 {user_identifier_for_logging} 记录选择 (img: {log_entry['image_id']})。当前批次日志数: {len(output_s_user_logs)}")
491
+
492
+ # 尝试批量保存日志
493
+ if len(output_s_user_logs) >= LOG_BATCH_SIZE:
494
+ print(f"累积用户选择日志达到 {LOG_BATCH_SIZE} 条,准备批量保存...")
495
+ batch_id_for_filename = f"run{output_s_run_no}_trialidx{output_s_trial_idx}_logcount{len(output_s_user_logs)}"
496
+ user_logs_save_success = save_collected_logs_batch(list(output_s_user_logs), user_identifier_for_logging, batch_id_for_filename)
497
+ if user_logs_save_success:
498
+ print("批量用户选择日志已成功(或尝试)保存,将清空累积的用户选择日志列表。")
499
+ output_s_user_logs = []
500
+ else:
501
+ print("严重错误:批量用户选择日志保存失败。实验无法继续。")
502
+ # 更新UI以显示错误并停止实验
503
+ error_message_ui = "错误:日志保存失败,可能是网络问题或API限流。实验已停止,请联系管理员。"
504
+ progress_message_ui = f"用户ID: {user_id_display_text} | 实验因错误停止在第 {output_s_run_no} 轮,试验 {output_s_trial_idx+1}"
505
+ error_ui_updates = create_ui_error_tuple(error_message_ui, progress_message_ui, stop_experiment=True)
506
+ return output_s_trial_idx, output_s_run_no, output_s_user_logs, output_s_current_trial_data, \
507
+ output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *error_ui_updates
508
+
509
+ # 尝试保存全局历史
510
+ if global_history_has_unsaved_changes:
511
+ print("检测到全局图片对历史自上次保存后有更新,将一并保存...")
512
+ if not save_global_shown_pairs():
513
+ print("严重错误:全局图片对历史保存失败。实验无法继续。")
514
+ error_message_ui = "错误:全局历史数据保存失败。实验已停止,请联系管理员。"
515
+ progress_message_ui = f"用户ID: {user_id_display_text} | 实验因错误停止在第 {output_s_run_no} 轮,试验 {output_s_trial_idx+1}"
516
+ error_ui_updates = create_ui_error_tuple(error_message_ui, progress_message_ui, stop_experiment=True)
517
+ return output_s_trial_idx, output_s_run_no, output_s_user_logs, output_s_current_trial_data, \
518
+ output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *error_ui_updates
519
+ else:
520
+ print(f"用户 {user_identifier_for_logging} 错误:记录选择时当前试验数据为空或缺少internal_label!")
521
+ # ... (这里的错误处理保持不变,因为它不是API保存错误) ...
522
+ 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) # 这种错误不一定停止实验
523
+ 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
524
+
525
+ # ... (start_experiment 和后续的试验获取逻辑基本保持不变) ...
526
+ # --- 在 start_experiment 开始新轮次前,或在轮次结束时,也需要检查并尝试保存剩余日志和全局历史 ---
527
+ if action_type == "start_experiment":
528
+ is_first = (output_s_num_trials_this_run == 0 and output_s_trial_idx == 0 and output_s_run_no == 1)
529
+ is_completed_for_restart = (output_s_num_trials_this_run > 0 and output_s_trial_idx >= output_s_num_trials_this_run)
530
+
531
+ if is_completed_for_restart: # 如果是完成一轮后准备开始下一轮
532
+ if output_s_user_logs: # 尝试保存上一轮剩余的日志
533
+ print(f"轮次 {output_s_run_no-1} 结束,尝试保存剩余的 {len(output_s_user_logs)} 条用户选择日志...")
534
+ batch_id_for_filename = f"run{output_s_run_no-1}_final_logcount{len(output_s_user_logs)}"
535
+ if not save_collected_logs_batch(list(output_s_user_logs), user_identifier_for_logging, batch_id_for_filename):
536
+ print("严重错误:保存上一轮剩余用户选择日志失败。实验无法继续。")
537
+ error_message_ui = "错误:日志保存失败。实验已停止,请联系管理员。"
538
+ progress_message_ui = f"用户ID: {user_id_display_text} | 实验因错误停止"
539
+ error_ui_updates = create_ui_error_tuple(error_message_ui, progress_message_ui, stop_experiment=True)
540
+ return output_s_trial_idx, output_s_run_no-1, output_s_user_logs, output_s_current_trial_data, \
541
+ output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *error_ui_updates
542
+ output_s_user_logs = [] # 清空
543
+
544
+ if global_history_has_unsaved_changes:
545
+ print("轮次结束,尝试保存全局图片对历史...")
546
+ if not save_global_shown_pairs():
547
+ print("严重错误:全局历史数据保存失败。实验无法继续。")
548
+ error_message_ui = "错误��全局历史数据保存失败。实验已停止,请联系管理员。"
549
+ progress_message_ui = f"用户ID: {user_id_display_text} | 实验因错误停止"
550
+ error_ui_updates = create_ui_error_tuple(error_message_ui, progress_message_ui, stop_experiment=True)
551
+ return output_s_trial_idx, output_s_run_no-1, output_s_user_logs, output_s_current_trial_data, \
552
+ output_s_user_session_id, output_s_current_run_image_list, output_s_num_trials_this_run, *error_ui_updates
553
+
554
+ # ... (后续的 start_experiment 逻辑不变,如选择图片,重置状态等) ...
555
+ if is_first or is_completed_for_restart:
556
+ if is_completed_for_restart: output_s_run_no += 1
557
+ available_master_images = [img for img in master_image_list if img not in exhausted_target_images]
558
+ print(f"开始轮次 {output_s_run_no}: 从 {len(master_image_list)}个总目标图片中筛选,可用图片 {len(available_master_images)}个 (已排除 {len(exhausted_target_images)}个已耗尽图片).")
559
+ if not available_master_images:
560
+ # ... (所有图片耗尽的逻辑不变) ...
561
+ msg = "所有目标图片的所有唯一图片对均已展示完毕!感谢您的参与。"
562
+ prog_text = f"用户ID: {user_id_display_text} | 实验完成!"
563
+ # 在这里,如果还有未保存的日志,也应该尝试保存
564
+ if output_s_user_logs:
565
+ print(f"最终轮次结束,尝试保存剩余的 {len(output_s_user_logs)} 条用户选择日志...")
566
+ batch_id_for_filename = f"run{output_s_run_no-1}_final_logcount{len(output_s_user_logs)}" # 使用上一轮的run_no
567
+ save_collected_logs_batch(list(output_s_user_logs), user_identifier_for_logging, batch_id_for_filename) # 即使失败,也继续显示完成信息
568
+ output_s_user_logs = []
569
+ if global_history_has_unsaved_changes:
570
+ print("实验最终结束,尝试保存全局图片对历史...")
571
+ save_global_shown_pairs() # 即使失败,也继续
572
+
573
+ ui_updates = list(create_ui_error_tuple(msg, prog_text, stop_experiment=True)) # 停止实验
574
+ return 0, output_s_run_no, [], {}, output_s_user_session_id, [], 0, *tuple(ui_updates)
575
+
576
+ # ... (后续 start_experiment 逻辑:选择图片,重置 trial_idx, user_logs, current_trial_data 等)
577
+ if REPEAT_SINGLE_TARGET_FOR_TESTING and available_master_images:
578
+ print(f"测试模式 (重复单一目标图) 已激活。")
579
+ single_image_to_repeat = available_master_images[0]
580
+ output_s_current_run_image_list = [single_image_to_repeat] * NUM_REPEATED_TRIALS_FOR_TESTING
581
+ output_s_num_trials_this_run = NUM_REPEATED_TRIALS_FOR_TESTING
582
+ print(f"测试模式:本轮将重复目标图片 '{single_image_to_repeat}' 共 {output_s_num_trials_this_run} 次。")
583
+ else:
584
+ num_really_avail = len(available_master_images)
585
+ current_run_max_trials = NUM_TRIALS_PER_RUN
586
+ run_size = min(num_really_avail, current_run_max_trials)
587
+ if run_size == 0:
588
+ error_ui = create_ui_error_tuple("错误: 可用图片采样数为0!", f"用户ID: {user_id_display_text} | 进度: 0/0", stop_experiment=False) # 不一定停止
589
+ return 0, output_s_run_no, output_s_user_logs, {}, output_s_user_session_id, [], 0, *error_ui
590
+ output_s_current_run_image_list = random.sample(available_master_images, run_size)
591
+ output_s_num_trials_this_run = run_size
592
+
593
+ output_s_trial_idx = 0
594
+ output_s_current_trial_data = {} # 新轮次重置当前试验数据
595
+ # output_s_user_logs 已经在 is_completed_for_restart 分支中被清空(如果保存成功)
596
+ # 如果是 is_first,它本身就是空的
597
+ if is_first: # 只有第一次启动时分配新用户ID
598
+ timestamp_str = datetime.now().strftime('%Y%m%d%H%M%S%f'); random_val = random.randint(10000, 99999)
599
+ if not output_s_user_session_id: # 确保只在没有 session_id 时创建 (例如从 handle_agree_and_start 那里已经有了)
600
+ output_s_user_session_id = f"user_{timestamp_str}_{random_val}"; user_identifier_for_logging = output_s_user_session_id
601
+ else:
602
+ user_identifier_for_logging = output_s_user_session_id # 使用从用户信息处获得的ID
603
+ print(f"用户会话ID: {output_s_user_session_id}")
604
+ print(f"开始/继续轮次 {output_s_run_no} (用户ID: {output_s_user_session_id}). 本轮共 {output_s_num_trials_this_run} 个试验。")
605
+ else: # 中途点击开始
606
+ print(f"用户 {user_identifier_for_logging} 在第 {output_s_run_no} 轮,试验 {output_s_trial_idx} 点击开始,但轮次未完成。忽略。")
607
+ no_change_ui = create_no_change_tuple()
608
+ 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
609
+
610
+
611
+ # --- 获取下一个试验并更新UI的逻辑 ---
612
+ # ... (这部分逻辑与您上一版包含while循环尝试获取有效试验的代码基本相同) ...
613
+ # ... (确保所有 yield 和 return 都返回7个状态变量 + 11个UI更新) ...
614
+ # ... (并且在循环结束仍未找到 trial_info 时,也尝试保存剩余日志和全局历史) ...
615
+ current_actual_trial_index_for_get_next = output_s_trial_idx
616
+ # (上一段 action_type == "record_choice" 已经处理了日志保存,所以这里主要处理 trial_info 获取)
617
+
618
+ if current_actual_trial_index_for_get_next >= output_s_num_trials_this_run and output_s_num_trials_this_run > 0:
619
+ # (这个条件实际上在 record_choice 后,或 start_experiment 的 is_completed_for_restart 后,output_s_trial_idx 可能已经等于或超过)
620
+ # (这里的逻辑主要是为了在 yield 返回前,如果恰好是轮次结束,则正确显示结束信息)
621
+ # (大部分轮次结束逻辑已移到 action_type == "record_choice" 和 "start_experiment" 的 is_completed_for_restart 部分)
622
+ print(f"调试: 在获取下一个试验前,检测到轮次 {output_s_run_no} 可能已结束 (idx: {current_actual_trial_index_for_get_next} >= num_trials: {output_s_num_trials_this_run})")
623
+ # (如果确实结束,应该已经在前面返回了,这里是一个保险或冗余检查)
624
+ 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} 轮 🎉"
625
+ ui_updates = list(create_ui_error_tuple(f"🎉 第 {output_s_run_no} 轮完成!请点击“开始试验 / 下一轮”继续。", prog_text, stop_experiment=False)) # 完成一轮不立即停止
626
+ ui_updates[7]=gr.update(interactive=True); ui_updates[8]=gr.update(interactive=False); ui_updates[9]=gr.update(interactive=False)
627
+ 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)
628
+ 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
629
+
630
+
631
+ if not output_s_current_run_image_list or output_s_num_trials_this_run == 0:
632
+ error_ui = create_ui_error_tuple("错误: 无法加载试验图片 (列表为空或试验数为0)", f"用户ID: {user_id_display_text} | 进度: N/A", stop_experiment=False)
633
+ return output_s_trial_idx, output_s_run_no, output_s_user_logs, {"data": None}, output_s_user_session_id, [], 0, *error_ui
634
+
635
+ trial_info = None
636
+ next_s_trial_idx_for_state_loop = current_actual_trial_index_for_get_next
637
+
638
+ while next_s_trial_idx_for_state_loop < output_s_num_trials_this_run:
639
+ current_target_image_for_trial = output_s_current_run_image_list[next_s_trial_idx_for_state_loop]
640
+ if current_target_image_for_trial in exhausted_target_images:
641
+ print(f"信息:目标图 '{current_target_image_for_trial}' 已在全局耗尽列表中,跳过此试验。")
642
+ next_s_trial_idx_for_state_loop += 1
643
+ output_s_trial_idx = next_s_trial_idx_for_state_loop
644
+ continue
645
+
646
+ _trial_info_candidate, _returned_next_idx = get_next_trial_info(next_s_trial_idx_for_state_loop, output_s_current_run_image_list, output_s_num_trials_this_run)
647
+
648
+ if _trial_info_candidate is not None:
649
+ trial_info = _trial_info_candidate
650
+ output_s_trial_idx = _returned_next_idx # 更新主状态的 trial_idx
651
+ break
652
+ else:
653
+ print(f"信息:目标图 '{current_target_image_for_trial}' 无法生成有效试验。尝试列表中的下一个。")
654
+ next_s_trial_idx_for_state_loop +=1
655
+ output_s_trial_idx = next_s_trial_idx_for_state_loop
656
+ # exhausted_target_images 应该在 get_next_trial_info 内部被更新
657
+
658
+ if trial_info is None:
659
+ print(f"轮次 {output_s_run_no} 中没有更多可用的有效试验了。结束本轮。")
660
+ # 尝试保存剩余日志
661
+ if output_s_user_logs:
662
+ print(f"轮次 {output_s_run_no} 无更多有效试验,尝试保存剩余 {len(output_s_user_logs)} 条日志...")
663
+ batch_id_for_filename = f"run{output_s_run_no}_no_more_trials_logcount{len(output_s_user_logs)}"
664
+ if not save_collected_logs_batch(list(output_s_user_logs), user_identifier_for_logging, batch_id_for_filename):
665
+ # 即使这里保存失败,也应该让用户知道轮次结束了,但可能需要更强的错误提示
666
+ print("严重错误:保存剩余日志失败。���验可能需要停止。")
667
+ # 可以选择在这里返回一个停止实验的UI更新
668
+ output_s_user_logs = []
669
+ if global_history_has_unsaved_changes:
670
+ print("轮次无更多有效试验,尝试保存全局图片对历史...")
671
+ if not save_global_shown_pairs():
672
+ print("严重错误:全局历史数据保存失败。实验可能需要停止。")
673
+
674
+ 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} 轮 (无更多可用试验)"
675
+ ui_updates = list(create_ui_error_tuple(f"第 {output_s_run_no} 轮因无更多可用试验而结束。请点击“开始试验 / 下一轮”。", prog_text, stop_experiment=False))
676
+ ui_updates[7]=gr.update(interactive=True); ui_updates[8]=gr.update(interactive=False); ui_updates[9]=gr.update(interactive=False)
677
+ 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)
678
+ yield output_s_num_trials_this_run, 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
679
+
680
+ output_s_current_trial_data = {"data": trial_info}
681
+ prog_text = f"用户ID: {output_s_user_session_id} | 进度:{trial_info['cur_no']}/{output_s_num_trials_this_run} | 第 {output_s_run_no} 轮"
682
+
683
+ ui_show_target_updates = list(create_no_change_tuple())
684
+ ui_show_target_updates[0]=gr.update(value=trial_info["target_path"],visible=True); ui_show_target_updates[1]=gr.update(value=None,visible=False); ui_show_target_updates[2]=gr.update(value=None,visible=False)
685
+ ui_show_target_updates[3]=""; ui_show_target_updates[4]=""; ui_show_target_updates[5]="请观察原图…"; ui_show_target_updates[6]=prog_text
686
+ ui_show_target_updates[7]=gr.update(interactive=False); ui_show_target_updates[8]=gr.update(interactive=False); ui_show_target_updates[9]=gr.update(interactive=False)
687
+ 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_target_updates
688
+
689
+ time.sleep(3)
690
+
691
+ ui_show_candidates_updates = list(create_no_change_tuple())
692
+ 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)
693
+ 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
694
+ ui_show_candidates_updates[5]="请选择更像原图的一张"; ui_show_candidates_updates[6]=prog_text
695
+ 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)
696
+ 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
697
+
698
+ # ==== Gradio UI 定义 和 程序入口 (与您上一版相同,包含下载按钮等) ====
699
+ # ... (welcome_page_markdown, handle_agree_and_start, handle_download_history_file) ...
700
+ # ... (with gr.Blocks(...) as demo: ...) ...
701
+ # ... (if __name__ == "__main__": ...) ...
702
+ # (此处省略这部分,因为它们与您包含下载按钮的上一版代码相同)
703
+ def handle_download_history_file(): # 确保这个函数在UI定义之前
704
+ global GLOBAL_HISTORY_FILE
705
+ if os.path.exists(GLOBAL_HISTORY_FILE):
706
+ try:
707
+ if os.path.getsize(GLOBAL_HISTORY_FILE) > 0:
708
+ print(f"准备提供文件下载: {GLOBAL_HISTORY_FILE}")
709
+ return GLOBAL_HISTORY_FILE, gr.update(value=f"点击上面的链接下载 '{os.path.basename(GLOBAL_HISTORY_FILE)}'")
710
+ else:
711
+ print(f"历史文件 '{GLOBAL_HISTORY_FILE}' 为空,不提供下载。")
712
+ return None, gr.update(value=f"提示: 历史文件 '{os.path.basename(GLOBAL_HISTORY_FILE)}' 当前为空。")
713
+ except Exception as e:
714
+ print(f"检查历史文件大小时出错 '{GLOBAL_HISTORY_FILE}': {e}")
715
+ return None, gr.update(value=f"错误: 检查历史文件时出错。")
716
+ else:
717
+ print(f"请求下载历史文件,但文件 '{GLOBAL_HISTORY_FILE}' 未找到。")
718
+ return None, gr.update(value=f"错误: JSON历史文件 '{os.path.basename(GLOBAL_HISTORY_FILE)}' 未找到。请先运行实验以生成数据并触发保存。")
719
+
720
+ welcome_page_markdown = """
721
+ ## 欢迎加入实验!
722
+ 您好!非常感谢您抽出宝贵时间参与我们的视觉偏好评估实验。您的选择将帮助我们改进重建算法,让机器生成的图像��贴近人类视觉体验!
723
+ 1. **实验目的**:通过比较两幅 重建图像 与原始 目标图像 的相似度。
724
+ 2. **操作流程**:
725
+ * 点击下方的「我已阅读并同意开始实验」按钮。
726
+ * 然后点击主实验界面的「开始试验 / 下一轮」按钮。
727
+ * 系统先展示一张 **目标图像**,持续 3 秒。
728
+ * 随后自动切换到 **两张重建图像**。
729
+ * 根据刚才的观察记忆,选出您认为与目标图像最相似的一张。
730
+ * 选择后系统会自动进入下一轮比较。
731
+ 3. **温馨提示**:
732
+ * 请勿刷新或关闭页面,以免中断实验。
733
+ * 若图片加载稍有延迟,请耐心等待;持续异常可联系邮箱 yangminghan@bupt.edu.cn。
734
+ * 本实验将保护您的任何个人隐私信息,所有数据仅用于学术研究,请您认真选择和填写。
735
+ 4. **奖励说明**:
736
+ * 完成全部轮次后,请截图记录您所完成的实验总数(可累积,页面左下角将显示进度,请保证截取到为您分配的ID,轮次)。
737
+ * 将截图发送至邮箱 yangminghan@bupt.edu.cn,我们将在核验后发放奖励。
738
+ 再次感谢您的参与与支持!您每一次认真选择都对我们的研究意义重大。祝您一切顺利,实验愉快!
739
+ """
740
+ def handle_agree_and_start(name, gender, age, education, request: gr.Request):
741
+ error_messages_list = []
742
+ if not name or str(name).strip() == "": error_messages_list.append("姓名 不能为空。")
743
+ if gender is None or str(gender).strip() == "": error_messages_list.append("性别 必须选择。")
744
+ if age is None: error_messages_list.append("年龄 不能为空。")
745
+ elif not (isinstance(age, (int, float)) and 1 <= age <= 120):
746
+ try: num_age = float(age);
747
+ except (ValueError, TypeError): error_messages_list.append("年龄必须是一个有效的数字。")
748
+ else:
749
+ if not (1 <= num_age <= 120): error_messages_list.append("年龄必须在 1 到 120 之间。")
750
+ if education is None or str(education).strip() == "其他": error_messages_list.append("学历 必须选择。")
751
+ if error_messages_list:
752
+ full_error_message = "请修正以下错误:\n" + "\n".join([f"- {msg}" for msg in error_messages_list])
753
+ print(f"用户输入验证失败: {full_error_message}")
754
+ return gr.update(), False, gr.update(visible=True), gr.update(visible=False), full_error_message
755
+ s_name = str(name).strip().replace(" ","_").replace("/","_").replace("\\","_")
756
+ s_gender = str(gender).strip().replace(" ","_").replace("/","_").replace("\\","_")
757
+ s_age = str(int(float(age)))
758
+ s_education = str(education).strip().replace(" ","_").replace("/","_").replace("\\","_")
759
+ user_id_str = f"N-{s_name}_G-{s_gender}_A-{s_age}_E-{s_education}"
760
+ print(f"用户信息收集完毕,生成用户ID: {user_id_str}")
761
+ return user_id_str, True, gr.update(visible=False), gr.update(visible=True), ""
762
+
763
+ with gr.Blocks(css=CSS, title="图像重建主观评估") as demo:
764
+ s_show_experiment_ui = gr.State(False); s_trial_index = gr.State(0); s_run_no = gr.State(1)
765
+ s_user_logs = gr.State([]); s_current_trial_data = gr.State({}); s_user_session_id = gr.State(None)
766
+ s_current_run_image_list = gr.State([]); s_num_trials_this_run = gr.State(0)
767
+
768
+ welcome_container = gr.Column(visible=True)
769
+ experiment_container = gr.Column(visible=False)
770
+
771
+ with welcome_container:
772
+ gr.Markdown(welcome_page_markdown)
773
+ with gr.Row(): user_name_input = gr.Textbox(label="请输入您的姓名或代号 (例如 张三 或 User001)", placeholder="例如:张三 -> ZS"); user_gender_input = gr.Radio(label="性别", choices=["男", "女"])
774
+ with gr.Row(): user_age_input = gr.Number(label="年龄 (请输入1-120的整数)", minimum=1, maximum=120, step=1); user_education_input = gr.Dropdown(label="学历", choices=["其他","初中及以下","高中(含中专)", "大专(含在读)", "本科(含在读)", "硕士(含在读)", "博士(含在读)"])
775
+ welcome_error_msg = gr.Markdown(value="")
776
+ btn_agree_and_start = gr.Button("我已阅读上述说明并同意参与实验")
777
+
778
+ with experiment_container:
779
+ gr.Markdown("## 🧠 图像重建主观评估实验"); gr.Markdown(f"每轮实验大约有 {NUM_TRIALS_PER_RUN} 次比较。")
780
+ with gr.Row():
781
+ 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
782
+ 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
783
+ with gr.Row(): target_img = gr.Image(label="目标图像 (观察3秒后消失)", visible=False, height=400, interactive=False)
784
+ with gr.Row(): status_text = gr.Markdown(value="请点击“开始试验 / 下一轮”按钮。")
785
+ with gr.Row(): progress_text = gr.Markdown() # 初始为空
786
+ with gr.Row():
787
+ btn_start = gr.Button("开始试验 / 下一轮")
788
+ btn_download_json = gr.Button("下载JSON历史记录")
789
+ json_download_output = gr.File(label="下载的文件会在此处提供", interactive=False)
790
+ file_out_placeholder = gr.File(label=" ", visible=False, interactive=False) # 保持这个占位符
791
+
792
+ outputs_ui_components_definition = [
793
+ target_img, left_img, right_img, left_lbl, right_lbl, status_text, progress_text,
794
+ btn_start, btn_left, btn_right, file_out_placeholder
795
+ ] # 确保这里是 11 个组件
796
+ click_inputs_base = [
797
+ s_trial_index, s_run_no, s_user_logs, s_current_trial_data, s_user_session_id,
798
+ s_current_run_image_list, s_num_trials_this_run
799
+ ]
800
+ event_outputs = [
801
+ s_trial_index, s_run_no, s_user_logs, s_current_trial_data, s_user_session_id,
802
+ s_current_run_image_list, s_num_trials_this_run, *outputs_ui_components_definition
803
+ ]
804
+
805
+ btn_agree_and_start.click(fn=handle_agree_and_start, inputs=[user_name_input, user_gender_input, user_age_input, user_education_input], outputs=[s_user_session_id, s_show_experiment_ui, welcome_container, experiment_container, welcome_error_msg])
806
+ btn_start.click(fn=partial(process_experiment_step, action_type="start_experiment"), inputs=click_inputs_base, outputs=event_outputs, queue=True)
807
+ btn_left.click(fn=partial(process_experiment_step, action_type="record_choice", choice_value="left"), inputs=click_inputs_base, outputs=event_outputs, queue=True)
808
+ btn_right.click(fn=partial(process_experiment_step, action_type="record_choice", choice_value="right"), inputs=click_inputs_base, outputs=event_outputs, queue=True)
809
+ btn_download_json.click(fn=handle_download_history_file, inputs=None, outputs=[json_download_output, status_text])
810
+
811
+
812
+
813
+
814
+
815
+
816
+
817
+
818
+
819
+
820
+
821
+
822
+
823
+
824
+
825
+
826
 
 
827
 
 
 
 
828
 
 
829
 
830
+ if __name__ == "__main__":
831
+ # ... (您的 __main__ 部分代码保持不变,包含所有路径检查和 Gradio 启动) ...
832
+ if not master_image_list: print("\n关键错误:程序无法启动,因无目标图片。"); exit()
833
+ else:
834
+ print(f"从 '{TARGET_DIR}' 加载 {len(master_image_list)} 张目标图片。")
835
+ if not METHOD_ROOTS: print(f"警告: '{BASE_IMAGE_DIR}' 无候选方法子目录。")
836
+ if not SUBJECTS: print("警告: SUBJECTS 列表为空。")
837
+ print(f"用户选择日志保存到 Dataset: '{DATASET_REPO_ID}' 的 '{BATCH_LOG_FOLDER}/ 文件夹") # 修正日志文件夹描述
838
+ if not os.getenv("HF_TOKEN"): print("警告: HF_TOKEN 未设置。日志无法保存到Hugging Face Dataset。\n 请在 Space Secrets 中设置 HF_TOKEN。")
839
+ else: print("HF_TOKEN 已找到。")
840
+ print(f"全局图片对历史将从 '{GLOBAL_HISTORY_FILE}' 加载/保存到此文件。")
841
 
842
+ allowed_paths_list = []
843
+ image_base_dir_to_allow = BASE_IMAGE_DIR
844
+ if os.path.exists(image_base_dir_to_allow) and os.path.isdir(image_base_dir_to_allow):
845
+ allowed_paths_list.append(os.path.abspath(image_base_dir_to_allow))
846
+ else:
847
+ print(f"关键警告:图片基础目录 '{image_base_dir_to_allow}' 不存在或非目录。")
848
 
849
+ if os.path.exists(PERSISTENT_STORAGE_BASE) and os.path.isdir(PERSISTENT_STORAGE_BASE):
850
+ allowed_paths_list.append(os.path.abspath(PERSISTENT_STORAGE_BASE))
851
+ else:
852
+ print(f"警告:持久化存储基础目录 '{PERSISTENT_STORAGE_BASE}' 不存在。JSON历史文件下载可能受影响。")
853
+ try:
854
+ os.makedirs(PERSISTENT_STORAGE_BASE, exist_ok=True)
855
+ print(f"信息:已尝试创建目录 '{PERSISTENT_STORAGE_BASE}'。")
856
+ if os.path.exists(PERSISTENT_STORAGE_BASE) and os.path.isdir(PERSISTENT_STORAGE_BASE):
857
+ allowed_paths_list.append(os.path.abspath(PERSISTENT_STORAGE_BASE))
858
+ except Exception as e_mkdir_main:
859
+ print(f"错误:在 main 中创建目录 '{PERSISTENT_STORAGE_BASE}' 失败: {e_mkdir_main}")
860
 
861
+ final_allowed_paths = list(set(allowed_paths_list)) # 去重
862
+ if final_allowed_paths:
863
+ print(f"Gradio `demo.launch()` 配置最终 allowed_paths: {final_allowed_paths}")
864
+ else:
865
+ print("警告:没有有效的 allowed_paths 被配置。Gradio文件访问可能受限。")
 
 
 
866
 
867
+ print("启动 Gradio 应用...")
868
+ if final_allowed_paths:
869
+ demo.launch(allowed_paths=final_allowed_paths)
870
+ else:
871
+ demo.launch()