Newbyl commited on
Commit
f817b37
·
1 Parent(s): bb261db

modif app

Browse files
Files changed (4) hide show
  1. .DS_Store +0 -0
  2. .gitignore +4 -1
  3. app.py +443 -260
  4. votes.csv +2 -0
.DS_Store CHANGED
Binary files a/.DS_Store and b/.DS_Store differ
 
.gitignore CHANGED
@@ -1,2 +1,5 @@
1
  common_videos.zip
2
- common_videos/aniportrait
 
 
 
 
1
  common_videos.zip
2
+ common_videos/aniportrait
3
+ app_local.py
4
+ reduce_size.py
5
+ compress.py
app.py CHANGED
@@ -1,275 +1,458 @@
1
  import os
2
- import gradio as gr
3
  import random
4
- import json
5
- import itertools
6
  from pathlib import Path
 
7
 
8
- # List of methods that already have results
9
- EXISTING_METHODS = ["liveportrait", "controltalk", "lia", "hallo2", "echomimic", "dimitra", "sadtalker", "wav2Lip"]
10
-
11
- # List of new methods to compare
12
- NEW_METHODS = ["fom", "xportrait", "mcnet", "emoortrait", "dagan", "liax", "omniavatar", "real3d"]
13
 
14
- # Number of videos per new method to compare
15
- VIDEOS_PER_NEW_METHOD = 23
 
16
 
17
- # File to save results
18
- RESULTS_FILE = "comparison_results.json"
19
 
20
- def find_videos_by_method(base_dir):
21
- """Find all videos in the method folders."""
22
- videos_by_method = {}
23
-
24
- # Check each method directory
25
- for method in EXISTING_METHODS + NEW_METHODS:
26
- method_dir = os.path.join(base_dir, method)
27
- if not os.path.exists(method_dir):
28
- continue
29
-
30
- videos = []
31
- # Walk through the directory structure to find all video files
32
- for root, _, files in os.walk(method_dir):
33
- for file in files:
34
- if file.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')):
35
- videos.append(os.path.join(root, file))
36
-
37
- if videos:
38
- videos_by_method[method] = videos
39
-
40
- return videos_by_method
41
-
42
- def generate_comparison_pairs(videos_by_method):
43
- """Generate pairs of videos between new methods and existing methods."""
44
- pairs = []
45
-
46
- for new_method in NEW_METHODS:
47
- if new_method not in videos_by_method:
48
  continue
49
-
50
- for existing_method in EXISTING_METHODS:
51
- if existing_method not in videos_by_method:
52
- continue
53
-
54
- # Get video basenames to match videos across methods
55
- new_method_videos = {os.path.basename(v): v for v in videos_by_method[new_method]}
56
- existing_method_videos = {os.path.basename(v): v for v in videos_by_method[existing_method]}
57
-
58
- # Find common videos between the two methods
59
- common_videos = set(new_method_videos.keys()) & set(existing_method_videos.keys())
60
-
61
- for video_name in common_videos:
62
- new_video = new_method_videos[video_name]
63
- existing_video = existing_method_videos[video_name]
64
-
65
- # Randomly order the videos (left/right)
66
- if random.choice([True, False]):
67
- pairs.append({
68
- 'left_video': new_video,
69
- 'right_video': existing_video,
70
- 'left_method': new_method,
71
- 'right_method': existing_method,
72
- 'video_name': video_name
73
- })
74
- else:
75
- pairs.append({
76
- 'left_video': existing_video,
77
- 'right_video': new_video,
78
- 'left_method': existing_method,
79
- 'right_method': new_method,
80
- 'video_name': video_name
81
- })
82
-
83
- # Shuffle pairs for unbiased comparison
84
- random.shuffle(pairs)
85
- return pairs
86
-
87
- def load_results():
88
- """Load existing comparison results."""
89
- if os.path.exists(RESULTS_FILE):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  try:
91
- with open(RESULTS_FILE, 'r') as f:
92
- return json.load(f)
93
- except json.JSONDecodeError:
94
  pass
