NidhiS09 commited on
Commit
61ce1f2
Β·
verified Β·
1 Parent(s): 742bccd

Update src/main.py

Browse files
Files changed (1) hide show
  1. src/main.py +386 -106
src/main.py CHANGED
@@ -1,114 +1,394 @@
 
 
 
 
 
 
 
1
  import streamlit as st
2
  import pandas as pd
3
- import os
4
- import hashlib
5
- from datetime import datetime
6
- from pathlib import Path
7
- from huggingface_hub import CommitScheduler
8
- from localization_eval import evaluate_submission
9
  from PIL import Image
 
 
10
 
11
- # --- PERSISTENCE CONFIGURATION ---
12
- DATA_FILENAME = "submissions.csv"
13
- USER_FILENAME = "users.csv"
14
- DATA_DIR = Path("data")
15
- DATA_DIR.mkdir(exist_ok=True)
16
-
17
- DATA_PATH = DATA_DIR / DATA_FILENAME
18
- USER_PATH = DATA_DIR / USER_FILENAME
19
-
20
- # Initialize CommitScheduler
21
- # This will automatically sync everything in the /data folder to your HF Dataset
22
- repo_id = "VizWiz-Challenges/submissions-db" # TODO: Change this
23
-
24
- scheduler = CommitScheduler(
25
- repo_id=repo_id,
26
- repo_type="dataset",
27
- folder_path=DATA_DIR,
28
- path_in_repo="data",
29
- every=5,
30
- token=os.getenv("SubmissionsToken")
31
  )
32
 
33
- # --- DB HELPERS (Replaced SQLite with Pandas/CSV) ---
34
-
35
- def init_db():
36
- # Create files if they don't exist
37
- if not USER_PATH.exists():
38
- pd.DataFrame(columns=["username", "password"]).to_csv(USER_PATH, index=False)
39
- if not DATA_PATH.exists():
40
- pd.DataFrame(columns=["username", "bbox_mAP", "bbox_AP50", "segm_mAP", "segm_AP50", "timestamp"]).to_csv(DATA_PATH, index=False)
41
-
42
- def make_hashes(password):
43
- return hashlib.sha256(str.encode(password)).hexdigest()
44
-
45
- def add_user(username, password):
46
- with scheduler.lock:
47
- df = pd.read_csv(USER_PATH)
48
- if username in df['username'].values:
49
- return False
50
- new_user = pd.DataFrame([{"username": username, "password": make_hashes(password)}])
51
- df = pd.concat([df, new_user], ignore_index=True)
52
- df.to_csv(USER_PATH, index=False)
53
- return True
54
-
55
- def login_user(username, password):
56
- df = pd.read_csv(USER_PATH)
57
- user_row = df[(df['username'] == username) & (df['password'] == make_hashes(password))]
58
- return not user_row.empty
59
-
60
- def save_submission(username, bbox_mAP, bbox_AP50, segm_mAP, segm_AP50):
61
- with scheduler.lock:
62
- df = pd.read_csv(DATA_PATH)
63
- new_sub = pd.DataFrame([{
64
- "username": username,
65
- "bbox_mAP": bbox_mAP,
66
- "bbox_AP50": bbox_AP50,
67
- "segm_mAP": segm_mAP,
68
- "segm_AP50": segm_AP50,
69
- "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
70
- }])
71
- df = pd.concat([df, new_sub], ignore_index=True)
72
- df.to_csv(DATA_PATH, index=False)
73
-
74
- def get_leaderboard_data():
75
- if not DATA_PATH.exists():
76
- return pd.DataFrame()
77
-
78
- df = pd.read_csv(DATA_PATH)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  if df.empty:
80
- return df
81
-
82
- # Logic to get the BEST score per user
83
- # Sorting by segm_mAP (desc) and timestamp (asc) to get best/earliest
84
- df = df.sort_values(by=['segm_mAP', 'timestamp'], ascending=[False, True])
85
- df_best = df.drop_duplicates(subset='username', keep='first')
86
-
87
- df_best = df_best.rename(columns={'segm_mAP': 'Best_segm_mAP'})
88
- return df_best
89
-
90
- # --- CONFIGURATION & SETUP ---
91
- st.set_page_config(page_title="AI Benchmark Arena", page_icon="πŸ†", layout="wide")
92
-
93
-
94
- def main_app():
95
-
96
- if st.button("Evaluate"):
97
- with st.spinner('Calculating...'):
98
- bbox_mAP, bbox_AP50, segm_mAP, segm_AP50 = evaluate_submission("src/biv_query.json", save_path)
99
- if bbox_mAP is not None:
100
- save_submission(st.session_state['username'], bbox_mAP, bbox_AP50, segm_mAP, segm_AP50)
101
- st.balloons()
102
- st.success("Submission Successful!")
103
-
104
- if __name__ == '__main__':
105
- init_db()
106
-
107
- if 'logged_in' not in st.session_state:
108
- st.session_state['logged_in'] = False
109
- st.session_state['username'] = None
110
-
111
- if not st.session_state['logged_in']:
112
- ui_login_signup()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  else:
114
- main_app()
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import uuid
4
+ import time
5
+ import tempfile
6
+ from typing import Any, Dict, List, Tuple
7
+
8
  import streamlit as st
