Bnot fixes ; Missing énoncés fixes.
This commit is contained in:
1 parent
f39868207d
commit
55d5cf16c9
18 files changed
+486
-44
No files matched your search
@@ -164,6 +164,7 @@ def rebuild(root: Path, mode: str, directory: Path, staging: Path, meta: dict,
|
||||
from copienator.commands import annotating
|
||||
from copienator.commands.reading_grouped_annotations import (
|
||||
get_extra_pdfs_as_images,
|
||||
save_checked_pdf,
|
||||
save_paginated_pdf,
|
||||
)
|
||||
|
||||
@@ -179,7 +180,7 @@ def rebuild(root: Path, mode: str, directory: Path, staging: Path, meta: dict,
|
||||
label for label, entry in info.items() if entry["not_empty"]
|
||||
}
|
||||
all_labels = utils.read_all_labels(root)
|
||||
images, groups = [], []
|
||||
images, groups, checked_images = [], [], []
|
||||
seen_contexts = set()
|
||||
order = sorted(bodies, key=utils.natural_key)
|
||||
for label in order:
|
||||
@@ -198,6 +199,7 @@ def rebuild(root: Path, mode: str, directory: Path, staging: Path, meta: dict,
|
||||
if info[label]["not_empty"]:
|
||||
images.append(image)
|
||||
if label in filtered:
|
||||
checked_images.append((label, image))
|
||||
extras = get_extra_pdfs_as_images(root, label, annotating, all_labels,
|
||||
seen_contexts=seen_contexts) if mode == "BGnot" else []
|
||||
groups.append([*extras, image])
|
||||
@@ -212,6 +214,7 @@ def rebuild(root: Path, mode: str, directory: Path, staging: Path, meta: dict,
|
||||
save_paginated_pdf(groups, staging / "Concat_F.pdf")
|
||||
else:
|
||||
stack([image for group in groups for image in group]).save(staging / "Concat_F.jpg")
|
||||
save_checked_pdf(root, checked_images, all_labels, staging / "Concat_F.pdf")
|
||||
|
||||
|
||||
def stage_baseline(root: Path, mode: str, output: Path, staging: Path,
|
||||
|
||||
@@ -59,16 +59,20 @@ def copy_return_artifacts(source_dir: Path, destination_dir: Path) -> None:
|
||||
shutil.copytree(answers_source, answers_destination)
|
||||
|
||||
|
||||
def copy_histogram(output_dir: Path) -> None:
|
||||
def copy_histogram(output_dir: Path, *, copy_merged_histogramme: bool = False) -> None:
|
||||
"""Copy the score histogram beside the per-student output folders."""
|
||||
if not HISTOGRAM_PATH.is_file():
|
||||
print(f"Warning: Missing histogram '{HISTOGRAM_PATH}'.")
|
||||
return
|
||||
destination = output_dir / "histogramme.pdf"
|
||||
shutil.copy2(HISTOGRAM_PATH, destination)
|
||||
print(f"Copied histogram: {destination}")
|
||||
sources = [(HISTOGRAM_PATH, "histogramme.pdf")]
|
||||
if copy_merged_histogramme:
|
||||
sources.append((HISTOGRAM_PATH.with_name("merged_histogramme.pdf"), "merged_histogramme.pdf"))
|
||||
for source, filename in sources:
|
||||
if not source.is_file():
|
||||
print(f"Warning: Missing histogram '{source}'.")
|
||||
continue
|
||||
destination = output_dir / filename
|
||||
shutil.copy2(source, destination)
|
||||
print(f"Copied histogram: {destination}")
|
||||
|
||||
def process_images(base_dir, output_dir):
|
||||
def process_images(base_dir, output_dir, *, copy_merged_histogramme=False):
|
||||
# 1. Load Data
|
||||
try:
|
||||
# header=None assumes the file starts directly with data.
|
||||
@@ -148,18 +152,20 @@ def process_images(base_dir, output_dir):
|
||||
for pdf_path in pdf_paths:
|
||||
shutil.copy2(pdf_path, student_output / pdf_path.name)
|
||||
|
||||
copy_histogram(output_dir)
|
||||
copy_histogram(output_dir, copy_merged_histogramme=copy_merged_histogramme)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description="Stamp scores on exam copies.")
|
||||
parser.add_argument("dir", type=Path, help="Root directory containing 'A Rendre' folder")
|
||||
parser.add_argument("--copy-merged-histogramme", action="store_true",
|
||||
help="Also copy merged_histogramme.pdf from the histogram staging directory")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
base_dir = args.dir.expanduser().resolve()
|
||||
output_dir = OUTPUT_DIR / base_dir.name
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
process_images(base_dir, output_dir)
|
||||
process_images(base_dir, output_dir, copy_merged_histogramme=args.copy_merged_histogramme)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ from copienator import (
|
||||
workspace_from_args,
|
||||
)
|
||||
from copienator.platform import WindowsLabelError, validate_windows_labels
|
||||
from copienator.utils import compile_to_pdf
|
||||
from copienator.utils import compile_to_pdf, warn_missing_exam_material
|
||||
|
||||
|
||||
def fetch_and_save_sub_text(ex_id, indices, label, text_path):
|
||||
@@ -327,6 +327,7 @@ def process_directory(workspace: EvaluationWorkspace) -> ExitCode:
|
||||
labels_staging.replace(labels_file)
|
||||
atomic_write_text(workspace.label_groups_file,
|
||||
"".join(", ".join(group) + "\n" for group in exercise_groups))
|
||||
warn_missing_exam_material(workspace)
|
||||
return ExitCode.PARTIAL if had_errors else ExitCode.SUCCESS
|
||||
|
||||
|
||||
|
||||
@@ -1024,6 +1024,7 @@ def write_exam_outputs(workspace: EvaluationWorkspace, grouped_extraction: Group
|
||||
|
||||
print(f"Wrote {len(grouped_extraction.groups)} groups; checking PDF compilation...")
|
||||
compilation_status = compile_existing_pdfs(workspace)
|
||||
utils.warn_missing_exam_material(workspace)
|
||||
return ExitCode.PARTIAL if processing_errors or compilation_status != ExitCode.SUCCESS else ExitCode.SUCCESS
|
||||
|
||||
|
||||
@@ -1093,7 +1094,10 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
if args.restart and (args.compile_only or args.groups_only or args.persp_only):
|
||||
raise CliError('--restart cannot be combined with a partial action', ExitCode.INVALID_ARGUMENTS)
|
||||
if args.compile_only:
|
||||
return compile_existing_pdfs(workspace_from_args(args))
|
||||
workspace = workspace_from_args(args)
|
||||
status = compile_existing_pdfs(workspace)
|
||||
utils.warn_missing_exam_material(workspace)
|
||||
return status
|
||||
if args.groups_only or args.persp_only:
|
||||
kwargs = {'delete_persp': True} if args.delete_persp else {}
|
||||
return refine_existing(workspace_from_args(args),
|
||||
|
||||
@@ -270,6 +270,12 @@ def apply_actions_and_regenerate(
|
||||
concat_image.save(staging / "Concat.jpg")
|
||||
if filtered_image is not None:
|
||||
filtered_image.save(staging / "Concat_F.jpg")
|
||||
# Imported here because the grouped reader also uses this module.
|
||||
from copienator.commands.reading_grouped_annotations import save_checked_pdf
|
||||
save_checked_pdf(
|
||||
workspace.root, list(zip(filtered_labels, filtered, strict=True)),
|
||||
all_labels, staging / "Concat_F.pdf",
|
||||
)
|
||||
stage_baseline(workspace.root, "Bnot", output_dir, staging, dirty_images,
|
||||
header_heights, build_answer_info(scores, labels_data, answer_labels),
|
||||
filtered_labels, scores)
|
||||
|
||||
@@ -147,6 +147,18 @@ def save_paginated_pdf(
|
||||
)
|
||||
|
||||
|
||||
def save_checked_pdf(root, labelled_images, all_labels, output_path):
|
||||
"""Build the checked-mode return PDF with the same supplements as BGnot."""
|
||||
groups = []
|
||||
seen_contexts = set()
|
||||
for label, image in labelled_images:
|
||||
extras = get_extra_pdfs_as_images(
|
||||
root, label, annotating, all_labels, seen_contexts=seen_contexts
|
||||
)
|
||||
groups.append([*extras, image])
|
||||
save_paginated_pdf(groups, output_path)
|
||||
|
||||
|
||||
def _scan_annotation_directory(
|
||||
directory: Path,
|
||||
only_ids: set[str] | None = None,
|
||||
@@ -420,6 +432,12 @@ def apply_actions_and_regenerate_grouped(
|
||||
[image for group in filtered_groups for image in group]
|
||||
)
|
||||
filtered_image.save(staging / "Concat_F.jpg")
|
||||
if annotation_dir == "Bnot":
|
||||
save_checked_pdf(
|
||||
workspace.root,
|
||||
[(label, dirty_images[label]) for label in filtered_labels],
|
||||
all_labels, staging / "Concat_F.pdf",
|
||||
)
|
||||
stage_baseline(workspace.root, annotation_dir, output_dir, staging, dirty_images,
|
||||
header_heights, build_answer_info(scores, labels_data, answer_labels, touched),
|
||||
filtered_labels, scores)
|
||||
|
||||
+52
-14
@@ -93,10 +93,7 @@ def pdf_images_of_contexts(root_dir, label, all_labels):
|
||||
|
||||
|
||||
def get_exam_file_content(folder_path, mode, label):
|
||||
"""
|
||||
Retrieves content from the Text or Sol directory for a specific label.
|
||||
Checks for exact filename matches or grouped 'prefix[a,b,c]' filenames.
|
||||
"""
|
||||
"""Use the most specific exact, grouped, or enclosing section available."""
|
||||
target_dir = Path(folder_path) / mode
|
||||
if not target_dir.is_dir():
|
||||
return None
|
||||
@@ -109,23 +106,37 @@ def get_exam_file_content(folder_path, mode, label):
|
||||
if direct_file.is_file():
|
||||
return direct_file.read_text(encoding="utf-8")
|
||||
|
||||
# 2. Search for grouped files: prefix[suffix1,suffix2,...]
|
||||
for file_path in target_dir.iterdir():
|
||||
candidates = []
|
||||
for file_path in sorted(target_dir.iterdir()):
|
||||
if not file_path.is_file():
|
||||
continue
|
||||
name = file_path.name
|
||||
if file_path.suffix in {".pdf", ".tex"}:
|
||||
continue
|
||||
covered_labels = [name]
|
||||
if "[" in name and name.endswith("]"):
|
||||
# Split 'prefix[suffixes]' -> 'prefix', 'suffixes'
|
||||
prefix, rest = name.split("[", 1)
|
||||
suffixes = rest[:-1].split(",") # Remove trailing ']' and split
|
||||
|
||||
# Check if any reconstructed label matches
|
||||
for s in suffixes:
|
||||
if (prefix + s) == safe_label:
|
||||
return file_path.read_text(encoding="utf-8")
|
||||
|
||||
covered_labels = [prefix + suffix.strip() for suffix in rest[:-1].split(",")]
|
||||
for covered in covered_labels:
|
||||
if covered == safe_label or _is_parent_label(covered, safe_label):
|
||||
candidates.append((len(covered), name == covered, file_path))
|
||||
if candidates:
|
||||
# Prefer a narrower section, then its direct file over a grouped file.
|
||||
candidates.sort(key=lambda item: (-item[0], -item[1], item[2].name))
|
||||
return candidates[0][2].read_text(encoding="utf-8")
|
||||
return None
|
||||
|
||||
|
||||
def _is_parent_label(parent, label):
|
||||
if not parent or not label.startswith(parent):
|
||||
return False
|
||||
suffix = label[len(parent):]
|
||||
# Respect component boundaries: Ex 1 must not match Ex 10, nor 1) match 10).
|
||||
return bool(
|
||||
suffix.startswith((" ", ":", "."))
|
||||
or (parent.endswith(")") and re.match(r"[A-Za-z0-9]+\)", suffix))
|
||||
)
|
||||
|
||||
def get_label_text_content(folder, label):
|
||||
return get_exam_file_content(folder, "Text", label)
|
||||
|
||||
@@ -135,6 +146,33 @@ def get_label_sol_content(folder, label):
|
||||
def get_label_persp_content(folder, label):
|
||||
return get_exam_file_content(folder, "Persp", label)
|
||||
|
||||
|
||||
def warn_missing_exam_material(workspace):
|
||||
"""Report prompt coverage after statement preparation without blocking work."""
|
||||
if not workspace.labels_file.is_file():
|
||||
print("[Vérification des prompts] Avertissement : fichier labels absent ; vérification impossible.")
|
||||
return
|
||||
labels = workspace.read_labels()
|
||||
missing_count = 0
|
||||
for label in labels:
|
||||
missing = []
|
||||
for mode, title in (("Text", "énoncé"), ("Sol", "corrigé")):
|
||||
try:
|
||||
content = get_exam_file_content(workspace.root, mode, label)
|
||||
if not content or not content.strip() or content.strip().rstrip(" :") == label:
|
||||
missing.append(f"{title} absent ou vide ({mode})")
|
||||
except (OSError, UnicodeError) as exc:
|
||||
missing.append(f"{title} illisible ({mode}) : {exc}")
|
||||
if missing:
|
||||
missing_count += 1
|
||||
print(f"[Vérification des prompts] Avertissement — {label} : {' ; '.join(missing)}")
|
||||
print(
|
||||
f"[Vérification des prompts] {len(labels) - missing_count}/{len(labels)} labels "
|
||||
"avec énoncé et corrigé non vides."
|
||||
)
|
||||
if missing_count:
|
||||
print("[Vérification des prompts] Vous pouvez continuer ; les corrections des labels signalés utiliseront des documents incomplets.")
|
||||
|
||||
import tempfile
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
Reference in new issue
Block a user