Escape some newlines in question extraction
This commit is contained in:
1 parent
e4e25eca55
commit
4c56b655fe
3 files changed
+42
-16
No files matched your search
@@ -8,7 +8,7 @@ from pathlib import Path
|
||||
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from copienator import configuration as config
|
||||
from copienator import utils
|
||||
@@ -22,7 +22,7 @@ from copienator import (
|
||||
workspace_from_args,
|
||||
)
|
||||
from copienator.platform import validate_windows_labels
|
||||
from copienator.latex_json import repair_latex_json
|
||||
from copienator.latex_json import repair_latex_json, normalize_literal_newlines
|
||||
from copienator.filesystem import staged_directory
|
||||
from copienator.utils import compile_to_pdf
|
||||
|
||||
@@ -43,7 +43,15 @@ MODEL_ID = config.MODEL_LITE_ID
|
||||
api_key = config.API_KEY
|
||||
|
||||
# --- Modèles pour la Requête 1 ---
|
||||
class QuestionOnlyItem(BaseModel):
|
||||
class StatementContent(BaseModel):
|
||||
@field_validator("question_content", "solution_content", "context_content",
|
||||
"rubric_content", "content", check_fields=False)
|
||||
@classmethod
|
||||
def normalize_newlines(cls, value: str) -> str:
|
||||
return normalize_literal_newlines(value)
|
||||
|
||||
|
||||
class QuestionOnlyItem(StatementContent):
|
||||
label: str = Field(description="Label unique de la question (par exemple '1.a' ou 'Exercice 1').")
|
||||
question_content: str = Field(description="Texte source de la question, extrait exactement du fichier d’énoncé, SANS le label lui-même.")
|
||||
|
||||
@@ -51,7 +59,7 @@ class ExamQuestions(BaseModel):
|
||||
questions: list[QuestionOnlyItem]
|
||||
|
||||
# --- Modèles pour la Requête 2 ---
|
||||
class SolutionOnlyItem(BaseModel):
|
||||
class SolutionOnlyItem(StatementContent):
|
||||
label: str = Field(description="Label exact de la question fourni en entrée, à conserver sans traduction.")
|
||||
solution_content: str = Field(description="Texte source de la solution, extrait exactement du fichier de correction.")
|
||||
|
||||
@@ -59,7 +67,7 @@ class ExamSolutions(BaseModel):
|
||||
solutions: list[SolutionOnlyItem]
|
||||
|
||||
# --- Modèles pour la Requête 3 ---
|
||||
class ExtractedContext(BaseModel):
|
||||
class ExtractedContext(StatementContent):
|
||||
target_question_label: str = Field(description="Label exact de la PREMIÈRE question située immédiatement APRÈS cette information dans l’énoncé.")
|
||||
last_question_label: str = Field(description="Label exact de la DERNIÈRE question qui utilise cette information.")
|
||||
context_content: str = Field(description="Texte source des définitions, notations ou hypothèses, extrait de l’énoncé.")
|
||||
@@ -68,7 +76,7 @@ class ExamContext(BaseModel):
|
||||
contexts: list[ExtractedContext]
|
||||
|
||||
# --- Modèles pour la Requête 4 (Barèmes) ---
|
||||
class RubricItem(BaseModel):
|
||||
class RubricItem(StatementContent):
|
||||
label: str = Field(description="Label exact de la question, à conserver sans traduction.")
|
||||
rubric_content: str = Field(description="Barème détaillé sur 4 points, en français, avec formules LaTeX. Échapper chaque antislash LaTeX conformément à la syntaxe JSON.")
|
||||
|
||||
@@ -110,6 +118,8 @@ Exemple d’échappement dans un barème (adapte les clés au schéma demandé)
|
||||
Ne confonds pas les commandes LaTeX avec les échappements JSON :
|
||||
\\frac, \\rightarrow, \\not, \\theta et \\begin sont des commandes LaTeX ;
|
||||
\n est réservé à un véritable saut de ligne du texte.
|
||||
N'écris jamais \\n pour un saut de ligne : après décodage JSON, le texte
|
||||
doit contenir un véritable saut de ligne, pas une commande LaTeX \n.
|
||||
"""
|
||||
|
||||
|
||||
@@ -124,12 +134,12 @@ def validate_rubric_content(content: str) -> str:
|
||||
return content
|
||||
|
||||
# --- Modèle fusionné (pour le reste du script) ---
|
||||
class QuestionItem(BaseModel):
|
||||
class QuestionItem(StatementContent):
|
||||
label: str
|
||||
question_content: str
|
||||
solution_content: str
|
||||
|
||||
class ContextItem(BaseModel):
|
||||
class ContextItem(StatementContent):
|
||||
target_question_label: str
|
||||
last_question_label: str
|
||||
content: str # Juste une string encapsulée pour le différencier facilement
|
||||
@@ -915,7 +925,7 @@ def process_exam(
|
||||
with open(text_dir / safe_group_filename, "w", encoding="utf-8") as f_text:
|
||||
f_text.write("\n".join(text_content_lines))
|
||||
|
||||
print(f"Success! Processed {len(grouped_extraction.groups)} groups.")
|
||||
print(f"Wrote {len(grouped_extraction.groups)} groups; checking PDF compilation...")
|
||||
# ==========================================
|
||||
# PDF COMPILATION (4 Threads)
|
||||
# ==========================================
|
||||
@@ -926,7 +936,7 @@ def process_exam(
|
||||
try:
|
||||
content = tex_file.read_text(encoding="utf-8")
|
||||
pdf_path = tex_file.with_suffix(".pdf")
|
||||
compile_to_pdf(content, pdf_path)
|
||||
compile_to_pdf(content, pdf_path, strict=True)
|
||||
except Exception as e: # noqa: BLE001 - compiler worker boundary
|
||||
return f"Error compiling {tex_file.name}: {e}"
|
||||
return None
|
||||
|
||||
+12
-6
@@ -142,7 +142,7 @@ import subprocess
|
||||
from .configuration import LATEX_AFTER, LATEX_BEFORE
|
||||
|
||||
|
||||
def compile_to_pdf(text, output_pdf_path):
|
||||
def compile_to_pdf(text, output_pdf_path, *, strict=False):
|
||||
"""Wraps text in standalone header/footer templates and compiles it to PDF."""
|
||||
latex_template = f"{LATEX_BEFORE}{text}{LATEX_AFTER}"
|
||||
|
||||
@@ -166,6 +166,7 @@ def compile_to_pdf(text, output_pdf_path):
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
encoding="utf-8", errors="replace",
|
||||
check=False
|
||||
)
|
||||
if "minted" in text:
|
||||
@@ -175,6 +176,7 @@ def compile_to_pdf(text, output_pdf_path):
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
encoding="utf-8", errors="replace",
|
||||
check=False)
|
||||
|
||||
generated_pdf = os.path.join(temp_dir, pdf_filename)
|
||||
@@ -183,17 +185,21 @@ def compile_to_pdf(text, output_pdf_path):
|
||||
shutil.move(generated_pdf, output_pdf_path)
|
||||
|
||||
if result.returncode != 0:
|
||||
error_lines = [
|
||||
line.strip() for line in result.stdout.splitlines()
|
||||
if line.lstrip().startswith("!")
|
||||
]
|
||||
detail = f": {error_lines[0]}" if error_lines else ""
|
||||
lines = result.stdout.splitlines()
|
||||
first = next((i for i, line in enumerate(lines) if line.lstrip().startswith("!")), None)
|
||||
detail = "\n" + "\n".join(lines[first:first + 5]) if first is not None else ""
|
||||
pdf_status = "a PDF was generated" if pdf_generated else "no PDF was generated"
|
||||
print(
|
||||
f"Warning: LaTeX compilation reported errors for {output_pdf_path} "
|
||||
f"(exit code {result.returncode}; {pdf_status}){detail}"
|
||||
)
|
||||
if strict:
|
||||
raise RuntimeError(f"LaTeX failed for {output_pdf_path}{detail}")
|
||||
elif not pdf_generated:
|
||||
print(f"Warning: LaTeX compilation produced no PDF for {output_pdf_path}")
|
||||
if strict:
|
||||
raise RuntimeError(f"No PDF generated for {output_pdf_path}")
|
||||
except Exception as e:
|
||||
if strict:
|
||||
raise
|
||||
print(f"Compilation error for {output_pdf_path}: {e}")
|
||||
Reference in new issue
Block a user