qiukingballball RyanCLL claude opus 4.6 ryancll118 commited on
Commit
c8b725d
·
0 Parent(s):

squash history

Browse files

Co-authored-by: ryancll <ryancll@users.noreply.huggingface.co>
Co-authored-by: claude opus 4.6 <claude opus 4.6@users.noreply.huggingface.co>
Co-authored-by: ryancll118 <ryancll118@users.noreply.huggingface.co>

.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Ignore metric.py - it's uploaded to dataset repo, not server repo
2
+ metric.py
3
+
4
+ metric_old.py
5
+ *.zip
6
+ *.csv
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
+ # you will also find guides on how best to write your Dockerfile
3
+
4
+ FROM python:3.9
5
+
6
+ RUN useradd -m -u 1000 user
7
+ USER user
8
+ ENV PATH="/home/user/.local/bin:/usr/local/bin:$PATH"
9
+
10
+ WORKDIR /app
11
+
12
+ COPY --chown=user ./requirements.txt requirements.txt
13
+ RUN pip install --no-cache-dir --upgrade --extra-index-url https://pypi.nvidia.com -r requirements.txt && \
14
+ pip uninstall -y opencv-python opencv-python-headless 2>/dev/null || true && \
15
+ pip install --no-cache-dir opencv-python-headless
16
+
17
+ COPY --chown=user . /app
18
+
19
+ # Create necessary directories
20
+ RUN mkdir -p /app/uploads /app/database
21
+
22
+ CMD ["sh", "-lc", "python worker.py & exec uvicorn app:app --host 0.0.0.0 --port 7860"]
README.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: ICRA26WM
3
+ emoji: 💻
4
+ colorFrom: purple
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
__pycache__/metric.cpython-310.pyc ADDED
Binary file (16.6 kB). View file
 
