Cyprien Claude Opus 5 (1M context) commited on
Commit
71a9867
·
1 Parent(s): b42781c

Fetch the picked model up front, with Classify dead until it lands

Browse files

Clicking Classify used to mean "download the model, then maybe run inference",
with nothing on screen saying which was happening -- and the ms reading next to
the prediction quietly included the download, so the first number of a session
was a network measurement dressed as a latency.

Both action buttons now start disabled and load() runs up front: on page load,
on picking a model, and on picking a commit. Classify reads "Downloading the
model..." and is greyed while it runs, so a click is only ever inference, and
the timer starts after load so the ms means what it says.

The wiring keeps it to one warm per pick. model.change updates the revision
menu and then warms, so it reads the revision the switch just chose rather than
the old repo's commit -- a sha from the previous repo would 404. The revision
menu warms on .input rather than .change, because .change also fires on the
programmatic update model.change just made, and on the 30s refresh, either of
which would re-warm and flicker the buttons.

A revision that fails to load hands the buttons back and reports why, so a bad
pick cannot leave a dead interface.

Shift+Enter still submits during a load: you can paste a transcript while the
model downloads, and an early keypress waits on the same cached load rather
than starting a second one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Files changed (4) hide show
  1. README.md +18 -0
  2. app/handlers.py +23 -3
  3. app/text.py +5 -0
  4. app/ui.py +16 -5
README.md CHANGED
@@ -77,6 +77,24 @@ the SetFit path, otherwise the model is loaded as an
77
  The **Evaluation** tab replays the test split and shows the confusion matrix,
78
  then the cases one by one, errors first.
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  ## Nothing is ever stale
81
 
82
  `main` is resolved to a commit on every call, and the caches are keyed on the
 
77
  The **Evaluation** tab replays the test split and shows the confusion matrix,
78
  then the cases one by one, errors first.
79
 
80
+ ## Clicking Classify never downloads
81
+
82
+ Both action buttons start disabled, and the picked revision is fetched up front:
83
+ on page load, on picking a model, and on picking a commit. While that runs,
84
+ Classify reads *Downloading the model…* and is greyed out. So a click is only
85
+ ever inference, and the `ms` reading next to the prediction is measured after the
86
+ load — it is inference alone, not a download that happened to be first.
87
+
88
+ Warming is wired so it runs once per pick. `model.change` updates the revision
89
+ menu and *then* warms, so it reads the revision the switch just chose rather than
90
+ the previous repo's commit; the revision menu warms on `.input`, which is
91
+ user-only, so the programmatic update that `model.change` just made does not warm
92
+ a second time. A revision that fails to load hands the buttons back and reports
93
+ why, instead of leaving a dead interface.
94
+
95
+ ⇧ Enter still submits while a model is loading — you can paste a transcript
96
+ during the download, and an early keypress simply waits on the same load.
97
+
98
  ## Nothing is ever stale
99
 
100
  `main` is resolved to a commit on every call, and the caches are keyed on the
app/handlers.py CHANGED
@@ -9,7 +9,7 @@ from . import evaluation
9
  from .config import display
10
  from .hub import model_repos, newest, revisions
11
  from .predictors import load
12
- from .text import LOADING, NO_MODEL, SCORING, pushed_at
13
  from .turns import window
14
 
15
  EXAMPLES = json.loads((Path(__file__).parent / "examples.json").read_text(encoding="utf-8"))
