Allow signaling issues in label verification
This commit is contained in:
1 parent
3754c0c11a
commit
9925362352
21 files changed
+1347
-36
No files matched your search
@@ -0,0 +1,157 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tkinter as tk
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from tkinter import messagebox, ttk
|
||||
|
||||
from copienator import CliError, utils
|
||||
from copienator.annotation_modifications import (
|
||||
DRAFT,
|
||||
baseline,
|
||||
body_image,
|
||||
edited_bodies,
|
||||
normalize_copy,
|
||||
read_instructions,
|
||||
validate_instructions,
|
||||
)
|
||||
from copienator.commands.resolve_manual import ManualInstruction
|
||||
from copienator.platform import open_path
|
||||
|
||||
from .cut_helper import CutHelper
|
||||
from .manual_resolution import ManualResolutionPanel
|
||||
|
||||
HELP = """Ajoutez une copie, un label source et un label cible. Source, Cible et Cut montrent uniquement le corps de la réponse avec ses annotations conservées, sans note, commentaire global, contexte, énoncé ou corrigé. Les en-têtes restent associés à leur label ; les scores ne changent pas.
|
||||
|
||||
Format : Copie43 Label source OP Label cible|
|
||||
-> ajoute la source à la cible ; x> la déplace ; -x copie en remplaçant la cible ; xx déplace en remplaçant. ss conserve les deux ; sx vide la cible ; xs vide la source.
|
||||
c{43}1> conserve les premiers 43 % de la source et ajoute le reste à la cible ; c{43}2x conserve la fin et remplace la cible par le début. Le pourcentage porte uniquement sur le corps de la réponse. |Cible place le fragment avant la cible ; Cible| le place après.
|
||||
|
||||
Une instruction par source. Une cible ne peut pas également être source. Plusieurs sources peuvent alimenter une cible dans l’ordre du fichier. Une cible sans image est vide.
|
||||
Cut affiche une commande à recopier dans l’éditeur. Enregistrez puis Recharger. Aperçu du résultat montre les corps après toutes les instructions enregistrées.
|
||||
|
||||
Make modifications applique uniquement le fichier enregistré. Chaque application repart des images originales relues : modifier ou retirer une ligne puis appliquer remplace ou annule son effet. Les lignes ### sont des commentaires ; conservez la ligne ### Source: BGnot ou Bnot. Le fichier est conservé. Une nouvelle lecture des annotations réapplique les dernières instructions appliquées, jamais un brouillon. Relancez ensuite la préparation de A Rendre (sans l’option limitée à answers) pour actualiser tous les livrables."""
|
||||
|
||||
|
||||
def annotation_mode_for_reader(reader):
|
||||
if reader.get("last_run_variant", reader.get("variant")) == "standard":
|
||||
return "Bnot"
|
||||
values = reader.get("last_run_values", reader.get("values", {}))
|
||||
if values.get("refaire") and values.get("annotation_dir") == "Bnot":
|
||||
return "Bnot"
|
||||
return "BGnot"
|
||||
|
||||
|
||||
class AnnotationModificationsPanel(ManualResolutionPanel):
|
||||
def __init__(self, parent, get_evaluation, get_mode):
|
||||
self.get_mode = get_mode
|
||||
self.previews = TemporaryDirectory(prefix="copienator-body-preview-")
|
||||
super().__init__(parent, get_evaluation, help_text=HELP, show_provenance=False)
|
||||
self.bind("<Destroy>", self._cleanup, add="+")
|
||||
form = ttk.Frame(self)
|
||||
form.pack(fill="x", before=self.editor_button.master, pady=5)
|
||||
self.copy_var, self.source_var, self.target_var = (tk.StringVar() for _ in range(3))
|
||||
ttk.Label(form, text="Copie").grid(row=0, column=0)
|
||||
ttk.Entry(form, textvariable=self.copy_var, width=9).grid(row=1, column=0, padx=3)
|
||||
self.label_choices = []
|
||||
for column, title, variable in ((1, "Label source", self.source_var), (2, "Label cible", self.target_var)):
|
||||
ttk.Label(form, text=title).grid(row=0, column=column)
|
||||
combo = ttk.Combobox(form, textvariable=variable, width=22)
|
||||
combo.grid(row=1, column=column, padx=3)
|
||||
self.label_choices.append(combo)
|
||||
ttk.Button(form, text="Ajouter", command=self.add_instruction).grid(row=1, column=3, padx=5)
|
||||
ttk.Button(self.editor_button.master, text="Aperçu du résultat", command=self.preview_result).pack(side="left")
|
||||
self.reload()
|
||||
|
||||
def _cleanup(self, event):
|
||||
if event.widget is self:
|
||||
self.previews.cleanup()
|
||||
|
||||
def manual_path(self):
|
||||
evaluation = self.get_evaluation()
|
||||
return evaluation / DRAFT if evaluation else None
|
||||
|
||||
def _read(self):
|
||||
path = self.manual_path()
|
||||
if path is None:
|
||||
raise CliError("Chargez une évaluation.")
|
||||
text = path.read_text(encoding="utf-8") if path.exists() else f"### Source: {self.get_mode()}\n"
|
||||
return path, text, read_instructions(text, self.get_mode())
|
||||
|
||||
def reload(self):
|
||||
super().reload()
|
||||
self.editor_button.configure(state="normal" if self.manual_path() else "disabled")
|
||||
try:
|
||||
path, _, instructions = self._read()
|
||||
validate_instructions(path.parent, self.get_mode(), instructions)
|
||||
for combo in getattr(self, "label_choices", []):
|
||||
combo.configure(values=utils.read_all_labels(path.parent))
|
||||
except (CliError, OSError, ValueError, KeyError) as exc:
|
||||
self.status.configure(text=str(exc))
|
||||
|
||||
def open_editor(self):
|
||||
try:
|
||||
path = self.manual_path()
|
||||
if path is None:
|
||||
raise CliError("Chargez une évaluation.")
|
||||
if not path.exists():
|
||||
path.write_text(f"### Source: {self.get_mode()}\n", encoding="utf-8")
|
||||
open_path(path)
|
||||
except (CliError, OSError, ValueError, RuntimeError) as exc:
|
||||
messagebox.showerror("Ouverture du fichier", str(exc))
|
||||
|
||||
def add_instruction(self):
|
||||
try:
|
||||
path, text, instructions = self._read()
|
||||
copy_id = normalize_copy(path.parent, self.copy_var.get())
|
||||
source, target = self.source_var.get().strip(), self.target_var.get().strip()
|
||||
item = ManualInstruction(copy_id, source, "->", target, False)
|
||||
validate_instructions(path.parent, self.get_mode(), [*instructions, item])
|
||||
text = text.rstrip() + f"\nCopie{copy_id} {source} -> {target}|\n"
|
||||
path.write_text(text, encoding="utf-8")
|
||||
self.reload()
|
||||
except (CliError, OSError, ValueError, KeyError) as exc:
|
||||
messagebox.showerror("Instruction invalide", str(exc))
|
||||
|
||||
def _show(self, image, name):
|
||||
path = Path(self.previews.name) / f"{name}.png"
|
||||
image.save(path)
|
||||
open_path(path)
|
||||
|
||||
def open_pdf(self, evaluation, copy_id, label):
|
||||
try:
|
||||
self._show(body_image(evaluation / self.get_mode() / f"Copie{copy_id}", label),
|
||||
f"Copie{copy_id} - {label} - corps")
|
||||
except (CliError, OSError, ValueError, RuntimeError, KeyError) as exc:
|
||||
messagebox.showinfo("Aperçu du corps annoté", str(exc))
|
||||
|
||||
def open_cut(self, evaluation, instruction):
|
||||
def accepted(operator):
|
||||
target = "|" + instruction.new_label if instruction.pipe_first else instruction.new_label + "|"
|
||||
self.cut_result.set(f"Copie{instruction.copy_id} {instruction.old_label} {operator} {target}")
|
||||
try:
|
||||
source = body_image(evaluation / self.get_mode() / f"Copie{instruction.copy_id}", instruction.old_label)
|
||||
initial = (*instruction.cut, instruction.operator[-1]) if instruction.cut else None
|
||||
CutHelper(self, source, accepted, initial)
|
||||
except (CliError, OSError, ValueError, RuntimeError, KeyError) as exc:
|
||||
messagebox.showerror("Coupe du corps annoté", str(exc))
|
||||
|
||||
def preview_result(self):
|
||||
try:
|
||||
path, _, instructions = self._read()
|
||||
validate_instructions(path.parent, self.get_mode(), instructions)
|
||||
# Preview just the requested copy, avoiding a window for every copy.
|
||||
copy_id = normalize_copy(path.parent, self.copy_var.get())
|
||||
selected = [i for i in instructions if i.copy_id == copy_id]
|
||||
if not selected:
|
||||
raise CliError("Aucune instruction pour cette copie. Saisissez son numéro dans Copie.")
|
||||
directory = path.parent / self.get_mode() / f"Copie{copy_id}"
|
||||
_, bodies, changed = edited_bodies(directory, baseline(directory), selected)
|
||||
for label in sorted(changed):
|
||||
if label in bodies:
|
||||
self._show(bodies[label], f"Copie{copy_id} - {label} - résultat")
|
||||
emptied = sorted(changed - bodies.keys())
|
||||
if emptied:
|
||||
messagebox.showinfo("Réponses vidées", ", ".join(emptied))
|
||||
except (CliError, OSError, ValueError, RuntimeError, KeyError) as exc:
|
||||
messagebox.showerror("Aperçu du résultat", str(exc))
|
||||
@@ -212,6 +212,7 @@ def plotting_shortcut_lines() -> list[str]:
|
||||
f"{display_names.get(PLOTTING_KB[action], PLOTTING_KB[action])} : {description}"
|
||||
for action, description in labels
|
||||
]
|
||||
lines.append("! : signaler une erreur et passer à la copie suivante")
|
||||
lines.append("Échap : fermer la fenêtre")
|
||||
return lines
|
||||
|
||||
@@ -1189,7 +1190,7 @@ class CopienatorApp(tk.Tk):
|
||||
)
|
||||
ttk.Label(copies, text=help_text + "Vérifiez la commande puis cliquez sur Exécuter.",
|
||||
wraplength=570).grid(row=3, column=0, columnspan=2, sticky="w", pady=(5, 0))
|
||||
if step.id in {"page_splitter", "cutleft"}:
|
||||
if step.id in {"page_splitter", "cutleft", "labels", "plotting"}:
|
||||
try:
|
||||
marked = copy_errors(EvaluationWorkspace(evaluation)) if evaluation else {}
|
||||
marked_summary = ", ".join(marked) or "Aucune copie signalée."
|
||||
@@ -1316,7 +1317,7 @@ class CopienatorApp(tk.Tk):
|
||||
self.arg_vars["target"].set(self._evaluation_arg())
|
||||
|
||||
def _run_marked_copies(self) -> None:
|
||||
if (not self.current_step or self.current_step.id not in {"page_splitter", "cutleft"}
|
||||
if (not self.current_step or self.current_step.id not in {"page_splitter", "cutleft", "labels", "plotting"}
|
||||
or self.active_step_id or self.runner.running or not self.state_store.evaluation):
|
||||
return
|
||||
self.arg_vars["target"].set(self._evaluation_arg())
|
||||
@@ -2146,7 +2147,7 @@ class CopienatorApp(tk.Tk):
|
||||
if self.current_step and self.current_step.id == "batch_status":
|
||||
self.watch_batches_button.configure(state="disabled" if running or self.batch_monitor.active or not self.state_store.evaluation else "normal")
|
||||
self.stop_watch_batches_button.configure(state="normal" if self.batch_monitor.active else "disabled")
|
||||
if running and self.current_step and self.current_step.id in {"page_splitter", "cutleft"}:
|
||||
if running and self.current_step and self.current_step.id in {"page_splitter", "cutleft", "labels", "plotting"}:
|
||||
self.marked_copies_button.configure(state="disabled")
|
||||
if self.current_step and self.current_step.id == "rename":
|
||||
self.validate_rename_button.configure(
|
||||
|
||||
@@ -292,7 +292,8 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"Pour les sous-pages étroites, la marge commence au bord gauche et est limitée à la largeur disponible. "
|
||||
"Dans la fenêtre de découpe, utilisez les flèches : ←/→ déplacent la zone "
|
||||
"de 50 px vers la gauche/droite, ↑ l’élargit de 50 px et ↓ la réduit de 50 px. "
|
||||
"1 utilise les pages entières, Entrée valide et ! signale une erreur."
|
||||
"1 utilise les pages entières, Entrée valide et ! signale une erreur. "
|
||||
"La validation efface uniquement le signalement de découpe, sans effacer celui des labels."
|
||||
f"{personnal_binding} "
|
||||
"Une copie précise peut être ciblée.",
|
||||
(python("default", "Découpe", "crop-labels"),),
|
||||
@@ -331,6 +332,14 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
(python("default", "Détection des labels", "labels"),),
|
||||
arguments=(
|
||||
arg_target(),
|
||||
ArgumentSpec(
|
||||
"marked",
|
||||
"Copies signalées uniquement",
|
||||
"bool",
|
||||
"--marked",
|
||||
help="Traite uniquement les copies ayant un signalement encore actif. "
|
||||
"En détection, régénère aussi les résultats existants.",
|
||||
),
|
||||
ArgumentSpec(
|
||||
"workers",
|
||||
"Requêtes simultanées",
|
||||
@@ -361,9 +370,20 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"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. "
|
||||
"Le raccourci p revient à l’image précédente, y compris dans la copie précédente.",
|
||||
"Le raccourci p revient à l’image précédente, y compris dans la copie précédente. "
|
||||
"! signale une erreur et passe à la copie suivante. Entrée valide chaque image ; "
|
||||
"le signalement de vérification est effacé après validation complète de la copie.",
|
||||
(python("default", "Vérification", "review-labels"),),
|
||||
arguments=(arg_target(),),
|
||||
arguments=(
|
||||
arg_target(),
|
||||
ArgumentSpec(
|
||||
"marked",
|
||||
"Copies signalées uniquement",
|
||||
"bool",
|
||||
"--marked",
|
||||
help="Vérifie uniquement les copies ayant un signalement encore actif.",
|
||||
),
|
||||
),
|
||||
requires=("labels", "Cutleft"),
|
||||
artifacts=("Copies/Copie*.json",),
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user