95
- return {"comparisons": [], "count": 0}
96
-
97
- def save_result(results, comparison_data):
98
- """Save the comparison result."""
99
- results["comparisons"].append(comparison_data)
100
- results["count"] += 1
101
-
102
- with open(RESULTS_FILE, 'w') as f:
103
- json.dump(results, f, indent=4)
104
-
105
- return results
106
-
107
- def get_total_expected_comparisons():
108
- """Calculate the total number of comparisons needed."""
109
- return len(NEW_METHODS) * VIDEOS_PER_NEW_METHOD
110
-
111
- def create_app(videos_dir):
112
- """Create the Gradio app for video comparison."""
113
- # Load existing results if any
114
- results = load_results()
115
-
116
- # Find videos in the directory structure
117
- videos_by_method = find_videos_by_method(videos_dir)
118
-
119
- # Generate pairs for comparison
120
- all_pairs = generate_comparison_pairs(videos_by_method)
121
-
122
- # Calculate the total expected comparisons
123
- total_expected = get_total_expected_comparisons()
124
-
125
- # Track the current state
126
- state = {
127
- "current_pair_index": 0,
128
- "results": results,
129
- "all_pairs": all_pairs,
130
- "total_expected": total_expected
131
- }
132
-
133
- def update_ui_state(state):
134
- """Update the UI based on current state."""
135
- if state["results"]["count"] >= state["total_expected"]:
136
- return {
137
- "left_video": None,
138
- "right_video": None,
139
- "status": f"Completed all {state['total_expected']} comparisons!",
140
- "left_button_visible": False,
141
- "right_button_visible": False,
142
- "done_visible": True,
143
- "progress": 100
144
- }
145
-
146
- if state["current_pair_index"] >= len(state["all_pairs"]):
147
- return {
148
- "left_video": None,
149
- "right_video": None,
150
- "status": "No more pairs to compare, but target count not reached.",
151
- "left_button_visible": False,
152
- "right_button_visible": False,
153
- "done_visible": True,
154
- "progress": (state["results"]["count"] / state["total_expected"]) * 100
155
- }
156
-
157
- current_pair = state["all_pairs"][state["current_pair_index"]]
158
- progress_percent = (state["results"]["count"] / state["total_expected"]) * 100
159
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  return {
161
- "left_video": current_pair["left_video"],
162
- "right_video": current_pair["right_video"],
163
- "status": f"Compare videos: {state['results']['count']}/{state['total_expected']} completed ({progress_percent:.1f}%)",
164
- "left_button_visible": True,
165
- "right_button_visible": True,
166
- "done_visible": False,
167
- "progress": progress_percent
 
 
 
 
168
  }
