jun12124 commited on
Commit
287d8d3
ยท
verified ยท
1 Parent(s): 599525e

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +592 -0
app.py ADDED
@@ -0,0 +1,592 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # app.py โ€” ๊ทธ๋กœ์„ผ๋”• AI Day 3 (์ ์ง‘ ์ปจ์…‰ ยท ๋”ฐ๋œปํ•œ ๋ฒ„์ „)
3
+ # ์ž์—ฐ์–ด ํ”„๋กฌํ”„ํŠธ ๊ธฐ๋ฐ˜ ๋””์ž์ธ / ๋ฌด์„ญ์ง€ ์•Š๊ณ  ์นœ๊ทผํ•œ ์ ์ง‘
4
+ # ============================================================
5
+
6
+ import warnings
7
+ warnings.filterwarnings("ignore", message=".*HF_TOKEN.*")
8
+
9
+ import gradio as gr
10
+ import torch, torch.nn as nn, timm
11
+ from torchvision import models, transforms
12
+ from huggingface_hub import hf_hub_download
13
+ from PIL import Image
14
+ import numpy as np
15
+
16
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
17
+ # 1. 5๊ฐœ AI ๋ชจ๋ธ ๋กœ๋“œ (Space ์‹œ์ž‘ ์‹œ 1๋ฒˆ๋งŒ) โ€ป ๊ทธ๋Œ€๋กœ ์œ ์ง€
18
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
19
+ REPO_ID = "junkim1209/isic-models"
20
+ MODEL_NAMES = ['efficientnet_b3', 'efficientnet_b5',
21
+ 'convnext_tiny', 'convnext_base', 'swin_base']
22
+
23
+ def build_model(name):
24
+ if name == 'efficientnet_b3':
25
+ m = models.efficientnet_b3(weights=None)
26
+ m.classifier[1] = nn.Linear(m.classifier[1].in_features, 1); return m, 300
27
+ elif name == 'efficientnet_b5':
28
+ return timm.create_model('efficientnet_b5', pretrained=False, num_classes=1), 456
29
+ elif name == 'convnext_tiny':
30
+ m = models.convnext_tiny(weights=None)
31
+ m.classifier[2] = nn.Linear(m.classifier[2].in_features, 1); return m, 224
32
+ elif name == 'convnext_base':
33
+ return timm.create_model('convnext_base', pretrained=False, num_classes=1), 224
34
+ elif name == 'swin_base':
35
+ return timm.create_model('swin_base_patch4_window12_384',
36
+ pretrained=False, num_classes=1), 384
37
+
38
+ print("๐Ÿ“ฅ ๋ฌด๋‹น 5๋ช… ๋ชจ์‹œ๋Š” ์ค‘...")
39
+ LOADED = {}
40
+ for name in MODEL_NAMES:
41
+ path = hf_hub_download(repo_id=REPO_ID, filename=f"{name}.pth")
42
+ model, size = build_model(name)
43
+ state = torch.load(path, map_location='cpu', weights_only=False)
44
+ model.load_state_dict(state['model_state_dict'], strict=True)
45
+ model.eval()
46
+ LOADED[name] = {'model': model, 'size': size}
47
+ print("โœ… ์ ์ง‘ ๊ฐœ์‹œ ์ค€๋น„ ์™„๋ฃŒ")
48
+
49
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
50
+ # 2. ๋ณธ์ธ 1์ผ์ฐจ ๊ฐ€์ค‘์น˜
51
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
52
+ MY_WEIGHTS = {
53
+ 'efficientnet_b3': 1.0, 'efficientnet_b5': 1.0,
54
+ 'convnext_tiny': 1.0, 'convnext_base': 1.0,
55
+ 'swin_base': 1.0,
56
+ }
57
+
58
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
59
+ # 3. ์ถ”๋ก  + ๋ฌธ์ง„ ๊ฒฐํ•ฉ โ€ป predict ๋ณธ์ฒด๋Š” ์œ ์ง€ํ•˜๊ณ  ํ•จ์ˆ˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋งŒ ํ™•์žฅ
60
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
61
+ def predict_with_questionnaire(image, age, family_history, mole_change):
62
+ if image is None:
63
+ return None, ""
64
+
65
+ img = Image.fromarray(image).convert('RGB')
66
+ probs = {}
67
+ for name, info in LOADED.items():
68
+ tfm = transforms.Compose([
69
+ transforms.Resize((info['size'], info['size'])),
70
+ transforms.ToTensor(),
71
+ transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
72
+ ])
73
+ x = tfm(img).unsqueeze(0)
74
+ with torch.no_grad():
75
+ probs[name] = torch.sigmoid(info['model'](x)).item()
76
+
77
+ total = sum(MY_WEIGHTS.values())
78
+ ai_prob = sum(probs[k] * w / total for k, w in MY_WEIGHTS.items())
79
+
80
+ bonus = 0.0
81
+ if family_history == "์˜ˆ": bonus += 0.10
82
+ if mole_change == "์˜ˆ": bonus += 0.15
83
+ if age >= 50: bonus += 0.05
84
+ final = min(ai_prob + bonus, 1.0)
85
+
86
+ label_data = {
87
+ "ํ‰(ๅ‡ถ) โ€” ๋ฉœ๋ผ๋…ธ๋งˆ ์˜์‹ฌ": float(final),
88
+ "๊ธธ(ๅ‰) โ€” ์–‘์„ฑ ๋ชจ๋ฐ˜": float(1 - final),
89
+ }
90
+
91
+ # ์œ„ํ—˜๋„๋ณ„ ์ ๊ด˜ ํ’€์ด (๋”ฐ๋œปํ•œ ํ†ค)
92
+ if final < 0.20:
93
+ tag = "๊ธธ์กฐ (ๅ‰ๅ…†)"
94
+ msg = "์ด ์ ์€ ํ‰์˜จํ•œ ์ ์ด๋ผ๋„ค.<br>์ผ๋ฐ˜์ ์ธ ๋ชจ๋ฐ˜์œผ๋กœ ๋ณด์ด์˜ค. ํ‰์†Œ์ฒ˜๋Ÿผ ์ž๊ฐ€ ๊ด€์ฐฐ์„ ์ด์–ด๊ฐ€์‹œ์˜ค."
95
+ tag_color = "#3F8B5F" # ๋ถ€๋“œ๋Ÿฌ์šด ์ดˆ๋ก
96
+ tag_bg = "#EDF6EF"
97
+ elif final < 0.50:
98
+ tag = "์ฃผ์˜ (ๆณจๆ„)"
99
+ msg = "์กฐ๊ธˆ ์‹ ๊ฒฝ ์“ฐ์ด๋Š” ๊ธฐ์šด์ด ์žˆ๊ตฌ๋ ค.<br>๊ฐ€๊นŒ์šด ํ”ผ๋ถ€๊ณผ์—์„œ ํ•œ๋ฒˆ ๋ด๋‹ฌ๋ผ ํ•˜์‹œ์˜ค."
100
+ tag_color = "#C9722E" # ๋”ฐ๋œปํ•œ ์ฃผํ™ฉ
101
+ tag_bg = "#FCF1E0"
102
+ else:
103
+ tag = "ํ‰์กฐ (ๅ‡ถๅ…†)"
104
+ msg = "์ด ์ ์€ ์ข€ ์‚ดํŽด๋ด์•ผ ํ•  ๊ฒƒ ๊ฐ™์†Œ.<br>๊ฐ€๋Šฅํ•œ ๋นจ๋ฆฌ ํ”ผ๋ถ€๊ณผ ์ „๋ฌธ์˜์—๊ฒŒ ์ง„๋ฃŒ๋ฅผ ๋ฐ›์œผ์‹œ์˜ค."
105
+ tag_color = "#B5394A" # ๋ฌด์„ญ์ง€ ์•Š์€ ์ง™์€ ์ฃผํ™
106
+ tag_bg = "#FBEAEC"
107
+
108
+ detail_html = f"""
109
+ <div style='background:{tag_bg}; border-left:5px solid {tag_color};
110
+ padding: 16px 18px; border-radius: 12px; margin-top: 14px;'>
111
+ <div style='color:{tag_color}; font-weight:800; font-size:16px;
112
+ margin-bottom:8px; letter-spacing:1.5px;'>
113
+ ๐Ÿ”ฎ {tag}
114
+ </div>
115
+ <div style='color:#4A3826; font-size:13.5px; line-height:1.65;'>{msg}</div>
116
+ </div>
117
+
118
+ <div style='background:#FFFBF0; padding:14px 16px; border-radius:12px;
119
+ margin-top:12px; font-size:12.5px; color:#6B4423;
120
+ border: 1px dashed #D4B873;'>
121
+ <div style='font-weight:700; margin-bottom:8px; color:#A8462C;
122
+ letter-spacing: 1px;'>๐Ÿ“œ ์ ๊ด˜ ํ’€์ด</div>
123
+ AI ๋ฌด๋‹น์˜ ์ง๊ด€: <strong>{ai_prob*100:.1f}%</strong><br>
124
+ ์šด์ˆ˜(๋ฌธ์ง„) ๊ฐ€์‚ฐ: <strong>+{bonus*100:.1f}%</strong><br>
125
+ โ”€ ์ตœ์ข… ํ‰์กฐ ์ •๋„: <strong style='color:{tag_color}; font-size:14px;'>{final*100:.1f}%</strong>
126
+ </div>
127
+ """
128
+ return label_data, detail_html
129
+
130
+
131
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
132
+ # 4. ์ ์ง‘ CSS โ€” ๋”ฐ๋œปํ•œ ํ•œ์ง€ยท์ฃผํ™ ํ†ค
133
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
134
+ custom_css = """
135
+ :root {
136
+ --brand: #C24536; /* ๋”ฐ๋œปํ•œ ์ฃผํ™ (ํ…Œ๋ผ์ฝ”ํƒ€) */
137
+ --brand-dark: #9E3528;
138
+ --brand-soft: #E8533D; /* ๋” ๋ฐ์€ ์ฃผํ™ (ํฌ์ธํŠธ) */
139
+ --gold: #D4A93B; /* ๋ถ€๋“œ๋Ÿฌ์šด ๊ธˆ์ƒ‰ */
140
+ --gold-light: #E8C868;
141
+ --paper: #FFF8E7; /* ํ•œ์ง€์ƒ‰ */
142
+ --paper-warm: #FCEFD3; /* ๋” ์ง„ํ•œ ํ•œ์ง€ */
143
+ --ink: #4A3826;
144
+ --ink-sub: #7A5C3D;
145
+ }
146
+
147
+ .gradio-container {
148
+ max-width: 430px !important;
149
+ margin: 0 auto !important;
150
+ padding: 0 !important;
151
+ background: var(--paper) !important;
152
+ min-height: 100vh !important;
153
+ font-family: 'Pretendard', -apple-system, BlinkMacSystemFont,
154
+ 'Noto Sans KR', sans-serif !important;
155
+ background-image:
156
+ radial-gradient(circle at 15% 8%, rgba(212,169,59,0.12) 0%, transparent 45%),
157
+ radial-gradient(circle at 85% 92%, rgba(232,83,61,0.08) 0%, transparent 45%);
158
+ }
159
+ footer { display: none !important; }
160
+
161
+ /* ํ•œ์ง€ ์œ„ ๋„์žฅ ๋А๋‚Œ ํ—ค๋” */
162
+ .scr-header {
163
+ background: linear-gradient(135deg, #C24536 0%, #9E3528 100%);
164
+ color: white;
165
+ padding: 22px 22px 20px;
166
+ margin-bottom: 14px;
167
+ position: relative;
168
+ border-radius: 0 0 16px 16px;
169
+ }
170
+ .scr-header::after {
171
+ content: "";
172
+ position: absolute; left: 18px; right: 18px; bottom: -2px; height: 2px;
173
+ background: repeating-linear-gradient(
174
+ 90deg, var(--gold) 0, var(--gold) 8px,
175
+ transparent 8px, transparent 14px
176
+ );
177
+ }
178
+ .scr-header .scr-title {
179
+ font-size: 22px;
180
+ font-weight: 800;
181
+ letter-spacing: 1.5px;
182
+ font-family: 'Noto Serif KR', 'Nanum Myeongjo', serif;
183
+ }
184
+ .scr-header .scr-sub {
185
+ font-size: 12.5px;
186
+ opacity: 0.94;
187
+ margin-top: 5px;
188
+ letter-spacing: 0.3px;
189
+ }
190
+
191
+ /* ์ง„ํ–‰ ํ‘œ์‹œ */
192
+ .progress-dots {
193
+ display: flex;
194
+ gap: 7px;
195
+ margin: 6px 18px 14px;
196
+ justify-content: center;
197
+ }
198
+ .progress-dot {
199
+ width: 28px;
200
+ height: 5px;
201
+ border-radius: 3px;
202
+ background: #ECDBB0;
203
+ }
204
+ .progress-dot.active {
205
+ background: var(--gold);
206
+ box-shadow: 0 0 8px rgba(212,169,59,0.5);
207
+ }
208
+
209
+ /* ํ•œ์ง€ ์นด๋“œ */
210
+ .app-card {
211
+ background: white;
212
+ border-radius: 14px;
213
+ padding: 16px 18px;
214
+ margin: 0 18px 12px;
215
+ border: 1px solid #EEDFB8;
216
+ box-shadow: 0 2px 8px rgba(194,69,54,0.06);
217
+ }
218
+ .app-card-title {
219
+ font-size: 14.5px;
220
+ font-weight: 800;
221
+ color: var(--brand);
222
+ margin-bottom: 6px;
223
+ letter-spacing: 0.5px;
224
+ font-family: 'Noto Serif KR', serif;
225
+ }
226
+ .app-card-desc {
227
+ font-size: 12.8px;
228
+ color: var(--ink-sub);
229
+ line-height: 1.7;
230
+ }
231
+
232
+ /* ๋””์Šคํด๋ ˆ์ด๋จธ โ€” ๋ถ€์  ๋А๋‚Œ */
233
+ .disclaimer {
234
+ background: #FFF3DE;
235
+ color: #8B5A2B;
236
+ padding: 11px 14px;
237
+ border-radius: 10px;
238
+ margin: 0 18px 12px;
239
+ font-size: 12px;
240
+ font-weight: 600;
241
+ text-align: center;
242
+ border: 1.5px dashed #D4A93B;
243
+ }
244
+
245
+ /* ํ™ˆ ํžˆ์–ด๋กœ */
246
+ .jeomjip-hero {
247
+ text-align: center;
248
+ padding: 12px 24px 12px;
249
+ }
250
+ .mudang-svg {
251
+ width: 240px;
252
+ max-width: 100%;
253
+ height: auto;
254
+ filter: drop-shadow(0 4px 8px rgba(0,0,0,0.08));
255
+ }
256
+ .hero-title {
257
+ font-size: 30px;
258
+ font-weight: 900;
259
+ color: var(--brand);
260
+ margin: 14px 0 8px;
261
+ letter-spacing: 3px;
262
+ font-family: 'Noto Serif KR', 'Nanum Myeongjo', serif;
263
+ }
264
+ .hero-sub {
265
+ font-size: 14px;
266
+ color: var(--ink-sub);
267
+ line-height: 1.75;
268
+ margin-bottom: 4px;
269
+ }
270
+ .hero-quote {
271
+ color: var(--brand);
272
+ font-weight: 700;
273
+ font-style: italic;
274
+ display: inline-block;
275
+ margin-top: 8px;
276
+ }
277
+
278
+ /* ๋„์žฅ ๋А๋‚Œ ์ฃผ์š” ๋ฒ„ํŠผ */
279
+ button.primary-btn {
280
+ background: var(--brand) !important;
281
+ color: white !important;
282
+ border: none !important;
283
+ padding: 15px !important;
284
+ font-size: 16px !important;
285
+ font-weight: 800 !important;
286
+ border-radius: 14px !important;
287
+ min-height: 56px !important;
288
+ margin: 0 18px 10px !important;
289
+ width: calc(100% - 36px) !important;
290
+ letter-spacing: 2.5px !important;
291
+ box-shadow: 0 4px 0 var(--brand-dark),
292
+ 0 6px 12px rgba(194,69,54,0.18) !important;
293
+ font-family: 'Noto Serif KR', serif !important;
294
+ transition: all 0.15s !important;
295
+ }
296
+ button.primary-btn:hover {
297
+ background: var(--brand-soft) !important;
298
+ transform: translateY(2px);
299
+ box-shadow: 0 2px 0 var(--brand-dark),
300
+ 0 3px 6px rgba(194,69,54,0.18) !important;
301
+ }
302
+
303
+ /* ์ข…์ด ๋А๋‚Œ ๋ณด์กฐ ๋ฒ„ํŠผ */
304
+ button.secondary-btn {
305
+ background: white !important;
306
+ color: var(--brand) !important;
307
+ border: 2px solid var(--brand) !important;
308
+ padding: 13px !important;
309
+ font-size: 14.5px !important;
310
+ font-weight: 800 !important;
311
+ border-radius: 14px !important;
312
+ min-height: 50px !important;
313
+ margin: 0 18px 10px !important;
314
+ width: calc(100% - 36px) !important;
315
+ letter-spacing: 1.5px !important;
316
+ font-family: 'Noto Serif KR', serif !important;
317
+ }
318
+ button.secondary-btn:hover {
319
+ background: #FFF3DE !important;
320
+ }
321
+
322
+ .row-buttons button { width: 100% !important; margin: 0 !important; }
323
+ .row-buttons { padding: 0 18px 10px; gap: 10px !important; }
324
+
325
+ label, .label, .gradio-container label { color: var(--ink) !important; }
326
+ """
327
+
328
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
329
+ # 5. ์นœ๊ทผํ•œ ๋ฌด๋‹น SVG โ€” ๋ฏธ์†Œ ๋ค ํ‘œ์ •
330
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
331
+ MUDANG_SVG = """
332
+ <svg class="mudang-svg" viewBox="0 0 260 230" xmlns="http://www.w3.org/2000/svg">
333
+ <!-- ๋ถ€์  ์  (๋ฐฐ๊ฒฝ ์žฅ์‹) -->
334
+ <circle cx="30" cy="40" r="3" fill="#D4A93B" opacity="0.4"/>
335
+ <circle cx="240" cy="60" r="2.5" fill="#D4A93B" opacity="0.4"/>
336
+ <circle cx="20" cy="180" r="2" fill="#C24536" opacity="0.3"/>
337
+
338
+ <!-- ๊นƒ๋Œ€ -->
339
+ <line x1="55" y1="30" x2="55" y2="210" stroke="#7A4F2A" stroke-width="5" stroke-linecap="round"/>
340
+ <circle cx="55" cy="30" r="6" fill="#D4A93B" stroke="#A8821A" stroke-width="0.6"/>
341
+
342
+ <!-- ๋งŒ์ž ๊นƒ๋ฐœ (๋ถ€๋“œ๋Ÿฌ์šด ๋นจ๊ฐ•) -->
343
+ <path d="M 55 38 L 142 38 L 130 65 L 142 92 L 55 92 Z"
344
+ fill="#C24536" stroke="#9E3528" stroke-width="1"/>
345
+ <text x="92" y="76" text-anchor="middle" fill="#FFF3DE"
346
+ font-size="34" font-weight="bold" font-family="serif">ๅ</text>
347
+ <!-- ๊นƒ๋ฐœ ๋ฐ”๋žŒ ํœ˜๋‚ ๋ฆผ ํ‘œ์‹œ -->
348
+ <path d="M 55 92 Q 60 96 55 100" stroke="#9E3528" stroke-width="1.5" fill="none"/>
349
+
350
+ <!-- ๊ทธ๋ฆผ์ž -->
351
+ <ellipse cx="175" cy="210" rx="42" ry="5" fill="#4A3826" opacity="0.13"/>
352
+
353
+ <!-- ํ•œ๋ณต ์น˜๋งˆ (๋ฐ์€ ์ฃผํ™) -->
354
+ <path d="M 138 138 Q 175 132 212 138 L 222 205 Q 175 211 128 205 Z"
355
+ fill="#E8533D" stroke="#9E3528" stroke-width="1"/>
356
+ <!-- ์น˜๋งˆ ๋ฌด๋Šฌ (๊ฝƒ์žŽ) -->
357
+ <circle cx="155" cy="175" r="2.5" fill="#D4A93B"/>
358
+ <circle cx="175" cy="185" r="2.5" fill="#D4A93B"/>
359
+ <circle cx="195" cy="172" r="2.5" fill="#D4A93B"/>
360
+ <path d="M 140 195 Q 175 192 210 195" stroke="#D4A93B" stroke-width="1.2" fill="none" opacity="0.6"/>
361
+
362
+ <!-- ๊ธˆ์ƒ‰ ๋  -->
363
+ <rect x="133" y="130" width="84" height="10" fill="#D4A93B" rx="1"/>
364
+ <rect x="133" y="130" width="84" height="10" fill="none" stroke="#A8821A" stroke-width="0.5" rx="1"/>
365
+
366
+ <!-- ์ €๊ณ ๋ฆฌ (๋”ฐ๋œปํ•œ ํฐ์ƒ‰) -->
367
+ <path d="M 143 95 Q 175 90 207 95 L 210 132 L 140 132 Z"
368
+ fill="#FFF8E7" stroke="#7A4F2A" stroke-width="1"/>
369
+ <!-- ์ €๊ณ ๋ฆฌ ๊นƒ (์ฃผํ™) -->
370
+ <path d="M 165 95 L 175 112 L 185 95" fill="#E8533D" stroke="#C24536" stroke-width="1.5" stroke-linejoin="round"/>
371
+
372
+ <!-- ์™ผํŒ” (๊นƒ๋Œ€ ์žก๊ธฐ) -->
373
+ <path d="M 143 105 Q 100 100 65 65" stroke="#FFF8E7" stroke-width="12"
374
+ fill="none" stroke-linecap="round"/>
375
+ <path d="M 143 105 Q 100 100 65 65" stroke="#7A4F2A" stroke-width="1"
376
+ fill="none" stroke-linecap="round" opacity="0.25"/>
377
+ <circle cx="63" cy="65" r="7" fill="#F5C9A0" stroke="#7A4F2A" stroke-width="0.8"/>
378
+
379
+ <!-- ์˜ค๋ฅธํŒ” (๋ฐ˜๊ฐ‘๊ฒŒ ๋“ค๊ธฐ) -->
380
+ <path d="M 205 105 Q 230 105 235 125" stroke="#FFF8E7" stroke-width="12"
381
+ fill="none" stroke-linecap="round"/>
382
+ <path d="M 205 105 Q 230 105 235 125" stroke="#7A4F2A" stroke-width="1"
383
+ fill="none" stroke-linecap="round" opacity="0.25"/>
384
+ <!-- ์† ํ”๋“œ๋Š” ๋ชจ์–‘ -->
385
+ <circle cx="237" cy="128" r="7" fill="#F5C9A0" stroke="#7A4F2A" stroke-width="0.8"/>
386
+ <!-- ์†๊ฐ€๋ฝ ํ‘œ์‹œ -->
387
+ <line x1="235" y1="122" x2="234" y2="118" stroke="#7A4F2A" stroke-width="1" stroke-linecap="round"/>
388
+ <line x1="240" y1="122" x2="241" y2="118" stroke="#7A4F2A" stroke-width="1" stroke-linecap="round"/>
389
+
390
+ <!-- ์–ผ๊ตด (๋‘ฅ๊ธ€๊ณ  ๋”ฐ๋œปํ•˜๊ฒŒ) -->
391
+ <ellipse cx="175" cy="73" rx="22" ry="24" fill="#F5C9A0" stroke="#7A4F2A" stroke-width="1"/>
392
+
393
+ <!-- ๋จธ๋ฆฌ์นด๋ฝ -->
394
+ <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"
395
+ fill="#3A2415"/>
396
+ <!-- ๋น„๋…€ -->
397
+ <line x1="190" y1="51" x2="208" y2="40" stroke="#D4A93B" stroke-width="2.8" stroke-linecap="round"/>
398
+ <circle cx="208" cy="40" r="3" fill="#D4A93B" stroke="#A8821A" stroke-width="0.5"/>
399
+
400
+ <!-- ์ด๋งˆ ์  (์ž‘์€ ๋นจ๊ฐ„์ , ๋ถ€์  ์˜๋ฏธ) -->
401
+ <circle cx="175" cy="63" r="2.4" fill="#C24536"/>
402
+
403
+ <!-- ์›ƒ๋Š” ๋ˆˆ (๋ฐ˜๋‹ฌ ๋ชจ์–‘) -->
404
+ <path d="M 165 74 Q 168 71 171 74" stroke="#3A2415" stroke-width="2"
405
+ fill="none" stroke-linecap="round"/>
406
+ <path d="M 179 74 Q 182 71 185 74" stroke="#3A2415" stroke-width="2"
407
+ fill="none" stroke-linecap="round"/>
408
+
409
+ <!-- ์ž… (์›ƒ๋Š” ์ž…) -->
410
+ <path d="M 168 83 Q 175 88 182 83" stroke="#9E3528" stroke-width="2"
411
+ fill="none" stroke-linecap="round"/>
412
+
413
+ <!-- ๋ณผํ„ฐ์น˜ (๋ถ„ํ™) -->
414
+ <ellipse cx="159" cy="79" rx="3.5" ry="2.5" fill="#E89090" opacity="0.55"/>
415
+ <ellipse cx="191" cy="79" rx="3.5" ry="2.5" fill="#E89090" opacity="0.55"/>
416
+ </svg>
417
+ """
418
+
419
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
420
+ # 6. UI โ€” 5๊ฐœ ํ™”๋ฉด
421
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
422
+ with gr.Blocks(css=custom_css, title="์šฐ๋ฆฌ ๋™๋„ค ์ ์ง‘",
423
+ theme=gr.themes.Soft(primary_hue="red")) as demo:
424
+
425
+ # =============== ํ™”๋ฉด 1: ํ™ˆ ===============
426
+ with gr.Column(visible=True) as home_screen:
427
+ gr.HTML(f"""
428
+ <div class="scr-header">
429
+ <div class="scr-title">์šฐ๋ฆฌ ๋™๋„ค ์ ์ง‘</div>
430
+ <div class="scr-sub">๊ทธ๋กœ์„ผ๋”• ยท AI ๋ฌด๋‹น์ด ๋ด์ฃผ๋Š” ์ (้ปž) ์ (ๅ )</div>
431
+ </div>
432
+ <div class="disclaimer">โš ๏ธ ๊ต์œก์šฉ ๋„๊ตฌ ยท ์˜๋ฃŒ ์ง„๋‹จ ๋Œ€์ฒด ๋ถˆ๊ฐ€</div>
433
+ <div class="jeomjip-hero">
434
+ {MUDANG_SVG}
435
+ <div class="hero-title">์  ๋ณด๋Ÿฌ ์˜ค์…จ์†Œ</div>
436
+ <div class="hero-sub">
437
+ "์–ด์„œ ์˜ค์‹œ์˜ค, ์†๋‹˜.<br>
438
+ ๊ทธ ์ , ๋ฌด๋‹น์ด ๋ด๋“œ๋ฆฌ๋ฆฌ๋‹ค."<br>
439
+ <span class="hero-quote">โ€” AI ๋ฌด๋‹น 5๋ช…์ด ํ•ฉ์‹ฌํ•˜์—ฌ ์ ๊ด˜๋ฅผ ํ’‰๋‹ˆ๋‹ค</span>
440
+ </div>
441
+ </div>
442
+ """)
443
+ start_btn = gr.Button("๐Ÿ”ฎ ์ ๊ฒฌ์  ๋ณด๊ธฐ", elem_classes="primary-btn")
444
+ info_from_home = gr.Button("๐Ÿ“œ ์ ์ด๋ž€?", elem_classes="secondary-btn")
445
+
446
+ # =============== ํ™”๋ฉด 2: ์šด์ˆ˜ ํ™•์ธ (๋ฌธ์ง„) ===============
447
+ with gr.Column(visible=False) as questionnaire_screen:
448
+ gr.HTML("""
449
+ <div class="scr-header">
450
+ <div class="scr-title">๐Ÿ“‹ ์šด์ˆ˜(้‹ๆ•ธ) ํ™•์ธ</div>
451
+ <div class="scr-sub">1 / 3 ยท ์ ๊ด˜ ํ’€๊ธฐ ์ „ ์‚ฌ์ฃผ ๋ฌป๊ธฐ</div>
452
+ </div>
453
+ <div class="progress-dots">
454
+ <div class="progress-dot active"></div>
455
+ <div class="progress-dot"></div>
456
+ <div class="progress-dot"></div>
457
+ </div>
458
+ <div class="app-card">
459
+ <div class="app-card-title">โ€” ๋ฌด๋‹น์˜ ์ฒซ ์งˆ๋ฌธ โ€”</div>
460
+ <div class="app-card-desc">์‚ฌ์ฃผ๋ฅผ ์•Œ์•„์•ผ ์ ์ด ๋” ์ •ํ™•ํ•˜๋‹ค์˜ค. ์†”์งํ•˜๊ฒŒ ๋‹ตํ•ด์ฃผ์‹œ์˜ค.</div>
461
+ </div>
462
+ """)
463
+ with gr.Column(elem_classes="app-card"):
464
+ age = gr.Slider(0, 80, value=30, step=1, label="๐ŸŽ‚ ์†๋‹˜ ๋‚˜์ด๋Š” ์–ด์ฐŒ ๋˜์‹œ์˜ค?",
465
+ info="ํ‘์ƒ‰์ข…์€ ๋‚˜์ด๊ฐ€ ๋“ค์ˆ˜๋ก ๋ฐœ์ƒ ๋นˆ๋„๊ฐ€ ๋†’์•„์ง„๋‹ค์˜ค")
466
+ family = gr.Radio(["์•„๋‹ˆ์˜ค", "์˜ˆ"], value="์•„๋‹ˆ์˜ค",
467
+ label="๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง ์ง‘์•ˆ์— ํ‘์ƒ‰์ข… ๊ฐ€์ง„ ๋ถ„์ด ์žˆ์†Œ?",
468
+ info="๋ถ€๋ชจยทํ˜•์ œ ๊ธฐ์ค€")
469
+ mole_change = gr.Radio(["์•„๋‹ˆ์˜ค", "์˜ˆ"], value="์•„๋‹ˆ์˜ค",
470
+ label="๐Ÿ”„ ๊ทธ ์ ์ด ์ตœ๊ทผ ๋ณ€(่ฎŠ)ํ–ˆ์†Œ?",
471
+ info="ํฌ๊ธฐยท์ƒ‰ยท๋ชจ์–‘ ๋ณ€ํ™” (์ตœ๊ทผ 6๊ฐœ์›” ์ด๋‚ด)")
472
+ with gr.Row(elem_classes="row-buttons"):
473
+ q_back = gr.Button("โ† ๋Œ์•„๊ฐ€๊ธฐ", elem_classes="secondary-btn")
474
+ q_next = gr.Button("์  ๋ณด์—ฌ์ฃผ์˜ค โ†’", elem_classes="primary-btn")
475
+
476
+ # =============== ํ™”๋ฉด 3: ์  ์‚ฌ์ง„ ===============
477
+ with gr.Column(visible=False) as photo_screen:
478
+ gr.HTML("""
479
+ <div class="scr-header">
480
+ <div class="scr-title">๐Ÿ“ธ ์ (้ปž) ์‚ฌ์ง„</div>
481
+ <div class="scr-sub">2 / 3 ยท ๋ฌด๋‹น์—๊ฒŒ ์ ์„ ๋ณด์—ฌ์ฃผ์˜ค</div>
482
+ </div>
483
+ <div class="progress-dots">
484
+ <div class="progress-dot active"></div>
485
+ <div class="progress-dot active"></div>
486
+ <div class="progress-dot"></div>
487
+ </div>
488
+ <div class="app-card">
489
+ <div class="app-card-title">โ€” ์ ์„ ๊ฐ€๊นŒ์ด ๋น„์ถ”์‹œ์˜ค โ€”</div>
490
+ <div class="app-card-desc">๋ฐ๏ฟฝ๏ฟฝ๏ฟฝ ๊ณณ์—์„œ, ์ ์ด ํ™”๋ฉด์— ๊ฐ€๋“ ์ฐจ๊ฒŒ ์ฐ์–ด์ฃผ์‹œ์˜ค. ์นด๋ฉ”๋ผ๋กœ ์ฐ๊ฑฐ๋‚˜ ๊ฐค๋Ÿฌ๋ฆฌ์—์„œ ๊ณจ๋ผ๋„ ์ข‹์†Œ.</div>
491
+ </div>
492
+ """)
493
+ image_input = gr.Image(sources=["upload", "webcam"], type="numpy",
494
+ label="", show_label=False, height=300)
495
+ with gr.Row(elem_classes="row-buttons"):
496
+ p_back = gr.Button("โ† ๋Œ์•„๊ฐ€๊ธฐ", elem_classes="secondary-btn")
497
+ analyze = gr.Button("๐Ÿ”ฎ ์ ๊ด˜ ๋ณด๊ธฐ", elem_classes="primary-btn")
498
+
499
+ # =============== ํ™”๋ฉด 4: ์ ๊ด˜ (๊ฒฐ๊ณผ) ===============
500
+ with gr.Column(visible=False) as result_screen:
501
+ gr.HTML("""
502
+ <div class="scr-header">
503
+ <div class="scr-title">๐Ÿ”ฎ ์˜ค๋Š˜์˜ ์ ๊ด˜</div>
504
+ <div class="scr-sub">3 / 3 ยท ๋ฌด๋‹น์˜ ํ’€์ด</div>
505
+ </div>
506
+ <div class="progress-dots">
507
+ <div class="progress-dot active"></div>
508
+ <div class="progress-dot active"></div>
509
+ <div class="progress-dot active"></div>
510
+ </div>
511
+ """)
512
+ with gr.Column(elem_classes="app-card"):
513
+ result_label = gr.Label(label="", show_label=False, num_top_classes=2)
514
+ result_detail = gr.HTML()
515
+ gr.HTML('<div class="disclaimer">์ฐธ๊ณ ์šฉ ์ ๊ด˜์ผ ๋ฟ, ์ง„์งœ ์ง„๋‹จ์€ ํ”ผ๋ถ€๊ณผ ์ „๋ฌธ์˜์—๊ฒŒ ๋ฐ›์œผ์‹œ์˜ค.</div>')
516
+ with gr.Row(elem_classes="row-buttons"):
517
+ again_btn = gr.Button("๐Ÿ”„ ๋‹ค์‹œ ์ ์น˜๊ธฐ", elem_classes="primary-btn")
518
+ info_from_result = gr.Button("๐Ÿ“œ ์ ์ด๋ž€?", elem_classes="secondary-btn")
519
+
520
+ # =============== ํ™”๋ฉด 5: ์ ์ด๋ž€ (์ •๋ณด) ===============
521
+ with gr.Column(visible=False) as info_screen:
522
+ gr.HTML("""
523
+ <div class="scr-header">
524
+ <div class="scr-title">๐Ÿ“œ ์ ์ด๋ž€ ๋ฌด์—‡์ด์˜ค?</div>
525
+ <div class="scr-sub">์•Œ์•„๋‘๋ฉด ์ข‹์€ ์˜ํ•™ ์ด์•ผ๊ธฐ</div>
526
+ </div>
527
+ """)
528
+ with gr.Column(elem_classes="app-card"):
529
+ with gr.Accordion("๐Ÿ” ํ‘์ƒ‰์ข…์ด๋ž€?", open=True):
530
+ gr.Markdown(
531
+ "**ํ‘์ƒ‰์ข…(Melanoma)** ์€ ํ”ผ๋ถ€์˜ ๋ฉœ๋ผ๋…ธ์‚ฌ์ดํŠธ(์ƒ‰์†Œ ์„ธํฌ)๊ฐ€ "
532
+ "์•…์„ฑ ๋ณ€ํ™”ํ•œ ์•”์ด์˜ค. ๋‹ค๋ฅธ ํ”ผ๋ถ€์•”๋ณด๋‹ค ๋ฐœ์ƒ ๋นˆ๋„๋Š” ๋‚ฎ์ง€๋งŒ "
533
+ "**์ „์ด๊ฐ€ ๋นจ๋ผ ๊ฐ€์žฅ ๊ณต๊ฒฉ์ **์ด๋ผ ์กฐ๊ธฐ ๋ฐœ๊ฒฌ์ด ์ค‘์š”ํ•˜๋‹ค์˜ค."
534
+ )
535
+ with gr.Accordion("๐Ÿ“ ABCDE ๊ทœ์น™ โ€” ์ž๊ฐ€ ๊ฒ€์ง„"):
536
+ gr.Markdown(
537
+ "- **A**symmetry โ€” ๋น„๋Œ€์นญ\n"
538
+ "- **B**order โ€” ๋“ค์ญ‰๋‚ ์ญ‰ํ•œ ๊ฒฝ๊ณ„\n"
539
+ "- **C**olor โ€” ํ•œ ์ ์— ์—ฌ๋Ÿฌ ์ƒ‰\n"
540
+ "- **D**iameter โ€” 6mm ์ด์ƒ\n"
541
+ "- **E**volving โ€” ๋ณ€ํ™”\n\n"
542
+ "์—ฌ๋Ÿฌ ํ•ญ๋ชฉ ํ•ด๋‹น ์‹œ ํ”ผ๋ถ€๊ณผ ์ง„๋ฃŒ๊ฐ€ ๋‹ต์ด์™ธ๋‹ค."
543
+ )
544
+ with gr.Accordion("๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์ธ์ด ์•Œ์•„์•ผ ํ•  ๊ฒƒ"):
545
+ gr.Markdown(
546
+ "ํ•œ๊ตญ์ธ ํ‘์ƒ‰์ข…์˜ **30~50%** ๋Š” **๋ง๋‹จํ‘์ƒ‰์  ํ‘์ƒ‰์ข…**(์†๋ฐ”๋‹ฅยท๋ฐœ๋ฐ”๋‹ฅยท์†ํ†ฑ) "
547
+ "ํ˜•ํƒœ๋ผ๋„ค. ์ž์™ธ์„ ๊ณผ ๋ฌด๊ด€ํ•˜๋ฉฐ ๋‹จ์ˆœ ์ ์ฒ˜๋Ÿผ ๋ณด์—ฌ ๋Šฆ๊ฒŒ ๋ฐœ๊ฒฌ๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์†Œ."
548
+ )
549
+ with gr.Accordion("๐Ÿฅ ๋ณ‘์›์— ๊ฐ€์•ผ ํ•  ๋•Œ"):
550
+ gr.Markdown(
551
+ "- ์ ์ด ๋ณ€ํ•  ๋•Œ (ํฌ๊ธฐยท์ƒ‰ยท๋ชจ์–‘)\n"
552
+ "- ์ ์—์„œ ์ถœํ˜ˆ\n"
553
+ "- ๊ฐ€๋ ต๊ฑฐ๋‚˜ ํ†ต์ฆ\n"
554
+ "- ์†ยท๋ฐœ๋ฐ”๋‹ฅ์— ์ƒˆ๋กœ์šด ์ \n"
555
+ "- ๊ฐ€์กฑ ์ค‘ ํ‘์ƒ‰์ข… ํ™˜์ž ์žˆ์Œ\n\n"
556
+ "โ†’ **ํ”ผ๋ถ€๊ณผ ์ „๋ฌธ์˜** ์ง„๋ฃŒ๊ฐ€ ์ •๋‹ต์ด์˜ค."
557
+ )
558
+ info_back = gr.Button("โ† ์ ์ง‘์œผ๋กœ", elem_classes="secondary-btn")
559
+
560
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
561
+ # 7. ํ™”๋ฉด ์ „ํ™˜ ๋กœ์ง
562
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
563
+ all_screens = [home_screen, questionnaire_screen, photo_screen,
564
+ result_screen, info_screen]
565
+
566
+ def goto(screen_name):
567
+ order = ["home", "questionnaire", "photo", "result", "info"]
568
+ return [gr.update(visible=(s == screen_name)) for s in order]
569
+
570
+ start_btn.click( fn=lambda: goto("questionnaire"), outputs=all_screens)
571
+ info_from_home.click( fn=lambda: goto("info"), outputs=all_screens)
572
+ q_back.click( fn=lambda: goto("home"), outputs=all_screens)
573
+ q_next.click( fn=lambda: goto("photo"), outputs=all_screens)
574
+ p_back.click( fn=lambda: goto("questionnaire"), outputs=all_screens)
575
+ again_btn.click( fn=lambda: goto("home"), outputs=all_screens)
576
+ info_from_result.click(fn=lambda: goto("info"), outputs=all_screens)
577
+ info_back.click( fn=lambda: goto("home"), outputs=all_screens)
578
+
579
+ # AI ์ ๊ด˜ โ†’ ๊ฒฐ๊ณผ ํ™”๋ฉด
580
+ def analyze_and_show(image, age_v, family_v, mole_change_v):
581
+ if image is None:
582
+ return goto("photo") + [None, "<div class='disclaimer'>๋จผ์ € ์  ์‚ฌ์ง„์„ ๋ณด์—ฌ์ฃผ์˜ค</div>"]
583
+ label, detail = predict_with_questionnaire(image, age_v, family_v, mole_change_v)
584
+ return goto("result") + [label, detail]
585
+
586
+ analyze.click(
587
+ fn=analyze_and_show,
588
+ inputs=[image_input, age, family, mole_change],
589
+ outputs=all_screens + [result_label, result_detail],
590
+ )
591
+
592
+ demo.launch()