openfree commited on
Commit
24f5139
·
verified ·
1 Parent(s): a371956

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +102 -169
app.py CHANGED
@@ -29,17 +29,19 @@ def generate_dummy_spaces(count):
29
  })
30
  return spaces
31
 
32
- # Function to fetch Zero-GPU (CPU-based) Spaces from Huggingface with pagination
33
  def fetch_trending_spaces(offset=0, limit=72):
34
  """
35
- hardware=cpu 파라미터를 추가하여 GPU가 없는(=CPU 전용) 스페이스만 가져옴
 
 
36
  """
37
  try:
38
  url = "https://huggingface.co/api/spaces"
39
  params = {
40
- "limit": 10000, # 많이 가져오기
41
- "hardware": "cpu" # <-- Zero GPU(=CPU) 필터 적용
42
- # "sort": "trending", # 필요 시 추가
43
  }
44
  response = requests.get(url, params=params, timeout=30)
45
 
@@ -53,19 +55,25 @@ def fetch_trending_spaces(offset=0, limit=72):
53
  and space.get('id', '').split('/', 1)[0] != 'None'
54
  ]
55
 
56
- # 페이징 슬라이싱
57
- start = min(offset, len(filtered_spaces))
58
- end = min(offset + limit, len(filtered_spaces))
59
 
60
- print(f"[fetch_trending_spaces] CPU기반 스페이스 총 {len(filtered_spaces)}개, "
61
- f"요청 구간 {start}~{end-1} 반환")
 
 
 
 
 
 
62
 
63
  return {
64
  'spaces': filtered_spaces[start:end],
65
- 'total': len(filtered_spaces),
66
  'offset': offset,
67
  'limit': limit,
68
- 'all_spaces': filtered_spaces # 통계 산출용 (전체 CPU 스페이스)
69
  }
70
  else:
71
  print(f"Error fetching spaces: {response.status_code}")
@@ -80,7 +88,6 @@ def fetch_trending_spaces(offset=0, limit=72):
80
 
81
  except Exception as e:
82
  print(f"Exception when fetching spaces: {e}")
83
- # 실패 시 더미 데이터 생성
84
  return {
85
  'spaces': generate_dummy_spaces(limit),
86
  'total': 200,
@@ -93,7 +100,7 @@ def fetch_trending_spaces(offset=0, limit=72):
93
  def transform_url(owner, name):
94
  """
95
  Hugging Face Space -> 서브도메인 접근 URL
96
- 예: huggingface.co/spaces/owner/spaceName -> owner-spacename.hf.space
97
  """
98
  name = name.replace('.', '-').replace('_', '-')
99
  owner = owner.lower()
@@ -103,7 +110,7 @@ def transform_url(owner, name):
103
  # Get space details
104
  def get_space_details(space_data, index, offset):
105
  """
106
- Zero GPU 스페이스에서 필요한 정보(타이틀, owner, description, avatar_url 등) 추출
107
  """
108
  try:
109
  if '/' in space_data.get('id', ''):
@@ -112,7 +119,6 @@ def get_space_details(space_data, index, offset):
112
  owner = space_data.get('owner', '')
113
  name = space_data.get('id', '')
114
 
115
- # Ignore if 'None'
116
  if owner == 'None' or name == 'None':
117
  return None
118
 
@@ -137,11 +143,11 @@ def get_space_details(space_data, index, offset):
137
  'description': short_desc,
138
  'avatar_url': avatar_url,
139
  'author_name': author_name,
 
140
  'rank': offset + index + 1
141
  }
142
  except Exception as e:
143
  print(f"Error processing space data: {e}")
