import sys import os 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 import random # ────────────────────────────────────────── # 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("📥 모델 로드 중...") 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': 0.3779502616311788, 'efficientnet_b5': 0.2031553438850185, 'convnext_tiny': 0.016351817610742503, 'convnext_base': 0.20957851686980142, 'swin_base': 0.1929640600032588, } # ────────────────────────────────────────── # 3. 추론 함수 # ────────────────────────────────────────── def predict(image): if image is None: return {"📷 사진을 업로드하거나 촬영해주세요": 1.0} 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()) final = sum(probs[k]*w/total for k,w in MY_WEIGHTS.items()) return {"멜라노마 의심": float(final), "양성 모반": float(1-final)} # --- 📱 병맛 곡선 애니메이션 풀버전 CSS --- css = """ /* 앱 전체 컨테이너 */ .gradio-container { max-width: 420px !important; margin: 15px auto !important; border: 4px solid #6c5ce7 !important; border-radius: 32px !important; box-shadow: 0 20px 60px rgba(108,92,231,0.3) !important; background: linear-gradient(135deg, #f8f9ff 0%, #e8f4f8 100%); font-family: 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif; } /* 입력 요소들이 가려지지 않도록 보장 */ .gradio-container input, .gradio-container label, .gradio-container .gr-button { position: relative; z-index: 20 !important; pointer-events: auto !important; } /* 🌈 메인 타이틀 - 출렁이는 효과 */ .main-title { text-align: center; font-size: 2.5em; font-weight: 900; background: linear-gradient(135deg, #6c5ce7, #a29bfe, #fd79a8, #fdcb6e); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-top: 25px; letter-spacing: -1px; animation: title-wobble 2s ease-in-out infinite; } @keyframes title-wobble { 0%, 100% { transform: rotate(-1deg) scale(1); } 50% { transform: rotate(1deg) scale(1.02); } } .sub-title { text-align: center; font-size: 1.05em; color: #6c5ce7; margin: 15px 20px 25px 20px; font-weight: bold; line-height: 1.5; background: #fff; padding: 12px 20px; border-radius: 16px; border: 2px dashed #a29bfe; animation: sub-float 3s ease-in-out infinite; } @keyframes sub-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } } /* ========================================== 🏠 홈 화면 - 병맛 점 캐릭터 (곡선 팔다리) ========================================== */ .home-stage { display: flex; flex-direction: column; align-items: center; justify-content: center; margin: 20px; height: 220px; background: linear-gradient(180deg, #ffeaa7 0%, #fdcb6e 100%); border-radius: 24px; border: 3px solid #f39c12; position: relative; overflow: hidden; } /* 떠다니는 구름들 */ .cloud { position: absolute; background: #fff; border-radius: 50px; opacity: 0.8; animation: cloud-drift 8s linear infinite; } .cloud::before, .cloud::after { content: ''; position: absolute; background: #fff; border-radius: 50%; } .cloud-1 { width: 50px; height: 20px; top: 20px; left: -60px; animation-delay: 0s; } .cloud-1::before { width: 25px; height: 25px; top: -12px; left: 8px; } .cloud-1::after { width: 18px; height: 18px; top: -8px; left: 28px; } .cloud-2 { width: 40px; height: 16px; top: 50px; left: -50px; animation-delay: 3s; animation-duration: 10s; } .cloud-2::before { width: 20px; height: 20px; top: -10px; left: 5px; } .cloud-2::after { width: 15px; height: 15px; top: -6px; left: 20px; } @keyframes cloud-drift { from { transform: translateX(0); } to { transform: translateX(500px); } } /* 반짝이 별 */ .sparkle { position: absolute; width: 12px; height: 12px; background: #fff; clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%); animation: twinkle 1.5s infinite; } .sparkle-1 { top: 25px; left: 30px; animation-delay: 0s; } .sparkle-2 { top: 40px; right: 40px; animation-delay: 0.3s; width: 16px; height: 16px; } .sparkle-3 { bottom: 50px; left: 50px; animation-delay: 0.6s; width: 10px; height: 10px; } .sparkle-4 { bottom: 35px; right: 35px; animation-delay: 0.9s; } @keyframes twinkle { 0%, 100% { opacity: 1; transform: scale(1) rotate(0deg); } 50% { opacity: 0.3; transform: scale(0.6) rotate(180deg); } } /* 메인 점 캐릭터 - 몸통 */ .mole-character { position: relative; width: 80px; height: 80px; background: radial-gradient(circle at 35% 35%, #636e72, #2d3436); border-radius: 50%; box-shadow: 0 12px 30px rgba(0,0,0,0.3), inset -6px -6px 15px rgba(0,0,0,0.4), inset 6px 6px 15px rgba(255,255,255,0.1); animation: mole-bounce 0.6s ease-in-out infinite alternate; z-index: 10; } @keyframes mole-bounce { from { transform: translateY(0) rotate(-3deg); } to { transform: translateY(-20px) rotate(3deg); } } /* 눈 */ .mole-eye { position: absolute; width: 18px; height: 18px; background: #fff; border-radius: 50%; top: 22px; } .mole-eye.left { left: 15px; } .mole-eye.right { right: 15px; } /* 눈동자 - 굴러다님 */ .mole-pupil { position: absolute; width: 9px; height: 9px; background: #2d3436; border-radius: 50%; top: 26px; animation: pupil-roll 2s ease-in-out infinite; } .mole-pupil.left { left: 19px; } .mole-pupil.right { right: 19px; } @keyframes pupil-roll { 0%, 100% { transform: translate(0, 0); } 25% { transform: translate(3px, 2px); } 50% { transform: translate(0, 3px); } 75% { transform: translate(-3px, 2px); } } /* 눈썹 - 꿈틀 */ .mole-brow { position: absolute; width: 14px; height: 6px; border: 3px solid #fff; border-bottom: none; border-radius: 14px 14px 0 0; top: 14px; animation: brow-wiggle 1.5s ease-in-out infinite; } .mole-brow.left { left: 17px; animation-delay: 0s; } .mole-brow.right { right: 17px; animation-delay: 0.2s; } @keyframes brow-wiggle { 0%, 100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-3px) rotate(5deg); } } /* 볼터치 - 두근두근 */ .mole-blush { position: absolute; width: 14px; height: 8px; background: #e17055; border-radius: 50%; top: 45px; opacity: 0.7; animation: blush-pulse 1s ease-in-out infinite; } .mole-blush.left { left: 6px; } .mole-blush.right { right: 6px; animation-delay: 0.5s; } @keyframes blush-pulse { 0%, 100% { opacity: 0.5; transform: scale(1); } 50% { opacity: 0.9; transform: scale(1.2); } } /* 입 - 쫑긋 */ .mole-mouth { position: absolute; width: 20px; height: 10px; border: 3px solid #fff; border-top: none; border-radius: 0 0 20px 20px; bottom: 14px; left: 50%; transform: translateX(-50%); animation: mouth-talk 0.8s ease-in-out infinite; } @keyframes mouth-talk { 0%, 100% { height: 10px; } 50% { height: 14px; } } /* 🦾 곡선 팔 (SVG 스타일 곡선) */ .mole-arm-wrap { position: absolute; top: -5px; width: 45px; height: 50px; } .mole-arm-wrap.left { left: -40px; } .mole-arm-wrap.right { right: -40px; transform: scaleX(-1); } .mole-arm-curve { position: absolute; width: 40px; height: 45px; border: 6px solid transparent; border-left: 6px solid #2d3436; border-radius: 50% 0 0 50%; transform-origin: bottom right; animation: arm-wave 0.4s ease-in-out infinite alternate; box-shadow: -2px 0 0 0 #636e72; } .mole-arm-wrap.left .mole-arm-curve { animation-delay: 0s; } .mole-arm-wrap.right .mole-arm-curve { animation-delay: 0.2s; } @keyframes arm-wave { from { transform: rotate(-20deg); } to { transform: rotate(-50deg); } } /* 손 - 동글동글 */ .mole-hand { position: absolute; width: 14px; height: 14px; background: radial-gradient(circle at 40% 40%, #636e72, #2d3436); border-radius: 50%; top: -8px; left: -2px; box-shadow: 0 2px 5px rgba(0,0,0,0.3); } /* 🦵 곡선 다리 */ .mole-legs { position: absolute; bottom: -35px; left: 50%; transform: translateX(-50%); display: flex; gap: 20px; } .mole-leg-wrap { width: 25px; height: 35px; position: relative; } .mole-leg-curve { position: absolute; width: 20px; height: 30px; border: 5px solid transparent; border-left: 5px solid #2d3436; border-radius: 0 0 0 50%; transform-origin: top center; box-shadow: -2px 0 0 0 #636e72; } .mole-leg-wrap.left .mole-leg-curve { border-radius: 0 0 0 50%; animation: leg-kick-l 0.6s ease-in-out infinite alternate; } .mole-leg-wrap.right .mole-leg-curve { border-left: none; border-right: 5px solid #2d3436; border-radius: 0 0 50% 0; box-shadow: 2px 0 0 0 #636e72; animation: leg-kick-r 0.6s ease-in-out infinite alternate-reverse; } @keyframes leg-kick-l { from { transform: rotate(10deg); } to { transform: rotate(-15deg); } } @keyframes leg-kick-r { from { transform: rotate(-10deg); } to { transform: rotate(15deg); } } /* 발 */ .mole-foot { position: absolute; width: 16px; height: 10px; background: radial-gradient(ellipse at 50% 30%, #636e72, #2d3436); border-radius: 50%; bottom: -5px; } .mole-leg-wrap.left .mole-foot { left: -8px; } .mole-leg-wrap.right .mole-foot { right: -8px; } /* ========================================== 📝 설문 화면 - 궁금해하는 캐릭터 ========================================== */ .survey-stage { display: flex; justify-content: center; align-items: center; margin: 15px 20px; padding: 15px; background: linear-gradient(135deg, #dfe6e9, #b2bec3); border-radius: 20px; border: 3px solid #636e72; } .curious-mole { position: relative; width: 50px; height: 50px; background: radial-gradient(circle at 35% 35%, #636e72, #2d3436); border-radius: 50%; animation: curious-tilt 2s ease-in-out infinite; } @keyframes curious-tilt { 0%, 100% { transform: rotate(0deg) translateY(0); } 25% { transform: rotate(-15deg) translateY(-5px); } 75% { transform: rotate(15deg) translateY(-5px); } } .curious-eye { position: absolute; width: 12px; height: 12px; background: #fff; border-radius: 50%; top: 15px; } .curious-eye.left { left: 10px; } .curious-eye.right { right: 10px; } .curious-pupil { position: absolute; width: 6px; height: 6px; background: #2d3436; border-radius: 50%; top: 18px; animation: curious-look 2s ease-in-out infinite; } .curious-pupil.left { left: 13px; } .curious-pupil.right { right: 13px; } @keyframes curious-look { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(3px); } } /* 물음표 */ .question-mark { position: absolute; top: -25px; right: -10px; font-size: 24px; font-weight: 900; color: #6c5ce7; animation: question-bounce 0.8s ease-in-out infinite; } @keyframes question-bounce { 0%, 100% { transform: translateY(0) rotate(-10deg); } 50% { transform: translateY(-8px) rotate(10deg); } } .think-bubble { position: absolute; top: -15px; right: -5px; width: 8px; height: 8px; background: #6c5ce7; border-radius: 50%; animation: bubble-pop 1s ease-in-out infinite; } .think-bubble.b2 { top: -8px; right: 0; width: 5px; height: 5px; animation-delay: 0.3s; } @keyframes bubble-pop { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.3); opacity: 0.6; } } /* ========================================== 📸 촬영 화면 - 기다리는 캐릭터 ========================================== */ .camera-stage { display: flex; justify-content: center; align-items: center; margin: 15px 20px; padding: 20px; background: linear-gradient(135deg, #74b9ff, #0984e3); border-radius: 20px; border: 3px solid #0984e3; position: relative; } .waiting-mole { position: relative; width: 55px; height: 55px; background: radial-gradient(circle at 35% 35%, #636e72, #2d3436); border-radius: 50%; animation: waiting-tap 0.5s ease-in-out infinite; } @keyframes waiting-tap { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-3px); } } .waiting-eye { position: absolute; width: 14px; height: 14px; background: #fff; border-radius: 50%; top: 16px; } .waiting-eye.left { left: 10px; } .waiting-eye.right { right: 10px; } /* 졸린 눈 깜빡 */ .waiting-lid { position: absolute; width: 14px; height: 0px; background: #2d3436; border-radius: 0 0 14px 14px; top: 16px; animation: sleepy-blink 3s ease-in-out infinite; } .waiting-lid.left { left: 10px; } .waiting-lid.right { right: 10px; } @keyframes sleepy-blink { 0%, 40%, 100% { height: 0px; } 45%, 55% { height: 10px; } 60% { height: 0px; } 80%, 90% { height: 14px; } } /* zzZ */ .zzz { position: absolute; top: -20px; right: -15px; font-weight: 900; color: #fff; animation: zzz-float 2s ease-in-out infinite; } .zzz-1 { font-size: 14px; top: -15px; right: -10px; animation-delay: 0s; } .zzz-2 { font-size: 18px; top: -30px; right: -20px; animation-delay: 0.4s; } .zzz-3 { font-size: 12px; top: -40px; right: -8px; animation-delay: 0.8s; } @keyframes zzz-float { 0%, 100% { opacity: 0; transform: translateY(5px); } 50% { opacity: 1; transform: translateY(-5px); } } /* 카메라 아이콘 */ .camera-icon { position: absolute; left: 20px; width: 40px; height: 30px; background: #2d3436; border-radius: 5px; animation: camera-flash 2s ease-in-out infinite; } .camera-icon::before { content: ''; position: absolute; top: -8px; left: 12px; width: 16px; height: 8px; background: #2d3436; border-radius: 3px 3px 0 0; } .camera-icon::after { content: ''; position: absolute; top: 8px; left: 10px; width: 20px; height: 20px; border: 4px solid #636e72; border-radius: 50%; background: radial-gradient(circle, #74b9ff, #0984e3); } @keyframes camera-flash { 0%, 90%, 100% { box-shadow: none; } 95% { box-shadow: 0 0 30px #fff, 0 0 60px #fff; } } /* ========================================== 📊 결과 화면 상단바 ========================================== */ .result-top-bar { display: flex; justify-content: space-between; align-items: center; background: #fff; border-radius: 20px; padding: 18px 22px; margin-bottom: 15px; border: 3px solid #dfe6e9; box-shadow: 0 6px 20px rgba(0,0,0,0.08); animation: result-pop 0.5s ease-out; } @keyframes result-pop { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } } .result-left { display: flex; flex-direction: column; } .result-label { font-size: 0.85em; color: #636e72; font-weight: 600; margin-bottom: 5px; } .result-value { font-size: 3.2em; font-weight: 900; line-height: 1; } .result-value.danger { color: #d63031; animation: value-shake 0.3s ease-in-out infinite; } .result-value.safe { color: #00b894; animation: value-happy 1s ease-in-out infinite; } @keyframes value-shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-3px); } 75% { transform: translateX(3px); } } @keyframes value-happy { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } } .result-badge { font-size: 1em; font-weight: 800; padding: 10px 20px; border-radius: 25px; color: #fff; } .result-badge.danger { background: linear-gradient(135deg, #d63031, #e17055); animation: badge-pulse 0.5s infinite; } .result-badge.safe { background: linear-gradient(135deg, #00b894, #55efc4); animation: badge-glow 1.5s infinite; } @keyframes badge-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } } @keyframes badge-glow { 0%, 100% { box-shadow: 0 0 10px rgba(0,184,148,0.5); } 50% { box-shadow: 0 0 25px rgba(0,184,148,0.8); } } /* ========================================== 💥 위험 스테이지 - 곡선 팔다리 괴한 ========================================== */ .danger-stage { background: linear-gradient(180deg, #ffeaa7 0%, #fab1a0 50%, #ff7675 100%); border: 4px solid #d63031; border-radius: 24px; padding: 25px 15px; margin: 10px 0; min-height: 240px; position: relative; overflow: hidden; display: flex; justify-content: space-between; align-items: center; } /* 불꽃 배경 */ .flame { position: absolute; bottom: 0; width: 30px; height: 50px; background: linear-gradient(180deg, transparent 0%, #fdcb6e 30%, #e17055 60%, #d63031 100%); border-radius: 50% 50% 20% 20%; animation: flame-flicker 0.2s infinite; opacity: 0.7; } .flame-1 { left: 10%; height: 40px; animation-delay: 0s; } .flame-2 { left: 30%; height: 55px; animation-delay: 0.1s; } .flame-3 { left: 50%; height: 45px; animation-delay: 0.05s; } .flame-4 { left: 70%; height: 50px; animation-delay: 0.15s; } .flame-5 { left: 90%; height: 35px; animation-delay: 0.08s; } @keyframes flame-flicker { 0%, 100% { transform: scaleY(1) scaleX(1); } 50% { transform: scaleY(1.1) scaleX(0.9); } } /* 괴한 캐릭터 (곡선 버전) */ .villain { position: relative; width: 70px; height: 140px; z-index: 10; } .villain.right { transform: scaleX(-1); } /* 머리 */ .villain-head { width: 38px; height: 38px; background: radial-gradient(circle at 35% 35%, #495057, #212529); border-radius: 50%; margin: 0 auto; position: relative; box-shadow: 0 4px 12px rgba(0,0,0,0.4); } .villain-mask { position: absolute; width: 30px; height: 10px; background: #000; top: 12px; left: 50%; transform: translateX(-50%); border-radius: 3px; } .villain-eye { position: absolute; width: 6px; height: 6px; background: #ff4757; border-radius: 50%; top: 14px; box-shadow: 0 0 8px #ff4757; animation: evil-eye 0.5s infinite; } .villain-eye.left { left: 8px; } .villain-eye.right { right: 8px; } @keyframes evil-eye { 0%, 100% { box-shadow: 0 0 5px #ff4757; } 50% { box-shadow: 0 0 15px #ff4757, 0 0 25px #ff4757; } } /* 몸통 */ .villain-torso { width: 28px; height: 35px; background: linear-gradient(180deg, #2d3436, #1a1a1a); margin: -5px auto 0; border-radius: 8px 8px 5px 5px; position: relative; } /* 곡선 팔 + 총 */ .villain-arm-set { position: absolute; top: 5px; right: -32px; animation: arm-recoil 0.12s infinite; } @keyframes arm-recoil { 0%, 100% { transform: translateX(0) rotate(0deg); } 50% { transform: translateX(-4px) rotate(-8deg); } } .villain-arm-curve { width: 28px; height: 20px; border: 5px solid transparent; border-top: 5px solid #2d3436; border-radius: 50% 50% 0 0; position: relative; } .villain-gun { position: absolute; right: -5px; top: -5px; width: 24px; height: 16px; background: linear-gradient(180deg, #636e72, #2d3436); border-radius: 4px; box-shadow: 2px 2px 5px rgba(0,0,0,0.4); } .villain-gun::before { content: ''; position: absolute; width: 14px; height: 8px; background: #2d3436; bottom: -6px; left: 3px; border-radius: 2px; } /* 총구 화염 - 삼각 불꽃 */ .muzzle-flash { position: absolute; right: -20px; top: 2px; width: 0; height: 0; border-left: 20px solid #fdcb6e; border-top: 10px solid transparent; border-bottom: 10px solid transparent; animation: flash-burst 0.1s infinite; filter: drop-shadow(0 0 8px #f39c12); } @keyframes flash-burst { 0%, 100% { opacity: 1; transform: scaleX(1); } 50% { opacity: 0.6; transform: scaleX(0.7); } } /* 곡선 불꽃 */ .muzzle-curve { position: absolute; right: -30px; top: -2px; width: 15px; height: 25px; border: 4px solid transparent; border-right: 4px solid #ff7675; border-radius: 0 50% 50% 0; animation: flame-curl 0.1s infinite alternate; } @keyframes flame-curl { from { transform: scaleY(0.8); } to { transform: scaleY(1.2); } } /* 곡선 다리 */ .villain-legs { display: flex; justify-content: center; gap: 6px; margin-top: -2px; } .villain-leg-curve { width: 18px; height: 38px; border: 5px solid transparent; border-radius: 0 0 40% 40%; position: relative; } .villain-leg-curve.left { border-left: 5px solid #2d3436; transform: rotate(8deg); animation: villain-step-l 0.4s ease-in-out infinite alternate; } .villain-leg-curve.right { border-right: 5px solid #2d3436; transform: rotate(-8deg); animation: villain-step-r 0.4s ease-in-out infinite alternate-reverse; } @keyframes villain-step-l { from { transform: rotate(5deg); } to { transform: rotate(15deg); } } @keyframes villain-step-r { from { transform: rotate(-5deg); } to { transform: rotate(-15deg); } } .villain-foot { position: absolute; bottom: -3px; width: 14px; height: 8px; background: #2d3436; border-radius: 50%; } .villain-leg-curve.left .villain-foot { left: -10px; } .villain-leg-curve.right .villain-foot { right: -10px; } /* 레이저 빔 - 물결 모양 */ .laser-beam { position: absolute; height: 6px; background: linear-gradient(90deg, #d63031, #ff7675, #fab1a0, #ff7675, #d63031); box-shadow: 0 0 15px #d63031, 0 0 30px #ff7675; animation: laser-wave 0.15s infinite; border-radius: 3px; } .laser-beam.left { left: 75px; width: 70px; top: 80px; } .laser-beam.right { right: 75px; width: 70px; top: 80px; } @keyframes laser-wave { 0%, 100% { transform: scaleY(1) translateY(0); height: 6px; } 25% { transform: scaleY(1.5) translateY(-2px); height: 4px; } 50% { transform: scaleY(0.8) translateY(2px); height: 8px; } 75% { transform: scaleY(1.3) translateY(-1px); height: 5px; } } /* 피격 점 캐릭터 */ .victim-mole { position: relative; width: 65px; height: 65px; background: radial-gradient(circle at 35% 35%, #636e72, #2d3436); border-radius: 50%; z-index: 10; animation: victim-shake 0.06s infinite; box-shadow: 0 0 0 6px #d63031, 0 0 30px rgba(214,48,49,0.6); } @keyframes victim-shake { 0% { transform: translate(3px, 2px) rotate(2deg); } 25% { transform: translate(-3px, 2px) rotate(-2deg); } 50% { transform: translate(2px, -3px) rotate(1deg); } 75% { transform: translate(-2px, -2px) rotate(-1deg); } 100% { transform: translate(3px, 2px) rotate(2deg); } } /* X_X 눈 */ .victim-x-eye { position: absolute; width: 16px; height: 16px; top: 20px; } .victim-x-eye.left { left: 12px; } .victim-x-eye.right { right: 12px; } .victim-x-eye::before, .victim-x-eye::after { content: ''; position: absolute; width: 16px; height: 4px; background: #fff; top: 6px; left: 0; border-radius: 2px; } .victim-x-eye::before { transform: rotate(45deg); } .victim-x-eye::after { transform: rotate(-45deg); } /* 흐느끼는 입 */ .victim-mouth-sad { position: absolute; width: 18px; height: 8px; border: 4px solid #74b9ff; border-bottom: none; border-radius: 18px 18px 0 0; bottom: 14px; left: 50%; transform: translateX(-50%); animation: mouth-tremble 0.2s infinite; } @keyframes mouth-tremble { 0%, 100% { transform: translateX(-50%) scaleX(1); } 50% { transform: translateX(-50%) scaleX(0.9); } } /* 눈물 - 곡선 흐름 */ .tear-drop { position: absolute; width: 10px; height: 16px; background: linear-gradient(180deg, #74b9ff, #0984e3); border-radius: 50% 50% 50% 50% / 30% 30% 70% 70%; animation: tear-fall 0.6s infinite; } .tear-drop.left { top: 40px; left: 16px; animation-delay: 0s; } .tear-drop.right { top: 40px; right: 16px; animation-delay: 0.3s; } @keyframes tear-fall { 0% { transform: translateY(0) scale(1); opacity: 1; } 100% { transform: translateY(25px) scale(0.5); opacity: 0; } } /* 스파크 - 별 모양 */ .spark-star { position: absolute; width: 22px; height: 22px; background: #fdcb6e; clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%); animation: spark-spin 0.2s infinite; } .spark-star.s1 { top: 55px; left: 42%; animation-delay: 0s; } .spark-star.s2 { top: 95px; left: 38%; animation-delay: 0.07s; width: 18px; height: 18px; } .spark-star.s3 { top: 75px; right: 40%; animation-delay: 0.14s; width: 16px; height: 16px; } @keyframes spark-spin { 0%, 100% { transform: scale(0.5) rotate(0deg); opacity: 0.3; } 50% { transform: scale(1.2) rotate(180deg); opacity: 1; } } /* ========================================== 🎉 안전 스테이지 - 디스코 파티 ========================================== */ .safe-stage { background: linear-gradient(180deg, #0c0c1e 0%, #1a1a3e 50%, #2d2d5a 100%); border: 4px solid #a29bfe; border-radius: 24px; padding: 20px 15px; margin: 10px 0; min-height: 260px; position: relative; overflow: hidden; display: flex; flex-direction: column; align-items: center; } /* 무지개 조명 빔 */ .rainbow-beam { position: absolute; width: 35px; height: 220px; opacity: 0.5; transform-origin: top center; animation: beam-swing 2.5s ease-in-out infinite; border-radius: 0 0 50% 50%; } .rainbow-beam.r1 { background: linear-gradient(180deg, #ff6b6b, transparent); left: 5%; animation-delay: 0s; } .rainbow-beam.r2 { background: linear-gradient(180deg, #feca57, transparent); left: 22%; animation-delay: 0.5s; } .rainbow-beam.r3 { background: linear-gradient(180deg, #48dbfb, transparent); left: 39%; animation-delay: 1s; } .rainbow-beam.r4 { background: linear-gradient(180deg, #ff9ff3, transparent); left: 56%; animation-delay: 1.5s; } .rainbow-beam.r5 { background: linear-gradient(180deg, #1dd1a1, transparent); left: 73%; animation-delay: 2s; } .rainbow-beam.r6 { background: linear-gradient(180deg, #a29bfe, transparent); left: 90%; animation-delay: 2.5s; } @keyframes beam-swing { 0%, 100% { transform: rotate(-20deg); opacity: 0.3; } 50% { transform: rotate(20deg); opacity: 0.6; } } /* 떨어지는 색종이 - 곡선 움직임 */ .confetti-piece { position: absolute; top: -20px; animation: confetti-swirl 2.5s linear infinite; } .confetti-piece::before { content: ''; display: block; width: 10px; height: 10px; border-radius: 2px; } .confetti-piece.c1 { left: 10%; animation-delay: 0s; } .confetti-piece.c1::before { background: #ff6b6b; } .confetti-piece.c2 { left: 25%; animation-delay: 0.3s; } .confetti-piece.c2::before { background: #feca57; border-radius: 50%; } .confetti-piece.c3 { left: 40%; animation-delay: 0.6s; } .confetti-piece.c3::before { background: #48dbfb; } .confetti-piece.c4 { left: 55%; animation-delay: 0.9s; } .confetti-piece.c4::before { background: #ff9ff3; border-radius: 50%; } .confetti-piece.c5 { left: 70%; animation-delay: 1.2s; } .confetti-piece.c5::before { background: #1dd1a1; } .confetti-piece.c6 { left: 85%; animation-delay: 1.5s; } .confetti-piece.c6::before { background: #a29bfe; border-radius: 50%; } @keyframes confetti-swirl { 0% { transform: translateY(0) rotate(0deg) translateX(0); opacity: 1; } 25% { transform: translateY(65px) rotate(180deg) translateX(15px); } 50% { transform: translateY(130px) rotate(360deg) translateX(-10px); } 75% { transform: translateY(195px) rotate(540deg) translateX(20px); } 100% { transform: translateY(260px) rotate(720deg) translateX(0); opacity: 0; } } /* 디스코볼 */ .disco-wrap { position: relative; margin-bottom: 15px; z-index: 10; } .disco-string { width: 3px; height: 25px; background: linear-gradient(180deg, #dfe6e9, #636e72); margin: 0 auto; } .disco-ball { width: 55px; height: 55px; background: radial-gradient(circle at 30% 30%, #fff 0%, #dfe6e9 30%, #b2bec3 60%, #636e72 100%); border-radius: 50%; position: relative; box-shadow: 0 0 25px #fff, 0 0 50px #a29bfe, 0 0 75px #fd79a8; animation: disco-spin 3s linear infinite; overflow: hidden; } @keyframes disco-spin { 100% { transform: rotateY(360deg); } } .disco-tiles { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: repeating-linear-gradient(0deg, transparent 0px, transparent 6px, rgba(255,255,255,0.5) 6px, rgba(255,255,255,0.5) 8px), repeating-linear-gradient(90deg, transparent 0px, transparent 6px, rgba(255,255,255,0.5) 6px, rgba(255,255,255,0.5) 8px); border-radius: 50%; } /* 반사광 점 */ .disco-sparkle { position: absolute; width: 5px; height: 5px; background: #fff; border-radius: 50%; animation: sparkle-twinkle 0.8s infinite; } .disco-sparkle.ds1 { top: 10px; left: 12px; animation-delay: 0s; } .disco-sparkle.ds2 { top: 25px; right: 10px; animation-delay: 0.2s; } .disco-sparkle.ds3 { bottom: 15px; left: 20px; animation-delay: 0.4s; } .disco-sparkle.ds4 { bottom: 10px; right: 18px; animation-delay: 0.6s; } @keyframes sparkle-twinkle { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.3; transform: scale(0.5); } } /* 댄스 플로어 */ .dance-floor { display: flex; justify-content: space-around; align-items: flex-end; width: 100%; z-index: 10; margin-top: 10px; } /* 댄서 (곡선 버전) */ .dancer-dude { display: flex; flex-direction: column; align-items: center; position: relative; } .dancer-dude.left { animation: dance-groove-l 0.4s ease-in-out infinite alternate; } .dancer-dude.right { animation: dance-groove-r 0.4s ease-in-out infinite alternate-reverse; } @keyframes dance-groove-l { 0% { transform: translateY(0) rotate(-12deg) scale(1); } 100% { transform: translateY(-15px) rotate(12deg) scale(1.05); } } @keyframes dance-groove-r { 0% { transform: translateY(-15px) rotate(12deg) scale(1.05); } 100% { transform: translateY(0) rotate(-12deg) scale(1); } } .dancer-head { width: 28px; height: 28px; background: radial-gradient(circle at 35% 35%, #fdcb6e, #f39c12); border-radius: 50%; position: relative; } /* 선글라스 */ .dancer-shades { position: absolute; top: 10px; left: 50%; transform: translateX(-50%); width: 22px; height: 8px; background: #2d3436; border-radius: 3px; } .dancer-shades::before, .dancer-shades::after { content: ''; position: absolute; width: 8px; height: 6px; background: linear-gradient(135deg, #74b9ff, #0984e3); top: 1px; border-radius: 2px; } .dancer-shades::before { left: 2px; } .dancer-shades::after { right: 2px; } .dancer-body { width: 20px; height: 30px; background: linear-gradient(180deg, #a29bfe, #6c5ce7); border-radius: 6px 6px 3px 3px; margin-top: -3px; position: relative; } /* 곡선 팔 (춤추는) */ .dancer-arm { position: absolute; width: 15px; height: 18px; border: 4px solid transparent; border-top: 4px solid #6c5ce7; border-radius: 50% 50% 0 0; top: 2px; } .dancer-arm.left { left: -12px; animation: arm-pump-l 0.4s ease-in-out infinite alternate; } .dancer-arm.right { right: -12px; transform: scaleX(-1); animation: arm-pump-r 0.4s ease-in-out infinite alternate-reverse; } @keyframes arm-pump-l { from { transform: rotate(-30deg); } to { transform: rotate(30deg); } } @keyframes arm-pump-r { from { transform: scaleX(-1) rotate(-30deg); } to { transform: scaleX(-1) rotate(30deg); } } /* 곡선 다리 */ .dancer-legs { display: flex; gap: 4px; margin-top: -2px; } .dancer-leg { width: 12px; height: 22px; border: 4px solid transparent; border-radius: 0 0 50% 50%; } .dancer-leg.left { border-left: 4px solid #6c5ce7; animation: leg-step-l 0.4s ease-in-out infinite alternate; } .dancer-leg.right { border-right: 4px solid #6c5ce7; animation: leg-step-r 0.4s ease-in-out infinite alternate-reverse; } @keyframes leg-step-l { from { transform: rotate(15deg); } to { transform: rotate(-10deg); } } @keyframes leg-step-r { from { transform: rotate(-15deg); } to { transform: rotate(10deg); } } /* 행복한 점 캐릭터 */ .party-mole { position: relative; width: 58px; height: 58px; background: radial-gradient(circle at 35% 35%, #636e72, #2d3436); border-radius: 50%; z-index: 10; animation: party-jump 0.45s ease-in-out infinite alternate; box-shadow: 0 0 0 5px #00b894, 0 0 30px rgba(0,184,148,0.7); } @keyframes party-jump { 0% { transform: translateY(0) scale(1) rotate(-5deg); } 100% { transform: translateY(-22px) scale(1.1) rotate(5deg); } } /* 초승달 눈 (행복) */ .party-eye { position: absolute; width: 14px; height: 7px; background: #fff; border-radius: 0 0 14px 14px; top: 18px; } .party-eye.left { left: 10px; } .party-eye.right { right: 10px; } /* 활짝 웃는 입 */ .party-mouth { position: absolute; width: 22px; height: 11px; border: 4px solid #fff; border-top: none; border-radius: 0 0 22px 22px; bottom: 12px; left: 50%; transform: translateX(-50%); animation: mouth-wide 0.45s ease-in-out infinite alternate; } @keyframes mouth-wide { from { width: 22px; } to { width: 26px; } } /* 볼터치 */ .party-blush { position: absolute; width: 10px; height: 6px; background: #e17055; border-radius: 50%; top: 30px; animation: blush-glow 0.45s infinite alternate; } .party-blush.left { left: 5px; } .party-blush.right { right: 5px; } @keyframes blush-glow { from { opacity: 0.5; } to { opacity: 1; } } /* 곡선 팔 (만세) */ .party-arm { position: absolute; width: 18px; height: 25px; border: 5px solid transparent; top: -15px; } .party-arm.left { left: -15px; border-left: 5px solid #2d3436; border-radius: 50% 0 0 50%; animation: party-wave-l 0.3s ease-in-out infinite alternate; } .party-arm.right { right: -15px; border-right: 5px solid #2d3436; border-radius: 0 50% 50% 0; animation: party-wave-r 0.3s ease-in-out infinite alternate-reverse; } @keyframes party-wave-l { from { transform: rotate(-10deg); } to { transform: rotate(-40deg); } } @keyframes party-wave-r { from { transform: rotate(10deg); } to { transform: rotate(40deg); } } /* ========================================== ℹ️ 정보 화면 - 읽는 캐릭터 ========================================== */ .info-stage { display: flex; justify-content: center; align-items: center; margin: 15px 20px; padding: 15px; background: linear-gradient(135deg, #55efc4, #00b894); border-radius: 20px; border: 3px solid #00b894; } .reading-mole { position: relative; width: 50px; height: 50px; background: radial-gradient(circle at 35% 35%, #636e72, #2d3436); border-radius: 50%; animation: reading-nod 1.5s ease-in-out infinite; } @keyframes reading-nod { 0%, 100% { transform: rotate(0deg); } 25% { transform: rotate(-5deg); } 75% { transform: rotate(5deg); } } .reading-eye { position: absolute; width: 10px; height: 10px; background: #fff; border-radius: 50%; top: 16px; } .reading-eye.left { left: 10px; } .reading-eye.right { right: 10px; } .reading-pupil { position: absolute; width: 5px; height: 5px; background: #2d3436; border-radius: 50%; top: 20px; animation: reading-scan 2s ease-in-out infinite; } .reading-pupil.left { left: 12px; } .reading-pupil.right { right: 12px; } @keyframes reading-scan { 0%, 100% { transform: translateX(-2px); } 50% { transform: translateX(2px); } } /* 안경 */ .reading-glasses { position: absolute; top: 14px; left: 50%; transform: translateX(-50%); width: 36px; height: 14px; border: 3px solid #fdcb6e; border-radius: 8px; background: transparent; } .reading-glasses::before { content: ''; position: absolute; width: 6px; height: 3px; background: #fdcb6e; top: 4px; left: 50%; transform: translateX(-50%); } /* 책 */ .reading-book { position: absolute; bottom: -15px; left: 50%; transform: translateX(-50%); width: 30px; height: 20px; background: linear-gradient(90deg, #dfe6e9 50%, #fff 50%); border: 2px solid #636e72; border-radius: 2px; animation: book-flip 2s ease-in-out infinite; } @keyframes book-flip { 0%, 45%, 55%, 100% { transform: translateX(-50%) rotateY(0deg); } 50% { transform: translateX(-50%) rotateY(20deg); } } /* ========================================== ⚠️ 주의사항 화면 - 경고 캐릭터 ========================================== */ .warning-stage { display: flex; justify-content: center; align-items: center; margin: 15px 20px; padding: 15px; background: linear-gradient(135deg, #ffeaa7, #fdcb6e); border-radius: 20px; border: 3px solid #f39c12; position: relative; } .warning-mole { position: relative; width: 50px; height: 50px; background: radial-gradient(circle at 35% 35%, #636e72, #2d3436); border-radius: 50%; animation: warning-shake 0.5s ease-in-out infinite; } @keyframes warning-shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px) rotate(-5deg); } 75% { transform: translateX(5px) rotate(5deg); } } .warning-eye { position: absolute; width: 12px; height: 12px; background: #fff; border-radius: 50%; top: 14px; } .warning-eye.left { left: 10px; } .warning-eye.right { right: 10px; } .warning-pupil { position: absolute; width: 6px; height: 6px; background: #2d3436; border-radius: 50%; top: 17px; } .warning-pupil.left { left: 13px; } .warning-pupil.right { right: 13px; } /* O 입 */ .warning-mouth { position: absolute; width: 12px; height: 12px; border: 3px solid #fff; border-radius: 50%; bottom: 10px; left: 50%; transform: translateX(-50%); animation: mouth-oh 0.5s infinite; } @keyframes mouth-oh { 0%, 100% { transform: translateX(-50%) scale(1); } 50% { transform: translateX(-50%) scale(1.2); } } /* 곡선 손 (양손 X) */ .warning-arm { position: absolute; width: 20px; height: 25px; border: 4px solid transparent; top: 10px; } .warning-arm.left { left: -18px; border-left: 4px solid #2d3436; border-radius: 50% 0 0 50%; animation: arm-cross-l 0.5s ease-in-out infinite; } .warning-arm.right { right: -18px; border-right: 4px solid #2d3436; border-radius: 0 50% 50% 0; animation: arm-cross-r 0.5s ease-in-out infinite; } @keyframes arm-cross-l { 0%, 100% { transform: rotate(20deg); } 50% { transform: rotate(-20deg); } } @keyframes arm-cross-r { 0%, 100% { transform: rotate(-20deg); } 50% { transform: rotate(20deg); } } /* 경고 삼각형 */ .warning-sign { position: absolute; top: -25px; right: -10px; width: 0; height: 0; border-left: 15px solid transparent; border-right: 15px solid transparent; border-bottom: 26px solid #d63031; animation: sign-blink 0.8s infinite; } .warning-sign::after { content: '!'; position: absolute; top: 8px; left: -4px; color: #fff; font-weight: 900; font-size: 14px; } @keyframes sign-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } /* 배너 */ .verdict-banner { text-align: center; font-size: 1.1em; font-weight: 800; padding: 14px 20px; border-radius: 16px; margin-top: 12px; } .verdict-banner.danger { background: linear-gradient(135deg, #ffeaa7, #fab1a0); color: #d63031; border: 3px solid #e17055; animation: banner-shake 0.3s infinite; } @keyframes banner-shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-2px); } 75% { transform: translateX(2px); } } .verdict-banner.safe { background: linear-gradient(135deg, #dfe6e9, #b2bec3); color: #00b894; border: 3px solid #55efc4; animation: banner-glow 1.5s infinite; } @keyframes banner-glow { 0%, 100% { box-shadow: 0 0 10px rgba(0,184,148,0.3); } 50% { box-shadow: 0 0 25px rgba(0,184,148,0.6); } } /* 버튼 */ .gradio-container button.primary { background: linear-gradient(135deg, #6c5ce7 0%, #a29bfe 100%) !important; border: none !important; border-radius: 20px !important; font-weight: 800 !important; font-size: 1.05em !important; padding: 12px 28px !important; box-shadow: 0 6px 20px rgba(108,92,231,0.4) !important; transition: all 0.3s ease !important; } .gradio-container button.primary:hover { transform: translateY(-3px) !important; box-shadow: 0 10px 30px rgba(108,92,231,0.5) !important; } """ # --- 분석 로직 --- def run_diagnosis(image): if image is None: top_bar = """
오류
사진 없음
오류
""" art_illustration = """
사진이 전송되지 않았습니다.
이전 화면으로 돌아가 사진을 다시 선택해주세요.
""" verdict_msg = "
사진을 업로드하거나 촬영해주세요
" return top_bar, art_illustration, verdict_msg, 0 # 이미지 로드 및 변환 if isinstance(image, str): img = Image.open(image).convert('RGB') else: 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()) final = sum(probs[k]*w/total for k,w in MY_WEIGHTS.items()) score = int(round(final * 100)) if score >= 50: top_bar = f"""
당신의 점이 흑색종일 가능성
{score}%
나쁨
""" art_illustration = """
""" verdict_msg = "
피부과 방문을 권고드립니다
" else: top_bar = f"""
당신의 점이 흑색종일 가능성
{score}%
좋음
""" art_illustration = """
""" verdict_msg = "
이상 무! 아주 깨끗하고 안전합니다
" return top_bar, art_illustration, verdict_msg, score # --- 네비게이션 --- def move_s2(): return gr.update(visible=False), gr.update(visible=True) def move_s3(): return gr.update(visible=False), gr.update(visible=True) def move_s4(img): t, a, m, s = run_diagnosis(img) return gr.update(visible=False), gr.update(visible=True), t, a, m, s def move_s5(): return gr.update(visible=False), gr.update(visible=True) def move_s6(): return gr.update(visible=False), gr.update(visible=True) def back_home(): return gr.update(visible=True), gr.update(visible=False), None # --- UI --- with gr.Blocks(css=css, theme=gr.themes.Default()) as demo: # 화면 1: 홈 with gr.Column(visible=True) as s1: gr.HTML("
점벼라 세상아!
") gr.HTML("""
""") gr.HTML("
스마트폰으로 간편하게
흑색종 위험 판단하기
") btn_s1 = gr.Button("점 진단하기", variant="primary", size="lg") # 화면 2: 설문 with gr.Column(visible=False) as s2: gr.HTML("""
?
""") gr.Markdown("### 단계 1. 설문 작성") age = gr.Number(label="당신의 만나이는?", value=24, interactive=True) history = gr.Radio(["있음", "없음"], label="가족 내 피부암 환자가 있었나요?", value="없음", interactive=True) change = gr.Radio(["크기가 크게 확장됨", "색상이 얼룩덜룩해짐", "경계선이 뭉개짐", "변화 없음"], label="최근 해당 점의 형태적 변화가 있었습니까?", value="변화 없음", interactive=True) btn_s2 = gr.Button("다음 단계", variant="primary") # 화면 3: 촬영 with gr.Column(visible=False) as s3: gr.HTML("""
z
Z
z
""") gr.Markdown("### 단계 2. 이미지 촬영") image_input = gr.Image(sources=["upload", "webcam"], type="filepath", label="카메라로 촬영하거나 사진 선택") btn_s3 = gr.Button("진단 실행", variant="primary") # 화면 4: 결과 with gr.Column(visible=False) as s4: out_top = gr.HTML() out_anim = gr.HTML() out_msg = gr.HTML() score_state = gr.State() # AI 점수 저장용 btn_s4 = gr.Button("다음", variant="primary") # 화면 5: 정보 with gr.Column(visible=False) as s5: gr.HTML("""
""") gr.Markdown("### 흑색종 자가 진단 가이드 (ABCDE)") gr.Markdown(""" * **A (Asymmetry) 비대칭**: 모양이 동그랗지 않고 한쪽으로 일그러짐 * **B (Border) 불규칙 경계**: 가장자리가 매끄럽지 못함 * **C (Color) 다채로운 색**: 연갈색, 검은색, 붉은색 등이 뒤섞임 * **D (Diameter) 큰 지름**: 직경이 6mm 이상 * **E (Evolution) 지속적 변화**: 크기와 깊이가 계속 변함 --- 초기 발견 시 **외과 절제 수술만으로 완치 가능**, 전이 시 **치명적**입니다. """) btn_s5 = gr.Button("다음", variant="primary") # 화면 6: 주의사항 with gr.Column(visible=False) as s6: gr.HTML("""
""") gr.Markdown("### 프로그램 사용 주의사항") gr.Markdown(""" 본 시뮬레이터의 수치는 **의학적 진단을 대체할 수 없습니다.** 이상이 있다면 **즉시 피부과를 방문**하세요. --- **제작자:** jj """) btn_s6 = gr.Button("처음 화면으로") btn_s1.click(move_s2, inputs=None, outputs=[s1, s2]) btn_s2.click(move_s3, inputs=None, outputs=[s2, s3]) btn_s3.click(move_s4, inputs=[image_input], outputs=[s3, s4, out_top, out_anim, out_msg, score_state]) btn_s4.click(move_s5, inputs=None, outputs=[s4, s5]) btn_s5.click(move_s6, inputs=None, outputs=[s5, s6]) btn_s6.click(back_home, inputs=None, outputs=[s1, s6, image_input]) demo.launch(debug=True)