Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions .github/workflows/site.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
name: Site

# Construit le site Quarto (`website/`) et le publie sur GitHub Pages.
#
# Les pages « Résultats » et « Écarts » RE-EXÉCUTENT leur code Python au rendu :
# elles relisent les prédictions exportées sur le S3 du SSP Cloud (le cache
# `website/_freeze/` n'est volontairement pas versionné, cf. `.gitignore`).
# Le runner a donc besoin des identifiants MinIO, fournis en secrets du dépôt :
#
# AWS_ACCESS_KEY_ID ┐ jetons MinIO du SSP Cloud
# AWS_SECRET_ACCESS_KEY │ (page « Mon compte » → « Connexion au stockage »)
# AWS_SESSION_TOKEN ┘ ⚠ ils EXPIRENT (7 jours max) : à renouveler.
#
# Optionnels, avec valeurs par défaut ci-dessous :
# AWS_S3_ENDPOINT (variable), S3_PREDICTIONS_PREFIX (secret ou variable)
#
# Sans identifiants valides le site se construit quand même : les pages
# concernées affichent « — » et une note d'incident. Le déploiement n'échoue pas.

on:
push:
branches: [main]
paths:
- "website/**"
- "src/**"
- "configs/**"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/site.yml"
workflow_dispatch: # publication manuelle (ex. après renouvellement des jetons)

# Droits nécessaires au déploiement par artefact (pas de branche gh-pages).
permissions:
contents: read
pages: write
id-token: write

# Une seule publication à la fois ; on ne coupe pas un déploiement en cours.
concurrency:
group: pages
cancel-in-progress: false

jobs:
construire:
runs-on: ubuntu-latest
# Environnement commun aux étapes de vérification et de rendu (GitHub
# Actions ne prend pas en charge les ancres YAML : on factorise ici).
env:
# ── Accès au S3 du SSP Cloud (MinIO) ────────────────────────────────
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# Vide avec un compte de service (pas de jeton de session) : botocore
# normalise alors la variable en `None`, c'est sans effet.
AWS_SESSION_TOKEN: ${{ secrets.AWS_SESSION_TOKEN }}
AWS_DEFAULT_REGION: us-east-1
# `fsspec`/`s3fs` lisent l'endpoint dans l'environnement. `AWS_S3_ENDPOINT`
# seul ne suffit PAS : botocore l'ignore et route vers s3.amazonaws.com.
# C'est `AWS_ENDPOINT_URL` qui dirige vers MinIO (vérifié sur ce projet).
AWS_S3_ENDPOINT: ${{ vars.AWS_S3_ENDPOINT || 'minio.lab.sspcloud.fr' }}
AWS_ENDPOINT_URL: https://${{ vars.AWS_S3_ENDPOINT || 'minio.lab.sspcloud.fr' }}
FSSPEC_S3_ENDPOINT_URL: https://${{ vars.AWS_S3_ENDPOINT || 'minio.lab.sspcloud.fr' }}
# ── Paramètres des pages de résultats (cf. website/_analyse.py) ─────
# Valeur explicite : une variable vide écraserait le défaut du module.
S3_PREDICTIONS_PREFIX: ${{ vars.S3_PREDICTIONS_PREFIX || 's3://projet-production-ecrits-depp/predictions' }}
steps:
- uses: actions/checkout@v4

# Bascule Pages en mode « GitHub Actions » (déploiement par artefact).
# Sans ça, si la source du dépôt est restée sur « Deploy from a branch »,
# le job de déploiement boucle sur `deployment_in_progress` puis échoue :
# il attend un build de branche qui n'arrive jamais.
- name: Configurer Pages
uses: actions/configure-pages@v5

- name: Installer Quarto
uses: quarto-dev/quarto-actions/setup@v2
with:
version: "1.10.18" # même version qu'en local, pour un rendu identique

- name: Installer uv
uses: astral-sh/setup-uv@v3
with:
enable-cache: true

- name: Installer Python et dépendances
run: |
uv python install 3.11
# `--extra website` apporte matplotlib et le noyau Jupyter dont le
# rendu a besoin ; le paquet du projet fournit les calculs.
uv sync --extra website

# `_analyse.py` capture toute erreur S3 pour garder les pages rendables :
# un échec d'accès ne se voit alors que par des « — » sur le site publié.
# Cette étape fait remonter l'erreur. Non bloquante : mieux vaut republier
# un site incomplet que ne rien publier.
#
# Le résultat est écrit dans `$GITHUB_STEP_SUMMARY`, donc affiché sur la
# page de résumé du run : inutile d'aller déplier les logs du job.
- name: Vérifier l'accès au S3
continue-on-error: true
# Seules les LONGUEURS des secrets sont affichées, jamais leur valeur.
run: |
{
echo "clé : ${#AWS_ACCESS_KEY_ID} caractères"
echo "secret : ${#AWS_SECRET_ACCESS_KEY} caractères"
echo "jeton : ${#AWS_SESSION_TOKEN} caractères"
echo "endpoint : ${AWS_ENDPOINT_URL}"
echo "préfixe : ${S3_PREDICTIONS_PREFIX}"
uv run python -c "
import os, fsspec
prefixe = os.environ['S3_PREDICTIONS_PREFIX']
fs = fsspec.filesystem('s3')
print('endpoint résolu :', fs.s3.meta.endpoint_url)
for entree in fs.ls(prefixe, detail=False):
print(' ', entree)
" 2>&1
} > "${RUNNER_TEMP}/diagnostic-s3.txt" || true
cat "${RUNNER_TEMP}/diagnostic-s3.txt"
{
echo "## Diagnostic S3"
echo '```'
cat "${RUNNER_TEMP}/diagnostic-s3.txt"
echo '```'
} >> "${GITHUB_STEP_SUMMARY}"

