diff --git a/Readme.org b/Readme.org index 0bf2093..5e4f02a 100644 --- a/Readme.org +++ b/Readme.org @@ -1,10 +1,11 @@ #+title: Script #+author: Sébastien Miquel #+date: 14-03-2026 -# Time-stamp: <08-05-26 22:52> +# Time-stamp: <14-05-26 08:55> #+OPTIONS: -* Quézaco +* Méta +** Quézaco Ce dépôt contient un certain nombre de script Python que j'utilise pour faire corriger des copies par Gemini. @@ -20,7 +21,7 @@ pour faire corriger des copies par Gemini. 4. Ces annotations manuscrites sont lues et recompilées en une version de la copie pour l'élève. -* Disclaimer +** Disclaimer J'utilise régulièrement cet outil et j'en suis satisfait, mais j'ai fait peu d'efforts pour le rendre universel et simple à l'emploi. @@ -37,9 +38,9 @@ examples du rendu final (dans le sous dossier =BGnot=). Cette situation s'améliorera peut-être, mais faciliter l'utilisation de ce système n'est pas une priorité. -* Requirements +** Requirements -** Python +*** Python Libraries : @@ -47,13 +48,13 @@ Libraries : pip install numpy pandas matplotlib pillow pydantic pypdf pdf2image reportlab img2pdf pymupdf ftfy ezodf google #+END_SRC -** Poppler (for pdf2image) +*** Poppler (for pdf2image) + Linux : install poppler-utils + Windows : Download from: https://github.com/oschwartz10612/poppler-windows and add it to your PATH -** Accès à Gemini +*** Accès à Gemini Il faut créer une clef API pour Gemini (pas facile). @@ -66,7 +67,7 @@ Puis ajouter =GEMINI_API_KEY= à l'environnement avec : export GEMINI_API_KEY=… #+END_SRC -* Correction d'un paquet de copies +** Correction d'un paquet de copies 1. Créer un fichier =names= dans le dossier courant, avec les noms/prénoms des élèves, un par ligne @@ -83,7 +84,8 @@ export GEMINI_API_KEY=… pour tel truc, etc) 6. Suivre les étapes plus bas. -* Prétraitement +* Étapes et Script +** Prétraitement 1. =./rotate_all.sh Interro= (facultatif) @@ -107,14 +109,14 @@ export GEMINI_API_KEY=… Rerun on a single file with =python cutleft.py Interro/Copie01.pdf= -* Génération d'information sur l'énoncé +** Génération d'information sur l'énoncé 1. =python enonce_info.py Interro= (gestion perso) OU 2. =python gemini_for_enonce.py Interro= + Nécessite =enonce.tex/org= et `correction.tex/org` -* Labelisation et regroupement +** Labelisation et regroupement Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~ @@ -130,25 +132,27 @@ Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~ + Quand un label est manquant, il est possible de cliquer sur l'image, ce qui copie les coordonnées dans le presse papier (sous linux…), puis on peut l'ajouter à la main. - + Utilisation de `_`, `|…` et `…|` + + Utilisation de `_`, `|…` et `…|` : + + `|…` n'est pas arrêté verticalement par son type opposé. + + `…|` est stoppé horizontalement par le `|…` le plus proche. Pour modifier une seule copie : =python plotting.py Interro/Copie01.pdf= It also generates les =Copie01.json=, à partir des =Copie01_01.json= - 3. En cas de soucis, (par exemple les pages ne sont pas dans le bon ordre) - - Réordonner les pages du fichier pdf - - Rerun =python cutleft.py Interro/Copie{id}= - - Rerun =python gemini_dir_batching.py Interro/Copie{id}= ?? À - vérifier, pas sûr que ça marche. - 4. =python splitting_int.py Interro= + 1. En cas de soucis, (par exemple les pages ne sont pas dans le bon ordre) + - Réordonner les pages du fichier pdf + - Rerun =python cutleft.py Interro/Copie{id}= + - Rerun =python gemini_dir_batching.py Interro/Copie{id}= ?? À + vérifier, pas sûr que ça marche. + 3. =python splitting_int.py Interro= Découpe les copies suivant les exercices - 5. =python grouping.py Interro= + 4. =python grouping.py Interro= Regroupe les mêmes questions de différentes copies en groupes de tailles raisonnables. -* Correction et annotation +** Correction et annotation Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~ @@ -170,16 +174,18 @@ 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 seront alors traitées sous au plus 24h. + =python correction.py Interro --batch= + + OU =python correction.py Interro --batch-from 'Ex 4'= + =python submit_batches.py Interro= + =python batch_status.py= + =python fetch_batched_results.py Interro= + =python correction.py Interro --deal-with-batched= 3. =python post-correction.py Interro= - Essaye de corriger des erreurs d'encodage/d'accents dans - =correction.json=. + - Essaye de corriger des erreurs d'encodage/d'accents dans + =correction.json=. + - aussi échappe les `_` en dehors du mode math, pour LaTeX. -* Génération des copies annotées +** Génération des copies annotées 1. =python annotating.py Interro= (facultatif) @@ -208,7 +214,7 @@ OU - Vider =Syncthing/Annotées= sur la tablette et localement. À automatiser, aussi c'est lent… -* Lecture de la correction manuscrite +** Lecture de la correction manuscrite 1. =python from_tablette.py Interro= (gestion perso) @@ -243,6 +249,7 @@ OU + =gestion_classe ne= pour créer l'interro puis + =gestion_classe we= (set barème here) + =python update_ods.py Interro= + ou =python update_ods.py Interro --sum= (en l'absence de barème) + =gestion_classe re= + =gestion_classe wsent= + =python add_final_score.py Interro21= @@ -252,10 +259,7 @@ OU + update the copies from =miqmacs.fr/admin=. 6. (gestion perso) Impression d'une copie. Via Evince » print to pdf. - - -* Recorrection d'une seule copie (peu testé) - +** Recorrection d'une seule copie (peu testé) !! Attention, refaire ne marchera pas si tu fais une annotation non groupée into refaire !! diff --git a/annotating_by_label.py b/annotating_by_label.py index 9771416..502d3ff 100644 --- a/annotating_by_label.py +++ b/annotating_by_label.py @@ -160,11 +160,35 @@ def main(): used_prefixes.add(unique_prefix) + existing_items = set() + max_existing_group = 0 + + + if not args.overwrite and os.path.exists(bgnot_dir): + for d in os.listdir(bgnot_dir): + if d.startswith(f"{unique_prefix} G"): + try: + g_id = int(d.split(' G')[-1]) + max_existing_group = max(max_existing_group, g_id) + except ValueError: + pass + + bnote_path = os.path.join(bgnot_dir, d, "bnote.json") + if os.path.exists(bnote_path): + with open(bnote_path, "r") as bf: + bdata = json.load(bf) + for img in bdata.get("images", []): + existing_items.add((img["id"], img["label"])) + items_to_render = [] for sid, lbls in results.items(): for lbl in labels: if lbl in lbls: - items_to_render.append((sid, lbl, lbls[lbl])) + # Only add if it hasn't been generated yet + if (sid, lbl) not in existing_items: + items_to_render.append((sid, lbl, lbls[lbl])) + if not items_to_render: + continue # Sort structurally: by student id and label items_to_render.sort(key=lambda x: (natural_key(x[0]), natural_key(x[1]))) @@ -217,7 +241,7 @@ def main(): batches = batches2 for i, batch in enumerate(batches, 1): - save_batch(batch, unique_prefix, i, root_dir, args.overwrite) + save_batch(batch, unique_prefix, max_existing_group + i, root_dir, args.overwrite) if __name__ == "__main__": main() diff --git a/correction.py b/correction.py index deaf78c..e5d8aa1 100644 --- a/correction.py +++ b/correction.py @@ -5,14 +5,11 @@ from pathlib import Path import argparse if len(sys.argv) < 2: - sys.exit("Usage: python script.py InterroTest/Ex 2/Group_1.jpg OR ") - -arg_path = Path(sys.argv[1]) -tasks = [] # List of tuples: (filepath_str, label_str) -results = {} + sys.exit("Usage: python script.py 'InterroTest/Ex 2/Group_1.jpg' OR OR 'file1' 'file2'") # Parse Arguments parser = argparse.ArgumentParser() +parser.add_argument("paths", nargs="+", help="List of images or directories") parser.add_argument("--overwrite", action="store_true", help="Force redo requests even if output exists") parser.add_argument("--limit", type=int, help="limit calls to gemini rpo integer") @@ -20,28 +17,40 @@ parser.add_argument("--refaire", action="store_true", help="Redo specific copies/labels defined in refaire.json") parser.add_argument("--batch", action="store_true", help="Generate a JSONL file of requests to send to the Gemini Batch API") +parser.add_argument("--batch-from", type=str, metavar="LABEL", + help="Do live requests before LABEL, and batch requests from LABEL onwards") parser.add_argument("--deal-with-batched", action="store_true", help="Process a JSONL file containing completed batch results") args, _ = parser.parse_known_args() +tasks = [] # List of tuples: (filepath_str, label_str) +results = {} + + +for path_str in args.paths: + arg_path = Path(path_str) -if arg_path.suffix == ".jpg": - INPUT_DIR = str(arg_path.parents[1]) - FULL_LABEL = arg_path.parent.name - tasks.append((str(arg_path), FULL_LABEL)) - results[FULL_LABEL] = [] -else: - # Directory behaviour - INPUT_DIR = str(arg_path) if not arg_path.exists(): - sys.exit(f"Directory {INPUT_DIR} not found.") + print(f"Warning: {path_str} not found. Skipping.") + continue - for sub in arg_path.iterdir(): - if sub.is_dir() and sub.name.startswith("Ex"): - label = sub.name + if arg_path.is_file() and arg_path.suffix.lower() == ".jpg": + # Handle individual file + # Note: assumes structure InterroTest/Ex 2/Group_1.jpg to get parents[1] + label = arg_path.parent.name + tasks.append((str(arg_path), label)) + if label not in results: results[label] = [] - for img in sub.glob("*.jpg"): - tasks.append((str(img), label)) + + elif arg_path.is_dir(): + # Handle directory (original behavior) + for sub in arg_path.iterdir(): + if sub.is_dir() and sub.name.startswith("Ex"): + label = sub.name + if label not in results: + results[label] = [] + for img in sub.glob("*.jpg"): + tasks.append((str(img), label)) my_prompt = """I'm giving you an image of several written answers to an exam. @@ -135,17 +144,15 @@ You are asked to score the question or exercice labeled `<