start-gui.sh et améliorations diverses

This commit is contained in:
2026-09-08 12:04:18 +02:00
parent 06d7bad04e
commit db4ed2ef31
14 changed files with 697 additions and 50 deletions
+117 -25
View File
@@ -22,6 +22,7 @@ from copienator import (
workspace_from_args,
)
from copienator.platform import validate_windows_labels
from copienator.filesystem import staged_directory
from copienator.utils import compile_to_pdf
@@ -73,6 +74,9 @@ class RubricItem(BaseModel):
class GroupRubrics(BaseModel):
rubrics: list[RubricItem]
class LabelGroups(BaseModel):
groups: list[list[str]]
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é pour CHAQUE question.
@@ -176,6 +180,104 @@ def find_file(folder: Path, base_name: str) -> Path | None:
return path
return None
def generate_rubrics(client, group_context_text: str) -> dict[str, str]:
"""Use the same rubric request for full and selective statement generation."""
response = client.models.generate_content(
model=MODEL_ID,
contents=[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=types.GenerateContentConfig(
system_instruction="Rédige tous les barèmes et consignes de notation en français. Conserve les clés JSON, les labels et les formules mathématiques.",
temperature=0.2,
response_mime_type="application/json",
response_json_schema=GroupRubrics.model_json_schema(),
),
)
rubrics = GroupRubrics.model_validate_json(response.text).rubrics
if len({item.label for item in rubrics}) != len(rubrics):
raise ValueError("Gemini returned duplicate rubric labels")
return {item.label: item.rubric_content for item in rubrics}
def validate_groups(groups: list[list[str]], labels: list[str]) -> None:
flattened = [label for group in groups for label in group]
if (not groups or any(not group for group in groups)
or len(flattened) != len(set(flattened)) or set(flattened) != set(labels)):
raise CliError("Groups must contain every existing label exactly once")
def refine_existing(workspace: EvaluationWorkspace, mode: str, *, api_client=None) -> ExitCode:
"""Regroup or replace rubrics without regenerating statements or solutions."""
labels = workspace.read_labels()
if not labels or len(labels) != len(set(labels)):
raise CliError("Generate unique question labels before refining the statement")
validate_windows_labels(labels)
questions = {}
for label in labels:
safe_label = label.replace("/", "_")
parts = []
for directory, title in (("Text2", "Question"), ("Sol2", "Correction")):
path = workspace.root / directory / f"{safe_label}.tex"
if not path.is_file():
raise CliError(f"Missing {path}; generate statements and solutions first")
parts.append(f"{title} [{label}]:\n{path.read_text(encoding='utf-8')}")
questions[label] = "\n".join(parts)
context = utils.enonce_total(workspace.root)
if api_client is None:
if not api_key:
raise CliError("GEMINI_API_KEY is not configured")
api_client = genai.Client(api_key=api_key)
if mode == "groups":
response = api_client.models.generate_content(
model=MODEL_ID,
contents=[types.Content(role="user", parts=[types.Part.from_text(text=(
"Regroupe ces questions dexamen en groupes cohérents pour la correction "
"et lannotation, selon leurs dépendances et leur contexte commun. "
"Ne mélange pas des exercices différents. Conserve lordre des questions. "
"Chaque label doit apparaître exactement une fois, sans modification. "
"Renvoie uniquement un objet JSON groups contenant des listes de labels.\n\n"
+ context + "\n\n" + "\n\n".join(questions.values())
))])],
config=types.GenerateContentConfig(
temperature=0.1, response_mime_type="application/json",
response_json_schema=LabelGroups.model_json_schema(),
),
)
groups = LabelGroups.model_validate_json(response.text).groups
validate_groups(groups, labels)
atomic_write_text(workspace.label_groups_file,
"".join(", ".join(group) + "\n" for group in groups))
print(f"Updated label_groups: {len(groups)} Gemini groups.")
elif mode == "persp":
if not workspace.label_groups_file.is_file():
raise CliError("Generate label_groups before generating rubrics")
groups = [[label.strip() for label in line.split(",") if label.strip()]
for line in workspace.label_groups_file.read_text(encoding="utf-8").splitlines()
if line.strip()]
validate_groups(groups, labels)
with staged_directory(workspace.root / "Persp") as staging:
for group in groups:
print(f"Generating rubric (Persp) for group: {', '.join(group)}...")
group_content = (
"Contexte général de lexamen, fourni uniquement pour comprendre les questions :\n"
+ context + "\n\nProduis des barèmes UNIQUEMENT pour les labels suivants : "
+ ", ".join(group) + "\n\n" + "\n\n".join(questions[label] for label in group)
)
rubrics = generate_rubrics(api_client, group_content)
if set(rubrics) != set(group) or any(not value.strip() for value in rubrics.values()):
raise CliError("Incomplete or unexpected Gemini rubrics; previous Persp preserved")
for label, rubric in rubrics.items():
(staging / label.replace("/", "_")).write_text(
f"{label}\n{rubric}", encoding="utf-8")
print("Replaced Persp with Gemini rubrics.")
else:
raise ValueError(f"Unknown statement refinement: {mode}")
return ExitCode.SUCCESS
def process_exam(
workspace: EvaluationWorkspace,
restart: bool = False,
@@ -701,32 +803,9 @@ def process_exam(
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(
system_instruction="Rédige tous les barèmes et consignes de notation en français. Conserve les clés JSON, les labels et les formules mathématiques.",
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}
rubrics_map = generate_rubrics(client, group_context_text)
except Exception as e: # noqa: BLE001 - remote API boundary
print(f"Error generating rubric for group {labels[0]}: {e}")
processing_errors.append(str(e))
@@ -822,12 +901,23 @@ def process_exam(
workspace.labels_file,
"".join(f"{label}\n" for label in labels_list),
)
atomic_write_text(
workspace.label_groups_file,
"".join(", ".join(item.label for item in group if isinstance(item, QuestionItem)) + "\n"
for group in grouped_extraction.groups
if any(isinstance(item, QuestionItem) for item in group)),
)
return ExitCode.PARTIAL if processing_errors else ExitCode.SUCCESS
def build_parser() -> argparse.ArgumentParser:
parser = evaluation_parser("Extract exam and solution code via Gemini")
actions = parser.add_mutually_exclusive_group()
actions.add_argument("--groups-only", action="store_true",
help="Regroup existing questions with Gemini; update only label_groups")
actions.add_argument("--persp-only", action="store_true",
help="Replace only Persp with Gemini rubrics for existing groups")
parser.add_argument(
"--restart",
action="store_true",
@@ -841,7 +931,9 @@ def main(argv: Sequence[str] | None = None) -> int:
return execute(
parser,
argv,
lambda args: process_exam(
lambda args: refine_existing(workspace_from_args(args),
"groups" if args.groups_only else "persp")
if args.groups_only or args.persp_only else process_exam(
workspace_from_args(args),
restart=args.restart,
),