ginipick commited on
Commit
589ff61
·
verified ·
1 Parent(s): a493285

Update app-backup1.py

Browse files
Files changed (1) hide show
  1. 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
- # 환경변수에서 API 키 로드
 
 
8
  FIREWORKS_API_KEY = os.getenv("FIREWORKS_API_KEY", "")
9
  FAL_KEY = os.getenv("FAL_KEY", "")
 
 
 
 
 
 
 
 
 
 
10
 
11
  # ============================================
12
- # 드라마틱한 커스텀 CSS - 사이버펑크 네온 테마
13
  # ============================================
14
- CUSTOM_CSS = """
15
- @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700;800;900&family=Rajdhani:wght@300;400;500;600;700&family=Share+Tech+Mono&display=swap');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
- :root {
18
- --neon-cyan: #00f5ff;
19
- --neon-magenta: #ff00ff;
20
- --neon-yellow: #ffff00;
21
- --neon-green: #39ff14;
22
- --deep-black: #0a0a0f;
23
- --dark-surface: #12121a;
24
- --glass-bg: rgba(18, 18, 26, 0.85);
25
- --glow-cyan: 0 0 20px rgba(0, 245, 255, 0.5), 0 0 40px rgba(0, 245, 255, 0.3);
26
- --glow-magenta: 0 0 20px rgba(255, 0, 255, 0.5), 0 0 40px rgba(255, 0, 255, 0.3);
27
- }
28
-
29
- /* 메인 컨테이너 */
30
- .gradio-container {
31
- background: var(--deep-black) !important;
32
- font-family: 'Rajdhani', sans-serif !important;
33
- min-height: 100vh;
34
- position: relative;
35
- overflow-x: hidden;
36
- }
 
 
 
 
 
 
37
 
38
- /* 애니메이션 배경 그리드 */
39
- .gradio-container::before {
40
- content: '';
41
- position: fixed;
42
- top: 0;
43
- left: 0;
44
- right: 0;
45
- bottom: 0;
46
- background:
47
- linear-gradient(90deg, rgba(0, 245, 255, 0.03) 1px, transparent 1px),
48
- linear-gradient(rgba(0, 245, 255, 0.03) 1px, transparent 1px);
49
- background-size: 50px 50px;
50
- animation: gridMove 20s linear infinite;
51
- pointer-events: none;
52
- z-index: 0;
53
- }
54
 
55
- @keyframes gridMove {
56
- 0% { transform: translate(0, 0); }
57
- 100% { transform: translate(50px, 50px); }
58
- }
 
 
59
 
60
- /* 플로팅 오브 효과 */
61
- .gradio-container::after {
62
- content: '';
63
- position: fixed;
64
- width: 600px;
65
- height: 600px;
66
- background: radial-gradient(circle, rgba(255, 0, 255, 0.15) 0%, transparent 70%);
67
- top: -200px;
68
- right: -200px;
69
- animation: floatOrb 15s ease-in-out infinite;
70
- pointer-events: none;
71
- z-index: 0;
72
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
- @keyframes floatOrb {
75
- 0%, 100% { transform: translate(0, 0) scale(1); opacity: 0.5; }
76
- 50% { transform: translate(-100px, 100px) scale(1.2); opacity: 0.8; }
77
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
- /* 모든 요소 z-index 조정 */
80
- .gradio-container > * {
81
- position: relative;
82
- z-index: 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  }
84
 
85
  /* 타이틀 스타일 */
86
  .cyber-title {
87
- font-family: 'Orbitron', monospace !important;
88
- font-size: 3.5rem !important;
89
- font-weight: 900 !important;
90
- text-transform: uppercase;
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
- animation: gradientShift 5s ease infinite;
98
- text-shadow: none;
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
- .glass-panel::before {
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
- background: rgba(0, 0, 0, 0.6) !important;
162
- border: 2px solid rgba(0, 245, 255, 0.3) !important;
163
- border-radius: 12px !important;
164
- color: #ffffff !important;
165
- font-family: 'Rajdhani', sans-serif !important;
166
- font-size: 1.1rem !important;
167
- font-weight: 500 !important;
168
- padding: 16px !important;
169
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
170
  }
171
 
172
  .gradio-container textarea:focus,
173
  .gradio-container input:focus {
174
- border-color: var(--neon-cyan) !important;
175
- box-shadow: var(--glow-cyan), inset 0 0 20px rgba(0, 245, 255, 0.1) !important;
176
  outline: none !important;
177
  }
178
 
179
  .gradio-container textarea::placeholder,
180
  .gradio-container input::placeholder {
181
- color: rgba(0, 245, 255, 0.4) !important;
182
  }
183
 
184
- /* 라벨 */
185
  .gradio-container label {
186
- font-family: 'Orbitron', monospace !important;
187
- color: var(--neon-cyan) !important;
188
- font-size: 0.9rem !important;
189
  font-weight: 600 !important;
190
- letter-spacing: 2px !important;
191
- text-transform: uppercase !important;
192
  }
193
 
194
  /* 드롭다운 */
195
  .gradio-container select,
196
  .gradio-container .wrap-inner {
197
- background: rgba(0, 0, 0, 0.6) !important;
198
- border: 2px solid rgba(0, 245, 255, 0.3) !important;
199
- border-radius: 12px !important;
200
- color: #ffffff !important;
201
- font-family: 'Rajdhani', sans-serif !important;
202
  }
203
 
204
- /* 메인 버튼 - 네온 글로우 */
205
  .neon-button {
206
- background: linear-gradient(135deg, rgba(0, 245, 255, 0.2), rgba(255, 0, 255, 0.2)) !important;
207
- border: 2px solid var(--neon-cyan) !important;
208
- border-radius: 12px !important;
209
  color: #ffffff !important;
210
- font-family: 'Orbitron', monospace !important;
211
- font-size: 1.2rem !important;
212
- font-weight: 700 !important;
213
- letter-spacing: 3px !important;
214
- text-transform: uppercase !important;
215
- padding: 20px 40px !important;
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::before {
223
- content: '';
224
- position: absolute;
225
- top: 0;
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
- .neon-button:hover {
234
- background: linear-gradient(135deg, rgba(0, 245, 255, 0.4), rgba(255, 0, 255, 0.4)) !important;
235
- border-color: var(--neon-magenta) !important;
236
- box-shadow: var(--glow-magenta), 0 10px 40px rgba(255, 0, 255, 0.3) !important;
237
- transform: translateY(-3px) !important;
238
  }
239
 
240
- .neon-button:hover::before {
241
- left: 100%;
 
 
 
 
242
  }
243
 
244
- /* 상태 표시 박스 */
245
- .status-box {
246
- background: rgba(0, 0, 0, 0.6) !important;
247
- border-left: 4px solid var(--neon-cyan) !important;
248
- border-radius: 8px !important;
249
- padding: 16px !important;
250
- font-family: 'Share Tech Mono', monospace !important;
251
- color: var(--neon-green) !important;
252
  }
253
 
254
- /* 증강 프롬프트 출력 */
255
- .enhanced-output textarea {
256
- background: linear-gradient(135deg, rgba(57, 255, 20, 0.05), rgba(0, 245, 255, 0.05)) !important;
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
- .image-card {
265
- background: var(--glass-bg) !important;
266
- border: 2px solid rgba(0, 245, 255, 0.2) !important;
267
- border-radius: 16px !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  overflow: hidden !important;
269
- transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important;
270
  }
271
 
272
- .image-card:hover {
273
- border-color: var(--neon-magenta) !important;
274
- box-shadow: var(--glow-magenta) !important;
275
- transform: scale(1.02) !important;
 
 
 
 
 
276
  }
277
 
278
  /* 이미지 컨테이너 */
279
- .gradio-container .image-container img {
280
- border-radius: 12px !important;
281
- box-shadow: 0 0 30px rgba(0, 245, 255, 0.2) !important;
 
282
  }
283
 
284
- /* 아코디언 */
285
- .gradio-container .accordion {
286
- background: var(--glass-bg) !important;
287
- border: 1px solid rgba(0, 245, 255, 0.2) !important;
288
- border-radius: 12px !important;
289
  }
290
 
291
- /* 예시 버튼 */
292
- .gradio-container .examples-button {
293
- background: rgba(0, 245, 255, 0.1) !important;
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
- .gradio-container .examples-button:hover {
302
- background: rgba(0, 245, 255, 0.2) !important;
303
- box-shadow: var(--glow-cyan) !important;
304
  }
305
 
306
- /* 섹션 헤더 */
307
- .section-header {
308
- font-family: 'Orbitron', monospace !important;
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
- .compare-label-enhanced {
329
- background: linear-gradient(90deg, rgba(57, 255, 20, 0.2), transparent) !important;
330
- border-left: 4px solid var(--neon-green) !important;
331
- color: var(--neon-green) !important;
332
- font-family: 'Orbitron', monospace !important;
333
- padding: 12px 20px !important;
334
- font-weight: 600 !important;
335
- letter-spacing: 2px !important;
336
- }
337
-
338
- /* 스캔라인 효과 */
339
- .scanlines::after {
340
- content: '';
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.default.minimal.svelte-1rjryqp,
406
  .svelte-1rjryqp:has(a[href*="huggingface"]),
407
- .wrap:has(> a[href*="huggingface"]) {
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
- # API 함수들
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
- gr.HTML("""
602
- <div class="scanlines">
603
- <div style="text-align: center; padding: 60px 20px 40px 20px; position: relative;">
604
- <!-- 배경 글로우 -->
605
- <div style="
606
- position: absolute;
607
- top: 50%;
608
- left: 50%;
609
- transform: translate(-50%, -50%);
610
- width: 400px;
611
- height: 400px;
612
- background: radial-gradient(circle, rgba(0, 245, 255, 0.15) 0%, transparent 70%);
613
- filter: blur(40px);
614
- pointer-events: none;
615
- "></div>
616
-
617
- <!-- 아이콘 -->
618
- <div style="
619
- width: 80px;
620
- height: 80px;
621
- margin: 0 auto 24px auto;
622
- background: linear-gradient(135deg, rgba(0, 245, 255, 0.3), rgba(255, 0, 255, 0.3));
623
- border: 2px solid rgba(0, 245, 255, 0.5);
624
- border-radius: 20px;
625
- display: flex;
626
- align-items: center;
627
- justify-content: center;
628
- font-size: 36px;
629
- box-shadow: 0 0 40px rgba(0, 245, 255, 0.4);
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
- with gr.Column(scale=1):
698
- gr.HTML("""
699
- <div style="
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.HTML("<div style='height: 16px;'></div>")
 
 
 
720
 
721
- generate_btn = gr.Button(
722
- "⚡ EXECUTE ENHANCEMENT",
723
- variant="primary",
724
- size="lg",
725
- elem_classes=["neon-button"]
726
- )
727
-
728
- gr.HTML("<div style='height: 30px;'></div>")
729
-
730
- # 상태 표시
731
- status_text = gr.Textbox(
732
- label="📊 SYSTEM STATUS",
733
- value="[ AWAITING INPUT ]",
734
- interactive=False,
735
- lines=3,
736
- elem_classes=["status-box"]
737
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
738
 
739
- gr.HTML("<div style='height: 20px;'></div>")
740
-
741
- # 증강 프롬프트 출력
742
- gr.HTML("""
743
- <div style="
744
- font-family: 'Orbitron', monospace;
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
- gr.HTML("<div style='height: 40px;'></div>")
763
-
764
- # 이미지 비교 섹션
765
- gr.HTML("""
766
- <div style="text-align: center; margin-bottom: 30px;">
767
- <div class="section-header">🖼️ VISUAL COMPARISON</div>
768
- <div style="
769
- width: 100px;
770
- height: 2px;
771
- background: linear-gradient(90deg, transparent, #ff00ff, transparent);
772
- margin: 16px auto 0 auto;
773
- "></div>
774
- </div>
775
- """)
776
-
777
- with gr.Row():
778
- with gr.Column(elem_classes=["image-card"]):
779
- gr.HTML("""
780
- <div class="compare-label-original">
781
- 📌 ORIGINAL PROMPT RESULT
782
- </div>
783
- """)
784
- original_image_output = gr.Image(
785
- label="",
786
- type="filepath",
787
- show_label=False,
788
- height=400
789
- )
790
-
791
- with gr.Column(elem_classes=["image-card"]):
792
- gr.HTML("""
793
- <div class="compare-label-enhanced">
794
- ENHANCED PROMPT RESULT
795
- </div>
796
- """)
797
- enhanced_image_output = gr.Image(
798
- label="",
799
- type="filepath",
800
- show_label=False,
801
- height=400
802
- )
803
-
804
- # 푸터
805
- gr.HTML("""
806
- <div style="text-align: center; padding: 50px 20px 30px 20px;">
807
- <div style="
808
- width: 60%;
809
- height: 1px;
810
- background: linear-gradient(90deg, transparent, rgba(0, 245, 255, 0.3), transparent);
811
- margin: 0 auto 30px auto;
812
- "></div>
813
- <p class="cyber-footer">
814
- POWERED BY FIREWORKS AI (GPT-OSS-120B) + FAL AI (NANO-BANANA-PRO)
815
- </p>
816
- <p class="cyber-footer" style="margin-top: 8px; opacity: 0.6;">
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()