Automatic page splitting
This commit is contained in:
1 parent
d4404e4c27
commit
78f3e4b2f0
13 files changed
+1959
-40
No files matched your search
@@ -0,0 +1,183 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user