zhouziyu02 Cursor commited on
Commit
62f5d66
·
1 Parent(s): 5063745

UI: eval countdown bar, refresh leaderboard only on eval complete.

Browse files

Remove 60s display reload timer; poll eval state internally and refresh
tables when a run starts or finishes. Progress bar shows time until next
hourly eval cycle.

Co-authored-by: Cursor <cursoragent@cursor.com>

README.md CHANGED
@@ -28,7 +28,6 @@ Optional variables (Settings → Variables):
28
  | Variable | Default | Purpose |
29
  |---|---|---|
30
  | `TSFM_BENCH_INTERVAL_SECONDS` | `3600` | Collect + eval cycle (1 hour) |
31
- | `TSFM_REFRESH_SECONDS` | `60` | UI reload from disk (seconds) |
32
 
33
  Without `TSFM_API_KEY`, the leaderboard only reloads static CSV files and **does not** call the API.
34
 
 
28
  | Variable | Default | Purpose |
29
  |---|---|---|
30
  | `TSFM_BENCH_INTERVAL_SECONDS` | `3600` | Collect + eval cycle (1 hour) |
 
31
 
32
  Without `TSFM_API_KEY`, the leaderboard only reloads static CSV files and **does not** call the API.
33
 
app.py CHANGED
@@ -11,12 +11,12 @@ from src.about import (
11
  LLM_BENCHMARKS_TEXT,
12
  TITLE,
13
  )
 
14
  from src.display.css_html_js import (
15
  build_custom_css,
16
- refresh_countdown_head_js,
17
- refresh_countdown_markup,
18
  )
19
- from src.benchmark_config import BENCHMARK_INTERVAL_SECONDS, DISPLAY_REFRESH_SECONDS
20
  from src.eval_scheduler import get_scheduler, start_live_eval_scheduler
21
  from src.utils import (
22
  get_grouped_dfs,
@@ -28,6 +28,8 @@ RESULTS_PATH = os.getenv("TSFM_RESULTS_PATH", "results")
28
 
29
  start_live_eval_scheduler()
30
 
 
 
31
 
32
  def load_online_status(results_path: str = RESULTS_PATH) -> dict:
33
  status_path = Path(results_path) / "online_status.json"
@@ -36,6 +38,16 @@ def load_online_status(results_path: str = RESULTS_PATH) -> dict:
36
  return json.loads(status_path.read_text())
37
 
38
 
 
 
 
 
 
 
 
 
 
 
39
  def build_status_line(status: dict) -> str:
40
  eval_state = get_scheduler().snapshot()
41
  finished = status.get("finished_at", "n/a")
@@ -47,17 +59,13 @@ def build_status_line(status: dict) -> str:
47
  elif eval_status == "disabled":
48
  live_hint = f"**Live eval off** — {eval_message}"
49
  else:
50
- secs = get_scheduler().seconds_until_next_run()
51
- next_hint = f" · Next eval in ~{secs}s" if secs is not None else ""
52
  live_hint = (
53
  f"**Live eval** · Last run `{finished}` · "
54
- f"collect + eval every **{BENCHMARK_INTERVAL_SECONDS // 60} min**{next_hint}"
 
55
  )
56
 
57
- return (
58
- f"{live_hint} · "
59
- f"Display reloads every {DISPLAY_REFRESH_SECONDS}s"
60
- )
61
 
62
 
63
  def refresh_leaderboard() -> tuple:
@@ -68,6 +76,7 @@ def refresh_leaderboard() -> tuple:
68
  prepare_values_table(grouped["overall_values"]),
69
  prepare_ranks_table(grouped["overall_ranks"]),
70
  build_status_line(status),
 
71
  ]
72
  for domain in DOMAINS:
73
  values_df = grouped["domain_values"].get(domain)
@@ -83,19 +92,37 @@ def refresh_leaderboard() -> tuple:
83
  return tuple(outputs)
84
 
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  grouped_initial = get_grouped_dfs(RESULTS_PATH)
87
  DOMAINS: list[str] = grouped_initial["domains"]
