
    jh                    n   U d Z ddlmZ ddlZddlZddlZddlZddlmZm	Z	 ddl
mZ dZdddd	d
ddd	dZ eej                  j                  d ee      j#                         j$                  dz              Zd:dZd;dZd<dZdZdZdZeeefZe G d d             Ze G d d             Z G d de      Zeddd=dZh dZ h dZ!d he z  e!z  Z"d>d!Z#eddd=d"Z$d?d#Z%d$ Z&ed%d@d&Z'ed%d@d'Z(g d(Z)d)e*d*<   d+Z+d,Z,dd-dAd.Z-dd-dBd/Z.d;d0Z/dBd1Z0d2dd3	 	 	 dCd4Z1dDd5Z2dBd6Z3dEd7Z4dFd8Z5dGd9Z6y)Ha)  OCR engine for the Togen OCR Tool (DVI-490, plan DVI-463).

Wraps OCRmyPDF to turn an image-only / scanned PDF into a text-selectable
("searchable") PDF and emit the recognized text as a sidecar.

Board directive (D1): keep this a clean seam so a cloud fallback for
low-confidence pages can be added later without route/UI rework. The engine
returns an `OcrResult` carrying the extracted text plus a per-page metadata
list; a future cloud-fallback pass can inspect `pages[*].confidence` and
re-OCR only the weak pages, then the route/UI stay unchanged.
    )annotationsN)	dataclassfield)PathlocalLocalznOn-device OCR (Tesseract) with region heuristics. Runs entirely on the Togen host; no data leaves the network.)idlabeldescriptionclaudezClaude Vision (Anthropic API)zUses Claude's vision model to read text from scanned drawings. Requires an Anthropic API key configured in Assistant settings. Only available when the Assistant feature is enabled and accessible to users (or for admins).)r   r   OCR_SETTINGS_FILEzocr_settings.jsonc                 b    t         j                         D  cg c]  } t        |        c} S c c} w )zDReturn the registered backends as a list of public dicts (UI-ready).)OCR_BACKENDSvaluesdict)bs    !/var/www/html/togen/ocr_engine.pylist_backendsr   :   s#    )0023DG333s   ,c                 2   t         j                         rZ	 t        j                  t         j	                               } t        | t              r| j                  d      t        v r| d   S t        S t        S # t        j                  t        f$ r Y t        S w xY w)z>Return the configured backend id, falling back to the default.backend)r   is_filejsonloads	read_text
isinstancer   getr   JSONDecodeErrorOSErrorDEFAULT_OCR_BACKEND)datas    r   load_backendr!   ?   s      "	::/99;<D$%$((9*=*MI&  $$g. 		s   AA5 5BBc                    | t         vrt        d|       t        j                  t	        j
                  d| id             y)z>Persist the selected backend id. Raises ValueError if unknown.zunknown OCR backend r      )indentN)r   
ValueErrorr   
write_textr   dumpsr   s    r   save_backendr)   K   s;    l"/{;<<  Y,@!KL    z	skip-textzredo-ocrz	force-ocrc                  *    e Zd ZU dZded<   dZded<   y)PageInfozPer-page OCR metadata. `confidence` is the seam for a cloud fallback:
    OCRmyPDF/Tesseract does not surface a per-page confidence through the
    public API, so it stays None here until a richer engine populates it.intnumberNzfloat | None
