nvalliant commited on
Commit
d781bf2
·
verified ·
1 Parent(s): d45a9d9

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +77 -63
main.py CHANGED
@@ -9,19 +9,14 @@ from gliner import GLiNER
9
 
10
  app = Flask(__name__)
11
 
12
- model = GLiNER.from_pretrained("urchade/gliner_large-v2.1")
13
-
14
- entityLabels = [
15
- "job title",
16
- "company name",
17
- "university degree or major",
18
- "certification or license",
19
- "years or months of experience",
20
- "programming language",
21
- "software framework or library",
22
- "technical skill",
23
- "email address or phone number",
24
- "soft skill or personal trait",
25
  ]
26
 
27
  BLOCKLIST = {
@@ -39,28 +34,17 @@ def extractPDF(pdf_bytes: str) -> str:
39
  return text
40
 
41
  def normalize(text: str) -> str:
42
- text = uni.normalize("NFKC", text)
43
-
44
- #normalize punct
45
  text = text.replace('\r\n', '\n').replace('\r', '\n')
46
-
47
- text = text.replace('\xa0', ' ')
48
-
49
- text = text.replace("\u200b", "")
50
-
51
- text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]', '', text)
52
-
53
- text = re.sub(r"[\u200b\u200c\u200d\ufeff\u00ad]", "", text)
54
 
55
  return text
56
 
57
 
58
  def cleanText(text: str) -> str:
59
- #remove page numbers
60
  text = re.sub(r'\bPage\s+\d+\s*(of\s*\d+)?\b', '', text, flags=re.IGNORECASE)
61
  text = re.sub(r'^\s*\d+\s*$', '', text, flags=re.MULTILINE)
62
-
63
- #remove white spaces
64
  text = re.sub(r'\n{3,}', '\n\n', text)
65
  text = re.sub(r'[ \t]+', ' ', text)
66
  return text.strip()
@@ -83,47 +67,76 @@ def buildJSON(entities: list[dict], resumeFile: str) -> dict:
83
  "entities": dict(grouped)
84
  }
85
 
86
- def chunkedPredict(model, text: str, labels: list, chunk_size=380, overlap=50, threshold=0.1):
87
- words = text.split()
88
- all_entities = []
89
  seen = set()
90
-
91
  i = 0
92
- char_search_start = 0
93
- while i < len(words):
94
- chunk_words = words[i:i + chunk_size]
95
-
96
- # Find real start position of first word
97
- chunk_start = text.find(chunk_words[0], char_search_start)
98
-
99
- # Find end position by locating each word sequentially
100
- pos = chunk_start
101
- for word in chunk_words:
102
- found = text.find(word, pos)
103
- if found == -1:
104
- break
105
- pos = found + len(word)
106
- chunk_end = pos
107
-
108
- # Slice directly from original text, preserving all whitespace
109
  chunk_text = text[chunk_start:chunk_end]
110
-
111
- entities = model.predict_entities(chunk_text, labels, threshold=threshold, flat_ner=False, multi_label=True, max_len=384)
112
-
 
 
113
  for ent in entities:
 
114
  abs_start = ent["start"] + chunk_start
115
  abs_end = ent["end"] + chunk_start
116
- key = (ent["label"], abs_start, abs_end)
 
117
  if key not in seen:
118
  seen.add(key)
119
- ent["start"] = abs_start
120
- ent["end"] = abs_end
121
- all_entities.append(ent)
122
-
123
- char_search_start = chunk_start
124
- i += chunk_size - overlap
125
-
126
- return all_entities
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
  @app.route('/')
129
  def index():
@@ -143,9 +156,10 @@ def analyze():
143
  raw_text = extractPDF(pdf_bytes)
144
  normalized = normalize(raw_text)
145
  cleaned = cleanText(normalized)
146
-
147
- entities = chunkedPredict(model, cleaned, entityLabels)
148
- output = buildJSON(entities, file.filename)
 
149
  output["text"] = cleaned
150
 
151
  return jsonify(output)
 
9
 
10
  app = Flask(__name__)
11
 
12
+ model_large = GLiNER.from_pretrained("knowledgator/gliner-multitask-large-v0.5")
13
+ model_nuner = GLiNER.from_pretrained("numind/NuNER_Zero")
14
+
15
+ BASE_LABELS = [
16
+ "job title", "company name", "university degree or major",
17
+ "technical skill", "programming language", "software framework or library",
18
+ "certification or license", "years or months of experience", "contact",
19
+ "soft skill or personal trait"
 
 
 
 
 
20
  ]
21
 
