extension de export/import aux autres modes de génération

This commit is contained in:
2026-08-22 12:14:20 +02:00
parent 9af05f2c86
commit 92a9f9883e
6 changed files with 242 additions and 30 deletions
+14 -5
View File
@@ -1,7 +1,7 @@
#+title: Script
#+author: Sébastien Miquel
#+date: 14-03-2026
# Time-stamp: <22-08-26 09:58>
# Time-stamp: <22-08-26 12:05>
#+OPTIONS:
* Méta
@@ -343,8 +343,6 @@ Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
(chères), pour une version low cost, passer =--limit 0=, toutes
les requêtes seront sur Gemini Flash.
Will it resume ? It seems so. Best to wait a bit.
Pour diminuer le coût, il est possible de batch les requêtes, qui
seront alors traitées sous au plus 24h.
+ =python correction.py Interro --batch=
@@ -414,10 +412,16 @@ Lors d'une régénération, les nouvelles sorties sont préparées dans un
dossier temporaire voisin. Une erreur de rendu conserve donc la sortie
précédente ; pour =BGnot --overwrite=, le dossier complet n'est remplacé
que si tous les groupes ont été produits.
3. =python export.py Interro= (gestion perso)
3. =python export.py Interro BGnot= (gestion perso)
Cela déplace les groupes dans le dossier configuré par =EXPORT_DIR=
(par défaut =Export=).
Le second argument peut être =BGnot=, =Bnot= ou =Anot= et reste
facultatif (=BGnot= par défaut). =Anot= exporte =Concat.jpg= ; les
deux autres modes exportent =Concat.pdf=. Dans le GUI, seuls les
dossiers présents sont proposés et le mode de la dernière génération
d'annotations est présélectionné.
Il faut ensuite annoter les fichiers dans `EXPORT_DIR` avec une
tablette graphique.
@@ -426,12 +430,17 @@ tablette graphique.
_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=
1. =python import.py Interro BGnot=
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=.
Comme pour l'export, le second argument accepte =BGnot=, =Bnot= ou
=Anot=. Le GUI présélectionne le dossier utilisé lors du dernier
export. Pour =Anot=, l'image est importée sous le nom
=Concat_annotated.jpg=.
2. =python reading_annotations.py Interro=
Lit les =Concat_annotated= dans =Bnot=, regénère les copies avec
+43 -2
View File
@@ -36,6 +36,12 @@ STATUS_LABELS = {
}
DEFAULT_HTTPS_PROXY = "http://10.0.0.1:3128"
ANNOTATION_DIRECTORIES = ("BGnot", "Bnot", "Anot")
ANNOTATION_VARIANT_DIRECTORIES = {
"simple": "Anot",
"checks": "Bnot",
"grouped": "BGnot",
}
def copy_pdf_paths(evaluation: Path) -> list[Path]:
@@ -57,6 +63,12 @@ def copy_pdf_paths(evaluation: Path) -> list[Path]:
return []
def detected_annotation_directories(evaluation: Path) -> tuple[str, ...]:
return tuple(
name for name in ANNOTATION_DIRECTORIES if (evaluation / name).is_dir()
)
def plotting_shortcut_lines() -> list[str]:
try:
from config import PLOTTING_KB
@@ -530,6 +542,9 @@ class CopienatorApp(tk.Tk):
if spec.variants and variant.id not in spec.variants:
continue
value = values.get(spec.name, value_for_default(spec.default, evaluation_arg))
choices = spec.choices
if spec.name == "annotation_dir" and step.id in {"export", "import"}:
choices, value = self._annotation_directory_choices(step.id)
ttk.Label(self.form, text=spec.label).grid(row=row, column=0, sticky="nw", pady=4, padx=(0, 8))
if spec.kind == "bool":
variable: tk.Variable = tk.BooleanVar(value=bool(value))
@@ -537,7 +552,7 @@ class CopienatorApp(tk.Tk):
widget.grid(row=row, column=1, sticky="w", pady=4)
elif spec.kind == "choice":
variable = tk.StringVar(value=str(value))
widget = ttk.Combobox(self.form, textvariable=variable, values=spec.choices, state="readonly")
widget = ttk.Combobox(self.form, textvariable=variable, values=choices, state="readonly")
widget.grid(row=row, column=1, sticky="ew", pady=4)
else:
variable = tk.StringVar(value=str(value))
@@ -626,6 +641,25 @@ class CopienatorApp(tk.Tk):
row += 1
return row
def _annotation_directory_choices(self, step_id: str) -> tuple[tuple[str, ...], str]:
evaluation = self.evaluation
detected = detected_annotation_directories(evaluation) if evaluation else ()
choices = detected or ANNOTATION_DIRECTORIES
if step_id == "export":
annotation = self.state_store.step("annotation")
variant = annotation.get("last_run_variant", annotation.get("variant", "grouped"))
preferred = ANNOTATION_VARIANT_DIRECTORIES.get(str(variant), "BGnot")
else:
export = self.state_store.step("export")
last_values = export.get("last_run_values", export.get("values", {}))
preferred = (
str(last_values.get("annotation_dir", "BGnot"))
if isinstance(last_values, dict)
else "BGnot"
)
return choices, preferred if preferred in choices else choices[0]
def _open_persp(self) -> None:
evaluation = self.evaluation
self._open_desktop_path(evaluation / "Persp" if evaluation else None, "dossier Persp")
@@ -775,9 +809,16 @@ class CopienatorApp(tk.Tk):
return
self._save_current_form()
run_values = self._values()
ordered_ids = [item.id for item in self.steps]
self.state_store.invalidate_after(ordered_ids, step.id)
self.state_store.update_step(step.id, status="running", command=command_display(command))
self.state_store.update_step(
step.id,
status="running",
command=command_display(command),
last_run_variant=variant.id,
last_run_values=run_values,
)
self.active_step_id = step.id
workspace = self.state_store.workspace
assert workspace is not None
+18 -2
View File
@@ -326,11 +326,19 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
StepDefinition(
"export",
"Génération des annotations",
"Exporter vers la tablette",
"Exporte les groupes vers le dossier EXPORT_DIR défini dans config.py.",
"Exporter",
"Exporte les annotations vers le dossier EXPORT_DIR défini dans config.py.",
(python("default", "Export", "export.py"),),
arguments=(
arg_target("Dossier de l’évaluation"),
ArgumentSpec(
"annotation_dir",
"Dossier dannotations",
"choice",
default="BGnot",
choices=("BGnot", "Bnot", "Anot"),
positional=True,
),
ArgumentSpec("refaire", "Mode refaire", "bool", "--refaire"),
),
optional=True,
@@ -350,6 +358,14 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
(python("default", "Import", "import.py"),),
arguments=(
arg_target("Dossier de l’évaluation"),
ArgumentSpec(
"annotation_dir",
"Dossier dannotations",
"choice",
default="BGnot",
choices=("BGnot", "Bnot", "Anot"),
positional=True,
),
ArgumentSpec("refaire", "Mode refaire", "bool", "--refaire"),
),
),
+35 -7
View File
@@ -13,6 +13,8 @@ from copienator import (
)
from platform_utils import replace_with_link_or_copy
ANNOTATION_DIRECTORIES = ("BGnot", "Bnot", "Anot")
def export_directory(
workspace: EvaluationWorkspace,
@@ -31,12 +33,22 @@ def export_directory(
missing_outputs = 0
for subdir in subdirs:
concat_file = subdir / "Concat.pdf"
if not concat_file.is_file():
print(f"Warning: file not found: {concat_file}", file=sys.stderr)
concat_file = next(
(
candidate
for candidate in (subdir / "Concat.pdf", subdir / "Concat.jpg")
if candidate.is_file()
),
None,
)
if concat_file is None:
print(
f"Warning: no Concat.pdf or Concat.jpg found in {subdir}",
file=sys.stderr,
)
missing_outputs += 1
continue
destination = sync_dir / f"{subdir.name}.pdf"
destination = sync_dir / f"{subdir.name}{concat_file.suffix.lower()}"
method = replace_with_link_or_copy(concat_file, destination, prefer="hardlink")
print(f"Exported: {destination} ({method})")
return ExitCode.PARTIAL if missing_outputs else ExitCode.SUCCESS
@@ -44,12 +56,24 @@ def export_directory(
def build_parser() -> argparse.ArgumentParser:
parser = evaluation_parser("Export annotated PDFs to the tablet directory.")
parser.add_argument(
"annotation_dir",
nargs="?",
choices=ANNOTATION_DIRECTORIES,
default="BGnot",
help="Annotation directory to export (default: BGnot)",
)
parser.add_argument("--refaire", action="store_true", help="Process only copies/labels defined in refaire.json")
return parser
def run(workspace: EvaluationWorkspace, *, refaire: bool = False) -> ExitCode:
return export_directory(workspace, "BRnot" if refaire else "BGnot")
def run(
workspace: EvaluationWorkspace,
*,
annotation_dir: str = "BGnot",
refaire: bool = False,
) -> ExitCode:
return export_directory(workspace, "BRnot" if refaire else annotation_dir)
def main(argv: Sequence[str] | None = None) -> int:
@@ -57,7 +81,11 @@ def main(argv: Sequence[str] | None = None) -> int:
return execute(
parser,
argv,
lambda args: run(workspace_from_args(args), refaire=args.refaire),
lambda args: run(
workspace_from_args(args),
annotation_dir=args.annotation_dir,
refaire=args.refaire,
),
)
+42 -12
View File
@@ -13,43 +13,69 @@ from copienator import (
workspace_from_args,
)
ANNOTATION_DIRECTORIES = ("BGnot", "Bnot", "Anot")
def sync_annotated(
workspace: EvaluationWorkspace,
*,
refaire: bool = False,
annotation_dir_name: str,
import_dir: Path,
) -> ExitCode:
annotation_name = "BRnot" if refaire else "BGnot"
workspace.require_directories(annotation_name)
annotation_dir = workspace.root / annotation_name
workspace.require_directories(annotation_dir_name)
annotation_dir = workspace.root / annotation_dir_name
annotated_dir = Path(import_dir).expanduser()
if not annotated_dir.is_dir():
print(f"Error: directory does not exist: {annotated_dir}", file=sys.stderr)
return ExitCode.INVALID_WORKSPACE
missing_targets = 0
for pdf_file in annotated_dir.glob("*.pdf"):
target_subdir = annotation_dir / pdf_file.stem
annotated_files = sorted(
(
path
for path in annotated_dir.iterdir()
if path.is_file() and path.suffix.casefold() in {".pdf", ".jpg", ".jpeg"}
),
key=lambda path: path.name.casefold(),
)
for annotated_file in annotated_files:
target_subdir = annotation_dir / annotated_file.stem
if not target_subdir.is_dir():
print(f"Warning: directory not found: {target_subdir}", file=sys.stderr)
missing_targets += 1
else:
dest_file = target_subdir / "Concat_annotated.pdf"
print(f"Copying {pdf_file} to {dest_file}")
shutil.copy2(pdf_file, dest_file)
suffix = annotated_file.suffix.lower()
dest_file = target_subdir / f"Concat_annotated{suffix}"
print(f"Copying {annotated_file} to {dest_file}")
shutil.copy2(annotated_file, dest_file)
return ExitCode.PARTIAL if missing_targets else ExitCode.SUCCESS
def build_parser() -> argparse.ArgumentParser:
parser = evaluation_parser("Import handwritten annotations from the tablet directory.")
parser.add_argument(
"annotation_dir",
nargs="?",
choices=ANNOTATION_DIRECTORIES,
default="BGnot",
help="Annotation directory receiving imported PDFs (default: BGnot)",
)
parser.add_argument("--refaire", action="store_true", help="Process only copies/labels defined in refaire.json")
return parser
def run(workspace: EvaluationWorkspace, *, refaire: bool = False) -> ExitCode:
return sync_annotated(workspace, refaire=refaire, import_dir=Path(IMPORT_DIR))
def run(
workspace: EvaluationWorkspace,
*,
annotation_dir: str = "BGnot",
refaire: bool = False,
) -> ExitCode:
return sync_annotated(
workspace,
annotation_dir_name="BRnot" if refaire else annotation_dir,
import_dir=Path(IMPORT_DIR),
)
def main(argv: Sequence[str] | None = None) -> int:
@@ -57,7 +83,11 @@ def main(argv: Sequence[str] | None = None) -> int:
return execute(
parser,
argv,
lambda args: run(workspace_from_args(args), refaire=args.refaire),
lambda args: run(
workspace_from_args(args),
annotation_dir=args.annotation_dir,
refaire=args.refaire,
),
)
+90 -2
View File
@@ -32,9 +32,11 @@ from copienator.annotation_actions import apply_checkbox_actions, apply_score_ov
from copienator.annotation_data import AnnotationLoadResult, load_annotation_data
from copienator.filesystem import staged_directory, staged_files
from copienator_gui.app import (
CopienatorApp,
DEFAULT_HTTPS_PROXY,
build_runner_environment,
copy_pdf_paths,
detected_annotation_directories,
has_manual_conflicts,
plotting_shortcut_lines,
process_status,
@@ -453,8 +455,16 @@ class StandardCliTests(unittest.TestCase):
"personal",
{"target": evaluation},
),
"export": ("export", "default", {"target": evaluation, "refaire": True}),
"import": ("import", "default", {"target": evaluation, "refaire": True}),
"export": (
"export",
"default",
{"target": evaluation, "annotation_dir": "Bnot", "refaire": True},
),
"import": (
"import",
"default",
{"target": evaluation, "annotation_dir": "Anot", "refaire": True},
),
"giving_names": (
"giving_names",
"default",
@@ -596,6 +606,23 @@ class StandardCliTests(unittest.TestCase):
exported = base / "Export" / "Exam" / "Ex 1.pdf"
self.assertEqual(exported.read_bytes(), b"annotated")
def test_export_accepts_each_annotation_directory(self) -> None:
module = self.modules["export"]
for annotation_dir in ("Anot", "Bnot"):
with self.subTest(annotation_dir=annotation_dir), tempfile.TemporaryDirectory() as directory:
base = Path(directory)
evaluation = base / "Exam"
source = evaluation / annotation_dir / "Copie01"
source.mkdir(parents=True)
suffix = ".jpg" if annotation_dir == "Anot" else ".pdf"
(source / f"Concat{suffix}").write_bytes(annotation_dir.encode())
with patch.object(module, "EXPORT_DIR", base / "Export"):
self.assertEqual(module.main([str(evaluation), annotation_dir]), 0)
self.assertEqual(
(base / "Export" / "Exam" / f"Copie01{suffix}").read_bytes(),
annotation_dir.encode(),
)
def test_import_main_copies_handwritten_annotations(self) -> None:
module = self.modules["import"]
with tempfile.TemporaryDirectory() as directory:
@@ -612,6 +639,24 @@ class StandardCliTests(unittest.TestCase):
(target / "Concat_annotated.pdf").read_bytes(), b"handwritten"
)
def test_import_accepts_each_annotation_directory(self) -> None:
module = self.modules["import"]
for annotation_dir in ("Anot", "Bnot"):
with self.subTest(annotation_dir=annotation_dir), tempfile.TemporaryDirectory() as directory:
base = Path(directory)
evaluation = base / "Exam"
target = evaluation / annotation_dir / "Copie01"
target.mkdir(parents=True)
import_dir = base / "Import"
import_dir.mkdir()
suffix = ".jpg" if annotation_dir == "Anot" else ".pdf"
(import_dir / f"Copie01{suffix}").write_bytes(b"handwritten")
with patch.object(module, "IMPORT_DIR", import_dir):
self.assertEqual(module.main([str(evaluation), annotation_dir]), 0)
self.assertEqual(
(target / f"Concat_annotated{suffix}").read_bytes(), b"handwritten"
)
def test_giving_names_main_builds_return_directory(self) -> None:
module = self.modules["giving_names"]
with tempfile.TemporaryDirectory() as directory:
@@ -1406,6 +1451,15 @@ class WorkflowTests(unittest.TestCase):
def test_review_persp_has_shorter_title(self) -> None:
self.assertEqual(self.steps["review_persp"].title, "Relire les barèmes")
def test_export_has_shorter_title_and_annotation_argument(self) -> None:
self.assertEqual(self.steps["export"].title, "Exporter")
command = self.command(
"export",
"default",
{"target": self.evaluation, "annotation_dir": "Bnot"},
)
self.assertEqual(command[3:], [self.evaluation, "Bnot"])
def test_copy_listing_prefers_processed_copies(self) -> None:
with tempfile.TemporaryDirectory() as directory:
evaluation = Path(directory)
@@ -1421,6 +1475,40 @@ class WorkflowTests(unittest.TestCase):
["Copie01.pdf", "Copie02.pdf"],
)
def test_annotation_directory_detection_uses_known_directories(self) -> None:
with tempfile.TemporaryDirectory() as directory:
evaluation = Path(directory)
(evaluation / "Bnot").mkdir()
(evaluation / "Anot").mkdir()
(evaluation / "Other").mkdir()
self.assertEqual(
detected_annotation_directories(evaluation), ("Bnot", "Anot")
)
def test_export_and_import_defaults_follow_previous_runs(self) -> None:
with tempfile.TemporaryDirectory() as directory:
evaluation = Path(directory)
(evaluation / "Bnot").mkdir()
(evaluation / "Anot").mkdir()
store = StateStore()
store.load(evaluation)
store.update_step("annotation", last_run_variant="simple")
app = SimpleNamespace(evaluation=evaluation, state_store=store)
choices, default = CopienatorApp._annotation_directory_choices(
app, "export"
)
self.assertEqual(choices, ("Bnot", "Anot"))
self.assertEqual(default, "Anot")
store.update_step(
"export", last_run_values={"annotation_dir": "Bnot"}
)
_choices, default = CopienatorApp._annotation_directory_choices(
app, "import"
)
self.assertEqual(default, "Bnot")
def test_plotting_shortcuts_describe_open_actions(self) -> None:
rendered = "\n".join(plotting_shortcut_lines())
self.assertIn("ouvrir l’énoncé", rendered)