169
-
170
- def handle_choice(choice, state):
171
- """Handle user's choice between videos."""
172
- if state["results"]["count"] >= state["total_expected"]:
173
- return state, update_ui_state(state)
174
-
175
- if state["current_pair_index"] < len(state["all_pairs"]):
176
- current_pair = state["all_pairs"][state["current_pair_index"]]
177
-
178
- # Record the comparison result
179
- comparison_data = {
180
- "video_name": current_pair["video_name"],
181
- "choice": choice,
182
- "left_method": current_pair["left_method"],
183
- "right_method": current_pair["right_method"],
184
- "timestamp": str(import_datetime().now())
185
- }
186
-
187
- # Update results
188
- state["results"] = save_result(state["results"], comparison_data)
189
- state["current_pair_index"] += 1
190
-
191
- return state, update_ui_state(state)
192
-
193
- def import_datetime():
194
- """Import datetime module on demand."""
195
- import datetime
196
- return datetime
197
-
198
- with gr.Blocks() as demo:
199
- gr.Markdown("# Video Comparison App")
200
- gr.Markdown("Compare the quality of videos generated by different methods")
201
-
202
- # State components
203
- state_data = gr.State(state)
204
-
205
- # Display components
206
- with gr.Row():
207
- with gr.Column():
208
- left_video = gr.Video(label="Video A")
209
- left_button = gr.Button("Choose Video A", variant="primary")
210
- with gr.Column():
211
- right_video = gr.Video(label="Video B")
212
- right_button = gr.Button("Choose Video B", variant="primary")
213
-
214
- progress_bar = gr.Progress()
215
- status_text = gr.Markdown("")
216
- done_text = gr.Markdown("All comparisons completed! Thank you for your participation.", visible=False)
217
-
218
- # Initialize UI with first pair
219
- ui_state = update_ui_state(state)
220
- left_video.value = ui_state["left_video"]
221
- right_video.value = ui_state["right_video"]
222
- status_text.value = ui_state["status"]
223
- left_button.visible = ui_state["left_button_visible"]
224
- right_button.visible = ui_state["right_button_visible"]
225
- done_text.visible = ui_state["done_visible"]
226
-
227
- # Button click handlers
228
- def handle_left_click(state_data):
229
- new_state, ui_state = handle_choice("left", state_data)
230
- return [
231
- new_state,
232
- ui_state["left_video"],
233
- ui_state["right_video"],
234
- ui_state["status"],
235
- ui_state["left_button_visible"],
236
- ui_state["right_button_visible"],
237
- ui_state["done_visible"],
238
- ui_state["progress"]
239
- ]
240
-
241
- def handle_right_click(state_data):
242
- new_state, ui_state = handle_choice("right", state_data)
243
- return [
244
- new_state,
245
- ui_state["left_video"],
246
- ui_state["right_video"],
247
- ui_state["status"],
248
- ui_state["left_button_visible"],
249
- ui_state["right_button_visible"],
250
- ui_state["done_visible"],
251
- ui_state["progress"]
252
- ]
253
-
254
- left_button.click(
255
- handle_left_click,
256
- inputs=[state_data],
257
- outputs=[state_data, left_video, right_video, status_text, left_button, right_button, done_text, progress_bar]
258
- )
259
-
260
- right_button.click(
261
- handle_right_click,
262
- inputs=[state_data],
263
- outputs=[state_data, left_video, right_video, status_text, left_button, right_button, done_text, progress_bar]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  )
265
-
266
- return demo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
  if __name__ == "__main__":
269
- # Set the directory containing the method folders
270
- videos_directory = "common_videos"
271
- if not videos_directory:
272
- videos_directory = "." # Default to current directory
273
-
274
- app = create_app(videos_directory)
275
- app.launch()
 
1
  import os
 
2
  import random
3
+ import threading
4
+ from datetime import datetime
5
  from pathlib import Path
6
+ from typing import Dict, List, Optional, Set, Tuple
7
 
8
+ import gradio as gr
9
+ import pandas as pd
 
 
 
10
 
11
+ # ----------------------------
12
+ # Configuration and constants
13
+ # ----------------------------
14
 
15
+ # Folder containing one subfolder per method with identically named video files
16
+ COMMON_VIDEOS_DIR = Path(__file__).resolve().parent / "common_videos"
17
 
18
+ # CSV file for persistent votes (prefer HF Spaces persistent storage if available)
19
+ def _resolve_votes_csv() -> Path:
20
+ candidates = [
21
+ Path(os.getenv("HF_DATA_DIR", "/data")),
22
+ Path(__file__).resolve().parent,
23
+ ]
24
+ for d in candidates:
25
+ try:
26
+ d.mkdir(parents=True, exist_ok=True)
27
+ test = d / ".write_test"
28
+ with open(test, "w") as f:
29
+ f.write("ok")
30
+ try:
31
+ test.unlink()
32
+ except Exception:
33
+ pass
34
+ return d / "votes.csv"
35
+ except Exception:
 
 
 
 
 
 
 
 
 
 
36
  continue
