"""Publish automatic splits into an evaluation, preserving sources and review data.""" from __future__ import annotations import hashlib import json import shutil import tempfile from pathlib import Path from copienator import CliError, ExitCode from copienator.auto_page_split import run as analyse_batch from copienator.copy_errors import clear_copy_error, copy_errors, mark_copy_error from copienator.filesystem import publish_file_sets from copienator.orientation import report_path def decision_path(workspace, stem): return workspace.metadata_dir / "page_splitting" / f"{stem}.json" def load_decisions(workspace, source): """Only reuse decisions for the exact preserved input and published output.""" try: data = json.loads(decision_path(workspace, source.stem).read_text()) if data["source_sha256"] != hashlib.sha256(source.read_bytes()).hexdigest(): return None output = workspace.copies_dir / source.name digest = ( hashlib.sha256(output.read_bytes()).hexdigest() if output.exists() else None ) if digest != data["output_sha256"]: return None pages = data["pages"] if not isinstance(pages, list) or not all( isinstance(row, dict) and type(row.get("source_page")) is int and isinstance(row.get("outputs"), list) and all( isinstance(o, dict) and o.get("side") in {"left", "right", "whole"} and type(o.get("keep")) is bool and isinstance(o.get("crop_points"), list) and len(o["crop_points"]) == 4 for o in row["outputs"] ) for row in pages ): return None return pages except (OSError, ValueError, KeyError, TypeError, AttributeError): return None def run(workspace, inputs: list[Path], *, workers=4): if workers < 1: raise CliError( "Le nombre de traitements parallèles doit être positif.", ExitCode.INVALID_ARGUMENTS, ) if not inputs: print("Aucune copie PDF à traiter.", flush=True) return ExitCode.SUCCESS if len({p.name for p in inputs}) != len(inputs): raise CliError( "Plusieurs copies portent le même nom.", ExitCode.INVALID_ARGUMENTS ) copy_errors(workspace) # Validate existing review metadata before publishing. workspace.runs_dir.mkdir(parents=True, exist_ok=True) run_dir = Path(tempfile.mkdtemp(prefix="page-split-", dir=workspace.runs_dir)) snapshots = run_dir / "inputs" snapshots.mkdir() for source in inputs: shutil.copy2(source, snapshots / source.name) result = run_dir / "result" print(f"Découpage automatique de {len(inputs)} copie(s).", flush=True) summary = analyse_batch(snapshots, result, workers=workers) records = json.loads((result / "report.json").read_text())["pages"] stages = { name: run_dir / name for name in ("copies", "originals", "origins", "decisions") } for stage in stages.values(): stage.mkdir() for source in inputs: rows = [r for r in records if r["file"] == source.name] expected = rows[0]["source_sha256"] if hashlib.sha256(source.read_bytes()).hexdigest() != expected: raise CliError( f"{source.name} a changé pendant l’analyse. Aucun PDF remplacé." ) shutil.copy2(snapshots / source.name, stages["originals"] / source.name) for directory, label in ( (workspace.copies_dir, "previous_copies"), (workspace.original_copies_dir, "previous_originals"), ): prior = directory / source.name if prior.exists(): backup = run_dir / label backup.mkdir(exist_ok=True) shutil.copy2(prior, backup / source.name) output = result / "pdfs" / source.name digest = ( hashlib.sha256(output.read_bytes()).hexdigest() if output.exists() else None ) if digest: shutil.copy2(output, stages["copies"] / source.name) origins = [ {"origin": r["source_page"], "crop_points": o["crop_points"]} for r in rows for o in r["outputs"] if o["keep"] ] if digest: (stages["origins"] / (source.stem + ".json")).write_text( json.dumps({"version": 1, "sha256": digest, "pages": origins}), encoding="utf-8", ) (stages["decisions"] / (source.stem + ".json")).write_text( json.dumps( { "version": 1, "source_sha256": expected, "output_sha256": digest, "pages": rows, } ), encoding="utf-8", ) names = [p.name for p in inputs] for source in inputs: if source.read_bytes() != (snapshots / source.name).read_bytes(): raise CliError( f"{source.name} a changé pendant l’analyse. Aucun PDF remplacé." ) publish_file_sets( { workspace.copies_dir: (stages["copies"], names), workspace.original_copies_dir: (stages["originals"], []), workspace.metadata_dir / "page_origins": ( stages["origins"], [p.stem + ".json" for p in inputs], ), workspace.metadata_dir / "page_splitting": (stages["decisions"], []), } ) for source in inputs: if ( source.parent.resolve() not in { workspace.copies_dir.resolve(), workspace.original_copies_dir.resolve(), } and source.read_bytes() == (snapshots / source.name).read_bytes() ): # Root scans become preserved originals, as in the manual workflow. source.unlink() rows = [r for r in records if r["file"] == source.name] output = workspace.copies_dir / source.name clear_copy_error(workspace, output, source="orientation") report_path(workspace, source.stem).unlink(missing_ok=True) reasons = {reason for r in rows for reason in r["review_reasons"]} if reasons or not output.exists(): mark_copy_error( workspace, output, "Découpage automatique à vérifier manuellement." if reasons else "Toutes les pages semblent vides : vérifier l’original.", source="page_splitter", ) else: clear_copy_error(workspace, output, source="page_splitter") print( f"Terminé : {summary['output_pages']} pages conservées, {summary['blank_removed']} pages vides retirées. " "Ordre des pages inchangé.", flush=True, ) print( f"Rapport et sauvegardes : {run_dir}\n" "Choisissez Manuellement pour vérifier ou modifier les propositions.", flush=True, ) return ExitCode.SUCCESS