Reuse enonce groups if availabel in annotating_by_label

This commit is contained in:
2026-08-22 12:22:46 +02:00
parent 92a9f9883e
commit 5a7ddd407f
4 changed files with 144 additions and 9 deletions
+1 -6
View File
@@ -1,7 +1,7 @@
#+title: Script
#+author: Sébastien Miquel
#+date: 14-03-2026
# Time-stamp: <22-08-26 12:05>
# Time-stamp: <22-08-26 12:22>
#+OPTIONS:
* Méta
@@ -407,11 +407,6 @@ OU
_Needs_ : label_groups file (made automatically by this function),
qui dit quelles questions regrouper.
Dans ces trois modes, les métadonnées JSON sont écrites atomiquement.
Lors d'une régénération, les nouvelles sorties sont préparées dans un
dossier temporaire voisin. Une erreur de rendu conserve donc la sortie
précédente ; pour =BGnot --overwrite=, le dossier complet n'est remplacé
que si tous les groupes ont été produits.
3. =python export.py Interro BGnot= (gestion perso)
Cela déplace les groupes dans le dossier configuré par =EXPORT_DIR=
(par défaut =Export=).
+72 -3
View File
@@ -125,11 +125,80 @@ def _initial_label_groups(labels: list[str]) -> str:
return "".join(",".join(items) + "\n" for items in groups.values())
def _gemini_label_groups(
workspace: EvaluationWorkspace, labels: list[str]
) -> list[list[str]] | None:
source = workspace.gemini_exam_items_file
if not source.is_file():
return None
groups: list[list[str]] = []
current: list[str] = []
try:
source_lines = source.read_text(encoding="utf-8").splitlines()
except (OSError, UnicodeError) as exc:
print(f"Warning: could not read Gemini question groups from {source}: {exc}")
return None
for raw_line in source_lines:
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if line == "---":
if current:
groups.append(current)
current = []
continue
if " ### " not in line:
continue
label = line.split(" ### ", 1)[0].strip()
if label and label != "CONTEXT":
current.append(label)
if current:
groups.append(current)
flattened = [label for group in groups for label in group]
known = set(labels)
if (
not flattened
or len(flattened) != len(set(flattened))
or set(flattened) != known
):
missing = sorted(known.difference(flattened), key=natural_key)
unknown = sorted(set(flattened).difference(known), key=natural_key)
details = []
if missing:
details.append("missing: " + ", ".join(missing))
if unknown:
details.append("unknown: " + ", ".join(unknown))
if len(flattened) != len(set(flattened)):
details.append("duplicate labels")
print(
f"Warning: ignoring incompatible Gemini question groups in {source}"
+ (f" ({'; '.join(details)})" if details else "")
)
return None
return groups
def _serialize_label_groups(groups: list[list[str]]) -> str:
return "".join(",".join(group) + "\n" for group in groups)
def _load_label_groups(workspace: EvaluationWorkspace, labels: list[str]) -> list[list[str]]:
label_groups = workspace.root / "label_groups"
label_groups = workspace.label_groups_file
if not label_groups.exists():
atomic_write_text(label_groups, _initial_label_groups(labels))
print(f"Created {label_groups}; review the groups before continuing.")
gemini_groups = _gemini_label_groups(workspace, labels)
if gemini_groups is not None:
initial_content = _serialize_label_groups(gemini_groups)
source_description = "the groups selected in gemini_for_enonce.py"
else:
initial_content = _initial_label_groups(labels)
source_description = "the label-prefix fallback"
atomic_write_text(label_groups, initial_content)
print(
f"Created {label_groups} from {source_description}; "
"review the groups before continuing."
)
utils.edit_file_and_enter(label_groups)
known_labels = set(labels)
groups: list[list[str]] = []
+8
View File
@@ -93,6 +93,14 @@ class EvaluationWorkspace:
def labels_file(self) -> Path:
return self.root / "labels"
@property
def label_groups_file(self) -> Path:
return self.root / "label_groups"
@property
def gemini_exam_items_file(self) -> Path:
return self.root / "Tmp" / "exam_items.txt"
@property
def correction_file(self) -> Path:
return self.root / "correction.json"
+63
View File
@@ -80,6 +80,10 @@ class WorkspaceTests(unittest.TestCase):
self.assertFalse(workspace.metadata_dir.exists())
self.assertEqual(workspace.labels_file, root / "labels")
self.assertEqual(workspace.label_groups_file, root / "label_groups")
self.assertEqual(
workspace.gemini_exam_items_file, root / "Tmp" / "exam_items.txt"
)
self.assertEqual(workspace.copies_dir, root / "Copies")
self.assertEqual(workspace.annotation_dir("grouped"), root / "BGnot")
self.assertEqual(workspace.state_database, root / ".copienator" / "state.sqlite3")
@@ -1400,6 +1404,65 @@ class StandardCliTests(unittest.TestCase):
(previous / "sentinel.txt").read_text(encoding="utf-8"), "old"
)
def test_grouped_annotations_default_to_gemini_question_groups(self) -> None:
module = self.modules["annotating_by_label"]
with tempfile.TemporaryDirectory() as directory:
evaluation = Path(directory) / "Exam"
items = evaluation / "Tmp" / "exam_items.txt"
items.parent.mkdir(parents=True)
items.write_text(
"# edited Gemini groups\n"
"Ex 1 ### First question\n"
"CONTEXT ### Shared context\n"
"Ex 2 ### Second question\n"
"\n---\n\n"
"Ex 3 ### Third question\n",
encoding="utf-8",
)
workspace = EvaluationWorkspace(evaluation)
with patch.object(module.utils, "edit_file_and_enter") as editor:
groups = module._load_label_groups(
workspace, ["Ex 1", "Ex 2", "Ex 3"]
)
self.assertEqual(groups, [["Ex 1", "Ex 2"], ["Ex 3"]])
self.assertEqual(
workspace.label_groups_file.read_text(encoding="utf-8"),
"Ex 1,Ex 2\nEx 3\n",
)
editor.assert_called_once_with(workspace.label_groups_file)
def test_grouped_annotations_fall_back_when_gemini_was_not_run(self) -> None:
module = self.modules["annotating_by_label"]
with tempfile.TemporaryDirectory() as directory:
workspace = EvaluationWorkspace(Path(directory) / "Exam")
workspace.root.mkdir()
labels = ["Ex 1 : a", "Ex 1 : b", "Ex 2"]
with patch.object(module.utils, "edit_file_and_enter"):
groups = module._load_label_groups(workspace, labels)
self.assertEqual(groups, [["Ex 1 : a", "Ex 1 : b"], ["Ex 2"]])
def test_existing_label_groups_override_gemini_defaults(self) -> None:
module = self.modules["annotating_by_label"]
with tempfile.TemporaryDirectory() as directory:
workspace = EvaluationWorkspace(Path(directory) / "Exam")
workspace.gemini_exam_items_file.parent.mkdir(parents=True)
workspace.gemini_exam_items_file.write_text(
"Ex 1 ### First\nEx 2 ### Second\n", encoding="utf-8"
)
workspace.label_groups_file.write_text(
"Ex 1\nEx 2\n", encoding="utf-8"
)
with patch.object(module.utils, "edit_file_and_enter") as editor:
groups = module._load_label_groups(workspace, ["Ex 1", "Ex 2"])
self.assertEqual(groups, [["Ex 1"], ["Ex 2"]])
editor.assert_not_called()
def test_grouped_batching_does_not_split_one_student(self) -> None:
module = self.modules["annotating_by_label"]
image = Image.new("RGB", (10, 60), "white")