app.py ADDED
@@ -0,0 +1,728 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ICRA26 Workshop Competition Test Server
3
+
4
+ 主应用文件,使用 FastAPI 构建。
5
+ """
6
+ import asyncio
7
+ import json
8
+ import os
9
+ import shutil
10
+ import uuid
11
+ import random
12
+ import fcntl
13
+ from datetime import datetime, timedelta
14
+ from pathlib import Path
15
+ from typing import Optional, Dict
16
+
17
+ # 修复 HuggingFace Space 中的 OMP_NUM_THREADS 问题
18
+ # HuggingFace 会设置这个变量但值可能无效(空或0),导致 OpenMP 报错
19
+ _omp_val = os.environ.get('OMP_NUM_THREADS', '')
20
+ if not _omp_val or _omp_val == '0':
21
+ os.environ['OMP_NUM_THREADS'] = '1'
22
+
23
+ from fastapi import FastAPI, Request, UploadFile, File, HTTPException
24
+ from fastapi.responses import HTMLResponse, JSONResponse
25
+ from fastapi.staticfiles import StaticFiles
26
+ from fastapi.templating import Jinja2Templates
27
+ from pydantic import BaseModel
28
+
29
+ from dataset_storage import init_storage, DatasetStorage
30
+
31
+ # ============== 配置 ==============
32
+ UPLOAD_DIR = Path(__file__).parent / "uploads"
33
+ UPLOAD_DIR.mkdir(exist_ok=True)
34
+
35
+ # 从环境变量获取配置
36
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
37
+ RESEND_KEY = os.environ.get("RESEND_KEY", "")
38
+ REGISTRATION_URL = "https://huggingface.co/datasets/agibot-world/IROSChallengeWMTrack/resolve/main/icra_form.csv"
39
+ HF_DATASET_REPO = "agibot-world/IROSChallengeWMTrack"
40
+
41
+ # 验证配置
42
+ MAX_DAILY_VALIDATIONS = 2 # 每队每天验证次数
43
+
44
+ app = FastAPI(title="ICRA26 Workshop Competition")
45
+
46
+ # 静态文件和模板
47
+ app.mount("/static", StaticFiles(directory="static"), name="static")
48
+ templates = Jinja2Templates(directory="templates")
49
+
50
+ # 初始化存储
51
+ storage: Optional[DatasetStorage] = None
52
+
53
+
54
+ def get_storage() -> DatasetStorage:
55
+ """获取存储实例"""
56
+ global storage
57
+ if storage is None:
58
+ storage = init_storage(HF_DATASET_REPO, HF_TOKEN)
59
+ return storage
60
+
61
+
62
+ # ============== 串行队列处理 ==============
63
+ SUBMISSIONS_LOCK_FILE = "/tmp/icra26wm_submissions.lock"
64
+
65
+ # ============== Metric 模块 ==============
66
+ metric_module = None
67
+
68
+
69
+ def load_metric_module():
70
+ """从 dataset repo 下载并加载 metric.py"""
71
+ global metric_module
72
+ # 强制重新加载(每次都重新下载)
73
+ metric_module = None
74
+
75
+ if not HF_TOKEN:
76
+ print("HF_TOKEN not configured, using mock evaluation")
77
+ return None
78
+
79
+ try:
80
+ from huggingface_hub import hf_hub_download
81
+ import importlib.util
82
+
83
+ # 下载 metric.py
84
+ print("Downloading metric.py from dataset repo...")
85
+ metric_path = hf_hub_download(
86
+ repo_id=HF_DATASET_REPO,
87
+ filename="metric.py",
88
+ token=HF_TOKEN,
89
+ repo_type="dataset"
90
+ )
91
+ print(f"Downloaded metric.py to: {metric_path}")
92
+
93
+ # 动态加载模块
94
+ spec = importlib.util.spec_from_file_location("metric", metric_path)
95
+ metric_module = importlib.util.module_from_spec(spec)
96
+ spec.loader.exec_module(metric_module)
97
+
98
+ print(f"Loaded metric.py, has evaluate_submission: {hasattr(metric_module, 'evaluate_submission')}")
99
+ return metric_module
100
+ except Exception as e:
101
+ import traceback
102
+ print(f"Failed to load metric.py: {e}")
103
+ traceback.print_exc()
104
+ return None
105
+
106
+
107
+ # ============== 验证码管理 ==============
108
+
109
+ verification_codes: Dict = {}
110
+
111
+
112
+ def load_registration():
113
+ """从 Hugging Face datasets 加载报名名单(新格式字段映射)"""
114
+ registration = {}
115
+ try:
116
+ import urllib.request
117
+ import csv
118
+ from io import StringIO
119
+
120
+ headers = {'User-Agent': 'Mozilla/5.0'}
121
+ if HF_TOKEN:
122
+ headers['Authorization'] = f'Bearer {HF_TOKEN}'
123
+ req = urllib.request.Request(REGISTRATION_URL, headers=headers)
124
+
125
+ with urllib.request.urlopen(req, timeout=10) as response:
126
+ content = response.read().decode('utf-8-sig')
127
+ reader = csv.DictReader(StringIO(content))
128
+ for row in reader:
129
+ email = (row.get('Contact Email') or '').strip().lower()
130
+ team_name = (row.get('Team Name') or '').strip()
131
+ if email:
132
+ registration[email] = team_name
133
+
134
+ print(f"Loaded {len(registration)} registrations from Hugging Face dataset")
135
+ except Exception as e:
136
+ print(f"Failed to load registration from Hugging Face: {e}")
137
+ return registration
138
+
139
+
140
+ def generate_verification_code() -> str:
141
+ """生成6位数字验证码"""
142
+ return ''.join([str(random.randint(0, 9)) for _ in range(6)])
143
+
144
+
145
+ def send_verification_code(email: str, code: str, team_name: str):
146
+ """发送验证码到邮箱"""
147
+ if not RESEND_KEY:
148
+ print(f"\n{'='*50}")
149
+ print(f"验证码发送(调试模式)- ICRA26 Workshop")
150
+ print(f"收件人: {email}")
151
+ print(f"队伍名: {team_name}")
152
+ print(f"验证码: {code}")
153
+ print(f"有效期: 5 分钟")
154
+ print(f"{'='*50}\n")
155
+ return True
156
+
157
+ try:
158
+ import resend
159
+ resend.api_key = RESEND_KEY
160
+ html_content = f"""
161
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
162
+ <h2 style="color: #6366f1;">AgiBot World Challenge 2026</h2>
163
+ <h3>World Model Track - Verification Code</h3>
164
+ <p style="color: #374151;">This email is for submission system verification.</p>
165
+ <p>Your team: <strong>{team_name}</strong></p>
166
+ <div style="background: #f3f4f6; padding: 20px; text-align: center; border-radius: 8px; margin: 20px 0;">
167
+ <span style="font-size: 32px; letter-spacing: 8px; font-weight: bold;">{code}</span>
168
+ </div>
169
+ <p style="color: #6b7280; font-size: 14px;">This code expires in 5 minutes.</p>
170
+ <p style="color: #9ca3af; font-size: 13px;">If you did not request this code, please ignore this email.</p>
171
+ </div>
172
+ """
173
+ resend.Emails.send({
174
+ "from": "AgiBot World Challenge 2026 <onboarding@resend.dev>",
175
+ "to": email,
176
+ "subject": "AgiBot World Challenge 2026 - World Model Track Verification Code",
177
+ "html": html_content
178
+ })
179
+ print(f"Verification code sent to {email}")
180
+ return True
181
+ except Exception as e:
182
+ print(f"Failed to send email: {e}")
183
+ return False
184
+
185
+
186
+ # ============== 数据操作函数 ==============
187
+
188
+ def get_teams() -> dict:
189
+ """获取所有队伍"""
190
+ try:
191
+ return get_storage().download_json("teams.json")
192
+ except:
193
+ return {"teams": {}}
194
+
195
+
196
+ def save_teams(data: dict):
197
+ """保存队伍数据"""
198
+ get_storage().upload_json("teams.json", data)
199
+
200
+
201
+ def get_submissions() -> dict:
202
+ """获取所有提交"""
203
+ try:
204
+ return get_storage().download_json("submissions.json")
205
+ except:
206
+ return {"submissions": []}
207
+
208
+
209
+ def save_submissions(data: dict):
210
+ """保存提交数据"""
211
+ get_storage().upload_json("submissions.json", data)
212
+
213
+
214
+ async def update_submissions(mutator):
215
+ """
216
+ 统一的 submissions.json 更新入口(进程内串行化)。
217
+ mutator 接收 data 并原地修改。
218
+ """
219
+ def _update():
220
+ # 跨进程文件锁,避免 web/worker 同时读改写 submissions.json
221
+ with open(SUBMISSIONS_LOCK_FILE, "w") as lock_f:
222
+ fcntl.flock(lock_f.fileno(), fcntl.LOCK_EX)
223
+ data = get_submissions()
224
+ mutator(data)
225
+ save_submissions(data)
226
+ fcntl.flock(lock_f.fileno(), fcntl.LOCK_UN)
227
+ return data
228
+
229
+ return await asyncio.to_thread(_update)
230
+
231
+
232
+ async def append_submission_record(remote_path: str, email: str, team_name: str) -> int:
233
+ """在锁内生成 submission_id 并追加记录,避免并发重复 id。"""
234
+ def _append():
235
+ with open(SUBMISSIONS_LOCK_FILE, "w") as lock_f:
236
+ fcntl.flock(lock_f.fileno(), fcntl.LOCK_EX)
237
+ data = get_submissions()
238
+ submission_id = max([s.get("id", 0) for s in data.get("submissions", [])], default=0) + 1
239
+ new_submission = {
240
+ "id": submission_id,
241
+ "email": email,
242
+ "team_name": team_name,
243
+ "file_path": remote_path,
244
+ "submit_time": datetime.now().isoformat(),
245
+ "score": None,
246
+ "status": "pending",
247
+ "error_message": None
248
+ }
249
+ data.setdefault("submissions", []).append(new_submission)
250
+ save_submissions(data)
251
+ fcntl.flock(lock_f.fileno(), fcntl.LOCK_UN)
252
+ return submission_id
253
+
254
+ return await asyncio.to_thread(_append)
255
+
256
+
257
+ def get_daily_limits() -> dict:
258
+ """获取每日限制"""
259
+ try:
260
+ return get_storage().download_json("daily_limits.json")
261
+ except:
262
+ return {}
263
+
264
+
265
+ def save_daily_limits(data: dict):
266
+ """保存每日限制"""
267
+ get_storage().upload_json("daily_limits.json", data)
268
+
269
+
270
+ def check_daily_limit(email: str, date: str) -> tuple[bool, int]:
271
+ """检查是否超过每日限制"""
272
+ limits = get_daily_limits()
273
+ current = limits.get(email, {}).get(date, 0)
274
+ return current >= MAX_DAILY_VALIDATIONS, current
275
+
276
+
277
+ def increment_daily_limit(email: str, date: str):
278
+ """增加每日验证计数"""
279
+ limits = get_daily_limits()
280
+ if email not in limits:
281
+ limits[email] = {}
282
+ if date not in limits[email]:
283
+ limits[email][date] = 0
284
+ limits[email][date] += 1
285
+ save_daily_limits(limits)
286
+
287
+
288
+ def upload_submission_to_hf(local_path: Path, timestamp: str) -> str:
289
+ """上传提交文件到 HF dataset"""
290
+ filename = f"submissions/{timestamp}.zip"
291
+ get_storage().upload_file(str(local_path), filename)
292
+ return filename
293
+
294
+
295
+ # ============== 后台验证任务 ==============
296
+
297
+ async def process_submissions():
298
+ """后台任务:处理待验证的提交"""
299
+ print("Background process_submissions started")
300
+ while True:
301
+ try:
302
+ data = await asyncio.to_thread(get_submissions)
303
+ submissions = data.get("submissions", [])
304
+
305
+ # 找出 pending 状态的提交
306
+ pending = [s for s in submissions if s.get("status") == "pending"]
307
+ print(f"Found {len(pending)} pending submissions")
308
+
309
+ if pending:
310
+ # 按时间排序,取最早的
311
+ pending.sort(key=lambda x: x.get("submit_time", ""))
312
+ sub = pending[0]
313
+ print(f"Processing submission {sub.get('id')}")
314
+
315
+ print(f"Starting evaluation for submission {sub.get('id')}")
316
+ # 串行执行:当前任务完成后再处理下一个
317
+ await run_evaluation(sub)
318
+ else:
319
+ await asyncio.sleep(3)
320
+
321
+ except Exception as e:
322
+ print(f"Error in process_submissions: {e}")
323
+ import traceback
324
+ traceback.print_exc()
325
+ await asyncio.sleep(5)
326
+
327
+
328
+ async def run_evaluation(submission: dict):
329
+ """执行单个验证"""
330
+ submission_id = submission.get("id")
331
+ email = submission.get("email")
332
+ team_name = submission.get("team_name")
333
+
334
+ # 串行模式:任务开始即切到 processing
335
+ await update_submissions(
336
+ lambda data: [
337
+ s.update({"status": "processing"})
338
+ for s in data.get("submissions", [])
339
+ if s.get("id") == submission_id
340
+ ]
341
+ )
342
+
343
+ print(f"[EVAL:{submission_id}] status set to processing")
344
+
345
+ try:
346
+ # 调用 metric.py 执行验证
347
+ print(f"Loading metric module for submission {submission_id}...")
348
+ metric = await asyncio.to_thread(load_metric_module)
349
+
350
+ if not metric or not hasattr(metric, 'evaluate_submission'):
351
+ raise RuntimeError("metric.py not found in dataset repo. Please upload metric.py to the dataset.")
352
+
353
+ print(f"[EVAL:{submission_id}] metric loaded, starting evaluate_submission...")
354
+ # 使用真实的 metric.py,返回 (score, error, email, team_name)
355
+ score, error, validated_email, validated_team = await asyncio.get_event_loop().run_in_executor(
356
+ None, metric.evaluate_submission, submission, HF_DATASET_REPO
357
+ )
358
+ print(f"[EVAL:{submission_id}] evaluation complete: score={score}, error={error}, email={validated_email}, team={validated_team}")
359
+
360
+ # === 处理校验失败(邮箱验证未通过)===
361
+ if error and error.startswith("VALIDATION_FAILED:"):
362
+ validation_error = error.replace("VALIDATION_FAILED:", "")
363
+ print(f"Validation failed for submission {submission_id}: {validation_error}")
364
+
365
+ # 根据错误类型设置状态
366
+ # 超过每日上限显示 overlimit,其他显示 error
367
+ if "Daily validation limit exceeded" in validation_error:
368
+ status = "overlimit"
369
+ # overlimit 时保留文件,记录也保留
370
+ else:
371
+ status = "error"
372
+ # 其他错误删除远程上传的文件
373
+ file_path = submission.get("file_path", "")
374
+ if file_path:
375
+ try:
376
+ await asyncio.to_thread(get_storage().delete_file, file_path)
377
+ print(f"Deleted remote file: {file_path}")
378
+ except Exception as del_err:
379
+ print(f"Failed to delete remote file: {del_err}")
380
+
381
+ print(f"[EVAL:{submission_id}] writing validation failure status={status}")
382
+ # 更新状态,显示错误信息,同时更新邮箱和队名
383
+ await update_submissions(
384
+ lambda data: [
385
+ s.update({
386
+ "status": status,
387
+ "error_message": validation_error,
388
+ **({"email": validated_email} if validated_email else {}),
389
+ **({"team_name": validated_team} if validated_team else {}),
390
+ })
391
+ for s in data.get("submissions", [])
392
+ if s.get("id") == submission_id
393
+ ]
394
+ )
395
+ print(f"[EVAL:{submission_id}] writeback done (validation failed)")
396
+ return
397
+ # === 结束 ===
398
+
399
+ print(f"[EVAL:{submission_id}] preparing final writeback")
400
+ # 更新结果
401
+ await update_submissions(
402
+ lambda data: [
403
+ s.update(
404
+ {
405
+ "status": "error" if error else "completed",
406
+ **({"error_message": error} if error else {}),
407
+ **({"score": round(score, 4), "evaluation_time": datetime.now().isoformat()} if not error else {}),
408
+ **({"email": validated_email} if validated_email else {}),
409
+ **({"team_name": validated_team} if validated_team else {}),
410
+ }
411
+ )
412
+ for s in data.get("submissions", [])
413
+ if s.get("id") == submission_id
414
+ ]
415
+ )
416
+ print(f"[EVAL:{submission_id}] final writeback done")
417
+
418
+ # 只有成功才增加每日计数
419
+ if not error:
420
+ date = datetime.now().strftime("%Y-%m-%d")
421
+ print(f"[EVAL:{submission_id}] incrementing daily limit for {validated_email} on {date}")
422
+ await asyncio.to_thread(increment_daily_limit, validated_email, date)
423
+ if error:
424
+ print(f"[EVAL:{submission_id}] completed with error: {error}")
425
+ else:
426
+ print(f"[EVAL:{submission_id}] completed successfully: score={score}")
427
+
428
+ except Exception as e:
429
+ # 更新为 error
430
+ await update_submissions(
431
+ lambda data: [
432
+ s.update({"status": "error", "error_message": str(e)})
433
+ for s in data.get("submissions", [])
434
+ if s.get("id") == submission_id
435
+ ]
436
+ )
437
+ print(f"[EVAL:{submission_id}] exception during evaluation/writeback: {e}")
438
+
439
+
440
+ # ============== 启动事件 ==============
441
+
442
+ @app.on_event("startup")
443
+ async def startup_event():
444
+ """启动时仅做轻量初始化;评测队列由独立 worker 进程处理"""
445
+ if HF_TOKEN:
446
+ await asyncio.to_thread(get_storage)
447
+ print("Web app startup complete (evaluation worker runs in separate process)")
448
+
449
+
450
+ # ============== API 端点 ==============
451
+
452
+ class SendCodeRequest(BaseModel):
453
+ email: str
454
+
455
+
456
+ class VerifyCodeRequest(BaseModel):
457
+ email: str
458
+ code: str
459
+
460
+
461
+ class SubmitResponse(BaseModel):
462
+ submission_id: int
463
+ status: str
464
+ message: str
465
+
466
+
467
+ class StatusResponse(BaseModel):
468
+ submission_id: int
469
+ status: str
470
+ score: Optional[float] = None
471
+ error_message: Optional[str] = None
472
+ submit_time: Optional[str] = None
473
+
474
+
475
+ @app.post("/api/validate-email")
476
+ async def validate_email_api(request: SendCodeRequest):
477
+ """校验邮箱是否在报名名单中(无验证码流程)"""
478
+ email = request.email.strip().lower()
479
+ registration = await asyncio.to_thread(load_registration)
480
+
481
+ if len(registration) == 0:
482
+ return JSONResponse(
483
+ status_code=404,
484
+ content={"detail": "Registration system temporarily unavailable. Please try again later."}
485
+ )
486
+
487
+ if email not in registration:
488
+ return JSONResponse(
489
+ status_code=404,
490
+ content={"detail": "Email not found in registration list. Please check your email address."}
491
+ )
492
+
493
+ return {
494
+ "success": True,
495
+ "message": "Email verified",
496
+ "email": email,
497
+ "team_name": registration[email]
498
+ }
499
+
500
+
501
+ @app.post("/api/send-verification-code")
502
+ async def send_verification_code_api(request: SendCodeRequest):
503
+ """发送验证码到邮箱"""
504
+ email = request.email.strip().lower()
505
+
506
+ # 检查邮箱是否在报名名单中
507
+ registration = await asyncio.to_thread(load_registration)
508
+ if len(registration) == 0:
509
+ return JSONResponse(
510
+ status_code=404,
511
+ content={"detail": "Registration system temporarily unavailable. Please try again later."}
512
+ )
513
+
514
+ if email not in registration:
515
+ return JSONResponse(
516
+ status_code=404,
517
+ content={"detail": "Email not found in registration list. Please check your email address."}
518
+ )
519
+
520
+ team_name = registration[email]
521
+
522
+ # 检查是否已有有效的验证码
523
+ if email in verification_codes:
524
+ code_info = verification_codes[email]
525
+ if code_info["expires_at"] > datetime.now():
526
+ return JSONResponse(
527
+ status_code=400,
528
+ content={"detail": "Verification code already sent. Please check your email or wait for it to expire."}
529
+ )
530
+
531
+ # 生成新验证码
532
+ code = generate_verification_code()
533
+ expires_at = datetime.now() + timedelta(minutes=5)
534
+
535
+ verification_codes[email] = {
536
+ "code": code,
537
+ "expires_at": expires_at,
538
+ "team_name": team_name
539
+ }
540
+
541
+ await asyncio.to_thread(send_verification_code, email, code, team_name)
542
+
543
+ return {
544
+ "success": True,
545
+ "message": "Verification code sent",
546
+ "expires_in": 300
547
+ }
548
+
549
+
550
+ @app.post("/api/verify-code")
551
+ async def verify_code_api(request: VerifyCodeRequest):
552
+ """校验验证码"""
553
+ email = request.email.strip().lower()
554
+ code = request.code.strip()
555
+
556
+ if email not in verification_codes:
557
+ return {
558
+ "success": False,
559
+ "message": "No verification code found. Please request a new code."
560
+ }
561
+
562
+ code_info = verification_codes[email]
563
+
564
+ if code_info["expires_at"] < datetime.now():
565
+ del verification_codes[email]
566
+ return {
567
+ "success": False,
568
+ "message": "Verification code expired. Please request a new code."
569
+ }
570
+
571
+ if code_info["code"] != code:
572
+ return {
573
+ "success": False,
574
+ "message": "Invalid verification code."
575
+ }
576
+
577
+ team_name = code_info["team_name"]
578
+ del verification_codes[email]
579
+
580
+ return {
581
+ "success": True,
582
+ "message": "Verification successful",
583
+ "team_name": team_name
584
+ }
585
+
586
+
587
+ @app.post("/api/submit", response_model=SubmitResponse)
588
+ async def submit_file(file: UploadFile = File(...)):
589
+ """提交 zip 文件进行验证"""
590
+ print(f"Submit request received: {file.filename}")
591
+
592
+ if not HF_TOKEN:
593
+ raise HTTPException(status_code=500, detail="HF_TOKEN not configured")
594
+
595
+ if not file.filename.endswith('.zip'):
596
+ raise HTTPException(status_code=400, detail="Only .zip files are allowed")
597
+
598
+ # 提取邮箱(从表单或从 zip)
599
+ # TODO: 从 zip 的 team_info.json 提取
600
+
601
+ # 保存临时文件
602
+ temp_path = UPLOAD_DIR / f"temp_{uuid.uuid4().hex}.zip"
603
+ try:
604
+ print("Reading file content...")
605
+ content = await file.read()
606
+ with open(temp_path, 'wb') as f:
607
+ f.write(content)
608
+ print(f"File saved to temp: {temp_path}, size: {len(content)} bytes")
609
+
610
+ # 上传到 HF dataset - 用时间戳+随机字符串命名
611
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f") + "_" + uuid.uuid4().hex[:8]
612
+ email = "unknown"
613
+ team_name = "unknown"
614
+
615
+ print(f"Uploading to HF with timestamp: {timestamp}")
616
+ remote_path = await asyncio.to_thread(upload_submission_to_hf, temp_path, timestamp)
617
+ print(f"Uploaded to: {remote_path}")
618
+
619
+ submission_id = await append_submission_record(remote_path, email, team_name)
620
+ print(f"Submissions saved, assigned submission_id: {submission_id}")
621
+
622
+ # 删除临时文件
623
+ temp_path.unlink()
624
+
625
+ return SubmitResponse(
626
+ submission_id=submission_id,
627
+ status="pending",
628
+ message="Submission received and queued for evaluation"
629
+ )
630
+
631
+ except Exception as e:
632
+ import traceback
633
+ print(f"Submit error: {e}")
634
+ traceback.print_exc()
635
+ if temp_path.exists():
636
+ temp_path.unlink()
637
+ raise HTTPException(status_code=500, detail=str(e))
638
+
639
+
640
+ @app.get("/api/status/{submission_id}", response_model=StatusResponse)
641
+ async def get_status(submission_id: int):
642
+ """获取提交状态"""
643
+ data = await asyncio.to_thread(get_submissions)
644
+ for sub in data.get("submissions", []):
645
+ if sub.get("id") == submission_id:
646
+ return StatusResponse(
647
+ submission_id=submission_id,
648
+ status=sub.get("status", "unknown"),
649
+ score=sub.get("score"),
650
+ error_message=sub.get("error_message"),
651
+ submit_time=sub.get("submit_time")
652
+ )
653
+ raise HTTPException(status_code=404, detail="Submission not found")
654
+
655
+
656
+ @app.get("/api/leaderboard")
657
+ async def get_leaderboard_api():
658
+ """获取排行榜"""
659
+ data = await asyncio.to_thread(get_submissions)
660
+ submissions = data.get("submissions", [])
661
+
662
+ # 按队伍分组,取最优成绩
663
+ team_scores = {}
664
+ for sub in submissions:
665
+ if sub.get("status") != "completed":
666
+ continue
667
+ email = sub.get("email")
668
+ team_name = sub.get("team_name")
669
+ score = sub.get("score", 0)
670
+
671
+ if email not in team_scores or score > team_scores[email]["best_score"]:
672
+ team_scores[email] = {
673
+ "team_name": team_name,
674
+ "best_score": score,
675
+ "last_submit_time": sub.get("submit_time")
676
+ }
677
+
678
+ # 转换为列表并排序
679
+ leaderboard = [
680
+ {
681
+ "team_name": v["team_name"],
682
+ "best_score": v["best_score"],
683
+ "last_submit_time": v["last_submit_time"]
684
+ }
685
+ for k, v in team_scores.items()
686
+ ]
687
+ leaderboard.sort(key=lambda x: x["best_score"] or 0, reverse=True)
688
+
689
+ return JSONResponse(content=leaderboard)
690
+
691
+
692
+ @app.get("/api/submissions/{identifier}")
693
+ async def get_team_submissions_api(identifier: str):
694
+ """获取队伍的提交记录,支持按邮箱或队名查询"""
695
+ identifier = identifier.replace("_at_", "@") # URL 编码还原
696
+ identifier = identifier.lower()
697
+ data = await asyncio.to_thread(get_submissions)
698
+ submissions = [
699
+ s for s in data.get("submissions", [])
700
+ if s.get("email", "").lower() == identifier
701
+ or s.get("team_name", "").lower() == identifier
702
+ ]
703
+ return JSONResponse(content=submissions)
704
+
705
+
706
+ # ============== 页面路由 ==============
707
+
708
+ @app.get("/", response_class=HTMLResponse)
709
+ async def index(request: Request):
710
+ """Challenge Description 页面"""
711
+ return templates.TemplateResponse("index.html", {"request": request})
712
+
713
+
714
+ @app.get("/submit", response_class=HTMLResponse)
715
+ async def submit_page(request: Request):
716
+ """Submission 页面"""
717
+ return templates.TemplateResponse("submit.html", {"request": request})
718
+
719
+
720
+ @app.get("/leaderboard", response_class=HTMLResponse)
721
+ async def leaderboard_page(request: Request):
722
+ """Leaderboard 页面"""
723
+ return templates.TemplateResponse("leaderboard.html", {"request": request})
724
+
725
+
726
+ if __name__ == "__main__":
727
+ import uvicorn
728
+ uvicorn.run(app, host="0.0.0.0", port=7860)
daily_limits.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {}
database/__init__.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import aiosqlite
2
+ import os
3
+ from pathlib import Path
4
+
5
+ DATABASE_PATH = Path(__file__).parent.parent / "database" / "submission.db"
6
+ DATABASE_PATH.parent.mkdir(exist_ok=True)
7
+
8
+
9
+ async def get_db():
10
+ """获取数据库连接"""
11
+ db = await aiosqlite.connect(str(DATABASE_PATH))
12
+ db.row_factory = aiosqlite.Row
13
+ return db
14
+
15
+
16
+ async def init_db():
17
+ """初始化数据库"""
18
+ db = await aiosqlite.connect(str(DATABASE_PATH))
19
+ with open(Path(__file__).parent / "schema.sql", "r") as f:
20
+ await db.executescript(f.read())
21
+ await db.commit()
22
+ await db.close()
23
+
24
+
25
+ async def get_or_create_team(db, team_name: str) -> int:
26
+ """获取或创建队伍,返回 team_id"""
27
+ cursor = await db.execute(
28
+ "SELECT id FROM teams WHERE name = ?", (team_name,)
29
+ )
30
+ row = await cursor.fetchone()
31
+ if row:
32
+ return row["id"]
33
+ await db.execute(
34
+ "INSERT INTO teams (name) VALUES (?)", (team_name,)
35
+ )
36
+ await db.commit()
37
+ cursor = await db.execute(
38
+ "SELECT id FROM teams WHERE name = ?", (team_name,)
39
+ )
40
+ row = await cursor.fetchone()
41
+ return row["id"]
42
+
43
+
44
+ async def check_daily_limit(db, team_id: int) -> tuple[bool, int]:
45
+ """
46
+ 检查每日限制,返回 (是否超限, 当日提交数)
47
+ """
48
+ today = date.today().isoformat()
49
+ cursor = await db.execute(
50
+ "SELECT count FROM daily_limits WHERE team_id = ? AND date = ?",
51
+ (team_id, today)
52
+ )
53
+ row = await cursor.fetchone()
54
+ if row:
55
+ return row["count"] >= 1, row["count"]
56
+ return False, 0
57
+
58
+
59
+ async def increment_daily_limit(db, team_id: int):
60
+ """增加当日提交计数"""
61
+ today = date.today().isoformat()
62
+ await db.execute(
63
+ """INSERT INTO daily_limits (team_id, date, count)
64
+ VALUES (?, ?, 1)
65
+ ON CONFLICT(team_id, date) DO UPDATE SET count = count + 1""",
66
+ (team_id, today)
67
+ )
68
+ await db.commit()
69
+
70
+
71
+ async def create_submission(db, team_id: int, file_path: str) -> int:
72
+ """创建提交记录,返回 submission_id"""
73
+ cursor = await db.execute(
74
+ """INSERT INTO submissions (team_id, file_path, status)
75
+ VALUES (?, ?, 'pending')""",
76
+ (team_id, file_path)
77
+ )
78
+ await db.commit()
79
+ return cursor.lastrowid
80
+
81
+
82
+ async def get_submission(submission_id: int):
83
+ """获取单个提交记录"""
84
+ db = await get_db()
85
+ cursor = await db.execute(
86
+ """SELECT s.*, t.name as team_name
87
+ FROM submissions s
88
+ JOIN teams t ON s.team_id = t.id
89
+ WHERE s.id = ?""",
90
+ (submission_id,)
91
+ )
92
+ row = await cursor.fetchone()
93
+ await db.close()
94
+ return dict(row) if row else None
95
+
96
+
97
+ async def update_submission_status(
98
+ submission_id: int,
99
+ status: str,
100
+ score: float = None,
101
+ error_message: str = None
102
+ ):
103
+ """更新提交状态"""
104
+ db = await get_db()
105
+ if score is not None:
106
+ await db.execute(
107
+ "UPDATE submissions SET status = ?, score = ?, error_message = ? WHERE id = ?",
108
+ (status, score, error_message, submission_id)
109
+ )
110
+ else:
111
+ await db.execute(
112
+ "UPDATE submissions SET status = ?, error_message = ? WHERE id = ?",
113
+ (status, error_message, submission_id)
114
+ )
115
+ await db.commit()
116
+ await db.close()
117
+
118
+
119
+ async def get_oldest_pending_submission():
120
+ """获取最老的 pending 提交"""
121
+ db = await get_db()
122
+ cursor = await db.execute(
123
+ """SELECT s.*, t.name as team_name
124
+ FROM submissions s
125
+ JOIN teams t ON s.team_id = t.id
126
+ WHERE s.status = 'pending'
127
+ ORDER BY s.submit_time ASC
128
+ LIMIT 1"""
129
+ )
130
+ row = await cursor.fetchone()
131
+ await db.close()
132
+ return dict(row) if row else None
133
+
134
+
135
+ async def get_team_submissions(team_id: int):
136
+ """获取队伍的所有提交记录"""
137
+ db = await get_db()
138
+ cursor = await db.execute(
139
+ """SELECT s.*, t.name as team_name
140
+ FROM submissions s
141
+ JOIN teams t ON s.team_id = t.id
142
+ WHERE s.team_id = ?
143
+ ORDER BY s.submit_time DESC""",
144
+ (team_id,)
145
+ )
146
+ rows = await cursor.fetchall()
147
+ await db.close()
148
+ return [dict(row) for row in rows]
149
+
150
+
151
+ async def get_leaderboard():
152
+ """获取排行榜数据(每队最优成绩)"""
153
+ db = await get_db()
154
+ cursor = await db.execute(
155
+ """SELECT t.name as team_name,
156
+ MAX(s.score) as best_score,
157
+ MAX(s.submit_time) as last_submit_time
158
+ FROM teams t
159
+ LEFT JOIN submissions s ON t.id = s.team_id AND s.status = 'completed'
160
+ GROUP BY t.id
161
+ ORDER BY best_score DESC NULLS LAST,
162
+ last_submit_time ASC"""
163
+ )
164
+ rows = await cursor.fetchall()
165
+ await db.close()
166
+ return [dict(row) for row in rows]
167
+
168
+
169
+ async def get_team_by_name(team_name: str):
170
+ """根据队名获取队伍信息"""
171
+ db = await get_db()
172
+ cursor = await db.execute(
173
+ "SELECT * FROM teams WHERE name = ?", (team_name,)
174
+ )
175
+ row = await cursor.fetchone()
176
+ await db.close()
177
+ return dict(row) if row else None
178
+
179
+
180
+ from datetime import date
database/schema.sql ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Teams table
2
+ CREATE TABLE IF NOT EXISTS teams (
3
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
4
+ name TEXT UNIQUE NOT NULL,
5
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
6
+ );
7
+
8
+ -- Submissions table
9
+ CREATE TABLE IF NOT EXISTS submissions (
10
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
11
+ team_id INTEGER NOT NULL,
12
+ file_path TEXT NOT NULL,
13
+ submit_time DATETIME DEFAULT CURRENT_TIMESTAMP,
14
+ score REAL,
15
+ status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'processing', 'completed', 'error', 'overlimit')),
16
+ error_message TEXT,
17
+ FOREIGN KEY (team_id) REFERENCES teams(id)
18
+ );
19
+
20
+ -- Daily limits tracking
21
+ CREATE TABLE IF NOT EXISTS daily_limits (
22
+ team_id INTEGER NOT NULL,
23
+ date DATE NOT NULL,
24
+ count INTEGER DEFAULT 0,
25
+ PRIMARY KEY (team_id, date),
26
+ FOREIGN KEY (team_id) REFERENCES teams(id)
27
+ );
28
+
29
+ -- Indexes for faster queries
30
+ CREATE INDEX IF NOT EXISTS idx_submissions_team_id ON submissions(team_id);
31
+ CREATE INDEX IF NOT EXISTS idx_submissions_status ON submissions(status);
32
+ CREATE INDEX IF NOT EXISTS idx_submissions_submit_time ON submissions(submit_time);
33
+ CREATE INDEX IF NOT EXISTS idx_daily_limits_team_date ON daily_limits(team_id, date);
dataset_storage.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Dataset Storage Module
3
+
4
+ 用于操作 Hugging Face Dataset Repo 中的 JSON 文件。
5
+ """
6
+ import json
7
+ import os
8
+ import tempfile
9
+ from pathlib import Path
10
+ from typing import Optional, Dict, Any
11
+
12
+ try:
13
+ from huggingface_hub import hf_hub_download, HfApi
14
+ except ImportError:
15
+ hf_hub_download = None
16
+ HfApi = None
17
+
18
+
19
+ class DatasetStorage:
20
+ """HF Dataset 存储操作类"""
21
+
22
+ def __init__(self, repo_id: str, token: str):
23
+ self.repo_id = repo_id
24
+ self.token = token
25
+ if token:
26
+ self.api = HfApi(token=token)
27
+ else:
28
+ self.api = None
29
+
30
+ def download_json(self, filename: str) -> Dict[str, Any]:
31
+ """下载 JSON 文件"""
32
+ if not hf_hub_download or not self.token:
33
+ raise ValueError("HF_TOKEN not configured")
34
+
35
+ try:
36
+ local_path = hf_hub_download(
37
+ repo_id=self.repo_id,
38
+ filename=filename,
39
+ token=self.token,
40
+ repo_type="dataset"
41
+ )
42
+ with open(local_path, 'r', encoding='utf-8') as f:
43
+ return json.load(f)
44
+ except Exception as e:
45
+ print(f"Failed to download {filename}: {e}")
46
+ # 返回默认空结构
47
+ return self._get_default_structure(filename)
48
+
49
+ def _get_default_structure(self, filename: str) -> Dict:
50
+ """返回默认空结构"""
51
+ if filename == "teams.json":
52
+ return {"teams": {}}
53
+ elif filename == "submissions.json":
54
+ return {"submissions": []}
55
+ elif filename == "daily_limits.json":
56
+ return {}
57
+ return {}
58
+
59
+ def upload_json(self, filename: str, data: Dict[str, Any]):
60
+ """上传 JSON 文件"""
61
+ if not self.api or not self.token:
62
+ raise ValueError("HF_TOKEN not configured")
63
+
64
+ # 创建临时文件
65
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False, encoding='utf-8') as f:
66
+ json.dump(data, f, indent=2, ensure_ascii=False)
67
+ temp_path = f.name
68
+
69
+ try:
70
+ self.api.upload_file(
71
+ path_or_fileobj=temp_path,
72
+ path_in_repo=filename,
73
+ repo_id=self.repo_id,
74
+ repo_type="dataset"
75
+ )
76
+ finally:
77
+ if os.path.exists(temp_path):
78
+ os.unlink(temp_path)
79
+
80
+ def upload_file(self, local_path: str, remote_path: str):
81
+ """上传任意文件"""
82
+ if not self.api or not self.token:
83
+ raise ValueError("HF_TOKEN not configured")
84
+
85
+ self.api.upload_file(
86
+ path_or_fileobj=local_path,
87
+ path_in_repo=remote_path,
88
+ repo_id=self.repo_id,
89
+ repo_type="dataset"
90
+ )
91
+
92
+ def delete_file(self, path_in_repo: str):
93
+ """删除 dataset repo 中的文件"""
94
+ if not self.api or not self.token:
95
+ raise ValueError("HF_TOKEN not configured")
96
+
97
+ self.api.delete_file(
98
+ path_in_repo,
99
+ repo_id=self.repo_id,
100
+ repo_type="dataset"
101
+ )
102
+
103
+
104
+ # 全局存储实例(需要初始化)
105
+ storage: Optional[DatasetStorage] = None
106
+
107
+
108
+ def init_storage(repo_id: str, token: str):
109
+ """初始化存储实例"""
110
+ global storage
111
+ storage = DatasetStorage(repo_id, token)
112
+ return storage
registration.csv ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # ICRA26 Workshop Registration
2
+ # Format: email,team_name
3
+ # 每行: 邮箱,队伍名
4
+ # 请在此文件中添加参赛队伍的邮箱和队名映射
5
+
6
+ # 示例(请替换为真实数据):
7
+ test@example.com,TestTeamAlpha
8
+ demo@icra.org,RoboChampions
9
+ participant2026@workshop.org,NeuralNinjas
requirements.txt ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ jinja2
4
+ python-multipart
5
+ aiofiles
6
+ aiosqlite
7
+ sqlalchemy
8
+ resend
9
+ huggingface_hub
10
+ numpy
11
+
12
+ # For evaluation metrics
13
+ torch
14
+ torchvision
15
+ opencv-python-headless
16
+ pillow
17
+ omegaconf
18
+ scikit-image
19
+ scipy
20
+ ultralytics
21
+ fastdtw
22
+ lap
scoring/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Scoring module exports
scoring/scorer.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 评分模块
3
+
4
+ 用于评估参赛队伍提交的 zip 文件中的结果。
5
+ 当前为 Mock 实现,待后续替换为真实评分逻辑。
6
+ """
7
+ import json
8
+ import zipfile
9
+ import random
10
+ import time
11
+ from pathlib import Path
12
+ from dataclasses import dataclass
13
+ from typing import Optional
14
+
15
+
16
+ @dataclass
17
+ class ScoreResult:
18
+ """评分结果"""
19
+ score: float
20
+ error: Optional[str] = None
21
+
22
+
23
+ def validate_zip_contents(zip_path: str) -> tuple[bool, Optional[str]]:
24
+ """
25
+ 验证 zip 文件内容是否合法
26
+ 返回 (是否合法, 错误信息)
27
+ """
28
+ try:
29
+ with zipfile.ZipFile(zip_path, 'r') as zf:
30
+ # 检查必要的文件
31
+ names = zf.namelist()
32
+ # 检查是否存在 team_info.json
33
+ if 'team_info.json' not in names:
34
+ return False, "Missing team_info.json in zip root"
35
+
36
+ # 读取并验证 team_info.json
37
+ try:
38
+ info = json.loads(zf.read('team_info.json').decode('utf-8'))
39
+ if 'team_name' not in info:
40
+ return False, "team_info.json must contain 'team_name' field"
41
+ except json.JSONDecodeError as e:
42
+ return False, f"Invalid team_info.json format: {e}"
43
+
44
+ return True, None
45
+ except zipfile.BadZipFile:
46
+ return False, "Invalid zip file"
47
+ except Exception as e:
48
+ return False, f"Unexpected error: {e}"
49
+
50
+
51
+ def extract_team_name(zip_path: str) -> Optional[str]:
52
+ """
53
+ 从 zip 文件中提取队伍名
54
+ """
55
+ try:
56
+ with zipfile.ZipFile(zip_path, 'r') as zf:
57
+ info = json.loads(zf.read('team_info.json').decode('utf-8'))
58
+ return info.get('team_name')
59
+ except Exception:
60
+ return None
61
+
62
+
63
+ def score_submission(zip_path: str, team_name: str) -> ScoreResult:
64
+ """
65
+ 评分函数
66
+
67
+ Args:
68
+ zip_path: zip 文件路径
69
+ team_name: 队伍名(从 zip 中提取)
70
+
71
+ Returns:
72
+ ScoreResult: 包含分数或错误信息
73
+ """
74
+ # 1. 验证 zip 内容
75
+ is_valid, error = validate_zip_contents(zip_path)
76
+ if not is_valid:
77
+ return ScoreResult(score=0.0, error=error)
78
+
79
+ # 2. Mock 评分逻辑
80
+ # TODO: 替换为真实评分逻辑
81
+ #
82
+ # 真实评分可能包含:
83
+ # - 解压 zip 文件
84
+ # - 运行验证脚本
85
+ # - 计算指标(如 Accuracy, F1, mAP 等)
86
+ # - 考虑推理时间等因素
87
+
88
+ # 模拟耗时操作(实际评分可能需要更长时间)
89
+ # time.sleep(2) # 取消注释用于测试等待效果
90
+
91
+ # Mock: 随机生成 0-100 之间的分数
92
+ score = random.uniform(0, 100)
93
+
94
+ # 也可以根据队伍名生成确定性的分数(用于测试)
95
+ # hash_value = hash(team_name) % 101
96
+ # score = float(hash_value)
97
+
98
+ return ScoreResult(score=round(score, 4))
99
+
100
+
101
+ def evaluate_submission(zip_path: str) -> tuple[float, Optional[str]]:
102
+ """
103
+ 评估提交并返回分数和错误信息
104
+
105
+ 这是主要调用的接口
106
+ """
107
+ team_name = extract_team_name(zip_path)
108
+ if team_name is None:
109
+ return 0.0, "Could not extract team name from zip"
110
+
111
+ result = score_submission(zip_path, team_name)
112
+ return result.score, result.error
static/css/style.css ADDED
@@ -0,0 +1,595 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Base Styles */
2
+ :root {
3
+ --primary-color: #6366f1;
4
+ --primary-hover: #4f46e5;
5
+ --secondary-color: #64748b;
6
+ --success-color: #22c55e;
7
+ --warning-color: #f59e0b;
8
+ --error-color: #ef4444;
9
+ --bg-color: #f8fafc;
10
+ --card-bg: #ffffff;
11
+ --sidebar-bg: #1e293b;
12
+ --sidebar-text: #e2e8f0;
13
+ --text-color: #1e293b;
14
+ --text-muted: #64748b;
15
+ --border-color: #e2e8f0;
16
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
17
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
18
+ --radius-sm: 6px;
19
+ --radius-md: 8px;
20
+ --radius-lg: 12px;
21
+ }
22
+
23
+ * {
24
+ margin: 0;
25
+ padding: 0;
26
+ box-sizing: border-box;
27
+ }
28
+
29
+ body {
30
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
31
+ background: var(--bg-color);
32
+ color: var(--text-color);
33
+ line-height: 1.6;
34
+ }
35
+
36
+ /* Layout */
37
+ .container {
38
+ display: flex;
39
+ min-height: 100vh;
40
+ }
41
+
42
+ /* Sidebar */
43
+ .sidebar {
44
+ width: 280px;
45
+ background: var(--sidebar-bg);
46
+ color: var(--sidebar-text);
47
+ display: flex;
48
+ flex-direction: column;
49
+ position: fixed;
50
+ height: 100vh;
51
+ z-index: 100;
52
+ }
53
+
54
+ .sidebar-header {
55
+ padding: 24px;
56
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
57
+ }
58
+
59
+ .sidebar-header h1 {
60
+ font-size: 1.5rem;
61
+ font-weight: 700;
62
+ margin-bottom: 4px;
63
+ }
64
+
65
+ .sidebar-header .subtitle {
66
+ font-size: 0.85rem;
67
+ color: var(--secondary-color);
68
+ }
69
+
70
+ .sidebar-nav {
71
+ flex: 1;
72
+ padding: 16px 12px;
73
+ }
74
+
75
+ .nav-item {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 12px;
79
+ padding: 12px 16px;
80
+ color: var(--sidebar-text);
81
+ text-decoration: none;
82
+ border-radius: var(--radius-md);
83
+ margin-bottom: 4px;
84
+ transition: all 0.2s ease;
85
+ }
86
+
87
+ .nav-item:hover {
88
+ background: rgba(255, 255, 255, 0.1);
89
+ }
90
+
91
+ .nav-item.active {
92
+ background: var(--primary-color);
93
+ color: white;
94
+ }
95
+
96
+ .nav-icon {
97
+ font-size: 1.2rem;
98
+ }
99
+
100
+ .sidebar-footer {
101
+ padding: 16px 24px;
102
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
103
+ font-size: 0.85rem;
104
+ color: var(--secondary-color);
105
+ }
106
+
107
+ /* Main Content */
108
+ .main-content {
109
+ flex: 1;
110
+ margin-left: 280px;
111
+ padding: 32px;
112
+ max-width: 1200px;
113
+ }
114
+
115
+ /* Page Header */
116
+ .page-header {
117
+ margin-bottom: 32px;
118
+ }
119
+
120
+ .page-header h1 {
121
+ font-size: 2rem;
122
+ font-weight: 700;
123
+ margin-bottom: 8px;
124
+ }
125
+
126
+ .page-subtitle {
127
+ color: var(--text-muted);
128
+ }
129
+
130
+ /* Content Card */
131
+ .content-card {
132
+ background: var(--card-bg);
133
+ border-radius: var(--radius-lg);
134
+ padding: 24px;
135
+ margin-bottom: 24px;
136
+ box-shadow: var(--shadow-sm);
137
+ border: 1px solid var(--border-color);
138
+ }
139
+
140
+ .content-card h2 {
141
+ font-size: 1.25rem;
142
+ font-weight: 600;
143
+ margin-bottom: 16px;
144
+ color: var(--text-color);
145
+ }
146
+
147
+ .content-card h3 {
148
+ font-size: 1rem;
149
+ font-weight: 600;
150
+ margin: 24px 0 12px;
151
+ }
152
+
153
+ .content-card p {
154
+ margin-bottom: 12px;
155
+ color: var(--text-color);
156
+ }
157
+
158
+ /* Buttons */
159
+ .btn {
160
+ display: inline-flex;
161
+ align-items: center;
162
+ justify-content: center;
163
+ padding: 10px 20px;
164
+ border-radius: var(--radius-md);
165
+ font-size: 0.9rem;
166
+ font-weight: 500;
167
+ cursor: pointer;
168
+ border: none;
169
+ transition: all 0.2s ease;
170
+ text-decoration: none;
171
+ }
172
+
173
+ .btn-primary {
174
+ background: var(--primary-color);
175
+ color: white;
176
+ }
177
+
178
+ .btn-primary:hover {
179
+ background: var(--primary-hover);
180
+ }
181
+
182
+ .btn-secondary {
183
+ background: var(--border-color);
184
+ color: var(--text-color);
185
+ }
186
+
187
+ .btn-secondary:hover {
188
+ background: #cbd5e1;
189
+ }
190
+
191
+ .btn-large {
192
+ padding: 14px 32px;
193
+ font-size: 1rem;
194
+ }
195
+
196
+ .btn:disabled {
197
+ opacity: 0.5;
198
+ cursor: not-allowed;
199
+ }
200
+
201
+ /* Upload Area */
202
+ .upload-area {
203
+ border: 2px dashed var(--border-color);
204
+ border-radius: var(--radius-lg);
205
+ padding: 48px;
206
+ text-align: center;
207
+ transition: all 0.2s ease;
208
+ cursor: pointer;
209
+ margin-bottom: 24px;
210
+ }
211
+
212
+ .upload-area:hover,
213
+ .upload-area.dragover {
214
+ border-color: var(--primary-color);
215
+ background: rgba(99, 102, 241, 0.05);
216
+ }
217
+
218
+ .upload-icon {
219
+ font-size: 3rem;
220
+ margin-bottom: 16px;
221
+ }
222
+
223
+ .upload-hint {
224
+ color: var(--text-muted);
225
+ margin: 12px 0;
226
+ }
227
+
228
+ .file-name {
229
+ margin-top: 16px;
230
+ font-weight: 500;
231
+ color: var(--primary-color);
232
+ }
233
+
234
+ /* Progress Bar */
235
+ .upload-progress {
236
+ margin-bottom: 24px;
237
+ }
238
+
239
+ .progress-bar {
240
+ height: 8px;
241
+ background: var(--border-color);
242
+ border-radius: 4px;
243
+ overflow: hidden;
244
+ margin-bottom: 8px;
245
+ }
246
+
247
+ .progress-fill {
248
+ height: 100%;
249
+ background: var(--primary-color);
250
+ width: 0%;
251
+ transition: width 0.3s ease;
252
+ }
253
+
254
+ /* Submit Result */
255
+ .submit-result {
256
+ padding: 16px;
257
+ border-radius: var(--radius-md);
258
+ margin-bottom: 24px;
259
+ display: flex;
260
+ align-items: center;
261
+ gap: 12px;
262
+ }
263
+
264
+ .submit-result.success {
265
+ background: rgba(34, 197, 94, 0.1);
266
+ color: var(--success-color);
267
+ border: 1px solid rgba(34, 197, 94, 0.3);
268
+ }
269
+
270
+ .submit-result.error {
271
+ background: rgba(239, 68, 68, 0.1);
272
+ color: var(--error-color);
273
+ border: 1px solid rgba(239, 68, 68, 0.3);
274
+ }
275
+
276
+ .submit-result.warning {
277
+ background: rgba(245, 158, 11, 0.1);
278
+ color: var(--warning-color);
279
+ border: 1px solid rgba(245, 158, 11, 0.3);
280
+ }
281
+
282
+ .result-icon {
283
+ font-size: 1.25rem;
284
+ }
285
+
286
+ /* Form */
287
+ .form-group {
288
+ margin-bottom: 20px;
289
+ }
290
+
291
+ .form-group label {
292
+ display: block;
293
+ font-weight: 500;
294
+ margin-bottom: 8px;
295
+ }
296
+
297
+ .form-group input[type="text"] {
298
+ width: 100%;
299
+ padding: 12px;
300
+ border: 1px solid var(--border-color);
301
+ border-radius: var(--radius-md);
302
+ font-size: 1rem;
303
+ }
304
+
305
+ .form-hint {
306
+ font-size: 0.85rem;
307
+ color: var(--text-muted);
308
+ margin-top: 8px;
309
+ }
310
+
311
+ /* Tables */
312
+ .submissions-table-container {
313
+ overflow-x: auto;
314
+ }
315
+
316
+ .submissions-table,
317
+ .leaderboard-table {
318
+ width: 100%;
319
+ border-collapse: collapse;
320
+ }
321
+
322
+ .submissions-table th,
323
+ .submissions-table td,
324
+ .leaderboard-table th,
325
+ .leaderboard-table td {
326
+ padding: 12px 16px;
327
+ text-align: left;
328
+ border-bottom: 1px solid var(--border-color);
329
+ }
330
+
331
+ .submissions-table th,
332
+ .leaderboard-table th {
333
+ font-weight: 600;
334
+ color: var(--text-muted);
335
+ font-size: 0.85rem;
336
+ text-transform: uppercase;
337
+ letter-spacing: 0.05em;
338
+ }
339
+
340
+ .submissions-table tbody tr:hover,
341
+ .leaderboard-table tbody tr:hover {
342
+ background: rgba(99, 102, 241, 0.02);
343
+ }
344
+
345
+ .empty-message {
346
+ text-align: center;
347
+ color: var(--text-muted);
348
+ padding: 32px;
349
+ }
350
+
351
+ /* Status Badges */
352
+ .status-badge {
353
+ display: inline-block;
354
+ padding: 4px 12px;
355
+ border-radius: 20px;
356
+ font-size: 0.8rem;
357
+ font-weight: 500;
358
+ }
359
+
360
+ .status-pending {
361
+ background: rgba(100, 116, 139, 0.15);
362
+ color: var(--secondary-color);
363
+ }
364
+
365
+ .status-processing {
366
+ background: rgba(99, 102, 241, 0.15);
367
+ color: var(--primary-color);
368
+ }
369
+
370
+ .status-completed {
371
+ background: rgba(34, 197, 94, 0.15);
372
+ color: var(--success-color);
373
+ }
374
+
375
+ .status-error {
376
+ background: rgba(239, 68, 68, 0.15);
377
+ color: var(--error-color);
378
+ }
379
+
380
+ .status-overlimit {
381
+ background: rgba(245, 158, 11, 0.15);
382
+ color: var(--warning-color);
383
+ }
384
+
385
+ /* Leaderboard Specific */
386
+ .leaderboard-table .rank-col {
387
+ width: 80px;
388
+ text-align: center;
389
+ }
390
+
391
+ .leaderboard-table .team-col {
392
+ text-align: left;
393
+ }
394
+
395
+ .leaderboard-table .score-col {
396
+ width: 150px;
397
+ text-align: right;
398
+ }
399
+
400
+ .leaderboard-table .time-col {
401
+ width: 200px;
402
+ text-align: right;
403
+ }
404
+
405
+ .rank-badge {
406
+ display: inline-flex;
407
+ align-items: center;
408
+ justify-content: center;
409
+ width: 36px;
410
+ height: 36px;
411
+ border-radius: 50%;
412
+ font-weight: 700;
413
+ font-size: 1rem;
414
+ background: var(--border-color);
415
+ color: var(--text-muted);
416
+ }
417
+
418
+ .rank-gold {
419
+ background: linear-gradient(135deg, #ffd700, #ffb700);
420
+ color: #7a5c00;
421
+ }
422
+
423
+ .rank-silver {
424
+ background: linear-gradient(135deg, #e0e0e0, #bdbdbd);
425
+ color: #616161;
426
+ }
427
+
428
+ .rank-bronze {
429
+ background: linear-gradient(135deg, #cd7f32, #a0522d);
430
+ color: white;
431
+ }
432
+
433
+ .score-cell .score-value {
434
+ font-weight: 600;
435
+ font-size: 1.1rem;
436
+ color: var(--primary-color);
437
+ }
438
+
439
+ .rank-gold .score-value {
440
+ color: #7a5c00;
441
+ }
442
+
443
+ /* Loading State */
444
+ .loading-cell {
445
+ text-align: center;
446
+ padding: 32px;
447
+ }
448
+
449
+ .loading-spinner {
450
+ display: inline-block;
451
+ width: 20px;
452
+ height: 20px;
453
+ border: 2px solid var(--border-color);
454
+ border-top-color: var(--primary-color);
455
+ border-radius: 50%;
456
+ animation: spin 0.8s linear infinite;
457
+ margin-right: 8px;
458
+ }
459
+
460
+ @keyframes spin {
461
+ to { transform: rotate(360deg); }
462
+ }
463
+
464
+ /* Empty State */
465
+ .empty-state {
466
+ text-align: center;
467
+ padding: 48px 24px;
468
+ }
469
+
470
+ .empty-icon {
471
+ font-size: 4rem;
472
+ margin-bottom: 16px;
473
+ }
474
+
475
+ .empty-state h3 {
476
+ margin-bottom: 8px;
477
+ }
478
+
479
+ .empty-state p {
480
+ color: var(--text-muted);
481
+ margin-bottom: 24px;
482
+ }
483
+
484
+ /* Code Blocks */
485
+ pre {
486
+ background: #1e293b;
487
+ color: #e2e8f0;
488
+ padding: 16px;
489
+ border-radius: var(--radius-md);
490
+ overflow-x: auto;
491
+ font-size: 0.9rem;
492
+ line-height: 1.5;
493
+ }
494
+
495
+ code {
496
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
497
+ }
498
+
499
+ p code,
500
+ li code {
501
+ background: rgba(99, 102, 241, 0.1);
502
+ padding: 2px 6px;
503
+ border-radius: 4px;
504
+ font-size: 0.85rem;
505
+ color: var(--primary-color);
506
+ }
507
+
508
+ /* Rules List */
509
+ .rules-list {
510
+ list-style: none;
511
+ padding: 0;
512
+ }
513
+
514
+ .rules-list li {
515
+ padding: 8px 0;
516
+ padding-left: 24px;
517
+ position: relative;
518
+ }
519
+
520
+ .rules-list li::before {
521
+ content: "✓";
522
+ position: absolute;
523
+ left: 0;
524
+ color: var(--success-color);
525
+ }
526
+
527
+ /* Info Box */
528
+ .info-box {
529
+ background: rgba(99, 102, 241, 0.08);
530
+ border-left: 4px solid var(--primary-color);
531
+ padding: 16px;
532
+ border-radius: 0 var(--radius-md) var(--radius-md) 0;
533
+ margin: 24px 0;
534
+ }
535
+
536
+ .info-box h4 {
537
+ margin-bottom: 8px;
538
+ color: var(--primary-color);
539
+ }
540
+
541
+ /* Timeline Table */
542
+ .timeline-table {
543
+ width: 100%;
544
+ border-collapse: collapse;
545
+ }
546
+
547
+ .timeline-table td {
548
+ padding: 12px 0;
549
+ border-bottom: 1px solid var(--border-color);
550
+ }
551
+
552
+ .timeline-table td:first-child {
553
+ font-weight: 500;
554
+ width: 200px;
555
+ }
556
+
557
+ /* CTA Section */
558
+ .cta-section {
559
+ text-align: center;
560
+ padding: 32px 0;
561
+ border-top: 1px solid var(--border-color);
562
+ margin-top: 32px;
563
+ }
564
+
565
+ .cta-section p {
566
+ color: var(--text-muted);
567
+ margin-bottom: 16px;
568
+ }
569
+
570
+ /* Hidden */
571
+ .hidden {
572
+ display: none !important;
573
+ }
574
+
575
+ /* Responsive */
576
+ @media (max-width: 768px) {
577
+ .sidebar {
578
+ width: 100%;
579
+ height: auto;
580
+ position: relative;
581
+ }
582
+
583
+ .main-content {
584
+ margin-left: 0;
585
+ padding: 16px;
586
+ }
587
+
588
+ .container {
589
+ flex-direction: column;
590
+ }
591
+
592
+ .upload-area {
593
+ padding: 32px 16px;
594
+ }
595
+ }
static/js/main.js ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * ICRA26 Workshop - Main JavaScript
3
+ * Shared utilities for all pages
4
+ */
5
+
6
+ // Common utility functions
7
+ const Utils = {
8
+ /**
9
+ * Format bytes to human readable string
10
+ */
11
+ formatBytes(bytes) {
12
+ if (bytes < 1024) return bytes + ' B';
13
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
14
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
15
+ },
16
+
17
+ /**
18
+ * Format date to local string
19
+ */
20
+ formatDate(dateString) {
21
+ return new Date(dateString).toLocaleString();
22
+ },
23
+
24
+ /**
25
+ * Escape HTML to prevent XSS
26
+ */
27
+ escapeHtml(text) {
28
+ const div = document.createElement('div');
29
+ div.textContent = text;
30
+ return div.innerHTML;
31
+ },
32
+
33
+ /**
34
+ * Show notification message
35
+ */
36
+ showNotification(message, type = 'info', duration = 5000) {
37
+ const container = document.getElementById('notification-container');
38
+ if (!container) return;
39
+
40
+ const notification = document.createElement('div');
41
+ notification.className = `notification notification-${type}`;
42
+ notification.innerHTML = `
43
+ <span class="notification-message">${message}</span>
44
+ <button class="notification-close">&times;</button>
45
+ `;
46
+
47
+ container.appendChild(notification);
48
+
49
+ // Close button handler
50
+ notification.querySelector('.notification-close').addEventListener('click', () => {
51
+ notification.remove();
52
+ });
53
+
54
+ // Auto-remove after duration
55
+ if (duration > 0) {
56
+ setTimeout(() => notification.remove(), duration);
57
+ }
58
+ },
59
+
60
+ /**
61
+ * Debounce function
62
+ */
63
+ debounce(func, wait) {
64
+ let timeout;
65
+ return function executedFunction(...args) {
66
+ const later = () => {
67
+ clearTimeout(timeout);
68
+ func(...args);
69
+ };
70
+ clearTimeout(timeout);
71
+ timeout = setTimeout(later, wait);
72
+ };
73
+ },
74
+
75
+ /**
76
+ * Check if element is in viewport
77
+ */
78
+ isInViewport(element) {
79
+ const rect = element.getBoundingClientRect();
80
+ return (
81
+ rect.top >= 0 &&
82
+ rect.left >= 0 &&
83
+ rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
84
+ rect.right <= (window.innerWidth || document.documentElement.clientWidth)
85
+ );
86
+ }
87
+ };
88
+
89
+ // API helper
90
+ const API = {
91
+ async get(url) {
92
+ const response = await fetch(url);
93
+ if (!response.ok) {
94
+ throw new Error(`HTTP error! status: ${response.status}`);
95
+ }
96
+ return response.json();
97
+ },
98
+
99
+ async post(url, data) {
100
+ const response = await fetch(url, {
101
+ method: 'POST',
102
+ headers: {
103
+ 'Content-Type': 'application/json',
104
+ },
105
+ body: JSON.stringify(data),
106
+ });
107
+ if (!response.ok) {
108
+ const error = await response.json().catch(() => ({ detail: 'Request failed' }));
109
+ throw new Error(error.detail || 'Request failed');
110
+ }
111
+ return response.json();
112
+ },
113
+
114
+ async upload(url, file, onProgress) {
115
+ return new Promise((resolve, reject) => {
116
+ const xhr = new XMLHttpRequest();
117
+ xhr.open('POST', url);
118
+
119
+ xhr.upload.addEventListener('progress', (e) => {
120
+ if (e.lengthComputable && onProgress) {
121
+ onProgress((e.loaded / e.total) * 100);
122
+ }
123
+ });
124
+
125
+ xhr.addEventListener('load', () => {
126
+ if (xhr.status >= 200 && xhr.status < 300) {
127
+ resolve(JSON.parse(xhr.responseText));
128
+ } else {
129
+ try {
130
+ const error = JSON.parse(xhr.responseText);
131
+ reject(new Error(error.detail || 'Upload failed'));
132
+ } catch {
133
+ reject(new Error('Upload failed'));
134
+ }
135
+ }
136
+ });
137
+
138
+ xhr.addEventListener('error', () => reject(new Error('Network error')));
139
+
140
+ const formData = new FormData();
141
+ formData.append('file', file);
142
+ xhr.send(formData);
143
+ });
144
+ }
145
+ };
146
+
147
+ // Auto-refresh helper for leaderboard
148
+ class AutoRefresh {
149
+ constructor(fetchFn, interval = 30000) {
150
+ this.fetchFn = fetchFn;
151
+ this.interval = interval;
152
+ this.isActive = true;
153
+ }
154
+
155
+ start() {
156
+ this.isActive = true;
157
+ this.fetchFn();
158
+ this.timer = setInterval(() => {
159
+ if (this.isActive) {
160
+ this.fetchFn();
161
+ }
162
+ }, this.interval);
163
+ }
164
+
165
+ stop() {
166
+ this.isActive = false;
167
+ if (this.timer) {
168
+ clearInterval(this.timer);
169
+ }
170
+ }
171
+
172
+ refresh() {
173
+ if (this.isActive) {
174
+ this.fetchFn();
175
+ }
176
+ }
177
+ }
178
+
179
+ // Status polling helper
180
+ class StatusPoller {
181
+ constructor(submissionId, onUpdate, onComplete, interval = 3000) {
182
+ this.submissionId = submissionId;
183
+ this.onUpdate = onUpdate;
184
+ this.onComplete = onComplete;
185
+ this.interval = interval;
186
+ this.timer = null;
187
+ }
188
+
189
+ start() {
190
+ const poll = async () => {
191
+ try {
192
+ const response = await fetch(`/api/status/${this.submissionId}`);
193
+ const data = await response.json();
194
+
195
+ this.onUpdate(data);
196
+
197
+ if (data.status === 'completed' || data.status === 'error') {
198
+ this.stop();
199
+ if (this.onComplete) {
200
+ this.onComplete(data);
201
+ }
202
+ } else {
203
+ this.timer = setTimeout(poll, this.interval);
204
+ }
205
+ } catch (error) {
206
+ console.error('Polling error:', error);
207
+ this.timer = setTimeout(poll, this.interval);
208
+ }
209
+ };
210
+
211
+ poll();
212
+ }
213
+
214
+ stop() {
215
+ if (this.timer) {
216
+ clearTimeout(this.timer);
217
+ this.timer = null;
218
+ }
219
+ }
220
+ }
221
+
222
+ // Export for use in inline scripts
223
+ window.Utils = Utils;
224
+ window.API = API;
225
+ window.AutoRefresh = AutoRefresh;
226
+ window.StatusPoller = StatusPoller;
submissions.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "submissions": []
3
+ }
teams.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "teams": {}
3
+ }
templates/base.html ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{% block title %}ICRA26 Workshop{% endblock %}</title>
7
+ <link rel="stylesheet" href="/static/css/style.css">
8
+ </head>
9
+ <body>
10
+ <div class="container">
11
+ <!-- Sidebar -->
12
+ <aside class="sidebar">
13
+ <div class="sidebar-header">
14
+ <h1>AgiBot World Challenge 2026</h1>
15
+ <p class="subtitle">World Model Track Test Server</p>
16
+ </div>
17
+ <nav class="sidebar-nav">
18
+ <a href="/" class="nav-item {% if request.url.path == '/' %}active{% endif %}">
19
+ <span class="nav-icon">📋</span>
20
+ Challenge Description
21
+ </a>
22
+ <a href="/submit" class="nav-item {% if request.url.path == '/submit' %}active{% endif %}">
23
+ <span class="nav-icon">📤</span>
24
+ Submission
25
+ </a>
26
+ <a href="/leaderboard" class="nav-item {% if request.url.path == '/leaderboard' %}active{% endif %}">
27
+ <span class="nav-icon">🏆</span>
28
+ Leaderboard
29
+ </a>
30
+ </nav>
31
+ <div class="sidebar-footer">
32
+ <p>ICRA 2026</p>
33
+ </div>
34
+ </aside>
35
+
36
+ <!-- Main Content -->
37
+ <main class="main-content">
38
+ {% block content %}{% endblock %}
39
+ </main>
40
+ </div>
41
+
42
+ <script src="/static/js/main.js"></script>
43
+ {% block scripts %}{% endblock %}
44
+ </body>
45
+ </html>
templates/index.html ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Challenge Description - ICRA26 Workshop{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="page-header">
7
+ <h1>Challenge Description</h1>
8
+ </div>
9
+
10
+ <div class="content-card">
11
+ <style>
12
+ .content-card pre,
13
+ .content-card pre code {
14
+ color: #ffffff;
15
+ }
16
+ .content-card pre code {
17
+ background: transparent !important;
18
+ padding: 0;
19
+ }
20
+
21
+ /* Normalize ordered-list indentation in this page */
22
+ .content-card ol {
23
+ margin: 0 0 0 1.4rem;
24
+ padding-left: 0;
25
+ }
26
+
27
+ .content-card ol > li {
28
+ margin-bottom: 12px;
29
+ padding-left: 0.2rem;
30
+ }
31
+
32
+ .content-card ol > li > p {
33
+ margin: 8px 0 0;
34
+ }
35
+ </style>
36
+
37
+ <h2>AgiBot World Challenge ICRA2026 - World Model Track</h2>
38
+ <p>
39
+ We've designed a series of challenging tasks covering scenarios including kitchen environments,
40
+ workbenches, dining tables, and bathroom settings, encompassing diverse robot-object interactions
41
+ e.g., collisions, grasping, placement, and dragging maneuvers, to thoroughly evaluate models'
42
+ generative capabilities.
43
+ </p>
44
+
45
+ <h3>🗂️ Dataset</h3>
46
+ <p>
47
+ The train, validation and test dataset for the competition can be downloaded from
48
+ <a href="https://huggingface.co/datasets/agibot-world/AgiBotWorldChallenge-2026/tree/main/WorldModel" target="_blank" rel="noopener noreferrer">
49
+ AgiBotWorld Challenge-2026 (World Model Track)
50
+ </a>.
51
+ </p>
52
+
53
+ <h3>🧪 Baseline</h3>
54
+ <p>
55
+ We've release the
56
+ <a href="https://github.com/AgibotTech/AgiBotWorldChallengeICRA2026-WorldModelBaseline" target="_blank" rel="noopener noreferrer">
57
+ baseline repo
58
+ </a>.
59
+ You can train your own world model based on the baseline repo or your own codebase.
60
+ </p>
61
+
62
+ <h3>📝 Rules</h3>
63
+
64
+ <p><strong>Authentication</strong>:</p>
65
+ <p>
66
+ Please register your team on the
67
+ <a href="https://agibot-world.com/challenge2026" target="_blank" rel="noopener noreferrer">
68
+ official website of AgiBotWorldChallengeICRA2026
69
+ </a>
70
+ before submitting your results. To identify the team associated with your submission, we require
71
+ you to include a <code>meta_info.txt</code> file in the compressed file. The first line of this file
72
+ must be the Email Address used during your team registration.
73
+ </p>
74
+
75
+ <p>A simple example of <code>meta_info.txt</code>:</p>
76
+ <pre><code>test@test.test</code></pre>
77
+
78
+ <p>
79
+ If the email address in <code>meta_info.txt</code> does not match the list of registered email addresses in
80
+ <a href="https://agibot-world.com/challenge2026" target="_blank" rel="noopener noreferrer">
81
+ official website of AgiBotWorldChallengeICRA2026
82
+ </a>,
83
+ the evaluation will fail. We update the Team Information on the Google Form daily, so you will be able to
84
+ submit your results normally <strong>after the day</strong> your registration.
85
+ </p>
86
+
87
+ <p><strong>Submission Limitation</strong>:</p>
88
+ <p>
89
+ Each team is limited to <strong>one successfully evaluated submission per calendar day</strong>.
90
+ The submission count resets daily at midnight, UTC time (based on Hugging Face's server time).
91
+ If your team exceeds the daily submission limit, the evaluation will not proceed.
92
+ </p>
93
+ <p>
94
+ <strong>Teams are strictly prohibited from registering multiple team accounts to gain extra evaluation attempts.
95
+ Any team found violating this rule will be disqualified and all related scores will be cancelled.</strong>
96
+ </p>
97
+
98
+ <p>
99
+ The size of your submitted compressed file should be less than <strong>500MB</strong>.
100
+ If the compressed file exceeds the size limit, the evaluation will not proceed.
101
+ </p>
102
+
103
+ <p><strong>Reproducibility</strong>:</p>
104
+ <p>
105
+ If your team ranking is in the top positions of the leaderboard (public score) when the competition closes,
106
+ we will send an email to your registered address requesting submission of relevant training and inference codes
107
+ for validity verification and reproducibility checks. Please be sure to check the detailed instructions in the
108
+ follow-up email.
109
+ </p>
110
+
111
+ <h3>📦 Submission</h3>
112
+ <ol>
113
+ <li>
114
+ As shown in the instruction of the
115
+ <a href="https://github.com/AgibotTech/AgiBotWorldChallengeICRA2026-WorldModelBaseline" target="_blank" rel="noopener noreferrer">
116
+ baseline repo
117
+ </a>,
118
+ the prediction directory should contain the following. Note that the number of prediction frames of each
119
+ episode should be equal to the number of actions minus one because the action array also incude the action
120
+ of the provided first frame. Moreover, as mentioned above, <strong>meta_info.txt</strong> should be included in
121
+ the submission folder. Finally, the entire size of the compression file (.zip) should be less than
122
+ <strong>500 MB</strong>.
123
+
124
+ <p>
125
+ We also provide a template submission file
126
+ <a href="https://huggingface.co/datasets/agibot-world/AgiBotWorldChallenge-2026/blob/main/WorldModel/template.zip" target="_blank" rel="noopener noreferrer">
127
+ here
128
+ </a>.
129
+ </p>
130
+
131
+ <pre><code>PREDICTIONS/
132
+ ├── meta_info.txt
133
+ ├── task_0/
134
+ │ ├── episode_0/
135
+ │ │ ├── 0/
136
+ │ │ │ └── video/
137
+ │ │ │ ├── frame_00000.jpg
138
+ │ │ │ ├── frame_00001.jpg
139
+ │ │ │ ├── ...
140
+ │ │ │ └── frame_T0.jpg
141
+ │ │ ├── 1/
142
+ │ │ │ └── video/
143
+ │ │ │ ├── frame_00000.jpg
144
+ │ │ │ ├── frame_00001.jpg
145
+ │ │ │ ├── ...
146
+ │ │ │ └── frame_T0.jpg
147
+ │ │ └── 2/
148
+ │ │ └── video/
149
+ │ │ ├── frame_00000.jpg
150
+ │ │ ├── frame_00001.jpg
151
+ │ │ ├── ...
152
+ │ │ └── frame_T0.jpg
153
+ │ ├── episode_1/
154
+ │ │ ├── 0/
155
+ │ │ │ └── video/
156
+ │ │ │ ├── frame_00000.jpg
157
+ │ │ │ ├── frame_00001.jpg
158
+ │ │ │ ├── ...
159
+ │ │ │ └── frame_T1.jpg
160
+ │ │ ├── 1/
161
+ │ │ │ └── video/
162
+ │ │ │ ├── frame_00000.jpg
163
+ │ │ │ ├── frame_00001.jpg
164
+ │ │ │ ├── ...
165
+ │ │ │ └── frame_T1.jpg
166
+ │ │ └── 2/
167
+ │ │ └── video/
168
+ │ │ ├── frame_00000.jpg
169
+ │ │ ├── frame_00001.jpg
170
+ │ │ ├── ...
171
+ │ │ └── frame_T1.jpg
172
+ │ ├── episode_2/
173
+ │ └── ...
174
+ ├── task_1/
175
+ └── ...</code></pre>
176
+ </li>
177
+ <li>
178
+ Compress and submit the predictions
179
+ <pre><code>cd PARENT_DIRECTORY_OF_PREDICTIONS
180
+ zip -r ./submission.zip ./PREDICTIONS</code></pre>
181
+ </li>
182
+ <li>Upload the <code>submission.zip</code>.</li>
183
+ </ol>
184
+
185
+ <h3>📊 Evaluation</h3>
186
+ <p>
187
+ Before evaluation, we will resize all predicted images to (480, 640) (the same as the size of ground-truth images).
188
+ Then, we use <strong>PSNR, scene consistency and nDTW</strong> for evaluation. More details can be found in
189
+ <a href="https://github.com/AgibotTech/EWMBench" target="_blank" rel="noopener noreferrer">EWMBench</a>
190
+ and
191
+ <a href="https://github.com/AgibotTech/AgiBotWorldChallengeICRA2026-WorldModelBaseline" target="_blank" rel="noopener noreferrer">
192
+ baseline repo
193
+ </a>.
194
+ </p>
195
+
196
+ <p>
197
+ To calculate the overall score, we normalize PSNR through
198
+ <code>np.clip(psnr, a_min=0, a_max=35)/35</code>
199
+ and then average the scene consistency, nDTW and the normalized PSNR.
200
+ </p>
201
+
202
+ <h3>📅 Timeline</h3>
203
+ <table class="timeline-table">
204
+ <tr>
205
+ <td>Competition Start</td>
206
+ <td>2/28</td>
207
+ </tr>
208
+ <tr>
209
+ <td>Submission Deadline</td>
210
+ <td>4/20</td>
211
+ </tr>
212
+ </table>
213
+
214
+ <div class="cta-section">
215
+ <p>Ready to compete?</p>
216
+ <a href="/submit" class="btn btn-primary">Go to Submission</a>
217
+ </div>
218
+ </div>
219
+ {% endblock %}
templates/leaderboard.html ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Leaderboard - ICRA26 Workshop{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="page-header">
7
+ <h1>Leaderboard</h1>
8
+ <p class="page-subtitle">Competition Rankings</p>
9
+ </div>
10
+
11
+ <div class="content-card">
12
+ <div class="leaderboard-container">
13
+ <table class="leaderboard-table">
14
+ <thead>
15
+ <tr>
16
+ <th class="rank-col">#</th>
17
+ <th class="team-col">Team</th>
18
+ <th class="score-col">Best Score</th>
19
+ <th class="time-col">Last Submit</th>
20
+ </tr>
21
+ </thead>
22
+ <tbody id="leaderboardBody">
23
+ <tr>
24
+ <td colspan="4" class="loading-cell">
25
+ <span class="loading-spinner"></span>
26
+ Loading...
27
+ </td>
28
+ </tr>
29
+ </tbody>
30
+ </table>
31
+ </div>
32
+
33
+ <div id="emptyState" class="empty-state hidden">
34
+ <div class="empty-icon">📊</div>
35
+ <h3>No Submissions Yet</h3>
36
+ <p>Be the first to submit and claim the top spot!</p>
37
+ <a href="/submit" class="btn btn-primary">Submit Now</a>
38
+ </div>
39
+ </div>
40
+ {% endblock %}
41
+
42
+ {% block scripts %}
43
+ <script>
44
+ async function loadLeaderboard() {
45
+ try {
46
+ const response = await fetch('/api/leaderboard');
47
+ const data = await response.json();
48
+
49
+ const tbody = document.getElementById('leaderboardBody');
50
+ const emptyState = document.getElementById('emptyState');
51
+
52
+ if (data.length === 0) {
53
+ tbody.innerHTML = '';
54
+ emptyState.classList.remove('hidden');
55
+ return;
56
+ }
57
+
58
+ emptyState.classList.add('hidden');
59
+
60
+ tbody.innerHTML = data.map((entry, index) => {
61
+ const rank = index + 1;
62
+ const score = entry.best_score !== null ? entry.best_score : '-';
63
+ const time = entry.last_submit_time
64
+ ? new Date(entry.last_submit_time).toLocaleString()
65
+ : '-';
66
+ const isTop3 = rank <= 3;
67
+
68
+ let rankClass = '';
69
+ let rankIcon = '';
70
+ if (rank === 1) {
71
+ rankClass = 'rank-gold';
72
+ rankIcon = '🥇';
73
+ } else if (rank === 2) {
74
+ rankClass = 'rank-silver';
75
+ rankIcon = '🥈';
76
+ } else if (rank === 3) {
77
+ rankClass = 'rank-bronze';
78
+ rankIcon = '🥉';
79
+ }
80
+
81
+ return `
82
+ <tr class="${rankClass}">
83
+ <td class="rank-cell">
84
+ <span class="rank-badge ${rankClass}">
85
+ ${rankIcon || rank}
86
+ </span>
87
+ </td>
88
+ <td class="team-cell">
89
+ <span class="team-name">${escapeHtml(entry.team_name)}</span>
90
+ </td>
91
+ <td class="score-cell">
92
+ <span class="score-value">${score}</span>
93
+ </td>
94
+ <td class="time-cell">${time}</td>
95
+ </tr>
96
+ `;
97
+ }).join('');
98
+
99
+ } catch (error) {
100
+ console.error('Failed to load leaderboard:', error);
101
+ document.getElementById('leaderboardBody').innerHTML = `
102
+ <tr>
103
+ <td colspan="4" class="error-cell">
104
+ Failed to load leaderboard. Please try again.
105
+ </td>
106
+ </tr>
107
+ `;
108
+ }
109
+ }
110
+
111
+ function escapeHtml(text) {
112
+ const div = document.createElement('div');
113
+ div.textContent = text;
114
+ return div.innerHTML;
115
+ }
116
+
117
+ // Load on page load
118
+ loadLeaderboard();
119
+
120
+ // Auto-refresh every 30 seconds
121
+ setInterval(loadLeaderboard, 30000);
122
+ </script>
123
+ {% endblock %}
templates/submit.html ADDED
@@ -0,0 +1,492 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Submission - ICRA26 Workshop{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="page-header">
7
+ <h1>Submission</h1>
8
+ </div>
9
+
10
+ <!-- Upload Section -->
11
+ <div class="content-card">
12
+ <h2>Submit Your Results</h2>
13
+
14
+ <div class="upload-area" id="uploadArea">
15
+ <div class="upload-icon">📁</div>
16
+ <p>Drag & drop your .zip file here</p>
17
+ <p class="upload-hint">or</p>
18
+ <label class="btn btn-secondary">
19
+ Browse Files
20
+ <input type="file" id="fileInput" accept=".zip" hidden>
21
+ </label>
22
+ <p class="file-name" id="fileName"></p>
23
+ </div>
24
+
25
+ <div id="uploadProgress" class="upload-progress hidden">
26
+ <div class="progress-bar">
27
+ <div class="progress-fill" id="progressFill"></div>
28
+ </div>
29
+ <p id="progressText">Uploading...</p>
30
+ </div>
31
+
32
+ <div id="submitResult" class="submit-result hidden"></div>
33
+
34
+ <button class="btn btn-primary btn-large" id="submitBtn" disabled>
35
+ Submit
36
+ </button>
37
+ </div>
38
+
39
+ <!-- Email Check Section -->
40
+ <div class="content-card">
41
+ <h2>View My Submissions</h2>
42
+ <p class="section-description">
43
+ Enter your registered email to view submission history directly.
44
+ </p>
45
+ <p class="form-hint">
46
+ Note: evaluations are queued. During <strong>pending/processing</strong>, records may not appear yet.
47
+ This section shows submissions after validation/evaluation results are written back.
48
+ </p>
49
+
50
+ <!-- Email Input -->
51
+ <div class="verification-section" id="emailSection">
52
+ <div class="form-row">
53
+ <div class="form-group form-group-large">
54
+ <label for="emailInput">Email Address</label>
55
+ <input type="email" id="emailInput" class="input-large" placeholder="Enter your registered email">
56
+ </div>
57
+ <div class="form-group form-group-small">
58
+ <label>&nbsp;</label>
59
+ <button class="btn btn-secondary btn-full" id="sendCodeBtn">
60
+ View
61
+ </button>
62
+ </div>
63
+ </div>
64
+ <p class="form-hint" id="emailHint"></p>
65
+ </div>
66
+ </div>
67
+
68
+ <!-- My Submissions (hidden until verified) -->
69
+ <div class="content-card hidden" id="submissionsCard">
70
+ <div class="submissions-header">
71
+ <h2>My Submissions</h2>
72
+ <span class="team-badge" id="teamBadge"></span>
73
+ </div>
74
+
75
+ <div class="submissions-table-container">
76
+ <table class="submissions-table" id="submissionsTable">
77
+ <thead>
78
+ <tr>
79
+ <th>Submit Time</th>
80
+ <th>Status</th>
81
+ <th>Score</th>
82
+ <th>Details</th>
83
+ </tr>
84
+ </thead>
85
+ <tbody id="submissionsBody">
86
+ </tbody>
87
+ </table>
88
+ </div>
89
+
90
+ <div class="verified-footer">
91
+ <p>Queued submissions may take time to appear. Refresh later to check the latest validated results.</p>
92
+ </div>
93
+ </div>
94
+ {% endblock %}
95
+
96
+ {% block scripts %}
97
+ <script>
98
+ // Elements
99
+ const emailInput = document.getElementById('emailInput');
100
+ const sendCodeBtn = document.getElementById('sendCodeBtn');
101
+ const emailHint = document.getElementById('emailHint');
102
+ const emailSection = document.getElementById('emailSection');
103
+ const submissionsCard = document.getElementById('submissionsCard');
104
+ const teamBadge = document.getElementById('teamBadge');
105
+ const submissionsBody = document.getElementById('submissionsBody');
106
+
107
+ // Upload elements
108
+ const fileInput = document.getElementById('fileInput');
109
+ const uploadArea = document.getElementById('uploadArea');
110
+ const fileName = document.getElementById('fileName');
111
+ const submitBtn = document.getElementById('submitBtn');
112
+ const uploadProgress = document.getElementById('uploadProgress');
113
+ const progressFill = document.getElementById('progressFill');
114
+ const progressText = document.getElementById('progressText');
115
+ const submitResult = document.getElementById('submitResult');
116
+
117
+ let selectedFile = null;
118
+ let verifiedIdentifier = null;
119
+
120
+ // ============== Utility Functions ==============
121
+
122
+ function getErrorMessage(data, fallback) {
123
+ // Safely extract error message from response
124
+ if (typeof data === 'string') return data;
125
+ if (data && typeof data.detail === 'string') return data.detail;
126
+ if (data && typeof data.message === 'string') return data.message;
127
+ if (data && typeof data.detail === 'object') return JSON.stringify(data.detail);
128
+ return fallback;
129
+ }
130
+
131
+ // ============== Email Validation Logic ==============
132
+ sendCodeBtn.addEventListener('click', async () => {
133
+ const email = emailInput.value.trim().toLowerCase();
134
+ if (!email) {
135
+ showEmailHint('Please enter your email address', 'error');
136
+ return;
137
+ }
138
+
139
+ // Basic email validation
140
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
141
+ if (!emailRegex.test(email)) {
142
+ showEmailHint('Please enter a valid email address', 'error');
143
+ return;
144
+ }
145
+
146
+ sendCodeBtn.disabled = true;
147
+ sendCodeBtn.textContent = 'Checking...';
148
+
149
+ try {
150
+ const response = await fetch('/api/validate-email', {
151
+ method: 'POST',
152
+ headers: { 'Content-Type': 'application/json' },
153
+ body: JSON.stringify({ email: email })
154
+ });
155
+
156
+ const data = await response.json();
157
+
158
+ if (response.ok) {
159
+ showEmailHint('Email verified.', 'success');
160
+ verifiedIdentifier = data.email || email;
161
+ teamBadge.textContent = data.team_name || verifiedIdentifier;
162
+ emailSection.classList.add('hidden');
163
+ submissionsCard.classList.remove('hidden');
164
+ loadSubmissions(verifiedIdentifier);
165
+ } else {
166
+ const errorMsg = getErrorMessage(data, 'Failed to verify email');
167
+ showEmailHint(errorMsg, 'error');
168
+ }
169
+ } catch (error) {
170
+ showEmailHint('Network error. Please try again.', 'error');
171
+ } finally {
172
+ sendCodeBtn.disabled = false;
173
+ sendCodeBtn.textContent = 'View';
174
+ }
175
+ });
176
+
177
+ function showEmailHint(message, type) {
178
+ emailHint.textContent = message;
179
+ emailHint.className = 'form-hint hint-' + type;
180
+ }
181
+
182
+ // ============== Load Submissions ==============
183
+
184
+ async function loadSubmissions(teamName) {
185
+ try {
186
+ const response = await fetch(`/api/submissions/${encodeURIComponent(teamName)}`);
187
+ const submissions = await response.json();
188
+
189
+ submissionsBody.innerHTML = '';
190
+
191
+ if (submissions.length === 0) {
192
+ submissionsBody.innerHTML = `
193
+ <tr>
194
+ <td colspan="4" class="empty-message">No submissions yet</td>
195
+ </tr>
196
+ `;
197
+ return;
198
+ }
199
+
200
+ submissions.forEach(sub => {
201
+ addSubmissionToTable(sub);
202
+ });
203
+ } catch (error) {
204
+ console.error('Failed to load submissions:', error);
205
+ submissionsBody.innerHTML = `
206
+ <tr>
207
+ <td colspan="4" class="error-cell">Failed to load submissions</td>
208
+ </tr>
209
+ `;
210
+ }
211
+ }
212
+
213
+ function addSubmissionToTable(sub) {
214
+ const row = document.createElement('tr');
215
+
216
+ let statusClass = '';
217
+ let statusText = '';
218
+ switch (sub.status) {
219
+ case 'pending':
220
+ statusClass = 'status-pending';
221
+ statusText = 'Queued';
222
+ break;
223
+ case 'processing':
224
+ statusClass = 'status-processing';
225
+ statusText = 'Processing';
226
+ break;
227
+ case 'completed':
228
+ statusClass = 'status-completed';
229
+ statusText = 'Completed';
230
+ break;
231
+ case 'error':
232
+ statusClass = 'status-error';
233
+ statusText = 'Error';
234
+ break;
235
+ case 'overlimit':
236
+ statusClass = 'status-overlimit';
237
+ statusText = 'Over Limit';
238
+ break;
239
+ }
240
+
241
+ const scoreDisplay = sub.score !== null ? sub.score : (sub.status === 'overlimit' ? '-' : '...');
242
+ const timeDisplay = new Date(sub.submit_time).toLocaleString();
243
+
244
+ row.innerHTML = `
245
+ <td>${timeDisplay}</td>
246
+ <td><span class="status-badge ${statusClass}">${statusText}</span></td>
247
+ <td class="score">${scoreDisplay}</td>
248
+ <td class="details">${sub.error_message || '-'}</td>
249
+ `;
250
+
251
+ submissionsBody.insertBefore(row, submissionsBody.firstChild);
252
+ }
253
+
254
+ // ============== File Upload Logic ==============
255
+
256
+ fileInput.addEventListener('change', (e) => {
257
+ if (e.target.files.length > 0) {
258
+ handleFileSelect(e.target.files[0]);
259
+ }
260
+ });
261
+
262
+ uploadArea.addEventListener('dragover', (e) => {
263
+ e.preventDefault();
264
+ uploadArea.classList.add('dragover');
265
+ });
266
+
267
+ uploadArea.addEventListener('dragleave', () => {
268
+ uploadArea.classList.remove('dragover');
269
+ });
270
+
271
+ uploadArea.addEventListener('drop', (e) => {
272
+ e.preventDefault();
273
+ uploadArea.classList.remove('dragover');
274
+ if (e.dataTransfer.files.length > 0) {
275
+ handleFileSelect(e.dataTransfer.files[0]);
276
+ }
277
+ });
278
+
279
+ function handleFileSelect(file) {
280
+ if (!file.name.endsWith('.zip')) {
281
+ showSubmitResult('error', 'Only .zip files are allowed');
282
+ return;
283
+ }
284
+
285
+ selectedFile = file;
286
+ fileName.textContent = `Selected: ${file.name} (${formatBytes(file.size)})`;
287
+ submitBtn.disabled = false;
288
+ submitResult.classList.add('hidden');
289
+ }
290
+
291
+ const MAX_FILE_SIZE_MB = 500;
292
+
293
+ submitBtn.addEventListener('click', async () => {
294
+ if (!selectedFile) return;
295
+
296
+ // Check file size before uploading
297
+ const fileSizeMB = selectedFile.size / (1024 * 1024);
298
+ if (fileSizeMB > MAX_FILE_SIZE_MB) {
299
+ showSubmitResult('error', `File too large: ${fileSizeMB.toFixed(2)}MB (max ${MAX_FILE_SIZE_MB}MB)`);
300
+ return;
301
+ }
302
+
303
+ const formData = new FormData();
304
+ formData.append('file', selectedFile);
305
+
306
+ submitBtn.disabled = true;
307
+ uploadProgress.classList.remove('hidden');
308
+ progressFill.style.width = '0%';
309
+ progressText.textContent = 'Uploading...';
310
+
311
+ try {
312
+ const xhr = new XMLHttpRequest();
313
+
314
+ xhr.upload.addEventListener('progress', (e) => {
315
+ if (e.lengthComputable) {
316
+ const percent = (e.loaded / e.total) * 100;
317
+ progressFill.style.width = `${percent}%`;
318
+ }
319
+ });
320
+
321
+ xhr.addEventListener('load', () => {
322
+ progressText.textContent = 'Processing...';
323
+ progressFill.style.width = '100%';
324
+
325
+ const response = JSON.parse(xhr.responseText);
326
+
327
+ if (response.status === 'overlimit') {
328
+ showSubmitResult('warning', response.message);
329
+ } else if (xhr.status === 200) {
330
+ showSubmitResult('success', response.message);
331
+ // Clear file input after successful upload
332
+ fileInput.value = '';
333
+ selectedFile = null;
334
+ fileName.textContent = 'No file selected';
335
+ submitBtn.disabled = true;
336
+ // Refresh submissions if verified
337
+ if (verifiedIdentifier) {
338
+ loadSubmissions(verifiedIdentifier);
339
+ }
340
+ } else {
341
+ showSubmitResult('error', response.detail || 'Submission failed');
342
+ }
343
+
344
+ submitBtn.disabled = false;
345
+ setTimeout(() => {
346
+ uploadProgress.classList.add('hidden');
347
+ }, 1000);
348
+ });
349
+
350
+ xhr.addEventListener('error', () => {
351
+ showSubmitResult('error', 'Network error occurred');
352
+ submitBtn.disabled = false;
353
+ uploadProgress.classList.add('hidden');
354
+ });
355
+
356
+ xhr.open('POST', '/api/submit');
357
+ xhr.send(formData);
358
+
359
+ } catch (error) {
360
+ showSubmitResult('error', error.message);
361
+ submitBtn.disabled = false;
362
+ uploadProgress.classList.add('hidden');
363
+ }
364
+ });
365
+
366
+ function showSubmitResult(type, message) {
367
+ submitResult.classList.remove('hidden', 'success', 'error', 'warning');
368
+ submitResult.classList.add(type);
369
+ submitResult.innerHTML = `
370
+ <span class="result-icon">${type === 'success' ? '✓' : type === 'error' ? '✗' : '⚠'}</span>
371
+ <span>${message}</span>
372
+ `;
373
+ }
374
+
375
+ function formatBytes(bytes) {
376
+ if (bytes < 1024) return bytes + ' B';
377
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
378
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
379
+ }
380
+
381
+ // Enter key handlers
382
+ emailInput.addEventListener('keypress', (e) => {
383
+ if (e.key === 'Enter') {
384
+ sendCodeBtn.click();
385
+ }
386
+ });
387
+ </script>
388
+
389
+ <style>
390
+ .section-description {
391
+ color: var(--text-muted);
392
+ margin-bottom: 20px;
393
+ }
394
+
395
+ .verification-section {
396
+ max-width: 600px;
397
+ }
398
+
399
+ .form-row {
400
+ display: flex;
401
+ gap: 16px;
402
+ align-items: flex-end;
403
+ }
404
+
405
+ .form-group {
406
+ flex: 1;
407
+ }
408
+
409
+ .form-group.form-group-large {
410
+ flex: 3;
411
+ }
412
+
413
+ .form-group.form-group-small {
414
+ flex: 1;
415
+ }
416
+
417
+ .input-large {
418
+ width: 100%;
419
+ padding: 14px 16px;
420
+ font-size: 1rem;
421
+ border: 1px solid var(--border-color);
422
+ border-radius: var(--radius-md);
423
+ transition: border-color 0.2s, box-shadow 0.2s;
424
+ }
425
+
426
+ .input-large:focus {
427
+ outline: none;
428
+ border-color: var(--primary-color);
429
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
430
+ }
431
+
432
+ .btn-full {
433
+ width: 100%;
434
+ padding: 14px 16px;
435
+ }
436
+
437
+ .code-actions {
438
+ display: flex;
439
+ gap: 12px;
440
+ margin-top: 16px;
441
+ }
442
+
443
+ .code-actions .btn {
444
+ flex: 1;
445
+ }
446
+
447
+ .hint-success {
448
+ color: var(--success-color);
449
+ }
450
+
451
+ .hint-error {
452
+ color: var(--error-color);
453
+ }
454
+
455
+ .submissions-header {
456
+ display: flex;
457
+ align-items: center;
458
+ justify-content: space-between;
459
+ margin-bottom: 20px;
460
+ }
461
+
462
+ .submissions-header h2 {
463
+ margin-bottom: 0;
464
+ }
465
+
466
+ .team-badge {
467
+ background: var(--primary-color);
468
+ color: white;
469
+ padding: 6px 16px;
470
+ border-radius: 20px;
471
+ font-weight: 500;
472
+ }
473
+
474
+ .verified-footer {
475
+ margin-top: 20px;
476
+ padding-top: 16px;
477
+ border-top: 1px solid var(--border-color);
478
+ text-align: center;
479
+ }
480
+
481
+ .verified-footer p {
482
+ color: var(--text-muted);
483
+ font-size: 0.9rem;
484
+ margin: 0;
485
+ }
486
+
487
+ .error-cell {
488
+ color: var(--error-color);
489
+ text-align: center;
490
+ }
491
+ </style>
492
+ {% endblock %}
worker.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Background evaluation worker.
3
+
4
+ Runs submission queue processing outside the web process so UI/API requests
5
+ remain responsive under evaluation load.
6
+ """
7
+ import asyncio
8
+ import os
9
+
10
+ from app import HF_TOKEN, get_storage, load_metric_module, process_submissions
11
+
12
+
13
+ async def main():
14
+ if not HF_TOKEN:
15
+ print("HF_TOKEN not configured; worker exiting.")
16
+ return
17
+
18
+ # Heavy setup is isolated to this worker process.
19
+ await asyncio.to_thread(get_storage)
20
+ await asyncio.to_thread(load_metric_module)
21
+ print("Evaluation worker started")
22
+
23
+ await process_submissions()
24
+
25
+
26
+ if __name__ == "__main__":
27
+ try:
28
+ asyncio.run(main())
29
+ except KeyboardInterrupt:
30
+ print("Worker stopped")