Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import pandas as pd | |
| import os | |
| import random | |
| from datetime import datetime | |
| from huggingface_hub import list_repo_files | |
| import gspread | |
| import json | |
| from dotenv import load_dotenv | |
| import numpy as np | |
| from scipy.optimize import minimize | |
| from scipy.special import expit | |
| load_dotenv() | |
| # --- 1. CONFIGURATION --- | |
| DATASET_REPO = "AhmadRH/SeqWMVideosFinal" | |
| DATASET_BRANCH = "main" | |
| VIDEO_BASE_URL = f"https://huggingface.co/datasets/{DATASET_REPO}/resolve/{DATASET_BRANCH}" | |
| # --- New: Google Sheets Configuration --- | |
| # The name of the Google Sheet you created | |
| GOOGLE_SHEET_NAME = "2 Step WM human preference study" | |
| # --- NEW: Elo Calculation Constants --- | |
| INITIAL_ELO = 1000 | |
| K_FACTOR = 32 | |
| NUM_EPOCHS = 1 | |
| ELO_SCALE_FACTOR = 400.0 / np.log(10.0) | |
| # --- New: Function to connect to Google Sheets securely --- | |
| def get_gsheet_client(): | |
| """Initializes and returns the gspread client using credentials from HF secrets.""" | |
| creds_json_str = os.environ.get('GOOGLE_CREDS_JSON') | |
| if not creds_json_str: | |
| raise ValueError("Google Sheets credentials not found in Hugging Face secrets (GOOGLE_CREDS_JSON).") | |
| creds_dict = json.loads(creds_json_str) | |
| client = gspread.service_account_from_dict(creds_dict) | |
| return client | |
| def get_worksheet(): | |
| """Opens the specific worksheet for reading/writing results.""" | |
| client = get_gsheet_client() | |
| spreadsheet = client.open(GOOGLE_SHEET_NAME) | |
| return spreadsheet.sheet1 # Use the first sheet | |
| # --- 2. LOAD AND PREPARE VIDEO PAIRS (No changes here) --- | |
| def load_video_pairs(): | |
| """Scans the video directory and creates a list of video pairs.""" | |
| files = list_repo_files(DATASET_REPO, repo_type="dataset", revision=DATASET_BRANCH) | |
| video_exts = {".mp4", ".webm", ".mov"} | |
| dataset_videos = [f for f in files if os.path.splitext(f)[1].lower() in video_exts] | |
| from collections import defaultdict | |
| model_to_files = defaultdict(set) | |
| model_to_paths = defaultdict(dict) | |
| for relpath in dataset_videos: | |
| parts = relpath.split("/") | |
| model_name, filename = parts | |
| model_to_files[model_name].add(filename) | |
| model_to_paths[model_name][filename] = relpath | |
| # models_to_compare = [("ltx_2B_2step", "ltx_2B_unconditional"), | |
| # ("ltx_13B_2step", "ltx_13B_unconditional"), | |
| # ("ltx_2B_2step", "predict1-7B"), | |
| # ("ltx_13B_2step", "predict2-2B"), | |
| # ("ltx_2B_2step", "ltx_2B_2step_from13B"), | |
| # ("ltx_2B_2step", "ltx_13B_2step_from2B"), | |
| # ("ltx_2B_2step", "ltx_13B_2step"), | |
| # ("ltx_2B_2step_from13B", "ltx_13B_2step_from2B"), | |
| # ("ltx_2B_2step_from13B", "ltx_13B_2step"), | |
| # ("ltx_13B_2step_from2B", "ltx_13B_2step")] | |
| # models_to_compare = [("ltx_2b_naive_lora", "ltx_2b_baseline"), | |
| # ("ltx_13b_naive_lora", "ltx_13b_baseline"), | |
| # ("ltx_2b_2step_noised", "ltx_2b_naive_lora"), | |
| # ("ltx_2b_2step_noised", "GEM"), | |
| # ("ltx_2b_2step_noised", "Vista"), | |
| # ("ltx_2b_2step_noised", "predict1-7b"), | |
| # ("ltx_2b_2step_noised", "predict1-14b"), | |
| # ("ltx_2b_2step_noised", "predict2-2b"), | |
| # ("ltx_13b_2step_noised", "ltx_13b_naive_lora"), | |
| # ("ltx_13b_2step_noised", "ltx_2b_2step_noised"), | |
| # ("ltx_13b_2step_noised", "predict2-2b"), | |
| # ("ltx_13b_2step_noised", "predict2-14b"), | |
| # ("ltx_13b_2step_noised", "predict1-7b"), | |
| # ("ltx_13b_2step_noised", "predict1-14b"), | |
| # ] | |
| models_to_compare = [('GEM_25_frames', "MAD-SVD-25Frames"), | |
| ("MAD-SVD-25Frames", "VISTA_25_frames"), | |
| ("MAD-SVD-25Frames", "SVD_25_frames"),] | |
| video_pairs = [] | |
| for model_a, model_b in models_to_compare: | |
| if model_a not in model_to_files or model_b not in model_to_files: | |
| continue | |
| common_filenames = sorted(model_to_files[model_a].intersection(model_to_files[model_b])) | |
| print(f"{model_a}, {model_b}: {len(common_filenames)}/{len(model_to_files)}") | |
| for filename in common_filenames: | |
| path_a = model_to_paths[model_a][filename] | |
| path_b = model_to_paths[model_b][filename] | |
| url_a = f"{VIDEO_BASE_URL}/{path_a}" | |
| url_b = f"{VIDEO_BASE_URL}/{path_b}" | |
| scene_name = os.path.splitext(filename)[0] | |
| pair_key = f"{scene_name}${model_a} vs {model_b}" | |
| video_pairs.append((url_a, url_b, pair_key, model_a, model_b, scene_name)) | |
| print(f"Loaded {len(video_pairs)} video pairs.") | |
| return video_pairs | |
| ALL_PAIRS = load_video_pairs() | |
| ALL_PAIR_KEYS = {key for _, _, key, _, _, _ in ALL_PAIRS} | |
| # --- 3. CORE APP LOGIC (Updated for Google Sheets and Sliders) --- | |
| def start_session(email): | |
| """Initializes a user session by reading past results from Google Sheets.""" | |
| if not email or "@" not in email: | |
| raise gr.Error("Please enter a valid email to start.") | |
| seen_pair_keys = set() | |
| try: | |
| worksheet = get_worksheet() | |
| all_results = worksheet.get_all_records() | |
| df_results = pd.DataFrame(all_results) | |
| if not df_results.empty and "email" in df_results.columns: | |
| user_results = df_results[df_results["email"] == email] | |
| if "pair_key" in user_results.columns: | |
| seen_pair_keys = set(user_results["pair_key"]) | |
| except gspread.exceptions.SpreadsheetNotFound: | |
| raise gr.Error(f"Spreadsheet '{GOOGLE_SHEET_NAME}' not found. Please check the name and sharing settings.") | |
| except Exception as e: | |
| gr.Warning(f"Could not load previous results: {e}") | |
| unseen_pair_keys = ALL_PAIR_KEYS - seen_pair_keys | |
| if not unseen_pair_keys: | |
| return None, gr.update(visible=False), gr.update(visible=False), gr.update( | |
| visible=True), None, None, "You have already completed all evaluations. Thank you!", *( | |
| [0] * 3) # Reset sliders | |
| unseen_pairs = [pair for pair in ALL_PAIRS if pair[2] in unseen_pair_keys] | |
| random.shuffle(unseen_pairs) | |
| user_state = [email, unseen_pairs] # Simplified user_state | |
| first_pair = unseen_pairs.pop() | |
| path_a, path_b, pair_key, model_a_name, model_b_name, scene_name = first_pair | |
| a_first = random.choice([True, False]) | |
| if a_first: | |
| video_a_path, video_b_path = path_a, path_b | |
| video_a_model, video_b_model = model_a_name, model_b_name | |
| else: | |
| video_a_path, video_b_path = path_b, path_a | |
| video_a_model, video_b_model = model_b_name, model_a_name | |
| # Store current pair info directly in the state | |
| user_state.append({ | |
| "current_pair_key": pair_key, "scene_name": scene_name, | |
| "video_a_model": video_a_model, "video_b_model": video_b_model, | |
| "video_a_path": video_a_path, "video_b_path": video_b_path | |
| }) | |
| progress = f"Progress: {len(seen_pair_keys) + 1} / {len(ALL_PAIRS)}" | |
| # Return updates for UI elements, including resetting sliders to 0 | |
| return user_state, gr.update(visible=False), gr.update(visible=True), gr.update( | |
| visible=False), video_a_path, video_b_path, progress, *([0] * 3) | |
| # --- New: Function to record slider ratings --- | |
| def record_ratings(user_state, general_look, motion_quality, visual_quality): | |
| """Records the user's slider ratings, saves to Google Sheets, and loads the next pair.""" | |
| email, unseen_pairs, current_pair_info = user_state | |
| new_row = [ | |
| datetime.now().strftime("%Y-%m-%d %H:%M:%S"), | |
| email, | |
| current_pair_info["current_pair_key"], | |
| current_pair_info["scene_name"], | |
| current_pair_info["video_a_model"], | |
| current_pair_info["video_b_model"], | |
| current_pair_info["video_a_path"], | |
| current_pair_info["video_b_path"], | |
| int(general_look), | |
| int(motion_quality), | |
| int(visual_quality) | |
| ] | |
| try: | |
| worksheet = get_worksheet() | |
| worksheet.append_row(new_row, value_input_option='USER_ENTERED') | |
| except Exception as e: | |
| raise gr.Error(f"Failed to save results to Google Sheets. Please try again. Error: {e}") | |
| if not unseen_pairs: | |
| return user_state, gr.update(visible=False), gr.update( | |
| visible=True), None, None, "All evaluations complete. Thank you!", *([0] * 3) | |
| next_pair = unseen_pairs.pop() | |
| path_a, path_b, pair_key, model_a_name, model_b_name, scene_name = next_pair | |
| a_first = random.choice([True, False]) | |
| if a_first: | |
| video_a_path, video_b_path = path_a, path_b | |
| video_a_model, video_b_model = model_a_name, model_b_name | |
| else: | |
| video_a_path, video_b_path = path_b, path_a | |
| video_a_model, video_b_model = model_b_name, model_a_name | |
| user_state[2] = { | |
| "current_pair_key": pair_key, "scene_name": scene_name, | |
| "video_a_model": video_a_model, "video_b_model": video_b_model, | |
| "video_a_path": video_a_path, "video_b_path": video_b_path, | |
| } | |
| progress = f"Progress: {len(ALL_PAIRS) - len(unseen_pairs)} / {len(ALL_PAIRS)}" | |
| # Reset sliders to 0 for the next pair | |
| return user_state, gr.update(visible=True), gr.update(visible=False), video_a_path, video_b_path, progress, *( | |
| [0] * 3) | |
| def calculate_elo_scores(k=K_FACTOR, initial_elo=INITIAL_ELO, num_epochs=NUM_EPOCHS): | |
| """ | |
| Fetches all results from the Google Sheet and calculates | |
| Elo ratings for each model based on the 'general_look' score. | |
| This version uses a 5-point scale (-2 to +2) to determine the | |
| actual score (S_A, S_B) for the match, allowing strong preferences | |
| to have a larger impact on the Elo update. | |
| """ | |
| try: | |
| worksheet = get_worksheet() | |
| all_results = worksheet.get_all_records() | |
| if not all_results: | |
| return {} | |
| df = pd.DataFrame(all_results) | |
| # Find the correct score column | |
| if 'general_look' in df.columns: | |
| score_col = 'general_look' | |
| elif 'general_look_score' in df.columns: | |
| score_col = 'general_look_score' | |
| else: | |
| print("Warning: No 'general_look' or 'general_look_score' column found.") | |
| return {} | |
| # Ensure score column is numeric, coercing errors to NaN and then dropping them | |
| df[score_col] = pd.to_numeric(df[score_col], errors='coerce') | |
| df = df.dropna(subset=[score_col, 'video_a_model', 'video_b_model']) | |
| df = df.head(650) | |
| # --- NEW: Graded Score Mapping --- | |
| # Maps the 5-point preference score to (actual_a, actual_b) tuples | |
| score_map = { | |
| -2: (1.0, 0.0), # Strong win for A | |
| -1: (0.75, 0.25), # Win for A | |
| 0: (0.5, 0.5), # Draw | |
| 1: (0.25, 0.75), # Win for B | |
| 2: (0.0, 1.0) # Strong win for B | |
| } | |
| # Use (0.5, 0.5) as a default for any unexpected scores | |
| default_score_tuple = (0.5, 0.5) | |
| elo_ratings = {} | |
| # Iterate through each match result to update Elo scores | |
| for i in range(num_epochs): | |
| # 3. Shuffle the data *for each epoch* | |
| # .sample(frac=1) is an efficient way to shuffle a DataFrame | |
| df_shuffled = df.sample(frac=1) | |
| for _, row in df_shuffled.iterrows(): | |
| model_a = row['video_a_model'] | |
| model_b = row['video_b_model'] | |
| score = int(row[score_col]) # Ensure score is an integer for dict lookup | |
| # Get current ELOs, default to initial if model is new | |
| elo_a = elo_ratings.get(model_a, initial_elo) | |
| elo_b = elo_ratings.get(model_b, initial_elo) | |
| # Calculate expected scores | |
| expected_a = 1 / (1 + 10 ** ((elo_b - elo_a) / 400)) | |
| expected_b = 1 - expected_a | |
| # --- UPDATED: Determine actual scores (S_A, S_B) --- | |
| # Get the (actual_a, actual_b) tuple from our map | |
| actual_a, actual_b = score_map.get(score, default_score_tuple) | |
| # Update ELOs | |
| elo_ratings[model_a] = elo_a + k * (actual_a - expected_a) | |
| elo_ratings[model_b] = elo_b + k * (actual_b - expected_b) | |
| return elo_ratings | |
| except Exception as e: | |
| print(f"Error calculating Elo: {e}") | |
| # In a real app, you might want to return {} or raise the exception | |
| # For Gradio, returning {} or a warning is safer | |
| return {} | |
| def calculate_elo_scores_exact(initial_elo=INITIAL_ELO): | |
| """ | |
| Fetches all results from the Google Sheet and calculates stable, | |
| order-independent Elo ratings using a maximum likelihood solver | |
| (L-BFGS-B) based on the Bradley-Terry model. | |
| """ | |
| try: | |
| worksheet = get_worksheet() | |
| all_results = worksheet.get_all_records() | |
| if not all_results: | |
| return {} | |
| df = pd.DataFrame(all_results) | |
| # --- 1. Data Preparation --- | |
| if 'general_look' in df.columns: | |
| score_col = 'general_look' | |
| elif 'general_look_score' in df.columns: | |
| score_col = 'general_look_score' | |
| else: | |
| print("Warning: No 'general_look' or 'general_look_score' column found.") | |
| return {} | |
| df[score_col] = pd.to_numeric(df[score_col], errors='coerce') | |
| df = df.dropna(subset=[score_col, 'video_a_model', 'video_b_model']) | |
| # keep only the first 600 rows | |
| df = df.head(600) | |
| # Graded Score Mapping (maps -2..+2 to 0..1 scale for prob.) | |
| score_map = { | |
| -2: 1.0, # Strong win for A | |
| -1: 0.75, # Win for A | |
| 0: 0.5, # Draw | |
| 1: 0.25, # Win for B (this is 1 - 0.75, so A's "win %" is 0.25) | |
| 2: 0.0 # Strong win for B (A's "win %" is 0.0) | |
| } | |
| # Get unique models and create a mapping to indices | |
| models = pd.concat([df['video_a_model'], df['video_b_model']]).unique() | |
| model_to_index = {model: i for i, model in enumerate(models)} | |
| num_models = len(models) | |
| # Pre-process match data into numpy arrays for speed | |
| model_a_indices = df['video_a_model'].map(model_to_index).values | |
| model_b_indices = df['video_b_model'].map(model_to_index).values | |
| # S_a = "Actual win-rate of model A" | |
| scores_a = df[score_col].map(score_map).values | |
| # --- 2. Define Objective Function (NLL) and Gradient --- | |
| def nll_and_gradient(thetas): | |
| """ | |
| Calculates the total Negative Log-Likelihood and its gradient. | |
| 'thetas' is a numpy array of strength parameters. | |
| """ | |
| # Get the strength difference for all matches at once | |
| diffs = thetas[model_a_indices] - thetas[model_b_indices] | |
| # --- Calculate NLL (Negative Log-Likelihood) --- | |
| # This is the "binary cross-entropy with logits" loss | |
| # log(1 + exp(-x)) + (1-y)*x | |
| nll_per_match = np.log(1.0 + np.exp(-diffs)) + (1.0 - scores_a) * diffs | |
| total_nll = np.sum(nll_per_match) | |
| # --- Calculate Gradient --- | |
| # Derivative of the NLL w.r.t. the strength difference 'diff' | |
| # This simplifies to: sigma(diff) - S_a | |
| common_term = expit(diffs) - scores_a | |
| # Initialize zero gradient | |
| grad = np.zeros(num_models) | |
| # Add the contribution from model A for each match | |
| # (np.bincount is a fast way to sum values by index) | |
| grad += np.bincount(model_a_indices, weights=common_term, minlength=num_models) | |
| # Subtract the contribution from model B for each match | |
| grad -= np.bincount(model_b_indices, weights=common_term, minlength=num_models) | |
| return total_nll, grad | |
| # --- 3. Run the Solver --- | |
| # Initial guess: all models have zero strength | |
| initial_thetas = np.zeros(num_models) | |
| # Constraint: The average strength must be 0. | |
| # This "anchors" the solution, as only relative scores matter. | |
| constraints = ({'type': 'eq', 'fun': lambda thetas: np.sum(thetas)}) | |
| # Run the L-BFGS-B optimizer | |
| result = minimize( | |
| nll_and_gradient, | |
| initial_thetas, | |
| method='L-BFGS-B', | |
| jac=True, # We are providing the gradient | |
| constraints=constraints | |
| ) | |
| if not result.success: | |
| print(f"Warning: Elo solver did not converge. Message: {result.message}") | |
| # --- 4. Convert Solver Strengths to Elo Ratings --- | |
| # result.x contains the final "theta" (strength) values | |
| final_thetas = result.x | |
| # Convert to Elo scale and anchor at the initial Elo (e.g., 1000) | |
| elo_ratings = {} | |
| for model, index in model_to_index.items(): | |
| elo_ratings[model] = initial_elo + final_thetas[index] * ELO_SCALE_FACTOR | |
| return elo_ratings | |
| except Exception as e: | |
| print(f"Error calculating Elo with solver: {e}") | |
| return {} | |
| def update_leaderboard(): | |
| """ | |
| A wrapper function for Gradio to calculate Elo scores | |
| and format them as a DataFrame for display. | |
| """ | |
| gr.Info("📊 Fetching new scores and updating leaderboard...") | |
| elo_scores = calculate_elo_scores() | |
| # elo_scores = calculate_elo_scores_exact() | |
| if not elo_scores: | |
| return pd.DataFrame(columns=["Rank", "Model", "Elo Score"]) # Return empty DF | |
| # Convert the elo dictionary to a DataFrame | |
| df = pd.DataFrame(list(elo_scores.items()), columns=['Model', 'Elo Score']) | |
| # Sort by Elo score, round, and add a rank | |
| df = df.sort_values(by='Elo Score', ascending=False).reset_index(drop=True) | |
| df['Rank'] = df.index + 1 | |
| df['Elo Score'] = df['Elo Score'].round().astype(int) # Clean up scores | |
| # Reorder columns | |
| df = df[['Rank', 'Model', 'Elo Score']] | |
| return df | |
| # --- 4. GRADIO UI DEFINITION (Updated) --- | |
| CUSTOM_CSS = """ | |
| /* === General App Styling === */ | |
| .gradio-container { | |
| font-size: 150%; /* Increases all font sizes by 10% */ | |
| } | |
| /* === Video Container Styling (No changes needed here) === */ | |
| #video-a-container { | |
| background-color: #4ade80; | |
| border: 2px solid #4ade80; | |
| border-radius: 12px; | |
| padding: 8px; | |
| } | |
| #video-b-container { | |
| background-color: #60a5fa; | |
| border: 2px solid #60a5fa; | |
| border-radius: 12px; | |
| padding: 8px; | |
| } | |
| /* === Radio Button Styling (CORRECTED SELECTORS) === */ | |
| /* The key is to target the div with the class "wrap" inside our custom class */ | |
| .full-width-radio > div.wrap { | |
| display: flex !important; | |
| justify-content: space-around !important; | |
| gap: 10px; | |
| } | |
| .rating-radio > div.wrap > label { | |
| flex: 1; | |
| text-align: center; | |
| border: 1px solid #d1d5db; | |
| border-radius: 8px; | |
| margin: 0 !important; | |
| padding: 8px 4px; | |
| transition: all 0.2s ease-in-out; | |
| color: #374151; | |
| } | |
| .rating-radio > div.wrap > label:hover { | |
| border-color: #6b7280; | |
| } | |
| /* --- Color Gradient for Radio Buttons --- */ | |
| .rating-radio > div.wrap > label:nth-child(1) { background-color: #4ade80; } | |
| .rating-radio > div.wrap > label:nth-child(2) { background-color: #86efac; } | |
| # .rating-radio > div.wrap > label:nth-child(3) { background-color: #bbf7d0; } | |
| .rating-radio > div.wrap > label:nth-child(3) { background-color: #e5e7eb; } | |
| # .rating-radio > div.wrap > label:nth-child(5) { background-color: #bfdbfe; } | |
| .rating-radio > div.wrap > label:nth-child(4) { background-color: #93c5fd; } | |
| .rating-radio > div.wrap > label:nth-child(5) { background-color: #60a5fa; } | |
| /* Style for the SELECTED radio button */ | |
| .rating-radio > div.wrap > label.selected { | |
| border: 3px solid #1e3a8a !important; | |
| color: #111827 !important; | |
| font-weight: bold; | |
| transform: scale(1.05); | |
| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); | |
| } | |
| """ | |
| with gr.Blocks(theme=gr.themes.Soft(), css=CUSTOM_CSS) as demo: | |
| user_state = gr.State() | |
| gr.Markdown("# 🎬 Video Generation Model Evaluation") | |
| with gr.Tabs(): | |
| with gr.Tab("Evaluate"): | |
| with gr.Column(visible=True) as login_ui: | |
| gr.Markdown( | |
| "### Thank you for participating! Please enter your email to begin." | |
| ) | |
| email_input = gr.Textbox(label="Your Email", placeholder="user@example.com") | |
| start_button = gr.Button("Start / Resume Evaluation", variant="primary") | |
| gr.Markdown("") | |
| gr.Markdown( | |
| "### You will be shown pairs of generated videos. You are asked to rate which video you prefer based on several criteria: the overall look, motion quality, and visual quality. " | |
| ) | |
| with gr.Column(visible=False) as eval_ui: | |
| gr.Markdown( | |
| "### You are shown pairs of generated videos. Please rate which video you prefer based on several criteria: the overall look, motion quality, and visual quality. " | |
| ) | |
| gr.Markdown( | |
| "### For each category, select the option that best represents your preference. **<span style='color:#16a34a;'>Green</span> favors Video A**, while **<span style='color:#2563eb;'>Blue</span> favors Video B**." | |
| ) | |
| progress_text = gr.Markdown(f"Progress: 1 / {len(ALL_PAIRS)}") | |
| with gr.Row(): | |
| video_a = gr.Video(label="Video A", width=1056, height=704, autoplay=True, loop=True, elem_id="video-a-container") | |
| video_b = gr.Video(label="Video B", width=1056, height=704, autoplay=True, loop=True, elem_id="video-b-container") | |
| # --- New: Using gr.Radio for a clear, clickable scale --- | |
| with gr.Column(): | |
| gr.Markdown("--- \n ### Rate your preference") | |
| # gr.Markdown( | |
| # "_(Strongly Prefer A Neutral Strongly Prefer B)_") | |
| # Helper list for choices. The list of (label, value) tuples is how you set custom labels. | |
| CHOICES = [ | |
| # ("A++", "-3"), | |
| ("A+ (Strong Pref)", "-2"), ("A (Pref)", "-1"), | |
| ("No Pref", "0"), | |
| ("B (Pref)", "1"), ("B+ (Strong Pref)", "2"), | |
| # ("B++", "3") | |
| ] | |
| general_look_radio = gr.Radio( | |
| choices=CHOICES, value="0", | |
| label="1. General Quality", | |
| info="Overall, which video do you prefer? In other words, which video is harder to distinguish from real videos?", | |
| type="value", | |
| elem_classes=["full-width-radio", "rating-radio"] | |
| ) | |
| motion_quality_radio = gr.Radio( | |
| choices=CHOICES, value="0", | |
| label="2. Motion Quality and Realistic Dynamics", | |
| info="Which video has more realistic, fluid, and coherent motion, and is physically and socially plausible? Stopping suddenly without reason, collisions between objects, or cars driving in wrong lanes are examples of poor motion quality.", | |
| type="value", | |
| elem_classes=["full-width-radio", "rating-radio"] | |
| ) | |
| visual_quality_radio = gr.Radio( | |
| choices=CHOICES, value="0", | |
| label="3. Visual Quality", | |
| info="Which video has better clarity, fewer artifacts, and more pleasing visuals? Blurry objects, change of color or shape over time, and visual distortions are examples of poor visual quality.", | |
| type="value", | |
| elem_classes=["full-width-radio", "rating-radio"] | |
| ) | |
| submit_button = gr.Button("Submit and Next Video", variant="primary") | |
| with gr.Column(visible=False) as thanks_ui: | |
| thanks_message = gr.Markdown( | |
| "## ✅ Thank You! \n You have completed the evaluation. Your responses have been saved. You can now close this window." | |
| ) | |
| # --- NEW: LEADERBOARD TAB --- | |
| with gr.TabItem("🏆 Leaderboard"): | |
| gr.Markdown("## Model Elo Score Leaderboard") | |
| gr.Markdown( | |
| "This leaderboard is calculated from the 'General Quality' scores submitted by all users. Scores are updated when you press the refresh button or when the app loads.") | |
| refresh_button = gr.Button("Refresh Leaderboard", variant="primary") | |
| leaderboard_df = gr.DataFrame( | |
| label="Elo Scores", | |
| headers=["Rank", "Model", "Elo Score"], | |
| interactive=False | |
| ) | |
| radio_inputs = [general_look_radio, motion_quality_radio, visual_quality_radio] | |
| radio_outputs = [general_look_radio, motion_quality_radio, visual_quality_radio] | |
| start_button.click( | |
| fn=start_session, | |
| inputs=[email_input], | |
| outputs=[user_state, login_ui, eval_ui, thanks_ui, video_a, video_b, progress_text] + radio_outputs | |
| ) | |
| submit_button.click( | |
| fn=record_ratings, | |
| inputs=[user_state] + radio_inputs, | |
| outputs=[user_state, eval_ui, thanks_ui, video_a, video_b, progress_text] + radio_outputs, | |
| ).then(fn=None, inputs=None, outputs=None, js="() => { window.scrollTo(0, 0); }") | |
| # Refresh button click | |
| refresh_button.click( | |
| fn=update_leaderboard, | |
| inputs=None, | |
| outputs=[leaderboard_df] | |
| ) | |
| # Load leaderboard when the app starts | |
| demo.load( | |
| fn=update_leaderboard, | |
| inputs=None, | |
| outputs=[leaderboard_df] | |
| ) | |
| # Launch the app | |
| demo.launch() |