22
  BLOCKLIST = {
 
34
  return text
35
 
36
  def normalize(text: str) -> str:
37
+ text = uni.normalize("NFKD", text)
 
 
38
  text = text.replace('\r\n', '\n').replace('\r', '\n')
39
+ text = text.encode("ascii", "ignore").decode("ascii")
40
+ text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', text)
 
 
 
 
 
 
41
 
42
  return text
43
 
44
 
45
  def cleanText(text: str) -> str:
 
46
  text = re.sub(r'\bPage\s+\d+\s*(of\s*\d+)?\b', '', text, flags=re.IGNORECASE)
47
  text = re.sub(r'^\s*\d+\s*$', '', text, flags=re.MULTILINE)
 
 
48
  text = re.sub(r'\n{3,}', '\n\n', text)
49
  text = re.sub(r'[ \t]+', ' ', text)
50
  return text.strip()
 
67
  "entities": dict(grouped)
68
  }
69
 
70
+ def run_gliner_chunked(model, text: str, is_nuner=False, chunk_size=300, overlap=75, threshold=0.6):
71
+ word_matches = list(re.finditer(r'\S+', text))
72
+ all_preds = []
73
  seen = set()
74
+
75
  i = 0
76
+ while i < len(word_matches):
77
+ chunk_matches = word_matches[i : i + chunk_size]
78
+ if not chunk_matches: break
79
+
80
+ chunk_start = chunk_matches[0].start()
81
+ chunk_end = chunk_matches[-1].end()
 
 
 
 
 
 
 
 
 
 
 
82
  chunk_text = text[chunk_start:chunk_end]
83
+
84
+ entities = model.predict_entities(
85
+ chunk_text, BASE_LABELS, threshold=threshold, flat_ner=False, multi_label=True, max_len=384
86
+ )
87
+
88
  for ent in entities:
89
+ base_label = ent["label"]
90
  abs_start = ent["start"] + chunk_start
91
  abs_end = ent["end"] + chunk_start
92
+
93
+ key = (base_label, abs_start, abs_end)
94
  if key not in seen:
95
  seen.add(key)
96
+ all_preds.append({
97
+ "label": base_label,
98
+ "start": abs_start,
99
+ "end": abs_end,
100
+ "score": ent["score"]
101
+ })
102
+
103
+ i += (chunk_size - overlap)
104
+
105
+ if is_nuner and all_preds:
106
+ all_preds = sorted(all_preds, key=lambda x: x['start'])
107
+ merged, current = [], all_preds[0].copy()
108
+ for next_ent in all_preds[1:]:
109
+ if next_ent['label'] == current['label'] and (next_ent['start'] <= current['end'] + 1):
110
+ current['end'] = max(current['end'], next_ent['end'])
111
+ current['score'] = max(current['score'], next_ent['score'])
112
+ else:
113
+ merged.append(current)
114
+ current = next_ent.copy()
115
+ merged.append(current)
116
+ return merged
117
+
118
+ return all_preds
119
+
120
+ def ensemble_predictions(preds_a, preds_b, full_text):
121
+ all_preds = sorted(preds_a + preds_b, key=lambda x: x['start'])
122
+ if not all_preds: return []
123
+
124
+ combined = []
125
+ current = all_preds[0].copy()
126
+
127
+ for next_ent in all_preds[1:]:
128
+ if next_ent['label'] == current['label'] and max(current['start'], next_ent['start']) <= min(current['end'], next_ent['end']):
129
+ current['start'] = min(current['start'], next_ent['start'])
130
+ current['end'] = max(current['end'], next_ent['end'])
131
+ current['score'] = max(current['score'], next_ent['score']) # Keep highest confidence
132
+ else:
133
+ current['text'] = full_text[current['start']:current['end']]
134
+ combined.append(current)
135
+ current = next_ent.copy()
136
+
137
+ current['text'] = full_text[current['start']:current['end']]
138
+ combined.append(current)
139
+ return combined
140
 
141
  @app.route('/')
142
  def index():
 
156
  raw_text = extractPDF(pdf_bytes)
157
  normalized = normalize(raw_text)
158
  cleaned = cleanText(normalized)
159
+ preds_large = run_gliner_chunked(model_large, cleaned, is_nuner=False)
160
+ preds_nuner = run_gliner_chunked(model_nuner, cleaned, is_nuner=True)
161
+ final_entities = ensemble_predictions(preds_large, preds_nuner, cleaned)
162
+ output = buildJSON(final_entities, file.filename)
163
  output["text"] = cleaned
164
 
165
  return jsonify(output)