Support evaluation-local configuration and names overrides

This commit is contained in:
sebastien committed 2026-11-03 18:44:56 +01:00
1 parent 0bb6f7d97c
commit d4404e4c27
36 files changed
+409 -184

No files matched your search

+8 -16
View File
@@ -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
+3 -3
View File
@@ -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')
+7 -5
View File
@@ -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]] = []
+16 -14
View File
@@ -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
+5 -5
View File
@@ -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')
+2 -2
View File
@@ -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)
+2 -1
View File
@@ -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:
+10 -13
View File
@@ -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
+3 -5
View File
@@ -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:
+4 -4
View File
@@ -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),
)
+3 -11
View File
@@ -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])
+3 -3
View File
@@ -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
+7 -6
View File
@@ -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)