Initial commit for finalisation web interface
This commit is contained in:
1 parent
b295f871a7
commit
c36642e8de
33 files changed
+2745
-138
No files matched your search
@@ -155,6 +155,9 @@ def build_cleanup_plan(workspace: EvaluationWorkspace) -> CleanupPlan:
|
||||
|
||||
kept_files = set(processed_copies)
|
||||
kept_files.add(workspace.correction_file)
|
||||
lock_file = workspace.root / ".copienator-finalisation.lock"
|
||||
if lock_file in files:
|
||||
kept_files.add(lock_file)
|
||||
kept_files.update(_return_artifacts(workspace))
|
||||
kept_files.update(
|
||||
path
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Optional browser workflow; all existing finalisation commands stay available."""
|
||||
from pathlib import Path
|
||||
import time
|
||||
import webbrowser
|
||||
|
||||
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('--no-browser', action='store_true')
|
||||
args = parser.parse_args(argv)
|
||||
from copienator.finalisation_server import WebSession
|
||||
session = None
|
||||
try:
|
||||
session = WebSession(Path(args.evaluation).resolve(), args.annotation_dir)
|
||||
print(session.url, flush=True)
|
||||
if not args.no_browser:
|
||||
webbrowser.open(session.url)
|
||||
while session.alive:
|
||||
time.sleep(.2)
|
||||
except KeyboardInterrupt:
|
||||
if session:
|
||||
print('Fermeture après la fin du traitement en cours…', flush=True)
|
||||
while session.service.job['running']:
|
||||
time.sleep(.2)
|
||||
session.request_stop()
|
||||
session.thread.join()
|
||||
except Exception as exc:
|
||||
print(f'Erreur : {exc}', flush=True)
|
||||
return 1
|
||||
return 0
|
||||
@@ -18,6 +18,7 @@ from copienator import (
|
||||
)
|
||||
from copienator.platform import replace_with_link_or_copy, safe_filename
|
||||
from copienator.return_answers import publish_answer_returns
|
||||
from copienator.score_editing import remember_return_source
|
||||
|
||||
ANNOTATION_CHOICES = ("BGnot", "Bnot", "Anot")
|
||||
|
||||
@@ -119,7 +120,9 @@ def update_named_return_answers(
|
||||
destination,
|
||||
answers_only=True,
|
||||
)
|
||||
remember_return_source(workspace.root, copy_id, source_folder)
|
||||
if refresh_named:
|
||||
replace_with_link_or_copy(source_folder / "score.json", destination / "score.json", prefer="symlink")
|
||||
stem = destination.name[:match.start()].rstrip()
|
||||
for source_name, suffix, enabled in (
|
||||
("Concat.jpg", ".jpg", configuration.RETURN_JPEG_ENABLED),
|
||||
@@ -167,6 +170,9 @@ def prepare_named_returns(
|
||||
print(f"Error processing {json_path}: {exc}", file=sys.stderr)
|
||||
had_errors = True
|
||||
|
||||
# A manually renamed folder is authoritative, keyed by stable copy ID.
|
||||
from copienator.return_identity import return_folders
|
||||
existing_returns = return_folders(workspace.root)
|
||||
assigned_names: set[str] = set()
|
||||
for name, copy_ids in copies_map.items():
|
||||
if name == "Unknown":
|
||||
@@ -190,17 +196,19 @@ def prepare_named_returns(
|
||||
continue
|
||||
|
||||
assigned_names.add(name)
|
||||
destination = workspace.return_dir / f"{safe_name} ({copy_id})"
|
||||
destination = existing_returns.get(int(copy_id), workspace.return_dir / f"{safe_name} ({copy_id})")
|
||||
output_name = destination.name.rsplit(" (", 1)[0]
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
publish_answer_returns(workspace.root, source_folder, destination)
|
||||
remember_return_source(workspace.root, copy_id, source_folder)
|
||||
except (OSError, TypeError, ValueError) as exc:
|
||||
print(f"Error preparing answers for {destination.name}: {exc}", file=sys.stderr)
|
||||
had_errors = True
|
||||
continue
|
||||
links = (
|
||||
("Concat.jpg", f"{safe_name}.jpg", configuration.RETURN_JPEG_ENABLED),
|
||||
("Concat_F.pdf", f"{safe_name}.pdf", configuration.RETURN_PDF_ENABLED),
|
||||
("Concat.jpg", f"{output_name}.jpg", configuration.RETURN_JPEG_ENABLED),
|
||||
("Concat_F.pdf", f"{output_name}.pdf", configuration.RETURN_PDF_ENABLED),
|
||||
("score.json", "score.json", True),
|
||||
)
|
||||
for source_name, destination_name, enabled in links:
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from copienator import review_snapshot
|
||||
|
||||
import argparse
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
@@ -160,6 +163,7 @@ def apply_actions_and_regenerate(
|
||||
*,
|
||||
update_score: bool = False,
|
||||
label_notes: dict[str, Image.Image] | None = None,
|
||||
review_metadata: dict | None = None,
|
||||
) -> ExitCode:
|
||||
"""Apply annotations and atomically merge the regenerated student files."""
|
||||
output_dir = workspace.annotation_dir("checks") / f"Copie{student_id}"
|
||||
@@ -168,15 +172,17 @@ def apply_actions_and_regenerate(
|
||||
content["result"].get("error") == "empty-answer"
|
||||
for content in data[student_id].values()
|
||||
)
|
||||
if not bnote_path.is_file() and not only_empty:
|
||||
if review_metadata is None and not bnote_path.is_file() and not only_empty:
|
||||
print(f" Missing {bnote_path}")
|
||||
return ExitCode.PARTIAL
|
||||
bnote_data = read_json(bnote_path) if bnote_path.is_file() else {"images": []}
|
||||
bnote_data = review_metadata if review_metadata is not None else (read_json(bnote_path) if bnote_path.is_file() else {"images": []})
|
||||
if not isinstance(bnote_data, dict):
|
||||
raise TypeError(f"Expected a JSON object in {bnote_path}")
|
||||
|
||||
labels_data = data[student_id]
|
||||
apply_checkbox_actions(labels_data, actions, print)
|
||||
reviewed_data = deepcopy(labels_data)
|
||||
captured_notes = {}
|
||||
apply_custom_remarks(workspace.root, student_id, labels_data)
|
||||
score_path = output_dir / "score.json"
|
||||
preserve_score_file = update_score and score_path.is_file()
|
||||
@@ -217,6 +223,8 @@ def apply_actions_and_regenerate(
|
||||
hmin = int(image_info.get("hmin", 0))
|
||||
hmax = int(image_info.get("hmax", 0))
|
||||
sub_note = notes_layer.crop((0, hmin, notes_layer.width, hmax))
|
||||
if sub_note is not None:
|
||||
captured_notes[label] = {"img": sub_note}
|
||||
has_notes = has_significant_notes(sub_note)
|
||||
|
||||
pdf_path = Path(content["pdf_path"])
|
||||
@@ -288,6 +296,7 @@ def apply_actions_and_regenerate(
|
||||
workspace.root, [(label, dirty_images[label]) for label in filtered_labels],
|
||||
all_labels, staging / "Concat_F.pdf", general=general,
|
||||
)
|
||||
review_snapshot.stage(output_dir, staging, reviewed_data, captured_notes, engine="standard", bnote=bnote_data)
|
||||
stage_baseline(workspace.root, "Bnot", output_dir, staging, dirty_images,
|
||||
header_heights, build_answer_info(scores, labels_data, answer_labels),
|
||||
filtered_labels, scores)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from copienator import review_snapshot
|
||||
|
||||
import argparse
|
||||
import concurrent.futures
|
||||
import unicodedata
|
||||
@@ -231,12 +234,14 @@ def apply_actions_and_regenerate_grouped(
|
||||
update_score: bool = False,
|
||||
annotation_dir: str = "BGnot",
|
||||
selected_labels: set[str] | None = None,
|
||||
force_filtered: set[str] | None = None,
|
||||
) -> tuple[ExitCode, str]:
|
||||
"""Regenerate a copy, preserving reviewed images outside the redo selection."""
|
||||
logs = [f"\nProcessing compilation for: Copie{student_id}"]
|
||||
output_dir = workspace.root / annotation_dir / f"Copie{student_id}"
|
||||
labels_data = data.get(student_id, {})
|
||||
apply_checkbox_actions(labels_data, actions, logs.append)
|
||||
reviewed_data = deepcopy(labels_data)
|
||||
apply_custom_remarks(workspace.root, student_id, labels_data)
|
||||
score_path = output_dir / "score.json"
|
||||
preserve_score_file = update_score and score_path.is_file()
|
||||
@@ -400,7 +405,7 @@ def apply_actions_and_regenerate_grouped(
|
||||
perfect = float(scores[label]) >= 4.0 and all(
|
||||
feedback.get("to_delete", False) for feedback in feedbacks
|
||||
)
|
||||
if not perfect or has_notes:
|
||||
if not perfect or has_notes or label in (force_filtered or set()):
|
||||
filtered_labels.append(label)
|
||||
extras = (
|
||||
get_extra_pdfs_as_images(workspace.root, label, annotating, all_labels,
|
||||
@@ -450,6 +455,10 @@ 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"}:
|
||||
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 ()))
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Re-render saved handwriting interpretation with current scores and remarks."""
|
||||
from copienator import EvaluationWorkspace, ExitCode, evaluation_parser, execute, workspace_from_args, utils
|
||||
from copienator.review_snapshot import load, FILENAME
|
||||
from copienator.commands.reading_annotations import apply_actions_and_regenerate
|
||||
from copienator.commands.reading_grouped_annotations import apply_actions_and_regenerate_grouped
|
||||
|
||||
|
||||
def run(workspace: EvaluationWorkspace, mode: str):
|
||||
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()))
|
||||
# Validate every snapshot before starting to publish any copy.
|
||||
for directory in directories:
|
||||
if not (directory / FILENAME).is_file():
|
||||
raise ValueError(f'{directory.name} : résultat de lecture sauvegardé absent ; relisez les annotations manuscrites.')
|
||||
load(workspace.root, directory)
|
||||
status = ExitCode.SUCCESS
|
||||
for directory in directories:
|
||||
cid = directory.name[5:]
|
||||
saved, data, notes = load(workspace.root, directory)
|
||||
if saved['engine'] == 'standard':
|
||||
result = apply_actions_and_regenerate(workspace, {cid: data}, cid, [], None,
|
||||
utils.read_all_labels(workspace.root), update_score=True,
|
||||
label_notes={label: note['img'] for label, note in notes.items()},
|
||||
review_metadata=saved['bnote'])
|
||||
else:
|
||||
result, log = apply_actions_and_regenerate_grouped(workspace, {cid: data}, cid, [], notes,
|
||||
utils.read_all_labels(workspace.root), update_score=True, annotation_dir=mode,
|
||||
force_filtered=set(saved.get('force_filtered', [])))
|
||||
print(log, flush=True)
|
||||
if result != ExitCode.SUCCESS:
|
||||
status = ExitCode.PARTIAL
|
||||
return status
|
||||
|
||||
|
||||
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)
|
||||
return execute(parser, argv, lambda args: run(workspace_from_args(args), args.annotation_dir))
|
||||
Reference in new issue
Block a user