adn
commited on
Update app.py
Browse files
app.py
CHANGED
@@ -8,6 +8,7 @@ import os
|
|
8 |
import logging
|
9 |
from typing import Dict, Any, List
|
10 |
from transformers import AutoTokenizer
|
|
|
11 |
|
12 |
# Setup logging
|
13 |
logging.basicConfig(level=logging.INFO)
|
@@ -18,11 +19,20 @@ MODEL_PATH = "model.tflite"
|
|
18 |
TOKENIZER_PATH = "tokenizer"
|
19 |
MAX_LENGTH = 128
|
20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
# Inisialisasi FastAPI
|
22 |
app = FastAPI(
|
23 |
title="Damkar Classification API (TFLite)",
|
24 |
-
description="API untuk klasifikasi tipe laporan damkar menggunakan TFLite model
|
25 |
-
version="1.
|
26 |
)
|
27 |
|
28 |
# Global variables
|
@@ -69,14 +79,14 @@ async def load_model():
|
|
69 |
raise e
|
70 |
|
71 |
def predict_tflite(text: str) -> Dict[str, Any]:
|
72 |
-
"""Fungsi prediksi menggunakan TFLite model - mengembalikan
|
73 |
global interpreter, tokenizer, input_details, output_details
|
74 |
|
75 |
if not all([interpreter, tokenizer]):
|
76 |
raise HTTPException(status_code=503, detail="Model components not loaded")
|
77 |
|
78 |
try:
|
79 |
-
# Resize input tensors
|
80 |
interpreter.resize_tensor_input(input_details[0]['index'], [1, MAX_LENGTH])
|
81 |
interpreter.resize_tensor_input(input_details[1]['index'], [1, MAX_LENGTH])
|
82 |
interpreter.resize_tensor_input(input_details[2]['index'], [1, MAX_LENGTH])
|
@@ -96,7 +106,7 @@ def predict_tflite(text: str) -> Dict[str, Any]:
|
|
96 |
token_type_ids = encoded['token_type_ids'].astype(np.int32)
|
97 |
attention_mask = encoded['attention_mask'].astype(np.int32)
|
98 |
|
99 |
-
# Set tensors - gunakan urutan yang benar
|
100 |
interpreter.set_tensor(input_details[0]['index'], attention_mask)
|
101 |
interpreter.set_tensor(input_details[1]['index'], input_ids)
|
102 |
interpreter.set_tensor(input_details[2]['index'], token_type_ids)
|
@@ -104,7 +114,7 @@ def predict_tflite(text: str) -> Dict[str, Any]:
|
|
104 |
# Run inference
|
105 |
interpreter.invoke()
|
106 |
|
107 |
-
# Get raw output
|
108 |
raw_output = interpreter.get_tensor(output_details[0]['index'])
|
109 |
|
110 |
# Hitung probabilitas dengan softmax
|
@@ -114,8 +124,12 @@ def predict_tflite(text: str) -> Dict[str, Any]:
|
|
114 |
predicted_class_index = int(np.argmax(raw_output, axis=1)[0])
|
115 |
max_confidence = float(np.max(probabilities))
|
116 |
|
|
|
|
|
|
|
117 |
return {
|
118 |
"predicted_class_index": predicted_class_index,
|
|
|
119 |
"confidence": max_confidence,
|
120 |
"raw_output": raw_output[0].tolist(), # Convert numpy array to list
|
121 |
"probabilities": probabilities.tolist(),
|
@@ -136,6 +150,7 @@ class InputText(BaseModel):
|
|
136 |
|
137 |
class PredictionResponse(BaseModel):
|
138 |
predicted_class_index: int
|
|
|
139 |
confidence: float
|
140 |
raw_output: List[float]
|
141 |
probabilities: List[float]
|
@@ -148,7 +163,7 @@ HTML_TEMPLATE = """
|
|
148 |
<!DOCTYPE html>
|
149 |
<html>
|
150 |
<head>
|
151 |
-
<title>Damkar Classification
|
152 |
<meta charset="UTF-8">
|
153 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
154 |
<style>
|
@@ -166,7 +181,7 @@ HTML_TEMPLATE = """
|
|
166 |
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
167 |
}
|
168 |
h1 {
|
169 |
-
color: #
|
170 |
text-align: center;
|
171 |
margin-bottom: 30px;
|
172 |
}
|
@@ -255,28 +270,33 @@ HTML_TEMPLATE = """
|
|
255 |
color: #0056b3;
|
256 |
}
|
257 |
.raw-output {
|
258 |
-
background-color: #
|
259 |
padding: 10px;
|
260 |
border-radius: 4px;
|
261 |
font-family: monospace;
|
262 |
font-size: 12px;
|
263 |
margin: 10px 0;
|
264 |
-
max-height:
|
265 |
overflow-y: auto;
|
|
|
|
|
266 |
}
|
267 |
-
.
|
268 |
-
|
|
|
|
|
|
|
|
|
269 |
padding: 10px;
|
270 |
-
|
271 |
-
|
272 |
-
font-size: 14px;
|
273 |
}
|
274 |
</style>
|
275 |
</head>
|
276 |
<body>
|
277 |
<div class="container">
|
278 |
-
<h1>π
|
279 |
-
<p style="text-align: center; color: #666;">
|
280 |
|
281 |
<div class="form-group">
|
282 |
<label for="textInput">Masukkan teks laporan:</label>
|
@@ -299,16 +319,23 @@ HTML_TEMPLATE = """
|
|
299 |
<div class="example-text" onclick="setExample('ular masuk ke dalam rumah warga')">
|
300 |
π "ular masuk ke dalam rumah warga"
|
301 |
</div>
|
302 |
-
|
303 |
-
π± "kucing terjebak di atas pohon tinggi"
|
304 |
-
</div>
|
305 |
-
<div class="example-text" onclick="setExample('pohon tumbang menghalangi jalan raya')">
|
306 |
π³ "pohon tumbang menghalangi jalan raya"
|
307 |
</div>
|
|
|
|
|
|
|
308 |
</div>
|
309 |
</div>
|
310 |
|
311 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
312 |
function setExample(text) {
|
313 |
document.getElementById('textInput').value = text;
|
314 |
}
|
@@ -341,34 +368,35 @@ HTML_TEMPLATE = """
|
|
341 |
const data = await response.json();
|
342 |
|
343 |
if (response.ok) {
|
|
|
|
|
344 |
let resultHTML = `
|
345 |
<h3>Hasil Prediksi:</h3>
|
346 |
-
<
|
347 |
<p><strong>Confidence:</strong> ${(data.confidence * 100).toFixed(2)}%</p>
|
348 |
|
349 |
-
<
|
350 |
-
<strong>Model Info:</strong><br>
|
351 |
-
Output Shape: ${JSON.stringify(data.model_info.output_shape)}<br>
|
352 |
-
Number of Classes: ${data.model_info.num_classes}
|
353 |
-
</div>
|
354 |
-
|
355 |
-
<h4>Probabilitas per Class:</h4>
|
356 |
`;
|
357 |
|
358 |
data.probabilities.forEach((prob, index) => {
|
359 |
const percentage = (prob * 100).toFixed(4);
|
360 |
const isMax = index === data.predicted_class_index;
|
|
|
361 |
resultHTML += `
|
362 |
<div class="prob-item" style="${isMax ? 'background-color: #fff3cd; font-weight: bold;' : ''}">
|
363 |
-
<span
|
364 |
<span>${percentage}%</span>
|
365 |
</div>
|
366 |
`;
|
367 |
});
|
368 |
|
369 |
resultHTML += `
|
370 |
-
<
|
371 |
-
|
|
|
|
|
|
|
|
|
372 |
`;
|
373 |
|
374 |
showResult('success', resultHTML);
|
@@ -390,9 +418,9 @@ HTML_TEMPLATE = """
|
|
390 |
resultDiv.style.display = 'block';
|
391 |
}
|
392 |
|
393 |
-
// Allow Enter
|
394 |
-
document.getElementById('textInput').addEventListener('
|
395 |
-
if (e.key === 'Enter' && e.ctrlKey) {
|
396 |
predict();
|
397 |
}
|
398 |
});
|
@@ -433,7 +461,8 @@ def health_check():
|
|
433 |
"dtype": str(detail['dtype'])
|
434 |
} for i, detail in enumerate(output_details)
|
435 |
],
|
436 |
-
"max_length": MAX_LENGTH
|
|
|
437 |
}
|
438 |
}
|
439 |
|
@@ -463,7 +492,7 @@ def test_endpoint():
|
|
463 |
return {
|
464 |
"message": "TFLite API is working!",
|
465 |
"status": "ok",
|
466 |
-
"version": "
|
467 |
"endpoints": {
|
468 |
"ui": "/",
|
469 |
"predict": "/predict",
|
|
|
8 |
import logging
|
9 |
from typing import Dict, Any, List
|
10 |
from transformers import AutoTokenizer
|
11 |
+
import json
|
12 |
|
13 |
# Setup logging
|
14 |
logging.basicConfig(level=logging.INFO)
|
|
|
19 |
TOKENIZER_PATH = "tokenizer"
|
20 |
MAX_LENGTH = 128
|
21 |
|
22 |
+
# Class label mapping
|
23 |
+
CLASS_LABELS = {
|
24 |
+
0: "Evakuasi/Penyelamatan Hewan",
|
25 |
+
1: "Kebakaran",
|
26 |
+
2: "Layanan Lingkungan & Fasilitas Umum",
|
27 |
+
3: "Penyelamatan Non Hewan & Bantuan Teknis"
|
28 |
+
}
|
29 |
+
|
30 |
+
|
31 |
# Inisialisasi FastAPI
|
32 |
app = FastAPI(
|
33 |
title="Damkar Classification API (TFLite)",
|
34 |
+
description="API untuk klasifikasi tipe laporan damkar menggunakan TFLite model",
|
35 |
+
version="1.1.0"
|
36 |
)
|
37 |
|
38 |
# Global variables
|
|
|
79 |
raise e
|
80 |
|
81 |
def predict_tflite(text: str) -> Dict[str, Any]:
|
82 |
+
"""Fungsi prediksi menggunakan TFLite model - mengembalikan output dengan label"""
|
83 |
global interpreter, tokenizer, input_details, output_details
|
84 |
|
85 |
if not all([interpreter, tokenizer]):
|
86 |
raise HTTPException(status_code=503, detail="Model components not loaded")
|
87 |
|
88 |
try:
|
89 |
+
# Resize input tensors (jika diperlukan)
|
90 |
interpreter.resize_tensor_input(input_details[0]['index'], [1, MAX_LENGTH])
|
91 |
interpreter.resize_tensor_input(input_details[1]['index'], [1, MAX_LENGTH])
|
92 |
interpreter.resize_tensor_input(input_details[2]['index'], [1, MAX_LENGTH])
|
|
|
106 |
token_type_ids = encoded['token_type_ids'].astype(np.int32)
|
107 |
attention_mask = encoded['attention_mask'].astype(np.int32)
|
108 |
|
109 |
+
# Set tensors - gunakan urutan yang benar sesuai model
|
110 |
interpreter.set_tensor(input_details[0]['index'], attention_mask)
|
111 |
interpreter.set_tensor(input_details[1]['index'], input_ids)
|
112 |
interpreter.set_tensor(input_details[2]['index'], token_type_ids)
|
|
|
114 |
# Run inference
|
115 |
interpreter.invoke()
|
116 |
|
117 |
+
# Get raw output (logits)
|
118 |
raw_output = interpreter.get_tensor(output_details[0]['index'])
|
119 |
|
120 |
# Hitung probabilitas dengan softmax
|
|
|
124 |
predicted_class_index = int(np.argmax(raw_output, axis=1)[0])
|
125 |
max_confidence = float(np.max(probabilities))
|
126 |
|
127 |
+
# Dapatkan label kelas dari index
|
128 |
+
predicted_class_label = CLASS_LABELS.get(predicted_class_index, "Unknown Class")
|
129 |
+
|
130 |
return {
|
131 |
"predicted_class_index": predicted_class_index,
|
132 |
+
"predicted_class_label": predicted_class_label,
|
133 |
"confidence": max_confidence,
|
134 |
"raw_output": raw_output[0].tolist(), # Convert numpy array to list
|
135 |
"probabilities": probabilities.tolist(),
|
|
|
150 |
|
151 |
class PredictionResponse(BaseModel):
|
152 |
predicted_class_index: int
|
153 |
+
predicted_class_label: str
|
154 |
confidence: float
|
155 |
raw_output: List[float]
|
156 |
probabilities: List[float]
|
|
|
163 |
<!DOCTYPE html>
|
164 |
<html>
|
165 |
<head>
|
166 |
+
<title>Damkar Classification</title>
|
167 |
<meta charset="UTF-8">
|
168 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
169 |
<style>
|
|
|
181 |
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
182 |
}
|
183 |
h1 {
|
184 |
+
color: #d32f2f; /* Red color for Damkar */
|
185 |
text-align: center;
|
186 |
margin-bottom: 30px;
|
187 |
}
|
|
|
270 |
color: #0056b3;
|
271 |
}
|
272 |
.raw-output {
|
273 |
+
background-color: #f0f0f0;
|
274 |
padding: 10px;
|
275 |
border-radius: 4px;
|
276 |
font-family: monospace;
|
277 |
font-size: 12px;
|
278 |
margin: 10px 0;
|
279 |
+
max-height: 150px;
|
280 |
overflow-y: auto;
|
281 |
+
white-space: pre-wrap;
|
282 |
+
word-wrap: break-word;
|
283 |
}
|
284 |
+
.predicted-label {
|
285 |
+
font-size: 1.5em;
|
286 |
+
font-weight: bold;
|
287 |
+
color: #0056b3;
|
288 |
+
text-align: center;
|
289 |
+
margin: 15px 0;
|
290 |
padding: 10px;
|
291 |
+
background-color: #e7f3ff;
|
292 |
+
border-radius: 6px;
|
|
|
293 |
}
|
294 |
</style>
|
295 |
</head>
|
296 |
<body>
|
297 |
<div class="container">
|
298 |
+
<h1>π Klasifikasi Laporan Damkar</h1>
|
299 |
+
<p style="text-align: center; color: #666;">Masukkan teks laporan untuk diklasifikasikan oleh model AI.</p>
|
300 |
|
301 |
<div class="form-group">
|
302 |
<label for="textInput">Masukkan teks laporan:</label>
|
|
|
319 |
<div class="example-text" onclick="setExample('ular masuk ke dalam rumah warga')">
|
320 |
π "ular masuk ke dalam rumah warga"
|
321 |
</div>
|
322 |
+
<div class="example-text" onclick="setExample('pohon tumbang menghalangi jalan raya')">
|
|
|
|
|
|
|
323 |
π³ "pohon tumbang menghalangi jalan raya"
|
324 |
</div>
|
325 |
+
<div class="example-text" onclick="setExample('cincin tidak bisa dilepas dari jari')">
|
326 |
+
π "cincin tidak bisa dilepas dari jari"
|
327 |
+
</div>
|
328 |
</div>
|
329 |
</div>
|
330 |
|
331 |
<script>
|
332 |
+
const CLASS_LABELS = {
|
333 |
+
0: "π Evakuasi/Penyelamatan Hewan",
|
334 |
+
1: "π₯ Kebakaran",
|
335 |
+
2: "π³ Layanan Lingkungan & Fasilitas Umum",
|
336 |
+
3: "π Penyelamatan Non Hewan & Bantuan Teknis"
|
337 |
+
};
|
338 |
+
|
339 |
function setExample(text) {
|
340 |
document.getElementById('textInput').value = text;
|
341 |
}
|
|
|
368 |
const data = await response.json();
|
369 |
|
370 |
if (response.ok) {
|
371 |
+
const label = CLASS_LABELS[data.predicted_class_index] || "Label tidak diketahui";
|
372 |
+
|
373 |
let resultHTML = `
|
374 |
<h3>Hasil Prediksi:</h3>
|
375 |
+
<div class="predicted-label">${label}</div>
|
376 |
<p><strong>Confidence:</strong> ${(data.confidence * 100).toFixed(2)}%</p>
|
377 |
|
378 |
+
<h4>Probabilitas per Kelas:</h4>
|
|
|
|
|
|
|
|
|
|
|
|
|
379 |
`;
|
380 |
|
381 |
data.probabilities.forEach((prob, index) => {
|
382 |
const percentage = (prob * 100).toFixed(4);
|
383 |
const isMax = index === data.predicted_class_index;
|
384 |
+
const classLabel = CLASS_LABELS[index] || `Class ${index}`;
|
385 |
resultHTML += `
|
386 |
<div class="prob-item" style="${isMax ? 'background-color: #fff3cd; font-weight: bold;' : ''}">
|
387 |
+
<span>${classLabel}</span>
|
388 |
<span>${percentage}%</span>
|
389 |
</div>
|
390 |
`;
|
391 |
});
|
392 |
|
393 |
resultHTML += `
|
394 |
+
<details>
|
395 |
+
<summary style="cursor: pointer; margin-top: 15px;">Lihat Raw Kategori Laporan (untuk developer)</summary>
|
396 |
+
<p><strong>Predicted Class Index:</strong> ${data.predicted_class_index}</p>
|
397 |
+
<h4>Raw Kategori Laporan (Logits):</h4>
|
398 |
+
<div class="raw-output">${JSON.stringify(data.raw_output, null, 2)}</div>
|
399 |
+
</details>
|
400 |
`;
|
401 |
|
402 |
showResult('success', resultHTML);
|
|
|
418 |
resultDiv.style.display = 'block';
|
419 |
}
|
420 |
|
421 |
+
// Allow Ctrl+Enter to submit
|
422 |
+
document.getElementById('textInput').addEventListener('keydown', function(e) {
|
423 |
+
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
424 |
predict();
|
425 |
}
|
426 |
});
|
|
|
461 |
"dtype": str(detail['dtype'])
|
462 |
} for i, detail in enumerate(output_details)
|
463 |
],
|
464 |
+
"max_length": MAX_LENGTH,
|
465 |
+
"class_labels": CLASS_LABELS
|
466 |
}
|
467 |
}
|
468 |
|
|
|
492 |
return {
|
493 |
"message": "TFLite API is working!",
|
494 |
"status": "ok",
|
495 |
+
"version": "1.1.0",
|
496 |
"endpoints": {
|
497 |
"ui": "/",
|
498 |
"predict": "/predict",
|