Spaces:
Sleeping
Sleeping
| """text_only adapter — handles providers that return structured text without geometry. | |
| Expected payload format: | |
| { | |
| "text": "full page text", | |
| "blocks": [ # optional | |
| {"text": "block text"}, | |
| ... | |
| ] | |
| } | |
| or simply: | |
| {"text": "full text"} | |
| This adapter is honest: geometry is marked as 'unknown' since the | |
| provider doesn't supply coordinates. ALTO export will be refused | |
| (no word geometry), but PAGE export may be partial and the viewer | |
| will show text without positioned overlays. | |
| """ | |
| from __future__ import annotations | |
| from typing import TYPE_CHECKING | |
| from src.app.domain.models import ( | |
| CanonicalDocument, | |
| Geometry, | |
| Provenance, | |
| RawProviderPayload, | |
| ) | |
| from src.app.domain.models.status import EvidenceType, GeometryStatus, InputType | |
| from src.app.normalization.canonical_builder import CanonicalBuilder | |
| from src.app.providers.adapters.base import BaseAdapter | |
| if TYPE_CHECKING: | |
| from src.app.domain.models.geometry import GeometryContext | |
| class TextOnlyAdapter(BaseAdapter): | |
| """Adapter for the text_only family (mLLM without geometry).""" | |
| def family(self) -> str: | |
| return "text_only" | |
| def version(self) -> str: | |
| return "adapter.text_only.v1" | |
| def normalize( | |
| self, | |
| raw: RawProviderPayload, | |
| geometry_context: GeometryContext, | |
| *, | |
| document_id: str, | |
| source_filename: str | None = None, | |
| ) -> CanonicalDocument: | |
| payload = raw.payload | |
| if not isinstance(payload, dict): | |
| raise ValueError( | |
| f"text_only expects a dict payload, got {type(payload).__name__}" | |
| ) | |
| builder = CanonicalBuilder( | |
| document_id=document_id, | |
| input_type=InputType.IMAGE, | |
| filename=source_filename, | |
| ) | |
| page_w = geometry_context.source_width | |
| page_h = geometry_context.source_height | |
| page = builder.add_page("p1", 0, page_w, page_h) | |
| # Placeholder bbox covering the full page — marked unknown | |
| full_page_geo = Geometry( | |
| bbox=(0, 0, page_w, page_h), | |
| status=GeometryStatus.UNKNOWN, | |
| ) | |
| # Extract text blocks | |
| blocks = payload.get("blocks") | |
| if blocks and isinstance(blocks, list): | |
| texts = [ | |
| str(b.get("text", "")) | |
| for b in blocks | |
| if isinstance(b, dict) and b.get("text") | |
| ] | |
| else: | |
| # Single text blob — split into paragraphs | |
| full_text = str(payload.get("text", "")) | |
| if not full_text.strip(): | |
| raise ValueError("text_only payload has no text content") | |
| texts = [p.strip() for p in full_text.split("\n\n") if p.strip()] | |
| if not texts: | |
| texts = [full_text.strip()] | |
| for bi, block_text in enumerate(texts): | |
| block_id = f"tb{bi + 1}" | |
| prov = Provenance( | |
| provider=raw.provider_id, | |
| adapter=self.version, | |
| source_ref=f"$.blocks[{bi}]" if blocks else f"$.text.paragraph[{bi}]", | |
| evidence_type=EvidenceType.PROVIDER_NATIVE, | |
| ) | |
| region = page.add_text_region( | |
| region_id=block_id, | |
| geometry=full_page_geo, | |
| provenance=prov, | |
| ) | |
| # Split block into lines | |
| lines = [ln.strip() for ln in block_text.split("\n") if ln.strip()] | |
| if not lines: | |
| lines = [block_text] | |
| for li, line_text in enumerate(lines): | |
| line_id = f"tl{bi + 1}_{li + 1}" | |
| word_id = f"w{bi + 1}_{li + 1}" | |
| line = region.add_line( | |
| line_id, geometry=full_page_geo, provenance=prov, | |
| ) | |
| # Each line becomes a single word (no word segmentation available) | |
| line.add_word( | |
| word_id, | |
| text=line_text, | |
| geometry=full_page_geo, | |
| provenance=prov, | |
| ) | |
| return builder.build() | |