videoeval_humaneval / app_newbutold.py
Youngsun Lim
dataset update
bd09d55
import os, io, csv, json, random
from datetime import datetime
import gradio as gr
from huggingface_hub import HfApi, hf_hub_download
# -------------------- Config --------------------
REPO_ID = os.getenv("RESULTS_REPO", "sgtlim/videoeval_results") # ์—…๋กœ๋“œํ•œ ๋ฆฌํฌ์™€ ์ผ์น˜
HF_TOKEN = os.getenv("HF_TOKEN")
RESULTS_FILE = "results.csv"
TOTAL_PER_PARTICIPANT = 30 # ๋ชฉํ‘œ ํ‰๊ฐ€ ๊ฐœ์ˆ˜(์„ธ์…˜ ๊ธฐ์ค€)
# videos.json ์˜ˆ์‹œ: {"url": "...mp4", "id": "BodyWeightSquats__XXXX.mp4", "action": "BodyWeightSquats"}
with open("videos.json", "r", encoding="utf-8") as f:
V = json.load(f)
api = HfApi()
# ๊ต์ˆ˜๋‹˜ ์ง€์นจ(๊ทธ๋Œ€๋กœ, ๊ตต๊ฒŒ ์ฒ˜๋ฆฌ ํฌํ•จ)
INSTRUCTION_MD = """
**Task:** You will watch a series of **AI-generated videos**. For each video, your job is to rate how well the personโ€™s action in the AI-generated video matches the action specified as "**expected action**". Some things to keep in mind:
- The generated video should **capture** the expected action **throughout the video**.
- Try to **focus only** on the expected action and do **not** judge **video quality**, **attractiveness**, **background**, **camera motion**, or **objects**.
- You will be **paid** once **all the videos are viewed and rated**.
"""
# -------------------- Helper funcs --------------------
def _load_eval_counts():
"""
Hugging Face dataset์˜ results.csv๋ฅผ ์ฝ์–ด video_id๋ณ„ ํ‰๊ฐ€ ๊ฐœ์ˆ˜(dict)๋ฅผ ๋ฐ˜ํ™˜.
์—†์œผ๋ฉด 0์œผ๋กœ ์ดˆ๊ธฐํ™”.
"""
# ๋ชจ๋“  id๋ฅผ 0์œผ๋กœ ์ดˆ๊ธฐํ™”
counts = {}
for v in V:
vid = _get_video_id(v)
counts[vid] = 0
b = _read_csv_bytes()
if not b:
return counts
s = io.StringIO(b.decode("utf-8", errors="ignore"))
r = csv.reader(s)
rows = list(r)
if not rows:
return counts
# ํ—ค๋” ํŒŒ์•…
header = rows[0]
body = rows[1:] if header and ("video_id" in header or "overall" in header) else rows
vid_col = None
if header and "video_id" in header:
vid_col = header.index("video_id")
for row in body:
try:
vid = row[vid_col] if vid_col is not None else row[2] # ๊ธฐ๋ณธ ํฌ๋งท: ts, pid, video_id, overall, notes
if vid in counts:
counts[vid] += 1
except Exception:
continue
return counts
def _get_video_id(v: dict) -> str:
if "id" in v and v["id"]:
return v["id"]
# id๊ฐ€ ์—†์œผ๋ฉด URL ํŒŒ์ผ๋ช…์œผ๋กœ ๋Œ€์ฒด
return os.path.basename(v.get("url", ""))
def _read_csv_bytes():
try:
p = hf_hub_download(
repo_id=REPO_ID, filename=RESULTS_FILE, repo_type="dataset",
token=HF_TOKEN, local_dir="/tmp", local_dir_use_symlinks=False
)
return open(p, "rb").read()
except Exception:
return None
def _append(old_bytes, row):
s = io.StringIO()
w = csv.writer(s)
if not old_bytes:
# โœ… ์ƒˆ ํ—ค๋”
w.writerow(["ts_iso", "participant_id", "video_id", "overall", "notes"])
else:
s.write(old_bytes.decode("utf-8", errors="ignore"))
w.writerow(row)
return s.getvalue().encode("utf-8")
# def push(participant_id, action_name, score, notes=""):
# if not participant_id or not participant_id.strip():
# return gr.update(visible=True, value="โ— Please enter your Participant ID before proceeding.")
# if not action_name or score is None:
# return gr.update(visible=True, value="โ— Fill out all fields.")
# old = _read_csv_bytes()
# row = [
# datetime.utcnow().isoformat(),
# participant_id.strip(),
# action_name,
# float(score),
# notes or ""
# ]
# newb = _append(old, row)
# api.upload_file(
# path_or_fileobj=io.BytesIO(newb),
# path_in_repo=RESULTS_FILE,
# repo_id=REPO_ID,
# repo_type="dataset",
# token=HF_TOKEN,
# commit_message="append"
# )
# return gr.update(visible=True, value=f"โœ… Saved for {action_name}.")
def push(participant_id, video_id, score, notes=""):
if not participant_id or not participant_id.strip():
return gr.update(visible=True, value="โ— Please enter your Participant ID before proceeding.")
if not video_id or score is None:
return gr.update(visible=True, value="โ— Fill out all fields.")
try:
old = _read_csv_bytes()
row = [
datetime.utcnow().isoformat(),
participant_id.strip(),
video_id, # โœ… action ๋Œ€์‹  video_id ์ €์žฅ
float(score), # overall
notes or ""
]
newb = _append(old, row)
if not REPO_ID:
return gr.update(visible=True, value="โ— RESULTS_REPO is not set.")
if not HF_TOKEN:
return gr.update(visible=True, value="โ— HF_TOKEN is missing. Set a write token for the dataset repo.")
api.upload_file(
path_or_fileobj=io.BytesIO(newb),
path_in_repo=RESULTS_FILE,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN,
commit_message="append"
)
return gr.update(visible=True, value=f"โœ… Saved for {video_id}.")
except Exception as e:
return gr.update(
visible=True,
value=f"โŒ Save failed: {type(e).__name__}: {e}\n"
f"- Check HF_TOKEN permission\n- Check REPO_ID\n- Create dataset repo if missing"
)
def _extract_action(v):
if "action" in v and v["action"]:
return v["action"]
raw = v.get("id", "")
return raw.split("__")[0].split(".")[0]
def pick_one():
v = random.choice(V)
return v["url"], _extract_action(v)
def _progress_html(done, total):
pct = int(100 * done / max(1, total))
return f"""
<div style="border:1px solid #ddd; height:20px; border-radius:6px; overflow:hidden; margin-top:6px;">
<div style="height:100%; width:{pct}%; background:#3b82f6;"></div>
</div>
<div style="font-size:12px; margin-top:4px;">{done} / {total}</div>
"""
def _build_order_with_anchor(total:int, anchor_idx:int, repeats:int, pool_size:int, min_gap:int=1):
"""
total: TOTAL_PER_PARTICIPANT (e.g., 30)
anchor_idx: index of the anchor video in V (0 for first item)
repeats: how many times to show anchor (e.g., 5)
pool_size: len(V)
min_gap: ์ตœ์†Œ ๊ฐ„๊ฒฉ(์ธ์ ‘ ๊ธˆ์ง€ => 1)
return: list of indices (length=total)
"""
assert repeats <= total, "repeats must be <= total"
assert pool_size >= 1, "videos pool must be non-empty"
# 1) ๋‹ค๋ฅธ ๋น„๋””์˜ค 25๊ฐœ(์ค‘๋ณต ์—†์ด) ๋ฝ‘๊ธฐ
others_needed = total - repeats
# anchor๋ฅผ ์ œ์™ธํ•œ ํ›„๋ณด ์ธ๋ฑ์Šค
candidates = list(range(1, pool_size)) if anchor_idx == 0 else [i for i in range(pool_size) if i != anchor_idx]
if len(candidates) < others_needed:
raise ValueError("Not enough unique non-anchor videos to fill the schedule without duplication.")
others = random.sample(candidates, k=others_needed)
# 2) ๊ธฐ๋ณธ ์‹œํ€€์Šค(others)๋ฅผ ๋ฌด์ž‘์œ„๋กœ ์„ž๊ธฐ
random.shuffle(others)
# 3) ์•ต์ปค๋ฅผ min_gap๋ฅผ ๋งŒ์กฑํ•˜๋„๋ก ์‚ฝ์ž…ํ•  ์œ„์น˜ ์„ ์ •
# 30๊ฐœ๋ฅผ 5๊ตฌ๊ฐ„์œผ๋กœ ๋‚˜๋ˆ , ๊ฐ ๊ตฌ๊ฐ„ ๋‚ด์—์„œ ์ถฉ๋Œ ๋œ ๋‚˜๊ฒŒ ๋ฐฐ์น˜
# (๊ฐ„๋‹จํ•˜๊ณ  ์•ˆ์ •์ ์ธ ๋ฐฉ์‹)
seq = others[:] # ๊ธธ์ด=25
anchor_positions = []
segment = total // repeats # 30//5 = 6
for k in range(repeats):
# ๊ฐ ๊ตฌ๊ฐ„ [k*segment, (k+1)*segment) ์•ˆ์—์„œ ํ›„๋ณด ์œ„์น˜๋ฅผ ๊ณ ๋ฆ„
lo = k * segment
hi = (k + 1) * segment if k < repeats - 1 else total # ๋งˆ์ง€๋ง‰์€ ๋๊นŒ์ง€
# ๊ฒฝ๊ณ„ ๋‚ด ์ž„์˜ ์˜คํ”„์…‹ ์„ ํƒ (์—ฌ์œ ๋ฅผ ๋‘๊ณ  ์ถฉ๋Œ์„ ํ”ผํ•จ)
candidate_pos = random.randrange(lo, hi)
# ์ธ์ ‘ ๊ธˆ์ง€ ๋ณด์ •: ์ด๋ฏธ ๋ฐฐ์ •๋œ anchor ์œ„์น˜๋“ค๊ณผ์˜ ๊ฑฐ๋ฆฌ๊ฐ€ min_gap ์ด์ƒ ๋˜๋„๋ก ์กฐ์ •
# ํ•„์š” ์‹œ ์ขŒ์šฐ๋กœ ๊ทผ์ ‘ํ•œ ๋นˆ ์Šฌ๋กฏ ํƒ์ƒ‰
def ok(pos):
return all(abs(pos - p) >= min_gap + 1 for p in anchor_positions) # ์—ฐ์†๊ธˆ์ง€ => ๊ฑฐ๋ฆฌ >= 2
# ๊ทผ๋ฐฉ ํƒ์ƒ‰ ํญ
found = None
for delta in range(0, segment): # ๊ตฌ๊ฐ„ ํฌ๊ธฐ ๋‚ด์—์„œ ํƒ์ƒ‰
# ์ขŒ/์šฐ ๋ฒˆ๊ฐˆ์•„๊ฐ€๋ฉฐ ํ›„๋ณด ์‹œ๋„
for sign in (+1, -1):
pos = candidate_pos + sign * delta
if 0 <= pos < total and ok(pos):
found = pos
break
if found is not None:
break
if found is None:
# ์ตœํ›„: 0..total-1 ๋ฒ”์œ„์—์„œ ์•„๋ฌด ๋ฐ๋‚˜ ์ถฉ๋Œ ์—†๋Š” ๊ณณ ์ฐพ๊ธฐ
for pos in range(total):
if ok(pos):
found = pos
break
if found is None:
raise RuntimeError("Failed to place anchor without adjacency. Try different strategy or loosen min_gap.")
anchor_positions.append(found)
# 4) others๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ธธ์ด total์˜ ๋นˆ ์‹œํ€€์Šค๋ฅผ ๋งŒ๋“ค๊ณ  anchor๋ฅผ ์ฃผ์ž…
# ์šฐ์„  ๋นˆ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋งŒ๋“ค๊ณ  anchor ์œ„์น˜๋ฅผ ์ฑ„์šด ํ›„, ๋‚˜๋จธ์ง€๋ฅผ others๋กœ ์ฑ„์›€
result = [None] * total
for pos in anchor_positions:
result[pos] = anchor_idx
# others ํฌ์ธํ„ฐ
j = 0
for i in range(total):
if result[i] is None:
result[i] = others[j]
j += 1
# ์•ˆ์ „์ฒดํฌ
assert len(result) == total
# ์ธ์ ‘ anchor ์—†๋Š”์ง€ ํ™•์ธ
for i in range(1, total):
assert not (result[i] == anchor_idx and result[i-1] == anchor_idx), "Adjacent anchors found."
# anchor ๊ฐœ์ˆ˜ ํ™•์ธ
assert sum(1 for x in result if x == anchor_idx) == repeats, "Anchor count mismatch."
return result
# -------------------- Example videos (download to local cache) --------------------
EXAMPLES = {
"BodyWeightSquats": {
"real": "examples/BodyWeightSquats_real.mp4",
"bad": "examples/BodyWeightSquats_bad.mp4",
},
"WallPushUps": {
"real": "examples/WallPushUps_real.mp4",
"bad": "examples/WallPushUps_bad.mp4",
},
}
EX_CACHE = {}
for cls, files in EXAMPLES.items():
EX_CACHE[cls] = {"real": None, "bad": None}
for kind, fname in files.items():
try:
EX_CACHE[cls][kind] = hf_hub_download(
repo_id=REPO_ID,
filename=fname,
repo_type="dataset",
token=HF_TOKEN,
local_dir="/tmp",
local_dir_use_symlinks=False,
)
except Exception as e:
print(f"[WARN] example missing: {cls} {kind} -> {fname}: {e}")
GLOBAL_CSS = """
/* ===== ๊ณตํ†ต ๋ณ€์ˆ˜ ํˆฌ๋ช…ํ™” (v3/v4 ๋‘˜๋‹ค) ===== */
:root, .gradio-container {
--body-background-fill: transparent !important;
--background-fill-primary: transparent !important;
--background-fill-secondary: transparent !important;
--block-background-fill: transparent !important;
--block-border-color: transparent !important;
--panel-background-fill: transparent !important;
--panel-border-color: transparent !important;
--section-header-background-fill: transparent !important;
--shadow-drop: 0 0 0 rgba(0,0,0,0) !important;
--shadow-spread: 0 0 0 rgba(0,0,0,0) !important;
}
/* ===== v4(Tailwind ๊ธฐ๋ฐ˜)์—์„œ ์ž์ฃผ ์“ฐ์ด๋Š” ๋ฐฐ๊ฒฝ/ํ…Œ๋‘๋ฆฌ/๊ทธ๋ฆผ์ž ์ œ๊ฑฐ ===== */
.gradio-container .bg-white,
.gradio-container .bg-gray-50,
.gradio-container .bg-gray-100,
.gradio-container .bg-slate-50,
.gradio-container .bg-neutral-50,
.gradio-container .bg-secondary,
.gradio-container .border,
.gradio-container .shadow,
.gradio-container .shadow-sm,
.gradio-container .shadow-md,
.gradio-container .ring-1,
.gradio-container .ring,
.gradio-container .gr-card,
.gradio-container .prose > *:where(hr) {
background: transparent !important;
box-shadow: none !important;
border-color: transparent !important;
}
/* ===== v3 ์ปดํฌ๋„ŒํŠธ ๊ณ„์—ด ===== */
.gradio-container .gr-panel,
.gradio-container .gr-group,
.gradio-container .gr-box,
.gradio-container .gr-row,
.gradio-container .gr-column,
.gradio-container .gr-accordion,
.gradio-container .gr-block,
.gradio-container .gr-form,
.gradio-container .gr-tabs,
.gradio-container .gr-tabitem,
.gradio-container .gr-section-header {
background: transparent !important;
box-shadow: none !important;
border: none !important;
}
/* ๊ตฌ๋ถ„์„ /ํ—ค๋” ๋ฐ” ์ œ๊ฑฐ */
.gradio-container hr,
.gradio-container .gr-divider,
.gradio-container .gr-accordion .label {
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
/* ๋ฐ”๊นฅ์ชฝ ํŽ˜์ด์ง€ ๋ฐฐ๊ฒฝ๋„ ๊ฐ•์ œ๋กœ ํˆฌ๋ช…/ํฐ์ƒ‰์œผ๋กœ */
html, body, .gradio-container { background: transparent !important; }
/* ๊ธฐ์กด CSS ์•„๋ž˜์— ์ถ”๊ฐ€ */
#eval [class*="bg-"],
#eval [class*="border"],
#eval [class*="shadow"],
#eval .gr-panel, #eval .gr-group, #eval .gr-box, #eval .gr-row, #eval .gr-column,
#eval .gr-block, #eval .gr-form, #eval .gr-section-header, #eval .gr-accordion {
background: transparent !important;
border-color: transparent !important;
box-shadow: none !important;
}
#eval .gr-form, #eval .gr-panel { background: transparent !important; box-shadow:none !important; border:none !important; }
"""
# -------------------- UI --------------------
with gr.Blocks(fill_height=True, css=GLOBAL_CSS) as demo:
order_state = gr.State(value=[]) # v4์—์„œ๋Š” value= ๊ถŒ์žฅ
ptr_state = gr.State(value=0)
cur_video_id = gr.State(value="")
# ------------------ PAGE 1: Intro + Examples ------------------
page_intro = gr.Group(visible=True)
with page_intro:
gr.Markdown("## ๐ŸŽฏ Action Consistency Human Evaluation")
gr.Markdown(INSTRUCTION_MD)
# Examples: Squats
with gr.Group():
gr.Markdown("### Examples: BodyWeightSquats")
with gr.Row():
with gr.Column():
gr.Markdown("**Expected depiction of action**")
gr.Video(value=EX_CACHE["BodyWeightSquats"]["real"], height=240, autoplay=False)
with gr.Column():
gr.Markdown("**Poorly generated action**")
gr.Video(value=EX_CACHE["BodyWeightSquats"]["bad"], height=240, autoplay=False)
if not (EX_CACHE["BodyWeightSquats"]["real"] and EX_CACHE["BodyWeightSquats"]["bad"]):
gr.Markdown("> โš ๏ธ Upload `examples/BodyWeightSquats_real.mp4` and `_bad.mp4` to show both samples.")
# Examples: WallPushUps
with gr.Group():
gr.Markdown("### Examples: WallPushUps")
with gr.Row():
with gr.Column():
gr.Markdown("**Expected depiction of action**")
gr.Video(value=EX_CACHE["WallPushUps"]["real"], height=240, autoplay=False)
with gr.Column():
gr.Markdown("**Poorly generated action**")
gr.Video(value=EX_CACHE["WallPushUps"]["bad"], height=240, autoplay=False)
if not (EX_CACHE["WallPushUps"]["real"] and EX_CACHE["WallPushUps"]["bad"]):
gr.Markdown("> โš ๏ธ Upload `examples/WallPushUps_real.mp4` and `_bad.mp4` to show both samples.")
understood = gr.Checkbox(label="I have read and understand the task.", value=False)
start_btn = gr.Button("Yes, start", variant="secondary", interactive=False)
def _toggle_start(checked: bool):
return gr.update(interactive=checked, variant=("primary" if checked else "secondary"))
understood.change(_toggle_start, inputs=understood, outputs=start_btn)
# ------------------ PAGE 2: Evaluation ------------------
page_eval = gr.Group(visible=False, elem_id="eval")
with page_eval:
# PID ์ž…๋ ฅ
with gr.Row():
pid = gr.Textbox(label="Participant ID (required)", placeholder="e.g., Youngsun-2025/10/01")
# ์ง€์นจ(์›๋ฌธ) + ๋น„๋””์˜ค + ์ง„ํ–‰๋ฐ” / ์˜ค๋ฅธ์ชฝ์— ์Šฌ๋ผ์ด๋” + Save&Next
with gr.Row(equal_height=True):
with gr.Column(scale=1):
gr.Markdown(INSTRUCTION_MD) # ๊ต์ˆ˜๋‹˜ ๋ฌธ๊ตฌ ๊ทธ๋Œ€๋กœ
video = gr.Video(label="Video", height=360)
progress = gr.HTML(_progress_html(0, TOTAL_PER_PARTICIPANT))
with gr.Column(scale=1):
action_tb = gr.Textbox(label="Expected action", interactive=False)
score = gr.Slider(minimum=0.0, maximum=10.0, step=0.1, value=5.0,
label="Action Consistency (0.0 (Worst) - 10.0 (Best))")
save_next = gr.Button("๐Ÿ’พ Save & Next โ–ถ", variant="secondary", interactive=False)
status = gr.Markdown(visible=False)
done_state = gr.State(0)
# PID ์ž…๋ ฅ์— ๋”ฐ๋ผ Save&Next ํ† ๊ธ€
def _toggle_by_pid(pid_text: str):
enabled = bool(pid_text and pid_text.strip())
return gr.update(interactive=enabled, variant=("primary" if enabled else "secondary"))
pid.change(_toggle_by_pid, inputs=pid, outputs=save_next)
# -------- ํŽ˜์ด์ง€ ์ „ํ™˜ & ์ฒซ ๋กœ๋“œ --------
ANCHOR_IDX = 0 # videos.json์˜ ๋งจ ์ฒซ ๋น„๋””์˜ค
ANCHOR_REPEATS = 5 # ์•ต์ปค 5ํšŒ
MIN_GAP = 1 # ์•ต์ปค ์—ฐ์† ๊ธˆ์ง€(์ธ์ ‘ ๊ธˆ์ง€)
def _build_order_least_first_with_anchor(total:int, anchor_idx:int, repeats:int, min_gap:int=1):
"""
- results.csv๋ฅผ ์ฝ์–ด video_id๋ณ„ ์นด์šดํŠธ๋ฅผ ๊ณ„์‚ฐ
- ์•ต์ปค(์ฒซ ๋น„๋””์˜ค) 5ํšŒ ํฌํ•จ, ์—ฐ์† ๊ธˆ์ง€
- ๋‚˜๋จธ์ง€๋Š” '๊ฐ€์žฅ ์ ๊ฒŒ ํ‰๊ฐ€๋œ ์ˆœ'์œผ๋กœ ์ค‘๋ณต ์—†์ด ์ฑ„์›€
"""
assert repeats <= total
N = len(V)
assert N >= 1
# 0) id ๋งคํ•‘
def vid_of(i): return _get_video_id(V[i])
# 1) ํ˜„์žฌ ๋ˆ„์  ์นด์šดํŠธ ๋กœ๋“œ
counts = _load_eval_counts()
# 2) ์•ต์ปค ์ œ์™ธ ํ›„๋ณด(์ค‘๋ณต ์—†์ด) ์ •๋ ฌ: ์นด์šดํŠธ ์˜ค๋ฆ„์ฐจ์ˆœ, ๋™๋ฅ ์€ ๋žœ๋ค ์…”ํ”Œ
anchor_vid = vid_of(anchor_idx)
candidates = [i for i in range(N) if i != anchor_idx]
# ๋™๋ฅ  ๋žœ๋คํ™”๋ฅผ ์œ„ํ•ด ์ผ๋‹จ ์…”ํ”Œ
random.shuffle(candidates)
candidates.sort(key=lambda i: counts.get(vid_of(i), 0))
others_needed = total - repeats
if len(candidates) < others_needed:
raise ValueError("Not enough unique non-anchor videos to fill the schedule without duplication.")
others = candidates[:others_needed] # ์ค‘๋ณต ์—†์ด ์„ ํƒ
# 3) others๋ฅผ ๋ฒ ์ด์Šค ์‹œํ€€์Šค๋กœ(๋žœ๋ค ์‚ด์ง ์„ž๊ธฐ)
random.shuffle(others)
# 4) ์•ต์ปค๋ฅผ ๊ตฌ๊ฐ„ ๋ฐฐ์น˜(์—ฐ์† ๊ธˆ์ง€)
seq = [None] * total
segment = total // repeats if repeats > 0 else total
anchor_positions = []
for k in range(repeats):
lo = k * segment
hi = (k + 1) * segment if k < repeats - 1 else total
cand = random.randrange(lo, hi)
def ok(pos):
return all(abs(pos - p) >= (min_gap + 1) for p in anchor_positions)
found = None
for d in range(0, max(1, segment)):
for sgn in (+1, -1):
pos = cand + sgn * d
if 0 <= pos < total and ok(pos):
found = pos
break
if found is not None:
break
if found is None:
# ๋งˆ์ง€๋ง‰ ์ˆ˜๋‹จ: ์ „์ฒด ํƒ์ƒ‰
for pos in range(total):
if ok(pos):
found = pos
break
if found is None:
raise RuntimeError("Failed to place anchor without adjacency.")
anchor_positions.append(found)
for pos in anchor_positions:
seq[pos] = anchor_idx
# 5) ๋นˆ ์ž๋ฆฌ๋ฅผ others๋กœ ์ฑ„์šฐ๊ธฐ
j = 0
for i in range(total):
if seq[i] is None:
seq[i] = others[j]
j += 1
# 6) ์•ˆ์ „ ์ฒดํฌ
assert sum(1 for x in seq if x == anchor_idx) == repeats
for i in range(1, total):
assert not (seq[i] == anchor_idx and seq[i-1] == anchor_idx), "Adjacent anchors found."
return seq
# def _start_and_load_first():
# total = TOTAL_PER_PARTICIPANT
# order = _build_order_with_anchor(
# total=total,
# anchor_idx=ANCHOR_IDX,
# repeats=ANCHOR_REPEATS,
# pool_size=len(V),
# min_gap=1 # ์ธ์ ‘ ๊ธˆ์ง€
# )
# first_idx = order[0]
# v0 = V[first_idx]
# url0 = v0["url"]
# action0 = _extract_action(v0)
# vid0 = _get_video_id(v0) # โœ… ์—ฌ๊ธฐ์„œ ์›๋ณธ id
# return (
# gr.update(visible=False), # page_intro off
# gr.update(visible=True), # page_eval on
# url0, # video
# action0, # action_tb (ํ‘œ์‹œ์šฉ)
# 5.0, # score ์ดˆ๊ธฐ๊ฐ’
# gr.update(visible=False, value=""),
# 0, # done_state
# _progress_html(0, TOTAL_PER_PARTICIPANT),
# order, # order_state
# 1, # ptr_state
# vid0 # โœ… cur_video_id
# )
def _start_and_load_first():
total = TOTAL_PER_PARTICIPANT
order = _build_order_least_first_with_anchor(
total=total,
anchor_idx=ANCHOR_IDX,
repeats=ANCHOR_REPEATS,
min_gap=MIN_GAP
)
first_idx = order[0]
v0 = V[first_idx]
return (
gr.update(visible=False),
gr.update(visible=True),
v0["url"],
_extract_action(v0),
5.0,
gr.update(visible=False, value=""),
0,
_progress_html(0, TOTAL_PER_PARTICIPANT),
order,
1,
_get_video_id(v0) # cur_video_id
)
start_btn.click(
_start_and_load_first,
inputs=[],
outputs=[page_intro, page_eval, video, action_tb, score, status, done_state, progress, order_state, ptr_state, cur_video_id] # โœ…
)
# -------- Save & Next (๋…ธํŠธ ์—†์Œ) --------
def save_and_next(participant_id, action_name, score_val, done_cnt, order, ptr):
if not participant_id or not participant_id.strip():
# PID ์—†์œผ๋ฉด ๊ธฐ์กด ํ™”๋ฉด ์œ ์ง€
return (
gr.update(visible=True, value="โ— Please enter your Participant ID."),
gr.update(), gr.update(), # video, action_tb ๋ณ€๊ฒฝ ์—†์Œ
done_cnt,
_progress_html(done_cnt, TOTAL_PER_PARTICIPANT),
5.0,
ptr,
video_id
)
status_msg = push(participant_id, action_name, score_val, "")
new_done = int(done_cnt) + 1
# ์ข…๋ฃŒ ์กฐ๊ฑด: ๋ชฉํ‘œ ๊ฐœ์ˆ˜ ๋‹ฌ์„ฑ or ์ˆœ์„œ ์†Œ์ง„
if new_done >= TOTAL_PER_PARTICIPANT or ptr >= len(order):
return (
status_msg, # status
None, # video ๋น„์šฐ๊ธฐ
"", # action_tb ๋น„์šฐ๊ธฐ
TOTAL_PER_PARTICIPANT, # done_state ์ตœ์ข…
_progress_html(TOTAL_PER_PARTICIPANT, TOTAL_PER_PARTICIPANT),
5.0, # score ๋ฆฌ์…‹
ptr,
video_id
)
# ๋‹ค์Œ ์˜์ƒ ๋กœ๋“œ
next_idx = order[ptr]
v = V[next_idx]
return (
status_msg,
v["url"],
_extract_action(v),
new_done,
_progress_html(new_done, TOTAL_PER_PARTICIPANT),
5.0,
ptr + 1,
_get_video_id(v) # โœ… ๋‹ค์Œ cur_video_id
)
# save_next.click(
# save_and_next,
# inputs=[pid, action_tb, score, done_state, order_state, ptr_state],
# outputs=[status, video, action_tb, done_state, progress, score, ptr_state]
# )
save_next.click(
save_and_next,
# โœ… cur_video_id๋ฅผ ๋‘ ๋ฒˆ์งธ ์ธ์ž๋กœ ๋„˜๊น€
inputs=[pid, cur_video_id, score, done_state, order_state, ptr_state],
# โœ… ๋งˆ์ง€๋ง‰์— cur_video_id๋ฅผ outputs๋กœ ๋ฐ›์Œ(์ƒํƒœ ๊ฐฑ์‹ )
outputs=[status, video, action_tb, done_state, progress, score, ptr_state, cur_video_id]
)
if __name__ == "__main__":
demo.launch()