confidence)__name__
__module____qualname____doc____annotations__r/    r*   r   r,   r,   \   s    M K#J#r*   r,   c                  8    e Zd ZU dZded<    ee      Zded<   y)	OcrResult strtext)default_factoryzlist[PageInfo]pagesN)r0   r1   r2   r:   r4   r   listr<   r5   r*   r   r7   r7   f   s    D#N!$7E>7r*   r7   c                      e Zd ZdZy)OcrEngineUnavailablezGRaised when the ocrmypdf package (or its host tools) are not installed.N)r0   r1   r2   r3   r5   r*   r   r?   r?   l   s    Qr*   r?   moder   c               x    |
t               }|t        vrt        }|dk(  rt        | ||      S t	        | ||      S )aa  OCR `in_path` into a searchable PDF at `out_path`.

    Dispatches to the configured backend (defaults to the admin-selected one in
    OCR_SETTINGS_FILE). Returns an OcrResult whose `text` is the recognized
    sidecar text and whose `pages` carries per-page metadata. Raises
    OcrEngineUnavailable if the backend's host tools are not installed.
    r   rA   )r!   r   r   _ocr_pdf_claude_ocr_pdf_local)in_pathout_pathrA   r   s       r   ocr_pdfrH   p   sC     .l"%(wt<<'8$77r*   >   .bmp.gif.jpg.png.tif.jpeg.tiff.webp>
   .doc.odp.ods.odt.ppt.rtf.xls.docx.pptx.xlsx.pdfc                f    t        t        |             j                  j                         t        v S )zDTrue if the filename's extension is something ocr_file() can handle.)r   r9   suffixlowerPROCESSABLE_EXTS)filenames    r   is_processablera      s&    H%%++-1AAAr*   c               2   t        |       } t        |      }| j                  j                         }|dk(  rt        | |||      S |t        v rKt        j                         5 }t        |      dz  }t        | |       t        ||t        |      cddd       S |t        v rOt        j                         5 }t        | t        |            }|t        d      t        ||||      cddd       S t        d|xs d       # 1 sw Y   rxY w# 1 sw Y   'xY w)a  OCR any supported file into a searchable PDF at `out_path`.

    PDFs go straight to ocr_pdf(). Images are wrapped into a one-page PDF first
    (and forced through OCR, since an image has no text layer). Office documents
    are converted to PDF via LibreOffice if it is installed on the host;
    otherwise OcrEngineUnavailable is raised with guidance. Unknown extensions
    raise ValueError.r[   r@   z	input.pdfNzConverting Office documents to PDF requires LibreOffice (soffice) on the Togen host. Convert the file to PDF first, or ask an admin to install LibreOffice.zUnsupported file type for OCR: z(none))r   r]   r^   rH   
IMAGE_EXTStempfileTemporaryDirectory_image_to_pdf	FORCE_OCROFFICE_EXTS_office_to_pdfr?   r%   )rF   rG   rA   r   exttdpdf_ins          r   ocr_filerm      s#    7mGH~H
..


 C
f}wtWEE
j((* 	Nb"X+F'6*68)WM	N 	N k((* 	Ib#GT"X6F~*> 
 68$H	I 	I 6sh6GH
II	N 	N
	I 	Is   !-D41DD
Dc                   	 ddl }|j                  t	        |             }	 |j                         }|j                          t        |      j                  |       y# t        $ r}t        d      |d}~ww xY w# |j                          w xY w)z9Wrap a raster image into a single-page PDF using PyMuPDF.r   Nz.PyMuPDF (fitz) is required to OCR image files.)	fitzImportErrorr?   openr9   convert_to_pdfcloser   write_bytes)img_pathpdf_outro   excdoc	pdf_bytess         r   rf   rf      s}    
 ))CM
"C&&(			Mi(  "<
	 			s"   A A9 	A6%A11A69Bc                d   ddl }ddl}|j                  d      xs |j                  d      }|sy	 |j                  |ddddt	        |      t	        |       gd	d	d
       t        |      t        |       j                  dz   z  }|j                         r|S dS # |j
                  t        f$ r Y yw xY w)zConvert an Office document to PDF via headless LibreOffice. Returns the
    output Path, or None if LibreOffice is unavailable / conversion failed.r   Nsofficelibreofficez
--headlessz--convert-topdfz--outdirT   )checkcapture_outputtimeoutr[   )
shutil
subprocesswhichrunr9   SubprocessErrorr   r   stemexists)srcwork_dir_shutilr   r{   outs         r   ri   ri      s     mmI&F'--*FGlNEXC2tS 	 	
 x.DINNV3
4C**,3(D( &&0 s   .B B/.B/rC   c               J   |t         vrt        dt          d|      	 ddl}t        |      }|j                  d      }t        ddit        d	dit        d
dii|   } |j                  t        |       t        |      ft        |      dd| d}|j                         r|j                  dd      }|r|j                  d      ng }t        t!        |            D 	cg c]  }	t#        |	dz          }
}	t%        ||
      S # t        $ r}t	        d      |d}~ww xY wc c}	w )z2Local (Tesseract/OCRmyPDF) backend implementation.zmode must be one of z, got r   Nzocrmypdf is not installed. The OCR host packages (ocrmypdf, tesseract, ghostscript, qpdf) are provisioned by the Linux Automation Engineer subtask (DVI-489).z.txt	skip_textTredo_ocr	force_ocrF)sidecarprogress_barr8   zutf-8replace)encodingerrors   r.   r:   r<   )VALID_MODESr%   ocrmypdfrp   r?   r   with_suffix	SKIP_TEXTREDO_OCRrg   ocrr9   r   r   splitrangelenr,   r7   )rF   rG   rA   r   rw   sidecar_pathflagsr:   
