227 lines
9.0 KiB
Python
227 lines
9.0 KiB
Python
"""Intermediate representation (IR) handed by parser adapters to the writer.
|
|
|
|
Parser engines do not write spec-shaped JSON directly. Each engine adapter
|
|
produces an :class:`IRDoc`; :func:`lightrag.sidecar.writer.write_sidecar`
|
|
turns that into ``*.parsed/`` files matching ``LightRAGSidecarFormat-zh.md``.
|
|
|
|
Why an in-process IR (not a serialized intermediate):
|
|
|
|
- One executable spec point. ``writer.py`` is the only place that knows id
|
|
formats, placeholder tags, blockid computation, ``asset_dir`` truth value.
|
|
- Engine adapters only translate; they never embed knowledge of the on-disk
|
|
format.
|
|
- The dataclasses below cover the spec contract plus an ``extras`` escape
|
|
hatch on item-level objects so engine-specific signals (rowspan, OCR
|
|
confidence, ...) can be passed through without spec churn.
|
|
|
|
Placeholder convention used by :attr:`IRBlock.content_template`:
|
|
|
|
- ``{{TBL:k}}`` — k is the placeholder key declared on the IRTable object
|
|
- ``{{IMG:k}}`` — IRDrawing
|
|
- ``{{EQ:k}}`` — block-level IREquation (``is_block=True``)
|
|
- ``{{EQI:k}}`` — inline IREquation (``is_block=False``); rendered without an
|
|
id, never enters ``equations.json``
|
|
|
|
The writer expands these templates after id allocation. Adapters MUST emit
|
|
exactly one placeholder per item; multiple in-content placeholders sharing
|
|
the same key are not supported.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
@dataclass
|
|
class IRPosition:
|
|
"""Block-level position. See spec §八.
|
|
|
|
``type`` values: ``"paraid"`` (docx) / ``"bbox"`` (pdf) /
|
|
``"heading"`` (md) / ``"absolute"`` (text).
|
|
|
|
``origin`` is meaningful only for ``type="bbox"`` and acts as a
|
|
per-position override of ``IRDoc.bbox_attributes.origin`` (spec §八).
|
|
Leave ``None`` to inherit the document-level origin; set explicitly
|
|
(e.g. ``"LEFTTOP"`` / ``"LEFTBOTTOM"``) when this position's
|
|
coordinate system differs from the document default — used by the
|
|
Docling adapter to record mixed ``coord_origin`` without flipping
|
|
coordinates.
|
|
"""
|
|
|
|
type: str
|
|
anchor: Any = None
|
|
range: list | None = None
|
|
charspan: list[int] | None = None
|
|
origin: str | None = None
|
|
|
|
def to_jsonable(self) -> dict[str, Any]:
|
|
out: dict[str, Any] = {"type": self.type}
|
|
if self.anchor is not None:
|
|
out["anchor"] = self.anchor
|
|
if self.range is not None:
|
|
out["range"] = list(self.range)
|
|
if self.charspan is not None:
|
|
out["charspan"] = list(self.charspan)
|
|
if self.origin is not None:
|
|
out["origin"] = self.origin
|
|
return out
|
|
|
|
|
|
@dataclass
|
|
class IRTable:
|
|
"""Spec §五. ``rows`` (preferred) or ``html`` describes the body.
|
|
|
|
The writer renders ``{{TBL:placeholder_key}}`` in IRBlock.content_template
|
|
as ``<table id="tb-..." format="json|html">body</table>``; ``format``
|
|
is chosen by which payload the adapter populated.
|
|
"""
|
|
|
|
placeholder_key: str
|
|
rows: list[list[str]] | None = None
|
|
html: str | None = None
|
|
num_rows: int = 0
|
|
num_cols: int = 0
|
|
caption: str = ""
|
|
footnotes: list[str] = field(default_factory=list)
|
|
# Repeating (cross-page) header lifted from the source. Its representation
|
|
# follows the table's format so merged-cell semantics survive end-to-end:
|
|
# * JSON tables → a 2-D grid (``list[list[str]]``); the writer stores it
|
|
# as a JSON 2-D array string.
|
|
# * HTML tables → the raw ``<thead>…</thead>`` HTML string, preserving
|
|
# ``rowspan`` / ``colspan``; the writer stores it verbatim.
|
|
# A grid supplied for an HTML table is rendered to a (span-less) ``<thead>``
|
|
# by the writer as a fallback.
|
|
table_header: list[list[str]] | str | None = None
|
|
# Spec §五 ``self_ref``: optional pointer into the engine's raw output
|
|
# (e.g. Docling JSON Pointer ``#/tables/2``). Empty string ⇒ writer
|
|
# omits the field. Used for traceability back to ``.docling_raw/``.
|
|
self_ref: str = ""
|
|
extras: dict[str, Any] = field(default_factory=dict)
|
|
# Optional verbatim body to render inside the ``<table …>…</table>`` tag
|
|
# in ``blocks.jsonl``. When set, the writer uses this string in the block
|
|
# text instead of re-encoding ``rows`` via ``json.dumps`` — preserving
|
|
# the parser's original whitespace/escaping when byte-equivalence with a
|
|
# pre-existing output is required. The ``tables.json`` ``content`` field
|
|
# is unaffected and remains the canonical
|
|
# ``json.dumps(rows, ensure_ascii=False)`` encoding.
|
|
#
|
|
# Coexistence with ``rows`` / ``html``: ``body_override`` does NOT replace
|
|
# the structured body. ``rows`` (or ``html``) must still be populated for
|
|
# the sidecar's ``content`` / ``dimension`` / ``format`` fields and for
|
|
# the writer's ``"json" vs "html"`` format selection. Adapters typically
|
|
# set BOTH (e.g. native docx sets ``rows`` from the parsed JSON AND sets
|
|
# ``body_override`` to the raw verbatim string). When JSON parsing fails
|
|
# in the adapter (``rows`` is None), ``html`` is used as the structured
|
|
# fallback and the writer renders ``format="html"`` with the body_override
|
|
# string verbatim — keeping the original (unparseable) bytes intact.
|
|
#
|
|
# MinerU HTML tables also use this: ``rows`` is None, ``html`` holds the
|
|
# unwrapped ``<table>…</table>`` for the sidecar ``content``, and
|
|
# ``body_override`` holds that table's *inner* body so the writer's own
|
|
# ``<table …>`` wrapper is not nested.
|
|
body_override: str | None = None
|
|
|
|
|
|
@dataclass
|
|
class IRDrawing:
|
|
"""Spec §四. ``asset_ref`` points to an :class:`AssetSpec` in IRDoc."""
|
|
|
|
placeholder_key: str
|
|
asset_ref: str
|
|
fmt: str = ""
|
|
caption: str = ""
|
|
footnotes: list[str] = field(default_factory=list)
|
|
src: str = ""
|
|
# Spec §四 ``self_ref``: optional pointer into the engine's raw output
|
|
# (e.g. Docling JSON Pointer ``#/pictures/3``). Empty string ⇒ writer
|
|
# omits the field. Used for traceability back to ``.docling_raw/``.
|
|
self_ref: str = ""
|
|
extras: dict[str, Any] = field(default_factory=dict)
|
|
# Optional verbatim path. When set, the writer emits this string in
|
|
# both the ``blocks.jsonl`` ``<drawing path>`` attribute and the
|
|
# ``drawings.json`` ``path`` field as-is — bypassing
|
|
# ``asset_paths`` resolution and the ``block_drawing_path_style``
|
|
# transformation. Used for linked / external image references (e.g.
|
|
# ``<drawing path="https://…/img.png" />``) that point at bytes not
|
|
# materialized into ``<base>.blocks.assets/``.
|
|
path_override: str | None = None
|
|
|
|
|
|
@dataclass
|
|
class IREquation:
|
|
"""Spec §六. ``is_block=False`` ⇒ inline; not allocated an id, not written
|
|
to ``equations.json``; rendered as ``<equation format="latex">…</equation>``
|
|
in block text.
|
|
"""
|
|
|
|
placeholder_key: str
|
|
latex: str
|
|
is_block: bool = True
|
|
caption: str = ""
|
|
footnotes: list[str] = field(default_factory=list)
|
|
# Spec §六 ``self_ref``: optional pointer into the engine's raw output
|
|
# (e.g. Docling JSON Pointer ``#/texts/15``). Empty string ⇒ writer
|
|
# omits the field. Only meaningful when ``is_block=True``; inline
|
|
# equations never enter ``equations.json``.
|
|
self_ref: str = ""
|
|
extras: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass
|
|
class IRBlock:
|
|
"""One content block (spec §3.2).
|
|
|
|
``content_template`` is the final block text with placeholder tokens
|
|
embedded. The writer expands tokens once ids are assigned.
|
|
"""
|
|
|
|
content_template: str
|
|
heading: str = ""
|
|
level: int = 0
|
|
parent_headings: list[str] = field(default_factory=list)
|
|
session_type: str = "body"
|
|
table_slice: str = "none"
|
|
table_header: str | None = None
|
|
positions: list[IRPosition] = field(default_factory=list)
|
|
tables: list[IRTable] = field(default_factory=list)
|
|
drawings: list[IRDrawing] = field(default_factory=list)
|
|
equations: list[IREquation] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class AssetSpec:
|
|
"""Describes one file that lands in ``<base>.blocks.assets/``.
|
|
|
|
``source`` may be:
|
|
|
|
- :class:`pathlib.Path` to an existing file on disk (writer copies it);
|
|
- :class:`bytes` payload (writer dumps it);
|
|
- ``None`` when the file is already in place at ``<assets_dir>/<suggested_name>``
|
|
(e.g. native docx parser writes assets during extraction); the writer
|
|
then records its size without touching it.
|
|
|
|
Carrier protocol: a drawing references the asset by :attr:`ref`; the
|
|
writer resolves that to a concrete filename inside the assets dir and
|
|
writes the result to both ``drawings.json`` (full relative path) and
|
|
the ``<drawing path>`` attribute in ``blocks.jsonl``.
|
|
"""
|
|
|
|
ref: str
|
|
suggested_name: str
|
|
source: Path | bytes | None = None
|
|
|
|
|
|
@dataclass
|
|
class IRDoc:
|
|
"""Top-level IR — the input to :func:`write_sidecar`."""
|
|
|
|
document_name: str
|
|
document_format: str
|
|
doc_title: str
|
|
split_option: dict[str, Any]
|
|
blocks: list[IRBlock]
|
|
assets: list[AssetSpec] = field(default_factory=list)
|
|
bbox_attributes: dict[str, Any] | None = None
|