ginipick commited on
Commit
2098f9b
·
verified ·
1 Parent(s): 76fcc2b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +394 -410
app.py CHANGED
@@ -2,15 +2,19 @@ import gradio as gr
2
  import requests
3
  import json
4
  import os
 
5
  import sqlite3
6
- from datetime import datetime, date
 
 
7
 
8
  # ============================================
9
  # 환경변수
10
  # ============================================
11
  FIREWORKS_API_KEY = os.getenv("FIREWORKS_API_KEY", "")
12
  FAL_KEY = os.getenv("FAL_KEY", "")
13
- ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "Time2175!@") # 관리자 패스워드
 
14
 
15
  # Persistent Storage 경로 (HuggingFace Spaces)
16
  DATA_DIR = "/data" if os.path.exists("/data") else "./data"
@@ -18,7 +22,7 @@ DB_PATH = os.path.join(DATA_DIR, "app.db")
18
 
19
  # 설정
20
  DAILY_LIMIT_FREE = 10
21
- DAILY_LIMIT_ADMIN = 9999
22
 
23
  # ============================================
24
  # 데이터베이스
@@ -33,12 +37,11 @@ def init_db():
33
  conn = get_db()
34
  cursor = conn.cursor()
35
 
36
- # HF OAuth용 users 테이블 (password 필요 없음)
37
  cursor.execute('''
38
  CREATE TABLE IF NOT EXISTS users (
39
  id INTEGER PRIMARY KEY AUTOINCREMENT,
40
- hf_username TEXT UNIQUE NOT NULL,
41
- hf_name TEXT,
42
  is_admin INTEGER DEFAULT 0,
43
  is_active INTEGER DEFAULT 1,
44
  daily_limit INTEGER DEFAULT 10,
@@ -47,6 +50,17 @@ def init_db():
47
  )
48
  ''')
49
 
 
 
 
 
 
 
 
 
 
 
 
50
  cursor.execute('''
51
  CREATE TABLE IF NOT EXISTS generations (
52
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -72,80 +86,78 @@ def init_db():
72
  ''')
73
 
74
  conn.commit()
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  conn.close()
76
- print("✅ Database initialized")
77
 
78
  # ============================================
79
- # 사용자 관리 (HuggingFace OAuth)
80
  # ============================================
81
- def get_or_create_user(hf_username: str, hf_name: str = None) -> dict:
82
- """HF 유저를 가져오거나 새로 생성"""
 
 
 
 
 
 
 
 
83
  conn = get_db()
84
  cursor = conn.cursor()
85
-
86
- cursor.execute("SELECT * FROM users WHERE hf_username = ?", (hf_username,))
87
- row = cursor.fetchone()
88
-
89
- if row:
90
- # 기존 유저 - last_login 업데이트
91
- cursor.execute("UPDATE users SET last_login = ? WHERE id = ?", (datetime.now(), row["id"]))
92
- conn.commit()
93
- user = {
94
- "id": row["id"],
95
- "hf_username": row["hf_username"],
96
- "hf_name": row["hf_name"],
97
- "is_admin": bool(row["is_admin"]),
98
- "is_active": bool(row["is_active"]),
99
- "daily_limit": row["daily_limit"]
100
- }
101
- else:
102
- # 새 유저 생성 (일반 사용자로)
103
- cursor.execute(
104
- "INSERT INTO users (hf_username, hf_name, is_admin, daily_limit, last_login) VALUES (?, ?, ?, ?, ?)",
105
- (hf_username, hf_name, 0, DAILY_LIMIT_FREE, datetime.now())
106
- )
107
- conn.commit()
108
- user_id = cursor.lastrowid
109
- user = {
110
- "id": user_id,
111
- "hf_username": hf_username,
112
- "hf_name": hf_name,
113
- "is_admin": False,
114
- "is_active": True,
115
- "daily_limit": DAILY_LIMIT_FREE
116
- }
117
- print(f"✅ New user created: {hf_username}")
118
-
119
  conn.close()