144
- # 최소한의 데이터라도 리턴
145
  return {
146
  'url': 'https://huggingface.co/spaces',
147
  'embedUrl': 'https://huggingface.co/spaces',
@@ -158,8 +164,8 @@ def get_space_details(space_data, index, offset):
158
  # Get owner statistics from all spaces
159
  def get_owner_stats(all_spaces):
160
  """
161
- all_spaces: Zero GPU(=CPU) 스페이스 목록(필터된 전체).
162
- 여기서 owner 카운트하여 상위 30명 추출.
163
  """
164
  owners = []
165
  for space in all_spaces:
@@ -175,23 +181,24 @@ def get_owner_stats(all_spaces):
175
  top_owners = owner_counts.most_common(30)
176
  return top_owners
177
 
 
 
178
  @app.route('/')
179
  def home():
180
- """
181
- index.html 템플릿 (프론트엔드) 로드
182
- """
183
  return render_template('index.html')
184
 
185
  @app.route('/api/trending-spaces', methods=['GET'])
186
  def trending_spaces():
187
  """
188
- /api/trending-spaces
189
- -> Zero GPU 스페이스 가져와서 검색, 페이징, 통계(오너 상위 30) 반환
 
190
  """
191
  search_query = request.args.get('search', '').lower()
192
  offset = int(request.args.get('offset', 0))
193
  limit = int(request.args.get('limit', 72))
194
 
 
195
  spaces_data = fetch_trending_spaces(offset, limit)
196
 
197
  results = []
@@ -210,7 +217,6 @@ def trending_spaces():
210
 
211
  results.append(space_info)
212
 
213
- # 전체 CPU 스페이스를 대상으로 Top 30 오너 추출
214
  top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
215
 
216
  return jsonify({
@@ -223,17 +229,16 @@ def trending_spaces():
223
 
224
  if __name__ == '__main__':
225
  """
226
- 서버 구동 시, templates/index.html 파일을 생성 후 Flask 실행
227
  """
228
  os.makedirs('templates', exist_ok=True)
229
 
230
- # index.html 생성 (깨진 이미지 대신 로봇 이모지 + 기존 전체 코드 유지)
231
  with open('templates/index.html', 'w', encoding='utf-8') as f:
232
  f.write('''<!DOCTYPE html>
233
  <html lang="en">
234
  <head>
235
  <meta charset="UTF-8">
236
- <title>Huggingface Zero-GPU Spaces</title>
237
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
238
  <style>
239
  /* Google Fonts & Base Styling */
@@ -266,7 +271,7 @@ if __name__ == '__main__':
266
  }
267
 
268
  body {
269
- font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
270
  line-height: 1.6;
271
  color: var(--text-primary);
272
  background-color: #f8f9fa;
@@ -755,60 +760,6 @@ if __name__ == '__main__':
755
  width: 100%;
756
  height: 500px;
757
  }
758
-
759
- /* Responsive Design */
760
- @media (max-width: 768px) {
761
- body {
762
- padding: 1rem;
763
- }
764
-
765
- .grid-container {
766
- grid-template-columns: 1fr;
767
- }
768
-
769
- .search-bar {
770
- flex-direction: column;
771
- padding: 10px;
772
- }
773
-
774
- .search-bar input {
775
- width: 100%;
776
- margin-bottom: 10px;
777
- }
778
-
779
- .search-bar .refresh-btn {
780
- width: 100%;
781
- justify-content: center;
782
- }
783
-
784
- .pagination {
785
- flex-wrap: wrap;
786
- }
787
-
788
- .chart-container {
789
- height: 300px;
790
- }
791
- }
792
-
793
- .error-emoji-detector {
794
- position: fixed;
795
- top: -9999px;
796
- left: -9999px;
797
- z-index: -1;
798
- opacity: 0;
799
- }
800
-
801
- /* 로봇 이모지 대체 */
802
- .emoji-avatar {
803
- font-size: 1.2rem;
804
- width: 32px;
805
- height: 32px;
806
- border-radius: 50%;
807
- border: 1px solid #ccc;
808
- display: flex;
809
- align-items: center;
810
- justify-content: center;
811
- }
812
  </style>
813
  </head>
814
  <body>
@@ -825,8 +776,8 @@ if __name__ == '__main__':
825
 
826
  <div class="mac-content">
827
  <div class="header">
828
- <h1>Zero GPU Spaces</h1>
829
- <p>Discover 'TOP 1,000' Zero GPU(Shared A100) spaces from Hugging Face</p>
830
  </div>
831
 
832
  <!-- Tab Navigation -->
@@ -835,7 +786,7 @@ if __name__ == '__main__':
835
  <button id="tabFixedButton" class="tab-button">Fixed Tab</button>
836
  </div>
837
 
838
- <!-- Trending(Zero GPU) Tab Content -->
839
  <div id="trendingTab" class="tab-content active">
840
  <div class="stats-window mac-window">
841
  <div class="mac-toolbar">
@@ -848,7 +799,7 @@ if __name__ == '__main__':
848
  </div>
849
  <div class="mac-content">
850
  <div class="stats-header">
851
- <div class="stats-title">Top 30 Creators by Count Number of TOP 1000 RANK</div>
852
  <button id="statsToggle" class="stats-toggle">Show Stats</button>
853
  </div>
854
  <div id="statsContent" class="stats-content">
@@ -891,7 +842,7 @@ if __name__ == '__main__':
891
  </div>
892
 
893
  <script>
894
- // DOM Elements
895
  const elements = {
896
  gridContainer: document.getElementById('gridContainer'),
897
  loadingIndicator: document.getElementById('loadingIndicator'),
@@ -932,10 +883,7 @@ if __name__ == '__main__':
932
 
933
  startChecking: function(iframe, owner, name, title, spaceKey) {
934
  this.checkQueue[spaceKey] = {
935
- iframe: iframe,
936
- owner: owner,
937
- name: name,
938
- title: title,
939
  attempts: 0,
940
  status: 'loading'
941
  };
@@ -963,10 +911,8 @@ if __name__ == '__main__':
963
  item.iframe.contentWindow.document.body;
964
  if (hasContent && item.iframe.contentWindow.document.body.innerHTML.length > 100) {
965
  const bodyText = item.iframe.contentWindow.document.body.textContent.toLowerCase();
966
- if (bodyText.includes('forbidden') ||
967
- bodyText.includes('404') ||
968
- bodyText.includes('not found') ||
969
- bodyText.includes('error')) {
970
  item.status = 'error';
971
  handleIframeError(item.iframe, item.owner, item.name, item.title);
972
  } else {
@@ -976,7 +922,7 @@ if __name__ == '__main__':
976
  return;
977
  }
978
  } catch(e) {
979
- // Cross-origin => not necessarily an error
980
  }
981
 
982
  const rect = item.iframe.getBoundingClientRect();
@@ -996,6 +942,7 @@ if __name__ == '__main__':
996
  delete this.checkQueue[spaceKey];
997
  return;
998
  }
 
999
  const nextDelay = this.checkInterval * Math.pow(1.5, item.attempts - 1);
1000
  setTimeout(() => this.checkIframeStatus(spaceKey), nextDelay);
1001
 
@@ -1016,7 +963,7 @@ if __name__ == '__main__':
1016
  state.statsVisible = !state.statsVisible;
1017
  elements.statsContent.classList.toggle('open', state.statsVisible);
1018
  elements.statsToggle.textContent = state.statsVisible ? 'Hide Stats' : 'Show Stats';
1019
-
1020
  if (state.statsVisible && state.topOwners.length > 0) {
1021
  renderCreatorStats();
1022
  }
@@ -1102,9 +1049,7 @@ if __name__ == '__main__':
1102
  const timeoutPromise = new Promise((_, reject) =>
1103
  setTimeout(() => reject(new Error('Request timeout')), 30000)
1104
  );
1105
- const fetchPromise = fetch(
1106
- `/api/trending-spaces?search=${encodeURIComponent(searchText)}&offset=${offset}&limit=${state.itemsPerPage}`
1107
- );
1108
  const response = await Promise.race([fetchPromise, timeoutPromise]);
1109
  const data = await response.json();
1110
 
@@ -1142,7 +1087,7 @@ if __name__ == '__main__':
1142
  elements.pagination.innerHTML = '';
1143
  const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
1144
 
1145
- // Previous page
1146
  const prevButton = document.createElement('button');
1147
  prevButton.className = 'pagination-button';
1148
  prevButton.textContent = 'Previous';
@@ -1154,6 +1099,7 @@ if __name__ == '__main__':
1154
  });
1155
  elements.pagination.appendChild(prevButton);
1156
 
 
1157
  const maxButtons = 7;
1158
  let startPage = Math.max(0, state.currentPage - Math.floor(maxButtons / 2));
1159
  let endPage = Math.min(totalPages - 1, startPage + maxButtons - 1);
@@ -1174,7 +1120,7 @@ if __name__ == '__main__':
1174
  elements.pagination.appendChild(pageButton);
1175
  }
1176
 
1177
- // Next page
1178
  const nextButton = document.createElement('button');
1179
  nextButton.className = 'pagination-button';
1180
  nextButton.textContent = 'Next';
@@ -1189,14 +1135,13 @@ if __name__ == '__main__':
1189
 
1190
  function handleIframeError(iframe, owner, name, title) {
1191
  const container = iframe.parentNode;
1192
-
1193
  const errorPlaceholder = document.createElement('div');
1194
  errorPlaceholder.className = 'error-placeholder';
1195
-
1196
  const errorMessage = document.createElement('p');
1197
  errorMessage.textContent = `"${title}" space couldn't be loaded`;
1198
  errorPlaceholder.appendChild(errorMessage);
1199
-
1200
  const directLink = document.createElement('a');
1201
  directLink.href = `https://huggingface.co/spaces/${owner}/${name}`;
1202
  directLink.target = '_blank';
@@ -1209,12 +1154,12 @@ if __name__ == '__main__':
1209
  directLink.style.borderRadius = '5px';
1210
  directLink.style.fontWeight = '600';
1211
  errorPlaceholder.appendChild(directLink);
1212
-
1213
  iframe.style.display = 'none';
1214
  container.appendChild(errorPlaceholder);
1215
  }
1216
 
1217
- // 카드 목록: 로봇 이모지로 대체
1218
  function renderGrid(spaces) {
1219
  elements.gridContainer.innerHTML = '';
1220
 
@@ -1239,75 +1184,64 @@ if __name__ == '__main__':
1239
  const gridItem = document.createElement('div');
1240
  gridItem.className = 'grid-item';
1241
 
 
1242
  const headerDiv = document.createElement('div');
1243
  headerDiv.className = 'grid-header';
1244
 
1245
- // [이모지 + 제목 + ZERO GPU 배지]
1246
- const spaceHeader = document.createElement('div');
1247
- spaceHeader.className = 'space-header';
1248
-
1249
- // 🤖 이모지
1250
- const emojiAvatar = document.createElement('div');
1251
- emojiAvatar.className = 'emoji-avatar';
1252
- emojiAvatar.textContent = '🤖';
1253
- spaceHeader.appendChild(emojiAvatar);
1254
 
1255
- // title + badge
1256
- const titleWrapper = document.createElement('div');
1257
- titleWrapper.style.display = 'flex';
1258
- titleWrapper.style.alignItems = 'center';
 
1259
 
 
1260
  const titleEl = document.createElement('h3');
1261
- titleEl.className = 'space-title';
1262
  titleEl.textContent = title;
1263
  titleEl.title = title;
1264
- titleWrapper.appendChild(titleEl);
1265
 
 
1266
  const zeroGpuBadge = document.createElement('span');
1267
- zeroGpuBadge.className = 'zero-gpu-badge';
 
 
1268
  zeroGpuBadge.textContent = 'ZERO GPU';
1269
- titleWrapper.appendChild(zeroGpuBadge);
1270
 
1271
- spaceHeader.appendChild(titleWrapper);
1272
- headerDiv.appendChild(spaceHeader);
1273
 
1274
- // rank + author + likes
1275
  const metaInfo = document.createElement('div');
1276
  metaInfo.className = 'grid-meta';
1277
- metaInfo.style.display = 'flex';
1278
- metaInfo.style.justifyContent = 'space-between';
1279
- metaInfo.style.alignItems = 'center';
1280
- metaInfo.style.marginTop = '6px';
1281
 
1282
- const leftMeta = document.createElement('div');
1283
- const rankBadge = document.createElement('div');
1284
- rankBadge.className = 'rank-badge';
1285
- rankBadge.textContent = `#${rank}`;
1286
- leftMeta.appendChild(rankBadge);
1287
-
1288
- const authorSpan = document.createElement('span');
1289
- authorSpan.className = 'author-name';
1290
- authorSpan.style.marginLeft = '8px';
1291
- authorSpan.textContent = `by ${author_name}`;
1292
- leftMeta.appendChild(authorSpan);
1293
-
1294
- metaInfo.appendChild(leftMeta);
1295
 
1296
  const likesDiv = document.createElement('div');
1297
- likesDiv.className = 'likes-wrapper';
1298
- likesDiv.innerHTML = `<span class="likes-heart">♥</span><span>${likes_count}</span>`;
1299
  metaInfo.appendChild(likesDiv);
1300
 
1301
  headerDiv.appendChild(metaInfo);
1302
- gridItem.appendChild(headerDiv);
1303
 
1304
  if (description) {
1305
  const descP = document.createElement('p');
1306
- descP.className = 'desc-text';
 
 
1307
  descP.textContent = description;
1308
- gridItem.appendChild(descP);
1309
  }
1310
 
 
 
 
1311
  const content = document.createElement('div');
1312
  content.className = 'grid-content';
1313
 
@@ -1342,9 +1276,9 @@ if __name__ == '__main__':
1342
  iframeContainer.appendChild(iframe);
1343
  content.appendChild(iframeContainer);
1344
 
 
1345
  const actions = document.createElement('div');
1346
  actions.className = 'grid-actions';
1347
-
1348
  const linkEl = document.createElement('a');
1349
  linkEl.href = url;
1350
  linkEl.target = '_blank';
@@ -1354,6 +1288,7 @@ if __name__ == '__main__':
1354
 
1355
  gridItem.appendChild(content);
1356
  gridItem.appendChild(actions);
 
1357
  elements.gridContainer.appendChild(gridItem);
1358
 
1359
  } catch (err) {
@@ -1362,6 +1297,7 @@ if __name__ == '__main__':
1362
  });
1363
  }
1364
 
 
1365
  function renderFixedGrid() {
1366
  fixedGridContainer.innerHTML = '';
1367
 
@@ -1415,27 +1351,25 @@ if __name__ == '__main__':
1415
  const headerTop = document.createElement('div');
1416
  headerTop.className = 'grid-header-top';
1417
 
1418
- // 이모지
1419
- const leftWrapper = document.createElement('div');
1420
- leftWrapper.style.display = 'flex';
1421
- leftWrapper.style.alignItems = 'center';
1422
-
1423
- const emojiAvatar = document.createElement('div');
1424
- emojiAvatar.className = 'emoji-avatar';
1425
- emojiAvatar.textContent = '🤖';
1426
- leftWrapper.appendChild(emojiAvatar);
1427
 
 
1428
  const titleEl = document.createElement('h3');
1429
  titleEl.textContent = title;
1430
  titleEl.title = title;
1431
- leftWrapper.appendChild(titleEl);
1432
-
1433
- headerTop.appendChild(leftWrapper);
1434
 
1435
- const rankBadge = document.createElement('div');
1436
- rankBadge.className = 'rank-badge';
1437
- rankBadge.textContent = `#${rank}`;
1438
- headerTop.appendChild(rankBadge);
 
 
 
1439
 
1440
  header.appendChild(headerTop);
1441
 
@@ -1506,7 +1440,6 @@ if __name__ == '__main__':
1506
  });
1507
  }
1508
 
1509
- // Tab switching
1510
  tabTrendingButton.addEventListener('click', () => {
1511
  tabTrendingButton.classList.add('active');
1512
  tabFixedButton.classList.remove('active');
@@ -1577,5 +1510,5 @@ if __name__ == '__main__':
1577
  </html>
1578
  ''')
1579
 
1580
- # Flask run
1581
  app.run(host='0.0.0.0', port=7860)
 
29
  })
30
  return spaces
31
 
32
+ # Function to fetch Zero-GPU (CPU-based) Spaces, TRENDING order, top 500
33
  def fetch_trending_spaces(offset=0, limit=72):
34
  """
35
+ 1) hardware=cpu
36
+ 2) sort=trending
37
+ 3) 상위 500개만 남김 (그 이후 offset/limit 페이징)
38
  """
39
  try:
40
  url = "https://huggingface.co/api/spaces"
41
  params = {
42
+ "limit": 10000, # 일단 많이 가져오기
43
+ "hardware": "cpu",
44
+ "sort": "trending" # 트렌딩 정렬
45
  }
46
  response = requests.get(url, params=params, timeout=30)
47
 
 
55
  and space.get('id', '').split('/', 1)[0] != 'None'
56
  ]
