fix(magazine): snap boxes also for vision-labelled paragraphs, per-side growth, run after page re-OCR

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-09-28 08:28:23 +02:00
parent 559c165010
commit 3ec7f89009
4 changed files with 21 additions and 13 deletions

View File

@ -12,7 +12,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
- **Pronunciation & spelling panel (per article).** Acronyms are spoken from the article's language (decided once per article, with per-block override for quotations) using an extended built-in list, letter-by-letter spelling for `USB`/`HDMI`-style acronyms, and an editable per-article glossary. Unfamiliar acronyms are listed with their sentence, and the language model can decide expansion / letters / word from context.
- **German and English spellchecker with fix-or-mark.** Unknown words are underlined in the review list and listed with suggestions per page and per article; click a suggestion (undoable), or *Keep* to add the word to the issue's dictionary. *Fix with language model* corrects only what passes strict guards (a dictionary suggestion, or a known word within two edits) and keeps names/terms; the whole-magazine quality check now runs the same pass and reports pages with words still marked.
- **Clean up boxes now also fits text boxes to their text.** A side that cuts through letters grows (at most a letter or two, never into a neighbouring box) and empty margins are trimmed. Boxes you drew or resized by hand are never touched.
- **Clean up boxes now also fits text boxes to their text.** A side that cuts through letters grows (at most a letter or two, never into a neighbouring box) and empty margins are trimmed. Boxes you drew or resized by hand are never touched. It also runs automatically after **OCR again (this page)**; on page 61 of the measured issue it widened the left-column paragraphs ~10 pt to cover their text, cutting clipped edges from 7 to 1.
### Fixed
- **The quality check no longer flags correct text as "suspect OCR".** The word check guessed the language per fragment (`iPhones statt Token` read as English) and flagged valid German; it now uses the page's language, the same lenient German rules as the spellchecker, and treats words that recur on the page (names, brands) as vocabulary. Page 61 of the measured issue went from 11 suspect regions to 3 (one a genuinely truncated word), and the issue from 758 to 610 before the language fix's full effect.

View File

@ -2949,7 +2949,7 @@ def snap_text_boxes(doc_dir: Path, layout: dict[str, Any],
for block in on_page:
if (block["type"] not in snappable or block.get("source") == "manual"
or block.get("box_manual") or block.get("region_label")):
or block.get("box_manual")):
continue
left, top, right, bottom = to_px(block["bbox"])
if right - left < _SNAP_TEXT_MIN_PX or bottom - top < _SNAP_TEXT_MIN_PX:
@ -2972,16 +2972,19 @@ def snap_text_boxes(doc_dir: Path, layout: dict[str, Any],
return bool(free.sum() >= 12 and (pixels[r0:r1, c0:c1] & free).sum() / free.sum() >= _AUDIT_CLIP_INK)
step = _SNAP_TEXT_STEP_PX
for _ in range(_SNAP_TEXT_MAX_GROW_PX // step):
if strip_has_text(top, bottom, left - step, left) and left - step >= 0:
left -= step
elif strip_has_text(top, bottom, right, right + step) and right + step <= width:
right += step
elif strip_has_text(top - step, top, left, right) and top - step >= 0:
top -= step
elif strip_has_text(bottom, bottom + step, left, right) and bottom + step <= height:
bottom += step
else:
budget = {"left": _SNAP_TEXT_MAX_GROW_PX, "right": _SNAP_TEXT_MAX_GROW_PX,
"top": _SNAP_TEXT_MAX_GROW_PX, "bottom": _SNAP_TEXT_MAX_GROW_PX}
while True: # each side has its own allowance; stop when none can grow
grew = False
if budget["left"] > 0 and left - step >= 0 and strip_has_text(top, bottom, left - step, left):
left -= step; budget["left"] -= step; grew = True
if budget["right"] > 0 and right + step <= width and strip_has_text(top, bottom, right, right + step):
right += step; budget["right"] -= step; grew = True
if budget["top"] > 0 and top - step >= 0 and strip_has_text(top - step, top, left, right):
top -= step; budget["top"] -= step; grew = True
if budget["bottom"] > 0 and bottom + step <= height and strip_has_text(bottom, bottom + step, left, right):
bottom += step; budget["bottom"] -= step; grew = True
if not grew:
break
if (left, top, right, bottom) == original:
# Not clipped: trim the empty margins instead. A line is empty margin when it

View File

@ -858,6 +858,7 @@ def _reocr_page_sync(doc_id: str, job_id: str, page: int,
_recognise_semantic_layout(updated, _load_settings(), page, doc_dir=d)
mag.refresh_visual_regions(d, updated, page)
updated, cleanup = mag.clean_up_regions(updated, pages=[page])
updated, _ = mag.snap_text_boxes(d, updated, pages=[page]) # boxes cover their whole text
articles = mag.load_articles(d)
mag.remap_reviewed_page(articles, layout, updated, page)
_add_split_paragraphs_to_articles(articles, cleanup)

View File

@ -2725,11 +2725,15 @@ class SnapTextBoxesTests(unittest.TestCase):
def test_boxes_placed_by_hand_and_labelled_regions_are_left_alone(self):
boxes = [("m", [60, 60, 250, 200], {"source": "manual"}), ("e", [60, 60, 250, 200], {"box_manual": True}),
("l", [60, 60, 250, 200], {"region_label": "in_picture"})]
("l", [60, 60, 250, 200], {"type": "furniture", "region_label": "in_picture"})]
got, moved, _ = self.snap(boxes)
self.assertEqual(moved, [])
self.assertEqual(got["m"], [60, 60, 250, 200])
def test_a_vision_detected_paragraph_labelled_text_is_snapped_too(self):
got, moved, _ = self.snap([("a", [60, 60, 250, 200], {"region_label": "text", "source": "vision_ocr"})])
self.assertEqual(len(moved), 1)
def test_a_box_already_fitted_does_not_move_again_and_text_is_untouched(self):
first, _, layout = self.snap([("a", [60, 60, 250, 200], {})])
from core.magazine import snap_text_boxes