mirajbhandari commited on
Commit
abf0891
·
verified ·
1 Parent(s): 688264a

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +546 -0
app.py ADDED
@@ -0,0 +1,546 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ast
2
+ import json
3
+ import re
4
+
5
+ import gradio as gr
6
+ import torch
7
+ from peft import PeftModel
8
+ from transformers import AutoModelForCausalLM, AutoTokenizer
9
+
10
+
11
+ # ---------------------------------------------------------
12
+ # Model configuration
13
+ # ---------------------------------------------------------
14
+
15
+ BASE_MODEL = "Qwen/Qwen2.5-0.5B-Instruct"
16
+ ADAPTER = "mirajbhandari/Entity_Extcation_Quen"
17
+
18
+ SYSTEM_PROMPT = (
19
+ "You are an NER model. Extract named entities from the sentence and "
20
+ 'return ONLY a JSON list of objects with keys "text" and "type". '
21
+ "Allowed types: PERSON, ORGANIZATION, LOCATION, DATE, EVENT, PRODUCT, "
22
+ "MONEY, TIME, WORK_OF_ART, LANGUAGE, NORP, FAC, GPE."
23
+ )
24
+
25
+
26
+ # ---------------------------------------------------------
27
+ # Load tokenizer and model
28
+ # ---------------------------------------------------------
29
+
30
+ print("Loading tokenizer...")
31
+
32
+ try:
33
+ tokenizer = AutoTokenizer.from_pretrained(ADAPTER)
34
+ except Exception:
35
+ tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
36
+
37
+ if tokenizer.pad_token_id is None:
38
+ tokenizer.pad_token_id = tokenizer.eos_token_id
39
+
40
+
41
+ print("Loading base model...")
42
+
43
+ dtype = torch.float16 if torch.cuda.is_available() else torch.float32
44
+
45
+ base_model = AutoModelForCausalLM.from_pretrained(
46
+ BASE_MODEL,
47
+ torch_dtype=dtype,
48
+ device_map="auto",
49
+ )
50
+
51
+
52
+ print("Loading LoRA adapter...")
53
+
54
+ model = PeftModel.from_pretrained(
55
+ base_model,
56
+ ADAPTER,
57
+ )
58
+
59
+
60
+ print("Merging LoRA adapter...")
61
+
62
+ model = model.merge_and_unload()
63
+ model.eval()
64
+
65
+ print("Model is ready!")
66
+
67
+
68
+ # ---------------------------------------------------------
69
+ # Entity colors
70
+ # ---------------------------------------------------------
71
+
72
+ ENTITY_COLORS = {
73
+ "PERSON": "#FECACA",
74
+ "ORGANIZATION": "#BFDBFE",
75
+ "LOCATION": "#BBF7D0",
76
+ "GPE": "#A7F3D0",
77
+ "DATE": "#FDE68A",
78
+ "TIME": "#FED7AA",
79
+ "EVENT": "#DDD6FE",
80
+ "PRODUCT": "#FBCFE8",
81
+ "MONEY": "#C7D2FE",
82
+ "WORK_OF_ART": "#E9D5FF",
83
+ "LANGUAGE": "#BAE6FD",
84
+ "NORP": "#F5D0FE",
85
+ "FAC": "#D9F99D",
86
+ }
87
+
88
+
89
+ # ---------------------------------------------------------
90
+ # Prompt construction
91
+ # ---------------------------------------------------------
92
+
93
+ def build_messages(sentence):
94
+ return [
95
+ {
96
+ "role": "system",
97
+ "content": SYSTEM_PROMPT,
98
+ },
99
+ {
100
+ "role": "user",
101
+ "content": sentence,
102
+ },
103
+ ]
104
+
105
+
106
+ # ---------------------------------------------------------
107
+ # JSON parsing
108
+ # ---------------------------------------------------------
109
+
110
+ def parse_entities(model_output):
111
+ """
112
+ Extract and parse the JSON list returned by the model.
113
+ """
114
+
115
+ output = model_output.strip()
116
+
117
+ # Remove Markdown code fences if the model adds them.
118
+ output = re.sub(
119
+ r"^```(?:json)?\s*",
120
+ "",
121
+ output,
122
+ flags=re.IGNORECASE,
123
+ )
124
+
125
+ output = re.sub(
126
+ r"\s*```$",
127
+ "",
128
+ output,
129
+ )
130
+
131
+ # Locate the JSON list.
132
+ start = output.find("[")
133
+ end = output.rfind("]")
134
+
135
+ if start == -1 or end == -1 or end < start:
136
+ raise ValueError(
137
+ "The model did not return a valid JSON list."
138
+ )
139
+
140
+ json_text = output[start:end + 1]
141
+
142
+ try:
143
+ entities = json.loads(json_text)
144
+ except json.JSONDecodeError:
145
+ # Handles occasional Python-style output with single quotes.
146
+ entities = ast.literal_eval(json_text)
147
+
148
+ if not isinstance(entities, list):
149
+ raise ValueError("NER result must be a list.")
150
+
151
+ cleaned_entities = []
152
+
153
+ for entity in entities:
154
+ if not isinstance(entity, dict):
155
+ continue
156
+
157
+ text = str(entity.get("text", "")).strip()
158
+ entity_type = str(entity.get("type", "")).strip().upper()
159
+
160
+ if text and entity_type:
161
+ cleaned_entities.append(
162
+ {
163
+ "text": text,
164
+ "type": entity_type,
165
+ }
166
+ )
167
+
168
+ return cleaned_entities
169
+
170
+
171
+ # ---------------------------------------------------------
172
+ # Find entity positions
173
+ # ---------------------------------------------------------
174
+
175
+ def find_entity_spans(sentence, entities):
176
+ """
177
+ Find the start and end positions of entities in the original sentence.
178
+
179
+ This also supports repeated entities.
180
+ """
181
+
182
+ spans = []
183
+ occupied_positions = []
184
+
185
+ for entity in entities:
186
+ entity_text = entity["text"]
187
+ entity_type = entity["type"]
188
+
189
+ # First try exact matching.
190
+ matches = list(
191
+ re.finditer(
192
+ re.escape(entity_text),
193
+ sentence,
194
+ )
195
+ )
196
+
197
+ # If exact matching fails, try case-insensitive matching.
198
+ if not matches:
199
+ matches = list(
200
+ re.finditer(
201
+ re.escape(entity_text),
202
+ sentence,
203
+ flags=re.IGNORECASE,
204
+ )
205
+ )
206
+
207
+ for match in matches:
208
+ start = match.start()
209
+ end = match.end()
210
+
211
+ overlaps = any(
212
+ start < existing_end and end > existing_start
213
+ for existing_start, existing_end in occupied_positions
214
+ )
215
+
216
+ if overlaps:
217
+ continue
218
+
219
+ spans.append(
220
+ {
221
+ "start": start,
222
+ "end": end,
223
+ "text": sentence[start:end],
224
+ "type": entity_type,
225
+ }
226
+ )
227
+
228
+ occupied_positions.append((start, end))
229
+
230
+ # Use one occurrence for each returned entity.
231
+ break
232
+
233
+ spans.sort(key=lambda item: item["start"])
234
+
235
+ return spans
236
+
237
+
238
+ # ---------------------------------------------------------
239
+ # Convert spans for Gradio HighlightedText
240
+ # ---------------------------------------------------------
241
+
242
+ def create_highlighted_output(sentence, spans):
243
+ """
244
+ Convert entity spans into the format expected by gr.HighlightedText.
245
+
246
+ Example:
247
+ [
248
+ ("Barack Obama", "PERSON"),
249
+ (" visited ", None),
250
+ ("Paris", "LOCATION")
251
+ ]
252
+ """
253
+
254
+ if not sentence:
255
+ return []
256
+
257
+ if not spans:
258
+ return [(sentence, None)]
259
+
260
+ highlighted_parts = []
261
+ current_position = 0
262
+
263
+ for span in spans:
264
+ start = span["start"]
265
+ end = span["end"]
266
+
267
+ if start > current_position:
268
+ highlighted_parts.append(
269
+ (sentence[current_position:start], None)
270
+ )
271
+
272
+ highlighted_parts.append(
273
+ (sentence[start:end], span["type"])
274
+ )
275
+
276
+ current_position = end
277
+
278
+ if current_position < len(sentence):
279
+ highlighted_parts.append(
280
+ (sentence[current_position:], None)
281
+ )
282
+
283
+ return highlighted_parts
284
+
285
+
286
+ # ---------------------------------------------------------
287
+ # Model inference
288
+ # ---------------------------------------------------------
289
+
290
+ @torch.inference_mode()
291
+ def extract_entities(sentence):
292
+ sentence = sentence.strip()
293
+
294
+ if not sentence:
295
+ raise gr.Error("Please enter a sentence.")
296
+
297
+ messages = build_messages(sentence)
298
+
299
+ prompt = tokenizer.apply_chat_template(
300
+ messages,
301
+ tokenize=False,
302
+ add_generation_prompt=True,
303
+ )
304
+
305
+ model_inputs = tokenizer(
306
+ prompt,
307
+ return_tensors="pt",
308
+ )
309
+
310
+ model_device = next(model.parameters()).device
311
+
312
+ model_inputs = {
313
+ key: value.to(model_device)
314
+ for key, value in model_inputs.items()
315
+ }
316
+
317
+ generated_ids = model.generate(
318
+ **model_inputs,
319
+ max_new_tokens=256,
320
+ do_sample=False,
321
+ repetition_penalty=1.05,
322
+ pad_token_id=tokenizer.pad_token_id,
323
+ eos_token_id=tokenizer.eos_token_id,
324
+ )
325
+
326
+ # Remove the original prompt tokens.
327
+ generated_tokens = generated_ids[
328
+ :,
329
+ model_inputs["input_ids"].shape[1]:
330
+ ]
331
+
332
+ model_output = tokenizer.batch_decode(
333
+ generated_tokens,
334
+ skip_special_tokens=True,
335
+ )[0].strip()
336
+
337
+ try:
338
+ entities = parse_entities(model_output)
339
+ except Exception as error:
340
+ return (
341
+ [(sentence, None)],
342
+ [],
343
+ {
344
+ "error": str(error),
345
+ "raw_model_output": model_output,
346
+ },
347
+ )
348
+
349
+ spans = find_entity_spans(sentence, entities)
350
+ highlighted_output = create_highlighted_output(sentence, spans)
351
+
352
+ entity_table = [
353
+ [
354
+ span["text"],
355
+ span["type"],
356
+ span["start"],
357
+ span["end"],
358
+ ]
359
+ for span in spans
360
+ ]
361
+
362
+ json_output = {
363
+ "sentence": sentence,
364
+ "entities": [
365
+ {
366
+ "text": span["text"],
367
+ "type": span["type"],
368
+ "start": span["start"],
369
+ "end": span["end"],
370
+ }
371
+ for span in spans
372
+ ],
373
+ }
374
+
375
+ return highlighted_output, entity_table, json_output
376
+
377
+
378
+ # ---------------------------------------------------------
379
+ # Clear interface
380
+ # ---------------------------------------------------------
381
+
382
+ def clear_outputs():
383
+ return "", [], [], None
384
+
385
+
386
+ # ---------------------------------------------------------
387
+ # Gradio user interface
388
+ # ---------------------------------------------------------
389
+
390
+ CUSTOM_CSS = """
391
+ .gradio-container {
392
+ max-width: 1100px !important;
393
+ margin: auto !important;
394
+ }
395
+
396
+ #main-title {
397
+ text-align: center;
398
+ margin-bottom: 4px;
399
+ }
400
+
401
+ #subtitle {
402
+ text-align: center;
403
+ color: #64748b;
404
+ margin-bottom: 24px;
405
+ }
406
+
407
+ #input-card,
408
+ #result-card {
409
+ border-radius: 14px;
410
+ }
411
+ """
412
+
413
+
414
+ with gr.Blocks(
415
+ title="Named Entity Recognition",
416
+ css=CUSTOM_CSS,
417
+ theme=gr.themes.Soft(),
418
+ ) as demo:
419
+
420
+ gr.Markdown(
421
+ """
422
+ # Named Entity Recognition
423
+ """,
424
+ elem_id="main-title",
425
+ )
426
+
427
+ gr.Markdown(
428
+ """
429
+ Enter a sentence to identify and highlight named entities.
430
+ """,
431
+ elem_id="subtitle",
432
+ )
433
+
434
+ with gr.Group(elem_id="input-card"):
435
+ sentence_input = gr.Textbox(
436
+ label="Original sentence",
437
+ placeholder=(
438
+ "Example: Sundar Pichai visited Google headquarters "
439
+ "in California on July 15, 2026."
440
+ ),
441
+ lines=4,
442
+ )
443
+
444
+ with gr.Row():
445
+ extract_button = gr.Button(
446
+ "Extract Entities",
447
+ variant="primary",
448
+ )
449
+
450
+ clear_button = gr.Button(
451
+ "Clear",
452
+ variant="secondary",
453
+ )
454
+
455
+ with gr.Group(elem_id="result-card"):
456
+ highlighted_output = gr.HighlightedText(
457
+ label="Highlighted sentence",
458
+ color_map=ENTITY_COLORS,
459
+ show_legend=True,
460
+ show_inline_category=True,
461
+ combine_adjacent=True,
462
+ )
463
+
464
+ entity_table = gr.Dataframe(
465
+ headers=[
466
+ "Entity",
467
+ "Entity Type",
468
+ "Start Position",
469
+ "End Position",
470
+ ],
471
+ datatype=[
472
+ "str",
473
+ "str",
474
+ "number",
475
+ "number",
476
+ ],
477
+ label="Detected entities",
478
+ interactive=False,
479
+ wrap=True,
480
+ )
481
+
482
+ with gr.Accordion(
483
+ "JSON output",
484
+ open=False,
485
+ ):
486
+ json_output = gr.JSON(
487
+ label="Structured NER result"
488
+ )
489
+
490
+ gr.Examples(
491
+ examples=[
492
+ [
493
+ "Sundar Pichai is the CEO of Google and lives in California."
494
+ ],
495
+ [
496
+ "Apple launched the iPhone in September 2025."
497
+ ],
498
+ [
499
+ "Barack Obama visited Paris on January 10, 2024."
500
+ ],
501
+ [
502
+ "Microsoft invested 10 billion dollars in OpenAI."
503
+ ],
504
+ [
505
+ "The FIFA World Cup was held in Qatar in 2022."
506
+ ],
507
+ ],
508
+ inputs=sentence_input,
509
+ )
510
+
511
+ extract_button.click(
512
+ fn=extract_entities,
513
+ inputs=sentence_input,
514
+ outputs=[
515
+ highlighted_output,
516
+ entity_table,
517
+ json_output,
518
+ ],
519
+ api_name="extract_entities",
520
+ )
521
+
522
+ sentence_input.submit(
523
+ fn=extract_entities,
524
+ inputs=sentence_input,
525
+ outputs=[
526
+ highlighted_output,
527
+ entity_table,
528
+ json_output,
529
+ ],
530
+ )
531
+
532
+ clear_button.click(
533
+ fn=clear_outputs,
534
+ inputs=[],
535
+ outputs=[
536
+ sentence_input,
537
+ highlighted_output,
538
+ entity_table,
539
+ json_output,
540
+ ],
541
+ queue=False,
542
+ )
543
+
544
+
545
+ if __name__ == "__main__":
546
+ demo.queue().launch()