Youngsun Lim commited on
Commit
d521b9d
ยท
1 Parent(s): eebf759

no sound videos

Browse files
app.py CHANGED
@@ -383,10 +383,10 @@ with gr.Blocks(fill_height=True, css=GLOBAL_CSS) as demo:
383
  gr.Markdown("### Examples: BodyWeightSquats")
384
  with gr.Row():
385
  with gr.Column():
386
- gr.Markdown("**Real / Good**")
387
  gr.Video(value=EX_CACHE["BodyWeightSquats"]["real"], height=240, autoplay=False)
388
  with gr.Column():
389
- gr.Markdown("**Generated / Bad**")
390
  gr.Video(value=EX_CACHE["BodyWeightSquats"]["bad"], height=240, autoplay=False)
391
  if not (EX_CACHE["BodyWeightSquats"]["real"] and EX_CACHE["BodyWeightSquats"]["bad"]):
392
  gr.Markdown("> โš ๏ธ Upload `examples/BodyWeightSquats_real.mp4` and `_bad.mp4` to show both samples.")
@@ -396,10 +396,10 @@ with gr.Blocks(fill_height=True, css=GLOBAL_CSS) as demo:
396
  gr.Markdown("### Examples: WallPushUps")
397
  with gr.Row():
398
  with gr.Column():
399
- gr.Markdown("**Real / Good**")
400
  gr.Video(value=EX_CACHE["WallPushUps"]["real"], height=240, autoplay=False)
401
  with gr.Column():
402
- gr.Markdown("**Generated / Bad**")
403
  gr.Video(value=EX_CACHE["WallPushUps"]["bad"], height=240, autoplay=False)
404
  if not (EX_CACHE["WallPushUps"]["real"] and EX_CACHE["WallPushUps"]["bad"]):
405
  gr.Markdown("> โš ๏ธ Upload `examples/WallPushUps_real.mp4` and `_bad.mp4` to show both samples.")
@@ -580,11 +580,6 @@ with gr.Blocks(fill_height=True, css=GLOBAL_CSS) as demo:
580
  )
581
 
582
 
583
- # start_btn.click(
584
- # _start_and_load_first,
585
- # inputs=[],
586
- # outputs=[page_intro, page_eval, video, action_tb, score, status, done_state, progress, order_state, ptr_state]
587
- # )
588
  start_btn.click(
589
  _start_and_load_first,
590
  inputs=[],
 
383
  gr.Markdown("### Examples: BodyWeightSquats")
384
  with gr.Row():
385
  with gr.Column():
386
+ gr.Markdown("**Expected depiction of action**")
387
  gr.Video(value=EX_CACHE["BodyWeightSquats"]["real"], height=240, autoplay=False)
388
  with gr.Column():
389
+ gr.Markdown("**Poorly generated action**")
390
  gr.Video(value=EX_CACHE["BodyWeightSquats"]["bad"], height=240, autoplay=False)
391
  if not (EX_CACHE["BodyWeightSquats"]["real"] and EX_CACHE["BodyWeightSquats"]["bad"]):
392
  gr.Markdown("> โš ๏ธ Upload `examples/BodyWeightSquats_real.mp4` and `_bad.mp4` to show both samples.")
 
396
  gr.Markdown("### Examples: WallPushUps")
397
  with gr.Row():
398
  with gr.Column():
399
+ gr.Markdown("**Expected depiction of action**")
400
  gr.Video(value=EX_CACHE["WallPushUps"]["real"], height=240, autoplay=False)
401
  with gr.Column():
402
+ gr.Markdown("**Poorly generated action**")
403
  gr.Video(value=EX_CACHE["WallPushUps"]["bad"], height=240, autoplay=False)
404
  if not (EX_CACHE["WallPushUps"]["real"] and EX_CACHE["WallPushUps"]["bad"]):
405
  gr.Markdown("> โš ๏ธ Upload `examples/WallPushUps_real.mp4` and `_bad.mp4` to show both samples.")
 
580
  )
581
 