120
- return user
121
 
122
- def get_user_by_username(hf_username: str) -> dict:
123
- """유저네임으로 유저 조회"""
124
- if not hf_username:
125
  return None
126
  conn = get_db()
127
  cursor = conn.cursor()
128
- cursor.execute("SELECT * FROM users WHERE hf_username = ?", (hf_username,))
 
 
 
 
129
  row = cursor.fetchone()
130
  conn.close()
131
  if row:
132
- return {
133
- "id": row["id"],
134
- "hf_username": row["hf_username"],
135
- "hf_name": row["hf_name"],
136
- "is_admin": bool(row["is_admin"]),
137
- "is_active": bool(row["is_active"]),
138
- "daily_limit": row["daily_limit"]
139
- }
140
  return None
141
 
 
 
 
 
 
 
 
142
  # ============================================
143
  # 사용량 관리
144
  # ============================================
145
  def get_daily_usage(user_id: int) -> int:
146
  conn = get_db()
147
  cursor = conn.cursor()
148
- cursor.execute("SELECT count FROM daily_usage WHERE user_id = ? AND date = ?", (user_id, date.today()))
149
  row = cursor.fetchone()
150
  conn.close()
151
  return row["count"] if row else 0
@@ -153,7 +165,7 @@ def get_daily_usage(user_id: int) -> int:
153
  def increment_usage(user_id: int) -> int:
154
  conn = get_db()
155
  cursor = conn.cursor()
