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)
|
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
|
# Ensure consistency of answer placements
|
||||||
for p in json_data:
|
for p in json_data:
|
||||||
pid = p["id"]
|
pid = p["id"]
|
||||||
@@ -899,7 +912,10 @@ def run_configured(args: argparse.Namespace) -> ExitCode:
|
|||||||
"data": b64_img,
|
"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 queue import Empty, Queue
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
|
import pymupdf
|
||||||
from pdf2image import convert_from_path
|
from pdf2image import convert_from_path
|
||||||
from PIL import Image, ImageTk
|
from PIL import Image, ImageTk
|
||||||
|
|
||||||
@@ -31,6 +32,8 @@ DELIMITER_COLOR = (0, 0, 0)
|
|||||||
OUTPUT_SIZE = (1800, 1000)
|
OUTPUT_SIZE = (1800, 1000)
|
||||||
CROP_SHIFT_STEP = 50
|
CROP_SHIFT_STEP = 50
|
||||||
CROP_WIDTH_STEP = 50
|
CROP_WIDTH_STEP = 50
|
||||||
|
RENDER_DPI = 200
|
||||||
|
NARROW_PAGE_RATIO = 0.8
|
||||||
pdf_cache_lock = threading.Lock()
|
pdf_cache_lock = threading.Lock()
|
||||||
|
|
||||||
# The requested spelling is kept as the public switch for this alternate layout.
|
# 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:
|
def stitch_images(image_list: list[Image.Image]) -> Image.Image | None:
|
||||||
if not image_list:
|
if not image_list:
|
||||||
return None
|
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
|
total_width += (len(image_list) - 1) * DELIMITER_WIDTH
|
||||||
max_height = max(image.height for image in image_list)
|
max_height = max(image.height for image in image_list)
|
||||||
combined = Image.new("RGB", (total_width, max_height), color="white")
|
combined = Image.new("RGB", (total_width, max_height), color="white")
|
||||||
x_offset = 0
|
x_offset = 0
|
||||||
for index, image in enumerate(image_list):
|
for index, image in enumerate(image_list):
|
||||||
combined.paste(image, (x_offset, 0))
|
combined.paste(image, (x_offset, 0))
|
||||||
x_offset += image.width
|
x_offset += column_width
|
||||||
if index < len(image_list) - 1:
|
if index < len(image_list) - 1:
|
||||||
delimiter = Image.new(
|
delimiter = Image.new(
|
||||||
"RGB", (DELIMITER_WIDTH, max_height), color=DELIMITER_COLOR
|
"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]:
|
def _get_pdf_pages_cached(pdf_path: Path) -> list[Image.Image]:
|
||||||
# Label coordinates use the full, displayed MediaBox, including on PDFs
|
# Label coordinates use the full, displayed MediaBox, including on PDFs
|
||||||
# previously cropped by crop-margins. Keep this in sync with split-answers.
|
# 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]:
|
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)
|
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(
|
def process_single_pdf(
|
||||||
pdf_path: Path,
|
pdf_path: Path,
|
||||||
shift_offset: int = 0,
|
shift_offset: int = 0,
|
||||||
@@ -91,10 +120,20 @@ def process_single_pdf(
|
|||||||
"""Convert one PDF into a preview, full-resolution splits and metadata."""
|
"""Convert one PDF into a preview, full-resolution splits and metadata."""
|
||||||
try:
|
try:
|
||||||
cropped_images = []
|
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
|
width, height = image.size
|
||||||
if max_per_file == 1:
|
if max_per_file == 1:
|
||||||
left, right = 0, width
|
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:
|
else:
|
||||||
left = max(0, 100 + shift_offset)
|
left = max(0, 100 + shift_offset)
|
||||||
right = min(
|
right = min(
|
||||||
@@ -103,6 +142,8 @@ def process_single_pdf(
|
|||||||
)
|
)
|
||||||
if right > left:
|
if right > left:
|
||||||
cropped_images.append(image.crop((left, 0, right, height)))
|
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:
|
if not cropped_images:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -123,6 +164,7 @@ def process_single_pdf(
|
|||||||
"total_pages": len(cropped_images),
|
"total_pages": len(cropped_images),
|
||||||
"number_of_files": len(split_images),
|
"number_of_files": len(split_images),
|
||||||
"columns_per_file": distribution,
|
"columns_per_file": distribution,
|
||||||
|
"full_width_pages": full_width_pages,
|
||||||
}
|
}
|
||||||
return preview, split_images, schema
|
return preview, split_images, schema
|
||||||
except Exception as exc: # noqa: BLE001 - interactive item failure
|
except Exception as exc: # noqa: BLE001 - interactive item failure
|
||||||
|
|||||||
+17
-3
@@ -1,6 +1,8 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import io
|
import io
|
||||||
|
import json
|
||||||
from . import utils
|
from . import utils
|
||||||
|
from . import read_json
|
||||||
from . import configuration as settings
|
from . import configuration as settings
|
||||||
from .gemini_config import thinking_config
|
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
|
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 ""
|
text = get_label_text_content(input_dir, full_label) or ""
|
||||||
corr = get_label_sol_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 ""
|
persp = get_label_persp_content(input_dir, full_label) or ""
|
||||||
@@ -117,7 +119,18 @@ def make_prompt(input_dir,full_label):
|
|||||||
+ persp
|
+ persp
|
||||||
+ "\n```\n"
|
+ "\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
|
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]
|
# The root model for parsing is be: List[EvaluationEntry]
|
||||||
def generate_request(input_dir, file, full_label, *, thinking_level=settings.THINKING_LEVEL_PRO):
|
def generate_request(input_dir, file, full_label, *, thinking_level=settings.THINKING_LEVEL_PRO):
|
||||||
"""Generates request for Gemini."""
|
"""Generates request for Gemini."""
|
||||||
prompt = make_prompt(input_dir, full_label)
|
|
||||||
image_path = Path(file)
|
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 = [
|
contents = [
|
||||||
types.Content(
|
types.Content(
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
|||||||
"Prétraitement des copies",
|
"Prétraitement des copies",
|
||||||
"Découper une partie à gauche pour détection des labels",
|
"Découper une partie à gauche pour détection des labels",
|
||||||
"Produit les images de la partie gauche des copies. "
|
"Produit les images de la partie gauche des copies. "
|
||||||
|
"Pour les sous-pages étroites, la marge commence au bord gauche et est limitée à la largeur disponible. "
|
||||||
"Dans la fenêtre de découpe, utilisez les flèches : ←/→ déplacent la zone "
|
"Dans la fenêtre de découpe, utilisez les flèches : ←/→ déplacent la zone "
|
||||||
"de 50 px vers la gauche/droite, ↑ l’élargit de 50 px et ↓ la réduit de 50 px. "
|
"de 50 px vers la gauche/droite, ↑ l’élargit de 50 px et ↓ la réduit de 50 px. "
|
||||||
"1 utilise les pages entières, Entrée valide et ! signale une erreur."
|
"1 utilise les pages entières, Entrée valide et ! signale une erreur."
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
from copienator import EvaluationWorkspace, atomic_write_json
|
||||||
|
from copienator.commands import correction
|
||||||
|
|
||||||
|
|
||||||
|
class CorrectionIdTests(unittest.TestCase):
|
||||||
|
def test_mismatched_ids_are_rejected_before_any_side_effects(self):
|
||||||
|
for ids, diagnostic in (
|
||||||
|
(["37", "03", "05"], "unexpected IDs: ['03']"),
|
||||||
|
(["37"], "missing IDs: ['05']"),
|
||||||
|
(["37", "05", "05"], "duplicate IDs: ['05']"),
|
||||||
|
):
|
||||||
|
with self.subTest(ids=ids), tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
group = root / "Par label" / "Ex 1" / "Group_1.jpg"
|
||||||
|
group.parent.mkdir(parents=True)
|
||||||
|
group.touch()
|
||||||
|
atomic_write_json(group.with_suffix(".json"), [
|
||||||
|
["37", 0, 500, 1], ["05", 550, 1000, 1],
|
||||||
|
])
|
||||||
|
pdf = root / "Copies" / "Copie37" / "Ex 1.pdf"
|
||||||
|
pdf.parent.mkdir(parents=True)
|
||||||
|
pdf.write_bytes(b"original")
|
||||||
|
workspace = EvaluationWorkspace(root)
|
||||||
|
prior = {"Other": [[{"id": "22", "result": {"score": 4}}]]}
|
||||||
|
atomic_write_json(workspace.correction_file, prior)
|
||||||
|
before = workspace.correction_file.read_bytes()
|
||||||
|
args = correction.build_parser().parse_args([str(root)])
|
||||||
|
correction.configure_runtime(workspace, [(str(group), "Ex 1")], args, api_client=Mock())
|
||||||
|
response = [{"id": pid, "result": {
|
||||||
|
"score": 0, "error": "empty-answer", "feedback": [],
|
||||||
|
}} for pid in ids]
|
||||||
|
with patch.object(correction.prompting, "generate_request", return_value=([], {})), patch.object(
|
||||||
|
correction, "handle_label_errors"
|
||||||
|
) as handle:
|
||||||
|
correction.process_single_task((str(group), "Ex 1"), json.dumps(response))
|
||||||
|
handle.assert_not_called()
|
||||||
|
self.assertEqual(pdf.read_bytes(), b"original")
|
||||||
|
self.assertFalse(pdf.with_name("Ex 1_old.pdf").exists())
|
||||||
|
self.assertEqual(workspace.correction_file.read_bytes(), before)
|
||||||
|
self.assertEqual(correction.completed_tasks, [])
|
||||||
|
self.assertEqual(correction.results, prior)
|
||||||
|
self.assertIn(diagnostic, correction.errors_summary[0][0])
|
||||||
|
|
||||||
|
def test_valid_ids_can_be_returned_in_different_order(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
group = root / "Par label" / "Ex 1" / "Group_1.jpg"
|
||||||
|
group.parent.mkdir(parents=True)
|
||||||
|
group.touch()
|
||||||
|
atomic_write_json(group.with_suffix(".json"), [
|
||||||
|
["37", 0, 500, 1], ["05", 550, 1000, 1],
|
||||||
|
])
|
||||||
|
workspace = EvaluationWorkspace(root)
|
||||||
|
args = correction.build_parser().parse_args([str(root)])
|
||||||
|
correction.configure_runtime(workspace, [(str(group), "Ex 1")], args, api_client=Mock())
|
||||||
|
response = [{"id": pid, "result": {
|
||||||
|
"score": 4, "error": "", "feedback": [],
|
||||||
|
}} for pid in ["05", "37"]]
|
||||||
|
with patch.object(correction.prompting, "generate_request", return_value=([], {})):
|
||||||
|
correction.process_single_task((str(group), "Ex 1"), json.dumps(response))
|
||||||
|
self.assertEqual(correction.errors_summary, [])
|
||||||
|
self.assertEqual(correction.results["Ex 1"], [response])
|
||||||
|
self.assertEqual(correction.completed_tasks, [(str(group), "Ex 1")])
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pymupdf
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from copienator.commands import cutleft
|
||||||
|
|
||||||
|
|
||||||
|
class NarrowPageCropTests(unittest.TestCase):
|
||||||
|
def test_mixed_pages_crop_narrow_margin_from_very_left(self):
|
||||||
|
normal = Image.new('RGB', (900, 600), 'red')
|
||||||
|
normal.paste('green', (100, 0, 400, 600))
|
||||||
|
narrow = Image.new('RGB', (450, 600), 'red')
|
||||||
|
narrow.paste('blue', (0, 0, 300, 600))
|
||||||
|
thin = Image.new('RGB', (200, 600), 'yellow')
|
||||||
|
with patch.object(cutleft, 'get_pdf_pages', return_value=[normal, narrow, thin]):
|
||||||
|
result = cutleft.process_single_pdf(Path('/tmp/no-original/Copy.pdf'))
|
||||||
|
_, images, schema = result
|
||||||
|
image = images[0]
|
||||||
|
self.assertEqual(schema['full_width_pages'], [3])
|
||||||
|
self.assertEqual(schema['columns_per_file'], [3])
|
||||||
|
self.assertEqual(image.size, (910, 600))
|
||||||
|
self.assertEqual(image.getpixel((0, 0)), (0, 128, 0))
|
||||||
|
self.assertEqual(image.getpixel((299, 0)), (0, 128, 0))
|
||||||
|
self.assertEqual(image.getpixel((300, 0)), (0, 0, 0))
|
||||||
|
self.assertEqual(image.getpixel((305, 0)), (0, 0, 255))
|
||||||
|
self.assertEqual(image.getpixel((604, 0)), (0, 0, 255))
|
||||||
|
self.assertEqual(image.getpixel((610, 0)), (255, 255, 0))
|
||||||
|
self.assertEqual(image.getpixel((809, 0)), (255, 255, 0))
|
||||||
|
self.assertEqual(image.getpixel((810, 0)), (255, 255, 255))
|
||||||
|
self.assertEqual(image.getpixel((909, 0)), (255, 255, 255))
|
||||||
|
|
||||||
|
def test_narrow_margin_can_be_shifted_and_enlarged(self):
|
||||||
|
normal = Image.new('RGB', (900, 600), 'white')
|
||||||
|
narrow = Image.new('RGB', (450, 600), 'red')
|
||||||
|
narrow.paste('blue', (50, 0, 450, 600))
|
||||||
|
with patch.object(cutleft, 'get_pdf_pages', return_value=[normal, narrow]):
|
||||||
|
result = cutleft.process_single_pdf(
|
||||||
|
Path('/tmp/no-original/Copy.pdf'), shift_offset=50, width_offset=100)
|
||||||
|
self.assertEqual(result[1][0].size, (805, 600))
|
||||||
|
self.assertEqual(result[1][0].getpixel((405, 0)), (0, 0, 255))
|
||||||
|
self.assertEqual(result[1][0].getpixel((804, 0)), (0, 0, 255))
|
||||||
|
|
||||||
|
def test_legacy_original_identifies_narrow_pages_without_normal_siblings(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
originals = root / 'Copies Originales'
|
||||||
|
originals.mkdir()
|
||||||
|
with pymupdf.open() as document:
|
||||||
|
document.new_page(width=1191, height=842)
|
||||||
|
document.save(originals / 'Copy.pdf')
|
||||||
|
images = [Image.new('RGB', (900, 2339)), Image.new('RGB', (750, 2339))]
|
||||||
|
with patch.object(cutleft, 'get_pdf_pages', return_value=images):
|
||||||
|
result = cutleft.process_single_pdf(root / 'Copies' / 'Copy.pdf')
|
||||||
|
self.assertEqual(result[2]['full_width_pages'], [])
|
||||||
|
self.assertEqual(result[1][0].width, 2 * int((1191 / 2 * 200 / 72) // 3) + 5)
|
||||||
|
|
||||||
|
def test_small_main_split_adjustments_keep_margin_cropping(self):
|
||||||
|
images = [Image.new('RGB', (1600, 2339)), Image.new('RGB', (1694, 2339))]
|
||||||
|
with patch.object(cutleft, 'get_pdf_pages', return_value=images):
|
||||||
|
result = cutleft.process_single_pdf(Path('/tmp/no-original/Copy.pdf'))
|
||||||
|
self.assertEqual(result[2]['full_width_pages'], [])
|
||||||
|
self.assertEqual(result[1][0].width, 2 * (1694 // 3) + 5)
|
||||||
|
|
||||||
|
def test_fullpage_option_still_keeps_every_page_separately(self):
|
||||||
|
images = [Image.new('RGB', (900, 600)), Image.new('RGB', (450, 600))]
|
||||||
|
with patch.object(cutleft, 'get_pdf_pages', return_value=images):
|
||||||
|
result = cutleft.process_single_pdf(Path('/tmp/no-original/Copy.pdf'), max_per_file=1)
|
||||||
|
self.assertEqual([image.size for image in result[1]], [(900, 600), (450, 600)])
|
||||||
|
self.assertEqual(result[2]['columns_per_file'], [1, 1])
|
||||||
@@ -26,6 +26,7 @@ class GeminiConfigTests(unittest.TestCase):
|
|||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
image = Path(directory) / "answer.jpg"
|
image = Path(directory) / "answer.jpg"
|
||||||
image.write_bytes(b"image")
|
image.write_bytes(b"image")
|
||||||
|
image.with_suffix(".json").write_text('[["01", 0, 400, 1.0, "Ex 1"]]')
|
||||||
for level in ("minimal", "low", "medium", "high", None):
|
for level in ("minimal", "low", "medium", "high", None):
|
||||||
with self.subTest(level=level), patch.object(prompting, "make_prompt", return_value="Grade"):
|
with self.subTest(level=level), patch.object(prompting, "make_prompt", return_value="Grade"):
|
||||||
_, config = prompting.generate_request(directory, image, "Ex 1", thinking_level=level)
|
_, config = prompting.generate_request(directory, image, "Ex 1", thinking_level=level)
|
||||||
|
|||||||
@@ -1775,8 +1775,9 @@ class StandardCliTests(unittest.TestCase):
|
|||||||
args,
|
args,
|
||||||
api_client=Mock(),
|
api_client=Mock(),
|
||||||
)
|
)
|
||||||
with patch.object(module.prompting, "make_prompt", return_value="prompt"):
|
with patch.object(module.prompting, "make_prompt", return_value="prompt") as make_prompt:
|
||||||
self.assertEqual(module.run_configured(args), 0)
|
self.assertEqual(module.run_configured(args), 0)
|
||||||
|
make_prompt.assert_called_once_with(evaluation, "Ex 1", expected_ids=["01"])
|
||||||
lines = (evaluation / "batch_requests_flash.jsonl").read_text().splitlines()
|
lines = (evaluation / "batch_requests_flash.jsonl").read_text().splitlines()
|
||||||
self.assertEqual(len(lines), 1)
|
self.assertEqual(len(lines), 1)
|
||||||
self.assertEqual(json.loads(lines[0])["key"], str(image))
|
self.assertEqual(json.loads(lines[0])["key"], str(image))
|
||||||
|
|||||||
@@ -1,10 +1,26 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from copienator import prompting
|
from copienator import prompting
|
||||||
|
|
||||||
|
|
||||||
class PromptingTests(unittest.TestCase):
|
class PromptingTests(unittest.TestCase):
|
||||||
|
def test_live_prompt_includes_only_metadata_ids_in_order(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
image = Path(directory) / "Group_16.jpg"
|
||||||
|
image.write_bytes(b"image")
|
||||||
|
image.with_suffix(".json").write_text(
|
||||||
|
'[["22", 50, 834, 1], ["37", 904, 3250, 1], '
|
||||||
|
'["13", 3320, 4072, 1], ["05", 4142, 4892, 1]]'
|
||||||
|
)
|
||||||
|
contents, _ = prompting.generate_request(directory, image, "I)12)")
|
||||||
|
prompt = contents[0].parts[1].text
|
||||||
|
self.assertIn('["22", "37", "13", "05"]', prompt)
|
||||||
|
self.assertIn("exactement un résultat par identifiant", prompt)
|
||||||
|
self.assertIn("ignore les numéros manuscrits", prompt)
|
||||||
|
|
||||||
def test_perspective_is_inserted_with_alternative_method_guidance(self) -> None:
|
def test_perspective_is_inserted_with_alternative_method_guidance(self) -> None:
|
||||||
with patch.object(
|
with patch.object(
|
||||||
prompting, "get_label_text_content", return_value="Question"
|
prompting, "get_label_text_content", return_value="Question"
|
||||||
|
|||||||
Reference in new issue
Block a user