File size: 6,762 Bytes
d52dd8c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f99e19b
 
 
 
 
 
 
 
d52dd8c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f99e19b
d52dd8c
 
 
f99e19b
 
 
 
 
d52dd8c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
"""
extractor.py β€” Structured entity & key-value extraction from OCR text.
Features:
  - Date extraction (multiple formats)
  - Monetary amount and currency detection
  - Email / phone / URL extraction
  - Invoice / receipt field detection
  - PII detection & redaction (Aadhaar, PAN, credit cards, SSN)
  - Document type classifier
"""

import re
from typing import Dict, List, Any, Optional


# ─────────────────────────────────────────────
# Regex Patterns
# ─────────────────────────────────────────────

DATE_PATTERNS = [
    r"\b(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})\b",
    r"\b(\d{4}[/-]\d{1,2}[/-]\d{1,2})\b",
    r"\b(\d{1,2}\s+(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\s+\d{4})\b",
    r"\b((?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\s+\d{1,2},?\s+\d{4})\b",
]

MONEY_PATTERNS = [
    r"(?:β‚Ή|Rs\.?|INR|USD|\$|€|Β£|EUR|GBP)\s*[\d,]+(?:\.\d{1,2})?",
    r"[\d,]+(?:\.\d{1,2})?\s*(?:β‚Ή|Rs\.?|INR|USD|EUR|GBP)",
]

EMAIL_PATTERN = r"\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b"
PHONE_PATTERN = r"(?:\+91[\s\-]?)?[6-9]\d{4}[\s\-]?\d{5}|(?:\+?1[\s\-]?)?\(?\d{3}\)?[\s\-]?\d{3}[\s\-]?\d{4}"
URL_PATTERN = r"https?://[^\s<>\"]+|www\.[^\s<>\"]{2,}"

# PII Patterns
AADHAAR_PATTERN = r"\b[2-9]\d{3}\s?\d{4}\s?\d{4}\b"
PAN_PATTERN = r"\b[A-Z]{5}[0-9]{4}[A-Z]\b"
CREDIT_CARD_PATTERN = r"\b(?:\d{4}[\s\-]?){3}\d{4}\b"
SSN_PATTERN = r"\b\d{3}[\-]\d{2}[\-]\d{4}\b"
PASSPORT_PATTERN = r"\b[A-Z][1-9][0-9]{7}\b"

# Invoice / Receipt fields
INVOICE_FIELDS = {
    "invoice_number": r"(?i)\b(?:invoice|inv|bill|order)[\s#:\.-]*([A-Z0-9\-_/]{3,30})",
    "po_number": r"(?i)\b(?:p\.?o\.?\s*(?:#|num(?:ber)?)?|purchase\s+order(?:\s*#)?)[\s#:\.-]*([A-Z0-9\-_/]{3,30})",
    "gstin": r"(?i)\b(?:GSTIN|GST\s*No\.?)[\s:\.]*([0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]Z[0-9A-Z])",
    "due_date": r"(?i)\b(?:due\s+date|payment\s+due|pay\s+by)[\s:]*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})",
    "total": r"(?i)\b(?:grand\s+total|total\s+due|total\s+amount|amount\s+due|net\s+payable|total)[\s:β‚Ή$]*([0-9,]+(?:\.[0-9]{2})?)",
    "subtotal": r"(?i)\b(?:subtotal|sub-total|taxable\s+amount)[\s:β‚Ή$]*([0-9,]+(?:\.[0-9]{2})?)",
    "tax": r"(?i)\b(?:tax(?:\s*\(\d+%\))?|gst|vat|igst|cgst|sgst)[\s:β‚Ή$%]*([0-9,]+(?:\.[0-9]{2})?)",
    "discount": r"(?i)\b(?:discount|special\s+offer)[\s:β‚Ή$%-]*([0-9,]+(?:\.[0-9]{2})?)",
}

