extension de export/import aux autres modes de génération
This commit is contained in:
+14
-5
@@ -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
@@ -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
|
||||
|
||||
@@ -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 d’annotations",
|
||||
"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 d’annotations",
|
||||
"choice",
|
||||
default="BGnot",
|
||||
choices=("BGnot", "Bnot", "Anot"),
|
||||
positional=True,
|
||||
),
|
||||
ArgumentSpec("refaire", "Mode refaire", "bool", "--refaire"),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user