Update app-backup1.py
Browse files- app-backup1.py +720 -706
app-backup1.py
CHANGED
|
@@ -3,392 +3,644 @@ import requests
|
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
import time
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
#
|
|
|
|
|
|
|
| 8 |
FIREWORKS_API_KEY = os.getenv("FIREWORKS_API_KEY", "")
|
| 9 |
FAL_KEY = os.getenv("FAL_KEY", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
# ============================================
|
| 12 |
-
#
|
| 13 |
# ============================================
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
}
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
}
|
| 84 |
|
| 85 |
/* 타이틀 스타일 */
|
| 86 |
.cyber-title {
|
| 87 |
-
font-family: '
|
| 88 |
-
font-size:
|
| 89 |
-
font-weight:
|
| 90 |
-
|
| 91 |
-
letter-spacing: 8px;
|
| 92 |
-
background: linear-gradient(135deg, var(--neon-cyan), var(--neon-magenta), var(--neon-yellow));
|
| 93 |
-
background-size: 300% 300%;
|
| 94 |
-webkit-background-clip: text;
|
| 95 |
-webkit-text-fill-color: transparent;
|
| 96 |
background-clip: text;
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
filter: drop-shadow(0 0 30px rgba(0, 245, 255, 0.5));
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
@keyframes gradientShift {
|
| 103 |
-
0% { background-position: 0% 50%; }
|
| 104 |
-
50% { background-position: 100% 50%; }
|
| 105 |
-
100% { background-position: 0% 50%; }
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
/* 서브타이틀 */
|
| 109 |
-
.cyber-subtitle {
|
| 110 |
-
font-family: 'Share Tech Mono', monospace !important;
|
| 111 |
-
color: var(--neon-cyan) !important;
|
| 112 |
-
font-size: 1.1rem !important;
|
| 113 |
-
letter-spacing: 4px;
|
| 114 |
-
opacity: 0.9;
|
| 115 |
-
animation: flicker 4s infinite;
|
| 116 |
-
}
|
| 117 |
-
|
| 118 |
-
@keyframes flicker {
|
| 119 |
-
0%, 100% { opacity: 0.9; }
|
| 120 |
-
92% { opacity: 0.9; }
|
| 121 |
-
93% { opacity: 0.4; }
|
| 122 |
-
94% { opacity: 0.9; }
|
| 123 |
-
96% { opacity: 0.4; }
|
| 124 |
-
97% { opacity: 0.9; }
|
| 125 |
-
}
|
| 126 |
-
|
| 127 |
-
/* 글래스모피즘 패널 */
|
| 128 |
-
.glass-panel {
|
| 129 |
-
background: var(--glass-bg) !important;
|
| 130 |
-
backdrop-filter: blur(20px) !important;
|
| 131 |
-
-webkit-backdrop-filter: blur(20px) !important;
|
| 132 |
-
border: 1px solid rgba(0, 245, 255, 0.2) !important;
|
| 133 |
-
border-radius: 16px !important;
|
| 134 |
-
box-shadow:
|
| 135 |
-
0 8px 32px rgba(0, 0, 0, 0.4),
|
| 136 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.05) !important;
|
| 137 |
-
position: relative;
|
| 138 |
-
overflow: hidden;
|
| 139 |
}
|
| 140 |
|
| 141 |
-
|
| 142 |
-
content: '';
|
| 143 |
-
position: absolute;
|
| 144 |
-
top: 0;
|
| 145 |
-
left: -100%;
|
| 146 |
-
width: 100%;
|
| 147 |
-
height: 100%;
|
| 148 |
-
background: linear-gradient(90deg, transparent, rgba(0, 245, 255, 0.1), transparent);
|
| 149 |
-
animation: shimmer 3s infinite;
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
@keyframes shimmer {
|
| 153 |
-
0% { left: -100%; }
|
| 154 |
-
100% { left: 100%; }
|
| 155 |
-
}
|
| 156 |
-
|
| 157 |
-
/* 입력 필드 */
|
| 158 |
.gradio-container textarea,
|
| 159 |
.gradio-container input[type="text"],
|
| 160 |
-
.gradio-container input[type="password"]
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
border
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
font-
|
| 167 |
-
font-
|
| 168 |
-
padding: 16px !important;
|
| 169 |
-
transition: all 0.
|
| 170 |
}
|
| 171 |
|
| 172 |
.gradio-container textarea:focus,
|
| 173 |
.gradio-container input:focus {
|
| 174 |
-
border-color: var(--
|
| 175 |
-
box-shadow:
|
| 176 |
outline: none !important;
|
| 177 |
}
|
| 178 |
|
| 179 |
.gradio-container textarea::placeholder,
|
| 180 |
.gradio-container input::placeholder {
|
| 181 |
-
color:
|
| 182 |
}
|
| 183 |
|
| 184 |
-
/* 라벨 */
|
| 185 |
.gradio-container label {
|
| 186 |
-
font-family: '
|
| 187 |
-
color: var(--
|
| 188 |
-
font-size: 0.9rem !important;
|
| 189 |
font-weight: 600 !important;
|
| 190 |
-
|
| 191 |
-
|
| 192 |
}
|
| 193 |
|
| 194 |
/* 드롭다운 */
|
| 195 |
.gradio-container select,
|
| 196 |
.gradio-container .wrap-inner {
|
| 197 |
-
background:
|
| 198 |
-
border: 2px solid
|
| 199 |
-
border-radius:
|
| 200 |
-
color:
|
| 201 |
-
font-family: 'Rajdhani', sans-serif !important;
|
| 202 |
}
|
| 203 |
|
| 204 |
-
/* 메인 버튼 -
|
| 205 |
.neon-button {
|
| 206 |
-
background: linear-gradient(135deg,
|
| 207 |
-
border:
|
| 208 |
-
border-radius:
|
| 209 |
color: #ffffff !important;
|
| 210 |
-
font-family: '
|
| 211 |
-
font-size:
|
| 212 |
-
font-weight:
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
position: relative !important;
|
| 217 |
-
overflow: hidden !important;
|
| 218 |
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
| 219 |
-
box-shadow: var(--glow-cyan) !important;
|
| 220 |
}
|
| 221 |
|
| 222 |
-
.neon-button
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
left: -100%;
|
| 227 |
-
width: 100%;
|
| 228 |
-
height: 100%;
|
| 229 |
-
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
| 230 |
-
transition: left 0.5s;
|
| 231 |
}
|
| 232 |
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
border-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
}
|
| 239 |
|
| 240 |
-
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
}
|
| 243 |
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
border-
|
| 248 |
-
|
| 249 |
-
padding:
|
| 250 |
-
font-family: 'Share Tech Mono', monospace !important;
|
| 251 |
-
color: var(--neon-green) !important;
|
| 252 |
}
|
| 253 |
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
border: 2px solid rgba(57, 255, 20, 0.4) !important;
|
| 258 |
-
color: var(--neon-green) !important;
|
| 259 |
-
font-family: 'Share Tech Mono', monospace !important;
|
| 260 |
-
text-shadow: 0 0 10px rgba(57, 255, 20, 0.3);
|
| 261 |
}
|
| 262 |
|
| 263 |
-
/*
|
| 264 |
-
.
|
| 265 |
-
background: var(--
|
| 266 |
-
border:
|
| 267 |
-
border-radius:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
overflow: hidden !important;
|
| 269 |
-
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
| 270 |
}
|
| 271 |
|
| 272 |
-
.
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
}
|
| 277 |
|
| 278 |
/* 이미지 컨테이너 */
|
| 279 |
-
.gradio-container .image-container
|
| 280 |
-
|
| 281 |
-
|
|
|
|
| 282 |
}
|
| 283 |
|
| 284 |
-
/*
|
| 285 |
-
.gradio-container .
|
| 286 |
-
background: var(--
|
| 287 |
-
border: 1px solid
|
| 288 |
-
border-radius:
|
| 289 |
}
|
| 290 |
|
| 291 |
-
/*
|
| 292 |
-
.gradio-container
|
| 293 |
-
|
| 294 |
-
border: 1px solid rgba(0, 245, 255, 0.3) !important;
|
| 295 |
-
border-radius: 20px !important;
|
| 296 |
-
color: var(--neon-cyan) !important;
|
| 297 |
-
font-family: 'Rajdhani', sans-serif !important;
|
| 298 |
-
transition: all 0.3s ease !important;
|
| 299 |
}
|
| 300 |
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
}
|
| 305 |
|
| 306 |
-
/*
|
| 307 |
-
.
|
| 308 |
-
|
| 309 |
-
color: var(--neon-magenta) !important;
|
| 310 |
-
font-size: 1.5rem !important;
|
| 311 |
-
font-weight: 700 !important;
|
| 312 |
-
letter-spacing: 4px !important;
|
| 313 |
-
text-transform: uppercase !important;
|
| 314 |
-
text-shadow: 0 0 20px rgba(255, 0, 255, 0.5);
|
| 315 |
-
}
|
| 316 |
-
|
| 317 |
-
/* 비교 라벨 */
|
| 318 |
-
.compare-label-original {
|
| 319 |
-
background: linear-gradient(90deg, rgba(255, 107, 107, 0.2), transparent) !important;
|
| 320 |
-
border-left: 4px solid #ff6b6b !important;
|
| 321 |
-
color: #ff6b6b !important;
|
| 322 |
-
font-family: 'Orbitron', monospace !important;
|
| 323 |
-
padding: 12px 20px !important;
|
| 324 |
-
font-weight: 600 !important;
|
| 325 |
-
letter-spacing: 2px !important;
|
| 326 |
}
|
| 327 |
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
position: fixed;
|
| 342 |
-
top: 0;
|
| 343 |
-
left: 0;
|
| 344 |
-
right: 0;
|
| 345 |
-
bottom: 0;
|
| 346 |
-
background: repeating-linear-gradient(
|
| 347 |
-
0deg,
|
| 348 |
-
rgba(0, 0, 0, 0.1) 0px,
|
| 349 |
-
rgba(0, 0, 0, 0.1) 1px,
|
| 350 |
-
transparent 1px,
|
| 351 |
-
transparent 2px
|
| 352 |
-
);
|
| 353 |
-
pointer-events: none;
|
| 354 |
-
z-index: 9999;
|
| 355 |
-
opacity: 0.3;
|
| 356 |
-
}
|
| 357 |
-
|
| 358 |
-
/* 푸터 */
|
| 359 |
-
.cyber-footer {
|
| 360 |
-
font-family: 'Share Tech Mono', monospace !important;
|
| 361 |
-
color: rgba(0, 245, 255, 0.6) !important;
|
| 362 |
-
font-size: 0.85rem !important;
|
| 363 |
-
letter-spacing: 2px !important;
|
| 364 |
-
}
|
| 365 |
-
|
| 366 |
-
/* 로딩 애니메이션 */
|
| 367 |
-
@keyframes pulse-glow {
|
| 368 |
-
0%, 100% { box-shadow: var(--glow-cyan); }
|
| 369 |
-
50% { box-shadow: var(--glow-magenta); }
|
| 370 |
-
}
|
| 371 |
-
|
| 372 |
-
.loading {
|
| 373 |
-
animation: pulse-glow 1.5s ease-in-out infinite;
|
| 374 |
-
}
|
| 375 |
-
|
| 376 |
-
/* 반응형 */
|
| 377 |
-
@media (max-width: 768px) {
|
| 378 |
-
.cyber-title {
|
| 379 |
-
font-size: 2rem !important;
|
| 380 |
-
letter-spacing: 4px;
|
| 381 |
-
}
|
| 382 |
-
|
| 383 |
-
.cyber-subtitle {
|
| 384 |
-
font-size: 0.9rem !important;
|
| 385 |
-
letter-spacing: 2px;
|
| 386 |
-
}
|
| 387 |
}
|
| 388 |
|
| 389 |
-
/*
|
| 390 |
-
|
| 391 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
.built-with,
|
| 393 |
.built-with-badge,
|
| 394 |
a[href*="huggingface.co/spaces"],
|
|
@@ -397,435 +649,197 @@ footer,
|
|
| 397 |
#footer,
|
| 398 |
.gradio-container > footer,
|
| 399 |
div[class*="footer"],
|
| 400 |
-
.gradio-container a[target="_blank"][href*="huggingface"],
|
| 401 |
-
.gradio-container .wrap:has(a[href*="huggingface"]),
|
| 402 |
.space-info,
|
| 403 |
.hf-space-header,
|
| 404 |
[class*="space-header"],
|
| 405 |
-
.wrap.
|
| 406 |
.svelte-1rjryqp:has(a[href*="huggingface"]),
|
| 407 |
-
.
|
| 408 |
display: none !important;
|
| 409 |
visibility: hidden !important;
|
| 410 |
opacity: 0 !important;
|
| 411 |
height: 0 !important;
|
| 412 |
width: 0 !important;
|
| 413 |
-
overflow: hidden !important;
|
| 414 |
position: absolute !important;
|
| 415 |
pointer-events: none !important;
|
| 416 |
-
z-index: -9999 !important;
|
| 417 |
}
|
| 418 |
|
| 419 |
-
/* 우측 상단 고정 위치 요소 숨기기 (허깅페이스 배지) */
|
| 420 |
-
.gradio-container > div:first-child > a[href*="huggingface"],
|
| 421 |
-
body > div > a[href*="huggingface"],
|
| 422 |
div[style*="position: fixed"][style*="right"],
|
| 423 |
-
div[style*="position: fixed"][style*="top: 0"]
|
| 424 |
-
.fixed.top-0.right-0,
|
| 425 |
-
a.svelte-1rjryqp[href*="huggingface"],
|
| 426 |
-
div.wrap.svelte-1rjryqp {
|
| 427 |
display: none !important;
|
| 428 |
-
visibility: hidden !important;
|
| 429 |
-
opacity: 0 !important;
|
| 430 |
}
|
| 431 |
"""
|
| 432 |
|
| 433 |
# ============================================
|
| 434 |
-
#
|
| 435 |
-
# ============================================
|
| 436 |
-
|
| 437 |
-
def enhance_prompt(prompt: str, fireworks_key: str) -> str:
|
| 438 |
-
"""Fireworks AI LLM API를 사용하여 프롬프트를 증강합니다."""
|
| 439 |
-
if not prompt.strip():
|
| 440 |
-
return "❌ 프롬프트를 입력해주세요."
|
| 441 |
-
|
| 442 |
-
if not fireworks_key.strip():
|
| 443 |
-
return "❌ Fireworks API 키를 입력해주세요."
|
| 444 |
-
|
| 445 |
-
system_message = """You are a professional prompt engineer specializing in AI image and video generation.
|
| 446 |
-
Your task is to enhance user prompts to create more detailed, vivid, and effective prompts for AI image/video generation models.
|
| 447 |
-
|
| 448 |
-
When enhancing prompts:
|
| 449 |
-
1. Add specific visual details (lighting, composition, style, mood)
|
| 450 |
-
2. Include artistic references or styles when appropriate
|
| 451 |
-
3. Specify camera angles, perspectives, and depth of field
|
| 452 |
-
4. Add color palette suggestions
|
| 453 |
-
5. Include quality modifiers (8K, cinematic, professional, etc.)
|
| 454 |
-
6. Maintain the original intent while enriching the description
|
| 455 |
-
7. Output the enhanced prompt in English for best AI model compatibility
|
| 456 |
-
|
| 457 |
-
Respond ONLY with the enhanced prompt, no explanations or additional text."""
|
| 458 |
-
|
| 459 |
-
url = "https://api.fireworks.ai/inference/v1/chat/completions"
|
| 460 |
-
|
| 461 |
-
payload = {
|
| 462 |
-
"model": "accounts/fireworks/models/gpt-oss-120b",
|
| 463 |
-
"max_tokens": 4096,
|
| 464 |
-
"top_p": 1,
|
| 465 |
-
"top_k": 40,
|
| 466 |
-
"presence_penalty": 0,
|
| 467 |
-
"frequency_penalty": 0,
|
| 468 |
-
"temperature": 0.6,
|
| 469 |
-
"messages": [
|
| 470 |
-
{"role": "system", "content": system_message},
|
| 471 |
-
{"role": "user", "content": f"다음 프롬프트를 이미지/영상 생성 AI에 최적화된 상세하고 풍부한 프롬프트로 증강해주세요:\n\n\"{prompt}\""}
|
| 472 |
-
]
|
| 473 |
-
}
|
| 474 |
-
|
| 475 |
-
headers = {
|
| 476 |
-
"Accept": "application/json",
|
| 477 |
-
"Content-Type": "application/json",
|
| 478 |
-
"Authorization": f"Bearer {fireworks_key}"
|
| 479 |
-
}
|
| 480 |
-
|
| 481 |
-
try:
|
| 482 |
-
response = requests.post(url, headers=headers, data=json.dumps(payload), timeout=60)
|
| 483 |
-
response.raise_for_status()
|
| 484 |
-
data = response.json()
|
| 485 |
-
enhanced = data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
| 486 |
-
return enhanced if enhanced else "❌ 응답에서 증강된 프롬프트를 찾을 수 없습니다."
|
| 487 |
-
except requests.exceptions.Timeout:
|
| 488 |
-
return "❌ API 요청 시간 초과. 다시 시도해주세요."
|
| 489 |
-
except requests.exceptions.HTTPError as e:
|
| 490 |
-
return f"❌ API 오류: {e.response.status_code}"
|
| 491 |
-
except Exception as e:
|
| 492 |
-
return f"❌ 오류 발생: {str(e)}"
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
def generate_image(prompt: str, fal_key: str, aspect_ratio: str = "1:1", resolution: str = "1K") -> str:
|
| 496 |
-
"""FAL AI의 nano-banana-pro 모델을 사용하여 이미지를 생성합니다."""
|
| 497 |
-
if not prompt.strip() or not fal_key.strip():
|
| 498 |
-
return None
|
| 499 |
-
|
| 500 |
-
url = "https://queue.fal.run/fal-ai/nano-banana-pro"
|
| 501 |
-
headers = {"Authorization": f"Key {fal_key}", "Content-Type": "application/json"}
|
| 502 |
-
payload = {
|
| 503 |
-
"prompt": prompt,
|
| 504 |
-
"num_images": 1,
|
| 505 |
-
"aspect_ratio": aspect_ratio,
|
| 506 |
-
"output_format": "png",
|
| 507 |
-
"resolution": resolution
|
| 508 |
-
}
|
| 509 |
-
|
| 510 |
-
try:
|
| 511 |
-
response = requests.post(url, headers=headers, json=payload, timeout=120)
|
| 512 |
-
response.raise_for_status()
|
| 513 |
-
data = response.json()
|
| 514 |
-
|
| 515 |
-
if "images" in data:
|
| 516 |
-
images = data.get("images", [])
|
| 517 |
-
if images:
|
| 518 |
-
return images[0].get("url", None)
|
| 519 |
-
|
| 520 |
-
request_id = data.get("request_id")
|
| 521 |
-
if request_id:
|
| 522 |
-
return poll_for_result(request_id, fal_key)
|
| 523 |
-
return None
|
| 524 |
-
except Exception as e:
|
| 525 |
-
print(f"이미지 생성 오류: {e}")
|
| 526 |
-
return None
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
def poll_for_result(request_id: str, fal_key: str, max_attempts: int = 60) -> str:
|
| 530 |
-
"""Queue 결과를 폴링하여 이미지 URL을 가져옵니다."""
|
| 531 |
-
status_url = f"https://queue.fal.run/fal-ai/nano-banana-pro/requests/{request_id}/status"
|
| 532 |
-
result_url = f"https://queue.fal.run/fal-ai/nano-banana-pro/requests/{request_id}"
|
| 533 |
-
headers = {"Authorization": f"Key {fal_key}", "Content-Type": "application/json"}
|
| 534 |
-
|
| 535 |
-
for _ in range(max_attempts):
|
| 536 |
-
try:
|
| 537 |
-
status_response = requests.get(status_url, headers=headers, timeout=30)
|
| 538 |
-
status_data = status_response.json()
|
| 539 |
-
status = status_data.get("status", "")
|
| 540 |
-
|
| 541 |
-
if status == "COMPLETED":
|
| 542 |
-
result_response = requests.get(result_url, headers=headers, timeout=30)
|
| 543 |
-
result_data = result_response.json()
|
| 544 |
-
images = result_data.get("images", [])
|
| 545 |
-
return images[0].get("url", None) if images else None
|
| 546 |
-
elif status == "FAILED":
|
| 547 |
-
return None
|
| 548 |
-
time.sleep(2)
|
| 549 |
-
except Exception:
|
| 550 |
-
time.sleep(2)
|
| 551 |
-
return None
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
def process_comparison(original_prompt: str, fireworks_key: str, fal_key: str, aspect_ratio: str, resolution: str):
|
| 555 |
-
"""원본 프롬프트 증강 → 두 이미지 생성 → 비교 결과 반환"""
|
| 556 |
-
|
| 557 |
-
# API 키 기본값 적용
|
| 558 |
-
fw_key = fireworks_key if fireworks_key.strip() else FIREWORKS_API_KEY
|
| 559 |
-
fl_key = fal_key if fal_key.strip() else FAL_KEY
|
| 560 |
-
|
| 561 |
-
yield "⚡ INITIALIZING PROMPT ENHANCEMENT SEQUENCE...", None, None, None
|
| 562 |
-
|
| 563 |
-
enhanced = enhance_prompt(original_prompt, fw_key)
|
| 564 |
-
|
| 565 |
-
if enhanced.startswith("❌"):
|
| 566 |
-
yield enhanced, None, None, None
|
| 567 |
-
return
|
| 568 |
-
|
| 569 |
-
yield "✅ PROMPT ENHANCED\n\n⚡ GENERATING ORIGINAL IMAGE...", enhanced, None, None
|
| 570 |
-
|
| 571 |
-
original_image = generate_image(original_prompt, fl_key, aspect_ratio, resolution)
|
| 572 |
-
|
| 573 |
-
if not original_image:
|
| 574 |
-
yield "✅ PROMPT ENHANCED\n\n❌ ORIGINAL IMAGE GENERATION FAILED", enhanced, None, None
|
| 575 |
-
else:
|
| 576 |
-
yield "✅ PROMPT ENHANCED\n✅ ORIGINAL IMAGE COMPLETE\n\n⚡ GENERATING ENHANCED IMAGE...", enhanced, original_image, None
|
| 577 |
-
|
| 578 |
-
enhanced_image = generate_image(enhanced, fl_key, aspect_ratio, resolution)
|
| 579 |
-
|
| 580 |
-
if not enhanced_image:
|
| 581 |
-
yield "✅ PROMPT ENHANCED\n✅ ORIGINAL IMAGE COMPLETE\n\n❌ ENHANCED IMAGE GENERATION FAILED", enhanced, original_image, None
|
| 582 |
-
else:
|
| 583 |
-
yield "✅ ALL SYSTEMS OPERATIONAL\n✅ COMPARISON READY", enhanced, original_image, enhanced_image
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
# ============================================
|
| 587 |
-
# Gradio UI 구성
|
| 588 |
# ============================================
|
|
|
|
| 589 |
|
| 590 |
examples = [
|
| 591 |
["한복을 입은 여성이 전통 한옥 마당에서 부채를 들고 있다"],
|
| 592 |
["사이버펑크 도시의 네온사인이 빛나는 밤거리"],
|
| 593 |
["바다 위 일몰과 작은 돛단배"],
|
| 594 |
-
["눈 덮인 산속 작은 오두막에서 연기가 피어오른다"],
|
| 595 |
-
["미래 도시의 하늘을 나는 자동차들"],
|
| 596 |
]
|
| 597 |
|
| 598 |
with gr.Blocks(title="AI PROMPT ENHANCER", css=CUSTOM_CSS) as demo:
|
|
|
|
| 599 |
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
<
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
animation: pulse-glow 3s ease-in-out infinite;
|
| 631 |
-
">🚀</div>
|
| 632 |
-
|
| 633 |
-
<!-- 타이틀 -->
|
| 634 |
-
<h1 class="cyber-title">PROMPT ENHANCER</h1>
|
| 635 |
-
|
| 636 |
-
<!-- 서브타이틀 -->
|
| 637 |
-
<p class="cyber-subtitle" style="margin-top: 16px;">
|
| 638 |
-
[ NEURAL NETWORK ACTIVATED ] — IMAGE GENERATION PROTOCOL v2.0
|
| 639 |
-
</p>
|
| 640 |
-
|
| 641 |
-
<!-- 데코 라인 -->
|
| 642 |
-
<div style="
|
| 643 |
-
width: 200px;
|
| 644 |
-
height: 2px;
|
| 645 |
-
background: linear-gradient(90deg, transparent, var(--neon-cyan), transparent);
|
| 646 |
-
margin: 24px auto 0 auto;
|
| 647 |
-
"></div>
|
| 648 |
-
</div>
|
| 649 |
-
</div>
|
| 650 |
-
""")
|
| 651 |
-
|
| 652 |
-
# API Keys (접힌 상태)
|
| 653 |
-
with gr.Accordion("⚙️ SYSTEM CONFIGURATION", open=not (FIREWORKS_API_KEY and FAL_KEY), elem_classes=["glass-panel"]):
|
| 654 |
-
with gr.Row():
|
| 655 |
-
fireworks_key_input = gr.Textbox(
|
| 656 |
-
label="FIREWORKS API KEY",
|
| 657 |
-
placeholder="fw_...",
|
| 658 |
-
type="password",
|
| 659 |
-
value=FIREWORKS_API_KEY,
|
| 660 |
-
)
|
| 661 |
-
fal_key_input = gr.Textbox(
|
| 662 |
-
label="FAL API KEY",
|
| 663 |
-
placeholder="Enter your FAL key...",
|
| 664 |
-
type="password",
|
| 665 |
-
value=FAL_KEY,
|
| 666 |
-
)
|
| 667 |
-
|
| 668 |
-
gr.HTML("<div style='height: 20px;'></div>")
|
| 669 |
-
|
| 670 |
-
# 메인 입력 섹션
|
| 671 |
-
with gr.Row():
|
| 672 |
-
with gr.Column(scale=2):
|
| 673 |
-
gr.HTML("""
|
| 674 |
-
<div style="
|
| 675 |
-
font-family: 'Orbitron', monospace;
|
| 676 |
-
color: #00f5ff;
|
| 677 |
-
font-size: 0.9rem;
|
| 678 |
-
letter-spacing: 3px;
|
| 679 |
-
margin-bottom: 12px;
|
| 680 |
-
text-transform: uppercase;
|
| 681 |
-
">📝 INPUT PROMPT</div>
|
| 682 |
-
""")
|
| 683 |
-
prompt_input = gr.Textbox(
|
| 684 |
-
label="",
|
| 685 |
-
placeholder="[ ENTER YOUR VISION HERE ]\n예: 한복을 입은 여성이 벚꽃 아래에서 걷고 있다",
|
| 686 |
-
lines=5,
|
| 687 |
-
show_label=False,
|
| 688 |
-
elem_classes=["glass-panel"]
|
| 689 |
-
)
|
| 690 |
-
|
| 691 |
-
gr.Examples(
|
| 692 |
-
examples=examples,
|
| 693 |
-
inputs=prompt_input,
|
| 694 |
-
label="💡 QUICK PROMPTS"
|
| 695 |
-
)
|
| 696 |
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
font-family: 'Orbitron', monospace;
|
| 701 |
-
color: #ff00ff;
|
| 702 |
-
font-size: 0.9rem;
|
| 703 |
-
letter-spacing: 3px;
|
| 704 |
-
margin-bottom: 12px;
|
| 705 |
-
text-transform: uppercase;
|
| 706 |
-
">⚙️ PARAMETERS</div>
|
| 707 |
-
""")
|
| 708 |
-
aspect_ratio = gr.Dropdown(
|
| 709 |
-
label="ASPECT RATIO",
|
| 710 |
-
choices=["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "21:9"],
|
| 711 |
-
value="1:1",
|
| 712 |
-
)
|
| 713 |
-
resolution = gr.Dropdown(
|
| 714 |
-
label="RESOLUTION",
|
| 715 |
-
choices=["1K", "2K", "4K"],
|
| 716 |
-
value="1K",
|
| 717 |
-
)
|
| 718 |
|
| 719 |
-
gr.
|
|
|
|
|
|
|
|
|
|
| 720 |
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 738 |
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
color: #39ff14;
|
| 746 |
-
font-size: 1rem;
|
| 747 |
-
letter-spacing: 3px;
|
| 748 |
-
margin-bottom: 12px;
|
| 749 |
-
text-transform: uppercase;
|
| 750 |
-
text-shadow: 0 0 20px rgba(57, 255, 20, 0.5);
|
| 751 |
-
">✨ ENHANCED OUTPUT</div>
|
| 752 |
-
""")
|
| 753 |
-
enhanced_output = gr.Textbox(
|
| 754 |
-
label="",
|
| 755 |
-
placeholder="[ ENHANCED PROMPT WILL APPEAR HERE ]",
|
| 756 |
-
lines=5,
|
| 757 |
-
interactive=True,
|
| 758 |
-
show_label=False,
|
| 759 |
-
elem_classes=["enhanced-output", "glass-panel"]
|
| 760 |
-
)
|
| 761 |
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
[ SYSTEM VERSION 2.0 ] — NEURAL PROMPT ENHANCEMENT PROTOCOL
|
| 818 |
-
</p>
|
| 819 |
-
</div>
|
| 820 |
-
""")
|
| 821 |
-
|
| 822 |
-
# 이벤트 핸들러
|
| 823 |
-
generate_btn.click(
|
| 824 |
-
fn=process_comparison,
|
| 825 |
-
inputs=[prompt_input, fireworks_key_input, fal_key_input, aspect_ratio, resolution],
|
| 826 |
-
outputs=[status_text, enhanced_output, original_image_output, enhanced_image_output]
|
| 827 |
-
)
|
| 828 |
-
|
| 829 |
|
| 830 |
if __name__ == "__main__":
|
| 831 |
demo.launch()
|
|
|
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
import time
|
| 6 |
+
import sqlite3
|
| 7 |
+
import hashlib
|
| 8 |
+
import secrets
|
| 9 |
+
from datetime import datetime, timedelta
|
| 10 |
|
| 11 |
+
# ============================================
|
| 12 |
+
# 환경변수
|
| 13 |
+
# ============================================
|
| 14 |
FIREWORKS_API_KEY = os.getenv("FIREWORKS_API_KEY", "")
|
| 15 |
FAL_KEY = os.getenv("FAL_KEY", "")
|
| 16 |
+
ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "arxivgpt@gmail.com")
|
| 17 |
+
SECRET_KEY = os.getenv("SECRET_KEY", "change-this-secret-key-in-production")
|
| 18 |
+
|
| 19 |
+
# Persistent Storage 경로 (HuggingFace Spaces)
|
| 20 |
+
DATA_DIR = "/data" if os.path.exists("/data") else "./data"
|
| 21 |
+
DB_PATH = os.path.join(DATA_DIR, "app.db")
|
| 22 |
+
|
| 23 |
+
# 설정
|
| 24 |
+
DAILY_LIMIT_FREE = 10
|
| 25 |
+
SESSION_EXPIRE_HOURS = 24 * 7
|
| 26 |
|
| 27 |
# ============================================
|
| 28 |
+
# 데이터베이스
|
| 29 |
# ============================================
|
| 30 |
+
def get_db():
|
| 31 |
+
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
| 32 |
+
conn.row_factory = sqlite3.Row
|
| 33 |
+
return conn
|
| 34 |
+
|
| 35 |
+
def init_db():
|
| 36 |
+
os.makedirs(DATA_DIR, exist_ok=True)
|
| 37 |
+
conn = get_db()
|
| 38 |
+
cursor = conn.cursor()
|
| 39 |
+
|
| 40 |
+
cursor.execute('''
|
| 41 |
+
CREATE TABLE IF NOT EXISTS users (
|
| 42 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 43 |
+
email TEXT UNIQUE NOT NULL,
|
| 44 |
+
password_hash TEXT NOT NULL,
|
| 45 |
+
is_admin INTEGER DEFAULT 0,
|
| 46 |
+
is_active INTEGER DEFAULT 1,
|
| 47 |
+
daily_limit INTEGER DEFAULT 10,
|
| 48 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 49 |
+
last_login TIMESTAMP
|
| 50 |
+
)
|
| 51 |
+
''')
|
| 52 |
+
|
| 53 |
+
cursor.execute('''
|
| 54 |
+
CREATE TABLE IF NOT EXISTS sessions (
|
| 55 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 56 |
+
user_id INTEGER NOT NULL,
|
| 57 |
+
token TEXT UNIQUE NOT NULL,
|
| 58 |
+
expires_at TIMESTAMP NOT NULL,
|
| 59 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 60 |
+
FOREIGN KEY (user_id) REFERENCES users (id)
|
| 61 |
+
)
|
| 62 |
+
''')
|
| 63 |
+
|
| 64 |
+
cursor.execute('''
|
| 65 |
+
CREATE TABLE IF NOT EXISTS generations (
|
| 66 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 67 |
+
user_id INTEGER NOT NULL,
|
| 68 |
+
original_prompt TEXT,
|
| 69 |
+
enhanced_prompt TEXT,
|
| 70 |
+
image_url TEXT,
|
| 71 |
+
status TEXT DEFAULT 'success',
|
| 72 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 73 |
+
FOREIGN KEY (user_id) REFERENCES users (id)
|
| 74 |
+
)
|
| 75 |
+
''')
|
| 76 |
+
|
| 77 |
+
cursor.execute('''
|
| 78 |
+
CREATE TABLE IF NOT EXISTS daily_usage (
|
| 79 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 80 |
+
user_id INTEGER NOT NULL,
|
| 81 |
+
date DATE NOT NULL,
|
| 82 |
+
count INTEGER DEFAULT 0,
|
| 83 |
+
UNIQUE(user_id, date),
|
| 84 |
+
FOREIGN KEY (user_id) REFERENCES users (id)
|
| 85 |
+
)
|
| 86 |
+
''')
|
| 87 |
+
|
| 88 |
+
conn.commit()
|
| 89 |
+
|
| 90 |
+
# 기본 관리자 계정
|
| 91 |
+
cursor.execute("SELECT id FROM users WHERE email = ?", (ADMIN_EMAIL,))
|
| 92 |
+
if not cursor.fetchone():
|
| 93 |
+
admin_password = secrets.token_urlsafe(12)
|
| 94 |
+
password_hash = hash_password(admin_password)
|
| 95 |
+
cursor.execute(
|
| 96 |
+
"INSERT INTO users (email, password_hash, is_admin, daily_limit) VALUES (?, ?, 1, 9999)",
|
| 97 |
+
(ADMIN_EMAIL, password_hash)
|
| 98 |
+
)
|
| 99 |
+
conn.commit()
|
| 100 |
+
print(f"[ADMIN] 관리자 계정: {ADMIN_EMAIL} / {admin_password}")
|
| 101 |
+
|
| 102 |
+
conn.close()
|
| 103 |
|
| 104 |
+
# ============================================
|
| 105 |
+
# 인증 유틸리티
|
| 106 |
+
# ============================================
|
| 107 |
+
def hash_password(password: str) -> str:
|
| 108 |
+
return hashlib.sha256((password + SECRET_KEY).encode()).hexdigest()
|
| 109 |
+
|
| 110 |
+
def verify_password(password: str, password_hash: str) -> bool:
|
| 111 |
+
return hash_password(password) == password_hash
|
| 112 |
+
|
| 113 |
+
def generate_session_token() -> str:
|
| 114 |
+
return secrets.token_urlsafe(32)
|
| 115 |
+
|
| 116 |
+
def create_session(user_id: int) -> str:
|
| 117 |
+
conn = get_db()
|
| 118 |
+
cursor = conn.cursor()
|
| 119 |
+
cursor.execute("DELETE FROM sessions WHERE user_id = ?", (user_id,))
|
| 120 |
+
token = generate_session_token()
|
| 121 |
+
expires_at = datetime.now() + timedelta(hours=SESSION_EXPIRE_HOURS)
|
| 122 |
+
cursor.execute(
|
| 123 |
+
"INSERT INTO sessions (user_id, token, expires_at) VALUES (?, ?, ?)",
|
| 124 |
+
(user_id, token, expires_at)
|
| 125 |
+
)
|
| 126 |
+
cursor.execute("UPDATE users SET last_login = ? WHERE id = ?", (datetime.now(), user_id))
|
| 127 |
+
conn.commit()
|
| 128 |
+
conn.close()
|
| 129 |
+
return token
|
| 130 |
|
| 131 |
+
def validate_session(token: str) -> dict:
|
| 132 |
+
if not token:
|
| 133 |
+
return None
|
| 134 |
+
conn = get_db()
|
| 135 |
+
cursor = conn.cursor()
|
| 136 |
+
cursor.execute('''
|
| 137 |
+
SELECT u.id, u.email, u.is_admin, u.daily_limit, u.is_active
|
| 138 |
+
FROM sessions s JOIN users u ON s.user_id = u.id
|
| 139 |
+
WHERE s.token = ? AND s.expires_at > ? AND u.is_active = 1
|
| 140 |
+
''', (token, datetime.now()))
|
| 141 |
+
row = cursor.fetchone()
|
| 142 |
+
conn.close()
|
| 143 |
+
if row:
|
| 144 |
+
return {"id": row["id"], "email": row["email"], "is_admin": bool(row["is_admin"]), "daily_limit": row["daily_limit"]}
|
| 145 |
+
return None
|
|
|
|
| 146 |
|
| 147 |
+
def delete_session(token: str):
|
| 148 |
+
conn = get_db()
|
| 149 |
+
cursor = conn.cursor()
|
| 150 |
+
cursor.execute("DELETE FROM sessions WHERE token = ?", (token,))
|
| 151 |
+
conn.commit()
|
| 152 |
+
conn.close()
|
| 153 |
|
| 154 |
+
# ============================================
|
| 155 |
+
# 사용량 관리
|
| 156 |
+
# ============================================
|
| 157 |
+
def get_daily_usage(user_id: int) -> int:
|
| 158 |
+
conn = get_db()
|
| 159 |
+
cursor = conn.cursor()
|
| 160 |
+
cursor.execute("SELECT count FROM daily_usage WHERE user_id = ? AND date = ?", (user_id, datetime.now().date()))
|
| 161 |
+
row = cursor.fetchone()
|
| 162 |
+
conn.close()
|
| 163 |
+
return row["count"] if row else 0
|
| 164 |
+
|
| 165 |
+
def increment_usage(user_id: int) -> int:
|
| 166 |
+
conn = get_db()
|
| 167 |
+
cursor = conn.cursor()
|
| 168 |
+
today = datetime.now().date()
|
| 169 |
+
cursor.execute('''
|
| 170 |
+
INSERT INTO daily_usage (user_id, date, count) VALUES (?, ?, 1)
|
| 171 |
+
ON CONFLICT(user_id, date) DO UPDATE SET count = count + 1
|
| 172 |
+
''', (user_id, today))
|
| 173 |
+
conn.commit()
|
| 174 |
+
cursor.execute("SELECT count FROM daily_usage WHERE user_id = ? AND date = ?", (user_id, today))
|
| 175 |
+
row = cursor.fetchone()
|
| 176 |
+
conn.close()
|
| 177 |
+
return row["count"]
|
| 178 |
+
|
| 179 |
+
def check_usage_limit(user_id: int, daily_limit: int) -> tuple:
|
| 180 |
+
current = get_daily_usage(user_id)
|
| 181 |
+
return (current < daily_limit, current, daily_limit)
|
| 182 |
|
| 183 |
+
# ============================================
|
| 184 |
+
# 생성 이력
|
| 185 |
+
# ============================================
|
| 186 |
+
def save_generation(user_id: int, original: str, enhanced: str, image_url: str, status: str = "success"):
|
| 187 |
+
conn = get_db()
|
| 188 |
+
cursor = conn.cursor()
|
| 189 |
+
cursor.execute('''
|
| 190 |
+
INSERT INTO generations (user_id, original_prompt, enhanced_prompt, image_url, status)
|
| 191 |
+
VALUES (?, ?, ?, ?, ?)
|
| 192 |
+
''', (user_id, original, enhanced, image_url, status))
|
| 193 |
+
conn.commit()
|
| 194 |
+
conn.close()
|
| 195 |
+
|
| 196 |
+
def get_user_generations(user_id: int, limit: int = 20) -> list:
|
| 197 |
+
conn = get_db()
|
| 198 |
+
cursor = conn.cursor()
|
| 199 |
+
cursor.execute('''
|
| 200 |
+
SELECT original_prompt, enhanced_prompt, image_url, status, created_at
|
| 201 |
+
FROM generations WHERE user_id = ? ORDER BY created_at DESC LIMIT ?
|
| 202 |
+
''', (user_id, limit))
|
| 203 |
+
rows = cursor.fetchall()
|
| 204 |
+
conn.close()
|
| 205 |
+
return [dict(row) for row in rows]
|
| 206 |
+
|
| 207 |
+
# ============================================
|
| 208 |
+
# 관리자 기능
|
| 209 |
+
# ============================================
|
| 210 |
+
def get_all_users() -> list:
|
| 211 |
+
conn = get_db()
|
| 212 |
+
cursor = conn.cursor()
|
| 213 |
+
cursor.execute('SELECT id, email, is_admin, is_active, daily_limit, created_at, last_login FROM users ORDER BY created_at DESC')
|
| 214 |
+
rows = cursor.fetchall()
|
| 215 |
+
conn.close()
|
| 216 |
+
return [dict(row) for row in rows]
|
| 217 |
+
|
| 218 |
+
def get_stats() -> dict:
|
| 219 |
+
conn = get_db()
|
| 220 |
+
cursor = conn.cursor()
|
| 221 |
+
today = datetime.now().date()
|
| 222 |
+
|
| 223 |
+
cursor.execute("SELECT COUNT(*) as count FROM users")
|
| 224 |
+
total_users = cursor.fetchone()["count"]
|
| 225 |
+
|
| 226 |
+
cursor.execute("SELECT COUNT(DISTINCT user_id) as count FROM daily_usage WHERE date = ?", (today,))
|
| 227 |
+
active_today = cursor.fetchone()["count"]
|
| 228 |
+
|
| 229 |
+
cursor.execute("SELECT COUNT(*) as count FROM generations")
|
| 230 |
+
total_generations = cursor.fetchone()["count"]
|
| 231 |
+
|
| 232 |
+
cursor.execute("SELECT COUNT(*) as count FROM generations WHERE DATE(created_at) = ?", (today,))
|
| 233 |
+
generations_today = cursor.fetchone()["count"]
|
| 234 |
+
|
| 235 |
+
conn.close()
|
| 236 |
+
return {"total_users": total_users, "active_today": active_today, "total_generations": total_generations, "generations_today": generations_today}
|
| 237 |
+
|
| 238 |
+
def update_user_limit(user_id: int, daily_limit: int) -> bool:
|
| 239 |
+
conn = get_db()
|
| 240 |
+
cursor = conn.cursor()
|
| 241 |
+
cursor.execute("UPDATE users SET daily_limit = ? WHERE id = ?", (daily_limit, user_id))
|
| 242 |
+
conn.commit()
|
| 243 |
+
conn.close()
|
| 244 |
+
return True
|
| 245 |
+
|
| 246 |
+
# ============================================
|
| 247 |
+
# 회원가입 / 로그인
|
| 248 |
+
# ============================================
|
| 249 |
+
def register_user(email: str, password: str, confirm_password: str) -> tuple:
|
| 250 |
+
if not email or not password:
|
| 251 |
+
return None, "❌ 이메일과 비밀번호를 입력해주세요."
|
| 252 |
+
if password != confirm_password:
|
| 253 |
+
return None, "❌ 비밀번호가 일치하지 않습니다."
|
| 254 |
+
if len(password) < 6:
|
| 255 |
+
return None, "❌ 비밀번호는 6자 이상이어야 합니다."
|
| 256 |
+
if "@" not in email:
|
| 257 |
+
return None, "❌ 올바른 이메일 형식이 아닙니다."
|
| 258 |
+
|
| 259 |
+
conn = get_db()
|
| 260 |
+
cursor = conn.cursor()
|
| 261 |
+
cursor.execute("SELECT id FROM users WHERE email = ?", (email,))
|
| 262 |
+
if cursor.fetchone():
|
| 263 |
+
conn.close()
|
| 264 |
+
return None, "❌ 이미 등록된 이메일입니다."
|
| 265 |
+
|
| 266 |
+
password_hash = hash_password(password)
|
| 267 |
+
cursor.execute("INSERT INTO users (email, password_hash, daily_limit) VALUES (?, ?, ?)", (email, password_hash, DAILY_LIMIT_FREE))
|
| 268 |
+
conn.commit()
|
| 269 |
+
user_id = cursor.lastrowid
|
| 270 |
+
conn.close()
|
| 271 |
+
|
| 272 |
+
token = create_session(user_id)
|
| 273 |
+
return token, f"✅ 회원가입 완료! 환영합니다, {email}"
|
| 274 |
+
|
| 275 |
+
def login_user(email: str, password: str) -> tuple:
|
| 276 |
+
if not email or not password:
|
| 277 |
+
return None, "❌ 이메일과 비밀번호를 입력해주세요."
|
| 278 |
+
|
| 279 |
+
conn = get_db()
|
| 280 |
+
cursor = conn.cursor()
|
| 281 |
+
cursor.execute("SELECT id, password_hash, is_active FROM users WHERE email = ?", (email,))
|
| 282 |
+
row = cursor.fetchone()
|
| 283 |
+
conn.close()
|
| 284 |
+
|
| 285 |
+
if not row:
|
| 286 |
+
return None, "❌ 등록되지 않은 이메일입니다."
|
| 287 |
+
if not row["is_active"]:
|
| 288 |
+
return None, "❌ 비활성화된 계정입니다."
|
| 289 |
+
if not verify_password(password, row["password_hash"]):
|
| 290 |
+
return None, "❌ 비밀번호가 올바르지 않습니다."
|
| 291 |
+
|
| 292 |
+
token = create_session(row["id"])
|
| 293 |
+
return token, "✅ 로그인 성공!"
|
| 294 |
+
|
| 295 |
+
def logout_user(token: str) -> str:
|
| 296 |
+
if token:
|
| 297 |
+
delete_session(token)
|
| 298 |
+
return "✅ 로그아웃되었습니다."
|
| 299 |
+
|
| 300 |
+
# ============================================
|
| 301 |
+
# API 함수들
|
| 302 |
+
# ============================================
|
| 303 |
+
def enhance_prompt(prompt: str, fireworks_key: str) -> str:
|
| 304 |
+
if not prompt.strip():
|
| 305 |
+
return "❌ 프롬프트를 입력해주세요."
|
| 306 |
+
if not fireworks_key.strip():
|
| 307 |
+
return "❌ Fireworks API 키가 필요합니다."
|
| 308 |
+
|
| 309 |
+
system_message = """You are a professional prompt engineer for AI image generation.
|
| 310 |
+
Enhance prompts with: visual details, lighting, composition, style, mood, camera angles, color palette, quality modifiers.
|
| 311 |
+
Output ONLY the enhanced prompt in English, no explanations."""
|
| 312 |
+
|
| 313 |
+
url = "https://api.fireworks.ai/inference/v1/chat/completions"
|
| 314 |
+
payload = {
|
| 315 |
+
"model": "accounts/fireworks/models/gpt-oss-120b",
|
| 316 |
+
"max_tokens": 4096,
|
| 317 |
+
"top_p": 1,
|
| 318 |
+
"top_k": 40,
|
| 319 |
+
"presence_penalty": 0,
|
| 320 |
+
"frequency_penalty": 0,
|
| 321 |
+
"temperature": 0.6,
|
| 322 |
+
"messages": [
|
| 323 |
+
{"role": "system", "content": system_message},
|
| 324 |
+
{"role": "user", "content": f"Enhance this prompt:\n\n\"{prompt}\""}
|
| 325 |
+
]
|
| 326 |
+
}
|
| 327 |
+
headers = {
|
| 328 |
+
"Accept": "application/json",
|
| 329 |
+
"Content-Type": "application/json",
|
| 330 |
+
"Authorization": f"Bearer {fireworks_key}"
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
try:
|
| 334 |
+
response = requests.post(url, headers=headers, data=json.dumps(payload), timeout=60)
|
| 335 |
+
response.raise_for_status()
|
| 336 |
+
data = response.json()
|
| 337 |
+
enhanced = data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
| 338 |
+
return enhanced if enhanced else "❌ 증강 실패"
|
| 339 |
+
except requests.exceptions.HTTPError as e:
|
| 340 |
+
return f"❌ HTTP 오류: {e.response.status_code}"
|
| 341 |
+
except Exception as e:
|
| 342 |
+
return f"❌ 오류: {str(e)}"
|
| 343 |
+
|
| 344 |
+
def generate_image(prompt: str, fal_key: str, aspect_ratio: str = "1:1", resolution: str = "1K") -> str:
|
| 345 |
+
if not prompt.strip() or not fal_key.strip():
|
| 346 |
+
return None
|
| 347 |
+
|
| 348 |
+
# FAL API - nano-banana-pro
|
| 349 |
+
url = "https://fal.run/fal-ai/nano-banana-pro"
|
| 350 |
+
headers = {"Authorization": f"Key {fal_key}", "Content-Type": "application/json"}
|
| 351 |
+
|
| 352 |
+
payload = {
|
| 353 |
+
"prompt": prompt,
|
| 354 |
+
"num_images": 1,
|
| 355 |
+
"aspect_ratio": aspect_ratio,
|
| 356 |
+
"output_format": "png",
|
| 357 |
+
"resolution": resolution
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
try:
|
| 361 |
+
response = requests.post(url, headers=headers, json=payload, timeout=120)
|
| 362 |
+
response.raise_for_status()
|
| 363 |
+
data = response.json()
|
| 364 |
+
if "images" in data and data["images"]:
|
| 365 |
+
return data["images"][0].get("url")
|
| 366 |
+
except Exception as e:
|
| 367 |
+
print(f"이미지 생성 오류: {e}")
|
| 368 |
+
return None
|
| 369 |
+
|
| 370 |
+
def process_comparison_with_auth(original_prompt, session_token, fireworks_key, fal_key, aspect_ratio, resolution):
|
| 371 |
+
user = validate_session(session_token)
|
| 372 |
+
if not user:
|
| 373 |
+
yield "❌ 로그인이 필요합니다.", None, None, None, ""
|
| 374 |
+
return
|
| 375 |
+
|
| 376 |
+
allowed, current, limit = check_usage_limit(user["id"], user["daily_limit"])
|
| 377 |
+
if not allowed:
|
| 378 |
+
yield f"❌ 일일 한도({limit}회) 초과", None, None, None, f"{current}/{limit}"
|
| 379 |
+
return
|
| 380 |
+
|
| 381 |
+
fw_key = fireworks_key if fireworks_key.strip() else FIREWORKS_API_KEY
|
| 382 |
+
fl_key = fal_key if fal_key.strip() else FAL_KEY
|
| 383 |
+
|
| 384 |
+
yield f"⚡ 처리 중... ({current}/{limit})", None, None, None, f"{current}/{limit}"
|
| 385 |
+
|
| 386 |
+
enhanced = enhance_prompt(original_prompt, fw_key)
|
| 387 |
+
if enhanced.startswith("❌"):
|
| 388 |
+
yield enhanced, None, None, None, f"{current}/{limit}"
|
| 389 |
+
return
|
| 390 |
+
|
| 391 |
+
yield "✅ 프롬프트 증강 완료\n⚡ 원본 이미지 생성 중...", enhanced, None, None, f"{current}/{limit}"
|
| 392 |
+
original_image = generate_image(original_prompt, fl_key, aspect_ratio, resolution)
|
| 393 |
+
|
| 394 |
+
yield "✅ 원본 이미지 완료\n⚡ 증강 이미지 생성 중...", enhanced, original_image, None, f"{current}/{limit}"
|
| 395 |
+
enhanced_image = generate_image(enhanced, fl_key, aspect_ratio, resolution)
|
| 396 |
+
|
| 397 |
+
new_count = increment_usage(user["id"])
|
| 398 |
+
save_generation(user["id"], original_prompt, enhanced, enhanced_image or original_image, "success" if enhanced_image else "partial")
|
| 399 |
+
|
| 400 |
+
yield f"✅ 완료! 사용량: {new_count}/{limit}", enhanced, original_image, enhanced_image, f"{new_count}/{limit}"
|
| 401 |
|
| 402 |
+
# ============================================
|
| 403 |
+
# CSS - 밝은 프로페셔널 테마
|
| 404 |
+
# ============================================
|
| 405 |
+
CUSTOM_CSS = """
|
| 406 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Poppins:wght@400;500;600;700&display=swap');
|
| 407 |
+
|
| 408 |
+
:root {
|
| 409 |
+
--primary-blue: #2563eb;
|
| 410 |
+
--primary-blue-light: #3b82f6;
|
| 411 |
+
--primary-blue-dark: #1d4ed8;
|
| 412 |
+
--accent-purple: #7c3aed;
|
| 413 |
+
--accent-green: #10b981;
|
| 414 |
+
--accent-orange: #f59e0b;
|
| 415 |
+
--bg-white: #ffffff;
|
| 416 |
+
--bg-gray-50: #f9fafb;
|
| 417 |
+
--bg-gray-100: #f3f4f6;
|
| 418 |
+
--bg-gray-200: #e5e7eb;
|
| 419 |
+
--text-primary: #111827;
|
| 420 |
+
--text-secondary: #4b5563;
|
| 421 |
+
--text-muted: #9ca3af;
|
| 422 |
+
--border-color: #e5e7eb;
|
| 423 |
+
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
| 424 |
+
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
| 425 |
+
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
| 426 |
+
--radius: 12px;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
/* 메인 컨테이너 - 밝은 배경 */
|
| 430 |
+
.gradio-container {
|
| 431 |
+
background: linear-gradient(135deg, var(--bg-gray-50) 0%, var(--bg-white) 50%, var(--bg-gray-100) 100%) !important;
|
| 432 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
|
| 433 |
+
min-height: 100vh;
|
| 434 |
}
|
| 435 |
|
| 436 |
/* 타이틀 스타일 */
|
| 437 |
.cyber-title {
|
| 438 |
+
font-family: 'Poppins', sans-serif !important;
|
| 439 |
+
font-size: 2.5rem !important;
|
| 440 |
+
font-weight: 700 !important;
|
| 441 |
+
background: linear-gradient(135deg, var(--primary-blue) 0%, var(--accent-purple) 100%);
|
|
|
|
|
|
|
|
|
|
| 442 |
-webkit-background-clip: text;
|
| 443 |
-webkit-text-fill-color: transparent;
|
| 444 |
background-clip: text;
|
| 445 |
+
text-align: center;
|
| 446 |
+
letter-spacing: -0.5px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
}
|
| 448 |
|
| 449 |
+
/* 입력 필드 - 밝은 스타일 */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
.gradio-container textarea,
|
| 451 |
.gradio-container input[type="text"],
|
| 452 |
+
.gradio-container input[type="password"],
|
| 453 |
+
.gradio-container input[type="email"] {
|
| 454 |
+
background: var(--bg-white) !important;
|
| 455 |
+
border: 2px solid var(--border-color) !important;
|
| 456 |
+
border-radius: var(--radius) !important;
|
| 457 |
+
color: var(--text-primary) !important;
|
| 458 |
+
font-family: 'Inter', sans-serif !important;
|
| 459 |
+
font-size: 0.95rem !important;
|
| 460 |
+
padding: 12px 16px !important;
|
| 461 |
+
transition: all 0.2s ease !important;
|
| 462 |
}
|
| 463 |
|
| 464 |
.gradio-container textarea:focus,
|
| 465 |
.gradio-container input:focus {
|
| 466 |
+
border-color: var(--primary-blue) !important;
|
| 467 |
+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1) !important;
|
| 468 |
outline: none !important;
|
| 469 |
}
|
| 470 |
|
| 471 |
.gradio-container textarea::placeholder,
|
| 472 |
.gradio-container input::placeholder {
|
| 473 |
+
color: var(--text-muted) !important;
|
| 474 |
}
|
| 475 |
|
| 476 |
+
/* 라벨 스타일 */
|
| 477 |
.gradio-container label {
|
| 478 |
+
font-family: 'Inter', sans-serif !important;
|
| 479 |
+
color: var(--text-primary) !important;
|
|
|
|
| 480 |
font-weight: 600 !important;
|
| 481 |
+
font-size: 0.9rem !important;
|
| 482 |
+
margin-bottom: 6px !important;
|
| 483 |
}
|
| 484 |
|
| 485 |
/* 드롭다운 */
|
| 486 |
.gradio-container select,
|
| 487 |
.gradio-container .wrap-inner {
|
| 488 |
+
background: var(--bg-white) !important;
|
| 489 |
+
border: 2px solid var(--border-color) !important;
|
| 490 |
+
border-radius: var(--radius) !important;
|
| 491 |
+
color: var(--text-primary) !important;
|
|
|
|
| 492 |
}
|
| 493 |
|
| 494 |
+
/* 메인 버튼 - 블루 그라디언트 */
|
| 495 |
.neon-button {
|
| 496 |
+
background: linear-gradient(135deg, var(--primary-blue) 0%, var(--primary-blue-dark) 100%) !important;
|
| 497 |
+
border: none !important;
|
| 498 |
+
border-radius: var(--radius) !important;
|
| 499 |
color: #ffffff !important;
|
| 500 |
+
font-family: 'Inter', sans-serif !important;
|
| 501 |
+
font-size: 0.95rem !important;
|
| 502 |
+
font-weight: 600 !important;
|
| 503 |
+
padding: 12px 24px !important;
|
| 504 |
+
box-shadow: var(--shadow-md), 0 4px 14px rgba(37, 99, 235, 0.25) !important;
|
| 505 |
+
transition: all 0.2s ease !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
}
|
| 507 |
|
| 508 |
+
.neon-button:hover {
|
| 509 |
+
background: linear-gradient(135deg, var(--primary-blue-light) 0%, var(--primary-blue) 100%) !important;
|
| 510 |
+
box-shadow: var(--shadow-lg), 0 6px 20px rgba(37, 99, 235, 0.35) !important;
|
| 511 |
+
transform: translateY(-2px) !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
}
|
| 513 |
|
| 514 |
+
/* 일반 버튼 */
|
| 515 |
+
.gradio-container button {
|
| 516 |
+
border-radius: var(--radius) !important;
|
| 517 |
+
font-family: 'Inter', sans-serif !important;
|
| 518 |
+
transition: all 0.2s ease !important;
|
| 519 |
}
|
| 520 |
|
| 521 |
+
/* 탭 스타일 */
|
| 522 |
+
.gradio-container .tabs {
|
| 523 |
+
background: var(--bg-white) !important;
|
| 524 |
+
border-radius: 16px !important;
|
| 525 |
+
padding: 8px !important;
|
| 526 |
+
box-shadow: var(--shadow-sm) !important;
|
| 527 |
}
|
| 528 |
|
| 529 |
+
.gradio-container .tab-nav button {
|
| 530 |
+
background: transparent !important;
|
| 531 |
+
color: var(--text-secondary) !important;
|
| 532 |
+
border-radius: 10px !important;
|
| 533 |
+
font-weight: 500 !important;
|
| 534 |
+
padding: 10px 20px !important;
|
|
|
|
|
|
|
| 535 |
}
|
| 536 |
|
| 537 |
+
.gradio-container .tab-nav button.selected {
|
| 538 |
+
background: var(--primary-blue) !important;
|
| 539 |
+
color: #ffffff !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
}
|
| 541 |
|
| 542 |
+
/* 아코디언 */
|
| 543 |
+
.gradio-container .accordion {
|
| 544 |
+
background: var(--bg-white) !important;
|
| 545 |
+
border: 1px solid var(--border-color) !important;
|
| 546 |
+
border-radius: var(--radius) !important;
|
| 547 |
+
box-shadow: var(--shadow-sm) !important;
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
/* 마크다운 텍스트 */
|
| 551 |
+
.gradio-container .markdown-text {
|
| 552 |
+
color: var(--text-primary) !important;
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.gradio-container .markdown-text h1,
|
| 556 |
+
.gradio-container .markdown-text h2,
|
| 557 |
+
.gradio-container .markdown-text h3 {
|
| 558 |
+
color: var(--text-primary) !important;
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
/* 테이블 */
|
| 562 |
+
.gradio-container table {
|
| 563 |
+
background: var(--bg-white) !important;
|
| 564 |
+
border-radius: var(--radius) !important;
|
| 565 |
overflow: hidden !important;
|
|
|
|
| 566 |
}
|
| 567 |
|
| 568 |
+
.gradio-container th {
|
| 569 |
+
background: var(--bg-gray-100) !important;
|
| 570 |
+
color: var(--text-primary) !important;
|
| 571 |
+
font-weight: 600 !important;
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
.gradio-container td {
|
| 575 |
+
color: var(--text-secondary) !important;
|
| 576 |
+
border-color: var(--border-color) !important;
|
| 577 |
}
|
| 578 |
|
| 579 |
/* 이미지 컨테이너 */
|
| 580 |
+
.gradio-container .image-container {
|
| 581 |
+
background: var(--bg-white) !important;
|
| 582 |
+
border-radius: var(--radius) !important;
|
| 583 |
+
box-shadow: var(--shadow-md) !important;
|
| 584 |
}
|
| 585 |
|
| 586 |
+
/* 카드 스타일 박스 */
|
| 587 |
+
.gradio-container .block {
|
| 588 |
+
background: var(--bg-white) !important;
|
| 589 |
+
border: 1px solid var(--border-color) !important;
|
| 590 |
+
border-radius: var(--radius) !important;
|
| 591 |
}
|
| 592 |
|
| 593 |
+
/* 슬라이더 */
|
| 594 |
+
.gradio-container input[type="range"] {
|
| 595 |
+
accent-color: var(--primary-blue) !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 596 |
}
|
| 597 |
|
| 598 |
+
/* 체크박스 */
|
| 599 |
+
.gradio-container input[type="checkbox"] {
|
| 600 |
+
accent-color: var(--primary-blue) !important;
|
| 601 |
}
|
| 602 |
|
| 603 |
+
/* 상태 메시지 */
|
| 604 |
+
.gradio-container .message {
|
| 605 |
+
border-radius: var(--radius) !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
}
|
| 607 |
|
| 608 |
+
/* 성공/에러 색상 */
|
| 609 |
+
.success-text {
|
| 610 |
+
color: var(--accent-green) !important;
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
.error-text {
|
| 614 |
+
color: #ef4444 !important;
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
/* 데이터프레임 */
|
| 618 |
+
.gradio-container .dataframe {
|
| 619 |
+
border-radius: var(--radius) !important;
|
| 620 |
+
overflow: hidden !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
}
|
| 622 |
|
| 623 |
+
/* 스크롤바 */
|
| 624 |
+
::-webkit-scrollbar {
|
| 625 |
+
width: 8px;
|
| 626 |
+
height: 8px;
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
::-webkit-scrollbar-track {
|
| 630 |
+
background: var(--bg-gray-100);
|
| 631 |
+
border-radius: 4px;
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
::-webkit-scrollbar-thumb {
|
| 635 |
+
background: var(--bg-gray-300);
|
| 636 |
+
border-radius: 4px;
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
::-webkit-scrollbar-thumb:hover {
|
| 640 |
+
background: var(--text-muted);
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
/* 허깅페이스 배지 숨기기 */
|
| 644 |
.built-with,
|
| 645 |
.built-with-badge,
|
| 646 |
a[href*="huggingface.co/spaces"],
|
|
|
|
| 649 |
#footer,
|
| 650 |
.gradio-container > footer,
|
| 651 |
div[class*="footer"],
|
|
|
|
|
|
|
| 652 |
.space-info,
|
| 653 |
.hf-space-header,
|
| 654 |
[class*="space-header"],
|
| 655 |
+
div.wrap.svelte-1rjryqp,
|
| 656 |
.svelte-1rjryqp:has(a[href*="huggingface"]),
|
| 657 |
+
a.svelte-1rjryqp[href*="huggingface"] {
|
| 658 |
display: none !important;
|
| 659 |
visibility: hidden !important;
|
| 660 |
opacity: 0 !important;
|
| 661 |
height: 0 !important;
|
| 662 |
width: 0 !important;
|
|
|
|
| 663 |
position: absolute !important;
|
| 664 |
pointer-events: none !important;
|
|
|
|
| 665 |
}
|
| 666 |
|
|
|
|
|
|
|
|
|
|
| 667 |
div[style*="position: fixed"][style*="right"],
|
| 668 |
+
div[style*="position: fixed"][style*="top: 0"] {
|
|
|
|
|
|
|
|
|
|
| 669 |
display: none !important;
|
|
|
|
|
|
|
| 670 |
}
|
| 671 |
"""
|
| 672 |
|
| 673 |
# ============================================
|
| 674 |
+
# Gradio UI
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 675 |
# ============================================
|
| 676 |
+
init_db()
|
| 677 |
|
| 678 |
examples = [
|
| 679 |
["한복을 입은 여성이 전통 한옥 마당에서 부채를 들고 있다"],
|
| 680 |
["사이버펑크 도시의 네온사인이 빛나는 밤거리"],
|
| 681 |
["바다 위 일몰과 작은 돛단배"],
|
|
|
|
|
|
|
| 682 |
]
|
| 683 |
|
| 684 |
with gr.Blocks(title="AI PROMPT ENHANCER", css=CUSTOM_CSS) as demo:
|
| 685 |
+
session_token = gr.State(value="")
|
| 686 |
|
| 687 |
+
gr.HTML('''
|
| 688 |
+
<div style="text-align:center; padding:40px 20px; background: linear-gradient(135deg, #ffffff 0%, #f0f9ff 100%); border-radius: 16px; margin-bottom: 20px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);">
|
| 689 |
+
<h1 class="cyber-title">🚀 PROMPT ENHANCER</h1>
|
| 690 |
+
<p style="color: #4b5563; font-size: 1.1rem; margin-top: 8px;">AI-Powered Image Generation Platform</p>
|
| 691 |
+
</div>
|
| 692 |
+
''')
|
| 693 |
+
|
| 694 |
+
with gr.Tabs() as tabs:
|
| 695 |
+
# Tab 1: 로그인
|
| 696 |
+
with gr.Tab("🔐 로그인"):
|
| 697 |
+
with gr.Row():
|
| 698 |
+
with gr.Column(scale=1):
|
| 699 |
+
pass
|
| 700 |
+
with gr.Column(scale=2):
|
| 701 |
+
with gr.Tabs():
|
| 702 |
+
with gr.Tab("로그인"):
|
| 703 |
+
login_email = gr.Textbox(label="이메일", placeholder="your@email.com")
|
| 704 |
+
login_password = gr.Textbox(label="비밀번호", type="password")
|
| 705 |
+
login_btn = gr.Button("로그인", elem_classes=["neon-button"])
|
| 706 |
+
login_status = gr.Markdown("")
|
| 707 |
+
with gr.Tab("회원가입"):
|
| 708 |
+
reg_email = gr.Textbox(label="이메일", placeholder="your@email.com")
|
| 709 |
+
reg_password = gr.Textbox(label="비밀번호", type="password", placeholder="6자 이상")
|
| 710 |
+
reg_confirm = gr.Textbox(label="비밀번호 확인", type="password")
|
| 711 |
+
reg_btn = gr.Button("회원가입", elem_classes=["neon-button"])
|
| 712 |
+
reg_status = gr.Markdown("")
|
| 713 |
+
current_user_display = gr.Markdown("로그인하지 않음")
|
| 714 |
+
logout_btn = gr.Button("로그아웃", visible=False)
|
| 715 |
+
with gr.Column(scale=1):
|
| 716 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 717 |
|
| 718 |
+
# Tab 2: 메인 서비스
|
| 719 |
+
with gr.Tab("✨ 이미지 생성"):
|
| 720 |
+
usage_display = gr.Markdown("사용량: 로그인 필요")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 721 |
|
| 722 |
+
with gr.Accordion("⚙️ API 설정", open=False):
|
| 723 |
+
with gr.Row():
|
| 724 |
+
fireworks_key_input = gr.Textbox(label="FIREWORKS API KEY", placeholder="환경변수 사용", type="password")
|
| 725 |
+
fal_key_input = gr.Textbox(label="FAL API KEY", placeholder="환경변수 사용", type="password")
|
| 726 |
|
| 727 |
+
with gr.Row():
|
| 728 |
+
with gr.Column(scale=2):
|
| 729 |
+
prompt_input = gr.Textbox(label="프롬프트 입력", placeholder="생성할 이미지를 설명해주세요...", lines=4)
|
| 730 |
+
gr.Examples(examples=examples, inputs=prompt_input, label="예시")
|
| 731 |
+
with gr.Column(scale=1):
|
| 732 |
+
aspect_ratio = gr.Dropdown(label="비율", choices=["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "21:9"], value="1:1")
|
| 733 |
+
resolution = gr.Dropdown(label="해상도", choices=["1K", "2K", "4K"], value="1K")
|
| 734 |
+
generate_btn = gr.Button("⚡ 생성하기", elem_classes=["neon-button"], size="lg")
|
| 735 |
+
|
| 736 |
+
status_text = gr.Textbox(label="상태", interactive=False, lines=2)
|
| 737 |
+
enhanced_output = gr.Textbox(label="증강된 프롬프트", lines=3)
|
| 738 |
+
|
| 739 |
+
with gr.Row():
|
| 740 |
+
with gr.Column():
|
| 741 |
+
gr.Markdown("### 📌 원본")
|
| 742 |
+
original_image_output = gr.Image(label="", height=400)
|
| 743 |
+
with gr.Column():
|
| 744 |
+
gr.Markdown("### ✨ 증강")
|
| 745 |
+
enhanced_image_output = gr.Image(label="", height=400)
|
| 746 |
+
|
| 747 |
+
# Tab 3: 내 계정
|
| 748 |
+
with gr.Tab("👤 내 계정") as account_tab:
|
| 749 |
+
account_info = gr.Markdown("로그인이 필요합니다.")
|
| 750 |
+
history_display = gr.Dataframe(headers=["프롬프트", "생성일시", "상태"], interactive=False)
|
| 751 |
+
refresh_history_btn = gr.Button("🔄 새로고침")
|
| 752 |
+
|
| 753 |
+
# Tab 4: 관리자
|
| 754 |
+
with gr.Tab("🛠️ 관리자") as admin_tab:
|
| 755 |
+
admin_auth_status = gr.Markdown("관리자 권한이 필요합니다.")
|
| 756 |
+
with gr.Row(visible=False) as admin_panel:
|
| 757 |
+
with gr.Column():
|
| 758 |
+
gr.Markdown("### 📊 통계")
|
| 759 |
+
stats_display = gr.Markdown("")
|
| 760 |
+
refresh_stats_btn = gr.Button("통계 새로고침")
|
| 761 |
+
with gr.Column():
|
| 762 |
+
gr.Markdown("### 👥 사용자 관리")
|
| 763 |
+
users_table = gr.Dataframe(headers=["ID", "이메일", "관리자", "활성", "일일제한", "가입일"], interactive=False)
|
| 764 |
+
refresh_users_btn = gr.Button("사용자 새로고침")
|
| 765 |
+
with gr.Row(visible=False) as admin_actions:
|
| 766 |
+
user_id_input = gr.Number(label="사용자 ID", precision=0)
|
| 767 |
+
toggle_active_btn = gr.Button("활성화/비활성화")
|
| 768 |
+
new_limit = gr.Number(label="새 일일 제한", value=10, precision=0)
|
| 769 |
+
update_limit_btn = gr.Button("제한 변경")
|
| 770 |
+
admin_action_status = gr.Markdown("")
|
| 771 |
+
|
| 772 |
+
# ========== 이벤트 핸들러 ==========
|
| 773 |
+
def do_login(email, password):
|
| 774 |
+
token, msg = login_user(email, password)
|
| 775 |
+
if token:
|
| 776 |
+
user = validate_session(token)
|
| 777 |
+
user_info = f"✅ **{user['email']}**" + (" (관리자)" if user['is_admin'] else "")
|
| 778 |
+
return token, msg, user_info, gr.update(visible=True)
|
| 779 |
+
return "", msg, "로그인하지 않음", gr.update(visible=False)
|
| 780 |
|
| 781 |
+
def do_register(email, password, confirm):
|
| 782 |
+
token, msg = register_user(email, password, confirm)
|
| 783 |
+
if token:
|
| 784 |
+
user = validate_session(token)
|
| 785 |
+
return token, msg, f"✅ **{user['email']}**", gr.update(visible=True)
|
| 786 |
+
return "", msg, "로그인하지 않음", gr.update(visible=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 787 |
|
| 788 |
+
def do_logout(token):
|
| 789 |
+
logout_user(token)
|
| 790 |
+
return "", "로그인하지 않음", gr.update(visible=False), "✅ 로그아웃됨"
|
| 791 |
+
|
| 792 |
+
def load_account_info(token):
|
| 793 |
+
user = validate_session(token)
|
| 794 |
+
if not user:
|
| 795 |
+
return "로그인이 필요합니다.", []
|
| 796 |
+
info = f"### 👤 계정 정보\n- **이메일**: {user['email']}\n- **등급**: {'관리자' if user['is_admin'] else '일반'}\n- **일일 제한**: {user['daily_limit']}회\n- **오늘 사용량**: {get_daily_usage(user['id'])}회"
|
| 797 |
+
history = get_user_generations(user["id"])
|
| 798 |
+
history_data = [[h["original_prompt"][:50]+"...", h["created_at"], h["status"]] for h in history]
|
| 799 |
+
return info, history_data
|
| 800 |
+
|
| 801 |
+
def load_admin_panel(token):
|
| 802 |
+
user = validate_session(token)
|
| 803 |
+
if not user or not user["is_admin"]:
|
| 804 |
+
return "❌ 관리자 권한이 필요합니다.", gr.update(visible=False), gr.update(visible=False), "", []
|
| 805 |
+
stats = get_stats()
|
| 806 |
+
stats_md = f"| 지표 | 값 |\n|---|---|\n| 총 사용자 | {stats['total_users']} |\n| 오늘 활성 | {stats['active_today']} |\n| 총 생성 | {stats['total_generations']} |\n| 오늘 생성 | {stats['generations_today']} |"
|
| 807 |
+
users = get_all_users()
|
| 808 |
+
users_data = [[u["id"], u["email"], "✅" if u["is_admin"] else "", "✅" if u["is_active"] else "❌", u["daily_limit"], u["created_at"]] for u in users]
|
| 809 |
+
return "✅ 관리자 패널", gr.update(visible=True), gr.update(visible=True), stats_md, users_data
|
| 810 |
+
|
| 811 |
+
def toggle_user_active(token, user_id):
|
| 812 |
+
user = validate_session(token)
|
| 813 |
+
if not user or not user["is_admin"]:
|
| 814 |
+
return "❌ 권한 없음"
|
| 815 |
+
conn = get_db()
|
| 816 |
+
cursor = conn.cursor()
|
| 817 |
+
cursor.execute("UPDATE users SET is_active = NOT is_active WHERE id = ?", (int(user_id),))
|
| 818 |
+
conn.commit()
|
| 819 |
+
conn.close()
|
| 820 |
+
return "✅ 상태 변경됨"
|
| 821 |
+
|
| 822 |
+
def change_user_limit(token, user_id, new_limit):
|
| 823 |
+
user = validate_session(token)
|
| 824 |
+
if not user or not user["is_admin"]:
|
| 825 |
+
return "❌ 권한 없음"
|
| 826 |
+
update_user_limit(int(user_id), int(new_limit))
|
| 827 |
+
return f"✅ 제한 변경됨: {int(new_limit)}"
|
| 828 |
+
|
| 829 |
+
# 이벤트 바인딩
|
| 830 |
+
login_btn.click(do_login, [login_email, login_password], [session_token, login_status, current_user_display, logout_btn])
|
| 831 |
+
reg_btn.click(do_register, [reg_email, reg_password, reg_confirm], [session_token, reg_status, current_user_display, logout_btn])
|
| 832 |
+
logout_btn.click(do_logout, [session_token], [session_token, current_user_display, logout_btn, login_status])
|
| 833 |
+
generate_btn.click(process_comparison_with_auth, [prompt_input, session_token, fireworks_key_input, fal_key_input, aspect_ratio, resolution], [status_text, enhanced_output, original_image_output, enhanced_image_output, usage_display])
|
| 834 |
+
refresh_history_btn.click(load_account_info, [session_token], [account_info, history_display])
|
| 835 |
+
refresh_stats_btn.click(load_admin_panel, [session_token], [admin_auth_status, admin_panel, admin_actions, stats_display, users_table])
|
| 836 |
+
refresh_users_btn.click(load_admin_panel, [session_token], [admin_auth_status, admin_panel, admin_actions, stats_display, users_table])
|
| 837 |
+
toggle_active_btn.click(toggle_user_active, [session_token, user_id_input], [admin_action_status])
|
| 838 |
+
update_limit_btn.click(change_user_limit, [session_token, user_id_input, new_limit], [admin_action_status])
|
| 839 |
+
|
| 840 |
+
# 탭 선택 시 자동 로드
|
| 841 |
+
admin_tab.select(load_admin_panel, [session_token], [admin_auth_status, admin_panel, admin_actions, stats_display, users_table])
|
| 842 |
+
account_tab.select(load_account_info, [session_token], [account_info, history_display])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 843 |
|
| 844 |
if __name__ == "__main__":
|
| 845 |
demo.launch()
|