améliorations diverses

This commit is contained in:
2026-09-17 22:19:05 +02:00
parent 5b8215e7f5
commit 0a453403cf
11 changed files with 1072 additions and 81 deletions
+170 -12
View File
@@ -22,6 +22,7 @@ from copienator.platform import (
from .diagnostics import collect_diagnostics
from .batch_monitor import BatchMonitor
from .giving_names import GivingNamesPanel, find_name_issues
from .notifications import notify_desktop
from .manual_resolution import ManualResolutionPanel
from .refaire import SECTION as REFAIRE_SECTION
@@ -58,6 +59,9 @@ STATUS_LABELS = {
}
DEFAULT_HTTPS_PROXY = "http://10.0.0.1:3128"
CORRECTION_PROGRESS_RE = re.compile(
r"\[Progression correction\] Groupes traités : (\d+)/(\d+)"
)
PERSONAL_INTERRO_SOURCE = Path("/home/sebastien/Prépa/Staging/Interro")
ANNOTATION_DIRECTORIES = ("BGnot", "Bnot", "Anot")
ANNOTATION_VARIANT_DIRECTORIES = {
@@ -65,6 +69,10 @@ ANNOTATION_VARIANT_DIRECTORIES = {
"checks": "Bnot",
"grouped": "BGnot",
}
ANNOTATION_VARIANT_READERS = {
"checks": "standard",
"grouped": "grouped",
}
def get_personal_interro_files(
@@ -259,8 +267,11 @@ class CopienatorApp(tk.Tk):
self.copy_paths: dict[str, Path] = {}
self._rendering = False
self.refaire_panel: RefaireSelection | None = None
self.giving_names_panel: GivingNamesPanel | None = None
self.complete_giving_names_button: ttk.Button | None = None
self.pending_refaire_commands: list[list[str]] = []
self.refaire_command_index = 0
self._correction_progress_buffer = ""
self.title("Copienator — assistant de correction")
self.geometry("1180x820")
@@ -752,7 +763,15 @@ class CopienatorApp(tk.Tk):
return
self._rendering = True
redo = step.section == REFAIRE_SECTION
expanded_form = redo or step.id in {"page_splitter", "crop_blank_margins", "cutleft", "labels", "manual_resolution"}
expanded_form = redo or step.id in {
"page_splitter",
"crop_blank_margins",
"cutleft",
"labels",
"plotting",
"manual_resolution",
"giving_names",
}
self.console.configure(height=8 if expanded_form else 14)
self.rowconfigure(1, weight=4 if expanded_form else 3)
self.rowconfigure(2, weight=1 if expanded_form else 2)
@@ -763,6 +782,8 @@ class CopienatorApp(tk.Tk):
child.destroy()
self.arg_vars.clear()
self.refaire_panel = None
self.giving_names_panel = None
self.complete_giving_names_button = None
entry = self.state_store.step(step.id) if self.state_store.evaluation else {}
saved_variant = entry.get("variant", step.variants[0].id)
if saved_variant not in {variant.id for variant in step.variants}:
@@ -909,6 +930,31 @@ class CopienatorApp(tk.Tk):
)
row += 1
return row
if step.id == "giving_names" and self.evaluation:
self.giving_names_panel = GivingNamesPanel(
self.form, self.evaluation
)
self.giving_names_panel.grid(
row=row,
column=0,
columnspan=2,
sticky="ew",
pady=(0, 8),
)
row += 1
self.complete_giving_names_button = ttk.Button(
self.form,
text="Marquer terminée",
command=self._complete_giving_names,
)
self.complete_giving_names_button.grid(
row=row,
column=0,
columnspan=2,
sticky="w",
pady=(0, 8),
)
row += 1
if step.section == REFAIRE_SECTION:
evaluation = self.evaluation
if step.id in {"refaire_selection", "refaire_correct"}:
@@ -935,7 +981,14 @@ class CopienatorApp(tk.Tk):
return row + 1
if step.id in {"review_persp", "correction"}:
row = self._correction_folder_buttons(row)
if step.id in {"page_splitter", "crop_blank_margins", "cutleft", "labels"}:
copy_selection_steps = {
"page_splitter",
"crop_blank_margins",
"cutleft",
"labels",
"plotting",
}
if step.id in copy_selection_steps:
evaluation = self.evaluation
paths = copy_pdf_paths(evaluation) if evaluation else []
self.copy_paths = {path.name: path for path in paths}
@@ -966,7 +1019,7 @@ class CopienatorApp(tk.Tk):
command=self._open_selected_copy,
state="normal" if names else "disabled",
).grid(row=1, column=1, padx=(6, 0), pady=(7, 0))
if step.id in {"page_splitter", "crop_blank_margins", "cutleft", "labels"}:
if step.id in copy_selection_steps:
actions = ttk.Frame(copies)
actions.grid(row=2, column=0, columnspan=2, sticky="w", pady=(7, 0))
label = "Refaire la copie sélectionnée" if step.id == "page_splitter" else "Cibler la copie sélectionnée"
@@ -978,6 +1031,8 @@ class CopienatorApp(tk.Tk):
if step.id == "page_splitter"
else "Seule la copie ciblée sera analysée par Gemini. "
if step.id == "labels"
else "Seule la copie ciblée sera vérifiée. "
if step.id == "plotting"
else "Seule la copie ciblée sera traitée. "
)
ttk.Label(copies, text=help_text + "Vérifiez la commande puis cliquez sur Exécuter.",
@@ -1352,8 +1407,7 @@ class CopienatorApp(tk.Tk):
return
if step.is_manual:
self._mark_step("success")
if step.section == REFAIRE_SECTION:
self._move_selection_from(step.id, 1)
self._move_selection_from(step.id, 1)
return
if os.name == "nt" and step.id != "statement":
labels_path = evaluation / "labels"
@@ -1400,6 +1454,9 @@ class CopienatorApp(tk.Tk):
last_run_values=run_values,
)
self.active_step_id = step.id
if step.id == "correction":
self._correction_progress_buffer = ""
self.info_var.set("Correction : préparation des groupes…")
workspace = self.state_store.workspace
assert workspace is not None
log_path = workspace.log_path(step.id)
@@ -1463,6 +1520,33 @@ class CopienatorApp(tk.Tk):
self._populate_tree()
self.info_var.set("Renommage validé — aucun fichier modifié.")
def _complete_giving_names(self) -> None:
if (
not self.current_step
or self.current_step.id != "giving_names"
or not self.evaluation
or self.active_step_id
or self.runner.running
):
return
return_dir = EvaluationWorkspace(self.evaluation).return_dir
if not return_dir.is_dir():
messagebox.showerror(
"A Rendre absent",
"Exécutez dabord lattribution des noms.",
)
return
issues = find_name_issues(self.evaluation)
if issues and not messagebox.askyesno(
"Noms encore à vérifier",
f"{len(issues)} copie(s) ont encore un nom inconnu ou dupliqué. "
"Marquer quand même l’étape comme terminée ?",
icon="warning",
):
return
self._mark_step("success")
self._move_selection_from("giving_names", 1)
def _skip_step(self) -> None:
if self.current_step and self.current_step.optional:
step_id = self.current_step.id
@@ -1477,7 +1561,9 @@ class CopienatorApp(tk.Tk):
except queue.Empty:
break
if event == "output":
self._append_console(str(payload))
output = str(payload)
self._track_correction_progress(output)
self._append_console(output)
elif event == "input_echo":
self._append_console(f"> {payload}")
elif event == "runner_error":
@@ -1487,6 +1573,24 @@ class CopienatorApp(tk.Tk):
self._finish_process(int(return_code), bool(interrupted))
self.after(60, self._poll_runner)
def _track_correction_progress(self, output: str) -> None:
if self.active_step_id != "correction":
return
self._correction_progress_buffer = (
self._correction_progress_buffer + output
)[-512:]
matches = list(CORRECTION_PROGRESS_RE.finditer(
self._correction_progress_buffer
))
if not matches:
return
completed, total = (int(value) for value in matches[-1].groups())
percent = round(100 * completed / total) if total else 100
self.info_var.set(
f"Correction : {completed} groupe(s) traité(s) sur {total} "
f"({percent} %)."
)
def _batch_watch_status_changed(self, message: str) -> None:
self.batch_watch_status.set(message)
self._update_controls()
@@ -1565,7 +1669,25 @@ class CopienatorApp(tk.Tk):
"été conservés.",
)
return
self.state_store.update_step(step_id, status=status, return_code=return_code)
stored_status = (
"detected"
if step_id == "giving_names" and status == "success"
else status
)
self.state_store.update_step(
step_id, status=stored_status, return_code=return_code
)
if step_id == "annotation" and status == "success":
generation_variant = self.state_store.step("annotation").get(
"last_run_variant"
)
reader_variant = ANNOTATION_VARIANT_READERS.get(
str(generation_variant)
)
if reader_variant:
self.state_store.update_step(
"read_annotations", variant=reader_variant
)
self.state_store.add_history(
{
"step": step_id,
@@ -1580,12 +1702,27 @@ class CopienatorApp(tk.Tk):
self.active_step_id = None
self._populate_tree()
self._update_controls()
if step_id == "giving_names" and status == "success":
if self.giving_names_panel:
self.giving_names_panel.reload()
self.info_var.set(
"A Rendre a été préparé. Vérifiez les noms, puis cliquez "
"sur « Marquer terminée »."
)
return
if status == "success":
if step_id == "batch_status" and self.batch_monitor.active:
self.batch_monitor.stop()
self._batch_results_ready()
return
self.after_idle(lambda completed_id=step_id: self._move_selection_from(completed_id, 1))
if step_id == "manual_resolution":
self.after_idle(self._select_refaire_correction)
else:
self.after_idle(
lambda completed_id=step_id: self._move_selection_from(
completed_id, 1
)
)
def _send_input(self) -> None:
text = self.stdin_var.get()
@@ -1597,12 +1734,21 @@ class CopienatorApp(tk.Tk):
self.stdin_var.set("")
def _interrupt(self) -> None:
if self.runner.running and messagebox.askyesno(
"Interrompre", "Envoyer une interruption au script en cours ?"
):
graceful_correction = self.active_step_id == "correction"
question = (
"Arrêter de lancer de nouveaux appels Gemini ? Les appels déjà en "
"cours pourront se terminer et leurs résultats seront sauvegardés."
if graceful_correction
else "Envoyer une interruption au script en cours ?"
)
if self.runner.running and messagebox.askyesno("Interrompre", question):
try:
self.runner.interrupt()
self.info_var.set("Interruption demandée. Utilisez « Forcer larrêt » si le script ne répond pas.")
self.info_var.set(
"Arrêt demandé : attente des appels Gemini en cours…"
if graceful_correction
else "Interruption demandée. Utilisez « Forcer larrêt » si le script ne répond pas."
)
except OSError as exc:
messagebox.showerror("Interruption impossible", str(exc))
@@ -1645,6 +1791,10 @@ class CopienatorApp(tk.Tk):
self.force_button.configure(state="normal" if running else "disabled")
self.send_button.configure(state="normal" if running else "disabled")
self.stdin_entry.configure(state="normal" if running else "disabled")
if self.complete_giving_names_button is not None:
self.complete_giving_names_button.configure(
state="disabled" if running else "normal"
)
def _move_selection(self, delta: int) -> None:
if not self.current_step:
@@ -1663,6 +1813,14 @@ class CopienatorApp(tk.Tk):
self.tree.selection_set(ids[target])
self.tree.see(ids[target])
def _select_refaire_correction(self) -> None:
"""Return from manual conflict resolution to the main redo correction."""
if self.runner.running or self.active_step_id or not self.state_store.evaluation:
return
self.state_store.update_step("correction", variant="refaire")
self.tree.selection_set("correction")
self.tree.see("correction")
def _on_close(self) -> None:
if self.runner.running:
if not messagebox.askyesno(
+27 -17
View File
@@ -259,8 +259,11 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
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.",
"Découper une partie à gauche pour détection des labels",
"Produit les images de la partie gauche des copies. Dans la fenêtre de découpe : "
"n décale la zone de 50 px vers la droite, N de 100 px, t de 50 px vers la gauche, "
"l l’élargit de 50 px, 1 utilise les pages entières, Entrée valide et s signale une erreur. "
"Une copie précise peut être ciblée.",
(python("default", "Découpe", "crop-labels"),),
arguments=(
arg_target(),
@@ -309,7 +312,8 @@ def build_workflow(show_personal_steps: bool) -> list[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.",
"Ouvre la fenêtre de vérification. La cible peut être toute l’évaluation ou une copie. "
"Le raccourci p revient à limage précédente, y compris dans la copie précédente.",
(python("default", "Vérification", "review-labels"),),
arguments=(arg_target(),),
requires=("labels", "Cutleft"),
@@ -364,7 +368,10 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
"correction",
"Correction",
"Lancer ou intégrer la correction",
"Choisir une correction immédiate, batch, hybride, une recorrection, ou lintégration dun batch.",
"Choisir une correction immédiate, batch, hybride, une recorrection, ou lintégration dun batch. "
"En correction immédiate, la barre d’état affiche le nombre de groupes traités et le total. "
"Interrompre bloque les nouveaux appels Gemini, attend ceux déjà en cours, sauvegarde leurs résultats puis arrête la commande. "
"Au prochain lancement, les validations encore nécessaires (mauvais label ou contenu supplémentaire) reprennent sans refaire la requête principale.",
(
python("live", "Correction immédiate", "correct"),
python("batch", "Préparer toutes les requêtes batch", "correct", fixed_args=("--batch",)),
@@ -386,7 +393,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
"bool",
"--overwrite",
help="Relance les corrections demandées même lorsquun résultat existe déjà.",
variants=("live", "batch", "hybrid", "refaire", "integrate"),
variants=("live", "batch", "hybrid", "integrate"),
),
ArgumentSpec(
"limit",
@@ -443,6 +450,18 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
optional=True,
skip_for_live_correction=True,
),
StepDefinition(
"manual_resolution",
"Correction",
"Résoudre les conflits manuels",
"Étape conditionnelle, uniquement si manual_resolutions.txt contient des conflits à traiter. "
"Après succès, le GUI revient à la correction avec « Recorrection depuis refaire.json » sélectionné.",
(python("default", "Résolution", "resolve-manual"),),
arguments=(arg_target("le dossier de l’évaluation"),),
optional=True,
requires=("manual_resolutions.txt", "correction.json"),
skip_without_manual_conflicts=True,
),
StepDefinition(
"post_correction",
"Correction",
@@ -452,17 +471,6 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
arguments=(arg_target("le 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"),),
arguments=(arg_target("le dossier de l’évaluation"),),
optional=True,
requires=("manual_resolutions.txt", "correction.json"),
skip_without_manual_conflicts=True,
),
StepDefinition(
"annotation",
"Génération des annotations",
@@ -596,7 +604,9 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
"giving_names",
"Finalisation",
"Attribuer les noms et préparer A Rendre",
"Crée le dossier A Rendre à partir du dossier dannotations choisi.",
"Crée le dossier A Rendre à partir du dossier dannotations choisi, sans passer automatiquement à l’étape suivante. "
"Après lexécution, vous pouvez renommer chaque dossier, ainsi que son fichier .jpg et son fichier .pdf, avec un autre nom ; le suffixe (id) du dossier est conservé. "
"Les outils affichés permettent didentifier et corriger les noms Unknown ou attribués à plusieurs copies. Cliquez ensuite sur « Marquer terminée ».",
(python("default", "Attribution des noms", "giving-names"),),
arguments=(
arg_target("le dossier de l’évaluation"),