From 7a687c8324cd0f824bf9e2cf91743d194f328f08 Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 01:50:27 +0530 Subject: [PATCH 01/33] fix(cleanup): strip page numbers and headless separators, rejoin sentences split by a page break --- wikify/engine/loader/cleanup.py | 46 +++++++++++++++++++++++++++++++-- wikify/tests/test_sectionize.py | 32 +++++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/wikify/engine/loader/cleanup.py b/wikify/engine/loader/cleanup.py index 6ff8236..2c498a3 100644 --- a/wikify/engine/loader/cleanup.py +++ b/wikify/engine/loader/cleanup.py @@ -31,6 +31,8 @@ # carrying >=2 distinct sign-off phrases (a lone "approved by" in a data row is kept). _SEP_ONLY = re.compile(r"^\s*\|[\s:|-]+\|\s*$") _SIGNOFF = ("prepared by", "issued by", "approved by", "reviewed by", "authorized by") +_PAGE_NUMBER = re.compile(r"^\s*\d{1,4}\s*$") +_SENTENCE_START = re.compile(r"^[a-z]") def _norm(line: str) -> str: @@ -60,6 +62,25 @@ def _strip_footer_blocks(md: str) -> str: return "\n".join(line for k, line in enumerate(lines) if k not in drop) +def _is_table_row(line: str) -> bool: + return line.strip().startswith("|") + + +def _strip_page_residue(md: str) -> str: + """Drop separator rows left headless by a stripped header box, and the closing page number.""" + lines = md.splitlines() + kept = [ + line + for index, line in enumerate(lines) + if not (_SEP_ONLY.match(line) and "-" in line and not (index and _is_table_row(lines[index - 1]))) + ] + while kept and not kept[-1].strip(): + kept.pop() + if kept and _PAGE_NUMBER.match(kept[-1]): + kept.pop() + return "\n".join(kept) + + def find_boilerplate(pages: list[tuple[int, str]]) -> set[str]: """Normalized lines that recur on a large fraction of pages.""" counts: Counter[str] = Counter() @@ -82,12 +103,33 @@ def strip_boilerplate(pages: list[tuple[int, str]], boilerplate: set[str]) -> li for line in md.splitlines() if not (_norm(line) and _norm(line) in boilerplate) and not _is_varying(line) ] - out.append((pno, _strip_footer_blocks("\n".join(kept)))) + out.append((pno, _strip_page_residue(_strip_footer_blocks("\n".join(kept))))) return out +def _ends_mid_sentence(line: str) -> bool: + text = line.strip() + return bool(text) and text[0] not in "#|<" and (text[-1].isalnum() or text[-1] == ",") + + +def join_page_breaks(pages: list[tuple[int, str]]) -> list[tuple[int, str]]: + """Rejoin a sentence the PDF split across a page break onto the page it started on.""" + page_lines = [md.splitlines() for _, md in pages] + for index in range(len(pages) - 1): + if pages[index + 1][0] != pages[index][0] + 1: + continue + lines, next_lines = page_lines[index], page_lines[index + 1] + last = next((i for i in range(len(lines) - 1, -1, -1) if lines[i].strip()), None) + first = next((i for i, line in enumerate(next_lines) if line.strip()), None) + if last is None or first is None: + continue + if _ends_mid_sentence(lines[last]) and _SENTENCE_START.match(next_lines[first].strip()): + lines[last] = f"{lines[last].rstrip()} {next_lines.pop(first).strip()}" + return [(page_no, "\n".join(lines)) for (page_no, _), lines in zip(pages, page_lines, strict=True)] + + def clean_pages(pages: list[tuple[int, str]]) -> list[tuple[int, str]]: - return strip_boilerplate(pages, find_boilerplate(pages)) + return join_page_breaks(strip_boilerplate(pages, find_boilerplate(pages))) def strip_outer_markdown_fence(text: str) -> str: diff --git a/wikify/tests/test_sectionize.py b/wikify/tests/test_sectionize.py index b9a248d..7f8ab90 100644 --- a/wikify/tests/test_sectionize.py +++ b/wikify/tests/test_sectionize.py @@ -161,6 +161,38 @@ def test_clean_pages_strips_signoff_footer_block(self): self.assertIn("real body", cleaned[1]) self.assertIn("more body", cleaned[2]) + def test_clean_pages_strips_page_numbers_and_headless_separator_rows(self): + header = "NEPHROLOGY MANUAL\n\n| MAN/RPT/NEPH/001 | Ver.: 1 | Pg. {page} of 398 |\n|---|---|---|" + table = "| Health worker | Action |\n|---|---|\n| Antibody 100 | Reassure |" + pages = [ + (1, header.format(page=121) + f"\n\n## 5.7 Billing\n\n- Pay 2500 by noon.\n\n{table}\n\n121"), + (2, header.format(page=122) + "\n\n- Second page item.\n\n122"), + (3, header.format(page=123) + "\n\n- Third page item.\n\n123"), + ] + cleaned = dict(clean_pages(pages)) + self.assertFalse(cleaned[1].startswith("|")) + self.assertEqual(cleaned[2].strip(), "- Second page item.") + self.assertNotIn("121", cleaned[1]) + self.assertIn(f"- Pay 2500 by noon.\n\n{table}", cleaned[1]) + + def test_clean_pages_rejoins_a_sentence_split_by_a_page_break(self): + pages = [ + (1, "- Organs are retrieved after withdrawal of life-sustaining\n\n132"), + ( + 2, + "measures; the kidneys may be recovered.\n\n## 5.8.12 Grievances\n\n- Last item ends.\n\n133", + ), + (3, "- New item starts the page.\n\n134"), + ] + cleaned = dict(clean_pages(pages)) + self.assertEqual( + cleaned[1], + "- Organs are retrieved after withdrawal of life-sustaining measures; the kidneys may be recovered.", + ) + self.assertNotIn("measures", cleaned[2]) + self.assertIn("## 5.8.12 Grievances", cleaned[2]) + self.assertEqual(cleaned[3], "- New item starts the page.") + def test_title_is_clipped_to_the_storable_length(self): long_title = "DETERMINATION OF RESIDENTIAL STATUS OF " + "HINDU UNDIVIDED FAMILY " * 8 secs = sectionize([(1, f"## {long_title}\nbody")]) From 3c95fad4d2473b9b879b67dfa6c161ff0a650fa1 Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 01:59:10 +0530 Subject: [PATCH 02/33] fix(cleanup): rejoin a page break only after a full-width line --- wikify/engine/loader/cleanup.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/wikify/engine/loader/cleanup.py b/wikify/engine/loader/cleanup.py index 2c498a3..8f890a0 100644 --- a/wikify/engine/loader/cleanup.py +++ b/wikify/engine/loader/cleanup.py @@ -33,6 +33,8 @@ _SIGNOFF = ("prepared by", "issued by", "approved by", "reviewed by", "authorized by") _PAGE_NUMBER = re.compile(r"^\s*\d{1,4}\s*$") _SENTENCE_START = re.compile(r"^[a-z]") +# A sentence cut by a page break ran to the page edge, so its last line is never a short label. +_MIN_BROKEN_LINE_LENGTH = 40 def _norm(line: str) -> str: @@ -109,7 +111,11 @@ def strip_boilerplate(pages: list[tuple[int, str]], boilerplate: set[str]) -> li def _ends_mid_sentence(line: str) -> bool: text = line.strip() - return bool(text) and text[0] not in "#|<" and (text[-1].isalnum() or text[-1] == ",") + return ( + len(text) >= _MIN_BROKEN_LINE_LENGTH + and text[0] not in "#|<" + and (text[-1].isalnum() or text[-1] == ",") + ) def join_page_breaks(pages: list[tuple[int, str]]) -> list[tuple[int, str]]: From e3cfe6edeee085af50f177e50e8561335f4245da Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 01:59:11 +0530 Subject: [PATCH 03/33] fix(sectionize): keep leading text, fold running sub-header variants and nest numbered list headings --- wikify/engine/loader/cleanup.py | 6 +- wikify/engine/loader/sectionizer.py | 92 ++++++++++++++++++++++------- wikify/tests/test_sectionize.py | 86 +++++++++++++++++++++++++++ 3 files changed, 163 insertions(+), 21 deletions(-) diff --git a/wikify/engine/loader/cleanup.py b/wikify/engine/loader/cleanup.py index 8f890a0..f924fc6 100644 --- a/wikify/engine/loader/cleanup.py +++ b/wikify/engine/loader/cleanup.py @@ -35,6 +35,9 @@ _SENTENCE_START = re.compile(r"^[a-z]") # A sentence cut by a page break ran to the page edge, so its last line is never a short label. _MIN_BROKEN_LINE_LENGTH = 40 +# A numbered heading repeated as a running sub-header is still the real section start on its first +# page; the sectionizer folds the repeats, so stripping them here would lose the section itself. +_NUMBERED_HEADING = re.compile(r"^\s*#{1,6}\s+[*_]*\d+(?:\.\d+)*\.?\s*[A-Za-z]") def _norm(line: str) -> str: @@ -103,7 +106,8 @@ def strip_boilerplate(pages: list[tuple[int, str]], boilerplate: set[str]) -> li kept = [ line for line in md.splitlines() - if not (_norm(line) and _norm(line) in boilerplate) and not _is_varying(line) + if not (_norm(line) and _norm(line) in boilerplate and not _NUMBERED_HEADING.match(line)) + and not _is_varying(line) ] out.append((pno, _strip_page_residue(_strip_footer_blocks("\n".join(kept))))) return out diff --git a/wikify/engine/loader/sectionizer.py b/wikify/engine/loader/sectionizer.py index fe80407..5fa79c4 100644 --- a/wikify/engine/loader/sectionizer.py +++ b/wikify/engine/loader/sectionizer.py @@ -15,10 +15,14 @@ _NUM_RE = re.compile(r"^(\d+(?:\.\d+)*)\.?\s+\S") _LEADING_NUM = re.compile(r"^(\d+)\b") _DOUBLE_NUM = re.compile(r"^\d+\.\s+\d") +_GLUED_NUM = re.compile(r"^(\d+(?:\.\d+)+\.?)(?=[A-Z])") +_BOLD_LINE = re.compile(r"^\*\*([^*]+)\*\*$") def _clean_title(raw: str) -> str: - title = raw.strip().strip("*_").strip() + title = _GLUED_NUM.sub(r"\1 ", raw.strip().strip("*_").strip()) + if title.endswith(".") and not title.endswith(".."): + title = title[:-1].rstrip() if len(title) <= MAX_TITLE_LENGTH: return title clipped = title[: MAX_TITLE_LENGTH - 1] @@ -42,6 +46,52 @@ def _looks_like_list_item(title: str) -> bool: return "*" in title or bool(_DOUBLE_NUM.match(title)) +def _section_number(title: str) -> tuple[int, ...] | None: + m = _NUM_RE.match(title) + return tuple(int(part) for part in m.group(1).split(".")) if m else None + + +def _title_words(title: str) -> list[str]: + m = _NUM_RE.match(title) + text = title[m.end(1) :] if m else title + return [word.removesuffix("s") for word in re.findall(r"[a-z0-9]+", text.lower())] + + +def _repeats_open_section(title: str, stack: list[tuple[int, str, bool]]) -> bool: + number = _section_number(title) + if number is None: + return False + words = _title_words(title) + return any( + numbered + and _section_number(open_title)[: len(number)] == number + and _title_words(open_title) == words + for _, open_title, numbered in stack + ) + + +def _continues_numbering(number: tuple[int, ...], last_number: tuple[int, ...]) -> bool: + if number == (*last_number, 1): + return True + depth = len(number) + return ( + depth <= len(last_number) + and number[:-1] == last_number[: depth - 1] + and number[-1] == last_number[depth - 1] + 1 + ) + + +def _promote_numbered_bold_line(line: str, last_number: tuple[int, ...]) -> str: + bold = _BOLD_LINE.match(line.strip()) + if not (bold and last_number): + return line + title = bold.group(1).strip() + number = _section_number(title) + if number is None or not _continues_numbering(number, last_number): + return line + return f"{'#' * min(6, len(number))} {title}" + + def running_header_titles(pages: list[tuple[int, str]]) -> set[str]: occurrences: dict[str, list[int]] = defaultdict(list) pages_seen: dict[str, set[int]] = defaultdict(set) @@ -81,6 +131,7 @@ def sectionize(pages: list[tuple[int, str]], level_map: dict[str, int] | None = max_chapter = 0 capital_chapters = True last_list_item = (0, 0) + last_number: tuple[int, ...] = () running_headers = running_header_titles(pages) opened_headers: set[str] = set() @@ -91,9 +142,13 @@ def flush(): for page_no, md in pages: for line in md.splitlines(): - m = _HEADING_RE.match(line) + m = _HEADING_RE.match(_promote_numbered_bold_line(line, last_number)) if m: title = _clean_title(m.group(2)) + if _repeats_open_section(title, stack): + if current is not None: + current.page_end = page_no + continue if title in running_headers: if title in opened_headers: if current is not None: @@ -110,6 +165,7 @@ def flush(): if ( _looks_like_list_item(title) or cnum <= max_chapter + or (last_number and cnum <= last_number[0]) or (max_chapter and capital_chapters and not title.isupper()) or (page_no, cnum - 1) == last_list_item ): @@ -123,9 +179,12 @@ def flush(): anchor = next((lvl for lvl, _, numbered in reversed(stack) if numbered), None) if anchor is not None: level = min(6, max(level, anchor + 1)) - while stack and stack[-1][0] >= level: + numbered_heading = not demoted and bool(_NUM_RE.match(title)) + while stack and (stack[-1][0] >= level or (numbered_heading and not stack[-1][2])): stack.pop() - stack.append((level, title, not demoted and bool(_NUM_RE.match(title)))) + stack.append((level, title, numbered_heading)) + if numbered_heading: + last_number = _section_number(title) current = Section( title=title, level=level, @@ -134,6 +193,14 @@ def flush(): page_end=page_no, ) else: + if current is None and line.strip(): + current = Section( + title="Preamble", + level=1, + hierarchy_path=["Preamble"], + page_start=page_no, + page_end=page_no, + ) if current is not None: current.page_end = page_no buf.append(line) @@ -157,19 +224,4 @@ def flush(): merged.append(sec) if is_numbered: numbered[key] = sec - sections = merged - - if not sections and pages: - whole = "\n".join(md for _, md in pages).strip() - if whole: - sections.append( - Section( - title="Preamble", - level=1, - hierarchy_path=["Preamble"], - page_start=pages[0][0], - page_end=pages[-1][0], - markdown=whole, - ) - ) - return sections + return merged diff --git a/wikify/tests/test_sectionize.py b/wikify/tests/test_sectionize.py index 7f8ab90..622adb5 100644 --- a/wikify/tests/test_sectionize.py +++ b/wikify/tests/test_sectionize.py @@ -132,6 +132,92 @@ def test_content_before_any_heading_becomes_preamble(self): self.assertEqual(secs[0].title, "Preamble") self.assertEqual(secs[0].level, 1) + def test_content_before_the_first_heading_is_kept(self): + pages = [ + ( + 1, + "- ✓ Concessions of Rs 2,500 are discussed.\n\n| a | b |\n|---|---|\n| 1 | 2 |\n\n## 5.7 Billing\nbill body", + ), + ] + secs = sectionize(pages) + self.assertEqual((secs[0].title, secs[0].hierarchy_path), ("Preamble", ["Preamble"])) + self.assertIn("Concessions of Rs 2,500", secs[0].markdown) + self.assertIn("| 1 | 2 |", secs[0].markdown) + self.assertEqual(secs[1].hierarchy_path, ["5.7 Billing"]) + + def test_running_sub_header_variants_fold_into_the_open_section(self): + running = "## 5.8 Policies on patient care" + pages = [ + (1, f"{running}\n### 5.8.1 Chaperone\na\n{running}\n### 5.8.2 Antibiotics\nb"), + (2, f"{running}\n### 5.8.3 Pain\nc"), + (3, f"{running}\n### 5.8.4 Sedation\nd\n## 5.8Policies on patient care\n### 5.8.5 Restraints\ne"), + ] + secs = sectionize(clean_pages(pages)) + self.assertEqual([s.title for s in secs].count("5.8 Policies on patient care"), 1) + self.assertEqual(secs[0].title, "5.8 Policies on patient care") + self.assertTrue(all(s.hierarchy_path[0] == "5.8 Policies on patient care" for s in secs)) + + def test_repeated_chapter_title_with_another_number_or_plural_is_not_a_new_section(self): + protocols = [ + (1, "# 6.1 PROTOCOLS FOR MANAGEMENT\n## 6.1.1 IgA nephropathy\na"), + (2, "# 6. PROTOCOLS FOR MANAGEMENT\n## 6.1.2 Lupus nephritis\n#### Hydroxychloroquine.\nb"), + ] + paths = {s.title: s.hierarchy_path for s in sectionize(protocols)} + self.assertNotIn("6. PROTOCOLS FOR MANAGEMENT", paths) + self.assertEqual( + paths["6.1.2 Lupus nephritis"], ["6.1 PROTOCOLS FOR MANAGEMENT", "6.1.2 Lupus nephritis"] + ) + self.assertIn("Hydroxychloroquine", paths) + + jobs = [ + (1, "# 3. JOB DESCRIPTIONS\n## 3.1 Head\na"), + (2, "## 3. JOB DESCRIPTION\n## 3.2 Unit head\nb"), + ] + paths = {s.title: s.hierarchy_path for s in sectionize(jobs)} + self.assertNotIn("3. JOB DESCRIPTION", paths) + self.assertEqual(paths["3.2 Unit head"], ["3. JOB DESCRIPTIONS", "3.2 Unit head"]) + + def test_bold_line_continuing_the_numbering_is_a_heading(self): + pages = [ + ( + 1, + "## 5.8.6 Blood Donation\na\n### 5.8.6.2 Donor criteria\nb\n" + "**5.8.6.3 Requesting blood from blood bank**\n**For use in ward**\nc\n" + "**5.8.6.4 For use in OR**\nd\n**1. Not a heading**\ne", + ) + ] + secs = sectionize(pages) + self.assertEqual( + [(s.title, s.level) for s in secs], + [ + ("5.8.6 Blood Donation", 3), + ("5.8.6.2 Donor criteria", 4), + ("5.8.6.3 Requesting blood from blood bank", 4), + ("5.8.6.4 For use in OR", 4), + ], + ) + self.assertNotIn("Requesting blood", secs[1].markdown) + self.assertIn("**For use in ward**", secs[2].markdown) + self.assertIn("**1. Not a heading**", secs[3].markdown) + + def test_unnumbered_heading_never_parents_a_numbered_heading(self): + pages = [(1, "# Contraindications\na\n# 5.8.4.2 Patient Assessment\nb\n### 5.8.5 Restraints\nc")] + paths = {s.title: s.hierarchy_path for s in sectionize(pages)} + self.assertEqual(paths["5.8.4.2 Patient Assessment"], ["5.8.4.2 Patient Assessment"]) + self.assertEqual(paths["5.8.5 Restraints"], ["5.8.5 Restraints"]) + + def test_numbered_list_headings_nest_under_a_deep_numbered_section(self): + pages = [ + (1, "#### 5.9.2.2 Staff tested for HIV\na\n# 1. Drug Regimen\nb\n# 2. Testing for HBsAg\nc"), + (2, "# 6.1 PROTOCOLS\n## 6.1.1 IgA nephropathy\nd"), + ] + paths = {s.title: s.hierarchy_path for s in sectionize(pages)} + self.assertEqual(paths["1. Drug Regimen"], ["5.9.2.2 Staff tested for HIV", "1. Drug Regimen"]) + self.assertEqual( + paths["2. Testing for HBsAg"], ["5.9.2.2 Staff tested for HIV", "2. Testing for HBsAg"] + ) + self.assertEqual(paths["6.1.1 IgA nephropathy"], ["6.1 PROTOCOLS", "6.1.1 IgA nephropathy"]) + def test_emphasis_is_stripped_from_heading_titles(self): pages = [ (1, "## _**Verbal Orders**_\nbody"), From 80cf4d35121328e8dac8bcad5879d4d7d5f84bea Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 02:04:30 +0530 Subject: [PATCH 04/33] fix(generate): show a group's own text as its first page --- wikify/engine/generate.py | 60 ++++++++++++++++++++++++++++++++--- wikify/tests/test_generate.py | 37 +++++++++++++++++++-- 2 files changed, 90 insertions(+), 7 deletions(-) diff --git a/wikify/engine/generate.py b/wikify/engine/generate.py index fb5126d..4d999b7 100644 --- a/wikify/engine/generate.py +++ b/wikify/engine/generate.py @@ -32,6 +32,7 @@ from wikify.engine.refs import smallest_covering DATA_MAX_LENGTH = 140 +OVERVIEW_TITLE = "Overview" def _upsert_wiki_document( @@ -92,6 +93,14 @@ def bounded_route(prefix: str, title: str, identifier: str) -> tuple[str, str]: return f"{prefix}/{slug}", slug +def overview_route(prefix: str, section_name: str) -> tuple[str, str]: + return bounded_route(prefix, OVERVIEW_TITLE, section_name) + + +def has_own_body(section: dict) -> bool: + return bool(section["is_group"] and (section["markdown"] or "").strip()) + + class _WikiGenerator: """Projects one approved Source Document tree into a Wiki Space (see module doc).""" @@ -113,6 +122,8 @@ def __init__( self.included = [s for s in self.sections if s["include_in_wiki"]] self.wiki_name: dict[str, str] = {} # section name → wiki document name self.wiki_route: dict[str, str] = {} # section name → wiki route + self.page_name: dict[str, str] = {} # section name → wiki document showing its content + self.page_route: dict[str, str] = {} self.content: dict[str, str] = {} # section name → content written self.deleted = 0 self.links = 0 @@ -153,6 +164,17 @@ def _sweep_stale(self) -> None: """ kept = {self.root_group.name} kept |= {s["wiki_document"] for s in self.included if s["wiki_document"]} + overview_routes = [ + overview_route(self.root_group.route, s["name"])[0] for s in self.included if has_own_body(s) + ] + if overview_routes: + kept |= set( + frappe.get_all( + "Wiki Document", + filters={"route": ["in", overview_routes], "is_group": 0}, + pluck="name", + ) + ) descendants = get_descendants_of("Wiki Document", self.root_group.name, ignore_permissions=True) stale = ( frappe.get_all( @@ -200,7 +222,7 @@ def _build_structure(self) -> None: doc = _upsert_wiki_document( s["wiki_document"], title=s["title"], - content=content, + content="" if has_own_body(s) else content, is_group=bool(s["is_group"]), parent=parent_name, route=route, @@ -209,11 +231,34 @@ def _build_structure(self) -> None: ) self.wiki_name[s["name"]] = doc.name self.wiki_route[s["name"]] = doc.route + self.page_name[s["name"]] = doc.name + self.page_route[s["name"]] = doc.route self.content[s["name"]] = content + if has_own_body(s): + self._add_overview(s, doc) + sort_counter[doc.name] = 1 store.set_section_wiki_document(s["name"], doc.name) if self.progress_cb: self.progress_cb(i + 1, total) + def _add_overview(self, section: dict, group) -> None: + """The live wiki redirects a group to its first page, so a group's own text would + never be shown; it goes on an Overview page sorted first instead.""" + route, slug = overview_route(self.root_group.route, section["name"]) + existing = frappe.db.get_value("Wiki Document", {"route": route, "is_group": 0}, "name") + doc = _upsert_wiki_document( + existing, + title=OVERVIEW_TITLE, + content=self.content[section["name"]], + is_group=False, + parent=group.name, + route=route, + slug=slug, + sort_order=0, + ) + self.page_name[section["name"]] = doc.name + self.page_route[section["name"]] = doc.route + def _rollup_empty_groups(self) -> None: """Give container pages (groups with no own body) a Contents list linking to their direct children, so a section landing page renders links instead of a bare heading. @@ -249,11 +294,11 @@ def _rewrite_links(self) -> None: self.content[s["name"]], page_count, self._route_for_page, - current_route=self.wiki_route[s["name"]], + current_route=self.page_route[s["name"]], ) if links: frappe.db.set_value( - "Wiki Document", self.wiki_name[s["name"]], "content", new_md, update_modified=False + "Wiki Document", self.page_name[s["name"]], "content", new_md, update_modified=False ) self.links += links @@ -340,11 +385,16 @@ def route_for_page(n: int) -> str | None: best = smallest_covering(routed, n) return routes[best["name"]] if best else None - own_route = routes.get(section_name) + target, own_route = sec.wiki_document, routes.get(section_name) + if has_own_body(sec): + own_route = overview_route(own_route.rpartition("/")[0], section_name)[0] + target = frappe.db.get_value("Wiki Document", {"route": own_route, "is_group": 0}, "name") + if not target: + return {"synced": False, "reason": "needs_regenerate"} page_count = frappe.db.get_value("Source Document", sec.source_document, "page_count") content, links = rewrite_page_refs(content, page_count or 10**9, route_for_page, current_route=own_route) - frappe.db.set_value("Wiki Document", sec.wiki_document, "content", content, update_modified=False) + frappe.db.set_value("Wiki Document", target, "content", content, update_modified=False) return {"synced": True, "chars": len(content), "links": links, "route": own_route} diff --git a/wikify/tests/test_generate.py b/wikify/tests/test_generate.py index b77264a..4ea088b 100644 --- a/wikify/tests/test_generate.py +++ b/wikify/tests/test_generate.py @@ -6,10 +6,11 @@ import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils.nestedset import get_descendants_of +from wiki.frappe_wiki.doctype.wiki_document.wiki_document import get_landing_page_for_route from wikify.api import imports as imports_api from wikify.engine import generate_wiki, preview_wiki, store -from wikify.engine.generate import DATA_MAX_LENGTH +from wikify.engine.generate import DATA_MAX_LENGTH, sync_section from wikify.engine.loader.sectionizer import Section from wikify.engine.loader.wiki import rewrite_page_refs, slugify from wikify.jobs import generate as generate_job @@ -205,6 +206,38 @@ def test_regenerate_after_rename_updates_route(self): self.assertIn("1.1 Goals", docs) self.assertTrue(docs["1.1 Goals"].route.endswith(f"{slugify('1.1 Goals')}-{purpose}")) + def test_group_body_shows_as_its_first_page(self): + res = self._generate() + docs = self._by_title(res["root_group"]) + intro, overview = docs["1. Intro"], docs["Overview"] + self.assertEqual(intro.content, "") + self.assertEqual(overview.is_group, 0) + self.assertEqual(overview.parent_wiki_document, intro.name) + self.assertEqual(overview.content, "overview") + self.assertLess(overview.sort_order, docs["1.1 Purpose"].sort_order) + self.assertEqual(get_landing_page_for_route(intro.route)["route"], overview.route) + + intro_section = frappe.db.get_value( + "Source Section", {"title": "1. Intro", "source_document": self.sd.name}, "name" + ) + frappe.db.set_value("Source Section", intro_section, "markdown", "edited overview") + self.assertTrue(sync_section(intro_section)["synced"]) + self.assertEqual(frappe.db.get_value("Wiki Document", overview.name, "content"), "edited overview") + + def test_regenerate_keeps_the_overview_and_drops_it_once_the_group_body_is_gone(self): + res = self._generate() + overview = self._by_title(res["root_group"])["Overview"].name + generate_wiki(self.sd.name, wiki_space=res["space"]) + self.assertEqual(self._by_title(res["root_group"])["Overview"].name, overview) + + frappe.db.set_value( + "Source Section", {"title": "1. Intro", "source_document": self.sd.name}, "markdown", "" + ) + generate_wiki(self.sd.name, wiki_space=res["space"]) + docs = self._by_title(res["root_group"]) + self.assertNotIn("Overview", docs) + self.assertIn("## Contents", docs["1. Intro"].content) + def test_data_max_length_matches_the_column_width(self): self.assertEqual(DATA_MAX_LENGTH, frappe.db.VARCHAR_LEN) @@ -216,7 +249,7 @@ def test_deep_tree_with_long_titles_stays_within_route_limit(self): res = self._generate() docs = self._docs_under(res["root_group"]) - self.assertEqual(len(docs), len(titles) + 1) + self.assertEqual(len(docs), 2 * len(titles)) self.assertTrue(all(len(doc.route) <= DATA_MAX_LENGTH for doc in docs.values())) self.assertEqual(len({doc.route for doc in docs.values()}), len(docs)) From eac9b053dadb981d1e0d01a86ccf374f08c1fd11 Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 02:20:14 +0530 Subject: [PATCH 05/33] feat(sectionize): let the llm decide which sections become wiki pages --- wikify/engine/loader/page_plan.py | 203 ++++++++++++++++++++++++++++++ wikify/engine/sectionize.py | 13 +- wikify/tests/test_page_plan.py | 198 +++++++++++++++++++++++++++++ 3 files changed, 410 insertions(+), 4 deletions(-) create mode 100644 wikify/engine/loader/page_plan.py create mode 100644 wikify/tests/test_page_plan.py diff --git a/wikify/engine/loader/page_plan.py b/wikify/engine/loader/page_plan.py new file mode 100644 index 0000000..691c3de --- /dev/null +++ b/wikify/engine/loader/page_plan.py @@ -0,0 +1,203 @@ +"""Decide which sections become wiki pages; every other section folds into its nearest +page ancestor as an in-page heading. Generation mirrors sections 1:1, so this is where +wiki page granularity is set. +""" + +from __future__ import annotations + +import json +from collections import Counter + +import frappe + +from wikify.engine import llm, settings +from wikify.engine.loader.context import context_block +from wikify.engine.loader.sectionizer import Section, _section_number +from wikify.engine.store import resolve_parent_indexes + +FALLBACK_PAGE_NUMBER_DEPTH = 3 +FALLBACK_PAGE_TREE_DEPTH = 1 +OUTLINE_BATCH_LINES = 250 + +_PROMPT = ( + "You are planning how a PDF document becomes a wiki. Below is its section outline in " + "document order, one section per line: index | depth | title | PDF pages | own words | " + "words including subsections | child sections.\n\n" + "Decide which sections start their own wiki page. Every other section is merged into its " + "nearest ancestor page as an in-page heading, so no text is lost.\n" + "- A wiki page is one coherent topic a reader would bookmark, mirroring how the document's " + "own table of contents presents topics.\n" + "- Sub-points fold into their parent topic: deeper numbering (e.g. 4.2.1.3 under 4.2.1), " + "bold or unnumbered sub-headings, and list-like headings (e.g. 'Class I', 'Type 2', " + "'1. Dosage').\n" + "- Sections that start on the same PDF page and belong to the same parent topic should " + "end up on the same wiki page.\n" + "- Avoid tiny pages (under ~150 words including their subsections) unless the section is " + "a standalone policy or topic.\n" + "- Split a very long topic (over ~4000 words) only at its natural numbered sub-sections.\n" + "- Depth-1 sections are always pages; a section can only be a page if its parent is one.\n\n" + 'Respond ONLY as JSON: {"pages": []}\n\n' +) + + +def plan_pages(sections: list[Section], project_context: str = "", use_llm: bool = False) -> list[Section]: + parents = resolve_parent_indexes(sections) + chosen = llm_pages(sections, parents, project_context) if use_llm else fallback_pages(sections, parents) + return fold_sections(sections, parents, enforce_invariants(sections, parents, chosen)) + + +def tree_depths(parents: list[int | None]) -> list[int]: + depths: list[int] = [] + for parent in parents: + depths.append(0 if parent is None else depths[parent] + 1) + return depths + + +def outline_numbers(sections: list[Section], parents: list[int | None]) -> list[tuple[int, ...] | None]: + """A heading's number only counts when it extends its nearest numbered ancestor's, so a + list item like "1. Dosage" under 4.2.1 reads as unnumbered.""" + numbers: list[tuple[int, ...] | None] = [] + numbered_ancestor: list[int | None] = [] + for index, section in enumerate(sections): + parent = parents[index] + ancestor = None if parent is None else (parent if numbers[parent] else numbered_ancestor[parent]) + number = _section_number(section.title) + if number and ancestor is not None: + prefix = numbers[ancestor] + if len(number) <= len(prefix) or number[: len(prefix)] != prefix: + number = None + numbers.append(number) + numbered_ancestor.append(ancestor) + return numbers + + +def fallback_pages(sections: list[Section], parents: list[int | None]) -> set[int]: + numbers = outline_numbers(sections, parents) + depths = tree_depths(parents) + pages: set[int] = set() + for index in range(len(sections)): + number = numbers[index] + has_numbered_ancestor = any(numbers[ancestor] for ancestor in ancestors(parents, index)) + if ( + parents[index] is None + or (number and len(number) <= FALLBACK_PAGE_NUMBER_DEPTH) + or (not number and not has_numbered_ancestor and depths[index] <= FALLBACK_PAGE_TREE_DEPTH) + ): + pages.add(index) + return pages + + +def ancestors(parents: list[int | None], index: int): + parent = parents[index] + while parent is not None: + yield parent + parent = parents[parent] + + +def enforce_invariants(sections: list[Section], parents: list[int | None], chosen: set[int]) -> set[int]: + pages: set[int] = set() + for index, parent in enumerate(parents): + if parent is None or (index in chosen and parent in pages): + pages.add(index) + child_counts = Counter(parent for parent in parents if parent is not None) + return { + index + for index in pages + if parents[index] is None + or index in child_counts + or (sections[index].markdown.strip() and child_counts[parents[index]] > 1) + } + + +def fold_sections(sections: list[Section], parents: list[int | None], pages: set[int]) -> list[Section]: + owner: dict[int, int] = {} + depth_below_page: dict[int, int] = {} + for index, section in enumerate(sections): + if index in pages: + owner[index], depth_below_page[index] = index, 0 + continue + parent = parents[index] + page = sections[owner[parent]] + owner[index], depth_below_page[index] = owner[parent], depth_below_page[parent] + 1 + heading = f"{'#' * min(6, depth_below_page[index] + 1)} {section.title}" + page.markdown = "\n\n".join( + part for part in (page.markdown.strip(), heading, section.markdown.strip()) if part + ) + page.page_end = max(page.page_end, section.page_end) + return [section for index, section in enumerate(sections) if index in pages] + + +def llm_pages(sections: list[Section], parents: list[int | None], project_context: str) -> set[int]: + chosen: set[int] = set() + for batch in outline_batches(parents): + chosen |= ask_for_pages(sections, parents, batch, project_context) + return chosen + + +def outline_batches(parents: list[int | None]) -> list[list[int]]: + chapters: list[list[int]] = [] + for index, parent in enumerate(parents): + if parent is None: + chapters.append([]) + chapters[-1].append(index) + batches: list[list[int]] = [] + for chapter in chapters: + if batches and len(batches[-1]) + len(chapter) <= OUTLINE_BATCH_LINES: + batches[-1].extend(chapter) + else: + batches.append(list(chapter)) + return batches + + +def format_outline(sections: list[Section], parents: list[int | None], batch: list[int]) -> str: + depths = tree_depths(parents) + own_words = [len(section.markdown.split()) for section in sections] + total_words = list(own_words) + child_counts = [0] * len(sections) + for index in reversed(range(len(sections))): + parent = parents[index] + if parent is not None: + total_words[parent] += total_words[index] + child_counts[parent] += 1 + lines = [] + for index in batch: + section = sections[index] + fields = ( + index, + depths[index] + 1, + section.title, + f"p{section.page_start}-{section.page_end}", + own_words[index], + total_words[index], + child_counts[index], + ) + lines.append(" " * depths[index] + " | ".join(str(field) for field in fields)) + return "\n".join(lines) + + +def ask_for_pages( + sections: list[Section], parents: list[int | None], batch: list[int], project_context: str +) -> set[int]: + prompt = ( + context_block(project_context) + _PROMPT + "OUTLINE:\n" + format_outline(sections, parents, batch) + ) + try: + response = llm.chat_completion( + settings.get("classifier_model"), + [{"role": "user", "content": prompt}], + label="page_plan", + response_format={"type": "json_object"}, + max_tokens=4096, + timeout=300, + ) + pages = json.loads(response["choices"][0]["message"]["content"] or "{}").get("pages") + if isinstance(pages, str): + # The Claude CLI backend's open object schema returns the list JSON-encoded. + pages = json.loads(pages) + except Exception: + frappe.log_error(title="Wikify page plan failed") + pages = None + batch_indexes = set(batch) + if not isinstance(pages, list): + return fallback_pages(sections, parents) & batch_indexes + return {index for index in pages if type(index) is int and index in batch_indexes} diff --git a/wikify/engine/sectionize.py b/wikify/engine/sectionize.py index 7473e5d..cbaa58a 100644 --- a/wikify/engine/sectionize.py +++ b/wikify/engine/sectionize.py @@ -20,19 +20,24 @@ import frappe -from wikify.engine import store +from wikify.engine import llm, store from wikify.engine.classify import classify_document from wikify.engine.lint import fix_table_separators from wikify.engine.loader.cleanup import clean_pages +from wikify.engine.loader.page_plan import plan_pages from wikify.engine.loader.sectionizer import _HEADING_RE, _clean_title, sectionize from wikify.engine.loader.toc import toc_level_map -def sectionize_document(source_document: str, pdf_path: str) -> int: +def sectionize_document(source_document: str, pdf_path: str, project_context: str = "") -> int: """Rebuild the Source Section tree from the doc's canonical pages. Returns the count.""" level_map = toc_level_map(str(pdf_path)) pages = clean_pages(store.get_canonical_pages(source_document)) - sections = sectionize(pages, level_map) + sections = plan_pages( + sectionize(pages, level_map), + project_context, + use_llm=llm.has_openrouter() and not frappe.flags.in_test, + ) # 0.6 auto-fix boundary: repair separator-less tables in the assembled section # product, pre-review. Page canonical stays untouched — pages are evidence. for sec in sections: @@ -55,7 +60,7 @@ def rebuild_and_classify( """ if stage_cb: stage_cb("Building section tree") - count = sectionize_document(source_document, pdf_path) + count = sectionize_document(source_document, pdf_path, project_context) if stage_cb: stage_cb("Classifying sections") classify_document( diff --git a/wikify/tests/test_page_plan.py b/wikify/tests/test_page_plan.py new file mode 100644 index 0000000..693d899 --- /dev/null +++ b/wikify/tests/test_page_plan.py @@ -0,0 +1,198 @@ +# Copyright (c) 2026, BWH and contributors +# For license information, please see license.txt +import json +from unittest.mock import patch + +from frappe.tests.utils import FrappeTestCase + +from wikify.engine.loader import page_plan +from wikify.engine.loader.page_plan import plan_pages +from wikify.engine.loader.sectionizer import Section + + +def _sec(path, page_start, page_end=None, markdown=None): + return Section( + title=path[-1], + level=len(path), + hierarchy_path=path, + page_start=page_start, + page_end=page_end or page_start, + markdown=markdown if markdown is not None else f"body of {path[-1]}", + ) + + +def _outline(): + return [ + _sec(["Preamble"], 1), + _sec(["5.8 Care"], 2, markdown=""), + _sec(["5.8 Care", "5.8.1 Chaperone"], 2), + _sec(["5.8 Care", "5.8.4 Sedation"], 3), + _sec(["5.8 Care", "5.8.4 Sedation", "5.8.4.1 Indications"], 4), + _sec(["5.8 Care", "5.8.4 Sedation", "5.8.4.1 Indications", "Contraindications"], 4), + _sec(["5.8 Care", "5.8.4 Sedation", "5.8.4.2 Consents"], 5, 6), + _sec(["5.8 Care", "5.8.9 Grievances"], 7), + _sec(["5.8 Care", "5.8.9 Grievances", "1. Drug Regimen"], 7), + _sec(["6.1 Protocols"], 8, markdown=""), + _sec(["6.1 Protocols", "6.1.2 Lupus nephritis"], 8), + _sec(["6.1 Protocols", "6.1.2 Lupus nephritis", "Class IV"], 9, markdown=""), + _sec(["6.1 Protocols", "6.1.2 Lupus nephritis", "Class V"], 10), + ] + + +def _llm_reply(payload): + content = payload if isinstance(payload, str) else json.dumps(payload) + return {"choices": [{"message": {"content": content}}]} + + +def _titles(sections): + return [section.title for section in sections] + + +class TestFallbackPagePlan(FrappeTestCase): + def test_pages_are_the_top_two_numbered_levels_below_the_chapter(self): + pages = plan_pages(_outline()) + self.assertEqual( + _titles(pages), + [ + "Preamble", + "5.8 Care", + "5.8.1 Chaperone", + "5.8.4 Sedation", + "5.8.9 Grievances", + "6.1 Protocols", + "6.1.2 Lupus nephritis", + ], + ) + + def test_unnumbered_document_keeps_its_top_two_heading_levels(self): + sections = [ + _sec(["Guide"], 1), + _sec(["Guide", "Setup"], 1), + _sec(["Guide", "Setup", "Prerequisites"], 2), + _sec(["Guide", "Usage"], 3), + ] + self.assertEqual(_titles(plan_pages(sections)), ["Guide", "Setup", "Usage"]) + + def test_a_lone_sub_section_folds_into_its_parent(self): + sections = [ + _sec(["5.7 Billing"], 1, markdown=""), + _sec(["5.7 Billing", "5.7.2 Inpatient billing"], 1, 2), + _sec(["5.8 Care"], 2), + _sec(["5.8 Care", "5.8.1 Chaperone"], 2), + _sec(["5.8 Care", "5.8.1 Chaperone", "5.8.1.1 Escort"], 3), + ] + pages = plan_pages(sections) + self.assertEqual(_titles(pages), ["5.7 Billing", "5.8 Care", "5.8.1 Chaperone"]) + self.assertEqual(pages[0].markdown, "## 5.7.2 Inpatient billing\n\nbody of 5.7.2 Inpatient billing") + + def test_folded_sections_become_headings_in_document_order(self): + sedation = plan_pages(_outline())[3] + self.assertEqual( + sedation.markdown, + "body of 5.8.4 Sedation\n\n" + "## 5.8.4.1 Indications\n\nbody of 5.8.4.1 Indications\n\n" + "### Contraindications\n\nbody of Contraindications\n\n" + "## 5.8.4.2 Consents\n\nbody of 5.8.4.2 Consents", + ) + self.assertEqual((sedation.page_start, sedation.page_end), (3, 6)) + + def test_an_empty_folded_section_keeps_its_heading(self): + lupus = plan_pages(_outline())[-1] + self.assertEqual( + lupus.markdown, + "body of 6.1.2 Lupus nephritis\n\n## Class IV\n\n## Class V\n\nbody of Class V", + ) + self.assertEqual(lupus.page_end, 10) + + def test_heading_depth_is_capped_at_six(self): + path = [f"Level {depth}" for depth in range(1, 9)] + sections = [_sec(path[:depth], 1) for depth in range(1, 9)] + child = plan_pages(sections)[1] + self.assertEqual(child.title, "Level 2") + self.assertIn("###### Level 7\n\nbody of Level 7\n\n###### Level 8", child.markdown) + + def test_hierarchy_of_the_remaining_pages_is_unchanged(self): + pages = plan_pages(_outline()) + self.assertEqual(pages[-1].hierarchy_path, ["6.1 Protocols", "6.1.2 Lupus nephritis"]) + + +class TestLlmPagePlan(FrappeTestCase): + def _plan(self, reply, sections=None): + with patch.object(page_plan.llm, "chat_completion", return_value=reply) as chat: + pages = plan_pages(sections or _outline(), "Hospital manual", use_llm=True) + return pages, chat + + def test_llm_choice_decides_the_pages(self): + pages, chat = self._plan(_llm_reply({"pages": [2, 3, 7, 10]})) + self.assertEqual( + _titles(pages), + [ + "Preamble", + "5.8 Care", + "5.8.1 Chaperone", + "5.8.4 Sedation", + "5.8.9 Grievances", + "6.1 Protocols", + "6.1.2 Lupus nephritis", + ], + ) + self.assertEqual(chat.call_args.kwargs["label"], "page_plan") + self.assertEqual(chat.call_args.kwargs["response_format"], {"type": "json_object"}) + prompt = chat.call_args.args[1][0]["content"] + self.assertIn("Hospital manual", prompt) + self.assertIn("4 | 3 | 5.8.4.1 Indications | p4-4 | 4 | 7 | 1", prompt) + + def test_a_json_encoded_page_list_is_accepted(self): + pages, _ = self._plan(_llm_reply({"pages": "[3, 7]"})) + self.assertEqual( + _titles(pages), ["Preamble", "5.8 Care", "5.8.4 Sedation", "5.8.9 Grievances", "6.1 Protocols"] + ) + + def test_top_level_sections_are_pages_even_when_the_llm_skips_them(self): + pages, _ = self._plan(_llm_reply({"pages": []})) + self.assertEqual(_titles(pages), ["Preamble", "5.8 Care", "6.1 Protocols"]) + self.assertIn("## 5.8.1 Chaperone", pages[1].markdown) + self.assertIn("### 5.8.4.1 Indications", pages[1].markdown) + + def test_a_page_under_a_folded_parent_folds_with_it(self): + pages, _ = self._plan(_llm_reply({"pages": [4, 5]})) + self.assertEqual(_titles(pages), ["Preamble", "5.8 Care", "6.1 Protocols"]) + + def test_an_empty_page_without_page_children_folds_into_its_parent(self): + pages, _ = self._plan(_llm_reply({"pages": [10, 11]})) + self.assertEqual(_titles(pages)[-2:], ["6.1 Protocols", "6.1.2 Lupus nephritis"]) + self.assertIn("## Class IV\n\n## Class V", pages[-1].markdown) + + def test_unusable_indices_are_ignored(self): + pages, _ = self._plan(_llm_reply({"pages": ["2", 99, -1, True, 3]})) + self.assertEqual(_titles(pages), ["Preamble", "5.8 Care", "5.8.4 Sedation", "6.1 Protocols"]) + + def test_invalid_reply_falls_back_to_the_numbering_rule(self): + expected = _titles(plan_pages(_outline())) + for reply in (_llm_reply("not json"), _llm_reply({"pages": "all"}), _llm_reply({})): + with patch.object(page_plan.frappe, "log_error"): + pages, _ = self._plan(reply) + self.assertEqual(_titles(pages), expected) + + def test_llm_error_falls_back_to_the_numbering_rule(self): + with ( + patch.object(page_plan.llm, "chat_completion", side_effect=RuntimeError("down")), + patch.object(page_plan.frappe, "log_error") as log_error, + ): + pages = plan_pages(_outline(), use_llm=True) + self.assertEqual(_titles(pages), _titles(plan_pages(_outline()))) + log_error.assert_called_once() + + def test_large_outlines_are_sent_per_chapter(self): + replies = [_llm_reply({"pages": [2]}), _llm_reply({"pages": [10]})] + with ( + patch.object(page_plan, "OUTLINE_BATCH_LINES", 9), + patch.object(page_plan.llm, "chat_completion", side_effect=replies) as chat, + ): + pages = plan_pages(_outline(), use_llm=True) + self.assertEqual(chat.call_count, 2) + self.assertNotIn("6.1 Protocols", chat.call_args_list[0].args[1][0]["content"]) + self.assertEqual( + _titles(pages), + ["Preamble", "5.8 Care", "5.8.1 Chaperone", "6.1 Protocols", "6.1.2 Lupus nephritis"], + ) From d082bc2b81d51a1fca57846ac1313c5df582a51f Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 02:48:28 +0530 Subject: [PATCH 06/33] fix(cleanup): strip sign-off footers written as text, not only as table rows --- wikify/engine/loader/cleanup.py | 18 ++++++++++++++++-- wikify/tests/test_sectionize.py | 17 +++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/wikify/engine/loader/cleanup.py b/wikify/engine/loader/cleanup.py index f924fc6..dae780c 100644 --- a/wikify/engine/loader/cleanup.py +++ b/wikify/engine/loader/cleanup.py @@ -13,6 +13,7 @@ import re from collections import Counter +from itertools import groupby _NORM = re.compile(r"[#*_`>\-\s]+") _VARYING = [ @@ -31,6 +32,8 @@ # carrying >=2 distinct sign-off phrases (a lone "approved by" in a data row is kept). _SEP_ONLY = re.compile(r"^\s*\|[\s:|-]+\|\s*$") _SIGNOFF = ("prepared by", "issued by", "approved by", "reviewed by", "authorized by") +_SIGNOFF_LABEL = re.compile(r"(?i)\b(prepared|issued|approved|reviewed|authori[sz]ed) by\s*[:\-\u2013]") +_SIGNOFF_LINE = re.compile(r"(?i)^[*_\s]*(prepared|issued|approved|reviewed|authori[sz]ed) by\s*[:\-\u2013]") _PAGE_NUMBER = re.compile(r"^\s*\d{1,4}\s*$") _SENTENCE_START = re.compile(r"^[a-z]") # A sentence cut by a page break ran to the page edge, so its last line is never a short label. @@ -47,15 +50,26 @@ def _norm(line: str) -> str: def _is_signoff_footer_row(line: str) -> bool: s = line.strip() if not (s.startswith("|") and s.endswith("|")): - return False + return len({label.lower() for label in _SIGNOFF_LABEL.findall(s)}) >= 2 low = s.lower() return sum(kw in low for kw in _SIGNOFF) >= 2 +def _signoff_line_runs(lines: list[str]) -> set[int]: + """Sign-off labels laid out one per paragraph, e.g. "Prepared by: …" then "Issued by: …".""" + drop: set[int] = set() + non_blank = [index for index, line in enumerate(lines) if line.strip()] + for is_signoff, group in groupby(non_blank, key=lambda index: bool(_SIGNOFF_LINE.match(lines[index]))): + run = list(group) + if is_signoff and len({_SIGNOFF_LINE.match(lines[index]).group(1).lower() for index in run}) >= 2: + drop.update(run) + return drop + + def _strip_footer_blocks(md: str) -> str: """Drop sign-off footer rows and any separator row orphaned next to them.""" lines = md.splitlines() - drop: set[int] = set() + drop = _signoff_line_runs(lines) for i, line in enumerate(lines): if _is_signoff_footer_row(line): drop.add(i) diff --git a/wikify/tests/test_sectionize.py b/wikify/tests/test_sectionize.py index 622adb5..ed86f5e 100644 --- a/wikify/tests/test_sectionize.py +++ b/wikify/tests/test_sectionize.py @@ -332,6 +332,23 @@ def test_clean_pages_keeps_data_row_mentioning_approved_by_once(self): self.assertIn("approved by committee", cleaned[1]) self.assertIn("|---|---|", cleaned[1]) + def test_clean_pages_strips_signoff_footer_written_as_text(self): + pipes = "Prepared by: Dr. A, Dr. B | Issued by: QMC | Approved by: Dr. C" + merged = "**Prepared by: Dr. A, Dr. Issued by: QMC Approved by: Dr. C B** " + paragraphs = "Prepared by: Dr. A, Dr. B\n\nIssued by: QMC\n\nApproved by: Dr. C" + pages = [ + (1, f"## DONOR WITH OBESITY\nreal body\n\n{pipes}"), + (2, f"- more body\n\n{merged}\n"), + (3, f"## DIABETIC PATIENT\n\n{paragraphs}\n\nThe protocol is approved by the HOD."), + ] + cleaned = dict(clean_pages(pages)) + for md in cleaned.values(): + self.assertNotIn("Prepared by", md) + self.assertNotIn("Issued by", md) + self.assertIn("real body", cleaned[1]) + self.assertIn("- more body", cleaned[2]) + self.assertIn("approved by the HOD", cleaned[3]) + class TestEmptySectionsAreFlagged(FrappeTestCase): def test_section_without_markdown_is_chunked_as_title_only(self): From c0333b37aa76e0af2c108b4ff8cdcb4d19fb297f Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 02:48:28 +0530 Subject: [PATCH 07/33] fix(sectionize): read a table of contents as one contents section and keep label headings as text --- wikify/engine/loader/sectionizer.py | 67 +++++++++++++++++++++++++++-- wikify/tests/test_sectionize.py | 46 ++++++++++++++++++++ 2 files changed, 109 insertions(+), 4 deletions(-) diff --git a/wikify/engine/loader/sectionizer.py b/wikify/engine/loader/sectionizer.py index 5fa79c4..455d3ed 100644 --- a/wikify/engine/loader/sectionizer.py +++ b/wikify/engine/loader/sectionizer.py @@ -3,7 +3,9 @@ import re from collections import defaultdict from dataclasses import dataclass +from itertools import pairwise +from wikify.engine.loader.cleanup import _SEP_ONLY from wikify.engine.loader.toc import correct_level MAX_TITLE_LENGTH = 140 @@ -17,10 +19,19 @@ _DOUBLE_NUM = re.compile(r"^\d+\.\s+\d") _GLUED_NUM = re.compile(r"^(\d+(?:\.\d+)+\.?)(?=[A-Z])") _BOLD_LINE = re.compile(r"^\*\*([^*]+)\*\*$") +_LINK = re.compile(r"\[([^\]]*)\]\([^)]*\)") +_CONTENTS_TITLE = re.compile(r"(?i)^(?:table of )?contents$|^index$") +_TOC_ENTRY = re.compile( + r"^(?:[-*+]\s+)?\|?\s*\d+(?:\.\d+)*\.?\s*\|?\s*[A-Za-z(].*?(?:\.{3,}|\s[-\u2013\u2014]\s|\||
|\s)\s*(\d{1,4})\s*\|?$" +) +_TOC_LEADER = re.compile(r"[A-Za-z].*\.{4,}\s*(\d{1,4})$") +TOC_MIN_ENTRIES = 5 +TOC_MIN_ENTRY_SHARE = 0.6 +TOC_MIN_ASCENDING_SHARE = 0.75 def _clean_title(raw: str) -> str: - title = _GLUED_NUM.sub(r"\1 ", raw.strip().strip("*_").strip()) + title = _GLUED_NUM.sub(r"\1 ", _LINK.sub(r"\1", raw.strip().strip("*_").strip()).strip()) if title.endswith(".") and not title.endswith(".."): title = title[:-1].rstrip() if len(title) <= MAX_TITLE_LENGTH: @@ -37,6 +48,12 @@ def _infer_level(title: str, fallback: int) -> int: return fallback +def _is_label(title: str, level_map: dict[str, int]) -> bool: + return ( + title not in level_map and not _NUM_RE.match(title) and (title[:1].islower() or title.endswith(":")) + ) + + def _chapter_num(title: str) -> int | None: m = _LEADING_NUM.match(title) return int(m.group(1)) if m else None @@ -92,6 +109,27 @@ def _promote_numbered_bold_line(line: str, last_number: tuple[int, ...]) -> str: return f"{'#' * min(6, len(number))} {title}" +def toc_end_line(lines: list[str]) -> int: + """Index of the last table-of-contents entry when the page is a TOC page, else -1. TOC entries name + real headings, so reading them as headings opens phantom sections and advances chapter numbering.""" + entries: list[tuple[int, int]] = [] + content_lines = 0 + for index, line in enumerate(lines): + text = line.strip() + if not text or _SEP_ONLY.match(text): + continue + content_lines += 1 + entry = _TOC_ENTRY.match(text) or _TOC_LEADER.search(text) + if entry: + entries.append((index, int(entry.group(1)))) + if len(entries) < TOC_MIN_ENTRIES or len(entries) < TOC_MIN_ENTRY_SHARE * content_lines: + return -1 + ascending = sum(later >= earlier for (_, earlier), (_, later) in pairwise(entries)) + if ascending < TOC_MIN_ASCENDING_SHARE * (len(entries) - 1): + return -1 + return entries[-1][0] + + def running_header_titles(pages: list[tuple[int, str]]) -> set[str]: occurrences: dict[str, list[int]] = defaultdict(list) pages_seen: dict[str, set[int]] = defaultdict(set) @@ -141,10 +179,31 @@ def flush(): sections.append(current) for page_no, md in pages: - for line in md.splitlines(): - m = _HEADING_RE.match(_promote_numbered_bold_line(line, last_number)) + lines = md.splitlines() + toc_end = toc_end_line(lines) + for line_index, line in enumerate(lines): + in_toc = line_index <= toc_end and bool(line.strip()) + m = _HEADING_RE.match(line if in_toc else _promote_numbered_bold_line(line, last_number)) + title = _clean_title(m.group(2)) if m else "" + if in_toc: + if not (current and _CONTENTS_TITLE.match(current.title)): + flush() + buf = [] + contents_title = title if _CONTENTS_TITLE.match(title) else "Contents" + stack = [(1, contents_title, False)] + current = Section( + title=contents_title, + level=1, + hierarchy_path=[contents_title], + page_start=page_no, + page_end=page_no, + ) + if contents_title == title: + continue + m = None + elif m and _is_label(title, level_map): + line, m = f"**{title}**", None if m: - title = _clean_title(m.group(2)) if _repeats_open_section(title, stack): if current is not None: current.page_end = page_no diff --git a/wikify/tests/test_sectionize.py b/wikify/tests/test_sectionize.py index ed86f5e..623058b 100644 --- a/wikify/tests/test_sectionize.py +++ b/wikify/tests/test_sectionize.py @@ -332,6 +332,52 @@ def test_clean_pages_keeps_data_row_mentioning_approved_by_once(self): self.assertIn("approved by committee", cleaned[1]) self.assertIn("|---|---|", cleaned[1]) + def test_table_of_contents_is_one_section_that_never_opens_chapters(self): + table_rows = "\n".join(f"| 1.{n} | Scope of service {n} | {18 + n} |" for n in range(1, 7)) + procedure_rows = "\n".join(f"| 5.{n} | Policies for topic {n} | {95 + n} |" for n in range(1, 7)) + list_rows = "\n".join(f" - 6.1.{n} Protocol {n} — {200 + n}" for n in range(1, 7)) + pages = [ + (1, "# PROCEDURE MANUAL\n\n## REVISION HISTORY\nrevisions"), + (2, f"## Contents\n\n| S.No | CONTENTS | Pg No |\n|---|---|---|\n{table_rows}"), + ( + 3, + f"## 5 Procedures for access of the patients\n\n| No. | Topic | Page |\n|---|---|---|\n{procedure_rows}", + ), + (4, f"- 6.1 Protocols — 199\n{list_rows}\n\n**4**"), + (5, "# 1. PROFILE OF THE DEPARTMENT\n\n## Overview\nIn the year 1900 the institution began."), + ] + secs = sectionize(pages) + self.assertEqual( + [(s.title, s.level) for s in secs], + [ + ("PROCEDURE MANUAL", 1), + ("REVISION HISTORY", 2), + ("Contents", 1), + ("1. PROFILE OF THE DEPARTMENT", 1), + ("Overview", 2), + ], + ) + contents = secs[2] + self.assertEqual((contents.page_start, contents.page_end), (2, 4)) + self.assertIn("5 Procedures for access of the patients", contents.markdown) + self.assertIn("6.1.6 Protocol 6 — 206", contents.markdown) + + def test_link_markup_is_stripped_from_heading_titles(self): + secs = sectionize([(1, "# [PROCEDURE MANUAL - NEPHROLOGY](#)\n\n## REVISION HISTORY\nrows")]) + self.assertEqual(secs[0].title, "PROCEDURE MANUAL - NEPHROLOGY") + + def test_lowercase_or_colon_heading_is_a_bold_label(self): + pages = [ + ( + 1, + "# **IMMEDIATE PRE-OPERATIVE PROTOCOLS**\nintro\n# **are carried out as below:**\n" + "- PreHD: WBC\n# **Drug list:**\n- MMF 8 am\n# 5.1 Definitions:\nterms", + ) + ] + secs = sectionize(pages) + self.assertEqual([s.title for s in secs], ["IMMEDIATE PRE-OPERATIVE PROTOCOLS", "5.1 Definitions:"]) + self.assertIn("**are carried out as below:**\n- PreHD: WBC\n**Drug list:**", secs[0].markdown) + def test_clean_pages_strips_signoff_footer_written_as_text(self): pipes = "Prepared by: Dr. A, Dr. B | Issued by: QMC | Approved by: Dr. C" merged = "**Prepared by: Dr. A, Dr. Issued by: QMC Approved by: Dr. C B** " From af39e72bc97011fe9e6228153bf38df2c66fd459 Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 02:48:28 +0530 Subject: [PATCH 08/33] fix(page-plan): fold unnumbered top-level fragments and split pages that run too long --- wikify/engine/loader/page_plan.py | 58 +++++++++++++++++++++++++++---- wikify/tests/test_page_plan.py | 54 ++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 6 deletions(-) diff --git a/wikify/engine/loader/page_plan.py b/wikify/engine/loader/page_plan.py index 691c3de..97c6ce5 100644 --- a/wikify/engine/loader/page_plan.py +++ b/wikify/engine/loader/page_plan.py @@ -6,7 +6,7 @@ from __future__ import annotations import json -from collections import Counter +from collections import Counter, defaultdict import frappe @@ -17,6 +17,9 @@ FALLBACK_PAGE_NUMBER_DEPTH = 3 FALLBACK_PAGE_TREE_DEPTH = 1 +FRAGMENT_WORDS = 40 +MAX_PAGE_WORDS = 2500 +MAX_PAGE_PDF_PAGES = 6 OUTLINE_BATCH_LINES = 250 _PROMPT = ( @@ -35,7 +38,9 @@ "- Avoid tiny pages (under ~150 words including their subsections) unless the section is " "a standalone policy or topic.\n" "- Split a very long topic (over ~4000 words) only at its natural numbered sub-sections.\n" - "- Depth-1 sections are always pages; a section can only be a page if its parent is one.\n\n" + "- Numbered depth-1 sections are always pages; an unnumbered depth-1 fragment (a form label, a " + "caption, a stray sentence) is not, and merges into the page before it. A section can only be a " + "page if its parent is one.\n\n" 'Respond ONLY as JSON: {"pages": []}\n\n' ) @@ -95,11 +100,17 @@ def ancestors(parents: list[int | None], index: int): def enforce_invariants(sections: list[Section], parents: list[int | None], chosen: set[int]) -> set[int]: + numbers = outline_numbers(sections, parents) + child_counts = Counter(parent for parent in parents if parent is not None) pages: set[int] = set() for index, parent in enumerate(parents): - if parent is None or (index in chosen and parent in pages): + if parent is None: + fragment = index not in child_counts and len(sections[index].markdown.split()) < FRAGMENT_WORDS + if index == 0 or numbers[index] or (index in chosen and not fragment): + pages.add(index) + elif index in chosen and parent in pages: pages.add(index) - child_counts = Counter(parent for parent in parents if parent is not None) + pages = split_long_pages(sections, parents, numbers, pages) return { index for index in pages @@ -109,6 +120,38 @@ def enforce_invariants(sections: list[Section], parents: list[int | None], chose } +def split_long_pages( + sections: list[Section], parents: list[int | None], numbers: list[tuple[int, ...] | None], pages: set[int] +) -> set[int]: + """A page too long to read in one scroll is split at its numbered children, recursively; tiny + children stay folded.""" + children: dict[int, list[int]] = defaultdict(list) + subtree_words = [len(section.markdown.split()) for section in sections] + for index in reversed(range(len(sections))): + parent = parents[index] + if parent is not None: + children[parent].insert(0, index) + subtree_words[parent] += subtree_words[index] + pages = set(pages) + queue = sorted(pages) + for page in queue: + folded = [page] + for index in folded: + folded.extend(child for child in children[index] if child not in pages) + words = sum(len(sections[index].markdown.split()) for index in folded) + pdf_pages = max(sections[index].page_end for index in folded) - sections[page].page_start + 1 + if words <= MAX_PAGE_WORDS and pdf_pages <= MAX_PAGE_PDF_PAGES: + continue + split = [ + child + for child in children[page] + if numbers[child] and child not in pages and subtree_words[child] >= FRAGMENT_WORDS + ] + pages.update(split) + queue.extend(split) + return pages + + def fold_sections(sections: list[Section], parents: list[int | None], pages: set[int]) -> list[Section]: owner: dict[int, int] = {} depth_below_page: dict[int, int] = {} @@ -117,8 +160,11 @@ def fold_sections(sections: list[Section], parents: list[int | None], pages: set owner[index], depth_below_page[index] = index, 0 continue parent = parents[index] - page = sections[owner[parent]] - owner[index], depth_below_page[index] = owner[parent], depth_below_page[parent] + 1 + if parent is None: + owner[index], depth_below_page[index] = owner[index - 1], 1 + else: + owner[index], depth_below_page[index] = owner[parent], depth_below_page[parent] + 1 + page = sections[owner[index]] heading = f"{'#' * min(6, depth_below_page[index] + 1)} {section.title}" page.markdown = "\n\n".join( part for part in (page.markdown.strip(), heading, section.markdown.strip()) if part diff --git a/wikify/tests/test_page_plan.py b/wikify/tests/test_page_plan.py index 693d899..94af85f 100644 --- a/wikify/tests/test_page_plan.py +++ b/wikify/tests/test_page_plan.py @@ -39,6 +39,10 @@ def _outline(): ] +def _words(count): + return " ".join(["word"] * count) + + def _llm_reply(payload): content = payload if isinstance(payload, str) else json.dumps(payload) return {"choices": [{"message": {"content": content}}]} @@ -196,3 +200,53 @@ def test_large_outlines_are_sent_per_chapter(self): _titles(pages), ["Preamble", "5.8 Care", "5.8.1 Chaperone", "6.1 Protocols", "6.1.2 Lupus nephritis"], ) + + def test_an_unnumbered_top_level_fragment_folds_into_the_page_before_it(self): + sections = [ + _sec(["Preamble"], 1), + _sec(["IMMEDIATE PRE-OPERATIVE PROTOCOLS"], 7, 9, markdown=_words(60)), + _sec(["Pre transplant day"], 9, markdown=""), + _sec(["PRE-TRANSPLANT RECIPIENT ORDER SHEET"], 9, 10, markdown=_words(60)), + _sec(["6.3.3.2 Donor evaluation"], 11), + ] + pages, _ = self._plan(_llm_reply({"pages": [1, 2]}), sections) + self.assertEqual( + _titles(pages), ["Preamble", "IMMEDIATE PRE-OPERATIVE PROTOCOLS", "6.3.3.2 Donor evaluation"] + ) + self.assertIn("## Pre transplant day\n\n## PRE-TRANSPLANT RECIPIENT ORDER SHEET", pages[1].markdown) + self.assertEqual(pages[1].page_end, 10) + self.assertEqual( + _titles(plan_pages(sections)), + [ + "Preamble", + "IMMEDIATE PRE-OPERATIVE PROTOCOLS", + "PRE-TRANSPLANT RECIPIENT ORDER SHEET", + "6.3.3.2 Donor evaluation", + ], + ) + + def test_a_page_spanning_too_many_pdf_pages_splits_at_its_numbered_children(self): + capd = "6.2.2 CAPD" + initiation = "6.2.2.1 Initiation" + sections = [ + _sec([capd], 4, markdown=""), + _sec([capd, "6.2.2.0 Introduction"], 4, markdown=_words(80)), + _sec([capd, initiation], 4, markdown=""), + _sec([capd, initiation, "6.2.2.1.1 Counselling"], 4, 5, markdown=_words(200)), + _sec([capd, initiation, "6.2.2.1.2 Pre-op evaluation"], 5, 17, markdown=_words(2000)), + _sec([capd, initiation, "6.2.2.1.3 Intra-op"], 17, 20, markdown=_words(800)), + _sec([capd, "6.2.2.2 Note"], 20, markdown=_words(10)), + ] + pages, _ = self._plan(_llm_reply({"pages": []}), sections) + self.assertEqual( + _titles(pages), + [ + capd, + "6.2.2.0 Introduction", + initiation, + "6.2.2.1.1 Counselling", + "6.2.2.1.2 Pre-op evaluation", + "6.2.2.1.3 Intra-op", + ], + ) + self.assertIn("## 6.2.2.2 Note", pages[0].markdown) From 3bef1fb0fb98a4bcd9898af11c239758fcdde2f5 Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 03:17:14 +0530 Subject: [PATCH 09/33] feat(images): crop body figures from the pdf and place them in reading order --- wikify/engine/__init__.py | 9 +- wikify/engine/images.py | 373 ++++++++++++++++++++++++++---------- wikify/engine/store.py | 17 +- wikify/tests/test_images.py | 116 +++++++++++ 4 files changed, 414 insertions(+), 101 deletions(-) create mode 100644 wikify/tests/test_images.py diff --git a/wikify/engine/__init__.py b/wikify/engine/__init__.py index bf14427..fc7a842 100644 --- a/wikify/engine/__init__.py +++ b/wikify/engine/__init__.py @@ -73,6 +73,7 @@ def parse_pdf( with fitz.open(pdf_path) as doc: total = doc.page_count + repeated = images.repeated_images(doc) sd = store.create_document( title=title, import_name=import_name, @@ -85,11 +86,11 @@ def parse_pdf( png = pdf_utils.render_png(page, dpi=dpi) kind = pdf_utils.classify_page(page, min_chars, min_drawings) markdown = baseline.parse_page(pdf_path, page_no) - # Rescue genuine graphics the baseline parser omits (diagrams/photos with no - # recoverable text) by embedding them inline; text-image placeholders are left - # for the cleanup pass. No-op on the common page with no genuine figure. - markdown = images.embed_genuine_images(page, markdown, sd, page_no, kind) page_name = store.add_page(sd, page_no, kind, png, markdown) + figures = images.page_figures(page, repeated, page_name, page_no) + if figures: + markdown = images.place_figures(markdown, figures, page_no) + store.set_baseline_markdown(page_name, markdown) # Visual pages MUST be judged (text GT is unreliable on diagrams); text # pages are judged only when the operator opted in (cost/latency). diff --git a/wikify/engine/images.py b/wikify/engine/images.py index 49f0b9e..8ec1570 100644 --- a/wikify/engine/images.py +++ b/wikify/engine/images.py @@ -1,102 +1,283 @@ -"""Extract genuine embedded graphics from a PDF page and embed them inline. +from __future__ import annotations -pymupdf4llm omits every image as a `==> picture [W x H] intentionally omitted <==` -placeholder. For an *image of text* it also recovers the text (a "picture text" block, -which the cleanup pass turns into real Markdown), so those don't need the image. But a -genuine graphic (diagram, flowchart, photo, chart) has no recoverable text — the -placeholder is all that's left, and cleanup strips even that, silently losing the figure. +import re +from collections import Counter +from dataclasses import dataclass -This replaces such genuine-graphic placeholders with an embedded image link to a private -Frappe File, so the figure survives into the section tree + wiki. Text-image placeholders -are left untouched for cleanup. Deterministic — no LLM, no key needed. -""" +from wikify.engine import pdf_utils, regions, store -from __future__ import annotations +FIGURE_DPI = 200 +CROP_PADDING = 4.0 +MERGE_GAP = 2.0 +MIN_FIGURE_SIDE = 72.0 +MIN_FIGURE_AREA = 0.02 +MAX_FIGURE_AREA = 0.85 +MIN_REPEATED_PAGES = 3 +REPEATED_PAGE_SHARE = 0.2 +CAPTION_GAP = 24.0 +MAX_CAPTION_CHARS = 120 +ANCHOR_LENGTHS = (60, 25) +MIN_ANCHOR_CHARS = 12 +EDGE_TOLERANCE = 2.0 -import re +FIGURE_TOKEN_RE = re.compile(r"\[\[FIGURE (\d+)(?::[ \t]*([^\]\n]*))?\]\]") +_CAPTION_RE = re.compile( + r"^(?:fig(?:ure)?|chart|graph|diagram|photo(?:graph)?|image|illustration|plate|exhibit)\.?\s*[\dA-Za-z]", + re.IGNORECASE, +) +_SKIPPED_MARKUP_RE = re.compile(r"!\[[^\]]*\]\([^)]*\)|<[^<>]*>|&#?\w+;") +_FENCE_RE = re.compile(r"^[ \t]*```", re.MULTILINE) + + +@dataclass +class Figure: + bbox: tuple[float, float, float, float] + page_size: tuple[float, float] = (1.0, 1.0) + caption: str = "" + text_before: str = "" + text_after: str = "" + url: str = "" + + +def repeated_images(pdf_document) -> set[bytes]: + pages_seen: Counter = Counter() + for page in pdf_document: + pages_seen.update({info["digest"] for info in page.get_image_info(hashes=True)}) + threshold = max(MIN_REPEATED_PAGES, pdf_document.page_count * REPEATED_PAGE_SHARE) + return {digest for digest, count in pages_seen.items() if count >= threshold} + + +def boxes_touch(box: tuple, other: tuple) -> bool: + return ( + box[0] - MERGE_GAP <= other[2] + and other[0] - MERGE_GAP <= box[2] + and box[1] - MERGE_GAP <= other[3] + and other[1] - MERGE_GAP <= box[3] + ) + + +def merge_touching(boxes: list[tuple]) -> list[tuple]: + merged: list[tuple] = [] + for box in boxes: + while touching := next((other for other in merged if boxes_touch(box, other)), None): + merged.remove(touching) + box = ( + min(box[0], touching[0]), + min(box[1], touching[1]), + max(box[2], touching[2]), + max(box[3], touching[3]), + ) + merged.append(box) + return merged + + +def is_figure_sized(box: tuple, page_box: tuple) -> bool: + if box[2] - box[0] < MIN_FIGURE_SIDE or box[3] - box[1] < MIN_FIGURE_SIDE: + return False + return MIN_FIGURE_AREA <= regions.area_fraction(box, page_box) <= MAX_FIGURE_AREA + + +def body_text_blocks(page, page_box: tuple) -> list[tuple[tuple, str]]: + blocks = [] + for block in page.get_text("blocks"): + text = " ".join((block[4] or "").split()) + box = tuple(float(value) for value in block[:4]) + if block[6] == 0 and text and not regions.is_furniture(box, page_box): + blocks.append((box, text)) + return blocks + + +def overlaps_horizontally(box: tuple, other: tuple) -> bool: + return min(box[2], other[2]) > max(box[0], other[0]) + + +def nearest_block(candidates: list[tuple[tuple, str]], figure_box: tuple, key) -> str: + aligned = [block for block in candidates if overlaps_horizontally(block[0], figure_box)] + pool = aligned or candidates + return min(pool, key=key)[1] if pool else "" + + +def find_caption(blocks: list[tuple[tuple, str]], figure_box: tuple) -> str: + for box, text in blocks: + near_below = 0 <= box[1] - figure_box[3] <= CAPTION_GAP + near_above = 0 <= figure_box[1] - box[3] <= CAPTION_GAP + if ( + (near_below or near_above) + and overlaps_horizontally(box, figure_box) + and len(text) <= MAX_CAPTION_CHARS + and _CAPTION_RE.match(text) + ): + return text + return "" + + +def find_figures(page, repeated: set[bytes]) -> list[Figure]: + page_box = tuple(float(value) for value in page.rect) + boxes = [] + for info in page.get_image_info(hashes=True): + if info["digest"] in repeated: + continue + box = tuple(float(value) for value in (page.rect & info["bbox"])) + if regions.area(box) and not regions.is_furniture(box, page_box): + boxes.append(box) + figure_boxes = sorted( + (box for box in merge_touching(boxes) if is_figure_sized(box, page_box)), + key=lambda box: (box[1], box[0]), + ) -import fitz # PyMuPDF -from frappe.utils.file_manager import save_file - -# Below this pixel area an "image" is almost always a rule, divider, bullet, icon, or -# logo — not a figure worth embedding. -_MIN_AREA = 200_000 - -# A page whose extractable text is below this is "sparse" — when it also carries a big -# image, the content lives in the image (a flowchart/diagram pymupdf dropped), so embed it -# even without a placeholder. Text-dense pages never trip this, so we don't re-embed page -# scans whose text the parser already recovered. -_SPARSE_TEXT = 700 - -_OMIT_RE = re.compile(r"\*{0,2}==>\s*picture \[(\d+) x (\d+)\] intentionally omitted <==\*{0,2}") -_PICTURE_TEXT = "Start of picture text" - - -def _is_text_image(markdown: str, match_end: int) -> bool: - """A "picture text" marker right after the placeholder means pymupdf recovered the - image's text — keep the text (cleanup handles it), don't embed the image.""" - return _PICTURE_TEXT in markdown[match_end : match_end + 200] - - -def _save_png(page, xref: int, source_document: str, page_no: int, idx: int) -> str | None: - """Extract one embedded image to PNG and host it as a private File on the document.""" - try: - pix = fitz.Pixmap(page.parent, xref) - if pix.n - pix.alpha >= 4: # CMYK / DeviceN → RGB so PNG encodes - pix = fitz.Pixmap(fitz.csRGB, pix) - png = pix.tobytes("png") - except Exception: - return None - file_doc = save_file( - f"page-{page_no:04d}-fig-{idx + 1}.png", - png, - "Source Document", - source_document, - is_private=1, + blocks = body_text_blocks(page, page_box) + figures = [] + for figure_box in figure_boxes: + outside = [block for block in blocks if regions.overlap_fraction(block[0], [figure_box]) < 0.5] + above = [block for block in outside if block[0][3] <= figure_box[1] + EDGE_TOLERANCE] + below = [block for block in outside if block[0][1] >= figure_box[3] - EDGE_TOLERANCE] + figures.append( + Figure( + bbox=figure_box, + page_size=(page.rect.width, page.rect.height), + caption=find_caption(outside, figure_box), + text_before=nearest_block(above, figure_box, key=lambda block: -block[0][3]), + text_after=nearest_block(below, figure_box, key=lambda block: block[0][1]), + ) + ) + return figures + + +def crop_figure(page, figure: Figure) -> bytes: + box = figure.bbox + clip = page.rect & ( + box[0] - CROP_PADDING, + box[1] - CROP_PADDING, + box[2] + CROP_PADDING, + box[3] + CROP_PADDING, + ) + return pdf_utils.render_png(page, dpi=FIGURE_DPI, clip=clip) + + +def page_figures(page, repeated: set[bytes], page_name: str, page_no: int) -> list[Figure]: + figures = find_figures(page, repeated) + for figure in figures: + figure.url = store.save_crop_file(page_name, page_no, crop_figure(page, figure)).file_url + return figures + + +def figure_hint(figures: list[Figure]) -> str: + if not figures: + return "" + positions = "\n".join( + f"- Figure {number}: {figure.bbox[1] / figure.page_size[1]:.0%} to " + f"{figure.bbox[3] / figure.page_size[1]:.0%} down the page, " + f"{figure.bbox[0] / figure.page_size[0]:.0%} to {figure.bbox[2] / figure.page_size[0]:.0%} across" + for number, figure in enumerate(figures, start=1) ) - return file_doc.file_url - - -def _big_images(page) -> list[int]: - """Substantial embedded image xrefs, de-duplicated, in page order.""" - imgs, seen = [], set() - for im in page.get_images(full=True): - xref = im[0] - if xref not in seen and im[2] * im[3] >= _MIN_AREA: - seen.add(xref) - imgs.append(xref) - return imgs - - -def embed_genuine_images(page, markdown: str, source_document: str, page_no: int, kind: str = "text") -> str: - """Embed genuine graphics the baseline parser would otherwise drop. - - Two cases: - 1. An omitted-picture placeholder with no recovered text → replace it inline with the - matching page image (precise placement). - 2. No placeholder, but a sparse-text page carrying a big image → a flowchart/diagram - pymupdf dropped silently; append it as a figure. - Returns the markdown unchanged on the common page (text-dense, or no substantial image, - or only text-image placeholders). Visual pages are left to the VLM re-parse. - """ - imgs = _big_images(page) - if not imgs: - return markdown - - # Case 1: genuine-graphic placeholders (omitted, no "picture text"), matched in order. - genuine = [m for m in _OMIT_RE.finditer(markdown) if not _is_text_image(markdown, m.end())] - if genuine: - out, cursor = [], 0 - for idx, m in enumerate(genuine): - out.append(markdown[cursor : m.start()]) - url = _save_png(page, imgs[idx], source_document, page_no, idx) if idx < len(imgs) else None - out.append(f"![Figure {page_no}.{idx + 1}]({url})" if url else m.group(0)) - cursor = m.end() - out.append(markdown[cursor:]) - return "".join(out) - - # Case 2: silently-dropped figure on a sparse text page (the content is in the image). - if kind == "visual" or "![" in markdown or len(page.get_text("text")) >= _SPARSE_TEXT: - return markdown - url = _save_png(page, imgs[0], source_document, page_no, 0) - return f"{markdown.rstrip()}\n\n![Figure {page_no}.1]({url})\n" if url else markdown + return ( + f"\n\nFIGURES: this page has {len(figures)} picture(s) that are cropped from the PDF and shown " + f"as images, numbered top to bottom:\n{positions}\n" + "Mark where each picture sits in the reading order with its token on a line of its own, " + "followed by the picture's printed title when it has one: [[FIGURE 1]] or " + "[[FIGURE 1: Admissions per year]]. Use every token exactly once and never write Markdown " + "image tags.\n" + "- A chart, graph, plot, photo, map or illustration is fully shown by its image: write only " + "its token. Never turn its bars, lines, axis ticks, legend or data labels into a table, a list " + "or numbers — the page does not print those values, so anything you write would be invented.\n" + "- A picture that is mostly text (a flowchart, form, notice, or a table printed as an image): " + "write its token, then transcribe its text below the token following the rules above.\n" + "- Text printed outside the pictures is transcribed as usual." + ) + + +def figure_tag(figure: Figure, number: int, page_no: int, title: str = "") -> str: + alt = " ".join((figure.caption or title or f"Figure {page_no}.{number}").split()) + return f"![{alt.replace('[', '(').replace(']', ')')}]({figure.url})" + + +def searchable(markdown: str) -> tuple[str, list[int]]: + skipped = [match.span() for match in _SKIPPED_MARKUP_RE.finditer(markdown)] + characters, positions = [], [] + span_index = 0 + for index, character in enumerate(markdown): + while span_index < len(skipped) and skipped[span_index][1] <= index: + span_index += 1 + if span_index < len(skipped) and skipped[span_index][0] <= index: + continue + if character.isalnum(): + characters.append(character.lower()) + positions.append(index) + return "".join(characters), positions + + +def normalized(text: str) -> str: + return "".join(character.lower() for character in text if character.isalnum()) + + +def enclosing_block(markdown: str, index: int) -> tuple[int, int] | None: + fences = [match.start() for match in _FENCE_RE.finditer(markdown)] + for opening, closing in zip(fences[::2], fences[1::2], strict=False): + if opening <= index <= closing: + line_end = markdown.find("\n", closing) + return opening, len(markdown) if line_end == -1 else line_end + table_start = markdown.rfind("", 0, index) < table_start: + table_end = markdown.find("", index) + if table_end != -1: + return table_start, table_end + len("") + return None + + +def line_bounds(markdown: str, index: int) -> tuple[int, int]: + start = markdown.rfind("\n", 0, index) + 1 + end = markdown.find("\n", index) + end = len(markdown) if end == -1 else end + block = enclosing_block(markdown, index) + if block: + return markdown.rfind("\n", 0, block[0]) + 1, block[1] + if markdown[start:end].lstrip().startswith("|"): + while start > 0 and markdown[markdown.rfind("\n", 0, start - 1) + 1 : start].lstrip().startswith("|"): + start = markdown.rfind("\n", 0, start - 1) + 1 + while end < len(markdown): + next_end = markdown.find("\n", end + 1) + next_end = len(markdown) if next_end == -1 else next_end + if not markdown[end + 1 : next_end].lstrip().startswith("|"): + break + end = next_end + return start, end + + +def anchor_position(markdown: str, figure: Figure) -> int | None: + plain, positions = searchable(markdown) + before, after = normalized(figure.text_before), normalized(figure.text_after) + for length in ANCHOR_LENGTHS: + tail, head = before[-length:], after[:length] + if len(tail) >= MIN_ANCHOR_CHARS and (found := plain.find(tail)) != -1: + return line_bounds(markdown, positions[found + len(tail) - 1])[1] + if len(head) >= MIN_ANCHOR_CHARS and (found := plain.find(head)) != -1: + return line_bounds(markdown, positions[found])[0] + return None + + +def insert_at(markdown: str, position: int, tag: str) -> str: + return f"{markdown[:position].rstrip()}\n\n{tag}\n\n{markdown[position:].lstrip()}" + + +def place_figures(markdown: str, figures: list[Figure], page_no: int) -> str: + if not figures: + return FIGURE_TOKEN_RE.sub("", markdown or "") + placed: set[int] = set() + + def swap_token(match: re.Match) -> str: + number = int(match.group(1)) + if not 1 <= number <= len(figures) or number in placed or figures[number - 1].url in markdown: + return "" + placed.add(number) + return figure_tag(figures[number - 1], number, page_no, match.group(2) or "") + + text = FIGURE_TOKEN_RE.sub(swap_token, markdown or "") + for number, figure in enumerate(figures, start=1): + if number in placed or not figure.url or figure.url in text: + continue + tag = figure_tag(figure, number, page_no) + position = anchor_position(text, figure) + if position is None: + position = 0 if figure.bbox[1] < figure.page_size[1] / 2 else len(text) + text = insert_at(text, position, tag) + return re.sub(r"\n{3,}", "\n\n", text).strip() diff --git a/wikify/engine/store.py b/wikify/engine/store.py index 36b28c2..5e55ee0 100644 --- a/wikify/engine/store.py +++ b/wikify/engine/store.py @@ -1,7 +1,7 @@ from __future__ import annotations import frappe -from frappe.utils.file_manager import save_file +from frappe.utils.file_manager import get_content_hash, save_file from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_random from wikify.engine.verify.harness import get_verdict @@ -51,6 +51,10 @@ def add_page( return page.name +def set_baseline_markdown(page_name: str, markdown: str) -> None: + frappe.db.set_value("Source Page", page_name, "baseline_markdown", markdown) + + def set_page_count(source_document: str, count: int) -> None: frappe.db.set_value("Source Document", source_document, "page_count", count) @@ -166,6 +170,17 @@ def get_page(source_document: str, page_no: int) -> dict | None: def save_crop_file(page_name: str, page_no: int, png_bytes: bytes): + existing = frappe.db.get_value( + "File", + { + "attached_to_doctype": "Source Page", + "attached_to_name": page_name, + "content_hash": get_content_hash(png_bytes), + }, + "name", + ) + if existing: + return frappe.get_doc("File", existing) return save_file(f"page-{page_no:04d}-crop.png", png_bytes, "Source Page", page_name, is_private=1) diff --git a/wikify/tests/test_images.py b/wikify/tests/test_images.py new file mode 100644 index 0000000..e196fc8 --- /dev/null +++ b/wikify/tests/test_images.py @@ -0,0 +1,116 @@ +# Copyright (c) 2026, BWH and contributors +# For license information, please see license.txt + +import tempfile +from pathlib import Path + +import fitz +from frappe.tests.utils import FrappeTestCase + +from wikify.engine import images + +CHART_RECT = fitz.Rect(100, 300, 400, 500) +TEXT_BEFORE = "Admissions rose steadily over the decade." +TEXT_AFTER = "Discharge planning starts on the first day." + + +def solid_pixmap(width: int, height: int, colour: tuple[int, int, int]) -> fitz.Pixmap: + pixmap = fitz.Pixmap(fitz.csRGB, fitz.IRect(0, 0, width, height), False) + pixmap.set_rect(pixmap.irect, colour) + return pixmap + + +def make_report_pdf(path: str) -> None: + logo = solid_pixmap(80, 80, (20, 40, 160)) + chart = solid_pixmap(600, 400, (200, 60, 30)) + document = fitz.open() + for page_no in range(1, 4): + page = document.new_page(width=612, height=792) + page.insert_image(fitz.Rect(72, 100, 152, 180), pixmap=logo) + page.insert_text((72, 230), f"Annual report section {page_no}", fontsize=14) + if page_no == 2: + page.insert_text((100, 280), TEXT_BEFORE, fontsize=11) + page.insert_image(CHART_RECT, pixmap=chart) + page.insert_text((100, 540), TEXT_AFTER, fontsize=11) + else: + page.insert_text((72, 280), "Plain text page with no pictures at all.", fontsize=11) + document.save(path) + document.close() + + +def chart_figure(**values) -> images.Figure: + return images.Figure( + bbox=tuple(CHART_RECT), + page_size=(612.0, 792.0), + text_before=TEXT_BEFORE, + text_after=TEXT_AFTER, + url="/private/files/chart.png", + **values, + ) + + +class TestFigureDetection(FrappeTestCase): + def test_finds_the_body_chart_and_skips_the_logo_repeated_on_every_page(self): + path = Path(tempfile.mkdtemp()) / "report.pdf" + make_report_pdf(str(path)) + with fitz.open(str(path)) as document: + repeated = images.repeated_images(document) + found = [images.find_figures(page, repeated) for page in document] + + self.assertEqual([len(figures) for figures in found], [0, 1, 0]) + chart = found[1][0] + self.assertEqual(fitz.Rect(chart.bbox), CHART_RECT) + self.assertEqual(chart.text_before, TEXT_BEFORE) + self.assertEqual(chart.text_after, TEXT_AFTER) + + +class TestFigurePlacement(FrappeTestCase): + def test_places_the_figure_after_the_text_that_precedes_it(self): + markdown = f"# Annual report\n\n{TEXT_BEFORE}\n\n## Discharge\n\n{TEXT_AFTER}" + + placed = images.place_figures(markdown, [chart_figure()], 2) + + self.assertEqual( + placed, + f"# Annual report\n\n{TEXT_BEFORE}\n\n![Figure 2.1](/private/files/chart.png)\n\n" + f"## Discharge\n\n{TEXT_AFTER}", + ) + + def test_a_figure_after_a_table_row_lands_below_the_whole_table(self): + markdown = f"\n\n\n
{TEXT_BEFORE}
Second row
\n\nClosing note." + + placed = images.place_figures(markdown, [chart_figure()], 2) + + self.assertTrue(placed.index("![Figure 2.1]") > placed.index("")) + self.assertTrue(placed.index("![Figure 2.1]") < placed.index("Closing note.")) + + def test_swaps_the_vlm_token_for_the_crop_titled_by_the_vlm(self): + markdown = f"{TEXT_BEFORE}\n\n[[FIGURE 1: Admissions per year]]\n\n[[FIGURE 2]]\n\n{TEXT_AFTER}" + + placed = images.place_figures(markdown, [chart_figure()], 2) + + self.assertEqual( + placed, f"{TEXT_BEFORE}\n\n![Admissions per year](/private/files/chart.png)\n\n{TEXT_AFTER}" + ) + + def test_a_printed_caption_wins_over_the_vlm_title(self): + placed = images.place_figures( + "[[FIGURE 1: Bar chart]]", [chart_figure(caption="Figure 3: Admissions")], 2 + ) + + self.assertEqual(placed, "![Figure 3: Admissions](/private/files/chart.png)") + + def test_inserts_a_figure_the_vlm_left_out_after_its_preceding_text(self): + markdown = f"## Admissions\n\n{TEXT_BEFORE}\n\n{TEXT_AFTER}" + + placed = images.place_figures(markdown, [chart_figure()], 2) + + self.assertEqual( + placed, + f"## Admissions\n\n{TEXT_BEFORE}\n\n![Figure 2.1](/private/files/chart.png)\n\n{TEXT_AFTER}", + ) + + def test_a_figure_already_in_the_markdown_is_not_added_twice(self): + markdown = f"{TEXT_BEFORE}\n\n![Admissions per year](/private/files/chart.png)\n\n{TEXT_AFTER}" + + self.assertEqual(images.place_figures(markdown, [chart_figure()], 2), markdown) From 25e05c63129c1f3bdc264d815ec6771d3b05eb5f Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 03:17:21 +0530 Subject: [PATCH 10/33] fix(remediate): keep figures as images instead of transcribing chart values --- wikify/engine/parsers/vlm.py | 3 +- wikify/engine/remediate.py | 13 ++++++-- wikify/engine/reparse.py | 10 ++++-- wikify/tests/test_images.py | 65 +++++++++++++++++++++++++++++++++++- 4 files changed, 85 insertions(+), 6 deletions(-) diff --git a/wikify/engine/parsers/vlm.py b/wikify/engine/parsers/vlm.py index eac3a20..4c97651 100644 --- a/wikify/engine/parsers/vlm.py +++ b/wikify/engine/parsers/vlm.py @@ -41,6 +41,7 @@ def parse_page_image( project_context: str = "", instruction: str = "", shape_hint: str = "", + figure_hint: str = "", ) -> str: preamble = context_block(project_context) + instruction_block(instruction) + shape_hint resp = llm.chat_completion( @@ -49,7 +50,7 @@ def parse_page_image( { "role": "user", "content": [ - {"type": "text", "text": preamble + _PROMPT}, + {"type": "text", "text": preamble + _PROMPT + figure_hint}, {"type": "image_url", "image_url": {"url": image_data_url}}, ], } diff --git a/wikify/engine/remediate.py b/wikify/engine/remediate.py index 9059306..88d3d62 100644 --- a/wikify/engine/remediate.py +++ b/wikify/engine/remediate.py @@ -5,7 +5,7 @@ import fitz -from wikify.engine import diagrams, llm, pdf_utils, regions, settings, store +from wikify.engine import diagrams, images, llm, pdf_utils, regions, settings, store from wikify.engine.loader.cleanup_llm import clean_markdown from wikify.engine.loader.table_stitch import stitch_cross_page_tables from wikify.engine.parsers import vlm @@ -93,6 +93,7 @@ def remediate_pdf( # noqa: C901 with fitz.open(pdf_path) as doc: furniture = det.find_furniture_lines([doc[p["page_no"] - 1].get_text("text") for p in pages]) + repeated = images.repeated_images(doc) doc_cost = 0.0 for i, p in enumerate(targets): @@ -102,6 +103,7 @@ def remediate_pdf( # noqa: C901 page_image = p["image"] or "" data_url = pdf_utils.png_to_data_url(pdf_utils.render_png(page, dpi=dpi)) page_regions = regions.find_regions(page) + figures = images.page_figures(page, repeated, p["name"], p["page_no"]) use_judge = judge_all or kind == "visual" img = data_url if use_judge else None @@ -122,7 +124,9 @@ def remediate_pdf( # noqa: C901 project_context=project_context, instruction=instruction, shape_hint=regions.shape_hint(page_regions), + figure_hint=images.figure_hint(figures), ) + vlm_md = images.place_figures(vlm_md, figures, p["page_no"]) vlm_md, diagram_notes = diagrams.remove_unverified_diagrams(vlm_md, page_image) errors.extend(diagram_notes) vlm_ps = score_page( @@ -134,7 +138,11 @@ def remediate_pdf( # noqa: C901 if kind != "visual": try: clean_md, clean_notes = diagrams.remove_unverified_diagrams( - clean_markdown(base_md, project_context=project_context, instruction=instruction), + images.place_figures( + clean_markdown(base_md, project_context=project_context, instruction=instruction), + figures, + p["page_no"], + ), page_image, ) errors.extend(clean_notes) @@ -175,6 +183,7 @@ def remediate_pdf( # noqa: C901 canon_src[p["page_no"]] = method else: canon_md[p["page_no"]] = base_md + canon_md[p["page_no"]] = images.place_figures(canon_md[p["page_no"]], figures, p["page_no"]) canon_md[p["page_no"]] = repair_broken_image_tags(canon_md[p["page_no"]], page_image) canon_md[p["page_no"]] = with_page_crop(canon_md[p["page_no"]], page_image) diff --git a/wikify/engine/reparse.py b/wikify/engine/reparse.py index 4b80f51..6b6c430 100644 --- a/wikify/engine/reparse.py +++ b/wikify/engine/reparse.py @@ -2,7 +2,7 @@ import fitz -from wikify.engine import diagrams, llm, pdf_utils, regions, remediate, settings, store +from wikify.engine import diagrams, images, llm, pdf_utils, regions, remediate, settings, store from wikify.engine.loader.cleanup_llm import clean_markdown from wikify.engine.parsers import vlm from wikify.engine.tags import find_tag_spans @@ -36,6 +36,7 @@ def reparse_page( fpage = doc[page_no - 1] gt = fpage.get_text("text") shape_hint = regions.shape_hint(regions.find_regions(fpage)) + figures = images.page_figures(fpage, images.repeated_images(doc), page["name"], page_no) data_url = pdf_utils.png_to_data_url(pdf_utils.render_png(fpage, dpi=dpi)) kind = page["kind"] @@ -47,11 +48,16 @@ def reparse_page( llm.reset_metrics() new_md = ( vlm.parse_page_image( - data_url, project_context=project_context, instruction=instruction, shape_hint=shape_hint + data_url, + project_context=project_context, + instruction=instruction, + shape_hint=shape_hint, + figure_hint=images.figure_hint(figures), ) if method == "vlm" else clean_markdown(base_md, project_context=project_context, instruction=instruction) ) + new_md = images.place_figures(new_md, figures, page_no) page_image = store.get_page_image(page["name"]) or "" new_md, diagram_notes = diagrams.remove_unverified_diagrams(new_md, page_image) new_md = remediate.repair_broken_image_tags(new_md, page_image) diff --git a/wikify/tests/test_images.py b/wikify/tests/test_images.py index e196fc8..68acd3a 100644 --- a/wikify/tests/test_images.py +++ b/wikify/tests/test_images.py @@ -3,11 +3,16 @@ import tempfile from pathlib import Path +from unittest.mock import patch import fitz +import frappe from frappe.tests.utils import FrappeTestCase -from wikify.engine import images +from wikify.engine import images, parse_pdf, remediate_pdf +from wikify.engine import settings as engine_settings +from wikify.tests import _cleanup +from wikify.tests.test_remediate_pipeline import _fake_chat CHART_RECT = fitz.Rect(100, 300, 400, 500) TEXT_BEFORE = "Admissions rose steadily over the decade." @@ -114,3 +119,61 @@ def test_a_figure_already_in_the_markdown_is_not_added_twice(self): markdown = f"{TEXT_BEFORE}\n\n![Admissions per year](/private/files/chart.png)\n\n{TEXT_AFTER}" self.assertEqual(images.place_figures(markdown, [chart_figure()], 2), markdown) + + +class TestFigureRemediation(FrappeTestCase): + def setUp(self): + real_get = engine_settings.get + patcher = patch( + "wikify.engine.settings.get", + side_effect=lambda field: 0 if field == "judge_all_pages" else real_get(field), + ) + patcher.start() + self.addCleanup(patcher.stop) + + def test_remediation_keeps_the_chart_as_its_crop_instead_of_a_transcription(self): + path = Path(tempfile.mkdtemp()) / "report.pdf" + make_report_pdf(str(path)) + vlm_markdown = f"{TEXT_BEFORE}\n\n[[FIGURE 1: Admissions per year]]\n\n{TEXT_AFTER}" + with ( + patch("wikify.engine.llm.has_openrouter", return_value=True), + patch("wikify.engine.llm.chat_completion", side_effect=_fake_chat), + ): + source_document = parse_pdf(str(path), title="Figure Remediation Test") + self.addCleanup(_cleanup.delete_document, source_document) + with ( + patch("wikify.engine.remediate.clean_markdown", side_effect=RuntimeError("offline")), + patch( + "wikify.engine.remediate.vlm.parse_page_image", return_value=vlm_markdown + ) as parse_page_image, + ): + remediate_pdf(source_document, str(path), scope="all") + + hints = [call.kwargs["figure_hint"] for call in parse_page_image.call_args_list] + self.assertEqual([bool(hint) for hint in hints], [False, True, False]) + self.assertIn("this page has 1 picture(s)", hints[1]) + self.assertIn("[[FIGURE 1]]", hints[1]) + self.assertIn("Never turn its bars, lines, axis ticks, legend or data labels into a table", hints[1]) + + page = frappe.db.get_value( + "Source Page", + {"source_document": source_document, "page_no": 2}, + ["name", "image", "remediation_method", "remediation_markdown", "canonical_markdown"], + as_dict=True, + ) + crop_urls = frappe.get_all( + "File", + filters={ + "attached_to_doctype": "Source Page", + "attached_to_name": page.name, + "file_url": ["!=", page.image], + }, + pluck="file_url", + ) + self.assertEqual(len(crop_urls), 1) + self.assertEqual(page.remediation_method, "vlm") + self.assertEqual( + page.remediation_markdown, + f"{TEXT_BEFORE}\n\n![Admissions per year]({crop_urls[0]})\n\n{TEXT_AFTER}", + ) + self.assertEqual(page.canonical_markdown.count(crop_urls[0]), 1) From e58367eaaca7ff1bbb3aa6a4e7e136773aa0c732 Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 05:53:21 +0530 Subject: [PATCH 11/33] fix(cleanup): strip page headers only at page edges and stitch table rows split by a page break --- wikify/engine/loader/cleanup.py | 86 ++++++++--- wikify/engine/loader/sectionizer.py | 8 +- wikify/engine/loader/table_stitch.py | 210 ++++++++++++++++++++++++++- wikify/tests/test_sectionize.py | 92 ++++++++++++ 4 files changed, 371 insertions(+), 25 deletions(-) diff --git a/wikify/engine/loader/cleanup.py b/wikify/engine/loader/cleanup.py index dae780c..1cbbace 100644 --- a/wikify/engine/loader/cleanup.py +++ b/wikify/engine/loader/cleanup.py @@ -1,9 +1,9 @@ """Strip repeated page headers/footers before sectionizing. Real manuals repeat a running header/footer on every page (doc title, doc code, -"Pg X of Y", version/date). Left in, each becomes a fake heading. We remove lines -that recur across many pages, plus a few varying-boilerplate patterns (page -numbers, doc codes) that won't match exactly page-to-page. +"Pg X of Y", version/date). Left in, each becomes a fake heading. We remove the run of +lines at each page edge that recur at the edges of many pages, plus a few +varying-boilerplate patterns (page numbers, doc codes) that won't match exactly page-to-page. Ported verbatim from the POC `loader/cleanup.py` (pure markdown, no I/O). Wired into the pipeline at sectionize time (Slice 4); shipped here per the Slice 3 cleanup port. @@ -15,15 +15,27 @@ from collections import Counter from itertools import groupby -_NORM = re.compile(r"[#*_`>\-\s]+") -_VARYING = [ +from wikify.engine.loader.table_stitch import ( + html_tables_to_markdown, + merge_continuation_rows, + stitch_cross_page_tables, +) + +_NORM = re.compile(r"[#*_`>|\-\s]+") +_MARKUP = re.compile(r"(?:|[*_`|]|^[\s>#]+)") +_PAGE_OF = [ re.compile(r"(?i)\bpg\.?\s*\d+\s*of\s*\d+"), re.compile(r"(?i)\bpage\s*\d+\s*of\s*\d+"), +] +_HEADER_FIELDS = [ re.compile(r"(?i)^man/[a-z0-9/]+"), re.compile(r"(?i)\bver\.?\s*:"), re.compile(r"(?i)\bissue\s*:\s*\d"), re.compile(r"(?i)^\s*date\s*:"), ] +_STRUCTURAL_LINE = re.compile(r"^\s*(?:<|```|~~~|!\[)") +_IMAGE_LINE = re.compile(r"^\s*!\[[^\]]*\]\([^)]*\)\s*$") +EDGE_LINES = 4 # Approval / sign-off footer block — QMS-manual page furniture rendered as a one- or # two-row Markdown table, e.g. `|Prepared by - Dr X|Issued by: QMC|Approved by - Dr Y|`. @@ -43,8 +55,13 @@ _NUMBERED_HEADING = re.compile(r"^\s*#{1,6}\s+[*_]*\d+(?:\.\d+)*\.?\s*[A-Za-z]") +def _plain(line: str) -> str: + """The line's visible text: emphasis, table pipes,
, blockquote and heading markers removed.""" + return " ".join(_MARKUP.sub(" ", line).split()) + + def _norm(line: str) -> str: - return _NORM.sub(" ", line).strip().lower() + return _NORM.sub(" ", _plain(line)).strip().lower() def _is_signoff_footer_row(line: str) -> bool: @@ -95,35 +112,64 @@ def _strip_page_residue(md: str) -> str: ] while kept and not kept[-1].strip(): kept.pop() - if kept and _PAGE_NUMBER.match(kept[-1]): + while kept and not kept[0].strip(): + kept.pop(0) + if kept and _PAGE_NUMBER.match(_plain(kept[-1])): kept.pop() return "\n".join(kept) +def _edge_lines(md: str) -> list[str]: + content = [line for line in md.splitlines() if _norm(line) and not _STRUCTURAL_LINE.match(line)] + return content[:EDGE_LINES] + content[-EDGE_LINES:] + + def find_boilerplate(pages: list[tuple[int, str]]) -> set[str]: - """Normalized lines that recur on a large fraction of pages.""" + """Normalized lines that recur at the top or bottom edge of a large fraction of pages.""" counts: Counter[str] = Counter() for _, md in pages: - for nl in {_norm(line) for line in md.splitlines() if _norm(line)}: + for nl in {_norm(line) for line in _edge_lines(md)}: counts[nl] += 1 threshold = max(3, int(0.30 * len(pages))) return {line for line, c in counts.items() if c >= threshold and len(line) <= 90} -def _is_varying(line: str) -> bool: - return any(p.search(line) for p in _VARYING) +def _is_page_of(line: str) -> bool: + plain = _plain(line) + return any(pattern.search(plain) for pattern in _PAGE_OF) + + +def _is_page_furniture(line: str, boilerplate: set[str], at_top: bool) -> bool: + plain = _plain(line) + return ( + (_norm(line) in boilerplate and not _NUMBERED_HEADING.match(line)) + or any(pattern.search(plain) for pattern in _HEADER_FIELDS) + or bool(_PAGE_NUMBER.match(plain)) + or (at_top and "-" in line and bool(_SEP_ONLY.match(line))) + ) + + +def _edge_furniture(lines: list[str], boilerplate: set[str]) -> set[int]: + """The unbroken header/footer run at each page edge; a label that also recurs mid-page survives.""" + drop: set[int] = set() + for at_top, indexes in ((True, range(len(lines))), (False, range(len(lines) - 1, -1, -1))): + for index in indexes: + line = lines[index] + if not line.strip() or _IMAGE_LINE.match(line): + continue + if not _is_page_furniture(line, boilerplate, at_top): + break + drop.add(index) + return drop def strip_boilerplate(pages: list[tuple[int, str]], boilerplate: set[str]) -> list[tuple[int, str]]: out: list[tuple[int, str]] = [] for pno, md in pages: - kept = [ - line - for line in md.splitlines() - if not (_norm(line) and _norm(line) in boilerplate and not _NUMBERED_HEADING.match(line)) - and not _is_varying(line) - ] - out.append((pno, _strip_page_residue(_strip_footer_blocks("\n".join(kept))))) + lines = [line for line in _strip_footer_blocks(md).splitlines() if not _is_page_of(line)] + drop = _edge_furniture(lines, boilerplate) + kept = [line for index, line in enumerate(lines) if index not in drop] + out.append((pno, _strip_page_residue("\n".join(kept)))) return out @@ -153,7 +199,9 @@ def join_page_breaks(pages: list[tuple[int, str]]) -> list[tuple[int, str]]: def clean_pages(pages: list[tuple[int, str]]) -> list[tuple[int, str]]: - return join_page_breaks(strip_boilerplate(pages, find_boilerplate(pages))) + pages = [(page_no, html_tables_to_markdown(md)) for page_no, md in pages] + stitched = stitch_cross_page_tables(strip_boilerplate(pages, find_boilerplate(pages))) + return join_page_breaks([(page_no, merge_continuation_rows(md)) for page_no, md in stitched]) def strip_outer_markdown_fence(text: str) -> str: diff --git a/wikify/engine/loader/sectionizer.py b/wikify/engine/loader/sectionizer.py index 455d3ed..cc1e072 100644 --- a/wikify/engine/loader/sectionizer.py +++ b/wikify/engine/loader/sectionizer.py @@ -31,7 +31,9 @@ def _clean_title(raw: str) -> str: - title = _GLUED_NUM.sub(r"\1 ", _LINK.sub(r"\1", raw.strip().strip("*_").strip()).strip()) + title = _GLUED_NUM.sub( + r"\1 ", _LINK.sub(r"\1", raw.replace("**", "").strip().strip("*_").strip()).strip() + ) if title.endswith(".") and not title.endswith(".."): title = title[:-1].rstrip() if len(title) <= MAX_TITLE_LENGTH: @@ -179,6 +181,10 @@ def flush(): sections.append(current) for page_no, md in pages: + # Without a blank line, GFM reads the next page's first line as another row of a table, more + # raw HTML, or a lazy continuation of a list item that ended the previous page. + if buf and buf[-1].strip(): + buf.append("") lines = md.splitlines() toc_end = toc_end_line(lines) for line_index, line in enumerate(lines): diff --git a/wikify/engine/loader/table_stitch.py b/wikify/engine/loader/table_stitch.py index 89aa693..66aff8d 100644 --- a/wikify/engine/loader/table_stitch.py +++ b/wikify/engine/loader/table_stitch.py @@ -1,8 +1,9 @@ """Merge tables that pymupdf split across a page boundary. If page N ends with a Markdown table and page N+1 begins with one of the same -column count, the rows are joined into a single table (a repeated header on the -continuation page is dropped). Heuristic, but cheap and reversible. +column count, the rows are joined into a single table (a repeated or empty header on +the continuation page is dropped; any other header is kept as a row, since the parser +often promotes the page's first data row to a header). Heuristic, but cheap and reversible. Ported verbatim from the POC `loader/table_stitch.py` (no I/O — pure markdown). """ @@ -10,13 +11,207 @@ from __future__ import annotations import re +from html.parser import HTMLParser _ROW = re.compile(r"^\s*\|.*\|\s*$") _SEP = re.compile(r"^\s*\|[\s:|-]+\|\s*$") +_CELL_SPLIT = re.compile(r"(?]*>.*?", re.IGNORECASE | re.DOTALL) +_INLINE_MARKS = {"strong": "**", "b": "**", "em": "*", "i": "*"} +_KEPT_INLINE_TAGS = ("sup", "sub") +_WRAPPER_TAGS = ("thead", "tbody", "tfoot", "span", "p", "u") +MIN_NUMBERED_ROWS = 2 + + +class _TableReader(HTMLParser): + def __init__(self): + super().__init__(convert_charrefs=True) + self.rows: list[list[str]] = [] + self.header_flags: list[list[bool]] = [] + self.cell: list[str] | None = None + self.row_open = False + self.saw_row_tag = False + self.table_depth = 0 + self.convertible = True + + def handle_starttag(self, tag, attrs): + if tag == "table": + self.table_depth += 1 + self.convertible = self.convertible and self.table_depth == 1 + elif tag == "tr": + self.close_cell() + self.saw_row_tag = True + self.start_row() + elif tag in ("td", "th"): + if any(name in ("rowspan", "colspan") and (value or "1").strip() != "1" for name, value in attrs): + self.convertible = False + self.close_cell() + if not self.row_open: + self.start_row() + self.cell = [] + self.header_flags[-1].append(tag == "th") + elif tag == "br": + self.append("
") + elif tag in _INLINE_MARKS: + self.append(_INLINE_MARKS[tag]) + elif tag in _KEPT_INLINE_TAGS: + self.append(f"<{tag}>") + elif tag not in _WRAPPER_TAGS: + self.convertible = False + + def handle_endtag(self, tag): + if tag in ("td", "th"): + self.close_cell() + elif tag == "tr": + self.close_cell() + self.row_open = False + elif tag in _INLINE_MARKS: + self.append(_INLINE_MARKS[tag]) + elif tag in _KEPT_INLINE_TAGS: + self.append(f"") + + def handle_data(self, data): + if self.cell is not None: + self.cell.append(data.replace("<", "<")) + elif data.strip(): + self.convertible = False + + def append(self, text: str): + if self.cell is not None: + self.cell.append(text) + + def start_row(self): + self.rows.append([]) + self.header_flags.append([]) + self.row_open = True + + def close_cell(self): + if self.cell is None: + return + text = " ".join("".join(self.cell).split()) + self.rows[-1].append(text.replace("|", "\\|")) + self.cell = None + + +def _format_row(cells: list[str]) -> str: + return "| " + " | ".join(cells) + " |" + + +def _html_table_to_markdown(html: str) -> str | None: + reader = _TableReader() + reader.feed(html) + reader.close() + reader.close_cell() + if not reader.convertible: + return None + rows, header_flags = reader.rows, reader.header_flags + if not reader.saw_row_tag: + columns = sum(header_flags[0]) if header_flags else 0 + if not columns: + return None + cells = rows[0] + rows = [cells[start : start + columns] for start in range(0, len(cells), columns)] + header_flags = [[index < columns for index in range(len(row))] for row in rows] + rows_with_flags = [(row, flags) for row, flags in zip(rows, header_flags, strict=True) if row] + if not rows_with_flags: + return None + columns = max(len(row) for row, _ in rows_with_flags) + padded = [row + [""] * (columns - len(row)) for row, _ in rows_with_flags] + if all(rows_with_flags[0][1]): + header, body = padded[0], padded[1:] + else: + header, body = [""] * columns, padded + return "\n".join([_format_row(header), "|" + "---|" * columns, *(_format_row(row) for row in body)]) + + +def html_tables_to_markdown(md: str) -> str: + """Rewrite flat HTML tables (no merged cells, inline content only) as pipe tables, repairing + unclosed cells/rows on the way, so they can be stitched and cleaned like any other table.""" + + def replace(match: re.Match) -> str: + converted = _html_table_to_markdown(match.group(0)) + if not converted: + return match.group(0) + before, after = md[: match.start()], md[match.end() :] + prefix = "" if not before or before.endswith("\n\n") else "\n" * (1 + (not before.endswith("\n"))) + suffix = "" if not after or after.startswith("\n\n") else "\n" * (1 + (not after.startswith("\n"))) + return f"{prefix}{converted}{suffix}" + + return _HTML_TABLE.sub(replace, md) + + +def _cells(row: str) -> list[str]: + text = row.strip() + text = text[1:] if text.startswith("|") else text + text = text[:-1] if text.endswith("|") and not text.endswith("\\|") else text + return [cell.strip() for cell in _CELL_SPLIT.split(text)] def _ncols(row: str) -> int: - return len(row.strip().strip("|").split("|")) + return len(_cells(row)) + + +def _plain_cell(cell: str) -> str: + return _EMPHASIS.sub("", cell).strip() + + +def _header_key(row: str) -> str: + return " ".join(_plain_cell(cell).lower() for cell in _cells(row)).strip() + + +def _is_numbered(rows: list[str]) -> bool: + return sum(1 for row in rows if _STEP.match(_plain_cell(_cells(row)[0]))) >= MIN_NUMBERED_ROWS + + +def _continues(row: str, after_page_break: bool) -> bool: + """A row that carries on the previous step instead of starting one: no step number and, in the + first non-empty cell, a lowercase start (or, right after a page break, any non-capital start).""" + cells = [_plain_cell(cell) for cell in _cells(row)] + if _STEP.match(cells[0]): + return False + if after_page_break: + text = next((cell for cell in cells if cell), "") + return bool(text) and not text[0].isupper() + return bool(cells[0]) and cells[0][0].islower() + + +def _merge_rows(previous: str, row: str) -> str: + merged = [ + " ".join(part for part in (first, second) if part) + for first, second in zip(_cells(previous), _cells(row), strict=False) + ] + return _format_row(merged) + + +def merge_continuation_rows(md: str) -> str: + """Fold a row a page break cut off (lowercase, unnumbered) back into the step it continues.""" + lines = md.split("\n") + out: list[str] = [] + index = 0 + while index < len(lines): + if not _ROW.match(lines[index]): + out.append(lines[index]) + index += 1 + continue + end = index + while end < len(lines) and _ROW.match(lines[end]): + end += 1 + block = lines[index:end] + body_start = 2 if len(block) > 1 and _SEP.match(block[1]) else 0 + rows = block[:body_start] + if _is_numbered(block[body_start:]): + for row in block[body_start:]: + if len(rows) > body_start and _ncols(row) == _ncols(rows[-1]) and _continues(row, False): + rows[-1] = _merge_rows(rows[-1], row) + else: + rows.append(row) + else: + rows = block + out.extend(rows) + index = end + return "\n".join(out) def _trailing_table(md: str): @@ -50,8 +245,13 @@ def stitch_cross_page_tables(pages: list[tuple[int, str]]) -> list[tuple[int, st b_tbl, b_after = b if not a_tbl or not b_tbl or _ncols(a_tbl[0]) != _ncols(b_tbl[0]): continue - # Drop a repeated header+separator at the start of the continuation table. - cont = b_tbl[2:] if len(b_tbl) >= 2 and _SEP.match(b_tbl[1]) else b_tbl + cont = b_tbl + if len(b_tbl) >= 2 and _SEP.match(b_tbl[1]): + repeated = _header_key(b_tbl[0]) in ("", _header_key(a_tbl[0])) + cont = b_tbl[2:] if repeated else [b_tbl[0], *b_tbl[2:]] + if cont and not _SEP.match(a_tbl[-1]) and _is_numbered(a_tbl) and _continues(cont[0], True): + a_tbl = [*a_tbl[:-1], _merge_rows(a_tbl[-1], cont[0])] + cont = cont[1:] out[k][1] = "\n".join(a_before + a_tbl + cont).strip() out[k + 1][1] = "\n".join(b_after).strip() return [(pno, md) for pno, md in out] diff --git a/wikify/tests/test_sectionize.py b/wikify/tests/test_sectionize.py index 623058b..5989f32 100644 --- a/wikify/tests/test_sectionize.py +++ b/wikify/tests/test_sectionize.py @@ -395,6 +395,98 @@ def test_clean_pages_strips_signoff_footer_written_as_text(self): self.assertIn("- more body", cleaned[2]) self.assertIn("approved by the HOD", cleaned[3]) + def test_clean_pages_keeps_html_tables_that_recur_on_every_page(self): + header = "PROCEDURE MANUAL - NEPHROLOGY\n\nMAN/RPT/NEPH/001 | Ver.: 1 | Pg. {page} of 398\n\n" + table = ( + "\n\n\n\n\n" + "\n\n\n\n" + "\n
Nursing ActionRationale
1. Check doctor's order.Gains cooperation
2. Administer inj. calcium gluconate.Prevents hypocalcemia\n" + "
3. Remove sutures.Avoids blood loss
" + ) + pages = [ + (page, header.format(page=page) + f"## Step {page}\n\n{table}\n\nNotes {page}.") + for page in (1, 2, 3) + ] + cleaned = dict(clean_pages(pages)) + self.assertIn( + "| Nursing Action | Rationale |\n|---|---|\n" + "| 1. Check doctor's order. | Gains cooperation |\n" + "| 2. Administer inj. calcium gluconate. | Prevents hypocalcemia |\n" + "| 3. Remove sutures. | Avoids blood loss |", + cleaned[2], + ) + + def test_clean_pages_stitches_a_table_row_split_by_a_page_break(self): + header = "PROCEDURE MANUAL - NEPHROLOGY\n\nMAN/RPT/NEPH/001 | Ver.: 1 | Pg. {page} of 398\n\n" + first = ( + "|**Nursing Action**|**Rationale**|\n|---|---|\n" + "|1. Inspect the previous dressing|Confirms the need.|\n" + "|2. Explain the procedure and
position him according to the|Helps gain co-operation.|\n" + "|**MAN/RPT/NEPH/001 Ver.:**|**1 Issue :1**
Pg.**2 of 398**|\n" + "|convenience to access the site.||\n" + "|3. Send blood for investigations|Electrolytes are removed during the process of|" + ) + second = ( + "\n\n\n\n\n" + "\n\n\n\n
(Ca, PO4, Na, K)plasmapheresis.
4. Check BP.Identifies complications.
\n\n" + "# **Specific Instructions:**\n\n- Avoid swimming." + ) + pages = [ + (1, header.format(page=1) + first), + (2, header.format(page=2) + second), + (3, header.format(page=3)), + ] + secs = sectionize(clean_pages(pages)) + self.assertEqual( + secs[0].markdown, + "|**Nursing Action**|**Rationale**|\n|---|---|\n" + "|1. Inspect the previous dressing|Confirms the need.|\n" + "| 2. Explain the procedure and
position him according to the convenience to access the site. " + "| Helps gain co-operation. |\n" + "| 3. Send blood for investigations (Ca, PO4, Na, K) " + "| Electrolytes are removed during the process of plasmapheresis. |\n" + "| 4. Check BP. | Identifies complications. |\n\n" + "**Specific Instructions:**\n\n- Avoid swimming.", + ) + + def test_clean_pages_strips_running_headers_wrapped_in_markup(self): + pages = [ + ( + 1, + "PROCEDURE MANUAL\n\n> **MAN/RPT/NEPH/001 Ver.: 1**\n\n> **Date:10/11/2023** Pg. **321 of 398**\n\n- First.", + ), + ( + 2, + "||PROCEDURE MANUAL||\n|---|---|---|\n|**MAN/RPT/NEPH/001**|
**Ver.: 1**|Pg.**322 of 398**|\n\n- Second.", + ), + ( + 3, + "PROCEDURE MANUAL\n\n**MAN/RPT/NEPH/001 Ver.: 1 Date:10/11/2023** Pg. **323 of 398**\n\n- Third.", + ), + ( + 4, + "PROCEDURE MANUAL **MAN/RPT/NEPH/001 Ver.: 1 Date:10/11/2023** Pg. **324 of 398**\n\n- Fourth.\n\n**324**", + ), + ] + cleaned = dict(clean_pages(pages)) + self.assertEqual(cleaned, {1: "- First.", 2: "- Second.", 3: "- Third.", 4: "- Fourth."}) + + def test_clean_pages_keeps_a_label_that_recurs_mid_page(self): + header = "PROCEDURE MANUAL\n\nMAN/RPT/NEPH/001 | Ver.: 1 | Pg. {page} of 398\n\n" + body = "## 6.4.{page} Step {page}\n\n**Definition**\n\nText {page}.\n\n## Procedure\n\n1. Do {page}.\n\nFooter note" + pages = [(page, header.format(page=page) + body.format(page=page)) for page in range(1, 6)] + cleaned = dict(clean_pages(pages)) + for page in range(1, 6): + self.assertEqual( + cleaned[page], + f"## 6.4.{page} Step {page}\n\n**Definition**\n\nText {page}.\n\n## Procedure\n\n1. Do {page}.", + ) + + def test_stray_bold_markers_are_stripped_from_titles(self): + secs = sectionize([(1, "# 3. **Other equipment:\n- Gloves\n# **CRRT Order sheet** :\n- Form")]) + self.assertEqual([s.title for s in secs], ["3. Other equipment:"]) + self.assertIn("**CRRT Order sheet :**", secs[0].markdown) + class TestEmptySectionsAreFlagged(FrappeTestCase): def test_section_without_markdown_is_chunked_as_title_only(self): From cb13d1cb1bb232718684810b8a4ff4ed3788b337 Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 05:53:21 +0530 Subject: [PATCH 12/33] fix(lint): escape form blanks and repair bold glued to its neighbours --- wikify/engine/lint.py | 82 +++++++++++++++++++++++++++++++++++++ wikify/engine/sectionize.py | 8 ++-- wikify/tests/test_lint.py | 48 +++++++++++++++++++++- 3 files changed, 133 insertions(+), 5 deletions(-) diff --git a/wikify/engine/lint.py b/wikify/engine/lint.py index 617546f..ec19725 100644 --- a/wikify/engine/lint.py +++ b/wikify/engine/lint.py @@ -132,3 +132,85 @@ def fix_table_separators(markdown: str) -> str: if i in insertions: out.append(insertions[i]) return "\n".join(out) + + +_BLANK_RUN_RE = re.compile(r"(? str: + """Escape form blanks (runs of 3+ underscores) so markdown can't read them as emphasis and eat + the text between two blanks. Idempotent; a line that is only underscores (a rule) is left alone.""" + lines = (markdown or "").split("\n") + for index in _prose_lines(lines): + if not _RULE_LINE_RE.match(lines[index]): + lines[index] = _BLANK_RUN_RE.sub(lambda match: "\\_" * len(match.group(0)), lines[index]) + return "\n".join(lines) + + +def _fix_bold_span(before: str, content: str, after: str) -> tuple[str, str, str]: + stripped = content.strip() + if not any(char.isalnum() for char in stripped): + return before + content, "", after + if len(stripped) == 1 and stripped.isalpha() and after[:1].islower() and not before[-1:].isalnum(): + return before + stripped, "", after + if len(stripped) > 2 and stripped[-2] == " " and stripped[-1].isupper() and after[:1].islower(): + stripped, after = stripped[:-2], f" {stripped[-1]}{after}" + if content[:1].isspace() or (before[-1:].isalpha() and stripped[:1].isalpha() and len(stripped) > 1): + before += " " + if (content[-1:].isspace() and not after[:1].isspace()) or ( + _LETTER_START_RE.match(after) and len(stripped) > 1 + ): + after = f" {after}" + return before, f"**{stripped}**", after + + +def _fix_bold_spans(text: str) -> str: + parts = text.split("**") + if len(parts) % 2 == 0: + return _fix_bold_spans(text.rstrip()[:-2]) if text.rstrip().endswith("**") else text + for index in range(1, len(parts), 2): + parts[index - 1], parts[index], parts[index + 1] = _fix_bold_span( + parts[index - 1], parts[index], parts[index + 1] + ) + return "".join(parts) + + +def fix_glued_emphasis(markdown: str) -> str: + """Repair bold the PDF extraction glued to its neighbours: `**To**confirm`, `Helps**to**gain`, + `**P**revents`, `**Post: H**ypo`, `efficiency**.**`, a stray trailing `**`. Cell by cell in tables.""" + lines = (markdown or "").split("\n") + for index in _prose_lines(lines): + line = lines[index] + if "**" not in line: + continue + if _is_table_row(line): + lines[index] = "|".join(_fix_bold_spans(cell) for cell in _UNESCAPED_PIPE_RE.split(line)) + else: + lines[index] = _fix_bold_spans(line) + return "\n".join(lines) + + +def repair_markdown(markdown: str) -> str: + """Every mechanical repair, in one pass, for the assembled section product.""" + return fix_glued_emphasis(escape_fill_in_blanks(fix_table_separators(markdown))) diff --git a/wikify/engine/sectionize.py b/wikify/engine/sectionize.py index cbaa58a..1aa049f 100644 --- a/wikify/engine/sectionize.py +++ b/wikify/engine/sectionize.py @@ -22,7 +22,7 @@ from wikify.engine import llm, store from wikify.engine.classify import classify_document -from wikify.engine.lint import fix_table_separators +from wikify.engine.lint import repair_markdown from wikify.engine.loader.cleanup import clean_pages from wikify.engine.loader.page_plan import plan_pages from wikify.engine.loader.sectionizer import _HEADING_RE, _clean_title, sectionize @@ -38,10 +38,10 @@ def sectionize_document(source_document: str, pdf_path: str, project_context: st project_context, use_llm=llm.has_openrouter() and not frappe.flags.in_test, ) - # 0.6 auto-fix boundary: repair separator-less tables in the assembled section + # 0.6 auto-fix boundary: mechanical markdown repairs in the assembled section # product, pre-review. Page canonical stays untouched — pages are evidence. for sec in sections: - sec.markdown = fix_table_separators(sec.markdown) + sec.markdown = repair_markdown(sec.markdown) return store.replace_sections(source_document, sections) @@ -158,7 +158,7 @@ def rebuild_section_markdown(section_name: str) -> dict: raise ValueError(f"No pages found in range {sec.page_start}-{sec.page_end}.") cleaned = clean_pages(pages) - markdown = fix_table_separators("\n\n".join(md.strip() for _, md in cleaned if md.strip())) + markdown = repair_markdown("\n\n".join(md.strip() for _, md in cleaned if md.strip())) first_line, _, body = markdown.partition("\n") heading = _HEADING_RE.match(first_line) if heading and _clean_title(heading.group(2)) == sec.title: diff --git a/wikify/tests/test_lint.py b/wikify/tests/test_lint.py index 1eca042..bdaa76a 100644 --- a/wikify/tests/test_lint.py +++ b/wikify/tests/test_lint.py @@ -17,7 +17,13 @@ from wikify.api import sections as api from wikify.engine import store -from wikify.engine.lint import fix_table_separators, lint_markdown, table_artifacts +from wikify.engine.lint import ( + escape_fill_in_blanks, + fix_glued_emphasis, + fix_table_separators, + lint_markdown, + table_artifacts, +) from wikify.engine.loader.sectionizer import Section from wikify.engine.sectionize import rebuild_section_markdown from wikify.engine.verify.deterministic import parser_artifacts @@ -83,6 +89,46 @@ def test_leaves_lone_rows_and_fenced_tables_alone(self): self.assertEqual(fix_table_separators(fenced), fenced) +class TestFormBlanks(FrappeTestCase): + def test_fill_in_blanks_are_escaped_outside_code_and_html(self): + row = "| | Replacement fluid | ______________________@_____ ___mL/h Additives_______ |" + untouched = "___\n\n`a___b`\n\nRate_____" + blank = "\\_" + fixed = escape_fill_in_blanks(f"{row}\n\n{untouched}") + self.assertEqual( + fixed, + f"| | Replacement fluid | {blank * 22}@{blank * 5} {blank * 3}mL/h Additives{blank * 7} |\n\n{untouched}", + ) + self.assertEqual(escape_fill_in_blanks(fixed), fixed) + + +class TestGluedEmphasis(FrappeTestCase): + def test_bold_glued_to_its_neighbours_is_separated(self): + md = ( + "|1. Inspect the dressing|**To**confirm the need.|\n" + "|3. Explain the procedure|Helps**to**gain co-operation.|\n" + "|8. Clean the site|**P**revents contamination.|\n" + "- **Postpheresis: H**ypokalemia, Bleeding tendencies\n" + "Saves time and improves efficiency**.**\n" + "**Date:10/11/2023 **Pg. 337\n" + "Continuous Renal Replacement Therapy Order sheet :**" + ) + self.assertEqual( + fix_glued_emphasis(md), + "|1. Inspect the dressing|**To** confirm the need.|\n" + "|3. Explain the procedure|Helps **to** gain co-operation.|\n" + "|8. Clean the site|Prevents contamination.|\n" + "- **Postpheresis:** Hypokalemia, Bleeding tendencies\n" + "Saves time and improves efficiency.\n" + "**Date:10/11/2023** Pg. 337\n" + "Continuous Renal Replacement Therapy Order sheet :", + ) + self.assertEqual( + fix_glued_emphasis("**AVF**s and **Artero Venous Fistula (AVF**) is"), + "**AVF**s and **Artero Venous Fistula (AVF**) is", + ) + + class TestPageArtifacts(FrappeTestCase): def test_parser_artifacts_include_table_breakage(self): self.assertIn("table missing separator row", parser_artifacts(MISSING_SEPARATOR)) From d200556fcc7161fef6d76496957f2eec13d61dfb Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 05:53:21 +0530 Subject: [PATCH 13/33] fix(parse): keep form checkboxes and fill-in blanks in vlm and cleanup output --- wikify/engine/loader/cleanup_llm.py | 2 ++ wikify/engine/parsers/vlm.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/wikify/engine/loader/cleanup_llm.py b/wikify/engine/loader/cleanup_llm.py index a08dce8..0c22b9d 100644 --- a/wikify/engine/loader/cleanup_llm.py +++ b/wikify/engine/loader/cleanup_llm.py @@ -28,6 +28,8 @@ "- Headings must be plain text: strip bold/italic emphasis from heading lines " "(e.g. `## _**Verbal Orders**_` → `## Verbal Orders`); never wrap a heading in * or _.\n" "- Keep ALL substantive text. Do NOT summarize, add, translate, or drop real content.\n" + "- Keep form checkboxes (☐ ☑; a symbol-font box such as \uf0a8 becomes ☐) and blank fill-in " + "lines (runs of underscores).\n" "- Keep Markdown image embeds ![alt](url) exactly as they are — they are real figures.\n" "- Output ONLY the cleaned Markdown — no commentary, no code fences.\n\nMARKDOWN:\n" ) diff --git a/wikify/engine/parsers/vlm.py b/wikify/engine/parsers/vlm.py index 4c97651..d254377 100644 --- a/wikify/engine/parsers/vlm.py +++ b/wikify/engine/parsers/vlm.py @@ -31,6 +31,8 @@ 'and statutory references (e.g. "u/s 115BAC", "section 44AD", "First Proviso") character ' "for character. Never round, re-word, convert or summarise a number, and never invent " "content.\n" + "Forms keep their furniture: write every checkbox as ☐ (empty) or ☑ (ticked) in front of its " + "option, and keep blank fill-in lines as runs of underscores.\n" "Output only the Markdown — no commentary, and no code fences except ```mermaid." ) From 922acb7dbbf9c6e1ec217fb11b582433ebf2911b Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 06:08:34 +0530 Subject: [PATCH 14/33] fix(sectionize): keep list items, running chapter headers and resumed parents in the numbered tree --- wikify/engine/loader/sectionizer.py | 122 ++++++++++++++++++++++++++-- wikify/tests/test_sectionize.py | 111 +++++++++++++++++++++++++ 2 files changed, 228 insertions(+), 5 deletions(-) diff --git a/wikify/engine/loader/sectionizer.py b/wikify/engine/loader/sectionizer.py index cc1e072..b5e1365 100644 --- a/wikify/engine/loader/sectionizer.py +++ b/wikify/engine/loader/sectionizer.py @@ -9,6 +9,7 @@ from wikify.engine.loader.toc import correct_level MAX_TITLE_LENGTH = 140 +PREAMBLE_TITLE = "Preamble" RUNNING_HEADER_MIN_PAGES = 3 TOP_OF_PAGE_LINES = 3 @@ -89,6 +90,83 @@ def _repeats_open_section(title: str, stack: list[tuple[int, str, bool]]) -> boo ) +def extends_number(number: tuple[int, ...] | None, prefix: tuple[int, ...]) -> bool: + return bool(number) and len(number) > len(prefix) and number[: len(prefix)] == prefix + + +def dotted_heading_positions(pages: list[tuple[int, str]]) -> list[tuple[int, int, tuple[int, ...]]]: + positions = [] + for page_index, (_, md) in enumerate(pages): + for line_index, line in enumerate(md.splitlines()): + match = _HEADING_RE.match(line) + number = _section_number(_clean_title(match.group(2))) if match else None + if number and len(number) > 1: + positions.append((page_index, line_index, number)) + return positions + + +def opens_next_chapter( + chapter: int, last_number: tuple[int, ...], next_dotted: tuple[int, ...] | None, title: str +) -> bool: + """Inside a numbered sub-section, "4." is a list item unless it is the next chapter: its own + sub-sections follow, or, with none left to read, it is set in capitals like a chapter title.""" + if chapter != last_number[0] + 1: + return False + return next_dotted[0] == chapter if next_dotted else title.isupper() + + +def adopt_preceding_descendants( + sections: list[Section], + current: Section | None, + stack: list[tuple[int, str, bool]], + title: str, + level: int, + page_no: int, +) -> None: + """A parent heading that resurfaces after its sub-sections were read (an excerpt that starts + mid-section) becomes their group instead of a new section after them.""" + number = _section_number(title) + + def descends(section: Section) -> bool: + return any( + extends_number(_section_number(path_title), number) for path_title in section.hierarchy_path + ) + + run = [*sections, current] if current else list(sections) + start = len(run) + while start and descends(run[start - 1]): + start -= 1 + if start == len(run): + return + for section in run[start:]: + depth = next( + index + for index, path_title in enumerate(section.hierarchy_path) + if extends_number(_section_number(path_title), number) + ) + section.hierarchy_path.insert(depth, title) + group_path = run[start].hierarchy_path[: run[start].hierarchy_path.index(title) + 1] + sections.insert( + start, + Section( + title=title, + level=level, + hierarchy_path=list(group_path), + page_start=run[start].page_start, + page_end=page_no, + ), + ) + stack_index = next( + ( + index + for index, (_, open_title, numbered) in enumerate(stack) + if numbered and extends_number(_section_number(open_title), number) + ), + len(stack), + ) + stack.insert(stack_index, (level, title, True)) + + def _continues_numbering(number: tuple[int, ...], last_number: tuple[int, ...]) -> bool: if number == (*last_number, 1): return True @@ -174,19 +252,21 @@ def sectionize(pages: list[tuple[int, str]], level_map: dict[str, int] | None = last_number: tuple[int, ...] = () running_headers = running_header_titles(pages) opened_headers: set[str] = set() + dotted_headings = dotted_heading_positions(pages) def flush(): if current is not None: current.markdown = "\n".join(buf).strip() sections.append(current) - for page_no, md in pages: + for page_index, (page_no, md) in enumerate(pages): # Without a blank line, GFM reads the next page's first line as another row of a table, more # raw HTML, or a lazy continuation of a list item that ended the previous page. if buf and buf[-1].strip(): buf.append("") lines = md.splitlines() toc_end = toc_end_line(lines) + first_line = next((index for index, line in enumerate(lines) if line.strip()), -1) for line_index, line in enumerate(lines): in_toc = line_index <= toc_end and bool(line.strip()) m = _HEADING_RE.match(line if in_toc else _promote_numbered_bold_line(line, last_number)) @@ -214,6 +294,19 @@ def flush(): if current is not None: current.page_end = page_no continue + number = _section_number(title) + if number and extends_number(last_number, number): + # Numbering that steps back to an ancestor is that ancestor's running header. + if len(number) > 1: + if not any(_section_number(open_title) == number for _, open_title, _ in stack): + level = correct_level(title, _infer_level(title, len(m.group(1))), level_map) + adopt_preceding_descendants(sections, current, stack, title, level, page_no) + if current is not None: + current.page_end = page_no + continue + next_line = next((text for text in lines[line_index + 1 :] if text.strip()), "") + if line_index == first_line and _HEADING_RE.match(next_line): + continue if title in running_headers: if title in opened_headers: if current is not None: @@ -227,10 +320,23 @@ def flush(): if level == 1 and title not in level_map: cnum = _chapter_num(title) if cnum is not None: + next_dotted = next( + ( + dotted_number + for dotted_page, dotted_line, dotted_number in dotted_headings + if (dotted_page, dotted_line) > (page_index, line_index) + ), + None, + ) if ( _looks_like_list_item(title) or cnum <= max_chapter or (last_number and cnum <= last_number[0]) + or ( + not max_chapter + and len(last_number) > 1 + and not opens_next_chapter(cnum, last_number, next_dotted, title) + ) or (max_chapter and capital_chapters and not title.isupper()) or (page_no, cnum - 1) == last_list_item ): @@ -245,11 +351,17 @@ def flush(): if anchor is not None: level = min(6, max(level, anchor + 1)) numbered_heading = not demoted and bool(_NUM_RE.match(title)) - while stack and (stack[-1][0] >= level or (numbered_heading and not stack[-1][2])): + while stack and ( + stack[-1][0] >= level + or ( + numbered_heading + and not (stack[-1][2] and extends_number(number, _section_number(stack[-1][1]))) + ) + ): stack.pop() stack.append((level, title, numbered_heading)) if numbered_heading: - last_number = _section_number(title) + last_number = number current = Section( title=title, level=level, @@ -260,9 +372,9 @@ def flush(): else: if current is None and line.strip(): current = Section( - title="Preamble", + title=PREAMBLE_TITLE, level=1, - hierarchy_path=["Preamble"], + hierarchy_path=[PREAMBLE_TITLE], page_start=page_no, page_end=page_no, ) diff --git a/wikify/tests/test_sectionize.py b/wikify/tests/test_sectionize.py index 5989f32..25b7f91 100644 --- a/wikify/tests/test_sectionize.py +++ b/wikify/tests/test_sectionize.py @@ -487,6 +487,117 @@ def test_stray_bold_markers_are_stripped_from_titles(self): self.assertEqual([s.title for s in secs], ["3. Other equipment:"]) self.assertIn("**CRRT Order sheet :**", secs[0].markdown) + def test_numbered_list_items_inside_a_sub_section_stay_under_it(self): + pages = [ + (5, "## 3.6.8 CAPD Nurse Educator\n\nThe CAPD nurse educator trains patients."), + ( + 7, + "# 3. Responsibilities of the CAPD Nurse Educator towards inpatients\n\n- Check the log-book", + ), + ( + 8, + "# 4. Extra procedures to be performed by the CAPD nurse educator:\n\n- Assist PET\n\n" + "# 5. Other responsibilities of the CAPD Nurse educator\n\n- Attend meetings", + ), + ( + 9, + "## 11. Administration\n\n- Plan duty rosters\n\n" + "## 3.6.9 RENAL TRANSPLANT CO-ORDINATOR\n\n**Qualification:** nurse", + ), + (13, "## 3.7 Allied Health Staff\n\n### 3.7.1 Pharmacist\n\n- Dispensing"), + (14, "# 4. PROTOCOLS\n\n## 4.1 Dialysis\n\n- Prime the circuit"), + ] + self.assertEqual( + [section.hierarchy_path for section in sectionize(pages)], + [ + ["3.6.8 CAPD Nurse Educator"], + [ + "3.6.8 CAPD Nurse Educator", + "3. Responsibilities of the CAPD Nurse Educator towards inpatients", + ], + [ + "3.6.8 CAPD Nurse Educator", + "4. Extra procedures to be performed by the CAPD nurse educator:", + ], + ["3.6.8 CAPD Nurse Educator", "5. Other responsibilities of the CAPD Nurse educator"], + ["3.6.8 CAPD Nurse Educator", "11. Administration"], + ["3.6.9 RENAL TRANSPLANT CO-ORDINATOR"], + ["3.7 Allied Health Staff"], + ["3.7 Allied Health Staff", "3.7.1 Pharmacist"], + ["4. PROTOCOLS"], + ["4. PROTOCOLS", "4.1 Dialysis"], + ], + ) + + def test_a_parent_heading_met_after_its_sub_sections_becomes_their_group(self): + policies = "6.2.1 POLICIES FOR MANAGEMENT OF HEMODIALYSIS" + pages = [ + ( + 1, + "Saline dialysis is reserved for bleeding patients.\n\n" + "## 6.2.1.3ASSESSMENT AND MONITORING\n\nPatients develop complications.", + ), + (3, "## 6.2.1.4 PROTOCOL FOR VASCULAR ACCESS\n\n### AV FISTULA\n\nCannulate with care."), + (19, f"# {policies}\n\n## 6.2.1.8 Emergency Dialysis\n\nDialyse within an hour."), + (20, f"## {policies}\n\n### 6.2.1.9 Temporary Vascular Access\n\nUse a femoral catheter."), + ] + sections = sectionize(pages) + self.assertEqual( + [section.hierarchy_path for section in sections], + [ + ["Preamble"], + [policies], + [policies, "6.2.1.3 ASSESSMENT AND MONITORING"], + [policies, "6.2.1.4 PROTOCOL FOR VASCULAR ACCESS"], + [policies, "6.2.1.4 PROTOCOL FOR VASCULAR ACCESS", "AV FISTULA"], + [policies, "6.2.1.8 Emergency Dialysis"], + [policies, "6.2.1.9 Temporary Vascular Access"], + ], + ) + self.assertEqual((sections[1].level, sections[1].markdown, sections[1].page_start), (3, "", 1)) + self.assertEqual(sections[4].markdown, "Cannulate with care.") + + def test_running_chapter_header_at_a_page_top_is_dropped(self): + pages = [ + (10, "## 3.6.9 RENAL TRANSPLANT CO-ORDINATOR\n\n- Maintain the registry."), + ( + 13, + "# 3. JOB DESCRIPTIONS\n\n## 3.7 Allied Health Staff\n\n### 3.7.1 Pharmacist\n\n- Dispensing", + ), + (14, "### 3.7.2 Transplant Coordinator\n\n- Counsel donors."), + (15, "# 3. JOB DESCRIPTION\n\n## 3.7.3 Dialysis therapists\n\n- Prime the circuit."), + ] + sections = sectionize(pages) + self.assertEqual( + [section.title for section in sections], + [ + "3.6.9 RENAL TRANSPLANT CO-ORDINATOR", + "3.7 Allied Health Staff", + "3.7.1 Pharmacist", + "3.7.2 Transplant Coordinator", + "3.7.3 Dialysis therapists", + ], + ) + self.assertFalse(any("JOB DESCRIPTION" in section.markdown for section in sections)) + + def test_a_numbered_section_never_nests_under_another_chapter(self): + pages = [ + ( + 1, + "# 3. **Other equipment:** \n\n- Gloves\n\n## Patient Preparation\n\nExplain the procedure.\n\n" + "# **6.4.2. CENTRAL VENOUS CATHETERIZATION** \n\n# **Definition** \n\nA double lumen catheter.", + ) + ] + self.assertEqual( + [section.hierarchy_path for section in sectionize(pages)], + [ + ["3. Other equipment:"], + ["3. Other equipment:", "Patient Preparation"], + ["6.4.2. CENTRAL VENOUS CATHETERIZATION"], + ["6.4.2. CENTRAL VENOUS CATHETERIZATION", "Definition"], + ], + ) + class TestEmptySectionsAreFlagged(FrappeTestCase): def test_section_without_markdown_is_chunked_as_title_only(self): From 3d94885656b26fed93d98024a8b229a765b53e4b Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 06:08:34 +0530 Subject: [PATCH 15/33] fix(cleanup): keep a list continued across a page break nested under its item --- wikify/engine/loader/cleanup.py | 66 ++++++++++++++++++++++++++++++++- wikify/tests/test_sectionize.py | 31 ++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/wikify/engine/loader/cleanup.py b/wikify/engine/loader/cleanup.py index 1cbbace..cbba525 100644 --- a/wikify/engine/loader/cleanup.py +++ b/wikify/engine/loader/cleanup.py @@ -53,6 +53,8 @@ # A numbered heading repeated as a running sub-header is still the real section start on its first # page; the sectionizer folds the repeats, so stripping them here would lose the section itself. _NUMBERED_HEADING = re.compile(r"^\s*#{1,6}\s+[*_]*\d+(?:\.\d+)*\.?\s*[A-Za-z]") +_LIST_ITEM = re.compile(r"^(\s*)([-*+]|\d+[.)])\s+\S") +_ENUMERATED_TEXT = re.compile(r"^(\d+)[.)]\s") def _plain(line: str) -> str: @@ -198,10 +200,72 @@ def join_page_breaks(pages: list[tuple[int, str]]) -> list[tuple[int, str]]: return [(page_no, "\n".join(lines)) for (page_no, _), lines in zip(pages, page_lines, strict=True)] +def list_kind(marker: str) -> str: + return "bullet" if marker in "-*+" else "ordered" + + +def last_list_item(lines: list[str]) -> re.Match | None: + for line in reversed(lines): + if not line.strip(): + continue + item = _LIST_ITEM.match(line) + if item or not line[0].isspace(): + return item + return None + + +def continuation_prefix(item: re.Match, first_line: str) -> str: + """What a list cut by a page break needs in front of its items on the next page, where it restarts + at the margin: the nesting it had, or the bullet it had when the bullets carried their own numbers.""" + follow = _LIST_ITEM.match(first_line) + if not follow or follow.group(1): + return "" + indent, marker = item.group(1), item.group(2) + if indent and list_kind(marker) == list_kind(follow.group(2)): + return indent + numbered_text = _ENUMERATED_TEXT.match(item.string[item.end(2) :].lstrip()) + if ( + list_kind(marker) == "bullet" + and list_kind(follow.group(2)) == "ordered" + and numbered_text + and int(follow.group(2)[:-1]) == int(numbered_text.group(1)) + 1 + ): + return f"{indent}{marker} " + return "" + + +def reindent_list_continuations(pages: list[tuple[int, str]]) -> list[tuple[int, str]]: + """Put a list continued across a page break back under the item it belongs to, up to the first + block that is not part of the list.""" + page_lines = [md.splitlines() for _, md in pages] + for index in range(1, len(pages)): + if pages[index][0] != pages[index - 1][0] + 1: + continue + item = last_list_item(page_lines[index - 1]) + lines = page_lines[index] + first = next((line for line in lines if line.strip()), "") + prefix = continuation_prefix(item, first) if item else "" + if not prefix: + continue + kind = list_kind(_LIST_ITEM.match(first).group(2)) + for line_index, line in enumerate(lines): + if not line.strip(): + continue + if line[0].isspace(): + lines[line_index] = " " * len(prefix) + line + continue + follow = _LIST_ITEM.match(line) + if not follow or list_kind(follow.group(2)) != kind: + break + lines[line_index] = prefix + line + return [(page_no, "\n".join(lines)) for (page_no, _), lines in zip(pages, page_lines, strict=True)] + + def clean_pages(pages: list[tuple[int, str]]) -> list[tuple[int, str]]: pages = [(page_no, html_tables_to_markdown(md)) for page_no, md in pages] stitched = stitch_cross_page_tables(strip_boilerplate(pages, find_boilerplate(pages))) - return join_page_breaks([(page_no, merge_continuation_rows(md)) for page_no, md in stitched]) + joined = join_page_breaks([(page_no, merge_continuation_rows(md)) for page_no, md in stitched]) + return reindent_list_continuations(joined) def strip_outer_markdown_fence(text: str) -> str: diff --git a/wikify/tests/test_sectionize.py b/wikify/tests/test_sectionize.py index 25b7f91..52ea7ff 100644 --- a/wikify/tests/test_sectionize.py +++ b/wikify/tests/test_sectionize.py @@ -598,6 +598,37 @@ def test_a_numbered_section_never_nests_under_another_chapter(self): ], ) + def test_clean_pages_keeps_a_list_continued_on_the_next_page_in_its_place(self): + pages = [ + ( + 169, + "## 7. HYPERKALEMIA\n\n4. **Drugs:** given on verbal order\n" + " - Administer Inj. Dextrose 50% with Inj. Actrapid.\n" + " - Administer Salbutamol nebulization 5mg.\n\n169", + ), + ( + 170, + "- Keep Inj.Calcium Gluconate 10% ready.\n\n## CENTRAL VENOUS CATHETER BLOCK\n\n- Flush it.\n\n170", + ), + ( + 323, + "# **2. Patient/family**\n\n- 1) Explain the cost to the patient\n\n- 2) Obtain consent\n\n323", + ), + ( + 324, + "3) Instruct patient not to touch the sterile area\n4) Explain it is a temporary access\n\n## Procedure", + ), + ] + cleaned = dict(clean_pages(pages)) + self.assertEqual( + cleaned[170], + " - Keep Inj.Calcium Gluconate 10% ready.\n\n## CENTRAL VENOUS CATHETER BLOCK\n\n- Flush it.", + ) + self.assertEqual( + cleaned[324], + "- 3) Instruct patient not to touch the sterile area\n- 4) Explain it is a temporary access\n\n## Procedure", + ) + class TestEmptySectionsAreFlagged(FrappeTestCase): def test_section_without_markdown_is_chunked_as_title_only(self): From 48d823b256169cfbc47af590bb4442c4c14d9e8a Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 06:08:34 +0530 Subject: [PATCH 16/33] fix(page-plan): split long pages at sub-headings, fold a short leading preamble and page numbered siblings alike --- wikify/engine/loader/page_plan.py | 103 +++++++++++++++++++++++++++--- wikify/tests/test_page_plan.py | 95 +++++++++++++++++++++++++-- 2 files changed, 181 insertions(+), 17 deletions(-) diff --git a/wikify/engine/loader/page_plan.py b/wikify/engine/loader/page_plan.py index 97c6ce5..d6b75f8 100644 --- a/wikify/engine/loader/page_plan.py +++ b/wikify/engine/loader/page_plan.py @@ -6,22 +6,27 @@ from __future__ import annotations import json +import re from collections import Counter, defaultdict import frappe from wikify.engine import llm, settings from wikify.engine.loader.context import context_block -from wikify.engine.loader.sectionizer import Section, _section_number +from wikify.engine.loader.sectionizer import PREAMBLE_TITLE, Section, _section_number from wikify.engine.store import resolve_parent_indexes FALLBACK_PAGE_NUMBER_DEPTH = 3 FALLBACK_PAGE_TREE_DEPTH = 1 FRAGMENT_WORDS = 40 +LEADING_FRAGMENT_WORDS = 60 +MIN_SPLIT_PART_WORDS = 150 MAX_PAGE_WORDS = 2500 MAX_PAGE_PDF_PAGES = 6 OUTLINE_BATCH_LINES = 250 +_LIST_ITEM_TITLE = re.compile(r"^(?:\d+[.)]?|[A-Za-z][.)]|[IVXivx]+[.)])\s") + _PROMPT = ( "You are planning how a PDF document becomes a wiki. Below is its section outline in " "document order, one section per line: index | depth | title | PDF pages | own words | " @@ -48,7 +53,8 @@ def plan_pages(sections: list[Section], project_context: str = "", use_llm: bool = False) -> list[Section]: parents = resolve_parent_indexes(sections) chosen = llm_pages(sections, parents, project_context) if use_llm else fallback_pages(sections, parents) - return fold_sections(sections, parents, enforce_invariants(sections, parents, chosen)) + pages, parents = enforce_invariants(sections, parents, chosen) + return fold_sections(sections, parents, pages) def tree_depths(parents: list[int | None]) -> list[int]: @@ -99,32 +105,62 @@ def ancestors(parents: list[int | None], index: int): parent = parents[parent] -def enforce_invariants(sections: list[Section], parents: list[int | None], chosen: set[int]) -> set[int]: +def enforce_invariants( + sections: list[Section], parents: list[int | None], chosen: set[int] +) -> tuple[set[int], list[int | None]]: numbers = outline_numbers(sections, parents) child_counts = Counter(parent for parent in parents if parent is not None) pages: set[int] = set() for index, parent in enumerate(parents): if parent is None: - fragment = index not in child_counts and len(sections[index].markdown.split()) < FRAGMENT_WORDS - if index == 0 or numbers[index] or (index in chosen and not fragment): + words = len(sections[index].markdown.split()) + fragment = index not in child_counts and words < FRAGMENT_WORDS + leading_fragment = ( + index == 0 + and len(sections) > 1 + and index not in child_counts + and not numbers[index] + and words < LEADING_FRAGMENT_WORDS + ) + if not leading_fragment and (index == 0 or numbers[index] or (index in chosen and not fragment)): pages.add(index) elif index in chosen and parent in pages: pages.add(index) - pages = split_long_pages(sections, parents, numbers, pages) - return { + pages = add_numbered_siblings(parents, numbers, pages) + pages, parents = split_long_pages(sections, parents, numbers, pages) + child_counts = Counter(parent for parent in parents if parent is not None) + pages = { index for index in pages if parents[index] is None or index in child_counts or (sections[index].markdown.strip() and child_counts[parents[index]] > 1) } + return pages, parents + + +def add_numbered_siblings( + parents: list[int | None], numbers: list[tuple[int, ...] | None], pages: set[int] +) -> set[int]: + """Numbered siblings are pages alike: once most of them are, the rest follow.""" + numbered_children: dict[int, list[int]] = defaultdict(list) + for index, parent in enumerate(parents): + if parent is not None and numbers[index]: + numbered_children[parent].append(index) + pages = set(pages) + for parent in sorted(numbered_children): + siblings = numbered_children[parent] + if parent in pages and 2 * len(pages.intersection(siblings)) > len(siblings): + pages.update(siblings) + return pages def split_long_pages( sections: list[Section], parents: list[int | None], numbers: list[tuple[int, ...] | None], pages: set[int] -) -> set[int]: +) -> tuple[set[int], list[int | None]]: """A page too long to read in one scroll is split at its numbered children, recursively; tiny - children stay folded.""" + children stay folded. Without numbered children it splits at its sub-headings instead.""" + parents = list(parents) children: dict[int, list[int]] = defaultdict(list) subtree_words = [len(section.markdown.split()) for section in sections] for index in reversed(range(len(sections))): @@ -147,19 +183,66 @@ def split_long_pages( for child in children[page] if numbers[child] and child not in pages and subtree_words[child] >= FRAGMENT_WORDS ] + if not split: + for part in heading_parts(sections, children[page], subtree_words): + split.append(part[0]) + for member in part[1:]: + parents[member] = part[0] + children[part[0]].append(member) + children[page] = [child for child in children[page] if child not in part[1:]] pages.update(split) queue.extend(split) - return pages + return pages, parents + + +def heading_parts(sections: list[Section], children: list[int], subtree_words: list[int]) -> list[list[int]]: + """Runs of children that each start at a sub-heading and hold at least MIN_SPLIT_PART_WORDS; a + tiny run joins the run before it, or the run after it when it is a bare heading.""" + starts = [child for child in children if not _LIST_ITEM_TITLE.match(sections[child].title)] + if len(starts) < 2: + starts = children + parts: list[list[int]] = [] + for child in children: + if child in starts: + parts.append([child]) + elif parts: + parts[-1].append(child) + merged: list[list[int]] = [] + carried: list[int] = [] + for part in parts: + part = carried + part + carried = [] + if sum(subtree_words[child] for child in part) >= MIN_SPLIT_PART_WORDS: + merged.append(part) + elif merged and sections[part[0]].markdown.strip(): + merged[-1].extend(part) + else: + carried = part + if carried and merged: + merged[-1].extend(carried) + elif carried: + merged.append(carried) + return merged if len(merged) > 1 else [] def fold_sections(sections: list[Section], parents: list[int | None], pages: set[int]) -> list[Section]: owner: dict[int, int] = {} depth_below_page: dict[int, int] = {} + page_parents = {parents[index] for index in pages} + leading: list[str] = [] for index, section in enumerate(sections): if index in pages: owner[index], depth_below_page[index] = index, 0 + if leading and index not in page_parents: + section.markdown = "\n\n".join(part for part in (*leading, section.markdown.strip()) if part) + section.page_start = min(section.page_start, sections[0].page_start) + leading = [] continue parent = parents[index] + if parent is None and index - 1 not in owner: + title = "" if section.title == PREAMBLE_TITLE else f"**{section.title}**" + leading.extend(part for part in (title, section.markdown.strip()) if part) + continue if parent is None: owner[index], depth_below_page[index] = owner[index - 1], 1 else: diff --git a/wikify/tests/test_page_plan.py b/wikify/tests/test_page_plan.py index 94af85f..0b711f7 100644 --- a/wikify/tests/test_page_plan.py +++ b/wikify/tests/test_page_plan.py @@ -21,9 +21,13 @@ def _sec(path, page_start, page_end=None, markdown=None): ) +def _words(count): + return " ".join(["word"] * count) + + def _outline(): return [ - _sec(["Preamble"], 1), + _sec(["Preamble"], 1, markdown=_words(60)), _sec(["5.8 Care"], 2, markdown=""), _sec(["5.8 Care", "5.8.1 Chaperone"], 2), _sec(["5.8 Care", "5.8.4 Sedation"], 3), @@ -39,10 +43,6 @@ def _outline(): ] -def _words(count): - return " ".join(["word"] * count) - - def _llm_reply(payload): content = payload if isinstance(payload, str) else json.dumps(payload) return {"choices": [{"message": {"content": content}}]} @@ -149,7 +149,15 @@ def test_llm_choice_decides_the_pages(self): def test_a_json_encoded_page_list_is_accepted(self): pages, _ = self._plan(_llm_reply({"pages": "[3, 7]"})) self.assertEqual( - _titles(pages), ["Preamble", "5.8 Care", "5.8.4 Sedation", "5.8.9 Grievances", "6.1 Protocols"] + _titles(pages), + [ + "Preamble", + "5.8 Care", + "5.8.1 Chaperone", + "5.8.4 Sedation", + "5.8.9 Grievances", + "6.1 Protocols", + ], ) def test_top_level_sections_are_pages_even_when_the_llm_skips_them(self): @@ -203,7 +211,7 @@ def test_large_outlines_are_sent_per_chapter(self): def test_an_unnumbered_top_level_fragment_folds_into_the_page_before_it(self): sections = [ - _sec(["Preamble"], 1), + _sec(["Preamble"], 1, markdown=_words(60)), _sec(["IMMEDIATE PRE-OPERATIVE PROTOCOLS"], 7, 9, markdown=_words(60)), _sec(["Pre transplant day"], 9, markdown=""), _sec(["PRE-TRANSPLANT RECIPIENT ORDER SHEET"], 9, 10, markdown=_words(60)), @@ -250,3 +258,76 @@ def test_a_page_spanning_too_many_pdf_pages_splits_at_its_numbered_children(self ], ) self.assertIn("## 6.2.2.2 Note", pages[0].markdown) + + def test_a_long_page_without_numbered_children_splits_at_its_sub_headings(self): + access = "6.2.1.4 PROTOCOL FOR VASCULAR ACCESS" + sections = [ + _sec([access], 3, markdown=""), + _sec([access, "AV FISTULA/AV GRAFT"], 3, markdown=_words(60)), + _sec([access, "A. Skin Preparation"], 3, 4, markdown=_words(230)), + _sec([access, "CENTRAL VENOUS CATHETER"], 5, markdown=""), + _sec([access, "CATHETER CARE"], 5, markdown=_words(260)), + _sec([access, "I. EXIT SITE CARE"], 6, 7, markdown=_words(140)), + _sec([access, "DIALYZER REUSE"], 7, markdown=_words(30)), + _sec([access, "IDENTIFYING AND MANAGING COMPLICATIONS"], 7, markdown=""), + _sec([access, "1. INTRA DIALYTIC HYPOTENSION"], 7, 8, markdown=_words(110)), + _sec([access, "2. HYPERKALEMIA"], 9, 10, markdown=_words(80)), + _sec([access, "Peritoneal Dialysis"], 11, markdown=""), + _sec([access, "1 Dialysis Prescription"], 11, markdown=_words(30)), + _sec([access, "2 Peritonitis"], 11, markdown=_words(90)), + _sec([access, "4 Exit Site Infection"], 11, markdown=_words(40)), + ] + pages, _ = self._plan(_llm_reply({"pages": []}), sections) + self.assertEqual( + _titles(pages), + [ + access, + "AV FISTULA/AV GRAFT", + "CENTRAL VENOUS CATHETER", + "IDENTIFYING AND MANAGING COMPLICATIONS", + "Peritoneal Dialysis", + ], + ) + self.assertEqual(pages[0].markdown, "") + self.assertIn("## CATHETER CARE", pages[2].markdown) + self.assertTrue(pages[2].markdown.endswith(f"## DIALYZER REUSE\n\n{_words(30)}")) + self.assertEqual(pages[4].hierarchy_path, [access, "Peritoneal Dialysis"]) + self.assertEqual((pages[4].page_start, pages[4].page_end), (11, 11)) + + def test_a_short_leading_preamble_joins_the_page_after_it(self): + sections = [ + _sec(["Preamble"], 1, markdown="Saline dialysis is reserved for bleeding patients."), + _sec(["6.2.1 POLICIES"], 1, markdown=""), + _sec(["6.2.1 POLICIES", "6.2.1.3 ASSESSMENT"], 1, 2), + _sec(["6.2.1 POLICIES", "6.2.1.4 VASCULAR ACCESS"], 3), + ] + pages, _ = self._plan(_llm_reply({"pages": [0, 1, 2, 3]}), sections) + self.assertEqual(_titles(pages), ["6.2.1 POLICIES", "6.2.1.3 ASSESSMENT", "6.2.1.4 VASCULAR ACCESS"]) + self.assertEqual(pages[0].markdown, "") + self.assertEqual( + pages[1].markdown, + "Saline dialysis is reserved for bleeding patients.\n\nbody of 6.2.1.3 ASSESSMENT", + ) + + def test_numbered_siblings_follow_the_majority_into_pages(self): + sections = [ + _sec(["5.8 Policies on patient care"], 1, markdown=_words(20)), + _sec(["5.8 Policies on patient care", "5.8.1 Chaperone"], 1, markdown=_words(90)), + _sec(["5.8 Policies on patient care", "5.8.2 Antibiotic policy"], 1, markdown=_words(29)), + _sec(["5.8 Policies on patient care", "5.8.3 Consent"], 2, markdown=_words(200)), + _sec(["5.8 Policies on patient care", "5.8.4 Sedation"], 3, markdown=_words(300)), + _sec(["5.8 Policies on patient care", "5.8.4 Sedation", "Contraindications"], 3), + ] + pages, _ = self._plan(_llm_reply({"pages": [1, 3, 4]}), sections) + self.assertEqual( + _titles(pages), + [ + "5.8 Policies on patient care", + "5.8.1 Chaperone", + "5.8.2 Antibiotic policy", + "5.8.3 Consent", + "5.8.4 Sedation", + ], + ) + pages, _ = self._plan(_llm_reply({"pages": [4]}), sections) + self.assertEqual(_titles(pages), ["5.8 Policies on patient care", "5.8.4 Sedation"]) From 46d9916d230472c64256368432d6b04e2ea4021c Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 06:18:03 +0530 Subject: [PATCH 17/33] fix(page-plan): keep numbered siblings after a page in reading order --- wikify/engine/loader/page_plan.py | 29 +++++++++++++++++++++++++++++ wikify/tests/test_page_plan.py | 22 +++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/wikify/engine/loader/page_plan.py b/wikify/engine/loader/page_plan.py index d6b75f8..6f91f66 100644 --- a/wikify/engine/loader/page_plan.py +++ b/wikify/engine/loader/page_plan.py @@ -128,6 +128,7 @@ def enforce_invariants( pages.add(index) pages = add_numbered_siblings(parents, numbers, pages) pages, parents = split_long_pages(sections, parents, numbers, pages) + pages, parents = keep_sibling_order(sections, parents, numbers, pages) child_counts = Counter(parent for parent in parents if parent is not None) pages = { index @@ -155,6 +156,34 @@ def add_numbered_siblings( return pages +def keep_sibling_order( + sections: list[Section], parents: list[int | None], numbers: list[tuple[int, ...] | None], pages: set[int] +) -> tuple[set[int], list[int | None]]: + """A numbered sibling after a page must not fold into the parent, which would show it before that + page: it becomes a page, or joins the last page before it when it is only a fragment.""" + parents = list(parents) + subtree_words = [len(section.markdown.split()) for section in sections] + for index in reversed(range(len(sections))): + if parents[index] is not None: + subtree_words[parents[index]] += subtree_words[index] + numbered_children: dict[int, list[int]] = defaultdict(list) + for index, parent in enumerate(parents): + if parent in pages and numbers[index]: + numbered_children[parent].append(index) + pages = set(pages) + for siblings in numbered_children.values(): + previous_page = None + for sibling in siblings: + if sibling in pages: + previous_page = sibling + elif previous_page is not None and subtree_words[sibling] >= FRAGMENT_WORDS: + pages.add(sibling) + previous_page = sibling + elif previous_page is not None: + parents[sibling] = max(page for page in pages if page < sibling) + return pages, parents + + def split_long_pages( sections: list[Section], parents: list[int | None], numbers: list[tuple[int, ...] | None], pages: set[int] ) -> tuple[set[int], list[int | None]]: diff --git a/wikify/tests/test_page_plan.py b/wikify/tests/test_page_plan.py index 0b711f7..22e957c 100644 --- a/wikify/tests/test_page_plan.py +++ b/wikify/tests/test_page_plan.py @@ -178,6 +178,7 @@ def test_an_empty_page_without_page_children_folds_into_its_parent(self): def test_unusable_indices_are_ignored(self): pages, _ = self._plan(_llm_reply({"pages": ["2", 99, -1, True, 3]})) self.assertEqual(_titles(pages), ["Preamble", "5.8 Care", "5.8.4 Sedation", "6.1 Protocols"]) + self.assertIn("5.8.9 Grievances", pages[2].markdown) def test_invalid_reply_falls_back_to_the_numbering_rule(self): expected = _titles(plan_pages(_outline())) @@ -257,7 +258,8 @@ def test_a_page_spanning_too_many_pdf_pages_splits_at_its_numbered_children(self "6.2.2.1.3 Intra-op", ], ) - self.assertIn("## 6.2.2.2 Note", pages[0].markdown) + self.assertIn("6.2.2.2 Note", pages[-1].markdown) + self.assertNotIn("6.2.2.2 Note", pages[0].markdown) def test_a_long_page_without_numbered_children_splits_at_its_sub_headings(self): access = "6.2.1.4 PROTOCOL FOR VASCULAR ACCESS" @@ -331,3 +333,21 @@ def test_numbered_siblings_follow_the_majority_into_pages(self): ) pages, _ = self._plan(_llm_reply({"pages": [4]}), sections) self.assertEqual(_titles(pages), ["5.8 Policies on patient care", "5.8.4 Sedation"]) + + def test_numbered_siblings_after_a_page_are_pages_so_the_order_holds(self): + sections = [ + _sec(["2. Organogram"], 1, markdown=_words(20)), + _sec(["2. Organogram", "2.1 The staff"], 1, markdown=_words(200)), + _sec(["2. Organogram", "2.2 Hierarchy"], 1, markdown=_words(200)), + _sec(["2. Organogram", "2.3 Staff list"], 2, markdown=_words(200)), + _sec(["2. Organogram", "2.4 Laboratory staff"], 3, markdown=_words(60)), + _sec(["2. Organogram", "2.5 Office staff"], 3, markdown=_words(60)), + _sec(["2. Organogram", "2.6 Support staff"], 3, markdown=_words(10)), + ] + pages, _ = self._plan(_llm_reply({"pages": [0, 2]}), sections) + self.assertEqual( + _titles(pages), + ["2. Organogram", "2.2 Hierarchy", "2.3 Staff list", "2.4 Laboratory staff", "2.5 Office staff"], + ) + self.assertIn("## 2.6 Support staff", pages[-1].markdown) + self.assertNotIn("2.6 Support staff", pages[0].markdown) From 3a784796e4a17b63bc1a1ddf192a775edb6eeb6b Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 06:19:00 +0530 Subject: [PATCH 18/33] fix(parse): keep lettered, roman and tick list markers instead of renumbering them --- wikify/engine/loader/cleanup_llm.py | 2 ++ wikify/engine/parsers/vlm.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/wikify/engine/loader/cleanup_llm.py b/wikify/engine/loader/cleanup_llm.py index 0c22b9d..f257150 100644 --- a/wikify/engine/loader/cleanup_llm.py +++ b/wikify/engine/loader/cleanup_llm.py @@ -30,6 +30,8 @@ "- Keep ALL substantive text. Do NOT summarize, add, translate, or drop real content.\n" "- Keep form checkboxes (☐ ☑; a symbol-font box such as \uf0a8 becomes ☐) and blank fill-in " "lines (runs of underscores).\n" + "- Keep list markers as printed: letters (a. b.), roman numerals (i. ii.) and ✓ ticks are never " + "renumbered into 1. 2. 3.\n" "- Keep Markdown image embeds ![alt](url) exactly as they are — they are real figures.\n" "- Output ONLY the cleaned Markdown — no commentary, no code fences.\n\nMARKDOWN:\n" ) diff --git a/wikify/engine/parsers/vlm.py b/wikify/engine/parsers/vlm.py index d254377..4764490 100644 --- a/wikify/engine/parsers/vlm.py +++ b/wikify/engine/parsers/vlm.py @@ -33,6 +33,8 @@ "content.\n" "Forms keep their furniture: write every checkbox as ☐ (empty) or ☑ (ticked) in front of its " "option, and keep blank fill-in lines as runs of underscores.\n" + "Keep list markers as printed: letters (a. b. c.), roman numerals (i. ii.) and ✓ ticks are " + "never renumbered into 1. 2. 3.\n" "Output only the Markdown — no commentary, and no code fences except ```mermaid." ) From 8168668b2897fbdcfee05b08f355c4ac322be77f Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 07:34:13 +0530 Subject: [PATCH 19/33] docs(media): add nephrology fidelity before and after screenshots --- .../media/nephrology-fidelity-chart.png | Bin 0 -> 158347 bytes .../media/nephrology-fidelity-grouping.png | Bin 0 -> 262292 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/implementation/media/nephrology-fidelity-chart.png create mode 100644 docs/implementation/media/nephrology-fidelity-grouping.png diff --git a/docs/implementation/media/nephrology-fidelity-chart.png b/docs/implementation/media/nephrology-fidelity-chart.png new file mode 100644 index 0000000000000000000000000000000000000000..34a1e13ae8fcfc57a6fd8f48fa9dc41832423a4c GIT binary patch literal 158347 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 r!!CRS| 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 V^Q%HS$2Y=AS!o5SObLCT{{;|J!leKJ literal 0 HcmV?d00001 diff --git a/docs/implementation/media/nephrology-fidelity-grouping.png b/docs/implementation/media/nephrology-fidelity-grouping.png new file mode 100644 index 0000000000000000000000000000000000000000..5781fb766727cf395c6a75cdbf0e0bf6624e1911 GIT binary patch literal 262292 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
uPEk6H_;eBsmKW1BLbnH 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%0AeK9Xz8CKs4dgdaOE@&s?DPIU}PFxYm1Hy z(}#2^Ea3sfqNN55XX`p1pIVipv|PDOA+xdQV4|Ex^%SK#nCq?d;je)}D79E`qNJce zql?mnQ@U&P@&&l-G8k1G0|EnCLzlYAdh1Po4&-1Ct}V<1>|^y^LdX4ax;UT6o_5~6Nen;qNuPQ| zy%tZITwJwzc& zYcbDcWr>)nc^;GXdLM7e89`eir=)}mah%q)ytE{>zLY-<+N@Y%o<3;8=`seSB8dKO zr#5V(nq)R2!A|#H_CEt$mY;4ECdb1bXPFh`sj>h%BZH)$m}kFzutL^t8Juy@nLFyH z3R`d(S){%cG%WoG$yX4e_NAuU92AyJSXaR>K)hfw1;*%j|G1u#T=Aj9JrBnmeY_Zb z1|BN(ya)iqoOnMRb5%0o4;<(eJ_jUyH(xUj}b8Sa-Mjo6p<))cV2`o5Zzt zvo{xA;r^Nfu@WQO%Lo5^K5vv$e`SAXrJq)Ezq+C<*T$H-4pJGm|JayzpM1lUu@PI% zpGUlye9!wqLYsb7o7b4Gz>sTRUynHVx~6+7)NX9jUlaxVn_07z-aCFp@k_h!?C7XG zLU-uU#m(1JS^HN}P+D3FErzcF8%+nAnkrX*2AN*@rnZp{?o85waqx_&!{NeVs{jKp zt@&=a^9DOF86#mtz7Kw?);hc3r7%E2r0B|k^08xMi6RIV|Fl+cXB7yuzW7-?I?8Nr zB1VW0khEA^0wxDaPtQQW2EV79z%UPN@-Be^xLm7JNz0<4sj0h~T0m&%EO+-ZVa4BP zZUYbrfct8kw~-i-eBpwy*MS*j+?B1vgfLzj{S8E`pm~A><{QA^a@ggOIyyUxor;Bh zfmW8Uu(NGM@Xb#|XCOEWDqmt=MoL2B8y=v9lY*ts3;f3;liB0|n<97~W8qd;S4@3O zN(7TT=#sA5v*)3M2dB)KfuC^&I6BS*m_gEc?%27LEnERW&J-k&sv2=#o}MH$yskL& zn0HFbse|hzhSag4nEBZmChYcj!Fp)cCw1(SX-|$A!yu>((#SG*V7N=Z9R|)NPEdtmY%vej-}lW>e&-v4(k` zN=N}-FcYqu>Et;u-SG6r>@y8W=PK^pVHHtNb#*#2C1?KFLwR`#4*T;@KA3fcbISc^ zr#$2LBqw2!_!*M`kGiPd-wd#XWnXQ0!HH#o=!r-yFccfiTp5(=!AhDlC!EJ0vePg- zVD`Ac4}YbB{rihARH`-2ab_Hip;BI0RgFw%|9O7S1LIq_5hEcy5%>r{b=|Lk*|W)F z1t`!dpiKKJ$jaKD*$INhhq+l(Q)w#StU^l)lOS&VkJryR&3n^^)Jlmf4BL3%wjWd7 zygcGB7+(B_4VdZA*4Tl}?8BqsLiN4vUY~fE;Wnc8D63V{daqa0fg`!8 zoBq={p?8=T`cRnJ<{>4!XBnuCX31lE`W5qXvOgPrexx8BVZef|TKpA7M?>GmmlIT$ z6kmkZ)DdgzXkW^8fHLCJA9XXJ*p!LJiSce-9n2;kdb}!fC()~UsaUM7`*UHuS3*L9 z4cZVpIft<7Fk>!PU@P?WE>~25Y9pg$$2!$Y<&`UC<7Vh6V{}D{DHwXu{F>}U!2_cH z>-TTu=gNA&uXUG*^T$Sf(j;SZ^A$WAti~ryI0R+SNHp%11dFFn_mue|GvkhpKSTvF zRQ}YNLVzVKZb@$g?(RoK`tjo?|6SBr);gu8$nigM!I_h+sj7;K?R`Fd?_P>B1NjR4 z7BQC=EC0i-hpyoE(!5~Yz;QwgLQU*@E{rAD;uWUq$pd!YK#2r729A%zhD}n^%sKEh z-8=K@_H*Hjs5(G|4oqRVrFJ-QR|2KrviOQdu$3nC0D$Z1n@TSrL#9gfGz3~$j^JVO z(_6PTS2m$kd4To$sHOTkI$4)4?Y;35=N(~&Ms0wUqE^CaIj~g0R1|^FVT(PCxuNgj zqTx{83YmdWr+E*C+dAb2g9mq)jjb=AI-Z>*;@6i&mq*}_t59&b$SqXim^2n}Mn)kB z*IT=*7n4F5yhYM$vU3dnkbrto)?xfN2zMwg6eAdu2;)aPb`Yc)B)^oCgKDk8Lc2!~ zVZeh-vGB1ONEkdIu)17qIe(=Dc~V{d5t%DDn2lAhk#^9buzwfX1~HFylF}3PaSMjU zB|k7kGQ{`%f&Kf19jl!0rNvLF(w16i#o8{$$PcU>dj-x?RcWb?vGGUJD6T>4b!W2J zofChD77f%JwlC_K3Lav?)VPQX>m$1U@Vrq(v1prj-goR80NFz>>430e<=Y^+F05UJ z9D%+QGYvikNkduUGvZtGjIqeL{3IaF!BZ)f;p&hh*z3Z7Y=gFWxD?;1!J|j}uoH_v z3#b#hfIt=9yh)L@XXuc3@mV?{9XP1 z-F8wd6FJ-ilqlw`F@@NL0_M95)8AO4TUzd^c}=8R#kx|s9{_As841V>VdCMzWRsnf zcbR5W4u=JHHdaXEutR%`P;xzr>Novze24D5!A@zo!5$C$ak>v)y0jje1fxFDp= zC-Y-iWKpy3%FJ9ie}1=F%8q}Suldtl)tzHL_qrOp=Ar+n|A(gEeE5*JRw-=#to)1& z)~*G#luD6P>qzzxy$tw$=(_%GZKp&yl)=pR~q9v@Cjs7wWGu&OFvz&#Fv zus_?)fc_D_axI6DDQ3uHQUWIv)jiRZ*o~}pf9PR6->BDgQ?FH4&LcJwp79mKM*(R9 zy#`X@^2Hte4bZ{Cx*)-?&BO{ras*BEPi*MQI{2Hgfk?x8B%`oc^mSD}pJnY;w7jLM z?fIdY@hhF39M3u?L_bt`xaQ-hMV=pAYlCOXB^MN^$Xlzp+xeXTy{xM;zWvX~!Y=7= zYns;0YWZ$i(fIJgsUMG*9`g;Z{(f^FnNG^0hR;CO_glfH0(w2bR>%b@d5x5P`yv>* z$#!F>L6^X4o36DqE6&ez-hKWYud~RD3Xy*(zX_RH6S8u1?e(|* zH!sI};y{?Tr&>|gwM>q!8ft6z@0l~^iFPl(wp+jIDjBdiN@P}hgf!ouxtrTj^~NrG zs*Nf*P@6^Ta9YF%Nwk@njU36cO_p>dUzu&V-Eg}z!cg2E`8}XP5NMj!w{9(>QN{kV zxcCue7VY6vYv@oOpPKAkf;&Y!mpSqittR?Hhy7UEpod}5_s5SF2^k{sS!jO8u06wQ z6c?90gdq|gp^WiAKr9sSKs6Hj2=cK)DVMcMb+lw~kUA4agN#Ww5_#Tt<2$h~FoDR7 z-k)5szBbC2cp7D{1nL2RpyqXm;943^Tc{tD;qeK zw@gejMlQtk7FktYjy;R(n3NReE0?~KxA@)`-ie+90m4VNg|a%}jmv;-ZBDI|XPbqO zKicEO)CVuu$BJLr=JeaQZcJNB$3U|=pY5hej_YXAZu?`nE<&bj%6#f3Cdwb)j%u-# zkV=*i6U<-m8e|B_`!1_B!7Pe_cj=nOT3Uj6VA9>sHk7!}*ZiwwXIiU<0;;A)$tH!y zlQDJTH7Z@y8Dor$KG2{dWWw3)Z2kJL-@hYP71mi}>qL~y&3(zOM7Bq7%Qavzx?qU@ z^y$W9YZhDV0V>^ZAo#tJaQPc{i?a{p^;ds$&)Ps)L=inNykLMEXw!5?oBN1&inEsOSGMvLYIAyvnpKQ=u&5UP^S zCJgZE=R`%}#B5P$bS^fIe^3rNvS6q%{v)93c&vCkj=vR{+pd;*} z01~5m*3;7~eR`6O0L=Q_sh4_!wE#_ZLB3>C4n;4Tde#T;X1W=P4&`Hfg{Zw?6UsfT zdijOIn&uKoDxNt4(173^!j8@otnBh09Z$o1_4SNM_z?n#jnw=b?62`q(9{V+eUfdP zobQw=4!It*(#jH#*m}eFqlrRWI(OJKDt;H2o0KvZ{gh2*s^n2g`Gy{&e0TzYFuPEfms%8#e@l2cQunY;M#6# z&rZ#oJbq3S2qm;Rg(G&)b3;SxPt3AHY(dqAav0A<1|rbyy?#AVSw^sVXVcq~C4#>Z zrbu8C=y(!uyu{IiHv&7osn_sVT!X(0OBy*t4CAuK$md&PcSOKiim$0qxirjTVIj+( zIGv3v$yct^fZksfYuiXRqHLjq=JT?IXgJeS5U+^8;XBFhtWEv)_Q_9f3nEo|$gyMl z*eEB|UNt#yt%G~Zew^lbVk~8z=IdKMyATi0md3_9jx9J0$t2<2=7a
c^i(AP0g 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

F0ZAh6BY;-EHP^ev}u@# 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Ξf*?@wku?k%>n6C{Bd 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 literal 0 HcmV?d00001 From f985c1403b167a919129be228dbc3f32fa81798b Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 07:42:15 +0530 Subject: [PATCH 20/33] fix(lint): avoid implicit string concatenation in lists and lists grown while iterated --- wikify/engine/images.py | 14 +++--- wikify/engine/loader/page_plan.py | 11 +++-- wikify/tests/test_sectionize.py | 80 ++++++++++++++----------------- 3 files changed, 52 insertions(+), 53 deletions(-) diff --git a/wikify/engine/images.py b/wikify/engine/images.py index 8ec1570..4f4990c 100644 --- a/wikify/engine/images.py +++ b/wikify/engine/images.py @@ -164,12 +164,14 @@ def page_figures(page, repeated: set[bytes], page_name: str, page_no: int) -> li def figure_hint(figures: list[Figure]) -> str: if not figures: return "" - positions = "\n".join( - f"- Figure {number}: {figure.bbox[1] / figure.page_size[1]:.0%} to " - f"{figure.bbox[3] / figure.page_size[1]:.0%} down the page, " - f"{figure.bbox[0] / figure.page_size[0]:.0%} to {figure.bbox[2] / figure.page_size[0]:.0%} across" - for number, figure in enumerate(figures, start=1) - ) + lines = [] + for number, figure in enumerate(figures, start=1): + width, height = figure.page_size + left, top, right, bottom = figure.bbox + down = f"{top / height:.0%} to {bottom / height:.0%} down the page" + across = f"{left / width:.0%} to {right / width:.0%} across" + lines.append(f"- Figure {number}: {down}, {across}") + positions = "\n".join(lines) return ( f"\n\nFIGURES: this page has {len(figures)} picture(s) that are cropped from the PDF and shown " f"as images, numbered top to bottom:\n{positions}\n" diff --git a/wikify/engine/loader/page_plan.py b/wikify/engine/loader/page_plan.py index 6f91f66..9e1db45 100644 --- a/wikify/engine/loader/page_plan.py +++ b/wikify/engine/loader/page_plan.py @@ -199,10 +199,13 @@ def split_long_pages( subtree_words[parent] += subtree_words[index] pages = set(pages) queue = sorted(pages) - for page in queue: - folded = [page] - for index in folded: - folded.extend(child for child in children[index] if child not in pages) + while queue: + page = queue.pop(0) + folded, pending = [], [page] + while pending: + index = pending.pop() + folded.append(index) + pending.extend(child for child in children[index] if child not in pages) words = sum(len(sections[index].markdown.split()) for index in folded) pdf_pages = max(sections[index].page_end for index in folded) - sections[page].page_start + 1 if words <= MAX_PAGE_WORDS and pdf_pages <= MAX_PAGE_PDF_PAGES: diff --git a/wikify/tests/test_sectionize.py b/wikify/tests/test_sectionize.py index 52ea7ff..88e29ac 100644 --- a/wikify/tests/test_sectionize.py +++ b/wikify/tests/test_sectionize.py @@ -178,14 +178,12 @@ def test_repeated_chapter_title_with_another_number_or_plural_is_not_a_new_secti self.assertEqual(paths["3.2 Unit head"], ["3. JOB DESCRIPTIONS", "3.2 Unit head"]) def test_bold_line_continuing_the_numbering_is_a_heading(self): - pages = [ - ( - 1, - "## 5.8.6 Blood Donation\na\n### 5.8.6.2 Donor criteria\nb\n" - "**5.8.6.3 Requesting blood from blood bank**\n**For use in ward**\nc\n" - "**5.8.6.4 For use in OR**\nd\n**1. Not a heading**\ne", - ) - ] + markdown = ( + "## 5.8.6 Blood Donation\na\n### 5.8.6.2 Donor criteria\nb\n" + "**5.8.6.3 Requesting blood from blood bank**\n**For use in ward**\nc\n" + "**5.8.6.4 For use in OR**\nd\n**1. Not a heading**\ne" + ) + pages = [(1, markdown)] secs = sectionize(pages) self.assertEqual( [(s.title, s.level) for s in secs], @@ -367,13 +365,11 @@ def test_link_markup_is_stripped_from_heading_titles(self): self.assertEqual(secs[0].title, "PROCEDURE MANUAL - NEPHROLOGY") def test_lowercase_or_colon_heading_is_a_bold_label(self): - pages = [ - ( - 1, - "# **IMMEDIATE PRE-OPERATIVE PROTOCOLS**\nintro\n# **are carried out as below:**\n" - "- PreHD: WBC\n# **Drug list:**\n- MMF 8 am\n# 5.1 Definitions:\nterms", - ) - ] + markdown = ( + "# **IMMEDIATE PRE-OPERATIVE PROTOCOLS**\nintro\n# **are carried out as below:**\n" + "- PreHD: WBC\n# **Drug list:**\n- MMF 8 am\n# 5.1 Definitions:\nterms" + ) + pages = [(1, markdown)] secs = sectionize(pages) self.assertEqual([s.title for s in secs], ["IMMEDIATE PRE-OPERATIVE PROTOCOLS", "5.1 Definitions:"]) self.assertIn("**are carried out as below:**\n- PreHD: WBC\n**Drug list:**", secs[0].markdown) @@ -488,22 +484,22 @@ def test_stray_bold_markers_are_stripped_from_titles(self): self.assertIn("**CRRT Order sheet :**", secs[0].markdown) def test_numbered_list_items_inside_a_sub_section_stay_under_it(self): + extra_duties = ( + "# 4. Extra procedures to be performed by the CAPD nurse educator:\n\n- Assist PET\n\n" + "# 5. Other responsibilities of the CAPD Nurse educator\n\n- Attend meetings" + ) + administration = ( + "## 11. Administration\n\n- Plan duty rosters\n\n" + "## 3.6.9 RENAL TRANSPLANT CO-ORDINATOR\n\n**Qualification:** nurse" + ) pages = [ (5, "## 3.6.8 CAPD Nurse Educator\n\nThe CAPD nurse educator trains patients."), ( 7, "# 3. Responsibilities of the CAPD Nurse Educator towards inpatients\n\n- Check the log-book", ), - ( - 8, - "# 4. Extra procedures to be performed by the CAPD nurse educator:\n\n- Assist PET\n\n" - "# 5. Other responsibilities of the CAPD Nurse educator\n\n- Attend meetings", - ), - ( - 9, - "## 11. Administration\n\n- Plan duty rosters\n\n" - "## 3.6.9 RENAL TRANSPLANT CO-ORDINATOR\n\n**Qualification:** nurse", - ), + (8, extra_duties), + (9, administration), (13, "## 3.7 Allied Health Staff\n\n### 3.7.1 Pharmacist\n\n- Dispensing"), (14, "# 4. PROTOCOLS\n\n## 4.1 Dialysis\n\n- Prime the circuit"), ] @@ -531,12 +527,12 @@ def test_numbered_list_items_inside_a_sub_section_stay_under_it(self): def test_a_parent_heading_met_after_its_sub_sections_becomes_their_group(self): policies = "6.2.1 POLICIES FOR MANAGEMENT OF HEMODIALYSIS" + first_page = ( + "Saline dialysis is reserved for bleeding patients.\n\n" + "## 6.2.1.3ASSESSMENT AND MONITORING\n\nPatients develop complications." + ) pages = [ - ( - 1, - "Saline dialysis is reserved for bleeding patients.\n\n" - "## 6.2.1.3ASSESSMENT AND MONITORING\n\nPatients develop complications.", - ), + (1, first_page), (3, "## 6.2.1.4 PROTOCOL FOR VASCULAR ACCESS\n\n### AV FISTULA\n\nCannulate with care."), (19, f"# {policies}\n\n## 6.2.1.8 Emergency Dialysis\n\nDialyse within an hour."), (20, f"## {policies}\n\n### 6.2.1.9 Temporary Vascular Access\n\nUse a femoral catheter."), @@ -581,13 +577,11 @@ def test_running_chapter_header_at_a_page_top_is_dropped(self): self.assertFalse(any("JOB DESCRIPTION" in section.markdown for section in sections)) def test_a_numbered_section_never_nests_under_another_chapter(self): - pages = [ - ( - 1, - "# 3. **Other equipment:** \n\n- Gloves\n\n## Patient Preparation\n\nExplain the procedure.\n\n" - "# **6.4.2. CENTRAL VENOUS CATHETERIZATION** \n\n# **Definition** \n\nA double lumen catheter.", - ) - ] + markdown = ( + "# 3. **Other equipment:** \n\n- Gloves\n\n## Patient Preparation\n\nExplain the procedure.\n\n" + "# **6.4.2. CENTRAL VENOUS CATHETERIZATION** \n\n# **Definition** \n\nA double lumen catheter." + ) + pages = [(1, markdown)] self.assertEqual( [section.hierarchy_path for section in sectionize(pages)], [ @@ -599,13 +593,13 @@ def test_a_numbered_section_never_nests_under_another_chapter(self): ) def test_clean_pages_keeps_a_list_continued_on_the_next_page_in_its_place(self): + hyperkalemia = ( + "## 7. HYPERKALEMIA\n\n4. **Drugs:** given on verbal order\n" + " - Administer Inj. Dextrose 50% with Inj. Actrapid.\n" + " - Administer Salbutamol nebulization 5mg.\n\n169" + ) pages = [ - ( - 169, - "## 7. HYPERKALEMIA\n\n4. **Drugs:** given on verbal order\n" - " - Administer Inj. Dextrose 50% with Inj. Actrapid.\n" - " - Administer Salbutamol nebulization 5mg.\n\n169", - ), + (169, hyperkalemia), ( 170, "- Keep Inj.Calcium Gluconate 10% ready.\n\n## CENTRAL VENOUS CATHETER BLOCK\n\n- Flush it.\n\n170", From ff545262fe014074e6db118bff367e5e12cdf836 Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 08:02:00 +0530 Subject: [PATCH 21/33] fix(cleanup): strip a running header that alternates styles across a long document --- wikify/engine/loader/cleanup.py | 4 +++- wikify/tests/test_sectionize.py | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/wikify/engine/loader/cleanup.py b/wikify/engine/loader/cleanup.py index cbba525..4eb4f6b 100644 --- a/wikify/engine/loader/cleanup.py +++ b/wikify/engine/loader/cleanup.py @@ -50,6 +50,7 @@ _SENTENCE_START = re.compile(r"^[a-z]") # A sentence cut by a page break ran to the page edge, so its last line is never a short label. _MIN_BROKEN_LINE_LENGTH = 40 +BOILERPLATE_MAX_PAGES = 10 # A numbered heading repeated as a running sub-header is still the real section start on its first # page; the sectionizer folds the repeats, so stripping them here would lose the section itself. _NUMBERED_HEADING = re.compile(r"^\s*#{1,6}\s+[*_]*\d+(?:\.\d+)*\.?\s*[A-Za-z]") @@ -132,7 +133,8 @@ def find_boilerplate(pages: list[tuple[int, str]]) -> set[str]: for _, md in pages: for nl in {_norm(line) for line in _edge_lines(md)}: counts[nl] += 1 - threshold = max(3, int(0.30 * len(pages))) + # Capped so a header that alternates between styles across a long document still counts. + threshold = max(3, min(int(0.30 * len(pages)), BOILERPLATE_MAX_PAGES)) return {line for line, c in counts.items() if c >= threshold and len(line) <= 90} diff --git a/wikify/tests/test_sectionize.py b/wikify/tests/test_sectionize.py index 88e29ac..ea8e4ca 100644 --- a/wikify/tests/test_sectionize.py +++ b/wikify/tests/test_sectionize.py @@ -234,6 +234,13 @@ def test_clean_pages_strips_varying_boilerplate_before_sectionizing(self): self.assertTrue(all("Pg" not in s.markdown for s in secs)) self.assertEqual([s.title for s in secs], ["1. Intro", "2. Next"]) + def test_clean_pages_strips_a_header_style_that_covers_only_part_of_a_long_document(self): + pages = [ + (number, f"{'NEPHROLOGY MANUAL' if number % 4 == 0 else 'PROCEDURE MANUAL'}\n\nbody {number}") + for number in range(1, 101) + ] + self.assertTrue(all("MANUAL" not in markdown for _, markdown in clean_pages(pages))) + def test_clean_pages_strips_signoff_footer_block(self): footer = "|**Prepared by - Dr. A**|**Issued by: QMC**|**Approved by - Dr. B**|\n|---|---|---|" pages = [(1, f"## 1. Intro\nreal body\n{footer}"), (2, f"## 2. Next\nmore body\n{footer}")] From 319b33faef7f42507fdb11cbef26be93b235a63d Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 08:36:38 +0530 Subject: [PATCH 22/33] fix(sectionize): give dotted sections their missing numeric ancestors and keep list items out of chapters --- wikify/engine/loader/sectionizer.py | 179 +++++++++++++++++++++++++--- wikify/tests/test_sectionize.py | 158 ++++++++++++++++++++++++ 2 files changed, 318 insertions(+), 19 deletions(-) diff --git a/wikify/engine/loader/sectionizer.py b/wikify/engine/loader/sectionizer.py index b5e1365..74ee8f9 100644 --- a/wikify/engine/loader/sectionizer.py +++ b/wikify/engine/loader/sectionizer.py @@ -1,6 +1,7 @@ from __future__ import annotations import re +from bisect import bisect_right from collections import defaultdict from dataclasses import dataclass from itertools import pairwise @@ -21,6 +22,9 @@ _GLUED_NUM = re.compile(r"^(\d+(?:\.\d+)+\.?)(?=[A-Z])") _BOLD_LINE = re.compile(r"^\*\*([^*]+)\*\*$") _LINK = re.compile(r"\[([^\]]*)\]\([^)]*\)") +_HTML_TAG = re.compile(r"]*>") +_LETTERED_NUM = re.compile(r"^(\d+(?:\.\d+)+)\.[a-z](?![a-z])\s*") +_TOC_NUMBERED = re.compile(r"^(\d+(?:\.\d+)*)\.?\s+(.*?)(?:\s*\.{3,})?(?:\s+\d{1,4})?$") _CONTENTS_TITLE = re.compile(r"(?i)^(?:table of )?contents$|^index$") _TOC_ENTRY = re.compile( r"^(?:[-*+]\s+)?\|?\s*\d+(?:\.\d+)*\.?\s*\|?\s*[A-Za-z(].*?(?:\.{3,}|\s[-\u2013\u2014]\s|\||
|\s)\s*(\d{1,4})\s*\|?$" @@ -32,9 +36,8 @@ def _clean_title(raw: str) -> str: - title = _GLUED_NUM.sub( - r"\1 ", _LINK.sub(r"\1", raw.replace("**", "").strip().strip("*_").strip()).strip() - ) + text = _HTML_TAG.sub("", raw).replace("**", "").strip().strip("*_").strip() + title = _GLUED_NUM.sub(r"\1 ", _LINK.sub(r"\1", text).strip()) if title.endswith(".") and not title.endswith(".."): title = title[:-1].rstrip() if len(title) <= MAX_TITLE_LENGTH: @@ -108,11 +111,12 @@ def dotted_heading_positions(pages: list[tuple[int, str]]) -> list[tuple[int, in def opens_next_chapter( chapter: int, last_number: tuple[int, ...], next_dotted: tuple[int, ...] | None, title: str ) -> bool: - """Inside a numbered sub-section, "4." is a list item unless it is the next chapter: its own - sub-sections follow, or, with none left to read, it is set in capitals like a chapter title.""" + """Inside a numbered sub-section, "4." is a list item unless it is the next chapter: the next dotted + heading no longer belongs to the current chapter, or, with none left to read, it is set in capitals + like a chapter title.""" if chapter != last_number[0] + 1: return False - return next_dotted[0] == chapter if next_dotted else title.isupper() + return next_dotted[0] >= chapter if next_dotted else title.isupper() def adopt_preceding_descendants( @@ -178,7 +182,9 @@ def _continues_numbering(number: tuple[int, ...], last_number: tuple[int, ...]) ) -def _promote_numbered_bold_line(line: str, last_number: tuple[int, ...]) -> str: +def _promote_numbered_bold_line( + line: str, last_number: tuple[int, ...], next_dotted: tuple[int, ...] | None +) -> str: bold = _BOLD_LINE.match(line.strip()) if not (bold and last_number): return line @@ -186,9 +192,122 @@ def _promote_numbered_bold_line(line: str, last_number: tuple[int, ...]) -> str: number = _section_number(title) if number is None or not _continues_numbering(number, last_number): return line + if ( + len(number) == 1 + and len(last_number) > 1 + and not opens_next_chapter(number[0], last_number, next_dotted, title) + ): + return line return f"{'#' * min(6, len(number))} {title}" +def next_dotted_number( + dotted_headings: list[tuple[int, int, tuple[int, ...]]], page_index: int, line_index: int +) -> tuple[int, ...] | None: + index = bisect_right(dotted_headings, (page_index, line_index), key=lambda heading: heading[:2]) + return dotted_headings[index][2] if index < len(dotted_headings) else None + + +def correct_chapter_typo(title: str, last_number: tuple[int, ...]) -> str: + """A deep number whose chapter jumps while its tail carries on the current numbering ("1.5.3.1.5" + after 6.5.3.1.4) is a misprint of the current chapter.""" + match = _NUM_RE.match(title) + if not (match and last_number): + return title + number = tuple(int(part) for part in match.group(1).split(".")) + corrected = (last_number[0], *number[1:]) + if len(number) < 3 or number[0] == last_number[0] or not _continues_numbering(corrected, last_number): + return title + return ".".join(str(part) for part in corrected) + title[match.end(1) :] + + +def toc_section_titles(pages: list[tuple[int, str]]) -> dict[tuple[int, ...], str]: + titles: dict[tuple[int, ...], str] = {} + for _, md in pages: + lines = md.splitlines() + for line in lines[: toc_end_line(lines) + 1]: + text = " ".join(_HTML_TAG.sub(" ", line).replace("|", " ").split()).lstrip("-*+ ") + entry = _TOC_NUMBERED.match(text) + if entry and entry.group(2): + number = tuple(int(part) for part in entry.group(1).split(".")) + titles.setdefault(number, _clean_title(f"{entry.group(1)} {entry.group(2)}")) + return titles + + +def recurring_section_titles(pages: list[tuple[int, str]]) -> dict[tuple[int, ...], list[str]]: + """Numbered heading titles printed on more than one page, in reading order: running headers that + name a section even where the body never opens it.""" + pages_seen: dict[str, set[int]] = defaultdict(set) + for page_no, md in pages: + for line in md.splitlines(): + match = _HEADING_RE.match(line) + if match: + pages_seen[_clean_title(match.group(2))].add(page_no) + recurring: dict[tuple[int, ...], list[str]] = defaultdict(list) + for title, seen in pages_seen.items(): + number = _section_number(title) + if number and len(seen) > 1: + recurring[number].append(title) + return recurring + + +def repeats_running_header( + title: str, stack: list[tuple[int, str, bool]], recurring: dict[tuple[int, ...], list[str]] +) -> bool: + """A lettered number ("6.2.1.b POLICIES…") under its open section 6.2.1 is that section's running + header "6.2.1 POLICIES…" printed with a suffix.""" + lettered = _LETTERED_NUM.match(title) + if not lettered: + return False + base = tuple(int(part) for part in lettered.group(1).split(".")) + words = _title_words(title[lettered.end() :]) + return any(numbered and _section_number(open_title) == base for _, open_title, numbered in stack) and any( + _title_words(running) == words for running in recurring.get(base, ()) + ) + + +def missing_ancestors( + number: tuple[int, ...], stack: list[tuple[int, str, bool]], last_at_depth: dict[int, tuple[int, ...]] +) -> list[tuple[int, ...]]: + """Numeric ancestors of a heading that the body never opened, where the numbering shows they belong: + the last number read at that depth is the ancestor's predecessor (6.1 after chapter 5).""" + deepest = max( + (len(_section_number(open_title)) for _, open_title, numbered in stack if numbered), default=0 + ) + missing = [] + for depth in range(deepest + 1, len(number)): + prefix = number[:depth] + previous = last_at_depth.get(depth) + if previous and previous[:-1] == prefix[:-1] and previous[-1] + 1 == prefix[-1]: + missing.append(prefix) + return missing + + +def ancestor_title( + prefix: tuple[int, ...], + recurring: dict[tuple[int, ...], list[str]], + toc_titles: dict[tuple[int, ...], str], + child_title: str, +) -> str: + if recurring.get(prefix): + return recurring[prefix][0] + if prefix in toc_titles: + return toc_titles[prefix] + label = ".".join(str(part) for part in prefix) + child = _NUM_RE.match(child_title) + if _section_number(child_title) == (*prefix, 0): + return label + child_title[child.end(1) :] + return label + + +def is_bare_heading_for(section: Section, title: str) -> bool: + return ( + not section.markdown + and not _NUM_RE.match(section.title) + and _title_words(section.title) == _title_words(title) + ) + + def toc_end_line(lines: list[str]) -> int: """Index of the last table-of-contents entry when the page is a TOC page, else -1. TOC entries name real headings, so reading them as headings opens phantom sections and advances chapter numbering.""" @@ -253,6 +372,9 @@ def sectionize(pages: list[tuple[int, str]], level_map: dict[str, int] | None = running_headers = running_header_titles(pages) opened_headers: set[str] = set() dotted_headings = dotted_heading_positions(pages) + recurring_titles = recurring_section_titles(pages) + toc_titles = toc_section_titles(pages) + last_at_depth: dict[int, tuple[int, ...]] = {} def flush(): if current is not None: @@ -269,7 +391,10 @@ def flush(): first_line = next((index for index, line in enumerate(lines) if line.strip()), -1) for line_index, line in enumerate(lines): in_toc = line_index <= toc_end and bool(line.strip()) - m = _HEADING_RE.match(line if in_toc else _promote_numbered_bold_line(line, last_number)) + next_dotted = next_dotted_number(dotted_headings, page_index, line_index) + m = _HEADING_RE.match( + line if in_toc else _promote_numbered_bold_line(line, last_number, next_dotted) + ) title = _clean_title(m.group(2)) if m else "" if in_toc: if not (current and _CONTENTS_TITLE.match(current.title)): @@ -290,7 +415,10 @@ def flush(): elif m and _is_label(title, level_map): line, m = f"**{title}**", None if m: - if _repeats_open_section(title, stack): + title = correct_chapter_typo(title, last_number) + if _repeats_open_section(title, stack) or repeats_running_header( + title, stack, recurring_titles + ): if current is not None: current.page_end = page_no continue @@ -320,21 +448,13 @@ def flush(): if level == 1 and title not in level_map: cnum = _chapter_num(title) if cnum is not None: - next_dotted = next( - ( - dotted_number - for dotted_page, dotted_line, dotted_number in dotted_headings - if (dotted_page, dotted_line) > (page_index, line_index) - ), - None, - ) if ( _looks_like_list_item(title) or cnum <= max_chapter or (last_number and cnum <= last_number[0]) or ( - not max_chapter - and len(last_number) > 1 + len(last_number) > 1 + and (next_dotted or not max_chapter) and not opens_next_chapter(cnum, last_number, next_dotted, title) ) or (max_chapter and capital_chapters and not title.isupper()) @@ -359,9 +479,30 @@ def flush(): ) ): stack.pop() + if numbered_heading: + for prefix in missing_ancestors(number, stack, last_at_depth): + group_title = ancestor_title(prefix, recurring_titles, toc_titles, title) + group_level = max(1, level - len(number) + len(prefix)) + group_start = page_no + if sections and is_bare_heading_for(sections[-1], group_title): + group_start = sections.pop().page_start + stack.append((group_level, group_title, True)) + sections.append( + Section( + title=group_title, + level=group_level, + hierarchy_path=[t for _, t, _ in stack], + page_start=group_start, + page_end=page_no, + ) + ) + last_at_depth[len(prefix)] = prefix + if len(prefix) == 1: + max_chapter = max(max_chapter, prefix[0]) stack.append((level, title, numbered_heading)) if numbered_heading: last_number = number + last_at_depth[len(number)] = number current = Section( title=title, level=level, diff --git a/wikify/tests/test_sectionize.py b/wikify/tests/test_sectionize.py index ea8e4ca..a847979 100644 --- a/wikify/tests/test_sectionize.py +++ b/wikify/tests/test_sectionize.py @@ -630,6 +630,164 @@ def test_clean_pages_keeps_a_list_continued_on_the_next_page_in_its_place(self): "- 3) Instruct patient not to touch the sterile area\n- 4) Explain it is a temporary access\n\n## Procedure", ) + def test_a_chapter_the_body_never_opens_becomes_the_group_of_its_sections(self): + chapter = "6. PROTOCOLS FOR CLINICAL NEPHROLOGY" + contents = "\n".join( + [ + "| S.No | CONTENTS | Pg No |", + "|---|---|---|", + "| 5 | Procedures for access | 96 |", + "| 5.9 | Policies on reporting | 136 |", + "| 6.1 | Protocols for clinical nephrology | 140 |", + "| 6.2 | Procedures in dialysis | 158 |", + "| 6.3 | Transplantation | 267 |", + "| 6.3.3 | Policies for pre-transplant evaluation | 270 |", + ] + ) + transplantation = ( + f"## {chapter}\n\n### 6.3.0 TRANSPLANTATION\n\nKidney transplants.\n\n" + "## 6.3.2 Organ donation\n\nBrain death.\n\n## POLICIES FOR PRE-TRANSPLANT EVALUATION\n\n" + "### 6.3.3.1 RECIPIENT WORKUP\n\nPreliminary evaluation." + ) + pages = [ + (2, contents), + (91, "# 5. PROCEDURES FOR ACCESS\n\n## 5.9 Policies on reporting\n\nReport incidents."), + (137, "# 6.1 PROTOCOLS FOR CLINICAL NEPHROLOGY\n\n## 6.1.1 IgA nephropathy\n\nGive steroids."), + (138, f"# {chapter}\n\n## 6.1.2 Lupus nephritis\n\nBiopsy first."), + (155, f"## {chapter}\n\n### 6.2 PROCEDURES IN DIALYSIS\n\nThe dialysis unit."), + (275, transplantation), + ] + protocols = [chapter, "6.1 PROTOCOLS FOR CLINICAL NEPHROLOGY"] + transplant = [chapter, "6.3 Transplantation"] + evaluation = [*transplant, "6.3.3 Policies for pre-transplant evaluation"] + self.assertEqual( + [section.hierarchy_path for section in sectionize(pages)[1:]], + [ + ["5. PROCEDURES FOR ACCESS"], + ["5. PROCEDURES FOR ACCESS", "5.9 Policies on reporting"], + [chapter], + protocols, + [*protocols, "6.1.1 IgA nephropathy"], + [*protocols, "6.1.2 Lupus nephritis"], + [chapter, "6.2 PROCEDURES IN DIALYSIS"], + transplant, + [*transplant, "6.3.0 TRANSPLANTATION"], + [*transplant, "6.3.2 Organ donation"], + evaluation, + [*evaluation, "6.3.3.1 RECIPIENT WORKUP"], + ], + ) + + def test_a_list_heading_numbered_like_the_next_chapter_stays_in_its_section(self): + complications = ( + "## 6.2.1.4 PROTOCOL FOR VASCULAR ACCESS\n\n# IDENTIFYING AND MANAGING COMPLICATIONS\n\n" + "## 4. AIR EMBOLISM\n\nClamp the line.\n\n# 7. HYPERKALEMIA\n\nGive calcium.\n\n" + "## CENTRAL VENOUS CATHETER BLOCK\n\nUse urokinase." + ) + pages = [ + (91, "# 5. PROCEDURES FOR ACCESS\n\n## 5.1 Policies for access\n\nRegister patients."), + (137, "# 6. PROTOCOLS\n\n## 6.2 PROCEDURES IN DIALYSIS\n\n### 6.2.1 HEMODIALYSIS\n\nThe unit."), + (167, complications), + (172, "## 6.2.1.5 HEPATITIS B VACCINATION\n\nVaccinate."), + (179, "## 6.2.1POLICIES FOR HEMODIALYSIS\n\n### 6.2.1.8 Emergency Dialysis\n\nDialyse now."), + (180, "## 6.2.1 POLICIES FOR HEMODIALYSIS\n\n### 6.2.1.9 Temporary Access\n\nFemoral line."), + (371, "# 7. PATIENT RIGHTS AND EDUCATION\n\n## 7.1 Privacy\n\nScreens are used."), + ] + hemodialysis = ["6. PROTOCOLS", "6.2 PROCEDURES IN DIALYSIS", "6.2.1 HEMODIALYSIS"] + access = [*hemodialysis, "6.2.1.4 PROTOCOL FOR VASCULAR ACCESS"] + paths = {section.title: section.hierarchy_path for section in sectionize(pages)} + self.assertEqual(paths["7. HYPERKALEMIA"], [*access, "7. HYPERKALEMIA"]) + self.assertEqual(paths["CENTRAL VENOUS CATHETER BLOCK"], [*access, "CENTRAL VENOUS CATHETER BLOCK"]) + self.assertEqual( + paths["6.2.1.5 HEPATITIS B VACCINATION"], [*hemodialysis, "6.2.1.5 HEPATITIS B VACCINATION"] + ) + self.assertEqual(paths["6.2.1.9 Temporary Access"], [*hemodialysis, "6.2.1.9 Temporary Access"]) + self.assertNotIn("6.2.1 POLICIES FOR HEMODIALYSIS", paths) + + def test_a_lettered_running_header_of_the_open_section_is_dropped(self): + policies = "6.2.1 POLICIES FOR MANAGEMENT OF HEMODIALYSIS" + introduction = ( + "### 6.2.1HEMODIALYSIS\n\n#### 6.2.1.aIntroduction to the unit\n\nThe unit has 40 machines." + ) + icu = ( + "# ICU dialysis\n\nBedside dialysis.\n\n# 6.2.1.b POLICIES FOR MANAGEMENT OF HEMODIALYSIS\n\n" + "## 6.2.1.1 PROTOCOL FOR INITIATING\n\nCheck consent." + ) + pages = [ + (155, introduction), + (157, icu), + (179, f"## {policies}\n\n### 6.2.1.8 Emergency Dialysis\n\nDialyse within an hour."), + (180, f"## {policies}\n\n### 6.2.1.9 Temporary Vascular Access\n\nUse a femoral line."), + ] + sections = sectionize(pages) + self.assertEqual( + [section.title for section in sections], + [ + "6.2.1 HEMODIALYSIS", + "6.2.1.aIntroduction to the unit", + "ICU dialysis", + "6.2.1.1 PROTOCOL FOR INITIATING", + "6.2.1.8 Emergency Dialysis", + "6.2.1.9 Temporary Vascular Access", + ], + ) + self.assertEqual(sections[2].markdown, "Bedside dialysis.") + + def test_a_bold_list_item_numbered_like_the_next_chapter_does_not_take_the_chapter(self): + pages = [ + (91, "# 5. PROCEDURES FOR ACCESS\n\n## 5.1 Policies for access\n\nRegister patients."), + (137, "# 6. PROTOCOLS\n\n## 6.2.1.4 PROTOCOL FOR VASCULAR ACCESS\n\nCannulate with care."), + (169, "**6. PULMONARY EDEMA**\n\nGive oxygen.\n\n**7. HYPERKALEMIA**\n\nGive calcium."), + (170, "## 6.2.1.5 HEPATITIS B VACCINATION\n\nVaccinate."), + ( + 371, + "# **7. PATIENT RIGHTS AND EDUCATION** \n\nRights are displayed.\n\n# **7.1 Privacy**\n\nScreens.", + ), + ] + sections = sectionize(pages) + self.assertEqual( + [section.hierarchy_path for section in sections if section.title.startswith("7")], + [["7. PATIENT RIGHTS AND EDUCATION"], ["7. PATIENT RIGHTS AND EDUCATION", "7.1 Privacy"]], + ) + self.assertIn("**7. HYPERKALEMIA**", sections[3].markdown) + + def test_a_misprinted_chapter_in_a_deep_number_is_read_as_the_current_chapter(self): + kinship = ( + "## 6.5.3. PROCEDURES FOR TESTING\n\n### 6.5.3.1. KINSHIP TESTING PROTOCOL\n\nDNA test.\n\n" + "#### 6.5.3.1.4. Turn-Around-Time:\n\nTwo weeks.\n\n" + "#### 1.5.3.1.5. PATIENT'S RIGHTS & CONFIDENTIALITY\n\nReports are sealed." + ) + endotoxin = ( + "## 6.5.3.2.0. Endotoxin Testing Protocol\n\nLAL assay.\n\n" + "## 1.5.3.2.1 Pre Analytical Phase :\n\nCollect water.\n\n## 6.5.4 RECORDS MAINTAINED\n\nRegisters." + ) + testing = "6.5.3. PROCEDURES FOR TESTING" + protocol = [testing, "6.5.3.1. KINSHIP TESTING PROTOCOL"] + endotoxin_group = [testing, "6.5.3.2. Endotoxin Testing Protocol"] + self.assertEqual( + [section.hierarchy_path for section in sectionize([(356, kinship), (361, endotoxin)])], + [ + [testing], + protocol, + [*protocol, "6.5.3.1.4. Turn-Around-Time:"], + [*protocol, "6.5.3.1.5. PATIENT'S RIGHTS & CONFIDENTIALITY"], + endotoxin_group, + [*endotoxin_group, "6.5.3.2.0. Endotoxin Testing Protocol"], + [*endotoxin_group, "6.5.3.2.1 Pre Analytical Phase :"], + ["6.5.4 RECORDS MAINTAINED"], + ], + ) + + def test_html_tags_are_stripped_from_heading_titles(self): + markdown = ( + "## 2.3. STAFF LIST as on 24th May 2018\n\nNames.\n\n" + "## **2.4 CODE RED PROTOCOL** \n\nEvacuate." + ) + self.assertEqual( + [section.title for section in sectionize([(29, markdown)])], + ["2.3. STAFF LIST as on 24th May 2018", "2.4 CODE RED PROTOCOL"], + ) + class TestEmptySectionsAreFlagged(FrappeTestCase): def test_section_without_markdown_is_chunked_as_title_only(self): From 48236360c9c6fe0df613b04ff335c11e32b3ed0c Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 08:36:38 +0530 Subject: [PATCH 23/33] fix(page-plan): keep short numbered siblings as pages and never split a page at a letterhead --- wikify/engine/loader/page_plan.py | 28 +++++++++++--- wikify/tests/test_page_plan.py | 62 +++++++++++++++++++++++++++---- 2 files changed, 77 insertions(+), 13 deletions(-) diff --git a/wikify/engine/loader/page_plan.py b/wikify/engine/loader/page_plan.py index 9e1db45..117dfeb 100644 --- a/wikify/engine/loader/page_plan.py +++ b/wikify/engine/loader/page_plan.py @@ -25,7 +25,7 @@ MAX_PAGE_PDF_PAGES = 6 OUTLINE_BATCH_LINES = 250 -_LIST_ITEM_TITLE = re.compile(r"^(?:\d+[.)]?|[A-Za-z][.)]|[IVXivx]+[.)])\s") +_LIST_ITEM_TITLE = re.compile(r"^(?:\d+[.)]?|[A-Za-z][.)]|[IVXivx]+[.)]|[Ss]tep\s+\d+[:.)]?)\s") _PROMPT = ( "You are planning how a PDF document becomes a wiki. Below is its section outline in " @@ -160,7 +160,7 @@ def keep_sibling_order( sections: list[Section], parents: list[int | None], numbers: list[tuple[int, ...] | None], pages: set[int] ) -> tuple[set[int], list[int | None]]: """A numbered sibling after a page must not fold into the parent, which would show it before that - page: it becomes a page, or joins the last page before it when it is only a fragment.""" + page: it becomes a page, or joins the last page before it when it is only a list-item fragment.""" parents = list(parents) subtree_words = [len(section.markdown.split()) for section in sections] for index in reversed(range(len(sections))): @@ -176,7 +176,10 @@ def keep_sibling_order( for sibling in siblings: if sibling in pages: previous_page = sibling - elif previous_page is not None and subtree_words[sibling] >= FRAGMENT_WORDS: + elif previous_page is not None and ( + subtree_words[sibling] >= FRAGMENT_WORDS + or not _LIST_ITEM_TITLE.match(sections[sibling].title) + ): pages.add(sibling) previous_page = sibling elif previous_page is not None: @@ -229,8 +232,17 @@ def split_long_pages( def heading_parts(sections: list[Section], children: list[int], subtree_words: list[int]) -> list[list[int]]: """Runs of children that each start at a sub-heading and hold at least MIN_SPLIT_PART_WORDS; a - tiny run joins the run before it, or the run after it when it is a bare heading.""" - starts = [child for child in children if not _LIST_ITEM_TITLE.match(sections[child].title)] + tiny run joins the run before it, or the run after it when it is a bare heading. A title the + document repeats, or a stack of bare headings, is a form letterhead, never a part's title.""" + title_counts = Counter(section.title.lower() for section in sections) + bare = [not sections[child].markdown.strip() for child in children] + starts = [ + child + for position, child in enumerate(children) + if not _LIST_ITEM_TITLE.match(sections[child].title) + and title_counts[sections[child].title.lower()] == 1 + and not in_bare_stack(bare, position) + ] if len(starts) < 2: starts = children parts: list[list[int]] = [] @@ -257,6 +269,12 @@ def heading_parts(sections: list[Section], children: list[int], subtree_words: l return merged if len(merged) > 1 else [] +def in_bare_stack(bare: list[bool], position: int) -> bool: + before = position > 0 and bare[position - 1] + after = position + 1 < len(bare) and bare[position + 1] + return bare[position] and (before or after) + + def fold_sections(sections: list[Section], parents: list[int | None], pages: set[int]) -> list[Section]: owner: dict[int, int] = {} depth_below_page: dict[int, int] = {} diff --git a/wikify/tests/test_page_plan.py b/wikify/tests/test_page_plan.py index 22e957c..9fdc244 100644 --- a/wikify/tests/test_page_plan.py +++ b/wikify/tests/test_page_plan.py @@ -177,8 +177,9 @@ def test_an_empty_page_without_page_children_folds_into_its_parent(self): def test_unusable_indices_are_ignored(self): pages, _ = self._plan(_llm_reply({"pages": ["2", 99, -1, True, 3]})) - self.assertEqual(_titles(pages), ["Preamble", "5.8 Care", "5.8.4 Sedation", "6.1 Protocols"]) - self.assertIn("5.8.9 Grievances", pages[2].markdown) + self.assertEqual( + _titles(pages), ["Preamble", "5.8 Care", "5.8.4 Sedation", "5.8.9 Grievances", "6.1 Protocols"] + ) def test_invalid_reply_falls_back_to_the_numbering_rule(self): expected = _titles(plan_pages(_outline())) @@ -207,7 +208,15 @@ def test_large_outlines_are_sent_per_chapter(self): self.assertNotIn("6.1 Protocols", chat.call_args_list[0].args[1][0]["content"]) self.assertEqual( _titles(pages), - ["Preamble", "5.8 Care", "5.8.1 Chaperone", "6.1 Protocols", "6.1.2 Lupus nephritis"], + [ + "Preamble", + "5.8 Care", + "5.8.1 Chaperone", + "5.8.4 Sedation", + "5.8.9 Grievances", + "6.1 Protocols", + "6.1.2 Lupus nephritis", + ], ) def test_an_unnumbered_top_level_fragment_folds_into_the_page_before_it(self): @@ -256,9 +265,9 @@ def test_a_page_spanning_too_many_pdf_pages_splits_at_its_numbered_children(self "6.2.2.1.1 Counselling", "6.2.2.1.2 Pre-op evaluation", "6.2.2.1.3 Intra-op", + "6.2.2.2 Note", ], ) - self.assertIn("6.2.2.2 Note", pages[-1].markdown) self.assertNotIn("6.2.2.2 Note", pages[0].markdown) def test_a_long_page_without_numbered_children_splits_at_its_sub_headings(self): @@ -334,7 +343,7 @@ def test_numbered_siblings_follow_the_majority_into_pages(self): pages, _ = self._plan(_llm_reply({"pages": [4]}), sections) self.assertEqual(_titles(pages), ["5.8 Policies on patient care", "5.8.4 Sedation"]) - def test_numbered_siblings_after_a_page_are_pages_so_the_order_holds(self): + def test_numbered_siblings_after_a_page_are_pages_even_when_short(self): sections = [ _sec(["2. Organogram"], 1, markdown=_words(20)), _sec(["2. Organogram", "2.1 The staff"], 1, markdown=_words(200)), @@ -347,7 +356,44 @@ def test_numbered_siblings_after_a_page_are_pages_so_the_order_holds(self): pages, _ = self._plan(_llm_reply({"pages": [0, 2]}), sections) self.assertEqual( _titles(pages), - ["2. Organogram", "2.2 Hierarchy", "2.3 Staff list", "2.4 Laboratory staff", "2.5 Office staff"], + [ + "2. Organogram", + "2.2 Hierarchy", + "2.3 Staff list", + "2.4 Laboratory staff", + "2.5 Office staff", + "2.6 Support staff", + ], + ) + self.assertNotIn("2.6 Support staff", pages[0].markdown + pages[-2].markdown) + + def test_a_long_page_never_splits_at_a_form_letterhead_or_a_step(self): + training = "6.2.2.1.5 PD TRAINING PROCEDURE" + sections = [ + _sec([training], 227, markdown=""), + _sec([training, "CHRISTIAN MEDICAL COLLEGE"], 227, markdown=""), + _sec([training, "DEPARTMENT OF NEPHROLOGY"], 227, markdown=""), + _sec([training, "PD TRAINING OBJECTIVES"], 227, 228, markdown=_words(200)), + _sec([training, "DAY WISE SCHEDULE OF TRAINING"], 229, 231, markdown=_words(300)), + _sec([training, "CHRISTIAN MEDICAL COLLEGE VELLORE"], 232, markdown=""), + _sec([training, "PROCEDURE MANUAL - NEPHROLOGY"], 232, markdown=""), + _sec([training, "PD EXCHANGE PROCEDURE"], 232, 233, markdown=_words(229)), + _sec([training, "Step 11: Check, measure, Record and Discard"], 233, 234, markdown=_words(191)), + _sec([training, "DISCHARGE CHECKLIST"], 239, 240, markdown=_words(193)), + _sec([training, "CONTACT INFORMATION SHEET"], 240, markdown=""), + _sec([training, "CHRISTIAN MEDICAL COLLEGE"], 240, markdown=""), + _sec([training, "DEPARTMENT OF NEPHROLOGY"], 240, markdown=_words(178)), + ] + pages, _ = self._plan(_llm_reply({"pages": []}), sections) + self.assertEqual( + _titles(pages), + [ + training, + "PD TRAINING OBJECTIVES", + "DAY WISE SCHEDULE OF TRAINING", + "PD EXCHANGE PROCEDURE", + "DISCHARGE CHECKLIST", + ], ) - self.assertIn("## 2.6 Support staff", pages[-1].markdown) - self.assertNotIn("2.6 Support staff", pages[0].markdown) + self.assertIn("## Step 11: Check, measure, Record and Discard", pages[3].markdown) + self.assertIn("## CONTACT INFORMATION SHEET", pages[4].markdown) From 515bf51e56b8c849fd0bac26a95322b4a99dbd4e Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 08:45:10 +0530 Subject: [PATCH 24/33] fix(page-plan): fold a numbered stub with almost no text into the page before it --- wikify/engine/loader/page_plan.py | 8 ++++++-- wikify/tests/test_page_plan.py | 8 +++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/wikify/engine/loader/page_plan.py b/wikify/engine/loader/page_plan.py index 117dfeb..d440e1b 100644 --- a/wikify/engine/loader/page_plan.py +++ b/wikify/engine/loader/page_plan.py @@ -19,6 +19,7 @@ FALLBACK_PAGE_NUMBER_DEPTH = 3 FALLBACK_PAGE_TREE_DEPTH = 1 FRAGMENT_WORDS = 40 +STUB_WORDS = 15 LEADING_FRAGMENT_WORDS = 60 MIN_SPLIT_PART_WORDS = 150 MAX_PAGE_WORDS = 2500 @@ -160,7 +161,7 @@ def keep_sibling_order( sections: list[Section], parents: list[int | None], numbers: list[tuple[int, ...] | None], pages: set[int] ) -> tuple[set[int], list[int | None]]: """A numbered sibling after a page must not fold into the parent, which would show it before that - page: it becomes a page, or joins the last page before it when it is only a list-item fragment.""" + page: it becomes a page, or joins the last page before it when it is a stub or a list-item fragment.""" parents = list(parents) subtree_words = [len(section.markdown.split()) for section in sections] for index in reversed(range(len(sections))): @@ -178,7 +179,10 @@ def keep_sibling_order( previous_page = sibling elif previous_page is not None and ( subtree_words[sibling] >= FRAGMENT_WORDS - or not _LIST_ITEM_TITLE.match(sections[sibling].title) + or ( + subtree_words[sibling] >= STUB_WORDS + and not _LIST_ITEM_TITLE.match(sections[sibling].title) + ) ): pages.add(sibling) previous_page = sibling diff --git a/wikify/tests/test_page_plan.py b/wikify/tests/test_page_plan.py index 9fdc244..ea16dad 100644 --- a/wikify/tests/test_page_plan.py +++ b/wikify/tests/test_page_plan.py @@ -34,7 +34,7 @@ def _outline(): _sec(["5.8 Care", "5.8.4 Sedation", "5.8.4.1 Indications"], 4), _sec(["5.8 Care", "5.8.4 Sedation", "5.8.4.1 Indications", "Contraindications"], 4), _sec(["5.8 Care", "5.8.4 Sedation", "5.8.4.2 Consents"], 5, 6), - _sec(["5.8 Care", "5.8.9 Grievances"], 7), + _sec(["5.8 Care", "5.8.9 Grievances"], 7, markdown=_words(30)), _sec(["5.8 Care", "5.8.9 Grievances", "1. Drug Regimen"], 7), _sec(["6.1 Protocols"], 8, markdown=""), _sec(["6.1 Protocols", "6.1.2 Lupus nephritis"], 8), @@ -265,9 +265,9 @@ def test_a_page_spanning_too_many_pdf_pages_splits_at_its_numbered_children(self "6.2.2.1.1 Counselling", "6.2.2.1.2 Pre-op evaluation", "6.2.2.1.3 Intra-op", - "6.2.2.2 Note", ], ) + self.assertIn("6.2.2.2 Note", pages[-1].markdown) self.assertNotIn("6.2.2.2 Note", pages[0].markdown) def test_a_long_page_without_numbered_children_splits_at_its_sub_headings(self): @@ -351,7 +351,8 @@ def test_numbered_siblings_after_a_page_are_pages_even_when_short(self): _sec(["2. Organogram", "2.3 Staff list"], 2, markdown=_words(200)), _sec(["2. Organogram", "2.4 Laboratory staff"], 3, markdown=_words(60)), _sec(["2. Organogram", "2.5 Office staff"], 3, markdown=_words(60)), - _sec(["2. Organogram", "2.6 Support staff"], 3, markdown=_words(10)), + _sec(["2. Organogram", "2.6 Support staff"], 3, markdown=_words(25)), + _sec(["2. Organogram", "2.7 Attendants"], 3, markdown=_words(5)), ] pages, _ = self._plan(_llm_reply({"pages": [0, 2]}), sections) self.assertEqual( @@ -366,6 +367,7 @@ def test_numbered_siblings_after_a_page_are_pages_even_when_short(self): ], ) self.assertNotIn("2.6 Support staff", pages[0].markdown + pages[-2].markdown) + self.assertIn("## 2.7 Attendants", pages[-1].markdown) def test_a_long_page_never_splits_at_a_form_letterhead_or_a_step(self): training = "6.2.2.1.5 PD TRAINING PROCEDURE" From c0fba6f4eaf11b7e0d91ef5c648fca17905d6f9a Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 08:56:20 +0530 Subject: [PATCH 25/33] fix(cleanup): strip a running header stitched inside a table and stitch tables only after headers are gone --- wikify/engine/loader/cleanup.py | 68 ++++++++++++++++++++++++++++ wikify/engine/loader/table_stitch.py | 2 +- wikify/engine/remediate.py | 4 +- wikify/tests/test_sectionize.py | 33 ++++++++++++++ 4 files changed, 103 insertions(+), 4 deletions(-) diff --git a/wikify/engine/loader/cleanup.py b/wikify/engine/loader/cleanup.py index 4eb4f6b..5821d51 100644 --- a/wikify/engine/loader/cleanup.py +++ b/wikify/engine/loader/cleanup.py @@ -34,6 +34,10 @@ re.compile(r"(?i)^\s*date\s*:"), ] _STRUCTURAL_LINE = re.compile(r"^\s*(?:<|```|~~~|!\[)") +_HTML_TABLE = re.compile(r"]*>.*?", re.IGNORECASE | re.DOTALL) +_HTML_CELL = re.compile(r"]*>(.*?)", re.IGNORECASE | re.DOTALL) +_HTML_TAG = re.compile(r"<[^>]+>") +_TABLE_CELL_SPLIT = re.compile(r"(?") _IMAGE_LINE = re.compile(r"^\s*!\[[^\]]*\]\([^)]*\)\s*$") EDGE_LINES = 4 @@ -167,9 +171,73 @@ def _edge_furniture(lines: list[str], boilerplate: set[str]) -> set[int]: return drop +def _is_header_text(text: str) -> bool: + plain = _plain(text) + return _is_page_of(text) or any(pattern.search(plain) for pattern in _HEADER_FIELDS) + + +def _is_header_cell(cell: str, boilerplate: set[str]) -> bool: + return _is_header_text(cell) or _is_page_furniture(cell, boilerplate, False) + + +def _is_header_block(cells: list[str], boilerplate: set[str]) -> bool: + """Cells that only carry a running header (title, doc code, version, page x of y) and name at least + one field of it, so a body table that merely repeats the document title is never mistaken for one.""" + filled = [cell for cell in cells if _plain(cell)] + return any(_is_header_text(cell) for cell in filled) and all( + _is_header_cell(cell, boilerplate) for cell in filled + ) + + +def _row_cells(line: str) -> list[str]: + return [cell for cell in _TABLE_CELL_SPLIT.split(line.strip()) if cell.strip()] + + +def _is_header_row(line: str, boilerplate: set[str]) -> bool: + cells = [cell for cell in _row_cells(line) if not _SEP_ONLY.match(f"|{cell}|")] + return all(_is_header_cell(cell, boilerplate) for cell in cells) + + +def _strip_html_header_tables(md: str, boilerplate: set[str]) -> str: + def strip(match: re.Match) -> str: + cells = [_HTML_TAG.sub(" ", cell) for cell in _HTML_CELL.findall(match.group(0))] + return "" if _is_header_block(cells, boilerplate) else match.group(0) + + return _HTML_TABLE.sub(strip, md) + + +def _split_at_header_rows(md: str, boilerplate: set[str]) -> list[str]: + """Split a page where a running header sits inside a table, which happens when an earlier pass + stitched the next page's table onto this one before its header was stripped.""" + lines = md.splitlines() + pieces: list[str] = [] + start = index = 0 + while index < len(lines): + end = index + while end < len(lines) and _is_table_row(lines[end]) and _is_header_row(lines[end], boilerplate): + end += 1 + if end > index and _is_header_block( + [cell for line in lines[index:end] for cell in _row_cells(line)], boilerplate + ): + pieces.append("\n".join(lines[start:index])) + start = end + index = max(end, index + 1) + pieces.append("\n".join(lines[start:])) + return pieces + + +def _strip_header_rows(md: str, boilerplate: set[str]) -> str: + pieces = _split_at_header_rows(md, boilerplate) + if len(pieces) == 1: + return md + rejoined = stitch_cross_page_tables(list(enumerate(pieces))) + return "\n\n".join(piece for _, piece in rejoined if piece.strip()) + + def strip_boilerplate(pages: list[tuple[int, str]], boilerplate: set[str]) -> list[tuple[int, str]]: out: list[tuple[int, str]] = [] for pno, md in pages: + md = _strip_header_rows(_strip_html_header_tables(md, boilerplate), boilerplate) lines = [line for line in _strip_footer_blocks(md).splitlines() if not _is_page_of(line)] drop = _edge_furniture(lines, boilerplate) kept = [line for index, line in enumerate(lines) if index not in drop] diff --git a/wikify/engine/loader/table_stitch.py b/wikify/engine/loader/table_stitch.py index 66aff8d..847a6d2 100644 --- a/wikify/engine/loader/table_stitch.py +++ b/wikify/engine/loader/table_stitch.py @@ -17,7 +17,7 @@ _SEP = re.compile(r"^\s*\|[\s:|-]+\|\s*$") _CELL_SPLIT = re.compile(r"(?]*>.*?", re.IGNORECASE | re.DOTALL) _INLINE_MARKS = {"strong": "**", "b": "**", "em": "*", "i": "*"} _KEPT_INLINE_TAGS = ("sup", "sub") diff --git a/wikify/engine/remediate.py b/wikify/engine/remediate.py index 88d3d62..43b23ac 100644 --- a/wikify/engine/remediate.py +++ b/wikify/engine/remediate.py @@ -7,7 +7,6 @@ from wikify.engine import diagrams, images, llm, pdf_utils, regions, settings, store from wikify.engine.loader.cleanup_llm import clean_markdown -from wikify.engine.loader.table_stitch import stitch_cross_page_tables from wikify.engine.parsers import vlm from wikify.engine.sectionize import rebuild_and_classify from wikify.engine.verify import deterministic as det @@ -202,11 +201,10 @@ def remediate_pdf( # noqa: C901 if progress_cb: progress_cb(i + 1, total) - stitched = dict(stitch_cross_page_tables([(p["page_no"], canon_md[p["page_no"]]) for p in pages])) with events.suspended_indexing(): for p in pages: pno = p["page_no"] - store.set_canonical(p["name"], stitched[pno], canon_comp[pno], canon_src[pno]) + store.set_canonical(p["name"], canon_md[pno], canon_comp[pno], canon_src[pno]) comps = [c for c in canon_comp.values() if c is not None] canonical_mean = round(sum(comps) / len(comps), 3) if comps else None diff --git a/wikify/tests/test_sectionize.py b/wikify/tests/test_sectionize.py index a847979..363594b 100644 --- a/wikify/tests/test_sectionize.py +++ b/wikify/tests/test_sectionize.py @@ -788,6 +788,39 @@ def test_html_tags_are_stripped_from_heading_titles(self): ["2.3. STAFF LIST as on 24th May 2018", "2.4 CODE RED PROTOCOL"], ) + def test_clean_pages_drops_a_running_header_stitched_inside_a_table(self): + header = "NEPHROLOGY MANUAL\n\nMAN/RPT/NEPH/001 | Ver.: 1 | Issue : 1 | Pg. {page} of 398\n\n" + timeline = ( + "| Year | Milestone |\n|---|---|\n" + "| 2005 | Nephrology Laboratory Started |\n" + "| 2007 | DNA fingerprinting facility established to ascertain genetic relationship between |\n" + "||NEPHROLOGY MANUAL
|\n" + "||**MAN/RPT/NEPH/001**
**Ver.: 1**
**Issue : 1**
**Date:10/11/2023**
Pg.**14 of 398**|\n" + "||transplant patient & prospective donor|\n" + "|2008|First CAPD catheterization done by percutaneous method|" + ) + header_box = ( + '\n\n\n\n\n' + "\n\n\n\n" + "\n
NEPHROLOGY MANUAL
MAN/RPT/NEPH/001Ver.: 1Issue : 1Pg. 15 of 398
\n\n" + ) + pages = [ + (13, header.format(page=13) + timeline), + (14, header.format(page=14) + "# Education\n\nTwo programmes."), + (15, header_box + "Every staff member keeps this manual correct."), + (16, header.format(page=16) + "Closing notes."), + ] + cleaned = dict(clean_pages(pages)) + self.assertEqual( + cleaned[13], + "| Year | Milestone |\n|---|---|\n" + "| 2005 | Nephrology Laboratory Started |\n" + "| 2007 | DNA fingerprinting facility established to ascertain genetic relationship between " + "transplant patient & prospective donor |\n" + "|2008|First CAPD catheterization done by percutaneous method|", + ) + self.assertEqual(cleaned[15], "Every staff member keeps this manual correct.") + class TestEmptySectionsAreFlagged(FrappeTestCase): def test_section_without_markdown_is_chunked_as_title_only(self): From 12d54653a013b2bd30244378a23a5d1f4b7f1c8f Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 08:59:28 +0530 Subject: [PATCH 26/33] fix(table-stitch): carry a table on across a page that dropped its empty column and read lists in html cells --- wikify/engine/loader/table_stitch.py | 69 ++++++++++++++++++++++++---- wikify/tests/test_sectionize.py | 27 +++++++++++ 2 files changed, 88 insertions(+), 8 deletions(-) diff --git a/wikify/engine/loader/table_stitch.py b/wikify/engine/loader/table_stitch.py index 847a6d2..75e92fb 100644 --- a/wikify/engine/loader/table_stitch.py +++ b/wikify/engine/loader/table_stitch.py @@ -22,6 +22,7 @@ _INLINE_MARKS = {"strong": "**", "b": "**", "em": "*", "i": "*"} _KEPT_INLINE_TAGS = ("sup", "sub") _WRAPPER_TAGS = ("thead", "tbody", "tfoot", "span", "p", "u") +_LIST_TAGS = ("ul", "ol") MIN_NUMBERED_ROWS = 2 @@ -35,6 +36,7 @@ def __init__(self): self.saw_row_tag = False self.table_depth = 0 self.convertible = True + self.list_counters: list[int | None] = [] def handle_starttag(self, tag, attrs): if tag == "table": @@ -45,19 +47,15 @@ def handle_starttag(self, tag, attrs): self.saw_row_tag = True self.start_row() elif tag in ("td", "th"): - if any(name in ("rowspan", "colspan") and (value or "1").strip() != "1" for name, value in attrs): - self.convertible = False - self.close_cell() - if not self.row_open: - self.start_row() - self.cell = [] - self.header_flags[-1].append(tag == "th") + self.start_cell(tag, attrs) elif tag == "br": self.append("
") elif tag in _INLINE_MARKS: self.append(_INLINE_MARKS[tag]) elif tag in _KEPT_INLINE_TAGS: self.append(f"<{tag}>") + elif tag in (*_LIST_TAGS, "li") and self.cell is not None: + self.start_list_tag(tag) elif tag not in _WRAPPER_TAGS: self.convertible = False @@ -71,6 +69,8 @@ def handle_endtag(self, tag): self.append(_INLINE_MARKS[tag]) elif tag in _KEPT_INLINE_TAGS: self.append(f"") + elif tag in _LIST_TAGS and self.list_counters: + self.list_counters.pop() def handle_data(self, data): if self.cell is not None: @@ -82,6 +82,31 @@ def append(self, text: str): if self.cell is not None: self.cell.append(text) + def start_cell(self, tag: str, attrs): + if any(name in ("rowspan", "colspan") and (value or "1").strip() != "1" for name, value in attrs): + self.convertible = False + self.close_cell() + if not self.row_open: + self.start_row() + self.cell = [] + self.header_flags[-1].append(tag == "th") + + def start_list_tag(self, tag: str): + if tag in _LIST_TAGS: + self.list_counters.append(0 if tag == "ol" else None) + elif self.list_counters: + self.start_list_item() + + def start_list_item(self): + counter = self.list_counters[-1] + if counter is None: + marker = "•" + else: + self.list_counters[-1] = counter = counter + 1 + marker = f"{counter}." + text = "".join(self.cell or []).rstrip() + self.cell = [f"{text}
{marker} " if text else f"{marker} "] + def start_row(self): self.rows.append([]) self.header_flags.append([]) @@ -234,6 +259,29 @@ def _leading_table(md: str): return lines[:j], lines[j:] # (table_lines, after) +def _empty_column(table: list[str]) -> int | None: + """The first column with no text in any body row: the column a parser drops when a table goes on + over a page break, since nothing on the continuation page shows it is there.""" + body = table[2:] if len(table) > 1 and _SEP.match(table[1]) else table + rows = [_cells(row) for row in body] + if not rows: + return None + return next( + ( + column + for column in range(_ncols(table[0])) + if not any(_plain_cell(row[column]) for row in rows if column < len(row)) + ), + None, + ) + + +def _with_blank_cell(row: str, column: int) -> str: + cells = _cells(row) + cells.insert(column, "") + return _format_row(cells) + + def stitch_cross_page_tables(pages: list[tuple[int, str]]) -> list[tuple[int, str]]: out = [[pno, md] for pno, md in pages] for k in range(len(out) - 1): @@ -243,12 +291,17 @@ def stitch_cross_page_tables(pages: list[tuple[int, str]]) -> list[tuple[int, st continue a_before, a_tbl = a b_tbl, b_after = b - if not a_tbl or not b_tbl or _ncols(a_tbl[0]) != _ncols(b_tbl[0]): + if not a_tbl or not b_tbl: + continue + dropped_column = _empty_column(a_tbl) if _ncols(b_tbl[0]) == _ncols(a_tbl[0]) - 1 else None + if _ncols(a_tbl[0]) != _ncols(b_tbl[0]) and dropped_column is None: continue cont = b_tbl if len(b_tbl) >= 2 and _SEP.match(b_tbl[1]): repeated = _header_key(b_tbl[0]) in ("", _header_key(a_tbl[0])) cont = b_tbl[2:] if repeated else [b_tbl[0], *b_tbl[2:]] + if dropped_column is not None: + cont = [_with_blank_cell(row, dropped_column) for row in cont] if cont and not _SEP.match(a_tbl[-1]) and _is_numbered(a_tbl) and _continues(cont[0], True): a_tbl = [*a_tbl[:-1], _merge_rows(a_tbl[-1], cont[0])] cont = cont[1:] diff --git a/wikify/tests/test_sectionize.py b/wikify/tests/test_sectionize.py index 363594b..d13688b 100644 --- a/wikify/tests/test_sectionize.py +++ b/wikify/tests/test_sectionize.py @@ -821,6 +821,33 @@ def test_clean_pages_drops_a_running_header_stitched_inside_a_table(self): ) self.assertEqual(cleaned[15], "Every staff member keeps this manual correct.") + def test_clean_pages_stitches_a_continuation_that_lost_an_empty_column_and_its_header(self): + checklist = ( + "### PRE-OPERATIVE CHECKLIST FOR PD NURSE\n\n\n\n\n\n\n\n" + "\n\n" + "\n\n\n" + "\n\n\n\n\n
Tick (√)
PD education material given in patient's own language\n
    \n" + "
  • Introductory PD pamphlet
  • \n
  • PD patient information sheet
  • \n
\n
Patient's ability to do CAPD ensured
\n\n212" + ) + continuation = ( + "| Care-giver identified | Specify |\n|---|---|\n" + "| Dedicated PD area at home identified | |\n" + "| Date of surgery decided | Date: |\n\n" + "Signature of the PD nurse." + ) + cleaned = dict(clean_pages([(212, checklist), (213, continuation)])) + self.assertEqual( + cleaned[212], + "### PRE-OPERATIVE CHECKLIST FOR PD NURSE\n\n| | Tick (√) | |\n|---|---|---|\n" + "| PD education material given in patient's own language
• Introductory PD pamphlet" + "
• PD patient information sheet | | |\n" + "| Patient's ability to do CAPD ensured | | |\n" + "| Care-giver identified | | Specify |\n" + "| Dedicated PD area at home identified | | |\n" + "| Date of surgery decided | | Date: |", + ) + self.assertEqual(cleaned[213], "Signature of the PD nurse.") + class TestEmptySectionsAreFlagged(FrappeTestCase): def test_section_without_markdown_is_chunked_as_title_only(self): From 38f96f3b17c930a8023930dc05668a81cabb8e64 Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 09:01:25 +0530 Subject: [PATCH 27/33] fix(cleanup): lay out contents lines as the contents table they continue --- wikify/engine/loader/cleanup.py | 37 ++++++++++++++++++++++++++++++++- wikify/tests/test_sectionize.py | 26 +++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/wikify/engine/loader/cleanup.py b/wikify/engine/loader/cleanup.py index 5821d51..a0d7b35 100644 --- a/wikify/engine/loader/cleanup.py +++ b/wikify/engine/loader/cleanup.py @@ -60,6 +60,8 @@ _NUMBERED_HEADING = re.compile(r"^\s*#{1,6}\s+[*_]*\d+(?:\.\d+)*\.?\s*[A-Za-z]") _LIST_ITEM = re.compile(r"^(\s*)([-*+]|\d+[.)])\s+\S") _ENUMERATED_TEXT = re.compile(r"^(\d+)[.)]\s") +_CONTENTS_LINE = re.compile(r"^(\d+(?:\.\d+)+\.?|\d+)\s+(\S.*?)\s*(?:\.{2,}\s*)?(\d{1,4})\s*$") +MIN_CONTENTS_LINES = 5 def _plain(line: str) -> str: @@ -331,9 +333,42 @@ def reindent_list_continuations(pages: list[tuple[int, str]]) -> list[tuple[int, return [(page_no, "\n".join(lines)) for (page_no, _), lines in zip(pages, page_lines, strict=True)] +def _contents_run(lines: list[str], start: int) -> tuple[int, list[re.Match]]: + entries: list[re.Match] = [] + end = start + while end < len(lines): + entry = _CONTENTS_LINE.match(lines[end].strip()) + if lines[end].strip() and not entry: + break + if entry: + entries.append(entry) + end += 1 + return end, entries + + +def contents_lines_to_table(md: str) -> str: + """Lay out a run of table-of-contents lines (number, title, page) as a headless table, so the page + stitches onto the contents table the pages around it were parsed as.""" + lines = md.splitlines() + out: list[str] = [] + index = 0 + while index < len(lines): + end, entries = _contents_run(lines, index) + if len(entries) < MIN_CONTENTS_LINES: + out.append(lines[index]) + index += 1 + continue + out.extend(["| | | |", "|---|---|---|"]) + out.extend(f"| {entry.group(1)} | {entry.group(2)} | {entry.group(3)} |" for entry in entries) + out.append("") + index = end + return "\n".join(out) + + def clean_pages(pages: list[tuple[int, str]]) -> list[tuple[int, str]]: pages = [(page_no, html_tables_to_markdown(md)) for page_no, md in pages] - stitched = stitch_cross_page_tables(strip_boilerplate(pages, find_boilerplate(pages))) + stripped = strip_boilerplate(pages, find_boilerplate(pages)) + stitched = stitch_cross_page_tables([(page_no, contents_lines_to_table(md)) for page_no, md in stripped]) joined = join_page_breaks([(page_no, merge_continuation_rows(md)) for page_no, md in stitched]) return reindent_list_continuations(joined) diff --git a/wikify/tests/test_sectionize.py b/wikify/tests/test_sectionize.py index d13688b..e147b57 100644 --- a/wikify/tests/test_sectionize.py +++ b/wikify/tests/test_sectionize.py @@ -848,6 +848,32 @@ def test_clean_pages_stitches_a_continuation_that_lost_an_empty_column_and_its_h ) self.assertEqual(cleaned[213], "Signature of the PD nurse.") + def test_clean_pages_lays_contents_lines_out_as_the_contents_table_they_continue(self): + contents_table = ( + "# CONTENTS\n\n| S.No | CONTENTS | Pg No |\n|---|---|---|\n" + "| 3.5.4 | Fellow in Interventional Nephrology | 46 |\n" + "| 3.5.5 | Responsibilities of Dialysis Registrar | 47 |" + ) + contents_lines = ( + "3.5.6 Transplant registrar 50\n3.5.7 Fellow in Renal Transplantation 51\n3.6 Nursing staff 59\n" + "3.6.1 Nurse manager 57\n3.9.1 Hospital Attendant -Posted in clinical Area (A.K. Lab) 88\n" + "4 Records maintained 93" + ) + cleaned = dict(clean_pages([(2, contents_table), (3, contents_lines)])) + self.assertEqual( + cleaned[2], + "# CONTENTS\n\n| S.No | CONTENTS | Pg No |\n|---|---|---|\n" + "| 3.5.4 | Fellow in Interventional Nephrology | 46 |\n" + "| 3.5.5 | Responsibilities of Dialysis Registrar | 47 |\n" + "| 3.5.6 | Transplant registrar | 50 |\n" + "| 3.5.7 | Fellow in Renal Transplantation | 51 |\n" + "| 3.6 | Nursing staff | 59 |\n" + "| 3.6.1 | Nurse manager | 57 |\n" + "| 3.9.1 | Hospital Attendant -Posted in clinical Area (A.K. Lab) | 88 |\n" + "| 4 | Records maintained | 93 |", + ) + self.assertEqual(cleaned[3], "") + class TestEmptySectionsAreFlagged(FrappeTestCase): def test_section_without_markdown_is_chunked_as_title_only(self): From 69406696703ea8ec966c29b43e98aa3baa2e7a77 Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 09:03:47 +0530 Subject: [PATCH 28/33] fix(cleanup): keep one copy of a figure the parser also transcribed --- wikify/engine/loader/cleanup.py | 63 +++++++++++++++++++++++++++++++-- wikify/tests/test_images.py | 36 +++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/wikify/engine/loader/cleanup.py b/wikify/engine/loader/cleanup.py index a0d7b35..387ee85 100644 --- a/wikify/engine/loader/cleanup.py +++ b/wikify/engine/loader/cleanup.py @@ -62,6 +62,11 @@ _ENUMERATED_TEXT = re.compile(r"^(\d+)[.)]\s") _CONTENTS_LINE = re.compile(r"^(\d+(?:\.\d+)+\.?|\d+)\s+(\S.*?)\s*(?:\.{2,}\s*)?(\d{1,4})\s*$") MIN_CONTENTS_LINES = 5 +_MERMAID_OPEN = re.compile(r"^\s*```\s*mermaid\b") +_FENCE_CLOSE = re.compile(r"^\s*```\s*$") +MAX_TABLE_TITLE_LINES = 2 +MAX_TABLE_TITLE_CHARS = 120 +MIN_TRANSCRIBED_ROWS = 4 def _plain(line: str) -> str: @@ -365,10 +370,64 @@ def contents_lines_to_table(md: str) -> str: return "\n".join(out) +def _table_rows_from(lines: list[str], start: int) -> int: + if lines[start].lstrip().startswith(" tuple[str, int, int] | None: + """What the parser wrote for the picture straight after it: ("mermaid", fence start, fence end) for a + diagram redrawn from it, ("table", first line, rows) for a table typed out under its title.""" + following = [index for index in range(figure + 1, len(lines)) if lines[index].strip()] + if not following: + return None + first = following[0] + if _MERMAID_OPEN.match(lines[first]): + close = next((index for index in following[1:] if _FENCE_CLOSE.match(lines[index])), None) + return ("mermaid", first, close) if close is not None else None + for index in following[: MAX_TABLE_TITLE_LINES + 1]: + if _is_table_row(lines[index]) or lines[index].lstrip().startswith(" MAX_TABLE_TITLE_CHARS or _STRUCTURAL_LINE.match(lines[index]): + return None + return None + + +def drop_transcribed_figures(md: str) -> str: + """Keep one copy of a picture the parser also transcribed: the crop for a diagram it redrew as mermaid, + the typed table for a table printed as an image, since that copy is exact and searchable.""" + lines = md.splitlines() + drop: set[int] = set() + for index, line in enumerate(lines): + transcription = _IMAGE_LINE.match(line) and _transcription_after_figure(lines, index) + if not transcription: + continue + kind, start, end = transcription + if kind == "mermaid": + drop.update(range(start, end + 1)) + elif end >= MIN_TRANSCRIBED_ROWS: + drop.add(index) + if not drop: + return md + return "\n".join(line for index, line in enumerate(lines) if index not in drop) + + def clean_pages(pages: list[tuple[int, str]]) -> list[tuple[int, str]]: pages = [(page_no, html_tables_to_markdown(md)) for page_no, md in pages] - stripped = strip_boilerplate(pages, find_boilerplate(pages)) - stitched = stitch_cross_page_tables([(page_no, contents_lines_to_table(md)) for page_no, md in stripped]) + stripped = [ + (page_no, contents_lines_to_table(drop_transcribed_figures(md))) + for page_no, md in strip_boilerplate(pages, find_boilerplate(pages)) + ] + stitched = stitch_cross_page_tables(stripped) joined = join_page_breaks([(page_no, merge_continuation_rows(md)) for page_no, md in stitched]) return reindent_list_continuations(joined) diff --git a/wikify/tests/test_images.py b/wikify/tests/test_images.py index 68acd3a..f9b553a 100644 --- a/wikify/tests/test_images.py +++ b/wikify/tests/test_images.py @@ -11,6 +11,7 @@ from wikify.engine import images, parse_pdf, remediate_pdf from wikify.engine import settings as engine_settings +from wikify.engine.loader.cleanup import drop_transcribed_figures from wikify.tests import _cleanup from wikify.tests.test_remediate_pipeline import _fake_chat @@ -121,6 +122,41 @@ def test_a_figure_already_in_the_markdown_is_not_added_twice(self): self.assertEqual(images.place_figures(markdown, [chart_figure()], 2), markdown) +class TestTranscribedFigures(FrappeTestCase): + def test_keeps_the_crop_of_a_redrawn_chart_and_the_typed_copy_of_a_table_image(self): + org_chart = ( + "![Figure 28.1](/private/files/page-0028-crop.png)\n\n" + '```mermaid\nflowchart TD\nA["Head of the Department"]\nB["Clinical"]\nA --> B\n```\n\n28' + ) + table_image = ( + "## DOSES OF IP ANTIBIOTICS\n\n![Figure 251.1](/private/files/page-0251-crop.png)\n\n" + "**TABLE 5**\nIntraperitoneal Antibiotic Dosing Recommendations\n\n" + "| | Intermittent | Continuous |\n|---|---|---|\n" + "| Amikacin | 2 mg/kg daily | LD 25 mg/L |\n| Gentamicin | 0.6 mg/kg daily | LD 8 mg/L |\n" + "| Cefazolin | 15 mg/kg daily | LD 500 mg/L |" + ) + + self.assertEqual( + drop_transcribed_figures(org_chart), "![Figure 28.1](/private/files/page-0028-crop.png)\n\n\n28" + ) + self.assertEqual( + drop_transcribed_figures(table_image), + "## DOSES OF IP ANTIBIOTICS\n\n\n**TABLE 5**\nIntraperitoneal Antibiotic Dosing Recommendations\n\n" + "| | Intermittent | Continuous |\n|---|---|---|\n" + "| Amikacin | 2 mg/kg daily | LD 25 mg/L |\n| Gentamicin | 0.6 mg/kg daily | LD 8 mg/L |\n" + "| Cefazolin | 15 mg/kg daily | LD 500 mg/L |", + ) + + def test_keeps_a_chart_followed_by_a_paragraph_and_a_table(self): + markdown = ( + "![Figure 3: Admissions](/private/files/chart.png)\n\n" + f"{TEXT_BEFORE} {TEXT_AFTER} {TEXT_BEFORE} {TEXT_AFTER}\n\n" + "| Year | Admissions |\n|---|---|\n| 2019 | 40 |\n| 2020 | 52 |\n| 2021 | 61 |" + ) + + self.assertEqual(drop_transcribed_figures(markdown), markdown) + + class TestFigureRemediation(FrappeTestCase): def setUp(self): real_get = engine_settings.get From dd5c445f90c95ee4c08f11edc4b5315f7cb507c6 Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 09:04:26 +0530 Subject: [PATCH 29/33] fix(images): never cut a text line in half at a figure crop's edge --- wikify/engine/images.py | 37 +++++++++++++++++++++++++++++-------- wikify/tests/test_images.py | 15 +++++++++++++++ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/wikify/engine/images.py b/wikify/engine/images.py index 4f4990c..53f843b 100644 --- a/wikify/engine/images.py +++ b/wikify/engine/images.py @@ -4,6 +4,8 @@ from collections import Counter from dataclasses import dataclass +import fitz + from wikify.engine import pdf_utils, regions, store FIGURE_DPI = 200 @@ -143,15 +145,34 @@ def find_figures(page, repeated: set[bytes]) -> list[Figure]: return figures -def crop_figure(page, figure: Figure) -> bytes: +def text_line_boxes(page) -> list[tuple]: + return [ + tuple(float(value) for value in line["bbox"]) + for block in page.get_text("dict")["blocks"] + if block["type"] == 0 + for line in block["lines"] + if "".join(span["text"] for span in line["spans"]).strip() + ] + + +def figure_clip(page, figure: Figure) -> fitz.Rect: + """The padded figure box, moved off any text line its top or bottom edge would cut in half: a line + mostly inside the picture is taken in whole, a line outside it (a title above) is left out whole.""" box = figure.bbox - clip = page.rect & ( - box[0] - CROP_PADDING, - box[1] - CROP_PADDING, - box[2] + CROP_PADDING, - box[3] + CROP_PADDING, - ) - return pdf_utils.render_png(page, dpi=FIGURE_DPI, clip=clip) + top, bottom = box[1] - CROP_PADDING, box[3] + CROP_PADDING + for line in text_line_boxes(page): + if not overlaps_horizontally(line, box): + continue + inside = max(0.0, min(line[3], box[3]) - max(line[1], box[1])) / max(line[3] - line[1], 1.0) + if line[1] < top < line[3]: + top = line[1] if inside >= 0.5 else line[3] + if line[1] < bottom < line[3]: + bottom = line[3] if inside >= 0.5 else line[1] + return page.rect & fitz.Rect(box[0] - CROP_PADDING, top, box[2] + CROP_PADDING, bottom) + + +def crop_figure(page, figure: Figure) -> bytes: + return pdf_utils.render_png(page, dpi=FIGURE_DPI, clip=figure_clip(page, figure)) def page_figures(page, repeated: set[bytes], page_name: str, page_no: int) -> list[Figure]: diff --git a/wikify/tests/test_images.py b/wikify/tests/test_images.py index f9b553a..bc5eed4 100644 --- a/wikify/tests/test_images.py +++ b/wikify/tests/test_images.py @@ -69,6 +69,21 @@ def test_finds_the_body_chart_and_skips_the_logo_repeated_on_every_page(self): self.assertEqual(chart.text_before, TEXT_BEFORE) self.assertEqual(chart.text_after, TEXT_AFTER) + def test_the_crop_leaves_out_a_title_line_its_padding_would_cut_in_half(self): + document = fitz.open() + page = document.new_page(width=612, height=792) + page.insert_text( + (100, CHART_RECT.y0 - 2), "DOSES OF IP ANTIBIOTICS (ISPD 2016 GUIDELINES)", fontsize=11 + ) + page.insert_image(CHART_RECT, pixmap=solid_pixmap(600, 400, (200, 60, 30))) + title_bottom = images.text_line_boxes(page)[0][3] + + clip = images.figure_clip(page, images.find_figures(page, set())[0]) + + self.assertGreater(title_bottom, CHART_RECT.y0 - images.CROP_PADDING) + self.assertEqual(clip.y0, title_bottom) + self.assertEqual(clip.y1, CHART_RECT.y1 + images.CROP_PADDING) + class TestFigurePlacement(FrappeTestCase): def test_places_the_figure_after_the_text_that_precedes_it(self): From 55607370d1d47814828ef54ae9599ce2619d803e Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 09:06:57 +0530 Subject: [PATCH 30/33] fix(sectionize): drop figure labels the parser copied out of a picture into the text after it --- wikify/engine/images.py | 59 +++++++++++++++++++++++++++++++++++++ wikify/engine/sectionize.py | 4 +-- wikify/tests/test_images.py | 36 ++++++++++++++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) diff --git a/wikify/engine/images.py b/wikify/engine/images.py index 53f843b..7e500e1 100644 --- a/wikify/engine/images.py +++ b/wikify/engine/images.py @@ -1,6 +1,7 @@ from __future__ import annotations import re +import unicodedata from collections import Counter from dataclasses import dataclass @@ -21,6 +22,12 @@ ANCHOR_LENGTHS = (60, 25) MIN_ANCHOR_CHARS = 12 EDGE_TOLERANCE = 2.0 +MAX_ECHO_CHARS = 120 +MIN_TEXT_LAYER_CHARS = 200 +_IMAGE_LINE_RE = re.compile(r"^\s*!\[[^\]]*\]\([^)]*\)\s*$") +_ECHO_BLOCKER_RE = re.compile(r"^\s*(?:[|]|```|[-*+]\s|\d+[.)]\s)") +_WORD_RE = re.compile(r"\w{3,}") +_TRANSCRIPTION_START_RE = re.compile(r"^\s*(?:\|| str: return "".join(character.lower() for character in text if character.isalnum()) +def layer_text(text: str) -> str: + return unicodedata.normalize("NFKC", text) + + +def is_echo(line: str, page_text: str, page_words: set[str]) -> bool: + """A short line the parser read off the picture itself: neither the line nor most of its words are + anywhere in the page's text layer, so it can only be a label or caption drawn inside the image.""" + text = line.strip() + if not text or len(text) > MAX_ECHO_CHARS or _ECHO_BLOCKER_RE.match(text): + return False + if normalized(layer_text(text)) in page_text: + return False + words = {word.lower() for word in _WORD_RE.findall(layer_text(text))} + return len(words & page_words) * 2 < len(words) or not words + + +def drop_echoes_after_figures(markdown: str, page_text: str) -> str: + page_words = {word.lower() for word in _WORD_RE.findall(layer_text(page_text))} + page_text = normalized(layer_text(page_text)) + lines = markdown.splitlines() + drop: set[int] = set() + for index, line in enumerate(lines): + if not _IMAGE_LINE_RE.match(line): + continue + echoes = [] + following = index + 1 + while following < len(lines) and ( + not lines[following].strip() or is_echo(lines[following], page_text, page_words) + ): + if lines[following].strip(): + echoes.append(following) + following += 1 + if following == len(lines) or not _TRANSCRIPTION_START_RE.match(lines[following]): + drop.update(echoes) + if not drop: + return markdown + return re.sub(r"\n{3,}", "\n\n", "\n".join(line for index, line in enumerate(lines) if index not in drop)) + + +def drop_figure_echoes(pages: list[tuple[int, str]], pdf_path: str) -> list[tuple[int, str]]: + """Drop the labels and captions a parser copied out of a picture into the text after it. Only on pages + with a text layer to check against, since on a scanned page every line would look drawn.""" + with fitz.open(pdf_path) as pdf_document: + cleaned = [] + for page_no, markdown in pages: + page_text = pdf_document[page_no - 1].get_text() if 0 < page_no <= pdf_document.page_count else "" + if "![" in markdown and len(normalized(layer_text(page_text))) >= MIN_TEXT_LAYER_CHARS: + markdown = drop_echoes_after_figures(markdown, page_text) + cleaned.append((page_no, markdown)) + return cleaned + + def enclosing_block(markdown: str, index: int) -> tuple[int, int] | None: fences = [match.start() for match in _FENCE_RE.finditer(markdown)] for opening, closing in zip(fences[::2], fences[1::2], strict=False): diff --git a/wikify/engine/sectionize.py b/wikify/engine/sectionize.py index 1aa049f..12ad56d 100644 --- a/wikify/engine/sectionize.py +++ b/wikify/engine/sectionize.py @@ -20,7 +20,7 @@ import frappe -from wikify.engine import llm, store +from wikify.engine import images, llm, store from wikify.engine.classify import classify_document from wikify.engine.lint import repair_markdown from wikify.engine.loader.cleanup import clean_pages @@ -32,7 +32,7 @@ def sectionize_document(source_document: str, pdf_path: str, project_context: str = "") -> int: """Rebuild the Source Section tree from the doc's canonical pages. Returns the count.""" level_map = toc_level_map(str(pdf_path)) - pages = clean_pages(store.get_canonical_pages(source_document)) + pages = clean_pages(images.drop_figure_echoes(store.get_canonical_pages(source_document), pdf_path)) sections = plan_pages( sectionize(pages, level_map), project_context, diff --git a/wikify/tests/test_images.py b/wikify/tests/test_images.py index bc5eed4..0307b04 100644 --- a/wikify/tests/test_images.py +++ b/wikify/tests/test_images.py @@ -172,6 +172,42 @@ def test_keeps_a_chart_followed_by_a_paragraph_and_a_table(self): self.assertEqual(drop_transcribed_figures(markdown), markdown) +class TestFigureEchoes(FrappeTestCase): + def test_drops_labels_read_off_the_picture_and_keeps_the_printed_text(self): + body = [ + "Figure 2 . Anatomy of lumbar spine", + "Prep and drape the area after identifying landmarks. Use lidocaine 1% with or without", + "epinephrine to anesthetize the skin and the deeper tissues under the insertion site", + "Assemble needle and manometer. Attach the 3-way stopcock to manometer", + ] + path = Path(tempfile.mkdtemp()) / "procedure.pdf" + document = fitz.open() + page = document.new_page(width=612, height=792) + page.insert_image(CHART_RECT, pixmap=solid_pixmap(600, 400, (200, 60, 30))) + for offset, line in enumerate(body): + page.insert_text((72, 530 + 16 * offset), line, fontsize=10) + document.save(str(path)) + markdown = ( + "![Figure 2 . Anatomy of lumbar spine](/private/files/page-0348-crop.png)\n\n" + "Puncture site (L4-5 interspace)\n\nLevel of posterior superior iliac crests\n\nL5 L4 L3\n\n" + "Figure 2 . Anatomy of lumbar spine\n\n" + "Prep and drape the area after identifying landmarks. Use lidocaine 1% with or without epinephrine " + "to anesthetize the skin and the deeper tissues under the insertion site\n\n" + "Assemble needle and manometer. Attach the 3-way stopcock to manometer" + ) + + [(_, cleaned)] = images.drop_figure_echoes([(1, markdown)], str(path)) + + self.assertEqual( + cleaned, + "![Figure 2 . Anatomy of lumbar spine](/private/files/page-0348-crop.png)\n\n" + "Figure 2 . Anatomy of lumbar spine\n\n" + "Prep and drape the area after identifying landmarks. Use lidocaine 1% with or without epinephrine " + "to anesthetize the skin and the deeper tissues under the insertion site\n\n" + "Assemble needle and manometer. Attach the 3-way stopcock to manometer", + ) + + class TestFigureRemediation(FrappeTestCase): def setUp(self): real_get = engine_settings.get From 13f18be18148b31b04c0ada9bc77cbe830ce9a18 Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 7 Oct 2026 09:08:31 +0530 Subject: [PATCH 31/33] fix(lint): close bold before a trailing bracket and drop boxes read off bullet glyphs --- wikify/engine/lint.py | 28 +++++++++++++++++++++++++++- wikify/tests/test_lint.py | 25 +++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/wikify/engine/lint.py b/wikify/engine/lint.py index ec19725..8cabe38 100644 --- a/wikify/engine/lint.py +++ b/wikify/engine/lint.py @@ -138,6 +138,10 @@ def fix_table_separators(markdown: str) -> str: _RULE_LINE_RE = re.compile(r"^\s*(?:_\s*){3,}$") _HTML_LINE_RE = re.compile(r"^\s* tuple[str, str, str stripped = content.strip() if not any(char.isalnum() for char in stripped): return before + content, "", after + if opener := _TRAILING_OPENER_RE.search(stripped): + stripped, after = stripped[: opener.start()], stripped[opener.start() :] + after if len(stripped) == 1 and stripped.isalpha() and after[:1].islower() and not before[-1:].isalnum(): return before + stripped, "", after if len(stripped) > 2 and stripped[-2] == " " and stripped[-1].isupper() and after[:1].islower(): @@ -211,6 +217,26 @@ def fix_glued_emphasis(markdown: str) -> str: return "\n".join(lines) +def drop_box_bullets(markdown: str) -> str: + """Drop a box a parser read off a bullet glyph (`- ☐ Peritonitis rates`): a bulleted item never needs + a second marker. A list where any item is ticked or offers several boxes is a form and is kept.""" + lines = (markdown or "").split("\n") + prose = set(_prose_lines(lines)) + index = 0 + while index < len(lines): + end = index + while end < len(lines) and end in prose and _LIST_LINE_RE.match(lines[end]): + end += 1 + if end == index: + index += 1 + continue + if not any(_FORM_MARK_RE.search(lines[line]) for line in range(index, end)): + for line in range(index, end): + lines[line] = _BOX_BULLET_RE.sub(r"\1", lines[line]) + index = end + return "\n".join(lines) + + def repair_markdown(markdown: str) -> str: """Every mechanical repair, in one pass, for the assembled section product.""" - return fix_glued_emphasis(escape_fill_in_blanks(fix_table_separators(markdown))) + return drop_box_bullets(fix_glued_emphasis(escape_fill_in_blanks(fix_table_separators(markdown)))) diff --git a/wikify/tests/test_lint.py b/wikify/tests/test_lint.py index bdaa76a..f950afc 100644 --- a/wikify/tests/test_lint.py +++ b/wikify/tests/test_lint.py @@ -22,6 +22,7 @@ fix_glued_emphasis, fix_table_separators, lint_markdown, + repair_markdown, table_artifacts, ) from wikify.engine.loader.sectionizer import Section @@ -129,6 +130,30 @@ def test_bold_glued_to_its_neighbours_is_separated(self): ) +class TestFormattingLeaks(FrappeTestCase): + def test_bold_closing_after_a_bracket_and_boxes_read_off_bullets_are_repaired(self): + indicators = ( + "PATIENT RELATED INDICATORS (TARGET)\n\n" + "1. **Haemoglobin > 11 g (**In > 80% of patients on PD)\n" + "2. **MEASUREMENT AND REPORTING OF PERITONITIS**\n" + " - ☐ Culture - negative peritonitis (< 15 % of all peritonitis episodes)\n" + " - ☐ Pre - PD peritonitis - % of total\n\n" + "- ☐ Yes ☐ No\n- ☐ Consent filed\n\n" + "- ☑ Catheter flushed\n- ☐ Dressing changed" + ) + + self.assertEqual( + repair_markdown(indicators), + "PATIENT RELATED INDICATORS (TARGET)\n\n" + "1. **Haemoglobin > 11 g** (In > 80% of patients on PD)\n" + "2. **MEASUREMENT AND REPORTING OF PERITONITIS**\n" + " - Culture - negative peritonitis (< 15 % of all peritonitis episodes)\n" + " - Pre - PD peritonitis - % of total\n\n" + "- ☐ Yes ☐ No\n- ☐ Consent filed\n\n" + "- ☑ Catheter flushed\n- ☐ Dressing changed", + ) + + class TestPageArtifacts(FrappeTestCase): def test_parser_artifacts_include_table_breakage(self): self.assertIn("table missing separator row", parser_artifacts(MISSING_SEPARATOR)) From 19fa30dcf48186f86d5ed7547f0211b0a7e4821e Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Fri, 9 Oct 2026 12:58:18 +0530 Subject: [PATCH 32/33] fix(page-plan): keep short sections that share a pdf page on one wiki page --- wikify/engine/loader/page_plan.py | 61 ++++++++++++++++++++++- wikify/tests/test_page_plan.py | 83 ++++++++++++++++++++++--------- 2 files changed, 120 insertions(+), 24 deletions(-) diff --git a/wikify/engine/loader/page_plan.py b/wikify/engine/loader/page_plan.py index d440e1b..b16048a 100644 --- a/wikify/engine/loader/page_plan.py +++ b/wikify/engine/loader/page_plan.py @@ -24,6 +24,7 @@ MIN_SPLIT_PART_WORDS = 150 MAX_PAGE_WORDS = 2500 MAX_PAGE_PDF_PAGES = 6 +OWN_PAGE_WORDS = 150 OUTLINE_BATCH_LINES = 250 _LIST_ITEM_TITLE = re.compile(r"^(?:\d+[.)]?|[A-Za-z][.)]|[IVXivx]+[.)]|[Ss]tep\s+\d+[:.)]?)\s") @@ -130,6 +131,7 @@ def enforce_invariants( pages = add_numbered_siblings(parents, numbers, pages) pages, parents = split_long_pages(sections, parents, numbers, pages) pages, parents = keep_sibling_order(sections, parents, numbers, pages) + pages, parents = keep_pdf_pages_whole(sections, parents, numbers, pages) child_counts = Counter(parent for parent in parents if parent is not None) pages = { index @@ -180,7 +182,7 @@ def keep_sibling_order( elif previous_page is not None and ( subtree_words[sibling] >= FRAGMENT_WORDS or ( - subtree_words[sibling] >= STUB_WORDS + (subtree_words[sibling] >= STUB_WORDS or opens_pdf_page(sections, sibling)) and not _LIST_ITEM_TITLE.match(sections[sibling].title) ) ): @@ -191,6 +193,63 @@ def keep_sibling_order( return pages, parents +def keep_pdf_pages_whole( + sections: list[Section], parents: list[int | None], numbers: list[tuple[int, ...] | None], pages: set[int] +) -> tuple[set[int], list[int | None]]: + """A short section that shares a PDF page joins the wiki page before it, unless that makes the page too + long to read in one scroll. A numbered section long enough to stand alone keeps its own page.""" + parents = list(parents) + page_parents = {parents[page] for page in pages} + words: Counter[int] = Counter() + last_pdf_page: dict[int, int] = {} + for index, owner in enumerate(page_owners(parents, pages)): + if owner is not None: + words[owner] += len(sections[index].markdown.split()) + last_pdf_page[owner] = max(last_pdf_page.get(owner, 0), sections[index].page_end) + pages = set(pages) + for index in sorted(pages): + if ( + parents[index] is None + or index in page_parents + or (numbers[index] and words[index] >= OWN_PAGE_WORDS) + or opens_pdf_page(sections, index) + ): + continue + target = max((page for page in pages if page < index), default=None) + if target is None: + continue + joined_pdf_pages = max(last_pdf_page[target], last_pdf_page[index]) - sections[target].page_start + 1 + if words[target] + words[index] > MAX_PAGE_WORDS or joined_pdf_pages > MAX_PAGE_PDF_PAGES: + continue + pages.discard(index) + parents[index] = target + words[target] += words[index] + last_pdf_page[target] = max(last_pdf_page[target], last_pdf_page[index]) + return pages, parents + + +def page_owners(parents: list[int | None], pages: set[int]) -> list[int | None]: + owners: list[int | None] = [] + for index, parent in enumerate(parents): + if index in pages: + owners.append(index) + elif parent is not None: + owners.append(owners[parent]) + else: + owners.append(owners[-1] if owners else None) + return owners + + +def opens_pdf_page(sections: list[Section], index: int) -> bool: + page_start = sections[index].page_start + for earlier in reversed(sections[:index]): + if earlier.page_end < page_start: + return True + if earlier.markdown.strip(): + return False + return True + + def split_long_pages( sections: list[Section], parents: list[int | None], numbers: list[tuple[int, ...] | None], pages: set[int] ) -> tuple[set[int], list[int | None]]: diff --git a/wikify/tests/test_page_plan.py b/wikify/tests/test_page_plan.py index ea16dad..b7e82cf 100644 --- a/wikify/tests/test_page_plan.py +++ b/wikify/tests/test_page_plan.py @@ -71,9 +71,9 @@ def test_pages_are_the_top_two_numbered_levels_below_the_chapter(self): def test_unnumbered_document_keeps_its_top_two_heading_levels(self): sections = [ _sec(["Guide"], 1), - _sec(["Guide", "Setup"], 1), - _sec(["Guide", "Setup", "Prerequisites"], 2), - _sec(["Guide", "Usage"], 3), + _sec(["Guide", "Setup"], 2), + _sec(["Guide", "Setup", "Prerequisites"], 3), + _sec(["Guide", "Usage"], 4), ] self.assertEqual(_titles(plan_pages(sections)), ["Guide", "Setup", "Usage"]) @@ -82,8 +82,8 @@ def test_a_lone_sub_section_folds_into_its_parent(self): _sec(["5.7 Billing"], 1, markdown=""), _sec(["5.7 Billing", "5.7.2 Inpatient billing"], 1, 2), _sec(["5.8 Care"], 2), - _sec(["5.8 Care", "5.8.1 Chaperone"], 2), - _sec(["5.8 Care", "5.8.1 Chaperone", "5.8.1.1 Escort"], 3), + _sec(["5.8 Care", "5.8.1 Chaperone"], 3), + _sec(["5.8 Care", "5.8.1 Chaperone", "5.8.1.1 Escort"], 4), ] pages = plan_pages(sections) self.assertEqual(_titles(pages), ["5.7 Billing", "5.8 Care", "5.8.1 Chaperone"]) @@ -110,7 +110,7 @@ def test_an_empty_folded_section_keeps_its_heading(self): def test_heading_depth_is_capped_at_six(self): path = [f"Level {depth}" for depth in range(1, 9)] - sections = [_sec(path[:depth], 1) for depth in range(1, 9)] + sections = [_sec(path[:depth], min(depth, 2)) for depth in range(1, 9)] child = plan_pages(sections)[1] self.assertEqual(child.title, "Level 2") self.assertIn("###### Level 7\n\nbody of Level 7\n\n###### Level 8", child.markdown) @@ -295,21 +295,23 @@ def test_a_long_page_without_numbered_children_splits_at_its_sub_headings(self): access, "AV FISTULA/AV GRAFT", "CENTRAL VENOUS CATHETER", - "IDENTIFYING AND MANAGING COMPLICATIONS", "Peritoneal Dialysis", ], ) self.assertEqual(pages[0].markdown, "") self.assertIn("## CATHETER CARE", pages[2].markdown) - self.assertTrue(pages[2].markdown.endswith(f"## DIALYZER REUSE\n\n{_words(30)}")) - self.assertEqual(pages[4].hierarchy_path, [access, "Peritoneal Dialysis"]) - self.assertEqual((pages[4].page_start, pages[4].page_end), (11, 11)) + self.assertIn( + f"## DIALYZER REUSE\n\n{_words(30)}\n\n## IDENTIFYING AND MANAGING COMPLICATIONS", + pages[2].markdown, + ) + self.assertEqual(pages[3].hierarchy_path, [access, "Peritoneal Dialysis"]) + self.assertEqual((pages[3].page_start, pages[3].page_end), (11, 11)) def test_a_short_leading_preamble_joins_the_page_after_it(self): sections = [ _sec(["Preamble"], 1, markdown="Saline dialysis is reserved for bleeding patients."), - _sec(["6.2.1 POLICIES"], 1, markdown=""), - _sec(["6.2.1 POLICIES", "6.2.1.3 ASSESSMENT"], 1, 2), + _sec(["6.2.1 POLICIES"], 2, markdown=""), + _sec(["6.2.1 POLICIES", "6.2.1.3 ASSESSMENT"], 2), _sec(["6.2.1 POLICIES", "6.2.1.4 VASCULAR ACCESS"], 3), ] pages, _ = self._plan(_llm_reply({"pages": [0, 1, 2, 3]}), sections) @@ -323,11 +325,11 @@ def test_a_short_leading_preamble_joins_the_page_after_it(self): def test_numbered_siblings_follow_the_majority_into_pages(self): sections = [ _sec(["5.8 Policies on patient care"], 1, markdown=_words(20)), - _sec(["5.8 Policies on patient care", "5.8.1 Chaperone"], 1, markdown=_words(90)), - _sec(["5.8 Policies on patient care", "5.8.2 Antibiotic policy"], 1, markdown=_words(29)), - _sec(["5.8 Policies on patient care", "5.8.3 Consent"], 2, markdown=_words(200)), - _sec(["5.8 Policies on patient care", "5.8.4 Sedation"], 3, markdown=_words(300)), - _sec(["5.8 Policies on patient care", "5.8.4 Sedation", "Contraindications"], 3), + _sec(["5.8 Policies on patient care", "5.8.1 Chaperone"], 2, markdown=_words(90)), + _sec(["5.8 Policies on patient care", "5.8.2 Antibiotic policy"], 3, markdown=_words(29)), + _sec(["5.8 Policies on patient care", "5.8.3 Consent"], 4, markdown=_words(200)), + _sec(["5.8 Policies on patient care", "5.8.4 Sedation"], 5, markdown=_words(300)), + _sec(["5.8 Policies on patient care", "5.8.4 Sedation", "Contraindications"], 5), ] pages, _ = self._plan(_llm_reply({"pages": [1, 3, 4]}), sections) self.assertEqual( @@ -347,12 +349,12 @@ def test_numbered_siblings_after_a_page_are_pages_even_when_short(self): sections = [ _sec(["2. Organogram"], 1, markdown=_words(20)), _sec(["2. Organogram", "2.1 The staff"], 1, markdown=_words(200)), - _sec(["2. Organogram", "2.2 Hierarchy"], 1, markdown=_words(200)), - _sec(["2. Organogram", "2.3 Staff list"], 2, markdown=_words(200)), - _sec(["2. Organogram", "2.4 Laboratory staff"], 3, markdown=_words(60)), - _sec(["2. Organogram", "2.5 Office staff"], 3, markdown=_words(60)), - _sec(["2. Organogram", "2.6 Support staff"], 3, markdown=_words(25)), - _sec(["2. Organogram", "2.7 Attendants"], 3, markdown=_words(5)), + _sec(["2. Organogram", "2.2 Hierarchy"], 2, markdown=_words(200)), + _sec(["2. Organogram", "2.3 Staff list"], 3, markdown=_words(200)), + _sec(["2. Organogram", "2.4 Laboratory staff"], 4, markdown=_words(60)), + _sec(["2. Organogram", "2.5 Office staff"], 5, markdown=_words(60)), + _sec(["2. Organogram", "2.6 Support staff"], 6, markdown=_words(25)), + _sec(["2. Organogram", "2.7 Attendants"], 6, markdown=_words(5)), ] pages, _ = self._plan(_llm_reply({"pages": [0, 2]}), sections) self.assertEqual( @@ -399,3 +401,38 @@ def test_a_long_page_never_splits_at_a_form_letterhead_or_a_step(self): ) self.assertIn("## Step 11: Check, measure, Record and Discard", pages[3].markdown) self.assertIn("## CONTACT INFORMATION SHEET", pages[4].markdown) + + def test_sections_sharing_a_pdf_page_share_a_wiki_page(self): + sections = [ + _sec(["2. Organogram"], 25, markdown=_words(12)), + _sec(["2. Organogram", "2.1 The staff"], 25, markdown=_words(50)), + _sec(["2. Organogram", "2.2 Staff hierarchy"], 26, 28, markdown=_words(390)), + _sec(["2. Organogram", "2.3 Staff list"], 29, 32, markdown=_words(1000)), + _sec(["2. Organogram", "2.4 Laboratory staff"], 33, markdown=_words(5)), + _sec(["2. Organogram", "2.5 Research staff"], 33, markdown=_words(5)), + _sec(["2. Organogram", "2.6 Office staff"], 33, markdown=_words(25)), + _sec(["2. Organogram", "2.7 Support staff"], 33, markdown=_words(30)), + ] + pages, _ = self._plan(_llm_reply({"pages": [0, 1, 2, 3, 4, 5, 6, 7]}), sections) + self.assertEqual( + _titles(pages), + ["2. Organogram", "2.2 Staff hierarchy", "2.3 Staff list", "2.4 Laboratory staff"], + ) + self.assertIn("## 2.1 The staff", pages[0].markdown) + for title in ("2.5 Research staff", "2.6 Office staff", "2.7 Support staff"): + self.assertIn(f"## {title}", pages[-1].markdown) + + def test_a_long_numbered_section_keeps_its_page_when_it_starts_mid_page(self): + sections = [ + _sec(["3. Job descriptions"], 34, markdown=_words(80)), + _sec(["3. Job descriptions", "3.1 Head of the department"], 34, 36, markdown=_words(600)), + _sec(["3. Job descriptions", "3.2 Head of the unit"], 37, 38, markdown=_words(300)), + _sec(["3. Job descriptions", "3.3 Consultant"], 38, 39, markdown=_words(180)), + _sec(["3. Job descriptions", "3.4 Registrar"], 39, markdown=_words(90)), + ] + pages, _ = self._plan(_llm_reply({"pages": [0, 1, 2, 3, 4]}), sections) + self.assertEqual( + _titles(pages), + ["3. Job descriptions", "3.1 Head of the department", "3.2 Head of the unit", "3.3 Consultant"], + ) + self.assertIn("## 3.4 Registrar", pages[-1].markdown) From a2806241a256d325973c374d3aad829fc8fe72a0 Mon Sep 17 00:00:00 2001 From: Rl0007 <12agrawalrahul@gmail.com> Date: Fri, 9 Oct 2026 18:21:43 +0530 Subject: [PATCH 33/33] feat(generate): add a contents page when the pdf prints none --- wikify/engine/generate.py | 53 ++++++++++++++++++++++++++++++++++- wikify/tests/test_generate.py | 30 +++++++++++++++++++- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/wikify/engine/generate.py b/wikify/engine/generate.py index 4d999b7..70f36e7 100644 --- a/wikify/engine/generate.py +++ b/wikify/engine/generate.py @@ -33,6 +33,8 @@ DATA_MAX_LENGTH = 140 OVERVIEW_TITLE = "Overview" +CONTENTS_TITLE = "Contents" +PRINTED_CONTENTS_TITLES = {"contents", "table of contents"} def _upsert_wiki_document( @@ -97,6 +99,10 @@ def overview_route(prefix: str, section_name: str) -> tuple[str, str]: return bounded_route(prefix, OVERVIEW_TITLE, section_name) +def contents_route(prefix: str, source_document: str) -> tuple[str, str]: + return bounded_route(prefix, CONTENTS_TITLE, source_document) + + def has_own_body(section: dict) -> bool: return bool(section["is_group"] and (section["markdown"] or "").strip()) @@ -135,6 +141,7 @@ def run(self) -> dict: self._stage("Building wiki pages") self._build_structure() self._rollup_empty_groups() + self._add_contents() self._stage("Resolving page references") self._rewrite_links() store.set_document_wiki(self.sd.name, self.space.name, self.root_group.name, status="Wiki-Generated") @@ -163,6 +170,14 @@ def _sweep_stale(self) -> None: rest under the root group deepest-first (NestedSet needs leaves gone first). """ kept = {self.root_group.name} + if self._needs_contents(): + kept |= set( + frappe.get_all( + "Wiki Document", + filters={"route": contents_route(self.root_group.route, self.sd.name)[0], "is_group": 0}, + pluck="name", + ) + ) kept |= {s["wiki_document"] for s in self.included if s["wiki_document"]} overview_routes = [ overview_route(self.root_group.route, s["name"])[0] for s in self.included if has_own_body(s) @@ -211,7 +226,7 @@ def _content_for(section: dict) -> str: return "" if section["is_group"] else f"# {section['title']}\n" def _build_structure(self) -> None: - sort_counter: dict[str, int] = {} # parent wiki name → next sort_order + sort_counter: dict[str, int] = {self.root_group.name: 1 if self._needs_contents() else 0} total = len(self.included) for i, s in enumerate(self.included): parent_name = self._parent_for(s) @@ -281,6 +296,42 @@ def _rollup_empty_groups(self) -> None: "Wiki Document", self.wiki_name[s["name"]], "content", toc, update_modified=False ) + def _needs_contents(self) -> bool: + return bool(self.included) and not any( + s["title"].strip().lower() in PRINTED_CONTENTS_TITLES for s in self.included + ) + + def _add_contents(self) -> None: + """The PDF printed no contents pages, so the space gets one built from the section tree.""" + if not self._needs_contents(): + return + depth: dict[str, int] = {} + lines = [] + for s in self.included: + parent = self.by_name.get(s["parent_source_section"]) + while parent is not None and parent["name"] not in depth: + parent = self.by_name.get(parent["parent_source_section"]) + depth[s["name"]] = depth[parent["name"]] + 1 if parent else 0 + pages = ( + f"p. {s['page_start']}" + if s["page_end"] in (None, s["page_start"]) + else f"pp. {s['page_start']}-{s['page_end']}" + ) + lines.append( + f"{' ' * depth[s['name']]}- [{s['title']}](/{self.wiki_route[s['name']]}) — {pages}\n" + ) + route, slug = contents_route(self.root_group.route, self.sd.name) + doc = _upsert_wiki_document( + frappe.db.get_value("Wiki Document", {"route": route, "is_group": 0}, "name"), + title=CONTENTS_TITLE, + content=f"# {CONTENTS_TITLE}\n\n" + "".join(lines), + is_group=False, + parent=self.root_group.name, + route=route, + slug=slug, + ) + frappe.db.set_value("Wiki Document", doc.name, "sort_order", 0, update_modified=False) + def _route_for_page(self, n: int) -> str | None: """Smallest-span included section whose PDF page range contains n → its route.""" best = smallest_covering(self.included, n) diff --git a/wikify/tests/test_generate.py b/wikify/tests/test_generate.py index 4ea088b..2d8e68e 100644 --- a/wikify/tests/test_generate.py +++ b/wikify/tests/test_generate.py @@ -238,6 +238,34 @@ def test_regenerate_keeps_the_overview_and_drops_it_once_the_group_body_is_gone( self.assertNotIn("Overview", docs) self.assertIn("## Contents", docs["1. Intro"].content) + def test_contents_page_is_built_when_the_pdf_prints_none(self): + res = self._generate() + docs = self._by_title(res["root_group"]) + contents = docs["Contents"] + self.assertEqual(contents.is_group, 0) + self.assertEqual(contents.parent_wiki_document, res["root_group"]) + self.assertLess(contents.sort_order, docs["1. Intro"].sort_order) + self.assertIn(f"- [1. Intro](/{docs['1. Intro'].route}) — pp. 1-3", contents.content) + self.assertIn(f" - [1.2 Scope](/{docs['1.2 Scope'].route}) — p. 2", contents.content) + self.assertEqual(get_landing_page_for_route(docs["Gen Test"].route)["route"], contents.route) + + generate_wiki(self.sd.name, wiki_space=res["space"]) + self.assertEqual(self._by_title(res["root_group"])["Contents"].name, contents.name) + + def test_printed_contents_replaces_the_built_one(self): + res = self._generate() + store.replace_sections( + self.sd.name, + [ + _sec("Contents", 1, ["Contents"], 1, 1, "1. Intro .... 2"), + _sec("1. Intro", 1, ["1. Intro"], 2, 5), + ], + ) + generate_wiki(self.sd.name, wiki_space=res["space"]) + docs = self._by_title(res["root_group"]) + self.assertEqual(docs["Contents"].content, "1. Intro .... 2") + self.assertEqual(len(docs), 3) + def test_data_max_length_matches_the_column_width(self): self.assertEqual(DATA_MAX_LENGTH, frappe.db.VARCHAR_LEN) @@ -249,7 +277,7 @@ def test_deep_tree_with_long_titles_stays_within_route_limit(self): res = self._generate() docs = self._docs_under(res["root_group"]) - self.assertEqual(len(docs), 2 * len(titles)) + self.assertEqual(len(docs), 2 * len(titles) + 1) self.assertTrue(all(len(doc.route) <= DATA_MAX_LENGTH for doc in docs.values())) self.assertEqual(len({doc.route for doc in docs.values()}), len(docs))