156
- today = date.today()
157
  cursor.execute('''
158
  INSERT INTO daily_usage (user_id, date, count) VALUES (?, ?, 1)
159
  ON CONFLICT(user_id, date) DO UPDATE SET count = count + 1
@@ -198,7 +210,7 @@ def get_user_generations(user_id: int, limit: int = 20) -> list:
198
  def get_all_users() -> list:
199
  conn = get_db()
200
  cursor = conn.cursor()
201
- cursor.execute('SELECT id, hf_username, hf_name, is_admin, is_active, daily_limit, created_at, last_login FROM users ORDER BY created_at DESC')
202
  rows = cursor.fetchall()
203
  conn.close()
204
  return [dict(row) for row in rows]
@@ -206,7 +218,7 @@ def get_all_users() -> list:
206
  def get_stats() -> dict:
207
  conn = get_db()
208
  cursor = conn.cursor()
209
- today = date.today()
210
 
211
  cursor.execute("SELECT COUNT(*) as count FROM users")
212
  total_users = cursor.fetchone()["count"]
@@ -221,12 +233,7 @@ def get_stats() -> dict:
221
  generations_today = cursor.fetchone()["count"]
222
 
223
  conn.close()
224
- return {
225
- "total_users": total_users,
226
- "active_today": active_today,
227
- "total_generations": total_generations,
228
- "generations_today": generations_today
229
- }
230
 
231
  def update_user_limit(user_id: int, daily_limit: int) -> bool:
232
  conn = get_db()
@@ -236,13 +243,59 @@ def update_user_limit(user_id: int, daily_limit: int) -> bool:
236
  conn.close()
237
  return True
238
 
239
- def toggle_user_active(user_id: int) -> bool:
 
 
 
 
 
 
 
 
 
 
 
 
240
  conn = get_db()
241
  cursor = conn.cursor()
242
- cursor.execute("UPDATE users SET is_active = NOT is_active WHERE id = ?", (user_id,))
 
 
 
 
 
 
243
  conn.commit()
 
244
  conn.close()
245
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
  # ============================================
248
  # API 함수들
@@ -292,6 +345,7 @@ def generate_image(prompt: str, fal_key: str, aspect_ratio: str = "1:1", resolut
292
  if not prompt.strip() or not fal_key.strip():
293
  return None
294
 
 
295
  url = "https://fal.run/fal-ai/nano-banana-pro"
296
  headers = {"Authorization": f"Key {fal_key}", "Content-Type": "application/json"}
297
 
@@ -313,54 +367,37 @@ def generate_image(prompt: str, fal_key: str, aspect_ratio: str = "1:1", resolut
313
  print(f"이미지 생성 오류: {e}")
314
  return None
315
 
316
- def process_generation(original_prompt, aspect_ratio, resolution, request: gr.Request):
317
- """이미지 생성 처리 - HF OAuth 사용"""
318
- # 로그인 확인
319
- if not request.username:
320
- yield "❌ 로그인이 필요합니다. 상단에서 HuggingFace로 로그인해주세요.", None, None, None, "🔐 로그인이 필요합니다"
321
  return
322
 
323
- # 유저 정보 가져오기/생성
324
- user = get_or_create_user(request.username)
325
- admin_badge = " 👑 관리자" if user["is_admin"] else ""
326
-
327
- if not user["is_active"]:
328
- yield "❌ 비활성화된 계정입니다.", None, None, None, f"**👋 {request.username}**{admin_badge} | ❌ 비활성화"
329
- return
330
-
331
- # 사용량 확인
332
  allowed, current, limit = check_usage_limit(user["id"], user["daily_limit"])
333
  if not allowed:
334
- yield f"❌ 일일 한도({limit}회) 초과", None, None, None, f"**👋 {request.username}**{admin_badge} | 사용량: {current}/{limit} ❌ 초과"
335
  return
336
 
337
- fw_key = FIREWORKS_API_KEY
338
- fl_key = FAL_KEY
339
 
340
- if not fw_key or not fl_key:
341
- yield "❌ API 키가 설정되지 않았습니다.", None, None, None, f"**👋 {request.username}**{admin_badge} | 사용량: {current}/{limit}"
342
- return
343
-
344
- status_base = f"**👋 {request.username}**{admin_badge}"
345
-
346
- yield f"⚡ 프롬프트 증강 중...", None, None, None, f"{status_base} | 사용량: {current}/{limit}"
347
 
348
  enhanced = enhance_prompt(original_prompt, fw_key)
349
  if enhanced.startswith("❌"):
350
- yield enhanced, None, None, None, f"{status_base} | 사용량: {current}/{limit}"
351
  return
352
 
353
- yield "✅ 프롬프트 증강 완료\n⚡ 원본 이미지 생성 중...", enhanced, None, None, f"{status_base} | 사용량: {current}/{limit}"
354
  original_image = generate_image(original_prompt, fl_key, aspect_ratio, resolution)
355
 
356
- yield "✅ 원본 이미지 완료\n⚡ 증강 이미지 생성 중...", enhanced, original_image, None, f"{status_base} | 사용량: {current}/{limit}"
357
  enhanced_image = generate_image(enhanced, fl_key, aspect_ratio, resolution)
358
 
359
- # 사용량 증가 및 저장
360
  new_count = increment_usage(user["id"])
361
  save_generation(user["id"], original_prompt, enhanced, enhanced_image or original_image, "success" if enhanced_image else "partial")
362
 
363
- yield f"✅ 완료! 사용량: {new_count}/{limit}", enhanced, original_image, enhanced_image, f"{status_base} | 사용량: {new_count}/{limit}"
364
 
365
  # ============================================
366
  # CSS - 밝은 프로페셔널 테마
@@ -374,23 +411,29 @@ CUSTOM_CSS = """
374
  --primary-blue-dark: #1d4ed8;
375
  --accent-purple: #7c3aed;
376
  --accent-green: #10b981;
 
377
  --bg-white: #ffffff;
378
  --bg-gray-50: #f9fafb;
379
  --bg-gray-100: #f3f4f6;
 
380
  --text-primary: #111827;
381
  --text-secondary: #4b5563;
382
  --text-muted: #9ca3af;
383
  --border-color: #e5e7eb;
384
  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
385
- --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
 
386
  --radius: 12px;
387
  }
388
 
 
389
  .gradio-container {
390
  background: linear-gradient(135deg, var(--bg-gray-50) 0%, var(--bg-white) 50%, var(--bg-gray-100) 100%) !important;
391
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
 
392
  }
393
 
 
394
  .cyber-title {
395
  font-family: 'Poppins', sans-serif !important;
396
  font-size: 2.5rem !important;
@@ -400,50 +443,87 @@ CUSTOM_CSS = """
400
  -webkit-text-fill-color: transparent;
401
  background-clip: text;
402
  text-align: center;
 
403
  }
404
 
 
405
  .gradio-container textarea,
406
  .gradio-container input[type="text"],
407
- .gradio-container input[type="password"] {
 
408
  background: var(--bg-white) !important;
409
  border: 2px solid var(--border-color) !important;
410
  border-radius: var(--radius) !important;
411
  color: var(--text-primary) !important;
412
  font-family: 'Inter', sans-serif !important;
 
 
 
413
  }
414
 
415
  .gradio-container textarea:focus,
416
  .gradio-container input:focus {
417
  border-color: var(--primary-blue) !important;
418
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1) !important;
 
 
 
 
 
 
419
  }
420
 
 
421
  .gradio-container label {
422
  font-family: 'Inter', sans-serif !important;
423
  color: var(--text-primary) !important;
424
  font-weight: 600 !important;
 
 
 
 
 
 
 
 
 
 
 
425
  }
426
 
 
427
  .neon-button {
428
  background: linear-gradient(135deg, var(--primary-blue) 0%, var(--primary-blue-dark) 100%) !important;
429
  border: none !important;
430
  border-radius: var(--radius) !important;
431
  color: #ffffff !important;
432
  font-family: 'Inter', sans-serif !important;
 
433
  font-weight: 600 !important;
434
  padding: 12px 24px !important;
435
  box-shadow: var(--shadow-md), 0 4px 14px rgba(37, 99, 235, 0.25) !important;
 
436
  }
437
 
438
  .neon-button:hover {
439
  background: linear-gradient(135deg, var(--primary-blue-light) 0%, var(--primary-blue) 100%) !important;
 
440
  transform: translateY(-2px) !important;
441
  }
442
 
 
 
 
 
 
 
 
 
443
  .gradio-container .tabs {
444
  background: var(--bg-white) !important;
445
  border-radius: 16px !important;
446
  padding: 8px !important;
 
447
  }
448
 
449
  .gradio-container .tab-nav button {
@@ -451,6 +531,7 @@ CUSTOM_CSS = """
451
  color: var(--text-secondary) !important;
452
  border-radius: 10px !important;
453
  font-weight: 500 !important;
 
454
  }
455
 
456
  .gradio-container .tab-nav button.selected {
@@ -458,158 +539,134 @@ CUSTOM_CSS = """
458
  color: #ffffff !important;
459
  }
460
 
461
- .user-info-box {
462
- background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
463
- border: 1px solid #bae6fd;
464
- border-radius: 12px;
465
- padding: 16px;
466
- margin-bottom: 16px;
467
  }
468
 
469
- .admin-badge {
470
- background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
471
- color: #92400e;
472
- padding: 4px 12px;
473
- border-radius: 20px;
474
- font-size: 0.85rem;
475
- font-weight: 600;
476
  }
477
 
478
- /* 허깅페이스 푸터 숨기기 */
479
- footer {
480
- display: none !important;
 
481
  }
482
 
483
- /* 허깅페이스 우측 상단 탭박스 숨기기 */
484
- #huggingface-space-header,
485
- .huggingface-space-header,
486
- div[id*="space-header"],
487
- div[class*="space-header"],
488
- .wrap.svelte-1rjryqp,
489
- div.wrap.svelte-1rjryqp,
490
- a[href*="huggingface.co/spaces"][target="_blank"],
491
- .built-with,
492
- .built-with-badge,
493
- div[style*="position: fixed"][style*="top"][style*="right"],
494
- div[style*="position: absolute"][style*="top: 0"][style*="right: 0"] {
495
- display: none !important;
496
- visibility: hidden !important;
497
- opacity: 0 !important;
498
- pointer-events: none !important;
499
  }
500
 
501
- /* Gradio LoginButton 스타일 오버라이드 */
502
- .hf-login-btn button,
503
- button.hf-login-btn,
504
- .hf-login-btn > button {
505
- background: linear-gradient(135deg, #ff9d00 0%, #ffb347 100%) !important;
506
- color: #000 !important;
507
  font-weight: 600 !important;
508
- font-size: 0.9rem !important;
509
- padding: 10px 20px !important;
510
- border-radius: 25px !important;
511
- border: none !important;
512
- cursor: pointer !important;
513
- box-shadow: 0 4px 12px rgba(255, 157, 0, 0.3) !important;
514
- transition: all 0.2s ease !important;
515
  }
516
 
517
- .hf-login-btn button:hover {
518
- transform: translateY(-2px) !important;
519
- box-shadow: 0 6px 16px rgba(255, 157, 0, 0.4) !important;
520
  }
521
 
522
- /* 로그아웃 버튼 */
523
- .logout-btn button,
524
- button.logout-btn {
525
- background: #f3f4f6 !important;
526
- color: #4b5563 !important;
527
- font-weight: 500 !important;
528
- font-size: 0.85rem !important;
529
- padding: 8px 16px !important;
530
- border-radius: 20px !important;
531
- border: 1px solid #e5e7eb !important;
 
 
532
  }
533
 
534
- .logout-btn button:hover {
535
- background: #e5e7eb !important;
 
536
  }
537
 
538
- /* 사용량 정보 */
539
- .usage-info {
540
- display: flex;
541
- align-items: center;
542
- padding: 8px 16px;
543
- background: #f0f9ff;
544
- border-radius: 20px;
545
- font-size: 0.9rem;
546
  }
547
 
548
- /* 헤더 Row */
549
- .header-row {
550
- background: linear-gradient(135deg, #ffffff 0%, #f0f9ff 100%);
551
- border-radius: 16px;
552
- padding: 10px 20px;
553
- margin-bottom: 20px;
554
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
555
- align-items: center;
556
  }
557
 
558
- .hf-login-btn svg {
559
- width: 20px;
560
- height: 20px;
561
  }
562
 
563
- /* 로그인된 상태 배지 */
564
- .user-badge {
565
- display: inline-flex !important;
566
- visibility: visible !important;
567
- opacity: 1 !important;
568
- align-items: center;
569
- gap: 8px;
570
- background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
571
- color: #fff !important;
572
- font-weight: 600;
573
- font-size: 0.9rem;
574
- padding: 10px 20px;
575
- border-radius: 25px;
576
- box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
577
  }
578
 
579
- .admin-crown {
580
- color: #fbbf24;
 
 
581
  }
582
 
583
- /* 헤더 컨테이너 */
584
- .header-container {
585
- display: flex !important;
586
- visibility: visible !important;
587
- opacity: 1 !important;
588
- justify-content: space-between;
589
- align-items: center;
590
- padding: 20px 30px;
591
- background: linear-gradient(135deg, #ffffff 0%, #f0f9ff 100%);
592
- border-radius: 16px;
593
- margin-bottom: 20px;
594
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
595
  }
596
 
597
- .header-left {
598
- flex: 1;
 
599
  }
600
 
601
- .header-right {
602
- display: flex;
603
- align-items: center;
604
- gap: 16px;
605
  }
606
 
607
- .usage-badge {
608
- background: #f3f4f6;
609
- padding: 8px 16px;
610
- border-radius: 20px;
611
- font-size: 0.85rem;
612
- color: #4b5563;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
613
  }
614
  """
615
 
@@ -624,152 +681,49 @@ examples = [
624
  ["바다 위 일몰과 작은 돛단배"],
625
  ]
626
 
627
- def get_user_info(request: gr.Request):
628
- """현재 로그인한 유저 정보 반환"""
629
- if not request.username:
630
- return "### 🔐 로그인이 필요합니다\n\n상단의 **HuggingFace로 로그인** 버튼을 클릭하세요.", ""
631
 
632
- user = get_or_create_user(request.username)
633
- usage = get_daily_usage(user["id"])
634
-
635
- admin_badge = "👑 **관리자**" if user["is_admin"] else ""
636
-
637
- info = f"""### 👋 안녕하세요, **{request.username}**님! {admin_badge}
638
-
639
- | 항목 | 값 |
640
- |:-----|:-----|
641
- | 오늘 사용량 | **{usage}** / {user['daily_limit']} |
642
- | 계정 상태 | {'✅ 활성' if user['is_active'] else '❌ 비활성'} |
643
- """
644
- return info, f"{usage}/{user['daily_limit']}"
645
-
646
- def load_account_info(request: gr.Request):
647
- """내 계정 정보 로드"""
648
- if not request.username:
649
- return "로그인이 필요합니다.", []
650
-
651
- user = get_or_create_user(request.username)
652
- usage = get_daily_usage(user["id"])
653
-
654
- info = f"""### 👤 계정 정보
655
-
656
- | 항목 | 값 |
657
- |:-----|:-----|
658
- | HuggingFace ID | **{user['hf_username']}** |
659
- | 등급 | {'👑 관리자' if user['is_admin'] else '일반 사용자'} |
660
- | 일일 제한 | {user['daily_limit']}회 |
661
- | 오늘 사용량 | {usage}회 |
662
- | 계정 상태 | {'✅ 활성' if user['is_active'] else '❌ 비활성'} |
663
- """
664
-
665
- history = get_user_generations(user["id"])
666
- history_data = [[h["original_prompt"][:50]+"..." if h["original_prompt"] else "", h["created_at"], h["status"]] for h in history]
667
-
668
- return info, history_data
669
-
670
- def admin_login(password):
671
- """관리자 패스워드 인증"""
672
- if password == ADMIN_PASSWORD:
673
- stats = get_stats()
674
- stats_md = f"""### 📊 통계
675
-
676
- | 지표 | 값 |
677
- |:-----|:-----|
678
- | 총 사용자 | {stats['total_users']} |
679
- | 오늘 활성 | {stats['active_today']} |
680
- | 총 생성 | {stats['total_generations']} |
681
- | 오늘 생성 | {stats['generations_today']} |
682
- """
683
- users = get_all_users()
684
- users_data = [[u["id"], u["hf_username"], "✅" if u["is_admin"] else "", "✅" if u["is_active"] else "❌", u["daily_limit"], str(u["created_at"])[:19] if u["created_at"] else ""] for u in users]
685
-
686
- return "✅ 관리자 인증 성공!", gr.update(visible=True), gr.update(visible=True), stats_md, users_data
687
- else:
688
- return "❌ 패스워드가 틀립니다.", gr.update(visible=False), gr.update(visible=False), "", []
689
-
690
- def load_admin_panel_refresh():
691
- """관리자 패널 새로고침"""
692
- stats = get_stats()
693
- stats_md = f"""### 📊 통계
694
-
695
- | 지표 | 값 |
696
- |:-----|:-----|
697
- | 총 사용자 | {stats['total_users']} |
698
- | 오늘 활성 | {stats['active_today']} |
699
- | 총 생성 | {stats['total_generations']} |
700
- | 오늘 생성 | {stats['generations_today']} |
701
- """
702
- users = get_all_users()
703
- users_data = [[u["id"], u["hf_username"], "✅" if u["is_admin"] else "", "✅" if u["is_active"] else "❌", u["daily_limit"], str(u["created_at"])[:19] if u["created_at"] else ""] for u in users]
704
-
705
- return stats_md, users_data
706
-
707
- def admin_toggle_user(user_id):
708
- """유저 활성화/비활성화"""
709
- toggle_user_active(int(user_id))
710
- return "✅ 상태 변경됨"
711
-
712
- def admin_change_limit(user_id, new_limit):
713
- """유저 일일 제한 변경"""
714
- update_user_limit(int(user_id), int(new_limit))
715
- return f"✅ 제한 변경됨: {int(new_limit)}"
716
-
717
- # Gradio 앱 빌드
718
- with gr.Blocks(title="AI PROMPT ENHANCER") as demo:
719
-
720
- # CSS 주입 + 커스텀 도메인 리다이렉트 스크립트
721
- gr.HTML(f"""
722
- <style>{CUSTOM_CSS}</style>
723
- <script>
724
- // 페이지 로드 즉시 실행
725
- const currentHost = window.location.hostname;
726
- const customDomain = '1street.ai';
727
-
728
- // huggingface.co 또는 hf.space 도메인이면 커스텀 도메인으로 리다이렉트
729
- if (currentHost.includes('huggingface.co') || currentHost.includes('hf.space')) {{
730
- window.location.href = 'https://' + customDomain;
731
- }}
732
-
733
- // DOM 로드 후에도 체크
734
- document.addEventListener('DOMContentLoaded', function() {{
735
- if (window.location.hostname.includes('huggingface.co') || window.location.hostname.includes('hf.space')) {{
736
- window.location.href = 'https://' + customDomain;
737
- }}
738
- }});
739
- </script>
740
- <noscript>
741
- <meta http-equiv="refresh" content="0;url=https://1street.ai">
742
- </noscript>
743
- """)
744
-
745
- # 헤더 영역
746
- with gr.Row():
747
- with gr.Column(scale=4):
748
- gr.HTML('''
749
- <div style="padding: 15px 0;">
750
- <h1 class="cyber-title" style="margin:0; font-size:2rem;">🚀 PROMPT ENHANCER</h1>
751
- <p style="color: #4b5563; font-size: 0.95rem; margin: 4px 0 0 0;">AI-Powered Image Generation</p>
752
- </div>
753
- ''')
754
- with gr.Column(scale=1, min_width=200):
755
- gr.HTML('''
756
- <a href="/login/huggingface?_target_url=https://1street.ai"
757
- style="display: inline-flex; align-items: center; gap: 8px;
758
- background: linear-gradient(135deg, #ff9d00 0%, #ffb347 100%);
759
- color: #000; font-weight: 600; font-size: 0.9rem;
760
- padding: 10px 20px; border-radius: 25px; text-decoration: none;
761
- box-shadow: 0 4px 12px rgba(255, 157, 0, 0.3);">
762
- 🤗 로그인
763
- </a>
764
- ''')
765
-
766
- # 유저 정보 표시
767
- user_status = gr.Markdown("")
768
- usage_info = gr.Markdown("")
769
 
770
  with gr.Tabs() as tabs:
771
- # Tab 1: 이미지 생성
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
772
  with gr.Tab("✨ 이미지 생성"):
 
 
 
 
 
 
 
773
  with gr.Row():
774
  with gr.Column(scale=2):
775
  prompt_input = gr.Textbox(label="프롬프트 입력", placeholder="생성할 이미지를 설명해주세요...", lines=4)
@@ -790,72 +744,102 @@ with gr.Blocks(title="AI PROMPT ENHANCER") as demo:
790
  gr.Markdown("### ✨ 증강")
791
  enhanced_image_output = gr.Image(label="", height=400)
792
 
793
- # Tab 2: 내 계정
794
  with gr.Tab("👤 내 계정") as account_tab:
795
  account_info = gr.Markdown("로그인이 필요합니다.")
796
  history_display = gr.Dataframe(headers=["프롬프트", "생성일시", "상태"], interactive=False)
797
  refresh_history_btn = gr.Button("🔄 새로고침")
798
 
799
- # Tab 3: ���리자
800
  with gr.Tab("🛠️ 관리자") as admin_tab:
801
- admin_auth_status = gr.Markdown("🔐 관리자 패스워드를 입력하세요.")
802
- with gr.Row():
803
- admin_password_input = gr.Textbox(label="관리자 패스워드", type="password", placeholder="패스워드 입력")
804
- admin_login_btn = gr.Button("🔓 인증", size="sm")
805
-
806
  with gr.Row(visible=False) as admin_panel:
807
  with gr.Column():
 
808
  stats_display = gr.Markdown("")
809
- refresh_stats_btn = gr.Button("📊 통계 새로고침")
810
  with gr.Column():
811
  gr.Markdown("### 👥 사용자 관리")
812
- users_table = gr.Dataframe(headers=["ID", "HF Username", "관리자", "활성", "일일제한", "가입일"], interactive=False)
813
- refresh_users_btn = gr.Button("🔄 사용자 새로고침")
814
  with gr.Row(visible=False) as admin_actions:
815
  user_id_input = gr.Number(label="사용자 ID", precision=0)
816
  toggle_active_btn = gr.Button("활성화/비활성화")
817
  new_limit = gr.Number(label="새 일일 제한", value=10, precision=0)
818
  update_limit_btn = gr.Button("제한 변경")
819
  admin_action_status = gr.Markdown("")
820
-
821
  # ========== 이벤트 핸들러 ==========
822
-
823
- # 페이지 로드 유저 상태 표시
824
- def show_user_status(request: gr.Request):
825
- if request.username:
826
- user = get_or_create_user(request.username)
827
- usage = get_daily_usage(user["id"])
828
- admin_badge = " 👑 관리자" if user["is_admin"] else ""
829
- return f"**👋 {request.username}**{admin_badge} | 사용량: {usage}/{user['daily_limit']}"
830
- return "🔐 로그인이 필요합니다"
831
-
832
- def show_usage_info(request: gr.Request):
833
- if request.username:
834
- user = get_or_create_user(request.username)
835
- usage = get_daily_usage(user["id"])
836
- return f"📊 **{usage}/{user['daily_limit']}**"
837
- return ""
838
-
839
- demo.load(show_user_status, None, [user_status])
840
- demo.load(show_usage_info, None, [usage_info])
841
-
842
- # 이미지 생성
843
- generate_btn.click(
844
- process_generation,
845
- [prompt_input, aspect_ratio, resolution],
846
- [status_text, enhanced_output, original_image_output, enhanced_image_output, user_status]
847
- )
848
-
849
- # 내 계정 탭
850
- account_tab.select(load_account_info, None, [account_info, history_display])
851
- refresh_history_btn.click(load_account_info, None, [account_info, history_display])
852
-
853
- # 관리자 - 패스워드 인증
854
- admin_login_btn.click(admin_login, [admin_password_input], [admin_auth_status, admin_panel, admin_actions, stats_display, users_table])
855
- refresh_stats_btn.click(load_admin_panel_refresh, None, [stats_display, users_table])
856
- refresh_users_btn.click(load_admin_panel_refresh, None, [stats_display, users_table])
857
- toggle_active_btn.click(admin_toggle_user, [user_id_input], [admin_action_status])
858
- update_limit_btn.click(admin_change_limit, [user_id_input, new_limit], [admin_action_status])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
859
 
860
  if __name__ == "__main__":
861
  demo.launch()
 
2
  import requests
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"
 
22
 
23
  # 설정
24
  DAILY_LIMIT_FREE = 10
25
+ SESSION_EXPIRE_HOURS = 24 * 7
26
 
27
  # ============================================
28
  # 데이터베이스
 
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,
 
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,
 
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
 
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
 
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]
 
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"]
 
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()
 
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 함수들
 
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
 
 
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 - 밝은 프로페셔널 테마
 
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;
 
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 {
 
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 {
 
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"],
647
+ footer,
648
+ .footer,
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
 
 
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)
 
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()