37
+ return Path(__file__).resolve().parent / "votes.csv"
38
+
39
+ VOTES_CSV = _resolve_votes_csv()
40
+
41
+ # Methods
42
+ GROUND_TRUTH = "used_videos"
43
+ OLD_METHODS = [
44
+ "liveportrait", "controltalk", "lia", "hallo2",
45
+ "echomimic_acc", "dimitra", "sadtalker", "wav2lip",
46
+ ]
47
+ NEW_METHODS = [
48
+ "fom_gen", "xportrait", "mcnet", "emoportrait",
49
+ "dagan", "liax", "omniavatar", "real3d",
50
+ ]
51
+
52
+ # Study parameters
53
+ VOTES_PER_PAIR = 23
54
+ STOP_TOTAL = 100 * VOTES_PER_PAIR # 100 new pairs * 23 votes each
55
+
56
+ # Allowed video extensions
57
+ VIDEO_EXTS = {".mp4", ".mov", ".webm", ".avi", ".mkv"}
58
+
59
+ # Thread lock for safe CSV writes
60
+ _write_lock = threading.Lock()
61
+
62
+ # ----------------------------
63
+ # Global in-memory state
64
+ # ----------------------------
65
+
66
+ # Mapping method -> set of available video filenames (e.g., "abc.mp4")
67
+ METHOD_VIDEOS: Dict[str, Set[str]] = {}
68
+
69
+ # All required unordered pairs as canonical tuples (m1, m2) sorted lexicographically
70
+ ALL_REQUIRED_PAIRS: List[Tuple[str, str]] = []
71
+
72
+ # Per-pair vote counts from CSV (key: (m1, m2) sorted tuple)
73
+ PAIR_COUNTS: Dict[Tuple[str, str], int] = {}
74
+
75
+ # Total votes recorded so far
76
+ TOTAL_VOTES: int = 0
77
+
78
+
79
+ # ----------------------------
80
+ # Utility functions
81
+ # ----------------------------
82
+
83
+ def ensure_votes_csv() -> pd.DataFrame:
84
+ """Ensure votes.csv exists with headers and load it."""
85
+ columns = ["method1", "method2", "video_name", "winner", "timestamp"]
86
+ if not VOTES_CSV.exists():
87
+ df = pd.DataFrame(columns=columns)
88
+ df.to_csv(VOTES_CSV, index=False)
89
+ return df
90
+ try:
91
+ df = pd.read_csv(VOTES_CSV)
92
+ # Normalize columns if needed
93
+ missing = [c for c in columns if c not in df.columns]
94
+ if missing:
95
+ for c in missing:
96
+ df[c] = None
97
+ df = df[columns]
98
+ df.to_csv(VOTES_CSV, index=False)
99
+ return df[columns]
100
+ except Exception:
101
+ # If corrupted, back up and start fresh
102
+ backup = VOTES_CSV.with_suffix(".bak.csv")
103
  try:
104
+ VOTES_CSV.replace(backup)
105
+ except Exception:
 
106
  pass
