Update app.py
Browse files
app.py
CHANGED
|
@@ -1,30 +1,110 @@
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
import os
|
| 3 |
import tempfile
|
| 4 |
from datetime import datetime
|
| 5 |
from PIL import Image
|
|
|
|
| 6 |
|
| 7 |
-
#
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
#
|
| 15 |
try:
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
except ImportError as e:
|
| 19 |
-
print(f"❌
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
# 커스텀 CSS 스타일 (
|
| 23 |
custom_css = """
|
| 24 |
/* ============================================
|
| 25 |
다크모드 자동 변경 템플릿 CSS
|
| 26 |
============================================ */
|
| 27 |
-
|
| 28 |
/* 1. CSS 변수 정의 (라이트모드 - 기본값) */
|
| 29 |
:root {
|
| 30 |
/* 메인 컬러 */
|
|
@@ -56,7 +136,6 @@ custom_css = """
|
|
| 56 |
/* 기타 */
|
| 57 |
--border-radius: 18px;
|
| 58 |
}
|
| 59 |
-
|
| 60 |
/* 2. 다크모드 색상 변수 (자동 감지) */
|
| 61 |
@media (prefers-color-scheme: dark) {
|
| 62 |
:root {
|
|
@@ -82,7 +161,6 @@ custom_css = """
|
|
| 82 |
--shadow-light: 0 2px 4px rgba(0, 0, 0, 0.2);
|
| 83 |
}
|
| 84 |
}
|
| 85 |
-
|
| 86 |
/* 3. 수동 다크모드 클래스 (Gradio 토글용) */
|
| 87 |
[data-theme="dark"],
|
| 88 |
.dark,
|
|
@@ -108,7 +186,6 @@ custom_css = """
|
|
| 108 |
--shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
|
| 109 |
--shadow-light: 0 2px 4px rgba(0, 0, 0, 0.2);
|
| 110 |
}
|
| 111 |
-
|
| 112 |
/* 4. 기본 요소 다크모드 적용 */
|
| 113 |
body {
|
| 114 |
font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
@@ -120,7 +197,6 @@ body {
|
|
| 120 |
font-size: 16px;
|
| 121 |
transition: background-color 0.3s ease, color 0.3s ease;
|
| 122 |
}
|
| 123 |
-
|
| 124 |
/* 5. Gradio 컨테이너 강제 적용 */
|
| 125 |
.gradio-container,
|
| 126 |
.gradio-container *,
|
|
@@ -130,19 +206,16 @@ body {
|
|
| 130 |
background-color: var(--background-color) !important;
|
| 131 |
color: var(--text-color) !important;
|
| 132 |
}
|
| 133 |
-
|
| 134 |
/* 푸터 숨김 설정 추가 */
|
| 135 |
footer {
|
| 136 |
visibility: hidden;
|
| 137 |
}
|
| 138 |
-
|
| 139 |
.gradio-container {
|
| 140 |
width: 100%;
|
| 141 |
margin: 0 auto;
|
| 142 |
padding: 20px;
|
| 143 |
background-color: var(--background-color);
|
| 144 |
}
|
| 145 |
-
|
| 146 |
/* ── 그룹 래퍼 배경 완전 제거 ── */
|
| 147 |
.custom-section-group,
|
| 148 |
.gr-block.gr-group {
|
|
@@ -156,13 +229,11 @@ footer {
|
|
| 156 |
display: none !important;
|
| 157 |
content: none !important;
|
| 158 |
}
|
| 159 |
-
|
| 160 |
/* 그룹 컨테이너 배경을 아이보리로, 그림자 제거 */
|
| 161 |
.custom-section-group {
|
| 162 |
background-color: var(--background-color) !important;
|
| 163 |
box-shadow: none !important;
|
| 164 |
}
|
| 165 |
-
|
| 166 |
/* 6. 카드 및 패널 스타일 */
|
| 167 |
.custom-frame,
|
| 168 |
.gr-form,
|
|
@@ -179,7 +250,6 @@ footer {
|
|
| 179 |
box-shadow: var(--shadow) !important;
|
| 180 |
color: var(--text-color) !important;
|
| 181 |
}
|
| 182 |
-
|
| 183 |
/* 섹션 그룹 스타일 - 회색 배경 완전 제거 */
|
| 184 |
.custom-section-group {
|
| 185 |
margin-top: 20px;
|
|
@@ -189,7 +259,6 @@ footer {
|
|
| 189 |
background-color: var(--background-color);
|
| 190 |
box-shadow: none !important;
|
| 191 |
}
|
| 192 |
-
|
| 193 |
/* 버튼 스타일 - 글자 크기 18px */
|
| 194 |
.custom-button {
|
| 195 |
border-radius: 30px !important;
|
|
@@ -205,7 +274,6 @@ footer {
|
|
| 205 |
transform: translateY(-2px);
|
| 206 |
box-shadow: 0 6px 12px rgba(251, 127, 13, 0.3);
|
| 207 |
}
|
| 208 |
-
|
| 209 |
/* 제목 스타일 (모든 항목명이 동일하게 custom-title 클래스로) */
|
| 210 |
.custom-title {
|
| 211 |
font-size: 28px;
|
|
@@ -215,7 +283,6 @@ footer {
|
|
| 215 |
border-bottom: 2px solid var(--primary-color);
|
| 216 |
padding-bottom: 5px;
|
| 217 |
}
|
| 218 |
-
|
| 219 |
/* 이미지 컨테이너 - 크기 고정 */
|
| 220 |
.image-container {
|
| 221 |
border-radius: var(--border-radius);
|
|
@@ -224,7 +291,6 @@ footer {
|
|
| 224 |
transition: all 0.3s ease;
|
| 225 |
background-color: var(--card-bg);
|
| 226 |
}
|
| 227 |
-
|
| 228 |
/* 이미지 업로드 영역 개선 */
|
| 229 |
.gradio-container .gr-image {
|
| 230 |
border: 2px dashed var(--border-color) !important;
|
|
@@ -232,12 +298,10 @@ footer {
|
|
| 232 |
background-color: var(--card-bg) !important;
|
| 233 |
transition: all 0.3s ease !important;
|
| 234 |
}
|
| 235 |
-
|
| 236 |
.gradio-container .gr-image:hover {
|
| 237 |
border-color: var(--primary-color) !important;
|
| 238 |
box-shadow: 0 4px 12px rgba(251, 127, 13, 0.15) !important;
|
| 239 |
}
|
| 240 |
-
|
| 241 |
/* 업로드 영역 내부 텍스트 */
|
| 242 |
.gradio-container .gr-image .upload-container,
|
| 243 |
.gradio-container .gr-image [data-testid="upload-container"] {
|
|
@@ -245,14 +309,12 @@ footer {
|
|
| 245 |
color: var(--text-color) !important;
|
| 246 |
border: none !important;
|
| 247 |
}
|
| 248 |
-
|
| 249 |
/* 업로드 영역 드래그 안내 텍스트 */
|
| 250 |
.gradio-container .gr-image .upload-container p,
|
| 251 |
.gradio-container .gr-image [data-testid="upload-container"] p {
|
| 252 |
color: var(--text-color) !important;
|
| 253 |
font-size: 14px !important;
|
| 254 |
}
|
| 255 |
-
|
| 256 |
/* 업로드 버튼 스타일 개선 */
|
| 257 |
.gradio-container .gr-image .upload-container button,
|
| 258 |
.gradio-container .gr-image [data-testid="upload-container"] button {
|
|
@@ -265,13 +327,11 @@ footer {
|
|
| 265 |
cursor: pointer !important;
|
| 266 |
transition: all 0.3s ease !important;
|
| 267 |
}
|
| 268 |
-
|
| 269 |
.gradio-container .gr-image .upload-container button:hover,
|
| 270 |
.gradio-container .gr-image [data-testid="upload-container"] button:hover {
|
| 271 |
background-color: var(--secondary-color) !important;
|
| 272 |
transform: translateY(-1px) !important;
|
| 273 |
}
|
| 274 |
-
|
| 275 |
/* 업로드 영역 아이콘 */
|
| 276 |
.gradio-container .gr-image .upload-container svg,
|
| 277 |
.gradio-container .gr-image [data-testid="upload-container"] svg {
|
|
@@ -279,13 +339,11 @@ footer {
|
|
| 279 |
width: 32px !important;
|
| 280 |
height: 32px !important;
|
| 281 |
}
|
| 282 |
-
|
| 283 |
/* 이미지가 업로드된 후 표시 영역 */
|
| 284 |
.gradio-container .gr-image img {
|
| 285 |
background-color: var(--card-bg) !important;
|
| 286 |
border-radius: var(--border-radius) !important;
|
| 287 |
}
|
| 288 |
-
|
| 289 |
/* 이미지 제거 버튼 */
|
| 290 |
.gradio-container .gr-image .image-container button,
|
| 291 |
.gradio-container .gr-image [data-testid="image"] button {
|
|
@@ -301,13 +359,11 @@ footer {
|
|
| 301 |
cursor: pointer !important;
|
| 302 |
transition: all 0.3s ease !important;
|
| 303 |
}
|
| 304 |
-
|
| 305 |
.gradio-container .gr-image .image-container button:hover,
|
| 306 |
.gradio-container .gr-image [data-testid="image"] button:hover {
|
| 307 |
background-color: rgba(255, 255, 255, 1) !important;
|
| 308 |
transform: scale(1.1) !important;
|
| 309 |
}
|
| 310 |
-
|
| 311 |
/* 업로드 이미지 컨테이너 (600x600) */
|
| 312 |
.upload-image-container {
|
| 313 |
width: 600px !important;
|
|
@@ -317,7 +373,6 @@ footer {
|
|
| 317 |
max-width: 600px !important;
|
| 318 |
max-height: 600px !important;
|
| 319 |
}
|
| 320 |
-
|
| 321 |
/* 출력 이미지 컨테이너 (700x600) */
|
| 322 |
.output-image-container {
|
| 323 |
width: 700px !important;
|
|
@@ -327,17 +382,14 @@ footer {
|
|
| 327 |
max-width: 700px !important;
|
| 328 |
max-height: 600px !important;
|
| 329 |
}
|
| 330 |
-
|
| 331 |
.image-container:hover {
|
| 332 |
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
| 333 |
}
|
| 334 |
-
|
| 335 |
.image-container img {
|
| 336 |
width: 100% !important;
|
| 337 |
height: 100% !important;
|
| 338 |
object-fit: contain !important;
|
| 339 |
}
|
| 340 |
-
|
| 341 |
/* Gradio 업로드 이미지 컴포넌트 크기 고정 (600x600) */
|
| 342 |
.gradio-container .gr-image.upload-image {
|
| 343 |
width: 600px !important;
|
|
@@ -347,7 +399,6 @@ footer {
|
|
| 347 |
max-width: 600px !important;
|
| 348 |
max-height: 600px !important;
|
| 349 |
}
|
| 350 |
-
|
| 351 |
/* Gradio 출력 이미지 컴포넌트 크기 고정 (700x600) */
|
| 352 |
.gradio-container .gr-image.output-image {
|
| 353 |
width: 700px !important;
|
|
@@ -357,7 +408,6 @@ footer {
|
|
| 357 |
max-width: 700px !important;
|
| 358 |
max-height: 600px !important;
|
| 359 |
}
|
| 360 |
-
|
| 361 |
/* 이미지 업로드 영역 크기 고정 */
|
| 362 |
.gradio-container .gr-image.upload-image > div {
|
| 363 |
width: 600px !important;
|
|
@@ -367,7 +417,6 @@ footer {
|
|
| 367 |
max-width: 600px !important;
|
| 368 |
max-height: 600px !important;
|
| 369 |
}
|
| 370 |
-
|
| 371 |
/* 이미지 출력 영역 크기 고정 */
|
| 372 |
.gradio-container .gr-image.output-image > div {
|
| 373 |
width: 700px !important;
|
|
@@ -377,7 +426,6 @@ footer {
|
|
| 377 |
max-width: 700px !important;
|
| 378 |
max-height: 600px !important;
|
| 379 |
}
|
| 380 |
-
|
| 381 |
/* 이미지 업로드 드래그 영역 크기 고정 */
|
| 382 |
.gradio-container .gr-image.upload-image .image-container,
|
| 383 |
.gradio-container .gr-image.upload-image [data-testid="image"],
|
|
@@ -389,7 +437,6 @@ footer {
|
|
| 389 |
max-width: 600px !important;
|
| 390 |
max-height: 600px !important;
|
| 391 |
}
|
| 392 |
-
|
| 393 |
/* 이미지 출력 드래그 영역 크기 고정 */
|
| 394 |
.gradio-container .gr-image.output-image .image-container,
|
| 395 |
.gradio-container .gr-image.output-image [data-testid="image"],
|
|
@@ -401,7 +448,6 @@ footer {
|
|
| 401 |
max-width: 700px !important;
|
| 402 |
max-height: 600px !important;
|
| 403 |
}
|
| 404 |
-
|
| 405 |
/* 7. 입력 필드 스타일 */
|
| 406 |
.gr-input, .gr-text-input, .gr-sample-inputs,
|
| 407 |
input[type="text"],
|
|
@@ -421,7 +467,6 @@ select,
|
|
| 421 |
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05) !important;
|
| 422 |
transition: all 0.3s ease !important;
|
| 423 |
}
|
| 424 |
-
|
| 425 |
.gr-input:focus, .gr-text-input:focus,
|
| 426 |
input[type="text"]:focus,
|
| 427 |
input[type="number"]:focus,
|
|
@@ -435,7 +480,6 @@ select:focus,
|
|
| 435 |
outline: none !important;
|
| 436 |
box-shadow: 0 0 0 2px rgba(251, 127, 13, 0.2) !important;
|
| 437 |
}
|
| 438 |
-
|
| 439 |
/* 8. 라벨 및 텍스트 요소 */
|
| 440 |
.gradio-container label,
|
| 441 |
label,
|
|
@@ -448,12 +492,10 @@ p, span, div {
|
|
| 448 |
font-weight: 600 !important;
|
| 449 |
margin-bottom: 8px !important;
|
| 450 |
}
|
| 451 |
-
|
| 452 |
/* 드롭다운 및 라디오 버튼 폰트 크기 */
|
| 453 |
.gr-radio label, .gr-dropdown label, .gr-checkbox label {
|
| 454 |
font-size: 15px !important;
|
| 455 |
}
|
| 456 |
-
|
| 457 |
/* 라디오 버튼 선택지 볼드 처리 제거 */
|
| 458 |
.gr-radio .gr-radio-option label,
|
| 459 |
.gr-radio input[type="radio"] + label,
|
|
@@ -461,26 +503,22 @@ p, span, div {
|
|
| 461 |
font-weight: normal !important;
|
| 462 |
font-size: 15px !important;
|
| 463 |
}
|
| 464 |
-
|
| 465 |
/* 라디오 버튼 그룹 내 모든 라벨 일반 폰트로 설정 */
|
| 466 |
.gr-radio fieldset label {
|
| 467 |
font-weight: normal !important;
|
| 468 |
}
|
| 469 |
-
|
| 470 |
/* 마크다운 텍스트 크기 증가 */
|
| 471 |
.gradio-container .gr-markdown {
|
| 472 |
font-size: 15px !important;
|
| 473 |
line-height: 1.6 !important;
|
| 474 |
color: var(--text-color) !important;
|
| 475 |
}
|
| 476 |
-
|
| 477 |
/* 텍스트박스 내용 폰트 크기 */
|
| 478 |
.gr-textbox textarea, .gr-textbox input {
|
| 479 |
font-size: 15px !important;
|
| 480 |
background-color: var(--input-bg) !important;
|
| 481 |
color: var(--text-color) !important;
|
| 482 |
}
|
| 483 |
-
|
| 484 |
/* 아코디언 제목 폰트 크기 */
|
| 485 |
.gr-accordion summary {
|
| 486 |
font-size: 17px !important;
|
|
@@ -488,44 +526,36 @@ p, span, div {
|
|
| 488 |
background-color: var(--card-bg) !important;
|
| 489 |
color: var(--text-color) !important;
|
| 490 |
}
|
| 491 |
-
|
| 492 |
/* 메인 컨텐츠 스크롤바 */
|
| 493 |
::-webkit-scrollbar {
|
| 494 |
width: 8px;
|
| 495 |
height: 8px;
|
| 496 |
}
|
| 497 |
-
|
| 498 |
::-webkit-scrollbar-track {
|
| 499 |
background: var(--card-bg);
|
| 500 |
border-radius: 10px;
|
| 501 |
}
|
| 502 |
-
|
| 503 |
::-webkit-scrollbar-thumb {
|
| 504 |
background: var(--primary-color);
|
| 505 |
border-radius: 10px;
|
| 506 |
}
|
| 507 |
-
|
| 508 |
::-webkit-scrollbar-thumb:hover {
|
| 509 |
background: var(--secondary-color);
|
| 510 |
}
|
| 511 |
-
|
| 512 |
/* 애니메이션 스타일 */
|
| 513 |
@keyframes fadeIn {
|
| 514 |
from { opacity: 0; transform: translateY(10px); }
|
| 515 |
to { opacity: 1; transform: translateY(0); }
|
| 516 |
}
|
| 517 |
-
|
| 518 |
.fade-in {
|
| 519 |
animation: fadeIn 0.5s ease-out;
|
| 520 |
}
|
| 521 |
-
|
| 522 |
/* 반응형 */
|
| 523 |
@media (max-width: 768px) {
|
| 524 |
.button-grid {
|
| 525 |
grid-template-columns: repeat(2, 1fr);
|
| 526 |
}
|
| 527 |
}
|
| 528 |
-
|
| 529 |
/* 섹션 제목 스타일 */
|
| 530 |
.section-title {
|
| 531 |
display: flex;
|
|
@@ -538,7 +568,6 @@ p, span, div {
|
|
| 538 |
border-bottom: 2px solid var(--primary-color);
|
| 539 |
font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 540 |
}
|
| 541 |
-
|
| 542 |
.section-title img {
|
| 543 |
margin-right: 12px;
|
| 544 |
width: 28px;
|
|
@@ -546,21 +575,18 @@ p, span, div {
|
|
| 546 |
/* 다크모드에서 아이콘 필터 적용 */
|
| 547 |
filter: brightness(0) saturate(100%) invert(27%) sepia(51%) saturate(2878%) hue-rotate(346deg) brightness(104%) contrast(97%);
|
| 548 |
}
|
| 549 |
-
|
| 550 |
/* 라이트모드에서는 원래 아이��� 색상 유지 */
|
| 551 |
@media (prefers-color-scheme: light) {
|
| 552 |
.section-title img {
|
| 553 |
filter: none;
|
| 554 |
}
|
| 555 |
}
|
| 556 |
-
|
| 557 |
/* 수동 다크모드 클래스에서도 아이콘 색상 적용 */
|
| 558 |
[data-theme="dark"] .section-title img,
|
| 559 |
.dark .section-title img,
|
| 560 |
.gr-theme-dark .section-title img {
|
| 561 |
filter: brightness(0) saturate(100%) invert(27%) sepia(51%) saturate(2878%) hue-rotate(346deg) brightness(104%) contrast(97%);
|
| 562 |
}
|
| 563 |
-
|
| 564 |
/* 10. 아코디언 및 드롭다운 - 수동설정 영역 회색 배경 제거 */
|
| 565 |
details,
|
| 566 |
.gr-accordion,
|
|
@@ -571,7 +597,6 @@ details,
|
|
| 571 |
border-radius: var(--border-radius) !important;
|
| 572 |
margin: 10px 0 !important;
|
| 573 |
}
|
| 574 |
-
|
| 575 |
details summary,
|
| 576 |
.gr-accordion summary,
|
| 577 |
.gr-accordion details summary {
|
|
@@ -584,20 +609,17 @@ details summary,
|
|
| 584 |
font-weight: 600 !important;
|
| 585 |
transition: all 0.3s ease !important;
|
| 586 |
}
|
| 587 |
-
|
| 588 |
details summary:hover,
|
| 589 |
.gr-accordion summary:hover,
|
| 590 |
.gr-accordion details summary:hover {
|
| 591 |
background-color: var(--table-hover-bg) !important;
|
| 592 |
}
|
| 593 |
-
|
| 594 |
/* 아코디언 내부 콘텐츠 */
|
| 595 |
details[open],
|
| 596 |
.gr-accordion[open],
|
| 597 |
.gr-accordion details[open] {
|
| 598 |
background-color: var(--background-color) !important;
|
| 599 |
}
|
| 600 |
-
|
| 601 |
details[open] > *:not(summary),
|
| 602 |
.gr-accordion[open] > *:not(summary),
|
| 603 |
.gr-accordion details[open] > *:not(summary) {
|
|
@@ -606,7 +628,6 @@ details[open] > *:not(summary),
|
|
| 606 |
padding: 16px !important;
|
| 607 |
border-top: 1px solid var(--border-color) !important;
|
| 608 |
}
|
| 609 |
-
|
| 610 |
/* 그룹 내부 스타일 - 수동설정 내부 그룹들 */
|
| 611 |
.gr-group,
|
| 612 |
details .gr-group,
|
|
@@ -617,7 +638,6 @@ details .gr-group,
|
|
| 617 |
margin: 8px 0 !important;
|
| 618 |
border-radius: var(--border-radius) !important;
|
| 619 |
}
|
| 620 |
-
|
| 621 |
/* 그룹 내부 제목 */
|
| 622 |
.gr-group .gr-markdown h3,
|
| 623 |
.gr-group h3 {
|
|
@@ -628,7 +648,6 @@ details .gr-group,
|
|
| 628 |
padding-bottom: 6px !important;
|
| 629 |
border-bottom: 1px solid var(--border-color) !important;
|
| 630 |
}
|
| 631 |
-
|
| 632 |
/* 11. 추가 Gradio 컴포넌트들 */
|
| 633 |
.gr-block,
|
| 634 |
.gr-group,
|
|
@@ -637,23 +656,12 @@ details .gr-group,
|
|
| 637 |
background-color: var(--background-color) !important;
|
| 638 |
color: var(--text-color) !important;
|
| 639 |
}
|
| 640 |
-
|
| 641 |
/* 12. 버튼은 기존 스타일 유지 (primary-color 사용) */
|
| 642 |
button:not([class*="custom"]):not([class*="primary"]):not([class*="secondary"]) {
|
| 643 |
background-color: var(--card-bg) !important;
|
| 644 |
color: var(--text-color) !important;
|
| 645 |
border-color: var(--border-color) !important;
|
| 646 |
}
|
| 647 |
-
|
| 648 |
-
/* 13. 코드 블록 및 pre 태그 */
|
| 649 |
-
code,
|
| 650 |
-
pre,
|
| 651 |
-
.code-block {
|
| 652 |
-
background-color: var(--table-even-bg) !important;
|
| 653 |
-
color: var(--text-color) !important;
|
| 654 |
-
border-color: var(--border-color) !important;
|
| 655 |
-
}
|
| 656 |
-
|
| 657 |
/* 14. 전환 애니메이션 */
|
| 658 |
* {
|
| 659 |
transition: background-color 0.3s ease,
|
|
@@ -719,22 +727,13 @@ def prepare_download_file(image, keyword):
|
|
| 719 |
image_to_save.save(file_path, 'JPEG', quality=95)
|
| 720 |
|
| 721 |
print(f"✅ 다운로드 파일 준비: {filename}")
|
|
|
|
| 722 |
return file_path
|
| 723 |
|
| 724 |
except Exception as e:
|
| 725 |
print(f"❌ 다운로드 파일 준비 실패: {e}")
|
| 726 |
return None
|
| 727 |
|
| 728 |
-
# 🎯 핵심: 클라이언트 연결 함수 (로그 최소화)
|
| 729 |
-
def get_client():
|
| 730 |
-
"""환경변수 기반 클라이언트 연결"""
|
| 731 |
-
try:
|
| 732 |
-
client = Client(API_ENDPOINT)
|
| 733 |
-
return client
|
| 734 |
-
except Exception as e:
|
| 735 |
-
print(f"❌ 연결 실패: {str(e)[:50]}...")
|
| 736 |
-
return None
|
| 737 |
-
|
| 738 |
def main():
|
| 739 |
with gr.Blocks(
|
| 740 |
css=custom_css,
|
|
@@ -927,116 +926,194 @@ def main():
|
|
| 927 |
interactive=False
|
| 928 |
)
|
| 929 |
|
| 930 |
-
#
|
| 931 |
-
|
| 932 |
-
def update_copy_type_description(selected_type):
|
| 933 |
-
"""원본과 동일한 카피 타입 선택시 설명 업데이트"""
|
| 934 |
-
try:
|
| 935 |
-
client = get_client()
|
| 936 |
-
if not client:
|
| 937 |
-
return "❌ 서버 연결 실패"
|
| 938 |
-
|
| 939 |
-
result = client.predict(
|
| 940 |
-
selected_type=selected_type,
|
| 941 |
-
api_name="/update_copy_type_description"
|
| 942 |
-
)
|
| 943 |
-
return result
|
| 944 |
-
except Exception as e:
|
| 945 |
-
print(f"❌ 카피 타입 설명 업데이트 실패: {str(e)[:50]}...")
|
| 946 |
-
return f"❌ 오류: 설명 업데이트 실패"
|
| 947 |
|
| 948 |
def handle_copy_generation(keyword, selected_type):
|
| 949 |
-
"""
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 954 |
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
api_name="/handle_copy_generation"
|
| 959 |
-
)
|
| 960 |
|
| 961 |
-
#
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
|
|
|
| 968 |
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 973 |
|
| 974 |
-
def handle_copy_selection(
|
| 975 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 976 |
try:
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 986 |
|
| 987 |
-
|
| 988 |
-
return result
|
| 989 |
|
| 990 |
except Exception as e:
|
| 991 |
-
print(f"❌
|
| 992 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 993 |
|
| 994 |
def handle_image_generation(input_image, main_text, sub_text, color_mode,
|
| 995 |
main_font_choice, sub_font_choice, manual_bg_color, manual_main_text_color,
|
| 996 |
manual_sub_text_color, manual_main_font_size, manual_sub_font_size,
|
| 997 |
top_bottom_margin, text_gap, current_keyword):
|
| 998 |
-
"""
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
|
| 1008 |
-
|
| 1009 |
-
|
| 1010 |
-
|
| 1011 |
-
|
| 1012 |
-
|
| 1013 |
-
|
| 1014 |
-
|
| 1015 |
-
|
| 1016 |
-
|
| 1017 |
-
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
api_name="/handle_image_generation"
|
| 1026 |
-
)
|
| 1027 |
-
|
| 1028 |
-
# 원본과 동일한 튜플 반환 (9개 요소)
|
| 1029 |
-
# [0] 이미지, [1] 색상모드, [2] 배경색, [3] 메인텍스트색, [4] 서브텍스트색,
|
| 1030 |
-
# [5] 메인폰트크기, [6] 서브폰트크기, [7] 다운로드파일, [8] 여백정보
|
| 1031 |
-
return result
|
| 1032 |
-
|
| 1033 |
-
except Exception as e:
|
| 1034 |
-
print(f"❌ 이미지 생성 실패: {str(e)[:50]}...")
|
| 1035 |
-
return (None, color_mode, manual_bg_color, manual_main_text_color,
|
| 1036 |
-
manual_sub_text_color, manual_main_font_size, manual_sub_font_size,
|
| 1037 |
-
None, "❌ 이미지 생성 중 오류가 발생했습니다")
|
| 1038 |
|
| 1039 |
-
#
|
| 1040 |
copy_type_selection.change(
|
| 1041 |
fn=update_copy_type_description,
|
| 1042 |
inputs=[copy_type_selection],
|
|
|
|
| 1 |
+
#app.py
|
| 2 |
import gradio as gr
|
| 3 |
import os
|
| 4 |
import tempfile
|
| 5 |
from datetime import datetime
|
| 6 |
from PIL import Image
|
| 7 |
+
import random
|
| 8 |
|
| 9 |
+
# 🔑 허깅페이스 Secrets에서 API 키들 가져와서 랜덤 선택
|
| 10 |
+
def get_random_gemini_api_key():
|
| 11 |
+
"""허깅페이스 Secrets에서 API 키 리스트를 가져와 랜덤 선택 (디버깅 강화)"""
|
| 12 |
+
try:
|
| 13 |
+
print("🔍 API 키 로딩 시작...")
|
| 14 |
+
|
| 15 |
+
# Secrets에서 API 키 리스트 가져오기 (콤마로 구분된 문자열)
|
| 16 |
+
api_keys_string = os.environ.get('GEMINI_API_KEYS')
|
| 17 |
+
print(f"🔍 GEMINI_API_KEYS 원본: {api_keys_string[:50] if api_keys_string else 'None'}...")
|
| 18 |
+
|
| 19 |
+
if api_keys_string:
|
| 20 |
+
# 콤마로 분리하고 공백 제거
|
| 21 |
+
api_keys = [key.strip() for key in api_keys_string.split(',') if key.strip()]
|
| 22 |
+
print(f"🔍 파싱된 키 개수: {len(api_keys)}")
|
| 23 |
+
|
| 24 |
+
for i, key in enumerate(api_keys):
|
| 25 |
+
print(f" 키 {i+1}: {key[:8]}***{key[-4:] if len(key) > 12 else '***'} (길이: {len(key)})")
|
| 26 |
+
|
| 27 |
+
if api_keys:
|
| 28 |
+
# 랜덤하게 하나 선택
|
| 29 |
+
selected_key = random.choice(api_keys)
|
| 30 |
+
print(f"🎲 랜덤 API 키 선택: {selected_key[:8]}***{selected_key[-4:]}")
|
| 31 |
+
return selected_key
|
| 32 |
+
else:
|
| 33 |
+
print("❌ 파싱된 키가 없음")
|
| 34 |
+
else:
|
| 35 |
+
print("❌ GEMINI_API_KEYS 환경변수가 없음")
|
| 36 |
+
|
| 37 |
+
# 폴백: 단일 키
|
| 38 |
+
fallback_key = os.environ.get('GEMINI_API_KEY')
|
| 39 |
+
if fallback_key:
|
| 40 |
+
print(f"🔑 폴백 API 키 사용: {fallback_key[:8]}***{fallback_key[-4:]}")
|
| 41 |
+
return fallback_key
|
| 42 |
+
|
| 43 |
+
print("❌ 모든 API 키를 찾을 수 없습니다")
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print(f"❌ API 키 선택 실패: {e}")
|
| 48 |
+
return os.environ.get('GEMINI_API_KEY')
|
| 49 |
|
| 50 |
+
# 허깅페이스 Secrets에서 모듈 코드 불러와서 실행
|
| 51 |
+
def load_module_from_env(env_var_name):
|
| 52 |
+
"""환경변수에서 코드를 불러와서 모듈로 실행"""
|
| 53 |
+
try:
|
| 54 |
+
code = os.environ.get(env_var_name)
|
| 55 |
+
if not code:
|
| 56 |
+
raise ImportError(f"Environment variable '{env_var_name}' not found")
|
| 57 |
+
|
| 58 |
+
# 모듈 namespace 생성하고 코드 실행
|
| 59 |
+
module_globals = {}
|
| 60 |
+
exec(code, module_globals)
|
| 61 |
+
|
| 62 |
+
print(f"✅ {env_var_name} 모듈 로드 성공")
|
| 63 |
+
return module_globals
|
| 64 |
+
|
| 65 |
+
except Exception as e:
|
| 66 |
+
print(f"❌ {env_var_name} 모듈 로드 실패: {e}")
|
| 67 |
+
return None
|
| 68 |
|
| 69 |
+
# 이미지 처리 모듈 임포트
|
| 70 |
try:
|
| 71 |
+
image_processor_module = load_module_from_env('IMAGE_PROCESSOR_CODE')
|
| 72 |
+
if image_processor_module:
|
| 73 |
+
create_uhp_image = image_processor_module['create_uhp_image']
|
| 74 |
+
save_image_to_downloads = image_processor_module['save_image_to_downloads']
|
| 75 |
+
print("✅ image_processor 모듈 임포트 성공")
|
| 76 |
+
else:
|
| 77 |
+
raise ImportError("IMAGE_PROCESSOR_CODE not found")
|
| 78 |
except ImportError as e:
|
| 79 |
+
print(f"❌ image_processor 임포트 오류: {e}")
|
| 80 |
+
|
| 81 |
+
def create_uhp_image(*args, **kwargs):
|
| 82 |
+
return None, "추천배경", "#FFFFFF", "#000000", "#000000", 100, 55, {}
|
| 83 |
+
|
| 84 |
+
def save_image_to_downloads(*args, **kwargs):
|
| 85 |
+
return None, "image_processor 모듈을 찾을 수 없습니다."
|
| 86 |
+
|
| 87 |
+
# 카피 생성 모듈 임포트
|
| 88 |
+
try:
|
| 89 |
+
copy_generator_module = load_module_from_env('COPY_GENERATOR_CODE')
|
| 90 |
+
if copy_generator_module:
|
| 91 |
+
generate_copy_suggestions = copy_generator_module['generate_copy_suggestions']
|
| 92 |
+
print("✅ copy_generator 모듈 임포트 성공")
|
| 93 |
+
else:
|
| 94 |
+
raise ImportError("COPY_GENERATOR_CODE not found")
|
| 95 |
+
except ImportError as e:
|
| 96 |
+
print(f"⚠️ copy_generator 임포트 오류: {e}")
|
| 97 |
+
|
| 98 |
+
def generate_copy_suggestions(keyword, api_key, selected_type):
|
| 99 |
+
return {
|
| 100 |
+
"error": "copy_generator 모듈을 찾을 수 없습니다. 카피 생성 기능이 비활성화됩니다."
|
| 101 |
+
}, ""
|
| 102 |
|
| 103 |
+
# 커스텀 CSS 스타일 (다크모드 적용 버전)
|
| 104 |
custom_css = """
|
| 105 |
/* ============================================
|
| 106 |
다크모드 자동 변경 템플릿 CSS
|
| 107 |
============================================ */
|
|
|
|
| 108 |
/* 1. CSS 변수 정의 (라이트모드 - 기본값) */
|
| 109 |
:root {
|
| 110 |
/* 메인 컬러 */
|
|
|
|
| 136 |
/* 기타 */
|
| 137 |
--border-radius: 18px;
|
| 138 |
}
|
|
|
|
| 139 |
/* 2. 다크모드 색상 변수 (자동 감지) */
|
| 140 |
@media (prefers-color-scheme: dark) {
|
| 141 |
:root {
|
|
|
|
| 161 |
--shadow-light: 0 2px 4px rgba(0, 0, 0, 0.2);
|
| 162 |
}
|
| 163 |
}
|
|
|
|
| 164 |
/* 3. 수동 다크모드 클래스 (Gradio 토글용) */
|
| 165 |
[data-theme="dark"],
|
| 166 |
.dark,
|
|
|
|
| 186 |
--shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
|
| 187 |
--shadow-light: 0 2px 4px rgba(0, 0, 0, 0.2);
|
| 188 |
}
|
|
|
|
| 189 |
/* 4. 기본 요소 다크모드 적용 */
|
| 190 |
body {
|
| 191 |
font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
|
|
| 197 |
font-size: 16px;
|
| 198 |
transition: background-color 0.3s ease, color 0.3s ease;
|
| 199 |
}
|
|
|
|
| 200 |
/* 5. Gradio 컨테이너 강제 적용 */
|
| 201 |
.gradio-container,
|
| 202 |
.gradio-container *,
|
|
|
|
| 206 |
background-color: var(--background-color) !important;
|
| 207 |
color: var(--text-color) !important;
|
| 208 |
}
|
|
|
|
| 209 |
/* 푸터 숨김 설정 추가 */
|
| 210 |
footer {
|
| 211 |
visibility: hidden;
|
| 212 |
}
|
|
|
|
| 213 |
.gradio-container {
|
| 214 |
width: 100%;
|
| 215 |
margin: 0 auto;
|
| 216 |
padding: 20px;
|
| 217 |
background-color: var(--background-color);
|
| 218 |
}
|
|
|
|
| 219 |
/* ── 그룹 래퍼 배경 완전 제거 ── */
|
| 220 |
.custom-section-group,
|
| 221 |
.gr-block.gr-group {
|
|
|
|
| 229 |
display: none !important;
|
| 230 |
content: none !important;
|
| 231 |
}
|
|
|
|
| 232 |
/* 그룹 컨테이너 배경을 아이보리로, 그림자 제거 */
|
| 233 |
.custom-section-group {
|
| 234 |
background-color: var(--background-color) !important;
|
| 235 |
box-shadow: none !important;
|
| 236 |
}
|
|
|
|
| 237 |
/* 6. 카드 및 패널 스타일 */
|
| 238 |
.custom-frame,
|
| 239 |
.gr-form,
|
|
|
|
| 250 |
box-shadow: var(--shadow) !important;
|
| 251 |
color: var(--text-color) !important;
|
| 252 |
}
|
|
|
|
| 253 |
/* 섹션 그룹 스타일 - 회색 배경 완전 제거 */
|
| 254 |
.custom-section-group {
|
| 255 |
margin-top: 20px;
|
|
|
|
| 259 |
background-color: var(--background-color);
|
| 260 |
box-shadow: none !important;
|
| 261 |
}
|
|
|
|
| 262 |
/* 버튼 스타일 - 글자 크기 18px */
|
| 263 |
.custom-button {
|
| 264 |
border-radius: 30px !important;
|
|
|
|
| 274 |
transform: translateY(-2px);
|
| 275 |
box-shadow: 0 6px 12px rgba(251, 127, 13, 0.3);
|
| 276 |
}
|
|
|
|
| 277 |
/* 제목 스타일 (모든 항목명이 동일하게 custom-title 클래스로) */
|
| 278 |
.custom-title {
|
| 279 |
font-size: 28px;
|
|
|
|
| 283 |
border-bottom: 2px solid var(--primary-color);
|
| 284 |
padding-bottom: 5px;
|
| 285 |
}
|
|
|
|
| 286 |
/* 이미지 컨테이너 - 크기 고정 */
|
| 287 |
.image-container {
|
| 288 |
border-radius: var(--border-radius);
|
|
|
|
| 291 |
transition: all 0.3s ease;
|
| 292 |
background-color: var(--card-bg);
|
| 293 |
}
|
|
|
|
| 294 |
/* 이미지 업로드 영역 개선 */
|
| 295 |
.gradio-container .gr-image {
|
| 296 |
border: 2px dashed var(--border-color) !important;
|
|
|
|
| 298 |
background-color: var(--card-bg) !important;
|
| 299 |
transition: all 0.3s ease !important;
|
| 300 |
}
|
|
|
|
| 301 |
.gradio-container .gr-image:hover {
|
| 302 |
border-color: var(--primary-color) !important;
|
| 303 |
box-shadow: 0 4px 12px rgba(251, 127, 13, 0.15) !important;
|
| 304 |
}
|
|
|
|
| 305 |
/* 업로드 영역 내부 텍스트 */
|
| 306 |
.gradio-container .gr-image .upload-container,
|
| 307 |
.gradio-container .gr-image [data-testid="upload-container"] {
|
|
|
|
| 309 |
color: var(--text-color) !important;
|
| 310 |
border: none !important;
|
| 311 |
}
|
|
|
|
| 312 |
/* 업로드 영역 드래그 안내 텍스트 */
|
| 313 |
.gradio-container .gr-image .upload-container p,
|
| 314 |
.gradio-container .gr-image [data-testid="upload-container"] p {
|
| 315 |
color: var(--text-color) !important;
|
| 316 |
font-size: 14px !important;
|
| 317 |
}
|
|
|
|
| 318 |
/* 업로드 버튼 스타일 개선 */
|
| 319 |
.gradio-container .gr-image .upload-container button,
|
| 320 |
.gradio-container .gr-image [data-testid="upload-container"] button {
|
|
|
|
| 327 |
cursor: pointer !important;
|
| 328 |
transition: all 0.3s ease !important;
|
| 329 |
}
|
|
|
|
| 330 |
.gradio-container .gr-image .upload-container button:hover,
|
| 331 |
.gradio-container .gr-image [data-testid="upload-container"] button:hover {
|
| 332 |
background-color: var(--secondary-color) !important;
|
| 333 |
transform: translateY(-1px) !important;
|
| 334 |
}
|
|
|
|
| 335 |
/* 업로드 영역 아이콘 */
|
| 336 |
.gradio-container .gr-image .upload-container svg,
|
| 337 |
.gradio-container .gr-image [data-testid="upload-container"] svg {
|
|
|
|
| 339 |
width: 32px !important;
|
| 340 |
height: 32px !important;
|
| 341 |
}
|
|
|
|
| 342 |
/* 이미지가 업로드된 후 표시 영역 */
|
| 343 |
.gradio-container .gr-image img {
|
| 344 |
background-color: var(--card-bg) !important;
|
| 345 |
border-radius: var(--border-radius) !important;
|
| 346 |
}
|
|
|
|
| 347 |
/* 이미지 제거 버튼 */
|
| 348 |
.gradio-container .gr-image .image-container button,
|
| 349 |
.gradio-container .gr-image [data-testid="image"] button {
|
|
|
|
| 359 |
cursor: pointer !important;
|
| 360 |
transition: all 0.3s ease !important;
|
| 361 |
}
|
|
|
|
| 362 |
.gradio-container .gr-image .image-container button:hover,
|
| 363 |
.gradio-container .gr-image [data-testid="image"] button:hover {
|
| 364 |
background-color: rgba(255, 255, 255, 1) !important;
|
| 365 |
transform: scale(1.1) !important;
|
| 366 |
}
|
|
|
|
| 367 |
/* 업로드 이미지 컨테이너 (600x600) */
|
| 368 |
.upload-image-container {
|
| 369 |
width: 600px !important;
|
|
|
|
| 373 |
max-width: 600px !important;
|
| 374 |
max-height: 600px !important;
|
| 375 |
}
|
|
|
|
| 376 |
/* 출력 이미지 컨테이너 (700x600) */
|
| 377 |
.output-image-container {
|
| 378 |
width: 700px !important;
|
|
|
|
| 382 |
max-width: 700px !important;
|
| 383 |
max-height: 600px !important;
|
| 384 |
}
|
|
|
|
| 385 |
.image-container:hover {
|
| 386 |
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
| 387 |
}
|
|
|
|
| 388 |
.image-container img {
|
| 389 |
width: 100% !important;
|
| 390 |
height: 100% !important;
|
| 391 |
object-fit: contain !important;
|
| 392 |
}
|
|
|
|
| 393 |
/* Gradio 업로드 이미지 컴포넌트 크기 고정 (600x600) */
|
| 394 |
.gradio-container .gr-image.upload-image {
|
| 395 |
width: 600px !important;
|
|
|
|
| 399 |
max-width: 600px !important;
|
| 400 |
max-height: 600px !important;
|
| 401 |
}
|
|
|
|
| 402 |
/* Gradio 출력 이미지 컴포넌트 크기 고정 (700x600) */
|
| 403 |
.gradio-container .gr-image.output-image {
|
| 404 |
width: 700px !important;
|
|
|
|
| 408 |
max-width: 700px !important;
|
| 409 |
max-height: 600px !important;
|
| 410 |
}
|
|
|
|
| 411 |
/* 이미지 업로드 영역 크기 고정 */
|
| 412 |
.gradio-container .gr-image.upload-image > div {
|
| 413 |
width: 600px !important;
|
|
|
|
| 417 |
max-width: 600px !important;
|
| 418 |
max-height: 600px !important;
|
| 419 |
}
|
|
|
|
| 420 |
/* 이미지 출력 영역 크기 고정 */
|
| 421 |
.gradio-container .gr-image.output-image > div {
|
| 422 |
width: 700px !important;
|
|
|
|
| 426 |
max-width: 700px !important;
|
| 427 |
max-height: 600px !important;
|
| 428 |
}
|
|
|
|
| 429 |
/* 이미지 업로드 드래그 영역 크기 고정 */
|
| 430 |
.gradio-container .gr-image.upload-image .image-container,
|
| 431 |
.gradio-container .gr-image.upload-image [data-testid="image"],
|
|
|
|
| 437 |
max-width: 600px !important;
|
| 438 |
max-height: 600px !important;
|
| 439 |
}
|
|
|
|
| 440 |
/* 이미지 출력 드래그 영역 크기 고정 */
|
| 441 |
.gradio-container .gr-image.output-image .image-container,
|
| 442 |
.gradio-container .gr-image.output-image [data-testid="image"],
|
|
|
|
| 448 |
max-width: 700px !important;
|
| 449 |
max-height: 600px !important;
|
| 450 |
}
|
|
|
|
| 451 |
/* 7. 입력 필드 스타일 */
|
| 452 |
.gr-input, .gr-text-input, .gr-sample-inputs,
|
| 453 |
input[type="text"],
|
|
|
|
| 467 |
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05) !important;
|
| 468 |
transition: all 0.3s ease !important;
|
| 469 |
}
|
|
|
|
| 470 |
.gr-input:focus, .gr-text-input:focus,
|
| 471 |
input[type="text"]:focus,
|
| 472 |
input[type="number"]:focus,
|
|
|
|
| 480 |
outline: none !important;
|
| 481 |
box-shadow: 0 0 0 2px rgba(251, 127, 13, 0.2) !important;
|
| 482 |
}
|
|
|
|
| 483 |
/* 8. 라벨 및 텍스트 요소 */
|
| 484 |
.gradio-container label,
|
| 485 |
label,
|
|
|
|
| 492 |
font-weight: 600 !important;
|
| 493 |
margin-bottom: 8px !important;
|
| 494 |
}
|
|
|
|
| 495 |
/* 드롭다운 및 라디오 버튼 폰트 크기 */
|
| 496 |
.gr-radio label, .gr-dropdown label, .gr-checkbox label {
|
| 497 |
font-size: 15px !important;
|
| 498 |
}
|
|
|
|
| 499 |
/* 라디오 버튼 선택지 볼드 처리 제거 */
|
| 500 |
.gr-radio .gr-radio-option label,
|
| 501 |
.gr-radio input[type="radio"] + label,
|
|
|
|
| 503 |
font-weight: normal !important;
|
| 504 |
font-size: 15px !important;
|
| 505 |
}
|
|
|
|
| 506 |
/* 라디오 버튼 그룹 내 모든 라벨 일반 폰트로 설정 */
|
| 507 |
.gr-radio fieldset label {
|
| 508 |
font-weight: normal !important;
|
| 509 |
}
|
|
|
|
| 510 |
/* 마크다운 텍스트 크기 증가 */
|
| 511 |
.gradio-container .gr-markdown {
|
| 512 |
font-size: 15px !important;
|
| 513 |
line-height: 1.6 !important;
|
| 514 |
color: var(--text-color) !important;
|
| 515 |
}
|
|
|
|
| 516 |
/* 텍스트박스 내용 폰트 크기 */
|
| 517 |
.gr-textbox textarea, .gr-textbox input {
|
| 518 |
font-size: 15px !important;
|
| 519 |
background-color: var(--input-bg) !important;
|
| 520 |
color: var(--text-color) !important;
|
| 521 |
}
|
|
|
|
| 522 |
/* 아코디언 제목 폰트 크기 */
|
| 523 |
.gr-accordion summary {
|
| 524 |
font-size: 17px !important;
|
|
|
|
| 526 |
background-color: var(--card-bg) !important;
|
| 527 |
color: var(--text-color) !important;
|
| 528 |
}
|
|
|
|
| 529 |
/* 메인 컨텐츠 스크롤바 */
|
| 530 |
::-webkit-scrollbar {
|
| 531 |
width: 8px;
|
| 532 |
height: 8px;
|
| 533 |
}
|
|
|
|
| 534 |
::-webkit-scrollbar-track {
|
| 535 |
background: var(--card-bg);
|
| 536 |
border-radius: 10px;
|
| 537 |
}
|
|
|
|
| 538 |
::-webkit-scrollbar-thumb {
|
| 539 |
background: var(--primary-color);
|
| 540 |
border-radius: 10px;
|
| 541 |
}
|
|
|
|
| 542 |
::-webkit-scrollbar-thumb:hover {
|
| 543 |
background: var(--secondary-color);
|
| 544 |
}
|
|
|
|
| 545 |
/* 애니메이션 스타일 */
|
| 546 |
@keyframes fadeIn {
|
| 547 |
from { opacity: 0; transform: translateY(10px); }
|
| 548 |
to { opacity: 1; transform: translateY(0); }
|
| 549 |
}
|
|
|
|
| 550 |
.fade-in {
|
| 551 |
animation: fadeIn 0.5s ease-out;
|
| 552 |
}
|
|
|
|
| 553 |
/* 반응형 */
|
| 554 |
@media (max-width: 768px) {
|
| 555 |
.button-grid {
|
| 556 |
grid-template-columns: repeat(2, 1fr);
|
| 557 |
}
|
| 558 |
}
|
|
|
|
| 559 |
/* 섹션 제목 스타일 */
|
| 560 |
.section-title {
|
| 561 |
display: flex;
|
|
|
|
| 568 |
border-bottom: 2px solid var(--primary-color);
|
| 569 |
font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 570 |
}
|
|
|
|
| 571 |
.section-title img {
|
| 572 |
margin-right: 12px;
|
| 573 |
width: 28px;
|
|
|
|
| 575 |
/* 다크모드에서 아이콘 필터 적용 */
|
| 576 |
filter: brightness(0) saturate(100%) invert(27%) sepia(51%) saturate(2878%) hue-rotate(346deg) brightness(104%) contrast(97%);
|
| 577 |
}
|
|
|
|
| 578 |
/* 라이트모드에서는 원래 아이��� 색상 유지 */
|
| 579 |
@media (prefers-color-scheme: light) {
|
| 580 |
.section-title img {
|
| 581 |
filter: none;
|
| 582 |
}
|
| 583 |
}
|
|
|
|
| 584 |
/* 수동 다크모드 클래스에서도 아이콘 색상 적용 */
|
| 585 |
[data-theme="dark"] .section-title img,
|
| 586 |
.dark .section-title img,
|
| 587 |
.gr-theme-dark .section-title img {
|
| 588 |
filter: brightness(0) saturate(100%) invert(27%) sepia(51%) saturate(2878%) hue-rotate(346deg) brightness(104%) contrast(97%);
|
| 589 |
}
|
|
|
|
| 590 |
/* 10. 아코디언 및 드롭다운 - 수동설정 영역 회색 배경 제거 */
|
| 591 |
details,
|
| 592 |
.gr-accordion,
|
|
|
|
| 597 |
border-radius: var(--border-radius) !important;
|
| 598 |
margin: 10px 0 !important;
|
| 599 |
}
|
|
|
|
| 600 |
details summary,
|
| 601 |
.gr-accordion summary,
|
| 602 |
.gr-accordion details summary {
|
|
|
|
| 609 |
font-weight: 600 !important;
|
| 610 |
transition: all 0.3s ease !important;
|
| 611 |
}
|
|
|
|
| 612 |
details summary:hover,
|
| 613 |
.gr-accordion summary:hover,
|
| 614 |
.gr-accordion details summary:hover {
|
| 615 |
background-color: var(--table-hover-bg) !important;
|
| 616 |
}
|
|
|
|
| 617 |
/* 아코디언 내부 콘텐츠 */
|
| 618 |
details[open],
|
| 619 |
.gr-accordion[open],
|
| 620 |
.gr-accordion details[open] {
|
| 621 |
background-color: var(--background-color) !important;
|
| 622 |
}
|
|
|
|
| 623 |
details[open] > *:not(summary),
|
| 624 |
.gr-accordion[open] > *:not(summary),
|
| 625 |
.gr-accordion details[open] > *:not(summary) {
|
|
|
|
| 628 |
padding: 16px !important;
|
| 629 |
border-top: 1px solid var(--border-color) !important;
|
| 630 |
}
|
|
|
|
| 631 |
/* 그룹 내부 스타일 - 수동설정 내부 그룹들 */
|
| 632 |
.gr-group,
|
| 633 |
details .gr-group,
|
|
|
|
| 638 |
margin: 8px 0 !important;
|
| 639 |
border-radius: var(--border-radius) !important;
|
| 640 |
}
|
|
|
|
| 641 |
/* 그룹 내부 제목 */
|
| 642 |
.gr-group .gr-markdown h3,
|
| 643 |
.gr-group h3 {
|
|
|
|
| 648 |
padding-bottom: 6px !important;
|
| 649 |
border-bottom: 1px solid var(--border-color) !important;
|
| 650 |
}
|
|
|
|
| 651 |
/* 11. 추가 Gradio 컴포넌트들 */
|
| 652 |
.gr-block,
|
| 653 |
.gr-group,
|
|
|
|
| 656 |
background-color: var(--background-color) !important;
|
| 657 |
color: var(--text-color) !important;
|
| 658 |
}
|
|
|
|
| 659 |
/* 12. 버튼은 기존 스타일 유지 (primary-color 사용) */
|
| 660 |
button:not([class*="custom"]):not([class*="primary"]):not([class*="secondary"]) {
|
| 661 |
background-color: var(--card-bg) !important;
|
| 662 |
color: var(--text-color) !important;
|
| 663 |
border-color: var(--border-color) !important;
|
| 664 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
/* 14. 전환 애니메이션 */
|
| 666 |
* {
|
| 667 |
transition: background-color 0.3s ease,
|
|
|
|
| 727 |
image_to_save.save(file_path, 'JPEG', quality=95)
|
| 728 |
|
| 729 |
print(f"✅ 다운로드 파일 준비: {filename}")
|
| 730 |
+
print(f"📁 파일 경로: {file_path}")
|
| 731 |
return file_path
|
| 732 |
|
| 733 |
except Exception as e:
|
| 734 |
print(f"❌ 다운로드 파일 준비 실패: {e}")
|
| 735 |
return None
|
| 736 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 737 |
def main():
|
| 738 |
with gr.Blocks(
|
| 739 |
css=custom_css,
|
|
|
|
| 926 |
interactive=False
|
| 927 |
)
|
| 928 |
|
| 929 |
+
# 이벤트 핸들러들
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 930 |
|
| 931 |
def handle_copy_generation(keyword, selected_type):
|
| 932 |
+
"""카피 생성 처리 (랜덤 API 키 사용)"""
|
| 933 |
+
# 🎲 랜덤 API 키 선택
|
| 934 |
+
api_key = get_random_gemini_api_key()
|
| 935 |
+
print(f"🔑 카피 생성용 API 키: {api_key[:8] if api_key else 'None'}***")
|
| 936 |
+
|
| 937 |
+
if not keyword.strip():
|
| 938 |
+
return ("⚠️ 상품 키워드를 입력해주세���.", {}, "", "", "", "", "", "", "", keyword.strip())
|
| 939 |
+
|
| 940 |
+
if not api_key:
|
| 941 |
+
return ("⚠️ API 키가 설정되지 않았습니다.", {}, "", "", "", "", "", "", "", keyword.strip())
|
| 942 |
+
|
| 943 |
+
if not selected_type:
|
| 944 |
+
return ("⚠️ 카피 타입을 먼저 선택해주세요.", {}, "", "", "", "", "", "", "", keyword.strip())
|
| 945 |
+
|
| 946 |
+
print(f"🚀 카피 생성 프로세스 시작: {keyword} - {selected_type}")
|
| 947 |
+
suggestions, analysis = generate_copy_suggestions(keyword, api_key, selected_type)
|
| 948 |
+
|
| 949 |
+
print(f"🔍 AI 응답 타입: {type(suggestions)}")
|
| 950 |
+
print(f"🔍 AI 응답 키들: {list(suggestions.keys()) if isinstance(suggestions, dict) else 'Not dict'}")
|
| 951 |
+
|
| 952 |
+
if "error" not in suggestions:
|
| 953 |
+
print("✅ 카피 생성 성공!")
|
| 954 |
|
| 955 |
+
# 선택된 타입의 카피 리스트 가져오기
|
| 956 |
+
copy_list = suggestions.get(selected_type, [])
|
| 957 |
+
print(f"📋 {selected_type} 카피 개수: {len(copy_list)}")
|
|
|
|
|
|
|
| 958 |
|
| 959 |
+
# 각 카피 아이템 검증
|
| 960 |
+
for i, item in enumerate(copy_list):
|
| 961 |
+
if isinstance(item, dict):
|
| 962 |
+
main = item.get("main", "")
|
| 963 |
+
sub = item.get("sub", "")
|
| 964 |
+
print(f" 추천{i+1}: 메인='{main}', 서브='{sub}'")
|
| 965 |
+
else:
|
| 966 |
+
print(f" 추천{i+1}: 잘못된 형식 {type(item)}")
|
| 967 |
|
| 968 |
+
# 표시용 카피 문자열 생성 (추천1~5 텍스트박스용)
|
| 969 |
+
copy_values = []
|
| 970 |
+
for i in range(5):
|
| 971 |
+
if i < len(copy_list) and isinstance(copy_list[i], dict):
|
| 972 |
+
main = copy_list[i].get("main", "")
|
| 973 |
+
sub = copy_list[i].get("sub", "")
|
| 974 |
+
combined = f"{main} / {sub}"
|
| 975 |
+
copy_values.append(combined)
|
| 976 |
+
print(f"📝 추천{i+1} 표시: {combined}")
|
| 977 |
+
else:
|
| 978 |
+
copy_values.append("")
|
| 979 |
+
print(f"📝 추천{i+1} 표시: (비어있음)")
|
| 980 |
+
|
| 981 |
+
# 첫 번째 카피를 메인/서브 텍스트박스에 자동 입력
|
| 982 |
+
first_main = ""
|
| 983 |
+
first_sub = ""
|
| 984 |
+
if copy_list and isinstance(copy_list[0], dict):
|
| 985 |
+
first_main = copy_list[0].get("main", "")
|
| 986 |
+
first_sub = copy_list[0].get("sub", "")
|
| 987 |
+
print(f"🎯 자동 선택: 메인='{first_main}', 서브='{first_sub}'")
|
| 988 |
+
|
| 989 |
+
return (f"✅ {selected_type} 카피 생성 완료!", suggestions, *copy_values, first_main, first_sub, keyword.strip())
|
| 990 |
+
else:
|
| 991 |
+
print(f"❌ 카피 생성 실패: {suggestions['error']}")
|
| 992 |
+
error_msg = f"❌ 오류: {suggestions['error']}"
|
| 993 |
+
return (error_msg, {}, "", "", "", "", "", "", "", keyword.strip())
|
| 994 |
+
|
| 995 |
+
def update_copy_type_description(selected_type):
|
| 996 |
+
"""카피 타입 선택시 설명 업데이트"""
|
| 997 |
+
descriptions = {
|
| 998 |
+
"장점요약형": "**장점요약형**: 제품의 장점을 한눈에 강조 - 핵심 기능과 혜택을 간결하게 요약하여 제시",
|
| 999 |
+
"문제제시형": "**문제제시형**: 문제를 제시 후 해결책 제안 - 고객의 불편함을 먼저 언급하고 해결방안 제시",
|
| 1000 |
+
"사회적증거형": "**사회적증거형**: 신뢰와 인기를 강조 - 타인의 사용후기와 검증된 실적으로 신뢰성 어필",
|
| 1001 |
+
"긴급성유도형": "**긴급성유도형**: 즉시 구매를 유도 - 한정수량, 시간제한 등으로 긴급감 조성",
|
| 1002 |
+
"가격경쟁력형": "**가격경쟁력형**: 합리적 가격을 강조해 성취 - 가성비, 할인��택 등 경제적 이득 강조",
|
| 1003 |
+
"매인변화형": "**매인변화형**: 강한 비교를 시선 집중 - 사용 전후 변화나 극적인 개선 효과 부각",
|
| 1004 |
+
"충동구매유도형": "**충동구매유도형**: 특정 구매욕구를 자극 - 특별함과 프리미엄 가치로 소유욕 자극",
|
| 1005 |
+
"공포소구형": "**공포소구형**: 불안 위험을 강조해 감정 유발 - 놓치면 후회할 기회나 위험성을 경고"
|
| 1006 |
+
}
|
| 1007 |
+
|
| 1008 |
+
if selected_type and selected_type in descriptions:
|
| 1009 |
+
return descriptions[selected_type]
|
| 1010 |
+
else:
|
| 1011 |
+
return "### 카피 타입을 선택하면 설명이 표시됩니다."
|
| 1012 |
|
| 1013 |
+
def handle_copy_selection(suggestions, selected_type, selected_copy):
|
| 1014 |
+
"""카피 선택시 메인/서브 텍스트박스 업데이트 (개선된 버전)"""
|
| 1015 |
+
print(f"🔘 라디오 선택: {selected_copy}, 타입: {selected_type}")
|
| 1016 |
+
print(f"🔍 suggestions 상태: {type(suggestions)}, 키: {list(suggestions.keys()) if suggestions else 'None'}")
|
| 1017 |
+
|
| 1018 |
+
if not suggestions or "error" in suggestions:
|
| 1019 |
+
print("❌ suggestions 데이터가 없거나 에러 상태")
|
| 1020 |
+
return "", ""
|
| 1021 |
+
|
| 1022 |
+
if not selected_type or not selected_copy:
|
| 1023 |
+
print("❌ 타입이나 선택된 카피가 없음")
|
| 1024 |
+
return "", ""
|
| 1025 |
+
|
| 1026 |
+
if selected_type not in suggestions:
|
| 1027 |
+
print(f"❌ {selected_type} 타입을 suggestions에서 찾을 수 없음")
|
| 1028 |
+
print(f"📋 사용 가능한 타입들: {list(suggestions.keys())}")
|
| 1029 |
+
return "", ""
|
| 1030 |
+
|
| 1031 |
+
copy_list = suggestions[selected_type]
|
| 1032 |
+
if not isinstance(copy_list, list):
|
| 1033 |
+
print(f"❌ {selected_type} 데이터가 리스트가 아님: {type(copy_list)}")
|
| 1034 |
+
return "", ""
|
| 1035 |
+
|
| 1036 |
+
print(f"📝 {selected_type} 카피 리스트: {len(copy_list)}개")
|
| 1037 |
+
|
| 1038 |
+
# 라디오 버튼 선택값에서 번호 추출 (더 안전한 방법)
|
| 1039 |
try:
|
| 1040 |
+
if "추천 1" in selected_copy:
|
| 1041 |
+
option_number = 0
|
| 1042 |
+
elif "추천 2" in selected_copy:
|
| 1043 |
+
option_number = 1
|
| 1044 |
+
elif "추천 3" in selected_copy:
|
| 1045 |
+
option_number = 2
|
| 1046 |
+
elif "추천 4" in selected_copy:
|
| 1047 |
+
option_number = 3
|
| 1048 |
+
elif "추천 5" in selected_copy:
|
| 1049 |
+
option_number = 4
|
| 1050 |
+
else:
|
| 1051 |
+
# 백업 방법: 숫자 추출
|
| 1052 |
+
import re
|
| 1053 |
+
numbers = re.findall(r'\d+', selected_copy)
|
| 1054 |
+
option_number = int(numbers[0]) - 1 if numbers else 0
|
| 1055 |
|
| 1056 |
+
print(f"🔢 추출된 옵션 번호: {option_number}")
|
|
|
|
| 1057 |
|
| 1058 |
except Exception as e:
|
| 1059 |
+
print(f"❌ 옵션 번호 추출 실패: {e}, 기본값 0 사용")
|
| 1060 |
+
option_number = 0
|
| 1061 |
+
|
| 1062 |
+
# 범위 체크
|
| 1063 |
+
if 0 <= option_number < len(copy_list):
|
| 1064 |
+
selected_copy_item = copy_list[option_number]
|
| 1065 |
+
|
| 1066 |
+
if not isinstance(selected_copy_item, dict):
|
| 1067 |
+
print(f"❌ 선택된 카피 아이템이 딕셔너리가 아님: {type(selected_copy_item)}")
|
| 1068 |
+
return "", ""
|
| 1069 |
+
|
| 1070 |
+
main_text = selected_copy_item.get("main", "")
|
| 1071 |
+
sub_text = selected_copy_item.get("sub", "")
|
| 1072 |
+
|
| 1073 |
+
print(f"✅ 선택된 카피 - 메인: '{main_text}', 서브: '{sub_text}'")
|
| 1074 |
+
|
| 1075 |
+
# 빈 문자열 체크
|
| 1076 |
+
if not main_text and not sub_text:
|
| 1077 |
+
print("⚠️ 메인카피와 서브카피가 모두 비어있음")
|
| 1078 |
+
|
| 1079 |
+
return main_text, sub_text
|
| 1080 |
+
else:
|
| 1081 |
+
print(f"❌ 잘못된 옵션 번호: {option_number} (범위: 0~{len(copy_list)-1})")
|
| 1082 |
+
return "", ""
|
| 1083 |
|
| 1084 |
def handle_image_generation(input_image, main_text, sub_text, color_mode,
|
| 1085 |
main_font_choice, sub_font_choice, manual_bg_color, manual_main_text_color,
|
| 1086 |
manual_sub_text_color, manual_main_font_size, manual_sub_font_size,
|
| 1087 |
top_bottom_margin, text_gap, current_keyword):
|
| 1088 |
+
"""이미지 생성 처리 (여백 조정 기능 포함)"""
|
| 1089 |
+
# 🎲 랜덤 API 키 선택
|
| 1090 |
+
api_key = get_random_gemini_api_key()
|
| 1091 |
+
print(f"🔑 이미지 생성용 API 키: {api_key[:8] if api_key else 'None'}***")
|
| 1092 |
+
|
| 1093 |
+
# 🎯 NEW: 여백 설정을 create_uhp_image 함수에 전달
|
| 1094 |
+
image_result, color_mode_result, new_bg_color, new_main_text_color, new_sub_text_color, new_main_font_size, new_sub_font_size, applied_margin_info = create_uhp_image(
|
| 1095 |
+
input_image, main_text, sub_text, color_mode,
|
| 1096 |
+
main_font_choice, sub_font_choice, manual_bg_color, manual_main_text_color, manual_sub_text_color,
|
| 1097 |
+
manual_main_font_size, manual_sub_font_size, api_key,
|
| 1098 |
+
top_bottom_margin, text_gap # 새로운 여백 파라미터 추가
|
| 1099 |
+
)
|
| 1100 |
+
|
| 1101 |
+
# 🎯 핵심: 다운로드 파일 준비
|
| 1102 |
+
download_file_path = None
|
| 1103 |
+
if image_result is not None:
|
| 1104 |
+
download_file_path = prepare_download_file(image_result, current_keyword)
|
| 1105 |
+
|
| 1106 |
+
# 🎯 NEW: 적용된 여백 정보 생성
|
| 1107 |
+
margin_info_text = f"""📏 **적용된 여백 정보:**
|
| 1108 |
+
• 상하 여백: {applied_margin_info.get('top_bottom_margin', 0)}px
|
| 1109 |
+
• 메인↔서브 간격: {applied_margin_info.get('text_gap', 0)}px
|
| 1110 |
+
• 이미지 크기: {applied_margin_info.get('canvas_width', 0)} × {applied_margin_info.get('canvas_height', 0)}px
|
| 1111 |
+
• 원본 크기: {applied_margin_info.get('original_width', 0)} × {applied_margin_info.get('original_height', 0)}px"""
|
| 1112 |
+
|
| 1113 |
+
return (image_result, color_mode_result, new_bg_color, new_main_text_color, new_sub_text_color,
|
| 1114 |
+
new_main_font_size, new_sub_font_size, download_file_path, margin_info_text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1115 |
|
| 1116 |
+
# 이벤트 연결
|
| 1117 |
copy_type_selection.change(
|
| 1118 |
fn=update_copy_type_description,
|
| 1119 |
inputs=[copy_type_selection],
|