SicheateR99 commited on
Commit
acebb0f
·
verified ·
1 Parent(s): a324ffe

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +55 -30
app.py CHANGED
@@ -7,14 +7,13 @@ from fastapi import FastAPI
7
  from pydantic import BaseModel
8
  from fastapi.middleware.cors import CORSMiddleware
9
 
10
- # Optimasi Memory (Wajib untuk HF Free Tier)
11
  os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
12
  os.environ["CUDA_VISIBLE_DEVICES"] = "-1"
13
  os.environ["TF_NUM_INTEROP_THREADS"] = "1"
14
  os.environ["TF_NUM_INTRAOP_THREADS"] = "1"
15
 
16
  app = FastAPI(title="NER API Service")
17
-
18
  app.add_middleware(
19
  CORSMiddleware,
20
  allow_origins=["*"],
@@ -23,36 +22,69 @@ app.add_middleware(
23
  allow_headers=["*"],
24
  )
25
 
26
- # Global variables untuk Lazy Loading
27
  ner_model = None
28
  vocab = None
29
-
30
- # Konfigurasi NER
31
  MAX_SEQ_LEN = 64
32
  IDX2LABEL = {0: "<PAD>", 1: "O", 2: "B-MERCHANT", 3: "I-MERCHANT", 4: "B-ITEM", 5: "I-ITEM", 6: "B-TOTAL", 7: "I-TOTAL"}
33
  UNK_TOKEN = "<UNK>"
34
  NUM_TOKEN = "<NUM>"
35
  PRICE_TOKEN = "<PRICE>"
36
 
37
- # Schema untuk menerima data JSON dari Space A
38
  class TextRequest(BaseModel):
39
  text: str
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  def get_ner_model():
42
  global ner_model, vocab
43
  if ner_model is None:
44
- with open("vocab.pkl", "rb") as f:
45
  vocab = pickle.load(f)
46
- # compile=False penting agar tidak error saat mencari custom loss function
47
- ner_model = tf.keras.models.load_model("receipt_ner_model.keras", compile=False)
 
 
 
 
 
48
  return ner_model, vocab
49
 
50
  def normalize_token(token: str):
51
  token = token.strip()
52
- if re.fullmatch(r"\d{1,3}([.,]\d{3})*", token):
53
- return PRICE_TOKEN
54
- if re.fullmatch(r"\d+", token):
55
- return NUM_TOKEN
56
  token = re.sub(r"^[^a-zA-Z0-9']+|[^a-zA-Z0-9']+$", "", token)
57
  return token.upper() if token else UNK_TOKEN
58
 
@@ -62,53 +94,46 @@ def predict_ner(request: TextRequest):
62
  raw_text = request.text
63
  entities = {"MERCHANT": [], "ITEM": [], "TOTAL": []}
64
 
65
- # Jika teks kosong, langsung kembalikan entitas kosong
66
  if not raw_text.strip():
67
  return {"success": True, "entities": entities}
68
 
69
  model, vcb = get_ner_model()
70
 
71
- # Tokenisasi sederhana
72
  tokens = [t for line in raw_text.split("\n") for t in line.strip().split() if t]
73
  if not tokens:
74
  return {"success": True, "entities": entities}
75
 
76
- # Encoding
77
  norm_tokens = [normalize_token(t) for t in tokens]
78
- X = np.zeros((1, MAX_SEQ_LEN), dtype=np.int32)
79
- mask = np.zeros((1, MAX_SEQ_LEN), dtype=np.float32)
80
 
 
 
81
  length = min(len(norm_tokens), MAX_SEQ_LEN)
 
82
  for i in range(length):
83
  X[0, i] = vcb.get(norm_tokens[i], vcb.get(UNK_TOKEN, 0))
84
- mask[0, i] = 1.0
85
 
86
- # Prediksi menggunakan TensorFlow
87
- probs = model([X, mask], training=False).numpy()
88
  labels = [IDX2LABEL[int(np.argmax(probs[0, i]))] for i in range(length)]
89
 
90
- # Ekstraksi IOB (Inside-Outside-Beginning)
91
  current_type = None
92
  span = []
93
  for tok, lbl in zip(tokens[:length], labels):
94
  if lbl.startswith("B-"):
95
- if current_type:
96
- entities[current_type].append(" ".join(span))
97
  current_type = lbl[2:]
98
  span = [tok]
99
  elif lbl.startswith("I-") and current_type == lbl[2:]:
100
  span.append(tok)
101
  else:
102
- if current_type:
103
- entities[current_type].append(" ".join(span))
104
  current_type = None
105
  span = []
106
- if current_type:
107
- entities[current_type].append(" ".join(span))
108
 
109
  return {"success": True, "entities": entities}
110
 
111
  except Exception as e:
112
- print(f"Error NER: {e}")
113
- # Jika terjadi error internal, kembalikan objek kosong agar Space A tidak crash
114
  return {"success": False, "error": str(e), "entities": {"MERCHANT": [], "ITEM": [], "TOTAL": []}}
 
7
  from pydantic import BaseModel
8
  from fastapi.middleware.cors import CORSMiddleware
9
 
10
+ # Optimasi RAM
11
  os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
12
  os.environ["CUDA_VISIBLE_DEVICES"] = "-1"
13
  os.environ["TF_NUM_INTEROP_THREADS"] = "1"
14
  os.environ["TF_NUM_INTRAOP_THREADS"] = "1"
15
 
16
  app = FastAPI(title="NER API Service")
 
17
  app.add_middleware(
18
  CORSMiddleware,
19
  allow_origins=["*"],
 
22
  allow_headers=["*"],
23
  )
24
 
 
25
  ner_model = None
26
  vocab = None
 
 
27
  MAX_SEQ_LEN = 64
28
  IDX2LABEL = {0: "<PAD>", 1: "O", 2: "B-MERCHANT", 3: "I-MERCHANT", 4: "B-ITEM", 5: "I-ITEM", 6: "B-TOTAL", 7: "I-TOTAL"}
29
  UNK_TOKEN = "<UNK>"
30
  NUM_TOKEN = "<NUM>"
31
  PRICE_TOKEN = "<PRICE>"
32
 
 
33
  class TextRequest(BaseModel):
34
  text: str
35
 
36
+ # =========================================================
37
+ # TRIK BYPASS: BANGUN ARSITEKTUR MANUAL BERDASARKAN LOG
38
+ # =========================================================
39
+ def build_manual_model(vocab_size=7929, max_len=64, num_classes=8):
40
+ # Nama setiap layer disamakan persis dengan log agar weights menempel sempurna
41
+ inputs = tf.keras.Input(shape=(max_len,), dtype='int32', name='tokens')
42
+
43
+ x = tf.keras.layers.Embedding(input_dim=vocab_size, output_dim=64, mask_zero=True, name='embedding')(inputs)
44
+ x = tf.keras.layers.SpatialDropout1D(0.3, name='spatial_dropout1d')(x)
45
+
46
+ # BiLSTM 1
47
+ lstm1_f = tf.keras.layers.LSTM(128, return_sequences=True, dropout=0.4, name='forward_lstm')
48
+ lstm1_b = tf.keras.layers.LSTM(128, return_sequences=True, go_backwards=True, dropout=0.4, name='backward_lstm')
49
+ x = tf.keras.layers.Bidirectional(lstm1_f, backward_layer=lstm1_b, name='bilstm_1')(x)
50
+
51
+ # BiLSTM 2
52
+ lstm2_f = tf.keras.layers.LSTM(64, return_sequences=True, dropout=0.4, name='forward_lstm_1')
53
+ lstm2_b = tf.keras.layers.LSTM(64, return_sequences=True, go_backwards=True, dropout=0.4, name='backward_lstm_1')
54
+ x = tf.keras.layers.Bidirectional(lstm2_f, backward_layer=lstm2_b, name='bilstm_2')(x)
55
+
56
+ # TimeDistributed Dense 1
57
+ dense1 = tf.keras.layers.Dense(64, activation='relu', name='dense')
58
+ x = tf.keras.layers.TimeDistributed(dense1, name='proj')(x)
59
+ x = tf.keras.layers.Dropout(0.4, name='dropout')(x)
60
+
61
+ # TimeDistributed Dense 2 (Logits)
62
+ dense2 = tf.keras.layers.Dense(num_classes, activation='linear', name='dense_1')
63
+ x = tf.keras.layers.TimeDistributed(dense2, name='logits')(x)
64
+
65
+ # Output Softmax
66
+ probs = tf.keras.layers.Activation('softmax', name='probabilities')(x)
67
+
68
+ return tf.keras.Model(inputs=inputs, outputs=probs, name='BiLSTM_NER')
69
+
70
  def get_ner_model():
71
  global ner_model, vocab
72
  if ner_model is None:
73
+ with open("data/processed/vocab.pkl", "rb") as f:
74
  vocab = pickle.load(f)
75
+
76
+ # 1. Buat kerangkanya
77
+ ner_model = build_manual_model()
78
+
79
+ # 2. HANYA isi bobotnya (Ini yang menyelamatkan kita dari error JSON!)
80
+ ner_model.load_weights("receipt_ner_model.keras")
81
+
82
  return ner_model, vocab
83
 
84
  def normalize_token(token: str):
85
  token = token.strip()
86
+ if re.fullmatch(r"\d{1,3}([.,]\d{3})*", token): return PRICE_TOKEN
87
+ if re.fullmatch(r"\d+", token): return NUM_TOKEN
 
 
88
  token = re.sub(r"^[^a-zA-Z0-9']+|[^a-zA-Z0-9']+$", "", token)
89
  return token.upper() if token else UNK_TOKEN
90
 
 
94
  raw_text = request.text
95
  entities = {"MERCHANT": [], "ITEM": [], "TOTAL": []}
96
 
 
97
  if not raw_text.strip():
98
  return {"success": True, "entities": entities}
99
 
100
  model, vcb = get_ner_model()
101
 
 
102
  tokens = [t for line in raw_text.split("\n") for t in line.strip().split() if t]
103
  if not tokens:
104
  return {"success": True, "entities": entities}
105
 
 
106
  norm_tokens = [normalize_token(t) for t in tokens]
 
 
107
 
108
+ # Hanya butuh array X, mask dihapus karena sudah otomatis ditangani layer Embedding
109
+ X = np.zeros((1, MAX_SEQ_LEN), dtype=np.int32)
110
  length = min(len(norm_tokens), MAX_SEQ_LEN)
111
+
112
  for i in range(length):
113
  X[0, i] = vcb.get(norm_tokens[i], vcb.get(UNK_TOKEN, 0))
 
114
 
115
+ # Inference super cepat
116
+ probs = model(X, training=False).numpy()
117
  labels = [IDX2LABEL[int(np.argmax(probs[0, i]))] for i in range(length)]
118
 
 
119
  current_type = None
120
  span = []
121
  for tok, lbl in zip(tokens[:length], labels):
122
  if lbl.startswith("B-"):
123
+ if current_type: entities[current_type].append(" ".join(span))
 
124
  current_type = lbl[2:]
125
  span = [tok]
126
  elif lbl.startswith("I-") and current_type == lbl[2:]:
127
  span.append(tok)
128
  else:
129
+ if current_type: entities[current_type].append(" ".join(span))
 
130
  current_type = None
131
  span = []
132
+ if current_type: entities[current_type].append(" ".join(span))
 
133
 
134
  return {"success": True, "entities": entities}
135
 
136
  except Exception as e:
137
+ import traceback
138
+ print(f"Error NER detail: {traceback.format_exc()}")
139
  return {"success": False, "error": str(e), "entities": {"MERCHANT": [], "ITEM": [], "TOTAL": []}}