Divers, en lien avec DMI04 (gemini_for_enonce.py, smaller images, link to enonce.pdf)
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
|
||||
MODEL_ID = "gemini-3-flash-preview"
|
||||
api_key = os.environ.get("GEMINI_API_KEY")
|
||||
|
||||
class QuestionItem(BaseModel):
|
||||
label: str = Field(description="The unique label of the question (e.g., '1.a', 'Exercice 1')")
|
||||
question_content: str = Field(description="The source text of the question, strictly extracted from the enonce file, EXCLUDING the label itself.")
|
||||
solution_content: str = Field(description="The source text of the solution, strictly extracted from the correction file.")
|
||||
|
||||
class ExamExtraction(BaseModel):
|
||||
questions: List[QuestionItem]
|
||||
|
||||
PROMPT = """I am providing:
|
||||
1. A PDF of an exam (`enonce.pdf`)
|
||||
2. The source code of the exam questions (`enonce` file)
|
||||
3. The source code of the exam solutions (`correction` file)
|
||||
|
||||
Your task:
|
||||
1. Identify all distinct question labels using the PDF document.
|
||||
These labels should be unique : use `Ex 1 : 1)a)` or `I)1)b)`.
|
||||
2. For each label, extract its exact corresponding question text
|
||||
from the `enonce` source file. Do not include the label itself
|
||||
in this extracted text (nor LaTeX like `item` nor org-mode list
|
||||
labelling like `2.`).
|
||||
3. For each label, extract its exact corresponding solution textual
|
||||
content from the `correction` source file. Return the result as
|
||||
a JSON list in the exact reading order of the document.
|
||||
"""
|
||||
|
||||
def find_file(folder: Path, base_name: str) -> Path:
|
||||
for ext in [".org", ".tex"]:
|
||||
path = folder / f"{base_name}{ext}"
|
||||
if path.is_file():
|
||||
return path
|
||||
return None
|
||||
|
||||
def process_exam(folder_path: str):
|
||||
folder = Path(folder_path)
|
||||
|
||||
# 1. Resolve files
|
||||
pdf_path = folder / "enonce.pdf"
|
||||
enonce_path = find_file(folder, "enonce")
|
||||
correction_path = find_file(folder, "correction")
|
||||
|
||||
missing = []
|
||||
if not pdf_path.is_file(): missing.append("enonce.pdf")
|
||||
if not enonce_path: missing.append("enonce.org or enonce.tex")
|
||||
if not correction_path: missing.append("correction.org or correction.tex")
|
||||
|
||||
if missing:
|
||||
print(f"Error: Missing files in {folder}: {', '.join(missing)}")
|
||||
sys.exit(1)
|
||||
|
||||
print("Reading files...")
|
||||
pdf_bytes = pdf_path.read_bytes()
|
||||
enonce_text = enonce_path.read_text(encoding="utf-8")
|
||||
correction_text = correction_path.read_text(encoding="utf-8")
|
||||
|
||||
client = genai.Client(api_key=api_key)
|
||||
|
||||
contents = [
|
||||
types.Content(
|
||||
role="user",
|
||||
parts=[
|
||||
types.Part.from_text(text=PROMPT),
|
||||
types.Part.from_bytes(data=pdf_bytes, mime_type="application/pdf"),
|
||||
types.Part.from_text(text=f"--- ENONCE SOURCE ({enonce_path.name}) ---\n{enonce_text}"),
|
||||
types.Part.from_text(text=f"--- CORRECTION SOURCE ({correction_path.name}) ---\n{correction_text}"),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
config = types.GenerateContentConfig(
|
||||
temperature=0.1,
|
||||
response_mime_type="application/json",
|
||||
response_json_schema=ExamExtraction.model_json_schema(),
|
||||
)
|
||||
|
||||
print("Sending request to Gemini...")
|
||||
response = client.models.generate_content(
|
||||
model=MODEL_ID,
|
||||
contents=contents,
|
||||
config=config
|
||||
)
|
||||
|
||||
extracted_data = ExamExtraction.model_validate_json(response.text)
|
||||
|
||||
# 2. Setup output directories
|
||||
text_dir = folder / "Text"
|
||||
sol_dir = folder / "Sol"
|
||||
text_dir.mkdir(exist_ok=True)
|
||||
sol_dir.mkdir(exist_ok=True)
|
||||
|
||||
labels_file = folder / "labels"
|
||||
|
||||
print("Writing files...")
|
||||
with open(labels_file, "w", encoding="utf-8") as flabels:
|
||||
for q in extracted_data.questions:
|
||||
# Sanitize label for filesystem (prevent directory traversal if label contains '/')
|
||||
safe_label = q.label.replace("/", "_")
|
||||
|
||||
flabels.write(f"{safe_label}\n")
|
||||
|
||||
# Fix double-escaped newlines
|
||||
q_content = q.question_content.replace("\\n", "\n")
|
||||
s_content = q.solution_content.replace("\\n", "\n")
|
||||
|
||||
|
||||
# Write Text/label
|
||||
with open(text_dir / safe_label, "w", encoding="utf-8") as f:
|
||||
f.write(f"{q.label}\n{q.question_content}")
|
||||
|
||||
# Write Sol/label
|
||||
with open(sol_dir / safe_label, "w", encoding="utf-8") as f:
|
||||
f.write(f"{q.label}\n{q.solution_content}")
|
||||
|
||||
print(f"Success! Processed {len(extracted_data.questions)} questions.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if not api_key:
|
||||
print("Error: GEMINI_API_KEY environment variable is not set.")
|
||||
sys.exit(1)
|
||||
|
||||
parser = argparse.ArgumentParser(description="Extract exam and solution code via Gemini.")
|
||||
parser.add_argument("folder", help="Directory containing the exam files")
|
||||
|
||||
args = parser.parse_args()
|
||||
process_exam(args.folder)
|
||||
Reference in New Issue
Block a user