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
|
permettent de copier la sélection ou toute la sortie ; =Ctrl+C= et
|
||||||
=Ctrl+A= sont également disponibles (=Cmd= sous macOS).
|
=Ctrl+A= sont également disponibles (=Cmd= sous macOS).
|
||||||
|
|
||||||
Pendant =Découper une partie à gauche pour détection des labels=, =n= décale
|
Pendant =Découper une partie à gauche pour détection des labels=, les
|
||||||
la zone de 50 px vers la droite, =N= de 100 px, =t= de 50 px vers la
|
flèches gauche et droite déplacent la zone de 50 px ; les flèches haut et
|
||||||
gauche et =l= l’élargit de 50 px. =1= utilise les pages entières. =s=
|
bas l’élargissent ou la réduisent de 50 px. Avec le workflow personnel,
|
||||||
signale la copie en erreur et passe à la suivante ; =Entrée= valide et
|
=t/s/r/n= reproduisent respectivement gauche/réduire/élargir/droite.
|
||||||
enregistre la découpe affichée.
|
=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
|
Une copie ignorée conserve ses anciennes découpes. Les signalements sont
|
||||||
conservés dans =.copienator/copy_errors.json=, même après fermeture.
|
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=
|
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écouper une partie à gauche pour détection des labels= reprend leur
|
||||||
découpage ; chaque signalement
|
découpage ; chaque signalement
|
||||||
est effacé seulement après validation et enregistrement avec =Entrée=.
|
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=
|
conserve le signalement. Les commandes =page-split= et =crop-labels=
|
||||||
acceptent aussi =--marked= pour traiter les copies signalées de l’évaluation.
|
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.filesystem import staged_files
|
||||||
from copienator.copy_errors import clear_copy_error, mark_copy_error, marked_copy_paths
|
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_WIDTH = 5
|
||||||
DELIMITER_COLOR = (0, 0, 0)
|
DELIMITER_COLOR = (0, 0, 0)
|
||||||
@@ -32,6 +33,9 @@ CROP_SHIFT_STEP = 50
|
|||||||
CROP_WIDTH_STEP = 50
|
CROP_WIDTH_STEP = 50
|
||||||
pdf_cache_lock = threading.Lock()
|
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]:
|
def distribute_pages(total_pages: int, max_per_file: int = 5) -> list[int]:
|
||||||
"""Distribute pages into balanced chunks no larger than max_per_file."""
|
"""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 = tk.Label(self.root, text="", font=("Arial", 12, "bold"))
|
||||||
self.label_info.pack(pady=5)
|
self.label_info.pack(pady=5)
|
||||||
self.root.bind("<Return>", self.on_next)
|
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.protocol("WM_DELETE_WINDOW", self.on_close)
|
||||||
self.root.bind("n", lambda _event: self.on_shift(CROP_SHIFT_STEP))
|
self.root.bind("<Left>", lambda _event: self.on_shift(-CROP_SHIFT_STEP))
|
||||||
self.root.bind("N", lambda _event: self.on_shift(2 * CROP_SHIFT_STEP))
|
self.root.bind("<Right>", lambda _event: self.on_shift(CROP_SHIFT_STEP))
|
||||||
self.root.bind("t", lambda _event: self.on_shift(-CROP_SHIFT_STEP))
|
self.root.bind("<Up>", lambda _event: self.on_enlarge(CROP_WIDTH_STEP))
|
||||||
self.root.bind("l", lambda _event: self.on_enlarge(CROP_WIDTH_STEP))
|
self.root.bind("<Down>", lambda _event: self.on_enlarge(-CROP_WIDTH_STEP))
|
||||||
self.root.bind("L", 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))
|
self.root.bind("1", lambda _event: self.on_set_max_pages(1))
|
||||||
|
|
||||||
Thread(target=self.prefetch_worker, daemon=True).start()
|
Thread(target=self.prefetch_worker, daemon=True).start()
|
||||||
@@ -283,6 +291,11 @@ class ImageReviewer:
|
|||||||
tk_image = ImageTk.PhotoImage(self.current_preview)
|
tk_image = ImageTk.PhotoImage(self.current_preview)
|
||||||
self.label_img.configure(image=tk_image)
|
self.label_img.configure(image=tk_image)
|
||||||
self.label_img.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(
|
self.label_info.configure(
|
||||||
text=(
|
text=(
|
||||||
f"[{self.index + 1}/{len(self.files)}] {filename} | "
|
f"[{self.index + 1}/{len(self.files)}] {filename} | "
|
||||||
@@ -290,8 +303,9 @@ class ImageReviewer:
|
|||||||
f"Extra width: {self.current_width_offset}px\n"
|
f"Extra width: {self.current_width_offset}px\n"
|
||||||
f"Files: {schema['number_of_files']} | "
|
f"Files: {schema['number_of_files']} | "
|
||||||
f"Cols: {schema['columns_per_file']}\n"
|
f"Cols: {schema['columns_per_file']}\n"
|
||||||
"Enter: Save and next | s: flag error and skip | n: +50 | N: +100 | t: -50 | "
|
"Arrows: left/right move by 50px | up/down widen/narrow by 50px\n"
|
||||||
"l: widen by 50 | Shift+l: undo widening by 50 | 1: use full pages"
|
"Enter: Save and next | !: flag error and skip | 1: use full pages"
|
||||||
|
f"{personal_instructions}"
|
||||||
),
|
),
|
||||||
fg="black",
|
fg="black",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ else:
|
|||||||
|
|
||||||
# Keep new optional settings compatible with older personal configuration files.
|
# Keep new optional settings compatible with older personal configuration files.
|
||||||
ALWAYS_CROP = False
|
ALWAYS_CROP = False
|
||||||
|
SHOW_PERSONAL_STEPS = False
|
||||||
RETURN_JPEG_ENABLED = True
|
RETURN_JPEG_ENABLED = True
|
||||||
RETURN_PDF_ENABLED = True
|
RETURN_PDF_ENABLED = True
|
||||||
RETURN_ANSWERS_ENABLED = False
|
RETURN_ANSWERS_ENABLED = False
|
||||||
|
|||||||
@@ -1749,6 +1749,15 @@ class CopienatorApp(tk.Tk):
|
|||||||
self.state_store.update_step(
|
self.state_store.update_step(
|
||||||
step_id, status=stored_status, return_code=return_code
|
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":
|
if step_id == "annotation" and status == "success":
|
||||||
generation_variant = self.state_store.step("annotation").get(
|
generation_variant = self.state_store.step("annotation").get(
|
||||||
"last_run_variant"
|
"last_run_variant"
|
||||||
|
|||||||
@@ -88,6 +88,12 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
manual = lambda ident, label="Étape manuelle": CommandVariant(
|
manual = lambda ident, label="Étape manuelle": CommandVariant(
|
||||||
ident, label, None, "manual"
|
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 = ()
|
return_answer_arguments = ()
|
||||||
if configuration.RETURN_ANSWERS_ENABLED:
|
if configuration.RETURN_ANSWERS_ENABLED:
|
||||||
@@ -136,7 +142,9 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"statement",
|
"statement",
|
||||||
"Prétraitement de l’énoncé",
|
"Prétraitement de l’énoncé",
|
||||||
"Analyser 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"),)
|
((python("personal", "Énoncés et solutions personnels (SHEETINFO)", "statement-personal"),)
|
||||||
if show_personal_steps else ())
|
if show_personal_steps else ())
|
||||||
+ (python("gemini", "Analyse avec Gemini", "statement"),),
|
+ (python("gemini", "Analyse avec Gemini", "statement"),),
|
||||||
@@ -156,16 +164,16 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
),
|
),
|
||||||
StepDefinition(
|
StepDefinition(
|
||||||
"statement_groups", "Prétraitement de l’énoncé", "Regrouper les questions avec Gemini",
|
"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 "
|
"Facultatif. Demande à Gemini de regrouper les questions selon leurs dépendances et leur contexte commun. "
|
||||||
"proposés par Gemini, en conservant les labels, les énoncés, les solutions et les barèmes.",
|
"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",)),),
|
(python("default", "Groupes Gemini", "statement", fixed_args=("--groups-only",)),),
|
||||||
arguments=(arg_target("le dossier de l’évaluation"),),
|
arguments=(arg_target("le dossier de l’évaluation"),),
|
||||||
optional=True, personal=True, requires=("labels", "Text2", "Sol2"),
|
optional=True, personal=True, requires=("labels", "Text2", "Sol2"),
|
||||||
),
|
),
|
||||||
StepDefinition(
|
StepDefinition(
|
||||||
"statement_persp", "Prétraitement de l’énoncé", "Remplacer les barèmes par Gemini",
|
"statement_persp", "Prétraitement de l’énoncé", "Générer les barèmes par Gemini",
|
||||||
"Facultatif : remplace Persp par des barèmes Gemini sur 4 points, pour les groupes actuels. "
|
"Facultatif. Inutile après l'analyse de l'énoncé par Gemini, puisque les barèmes ont déjà été générés. ",
|
||||||
"Les énoncés et les solutions personnels sont conservés.",
|
|
||||||
(python("default", "Barèmes Gemini", "statement", fixed_args=("--persp-only",)),),
|
(python("default", "Barèmes Gemini", "statement", fixed_args=("--persp-only",)),),
|
||||||
arguments=(arg_target("le dossier de l’évaluation"),),
|
arguments=(arg_target("le dossier de l’évaluation"),),
|
||||||
optional=True, personal=True, requires=("labels", "label_groups", "Text2", "Sol2"),
|
optional=True, personal=True, requires=("labels", "label_groups", "Text2", "Sol2"),
|
||||||
@@ -261,9 +269,11 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"cutleft",
|
"cutleft",
|
||||||
"Prétraitement des copies",
|
"Prétraitement des copies",
|
||||||
"Découper une partie à gauche pour détection des labels",
|
"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 : "
|
"Produit les images de la partie gauche des copies. "
|
||||||
"n décale la zone de 50 px vers la droite, N de 100 px, t de 50 px vers la gauche, "
|
"Dans la fenêtre de découpe, utilisez les flèches : ←/→ déplacent la zone "
|
||||||
"l l’élargit de 50 px, 1 utilise les pages entières, Entrée valide et s signale une erreur. "
|
"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.",
|
"Une copie précise peut être ciblée.",
|
||||||
(python("default", "Découpe", "crop-labels"),),
|
(python("default", "Découpe", "crop-labels"),),
|
||||||
arguments=(
|
arguments=(
|
||||||
|
|||||||
+2
-1
@@ -28,11 +28,12 @@ FINAL_SCORE_HISTOGRAM_PATH = Path("histogramme.pdf")
|
|||||||
FINAL_SCORE_FONT_PATH = None
|
FINAL_SCORE_FONT_PATH = None
|
||||||
|
|
||||||
# Modèle pour des choses très légères
|
# 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.5-flash-lite"
|
||||||
MODEL_LITE_ID = "gemini-3.8-flash"
|
MODEL_LITE_ID = "gemini-3.8-flash"
|
||||||
|
|
||||||
# Modèle pour identifier visuellement les labels
|
# 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"
|
MODEL_FOR_LABEL_ID = "gemini-3.8-flash"
|
||||||
|
|
||||||
# Modèle pour des choses normales
|
# Modèle pour des choses normales
|
||||||
|
|||||||
@@ -81,6 +81,24 @@ class GuiConvenienceTests(unittest.TestCase):
|
|||||||
style.lookup("MissingPrerequisite.TLabel", "foreground"), "#c62828"
|
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):
|
def test_compact_evaluation_input_and_open_folder_button(self):
|
||||||
self.assertEqual(int(self.app.evaluation_entry.cget("width")), 42)
|
self.assertEqual(int(self.app.evaluation_entry.cget("width")), 42)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|||||||
@@ -2245,10 +2245,13 @@ class WorkflowTests(unittest.TestCase):
|
|||||||
step.title,
|
step.title,
|
||||||
"Découper une partie à gauche pour détection des labels",
|
"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):
|
with self.subTest(shortcut=shortcut):
|
||||||
self.assertIn(shortcut, step.description)
|
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:
|
def test_export_has_shorter_title_and_annotation_argument(self) -> None:
|
||||||
self.assertEqual(self.steps["export"].title, "Exporter")
|
self.assertEqual(self.steps["export"].title, "Exporter")
|
||||||
command = self.command(
|
command = self.command(
|
||||||
|
|||||||
@@ -107,6 +107,11 @@ class MarkedCopiesTests(unittest.TestCase):
|
|||||||
self.assertEqual(review.current_width_offset, 50)
|
self.assertEqual(review.current_width_offset, 50)
|
||||||
review.trigger_processing.assert_called_once_with(self.files[0], 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):
|
def test_process_single_pdf_enlarges_crop_and_caps_it_at_page_edge(self):
|
||||||
page = Image.new("RGB", (900, 300), "white")
|
page = Image.new("RGB", (900, 300), "white")
|
||||||
with patch.object(cutleft, "get_pdf_pages", return_value=[page]):
|
with patch.object(cutleft, "get_pdf_pages", return_value=[page]):
|
||||||
@@ -188,7 +193,7 @@ class MarkedCopiesGuiTests(unittest.TestCase):
|
|||||||
self.assertTrue(reviewer.completed)
|
self.assertTrue(reviewer.completed)
|
||||||
return reviewer
|
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.assertTrue(first.had_errors)
|
||||||
self.assertEqual(set(copy_errors(self.workspace)), {"Copie01.pdf"})
|
self.assertEqual(set(copy_errors(self.workspace)), {"Copie01.pdf"})
|
||||||
self.assertFalse((self.workspace.cutleft_dir / "Copie01_01.jpg").exists())
|
self.assertFalse((self.workspace.cutleft_dir / "Copie01_01.jpg").exists())
|
||||||
|
|||||||
Reference in new issue
Block a user