paperdetection / app.py
irhamni's picture
Update app.py
164e595 verified
# app.py
# HF Spaces (Gradio 6.x) + TensorFlow Keras .keras image classifier
# UPDATE FINAL: Panduan tindakan hanya 1 (berdasarkan prediksi utama & threshold)
import os
import json
from pathlib import Path
import numpy as np
from PIL import Image
import gradio as gr
import tensorflow as tf
# -----------------------
# Config
# -----------------------
MODEL_PATH = os.environ.get("MODEL_PATH", "export/best_model.keras")
CLASSES_PATH = os.environ.get("CLASSES_PATH", "export/class_names.json")
IMG_SIZE = int(os.environ.get("IMG_SIZE", "224"))
TOP_K = int(os.environ.get("TOP_K", "3"))
# Ambang minimum agar panduan spesifik muncul (mis. 0.50 = 50%)
GUIDE_THRESHOLD = float(os.environ.get("GUIDE_THRESHOLD", "0.50"))
# Optional: suppress TF noisy logs
os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "2") # 0=all, 1=info, 2=warning, 3=error
# Lazy load
_model = None
_class_names = None
def _must_exist(path: str, label: str) -> Path:
p = Path(path)
if not p.exists():
raise FileNotFoundError(
f"{label} tidak ditemukan: '{path}'.\n"
f"Pastikan file ada di repo Space dan path-nya benar.\n"
f"Struktur yang disarankan:\n"
f" export/best_model.keras\n"
f" export/class_names.json"
)
return p
def load_assets():
"""Load model and class names once (lazy)."""
global _model, _class_names
if _model is None:
_must_exist(MODEL_PATH, "Model file")
_model = tf.keras.models.load_model(MODEL_PATH, compile=False)
if _class_names is None:
_must_exist(CLASSES_PATH, "Class names file")
with open(CLASSES_PATH, "r", encoding="utf-8") as f:
data = json.load(f)
# class_names.json boleh berupa:
# - list: ["kelas1","kelas2",...]
# - dict: {"0":"kelas1","1":"kelas2",...}
if isinstance(data, dict):
_class_names = [v for k, v in sorted(data.items(), key=lambda kv: int(kv[0]))]
elif isinstance(data, list):
_class_names = data
else:
raise ValueError("class_names.json harus berupa list atau dict index->label.")
if len(_class_names) == 0:
raise ValueError("class_names.json kosong.")
return _model, _class_names
def preprocess(img: Image.Image) -> np.ndarray:
img = img.convert("RGB").resize((IMG_SIZE, IMG_SIZE))
arr = np.asarray(img, dtype=np.float32) / 255.0
return np.expand_dims(arr, axis=0) # (1,H,W,3)
# ============================================================
# Panduan tindakan (1 panduan saja) — berdasarkan label TOP-1
# ============================================================
def _norm_label(s: str) -> str:
return (s or "").strip().lower().replace("-", " ").replace("_", " ")
GUIDE_INSECT = """\
## Kerusakan akibat serangga / hama
**Tindakan cepat (prioritas 0–24 jam):**
1. **Isolasi** item (kantong plastik bersih/zip) agar hama tidak menyebar ke koleksi lain.
2. **Jangan disikat keras.** Jika ada serbuk/fragmen, lakukan pembersihan **kering dan sangat lembut** (kuas halus) di area kerja terpisah.
3. **Karantina & catat**: lokasi temuan, tingkat kerusakan, indikasi serangga hidup/telur.
**Stabilisasi & penanganan:**
- Jika tersedia fasilitas, pertimbangkan **perlakuan beku (freezing)** untuk mematikan serangga/telur (sesuai SOP lembaga; bungkus berlapis untuk mencegah kondensasi).
- Setelah bebas hama, lakukan **perbaikan fisik** (penguatan sobekan, perataan, perbaikan jilid) oleh petugas kompeten.
**Pencegahan berulang (lingkungan):**
- Housekeeping: vakum rak/area, buang sumber makanan (debu/organik).
- Pantau perangkap serangga, cek titik lembap, perbaiki ventilasi.
- Jaga lingkungan simpan stabil (kelembapan tidak tinggi, sirkulasi baik).
"""
GUIDE_WATER_MOLD = """\
## Kerusakan akibat air / lembap / mold (jamur)
**Tindakan cepat (prioritas 0–24 jam):**
1. **Pisahkan** item basah dari yang kering. Hindari menumpuk (mencegah transfer tinta & jamur).
2. Jika **basah**: lakukan **pengeringan darurat** secepatnya.
- Bentangkan di permukaan bersih, beri alas penyerap.
- Gunakan **kipas/sirkulasi udara** (hindari udara panas langsung).
3. Jika ada indikasi **mold** (bau apek kuat, bercak berbulu/berdebu): **isolasi** dan gunakan APD minimum (masker/respirator sesuai SOP, sarung tangan).
**Stabilisasi & penanganan:**
- Untuk item sangat basah/banyak: pertimbangkan **freeze-dry / pembekuan** sebagai stabilisasi darurat (sesuai SOP & fasilitas).
- Pembersihan mold dilakukan **kering** (vakum HEPA/kuas lembut) di area terkontrol; jangan menggosok agresif.
- Setelah kering stabil, evaluasi distorsi/tinta luntur/halaman menempel—rujuk konservator bila parah.
**Kontrol lingkungan (kunci pencegahan jamur):**
- Turunkan risiko dengan mengurangi kelembapan dan memperbaiki sumber kebocoran.
- Pastikan ruang simpan berventilasi baik; monitoring suhu–RH berkala.
- Bersihkan area terdampak dan inspeksi rak sekitar.
"""
DEFAULT_GUIDE = """\
## Panduan umum (kepercayaan model belum cukup tinggi)
**Langkah aman:**
1. **Stabilisasi dulu**: minimalkan sentuhan, jangan ditekan, simpan rata.
2. **Dokumentasi**: foto, lokasi, tanggal, gejala kerusakan.
3. **Isolasi bila ragu** (bau apek, bercak, serbuk): cegah penyebaran ke koleksi lain.
4. **Rujuk SOP institusi/konservator** untuk tindakan lanjut.
Ambang panduan spesifik saat ini: **{thr:.0%}** (GUIDE_THRESHOLD).
"""
def guide_for_top1(label_top1: str, prob_top1: float) -> str:
"""
Keluarkan 1 panduan saja:
- Jika prob_top1 >= threshold: panduan sesuai kelas top-1
- Jika tidak: panduan umum
"""
if prob_top1 < GUIDE_THRESHOLD:
return DEFAULT_GUIDE.format(thr=GUIDE_THRESHOLD)
lab = _norm_label(label_top1)
# Cocokkan label top-1 ke kategori panduan (sesuaikan keyword sesuai class_names Anda)
insect_keys = ["serangga", "insect", "hama", "rayap", "termite", "kutu", "booklice"]
water_mold_keys = ["air", "water", "basah", "banjir", "lembap", "humidity", "mold", "jamur", "kapang"]
if any(k in lab for k in insect_keys):
return GUIDE_INSECT
if any(k in lab for k in water_mold_keys):
return GUIDE_WATER_MOLD
# Kalau top-1 tinggi tapi tidak termasuk dua kategori di atas
return """\
## Rekomendasi umum untuk kelas terdeteksi
- **Prioritaskan stabilisasi** (minimalkan penanganan, simpan rata, dokumentasi).
- Ikuti **SOP konservasi** sesuai tipe kerusakan pada label.
- Bila ada risiko kontaminasi (bau apek/bercak/serbuk), lakukan **isolasi**.
Catatan: Label terdeteksi belum dipetakan ke panduan khusus.
"""
def predict_image(image: Image.Image):
"""
Return:
1) dict label->prob (untuk gr.Label top-k)
2) markdown panduan (hanya 1)
"""
model, class_names = load_assets()
x = preprocess(image)
probs = model.predict(x, verbose=0)[0] # (num_classes,)
probs = np.asarray(probs, dtype=np.float32).reshape(-1)
k = int(min(TOP_K, probs.shape[0], len(class_names)))
topk_idx = probs.argsort()[-k:][::-1]
out = {}
top_labels = []
top_probs = []
for idx in topk_idx:
idx = int(idx)
label = class_names[idx] if idx < len(class_names) else f"class_{idx}"
p = float(probs[idx])
out[label] = p
top_labels.append(label)
top_probs.append(p)
label_top1 = top_labels[0]
prob_top1 = top_probs[0]
header = (
f"### Rekomendasi tindakan berbasis prediksi (Top-{k})\n"
f"- **Prediksi utama:** **{label_top1}** (**{prob_top1:.0%}**)\n"
f"- Ambang panduan spesifik: **{GUIDE_THRESHOLD:.0%}**\n\n"
)
guide_md = guide_for_top1(label_top1, prob_top1)
return out, header + guide_md
def build_demo():
title = "Preservation Classifier"
desc = "Upload an image of a document/paper fragment. The model predicts the class (top-k) + provides ONE preservation guidance based on the top-1 probability threshold."
with gr.Blocks() as demo:
gr.Markdown(f"# {title}\n{desc}")
with gr.Row():
inp = gr.Image(type="pil", label="Upload image")
out_label = gr.Label(num_top_classes=TOP_K, label="Predictions")
out_guide = gr.Markdown(label="Panduan Tindakan Preservasi")
with gr.Row():
btn_clear = gr.Button("Clear")
btn_submit = gr.Button("Submit", variant="primary")
btn_submit.click(fn=predict_image, inputs=inp, outputs=[out_label, out_guide])
btn_clear.click(fn=lambda: (None, None, ""), inputs=None, outputs=[inp, out_label, out_guide])
return demo
demo = build_demo()
if __name__ == "__main__":
try:
load_assets()
except Exception as e:
print("Asset load warning:", e)
demo.launch(server_name="0.0.0.0", server_port=7860)