my_app_jun / app.py
jun12124's picture
Create app.py
287d8d3 verified
# ============================================================
# app.py โ€” ๊ทธ๋กœ์„ผ๋”• AI Day 3 (์ ์ง‘ ์ปจ์…‰ ยท ๋”ฐ๋œปํ•œ ๋ฒ„์ „)
# ์ž์—ฐ์–ด ํ”„๋กฌํ”„ํŠธ ๊ธฐ๋ฐ˜ ๋””์ž์ธ / ๋ฌด์„ญ์ง€ ์•Š๊ณ  ์นœ๊ทผํ•œ ์ ์ง‘
# ============================================================
import warnings
warnings.filterwarnings("ignore", message=".*HF_TOKEN.*")
import gradio as gr
import torch, torch.nn as nn, timm
from torchvision import models, transforms
from huggingface_hub import hf_hub_download
from PIL import Image
import numpy as np
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 1. 5๊ฐœ AI ๋ชจ๋ธ ๋กœ๋“œ (Space ์‹œ์ž‘ ์‹œ 1๋ฒˆ๋งŒ) โ€ป ๊ทธ๋Œ€๋กœ ์œ ์ง€
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
REPO_ID = "junkim1209/isic-models"
MODEL_NAMES = ['efficientnet_b3', 'efficientnet_b5',
'convnext_tiny', 'convnext_base', 'swin_base']
def build_model(name):
if name == 'efficientnet_b3':
m = models.efficientnet_b3(weights=None)
m.classifier[1] = nn.Linear(m.classifier[1].in_features, 1); return m, 300
elif name == 'efficientnet_b5':
return timm.create_model('efficientnet_b5', pretrained=False, num_classes=1), 456
elif name == 'convnext_tiny':
m = models.convnext_tiny(weights=None)
m.classifier[2] = nn.Linear(m.classifier[2].in_features, 1); return m, 224
elif name == 'convnext_base':
return timm.create_model('convnext_base', pretrained=False, num_classes=1), 224
elif name == 'swin_base':
return timm.create_model('swin_base_patch4_window12_384',
pretrained=False, num_classes=1), 384
print("๐Ÿ“ฅ ๋ฌด๋‹น 5๋ช… ๋ชจ์‹œ๋Š” ์ค‘...")
LOADED = {}
for name in MODEL_NAMES:
path = hf_hub_download(repo_id=REPO_ID, filename=f"{name}.pth")
model, size = build_model(name)
state = torch.load(path, map_location='cpu', weights_only=False)
model.load_state_dict(state['model_state_dict'], strict=True)
model.eval()
LOADED[name] = {'model': model, 'size': size}
print("โœ… ์ ์ง‘ ๊ฐœ์‹œ ์ค€๋น„ ์™„๋ฃŒ")
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 2. ๋ณธ์ธ 1์ผ์ฐจ ๊ฐ€์ค‘์น˜
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
MY_WEIGHTS = {
'efficientnet_b3': 1.0, 'efficientnet_b5': 1.0,
'convnext_tiny': 1.0, 'convnext_base': 1.0,
'swin_base': 1.0,
}
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 3. ์ถ”๋ก  + ๋ฌธ์ง„ ๊ฒฐํ•ฉ โ€ป predict ๋ณธ์ฒด๋Š” ์œ ์ง€ํ•˜๊ณ  ํ•จ์ˆ˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋งŒ ํ™•์žฅ
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def predict_with_questionnaire(image, age, family_history, mole_change):
if image is None:
return None, ""
img = Image.fromarray(image).convert('RGB')
probs = {}
for name, info in LOADED.items():
tfm = transforms.Compose([
transforms.Resize((info['size'], info['size'])),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])
x = tfm(img).unsqueeze(0)
with torch.no_grad():
probs[name] = torch.sigmoid(info['model'](x)).item()
total = sum(MY_WEIGHTS.values())
ai_prob = sum(probs[k] * w / total for k, w in MY_WEIGHTS.items())
bonus = 0.0
if family_history == "์˜ˆ": bonus += 0.10
if mole_change == "์˜ˆ": bonus += 0.15
if age >= 50: bonus += 0.05
final = min(ai_prob + bonus, 1.0)
label_data = {
"ํ‰(ๅ‡ถ) โ€” ๋ฉœ๋ผ๋…ธ๋งˆ ์˜์‹ฌ": float(final),
"๊ธธ(ๅ‰) โ€” ์–‘์„ฑ ๋ชจ๋ฐ˜": float(1 - final),
}
# ์œ„ํ—˜๋„๋ณ„ ์ ๊ด˜ ํ’€์ด (๋”ฐ๋œปํ•œ ํ†ค)
if final < 0.20:
tag = "๊ธธ์กฐ (ๅ‰ๅ…†)"
msg = "์ด ์ ์€ ํ‰์˜จํ•œ ์ ์ด๋ผ๋„ค.<br>์ผ๋ฐ˜์ ์ธ ๋ชจ๋ฐ˜์œผ๋กœ ๋ณด์ด์˜ค. ํ‰์†Œ์ฒ˜๋Ÿผ ์ž๊ฐ€ ๊ด€์ฐฐ์„ ์ด์–ด๊ฐ€์‹œ์˜ค."
tag_color = "#3F8B5F" # ๋ถ€๋“œ๋Ÿฌ์šด ์ดˆ๋ก
tag_bg = "#EDF6EF"
elif final < 0.50:
tag = "์ฃผ์˜ (ๆณจๆ„)"
msg = "์กฐ๊ธˆ ์‹ ๊ฒฝ ์“ฐ์ด๋Š” ๊ธฐ์šด์ด ์žˆ๊ตฌ๋ ค.<br>๊ฐ€๊นŒ์šด ํ”ผ๋ถ€๊ณผ์—์„œ ํ•œ๋ฒˆ ๋ด๋‹ฌ๋ผ ํ•˜์‹œ์˜ค."
tag_color = "#C9722E" # ๋”ฐ๋œปํ•œ ์ฃผํ™ฉ
tag_bg = "#FCF1E0"
else:
tag = "ํ‰์กฐ (ๅ‡ถๅ…†)"
msg = "์ด ์ ์€ ์ข€ ์‚ดํŽด๋ด์•ผ ํ•  ๊ฒƒ ๊ฐ™์†Œ.<br>๊ฐ€๋Šฅํ•œ ๋นจ๋ฆฌ ํ”ผ๋ถ€๊ณผ ์ „๋ฌธ์˜์—๊ฒŒ ์ง„๋ฃŒ๋ฅผ ๋ฐ›์œผ์‹œ์˜ค."
tag_color = "#B5394A" # ๋ฌด์„ญ์ง€ ์•Š์€ ์ง™์€ ์ฃผํ™
tag_bg = "#FBEAEC"
detail_html = f"""
<div style='background:{tag_bg}; border-left:5px solid {tag_color};
padding: 16px 18px; border-radius: 12px; margin-top: 14px;'>
<div style='color:{tag_color}; font-weight:800; font-size:16px;
margin-bottom:8px; letter-spacing:1.5px;'>
๐Ÿ”ฎ {tag}
</div>
<div style='color:#4A3826; font-size:13.5px; line-height:1.65;'>{msg}</div>
</div>
<div style='background:#FFFBF0; padding:14px 16px; border-radius:12px;
margin-top:12px; font-size:12.5px; color:#6B4423;
border: 1px dashed #D4B873;'>
<div style='font-weight:700; margin-bottom:8px; color:#A8462C;
letter-spacing: 1px;'>๐Ÿ“œ ์ ๊ด˜ ํ’€์ด</div>
AI ๋ฌด๋‹น์˜ ์ง๊ด€: <strong>{ai_prob*100:.1f}%</strong><br>
์šด์ˆ˜(๋ฌธ์ง„) ๊ฐ€์‚ฐ: <strong>+{bonus*100:.1f}%</strong><br>
โ”€ ์ตœ์ข… ํ‰์กฐ ์ •๋„: <strong style='color:{tag_color}; font-size:14px;'>{final*100:.1f}%</strong>
</div>
"""
return label_data, detail_html
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 4. ์ ์ง‘ CSS โ€” ๋”ฐ๋œปํ•œ ํ•œ์ง€ยท์ฃผํ™ ํ†ค
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
custom_css = """
:root {
--brand: #C24536; /* ๋”ฐ๋œปํ•œ ์ฃผํ™ (ํ…Œ๋ผ์ฝ”ํƒ€) */
--brand-dark: #9E3528;
--brand-soft: #E8533D; /* ๋” ๋ฐ์€ ์ฃผํ™ (ํฌ์ธํŠธ) */
--gold: #D4A93B; /* ๋ถ€๋“œ๋Ÿฌ์šด ๊ธˆ์ƒ‰ */
--gold-light: #E8C868;
--paper: #FFF8E7; /* ํ•œ์ง€์ƒ‰ */
--paper-warm: #FCEFD3; /* ๋” ์ง„ํ•œ ํ•œ์ง€ */
--ink: #4A3826;
--ink-sub: #7A5C3D;
}
.gradio-container {
max-width: 430px !important;
margin: 0 auto !important;
padding: 0 !important;
background: var(--paper) !important;
min-height: 100vh !important;
font-family: 'Pretendard', -apple-system, BlinkMacSystemFont,
'Noto Sans KR', sans-serif !important;
background-image:
radial-gradient(circle at 15% 8%, rgba(212,169,59,0.12) 0%, transparent 45%),
radial-gradient(circle at 85% 92%, rgba(232,83,61,0.08) 0%, transparent 45%);
}
footer { display: none !important; }
/* ํ•œ์ง€ ์œ„ ๋„์žฅ ๋А๋‚Œ ํ—ค๋” */
.scr-header {
background: linear-gradient(135deg, #C24536 0%, #9E3528 100%);
color: white;
padding: 22px 22px 20px;
margin-bottom: 14px;
position: relative;
border-radius: 0 0 16px 16px;
}
.scr-header::after {
content: "";
position: absolute; left: 18px; right: 18px; bottom: -2px; height: 2px;
background: repeating-linear-gradient(
90deg, var(--gold) 0, var(--gold) 8px,
transparent 8px, transparent 14px
);
}
.scr-header .scr-title {
font-size: 22px;
font-weight: 800;
letter-spacing: 1.5px;
font-family: 'Noto Serif KR', 'Nanum Myeongjo', serif;
}
.scr-header .scr-sub {
font-size: 12.5px;
opacity: 0.94;
margin-top: 5px;
letter-spacing: 0.3px;
}
/* ์ง„ํ–‰ ํ‘œ์‹œ */
.progress-dots {
display: flex;
gap: 7px;
margin: 6px 18px 14px;
justify-content: center;
}
.progress-dot {
width: 28px;
height: 5px;
border-radius: 3px;
background: #ECDBB0;
}
.progress-dot.active {
background: var(--gold);
box-shadow: 0 0 8px rgba(212,169,59,0.5);
}
/* ํ•œ์ง€ ์นด๋“œ */
.app-card {
background: white;
border-radius: 14px;
padding: 16px 18px;
margin: 0 18px 12px;
border: 1px solid #EEDFB8;
box-shadow: 0 2px 8px rgba(194,69,54,0.06);
}
.app-card-title {
font-size: 14.5px;
font-weight: 800;
color: var(--brand);
margin-bottom: 6px;
letter-spacing: 0.5px;
font-family: 'Noto Serif KR', serif;
}
.app-card-desc {
font-size: 12.8px;
color: var(--ink-sub);
line-height: 1.7;
}
/* ๋””์Šคํด๋ ˆ์ด๋จธ โ€” ๋ถ€์  ๋А๋‚Œ */
.disclaimer {
background: #FFF3DE;
color: #8B5A2B;
padding: 11px 14px;
border-radius: 10px;
margin: 0 18px 12px;
font-size: 12px;
font-weight: 600;
text-align: center;
border: 1.5px dashed #D4A93B;
}
/* ํ™ˆ ํžˆ์–ด๋กœ */
.jeomjip-hero {
text-align: center;
padding: 12px 24px 12px;
}
.mudang-svg {
width: 240px;
max-width: 100%;
height: auto;
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.08));
}
.hero-title {
font-size: 30px;
font-weight: 900;
color: var(--brand);
margin: 14px 0 8px;
letter-spacing: 3px;
font-family: 'Noto Serif KR', 'Nanum Myeongjo', serif;
}
.hero-sub {
font-size: 14px;
color: var(--ink-sub);
line-height: 1.75;
margin-bottom: 4px;
}
.hero-quote {
color: var(--brand);
font-weight: 700;
font-style: italic;
display: inline-block;
margin-top: 8px;
}
/* ๋„์žฅ ๋А๋‚Œ ์ฃผ์š” ๋ฒ„ํŠผ */
button.primary-btn {
background: var(--brand) !important;
color: white !important;
border: none !important;
padding: 15px !important;
font-size: 16px !important;
font-weight: 800 !important;
border-radius: 14px !important;
min-height: 56px !important;
margin: 0 18px 10px !important;
width: calc(100% - 36px) !important;
letter-spacing: 2.5px !important;
box-shadow: 0 4px 0 var(--brand-dark),
0 6px 12px rgba(194,69,54,0.18) !important;
font-family: 'Noto Serif KR', serif !important;
transition: all 0.15s !important;
}
button.primary-btn:hover {
background: var(--brand-soft) !important;
transform: translateY(2px);
box-shadow: 0 2px 0 var(--brand-dark),
0 3px 6px rgba(194,69,54,0.18) !important;
}
/* ์ข…์ด ๋А๋‚Œ ๋ณด์กฐ ๋ฒ„ํŠผ */
button.secondary-btn {
background: white !important;
color: var(--brand) !important;
border: 2px solid var(--brand) !important;
padding: 13px !important;
font-size: 14.5px !important;
font-weight: 800 !important;
border-radius: 14px !important;
min-height: 50px !important;
margin: 0 18px 10px !important;
width: calc(100% - 36px) !important;
letter-spacing: 1.5px !important;
font-family: 'Noto Serif KR', serif !important;
}
button.secondary-btn:hover {
background: #FFF3DE !important;
}
.row-buttons button { width: 100% !important; margin: 0 !important; }
.row-buttons { padding: 0 18px 10px; gap: 10px !important; }
label, .label, .gradio-container label { color: var(--ink) !important; }
"""
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 5. ์นœ๊ทผํ•œ ๋ฌด๋‹น SVG โ€” ๋ฏธ์†Œ ๋ค ํ‘œ์ •
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
MUDANG_SVG = """
<svg class="mudang-svg" viewBox="0 0 260 230" xmlns="http://www.w3.org/2000/svg">
<!-- ๋ถ€์  ์  (๋ฐฐ๊ฒฝ ์žฅ์‹) -->
<circle cx="30" cy="40" r="3" fill="#D4A93B" opacity="0.4"/>
<circle cx="240" cy="60" r="2.5" fill="#D4A93B" opacity="0.4"/>
<circle cx="20" cy="180" r="2" fill="#C24536" opacity="0.3"/>
<!-- ๊นƒ๋Œ€ -->
<line x1="55" y1="30" x2="55" y2="210" stroke="#7A4F2A" stroke-width="5" stroke-linecap="round"/>
<circle cx="55" cy="30" r="6" fill="#D4A93B" stroke="#A8821A" stroke-width="0.6"/>
<!-- ๋งŒ์ž ๊นƒ๋ฐœ (๋ถ€๋“œ๋Ÿฌ์šด ๋นจ๊ฐ•) -->
<path d="M 55 38 L 142 38 L 130 65 L 142 92 L 55 92 Z"
fill="#C24536" stroke="#9E3528" stroke-width="1"/>
<text x="92" y="76" text-anchor="middle" fill="#FFF3DE"
font-size="34" font-weight="bold" font-family="serif">ๅ</text>
<!-- ๊นƒ๋ฐœ ๋ฐ”๋žŒ ํœ˜๋‚ ๋ฆผ ํ‘œ์‹œ -->
<path d="M 55 92 Q 60 96 55 100" stroke="#9E3528" stroke-width="1.5" fill="none"/>
<!-- ๊ทธ๋ฆผ์ž -->
<ellipse cx="175" cy="210" rx="42" ry="5" fill="#4A3826" opacity="0.13"/>
<!-- ํ•œ๋ณต ์น˜๋งˆ (๋ฐ์€ ์ฃผํ™) -->
<path d="M 138 138 Q 175 132 212 138 L 222 205 Q 175 211 128 205 Z"
fill="#E8533D" stroke="#9E3528" stroke-width="1"/>
<!-- ์น˜๋งˆ ๋ฌด๋Šฌ (๊ฝƒ์žŽ) -->
<circle cx="155" cy="175" r="2.5" fill="#D4A93B"/>
<circle cx="175" cy="185" r="2.5" fill="#D4A93B"/>
<circle cx="195" cy="172" r="2.5" fill="#D4A93B"/>
<path d="M 140 195 Q 175 192 210 195" stroke="#D4A93B" stroke-width="1.2" fill="none" opacity="0.6"/>
<!-- ๊ธˆ์ƒ‰ ๋  -->
<rect x="133" y="130" width="84" height="10" fill="#D4A93B" rx="1"/>
<rect x="133" y="130" width="84" height="10" fill="none" stroke="#A8821A" stroke-width="0.5" rx="1"/>
<!-- ์ €๊ณ ๋ฆฌ (๋”ฐ๋œปํ•œ ํฐ์ƒ‰) -->
<path d="M 143 95 Q 175 90 207 95 L 210 132 L 140 132 Z"
fill="#FFF8E7" stroke="#7A4F2A" stroke-width="1"/>
<!-- ์ €๊ณ ๋ฆฌ ๊นƒ (์ฃผํ™) -->
<path d="M 165 95 L 175 112 L 185 95" fill="#E8533D" stroke="#C24536" stroke-width="1.5" stroke-linejoin="round"/>
<!-- ์™ผํŒ” (๊นƒ๋Œ€ ์žก๊ธฐ) -->
<path d="M 143 105 Q 100 100 65 65" stroke="#FFF8E7" stroke-width="12"
fill="none" stroke-linecap="round"/>
<path d="M 143 105 Q 100 100 65 65" stroke="#7A4F2A" stroke-width="1"
fill="none" stroke-linecap="round" opacity="0.25"/>
<circle cx="63" cy="65" r="7" fill="#F5C9A0" stroke="#7A4F2A" stroke-width="0.8"/>
<!-- ์˜ค๋ฅธํŒ” (๋ฐ˜๊ฐ‘๊ฒŒ ๋“ค๊ธฐ) -->
<path d="M 205 105 Q 230 105 235 125" stroke="#FFF8E7" stroke-width="12"
fill="none" stroke-linecap="round"/>
<path d="M 205 105 Q 230 105 235 125" stroke="#7A4F2A" stroke-width="1"
fill="none" stroke-linecap="round" opacity="0.25"/>
<!-- ์† ํ”๋“œ๋Š” ๋ชจ์–‘ -->
<circle cx="237" cy="128" r="7" fill="#F5C9A0" stroke="#7A4F2A" stroke-width="0.8"/>
<!-- ์†๊ฐ€๋ฝ ํ‘œ์‹œ -->
<line x1="235" y1="122" x2="234" y2="118" stroke="#7A4F2A" stroke-width="1" stroke-linecap="round"/>
<line x1="240" y1="122" x2="241" y2="118" stroke="#7A4F2A" stroke-width="1" stroke-linecap="round"/>
<!-- ์–ผ๊ตด (๋‘ฅ๊ธ€๊ณ  ๋”ฐ๋œปํ•˜๊ฒŒ) -->
<ellipse cx="175" cy="73" rx="22" ry="24" fill="#F5C9A0" stroke="#7A4F2A" stroke-width="1"/>
<!-- ๋จธ๋ฆฌ์นด๋ฝ -->
<path d="M 153 64 Q 153 42 175 40 Q 197 42 197 64 Q 197 58 192 53 L 158 53 Q 153 58 153 64 Z"
fill="#3A2415"/>
<!-- ๋น„๋…€ -->
<line x1="190" y1="51" x2="208" y2="40" stroke="#D4A93B" stroke-width="2.8" stroke-linecap="round"/>
<circle cx="208" cy="40" r="3" fill="#D4A93B" stroke="#A8821A" stroke-width="0.5"/>
<!-- ์ด๋งˆ ์  (์ž‘์€ ๋นจ๊ฐ„์ , ๋ถ€์  ์˜๋ฏธ) -->
<circle cx="175" cy="63" r="2.4" fill="#C24536"/>
<!-- ์›ƒ๋Š” ๋ˆˆ (๋ฐ˜๋‹ฌ ๋ชจ์–‘) -->
<path d="M 165 74 Q 168 71 171 74" stroke="#3A2415" stroke-width="2"
fill="none" stroke-linecap="round"/>
<path d="M 179 74 Q 182 71 185 74" stroke="#3A2415" stroke-width="2"
fill="none" stroke-linecap="round"/>
<!-- ์ž… (์›ƒ๋Š” ์ž…) -->
<path d="M 168 83 Q 175 88 182 83" stroke="#9E3528" stroke-width="2"
fill="none" stroke-linecap="round"/>
<!-- ๋ณผํ„ฐ์น˜ (๋ถ„ํ™) -->
<ellipse cx="159" cy="79" rx="3.5" ry="2.5" fill="#E89090" opacity="0.55"/>
<ellipse cx="191" cy="79" rx="3.5" ry="2.5" fill="#E89090" opacity="0.55"/>
</svg>
"""
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 6. UI โ€” 5๊ฐœ ํ™”๋ฉด
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with gr.Blocks(css=custom_css, title="์šฐ๋ฆฌ ๋™๋„ค ์ ์ง‘",
theme=gr.themes.Soft(primary_hue="red")) as demo:
# =============== ํ™”๋ฉด 1: ํ™ˆ ===============
with gr.Column(visible=True) as home_screen:
gr.HTML(f"""
<div class="scr-header">
<div class="scr-title">์šฐ๋ฆฌ ๋™๋„ค ์ ์ง‘</div>
<div class="scr-sub">๊ทธ๋กœ์„ผ๋”• ยท AI ๋ฌด๋‹น์ด ๋ด์ฃผ๋Š” ์ (้ปž) ์ (ๅ )</div>
</div>
<div class="disclaimer">โš ๏ธ ๊ต์œก์šฉ ๋„๊ตฌ ยท ์˜๋ฃŒ ์ง„๋‹จ ๋Œ€์ฒด ๋ถˆ๊ฐ€</div>
<div class="jeomjip-hero">
{MUDANG_SVG}
<div class="hero-title">์  ๋ณด๋Ÿฌ ์˜ค์…จ์†Œ</div>
<div class="hero-sub">
"์–ด์„œ ์˜ค์‹œ์˜ค, ์†๋‹˜.<br>
๊ทธ ์ , ๋ฌด๋‹น์ด ๋ด๋“œ๋ฆฌ๋ฆฌ๋‹ค."<br>
<span class="hero-quote">โ€” AI ๋ฌด๋‹น 5๋ช…์ด ํ•ฉ์‹ฌํ•˜์—ฌ ์ ๊ด˜๋ฅผ ํ’‰๋‹ˆ๋‹ค</span>
</div>
</div>
""")
start_btn = gr.Button("๐Ÿ”ฎ ์ ๊ฒฌ์  ๋ณด๊ธฐ", elem_classes="primary-btn")
info_from_home = gr.Button("๐Ÿ“œ ์ ์ด๋ž€?", elem_classes="secondary-btn")
# =============== ํ™”๋ฉด 2: ์šด์ˆ˜ ํ™•์ธ (๋ฌธ์ง„) ===============
with gr.Column(visible=False) as questionnaire_screen:
gr.HTML("""
<div class="scr-header">
<div class="scr-title">๐Ÿ“‹ ์šด์ˆ˜(้‹ๆ•ธ) ํ™•์ธ</div>
<div class="scr-sub">1 / 3 ยท ์ ๊ด˜ ํ’€๊ธฐ ์ „ ์‚ฌ์ฃผ ๋ฌป๊ธฐ</div>
</div>
<div class="progress-dots">
<div class="progress-dot active"></div>
<div class="progress-dot"></div>
<div class="progress-dot"></div>
</div>
<div class="app-card">
<div class="app-card-title">โ€” ๋ฌด๋‹น์˜ ์ฒซ ์งˆ๋ฌธ โ€”</div>
<div class="app-card-desc">์‚ฌ์ฃผ๋ฅผ ์•Œ์•„์•ผ ์ ์ด ๋” ์ •ํ™•ํ•˜๋‹ค์˜ค. ์†”์งํ•˜๊ฒŒ ๋‹ตํ•ด์ฃผ์‹œ์˜ค.</div>
</div>
""")
with gr.Column(elem_classes="app-card"):
age = gr.Slider(0, 80, value=30, step=1, label="๐ŸŽ‚ ์†๋‹˜ ๋‚˜์ด๋Š” ์–ด์ฐŒ ๋˜์‹œ์˜ค?",
info="ํ‘์ƒ‰์ข…์€ ๋‚˜์ด๊ฐ€ ๋“ค์ˆ˜๋ก ๋ฐœ์ƒ ๋นˆ๋„๊ฐ€ ๋†’์•„์ง„๋‹ค์˜ค")
family = gr.Radio(["์•„๋‹ˆ์˜ค", "์˜ˆ"], value="์•„๋‹ˆ์˜ค",
label="๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง ์ง‘์•ˆ์— ํ‘์ƒ‰์ข… ๊ฐ€์ง„ ๋ถ„์ด ์žˆ์†Œ?",
info="๋ถ€๋ชจยทํ˜•์ œ ๊ธฐ์ค€")
mole_change = gr.Radio(["์•„๋‹ˆ์˜ค", "์˜ˆ"], value="์•„๋‹ˆ์˜ค",
label="๐Ÿ”„ ๊ทธ ์ ์ด ์ตœ๊ทผ ๋ณ€(่ฎŠ)ํ–ˆ์†Œ?",
info="ํฌ๊ธฐยท์ƒ‰ยท๋ชจ์–‘ ๋ณ€ํ™” (์ตœ๊ทผ 6๊ฐœ์›” ์ด๋‚ด)")
with gr.Row(elem_classes="row-buttons"):
q_back = gr.Button("โ† ๋Œ์•„๊ฐ€๊ธฐ", elem_classes="secondary-btn")
q_next = gr.Button("์  ๋ณด์—ฌ์ฃผ์˜ค โ†’", elem_classes="primary-btn")
# =============== ํ™”๋ฉด 3: ์  ์‚ฌ์ง„ ===============
with gr.Column(visible=False) as photo_screen:
gr.HTML("""
<div class="scr-header">
<div class="scr-title">๐Ÿ“ธ ์ (้ปž) ์‚ฌ์ง„</div>
<div class="scr-sub">2 / 3 ยท ๋ฌด๋‹น์—๊ฒŒ ์ ์„ ๋ณด์—ฌ์ฃผ์˜ค</div>
</div>
<div class="progress-dots">
<div class="progress-dot active"></div>
<div class="progress-dot active"></div>
<div class="progress-dot"></div>
</div>
<div class="app-card">
<div class="app-card-title">โ€” ์ ์„ ๊ฐ€๊นŒ์ด ๋น„์ถ”์‹œ์˜ค โ€”</div>
<div class="app-card-desc">๋ฐ์€ ๊ณณ์—์„œ, ์ ์ด ํ™”๋ฉด์— ๊ฐ€๋“ ์ฐจ๊ฒŒ ์ฐ์–ด์ฃผ์‹œ์˜ค. ์นด๋ฉ”๋ผ๋กœ ์ฐ๊ฑฐ๋‚˜ ๊ฐค๋Ÿฌ๋ฆฌ์—์„œ ๊ณจ๋ผ๋„ ์ข‹์†Œ.</div>
</div>
""")
image_input = gr.Image(sources=["upload", "webcam"], type="numpy",
label="", show_label=False, height=300)
with gr.Row(elem_classes="row-buttons"):
p_back = gr.Button("โ† ๋Œ์•„๊ฐ€๊ธฐ", elem_classes="secondary-btn")
analyze = gr.Button("๐Ÿ”ฎ ์ ๊ด˜ ๋ณด๊ธฐ", elem_classes="primary-btn")
# =============== ํ™”๋ฉด 4: ์ ๊ด˜ (๊ฒฐ๊ณผ) ===============
with gr.Column(visible=False) as result_screen:
gr.HTML("""
<div class="scr-header">
<div class="scr-title">๐Ÿ”ฎ ์˜ค๋Š˜์˜ ์ ๊ด˜</div>
<div class="scr-sub">3 / 3 ยท ๋ฌด๋‹น์˜ ํ’€์ด</div>
</div>
<div class="progress-dots">
<div class="progress-dot active"></div>
<div class="progress-dot active"></div>
<div class="progress-dot active"></div>
</div>
""")
with gr.Column(elem_classes="app-card"):
result_label = gr.Label(label="", show_label=False, num_top_classes=2)
result_detail = gr.HTML()
gr.HTML('<div class="disclaimer">์ฐธ๊ณ ์šฉ ์ ๊ด˜์ผ ๋ฟ, ์ง„์งœ ์ง„๋‹จ์€ ํ”ผ๋ถ€๊ณผ ์ „๋ฌธ์˜์—๊ฒŒ ๋ฐ›์œผ์‹œ์˜ค.</div>')
with gr.Row(elem_classes="row-buttons"):
again_btn = gr.Button("๐Ÿ”„ ๋‹ค์‹œ ์ ์น˜๊ธฐ", elem_classes="primary-btn")
info_from_result = gr.Button("๐Ÿ“œ ์ ์ด๋ž€?", elem_classes="secondary-btn")
# =============== ํ™”๋ฉด 5: ์ ์ด๋ž€ (์ •๋ณด) ===============
with gr.Column(visible=False) as info_screen:
gr.HTML("""
<div class="scr-header">
<div class="scr-title">๐Ÿ“œ ์ ์ด๋ž€ ๋ฌด์—‡์ด์˜ค?</div>
<div class="scr-sub">์•Œ์•„๋‘๋ฉด ์ข‹์€ ์˜ํ•™ ์ด์•ผ๊ธฐ</div>
</div>
""")
with gr.Column(elem_classes="app-card"):
with gr.Accordion("๐Ÿ” ํ‘์ƒ‰์ข…์ด๋ž€?", open=True):
gr.Markdown(
"**ํ‘์ƒ‰์ข…(Melanoma)** ์€ ํ”ผ๋ถ€์˜ ๋ฉœ๋ผ๋…ธ์‚ฌ์ดํŠธ(์ƒ‰์†Œ ์„ธํฌ)๊ฐ€ "
"์•…์„ฑ ๋ณ€ํ™”ํ•œ ์•”์ด์˜ค. ๋‹ค๋ฅธ ํ”ผ๋ถ€์•”๋ณด๋‹ค ๋ฐœ์ƒ ๋นˆ๋„๋Š” ๋‚ฎ์ง€๋งŒ "
"**์ „์ด๊ฐ€ ๋นจ๋ผ ๊ฐ€์žฅ ๊ณต๊ฒฉ์ **์ด๋ผ ์กฐ๊ธฐ ๋ฐœ๊ฒฌ์ด ์ค‘์š”ํ•˜๋‹ค์˜ค."
)
with gr.Accordion("๐Ÿ“ ABCDE ๊ทœ์น™ โ€” ์ž๊ฐ€ ๊ฒ€์ง„"):
gr.Markdown(
"- **A**symmetry โ€” ๋น„๋Œ€์นญ\n"
"- **B**order โ€” ๋“ค์ญ‰๋‚ ์ญ‰ํ•œ ๊ฒฝ๊ณ„\n"
"- **C**olor โ€” ํ•œ ์ ์— ์—ฌ๋Ÿฌ ์ƒ‰\n"
"- **D**iameter โ€” 6mm ์ด์ƒ\n"
"- **E**volving โ€” ๋ณ€ํ™”\n\n"
"์—ฌ๋Ÿฌ ํ•ญ๋ชฉ ํ•ด๋‹น ์‹œ ํ”ผ๋ถ€๊ณผ ์ง„๋ฃŒ๊ฐ€ ๋‹ต์ด์™ธ๋‹ค."
)
with gr.Accordion("๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์ธ์ด ์•Œ์•„์•ผ ํ•  ๊ฒƒ"):
gr.Markdown(
"ํ•œ๊ตญ์ธ ํ‘์ƒ‰์ข…์˜ **30~50%** ๋Š” **๋ง๋‹จํ‘์ƒ‰์  ํ‘์ƒ‰์ข…**(์†๋ฐ”๋‹ฅยท๋ฐœ๋ฐ”๋‹ฅยท์†ํ†ฑ) "
"ํ˜•ํƒœ๋ผ๋„ค. ์ž์™ธ์„ ๊ณผ ๋ฌด๊ด€ํ•˜๋ฉฐ ๋‹จ์ˆœ ์ ์ฒ˜๋Ÿผ ๋ณด์—ฌ ๋Šฆ๊ฒŒ ๋ฐœ๊ฒฌ๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์†Œ."
)
with gr.Accordion("๐Ÿฅ ๋ณ‘์›์— ๊ฐ€์•ผ ํ•  ๋•Œ"):
gr.Markdown(
"- ์ ์ด ๋ณ€ํ•  ๋•Œ (ํฌ๊ธฐยท์ƒ‰ยท๋ชจ์–‘)\n"
"- ์ ์—์„œ ์ถœํ˜ˆ\n"
"- ๊ฐ€๋ ต๊ฑฐ๋‚˜ ํ†ต์ฆ\n"
"- ์†ยท๋ฐœ๋ฐ”๋‹ฅ์— ์ƒˆ๋กœ์šด ์ \n"
"- ๊ฐ€์กฑ ์ค‘ ํ‘์ƒ‰์ข… ํ™˜์ž ์žˆ์Œ\n\n"
"โ†’ **ํ”ผ๋ถ€๊ณผ ์ „๋ฌธ์˜** ์ง„๋ฃŒ๊ฐ€ ์ •๋‹ต์ด์˜ค."
)
info_back = gr.Button("โ† ์ ์ง‘์œผ๋กœ", elem_classes="secondary-btn")
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 7. ํ™”๋ฉด ์ „ํ™˜ ๋กœ์ง
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
all_screens = [home_screen, questionnaire_screen, photo_screen,
result_screen, info_screen]
def goto(screen_name):
order = ["home", "questionnaire", "photo", "result", "info"]
return [gr.update(visible=(s == screen_name)) for s in order]
start_btn.click( fn=lambda: goto("questionnaire"), outputs=all_screens)
info_from_home.click( fn=lambda: goto("info"), outputs=all_screens)
q_back.click( fn=lambda: goto("home"), outputs=all_screens)
q_next.click( fn=lambda: goto("photo"), outputs=all_screens)
p_back.click( fn=lambda: goto("questionnaire"), outputs=all_screens)
again_btn.click( fn=lambda: goto("home"), outputs=all_screens)
info_from_result.click(fn=lambda: goto("info"), outputs=all_screens)
info_back.click( fn=lambda: goto("home"), outputs=all_screens)
# AI ์ ๊ด˜ โ†’ ๊ฒฐ๊ณผ ํ™”๋ฉด
def analyze_and_show(image, age_v, family_v, mole_change_v):
if image is None:
return goto("photo") + [None, "<div class='disclaimer'>๋จผ์ € ์  ์‚ฌ์ง„์„ ๋ณด์—ฌ์ฃผ์˜ค</div>"]
label, detail = predict_with_questionnaire(image, age_v, family_v, mole_change_v)
return goto("result") + [label, detail]
analyze.click(
fn=analyze_and_show,
inputs=[image_input, age, family, mole_change],
outputs=all_screens + [result_label, result_detail],
)
demo.launch()