Spaces:
Sleeping
Sleeping
File size: 17,687 Bytes
8f85a32 8537a21 8f85a32 65c2797 8f85a32 a44001e 8f85a32 a44001e 8f85a32 80cef6e 8f85a32 e572816 8f85a32 e572816 8f85a32 e572816 8f85a32 e572816 8f85a32 e572816 8f85a32 e572816 8f85a32 e572816 65c2797 e572816 65c2797 e572816 65c2797 e572816 8f85a32 e572816 8f85a32 e572816 8f85a32 e572816 8f85a32 e572816 8f85a32 e8f878e 8f85a32 e572816 8f85a32 e572816 8f85a32 e572816 8f85a32 e572816 8f85a32 65c2797 8f85a32 1a14675 e572816 04e8443 8f85a32 65c2797 e572816 8f85a32 e572816 8f85a32 e572816 8f85a32 e572816 8f85a32 e572816 8f85a32 e572816 8f85a32 1a14675 | 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 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 | import os
import json
import unicodedata
import zipfile
from collections import defaultdict
import numpy as np
import onnxruntime as ort
import gradio as gr
# =====================================================================
# CHUẨN ĐẶC TẢ BERT TOKENIZER (KEEP ORIGINAL LOGIC)
# =====================================================================
class PureBertTokenizer:
def __init__(self, vocab_file, do_lower_case=None):
self.vocab = {}
self.id_to_vocab = {}
with open(vocab_file, "r", encoding="utf-8") as f:
for idx, line in enumerate(f):
token = line.strip("\r\n")
self.vocab[token] = idx
self.id_to_vocab[idx] = token
if do_lower_case is None:
has_uppercase = any(any(c.isupper() for c in t) for t in self.vocab if not t.startswith("["))
self.do_lower_case = not has_uppercase
else:
self.do_lower_case = do_lower_case
def _clean_text(self, text):
output = []
for char in text:
cp = ord(char)
if cp == 0 or cp == 0xfffd or self._is_control(char):
continue
if self._is_whitespace(char):
output.append(" ")
else:
output.append(char)
return "".join(output)
def _is_whitespace(self, char):
if char in [" ", "\t", "\n", "\r"]:
return True
return unicodedata.category(char) == "Zs"
def _is_control(self, char):
if char in [" ", "\t", "\n", "\r"]:
return False
return unicodedata.category(char).startswith("C")
def _tokenize_chinese_chars(self, text):
output = []
for char in text:
if self._is_chinese_char(ord(char)):
output.append(" ")
output.append(char)
output.append(" ")
else:
output.append(char)
return "".join(output)
def _is_chinese_char(self, cp):
if ((cp >= 0x4E00 and cp <= 0x9FFF) or (cp >= 0x3400 and cp <= 0x4DBF) or
(cp >= 0x20000 and cp <= 0x2A6DF) or (cp >= 0x2A700 and cp <= 0x2B73F) or
(cp >= 0x2B740 and cp <= 0x2B81F) or (cp >= 0x2B820 and cp <= 0x2CEAF) or
(cp >= 0xF900 and cp <= 0xFAFF) or (cp >= 0x2F800 and cp <= 0x2FA1F)):
return True
return False
def _run_strip_accents(self, text):
text = unicodedata.normalize("NFD", text)
output = []
for char in text:
if unicodedata.category(char) == "Mn":
continue
output.append(char)
return "".join(output)
def _run_split_on_punc(self, text):
chars = list(text)
i = 0
start_new_token = True
output = []
while i < len(chars):
char = chars[i]
if self._is_punctuation(char):
output.append([char])
start_new_token = True
else:
if start_new_token:
output.append([])
start_new_token = False
output[-1].append(char)
i += 1
return ["".join(x) for x in output]
def _is_punctuation(self, char):
cp = ord(char)
if (33 <= cp <= 47) or (58 <= cp <= 64) or (91 <= cp <= 96) or (123 <= cp <= 126):
return True
return unicodedata.category(char).startswith("P")
def tokenize(self, text):
text = unicodedata.normalize("NFC", text)
text = self._clean_text(text)
text = self._tokenize_chinese_chars(text)
orig_tokens = text.split()
split_tokens = []
for token in orig_tokens:
if self.do_lower_case:
token = token.lower()
token = self._run_strip_accents(token)
split_tokens.extend(self._run_split_on_punc(token))
output_tokens = []
for token in split_tokens:
chars = list(token)
if len(chars) > 100:
output_tokens.append("[UNK]")
continue
is_bad = False
start = 0
sub_tokens = []
while start < len(chars):
end = len(chars)
cur_substr = None
while start < end:
substr = "".join(chars[start:end])
if start > 0:
substr = "##" + substr
if substr in self.vocab:
cur_substr = substr
break
end -= 1
if cur_substr is None:
is_bad = True
break
sub_tokens.append(cur_substr)
start = end
if is_bad:
output_tokens.append("[UNK]")
else:
output_tokens.extend(sub_tokens)
return output_tokens
def encode_batch(self, texts, max_length=512, padding=True, truncation=True):
batch_input_ids, batch_attention_mask, batch_token_type_ids = [], [], []
max_len_in_batch = 0
tokenized_batch = []
for text in texts:
tokens = self.tokenize(text)
if truncation and len(tokens) > max_length - 2:
tokens = tokens[:max_length - 2]
input_ids = (
[self.vocab["[CLS]"]] +
[self.vocab.get(t, self.vocab["[UNK]"]) for t in tokens] +
[self.vocab["[SEP]"]]
)
tokenized_batch.append(input_ids)
if len(input_ids) > max_len_in_batch:
max_len_in_batch = len(input_ids)
target_len = max_length if (padding and max_len_in_batch > max_length) else max_len_in_batch
if not padding:
target_len = max_len_in_batch
for input_ids in tokenized_batch:
if len(input_ids) > target_len:
input_ids = input_ids[:target_len]
pad_len = target_len - len(input_ids)
attention_mask = [1] * len(input_ids) + [0] * pad_len
token_type_ids = [0] * target_len
input_ids = input_ids + [self.vocab["[PAD]"]] * pad_len
batch_input_ids.append(input_ids)
batch_attention_mask.append(attention_mask)
batch_token_type_ids.append(token_type_ids)
return {
"input_ids": np.array(batch_input_ids, dtype=np.int64),
"attention_mask": np.array(batch_attention_mask, dtype=np.int64),
"token_type_ids": np.array(batch_token_type_ids, dtype=np.int64)
}
def convert_ids_to_tokens(self, ids):
return [self.id_to_vocab.get(int(i), "[UNK]") for i in ids]
def join_bert_tokens(token_list):
text = ""
for token in token_list:
if token.startswith("##"):
text += token[2:]
else:
if text and ('\u4e00' <= token <= '\u9fff' or ('\u4e00' <= text[-1] <= '\u9fff')):
text += token
else:
text += (" " if text else "") + token
return text.strip()
# =====================================================================
# KHỞI TẠO CẤU HÌNH TOÀN CỤC & HỆ THỐNG QUẢN LÝ SESSION CACHE
# =====================================================================
VOCAB_FILE = "vocab.txt"
CONFIG_FILE = "config.json"
BATCH_SIZE = 64
MAX_LENGTH = 512
if not all(os.path.exists(f) for f in [VOCAB_FILE, CONFIG_FILE]):
raise FileNotFoundError("Thiếu file cấu hình (vocab.txt hoặc config.json) ở thư mục hiện tại!")
tokenizer = PureBertTokenizer(VOCAB_FILE)
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
config_data = json.load(f)
id2label = {int(k): v for k, v in config_data["id2label"].items()}
# Lưu trữ các ONNX session đã khởi tạo để tránh load lại file nhiều lần
_SESSIONS = {}
def get_onnx_session(model_name):
"""Hàm nạp động và lưu cache cho từng loại model ONNX"""
if model_name not in _SESSIONS:
if not os.path.exists(model_name):
raise FileNotFoundError(f"Không tìm thấy file mô hình '{model_name}' ở thư mục hiện tại!")
sess_options = ort.SessionOptions()
sess_options.intra_op_num_threads = 0
sess_options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
_SESSIONS[model_name] = ort.InferenceSession(model_name, sess_options, providers=["CPUExecutionProvider"])
return _SESSIONS[model_name]
# =====================================================================
# HÀM XỬ LÝ INFERENCE DẠNG GENERATOR (YIELD REAL-TIME OUTPUT)
# =====================================================================
def ner_inference(file_obj, selected_entities, min_count, model_choice):
if file_obj is None:
yield "❌ Thất bại", "Vui lòng tải lên một file dữ liệu dạng .txt hoặc .zip"
return
yield f"⏳ Đang nạp hoặc kiểm tra mô hình {model_choice}...", "Đang chuẩn bị dữ liệu văn bản..."
try:
session = get_onnx_session(model_choice)
except Exception as e:
yield "❌ Lỗi hệ thống", f"Không thể kích hoạt mô hình: {str(e)}"
return
lines = []
file_path = file_obj.name
# TRƯỜNG HỢP 1: FILE ĐẦU VÀO LÀ FILE ZIP
if file_path.lower().endswith('.zip'):
try:
with zipfile.ZipFile(file_path, 'r') as zip_ref:
txt_files = [f for f in zip_ref.namelist() if f.lower().endswith('.txt') and not f.startswith('__MACOSX')]
if not txt_files:
yield "❌ Thất bại", "Không tìm thấy file .txt nào hợp lệ bên trong file ZIP."
return
for txt_file in txt_files:
with zip_ref.open(txt_file) as f:
content = f.read().decode('utf-8', errors='ignore')
file_lines = [line.strip() for line in content.splitlines() if line.strip()]
lines.extend(file_lines)
except Exception as e:
yield "❌ Lỗi đọc file ZIP", str(e)
return
# TRƯỜNG HỢP 2: FILE ĐẦU VÀO LÀ FILE TEXT ĐƠN LẺ
else:
try:
with open(file_path, "r", encoding="utf-8", errors='ignore') as f:
lines = [line.strip() for line in f if line.strip()]
except Exception as e:
yield "❌ Lỗi đọc file TXT", str(e)
return
if not lines:
yield "❌ Thất bại", "Không trích xuất được dòng văn bản nào hợp lệ."
return
lines.sort(key=len)
batches = [lines[i:i + BATCH_SIZE] for i in range(0, len(lines), BATCH_SIZE)]
entity_count = defaultdict(int)
total_batches = len(batches)
# Vòng lặp tính toán và stream dữ liệu cuốn chiếu liên tục
for idx, batch_lines in enumerate(batches):
progress_msg = f"⏳ Đang quét: Batch {idx + 1}/{total_batches} (Tổng cộng {len(lines)} dòng văn bản)"
encoded = tokenizer.encode_batch(batch_lines, padding=True, truncation=True, max_length=MAX_LENGTH)
ort_inputs = {
"input_ids": encoded["input_ids"],
"attention_mask": encoded["attention_mask"],
"token_type_ids": encoded["token_type_ids"]
}
ort_outputs = session.run(["logits"], ort_inputs)
logits = ort_outputs[0]
predictions = np.argmax(logits, axis=-1)
for i in range(len(batch_lines)):
input_ids_seq = encoded["input_ids"][i]
attention_mask_seq = encoded["attention_mask"][i]
pred_seq = predictions[i]
tokens = tokenizer.convert_ids_to_tokens(input_ids_seq)
current_entity_tokens = []
current_entity_label = None
for token, mask, label_id in zip(tokens, attention_mask_seq, pred_seq):
if mask == 0 or token == "[SEP]":
break
if token == "[CLS]":
continue
label = id2label[label_id]
prefix = label.split("-")[0] if "-" in label else label
ent_type = label.split("-")[1] if "-" in label else None
if prefix in ["B", "S", "O"] or (prefix in ["M", "I", "E"] and ent_type != current_entity_label):
current_entity_tokens = []
current_entity_label = None
if prefix == "S":
final_name = join_bert_tokens([token])
if final_name:
entity_count[(final_name, ent_type)] += 1
elif prefix == "B":
current_entity_label = ent_type
current_entity_tokens.append(token)
elif prefix in ["M", "I"]:
if current_entity_label == ent_type:
current_entity_tokens.append(token)
elif prefix == "E":
if current_entity_label == ent_type:
current_entity_tokens.append(token)
final_name = join_bert_tokens(current_entity_tokens)
if final_name:
entity_count[(final_name, current_entity_label)] += 1
current_entity_tokens = []
current_entity_label = None
# Sắp xếp danh sách thực thể theo số lần xuất hiện giảm dần để giao diện hiển thị tối ưu nhất
sorted_entities = sorted(entity_count.items(), key=lambda x: (-x[1], x[0][0]))
# Lọc và tạo chuỗi văn bản kết quả tính đến thời điểm hiện tại
output_lines = []
for (name, label), count in sorted_entities:
if count >= min_count and (not selected_entities or label in selected_entities):
output_lines.append(f"{name}={label}={count}")
current_stream_text = "\n".join(output_lines) if output_lines else "Chưa tìm thấy thực thể nào vượt qua ngưỡng min_count..."
# Đẩy trạng thái tiến độ và danh sách cập nhật trực tiếp lên UI
yield progress_msg, current_stream_text
# Trả về kết quả chốt sau khi hoàn thành toàn bộ tác vụ
final_progress = f"🎉 Hoàn thành xuất sắc! Đã quét xong toàn bộ {total_batches} batches."
if not output_lines:
yield final_progress, "Không tìm thấy thực thể nào khớp với bộ lọc hiện tại sau khi quét hết file."
else:
yield final_progress, "\n".join(output_lines)
# =====================================================================
# THIẾT KẾ GIAO DIỆN GRADIO UI NÂNG CẤP
# =====================================================================
css = "h1 { text-align: center; color: #1A202C; }"
with gr.Blocks() as demo:
gr.Markdown("# BERT NER Bulk Inference Engine (Real-time Stream Edition)")
gr.Markdown("Hỗ trợ tải lên file đơn `.txt` hoặc tập hợp `.zip`. Hệ thống hiển thị tiến độ và cập nhật tần suất thực thể trực tiếp.")
with gr.Row():
with gr.Column(scale=1):
input_file = gr.File(label="Upload File (.txt hoặc .zip)", file_types=[".txt", ".zip"])
# Tính năng mới: Chọn mô hình chạy trực tiếp
model_choice = gr.Dropdown(
label="Chọn mô hình ONNX Inference",
choices=["bert_ner_int8.onnx", "bert_ner_fp32.onnx"],
value="bert_ner_int8.onnx"
)
entity_filter = gr.CheckboxGroup(
label="Entities Filter",
choices=["PER", "ORG", "LOC", "GPE"],
value=["PER", "ORG", "LOC", "GPE"]
)
count_entities = gr.Number(
label="Min Frequency Threshold",
minimum=1,
maximum=500,
step=1,
value=1
)
submit_btn = gr.Button("Extract Entities", variant="primary")
with gr.Column(scale=1):
# Output 1: Hiển thị tiến trình thay đổi liên tục
progress_text = gr.Textbox(
label="Tiến độ thực thi (Progress Status)",
interactive=False,
lines=2,
placeholder="Đang chờ lệnh từ người dùng..."
)
# Output 2: Stream dữ liệu số lượng thực thể tăng tiến
output_text = gr.Textbox(
label="Kết quả thực thể Real-time (Stream Output)",
interactive=False,
lines=15,
max_lines=25,
placeholder="Danh sách thực thể kèm số lượng sẽ nhảy số liên tục tại đây..."
)
submit_btn.click(
fn=ner_inference,
inputs=[input_file, entity_filter, count_entities, model_choice],
outputs=[progress_text, output_text]
)
demo.launch(
css=css,
theme=gr.themes.Soft()
) |