Restructuration de l'application
This commit is contained in:
+7
-16
@@ -56,21 +56,10 @@ Les codes de sortie communs sont :
|
|||||||
| 4 | traitement partiel, avec avertissements |
|
| 4 | traitement partiel, avec avertissements |
|
||||||
| 130 | interruption par l'utilisateur |
|
| 130 | interruption par l'utilisateur |
|
||||||
|
|
||||||
Le GUI distingue notamment un traitement partiel d'un échec. Les
|
Le GUI distingue notamment un traitement partiel d'un échec. Toutes les
|
||||||
scripts migrés vers cette convention sont actuellement :
|
commandes du paquet suivent désormais cette convention. Elles sont
|
||||||
|
regroupées dans =copienator.commands= et exposées par le répartiteur
|
||||||
- =copies_tools.py=, =grouping.py= et =verify_groups.py= ;
|
=python -m copienator=.
|
||||||
- =post-correction.py= et =resolve_manual.py= ;
|
|
||||||
- =page_splitter.py=, =cutleft.py=, =plotting.py= et
|
|
||||||
=splitting_int.py= ;
|
|
||||||
- =gemini_for_labels.py= ;
|
|
||||||
- =gemini_for_enonce.py= et =enonce_info.py= ;
|
|
||||||
- =correction.py=, =submit_batches.py=, =batch_status.py= et
|
|
||||||
=fetch_batched_results.py= ;
|
|
||||||
- =annotating.py=, =annotating_with_checks.py= et
|
|
||||||
=annotating_by_label.py= ;
|
|
||||||
- =reading_annotations.py= et =reading_grouped_annotations.py= ;
|
|
||||||
- =export.py=, =import.py= et =giving_names.py=.
|
|
||||||
|
|
||||||
* Organisation du code
|
* Organisation du code
|
||||||
|
|
||||||
@@ -78,8 +67,10 @@ scripts migrés vers cette convention sont actuellement :
|
|||||||
scripts, notamment =EvaluationWorkspace= et les écritures atomiques.
|
scripts, notamment =EvaluationWorkspace= et les écritures atomiques.
|
||||||
- Le paquet =copienator_gui= contient la définition du workflow, l'état
|
- Le paquet =copienator_gui= contient la définition du workflow, l'état
|
||||||
persistant, le lanceur de processus et l'application Tk.
|
persistant, le lanceur de processus et l'application Tk.
|
||||||
- Les scripts situés à la racine restent des points d'entrée autonomes.
|
- Le sous-paquet =copienator.commands= contient les points d'entrée.
|
||||||
Leur import ne doit pas déclencher de traitement.
|
Leur import ne doit pas déclencher de traitement.
|
||||||
|
- =copienator.dispatcher= expose le CLI unifié et =copienator_gui=
|
||||||
|
contient l'application Tk.
|
||||||
|
|
||||||
* État et exécution du GUI
|
* État et exécution du GUI
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -41,8 +41,13 @@ Libraries :
|
|||||||
|
|
||||||
#+BEGIN_SRC bash
|
#+BEGIN_SRC bash
|
||||||
pip install numpy pandas matplotlib pillow pydantic pypdf pdf2image reportlab img2pdf pymupdf ftfy ezodf google
|
pip install numpy pandas matplotlib pillow pydantic pypdf pdf2image reportlab img2pdf pymupdf ftfy ezodf google
|
||||||
|
pip install -e .
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
|
La seconde commande installe les exécutables =copienator= et
|
||||||
|
=copienator-gui=. Sans installation, les mêmes commandes restent
|
||||||
|
accessibles avec =python -m copienator=.
|
||||||
|
|
||||||
*** Poppler (for pdf2image)
|
*** Poppler (for pdf2image)
|
||||||
|
|
||||||
+ Linux : install poppler-utils
|
+ Linux : install poppler-utils
|
||||||
@@ -79,10 +84,10 @@ Copier `default_config.py` en `config.py`. Éventuellement le modifier.
|
|||||||
Lancer l'assistant avec :
|
Lancer l'assistant avec :
|
||||||
|
|
||||||
#+BEGIN_SRC bash
|
#+BEGIN_SRC bash
|
||||||
python gui.py
|
python -m copienator gui
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
On peut aussi ouvrir directement une évaluation avec =python gui.py
|
On peut aussi ouvrir directement une évaluation avec =python -m copienator gui
|
||||||
Interro=. L'interface conserve l'état et l'historique des étapes dans
|
Interro=. L'interface conserve l'état et l'historique des étapes dans
|
||||||
=Interro/.copienator-gui.json=, et les sorties complètes dans
|
=Interro/.copienator-gui.json=, et les sorties complètes dans
|
||||||
=Interro/.copienator/logs/=. Une relance d'une étape antérieure ne
|
=Interro/.copienator/logs/=. Une relance d'une étape antérieure ne
|
||||||
|
|||||||
+60
-61
@@ -11,7 +11,7 @@ les parcours alternatifs.
|
|||||||
|
|
||||||
* Étapes et Script
|
* Étapes et Script
|
||||||
|
|
||||||
Utiliser `python gui.py` ou `python gui.py Interro` pour lancer un GUI
|
Utiliser `python -m copienator gui` ou `python -m copienator gui Interro` pour lancer un GUI
|
||||||
qui suit automatiquement les étapes décrites ci-dessous.
|
qui suit automatiquement les étapes décrites ci-dessous.
|
||||||
|
|
||||||
** Prétraitement de l'énoncé
|
** Prétraitement de l'énoncé
|
||||||
@@ -20,8 +20,8 @@ Dans le dossier de l'évaluation, mettre les fichiers suivants de l'évaluation
|
|||||||
|
|
||||||
`enonce.pdf`, `enonce.tex`, `correction.tex`.
|
`enonce.pdf`, `enonce.tex`, `correction.tex`.
|
||||||
|
|
||||||
- `python gemini_for_enonce.py Interro` or
|
- `python -m copienator statement Interro` or
|
||||||
`python gemini_for_enonce.py Interro --restart`
|
`python -m copienator statement Interro --restart`
|
||||||
|
|
||||||
À partir des trois fichiers précédents, se charge de détecter les
|
À partir des trois fichiers précédents, se charge de détecter les
|
||||||
labels des questions et leur contenu.
|
labels des questions et leur contenu.
|
||||||
@@ -52,25 +52,25 @@ Dans le dossier de l'évaluation, mettre les fichiers suivants de l'évaluation
|
|||||||
|
|
||||||
Éventuellement : vérifier et modifier les barèmes dans `Persp`.
|
Éventuellement : vérifier et modifier les barèmes dans `Persp`.
|
||||||
|
|
||||||
- Alternative personnelle : `python enonce_info.py Interro`
|
- Alternative personnelle : `python -m copienator statement-personal Interro`
|
||||||
|
|
||||||
Ces deux commandes suivent la convention des scripts standardisés.
|
Ces deux commandes suivent la convention des scripts standardisés.
|
||||||
Leur import ne lance aucun traitement et les erreurs partielles sont
|
Leur import ne lance aucun traitement et les erreurs partielles sont
|
||||||
distinguées des échecs. Les réponses d'extraction mises en cache par
|
distinguées des échecs. Les réponses d'extraction mises en cache par
|
||||||
=gemini_for_enonce.py= et le fichier =labels= sont publiés
|
=copienator statement= et le fichier =labels= sont publiés
|
||||||
atomiquement.
|
atomiquement.
|
||||||
|
|
||||||
** Prétraitement des copies
|
** Prétraitement des copies
|
||||||
|
|
||||||
Mettre les copies scannées au format pdf dans =Interro=.
|
Mettre les copies scannées au format pdf dans =Interro=.
|
||||||
|
|
||||||
1. =python copies_tools.py rotate Interro= (facultatif)
|
1. =python -m copienator copies rotate Interro= (facultatif)
|
||||||
|
|
||||||
Retourne tous les pdf de 180°, si la photocopie a été faite à
|
Retourne tous les pdf de 180°, si la photocopie a été faite à
|
||||||
l'envers.
|
l'envers.
|
||||||
2. =python copies_tools.py rename Interro=
|
2. =python -m copienator copies rename Interro=
|
||||||
change le nom des copies en =Copie{id}.pdf=
|
change le nom des copies en =Copie{id}.pdf=
|
||||||
3. =python page_splitter.py Interro=
|
3. =python -m copienator page-split Interro=
|
||||||
|
|
||||||
Découpe des copies A3 en pages A4, en retirant les pages vides.
|
Découpe des copies A3 en pages A4, en retirant les pages vides.
|
||||||
|
|
||||||
@@ -80,21 +80,21 @@ Mettre les copies scannées au format pdf dans =Interro=.
|
|||||||
+ de jeter les deux pages
|
+ de jeter les deux pages
|
||||||
+ de déplacer la délimitation à droite/gauche
|
+ de déplacer la délimitation à droite/gauche
|
||||||
|
|
||||||
Fix issues with =python page_splitter.py Interro14/Copies/Copie01.pdf=
|
Fix issues with =python -m copienator page-split Interro14/Copies/Copie01.pdf=
|
||||||
|
|
||||||
Le PDF transformé est construit dans un dossier temporaire. La
|
Le PDF transformé est construit dans un dossier temporaire. La
|
||||||
copie produite et la sauvegarde dans =Copies Originales= sont
|
copie produite et la sauvegarde dans =Copies Originales= sont
|
||||||
ensuite installées avec rollback : une erreur conserve les deux
|
ensuite installées avec rollback : une erreur conserve les deux
|
||||||
versions précédentes. Une relance ciblée lit directement la
|
versions précédentes. Une relance ciblée lit directement la
|
||||||
sauvegarde originale sans la déplacer au préalable.
|
sauvegarde originale sans la déplacer au préalable.
|
||||||
4. =python cutleft.py Interro=
|
4. =python -m copienator crop-labels Interro=
|
||||||
|
|
||||||
Découpe la partie gauche des copies, là où il devrait y avoir les
|
Découpe la partie gauche des copies, là où il devrait y avoir les
|
||||||
labels des exercices/questions.
|
labels des exercices/questions.
|
||||||
|
|
||||||
=python cutleft.py Interro --fullpage= to use the full page always.
|
=python -m copienator crop-labels Interro --fullpage= to use the full page always.
|
||||||
|
|
||||||
Rerun on a single file with =python cutleft.py Interro/Copies/Copie01.pdf=
|
Rerun on a single file with =python -m copienator crop-labels Interro/Copies/Copie01.pdf=
|
||||||
|
|
||||||
Les images et le fichier =_schema.json= d'une copie sont remplacés
|
Les images et le fichier =_schema.json= d'une copie sont remplacés
|
||||||
ensemble. Fermer l'outil juste après la dernière validation ne peut
|
ensemble. Fermer l'outil juste après la dernière validation ne peut
|
||||||
@@ -104,7 +104,7 @@ Mettre les copies scannées au format pdf dans =Interro=.
|
|||||||
|
|
||||||
Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
||||||
|
|
||||||
1. =python gemini_for_labels.py Interro=, avec éventuellement =--overwrite=
|
1. =python -m copienator labels Interro=, avec éventuellement =--overwrite=
|
||||||
|
|
||||||
Fait des requêtes à Gemini pour identifier les labels des
|
Fait des requêtes à Gemini pour identifier les labels des
|
||||||
questions dans images générées à partir des parties gauches des copies.
|
questions dans images générées à partir des parties gauches des copies.
|
||||||
@@ -116,7 +116,7 @@ Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
|||||||
copies différentes sont traitées en parallèle. Chaque réponse JSON
|
copies différentes sont traitées en parallèle. Chaque réponse JSON
|
||||||
validée est écrite atomiquement. Une cible sans image correspondante
|
validée est écrite atomiquement. Une cible sans image correspondante
|
||||||
produit le code de sortie 4.
|
produit le code de sortie 4.
|
||||||
2. =python plotting.py Interro=
|
2. =python -m copienator review-labels Interro=
|
||||||
|
|
||||||
Permet de vérifier visuellement les labels trouvés.
|
Permet de vérifier visuellement les labels trouvés.
|
||||||
+ Sous linux, on peut faire =e= pour ouvrir le fichier .json et
|
+ Sous linux, on peut faire =e= pour ouvrir le fichier .json et
|
||||||
@@ -128,7 +128,7 @@ Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
|||||||
+ `|…` n'est pas arrêté verticalement par son type opposé.
|
+ `|…` n'est pas arrêté verticalement par son type opposé.
|
||||||
+ `…|` est stoppé horizontalement par le `|…` le plus proche.
|
+ `…|` est stoppé horizontalement par le `|…` le plus proche.
|
||||||
Pour modifier une seule copie :
|
Pour modifier une seule copie :
|
||||||
=python plotting.py Interro/Copies/Copie01.pdf=
|
=python -m copienator review-labels Interro/Copies/Copie01.pdf=
|
||||||
|
|
||||||
Les coordonnées agrégées sont écrites atomiquement dans le JSON de
|
Les coordonnées agrégées sont écrites atomiquement dans le JSON de
|
||||||
la copie. Fermer la fenêtre avant la fin d'une copie ne remplace pas
|
la copie. Fermer la fenêtre avant la fin d'une copie ne remplace pas
|
||||||
@@ -137,9 +137,9 @@ Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
|||||||
It also generates les =Copie01.json=, à partir des =Copie01_01.json=
|
It also generates les =Copie01.json=, à partir des =Copie01_01.json=
|
||||||
En cas de soucis, (par exemple les pages ne sont pas dans le bon ordre)
|
En cas de soucis, (par exemple les pages ne sont pas dans le bon ordre)
|
||||||
- Réordonner les pages du fichier pdf
|
- Réordonner les pages du fichier pdf
|
||||||
- Rerun =python cutleft.py Interro/Copie{id}=
|
- Rerun =python -m copienator crop-labels Interro/Copie{id}=
|
||||||
- Rerun =python gemini_for_labels.py Interro/Copie{id}=
|
- Rerun =python -m copienator labels Interro/Copie{id}=
|
||||||
3. =python splitting_int.py Interro=
|
3. =python -m copienator split-answers Interro=
|
||||||
|
|
||||||
Découpe les copies suivant les exercices
|
Découpe les copies suivant les exercices
|
||||||
Peut-être appelé avec une seule copie.
|
Peut-être appelé avec une seule copie.
|
||||||
@@ -148,11 +148,11 @@ Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
|||||||
puis remplacent ensemble le dossier précédent. En cas d'erreur,
|
puis remplacent ensemble le dossier précédent. En cas d'erreur,
|
||||||
l'ancienne version est conservée. Les réponses devenues obsolètes
|
l'ancienne version est conservée. Les réponses devenues obsolètes
|
||||||
restent archivées dans le sous-dossier =Missing=.
|
restent archivées dans le sous-dossier =Missing=.
|
||||||
4. =python grouping.py Interro=
|
4. =python -m copienator group-answers Interro=
|
||||||
|
|
||||||
Regroupe les mêmes questions de différentes copies en groupes de
|
Regroupe les mêmes questions de différentes copies en groupes de
|
||||||
tailles raisonnables.
|
tailles raisonnables.
|
||||||
5. Facultatif : =python verify_groups.py Interro=
|
5. Facultatif : =python -m copienator verify-groups Interro=
|
||||||
|
|
||||||
Vérifie que chaque réponse PDF apparaît bien dans les métadonnées
|
Vérifie que chaque réponse PDF apparaît bien dans les métadonnées
|
||||||
des groupes. La commande renvoie un code non nul si une réponse est
|
des groupes. La commande renvoie un code non nul si une réponse est
|
||||||
@@ -163,14 +163,14 @@ Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
|||||||
Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
||||||
|
|
||||||
1. Il faut créer des persp, pour indication de comment corriger, et
|
1. Il faut créer des persp, pour indication de comment corriger, et
|
||||||
relancer =enonce_info.py=
|
relancer =copienator statement-personal=
|
||||||
2. =python correction.py Interro --limit 240= OU
|
2. =python -m copienator correct Interro --limit 240= OU
|
||||||
=python correction.py Interro/Par\ label/Ex\ 2/Group_1.jpg= OU
|
=python -m copienator correct Interro/Par\ label/Ex\ 2/Group_1.jpg= OU
|
||||||
=python correction.py Interro --overwrite=
|
=python -m copienator correct Interro --overwrite=
|
||||||
|
|
||||||
Fais les requêtes de correction à Gemini.
|
Fais les requêtes de correction à Gemini.
|
||||||
|
|
||||||
=correction.py= peut être relancé sans supprimer son état. Les
|
=copienator correct= peut être relancé sans supprimer son état. Les
|
||||||
fichiers =correction.json= et =correction_progress.json= sont mis à
|
fichiers =correction.json= et =correction_progress.json= sont mis à
|
||||||
jour atomiquement. Avec =--overwrite=, leur version précédente reste
|
jour atomiquement. Avec =--overwrite=, leur version précédente reste
|
||||||
en place jusqu'à la première écriture réussie de la nouvelle
|
en place jusqu'à la première écriture réussie de la nouvelle
|
||||||
@@ -183,23 +183,23 @@ Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
|||||||
|
|
||||||
Pour diminuer le coût, il est possible de batch les requêtes, qui
|
Pour diminuer le coût, il est possible de batch les requêtes, qui
|
||||||
seront alors traitées sous au plus 24h.
|
seront alors traitées sous au plus 24h.
|
||||||
+ =python correction.py Interro --batch=
|
+ =python -m copienator correct Interro --batch=
|
||||||
+ OU =python correction.py Interro --batch-from 'Ex 4'=
|
+ OU =python -m copienator correct Interro --batch-from 'Ex 4'=
|
||||||
+ =python submit_batches.py Interro=
|
+ =python -m copienator batch-submit Interro=
|
||||||
+ =python batch_status.py=
|
+ =python -m copienator batch-status=
|
||||||
+ =python fetch_batched_results.py Interro=
|
+ =python -m copienator batch-fetch Interro=
|
||||||
+ =python correction.py Interro --deal-with-batched=
|
+ =python -m copienator correct Interro --deal-with-batched=
|
||||||
|
|
||||||
Les quatre commandes de ce flux suivent la convention des scripts
|
Les quatre commandes de ce flux suivent la convention des scripts
|
||||||
standardisés. Les fichiers de requêtes et le résultat JSONL combiné
|
standardisés. Les fichiers de requêtes et le résultat JSONL combiné
|
||||||
sont publiés atomiquement : une interruption ne laisse pas de fichier
|
sont publiés atomiquement : une interruption ne laisse pas de fichier
|
||||||
final partiellement écrit. =submit_batches.py= conserve aussi les
|
final partiellement écrit. =copienator batch-submit= conserve aussi les
|
||||||
identifiants distants dans =batch_jobs.json= ; la récupération les
|
identifiants distants dans =batch_jobs.json= ; la récupération les
|
||||||
utilise en priorité et garde la recherche par nom pour les anciens
|
utilise en priorité et garde la recherche par nom pour les anciens
|
||||||
batchs. =batch_status.py --download JOB --output
|
batchs. =python -m copienator batch-status --download JOB --output
|
||||||
resultat.jsonl= permet aussi de télécharger atomiquement le résultat
|
resultat.jsonl= permet aussi de télécharger atomiquement le résultat
|
||||||
d'un job particulier.
|
d'un job particulier.
|
||||||
3. =python post-correction.py Interro=
|
3. =python -m copienator post-correction Interro=
|
||||||
|
|
||||||
- Essaye de corriger des erreurs d'encodage/d'accents dans
|
- Essaye de corriger des erreurs d'encodage/d'accents dans
|
||||||
=correction.json=.
|
=correction.json=.
|
||||||
@@ -214,29 +214,29 @@ Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
|||||||
+ `xx` : move to goal.
|
+ `xx` : move to goal.
|
||||||
+ `xs` : remove old, keep goal.
|
+ `xs` : remove old, keep goal.
|
||||||
|
|
||||||
Then call `python resolve_manual.py Interro`
|
Then call `python -m copienator resolve-manual Interro`
|
||||||
5. Call `python correction.py Interro --refaire`.
|
5. Call `python -m copienator correct Interro --refaire`.
|
||||||
|
|
||||||
|
|
||||||
** Génération des copies annotées
|
** Génération des copies annotées
|
||||||
|
|
||||||
1. =python annotating.py Interro= (facultatif)
|
1. =python -m copienator annotate-simple Interro= (facultatif)
|
||||||
|
|
||||||
Ajoute les annotations Gemini aux copies, enregistrées dans le dossier =Anot=.
|
Ajoute les annotations Gemini aux copies, enregistrées dans le dossier =Anot=.
|
||||||
On peut passer l'argument =--overwrite=.
|
On peut passer l'argument =--overwrite=.
|
||||||
OU
|
OU
|
||||||
2. =python annotating_with_checks.py Interro=
|
2. =python -m copienator annotate-checks Interro=
|
||||||
|
|
||||||
Ajoute les annotations Gemini, et des checkboxes à cocher.
|
Ajoute les annotations Gemini, et des checkboxes à cocher.
|
||||||
Enregistrées dans le dossier =Bnot=,
|
Enregistrées dans le dossier =Bnot=,
|
||||||
=--overwrite=
|
=--overwrite=
|
||||||
|
|
||||||
Une seule copie peut être ciblée avec, par exemple,
|
Une seule copie peut être ciblée avec, par exemple,
|
||||||
=python annotating_with_checks.py Interro/Copies/Copie01.pdf=.
|
=python -m copienator annotate-checks Interro/Copies/Copie01.pdf=.
|
||||||
Le mode =--refaire= exige un fichier =refaire.json= et écrit dans
|
Le mode =--refaire= exige un fichier =refaire.json= et écrit dans
|
||||||
=BRnot=.
|
=BRnot=.
|
||||||
OU
|
OU
|
||||||
2. =python annotating_by_label.py Interro= dans =BGnot=
|
2. =python -m copienator annotate-grouped Interro= dans =BGnot=
|
||||||
|
|
||||||
Ajoute les annotations Gemini, et des checkboxes, et regroupe les
|
Ajoute les annotations Gemini, et des checkboxes, et regroupe les
|
||||||
réponses par question.
|
réponses par question.
|
||||||
@@ -245,7 +245,7 @@ OU
|
|||||||
_Needs_ : label_groups file (made automatically by this function),
|
_Needs_ : label_groups file (made automatically by this function),
|
||||||
qui dit quelles questions regrouper.
|
qui dit quelles questions regrouper.
|
||||||
|
|
||||||
3. =python export.py Interro BGnot= (gestion perso)
|
3. =python -m copienator export Interro BGnot= (gestion perso)
|
||||||
Cela déplace les groupes dans le dossier configuré par =EXPORT_DIR=
|
Cela déplace les groupes dans le dossier configuré par =EXPORT_DIR=
|
||||||
(par défaut =Export=).
|
(par défaut =Export=).
|
||||||
|
|
||||||
@@ -263,7 +263,7 @@ tablette graphique.
|
|||||||
_Before_ : vider le dossier configuré par =IMPORT_DIR= (par défaut
|
_Before_ : vider le dossier configuré par =IMPORT_DIR= (par défaut
|
||||||
=Import=), puis y copier ou synchroniser les fichiers depuis la tablette.
|
=Import=), puis y copier ou synchroniser les fichiers depuis la tablette.
|
||||||
|
|
||||||
1. =python import.py Interro BGnot=
|
1. =python -m copienator import Interro BGnot=
|
||||||
|
|
||||||
Une fois les corrections manuelles appliquées aux fichiers
|
Une fois les corrections manuelles appliquées aux fichiers
|
||||||
=Concat.pdf=, il faut enregistrer le fichier annoté au même endroit,
|
=Concat.pdf=, il faut enregistrer le fichier annoté au même endroit,
|
||||||
@@ -274,7 +274,7 @@ _Before_ : vider le dossier configuré par =IMPORT_DIR= (par défaut
|
|||||||
export. Pour =Anot=, l'image est importée sous le nom
|
export. Pour =Anot=, l'image est importée sous le nom
|
||||||
=Concat_annotated.jpg=.
|
=Concat_annotated.jpg=.
|
||||||
|
|
||||||
2. =python reading_annotations.py Interro=
|
2. =python -m copienator read-annotations Interro=
|
||||||
|
|
||||||
Lit les =Concat_annotated= dans =Bnot=, regénère les copies avec
|
Lit les =Concat_annotated= dans =Bnot=, regénère les copies avec
|
||||||
les modifications. Les fichiers générés (=score.json=,
|
les modifications. Les fichiers générés (=score.json=,
|
||||||
@@ -282,12 +282,12 @@ _Before_ : vider le dossier configuré par =IMPORT_DIR= (par défaut
|
|||||||
ensemble. Les entrées du dossier =Bnot= restent en place et une
|
ensemble. Les entrées du dossier =Bnot= restent en place et une
|
||||||
erreur de génération conserve les anciennes sorties.
|
erreur de génération conserve les anciennes sorties.
|
||||||
OU
|
OU
|
||||||
2. =python reading_grouped_annotations.py Interro=
|
2. =python -m copienator read-grouped Interro=
|
||||||
|
|
||||||
Idem, mais pour =BGnot=. Les tâches parallèles remontent leurs
|
Idem, mais pour =BGnot=. Les tâches parallèles remontent leurs
|
||||||
erreurs au processus principal au lieu de les ignorer.
|
erreurs au processus principal au lieu de les ignorer.
|
||||||
|
|
||||||
3. =python giving_names.py Interro BGnot=
|
3. =python -m copienator giving-names Interro BGnot=
|
||||||
|
|
||||||
Crée un dossier =A Rendre= avec des liens symboliques vers
|
Crée un dossier =A Rendre= avec des liens symboliques vers
|
||||||
+ La copie à rendre
|
+ La copie à rendre
|
||||||
@@ -297,17 +297,17 @@ OU
|
|||||||
4. Éventuellement, faire des modifications manuelles aux =score.json=.
|
4. Éventuellement, faire des modifications manuelles aux =score.json=.
|
||||||
|
|
||||||
Puis
|
Puis
|
||||||
- `python reading_annotations.py --update-score Interro`
|
- `python -m copienator read-annotations --update-score Interro`
|
||||||
- `python reading_grouped_annotations.py --update-score Interro`
|
- `python -m copienator read-grouped --update-score Interro`
|
||||||
pour mettre à jour les scores dans les images.
|
pour mettre à jour les scores dans les images.
|
||||||
4. (gestion perso)
|
4. (gestion perso)
|
||||||
+ =gestion_classe ne= pour créer l'interro puis
|
+ =gestion_classe ne= pour créer l'interro puis
|
||||||
+ =gestion_classe we= (set barème here)
|
+ =gestion_classe we= (set barème here)
|
||||||
+ =python update_ods.py Interro=
|
+ =python -m copienator update-ods Interro=
|
||||||
ou =python update_ods.py Interro --sum= (en l'absence de barème)
|
ou =python -m copienator update-ods Interro --sum= (en l'absence de barème)
|
||||||
+ =gestion_classe re=
|
+ =gestion_classe re=
|
||||||
+ =gestion_classe wsent=
|
+ =gestion_classe wsent=
|
||||||
+ =python add_final_score.py Interro21=
|
+ =python -m copienator add-final-score Interro21=
|
||||||
(this makes files in =Server/copies=)
|
(this makes files in =Server/copies=)
|
||||||
5. (gestion perso)
|
5. (gestion perso)
|
||||||
+ Deploy =miqmacs-copies-assets=, and
|
+ Deploy =miqmacs-copies-assets=, and
|
||||||
@@ -321,8 +321,8 @@ OU
|
|||||||
groupée into refaire !!
|
groupée into refaire !!
|
||||||
|
|
||||||
1. Redécoupage
|
1. Redécoupage
|
||||||
+ =python plotting.py InterroTest/Copie01.pdf=
|
+ =python -m copienator review-labels InterroTest/Copie01.pdf=
|
||||||
+ =python splitting_int.py InterroTest/Copie20.pdf=
|
+ =python -m copienator split-answers InterroTest/Copie20.pdf=
|
||||||
2. Créer =refaire.json=, avec un contenu comme
|
2. Créer =refaire.json=, avec un contenu comme
|
||||||
: [["Copie02", []],
|
: [["Copie02", []],
|
||||||
: ["Copie01", ["Ex 1 : 1)"]]]
|
: ["Copie01", ["Ex 1 : 1)"]]]
|
||||||
@@ -333,10 +333,10 @@ groupée into refaire !!
|
|||||||
Ou non, si tu veux le faire à la main.
|
Ou non, si tu veux le faire à la main.
|
||||||
4. ?? Si je fais refaire, avant d'avoir créer les annotating with
|
4. ?? Si je fais refaire, avant d'avoir créer les annotating with
|
||||||
checks, que se passe-t-il ???
|
checks, que se passe-t-il ???
|
||||||
5. Appeler =annotating_with_checks.py --refaire --overwrite=
|
5. Appeler =python -m copienator annotate-checks --refaire --overwrite=
|
||||||
6. =python export.py --refaire Interro24=
|
6. =python -m copienator export --refaire Interro24=
|
||||||
6. =python import.py --refaire Interro24=
|
6. =python -m copienator import --refaire Interro24=
|
||||||
7. =python reading_grouped_annotations.py --refaire Interro24=
|
7. =python -m copienator read-grouped --refaire Interro24=
|
||||||
|
|
||||||
Avec =--refaire=, =refaire.json= et le dossier =BRnot= sont des
|
Avec =--refaire=, =refaire.json= et le dossier =BRnot= sont des
|
||||||
prérequis obligatoires ; leur absence produit le code de sortie 3.
|
prérequis obligatoires ; leur absence produit le code de sortie 3.
|
||||||
@@ -344,11 +344,10 @@ groupée into refaire !!
|
|||||||
** Exemple de replotting, refaire d'une copie
|
** Exemple de replotting, refaire d'une copie
|
||||||
|
|
||||||
1. replot it.
|
1. replot it.
|
||||||
2. `python splitting_int.py DS09VA/Copies/Copie25.pdf`
|
2. `python -m copienator split-answers DS09VA/Copies/Copie25.pdf`
|
||||||
this will get rid of old/new.
|
this will get rid of old/new.
|
||||||
!! Attention, et si ça dégage un new : bad bad bad.
|
!! Attention, et si ça dégage un new : bad bad bad.
|
||||||
3. Make `refaire.json`, avec la copie, et les labels à refaire.
|
3. Make `refaire.json`, avec la copie, et les labels à refaire.
|
||||||
4. `python correction.py DS09VA --refaire`
|
4. `python -m copienator correct DS09VA --refaire`
|
||||||
5. `python annotating_with_checks.py DS09VA --refaire`
|
5. `python -m copienator annotate-checks DS09VA --refaire`
|
||||||
6. `python import.py Interro24 --refaire`
|
6. `python -m copienator import Interro24 --refaire`
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from .dispatcher import main
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""Executable Copienator commands.
|
||||||
|
|
||||||
|
Command modules expose a ``main(argv=None)`` entry point and may also expose
|
||||||
|
their processing functions for reuse and tests.
|
||||||
|
"""
|
||||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
from config import FINAL_SCORE_FONT_PATH, FINAL_SCORE_ODS_PATH, FINAL_SCORE_OUTPUT_DIR
|
from copienator.configuration import FINAL_SCORE_FONT_PATH, FINAL_SCORE_ODS_PATH, FINAL_SCORE_OUTPUT_DIR
|
||||||
|
|
||||||
# Configuration constants
|
# Configuration constants
|
||||||
ODS_PATH = Path(FINAL_SCORE_ODS_PATH).expanduser()
|
ODS_PATH = Path(FINAL_SCORE_ODS_PATH).expanduser()
|
||||||
@@ -112,14 +112,17 @@ def process_images(base_dir, output_dir):
|
|||||||
shutil.copy(str(pdf_path), str(save_path))
|
shutil.copy(str(pdf_path), str(save_path))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def main(argv=None):
|
||||||
parser = argparse.ArgumentParser(description="Stamp scores on exam copies.")
|
parser = argparse.ArgumentParser(description="Stamp scores on exam copies.")
|
||||||
parser.add_argument("dir", type=Path, help="Root directory containing 'A Rendre' folder")
|
parser.add_argument("dir", type=Path, help="Root directory containing 'A Rendre' folder")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
base_dir = args.dir.expanduser().resolve()
|
base_dir = args.dir.expanduser().resolve()
|
||||||
output_dir = OUTPUT_DIR / base_dir.name
|
output_dir = OUTPUT_DIR / base_dir.name
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
process_images(base_dir, output_dir)
|
process_images(base_dir, output_dir)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -19,8 +19,8 @@ from matplotlib import pyplot as plt
|
|||||||
from pdf2image import convert_from_path
|
from pdf2image import convert_from_path
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
import utils
|
from copienator import utils
|
||||||
from config import LATEX_ANOT_AFTER, LATEX_ANOT_BEFORE
|
from copienator.configuration import LATEX_ANOT_AFTER, LATEX_ANOT_BEFORE
|
||||||
from copienator import (
|
from copienator import (
|
||||||
EvaluationWorkspace,
|
EvaluationWorkspace,
|
||||||
ExitCode,
|
ExitCode,
|
||||||
@@ -31,7 +31,7 @@ from copienator import (
|
|||||||
)
|
)
|
||||||
from copienator.annotation_data import load_annotation_data
|
from copienator.annotation_data import load_annotation_data
|
||||||
from copienator.filesystem import staged_directory
|
from copienator.filesystem import staged_directory
|
||||||
from utils import natural_key
|
from copienator.utils import natural_key
|
||||||
|
|
||||||
MARGIN_LEFT = 300
|
MARGIN_LEFT = 300
|
||||||
ANNOT_WIDTH = 600
|
ANNOT_WIDTH = 600
|
||||||
@@ -10,9 +10,9 @@ from typing import Any
|
|||||||
from PIL import Image, ImageDraw
|
from PIL import Image, ImageDraw
|
||||||
from reportlab.pdfgen import canvas
|
from reportlab.pdfgen import canvas
|
||||||
|
|
||||||
import annotating
|
from copienator.commands import annotating
|
||||||
import annotating_with_checks
|
from copienator.commands import annotating_with_checks
|
||||||
import utils
|
from copienator import utils
|
||||||
from copienator import (
|
from copienator import (
|
||||||
CliError,
|
CliError,
|
||||||
EvaluationWorkspace,
|
EvaluationWorkspace,
|
||||||
@@ -26,7 +26,7 @@ from copienator import (
|
|||||||
)
|
)
|
||||||
from copienator.annotation_data import load_annotation_data
|
from copienator.annotation_data import load_annotation_data
|
||||||
from copienator.filesystem import staged_directory
|
from copienator.filesystem import staged_directory
|
||||||
from utils import natural_key
|
from copienator.utils import natural_key
|
||||||
|
|
||||||
MAX_HEIGHT_PX = 25000
|
MAX_HEIGHT_PX = 25000
|
||||||
|
|
||||||
@@ -14,8 +14,8 @@ matplotlib.use("Agg")
|
|||||||
from PIL import Image, ImageFont
|
from PIL import Image, ImageFont
|
||||||
from reportlab.pdfgen import canvas
|
from reportlab.pdfgen import canvas
|
||||||
|
|
||||||
import annotating
|
from copienator.commands import annotating
|
||||||
import utils
|
from copienator import utils
|
||||||
from copienator import (
|
from copienator import (
|
||||||
CliError,
|
CliError,
|
||||||
EvaluationWorkspace,
|
EvaluationWorkspace,
|
||||||
@@ -28,7 +28,7 @@ from copienator import (
|
|||||||
)
|
)
|
||||||
from copienator.annotation_data import load_annotation_data
|
from copienator.annotation_data import load_annotation_data
|
||||||
from copienator.filesystem import staged_directory
|
from copienator.filesystem import staged_directory
|
||||||
from utils import natural_key
|
from copienator.utils import natural_key
|
||||||
|
|
||||||
BOX_SIZE = 30
|
BOX_SIZE = 30
|
||||||
SCORE_BOX_SIZE = 40
|
SCORE_BOX_SIZE = 40
|
||||||
@@ -328,3 +328,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from google import genai
|
from google import genai
|
||||||
|
|
||||||
import config
|
from copienator import configuration as config
|
||||||
from copienator import (
|
from copienator import (
|
||||||
CliError,
|
CliError,
|
||||||
ExitCode,
|
ExitCode,
|
||||||
@@ -134,3 +134,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|
||||||
@@ -15,9 +15,9 @@ from pathlib import Path
|
|||||||
|
|
||||||
from google import genai
|
from google import genai
|
||||||
|
|
||||||
import config
|
from copienator import configuration as config
|
||||||
import grouping
|
from copienator.commands import grouping
|
||||||
import prompting
|
from copienator import prompting
|
||||||
from copienator import (
|
from copienator import (
|
||||||
CliError,
|
CliError,
|
||||||
EvaluationWorkspace,
|
EvaluationWorkspace,
|
||||||
@@ -29,7 +29,7 @@ from copienator import (
|
|||||||
target_parser,
|
target_parser,
|
||||||
workspace_from_target,
|
workspace_from_target,
|
||||||
)
|
)
|
||||||
from utils import enonce_total, read_all_labels
|
from copienator.utils import enonce_total, read_all_labels
|
||||||
|
|
||||||
NB_THREADS = 12
|
NB_THREADS = 12
|
||||||
|
|
||||||
@@ -874,7 +874,7 @@ def run_configured(args: argparse.Namespace) -> ExitCode:
|
|||||||
+ "\n",
|
+ "\n",
|
||||||
)
|
)
|
||||||
print(f"\n[!] Unresolved delayed tasks found! Wrote to {manual_path}.")
|
print(f"\n[!] Unresolved delayed tasks found! Wrote to {manual_path}.")
|
||||||
print(" Please edit it manually, then run `python resolve_manual.py <InputDir>`")
|
print(" Please edit it manually, then run `python -m copienator resolve-manual <InputDir>`")
|
||||||
|
|
||||||
end_time = time.time()
|
end_time = time.time()
|
||||||
print("Time elapsed : ", end_time - start_time)
|
print("Time elapsed : ", end_time - start_time)
|
||||||
@@ -884,7 +884,7 @@ def run_configured(args: argparse.Namespace) -> ExitCode:
|
|||||||
for (err, file) in errors_summary:
|
for (err, file) in errors_summary:
|
||||||
print(err, file=sys.stderr)
|
print(err, file=sys.stderr)
|
||||||
escaped_path = shlex.quote(str(file))
|
escaped_path = shlex.quote(str(file))
|
||||||
print(f"Run : python correction.py {escaped_path}")
|
print(f"Run : python -m copienator correct {escaped_path}")
|
||||||
return ExitCode.PARTIAL if errors_summary else ExitCode.SUCCESS
|
return ExitCode.PARTIAL if errors_summary else ExitCode.SUCCESS
|
||||||
|
|
||||||
|
|
||||||
@@ -341,3 +341,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|
||||||
@@ -17,8 +17,8 @@ from copienator import (
|
|||||||
execute,
|
execute,
|
||||||
workspace_from_args,
|
workspace_from_args,
|
||||||
)
|
)
|
||||||
from platform_utils import WindowsLabelError, validate_windows_labels
|
from copienator.platform import WindowsLabelError, validate_windows_labels
|
||||||
from utils import compile_to_pdf
|
from copienator.utils import compile_to_pdf
|
||||||
|
|
||||||
|
|
||||||
def fetch_and_save_sub_text(ex_id, indices, label, text_path):
|
def fetch_and_save_sub_text(ex_id, indices, label, text_path):
|
||||||
@@ -293,3 +293,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ import sys
|
|||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from config import EXPORT_DIR
|
from copienator.configuration import EXPORT_DIR
|
||||||
from copienator import (
|
from copienator import (
|
||||||
EvaluationWorkspace,
|
EvaluationWorkspace,
|
||||||
ExitCode,
|
ExitCode,
|
||||||
@@ -11,7 +11,7 @@ from copienator import (
|
|||||||
execute,
|
execute,
|
||||||
workspace_from_args,
|
workspace_from_args,
|
||||||
)
|
)
|
||||||
from platform_utils import replace_with_link_or_copy
|
from copienator.platform import replace_with_link_or_copy
|
||||||
|
|
||||||
ANNOTATION_DIRECTORIES = ("BGnot", "Bnot", "Anot")
|
ANNOTATION_DIRECTORIES = ("BGnot", "Bnot", "Anot")
|
||||||
|
|
||||||
@@ -5,7 +5,7 @@ from collections.abc import Sequence
|
|||||||
|
|
||||||
from google import genai
|
from google import genai
|
||||||
|
|
||||||
import config
|
from copienator import configuration as config
|
||||||
from copienator import (
|
from copienator import (
|
||||||
CliError,
|
CliError,
|
||||||
EvaluationWorkspace,
|
EvaluationWorkspace,
|
||||||
@@ -10,8 +10,8 @@ from google import genai
|
|||||||
from google.genai import types
|
from google.genai import types
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
import config
|
from copienator import configuration as config
|
||||||
import utils
|
from copienator import utils
|
||||||
from copienator import (
|
from copienator import (
|
||||||
CliError,
|
CliError,
|
||||||
EvaluationWorkspace,
|
EvaluationWorkspace,
|
||||||
@@ -21,8 +21,8 @@ from copienator import (
|
|||||||
execute,
|
execute,
|
||||||
workspace_from_args,
|
workspace_from_args,
|
||||||
)
|
)
|
||||||
from platform_utils import validate_windows_labels
|
from copienator.platform import validate_windows_labels
|
||||||
from utils import compile_to_pdf
|
from copienator.utils import compile_to_pdf
|
||||||
|
|
||||||
|
|
||||||
def get_lcp(s1: str, s2: str) -> str:
|
def get_lcp(s1: str, s2: str) -> str:
|
||||||
@@ -13,7 +13,7 @@ from google import genai
|
|||||||
from google.genai import types
|
from google.genai import types
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
import config
|
from copienator import configuration as config
|
||||||
from copienator import (
|
from copienator import (
|
||||||
CliError,
|
CliError,
|
||||||
EvaluationWorkspace,
|
EvaluationWorkspace,
|
||||||
@@ -24,7 +24,7 @@ from copienator import (
|
|||||||
target_parser,
|
target_parser,
|
||||||
workspace_from_target,
|
workspace_from_target,
|
||||||
)
|
)
|
||||||
from utils import natural_key, read_all_labels
|
from copienator.utils import natural_key, read_all_labels
|
||||||
|
|
||||||
MODEL_ID = config.MODEL_FOR_LABEL_ID
|
MODEL_ID = config.MODEL_FOR_LABEL_ID
|
||||||
api_key = config.API_KEY
|
api_key = config.API_KEY
|
||||||
@@ -422,7 +422,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||||||
|
|
||||||
def handle(args: argparse.Namespace) -> ExitCode:
|
def handle(args: argparse.Namespace) -> ExitCode:
|
||||||
workspace, target = workspace_from_target(
|
workspace, target = workspace_from_target(
|
||||||
args, repository=Path(__file__).resolve().parent
|
args, repository=Path(__file__).resolve().parents[2]
|
||||||
)
|
)
|
||||||
targets = [target]
|
targets = [target]
|
||||||
for additional in args.additional_targets:
|
for additional in args.additional_targets:
|
||||||
@@ -15,7 +15,7 @@ from copienator import (
|
|||||||
read_json,
|
read_json,
|
||||||
workspace_from_args,
|
workspace_from_args,
|
||||||
)
|
)
|
||||||
from platform_utils import replace_with_link_or_copy, safe_filename
|
from copienator.platform import replace_with_link_or_copy, safe_filename
|
||||||
|
|
||||||
ANNOTATION_CHOICES = ("BGnot", "Bnot", "Anot")
|
ANNOTATION_CHOICES = ("BGnot", "Bnot", "Anot")
|
||||||
|
|
||||||
@@ -163,3 +163,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ def create_jpg(identifier, group_index, group, root_dir):
|
|||||||
|
|
||||||
print(f"Saved {output_path} with {len(group)} ({os.path.getsize(output_path)/1024/1024:.2f} MB)")
|
print(f"Saved {output_path} with {len(group)} ({os.path.getsize(output_path)/1024/1024:.2f} MB)")
|
||||||
|
|
||||||
from utils import natural_key
|
from copienator.utils import natural_key
|
||||||
|
|
||||||
|
|
||||||
def process_identifier(identifier, files_info, output_dir):
|
def process_identifier(identifier, files_info, output_dir):
|
||||||
@@ -275,3 +275,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@ import sys
|
|||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from config import IMPORT_DIR
|
from copienator.configuration import IMPORT_DIR
|
||||||
from copienator import (
|
from copienator import (
|
||||||
EvaluationWorkspace,
|
EvaluationWorkspace,
|
||||||
ExitCode,
|
ExitCode,
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import concurrent.futures
|
||||||
|
from pathlib import Path
|
||||||
|
from copienator.commands import correction
|
||||||
|
|
||||||
|
def get_missing_tasks():
|
||||||
|
"""
|
||||||
|
Identifies tasks (groups) where NONE of the student IDs in that group
|
||||||
|
appear in the existing results for that label in correction.json.
|
||||||
|
"""
|
||||||
|
missing = []
|
||||||
|
|
||||||
|
# correction.results is already loaded from correction.json during 'from copienator.commands import correction'
|
||||||
|
# correction.tasks is populated with (filepath, label) during 'from copienator.commands import correction'
|
||||||
|
|
||||||
|
for task in correction.tasks:
|
||||||
|
file_path, label = task
|
||||||
|
# Find the group metadata file (Group_X.json) to know which IDs are inside
|
||||||
|
meta_path = Path(file_path).with_suffix('.json')
|
||||||
|
|
||||||
|
if not meta_path.exists():
|
||||||
|
print("Missing meta_path :", meta_path)
|
||||||
|
continue
|
||||||
|
|
||||||
|
with open(meta_path, 'r', encoding="utf-8") as f:
|
||||||
|
# group_data entries: [pid, ymin, ymax, width_ratio]
|
||||||
|
group_data = json.load(f)
|
||||||
|
|
||||||
|
pids_in_group = [str(item[0]) for item in group_data]
|
||||||
|
|
||||||
|
# Check correction.json results for this specific label
|
||||||
|
label_results = correction.results.get(label, [])
|
||||||
|
|
||||||
|
# Collect all student IDs that have already been processed for this label
|
||||||
|
covered_ids = set()
|
||||||
|
for result_list in label_results:
|
||||||
|
for entry in result_list:
|
||||||
|
covered_ids.add(str(entry.get('id')))
|
||||||
|
|
||||||
|
# Logic: Only process if EVERY ID in this group is missing from correction.json
|
||||||
|
if all(pid not in covered_ids for pid in pids_in_group):
|
||||||
|
missing.append(task)
|
||||||
|
|
||||||
|
return missing
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
if argv:
|
||||||
|
print("missing-correction does not accept command-line arguments.", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
missing_tasks = get_missing_tasks()
|
||||||
|
print("\n Total nb of tasks : ", len(correction.tasks))
|
||||||
|
|
||||||
|
if not missing_tasks:
|
||||||
|
print("All groups are already present in correction.json. Nothing to do.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("\nThe following groups are missing from correction.json:")
|
||||||
|
for path, label in missing_tasks:
|
||||||
|
print(f" - [{label}] {path}")
|
||||||
|
|
||||||
|
confirm = input(f"\nFound {len(missing_tasks)} missing groups. Start processing? (y/N): ")
|
||||||
|
if confirm.lower() != 'y':
|
||||||
|
print("Aborted.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Processing {len(missing_tasks)} tasks with {correction.NB_THREADS} threads...")
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=correction.NB_THREADS) as executor:
|
||||||
|
# Map tasks to the processing function defined in correction.py
|
||||||
|
futures = {executor.submit(correction.process_single_task, t): t for t in missing_tasks}
|
||||||
|
|
||||||
|
for future in concurrent.futures.as_completed(futures):
|
||||||
|
try:
|
||||||
|
# Handle potential sub-tasks (like label errors) generated during processing
|
||||||
|
new_generated_tasks = future.result()
|
||||||
|
if new_generated_tasks:
|
||||||
|
for nt in new_generated_tasks:
|
||||||
|
executor.submit(correction.process_single_task, nt)
|
||||||
|
except Exception as e:
|
||||||
|
t = futures[future]
|
||||||
|
print(f"Error processing {t[0]}: {e}")
|
||||||
|
|
||||||
|
print("\nProcessing complete.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -16,7 +16,7 @@ import fitz # PyMuPDF
|
|||||||
from PIL import Image, ImageDraw, ImageTk
|
from PIL import Image, ImageDraw, ImageTk
|
||||||
from pypdf import PdfReader, PdfWriter
|
from pypdf import PdfReader, PdfWriter
|
||||||
|
|
||||||
from config import PAGE_SPLITTER_KB
|
from copienator.configuration import PAGE_SPLITTER_KB
|
||||||
from copienator import (
|
from copienator import (
|
||||||
CliError,
|
CliError,
|
||||||
EvaluationWorkspace,
|
EvaluationWorkspace,
|
||||||
@@ -25,7 +25,7 @@ from copienator import (
|
|||||||
target_parser,
|
target_parser,
|
||||||
workspace_from_target,
|
workspace_from_target,
|
||||||
)
|
)
|
||||||
from platform_utils import launch_pdf_arranger
|
from copienator.platform import launch_pdf_arranger
|
||||||
|
|
||||||
# --- Constants ---
|
# --- Constants ---
|
||||||
# Conversion factor: 1 cm to points (1 inch = 2.54 cm, 72 points = 1 inch)
|
# Conversion factor: 1 cm to points (1 inch = 2.54 cm, 72 points = 1 inch)
|
||||||
@@ -19,8 +19,8 @@ from copienator import (
|
|||||||
target_parser,
|
target_parser,
|
||||||
workspace_from_target,
|
workspace_from_target,
|
||||||
)
|
)
|
||||||
from platform_utils import open_path
|
from copienator.platform import open_path
|
||||||
from utils import natural_key, read_all_labels
|
from copienator.utils import natural_key, read_all_labels
|
||||||
|
|
||||||
# --- Configuration & Globals ---
|
# --- Configuration & Globals ---
|
||||||
padding = 60
|
padding = 60
|
||||||
@@ -472,3 +472,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ from copienator import (
|
|||||||
workspace_from_args,
|
workspace_from_args,
|
||||||
)
|
)
|
||||||
|
|
||||||
WORD_LIST_FILE = Path(__file__).with_name("liste_francais.txt")
|
WORD_LIST_FILE = Path(__file__).resolve().parents[1] / "data" / "liste_francais.txt"
|
||||||
ACCENT_PATTERN = re.compile(r"[éèêëàâäîïôöùûüçœÉÈÊËÀÂÄÎÏÔÖÙÛÜÇŒ]")
|
ACCENT_PATTERN = re.compile(r"[éèêëàâäîïôöùûüçœÉÈÊËÀÂÄÎÏÔÖÙÛÜÇŒ]")
|
||||||
|
|
||||||
|
|
||||||
@@ -9,8 +9,8 @@ import numpy as np
|
|||||||
from pdf2image import convert_from_path
|
from pdf2image import convert_from_path
|
||||||
from PIL import Image, ImageChops, ImageDraw, ImageFilter
|
from PIL import Image, ImageChops, ImageDraw, ImageFilter
|
||||||
|
|
||||||
import annotating
|
from copienator.commands import annotating
|
||||||
import utils
|
from copienator import utils
|
||||||
from copienator import (
|
from copienator import (
|
||||||
EvaluationWorkspace,
|
EvaluationWorkspace,
|
||||||
ExitCode,
|
ExitCode,
|
||||||
@@ -284,3 +284,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|
||||||
@@ -9,8 +9,8 @@ from typing import Any
|
|||||||
|
|
||||||
from PIL import Image, ImageDraw
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
import annotating
|
from copienator.commands import annotating
|
||||||
import utils
|
from copienator import utils
|
||||||
from copienator import (
|
from copienator import (
|
||||||
EvaluationWorkspace,
|
EvaluationWorkspace,
|
||||||
ExitCode,
|
ExitCode,
|
||||||
@@ -23,7 +23,7 @@ from copienator import (
|
|||||||
from copienator.annotation_actions import apply_checkbox_actions, apply_score_overrides
|
from copienator.annotation_actions import apply_checkbox_actions, apply_score_overrides
|
||||||
from copienator.annotation_data import AnnotationData, RefaireList, load_annotation_data
|
from copienator.annotation_data import AnnotationData, RefaireList, load_annotation_data
|
||||||
from copienator.filesystem import staged_files
|
from copienator.filesystem import staged_files
|
||||||
from reading_annotations import (
|
from copienator.commands.reading_annotations import (
|
||||||
concatenate,
|
concatenate,
|
||||||
detect_checks_and_notes,
|
detect_checks_and_notes,
|
||||||
has_significant_notes,
|
has_significant_notes,
|
||||||
@@ -398,3 +398,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|
||||||
@@ -259,7 +259,7 @@ def resolve_manual(workspace: EvaluationWorkspace) -> ExitCode:
|
|||||||
if refaire_tasks:
|
if refaire_tasks:
|
||||||
print(
|
print(
|
||||||
f"File {workspace.refaire_file.name} generated. Run "
|
f"File {workspace.refaire_file.name} generated. Run "
|
||||||
f'`python correction.py "{workspace.command_argument()}" --refaire` '
|
f'`python -m copienator correct "{workspace.command_argument()}" --refaire` '
|
||||||
"to process updates."
|
"to process updates."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -10,7 +10,7 @@ from pathlib import Path
|
|||||||
import fitz
|
import fitz
|
||||||
from pypdf import PdfReader, PdfWriter
|
from pypdf import PdfReader, PdfWriter
|
||||||
|
|
||||||
import utils
|
from copienator import utils
|
||||||
from copienator import (
|
from copienator import (
|
||||||
CliError,
|
CliError,
|
||||||
EvaluationWorkspace,
|
EvaluationWorkspace,
|
||||||
@@ -264,3 +264,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ from collections.abc import Sequence
|
|||||||
from google import genai
|
from google import genai
|
||||||
from google.genai import types
|
from google.genai import types
|
||||||
|
|
||||||
import config
|
from copienator import configuration as config
|
||||||
from copienator import (
|
from copienator import (
|
||||||
CliError,
|
CliError,
|
||||||
EvaluationWorkspace,
|
EvaluationWorkspace,
|
||||||
@@ -6,18 +6,18 @@ from pathlib import Path
|
|||||||
|
|
||||||
import ezodf
|
import ezodf
|
||||||
|
|
||||||
from config import CURRENT_SCORE_ODS_PATH
|
from copienator.configuration import CURRENT_SCORE_ODS_PATH
|
||||||
from utils import read_all_labels
|
from copienator.utils import read_all_labels
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
ODS_PATH = Path(CURRENT_SCORE_ODS_PATH).expanduser()
|
ODS_PATH = Path(CURRENT_SCORE_ODS_PATH).expanduser()
|
||||||
TARGET_DIR_NAME = "A Rendre"
|
TARGET_DIR_NAME = "A Rendre"
|
||||||
|
|
||||||
def main():
|
def main(argv=None):
|
||||||
parser = argparse.ArgumentParser(description="Update ODS with student scores.")
|
parser = argparse.ArgumentParser(description="Update ODS with student scores.")
|
||||||
parser.add_argument("work_dir", nargs="?", default=os.getcwd(), help="Directory to process")
|
parser.add_argument("work_dir", nargs="?", default=os.getcwd(), help="Directory to process")
|
||||||
parser.add_argument("--sum", action="store_true", help="Write only the total sum per student")
|
parser.add_argument("--sum", action="store_true", help="Write only the total sum per student")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
work_dir = os.path.abspath(args.work_dir)
|
work_dir = os.path.abspath(args.work_dir)
|
||||||
|
|
||||||
@@ -169,4 +169,4 @@ def main():
|
|||||||
print("Done.")
|
print("Done.")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from copienator import (
|
||||||
|
EvaluationWorkspace,
|
||||||
|
ExitCode,
|
||||||
|
evaluation_parser,
|
||||||
|
execute,
|
||||||
|
read_json,
|
||||||
|
workspace_from_args,
|
||||||
|
)
|
||||||
|
|
||||||
|
COPY_PATTERN = re.compile(r"Copie(\d+)")
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
return evaluation_parser("Verify that every answer PDF appears in group metadata.")
|
||||||
|
|
||||||
|
|
||||||
|
def collect_source_pdfs(copies_dir: Path) -> set[tuple[str, str]]:
|
||||||
|
source_pdfs: set[tuple[str, str]] = set()
|
||||||
|
for copy_dir in copies_dir.iterdir():
|
||||||
|
match = COPY_PATTERN.fullmatch(copy_dir.name)
|
||||||
|
if not match or not copy_dir.is_dir():
|
||||||
|
continue
|
||||||
|
copy_id = match.group(1)
|
||||||
|
for pdf_path in copy_dir.glob("*.pdf"):
|
||||||
|
source_pdfs.add((pdf_path.stem, copy_id))
|
||||||
|
return source_pdfs
|
||||||
|
|
||||||
|
|
||||||
|
def collect_grouped_pdfs(groups_dir: Path) -> tuple[set[tuple[str, str]], int]:
|
||||||
|
grouped: set[tuple[str, str]] = set()
|
||||||
|
read_errors = 0
|
||||||
|
for json_path in groups_dir.glob("*/Group_*.json"):
|
||||||
|
try:
|
||||||
|
data = read_json(json_path)
|
||||||
|
if not isinstance(data, list):
|
||||||
|
raise TypeError("expected a JSON array")
|
||||||
|
for entry in data:
|
||||||
|
grouped.add((str(entry[4]), str(entry[0])))
|
||||||
|
except (IndexError, OSError, TypeError, ValueError) as exc:
|
||||||
|
print(f"Error reading {json_path}: {exc}", file=sys.stderr)
|
||||||
|
read_errors += 1
|
||||||
|
return grouped, read_errors
|
||||||
|
|
||||||
|
|
||||||
|
def verify_groups(workspace: EvaluationWorkspace) -> ExitCode:
|
||||||
|
workspace.require_directories("Copies", "Par label")
|
||||||
|
source_pdfs = collect_source_pdfs(workspace.copies_dir)
|
||||||
|
grouped_pdfs, read_errors = collect_grouped_pdfs(workspace.groups_dir)
|
||||||
|
missing = source_pdfs - grouped_pdfs
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
print(f"Verification failed: {len(missing)} files missing from groups:")
|
||||||
|
for label, copy_id in sorted(missing):
|
||||||
|
print(f"Copie{copy_id}/{label}.pdf")
|
||||||
|
return ExitCode.FAILURE
|
||||||
|
if read_errors:
|
||||||
|
print("Verification incomplete because some metadata could not be read.")
|
||||||
|
return ExitCode.PARTIAL
|
||||||
|
print("Verification successful: all files accounted for.")
|
||||||
|
return ExitCode.SUCCESS
|
||||||
|
|
||||||
|
|
||||||
|
def run(workspace: EvaluationWorkspace) -> ExitCode:
|
||||||
|
return verify_groups(workspace)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
|
parser = build_parser()
|
||||||
|
return execute(parser, argv, lambda args: run(workspace_from_args(args)))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
|
||||||
|
def _user_config_path() -> Path | None:
|
||||||
|
explicit = os.environ.get("COPIENATOR_CONFIG", "").strip()
|
||||||
|
if explicit:
|
||||||
|
return Path(explicit).expanduser().resolve()
|
||||||
|
local = Path.cwd() / "config.py"
|
||||||
|
return local.resolve() if local.is_file() else None
|
||||||
|
|
||||||
|
|
||||||
|
def _load_user_config(path: Path) -> ModuleType:
|
||||||
|
if not path.is_file():
|
||||||
|
raise FileNotFoundError(f"Copienator configuration not found: {path}")
|
||||||
|
spec = importlib.util.spec_from_file_location("_copienator_user_config", path)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
raise ImportError(f"Could not load Copienator configuration: {path}")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_PATH = _user_config_path()
|
||||||
|
if CONFIG_PATH is None:
|
||||||
|
import default_config as _configuration
|
||||||
|
else:
|
||||||
|
_configuration = _load_user_config(CONFIG_PATH)
|
||||||
|
|
||||||
|
for _name in dir(_configuration):
|
||||||
|
if not _name.startswith("_"):
|
||||||
|
globals()[_name] = getattr(_configuration, _name)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,76 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import sys
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Command:
|
||||||
|
module: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
|
||||||
|
COMMANDS: dict[str, Command] = {
|
||||||
|
"statement": Command("gemini_for_enonce", "Analyse the exam statement with Gemini"),
|
||||||
|
"statement-personal": Command("enonce_info", "Generate personal statement metadata"),
|
||||||
|
"copies": Command("copies_tools", "Rotate or rename scanned copies"),
|
||||||
|
"page-split": Command("page_splitter", "Split and reorder scanned PDF pages"),
|
||||||
|
"crop-labels": Command("cutleft", "Crop the label margin from copies"),
|
||||||
|
"labels": Command("gemini_for_labels", "Detect question labels with Gemini"),
|
||||||
|
"review-labels": Command("plotting", "Review detected labels interactively"),
|
||||||
|
"split-answers": Command("splitting_int", "Split copies into answers"),
|
||||||
|
"group-answers": Command("grouping", "Group answers by question"),
|
||||||
|
"verify-groups": Command("verify_groups", "Verify grouped answer metadata"),
|
||||||
|
"correct": Command("correction", "Generate or integrate corrections"),
|
||||||
|
"batch-submit": Command("submit_batches", "Submit Gemini batch jobs"),
|
||||||
|
"batch-status": Command("batch_status", "Inspect Gemini batch jobs"),
|
||||||
|
"batch-fetch": Command("fetch_batched_results", "Fetch Gemini batch results"),
|
||||||
|
"post-correction": Command("post_correction", "Clean generated correction text"),
|
||||||
|
"resolve-manual": Command("resolve_manual", "Resolve manual label conflicts"),
|
||||||
|
"annotate-simple": Command("annotating", "Generate simple annotations"),
|
||||||
|
"annotate-checks": Command("annotating_with_checks", "Generate checkable annotations"),
|
||||||
|
"annotate-grouped": Command("annotating_by_label", "Generate grouped annotations"),
|
||||||
|
"export": Command("export", "Export annotations"),
|
||||||
|
"import": Command("import_annotations", "Import handwritten annotations"),
|
||||||
|
"read-annotations": Command("reading_annotations", "Read checkable annotations"),
|
||||||
|
"read-grouped": Command("reading_grouped_annotations", "Read grouped annotations"),
|
||||||
|
"giving-names": Command("giving_names", "Name copies and prepare A Rendre"),
|
||||||
|
"update-ods": Command("update_ods", "Update the configured score spreadsheet"),
|
||||||
|
"add-final-score": Command("add_final_score", "Stamp final scores on copies"),
|
||||||
|
"gui": Command("@gui", "Launch the graphical workflow assistant"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _print_help() -> None:
|
||||||
|
print("Usage: copienator COMMAND [ARGUMENTS]")
|
||||||
|
print(" python -m copienator COMMAND [ARGUMENTS]")
|
||||||
|
print("\nCommands:")
|
||||||
|
width = max(len(name) for name in COMMANDS)
|
||||||
|
for name, command in COMMANDS.items():
|
||||||
|
print(f" {name:<{width}} {command.description}")
|
||||||
|
print("\nUse 'copienator COMMAND --help' for command-specific help.")
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
|
arguments = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
if not arguments or arguments[0] in {"-h", "--help"}:
|
||||||
|
_print_help()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
command_name = arguments.pop(0)
|
||||||
|
command = COMMANDS.get(command_name)
|
||||||
|
if command is None:
|
||||||
|
print(f"Unknown command: {command_name}", file=sys.stderr)
|
||||||
|
print("Use 'copienator --help' to list commands.", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
module_name = (
|
||||||
|
"copienator_gui.__main__"
|
||||||
|
if command.module == "@gui"
|
||||||
|
else f"copienator.commands.{command.module}"
|
||||||
|
)
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
result = module.main(arguments)
|
||||||
|
return int(result or 0)
|
||||||
@@ -118,3 +118,4 @@ def replace_with_link_or_copy(
|
|||||||
|
|
||||||
shutil.copy2(source_path, destination_path)
|
shutil.copy2(source_path, destination_path)
|
||||||
return "copy"
|
return "copy"
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import io
|
import io
|
||||||
import utils
|
from . import utils
|
||||||
|
|
||||||
main_prompt = """I'm giving you an image of several written answers to an exam.
|
main_prompt = """I'm giving you an image of several written answers to an exam.
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ Here is a possible correct answer :
|
|||||||
You are asked to score the question or exercice labeled `<<label>>`,
|
You are asked to score the question or exercice labeled `<<label>>`,
|
||||||
do not score or give feedback to any other question."""
|
do not score or give feedback to any other question."""
|
||||||
|
|
||||||
from utils import get_label_text_content, get_label_sol_content, get_label_persp_content
|
from .utils import get_label_text_content, get_label_sol_content, get_label_persp_content
|
||||||
|
|
||||||
def make_prompt(input_dir,full_label):
|
def make_prompt(input_dir,full_label):
|
||||||
text = get_label_text_content(input_dir, full_label) or ""
|
text = get_label_text_content(input_dir, full_label) or ""
|
||||||
@@ -298,3 +298,4 @@ Here is a list of all possible labels. You need to answer with a list one of the
|
|||||||
])]
|
])]
|
||||||
config = types.GenerateContentConfig(temperature=1.0, response_mime_type="application/json")
|
config = types.GenerateContentConfig(temperature=1.0, response_mime_type="application/json")
|
||||||
return contents, config
|
return contents, config
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@ import re
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from copienator import EvaluationWorkspace
|
from copienator import EvaluationWorkspace
|
||||||
from platform_utils import validate_windows_labels
|
from .platform import validate_windows_labels
|
||||||
|
|
||||||
def natural_key(text):
|
def natural_key(text):
|
||||||
return [int(c) if c.isdigit() else c.lower() for c in re.split(r'(\d+)', str(text))]
|
return [int(c) if c.isdigit() else c.lower() for c in re.split(r'(\d+)', str(text))]
|
||||||
@@ -30,7 +30,7 @@ def enonce_total(base_dir):
|
|||||||
import os
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
|
|
||||||
from platform_utils import open_path
|
from .platform import open_path
|
||||||
|
|
||||||
def edit_file_and_enter(file):
|
def edit_file_and_enter(file):
|
||||||
editor = os.environ.get("EDITOR")
|
editor = os.environ.get("EDITOR")
|
||||||
@@ -139,7 +139,7 @@ import tempfile
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from config import LATEX_AFTER, LATEX_BEFORE
|
from .configuration import LATEX_AFTER, LATEX_BEFORE
|
||||||
|
|
||||||
|
|
||||||
def compile_to_pdf(text, output_pdf_path):
|
def compile_to_pdf(text, output_pdf_path):
|
||||||
@@ -8,25 +8,23 @@ from copienator_gui.app import CopienatorApp
|
|||||||
|
|
||||||
def personal_steps_enabled() -> bool:
|
def personal_steps_enabled() -> bool:
|
||||||
try:
|
try:
|
||||||
from config import SHOW_PERSONAL_STEPS
|
from copienator.configuration import SHOW_PERSONAL_STEPS
|
||||||
except (ImportError, AttributeError):
|
|
||||||
try:
|
|
||||||
from default_config import SHOW_PERSONAL_STEPS
|
|
||||||
except (ImportError, AttributeError):
|
except (ImportError, AttributeError):
|
||||||
return False
|
return False
|
||||||
return bool(SHOW_PERSONAL_STEPS)
|
return bool(SHOW_PERSONAL_STEPS)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main(argv=None) -> int:
|
||||||
parser = argparse.ArgumentParser(description="Interface graphique du workflow Copienator")
|
parser = argparse.ArgumentParser(description="Interface graphique du workflow Copienator")
|
||||||
parser.add_argument("evaluation", nargs="?", type=Path, help="Dossier d’évaluation à ouvrir")
|
parser.add_argument("evaluation", nargs="?", type=Path, help="Dossier d’évaluation à ouvrir")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
repository = Path(__file__).resolve().parent
|
repository = Path.cwd().resolve()
|
||||||
evaluation = args.evaluation.resolve() if args.evaluation else None
|
evaluation = args.evaluation.resolve() if args.evaluation else None
|
||||||
app = CopienatorApp(repository, personal_steps_enabled(), evaluation)
|
app = CopienatorApp(repository, personal_steps_enabled(), evaluation)
|
||||||
app.mainloop()
|
app.mainloop()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
raise SystemExit(main())
|
||||||
@@ -8,7 +8,7 @@ from tkinter import filedialog, messagebox, ttk
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from copienator import ExitCode
|
from copienator import ExitCode
|
||||||
from platform_utils import WindowsLabelError, open_path, validate_windows_labels
|
from copienator.platform import WindowsLabelError, open_path, validate_windows_labels
|
||||||
|
|
||||||
from .diagnostics import collect_diagnostics
|
from .diagnostics import collect_diagnostics
|
||||||
from .runner import ProcessRunner
|
from .runner import ProcessRunner
|
||||||
@@ -71,7 +71,7 @@ def detected_annotation_directories(evaluation: Path) -> tuple[str, ...]:
|
|||||||
|
|
||||||
def plotting_shortcut_lines() -> list[str]:
|
def plotting_shortcut_lines() -> list[str]:
|
||||||
try:
|
try:
|
||||||
from config import PLOTTING_KB
|
from copienator.configuration import PLOTTING_KB
|
||||||
except (ImportError, AttributeError):
|
except (ImportError, AttributeError):
|
||||||
from default_config import PLOTTING_KB
|
from default_config import PLOTTING_KB
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import sys
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from platform_utils import windows_filename_problems
|
from copienator.platform import windows_filename_problems
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -111,7 +111,7 @@ def collect_diagnostics(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
from config import CURRENT_SCORE_ODS_PATH, FINAL_SCORE_ODS_PATH
|
from copienator.configuration import CURRENT_SCORE_ODS_PATH, FINAL_SCORE_ODS_PATH
|
||||||
|
|
||||||
for label, configured_path in (
|
for label, configured_path in (
|
||||||
("ODS courant", CURRENT_SCORE_ODS_PATH),
|
("ODS courant", CURRENT_SCORE_ODS_PATH),
|
||||||
|
|||||||
+33
-33
@@ -90,8 +90,8 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Analyser l’énoncé",
|
"Analyser l’énoncé",
|
||||||
"Détecte les questions, leurs groupes, le corrigé et les indications de barème.",
|
"Détecte les questions, leurs groupes, le corrigé et les indications de barème.",
|
||||||
(
|
(
|
||||||
python("gemini", "Analyse avec Gemini", "gemini_for_enonce.py"),
|
python("gemini", "Analyse avec Gemini", "statement"),
|
||||||
python("personal", "Alternative enonce_info.py", "enonce_info.py"),
|
python("personal", "Alternative personnelle", "statement-personal"),
|
||||||
),
|
),
|
||||||
arguments=(
|
arguments=(
|
||||||
arg_target("Dossier de l’évaluation"),
|
arg_target("Dossier de l’évaluation"),
|
||||||
@@ -124,7 +124,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
CommandVariant(
|
CommandVariant(
|
||||||
"rotate",
|
"rotate",
|
||||||
"Rotation",
|
"Rotation",
|
||||||
"copies_tools.py",
|
"copies",
|
||||||
"python",
|
"python",
|
||||||
("rotate",),
|
("rotate",),
|
||||||
fixed_args_before_positionals=True,
|
fixed_args_before_positionals=True,
|
||||||
@@ -142,7 +142,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
CommandVariant(
|
CommandVariant(
|
||||||
"rename",
|
"rename",
|
||||||
"Renommage",
|
"Renommage",
|
||||||
"copies_tools.py",
|
"copies",
|
||||||
"python",
|
"python",
|
||||||
("rename",),
|
("rename",),
|
||||||
fixed_args_before_positionals=True,
|
fixed_args_before_positionals=True,
|
||||||
@@ -155,7 +155,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Prétraitement des copies",
|
"Prétraitement des copies",
|
||||||
"Séparer et réordonner les pages",
|
"Séparer et réordonner les pages",
|
||||||
"Ouvre l’outil interactif de découpage A3 vers A4. La cible peut être un dossier ou un PDF.",
|
"Ouvre l’outil interactif de découpage A3 vers A4. La cible peut être un dossier ou un PDF.",
|
||||||
(python("default", "Séparation des pages", "page_splitter.py"),),
|
(python("default", "Séparation des pages", "page-split"),),
|
||||||
arguments=(arg_target(),),
|
arguments=(arg_target(),),
|
||||||
artifacts=("Copies", "Copies Originales"),
|
artifacts=("Copies", "Copies Originales"),
|
||||||
),
|
),
|
||||||
@@ -164,7 +164,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Prétraitement des copies",
|
"Prétraitement des copies",
|
||||||
"Découper la marge des labels",
|
"Découper la marge des labels",
|
||||||
"Produit les images de la partie gauche des copies. Une copie précise peut être ciblée.",
|
"Produit les images de la partie gauche des copies. Une copie précise peut être ciblée.",
|
||||||
(python("default", "Découpe", "cutleft.py"),),
|
(python("default", "Découpe", "crop-labels"),),
|
||||||
arguments=(
|
arguments=(
|
||||||
arg_target(),
|
arg_target(),
|
||||||
ArgumentSpec("fullpage", "Toujours utiliser la page entière", "bool", "--fullpage"),
|
ArgumentSpec("fullpage", "Toujours utiliser la page entière", "bool", "--fullpage"),
|
||||||
@@ -177,7 +177,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Labels et regroupement",
|
"Labels et regroupement",
|
||||||
"Détecter les labels avec Gemini",
|
"Détecter les labels avec Gemini",
|
||||||
"Identifie les labels dans les images produites par la découpe gauche.",
|
"Identifie les labels dans les images produites par la découpe gauche.",
|
||||||
(python("default", "Détection des labels", "gemini_for_labels.py"),),
|
(python("default", "Détection des labels", "labels"),),
|
||||||
arguments=(
|
arguments=(
|
||||||
arg_target(),
|
arg_target(),
|
||||||
ArgumentSpec("overwrite", "Régénérer les résultats", "bool", "--overwrite"),
|
ArgumentSpec("overwrite", "Régénérer les résultats", "bool", "--overwrite"),
|
||||||
@@ -190,7 +190,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Labels et regroupement",
|
"Labels et regroupement",
|
||||||
"Vérifier visuellement les labels",
|
"Vérifier visuellement les labels",
|
||||||
"Ouvre la fenêtre de vérification. La cible peut être toute l’évaluation ou une copie.",
|
"Ouvre la fenêtre de vérification. La cible peut être toute l’évaluation ou une copie.",
|
||||||
(python("default", "Vérification", "plotting.py"),),
|
(python("default", "Vérification", "review-labels"),),
|
||||||
arguments=(arg_target(),),
|
arguments=(arg_target(),),
|
||||||
requires=("labels", "Cutleft"),
|
requires=("labels", "Cutleft"),
|
||||||
artifacts=("Copies/Copie*.json",),
|
artifacts=("Copies/Copie*.json",),
|
||||||
@@ -200,7 +200,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Labels et regroupement",
|
"Labels et regroupement",
|
||||||
"Découper les réponses par question",
|
"Découper les réponses par question",
|
||||||
"Découpe les copies à partir des coordonnées de labels vérifiées.",
|
"Découpe les copies à partir des coordonnées de labels vérifiées.",
|
||||||
(python("default", "Découpage", "splitting_int.py"),),
|
(python("default", "Découpage", "split-answers"),),
|
||||||
arguments=(arg_target(),),
|
arguments=(arg_target(),),
|
||||||
requires=("Copies",),
|
requires=("Copies",),
|
||||||
artifacts=("Copies/Copie*/*",),
|
artifacts=("Copies/Copie*/*",),
|
||||||
@@ -211,7 +211,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Labels et regroupement",
|
"Labels et regroupement",
|
||||||
"Regrouper les réponses",
|
"Regrouper les réponses",
|
||||||
"Regroupe les réponses portant le même label pour préparer les requêtes.",
|
"Regroupe les réponses portant le même label pour préparer les requêtes.",
|
||||||
(python("default", "Regroupement", "grouping.py"),),
|
(python("default", "Regroupement", "group-answers"),),
|
||||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||||
requires=("Copies",),
|
requires=("Copies",),
|
||||||
artifacts=("Par label",),
|
artifacts=("Par label",),
|
||||||
@@ -222,7 +222,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Labels et regroupement",
|
"Labels et regroupement",
|
||||||
"Vérifier les groupes produits",
|
"Vérifier les groupes produits",
|
||||||
"Vérifie que chaque réponse PDF apparaît dans les métadonnées des groupes.",
|
"Vérifie que chaque réponse PDF apparaît dans les métadonnées des groupes.",
|
||||||
(python("default", "Vérification des groupes", "verify_groups.py"),),
|
(python("default", "Vérification des groupes", "verify-groups"),),
|
||||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||||
optional=True,
|
optional=True,
|
||||||
requires=("Copies", "Par label"),
|
requires=("Copies", "Par label"),
|
||||||
@@ -233,17 +233,17 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Lancer ou intégrer la correction",
|
"Lancer ou intégrer la correction",
|
||||||
"Choisir une correction immédiate, batch, hybride, une recorrection, ou l’intégration d’un batch.",
|
"Choisir une correction immédiate, batch, hybride, une recorrection, ou l’intégration d’un batch.",
|
||||||
(
|
(
|
||||||
python("live", "Correction immédiate", "correction.py"),
|
python("live", "Correction immédiate", "correct"),
|
||||||
python("batch", "Préparer toutes les requêtes batch", "correction.py", fixed_args=("--batch",)),
|
python("batch", "Préparer toutes les requêtes batch", "correct", fixed_args=("--batch",)),
|
||||||
python("hybrid", "Batch à partir d’un label", "correction.py"),
|
python("hybrid", "Batch à partir d’un label", "correct"),
|
||||||
python("refaire", "Recorrection depuis refaire.json", "correction.py", fixed_args=("--refaire",)),
|
python("refaire", "Recorrection depuis refaire.json", "correct", fixed_args=("--refaire",)),
|
||||||
python(
|
python(
|
||||||
"integrate",
|
"integrate",
|
||||||
"Intégrer les résultats batch",
|
"Intégrer les résultats batch",
|
||||||
"correction.py",
|
"correct",
|
||||||
fixed_args=("--deal-with-batched",),
|
fixed_args=("--deal-with-batched",),
|
||||||
),
|
),
|
||||||
python("reset", "Réinitialiser les corrections", "correction.py", fixed_args=("--reset",), dangerous=True),
|
python("reset", "Réinitialiser les corrections", "correct", fixed_args=("--reset",), dangerous=True),
|
||||||
),
|
),
|
||||||
arguments=(
|
arguments=(
|
||||||
arg_target("Évaluation ou image Group_X.jpg"),
|
arg_target("Évaluation ou image Group_X.jpg"),
|
||||||
@@ -259,7 +259,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Correction",
|
"Correction",
|
||||||
"Envoyer les batchs",
|
"Envoyer les batchs",
|
||||||
"Envoie à Gemini les fichiers JSONL produits par le mode batch.",
|
"Envoie à Gemini les fichiers JSONL produits par le mode batch.",
|
||||||
(python("default", "Envoi", "submit_batches.py"),),
|
(python("default", "Envoi", "batch-submit"),),
|
||||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||||
optional=True,
|
optional=True,
|
||||||
artifacts=("batch_jobs.json",),
|
artifacts=("batch_jobs.json",),
|
||||||
@@ -270,7 +270,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Correction",
|
"Correction",
|
||||||
"Consulter l’état des batchs",
|
"Consulter l’état des batchs",
|
||||||
"Affiche les jobs Gemini en cours. L’identifiant de téléchargement est facultatif.",
|
"Affiche les jobs Gemini en cours. L’identifiant de téléchargement est facultatif.",
|
||||||
(python("default", "État des batchs", "batch_status.py"),),
|
(python("default", "État des batchs", "batch-status"),),
|
||||||
arguments=(ArgumentSpec("download", "Télécharger le job", "text", "--download"),),
|
arguments=(ArgumentSpec("download", "Télécharger le job", "text", "--download"),),
|
||||||
optional=True,
|
optional=True,
|
||||||
skip_for_live_correction=True,
|
skip_for_live_correction=True,
|
||||||
@@ -280,7 +280,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Correction",
|
"Correction",
|
||||||
"Récupérer les résultats batch",
|
"Récupérer les résultats batch",
|
||||||
"Télécharge et rassemble les réponses des jobs terminés.",
|
"Télécharge et rassemble les réponses des jobs terminés.",
|
||||||
(python("default", "Récupération", "fetch_batched_results.py"),),
|
(python("default", "Récupération", "batch-fetch"),),
|
||||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||||
optional=True,
|
optional=True,
|
||||||
skip_for_live_correction=True,
|
skip_for_live_correction=True,
|
||||||
@@ -290,7 +290,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Correction",
|
"Correction",
|
||||||
"Nettoyer la correction",
|
"Nettoyer la correction",
|
||||||
"Corrige certains problèmes d’encodage et prépare le texte pour LaTeX.",
|
"Corrige certains problèmes d’encodage et prépare le texte pour LaTeX.",
|
||||||
(python("default", "Post-correction", "post-correction.py"),),
|
(python("default", "Post-correction", "post-correction"),),
|
||||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||||
requires=("correction.json",),
|
requires=("correction.json",),
|
||||||
),
|
),
|
||||||
@@ -299,7 +299,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Correction",
|
"Correction",
|
||||||
"Résoudre les conflits manuels",
|
"Résoudre les conflits manuels",
|
||||||
"Étape conditionnelle, uniquement si manual_resolutions.txt contient des conflits à traiter.",
|
"Étape conditionnelle, uniquement si manual_resolutions.txt contient des conflits à traiter.",
|
||||||
(python("default", "Résolution", "resolve_manual.py"),),
|
(python("default", "Résolution", "resolve-manual"),),
|
||||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||||
optional=True,
|
optional=True,
|
||||||
requires=("manual_resolutions.txt", "correction.json"),
|
requires=("manual_resolutions.txt", "correction.json"),
|
||||||
@@ -311,9 +311,9 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Générer les copies annotées",
|
"Générer les copies annotées",
|
||||||
"Les trois modes sont exclusifs pour un parcours donné.",
|
"Les trois modes sont exclusifs pour un parcours donné.",
|
||||||
(
|
(
|
||||||
python("simple", "Annotations simples (Anot)", "annotating.py"),
|
python("simple", "Annotations simples (Anot)", "annotate-simple"),
|
||||||
python("checks", "Annotations avec cases (Bnot)", "annotating_with_checks.py"),
|
python("checks", "Annotations avec cases (Bnot)", "annotate-checks"),
|
||||||
python("grouped", "Annotations groupées (BGnot)", "annotating_by_label.py"),
|
python("grouped", "Annotations groupées (BGnot)", "annotate-grouped"),
|
||||||
),
|
),
|
||||||
arguments=(
|
arguments=(
|
||||||
arg_target("Dossier de l’évaluation"),
|
arg_target("Dossier de l’évaluation"),
|
||||||
@@ -328,7 +328,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Génération des annotations",
|
"Génération des annotations",
|
||||||
"Exporter",
|
"Exporter",
|
||||||
"Exporte les annotations vers le dossier EXPORT_DIR défini dans config.py.",
|
"Exporte les annotations vers le dossier EXPORT_DIR défini dans config.py.",
|
||||||
(python("default", "Export", "export.py"),),
|
(python("default", "Export", "export"),),
|
||||||
arguments=(
|
arguments=(
|
||||||
arg_target("Dossier de l’évaluation"),
|
arg_target("Dossier de l’évaluation"),
|
||||||
ArgumentSpec(
|
ArgumentSpec(
|
||||||
@@ -355,7 +355,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Correction manuscrite",
|
"Correction manuscrite",
|
||||||
"Importer les annotations manuscrites",
|
"Importer les annotations manuscrites",
|
||||||
"Copie les PDF présents dans IMPORT_DIR vers l’évaluation.",
|
"Copie les PDF présents dans IMPORT_DIR vers l’évaluation.",
|
||||||
(python("default", "Import", "import.py"),),
|
(python("default", "Import", "import"),),
|
||||||
arguments=(
|
arguments=(
|
||||||
arg_target("Dossier de l’évaluation"),
|
arg_target("Dossier de l’évaluation"),
|
||||||
ArgumentSpec(
|
ArgumentSpec(
|
||||||
@@ -375,8 +375,8 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Lire les annotations manuscrites",
|
"Lire les annotations manuscrites",
|
||||||
"Le mode doit correspondre au mode choisi lors de la génération des annotations.",
|
"Le mode doit correspondre au mode choisi lors de la génération des annotations.",
|
||||||
(
|
(
|
||||||
python("standard", "Lecture Bnot", "reading_annotations.py"),
|
python("standard", "Lecture Bnot", "read-annotations"),
|
||||||
python("grouped", "Lecture BGnot", "reading_grouped_annotations.py"),
|
python("grouped", "Lecture BGnot", "read-grouped"),
|
||||||
),
|
),
|
||||||
arguments=(
|
arguments=(
|
||||||
arg_target("Dossier de l’évaluation"),
|
arg_target("Dossier de l’évaluation"),
|
||||||
@@ -389,7 +389,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Finalisation",
|
"Finalisation",
|
||||||
"Attribuer les noms et préparer A Rendre",
|
"Attribuer les noms et préparer A Rendre",
|
||||||
"Crée le dossier A Rendre à partir du dossier d’annotations choisi.",
|
"Crée le dossier A Rendre à partir du dossier d’annotations choisi.",
|
||||||
(python("default", "Attribution des noms", "giving_names.py"),),
|
(python("default", "Attribution des noms", "giving-names"),),
|
||||||
arguments=(
|
arguments=(
|
||||||
arg_target("Dossier de l’évaluation"),
|
arg_target("Dossier de l’évaluation"),
|
||||||
ArgumentSpec(
|
ArgumentSpec(
|
||||||
@@ -424,7 +424,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Étapes personnelles",
|
"Étapes personnelles",
|
||||||
"Mettre à jour le fichier ODS",
|
"Mettre à jour le fichier ODS",
|
||||||
"Transfère les scores, avec la possibilité de n’écrire que leur somme.",
|
"Transfère les scores, avec la possibilité de n’écrire que leur somme.",
|
||||||
(python("default", "Mise à jour ODS", "update_ods.py"),),
|
(python("default", "Mise à jour ODS", "update-ods"),),
|
||||||
arguments=(
|
arguments=(
|
||||||
arg_target("Dossier de l’évaluation"),
|
arg_target("Dossier de l’évaluation"),
|
||||||
ArgumentSpec("sum", "Écrire seulement la somme", "bool", "--sum"),
|
ArgumentSpec("sum", "Écrire seulement la somme", "bool", "--sum"),
|
||||||
@@ -452,7 +452,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Étapes personnelles",
|
"Étapes personnelles",
|
||||||
"Ajouter le score final",
|
"Ajouter le score final",
|
||||||
"Génère les fichiers de diffusion avec le score final.",
|
"Génère les fichiers de diffusion avec le score final.",
|
||||||
(python("default", "Score final", "add_final_score.py"),),
|
(python("default", "Score final", "add-final-score"),),
|
||||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||||
personal=True,
|
personal=True,
|
||||||
),
|
),
|
||||||
@@ -494,7 +494,7 @@ def build_command(
|
|||||||
|
|
||||||
program_path = repository / variant.program
|
program_path = repository / variant.program
|
||||||
if variant.kind == "python":
|
if variant.kind == "python":
|
||||||
command = [sys.executable, "-u", str(program_path)]
|
command = [sys.executable, "-u", "-m", "copienator", variant.program]
|
||||||
elif variant.kind == "shell":
|
elif variant.kind == "shell":
|
||||||
command = [str(program_path)]
|
command = [str(program_path)]
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=68"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "copienator"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Workflow assistant for preparing and correcting scanned exams"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
copienator = "copienator.dispatcher:main"
|
||||||
|
copienator-gui = "copienator_gui.__main__:main"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
py-modules = ["default_config"]
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
include = ["copienator*", "copienator_gui*"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
copienator = ["data/*.txt"]
|
||||||
+50
-27
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import importlib.util
|
import importlib
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -31,6 +31,7 @@ from copienator import (
|
|||||||
from copienator.annotation_actions import apply_checkbox_actions, apply_score_overrides
|
from copienator.annotation_actions import apply_checkbox_actions, apply_score_overrides
|
||||||
from copienator.annotation_data import AnnotationLoadResult, load_annotation_data
|
from copienator.annotation_data import AnnotationLoadResult, load_annotation_data
|
||||||
from copienator.filesystem import staged_directory, staged_files
|
from copienator.filesystem import staged_directory, staged_files
|
||||||
|
from copienator.dispatcher import COMMANDS, main as dispatcher_main
|
||||||
from copienator_gui.app import (
|
from copienator_gui.app import (
|
||||||
CopienatorApp,
|
CopienatorApp,
|
||||||
DEFAULT_HTTPS_PROXY,
|
DEFAULT_HTTPS_PROXY,
|
||||||
@@ -45,8 +46,8 @@ from copienator_gui.diagnostics import collect_diagnostics
|
|||||||
from copienator_gui.runner import ProcessRunner
|
from copienator_gui.runner import ProcessRunner
|
||||||
from copienator_gui.state import StateStore
|
from copienator_gui.state import StateStore
|
||||||
from copienator_gui.workflow import build_command, build_workflow, evaluation_argument
|
from copienator_gui.workflow import build_command, build_workflow, evaluation_argument
|
||||||
from copies_tools import rename_all, rotate_all
|
from copienator.commands.copies_tools import rename_all, rotate_all
|
||||||
from platform_utils import (
|
from copienator.platform import (
|
||||||
WindowsLabelError,
|
WindowsLabelError,
|
||||||
replace_with_link_or_copy,
|
replace_with_link_or_copy,
|
||||||
safe_filename,
|
safe_filename,
|
||||||
@@ -57,18 +58,16 @@ from platform_utils import (
|
|||||||
REPOSITORY = Path(__file__).resolve().parents[1]
|
REPOSITORY = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
def load_script_module(filename: str, module_name: str):
|
def load_script_module(filename: str, _module_name: str):
|
||||||
spec = importlib.util.spec_from_file_location(module_name, REPOSITORY / filename)
|
stem = Path(filename).stem.replace("-", "_")
|
||||||
if spec is None or spec.loader is None:
|
if stem == "import":
|
||||||
raise RuntimeError(f"Could not load {filename}")
|
stem = "import_annotations"
|
||||||
module = importlib.util.module_from_spec(spec)
|
return importlib.import_module(f"copienator.commands.{stem}")
|
||||||
sys.modules[module_name] = module
|
|
||||||
try:
|
|
||||||
spec.loader.exec_module(module)
|
def command_arguments(command: list[str]) -> list[str]:
|
||||||
except Exception:
|
assert command[2:4] == ["-m", "copienator"]
|
||||||
sys.modules.pop(module_name, None)
|
return command[5:]
|
||||||
raise
|
|
||||||
return module
|
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceTests(unittest.TestCase):
|
class WorkspaceTests(unittest.TestCase):
|
||||||
@@ -129,6 +128,24 @@ class WorkspaceTests(unittest.TestCase):
|
|||||||
self.assertEqual(workspace.names_file(), root / "names")
|
self.assertEqual(workspace.names_file(), root / "names")
|
||||||
|
|
||||||
|
|
||||||
|
class DispatcherTests(unittest.TestCase):
|
||||||
|
def test_unknown_command_has_argument_error_code(self) -> None:
|
||||||
|
with redirect_stderr(io.StringIO()):
|
||||||
|
self.assertEqual(dispatcher_main(["does-not-exist"]), 2)
|
||||||
|
|
||||||
|
def test_every_gui_python_command_is_registered(self) -> None:
|
||||||
|
for step in build_workflow(True):
|
||||||
|
for variant in step.variants:
|
||||||
|
if variant.kind == "python":
|
||||||
|
with self.subTest(step=step.id, command=variant.program):
|
||||||
|
self.assertIn(variant.program, COMMANDS)
|
||||||
|
|
||||||
|
def test_root_contains_only_configuration_python_modules(self) -> None:
|
||||||
|
root_modules = {path.name for path in REPOSITORY.glob("*.py")}
|
||||||
|
self.assertIn("default_config.py", root_modules)
|
||||||
|
self.assertLessEqual(root_modules, {"config.py", "default_config.py"})
|
||||||
|
|
||||||
|
|
||||||
class AtomicJsonTests(unittest.TestCase):
|
class AtomicJsonTests(unittest.TestCase):
|
||||||
def test_atomic_binary_round_trip(self) -> None:
|
def test_atomic_binary_round_trip(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
@@ -561,7 +578,9 @@ class StandardCliTests(unittest.TestCase):
|
|||||||
variant = next(item for item in step.variants if item.id == variant_id)
|
variant = next(item for item in step.variants if item.id == variant_id)
|
||||||
command = build_command(REPOSITORY, step, variant, values, evaluation)
|
command = build_command(REPOSITORY, step, variant, values, evaluation)
|
||||||
with self.subTest(script=module_name):
|
with self.subTest(script=module_name):
|
||||||
parsed = self.modules[module_name].build_parser().parse_args(command[3:])
|
parsed = self.modules[module_name].build_parser().parse_args(
|
||||||
|
command_arguments(command)
|
||||||
|
)
|
||||||
parsed_path = getattr(parsed, "evaluation", None) or parsed.target
|
parsed_path = getattr(parsed, "evaluation", None) or parsed.target
|
||||||
self.assertEqual(str(parsed_path), evaluation)
|
self.assertEqual(str(parsed_path), evaluation)
|
||||||
|
|
||||||
@@ -577,7 +596,7 @@ class StandardCliTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
with self.subTest(script=f"copies_tools:{step_id}"):
|
with self.subTest(script=f"copies_tools:{step_id}"):
|
||||||
parsed = self.modules["copies_tools"].build_parser().parse_args(
|
parsed = self.modules["copies_tools"].build_parser().parse_args(
|
||||||
command[3:]
|
command_arguments(command)
|
||||||
)
|
)
|
||||||
self.assertEqual(parsed.operation, step_id)
|
self.assertEqual(parsed.operation, step_id)
|
||||||
self.assertEqual(str(parsed.evaluation), evaluation)
|
self.assertEqual(str(parsed.evaluation), evaluation)
|
||||||
@@ -1521,7 +1540,7 @@ class WorkflowTests(unittest.TestCase):
|
|||||||
"default",
|
"default",
|
||||||
{"target": self.evaluation, "annotation_dir": "Bnot"},
|
{"target": self.evaluation, "annotation_dir": "Bnot"},
|
||||||
)
|
)
|
||||||
self.assertEqual(command[3:], [self.evaluation, "Bnot"])
|
self.assertEqual(command_arguments(command), [self.evaluation, "Bnot"])
|
||||||
|
|
||||||
def test_copy_listing_prefers_processed_copies(self) -> None:
|
def test_copy_listing_prefers_processed_copies(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
@@ -1607,27 +1626,31 @@ class WorkflowTests(unittest.TestCase):
|
|||||||
"live",
|
"live",
|
||||||
{"target": self.evaluation, "overwrite": True, "limit": "0"},
|
{"target": self.evaluation, "overwrite": True, "limit": "0"},
|
||||||
)
|
)
|
||||||
self.assertEqual(command[3:], [self.evaluation, "--overwrite", "--limit", "0"])
|
self.assertEqual(
|
||||||
self.assertIn("correction.py", command[2])
|
command_arguments(command),
|
||||||
|
[self.evaluation, "--overwrite", "--limit", "0"],
|
||||||
|
)
|
||||||
|
self.assertEqual(command[4], "correct")
|
||||||
|
|
||||||
def test_hybrid_correction_arguments(self) -> None:
|
def test_hybrid_correction_arguments(self) -> None:
|
||||||
command = self.command(
|
command = self.command(
|
||||||
"correction", "hybrid", {"target": self.evaluation, "batch_from": "Ex 4"}
|
"correction", "hybrid", {"target": self.evaluation, "batch_from": "Ex 4"}
|
||||||
)
|
)
|
||||||
self.assertEqual(command[3:], [self.evaluation, "--batch-from", "Ex 4"])
|
self.assertEqual(
|
||||||
|
command_arguments(command), [self.evaluation, "--batch-from", "Ex 4"]
|
||||||
|
)
|
||||||
|
|
||||||
def test_annotation_variants_are_exclusive_commands(self) -> None:
|
def test_annotation_variants_are_exclusive_commands(self) -> None:
|
||||||
command = self.command(
|
command = self.command(
|
||||||
"annotation", "grouped", {"target": self.evaluation, "overwrite": True}
|
"annotation", "grouped", {"target": self.evaluation, "overwrite": True}
|
||||||
)
|
)
|
||||||
self.assertIn("annotating_by_label.py", command[2])
|
self.assertEqual(command[4], "annotate-grouped")
|
||||||
self.assertNotIn("annotating.py", command[2])
|
|
||||||
|
|
||||||
def test_copy_preparation_commands_are_python(self) -> None:
|
def test_copy_preparation_commands_are_python(self) -> None:
|
||||||
rotate = self.command("rotate", "rotate", {"target": self.evaluation})
|
rotate = self.command("rotate", "rotate", {"target": self.evaluation})
|
||||||
rename = self.command("rename", "rename", {"target": self.evaluation})
|
rename = self.command("rename", "rename", {"target": self.evaluation})
|
||||||
self.assertEqual(rotate[2:], [str(REPOSITORY / "copies_tools.py"), "rotate", self.evaluation])
|
self.assertEqual(rotate[2:], ["-m", "copienator", "copies", "rotate", self.evaluation])
|
||||||
self.assertEqual(rename[2:], [str(REPOSITORY / "copies_tools.py"), "rename", self.evaluation])
|
self.assertEqual(rename[2:], ["-m", "copienator", "copies", "rename", self.evaluation])
|
||||||
|
|
||||||
|
|
||||||
class CrossPlatformFileTests(unittest.TestCase):
|
class CrossPlatformFileTests(unittest.TestCase):
|
||||||
@@ -1665,8 +1688,8 @@ class CrossPlatformFileTests(unittest.TestCase):
|
|||||||
source = root / "source.txt"
|
source = root / "source.txt"
|
||||||
destination = root / "destination.txt"
|
destination = root / "destination.txt"
|
||||||
source.write_text("content", encoding="utf-8")
|
source.write_text("content", encoding="utf-8")
|
||||||
with patch("platform_utils.os.link", side_effect=OSError), patch(
|
with patch("copienator.platform.os.link", side_effect=OSError), patch(
|
||||||
"platform_utils.os.symlink", side_effect=OSError
|
"copienator.platform.os.symlink", side_effect=OSError
|
||||||
):
|
):
|
||||||
method = replace_with_link_or_copy(source, destination)
|
method = replace_with_link_or_copy(source, destination)
|
||||||
self.assertEqual(method, "copy")
|
self.assertEqual(method, "copy")
|
||||||
|
|||||||
Reference in New Issue
Block a user