HDR-user-study / app.py
naomiKenKorem's picture
Super-squash branch 'main' using huggingface_hub
6557c00
"""Gradio app for the text-to-HDR pairwise user study.
Each rater is shown 90 stacked-bracket comparisons (Method A on top vs
Method B on bottom, methods hidden) and asked which set looks more
natural. Votes are appended to a private HuggingFace dataset.
Deploy as a free HF Space:
- app.py, requirements.txt, pairs.json, prompts.json, pairs/*.png
The app does NOT reveal which method is on top β€” the assignment is
recorded in pairs.json and joined post-hoc when scoring.
"""
import json
import os
import random
import uuid
from datetime import datetime, timezone
from pathlib import Path
import gradio as gr
from huggingface_hub import HfApi
ROOT = Path(__file__).resolve().parent
PAIRS = json.loads((ROOT / "pairs.json").read_text())
PROMPTS = {p["n"]: p for p in json.loads((ROOT / "prompts.json").read_text())}
# Configure via Space secrets:
HF_TOKEN = os.environ.get("HF_TOKEN") # write-scoped token
HF_DATASET_REPO = os.environ.get("HF_DATASET_REPO", "") # e.g. "naomi/t2hdr-user-study-votes"
CHOICES = [
("Top is much better", "top_much"),
("Top is slightly better", "top_slight"),
("Tie / cannot tell", "tie"),
("Bottom is slightly better", "bot_slight"),
("Bottom is much better", "bot_much"),
]
def _hf_upload(path: Path, repo_path: str) -> None:
"""Upload a single file to the HF dataset repo."""
if not HF_TOKEN or not HF_DATASET_REPO:
return # local dev β€” skip upload
api = HfApi(token=HF_TOKEN)
api.upload_file(
path_or_fileobj=str(path),
path_in_repo=repo_path,
repo_id=HF_DATASET_REPO,
repo_type="dataset",
)
def new_session() -> dict:
"""Initialize a rater session: rater_id + shuffled pair order."""
rater_id = uuid.uuid4().hex[:12]
order = list(range(len(PAIRS)))
random.shuffle(order)
return {"rater_id": rater_id, "order": order, "idx": 0, "votes": []}
def _stim_for(pair_idx: int, position: int) -> tuple[str, str, str]:
"""pair_idx = index into PAIRS; position = 0-based position in rater queue."""
pair = PAIRS[pair_idx]
img = str(ROOT / pair["image"])
prompt = PROMPTS[pair["prompt_n"]]
progress = f"**Pair {position + 1} / {len(PAIRS)}**"
caption = (
f"**Prompt #{prompt['n']:03d} β€” {prompt['cat']}**\n\n"
f"{prompt['text']}\n\n"
f"Which set of 3 exposures looks more natural and more like a "
f"high-quality photograph of the scene above?"
)
return img, caption, progress
def start_session():
state = new_session()
img, caption, progress = _stim_for(state["order"][0], state["idx"])
return state, img, caption, progress, gr.update(visible=False), gr.update(visible=True)
def cast_vote(state: dict, choice_label: str):
print(f"[cast_vote] state.idx={state['idx'] if state else None} choice={choice_label!r}", flush=True)
if state is None or "order" not in state:
return (state, None, "Click **Begin** to start.", "",
gr.update(visible=True), gr.update(visible=False), gr.update(value=None))
choice_value = dict(CHOICES).get(choice_label, "tie")
pair = PAIRS[state["order"][state["idx"]]]
record = {
"rater_id": state["rater_id"],
"pair_id": pair["pair_id"],
"prompt_n": pair["prompt_n"],
"top_method": pair["top_method"],
"bottom_method": pair["bottom_method"],
"choice": choice_value,
"ts": datetime.now(timezone.utc).isoformat(),
}
# Build a NEW state dict (immutable update) so Gradio detects the change.
new_state = {
"rater_id": state["rater_id"],
"order": state["order"],
"idx": state["idx"] + 1,
"votes": state["votes"] + [record],
}
print(f"[cast_vote] new_state.idx={new_state['idx']} votes_count={len(new_state['votes'])}", flush=True)
if new_state["idx"] >= len(PAIRS):
out = ROOT / f"votes_{new_state['rater_id']}.jsonl"
out.write_text("\n".join(json.dumps(v) for v in new_state["votes"]) + "\n")
try:
_hf_upload(out, f"votes/votes_{new_state['rater_id']}.jsonl")
except Exception as e:
print(f"upload failed: {e}")
return (
new_state, None,
f"### βœ… Done β€” thank you!\n\n"
f"Your rater id: `{new_state['rater_id']}`. "
f"You can close this tab.",
f"{len(new_state['votes'])} / {len(PAIRS)} complete",
gr.update(visible=False), gr.update(visible=False), gr.update(value=None),
)
img, caption, progress = _stim_for(new_state["order"][new_state["idx"]], new_state["idx"])
# Reset the radio so the rater has to actively choose for each pair.
return (new_state, img, caption, progress,
gr.update(visible=False), gr.update(visible=True), gr.update(value=None))
with gr.Blocks(title="Text-to-HDR study") as demo:
gr.Markdown(
"""
# Text-to-HDR β€” pairwise comparison
You will see **30 image pairs**. Each pair has two stacked
rows: top and bottom. Each row shows the same scene at three
exposures (dark / normal / bright). Your task is to pick which
row looks more natural and more like a real high-quality
photograph of the prompt.
Pick a choice in the radio, then click **Next pair**.
Take your time. Total time β‰ˆ 3–5 minutes.
"""
)
state = gr.State(None)
start_panel = gr.Group(visible=True)
with start_panel:
start_btn = gr.Button("Begin", variant="primary")
rate_panel = gr.Group(visible=False)
with rate_panel:
progress = gr.Markdown("")
caption = gr.Markdown("")
image = gr.Image(label="", interactive=False, height=600)
choice_radio = gr.Radio(
choices=[label for label, _ in CHOICES],
label="Your judgement",
value=None,
)
submit_btn = gr.Button("Next pair", variant="primary")
submit_btn.click(
cast_vote,
inputs=[state, choice_radio],
outputs=[state, image, caption, progress, start_panel, rate_panel, choice_radio],
)
start_btn.click(
start_session,
outputs=[state, image, caption, progress, start_panel, rate_panel],
)
if __name__ == "__main__":
demo.launch()