gui
This commit is contained in:
@@ -0,0 +1,485 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import sys
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
EVALUATION = "${evaluation}"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ArgumentSpec:
|
||||
name: str
|
||||
label: str
|
||||
kind: str = "text" # text, int, bool, choice, path
|
||||
flag: str | None = None
|
||||
default: object = ""
|
||||
choices: tuple[str, ...] = ()
|
||||
help: str = ""
|
||||
positional: bool = False
|
||||
variants: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CommandVariant:
|
||||
id: str
|
||||
label: str
|
||||
program: str | None
|
||||
kind: str = "python" # python, shell, external, manual
|
||||
fixed_args: tuple[str, ...] = ()
|
||||
dangerous: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StepDefinition:
|
||||
id: str
|
||||
section: str
|
||||
title: str
|
||||
description: str
|
||||
variants: tuple[CommandVariant, ...]
|
||||
arguments: tuple[ArgumentSpec, ...] = ()
|
||||
optional: bool = False
|
||||
personal: bool = False
|
||||
requires: tuple[str, ...] = ()
|
||||
artifacts: tuple[str, ...] = ()
|
||||
|
||||
@property
|
||||
def is_manual(self) -> bool:
|
||||
return all(variant.kind == "manual" for variant in self.variants)
|
||||
|
||||
|
||||
def arg_target(help_text: str = "Dossier d’évaluation ou fichier à traiter") -> ArgumentSpec:
|
||||
return ArgumentSpec(
|
||||
"target",
|
||||
"Cible",
|
||||
kind="path",
|
||||
default=EVALUATION,
|
||||
positional=True,
|
||||
help=help_text,
|
||||
)
|
||||
|
||||
|
||||
def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
python = lambda ident, label, script, **kwargs: CommandVariant(
|
||||
ident, label, script, "python", **kwargs
|
||||
)
|
||||
manual = lambda ident, label="Étape manuelle": CommandVariant(
|
||||
ident, label, None, "manual"
|
||||
)
|
||||
|
||||
steps = [
|
||||
StepDefinition(
|
||||
"inputs",
|
||||
"Préparation",
|
||||
"Vérifier les fichiers d’entrée",
|
||||
"Le dossier doit contenir enonce.pdf, enonce.tex et correction.tex. "
|
||||
"Le fichier names doit être présent dans l’évaluation ou à la racine du projet.",
|
||||
(manual("check"),),
|
||||
requires=("enonce.pdf", "enonce.tex", "correction.tex"),
|
||||
),
|
||||
StepDefinition(
|
||||
"statement",
|
||||
"Prétraitement de l’énoncé",
|
||||
"Analyser l’énoncé",
|
||||
"Détecte les questions, leurs groupes, le corrigé et les indications de barème.",
|
||||
(
|
||||
python("gemini", "Analyse avec Gemini", "gemini_for_enonce.py"),
|
||||
python("personal", "Alternative enonce_info.py", "enonce_info.py"),
|
||||
),
|
||||
arguments=(
|
||||
arg_target("Dossier de l’évaluation"),
|
||||
ArgumentSpec(
|
||||
"restart",
|
||||
"Ignorer le cache (--restart)",
|
||||
kind="bool",
|
||||
flag="--restart",
|
||||
variants=("gemini",),
|
||||
),
|
||||
),
|
||||
requires=("enonce.pdf", "enonce.tex", "correction.tex"),
|
||||
artifacts=("labels", "Text", "Sol", "Persp"),
|
||||
),
|
||||
StepDefinition(
|
||||
"review_persp",
|
||||
"Prétraitement de l’énoncé",
|
||||
"Relire les barèmes dans Persp",
|
||||
"Étape manuelle facultative : vérifier et modifier les instructions de correction.",
|
||||
(manual("review"),),
|
||||
optional=True,
|
||||
requires=("Persp",),
|
||||
),
|
||||
StepDefinition(
|
||||
"rotate",
|
||||
"Prétraitement des copies",
|
||||
"Retourner toutes les copies",
|
||||
"Rotation facultative de 180° lorsque les scans sont à l’envers.",
|
||||
(CommandVariant("rotate", "Rotation", "rotate_all.sh", "shell"),),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
optional=True,
|
||||
),
|
||||
StepDefinition(
|
||||
"rename",
|
||||
"Prétraitement des copies",
|
||||
"Renommer les PDF en CopieXX.pdf",
|
||||
"Renomme les PDF du dossier d’évaluation dans leur ordre courant.",
|
||||
(CommandVariant("rename", "Renommage", "rename_to_copie.sh", "shell"),),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
),
|
||||
StepDefinition(
|
||||
"page_splitter",
|
||||
"Prétraitement des copies",
|
||||
"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.",
|
||||
(python("default", "Séparation des pages", "page_splitter.py"),),
|
||||
arguments=(arg_target(),),
|
||||
artifacts=("Copies", "Copies Originales"),
|
||||
),
|
||||
StepDefinition(
|
||||
"cutleft",
|
||||
"Prétraitement des copies",
|
||||
"Découper la marge des labels",
|
||||
"Produit les images de la partie gauche des copies. Une copie précise peut être ciblée.",
|
||||
(python("default", "Découpe", "cutleft.py"),),
|
||||
arguments=(
|
||||
arg_target(),
|
||||
ArgumentSpec("fullpage", "Toujours utiliser la page entière", "bool", "--fullpage"),
|
||||
),
|
||||
requires=("Copies",),
|
||||
artifacts=("Cutleft",),
|
||||
),
|
||||
StepDefinition(
|
||||
"labels",
|
||||
"Labels et regroupement",
|
||||
"Détecter les labels avec Gemini",
|
||||
"Identifie les labels dans les images produites par la découpe gauche.",
|
||||
(python("default", "Détection des labels", "gemini_for_labels.py"),),
|
||||
arguments=(
|
||||
arg_target(),
|
||||
ArgumentSpec("overwrite", "Régénérer les résultats", "bool", "--overwrite"),
|
||||
),
|
||||
requires=("labels", "Copies", "Cutleft"),
|
||||
artifacts=("Copies/*.json",),
|
||||
),
|
||||
StepDefinition(
|
||||
"plotting",
|
||||
"Labels et regroupement",
|
||||
"Vérifier visuellement les labels",
|
||||
"Ouvre la fenêtre de vérification. La cible peut être toute l’évaluation ou une copie.",
|
||||
(python("default", "Vérification", "plotting.py"),),
|
||||
arguments=(arg_target(),),
|
||||
requires=("labels", "Cutleft"),
|
||||
artifacts=("Copies/Copie*.json",),
|
||||
),
|
||||
StepDefinition(
|
||||
"splitting",
|
||||
"Labels et regroupement",
|
||||
"Découper les réponses par question",
|
||||
"Découpe les copies à partir des coordonnées de labels vérifiées.",
|
||||
(python("default", "Découpage", "splitting_int.py"),),
|
||||
arguments=(arg_target(),),
|
||||
requires=("Copies",),
|
||||
artifacts=("Copies/Copie*/*",),
|
||||
),
|
||||
StepDefinition(
|
||||
"grouping",
|
||||
"Labels et regroupement",
|
||||
"Regrouper les réponses",
|
||||
"Regroupe les réponses portant le même label pour préparer les requêtes.",
|
||||
(python("default", "Regroupement", "grouping.py"),),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
requires=("Copies",),
|
||||
artifacts=("Par label",),
|
||||
),
|
||||
StepDefinition(
|
||||
"correction",
|
||||
"Correction",
|
||||
"Lancer ou intégrer la correction",
|
||||
"Choisir une correction immédiate, batch, hybride, une recorrection, ou l’intégration d’un batch.",
|
||||
(
|
||||
python("live", "Correction immédiate", "correction.py"),
|
||||
python("batch", "Préparer toutes les requêtes batch", "correction.py", fixed_args=("--batch",)),
|
||||
python("hybrid", "Batch à partir d’un label", "correction.py"),
|
||||
python("refaire", "Recorrection depuis refaire.json", "correction.py", fixed_args=("--refaire",)),
|
||||
python(
|
||||
"integrate",
|
||||
"Intégrer les résultats batch",
|
||||
"correction.py",
|
||||
fixed_args=("--deal-with-batched",),
|
||||
),
|
||||
python("reset", "Réinitialiser les corrections", "correction.py", fixed_args=("--reset",), dangerous=True),
|
||||
),
|
||||
arguments=(
|
||||
arg_target("Évaluation ou image Group_X.jpg"),
|
||||
ArgumentSpec("overwrite", "Écraser les corrections existantes", "bool", "--overwrite", variants=("live",)),
|
||||
ArgumentSpec("limit", "Limite d’appels Pro", "int", "--limit", variants=("live",)),
|
||||
ArgumentSpec("batch_from", "Premier label envoyé en batch", "text", "--batch-from", variants=("hybrid",)),
|
||||
),
|
||||
requires=("Par label", "Persp", "labels"),
|
||||
artifacts=("correction.json", "batch_requests_*.jsonl"),
|
||||
),
|
||||
StepDefinition(
|
||||
"submit_batches",
|
||||
"Correction",
|
||||
"Envoyer les batchs",
|
||||
"Envoie à Gemini les fichiers JSONL produits par le mode batch.",
|
||||
(python("default", "Envoi", "submit_batches.py"),),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
optional=True,
|
||||
artifacts=("batch_jobs.json",),
|
||||
),
|
||||
StepDefinition(
|
||||
"batch_status",
|
||||
"Correction",
|
||||
"Consulter l’état des batchs",
|
||||
"Affiche les jobs Gemini en cours. L’identifiant de téléchargement est facultatif.",
|
||||
(python("default", "État des batchs", "batch_status.py"),),
|
||||
arguments=(ArgumentSpec("download", "Télécharger le job", "text", "--download"),),
|
||||
optional=True,
|
||||
),
|
||||
StepDefinition(
|
||||
"fetch_batches",
|
||||
"Correction",
|
||||
"Récupérer les résultats batch",
|
||||
"Télécharge et rassemble les réponses des jobs terminés.",
|
||||
(python("default", "Récupération", "fetch_batched_results.py"),),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
optional=True,
|
||||
),
|
||||
StepDefinition(
|
||||
"post_correction",
|
||||
"Correction",
|
||||
"Nettoyer la correction",
|
||||
"Corrige certains problèmes d’encodage et prépare le texte pour LaTeX.",
|
||||
(python("default", "Post-correction", "post-correction.py"),),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
requires=("correction.json",),
|
||||
),
|
||||
StepDefinition(
|
||||
"manual_resolution",
|
||||
"Correction",
|
||||
"Résoudre les conflits manuels",
|
||||
"Étape conditionnelle, uniquement si manual_resolutions.txt contient des conflits à traiter.",
|
||||
(python("default", "Résolution", "resolve_manual.py"),),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
optional=True,
|
||||
requires=("manual_resolutions.txt", "correction.json"),
|
||||
),
|
||||
StepDefinition(
|
||||
"annotation",
|
||||
"Génération des annotations",
|
||||
"Générer les copies annotées",
|
||||
"Les trois modes sont exclusifs pour un parcours donné.",
|
||||
(
|
||||
python("simple", "Annotations simples (Anot)", "annotating.py"),
|
||||
python("checks", "Annotations avec cases (Bnot)", "annotating_with_checks.py"),
|
||||
python("grouped", "Annotations groupées (BGnot)", "annotating_by_label.py"),
|
||||
),
|
||||
arguments=(
|
||||
arg_target("Dossier de l’évaluation"),
|
||||
ArgumentSpec("overwrite", "Écraser les sorties", "bool", "--overwrite"),
|
||||
ArgumentSpec("refaire", "Mode refaire", "bool", "--refaire", variants=("checks",)),
|
||||
),
|
||||
requires=("correction.json",),
|
||||
artifacts=("Anot", "Bnot", "BGnot"),
|
||||
),
|
||||
StepDefinition(
|
||||
"export",
|
||||
"Génération des annotations",
|
||||
"Exporter vers la tablette",
|
||||
"Exporte les groupes vers le dossier EXPORT_DIR défini dans config.py.",
|
||||
(python("default", "Export", "export.py"),),
|
||||
arguments=(
|
||||
arg_target("Dossier de l’évaluation"),
|
||||
ArgumentSpec("refaire", "Mode refaire", "bool", "--refaire"),
|
||||
),
|
||||
optional=True,
|
||||
),
|
||||
StepDefinition(
|
||||
"tablet",
|
||||
"Correction manuscrite",
|
||||
"Annoter les PDF sur la tablette",
|
||||
"Étape manuelle : enregistrer chaque résultat sous le nom Concat_annotated.pdf.",
|
||||
(manual("tablet"),),
|
||||
),
|
||||
StepDefinition(
|
||||
"import",
|
||||
"Correction manuscrite",
|
||||
"Importer les annotations manuscrites",
|
||||
"Copie les PDF présents dans IMPORT_DIR vers l’évaluation.",
|
||||
(python("default", "Import", "import.py"),),
|
||||
arguments=(
|
||||
arg_target("Dossier de l’évaluation"),
|
||||
ArgumentSpec("refaire", "Mode refaire", "bool", "--refaire"),
|
||||
),
|
||||
),
|
||||
StepDefinition(
|
||||
"read_annotations",
|
||||
"Finalisation",
|
||||
"Lire les annotations manuscrites",
|
||||
"Le mode doit correspondre au mode choisi lors de la génération des annotations.",
|
||||
(
|
||||
python("standard", "Lecture Bnot", "reading_annotations.py"),
|
||||
python("grouped", "Lecture BGnot", "reading_grouped_annotations.py"),
|
||||
),
|
||||
arguments=(
|
||||
arg_target("Dossier de l’évaluation"),
|
||||
ArgumentSpec("update_score", "Réappliquer les score.json", "bool", "--update-score"),
|
||||
ArgumentSpec("refaire", "Mode refaire", "bool", "--refaire", variants=("grouped",)),
|
||||
),
|
||||
),
|
||||
StepDefinition(
|
||||
"giving_names",
|
||||
"Finalisation",
|
||||
"Attribuer les noms et préparer A Rendre",
|
||||
"Crée le dossier A Rendre à partir du dossier d’annotations choisi.",
|
||||
(python("default", "Attribution des noms", "giving_names.py"),),
|
||||
arguments=(
|
||||
arg_target("Dossier de l’évaluation"),
|
||||
ArgumentSpec(
|
||||
"annotation_dir",
|
||||
"Dossier d’annotations",
|
||||
"choice",
|
||||
default="BGnot",
|
||||
choices=("BGnot", "Bnot", "Anot"),
|
||||
positional=True,
|
||||
),
|
||||
),
|
||||
artifacts=("A Rendre",),
|
||||
),
|
||||
StepDefinition(
|
||||
"personal_create",
|
||||
"Étapes personnelles",
|
||||
"Créer l’interro dans gestion_classe",
|
||||
"Exécute gestion_classe ne.",
|
||||
(CommandVariant("default", "gestion_classe ne", "gestion_classe", "external", ("ne",)),),
|
||||
personal=True,
|
||||
),
|
||||
StepDefinition(
|
||||
"personal_scale",
|
||||
"Étapes personnelles",
|
||||
"Renseigner le barème",
|
||||
"Exécute gestion_classe we.",
|
||||
(CommandVariant("default", "gestion_classe we", "gestion_classe", "external", ("we",)),),
|
||||
personal=True,
|
||||
),
|
||||
StepDefinition(
|
||||
"update_ods",
|
||||
"Étapes personnelles",
|
||||
"Mettre à jour le fichier ODS",
|
||||
"Transfère les scores, avec la possibilité de n’écrire que leur somme.",
|
||||
(python("default", "Mise à jour ODS", "update_ods.py"),),
|
||||
arguments=(
|
||||
arg_target("Dossier de l’évaluation"),
|
||||
ArgumentSpec("sum", "Écrire seulement la somme", "bool", "--sum"),
|
||||
),
|
||||
personal=True,
|
||||
),
|
||||
StepDefinition(
|
||||
"personal_read",
|
||||
"Étapes personnelles",
|
||||
"Relire avec gestion_classe",
|
||||
"Exécute gestion_classe re.",
|
||||
(CommandVariant("default", "gestion_classe re", "gestion_classe", "external", ("re",)),),
|
||||
personal=True,
|
||||
),
|
||||
StepDefinition(
|
||||
"personal_sent",
|
||||
"Étapes personnelles",
|
||||
"Marquer comme envoyé",
|
||||
"Exécute gestion_classe wsent.",
|
||||
(CommandVariant("default", "gestion_classe wsent", "gestion_classe", "external", ("wsent",)),),
|
||||
personal=True,
|
||||
),
|
||||
StepDefinition(
|
||||
"final_score",
|
||||
"Étapes personnelles",
|
||||
"Ajouter le score final",
|
||||
"Génère les fichiers de diffusion avec le score final.",
|
||||
(python("default", "Score final", "add_final_score.py"),),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
personal=True,
|
||||
),
|
||||
StepDefinition(
|
||||
"personal_deploy",
|
||||
"Étapes personnelles",
|
||||
"Déployer et publier les copies",
|
||||
"Étape manuelle : déployer miqmacs-copies-assets puis mettre à jour les copies depuis l’administration.",
|
||||
(manual("deploy"),),
|
||||
personal=True,
|
||||
),
|
||||
StepDefinition(
|
||||
"personal_print",
|
||||
"Étapes personnelles",
|
||||
"Imprimer une copie",
|
||||
"Étape manuelle via le lecteur PDF.",
|
||||
(manual("print"),),
|
||||
personal=True,
|
||||
optional=True,
|
||||
),
|
||||
]
|
||||
return [step for step in steps if show_personal_steps or not step.personal]
|
||||
|
||||
|
||||
def value_for_default(value: object, evaluation_arg: str) -> object:
|
||||
return evaluation_arg if value == EVALUATION else value
|
||||
|
||||
|
||||
def build_command(
|
||||
repository: Path,
|
||||
step: StepDefinition,
|
||||
variant: CommandVariant,
|
||||
values: dict[str, object],
|
||||
evaluation_arg: str,
|
||||
extra_arguments: str = "",
|
||||
) -> list[str]:
|
||||
if variant.kind == "manual" or not variant.program:
|
||||
return []
|
||||
|
||||
program_path = repository / variant.program
|
||||
if variant.kind == "python":
|
||||
command = [sys.executable, "-u", str(program_path)]
|
||||
elif variant.kind == "shell":
|
||||
command = [str(program_path)]
|
||||
else:
|
||||
command = [variant.program]
|
||||
|
||||
positionals: list[str] = []
|
||||
options: list[str] = []
|
||||
for spec in step.arguments:
|
||||
if spec.variants and variant.id not in spec.variants:
|
||||
continue
|
||||
value = values.get(spec.name, value_for_default(spec.default, evaluation_arg))
|
||||
if spec.kind == "bool":
|
||||
if bool(value) and spec.flag:
|
||||
options.append(spec.flag)
|
||||
continue
|
||||
if value is None or str(value).strip() == "":
|
||||
continue
|
||||
rendered = str(value)
|
||||
if spec.positional:
|
||||
positionals.append(rendered)
|
||||
elif spec.flag:
|
||||
options.extend((spec.flag, rendered))
|
||||
|
||||
command.extend(positionals)
|
||||
command.extend(variant.fixed_args)
|
||||
command.extend(options)
|
||||
if extra_arguments.strip():
|
||||
command.extend(shlex.split(extra_arguments, posix=os.name != "nt"))
|
||||
return command
|
||||
|
||||
|
||||
def command_display(command: Iterable[str]) -> str:
|
||||
if os.name == "nt":
|
||||
return " ".join(f'"{part}"' if " " in part else part for part in command)
|
||||
return shlex.join(command)
|
||||
|
||||
|
||||
def evaluation_argument(repository: Path, evaluation: Path) -> str:
|
||||
try:
|
||||
return str(evaluation.resolve().relative_to(repository.resolve()))
|
||||
except ValueError:
|
||||
return str(evaluation.resolve())
|
||||
Reference in New Issue
Block a user