ó
    é›tjY*  ã                  ó:  • S r SSKJr  SSKrSSKJr  SSKJr  SSK	J
r
  SSKJrJrJrJrJrJr  SSKJr  SS	KJrJr  \(       a  SS
KJr  \S   rSrSS jr " S S\5      rSS jrSS jrSS jrSS jr       SS jr!SS jr"SS jr#\4     S S jjr$S!S jr%g)"uÒ  
Camelot acquisition layer: get candidate cell grids out of a PDF.

This module knows nothing about LLMs or the pipeline graph. It renders
page images, runs both Camelot flavors in spawn-context subprocesses,
and converts each Camelot DataFrame into a serializable
`CamelotCandidate`.

Both Camelot flavors run in parallel:

- `lattice` â€” visible grid lines.
- `stream` â€” whitespace-based; more permissive.

There is deliberately no lattice-first / fall-back-to-stream gate: on
pages where tables are aligned by whitespace alone, lattice still
returns grids â€” empty shells â€” so "fall back only when lattice finds
nothing" let those shells through as false positives. `is_content_empty`
catches the shells instead.
é    )ÚannotationsN)ÚProcessPoolExecutor)ÚTimeoutError)ÚPath)ÚTYPE_CHECKINGÚAnyÚListÚLiteralÚOptionalÚTuple)Úlogger)Ú	BaseModelÚField)Ú	DataFrame)ÚlatticeÚstreamg      N@c                óÌ   • SSK Jn  U" [        U 5      US[        U5      S9n/ n[        USS9 H2  u  pgUSUS S	3-  nUR	                  US
5        UR                  U5        M4     U$ )Nr   )Úconvert_from_pathÚpng)ÚdpiÚfmtÚoutput_folderé   ©Ústartzpage-Ú04dz.pngÚPNG)Ú	pdf2imager   ÚstrÚ	enumerateÚsaveÚappend)	Úsourcer   Úout_dirr   ÚimagesÚpathsÚiÚimgÚpaths	            ÚC/home/mande/repo/quber/src/quber/core/extractors/camelot/acquire.pyÚrender_pagesr+   (   sl   € Ý+áœs 6›{°¸ÌcÐRYËlÑ[€FØ€EÜ˜F¨!Ô,‰ˆØ˜5  3  tÐ,Ñ,ˆØ�‰��uÔØ�‰�TÖñ -ð €Ló    c                  ó¾   • \ rS rSr% Sr\" SS9rS\S'   S\S'   \" S	S
9rS\S'   Sr	S\S'   \" SSSS9r
S\S'   \" \SS9rS\S'   \" \SS9rS\S'   \" SS9rS\S'   Srg)ÚCamelotCandidateé4   a§  Serializable per-Camelot-table output. Crosses the spawn-process
boundary; only picklable primitives, no Camelot internals.

`cells` is the raw grid (rows of cell strings) straight from Camelot's
DataFrame; `markdown` is its rendering. The correspondence flow works
on `cells` so it can assemble, audit and fill at the grid level and
render markdown once at the end; the legacy classifier/unifier path
consumes `markdown`.
z1Stable id assigned by the parent; flavor-page-idx)Údescriptionr   Úcandidate_idÚFlavorÚflavorr   )ÚgeÚintÚpageNz+Optional[Tuple[float, float, float, float]]Úbboxç        g      Y@)Údefaultr4   ÚleÚfloatÚaccuracyz)Camelot's raw cell grid (rows of strings))Údefault_factoryr0   úList[List[str]]Úcellsa;  Per-cell geometry aligned 1:1 with `cells`: same row/column shape, each entry the Camelot cell box (x1, y1, x2, y2) in PDF points with a bottom-left page origin, or None where Camelot exposed no box for that cell. Empty for candidates not built directly from a Camelot grid (e.g. assembled or recovered candidates).ú7List[List[Optional[Tuple[float, float, float, float]]]]Ú
cell_boxesz;Camelot's raw markdown for this table; rendering of `cells`Úmarkdown© )Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__Ú__doc__r   r1   Ú__annotations__r6   r7   r<   Úlistr?   rA   rB   Ú__static_attributes__rC   r,   r*   r.   r.   4   s“   ‡ ññ Ð*]Ñ^€L�#Ó^ØƒNÙ˜‘€Dˆ#ÓØ8<€DÐ
5Ó<Ù C¨C°EÑ:€HˆeÓ:Ù"ØÐ*Uñ€Eˆ?ó ñ KPØðUñK€JÐGó ñ Ð&cÑd€HˆcÖdr,   r.   c                ó  ^^• U (       d  g[        S U  5       5      mSU4S jjmT" U S   5      SSR                  S [        T5       5       5      -   S-   /nUR                  U4S	 jU S
S  5       5        SR                  U5      $ )zÆRender a cell grid (rows of strings) to a markdown table. Row 0 is
the header; rows are padded to the widest row so the column count is
uniform. Single rendering point for the structured-grid flow.
Ú c              3  ó8   #   • U  H  n[        U5      v •  M     g 7f©N©Úlen©Ú.0Úrs     r*   Ú	<genexpr>Ú#grid_to_markdown.<locals>.<genexpr>Z   ó   é € Ð&¢˜1”�A—�¢ùó   ‚c                óš   >• U  Vs/ s H  n[        U5      PM     snS/T[        U 5      -
  -  -   nSSR                  U5      -   S-   $ s  snf )NrM   ú| ú | ú |)r   rQ   Újoin)ÚrowÚcÚpaddedÚwidths      €r*   ÚrenderÚ grid_to_markdown.<locals>.render\   sN   ø€ Ù"%Ó&¢#˜Q”#�a–&¡#Ñ&¨"¨°¼¸S»Ñ1AÑ)BÑBˆØ�e—j‘j Ó(Ñ(¨4Ñ/Ð/ùò 's   †Ar   rZ   r[   c              3  ó&   #   • U  H  nS v •  M	     g7f©z---NrC   ©rS   Ú_s     r*   rU   rV   `   s   é € Ð0MÂ¸1µÂùó   ‚r\   c              3  ó4   >#   • U  H  nT" U5      v •  M     g 7frO   rC   )rS   r^   rb   s     €r*   rU   rV   a   s   øé € Ð2ª	 ‘˜—�ª	ùs   ƒr   NÚ
)r^   ú	List[str]Úreturnr   )Úmaxr]   ÚrangeÚextend)r?   Úlinesrb   ra   s     @@r*   Úgrid_to_markdownrq   S   sz   ù€ ö
 ØÜÑ&¡Ó&Ó&€E÷0ñ �E˜!‘HÓ˜t e§j¡jÑ0MÄÀeÄÓ0MÓ&MÑMÐPTÑTÐU€EØ	‡L�LÔ2¨¨a¨b©	Ó2Ô2Ø�9‰9�UÓÐr,   c                óŠ   • SnU S-  n U (       a4  [        U S-
  S5      u  p[        [        S5      U-   5      U-   nU (       a  M4  U$ )zB0-based column index to a spreadsheet letter: 0->A, 25->Z, 26->AA.rM   r   é   ÚA)ÚdivmodÚchrÚord)ÚindexÚoutÚrems      r*   Úcolumn_letterr{   e   sJ   € à
€CØ	ˆQ�J€EÞ
Ü˜E A™I rÓ*‰
ˆÜ”#�c“(˜S‘.Ó! CÑ'ˆ÷ ˆ%ð €Jr,   c                ó,  ^^^• U (       d  g[        S U  5       5      mSS jmSUU4S jjmT" SU S   5      SSR                  S	 [        T5       5       5      -   S
-   /nUR                  U4S j[	        U SS SS9 5       5        SR                  U5      $ )u”  Render the grid with each non-empty cell's address printed inside it.

Every cell with text carries an inline tag â€” `[B3] 1,637` means data
column B (A=0), grid row 3 (1-based) â€” so the agent READS an address off
the label sitting next to the text; it never counts rows, columns, or
pipes. Blank cells stay blank. The tags are reference only â€” the agent's
corrected output must never contain them.
rM   c              3  ó8   #   • U  H  n[        U5      v •  M     g 7frO   rP   rR   s     r*   rU   Ú-grid_to_addressed_markdown.<locals>.<genexpr>z   rW   rX   c                óh   • [        U5      R                  5       (       a  S[        U5       U  SU 3$ S$ )NÚ[z] rM   )r   Ústripr{   )rT   r_   Útexts      r*   ÚtagÚ'grid_to_addressed_markdown.<locals>.tag|   s4   € Ü47¸³I·O±O×4EÑ4E�”= Ó#Ð$ Q C r¨$¨Ð0ÐMÈ2ÐMr,   c                óÄ   >^ • U Vs/ s H  n[        U5      PM     snS/T[        U5      -
  -  -   nSSR                  U U4S j[        U5       5       5      -   S-   $ s  snf )NrM   rZ   r[   c              3  ó:   >#   • U  H  u  pT" TX5      v •  M     g 7frO   rC   )rS   r_   ÚvrT   rƒ   s      €€r*   rU   ÚGgrid_to_addressed_markdown.<locals>.render_addressed.<locals>.<genexpr>�   s   øé € Ð LÒ:K±$°!¡ Q¨§ Ò:Kùs   ƒr\   )r   rQ   r]   r    )rT   r^   r_   r`   rƒ   ra   s   `   €€r*   Úrender_addressedÚ4grid_to_addressed_markdown.<locals>.render_addressed   sY   ù€ Ù"%Ó&¢#˜Q”#�a–&¡#Ñ&¨"¨°¼¸S»Ñ1AÑ)BÑBˆØ�e—j‘jÕ L¼)ÀFÔ:KÓ LÓLÑLÈtÑSÐSùò 's   ‡Ar   r   rZ   r[   c              3  ó&   #   • U  H  nS v •  M	     g7fre   rC   rf   s     r*   rU   r~   ƒ   s   é € Ð=ZÊ\È½eÊ\ùrh   r\   c              3  ó8   >#   • U  H  u  pT" X5      v •  M     g 7frO   rC   )rS   r'   r^   r‰   s      €r*   rU   r~   „   s   øé € ÐVÒ8U©f¨aÑ! !×)Ð)Ò8Uùs   ƒNé   r   rj   )rT   r5   r_   r5   r‚   r   rl   r   )rT   r5   r^   rk   rl   r   )rm   r]   rn   ro   r    )r?   rp   r‰   rƒ   ra   s     @@@r*   Úgrid_to_addressed_markdownrŽ   o   s’   ú€ ö ØÜÑ&¡Ó&Ó&€EôN÷Tð Tñ ˜a  q¡Ó*¨D°5·:±:Ñ=ZÌUÐSXÌ\Ó=ZÓ3ZÑ,ZÐ]aÑ,aÐb€EØ	‡L�LÔV¼	À%ÈÈÀ)ÐSTÒ8UÓVÔVØ�9‰9�UÓÐr,   c           
     ó¤   • U R                   R                  5        VVs/ s H  o Vs/ s H  n[        U5      PM     snPM      snn$ s  snf s  snnf )z5Camelot DataFrame -> raw cell grid (rows of strings).)ÚvaluesÚtolistr   )Údfr^   r_   s      r*   Údf_to_cellsr“   ˆ   s?   € à-/¯Y©Y×-=Ñ-=Ô-?Ô@Ò-? c˜SÓ!šS˜ŒS�ŽV™SÔ!Ñ-?Ò@Ð@ùÒ!ùÓ@s   ž	A§A¼AÁAc           
     ó8  ^	• U b  [        U 5      O/ n/ n[        U5       H÷  u  pEU[        U5      :  a  [        X$   5      O/ n/ n[        [        U5      5       H©  nU[        U5      :  a  Xh   OSm	T	b}  [	        U	4S jS 5       5      (       ac  UR                  [        T	R                  5      [        T	R                  5      [        T	R                  5      [        T	R                  5      45        M˜  UR                  S5        M«     UR                  U5        Mù     U$ )u¨  Per-cell boxes aligned to `grid`'s shape from Camelot's Cell objects.

`raw_cells` is Camelot's `table.cells` â€” rows of `Cell` objects, each with
x1/y1/x2/y2 in PDF points (bottom-left origin). `table.df` (hence `grid`) is
built from that same grid, so index i,j lines up; still, we clamp to `grid`'s
shape and fill None for any position Camelot did not cover, so `cell_boxes`
is always exactly the same shape as `cells`.
Nc              3  ó<   >#   • U  H  n[        TU5      v •  M     g 7frO   )Úhasattr)rS   ÚaÚcells     €r*   rU   Ú!cells_to_boxes.<locals>.<genexpr>Ÿ   s   øé € Ð'[ÒBZ¸Q¬°°a×(8Ð(8ÒBZùs   ƒ)Úx1Úy1Úx2Úy2)rJ   r    rQ   rn   Úallr"   r;   rš   r›   rœ   r�   )
Ú	raw_cellsÚgridÚrowsÚboxesr'   Úgrid_rowÚraw_rowÚout_rowÚjr˜   s
            @r*   Úcells_to_boxesr§   �   sß   ø€ ð (Ñ3Œ4�	Œ?¸€DØEG€EÜ  –‰ˆØ#$¤s¨4£y£=”$�t‘w”-°bˆØEGˆÜ”s˜8“}Ö%ˆAØ!"¤S¨£\Ó!1�7’:°tˆDØÑ¤CÔ'[ÑBZÓ'[×$[Ñ$[Ø—‘¤ d§g¡g£´°d·g±g³ÄÀdÇgÁgÃÔPUÐVZ×V]ÑV]ÓP^Ð_Ö`à—‘˜tÖ$ñ &ð 	�‰�WÖñ 'ð €Lr,   c                ó*   • [        [        U 5      5      $ rO   )rq   r“   )r’   s    r*   Údf_to_markdownr©   §   s   € ÜœK¨›OÓ,Ð,r,   c                óŠ  • SSK nSSKJn  SSKJn  / nUR                  U SUS9n0 n[        U5       GH  u  p‰[        U	SS5      n
U
b  [        U
5      OSn[        U	S	S5      nU(       a  [        U5      OSn[        U	S
0 5      =(       d    0 n[        UR                  SS5      =(       d    S5      n[        U	R                  5      n[        [        U	SS5      U5      nX·;  a  U" [        U 5      U5      u  nnnUU4X{'   X{   u  nnU" UUUU5      nUR!                  [#        U SU SU 3UUUUUU[%        U5      S95        GM     U$ )z¥Top-level entrypoint for the spawned process. Imports camelot
fresh in the child so OpenCV initialization stays inside the worker
and never crosses a fork boundary.
r   N)Ú
page_words)Útighten_cell_boxesrž   )Úpagesr3   r6   r   Ú_bboxÚparsing_reportr<   r8   r?   z-pÚ-)r1   r3   r6   r7   r<   r?   rA   rB   )ÚcamelotÚquber.agents.completenessr«   Ú%quber.core.extractors.camelot.tightenr¬   Úread_pdfr    Úgetattrr5   Útupler;   Úgetr“   r’   r§   r   r"   r.   rq   )Ú
source_strr3   r±   r«   r¬   ry   ÚtablesÚwords_cacheÚidxÚtableÚraw_pager6   Úraw_bboxr7   Úreportr<   r?   rA   rg   Úpage_hÚwordss                        r*   Úcamelot_workerrÂ   «   sZ  € ó
 å4ÝHà"$€CØ×Ñ˜j°¸fÐÐE€Fà68€KÜ ×'‰
ˆÜ˜5 &¨!Ó,ˆØ (Ñ 4Œs�8Œ}¸!ˆô ˜5 '¨4Ó0ˆÞ"*Œu�XŒ°ˆÜ˜Ð 0°"Ó5×;¸ˆÜ˜Ÿ™ J°Ó4×;¸Ó<ˆÜ˜EŸH™HÓ%ˆÜ#¤G¨E°7¸DÓ$AÀ5ÓIˆ
ØÓ"Ù)¬$¨zÓ*:¸DÓAÑˆAˆv�uØ!'¨ ˆKÑØ#Ñ)‰ˆ�Ù'¨¨z¸5À&ÓIˆ
Ø�
‰
ÜØ &˜x r¨$¨¨q°°Ð6ØØØØ!ØØ%Ü)¨%Ó0ñ	÷	
ñ! (ð8 €Jr,   c                óœ  • [         R                  " S5      n/ nSS/n[        SUS9 nU Vs0 s H#  ofUR                  [        [        U 5      U5      _M%     nnUR                  5        H$  u  ph UR                  UR                  US95        M&     SSS5        U(       d!  [        R                  " SU R                  5        U$ s  snf ! [         a5    [        R                  " SUUU R                  S9  UR                  5          M�  [         a,  n	[        R                  " S	UU R                  U	S
9   Sn	A	MÍ  Sn	A	ff = f! , (       d  f       N³= f)u	  Run lattice + stream concurrently in spawn-context subprocesses.

A flavor that times out or raises is logged ERROR and contributes
zero candidates; the other flavor's output is still returned. If
BOTH fail, returns an empty list â€” the caller decides what to do.
Úspawnr   r   r�   )Úmax_workersÚ
mp_context)Útimeoutz7camelot {flavor} timed out after {timeout}s on {source})r3   rÇ   r#   z*camelot {flavor} raised on {source}: {exc})r3   r#   ÚexcNz5camelot produced 0 candidates from BOTH flavors on {})ÚmpÚget_contextr   ÚsubmitrÂ   r   Úitemsro   ÚresultÚFutureTimeoutErrorr   ÚerrorÚnameÚcancelÚ	Exception)
r#   Ú	timeout_sÚctxÚ
candidatesÚflavorsÚpoolr3   ÚfuturesÚfuturerÈ   s
             r*   Úrun_camelot_flavors_parallelrÚ   Ø   s  € ô �.Š.˜Ó
!€CØ)+€JØ&¨Ð1€Gä	¨°sÒ	;¸tÙZaÓbÒZaÐPV˜4Ÿ;™;¤~´s¸6³{ÀFÓKÒKÑZaˆÐbØ%Ÿm™mžo‰NˆFðØ×!Ñ! &§-¡-¸	 -Ð"BÖCñ .÷ 
<ö* Ü�ŠÐLÈfÏkÉkÔZØÐùò- cøô &ó  Ü—’ØMØ!Ø%Ø!Ÿ;™;ò	ð —‘—Üó Ü—’Ø@Ø!Ø!Ÿ;™;Ø÷	ð ûðú÷ 
<Õ	;úsR   §D=¬*CÁD=Á.CÂD=ÃD=Ã;D:ÄD=Ä	D:Ä!D5Ä/D=Ä5D:Ä:D=Ä=
Ec                ó0  • U R                  5       (       d  gU R                  5        Hg  nUR                  5       R                  S5      R                  S5       Vs/ s H  o"R                  5       PM     nn[        S U 5       5      (       d  Mg    g   gs  snf )zùA Camelot output is content-empty when every non-delimiter cell
is whitespace. Lattice's false-positive shells on whitespace-aligned
PDFs look like a 1-row grid of empty pipes; this catches them
before they reach the classifier or correction stage.
TÚ|c              3  óT   #   • U  H  o=(       a    [        U5      S S1::  + v •  M      g7f)r°   Ú N)Úset)rS   r_   s     r*   rU   Ú#is_content_empty.<locals>.<genexpr>
  s#   é € Ð=²u°!×-œ˜Q› C¨ :Ò-Ô-²uùs   ‚&(F)r�   Ú
splitlinesÚsplitÚany)rB   Úliner_   r?   s       r*   Úis_content_emptyrå      s|   € ð �>‰>×ÑØØ×#Ñ#Ö%ˆØ$(§J¡J£L×$6Ñ$6°sÓ$;×$AÑ$AÀ#Ô$FÓGÒ$F˜q—‘–Ñ$FˆÐGÜÑ=±uÓ=×=Ó=Ùñ &ð ùò Hs   ÁB)r#   r   r   r5   r$   r   rl   z
List[Path])r?   r>   rl   r   )rx   r5   rl   r   )r’   r   rl   r>   )rŸ   r   r    r>   rl   r@   )r’   r   rl   r   )r¸   r   r3   r2   rl   úList[CamelotCandidate])r#   r   rÓ   r;   rl   ræ   )rB   r   rl   Úbool)&rH   Ú
__future__r   ÚmultiprocessingrÉ   Úconcurrent.futuresr   r   rÎ   Úpathlibr   Útypingr   r   r	   r
   r   r   Úlogurur   Úpydanticr   r   Úpandasr   r2   ÚCAMELOT_FLAVOR_TIMEOUT_Sr+   r.   rq   r{   rŽ   r“   r§   r©   rÂ   rÚ   rå   rC   r,   r*   Ú<module>rñ      s¸   ðñõ( #ã Ý 2Ý AÝ ß E× Eå ß %æÝ à	Ð$Ñ	%€àÐ ô	ôe�yô eô>ô$ôô2Að
ØðØ)ðà<ôô4-ô*ð^ 0ð%Øð%àð%ð õ%õPr,   