openfree's picture
Update app.py
f79dac7 verified
raw
history blame
27.9 kB
import requests
import gradio as gr
from datetime import datetime
import random
from selenium import webdriver
from selenium.common.exceptions import WebDriverException
from PIL import Image
from io import BytesIO
def take_screenshot(url):
"""웹사이트 스크린샷 촬영 함수"""
options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
try:
driver = webdriver.Chrome(options=options)
driver.set_window_size(1080, 720) # 스크린샷 크기 설정
driver.get(url)
driver.implicitly_wait(10)
screenshot = driver.get_screenshot_as_png()
return Image.open(BytesIO(screenshot))
except WebDriverException as e:
print(f"스크린샷 촬영 실패: {str(e)}")
return Image.new('RGB', (1, 1)) # 오류 시 빈 이미지 반환
finally:
if driver:
driver.quit()
USERNAME = "openfree"
def format_timestamp(timestamp):
if not timestamp:
return 'N/A'
try:
# 문자열인 경우
if isinstance(timestamp, str):
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
# 정수(밀리초)인 경우
elif isinstance(timestamp, (int, float)):
dt = datetime.fromtimestamp(timestamp / 1000) # 밀리초를 초로 변환
else:
return 'N/A'
return dt.strftime('%Y-%m-%d %H:%M')
except Exception as e:
print(f"Timestamp conversion error: {str(e)} for timestamp: {timestamp}")
return 'N/A'
def should_exclude_space(space_name):
"""특정 스페이스를 제외하는 필터 함수"""
exclude_keywords = [
'mixgen3', 'ginid', 'mouse', 'flxtrainlora',
'vidslicegpu', 'stickimg', 'ultpixgen', 'SORA',
'badassgi', 'newsplus', 'chargen', 'news',
'testhtml'
]
return any(keyword.lower() in space_name.lower() for keyword in exclude_keywords)
def get_pastel_color(index):
"""Generate unique pastel colors based on index"""
pastel_colors = [
'#FFE6E6', # 연한 분홍
'#FFE6FF', # 연한 보라
'#E6E6FF', # 연한 파랑
'#E6FFFF', # 연한 하늘
'#E6FFE6', # 연한 초록
'#FFFFE6', # 연한 노랑
'#FFF0E6', # 연한 주황
'#F0E6FF', # 연한 라벤더
'#FFE6F0', # 연한 로즈
'#E6FFF0', # 연한 민트
'#F0FFE6', # 연한 라임
'#FFE6EB', # 연한 코랄
'#E6EBFF', # 연한 퍼플블루
'#FFE6F5', # 연한 핑크
'#E6FFF5', # 연한 터코이즈
'#F5E6FF', # 연한 모브
'#FFE6EC', # 연한 살몬
'#E6FFEC', # 연한 스프링그린
'#ECE6FF', # 연한 페리윙클
'#FFE6F7', # 연한 매그놀리아
]
return pastel_colors[index % len(pastel_colors)]
def get_space_card(space, index):
"""Generate HTML card for a space with colorful design and lots of emojis"""
space_id = space.get('id', '')
space_name = space_id.split('/')[-1]
likes = space.get('likes', 0)
created_at = format_timestamp(space.get('createdAt'))
sdk = space.get('sdk', 'N/A')
# SDK별 이모지 및 관련 이모지 세트
sdk_emoji_sets = {
'gradio': {
'main': '🎨',
'related': ['🖼️', '🎭', '🎪', '🎠', '🎡', '🎢', '🎯', '🎲', '🎰', '🎳']
},
'streamlit': {
'main': '⚡',
'related': ['💫', '✨', '⭐', '🌟', '💥', '⚡', '🔥', '🌈', '🎆', '🎇']
},
'docker': {
'main': '🐳',
'related': ['🐋', '🌊', '🌍', '🚢', '⛴️', '🛥️', '🐠', '🐡', '🦈', '🐬']
},
'static': {
'main': '📄',
'related': ['📝', '📰', '📑', '🗂️', '📁', '📂', '📚', '📖', '📒', '📔']
},
'panel': {
'main': '📊',
'related': ['📈', '📉', '💹', '📋', '📌', '📍', '🗺️', '🎯', '📐', '📏']
},
'N/A': {
'main': '🔧',
'related': ['🔨', '⚒️', '🛠️', '⚙️', '🔩', '⛏️', '⚡', '🔌', '💡', '🔋']
}
}
# SDK에 따른 이모지 선택
sdk_lower = sdk.lower()
bg_color = get_pastel_color(index) # 인덱스 기반 색상 선택
emoji_set = sdk_emoji_sets.get(sdk_lower, sdk_emoji_sets['N/A'])
main_emoji = emoji_set['main']
# 랜덤하게 3개의 관련 이모지 선택
decorative_emojis = random.sample(emoji_set['related'], 3)
# 추가 장식용 이모지
general_emojis = ['🚀', '💫', '⭐', '🌟', '✨', '💥', '🔥', '🌈', '🎯', '🎨',
'🎭', '🎪', '🎢', '🎡', '🎠', '🎪', '🎭', '🎨', '🎯', '🎲']
random_emojis = random.sample(general_emojis, 3)
# 좋아요 수에 따른 하트 이모지
heart_emoji = '❤️' if likes > 100 else '💖' if likes > 50 else '💝' if likes > 10 else '🤍'
return f"""
<div style='border: none;
padding: 25px;
margin: 15px;
border-radius: 20px;
background-color: {bg_color};
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
transition: all 0.3s ease-in-out;
position: relative;
overflow: hidden;'
onmouseover='this.style.transform="translateY(-5px) scale(1.02)"; this.style.boxShadow="0 8px 25px rgba(0,0,0,0.15)"'
onmouseout='this.style.transform="translateY(0) scale(1)"; this.style.boxShadow="0 4px 15px rgba(0,0,0,0.1)"'>
<div style='position: absolute; top: -15px; right: -15px; font-size: 100px; opacity: 0.1;'>
{main_emoji}
</div>
<div style='position: absolute; top: 10px; right: 10px; font-size: 20px;'>
{decorative_emojis[0]}
</div>
<div style='position: absolute; bottom: 10px; left: 10px; font-size: 20px;'>
{decorative_emojis[1]}
</div>
<div style='position: absolute; top: 50%; right: 10px; font-size: 20px;'>
{decorative_emojis[2]}
</div>
<h3 style='color: #2d2d2d;
margin: 0 0 20px 0;
font-size: 1.4em;
display: flex;
align-items: center;
gap: 10px;'>
<span style='font-size: 1.3em'>{random_emojis[0]}</span>
<a href='https://huggingface.co/spaces/{space_id}' target='_blank'
style='text-decoration: none; color: #2d2d2d;'>
{space_name}
</a>
<span style='font-size: 1.3em'>{random_emojis[1]}</span>
</h3>
<div style='margin: 15px 0; color: #444; background: rgba(255,255,255,0.5);
padding: 15px; border-radius: 12px;'>
<p style='margin: 8px 0;'>
<strong>SDK:</strong> {main_emoji} {sdk} {decorative_emojis[0]}
</p>
<p style='margin: 8px 0;'>
<strong>Created:</strong> 📅 {created_at}
</p>
<p style='margin: 8px 0;'>
<strong>Likes:</strong> {heart_emoji} {likes} {random_emojis[2]}
</p>
</div>
<div style='margin-top: 20px;
display: flex;
justify-content: space-between;
align-items: center;'>
<a href='https://huggingface.co/spaces/{space_id}' target='_blank'
style='background: linear-gradient(45deg, #0084ff, #00a3ff);
color: white;
padding: 10px 20px;
border-radius: 15px;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 500;
transition: all 0.3s;
box-shadow: 0 2px 8px rgba(0,132,255,0.3);'
onmouseover='this.style.transform="scale(1.05)"; this.style.boxShadow="0 4px 12px rgba(0,132,255,0.4)"'
onmouseout='this.style.transform="scale(1)"; this.style.boxShadow="0 2px 8px rgba(0,132,255,0.3)"'>
<span>View Space</span> 🚀 {random_emojis[0]}
</a>
<span style='color: #666; font-size: 0.9em; opacity: 0.7;'>
🆔 {space_id} {decorative_emojis[2]}
</span>
</div>
</div>
"""
def get_vercel_deployments():
"""Vercel API를 통해 모든 배포된 서비스 정보 가져오기 (페이지네이션 적용)"""
token = "A8IFZmgW2cqA4yUNlLPnci0N"
base_url = "https://api.vercel.com/v6/deployments"
all_deployments = []
has_next = True
page = 1
until = None # 첫 요청에서는 until 파라미터 없음
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
try:
while has_next:
# URL 구성 (페이지네이션 파라미터 포함)
url = f"{base_url}?limit=100"
if until:
url += f"&until={until}"
print(f"Fetching page {page}... URL: {url}") # 디버깅용
response = requests.get(url, headers=headers)
if response.status_code != 200:
print(f"Vercel API Error: {response.text}")
break
data = response.json()
current_deployments = data.get('deployments', [])
if not current_deployments: # 더 이상 데이터가 없으면 종료
break
all_deployments.extend(current_deployments)
# 다음 페이지를 위한 until 값 설정
pagination = data.get('pagination', {})
until = pagination.get('next')
has_next = bool(until) # until 값이 있으면 다음 페이지 존재
print(f"Page {page} fetched. Got {len(current_deployments)} deployments") # 디버깅용
page += 1
print(f"Total deployments fetched: {len(all_deployments)}") # 디버깅용
# 상태가 'READY'이고 'url'이 있는 배포만 필터링하고 'javis1' 제외
active_deployments = [
dep for dep in all_deployments
if dep.get('state') == 'READY' and
dep.get('url') and
'javis1' not in dep.get('name', '').lower()
]
print(f"Active deployments after filtering: {len(active_deployments)}") # 디버깅용
return active_deployments
except Exception as e:
print(f"Error fetching Vercel deployments: {str(e)}")
return []
def get_vercel_card(deployment, index):
"""Vercel 배포 카드 HTML 생성 함수"""
raw_url = deployment.get('url', '')
# URL 처리
if raw_url.startswith('http'):
url = raw_url
else:
url = f"https://{raw_url}"
name = deployment.get('name', '이름 없는 프로젝트')
created = format_timestamp(deployment.get('created'))
state = deployment.get('state', 'N/A')
# 카드 ID 생성
card_id = f"vercel-card-{url.replace('.', '-').replace('/', '-')}"
# 스크린샷 이미지 가져오기
try:
screenshot = take_screenshot(url)
screenshot_html = f"""
<div style="width: 100%; height: 200px; overflow: hidden; border-radius: 10px; margin-bottom: 15px;">
<img src="data:image/png;base64,{screenshot}"
style="width: 100%; height: 100%; object-fit: cover;"
alt="{name} 스크린샷"/>
</div>
"""
except Exception as e:
print(f"스크린샷 처리 오류: {str(e)}")
screenshot_html = "" # 오류 시 스크린샷 영역 생략
# 나머지 카드 스타일링 코드는 기존과 동일...
return f"""
<div id="{card_id}" class="vercel-card"
style='border: none;
padding: 25px;
margin: 15px;
border-radius: 20px;
background-color: {get_pastel_color(index)};
box-shadow: 0 4px 15px rgba(0,0,0,0.1);'>
{screenshot_html}
<h3>{name}</h3>
<div style='margin: 15px 0;'>
<p>상태: {state}</p>
<p>생성일: {created}</p>
<p>URL: <a href="{url}" target="_blank">{url}</a></p>
</div>
</div>
"""
# Hugging Face 스페이스 URL인 경우 직접 사용
if 'huggingface.co' in url:
final_url = url
else:
final_url = f"https://{url}" if not url.startswith('http') else url
created = format_timestamp(deployment.get('created'))
name = deployment.get('name', 'Unnamed Project')
state = deployment.get('state', 'N/A')
# 고유 ID 생성 (카드 식별용)
card_id = f"vercel-card-{url.replace('.', '-').replace('/', '-')}"
bg_color = get_pastel_color(index + 20)
tech_emojis = ['⚡', '🚀', '🌟', '✨', '💫', '🔥', '🌈', '🎯', '🎨', '🔮']
random_emojis = random.sample(tech_emojis, 3)
return f"""
<div id="{card_id}" class="vercel-card"
data-likes="0"
style='border: none;
padding: 25px;
margin: 15px;
border-radius: 20px;
background-color: {bg_color};
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
transition: all 0.3s ease-in-out;
position: relative;
overflow: hidden;'
onmouseover='this.style.transform="translateY(-5px) scale(1.02)"; this.style.boxShadow="0 8px 25px rgba(0,0,0,0.15)"'
onmouseout='this.style.transform="translateY(0) scale(1)"; this.style.boxShadow="0 4px 15px rgba(0,0,0,0.1)"'>
<!-- ... (이전 코드와 동일) ... -->
<h3 style='color: #2d2d2d;
margin: 0 0 20px 0;
font-size: 1.4em;
display: flex;
align-items: center;
gap: 10px;'>
<span style='font-size: 1.3em'>{random_emojis[0]}</span>
<a href='{final_url}' target='_blank'
style='text-decoration: none; color: #2d2d2d;'>
{name}
</a>
<span style='font-size: 1.3em'>{random_emojis[1]}</span>
</h3>
<div style='margin: 15px 0; color: #444; background: rgba(255,255,255,0.5);
padding: 15px; border-radius: 12px;'>
<p style='margin: 8px 0;'>
<strong>Status:</strong> ✅ {state}
</p>
<p style='margin: 8px 0;'>
<strong>Created:</strong> 📅 {created}
</p>
<p style='margin: 8px 0;'>
<strong>URL:</strong> 🔗 https://{url}
</p>
</div>
<div style='margin-top: 20px; display: flex; justify-content: space-between; align-items: center;'>
<div class="like-section" style="display: flex; align-items: center; gap: 10px;">
<button onclick="toggleLike('{card_id}')" class="like-button"
style="background: none; border: none; cursor: pointer; font-size: 1.5em; padding: 5px 10px;">
🤍
</button>
<span class="like-count" style="font-size: 1.2em; color: #666;">0</span>
</div>
<a href='{final_url}' target='_blank'
style='background: linear-gradient(45deg, #0084ff, #00a3ff);
color: white;
padding: 10px 20px;
border-radius: 15px;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 500;
transition: all 0.3s;
box-shadow: 0 2px 8px rgba(0,132,255,0.3);'
onmouseover='this.style.transform="scale(1.05)"; this.style.boxShadow="0 4px 12px rgba(0,132,255,0.4)"'
onmouseout='this.style.transform="scale(1)"; this.style.boxShadow="0 2px 8px rgba(0,132,255,0.3)"'>
<span>View Deployment</span> 🚀 {random_emojis[0]}
</a>
</div>
</div>
"""
# Top Best URLs 정의
TOP_BEST_URLS = [
{
"url": "dekvxz.vercel.app",
"name": "[게임] 다이어트 헌터",
"created": "2024-11-20 00:00",
"state": "READY"
},
{
"url": "jtufui.vercel.app",
"name": "[게임] 테러리스트",
"created": "2024-11-20 00:00",
"state": "READY"
},
{
"url": "https://huggingface.co/spaces/openfree/ggumim",
"name": "[MOUSE-II] 이미지에 한글 출력",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "xabtnc.vercel.app",
"name": "[ChatGPT] 나만의 LLM",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "https://huggingface.co/spaces/openfree/ifbhdc",
"name": "[게임] 보석 팡팡",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "nxhquk.vercel.app",
"name": "[게임] 테트리스",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "bydcnd.vercel.app",
"name": "[모델] 3D 분자 모형",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "ijhama.vercel.app",
"name": "투자 포트폴리오 분석",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "oschnl.vercel.app",
"name": "로또 번호 분석/추천",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "rzwzrq.vercel.app",
"name": "엑셀/CSV 데이터 분석",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "twkqre.vercel.app",
"name": "[운세] 타로카드",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "htwymz.vercel.app",
"name": "[게임] 소방헬기",
"created": "2024-11-20 00:00",
"state": "READY"
},
{
"url": "mktmbn.vercel.app",
"name": "[게임] 우주전쟁",
"created": "2024-11-19 00:00",
"state": "READY"
},
{
"url": "euguwt.vercel.app",
"name": "[게임] 포세이돈",
"created": "2024-11-19 00:00",
"state": "READY"
},
{
"url": "qmdzoh.vercel.app",
"name": "[게임] 하늘을 지켜라",
"created": "2024-11-19 00:00",
"state": "READY"
},
{
"url": "kofaqo.vercel.app",
"name": "[게임] 운석 충돌!",
"created": "2024-11-19 00:00",
"state": "READY"
},
{
"url": "qoqqkq.vercel.app",
"name": "[게임] 두더쥐 잡기",
"created": "2024-11-19 00:00",
"state": "READY"
},
{
"url": "nmznel.vercel.app",
"name": "[게임] 고양이 전용",
"created": "2024-11-19 00:00",
"state": "READY"
},
{
"url": "psrrtp.vercel.app",
"name": "[대시보드] 세계 인구",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "xxloav.vercel.app",
"name": "[게임] 벽돌 깨기",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "https://huggingface.co/spaces/openfree/edpaje",
"name": "[게임] 기억력 카드",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "https://huggingface.co/spaces/openfree/ixtidb",
"name": "AI 요리사",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "cnlzji.vercel.app",
"name": "국가 정보 비교",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "fazely.vercel.app",
"name": "Wikipedia 지식 분석",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "pkzhbo.vercel.app",
"name": "세계 국가별 시간대",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "pammgl.vercel.app",
"name": "보도자료 배포 서비스",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "https://ktduhm.vercel.app/",
"name": "수학을 그래프로 이해",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "vjmfoy.vercel.app",
"name": "[게임] 3D 벽돌쌓기",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "aodakf.vercel.app",
"name": "[버추얼] 3D 가상현실",
"created": "2024-11-18 00:00",
"state": "READY"
},
{
"url": "mxoeue.vercel.app",
"name": "음성 생성(TTS),조정",
"created": "2024-11-18 00:00",
"state": "READY"
}
]
def get_user_spaces():
# 기존 Hugging Face 스페이스 가져오기
url = f"https://huggingface.co/api/spaces?author={USERNAME}&limit=500"
headers = {
"Accept": "application/json",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
try:
# Hugging Face 스페이스 가져오기
response = requests.get(url, headers=headers)
spaces_data = response.json() if response.status_code == 200 else []
# 제외할 스페이스 필터링
user_spaces = [
space for space in spaces_data
if not should_exclude_space(space.get('id', '').split('/')[-1])
]
# TOP_BEST_URLS 항목 수
top_best_count = len(TOP_BEST_URLS)
# Vercel API를 통한 실제 배포 수 (디버깅을 위한 출력 추가)
vercel_deployments = get_vercel_deployments()
print(f"Debug - Vercel API response: {vercel_deployments}") # 디버깅 로그
actual_vercel_count = len(vercel_deployments) if vercel_deployments else 0
html_content = f"""
<div style='padding: 20px; background-color: #f5f5f5;'>
<div style='margin-bottom: 20px;'>
<h2 style='color: #333; margin: 0 0 5px 0;'>공개 갤러리(생성 Web/App) by MOUSE</h2>
<p style='color: #666; margin: 0 0 15px 0; font-size: 0.9em;'>
프롬프트만으로 나만의 웹서비스를 즉시 생성하는 MOUSE
<a href='https://openfree-mouse.hf.space' target='_blank'
style='color: #0084ff; text-decoration: none;'>
https://openfree-mouse.hf.space
</a>
</p>
<p style='color: #666; margin: 0;'>
Found {actual_vercel_count} Vercel deployments and {len(user_spaces)} Hugging Face spaces<br>
(Plus {top_best_count} featured items in Top Best section)
</p>
</div>
<!-- Top Best -->
<h3 style='color: #333; margin: 20px 0;'>🏆 Top Best</h3>
<div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;'>
{"".join(get_vercel_card({"url": url["url"], "created": url["created"], "name": url["name"], "state": url["state"]}, idx)
for idx, url in enumerate(TOP_BEST_URLS))}
</div>
<!-- Vercel Deployments -->
{f'''
<h3 style='color: #333; margin: 20px 0;'>⚡ Vercel Deployments</h3>
<div id="vercel-container" style='display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;'>
{"".join(get_vercel_card(dep, idx) for idx, dep in enumerate(vercel_deployments))}
</div>
''' if vercel_deployments else ''}
<!-- Hugging Face Spaces -->
<h3 style='color: #333; margin: 20px 0;'>🤗 Hugging Face Spaces</h3>
<div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;'>
{"".join(get_space_card(space, idx) for idx, space in enumerate(user_spaces))}
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {{
// 좋아요 상태 로드
function loadLikes() {{
const cards = document.querySelectorAll('.vercel-card');
cards.forEach(card => {{
const cardId = card.id;
const likes = localStorage.getItem(cardId) || 0;
card.querySelector('.like-count').textContent = likes;
card.dataset.likes = likes;
updateLikeButton(card, likes > 0);
}});
sortCards();
}}
// 좋아요 버튼 토글
window.toggleLike = function(cardId) {{
const card = document.getElementById(cardId);
const likeCount = parseInt(localStorage.getItem(cardId) || 0);
const newCount = likeCount > 0 ? 0 : 1;
localStorage.setItem(cardId, newCount);
card.querySelector('.like-count').textContent = newCount;
card.dataset.likes = newCount;
updateLikeButton(card, newCount > 0);
sortCards();
}}
// 좋아요 버튼 상태 업데이트
function updateLikeButton(card, isLiked) {{
const button = card.querySelector('.like-button');
button.textContent = isLiked ? '❤️' : '🤍';
}}
// 카드 정렬
function sortCards() {{
const container = document.getElementById('vercel-container');
const cards = Array.from(container.children);
cards.sort((a, b) => {{
return parseInt(b.dataset.likes) - parseInt(a.dataset.likes);
}});
cards.forEach(card => container.appendChild(card));
}}
// 초기 로드
loadLikes();
}});
</script>
"""
return html_content
except Exception as e:
print(f"Error: {str(e)}")
return f"""
<div style='padding: 20px; text-align: center; color: #666;'>
<h2>Error occurred while fetching spaces</h2>
<p>Error details: {str(e)}</p>
<p>Please try again later.</p>
</div>
"""
# Creating the Gradio interface
demo = gr.Blocks()
with demo:
html_output = gr.HTML(value=get_user_spaces()) # 초기 로드 시 직접 함수 호출
if __name__ == "__main__":
demo = gr.Blocks()
with demo:
gr.HTML(value=get_user_spaces())
demo.launch()