openfree commited on
Commit
a371956
ยท
verified ยท
1 Parent(s): 337a659

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +46 -145
app.py CHANGED
@@ -32,15 +32,14 @@ def generate_dummy_spaces(count):
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 ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ
36
- GPU๊ฐ€ ์—†๋Š”(=CPU ์ „์šฉ) ์ŠคํŽ˜์ด์Šค๋งŒ ํ•„ํ„ฐ๋ง
37
  """
38
  try:
39
  url = "https://huggingface.co/api/spaces"
40
  params = {
41
  "limit": 10000, # ๋” ๋งŽ์ด ๊ฐ€์ ธ์˜ค๊ธฐ
42
  "hardware": "cpu" # <-- Zero GPU(=CPU) ํ•„ํ„ฐ ์ ์šฉ
43
- # "sort": "trending", # ํ•„์š”์‹œ ์ถ”๊ฐ€ (HF API ์ง€์› ์—ฌ๋ถ€์— ๋”ฐ๋ผ)
44
  }
45
  response = requests.get(url, params=params, timeout=30)
46
 
@@ -54,7 +53,7 @@ def fetch_trending_spaces(offset=0, limit=72):
54
  and space.get('id', '').split('/', 1)[0] != 'None'
55
  ]
56
 
57
- # Slice according to requested offset and limit
58
  start = min(offset, len(filtered_spaces))
59
  end = min(offset + limit, len(filtered_spaces))
60
 
@@ -66,7 +65,7 @@ def fetch_trending_spaces(offset=0, limit=72):
66
  'total': len(filtered_spaces),
67
  'offset': offset,
68
  'limit': limit,
69
- 'all_spaces': filtered_spaces # ํ†ต๊ณ„ ์‚ฐ์ถœ์šฉ
70
  }
71
  else:
72
  print(f"Error fetching spaces: {response.status_code}")
@@ -94,23 +93,17 @@ def fetch_trending_spaces(offset=0, limit=72):
94
  def transform_url(owner, name):
95
  """
96
  Hugging Face Space -> ์„œ๋ธŒ๋„๋ฉ”์ธ ์ ‘๊ทผ URL
97
- ์˜ˆ) huggingface.co/spaces/owner/spaceName -> owner-spacename.hf.space
98
  """
99
- # 1. Replace '.' with '-'
100
- name = name.replace('.', '-')
101
- # 2. Replace '_' with '-'
102
- name = name.replace('_', '-')
103
- # 3. Convert to lowercase
104
  owner = owner.lower()
105
  name = name.lower()
106
-
107
  return f"https://{owner}-{name}.hf.space"
108
 
109
  # Get space details
110
  def get_space_details(space_data, index, offset):
111
  """
112
- ์›๋ณธ ์ฝ”๋“œ์—์„œ ์ˆ˜์ •:
113
- - description, avatar_url, author_name ๋“ฑ ์ถ”๊ฐ€ ํ•„๋“œ๋ฅผ ์ถ”์ถœ
114
  """
115
  try:
116
  if '/' in space_data.get('id', ''):
@@ -119,24 +112,17 @@ def get_space_details(space_data, index, offset):
119
  owner = space_data.get('owner', '')
120
  name = space_data.get('id', '')
121
 
122
- # Ignore if contains None
123
  if owner == 'None' or name == 'None':
124
  return None
125
 
126
- # Construct URLs
127
  original_url = f"https://huggingface.co/spaces/{owner}/{name}"
128
  embed_url = transform_url(owner, name)
129
 
130
- # Likes count
131
  likes_count = space_data.get('likes', 0)
132
-
133
- # Title
134
  title = space_data.get('title') or name
135
-
136
- # Description
137
  short_desc = space_data.get('description', '')
138
 
139
- # User info (avatar, name)
140
  user_info = space_data.get('user', {})
141
  avatar_url = user_info.get('avatar_url', '')
142
  author_name = user_info.get('name') or owner
@@ -155,7 +141,7 @@ def get_space_details(space_data, index, offset):
155
  }
156
  except Exception as e:
157
  print(f"Error processing space data: {e}")
158
- # Return basic object even if error occurs
159
  return {
160
  'url': 'https://huggingface.co/spaces',
161
  'embedUrl': 'https://huggingface.co/spaces',
@@ -172,7 +158,8 @@ def get_space_details(space_data, index, offset):
172
  # Get owner statistics from all spaces
173
  def get_owner_stats(all_spaces):
174
  """