DOCUMENT_TYPE_KEYWORDS = {
    "Invoice / Receipt": ["invoice", "receipt", "bill", "amount due", "gst", "total", "subtotal", "payment"],
    "Identity Document": ["aadhaar", "pan", "passport", "driving licence", "voter", "date of birth", "dob", "nationality"],
    "Medical / Prescription": ["patient", "diagnosis", "prescription", "mg", "tablet", "dosage", "doctor", "hospital", "lab"],
    "Legal / Contract": ["whereas", "party", "agreement", "clause", "indemnify", "jurisdiction", "hereby", "execute"],
    "Academic": ["certificate", "degree", "university", "marks", "grade", "cgpa", "semester", "roll no"],
    "Business Card": ["ceo", "manager", "director", "email", "linkedin", "website", "@", "pvt ltd", "inc"],
}


def extract_dates(text: str) -> List[str]:
    dates = []
    for pattern in DATE_PATTERNS:
        dates.extend(re.findall(pattern, text, re.IGNORECASE))
    return list(set(dates))


def extract_amounts(text: str) -> List[str]:
    amounts = []
    for pattern in MONEY_PATTERNS:
        amounts.extend(re.findall(pattern, text))
    return list(set(amounts))


def extract_emails(text: str) -> List[str]:
    return list(set(re.findall(EMAIL_PATTERN, text)))


def extract_phones(text: str) -> List[str]:
    return list(set(re.findall(PHONE_PATTERN, text)))


def extract_urls(text: str) -> List[str]:
    return list(set(re.findall(URL_PATTERN, text)))


def detect_pii(text: str) -> Dict[str, List[str]]:
    """Detect all PII in text and return by category."""
    return {
        "aadhaar": re.findall(AADHAAR_PATTERN, text),
        "pan": re.findall(PAN_PATTERN, text),
        "credit_card": re.findall(CREDIT_CARD_PATTERN, text),
        "ssn": re.findall(SSN_PATTERN, text),
        "passport": re.findall(PASSPORT_PATTERN, text),
        "email": extract_emails(text),
        "phone": extract_phones(text),
    }


def redact_pii(text: str, redact_char: str = "β–ˆ") -> tuple[str, int]:
    """
    Replace detected PII in text with redaction blocks.
    Returns (redacted_text, count_redacted).
    """
    redacted = text
    count = 0

    patterns = [
        AADHAAR_PATTERN,
        PAN_PATTERN,
        CREDIT_CARD_PATTERN,
        SSN_PATTERN,
        PASSPORT_PATTERN,
    ]
    for pattern in patterns:
        matches = re.findall(pattern, redacted)
        for match in matches:
            replacement = redact_char * len(match)
            redacted = redacted.replace(match, replacement)
            count += 1

    return redacted, count


def extract_invoice_fields(text: str) -> Dict[str, str]:
    """Extract structured key-value invoice fields."""
    fields = {}
    stopwords = {"se", "no", "is", "of", "to", "in", "on", "at", "by", "or", "and", "the", "for"}
    for field_name, pattern in INVOICE_FIELDS.items():
        match = re.search(pattern, text)
        if match:
            val = match.group(1).strip() if match.lastindex else match.group(0).strip()
            # Eliminate trivial noise and punctuation-only values
            clean_val = val.strip(" :#-.")
            if len(clean_val) >= 2 and clean_val.lower() not in stopwords:
                fields[field_name] = clean_val
    return fields


def classify_document(text: str) -> str:
    """Classify the document type based on keyword analysis."""
    text_lower = text.lower()
    scores = {}
    for doc_type, keywords in DOCUMENT_TYPE_KEYWORDS.items():
        score = sum(1 for kw in keywords if kw in text_lower)
        if score > 0:
            scores[doc_type] = score

    if not scores:
        return "General Document"
    return max(scores, key=scores.get)


def extract_all(text: str) -> Dict[str, Any]:
    """Run all extractors and return a consolidated entities dict."""
    return {
        "document_type": classify_document(text),
        "dates": extract_dates(text),
        "amounts": extract_amounts(text),
        "emails": extract_emails(text),
        "phones": extract_phones(text),
        "urls": extract_urls(text),
        "pii": detect_pii(text),
        "invoice_fields": extract_invoice_fields(text),
    }