File size: 9,453 Bytes
0e39d80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
"""Experience letter field extraction and validation โ€” pure OCR-based."""

from __future__ import annotations

import re
from config import FREE_EMAIL_DOMAINS
from ml_utils.ocr import OcrResult, get_full_text, get_average_confidence
from ml_utils.extract import find_value_near_label, get_text_in_region

EMAIL_RE = re.compile(r"[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}")
PHONE_RE = re.compile(r"(?:\+91[\s\-]?)?[6-9]\d{9}")
DATE_RE = re.compile(
    r"\d{1,2}[/\-]\d{1,2}[/\-]\d{2,4}|"
    r"\d{1,2}\s+(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+\d{4}",
    re.I,
)
COMPANY_RE = re.compile(
    r"([A-Z][A-Za-z0-9\s&.,'\-]+(?:Pvt\.?\s*Ltd\.?|Limited|LLP|Inc\.?|Corporation|Technologies|Solutions|Services|Consulting|Enterprises|Group|Company))",
    re.I,
)
EXP_KEYWORDS = ["experience", "certify", "employed", "worked", "tenure", "designation",
                 "employment", "relieving", "service", "joining"]
PREFERRED_EMAIL_PREFIXES = ("hr@", "careers@", "jobs@", "recruitment@", "talent@", "admin@")

NAME_LABELS = ["name", "employee", "mr.", "ms.", "mrs."]
DESIGNATION_LABELS = ["designation", "position", "role", "title", "เคชเคฆเคจเคพเคฎ"]
DEPARTMENT_LABELS = ["department", "dept", "division", "เคตเคฟเคญเคพเค—"]
JOINING_LABELS = ["joining", "join date", "date of joining", "from", "start"]
EXIT_LABELS = ["relieving", "exit", "last working", "to", "end date", "separation"]
SALARY_LABELS = ["salary", "ctc", "compensation", "remuneration", "package"]


def _rank_email(emails: list[str], company_name: str | None) -> str | None:
    """Pick the most relevant email (HR > corporate > personal)."""
    if not emails:
        return None

    def score(email: str) -> int:
        lower = email.lower()
        s = 0
        if any(lower.startswith(p) for p in PREFERRED_EMAIL_PREFIXES):
            s += 10
        if company_name:
            company_token = company_name.split()[0].lower()
            if company_token and company_token in lower.split("@")[-1]:
                s += 8
        if lower.split("@")[-1] in FREE_EMAIL_DOMAINS:
            s -= 5
        return s

    return max(emails, key=score)


# โ”€โ”€ Parser (from OCR results) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def parse_experience_fields_from_ocr(ocr_results: list[OcrResult]) -> dict:
    """Extract experience letter fields from OCR results."""
    full_text = get_full_text(ocr_results)
    return _parse_from_text(full_text, ocr_results)


# โ”€โ”€ Parser (from plain text โ€” legacy compat) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def parse_experience_fields(text: str) -> dict:
    """Extract experience letter fields from plain text."""
    return _parse_from_text(text, [])


