Spaces:
Sleeping
Sleeping
| # ========================================================= | |
| # 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 & 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; | |
| ">π§ 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 & 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() |