88
  INITIAL_OUTPUTS = refresh_leaderboard()
89
 
90
  demo = gr.Blocks(
91
- css=build_custom_css(DISPLAY_REFRESH_SECONDS),
92
- head=refresh_countdown_head_js(DISPLAY_REFRESH_SECONDS),
93
  )
94
  with demo:
95
  gr.HTML(TITLE)
96
  gr.Markdown(INTRODUCTION_TEXT, elem_classes="markdown-text")
97
  status = gr.Markdown(INITIAL_OUTPUTS[2], elem_classes="markdown-text")
98
- gr.HTML(refresh_countdown_markup(DISPLAY_REFRESH_SECONDS))
99
 
100
  domain_tables: list[tuple[gr.Dataframe, gr.Dataframe]] = []
101
  with gr.Tabs():
@@ -116,13 +143,13 @@ with demo:
116
  for i, domain in enumerate(DOMAINS):
117
  with gr.Tab(domain):
118
  values_df = gr.Dataframe(
119
- value=INITIAL_OUTPUTS[3 + i * 2],
120
  interactive=False,
121
  wrap=True,
122
  label="Metric values (lower is better)",
123
  )
124
  ranks_df = gr.Dataframe(
125
- value=INITIAL_OUTPUTS[4 + i * 2],
126
  interactive=False,
127
  wrap=True,
128
  label="Ranks (lower is better)",
@@ -140,13 +167,13 @@ with demo:
140
  show_copy_button=True,
141
  )
142
 
143
- refresh_outputs = [overall_values_df, overall_ranks_df, status]
144
  for values_df, ranks_df in domain_tables:
145
  refresh_outputs.extend([values_df, ranks_df])
146
 
147
  demo.load(refresh_leaderboard, inputs=[], outputs=refresh_outputs)
148
- gr.Timer(value=DISPLAY_REFRESH_SECONDS).tick(
149
- refresh_leaderboard, inputs=[], outputs=refresh_outputs
150
  )
151
 
152
  demo.queue(default_concurrency_limit=20).launch()
 
11
  LLM_BENCHMARKS_TEXT,
12
  TITLE,
13
  )
14
+ from src.benchmark_config import BENCHMARK_INTERVAL_SECONDS, EVAL_WATCH_POLL_SECONDS
15
  from src.display.css_html_js import (
16
  build_custom_css,
17
+ eval_countdown_head_js,
18
+ eval_countdown_markup,
19
  )
 
20
  from src.eval_scheduler import get_scheduler, start_live_eval_scheduler
21
  from src.utils import (
22
  get_grouped_dfs,
 
28
 
29
  start_live_eval_scheduler()
30
 
31
+ _last_seen_eval_token: str | None = None
32
+
33
 
34
  def load_online_status(results_path: str = RESULTS_PATH) -> dict:
35
  status_path = Path(results_path) / "online_status.json"
 
38
  return json.loads(status_path.read_text())
39
 
40
 
41
+ def build_countdown_html() -> str:
42
+ scheduler = get_scheduler()
43
+ running = scheduler.is_running()
44
+ return eval_countdown_markup(
45
+ next_eval_at_unix=scheduler.next_eval_at_unix(),
46
+ interval_seconds=BENCHMARK_INTERVAL_SECONDS,
47
+ running=running,
48
+ )
49
+
50
+
51
  def build_status_line(status: dict) -> str:
52
  eval_state = get_scheduler().snapshot()
53
  finished = status.get("finished_at", "n/a")
 
59
  elif eval_status == "disabled":
60
  live_hint = f"**Live eval off** — {eval_message}"
61
  else:
 
 
62
  live_hint = (
63
  f"**Live eval** · Last run `{finished}` · "
64
+ f"collect + eval every **{BENCHMARK_INTERVAL_SECONDS // 60} min** · "
65
+ f"leaderboard refreshes when each run completes"
66
  )
67
 
68
+ return live_hint
 
 
 
69
 
70
 
71
  def refresh_leaderboard() -> tuple:
 
76
  prepare_values_table(grouped["overall_values"]),
77
  prepare_ranks_table(grouped["overall_ranks"]),
78
  build_status_line(status),
79
+ build_countdown_html(),
80
  ]
