""" Image Similarity Rating App ---------------------------- Reads pairs.csv (committed to the Space repo) and shows all pairs in random order to each user. No repetitions within a session. pairs.csv columns: describer, generator, experiment, episode, turn, original_image_url, generated_image_url """ import io import os import uuid import random import pandas as pd import gradio as gr from datetime import datetime from datasets import Dataset from huggingface_hub import HfApi # ── Config ──────────────────────────────────────────────────────────────────── HF_TOKEN = os.environ.get("HF_TOKEN", "") HF_DATASET_REPO = os.environ.get("HF_DATASET_REPO", "") # where votes are saved CSV_PATH = "pairs.csv" # committed to Space repo # ── Load pairs CSV once at startup ─────────────────────────────────────────── print("Loading pairs.csv ...") _pairs_df = pd.read_csv(CSV_PATH) _pairs = _pairs_df.to_dict(orient="records") print(f"Loaded {len(_pairs)} pairs.") # ── Persistence ─────────────────────────────────────────────────────────────── VOTES_FILE = "votes.parquet" # single file in the results repo def save_votes_to_hub(votes: list[dict]): """ Append this session's votes to a single votes.parquet in the results repo. Strategy: download existing file -> concat -> upload back. """ if not HF_DATASET_REPO or not HF_TOKEN: print("HF_DATASET_REPO or HF_TOKEN not set -- votes not saved remotely.") return try: api = HfApi(token=HF_TOKEN) new_df = pd.DataFrame(votes) # Try to download the existing parquet and append try: existing_path = api.hf_hub_download( repo_id=HF_DATASET_REPO, repo_type="dataset", filename=VOTES_FILE, ) existing_df = pd.read_parquet(existing_path) combined_df = pd.concat([existing_df, new_df], ignore_index=True) except Exception: # File doesn't exist yet -- first run combined_df = new_df buf = io.BytesIO() combined_df.to_parquet(buf, index=False) buf.seek(0) api.upload_file( path_or_fileobj=buf, path_in_repo=VOTES_FILE, repo_id=HF_DATASET_REPO, repo_type="dataset", ) print(f"Appended {len(votes)} votes to {HF_DATASET_REPO}/{VOTES_FILE} " f"(total rows: {len(combined_df)})") except Exception as ex: print(f"Failed to save votes: {ex}") # ── CSS ─────────────────────────────────────────────────────────────────────── CSS = """ /* Light mode */ @media (prefers-color-scheme: light) { body, .gradio-container { background: #f5f5f5; color: #111; } .instructions { background: #fff; border-color: #ddd; color: #444; } .instructions strong { color: #111; } .scale-list li { color: #555; } .done-banner { background: #fff; border-color: #bbb; } .done-banner p { color: #555; } .progress-track { background: #ddd; } .progress-fill { background: #333; } } /* Dark mode */ @media (prefers-color-scheme: dark) { body, .gradio-container { background: #141414; color: #e8e8e8; } .instructions { background: #1e1e1e; border-color: #333; color: #bbb; } .instructions strong { color: #eee; } .scale-list li { color: #999; } .scale-val { background: #e8e8e8 !important; color: #111 !important; } .done-banner { background: #1e1e1e; border-color: #444; } .done-banner h2 { color: #e8e8e8; } .done-banner p { color: #888; } .progress-track { background: #333; } .progress-fill { background: #ccc; } } /* ── Header ── */ h1 { font-size: 1.5rem; font-weight: 700; margin: 28px 0 4px; text-align: center; } .subtitle { text-align: center; color: #888; margin-bottom: 20px; font-size: 0.9rem; } /* ── Progress ── */ .progress-wrap { margin-bottom: 16px; } .progress-label { font-size: 0.8rem; color: #888; margin-bottom: 4px; text-align: right; } .progress-track { height: 4px; border-radius: 4px; overflow: hidden; } .progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s ease; } /* ── Instructions ── */ .instructions { border: 1px solid; border-radius: 8px; padding: 16px 20px; margin-bottom: 16px; font-size: 0.88rem; line-height: 1.6; } .scale-list { list-style: none; padding: 0; margin: 10px 0 0; display: flex; flex-direction: column; gap: 6px; } .scale-list li { display: flex; align-items: center; gap: 10px; font-size: 0.85rem; } .scale-val { background: #222; color: #fff; border-radius: 4px; padding: 2px 8px; font-weight: 700; font-size: 0.8rem; min-width: 36px; text-align: center; flex-shrink: 0; } /* ── Done banner ── */ .done-banner { border: 1px solid; border-radius: 8px; padding: 64px 24px; text-align: center; margin: 32px 0; } .done-icon { font-size: 3.5rem; margin-bottom: 16px; } .done-banner h2 { font-size: 1.8rem; margin: 0 0 12px; } .done-banner p { margin: 0; font-size: 0.95rem; line-height: 1.7; } footer { display: none !important; } """ # ── App ─────────────────────────────────────────────────────────────────────── def make_app(): with gr.Blocks(css=CSS, title="Image Similarity Rating") as demo: # State user_id_state = gr.State(lambda: str(uuid.uuid4())) queue_state = gr.State([]) index_state = gr.State(0) votes_state = gr.State([]) total_state = gr.State(0) # Header gr.HTML("
Rate how similar Image B is to Image A.
") # Done banner — hidden until all pairs are rated done_html = gr.HTML(visible=False) # Rating UI — hidden when done with gr.Column(visible=True) as rating_col: # Progress progress_html = gr.HTML() # Images with gr.Row(equal_height=True): img_left = gr.Image(label="Image A — Original", show_label=True, interactive=False, height=520) img_right = gr.Image(label="Image B — Generated", show_label=True, interactive=False, height=520) # Instructions gr.HTML("""