9
  import pandas as pd
 
 
 
 
 
 
10
  from PIL import Image
11
+ from huggingface_hub import HfApi, hf_hub_download
12
+ from huggingface_hub.utils import HfHubHTTPError
13
 
14
+
15
+ # =========================
16
+ # CONFIG
17
+ # =========================
18
+
19
+ st.set_page_config(
20
+ page_title="AI Benchmark Arena",
21
+ page_icon="πŸ†",
22
+ layout="wide",
23
+ initial_sidebar_state="expanded",
 
 
 
 
 
 
 
 
 
 
24
  )
25
 
26
+ #Set this to the private dataset repo that acts as the "database"
27
+ DB_REPO_ID = os.getenv("DB_REPO_ID", "NidhiS09/VizWiz-submissions-db")
28
+ DB_REPO_TYPE = "dataset"
29
+
30
+ # This must exist as a Space Secret in the PUBLIC UI Space
31
+ SUBMISSIONS_TOKEN = os.getenv("SUBMISSIONS_TOKEN", "")
32
+
33
+ # Phase config (copied from EvalAI config intent)
34
+ PHASES = [
35
+ {"label": "Dev (qeury-dev2024)", "codename": "test-dev2024"},
36
+ {"label": "Standard (query-standard2024)", "codename": "test-standard2024"},
37
+ {"label": "Challenge (query-challenge2024)", "codename": "test-challenge2024"},
38
+ ]
39
+
40
+ CHALLENGE_TYPES = ["Object Detection", "Instance Segmentation"]
41
+
42
+ # Leaderboard columns from your EvalAI yaml
43
+ LEADERBOARD_METRICS = ["bbox_mAP", "bbox_AP50", "segm_mAP", "segm_AP50"]
44
+ DEFAULT_SORT_METRIC = "segm_AP50"
45
+
46
+
47
+ # =========================
48
+ # HELPERS
49
+ # =========================
50
+
51
+ def _require_token() -> None:
52
+ if not SUBMISSIONS_TOKEN:
53
+ st.error(
54
+ "Missing SUBMISSIONS_TOKEN. Add it in Space Settings β†’ Secrets "
55
+ "(token must have read/write access ONLY to the private DB dataset repo)."
56
+ )
57
+ st.stop()
58
+
59
+
60
+ def _validate_submission_json(obj: Any) -> Tuple[bool, str]:
61
+ """
62
+ Validates the submission format from your instructions:
63
+ - Top-level must be a list
64
+ - Each item must be a dict containing:
65
+ image_id (int), score (number), category_id (int), area (number),
66
+ bbox ([x,y,w,h]), segmentation (list)
67
+ """
68
+ if not isinstance(obj, list):
69
+ return False, "Submission must be a JSON list of annotations."
70
+
71
+ required_keys = {"image_id", "score", "category_id", "area", "bbox", "segmentation"}
72
+
73
+ for i, ann in enumerate(obj):
74
+ if not isinstance(ann, dict):
75
+ return False, f"Annotation at index {i} must be an object/dict."
76
+
77
+ missing = required_keys - set(ann.keys())
78
+ if missing:
79
+ return False, f"Annotation at index {i} missing keys: {sorted(list(missing))}"
80
+
81
+ # Basic type checks
82
+ if not isinstance(ann["image_id"], int):
83
+ return False, f"image_id at index {i} must be an integer."
84
+ if not isinstance(ann["category_id"], int):
85
+ return False, f"category_id at index {i} must be an integer."
86
+
87
+ if not isinstance(ann["score"], (int, float)):
88
+ return False, f"score at index {i} must be a number."
89
+
90
+ if not isinstance(ann["area"], (int, float)):
91
+ return False, f"area at index {i} must be a number."
92
+
93
+ bbox = ann["bbox"]
94
+ if not (isinstance(bbox, list) and len(bbox) == 4 and all(isinstance(x, (int, float)) for x in bbox)):
95
+ return False, f"bbox at index {i} must be a list of 4 numbers: [x, y, w, h]."
96
+
97
+ segm = ann["segmentation"]
98
+ if not isinstance(segm, list):
99
+ return False, f"segmentation at index {i} must be a list."
100
+
101
+ return True, "OK"
102
+
103
+
104
+ def _upload_json(api: HfApi, data: Dict[str, Any] | List[Any], path_in_repo: str) -> None:
105
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
106
+ json.dump(data, tmp, ensure_ascii=False)
107
+ tmp_path = tmp.name
108
+
109
+ try:
110
+ api.upload_file(
111
+ path_or_fileobj=tmp_path,
112
+ path_in_repo=path_in_repo,
113
+ repo_id=DB_REPO_ID,
114
+ repo_type=DB_REPO_TYPE,
115
+ token=SUBMISSIONS_TOKEN,
116
+ commit_message=f"Add {path_in_repo}",
117
+ )
118
+ finally:
119
+ try:
120
+ os.remove(tmp_path)
121
+ except OSError:
122
+ pass
123
+
124
+
125
+ def _create_submission_record(
126
+ *,
127
+ pred: List[Dict[str, Any]],
128
+ team: str,
129
+ model_name: str,
130
+ phase_codename: str,
131
+ challenge_type: str,
132
+ original_filename: str,
133
+ ) -> str:
134
+ """
135
+ Writes pred/meta/status to the private DB dataset repo.
136
+ Returns submission_id.
137
+ """
138
+ _require_token()
139
+ api = HfApi()
140
+
141
+ submission_id = str(uuid.uuid4())
142
+ ts = int(time.time())
143
+
144
+ meta = {
145
+ "submission_id": submission_id,
146
+ "team": team.strip(),
147
+ "model": model_name.strip(),
148
+ "phase_codename": phase_codename,
149
+ "challenge_type": challenge_type,
150
+ "timestamp": ts,
151
+ "original_filename": original_filename,
152
+ }
153
+
154
+ status = {"state": "queued", "timestamp": ts}
155
+
156
+ base = f"submissions/{submission_id}"
157
+ _upload_json(api, pred, f"{base}/pred.json")
158
+ _upload_json(api, meta, f"{base}/meta.json")
159
+ _upload_json(api, status, f"{base}/status.json")
160
+
161
+ return submission_id
162
+
163
+
164
+ def _download_leaderboard_jsonl() -> str | None:
165
+ """
166
+ Downloads leaderboard.jsonl from the DB repo.
167
+ Returns local path or None if missing.
168
+ """
169
+ _require_token()
170
+ try:
171
+ return hf_hub_download(
172
+ repo_id=DB_REPO_ID,
173
+ repo_type=DB_REPO_TYPE,
174
+ filename="leaderboard.jsonl",
175
+ token=SUBMISSIONS_TOKEN,
176
+ )
177
+ except HfHubHTTPError as e:
178
+ # Most common: 404 when file doesn't exist yet
179
+ if "404" in str(e):
180
+ return None
181
+ raise
182
+
183
+
184
+ def _load_leaderboard_df() -> pd.DataFrame:
185
+ path = _download_leaderboard_jsonl()
186
+ if path is None:
187
+ return pd.DataFrame(columns=["team", "model", "phase_codename", *LEADERBOARD_METRICS, "timestamp"])
188
+
189
+ rows = []
190
+ with open(path, "r", encoding="utf-8") as f:
191
+ for line in f:
192
+ line = line.strip()
193
+ if not line:
194
+ continue
195
+ try:
196
+ rows.append(json.loads(line))
197
+ except json.JSONDecodeError:
198
+ # Skip malformed lines rather than crashing the UI
199
+ continue
200
+
201
+ if not rows:
202
+ return pd.DataFrame(columns=["team", "model", "phase_codename", *LEADERBOARD_METRICS, "timestamp"])
203
+
204
+ df = pd.DataFrame(rows)
205
+
206
+ # Ensure columns exist
207
+ for col in ["team", "model", "phase_codename", "timestamp", *LEADERBOARD_METRICS]:
208
+ if col not in df.columns:
209
+ df[col] = None
210
+
211
+ # Sort descending by default metric
212
+ if DEFAULT_SORT_METRIC in df.columns:
213
+ df = df.sort_values(by=DEFAULT_SORT_METRIC, ascending=False, kind="mergesort")
214
+
215
+ return df
216
+
217
+
218
+ # =========================
219
+ # UI
220
+ # =========================
221
+
222
+ def render_overview():
223
+ with st.expander("ℹ️ Overview of the AI Benchmark Arena"):
224
+ st.markdown(
225
+ """
226
+
227
+ **Note:** This Hugging Face Space queues submissions for evaluation and persists results in a private database repo.
228
+ """
229
+ )
230
+ # Keep this optional so missing image doesn't crash the Space
231
+ try:
232
+ overview_image = Image.open("src/overview_image.png").resize((600, 600))
233
+ st.image(overview_image, caption="Example of an object localization task")
234
+ except Exception:
235
+ st.info("Overview image not found at src/overview_image.png (optional).")
236
+
237
+
238
+ def render_eval_details():
239
+ with st.expander("πŸ“ How is the Score Calculated?"):
240
+ st.markdown(
241
+ """
242
+ Your submission is evaluated offline by a private evaluator against hidden ground-truth annotations.
243
+ The leaderboard reports:
244
+
245
+ - bbox_mAP
246
+ - bbox_AP50
247
+ - segm_mAP
248
+ - segm_AP50 (default ranking)
249
+
250
+ Raw submissions are kept private; only scores and metadata are shown.
251
+ """
252
+ )
253
+
254
+
255
+ def page_submit():
256
+ st.header("πŸš€ Submit your Predictions")
257
+
258
+ col1, col2 = st.columns([2, 1])
259
+
260
+ with col2:
261
+ st.subheader("Submission Info")
262
+ team = st.text_input("Team / Display Name", value=st.session_state.get("team", ""))
263
+ model_name = st.text_input("Model Name", value=st.session_state.get("model_name", ""))
264
+
265
+ phase_label = st.selectbox("Phase", [p["label"] for p in PHASES])
266
+ phase_codename = next(p["codename"] for p in PHASES if p["label"] == phase_label)
267
+
268
+ challenge_type = st.radio("Challenge type", CHALLENGE_TYPES, horizontal=False)
269
+
270
+ st.session_state["team"] = team
271
+ st.session_state["model_name"] = model_name
272
+
273
+ st.caption("Your submission will be queued for evaluation. Scores appear on the leaderboard after processing.")
274
+
275
+ with col1:
276
+ st.subheader("Upload Submission File")
277
+ uploaded_file = st.file_uploader("Choose a JSON file", type=["json"])
278
+
279
+ if uploaded_file is None:
280
+ st.info("Upload a JSON file that contains a list of annotations.")
281
+ return
282
+
283
+ # Parse JSON
284
+ try:
285
+ raw = uploaded_file.getvalue().decode("utf-8")
286
+ pred_obj = json.loads(raw)
287
+ except Exception:
288
+ st.error("Could not parse JSON. Please upload a valid JSON file.")
289
+ return
290
+
291
+ ok, msg = _validate_submission_json(pred_obj)
292
+ if not ok:
293
+ st.error(f"Invalid submission format: {msg}")
294
+ return
295
+
296
+ st.success("Submission file looks valid βœ…")
297
+
298
+ submit_clicked = st.button("Submit (Queue for Evaluation)", type="primary")
299
+
300
+ if submit_clicked:
301
+ if not team.strip():
302
+ st.error("Please enter Team / Display Name.")
303
+ return
304
+ if not model_name.strip():
305
+ st.error("Please enter Model Name.")
306
+ return
307
+
308
+ with st.spinner("Uploading submission to the private database repo..."):
309
+ try:
310
+ submission_id = _create_submission_record(
311
+ pred=pred_obj,
312
+ team=team,
313
+ model_name=model_name,
314
+ phase_codename=phase_codename,
315
+ challenge_type=challenge_type,
316
+ original_filename=uploaded_file.name,
317
+ )
318
+ except Exception as e:
319
+ st.error(f"Upload failed: {e}")
320
+ return
321
+
322
+ st.balloons()
323
+ st.success("Submission queued successfully!")
324
+ st.code(f"Submission ID: {submission_id}")
325
+
326
+ st.info("Next: the private evaluator will score your submission and update the leaderboard.")
327
+
328
+
329
+ def page_leaderboard():
330
+ st.header("πŸ† Leaderboard")
331
+ st.write(f"Ranked by **{DEFAULT_SORT_METRIC}** (descending).")
332
+
333
+ with st.spinner("Loading leaderboard from private database repo..."):
334
+ try:
335
+ df = _load_leaderboard_df()
336
+ except Exception as e:
337
+ st.error(f"Could not load leaderboard: {e}")
338
+ return
339
+
340
  if df.empty:
