Support evaluation-local configuration and names overrides
This commit is contained in:
1 parent
0bb6f7d97c
commit
d4404e4c27
36 files changed
+409
-184
No files matched your search
@@ -10,21 +10,11 @@ import pandas as pd
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from copienator import atomic_write_json, read_json
|
||||
from copienator.configuration import (
|
||||
FINAL_SCORE_FONT_PATH,
|
||||
FINAL_SCORE_HISTOGRAM_PATH,
|
||||
FINAL_SCORE_ODS_PATH,
|
||||
FINAL_SCORE_OUTPUT_DIR,
|
||||
)
|
||||
|
||||
# Configuration constants
|
||||
ODS_PATH = Path(FINAL_SCORE_ODS_PATH).expanduser()
|
||||
OUTPUT_DIR = Path(FINAL_SCORE_OUTPUT_DIR).expanduser()
|
||||
HISTOGRAM_PATH = Path(FINAL_SCORE_HISTOGRAM_PATH).expanduser()
|
||||
from copienator import configuration
|
||||
|
||||
|
||||
def score_font(size):
|
||||
candidates = [FINAL_SCORE_FONT_PATH, "DejaVuSans-Bold.ttf", "arial.ttf"]
|
||||
candidates = [configuration.FINAL_SCORE_FONT_PATH, "DejaVuSans-Bold.ttf", "arial.ttf"]
|
||||
for candidate in candidates:
|
||||
if not candidate:
|
||||
continue
|
||||
@@ -64,9 +54,10 @@ def copy_return_artifacts(source_dir: Path, destination_dir: Path) -> None:
|
||||
|
||||
def copy_histogram(output_dir: Path, *, copy_merged_histogramme: bool = False) -> None:
|
||||
"""Copy the score histogram beside the per-student output folders."""
|
||||
sources = [(HISTOGRAM_PATH, "histogramme.pdf")]
|
||||
histogram_path = Path(configuration.FINAL_SCORE_HISTOGRAM_PATH).expanduser()
|
||||
sources = [(histogram_path, "histogramme.pdf")]
|
||||
if copy_merged_histogramme:
|
||||
sources.append((HISTOGRAM_PATH.with_name("merged_histogramme.pdf"), "merged_histogramme.pdf"))
|
||||
sources.append((histogram_path.with_name("merged_histogramme.pdf"), "merged_histogramme.pdf"))
|
||||
for source, filename in sources:
|
||||
if not source.is_file():
|
||||
print(f"Warning: Missing histogram '{source}'.")
|
||||
@@ -113,7 +104,7 @@ def process_images(base_dir, output_dir, *, copy_merged_histogramme=False):
|
||||
try:
|
||||
# header=None assumes the file starts directly with data.
|
||||
# If row 0 is a header, change to header=0
|
||||
df = pd.read_excel(ODS_PATH, engine="odf", header=None)
|
||||
df = pd.read_excel(Path(configuration.FINAL_SCORE_ODS_PATH).expanduser(), engine="odf", header=None)
|
||||
# Create a lookup dictionary: {Name: Score}
|
||||
score_db = dict(zip(df[0], df[1]))
|
||||
except Exception as e:
|
||||
@@ -209,7 +200,8 @@ def main(argv=None):
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
base_dir = args.dir.expanduser().resolve()
|
||||
output_dir = OUTPUT_DIR / base_dir.name
|
||||
configuration.use_evaluation(base_dir)
|
||||
output_dir = Path(configuration.FINAL_SCORE_OUTPUT_DIR).expanduser() / base_dir.name
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
process_images(base_dir, output_dir, copy_merged_histogramme=args.copy_merged_histogramme)
|
||||
return 0
|
||||
|
||||
@@ -20,8 +20,8 @@ from matplotlib import pyplot as plt
|
||||
from pdf2image import convert_from_path
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from copienator import configuration
|
||||
from copienator import utils
|
||||
from copienator.configuration import LATEX_ANOT_AFTER, LATEX_ANOT_BEFORE
|
||||
from copienator import (
|
||||
EvaluationWorkspace,
|
||||
ExitCode,
|
||||
@@ -190,10 +190,10 @@ def render_real_latex_text(text, width_px, bg_color=(255, 255, 255, 255), max_li
|
||||
line_spacing = int(fontsize * 1.2)
|
||||
|
||||
# Use the 'standalone' class with 'varwidth' to auto-crop height while restricting width
|
||||
header = LATEX_ANOT_BEFORE.substitute(
|
||||
header = configuration.LATEX_ANOT_BEFORE.substitute(
|
||||
width_in=width_in, fontsize=fontsize, line_spacing=line_spacing
|
||||
)
|
||||
latex_template = f"{header}{text}{LATEX_ANOT_AFTER}"
|
||||
latex_template = f"{header}{text}{configuration.LATEX_ANOT_AFTER}"
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tex_path = os.path.join(temp_dir, 'text.tex')
|
||||
|
||||
@@ -23,7 +23,7 @@ from copienator import (
|
||||
workspace_from_args,
|
||||
)
|
||||
from copienator.annotation_data import load_annotation_data
|
||||
from copienator.annotation_documents import MAX_HEIGHT_PX, MAX_PIXELS, document_fits
|
||||
from copienator.annotation_documents import annotation_limits, document_fits
|
||||
from copienator.commands import annotating, annotating_with_checks
|
||||
from copienator.commands.import_annotations import clear_import_directory
|
||||
from copienator.filesystem import staged_directory
|
||||
@@ -60,15 +60,16 @@ def render_item(item):
|
||||
|
||||
|
||||
def save_batch(batch, prefix, group_id, output_root: Path) -> None:
|
||||
max_height_px, max_pixels = annotation_limits()
|
||||
output_dir = output_root / f"{prefix} G{group_id}"
|
||||
print(f"Generating group PDF: {prefix} G{group_id} ({len(batch)} elements)")
|
||||
max_width = max(item[2].width for item in batch)
|
||||
total_height = sum(item[2].height for item in batch)
|
||||
if not document_fits(max_width, total_height, MAX_HEIGHT_PX, MAX_PIXELS):
|
||||
if not document_fits(max_width, total_height, max_height_px, max_pixels):
|
||||
raise CliError(
|
||||
f"{prefix} G{group_id} would be {max_width} × {total_height} px "
|
||||
f"({max_width * total_height} pixels); document limits are "
|
||||
f"{MAX_HEIGHT_PX} px high and {MAX_PIXELS} pixels."
|
||||
f"{max_height_px} px high and {max_pixels} pixels."
|
||||
)
|
||||
concatenated = Image.new("RGB", (max_width, total_height), "white")
|
||||
draw = ImageDraw.Draw(concatenated)
|
||||
@@ -280,12 +281,13 @@ def _existing_group_state(
|
||||
|
||||
|
||||
def split_batches(rendered):
|
||||
max_height_px, max_pixels = annotation_limits()
|
||||
batches = []
|
||||
heights = []
|
||||
widths = []
|
||||
|
||||
def fits(width, height):
|
||||
return document_fits(width, height, MAX_HEIGHT_PX, MAX_PIXELS)
|
||||
return document_fits(width, height, max_height_px, max_pixels)
|
||||
|
||||
def add_block(items):
|
||||
width = max(item[2].width for item in items)
|
||||
@@ -326,7 +328,7 @@ def split_batches(rendered):
|
||||
f"Copie{student_id} ({item[1]}) is {image.width} × "
|
||||
f"{image.height} px ({image.width * image.height} pixels); "
|
||||
f"a single answer cannot be split. Document limits are "
|
||||
f"{MAX_HEIGHT_PX} px high and {MAX_PIXELS} pixels."
|
||||
f"{max_height_px} px high and {max_pixels} pixels."
|
||||
)
|
||||
if block and not fits(max(width, image.width), height + image.height):
|
||||
add_block(block)
|
||||
|
||||
@@ -26,7 +26,7 @@ from copienator import (
|
||||
workspace_from_target,
|
||||
)
|
||||
from copienator.annotation_data import load_annotation_data
|
||||
from copienator.annotation_documents import MAX_HEIGHT_PX, MAX_PIXELS, copy_documents, document_fits
|
||||
from copienator.annotation_documents import annotation_limits, copy_documents, document_fits
|
||||
from copienator.commands import annotating
|
||||
from copienator.commands.import_annotations import clear_import_directory
|
||||
from copienator.filesystem import staged_directory
|
||||
@@ -160,6 +160,7 @@ def _render_student(
|
||||
output_mode: str,
|
||||
output_root: Path | None = None,
|
||||
) -> str:
|
||||
max_height_px, max_pixels = annotation_limits()
|
||||
output_dir = (
|
||||
output_root
|
||||
if output_root is not None
|
||||
@@ -214,15 +215,15 @@ def _render_student(
|
||||
height = 0
|
||||
width = 0
|
||||
for index, image in enumerate(label_images):
|
||||
if not document_fits(image.width, image.height, MAX_HEIGHT_PX, MAX_PIXELS):
|
||||
if not document_fits(image.width, image.height, max_height_px, max_pixels):
|
||||
raise CliError(
|
||||
f"Copie{student_id} ({bnote_entries[index]['label']}) is "
|
||||
f"{image.width} × {image.height} px ({image.width * image.height} pixels); "
|
||||
f"a single answer cannot be split. Document limits are "
|
||||
f"{MAX_HEIGHT_PX} px high and {MAX_PIXELS} pixels."
|
||||
f"{max_height_px} px high and {max_pixels} pixels."
|
||||
)
|
||||
if not document_fits(max(width, image.width), height + image.height,
|
||||
MAX_HEIGHT_PX, MAX_PIXELS):
|
||||
max_height_px, max_pixels):
|
||||
batches.append((start, index))
|
||||
start = index
|
||||
height = 0
|
||||
@@ -254,13 +255,14 @@ def _save_document(
|
||||
checkbox_groups: list[list[dict[str, Any]]],
|
||||
bnote_entries: list[dict[str, Any]],
|
||||
) -> None:
|
||||
max_height_px, max_pixels = annotation_limits()
|
||||
max_width = max(image.width for image in label_images)
|
||||
total_height = sum(image.height for image in label_images)
|
||||
if not document_fits(max_width, total_height, MAX_HEIGHT_PX, MAX_PIXELS):
|
||||
if not document_fits(max_width, total_height, max_height_px, max_pixels):
|
||||
raise CliError(
|
||||
f"Document would be {max_width} × {total_height} px "
|
||||
f"({max_width * total_height} pixels); document limits are "
|
||||
f"{MAX_HEIGHT_PX} px high and {MAX_PIXELS} pixels."
|
||||
f"{max_height_px} px high and {max_pixels} pixels."
|
||||
)
|
||||
concatenated = Image.new("RGB", (max_width, total_height), "white")
|
||||
checkbox_map: list[dict[str, Any]] = []
|
||||
|
||||
@@ -41,9 +41,6 @@ from copienator.utils import enonce_total, read_all_labels
|
||||
NB_THREADS = 12
|
||||
|
||||
# PROXY_URL = "http://192.168.241.1:3128"
|
||||
MODEL_ID_pro = config.MODEL_PRO_ID
|
||||
MODEL_ID_flash = config.MODEL_FLASH_ID
|
||||
api_key = config.API_KEY
|
||||
|
||||
# Runtime globals retained while the processing helpers are migrated incrementally.
|
||||
INPUT_DIR = Path()
|
||||
@@ -270,13 +267,18 @@ def reset_workspace(workspace: EvaluationWorkspace) -> None:
|
||||
"_new PDFs from 'Par label'."
|
||||
)
|
||||
|
||||
_DEFAULT_FALLBACK = object()
|
||||
|
||||
|
||||
def call_gemini_with_retries(model_id, contents, config,
|
||||
fallback_model_id=MODEL_ID_flash):
|
||||
fallback_model_id=_DEFAULT_FALLBACK):
|
||||
"""Handles requests to Gemini with a 1min and 5min retry mechanism, and quota fallback."""
|
||||
global pro_quota_exhausted
|
||||
delays = [60, 300]
|
||||
request_seed = None
|
||||
# Own the retries here so the SDK cannot silently resend a fixed seed.
|
||||
if fallback_model_id is _DEFAULT_FALLBACK:
|
||||
fallback_model_id = settings.MODEL_FLASH_ID
|
||||
http_options = config.http_options or types.HttpOptions()
|
||||
config = config.model_copy(update={"http_options": http_options.model_copy(
|
||||
update={"retry_options": types.HttpRetryOptions(attempts=1)}
|
||||
@@ -289,7 +291,7 @@ def call_gemini_with_retries(model_id, contents, config,
|
||||
config = config.model_copy(update={"seed": request_seed})
|
||||
tprint(f"\tGemini attempt {attempt + 1}/3: seed={request_seed}")
|
||||
# Switch to fallback immediately if quota was exhausted by another thread
|
||||
if model_id == MODEL_ID_pro and pro_quota_exhausted and fallback_model_id:
|
||||
if model_id == settings.MODEL_PRO_ID and pro_quota_exhausted and fallback_model_id:
|
||||
model_id = fallback_model_id
|
||||
config = config.model_copy(update={"thinking_config": thinking_config(
|
||||
settings.THINKING_LEVEL_FLASH
|
||||
@@ -324,7 +326,7 @@ def call_gemini_with_retries(model_id, contents, config,
|
||||
continue # Retry same model
|
||||
|
||||
# Immediately fallback to Flash without waiting if it's a Pro quota error
|
||||
if is_quota_error and model_id == MODEL_ID_pro and fallback_model_id:
|
||||
if is_quota_error and model_id == settings.MODEL_PRO_ID and fallback_model_id:
|
||||
tprint(f"\tGemini Pro quota hit ({e}). \n\n\tFalling back to Flash permanently...")
|
||||
model_id = fallback_model_id
|
||||
config = config.model_copy(update={"thinking_config": thinking_config(
|
||||
@@ -347,7 +349,7 @@ def correct_boxes_with_gemini(pid, label, pdf_path, original_feedbacks,
|
||||
# pdf_path = COPIES_DIR / f"Copie{pid}" / f"{label}.pdf"
|
||||
|
||||
contents, config = prompting.request_for_box_correction(pdf_path, original_feedbacks)
|
||||
response_text = call_gemini_with_retries(MODEL_ID_flash, contents, config)
|
||||
response_text = call_gemini_with_retries(settings.MODEL_FLASH_ID, contents, config)
|
||||
corrected_feedbacks = parse_correction_response(response_text)
|
||||
|
||||
global_feedbacks = [f for f in original_feedbacks if not f["box_2d"]]
|
||||
@@ -404,7 +406,7 @@ def handle_label_errors(pid, label, res, pdf_path, *, group_geometry=None):
|
||||
if error_type == "wrong-label":
|
||||
tprint(f"\tHandling wrong-label for {pid} {label}")
|
||||
contents, config = prompting.request_for_wrong_label(pdf_path, label, enonce, labels_txt)
|
||||
new_label = call_gemini_with_retries(MODEL_ID_flash, contents, config).strip().strip('"\'')
|
||||
new_label = call_gemini_with_retries(settings.MODEL_FLASH_ID, contents, config).strip().strip('"\'')
|
||||
if new_label not in all_labels:
|
||||
tprint(f"\t\tCopie{pid} returned an incorrect label {new_label} from an initial wrong label {label}. Ignoring")
|
||||
res["error"] = "wrg-lbl:cldtfix"
|
||||
@@ -413,7 +415,7 @@ def handle_label_errors(pid, label, res, pdf_path, *, group_geometry=None):
|
||||
tprint(f"\t\tLabel confirmed for Copie{pid}: regrading {label}.")
|
||||
contents, config = prompting.request_for_confirmed_label(INPUT_DIR, pdf_path, label, pid)
|
||||
response = parse_correction_response(
|
||||
call_gemini_with_retries(MODEL_ID_flash, contents, config)
|
||||
call_gemini_with_retries(settings.MODEL_FLASH_ID, contents, config)
|
||||
)
|
||||
if not isinstance(response, list) or len(response) != 1 or response[0].get("id") != pid:
|
||||
raise ValueError(f"Confirmed-label regrading must return exactly Copie{pid}")
|
||||
@@ -469,7 +471,7 @@ def handle_label_errors(pid, label, res, pdf_path, *, group_geometry=None):
|
||||
contents, config = prompting.request_for_additional_answer(pdf_path, label, enonce, labels_txt)
|
||||
tprint(f"\tHandling additional-answer for {pid} {label}")
|
||||
try:
|
||||
add_labels = json.loads(call_gemini_with_retries(MODEL_ID_flash, contents, config))
|
||||
add_labels = json.loads(call_gemini_with_retries(settings.MODEL_FLASH_ID, contents, config))
|
||||
except CorrectionStopRequested:
|
||||
raise
|
||||
except Exception: # noqa: BLE001 - invalid auxiliary model response
|
||||
@@ -555,7 +557,7 @@ def process_single_task(task_tuple, precomputed_response=None):
|
||||
thinking_level=(settings.THINKING_LEVEL_FLASH if use_flash
|
||||
else settings.THINKING_LEVEL_PRO),
|
||||
)
|
||||
model_to_use = MODEL_ID_flash if use_flash else MODEL_ID_pro
|
||||
model_to_use = settings.MODEL_FLASH_ID if use_flash else settings.MODEL_PRO_ID
|
||||
|
||||
if precomputed_response:
|
||||
tprint(f"Using saved response for: {label} {group_name}")
|
||||
@@ -810,7 +812,7 @@ def run_configured(args: argparse.Namespace) -> ExitCode:
|
||||
global client, tasks_to_process
|
||||
workers = request_workers(args)
|
||||
if client is None:
|
||||
client = genai.Client(api_key=api_key)
|
||||
client = genai.Client(api_key=settings.API_KEY)
|
||||
if args.refaire:
|
||||
refaire_path = INPUT_DIR / "refaire.json"
|
||||
overwritten_path = INPUT_DIR / "overwritten_correction.json"
|
||||
@@ -979,8 +981,8 @@ def run_configured(args: argparse.Namespace) -> ExitCode:
|
||||
)
|
||||
|
||||
print("Batch generation complete.")
|
||||
print(f" - {count_flash} requests saved to {batch_flash_file} (for {MODEL_ID_flash})")
|
||||
print(f" - {count_pro} requests saved to {batch_pro_file} (for {MODEL_ID_pro})")
|
||||
print(f" - {count_flash} requests saved to {batch_flash_file} (for {settings.MODEL_FLASH_ID})")
|
||||
print(f" - {count_pro} requests saved to {batch_pro_file} (for {settings.MODEL_PRO_ID})")
|
||||
print("Upload these files via the File API and create two separate batch jobs.")
|
||||
|
||||
# If there's no live tasks to do, and we aren't doing a batched ingestion, exit right away
|
||||
|
||||
@@ -15,6 +15,7 @@ import pymupdf
|
||||
from pdf2image import convert_from_path
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
from copienator import configuration
|
||||
from copienator import (
|
||||
CliError,
|
||||
EvaluationWorkspace,
|
||||
@@ -27,7 +28,6 @@ from copienator import (
|
||||
)
|
||||
from copienator.filesystem import staged_files
|
||||
from copienator.copy_errors import clear_copy_error, mark_copy_error, marked_copy_paths
|
||||
from copienator.configuration import SHOW_PERSONAL_STEPS
|
||||
from copienator.left_margin import analyse_margins
|
||||
from copienator.name_grids import name_bottom_height_fraction
|
||||
from copienator.annotation_layout import fingerprint
|
||||
@@ -42,8 +42,8 @@ NARROW_PAGE_RATIO = 0.8
|
||||
AUTO_STRIP_WIDTH_FACTOR = 1.10
|
||||
pdf_cache_lock = threading.Lock()
|
||||
|
||||
# The requested spelling is kept as the public switch for this alternate layout.
|
||||
personnal_binding = bool(SHOW_PERSONAL_STEPS)
|
||||
def personal_bindings_enabled() -> bool:
|
||||
return bool(configuration.SHOW_PERSONAL_STEPS)
|
||||
|
||||
|
||||
def distribute_pages(total_pages: int, max_per_file: int = 5) -> list[int]:
|
||||
@@ -316,7 +316,7 @@ class ImageReviewer:
|
||||
self.root.bind("<Right>", lambda _event: self.on_shift(CROP_SHIFT_STEP))
|
||||
self.root.bind("<Up>", lambda _event: self.on_enlarge(CROP_WIDTH_STEP))
|
||||
self.root.bind("<Down>", lambda _event: self.on_enlarge(-CROP_WIDTH_STEP))
|
||||
if personnal_binding:
|
||||
if personal_bindings_enabled():
|
||||
self.root.bind("t", lambda _event: self.on_shift(-CROP_SHIFT_STEP))
|
||||
self.root.bind("n", lambda _event: self.on_shift(CROP_SHIFT_STEP))
|
||||
self.root.bind("r", lambda _event: self.on_enlarge(CROP_WIDTH_STEP))
|
||||
@@ -416,7 +416,7 @@ class ImageReviewer:
|
||||
self.label_img.image = tk_image
|
||||
personal_instructions = (
|
||||
"\nt/s/r/n also available: left / narrower / wider / right"
|
||||
if personnal_binding
|
||||
if personal_bindings_enabled()
|
||||
else ""
|
||||
)
|
||||
analysis = schema.get('auto_margin')
|
||||
|
||||
@@ -3,6 +3,7 @@ import sys
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
|
||||
from copienator import configuration
|
||||
from copienator import (
|
||||
EvaluationWorkspace,
|
||||
ExitCode,
|
||||
@@ -10,7 +11,6 @@ from copienator import (
|
||||
execute,
|
||||
workspace_from_args,
|
||||
)
|
||||
from copienator.configuration import EXPORT_DIR
|
||||
from copienator.annotation_documents import tablet_documents
|
||||
from copienator.platform import replace_with_link_or_copy
|
||||
|
||||
@@ -25,7 +25,7 @@ def export_directory(
|
||||
source_dir = workspace.annotation_dir("refaire") if source_dir_name == "BRnot" else workspace.root / source_dir_name
|
||||
session_id = workspace.refaire_session_id if source_dir_name == "BRnot" else None
|
||||
prefix = f"{session_id}__" if session_id else ""
|
||||
sync_dir = Path(EXPORT_DIR).expanduser() / workspace.name
|
||||
sync_dir = Path(configuration.EXPORT_DIR).expanduser() / workspace.name
|
||||
if session_id:
|
||||
sync_dir /= session_id
|
||||
sync_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -3,7 +3,7 @@ from pathlib import Path
|
||||
import time
|
||||
import webbrowser
|
||||
|
||||
from copienator import evaluation_parser
|
||||
from copienator import configuration, evaluation_parser
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
@@ -14,6 +14,7 @@ def main(argv=None):
|
||||
from copienator.finalisation_server import WebSession
|
||||
session = None
|
||||
try:
|
||||
configuration.use_evaluation(args.evaluation)
|
||||
session = WebSession(Path(args.evaluation).resolve(), args.annotation_dir)
|
||||
print(session.url, flush=True)
|
||||
if not args.no_browser:
|
||||
|
||||
@@ -48,9 +48,6 @@ def get_lcp(s1: str, s2: str) -> str:
|
||||
return lcp
|
||||
|
||||
|
||||
MODEL_ID = config.MODEL_LITE_ID
|
||||
api_key = config.API_KEY
|
||||
|
||||
# --- Modèles pour la Requête 1 ---
|
||||
class StatementContent(BaseModel):
|
||||
@field_validator("question_content", "solution_content", "context_content",
|
||||
@@ -246,7 +243,7 @@ def find_file(folder: Path, base_name: str) -> Path | None:
|
||||
|
||||
def _request_rubrics(client, group_context_text: str):
|
||||
response = client.models.generate_content(
|
||||
model=MODEL_ID,
|
||||
model=config.MODEL_LITE_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}"),
|
||||
@@ -378,13 +375,13 @@ def refine_existing(workspace: EvaluationWorkspace, mode: str, *, api_client=Non
|
||||
questions[label] = "\n".join(parts)
|
||||
context = utils.enonce_total(workspace.root)
|
||||
if api_client is None:
|
||||
if not api_key:
|
||||
if not config.API_KEY:
|
||||
raise CliError("GEMINI_API_KEY is not configured")
|
||||
api_client = genai.Client(api_key=api_key)
|
||||
api_client = genai.Client(api_key=config.API_KEY)
|
||||
|
||||
if mode == "groups":
|
||||
response = api_client.models.generate_content(
|
||||
model=MODEL_ID,
|
||||
model=config.MODEL_LITE_ID,
|
||||
contents=[types.Content(role="user", parts=[types.Part.from_text(text=(
|
||||
"Regroupe ces questions d’examen en groupes cohérents pour la correction "
|
||||
"et l’annotation, selon leurs dépendances et leur contexte commun. "
|
||||
@@ -515,9 +512,9 @@ def process_exam(
|
||||
correction_text = correction_path.read_text(encoding="utf-8")
|
||||
|
||||
if api_client is None:
|
||||
if not api_key:
|
||||
if not config.API_KEY:
|
||||
raise CliError("GEMINI_API_KEY is not configured")
|
||||
api_client = genai.Client(api_key=api_key)
|
||||
api_client = genai.Client(api_key=config.API_KEY)
|
||||
client = api_client
|
||||
|
||||
# ==========================================
|
||||
@@ -543,7 +540,7 @@ def process_exam(
|
||||
|
||||
questions_data = ExamQuestions(questions=extract_stage(
|
||||
folder, 'questions', QuestionOnlyItem, 'question_content',
|
||||
lambda _pending: client.models.generate_content(model=MODEL_ID, contents=contents_1, config=config_1),
|
||||
lambda _pending: client.models.generate_content(model=config.MODEL_LITE_ID, contents=contents_1, config=config_1),
|
||||
))
|
||||
|
||||
# ==========================================
|
||||
@@ -567,7 +564,7 @@ def process_exam(
|
||||
types.Part.from_text(text='--- QUESTIONS EXTRAITES ---\n' + selected.model_dump_json(indent=2)),
|
||||
types.Part.from_text(text=f'--- CORRECTION SOURCE ({correction_path.name}) ---\n{correction_text}'),
|
||||
])]
|
||||
return client.models.generate_content(model=MODEL_ID, contents=contents, config=config_2)
|
||||
return client.models.generate_content(model=config.MODEL_LITE_ID, contents=contents, config=config_2)
|
||||
solutions_data = ExamSolutions(solutions=extract_stage(
|
||||
folder, 'solutions', SolutionOnlyItem, 'solution_content', request_solutions, labels=question_labels,
|
||||
))
|
||||
@@ -599,7 +596,7 @@ def process_exam(
|
||||
contents = [types.Content(role='user', parts=[*contents_3[0].parts,
|
||||
types.Part.from_text(text='Produis des contextes UNIQUEMENT pour les labels cibles suivants : '
|
||||
+ ', '.join(pending))])]
|
||||
return client.models.generate_content(model=MODEL_ID, contents=contents, config=config_3)
|
||||
return client.models.generate_content(model=config.MODEL_LITE_ID, contents=contents, config=config_3)
|
||||
context_data = ExamContext(contexts=extract_stage(
|
||||
folder, 'contexts', ExtractedContext, 'context_content', request_contexts, labels=question_labels,
|
||||
))
|
||||
@@ -1083,7 +1080,7 @@ def compile_existing_pdfs(workspace: EvaluationWorkspace) -> ExitCode:
|
||||
if not source_path.is_file():
|
||||
source_path = None
|
||||
fingerprint = hashlib.sha256((utils.fragment_latex_header(source_path)
|
||||
+ utils.LATEX_AFTER + str(source_path)).encode()).hexdigest()
|
||||
+ config.LATEX_AFTER + str(source_path)).encode()).hexdigest()
|
||||
status = json.loads(status_path.read_text()) if status_path.exists() else {}
|
||||
failed_before = not status.get('complete') or status.get('preamble_hash') != fingerprint
|
||||
if (pdf_path.is_file() and pdf_path.stat().st_size > 0
|
||||
|
||||
@@ -31,8 +31,6 @@ from copienator import (
|
||||
)
|
||||
from copienator.utils import natural_key, read_all_labels
|
||||
|
||||
MODEL_ID = config.MODEL_FOR_LABEL_ID
|
||||
api_key = config.API_KEY
|
||||
REQUEST_TIMEOUT_SECONDS = 80
|
||||
|
||||
my_prompt = """I'm giving you an image of the left columns of a written exam.
|
||||
@@ -583,7 +581,7 @@ def process_copy_group(
|
||||
known_name=known_name,
|
||||
)
|
||||
response = client.models.generate_content(
|
||||
model=MODEL_ID,
|
||||
model=config.MODEL_FOR_LABEL_ID,
|
||||
contents=contents,
|
||||
config=request_config,
|
||||
)
|
||||
@@ -720,7 +718,7 @@ def run(
|
||||
line.strip() for line in names_text.splitlines() if line.strip()
|
||||
} | {"Unknown", "Continued"}
|
||||
if client is None:
|
||||
client = genai.Client(api_key=api_key)
|
||||
client = genai.Client(api_key=config.API_KEY)
|
||||
|
||||
groups = group_images(images)
|
||||
pending_groups = {key: files for key, files in groups.items()
|
||||
@@ -828,7 +826,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
|
||||
def handle(args: argparse.Namespace) -> ExitCode:
|
||||
workspace, target = workspace_from_target(
|
||||
args, repository=Path(__file__).resolve().parents[2]
|
||||
args, repository=Path.cwd()
|
||||
)
|
||||
targets = [target]
|
||||
for additional in args.additional_targets:
|
||||
|
||||
@@ -16,15 +16,15 @@ from copienator import configuration
|
||||
from copienator.annotation_documents import tablet_documents
|
||||
|
||||
ANNOTATION_DIRECTORIES = ("BGnot", "Bnot", "Anot")
|
||||
IMPORT_DIR = getattr(configuration, "IMPORT_DIR", None)
|
||||
|
||||
|
||||
def clear_import_directory(workspace: EvaluationWorkspace) -> None:
|
||||
"""Empty the configured import folder while preserving the folder itself."""
|
||||
if IMPORT_DIR is None or not str(IMPORT_DIR).strip():
|
||||
import_dir = configuration.IMPORT_DIR
|
||||
if import_dir is None or not str(import_dir).strip():
|
||||
print("Aucun dossier d’import configuré ; aucun fichier supprimé.")
|
||||
return
|
||||
directory = Path(IMPORT_DIR).expanduser().resolve()
|
||||
directory = Path(import_dir).expanduser().resolve()
|
||||
if workspace.root.resolve().is_relative_to(directory) or Path.cwd().resolve().is_relative_to(directory):
|
||||
raise CliError(f"Le dossier d’import ne peut pas contenir l’évaluation ou le projet : {directory}",
|
||||
ExitCode.INVALID_ARGUMENTS)
|
||||
@@ -112,7 +112,7 @@ def run(
|
||||
return sync_annotated(
|
||||
workspace,
|
||||
annotation_dir_name="BRnot" if refaire else annotation_dir,
|
||||
import_dir=Path(IMPORT_DIR),
|
||||
import_dir=Path(configuration.IMPORT_DIR),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import pymupdf # PyMuPDF
|
||||
from PIL import Image, ImageDraw, ImageTk
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
from copienator.configuration import PAGE_SPLITTER_KB
|
||||
from copienator import configuration
|
||||
from copienator import (
|
||||
CliError,
|
||||
EvaluationWorkspace,
|
||||
@@ -29,14 +29,6 @@ from copienator.platform import launch_pdf_arranger
|
||||
from copienator.copy_errors import marked_copy_paths, clear_copy_error
|
||||
from copienator.orientation import save_provenance, report_path
|
||||
|
||||
# Keep new shortcuts available with older personal configuration files.
|
||||
PAGE_SPLITTER_KB = {
|
||||
"reverse_pages": "i",
|
||||
"add_secondary_splitter": "<Insert>",
|
||||
"delete_secondary_splitter": "<Delete>",
|
||||
**PAGE_SPLITTER_KB,
|
||||
}
|
||||
|
||||
# --- Constants ---
|
||||
# Conversion factor: 1 cm to points (1 inch = 2.54 cm, 72 points = 1 inch)
|
||||
CM_TO_POINTS = (1 / 2.54) * 72
|
||||
@@ -172,7 +164,7 @@ class PDFPreviewer:
|
||||
self.master.geometry("800x1000")
|
||||
|
||||
def fmt(action):
|
||||
k = PAGE_SPLITTER_KB.get(action, "")
|
||||
k = configuration.PAGE_SPLITTER_KB.get(action, "")
|
||||
return k
|
||||
|
||||
# Dynamic instructions text
|
||||
@@ -216,7 +208,7 @@ class PDFPreviewer:
|
||||
"prev_file": self.go_to_previous_file,
|
||||
}
|
||||
|
||||
for action, key in PAGE_SPLITTER_KB.items():
|
||||
for action, key in configuration.PAGE_SPLITTER_KB.items():
|
||||
if action in action_map:
|
||||
self.master.bind(key, action_map[action])
|
||||
|
||||
|
||||
@@ -420,19 +420,19 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
parser.add_argument(
|
||||
"--return-answers-context",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=configuration.RETURN_ANSWERS_CONTEXT,
|
||||
default=None,
|
||||
help="Include applicable context pages in individual answer exports",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--return-answers-question",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=configuration.RETURN_ANSWERS_QUESTION,
|
||||
default=None,
|
||||
help="Include the current question PDF in individual answer exports",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--return-answers-solution",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=configuration.RETURN_ANSWERS_SOLUTION,
|
||||
default=None,
|
||||
help="Include the current solution PDF in individual answer exports",
|
||||
)
|
||||
return parser
|
||||
|
||||
@@ -750,19 +750,19 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
parser.add_argument(
|
||||
"--return-answers-context",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=configuration.RETURN_ANSWERS_CONTEXT,
|
||||
default=None,
|
||||
help="Include applicable context pages in individual answer exports",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--return-answers-question",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=configuration.RETURN_ANSWERS_QUESTION,
|
||||
default=None,
|
||||
help="Include the current question PDF in individual answer exports",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--return-answers-solution",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=configuration.RETURN_ANSWERS_SOLUTION,
|
||||
default=None,
|
||||
help="Include the current solution PDF in individual answer exports",
|
||||
)
|
||||
return parser
|
||||
|
||||
@@ -6,11 +6,10 @@ from pathlib import Path
|
||||
|
||||
import ezodf
|
||||
|
||||
from copienator.configuration import CURRENT_SCORE_ODS_PATH
|
||||
from copienator import configuration
|
||||
from copienator.utils import read_all_labels
|
||||
|
||||
# Configuration
|
||||
ODS_PATH = Path(CURRENT_SCORE_ODS_PATH).expanduser()
|
||||
TARGET_DIR_NAME = "A Rendre"
|
||||
|
||||
def main(argv=None):
|
||||
@@ -20,6 +19,8 @@ def main(argv=None):
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
work_dir = os.path.abspath(args.work_dir)
|
||||
configuration.use_evaluation(work_dir)
|
||||
ods_path = Path(configuration.CURRENT_SCORE_ODS_PATH).expanduser()
|
||||
|
||||
all_labels = read_all_labels(Path(work_dir))
|
||||
|
||||
@@ -29,13 +30,13 @@ def main(argv=None):
|
||||
print(f"Error: Directory '{TARGET_DIR_NAME}' not found in {work_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
if not os.path.exists(ODS_PATH):
|
||||
print(f"Error: ODS file not found at {ODS_PATH}")
|
||||
if not os.path.exists(ods_path):
|
||||
print(f"Error: ODS file not found at {ods_path}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Opening ODS file: {ODS_PATH}...")
|
||||
print(f"Opening ODS file: {ods_path}...")
|
||||
try:
|
||||
doc = ezodf.opendoc(str(ODS_PATH))
|
||||
doc = ezodf.opendoc(str(ods_path))
|
||||
except Exception as e:
|
||||
print(f"Failed to open ODS: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
Reference in new issue
Block a user