evanzyfan Cursor commited on
Commit
27d0b7a
·
1 Parent(s): b339a0d

Add preference ranking user study app

Browse files

- app.py: Gradio app for collecting preference rankings of AI-generated movies
- requirements.txt: Python dependencies

Co-authored-by: Cursor <cursoragent@cursor.com>

Files changed (2) hide show
  1. app.py +621 -0
  2. requirements.txt +2 -0
app.py ADDED
@@ -0,0 +1,621 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MovieBench Preference Ranking User Study Application (Gradio Version)
3
+
4
+ A simplified Gradio web application for collecting human preference rankings
5
+ of AI-generated movies. For each story, presents results from different methods
6
+ side-by-side with shuffled anonymous labels, and collects preference ordering.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import random
12
+ import threading
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ import gradio as gr
18
+ from huggingface_hub import CommitScheduler, snapshot_download
19
+
20
+ # ============================================================================
21
+ # Configuration
22
+ # ============================================================================
23
+
24
+ DATA_DIR = os.environ.get("DATA_DIR", "./data")
25
+ OUTPUT_DIR = os.environ.get("OUTPUT_DIR", "./results_pref")
26
+ NUM_GROUPS = int(os.environ.get("NUM_GROUPS", "5"))
27
+ RESULTS_REPO_ID = os.environ.get("RESULTS_REPO_ID", "")
28
+ DATA_REPO_ID = os.environ.get("DATA_REPO_ID", "")
29
+ HF_TOKEN = os.environ.get("HF_TOKEN", None)
30
+ MAX_METHODS = 8
31
+
32
+ if DATA_REPO_ID and not Path(DATA_DIR).exists():
33
+ print(f"Downloading data from {DATA_REPO_ID} ...")
34
+ downloaded = snapshot_download(
35
+ repo_id=DATA_REPO_ID,
36
+ repo_type="dataset",
37
+ local_dir=DATA_DIR,
38
+ token=HF_TOKEN,
39
+ )
40
+ print(f"Data downloaded to {downloaded}")
41
+
42
+ Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
43
+
44
+ scheduler: Optional[CommitScheduler] = None
45
+ if RESULTS_REPO_ID:
46
+ scheduler = CommitScheduler(
47
+ repo_id=RESULTS_REPO_ID,
48
+ repo_type="dataset",
49
+ folder_path=OUTPUT_DIR,
50
+ every=3,
51
+ path_in_repo="result_new/preference",
52
+ token=HF_TOKEN,
53
+ )
54
+
55
+
56
+ # ============================================================================
57
+ # Data Loading Functions
58
+ # ============================================================================
59
+
60
+ def _load_story_scripts() -> Dict[str, str]:
61
+ """Load original story scripts from vistory_test_lite.json (keyed by story id)."""
62
+ script_path = Path(DATA_DIR) / "vistory_test_lite.json"
63
+ if script_path.exists():
64
+ with open(script_path, "r", encoding="utf-8-sig") as f:
65
+ entries = json.load(f)
66
+ return {entry["id"]: entry["script"] for entry in entries}
67
+ return {}
68
+
69
+
70
+ STORY_SCRIPTS = _load_story_scripts()
71
+
72
+
73
+ def load_summary() -> List[Dict[str, str]]:
74
+ """Load summary.json that maps sample IDs to agents and story IDs."""
75
+ summary_path = Path(DATA_DIR) / "summary.json"
76
+ if summary_path.exists():
77
+ with open(summary_path, "r", encoding="utf-8-sig") as f:
78
+ return json.load(f)
79
+ return []
80
+
81
+
82
+ def get_available_samples() -> List[str]:
83
+ """Get list of available sample directory IDs."""
84
+ data_path = Path(DATA_DIR)
85
+ if not data_path.exists():
86
+ return []
87
+ return sorted([d.name for d in data_path.iterdir() if d.is_dir()])
88
+
89
+
90
+ def get_stories_with_agents() -> Dict[str, List[Dict[str, str]]]:
91
+ """Build mapping: story_id -> [{agent, shuffled_id}, ...]."""
92
+ summary = load_summary()
93
+ available = set(get_available_samples())
94
+ mapping: Dict[str, List[Dict[str, str]]] = {}
95
+ for entry in summary:
96
+ sid = entry["shuffled_id"]
97
+ if sid not in available:
98
+ continue
99
+ story_id = entry["story_id"]
100
+ mapping.setdefault(story_id, []).append({
101
+ "agent": entry["agent"],
102
+ "shuffled_id": sid,
103
+ })
104
+ for v in mapping.values():
105
+ v.sort(key=lambda x: x["agent"])
106
+ return mapping
107
+
108
+
109
+ def get_movie_video_path(shuffled_id: str) -> str:
110
+ """Return the path to a sample's final movie video."""
111
+ p = Path(DATA_DIR) / shuffled_id / "final_video.mp4"
112
+ return str(p) if p.exists() else ""
113
+
114
+
115
+ _save_lock = threading.Lock()
116
+
117
+
118
+ # ============================================================================
119
+ # Group Management
120
+ # ============================================================================
121
+
122
+ def _partition_list(items: List, num_chunks: int) -> List[List]:
123
+ """Split items into num_chunks chunks as evenly as possible."""
124
+ chunk_size, remainder = divmod(len(items), num_chunks)
125
+ chunks: List[List] = []
126
+ start = 0
127
+ for i in range(num_chunks):
128
+ end = start + chunk_size + (1 if i < remainder else 0)
129
+ chunks.append(items[start:end])
130
+ start = end
131
+ return chunks
132
+
133
+
134
+ def get_or_create_group_config(group_id: str) -> Dict[str, Any]:
135
+ """Load existing group config or create a new one."""
136
+ group_dir = Path(OUTPUT_DIR) / f"group_{group_id}"
137
+ mapping_path = group_dir / "mapping.json"
138
+
139
+ if mapping_path.exists():
140
+ with open(mapping_path, "r", encoding="utf-8-sig") as f:
141
+ return json.load(f)
142
+
143
+ return create_group_config(group_id)
144
+
145
+
146
+ def create_group_config(group_id: str) -> Dict[str, Any]:
147
+ """Create a group config with deterministic story partitioning and method shuffle.
148
+
149
+ Stories are shuffled with a fixed global seed and split into NUM_GROUPS
150
+ non-overlapping chunks. The agent display order is shuffled per-group
151
+ so that anonymous labels (Method A, B, ...) are consistent within a group
152
+ but differ across groups.
153
+ """
154
+ group_dir = Path(OUTPUT_DIR) / f"group_{group_id}"
155
+ group_dir.mkdir(parents=True, exist_ok=True)
156
+
157
+ stories_map = get_stories_with_agents()
158
+
159
+ try:
160
+ group_index = (int(group_id) - 1) % NUM_GROUPS
161
+ except ValueError:
162
+ group_index = hash(group_id) % NUM_GROUPS
163
+
164
+ unique_stories = sorted(stories_map.keys())
165
+ story_rng = random.Random("moviebench_pref_story_partition")
166
+ story_rng.shuffle(unique_stories)
167
+
168
+ story_chunks = _partition_list(unique_stories, NUM_GROUPS)
169
+ selected_stories = story_chunks[group_index]
170
+
171
+ all_agents = set()
172
+ for story_id in selected_stories:
173
+ for entry in stories_map.get(story_id, []):
174
+ all_agents.add(entry["agent"])
175
+ all_agents_sorted = sorted(all_agents)
176
+
177
+ method_rng = random.Random(f"moviebench_pref_group_{group_id}")
178
+ shuffled_agents = list(all_agents_sorted)
179
+ method_rng.shuffle(shuffled_agents)
180
+
181
+ labels = [chr(ord("A") + i) for i in range(len(shuffled_agents))]
182
+ method_display_map = {}
183
+ for i, agent in enumerate(shuffled_agents):
184
+ method_display_map[f"Method {labels[i]}"] = agent
185
+
186
+ presentation_rng = random.Random(f"moviebench_pref_order_{group_id}")
187
+ story_order = list(selected_stories)
188
+ presentation_rng.shuffle(story_order)
189
+
190
+ config = {
191
+ "group_id": group_id,
192
+ "group_index": group_index,
193
+ "num_groups": NUM_GROUPS,
194
+ "created_at": datetime.now().isoformat(),
195
+ "stories": story_order,
196
+ "total_stories": len(unique_stories),
197
+ "stories_in_group": len(story_order),
198
+ "agents": all_agents_sorted,
199
+ "method_order": shuffled_agents,
200
+ "method_display_map": method_display_map,
201
+ }
202
+
203
+ with _save_lock:
204
+ with open(group_dir / "mapping.json", "w", encoding="utf-8") as f:
205
+ json.dump(config, f, indent=2, ensure_ascii=False)
206
+
207
+ return config
208
+
209
+
210
+ def save_ranking_result(
211
+ group_id: str,
212
+ story_id: str,
213
+ evaluator_id: str,
214
+ method_display_map: Dict[str, str],
215
+ ranking: Dict[str, int],
216
+ comment: str,
217
+ ) -> str:
218
+ """Save a preference ranking result to JSON. Returns a status message."""
219
+ group_dir = Path(OUTPUT_DIR) / f"group_{group_id}"
220
+ story_dir = group_dir / story_id
221
+ filename = f"{story_id}_{evaluator_id}.json"
222
+
223
+ result_data = {
224
+ "evaluator_id": evaluator_id,
225
+ "group_id": group_id,
226
+ "timestamp": datetime.now().isoformat(),
227
+ "story_id": story_id,
228
+ "method_order": method_display_map,
229
+ "ranking": ranking,
230
+ "comment": comment,
231
+ }
232
+
233
+ filepath = story_dir / filename
234
+ with _save_lock:
235
+ story_dir.mkdir(parents=True, exist_ok=True)
236
+ with open(filepath, "w", encoding="utf-8") as f:
237
+ json.dump(result_data, f, indent=4, ensure_ascii=False)
238
+
239
+ return f"Saved to {filepath}"
240
+
241
+
242
+ # ============================================================================
243
+ # Gradio Interface
244
+ # ============================================================================
245
+
246
+ CUSTOM_CSS = """
247
+ .gradio-container {
248
+ max-width: 1600px !important;
249
+ margin-left: auto !important;
250
+ margin-right: auto !important;
251
+ }
252
+ .title-text {
253
+ text-align: center;
254
+ background: linear-gradient(135deg, #7c5cff 0%, #ff6b9d 100%);
255
+ -webkit-background-clip: text;
256
+ -webkit-text-fill-color: transparent;
257
+ font-size: 2rem;
258
+ font-weight: 700;
259
+ margin-bottom: 1rem;
260
+ }
261
+ .method-label {
262
+ text-align: center;
263
+ font-size: 1.1rem;
264
+ font-weight: 600;
265
+ padding: 6px 0;
266
+ }
267
+ """
268
+
269
+
270
+ def create_app():
271
+ """Create the Gradio application."""
272
+
273
+ with gr.Blocks(
274
+ title="MovieBench: Preference Ranking",
275
+ css=CUSTOM_CSS,
276
+ theme=gr.themes.Soft(
277
+ primary_hue="purple",
278
+ secondary_hue="pink",
279
+ neutral_hue="slate",
280
+ ),
281
+ ) as app:
282
+
283
+ current_evaluator = gr.State("anonymous")
284
+ current_group = gr.State("")
285
+ group_config_state = gr.State({})
286
+ current_story_idx = gr.State(0)
287
+
288
+ gr.Markdown(
289
+ "# MovieBench: Preference Ranking",
290
+ elem_classes=["title-text"],
291
+ )
292
+
293
+ # ================================================================
294
+ # Tab 1: Setup
295
+ # ================================================================
296
+ with gr.Tab("Setup", id="tab_setup"):
297
+ gr.Markdown("### Enter your evaluator ID and group ID to begin")
298
+
299
+ with gr.Row():
300
+ evaluator_input = gr.Textbox(
301
+ label="Evaluator ID",
302
+ placeholder="Enter your name or ID",
303
+ value="anonymous",
304
+ scale=2,
305
+ )
306
+ group_input = gr.Textbox(
307
+ label="Group ID",
308
+ placeholder=f"Enter group ID (1-{NUM_GROUPS})",
309
+ value="",
310
+ scale=2,
311
+ )
312
+
313
+ load_group_btn = gr.Button("Load / Create Group", variant="primary")
314
+ group_info = gr.Markdown("*Enter a Group ID and click 'Load / Create Group'*")
315
+
316
+ def load_group(group_id: str, evaluator_id: str):
317
+ if not group_id:
318
+ return (
319
+ "*Please enter a Group ID*",
320
+ evaluator_id,
321
+ group_id,
322
+ {},
323
+ )
324
+ config = get_or_create_group_config(group_id)
325
+ stories = config.get("stories", [])
326
+ agents = config.get("agents", [])
327
+ method_map = config.get("method_display_map", {})
328
+ display_lines = ", ".join(sorted(method_map.keys()))
329
+
330
+ info_md = (
331
+ f"### Group `{group_id}` loaded "
332
+ f"(partition {config.get('group_index', 0) + 1}/{config.get('num_groups', NUM_GROUPS)})\n\n"
333
+ f"**Stories in group:** {len(stories)}/{config.get('total_stories', '?')}\n\n"
334
+ f"**Agents:** {len(agents)} ({', '.join(agents)})\n\n"
335
+ f"**Display labels:** {display_lines}\n\n"
336
+ f"**Story order:** {', '.join(stories)}\n\n"
337
+ f"**Created:** {config.get('created_at', 'N/A')}\n\n"
338
+ f"Go to the **Preference Evaluation** tab to start ranking."
339
+ )
340
+ return info_md, evaluator_id, group_id, config
341
+
342
+ load_group_btn.click(
343
+ load_group,
344
+ inputs=[group_input, evaluator_input],
345
+ outputs=[group_info, current_evaluator, current_group, group_config_state],
346
+ )
347
+
348
+ # ================================================================
349
+ # Tab 2: Preference Evaluation
350
+ # ================================================================
351
+ with gr.Tab("Preference Evaluation", id="tab_eval"):
352
+ gr.Markdown("### Rank the methods by preference for each story")
353
+
354
+ with gr.Row():
355
+ story_progress = gr.Markdown("**Progress:** Load a group first")
356
+ story_nav_prev = gr.Button("Previous Story", size="sm")
357
+ story_nav_next = gr.Button("Next Story", size="sm")
358
+
359
+ with gr.Accordion("Story Script", open=True):
360
+ story_script_display = gr.Markdown(
361
+ "*Load a group and go to this tab to see stories*"
362
+ )
363
+
364
+ gr.Markdown("---")
365
+ gr.Markdown("### Method Videos")
366
+
367
+ method_cols: List[gr.Column] = []
368
+ method_videos: List[gr.Video] = []
369
+ method_labels: List[gr.Markdown] = []
370
+ method_ranks: List[gr.Dropdown] = []
371
+
372
+ with gr.Row():
373
+ for i in range(MAX_METHODS):
374
+ with gr.Column(visible=False) as col:
375
+ lbl = gr.Markdown(
376
+ f"**Method {chr(ord('A') + i)}**",
377
+ elem_classes=["method-label"],
378
+ )
379
+ vid = gr.Video(
380
+ label=f"Method {chr(ord('A') + i)}",
381
+ height=300,
382
+ )
383
+ rank = gr.Dropdown(
384
+ label="Rank",
385
+ choices=[],
386
+ value=None,
387
+ interactive=True,
388
+ )
389
+ method_cols.append(col)
390
+ method_videos.append(vid)
391
+ method_labels.append(lbl)
392
+ method_ranks.append(rank)
393
+
394
+ gr.Markdown("---")
395
+
396
+ rank_comment = gr.Textbox(
397
+ label="Comment (optional)",
398
+ placeholder="Any additional notes about your ranking decision...",
399
+ lines=2,
400
+ )
401
+
402
+ with gr.Row():
403
+ submit_btn = gr.Button("Submit & Next Story", variant="primary")
404
+ eval_status = gr.Markdown("")
405
+
406
+ # ============================================================
407
+ # Helper functions
408
+ # ============================================================
409
+
410
+ def _build_story_display(story_idx: int, config: Dict[str, Any]):
411
+ """Build all output values for displaying a given story.
412
+
413
+ Returns a flat list matching the outputs wired to the UI:
414
+ [progress_md, script_md,
415
+ col_0_visible, vid_0, lbl_0, rank_0_choices, rank_0_value,
416
+ col_1_visible, vid_1, lbl_1, rank_1_choices, rank_1_value,
417
+ ... (MAX_METHODS times)]
418
+ """
419
+ stories = config.get("stories", [])
420
+ method_order: List[str] = config.get("method_order", [])
421
+ method_display_map: Dict[str, str] = config.get("method_display_map", {})
422
+ stories_map = get_stories_with_agents()
423
+
424
+ num_methods = len(method_order)
425
+ rank_choices = [str(r) for r in range(1, num_methods + 1)]
426
+
427
+ if not stories or story_idx >= len(stories):
428
+ outputs: list = [
429
+ "**Progress:** No stories loaded",
430
+ "*Load a group first*",
431
+ ]
432
+ for _ in range(MAX_METHODS):
433
+ outputs.extend([
434
+ gr.update(visible=False),
435
+ None,
436
+ "",
437
+ gr.update(choices=[], value=None),
438
+ ])
439
+ return outputs
440
+
441
+ story_id = stories[story_idx]
442
+ script_text = STORY_SCRIPTS.get(story_id, "(Script not available)")
443
+
444
+ progress_md = f"**Progress:** Story {story_idx + 1}/{len(stories)} (`{story_id}`)"
445
+ script_md = f"**Story ID:** `{story_id}`\n\n{script_text}"
446
+
447
+ agent_to_sid: Dict[str, str] = {}
448
+ for entry in stories_map.get(story_id, []):
449
+ agent_to_sid[entry["agent"]] = entry["shuffled_id"]
450
+
451
+ label_to_agent = {}
452
+ for label in sorted(method_display_map.keys()):
453
+ label_to_agent[label] = method_display_map[label]
454
+
455
+ sorted_labels = sorted(label_to_agent.keys())
456
+
457
+ outputs = [progress_md, script_md]
458
+ for i in range(MAX_METHODS):
459
+ if i < len(sorted_labels):
460
+ label = sorted_labels[i]
461
+ agent = label_to_agent[label]
462
+ sid = agent_to_sid.get(agent, "")
463
+ video_path = get_movie_video_path(sid) if sid else ""
464
+ outputs.extend([
465
+ gr.update(visible=True),
466
+ video_path if video_path else None,
467
+ f"**{label}**",
468
+ gr.update(choices=rank_choices, value=None),
469
+ ])
470
+ else:
471
+ outputs.extend([
472
+ gr.update(visible=False),
473
+ None,
474
+ "",
475
+ gr.update(choices=[], value=None),
476
+ ])
477
+ return outputs
478
+
479
+ def update_story_display(story_idx: int, config: Dict[str, Any]):
480
+ return _build_story_display(story_idx, config)
481
+
482
+ def go_prev_story(story_idx: int):
483
+ return max(0, story_idx - 1)
484
+
485
+ def go_next_story(story_idx: int, config: Dict[str, Any]):
486
+ stories = config.get("stories", [])
487
+ return min(len(stories) - 1, story_idx + 1) if stories else 0
488
+
489
+ def submit_ranking(
490
+ story_idx: int,
491
+ evaluator_id: str,
492
+ group_id: str,
493
+ config: Dict[str, Any],
494
+ comment: str,
495
+ *rank_values,
496
+ ):
497
+ """Validate and save the ranking, then advance to the next story."""
498
+ if not group_id or not config:
499
+ return "Please load a group first", story_idx, gr.update()
500
+
501
+ stories = config.get("stories", [])
502
+ if not stories or story_idx >= len(stories):
503
+ return "No stories available", story_idx, gr.update()
504
+
505
+ method_display_map = config.get("method_display_map", {})
506
+ sorted_labels = sorted(method_display_map.keys())
507
+ num_methods = len(sorted_labels)
508
+
509
+ ranking: Dict[str, int] = {}
510
+ used_ranks = set()
511
+ for i in range(num_methods):
512
+ val = rank_values[i] if i < len(rank_values) else None
513
+ if val is None or val == "":
514
+ return (
515
+ f"Please assign a rank to **{sorted_labels[i]}**",
516
+ story_idx,
517
+ gr.update(),
518
+ )
519
+ r = int(val)
520
+ if r in used_ranks:
521
+ return (
522
+ f"Duplicate rank {r} — each method must have a unique rank",
523
+ story_idx,
524
+ gr.update(),
525
+ )
526
+ used_ranks.add(r)
527
+ ranking[sorted_labels[i]] = r
528
+
529
+ story_id = stories[story_idx]
530
+ status = save_ranking_result(
531
+ group_id=group_id,
532
+ story_id=story_id,
533
+ evaluator_id=evaluator_id,
534
+ method_display_map=method_display_map,
535
+ ranking=ranking,
536
+ comment=comment or "",
537
+ )
538
+
539
+ next_idx = min(len(stories) - 1, story_idx + 1)
540
+ if next_idx == story_idx:
541
+ return (
542
+ f"{status}\n\nAll stories evaluated! Thank you!",
543
+ next_idx,
544
+ "",
545
+ )
546
+ return (
547
+ f"{status} | Moving to next story...",
548
+ next_idx,
549
+ "",
550
+ )
551
+
552
+ # ============================================================
553
+ # Wire up events
554
+ # ============================================================
555
+
556
+ display_outputs = [story_progress, story_script_display]
557
+ for i in range(MAX_METHODS):
558
+ display_outputs.extend([
559
+ method_cols[i],
560
+ method_videos[i],
561
+ method_labels[i],
562
+ method_ranks[i],
563
+ ])
564
+
565
+ # When group config changes, reset to story 0
566
+ group_config_state.change(
567
+ lambda cfg: [0] + _build_story_display(0, cfg),
568
+ inputs=[group_config_state],
569
+ outputs=[current_story_idx] + display_outputs,
570
+ )
571
+
572
+ # When story idx changes, update display
573
+ current_story_idx.change(
574
+ update_story_display,
575
+ inputs=[current_story_idx, group_config_state],
576
+ outputs=display_outputs,
577
+ )
578
+
579
+ story_nav_prev.click(
580
+ go_prev_story,
581
+ inputs=[current_story_idx],
582
+ outputs=[current_story_idx],
583
+ )
584
+ story_nav_next.click(
585
+ go_next_story,
586
+ inputs=[current_story_idx, group_config_state],
587
+ outputs=[current_story_idx],
588
+ )
589
+
590
+ submit_inputs = [
591
+ current_story_idx,
592
+ current_evaluator,
593
+ current_group,
594
+ group_config_state,
595
+ rank_comment,
596
+ ] + method_ranks
597
+
598
+ submit_btn.click(
599
+ submit_ranking,
600
+ inputs=submit_inputs,
601
+ outputs=[eval_status, current_story_idx, rank_comment],
602
+ )
603
+
604
+ return app
605
+
606
+
607
+ # ============================================================================
608
+ # Main Entry Point
609
+ # ============================================================================
610
+
611
+ demo = create_app()
612
+
613
+ if __name__ == "__main__":
614
+ data_dir_abs = str(Path(DATA_DIR).resolve())
615
+ demo.launch(
616
+ server_name="0.0.0.0",
617
+ server_port=7860,
618
+ share=False,
619
+ show_error=True,
620
+ allowed_paths=[data_dir_abs],
621
+ )
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio
2
+ huggingface_hub