Some documentation changes

This commit is contained in:
sebastien committed 2026-09-26 22:08:27 +02:00
1 parent 4c56b655fe
commit a03e0e3b4c
9 files changed
+88 -26

No files matched your search

+7 -6
View File
@@ -197,11 +197,12 @@ racine du projet). Dans la console, les boutons de copie et le clic droit
permettent de copier la sélection ou toute la sortie ; =Ctrl+C= et
=Ctrl+A= sont également disponibles (=Cmd= sous macOS).
Pendant =Découper une partie à gauche pour détection des labels=, =n= décale
la zone de 50 px vers la droite, =N= de 100 px, =t= de 50 px vers la
gauche et =l= l’élargit de 50 px. =1= utilise les pages entières. =s=
signale la copie en erreur et passe à la suivante ; =Entrée= valide et
enregistre la découpe affichée.
Pendant =Découper une partie à gauche pour détection des labels=, les
flèches gauche et droite déplacent la zone de 50 px ; les flèches haut et
bas l’élargissent ou la réduisent de 50 px. Avec le workflow personnel,
=t/s/r/n= reproduisent respectivement gauche/réduire/élargir/droite.
=1= utilise les pages entières. =!= signale la copie en erreur et passe à
la suivante ; =Entrée= valide et enregistre la découpe affichée.
Une copie ignorée conserve ses anciennes découpes. Les signalements sont
conservés dans =.copienator/copy_errors.json=, même après fermeture.
Dans =Séparer et réordonner les pages=, =Traiter les copies signalées=
@@ -209,7 +210,7 @@ reprend ces copies à partir des originaux conservés. Le même bouton dans
=Découper une partie à gauche pour détection des labels= reprend leur
découpage ; chaque signalement
est effacé seulement après validation et enregistrement avec =Entrée=.
Fermer la fenêtre, appuyer à nouveau sur =s= ou rencontrer une erreur
Fermer la fenêtre, appuyer à nouveau sur =!= ou rencontrer une erreur
conserve le signalement. Les commandes =page-split= et =crop-labels=
acceptent aussi =--marked= pour traiter les copies signalées de l’évaluation.
+22 -8
View File
@@ -24,6 +24,7 @@ from copienator import (
)
from copienator.filesystem import staged_files
from copienator.copy_errors import clear_copy_error, mark_copy_error, marked_copy_paths
from copienator.configuration import SHOW_PERSONAL_STEPS
DELIMITER_WIDTH = 5
DELIMITER_COLOR = (0, 0, 0)
@@ -32,6 +33,9 @@ CROP_SHIFT_STEP = 50
CROP_WIDTH_STEP = 50
pdf_cache_lock = threading.Lock()
# The requested spelling is kept as the public switch for this alternate layout.
personnal_binding = bool(SHOW_PERSONAL_STEPS)
def distribute_pages(total_pages: int, max_per_file: int = 5) -> list[int]:
"""Distribute pages into balanced chunks no larger than max_per_file."""
@@ -189,13 +193,17 @@ class ImageReviewer:
self.label_info = tk.Label(self.root, text="", font=("Arial", 12, "bold"))
self.label_info.pack(pady=5)
self.root.bind("<Return>", self.on_next)
self.root.bind("s", self.on_skip)
self.root.bind("!", self.on_skip)
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
self.root.bind("n", lambda _event: self.on_shift(CROP_SHIFT_STEP))
self.root.bind("N", lambda _event: self.on_shift(2 * CROP_SHIFT_STEP))
self.root.bind("t", lambda _event: self.on_shift(-CROP_SHIFT_STEP))
self.root.bind("l", lambda _event: self.on_enlarge(CROP_WIDTH_STEP))
self.root.bind("L", lambda _event: self.on_enlarge(-CROP_WIDTH_STEP))
self.root.bind("<Left>", lambda _event: self.on_shift(-CROP_SHIFT_STEP))
self.root.bind("<Right>", lambda _event: self.on_shift(CROP_SHIFT_STEP))
self.root.bind("<Up>", lambda _event: self.on_enlarge(CROP_WIDTH_STEP))
self.root.bind("<Down>", lambda _event: self.on_enlarge(-CROP_WIDTH_STEP))
if personnal_binding:
self.root.bind("t", lambda _event: self.on_shift(-CROP_SHIFT_STEP))
self.root.bind("n", lambda _event: self.on_shift(CROP_SHIFT_STEP))
self.root.bind("r", lambda _event: self.on_enlarge(CROP_WIDTH_STEP))
self.root.bind("s", lambda _event: self.on_enlarge(-CROP_WIDTH_STEP))
self.root.bind("1", lambda _event: self.on_set_max_pages(1))
Thread(target=self.prefetch_worker, daemon=True).start()
@@ -283,6 +291,11 @@ class ImageReviewer:
tk_image = ImageTk.PhotoImage(self.current_preview)
self.label_img.configure(image=tk_image)
self.label_img.image = tk_image
personal_instructions = (
"\nt/s/r/n also available: left / narrower / wider / right"
if personnal_binding
else ""
)
self.label_info.configure(
text=(
f"[{self.index + 1}/{len(self.files)}] {filename} | "
@@ -290,8 +303,9 @@ class ImageReviewer:
f"Extra width: {self.current_width_offset}px\n"
f"Files: {schema['number_of_files']} | "
f"Cols: {schema['columns_per_file']}\n"
"Enter: Save and next | s: flag error and skip | n: +50 | N: +100 | t: -50 | "
"l: widen by 50 | Shift+l: undo widening by 50 | 1: use full pages"
"Arrows: left/right move by 50px | up/down widen/narrow by 50px\n"
"Enter: Save and next | !: flag error and skip | 1: use full pages"
f"{personal_instructions}"
),
fg="black",
)
+1
View File
@@ -33,6 +33,7 @@ else:
# Keep new optional settings compatible with older personal configuration files.
ALWAYS_CROP = False
SHOW_PERSONAL_STEPS = False
RETURN_JPEG_ENABLED = True
RETURN_PDF_ENABLED = True
RETURN_ANSWERS_ENABLED = False
+9
View File
@@ -1749,6 +1749,15 @@ class CopienatorApp(tk.Tk):
self.state_store.update_step(
step_id, status=stored_status, return_code=return_code
)
if (step_id == "statement" and status == "success"
and self.state_store.step(step_id).get("last_run_variant") == "gemini"):
reason = "Barèmes déjà générés par l’analyse Gemini de l’énoncé."
self.state_store.update_step("statement_persp", status="skipped")
self.state_store.add_history({
"step": "statement_persp", "status": "skipped",
"manual": False, "reason": reason,
})
self._append_console(f"Remplacer les barèmes par Gemini : ignorée — {reason}\n")
if step_id == "annotation" and status == "success":
generation_variant = self.state_store.step("annotation").get(
"last_run_variant"
+19 -9
View File
@@ -88,6 +88,12 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
manual = lambda ident, label="Étape manuelle": CommandVariant(
ident, label, None, "manual"
)
personnal_binding = (
"\nWorkflow personnel : les raccourcis t/s/r/n sont aussi disponibles "
"(t : gauche, s : réduire, r : élargir, n : droite)."
if show_personal_steps
else ""
)
return_answer_arguments = ()
if configuration.RETURN_ANSWERS_ENABLED:
@@ -136,7 +142,9 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
"statement",
"Prétraitement de l’énoncé",
"Analyser l’énoncé",
"Détecte les questions, leurs groupes, le corrigé et les indications de barème.",
"Avec Gemini : extrait les questions, les contextes et les solutions."
"Propose, sur la base des labels, des groupement de questions. Ces groupes seront utilisés lors des requêtes de correction."
"Génère aussi des barèmes détaillés de chaque question. ",
((python("personal", "Énoncés et solutions personnels (SHEETINFO)", "statement-personal"),)
if show_personal_steps else ())
+ (python("gemini", "Analyse avec Gemini", "statement"),),
@@ -156,16 +164,16 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
),
StepDefinition(
"statement_groups", "Prétraitement de l’énoncé", "Regrouper les questions avec Gemini",
"Facultatif après la génération : remplace les groupes par exercice par des groupes "
"proposés par Gemini, en conservant les labels, les énoncés, les solutions et les barèmes.",
"Facultatif. Demande à Gemini de regrouper les questions selon leurs dépendances et leur contexte commun. "
"L’analyse Gemini initiale regroupe d’après la structure des labels ; cette étape peut donc proposer un autre découpage. "
"Ignorez cette étape si les groupes actuels vous conviennent.",
(python("default", "Groupes Gemini", "statement", fixed_args=("--groups-only",)),),
arguments=(arg_target("le dossier de l’évaluation"),),
optional=True, personal=True, requires=("labels", "Text2", "Sol2"),
),
StepDefinition(
"statement_persp", "Prétraitement de l’énoncé", "Remplacer les barèmes par Gemini",
"Facultatif : remplace Persp par des barèmes Gemini sur 4 points, pour les groupes actuels. "
"Les énoncés et les solutions personnels sont conservés.",
"statement_persp", "Prétraitement de l’énoncé", "Générer les barèmes par Gemini",
"Facultatif. Inutile après l'analyse de l'énoncé par Gemini, puisque les barèmes ont déjà été générés. ",
(python("default", "Barèmes Gemini", "statement", fixed_args=("--persp-only",)),),
arguments=(arg_target("le dossier de l’évaluation"),),
optional=True, personal=True, requires=("labels", "label_groups", "Text2", "Sol2"),
@@ -261,9 +269,11 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
"cutleft",
"Prétraitement des copies",
"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. "
"Produit les images de la partie gauche des copies. "
"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."
f"{personnal_binding} "
"Une copie précise peut être ciblée.",
(python("default", "Découpe", "crop-labels"),),
arguments=(
+2 -1
View File
@@ -28,11 +28,12 @@ FINAL_SCORE_HISTOGRAM_PATH = Path("histogramme.pdf")
FINAL_SCORE_FONT_PATH = None
# Modèle pour des choses très légères
# Le modèle 3.5-flash-lite est un peu moins bon pour la duplication de contexte pour des questions, dans l'analyse de l'énoncé.
# MODEL_LITE_ID = "gemini-3.5-flash-lite"
MODEL_LITE_ID = "gemini-3.8-flash"
# Modèle pour identifier visuellement les labels
# MODEL_FOR_LABEL_ID = "gemini-3.5-flash-lite" # 3.5 flash lite marche moyennement, il insiste pour mettre les labels dans l'ordre, sans considérer ce qu'il y a écrit
# MODEL_FOR_LABEL_ID = "gemini-3.5-flash-lite" # 3.5 flash lite marche plutôt mal, il insiste pour mettre les labels dans l'ordre, sans considérer ce qu'il y a écrit
MODEL_FOR_LABEL_ID = "gemini-3.8-flash"
# Modèle pour des choses normales
+18
View File
@@ -81,6 +81,24 @@ class GuiConvenienceTests(unittest.TestCase):
style.lookup("MissingPrerequisite.TLabel", "foreground"), "#c62828"
)
def test_only_successful_gemini_analysis_skips_rubric_replacement(self):
for variant, code, interrupted, expected in (
("gemini", 0, False, "skipped"),
("personal", 0, False, "ready"),
("gemini", 4, False, "ready"),
("gemini", 1, False, "ready"),
("gemini", 0, True, "ready"),
):
with self.subTest(variant=variant, code=code, interrupted=interrupted):
self.app.state_store.update_step("statement_persp", status="ready")
self.app.state_store.update_step("statement_groups", status="ready")
self.app.state_store.update_step("statement", last_run_variant=variant)
self.app.active_step_id = "statement"
with patch.object(self.app, "after_idle"):
self.app._finish_process(code, interrupted)
self.assertEqual(self.app.state_store.step("statement_persp")["status"], expected)
self.assertEqual(self.app.state_store.step("statement_groups")["status"], "ready")
def test_compact_evaluation_input_and_open_folder_button(self):
self.assertEqual(int(self.app.evaluation_entry.cget("width")), 42)
self.assertEqual(
+4 -1
View File
@@ -2245,10 +2245,13 @@ class WorkflowTests(unittest.TestCase):
step.title,
"Découper une partie à gauche pour détection des labels",
)
for shortcut in ("n", "N", "t", "l", "1", "Entrée", "s"):
for shortcut in ("←", "→", "↑", "↓", "1", "Entrée", "!", "t/s/r/n"):
with self.subTest(shortcut=shortcut):
self.assertIn(shortcut, step.description)
standard = next(step for step in build_workflow(False) if step.id == "cutleft")
self.assertNotIn("t/s/r/n", standard.description)
def test_export_has_shorter_title_and_annotation_argument(self) -> None:
self.assertEqual(self.steps["export"].title, "Exporter")
command = self.command(
+6 -1
View File
@@ -107,6 +107,11 @@ class MarkedCopiesTests(unittest.TestCase):
self.assertEqual(review.current_width_offset, 50)
review.trigger_processing.assert_called_once_with(self.files[0], 50)
def test_personal_crop_binding_follows_personal_workflow_option(self):
from copienator.configuration import SHOW_PERSONAL_STEPS
self.assertEqual(cutleft.personnal_binding, bool(SHOW_PERSONAL_STEPS))
def test_process_single_pdf_enlarges_crop_and_caps_it_at_page_edge(self):
page = Image.new("RGB", (900, 300), "white")
with patch.object(cutleft, "get_pdf_pages", return_value=[page]):
@@ -188,7 +193,7 @@ class MarkedCopiesGuiTests(unittest.TestCase):
self.assertTrue(reviewer.completed)
return reviewer
first = review_with_keys(self.files, ["<KeyPress-s>", "<Return>"])
first = review_with_keys(self.files, ["<KeyPress-exclam>", "<Return>"])
self.assertTrue(first.had_errors)
self.assertEqual(set(copy_errors(self.workspace)), {"Copie01.pdf"})
self.assertFalse((self.workspace.cutleft_dir / "Copie01_01.jpg").exists())