Cyprien Claude Opus 5 (1M context) commited on
Commit
787b0b5
·
1 Parent(s): 9668975

Default to the newest backbone and show when it was pushed

Browse files

The startup model was repos[0] of an alphabetically sorted list, so the
default never moved off breakdown-risk-granite-* however recently another
backbone had trained. model_repos now returns {repo: last_modified}, keyed
alphabetically so the dropdown stays scannable, and the initial value is
max() over the timestamps. Revision default is unchanged, so the pair is
"newest backbone @ its tip".

A line under the pickers names that date and follows the selection, which
makes the default legible -- and exposes the caveat that last_modified is
repo-level, so a README push moves the default too.

list_models sort is what makes the Hub populate last_modified at all, but
newest() uses max() rather than trusting the response order, since hub 1.x
dropped the direction argument. That API is also why the floor moves to
huggingface-hub>=1: on 0.x the key was "lastModified" and descending order
needed direction=-1. Resolved versions are untouched, hub stays at 1.25.1.

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

Files changed (7) hide show
  1. README.md +8 -0
  2. app/handlers.py +12 -8
  3. app/hub.py +9 -2
  4. app/text.py +9 -0
  5. app/ui.py +19 -18
  6. pyproject.toml +1 -1
  7. uv.lock +1 -1
README.md CHANGED
@@ -56,6 +56,14 @@ listés au démarrage, et chaque commit du dépôt choisi est proposé comme
56
  révision — de quoi comparer deux entraînements successifs sans redéployer. `↻`
57
  recharge la liste après un nouveau push de `train.py`.
58
 
 
 
 
 
 
 
 
 
59
  Les deux sorties de `train.py` sont acceptées : la présence de `model_head.pkl`
60
  sélectionne le chemin SetFit, sinon le modèle est chargé comme un
61
  `AutoModelForSequenceClassification`.
 
56
  révision — de quoi comparer deux entraînements successifs sans redéployer. `↻`
57
  recharge la liste après un nouveau push de `train.py`.
58
 
59
+ La liste reste triée par nom, pour qu'un dépôt garde sa place quand on la
60
+ parcourt, mais la sélection initiale est le dépôt poussé le plus récemment,
61
+ sur `main` : au démarrage la démo montre donc le dernier entraînement, quel
62
+ que soit le backbone. La date de ce dernier push est affichée sous les deux
63
+ menus et suit le dépôt sélectionné, ce qui rend le choix par défaut lisible.
64
+ Attention, `last_modified` vaut pour le dépôt entier — une correction de
65
+ README suffit à déplacer le choix par défaut, et la date affichée le montre.
66
+
67
  Les deux sorties de `train.py` sont acceptées : la présence de `model_head.pkl`
68
  sélectionne le chemin SetFit, sinon le modèle est chargé comme un
69
  `AutoModelForSequenceClassification`.
app/handlers.py CHANGED
@@ -7,9 +7,9 @@ import gradio as gr
7
 
8
  from . import evaluation
9
  from .config import display
10
- from .hub import model_repos, revisions
11
  from .predictors import load
12
- from .text import LOADING, NO_MODEL, SCORING
13
  from .turns import window
14
 
15
  EXAMPLES = json.loads((Path(__file__).parent / "examples.json").read_text(encoding="utf-8"))
@@ -49,18 +49,22 @@ def evaluate(repo: str, revision: str, progress=gr.Progress()):
49
  return summary(report), report.confusion, report.cases
50
 
51
 
52
- def pick_revision(repo: str) -> gr.Dropdown:
53
  choices = revisions(repo)
54
- return gr.Dropdown(choices=choices, value=choices[0][1] if choices else None)
 
 
 
55
 
56
 
57
- def refresh(repo: str, revision: str) -> tuple[gr.Dropdown, gr.Dropdown]:
58
- repos = model_repos()
59
- chosen = repo if repo in repos else (repos[0] if repos else None)
60
  choices = revisions(chosen) if chosen else []
61
  shas = [sha for _, sha in choices]
62
  keep = revision if revision in shas else (shas[0] if shas else None)
