Spaces:
Running
Running
| # ============================================================ | |
| # 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() |