Standardisation 3
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .json_io import read_json
|
||||
from .workspace import EvaluationWorkspace
|
||||
|
||||
AnnotationData = dict[str, dict[str, dict[str, Any]]]
|
||||
RefaireList = list[list[Any]]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class GroupCoordinates:
|
||||
minimum: int
|
||||
maximum: int
|
||||
width: int
|
||||
height: int
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AnnotationLoadResult:
|
||||
data: AnnotationData
|
||||
warnings: list[str]
|
||||
|
||||
|
||||
def _coordinate_index(
|
||||
workspace: EvaluationWorkspace,
|
||||
) -> tuple[dict[tuple[str, str], GroupCoordinates], list[str]]:
|
||||
index: dict[tuple[str, str], GroupCoordinates] = {}
|
||||
warnings: list[str] = []
|
||||
if not workspace.groups_dir.is_dir():
|
||||
return index, [f"Group directory not found: {workspace.groups_dir}"]
|
||||
|
||||
for metadata_path in sorted(workspace.groups_dir.glob("*/Group_*.json")):
|
||||
image_path = metadata_path.with_suffix(".jpg")
|
||||
try:
|
||||
entries = read_json(metadata_path)
|
||||
if not isinstance(entries, list):
|
||||
raise TypeError("expected a JSON array")
|
||||
with Image.open(image_path) as image:
|
||||
width, height = image.size
|
||||
for entry in entries:
|
||||
copy_id = str(entry[0])
|
||||
minimum = int(entry[1])
|
||||
maximum = int(entry[2])
|
||||
label = str(entry[4])
|
||||
index.setdefault(
|
||||
(label, copy_id),
|
||||
GroupCoordinates(minimum, maximum, width, height),
|
||||
)
|
||||
except (IndexError, OSError, TypeError, ValueError) as exc:
|
||||
warnings.append(f"Could not read group metadata {metadata_path}: {exc}")
|
||||
return index, warnings
|
||||
|
||||
|
||||
def _scaled_result(result: dict[str, Any], coordinates: GroupCoordinates | None):
|
||||
scaled = copy.deepcopy(result)
|
||||
if coordinates is None:
|
||||
return scaled
|
||||
for feedback in scaled.get("feedback", []):
|
||||
box = feedback.get("box_2d")
|
||||
if not box or len(box) != 4:
|
||||
continue
|
||||
box[0] = int(box[0] * coordinates.height) // 1000
|
||||
box[2] = int(box[2] * coordinates.height) // 1000
|
||||
box[1] = int(box[1] * coordinates.width) // 1000
|
||||
box[3] = int(box[3] * coordinates.width) // 1000
|
||||
return scaled
|
||||
|
||||
|
||||
def _answer_pdf(
|
||||
workspace: EvaluationWorkspace,
|
||||
copy_id: str,
|
||||
label: str,
|
||||
suffix: str = "",
|
||||
) -> Path:
|
||||
copy_dir = workspace.copies_dir / f"Copie{copy_id}"
|
||||
preferred = copy_dir / f"{label}{suffix}.pdf"
|
||||
if preferred.exists():
|
||||
return preferred
|
||||
for candidate in (
|
||||
copy_dir / f"{label}.pdf",
|
||||
copy_dir / f"{label}_new.pdf",
|
||||
):
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
return preferred
|
||||
|
||||
|
||||
def _dummy_entry(
|
||||
workspace: EvaluationWorkspace,
|
||||
copy_id: str,
|
||||
label: str,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"pdf_path": _answer_pdf(workspace, copy_id, label),
|
||||
"result": {
|
||||
"score": 0.0,
|
||||
"feedback": [],
|
||||
"error": "non traité",
|
||||
},
|
||||
"coordinates": (0, 0),
|
||||
"issues": ["No correction result was available for this answer."],
|
||||
}
|
||||
|
||||
|
||||
def _apply_refaire_filter(
|
||||
workspace: EvaluationWorkspace,
|
||||
data: AnnotationData,
|
||||
refaire_list: RefaireList,
|
||||
warnings: list[str],
|
||||
) -> AnnotationData:
|
||||
filtered: AnnotationData = {}
|
||||
for raw_entry in refaire_list:
|
||||
if not isinstance(raw_entry, list) or len(raw_entry) != 2:
|
||||
warnings.append(f"Ignoring malformed refaire entry: {raw_entry!r}")
|
||||
continue
|
||||
copy_name, requested_labels = raw_entry
|
||||
copy_id = str(copy_name).removeprefix("Copie")
|
||||
available = data.get(copy_id, {})
|
||||
if not requested_labels:
|
||||
filtered[copy_id] = dict(available)
|
||||
continue
|
||||
selected: dict[str, dict[str, Any]] = {}
|
||||
for raw_label in requested_labels:
|
||||
label = str(raw_label)
|
||||
selected[label] = (
|
||||
available[label]
|
||||
if label in available
|
||||
else _dummy_entry(workspace, copy_id, label)
|
||||
)
|
||||
filtered[copy_id] = selected
|
||||
return filtered
|
||||
|
||||
|
||||
def load_annotation_data(
|
||||
workspace: EvaluationWorkspace,
|
||||
*,
|
||||
refaire_list: RefaireList | None = None,
|
||||
copy_id: str | None = None,
|
||||
) -> AnnotationLoadResult:
|
||||
workspace.require_files("correction.json")
|
||||
corrections = read_json(workspace.correction_file)
|
||||
if not isinstance(corrections, dict):
|
||||
raise TypeError("correction.json must contain a JSON object")
|
||||
|
||||
coordinate_index, warnings = _coordinate_index(workspace)
|
||||
data: AnnotationData = {}
|
||||
for label, raw_batches in corrections.items():
|
||||
if not isinstance(raw_batches, list):
|
||||
warnings.append(f"Ignoring malformed correction batches for {label!r}")
|
||||
continue
|
||||
for raw_batch in raw_batches:
|
||||
if not isinstance(raw_batch, list):
|
||||
warnings.append(f"Ignoring malformed correction batch for {label!r}")
|
||||
continue
|
||||
for item in raw_batch:
|
||||
if not isinstance(item, dict) or not isinstance(item.get("result"), dict):
|
||||
warnings.append(f"Ignoring malformed correction item for {label!r}")
|
||||
continue
|
||||
student_id = str(item.get("id", ""))
|
||||
if not student_id:
|
||||
warnings.append(f"Ignoring correction item without an id for {label!r}")
|
||||
continue
|
||||
result = item["result"]
|
||||
suffix = str(result.get("suffix", ""))
|
||||
if suffix == "_old":
|
||||
continue
|
||||
coordinates = coordinate_index.get((str(label), student_id))
|
||||
issues: list[str] = []
|
||||
if coordinates is None:
|
||||
issues.append("Group coordinates were not found.")
|
||||
pdf_path = _answer_pdf(workspace, student_id, str(label), suffix)
|
||||
if not pdf_path.exists():
|
||||
issues.append(f"Answer PDF not found: {pdf_path}")
|
||||
data.setdefault(student_id, {})[str(label)] = {
|
||||
"pdf_path": pdf_path,
|
||||
"result": _scaled_result(result, coordinates),
|
||||
"coordinates": (
|
||||
(coordinates.minimum, coordinates.maximum)
|
||||
if coordinates is not None
|
||||
else (0, 0)
|
||||
),
|
||||
"issues": issues,
|
||||
}
|
||||
warnings.extend(
|
||||
f"Copie{student_id} {label}: {issue}" for issue in issues
|
||||
)
|
||||
|
||||
if refaire_list is not None:
|
||||
data = _apply_refaire_filter(workspace, data, refaire_list, warnings)
|
||||
if copy_id is not None:
|
||||
data = {copy_id: data[copy_id]} if copy_id in data else {}
|
||||
if not data:
|
||||
warnings.append(f"Copy id {copy_id} was not found in correction.json")
|
||||
return AnnotationLoadResult(data, warnings)
|
||||
Reference in New Issue
Block a user