Spaces:
Running
Running
i18n: UI copy to Simplified Chinese
Browse files
app.py
CHANGED
|
@@ -94,7 +94,7 @@ def _extract_prompt(task: dict[str, Any]) -> str:
|
|
| 94 |
val = prompts.get(level)
|
| 95 |
if isinstance(val, list) and val:
|
| 96 |
n = len(val)
|
| 97 |
-
return "\n\n".join(f"—
|
| 98 |
if isinstance(val, str) and val:
|
| 99 |
return val
|
| 100 |
return "(no prompt found)"
|
|
@@ -247,9 +247,9 @@ def _stats_md() -> str:
|
|
| 247 |
total = len(POOL)
|
| 248 |
pct = 100.0 * n_sub / total if total else 0.0
|
| 249 |
return (
|
| 250 |
-
f"**
|
| 251 |
-
f"
|
| 252 |
-
f"
|
| 253 |
)
|
| 254 |
|
| 255 |
|
|
@@ -257,9 +257,9 @@ def _meta_md(model: str, task: dict[str, Any], state: dict) -> str:
|
|
| 257 |
done_by_me = state.get("submitted_count", 0)
|
| 258 |
claimed_n = state.get("claimed_n", 0)
|
| 259 |
lines = [
|
| 260 |
-
f"**
|
| 261 |
-
f"**
|
| 262 |
-
f"**task_id**
|
| 263 |
]
|
| 264 |
return "\n\n".join(lines)
|
| 265 |
|
|
@@ -268,41 +268,36 @@ def _render_video_html(url: str) -> str:
|
|
| 268 |
return (
|
| 269 |
f'<video src="{url}" controls autoplay loop muted playsinline '
|
| 270 |
f'style="width:100%;height:520px;background:#000;border-radius:8px;object-fit:contain;">'
|
| 271 |
-
f'
|
| 272 |
)
|
| 273 |
|
| 274 |
|
| 275 |
PLACEHOLDER_VIDEO = (
|
| 276 |
"<div style='height:520px;display:flex;align-items:center;justify-content:center;"
|
| 277 |
"color:#888;background:#111;border-radius:8px;'>"
|
| 278 |
-
"
|
| 279 |
"</div>"
|
| 280 |
)
|
| 281 |
|
| 282 |
ALL_DONE_VIDEO = (
|
| 283 |
"<div style='height:520px;display:flex;align-items:center;justify-content:center;"
|
| 284 |
"color:#4a7;background:#111;border-radius:8px;font-size:18px;'>"
|
| 285 |
-
"🎉
|
| 286 |
"</div>"
|
| 287 |
)
|
| 288 |
|
| 289 |
|
| 290 |
def _next_item_for(state: dict) -> tuple[str, str, str, dict]:
|
| 291 |
-
"""Advance `state` to the next unclaimed item and return render strings.
|
| 292 |
-
|
| 293 |
-
Returns (video_html, meta_md, prompt_text, updated_state).
|
| 294 |
-
"""
|
| 295 |
annotator = state["annotator"]
|
| 296 |
order = state["order"]
|
| 297 |
idx = state.get("idx", 0)
|
| 298 |
|
| 299 |
-
# Release the currently-held claim if any (e.g. after submit the caller
|
| 300 |
-
# already released it; on first call there's nothing to release).
|
| 301 |
new_idx, mt = _claim_next(annotator, order, idx)
|
| 302 |
state["idx"] = new_idx
|
| 303 |
if mt is None:
|
| 304 |
state["current"] = None
|
| 305 |
-
return ALL_DONE_VIDEO, "**
|
| 306 |
|
| 307 |
state["current"] = mt
|
| 308 |
state["claimed_n"] = state.get("claimed_n", 0) + 1
|
|
@@ -325,18 +320,15 @@ def start_session(annotator: str, state: dict):
|
|
| 325 |
if not annotator:
|
| 326 |
return (
|
| 327 |
state, PLACEHOLDER_VIDEO, "", "",
|
| 328 |
-
"⚠️
|
| 329 |
)
|
| 330 |
-
# Release any claim an older session of this user still holds in-memory.
|
| 331 |
-
# (We can't detect cross-browser sessions of the same user, but within one
|
| 332 |
-
# process a user holding an old claim and logging in again will free it.)
|
| 333 |
order = list(range(len(POOL)))
|
| 334 |
rng = random.Random(f"{annotator}-{int(time.time())}-{uuid.uuid4().hex}")
|
| 335 |
rng.shuffle(order)
|
| 336 |
state = {"annotator": annotator, "order": order, "idx": 0,
|
| 337 |
"current": None, "submitted_count": 0, "claimed_n": 0}
|
| 338 |
video_html, meta, prompt, state = _next_item_for(state)
|
| 339 |
-
status = f"✅
|
| 340 |
return state, video_html, meta, prompt, status, _stats_md()
|
| 341 |
|
| 342 |
|
|
@@ -344,15 +336,14 @@ def submit_and_next(state: dict, verdict: str, note: str):
|
|
| 344 |
"""Record current claim as submitted, then advance."""
|
| 345 |
if not state or "annotator" not in state:
|
| 346 |
return (
|
| 347 |
-
state, PLACEHOLDER_VIDEO, "", "", "
|
| 348 |
-
"⚠️
|
| 349 |
)
|
| 350 |
current = state.get("current")
|
| 351 |
if current is None:
|
| 352 |
-
# No active claim — nothing to submit.
|
| 353 |
return (
|
| 354 |
-
state, ALL_DONE_VIDEO, "**
|
| 355 |
-
"
|
| 356 |
)
|
| 357 |
model, task_id = current
|
| 358 |
record = {
|
|
@@ -362,8 +353,8 @@ def submit_and_next(state: dict, verdict: str, note: str):
|
|
| 362 |
"process_id": PROCESS_ID,
|
| 363 |
"model": model,
|
| 364 |
"task_id": task_id,
|
| 365 |
-
"memory_issue": verdict == "
|
| 366 |
-
"verdict": verdict,
|
| 367 |
"note": (note or "").strip(),
|
| 368 |
}
|
| 369 |
_append_annotation(record)
|
|
@@ -375,9 +366,9 @@ def submit_and_next(state: dict, verdict: str, note: str):
|
|
| 375 |
video_html, meta, prompt, state = _next_item_for(state)
|
| 376 |
return (
|
| 377 |
state, video_html, meta, prompt,
|
| 378 |
-
"
|
| 379 |
-
"",
|
| 380 |
-
f"✅
|
| 381 |
_stats_md(),
|
| 382 |
)
|
| 383 |
|
|
@@ -386,8 +377,8 @@ def skip_and_next(state: dict):
|
|
| 386 |
"""Release the current claim back to the pool and advance."""
|
| 387 |
if not state or "annotator" not in state:
|
| 388 |
return (
|
| 389 |
-
state, PLACEHOLDER_VIDEO, "", "", "
|
| 390 |
-
"⚠️
|
| 391 |
)
|
| 392 |
current = state.get("current")
|
| 393 |
if current is not None:
|
|
@@ -398,8 +389,8 @@ def skip_and_next(state: dict):
|
|
| 398 |
video_html, meta, prompt, state = _next_item_for(state)
|
| 399 |
return (
|
| 400 |
state, video_html, meta, prompt,
|
| 401 |
-
"
|
| 402 |
-
f"⏭️
|
| 403 |
_stats_md(),
|
| 404 |
)
|
| 405 |
|
|
@@ -412,14 +403,13 @@ CUSTOM_CSS = """
|
|
| 412 |
#prompt_box textarea { height: 480px !important; overflow-y: auto !important; }
|
| 413 |
"""
|
| 414 |
|
| 415 |
-
with gr.Blocks(title="MBench-V
|
| 416 |
css=CUSTOM_CSS) as demo:
|
| 417 |
gr.Markdown(
|
| 418 |
"""
|
| 419 |
-
# 🎬 MBench-V
|
| 420 |
|
| 421 |
-
|
| 422 |
-
answer **Yes** or **No**.
|
| 423 |
"""
|
| 424 |
)
|
| 425 |
|
|
@@ -429,37 +419,37 @@ with gr.Blocks(title="MBench-V Annotation", theme=gr.themes.Soft(),
|
|
| 429 |
|
| 430 |
with gr.Row():
|
| 431 |
annotator_in = gr.Textbox(
|
| 432 |
-
label="
|
| 433 |
)
|
| 434 |
-
login_btn = gr.Button("
|
| 435 |
|
| 436 |
-
status_md = gr.Markdown("
|
| 437 |
|
| 438 |
-
# =========
|
| 439 |
with gr.Row(equal_height=True):
|
| 440 |
with gr.Column(scale=3):
|
| 441 |
-
video = gr.HTML(value=PLACEHOLDER_VIDEO, label="
|
| 442 |
with gr.Column(scale=2):
|
| 443 |
prompt_tb = gr.Textbox(
|
| 444 |
-
label="
|
| 445 |
lines=22, max_lines=22,
|
| 446 |
interactive=False, show_copy_button=True,
|
| 447 |
elem_id="prompt_box",
|
| 448 |
)
|
| 449 |
|
| 450 |
-
# =========
|
| 451 |
with gr.Row():
|
| 452 |
with gr.Column(scale=3):
|
| 453 |
-
meta_md = gr.Markdown("
|
| 454 |
with gr.Column(scale=2):
|
| 455 |
verdict = gr.Radio(
|
| 456 |
-
choices=["
|
| 457 |
-
label="
|
| 458 |
)
|
| 459 |
-
note = gr.Textbox(label="
|
| 460 |
with gr.Row():
|
| 461 |
-
submit_btn = gr.Button("✅
|
| 462 |
-
skip_btn = gr.Button("⏭️
|
| 463 |
|
| 464 |
# ========= Wiring =========
|
| 465 |
login_outputs = [state, video, meta_md, prompt_tb, status_md, stats_md]
|
|
|
|
| 94 |
val = prompts.get(level)
|
| 95 |
if isinstance(val, list) and val:
|
| 96 |
n = len(val)
|
| 97 |
+
return "\n\n".join(f"— 第 {i}/{n} 段 —\n{seg}" for i, seg in enumerate(val, 1))
|
| 98 |
if isinstance(val, str) and val:
|
| 99 |
return val
|
| 100 |
return "(no prompt found)"
|
|
|
|
| 247 |
total = len(POOL)
|
| 248 |
pct = 100.0 * n_sub / total if total else 0.0
|
| 249 |
return (
|
| 250 |
+
f"**全局进度**:已提交 {n_sub} / {total}({pct:.1f}%)"
|
| 251 |
+
f" ・ 正在被其他人标注 {n_pend}"
|
| 252 |
+
f" ・ 剩余 {total - n_sub - n_pend}"
|
| 253 |
)
|
| 254 |
|
| 255 |
|
|
|
|
| 257 |
done_by_me = state.get("submitted_count", 0)
|
| 258 |
claimed_n = state.get("claimed_n", 0)
|
| 259 |
lines = [
|
| 260 |
+
f"**本次会话已提交**:{done_by_me} 条(共已领取 {claimed_n} 条)",
|
| 261 |
+
f"**模型**:`{model}`",
|
| 262 |
+
f"**task_id**:`{task['task_id']}`",
|
| 263 |
]
|
| 264 |
return "\n\n".join(lines)
|
| 265 |
|
|
|
|
| 268 |
return (
|
| 269 |
f'<video src="{url}" controls autoplay loop muted playsinline '
|
| 270 |
f'style="width:100%;height:520px;background:#000;border-radius:8px;object-fit:contain;">'
|
| 271 |
+
f'您的浏览器不支持 HTML5 视频。</video>'
|
| 272 |
)
|
| 273 |
|
| 274 |
|
| 275 |
PLACEHOLDER_VIDEO = (
|
| 276 |
"<div style='height:520px;display:flex;align-items:center;justify-content:center;"
|
| 277 |
"color:#888;background:#111;border-radius:8px;'>"
|
| 278 |
+
"请先输入名字并点击 <b>开始</b> 加载视频。"
|
| 279 |
"</div>"
|
| 280 |
)
|
| 281 |
|
| 282 |
ALL_DONE_VIDEO = (
|
| 283 |
"<div style='height:520px;display:flex;align-items:center;justify-content:center;"
|
| 284 |
"color:#4a7;background:#111;border-radius:8px;font-size:18px;'>"
|
| 285 |
+
"🎉 所有视频都已标注完成,感谢您的参与!"
|
| 286 |
"</div>"
|
| 287 |
)
|
| 288 |
|
| 289 |
|
| 290 |
def _next_item_for(state: dict) -> tuple[str, str, str, dict]:
|
| 291 |
+
"""Advance `state` to the next unclaimed item and return render strings."""
|
|
|
|
|
|
|
|
|
|
| 292 |
annotator = state["annotator"]
|
| 293 |
order = state["order"]
|
| 294 |
idx = state.get("idx", 0)
|
| 295 |
|
|
|
|
|
|
|
| 296 |
new_idx, mt = _claim_next(annotator, order, idx)
|
| 297 |
state["idx"] = new_idx
|
| 298 |
if mt is None:
|
| 299 |
state["current"] = None
|
| 300 |
+
return ALL_DONE_VIDEO, "**全部完成**,池中已无剩余任务。", "", state
|
| 301 |
|
| 302 |
state["current"] = mt
|
| 303 |
state["claimed_n"] = state.get("claimed_n", 0) + 1
|
|
|
|
| 320 |
if not annotator:
|
| 321 |
return (
|
| 322 |
state, PLACEHOLDER_VIDEO, "", "",
|
| 323 |
+
"⚠️ 请先输入您的名字。", _stats_md(),
|
| 324 |
)
|
|
|
|
|
|
|
|
|
|
| 325 |
order = list(range(len(POOL)))
|
| 326 |
rng = random.Random(f"{annotator}-{int(time.time())}-{uuid.uuid4().hex}")
|
| 327 |
rng.shuffle(order)
|
| 328 |
state = {"annotator": annotator, "order": order, "idx": 0,
|
| 329 |
"current": None, "submitted_count": 0, "claimed_n": 0}
|
| 330 |
video_html, meta, prompt, state = _next_item_for(state)
|
| 331 |
+
status = f"✅ 欢迎 `{annotator}`,开始标注吧!"
|
| 332 |
return state, video_html, meta, prompt, status, _stats_md()
|
| 333 |
|
| 334 |
|
|
|
|
| 336 |
"""Record current claim as submitted, then advance."""
|
| 337 |
if not state or "annotator" not in state:
|
| 338 |
return (
|
| 339 |
+
state, PLACEHOLDER_VIDEO, "", "", "否", "",
|
| 340 |
+
"⚠️ 请先登录。", _stats_md(),
|
| 341 |
)
|
| 342 |
current = state.get("current")
|
| 343 |
if current is None:
|
|
|
|
| 344 |
return (
|
| 345 |
+
state, ALL_DONE_VIDEO, "**全部完成**。", "", "否", "",
|
| 346 |
+
"当前没有可提交的任务。", _stats_md(),
|
| 347 |
)
|
| 348 |
model, task_id = current
|
| 349 |
record = {
|
|
|
|
| 353 |
"process_id": PROCESS_ID,
|
| 354 |
"model": model,
|
| 355 |
"task_id": task_id,
|
| 356 |
+
"memory_issue": verdict == "是",
|
| 357 |
+
"verdict": verdict,
|
| 358 |
"note": (note or "").strip(),
|
| 359 |
}
|
| 360 |
_append_annotation(record)
|
|
|
|
| 366 |
video_html, meta, prompt, state = _next_item_for(state)
|
| 367 |
return (
|
| 368 |
state, video_html, meta, prompt,
|
| 369 |
+
"否", # reset verdict
|
| 370 |
+
"", # reset note
|
| 371 |
+
f"✅ 已提交第 {state['submitted_count']} 条,下一条 →",
|
| 372 |
_stats_md(),
|
| 373 |
)
|
| 374 |
|
|
|
|
| 377 |
"""Release the current claim back to the pool and advance."""
|
| 378 |
if not state or "annotator" not in state:
|
| 379 |
return (
|
| 380 |
+
state, PLACEHOLDER_VIDEO, "", "", "否", "",
|
| 381 |
+
"⚠️ 请先登录。", _stats_md(),
|
| 382 |
)
|
| 383 |
current = state.get("current")
|
| 384 |
if current is not None:
|
|
|
|
| 389 |
video_html, meta, prompt, state = _next_item_for(state)
|
| 390 |
return (
|
| 391 |
state, video_html, meta, prompt,
|
| 392 |
+
"否", "",
|
| 393 |
+
f"⏭️ 已跳过。本次已提交 {state.get('submitted_count', 0)} 条",
|
| 394 |
_stats_md(),
|
| 395 |
)
|
| 396 |
|
|
|
|
| 403 |
#prompt_box textarea { height: 480px !important; overflow-y: auto !important; }
|
| 404 |
"""
|
| 405 |
|
| 406 |
+
with gr.Blocks(title="MBench-V 标注", theme=gr.themes.Soft(),
|
| 407 |
css=CUSTOM_CSS) as demo:
|
| 408 |
gr.Markdown(
|
| 409 |
"""
|
| 410 |
+
# 🎬 MBench-V 视频标注 —— *该视频是否出现了记忆问题?*
|
| 411 |
|
| 412 |
+
请对照右侧的 5 段 Prompt 观看左侧视频,然后选择 **是** 或 **否**。
|
|
|
|
| 413 |
"""
|
| 414 |
)
|
| 415 |
|
|
|
|
| 419 |
|
| 420 |
with gr.Row():
|
| 421 |
annotator_in = gr.Textbox(
|
| 422 |
+
label="标注员名字", placeholder="例如:alice", scale=4, autofocus=True,
|
| 423 |
)
|
| 424 |
+
login_btn = gr.Button("开始", variant="primary", scale=1)
|
| 425 |
|
| 426 |
+
status_md = gr.Markdown("_尚未开始。_")
|
| 427 |
|
| 428 |
+
# ========= 上排:视频 ↔ Prompt =========
|
| 429 |
with gr.Row(equal_height=True):
|
| 430 |
with gr.Column(scale=3):
|
| 431 |
+
video = gr.HTML(value=PLACEHOLDER_VIDEO, label="生成的视频")
|
| 432 |
with gr.Column(scale=2):
|
| 433 |
prompt_tb = gr.Textbox(
|
| 434 |
+
label="生成 Prompt(共 5 段)",
|
| 435 |
lines=22, max_lines=22,
|
| 436 |
interactive=False, show_copy_button=True,
|
| 437 |
elem_id="prompt_box",
|
| 438 |
)
|
| 439 |
|
| 440 |
+
# ========= 下排:元信息 ↔ 选项 =========
|
| 441 |
with gr.Row():
|
| 442 |
with gr.Column(scale=3):
|
| 443 |
+
meta_md = gr.Markdown("_当前没有加载任务。_")
|
| 444 |
with gr.Column(scale=2):
|
| 445 |
verdict = gr.Radio(
|
| 446 |
+
choices=["否", "是"], value="否",
|
| 447 |
+
label="该视频是否出现了记忆问题?",
|
| 448 |
)
|
| 449 |
+
note = gr.Textbox(label="备注(可选)", lines=2)
|
| 450 |
with gr.Row():
|
| 451 |
+
submit_btn = gr.Button("✅ 提交并下一条", variant="primary")
|
| 452 |
+
skip_btn = gr.Button("⏭️ 跳过")
|
| 453 |
|
| 454 |
# ========= Wiring =========
|
| 455 |
login_outputs = [state, video, meta_md, prompt_tb, status_md, stats_md]
|