107
+ df = pd.DataFrame(columns=columns)
108
+ df.to_csv(VOTES_CSV, index=False)
109
+ return df
110
+
111
+
112
+ def scan_method_videos() -> Dict[str, Set[str]]:
113
+ """Scan common_videos/ for each method, returning mapping method -> set of filenames."""
114
+ methods = [GROUND_TRUTH] + OLD_METHODS + NEW_METHODS
115
+ mapping: Dict[str, Set[str]] = {}
116
+ for m in methods:
117
+ folder = COMMON_VIDEOS_DIR / m
118
+ if not folder.exists() or not folder.is_dir():
119
+ mapping[m] = set()
120
+ continue
121
+ files = set()
122
+ for p in folder.iterdir():
123
+ if p.is_file() and p.suffix.lower() in VIDEO_EXTS:
124
+ files.add(p.name)
125
+ mapping[m] = files
126
+ return mapping
127
+
128
+
129
+ def generate_required_pairs() -> List[Tuple[str, str]]:
130
+ """Generate the 100 required pairs: NEW vs NEW, NEW vs OLD, NEW vs GT."""
131
+ pairs: Set[Tuple[str, str]] = set()
132
+
133
+ # NEW vs NEW
134
+ for i in range(len(NEW_METHODS)):
135
+ for j in range(i + 1, len(NEW_METHODS)):
136
+ a, b = sorted((NEW_METHODS[i], NEW_METHODS[j]))
137
+ pairs.add((a, b))
138
+
139
+ # NEW vs OLD
140
+ for n in NEW_METHODS:
141
+ for o in OLD_METHODS:
142
+ a, b = sorted((n, o))
143
+ pairs.add((a, b))
144
+
145
+ # NEW vs GT
146
+ for n in NEW_METHODS:
147
+ a, b = sorted((n, GROUND_TRUTH))
148
+ pairs.add((a, b))
149
+
150
+ # Sanity: should be 100
151
+ return sorted(pairs)
152
+
153
+
154
+ def rebuild_counts_from_csv(df: pd.DataFrame) -> Tuple[Dict[Tuple[str, str], int], int]:
155
+ """Rebuild per-pair counts and total votes from the CSV."""
156
+ counts: Dict[Tuple[str, str], int] = {pair: 0 for pair in ALL_REQUIRED_PAIRS}
157
+ total = 0
158
+ if df is not None and not df.empty:
159
+ for _, row in df.iterrows():
160
+ # normalize to canonical sorted tuple
161
+ pair = tuple(sorted((str(row["method1"]), str(row["method2"]))))
162
+ # Only count votes that are part of this study's 100 pairs
163
+ if pair in counts:
164
+ counts[pair] += 1
165
+ total += 1
166
+ return counts, total
167
+
168
+
169
+ def select_next_pair() -> Optional[Tuple[str, str]]:
170
+ """Pick an unordered pair (m1, m2) with the fewest votes (<23), breaking ties randomly."""
171
+ # Filter to those under the per-pair quota
172
+ under_quota = [p for p in ALL_REQUIRED_PAIRS if PAIR_COUNTS.get(p, 0) < VOTES_PER_PAIR]
173
+ if not under_quota:
174
+ return None
175
+ # Find minimal count among under-quota pairs
176
+ min_count = min(PAIR_COUNTS.get(p, 0) for p in under_quota)
177
+ candidates = [p for p in under_quota if PAIR_COUNTS.get(p, 0) == min_count]
178
+ return random.choice(candidates)
179
+
180
+
181
+ def pick_video_for_pair(m1: str, m2: str) -> Optional[str]:
182
+ """Pick a random video filename available for both methods."""
183
+ set1 = METHOD_VIDEOS.get(m1, set())
184
+ set2 = METHOD_VIDEOS.get(m2, set())
185
+ common = list(set1 & set2)
186
+ if not common:
187
+ return None
188
+ return random.choice(common)
189
+
190
+
191
+ def video_path(method: str, filename: str) -> str:
192
+ """Build absolute path to a method's video file."""
193
+ return str(COMMON_VIDEOS_DIR / method / filename)
194
+
195
+
196
+ def progress_text() -> str:
197
+ return f"Votes Collected: {min(TOTAL_VOTES, STOP_TOTAL)} / {STOP_TOTAL}"
198
+
199
+
200
+ def prepare_next_display():
201
+ """Compute the next pair, randomize sides, and return UI payload."""
202
+ global TOTAL_VOTES
203
+ # Stop condition on total votes
204
+ if TOTAL_VOTES >= STOP_TOTAL:
205
  return {
206
+ "left_src": None,
207
+ "right_src": None,
208
+ "status": "Study Complete. Thank you!",
209
+ "progress": progress_text(),
210
+ "state": {
211
+ "method_left": None,
212
+ "method_right": None,
213
+ "video_name": None,
214
+ "pair": None,
215
+ },
216
+ "disable": True,
217
  }
