184 lines
7.0 KiB
Python
184 lines
7.0 KiB
Python
"""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
|