81
  for domain in DOMAINS:
82
  values_df = grouped["domain_values"].get(domain)
 
92
  return tuple(outputs)
93
 
94
 
95
+ def maybe_refresh_after_eval() -> tuple:
96
+ """Refresh leaderboard only when eval state changes (start or finish)."""
97
+ global _last_seen_eval_token
98
+
99
+ eval_state = get_scheduler().snapshot()
100
+ token = "|".join(
101
+ [
102
+ str(eval_state.get("status", "")),
103
+ str(eval_state.get("updated_at", "")),
104
+ str(eval_state.get("finished_at", "")),
105
+ ]
106
+ )
107
+ if token == _last_seen_eval_token:
108
+ raise gr.Skip()
109
+ _last_seen_eval_token = token
110
+ return refresh_leaderboard()
111
+
112
+
113
  grouped_initial = get_grouped_dfs(RESULTS_PATH)
114
  DOMAINS: list[str] = grouped_initial["domains"]
115
  INITIAL_OUTPUTS = refresh_leaderboard()
116
 
117
  demo = gr.Blocks(
118
+ css=build_custom_css(),
119
+ head=eval_countdown_head_js(),
120
  )
121
  with demo:
122
  gr.HTML(TITLE)
123
  gr.Markdown(INTRODUCTION_TEXT, elem_classes="markdown-text")
124
  status = gr.Markdown(INITIAL_OUTPUTS[2], elem_classes="markdown-text")
125
+ countdown = gr.HTML(INITIAL_OUTPUTS[3])
126
 
127
  domain_tables: list[tuple[gr.Dataframe, gr.Dataframe]] = []
128
  with gr.Tabs():
 
143
  for i, domain in enumerate(DOMAINS):
144
  with gr.Tab(domain):
145
  values_df = gr.Dataframe(
146
+ value=INITIAL_OUTPUTS[4 + i * 2],
147
  interactive=False,
148
  wrap=True,
149
  label="Metric values (lower is better)",
150
  )
151
  ranks_df = gr.Dataframe(
152
+ value=INITIAL_OUTPUTS[5 + i * 2],
153
  interactive=False,
154
  wrap=True,
155
  label="Ranks (lower is better)",
 
167
  show_copy_button=True,
168
  )
169
 
170
+ refresh_outputs = [overall_values_df, overall_ranks_df, status, countdown]
171
  for values_df, ranks_df in domain_tables:
172
  refresh_outputs.extend([values_df, ranks_df])
173
 
174
  demo.load(refresh_leaderboard, inputs=[], outputs=refresh_outputs)
175
+ gr.Timer(value=EVAL_WATCH_POLL_SECONDS).tick(
176
+ maybe_refresh_after_eval, inputs=[], outputs=refresh_outputs
177
  )
178
 
179
  demo.queue(default_concurrency_limit=20).launch()
src/about.py CHANGED
@@ -14,8 +14,8 @@ LLM_BENCHMARKS_TEXT = """
14
 
15
  1. **Background scheduler** runs every **`TSFM_BENCH_INTERVAL_SECONDS`** (default **3600 = 1 hour**):
16
  TS-Bench collect → TSFM.ai eval → update `results/`.
17
- 2. Each run calls **TSFM.ai** for all configured models.
18
- 3. The Gradio UI reloads tables every **`TSFM_REFRESH_SECONDS`** (default 60s) from disk.
19
 
20
  ### Required HF Space secret
21
 
@@ -29,7 +29,6 @@ LLM_BENCHMARKS_TEXT = """
29
  |---|---|---|
30
  | `TSFM_BENCH_INTERVAL_SECONDS` | `3600` | **Main knob**: seconds between collect+eval cycles (1 h) |