341
+ st.info("No scored submissions yet. Submit a model to get started!")
342
+ return
343
+
344
+ # Add Rank column
345
+ df_display = df.copy()
346
+ df_display.insert(0, "Rank", range(1, len(df_display) + 1))
347
+
348
+ # Optional: pretty timestamp
349
+ if "timestamp" in df_display.columns:
350
+ df_display["timestamp"] = pd.to_datetime(df_display["timestamp"], unit="s", errors="coerce")
351
+
352
+ st.dataframe(
353
+ df_display,
354
+ column_config={
355
+ "Rank": st.column_config.Column("Rank", width="small"),
356
+ "team": "Team",
357
+ "model": "Model",
358
+ "phase_codename": "Phase",
359
+ "bbox_mAP": st.column_config.NumberColumn("bbox_mAP", format="%.4f"),
360
+ "bbox_AP50": st.column_config.NumberColumn("bbox_AP50", format="%.4f"),
361
+ "segm_mAP": st.column_config.NumberColumn("segm_mAP", format="%.4f"),
362
+ "segm_AP50": st.column_config.NumberColumn("segm_AP50", format="%.4f"),
363
+ "timestamp": st.column_config.DatetimeColumn("Scored at", format="D MMM YYYY, h:mm a"),
364
+ },
365
+ use_container_width=True,
366
+ hide_index=True,
367
+ )
368
+
369
+
370
+ def main():
371
+ st.sidebar.title("AI Benchmark Arena πŸ†")
372
+
373
+ # Warn early if DB repo isn't configured
374
+ if DB_REPO_ID.startswith("NidhiS09/"):
375
+ st.sidebar.warning("Set DB_REPO_ID env var or hardcode your private DB dataset repo id in main.py.")
376
+
377
+ menu = ["Submit Model", "Leaderboard"]
378
+ choice = st.sidebar.radio("Navigation", menu)
379
+
380
+ st.sidebar.markdown("---")
381
+ st.sidebar.caption("This Space queues submissions to a private DB repo and reads leaderboard results from it.")
382
+
383
+ render_overview()
384
+ render_eval_details()
385
+ st.markdown("---")
386
+
387
+ if choice == "Submit Model":
388
+ page_submit()
389
  else:
390
+ page_leaderboard()
391
+
392
+
393
+ if __name__ == "__main__":
394
+ main()