218
+
219
+ pair = select_next_pair()
220
+ if pair is None:
221
+ # No more pairs under quota; either done or cannot proceed
222
+ return {
223
+ "left_src": None,
224
+ "right_src": None,
225
+ "status": "Study Complete. Thank you!",
226
+ "progress": progress_text(),
227
+ "state": {
228
+ "method_left": None,
229
+ "method_right": None,
230
+ "video_name": None,
231
+ "pair": None,
232
+ },
233
+ "disable": True,
234
+ }
235
+
236
+ m1, m2 = pair
237
+ filename = pick_video_for_pair(m1, m2)
238
+ # If no common file found, try a few times; fallback to disable
239
+ tries = 5
240
+ while filename is None and tries > 0:
241
+ filename = pick_video_for_pair(m1, m2)
242
+ tries -= 1
243
+ if filename is None:
244
+ return {
245
+ "left_src": None,
246
+ "right_src": None,
247
+ "status": "No common video found for selected pair. Please try again.",
248
+ "progress": progress_text(),
249
+ "state": {
250
+ "method_left": None,
251
+ "method_right": None,
252
+ "video_name": None,
253
+ "pair": None,
254
+ },
255
+ "disable": False,
256
+ }
257
+
258
+ # Randomize left/right
259
+ if random.random() < 0.5:
260
+ left_m, right_m = m1, m2
261
+ else:
262
+ left_m, right_m = m2, m1
263
+
264
+ return {
265
+ "left_src": video_path(left_m, filename),
266
+ "right_src": video_path(right_m, filename),
267
+ "status": "",
268
+ "progress": progress_text(),
269
+ "state": {
270
+ "method_left": left_m,
271
+ "method_right": right_m,
272
+ "video_name": filename,
273
+ "pair": tuple(sorted((m1, m2))),
274
+ },
275
+ "disable": False,
276
+ }
277
+
278
+
279
+ def append_vote(method1: str, method2: str, video_name: str, winner: str):
280
+ """Append a vote to CSV safely and update in-memory counters."""
281
+ global TOTAL_VOTES
282
+ ts = datetime.utcnow().isoformat()
283
+ row = {
284
+ "method1": method1,
285
+ "method2": method2,
286
+ "video_name": video_name,
287
+ "winner": winner,
288
+ "timestamp": ts,
289
+ }
290
+ with _write_lock:
291
+ # Append efficiently without re-reading whole CSV
292
+ exists = VOTES_CSV.exists()
293
+ df_row = pd.DataFrame([row])
294
+ df_row.to_csv(VOTES_CSV, mode="a", header=not exists, index=False)
295
+ # Update memory
296
+ PAIR_COUNTS[(method1, method2)] = PAIR_COUNTS.get((method1, method2), 0) + 1
297
+ TOTAL_VOTES += 1
298
+
299
+
300
+ # ----------------------------
301
+ # Gradio callback functions
302
+ # ----------------------------
303
+
304
+ def on_load():
305
+ """Load initial pair and media."""
306
+ payload = prepare_next_display()
307
+ disable = payload["disable"]
308
+ return (
309
+ payload["left_src"],
310
+ payload["right_src"],
311
+ payload["progress"],
312
+ payload["status"],
313
+ payload["state"],
314
+ gr.update(interactive=not disable),
315
+ gr.update(interactive=not disable),
316
+ )
317
+
318
+
319
+ def on_vote(choice: str, state: dict):
320
+ """Handle a vote and load the next pair."""
321
+ # If study complete or invalid state, just refresh next
322
+ if not state or not state.get("pair") or not state.get("video_name"):
323
+ payload = prepare_next_display()
324
+ disable = payload["disable"]
325
+ return (
326
+ payload["left_src"],
327
+ payload["right_src"],
328
+ payload["progress"],
329
+ payload["status"],
330
+ payload["state"],
331
+ gr.update(interactive=not disable),
332
+ gr.update(interactive=not disable),
333
  )
