Some documentation changes
This commit is contained in:
1 parent
4c56b655fe
commit
a03e0e3b4c
9 files changed
+88
-26
No files matched your search
+7
-6
@@ -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.
|
||||
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in new issue
Block a user