582
 
 
 
 
 
 
583
  start_btn.click(
584
  _start_and_load_first,
585
  inputs=[],
app_old.py CHANGED
@@ -1,123 +1,648 @@
1
- # app.py (Action Consistency human eval - updated)
2
  import os, io, csv, json, random
3
  from datetime import datetime
4
  import gradio as gr
5
  from huggingface_hub import HfApi, hf_hub_download
6
 
7
- REPO_ID = os.getenv("RESULTS_REPO", "SGTLIM/videoeval_results")
 
8
  HF_TOKEN = os.getenv("HF_TOKEN")
9
  RESULTS_FILE = "results.csv"
 
10
 
11
- # videos.json ์˜ˆ์‹œ ํ•ญ๋ชฉ: {"url": "...mp4", "id": "vid_0001", "action": "jumping jacks"}
12
- with open("videos.json","r",encoding="utf-8") as f:
 
13
  V = json.load(f)
14
 
15
  api = HfApi()
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  def _read_csv_bytes():
18
  try:
19
  p = hf_hub_download(
20
  repo_id=REPO_ID, filename=RESULTS_FILE, repo_type="dataset",
21
  token=HF_TOKEN, local_dir="/tmp", local_dir_use_symlinks=False
22
  )
23
- return open(p,"rb").read()
24
  except Exception:
25
- return None # ํŒŒ์ผ์ด ์•„์ง ์—†์œผ๋ฉด None
 
26
 
27
  def _append(old_bytes, row):
28
  s = io.StringIO()
29
  w = csv.writer(s)
30
  if not old_bytes:
31
- # ํ—ค๋” ๋ณ€๊ฒฝ: video_id -> action
32
- w.writerow(["ts_iso","participant_id","action","score_0_10","notes"])
33
  else:
34
  s.write(old_bytes.decode("utf-8", errors="ignore"))
35
  w.writerow(row)
36
  return s.getvalue().encode("utf-8")
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
- def push(participant_id, action_name, score, notes):
40
- # โ‘  Participant ID ํ•„์ˆ˜
41
- if not participant_id:
42
  return gr.update(visible=True, value="โ— Please enter your Participant ID before proceeding.")
43
- if not action_name or score is None:
44
- return gr.update(visible=True, value="โ— ๋ชจ๋“  ๊ฐ’์„ ์ฑ„์›Œ์ฃผ์„ธ์š”.")
45
- old = _read_csv_bytes()
46
- row = [
47
- datetime.utcnow().isoformat(),
48
- participant_id,
49
- action_name,
50
- float(score),
51
- notes or ""
52
- ]
53
- newb = _append(old, row)
54
- api.upload_file(
55
- path_or_fileobj=io.BytesIO(newb),
56
- path_in_repo=RESULTS_FILE,
57
- repo_id=REPO_ID,
58
- repo_type="dataset",
59
- token=HF_TOKEN,
60
- commit_message="append"
61
- )
62
- return gr.update(visible=True, value=f"โœ… Saved for {action_name}.")
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
  def pick_one():
66
  v = random.choice(V)
67
- # id์—์„œ action ์ด๋ฆ„ ์ถ”์ถœ (์˜ˆ: BodyWeightSquats__3E134F11A3.mp4 โ†’ BodyWeightSquats)
68
- raw_action = v.get("id", "")
69
- clean_action = raw_action.split("__")[0].split(".")[0]
70
- return v["url"], clean_action
71
 
 
 
 
 
 
 
 
 
72
 
73
- with gr.Blocks(theme="soft", fill_height=True) as demo:
74
- gr.Markdown("## ๐ŸŽฏ Action Consistency Human Evaluation")
75
- gr.Markdown(
76
- """
77
- **Task:** You will watch **AI-generated videos**. Your job is to **rate how well the personโ€™s action in the video aligns with the "Expected Action"**.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
- **Evaluate ONLY semantic alignment with the Expected Action.**
80
- Do **not** judge video quality, attractiveness, background, camera motion, or objects.
81
 
82
- **Examples**
83
- - *PushUps*: Arms bend and extend together with the body lowering and rising in a plank posture. If these movements are present, rate high (even if the room, textures, or lighting look odd).
84
- - *HulaHoop*: Even if the hoop object is missing or poorly rendered, rate high if the torso/hip rotation and arm posture clearly indicate hooping.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  )
87
 
88
- with gr.Row():
89
- # next_btn = gr.Button("Next video โ–ถ", variant="primary", scale=0)
90
- pid = gr.Textbox(label="Participant ID (required)", placeholder="e.g., Youngsun-2025/10/01")
91
-
92
- with gr.Row(equal_height=True):
93
- # ์™ผ์ชฝ: ๋น„๋””์˜ค (์ ˆ๋ฐ˜ ๋„ˆ๋น„)
94
- with gr.Column(scale=1):
95
- # ์ ˆ๋ฐ˜ ํญ ๊ตฌํ˜„: 2-์ปฌ๋Ÿผ + ๋†’์ด ์ œํ•œ
96
- video = gr.Video(label="Video", height=360) # ๋†’์ด๋กœ๋„ ํฌ๊ธฐ ์ œํ•œ
97
- # ์˜ค๋ฅธ์ชฝ: ๋ฉ”ํƒ€/์Šฌ๋ผ์ด๋”/๋…ธํŠธ
98
- with gr.Column(scale=1):
99
- action_tb = gr.Textbox(label="Expected action", interactive=False)
100
- score = gr.Slider(
101
- minimum=0.0, maximum=10.0, step=0.1, value=5.0,
102
- label="Action Consistency (0.0 (Worst) - 10.0 (Best))"
103
  )
104
- notes = gr.Textbox(label="Reason (optional)", lines=3, placeholder="Any quick commentsโ€ฆ")
105
- with gr.Row():
106
- save = gr.Button("๐Ÿ’พ Save", variant="secondary")
107
- next_btn = gr.Button("Next video โ–ถ", variant="primary")
108
 
109
- status = gr.Markdown(visible=False)
110
 
111
- # ์ดˆ๊ธฐ ๋กœ๋“œ ๋ฐ ๋‹ค์Œ ๋ฒ„ํŠผ ๋™์ž‘: ๋ฌด์ž‘์œ„ ์ƒ˜ํ”Œ ์„ ํƒ
112
- def _load_new():
113
- url, action = pick_one()
114
- return url, action, 5.0, "" # ๊ธฐ๋ณธ์ ์ˆ˜ 5.0, ๋…ธํŠธ ์ดˆ๊ธฐํ™”
 
 
 
 
 
 
 
 
 
115
 
116
- demo.load(_load_new, [], [video, action_tb, score, notes])
117
- next_btn.click(_load_new, [], [video, action_tb, score, notes])
 
 
 
 
 
 
 
 
 
 
 
118
 
119
- # ์ €์žฅ
120
- save.click(push, [pid, action_tb, score, notes], [status])
 
 
 
 
 
 
 
 
 
 
121
 
122
  if __name__ == "__main__":
123
  demo.launch()
 
 
1
  import os, io, csv, json, random
2
  from datetime import datetime
3
  import gradio as gr
4
  from huggingface_hub import HfApi, hf_hub_download
5
 
6
+ # -------------------- Config --------------------
7
+ REPO_ID = os.getenv("RESULTS_REPO", "sgtlim/videoeval_results") # ์—…๋กœ๋“œํ•œ ๋ฆฌํฌ์™€ ์ผ์น˜
8
  HF_TOKEN = os.getenv("HF_TOKEN")
9
  RESULTS_FILE = "results.csv"
10
+ TOTAL_PER_PARTICIPANT = 30 # ๋ชฉํ‘œ ํ‰๊ฐ€ ๊ฐœ์ˆ˜(์„ธ์…˜ ๊ธฐ์ค€)
11
 
12
+
13
+ # videos.json ์˜ˆ์‹œ: {"url": "...mp4", "id": "BodyWeightSquats__XXXX.mp4", "action": "BodyWeightSquats"}
14
+ with open("videos.json", "r", encoding="utf-8") as f:
15
  V = json.load(f)
16
 
17
  api = HfApi()
18
 
19
+ # ๊ต์ˆ˜๋‹˜ ์ง€์นจ(๊ทธ๋Œ€๋กœ, ๊ตต๊ฒŒ ์ฒ˜๋ฆฌ ํฌํ•จ)
20
+ INSTRUCTION_MD = """
21
+ **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:
22
+ - The generated video should **capture** the expected action **throughout the video**.
23
+ - Try to **focus only** on the expected action and do **not** judge **video quality**, **attractiveness**, **background**, **camera motion**, or **objects**.
24
+ - You will be **paid** once **all the videos are viewed and rated**.
25
+ """
26
+
27
+ # -------------------- Helper funcs --------------------
28
+ def _load_eval_counts():
29
+ """
30
+ Hugging Face dataset์˜ results.csv๋ฅผ ์ฝ์–ด video_id๋ณ„ ํ‰๊ฐ€ ๊ฐœ์ˆ˜(dict)๋ฅผ ๋ฐ˜ํ™˜.
31
+ ์—†์œผ๋ฉด 0์œผ๋กœ ์ดˆ๊ธฐํ™”.
32
+ """
33
+ # ๋ชจ๋“  id๋ฅผ 0์œผ๋กœ ์ดˆ๊ธฐํ™”
34
+ counts = {}
35
+ for v in V:
36
+ vid = _get_video_id(v)
37
+ counts[vid] = 0
38
+
39
+ b = _read_csv_bytes()
40
+ if not b:
41
+ return counts
42
+
43
+ s = io.StringIO(b.decode("utf-8", errors="ignore"))
44
+ r = csv.reader(s)
45
+ rows = list(r)
46
+ if not rows:
47
+ return counts
48
+
49
+ # ํ—ค๋” ํŒŒ์•…
50
+ header = rows[0]
51
+ body = rows[1:] if header and ("video_id" in header or "overall" in header) else rows
52
+ vid_col = None
53
+ if header and "video_id" in header:
54
+ vid_col = header.index("video_id")
55
+
56
+ for row in body:
57
+ try:
58
+ vid = row[vid_col] if vid_col is not None else row[2] # ๊ธฐ๋ณธ ํฌ๋งท: ts, pid, video_id, overall, notes
59
+ if vid in counts:
60
+ counts[vid] += 1
61
+ except Exception:
62
+ continue
63
+ return counts
64
+
65
+ def _get_video_id(v: dict) -> str:
66
+ if "id" in v and v["id"]:
67
+ return v["id"]
68
+ # id๊ฐ€ ์—†์œผ๋ฉด URL ํŒŒ์ผ๋ช…์œผ๋กœ ๋Œ€์ฒด
69
+ return os.path.basename(v.get("url", ""))
70
+
71
  def _read_csv_bytes():
72
  try:
73
  p = hf_hub_download(
74
  repo_id=REPO_ID, filename=RESULTS_FILE, repo_type="dataset",
75
  token=HF_TOKEN, local_dir="/tmp", local_dir_use_symlinks=False
76
  )
77
+ return open(p, "rb").read()
78
  except Exception:
79
+ return None
80
+
81
 
82
  def _append(old_bytes, row):
83
  s = io.StringIO()
84
  w = csv.writer(s)
85
  if not old_bytes:
86
+ # โœ… ์ƒˆ ํ—ค๋”
87
+ w.writerow(["ts_iso", "participant_id", "video_id", "overall", "notes"])
88
  else:
89
  s.write(old_bytes.decode("utf-8", errors="ignore"))
90
  w.writerow(row)
91
  return s.getvalue().encode("utf-8")
92
 
93
+ # def push(participant_id, action_name, score, notes=""):
94
+ # if not participant_id or not participant_id.strip():
95
+ # return gr.update(visible=True, value="โ— Please enter your Participant ID before proceeding.")
96
+ # if not action_name or score is None:
97
+ # return gr.update(visible=True, value="โ— Fill out all fields.")
98
+ # old = _read_csv_bytes()
99
+ # row = [
100
+ # datetime.utcnow().isoformat(),
101
+ # participant_id.strip(),
102
+ # action_name,
103
+ # float(score),
104
+ # notes or ""
105
+ # ]
106
+ # newb = _append(old, row)
107
+ # api.upload_file(
108
+ # path_or_fileobj=io.BytesIO(newb),
109
+ # path_in_repo=RESULTS_FILE,
110
+ # repo_id=REPO_ID,
111
+ # repo_type="dataset",
112
+ # token=HF_TOKEN,
113
+ # commit_message="append"
114
+ # )
115
+ # return gr.update(visible=True, value=f"โœ… Saved for {action_name}.")
116
 
117
+ def push(participant_id, video_id, score, notes=""):
118
+ if not participant_id or not participant_id.strip():
 
119
  return gr.update(visible=True, value="โ— Please enter your Participant ID before proceeding.")
120
+ if not video_id or score is None:
121
+ return gr.update(visible=True, value="โ— Fill out all fields.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
+ try:
124
+ old = _read_csv_bytes()
125
+ row = [
126
+ datetime.utcnow().isoformat(),
127
+ participant_id.strip(),
128
+ video_id, # โœ… action ๋Œ€์‹  video_id ์ €์žฅ
129
+ float(score), # overall
130
+ notes or ""
131
+ ]
132
+ newb = _append(old, row)
133
+
134
+ if not REPO_ID:
135
+ return gr.update(visible=True, value="โ— RESULTS_REPO is not set.")
136
+ if not HF_TOKEN:
137
+ return gr.update(visible=True, value="โ— HF_TOKEN is missing. Set a write token for the dataset repo.")
138
+
139
+ api.upload_file(
140
+ path_or_fileobj=io.BytesIO(newb),
141
+ path_in_repo=RESULTS_FILE,
142
+ repo_id=REPO_ID,
143
+ repo_type="dataset",
144
+ token=HF_TOKEN,
145
+ commit_message="append"
146
+ )
147
+ return gr.update(visible=True, value=f"โœ… Saved for {video_id}.")
148
+ except Exception as e:
149
+ return gr.update(
150
+ visible=True,
151
+ value=f"โŒ Save failed: {type(e).__name__}: {e}\n"
152
+ f"- Check HF_TOKEN permission\n- Check REPO_ID\n- Create dataset repo if missing"
153
+ )
154
+
155
+ def _extract_action(v):
156
+ if "action" in v and v["action"]:
157
+ return v["action"]
158
+ raw = v.get("id", "")
159
+ return raw.split("__")[0].split(".")[0]
160
 
161
  def pick_one():
162
  v = random.choice(V)
163
+ return v["url"], _extract_action(v)
 
 
 
164
 
165
+ def _progress_html(done, total):
166
+ pct = int(100 * done / max(1, total))
167
+ return f"""
168
+ <div style="border:1px solid #ddd; height:20px; border-radius:6px; overflow:hidden; margin-top:6px;">
169
+ <div style="height:100%; width:{pct}%; background:#3b82f6;"></div>
170
+ </div>
171
+ <div style="font-size:12px; margin-top:4px;">{done} / {total}</div>
172
+ """
173
 
174
+ def _build_order_with_anchor(total:int, anchor_idx:int, repeats:int, pool_size:int, min_gap:int=1):
175
+ """
176
+ total: TOTAL_PER_PARTICIPANT (e.g., 30)
177
+ anchor_idx: index of the anchor video in V (0 for first item)
178
+ repeats: how many times to show anchor (e.g., 5)
179
+ pool_size: len(V)
180
+ min_gap: ์ตœ์†Œ ๊ฐ„๊ฒฉ(์ธ์ ‘ ๊ธˆ์ง€ => 1)
181
+
182
+ return: list of indices (length=total)
183
+ """
184
+ assert repeats <= total, "repeats must be <= total"
185
+ assert pool_size >= 1, "videos pool must be non-empty"
186
+
187
+ # 1) ๋‹ค๋ฅธ ๋น„๋””์˜ค 25๊ฐœ(์ค‘๋ณต ์—†์ด) ๋ฝ‘๊ธฐ
188
+ others_needed = total - repeats
189
+ # anchor๋ฅผ ์ œ์™ธํ•œ ํ›„๋ณด ์ธ๋ฑ์Šค
190
+ candidates = list(range(1, pool_size)) if anchor_idx == 0 else [i for i in range(pool_size) if i != anchor_idx]
191
+ if len(candidates) < others_needed:
192
+ raise ValueError("Not enough unique non-anchor videos to fill the schedule without duplication.")
193
+
194
+ others = random.sample(candidates, k=others_needed)
195
+
196
+ # 2) ๊ธฐ๋ณธ ์‹œํ€€์Šค(others)๋ฅผ ๋ฌด์ž‘์œ„๋กœ ์„ž๊ธฐ
197
+ random.shuffle(others)
198
+
199
+ # 3) ์•ต์ปค๋ฅผ min_gap๋ฅผ ๋งŒ์กฑํ•˜๋„๋ก ์‚ฝ์ž…ํ•  ์œ„์น˜ ์„ ์ •
200
+ # 30๊ฐœ๋ฅผ 5๊ตฌ๊ฐ„์œผ๋กœ ๋‚˜๋ˆ , ๊ฐ ๊ตฌ๊ฐ„ ๋‚ด์—์„œ ์ถฉ๋Œ ๋œ ๋‚˜๊ฒŒ ๋ฐฐ์น˜
201
+ # (๊ฐ„๋‹จํ•˜๊ณ  ์•ˆ์ •์ ์ธ ๋ฐฉ์‹)
202
+ seq = others[:] # ๊ธธ์ด=25
203
+ anchor_positions = []
204
+ segment = total // repeats # 30//5 = 6
205
+
206
+ for k in range(repeats):
207
+ # ๊ฐ ๊ตฌ๊ฐ„ [k*segment, (k+1)*segment) ์•ˆ์—์„œ ํ›„๋ณด ์œ„์น˜๋ฅผ ๊ณ ๋ฆ„
208
+ lo = k * segment
209
+ hi = (k + 1) * segment if k < repeats - 1 else total # ๋งˆ์ง€๋ง‰์€ ๋๊นŒ์ง€
210
+ # ๊ฒฝ๊ณ„ ๋‚ด ์ž„์˜ ์˜คํ”„์…‹ ์„ ํƒ (์—ฌ์œ ๋ฅผ ๋‘๊ณ  ์ถฉ๋Œ์„ ํ”ผํ•จ)
211
+ candidate_pos = random.randrange(lo, hi)
212
+
213
+ # ์ธ์ ‘ ๊ธˆ์ง€ ๋ณด์ •: ์ด๋ฏธ ๋ฐฐ์ •๋œ anchor ์œ„์น˜๋“ค๊ณผ์˜ ๊ฑฐ๋ฆฌ๊ฐ€ min_gap ์ด์ƒ ๋˜๋„๋ก ์กฐ์ •
214
+ # ํ•„์š” ์‹œ ์ขŒ์šฐ๋กœ ๊ทผ์ ‘ํ•œ ๋นˆ ์Šฌ๋กฏ ํƒ์ƒ‰
215
+ def ok(pos):
216
+ return all(abs(pos - p) >= min_gap + 1 for p in anchor_positions) # ์—ฐ์†๊ธˆ์ง€ => ๊ฑฐ๋ฆฌ >= 2
217
+
218
+ # ๊ทผ๋ฐฉ ํƒ์ƒ‰ ํญ
219
+ found = None
220
+ for delta in range(0, segment): # ๊ตฌ๊ฐ„ ํฌ๊ธฐ ๋‚ด์—์„œ ํƒ์ƒ‰
221
+ # ์ขŒ/์šฐ ๋ฒˆ๊ฐˆ์•„๊ฐ€๋ฉฐ ํ›„๋ณด ์‹œ๋„
222
+ for sign in (+1, -1):
223
+ pos = candidate_pos + sign * delta
224
+ if 0 <= pos < total and ok(pos):
225
+ found = pos
226
+ break
227
+ if found is not None:
228
+ break
229
+
230
+ if found is None:
231
+ # ์ตœํ›„: 0..total-1 ๋ฒ”์œ„์—์„œ ์•„๋ฌด ๋ฐ๋‚˜ ์ถฉ๋Œ ์—†๋Š” ๊ณณ ์ฐพ๊ธฐ
232
+ for pos in range(total):
233
+ if ok(pos):
234
+ found = pos
235
+ break
236
+ if found is None:
237
+ raise RuntimeError("Failed to place anchor without adjacency. Try different strategy or loosen min_gap.")
238
+
239
+ anchor_positions.append(found)
240
+
241
+ # 4) others๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ธธ์ด total์˜ ๋นˆ ์‹œํ€€์Šค๋ฅผ ๋งŒ๋“ค๊ณ  anchor๋ฅผ ์ฃผ์ž…
242
+ # ์šฐ์„  ๋นˆ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋งŒ๋“ค๊ณ  anchor ์œ„์น˜๋ฅผ ์ฑ„์šด ํ›„, ๋‚˜๋จธ์ง€๋ฅผ others๋กœ ์ฑ„์›€
243
+ result = [None] * total
244
+ for pos in anchor_positions:
245
+ result[pos] = anchor_idx
246
+
247
+ # others ํฌ์ธํ„ฐ
248
+ j = 0
249
+ for i in range(total):
250
+ if result[i] is None:
251
+ result[i] = others[j]
252
+ j += 1
253
+
254
+ # ์•ˆ์ „์ฒดํฌ
255
+ assert len(result) == total
256
+ # ์ธ์ ‘ anchor ์—†๋Š”์ง€ ํ™•์ธ
257
+ for i in range(1, total):
258
+ assert not (result[i] == anchor_idx and result[i-1] == anchor_idx), "Adjacent anchors found."
259
+ # anchor ๊ฐœ์ˆ˜ ํ™•์ธ
260
+ assert sum(1 for x in result if x == anchor_idx) == repeats, "Anchor count mismatch."
261
+
262
+ return result
263
+
264
+
265
+ # -------------------- Example videos (download to local cache) --------------------
266
+ EXAMPLES = {
267
+ "BodyWeightSquats": {
268
+ "real": "examples/BodyWeightSquats_real.mp4",
269
+ "bad": "examples/BodyWeightSquats_bad.mp4",
270
+ },
271
+ "WallPushUps": {
272
+ "real": "examples/WallPushUps_real.mp4",
273
+ "bad": "examples/WallPushUps_bad.mp4",
274
+ },
275
+ }
276
+ EX_CACHE = {}
277
+ for cls, files in EXAMPLES.items():
278
+ EX_CACHE[cls] = {"real": None, "bad": None}
279
+ for kind, fname in files.items():
280
+ try:
281
+ EX_CACHE[cls][kind] = hf_hub_download(
282
+ repo_id=REPO_ID,
283
+ filename=fname,
284
+ repo_type="dataset",
285
+ token=HF_TOKEN,
286
+ local_dir="/tmp",
287
+ local_dir_use_symlinks=False,
288
+ )
289
+ except Exception as e:
290
+ print(f"[WARN] example missing: {cls} {kind} -> {fname}: {e}")
291
+
292
+ GLOBAL_CSS = """
293
+ /* ===== ๊ณตํ†ต ๋ณ€์ˆ˜ ํˆฌ๋ช…ํ™” (v3/v4 ๋‘˜๋‹ค) ===== */
294
+ :root, .gradio-container {
295
+ --body-background-fill: transparent !important;
296
+ --background-fill-primary: transparent !important;
297
+ --background-fill-secondary: transparent !important;
298
+ --block-background-fill: transparent !important;
299
+ --block-border-color: transparent !important;
300
+ --panel-background-fill: transparent !important;
301
+ --panel-border-color: transparent !important;
302
+ --section-header-background-fill: transparent !important;
303
+ --shadow-drop: 0 0 0 rgba(0,0,0,0) !important;
304
+ --shadow-spread: 0 0 0 rgba(0,0,0,0) !important;
305
+ }
306
+
307
+ /* ===== v4(Tailwind ๊ธฐ๋ฐ˜)์—์„œ ์ž์ฃผ ์“ฐ์ด๋Š” ๋ฐฐ๊ฒฝ/ํ…Œ๋‘๋ฆฌ/๊ทธ๋ฆผ์ž ์ œ๊ฑฐ ===== */
308
+ .gradio-container .bg-white,
309
+ .gradio-container .bg-gray-50,
310
+ .gradio-container .bg-gray-100,
311
+ .gradio-container .bg-slate-50,
312
+ .gradio-container .bg-neutral-50,
313
+ .gradio-container .bg-secondary,
314
+ .gradio-container .border,
315
+ .gradio-container .shadow,
316
+ .gradio-container .shadow-sm,
317
+ .gradio-container .shadow-md,
318
+ .gradio-container .ring-1,
319
+ .gradio-container .ring,
320
+ .gradio-container .gr-card,
321
+ .gradio-container .prose > *:where(hr) {
322
+ background: transparent !important;
323
+ box-shadow: none !important;
324
+ border-color: transparent !important;
325
+ }
326
+
327
+ /* ===== v3 ์ปดํฌ๋„ŒํŠธ ๊ณ„์—ด ===== */
328
+ .gradio-container .gr-panel,
329
+ .gradio-container .gr-group,
330
+ .gradio-container .gr-box,
331
+ .gradio-container .gr-row,
332
+ .gradio-container .gr-column,
333
+ .gradio-container .gr-accordion,
334
+ .gradio-container .gr-block,
335
+ .gradio-container .gr-form,
336
+ .gradio-container .gr-tabs,
337
+ .gradio-container .gr-tabitem,
338
+ .gradio-container .gr-section-header {
339
+ background: transparent !important;
340
+ box-shadow: none !important;
341
+ border: none !important;
342
+ }
343
+
344
+ /* ๊ตฌ๋ถ„์„ /ํ—ค๋” ๋ฐ” ์ œ๊ฑฐ */
345
+ .gradio-container hr,
346
+ .gradio-container .gr-divider,
347
+ .gradio-container .gr-accordion .label {
348
+ background: transparent !important;
349
+ border: none !important;
350
+ box-shadow: none !important;
351
+ }
352
+
353
+ /* ๋ฐ”๊นฅ์ชฝ ํŽ˜์ด์ง€ ๋ฐฐ๊ฒฝ๋„ ๊ฐ•์ œ๋กœ ํˆฌ๋ช…/ํฐ์ƒ‰์œผ๋กœ */
354
+ html, body, .gradio-container { background: transparent !important; }
355
+
356
+ /* ๊ธฐ์กด CSS ์•„๋ž˜์— ์ถ”๊ฐ€ */
357
+ #eval [class*="bg-"],
358
+ #eval [class*="border"],
359
+ #eval [class*="shadow"],
360
+ #eval .gr-panel, #eval .gr-group, #eval .gr-box, #eval .gr-row, #eval .gr-column,
361
+ #eval .gr-block, #eval .gr-form, #eval .gr-section-header, #eval .gr-accordion {
362
+ background: transparent !important;
363
+ border-color: transparent !important;
364
+ box-shadow: none !important;
365
+ }
366
+ #eval .gr-form, #eval .gr-panel { background: transparent !important; box-shadow:none !important; border:none !important; }
367
 
368
+ """
 
369
 
370
+ # -------------------- UI --------------------
371
+ with gr.Blocks(fill_height=True, css=GLOBAL_CSS) as demo:
372
+ order_state = gr.State(value=[]) # v4์—์„œ๋Š” value= ๊ถŒ์žฅ
373
+ ptr_state = gr.State(value=0)
374
+ cur_video_id = gr.State(value="")
375
+ # ------------------ PAGE 1: Intro + Examples ------------------
376
+ page_intro = gr.Group(visible=True)
377
+ with page_intro:
378
+ gr.Markdown("## ๐ŸŽฏ Action Consistency Human Evaluation")
379
+ gr.Markdown(INSTRUCTION_MD)
380
+
381
+ # Examples: Squats
382
+ with gr.Group():
383
+ gr.Markdown("### Examples: BodyWeightSquats")
384
+ with gr.Row():
385
+ with gr.Column():
386
+ gr.Markdown("**Expected depiction of action**")
387
+ gr.Video(value=EX_CACHE["BodyWeightSquats"]["real"], height=240, autoplay=False)
388
+ with gr.Column():
389
+ gr.Markdown("**Poorly generated action**")
390
+ gr.Video(value=EX_CACHE["BodyWeightSquats"]["bad"], height=240, autoplay=False)
391
+ if not (EX_CACHE["BodyWeightSquats"]["real"] and EX_CACHE["BodyWeightSquats"]["bad"]):
392
+ gr.Markdown("> โš ๏ธ Upload `examples/BodyWeightSquats_real.mp4` and `_bad.mp4` to show both samples.")
393
+
394
+ # Examples: WallPushUps
395
+ with gr.Group():
396
+ gr.Markdown("### Examples: WallPushUps")
397
+ with gr.Row():
398
+ with gr.Column():
399
+ gr.Markdown("**Expected depiction of action**")
400
+ gr.Video(value=EX_CACHE["WallPushUps"]["real"], height=240, autoplay=False)
401
+ with gr.Column():
402
+ gr.Markdown("**Poorly generated action**")
403
+ gr.Video(value=EX_CACHE["WallPushUps"]["bad"], height=240, autoplay=False)
404
+ if not (EX_CACHE["WallPushUps"]["real"] and EX_CACHE["WallPushUps"]["bad"]):
405
+ gr.Markdown("> โš ๏ธ Upload `examples/WallPushUps_real.mp4` and `_bad.mp4` to show both samples.")
406
+
407
+ understood = gr.Checkbox(label="I have read and understand the task.", value=False)
408
+ start_btn = gr.Button("Yes, start", variant="secondary", interactive=False)
409
+
410
+ def _toggle_start(checked: bool):
411
+ return gr.update(interactive=checked, variant=("primary" if checked else "secondary"))
412
+ understood.change(_toggle_start, inputs=understood, outputs=start_btn)
413
+
414
+
415
+ # ------------------ PAGE 2: Evaluation ------------------
416
+ page_eval = gr.Group(visible=False, elem_id="eval")
417
+ with page_eval:
418
+ # PID ์ž…๋ ฅ
419
+ with gr.Row():
420
+ pid = gr.Textbox(label="Participant ID (required)", placeholder="e.g., Youngsun-2025/10/01")
421
+
422
+ # ์ง€์นจ(์›๋ฌธ) + ๋น„๋””์˜ค + ์ง„ํ–‰๋ฐ” / ์˜ค๋ฅธ์ชฝ์— ์Šฌ๋ผ์ด๋” + Save&Next
423
+ with gr.Row(equal_height=True):
424
+ with gr.Column(scale=1):
425
+ gr.Markdown(INSTRUCTION_MD) # ๊ต์ˆ˜๋‹˜ ๋ฌธ๊ตฌ ๊ทธ๋Œ€๋กœ
426
+ video = gr.Video(label="Video", height=360)
427
+ progress = gr.HTML(_progress_html(0, TOTAL_PER_PARTICIPANT))
428
+ with gr.Column(scale=1):
429
+ action_tb = gr.Textbox(label="Expected action", interactive=False)
430
+ score = gr.Slider(minimum=0.0, maximum=10.0, step=0.1, value=5.0,
431
+ label="Action Consistency (0.0 (Worst) - 10.0 (Best))")
432
+ save_next = gr.Button("๐Ÿ’พ Save & Next โ–ถ", variant="secondary", interactive=False)
433
+
434
+ status = gr.Markdown(visible=False)
435
+ done_state = gr.State(0)
436
+
437
+ # PID ์ž…๋ ฅ์— ๋”ฐ๋ผ Save&Next ํ† ๊ธ€
438
+ def _toggle_by_pid(pid_text: str):
439
+ enabled = bool(pid_text and pid_text.strip())
440
+ return gr.update(interactive=enabled, variant=("primary" if enabled else "secondary"))
441
+ pid.change(_toggle_by_pid, inputs=pid, outputs=save_next)
442
+
443
+ # -------- ํŽ˜์ด์ง€ ์ „ํ™˜ & ์ฒซ ๋กœ๋“œ --------
444
+ ANCHOR_IDX = 0 # videos.json์˜ ๋งจ ์ฒซ ๋น„๋””์˜ค
445
+ ANCHOR_REPEATS = 5 # ์•ต์ปค 5ํšŒ
446
+ MIN_GAP = 1 # ์•ต์ปค ์—ฐ์† ๊ธˆ์ง€(์ธ์ ‘ ๊ธˆ์ง€)
447
+
448
+ def _build_order_least_first_with_anchor(total:int, anchor_idx:int, repeats:int, min_gap:int=1):
449
  """
450
+ - results.csv๋ฅผ ์ฝ์–ด video_id๋ณ„ ์นด์šดํŠธ๋ฅผ ๊ณ„์‚ฐ
451
+ - ์•ต์ปค(์ฒซ ๋น„๋””์˜ค) 5ํšŒ ํฌํ•จ, ์—ฐ์† ๊ธˆ์ง€
452
+ - ๋‚˜๋จธ์ง€๋Š” '๊ฐ€์žฅ ์ ๊ฒŒ ํ‰๊ฐ€๋œ ์ˆœ'์œผ๋กœ ์ค‘๋ณต ์—†์ด ์ฑ„์›€
453
+ """
454
+ assert repeats <= total
455
+ N = len(V)
456
+ assert N >= 1
457
+
458
+ # 0) id ๋งคํ•‘
459
+ def vid_of(i): return _get_video_id(V[i])
460
+
461
+ # 1) ํ˜„์žฌ ๋ˆ„์  ์นด์šดํŠธ ๋กœ๋“œ
462
+ counts = _load_eval_counts()
463
+
464
+ # 2) ์•ต์ปค ์ œ์™ธ ํ›„๋ณด(์ค‘๋ณต ์—†์ด) ์ •๋ ฌ: ์นด์šดํŠธ ์˜ค๋ฆ„์ฐจ์ˆœ, ๋™๋ฅ ์€ ๋žœ๋ค ์…”ํ”Œ
465
+ anchor_vid = vid_of(anchor_idx)
466
+ candidates = [i for i in range(N) if i != anchor_idx]
467
+ # ๋™๋ฅ  ๋žœ๋คํ™”๋ฅผ ์œ„ํ•ด ์ผ๋‹จ ์…”ํ”Œ
468
+ random.shuffle(candidates)
469
+ candidates.sort(key=lambda i: counts.get(vid_of(i), 0))
470
+
471
+ others_needed = total - repeats
472
+ if len(candidates) < others_needed:
473
+ raise ValueError("Not enough unique non-anchor videos to fill the schedule without duplication.")
474
+
475
+ others = candidates[:others_needed] # ์ค‘๋ณต ์—†์ด ์„ ํƒ
476
+
477
+ # 3) others๋ฅผ ๋ฒ ์ด์Šค ์‹œํ€€์Šค๋กœ(๋žœ๋ค ์‚ด์ง ์„ž๊ธฐ)
478
+ random.shuffle(others)
479
+
480
+ # 4) ์•ต์ปค๋ฅผ ๊ตฌ๊ฐ„ ๋ฐฐ์น˜(์—ฐ์† ๊ธˆ์ง€)
481
+ seq = [None] * total
482
+ segment = total // repeats if repeats > 0 else total
483
+ anchor_positions = []
484
+ for k in range(repeats):
485
+ lo = k * segment
486
+ hi = (k + 1) * segment if k < repeats - 1 else total
487
+ cand = random.randrange(lo, hi)
488
+
489
+ def ok(pos):
490
+ return all(abs(pos - p) >= (min_gap + 1) for p in anchor_positions)
491
+
492
+ found = None
493
+ for d in range(0, max(1, segment)):
494
+ for sgn in (+1, -1):
495
+ pos = cand + sgn * d
496
+ if 0 <= pos < total and ok(pos):
497
+ found = pos
498
+ break
499
+ if found is not None:
500
+ break
501
+ if found is None:
502
+ # ๋งˆ์ง€๋ง‰ ์ˆ˜๋‹จ: ์ „์ฒด ํƒ์ƒ‰
503
+ for pos in range(total):
504
+ if ok(pos):
505
+ found = pos
506
+ break
507
+ if found is None:
508
+ raise RuntimeError("Failed to place anchor without adjacency.")
509
+ anchor_positions.append(found)
510
+
511
+ for pos in anchor_positions:
512
+ seq[pos] = anchor_idx
513
+
514
+ # 5) ๋นˆ ์ž๋ฆฌ๋ฅผ others๋กœ ์ฑ„์šฐ๊ธฐ
515
+ j = 0
516
+ for i in range(total):
517
+ if seq[i] is None:
518
+ seq[i] = others[j]
519
+ j += 1
520
+
521
+ # 6) ์•ˆ์ „ ์ฒดํฌ
522
+ assert sum(1 for x in seq if x == anchor_idx) == repeats
523
+ for i in range(1, total):
524
+ assert not (seq[i] == anchor_idx and seq[i-1] == anchor_idx), "Adjacent anchors found."
525
+
526
+ return seq
527
+
528
+
529
+ # def _start_and_load_first():
530
+ # total = TOTAL_PER_PARTICIPANT
531
+ # order = _build_order_with_anchor(
532
+ # total=total,
533
+ # anchor_idx=ANCHOR_IDX,
534
+ # repeats=ANCHOR_REPEATS,
535
+ # pool_size=len(V),
536
+ # min_gap=1 # ์ธ์ ‘ ๊ธˆ์ง€
537
+ # )
538
+
539
+ # first_idx = order[0]
540
+ # v0 = V[first_idx]
541
+ # url0 = v0["url"]
542
+ # action0 = _extract_action(v0)
543
+ # vid0 = _get_video_id(v0) # โœ… ์—ฌ๊ธฐ์„œ ์›๋ณธ id
544
+
545
+ # return (
546
+ # gr.update(visible=False), # page_intro off
547
+ # gr.update(visible=True), # page_eval on
548
+ # url0, # video
549
+ # action0, # action_tb (ํ‘œ์‹œ์šฉ)
550
+ # 5.0, # score ์ดˆ๊ธฐ๊ฐ’
551
+ # gr.update(visible=False, value=""),
552
+ # 0, # done_state
553
+ # _progress_html(0, TOTAL_PER_PARTICIPANT),
554
+ # order, # order_state
555
+ # 1, # ptr_state
556
+ # vid0 # โœ… cur_video_id
557
+ # )
558
+ def _start_and_load_first():
559
+ total = TOTAL_PER_PARTICIPANT
560
+ order = _build_order_least_first_with_anchor(
561
+ total=total,
562
+ anchor_idx=ANCHOR_IDX,
563
+ repeats=ANCHOR_REPEATS,
564
+ min_gap=MIN_GAP
565
+ )
566
+ first_idx = order[0]
567
+ v0 = V[first_idx]
568
+ return (
569
+ gr.update(visible=False),
570
+ gr.update(visible=True),
571
+ v0["url"],
572
+ _extract_action(v0),
573
+ 5.0,
574
+ gr.update(visible=False, value=""),
575
+ 0,
576
+ _progress_html(0, TOTAL_PER_PARTICIPANT),
577
+ order,
578
+ 1,
579
+ _get_video_id(v0) # cur_video_id
580
+ )
581
+
582
+
583
+ start_btn.click(
584
+ _start_and_load_first,
585
+ inputs=[],
586
+ outputs=[page_intro, page_eval, video, action_tb, score, status, done_state, progress, order_state, ptr_state, cur_video_id] # โœ…
587
  )
588
 
589
+
590
+ # -------- Save & Next (๋…ธํŠธ ์—†์Œ) --------
591
+ def save_and_next(participant_id, action_name, score_val, done_cnt, order, ptr):
592
+ if not participant_id or not participant_id.strip():
593
+ # PID ์—†์œผ๋ฉด ๊ธฐ์กด ํ™”๋ฉด ์œ ์ง€
594
+ return (
595
+ gr.update(visible=True, value="โ— Please enter your Participant ID."),
596
+ gr.update(), gr.update(), # video, action_tb ๋ณ€๊ฒฝ ์—†์Œ
597
+ done_cnt,
598
+ _progress_html(done_cnt, TOTAL_PER_PARTICIPANT),
599
+ 5.0,
600
+ ptr,
601
+ video_id
 
 
602
  )
 
 
 
 
603
 
604
+ status_msg = push(participant_id, action_name, score_val, "")
605
 
606
+ new_done = int(done_cnt) + 1
607
+ # ์ข…๋ฃŒ ์กฐ๊ฑด: ๋ชฉํ‘œ ๊ฐœ์ˆ˜ ๋‹ฌ์„ฑ or ์ˆœ์„œ ์†Œ์ง„
608
+ if new_done >= TOTAL_PER_PARTICIPANT or ptr >= len(order):
609
+ return (
610
+ status_msg, # status
611
+ None, # video ๋น„์šฐ๊ธฐ
612
+ "", # action_tb ๋น„์šฐ๊ธฐ
613
+ TOTAL_PER_PARTICIPANT, # done_state ์ตœ์ข…
614
+ _progress_html(TOTAL_PER_PARTICIPANT, TOTAL_PER_PARTICIPANT),
615
+ 5.0, # score ๋ฆฌ์…‹
616
+ ptr,
617
+ video_id
618
+ )
619
 
620
+ # ๋‹ค์Œ ์˜์ƒ ๋กœ๋“œ
621
+ next_idx = order[ptr]
622
+ v = V[next_idx]
623
+ return (
624
+ status_msg,
625
+ v["url"],
626
+ _extract_action(v),
627
+ new_done,
628
+ _progress_html(new_done, TOTAL_PER_PARTICIPANT),
629
+ 5.0,
630
+ ptr + 1,
631
+ _get_video_id(v) # โœ… ๋‹ค์Œ cur_video_id
632
+ )
633
 
634
+ # save_next.click(
635
+ # save_and_next,
636
+ # inputs=[pid, action_tb, score, done_state, order_state, ptr_state],
637
+ # outputs=[status, video, action_tb, done_state, progress, score, ptr_state]
638
+ # )
639
+ save_next.click(
640
+ save_and_next,
641
+ # โœ… cur_video_id๋ฅผ ๋‘ ๋ฒˆ์งธ ์ธ์ž๋กœ ๋„˜๊น€
642
+ inputs=[pid, cur_video_id, score, done_state, order_state, ptr_state],
643
+ # โœ… ๋งˆ์ง€๋ง‰์— cur_video_id๋ฅผ outputs๋กœ ๋ฐ›์Œ(์ƒํƒœ ๊ฐฑ์‹ )
644
+ outputs=[status, video, action_tb, done_state, progress, score, ptr_state, cur_video_id]
645
+ )
646
 
647
  if __name__ == "__main__":
648
  demo.launch()
app_newbutold.py โ†’ app_older.py RENAMED
@@ -1,6 +1,7 @@
1
- # app.py (Two-page evaluation: Intro+Examples -> Evaluation, with PID gate & progress bar)
2
- import os, io, csv, json, random
3
  from datetime import datetime
 
4
  import gradio as gr
5
  from huggingface_hub import HfApi, hf_hub_download
6
 
@@ -8,8 +9,17 @@ from huggingface_hub import HfApi, hf_hub_download
8
  REPO_ID = os.getenv("RESULTS_REPO", "sgtlim/videoeval_results") # ์—…๋กœ๋“œํ•œ ๋ฆฌํฌ์™€ ์ผ์น˜
9
  HF_TOKEN = os.getenv("HF_TOKEN")
10
  RESULTS_FILE = "results.csv"
11
- TOTAL_PER_PARTICIPANT = 50 # ๋ชฉํ‘œ ํ‰๊ฐ€ ๊ฐœ์ˆ˜(์„ธ์…˜ ๊ธฐ์ค€)
 
 
 
 
 
 
 
 
12
 
 
13
  # videos.json ์˜ˆ์‹œ: {"url": "...mp4", "id": "BodyWeightSquats__XXXX.mp4", "action": "BodyWeightSquats"}
14
  with open("videos.json", "r", encoding="utf-8") as f:
15
  V = json.load(f)
@@ -22,11 +32,81 @@ INSTRUCTION_MD = """
22
  - The generated video should **capture** the expected action **throughout the video**.
23
  - Try to **focus only** on the expected action and do **not** judge **video quality**, **attractiveness**, **background**, **camera motion**, or **objects**.
24
  - You will be **paid** once **all the videos are viewed and rated**.
25
-
26
- Below are an example of what a **good** v/s **bad** **action consistent** videos look like: <insert a few examples>
27
  """
28
 
29
- # -------------------- Helper funcs --------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  def _read_csv_bytes():
31
  try:
32
  p = hf_hub_download(
@@ -41,45 +121,78 @@ def _append(old_bytes, row):
41
  s = io.StringIO()
42
  w = csv.writer(s)
43
  if not old_bytes:
44
- w.writerow(["ts_iso", "participant_id", "action", "score_0_10", "notes"])
45
  else:
46
  s.write(old_bytes.decode("utf-8", errors="ignore"))
47
  w.writerow(row)
48
  return s.getvalue().encode("utf-8")
49
 
50
- def push(participant_id, action_name, score, notes=""):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  if not participant_id or not participant_id.strip():
52
  return gr.update(visible=True, value="โ— Please enter your Participant ID before proceeding.")
53
- if not action_name or score is None:
54
  return gr.update(visible=True, value="โ— Fill out all fields.")
55
- old = _read_csv_bytes()
56
- row = [
57
- datetime.utcnow().isoformat(),
58
- participant_id.strip(),
59
- action_name,
60
- float(score),
61
- notes or ""
62
- ]
63
- newb = _append(old, row)
64
- api.upload_file(
65
- path_or_fileobj=io.BytesIO(newb),
66
- path_in_repo=RESULTS_FILE,
67
- repo_id=REPO_ID,
68
- repo_type="dataset",
69
- token=HF_TOKEN,
70
- commit_message="append"
71
- )
72
- return gr.update(visible=True, value=f"โœ… Saved for {action_name}.")
73
 
74
- def _extract_action(v):
75
- if "action" in v and v["action"]:
76
- return v["action"]
77
- raw = v.get("id", "")
78
- return raw.split("__")[0].split(".")[0]
 
 
 
 
 
79
 
80
- def pick_one():
81
- v = random.choice(V)
82
- return v["url"], _extract_action(v)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
  def _progress_html(done, total):
85
  pct = int(100 * done / max(1, total))
@@ -90,6 +203,76 @@ def _progress_html(done, total):
90
  <div style="font-size:12px; margin-top:4px;">{done} / {total}</div>
91
  """
92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  # -------------------- Example videos (download to local cache) --------------------
94
  EXAMPLES = {
95
  "BodyWeightSquats": {
@@ -117,35 +300,116 @@ for cls, files in EXAMPLES.items():
117
  except Exception as e:
118
  print(f"[WARN] example missing: {cls} {kind} -> {fname}: {e}")
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  # -------------------- UI --------------------
121
- with gr.Blocks(theme="soft", fill_height=True) as demo:
 
 
 
 
122
  # ------------------ PAGE 1: Intro + Examples ------------------
123
  page_intro = gr.Group(visible=True)
124
  with page_intro:
125
  gr.Markdown("## ๐ŸŽฏ Action Consistency Human Evaluation")
126
  gr.Markdown(INSTRUCTION_MD)
127
 
128
- # Examples: Squats
129
- with gr.Accordion("Examples: BodyWeightSquats", open=True):
 
130
  with gr.Row():
131
  with gr.Column():
132
- gr.Markdown("**Real / Good**")
133
- gr.Video(value=EX_CACHE["BodyWeightSquats"]["real"], height=240, autoplay=False)
134
  with gr.Column():
135
- gr.Markdown("**Generated / Bad**")
136
- gr.Video(value=EX_CACHE["BodyWeightSquats"]["bad"], height=240, autoplay=False)
137
  if not (EX_CACHE["BodyWeightSquats"]["real"] and EX_CACHE["BodyWeightSquats"]["bad"]):
138
  gr.Markdown("> โš ๏ธ Upload `examples/BodyWeightSquats_real.mp4` and `_bad.mp4` to show both samples.")
139
 
140
  # Examples: WallPushUps
141
- with gr.Accordion("Examples: WallPushUps", open=False):
 
142
  with gr.Row():
143
  with gr.Column():
144
- gr.Markdown("**Real / Good**")
145
- gr.Video(value=EX_CACHE["WallPushUps"]["real"], height=240, autoplay=False)
146
  with gr.Column():
147
- gr.Markdown("**Generated / Bad**")
148
- gr.Video(value=EX_CACHE["WallPushUps"]["bad"], height=240, autoplay=False)
149
  if not (EX_CACHE["WallPushUps"]["real"] and EX_CACHE["WallPushUps"]["bad"]):
150
  gr.Markdown("> โš ๏ธ Upload `examples/WallPushUps_real.mp4` and `_bad.mp4` to show both samples.")
151
 
@@ -157,7 +421,7 @@ with gr.Blocks(theme="soft", fill_height=True) as demo:
157
  understood.change(_toggle_start, inputs=understood, outputs=start_btn)
158
 
159
  # ------------------ PAGE 2: Evaluation ------------------
160
- page_eval = gr.Group(visible=False)
161
  with page_eval:
162
  # PID ์ž…๋ ฅ
163
  with gr.Row():
@@ -185,56 +449,94 @@ with gr.Blocks(theme="soft", fill_height=True) as demo:
185
  pid.change(_toggle_by_pid, inputs=pid, outputs=save_next)
186
 
187
  # -------- ํŽ˜์ด์ง€ ์ „ํ™˜ & ์ฒซ ๋กœ๋“œ --------
 
 
 
 
188
  def _start_and_load_first():
189
- url, action = pick_one()
 
 
 
 
 
 
 
 
190
  return (
191
- gr.update(visible=False), # page_intro off
192
- gr.update(visible=True), # page_eval on
193
- url, action, 5.0, "", 0, _progress_html(0, TOTAL_PER_PARTICIPANT)
 
 
 
 
 
 
 
 
194
  )
195
 
196
  start_btn.click(
197
  _start_and_load_first,
198
  inputs=[],
199
- outputs=[page_intro, page_eval, video, action_tb, score, status, done_state, progress]
200
  )
201
 
202
- # -------- Save & Next (๋…ธํŠธ ์—†์Œ) --------
203
- def save_and_next(participant_id, action_name, score_val, done_cnt):
204
  if not participant_id or not participant_id.strip():
 
205
  return (
206
  gr.update(visible=True, value="โ— Please enter your Participant ID."),
207
- gr.update(), gr.update(), # video, action_tb (๋ณ€๊ฒฝ ์—†์Œ)
208
  done_cnt,
209
  _progress_html(done_cnt, TOTAL_PER_PARTICIPANT),
210
- 5.0 # score reset ์œ ์ง€
 
 
211
  )
212
- status_msg = push(participant_id, action_name, score_val, "")
 
213
 
214
  new_done = int(done_cnt) + 1
215
- if new_done >= TOTAL_PER_PARTICIPANT:
 
216
  return (
217
- status_msg,
218
- None, "", # ๋งˆ์ง€๋ง‰: ์ƒˆ ๋น„๋””์˜ค ๋กœ๋“œ ์•ˆ ํ•จ
219
- TOTAL_PER_PARTICIPANT,
 
220
  _progress_html(TOTAL_PER_PARTICIPANT, TOTAL_PER_PARTICIPANT),
221
- 5.0
 
 
222
  )
223
 
224
- url, next_action = pick_one()
 
 
 
225
  return (
226
  status_msg,
227
- url, next_action,
 
228
  new_done,
229
  _progress_html(new_done, TOTAL_PER_PARTICIPANT),
230
- 5.0
 
 
231
  )
232
 
233
  save_next.click(
234
  save_and_next,
235
- inputs=[pid, action_tb, score, done_state],
236
- outputs=[status, video, action_tb, done_state, progress, score]
 
 
237
  )
238
 
239
  if __name__ == "__main__":
 
 
240
  demo.launch()
 
1
+ # app.py โ€” Human Eval UI (audio-stripped delivery)
2
+ import os, io, csv, json, random, subprocess, hashlib, urllib.request, shutil
3
  from datetime import datetime
4
+
5
  import gradio as gr
6
  from huggingface_hub import HfApi, hf_hub_download
7
 
 
9
  REPO_ID = os.getenv("RESULTS_REPO", "sgtlim/videoeval_results") # ์—…๋กœ๋“œํ•œ ๋ฆฌํฌ์™€ ์ผ์น˜
10
  HF_TOKEN = os.getenv("HF_TOKEN")
11
  RESULTS_FILE = "results.csv"
12
+ TOTAL_PER_PARTICIPANT = 30 # ๋ชฉํ‘œ ํ‰๊ฐ€ ๊ฐœ์ˆ˜(์„ธ์…˜ ๊ธฐ์ค€)
13
+
14
+ # ๐Ÿ”‡ Audio stripping (runtime mute) config
15
+ MUTE_AUDIO = True
16
+ HAS_FFMPEG = shutil.which("ffmpeg") is not None
17
+ RAW_DIR = "/tmp/raw_videos"
18
+ MUTED_DIR = "/tmp/muted_videos"
19
+ os.makedirs(RAW_DIR, exist_ok=True)
20
+ os.makedirs(MUTED_DIR, exist_ok=True)
21
 
22
+ # -------------------- Data --------------------
23
  # videos.json ์˜ˆ์‹œ: {"url": "...mp4", "id": "BodyWeightSquats__XXXX.mp4", "action": "BodyWeightSquats"}
24
  with open("videos.json", "r", encoding="utf-8") as f:
25
  V = json.load(f)
 
32
  - The generated video should **capture** the expected action **throughout the video**.
33
  - Try to **focus only** on the expected action and do **not** judge **video quality**, **attractiveness**, **background**, **camera motion**, or **objects**.
34
  - You will be **paid** once **all the videos are viewed and rated**.
 
 
35
  """
36
 
37
+ # -------------------- Audio-strip helpers --------------------
38
+ def _safe_name(s: str) -> str:
39
+ return hashlib.sha1(s.encode("utf-8", errors="ignore")).hexdigest()
40
+
41
+ def _get_video_id(v: dict) -> str:
42
+ if "id" in v and v["id"]:
43
+ return v["id"]
44
+ return os.path.basename(v.get("url", ""))
45
+
46
+ def _download_to_tmp(url: str) -> str:
47
+ """Download remote video to RAW_DIR; return local path."""
48
+ basename = _safe_name(url) + ".mp4"
49
+ dst = os.path.join(RAW_DIR, basename)
50
+ if not os.path.exists(dst):
51
+ urllib.request.urlretrieve(url, dst)
52
+ return dst
53
+
54
+ def _muted_copy_fast(local_in: str, out_path: str):
55
+ """Fast path: copy video stream, drop audio (-an)."""
56
+ cmd = ["ffmpeg", "-y", "-i", local_in, "-c:v", "copy", "-an", out_path]
57
+ subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
58
+
59
+ def _muted_copy_reencode(local_in: str, out_path: str):
60
+ """Compatibility fallback: re-encode video (H.264), drop audio."""
61
+ cmd = [
62
+ "ffmpeg","-y","-i", local_in,
63
+ "-vf","format=yuv420p","-movflags","+faststart",
64
+ "-c:v","libx264","-crf","18","-preset","veryfast",
65
+ "-an", out_path
66
+ ]
67
+ subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
68
+
69
+ def _src_for_gradio(v: dict) -> str:
70
+ """
71
+ Return a local path to a muted copy of the video if MUTE_AUDIO & ffmpeg available;
72
+ otherwise return original url/path.
73
+ """
74
+ src = v.get("url", "")
75
+ vid_id = _get_video_id(v) or _safe_name(src)
76
+
77
+ if not MUTE_AUDIO or not HAS_FFMPEG or not src:
78
+ return src # fall back to original (no mute possible)
79
+
80
+ # Resolve local input path (handles http(s) and local paths)
81
+ if src.startswith("http://") or src.startswith("https://"):
82
+ local_in = _download_to_tmp(src)
83
+ else:
84
+ local_in = src
85
+
86
+ # Build a stable muted cache filename by video_id
87
+ muted_out = os.path.join(MUTED_DIR, f"{_safe_name(vid_id)}.mp4")
88
+ if os.path.exists(muted_out):
89
+ return muted_out
90
+
91
+ try:
92
+ _muted_copy_fast(local_in, muted_out)
93
+ except Exception:
94
+ try:
95
+ _muted_copy_reencode(local_in, muted_out)
96
+ except Exception as e2:
97
+ # As a last resort, return original source
98
+ print(f"[WARN] failed to produce muted copy for {vid_id}: {type(e2).__name__}: {e2}")
99
+ return src
100
+
101
+ return muted_out
102
+
103
+ def _extract_action(v):
104
+ if "action" in v and v["action"]:
105
+ return v["action"]
106
+ raw = v.get("id", "")
107
+ return raw.split("__")[0].split(".")[0]
108
+
109
+ # -------------------- HF CSV helpers --------------------
110
  def _read_csv_bytes():
111
  try:
112
  p = hf_hub_download(
 
121
  s = io.StringIO()
122
  w = csv.writer(s)
123
  if not old_bytes:
124
+ w.writerow(["ts_iso", "participant_id", "video_id", "overall", "notes"])
125
  else:
126
  s.write(old_bytes.decode("utf-8", errors="ignore"))
127
  w.writerow(row)
128
  return s.getvalue().encode("utf-8")
129
 
130
+ def _load_eval_counts():
131
+ """
132
+ Hugging Face dataset์˜ results.csv๋ฅผ ์ฝ์–ด video_id๋ณ„ ํ‰๊ฐ€ ๊ฐœ์ˆ˜(dict)๋ฅผ ๋ฐ˜ํ™˜.
133
+ ์—†์œผ๋ฉด 0์œผ๋กœ ์ดˆ๊ธฐํ™”.
134
+ """
135
+ counts = {_get_video_id(v): 0 for v in V}
136
+ b = _read_csv_bytes()
137
+ if not b:
138
+ return counts
139
+
140
+ s = io.StringIO(b.decode("utf-8", errors="ignore"))
141
+ r = csv.reader(s)
142
+ rows = list(r)
143
+ if not rows:
144
+ return counts
145
+
146
+ header = rows[0]
147
+ body = rows[1:] if header and ("video_id" in header or "overall" in header) else rows
148
+ vid_col = header.index("video_id") if header and "video_id" in header else None
149
+
150
+ for row in body:
151
+ try:
152
+ vid = row[vid_col] if vid_col is not None else row[2] # ts, pid, video_id, overall, notes
153
+ if vid in counts:
154
+ counts[vid] += 1
155
+ except Exception:
156
+ continue
157
+ return counts
158
+
159
+ def push(participant_id, video_id, score, notes=""):
160
  if not participant_id or not participant_id.strip():
161
  return gr.update(visible=True, value="โ— Please enter your Participant ID before proceeding.")
162
+ if not video_id or score is None:
163
  return gr.update(visible=True, value="โ— Fill out all fields.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
+ try:
166
+ old = _read_csv_bytes()
167
+ row = [
168
+ datetime.utcnow().isoformat(),
169
+ participant_id.strip(),
170
+ video_id, # โœ… action ๋Œ€์‹  video_id ์ €์žฅ
171
+ float(score), # overall
172
+ notes or ""
173
+ ]
174
+ newb = _append(old, row)
175
 
176
+ if not REPO_ID:
177
+ return gr.update(visible=True, value="โ— RESULTS_REPO is not set.")
178
+ if not HF_TOKEN:
179
+ return gr.update(visible=True, value="โ— HF_TOKEN is missing. Set a write token for the dataset repo.")
180
+
181
+ api.upload_file(
182
+ path_or_fileobj=io.BytesIO(newb),
183
+ path_in_repo=RESULTS_FILE,
184
+ repo_id=REPO_ID,
185
+ repo_type="dataset",
186
+ token=HF_TOKEN,
187
+ commit_message="append"
188
+ )
189
+ return gr.update(visible=True, value=f"โœ… Saved for {video_id}.")
190
+ except Exception as e:
191
+ return gr.update(
192
+ visible=True,
193
+ value=f"โŒ Save failed: {type(e).__name__}: {e}\n"
194
+ f"- Check HF_TOKEN permission\n- Check REPO_ID\n- Create dataset repo if missing"
195
+ )
196
 
197
  def _progress_html(done, total):
198
  pct = int(100 * done / max(1, total))
 
203
  <div style="font-size:12px; margin-top:4px;">{done} / {total}</div>
204
  """
205
 
206
+ # -------------------- Scheduling (least-first + anchor) --------------------
207
+ def _build_order_least_first_with_anchor(total:int, anchor_idx:int, repeats:int, min_gap:int=1):
208
+ """
209
+ - results.csv๋ฅผ ์ฝ์–ด video_id๋ณ„ ์นด์šดํŠธ๋ฅผ ๊ณ„์‚ฐ
210
+ - ์•ต์ปค(์ฒซ ๋น„๋””์˜ค) NํšŒ ํฌํ•จ, ์—ฐ์† ๊ธˆ์ง€
211
+ - ๋‚˜๋จธ์ง€๋Š” '๊ฐ€์žฅ ์ ๊ฒŒ ํ‰๊ฐ€๋œ ์ˆœ'์œผ๋กœ ์ค‘๋ณต ์—†์ด ์ฑ„์›€
212
+ """
213
+ assert repeats <= total
214
+ N = len(V)
215
+ assert N >= 1
216
+
217
+ def vid_of(i): return _get_video_id(V[i])
218
+
219
+ counts = _load_eval_counts()
220
+
221
+ candidates = [i for i in range(N) if i != anchor_idx]
222
+ random.shuffle(candidates) # ๋™๋ฅ  ๋žœ๋คํ™”
223
+ candidates.sort(key=lambda i: counts.get(vid_of(i), 0))
224
+
225
+ others_needed = total - repeats
226
+ if len(candidates) < others_needed:
227
+ raise ValueError("Not enough unique non-anchor videos to fill the schedule without duplication.")
228
+
229
+ others = candidates[:others_needed]
230
+ random.shuffle(others)
231
+
232
+ seq = [None] * total
233
+ segment = total // repeats if repeats > 0 else total
234
+ anchor_positions = []
235
+ for k in range(repeats):
236
+ lo = k * segment
237
+ hi = (k + 1) * segment if k < repeats - 1 else total
238
+ cand = random.randrange(lo, hi)
239
+
240
+ def ok(pos):
241
+ return all(abs(pos - p) >= (min_gap + 1) for p in anchor_positions)
242
+
243
+ found = None
244
+ for d in range(0, max(1, segment)):
245
+ for sgn in (+1, -1):
246
+ pos = cand + sgn * d
247
+ if 0 <= pos < total and ok(pos):
248
+ found = pos
249
+ break
250
+ if found is not None:
251
+ break
252
+ if found is None:
253
+ for pos in range(total):
254
+ if ok(pos):
255
+ found = pos
256
+ break
257
+ if found is None:
258
+ raise RuntimeError("Failed to place anchor without adjacency.")
259
+ anchor_positions.append(found)
260
+
261
+ for pos in anchor_positions:
262
+ seq[pos] = anchor_idx
263
+
264
+ j = 0
265
+ for i in range(total):
266
+ if seq[i] is None:
267
+ seq[i] = others[j]
268
+ j += 1
269
+
270
+ assert sum(1 for x in seq if x == anchor_idx) == repeats
271
+ for i in range(1, total):
272
+ assert not (seq[i] == anchor_idx and seq[i-1] == anchor_idx), "Adjacent anchors found."
273
+
274
+ return seq
275
+
276
  # -------------------- Example videos (download to local cache) --------------------
277
  EXAMPLES = {
278
  "BodyWeightSquats": {
 
300
  except Exception as e:
301
  print(f"[WARN] example missing: {cls} {kind} -> {fname}: {e}")
302
 
303
+ def _example_src(cls: str, kind: str):
304
+ p = EX_CACHE[cls][kind]
305
+ if not p:
306
+ return None
307
+ # dict ๋ชจ์–‘์œผ๋กœ ๊ฐ์‹ธ์„œ ๋™์ผ ํŒŒ์ดํ”„๋ผ์ธ(_src_for_gradio) ์‚ฌ์šฉ
308
+ return _src_for_gradio({"url": p, "id": f"example::{cls}::{kind}"})
309
+
310
+ # -------------------- CSS --------------------
311
+ GLOBAL_CSS = """
312
+ /* ===== ๊ณตํ†ต ๋ณ€์ˆ˜ ํˆฌ๋ช…ํ™” (v3/v4 ๋‘˜๋‹ค) ===== */
313
+ :root, .gradio-container {
314
+ --body-background-fill: transparent !important;
315
+ --background-fill-primary: transparent !important;
316
+ --background-fill-secondary: transparent !important;
317
+ --block-background-fill: transparent !important;
318
+ --block-border-color: transparent !important;
319
+ --panel-background-fill: transparent !important;
320
+ --panel-border-color: transparent !important;
321
+ --section-header-background-fill: transparent !important;
322
+ --shadow-drop: 0 0 0 rgba(0,0,0,0) !important;
323
+ --shadow-spread: 0 0 0 rgba(0,0,0,0) !important;
324
+ }
325
+ .gradio-container .bg-white,
326
+ .gradio-container .bg-gray-50,
327
+ .gradio-container .bg-gray-100,
328
+ .gradio-container .bg-slate-50,
329
+ .gradio-container .bg-neutral-50,
330
+ .gradio-container .bg-secondary,
331
+ .gradio-container .border,
332
+ .gradio-container .shadow,
333
+ .gradio-container .shadow-sm,
334
+ .gradio-container .shadow-md,
335
+ .gradio-container .ring-1,
336
+ .gradio-container .ring,
337
+ .gradio-container .gr-card,
338
+ .gradio-container .prose > *:where(hr) {
339
+ background: transparent !important;
340
+ box-shadow: none !important;
341
+ border-color: transparent !important;
342
+ }
343
+ .gradio-container .gr-panel,
344
+ .gradio-container .gr-group,
345
+ .gradio-container .gr-box,
346
+ .gradio-container .gr-row,
347
+ .gradio-container .gr-column,
348
+ .gradio-container .gr-accordion,
349
+ .gradio-container .gr-block,
350
+ .gradio-container .gr-form,
351
+ .gradio-container .gr-tabs,
352
+ .gradio-container .gr-tabitem,
353
+ .gradio-container .gr-section-header {
354
+ background: transparent !important;
355
+ box-shadow: none !important;
356
+ border: none !important;
357
+ }
358
+ .gradio-container hr,
359
+ .gradio-container .gr-divider,
360
+ .gradio-container .gr-accordion .label {
361
+ background: transparent !important;
362
+ border: none !important;
363
+ box-shadow: none !important;
364
+ }
365
+ html, body, .gradio-container { background: transparent !important; }
366
+ #eval [class*="bg-"],
367
+ #eval [class*="border"],
368
+ #eval [class*="shadow"],
369
+ #eval .gr-panel, #eval .gr-group, #eval .gr-box, #eval .gr-row, #eval .gr-column,
370
+ #eval .gr-block, #eval .gr-form, #eval .gr-section-header, #eval .gr-accordion {
371
+ background: transparent !important;
372
+ border-color: transparent !important;
373
+ box-shadow: none !important;
374
+ }
375
+ #eval .gr-form, #eval .gr-panel { background: transparent !important; box-shadow:none !important; border:none !important; }
376
+ """
377
+
378
  # -------------------- UI --------------------
379
+ with gr.Blocks(fill_height=True, css=GLOBAL_CSS) as demo:
380
+ order_state = gr.State(value=[])
381
+ ptr_state = gr.State(value=0)
382
+ cur_video_id = gr.State(value="")
383
+
384
  # ------------------ PAGE 1: Intro + Examples ------------------
385
  page_intro = gr.Group(visible=True)
386
  with page_intro:
387
  gr.Markdown("## ๐ŸŽฏ Action Consistency Human Evaluation")
388
  gr.Markdown(INSTRUCTION_MD)
389
 
390
+ # Examples: BodyWeightSquats
391
+ with gr.Group():
392
+ gr.Markdown("### Examples: BodyWeightSquats")
393
  with gr.Row():
394
  with gr.Column():
395
+ gr.Markdown("**Expected depiction of action**")
396
+ gr.Video(value=_example_src("BodyWeightSquats","real"), height=240, autoplay=False)
397
  with gr.Column():
398
+ gr.Markdown("**Poorly generated action**")
399
+ gr.Video(value=_example_src("BodyWeightSquats","bad"), height=240, autoplay=False)
400
  if not (EX_CACHE["BodyWeightSquats"]["real"] and EX_CACHE["BodyWeightSquats"]["bad"]):
401
  gr.Markdown("> โš ๏ธ Upload `examples/BodyWeightSquats_real.mp4` and `_bad.mp4` to show both samples.")
402
 
403
  # Examples: WallPushUps
404
+ with gr.Group():
405
+ gr.Markdown("### Examples: WallPushUps")
406
  with gr.Row():
407
  with gr.Column():
408
+ gr.Markdown("**Expected depiction of action**")
409
+ gr.Video(value=_example_src("WallPushUps","real"), height=240, autoplay=False)
410
  with gr.Column():
411
+ gr.Markdown("**Poorly generated action**")
412
+ gr.Video(value=_example_src("WallPushUps","bad"), height=240, autoplay=False)
413
  if not (EX_CACHE["WallPushUps"]["real"] and EX_CACHE["WallPushUps"]["bad"]):
414
  gr.Markdown("> โš ๏ธ Upload `examples/WallPushUps_real.mp4` and `_bad.mp4` to show both samples.")
415
 
 
421
  understood.change(_toggle_start, inputs=understood, outputs=start_btn)
422
 
423
  # ------------------ PAGE 2: Evaluation ------------------
424
+ page_eval = gr.Group(visible=False, elem_id="eval")
425
  with page_eval:
426
  # PID ์ž…๋ ฅ
427
  with gr.Row():
 
449
  pid.change(_toggle_by_pid, inputs=pid, outputs=save_next)
450
 
451
  # -------- ํŽ˜์ด์ง€ ์ „ํ™˜ & ์ฒซ ๋กœ๋“œ --------
452
+ ANCHOR_IDX = 0 # videos.json์˜ ๋งจ ์ฒซ ๋น„๋””์˜ค
453
+ ANCHOR_REPEATS = 5 # ์•ต์ปค 5ํšŒ
454
+ MIN_GAP = 1 # ์•ต์ปค ์—ฐ์† ๊ธˆ์ง€(์ธ์ ‘ ๊ธˆ์ง€)
455
+
456
  def _start_and_load_first():
457
+ total = TOTAL_PER_PARTICIPANT
458
+ order = _build_order_least_first_with_anchor(
459
+ total=total,
460
+ anchor_idx=ANCHOR_IDX,
461
+ repeats=ANCHOR_REPEATS,
462
+ min_gap=MIN_GAP
463
+ )
464
+ first_idx = order[0]
465
+ v0 = V[first_idx]
466
  return (
467
+ gr.update(visible=False), # page_intro off
468
+ gr.update(visible=True), # page_eval on
469
+ _src_for_gradio(v0), # ๐Ÿ”‡ muted source
470
+ _extract_action(v0), # expected action label
471
+ 5.0, # score reset
472
+ gr.update(visible=False, value=""), # status hide
473
+ 0, # done count
474
+ _progress_html(0, TOTAL_PER_PARTICIPANT),
475
+ order, # order_state
476
+ 1, # ptr_state
477
+ _get_video_id(v0) # cur_video_id
478
  )
479
 
480
  start_btn.click(
481
  _start_and_load_first,
482
  inputs=[],
483
+ outputs=[page_intro, page_eval, video, action_tb, score, status, done_state, progress, order_state, ptr_state, cur_video_id]
484
  )
485
 
486
+ # -------- Save & Next --------
487
+ def save_and_next(participant_id, current_video_id, score_val, done_cnt, order, ptr):
488
  if not participant_id or not participant_id.strip():
489
+ # PID ์—†์œผ๋ฉด ๊ธฐ์กด ํ™”๋ฉด ์œ ์ง€
490
  return (
491
  gr.update(visible=True, value="โ— Please enter your Participant ID."),
492
+ gr.update(), gr.update(), # video, action_tb ๋ณ€๊ฒฝ ์—†์Œ
493
  done_cnt,
494
  _progress_html(done_cnt, TOTAL_PER_PARTICIPANT),
495
+ 5.0,
496
+ ptr,
497
+ current_video_id
498
  )
499
+
500
+ status_msg = push(participant_id, current_video_id, score_val, "")
501
 
502
  new_done = int(done_cnt) + 1
503
+ # ์ข…๋ฃŒ ์กฐ๊ฑด: ๋ชฉํ‘œ ๊ฐœ์ˆ˜ ๋‹ฌ์„ฑ or ์ˆœ์„œ ์†Œ์ง„
504
+ if new_done >= TOTAL_PER_PARTICIPANT or ptr >= len(order):
505
  return (
506
+ status_msg, # status
507
+ None, # video ๋น„์šฐ๊ธฐ
508
+ "", # action_tb ๋น„์šฐ๊ธฐ
509
+ TOTAL_PER_PARTICIPANT, # done_state ์ตœ์ข…
510
  _progress_html(TOTAL_PER_PARTICIPANT, TOTAL_PER_PARTICIPANT),
511
+ 5.0, # score ๋ฆฌ์…‹
512
+ ptr,
513
+ current_video_id
514
  )
515
 
516
+ # ๋‹ค์Œ ์˜์ƒ ๋กœ๋“œ
517
+ next_idx = order[ptr]
518
+ v = V[next_idx]
519
+ next_vid = _get_video_id(v)
520
  return (
521
  status_msg,
522
+ _src_for_gradio(v), # ๐Ÿ”‡ muted source
523
+ _extract_action(v),
524
  new_done,
525
  _progress_html(new_done, TOTAL_PER_PARTICIPANT),
526
+ 5.0,
527
+ ptr + 1,
528
+ next_vid
529
  )
530
 
531
  save_next.click(
532
  save_and_next,
533
+ # โœ… cur_video_id๋ฅผ ๋‘ ๋ฒˆ์งธ ์ธ์ž๋กœ ๋„˜๊น€
534
+ inputs=[pid, cur_video_id, score, done_state, order_state, ptr_state],
535
+ # โœ… ๋งˆ์ง€๋ง‰์— cur_video_id๋ฅผ outputs๋กœ ๋ฐ›์Œ(์ƒํƒœ ๊ฐฑ์‹ )
536
+ outputs=[status, video, action_tb, done_state, progress, score, ptr_state, cur_video_id]
537
  )
538
 
539
  if __name__ == "__main__":
540
+ if MUTE_AUDIO and not HAS_FFMPEG:
541
+ print("[WARN] MUTE_AUDIO=True but ffmpeg not found. Videos will be served with original audio.")
542
  demo.launch()
upload_videos.py โ†’ upload_videos_toHF.py RENAMED
File without changes