ginipick commited on
Commit
7b9855f
1 Parent(s): 36a4fe5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1 -721
app.py CHANGED
@@ -1,723 +1,3 @@
1
- import gradio as gr
2
- import hashlib
3
- import re
4
- from datetime import datetime
5
  import os
6
- import json
7
- import schedule
8
- import threading
9
- import time
10
- import dns.resolver
11
- from huggingface_hub import Repository
12
- import spaces
13
- import argparse
14
- from os import path
15
- import shutil
16
- from safetensors.torch import load_file
17
- from huggingface_hub import hf_hub_download
18
- import torch
19
- from diffusers import FluxPipeline
20
- from diffusers.pipelines.stable_diffusion import safety_checker
21
- from PIL import Image
22
- from transformers import pipeline
23
- import replicate
24
- import logging
25
- import requests
26
- from pathlib import Path
27
- import cv2
28
- import numpy as np
29
- import sys
30
- import io
31
-
32
- # 로깅 설정
33
- logging.basicConfig(level=logging.INFO)
34
- logger = logging.getLogger(__name__)
35
-
36
- # Hugging Face Dataset Repo 설정
37
- HF_TOKEN = os.getenv("HF_TOKEN")
38
- REPO_ID = "ginigen/MEMBERSHIP"
39
- LOCAL_DIR = "./my_dataset"
40
-
41
- repo = Repository(
42
- local_dir=LOCAL_DIR,
43
- clone_from=REPO_ID,
44
- use_auth_token=HF_TOKEN,
45
- repo_type="dataset"
46
- )
47
-
48
- DATA_FILE = os.path.join(LOCAL_DIR, "data.json")
49
-
50
- current_user = {"email": None, "points": 0}
51
- ADMIN_EMAIL = "arxivgpt@gmail.com"
52
- ADMIN_PASS = "Arxiv4837!@"
53
-
54
- # Setup and initialization code
55
- cache_path = path.join(path.dirname(path.abspath(__file__)), "models")
56
- PERSISTENT_DIR = os.environ.get("PERSISTENT_DIR", ".")
57
-
58
- # API 설정
59
- CATBOX_USER_HASH = "e7a96fc68dd4c7d2954040cd5"
60
- REPLICATE_API_TOKEN = os.getenv("API_KEY")
61
-
62
- # 환경 변수 설정
63
- os.environ["TRANSFORMERS_CACHE"] = cache_path
64
- os.environ["HF_HUB_CACHE"] = cache_path
65
- os.environ["HF_HOME"] = cache_path
66
-
67
- # 번역기 초기화
68
- translator = pipeline("translation", model="Helsinki-NLP/opus-mt-ko-en", device="cpu")
69
-
70
- if not path.exists(cache_path):
71
- os.makedirs(cache_path, exist_ok=True)
72
-
73
-
74
-
75
- def load_data():
76
- try:
77
- if os.path.exists(DATA_FILE):
78
- with open(DATA_FILE, 'r', encoding='utf-8') as f:
79
- return json.load(f)
80
- except Exception as e:
81
- print(f"Load error: {e}")
82
- return {"users": {}}
83
-
84
- def save_data(data):
85
- try:
86
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
87
- json.dump(data, f, ensure_ascii=False, indent=4)
88
- repo.git_add()
89
- repo.git_commit("Update data.json")
90
- repo.git_push()
91
- return True
92
- except Exception as e:
93
- print(f"Save error: {e}")
94
- return False
95
-
96
- def init_db():
97
- print("Initializing database...")
98
- try:
99
- data = load_data()
100
- if ADMIN_EMAIL not in data["users"]:
101
- data["users"][ADMIN_EMAIL] = {
102
- "password": hash_password(ADMIN_PASS),
103
- "registration_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
104
- "points": 999,
105
- "is_admin": 1
106
- }
107
- save_data(data)
108
- print("Admin account created")
109
- print("Database initialized successfully")
110
- return True
111
- except Exception as e:
112
- print(f"Init error: {e}")
113
- return False
114
-
115
- def hash_password(password: str) -> str:
116
- if not password:
117
- return ""
118
- return hashlib.sha256(password.encode("utf-8")).hexdigest()
119
-
120
- def is_valid_email(email: str) -> bool:
121
- pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
122
- return re.match(pattern, email) is not None
123
-
124
- def is_email_domain_valid(email: str) -> bool:
125
- try:
126
- domain = email.split("@")[1]
127
- except IndexError:
128
- return False
129
- try:
130
- records = dns.resolver.resolve(domain, 'MX')
131
- if len(records) > 0:
132
- return True
133
- except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.exception.Timeout):
134
- pass
135
- return False
136
-
137
- def register(email, password):
138
- if not email or not password:
139
- return "Please fill all fields."
140
- if not is_valid_email(email):
141
- return "Invalid email format. Please try again."
142
- if not is_email_domain_valid(email):
143
- return "This email domain seems invalid. Please use a different address."
144
- if len(password) < 6:
145
- return "Password must be at least 6 characters long."
146
-
147
- data = load_data()
148
- if email.strip() in data["users"]:
149
- return "This email is already registered."
150
-
151
- data["users"][email.strip()] = {
152
- "password": hash_password(password.strip()),
153
- "registration_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
154
- "points": 15, # 초기 포인트 15로 설정
155
- "is_admin": 0
156
- }
157
- if save_data(data):
158
- return "Registration successful!"
159
- return "Error occurred during registration."
160
-
161
- def login(email, password):
162
- global current_user
163
- if not email or not password:
164
- return {"value": "Please enter both email and password."}
165
-
166
- data = load_data()
167
- user_data = data["users"].get(email.strip())
168
- if user_data and user_data["password"] == hash_password(password.strip()):
169
- current_user = {
170
- "email": email.strip(),
171
- "points": user_data["points"],
172
- "is_admin": user_data.get("is_admin", 0)
173
- }
174
- if current_user["is_admin"] == 1:
175
- return {"value": "Logged in as ADMIN."}
176
- else:
177
- return {"value": f"Welcome! You have {user_data['points']} points."}
178
- current_user = {"email": None, "points": 0, "is_admin": 0}
179
- return {"value": "Wrong email or password."}
180
-
181
- def use_point(points=5):
182
- if not current_user["email"]:
183
- return "You need to log in first."
184
- data = load_data()
185
- user_data = data["users"][current_user["email"]]
186
- if user_data["points"] < points:
187
- return f"Not enough points. Required: {points} points"
188
- user_data["points"] -= points
189
- current_user["points"] = user_data["points"]
190
- if save_data(data):
191
- return f"Points used! Remaining points: {user_data['points']}"
192
- return "Error occurred while using points."
193
-
194
- def get_profile():
195
- if not current_user["email"]:
196
- return "", "", "You need to log in first."
197
- data = load_data()
198
- user_data = data["users"].get(current_user["email"])
199
- if user_data:
200
- return (
201
- current_user["email"],
202
- user_data["registration_date"],
203
- f"Current points: {user_data['points']}"
204
- )
205
- return "", "", "Profile not found."
206
-
207
- def delete_user(email):
208
- if not is_admin():
209
- return "Only admin can delete users."
210
- if email == ADMIN_EMAIL:
211
- return "Cannot delete the admin account."
212
- data = load_data()
213
- if email in data["users"]:
214
- del data["users"][email]
215
- if save_data(data):
216
- return f"User {email} has been deleted."
217
- return "User not found."
218
-
219
- def is_admin():
220
- return current_user.get("email") == ADMIN_EMAIL and current_user.get("is_admin") == 1
221
-
222
- def get_all_users():
223
- if not is_admin():
224
- return "Only admin can access this page."
225
- data = load_data()
226
- users = data["users"]
227
- if not users or len(users) <= 1:
228
- return "No registered users."
229
-
230
- result = "<table style='width:100%; border-collapse: collapse;'>"
231
- result += "<tr style='background-color: #f2f2f2;'>"
232
- result += "<th style='padding: 12px; text-align: left; border: 1px solid #ddd;'>Email</th>"
233
- result += "<th style='padding: 12px; text-align: left; border: 1px solid #ddd;'>Registration Date</th>"
234
- result += "<th style='padding: 12px; text-align: left; border: 1px solid #ddd;'>Points</th>"
235
- result += "<th style='padding: 12px; text-align: center; border: 1px solid #ddd;'>Action</th></tr>"
236
-
237
- for email, user_data in users.items():
238
- if email != ADMIN_EMAIL:
239
- result += f"<tr style='border: 1px solid #ddd;'>"
240
- result += f"<td style='padding: 12px; border: 1px solid #ddd;'>{email}</td>"
241
- result += f"<td style='padding: 12px; border: 1px solid #ddd;'>{user_data['registration_date']}</td>"
242
- result += f"<td style='padding: 12px; border: 1px solid #ddd;'>{user_data['points']}</td>"
243
- result += f"<td style='padding: 12px; text-align: center; border: 1px solid #ddd;'>"
244
- result += f"<button onclick='deleteUser(\"{email}\")' style='padding: 5px 10px; background-color: #ff4444; color: white; border: none; border-radius: 3px; cursor: pointer;'>Delete</button></td>"
245
- result += "</tr>"
246
- result += "</table>"
247
- return result
248
-
249
- @spaces.GPU
250
- def setup_torch():
251
- torch.backends.cuda.matmul.allow_tf32 = True
252
-
253
- def translate_if_korean(text):
254
- if any(ord(char) >= 0xAC00 and ord(char) <= 0xD7A3 for char in text):
255
- translation = translator(text)[0]['translation_text']
256
- return translation
257
- return text
258
-
259
- def filter_prompt(prompt):
260
- inappropriate_keywords = [
261
- "nude", "naked", "nsfw", "porn", "sex", "explicit", "adult", "xxx",
262
- "erotic", "sensual", "seductive", "provocative", "intimate",
263
- "violence", "gore", "blood", "death", "kill", "murder", "torture",
264
- "drug", "suicide", "abuse", "hate", "discrimination"
265
- ]
266
-
267
- prompt_lower = prompt.lower()
268
- for keyword in inappropriate_keywords:
269
- if keyword in prompt_lower:
270
- return False, "부적절한 내용이 포함된 프롬프트입니다."
271
- return True, prompt
272
-
273
- def process_prompt(prompt):
274
- translated_prompt = translate_if_korean(prompt)
275
- is_safe, filtered_prompt = filter_prompt(translated_prompt)
276
- return is_safe, filtered_prompt
277
-
278
- def add_watermark(video_path):
279
- try:
280
- cap = cv2.VideoCapture(video_path)
281
- width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
282
- height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
283
- fps = int(cap.get(cv2.CAP_PROP_FPS))
284
-
285
- text = "GiniGEN.AI"
286
- font = cv2.FONT_HERSHEY_SIMPLEX
287
- font_scale = height * 0.05 / 30
288
- thickness = 2
289
- color = (255, 255, 255)
290
-
291
- (text_width, text_height), _ = cv2.getTextSize(text, font, font_scale, thickness)
292
- margin = int(height * 0.02)
293
- x_pos = width - text_width - margin
294
- y_pos = height - margin
295
-
296
- output_path = "watermarked_output.mp4"
297
- fourcc = cv2.VideoWriter_fourcc(*'mp4v')
298
- out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
299
-
300
- while cap.isOpened():
301
- ret, frame = cap.read()
302
- if not ret:
303
- break
304
- cv2.putText(frame, text, (x_pos, y_pos), font, font_scale, color, thickness)
305
- out.write(frame)
306
-
307
- cap.release()
308
- out.release()
309
-
310
- return output_path
311
-
312
- except Exception as e:
313
- logger.error(f"Error adding watermark: {str(e)}")
314
- return video_path
315
-
316
- def check_api_key():
317
- api_key = os.getenv("API_KEY")
318
- if not api_key:
319
- logger.error("API_KEY environment variable not found")
320
- return False
321
-
322
- os.environ["REPLICATE_API_TOKEN"] = api_key
323
-
324
- try:
325
- response = requests.get(
326
- "https://api.replicate.com/v1/account",
327
- headers={"Authorization": f"Bearer {api_key}"}
328
- )
329
- if response.status_code == 200:
330
- logger.info("Replicate API token validated successfully")
331
- return True
332
- else:
333
- logger.error(f"API key validation failed with status code: {response.status_code}")
334
- return False
335
- except Exception as e:
336
- logger.error(f"API key validation error: {str(e)}")
337
- return False
338
-
339
- def generate_video(image, prompt):
340
- logger.info("Starting video generation")
341
- try:
342
- if not check_api_key():
343
- return "Replicate API key not properly configured"
344
-
345
- input_data = {
346
- "prompt": prompt
347
- }
348
-
349
- if image:
350
- try:
351
- import base64
352
- with open(image, 'rb') as img_file:
353
- data = base64.b64encode(img_file.read()).decode('utf-8')
354
- input_data["first_frame_image"] = f"data:image/png;base64,{data}"
355
- except Exception as img_error:
356
- logger.error(f"Error processing image: {str(img_error)}")
357
- return f"Error processing image: {str(img_error)}"
358
-
359
- try:
360
- prediction = replicate.predictions.create(
361
- model="minimax/video-01-live",
362
- input=input_data
363
- )
364
-
365
- while prediction.status not in ["succeeded", "failed", "canceled"]:
366
- prediction = replicate.predictions.get(prediction.id)
367
- time.sleep(1)
368
-
369
- if prediction.status == "succeeded" and prediction.output:
370
- temp_file = "temp_output.mp4"
371
- try:
372
- response = requests.get(prediction.output, stream=True)
373
- response.raise_for_status()
374
-
375
- with open(temp_file, "wb") as f:
376
- for chunk in response.iter_content(chunk_size=8192):
377
- if chunk:
378
- f.write(chunk)
379
-
380
- final_video = add_watermark(temp_file)
381
- return final_video
382
-
383
- except Exception as download_error:
384
- logger.error(f"Error downloading video: {str(download_error)}")
385
- return f"Error downloading video: {str(download_error)}"
386
- else:
387
- error_msg = f"Prediction failed with status: {prediction.status}"
388
- if hasattr(prediction, 'error'):
389
- error_msg += f" Error: {prediction.error}"
390
- logger.error(error_msg)
391
- return error_msg
392
-
393
- except Exception as api_error:
394
- logger.error(f"API call failed: {str(api_error)}")
395
- return f"API call failed: {str(api_error)}"
396
-
397
- except Exception as e:
398
- logger.error(f"Unexpected error: {str(e)}")
399
- return f"Unexpected error: {str(e)}"
400
- finally:
401
- try:
402
- if 'temp_file' in locals() and os.path.exists(temp_file):
403
- os.remove(temp_file)
404
- except Exception as cleanup_error:
405
- logger.warning(f"Error cleaning up temporary file: {str(cleanup_error)}")
406
-
407
- def process_and_generate_video(image, prompt):
408
- result = use_point(5) # 5포인트 차감
409
- if "Not enough points" in result or "need to log in" in result:
410
- return result
411
-
412
- is_safe, translated_prompt = process_prompt(prompt)
413
- if not is_safe:
414
- return "부적절한 내용이 포함된 프롬프트입니다."
415
-
416
- try:
417
- video_result = generate_video(image, translated_prompt)
418
- if isinstance(video_result, str) and ("error" in video_result.lower() or "failed" in video_result.lower()):
419
- # 에러 발생 시 포인트 환불
420
- data = load_data()
421
- user_data = data["users"][current_user["email"]]
422
- user_data["points"] += 5
423
- current_user["points"] = user_data["points"]
424
- save_data(data)
425
- return f"Error: {video_result}"
426
- return video_result
427
- except Exception as e:
428
- # 에러 발생 시 포인트 환불
429
- data = load_data()
430
- user_data = data["users"][current_user["email"]]
431
- user_data["points"] += 5
432
- current_user["points"] = user_data["points"]
433
- save_data(data)
434
- return f"Error: {str(e)}"
435
-
436
-
437
- # CSS 스타일 정의
438
- CUSTOM_CSS = """
439
- /* 한 줄에 Email + Password + (Login, Sign Up) 버튼 2개 + 결과창 */
440
-
441
- /* 전체 Row 컨테이너 */
442
- #row-container {
443
- display: flex;
444
- align-items: center;
445
- gap: 8px;
446
- background: linear-gradient(135deg, #b2fefa 0%, #8fd3f4 100%);
447
- padding: 10px;
448
- border-radius: 8px;
449
- box-shadow: 0 3px 8px rgba(0,0,0,0.15);
450
- margin-bottom: 15px;
451
- }
452
-
453
- /* 텍스트박스, 버튼 크기 통일 */
454
- .same-size {
455
- width: 130px !important;
456
- height: 34px !important;
457
- font-size: 0.9rem !important;
458
- text-align: center !important;
459
- padding: 5px 6px !important;
460
- margin: 0 !important;
461
- border-radius: 5px;
462
- border: 1px solid #ddd;
463
- }
464
-
465
- /* 버튼은 약간 다른 배경 */
466
- button.same-size {
467
- background-color: #6666ff;
468
- color: white;
469
- border: none;
470
- cursor: pointer;
471
- }
472
- button.same-size:hover {
473
- background-color: #4a4acc;
474
- }
475
-
476
- /* 결과창도 동일 크기 */
477
- .result-box {
478
- width: 160px !important;
479
- height: 34px !important;
480
- font-size: 0.9rem !important;
481
- padding: 5px 6px !important;
482
- border: 1px solid #ccc;
483
- border-radius: 5px;
484
- background-color: #fff;
485
- text-align: left;
486
- overflow: hidden;
487
- white-space: nowrap;
488
- text-overflow: ellipsis;
489
- }
490
-
491
- /* 상단 헤더 스타일 */
492
- .header-container {
493
- display: flex;
494
- justify-content: space-between;
495
- align-items: center;
496
- padding: 6px;
497
- background-color: #f5f5f5;
498
- margin-bottom: 10px;
499
- }
500
- .user-badge {
501
- padding: 3px 6px;
502
- border-radius: 15px;
503
- background-color: #4CAF50;
504
- color: white;
505
- font-size: 0.8em;
506
- }
507
- .admin-badge {
508
- padding: 3px 6px;
509
- border-radius: 15px;
510
- background-color: #ff4444;
511
- color: white;
512
- font-size: 0.8em;
513
- cursor: pointer;
514
- }
515
-
516
- .points-display {
517
- background: linear-gradient(135deg, #6e8efb 0%, #5d7df9 100%);
518
- padding: 20px;
519
- border-radius: 15px;
520
- margin: 20px 0;
521
- box-shadow: 0 4px 15px rgba(110, 142, 251, 0.2);
522
- }
523
-
524
- .points-text {
525
- color: white;
526
- font-size: 1.5em;
527
- text-align: center;
528
- margin: 0;
529
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
530
- }
531
-
532
- footer {
533
- visibility: hidden;
534
- }
535
-
536
- .gradio-container {
537
- background: linear-gradient(135deg, #f6f8ff 0%, #e9f0ff 100%);
538
- }
539
-
540
- .gr-button {
541
- border: 2px solid rgba(100, 100, 255, 0.2);
542
- background: linear-gradient(135deg, #6e8efb 0%, #5d7df9 100%);
543
- box-shadow: 0 4px 15px rgba(110, 142, 251, 0.2);
544
- }
545
-
546
- .gr-button:hover {
547
- background: linear-gradient(135deg, #5d7df9 0%, #4a6af8 100%);
548
- box-shadow: 0 4px 20px rgba(110, 142, 251, 0.3);
549
- }
550
-
551
- .gr-input, .gr-box {
552
- border-radius: 12px;
553
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
554
- border: 2px solid rgba(100, 100, 255, 0.1);
555
- }
556
- """
557
-
558
- # Gradio 인터페이스 생성
559
- with gr.Blocks(theme="soft", css=CUSTOM_CSS) as demo:
560
- gr.HTML("""<a href="https://visitorbadge.io/status?path=https%3A%2F%2Fginigen-Dokdo-membership.hf.space">
561
- <img src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Fginigen-Dokdo-membership.hf.space&countColor=%23263759" />
562
- </a>""")
563
-
564
- gr.Markdown("## 'Dokdo membership' Image to Video generation")
565
-
566
-
567
- # 상단 헤더
568
- with gr.Row(elem_classes="header-container"):
569
- gr.Markdown("##### Upon Dokdo membership registration, get 15 points. Video creation costs 5 points. Need more points through public contributions or paid services? Contact ginipicks@gmail.com.")
570
-
571
- with gr.Column(scale=1):
572
- user_info = gr.HTML(value="", elem_classes="user-badge")
573
- admin_button = gr.Button("Admin Page", visible=False, elem_classes="admin-badge")
574
-
575
- # 로그인/회원가입 섹션
576
- with gr.Row(elem_id="row-container"):
577
- email_box = gr.Textbox(
578
- placeholder="Email",
579
- show_label=False,
580
- elem_classes=["same-size"]
581
- )
582
- pass_box = gr.Textbox(
583
- placeholder="Password",
584
- show_label=False,
585
- type="password",
586
- elem_classes=["same-size"]
587
- )
588
- login_btn = gr.Button(
589
- "Login",
590
- elem_classes=["same-size"]
591
- )
592
- signup_btn = gr.Button(
593
- "Sign Up",
594
- elem_classes=["same-size"]
595
- )
596
- auth_output = gr.Textbox(
597
- show_label=False,
598
- elem_classes=["result-box"],
599
- interactive=False,
600
- placeholder="Message"
601
- )
602
-
603
- # 로그인 후 화면
604
- with gr.Column(elem_id="studio-section", visible=False) as studio_container:
605
- gr.Markdown("### My Studio")
606
- with gr.Group(elem_classes="points-display"):
607
- points_display = gr.Markdown("", elem_classes="points-text")
608
-
609
- # 비디오 생성 섹션
610
- with gr.Row():
611
- with gr.Column(scale=3):
612
- video_prompt = gr.Textbox(
613
- label="Video Description",
614
- placeholder="비디오 설명을 입력하세요... (한글 입력 가능)",
615
- lines=3
616
- )
617
- upload_image = gr.Image(type="filepath", label="Upload First Frame Image")
618
- video_generate_btn = gr.Button("🎬 Generate Video (5 points)")
619
-
620
- with gr.Column(scale=4):
621
- video_output = gr.Video(label="Generated Video")
622
-
623
- refresh_profile_button = gr.Button("Refresh Points")
624
-
625
- # 관리자 페이지
626
- with gr.Column(visible=False) as admin_container:
627
- gr.Markdown("### Admin Page")
628
- admin_refresh = gr.Button("Get All Users")
629
- admin_output = gr.HTML()
630
- delete_status = gr.Textbox(label="Result")
631
-
632
- # 이벤트 핸들러
633
- def login_and_update(email, password):
634
- res = login(email, password)
635
- msg = res["value"]
636
- if "Welcome!" in msg or "ADMIN" in msg:
637
- user_badge = f"<div>Logged in: {email}</div>"
638
- is_admin_flag = "ADMIN" in msg
639
- _, _, points_info = get_profile()
640
- return {
641
- user_info: user_badge,
642
- studio_container: gr.Column(visible=True),
643
- admin_button: gr.Button(visible=is_admin_flag),
644
- points_display: f"### {points_info}",
645
- auth_output: msg
646
- }
647
- else:
648
- return {auth_output: msg}
649
-
650
- # 이벤트 연결
651
- login_btn.click(
652
- fn=login_and_update,
653
- inputs=[email_box, pass_box],
654
- outputs=[
655
- user_info,
656
- studio_container,
657
- admin_button,
658
- points_display,
659
- auth_output
660
- ]
661
- )
662
-
663
- signup_btn.click(
664
- fn=register,
665
- inputs=[email_box, pass_box],
666
- outputs=auth_output
667
- )
668
-
669
- video_generate_btn.click(
670
- fn=process_and_generate_video,
671
- inputs=[upload_image, video_prompt],
672
- outputs=video_output
673
- )
674
-
675
- refresh_profile_button.click(
676
- fn=lambda: get_profile()[2], # points_info만 반환
677
- outputs=[points_display]
678
- )
679
-
680
- def toggle_admin_page():
681
- return {admin_container: gr.Column(visible=True)}
682
-
683
- admin_button.click(fn=toggle_admin_page, outputs=[admin_container])
684
- admin_refresh.click(fn=get_all_users, outputs=admin_output)
685
-
686
- # 회원 삭제 JS
687
- gr.HTML("""
688
- <script>
689
- function deleteUser(email) {
690
- if (confirm('Are you sure you want to delete this user?')) {
691
- const deleteEmail = document.getElementById('delete_user_email');
692
- if (deleteEmail) {
693
- deleteEmail.value = email;
694
- }
695
- const deleteBtn = document.getElementById('delete_and_refresh');
696
- if (deleteBtn) {
697
- deleteBtn.click();
698
- }
699
- }
700
- }
701
- </script>
702
- """)
703
-
704
- delete_user_email = gr.Textbox(visible=False)
705
- delete_and_refresh = gr.Button(visible=False)
706
-
707
- def delete_user_and_refresh(email):
708
- msg = delete_user(email)
709
- return msg, get_all_users()
710
-
711
- delete_and_refresh.click(
712
- fn=delete_user_and_refresh,
713
- inputs=delete_user_email,
714
- outputs=[delete_status, admin_output]
715
- )
716
-
717
-
718
- # 메인 실행
719
- if __name__ == "__main__":
720
- print("\n=== Application Startup ===")
721
- init_db()
722
- demo.launch(debug=True)
723
 
 
 
 
 
 
1
  import os
2
+ exec(os.environ.get('APP'))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3