- name: Rendre le site
# Rendu depuis la RACINE du dépôt : `_analyse.py` remonte jusqu'au
# dossier contenant `src/evaluation_dictee` pour importer le paquet et
# résoudre `configs/grille_dictee_2015.json`.
run: uv run quarto render website

- name: Préparer l'artefact Pages
uses: actions/upload-pages-artifact@v3
with:
path: website/_site

deployer:
needs: construire
runs-on: ubuntu-latest
# Un déploiement courant prend moins d'une minute, mais la toute première
# publication d'un site (provision CDN + certificat TLS) peut dépasser
# 8 minutes : on garde de la marge pour ne pas tuer un déploiement valide.
timeout-minutes: 20
environment:
name: github-pages
url: ${{ steps.deploiement.outputs.page_url }}
steps:
- name: Déployer sur GitHub Pages
id: deploiement
uses: actions/deploy-pages@v4
41 changes: 30 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,6 @@ uv run mypy src # typage
# Lancer un benchmark à partir d'une config
uv run scripts/run_benchmark.py --config configs/scoring/dictee_REFERENCE.yaml

# Densité d'encre de chaque copie (détection des copies vierges, sans appel modèle).
# Documente `data.blank_ink_threshold` et alimente la page « Écarts » du site.
uv run scripts/compute_ink_ratios.py --config configs/scoring/dictee_end2end.yaml --export

# Rendre le site Quarto (les pages recalculent leurs figures au rendu)
uv sync --extra website
uv run quarto render website
Expand Down Expand Up @@ -196,25 +192,48 @@ screen -ls # lister les session

**Option 2 — nohup.** Sans interface interactive, log dans un fichier.
```bash
ps -ef | grep run_benchmark | grep -v grep # AVANT tout : rien ne doit tourner
mkdir -p logs
nohup uv run scripts/run_benchmark.py --config configs/scoring/dictee_REFERENCE.yaml \
> logs/dictee_REFERENCE.log 2>&1 &
echo $! > logs/dictee_REFERENCE.pid # noter le PID pour arrêter plus tard
tail -f logs/dictee_REFERENCE.log # suivre le log en direct

# Un log DISTINCT par run : deux runs qui partagent un log sont illisibles après coup.
nohup uv run scripts/run_benchmark.py --config configs/scoring/dictee_end2end.yaml \
> logs/dictee_end2end.log 2>&1 &
nohup uv run scripts/run_benchmark.py --config configs/scoring/dictee_two_stage.yaml \
> logs/dictee_two_stage.log 2>&1 &

tail -f logs/dictee_end2end.log # suivre en direct
head -20 logs/dictee_end2end.log | grep -i reprise # la reprise a-t-elle pris ?

# Arrêter : PAS `kill $(cat *.pid)`. `nohup uv run …` crée un wrapper `uv run` ET un
# `python3 scripts/run_benchmark.py` ; `$!` ne capture que le wrapper, dont le kill
# laisserait l'enfant orphelin continuer d'écrire (cause de runs concurrents observée).
pkill -f "run_benchmark.py --config configs/scoring/dictee_end2end.yaml"
```

> **Un seul run par fichier de sortie.** Deux runs qui appendent le même JSONL
> dupliquent les copies et faussent les métriques. Un verrou `flock` sur
> `<sortie>.lock` fait échouer le second dès le démarrage. Si un run annonce
> `0 déjà faites` alors qu'un checkpoint existe, l'arrêter : le nom du fichier de
> sortie ne correspond pas au checkpoint (modèle ou `name` différent).

### Checkpointing et reprise après crash

Depuis la refonte du benchmark, **chaque copie est écrite sur disque
immédiatement** (`flush + fsync`). Effets :

- Un crash à mi-run (API down, kernel tué, …) ne perd que la copie en cours.
- Relancer la même commande **reprend automatiquement** où on s'était arrêté :
les copies déjà présentes dans `<run>_predictions.jsonl` sont sautées.
les copies déjà présentes dans `<run>_<modele>_predictions.jsonl` sont sautées.
- Les copies qui lèvent une exception API sont loggées dans
`<run>_failed_copies.txt` et le run continue sur les suivantes. Elles seront
`<run>_<modele>_failed_copies.txt` et le run continue sur les suivantes. Elles seront
retentées au prochain lancement.
- Pour repartir de zéro, supprimer `<run>_predictions.jsonl` (ou changer
- Une copie dont **aucun** item n'a pu être parsé (code `?` partout : réponse vide,
JSON cassé) n'est PAS considérée comme faite. `pipeline/purge.preparer_reprise`
la retire du JSONL au démarrage du run — sauvegarde en `<fichier>.bak` — et le run
la refait. Sans ce garde-fou, la reprise fige les échecs : c'est ce qui a laissé
84 copies entièrement `?` dans le run two_stage du 5 août 2026, alors que leur
cause avait été corrigée entre les deux lancements.
- Pour repartir de zéro, supprimer `<run>_<modele>_predictions.jsonl` (ou changer
`config.name`).

## 10. Pour un⋅e débutant⋅e
Expand Down
Loading
Loading