page_textsir<   s              r   rE   rE      sN   ;/}F4(KLL H~H''/L 	K&:t$K& 	E HLLGH L!	
  D%%wy%I &*D!rJ-23z?-CDXQU#DED$e,,C  ";
 		> Es   D D 	DDDc                  	 ddl m}m}m} 	 ddl}t        |       } t        |      }	 |j                  t        |             }g }	g }
	 t        t        |            D ]  }|j                  |      }|j                  dd      }|j                  |      }|j!                  d	      }	  ||d
g      }|j#                  d
d      }|	j%                  |       |
j%                  t'        |dz                 	 |j)                          ddl}|j-                  t        |       t        |             dj/                  |	      }t1        ||
      S # t        $ r}t        d      |d}~ww xY w# t        $ r}t        d      |d}~ww xY w# t        $ r}t        d|       |d}~ww xY w# ||f$ r}t        d|dz    d|       |d}~ww xY w# |j)                          w xY w)a#  Claude-vision OCR backend: rasterize each PDF page and send to Claude.

    Requires the org-level Anthropic API key configured in assistant_settings.json
    (via the Assistant admin settings). Falls back gracefully when the key is
    absent or the anthropic package is not installed.
    r   )extract_fieldsAssistantUnavailableAssistantErroru@   assistant module not found — cannot use Claude-vision backend.NuQ   PyMuPDF (fitz) is not installed — required for Claude-vision OCR rasterization.z*Failed to open PDF for Claude-vision OCR: g @)matrixpng	full_textr8   z!Claude-vision OCR failed on page r   z: r   r   r   )	assistantr   r   r   rp   r?   ro   r   rq   r9   	Exceptionr   r   	load_pageMatrix
get_pixmaptobytesr   appendr,   rs   r   copy2joinr7   )rF   rG   rA   r   r   r   rw   ro   rx   all_textr<   page_numpagematpix	img_bytesresult	page_textr   combineds                       r   rD   rD     s   RR 7mGH~H`iiG% HEc#h 	8H==*D++h1C///-CE*I	' M #JJ{B7	
 OOI&LLA67#	8& 			 
LLWs8}-yy"H(%00g  "N
	  "_
	  `"%OPSu#UV\__`& ).9 *71~RuM 			sv   
E	 E& F AG %F#1G 		E#EE#&	F /E;;F 	F FF #G*F??GG G)')customer_namezcustomer name)r   customer)r   zgeneral contractor)r   zcontractor name)r   
contractor)r   client)r   zsold to)project_namezproject name)r   zproject title)r   project)
job_numberz
job number)r   zjob no)r   zjob #)product_codezproduct code)r   z
catalog no)r   zmodel no)r   product)part_refzpart reference)r   zpart ref)r   zpart number)r   zpart no)r   z
piece mark)r   mark)weightzunit weight)r   r   )structure_idzstructure id)r   zassembly id)r   	structure)r   assembly)	spec_typez
spec./type)r   z	spec type)r   specification)r   spec)structure_sizezstructure size)r   size)stationr   )r   sta)production_datezproduction date)r   datezlist[tuple[str, str]]_FIELD_ALIASES              ?r(   c                   d}	 t        |       }t        |      }|r|S 	 t        | |      }t        |      S # t        $ r d}Y 4w xY w# t        $ r d}Y t        |      S w xY w)u  Read labeled metadata from a PDF's title block and map it to Create
    dialog fields. Returns a dict of {field: value} for whatever was found
    (possibly empty). Never raises for a malformed/unreadable PDF — extraction
    is best-effort prefill, so failures degrade to an empty result.

    Strategy: prefer the embedded text layer (vector drawings / carrier sheets),
    restricted to the title-block region for region heuristics. If that yields
    nothing (image-only/scanned PDF), fall back to OCR via the selected backend.
    r8   )_titleblock_text_embeddedr   _parse_labeled_fields_titleblock_text_ocr)pdf_pathr   r:   fieldsocr_texts        r   extract_title_block_fieldsr     s     D(2 #4(F'': !**     **s    6 A AAAAc                   	 t        |       }|r|j                         r|S 	 t        | |      xs dS # t        $ r d}Y 2w xY w# t        $ r Y yw xY w)u3  Return the raw title-block text (embedded layer, else OCR fallback) that
    feeds extract_title_block_fields. Exposed for callers that need to scan the
    text directly rather than rely on labeled "LABEL: value" pairs — e.g. the
    Project Builder tranId discovery (DVI-661). Best-effort: never raises.r8   )r   r   stripr   )r   r   r:   s      r   extract_title_block_textr     sc    
