Provide Gemini with the list of IDs in correction request
This commit is contained in:
1 parent
f978310c4c
commit
2d0f7a219f
9 files changed
+242
-9
No files matched your search
@@ -532,6 +532,19 @@ def process_single_task(task_tuple, precomputed_response=None):
|
||||
|
||||
json_data = parse_correction_response(full_response_text)
|
||||
|
||||
# Validate the whole group before moving PDFs or applying corrections.
|
||||
returned_ids = [entry["id"] for entry in json_data]
|
||||
unexpected = sorted(set(returned_ids) - set(d_data))
|
||||
missing = sorted(set(d_data) - set(returned_ids))
|
||||
duplicates = sorted({pid for pid in returned_ids if returned_ids.count(pid) > 1})
|
||||
if unexpected or missing or duplicates:
|
||||
raise ValueError(
|
||||
f"Correction ID mismatch: expected {list(d_data)}, "
|
||||
f"received {returned_ids}; unexpected IDs: {unexpected}; "
|
||||
f"missing IDs: {missing}; duplicate IDs: {duplicates}. "
|
||||
"No corrections from this response were applied."
|
||||
)
|
||||
|
||||
# Ensure consistency of answer placements
|
||||
for p in json_data:
|
||||
pid = p["id"]
|
||||
@@ -899,7 +912,10 @@ def run_configured(args: argparse.Namespace) -> ExitCode:
|
||||
"data": b64_img,
|
||||
}
|
||||
},
|
||||
{"text": prompting.make_prompt(INPUT_DIR, label)},
|
||||
{"text": prompting.make_prompt(
|
||||
INPUT_DIR, label,
|
||||
expected_ids=[row[0] for row in group_data],
|
||||
)},
|
||||
],
|
||||
}
|
||||
],
|
||||
|
||||
@@ -10,6 +10,7 @@ from pathlib import Path
|
||||
from queue import Empty, Queue
|
||||
from threading import Thread
|
||||
|
||||
import pymupdf
|
||||
from pdf2image import convert_from_path
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
@@ -31,6 +32,8 @@ DELIMITER_COLOR = (0, 0, 0)
|
||||
OUTPUT_SIZE = (1800, 1000)
|
||||
CROP_SHIFT_STEP = 50
|
||||
CROP_WIDTH_STEP = 50
|
||||
RENDER_DPI = 200
|
||||
NARROW_PAGE_RATIO = 0.8
|
||||
pdf_cache_lock = threading.Lock()
|
||||
|
||||
# The requested spelling is kept as the public switch for this alternate layout.
|
||||
@@ -52,14 +55,17 @@ def distribute_pages(total_pages: int, max_per_file: int = 5) -> list[int]:
|
||||
def stitch_images(image_list: list[Image.Image]) -> Image.Image | None:
|
||||
if not image_list:
|
||||
return None
|
||||
total_width = sum(image.width for image in image_list)
|
||||
# Downstream label coordinates assume equally spaced page columns. Pad
|
||||
# narrower strips instead of scaling their content or vertical coordinates.
|
||||
column_width = max(image.width for image in image_list)
|
||||
total_width = column_width * len(image_list)
|
||||
total_width += (len(image_list) - 1) * DELIMITER_WIDTH
|
||||
max_height = max(image.height for image in image_list)
|
||||
combined = Image.new("RGB", (total_width, max_height), color="white")
|
||||
x_offset = 0
|
||||
for index, image in enumerate(image_list):
|
||||
combined.paste(image, (x_offset, 0))
|
||||
x_offset += image.width
|
||||
x_offset += column_width
|
||||
if index < len(image_list) - 1:
|
||||
delimiter = Image.new(
|
||||
"RGB", (DELIMITER_WIDTH, max_height), color=DELIMITER_COLOR
|
||||
@@ -73,7 +79,7 @@ def stitch_images(image_list: list[Image.Image]) -> Image.Image | None:
|
||||
def _get_pdf_pages_cached(pdf_path: Path) -> list[Image.Image]:
|
||||
# Label coordinates use the full, displayed MediaBox, including on PDFs
|
||||
# previously cropped by crop-margins. Keep this in sync with split-answers.
|
||||
return convert_from_path(pdf_path, use_cropbox=False)
|
||||
return convert_from_path(pdf_path, use_cropbox=False, dpi=RENDER_DPI)
|
||||
|
||||
|
||||
def get_pdf_pages(pdf_path: Path) -> list[Image.Image]:
|
||||
@@ -82,6 +88,29 @@ def get_pdf_pages(pdf_path: Path) -> list[Image.Image]:
|
||||
return _get_pdf_pages_cached(pdf_path)
|
||||
|
||||
|
||||
def normal_page_width(pdf_path: Path, images: list[Image.Image]) -> float:
|
||||
"""Infer the unsplit sheet width, including for legacy PDFs without metadata."""
|
||||
reference = max(image.width for image in images)
|
||||
original = pdf_path.parent.parent / "Copies Originales" / pdf_path.name
|
||||
try:
|
||||
if not original.is_file():
|
||||
return reference
|
||||
with pymupdf.open(original) as document:
|
||||
# Landscape scans contain two normal booklet pages; portrait scans
|
||||
# contain one. MediaBox dimensions match get_pdf_pages above.
|
||||
widths = []
|
||||
for page in document:
|
||||
rect = page.mediabox * page.rotation_matrix
|
||||
width = rect.width / 2 if rect.width > rect.height else rect.width
|
||||
widths.append(width * RENDER_DPI / 72)
|
||||
if widths:
|
||||
reference = max(widths)
|
||||
except (OSError, RuntimeError, ValueError):
|
||||
# A missing/unreadable backup must not prevent reviewing a valid copy.
|
||||
pass
|
||||
return reference
|
||||
|
||||
|
||||
def process_single_pdf(
|
||||
pdf_path: Path,
|
||||
shift_offset: int = 0,
|
||||
@@ -91,10 +120,20 @@ def process_single_pdf(
|
||||
"""Convert one PDF into a preview, full-resolution splits and metadata."""
|
||||
try:
|
||||
cropped_images = []
|
||||
for image in get_pdf_pages(pdf_path):
|
||||
images = get_pdf_pages(pdf_path)
|
||||
if not images:
|
||||
return None
|
||||
reference_width = normal_page_width(pdf_path, images)
|
||||
full_width_pages = []
|
||||
for page_index, image in enumerate(images):
|
||||
width, height = image.size
|
||||
if max_per_file == 1:
|
||||
left, right = 0, width
|
||||
elif width < NARROW_PAGE_RATIO * reference_width:
|
||||
# Secondary subpages start at their own left edge. Keep the
|
||||
# normal margin width, capped to the available page content.
|
||||
left = min(width - 1, max(0, shift_offset))
|
||||
right = min(width, left + int(reference_width // 3) + max(0, width_offset))
|
||||
else:
|
||||
left = max(0, 100 + shift_offset)
|
||||
right = min(
|
||||
@@ -103,6 +142,8 @@ def process_single_pdf(
|
||||
)
|
||||
if right > left:
|
||||
cropped_images.append(image.crop((left, 0, right, height)))
|
||||
if left == 0 and right == width:
|
||||
full_width_pages.append(page_index + 1)
|
||||
if not cropped_images:
|
||||
return None
|
||||
|
||||
@@ -123,6 +164,7 @@ def process_single_pdf(
|
||||
"total_pages": len(cropped_images),
|
||||
"number_of_files": len(split_images),
|
||||
"columns_per_file": distribution,
|
||||
"full_width_pages": full_width_pages,
|
||||
}
|
||||
return preview, split_images, schema
|
||||
except Exception as exc: # noqa: BLE001 - interactive item failure
|
||||
|
||||
+17
-3
@@ -1,6 +1,8 @@
|
||||
from pathlib import Path
|
||||
import io
|
||||
import json
|
||||
from . import utils
|
||||
from . import read_json
|
||||
from . import configuration as settings
|
||||
from .gemini_config import thinking_config
|
||||
|
||||
@@ -103,7 +105,7 @@ sur les autres questions."""
|
||||
|
||||
from .utils import get_label_text_content, get_label_sol_content, get_label_persp_content
|
||||
|
||||
def make_prompt(input_dir,full_label):
|
||||
def make_prompt(input_dir,full_label, *, expected_ids=None):
|
||||
text = get_label_text_content(input_dir, full_label) or ""
|
||||
corr = get_label_sol_content(input_dir, full_label) or ""
|
||||
persp = get_label_persp_content(input_dir, full_label) or ""
|
||||
@@ -117,7 +119,18 @@ def make_prompt(input_dir,full_label):
|
||||
+ persp
|
||||
+ "\n```\n"
|
||||
)
|
||||
return main_prompt.replace("<<text>>", text).replace("<<corr>>", corr).replace("<<persp>>", persp).replace("<<label>>", full_label)
|
||||
prompt = main_prompt.replace("<<text>>", text).replace("<<corr>>", corr).replace("<<persp>>", persp).replace("<<label>>", full_label)
|
||||
if expected_ids is not None:
|
||||
prompt += (
|
||||
"\n\nIdentifiants exacts présents dans cette image, de haut en bas : "
|
||||
+ json.dumps(list(expected_ids), ensure_ascii=False)
|
||||
+ ".\nRenvoie exactement un résultat par identifiant de cette liste, "
|
||||
"sans omission, doublon ni identifiant supplémentaire. Conserve les zéros initiaux. "
|
||||
"Utilise uniquement les en-têtes imprimés « ID: … » pour identifier les réponses ; "
|
||||
"ignore les numéros manuscrits. Tout le contenu jusqu’au prochain en-tête "
|
||||
"imprimé appartient au même identifiant, même s’il contient plusieurs feuilles."
|
||||
)
|
||||
return prompt
|
||||
|
||||
|
||||
from pydantic import BaseModel, Field, TypeAdapter
|
||||
@@ -190,8 +203,9 @@ def batch_generation_config(thinking_level):
|
||||
# The root model for parsing is be: List[EvaluationEntry]
|
||||
def generate_request(input_dir, file, full_label, *, thinking_level=settings.THINKING_LEVEL_PRO):
|
||||
"""Generates request for Gemini."""
|
||||
prompt = make_prompt(input_dir, full_label)
|
||||
image_path = Path(file)
|
||||
group_data = read_json(image_path.with_suffix(".json"))
|
||||
prompt = make_prompt(input_dir, full_label, expected_ids=[row[0] for row in group_data])
|
||||
|
||||
contents = [
|
||||
types.Content(
|
||||
|
||||
Reference in new issue
Block a user