175 lines
7.8 KiB
Python
175 lines
7.8 KiB
Python
import hashlib
|
|
import json
|
|
from typing import List, Dict, Any, Tuple
|
|
from sqlalchemy.orm import Session
|
|
from models.template_models import Template, TemplateDocument, DocumentLayout, TemplateRecognitionHistory
|
|
from schemas.template_schemas import TemplateRecognitionResult
|
|
|
|
class TemplateRecognitionEngine:
|
|
def __init__(self, db: Session):
|
|
self.db = db
|
|
|
|
def generate_fingerprint(self, layouts: List[DocumentLayout]) -> str:
|
|
"""
|
|
Generate a structural fingerprint based on the document's layout.
|
|
We use relative positions of HEADER, VENDOR, TABLE_HEADER blocks.
|
|
"""
|
|
key_blocks = []
|
|
for layout in layouts:
|
|
if layout.block_type in ["HEADER", "VENDOR", "TABLE_HEADER", "TOTAL"]:
|
|
key_blocks.append({
|
|
"type": layout.block_type,
|
|
"text": layout.text_value[:50] if layout.text_value else "", # first 50 chars
|
|
"rx": round(float(layout.x_coordinate) / 100) if layout.x_coordinate else 0, # relative bucket x
|
|
"ry": round(float(layout.y_coordinate) / 100) if layout.y_coordinate else 0 # relative bucket y
|
|
})
|
|
|
|
# Sort top to bottom, left to right
|
|
key_blocks.sort(key=lambda b: (b['ry'], b['rx']))
|
|
|
|
fingerprint_data = json.dumps(key_blocks)
|
|
return hashlib.sha256(fingerprint_data.encode('utf-8')).hexdigest()
|
|
|
|
def recognize_template(self, document_id: int) -> TemplateRecognitionResult:
|
|
"""
|
|
Matches a document against existing templates using a 4-level weighted score:
|
|
Level 1: Vendor Match (40%)
|
|
Level 2: Header Similarity (20%)
|
|
Level 3: Layout/Fingerprint Similarity (20%)
|
|
Level 4: Coordinate Similarity (20%)
|
|
Threshold: 85%
|
|
"""
|
|
layouts = self.db.query(DocumentLayout).filter(DocumentLayout.fk_document_id == document_id).all()
|
|
if not layouts:
|
|
return TemplateRecognitionResult(templateMatched=False)
|
|
|
|
doc_fingerprint = self.generate_fingerprint(layouts)
|
|
|
|
# Extract features for scoring
|
|
doc_vendor = self._extract_vendor_name(layouts)
|
|
doc_headers = self._extract_headers(layouts)
|
|
|
|
templates = self.db.query(Template).filter(Template.active_flag == True).all()
|
|
best_match = None
|
|
highest_score = 0
|
|
|
|
for template in templates:
|
|
score = 0.0
|
|
|
|
# Level 1: Fingerprint Exact Match (counts for Layout + Vendor + Header if exact)
|
|
if template.template_fingerprint == doc_fingerprint:
|
|
score += 100.0
|
|
else:
|
|
# Need to load template sample mapping to compare heuristics
|
|
# In a real system, we'd compare against the mapped fields' coordinates
|
|
score += self._calculate_heuristic_score(template, doc_vendor, doc_headers, layouts)
|
|
|
|
if score > highest_score:
|
|
highest_score = score
|
|
best_match = template
|
|
|
|
# Store history
|
|
if best_match:
|
|
history = TemplateRecognitionHistory(
|
|
fk_template_id=best_match.pk_template_id,
|
|
fk_document_id=document_id,
|
|
recognition_score=highest_score,
|
|
matched_flag=(highest_score >= 85.0)
|
|
)
|
|
self.db.add(history)
|
|
self.db.commit()
|
|
|
|
if best_match and highest_score >= 85.0:
|
|
extracted_fields = self._auto_map_fields(best_match, layouts)
|
|
return TemplateRecognitionResult(
|
|
templateMatched=True,
|
|
templateId=best_match.pk_template_id,
|
|
confidence=highest_score,
|
|
extractedFields=extracted_fields
|
|
)
|
|
|
|
return TemplateRecognitionResult(templateMatched=False)
|
|
|
|
def _extract_vendor_name(self, layouts: List[DocumentLayout]) -> str:
|
|
for layout in layouts:
|
|
if layout.block_type == "VENDOR":
|
|
return layout.text_value.lower()
|
|
return ""
|
|
|
|
def _extract_headers(self, layouts: List[DocumentLayout]) -> List[str]:
|
|
headers = []
|
|
for layout in layouts:
|
|
if layout.block_type == "TABLE_HEADER" and layout.text_value:
|
|
headers.append(layout.text_value.lower())
|
|
return headers
|
|
|
|
def _calculate_heuristic_score(self, template: Template, doc_vendor: str, doc_headers: List[str], layouts: List[DocumentLayout]) -> float:
|
|
score = 0.0
|
|
|
|
# This requires the template to have some stored metadata or we check its fields
|
|
# E.g., if template has a field "Vendor Name" mapped to a specific text
|
|
|
|
# 1. Vendor Match (40%)
|
|
# For this mockup, we check if the template name matches the vendor
|
|
if template.template_name.lower() in doc_vendor or doc_vendor in template.template_name.lower():
|
|
score += 40.0
|
|
|
|
# 2. Header Similarity (20%)
|
|
# Check if template fields exist that match doc_headers
|
|
field_labels = [f.field_label.lower() for f in template.fields]
|
|
header_matches = sum(1 for h in doc_headers if any(h in fl or fl in h for fl in field_labels))
|
|
if len(doc_headers) > 0:
|
|
score += (header_matches / len(doc_headers)) * 20.0
|
|
|
|
# 3. Layout / Coordinate Similarity
|
|
# We can check the template's previous mappings coordinates against current document
|
|
# If coordinates are within a tolerance, we add score
|
|
mapping_count = 0
|
|
match_count = 0
|
|
for mapping in template.mappings:
|
|
mapping_count += 1
|
|
# Find a layout block in current document that is near mapping coordinates
|
|
for layout in layouts:
|
|
if layout.page_no == mapping.page_no:
|
|
# check distance
|
|
if mapping.x_coordinate and mapping.y_coordinate and layout.x_coordinate and layout.y_coordinate:
|
|
dx = abs(float(mapping.x_coordinate) - float(layout.x_coordinate))
|
|
dy = abs(float(mapping.y_coordinate) - float(layout.y_coordinate))
|
|
if dx < 50 and dy < 20: # arbitrary tolerance
|
|
match_count += 1
|
|
break
|
|
|
|
if mapping_count > 0:
|
|
coord_score = (match_count / mapping_count) * 40.0 # Layout 20% + Coord 20%
|
|
score += coord_score
|
|
|
|
return score
|
|
|
|
def _auto_map_fields(self, template: Template, layouts: List[DocumentLayout]) -> List[dict]:
|
|
extracted = []
|
|
for mapping in template.mappings:
|
|
# Find closest block in new document
|
|
best_block = None
|
|
min_dist = float('inf')
|
|
|
|
for layout in layouts:
|
|
if layout.page_no == mapping.page_no:
|
|
if mapping.x_coordinate and mapping.y_coordinate and layout.x_coordinate and layout.y_coordinate:
|
|
dx = abs(float(mapping.x_coordinate) - float(layout.x_coordinate))
|
|
dy = abs(float(mapping.y_coordinate) - float(layout.y_coordinate))
|
|
dist = dx**2 + dy**2
|
|
if dist < min_dist and dist < 5000: # tolerance squared
|
|
min_dist = dist
|
|
best_block = layout
|
|
|
|
if best_block:
|
|
extracted.append({
|
|
"field_id": mapping.fk_template_field_id,
|
|
"field_label": mapping.field.field_label if mapping.field else "",
|
|
"value": best_block.text_value,
|
|
"confidence": 95.0, # arbitrary high confidence for coordinate match
|
|
"layout_id": best_block.pk_document_data_id
|
|
})
|
|
|
|
return extracted
|