175
- ๋ชจ๋“  ์ŠคํŽ˜์ด์Šค์—์„œ owner ๋“ฑ์žฅ ๋นˆ๋„ ์ƒ์œ„ 30๋ช… ์ถ”์ถœ
 
176
  """
177
  owners = []
178
  for space in all_spaces:
@@ -184,43 +171,36 @@ def get_owner_stats(all_spaces):
184
  if owner != 'None':
185
  owners.append(owner)
186
 
187
- # Count occurrences of each owner
188
  owner_counts = Counter(owners)
189
-
190
- # Get top 30 owners by count
191
  top_owners = owner_counts.most_common(30)
192
-
193
  return top_owners
194
 
195
- # Homepage route
196
  @app.route('/')
197
  def home():
198
  """
199
- index.html ํ…œํ”Œ๋ฆฟ ๋ Œ๋”๋ง (๋ฉ”์ธ ํŽ˜์ด์ง€)
200
  """
201
  return render_template('index.html')
202
 
203
- # Zero-GPU spaces API (์›๋ณธ: 'trending-spaces' -> ์ด์ œ CPU only)
204
  @app.route('/api/trending-spaces', methods=['GET'])
205
  def trending_spaces():
206
  """
207
- hardware=cpu ์ŠคํŽ˜์ด์Šค ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์™€ ๊ฒ€์ƒ‰, ํŽ˜์ด์ง•, ํ†ต๊ณ„ ๋“ฑ์„ ์ ์šฉ
 
208
  """
209
  search_query = request.args.get('search', '').lower()
210
  offset = int(request.args.get('offset', 0))
211
- limit = int(request.args.get('limit', 72)) # Default 72
212
 
213
- # Fetch zero-gpu (cpu) spaces
214
  spaces_data = fetch_trending_spaces(offset, limit)
215
 
216
- # Process and filter spaces
217
  results = []
218
  for index, space_data in enumerate(spaces_data['spaces']):
219
  space_info = get_space_details(space_data, index, offset)
220
  if not space_info:
221
  continue
222
 
223
- # Apply search filter if needed
224
  if search_query:
225
  if (search_query not in space_info['title'].lower()
226
  and search_query not in space_info['owner'].lower()
@@ -230,7 +210,7 @@ def trending_spaces():
230
 
231
  results.append(space_info)
232
 
233
- # Get owner statistics for all spaces
234
  top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
235
 
236
  return jsonify({
@@ -245,10 +225,9 @@ if __name__ == '__main__':
245
  """
246
  ์„œ๋ฒ„ ๊ตฌ๋™ ์‹œ, templates/index.html ํŒŒ์ผ์„ ์ƒ์„ฑ ํ›„ Flask ์‹คํ–‰
