Support for A.pdf, A.tex and Ac.tex for initial files

This commit is contained in:
sebastien committed 2026-09-26 11:22:09 +02:00
1 parent 42a5b9778e
commit e4e25eca55
12 files changed
+104 -32

No files matched your search

+4 -1
View File
@@ -109,7 +109,10 @@ def _return_artifacts(workspace: EvaluationWorkspace) -> list[Path]:
def _is_statement_text_file(workspace: EvaluationWorkspace, path: Path) -> bool: def _is_statement_text_file(workspace: EvaluationWorkspace, path: Path) -> bool:
relative = path.relative_to(workspace.root) relative = path.relative_to(workspace.root)
if len(relative.parts) == 1: 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] top_level = relative.parts[0]
if top_level in STATEMENT_TEXT_DIRECTORIES: if top_level in STATEMENT_TEXT_DIRECTORIES:
return path.suffix.casefold() in {"", ".json", ".tex", ".txt"} return path.suffix.casefold() in {"", ".json", ".tex", ".txt"}
+1 -2
View File
@@ -24,7 +24,7 @@ def copy_pdfs(directory: Path) -> list[Path]:
( (
path path
for path in directory.glob("*.pdf") 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(), key=lambda path: path.name.casefold(),
) )
@@ -134,4 +134,3 @@ def main(argv: Sequence[str] | None = None) -> int:
if __name__ == "__main__": if __name__ == "__main__":
raise SystemExit(main()) raise SystemExit(main())
+1 -1
View File
@@ -143,7 +143,7 @@ def save_split_content(text, path, base_fname, problem):
def process_directory(workspace: EvaluationWorkspace) -> ExitCode: def process_directory(workspace: EvaluationWorkspace) -> ExitCode:
directory = str(workspace.root) directory = str(workspace.root)
# Find the first .tex file in the directory # 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"))) tex_files = [str(enonce)] if enonce.is_file() else sorted(glob.glob(os.path.join(directory, "*.tex")))
if not tex_files: if not tex_files:
print(f"No .tex file found in {directory}. Looking in /Staging/Interro/") print(f"No .tex file found in {directory}. Looking in /Staging/Interro/")
+7 -3
View File
@@ -331,14 +331,18 @@ def process_exam(
cache_c_file = cache_dir / "gemini_context.json" cache_c_file = cache_dir / "gemini_context.json"
# 1. Resolve files # 1. Resolve files
pdf_path = folder / "enonce.pdf" pdf_path = workspace.statement_pdf_file
enonce_path = find_file(folder, "enonce") enonce_path = find_file(folder, "enonce")
if enonce_path is None:
enonce_path = workspace.statement_source_file
correction_path = find_file(folder, "correction") correction_path = find_file(folder, "correction")
if correction_path is None:
correction_path = workspace.correction_source_file
missing = [] missing = []
if not pdf_path.is_file(): missing.append("enonce.pdf") if not pdf_path.is_file(): missing.append("enonce.pdf")
if not enonce_path: missing.append("enonce.org or enonce.tex") if not enonce_path.is_file(): missing.append("enonce.tex")
if not correction_path: missing.append("correction.org or correction.tex") if not correction_path.is_file(): missing.append("correction.tex")
if missing: if missing:
raise CliError( raise CliError(
+3 -9
View File
@@ -476,17 +476,11 @@ class ImageViewer:
def on_open_interro(self, event): def on_open_interro(self, event):
if self.is_viewing and self.current_json_path: if self.is_viewing and self.current_json_path:
# Check local directory first # Check local directory first
local_accent = self.base_dir / "énoncé.pdf" pdf_path = EvaluationWorkspace(self.base_dir).statement_pdf_file
local_plain = self.base_dir / "enonce.pdf" if not pdf_path.is_file():
if local_accent.exists():
pdf_path = str(local_accent)
elif local_plain.exists():
pdf_path = local_plain
else:
messagebox.showerror( messagebox.showerror(
"PDF not found", "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 return
+40 -3
View File
@@ -63,12 +63,49 @@ class EvaluationWorkspace:
".copienator", ".copienator",
".copienator-gui.json", ".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 @property
def name(self) -> str: def name(self) -> str:
return self.root.name 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 @property
def metadata_dir(self) -> Path: def metadata_dir(self) -> Path:
return self.root / ".copienator" return self.root / ".copienator"
@@ -220,7 +257,7 @@ class EvaluationWorkspace:
missing = [ missing = [
relative_path relative_path
for relative_path in relative_paths for relative_path in relative_paths
if not (self.root / relative_path).exists() if not self.resolve(relative_path).exists()
] ]
if missing: if missing:
raise WorkspaceValidationError(self.root, missing) raise WorkspaceValidationError(self.root, missing)
@@ -229,7 +266,7 @@ class EvaluationWorkspace:
missing = [ missing = [
relative_path relative_path
for relative_path in relative_paths 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: if missing:
raise WorkspaceValidationError(self.root, missing) raise WorkspaceValidationError(self.root, missing)
+5 -3
View File
@@ -173,7 +173,7 @@ def copy_pdf_paths(evaluation: Path) -> list[Path]:
( (
path path
for path in location.glob("*.pdf") 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(), key=lambda path: path.name.casefold(),
) )
@@ -578,7 +578,8 @@ class CopienatorApp(tk.Tk):
if self.state_store.step(step.id).get("status") == "running": if self.state_store.step(step.id).get("status") == "running":
self.state_store.update_step(step.id, status="interrupted") self.state_store.update_step(step.id, status="interrupted")
self._populate_tree() 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() names_found = (evaluation / "names").exists() or (self.repository / "names").exists()
details = [] details = []
if missing: if missing:
@@ -678,7 +679,8 @@ class CopienatorApp(tk.Tk):
if any(char in pattern for char in "*?["): if any(char in pattern for char in "*?["):
exists = next(evaluation.glob(pattern), None) is not None exists = next(evaluation.glob(pattern), None) is not None
else: 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() exists = path.exists()
if not exists: if not exists:
missing.append(pattern) missing.append(pattern)
+2 -1
View File
@@ -126,7 +126,8 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
"inputs", "inputs",
"Préparation", "Préparation",
"Vérifier les fichiers d’entrée", "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.", "Le fichier names doit être présent dans l’évaluation ou à la racine du projet.",
(manual("check"),), (manual("check"),),
requires=("enonce.pdf", "enonce.tex", "correction.tex"), requires=("enonce.pdf", "enonce.tex", "correction.tex"),
+2 -1
View File
@@ -28,7 +28,8 @@ 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
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 # 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 moyennement, il insiste pour mettre les labels dans l'ordre, sans considérer ce qu'il y a écrit
+2 -1
View File
@@ -8,13 +8,14 @@ fi
# Go to the directory # Go to the directory
cd "$1" || exit cd "$1" || exit
folder_name="$(basename "$PWD")"
count=1 count=1
for file in *.pdf; do for file in *.pdf; do
# Handle case where no pdfs exist # Handle case where no pdfs exist
[ -e "$file" ] || continue [ -e "$file" ] || continue
if [ "$file" = "enonce.pdf" ]; then if [ "$file" = "enonce.pdf" ] || [ "$file" = "énoncé.pdf" ] || [ "$file" = "$folder_name.pdf" ]; then
echo "Skipping: $file" echo "Skipping: $file"
continue continue
fi fi
+2 -1
View File
@@ -8,12 +8,13 @@ fi
# Try to change into the directory, exit on failure # Try to change into the directory, exit on failure
cd "$1" || { echo "Error: Cannot access directory '$1'"; exit 1; } 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" # Enable nullglob: if no pdfs exist, the loop won't run once with "*.pdf"
shopt -s nullglob shopt -s nullglob
for file in *.pdf; do 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" echo "Skipping: $file"
continue continue
fi fi
+35 -6
View File
@@ -108,6 +108,33 @@ class WorkspaceTests(unittest.TestCase):
workspace = EvaluationWorkspace.discover(artifact) workspace = EvaluationWorkspace.discover(artifact)
self.assertEqual(workspace.root, root) 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: def test_discover_and_require_report_clear_errors(self) -> None:
with tempfile.TemporaryDirectory() as directory: with tempfile.TemporaryDirectory() as directory:
root = Path(directory) root = Path(directory)
@@ -2574,8 +2601,9 @@ class WorkflowTests(unittest.TestCase):
class CrossPlatformFileTests(unittest.TestCase): class CrossPlatformFileTests(unittest.TestCase):
def test_rotate_all_skips_statement(self) -> None: def test_rotate_all_skips_statement(self) -> None:
with tempfile.TemporaryDirectory() as directory: with tempfile.TemporaryDirectory() as directory:
root = Path(directory) root = Path(directory) / "A"
for filename in ("Copie.pdf", "enonce.pdf"): root.mkdir()
for filename in ("Copie.pdf", "A.pdf"):
writer = PdfWriter() writer = PdfWriter()
writer.add_blank_page(width=100, height=200) writer.add_blank_page(width=100, height=200)
with (root / filename).open("wb") as output: with (root / filename).open("wb") as output:
@@ -2583,22 +2611,23 @@ class CrossPlatformFileTests(unittest.TestCase):
self.assertEqual(rotate_all(root), 1) self.assertEqual(rotate_all(root), 1)
self.assertEqual(PdfReader(root / "Copie.pdf").pages[0].rotation, 180) 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: def test_rename_all_is_collision_safe(self) -> None:
with tempfile.TemporaryDirectory() as directory: with tempfile.TemporaryDirectory() as directory:
root = Path(directory) root = Path(directory) / "A"
root.mkdir()
(root / "z.pdf").write_bytes(b"z") (root / "z.pdf").write_bytes(b"z")
(root / "a.pdf").write_bytes(b"a") (root / "a.pdf").write_bytes(b"a")
(root / "Copie01.pdf").write_bytes(b"existing") (root / "Copie01.pdf").write_bytes(b"existing")
(root / "enonce.pdf").write_bytes(b"statement") (root / "A.pdf").write_bytes(b"statement")
rename_all(root) rename_all(root)
self.assertEqual((root / "Copie01.pdf").read_bytes(), b"a") self.assertEqual((root / "Copie01.pdf").read_bytes(), b"a")
self.assertEqual((root / "Copie02.pdf").read_bytes(), b"existing") self.assertEqual((root / "Copie02.pdf").read_bytes(), b"existing")
self.assertEqual((root / "Copie03.pdf").read_bytes(), b"z") 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: def test_link_falls_back_to_copy(self) -> None:
with tempfile.TemporaryDirectory() as directory: with tempfile.TemporaryDirectory() as directory: