Reuse enonce groups if availabel in annotating_by_label
This commit is contained in:
+1
-6
@@ -1,7 +1,7 @@
|
|||||||
#+title: Script
|
#+title: Script
|
||||||
#+author: Sébastien Miquel
|
#+author: Sébastien Miquel
|
||||||
#+date: 14-03-2026
|
#+date: 14-03-2026
|
||||||
# Time-stamp: <22-08-26 12:05>
|
# Time-stamp: <22-08-26 12:22>
|
||||||
#+OPTIONS:
|
#+OPTIONS:
|
||||||
|
|
||||||
* Méta
|
* Méta
|
||||||
@@ -407,11 +407,6 @@ OU
|
|||||||
_Needs_ : label_groups file (made automatically by this function),
|
_Needs_ : label_groups file (made automatically by this function),
|
||||||
qui dit quelles questions regrouper.
|
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)
|
3. =python export.py Interro BGnot= (gestion perso)
|
||||||
Cela déplace les groupes dans le dossier configuré par =EXPORT_DIR=
|
Cela déplace les groupes dans le dossier configuré par =EXPORT_DIR=
|
||||||
(par défaut =Export=).
|
(par défaut =Export=).
|
||||||
|
|||||||
+72
-3
@@ -125,11 +125,80 @@ def _initial_label_groups(labels: list[str]) -> str:
|
|||||||
return "".join(",".join(items) + "\n" for items in groups.values())
|
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]]:
|
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():
|
if not label_groups.exists():
|
||||||
atomic_write_text(label_groups, _initial_label_groups(labels))
|
gemini_groups = _gemini_label_groups(workspace, labels)
|
||||||
print(f"Created {label_groups}; review the groups before continuing.")
|
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)
|
utils.edit_file_and_enter(label_groups)
|
||||||
known_labels = set(labels)
|
known_labels = set(labels)
|
||||||
groups: list[list[str]] = []
|
groups: list[list[str]] = []
|
||||||
|
|||||||
@@ -93,6 +93,14 @@ class EvaluationWorkspace:
|
|||||||
def labels_file(self) -> Path:
|
def labels_file(self) -> Path:
|
||||||
return self.root / "labels"
|
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
|
@property
|
||||||
def correction_file(self) -> Path:
|
def correction_file(self) -> Path:
|
||||||
return self.root / "correction.json"
|
return self.root / "correction.json"
|
||||||
|
|||||||
@@ -80,6 +80,10 @@ class WorkspaceTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertFalse(workspace.metadata_dir.exists())
|
self.assertFalse(workspace.metadata_dir.exists())
|
||||||
self.assertEqual(workspace.labels_file, root / "labels")
|
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.copies_dir, root / "Copies")
|
||||||
self.assertEqual(workspace.annotation_dir("grouped"), root / "BGnot")
|
self.assertEqual(workspace.annotation_dir("grouped"), root / "BGnot")
|
||||||
self.assertEqual(workspace.state_database, root / ".copienator" / "state.sqlite3")
|
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"
|
(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:
|
def test_grouped_batching_does_not_split_one_student(self) -> None:
|
||||||
module = self.modules["annotating_by_label"]
|
module = self.modules["annotating_by_label"]
|
||||||
image = Image.new("RGB", (10, 60), "white")
|
image = Image.new("RGB", (10, 60), "white")
|
||||||
|
|||||||
Reference in New Issue
Block a user