dismissed wrong label triggers recorrection

This commit is contained in:
sebastien committed 2026-10-23 08:51:28 +02:00
1 parent 1959c5d3bb
commit f39868207d
5 files changed
+198 -14

No files matched your search

+38 -11
View File
@@ -392,7 +392,7 @@ def reserve_next_group_idx(label: str) -> int:
reserved_group_indices[label] = idx + 1
return idx
def handle_label_errors(pid, label, res, pdf_path):
def handle_label_errors(pid, label, res, pdf_path, *, group_geometry=None):
"""Handles Gemini labeling errors, moves/copies files, and returns new tasks."""
new_tasks = []
error_type = res.get("error")
@@ -410,7 +410,33 @@ def handle_label_errors(pid, label, res, pdf_path):
res["error"] = "wrg-lbl:cldtfix"
return []
if new_label == label:
res["error"] = ""
tprint(f"\t\tLabel confirmed for Copie{pid}: regrading {label}.")
contents, config = prompting.request_for_confirmed_label(INPUT_DIR, pdf_path, label, pid)
response = parse_correction_response(
call_gemini_with_retries(MODEL_ID_flash, contents, config)
)
if not isinstance(response, list) or len(response) != 1 or response[0].get("id") != pid:
raise ValueError(f"Confirmed-label regrading must return exactly Copie{pid}")
replacement = prompting.EvaluationEntry.model_validate(response[0]).result.model_dump()
if replacement["error"] == "wrong-label":
raise ValueError(f"Regrading rejected the confirmed label {label} for Copie{pid}")
if group_geometry is not None:
yming, ymaxg, width_r, total_height = group_geometry
for feedback in replacement["feedback"]:
box = feedback.get("box_2d")
if box and valid_feedback_box(box):
ymin, xmin, ymax, xmax = box
feedback["box_2d"] = [
(yming + ymin * (ymaxg - yming) / 1000) * 1000 / total_height,
xmin * width_r,
(yming + ymax * (ymaxg - yming) / 1000) * 1000 / total_height,
xmax * width_r,
]
# Keep the original response intact until regrading has succeeded.
res.clear()
res.update(replacement)
if res["error"] == "additional-answer":
return handle_label_errors(pid, label, res, pdf_path, group_geometry=group_geometry)
return []
base_new_pdf_path = COPIES_DIR / f"Copie{pid}" / f"{new_label}.pdf"
@@ -574,14 +600,6 @@ def process_single_task(task_tuple, precomputed_response=None):
pdf_path = pdf_path.with_name(f"{label}_old.pdf")
current_suffix = "_old"
# 1. Gestion de empty-answer
if res.get("error") == "empty-answer":
old_path = pdf_path.with_name(f"{label}_old.pdf")
if pdf_path.exists() and pdf_path != old_path:
move_answer_pdf(pdf_path, old_path)
pdf_path = old_path
current_suffix = "_old"
if (not can_spawn_tasks) and res["error"] == "additional-answer":
tprint("\tSwallowing an additional-answer from a subsequent task.")
res["error"]= ""
@@ -601,7 +619,8 @@ def process_single_task(task_tuple, precomputed_response=None):
raise CorrectionStopRequested
try:
new_tasks.extend(
handle_label_errors(pid, label, res, pdf_path)
handle_label_errors(pid, label, res, pdf_path,
group_geometry=(yming, ymaxg, width_r, total_height))
)
except CorrectionStopRequested:
with io_lock:
@@ -614,6 +633,14 @@ def process_single_task(task_tuple, precomputed_response=None):
if res.get("error", "").startswith("wrg-lbl-moved-to:"):
current_suffix = "_old"
# Regrading a confirmed label can also identify an empty answer.
if res.get("error") == "empty-answer":
old_path = pdf_path.with_name(f"{label}_old.pdf")
if pdf_path.exists() and pdf_path != old_path:
move_answer_pdf(pdf_path, old_path)
pdf_path = old_path
current_suffix = "_old"
# 5. Enregistrer l'information dans correction.json
if current_suffix:
res["suffix"] = current_suffix
+27 -2
View File
@@ -227,7 +227,11 @@ def generate_request(input_dir, file, full_label, *, thinking_level=settings.THI
)
]
generate_content_config = types.GenerateContentConfig(
return (contents, grading_generation_config(thinking_level))
def grading_generation_config(thinking_level):
return types.GenerateContentConfig(
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
thinking_config=thinking_config(thinking_level),
seed=fresh_seed(),
@@ -235,7 +239,28 @@ def generate_request(input_dir, file, full_label, *, thinking_level=settings.THI
response_mime_type= "application/json",
response_json_schema= TypeAdapter(List[EvaluationEntry]).json_schema()
)
return (contents, generate_content_config)
def request_for_confirmed_label(input_dir, pdf_path, label, student_id):
prompt = make_prompt(input_dir, label)
prompt += f"""
Cette image contient uniquement la réponse de l'élève d'identifiant `{student_id}`.
Il n'y a pas d'en-tête imprimé à rechercher : utilise cet identifiant exact.
Une vérification indépendante a confirmé que cette réponse appartient bien
à la question `{label}`. Reprends sa correction mathématique depuis le début,
avec une nouvelle note et des commentaires précis sur les erreurs ou les étapes
manquantes. Une preuve incorrecte ou incomplète n'est pas une erreur de label.
Ne renvoie pas `wrong-label` et ne rejette pas la réponse comme hors sujet.
Les autres cas `additional-answer` et `empty-answer` restent applicables.
Renvoie une liste JSON contenant exactement un résultat, pour `{student_id}`.
Les coordonnées éventuelles portent sur l'image fournie entière.
"""
contents = [types.Content(role="user", parts=[
types.Part.from_bytes(data=get_single_image_bytes(pdf_path), mime_type="image/jpeg"),
types.Part.from_text(text=prompt),
])]
return contents, grading_generation_config(settings.THINKING_LEVEL_FLASH)
from pdf2image import convert_from_path