57
 
58
+ # 트렌딩 중 상위 500개만 남김
59
+ if len(filtered_spaces) > 500:
60
+ filtered_spaces = filtered_spaces[:500]
61
 
62
+ total = len(filtered_spaces)
63
+
64
+ # 페이징
65
+ start = min(offset, total)
66
+ end = min(offset + limit, total)
67
+
68
+ print(f"[fetch_trending_spaces] Zero-GPU TRENDING Spaces: {total}개 중, "
69
+ f"{start}~{end-1} 반환")
70
 
71
  return {
72
  'spaces': filtered_spaces[start:end],
73
+ 'total': total,
74
  'offset': offset,
75
  'limit': limit,
76
+ 'all_spaces': filtered_spaces # 상위 500 전체
77
  }
78
  else:
79
  print(f"Error fetching spaces: {response.status_code}")
 
88
 
89
  except Exception as e:
90
  print(f"Exception when fetching spaces: {e}")
 
91
  return {
92
  'spaces': generate_dummy_spaces(limit),
93
  'total': 200,
 
100
  def transform_url(owner, name):
101
  """
102
  Hugging Face Space -> 서브도메인 접근 URL
103
+ 예) huggingface.co/spaces/owner/spaceName -> owner-spacename.hf.space
104
  """
105
  name = name.replace('.', '-').replace('_', '-')
106
  owner = owner.lower()
 
110
  # Get space details
111
  def get_space_details(space_data, index, offset):
112
  """
113
+ space의 주요 정보(타이틀, likes, owner, url 등) 추출
114
  """
115
  try:
116
  if '/' in space_data.get('id', ''):
 
119
  owner = space_data.get('owner', '')
120
  name = space_data.get('id', '')
121
 
 
122
  if owner == 'None' or name == 'None':
123
  return None
124
 
 
143
  'description': short_desc,
144
  'avatar_url': avatar_url,
145
  'author_name': author_name,
146
+ # 'rank'는 (offset + index + 1)로 표시
147
  'rank': offset + index + 1
148
  }
