Spaces:
Sleeping
Sleeping
| # 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) |