Initial Persp generation

This commit is contained in:
2026-07-11 20:30:27 +02:00
parent fd35675e4c
commit 09befe1d55
4 changed files with 175 additions and 14 deletions
+132 -9
View File
@@ -42,11 +42,32 @@ class ExamSolutions(BaseModel):
# --- Modèles pour la Requête 3 --- # --- Modèles pour la Requête 3 ---
class ExtractedContext(BaseModel): class ExtractedContext(BaseModel):
target_question_label: str = Field(description="The exact label of the FIRST question that comes immediately AFTER this information in the exam.") target_question_label: str = Field(description="The exact label of the FIRST question that comes immediately AFTER this information in the exam.")
last_question_label: str = Field(description="The exact label of the LAST question that uses or relies on this information.")
context_content: str = Field(description="The source text of the definitions, notations, or hypotheses, extracted from the enonce.") context_content: str = Field(description="The source text of the definitions, notations, or hypotheses, extracted from the enonce.")
class ExamContext(BaseModel): class ExamContext(BaseModel):
contexts: List[ExtractedContext] contexts: List[ExtractedContext]
# --- Modèles pour la Requête 4 (Barèmes) ---
class RubricItem(BaseModel):
label: str = Field(description="The exact label of the question.")
rubric_content: str = Field(description="Le barème détaillé en français.")
class GroupRubrics(BaseModel):
rubrics: List[RubricItem]
PROMPT_4 = """Je te fournis les questions, le contexte éventuel, et les corrections pour un groupe de questions d'un examen.
Ta tâche :
Établir un barème de correction détaillé en français pour CHAQUE question.
Chaque question DOIT être notée sur exactement 4 points. Propose une répartition logique de ces points.
Par exemple :
- Au moins 2 points si le résultat est correct.
- Mettre la moitié des points si le raisonnement est correct mais pas le résultat.
- Retirer 1.5 points si les hypothèses d'un théorème ou d'une question précédente ne sont pas vérifiées.
Renvoie le résultat sous forme de liste JSON correspondant aux labels des questions fournies.
"""
# --- Modèle fusionné (pour le reste du script) --- # --- Modèle fusionné (pour le reste du script) ---
class QuestionItem(BaseModel): class QuestionItem(BaseModel):
label: str label: str
@@ -54,6 +75,8 @@ class QuestionItem(BaseModel):
solution_content: str solution_content: str
class ContextItem(BaseModel): class ContextItem(BaseModel):
target_question_label: str
last_question_label: str
content: str # Juste une string encapsulée pour le différencier facilement content: str # Juste une string encapsulée pour le différencier facilement
class ExamExtraction(BaseModel): class ExamExtraction(BaseModel):
@@ -91,7 +114,9 @@ PROMPT_3 = """I am providing:
Your task: Your task:
Extract important information necessary to understand the questions (e.g., definitions of objects, global notations, hypotheses, context) that are NOT part of the question texts themselves. Extract important information necessary to understand the questions (e.g., definitions of objects, global notations, hypotheses, context) that are NOT part of the question texts themselves.
For each extracted piece of information, identify the label of the FIRST question that comes immediately AFTER this information in the exam. For each extracted piece of information, identify:
1. The label of the FIRST question that comes immediately AFTER this information in the exam.
2. The label of the LAST question that uses or relies on this information.
Return the result as a JSON list. Return the result as a JSON list.
""" """
@@ -247,21 +272,45 @@ def process_exam(folder_path: str):
# ========================================== # ==========================================
sol_map = {s.label: s.solution_content for s in solutions_data.solutions} sol_map = {s.label: s.solution_content for s in solutions_data.solutions}
# Map labels to their index to validate ordering
label_to_idx = {q.label: i for i, q in enumerate(questions_data.questions)}
for c in context_data.contexts:
first_idx = label_to_idx.get(c.target_question_label, -1)
last_idx = label_to_idx.get(c.last_question_label, -1)
# Enforce LAST is after (or equal to) FIRST
if first_idx != -1 and last_idx != -1 and last_idx < first_idx:
print(f"Warning: LAST question ({c.last_question_label}) is before FIRST ({c.target_question_label}). Fixing.")
c.last_question_label = c.target_question_label
elif last_idx == -1: # Fallback if invalid
c.last_question_label = c.target_question_label
# Grouper les contextes par label cible # Grouper les contextes par label cible
ctx_map = {} ctx_map = {}
for c in context_data.contexts: for c in context_data.contexts:
if c.target_question_label in ctx_map: if c.target_question_label in ctx_map:
ctx_map[c.target_question_label] += "\n\n" + c.context_content ctx_map[c.target_question_label]['content'] += "\n\n" + c.context_content
# Keep the furthest LAST question label
curr_last = ctx_map[c.target_question_label]['last']
if label_to_idx.get(c.last_question_label, -1) > label_to_idx.get(curr_last, -1):
ctx_map[c.target_question_label]['last'] = c.last_question_label
else: else:
ctx_map[c.target_question_label] = c.context_content ctx_map[c.target_question_label] = {
'content': c.context_content,
'last': c.last_question_label
}
merged_items = [] merged_items = []
for q in questions_data.questions: for q in questions_data.questions:
# 1. S'il y a un contexte pour cette question, on l'insère d'abord dans la liste
if q.label in ctx_map: if q.label in ctx_map:
merged_items.append(ContextItem(content=ctx_map[q.label])) merged_items.append(ContextItem(
target_question_label=q.label,
last_question_label=ctx_map[q.label]['last'],
content=ctx_map[q.label]['content']
))
# 2. Puis on ajoute la question
sol_content = sol_map.get(q.label, "") sol_content = sol_map.get(q.label, "")
merged_items.append(QuestionItem( merged_items.append(QuestionItem(
label=q.label, label=q.label,
@@ -328,9 +377,11 @@ def process_exam(folder_path: str):
# ---- Transform labels, and check uniqueness # ---- Transform labels, and check uniqueness
seen_labels = set() seen_labels = set()
label_updates = {}
for group in initial_groups: for group in initial_groups:
for item in group: for item in group:
if isinstance(item, QuestionItem): if isinstance(item, QuestionItem):
orig_label = item.label
# 1. Transform label # 1. Transform label
item.label = item.label.replace("Exercice", "Ex") item.label = item.label.replace("Exercice", "Ex")
item.label = item.label.replace(".", ")") item.label = item.label.replace(".", ")")
@@ -339,7 +390,14 @@ def process_exam(folder_path: str):
while item.label in seen_labels: while item.label in seen_labels:
item.label = f"XX{item.label}" item.label = f"XX{item.label}"
seen_labels.add(item.label) seen_labels.add(item.label)
label_updates[orig_label] = item.label
# Sync the modified labels to ContextItem
for group in initial_groups:
for item in group:
if isinstance(item, ContextItem):
item.target_question_label = label_updates.get(item.target_question_label, item.target_question_label)
item.last_question_label = label_updates.get(item.last_question_label, item.last_question_label)
# --- WRITE TEXT FILES --- # --- WRITE TEXT FILES ---
print(f"Writing items files to {items_file.name} and {full_items_file.name}...") print(f"Writing items files to {items_file.name} and {full_items_file.name}...")
@@ -438,7 +496,8 @@ def process_exam(folder_path: str):
edited_content = edited_content_raw.replace(' \\n ', '\n') edited_content = edited_content_raw.replace(' \\n ', '\n')
if orig_idx < len(extracted_data.items): if orig_idx < len(extracted_data.items):
orig_item = extracted_data.items[orig_idx] orig_item = extracted_data.items[orig_idx] # <-- Add this line
if isinstance(orig_item, QuestionItem): if isinstance(orig_item, QuestionItem):
current_group.append(QuestionItem( current_group.append(QuestionItem(
label=new_label, label=new_label,
@@ -446,7 +505,11 @@ def process_exam(folder_path: str):
solution_content=orig_item.solution_content solution_content=orig_item.solution_content
)) ))
elif isinstance(orig_item, ContextItem): elif isinstance(orig_item, ContextItem):
current_group.append(ContextItem(content=edited_content)) current_group.append(ContextItem(
target_question_label=orig_item.target_question_label,
last_question_label=orig_item.last_question_label,
content=edited_content
))
orig_idx += 1 orig_idx += 1
if current_group: if current_group:
@@ -467,7 +530,8 @@ def process_exam(folder_path: str):
sol_dir = folder / "Sol" sol_dir = folder / "Sol"
text2_dir = folder / "Text2" text2_dir = folder / "Text2"
sol2_dir = folder / "Sol2" sol2_dir = folder / "Sol2"
dirs = [text_dir, sol_dir, text2_dir, sol2_dir] persp_dir = folder / "Persp"
dirs = [text_dir, sol_dir, text2_dir, sol2_dir, persp_dir]
import shutil import shutil
# Ask only if any directory already exists # Ask only if any directory already exists
@@ -502,6 +566,47 @@ def process_exam(folder_path: str):
if not labels: if not labels:
continue # Skip if a group has no questions (only contexts) continue # Skip if a group has no questions (only contexts)
# ==========================================
# REQUÊTE 4 : Génération du Barème pour le groupe
# ==========================================
group_text_parts = []
for item in group:
if isinstance(item, QuestionItem):
group_text_parts.append(f"Question [{item.label}]:\n{item.question_content}\nCorrection [{item.label}]:\n{item.solution_content}")
elif isinstance(item, ContextItem):
group_text_parts.append(f"Contexte (Cible: {item.target_question_label}):\n{item.content}")
group_context_text = "\n\n---\n\n".join(group_text_parts)
contents_4 = [
types.Content(
role="user",
parts=[
types.Part.from_text(text=PROMPT_4),
types.Part.from_text(text=f"--- CONTENU DU GROUPE ---\n{group_context_text}"),
],
)
]
config_4 = types.GenerateContentConfig(
temperature=0.2,
response_mime_type="application/json",
response_json_schema=GroupRubrics.model_json_schema(),
)
print(f"Generating rubric (Persp) for group: {', '.join(labels)}...")
try:
response_r = client.models.generate_content(
model=MODEL_ID,
contents=contents_4,
config=config_4
)
rubrics_data = GroupRubrics.model_validate_json(response_r.text)
rubrics_map = {r.label: r.rubric_content for r in rubrics_data.rubrics}
except Exception as e:
print(f"Error generating rubric for group {labels[0]}: {e}")
rubrics_map = {}
# 1. Compute the common prefix for the group # 1. Compute the common prefix for the group
prefix = labels[0] prefix = labels[0]
for lbl in labels[1:]: for lbl in labels[1:]:
@@ -536,12 +641,30 @@ def process_exam(folder_path: str):
with open(sol2_dir / f"{safe_label}.tex", "w", encoding="utf-8") as f_s2: with open(sol2_dir / f"{safe_label}.tex", "w", encoding="utf-8") as f_s2:
f_s2.write(f"\\textbf{{{item.label}}} {item.solution_content}") f_s2.write(f"\\textbf{{{item.label}}} {item.solution_content}")
# --- Écriture du Barème (Persp) ---
rubric_text = rubrics_map.get(item.label, "")
with open(persp_dir / safe_label, "w", encoding="utf-8") as f_persp:
f_persp.write(f"{item.label}\n{rubric_text}")
elif isinstance(item, ContextItem): elif isinstance(item, ContextItem):
raw_ctx = item.content.strip() raw_ctx = item.content.strip()
tabulated_ctx = "\t" + re.sub(r'\n\s*', '\n\t', raw_ctx) tabulated_ctx = "\t" + re.sub(r'\n\s*', '\n\t', raw_ctx)
text_content_lines.append(f"CONTEXT :") text_content_lines.append(f"CONTEXT :")
text_content_lines.append(tabulated_ctx) text_content_lines.append(tabulated_ctx)
# --- Save context to Text2 (Concatenating if exists) ---
safe_first = item.target_question_label.replace("/", "_")
safe_last = item.last_question_label.replace("/", "_")
ctxt_filename = f"CTXT {safe_first} -> {safe_last}.tex"
ctxt_path = text2_dir / ctxt_filename
# If file exists, prepend some spacing before appending
prefix = "\n\n" if ctxt_path.exists() else ""
with open(ctxt_path, "a", encoding="utf-8") as f_c2:
f_c2.write(prefix + item.content)
# 4. Write the grouped Text file # 4. Write the grouped Text file
with open(text_dir / safe_group_filename, "w", encoding="utf-8") as f_text: with open(text_dir / safe_group_filename, "w", encoding="utf-8") as f_text:
f_text.write("\n".join(text_content_lines)) f_text.write("\n".join(text_content_lines))
+1 -1
View File
@@ -42,7 +42,7 @@ unless the distinction is very important.
In some case, you may find that either In some case, you may find that either
- The student didn't answer the right question. Set the score to 0. - The student didn't answer the right question. Set the score to 0.
Since it could be a labeling error, indicate is by setting `error` Since it could be a labeling error, indicate it by setting `error`
to \"wrong-label\". to \"wrong-label\".
- You can find an answer to another question of the exercice (taking - You can find an answer to another question of the exercice (taking
more than a couple of lines). Score the question you are supposed more than a couple of lines). Score the question you are supposed
+5 -4
View File
@@ -9,14 +9,15 @@ import threading
import annotating import annotating
from utils import natural_key, pdf_image_of_enonce, pdf_image_of_solution from utils import natural_key, pdf_image_of_enonce, pdf_image_of_solution, pdf_images_of_contexts
from reading_annotations import detect_checks_and_notes, has_significant_notes from reading_annotations import detect_checks_and_notes, has_significant_notes
def get_extra_pdfs_as_images(root_dir, label, annotating_module): def get_extra_pdfs_as_images(root_dir, label, annotating_module, all_labels):
"""Fetches Text and Sol pdfs for a given label and converts them to images.""" """Fetches Text and Sol pdfs for a given label and converts them to images."""
extra_images = [] extra_images = []
a, b = pdf_image_of_enonce(root_dir, label), pdf_image_of_solution(root_dir, label) a, b = pdf_image_of_enonce(root_dir, label), pdf_image_of_solution(root_dir, label)
for c in [a, b]: e = pdf_images_of_contexts(root_dir, label, all_labels)
for c in e + [a, b]:
if c: if c:
img, _, _ = annotating_module.make_base_image(c) img, _, _ = annotating_module.make_base_image(c)
if img: if img:
@@ -239,7 +240,7 @@ def apply_actions_and_regenerate_grouped(root_dir, data, student_id,
perfect_no_comment = False perfect_no_comment = False
if not perfect_no_comment or has_notes: if not perfect_no_comment or has_notes:
extras = get_extra_pdfs_as_images(root_dir, label, annotating) extras = get_extra_pdfs_as_images(root_dir, label, annotating, all_labels)
extras.append(final_img) extras.append(final_img)
concat_list_F.append(extras) concat_list_F.append(extras)
+37
View File
@@ -34,6 +34,43 @@ def pdf_image_of_solution(root_dir, label):
if os.path.exists(pdf_path): if os.path.exists(pdf_path):
return pdf_path return pdf_path
def pdf_images_of_contexts(root_dir, label, all_labels):
text2_dir = os.path.join(root_dir, "Text2")
if not os.path.isdir(text2_dir):
return []
# Map safe labels (with '/' replaced by '_') to their chronological index
safe_to_idx = {l.replace("/", "_"): i for i, l in enumerate(all_labels)}
safe_target = label.replace("/", "_")
target_idx = safe_to_idx.get(safe_target, -1)
if target_idx == -1:
return []
pertinent_contexts = []
for filename in os.listdir(text2_dir):
if filename.startswith("CTXT ") and filename.endswith(".pdf"):
# Extract "first_label -> last_label" from "CTXT first_label -> last_label.pdf"
core = filename[5:-4]
parts = core.split(" -> ")
if len(parts) == 2:
first_safe, last_safe = parts
first_idx = safe_to_idx.get(first_safe, -1)
last_idx = safe_to_idx.get(last_safe, -1)
# Check if the current label falls within the context's validity range
if first_idx != -1 and last_idx != -1:
if first_idx <= target_idx <= last_idx:
pdf_path = os.path.join(text2_dir, filename)
pertinent_contexts.append((first_idx, pdf_path))
# Sort by first_idx to ensure contexts are returned in logical reading order
pertinent_contexts.sort(key=lambda x: x[0])
return [path for _, path in pertinent_contexts]
def get_exam_file_content(folder_path, mode, label): def get_exam_file_content(folder_path, mode, label):
""" """