247
  """
248
- # Create templates folder if not exists
249
  os.makedirs('templates', exist_ok=True)
250
 
251
- # index.html ์ „์ฒด๋ฅผ ์ƒˆ๋กœ ์ž‘์„ฑ
252
  with open('templates/index.html', 'w', encoding='utf-8') as f:
253
  f.write('''<!DOCTYPE html>
254
  <html lang="en">
@@ -819,70 +798,7 @@ if __name__ == '__main__':
819
  opacity: 0;
820
  }
821
 
822
- /* ์ถ”๊ฐ€ ๋ ˆ์ด์•„์›ƒ ์ˆ˜์ •(์•„๋ฐ”ํƒ€, ZERO GPU ๋ฑƒ์ง€ ๋“ฑ)์„ ์œ„ํ•ด
823
- ์•„๋ž˜ ํด๋ž˜์Šค๋“ค์„ ์ผ๋ถ€ ์ถ”๊ฐ€/์ˆ˜์ •ํ•ด๋„ ์ข‹์ง€๋งŒ
824
- ์˜ˆ์‹œ๋Š” ์œ„ ์˜์—ญ์—์„œ ์ด๋ฏธ ์ถฉ๋ถ„ํžˆ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ์ƒ๋žต */
825
-
826
- /* ๋‹ค์Œ ๋ถ€๋ถ„์€ Zero GPU Spaces์šฉ ์นด๋“œ ๊ตฌ์กฐ์—์„œ ํ™œ์šฉ */
827
- .space-header {
828
- display: flex;
829
- align-items: center;
830
- gap: 10px;
831
- margin-bottom: 4px;
832
- }
833
- .avatar-img {
834
- width: 32px;
835
- height: 32px;
836
- border-radius: 50%;
837
- object-fit: cover;
838
- border: 1px solid #ccc;
839
- }
840
- .space-title {
841
- font-size: 1rem;
842
- font-weight: 600;
843
- margin: 0;
844
- overflow: hidden;
845
- text-overflow: ellipsis;
846
- white-space: nowrap;
847
- max-width: 200px;
848
- }
849
- .zero-gpu-badge {
850
- font-size: 0.7rem;
851
- background-color: #e6fffa;
852
- color: #319795;
853
- border: 1px solid #81e6d9;
854
- border-radius: 6px;
855
- padding: 2px 6px;
856
- font-weight: 600;
857
- margin-left: 8px;
858
- }
859
- .desc-text {
860
- font-size: 0.85rem;
861
- color: #444;
862
- margin: 4px 0;
863
- line-clamp: 2;
864
- display: -webkit-box;
865
- -webkit-box-orient: vertical;
866
- overflow: hidden;
867
- }
868
- .author-name {
869
- font-size: 0.8rem;
870
- color: #666;
871
- }
872
- .likes-wrapper {
873
- display: flex;
874
- align-items: center;
875
- gap: 4px;
876
- color: #e53e3e;
877
- font-weight: bold;
878
- font-size: 0.85rem;
879
- }
880
- .likes-heart {
881
- font-size: 1rem;
882
- line-height: 1rem;
883
- color: #f56565;
884
- }
885
- /* ์ด๋ชจ์ง€ ์ „์šฉ ์Šคํƒ€์ผ (์„ ํƒ์‚ฌํ•ญ) */
886
  .emoji-avatar {
887
  font-size: 1.2rem;
888
  width: 32px;
@@ -909,15 +825,14 @@ if __name__ == '__main__':
909
 
910
  <div class="mac-content">
911
  <div class="header">
912
- <!-- ์ฒซ ๋ฒˆ์งธ ํƒญ ์ œ๋ชฉ์„ Zero GPU Spaces๋กœ ๋ณ€๊ฒฝ -->
913
- <h1>ZeroGPU Spaces Leaderboard</h1>
914
- <p>Discover Zero GPU(Shared A100) spaces from Hugging Face</p>
915
  </div>
916
 
917
  <!-- Tab Navigation -->
918
  <div class="tab-nav">
919
- <button id="tabTrendingButton" class="tab-button active">Trending</button>
920
- <button id="tabFixedButton" class="tab-button">Picks</button>
921
  </div>
922
 
923
  <!-- Trending(Zero GPU) Tab Content -->
@@ -957,7 +872,7 @@ if __name__ == '__main__':
957
  <div id="pagination" class="pagination"></div>
958
  </div>
959
 
960
- <!-- Fixed Tab Content (๊ธฐ์กด ์˜ˆ์‹œ ์œ ์ง€) -->
961
  <div id="fixedTab" class="tab-content">
962
  <div id="fixedGrid" class="grid-container"></div>
963
  </div>
@@ -1009,7 +924,7 @@ if __name__ == '__main__':
1009
  iframeStatuses: {}
1010
  };
1011
 
1012
- // Advanced iframe loader for better error detection
1013
  const iframeLoader = {
1014
  checkQueue: {},
1015
  maxAttempts: 5,
@@ -1029,29 +944,29 @@ if __name__ == '__main__':
1029
 
1030
  checkIframeStatus: function(spaceKey) {
1031
  if (!this.checkQueue[spaceKey]) return;
1032
-
1033
  const item = this.checkQueue[spaceKey];
1034
  if (item.status !== 'loading') {
1035
  delete this.checkQueue[spaceKey];
1036
  return;
1037
  }
1038
  item.attempts++;
1039
-
1040
  try {
1041
  if (!item.iframe || !item.iframe.parentNode) {
1042
  delete this.checkQueue[spaceKey];
1043
  return;
1044
  }
1045
 
1046
- // Check if content loaded
1047
  try {
1048
- const hasContent = item.iframe.contentWindow &&
1049
- item.iframe.contentWindow.document &&
1050
  item.iframe.contentWindow.document.body;
1051
  if (hasContent && item.iframe.contentWindow.document.body.innerHTML.length > 100) {
1052
  const bodyText = item.iframe.contentWindow.document.body.textContent.toLowerCase();
1053
- if (bodyText.includes('forbidden') || bodyText.includes('404') ||
1054
- bodyText.includes('not found') || bodyText.includes('error')) {
 
 
1055
  item.status = 'error';
1056
  handleIframeError(item.iframe, item.owner, item.name, item.title);
1057
  } else {
@@ -1061,10 +976,9 @@ if __name__ == '__main__':
1061
  return;
1062
  }
1063
  } catch(e) {
1064
- // Cross-origin issues can happen; not always an error
1065
  }
1066
 
1067
- // Check if iframe is visible
1068
  const rect = item.iframe.getBoundingClientRect();
1069
  if (rect.width > 50 && rect.height > 50 && item.attempts > 2) {
1070
  item.status = 'success';
@@ -1072,7 +986,6 @@ if __name__ == '__main__':
1072
  return;
1073
  }
1074
 
1075
- // If max attempts reached
1076
  if (item.attempts >= this.maxAttempts) {
1077
  if (item.iframe.offsetWidth > 0 && item.iframe.offsetHeight > 0) {
1078
  item.status = 'success';
@@ -1083,8 +996,6 @@ if __name__ == '__main__':
1083
  delete this.checkQueue[spaceKey];
1084
  return;
1085
  }
1086
-
1087
- // Re-check after some delay
1088
  const nextDelay = this.checkInterval * Math.pow(1.5, item.attempts - 1);
1089
  setTimeout(() => this.checkIframeStatus(spaceKey), nextDelay);
1090
 
@@ -1133,7 +1044,7 @@ if __name__ == '__main__':
1133
  label: 'Number of Spaces',
1134
  data: data,
1135
  backgroundColor: colors,
1136
- borderColor: colors.map(color => color.replace('0.7', '1')),
1137
  borderWidth: 1
1138
  }]
1139
  },
@@ -1303,7 +1214,7 @@ if __name__ == '__main__':
1303
  container.appendChild(errorPlaceholder);
1304
  }
1305
 
1306
- // ์—ฌ๊ธฐ์„œ๋ถ€ํ„ฐ ์นด๋“œ HTML ๊ตฌ์กฐ (Zero-GPU ์ „์šฉ) ๋ Œ๋”๋ง
1307
  function renderGrid(spaces) {
1308
  elements.gridContainer.innerHTML = '';
1309
 
@@ -1328,21 +1239,20 @@ if __name__ == '__main__':
1328
  const gridItem = document.createElement('div');
1329
  gridItem.className = 'grid-item';
1330
 
1331
- // ์ƒ๋‹จ ํ—ค๋”
1332
  const headerDiv = document.createElement('div');
1333
  headerDiv.className = 'grid-header';
1334
 
1335
- // space-header (๋กœ๋ด‡ ์ด๋ชจ์ง€ + ์ œ๋ชฉ + Zero GPU ๋ฐฐ์ง€)
1336
  const spaceHeader = document.createElement('div');
1337
  spaceHeader.className = 'space-header';
1338
 
1339
- // ๋กœ๋ด‡ ์ด๋ชจ์ง€ ๋Œ€์ฒด
1340
  const emojiAvatar = document.createElement('div');
1341
  emojiAvatar.className = 'emoji-avatar';
1342
  emojiAvatar.textContent = '๐Ÿค–';
1343
  spaceHeader.appendChild(emojiAvatar);
1344
 
1345
- // ์ œ๋ชฉ+๋ฐฐ์ง€
1346
  const titleWrapper = document.createElement('div');
1347
  titleWrapper.style.display = 'flex';
1348
  titleWrapper.style.alignItems = 'center';
@@ -1369,9 +1279,7 @@ if __name__ == '__main__':
1369
  metaInfo.style.alignItems = 'center';
1370
  metaInfo.style.marginTop = '6px';
1371
 
1372
- // ์™ผ์ชฝ: rank + author
1373
  const leftMeta = document.createElement('div');
1374
-
1375
  const rankBadge = document.createElement('div');
1376
  rankBadge.className = 'rank-badge';
1377
  rankBadge.textContent = `#${rank}`;
@@ -1385,7 +1293,6 @@ if __name__ == '__main__':
1385
 
1386
  metaInfo.appendChild(leftMeta);
1387
 
1388
- // ์˜ค๋ฅธ์ชฝ: likes
1389
  const likesDiv = document.createElement('div');
1390
  likesDiv.className = 'likes-wrapper';
1391
  likesDiv.innerHTML = `<span class="likes-heart">โ™ฅ</span><span>${likes_count}</span>`;
@@ -1394,7 +1301,6 @@ if __name__ == '__main__':
1394
  headerDiv.appendChild(metaInfo);
1395
  gridItem.appendChild(headerDiv);
1396
 
1397
- // description
1398
  if (description) {
1399
  const descP = document.createElement('p');
1400
  descP.className = 'desc-text';
@@ -1402,7 +1308,6 @@ if __name__ == '__main__':
1402
  gridItem.appendChild(descP);
1403
  }
1404
 
1405
- // iframe container
1406
  const content = document.createElement('div');
1407
  content.className = 'grid-content';
1408
 
@@ -1410,11 +1315,11 @@ if __name__ == '__main__':
1410
  iframeContainer.className = 'iframe-container';
1411
 
1412
  const iframe = document.createElement('iframe');
1413
- iframe.src = embedUrl; // transformed direct URL
1414
  iframe.title = title;
1415
  iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
1416
- iframe.setAttribute('allowfullscreen', '');
1417
- iframe.setAttribute('frameborder', '0');
1418
  iframe.loading = 'lazy';
1419
 
1420
  const spaceKey = `${owner}/${name}`;
@@ -1437,7 +1342,6 @@ if __name__ == '__main__':
1437
  iframeContainer.appendChild(iframe);
1438
  content.appendChild(iframeContainer);
1439
 
1440
- // actions
1441
  const actions = document.createElement('div');
1442
  actions.className = 'grid-actions';
1443
 
@@ -1446,11 +1350,10 @@ if __name__ == '__main__':
1446
  linkEl.target = '_blank';
1447
  linkEl.className = 'open-link';
1448
  linkEl.textContent = 'Open in new window';
1449
-
1450
  actions.appendChild(linkEl);
 
1451
  gridItem.appendChild(content);
1452
  gridItem.appendChild(actions);
1453
-
1454
  elements.gridContainer.appendChild(gridItem);
1455
 
1456
  } catch (err) {
@@ -1460,7 +1363,6 @@ if __name__ == '__main__':
1460
  }
1461
 
1462
  function renderFixedGrid() {
1463
- // Fixed Tab ์˜ˆ์‹œ์šฉ (์›๋ณธ ์ฝ”๋“œ ์˜ˆ์‹œ ์œ ์ง€)
1464
  fixedGridContainer.innerHTML = '';
1465
 
1466
  const staticSpaces = [
@@ -1513,7 +1415,7 @@ if __name__ == '__main__':
1513
  const headerTop = document.createElement('div');
1514
  headerTop.className = 'grid-header-top';
1515
 
1516
- // ๋กœ๋ด‡ ์ด๋ชจ์ง€ + ํƒ€์ดํ‹€ ํ•จ๊ป˜ ํ‘œ์‹œ
1517
  const leftWrapper = document.createElement('div');
1518
  leftWrapper.style.display = 'flex';
1519
  leftWrapper.style.alignItems = 'center';
@@ -1563,8 +1465,8 @@ if __name__ == '__main__':
1563
  iframe.src = "https://" + owner.toLowerCase() + "-" + name.toLowerCase() + ".hf.space";
1564
  iframe.title = title;
1565
  iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
1566
- iframe.setAttribute('allowfullscreen', '');
1567
- iframe.setAttribute('frameborder', '0');
1568
  iframe.loading = 'lazy';
1569
 
1570
  const spaceKey = `${owner}/${name}`;
@@ -1675,6 +1577,5 @@ if __name__ == '__main__':
1675
  </html>
1676
  ''')
1677
 
1678
- # Flask ์•ฑ ์‹คํ–‰ (ํฌํŠธ 7860)
1679
  app.run(host='0.0.0.0', port=7860)
1680
-
 
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
  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
 
 
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}")
 
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()
100
  name = name.lower()
 
101
  return f"https://{owner}-{name}.hf.space"
102
 
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
  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
 
 
119
  original_url = f"https://huggingface.co/spaces/{owner}/{name}"
120
  embed_url = transform_url(owner, name)
121
 
 
122
  likes_count = space_data.get('likes', 0)
 
 
123
  title = space_data.get('title') or name
 
 
124
  short_desc = space_data.get('description', '')
125
 
 
126
  user_info = space_data.get('user', {})
127
  avatar_url = user_info.get('avatar_url', '')
128
  author_name = user_info.get('name') or owner
 
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
  # 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:
 
171
  if owner != 'None':
172
  owners.append(owner)
173
 
 
174
  owner_counts = Counter(owners)
 
 
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 = []
198
  for index, space_data in enumerate(spaces_data['spaces']):
199
  space_info = get_space_details(space_data, index, offset)
200
  if not space_info:
201
  continue
202
 
203
+ # ๊ฒ€์ƒ‰ ํ•„ํ„ฐ
204
  if search_query:
205
  if (search_query not in space_info['title'].lower()
206
  and search_query not in space_info['owner'].lower()
 
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({
 
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">
 
798
  opacity: 0;
799
  }
800
 
801
+ /* ๋กœ๋ด‡ ์ด๋ชจ์ง€ ๋Œ€์ฒด */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
802
  .emoji-avatar {
803
  font-size: 1.2rem;
804
  width: 32px;
 
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 -->
833
  <div class="tab-nav">
834
+ <button id="tabTrendingButton" class="tab-button active">Zero GPU Spaces</button>
835
+ <button id="tabFixedButton" class="tab-button">Fixed Tab</button>
836
  </div>
837
 
838
  <!-- Trending(Zero GPU) Tab Content -->
 
872
  <div id="pagination" class="pagination"></div>
873
  </div>
874
 
875
+ <!-- Fixed Tab Content -->
876
  <div id="fixedTab" class="tab-content">
877
  <div id="fixedGrid" class="grid-container"></div>
878
  </div>
 
924
  iframeStatuses: {}
925
  };
926
 
927
+ // Advanced iframe loader
928
  const iframeLoader = {
929
  checkQueue: {},
930
  maxAttempts: 5,
 
944
 
945
  checkIframeStatus: function(spaceKey) {
946
  if (!this.checkQueue[spaceKey]) return;
 
947
  const item = this.checkQueue[spaceKey];
948
  if (item.status !== 'loading') {
949
  delete this.checkQueue[spaceKey];
950
  return;
951
  }
952
  item.attempts++;
953
+
954
  try {
955
  if (!item.iframe || !item.iframe.parentNode) {
956
  delete this.checkQueue[spaceKey];
957
  return;
958
  }
959
 
 
960
  try {
961
+ const hasContent = item.iframe.contentWindow &&
962
+ item.iframe.contentWindow.document &&
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
  return;
977
  }
978
  } catch(e) {
979
+ // Cross-origin => not necessarily an error
980
  }
981
 
 
982
  const rect = item.iframe.getBoundingClientRect();
983
  if (rect.width > 50 && rect.height > 50 && item.attempts > 2) {
984
  item.status = 'success';
 
986
  return;
987
  }
988
 
 
989
  if (item.attempts >= this.maxAttempts) {
990
  if (item.iframe.offsetWidth > 0 && item.iframe.offsetHeight > 0) {
991
  item.status = 'success';
 
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
 
 
1044
  label: 'Number of Spaces',
1045
  data: data,
1046
  backgroundColor: colors,
1047
+ borderColor: colors.map(color => color.replace('0.7','1')),
1048
  borderWidth: 1
1049
  }]
1050
  },
 
1214
  container.appendChild(errorPlaceholder);
1215
  }
1216
 
1217
+ // ์นด๋“œ ๋ชฉ๋ก: ๋กœ๋ด‡ ์ด๋ชจ์ง€๋กœ ๋Œ€์ฒด
1218
  function renderGrid(spaces) {
1219
  elements.gridContainer.innerHTML = '';
1220
 
 
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';
 
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}`;
 
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>`;
 
1301
  headerDiv.appendChild(metaInfo);
1302
  gridItem.appendChild(headerDiv);
1303
 
 
1304
  if (description) {
1305
  const descP = document.createElement('p');
1306
  descP.className = 'desc-text';
 
1308
  gridItem.appendChild(descP);
1309
  }
1310
 
 
1311
  const content = document.createElement('div');
1312
  content.className = 'grid-content';
1313
 
 
1315
  iframeContainer.className = 'iframe-container';
1316
 
1317
  const iframe = document.createElement('iframe');
1318
+ iframe.src = embedUrl;
1319
  iframe.title = title;
1320
  iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
1321
+ iframe.setAttribute('allowfullscreen','');
1322
+ iframe.setAttribute('frameborder','0');
1323
  iframe.loading = 'lazy';
1324
 
1325
  const spaceKey = `${owner}/${name}`;
 
1342
  iframeContainer.appendChild(iframe);
1343
  content.appendChild(iframeContainer);
1344
 
 
1345
  const actions = document.createElement('div');
1346
  actions.className = 'grid-actions';
1347
 
 
1350
  linkEl.target = '_blank';
1351
  linkEl.className = 'open-link';
1352
  linkEl.textContent = 'Open in new window';
 
1353
  actions.appendChild(linkEl);
1354
+
1355
  gridItem.appendChild(content);
1356
  gridItem.appendChild(actions);
 
1357
  elements.gridContainer.appendChild(gridItem);
1358
 
1359
  } catch (err) {
 
1363
  }
1364
 
1365
  function renderFixedGrid() {
 
1366
  fixedGridContainer.innerHTML = '';
1367
 
1368
  const staticSpaces = [
 
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';
 
1465
  iframe.src = "https://" + owner.toLowerCase() + "-" + name.toLowerCase() + ".hf.space";
1466
  iframe.title = title;
1467
  iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
1468
+ iframe.setAttribute('allowfullscreen','');
1469
+ iframe.setAttribute('frameborder','0');
1470
  iframe.loading = 'lazy';
1471
 
1472
  const spaceKey = `${owner}/${name}`;
 
1577
  </html>
1578
  ''')
1579
 
1580
+ # Flask run
1581
  app.run(host='0.0.0.0', port=7860)