enonce_info.py
This commit is contained in:
+468
-93
@@ -1,31 +1,70 @@
|
||||
import shlex
|
||||
import re
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List
|
||||
from typing import List, Union
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from utils import compile_to_pdf
|
||||
|
||||
def get_lcp(s1: str, s2: str) -> str:
|
||||
i = 0
|
||||
while i < len(s1) and i < len(s2) and s1[i] == s2[i]:
|
||||
i += 1
|
||||
return s1[:i]
|
||||
|
||||
|
||||
# Bug : l'output est limité à 8k token…
|
||||
# MODEL_ID = "gemini-3-flash-preview"
|
||||
MODEL_ID = "gemini-3.1-flash-lite"
|
||||
api_key = os.environ.get("GEMINI_API_KEY")
|
||||
|
||||
class QuestionItem(BaseModel):
|
||||
# --- Modèles pour la Requête 1 ---
|
||||
class QuestionOnlyItem(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.")
|
||||
|
||||
class ExamQuestions(BaseModel):
|
||||
questions: List[QuestionOnlyItem]
|
||||
|
||||
# --- Modèles pour la Requête 2 ---
|
||||
class SolutionOnlyItem(BaseModel):
|
||||
label: str = Field(description="The exact unique label of the question provided in the input.")
|
||||
solution_content: str = Field(description="The source text of the solution, strictly extracted from the correction file.")
|
||||
|
||||
class ExamExtraction(BaseModel):
|
||||
questions: List[QuestionItem]
|
||||
class ExamSolutions(BaseModel):
|
||||
solutions: List[SolutionOnlyItem]
|
||||
|
||||
PROMPT = """I am providing:
|
||||
# --- Modèles pour la Requête 3 ---
|
||||
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.")
|
||||
context_content: str = Field(description="The source text of the definitions, notations, or hypotheses, extracted from the enonce.")
|
||||
|
||||
class ExamContext(BaseModel):
|
||||
contexts: List[ExtractedContext]
|
||||
|
||||
# --- Modèle fusionné (pour le reste du script) ---
|
||||
class QuestionItem(BaseModel):
|
||||
label: str
|
||||
question_content: str
|
||||
solution_content: str
|
||||
|
||||
class ContextItem(BaseModel):
|
||||
content: str # Juste une string encapsulée pour le différencier facilement
|
||||
|
||||
class ExamExtraction(BaseModel):
|
||||
items: List[Union[QuestionItem, ContextItem]] # Liste mixte
|
||||
|
||||
class GroupedExamExtraction(BaseModel):
|
||||
groups: List[List[Union[QuestionItem, ContextItem]]]
|
||||
|
||||
PROMPT_1 = """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.
|
||||
@@ -34,9 +73,26 @@ Your task:
|
||||
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.
|
||||
Return the result as a JSON list in the exact reading order of the document.
|
||||
"""
|
||||
|
||||
PROMPT_2 = """I am providing:
|
||||
1. A JSON list of question labels and their texts extracted from an exam.
|
||||
2. The source code of the exam solutions (`correction` file).
|
||||
|
||||
Your task:
|
||||
For each question label provided in the JSON, extract its exact corresponding solution textual
|
||||
content from the `correction` source file. Return the result as a JSON list in the exact same order.
|
||||
"""
|
||||
|
||||
PROMPT_3 = """I am providing:
|
||||
1. A JSON list of question labels and their texts extracted from an exam.
|
||||
2. The source code of the exam questions (`enonce` file).
|
||||
|
||||
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.
|
||||
For each extracted piece of information, identify the label of the FIRST question that comes immediately AFTER this information in the exam.
|
||||
Return the result as a JSON list.
|
||||
"""
|
||||
|
||||
def find_file(folder: Path, base_name: str) -> Path:
|
||||
@@ -70,126 +126,445 @@ def process_exam(folder_path: str):
|
||||
|
||||
client = genai.Client(api_key=api_key)
|
||||
|
||||
contents = [
|
||||
# ==========================================
|
||||
# REQUÊTE 1 : Extraction des Énoncés
|
||||
# ==========================================
|
||||
contents_1 = [
|
||||
types.Content(
|
||||
role="user",
|
||||
parts=[
|
||||
types.Part.from_text(text=PROMPT),
|
||||
types.Part.from_text(text=PROMPT_1),
|
||||
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}"),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
config_1 = types.GenerateContentConfig(
|
||||
temperature=0.1,
|
||||
response_mime_type="application/json",
|
||||
response_json_schema=ExamQuestions.model_json_schema(),
|
||||
)
|
||||
|
||||
cache_q_file = folder / "gemini_questions.json"
|
||||
|
||||
if cache_q_file.is_file():
|
||||
print("Loading cached questions from gemini_questions.json...")
|
||||
response_q_text = cache_q_file.read_text(encoding="utf-8")
|
||||
else:
|
||||
print("Sending request 1 (Questions) to Gemini...")
|
||||
response_q = client.models.generate_content(
|
||||
model=MODEL_ID,
|
||||
contents=contents_1,
|
||||
config=config_1
|
||||
)
|
||||
response_q_text = response_q.text
|
||||
print("Saving questions to cache...")
|
||||
cache_q_file.write_text(response_q_text, encoding="utf-8")
|
||||
|
||||
questions_data = ExamQuestions.model_validate_json(response_q_text)
|
||||
|
||||
# ==========================================
|
||||
# REQUÊTE 2 : Extraction des Corrections
|
||||
# ==========================================
|
||||
extracted_questions_json = questions_data.model_dump_json(indent=2)
|
||||
|
||||
contents_2 = [
|
||||
types.Content(
|
||||
role="user",
|
||||
parts=[
|
||||
types.Part.from_text(text=PROMPT_2),
|
||||
types.Part.from_text(text=f"--- EXTRACTED QUESTIONS ---\n{extracted_questions_json}"),
|
||||
types.Part.from_text(text=f"--- CORRECTION SOURCE ({correction_path.name}) ---\n{correction_text}"),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
config = types.GenerateContentConfig(
|
||||
config_2 = types.GenerateContentConfig(
|
||||
temperature=0.1,
|
||||
response_mime_type="application/json",
|
||||
response_json_schema=ExamExtraction.model_json_schema(),
|
||||
response_json_schema=ExamSolutions.model_json_schema(),
|
||||
)
|
||||
|
||||
cache_file = folder / "gemini_response.json"
|
||||
cache_s_file = folder / "gemini_solutions.json"
|
||||
|
||||
if cache_file.is_file():
|
||||
print("Loading cached response from gemini_response.json...")
|
||||
response_text = cache_file.read_text(encoding="utf-8")
|
||||
if cache_s_file.is_file():
|
||||
print("Loading cached solutions from gemini_solutions.json...")
|
||||
response_s_text = cache_s_file.read_text(encoding="utf-8")
|
||||
else:
|
||||
print("Sending request to Gemini...")
|
||||
response = client.models.generate_content(
|
||||
print("Sending request 2 (Solutions) to Gemini...")
|
||||
response_s = client.models.generate_content(
|
||||
model=MODEL_ID,
|
||||
contents=contents,
|
||||
config=config
|
||||
contents=contents_2,
|
||||
config=config_2
|
||||
)
|
||||
response_text = response.text
|
||||
response_s_text = response_s.text
|
||||
print("Saving solutions to cache...")
|
||||
cache_s_file.write_text(response_s_text, encoding="utf-8")
|
||||
|
||||
print("Saving response to cache...")
|
||||
cache_file.write_text(response_text, encoding="utf-8")
|
||||
solutions_data = ExamSolutions.model_validate_json(response_s_text)
|
||||
|
||||
# Validate from the text variable (cached or fresh)
|
||||
extracted_data = ExamExtraction.model_validate_json(response_text)
|
||||
# ==========================================
|
||||
# REQUÊTE 3 : Extraction du Contexte (Notations, etc.)
|
||||
# ==========================================
|
||||
contents_3 = [
|
||||
types.Content(
|
||||
role="user",
|
||||
parts=[
|
||||
types.Part.from_text(text=PROMPT_3),
|
||||
types.Part.from_text(text=f"--- EXTRACTED QUESTIONS ---\n{extracted_questions_json}"),
|
||||
types.Part.from_text(text=f"--- ENONCE SOURCE ({enonce_path.name}) ---\n{enonce_text}"),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
config_3 = types.GenerateContentConfig(
|
||||
temperature=0.1,
|
||||
response_mime_type="application/json",
|
||||
response_json_schema=ExamContext.model_json_schema(),
|
||||
)
|
||||
|
||||
cache_c_file = folder / "gemini_context.json"
|
||||
|
||||
if cache_c_file.is_file():
|
||||
print("Loading cached context from gemini_context.json...")
|
||||
response_c_text = cache_c_file.read_text(encoding="utf-8")
|
||||
else:
|
||||
print("Sending request 3 (Context) to Gemini...")
|
||||
response_c = client.models.generate_content(
|
||||
model=MODEL_ID,
|
||||
contents=contents_3,
|
||||
config=config_3
|
||||
)
|
||||
response_c_text = response_c.text
|
||||
print("Saving context to cache...")
|
||||
cache_c_file.write_text(response_c_text, encoding="utf-8")
|
||||
|
||||
context_data = ExamContext.model_validate_json(response_c_text)
|
||||
|
||||
# ==========================================
|
||||
# FUSION des trois résultats
|
||||
# ==========================================
|
||||
sol_map = {s.label: s.solution_content for s in solutions_data.solutions}
|
||||
|
||||
# Grouper les contextes par label cible
|
||||
ctx_map = {}
|
||||
for c in context_data.contexts:
|
||||
if c.target_question_label in ctx_map:
|
||||
ctx_map[c.target_question_label] += "\n\n" + c.context_content
|
||||
else:
|
||||
ctx_map[c.target_question_label] = c.context_content
|
||||
|
||||
merged_items = []
|
||||
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:
|
||||
merged_items.append(ContextItem(content=ctx_map[q.label]))
|
||||
|
||||
# 2. Puis on ajoute la question
|
||||
sol_content = sol_map.get(q.label, "")
|
||||
merged_items.append(QuestionItem(
|
||||
label=q.label,
|
||||
question_content=q.question_content,
|
||||
solution_content=sol_content
|
||||
))
|
||||
|
||||
extracted_data = ExamExtraction(items=merged_items)
|
||||
|
||||
# ==========================================
|
||||
# INITIAL GROUPING COMPUTATION
|
||||
# ==========================================
|
||||
items_file = folder / "exam_items.txt"
|
||||
full_items_file = folder / "exam_items_full.txt"
|
||||
trunc_map = {}
|
||||
|
||||
# --- INITIAL GROUPING COMPUTATION ---
|
||||
questions_only = [item for item in extracted_data.items if isinstance(item, QuestionItem)]
|
||||
|
||||
q_group_indices = []
|
||||
if questions_only:
|
||||
current_g = [0]
|
||||
for i in range(1, len(questions_only)):
|
||||
p = get_lcp(questions_only[current_g[0]].label, questions_only[i].label)
|
||||
proposed = current_g + [i]
|
||||
valid = True
|
||||
for k in range(len(proposed) - 1):
|
||||
if get_lcp(questions_only[proposed[k]].label, questions_only[proposed[k+1]].label) != p:
|
||||
valid = False
|
||||
break
|
||||
if valid:
|
||||
current_g.append(i)
|
||||
else:
|
||||
q_group_indices.append(current_g)
|
||||
current_g = [i]
|
||||
q_group_indices.append(current_g)
|
||||
|
||||
group_starter_labels = {questions_only[g[0]].label for g in q_group_indices[1:]} if q_group_indices else set()
|
||||
|
||||
initial_groups = []
|
||||
current_group = []
|
||||
|
||||
for i, item in enumerate(extracted_data.items):
|
||||
is_new_group = False
|
||||
if isinstance(item, QuestionItem):
|
||||
if item.label in group_starter_labels:
|
||||
if not (len(current_group) > 0 and isinstance(current_group[-1], ContextItem)):
|
||||
is_new_group = True
|
||||
elif isinstance(item, ContextItem):
|
||||
if i + 1 < len(extracted_data.items):
|
||||
next_item = extracted_data.items[i+1]
|
||||
if isinstance(next_item, QuestionItem) and next_item.label in group_starter_labels:
|
||||
is_new_group = True
|
||||
|
||||
if is_new_group and current_group:
|
||||
initial_groups.append(current_group)
|
||||
current_group = []
|
||||
|
||||
current_group.append(item)
|
||||
|
||||
if current_group:
|
||||
initial_groups.append(current_group)
|
||||
|
||||
# ---- Transform labels, and check uniqueness
|
||||
|
||||
seen_labels = set()
|
||||
for group in initial_groups:
|
||||
for item in group:
|
||||
if isinstance(item, QuestionItem):
|
||||
# 1. Transform label
|
||||
item.label = item.label.replace("Exercice", "Ex")
|
||||
item.label = item.label.replace(".", ")")
|
||||
|
||||
# 2. Ensure uniqueness (prefix with XX)
|
||||
while item.label in seen_labels:
|
||||
item.label = f"XX{item.label}"
|
||||
seen_labels.add(item.label)
|
||||
|
||||
|
||||
# --- WRITE TEXT FILES ---
|
||||
print(f"Writing items files to {items_file.name} and {full_items_file.name}...")
|
||||
|
||||
with open(items_file, "w", encoding="utf-8") as f, \
|
||||
open(full_items_file, "w", encoding="utf-8") as f_full:
|
||||
|
||||
header = "# Edit labels. Modify groups (---). Ensure label uniqueness (XX). Duplicate CONTEXT.\n\n"
|
||||
f.write(header)
|
||||
f_full.write(header)
|
||||
|
||||
for g_idx, group in enumerate(initial_groups):
|
||||
if g_idx > 0:
|
||||
f.write("\n---\n\n")
|
||||
f_full.write("\n---\n\n")
|
||||
|
||||
for item in group:
|
||||
if isinstance(item, QuestionItem):
|
||||
safe_content = item.question_content.replace('\n', ' \\n ')
|
||||
f_full.write(f"{item.label} ### {safe_content}\n")
|
||||
|
||||
if len(safe_content) > 65:
|
||||
trunc_content = safe_content[:64] + "…"
|
||||
trunc_map[trunc_content] = safe_content
|
||||
else:
|
||||
trunc_content = safe_content
|
||||
|
||||
f.write(f"{item.label} ### {trunc_content}\n")
|
||||
|
||||
elif isinstance(item, ContextItem):
|
||||
safe_content = item.content.replace('\n', ' \\n ')
|
||||
f.write(f"CONTEXT ### {safe_content}\n")
|
||||
f_full.write(f"CONTEXT ### {safe_content}\n")
|
||||
|
||||
# --- OPEN EDITOR AND PARSE ---
|
||||
while True:
|
||||
print("Opening items file for editing...")
|
||||
editor = os.environ.get("EDITOR")
|
||||
try:
|
||||
if editor:
|
||||
subprocess.run(shlex.split(editor) + [str(items_file)])
|
||||
else:
|
||||
if sys.platform.startswith("linux"):
|
||||
subprocess.run(["xdg-open", str(items_file)])
|
||||
elif sys.platform == "darwin":
|
||||
subprocess.run(["open", str(items_file)])
|
||||
else:
|
||||
os.startfile(str(items_file))
|
||||
input("Press ENTER here once you have saved and closed the text file...")
|
||||
except Exception as e:
|
||||
print(f"Error running editor: {e}")
|
||||
|
||||
print(f"Parsing edited items from {items_file.name}...")
|
||||
with open(items_file, "r", encoding="utf-8") as f:
|
||||
edited_lines = [line.strip() for line in f if line.strip() and not line.startswith("#")]
|
||||
|
||||
# 1. Validation for XX labels
|
||||
has_xx = False
|
||||
for line in edited_lines:
|
||||
if " ### " in line:
|
||||
lbl = line.split(" ### ", 1)[0].strip()
|
||||
if lbl.startswith("XX"):
|
||||
has_xx = True
|
||||
break
|
||||
|
||||
if has_xx:
|
||||
print("\n!!! ERROR: Some labels still start with 'XX'. Please remove the 'XX' prefixes to ensure unique, valid labels.")
|
||||
input("Press ENTER to return to the editor...")
|
||||
continue
|
||||
|
||||
# 2. Actual Parsing
|
||||
grouped_items = []
|
||||
current_group = []
|
||||
labels_list = []
|
||||
orig_idx = 0
|
||||
|
||||
for line in edited_lines:
|
||||
if line == "---":
|
||||
if current_group:
|
||||
grouped_items.append(current_group)
|
||||
current_group = []
|
||||
continue
|
||||
|
||||
if " ### " not in line:
|
||||
continue
|
||||
|
||||
new_label, edited_content_raw = line.split(" ### ", 1)
|
||||
new_label = new_label.strip()
|
||||
|
||||
if new_label != "CONTEXT":
|
||||
labels_list.append(new_label)
|
||||
|
||||
if "…" in edited_content_raw and edited_content_raw in trunc_map:
|
||||
edited_content_raw = trunc_map[edited_content_raw]
|
||||
|
||||
edited_content = edited_content_raw.replace(' \\n ', '\n')
|
||||
|
||||
if orig_idx < len(extracted_data.items):
|
||||
orig_item = extracted_data.items[orig_idx]
|
||||
if isinstance(orig_item, QuestionItem):
|
||||
current_group.append(QuestionItem(
|
||||
label=new_label,
|
||||
question_content=edited_content,
|
||||
solution_content=orig_item.solution_content
|
||||
))
|
||||
elif isinstance(orig_item, ContextItem):
|
||||
current_group.append(ContextItem(content=edited_content))
|
||||
orig_idx += 1
|
||||
|
||||
if current_group:
|
||||
grouped_items.append(current_group)
|
||||
|
||||
# If we reached here without 'continue', the data is valid
|
||||
break
|
||||
|
||||
# Save labels and proceed
|
||||
with open(folder / "labels", 'w', encoding='utf-8') as f_labels:
|
||||
for label in labels_list:
|
||||
f_labels.write(f"{label}\n")
|
||||
|
||||
grouped_extraction = GroupedExamExtraction(groups=grouped_items)
|
||||
|
||||
# 2. Setup output directories
|
||||
text_dir = folder / "Text"
|
||||
sol_dir = folder / "Sol"
|
||||
text_dir.mkdir(exist_ok=True)
|
||||
sol_dir.mkdir(exist_ok=True)
|
||||
text2_dir = folder / "Text2"
|
||||
sol2_dir = folder / "Sol2"
|
||||
dirs = [text_dir, sol_dir, text2_dir, sol2_dir]
|
||||
|
||||
labels_file = folder / "labels"
|
||||
import shutil
|
||||
# Ask only if any directory already exists
|
||||
if any(d.exists() for d in dirs):
|
||||
answer = input(
|
||||
"Output directories already exist. Delete their contents? [y/N] "
|
||||
).strip().lower()
|
||||
|
||||
# Step 1: Write initial labels
|
||||
print("Writing initial labels file...")
|
||||
with open(labels_file, "w", encoding="utf-8") as flabels:
|
||||
for q in extracted_data.questions:
|
||||
flabels.write(f"{q.label}\n")
|
||||
if answer not in ("y", "yes"):
|
||||
print("Aborted.")
|
||||
sys.exit(1)
|
||||
# Empty each directory
|
||||
for d in dirs:
|
||||
if d.exists():
|
||||
shutil.rmtree(d)
|
||||
d.mkdir(parents=True)
|
||||
else:
|
||||
# Create them if they don't exist
|
||||
for d in dirs:
|
||||
d.mkdir(parents=True)
|
||||
|
||||
# Step 2: Open labels file for user editing
|
||||
print("Opening labels file for editing...")
|
||||
editor = os.environ.get("EDITOR")
|
||||
try:
|
||||
if editor:
|
||||
subprocess.run(shlex.split(editor) + [str(labels_file)])
|
||||
else:
|
||||
# Fallbacks if $EDITOR is not set
|
||||
if sys.platform.startswith("linux"):
|
||||
subprocess.Popen(["xdg-open", str(labels_file)])
|
||||
elif sys.platform == "darwin":
|
||||
subprocess.Popen(["open", str(labels_file)])
|
||||
else:
|
||||
os.startfile(str(labels_file))
|
||||
text_dir.mkdir(exist_ok=True)
|
||||
sol_dir.mkdir(exist_ok=True)
|
||||
|
||||
# xdg-open/open usually do not block, so we wait for user confirmation
|
||||
input("Press ENTER here once you have saved and closed the labels file...")
|
||||
except Exception:
|
||||
print("Error running editor, using labels as given.")
|
||||
|
||||
# Step 3 & 4: Read the edited file back and create a mapping
|
||||
with open(labels_file, "r", encoding="utf-8") as flabels:
|
||||
edited_lines = [line.strip() for line in flabels if line.strip()]
|
||||
print("Writing grouped question and solution files...")
|
||||
|
||||
mapping = []
|
||||
final_labels = []
|
||||
orig_idx = 0
|
||||
for group in grouped_extraction.groups:
|
||||
q_items = [item for item in group if isinstance(item, QuestionItem)]
|
||||
labels = [q.label for q in q_items]
|
||||
|
||||
for line in edited_lines:
|
||||
if line.startswith("+"):
|
||||
new_label = line[1:].lstrip()
|
||||
final_labels.append(new_label)
|
||||
# New label, no source content
|
||||
mapping.append((new_label, None))
|
||||
else:
|
||||
new_label = line
|
||||
final_labels.append(new_label)
|
||||
# Map to initial order, advancing index only for non-'+' items
|
||||
q_item = extracted_data.questions[orig_idx] if orig_idx < len(extracted_data.questions) else None
|
||||
mapping.append((new_label, q_item))
|
||||
orig_idx += 1
|
||||
if not labels:
|
||||
continue # Skip if a group has no questions (only contexts)
|
||||
|
||||
# Rewrite the labels file cleanly (removing '+' prefixes)
|
||||
with open(labels_file, "w", encoding="utf-8") as flabels:
|
||||
for lbl in final_labels:
|
||||
flabels.write(f"{lbl}\n")
|
||||
# 1. Compute the common prefix for the group
|
||||
prefix = labels[0]
|
||||
for lbl in labels[1:]:
|
||||
prefix = get_lcp(prefix, lbl)
|
||||
|
||||
# Step 5: Write the final question and solution files
|
||||
print("Writing question and solution files...")
|
||||
for new_label, q_item in mapping:
|
||||
safe_label = new_label.replace("/", "_")
|
||||
# 2. Format the Text filename: prefix [label1, label2]
|
||||
labels_str = ",".join([label[len(prefix):] for label in labels])
|
||||
group_filename = f"{prefix}[{labels_str}]"
|
||||
safe_group_filename = group_filename.replace("/", "_")
|
||||
|
||||
if q_item:
|
||||
q_content = q_item.question_content.replace("\\n", "\n")
|
||||
s_content = q_item.solution_content.replace("\\n", "\n")
|
||||
else:
|
||||
q_content = ""
|
||||
s_content = ""
|
||||
text_content_lines = []
|
||||
|
||||
# Write Text/label
|
||||
with open(text_dir / safe_label, "w", encoding="utf-8") as f:
|
||||
f.write(f"{new_label}\n{q_content}")
|
||||
# 3. Process each item in the group
|
||||
for item in group:
|
||||
if isinstance(item, QuestionItem):
|
||||
# 1. Prepare tabulated content:
|
||||
# Start with a tab, then replace every newline+whitespace with newline+tab
|
||||
raw_content = item.question_content.strip()
|
||||
tabulated = "\t" + re.sub(r'\n\s*', '\n\t', raw_content)
|
||||
|
||||
# Write Sol/label
|
||||
with open(sol_dir / safe_label, "w", encoding="utf-8") as f:
|
||||
f.write(f"{new_label}\n{s_content}")
|
||||
# 2. Build Text entry
|
||||
text_content_lines.append(f"{item.label} :")
|
||||
text_content_lines.append(tabulated)
|
||||
|
||||
# Write individual Sol file (remains unchanged)
|
||||
safe_label = item.label.replace("/", "_")
|
||||
with open(sol_dir / safe_label, "w", encoding="utf-8") as f_sol:
|
||||
f_sol.write(f"{item.label}\n{item.solution_content}")
|
||||
|
||||
with open(text2_dir / f"{safe_label}.tex", "w", encoding="utf-8") as f_t2:
|
||||
f_t2.write(f"\\textbf{{{item.label}}} {item.question_content}")
|
||||
|
||||
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}")
|
||||
elif isinstance(item, ContextItem):
|
||||
raw_ctx = item.content.strip()
|
||||
tabulated_ctx = "\t" + re.sub(r'\n\s*', '\n\t', raw_ctx)
|
||||
text_content_lines.append(f"CONTEXT :")
|
||||
text_content_lines.append(tabulated_ctx)
|
||||
|
||||
# 4. Write the grouped Text file
|
||||
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.")
|
||||
# ==========================================
|
||||
# PDF COMPILATION (4 Threads)
|
||||
# ==========================================
|
||||
all_tex_files = list(text2_dir.glob("*.tex")) + list(sol2_dir.glob("*.tex"))
|
||||
|
||||
def compile_worker(tex_file: Path):
|
||||
"""Helper to read content and call the utility function."""
|
||||
try:
|
||||
content = tex_file.read_text(encoding="utf-8")
|
||||
pdf_path = tex_file.with_suffix(".pdf")
|
||||
compile_to_pdf(content, pdf_path)
|
||||
except Exception as e:
|
||||
print(f"Error compiling {tex_file.name}: {e}")
|
||||
|
||||
print(f"Compiling {len(all_tex_files)} files to PDF using 4 threads...")
|
||||
with ThreadPoolExecutor(max_workers=4) as executor:
|
||||
executor.map(compile_worker, all_tex_files)
|
||||
|
||||
print(f"Success! Processed {len(mapping)} labels.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if not api_key:
|
||||
|
||||
Reference in New Issue
Block a user