Spaces:
Sleeping
Sleeping
Download app.py from RantoG/deteks-api: direct link, hf CLI and curl.
- Browser
- Download file 6.97 kB
-
https://huggingface.co/spaces/RantoG/deteks-api/resolve/main/app.py
- Command line
-
hf download hf://spaces/RantoG/deteks-api/app.py
-
curl -L -o app.py https://huggingface.co/spaces/RantoG/deteks-api/resolve/main/app.py
6.97 kB
| import os | |
| import spaces | |
| import gradio as gr | |
| from fastapi import HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel | |
| import joblib | |
| import pandas as pd | |
| import numpy as np | |
| # --- 1. LOAD MODEL AI --- | |
| # Mengecek di root folder atau di dalam folder models/ | |
| MODEL_PATH = "deteks_isolation_forest.pkl" | |
| if not os.path.exists(MODEL_PATH) and os.path.exists("models/deteks_isolation_forest.pkl"): | |
| MODEL_PATH = "models/deteks_isolation_forest.pkl" | |
| try: | |
| ai_model = joblib.load(MODEL_PATH) | |
| print(f"Model AI berhasil di-load dari {MODEL_PATH}!") | |
| except Exception as e: | |
| ai_model = None | |
| print(f"Error loading model: {e}") | |
| FITUR_AI = [ | |
| 'n_event_kunjungan', 'n_faskes_kunjungan', 'same_day_hits', 'unbundling_hits', | |
| 'n_event_obat', 'n_faskes_obat', 'qty_30d', 'min_interval', 'overlap_days' | |
| ] | |
| # --- 2. FUNGSI UTAMA DENGAN @spaces.GPU (Terhubung Langsung ke Gradio) --- | |
| def calculate_risk_fusion( | |
| participant_key: str, | |
| n_event_kunjungan: float, | |
| n_faskes_kunjungan: float, | |
| same_day_hits: float, | |
| unbundling_hits: float, | |
| n_event_obat: float, | |
| n_faskes_obat: float, | |
| qty_30d: float, | |
| min_interval: float, | |
| overlap_days: float, | |
| rules_score_tambahan: float = 0.0 | |
| ) -> dict: | |
| if ai_model is None: | |
| raise RuntimeError("Model belum ter-load. Pastikan file .pkl ada di repositori.") | |
| features_dict = { | |
| 'n_event_kunjungan': float(n_event_kunjungan), | |
| 'n_faskes_kunjungan': float(n_faskes_kunjungan), | |
| 'same_day_hits': float(same_day_hits), | |
| 'unbundling_hits': float(unbundling_hits), | |
| 'n_event_obat': float(n_event_obat), | |
| 'n_faskes_obat': float(n_faskes_obat), | |
| 'qty_30d': float(qty_30d), | |
| 'min_interval': float(min_interval), | |
| 'overlap_days': float(overlap_days) | |
| } | |
| df_input = pd.DataFrame([features_dict])[FITUR_AI] | |
| raw_score = float(-ai_model.score_samples(df_input)[0]) | |
| # Konversi raw_score ke persentil (skala 0 - 100) | |
| ai_percentile = float(np.clip((raw_score - 0.35) / 0.40 * 100, 0, 100)) | |
| # Hitung & Normalisasi Skor Rules (skala 0 - 100) | |
| total_rules = float(same_day_hits) + float(unbundling_hits) + float(rules_score_tambahan) | |
| rules_norm = float(min((total_rules / 140.0) * 100, 100.0)) | |
| # Risk Fusion (60% Rules + 40% AI) | |
| review_priority = (0.6 * rules_norm) + (0.4 * ai_percentile) | |
| # Reason Codes (Explainability) | |
| reasons = [] | |
| if same_day_hits > 0: | |
| reasons.append(f"Kunjungan di hari yang sama ({int(same_day_hits)}x)") | |
| if unbundling_hits > 0: | |
| reasons.append(f"Indikasi unbundling kunjungan ({int(unbundling_hits)}x)") | |
| if min_interval < 25: | |
| reasons.append(f"Early refill obat (interval {int(min_interval)} hari)") | |
| if overlap_days >= 3: | |
| reasons.append(f"Overlap stok obat tinggi ({int(overlap_days)} hari)") | |
| if n_faskes_obat > 1: | |
| reasons.append(f"Pengambilan obat lintas faskes ({int(n_faskes_obat)} faskes)") | |
| if not reasons: | |
| reasons.append("Pola terpantau normal / risiko rendah") | |
| if review_priority >= 70: | |
| status = "Tinggi" | |
| elif review_priority >= 40: | |
| status = "Sedang" | |
| else: | |
| status = "Rendah" | |
| return { | |
| "participant_key": str(participant_key), | |
| "review_priority": round(review_priority, 2), | |
| "risk_band": status, | |
| "reasons": reasons, | |
| "detail_scores": { | |
| "total_rules_raw": round(total_rules, 2), | |
| "rules_score_normalized": round(rules_norm, 2), | |
| "ai_anomaly_percentile": round(ai_percentile, 2), | |
| "raw_if_score": round(raw_score, 4) | |
| } | |
| } | |
| # --- 3. ANTARMUKA GRADIO --- | |
| with gr.Blocks(title="DETEKS AI Engine") as demo: | |
| gr.Markdown("## 🏥 DETEKS - AI Risk Scoring Service (ZeroGPU)") | |
| gr.Markdown("Antarmuka uji coba model **Hybrid Rules + Isolation Forest**. Endpoint REST API tersedia di `POST /api/v1/analyze_risk`.") | |
| with gr.Row(): | |
| with gr.Column(): | |
| p_key = gr.Textbox(label="ID Peserta (participant_key)", value="P-0951") | |
| n_ev_kunj = gr.Number(label="Total Kunjungan RJTL (n_event_kunjungan)", value=5) | |
| n_fas_kunj = gr.Number(label="Jumlah Faskes Kunjungan (n_faskes_kunjungan)", value=1) | |
| same_day = gr.Number(label="Same Day Hits", value=0) | |
| unbundling = gr.Number(label="Unbundling Hits", value=1) | |
| with gr.Column(): | |
| n_ev_obat = gr.Number(label="Frekuensi Tebus Obat (n_event_obat)", value=2) | |
| n_fas_obat = gr.Number(label="Jumlah Faskes Obat (n_faskes_obat)", value=2) | |
| qty_30d = gr.Number(label="Kuantitas Obat 30 Hari (qty_30d)", value=2) | |
| min_int = gr.Number(label="Interval Terpendek Ambil Obat (min_interval)", value=20) | |
| overlap = gr.Number(label="Hari Overlap Obat (overlap_days)", value=5) | |
| rules_ext = gr.Number(label="Skor Rules Obat Tambahan", value=10) | |
| btn = gr.Button("🔍 Analisis Risiko Pasien", variant="primary") | |
| output_json = gr.JSON(label="Hasil Output DETEKS (JSON)") | |
| # Fungsi @spaces.GPU didaftarkan langsung ke komponen Gradio | |
| btn.click( | |
| fn=calculate_risk_fusion, | |
| inputs=[p_key, n_ev_kunj, n_fas_kunj, same_day, unbundling, n_ev_obat, n_fas_obat, qty_30d, min_int, overlap, rules_ext], | |
| outputs=output_json, | |
| api_name="analyze_risk" | |
| ) | |
| # --- 4. TAMBAHKAN CUSTOM FASTAPI ENDPOINT KE SERVER GRADIO --- | |
| class PatientFeatures(BaseModel): | |
| participant_key: str = "P-0001" | |
| n_event_kunjungan: float = 2.0 | |
| n_faskes_kunjungan: float = 1.0 | |
| same_day_hits: float = 0.0 | |
| unbundling_hits: float = 0.0 | |
| n_event_obat: float = 2.0 | |
| n_faskes_obat: float = 1.0 | |
| qty_30d: float = 2.0 | |
| min_interval: float = 27.0 | |
| overlap_days: float = 4.0 | |
| rules_score_tambahan: float = 0.0 | |
| # Mengaktifkan CORS pada aplikasi FastAPI internal milik Gradio | |
| demo.app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| def analyze_risk_api(patient: PatientFeatures): | |
| try: | |
| return calculate_risk_fusion( | |
| participant_key=patient.participant_key, | |
| n_event_kunjungan=patient.n_event_kunjungan, | |
| n_faskes_kunjungan=patient.n_faskes_kunjungan, | |
| same_day_hits=patient.same_day_hits, | |
| unbundling_hits=patient.unbundling_hits, | |
| n_event_obat=patient.n_event_obat, | |
| n_faskes_obat=patient.n_faskes_obat, | |
| qty_30d=patient.qty_30d, | |
| min_interval=patient.min_interval, | |
| overlap_days=patient.overlap_days, | |
| rules_score_tambahan=patient.rules_score_tambahan | |
| ) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # --- 5. JALANKAN DENGAN DEMO.LAUNCH() --- | |
| if __name__ == "__main__": | |
| demo.launch() |