334
+
335
+ left_m = state["method_left"]
336
+ right_m = state["method_right"]
337
+ filename = state["video_name"]
338
+ pair = state["pair"] # canonical sorted tuple
339
+
340
+ # Determine winner label
341
+ if choice == "left":
342
+ winner = left_m
343
+ elif choice == "right":
344
+ winner = right_m
345
+ else:
346
+ winner = "equal"
347
+
348
+ # Persist vote (canonical pair order in CSV)
349
+ append_vote(pair[0], pair[1], filename, winner)
350
+
351
+ # Prepare next
352
+ payload = prepare_next_display()
353
+ disable = payload["disable"]
354
+ return (
355
+ payload["left_src"],
356
+ payload["right_src"],
357
+ payload["progress"],
358
+ payload["status"],
359
+ payload["state"],
360
+ gr.update(interactive=not disable),
361
+ gr.update(interactive=not disable),
362
+ )
363
+
364
+
365
+ # ----------------------------
366
+ # App initialization
367
+ # ----------------------------
368
+
369
+ def initialize():
370
+ """Initialize global state: CSV, files, pairs, counts."""
371
+ global METHOD_VIDEOS, ALL_REQUIRED_PAIRS, PAIR_COUNTS, TOTAL_VOTES
372
+
373
+ # Ensure dirs exist
374
+ if not COMMON_VIDEOS_DIR.exists():
375
+ os.makedirs(COMMON_VIDEOS_DIR, exist_ok=True)
376
+
377
+ # Scan files
378
+ METHOD_VIDEOS = scan_method_videos()
379
+
380
+ # Pairs
381
+ ALL_REQUIRED_PAIRS = generate_required_pairs() # 100 pairs
382
+
383
+ # Load votes and rebuild counts
384
+ df = ensure_votes_csv()
385
+ PAIR_COUNTS, TOTAL_VOTES = rebuild_counts_from_csv(df)
386
+
387
+
388
+ # Initialize on import
389
+ initialize()
390
+
391
+
392
+ # ----------------------------
393
+ # Gradio UI
394
+ # ----------------------------
395
+
396
+ with gr.Blocks(title="Scientific Video Comparison Study") as demo:
397
+ gr.Markdown("Compare the two videos and vote. Randomized positions prevent bias.")
398
+
399
+ with gr.Row():
400
+ left_video = gr.Video(label="Left Video", autoplay=True, muted=True, height=360)
401
+ right_video = gr.Video(label="Right Video", autoplay=True, muted=True, height=360)
402
+
403
+ progress = gr.Markdown(progress_text())
404
+ status = gr.Markdown("")
405
+
406
+ # Hidden state storing current assignment and video filename
407
+ state = gr.State(value={
408
+ "method_left": None,
409
+ "method_right": None,
410
+ "video_name": None,
411
+ "pair": None,
412
+ })
413
+
414
+ with gr.Row():
415
+ btn_left = gr.Button("Left Video is Better", variant="primary")
416
+ btn_right = gr.Button("Right Video is Better", variant="primary")
417
+ btn_toggle = gr.Button("Play/Pause Both")
418
+
419
+ # Wire events
420
+ demo.load(
421
+ fn=on_load,
422
+ inputs=None,
423
+ outputs=[left_video, right_video, progress, status, state, btn_left, btn_right],
424
+ )
425
+
426
+ btn_left.click(
427
+ fn=lambda s: on_vote("left", s),
428
+ inputs=[state],
429
+ outputs=[left_video, right_video, progress, status, state, btn_left, btn_right],
430
+ )
431
+ btn_right.click(
432
+ fn=lambda s: on_vote("right", s),
433
+ inputs=[state],
434
+ outputs=[left_video, right_video, progress, status, state, btn_left, btn_right],
435
+ )
436
+
437
+ # Client-side JS to toggle both <video> elements simultaneously
438
+ btn_toggle.click(
439
+ fn=None,
440
+ inputs=None,
441
+ outputs=None,
442
+ js="""
443
+ () => {
444
+ const vids = Array.from(document.querySelectorAll('video'));
445
+ if (vids.length === 0) return;
446
+ const anyPlaying = vids.some(v => !v.paused && !v.ended && v.readyState > 2);
447
+ if (anyPlaying) {
448
+ vids.forEach(v => v.pause());
449
+ } else {
450
+ vids.forEach(v => v.play());
451
+ }
452
+ }
453
+ """
454
+ )
455
 
456
  if __name__ == "__main__":
457
+ # Launch locally; adjust server_name/port as needed
458
+ demo.queue().launch()
 
 
 
 
 
votes.csv ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ method1,method2,video_name,winner,timestamp
2
+