(2 

#Hg6<"<    s   2 A A A 	AAc           	     4   ddl m}  |t        |             }|j                  sy|j                  d   }|j                  }t        |j                        xs d}t        |j                        xs d}|t        z  |t        z  g fd}|j                  |       syj                  d        g }g }	d	}
d
}D ]  \  }}}}t        ddt        ||      z        }|
t        ||
z
        |k  r&|	j                  ||f       |
|n|
}
t        ||      }Z|j                  dj                  d t!        |	      D                     ||fg}	|}
|} |	r0|j                  dj                  d t!        |	      D                     dj                  |      S )zExtract embedded text from the title-block region of page 1, preserving
    rough line structure. Returns "" if the page has no usable text layer.r   )	PdfReaderr8   r   c           	         | r| j                         sy |d   |d   }}|k\  r)|	k  r#j                  ||| t        |xs d      f       y y y )N      r   )r   r   float)
r:   cmtm	font_dict	font_sizexychunksx_miny_maxs
          r   _visitorz+_titleblock_text_embedded.<locals>._visitor  sU    4::<!ube1:!u*MM1auY-=#'>?@ %:r*   )visitor_textc                    | d    | d   fS )Nr   r   r5   )cs    r   <lambda>z+_titleblock_text_embedded.<locals>.<lambda>  s    !uadm r*   )keyNr         @g? c              3  &   K   | ]	  \  }}|  y wNr5   .0_xts      r   	<genexpr>z,_titleblock_text_embedded.<locals>.<genexpr>  s     !=A!!=   c              3  &   K   | ]	  \  }}|  y wr   r5   r   s      r   r  z,_titleblock_text_embedded.<locals>.<genexpr>  s     9EBa9r  
)pypdfr   r9   r<   mediaboxr   widthheight_TITLEBLOCK_X_FRAC_TITLEBLOCK_Y_FRACextract_textsortmaxabsr   r   sorted)r   r   readerr   boxr	  r
  r   linescurcur_ycur_fsr   r   r  fstolr   r   r   s                    @@@r   r   r     s     s8}%F<<<<?D
--C#))#E3::%#F&&E''E46FA 	8, KK+K,E#%CEF 
1a#sSV_,-=CE	Nc1JJ1vAEE_FLL!=!==>q6(CEF
 SXX9VC[99:99Ur*   c                ~   	 ddl m}m} t	        j
                         5 }t        |      }	  |t        |             }|j                  s
	 ddd       y |       }|j                  |j                  d          |dz  }t        |d      5 }	|j                  |	       ddd       |dz  }
	 t        ||