31
  | `TSFM_EVAL_INTERVAL_SECONDS` | (alias) | Same as above if `TSFM_BENCH_INTERVAL_SECONDS` unset |
32
- | `TSFM_REFRESH_SECONDS` | `60` | UI table reload interval (display only) |
33
  | `TS_BENCH_ROOT` | `vendor/ts_bench` | Path to TS-Bench collect scripts |
34
 
35
  ## Metrics
 
14
 
15
  1. **Background scheduler** runs every **`TSFM_BENCH_INTERVAL_SECONDS`** (default **3600 = 1 hour**):
16
  TS-Bench collect → TSFM.ai eval → update `results/`.
17
+ 2. The leaderboard **refreshes once** when each eval run starts or finishes.
18
+ 3. The progress bar shows **time until the next eval cycle**.
19
 
20
  ### Required HF Space secret
21
 
 
29
  |---|---|---|
30
  | `TSFM_BENCH_INTERVAL_SECONDS` | `3600` | **Main knob**: seconds between collect+eval cycles (1 h) |
31
  | `TSFM_EVAL_INTERVAL_SECONDS` | (alias) | Same as above if `TSFM_BENCH_INTERVAL_SECONDS` unset |
 
32
  | `TS_BENCH_ROOT` | `vendor/ts_bench` | Path to TS-Bench collect scripts |
33
 
34
  ## Metrics
src/benchmark_config.py CHANGED
@@ -5,7 +5,6 @@ from __future__ import annotations
5
  import os
6
 
7
  # One full cycle: TS-Bench collect → TSFM.ai eval → write results → leaderboard reload.
