# ============================================================
# 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 = "이 점은 평온한 점이라네. 일반적인 모반으로 보이오. 평소처럼 자가 관찰을 이어가시오."
tag_color = "#3F8B5F" # 부드러운 초록
tag_bg = "#EDF6EF"
elif final < 0.50:
tag = "주의 (注意)"
msg = "조금 신경 쓰이는 기운이 있구려. 가까운 피부과에서 한번 봐달라 하시오."
tag_color = "#C9722E" # 따뜻한 주황
tag_bg = "#FCF1E0"
else:
tag = "흉조 (凶兆)"
msg = "이 점은 좀 살펴봐야 할 것 같소. 가능한 빨리 피부과 전문의에게 진료를 받으시오."
tag_color = "#B5394A" # 무섭지 않은 짙은 주홍
tag_bg = "#FBEAEC"
detail_html = f"""
🔮 {tag}
{msg}
📜 점괘 풀이
AI 무당의 직관: {ai_prob*100:.1f}%
운수(문진) 가산: +{bonus*100:.1f}%
─ 최종 흉조 정도: {final*100:.1f}%
"어서 오시오, 손님.
그 점, 무당이 봐드리리다." — AI 무당 5명이 합심하여 점괘를 풉니다
""")
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("""
📋 운수(運數) 확인
1 / 3 · 점괘 풀기 전 사주 묻기
— 무당의 첫 질문 —
사주를 알아야 점이 더 정확하다오. 솔직하게 답해주시오.
""")
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("""
📸 점(點) 사진
2 / 3 · 무당에게 점을 보여주오
— 점을 가까이 비추시오 —
밝은 곳에서, 점이 화면에 가득 차게 찍어주시오. 카메라로 찍거나 갤러리에서 골라도 좋소.
""")
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("""