Source code for manuscript.data.structures

import json
from pathlib import Path
from typing import List, Optional, Tuple, Union

from pydantic import BaseModel, ConfigDict, Field


[docs] class TextSpan(BaseModel): """ A single detected or recognized text span. A text span is the smallest OCR region in the pipeline. It may correspond to a word, a whole text line, or any other contiguous text segment returned by a detector. Attributes ---------- polygon : List[Tuple[float, float]] Polygon vertices (x, y), ordered clockwise. The public data model supports arbitrary polygons with 4 or more points. For quadrilateral text regions, the canonical order is TL -> TR -> BR -> BL (Top-Left, Top-Right, Bottom-Right, Bottom-Left). detection_confidence : float Text detection confidence score from detector (0.0 to 1.0). text : str, optional Recognized text content (populated by OCR pipeline). None if only detection was performed. recognition_confidence : float, optional Text recognition confidence score from recognizer (0.0 to 1.0). None if only detection was performed. order : int, optional Text span position inside the line after sorting. None before sorting. Examples -------- >>> text_span = TextSpan( ... polygon=[(10, 20), (100, 20), (100, 40), (10, 40)], ... detection_confidence=0.95, ... text="Hello", ... recognition_confidence=0.98 ... ) >>> print(text_span.text) Hello """ model_config = ConfigDict(extra="forbid") polygon: List[Tuple[float, float]] = Field( ..., min_length=4, description=( "Polygon vertices (x, y), ordered clockwise. Supports arbitrary " "polygons with 4 or more points. For quadrilateral text regions: " "TL -> TR -> BR -> BL." ), ) detection_confidence: float = Field( ..., ge=0.0, le=1.0, description="Text detection confidence score from detector" ) text: Optional[str] = Field( None, description="Recognized text content (populated by OCR pipeline)" ) recognition_confidence: Optional[float] = Field( None, ge=0.0, le=1.0, description="Text recognition confidence score from recognizer", ) order: Optional[int] = Field( None, description="Text span position inside the line after sorting. None before sorting.", )
[docs] class Line(BaseModel): """ A single text line containing one or more text spans. Attributes ---------- text_spans : List[TextSpan] List of text spans in the line. order : int, optional Line position inside a block or page after sorting. None before sorting. Examples -------- >>> line = Line(text_spans=[ ... TextSpan( ... polygon=[(10, 20), (50, 20), (50, 40), (10, 40)], ... detection_confidence=0.95, ... text="Hello", ... ), ... TextSpan( ... polygon=[(60, 20), (110, 20), (110, 40), (60, 40)], ... detection_confidence=0.97, ... text="World", ... ), ... ]) >>> print(len(line.text_spans)) 2 """ model_config = ConfigDict(extra="forbid") text_spans: List[TextSpan] = Field( default_factory=list, description="List of text spans in the line.", ) order: Optional[int] = Field( None, description="Line position inside a block or page after sorting. None before sorting.", )
[docs] class Block(BaseModel): """ A logical text block (e.g., paragraph, column). Attributes ---------- lines : List[Line] List of text lines in the block. text_spans : List[TextSpan], optional Optional flat list of text spans used as a shorthand input. If ``lines`` is empty and ``text_spans`` are provided, they are wrapped into a single line. order : int, optional Block reading-order position after sorting. None before sorting. Examples -------- >>> block = Block(lines=[ ... Line(text_spans=[ ... TextSpan( ... polygon=[(10, 20), (50, 20), (50, 40), (10, 40)], ... detection_confidence=0.95, ... text="Line 1", ... ) ... ]), ... Line(text_spans=[ ... TextSpan( ... polygon=[(10, 50), (50, 50), (50, 70), (10, 70)], ... detection_confidence=0.97, ... text="Line 2", ... ) ... ]), ... ]) >>> print(len(block.lines)) 2 """ model_config = ConfigDict(extra="forbid") lines: List[Line] = Field(default_factory=list) text_spans: List[TextSpan] = Field( default_factory=list, description=( "Optional flat list of text spans. Use 'lines' for structured output." ), ) order: Optional[int] = Field( None, description="Block reading-order position after sorting. None before sorting.", )
[docs] def __init__(self, **data): """Initialize Block, normalizing flat ``text_spans`` into one line.""" super().__init__(**data) if not self.lines and self.text_spans: self.lines = [Line(text_spans=self.text_spans)]
[docs] class Page(BaseModel): """ A document page containing blocks of text. For a full visual diagram of the data model, see: ``DATA_MODEL.md`` located in the same module directory. Attributes ---------- blocks : List[Block] List of text blocks on the page. Examples -------- >>> page = Page(blocks=[ ... Block(lines=[ ... Line(text_spans=[ ... TextSpan( ... polygon=[(10, 20), (50, 20), (50, 40), (10, 40)], ... detection_confidence=0.95, ... text="Hello", ... ) ... ]) ... ]) ... ]) >>> print(len(page.blocks)) 1 """ model_config = ConfigDict(extra="forbid") blocks: List[Block]
[docs] def to_json(self, path: Optional[Union[str, Path]] = None, indent: int = 2) -> str: """ Export Page to JSON. Parameters ---------- path : str or Path, optional If provided, saves JSON to file. indent : int, optional JSON indentation. Default is 2. Returns ------- str JSON string representation. Examples -------- >>> page.to_json("result.json") # save to file >>> json_str = page.to_json() # get as string """ data = self.model_dump() json_str = json.dumps(data, ensure_ascii=False, indent=indent) if path: Path(path).write_text(json_str, encoding="utf-8") return json_str
[docs] @classmethod def from_json(cls, source: Union[str, Path]) -> "Page": """ Load Page from JSON file or string. Parameters ---------- source : str or Path Path to JSON file or JSON string. Returns ------- Page Loaded Page object. Examples -------- >>> page = Page.from_json("result.json") >>> page = Page.from_json('{"blocks": [...]}') """ path = Path(source) if path.exists(): data = json.loads(path.read_text(encoding="utf-8")) else: data = json.loads(source) return cls.model_validate(data)