63
  return (
64
- gr.Dropdown(choices=repos, value=chosen),
65
  gr.Dropdown(choices=choices, value=keep),
 
66
  )
 
7
 
8
  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"))
 
49
  return summary(report), report.confusion, report.cases
50
 
51
 
52
+ def pick_revision(repo: str) -> tuple[gr.Dropdown, str]:
53
  choices = revisions(repo)
54
+ return (
55
+ gr.Dropdown(choices=choices, value=choices[0][1] if choices else None),
56
+ pushed_at(model_repos().get(repo)),
57
+ )
58
 
59
 
60
+ def refresh(repo: str, revision: str) -> tuple[gr.Dropdown, gr.Dropdown, str]:
61
+ pushes = model_repos()
62
+ chosen = repo if repo in pushes else newest(pushes)
63
  choices = revisions(chosen) if chosen else []
64
  shas = [sha for _, sha in choices]
65
  keep = revision if revision in shas else (shas[0] if shas else None)
66
  return (
67
+ gr.Dropdown(choices=list(pushes), value=chosen),
68
  gr.Dropdown(choices=choices, value=keep),
69
+ pushed_at(pushes[chosen] if chosen else None),
70
  )
app/hub.py CHANGED
@@ -1,4 +1,5 @@
1
  import re
 
2
  from pathlib import Path
3
 
4
  from huggingface_hub import HfApi, hf_hub_download
@@ -17,8 +18,14 @@ def resolve(repo: str, revision: str, repo_type: str = "model") -> str:
17
  return info.sha
18
 
19
 
20
- def model_repos() -> list[str]:
21
- return sorted(model.id for model in api.list_models(author=ORG, search=MODEL_SEARCH))
 
 
 
 
 
 
22
 
23
 
24
  def revisions(repo: str) -> list[tuple[str, str]]:
 
1
  import re
2
+ from datetime import datetime
3
  from pathlib import Path
4
 
5
  from huggingface_hub import HfApi, hf_hub_download
 
18
  return info.sha
19
 
20
 
21
+ def model_repos() -> dict[str, datetime]:
22
+ """Every backbone with the date of its last push, keyed alphabetically."""
23
+ models = api.list_models(author=ORG, search=MODEL_SEARCH, sort="last_modified")
24
+ return {model.id: model.last_modified for model in sorted(models, key=lambda m: m.id)}
25
+
26
+
27
+ def newest(pushes: dict[str, datetime]) -> str | None:
28
+ return max(pushes, key=lambda repo: pushes[repo], default=None)
29
 
30
 
31
  def revisions(repo: str) -> list[tuple[str, str]]:
app/text.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  from .config import CALLER_TURNS, DATASET_REPO, ORG
2
 
3
  HEADER = f"""
@@ -22,3 +24,10 @@ TRANSCRIPT_INFO = (
22
  NO_MODEL = "Aucun modèle sélectionné."
23
  LOADING = "Chargement du modèle"
24
  SCORING = "Classement du split de test"
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+
3
  from .config import CALLER_TURNS, DATASET_REPO, ORG
4
 
5
  HEADER = f"""
 
24
  NO_MODEL = "Aucun modèle sélectionné."
25
  LOADING = "Chargement du modèle"
26
  SCORING = "Classement du split de test"
27
+
28
+
29
+ def pushed_at(when: datetime | None) -> str:
30
+ """Line under the pickers: when the selected repo was last touched."""
31
+ if when is None:
32
+ return ""
33
+ return f"<sub>Dépôt mis à jour le {when:%d/%m/%Y à %H:%M} UTC</sub>"
app/ui.py CHANGED
@@ -4,24 +4,26 @@ 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, revisions
8
- from .text import HEADER, PLACEHOLDER, TRANSCRIPT_INFO
9
 
10
  CMD_ENTER_JS = (Path(__file__).parent / "cmd_enter.js").read_text(encoding="utf-8")
11
 
12
 
