RangGaraga commited on
Commit
3ebeab3
·
verified ·
1 Parent(s): 688c1f0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +81 -322
app.py CHANGED
@@ -1,20 +1,13 @@
1
- import gradio as gr
2
- from transformers import pipeline
3
- import fitz
4
- import tempfile
5
  import os
6
  import re
7
  import time
8
- import base64
 
9
 
10
- # Load model
11
- import time as _t
12
- _t0 = _t.time()
13
- ner = pipeline("ner", model="cahya/NusaBert-ner-v1.3", aggregation_strategy="simple")
14
- print(f"[STARTUP] Model loaded in {_t.time()-_t0:.2f}s — running warmup...")
15
- ner("PT Bank Indonesia") # warmup: paksa model fully loaded ke memori
16
- print(f"[STARTUP] Warmup selesai. Total: {_t.time()-_t0:.2f}s")
17
 
 
 
18
 
19
  # Label mapping
20
  LABEL_MAP = {
@@ -26,19 +19,6 @@ LABEL_MAP = {
26
  "REG": "Agama", "TIM": "Waktu", "WOA": "Karya Seni", "LAN": "Bahasa",
27
  }
28
 
29
- RTF_COLOR_TABLE = [
30
- (255,191,191),(174,221,255),(141,199,255),(184,255,184),(153,242,204),
31
- (217,255,165),(255,229,140),(255,204,102),(204,255,218),(242,204,255),
32
- (224,191,255),(255,242,178),(199,242,242),(255,186,186),(255,217,165),
33
- (191,223,255),(230,218,255),(255,230,218),(204,255,242),
34
- ]
35
-
36
- LABEL_RTF_COLOR_IDX = {
37
- "PER":1,"ORG":2,"NOR":3,"LOC":4,"GPE":5,"FAC":6,"DAT":7,"TIM":8,
38
- "MON":9,"CRD":10,"ORD":11,"PRC":12,"QTY":13,"LAW":14,"EVT":15,
39
- "PRD":16,"REG":17,"WOA":18,"LAN":19,
40
- }
41
-
42
  LABEL_HEX = {
43
  "PER":"#FFBFBF","ORG":"#AEDDFF","NOR":"#8DC7FF","LOC":"#B8FFB8",
44
  "GPE":"#99F2CC","FAC":"#D9FFA5","DAT":"#FFE58C","TIM":"#FFCC66",
@@ -91,23 +71,7 @@ def get_label_id(raw_label: str) -> str:
91
  label_id = raw_label.replace("B-","").replace("I-","").replace("B_","").replace("I_","")
92
  return label_id.split("-")[-1].upper().strip()
93
 
94
- def escape_rtf(text: str) -> str:
95
- out = []
96
- for ch in text:
97
- if ch == '\\': out.append('\\\\')
98
- elif ch == '{': out.append('\\{')
99
- elif ch == '}': out.append('\\}')
100
- elif ord(ch) > 127: out.append(f'\\u{ord(ch)}?')
101
- else: out.append(ch)
102
- return ''.join(out)
103
-
104
- def build_rtf_color_table() -> str:
105
- entries = "\\red0\\green0\\blue0;"
106
- for r,g,b in RTF_COLOR_TABLE:
107
- entries += f"\\red{r}\\green{g}\\blue{b};"
108
- return "{\\colortbl ;" + entries + "}"
109
-
110
- def text_to_rtf_with_highlights(text: str, entity_map: dict) -> str:
111
  sorted_entities = sorted(entity_map.items(), key=lambda x: len(x[0]), reverse=True)
112
  spans = []
113
  used = [False] * len(text)
@@ -123,140 +87,55 @@ def text_to_rtf_with_highlights(text: str, entity_map: dict) -> str:
123
  for i in range(s, e):
124
  used[i] = True
125
  spans.sort(key=lambda x: x[0])
126
- rtf_parts = []
127
  cursor = 0
128
  for s, e, label_id in spans:
129
  if cursor < s:
130
- rtf_parts.append(escape_rtf(text[cursor:s]))
131
- color_idx = LABEL_RTF_COLOR_IDX.get(label_id, 1)
132
- highlighted = escape_rtf(text[s:e])
133
- rtf_parts.append(f"{{\\highlight{color_idx} {highlighted}}}")
 
 
 
 
134
  cursor = e
135
  if cursor < len(text):
136
- rtf_parts.append(escape_rtf(text[cursor:]))
137
- return "".join(rtf_parts)
138
-
139
- def wrap_rtf(content: str) -> str:
140
- color_table = build_rtf_color_table()
141
- content_rtf = content.replace("\r\n","\n").replace("\r","\n").replace("\n","\\par\n")
142
  return (
143
- "{\\rtf1\\ansi\\ansicpg1252\\deff0\n"
144
- "{\\fonttbl{\\f0\\froman\\fcharset0 Times New Roman;}}\n"
145
- f"{color_table}\n"
146
- "\\widowctrl\\wpaper12240\\wpaperh15840\n"
147
- "\\margl1800\\margr1800\\margt1440\\margb1440\n"
148
- "\\f0\\fs24\\sl360\\slmult1\n"
149
- f"{content_rtf}"
150
- "}"
151
- )
152
-
153
- def _build_debug_html(lines: list) -> str:
154
- rows = "".join(
155
- f'<div style="padding:6px 10px; border-bottom:1px solid rgba(255,255,255,0.07); '
156
- f'font-size:13px; color:#cbd5e1; font-family:monospace;">{line}</div>'
157
- for line in lines
158
  )
159
- return f"""
160
- <div style="margin-top:12px; background:rgba(15,23,42,0.85); border:1px solid rgba(99,102,241,0.35);
161
- border-radius:12px; overflow:hidden;">
162
- <div style="background:linear-gradient(135deg,#1e40af,#6d28d9); padding:8px 14px;
163
- font-size:11px; font-weight:700; letter-spacing:0.1em; color:#fff;">
164
- 🔍 DEBUG — WAKTU PER LANGKAH
165
- </div>
166
- {rows}
167
- </div>"""
168
-
169
  # NER Teks
170
- def run_ner(text: str):
171
- if not text or not text.strip():
172
- return "<p style='color:#94a3b8; font-style:italic; padding:16px;'>Masukkan teks terlebih dahulu.</p>"
173
- results = ner(text.strip())
174
- if not results:
175
- return "<p style='color:#94a3b8; font-style:italic; padding:16px;'>Tidak ada entitas yang ditemukan.</p>"
176
-
177
- rows_html = ""
178
- row_num = 1
179
- seen_words = set()
180
- for ent in results:
181
- raw_label = ent["entity_group"]
182
- label_id = get_label_id(raw_label)
183
- label_idn = LABEL_MAP.get(label_id, raw_label)
184
- word = clean_word(ent["word"])
185
- if not word: continue
186
- word_key = word.lower()
187
- if word_key in seen_words: continue
188
- seen_words.add(word_key)
189
- hex_color = LABEL_HEX.get(label_id, "#e2e8f0")
190
- score = f"{ent['score']:.2%}"
191
- row_bg = "#f8faff" if row_num % 2 == 0 else "#ffffff"
192
- rows_html += f"""
193
- <tr style="background:{row_bg};">
194
- <td style="padding:9px 14px;border-bottom:1px solid #e8edf5;text-align:center;color:#64748b;font-size:12px;">{row_num}</td>
195
- <td style="padding:9px 14px;border-bottom:1px solid #e8edf5;font-weight:600;color:#1e293b;">{word}</td>
196
- <td style="padding:9px 14px;border-bottom:1px solid #e8edf5;">
197
- <span style="background:{hex_color};padding:3px 10px;border-radius:20px;font-size:12px;font-weight:600;color:#1e293b;">{label_idn}</span>
198
- </td>
199
- <td style="padding:9px 14px;border-bottom:1px solid #e8edf5;font-size:12px;color:#64748b;text-align:center;">{score}</td>
200
- </tr>"""
201
- row_num += 1
202
-
203
- if not rows_html:
204
- return "<p style='color:#94a3b8; font-style:italic; padding:16px;'>Tidak ada entitas yang ditemukan.</p>"
205
-
206
- return f"""
207
- <div style="overflow-x:auto;margin-top:4px;border-radius:12px;border:1px solid #e2e8f0;box-shadow:0 2px 12px rgba(0,0,0,0.06);">
208
- <table style="width:100%;border-collapse:collapse;font-size:14px;font-family:'Segoe UI',sans-serif;">
209
- <thead>
210
- <tr style="background:linear-gradient(135deg,#1e40af,#6d28d9);">
211
- <th style="padding:12px 14px;color:#fff;width:55px;font-weight:600;font-size:12px;letter-spacing:0.05em;">NO</th>
212
- <th style="padding:12px 14px;color:#fff;text-align:left;font-weight:600;font-size:12px;letter-spacing:0.05em;">KATA / FRASA</th>
213
- <th style="padding:12px 14px;color:#fff;text-align:left;font-weight:600;font-size:12px;letter-spacing:0.05em;">ENTITAS</th>
214
- <th style="padding:12px 14px;color:#fff;width:90px;font-weight:600;font-size:12px;letter-spacing:0.05em;">SKOR</th>
215
- </tr>
216
- </thead>
217
- <tbody>{rows_html}</tbody>
218
- </table>
219
- </div>"""
220
-
221
-
222
- # NER PDF → RTF
223
- def run_ner_pdf(pdf_file):
224
- if pdf_file is None:
225
- return "", "", "<p style='color:#94a3b8;font-style:italic;padding:16px;'>Unggah file PDF terlebih dahulu.</p>", ""
226
-
227
- pdf_path = pdf_file if isinstance(pdf_file, str) else pdf_file.name
228
- doc = fitz.open(pdf_path)
229
- page_count = len(doc)
230
-
231
- if page_count > MAX_PDF_PAGES:
232
  doc.close()
233
- return "", "", f"""
234
- <div style="background:#fff1f2;border:1.5px solid #fda4af;border-radius:12px;
235
- padding:16px 20px;display:flex;align-items:center;gap:12px;">
236
- <span style="font-size:24px;">⚠️</span>
237
- <div>
238
- <p style="margin:0;font-weight:700;color:#be123c;font-size:15px;">PDF Terlalu Banyak Halaman</p>
239
- <p style="margin:4px 0 0;color:#9f1239;font-size:13px;">
240
- File Anda memiliki <strong>{page_count} halaman</strong>.
241
- Maksimal <strong>{MAX_PDF_PAGES} halaman</strong>.
242
- </p>
243
- </div>
244
- </div>""", ""
245
-
246
- debug_lines = []
247
- t_total = time.time()
248
-
249
- # LANGKAH 1: Ekstrak teks
250
- t0 = time.time()
251
- pages_text = [page.get_text() for page in doc]
252
- doc.close()
253
- full_text = "\n\n".join(pages_text)
254
- char_count = len(full_text)
255
- t1 = time.time()
256
- debug_lines.append(f"✅ <b>Langkah 1</b> — Ekstrak teks PDF: <b>{t1-t0:.3f}s</b> | {char_count:,} karakter | {page_count} halaman")
257
-
258
- # ── LANGKAH 2: Chunking ──
259
- t0 = time.time()
260
  chunks = []
261
  start = 0
262
  while start < len(full_text):
@@ -265,137 +144,42 @@ def run_ner_pdf(pdf_file):
265
  if end == len(full_text):
266
  break
267
  start = end - OVERLAP_CHARS
268
- t1 = time.time()
269
- debug_lines.append(f"✅ <b>Langkah 2</b> — Chunking teks: <b>{t1-t0:.4f}s</b> | {len(chunks)} chunk @ {MAX_CHUNK_CHARS} karakter")
270
 
271
- # ── LANGKAH 3: NER model (ini yang biasanya paling lama) ──
272
- t0 = time.time()
273
  all_ner_results = []
274
- chunk_times = []
275
- for i, chunk in enumerate(chunks):
276
- tc = time.time()
277
- chunk_results = ner(chunk.strip())
278
- chunk_times.append(time.time() - tc)
279
- all_ner_results.extend(chunk_results)
280
- t1 = time.time()
281
- avg_chunk = sum(chunk_times)/len(chunk_times) if chunk_times else 0
282
- debug_lines.append(
283
- f"✅ <b>Langkah 3</b> — NER model: <b>{t1-t0:.2f}s</b> total | "
284
- f"{len(chunks)} chunk | avg/chunk: {avg_chunk:.2f}s | {len(all_ner_results)} entitas raw"
285
- )
286
- # Detail per chunk
287
- for i, ct in enumerate(chunk_times):
288
- debug_lines.append(f"&nbsp;&nbsp;&nbsp;↳ chunk {i+1}/{len(chunks)}: {ct:.2f}s ({len(chunks[i])} karakter)")
289
 
290
  if not all_ner_results:
291
- dh = _build_debug_html(debug_lines)
292
- return None, "", "<p style='color:#94a3b8;padding:16px;'>Tidak ada entitas ditemukan.</p>", dh
293
 
294
- # ── LANGKAH 4: Bangun entity_map ──
295
- t0 = time.time()
296
  entity_map: dict[str, str] = {}
297
  for ent in all_ner_results:
298
  word = clean_word(ent["word"])
299
- if len(word) < 2: continue
 
300
  label_id = get_label_id(ent["entity_group"])
301
  w_lower = word.lower()
302
  if w_lower not in entity_map:
303
  entity_map[w_lower] = label_id
304
- t1 = time.time()
305
- debug_lines.append(f"✅ <b>Langkah 4</b> — Bangun entity_map: <b>{t1-t0:.4f}s</b> | {len(entity_map)} entitas unik")
306
 
307
  if not entity_map:
308
- dh = _build_debug_html(debug_lines)
309
- return None, "", "<p style='color:#94a3b8;padding:16px;'>Tidak ada entitas ditemukan.</p>", dh
310
-
311
- # ── LANGKAH 5: Highlight → RTF content ──
312
- t0 = time.time()
313
- rtf_content = text_to_rtf_with_highlights(full_text, entity_map)
314
- t1 = time.time()
315
- debug_lines.append(f"✅ <b>Langkah 5</b> — Highlight & build RTF content: <b>{t1-t0:.4f}s</b>")
316
-
317
- # ── LANGKAH 6: Build Download Link ──
318
- t0 = time.time()
319
 
320
- rtf_full = wrap_rtf(rtf_content)
321
 
322
- rtf_bytes = rtf_full.encode(
323
- "ascii",
324
- errors="replace"
 
 
 
 
325
  )
 
326
 
327
- b64 = base64.b64encode(
328
- rtf_bytes
329
- ).decode()
330
-
331
- download_html = f"""
332
- <div style="padding:12px 0;">
333
- <a
334
- href="data:application/rtf;base64,{b64}"
335
- download="hasil_ner.rtf"
336
- style="
337
- display:inline-block;
338
- background:linear-gradient(135deg,#1e40af,#6d28d9);
339
- color:white;
340
- text-decoration:none;
341
- padding:12px 20px;
342
- border-radius:10px;
343
- font-weight:600;
344
- font-family:'DM Sans',sans-serif;
345
- ">
346
- 📥 Download Hasil RTF
347
- </a>
348
- </div>
349
- """
350
- t1 = time.time()
351
-
352
- debug_lines.append(
353
- f"✅ <b>Langkah 6</b> — Build Download Link: <b>{t1-t0:.4f}s</b>"
354
- )
355
-
356
- t_end = time.time()
357
- debug_lines.append(f"⏱ <b>TOTAL WAKTU: {t_end-t_total:.2f}s</b>")
358
-
359
- debug_html = _build_debug_html(debug_lines)
360
- found_labels: set[str] = set(entity_map.values())
361
-
362
- # Legend
363
- legend_rows = ""
364
- for label_id in sorted(found_labels):
365
- label_idn = LABEL_MAP.get(label_id, label_id)
366
- hex_color = LABEL_HEX.get(label_id, "#e2e8f0")
367
- legend_rows += f"""
368
- <tr>
369
- <td style="padding:8px 14px;border-bottom:1px solid #e8edf5;">
370
- <span style="display:inline-block;width:24px;height:24px;border-radius:6px;
371
- background:{hex_color};vertical-align:middle;border:1px solid rgba(0,0,0,0.1);"></span>
372
- </td>
373
- <td style="padding:8px 14px;border-bottom:1px solid #e8edf5;font-weight:600;color:#1e293b;font-size:13px;">{label_id}</td>
374
- <td style="padding:8px 14px;border-bottom:1px solid #e8edf5;color:#475569;font-size:13px;">{label_idn}</td>
375
- </tr>"""
376
-
377
- legend_html = f"""
378
- <div style="margin-top:4px;">
379
- <p style="font-size:13px;color:#64748b;margin-bottom:10px;">
380
- Ditemukan {len(found_labels)}jenis entitas
381
- dari {page_count}halaman PDF.
382
- Buka file rtf di Microsoft Word atau LibreOffice Writer.
383
- </p>
384
- <div style="overflow-x:auto;border-radius:12px;border:1px solid #e2e8f0;box-shadow:0 2px 12px rgba(0,0,0,0.06);">
385
- <table style="width:100%;border-collapse:collapse;font-family:'Segoe UI',sans-serif;">
386
- <thead>
387
- <tr style="background:linear-gradient(135deg,#1e40af,#6d28d9);">
388
- <th style="padding:11px 14px;color:#fff;font-size:12px;letter-spacing:0.05em;width:50px;">WARNA</th>
389
- <th style="padding:11px 14px;color:#fff;font-size:12px;letter-spacing:0.05em;text-align:left;">KODE</th>
390
- <th style="padding:11px 14px;color:#fff;font-size:12px;letter-spacing:0.05em;text-align:left;">NAMA ENTITAS</th>
391
- </tr>
392
- </thead>
393
- <tbody>{legend_rows}</tbody>
394
- </table>
395
- </div>
396
- </div>"""
397
-
398
- return download_html, legend_html, "", debug_html
399
 
400
  # CSS
401
  CUSTOM_CSS = """
@@ -462,58 +246,33 @@ with gr.Blocks(title="NER for Financial Statements", css=CUSTOM_CSS) as demo:
462
  gr.HTML('<div class="section-heading" style="margin-top:20px;">Hasil Analisis Entitas</div>')
463
  text_output = gr.HTML(value="<p style='color:#64748b;font-size:14px;font-family:DM Sans,sans-serif;padding:20px;text-align:center;'>Masukkan teks lalu klik Lakukan Analisis.</p>")
464
 
465
- with gr.Tab("Analisis PDF"):
466
  with gr.Column(elem_id="center-col"):
467
- gr.HTML(
468
- '''<div style="margin:0 0 16px;">
469
- <p style="color:#94a3b8;font-family:DM Sans,sans-serif;font-size:13px;margin:0 0 8px;">
470
- Unggah PDF laporan keuangan maks 5 halaman.
471
- Output berupa file.rtf dengan stabilo warna —
472
- bisa dibuka di Microsoft Word atau LibreOffice.
473
- </p>
474
- </div>'''
475
- )
476
- gr.HTML('<div class="section-heading">Unggah PDF</div>')
477
- pdf_input = gr.File(label="", file_types=[".pdf"], type="filepath")
478
- analyze_pdf_btn = gr.Button("Highlight Entitas → Download RTF", elem_id="analyze-pdf-btn")
479
- pdf_warning = gr.HTML(value="")
480
-
481
- with gr.Column(visible=False) as pdf_results_col:
482
- gr.HTML('<div class="section-heading" style="margin-top:20px;">Download Hasil RTF</div>')
483
- pdf_output = gr.HTML(value="")
484
- gr.HTML('<div class="section-heading" style="margin-top:20px;">Keterangan Warna Entitas</div>')
485
- pdf_legend = gr.HTML(value="")
486
- # Debug panel — selalu tampil setelah proses
487
- pdf_debug = gr.HTML(value="")
488
-
489
  gr.HTML('<div id="footer">NER for Financial Statements 2026</div>')
490
-
491
  # Wiring
492
  for btn, body in example_btns:
493
  btn.click(fn=lambda b=body: b, inputs=[], outputs=text_input)
494
-
495
  analyze_btn.click(fn=run_ner, inputs=text_input, outputs=text_output)
496
  text_input.submit(fn=run_ner, inputs=text_input, outputs=text_output)
497
 
498
- def handle_pdf(pdf_file):
499
- download_html, legend_html, warn_html, debug_html = run_ner_pdf(pdf_file)
500
-
501
- show = gr.Column(
502
- visible=bool(download_html)
503
- )
504
-
505
- return (
506
- download_html,
507
- legend_html,
508
- warn_html,
509
- debug_html,
510
- show
511
- )
512
-
513
  analyze_pdf_btn.click(
514
- fn=handle_pdf,
515
  inputs=pdf_input,
516
- outputs=[pdf_output, pdf_legend, pdf_warning, pdf_debug, pdf_results_col],
517
  )
518
 
519
  if __name__ == "__main__":
 
 
 
 
 
1
  import os
2
  import re
3
  import time
4
+ import fitz
5
+ import gradio as gr
6
 
7
+ from transformers import pipeline
 
 
 
 
 
 
8
 
9
+ # Load model
10
+ ner = pipeline("ner", model="cahya/NusaBert-ner-v1.3", aggregation_strategy="simple")
11
 
12
  # Label mapping
13
  LABEL_MAP = {
 
19
  "REG": "Agama", "TIM": "Waktu", "WOA": "Karya Seni", "LAN": "Bahasa",
20
  }
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  LABEL_HEX = {
23
  "PER":"#FFBFBF","ORG":"#AEDDFF","NOR":"#8DC7FF","LOC":"#B8FFB8",
24
  "GPE":"#99F2CC","FAC":"#D9FFA5","DAT":"#FFE58C","TIM":"#FFCC66",
 
71
  label_id = raw_label.replace("B-","").replace("I-","").replace("B_","").replace("I_","")
72
  return label_id.split("-")[-1].upper().strip()
73
 
74
+ def highlight_html(text: str, entity_map: dict) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  sorted_entities = sorted(entity_map.items(), key=lambda x: len(x[0]), reverse=True)
76
  spans = []
77
  used = [False] * len(text)
 
87
  for i in range(s, e):
88
  used[i] = True
89
  spans.sort(key=lambda x: x[0])
90
+ parts = []
91
  cursor = 0
92
  for s, e, label_id in spans:
93
  if cursor < s:
94
+ parts.append(text[cursor:s].replace("\n", "<br>"))
95
+ hex_color = LABEL_HEX.get(label_id, "#e2e8f0")
96
+ label_idn = LABEL_MAP.get(label_id, label_id)
97
+ word = text[s:e]
98
+ parts.append(
99
+ f'<mark style="background:{hex_color};border-radius:4px;padding:1px 4px;'
100
+ f'font-weight:600;" title="{label_idn}">{word}</mark>'
101
+ )
102
  cursor = e
103
  if cursor < len(text):
104
+ parts.append(text[cursor:].replace("\n", "<br>"))
 
 
 
 
 
105
  return (
106
+ '<div style="font-family:\'DM Sans\',sans-serif;font-size:14px;line-height:2;'
107
+ 'color:#1e293b;background:#fff;border-radius:12px;padding:20px 24px;'
108
+ 'border:1px solid #e2e8f0;white-space:pre-wrap;">'
109
+ + "".join(parts) + "</div>"
 
 
 
 
 
 
 
 
 
 
 
110
  )
111
+
 
 
 
 
 
 
 
 
 
112
  # NER Teks
113
+ def run_ner_file(upload_file):
114
+ if upload_file is None:
115
+ return "<p style='color:#94a3b8;font-style:italic;padding:16px;'>Unggah file terlebih dahulu.</p>", ""
116
+
117
+ file_path = upload_file if isinstance(upload_file, str) else upload_file.name
118
+ ext = os.path.splitext(file_path)[-1].lower()
119
+
120
+ if ext == ".txt":
121
+ with open(file_path, "r", encoding="utf-8", errors="replace") as f:
122
+ full_text = f.read()
123
+ page_count = 1
124
+ elif ext == ".pdf":
125
+ doc = fitz.open(file_path)
126
+ page_count = len(doc)
127
+ if page_count > MAX_PDF_PAGES:
128
+ doc.close()
129
+ return (
130
+ f"<p style='color:#be123c;padding:16px;'>PDF terlalu banyak halaman "
131
+ f"({page_count}). Maks {MAX_PDF_PAGES} halaman.</p>", ""
132
+ )
133
+ full_text = "\n\n".join(page.get_text() for page in doc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  doc.close()
135
+ else:
136
+ return "<p style='color:#be123c;padding:16px;'>Format tidak didukung.</p>", ""
137
+
138
+ # Chunking
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  chunks = []
140
  start = 0
141
  while start < len(full_text):
 
144
  if end == len(full_text):
145
  break
146
  start = end - OVERLAP_CHARS
 
 
147
 
148
+ # NER
 
149
  all_ner_results = []
150
+ for chunk in chunks:
151
+ all_ner_results.extend(ner(chunk.strip()))
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
  if not all_ner_results:
154
+ return "<p style='color:#94a3b8;padding:16px;'>Tidak ada entitas ditemukan.</p>", ""
 
155
 
156
+ # Bangun entity_map
 
157
  entity_map: dict[str, str] = {}
158
  for ent in all_ner_results:
159
  word = clean_word(ent["word"])
160
+ if len(word) < 2:
161
+ continue
162
  label_id = get_label_id(ent["entity_group"])
163
  w_lower = word.lower()
164
  if w_lower not in entity_map:
165
  entity_map[w_lower] = label_id
 
 
166
 
167
  if not entity_map:
168
+ return "<p style='color:#94a3b8;padding:16px;'>Tidak ada entitas ditemukan.</p>", ""
 
 
 
 
 
 
 
 
 
 
169
 
170
+ highlighted = highlight_html(full_text, entity_map)
171
 
172
+ # Badge legend
173
+ found_labels = set(entity_map.values())
174
+ badges = "".join(
175
+ f'<span style="background:{LABEL_HEX.get(l,"#e2e8f0")};border-radius:20px;'
176
+ f'padding:3px 10px;font-size:12px;font-weight:600;color:#1e293b;margin:3px;">'
177
+ f'{LABEL_MAP.get(l, l)}</span>'
178
+ for l in sorted(found_labels)
179
  )
180
+ legend_html = f'<div style="margin-bottom:12px;line-height:2;">{badges}</div>'
181
 
182
+ return highlighted, legend_html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
  # CSS
185
  CUSTOM_CSS = """
 
246
  gr.HTML('<div class="section-heading" style="margin-top:20px;">Hasil Analisis Entitas</div>')
247
  text_output = gr.HTML(value="<p style='color:#64748b;font-size:14px;font-family:DM Sans,sans-serif;padding:20px;text-align:center;'>Masukkan teks lalu klik Lakukan Analisis.</p>")
248
 
249
+ with gr.Tab("Analisis File"):
250
  with gr.Column(elem_id="center-col"):
251
+ gr.HTML('<p style="color:#94a3b8;font-family:DM Sans,sans-serif;font-size:13px;margin:0 0 16px;">'
252
+ 'Unggah file .pdf (maks 5 halaman) atau .txt.</p>'
253
+ )
254
+ gr.HTML('<div class="section-heading">Unggah File</div>')
255
+ pdf_input = gr.File(label="", file_types=[".pdf", ".txt"], type="filepath")
256
+ analyze_pdf_btn = gr.Button("Analisis & Highlight Entitas", elem_id="analyze-pdf-btn")
257
+
258
+ gr.HTML('<div class="section-heading" style="margin-top:20px;">Entitas Ditemukan</div>')
259
+ pdf_legend = gr.HTML(value="")
260
+ gr.HTML('<div class="section-heading" style="margin-top:12px;">Teks Ter-highlight</div>')
261
+ pdf_output = gr.HTML(value="")
262
+
 
 
 
 
 
 
 
 
 
 
263
  gr.HTML('<div id="footer">NER for Financial Statements 2026</div>')
264
+
265
  # Wiring
266
  for btn, body in example_btns:
267
  btn.click(fn=lambda b=body: b, inputs=[], outputs=text_input)
268
+
269
  analyze_btn.click(fn=run_ner, inputs=text_input, outputs=text_output)
270
  text_input.submit(fn=run_ner, inputs=text_input, outputs=text_output)
271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  analyze_pdf_btn.click(
273
+ fn=run_ner_file,
274
  inputs=pdf_input,
275
+ outputs=[pdf_output, pdf_legend],
276
  )
277
 
278
  if __name__ == "__main__":