From e4e25eca55d757920ba5dd575b294f445a96a4ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Miquel?= Date: Sat, 26 Sep 2026 11:22:09 +0200 Subject: [PATCH] Support for A.pdf, A.tex and Ac.tex for initial files --- copienator/commands/clean.py | 5 ++- copienator/commands/copies_tools.py | 3 +- copienator/commands/enonce_info.py | 2 +- copienator/commands/gemini_for_enonce.py | 10 ++++-- copienator/commands/plotting.py | 12 ++----- copienator/workspace.py | 43 ++++++++++++++++++++++-- copienator_gui/app.py | 8 +++-- copienator_gui/workflow.py | 3 +- default_config.py | 3 +- rename_to_copie.sh | 3 +- rotate_all.sh | 3 +- tests/test_gui_core.py | 41 ++++++++++++++++++---- 12 files changed, 104 insertions(+), 32 deletions(-) diff --git a/copienator/commands/clean.py b/copienator/commands/clean.py index 303329b..455bdb8 100644 --- a/copienator/commands/clean.py +++ b/copienator/commands/clean.py @@ -109,7 +109,10 @@ def _return_artifacts(workspace: EvaluationWorkspace) -> list[Path]: def _is_statement_text_file(workspace: EvaluationWorkspace, path: Path) -> bool: relative = path.relative_to(workspace.root) if len(relative.parts) == 1: - return relative.name in STATEMENT_ROOT_FILES + return relative.name in STATEMENT_ROOT_FILES or path in { + *workspace.input_candidates("enonce.tex"), + *workspace.input_candidates("correction.tex"), + } top_level = relative.parts[0] if top_level in STATEMENT_TEXT_DIRECTORIES: return path.suffix.casefold() in {"", ".json", ".tex", ".txt"} diff --git a/copienator/commands/copies_tools.py b/copienator/commands/copies_tools.py index eab10af..b0686d7 100644 --- a/copienator/commands/copies_tools.py +++ b/copienator/commands/copies_tools.py @@ -24,7 +24,7 @@ def copy_pdfs(directory: Path) -> list[Path]: ( path for path in directory.glob("*.pdf") - if path.name.casefold() not in {"enonce.pdf", "énoncé.pdf"} + if not EvaluationWorkspace(directory).is_statement_pdf(path) ), key=lambda path: path.name.casefold(), ) @@ -134,4 +134,3 @@ def main(argv: Sequence[str] | None = None) -> int: if __name__ == "__main__": raise SystemExit(main()) - diff --git a/copienator/commands/enonce_info.py b/copienator/commands/enonce_info.py index 9104c2e..a2aca57 100644 --- a/copienator/commands/enonce_info.py +++ b/copienator/commands/enonce_info.py @@ -143,7 +143,7 @@ def save_split_content(text, path, base_fname, problem): def process_directory(workspace: EvaluationWorkspace) -> ExitCode: directory = str(workspace.root) # Find the first .tex file in the directory - enonce = workspace.root / "enonce.tex" + enonce = workspace.statement_source_file tex_files = [str(enonce)] if enonce.is_file() else sorted(glob.glob(os.path.join(directory, "*.tex"))) if not tex_files: print(f"No .tex file found in {directory}. Looking in /Staging/Interro/") diff --git a/copienator/commands/gemini_for_enonce.py b/copienator/commands/gemini_for_enonce.py index 03b3aee..058b542 100644 --- a/copienator/commands/gemini_for_enonce.py +++ b/copienator/commands/gemini_for_enonce.py @@ -331,14 +331,18 @@ def process_exam( cache_c_file = cache_dir / "gemini_context.json" # 1. Resolve files - pdf_path = folder / "enonce.pdf" + pdf_path = workspace.statement_pdf_file enonce_path = find_file(folder, "enonce") + if enonce_path is None: + enonce_path = workspace.statement_source_file correction_path = find_file(folder, "correction") + if correction_path is None: + correction_path = workspace.correction_source_file missing = [] if not pdf_path.is_file(): missing.append("enonce.pdf") - if not enonce_path: missing.append("enonce.org or enonce.tex") - if not correction_path: missing.append("correction.org or correction.tex") + if not enonce_path.is_file(): missing.append("enonce.tex") + if not correction_path.is_file(): missing.append("correction.tex") if missing: raise CliError( diff --git a/copienator/commands/plotting.py b/copienator/commands/plotting.py index 93f39bb..cb9c5c6 100644 --- a/copienator/commands/plotting.py +++ b/copienator/commands/plotting.py @@ -476,17 +476,11 @@ class ImageViewer: def on_open_interro(self, event): if self.is_viewing and self.current_json_path: # Check local directory first - local_accent = self.base_dir / "énoncé.pdf" - local_plain = self.base_dir / "enonce.pdf" - - if local_accent.exists(): - pdf_path = str(local_accent) - elif local_plain.exists(): - pdf_path = local_plain - else: + pdf_path = EvaluationWorkspace(self.base_dir).statement_pdf_file + if not pdf_path.is_file(): messagebox.showerror( "PDF not found", - f"Neither {local_accent.name} nor {local_plain.name} exists in {self.base_dir}.", + f"No statement PDF exists in {self.base_dir}.", ) return diff --git a/copienator/workspace.py b/copienator/workspace.py index aadc914..b36b1c5 100644 --- a/copienator/workspace.py +++ b/copienator/workspace.py @@ -63,12 +63,49 @@ class EvaluationWorkspace: ".copienator", ".copienator-gui.json", ) - return path.is_dir() and any((path / marker).exists() for marker in markers) + if not path.is_dir(): + return False + aliases = (f"{path.name}.pdf", f"{path.name}.tex", f"{path.name}c.tex") + return any((path / marker).exists() for marker in (*markers, *aliases)) @property def name(self) -> str: return self.root.name + def input_candidates(self, canonical_name: str) -> tuple[Path, ...]: + """Return preferred paths for one of the three statement inputs.""" + aliases = { + "enonce.pdf": ("enonce.pdf", "énoncé.pdf", f"{self.name}.pdf"), + "enonce.tex": ("enonce.tex", f"{self.name}.tex"), + "correction.tex": ("correction.tex", f"{self.name}c.tex"), + } + names = aliases.get(canonical_name, (canonical_name,)) + return tuple(self.root / name for name in names) + + def resolve(self, relative_path: str) -> Path: + """Resolve a logical workspace path, including statement aliases.""" + candidates = self.input_candidates(relative_path) + return next((path for path in candidates if path.exists()), candidates[0]) + + def resolve_file(self, relative_path: str) -> Path: + candidates = self.input_candidates(relative_path) + return next((path for path in candidates if path.is_file()), candidates[0]) + + @property + def statement_pdf_file(self) -> Path: + return self.resolve_file("enonce.pdf") + + @property + def statement_source_file(self) -> Path: + return self.resolve_file("enonce.tex") + + @property + def correction_source_file(self) -> Path: + return self.resolve_file("correction.tex") + + def is_statement_pdf(self, path: Path) -> bool: + return path in self.input_candidates("enonce.pdf") + @property def metadata_dir(self) -> Path: return self.root / ".copienator" @@ -220,7 +257,7 @@ class EvaluationWorkspace: missing = [ relative_path for relative_path in relative_paths - if not (self.root / relative_path).exists() + if not self.resolve(relative_path).exists() ] if missing: raise WorkspaceValidationError(self.root, missing) @@ -229,7 +266,7 @@ class EvaluationWorkspace: missing = [ relative_path for relative_path in relative_paths - if not (self.root / relative_path).is_file() + if not self.resolve_file(relative_path).is_file() ] if missing: raise WorkspaceValidationError(self.root, missing) diff --git a/copienator_gui/app.py b/copienator_gui/app.py index b4de6d2..eeb4158 100644 --- a/copienator_gui/app.py +++ b/copienator_gui/app.py @@ -173,7 +173,7 @@ def copy_pdf_paths(evaluation: Path) -> list[Path]: ( path for path in location.glob("*.pdf") - if path.name.casefold() not in {"enonce.pdf", "énoncé.pdf"} + if not EvaluationWorkspace(evaluation).is_statement_pdf(path) ), key=lambda path: path.name.casefold(), ) @@ -578,7 +578,8 @@ class CopienatorApp(tk.Tk): if self.state_store.step(step.id).get("status") == "running": self.state_store.update_step(step.id, status="interrupted") self._populate_tree() - missing = [name for name in ("enonce.pdf", "enonce.tex", "correction.tex") if not (evaluation / name).exists()] + workspace = EvaluationWorkspace(evaluation) + missing = [name for name in ("enonce.pdf", "enonce.tex", "correction.tex") if not workspace.resolve(name).exists()] names_found = (evaluation / "names").exists() or (self.repository / "names").exists() details = [] if missing: @@ -678,7 +679,8 @@ class CopienatorApp(tk.Tk): if any(char in pattern for char in "*?["): exists = next(evaluation.glob(pattern), None) is not None else: - path = EvaluationWorkspace(evaluation).annotation_dir("refaire") if pattern == "BRnot" else evaluation / pattern + workspace = EvaluationWorkspace(evaluation) + path = workspace.annotation_dir("refaire") if pattern == "BRnot" else workspace.resolve(pattern) exists = path.exists() if not exists: missing.append(pattern) diff --git a/copienator_gui/workflow.py b/copienator_gui/workflow.py index 6a3c1cf..9872673 100644 --- a/copienator_gui/workflow.py +++ b/copienator_gui/workflow.py @@ -126,7 +126,8 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]: "inputs", "Préparation", "Vérifier les fichiers d’entrée", - "Le dossier doit contenir enonce.pdf, enonce.tex et correction.tex. " + "Le dossier doit contenir enonce.pdf, enonce.tex et correction.tex, ou, " + "s’il s’appelle A, les fichiers A.pdf, A.tex et Ac.tex. " "Le fichier names doit être présent dans l’évaluation ou à la racine du projet.", (manual("check"),), requires=("enonce.pdf", "enonce.tex", "correction.tex"), diff --git a/default_config.py b/default_config.py index af648ef..7e8ffad 100644 --- a/default_config.py +++ b/default_config.py @@ -28,7 +28,8 @@ FINAL_SCORE_HISTOGRAM_PATH = Path("histogramme.pdf") FINAL_SCORE_FONT_PATH = None # Modèle pour des choses très légères -MODEL_LITE_ID = "gemini-3.5-flash-lite" +# 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 diff --git a/rename_to_copie.sh b/rename_to_copie.sh index a4a0ef8..a4b85a5 100755 --- a/rename_to_copie.sh +++ b/rename_to_copie.sh @@ -8,13 +8,14 @@ fi # Go to the directory cd "$1" || exit +folder_name="$(basename "$PWD")" count=1 for file in *.pdf; do # Handle case where no pdfs exist [ -e "$file" ] || continue - if [ "$file" = "enonce.pdf" ]; then + if [ "$file" = "enonce.pdf" ] || [ "$file" = "énoncé.pdf" ] || [ "$file" = "$folder_name.pdf" ]; then echo "Skipping: $file" continue fi diff --git a/rotate_all.sh b/rotate_all.sh index 56e61c9..3e7df4e 100755 --- a/rotate_all.sh +++ b/rotate_all.sh @@ -8,12 +8,13 @@ fi # Try to change into the directory, exit on failure cd "$1" || { echo "Error: Cannot access directory '$1'"; exit 1; } +folder_name="$(basename "$PWD")" # Enable nullglob: if no pdfs exist, the loop won't run once with "*.pdf" shopt -s nullglob for file in *.pdf; do - if [ "$file" = "enonce.pdf" ]; then + if [ "$file" = "enonce.pdf" ] || [ "$file" = "énoncé.pdf" ] || [ "$file" = "$folder_name.pdf" ]; then echo "Skipping: $file" continue fi diff --git a/tests/test_gui_core.py b/tests/test_gui_core.py index bd8e9a7..b19c946 100644 --- a/tests/test_gui_core.py +++ b/tests/test_gui_core.py @@ -108,6 +108,33 @@ class WorkspaceTests(unittest.TestCase): workspace = EvaluationWorkspace.discover(artifact) self.assertEqual(workspace.root, root) + def test_folder_named_statement_inputs_are_logical_canonical_files(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) / "A" + root.mkdir() + for name in ("A.pdf", "A.tex", "Ac.tex"): + (root / name).touch() + + workspace = EvaluationWorkspace.discover(root) + workspace.require_files("enonce.pdf", "enonce.tex", "correction.tex") + + self.assertEqual(workspace.statement_pdf_file, root / "A.pdf") + self.assertEqual(workspace.statement_source_file, root / "A.tex") + self.assertEqual(workspace.correction_source_file, root / "Ac.tex") + + def test_canonical_statement_inputs_take_precedence_over_folder_aliases(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) / "A" + root.mkdir() + for name in ("enonce.pdf", "A.pdf", "enonce.tex", "A.tex", + "correction.tex", "Ac.tex"): + (root / name).touch() + + workspace = EvaluationWorkspace(root) + self.assertEqual(workspace.statement_pdf_file, root / "enonce.pdf") + self.assertEqual(workspace.statement_source_file, root / "enonce.tex") + self.assertEqual(workspace.correction_source_file, root / "correction.tex") + def test_discover_and_require_report_clear_errors(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) @@ -2574,8 +2601,9 @@ class WorkflowTests(unittest.TestCase): class CrossPlatformFileTests(unittest.TestCase): def test_rotate_all_skips_statement(self) -> None: with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - for filename in ("Copie.pdf", "enonce.pdf"): + root = Path(directory) / "A" + root.mkdir() + for filename in ("Copie.pdf", "A.pdf"): writer = PdfWriter() writer.add_blank_page(width=100, height=200) with (root / filename).open("wb") as output: @@ -2583,22 +2611,23 @@ class CrossPlatformFileTests(unittest.TestCase): self.assertEqual(rotate_all(root), 1) self.assertEqual(PdfReader(root / "Copie.pdf").pages[0].rotation, 180) - self.assertEqual(PdfReader(root / "enonce.pdf").pages[0].rotation, 0) + self.assertEqual(PdfReader(root / "A.pdf").pages[0].rotation, 0) def test_rename_all_is_collision_safe(self) -> None: with tempfile.TemporaryDirectory() as directory: - root = Path(directory) + root = Path(directory) / "A" + root.mkdir() (root / "z.pdf").write_bytes(b"z") (root / "a.pdf").write_bytes(b"a") (root / "Copie01.pdf").write_bytes(b"existing") - (root / "enonce.pdf").write_bytes(b"statement") + (root / "A.pdf").write_bytes(b"statement") rename_all(root) self.assertEqual((root / "Copie01.pdf").read_bytes(), b"a") self.assertEqual((root / "Copie02.pdf").read_bytes(), b"existing") self.assertEqual((root / "Copie03.pdf").read_bytes(), b"z") - self.assertEqual((root / "enonce.pdf").read_bytes(), b"statement") + self.assertEqual((root / "A.pdf").read_bytes(), b"statement") def test_link_falls_back_to_copy(self) -> None: with tempfile.TemporaryDirectory() as directory: