diff --git a/_check_effect.py b/_check_effect.py new file mode 100644 index 00000000..67035582 --- /dev/null +++ b/_check_effect.py @@ -0,0 +1,50 @@ +import json +from pathlib import Path + +json_path = Path("D:/L/OB/Literature-hub/System/PaperForge/ocr/7C8829BD/json/result.json") +data = json.loads(json_path.read_text(encoding="utf-8")) + +pageno = 0 +for payload in data: + for res in payload.get("layoutParsingResults", []): + pageno += 1 + if pageno != 7: + continue + blocks = res.get("prunedResult", {}).get("parsing_res_list", []) + + from paperforge.worker.ocr import block_sort_key, validate_block_order, is_embedded_figure_text_block + from paperforge.worker.ocr_orchestrator import reorder_blocks_layered + + sorted_blocks = sorted(blocks, key=block_sort_key) + validated = validate_block_order(sorted_blocks, 1191) + + print("=== is_embedded_figure_text_block on VALIDATED blocks ===") + for b in validated: + lbl = b.get("block_label", "") + if lbl == "paragraph_title": + result = is_embedded_figure_text_block(b, validated, page_width=1191, page_height=1684) + txt = b.get("block_content", "")[:60] + print(f" [{txt}] => embedded={result}") + + layered = reorder_blocks_layered(validated, page_width=1191, page_height=1684) + + print() + print("=== is_embedded_figure_text_block on LAYERED blocks ===") + for b in layered: + lbl = b.get("block_label", "") + if lbl == "paragraph_title": + result = is_embedded_figure_text_block(b, layered, page_width=1191, page_height=1684) + txt = b.get("block_content", "")[:60] + print(f" [{txt}] => embedded={result}") + + print() + print("=== Role assignment ===") + from paperforge.worker.ocr_roles import assign_block_role + for b in validated: + lbl = b.get("block_label", "") + if lbl == "paragraph_title": + role = assign_block_role(b, validated, page_width=1191, page_height=1684) + txt = b.get("block_content", "")[:60] + print(f" [{txt}] => role={role.role} conf={role.confidence}") + break + break diff --git a/_extract_master_figs.py b/_extract_master_figs.py new file mode 100644 index 00000000..637a72b6 --- /dev/null +++ b/_extract_master_figs.py @@ -0,0 +1,52 @@ +"""Extract figure/clustering functions from origin/master ocr.py.""" +import subprocess, re, sys + +raw = subprocess.run( + ["git", "show", "origin/master:paperforge/worker/ocr.py"], + capture_output=True, text=True, encoding="utf-8", errors="replace" +).stdout + +func_starts = [ + "def clean_block_text", "def is_subfigure_label", + "def media_clusters", "def _bbox_width", "def _bbox_height", + "def _bbox_horizontal_overlap", "def _bbox_vertical_overlap", + "def _bbox_horizontal_overlap_ratio", "def _bbox_center_x", + "def _bbox_center_y", "def _cluster_bbox", "def _union_bboxes", + "def is_formal_figure_legend", "def is_numbered_figure_caption", + "def _figure_caption_blocks", "def estimate_body_column_width", + "def is_body_paragraph_like_text_block", + "def _precaption_media_region", "def compute_precaption_composite_regions", + "def is_embedded_figure_text_block", +] + +lines = raw.split("\n") + +# Find line numbers for each function +func_lines = {} +for i, line in enumerate(lines): + stripped = line.strip() + for fname in func_starts: + if stripped.startswith(fname) and fname not in func_lines: + func_lines[fname] = i + +# Extract function bodies (from def to next def at same indent level) +extracted = {} +for fname, start_line in func_lines.items(): + # Find the indentation of the def + orig_indent = len(lines[start_line]) - len(lines[start_line].lstrip()) + # Read until next top-level def + end_line = len(lines) + for j in range(start_line + 1, len(lines)): + ls = lines[j] + if ls.strip() and not ls.startswith("#") and not ls.startswith(" ") and not ls.startswith("\n"): + curr_indent = len(ls) - len(ls.lstrip()) + if curr_indent <= orig_indent and not ls.startswith(" "): + end_line = j + break + extracted[fname] = "\n".join(lines[start_line:end_line]) + +# Print all extracted functions +for fname in sorted(extracted.keys()): + print(f"=== {fname} ===") + print(extracted[fname]) + print() diff --git a/_master_funcs.txt b/_master_funcs.txt new file mode 100644 index 00000000..b5c04049 --- /dev/null +++ b/_master_funcs.txt @@ -0,0 +1,468 @@ +=== def _bbox_center_x === +def _bbox_center_x(bbox: list[int]) -> float: + return (int(bbox[0]) + int(bbox[2])) / 2 + + + +=== def _bbox_center_y === +def _bbox_center_y(bbox: list[int]) -> float: + return (int(bbox[1]) + int(bbox[3])) / 2 + + + +=== def _bbox_height === +def _bbox_height(bbox: list[int]) -> int: + return max(0, int(bbox[3]) - int(bbox[1])) + + + +=== def _bbox_horizontal_overlap === +def _bbox_horizontal_overlap(a: list[int], b: list[int]) -> int: + return max(0, min(int(a[2]), int(b[2])) - max(int(a[0]), int(b[0]))) + + + +=== def _bbox_horizontal_overlap_ratio === +def _bbox_horizontal_overlap_ratio(a: list[int], b: list[int]) -> float: + width = min(_bbox_width(a), _bbox_width(b)) + if width <= 0: + return 0.0 + return _bbox_horizontal_overlap(a, b) / width + + + +=== def _bbox_vertical_overlap === +def _bbox_vertical_overlap(a: list[int], b: list[int]) -> int: + return max(0, min(int(a[3]), int(b[3])) - max(int(a[1]), int(b[1]))) + + + +=== def _bbox_width === +def _bbox_width(bbox: list[int]) -> int: + return max(0, int(bbox[2]) - int(bbox[0])) + + + +=== def _cluster_bbox === +def _cluster_bbox(cluster: list[dict]) -> list[int]: + return [ + min(int(item["block_bbox"][0]) for item in cluster), + min(int(item["block_bbox"][1]) for item in cluster), + max(int(item["block_bbox"][2]) for item in cluster), + max(int(item["block_bbox"][3]) for item in cluster), + ] + + + +=== def _figure_caption_blocks === +def _figure_caption_blocks(blocks: list[dict]) -> list[dict]: + captions = [] + for block in blocks: + if block.get("block_label") not in {"figure_title", "paragraph_title", "text"}: + continue + text = clean_block_text(block.get("block_content", "")) + if is_formal_figure_legend(text): + captions.append(block) + return captions + + + +=== def _precaption_media_region === +def _precaption_media_region( + caption_bbox: list[int], + cluster_bboxes: list[list[int]], + blocks: list[dict] | None = None, + body_column_width: int = 0, + +=== def _union_bboxes === +def _union_bboxes(bbox_list: list[list[int]]) -> list[int]: + return [ + min(int(bbox[0]) for bbox in bbox_list), + min(int(bbox[1]) for bbox in bbox_list), + max(int(bbox[2]) for bbox in bbox_list), + max(int(bbox[3]) for bbox in bbox_list), + ] + + + +=== def clean_block_text === +def clean_block_text(text: str) -> str: + text = html.unescape(normalize_obsidian_markdown(text)).strip() + return text + + + +=== def compute_precaption_composite_regions === +def compute_precaption_composite_regions(blocks: list[dict], page_width: int = 0, page_height: int = 0) -> list[dict]: + caption_blocks = _figure_caption_blocks(blocks) + _, clusters = media_clusters(blocks) + cluster_bboxes = [_cluster_bbox(cluster) for cluster in clusters] + body_column_width = estimate_body_column_width(blocks, page_width=page_width) + regions: list[dict] = [] + for caption in caption_blocks: + caption_bbox = [int(value) for value in caption.get("block_bbox", [0, 0, 0, 0])] + precaption_region = _precaption_media_region( + caption_bbox, cluster_bboxes, blocks=blocks, body_column_width=body_column_width + ) + if not precaption_region: + continue + region_blocks = [] + for block in blocks: + label = block.get("block_label", "") + bbox = [int(value) for value in block.get("block_bbox", [0, 0, 0, 0])] + if bbox[3] > caption_bbox[1] + 24: + continue + if bbox[3] < precaption_region[1] - 240: + continue + vertical_overlap_with_region = _bbox_vertical_overlap(bbox, precaption_region) + near_region_side = vertical_overlap_with_region > 0 and ( + 0 <= precaption_region[0] - bbox[2] <= 80 or 0 <= bbox[0] - precaption_region[2] <= 80 + ) + intersects_region = ( + _bbox_horizontal_overlap(bbox, precaption_region) > 0 + or precaption_region[0] - 24 <= _bbox_center_x(bbox) <= precaption_region[2] + 24 + or near_region_side + ) + if not intersects_region: + continue + if label in {"image", "chart"}: + if _bbox_vertical_overlap(bbox, precaption_region) <= 0 and bbox[3] < precaption_region[1] - 80: + continue + region_blocks.append(block) + continue + if label in {"text", "paragraph_title"}: + if bbox[3] < precaption_region[1] - 80: + continue + width = _bbox_width(bbox) + text = clean_block_text(block.get("block_content", "")) + if is_body_paragraph_like_text_block( + block, + body_column_width=body_column_width, + cluster_bboxes=cluster_bboxes, + caption_bbox=caption_bbox, + ): + continue + if ( + text + and ( + not re.match( + "^(?:Extended\\s+Data\\s+Fig\\.?|Extended\\s+Data\\s+Figure|Figure|Fig\\.?|Table)\\s+\\w+", + text, + flags=re.IGNORECASE, + ) + ) + and ( + width <= int(max(body_column_width, 1) * 0.78) + or is_embedded_figure_text_block(block, blocks, page_width=page_width, page_height=page_height) + ) + ): + region_blocks.append(block) + media_ids = {block.get("block_id") for block in region_blocks if block.get("block_label") in {"image", "chart"}} + text_ids = { + block.get("block_id") for block in region_blocks if block.get("block_label") in {"text", "paragraph_title"} + } + if len(media_ids) < 1 or not text_ids or len(region_blocks) < 3: + continue + region_bbox = [ + min(int(block["block_bbox"][0]) for block in region_blocks), + min(int(block["block_bbox"][1]) for block in region_blocks), + max(int(block["block_bbox"][2]) for block in region_blocks), + max(int(block["block_bbox"][3]) for block in region_blocks), + ] + regions.append( + { + "bbox": region_bbox, + "block_ids": {block.get("block_id") for block in region_blocks}, + "caption_block_id": caption.get("block_id"), + } + ) + return regions + + + +=== def estimate_body_column_width === +def estimate_body_column_width(blocks: list[dict], page_width: int = 0) -> int: + widths: list[int] = [] + for block in blocks: + if block.get("block_label") not in {"text", "paragraph_title", "abstract"}: + continue + text = clean_block_text(block.get("block_content", "")) + if ( + not text + or is_subfigure_label(text) + or re.match("^(?:Figure|Fig\\.?|Table)\\s+\\w+", text, flags=re.IGNORECASE) + ): + continue + bbox = [int(value) for value in block.get("block_bbox", [0, 0, 0, 0])] + width = _bbox_width(bbox) + height = _bbox_height(bbox) + if width <= 0 or height <= 0: + continue + if page_width and width >= int(page_width * 0.82): + widths.append(width) + continue + if width >= 430: + widths.append(width) + if not widths: + return int(page_width * 0.45) if page_width else 520 + widths.sort() + return widths[len(widths) // 2] + + + +=== def is_body_paragraph_like_text_block === +def is_body_paragraph_like_text_block( + block: dict, + body_column_width: int = 0, + cluster_bboxes: list[list[int]] | None = None, + caption_bbox: list[int] | None = None, + +=== def is_embedded_figure_text_block === +def is_embedded_figure_text_block(block: dict, blocks: list[dict], page_width: int = 0, page_height: int = 0) -> bool: + label = block.get("block_label", "") + if label not in {"text", "paragraph_title"}: + return False + if label == "paragraph_title": + return False + text = clean_block_text(block.get("block_content", "")) + if not text: + return False + if is_formal_figure_legend(text): + return False + bbox = [int(value) for value in block.get("block_bbox", [0, 0, 0, 0])] + width = _bbox_width(bbox) + height = _bbox_height(bbox) + if width <= 0 or height <= 0: + return False + if label == "paragraph_title" and is_subfigure_label(text): + return True + caption_blocks = _figure_caption_blocks(blocks) + cluster_index, clusters = media_clusters(blocks) + cluster_bboxes = [_cluster_bbox(cluster) for cluster in clusters] + body_column_width = estimate_body_column_width(blocks, page_width=page_width) + del cluster_index + nearest_media = None + nearest_media_distance = None + close_media_count = 0 + stacked_media_above = False + stacked_media_below = False + side_media = False + for cluster_bbox in cluster_bboxes: + horizontal_ratio = _bbox_horizontal_overlap_ratio(bbox, cluster_bbox) + vertical_overlap = _bbox_vertical_overlap(bbox, cluster_bbox) + top_gap = int(bbox[1]) - int(cluster_bbox[3]) + bottom_gap = int(cluster_bbox[1]) - int(bbox[3]) + center_inside_x = int(cluster_bbox[0]) <= _bbox_center_x(bbox) <= int(cluster_bbox[2]) + center_inside_y = int(cluster_bbox[1]) <= _bbox_center_y(bbox) <= int(cluster_bbox[3]) + if ( + horizontal_ratio >= 0.45 + and (0 <= top_gap <= 48 or 0 <= bottom_gap <= 48 or vertical_overlap > 0) + or (center_inside_x and abs(_bbox_center_y(bbox) - _bbox_center_y(cluster_bbox)) <= max(80, height * 4)) + ): + close_media_count += 1 + if horizontal_ratio >= 0.5 and 0 <= top_gap <= 90: + stacked_media_above = True + if horizontal_ratio >= 0.5 and 0 <= bottom_gap <= 90: + stacked_media_below = True + if vertical_overlap > 0 and ( + 0 <= int(bbox[0]) - int(cluster_bbox[2]) <= 60 or 0 <= int(cluster_bbox[0]) - int(bbox[2]) <= 60 + ): + side_media = True + dx = 0 + if int(bbox[2]) < int(cluster_bbox[0]): + dx = int(cluster_bbox[0]) - int(bbox[2]) + elif int(cluster_bbox[2]) < int(bbox[0]): + dx = int(bbox[0]) - int(cluster_bbox[2]) + dy = 0 + if int(bbox[3]) < int(cluster_bbox[1]): + dy = int(cluster_bbox[1]) - int(bbox[3]) + elif int(cluster_bbox[3]) < int(bbox[1]): + dy = int(bbox[1]) - int(cluster_bbox[3]) + distance = dx + dy + if nearest_media_distance is None or distance < nearest_media_distance: + nearest_media_distance = distance + nearest_media = cluster_bbox + del center_inside_y + nearest_caption_above = None + nearest_caption_below = None + for caption in caption_blocks: + cb = [int(value) for value in caption.get("block_bbox", [0, 0, 0, 0])] + if cb[3] <= bbox[1] and (nearest_caption_above is None or cb[3] > nearest_caption_above[3]): + nearest_caption_above = cb + if cb[1] >= bbox[3] and (nearest_caption_below is None or cb[1] < nearest_caption_below[1]): + nearest_caption_below = cb + media_between_block_and_caption = 0 + media_x_covering_block = 0 + precaption_region = None + if nearest_caption_below: + precaption_region = _precaption_media_region(nearest_caption_below, cluster_bboxes) + for cluster_bbox in cluster_bboxes: + if cluster_bbox[1] >= nearest_caption_below[1] + 24: + continue + if cluster_bbox[3] <= bbox[1] - 24: + continue + media_between_block_and_caption += 1 + if ( + cluster_bbox[0] - 24 <= _bbox_center_x(bbox) <= cluster_bbox[2] + 24 + or _bbox_horizontal_overlap_ratio(bbox, cluster_bbox) >= 0.35 + ): + media_x_covering_block += 1 + if nearest_caption_above: + caption_gap = bbox[1] - nearest_caption_above[3] + left_align_gap = abs(bbox[0] - nearest_caption_above[0]) + effective_page_width = max(page_width, width) + if ( + 0 <= caption_gap <= 72 + and left_align_gap <= 80 + and (width >= int(effective_page_width * 0.45)) + and (not stacked_media_above) + ): + return False + if is_body_paragraph_like_text_block( + block, + body_column_width=body_column_width, + cluster_bboxes=cluster_bboxes, + caption_bbox=nearest_caption_below, + ): + return False + score = 0.0 + if is_subfigure_label(text): + score += 4.0 + if width <= int(max(body_column_width, 1) * 0.78): + score += 1.4 + elif width <= int(max(body_column_width, 1) * 0.9): + score += 0.5 + if label == "paragraph_title" and len(text) <= 24: + score += 0.8 + if height <= 26: + score += 0.8 + elif height <= 34: + score += 0.4 + if page_width and width <= int(page_width * 0.22): + score += 0.5 + if close_media_count >= 2: + score += 2.0 + elif close_media_count == 1: + score += 1.1 + if stacked_media_above and stacked_media_below: + score += 2.2 + elif stacked_media_above or stacked_media_below: + score += 0.9 + if side_media: + score += 1.0 + if nearest_caption_below and nearest_media: + caption_gap = nearest_caption_below[1] - bbox[3] + media_gap = min( + abs(bbox[1] - nearest_media[3]), + abs(nearest_media[1] - bbox[3]), + abs(_bbox_center_y(bbox) - _bbox_center_y(nearest_media)), + ) + if 0 <= caption_gap <= 120 and media_gap <= 80: + score += 0.8 + if nearest_caption_below: + caption_gap = nearest_caption_below[1] - bbox[3] + if 0 <= caption_gap <= 520 and media_between_block_and_caption >= 3: + score += 0.9 + if 0 <= caption_gap <= 520 and media_x_covering_block >= 2: + score += 1.3 + if ( + 0 <= caption_gap <= 520 + and media_x_covering_block >= 1 + and (width <= int(max(page_width, width) * 0.82)) + and (height <= 96) + ): + score += 0.8 + if precaption_region: + region_overlap = _bbox_horizontal_overlap_ratio(bbox, precaption_region) + within_region_y = ( + int(precaption_region[1]) - 24 <= int(bbox[1]) <= int(precaption_region[3]) + 24 + and int(bbox[3]) <= int(nearest_caption_below[1]) + 24 + ) + if region_overlap >= 0.55 and within_region_y: + score += 1.5 + if ( + int(precaption_region[0]) - 24 <= _bbox_center_x(bbox) <= int(precaption_region[2]) + 24 + and within_region_y + and (media_between_block_and_caption >= 2) + ): + score += 1.1 + if len(text) <= 22: + score += 0.15 + return score >= 2.6 + + + +=== def is_formal_figure_legend === +def is_formal_figure_legend(text: str) -> bool: + cleaned = clean_block_text(text) + if not cleaned: + return False + return bool( + re.match( + "^(?:Extended\\s+Data\\s+Fig\\.?\\s+\\w+|Extended\\s+Data\\s+Figure\\s+\\w+|Extended\\s+Data\\s+Table\\s+\\w+|Supplementary\\s+Fig\\.?\\s+\\w+|Supplementary\\s+Figure\\s+\\w+|Supplementary\\s+Table\\s+\\w+|Supplementary\\s+Video\\s+\\w+|Figure\\s+\\d+|Fig\\.?\\s+\\d+|Table\\s+\\d+|Scheme\\s+\\w+|Graphical\\s+Abstract(?:\\s*[:|.\\-].*)?)", + cleaned, + flags=re.IGNORECASE, + ) + ) + + + +=== def is_numbered_figure_caption === +def is_numbered_figure_caption(text: str) -> bool: + cleaned = clean_block_text(text) + if not re.match(r"^(?:Figure|Fig\.?)\s+\d+\b", cleaned, flags=re.IGNORECASE): + return False + return not re.match( + r"^(?:Figure|Fig\.?)\s+\d+\s+(?:shows?|illustrates?|depicts?|describes?|summarizes?)\b", + cleaned, + flags=re.IGNORECASE, + ) + + + +=== def is_subfigure_label === +def is_subfigure_label(text: str) -> bool: + compact = re.sub("\\s+", " ", text.strip().lower()) + return bool( + re.fullmatch("(?:\\([a-z]\\)\\s*)+", compact) + or re.fullmatch("[a-z]", compact) + or re.fullmatch("[a-z]\\)", compact) + ) + + + +=== def media_clusters === +def media_clusters(blocks: list[dict]) -> tuple[dict[int, int], list[list[dict]]]: + media = [b for b in blocks if b.get("block_label") in {"image", "chart"}] + clusters: list[list[dict]] = [] + block_to_cluster: dict[int, int] = {} + for block in media: + x1, y1, x2, y2 = block.get("block_bbox", [0, 0, 0, 0]) + assigned = None + for idx, cluster in enumerate(clusters): + cx1 = min(item["block_bbox"][0] for item in cluster) - 40 + cy1 = min(item["block_bbox"][1] for item in cluster) - 40 + cx2 = max(item["block_bbox"][2] for item in cluster) + 40 + cy2 = max(item["block_bbox"][3] for item in cluster) + 40 + vertical_overlap = max(0, min(y2, cy2) - max(y1, cy1)) + min_height = max(1, min(y2 - y1, cy2 - cy1)) + if x2 < cx1: + horizontal_gap = cx1 - x2 + elif x1 > cx2: + horizontal_gap = x1 - cx2 + else: + horizontal_gap = 0 + overlaps_cluster = not (x2 < cx1 or x1 > cx2 or y2 < cy1 or y1 > cy2) + side_by_side_panel = vertical_overlap >= int(min_height * 0.35) and horizontal_gap <= 60 + if overlaps_cluster or side_by_side_panel: + assigned = idx + break + if assigned is None: + assigned = len(clusters) + clusters.append([]) + clusters[assigned].append(block) + block_to_cluster[block.get("block_id", -1)] = assigned + return (block_to_cluster, clusters) + + + diff --git a/_render_full.py b/_render_full.py new file mode 100644 index 00000000..bd90a9f5 --- /dev/null +++ b/_render_full.py @@ -0,0 +1,47 @@ +import json, sys, os +from pathlib import Path + +sys.path.insert(0, "D:/L/Med/Research/99_System/LiteraturePipeline/ocr-reading-order-layers") +from paperforge.worker.ocr import render_page_blocks, block_sort_key, validate_block_order + +json_path = Path("D:/L/OB/Literature-hub/System/PaperForge/ocr/7C8829BD/json/result.json") +data = json.loads(json_path.read_text(encoding="utf-8")) + +vault = Path("D:/L/OB/Literature-hub") +images_dir = Path("D:/L/OB/Literature-hub/System/PaperForge/ocr/7C8829BD/images") +page_cache_dir = Path("D:/L/OB/Literature-hub/System/PaperForge/ocr/7C8829BD/pages") + +pageno = 0 +all_lines = [] + +for payload in data: + for res in payload.get("layoutParsingResults", []): + pageno += 1 + pruned = res.get("prunedResult", {}) + blocks = pruned.get("parsing_res_list", []) + if not blocks: + continue + try: + rendered = render_page_blocks(vault, pageno, res, images_dir, page_cache_dir, pdf_doc=None) + all_lines.extend(rendered) + except Exception as e: + all_lines.append(f"") + +output = "\n\n".join(all_lines) +out_path = Path(os.environ.get("TEMP", "/tmp")) / "7c8829bd_layered_fulltext.md" +out_path.write_text(output, encoding="utf-8") + +# Stats +heading_count = output.count("### ") +page_count = output.count("