t        |       	 t!        |
      cddd       S # t        $ r Y yw xY w# 1 sw Y   FxY w# t        $ r t        |       }Y _w xY w# t        $ r Y ddd       yw xY w# t        $ r Y ddd       yw xY w# 1 sw Y   yxY w)	a  OCR fallback for scanned/image-only PDFs: OCR page 1 via the configured
    backend, then re-read the *coordinate-aware* text from the resulting
    searchable PDF. Returns "" if OCR is unavailable.

    We deliberately do NOT use the linear OCR sidecar text here: title blocks are
    laid out as two columns (labels left, values right), and Tesseract's page
    segmentation emits the whole label column and then the whole value column, so
    "Customer Name:" and its value land on different lines and never pair up
    (DVI-504). Reading the OCR'd PDF's embedded text layer restores per-word
    positions, so _titleblock_text_embedded groups each label with its value by
    row and the same-line "LABEL: value" parser works.r   )r   	PdfWriterr8   Nz	page1.pdfwbzocr.pdfr@   )r  r   r  rp   rd   re   r   r9   r<   add_pagerq   writer   rH   rg   r?   r   )r   r   r   r  rk   td_pathr  writerrF   fhrG   s              r   r   r     sI   . 
	$	$	& "r(
	%s8}-F<<  [FOOFLLO,+Ggt$ !R !
 Y&	GXIwG	,X6+   ! ! 	%8nG	% $ 	' $	  	/ ,	- s   C D3C+6C+CC+"D3(D<
D	CCC(	$C++D?D3DD3	DD3DD3	D0%D3/D00D33D<F)rawr   c               N    t        | |||      }|rt        |      S t        |      S )u7  OCR a user-selected sub-region of one page (DVI-645).

    `frac_rect` is (x0, y0, x1, y1) as fractions (0..1) of the page's
    rendered width/height — the front-end maps the on-screen selection to
    fractions so it never needs to know the server render zoom.

    Strategy mirrors extract_title_block_fields: prefer embedded text inside
    the rectangle (instant, works for vector drawings / carrier sheets); if the
    region has no text layer (scanned/image PDF), render just the clip and OCR
    it. Never raises — region OCR is convenience and degrades to empty output.

    Returns the raw recognized text (str) when `raw` is True — used to fill one
    targeted field. Otherwise returns a {field: value} dict parsed from
    "LABEL: value" lines — used for the "OCR this region → best guess" flow.
    )_region_text_clean_region_textr   )r   page_no	frac_rectr"  r   r:   s         r   extract_regionr(    s-    " '9g>D
!$'' &&r*   c                2   	 ddl }	 |\  }}}}	 |j	                  t        |             }		 |dk  s||	j                  k\  r	 |	j                          y|	j                  |      }
|
j                  }|j                  |j                  ||j                  z  z   |j                  ||j                  z  z   |j                  ||j                  z  z   |j                  ||j                  z  z         }	 |
j                  d|      xs dj!                         }|r||	j                          S t#        |
||      |	j                          S # t        $ r Y yw xY w# t        t        f$ r Y yw xY w# t        $ r Y yw xY w# t        $ r d}Y qw xY w# |	j                          w xY w)uK   Embedded-text-in-rect → OCR-the-clip fallback. Returns "" on any failure.r   Nr8   r:   )clip)ro   r   	TypeErrorr%   rq   r9   
page_countrs   r   rectRectx0r	  y0r
  get_textr   _region_ocr_text)r   r&  r'  r   ro   fx0fy0fx1fy1rx   r   rr*  embeddeds                 r   r$  r$  '  s}   &S#siiH&Q;'S^^3 			 }}W%IIyyDD3= !$$qxx"7DD3= !$$qxx"7
	f48>BEEGH  			  dG4		7   z"     	H	 			so   E  E E$ F BF *%E3 F #F  	EEE! E!$	E0/E03F>F  FF Fc                   	 ddl }	 | j                  |j                  dd      |      }t	        j
                         5 }t        |      }|dz  }|dz  }	 |j                  |j                  d      d	      }		 |	j                         }
|	j                          |j                  |
       	 t        ||t        |
       	 |j                  t        |            }	 |j!                  d      j#                  d      xs dj%                         |j                          cddd       S # t        $ r Y yw xY w# t        $ r Y yw xY w# |	j                          w xY w# t        $ r Y ddd       yw xY w# t        $ r Y ddd       yt        $ r Y ddd       yw xY w# |j                          w xY w# t        $ r Y ddd       yw xY w# 1 sw Y   yxY w)u   Render the clip to an image-only PDF and OCR it, then read the
    coordinate-aware text from the result (the linear sidecar jumbles two-column
    title blocks — see _titleblock_text_ocr). Returns "" if OCR is unavailable.r   Nr8   r   )r   r*  z