13
- def selectors() -> tuple[gr.Dropdown, gr.Dropdown, gr.Button]:
14
- repos = model_repos()
15
- repo = repos[0] if repos else None
16
  revs = revisions(repo) if repo else []
17
- model = gr.Dropdown(label="Modèle", choices=repos, value=repo, scale=3)
18
- revision = gr.Dropdown(
19
- label="Révision",
20
- choices=revs,
21
- value=revs[0][1] if revs else None,
22
- scale=4,
23
- )
24
- return model, revision, gr.Button("↻", scale=0, min_width=48)
 
 
25
 
26
 
27
  def classify_tab() -> tuple[gr.Textbox, gr.Button, list]:
@@ -77,8 +79,7 @@ def build() -> gr.Blocks:
77
  with gr.Blocks(title="Risque d'immobilisation") as demo:
78
  gr.Markdown(HEADER)
79
 
80
- with gr.Row():
81
- model, revision, refresh_button = selectors()
82
  selection = [model, revision]
83
 
84
  with gr.Tab("Classer"):
@@ -88,9 +89,9 @@ def build() -> gr.Blocks:
88
  evaluate_button, report, progress_target = evaluation_tab()
89
 
90
  timer = gr.Timer(REFRESH_SECONDS)
91
- timer.tick(refresh, selection, selection, show_progress="hidden")
92
- refresh_button.click(refresh, selection, selection)
93
- model.change(pick_revision, model, revision)
94
  gr.on(
95
  [run.click, transcript.submit],
96
  classify,
 
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
 
12
 
13
+ def selectors() -> tuple[gr.Dropdown, gr.Dropdown, gr.Button, gr.Markdown]:
14
+ pushes = model_repos()
15
+ repo = newest(pushes)
16
  revs = revisions(repo) if repo else []
17
+ with gr.Row():
18
+ model = gr.Dropdown(label="Modèle", choices=list(pushes), value=repo, scale=3)
19
+ revision = gr.Dropdown(
20
+ label="Révision",
21
+ choices=revs,
22
+ value=revs[0][1] if revs else None,
23
+ scale=4,
24
+ )
25
+ reload_button = gr.Button("↻", scale=0, min_width=48)
26
+ return model, revision, reload_button, gr.Markdown(pushed_at(pushes[repo] if repo else None))
27
 
28
 
29
  def classify_tab() -> tuple[gr.Textbox, gr.Button, list]:
 
79
  with gr.Blocks(title="Risque d'immobilisation") as demo:
80
  gr.Markdown(HEADER)
81
 
82
+ model, revision, refresh_button, pushed = selectors()
 
83
  selection = [model, revision]
84
 
85
  with gr.Tab("Classer"):
 
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,
pyproject.toml CHANGED
@@ -11,7 +11,7 @@ dependencies = [
11
  "datasets>=3",
12
  "scikit-learn>=1.5",
13
  "joblib>=1.4",
14
- "huggingface-hub>=0.30",
15
  "torch>=2.2",
16
  "numpy>=2",
17
  ]
 
11
  "datasets>=3",
12
  "scikit-learn>=1.5",
13
  "joblib>=1.4",
14
+ "huggingface-hub>=1",
15
  "torch>=2.2",
16
  "numpy>=2",
17
  ]
uv.lock CHANGED
@@ -127,7 +127,7 @@ dependencies = [
127
  requires-dist = [
128
  { name = "datasets", specifier = ">=3" },
129
  { name = "gradio", specifier = "==6.20.0" },
130
- { name = "huggingface-hub", specifier = ">=0.30" },
131
  { name = "joblib", specifier = ">=1.4" },
132
  { name = "numpy", specifier = ">=2" },
133
  { name = "scikit-learn", specifier = ">=1.5" },
 
127
  requires-dist = [
128
  { name = "datasets", specifier = ">=3" },
129
  { name = "gradio", specifier = "==6.20.0" },
130
+ { name = "huggingface-hub", specifier = ">=1" },
131
  { name = "joblib", specifier = ">=1.4" },
132
  { name = "numpy", specifier = ">=2" },
133
  { name = "scikit-learn", specifier = ">=1.5" },