149
  except Exception as e:
150
  print(f"Error processing space data: {e}")
 
151
  return {
152
  'url': 'https://huggingface.co/spaces',
153
  'embedUrl': 'https://huggingface.co/spaces',
 
164
  # Get owner statistics from all spaces
165
  def get_owner_stats(all_spaces):
166
  """
167
+ all_spaces: 상위 500개 트렌딩 CPU 스페이스
168
+ -> owner별로 개인지 세서 상위 30명 반환
169
  """
170
  owners = []
171
  for space in all_spaces:
 
181
  top_owners = owner_counts.most_common(30)
182
  return top_owners
183
 
184
+ from flask import render_template
185
+
186
  @app.route('/')
187
  def home():
 
 
 
188
  return render_template('index.html')
189
 
190
  @app.route('/api/trending-spaces', methods=['GET'])
191
  def trending_spaces():
192
  """
193
+ 1) 상위 500 트렌딩 CPU 스페이스
194
+ 2) offset, limit으로 페이지 나눠서 반환
195
+ 3) top 30 creator stats
196
  """
197
  search_query = request.args.get('search', '').lower()
198
  offset = int(request.args.get('offset', 0))
199
  limit = int(request.args.get('limit', 72))
200
 
201
+ # 500개 (CPU+trending) 받아오기
202
  spaces_data = fetch_trending_spaces(offset, limit)
203
 
204
  results = []
 
217
 
218
  results.append(space_info)
219
 
 
220
  top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
221
 
222
  return jsonify({
 
229
 
230
  if __name__ == '__main__':
231
  """
232
+ Flask 실행 시, templates/index.html 생성
233
  """
234
  os.makedirs('templates', exist_ok=True)
235
 
 
236
  with open('templates/index.html', 'w', encoding='utf-8') as f:
237
  f.write('''<!DOCTYPE html>
238
  <html lang="en">
239
  <head>
240
  <meta charset="UTF-8">
241
+ <title>Zero-GPU Trending (Top 500)</title>
242
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
243
  <style>
244
  /* Google Fonts & Base Styling */
 
271
  }
272
 
273
  body {
274
+ font-family: 'Nunito', sans-serif;
275
  line-height: 1.6;
276
  color: var(--text-primary);
277
  background-color: #f8f9fa;
 
760
  width: 100%;
761
  height: 500px;
762
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
763
  </style>
764
  </head>
765
  <body>
 
776
 
777
  <div class="mac-content">
778
  <div class="header">
779
+ <h1>Zero GPU Spaces (Top 500 Trending)</h1>
780
+ <p>Discover trending CPU-based spaces from Hugging Face. Only top 500 are considered!</p>
781
  </div>
782
 
783
  <!-- Tab Navigation -->
 
786
  <button id="tabFixedButton" class="tab-button">Fixed Tab</button>
787
  </div>
788
 
789
+ <!-- Trending Tab Content -->
790
  <div id="trendingTab" class="tab-content active">
791
  <div class="stats-window mac-window">
792
  <div class="mac-toolbar">
 
799
  </div>
800
  <div class="mac-content">
801
  <div class="stats-header">
802
+ <div class="stats-title">Top 30 Creators by Space Count (of Top 500)</div>
803
  <button id="statsToggle" class="stats-toggle">Show Stats</button>
804
  </div>
805
  <div id="statsContent" class="stats-content">
 
842
  </div>
843
 
844
  <script>
845
+ // DOM References
846
  const elements = {
847
  gridContainer: document.getElementById('gridContainer'),
848
  loadingIndicator: document.getElementById('loadingIndicator'),
 
883
 
884
  startChecking: function(iframe, owner, name, title, spaceKey) {
885
  this.checkQueue[spaceKey] = {
886
+ iframe, owner, name, title,
 
 
 
887
  attempts: 0,
888
  status: 'loading'
889
  };
 
911
  item.iframe.contentWindow.document.body;
912
  if (hasContent && item.iframe.contentWindow.document.body.innerHTML.length > 100) {
913
  const bodyText = item.iframe.contentWindow.document.body.textContent.toLowerCase();
914
+ if (bodyText.includes('forbidden') || bodyText.includes('404') ||
915
+ bodyText.includes('not found') || bodyText.includes('error')) {
 
 
916
  item.status = 'error';
917
  handleIframeError(item.iframe, item.owner, item.name, item.title);
918
  } else {
 
922
  return;
923
  }
924
  } catch(e) {
925
+ // cross-origin => not necessarily an error
926
  }
927
 
928
  const rect = item.iframe.getBoundingClientRect();
 
942
  delete this.checkQueue[spaceKey];
943
  return;
944
  }
945
+
946
  const nextDelay = this.checkInterval * Math.pow(1.5, item.attempts - 1);
947
  setTimeout(() => this.checkIframeStatus(spaceKey), nextDelay);
948
 
 
963
  state.statsVisible = !state.statsVisible;
964
  elements.statsContent.classList.toggle('open', state.statsVisible);
965
  elements.statsToggle.textContent = state.statsVisible ? 'Hide Stats' : 'Show Stats';
966
+
967
  if (state.statsVisible && state.topOwners.length > 0) {
968
  renderCreatorStats();
969
  }
 
1049
  const timeoutPromise = new Promise((_, reject) =>
1050
  setTimeout(() => reject(new Error('Request timeout')), 30000)
1051
  );
1052
+ const fetchPromise = fetch(`/api/trending-spaces?search=${encodeURIComponent(searchText)}&offset=${offset}&limit=${state.itemsPerPage}`);
 
 
1053
  const response = await Promise.race([fetchPromise, timeoutPromise]);
1054
  const data = await response.json();
1055
 
 
1087
  elements.pagination.innerHTML = '';
1088
  const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
1089
 
1090
+ // Prev
1091
  const prevButton = document.createElement('button');
1092
  prevButton.className = 'pagination-button';
1093
  prevButton.textContent = 'Previous';
 
1099
  });
1100
  elements.pagination.appendChild(prevButton);
1101
 
1102
+ // Middle pages
1103
  const maxButtons = 7;
1104
  let startPage = Math.max(0, state.currentPage - Math.floor(maxButtons / 2));
1105
  let endPage = Math.min(totalPages - 1, startPage + maxButtons - 1);
 
1120
  elements.pagination.appendChild(pageButton);
1121
  }
1122
 
1123
+ // Next
1124
  const nextButton = document.createElement('button');
1125
  nextButton.className = 'pagination-button';
1126
  nextButton.textContent = 'Next';
 
1135
 
1136
  function handleIframeError(iframe, owner, name, title) {
1137
  const container = iframe.parentNode;
 
1138
  const errorPlaceholder = document.createElement('div');
1139
  errorPlaceholder.className = 'error-placeholder';
1140
+
1141
  const errorMessage = document.createElement('p');
1142
  errorMessage.textContent = `"${title}" space couldn't be loaded`;
1143
  errorPlaceholder.appendChild(errorMessage);
1144
+
1145
  const directLink = document.createElement('a');
1146
  directLink.href = `https://huggingface.co/spaces/${owner}/${name}`;
1147
  directLink.target = '_blank';
 
1154
  directLink.style.borderRadius = '5px';
1155
  directLink.style.fontWeight = '600';
1156
  errorPlaceholder.appendChild(directLink);
1157
+
1158
  iframe.style.display = 'none';
1159
  container.appendChild(errorPlaceholder);
1160
  }
1161
 
1162
+ // renderGrid: 로봇 이모지 제거 -> rankBadge를 그 자리에 배치
1163
  function renderGrid(spaces) {
1164
  elements.gridContainer.innerHTML = '';
1165
 
 
1184
  const gridItem = document.createElement('div');
1185
  gridItem.className = 'grid-item';
1186
 
1187
+ // 상단 header
1188
  const headerDiv = document.createElement('div');
1189
  headerDiv.className = 'grid-header';
1190
 
1191
+ // grid-header-top (rank + title + zero gpu)
1192
+ const headerTop = document.createElement('div');
1193
+ headerTop.className = 'grid-header-top';
 
 
 
 
 
 
1194
 
1195
+ // 왼쪽: rank badge
1196
+ const rankBadge = document.createElement('div');
1197
+ rankBadge.className = 'rank-badge';
1198
+ rankBadge.textContent = `#${rank}`;
1199
+ headerTop.appendChild(rankBadge);
1200
 
1201
+ // 중간: title
1202
  const titleEl = document.createElement('h3');
 
1203
  titleEl.textContent = title;
1204
  titleEl.title = title;
1205
+ headerTop.appendChild(titleEl);
1206
 
1207
+ // 오른쪽(Zero GPU)
1208
  const zeroGpuBadge = document.createElement('span');
1209
+ zeroGpuBadge.className = 'rank-badge';
1210
+ zeroGpuBadge.style.backgroundColor = '#319795';
1211
+ zeroGpuBadge.style.marginLeft = '10px';
1212
  zeroGpuBadge.textContent = 'ZERO GPU';
1213
+ headerTop.appendChild(zeroGpuBadge);
1214
 
1215
+ headerDiv.appendChild(headerTop);
 
1216
 
1217
+ // 둘째줄: meta (owner / likes)
1218
  const metaInfo = document.createElement('div');
1219
  metaInfo.className = 'grid-meta';
 
 
 
 
1220
 
1221
+ const ownerEl = document.createElement('div');
1222
+ ownerEl.className = 'owner-info';
1223
+ ownerEl.textContent = `by ${author_name}`;
1224
+ metaInfo.appendChild(ownerEl);
 
 
 
 
 
 
 
 
 
1225
 
1226
  const likesDiv = document.createElement('div');
1227
+ likesDiv.className = 'likes-counter';
1228
+ likesDiv.innerHTML = '♥ <span>' + likes_count + '</span>';
1229
  metaInfo.appendChild(likesDiv);
1230
 
1231
  headerDiv.appendChild(metaInfo);
 
1232
 
1233
  if (description) {
1234
  const descP = document.createElement('p');
1235
+ descP.style.fontSize = '0.9rem';
1236
+ descP.style.marginTop = '6px';
1237
+ descP.style.color = '#444';
1238
  descP.textContent = description;
1239
+ headerDiv.appendChild(descP);
1240
  }
1241
 
1242
+ gridItem.appendChild(headerDiv);
1243
+
1244
+ // iframe container
1245
  const content = document.createElement('div');
1246
  content.className = 'grid-content';
1247
 
 
1276
  iframeContainer.appendChild(iframe);
1277
  content.appendChild(iframeContainer);
1278
 
1279
+ // actions
1280
  const actions = document.createElement('div');
1281
  actions.className = 'grid-actions';
 
1282
  const linkEl = document.createElement('a');
1283
  linkEl.href = url;
1284
  linkEl.target = '_blank';
 
1288
 
1289
  gridItem.appendChild(content);
1290
  gridItem.appendChild(actions);
1291
+
1292
  elements.gridContainer.appendChild(gridItem);
1293
 
1294
  } catch (err) {
 
1297
  });
1298
  }
1299
 
1300
+ // Fixed Tab (예시)
1301
  function renderFixedGrid() {
1302
  fixedGridContainer.innerHTML = '';
1303
 
 
1351
  const headerTop = document.createElement('div');
1352
  headerTop.className = 'grid-header-top';
1353
 
1354
+ // rank
1355
+ const rankBadge = document.createElement('div');
1356
+ rankBadge.className = 'rank-badge';
1357
+ rankBadge.textContent = `#${rank}`;
1358
+ headerTop.appendChild(rankBadge);
 
 
 
 
1359
 
1360
+ // title
1361
  const titleEl = document.createElement('h3');
1362
  titleEl.textContent = title;
1363
  titleEl.title = title;
1364
+ headerTop.appendChild(titleEl);
 
 
1365
 
1366
+ // zero gpu (임의)
1367
+ const zeroBadge = document.createElement('div');
1368
+ zeroBadge.className = 'rank-badge';
1369
+ zeroBadge.style.backgroundColor = '#319795';
1370
+ zeroBadge.style.marginLeft = '10px';
1371
+ zeroBadge.textContent = 'ZERO GPU';
1372
+ headerTop.appendChild(zeroBadge);
1373
 
1374
  header.appendChild(headerTop);
1375
 
 
1440
  });
1441
  }
1442
 
 
1443
  tabTrendingButton.addEventListener('click', () => {
1444
  tabTrendingButton.classList.add('active');
1445
  tabFixedButton.classList.remove('active');
 
1510
  </html>
1511
  ''')
1512
 
1513
+ # Flask Run
1514
  app.run(host='0.0.0.0', port=7860)