region.pdfzregion_ocr.pdfr   )streamfiletyper@   r:   )ro   r   r   r   rd   re   r   rq   r   rr   rs   rt   rH   rg   r?   r9   r   r1  r   )r   r*  r   ro   r   rk   r  rF   rG   img_docry   ods               r   r2  r2  I  s   ooT[[c%:oF 
	$	$	& "r(L(--	iis{{5'9EiJG #224		*	GXIwG
	3x=)BQ008>BEEG
1       	 	 $ 	! "  	% "	 
 	5 2	3 s   D$ #D3 G"E8E!E*E.>F,1F
F,$	D0/D03	D?>D?EE	E+ G*E++G.	F7GF	GFGF))F,,	G 5G?G  GGc                R    | syt        j                  dd|       j                         S )zwCollapse a region's recognized text into a single trimmed line, suitable
    for dropping straight into one form field.r8   \s+r   )resubr   )r:   s    r   r%  r%  r  s&     66&#t$**,,r*   c                L   | si S t        t        d d      }i }| j                         D ]  }t        j                  d|      D cg c]  }|j                          }}t        |      D ]  \  }}|rd|vrd|vr|D ]  \  }}	||v rt        j                  t        j                  |	      dz   |t        j                        }
|
sIt        |
j                  d            }|s0|dz   t        |      k  r||dz      nd	}|rd|vrd|vrt        |      }|r|||<       |S c c}w )
a  Parse "LABEL: value" pairs out of title-block text into Create fields.

    Splits each line into column segments on runs of 2+ spaces/tabs (title
    blocks are grid-like), then matches the longest known label that a segment
    starts with. First non-empty value per field wins.c                    t        | d         S )Nr   )r   )fas    r   r   z'_parse_labeled_fields.<locals>.<lambda>  s    #be* r*   T)r   reversez
\s{2,}|\t+:#z\s*[:#]+\s*(.*)$r   r8   )r  r   
splitlinesr@  r   r   	enumeratematchescape
IGNORECASE_clean_valuegroupr   )r:   pairsr   raw_linessegsr   seg	fieldnamealiasmvalnxts                r   r   r   z  s7    	>'<dKECOO% #%88M8#DEa	EEo 	FAs3c>cn$)  	5#HHRYYu-0CCS"--X"1771:. *+QT):$q1u+Cs#~#S.*3/%(C	N%	0 J/ Fs   D!c                    t        j                  dd|       j                  d      j                         } | j                         dv ry| dd S )zTidy a captured value: collapse whitespace, strip stray separators, cap
    length. Drops values that are obviously empty placeholders.r?  r   z 	:#-_/|)r8   zn/ananonetbd-r8   Nx   )r@  rA  r   r^   )rW  s    r   rM  rM    sH     &&c
"
(
(
5
;
;
=C
yy{;;t9r*   )returnz
list[dict])r_  r9   )r   r9   r_  None)rA   r9   r   
str | Noner_  r7   )r_  bool)r_  r`  )rA   r9   r_  r7   )r   ra  r_  dict[str, str])r   ra  r_  r9   )r&  r-   r"  rb  r   ra  )r&  r-   r   ra  r_  r9   )r:   r9   r_  r9   )r:   r9   r_  rc  )rW  r9   r_  r9   )7r3   
__future__r   r   osr@  rd   dataclassesr   r   pathlibr   r   r   environr   __file__resolveparentr   r   r!   r)   r   r   rg   r   r,   r7   RuntimeErrorr?   rH   rc   rh   r_   ra   rm   rf   ri   rE   rD   r   r4   r  r  r   r   r   r   r(  r$  r2  r%  r   rM  r5   r*   r   <module>rm     s  
 #  	 	  (    =	 0(		, JJNNT(^335<<?RR 4
	M 		(I. $ $ $ 8 8 8
R< R /8t 8. Q
(8j(;6 B
 09PT J>) )* 6? (-V 7@ <1R()% (b    CG +6 AE "7t(X  %D''/9'.D&R-#Lr*   