| """ |
| 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. |
| """ |
|
|
| 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 |
|
|
| |
| HF_TOKEN = os.environ.get("HF_TOKEN", "") |
| HF_DATASET_REPO = os.environ.get("HF_DATASET_REPO", "") |
| CSV_PATH = "pairs.csv" |
|
|
| |
| print("Loading pairs.csv ...") |
| _pairs_df = pd.read_csv(CSV_PATH) |
| _pairs = _pairs_df.to_dict(orient="records") |
| print(f"Loaded {len(_pairs)} pairs.") |
|
|
|
|
| |
| VOTES_FILE = "votes.parquet" |
|
|
| 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: |
| 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: |
| |
| 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 = """ |
| /* ββ Main Container & Light Background ββ */ |
| body, .gradio-container { |
| background: #ffffff !important; |
| color: #000000 !important; |
| } |
| |
| /* ββ Instructions Box ββ */ |
| .instructions { |
| background: #f9f9f9; |
| border: 1px solid #ddd; |
| border-radius: 8px; |
| padding: 16px 20px; |
| margin-bottom: 16px; |
| font-size: 0.88rem; |
| line-height: 1.6; |
| color: #444; |
| } |
| .instructions strong { color: #000; } |
| |
| /* ββ Scale Values (Your specific request) ββ */ |
| .scale-list li { color: #555; display: flex; align-items: center; gap: 10px; font-size: 0.85rem; } |
| |
| .scale-val { |
| background: #ffffff !important; |
| color: #000000 !important; |
| border: 1px solid #ddd; /* Added a light border so white-on-white is visible */ |
| border-radius: 4px; |
| padding: 2px 8px; |
| font-weight: 700; |
| font-size: 0.8rem; |
| min-width: 36px; |
| text-align: center; |
| flex-shrink: 0; |
| } |
| |
| /* ββ Progress Bar ββ */ |
| .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; background: #eee; } |
| .progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s ease; background: #000; } |
| |
| /* ββ Header ββ */ |
| h1 { font-size: 1.5rem; font-weight: 700; margin: 28px 0 4px; text-align: center; color: #000; } |
| .subtitle { text-align: center; color: #888; margin-bottom: 20px; font-size: 0.9rem; } |
| |
| /* ββ Done Banner ββ */ |
| .done-banner { |
| background: #fdfdfd; |
| border: 1px solid #bbb; |
| 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; color: #000; } |
| .done-banner p { margin: 0; font-size: 0.95rem; line-height: 1.7; color: #555; } |
| |
| footer { display: none !important; } |
| """ |
|
|
|
|
| |
| def make_app(): |
| with gr.Blocks(css=CSS, title="Image Similarity Rating") as demo: |
|
|
| |
| 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) |
|
|
| |
| gr.HTML("<h1>Image Similarity Rating</h1>") |
| gr.HTML("<p class='subtitle'>Rate how similar Image B is to Image A.</p>") |
|
|
| |
| done_html = gr.HTML(visible=False) |
|
|
| |
| with gr.Column(visible=True) as rating_col: |
|
|
| |
| progress_html = gr.HTML() |
|
|
| |
| 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) |
|
|
| |
| gr.HTML(""" |
| <div class="instructions"> |
| <strong>How similar is Image B to Image A?</strong><br> |
| Image A is the original; Image B was reconstructed by an AI model. |
| Rate their overall visual and semantic similarity: |
| <ul class="scale-list"> |
| <li><span class="scale-val">0</span> Completely different</li> |
| <li><span class="scale-val">1β3</span> Very different; only a few elements match</li> |
| <li><span class="scale-val">4β6</span> Partial match; some key elements correct, but notable differences</li> |
| <li><span class="scale-val">7β9</span> Strong match; mostly correct with minor differences</li> |
| <li><span class="scale-val">10</span> Identical or indistinguishable</li> |
| </ul> |
| </div> |
| """) |
|
|
| |
| score_slider = gr.Slider(minimum=0, maximum=10, step=1, value=5, |
| label="Similarity score (0β10)", interactive=True) |
| next_btn = gr.Button("Submit and continue β", variant="primary", size="lg") |
|
|
| |
|
|
| def build_progress(idx, total): |
| pct = int(idx / total * 100) if total else 0 |
| return f""" |
| <div class="progress-wrap"> |
| <div class="progress-label">{idx} / {total}</div> |
| <div class="progress-track"> |
| <div class="progress-fill" style="width:{pct}%"></div> |
| </div> |
| </div>""" |
|
|
| |
|
|
| def on_load(user_id): |
| queue = random.sample(_pairs, len(_pairs)) |
| total = len(queue) |
| entry = queue[0] |
| return ( |
| queue, 0, [], total, |
| build_progress(0, total), |
| entry["original_path"], |
| entry["final_path"], |
| 5, |
| gr.update(visible=True), |
| gr.update(visible=False), |
| ) |
|
|
| demo.load( |
| on_load, |
| inputs=[user_id_state], |
| outputs=[queue_state, index_state, votes_state, total_state, |
| progress_html, img_left, img_right, score_slider, |
| rating_col, done_html], |
| ) |
|
|
| |
|
|
| def on_next(score, queue, idx, votes, total, user_id): |
| entry = queue[idx] |
| vote = { |
| "user_id": user_id, |
| "timestamp": datetime.utcnow().isoformat(), |
| "vote_index": idx, |
| "score": int(score), |
| "describer": entry["describer"], |
| "generator": entry["generator"], |
| "experiment": entry["experiment"], |
| "episode": entry["episode"], |
| "final_image_url": entry["final_path"], |
| "original_image_url": entry["original_path"], |
| } |
| votes = votes + [vote] |
| idx += 1 |
|
|
| |
| if idx >= total: |
| save_votes_to_hub(votes) |
| done = """ |
| <div class="done-banner"> |
| <div class="done-icon">β</div> |
| <h2>Thank you!</h2> |
| <p>You have rated all image pairs.<br> |
| Your responses have been saved and will help us evaluate AI-generated images.</p> |
| </div>""" |
| return ( |
| votes, idx, |
| build_progress(total, total), |
| gr.update(), |
| gr.update(), |
| gr.update(), |
| gr.update(visible=False), |
| gr.update(value=done, visible=True), |
| ) |
|
|
| |
| next_entry = queue[idx] |
| return ( |
| votes, idx, |
| build_progress(idx, total), |
| gr.update(value=next_entry["original_path"]), |
| gr.update(value=next_entry["final_path"]), |
| 5, |
| gr.update(visible=True), |
| gr.update(visible=False), |
| ) |
|
|
| next_btn.click( |
| on_next, |
| inputs=[score_slider, queue_state, index_state, votes_state, |
| total_state, user_id_state], |
| outputs=[votes_state, index_state, |
| progress_html, img_left, img_right, score_slider, |
| rating_col, done_html], |
| ) |
|
|
| return demo |
|
|
|
|
| if __name__ == "__main__": |
| make_app().launch() |