From 3ec7f8900997cc39ac43c203218f996ba50b35c9 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Mon, 28 Sep 2026 08:28:23 +0200 Subject: [PATCH] fix(magazine): snap boxes also for vision-labelled paragraphs, per-side growth, run after page re-OCR Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 2 +- core/magazine.py | 25 ++++++++++++++----------- routes/magazine.py | 1 + tests/test_magazine.py | 6 +++++- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04853c3..8084630 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/core/magazine.py b/core/magazine.py index d1d439b..57d53a1 100644 --- a/core/magazine.py +++ b/core/magazine.py @@ -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 diff --git a/routes/magazine.py b/routes/magazine.py index 8c3122a..0058ab2 100644 --- a/routes/magazine.py +++ b/routes/magazine.py @@ -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) diff --git a/tests/test_magazine.py b/tests/test_magazine.py index 19ce58b..c4f7952 100644 --- a/tests/test_magazine.py +++ b/tests/test_magazine.py @@ -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