def _parse_from_text(text: str, ocr_results: list[OcrResult]) -> dict:
    fields: dict = {}

    # Company Name
    company_match = COMPANY_RE.search(text)
    company_name = company_match.group(1).strip() if company_match else None
    if not company_name:
        for line in text.splitlines()[:12]:
            if any(x in line for x in ["Ltd", "Limited", "Pvt", "LLP", "Inc", "Technologies", "Solutions"]):
                company_name = line.strip()[:120]
                break
    fields["company_name"] = company_name

    # Letterhead detection (top 15%)
    if ocr_results:
        top_text = get_text_in_region(ocr_results, y_start_pct=0.0, y_end_pct=0.15)
        fields["letterhead_detected"] = bool(company_name and company_name.lower() in top_text.lower())
    else:
        header_lines = "\n".join(text.splitlines()[:3]).lower()
        fields["letterhead_detected"] = bool(company_name and company_name.lower() in header_lines)

    # HR Email
    emails = EMAIL_RE.findall(text)
    fields["hr_email"] = _rank_email(emails, company_name)

    # Phone
    phones = PHONE_RE.findall(text)
    fields["phone"] = phones[0] if phones else None

    # Employee Name
    employee_name = None
    for pat in [
        r"(?:certify that|hereby certify that)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)",
        r"Dear\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)",
        r"(?:Mr\.|Ms\.|Mrs\.)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)",
        r"(?:employee|name)\s*[:\-]\s*([A-Za-z\s.]+)",
    ]:
        m = re.search(pat, text, re.I)
        if m:
            employee_name = m.group(1).strip()[:80]
            break
    if not employee_name and ocr_results:
        name_val = find_value_near_label(ocr_results, NAME_LABELS)
        if name_val:
            cleaned = re.sub(r"[^A-Za-z\s.]", "", name_val).strip()
            if len(cleaned) >= 3:
                employee_name = cleaned
    fields["employee_name"] = employee_name

    # Dates
    dates = DATE_RE.findall(text)
    fields["date_from"] = dates[0] if len(dates) > 0 else None
    fields["date_to"] = dates[1] if len(dates) > 1 else None

    # Also try spatial extraction for dates
    if ocr_results:
        if not fields["date_from"]:
            joining = find_value_near_label(ocr_results, JOINING_LABELS)
            if joining:
                dm = DATE_RE.search(joining)
                if dm:
                    fields["date_from"] = dm.group(0)
        if not fields["date_to"]:
            exit_date = find_value_near_label(ocr_results, EXIT_LABELS)
            if exit_date:
                dm = DATE_RE.search(exit_date)
                if dm:
                    fields["date_to"] = dm.group(0)

    # Designation
    designation = None
    des_match = re.search(r"(?:designation|position|role|title)\s*[:\-]?\s*([A-Za-z\s/]+)", text, re.I)
    if des_match:
        designation = des_match.group(1).strip()[:80]
    if not designation and ocr_results:
        des_val = find_value_near_label(ocr_results, DESIGNATION_LABELS)
        if des_val:
            designation = des_val[:80]
    fields["designation"] = designation

    # Department
    dept = None
    dept_match = re.search(r"(?:department|dept|division)\s*[:\-]?\s*([A-Za-z\s/]+)", text, re.I)
    if dept_match:
        dept = dept_match.group(1).strip()[:60]
    if not dept and ocr_results:
        dept_val = find_value_near_label(ocr_results, DEPARTMENT_LABELS)
        if dept_val:
            dept = dept_val[:60]
    fields["department"] = dept

    # Salary/CTC (optional)
    salary_match = re.search(r"(?:salary|ctc|compensation|remuneration)\s*[:\-]?\s*([\d,.\s]+(?:lpa|per annum|p\.a\.)?)", text, re.I)
    if salary_match:
        fields["salary"] = salary_match.group(1).strip()

    # Seal/Signature presence (bottom 25%)
    if ocr_results:
        bottom_text = get_text_in_region(ocr_results, y_start_pct=0.75, y_end_pct=1.0)
    else:
        lines = text.splitlines()
        bottom_text = "\n".join(lines[-(len(lines) // 4):]) if lines else ""

    fields["seal_present"] = bool(re.search(r"seal|stamp|เคฎเฅเคฆเฅเคฐเคพ", bottom_text, re.IGNORECASE))
    fields["signature_present"] = bool(re.search(r"signature|sign|authorized|เคนเคธเฅเคคเคพเค•เฅเคทเคฐ", bottom_text, re.IGNORECASE))

    return fields


# โ”€โ”€ Validator โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def is_free_email(email: str | None) -> bool:
    if not email:
        return False
    domain = email.split("@")[-1].lower()
    return domain in FREE_EMAIL_DOMAINS


def validate_experience(fields: dict, text: str, text_source: str, ocr_results: list[OcrResult]) -> tuple[float, list[str]]:
    """Validate experience letter. Returns (validation_score_0_to_20, flags)."""
    flags: list[str] = []
    checks_total = 0
    checks_passed = 0

    # Email presence
    checks_total += 1
    if fields.get("hr_email"):
        if is_free_email(fields["hr_email"]):
            flags.append("FREE_EMAIL")
            checks_passed += 0.5
        else:
            checks_passed += 1
    else:
        flags.append("NO_EMAIL")

    # Company name
    checks_total += 1
    if fields.get("company_name"):
        checks_passed += 1
    else:
        flags.append("NO_COMPANY")

    # Dates
    checks_total += 1
    if fields.get("date_from") or fields.get("date_to"):
        checks_passed += 1
    else:
        flags.append("MISSING_DATES")

    # Employee name
    checks_total += 1
    if fields.get("employee_name"):
        checks_passed += 1
    else:
        flags.append("MISSING_EMPLOYEE_NAME")

    # Experience keywords
    checks_total += 1
    lower = text.lower()
    if any(k in lower for k in EXP_KEYWORDS):
        checks_passed += 1
    else:
        flags.append("MISSING_EXPERIENCE_KEYWORDS")

    # Text source quality
    if text_source == "ocr":
        flags.append("SCANNED_DOCUMENT")
    elif text_source == "ocr_hindi":
        flags.append("HINDI_OCR_USED")

    # OCR confidence
    if ocr_results:
        avg_conf = get_average_confidence(ocr_results)
        if avg_conf < 0.4:
            flags.append("LOW_OCR_CONFIDENCE")

    validation_ratio = checks_passed / max(checks_total, 1)
    return validation_ratio * 20.0, flags


def score_experience(fields: dict, text: str, text_source: str) -> tuple[float, list[str]]:
    """Legacy compatibility โ€” returns (score_0_100, flags)."""
    validation_score, flags = validate_experience(fields, text, text_source, [])
    return validation_score * 5.0, flags  # Scale to 0-100