Spaces:
Sleeping
Fetch the picked model up front, with Classify dead until it lands
Browse filesClicking 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>
- README.md +18 -0
- app/handlers.py +23 -3
- app/text.py +5 -0
- app/ui.py +16 -5
|
@@ -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
|
|
@@ -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"
|
|
@@ -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."""
|
|
@@ -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(
|
|
|
|
|
|
|
| 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(
|
| 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,
|