Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c56b655fe | ||
|
|
e4e25eca55 |
No files matched your search
@@ -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"}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|
||||||
@@ -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/")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from google import genai
|
from google import genai
|
||||||
from google.genai import types
|
from google.genai import types
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
from copienator import configuration as config
|
from copienator import configuration as config
|
||||||
from copienator import utils
|
from copienator import utils
|
||||||
@@ -22,7 +22,7 @@ from copienator import (
|
|||||||
workspace_from_args,
|
workspace_from_args,
|
||||||
)
|
)
|
||||||
from copienator.platform import validate_windows_labels
|
from copienator.platform import validate_windows_labels
|
||||||
from copienator.latex_json import repair_latex_json
|
from copienator.latex_json import repair_latex_json, normalize_literal_newlines
|
||||||
from copienator.filesystem import staged_directory
|
from copienator.filesystem import staged_directory
|
||||||
from copienator.utils import compile_to_pdf
|
from copienator.utils import compile_to_pdf
|
||||||
|
|
||||||
@@ -43,7 +43,15 @@ MODEL_ID = config.MODEL_LITE_ID
|
|||||||
api_key = config.API_KEY
|
api_key = config.API_KEY
|
||||||
|
|
||||||
# --- Modèles pour la Requête 1 ---
|
# --- Modèles pour la Requête 1 ---
|
||||||
class QuestionOnlyItem(BaseModel):
|
class StatementContent(BaseModel):
|
||||||
|
@field_validator("question_content", "solution_content", "context_content",
|
||||||
|
"rubric_content", "content", check_fields=False)
|
||||||
|
@classmethod
|
||||||
|
def normalize_newlines(cls, value: str) -> str:
|
||||||
|
return normalize_literal_newlines(value)
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionOnlyItem(StatementContent):
|
||||||
label: str = Field(description="Label unique de la question (par exemple '1.a' ou 'Exercice 1').")
|
label: str = Field(description="Label unique de la question (par exemple '1.a' ou 'Exercice 1').")
|
||||||
question_content: str = Field(description="Texte source de la question, extrait exactement du fichier d’énoncé, SANS le label lui-même.")
|
question_content: str = Field(description="Texte source de la question, extrait exactement du fichier d’énoncé, SANS le label lui-même.")
|
||||||
|
|
||||||
@@ -51,7 +59,7 @@ class ExamQuestions(BaseModel):
|
|||||||
questions: list[QuestionOnlyItem]
|
questions: list[QuestionOnlyItem]
|
||||||
|
|
||||||
# --- Modèles pour la Requête 2 ---
|
# --- Modèles pour la Requête 2 ---
|
||||||
class SolutionOnlyItem(BaseModel):
|
class SolutionOnlyItem(StatementContent):
|
||||||
label: str = Field(description="Label exact de la question fourni en entrée, à conserver sans traduction.")
|
label: str = Field(description="Label exact de la question fourni en entrée, à conserver sans traduction.")
|
||||||
solution_content: str = Field(description="Texte source de la solution, extrait exactement du fichier de correction.")
|
solution_content: str = Field(description="Texte source de la solution, extrait exactement du fichier de correction.")
|
||||||
|
|
||||||
@@ -59,7 +67,7 @@ class ExamSolutions(BaseModel):
|
|||||||
solutions: list[SolutionOnlyItem]
|
solutions: list[SolutionOnlyItem]
|
||||||
|
|
||||||
# --- Modèles pour la Requête 3 ---
|
# --- Modèles pour la Requête 3 ---
|
||||||
class ExtractedContext(BaseModel):
|
class ExtractedContext(StatementContent):
|
||||||
target_question_label: str = Field(description="Label exact de la PREMIÈRE question située immédiatement APRÈS cette information dans l’énoncé.")
|
target_question_label: str = Field(description="Label exact de la PREMIÈRE question située immédiatement APRÈS cette information dans l’énoncé.")
|
||||||
last_question_label: str = Field(description="Label exact de la DERNIÈRE question qui utilise cette information.")
|
last_question_label: str = Field(description="Label exact de la DERNIÈRE question qui utilise cette information.")
|
||||||
context_content: str = Field(description="Texte source des définitions, notations ou hypothèses, extrait de l’énoncé.")
|
context_content: str = Field(description="Texte source des définitions, notations ou hypothèses, extrait de l’énoncé.")
|
||||||
@@ -68,7 +76,7 @@ class ExamContext(BaseModel):
|
|||||||
contexts: list[ExtractedContext]
|
contexts: list[ExtractedContext]
|
||||||
|
|
||||||
# --- Modèles pour la Requête 4 (Barèmes) ---
|
# --- Modèles pour la Requête 4 (Barèmes) ---
|
||||||
class RubricItem(BaseModel):
|
class RubricItem(StatementContent):
|
||||||
label: str = Field(description="Label exact de la question, à conserver sans traduction.")
|
label: str = Field(description="Label exact de la question, à conserver sans traduction.")
|
||||||
rubric_content: str = Field(description="Barème détaillé sur 4 points, en français, avec formules LaTeX. Échapper chaque antislash LaTeX conformément à la syntaxe JSON.")
|
rubric_content: str = Field(description="Barème détaillé sur 4 points, en français, avec formules LaTeX. Échapper chaque antislash LaTeX conformément à la syntaxe JSON.")
|
||||||
|
|
||||||
@@ -110,6 +118,8 @@ Exemple d’échappement dans un barème (adapte les clés au schéma demandé)
|
|||||||
Ne confonds pas les commandes LaTeX avec les échappements JSON :
|
Ne confonds pas les commandes LaTeX avec les échappements JSON :
|
||||||
\\frac, \\rightarrow, \\not, \\theta et \\begin sont des commandes LaTeX ;
|
\\frac, \\rightarrow, \\not, \\theta et \\begin sont des commandes LaTeX ;
|
||||||
\n est réservé à un véritable saut de ligne du texte.
|
\n est réservé à un véritable saut de ligne du texte.
|
||||||
|
N'écris jamais \\n pour un saut de ligne : après décodage JSON, le texte
|
||||||
|
doit contenir un véritable saut de ligne, pas une commande LaTeX \n.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -124,12 +134,12 @@ def validate_rubric_content(content: str) -> str:
|
|||||||
return content
|
return content
|
||||||
|
|
||||||
# --- Modèle fusionné (pour le reste du script) ---
|
# --- Modèle fusionné (pour le reste du script) ---
|
||||||
class QuestionItem(BaseModel):
|
class QuestionItem(StatementContent):
|
||||||
label: str
|
label: str
|
||||||
question_content: str
|
question_content: str
|
||||||
solution_content: str
|
solution_content: str
|
||||||
|
|
||||||
class ContextItem(BaseModel):
|
class ContextItem(StatementContent):
|
||||||
target_question_label: str
|
target_question_label: str
|
||||||
last_question_label: str
|
last_question_label: str
|
||||||
content: str # Juste une string encapsulée pour le différencier facilement
|
content: str # Juste une string encapsulée pour le différencier facilement
|
||||||
@@ -331,14 +341,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(
|
||||||
@@ -911,7 +925,7 @@ def process_exam(
|
|||||||
with open(text_dir / safe_group_filename, "w", encoding="utf-8") as f_text:
|
with open(text_dir / safe_group_filename, "w", encoding="utf-8") as f_text:
|
||||||
f_text.write("\n".join(text_content_lines))
|
f_text.write("\n".join(text_content_lines))
|
||||||
|
|
||||||
print(f"Success! Processed {len(grouped_extraction.groups)} groups.")
|
print(f"Wrote {len(grouped_extraction.groups)} groups; checking PDF compilation...")
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# PDF COMPILATION (4 Threads)
|
# PDF COMPILATION (4 Threads)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
@@ -922,7 +936,7 @@ def process_exam(
|
|||||||
try:
|
try:
|
||||||
content = tex_file.read_text(encoding="utf-8")
|
content = tex_file.read_text(encoding="utf-8")
|
||||||
pdf_path = tex_file.with_suffix(".pdf")
|
pdf_path = tex_file.with_suffix(".pdf")
|
||||||
compile_to_pdf(content, pdf_path)
|
compile_to_pdf(content, pdf_path, strict=True)
|
||||||
except Exception as e: # noqa: BLE001 - compiler worker boundary
|
except Exception as e: # noqa: BLE001 - compiler worker boundary
|
||||||
return f"Error compiling {tex_file.name}: {e}"
|
return f"Error compiling {tex_file.name}: {e}"
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
+12
-6
@@ -142,7 +142,7 @@ import subprocess
|
|||||||
from .configuration import LATEX_AFTER, LATEX_BEFORE
|
from .configuration import LATEX_AFTER, LATEX_BEFORE
|
||||||
|
|
||||||
|
|
||||||
def compile_to_pdf(text, output_pdf_path):
|
def compile_to_pdf(text, output_pdf_path, *, strict=False):
|
||||||
"""Wraps text in standalone header/footer templates and compiles it to PDF."""
|
"""Wraps text in standalone header/footer templates and compiles it to PDF."""
|
||||||
latex_template = f"{LATEX_BEFORE}{text}{LATEX_AFTER}"
|
latex_template = f"{LATEX_BEFORE}{text}{LATEX_AFTER}"
|
||||||
|
|
||||||
@@ -166,6 +166,7 @@ def compile_to_pdf(text, output_pdf_path):
|
|||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.STDOUT,
|
stderr=subprocess.STDOUT,
|
||||||
text=True,
|
text=True,
|
||||||
|
encoding="utf-8", errors="replace",
|
||||||
check=False
|
check=False
|
||||||
)
|
)
|
||||||
if "minted" in text:
|
if "minted" in text:
|
||||||
@@ -175,6 +176,7 @@ def compile_to_pdf(text, output_pdf_path):
|
|||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.STDOUT,
|
stderr=subprocess.STDOUT,
|
||||||
text=True,
|
text=True,
|
||||||
|
encoding="utf-8", errors="replace",
|
||||||
check=False)
|
check=False)
|
||||||
|
|
||||||
generated_pdf = os.path.join(temp_dir, pdf_filename)
|
generated_pdf = os.path.join(temp_dir, pdf_filename)
|
||||||
@@ -183,17 +185,21 @@ def compile_to_pdf(text, output_pdf_path):
|
|||||||
shutil.move(generated_pdf, output_pdf_path)
|
shutil.move(generated_pdf, output_pdf_path)
|
||||||
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
error_lines = [
|
lines = result.stdout.splitlines()
|
||||||
line.strip() for line in result.stdout.splitlines()
|
first = next((i for i, line in enumerate(lines) if line.lstrip().startswith("!")), None)
|
||||||
if line.lstrip().startswith("!")
|
detail = "\n" + "\n".join(lines[first:first + 5]) if first is not None else ""
|
||||||
]
|
|
||||||
detail = f": {error_lines[0]}" if error_lines else ""
|
|
||||||
pdf_status = "a PDF was generated" if pdf_generated else "no PDF was generated"
|
pdf_status = "a PDF was generated" if pdf_generated else "no PDF was generated"
|
||||||
print(
|
print(
|
||||||
f"Warning: LaTeX compilation reported errors for {output_pdf_path} "
|
f"Warning: LaTeX compilation reported errors for {output_pdf_path} "
|
||||||
f"(exit code {result.returncode}; {pdf_status}){detail}"
|
f"(exit code {result.returncode}; {pdf_status}){detail}"
|
||||||
)
|
)
|
||||||
|
if strict:
|
||||||
|
raise RuntimeError(f"LaTeX failed for {output_pdf_path}{detail}")
|
||||||
elif not pdf_generated:
|
elif not pdf_generated:
|
||||||
print(f"Warning: LaTeX compilation produced no PDF for {output_pdf_path}")
|
print(f"Warning: LaTeX compilation produced no PDF for {output_pdf_path}")
|
||||||
|
if strict:
|
||||||
|
raise RuntimeError(f"No PDF generated for {output_pdf_path}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if strict:
|
||||||
|
raise
|
||||||
print(f"Compilation error for {output_pdf_path}: {e}")
|
print(f"Compilation error for {output_pdf_path}: {e}")
|
||||||
+40
-3
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ from copienator import utils
|
|||||||
|
|
||||||
|
|
||||||
class CompileToPdfTests(unittest.TestCase):
|
class CompileToPdfTests(unittest.TestCase):
|
||||||
|
def test_strict_compilation_propagates_failure_with_context(self):
|
||||||
|
result = subprocess.CompletedProcess(args=[], returncode=1,
|
||||||
|
stdout="! Undefined control sequence.\nl.13 broken command\n")
|
||||||
|
with tempfile.TemporaryDirectory() as directory, \
|
||||||
|
patch.object(utils.subprocess, "run", return_value=result) as run:
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "LaTeX failed") as error:
|
||||||
|
utils.compile_to_pdf("broken", Path(directory) / "out.pdf", strict=True)
|
||||||
|
self.assertIn("l.13 broken command", str(error.exception))
|
||||||
|
self.assertEqual(run.call_args.kwargs["errors"], "replace")
|
||||||
|
|
||||||
def test_warns_when_latex_fails_even_if_partial_pdf_exists(self):
|
def test_warns_when_latex_fails_even_if_partial_pdf_exists(self):
|
||||||
with tempfile.TemporaryDirectory() as temporary:
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
output = Path(temporary) / "result.pdf"
|
output = Path(temporary) / "result.pdf"
|
||||||
|
|||||||
Reference in new issue
Block a user