feat(magazine): auto-add figures for unboxed pictures, junk filter for linking, keep quality report in step
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
bf5c795f02
commit
c62cc6953d
@ -31,6 +31,10 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
|||||||
|
|
||||||
- **"Article ends" is only claimed when it is true.** An article counts as ended when it is signed off (`(rah) et`, `(hag)` — OCR noise after the code is tolerated) or when the next region, on the same page or at the top of the next, is a headline, deck or byline. Otherwise the last box shows a clickable *no end marker — link page N ›* chip that adds the next page's text in one click.
|
- **"Article ends" is only claimed when it is true.** An article counts as ended when it is signed off (`(rah) et`, `(hag)` — OCR noise after the code is tolerated) or when the next region, on the same page or at the top of the next, is a headline, deck or byline. Otherwise the last box shows a clickable *no end marker — link page N ›* chip that adds the next page's text in one click.
|
||||||
|
|
||||||
|
- **Photos and graphics that no box covers get a figure region automatically** (a dense unboxed blob of real size; thin, line-structured ink is text and is left alone). New figures join the article with the most text on their page.
|
||||||
|
- **Text that is not narratable page furniture stays out of linked articles:** running heads that recur on 3+ pages, lists of page numbers and reference/source lists are no longer pulled in when continuation pages are linked. An article made only of text you already approved becomes approved.
|
||||||
|
- The per-page fix and the whole-issue run keep `quality_report.json` in step, so the thumbnail badges and the list of pages needing a decision match the layout as it is now. Running heads and adverts are no longer audited for clipped edges or overlaps.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- **Reading order is now applied automatically after import, *OCR again* and every per-page fix**, not only by the manual *Fix reading order* button. Until now the order after import was whatever the vision/LLM step returned, so a column's short last box could be read after the whole other column; on the measured issue 101 of 204 pages had a saved order that differs from the column-aware order. Pages whose order you fixed by hand stay untouched, except after *OCR again*, when every region is new and a hand order can no longer apply.
|
- **Reading order is now applied automatically after import, *OCR again* and every per-page fix**, not only by the manual *Fix reading order* button. Until now the order after import was whatever the vision/LLM step returned, so a column's short last box could be read after the whole other column; on the measured issue 101 of 204 pages had a saved order that differs from the column-aware order. Pages whose order you fixed by hand stay untouched, except after *OCR again*, when every region is new and a hand order can no longer apply.
|
||||||
- **The review list shows each region's whole text**, not the first 220 characters, so every misspelling gets its red wavy underline (like a word processor). The engine's `[Non-Text]` marker and hairline boxes (under 3 pt tall) are now treated as OCR noise by *Clean up boxes* and after a page re-OCR, so they no longer show up as near-invisible regions. Fitted boxes keep a little air on grown sides.
|
- **The review list shows each region's whole text**, not the first 220 characters, so every misspelling gets its red wavy underline (like a word processor). The engine's `[Non-Text]` marker and hairline boxes (under 3 pt tall) are now treated as OCR noise by *Clean up boxes* and after a page re-OCR, so they no longer show up as near-invisible regions. Fitted boxes keep a little air on grown sides.
|
||||||
|
|||||||
@ -3144,6 +3144,40 @@ def detect_inset_pullquotes(doc_dir: Path, layout: dict[str, Any], pages: Iterab
|
|||||||
return layout, added
|
return layout, added
|
||||||
|
|
||||||
|
|
||||||
|
_PICTURE_DENSITY = 0.35 # inked share of an unboxed blob above which it is a picture, not text
|
||||||
|
_PICTURE_MIN_PT = 30.0
|
||||||
|
|
||||||
|
|
||||||
|
def add_uncovered_pictures(doc_dir: Path, layout: dict[str, Any], page_no: int) -> tuple[dict[str, Any], list[str]]:
|
||||||
|
"""Give a figure region to each photo or graphic that no box covers.
|
||||||
|
|
||||||
|
The page audit lists ink outside every box. A dense blob of a real size is a picture the
|
||||||
|
layout model missed (text is thin and line-structured, ink share well under a third), so it
|
||||||
|
becomes a figure -- cropped for playback, and no longer reported as missing text. Returns the
|
||||||
|
layout and the new image ids. Idempotent: a covered picture is not found again.
|
||||||
|
"""
|
||||||
|
audit = audit_page_coverage(doc_dir, layout, page_no)
|
||||||
|
if not audit.get("uncovered"):
|
||||||
|
return layout, []
|
||||||
|
meta = next((p for p in layout.get("pages", []) if p["page"] == page_no), None)
|
||||||
|
ink = page_ink_mask(doc_dir / "pages" / f"{page_no:04d}.png")
|
||||||
|
if meta is None or ink is None:
|
||||||
|
return layout, []
|
||||||
|
height, width = ink.shape
|
||||||
|
sx, sy = width / max(1.0, float(meta["width"])), height / max(1.0, float(meta["height"]))
|
||||||
|
added: list[str] = []
|
||||||
|
for x, y, w, h in audit["uncovered"]:
|
||||||
|
if w < _PICTURE_MIN_PT or h < _PICTURE_MIN_PT:
|
||||||
|
continue
|
||||||
|
region = ink[int(y * sy):int((y + h) * sy), int(x * sx):int((x + w) * sx)]
|
||||||
|
if region.size == 0 or float(region.mean()) < _PICTURE_DENSITY:
|
||||||
|
continue
|
||||||
|
before = {i["id"] for i in layout.get("images", [])}
|
||||||
|
layout = add_manual_region(doc_dir, layout, page_no, [x, y, w, h], "image")
|
||||||
|
added += [i["id"] for i in layout.get("images", []) if i["id"] not in before]
|
||||||
|
return layout, added
|
||||||
|
|
||||||
|
|
||||||
def split_around_pullquotes(doc_dir: Path, layout: dict[str, Any],
|
def split_around_pullquotes(doc_dir: Path, layout: dict[str, Any],
|
||||||
pages: Iterable[int] | None = None) -> tuple[dict[str, Any], list[dict[str, str]]]:
|
pages: Iterable[int] | None = None) -> tuple[dict[str, Any], list[dict[str, str]]]:
|
||||||
"""Cut a paragraph's box where its text wraps around an inset pull quote.
|
"""Cut a paragraph's box where its text wraps around an inset pull quote.
|
||||||
@ -3373,7 +3407,8 @@ def audit_page_coverage(doc_dir: Path, layout: dict[str, Any], page_no: int) ->
|
|||||||
suspects = [b["id"] for b in blocks
|
suspects = [b["id"] for b in blocks
|
||||||
if b["type"] in eligible
|
if b["type"] in eligible
|
||||||
and suspect_ocr_tokens(" ".join((b.get("text") or "").split()), page_language, page_vocabulary)]
|
and suspect_ocr_tokens(" ".join((b.get("text") or "").split()), page_language, page_vocabulary)]
|
||||||
text_blocks = [b for b in blocks if b["type"] not in VISUAL_TYPES]
|
# Running heads and adverts are never narrated: a box edge or an overlap there is not a problem.
|
||||||
|
text_blocks = [b for b in blocks if b["type"] not in VISUAL_TYPES and b["type"] not in (BLOCK_FURNITURE, BLOCK_AD)]
|
||||||
overlaps = [[a["id"], b["id"]] for i, a in enumerate(text_blocks) for b in text_blocks[i + 1:]
|
overlaps = [[a["id"], b["id"]] for i, a in enumerate(text_blocks) for b in text_blocks[i + 1:]
|
||||||
if max(_bbox_intersection_ratio(a["bbox"], b["bbox"]),
|
if max(_bbox_intersection_ratio(a["bbox"], b["bbox"]),
|
||||||
_bbox_intersection_ratio(b["bbox"], a["bbox"])) >= _AUDIT_OVERLAP_RATIO]
|
_bbox_intersection_ratio(b["bbox"], a["bbox"])) >= _AUDIT_OVERLAP_RATIO]
|
||||||
@ -4679,6 +4714,26 @@ def _is_placeholder_article(article: dict[str, Any]) -> bool:
|
|||||||
return bool(_PLACEHOLDER_TITLE.fullmatch((article.get("title") or "").strip()))
|
return bool(_PLACEHOLDER_TITLE.fullmatch((article.get("title") or "").strip()))
|
||||||
|
|
||||||
|
|
||||||
|
_REFERENCE_HEAD_RE = re.compile(r"^\s*(literatur|quellen|studien, weitere quellen|weiterführende|links?\b|anmerkungen?\b)", re.I)
|
||||||
|
|
||||||
|
|
||||||
|
def looks_like_page_junk(text: str, repeats: int = 1) -> bool:
|
||||||
|
"""Text that is on a page but is not something to narrate: a running head that recurs on many
|
||||||
|
pages, a list of page numbers, or a reference/source list. Never used to delete anything the
|
||||||
|
reviewer approved -- only to keep such text out of an article automatically."""
|
||||||
|
t = " ".join((text or "").split())
|
||||||
|
if not t:
|
||||||
|
return True
|
||||||
|
letters = sum(c.isalpha() for c in t)
|
||||||
|
if letters < 0.5 * len(t.replace(" ", "")) and len(t) < 200:
|
||||||
|
return True # "66 90,98 50,56 82, 114 ..."
|
||||||
|
if repeats >= 3 and len(t) < 80:
|
||||||
|
return True # the same short line on 3+ pages: a running head
|
||||||
|
if _REFERENCE_HEAD_RE.match(t) and len(t) < 400:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def auto_link_pages(layout: dict[str, Any], articles: list[dict[str, Any]],
|
def auto_link_pages(layout: dict[str, Any], articles: list[dict[str, Any]],
|
||||||
max_gap_pages: int = 2) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
max_gap_pages: int = 2) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
||||||
"""Attach text that no real article owns to the article running from the page before.
|
"""Attach text that no real article owns to the article running from the page before.
|
||||||
@ -4700,8 +4755,16 @@ def auto_link_pages(layout: dict[str, Any], articles: list[dict[str, Any]],
|
|||||||
approved_ids = {b for a in articles if a.get("status") == "approved" for b in a["blocks"]}
|
approved_ids = {b for a in articles if a.get("status") == "approved" for b in a["blocks"]}
|
||||||
blocks = {b["id"]: b for b in layout.get("blocks", [])}
|
blocks = {b["id"]: b for b in layout.get("blocks", [])}
|
||||||
by_page: dict[int, list[dict[str, Any]]] = {}
|
by_page: dict[int, list[dict[str, Any]]] = {}
|
||||||
|
seen: dict[str, int] = {}
|
||||||
for b in layout.get("blocks", []):
|
for b in layout.get("blocks", []):
|
||||||
if b["type"] in NARRATABLE and (b.get("text") or "").strip():
|
if b["type"] in NARRATABLE and (b.get("text") or "").strip():
|
||||||
|
key = re.sub(r"\d+", "#", " ".join(b["text"].split()).casefold())
|
||||||
|
seen[key] = seen.get(key, 0) + 1
|
||||||
|
for b in layout.get("blocks", []):
|
||||||
|
if b["type"] in NARRATABLE and (b.get("text") or "").strip():
|
||||||
|
key = re.sub(r"\d+", "#", " ".join(b["text"].split()).casefold())
|
||||||
|
if b["id"] not in owner and looks_like_page_junk(b["text"], seen[key]):
|
||||||
|
continue # not narratable page furniture: leave it out
|
||||||
by_page.setdefault(b["page"], []).append(b)
|
by_page.setdefault(b["page"], []).append(b)
|
||||||
report = {"linked": 0, "created": 0, "unlinked": [], "dropped": 0}
|
report = {"linked": 0, "created": 0, "unlinked": [], "dropped": 0}
|
||||||
taken = {int(m.group(1)) for a in articles if (m := re.fullmatch(r"article-(\d+)", a["id"]))}
|
taken = {int(m.group(1)) for a in articles if (m := re.fullmatch(r"article-(\d+)", a["id"]))}
|
||||||
@ -4783,6 +4846,12 @@ def auto_link_pages(layout: dict[str, Any], articles: list[dict[str, Any]],
|
|||||||
report["dropped"] += 1
|
report["dropped"] += 1
|
||||||
continue
|
continue
|
||||||
kept.append(a)
|
kept.append(a)
|
||||||
|
# An article whose every text region the reviewer had already approved (in whichever article
|
||||||
|
# it sat before) is as reviewed as it was: do not make it be approved a second time.
|
||||||
|
for a in kept:
|
||||||
|
narrated = [b for b in a["blocks"] if blocks.get(b, {}).get("type") in NARRATABLE]
|
||||||
|
if a.get("status") == "detected" and narrated and all(b in approved_ids for b in narrated):
|
||||||
|
a["status"] = "approved"
|
||||||
return kept, report
|
return kept, report
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1066,6 +1066,34 @@ def _vision_args(settings: dict[str, Any]) -> tuple[str | None, str | None, str
|
|||||||
pick("vision_ocr_fallback_url"), pick("vision_ocr_fallback_model"))
|
pick("vision_ocr_fallback_url"), pick("vision_ocr_fallback_model"))
|
||||||
|
|
||||||
|
|
||||||
|
def _refresh_quality_rows(d: Path, layout: dict[str, Any], pages: list[int]) -> None:
|
||||||
|
"""Re-measure `pages` and update their rows in quality_report.json, so the thumbnail badges and
|
||||||
|
the list of pages needing a decision match the layout as it is now (a per-page fix otherwise
|
||||||
|
left the last whole-issue run's numbers standing)."""
|
||||||
|
path = d / _QUALITY_REPORT
|
||||||
|
try:
|
||||||
|
report = json.loads(path.read_text("utf-8"))
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return
|
||||||
|
rows = {r["page"]: r for r in report.get("pages", [])}
|
||||||
|
for page in pages:
|
||||||
|
if mag.page_is_advertisement(layout, page):
|
||||||
|
continue
|
||||||
|
fresh = mag.audit_page_coverage(d, layout, page)
|
||||||
|
row = rows.get(page, {"page": page})
|
||||||
|
row.update({k: fresh[k] for k in ("uncovered", "uncovered_ratio", "suspect_blocks", "overlaps", "clipped", "measured")})
|
||||||
|
spell = row.get("spelling") or []
|
||||||
|
row["needs_review"] = bool(row["uncovered"] or row["overlaps"] or row["clipped"] or row.get("error")
|
||||||
|
or row.get("duplicates"))
|
||||||
|
row["words_to_check"] = bool(row["suspect_blocks"] or spell)
|
||||||
|
rows[page] = row
|
||||||
|
report["pages"] = [rows[p] for p in sorted(rows)]
|
||||||
|
summary = report.setdefault("summary", {})
|
||||||
|
summary["pages_needing_review"] = [r["page"] for r in report["pages"] if r.get("needs_review")]
|
||||||
|
summary["pages_with_words"] = [r["page"] for r in report["pages"] if r.get("words_to_check") and not r.get("needs_review")]
|
||||||
|
path.write_text(json.dumps(report, ensure_ascii=False), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def _fix_page_geometry(d: Path, layout: dict[str, Any], articles: list[dict[str, Any]], page: int,
|
def _fix_page_geometry(d: Path, layout: dict[str, Any], articles: list[dict[str, Any]], page: int,
|
||||||
settings: dict[str, Any]) -> tuple[dict[str, Any], dict[str, int], bool]:
|
settings: dict[str, Any]) -> tuple[dict[str, Any], dict[str, int], bool]:
|
||||||
"""The deterministic box fixes for one page, in the order that lets each rely on the last.
|
"""The deterministic box fixes for one page, in the order that lets each rely on the last.
|
||||||
@ -1087,8 +1115,17 @@ def _fix_page_geometry(d: Path, layout: dict[str, Any], articles: list[dict[str,
|
|||||||
layout = mag.reorder_page(layout, page)
|
layout = mag.reorder_page(layout, page)
|
||||||
if url and model and any(m.get("lines_added") for m in snapped):
|
if url and model and any(m.get("lines_added") for m in snapped):
|
||||||
layout, _ = mag.reread_grown_regions(d, layout, snapped, url, model, fb_url, fb_model)
|
layout, _ = mag.reread_grown_regions(d, layout, snapped, url, model, fb_url, fb_model)
|
||||||
|
layout, pictures = mag.add_uncovered_pictures(d, layout, page)
|
||||||
changed = _add_split_paragraphs_to_articles(articles, cleanup)
|
changed = _add_split_paragraphs_to_articles(articles, cleanup)
|
||||||
|
if pictures and articles:
|
||||||
|
# A new figure belongs to the article with the most text on its page.
|
||||||
|
on_page = {b["id"] for b in layout.get("blocks", []) if b["page"] == page}
|
||||||
|
owner = max(articles, key=lambda a: len(on_page & set(a.get("blocks") or [])), default=None)
|
||||||
|
if owner is not None and on_page & set(owner.get("blocks") or []):
|
||||||
|
owner["images"] = list(dict.fromkeys([*(owner.get("images") or []), *pictures]))
|
||||||
|
changed = True
|
||||||
counts = {"noise_retyped": noise, "pullquotes_found": len(quotes), "paragraphs_cut": len(wrap),
|
counts = {"noise_retyped": noise, "pullquotes_found": len(quotes), "paragraphs_cut": len(wrap),
|
||||||
|
"pictures_added": len(pictures),
|
||||||
"boxes_fitted": len(snapped), "bylines_split": len(cleanup.get("byline_split", []))}
|
"boxes_fitted": len(snapped), "bylines_split": len(cleanup.get("byline_split", []))}
|
||||||
return layout, counts, changed
|
return layout, counts, changed
|
||||||
|
|
||||||
@ -1259,6 +1296,7 @@ def _autofix_page_sync(doc_id: str, job_id: str, page: int, mode: str, settings:
|
|||||||
mag.save_layout(d, layout)
|
mag.save_layout(d, layout)
|
||||||
if moved:
|
if moved:
|
||||||
mag.save_articles(d, articles)
|
mag.save_articles(d, articles)
|
||||||
|
_refresh_quality_rows(d, layout, [page])
|
||||||
_seal_undo(d, token)
|
_seal_undo(d, token)
|
||||||
done["undo"] = token
|
done["undo"] = token
|
||||||
fixed = sum(v for k, v in done.items() if k != "undo" and isinstance(v, int))
|
fixed = sum(v for k, v in done.items() if k != "undo" and isinstance(v, int))
|
||||||
|
|||||||
@ -3002,6 +3002,15 @@ class AutoLinkPagesTests(unittest.TestCase):
|
|||||||
self.assertEqual(out[0]["status"], "approved") # the stand-ins were approved: still reviewed
|
self.assertEqual(out[0]["status"], "approved") # the stand-ins were approved: still reviewed
|
||||||
self.assertEqual((report["linked"], report["dropped"]), (2, 2))
|
self.assertEqual((report["linked"], report["dropped"]), (2, 2))
|
||||||
|
|
||||||
|
def test_a_detected_article_made_only_of_already_approved_text_becomes_approved(self):
|
||||||
|
blocks = [self.blk("t", 1, "title", "Der Butler", 0), self.blk("a", 1, "body", "Der Text beginnt und", 1),
|
||||||
|
self.blk("b", 2, "body", "geht weiter.", 0)]
|
||||||
|
arts = [self.art("article-001", "Der Butler", [1], ["t", "a"], "detected"),
|
||||||
|
self.art("article-002", "Page 1", [1], ["t", "a"], "approved"),
|
||||||
|
self.art("article-003", "Page 2", [2], ["b"], "approved")]
|
||||||
|
out = self.link(blocks, arts)[0]
|
||||||
|
self.assertEqual([(a["id"], a["status"]) for a in out], [("article-001", "approved")])
|
||||||
|
|
||||||
def test_text_nobody_approved_sends_the_article_back_to_review(self):
|
def test_text_nobody_approved_sends_the_article_back_to_review(self):
|
||||||
blocks = [self.blk("t", 1, "title", "Der Butler", 0), self.blk("a", 1, "body", "Der Text beginnt und", 1),
|
blocks = [self.blk("t", 1, "title", "Der Butler", 0), self.blk("a", 1, "body", "Der Text beginnt und", 1),
|
||||||
self.blk("b", 2, "body", "geht weiter.", 0)]
|
self.blk("b", 2, "body", "geht weiter.", 0)]
|
||||||
@ -3078,3 +3087,62 @@ class ArticleEndedTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_a_headline_two_pages_on_does_not_end_it(self):
|
def test_a_headline_two_pages_on_does_not_end_it(self):
|
||||||
self.assertFalse(self.ended([("a", 1, "body", "Ohne Kürzel am Ende"), ("h", 3, "title", "Viel später")], ["a"]))
|
self.assertFalse(self.ended([("a", 1, "body", "Ohne Kürzel am Ende"), ("h", 3, "title", "Viel später")], ["a"]))
|
||||||
|
|
||||||
|
|
||||||
|
class PageJunkTests(unittest.TestCase):
|
||||||
|
def test_page_lists_running_heads_and_reference_lists_are_junk(self):
|
||||||
|
from core.magazine import looks_like_page_junk
|
||||||
|
self.assertTrue(looks_like_page_junk("66 90,98 50,56 82, 114, 120 148, 168 172, 194"))
|
||||||
|
self.assertTrue(looks_like_page_junk("46 Arbeitswelt. Wirtschaft", repeats=5))
|
||||||
|
self.assertTrue(looks_like_page_junk("Literatur [1] Trinkwalder, Wunderbare Einhörner"))
|
||||||
|
self.assertTrue(looks_like_page_junk("Studien, weitere Quellen und Tools:"))
|
||||||
|
self.assertFalse(looks_like_page_junk("Der Konzern braucht eine, die iPhone, iPad und Mac unentbehrlich macht."))
|
||||||
|
self.assertFalse(looks_like_page_junk("Ein kurzer Satz.", repeats=1))
|
||||||
|
|
||||||
|
def test_auto_link_leaves_such_text_out_of_the_article(self):
|
||||||
|
from core.magazine import auto_link_pages
|
||||||
|
blk = lambda i, pg, ty, t, n: {"id": i, "page": pg, "type": ty, "text": t, "reading_order": n, "bbox": [0, 0, 1, 1]}
|
||||||
|
layout = {"pages": [], "images": [], "blocks": [
|
||||||
|
blk("t", 1, "title", "Der Butler", 0), blk("a", 1, "body", "Der Text beginnt und", 1),
|
||||||
|
blk("b", 2, "body", "geht auf Seite zwei weiter.", 0), blk("j", 2, "body", "66 90,98 50,56 82, 114", 1)]}
|
||||||
|
art = lambda i, ti, pg, bl, st: {"id": i, "title": ti, "author": "", "pages": pg, "blocks": bl, "images": [], "status": st,
|
||||||
|
"confidence": 0.5, "section": "", "continued_to": [], "audio_status": "none"}
|
||||||
|
out, _ = auto_link_pages(layout, [art("article-001", "Der Butler", [1], ["t", "a"], "approved"), art("article-002", "Page 2", [2], ["b"], "approved")])
|
||||||
|
self.assertEqual(out[0]["blocks"], ["t", "a", "b"])
|
||||||
|
|
||||||
|
|
||||||
|
class UncoveredPictureTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
from pathlib import Path
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
self.dir = Path(tempfile.mkdtemp(prefix="ttvc-pic-"))
|
||||||
|
(self.dir / "pages").mkdir()
|
||||||
|
im = Image.new("RGB", (600, 800), (248, 242, 244))
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
draw.rectangle([100, 200, 300, 350], fill=(60, 90, 140)) # a photo: dense ink, 200x150
|
||||||
|
for y in range(500, 560, 12): # text lines: thin, sparse
|
||||||
|
draw.rectangle([100, y, 300, y + 3], fill=(93, 91, 92))
|
||||||
|
im.save(self.dir / "pages" / "0001.png")
|
||||||
|
self.layout = {"pages": [{"page": 1, "width": 600, "height": 800}], "images": [], "blocks": []}
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
shutil.rmtree(self.dir, ignore_errors=True)
|
||||||
|
|
||||||
|
def test_a_dense_unboxed_blob_becomes_a_figure_and_text_lines_do_not(self):
|
||||||
|
from core.magazine import add_uncovered_pictures
|
||||||
|
out, added = add_uncovered_pictures(self.dir, self.layout, 1)
|
||||||
|
self.assertEqual(len(added), 1)
|
||||||
|
x, y, w, h = out["images"][0]["bbox"]
|
||||||
|
self.assertTrue(90 <= x <= 105 and 190 <= y <= 205 and w >= 195 and h >= 145, out["images"][0]["bbox"])
|
||||||
|
self.assertTrue((self.dir / "images" / out["images"][0]["filename"]).is_file())
|
||||||
|
|
||||||
|
def test_running_it_again_finds_nothing_new(self):
|
||||||
|
from core.magazine import add_uncovered_pictures
|
||||||
|
once, _ = add_uncovered_pictures(self.dir, self.layout, 1)
|
||||||
|
self.assertEqual(add_uncovered_pictures(self.dir, once, 1)[1], [])
|
||||||
|
|
||||||
|
def test_running_heads_and_adverts_are_not_audited_for_clipped_edges(self):
|
||||||
|
from core.magazine import audit_page_coverage
|
||||||
|
layout = dict(self.layout, blocks=[{"id": "f", "page": 1, "type": "furniture", "text": "45", "bbox": [100, 500, 100, 10]},
|
||||||
|
{"id": "a", "page": 1, "type": "ad", "text": "Kaufen", "bbox": [100, 520, 100, 10]}])
|
||||||
|
self.assertEqual(audit_page_coverage(self.dir, layout, 1)["clipped"], [])
|
||||||
|
|||||||
@ -1462,6 +1462,7 @@ class QualityRunJobTests(unittest.TestCase):
|
|||||||
patch('routes.magazine.mag.recover_missing_text', return_value=(layout, 1)), \
|
patch('routes.magazine.mag.recover_missing_text', return_value=(layout, 1)), \
|
||||||
patch('routes.magazine.mag.verify_ocr_recognition', return_value=(layout, 3, 2)) as verify, \
|
patch('routes.magazine.mag.verify_ocr_recognition', return_value=(layout, 3, 2)) as verify, \
|
||||||
patch('routes.magazine.mag.audit_page_coverage', side_effect=audit_row), \
|
patch('routes.magazine.mag.audit_page_coverage', side_effect=audit_row), \
|
||||||
|
patch('routes.magazine.mag.add_uncovered_pictures', side_effect=lambda d, layout, page: (layout, [])), \
|
||||||
patch('routes.magazine.mag.save_layout'), \
|
patch('routes.magazine.mag.save_layout'), \
|
||||||
patch('routes.magazine._LIB', lib), \
|
patch('routes.magazine._LIB', lib), \
|
||||||
patch('routes.magazine.background_job_update') as update:
|
patch('routes.magazine.background_job_update') as update:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user