Improve web finalisation navigation, caching and Anot support

This commit is contained in:
sebastien committed 2026-11-03 11:38:39 +01:00
1 parent 5de70dbc0d
commit b069805988
21 files changed
+339 -58

No files matched your search

+11 -1
View File
@@ -1,5 +1,6 @@
import argparse
import math
import re
import shutil
import sys
from decimal import Decimal, InvalidOperation, ROUND_CEILING
@@ -37,7 +38,7 @@ def get_rounded_score(score):
"""Round score to one decimal place below (floor)."""
try:
val = float(score)
return math.floor(val * 10) / 10
return math.floor(val * 10) / 10 if math.isfinite(val) else None
except (ValueError, TypeError):
return None
@@ -130,6 +131,7 @@ def process_images(base_dir, output_dir, *, copy_merged_histogramme=False):
print(f"Error: Directory '{search_path}' not found.")
sys.exit(1)
exported_marks = {}
for student_source in sorted(path for path in search_path.iterdir() if path.is_dir()):
image_paths = sorted(student_source.glob("*.jpg"))
pdf_paths = sorted(student_source.glob("*.pdf"))
@@ -159,6 +161,7 @@ def process_images(base_dir, output_dir, *, copy_merged_histogramme=False):
if score is None:
print(f"Error: Invalid score '{raw_score}' for '{student_name}'.")
else:
score_published = not image_paths
# 5. Process Images
for img_path in image_paths:
try:
@@ -180,13 +183,20 @@ def process_images(base_dir, output_dir, *, copy_merged_histogramme=False):
draw.text((x, y), text, fill=(255, 0, 0), font=font)
img.save(student_output / img_path.name)
score_published = True
print(f"Processed: {student_name} -> {score}")
except Exception as e:
print(f"Error processing image for '{student_name}': {e}")
match = re.search(r'\((\d+)\)$', student_source.name)
if match and score_published:
exported_marks[str(int(match[1]))] = {'name': student_name, 'score': str(score)}
for pdf_path in pdf_paths:
shutil.copy2(pdf_path, student_output / pdf_path.name)
from copienator.final_scores import FINAL_SCORES_FILE
atomic_write_json(base_dir / FINAL_SCORES_FILE, {'version': 1, 'copies': exported_marks})
write_score_statistics(search_path, output_dir)
copy_histogram(output_dir, copy_merged_histogramme=copy_merged_histogramme)
+10 -1
View File
@@ -516,6 +516,7 @@ def process_student(student_id, labels_data, root_dir, all_labels, overwrite):
d_notes = dict.fromkeys(all_labels, "")
label_images = []
answer_labels = []
reviewed_layouts = {}
sorted_labels = sorted(labels_data.items(), key=lambda item: natural_key(item[0]))
for label, content in sorted_labels:
@@ -534,7 +535,7 @@ def process_student(student_id, labels_data, root_dir, all_labels, overwrite):
result = content.get('result', {})
coordinates = content.get('coordinates', (0, 0))
d_notes[label] = str(result.get('score', 0))
final_img, _ = compose_label_image(
final_img, header_height = compose_label_image(
base_img,
label,
result,
@@ -543,6 +544,8 @@ def process_student(student_id, labels_data, root_dir, all_labels, overwrite):
render_fn=render_real_latex_text,
source_pdf=pdf_full_path,
)
reviewed_layouts[label] = {'img': None, 'old_header_h': header_height,
'layout': final_img.info.get('annotation_layout', layout_for(pdf_full_path))}
final_img.save(staging / f"{label}.jpg")
if result.get('error', "") != "empty-answer":
label_images.append(final_img)
@@ -561,7 +564,13 @@ def process_student(student_id, labels_data, root_dir, all_labels, overwrite):
canvas.paste(image, (0, current_y))
current_y += image.height
canvas.save(staging / "Concat.jpg")
if not problems:
from copienator.review_snapshot import stage
stage(output_dir, staging, labels_data, reviewed_layouts, engine='grouped')
# An all-empty copy still has valid individual return images.
if not problems:
from copienator.reviewed_cache import ReviewedReadCache
ReviewedReadCache(EvaluationWorkspace(Path(root_dir)), "Anot", [output_dir]).remember([student_id])
return "partial" if problems else "success"
+1 -1
View File
@@ -8,7 +8,7 @@ from copienator import evaluation_parser
def main(argv=None):
parser = evaluation_parser('Open the local finalisation workspace')
parser.add_argument('--annotation-dir', choices=('BGnot', 'Bnot'), default='BGnot')
parser.add_argument('--annotation-dir', choices=('BGnot', 'Bnot', 'Anot'), default='BGnot')
parser.add_argument('--no-browser', action='store_true')
args = parser.parse_args(argv)
from copienator.finalisation_server import WebSession
@@ -455,7 +455,7 @@ def apply_actions_and_regenerate_grouped(
[(label, dirty_images[label]) for label in filtered_labels],
all_labels, staging / "Concat_F.pdf", general=general,
)
if annotation_dir in {"Bnot", "BGnot"}:
if annotation_dir in {"Bnot", "BGnot", "Anot"}:
review_snapshot.stage(output_dir, staging, reviewed_data, label_notes,
engine="grouped", selected=selected_labels,
force_filtered=(set(filtered_labels) - selected_labels) if selected_labels else (force_filtered or ()))
+21 -1
View File
@@ -8,6 +8,26 @@ from copienator.commands.reading_grouped_annotations import apply_actions_and_re
def run(workspace: EvaluationWorkspace, mode: str, *, incremental: bool = False):
directories = sorted((p for p in (workspace.root / mode).glob('Copie*')
if p.is_dir() and p.name[5:].isdigit() and (p / 'score.json').is_file()))
# Legacy Anot outputs predate saved rendering inputs; create them without
# replacing any existing score or annotated image.
missing = [d for d in directories if not (d / FILENAME).is_file()]
if mode == 'Anot' and missing:
from copienator.annotation_data import load_annotation_data
from copienator.annotation_layout import layout_for
from copienator.filesystem import staged_files
from copienator.review_snapshot import stage
loaded = load_annotation_data(workspace)
if loaded.warnings:
raise ValueError("Relancez la génération Anot : " + "; ".join(loaded.warnings))
for directory in missing:
cid = directory.name[5:]
if cid not in loaded.data:
raise ValueError(f"Données Anot absentes pour {directory.name}.")
data = loaded.data[cid]
notes = {label: {'img': None, 'old_header_h': 0, 'layout': layout_for(content['pdf_path'])}
for label, content in data.items()}
with staged_files(directory) as staging:
stage(directory, staging, data, notes, engine='grouped')
from copienator.reviewed_cache import ReviewedReadCache
cache = ReviewedReadCache(workspace, mode, directories) if incremental else None
if cache:
@@ -47,6 +67,6 @@ def run(workspace: EvaluationWorkspace, mode: str, *, incremental: bool = False)
def main(argv=None):
parser = evaluation_parser('Regenerate from saved reviewed inputs without rescanning handwriting')
parser.add_argument('--annotation-dir', choices=('Bnot', 'BGnot'), required=True)
parser.add_argument('--annotation-dir', choices=('Bnot', 'BGnot', 'Anot'), required=True)
parser.add_argument("--incremental", action="store_true", help="Skip unchanged copies")
return execute(parser, argv, lambda args: run(workspace_from_args(args), args.annotation_dir, incremental=args.incremental))