Allow signaling issues in label verification
This commit is contained in:
1 parent
3754c0c11a
commit
9925362352
21 files changed
+1347
-36
No files matched your search
@@ -64,7 +64,7 @@ def make_base_image(pdf_path):
|
||||
|
||||
current_y = 0
|
||||
for page in pages:
|
||||
base_img.paste(page.convert("RGBA"), (0, current_y))
|
||||
base_img.paste(page.convert("RGBA"), ((max_w - page.width) // 2, current_y))
|
||||
current_y += page.height
|
||||
return (base_img, total_h, max_w)
|
||||
|
||||
|
||||
@@ -471,7 +471,7 @@ def run(
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = target_parser("Interactively crop the label margin from PDF copies")
|
||||
parser.add_argument("--marked", action="store_true", help="Review flagged copies and clear each flag after saving with Enter")
|
||||
parser.add_argument("--marked", action="store_true", help="Review flagged copies and clear only margin-cropping flags after saving with Enter")
|
||||
parser.add_argument(
|
||||
"--fullpage",
|
||||
action="store_true",
|
||||
|
||||
@@ -16,6 +16,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from copienator import configuration as config
|
||||
from copienator.gemini_config import thinking_config
|
||||
from copienator.copy_errors import marked_copy_paths
|
||||
from copienator import (
|
||||
CliError,
|
||||
EvaluationWorkspace,
|
||||
@@ -530,6 +531,7 @@ def run(
|
||||
targets: list[Path],
|
||||
*,
|
||||
overwrite: bool = False,
|
||||
marked: bool = False,
|
||||
client=None,
|
||||
sleep: Sleep = time.sleep,
|
||||
max_workers: int = 12,
|
||||
@@ -538,6 +540,12 @@ def run(
|
||||
raise CliError("Le nombre de requêtes simultanées doit être positif.",
|
||||
ExitCode.INVALID_ARGUMENTS)
|
||||
workspace.require_files("labels")
|
||||
if marked:
|
||||
targets = marked_copy_paths(workspace)
|
||||
if not targets:
|
||||
print("No flagged copies to detect labels for.")
|
||||
return ExitCode.SUCCESS
|
||||
overwrite = True
|
||||
images, warnings = selected_images(workspace, targets)
|
||||
for warning in warnings:
|
||||
print(f"Warning: {warning}")
|
||||
@@ -626,6 +634,8 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
type=Path,
|
||||
help="Additional copy PDFs or Cutleft images from the same evaluation",
|
||||
)
|
||||
parser.add_argument("--marked", action="store_true",
|
||||
help="Regenerate labels only for copies with remaining flags")
|
||||
parser.add_argument(
|
||||
"--overwrite",
|
||||
action="store_true",
|
||||
@@ -664,7 +674,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
ExitCode.INVALID_ARGUMENTS,
|
||||
)
|
||||
targets.append(resolved)
|
||||
return run(workspace, targets, overwrite=args.overwrite, max_workers=args.workers)
|
||||
return run(workspace, targets, overwrite=args.overwrite, marked=args.marked, max_workers=args.workers)
|
||||
|
||||
return execute(parser, argv, handle)
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ def group_files(file_list):
|
||||
return [g['items'] for g in groups]
|
||||
|
||||
def stitch_pdf_pages(images_list):
|
||||
"""Vertically concatenates a list of PIL images with no separator."""
|
||||
"""Stack PIL images vertically, centering each within the widest image."""
|
||||
if not images_list:
|
||||
return None
|
||||
if len(images_list) == 1:
|
||||
@@ -130,7 +130,7 @@ def stitch_pdf_pages(images_list):
|
||||
|
||||
y_offset = 0
|
||||
for img in images_list:
|
||||
combined.paste(img, (0, y_offset))
|
||||
combined.paste(img, ((max_width - img.width) // 2, y_offset))
|
||||
y_offset += img.height
|
||||
|
||||
return combined
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from copienator import (
|
||||
EvaluationWorkspace,
|
||||
ExitCode,
|
||||
evaluation_parser,
|
||||
execute,
|
||||
read_json,
|
||||
workspace_from_args,
|
||||
)
|
||||
from copienator.annotation_modifications import (
|
||||
APPLIED,
|
||||
DRAFT,
|
||||
MODES,
|
||||
baseline,
|
||||
output_removals,
|
||||
read_instructions,
|
||||
rebuild,
|
||||
validate_instructions,
|
||||
)
|
||||
from copienator.filesystem import publish_file_sets
|
||||
|
||||
|
||||
def run(workspace: EvaluationWorkspace, *, annotation_dir: str = "BGnot") -> ExitCode:
|
||||
workspace.require_files("labels", DRAFT)
|
||||
workspace.require_directories("Copies", annotation_dir)
|
||||
text = (workspace.root / DRAFT).read_text(encoding="utf-8")
|
||||
instructions = read_instructions(text, annotation_dir)
|
||||
validate_instructions(workspace.root, annotation_dir, instructions)
|
||||
directory = workspace.root / annotation_dir
|
||||
applied = directory / APPLIED
|
||||
previous = read_instructions(applied.read_text(encoding="utf-8"), annotation_dir) if applied.exists() else []
|
||||
copy_ids = sorted({item.copy_id for item in [*previous, *instructions]})
|
||||
# Prepare every copy before publishing anything, including the applied file.
|
||||
with TemporaryDirectory(prefix=".annotation-edits-", dir=directory) as temporary:
|
||||
staging_root = Path(temporary)
|
||||
file_sets = {}
|
||||
for copy_id in copy_ids:
|
||||
output = directory / f"Copie{copy_id}"
|
||||
meta = baseline(output)
|
||||
staging = staging_root / output.name
|
||||
staging.mkdir()
|
||||
selected = [item for item in instructions if item.copy_id == copy_id]
|
||||
rebuild(workspace.root, annotation_dir, output, staging, meta, selected,
|
||||
read_json(output / "score.json"))
|
||||
file_sets[output] = (staging, output_removals(output))
|
||||
manifest = staging_root / "manifest"
|
||||
manifest.mkdir()
|
||||
(manifest / APPLIED).write_text(text, encoding="utf-8")
|
||||
file_sets[directory] = (manifest, ())
|
||||
publish_file_sets(file_sets)
|
||||
print(f"Modifications appliquées à {len(copy_ids)} copie(s). Les scores sont conservés.")
|
||||
print("Relancez « Attribuer les noms et préparer A Rendre » pour actualiser tous les livrables.")
|
||||
return ExitCode.SUCCESS
|
||||
|
||||
|
||||
def build_parser():
|
||||
parser = evaluation_parser("Modify reviewed answer bodies using annotation_modifications.txt")
|
||||
parser.add_argument("--annotation-dir", choices=MODES, default="BGnot")
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
return execute(build_parser(), argv, lambda args: run(
|
||||
workspace_from_args(args), annotation_dir=args.annotation_dir))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -21,6 +21,7 @@ from copienator import (
|
||||
target_parser,
|
||||
workspace_from_target,
|
||||
)
|
||||
from copienator.copy_errors import clear_copy_error, mark_copy_error, marked_copy_paths
|
||||
from copienator.platform import open_path
|
||||
from copienator.utils import natural_key, read_all_labels
|
||||
|
||||
@@ -307,6 +308,7 @@ class ImageViewer:
|
||||
self.forward_stack = []
|
||||
self.current_pil_image = None
|
||||
self.failed = False
|
||||
self.skipped_copies = set()
|
||||
|
||||
from config import PLOTTING_KB
|
||||
|
||||
@@ -314,6 +316,7 @@ class ImageViewer:
|
||||
self.root.bind(PLOTTING_KB["OK"], self.on_enter)
|
||||
self.root.bind(PLOTTING_KB["previous"], self.on_previous)
|
||||
self.root.bind("P", self.on_previous)
|
||||
self.root.bind("!", self.on_skip)
|
||||
self.root.bind(PLOTTING_KB["edit"], self.on_edit)
|
||||
self.root.bind(PLOTTING_KB["open pdf"], self.on_open_pdf)
|
||||
self.root.bind(PLOTTING_KB["open original pdf"], self.on_open_ori_pdf)
|
||||
@@ -345,6 +348,10 @@ class ImageViewer:
|
||||
self.root.quit()
|
||||
return
|
||||
|
||||
if metadata["copie"] in self.skipped_copies:
|
||||
self.root.after(0, self.poll_queue)
|
||||
return
|
||||
|
||||
# Check if we switched to a new "Copie" group
|
||||
if self.active_copie_name != metadata["copie"]:
|
||||
self.save_current_batch() # Write previous group to disk
|
||||
@@ -361,8 +368,21 @@ class ImageViewer:
|
||||
"""Writes the accumulated data to the main JSON file."""
|
||||
if self.active_copie_name and self.accumulated_results:
|
||||
main_json_path = self.base_dir / "Copies" / f"{self.active_copie_name}.json"
|
||||
accepted = [item[2] for item in self.history
|
||||
if item[3] == self.active_copie_name]
|
||||
parts = {item["part"] for item in accepted}
|
||||
expected = (set(range(1, len(accepted[-1]["schema"]["columns_per_file"]) + 1))
|
||||
if accepted else set())
|
||||
if not expected or parts != expected:
|
||||
mark_copy_error(self.workspace, main_json_path.with_suffix(".pdf"),
|
||||
"Vérification des labels incomplète", source="plotting")
|
||||
self.failed = True
|
||||
self.accumulated_results = None
|
||||
return
|
||||
print(f"Writing aggregated result to {main_json_path}")
|
||||
atomic_write_json(main_json_path, self.accumulated_results)
|
||||
clear_copy_error(self.workspace, main_json_path.with_suffix(".pdf"),
|
||||
source="plotting")
|
||||
self.accumulated_results = None
|
||||
|
||||
def close(self):
|
||||
@@ -467,6 +487,21 @@ class ImageViewer:
|
||||
self.is_viewing = False
|
||||
self.label.config(image="", text="Loading next...")
|
||||
|
||||
def on_skip(self, _event=None):
|
||||
if not self.is_viewing:
|
||||
return
|
||||
copie = self.current_meta["copie"]
|
||||
mark_copy_error(self.workspace, self.workspace.copies_dir / f"{copie}.pdf",
|
||||
"Problème repéré pendant la vérification des labels", source="plotting")
|
||||
self.failed = True
|
||||
self.skipped_copies.add(copie)
|
||||
# Discard accepted parts of this copy without replacing its previous result.
|
||||
self.accumulated_results = None
|
||||
self.history = [item for item in self.history if item[3] != copie]
|
||||
self.forward_stack = [item for item in self.forward_stack if item[2]["copie"] != copie]
|
||||
self.is_viewing = False
|
||||
self.label.config(image="", text="Copie signalée. Chargement de la suivante…")
|
||||
|
||||
def on_open_pdf(self, event):
|
||||
if self.is_viewing and self.current_json_path:
|
||||
a = self.current_json_path.stem.split('_')[0] + ".pdf"
|
||||
@@ -530,17 +565,24 @@ def _selected_images(workspace: EvaluationWorkspace, target: Path) -> list[Path]
|
||||
return sorted(workspace.cutleft_dir.glob("*.jpg"), key=natural_key)
|
||||
|
||||
|
||||
def run(workspace: EvaluationWorkspace, target: Path) -> ExitCode:
|
||||
def run(workspace: EvaluationWorkspace, target: Path, *, marked: bool = False) -> ExitCode:
|
||||
workspace.require_files("labels")
|
||||
all_labels = read_all_labels(workspace.root)
|
||||
files_to_process = _selected_images(workspace, target)
|
||||
if marked:
|
||||
copies = marked_copy_paths(workspace)
|
||||
if not copies:
|
||||
print("No flagged copies to verify.")
|
||||
return ExitCode.SUCCESS
|
||||
files_to_process = [image for pdf in copies for image in _selected_images(workspace, pdf)]
|
||||
else:
|
||||
files_to_process = _selected_images(workspace, target)
|
||||
if not files_to_process:
|
||||
print(f"No Cutleft images found for {target}")
|
||||
return ExitCode.PARTIAL
|
||||
|
||||
print(
|
||||
"o to open pdf, O original pdf, e to edit part, p to go back, "
|
||||
"i to open the statement, click for coordinates"
|
||||
"i to open the statement, ! to flag and skip the copy, click for coordinates"
|
||||
)
|
||||
input_queue = queue.Queue(maxsize=5)
|
||||
worker = threading.Thread(
|
||||
@@ -556,7 +598,9 @@ def run(workspace: EvaluationWorkspace, target: Path) -> ExitCode:
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
return target_parser("Interactively verify detected label coordinates")
|
||||
parser = target_parser("Interactively verify detected label coordinates")
|
||||
parser.add_argument("--marked", action="store_true", help="Verify only copies with remaining flags")
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
@@ -564,7 +608,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
|
||||
def handle(args: argparse.Namespace) -> ExitCode:
|
||||
workspace, target = workspace_from_target(args)
|
||||
return run(workspace, target)
|
||||
return run(workspace, target, marked=args.marked)
|
||||
|
||||
return execute(parser, argv, handle)
|
||||
|
||||
|
||||
Reference in new issue
Block a user