openfree commited on
Commit
db22c56
β€’
1 Parent(s): 2f1c222

Create app-backup1.py

Browse files
Files changed (1) hide show
  1. app-backup1.py +1719 -0
app-backup1.py ADDED
@@ -0,0 +1,1719 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import random
3
+ import base64
4
+ import requests
5
+ from selenium import webdriver
6
+ from selenium.webdriver.support.ui import WebDriverWait
7
+ from selenium.webdriver.support import expected_conditions as EC
8
+ from selenium.webdriver.common.by import By
9
+ from selenium.common.exceptions import WebDriverException, TimeoutException
10
+ from PIL import Image
11
+ from io import BytesIO
12
+ from datetime import datetime
13
+ import gradio as gr
14
+ from typing import Tuple
15
+ import time
16
+ from pathlib import Path # μΆ”κ°€
17
+
18
+ # μŠ€ν¬λ¦°μƒ· μΊμ‹œ 디렉토리 μ„€μ •
19
+ CACHE_DIR = Path("screenshot_cache")
20
+ CACHE_DIR.mkdir(exist_ok=True)
21
+
22
+ # μ „μ—­ λ³€μˆ˜λ‘œ μŠ€ν¬λ¦°μƒ· μΊμ‹œ μ„ μ–Έ
23
+ SCREENSHOT_CACHE = {}
24
+
25
+ def get_cached_screenshot(url: str) -> str:
26
+ """μΊμ‹œλœ μŠ€ν¬λ¦°μƒ· κ°€μ Έμ˜€κΈ° λ˜λŠ” μƒˆλ‘œ 생성"""
27
+ cache_file = CACHE_DIR / f"{base64.b64encode(url.encode()).decode()}.png"
28
+
29
+ if cache_file.exists():
30
+ with open(cache_file, "rb") as f:
31
+ return base64.b64encode(f.read()).decode()
32
+
33
+ return take_screenshot(url)
34
+
35
+ def take_screenshot(url):
36
+ """μ›Ήμ‚¬μ΄νŠΈ μŠ€ν¬λ¦°μƒ· 촬영 ν•¨μˆ˜ (λ‘œλ”© λŒ€κΈ° μ‹œκ°„ μΆ”κ°€)"""
37
+ if url in SCREENSHOT_CACHE:
38
+ return SCREENSHOT_CACHE[url]
39
+
40
+ if not url.startswith('http'):
41
+ url = f"https://{url}"
42
+
43
+ options = webdriver.ChromeOptions()
44
+ options.add_argument('--headless')
45
+ options.add_argument('--no-sandbox')
46
+ options.add_argument('--disable-dev-shm-usage')
47
+ options.add_argument('--window-size=1080,720')
48
+
49
+ try:
50
+ driver = webdriver.Chrome(options=options)
51
+ driver.get(url)
52
+
53
+ # λͺ…μ‹œμ  λŒ€κΈ°: body μš”μ†Œκ°€ λ‘œλ“œλ  λ•ŒκΉŒμ§€ λŒ€κΈ° (μ΅œλŒ€ 10초)
54
+ try:
55
+ WebDriverWait(driver, 10).until(
56
+ EC.presence_of_element_located((By.TAG_NAME, "body"))
57
+ )
58
+ except TimeoutException:
59
+ print(f"νŽ˜μ΄μ§€ λ‘œλ”© νƒ€μž„μ•„μ›ƒ: {url}")
60
+
61
+ # μΆ”κ°€ λŒ€κΈ° μ‹œκ°„μ„ 2초둜 증가
62
+ time.sleep(2) # 1μ΄ˆμ—μ„œ 2초둜 λ³€κ²½
63
+
64
+ # JavaScript μ‹€ν–‰ μ™„λ£Œ λŒ€κΈ°
65
+ driver.execute_script("return document.readyState") == "complete"
66
+
67
+ # μŠ€ν¬λ¦°μƒ· 촬영
68
+ screenshot = driver.get_screenshot_as_png()
69
+ img = Image.open(BytesIO(screenshot))
70
+ buffered = BytesIO()
71
+ img.save(buffered, format="PNG")
72
+ base64_image = base64.b64encode(buffered.getvalue()).decode()
73
+
74
+ # μΊμ‹œμ— μ €μž₯
75
+ SCREENSHOT_CACHE[url] = base64_image
76
+ return base64_image
77
+
78
+ except WebDriverException as e:
79
+ print(f"μŠ€ν¬λ¦°μƒ· 촬영 μ‹€νŒ¨: {str(e)} for URL: {url}")
80
+ return None
81
+ except Exception as e:
82
+ print(f"μ˜ˆμƒμΉ˜ λͺ»ν•œ 였λ₯˜: {str(e)} for URL: {url}")
83
+ return None
84
+ finally:
85
+ if 'driver' in locals():
86
+ driver.quit()
87
+
88
+ from datetime import datetime, timedelta
89
+
90
+ def calculate_rising_rate(created_date: str, rank: int) -> int:
91
+ """AI Rising Rate 계산"""
92
+ # 생성일 κΈ°μ€€ 점수 계산
93
+ created = datetime.strptime(created_date.split('T')[0], '%Y-%m-%d')
94
+ today = datetime.now()
95
+ days_diff = (today - created).days
96
+ date_score = max(0, 300 - days_diff) # μ΅œλŒ€ 300점
97
+
98
+ # μˆœμœ„ κΈ°μ€€ 점수 계산
99
+ rank_score = max(0, 300 - rank) # μ΅œλŒ€ 300점
100
+
101
+ # 총점 계산
102
+ total_score = date_score + rank_score
103
+
104
+ # 별 개수 계산 (0~5)
105
+ if total_score <= 100:
106
+ stars = 1
107
+ elif total_score <= 200:
108
+ stars = 2
109
+ elif total_score <= 300:
110
+ stars = 3
111
+ elif total_score <= 400:
112
+ stars = 4
113
+ else:
114
+ stars = 5
115
+
116
+ return stars
117
+
118
+ def get_popularity_grade(likes: int, stars: int) -> tuple:
119
+ """AI Popularity Score λ“±κΈ‰ 계산"""
120
+ # 기본 점수 (likes)
121
+ base_score = min(likes, 10000) # μ΅œλŒ€ 10000점
122
+
123
+ # 별점 μΆ”κ°€ 점수 (별 ν•˜λ‚˜λ‹Ή 500점)
124
+ star_score = stars * 500
125
+
126
+ # 총점
127
+ total_score = base_score + star_score
128
+
129
+ # λ“±κΈ‰ ν…Œμ΄λΈ” (18단계)
130
+ grades = [
131
+ (9000, "AAA+"), (8500, "AAA"), (8000, "AAA-"),
132
+ (7500, "AA+"), (7000, "AA"), (6500, "AA-"),
133
+ (6000, "A+"), (5500, "A"), (5000, "A-"),
134
+ (4500, "BBB+"), (4000, "BBB"), (3500, "BBB-"),
135
+ (3000, "BB+"), (2500, "BB"), (2000, "BB-"),
136
+ (1500, "B+"), (1000, "B"), (500, "B-")
137
+ ]
138
+
139
+ for threshold, grade in grades:
140
+ if total_score >= threshold:
141
+ return grade, total_score
142
+
143
+ return "B-", total_score
144
+
145
+ # get_card ν•¨μˆ˜ λ‚΄μ˜ hardware_info 뢀뢄을 λ‹€μŒμœΌλ‘œ ꡐ체:
146
+ def get_rating_info(item: dict, index: int) -> str:
147
+ """평가 정보 HTML 생성"""
148
+ created = item.get('createdAt', '').split('T')[0]
149
+ likes = int(str(item.get('likes', '0')).replace(',', ''))
150
+
151
+ # AI Rising Rate 계산
152
+ stars = calculate_rising_rate(created, index + 1)
153
+ star_html = "β˜…" * stars + "β˜†" * (5 - stars) # μ±„μ›Œμ§„ 별과 빈 별 μ‘°ν•©
154
+
155
+ # AI Popularity Score 계산
156
+ grade, score = get_popularity_grade(likes, stars)
157
+
158
+ # 등급별 색상 μ„€μ •
159
+ grade_colors = {
160
+ 'AAA': '#FFD700', 'AA': '#FFA500', 'A': '#FF4500',
161
+ 'BBB': '#4169E1', 'BB': '#1E90FF', 'B': '#00BFFF'
162
+ }
163
+ grade_base = grade.rstrip('+-')
164
+ grade_color = grade_colors.get(grade_base, '#666666')
165
+
166
+ return f"""
167
+ <div style='
168
+ margin-top: 15px;
169
+ padding: 15px;
170
+ background: rgba(255,255,255,0.4);
171
+ border-radius: 10px;
172
+ font-size: 0.9em;
173
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);'>
174
+ <div style='
175
+ display: grid;
176
+ grid-template-columns: repeat(2, 1fr);
177
+ gap: 15px;'>
178
+ <div style='
179
+ color: #333;
180
+ display: flex;
181
+ flex-direction: column;
182
+ gap: 5px;'>
183
+ <span style='font-weight: bold;'>AI Rising Rate:</span>
184
+ <span style='
185
+ color: #FF8C00;
186
+ font-size: 1.4em;
187
+ letter-spacing: 2px;
188
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.1);'>{star_html}</span>
189
+ </div>
190
+ <div style='
191
+ color: #333;
192
+ display: flex;
193
+ flex-direction: column;
194
+ gap: 5px;'>
195
+ <span style='font-weight: bold;'>AI Popularity Score:</span>
196
+ <span style='
197
+ font-size: 1.2em;
198
+ font-weight: bold;
199
+ color: {grade_color};
200
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.1);'>{grade} ({score:,})</span>
201
+ </div>
202
+ </div>
203
+ </div>
204
+ """
205
+
206
+ def get_hardware_info(item: dict) -> tuple:
207
+ """ν•˜λ“œμ›¨μ–΄ 정보 μΆ”μΆœ"""
208
+ try:
209
+ # runtime 정보 확인
210
+ runtime = item.get('runtime', {})
211
+
212
+ # CPU 정보 처리
213
+ cpu_info = runtime.get('cpu', 'Standard')
214
+
215
+ # GPU 정보 처리
216
+ gpu_info = "None"
217
+ if runtime.get('accelerator') == "gpu":
218
+ gpu_type = runtime.get('gpu', {}).get('name', '')
219
+ gpu_memory = runtime.get('gpu', {}).get('memory', '')
220
+ if gpu_type:
221
+ gpu_info = f"{gpu_type}"
222
+ if gpu_memory:
223
+ gpu_info += f" ({gpu_memory}GB)"
224
+
225
+ # spaces decorator 확인
226
+ if '@spaces.GPU' in str(item.get('sdk_version', '')):
227
+ if gpu_info == "None":
228
+ gpu_info = "GPU Enabled"
229
+
230
+ # SDK 정보 처리
231
+ sdk = item.get('sdk', 'N/A')
232
+
233
+ print(f"Debug - Runtime Info: {runtime}") # 디버그 좜λ ₯
234
+ print(f"Debug - GPU Info: {gpu_info}") # 디버그 좜λ ₯
235
+
236
+ return cpu_info, gpu_info, sdk
237
+
238
+ except Exception as e:
239
+ print(f"Error parsing hardware info: {str(e)}")
240
+ return 'Standard', 'None', 'N/A'
241
+
242
+ def get_card(item: dict, index: int, card_type: str = "space") -> str:
243
+ """톡합 μΉ΄λ“œ HTML 생성"""
244
+ item_id = item.get('id', '')
245
+ author, title = item_id.split('/', 1)
246
+ likes = format(item.get('likes', 0), ',')
247
+ created = item.get('createdAt', '').split('T')[0]
248
+
249
+ # short_description κ°€μ Έμ˜€κΈ°
250
+ short_description = item.get('cardData', {}).get('short_description', '')
251
+
252
+ # titleκ³Ό short_description을 ν¬ν•¨ν•œ 헀더 HTML
253
+ title_html = f"""
254
+ <h3 style='
255
+ margin: 0 0 15px 0;
256
+ color: #333;
257
+ font-size: 1.3em;
258
+ line-height: 1.4;
259
+ display: -webkit-box;
260
+ -webkit-line-clamp: 2;
261
+ -webkit-box-orient: vertical;
262
+ overflow: hidden;
263
+ text-overflow: ellipsis;
264
+ text-shadow: 1px 1px 1px rgba(255,255,255,0.8);'>
265
+ {title}
266
+ {f'<span style="display: block; font-size: 0.7em; color: #666; margin-top: 5px; font-weight: normal; font-style: italic;">{short_description}</span>' if short_description else ''}
267
+ </h3>
268
+ """
269
+
270
+
271
+ # URL μ •μ˜
272
+ if card_type == "space":
273
+ url = f"https://huggingface.co/spaces/{item_id}"
274
+ elif card_type == "model":
275
+ url = f"https://huggingface.co/{item_id}"
276
+ else: # dataset
277
+ url = f"https://huggingface.co/datasets/{item_id}"
278
+
279
+ # 메타데이터 처리
280
+ tags = item.get('tags', [])
281
+ pipeline_tag = item.get('pipeline_tag', '')
282
+ license = item.get('license', '')
283
+ sdk = item.get('sdk', 'N/A')
284
+
285
+ # AI Rating 정보 κ°€μ Έμ˜€κΈ°
286
+ rating_info = get_rating_info(item, index)
287
+
288
+ # μΉ΄λ“œ νƒ€μž…λ³„ κ·ΈλΌλ°μ΄μ…˜ μ„€μ •
289
+ if card_type == "space":
290
+ gradient_colors = """
291
+ rgba(255, 182, 193, 0.7), /* νŒŒμŠ€ν…” 핑크 */
292
+ rgba(173, 216, 230, 0.7), /* νŒŒμŠ€ν…” 블루 */
293
+ rgba(255, 218, 185, 0.7) /* νŒŒμŠ€ν…” ν”ΌμΉ˜ */
294
+ """
295
+ bg_content = f"""
296
+ background-image: url(data:image/png;base64,{get_cached_screenshot(url) if get_cached_screenshot(url) else ''});
297
+ background-size: cover;
298
+ background-position: center;
299
+ """
300
+ type_icon = "🎯"
301
+ type_label = "SPACE"
302
+ elif card_type == "model":
303
+ gradient_colors = """
304
+ rgba(110, 142, 251, 0.7), /* λͺ¨λΈ 블루 */
305
+ rgba(130, 158, 251, 0.7),
306
+ rgba(150, 174, 251, 0.7)
307
+ """
308
+ bg_content = f"""
309
+ background: linear-gradient(135deg, #6e8efb, #4a6cf7);
310
+ padding: 15px;
311
+ """
312
+ type_icon = "πŸ€–"
313
+ type_label = "MODEL"
314
+ else: # dataset
315
+ gradient_colors = """
316
+ rgba(255, 107, 107, 0.7), /* 데이터셋 λ ˆλ“œ */
317
+ rgba(255, 127, 127, 0.7),
318
+ rgba(255, 147, 147, 0.7)
319
+ """
320
+ bg_content = f"""
321
+ background: linear-gradient(135deg, #ff6b6b, #ff8787);
322
+ padding: 15px;
323
+ """
324
+ type_icon = "πŸ“Š"
325
+ type_label = "DATASET"
326
+
327
+ content_bg = f"""
328
+ background: linear-gradient(135deg, {gradient_colors});
329
+ backdrop-filter: blur(10px);
330
+ """
331
+
332
+
333
+ # νƒœκ·Έ ν‘œμ‹œ (models와 datasets용)
334
+ tags_html = ""
335
+ if card_type != "space":
336
+ tags_html = f"""
337
+ <div style='
338
+ position: absolute;
339
+ top: 50%;
340
+ left: 50%;
341
+ transform: translate(-50%, -50%);
342
+ display: flex;
343
+ flex-wrap: wrap;
344
+ gap: 5px;
345
+ justify-content: center;
346
+ width: 90%;'>
347
+ {' '.join([f'''
348
+ <span style='
349
+ background: rgba(255,255,255,0.2);
350
+ padding: 5px 10px;
351
+ border-radius: 15px;
352
+ color: white;
353
+ font-size: 0.8em;'>
354
+ #{tag}
355
+ </span>
356
+ ''' for tag in tags[:5]])}
357
+ </div>
358
+ """
359
+
360
+ # μΉ΄λ“œ HTML λ°˜ν™˜
361
+ return f"""
362
+ <div class="card" style='
363
+ position: relative;
364
+ border: none;
365
+ padding: 0;
366
+ margin: 10px;
367
+ border-radius: 20px;
368
+ box-shadow: 0 10px 20px rgba(0,0,0,0.1);
369
+ background: white;
370
+ transition: all 0.3s ease;
371
+ overflow: hidden;
372
+ min-height: 400px;
373
+ cursor: pointer;
374
+ transform-origin: center;'
375
+ onmouseover="this.style.transform='scale(0.98) translateY(5px)'; this.style.boxShadow='0 5px 15px rgba(0,0,0,0.2)';"
376
+ onmouseout="this.style.transform='scale(1) translateY(0)'; this.style.boxShadow='0 10px 20px rgba(0,0,0,0.1)';"
377
+ onclick="window.open('{url}', '_blank')">
378
+
379
+ <!-- 상단 μ˜μ—­ -->
380
+ <div style='
381
+ width: 100%;
382
+ height: 200px;
383
+ {bg_content}
384
+ position: relative;'>
385
+
386
+ <!-- μˆœμœ„ 뱃지 -->
387
+ <div style='
388
+ position: absolute;
389
+ top: 10px;
390
+ left: 10px;
391
+ background: rgba(0,0,0,0.7);
392
+ color: white;
393
+ padding: 5px 15px;
394
+ border-radius: 20px;
395
+ font-weight: bold;
396
+ font-size: 0.9em;
397
+ backdrop-filter: blur(5px);'>
398
+ #{index + 1}
399
+ </div>
400
+
401
+ <!-- νƒ€μž… 뱃지 -->
402
+ <div style='
403
+ position: absolute;
404
+ top: 10px;
405
+ right: 10px;
406
+ background: rgba(255,255,255,0.9);
407
+ padding: 5px 15px;
408
+ border-radius: 20px;
409
+ font-weight: bold;
410
+ font-size: 0.8em;'>
411
+ {type_icon} {type_label}
412
+ </div>
413
+
414
+ {tags_html}
415
+ </div>
416
+
417
+ <!-- μ½˜ν…μΈ  μ˜μ—­ -->
418
+ <div style='
419
+ padding: 20px;
420
+ {content_bg}
421
+ border-radius: 0 0 20px 20px;
422
+ border-top: 1px solid rgba(255,255,255,0.5);'>
423
+ <h3 style='
424
+ margin: 0 0 15px 0;
425
+ color: #333;
426
+ font-size: 1.3em;
427
+ line-height: 1.4;
428
+ display: -webkit-box;
429
+ -webkit-line-clamp: 2;
430
+ -webkit-box-orient: vertical;
431
+ overflow: hidden;
432
+ text-overflow: ellipsis;
433
+ text-shadow: 1px 1px 1px rgba(255,255,255,0.8);'>
434
+ {title}
435
+ </h3>
436
+
437
+ <div style='
438
+ display: grid;
439
+ grid-template-columns: repeat(2, 1fr);
440
+ gap: 10px;
441
+ font-size: 0.9em;
442
+ background: rgba(255,255,255,0.3);
443
+ padding: 10px;
444
+ border-radius: 10px;'>
445
+ <div style='color: #444;'>
446
+ <span style='margin-right: 5px;'>πŸ‘€</span> {author}
447
+ </div>
448
+ <div style='color: #444;'>
449
+ <span style='margin-right: 5px;'>❀️</span> {likes}
450
+ </div>
451
+ <div style='color: #444; grid-column: span 2;'>
452
+ <span style='margin-right: 5px;'>πŸ“…</span> {created}
453
+ </div>
454
+ </div>
455
+
456
+ {rating_info}
457
+ </div>
458
+ </div>
459
+ """
460
+
461
+ def get_trending_spaces(search_query="", sort_by="rank", progress=gr.Progress()) -> Tuple[str, str]:
462
+ """νŠΈλ Œλ”© 슀페이슀 κ°€μ Έμ˜€κΈ°"""
463
+ url = "https://huggingface.co/api/spaces"
464
+
465
+ try:
466
+ progress(0, desc="Fetching spaces data...")
467
+ params = {
468
+ 'full': 'true',
469
+ 'limit': 100
470
+ }
471
+
472
+ response = requests.get(url, params=params)
473
+ response.raise_for_status()
474
+ spaces = response.json()
475
+
476
+ # κ²€μƒ‰μ–΄λ‘œ 필터링
477
+ if search_query:
478
+ spaces = [space for space in spaces if search_query.lower() in
479
+ (space.get('id', '') + ' ' + space.get('title', '')).lower()]
480
+
481
+ # μ •λ ¬
482
+ sort_by = sort_by.lower()
483
+ if sort_by == "rising_rate":
484
+ spaces.sort(key=lambda x: calculate_rising_rate(x.get('createdAt', ''), 0), reverse=True)
485
+ elif sort_by == "popularity":
486
+ spaces.sort(key=lambda x: get_popularity_grade(
487
+ int(str(x.get('likes', '0')).replace(',', '')),
488
+ calculate_rising_rate(x.get('createdAt', ''), 0))[1],
489
+ reverse=True)
490
+
491
+ progress(0.1, desc="Creating gallery...")
492
+ html_content = """
493
+ <div style='padding: 20px; background: #f5f5f5;'>
494
+ <div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;'>
495
+ """
496
+
497
+ for idx, space in enumerate(spaces):
498
+ html_content += get_card(space, idx, "space")
499
+ progress((0.1 + 0.9 * idx/len(spaces)), desc=f"Loading space {idx+1}/{len(spaces)}...")
500
+
501
+ html_content += "</div></div>"
502
+
503
+ progress(1.0, desc="Complete!")
504
+ return html_content, f"Found {len(spaces)} spaces"
505
+
506
+ except Exception as e:
507
+ error_html = f'<div style="color: red; padding: 20px;">Error: {str(e)}</div>'
508
+ return error_html, f"Error: {str(e)}"
509
+
510
+ def get_models(search_query="", sort_by="rank", progress=gr.Progress()) -> Tuple[str, str]:
511
+ """인기 λͺ¨λΈ κ°€μ Έμ˜€κΈ°"""
512
+ url = "https://huggingface.co/api/models"
513
+
514
+ try:
515
+ progress(0, desc="Fetching models data...")
516
+ params = {
517
+ 'full': 'true',
518
+ 'limit': 300
519
+ }
520
+ response = requests.get(url, params=params)
521
+ response.raise_for_status()
522
+ models = response.json()
523
+
524
+ # κ²€μƒ‰μ–΄λ‘œ 필터링
525
+ if search_query:
526
+ models = [model for model in models if search_query.lower() in
527
+ (model.get('id', '') + ' ' + model.get('title', '')).lower()]
528
+
529
+ # μ •λ ¬
530
+ sort_by = sort_by.lower()
531
+ if sort_by == "rising_rate":
532
+ models.sort(key=lambda x: calculate_rising_rate(x.get('createdAt', ''), 0), reverse=True)
533
+ elif sort_by == "popularity":
534
+ models.sort(key=lambda x: get_popularity_grade(
535
+ int(str(x.get('likes', '0')).replace(',', '')),
536
+ calculate_rising_rate(x.get('createdAt', ''), 0))[1],
537
+ reverse=True)
538
+
539
+ progress(0.1, desc="Creating gallery...")
540
+ html_content = """
541
+ <div style='padding: 20px; background: #f5f5f5;'>
542
+ <div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;'>
543
+ """
544
+
545
+ for idx, model in enumerate(models):
546
+ html_content += get_card(model, idx, "model")
547
+ progress((0.1 + 0.9 * idx/len(models)), desc=f"Loading model {idx+1}/{len(models)}...")
548
+
549
+ html_content += "</div></div>"
550
+
551
+ progress(1.0, desc="Complete!")
552
+ return html_content, f"Found {len(models)} models"
553
+
554
+ except Exception as e:
555
+ error_html = f'<div style="color: red; padding: 20px;">Error: {str(e)}</div>'
556
+ return error_html, f"Error: {str(e)}"
557
+
558
+ def get_datasets(search_query="", sort_by="rank", progress=gr.Progress()) -> Tuple[str, str]:
559
+ """인기 데이터셋 κ°€μ Έμ˜€κΈ°"""
560
+ url = "https://huggingface.co/api/datasets"
561
+
562
+ try:
563
+ progress(0, desc="Fetching datasets data...")
564
+ params = {
565
+ 'full': 'true',
566
+ 'limit': 300
567
+ }
568
+ response = requests.get(url, params=params)
569
+ response.raise_for_status()
570
+ datasets = response.json()
571
+
572
+ # κ²€μƒ‰μ–΄λ‘œ 필터링
573
+ if search_query:
574
+ datasets = [dataset for dataset in datasets if search_query.lower() in
575
+ (dataset.get('id', '') + ' ' + dataset.get('title', '')).lower()]
576
+
577
+ # μ •λ ¬
578
+ sort_by = sort_by.lower()
579
+ if sort_by == "rising_rate":
580
+ datasets.sort(key=lambda x: calculate_rising_rate(x.get('createdAt', ''), 0), reverse=True)
581
+ elif sort_by == "popularity":
582
+ datasets.sort(key=lambda x: get_popularity_grade(
583
+ int(str(x.get('likes', '0')).replace(',', '')),
584
+ calculate_rising_rate(x.get('createdAt', ''), 0))[1],
585
+ reverse=True)
586
+
587
+ progress(0.1, desc="Creating gallery...")
588
+ html_content = """
589
+ <div style='padding: 20px; background: #f5f5f5;'>
590
+ <div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;'>
591
+ """
592
+
593
+ for idx, dataset in enumerate(datasets):
594
+ html_content += get_card(dataset, idx, "dataset")
595
+ progress((0.1 + 0.9 * idx/len(datasets)), desc=f"Loading dataset {idx+1}/{len(datasets)}...")
596
+
597
+ html_content += "</div></div>"
598
+
599
+ progress(1.0, desc="Complete!")
600
+ return html_content, f"Found {len(datasets)} datasets"
601
+
602
+ except Exception as e:
603
+ error_html = f'<div style="color: red; padding: 20px;">Error: {str(e)}</div>'
604
+ return error_html, f"Error: {str(e)}"
605
+
606
+ # μ •λ ¬ ν•¨μˆ˜ μΆ”κ°€
607
+ def sort_items(items, sort_by):
608
+ if sort_by == "rank":
609
+ return items # 이미 μˆœμœ„λŒ€λ‘œ μ •λ ¬λ˜μ–΄ 있음
610
+ elif sort_by == "rising_rate":
611
+ return sorted(items, key=lambda x: calculate_rising_rate(x.get('createdAt', ''), 0), reverse=True)
612
+ elif sort_by == "popularity":
613
+ return sorted(items, key=lambda x: get_popularity_grade(int(str(x.get('likes', '0')).replace(',', '')),
614
+ calculate_rising_rate(x.get('createdAt', ''), 0))[1], reverse=True)
615
+ return items
616
+
617
+ # API 호좜 ν•¨μˆ˜ μˆ˜μ •
618
+ def fetch_items(item_type, search_query="", sort_by="rank", limit=1000):
619
+ """μ•„μ΄ν…œ κ°€μ Έμ˜€κΈ° (spaces/models/datasets)"""
620
+ base_url = f"https://huggingface.co/api/{item_type}"
621
+ params = {
622
+ 'full': 'true',
623
+ 'limit': limit,
624
+ 'search': search_query
625
+ }
626
+
627
+ try:
628
+ response = requests.get(base_url, params=params)
629
+ response.raise_for_status()
630
+ items = response.json()
631
+
632
+ # κ²€μƒ‰μ–΄λ‘œ 필터링
633
+ if search_query:
634
+ items = [item for item in items if search_query.lower() in
635
+ (item.get('id', '') + item.get('title', '')).lower()]
636
+
637
+ # μ •λ ¬
638
+ items = sort_items(items, sort_by)
639
+
640
+ return items[:300] # μƒμœ„ 300개만 λ°˜ν™˜
641
+ except Exception as e:
642
+ print(f"Error fetching items: {e}")
643
+ return []
644
+
645
+ def create_interface():
646
+ with gr.Blocks(title="HuggingFace Trending Board", css="""
647
+ .search-sort-container {
648
+ background: linear-gradient(135deg, rgba(255,255,255,0.95), rgba(240,240,255,0.95));
649
+ border-radius: 15px;
650
+ padding: 20px;
651
+ margin: 10px 0;
652
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
653
+ overflow: visible;
654
+ }
655
+ .search-box {
656
+ border: 2px solid #e1e1e1;
657
+ border-radius: 10px;
658
+ padding: 12px;
659
+ transition: all 0.3s ease;
660
+ background: linear-gradient(135deg, #ffffff, #f8f9ff);
661
+ width: 100%;
662
+ }
663
+ .search-box:focus {
664
+ border-color: #7b61ff;
665
+ box-shadow: 0 0 0 2px rgba(123,97,255,0.2);
666
+ background: linear-gradient(135deg, #ffffff, #f0f3ff);
667
+ }
668
+ .refresh-btn {
669
+ background: linear-gradient(135deg, #7b61ff, #6366f1);
670
+ color: white;
671
+ border: none;
672
+ padding: 10px 20px;
673
+ border-radius: 10px;
674
+ cursor: pointer;
675
+ transition: all 0.3s ease;
676
+ width: 120px;
677
+ height: 80px !important;
678
+ display: flex;
679
+ align-items: center;
680
+ justify-content: center;
681
+ margin-left: auto;
682
+ font-size: 1.2em !important;
683
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
684
+ }
685
+ .refresh-btn:hover {
686
+ transform: translateY(-2px);
687
+ box-shadow: 0 6px 12px rgba(0,0,0,0.2);
688
+ background: linear-gradient(135deg, #8b71ff, #7376f1);
689
+ }
690
+ """) as interface:
691
+
692
+ gr.Markdown("""
693
+ # πŸ€— HuggingFace Trending TOP 300 Board
694
+ <div style='margin-bottom: 20px; padding: 10px; background: linear-gradient(135deg, rgba(123,97,255,0.1), rgba(99,102,241,0.1)); border-radius: 10px;'>
695
+ Explore, search, and sort through the Shows Top 300 Trending spaces with AI Ratings
696
+ </div>
697
+ """)
698
+
699
+ with gr.Tabs() as tabs:
700
+ # Spaces νƒ­
701
+ with gr.Tab("🎯 Trending Spaces"):
702
+ with gr.Row(elem_classes="search-sort-container"):
703
+ with gr.Column(scale=2):
704
+ spaces_search = gr.Textbox(
705
+ label="πŸ” Search Spaces",
706
+ placeholder="Enter keywords to search...",
707
+ elem_classes="search-box"
708
+ )
709
+ with gr.Column(scale=2):
710
+ spaces_sort = gr.Radio(
711
+ choices=["rank", "rising_rate", "popularity"],
712
+ value="rank",
713
+ label="Sort by",
714
+ interactive=True
715
+ )
716
+ with gr.Column(scale=1):
717
+ spaces_refresh_btn = gr.Button(
718
+ "πŸ”„ Refresh",
719
+ variant="primary",
720
+ elem_classes="refresh-btn"
721
+ )
722
+ spaces_gallery = gr.HTML()
723
+ spaces_status = gr.Markdown("Loading...")
724
+
725
+ # Models νƒ­
726
+ with gr.Tab("πŸ€– Trending Models"):
727
+ with gr.Row(elem_classes="search-sort-container"):
728
+ with gr.Column(scale=2):
729
+ models_search = gr.Textbox(
730
+ label="πŸ” Search Models",
731
+ placeholder="Enter keywords to search...",
732
+ elem_classes="search-box"
733
+ )
734
+ with gr.Column(scale=2):
735
+ models_sort = gr.Radio(
736
+ choices=["rank", "rising_rate", "popularity"],
737
+ value="rank",
738
+ label="Sort by",
739
+ interactive=True
740
+ )
741
+ with gr.Column(scale=1):
742
+ models_refresh_btn = gr.Button(
743
+ "πŸ”„ Refresh",
744
+ variant="primary",
745
+ elem_classes="refresh-btn"
746
+ )
747
+ models_gallery = gr.HTML()
748
+ models_status = gr.Markdown("Loading...")
749
+
750
+ # Datasets νƒ­
751
+ with gr.Tab("πŸ“Š Trending Datasets"):
752
+ with gr.Row(elem_classes="search-sort-container"):
753
+ with gr.Column(scale=2):
754
+ datasets_search = gr.Textbox(
755
+ label="πŸ” Search Datasets",
756
+ placeholder="Enter keywords to search...",
757
+ elem_classes="search-box"
758
+ )
759
+ with gr.Column(scale=2):
760
+ datasets_sort = gr.Radio(
761
+ choices=["rank", "rising_rate", "popularity"],
762
+ value="rank",
763
+ label="Sort by",
764
+ interactive=True
765
+ )
766
+ with gr.Column(scale=1):
767
+ datasets_refresh_btn = gr.Button(
768
+ "πŸ”„ Refresh",
769
+ variant="primary",
770
+ elem_classes="refresh-btn"
771
+ )
772
+ datasets_gallery = gr.HTML()
773
+ datasets_status = gr.Markdown("Loading...")
774
+
775
+ # Event handlers
776
+ spaces_refresh_btn.click(
777
+ fn=get_trending_spaces,
778
+ inputs=[spaces_search, spaces_sort],
779
+ outputs=[spaces_gallery, spaces_status]
780
+ )
781
+
782
+ models_refresh_btn.click(
783
+ fn=get_models,
784
+ inputs=[models_search, models_sort],
785
+ outputs=[models_gallery, models_status]
786
+ )
787
+
788
+ datasets_refresh_btn.click(
789
+ fn=get_datasets,
790
+ inputs=[datasets_search, datasets_sort],
791
+ outputs=[datasets_gallery, datasets_status]
792
+ )
793
+
794
+ # 검색어 λ³€κ²½ μ‹œ μžλ™ μƒˆλ‘œκ³ μΉ¨
795
+ spaces_search.change(
796
+ fn=get_trending_spaces,
797
+ inputs=[spaces_search, spaces_sort],
798
+ outputs=[spaces_gallery, spaces_status]
799
+ )
800
+
801
+ models_search.change(
802
+ fn=get_models,
803
+ inputs=[models_search, models_sort],
804
+ outputs=[models_gallery, models_status]
805
+ )
806
+
807
+ datasets_search.change(
808
+ fn=get_datasets,
809
+ inputs=[datasets_search, datasets_sort],
810
+ outputs=[datasets_gallery, datasets_status]
811
+ )
812
+
813
+ # μ •λ ¬ 방식 λ³€κ²½ μ‹œ μžλ™ μƒˆλ‘œκ³ μΉ¨
814
+ spaces_sort.change(
815
+ fn=get_trending_spaces,
816
+ inputs=[spaces_search, spaces_sort],
817
+ outputs=[spaces_gallery, spaces_status]
818
+ )
819
+
820
+ models_sort.change(
821
+ fn=get_models,
822
+ inputs=[models_search, models_sort],
823
+ outputs=[models_gallery, models_status]
824
+ )
825
+
826
+ datasets_sort.change(
827
+ fn=get_datasets,
828
+ inputs=[datasets_search, datasets_sort],
829
+ outputs=[datasets_gallery, datasets_status]
830
+ )
831
+
832
+ # 초기 데이터 λ‘œλ“œ
833
+ interface.load(
834
+ fn=get_trending_spaces,
835
+ inputs=[spaces_search, spaces_sort],
836
+ outputs=[spaces_gallery, spaces_status]
837
+ )
838
+ interface.load(
839
+ fn=get_models,
840
+ inputs=[models_search, models_sort],
841
+ outputs=[models_gallery, models_status]
842
+ )
843
+ interface.load(
844
+ fn=get_datasets,
845
+ inputs=[datasets_search, datasets_sort],
846
+ outputs=[datasets_gallery, datasets_status]
847
+ )
848
+
849
+ return interface
850
+
851
+ if __name__ == "__main__":
852
+ try:
853
+ demo = create_interface()
854
+ demo.launch(
855
+ share=True,
856
+ inbrowser=True,
857
+ show_api=False
858
+ )
859
+ except Exception as e:
860
+ print(f"Error launching app: {e}")import os
861
+ import random
862
+ import base64
863
+ import requests
864
+ from selenium import webdriver
865
+ from selenium.webdriver.support.ui import WebDriverWait
866
+ from selenium.webdriver.support import expected_conditions as EC
867
+ from selenium.webdriver.common.by import By
868
+ from selenium.common.exceptions import WebDriverException, TimeoutException
869
+ from PIL import Image
870
+ from io import BytesIO
871
+ from datetime import datetime
872
+ import gradio as gr
873
+ from typing import Tuple
874
+ import time
875
+ from pathlib import Path # μΆ”κ°€
876
+
877
+ # μŠ€ν¬λ¦°μƒ· μΊμ‹œ 디렉토리 μ„€μ •
878
+ CACHE_DIR = Path("screenshot_cache")
879
+ CACHE_DIR.mkdir(exist_ok=True)
880
+
881
+ # μ „μ—­ λ³€μˆ˜λ‘œ μŠ€ν¬λ¦°μƒ· μΊμ‹œ μ„ μ–Έ
882
+ SCREENSHOT_CACHE = {}
883
+
884
+ def get_cached_screenshot(url: str) -> str:
885
+ """μΊμ‹œλœ μŠ€ν¬λ¦°μƒ· κ°€μ Έμ˜€κΈ° λ˜λŠ” μƒˆλ‘œ 생성"""
886
+ cache_file = CACHE_DIR / f"{base64.b64encode(url.encode()).decode()}.png"
887
+
888
+ if cache_file.exists():
889
+ with open(cache_file, "rb") as f:
890
+ return base64.b64encode(f.read()).decode()
891
+
892
+ return take_screenshot(url)
893
+
894
+ def take_screenshot(url):
895
+ """μ›Ήμ‚¬μ΄νŠΈ μŠ€ν¬λ¦°μƒ· 촬영 ν•¨μˆ˜ (λ‘œλ”© λŒ€κΈ° μ‹œκ°„ μΆ”κ°€)"""
896
+ if url in SCREENSHOT_CACHE:
897
+ return SCREENSHOT_CACHE[url]
898
+
899
+ if not url.startswith('http'):
900
+ url = f"https://{url}"
901
+
902
+ options = webdriver.ChromeOptions()
903
+ options.add_argument('--headless')
904
+ options.add_argument('--no-sandbox')
905
+ options.add_argument('--disable-dev-shm-usage')
906
+ options.add_argument('--window-size=1080,720')
907
+
908
+ try:
909
+ driver = webdriver.Chrome(options=options)
910
+ driver.get(url)
911
+
912
+ # λͺ…μ‹œμ  λŒ€κΈ°: body μš”μ†Œκ°€ λ‘œλ“œλ  λ•ŒκΉŒμ§€ λŒ€κΈ° (μ΅œλŒ€ 10초)
913
+ try:
914
+ WebDriverWait(driver, 10).until(
915
+ EC.presence_of_element_located((By.TAG_NAME, "body"))
916
+ )
917
+ except TimeoutException:
918
+ print(f"νŽ˜μ΄μ§€ λ‘œλ”© νƒ€μž„μ•„μ›ƒ: {url}")
919
+
920
+ # μΆ”κ°€ λŒ€κΈ° μ‹œκ°„μ„ 2초둜 증가
921
+ time.sleep(2) # 1μ΄ˆμ—μ„œ 2초둜 λ³€κ²½
922
+
923
+ # JavaScript μ‹€ν–‰ μ™„λ£Œ λŒ€κΈ°
924
+ driver.execute_script("return document.readyState") == "complete"
925
+
926
+ # μŠ€ν¬λ¦°μƒ· 촬영
927
+ screenshot = driver.get_screenshot_as_png()
928
+ img = Image.open(BytesIO(screenshot))
929
+ buffered = BytesIO()
930
+ img.save(buffered, format="PNG")
931
+ base64_image = base64.b64encode(buffered.getvalue()).decode()
932
+
933
+ # μΊμ‹œμ— μ €μž₯
934
+ SCREENSHOT_CACHE[url] = base64_image
935
+ return base64_image
936
+
937
+ except WebDriverException as e:
938
+ print(f"μŠ€ν¬λ¦°μƒ· 촬영 μ‹€νŒ¨: {str(e)} for URL: {url}")
939
+ return None
940
+ except Exception as e:
941
+ print(f"μ˜ˆμƒμΉ˜ λͺ»ν•œ 였λ₯˜: {str(e)} for URL: {url}")
942
+ return None
943
+ finally:
944
+ if 'driver' in locals():
945
+ driver.quit()
946
+
947
+ from datetime import datetime, timedelta
948
+
949
+ def calculate_rising_rate(created_date: str, rank: int) -> int:
950
+ """AI Rising Rate 계산"""
951
+ # 생성일 κΈ°μ€€ 점수 계산
952
+ created = datetime.strptime(created_date.split('T')[0], '%Y-%m-%d')
953
+ today = datetime.now()
954
+ days_diff = (today - created).days
955
+ date_score = max(0, 300 - days_diff) # μ΅œλŒ€ 300점
956
+
957
+ # μˆœμœ„ κΈ°μ€€ 점수 계산
958
+ rank_score = max(0, 300 - rank) # μ΅œλŒ€ 300점
959
+
960
+ # 총점 계산
961
+ total_score = date_score + rank_score
962
+
963
+ # 별 개수 계산 (0~5)
964
+ if total_score <= 100:
965
+ stars = 1
966
+ elif total_score <= 200:
967
+ stars = 2
968
+ elif total_score <= 300:
969
+ stars = 3
970
+ elif total_score <= 400:
971
+ stars = 4
972
+ else:
973
+ stars = 5
974
+
975
+ return stars
976
+
977
+ def get_popularity_grade(likes: int, stars: int) -> tuple:
978
+ """AI Popularity Score λ“±κΈ‰ 계산"""
979
+ # 기본 점수 (likes)
980
+ base_score = min(likes, 10000) # μ΅œλŒ€ 10000점
981
+
982
+ # 별점 μΆ”κ°€ 점수 (별 ν•˜λ‚˜λ‹Ή 500점)
983
+ star_score = stars * 500
984
+
985
+ # 총점
986
+ total_score = base_score + star_score
987
+
988
+ # λ“±κΈ‰ ν…Œμ΄λΈ” (18단계)
989
+ grades = [
990
+ (9000, "AAA+"), (8500, "AAA"), (8000, "AAA-"),
991
+ (7500, "AA+"), (7000, "AA"), (6500, "AA-"),
992
+ (6000, "A+"), (5500, "A"), (5000, "A-"),
993
+ (4500, "BBB+"), (4000, "BBB"), (3500, "BBB-"),
994
+ (3000, "BB+"), (2500, "BB"), (2000, "BB-"),
995
+ (1500, "B+"), (1000, "B"), (500, "B-")
996
+ ]
997
+
998
+ for threshold, grade in grades:
999
+ if total_score >= threshold:
1000
+ return grade, total_score
1001
+
1002
+ return "B-", total_score
1003
+
1004
+ # get_card ν•¨μˆ˜ λ‚΄μ˜ hardware_info 뢀뢄을 λ‹€μŒμœΌλ‘œ ꡐ체:
1005
+ def get_rating_info(item: dict, index: int) -> str:
1006
+ """평가 정보 HTML 생성"""
1007
+ created = item.get('createdAt', '').split('T')[0]
1008
+ likes = int(str(item.get('likes', '0')).replace(',', ''))
1009
+
1010
+ # AI Rising Rate 계산
1011
+ stars = calculate_rising_rate(created, index + 1)
1012
+ star_html = "β˜…" * stars + "β˜†" * (5 - stars) # μ±„μ›Œμ§„ 별과 빈 별 μ‘°ν•©
1013
+
1014
+ # AI Popularity Score 계산
1015
+ grade, score = get_popularity_grade(likes, stars)
1016
+
1017
+ # 등급별 색상 μ„€μ •
1018
+ grade_colors = {
1019
+ 'AAA': '#FFD700', 'AA': '#FFA500', 'A': '#FF4500',
1020
+ 'BBB': '#4169E1', 'BB': '#1E90FF', 'B': '#00BFFF'
1021
+ }
1022
+ grade_base = grade.rstrip('+-')
1023
+ grade_color = grade_colors.get(grade_base, '#666666')
1024
+
1025
+ return f"""
1026
+ <div style='
1027
+ margin-top: 15px;
1028
+ padding: 15px;
1029
+ background: rgba(255,255,255,0.4);
1030
+ border-radius: 10px;
1031
+ font-size: 0.9em;
1032
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);'>
1033
+ <div style='
1034
+ display: grid;
1035
+ grid-template-columns: repeat(2, 1fr);
1036
+ gap: 15px;'>
1037
+ <div style='
1038
+ color: #333;
1039
+ display: flex;
1040
+ flex-direction: column;
1041
+ gap: 5px;'>
1042
+ <span style='font-weight: bold;'>AI Rising Rate:</span>
1043
+ <span style='
1044
+ color: #FF8C00;
1045
+ font-size: 1.4em;
1046
+ letter-spacing: 2px;
1047
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.1);'>{star_html}</span>
1048
+ </div>
1049
+ <div style='
1050
+ color: #333;
1051
+ display: flex;
1052
+ flex-direction: column;
1053
+ gap: 5px;'>
1054
+ <span style='font-weight: bold;'>AI Popularity Score:</span>
1055
+ <span style='
1056
+ font-size: 1.2em;
1057
+ font-weight: bold;
1058
+ color: {grade_color};
1059
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.1);'>{grade} ({score:,})</span>
1060
+ </div>
1061
+ </div>
1062
+ </div>
1063
+ """
1064
+
1065
+ def get_hardware_info(item: dict) -> tuple:
1066
+ """ν•˜λ“œμ›¨μ–΄ 정보 μΆ”μΆœ"""
1067
+ try:
1068
+ # runtime 정보 확인
1069
+ runtime = item.get('runtime', {})
1070
+
1071
+ # CPU 정보 처리
1072
+ cpu_info = runtime.get('cpu', 'Standard')
1073
+
1074
+ # GPU 정보 처리
1075
+ gpu_info = "None"
1076
+ if runtime.get('accelerator') == "gpu":
1077
+ gpu_type = runtime.get('gpu', {}).get('name', '')
1078
+ gpu_memory = runtime.get('gpu', {}).get('memory', '')
1079
+ if gpu_type:
1080
+ gpu_info = f"{gpu_type}"
1081
+ if gpu_memory:
1082
+ gpu_info += f" ({gpu_memory}GB)"
1083
+
1084
+ # spaces decorator 확인
1085
+ if '@spaces.GPU' in str(item.get('sdk_version', '')):
1086
+ if gpu_info == "None":
1087
+ gpu_info = "GPU Enabled"
1088
+
1089
+ # SDK 정보 처리
1090
+ sdk = item.get('sdk', 'N/A')
1091
+
1092
+ print(f"Debug - Runtime Info: {runtime}") # 디버그 좜λ ₯
1093
+ print(f"Debug - GPU Info: {gpu_info}") # 디버그 좜λ ₯
1094
+
1095
+ return cpu_info, gpu_info, sdk
1096
+
1097
+ except Exception as e:
1098
+ print(f"Error parsing hardware info: {str(e)}")
1099
+ return 'Standard', 'None', 'N/A'
1100
+
1101
+ def get_card(item: dict, index: int, card_type: str = "space") -> str:
1102
+ """톡합 μΉ΄λ“œ HTML 생성"""
1103
+ item_id = item.get('id', '')
1104
+ author, title = item_id.split('/', 1)
1105
+ likes = format(item.get('likes', 0), ',')
1106
+ created = item.get('createdAt', '').split('T')[0]
1107
+
1108
+ # short_description κ°€μ Έμ˜€κΈ°
1109
+ short_description = item.get('cardData', {}).get('short_description', '')
1110
+
1111
+ # titleκ³Ό short_description을 ν¬ν•¨ν•œ 헀더 HTML
1112
+ title_html = f"""
1113
+ <h3 style='
1114
+ margin: 0 0 15px 0;
1115
+ color: #333;
1116
+ font-size: 1.3em;
1117
+ line-height: 1.4;
1118
+ display: -webkit-box;
1119
+ -webkit-line-clamp: 2;
1120
+ -webkit-box-orient: vertical;
1121
+ overflow: hidden;
1122
+ text-overflow: ellipsis;
1123
+ text-shadow: 1px 1px 1px rgba(255,255,255,0.8);'>
1124
+ {title}
1125
+ {f'<span style="display: block; font-size: 0.7em; color: #666; margin-top: 5px; font-weight: normal; font-style: italic;">{short_description}</span>' if short_description else ''}
1126
+ </h3>
1127
+ """
1128
+
1129
+
1130
+ # URL μ •μ˜
1131
+ if card_type == "space":
1132
+ url = f"https://huggingface.co/spaces/{item_id}"
1133
+ elif card_type == "model":
1134
+ url = f"https://huggingface.co/{item_id}"
1135
+ else: # dataset
1136
+ url = f"https://huggingface.co/datasets/{item_id}"
1137
+
1138
+ # 메타데이터 처리
1139
+ tags = item.get('tags', [])
1140
+ pipeline_tag = item.get('pipeline_tag', '')
1141
+ license = item.get('license', '')
1142
+ sdk = item.get('sdk', 'N/A')
1143
+
1144
+ # AI Rating 정보 κ°€μ Έμ˜€κΈ°
1145
+ rating_info = get_rating_info(item, index)
1146
+
1147
+ # μΉ΄λ“œ νƒ€μž…λ³„ κ·ΈλΌλ°μ΄μ…˜ μ„€μ •
1148
+ if card_type == "space":
1149
+ gradient_colors = """
1150
+ rgba(255, 182, 193, 0.7), /* νŒŒμŠ€ν…” 핑크 */
1151
+ rgba(173, 216, 230, 0.7), /* νŒŒμŠ€ν…” 블루 */
1152
+ rgba(255, 218, 185, 0.7) /* νŒŒμŠ€ν…” ν”ΌμΉ˜ */
1153
+ """
1154
+ bg_content = f"""
1155
+ background-image: url(data:image/png;base64,{get_cached_screenshot(url) if get_cached_screenshot(url) else ''});
1156
+ background-size: cover;
1157
+ background-position: center;
1158
+ """
1159
+ type_icon = "🎯"
1160
+ type_label = "SPACE"
1161
+ elif card_type == "model":
1162
+ gradient_colors = """
1163
+ rgba(110, 142, 251, 0.7), /* λͺ¨λΈ 블루 */
1164
+ rgba(130, 158, 251, 0.7),
1165
+ rgba(150, 174, 251, 0.7)
1166
+ """
1167
+ bg_content = f"""
1168
+ background: linear-gradient(135deg, #6e8efb, #4a6cf7);
1169
+ padding: 15px;
1170
+ """
1171
+ type_icon = "πŸ€–"
1172
+ type_label = "MODEL"
1173
+ else: # dataset
1174
+ gradient_colors = """
1175
+ rgba(255, 107, 107, 0.7), /* 데이터셋 λ ˆλ“œ */
1176
+ rgba(255, 127, 127, 0.7),
1177
+ rgba(255, 147, 147, 0.7)
1178
+ """
1179
+ bg_content = f"""
1180
+ background: linear-gradient(135deg, #ff6b6b, #ff8787);
1181
+ padding: 15px;
1182
+ """
1183
+ type_icon = "πŸ“Š"
1184
+ type_label = "DATASET"
1185
+
1186
+ content_bg = f"""
1187
+ background: linear-gradient(135deg, {gradient_colors});
1188
+ backdrop-filter: blur(10px);
1189
+ """
1190
+
1191
+
1192
+ # νƒœκ·Έ ν‘œμ‹œ (models와 datasets용)
1193
+ tags_html = ""
1194
+ if card_type != "space":
1195
+ tags_html = f"""
1196
+ <div style='
1197
+ position: absolute;
1198
+ top: 50%;
1199
+ left: 50%;
1200
+ transform: translate(-50%, -50%);
1201
+ display: flex;
1202
+ flex-wrap: wrap;
1203
+ gap: 5px;
1204
+ justify-content: center;
1205
+ width: 90%;'>
1206
+ {' '.join([f'''
1207
+ <span style='
1208
+ background: rgba(255,255,255,0.2);
1209
+ padding: 5px 10px;
1210
+ border-radius: 15px;
1211
+ color: white;
1212
+ font-size: 0.8em;'>
1213
+ #{tag}
1214
+ </span>
1215
+ ''' for tag in tags[:5]])}
1216
+ </div>
1217
+ """
1218
+
1219
+ # μΉ΄λ“œ HTML λ°˜ν™˜
1220
+ return f"""
1221
+ <div class="card" style='
1222
+ position: relative;
1223
+ border: none;
1224
+ padding: 0;
1225
+ margin: 10px;
1226
+ border-radius: 20px;
1227
+ box-shadow: 0 10px 20px rgba(0,0,0,0.1);
1228
+ background: white;
1229
+ transition: all 0.3s ease;
1230
+ overflow: hidden;
1231
+ min-height: 400px;
1232
+ cursor: pointer;
1233
+ transform-origin: center;'
1234
+ onmouseover="this.style.transform='scale(0.98) translateY(5px)'; this.style.boxShadow='0 5px 15px rgba(0,0,0,0.2)';"
1235
+ onmouseout="this.style.transform='scale(1) translateY(0)'; this.style.boxShadow='0 10px 20px rgba(0,0,0,0.1)';"
1236
+ onclick="window.open('{url}', '_blank')">
1237
+
1238
+ <!-- 상단 μ˜μ—­ -->
1239
+ <div style='
1240
+ width: 100%;
1241
+ height: 200px;
1242
+ {bg_content}
1243
+ position: relative;'>
1244
+
1245
+ <!-- μˆœμœ„ 뱃지 -->
1246
+ <div style='
1247
+ position: absolute;
1248
+ top: 10px;
1249
+ left: 10px;
1250
+ background: rgba(0,0,0,0.7);
1251
+ color: white;
1252
+ padding: 5px 15px;
1253
+ border-radius: 20px;
1254
+ font-weight: bold;
1255
+ font-size: 0.9em;
1256
+ backdrop-filter: blur(5px);'>
1257
+ #{index + 1}
1258
+ </div>
1259
+
1260
+ <!-- νƒ€μž… 뱃지 -->
1261
+ <div style='
1262
+ position: absolute;
1263
+ top: 10px;
1264
+ right: 10px;
1265
+ background: rgba(255,255,255,0.9);
1266
+ padding: 5px 15px;
1267
+ border-radius: 20px;
1268
+ font-weight: bold;
1269
+ font-size: 0.8em;'>
1270
+ {type_icon} {type_label}
1271
+ </div>
1272
+
1273
+ {tags_html}
1274
+ </div>
1275
+
1276
+ <!-- μ½˜ν…μΈ  μ˜μ—­ -->
1277
+ <div style='
1278
+ padding: 20px;
1279
+ {content_bg}
1280
+ border-radius: 0 0 20px 20px;
1281
+ border-top: 1px solid rgba(255,255,255,0.5);'>
1282
+ <h3 style='
1283
+ margin: 0 0 15px 0;
1284
+ color: #333;
1285
+ font-size: 1.3em;
1286
+ line-height: 1.4;
1287
+ display: -webkit-box;
1288
+ -webkit-line-clamp: 2;
1289
+ -webkit-box-orient: vertical;
1290
+ overflow: hidden;
1291
+ text-overflow: ellipsis;
1292
+ text-shadow: 1px 1px 1px rgba(255,255,255,0.8);'>
1293
+ {title}
1294
+ </h3>
1295
+
1296
+ <div style='
1297
+ display: grid;
1298
+ grid-template-columns: repeat(2, 1fr);
1299
+ gap: 10px;
1300
+ font-size: 0.9em;
1301
+ background: rgba(255,255,255,0.3);
1302
+ padding: 10px;
1303
+ border-radius: 10px;'>
1304
+ <div style='color: #444;'>
1305
+ <span style='margin-right: 5px;'>πŸ‘€</span> {author}
1306
+ </div>
1307
+ <div style='color: #444;'>
1308
+ <span style='margin-right: 5px;'>❀️</span> {likes}
1309
+ </div>
1310
+ <div style='color: #444; grid-column: span 2;'>
1311
+ <span style='margin-right: 5px;'>πŸ“…</span> {created}
1312
+ </div>
1313
+ </div>
1314
+
1315
+ {rating_info}
1316
+ </div>
1317
+ </div>
1318
+ """
1319
+
1320
+ def get_trending_spaces(search_query="", sort_by="rank", progress=gr.Progress()) -> Tuple[str, str]:
1321
+ """νŠΈλ Œλ”© 슀페이슀 κ°€μ Έμ˜€κΈ°"""
1322
+ url = "https://huggingface.co/api/spaces"
1323
+
1324
+ try:
1325
+ progress(0, desc="Fetching spaces data...")
1326
+ params = {
1327
+ 'full': 'true',
1328
+ 'limit': 100
1329
+ }
1330
+
1331
+ response = requests.get(url, params=params)
1332
+ response.raise_for_status()
1333
+ spaces = response.json()
1334
+
1335
+ # κ²€μƒ‰μ–΄λ‘œ 필터링
1336
+ if search_query:
1337
+ spaces = [space for space in spaces if search_query.lower() in
1338
+ (space.get('id', '') + ' ' + space.get('title', '')).lower()]
1339
+
1340
+ # μ •λ ¬
1341
+ sort_by = sort_by.lower()
1342
+ if sort_by == "rising_rate":
1343
+ spaces.sort(key=lambda x: calculate_rising_rate(x.get('createdAt', ''), 0), reverse=True)
1344
+ elif sort_by == "popularity":
1345
+ spaces.sort(key=lambda x: get_popularity_grade(
1346
+ int(str(x.get('likes', '0')).replace(',', '')),
1347
+ calculate_rising_rate(x.get('createdAt', ''), 0))[1],
1348
+ reverse=True)
1349
+
1350
+ progress(0.1, desc="Creating gallery...")
1351
+ html_content = """
1352
+ <div style='padding: 20px; background: #f5f5f5;'>
1353
+ <div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;'>
1354
+ """
1355
+
1356
+ for idx, space in enumerate(spaces):
1357
+ html_content += get_card(space, idx, "space")
1358
+ progress((0.1 + 0.9 * idx/len(spaces)), desc=f"Loading space {idx+1}/{len(spaces)}...")
1359
+
1360
+ html_content += "</div></div>"
1361
+
1362
+ progress(1.0, desc="Complete!")
1363
+ return html_content, f"Found {len(spaces)} spaces"
1364
+
1365
+ except Exception as e:
1366
+ error_html = f'<div style="color: red; padding: 20px;">Error: {str(e)}</div>'
1367
+ return error_html, f"Error: {str(e)}"
1368
+
1369
+ def get_models(search_query="", sort_by="rank", progress=gr.Progress()) -> Tuple[str, str]:
1370
+ """인기 λͺ¨λΈ κ°€μ Έμ˜€κΈ°"""
1371
+ url = "https://huggingface.co/api/models"
1372
+
1373
+ try:
1374
+ progress(0, desc="Fetching models data...")
1375
+ params = {
1376
+ 'full': 'true',
1377
+ 'limit': 300
1378
+ }
1379
+ response = requests.get(url, params=params)
1380
+ response.raise_for_status()
1381
+ models = response.json()
1382
+
1383
+ # κ²€μƒ‰μ–΄λ‘œ 필터링
1384
+ if search_query:
1385
+ models = [model for model in models if search_query.lower() in
1386
+ (model.get('id', '') + ' ' + model.get('title', '')).lower()]
1387
+
1388
+ # μ •λ ¬
1389
+ sort_by = sort_by.lower()
1390
+ if sort_by == "rising_rate":
1391
+ models.sort(key=lambda x: calculate_rising_rate(x.get('createdAt', ''), 0), reverse=True)
1392
+ elif sort_by == "popularity":
1393
+ models.sort(key=lambda x: get_popularity_grade(
1394
+ int(str(x.get('likes', '0')).replace(',', '')),
1395
+ calculate_rising_rate(x.get('createdAt', ''), 0))[1],
1396
+ reverse=True)
1397
+
1398
+ progress(0.1, desc="Creating gallery...")
1399
+ html_content = """
1400
+ <div style='padding: 20px; background: #f5f5f5;'>
1401
+ <div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;'>
1402
+ """
1403
+
1404
+ for idx, model in enumerate(models):
1405
+ html_content += get_card(model, idx, "model")
1406
+ progress((0.1 + 0.9 * idx/len(models)), desc=f"Loading model {idx+1}/{len(models)}...")
1407
+
1408
+ html_content += "</div></div>"
1409
+
1410
+ progress(1.0, desc="Complete!")
1411
+ return html_content, f"Found {len(models)} models"
1412
+
1413
+ except Exception as e:
1414
+ error_html = f'<div style="color: red; padding: 20px;">Error: {str(e)}</div>'
1415
+ return error_html, f"Error: {str(e)}"
1416
+
1417
+ def get_datasets(search_query="", sort_by="rank", progress=gr.Progress()) -> Tuple[str, str]:
1418
+ """인기 데이터셋 κ°€μ Έμ˜€κΈ°"""
1419
+ url = "https://huggingface.co/api/datasets"
1420
+
1421
+ try:
1422
+ progress(0, desc="Fetching datasets data...")
1423
+ params = {
1424
+ 'full': 'true',
1425
+ 'limit': 300
1426
+ }
1427
+ response = requests.get(url, params=params)
1428
+ response.raise_for_status()
1429
+ datasets = response.json()
1430
+
1431
+ # κ²€μƒ‰μ–΄λ‘œ 필터링
1432
+ if search_query:
1433
+ datasets = [dataset for dataset in datasets if search_query.lower() in
1434
+ (dataset.get('id', '') + ' ' + dataset.get('title', '')).lower()]
1435
+
1436
+ # μ •λ ¬
1437
+ sort_by = sort_by.lower()
1438
+ if sort_by == "rising_rate":
1439
+ datasets.sort(key=lambda x: calculate_rising_rate(x.get('createdAt', ''), 0), reverse=True)
1440
+ elif sort_by == "popularity":
1441
+ datasets.sort(key=lambda x: get_popularity_grade(
1442
+ int(str(x.get('likes', '0')).replace(',', '')),
1443
+ calculate_rising_rate(x.get('createdAt', ''), 0))[1],
1444
+ reverse=True)
1445
+
1446
+ progress(0.1, desc="Creating gallery...")
1447
+ html_content = """
1448
+ <div style='padding: 20px; background: #f5f5f5;'>
1449
+ <div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;'>
1450
+ """
1451
+
1452
+ for idx, dataset in enumerate(datasets):
1453
+ html_content += get_card(dataset, idx, "dataset")
1454
+ progress((0.1 + 0.9 * idx/len(datasets)), desc=f"Loading dataset {idx+1}/{len(datasets)}...")
1455
+
1456
+ html_content += "</div></div>"
1457
+
1458
+ progress(1.0, desc="Complete!")
1459
+ return html_content, f"Found {len(datasets)} datasets"
1460
+
1461
+ except Exception as e:
1462
+ error_html = f'<div style="color: red; padding: 20px;">Error: {str(e)}</div>'
1463
+ return error_html, f"Error: {str(e)}"
1464
+
1465
+ # μ •λ ¬ ν•¨μˆ˜ μΆ”κ°€
1466
+ def sort_items(items, sort_by):
1467
+ if sort_by == "rank":
1468
+ return items # 이미 μˆœμœ„λŒ€λ‘œ μ •λ ¬λ˜μ–΄ 있음
1469
+ elif sort_by == "rising_rate":
1470
+ return sorted(items, key=lambda x: calculate_rising_rate(x.get('createdAt', ''), 0), reverse=True)
1471
+ elif sort_by == "popularity":
1472
+ return sorted(items, key=lambda x: get_popularity_grade(int(str(x.get('likes', '0')).replace(',', '')),
1473
+ calculate_rising_rate(x.get('createdAt', ''), 0))[1], reverse=True)
1474
+ return items
1475
+
1476
+ # API 호좜 ν•¨μˆ˜ μˆ˜μ •
1477
+ def fetch_items(item_type, search_query="", sort_by="rank", limit=1000):
1478
+ """μ•„μ΄ν…œ κ°€μ Έμ˜€κΈ° (spaces/models/datasets)"""
1479
+ base_url = f"https://huggingface.co/api/{item_type}"
1480
+ params = {
1481
+ 'full': 'true',
1482
+ 'limit': limit,
1483
+ 'search': search_query
1484
+ }
1485
+
1486
+ try:
1487
+ response = requests.get(base_url, params=params)
1488
+ response.raise_for_status()
1489
+ items = response.json()
1490
+
1491
+ # κ²€μƒ‰μ–΄λ‘œ 필터링
1492
+ if search_query:
1493
+ items = [item for item in items if search_query.lower() in
1494
+ (item.get('id', '') + item.get('title', '')).lower()]
1495
+
1496
+ # μ •λ ¬
1497
+ items = sort_items(items, sort_by)
1498
+
1499
+ return items[:300] # μƒμœ„ 300개만 λ°˜ν™˜
1500
+ except Exception as e:
1501
+ print(f"Error fetching items: {e}")
1502
+ return []
1503
+
1504
+ def create_interface():
1505
+ with gr.Blocks(title="HuggingFace Trending Board", css="""
1506
+ .search-sort-container {
1507
+ background: linear-gradient(135deg, rgba(255,255,255,0.95), rgba(240,240,255,0.95));
1508
+ border-radius: 15px;
1509
+ padding: 20px;
1510
+ margin: 10px 0;
1511
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
1512
+ overflow: visible;
1513
+ }
1514
+ .search-box {
1515
+ border: 2px solid #e1e1e1;
1516
+ border-radius: 10px;
1517
+ padding: 12px;
1518
+ transition: all 0.3s ease;
1519
+ background: linear-gradient(135deg, #ffffff, #f8f9ff);
1520
+ width: 100%;
1521
+ }
1522
+ .search-box:focus {
1523
+ border-color: #7b61ff;
1524
+ box-shadow: 0 0 0 2px rgba(123,97,255,0.2);
1525
+ background: linear-gradient(135deg, #ffffff, #f0f3ff);
1526
+ }
1527
+ .refresh-btn {
1528
+ background: linear-gradient(135deg, #7b61ff, #6366f1);
1529
+ color: white;
1530
+ border: none;
1531
+ padding: 10px 20px;
1532
+ border-radius: 10px;
1533
+ cursor: pointer;
1534
+ transition: all 0.3s ease;
1535
+ width: 120px;
1536
+ height: 80px !important;
1537
+ display: flex;
1538
+ align-items: center;
1539
+ justify-content: center;
1540
+ margin-left: auto;
1541
+ font-size: 1.2em !important;
1542
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
1543
+ }
1544
+ .refresh-btn:hover {
1545
+ transform: translateY(-2px);
1546
+ box-shadow: 0 6px 12px rgba(0,0,0,0.2);
1547
+ background: linear-gradient(135deg, #8b71ff, #7376f1);
1548
+ }
1549
+ """) as interface:
1550
+
1551
+ gr.Markdown("""
1552
+ # πŸ€— HuggingFace Trending TOP 300 Board
1553
+ <div style='margin-bottom: 20px; padding: 10px; background: linear-gradient(135deg, rgba(123,97,255,0.1), rgba(99,102,241,0.1)); border-radius: 10px;'>
1554
+ Explore, search, and sort through the Shows Top 300 Trending spaces with AI Ratings
1555
+ </div>
1556
+ """)
1557
+
1558
+ with gr.Tabs() as tabs:
1559
+ # Spaces νƒ­
1560
+ with gr.Tab("🎯 Trending Spaces"):
1561
+ with gr.Row(elem_classes="search-sort-container"):
1562
+ with gr.Column(scale=2):
1563
+ spaces_search = gr.Textbox(
1564
+ label="πŸ” Search Spaces",
1565
+ placeholder="Enter keywords to search...",
1566
+ elem_classes="search-box"
1567
+ )
1568
+ with gr.Column(scale=2):
1569
+ spaces_sort = gr.Radio(
1570
+ choices=["rank", "rising_rate", "popularity"],
1571
+ value="rank",
1572
+ label="Sort by",
1573
+ interactive=True
1574
+ )
1575
+ with gr.Column(scale=1):
1576
+ spaces_refresh_btn = gr.Button(
1577
+ "πŸ”„ Refresh",
1578
+ variant="primary",
1579
+ elem_classes="refresh-btn"
1580
+ )
1581
+ spaces_gallery = gr.HTML()
1582
+ spaces_status = gr.Markdown("Loading...")
1583
+
1584
+ # Models νƒ­
1585
+ with gr.Tab("πŸ€– Trending Models"):
1586
+ with gr.Row(elem_classes="search-sort-container"):
1587
+ with gr.Column(scale=2):
1588
+ models_search = gr.Textbox(
1589
+ label="πŸ” Search Models",
1590
+ placeholder="Enter keywords to search...",
1591
+ elem_classes="search-box"
1592
+ )
1593
+ with gr.Column(scale=2):
1594
+ models_sort = gr.Radio(
1595
+ choices=["rank", "rising_rate", "popularity"],
1596
+ value="rank",
1597
+ label="Sort by",
1598
+ interactive=True
1599
+ )
1600
+ with gr.Column(scale=1):
1601
+ models_refresh_btn = gr.Button(
1602
+ "πŸ”„ Refresh",
1603
+ variant="primary",
1604
+ elem_classes="refresh-btn"
1605
+ )
1606
+ models_gallery = gr.HTML()
1607
+ models_status = gr.Markdown("Loading...")
1608
+
1609
+ # Datasets νƒ­
1610
+ with gr.Tab("πŸ“Š Trending Datasets"):
1611
+ with gr.Row(elem_classes="search-sort-container"):
1612
+ with gr.Column(scale=2):
1613
+ datasets_search = gr.Textbox(
1614
+ label="πŸ” Search Datasets",
1615
+ placeholder="Enter keywords to search...",
1616
+ elem_classes="search-box"
1617
+ )
1618
+ with gr.Column(scale=2):
1619
+ datasets_sort = gr.Radio(
1620
+ choices=["rank", "rising_rate", "popularity"],
1621
+ value="rank",
1622
+ label="Sort by",
1623
+ interactive=True
1624
+ )
1625
+ with gr.Column(scale=1):
1626
+ datasets_refresh_btn = gr.Button(
1627
+ "πŸ”„ Refresh",
1628
+ variant="primary",
1629
+ elem_classes="refresh-btn"
1630
+ )
1631
+ datasets_gallery = gr.HTML()
1632
+ datasets_status = gr.Markdown("Loading...")
1633
+
1634
+ # Event handlers
1635
+ spaces_refresh_btn.click(
1636
+ fn=get_trending_spaces,
1637
+ inputs=[spaces_search, spaces_sort],
1638
+ outputs=[spaces_gallery, spaces_status]
1639
+ )
1640
+
1641
+ models_refresh_btn.click(
1642
+ fn=get_models,
1643
+ inputs=[models_search, models_sort],
1644
+ outputs=[models_gallery, models_status]
1645
+ )
1646
+
1647
+ datasets_refresh_btn.click(
1648
+ fn=get_datasets,
1649
+ inputs=[datasets_search, datasets_sort],
1650
+ outputs=[datasets_gallery, datasets_status]
1651
+ )
1652
+
1653
+ # 검색어 λ³€κ²½ μ‹œ μžλ™ μƒˆλ‘œκ³ μΉ¨
1654
+ spaces_search.change(
1655
+ fn=get_trending_spaces,
1656
+ inputs=[spaces_search, spaces_sort],
1657
+ outputs=[spaces_gallery, spaces_status]
1658
+ )
1659
+
1660
+ models_search.change(
1661
+ fn=get_models,
1662
+ inputs=[models_search, models_sort],
1663
+ outputs=[models_gallery, models_status]
1664
+ )
1665
+
1666
+ datasets_search.change(
1667
+ fn=get_datasets,
1668
+ inputs=[datasets_search, datasets_sort],
1669
+ outputs=[datasets_gallery, datasets_status]
1670
+ )
1671
+
1672
+ # μ •λ ¬ 방식 λ³€κ²½ μ‹œ μžλ™ μƒˆλ‘œκ³ μΉ¨
1673
+ spaces_sort.change(
1674
+ fn=get_trending_spaces,
1675
+ inputs=[spaces_search, spaces_sort],
1676
+ outputs=[spaces_gallery, spaces_status]
1677
+ )
1678
+
1679
+ models_sort.change(
1680
+ fn=get_models,
1681
+ inputs=[models_search, models_sort],
1682
+ outputs=[models_gallery, models_status]
1683
+ )
1684
+
1685
+ datasets_sort.change(
1686
+ fn=get_datasets,
1687
+ inputs=[datasets_search, datasets_sort],
1688
+ outputs=[datasets_gallery, datasets_status]
1689
+ )
1690
+
1691
+ # 초기 데이터 λ‘œλ“œ
1692
+ interface.load(
1693
+ fn=get_trending_spaces,
1694
+ inputs=[spaces_search, spaces_sort],
1695
+ outputs=[spaces_gallery, spaces_status]
1696
+ )
1697
+ interface.load(
1698
+ fn=get_models,
1699
+ inputs=[models_search, models_sort],
1700
+ outputs=[models_gallery, models_status]
1701
+ )
1702
+ interface.load(
1703
+ fn=get_datasets,
1704
+ inputs=[datasets_search, datasets_sort],
1705
+ outputs=[datasets_gallery, datasets_status]
1706
+ )
1707
+
1708
+ return interface
1709
+
1710
+ if __name__ == "__main__":
1711
+ try:
1712
+ demo = create_interface()
1713
+ demo.launch(
1714
+ share=True,
1715
+ inbrowser=True,
1716
+ show_api=False
1717
+ )
1718
+ except Exception as e:
1719
+ print(f"Error launching app: {e}")