Windows compatibility
This commit is contained in:
+65
-14
@@ -1,7 +1,7 @@
|
||||
#+title: Script
|
||||
#+author: Sébastien Miquel
|
||||
#+date: 14-03-2026
|
||||
# Time-stamp: <20-08-26 09:59>
|
||||
# Time-stamp: <20-08-26 11:37>
|
||||
#+OPTIONS:
|
||||
|
||||
* Méta
|
||||
@@ -38,6 +38,23 @@ examples du rendu final (dans le sous dossier =BGnot=).
|
||||
Cette situation s'améliorera peut-être, mais faciliter l'utilisation
|
||||
de ce système n'est pas une priorité.
|
||||
|
||||
** Limitations
|
||||
|
||||
Pour l'instant, la correction est faite question par question : le LLM
|
||||
n'a accès qu'à la partie de la copie correspondant à une question
|
||||
fixée. Ça ne gère donc pas les cas où un argument a été donné dans une
|
||||
question précédente ou autre.
|
||||
|
||||
*** Noms de labels sous Windows
|
||||
|
||||
Certains labels sont utilisés directement comme noms de fichiers et de
|
||||
dossiers. Les labels contenant notamment =:= ne sont pas acceptés par
|
||||
Windows. Sous Windows, les scripts refusent ces labels avant de
|
||||
poursuivre et affichent la liste des valeurs à corriger. Aucune
|
||||
conversion silencieuse n'est appliquée ; utiliser par exemple
|
||||
=Ex 1 - a)= au lieu de =Ex 1 : a)=. Le diagnostic du GUI permet de les
|
||||
repérer avant de lancer une étape.
|
||||
|
||||
** Requirements
|
||||
|
||||
*** Python
|
||||
@@ -74,6 +91,34 @@ ou éventuellement, la renseigner directement dans le fichier
|
||||
|
||||
Copier `default_config.py` en `config.py`. Éventuellement le modifier.
|
||||
|
||||
*** Interface graphique
|
||||
|
||||
Lancer l'assistant avec :
|
||||
|
||||
#+BEGIN_SRC bash
|
||||
python gui.py
|
||||
#+END_SRC
|
||||
|
||||
On peut aussi ouvrir directement une évaluation avec
|
||||
=python gui.py Interro=. L'interface conserve l'état et l'historique
|
||||
des étapes dans =Interro/.copienator-gui.json=, et les sorties complètes
|
||||
dans =Interro/.copienator/logs/=. Une relance d'une étape antérieure ne
|
||||
supprime aucun résultat ; les étapes suivantes sont seulement marquées
|
||||
comme étant à revalider.
|
||||
|
||||
La variable =SHOW_PERSONAL_STEPS= de =config.py= permet d'afficher ou de
|
||||
masquer les étapes propres au workflow personnel.
|
||||
|
||||
Le bouton =Diagnostic…= vérifie les modules Python, Poppler, LaTeX,
|
||||
PDF Arranger et la configuration Gemini. Sous Windows, les exécutables
|
||||
externes doivent être accessibles depuis =PATH=. Quand la création de
|
||||
liens symboliques ou physiques n'est pas autorisée, l'export et la
|
||||
préparation de =A Rendre= utilisent automatiquement une copie normale.
|
||||
|
||||
Les chemins des étapes personnelles peuvent être adaptés avec
|
||||
=CURRENT_SCORE_ODS_PATH=, =FINAL_SCORE_ODS_PATH=,
|
||||
=FINAL_SCORE_OUTPUT_DIR= et =FINAL_SCORE_FONT_PATH= dans =config.py=.
|
||||
|
||||
** Correction d'un paquet de copies
|
||||
|
||||
1. Créer un fichier =names= dans le dossier courant, avec les
|
||||
@@ -82,7 +127,12 @@ Copier `default_config.py` en `config.py`. Éventuellement le modifier.
|
||||
suite)
|
||||
3. Suivre les instructions suivantes
|
||||
|
||||
|
||||
* Étapes et Script
|
||||
|
||||
Utiliser `python gui.py` ou `python gui.py Interro` pour lancer un GUI
|
||||
qui suit automatiquement les étapes décrites ci-dessous.
|
||||
|
||||
** Prétraitement de l'énoncé
|
||||
|
||||
Dans le dossier de l'évaluation, mettre les fichiers suivants de l'évaluation :
|
||||
@@ -127,11 +177,11 @@ Dans le dossier de l'évaluation, mettre les fichiers suivants de l'évaluation
|
||||
|
||||
Mettre les copies scannées au format pdf dans =Interro=.
|
||||
|
||||
1. =./rotate_all.sh Interro= (facultatif)
|
||||
1. =python copies_tools.py rotate Interro= (facultatif)
|
||||
|
||||
Retourne tous les pdf de 180°, si la photocopie a été faite à
|
||||
l'envers.
|
||||
2. =./rename_to_copie.sh Interro=
|
||||
2. =python copies_tools.py rename Interro=
|
||||
change le nom des copies en =Copie{id}.pdf=
|
||||
3. =python page_splitter.py Interro=
|
||||
|
||||
@@ -198,7 +248,6 @@ Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
||||
2. =python correction.py Interro --limit 240= OU
|
||||
=python correction.py Interro/Par\ label/Ex\ 2/Group_1.jpg= OU
|
||||
=python correction.py Interro --overwrite=
|
||||
=python correction.py Interro --pro-by-label= (needs `labels_for_pro`)
|
||||
|
||||
Fais les requêtes de correction à Gemini.
|
||||
|
||||
@@ -259,20 +308,20 @@ OU
|
||||
3. =python export.py Interro= (gestion perso)
|
||||
Cela déplace les groupes dans le dossier configuré par =EXPORT_DIR=
|
||||
(par défaut =Export=).
|
||||
- Les mettre dans le dossier racine de la tablette, et renommer en =aaa=.
|
||||
- Vider =Syncthing/Annotées= sur la tablette et localement.
|
||||
À automatiser, aussi c'est lent…
|
||||
|
||||
Il faut ensuite annoter les fichiers dans `EXPORT_DIR` avec une
|
||||
tablette graphique.
|
||||
|
||||
** Lecture de la correction manuscrite
|
||||
|
||||
1. =python import.py Interro= (gestion perso)
|
||||
_Before_ : vider le dossier configuré par =IMPORT_DIR= (par défaut
|
||||
=Import=), puis y copier ou synchroniser les fichiers depuis la tablette.
|
||||
|
||||
_Before_ : vider le dossier configuré par =IMPORT_DIR= (par défaut
|
||||
=Import=), puis y copier ou synchroniser les fichiers depuis la tablette.
|
||||
1. =python import.py Interro=
|
||||
|
||||
Une fois les corrections manuelles appliquées aux fichiers
|
||||
=Concat.pdf=, il faut enregistrer le fichier annoté au même endroit,
|
||||
sous le nom =Concat_annotated.pdf=.
|
||||
Une fois les corrections manuelles appliquées aux fichiers
|
||||
=Concat.pdf=, il faut enregistrer le fichier annoté au même endroit,
|
||||
sous le nom =Concat_annotated.pdf=.
|
||||
|
||||
2. =python reading_annotations.py Interro=
|
||||
|
||||
@@ -290,8 +339,9 @@ OU
|
||||
+ un fichier =score.json= qui contient les notes par question
|
||||
|
||||
Si un nom est =Unknown= : renommer à la main le dossier et le fichier dedans.
|
||||
4. Éventuellement, faire des modifications manuelles aux =score.json=.
|
||||
|
||||
4. On peut faire des changements manuels aux =score.json= ici, puis
|
||||
Puis
|
||||
- `python reading_annotations.py --update-score Interro`
|
||||
- `python reading_grouped_annotations.py --update-score Interro`
|
||||
pour mettre à jour les scores dans les images.
|
||||
@@ -309,6 +359,7 @@ OU
|
||||
+ update the copies from =miqmacs.fr/admin=.
|
||||
6. (gestion perso) Impression d'une copie. Via Evince » print to pdf.
|
||||
|
||||
* Autres
|
||||
** Recorrection d'une seule copie (peu testé)
|
||||
|
||||
!! Attention, refaire ne marchera pas si tu fais une annotation non
|
||||
|
||||
+27
-18
@@ -1,16 +1,29 @@
|
||||
import argparse
|
||||
import math
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from config import FINAL_SCORE_FONT_PATH, FINAL_SCORE_ODS_PATH, FINAL_SCORE_OUTPUT_DIR
|
||||
|
||||
# Configuration constants
|
||||
ODS_PATH = "/home/sebastien/Rust/gestion_classe/Staging/simple_eval.ods"
|
||||
OUTPUT_DIR = Path("/home/sebastien/Rust/Server/copies")
|
||||
FONT_PATH = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" # Standard Linux font path
|
||||
ODS_PATH = Path(FINAL_SCORE_ODS_PATH).expanduser()
|
||||
OUTPUT_DIR = Path(FINAL_SCORE_OUTPUT_DIR).expanduser()
|
||||
|
||||
|
||||
def score_font(size):
|
||||
candidates = [FINAL_SCORE_FONT_PATH, "DejaVuSans-Bold.ttf", "arial.ttf"]
|
||||
for candidate in candidates:
|
||||
if not candidate:
|
||||
continue
|
||||
try:
|
||||
return ImageFont.truetype(str(candidate), size)
|
||||
except OSError:
|
||||
continue
|
||||
return ImageFont.load_default()
|
||||
|
||||
def get_rounded_score(score):
|
||||
"""Round score to one decimal place below (floor)."""
|
||||
@@ -20,7 +33,7 @@ def get_rounded_score(score):
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
def process_images(base_dir):
|
||||
def process_images(base_dir, output_dir):
|
||||
# 1. Load Data
|
||||
try:
|
||||
# header=None assumes the file starts directly with data.
|
||||
@@ -33,7 +46,7 @@ def process_images(base_dir):
|
||||
sys.exit(1)
|
||||
|
||||
# 2. Prepare Output Directory
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 3. Iterate Files
|
||||
# Structure: Dir/A Rendre/{name}/{name}.jpg
|
||||
@@ -68,12 +81,7 @@ def process_images(base_dir):
|
||||
# Dynamic font size (15% of image height)
|
||||
font_size = int(width * 0.08)
|
||||
|
||||
try:
|
||||
font = ImageFont.truetype(FONT_PATH, font_size)
|
||||
except IOError:
|
||||
# Fallback if specific font not found
|
||||
font = ImageFont.load_default()
|
||||
print(f"Warning: System font not found, using default for {student_name}")
|
||||
font = score_font(font_size)
|
||||
|
||||
text = str(score)
|
||||
|
||||
@@ -90,7 +98,7 @@ def process_images(base_dir):
|
||||
draw.text((x, y), text, fill=(255, 0, 0), font=font)
|
||||
|
||||
# Save
|
||||
save_path = OUTPUT_DIR / f"{student_name}.jpg"
|
||||
save_path = output_dir / f"{student_name}.jpg"
|
||||
img.save(save_path)
|
||||
print(f"Processed: {student_name} -> {score}")
|
||||
|
||||
@@ -99,7 +107,7 @@ def process_images(base_dir):
|
||||
|
||||
for pdf_path in sorted(search_path.glob("*/*.pdf")):
|
||||
student_name = pdf_path.stem # Filename without extension
|
||||
save_path = OUTPUT_DIR / f"{student_name}.pdf"
|
||||
save_path = output_dir / f"{student_name}.pdf"
|
||||
|
||||
shutil.copy(str(pdf_path), str(save_path))
|
||||
|
||||
@@ -111,6 +119,7 @@ if __name__ == "__main__":
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
OUTPUT_DIR = OUTPUT_DIR / args.dir
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
process_images(args.dir)
|
||||
base_dir = args.dir.expanduser().resolve()
|
||||
output_dir = OUTPUT_DIR / base_dir.name
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
process_images(base_dir, output_dir)
|
||||
|
||||
@@ -103,6 +103,7 @@ def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
root_dir = args.input_path
|
||||
validated_labels = utils.read_all_labels(root_dir)
|
||||
results = annotating.make_dictionary(root_dir)
|
||||
label_groups = os.path.join(root_dir, "label_groups")
|
||||
all_labels = os.path.join(root_dir, "labels")
|
||||
@@ -113,8 +114,7 @@ def main():
|
||||
print(f"Error: {all_labels} not found.")
|
||||
sys.exit(1)
|
||||
|
||||
with open(all_labels, 'r') as f:
|
||||
lines = [l.strip() for l in f if l.strip()]
|
||||
lines = validated_labels
|
||||
|
||||
groups = {}
|
||||
for line in lines:
|
||||
|
||||
@@ -13,6 +13,7 @@ matplotlib.use('Agg')
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import annotating
|
||||
import utils
|
||||
from annotating import MARGIN_LEFT, ANNOT_WIDTH
|
||||
|
||||
# Global lock for Matplotlib/Latex rendering to prevent race conditions
|
||||
@@ -230,6 +231,9 @@ if __name__ == "__main__":
|
||||
else:
|
||||
root_dir = input_path
|
||||
|
||||
if os.path.exists(os.path.join(root_dir, "labels")):
|
||||
utils.read_all_labels(root_dir)
|
||||
|
||||
if not args.refaire:
|
||||
results = annotating.make_dictionary(root_dir)
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ from pathlib import Path
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
from typing import Any
|
||||
|
||||
from platform_utils import WindowsLabelError, validate_windows_labels
|
||||
|
||||
from .diagnostics import collect_diagnostics
|
||||
from .runner import ProcessRunner
|
||||
from .state import StateStore
|
||||
from .workflow import (
|
||||
@@ -42,6 +45,7 @@ class CopienatorApp(tk.Tk):
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.repository = repository.resolve()
|
||||
self.show_personal_steps = show_personal_steps
|
||||
self.steps = build_workflow(show_personal_steps)
|
||||
self.step_by_id = {step.id: step for step in self.steps}
|
||||
self.state_store = StateStore()
|
||||
@@ -93,6 +97,7 @@ class CopienatorApp(tk.Tk):
|
||||
path_entry.bind("<Return>", lambda _event: self._load_evaluation())
|
||||
ttk.Button(top, text="Parcourir…", command=self._browse_evaluation).grid(row=0, column=2, padx=6)
|
||||
ttk.Button(top, text="Charger", command=self._load_evaluation).grid(row=0, column=3)
|
||||
ttk.Button(top, text="Diagnostic…", command=self._show_diagnostics).grid(row=0, column=4, padx=(6, 0))
|
||||
|
||||
ttk.Label(top, text="Clé Gemini").grid(row=1, column=0, sticky="w", padx=(0, 8), pady=(7, 0))
|
||||
ttk.Entry(top, textvariable=self.api_key_var, show="•", width=32).grid(
|
||||
@@ -194,6 +199,47 @@ class CopienatorApp(tk.Tk):
|
||||
status = ttk.Label(self, textvariable=self.info_var, anchor="w", relief="sunken", padding=(6, 3))
|
||||
status.grid(row=3, column=0, sticky="ew")
|
||||
|
||||
def _show_diagnostics(self) -> None:
|
||||
checks = collect_diagnostics(
|
||||
self.show_personal_steps, self.api_key_var.get(), self.evaluation
|
||||
)
|
||||
dialog = tk.Toplevel(self)
|
||||
dialog.title("Diagnostic Copienator")
|
||||
dialog.geometry("760x480")
|
||||
dialog.transient(self)
|
||||
dialog.columnconfigure(0, weight=1)
|
||||
dialog.rowconfigure(1, weight=1)
|
||||
|
||||
required_missing = sum(1 for check in checks if check.required and not check.ok)
|
||||
summary = (
|
||||
"Tous les prérequis obligatoires sont disponibles."
|
||||
if required_missing == 0
|
||||
else f"{required_missing} prérequis obligatoire(s) manquant(s)."
|
||||
)
|
||||
ttk.Label(dialog, text=summary, padding=10, font=("TkDefaultFont", 11, "bold")).grid(
|
||||
row=0, column=0, sticky="w"
|
||||
)
|
||||
|
||||
tree = ttk.Treeview(dialog, columns=("state", "need", "detail"), show="headings")
|
||||
tree.heading("state", text="État")
|
||||
tree.heading("need", text="Niveau")
|
||||
tree.heading("detail", text="Composant et détail")
|
||||
tree.column("state", width=85, anchor="center")
|
||||
tree.column("need", width=100, anchor="center")
|
||||
tree.column("detail", width=530)
|
||||
for check in checks:
|
||||
tree.insert(
|
||||
"",
|
||||
"end",
|
||||
values=(
|
||||
"OK" if check.ok else "Manquant",
|
||||
"Obligatoire" if check.required else "Optionnel",
|
||||
f"{check.name} — {check.detail}",
|
||||
),
|
||||
)
|
||||
tree.grid(row=1, column=0, sticky="nsew", padx=10)
|
||||
ttk.Button(dialog, text="Fermer", command=dialog.destroy).grid(row=2, column=0, pady=10)
|
||||
|
||||
def _browse_evaluation(self) -> None:
|
||||
selected = filedialog.askdirectory(initialdir=self.evaluation_var.get() or self.repository)
|
||||
if selected:
|
||||
@@ -482,6 +528,19 @@ class CopienatorApp(tk.Tk):
|
||||
if step.is_manual:
|
||||
self._mark_step("success")
|
||||
return
|
||||
if os.name == "nt" and step.id != "statement":
|
||||
labels_path = evaluation / "labels"
|
||||
if labels_path.is_file():
|
||||
labels = [
|
||||
line.strip()
|
||||
for line in labels_path.read_text(encoding="utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
try:
|
||||
validate_windows_labels(labels)
|
||||
except WindowsLabelError as exc:
|
||||
messagebox.showerror("Labels incompatibles avec Windows", str(exc))
|
||||
return
|
||||
if not self._validate_arguments():
|
||||
return
|
||||
missing = self._missing_requirements(step)
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from platform_utils import windows_filename_problems
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DiagnosticCheck:
|
||||
name: str
|
||||
ok: bool
|
||||
detail: str
|
||||
required: bool = True
|
||||
|
||||
|
||||
def module_available(module: str) -> bool:
|
||||
try:
|
||||
return importlib.util.find_spec(module) is not None
|
||||
except (ImportError, ModuleNotFoundError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def collect_diagnostics(
|
||||
show_personal_steps: bool,
|
||||
api_key_override: str = "",
|
||||
evaluation: Path | None = None,
|
||||
) -> list[DiagnosticCheck]:
|
||||
checks = [
|
||||
DiagnosticCheck(
|
||||
"Système",
|
||||
os.name == "nt" or sys.platform.startswith("linux"),
|
||||
f"{sys.platform} — Linux et Windows sont pris en charge",
|
||||
),
|
||||
DiagnosticCheck("Python", True, sys.executable),
|
||||
]
|
||||
|
||||
modules = {
|
||||
"numpy": "numpy",
|
||||
"pandas": "pandas",
|
||||
"matplotlib": "matplotlib",
|
||||
"Pillow": "PIL",
|
||||
"pydantic": "pydantic",
|
||||
"pypdf": "pypdf",
|
||||
"pdf2image": "pdf2image",
|
||||
"reportlab": "reportlab",
|
||||
"img2pdf": "img2pdf",
|
||||
"PyMuPDF": "fitz",
|
||||
"ftfy": "ftfy",
|
||||
"ezodf": "ezodf",
|
||||
"Google GenAI": "google.genai",
|
||||
}
|
||||
checks.extend(
|
||||
DiagnosticCheck(label, module_available(module), f"Module Python : {module}")
|
||||
for label, module in modules.items()
|
||||
)
|
||||
|
||||
programs = (
|
||||
("Poppler", ("pdftoppm", "pdftocairo"), True),
|
||||
("LaTeX", ("pdflatex",), True),
|
||||
("PDF Arranger", ("pdf-arranger", "pdfarranger"), False),
|
||||
)
|
||||
for label, candidates, required in programs:
|
||||
executable = next((shutil.which(candidate) for candidate in candidates if shutil.which(candidate)), None)
|
||||
detail = executable or "Introuvable dans PATH"
|
||||
checks.append(DiagnosticCheck(label, executable is not None, detail, required))
|
||||
|
||||
api_key = api_key_override.strip() or os.environ.get("GEMINI_API_KEY")
|
||||
checks.append(
|
||||
DiagnosticCheck(
|
||||
"Clé Gemini",
|
||||
bool(api_key),
|
||||
"GEMINI_API_KEY est définie" if api_key else "À saisir dans le GUI ou dans l’environnement",
|
||||
)
|
||||
)
|
||||
|
||||
if evaluation and (evaluation / "labels").is_file():
|
||||
labels = [
|
||||
line.strip()
|
||||
for line in (evaluation / "labels").read_text(encoding="utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
unsafe = [label for label in labels if windows_filename_problems(label)]
|
||||
detail = (
|
||||
"Tous les labels sont utilisables comme noms de fichiers"
|
||||
if not unsafe
|
||||
else "Caractères Windows interdits : " + ", ".join(unsafe[:3])
|
||||
)
|
||||
if len(unsafe) > 3:
|
||||
detail += f" (+{len(unsafe) - 3})"
|
||||
checks.append(
|
||||
DiagnosticCheck(
|
||||
"Labels compatibles Windows",
|
||||
not unsafe,
|
||||
detail,
|
||||
os.name == "nt",
|
||||
)
|
||||
)
|
||||
|
||||
if show_personal_steps:
|
||||
checks.append(
|
||||
DiagnosticCheck(
|
||||
"gestion_classe",
|
||||
shutil.which("gestion_classe") is not None,
|
||||
shutil.which("gestion_classe") or "Introuvable dans PATH",
|
||||
False,
|
||||
)
|
||||
)
|
||||
try:
|
||||
from config import CURRENT_SCORE_ODS_PATH, FINAL_SCORE_ODS_PATH
|
||||
|
||||
for label, configured_path in (
|
||||
("ODS courant", CURRENT_SCORE_ODS_PATH),
|
||||
("ODS final", FINAL_SCORE_ODS_PATH),
|
||||
):
|
||||
path = Path(configured_path).expanduser()
|
||||
checks.append(DiagnosticCheck(label, path.is_file(), str(path), False))
|
||||
except ImportError:
|
||||
checks.append(DiagnosticCheck("Configuration personnelle", False, "Variables absentes", False))
|
||||
|
||||
return checks
|
||||
@@ -30,6 +30,7 @@ class CommandVariant:
|
||||
program: str | None
|
||||
kind: str = "python" # python, shell, external, manual
|
||||
fixed_args: tuple[str, ...] = ()
|
||||
fixed_args_before_positionals: bool = False
|
||||
dangerous: bool = False
|
||||
|
||||
|
||||
@@ -116,7 +117,16 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"Prétraitement des copies",
|
||||
"Retourner toutes les copies",
|
||||
"Rotation facultative de 180° lorsque les scans sont à l’envers.",
|
||||
(CommandVariant("rotate", "Rotation", "rotate_all.sh", "shell"),),
|
||||
(
|
||||
CommandVariant(
|
||||
"rotate",
|
||||
"Rotation",
|
||||
"copies_tools.py",
|
||||
"python",
|
||||
("rotate",),
|
||||
fixed_args_before_positionals=True,
|
||||
),
|
||||
),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
optional=True,
|
||||
),
|
||||
@@ -125,7 +135,16 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"Prétraitement des copies",
|
||||
"Renommer les PDF en CopieXX.pdf",
|
||||
"Renomme les PDF du dossier d’évaluation dans leur ordre courant.",
|
||||
(CommandVariant("rename", "Renommage", "rename_to_copie.sh", "shell"),),
|
||||
(
|
||||
CommandVariant(
|
||||
"rename",
|
||||
"Renommage",
|
||||
"copies_tools.py",
|
||||
"python",
|
||||
("rename",),
|
||||
fixed_args_before_positionals=True,
|
||||
),
|
||||
),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
),
|
||||
StepDefinition(
|
||||
@@ -464,8 +483,11 @@ def build_command(
|
||||
elif spec.flag:
|
||||
options.extend((spec.flag, rendered))
|
||||
|
||||
if variant.fixed_args_before_positionals:
|
||||
command.extend(variant.fixed_args)
|
||||
command.extend(positionals)
|
||||
command.extend(variant.fixed_args)
|
||||
if not variant.fixed_args_before_positionals:
|
||||
command.extend(variant.fixed_args)
|
||||
command.extend(options)
|
||||
if extra_arguments.strip():
|
||||
command.extend(shlex.split(extra_arguments, posix=os.name != "nt"))
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
|
||||
def copy_pdfs(directory: Path) -> list[Path]:
|
||||
directory = directory.expanduser().resolve()
|
||||
if not directory.is_dir():
|
||||
raise NotADirectoryError(f"Dossier introuvable : {directory}")
|
||||
return sorted(
|
||||
(
|
||||
path
|
||||
for path in directory.glob("*.pdf")
|
||||
if path.name.casefold() not in {"enonce.pdf", "énoncé.pdf"}
|
||||
),
|
||||
key=lambda path: path.name.casefold(),
|
||||
)
|
||||
|
||||
|
||||
def rotate_pdf(path: Path) -> None:
|
||||
temporary = path.with_name(f".{path.stem}.rotate-{uuid.uuid4().hex}.pdf")
|
||||
try:
|
||||
with path.open("rb") as source, temporary.open("wb") as destination:
|
||||
reader = PdfReader(source)
|
||||
writer = PdfWriter()
|
||||
for page in reader.pages:
|
||||
writer.add_page(page.rotate(180))
|
||||
if reader.metadata:
|
||||
metadata = {
|
||||
str(key): str(value)
|
||||
for key, value in reader.metadata.items()
|
||||
if value is not None
|
||||
}
|
||||
writer.add_metadata(metadata)
|
||||
writer.write(destination)
|
||||
temporary.replace(path)
|
||||
finally:
|
||||
if temporary.exists():
|
||||
temporary.unlink()
|
||||
|
||||
|
||||
def rotate_all(directory: Path) -> int:
|
||||
files = copy_pdfs(directory)
|
||||
for path in files:
|
||||
rotate_pdf(path)
|
||||
print(f"Rotated: {path.name}")
|
||||
if not files:
|
||||
print("No PDF copies found.")
|
||||
return len(files)
|
||||
|
||||
|
||||
def rename_all(directory: Path) -> list[tuple[Path, Path]]:
|
||||
directory = directory.expanduser().resolve()
|
||||
files = copy_pdfs(directory)
|
||||
width = max(2, len(str(len(files))))
|
||||
plan = [
|
||||
(source, directory / f"Copie{index:0{width}d}.pdf")
|
||||
for index, source in enumerate(files, start=1)
|
||||
]
|
||||
staged: list[tuple[Path, Path, Path]] = []
|
||||
|
||||
try:
|
||||
for source, destination in plan:
|
||||
temporary = directory / f".copienator-rename-{uuid.uuid4().hex}.pdf"
|
||||
source.replace(temporary)
|
||||
staged.append((source, temporary, destination))
|
||||
|
||||
completed: list[tuple[Path, Path, Path]] = []
|
||||
try:
|
||||
for source, temporary, destination in staged:
|
||||
temporary.replace(destination)
|
||||
completed.append((source, temporary, destination))
|
||||
print(f"Renamed: {source.name} -> {destination.name}")
|
||||
except OSError:
|
||||
for _source, temporary, destination in completed:
|
||||
if destination.exists():
|
||||
destination.replace(temporary)
|
||||
raise
|
||||
except OSError:
|
||||
for source, temporary, _destination in staged:
|
||||
if temporary.exists():
|
||||
temporary.replace(source)
|
||||
raise
|
||||
|
||||
if not plan:
|
||||
print("No PDF copies found.")
|
||||
return [(source, destination) for source, _temporary, destination in staged]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Prepare scanned PDF copies.")
|
||||
subparsers = parser.add_subparsers(dest="operation", required=True)
|
||||
for operation in ("rotate", "rename"):
|
||||
subparser = subparsers.add_parser(operation)
|
||||
subparser.add_argument("directory", type=Path)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.operation == "rotate":
|
||||
rotate_all(args.directory)
|
||||
else:
|
||||
rename_all(args.directory)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -6,6 +6,15 @@ API_KEY = os.environ.get("GEMINI_API_KEY")
|
||||
EXPORT_DIR = Path("Export")
|
||||
IMPORT_DIR = Path("Import")
|
||||
|
||||
# Les étapes gestion_classe, ODS et publication sont masquées par défaut.
|
||||
SHOW_PERSONAL_STEPS = False
|
||||
|
||||
# Chemins utilisés uniquement par les étapes personnelles.
|
||||
CURRENT_SCORE_ODS_PATH = Path("current_eval.ods")
|
||||
FINAL_SCORE_ODS_PATH = Path("simple_eval.ods")
|
||||
FINAL_SCORE_OUTPUT_DIR = Path("Server") / "copies"
|
||||
FINAL_SCORE_FONT_PATH = None
|
||||
|
||||
# Modèle pour des choses très légères
|
||||
MODEL_LITE_ID = "gemini-3.5-flash-lite"
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import subprocess
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
from platform_utils import WindowsLabelError, validate_windows_labels
|
||||
from utils import compile_to_pdf
|
||||
|
||||
def fetch_and_save_sub_text(ex_id, indices, label, text_path):
|
||||
@@ -191,6 +192,7 @@ def process_directory(directory):
|
||||
indexes = data.get('indexes', [])
|
||||
if not indexes:
|
||||
label = f"Ex {current_ex_num}"
|
||||
validate_windows_labels([label])
|
||||
f_labels.write(f"{label}\n")
|
||||
fetch_and_save_sub_text(ids, [], label, paths['Text2'])
|
||||
fetch_and_save_sub_sol(ids, [], label, paths['Sol2'])
|
||||
@@ -198,6 +200,7 @@ def process_directory(directory):
|
||||
for item in indexes:
|
||||
suffix = format_indices(item['indices'], problem)
|
||||
label = f"Ex {current_ex_num}" + (f" : {suffix}" if suffix else "")
|
||||
validate_windows_labels([label])
|
||||
f_labels.write(f"{label}\n")
|
||||
fetch_and_save_sub_text(ids, item['indices'], label, paths['Text2'])
|
||||
fetch_and_save_sub_sol(ids, item['indices'], label, paths['Sol2'])
|
||||
@@ -243,6 +246,8 @@ def process_directory(directory):
|
||||
|
||||
current_ex_num += 1
|
||||
|
||||
except WindowsLabelError:
|
||||
raise
|
||||
except json.JSONDecodeError:
|
||||
print(f"Error decoding JSON in block: {json_str}")
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,52 +1,36 @@
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from config import EXPORT_DIR
|
||||
from platform_utils import replace_with_link_or_copy
|
||||
|
||||
def process_directory(dir_arg):
|
||||
# Résolution des chemins
|
||||
base_dir = Path(dir_arg)
|
||||
bgnot_dir = base_dir / "BGnot"
|
||||
sync_dir = EXPORT_DIR / dir_arg
|
||||
|
||||
# Création du dossier de destination s'il n'existe pas
|
||||
def export_directory(base_dir, source_dir_name):
|
||||
base_dir = Path(base_dir).expanduser().resolve()
|
||||
source_dir = base_dir / source_dir_name
|
||||
sync_dir = Path(EXPORT_DIR).expanduser() / base_dir.name
|
||||
sync_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if not bgnot_dir.is_dir():
|
||||
print(f"Erreur : le sous-dossier {bgnot_dir} n'existe pas.")
|
||||
if not source_dir.is_dir():
|
||||
print(f"Erreur : le sous-dossier {source_dir} n'existe pas.")
|
||||
return
|
||||
|
||||
# Récupération de tous les sous-dossiers
|
||||
subdirs = [d for d in bgnot_dir.iterdir() if d.is_dir()]
|
||||
if not subdirs:
|
||||
return
|
||||
subdirs = [directory for directory in source_dir.iterdir() if directory.is_dir()]
|
||||
if source_dir_name == "BGnot" and subdirs:
|
||||
all_start_with_copie = all(directory.name.startswith("Copie") for directory in subdirs)
|
||||
if not all_start_with_copie:
|
||||
subdirs = [directory for directory in subdirs if not directory.name.startswith("Copie")]
|
||||
|
||||
# Application des règles de sélection
|
||||
all_start_with_copie = all(d.name.startswith("Copie") for d in subdirs)
|
||||
if all_start_with_copie:
|
||||
chosen_dirs = subdirs
|
||||
else:
|
||||
chosen_dirs = [d for d in subdirs if not d.name.startswith("Copie")]
|
||||
|
||||
# Traitement des dossiers sélectionnés
|
||||
for subdir in chosen_dirs:
|
||||
for subdir in subdirs:
|
||||
concat_file = subdir / "Concat.pdf"
|
||||
|
||||
if not concat_file.is_file():
|
||||
print(f"Attention : le fichier {concat_file} est introuvable.")
|
||||
continue
|
||||
destination = sync_dir / f"{subdir.name}.pdf"
|
||||
method = replace_with_link_or_copy(concat_file, destination, prefer="hardlink")
|
||||
print(f"Exported: {destination} ({method})")
|
||||
|
||||
symlink_path = sync_dir / f"{subdir.name}.pdf"
|
||||
|
||||
# Supprime le lien précédent s'il existe pour éviter une erreur
|
||||
if symlink_path.is_symlink() or symlink_path.exists():
|
||||
symlink_path.unlink()
|
||||
|
||||
# Création du lien symbolique (pointe vers le chemin absolu pour éviter les problèmes)
|
||||
os.link(concat_file.absolute(), symlink_path)
|
||||
|
||||
import argparse
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Move to tablette folder.")
|
||||
@@ -54,21 +38,7 @@ if __name__ == "__main__":
|
||||
parser.add_argument("--refaire", action="store_true", help="Process only copies/labels defined in refaire.json")
|
||||
|
||||
args = parser.parse_args()
|
||||
root_dir = args.dir
|
||||
|
||||
if args.refaire:
|
||||
base_dir = Path(root_dir)
|
||||
brnot_dir = base_dir / "BRnot"
|
||||
sync_dir = EXPORT_DIR / root_dir
|
||||
sync_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for f in brnot_dir.iterdir():
|
||||
concat_file = f / "Concat.pdf"
|
||||
if f.is_dir() and concat_file.is_file():
|
||||
symlink_path = sync_dir / f"{f.name}.pdf"
|
||||
if symlink_path.exists():
|
||||
symlink_path.unlink()
|
||||
os.link(concat_file.absolute(), symlink_path)
|
||||
export_directory(args.dir, "BRnot")
|
||||
sys.exit(0)
|
||||
else:
|
||||
process_directory(root_dir)
|
||||
export_directory(args.dir, "BGnot")
|
||||
|
||||
@@ -10,6 +10,7 @@ from typing import List, Union
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from platform_utils import validate_windows_labels
|
||||
from utils import compile_to_pdf
|
||||
|
||||
def get_lcp(s1: str, s2: str) -> str:
|
||||
@@ -602,6 +603,7 @@ def process_exam(folder_path: str, restart: bool = False):
|
||||
break
|
||||
|
||||
labels_list = [item.label for group in grouped_items for item in group if isinstance(item, QuestionItem)]
|
||||
validate_windows_labels(labels_list)
|
||||
|
||||
# Save labels and proceed
|
||||
with open(folder / "labels", 'w', encoding='utf-8') as f_labels:
|
||||
|
||||
@@ -186,7 +186,7 @@ args = parser.parse_args()
|
||||
# input_arg = Path(args.input_path)
|
||||
image_files = []
|
||||
|
||||
from utils import natural_key
|
||||
from utils import natural_key, read_all_labels
|
||||
|
||||
for path_str in args.input_paths:
|
||||
input_arg = Path(path_str)
|
||||
@@ -222,8 +222,9 @@ for path_str in args.input_paths:
|
||||
else:
|
||||
print(f"Warning: No variants found for {target_file.stem} in {CUTLEFT_DIR}")
|
||||
|
||||
labels_txt = (INPUT_DIR / "labels").read_text()
|
||||
valid_labels_set = set(line.strip() for line in labels_txt.splitlines() if line.strip())
|
||||
all_labels = read_all_labels(INPUT_DIR)
|
||||
labels_txt = "\n".join(all_labels) + "\n"
|
||||
valid_labels_set = set(all_labels)
|
||||
names_path = (INPUT_DIR / "names")
|
||||
if not os.path.exists(names_path):
|
||||
names_path = Path("names")
|
||||
|
||||
+10
-7
@@ -1,10 +1,11 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
from collections import defaultdict
|
||||
from platform_utils import replace_with_link_or_copy, safe_filename
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
@@ -54,7 +55,7 @@ def main():
|
||||
elif len(ids) > 1:
|
||||
print(f"ALERT: Name '{name}' assigned to multiple IDs: {', '.join(ids)}")
|
||||
|
||||
safe_name = re.sub(r'[<>:"/\\|?*]', '', name).strip()
|
||||
safe_name = safe_filename(name)
|
||||
|
||||
for copie_id in ids:
|
||||
path_b = os.path.join(work_dir, f"{bnot_dir}/Copie{copie_id}")
|
||||
@@ -81,9 +82,11 @@ def main():
|
||||
src_file = os.path.join(source_folder, src_name)
|
||||
dst_link = os.path.join(dest_path, dst_name)
|
||||
try:
|
||||
if os.path.lexists(dst_link): os.remove(dst_link)
|
||||
if os.path.exists(src_file): os.symlink(src_file, dst_link)
|
||||
except Exception as e:
|
||||
if os.path.exists(src_file):
|
||||
method = replace_with_link_or_copy(src_file, dst_link, prefer="symlink")
|
||||
if method == "copy":
|
||||
print(f"Copied {src_name} for {dest_folder_name} (links unavailable)")
|
||||
except OSError as e:
|
||||
print(f"Error linking {src_name} for {dest_folder_name}: {e}")
|
||||
|
||||
# --- 4. Print Unassigned Names ---
|
||||
|
||||
+12
-8
@@ -1,16 +1,17 @@
|
||||
import fitz # PyMuPDF
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
from PIL import Image, ImageTk, ImageDraw
|
||||
import sys
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import glob
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
|
||||
import fitz # PyMuPDF
|
||||
from PIL import Image, ImageDraw, ImageTk
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
from config import PAGE_SPLITTER_KB
|
||||
from platform_utils import launch_pdf_arranger
|
||||
|
||||
|
||||
# --- Constants ---
|
||||
@@ -169,7 +170,10 @@ class PDFPreviewer:
|
||||
self.current_zoom = 1.0
|
||||
|
||||
def start_arranger(self):
|
||||
subprocess.Popen(["pdf-arranger", self.pdf_path])
|
||||
try:
|
||||
launch_pdf_arranger(self.pdf_path)
|
||||
except FileNotFoundError as exc:
|
||||
messagebox.showerror("PDF Arranger", str(exc))
|
||||
|
||||
def on_resize(self, event):
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
WINDOWS_RESERVED_NAMES = {
|
||||
"CON",
|
||||
"PRN",
|
||||
"AUX",
|
||||
"NUL",
|
||||
*(f"COM{number}" for number in range(1, 10)),
|
||||
*(f"LPT{number}" for number in range(1, 10)),
|
||||
}
|
||||
|
||||
|
||||
class WindowsLabelError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def windows_filename_problems(value: str) -> list[str]:
|
||||
problems = []
|
||||
forbidden = sorted({character for character in value if character in '<>:"/\\|?*'})
|
||||
if forbidden:
|
||||
problems.append("caractères interdits " + " ".join(repr(char) for char in forbidden))
|
||||
if any(ord(character) < 32 for character in value):
|
||||
problems.append("caractère de contrôle")
|
||||
if value.endswith((" ", ".")):
|
||||
problems.append("espace ou point final")
|
||||
if not value or value in {".", ".."}:
|
||||
problems.append("nom vide ou réservé")
|
||||
elif value.split(".", 1)[0].upper() in WINDOWS_RESERVED_NAMES:
|
||||
problems.append("nom système réservé")
|
||||
return problems
|
||||
|
||||
|
||||
def validate_windows_labels(
|
||||
labels: list[str] | tuple[str, ...], platform_name: str | None = None
|
||||
) -> None:
|
||||
"""Reject labels that cannot be used as filenames on native Windows."""
|
||||
if (platform_name or os.name) != "nt":
|
||||
return
|
||||
invalid = []
|
||||
for label in labels:
|
||||
problems = windows_filename_problems(label)
|
||||
if problems:
|
||||
invalid.append((label, problems))
|
||||
if not invalid:
|
||||
return
|
||||
details = "\n".join(
|
||||
f" - {label!r}: {', '.join(problems)}" for label, problems in invalid
|
||||
)
|
||||
raise WindowsLabelError(
|
||||
"Labels incompatibles avec Windows :\n"
|
||||
f"{details}\n"
|
||||
"Modifiez ces labels avant de poursuivre (par exemple, remplacez ':' par ' - ')."
|
||||
)
|
||||
|
||||
|
||||
def safe_filename(value: str, fallback: str = "Unknown") -> str:
|
||||
"""Return a filename component accepted by both Linux and Windows."""
|
||||
cleaned = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "", value).strip().rstrip(". ")
|
||||
if not cleaned:
|
||||
cleaned = fallback
|
||||
if cleaned.split(".", 1)[0].upper() in WINDOWS_RESERVED_NAMES:
|
||||
cleaned = f"_{cleaned}"
|
||||
return cleaned
|
||||
|
||||
|
||||
def open_path(path: str | Path) -> None:
|
||||
"""Open a file with the desktop's default application."""
|
||||
target = str(Path(path).expanduser().resolve())
|
||||
if os.name == "nt":
|
||||
os.startfile(target) # type: ignore[attr-defined]
|
||||
elif sys.platform.startswith("linux"):
|
||||
subprocess.Popen(["xdg-open", target])
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported platform: {sys.platform}")
|
||||
|
||||
|
||||
def launch_pdf_arranger(path: str | Path) -> None:
|
||||
executable = shutil.which("pdf-arranger") or shutil.which("pdfarranger")
|
||||
if not executable:
|
||||
raise FileNotFoundError(
|
||||
"PDF Arranger is not installed or is not available in PATH."
|
||||
)
|
||||
subprocess.Popen([executable, str(Path(path).expanduser().resolve())])
|
||||
|
||||
|
||||
def replace_with_link_or_copy(
|
||||
source: str | Path,
|
||||
destination: str | Path,
|
||||
*,
|
||||
prefer: Literal["hardlink", "symlink"] = "hardlink",
|
||||
) -> str:
|
||||
"""Replace destination with a link, falling back to a regular copy."""
|
||||
source_path = Path(source).expanduser().resolve()
|
||||
destination_path = Path(destination).expanduser()
|
||||
destination_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if os.path.lexists(destination_path):
|
||||
destination_path.unlink()
|
||||
|
||||
strategies = (prefer, "symlink" if prefer == "hardlink" else "hardlink")
|
||||
for strategy in strategies:
|
||||
try:
|
||||
if strategy == "hardlink":
|
||||
os.link(source_path, destination_path)
|
||||
else:
|
||||
os.symlink(source_path, destination_path)
|
||||
return strategy
|
||||
except (NotImplementedError, OSError):
|
||||
continue
|
||||
|
||||
shutil.copy2(source_path, destination_path)
|
||||
return "copy"
|
||||
+17
-16
@@ -1,14 +1,15 @@
|
||||
import sys
|
||||
import json
|
||||
import threading
|
||||
import re
|
||||
import queue
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
from pathlib import Path
|
||||
from tkinter import messagebox
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageTk
|
||||
|
||||
from platform_utils import open_path
|
||||
|
||||
print("o to open pdf, O original pdf, e to emacs part, p to go back, i to interro, click for coordinates")
|
||||
|
||||
# --- Configuration & Globals ---
|
||||
@@ -336,7 +337,7 @@ class ImageViewer:
|
||||
a = self.current_json_path.stem.split('_')[0] + ".pdf"
|
||||
pdf_path = self.current_json_path.with_name(a)
|
||||
print(f"Opening {pdf_path}")
|
||||
subprocess.Popen(['xdg-open', str(pdf_path.absolute())])
|
||||
open_path(pdf_path)
|
||||
|
||||
def on_open_interro(self, event):
|
||||
if self.is_viewing and self.current_json_path:
|
||||
@@ -347,25 +348,28 @@ class ImageViewer:
|
||||
if local_accent.exists():
|
||||
pdf_path = str(local_accent)
|
||||
elif local_plain.exists():
|
||||
pdf_path = str(local_plain)
|
||||
pdf_path = local_plain
|
||||
else:
|
||||
# Fallback to the Interro staging directory
|
||||
pdf_path = f"/home/sebastien/Prépa/Staging/Interro/{self.base_dir.name}.pdf"
|
||||
messagebox.showerror(
|
||||
"PDF not found",
|
||||
f"Neither {local_accent.name} nor {local_plain.name} exists in {self.base_dir}.",
|
||||
)
|
||||
return
|
||||
|
||||
print(f"Opening {pdf_path}")
|
||||
subprocess.Popen(['xdg-open', pdf_path])
|
||||
open_path(pdf_path)
|
||||
|
||||
def on_open_ori_pdf(self, event):
|
||||
if self.is_viewing and self.current_json_path:
|
||||
new_filename = self.current_json_path.stem.split('_')[0] + ".pdf"
|
||||
pdf_path = self.base_dir / "Copies Originales" / new_filename
|
||||
print(f"Opening {pdf_path}")
|
||||
subprocess.Popen(['xdg-open', str(pdf_path.absolute())])
|
||||
open_path(pdf_path)
|
||||
|
||||
def on_edit(self, event):
|
||||
if self.is_viewing and self.current_json_path:
|
||||
print(f"Opening {self.current_json_path}")
|
||||
subprocess.Popen(['xdg-open', str(self.current_json_path.absolute())])
|
||||
open_path(self.current_json_path)
|
||||
|
||||
def on_click(self, event):
|
||||
if not self.is_viewing: return
|
||||
@@ -418,14 +422,11 @@ if __name__ == "__main__":
|
||||
sys.exit(1)
|
||||
files_to_process = sorted(cutleft_dir.glob("*.jpg"))
|
||||
|
||||
labels_txt = (base_dir / "labels").read_text()
|
||||
valid_labels_set = set(line.strip() for line in labels_txt.splitlines() if line.strip())
|
||||
|
||||
|
||||
try:
|
||||
all_labels = read_all_labels(base_dir)
|
||||
except FileNotFoundError:
|
||||
all_labels = []
|
||||
valid_labels_set = set(all_labels)
|
||||
|
||||
t = threading.Thread(target=worker_thread, args=(base_dir, files_to_process, all_labels))
|
||||
t.daemon = True
|
||||
|
||||
@@ -8,6 +8,8 @@ import shutil
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
|
||||
from utils import read_all_labels
|
||||
|
||||
carreau = 1000 // 38
|
||||
|
||||
|
||||
@@ -208,6 +210,8 @@ if __name__ == "__main__":
|
||||
print(f"Error: {input_arg} is not a valid file or directory.")
|
||||
sys.exit(1)
|
||||
|
||||
read_all_labels(base_dir)
|
||||
|
||||
for pdf_path in pdf_files:
|
||||
json_path = pdf_path.with_suffix(".json")
|
||||
# print("Debug :", json_path)
|
||||
|
||||
+7
-6
@@ -1,15 +1,16 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import ezodf
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from utils import natural_key, read_all_labels
|
||||
import ezodf
|
||||
|
||||
from config import CURRENT_SCORE_ODS_PATH
|
||||
from utils import read_all_labels
|
||||
|
||||
# Configuration
|
||||
ODS_PATH = "/home/sebastien/Rust/gestion_classe/Staging/current_eval.ods"
|
||||
ODS_PATH = Path(CURRENT_SCORE_ODS_PATH).expanduser()
|
||||
TARGET_DIR_NAME = "A Rendre"
|
||||
|
||||
def main():
|
||||
@@ -34,7 +35,7 @@ def main():
|
||||
|
||||
print(f"Opening ODS file: {ODS_PATH}...")
|
||||
try:
|
||||
doc = ezodf.opendoc(ODS_PATH)
|
||||
doc = ezodf.opendoc(str(ODS_PATH))
|
||||
except Exception as e:
|
||||
print(f"Failed to open ODS: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from platform_utils import validate_windows_labels
|
||||
|
||||
def natural_key(text):
|
||||
return [int(c) if c.isdigit() else c.lower() for c in re.split(r'(\d+)', str(text))]
|
||||
|
||||
def read_all_labels(base_dir):
|
||||
return list(filter(None, (Path(base_dir) / "labels").read_text().splitlines()))
|
||||
labels = list(filter(None, (Path(base_dir) / "labels").read_text().splitlines()))
|
||||
validate_windows_labels(labels)
|
||||
return labels
|
||||
|
||||
def enonce_total(base_dir):
|
||||
text_dir = Path(base_dir) / 'Text'
|
||||
@@ -25,18 +29,15 @@ def enonce_total(base_dir):
|
||||
import os
|
||||
import shlex
|
||||
|
||||
from platform_utils import open_path
|
||||
|
||||
def edit_file_and_enter(file):
|
||||
editor = os.environ.get("EDITOR")
|
||||
try:
|
||||
if editor:
|
||||
subprocess.run(shlex.split(editor) + [str(file)])
|
||||
subprocess.run(shlex.split(editor, posix=os.name != "nt") + [str(file)])
|
||||
else:
|
||||
if sys.platform.startswith("linux"):
|
||||
subprocess.run(["xdg-open", str(file)])
|
||||
elif sys.platform == "darwin":
|
||||
subprocess.run(["open", str(file)])
|
||||
else:
|
||||
os.startfile(str(file))
|
||||
open_path(file)
|
||||
input("Press ENTER here once you have saved and closed the text file...")
|
||||
except Exception as e:
|
||||
print(f"Error running editor: {e}")
|
||||
|
||||
Reference in New Issue
Block a user