Support for post annotation label reassignement.
This commit is contained in:
1 parent
698ac4240c
commit
370f3252ef
10 files changed
+242
-36
No files matched your search
@@ -9,20 +9,26 @@ import numpy as np
|
||||
from pdf2image import convert_from_path
|
||||
from PIL import Image, ImageChops, ImageDraw, ImageFilter
|
||||
|
||||
from copienator.commands import annotating
|
||||
from copienator import configuration, utils
|
||||
from copienator import (
|
||||
EvaluationWorkspace,
|
||||
ExitCode,
|
||||
atomic_write_json,
|
||||
configuration,
|
||||
evaluation_parser,
|
||||
execute,
|
||||
read_json,
|
||||
utils,
|
||||
workspace_from_args,
|
||||
)
|
||||
from copienator.annotation_actions import apply_checkbox_actions, apply_score_overrides
|
||||
from copienator.answer_info import build_answer_info
|
||||
from copienator.annotation_data import AnnotationData, load_annotation_data
|
||||
from copienator.annotation_modifications import (
|
||||
BASELINE,
|
||||
output_removals,
|
||||
stage_baseline,
|
||||
)
|
||||
from copienator.answer_info import build_answer_info
|
||||
from copienator.commands import annotating
|
||||
from copienator.filesystem import staged_files
|
||||
from copienator.return_answers import save_return_answer_options
|
||||
|
||||
@@ -168,6 +174,8 @@ def apply_actions_and_regenerate(
|
||||
scores = dict.fromkeys(all_labels, "")
|
||||
answer_labels: list[str] = []
|
||||
dirty_images: dict[str, Image.Image] = {}
|
||||
header_heights: dict[str, int] = {}
|
||||
filtered_labels: list[str] = []
|
||||
concatenated: list[Image.Image] = []
|
||||
filtered: list[Image.Image] = []
|
||||
incomplete = False
|
||||
@@ -221,15 +229,18 @@ def apply_actions_and_regenerate(
|
||||
final_image.paste(body, (0, new_header_height), mask=body)
|
||||
|
||||
dirty_images[label] = final_image
|
||||
header_heights[label] = new_header_height
|
||||
answer_labels.append(label)
|
||||
concatenated.append(final_image)
|
||||
if float(scores[label]) != 4.0 or result.get("feedback", []):
|
||||
filtered.append(final_image)
|
||||
filtered_labels.append(label)
|
||||
|
||||
if incomplete:
|
||||
return ExitCode.PARTIAL
|
||||
concat_image = concatenate(concatenated)
|
||||
filtered_image = concatenate(filtered)
|
||||
with staged_files(output_dir, remove=("Concat.jpg", "Concat_F.jpg", "Concat_F.pdf",
|
||||
"touched.json", "answer_labels.json")) as staging:
|
||||
with staged_files(output_dir, remove=(*output_removals(output_dir), BASELINE)) as staging:
|
||||
for label, image in dirty_images.items():
|
||||
image.save(staging / f"{label}.jpg")
|
||||
if not preserve_score_file:
|
||||
@@ -241,6 +252,9 @@ def apply_actions_and_regenerate(
|
||||
concat_image.save(staging / "Concat.jpg")
|
||||
if filtered_image is not None:
|
||||
filtered_image.save(staging / "Concat_F.jpg")
|
||||
stage_baseline(workspace.root, "Bnot", output_dir, staging, dirty_images,
|
||||
header_heights, build_answer_info(scores, labels_data, answer_labels),
|
||||
filtered_labels, scores)
|
||||
|
||||
if preserve_score_file:
|
||||
print(f" Preserved existing score.json in {output_dir}")
|
||||
|
||||
@@ -9,11 +9,11 @@ from typing import Any
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from copienator import configuration
|
||||
from copienator import (
|
||||
EvaluationWorkspace,
|
||||
ExitCode,
|
||||
atomic_write_json,
|
||||
configuration,
|
||||
evaluation_parser,
|
||||
execute,
|
||||
read_json,
|
||||
@@ -21,8 +21,15 @@ from copienator import (
|
||||
workspace_from_args,
|
||||
)
|
||||
from copienator.annotation_actions import apply_checkbox_actions, apply_score_overrides
|
||||
from copienator.answer_info import build_answer_info
|
||||
from copienator.annotation_data import AnnotationData, RefaireList, load_annotation_data
|
||||
from copienator.annotation_modifications import (
|
||||
BASELINE,
|
||||
baseline,
|
||||
load_image,
|
||||
output_removals,
|
||||
stage_baseline,
|
||||
)
|
||||
from copienator.answer_info import build_answer_info
|
||||
from copienator.commands import annotating
|
||||
from copienator.commands.reading_annotations import (
|
||||
concatenate,
|
||||
@@ -247,6 +254,9 @@ def apply_actions_and_regenerate_grouped(
|
||||
touched = dict.fromkeys(all_labels, False)
|
||||
answer_labels: list[str] = []
|
||||
dirty_images: dict[str, Image.Image] = {}
|
||||
header_heights: dict[str, int] = {}
|
||||
filtered_labels: list[str] = []
|
||||
previous_baseline = baseline(output_dir) if (output_dir / BASELINE).is_file() else None
|
||||
concat_images: list[Image.Image] = []
|
||||
filtered_groups: list[list[Image.Image]] = []
|
||||
incomplete = False
|
||||
@@ -266,9 +276,19 @@ def apply_actions_and_regenerate_grouped(
|
||||
if result.get("error") == "empty-answer":
|
||||
continue
|
||||
saved_image = output_dir / f"{label}.jpg"
|
||||
if selected_labels and label not in selected_labels and saved_image.is_file():
|
||||
with Image.open(saved_image) as saved:
|
||||
final_image = saved.convert("RGB").copy()
|
||||
baseline_entry = (previous_baseline or {}).get("images", {}).get(label)
|
||||
if selected_labels and label not in selected_labels and (saved_image.is_file() or baseline_entry):
|
||||
if annotation_dir in {"BGnot", "Bnot"}:
|
||||
if previous_baseline is not None and label in previous_baseline["images"]:
|
||||
entry = previous_baseline["images"][label]
|
||||
final_image = load_image(output_dir, entry)
|
||||
header_heights[label] = entry["header_height"]
|
||||
else:
|
||||
with Image.open(saved_image) as saved:
|
||||
final_image = saved.convert("RGB").copy()
|
||||
else:
|
||||
with Image.open(saved_image) as saved:
|
||||
final_image = saved.convert("RGB").copy()
|
||||
if (
|
||||
simple_annotated is not None
|
||||
and label in simple_layout["images"]
|
||||
@@ -278,10 +298,11 @@ def apply_actions_and_regenerate_grouped(
|
||||
final_image = simple_annotated.crop(
|
||||
(0, hmin, simple_annotated.width, hmax)
|
||||
)
|
||||
dirty_images[label] = final_image
|
||||
dirty_images[label] = final_image
|
||||
scores[label] = str(old_scores.get(label, scores[label]))
|
||||
concat_images.append(final_image)
|
||||
answer_labels.append(label)
|
||||
filtered_labels.append(label)
|
||||
# Keep previously reviewed content, including handwriting.
|
||||
if annotation_dir == "BGnot":
|
||||
extras = get_extra_pdfs_as_images(
|
||||
@@ -327,6 +348,7 @@ def apply_actions_and_regenerate_grouped(
|
||||
|
||||
# Persist every final block, including unchanged answers, for returns.
|
||||
dirty_images[label] = final_image
|
||||
header_heights[label] = new_header_height
|
||||
answer_labels.append(label)
|
||||
concat_images.append(final_image)
|
||||
|
||||
@@ -335,6 +357,7 @@ def apply_actions_and_regenerate_grouped(
|
||||
feedback.get("to_delete", False) for feedback in feedbacks
|
||||
)
|
||||
if not perfect or has_notes:
|
||||
filtered_labels.append(label)
|
||||
extras = (
|
||||
get_extra_pdfs_as_images(workspace.root, label, annotating, all_labels)
|
||||
if annotation_dir == "BGnot"
|
||||
@@ -346,8 +369,7 @@ def apply_actions_and_regenerate_grouped(
|
||||
concat_image = concatenate(concat_images)
|
||||
if incomplete:
|
||||
return ExitCode.PARTIAL, "\n".join(logs)
|
||||
with staged_files(output_dir, remove=("Concat.jpg", "Concat_F.pdf", "Concat_F.jpg",
|
||||
"touched.json", "answer_labels.json")) as staging:
|
||||
with staged_files(output_dir, remove=(*output_removals(output_dir), BASELINE)) as staging:
|
||||
if simple_layout is not None:
|
||||
simple_layout["replaced"] = sorted(
|
||||
set(simple_layout["replaced"]) | selected_labels
|
||||
@@ -370,6 +392,9 @@ def apply_actions_and_regenerate_grouped(
|
||||
[image for group in filtered_groups for image in group]
|
||||
)
|
||||
filtered_image.save(staging / "Concat_F.jpg")
|
||||
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)
|
||||
if preserve_score_file:
|
||||
logs.append(f" Preserved existing score.json in {output_dir}")
|
||||
logs.append(f" Saved regenerated files in {output_dir}")
|
||||
|
||||
@@ -40,6 +40,7 @@ COMMANDS: dict[str, Command] = {
|
||||
"import": Command("import_annotations", "Import handwritten annotations"),
|
||||
"read-annotations": Command("reading_annotations", "Read checkable annotations"),
|
||||
"read-grouped": Command("reading_grouped_annotations", "Read grouped annotations"),
|
||||
"modify-annotations": Command("modify_annotations", "Modify reviewed answer bodies"),
|
||||
"giving-names": Command("giving_names", "Name copies and prepare A Rendre"),
|
||||
"update-ods": Command("update_ods", "Update the configured score spreadsheet"),
|
||||
"add-final-score": Command("add_final_score", "Stamp final scores on copies"),
|
||||
|
||||
@@ -92,3 +92,45 @@ def staged_files(
|
||||
if not destination_path.exists():
|
||||
saved.replace(destination_path)
|
||||
_remove_path(backup)
|
||||
|
||||
|
||||
def publish_file_sets(file_sets: dict[Path, tuple[Path, Iterable[str]]]) -> None:
|
||||
"""Commit several staged file sets together, rolling every set back on error.
|
||||
|
||||
Staging and destination directories must be on the same filesystem. Unlike
|
||||
nesting staged_files, failure in a later directory restores earlier ones too.
|
||||
"""
|
||||
token = uuid.uuid4().hex
|
||||
backups: list[tuple[Path, Path]] = []
|
||||
committed: list[Path] = []
|
||||
try:
|
||||
for target, (staging, remove) in file_sets.items():
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
backup = target / f".publish-{token}.backup"
|
||||
backup.mkdir()
|
||||
backups.append((target, backup))
|
||||
staged = sorted(path for path in staging.iterdir() if path.is_file())
|
||||
names = {path.name for path in staged} | set(remove)
|
||||
for name in sorted(names):
|
||||
if Path(name).name != name:
|
||||
raise ValueError(f"Invalid output filename: {name}")
|
||||
destination = target / name
|
||||
if destination.exists() or destination.is_symlink():
|
||||
destination.replace(backup / name)
|
||||
for source in staged:
|
||||
destination = target / source.name
|
||||
source.replace(destination)
|
||||
committed.append(destination)
|
||||
except Exception:
|
||||
for destination in reversed(committed):
|
||||
_remove_path(destination)
|
||||
for target, backup in reversed(backups):
|
||||
for saved in backup.iterdir():
|
||||
saved.replace(target / saved.name)
|
||||
raise
|
||||
finally:
|
||||
for _, backup in backups:
|
||||
if backup.exists() and not any(backup.iterdir()):
|
||||
backup.rmdir()
|
||||
for _, backup in backups:
|
||||
_remove_path(backup)
|
||||
Reference in new issue
Block a user