Spaces:
Sleeping
Sleeping
zhouziyu02 Cursor commited on
Commit ·
62f5d66
1
Parent(s): 5063745
UI: eval countdown bar, refresh leaderboard only on eval complete.
Browse filesRemove 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 +0 -1
- app.py +45 -18
- src/about.py +2 -3
- src/benchmark_config.py +2 -3
- src/display/css_html_js.py +76 -54
- src/eval_scheduler.py +10 -1
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 |
-
|
| 17 |
-
|
| 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**
|
|
|
|
| 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(
|
| 92 |
-
head=
|
| 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(
|
| 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[
|
| 120 |
interactive=False,
|
| 121 |
wrap=True,
|
| 122 |
label="Metric values (lower is better)",
|
| 123 |
)
|
| 124 |
ranks_df = gr.Dataframe(
|
| 125 |
-
value=INITIAL_OUTPUTS[
|
| 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=
|
| 149 |
-
|
| 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.
|
| 18 |
-
3. The
|
| 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 |
-
#
|
| 17 |
-
|
| 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(
|
| 2 |
-
return
|
| 3 |
-
.markdown-text {
|
| 4 |
-
.tab-buttons button {
|
| 5 |
-
#leaderboard-table {
|
| 6 |
-
.
|
| 7 |
margin: 0.75rem 0 1.25rem 0;
|
| 8 |
padding: 0;
|
| 9 |
-
}
|
| 10 |
-
.
|
| 11 |
font-size: 0.9rem;
|
| 12 |
color: #94a3b8;
|
| 13 |
margin-bottom: 0.45rem;
|
| 14 |
-
}
|
| 15 |
-
.
|
| 16 |
width: 100%;
|
| 17 |
height: 8px;
|
| 18 |
border-radius: 999px;
|
| 19 |
background: rgba(148, 163, 184, 0.2);
|
| 20 |
overflow: hidden;
|
| 21 |
-
}
|
| 22 |
-
.
|
| 23 |
height: 100%;
|
| 24 |
width: 0%;
|
| 25 |
border-radius: 999px;
|
| 26 |
background: linear-gradient(90deg, #f97316, #fb923c);
|
| 27 |
-
|
| 28 |
-
}
|
| 29 |
-
@keyframes tsfm-refresh-progress {{
|
| 30 |
-
from {{ width: 0%; }}
|
| 31 |
-
to {{ width: 100%; }}
|
| 32 |
-
}}
|
| 33 |
"""
|
| 34 |
|
| 35 |
|
| 36 |
-
def
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
return f"""
|
| 39 |
-
<div class="
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
| 43 |
</div>
|
| 44 |
</div>
|
| 45 |
"""
|
| 46 |
|
| 47 |
|
| 48 |
-
def
|
| 49 |
-
"""
|
| 50 |
-
return
|
| 51 |
<script>
|
| 52 |
-
(function () {
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
-
function
|
| 56 |
-
const
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
}}
|
| 61 |
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
if (!label) {{
|
| 66 |
-
setTimeout(startTimer, 250);
|
| 67 |
return;
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
-
if (document.readyState === "loading") {
|
| 78 |
-
document.addEventListener("DOMContentLoaded",
|
| 79 |
-
}
|
| 80 |
-
|
| 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
|
| 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()
|