8
- # Override with TSFM_BENCH_INTERVAL_SECONDS (preferred) or TSFM_EVAL_INTERVAL_SECONDS.
9
  BENCHMARK_INTERVAL_SECONDS = int(
10
  os.getenv(
11
  "TSFM_BENCH_INTERVAL_SECONDS",
@@ -13,7 +12,7 @@ BENCHMARK_INTERVAL_SECONDS = int(
13
  )
14
  )
15
 
16
- # UI table reload (reads results/ from disk; does not call TSFM.ai).
17
- DISPLAY_REFRESH_SECONDS = int(os.getenv("TSFM_REFRESH_SECONDS", "60"))
18
 
19
  BENCHMARK_INTERVAL_MINUTES = BENCHMARK_INTERVAL_SECONDS // 60
 
5
  import os
6
 
7
  # One full cycle: TS-Bench collect → TSFM.ai eval → write results → leaderboard reload.
 
8
  BENCHMARK_INTERVAL_SECONDS = int(
9
  os.getenv(
10
  "TSFM_BENCH_INTERVAL_SECONDS",
 
12
  )
13
  )
14
 
15
+ # Background poll interval: detect eval completion and refresh UI once (not shown to users).
16
+ EVAL_WATCH_POLL_SECONDS = int(os.getenv("TSFM_EVAL_WATCH_POLL_SECONDS", "15"))
17
 
18
  BENCHMARK_INTERVAL_MINUTES = BENCHMARK_INTERVAL_SECONDS // 60
src/display/css_html_js.py CHANGED
@@ -1,84 +1,106 @@
1
- def build_custom_css(refresh_seconds: int) -> str:
2
- return f"""
3
- .markdown-text {{ font-size: 16px !important; }}
4
- .tab-buttons button {{ font-size: 20px; }}
5
- #leaderboard-table {{ margin-top: 15px; }}
6
- .auto-refresh-wrap {{
7
  margin: 0.75rem 0 1.25rem 0;
8
  padding: 0;
9
- }}
10
- .auto-refresh-label {{
11
  font-size: 0.9rem;
12
  color: #94a3b8;
13
  margin-bottom: 0.45rem;
14
- }}
15
- .auto-refresh-track {{
16
  width: 100%;
17
  height: 8px;
18
  border-radius: 999px;
19
  background: rgba(148, 163, 184, 0.2);
20
  overflow: hidden;
21
- }}
22
- .auto-refresh-fill {{
23
  height: 100%;
24
  width: 0%;
25
  border-radius: 999px;
26
  background: linear-gradient(90deg, #f97316, #fb923c);
27
- animation: tsfm-refresh-progress {refresh_seconds}s linear infinite;
28
- }}
29
- @keyframes tsfm-refresh-progress {{
30
- from {{ width: 0%; }}
31
- to {{ width: 100%; }}
32
- }}
33
  """
34
 
35
 
36
- def refresh_countdown_markup(seconds: int) -> str:
37
- """Static HTML only — Gradio on HF Spaces strips <script> inside gr.HTML."""
 
 
 
 
 
 
38
  return f"""
39
- <div class="auto-refresh-wrap" id="tsfm-refresh-widget">
40
- <div class="auto-refresh-label" id="tsfm-refresh-label">Next display reload in {seconds}s</div>
41
- <div class="auto-refresh-track">
42
- <div class="auto-refresh-fill" id="tsfm-refresh-fill"></div>
 
 
 
43
  </div>
44
  </div>
45
  """
46
 
47
 
48
- def refresh_countdown_head_js(seconds: int) -> str:
49
- """Inject via Blocks(head=...) so countdown JS runs on Hugging Face Spaces."""
50
- return f"""
51
  <script>
52
- (function () {{
53
- const total = {seconds};
 
 
 
 
 
 
 
 
 
 
 
54
 
55
- function render(remaining) {{
56
- const label = document.getElementById("tsfm-refresh-label");
57
- if (label) {{
58
- label.textContent = "Next display reload in " + remaining + "s";
59
- }}
60
- }}
61
 
62
- function startTimer() {{
63
- if (window.__tsfmRefreshTimerId) return;
64
- const label = document.getElementById("tsfm-refresh-label");
65
- if (!label) {{
66
- setTimeout(startTimer, 250);
67
  return;
68
- }}
69
- let remaining = total;
70
- render(remaining);
71
- window.__tsfmRefreshTimerId = setInterval(function () {{
72
- remaining = remaining <= 0 ? total : remaining - 1;
73
- render(remaining);
74
- }}, 1000);
75
- }}
 
 
 
 
 
 
 
 
 
76
 
77
- if (document.readyState === "loading") {{
78
- document.addEventListener("DOMContentLoaded", startTimer);
79
- }} else {{
80
- startTimer();
81
- }}
82
- }})();
83
  </script>
84
  """
 
1
+ def build_custom_css() -> str:
2
+ return """
3
+ .markdown-text { font-size: 16px !important; }
4
+ .tab-buttons button { font-size: 20px; }
5
+ #leaderboard-table { margin-top: 15px; }
6
+ .eval-countdown-wrap {
7
  margin: 0.75rem 0 1.25rem 0;
8
  padding: 0;
9
+ }
10
+ .eval-countdown-label {
11
  font-size: 0.9rem;
12
  color: #94a3b8;
13
  margin-bottom: 0.45rem;
14
+ }
15
+ .eval-countdown-track {
16
  width: 100%;
17
  height: 8px;
18
  border-radius: 999px;
19
  background: rgba(148, 163, 184, 0.2);
20
  overflow: hidden;
21
+ }
22
+ .eval-countdown-fill {
23
  height: 100%;
24
  width: 0%;
25
  border-radius: 999px;
26
  background: linear-gradient(90deg, #f97316, #fb923c);
27
+ transition: width 1s linear;
28
+ }
 
 
 
 
29
  """
30
 
31
 
32
+ def eval_countdown_markup(next_eval_at_unix: int, interval_seconds: int, running: bool = False) -> str:
33
+ if running:
34
+ label = "Evaluation in progress…"
35
+ progress = 0
36
+ else:
37
+ label = "Next eval cycle loading…"
38
+ progress = 0
39
+
40
  return f"""
41
+ <div class="eval-countdown-wrap" id="tsfm-eval-countdown"
42
+ data-next-at="{next_eval_at_unix}"
43
+ data-interval="{interval_seconds}"
44
+ data-running="{'1' if running else '0'}">
45
+ <div class="eval-countdown-label" id="tsfm-eval-countdown-label">{label}</div>
46
+ <div class="eval-countdown-track">
47
+ <div class="eval-countdown-fill" id="tsfm-eval-countdown-fill" style="width: {progress}%;"></div>
48
  </div>
49
  </div>
50
  """
51
 
52
 
53
+ def eval_countdown_head_js() -> str:
54
+ """Countdown to next eval cycle; progress bar fills as the interval elapses."""
55
+ return """
56
  <script>
57
+ (function () {
58
+ function formatRemaining(totalSeconds) {
59
+ const h = Math.floor(totalSeconds / 3600);
60
+ const m = Math.floor((totalSeconds % 3600) / 60);
61
+ const s = totalSeconds % 60;
62
+ if (h > 0) {
63
+ return h + "h " + m + "m " + s + "s";
64
+ }
65
+ if (m > 0) {
66
+ return m + "m " + s + "s";
67
+ }
68
+ return s + "s";
69
+ }
70
 
71
+ function tick() {
72
+ const root = document.getElementById("tsfm-eval-countdown");
73
+ const label = document.getElementById("tsfm-eval-countdown-label");
74
+ const fill = document.getElementById("tsfm-eval-countdown-fill");
75
+ if (!root || !label || !fill) return;
 
76
 
77
+ if (root.dataset.running === "1") {
78
+ label.textContent = "Evaluation in progress…";
79
+ fill.style.width = "0%";
 
 
80
  return;
81
+ }
82
+
83
+ const nextAt = parseInt(root.dataset.nextAt || "0", 10);
84
+ const interval = parseInt(root.dataset.interval || "3600", 10);
85
+ const remaining = Math.max(0, nextAt - Math.floor(Date.now() / 1000));
86
+ const elapsed = Math.max(0, interval - remaining);
87
+ const pct = interval > 0 ? Math.min(100, (elapsed / interval) * 100) : 0;
88
+
89
+ label.textContent = "Next eval in " + formatRemaining(remaining);
90
+ fill.style.width = pct + "%";
91
+ }
92
+
93
+ function start() {
94
+ if (window.__tsfmEvalCountdownId) return;
95
+ tick();
96
+ window.__tsfmEvalCountdownId = setInterval(tick, 1000);
97
+ }
98
 
99
+ if (document.readyState === "loading") {
100
+ document.addEventListener("DOMContentLoaded", start);
101
+ } else {
102
+ start();
103
+ }
104
+ })();
105
  </script>
106
  """
src/eval_scheduler.py CHANGED
@@ -12,7 +12,7 @@ import time
12
  from datetime import datetime, timezone
13
  from pathlib import Path
14
 
15
- from src.benchmark_config import BENCHMARK_INTERVAL_SECONDS, DISPLAY_REFRESH_SECONDS
16
 
17
  logger = logging.getLogger(__name__)
18
 
@@ -76,6 +76,15 @@ class LiveEvalScheduler:
76
  return None
77
  return max(0, int(self._next_run_at - time.monotonic()))
78
 
 
 
 
 
 
 
 
 
 
79
  def _loop(self) -> None:
80
  if self.run_on_startup:
81
  self._run_once()
 
12
  from datetime import datetime, timezone
13
  from pathlib import Path
14
 
15
+ from src.benchmark_config import BENCHMARK_INTERVAL_SECONDS
16
 
17
  logger = logging.getLogger(__name__)
18
 
 
76
  return None
77
  return max(0, int(self._next_run_at - time.monotonic()))
78
 
79
+ def next_eval_at_unix(self) -> int:
80
+ remaining = self.seconds_until_next_run()
81
+ if remaining is None:
82
+ return int(time.time()) + self.interval_seconds
83
+ return int(time.time()) + remaining
84
+
85
+ def is_running(self) -> bool:
86
+ return self.snapshot().get("status") == "running"
87
+
88
  def _loop(self) -> None:
89
  if self.run_on_startup:
90
  self._run_once()