RicAIEng's picture
Initial deploy: Disaster Detector AI
2c62cbf
Raw
History Blame Contribute Delete
39.2 kB
# =========================================================
# IMPORT LIBRARY
# =========================================================
import gradio as gr
import torch
import torch.nn as nn
from torchvision import models, transforms
from PIL import Image
import cv2
import json
import numpy as np
from collections import Counter
# =========================================================
# CONFIGURATION & LABELS
# =========================================================
try:
with open('class_labels.json', 'r') as f:
class_names = json.load(f)
except FileNotFoundError:
class_names = [
"Water_Disaster", "Infrastructure", "Earthquake",
"Human_Damage", "Urban_Fire", "Wild_Fire",
"Land_Slide", "Drought",
"Safe_1", "Safe_2", "Safe_3", "Safe_4"
]
DANGEROUS_CLASSES = [
"Water_Disaster",
"Infrastructure",
"Earthquake",
"Human_Damage",
"Urban_Fire",
"Wild_Fire",
"Land_Slide",
"Drought"
]
CLASS_ICONS = {
"Water_Disaster": "🌊",
"Infrastructure": "πŸ—οΈ",
"Earthquake": "⚑",
"Human_Damage": "🚨",
"Urban_Fire": "πŸ”₯",
"Wild_Fire": "πŸŒ²β€πŸ”₯",
"Land_Slide": "⛰️",
"Drought": "β˜€οΈ",
"Safe_1": "βœ…",
"Safe_2": "βœ…",
"Safe_3": "βœ…",
"Safe_4": "βœ…",
}
# =========================================================
# LOAD MODEL
# =========================================================
def init_resnet50():
model = models.resnet50(weights=None)
num_ftrs = model.fc.in_features
model.fc = nn.Sequential(
nn.Linear(num_ftrs, 512),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(512, 12)
)
try:
model.load_state_dict(
torch.load('best_model_resnet50.pth', map_location='cpu')
)
except Exception as e:
print(f"Peringatan Load ResNet50: {e}")
model.eval()
return model
def init_mobilenetv2():
model = models.mobilenet_v2(weights=None)
model.classifier[1] = nn.Linear(model.last_channel, 12)
try:
model.load_state_dict(
torch.load('best_model_mobilenetv2.pth', map_location='cpu')
)
except Exception as e:
print(f"Peringatan Load MobileNetV2: {e}")
model.eval()
return model
models_dict = {
"ResNet50": init_resnet50(),
"MobileNetV2": init_mobilenetv2()
}
# =========================================================
# IMAGE PREPROCESSING
# =========================================================
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(
[0.485, 0.456, 0.406],
[0.229, 0.224, 0.225]
)
])
# =========================================================
# RESULT CARD
# =========================================================
def build_result_card(model_name, label, confidence, dangerous):
icon = CLASS_ICONS.get(label.split(" ")[0], "πŸ”")
if dangerous:
status_text = "BERBAHAYA / SENSITIF"
status_icon = "⚠️"
accent = "#ef4444"
accent_light = "#fca5a5"
soft_bg = "rgba(239,68,68,0.08)"
border_color = "rgba(239,68,68,0.25)"
badge_bg = "rgba(239,68,68,0.15)"
desc = "Konten terindikasi mengandung unsur bencana alam atau kerusakan fisik serius. Diperlukan tindakan segera."
glow = "rgba(239,68,68,0.2)"
else:
status_text = "AMAN"
status_icon = "βœ…"
accent = "#10b981"
accent_light = "#6ee7b7"
soft_bg = "rgba(16,185,129,0.08)"
border_color = "rgba(16,185,129,0.25)"
badge_bg = "rgba(16,185,129,0.15)"
desc = "Konten visual terdeteksi aman dan tidak menunjukkan indikasi bencana maupun konten sensitif."
glow = "rgba(16,185,129,0.2)"
bar_width = min(confidence, 100)
bar_color = f"linear-gradient(90deg, {accent}, {accent_light})"
html = f"""
<div style="
background: linear-gradient(145deg, #1e293b 0%, #0f172a 100%);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 24px;
padding: 32px;
font-family: 'DM Sans', 'Segoe UI', sans-serif;
box-shadow: 0 0 40px {glow}, 0 20px 60px rgba(0,0,0,0.4);
position: relative;
overflow: hidden;
">
<!-- Decorative corner accent -->
<div style="
position:absolute; top:0; right:0;
width:160px; height:160px;
background: radial-gradient(circle at top right, {glow} 0%, transparent 70%);
pointer-events:none;
"></div>
<!-- Top row: icon + label + model badge -->
<div style="
display:flex;
align-items:flex-start;
justify-content:space-between;
margin-bottom:28px;
gap:16px;
">
<div style="display:flex; align-items:center; gap:16px;">
<div style="
width:60px; height:60px;
background:{soft_bg};
border:1px solid {border_color};
border-radius:16px;
display:flex; align-items:center; justify-content:center;
font-size:28px;
flex-shrink:0;
">
{icon}
</div>
<div>
<p style="
margin:0 0 6px 0;
color:#64748b;
font-size:11px;
font-weight:700;
letter-spacing:2px;
text-transform:uppercase;
">Hasil Klasifikasi</p>
<h2 style="
margin:0;
color:#f1f5f9;
font-size:26px;
font-weight:800;
letter-spacing:-0.5px;
line-height:1.1;
">{label}</h2>
</div>
</div>
<div style="
background:{badge_bg};
color:{accent};
border:1px solid {border_color};
padding:8px 16px;
border-radius:999px;
font-size:12px;
font-weight:800;
letter-spacing:0.5px;
white-space:nowrap;
flex-shrink:0;
">
{model_name}
</div>
</div>
<!-- Confidence bar -->
<div style="
background:rgba(255,255,255,0.04);
border:1px solid rgba(255,255,255,0.06);
border-radius:16px;
padding:20px;
margin-bottom:20px;
">
<div style="
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:14px;
">
<span style="color:#94a3b8; font-size:13px; font-weight:600; letter-spacing:0.5px;">
Tingkat Keyakinan Model
</span>
<span style="
color:{accent};
font-size:22px;
font-weight:900;
letter-spacing:-1px;
">
{confidence:.1f}%
</span>
</div>
<div style="
width:100%;
height:8px;
background:rgba(255,255,255,0.06);
border-radius:999px;
overflow:hidden;
">
<div style="
width:{bar_width}%;
height:100%;
background:{bar_color};
border-radius:999px;
transition:width 0.8s ease;
"></div>
</div>
<div style="
display:flex; justify-content:space-between;
margin-top:8px;
">
<span style="color:#334155; font-size:11px;">0%</span>
<span style="color:#334155; font-size:11px;">100%</span>
</div>
</div>
<!-- Status banner -->
<div style="
background:{soft_bg};
border:1px solid {border_color};
border-radius:16px;
padding:20px 24px;
display:flex;
align-items:flex-start;
gap:16px;
">
<div style="
font-size:28px;
flex-shrink:0;
line-height:1;
margin-top:2px;
">{status_icon}</div>
<div>
<p style="
margin:0 0 6px 0;
color:{accent};
font-size:14px;
font-weight:900;
letter-spacing:1.5px;
text-transform:uppercase;
">STATUS : {status_text}</p>
<p style="
margin:0;
color:#94a3b8;
font-size:13px;
line-height:1.7;
">{desc}</p>
</div>
</div>
</div>
"""
return html
# =========================================================
# IMAGE PREDICTION
# =========================================================
def predict_image(image, model_name):
if image is None:
return _empty_state("Silakan unggah gambar terlebih dahulu.")
if isinstance(image, np.ndarray):
image = Image.fromarray(image.astype('uint8'), 'RGB')
model = models_dict[model_name]
img_tensor = transform(image).unsqueeze(0)
with torch.no_grad():
outputs = model(img_tensor)
probabilities = torch.nn.functional.softmax(outputs, dim=1)[0] * 100
_, predicted = outputs.max(1)
label = class_names[predicted.item()]
confidence = probabilities[predicted.item()].item()
dangerous = label in DANGEROUS_CLASSES
return build_result_card(model_name, label, confidence, dangerous)
# =========================================================
# VIDEO PREDICTION
# =========================================================
def predict_video(video_path, model_name, progress=gr.Progress()):
if video_path is None:
return _empty_state("Silakan unggah video terlebih dahulu.")
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
return _empty_state("Gagal membuka file video.")
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap.get(cv2.CAP_PROP_FPS)
frame_interval = max(1, int(fps))
frame_count = 0
predictions = []
model = models_dict[model_name]
progress(0, desc="Menganalisis frame video...")
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
if frame_count % frame_interval == 0:
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_img = Image.fromarray(frame_rgb)
img_tensor = transform(pil_img).unsqueeze(0)
with torch.no_grad():
outputs = model(img_tensor)
probabilities = torch.nn.functional.softmax(outputs, dim=1)[0] * 100
_, predicted = outputs.max(1)
label = class_names[predicted.item()]
confidence = probabilities[predicted.item()].item()
predictions.append((label, confidence))
frame_count += 1
if total_frames > 0:
progress(min(0.99, frame_count / total_frames))
cap.release()
if not predictions:
return _empty_state("Tidak ada frame valid untuk dianalisis.")
labels_only = [p[0] for p in predictions]
counter = Counter(labels_only)
most_common_label, occurrence = counter.most_common(1)[0]
avg_confidence = np.mean([p[1] for p in predictions if p[0] == most_common_label])
dangerous = most_common_label in DANGEROUS_CLASSES
return build_result_card(
model_name,
f"{most_common_label} ({occurrence} frame)",
avg_confidence,
dangerous
)
def _empty_state(msg):
return f"""
<div style="
min-height:280px;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
background:linear-gradient(145deg,#1e293b,#0f172a);
border:1px solid rgba(255,255,255,0.06);
border-radius:24px;
color:#475569;
font-family:'DM Sans','Segoe UI',sans-serif;
gap:12px;
">
<div style="font-size:36px; opacity:0.4;">πŸ”</div>
<p style="margin:0; font-size:14px; font-weight:600;">{msg}</p>
</div>
"""
# =========================================================
# THEME
# =========================================================
ai_theme = gr.themes.Base(
primary_hue="emerald",
secondary_hue="teal",
neutral_hue="slate",
font=[gr.themes.GoogleFont("DM Sans"), "Segoe UI", "sans-serif"],
font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"],
).set(
body_background_fill="#0b1120",
body_background_fill_dark="#0b1120",
body_text_color="#e2e8f0",
body_text_color_dark="#e2e8f0",
block_background_fill="#111827",
block_background_fill_dark="#111827",
block_border_color="#1e293b",
block_border_color_dark="#1e293b",
block_label_text_color="#94a3b8",
block_label_text_color_dark="#94a3b8",
block_radius="20px",
button_primary_background_fill="linear-gradient(135deg,#10b981,#059669)",
button_primary_background_fill_hover="linear-gradient(135deg,#059669,#047857)",
button_primary_text_color="#ffffff",
button_primary_border_color="transparent",
button_large_radius="14px",
button_large_padding="14px 28px",
input_background_fill="#1e293b",
input_background_fill_dark="#1e293b",
input_border_color="#334155",
input_border_color_dark="#334155",
input_placeholder_color="#475569",
checkbox_label_background_fill_selected="#10b981",
slider_color="#10b981",
table_even_background_fill="#1e293b",
table_odd_background_fill="#111827",
)
# =========================================================
# CSS β€” Minimal overrides to avoid breaking Gradio internals
# =========================================================
custom_css = """
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,600;0,9..40,700;0,9..40,800;0,9..40,900;1,9..40,400&family=JetBrains+Mono:wght@400;700&display=swap');
/* ── Global ─────────────────────────────────────────── */
footer { visibility: hidden !important; }
body, .gradio-container {
background: #0b1120 !important;
font-family: 'DM Sans', 'Segoe UI', sans-serif !important;
min-height: 100vh;
}
/* Subtle animated grid background */
.gradio-container::before {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(16,185,129,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(16,185,129,0.03) 1px, transparent 1px);
background-size: 48px 48px;
pointer-events: none;
z-index: 0;
}
/* ── Main wrapper ────────────────────────────────────── */
.main-box {
max-width: 1080px;
margin: 0 auto;
padding: 24px 20px 48px;
position: relative;
z-index: 1;
}
/* ── Block cards ─────────────────────────────────────── */
.block.gradio-group,
.block.gradio-column,
.form {
background: rgba(17,24,39,0.85) !important;
backdrop-filter: blur(20px) !important;
border: 1px solid rgba(255,255,255,0.06) !important;
border-radius: 20px !important;
box-shadow: 0 8px 32px rgba(0,0,0,0.3) !important;
}
/* ── Labels ──────────────────────────────────────────── */
label span,
.block label > span {
color: #94a3b8 !important;
font-size: 12px !important;
font-weight: 700 !important;
letter-spacing: 1.5px !important;
text-transform: uppercase !important;
}
/* ── Inputs (text, selects) ──────────────────────────── */
input[type="text"],
textarea {
background: #1e293b !important;
color: #e2e8f0 !important;
border: 1px solid #334155 !important;
border-radius: 12px !important;
font-family: 'DM Sans', sans-serif !important;
}
input[type="text"]:focus,
textarea:focus {
border-color: #10b981 !important;
box-shadow: 0 0 0 3px rgba(16,185,129,0.15) !important;
}
/* ── Model Toggle Buttons (Radio styled as pill buttons) ─ */
#model-toggle .wrap {
display: flex !important;
flex-direction: row !important;
gap: 12px !important;
background: transparent !important;
border: none !important;
padding: 0 !important;
}
#model-toggle label {
flex: 1 !important;
cursor: pointer !important;
margin: 0 !important;
}
/* Hide the actual radio circle */
#model-toggle input[type="radio"] {
display: none !important;
}
/* Style each label as a card button */
#model-toggle .wrap label > span {
display: flex !important;
align-items: center !important;
justify-content: center !important;
gap: 10px !important;
width: 100% !important;
padding: 16px 20px !important;
background: rgba(30,41,59,0.8) !important;
border: 1px solid rgba(255,255,255,0.08) !important;
border-radius: 16px !important;
color: #64748b !important;
font-size: 15px !important;
font-weight: 700 !important;
font-family: 'DM Sans', sans-serif !important;
letter-spacing: 0.3px !important;
text-transform: none !important;
transition: all 0.2s ease !important;
cursor: pointer !important;
}
#model-toggle .wrap label > span:hover {
background: rgba(16,185,129,0.08) !important;
border-color: rgba(16,185,129,0.3) !important;
color: #10b981 !important;
transform: translateY(-1px) !important;
}
/* Selected state */
#model-toggle .wrap label:has(input:checked) > span {
background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important;
border-color: transparent !important;
color: #ffffff !important;
box-shadow: 0 6px 20px rgba(16,185,129,0.35) !important;
transform: translateY(-1px) !important;
}
/* ── Tabs ────────────────────────────────────────────── */
.tabs > .tab-nav {
background: transparent !important;
border: none !important;
gap: 10px !important;
padding: 0 !important;
margin-bottom: 16px !important;
}
.tabs > .tab-nav > button {
background: rgba(30,41,59,0.7) !important;
border: 1px solid rgba(255,255,255,0.07) !important;
color: #64748b !important;
border-radius: 14px !important;
font-weight: 700 !important;
font-size: 14px !important;
padding: 12px 24px !important;
transition: all 0.2s ease !important;
font-family: 'DM Sans', sans-serif !important;
letter-spacing: 0.3px !important;
}
.tabs > .tab-nav > button:hover {
background: rgba(16,185,129,0.08) !important;
color: #10b981 !important;
border-color: rgba(16,185,129,0.25) !important;
}
.tabs > .tab-nav > button.selected {
background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important;
color: #ffffff !important;
border-color: transparent !important;
box-shadow: 0 6px 20px rgba(16,185,129,0.35) !important;
}
/* ── Primary button ──────────────────────────────────── */
button.lg.primary {
background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important;
color: #fff !important;
font-weight: 800 !important;
font-size: 15px !important;
border: none !important;
border-radius: 14px !important;
padding: 14px 28px !important;
letter-spacing: 0.3px !important;
transition: all 0.2s ease !important;
box-shadow: 0 6px 20px rgba(16,185,129,0.3) !important;
width: 100% !important;
font-family: 'DM Sans', sans-serif !important;
}
button.lg.primary:hover {
transform: translateY(-2px) !important;
box-shadow: 0 10px 28px rgba(16,185,129,0.45) !important;
}
button.lg.primary:active {
transform: translateY(0) !important;
}
/* ── Upload / Image / Video areas ───────────────────── */
.image-container img,
.image-preview img {
border-radius: 14px !important;
object-fit: contain !important;
}
.upload-container,
.empty.svelte-1occ011,
.wrap.svelte-1occ011 {
background: rgba(30,41,59,0.5) !important;
border: 2px dashed rgba(16,185,129,0.25) !important;
border-radius: 18px !important;
color: #475569 !important;
transition: border-color 0.2s ease !important;
}
.upload-container:hover,
.wrap.svelte-1occ011:hover {
border-color: rgba(16,185,129,0.5) !important;
}
/* ── Progress bar ────────────────────────────────────── */
.progress-bar,
progress {
accent-color: #10b981 !important;
}
/* ── Scrollbar ───────────────────────────────────────── */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: #0f172a; }
::-webkit-scrollbar-thumb {
background: #334155;
border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover { background: #10b981; }
/* ── Remove extra Gradio padding inside tabs ─────────── */
.tabitem > .gradio-group {
margin: 0 !important;
}
"""
# =========================================================
# INTERFACE
# =========================================================
with gr.Blocks(
theme=ai_theme,
title="Disaster Detector AI",
css=custom_css
) as demo:
with gr.Column(elem_classes="main-box"):
# ─── HEADER ──────────────────────────────────────
gr.HTML("""
<div style="
text-align: center;
padding: 40px 20px 36px;
position: relative;
">
<!-- Glow orb -->
<div style="
position: absolute;
top: 0; left: 50%; transform: translateX(-50%);
width: 400px; height: 200px;
background: radial-gradient(ellipse, rgba(16,185,129,0.12) 0%, transparent 70%);
pointer-events: none;
"></div>
<!-- Top badge -->
<div style="
display: inline-flex;
align-items: center;
gap: 8px;
padding: 7px 18px;
background: rgba(16,185,129,0.1);
border: 1px solid rgba(16,185,129,0.3);
border-radius: 999px;
color: #34d399;
font-size: 12px;
font-weight: 800;
letter-spacing: 2px;
text-transform: uppercase;
margin-bottom: 22px;
">
<span style="
width:7px; height:7px;
background:#10b981;
border-radius:50%;
display:inline-block;
box-shadow:0 0 8px #10b981;
animation: pulse-dot 2s infinite;
"></span>
AI Computer Vision System Β· UDINUS
</div>
<!-- Title -->
<h1 style="
font-family: 'DM Sans', 'Segoe UI', sans-serif;
font-size: 48px;
font-weight: 900;
margin: 0 0 8px 0;
color: #f1f5f9;
letter-spacing: -2px;
line-height: 1.1;
">
Disaster &amp; Sensitive
<span style="
background: linear-gradient(135deg, #10b981 0%, #34d399 50%, #6ee7b7 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
">Content Detector</span>
</h1>
<!-- Subtitle -->
<p style="
margin: 16px auto 0;
color: #64748b;
font-size: 16px;
line-height: 1.8;
max-width: 680px;
font-weight: 400;
">
Sistem analisis citra berbasis <strong style='color:#94a3b8;'>Deep Learning</strong>
untuk mendeteksi <strong style='color:#94a3b8;'>12 kategori</strong> visual bencana alam,
kerusakan infrastruktur, dan konten sensitif secara otomatis.
</p>
<!-- Divider -->
<div style="
width: 64px;
height: 3px;
background: linear-gradient(90deg, #10b981, #059669);
border-radius: 999px;
margin: 28px auto 0;
box-shadow: 0 0 12px rgba(16,185,129,0.4);
"></div>
</div>
<style>
@keyframes pulse-dot {
0%,100% { opacity:1; box-shadow:0 0 8px #10b981; }
50% { opacity:0.5; box-shadow:0 0 14px #10b981; }
}
</style>
""")
# ─── STATS STRIP ─────────────────────────────────
gr.HTML("""
<div style="
display: flex;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 28px;
padding: 0 4px;
">
<div style="
background: rgba(17,24,39,0.8);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 14px;
padding: 14px 24px;
display: flex;
align-items: center;
gap: 12px;
">
<span style="font-size:22px;">🏷️</span>
<div>
<p style="margin:0; color:#10b981; font-size:18px; font-weight:900; line-height:1;">12</p>
<p style="margin:0; color:#475569; font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:1px;">Kategori</p>
</div>
</div>
<div style="
background: rgba(17,24,39,0.8);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 14px;
padding: 14px 24px;
display: flex;
align-items: center;
gap: 12px;
">
<span style="font-size:22px;">🧠</span>
<div>
<p style="margin:0; color:#10b981; font-size:18px; font-weight:900; line-height:1;">2</p>
<p style="margin:0; color:#475569; font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:1px;">Arsitektur</p>
</div>
</div>
<div style="
background: rgba(17,24,39,0.8);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 14px;
padding: 14px 24px;
display: flex;
align-items: center;
gap: 12px;
">
<span style="font-size:22px;">πŸ“Έ</span>
<div>
<p style="margin:0; color:#10b981; font-size:18px; font-weight:900; line-height:1;">224px</p>
<p style="margin:0; color:#475569; font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:1px;">Input Size</p>
</div>
</div>
<div style="
background: rgba(17,24,39,0.8);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 14px;
padding: 14px 24px;
display: flex;
align-items: center;
gap: 12px;
">
<span style="font-size:22px;">πŸŽ“</span>
<div>
<p style="margin:0; color:#10b981; font-size:18px; font-weight:900; line-height:1;">UDINUS</p>
<p style="margin:0; color:#475569; font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:1px;">Final Project</p>
</div>
</div>
</div>
""")
# ─── MODEL SELECTOR (Toggle Buttons) ─────────────
gr.HTML("""
<p style="
color:#64748b;
font-size:11px;
font-weight:700;
letter-spacing:2px;
text-transform:uppercase;
margin:0 0 10px 2px;
font-family:'DM Sans',sans-serif;
">🧠 &nbsp;Arsitektur Deep Learning</p>
""")
model_selector = gr.Radio(
show_label=False,
choices=["ResNet50", "MobileNetV2"],
value="ResNet50",
label="",
interactive=True,
elem_id="model-toggle",
)
# ─── TABS ─────────────────────────────────────────
with gr.Tabs(elem_classes="tabs"):
# ── IMAGE TAB ──────────────────────────────────
with gr.TabItem("πŸ–ΌοΈ Pengujian Gambar"):
with gr.Row(equal_height=True):
with gr.Column(scale=1, min_width=300):
image_input = gr.Image(
type="numpy",
label="Unggah Gambar",
height=280,
)
btn_image = gr.Button(
"πŸ” Analisis Gambar",
variant="primary",
size="lg",
)
with gr.Column(scale=1, min_width=300):
image_result = gr.HTML(
value=_empty_state("Unggah gambar, lalu klik Analisis.")
)
btn_image.click(
fn=predict_image,
inputs=[image_input, model_selector],
outputs=image_result,
)
# ── VIDEO TAB ──────────────────────────────────
with gr.TabItem("πŸŽ₯ Pengujian Video"):
with gr.Row(equal_height=True):
with gr.Column(scale=1, min_width=300):
video_input = gr.Video(
label="Unggah Video",
height=280,
)
btn_video = gr.Button(
"⚑ Analisis Video",
variant="primary",
size="lg",
)
with gr.Column(scale=1, min_width=300):
video_result = gr.HTML(
value=_empty_state("Unggah video, lalu klik Analisis.")
)
btn_video.click(
fn=predict_video,
inputs=[video_input, model_selector],
outputs=video_result,
)
# ─── CATEGORY GUIDE ──────────────────────────────
gr.HTML("""
<div style="
margin-top: 32px;
background: rgba(17,24,39,0.7);
border: 1px solid rgba(255,255,255,0.05);
border-radius: 20px;
padding: 28px;
font-family: 'DM Sans', sans-serif;
">
<p style="
margin: 0 0 20px 0;
color: #64748b;
font-size: 11px;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
">Panduan Kategori Klasifikasi</p>
<div style="display:flex; flex-wrap:wrap; gap:10px;">
<!-- Dangerous categories -->
<div style="
background:rgba(239,68,68,0.08);
border:1px solid rgba(239,68,68,0.2);
border-radius:10px;
padding:8px 14px;
display:flex; align-items:center; gap:8px;
">
<span style="font-size:16px;">🌊</span>
<span style="color:#fca5a5; font-size:13px; font-weight:600;">Water Disaster</span>
</div>
<div style="
background:rgba(239,68,68,0.08);
border:1px solid rgba(239,68,68,0.2);
border-radius:10px;
padding:8px 14px;
display:flex; align-items:center; gap:8px;
">
<span style="font-size:16px;">πŸ—οΈ</span>
<span style="color:#fca5a5; font-size:13px; font-weight:600;">Infrastructure</span>
</div>
<div style="
background:rgba(239,68,68,0.08);
border:1px solid rgba(239,68,68,0.2);
border-radius:10px;
padding:8px 14px;
display:flex; align-items:center; gap:8px;
">
<span style="font-size:16px;">⚑</span>
<span style="color:#fca5a5; font-size:13px; font-weight:600;">Earthquake</span>
</div>
<div style="
background:rgba(239,68,68,0.08);
border:1px solid rgba(239,68,68,0.2);
border-radius:10px;
padding:8px 14px;
display:flex; align-items:center; gap:8px;
">
<span style="font-size:16px;">🚨</span>
<span style="color:#fca5a5; font-size:13px; font-weight:600;">Human Damage</span>
</div>
<div style="
background:rgba(239,68,68,0.08);
border:1px solid rgba(239,68,68,0.2);
border-radius:10px;
padding:8px 14px;
display:flex; align-items:center; gap:8px;
">
<span style="font-size:16px;">πŸ”₯</span>
<span style="color:#fca5a5; font-size:13px; font-weight:600;">Urban Fire</span>
</div>
<div style="
background:rgba(239,68,68,0.08);
border:1px solid rgba(239,68,68,0.2);
border-radius:10px;
padding:8px 14px;
display:flex; align-items:center; gap:8px;
">
<span style="font-size:16px;">🌲</span>
<span style="color:#fca5a5; font-size:13px; font-weight:600;">Wild Fire</span>
</div>
<div style="
background:rgba(239,68,68,0.08);
border:1px solid rgba(239,68,68,0.2);
border-radius:10px;
padding:8px 14px;
display:flex; align-items:center; gap:8px;
">
<span style="font-size:16px;">⛰️</span>
<span style="color:#fca5a5; font-size:13px; font-weight:600;">Land Slide</span>
</div>
<div style="
background:rgba(239,68,68,0.08);
border:1px solid rgba(239,68,68,0.2);
border-radius:10px;
padding:8px 14px;
display:flex; align-items:center; gap:8px;
">
<span style="font-size:16px;">β˜€οΈ</span>
<span style="color:#fca5a5; font-size:13px; font-weight:600;">Drought</span>
</div>
<!-- Safe categories -->
<div style="
background:rgba(16,185,129,0.08);
border:1px solid rgba(16,185,129,0.2);
border-radius:10px;
padding:8px 14px;
display:flex; align-items:center; gap:8px;
">
<span style="font-size:16px;">βœ…</span>
<span style="color:#6ee7b7; font-size:13px; font-weight:600;">Safe (1–4)</span>
</div>
</div>
</div>
""")
# ─── FOOTER ──────────────────────────────────────
gr.HTML("""
<div style="
text-align: center;
margin-top: 40px;
padding: 28px;
border-top: 1px solid rgba(255,255,255,0.05);
font-family: 'DM Sans', sans-serif;
">
<div style="
display: inline-flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
">
<div style="
width: 32px; height: 32px;
background: linear-gradient(135deg,#10b981,#059669);
border-radius: 8px;
display:flex; align-items:center; justify-content:center;
font-size:16px;
">πŸŽ“</div>
<span style="color:#34d399; font-size:16px; font-weight:800; letter-spacing:-0.3px;">
Disaster Research Team
</span>
</div>
<p style="
margin: 0;
color: #334155;
font-size: 13px;
line-height: 1.8;
">
Final Project β€” Computer Vision &amp; Image Analysis<br>
Faculty of Computer Science Β·
<span style="color:#475569; font-weight:600;">
Universitas Dian Nuswantoro (UDINUS), Semarang
</span>
</p>
</div>
""")
# =========================================================
# RUN APP
# =========================================================
if __name__ == "__main__":
demo.queue().launch()