@@ -21,14 +21,34 @@ def classify(repo: str, revision: str, transcript: str) -> tuple[dict[str, float
21
  if not repo or not text:
22
  return {}, "", ""
23
 
24
- started = time.perf_counter()
25
  predictor, sha = load(repo, revision)
 
26
  labels, probabilities = predictor([text])
27
- scores = {display(name): float(p) for name, p in zip(labels, probabilities[0])}
28
  elapsed = (time.perf_counter() - started) * 1000
 
29
  return scores, text, f"`{sha[:7]}` · `{elapsed:.0f} ms`"
30
 
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  def summary(report: evaluation.Report) -> str:
33
  return (
34
  f"### {report.hits}/{report.total} — accuracy {report.hits / report.total:.1%}\n"
 
9
  from .config import display
10
  from .hub import model_repos, newest, revisions
11
  from .predictors import load
12
+ from .text import CLASSIFY, EVALUATE, LOADING, NO_MODEL, SCORING, WARM_FAILED, WARMING, pushed_at
13
  from .turns import window
14
 
15
  EXAMPLES = json.loads((Path(__file__).parent / "examples.json").read_text(encoding="utf-8"))
 
21
  if not repo or not text:
22
  return {}, "", ""
23
 
 
24
  predictor, sha = load(repo, revision)
25
+ started = time.perf_counter() # after load, so the reading is inference alone
26
  labels, probabilities = predictor([text])
 
27
  elapsed = (time.perf_counter() - started) * 1000
28
+ scores = {display(name): float(p) for name, p in zip(labels, probabilities[0])}
29
  return scores, text, f"`{sha[:7]}` · `{elapsed:.0f} ms`"
30
 
31
 
32
+ def _actions(classify_label: str, ready: bool) -> tuple[gr.Button, gr.Button]:
33
+ return gr.Button(classify_label, interactive=ready), gr.Button(EVALUATE, interactive=ready)
34
+
35
+
36
+ def warm(repo: str, revision: str):
37
+ """Fetch the picked revision up front, so Classify only ever runs inference."""
38
+ if not repo:
39
+ yield _actions(CLASSIFY, ready=False)
40
+ return
41
+
42
+ yield _actions(WARMING, ready=False)
43
+ try:
44
+ load(repo, revision)
45
+ except Exception as failure:
46
+ # a bad revision must hand the buttons back, not dead-end the interface
47
+ yield _actions(CLASSIFY, ready=True)
48
+ raise gr.Error(f"{WARM_FAILED}: {failure}") from failure
49
+ yield _actions(CLASSIFY, ready=True)
50
+
51
+
52
  def summary(report: evaluation.Report) -> str:
53
  return (
54
  f"### {report.hits}/{report.total} — accuracy {report.hits / report.total:.1%}\n"
app/text.py CHANGED
@@ -26,6 +26,11 @@ NO_MODEL = "No model selected."
26
  LOADING = "Loading the model"
27
  SCORING = "Classifying the test split"
28
 
 
 
 
 
 
29
 
30
  def pushed_at(when: datetime | None) -> str:
31
  """Line under the pickers: when the selected repo was last touched."""
 
26
  LOADING = "Loading the model"
27
  SCORING = "Classifying the test split"
28
 
29
+ CLASSIFY = "Classify"
30
+ EVALUATE = "Evaluate on the test split"
31
+ WARMING = "Downloading the model…"
32
+ WARM_FAILED = "Could not load that revision"
33
+
34
 
35
  def pushed_at(when: datetime | None) -> str:
36
  """Line under the pickers: when the selected repo was last touched."""
app/ui.py CHANGED
@@ -3,9 +3,9 @@ from pathlib import Path
3
  import gradio as gr
4
 
5
  from .config import REFRESH_SECONDS, display
6
- from .handlers import EXAMPLES, classify, evaluate, pick_revision, refresh
7
  from .hub import model_repos, newest, revisions
8
- from .text import HEADER, PLACEHOLDER, TRANSCRIPT_INFO, pushed_at
9
 
10
  CMD_ENTER_JS = (Path(__file__).parent / "cmd_enter.js").read_text(encoding="utf-8")
11
 
@@ -36,7 +36,9 @@ def classify_tab() -> tuple[gr.Textbox, gr.Button, list]:
36
  lines=7,
37
  max_lines=14,
38
  )
39
- run = gr.Button("Classify", variant="primary", elem_id="run-classify")
 
 
40
  gr.Examples(
41
  examples=[[row["text"]] for row in EXAMPLES],
42
  example_labels=[f"{display(row['gold'])} — {row['id']}" for row in EXAMPLES],
@@ -58,7 +60,7 @@ def classify_tab() -> tuple[gr.Textbox, gr.Button, list]:
58
 
59
 
60
  def evaluation_tab() -> tuple[gr.Button, list, gr.Dataframe]:
61
- run = gr.Button("Evaluate on the test split", variant="primary")
62
  score = gr.Markdown()
63
  matrix = gr.Dataframe(
64
  label="Confusion matrix",
@@ -88,10 +90,19 @@ def build() -> gr.Blocks:
88
  with gr.Tab("Evaluation"):
89
  evaluate_button, report, progress_target = evaluation_tab()
90
 
 
 
 
 
 
 
 
91
  timer = gr.Timer(REFRESH_SECONDS)
92
  timer.tick(refresh, selection, [*selection, pushed], show_progress="hidden")
93
  refresh_button.click(refresh, selection, [*selection, pushed])
94
- model.change(pick_revision, model, [revision, pushed])
 
 
95
  gr.on(
96
  [run.click, transcript.submit],
97
  classify,
 
3
  import gradio as gr
4
 
5
  from .config import REFRESH_SECONDS, display
6
+ from .handlers import EXAMPLES, classify, evaluate, pick_revision, refresh, warm
7
  from .hub import model_repos, newest, revisions
8
+ from .text import CLASSIFY, EVALUATE, HEADER, PLACEHOLDER, TRANSCRIPT_INFO, pushed_at
9
 
10
  CMD_ENTER_JS = (Path(__file__).parent / "cmd_enter.js").read_text(encoding="utf-8")
11
 
 
36
  lines=7,
37
  max_lines=14,
38
  )
39
+ run = gr.Button(
40
+ CLASSIFY, variant="primary", elem_id="run-classify", interactive=False
41
+ )
42
  gr.Examples(
43
  examples=[[row["text"]] for row in EXAMPLES],
44
  example_labels=[f"{display(row['gold'])} — {row['id']}" for row in EXAMPLES],
 
60
 
61
 
62
  def evaluation_tab() -> tuple[gr.Button, list, gr.Dataframe]:
63
+ run = gr.Button(EVALUATE, variant="primary", interactive=False)
64
  score = gr.Markdown()
65
  matrix = gr.Dataframe(
66
  label="Confusion matrix",
 
90
  with gr.Tab("Evaluation"):
91
  evaluate_button, report, progress_target = evaluation_tab()
92
 
93
+ # Buttons start dead and the model is fetched up front, so a click is
94
+ # never a download. .input() is user-only, so the revision update that
95
+ # picking a model triggers does not warm a second time.
96
+ actions = [run, evaluate_button]
97
+ demo.load(warm, selection, actions, show_progress="hidden")
98
+ revision.input(warm, selection, actions, show_progress="hidden")
99
+
100
  timer = gr.Timer(REFRESH_SECONDS)
101
  timer.tick(refresh, selection, [*selection, pushed], show_progress="hidden")
102
  refresh_button.click(refresh, selection, [*selection, pushed])
103
+ model.change(pick_revision, model, [revision, pushed]).then(
104
+ warm, selection, actions, show_progress="hidden"
105
+ )
106
  gr.on(
107
  [run.click, transcript.submit],
108
  classify,