Support for post annotation label reassignement.

This commit is contained in:
sebastien committed 2026-10-11 19:03:04 +02:00
1 parent 698ac4240c
commit 370f3252ef
10 files changed
+242 -36

No files matched your search

+22 -4
View File
@@ -30,6 +30,7 @@ from .scores import ScoreEditorPanel
from .notifications import notify_desktop
from .syncthing import SyncthingPanel
from .manual_resolution import ManualResolutionPanel
from .annotation_modifications import AnnotationModificationsPanel, annotation_mode_for_reader
from .recorrection import prepare_selection_file
from .refaire import SECTION as REFAIRE_SECTION
from .refaire import (
@@ -824,6 +825,7 @@ class CopienatorApp(tk.Tk):
"labels",
"plotting",
"manual_resolution",
"modify_annotations",
"giving_names",
"edit_scores",
"statement_persp",
@@ -888,6 +890,9 @@ class CopienatorApp(tk.Tk):
value = False # Deletion is a one-run choice, never a saved default.
if step.id == 'statement' and spec.name == 'restart':
value = False
if step.id == "modify_annotations" and spec.name == "annotation_dir" and spec.name not in values:
reader = self.state_store.step("read_annotations")
value = annotation_mode_for_reader(reader)
choices = spec.choices
if spec.name == "annotation_dir" and step.id in {"export", "import"}:
choices, value = self._annotation_directory_choices(step.id)
@@ -939,13 +944,20 @@ class CopienatorApp(tk.Tk):
self.edit_recorrection_button.grid(row=row, column=0, columnspan=2, sticky="w", pady=8)
row += 1
if step.id == "manual_resolution":
if step.id in {"manual_resolution", "modify_annotations"}:
def manual_evaluation():
target = self.arg_vars.get("target")
value = target.get().strip() if target else ""
return (self.repository / value).resolve() if value else self.evaluation
self.manual_panel = ManualResolutionPanel(self.form, manual_evaluation)
if step.id == "modify_annotations":
self.manual_panel = AnnotationModificationsPanel(
self.form, manual_evaluation,
lambda: self.arg_vars["annotation_dir"].get(),
)
self.arg_vars["annotation_dir"].trace_add("write", lambda *_: self.manual_panel.reload())
else:
self.manual_panel = ManualResolutionPanel(self.form, manual_evaluation)
self.manual_panel.grid(row=row, column=0, columnspan=2, sticky="nsew", pady=8)
if "target" in self.arg_vars:
self.arg_vars["target"].trace_add("write", lambda *_args: self.manual_panel.reload())
@@ -966,7 +978,7 @@ class CopienatorApp(tk.Tk):
attach_tooltip(extra_entry, step.extra_arguments_help)
row += 1
self.run_button.configure(text="Enregistrer la sélection" if step.id == "refaire_selection" else ("Marquer terminée" if step.is_manual else "Exécuter"))
self.run_button.configure(text="Make modifications" if step.id == "modify_annotations" else "Enregistrer la sélection" if step.id == "refaire_selection" else ("Marquer terminée" if step.is_manual else "Exécuter"))
if step.id == "personal_create":
self.run_button.configure(text="Marquer Réussie")
if step.id == "personal_read":
@@ -1953,7 +1965,7 @@ class CopienatorApp(tk.Tk):
self._append_console(f"Impossible de lancer la copie suivante : {exc}\n")
status, return_code = "failed", 1
self.pending_refaire_commands = []
if step_id == "refaire_merge" and status == "success":
if step_id in {"refaire_merge", "modify_annotations"} and status == "success":
for downstream in ("giving_names", "update_ods", "final_score", "personal_deploy", "personal_sent"):
entry = self.state_store.step(downstream)
if entry.get("status") in {"success", "detected", "skipped"}:
@@ -1995,6 +2007,12 @@ class CopienatorApp(tk.Tk):
"manual": False, "reason": reason,
})
self._append_console(f"Remplacer les barèmes par Gemini : ignorée — {reason}\n")
if step_id == "read_annotations" and status == "success":
reader = self.state_store.step(step_id)
mode = annotation_mode_for_reader(reader)
values = dict(self.state_store.step("modify_annotations").get("values", {}))
values["annotation_dir"] = mode
self.state_store.update_step("modify_annotations", values=values)
if step_id == "annotation" and status == "success":
generation_variant = self.state_store.step("annotation").get(
"last_run_variant"
+23 -15
View File
@@ -45,21 +45,29 @@ def cut_operator(percent: float, keep: int, mode: str) -> str:
class CutHelper(tk.Toplevel):
def __init__(self, parent, path: Path, on_accept, initial=None):
# Load the source before creating a window so invalid PDFs leave no dialog.
with pymupdf.open(path) as document:
if not len(document):
raise ValueError("Le PDF est vide.")
self.heights = [page.rect.height for page in document]
width = min(850, parent.winfo_screenwidth() - 100)
self.scale = min(1.5, width / max(page.rect.width for page in document))
rendered = []
for page in document:
pix = page.get_pixmap(matrix=pymupdf.Matrix(self.scale, self.scale), alpha=False,
colorspace=pymupdf.csRGB)
rendered.append(Image.frombytes("RGB", (pix.width, pix.height), pix.samples))
def __init__(self, parent, path: Path | Image.Image, on_accept, initial=None):
# Body edits pass a raster without headers or supplementary pages.
width = min(850, parent.winfo_screenwidth() - 100)
if isinstance(path, Image.Image):
self.heights = [path.height]
self.scale = min(1.5, width / path.width)
rendered = [path.resize((round(path.width * self.scale),
round(path.height * self.scale)), Image.Resampling.LANCZOS)]
title = "Corps de la réponse annotée"
else:
with pymupdf.open(path) as document:
if not len(document):
raise ValueError("Le PDF est vide.")
self.heights = [page.rect.height for page in document]
self.scale = min(1.5, width / max(page.rect.width for page in document))
rendered = []
for page in document:
pix = page.get_pixmap(matrix=pymupdf.Matrix(self.scale, self.scale), alpha=False,
colorspace=pymupdf.csRGB)
rendered.append(Image.frombytes("RGB", (pix.width, pix.height), pix.samples))
title = path.name
super().__init__(parent)
self.title(f"Cut — {path.name}")
self.title(f"Cut — {title}")
self.geometry(f"{width + 45}x{min(850, parent.winfo_screenheight() - 100)}")
self.transient(parent.winfo_toplevel())
self.on_accept = on_accept
@@ -125,7 +133,7 @@ class CutHelper(tk.Toplevel):
operator = cut_operator(self.percent, self.keep.get(), self.mode.get())
rounded = float(operator.split("{")[1].split("}")[0])
if not 0 < rounded < 100:
messagebox.showerror("Coupe invalide", "Chaque partie doit contenir une portion du PDF.", parent=self)
messagebox.showerror("Coupe invalide", "Chaque partie doit contenir une portion de la réponse.", parent=self)
return
self.destroy()
self.on_accept(operator)
+5 -4
View File
@@ -37,9 +37,10 @@ Enregistrez dans l’éditeur, puis cliquez sur Recharger et enfin sur Exécuter
class ManualResolutionPanel(ttk.Frame):
def __init__(self, parent, get_evaluation):
def __init__(self, parent, get_evaluation, *, help_text=HELP, show_provenance=True):
super().__init__(parent)
self.get_evaluation = get_evaluation
self.show_provenance = show_provenance
self.pdf_buttons = []
self.cut_buttons = []
self.copy_buttons = []
@@ -67,7 +68,7 @@ class ManualResolutionPanel(ttk.Frame):
self.text.configure(yscrollcommand=scroll.set)
scroll.pack(side="right", fill="y")
self.text.pack(fill="both", expand=True)
ttk.Label(self, text=HELP, wraplength=650, justify="left").pack(fill="x", pady=8)
ttk.Label(self, text=help_text, wraplength=650, justify="left").pack(fill="x", pady=8)
self.reload()
def manual_path(self) -> Path | None:
@@ -123,7 +124,7 @@ class ManualResolutionPanel(ttk.Frame):
raise FileNotFoundError("Chargez une évaluation.")
content = path.read_text(encoding="utf-8")
except (OSError, UnicodeError) as exc:
self.status.configure(text=f"manual_resolutions.txt indisponible : {exc}")
self.status.configure(text=f"{path.name if path else 'Fichier'} indisponible : {exc}")
else:
self.editor_button.configure(state="normal")
malformed = []
@@ -142,7 +143,7 @@ class ManualResolutionPanel(ttk.Frame):
key = (instruction.copy_id, instruction.old_label)
if key not in provenance:
try:
provenance[key] = populated_labels(path.parent, *key)
provenance[key] = populated_labels(path.parent, *key) if self.show_provenance else []
except (OSError, ValueError) as exc:
provenance[key] = []
provenance_error = f" — Provenance indisponible : {exc}"
+17
View File
@@ -668,6 +668,23 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
),
) + return_answer_arguments,
),
StepDefinition(
"modify_annotations",
"Finalisation",
"Modifier les réponses annotées",
"Coupez ou transférez les corps de réponses après lecture des annotations. "
"Les en-têtes, les scores et les documents complémentaires restent associés à chaque label.",
(python("default", "Modifications", "modify-annotations"),),
arguments=(
arg_target("le dossier de l’évaluation"),
ArgumentSpec("annotation_dir", "Dossier d’annotations", "choice", "--annotation-dir",
default="BGnot", choices=("BGnot", "Bnot"),
help="Choisissez le mode utilisé pour lire les annotations manuscrites ; les coupes portent uniquement sur les corps des réponses."),
),
optional=True,
requires=("labels",),
artifacts=("annotation_modifications.txt",),
),
StepDefinition(
"giving_names",
"Finalisation",