Keep empty answers in the final individual outputs.
This commit is contained in:
1 parent
b1f1e0bc86
commit
e7094d850e
8 files changed
+164
-37
No files matched your search
@@ -155,7 +155,7 @@ def output_removals(directory: Path) -> tuple[str, ...]:
|
||||
info = read_json(directory / "info.json") if (directory / "info.json").exists() else {}
|
||||
return ("Concat.jpg", "Concat_F.jpg", "Concat_F.pdf", "touched.json", "answer_labels.json",
|
||||
*(f"{label}.jpg" for label, entry in info.items()
|
||||
if entry.get("not_empty") and Path(label).name == label))
|
||||
if Path(label).name == label))
|
||||
|
||||
|
||||
def rebuild(root: Path, mode: str, directory: Path, staging: Path, meta: dict,
|
||||
@@ -169,7 +169,15 @@ def rebuild(root: Path, mode: str, directory: Path, staging: Path, meta: dict,
|
||||
|
||||
headers, bodies, changed = edited_bodies(directory, meta, instructions)
|
||||
info = deepcopy(meta["info"])
|
||||
filtered = set(meta["filtered"]) | changed
|
||||
for label, entry in info.items():
|
||||
# Retained empty scans are images, not evidence of a non-empty answer.
|
||||
entry["not_empty"] = label in bodies and (entry["not_empty"] or label in changed)
|
||||
if label in changed:
|
||||
entry["present"] = label in bodies
|
||||
entry["score"] = scores.get(label, entry["score"])
|
||||
filtered = (set(meta["filtered"]) | changed) & {
|
||||
label for label, entry in info.items() if entry["not_empty"]
|
||||
}
|
||||
all_labels = utils.read_all_labels(root)
|
||||
images, groups = [], []
|
||||
seen_contexts = set()
|
||||
@@ -187,15 +195,13 @@ def rebuild(root: Path, mode: str, directory: Path, staging: Path, meta: dict,
|
||||
fill="black", font_size=30)
|
||||
image = stack([header, body])
|
||||
image.save(staging / f"{label}.jpg")
|
||||
images.append(image)
|
||||
if info[label]["not_empty"]:
|
||||
images.append(image)
|
||||
if label in filtered:
|
||||
extras = get_extra_pdfs_as_images(root, label, annotating, all_labels,
|
||||
seen_contexts=seen_contexts) if mode == "BGnot" else []
|
||||
groups.append([*extras, image])
|
||||
for label, entry in info.items():
|
||||
entry["not_empty"] = label in bodies
|
||||
if label in changed and label in bodies:
|
||||
entry["present"] = True
|
||||
entry["touched"] = mode == "BGnot" and label in bodies and label in filtered
|
||||
entry["score"] = scores.get(label, entry["score"])
|
||||
atomic_write_json(staging / "info.json", info)
|
||||
|
||||
@@ -550,8 +550,7 @@ 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")
|
||||
elif labels_data:
|
||||
problems = True
|
||||
# An all-empty copy still has valid individual return images.
|
||||
return "partial" if problems else "success"
|
||||
|
||||
|
||||
|
||||
@@ -157,10 +157,14 @@ def apply_actions_and_regenerate(
|
||||
"""Apply annotations and atomically merge the regenerated student files."""
|
||||
output_dir = workspace.annotation_dir("checks") / f"Copie{student_id}"
|
||||
bnote_path = output_dir / "bnote.json"
|
||||
if not bnote_path.is_file():
|
||||
only_empty = bool(data[student_id]) and all(
|
||||
content["result"].get("error") == "empty-answer"
|
||||
for content in data[student_id].values()
|
||||
)
|
||||
if not bnote_path.is_file() and not only_empty:
|
||||
print(f" Missing {bnote_path}")
|
||||
return ExitCode.PARTIAL
|
||||
bnote_data = read_json(bnote_path)
|
||||
bnote_data = 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}")
|
||||
|
||||
@@ -180,7 +184,14 @@ def apply_actions_and_regenerate(
|
||||
filtered: list[Image.Image] = []
|
||||
incomplete = False
|
||||
|
||||
for image_info in bnote_data.get("images", []):
|
||||
review_images = list(bnote_data.get("images", []))
|
||||
reviewed_labels = {str(item.get("label", "")) for item in review_images if isinstance(item, dict)}
|
||||
# Empty answers are omitted from the review sheet, but still need return images.
|
||||
review_images.extend(
|
||||
{"label": label} for label, content in labels_data.items()
|
||||
if label not in reviewed_labels and content["result"].get("error") == "empty-answer"
|
||||
)
|
||||
for image_info in review_images:
|
||||
if not isinstance(image_info, dict):
|
||||
incomplete = True
|
||||
continue
|
||||
@@ -191,11 +202,10 @@ def apply_actions_and_regenerate(
|
||||
content = labels_data[label]
|
||||
result = content["result"]
|
||||
scores[label] = str(result.get("score", 0))
|
||||
if result.get("error") == "empty-answer":
|
||||
continue
|
||||
is_empty = result.get("error") == "empty-answer"
|
||||
|
||||
sub_note = None
|
||||
if notes_layer is not None:
|
||||
if notes_layer is not None and "hmin" in image_info:
|
||||
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))
|
||||
@@ -213,6 +223,7 @@ def apply_actions_and_regenerate(
|
||||
result,
|
||||
content["coordinates"][0],
|
||||
with_error=False,
|
||||
with_empty=True,
|
||||
)
|
||||
if final_image is None:
|
||||
incomplete = True
|
||||
@@ -230,6 +241,8 @@ def apply_actions_and_regenerate(
|
||||
|
||||
dirty_images[label] = final_image
|
||||
header_heights[label] = new_header_height
|
||||
if is_empty:
|
||||
continue
|
||||
answer_labels.append(label)
|
||||
concatenated.append(final_image)
|
||||
if float(scores[label]) != 4.0 or result.get("feedback", []):
|
||||
@@ -302,13 +315,15 @@ def run(
|
||||
status = ExitCode.PARTIAL if loaded.warnings else ExitCode.SUCCESS
|
||||
for student_id in sorted(loaded.data, key=utils.natural_key):
|
||||
output_dir = workspace.annotation_dir("checks") / f"Copie{student_id}"
|
||||
if not output_dir.is_dir():
|
||||
only_empty = all(content["result"].get("error") == "empty-answer"
|
||||
for content in loaded.data[student_id].values())
|
||||
if not output_dir.is_dir() and not only_empty:
|
||||
print(f"Warning: missing annotation directory {output_dir}")
|
||||
status = ExitCode.PARTIAL
|
||||
continue
|
||||
print(f"Processing annotations for: {student_id}")
|
||||
actions, notes = detect_checks_and_notes(output_dir)
|
||||
if notes is None and not actions and not update_score:
|
||||
actions, notes = ([], None) if only_empty else detect_checks_and_notes(output_dir)
|
||||
if notes is None and not actions and not update_score and not only_empty:
|
||||
print(" No readable annotation input found.")
|
||||
status = ExitCode.PARTIAL
|
||||
continue
|
||||
|
||||
@@ -292,8 +292,7 @@ def apply_actions_and_regenerate_grouped(
|
||||
result["score"] = old_scores[label]
|
||||
scores[label] = str(result.get("score", 0))
|
||||
touched[label] = False
|
||||
if result.get("error") == "empty-answer":
|
||||
continue
|
||||
is_empty = result.get("error") == "empty-answer"
|
||||
saved_image = output_dir / f"{label}.jpg"
|
||||
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):
|
||||
@@ -319,6 +318,8 @@ def apply_actions_and_regenerate_grouped(
|
||||
)
|
||||
dirty_images[label] = final_image
|
||||
scores[label] = str(old_scores.get(label, scores[label]))
|
||||
if is_empty:
|
||||
continue
|
||||
concat_images.append(final_image)
|
||||
answer_labels.append(label)
|
||||
filtered_labels.append(label)
|
||||
@@ -344,6 +345,7 @@ def apply_actions_and_regenerate_grouped(
|
||||
result,
|
||||
content["coordinates"][0],
|
||||
with_error=False,
|
||||
with_empty=True,
|
||||
)
|
||||
if final_image is None:
|
||||
incomplete = True
|
||||
@@ -368,6 +370,8 @@ 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
|
||||
if is_empty:
|
||||
continue
|
||||
answer_labels.append(label)
|
||||
concat_images.append(final_image)
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ def publish_answer_returns(
|
||||
raise ValueError(f"Expected a real answer directory: {answers_dir}")
|
||||
if configuration.RETURN_ANSWERS_ENABLED:
|
||||
options = load_return_answer_options(root)
|
||||
labels = [label for label, entry in info.items() if entry["present"] and entry["not_empty"]]
|
||||
labels = [label for label, entry in info.items() if entry["present"]]
|
||||
|
||||
# Import the rendering backend only when individual images are requested.
|
||||
from copienator.commands.annotating import make_base_image
|
||||
|
||||
Reference in new issue
Block a user