openfree commited on
Commit
c73ae40
ยท
verified ยท
1 Parent(s): da35051

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +623 -214
app.py CHANGED
@@ -3,12 +3,15 @@ import requests
3
  import os
4
  from collections import Counter
5
 
6
- app = Flask(__name__)
 
 
7
 
8
- # --------------------- ์ „์—ญ ์บ์‹œ ---------------------
9
- SPACE_CACHE = [] # ์„œ๋ฒ„ ์‹œ์ž‘ ์‹œ ํ•œ ๋ฒˆ๋งŒ ๋กœ๋”ฉ๋œ ์ŠคํŽ˜์ด์Šค ๋ชฉ๋ก์„ ์ €์žฅ
 
 
10
 
11
- # --------------------- ๋”๋ฏธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ํ•จ์ˆ˜ ---------------------
12
  def generate_dummy_spaces(count):
13
  """
14
  API ํ˜ธ์ถœ ์‹คํŒจ ์‹œ ์˜ˆ์‹œ์šฉ ๋”๋ฏธ ์ŠคํŽ˜์ด์Šค ์ƒ์„ฑ
@@ -30,15 +33,19 @@ def generate_dummy_spaces(count):
30
  })
31
  return spaces
32
 
33
- # --------------------- ์„œ๋ฒ„ ์‹œ์ž‘ ์‹œ ํ•œ ๋ฒˆ๋งŒ ํ˜ธ์ถœํ•˜์—ฌ CACHE ์ฑ„์šฐ๊ธฐ ---------------------
 
 
 
34
  def fetch_zero_gpu_spaces_once():
35
  """
36
- Hugging Face API์—์„œ hardware=cpu ์ŠคํŽ˜์ด์Šค ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ด (limit์„ ์ž‘๊ฒŒ ์„ค์ •)
 
37
  """
38
  try:
39
  url = "https://huggingface.co/api/spaces"
40
  params = {
41
- "limit": 500, # limit์„ ์ž‘๊ฒŒ ํ•˜์—ฌ ๋น ๋ฅธ ์‘๋‹ต ์œ ๋„
42
  "hardware": "cpu"
43
  }
44
  resp = requests.get(url, params=params, timeout=30)
@@ -52,45 +59,52 @@ def fetch_zero_gpu_spaces_once():
52
  and sp.get('id', '').split('/', 1)[0] != 'None'
53
  ]
54
 
55
- # ๊ธ€๋กœ๋ฒŒ ๋žญํฌ ๋ถ€์—ฌ
56
  for i, sp in enumerate(filtered):
57
  sp['global_rank'] = i + 1
58
 
59
- print(f"[fetch_zero_gpu_spaces_once] ์„ฑ๊ณต์ ์œผ๋กœ {len(filtered)}๊ฐœ ๋กœ๋“œ")
60
  return filtered
61
  else:
62
  print(f"[fetch_zero_gpu_spaces_once] API ์—๋Ÿฌ: {resp.status_code}")
63
  except Exception as e:
64
  print(f"[fetch_zero_gpu_spaces_once] ์˜ˆ์™ธ ๋ฐœ์ƒ: {e}")
65
 
66
- # ์‹คํŒจ ์‹œ ๋”๋ฏธ ๋ฐ์ดํ„ฐ ๋ฆฌํ„ด
67
  print("[fetch_zero_gpu_spaces_once] ์‹คํŒจ โ†’ ๋”๋ฏธ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ")
68
  return generate_dummy_spaces(100)
69
 
70
- @app.before_first_request
71
- def load_cache():
72
  """
73
- Flask๊ฐ€ ์ฒซ ์š”์ฒญ์„ ๋ฐ›๊ธฐ ์ „(์„œ๋ฒ„ ์‹œ์ž‘ ์งํ›„) ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰.
74
- ์ „์—ญ ๋ณ€์ˆ˜ SPACE_CACHE์— CPU ์ „์šฉ ์ŠคํŽ˜์ด์Šค ์ •๋ณด๋ฅผ ๋กœ๋”ฉํ•œ๋‹ค.
 
75
  """
76
- global SPACE_CACHE
77
- SPACE_CACHE = fetch_zero_gpu_spaces_once()
78
- print(f"[load_cache] Loaded {len(SPACE_CACHE)} CPU-based Spaces into cache.")
 
 
 
 
 
 
 
 
79
 
80
- # --------------------- URL ๋ณ€ํ™˜ ํ•จ์ˆ˜ ---------------------
81
  def transform_url(owner, name):
82
  """
83
- huggingface.co/spaces/owner/spaceName -> owner-spacename.hf.space
84
  """
85
  owner = owner.lower()
86
- # '.'๋‚˜ '_' ๋“ฑ์„ '-'๋กœ ๋ณ€ํ™˜
87
  name = name.replace('.', '-').replace('_', '-').lower()
88
  return f"https://{owner}-{name}.hf.space"
89
 
90
- # --------------------- space_details ์ƒ์„ฑ ํ•จ์ˆ˜ ---------------------
91
  def get_space_details(space_data, index, offset):
92
  """
93
- ์ŠคํŽ˜์ด์Šค์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์ถ”์ถœํ•˜๊ณ , ํŽ˜์ด์ง€ offset+index์— ๋”ฐ๋ผ ์ˆœ์œ„(rank)๋ฅผ ๋ถ€์—ฌ
 
94
  """
95
  try:
96
  space_id = space_data.get('id', '')
@@ -126,14 +140,12 @@ def get_space_details(space_data, index, offset):
126
  'rank': offset + index + 1
127
  }
128
  except Exception as e:
129
- print(f"[get_space_details] ์˜ˆ์™ธ ๋ฐœ์ƒ: {e}")
130
  return None
131
 
132
- # --------------------- owner ํ†ต๊ณ„ ํ•จ์ˆ˜ ---------------------
133
  def get_owner_stats(all_spaces):
134
  """
135
- ์ƒ์œ„ 500(global_rank <= 500)์— ์žˆ๋Š” ๏ฟฝ๏ฟฝํŽ˜์ด์Šค๋“ค์˜ owner๋ฅผ ์ถ”์ถœ,
136
- ๊ฐ owner๋ณ„ ๋“ฑ์žฅ ํšŸ์ˆ˜๋ฅผ ๊ตฌํ•œ ๋’ค ์ƒ์œ„ 30์ธ๋งŒ ๋ฐ˜ํ™˜
137
  """
138
  top_500 = [s for s in all_spaces if s.get('global_rank', 999999) <= 500]
139
  owners = []
@@ -148,8 +160,10 @@ def get_owner_stats(all_spaces):
148
  counts = Counter(owners)
149
  return counts.most_common(30)
150
 
151
- # --------------------- ์บ์‹œ์— ์žˆ๋Š” ์ŠคํŽ˜์ด์Šค์—์„œ ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ฒ˜๋ฆฌ ---------------------
152
  def fetch_trending_spaces(offset=0, limit=24):
 
 
 
153
  global SPACE_CACHE
154
  total = len(SPACE_CACHE)
155
  start = min(offset, total)
@@ -163,23 +177,29 @@ def fetch_trending_spaces(offset=0, limit=24):
163
  'all_spaces': SPACE_CACHE
164
  }
165
 
166
- # --------------------- ๋ผ์šฐํŠธ: ๋ฉ”์ธ ํŽ˜์ด์ง€ ---------------------
 
 
 
167
  @app.route('/')
168
  def home():
169
  """
170
- index.html ํ…œํ”Œ๋ฆฟ์„ ๋ Œ๋”๋ง
171
  """
172
  return render_template('index.html')
173
 
174
- # --------------------- ๋ผ์šฐํŠธ: Zero-GPU ์ŠคํŽ˜์ด์Šค API ---------------------
175
  @app.route('/api/trending-spaces', methods=['GET'])
176
  def trending_spaces():
177
  """
178
- - GET ํŒŒ๋ผ๋ฏธํ„ฐ search, offset, limit ์ฒ˜๋ฆฌ
179
- - ์บ์‹œ๋œ ์ŠคํŽ˜์ด์Šค ๋ชฉ๋ก์—์„œ ํŽ˜์ด์ง€๋„ค์ด์…˜
180
- - ๊ฒ€์ƒ‰์–ด ๊ฒ€์ƒ‰
181
- - ํ†ต๊ณ„(owner) ๊ณ„์‚ฐ
 
182
  """
 
 
 
183
  search_query = request.args.get('search', '').lower()
184
  offset = int(request.args.get('offset', 0))
185
  limit = int(request.args.get('limit', 24))
@@ -192,7 +212,7 @@ def trending_spaces():
192
  if not info:
193
  continue
194
 
195
- # ๊ฒ€์ƒ‰์–ด ํ•„ํ„ฐ
196
  if search_query:
197
  text_block = " ".join([
198
  info['title'].lower(),
@@ -215,14 +235,18 @@ def trending_spaces():
215
  'top_owners': top_owners
216
  })
217
 
218
- # --------------------- ์„œ๋ฒ„ ์‹คํ–‰ ---------------------
 
 
 
219
  if __name__ == '__main__':
220
- """
221
- ์‹คํ–‰ ์‹œ templates/index.html ํŒŒ์ผ์„ ์ž๋™ ์ƒ์„ฑ ํ›„ Flask ์„œ๋ฒ„ ์‹œ์ž‘
222
- """
223
  os.makedirs('templates', exist_ok=True)
224
 
225
- # index.html ์˜ˆ์‹œ ํ…œํ”Œ๋ฆฟ (์›๋ณธ ์งˆ๋ฌธ์˜ ๋‚ด์šฉ์„ ํ† ๋Œ€๋กœ ์ž‘์„ฑ)
 
 
 
226
  with open('templates/index.html', 'w', encoding='utf-8') as f:
227
  f.write('''<!DOCTYPE html>
228
  <html lang="en">
@@ -231,8 +255,9 @@ if __name__ == '__main__':
231
  <title>Huggingface Zero-GPU Spaces</title>
232
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
233
  <style>
234
- /* (์งˆ๋ฌธ ๋ณธ๋ฌธ์— ์ฃผ์–ด์ง„ CSS ์ „๋ถ€ ์‚ฝ์ž…) */
235
  @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
 
236
  :root {
237
  --pastel-pink: #FFD6E0;
238
  --pastel-blue: #C5E8FF;
@@ -240,17 +265,25 @@ if __name__ == '__main__':
240
  --pastel-yellow: #FFF2CC;
241
  --pastel-green: #C7F5D9;
242
  --pastel-orange: #FFE0C3;
 
243
  --mac-window-bg: rgba(250, 250, 250, 0.85);
244
  --mac-toolbar: #F5F5F7;
245
  --mac-border: #E2E2E2;
246
  --mac-button-red: #FF5F56;
247
  --mac-button-yellow: #FFBD2E;
248
  --mac-button-green: #27C93F;
 
249
  --text-primary: #333;
250
  --text-secondary: #666;
251
  --box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
252
  }
253
- * { margin: 0; padding: 0; box-sizing: border-box; }
 
 
 
 
 
 
254
  body {
255
  font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
256
  line-height: 1.6;
@@ -260,7 +293,12 @@ if __name__ == '__main__':
260
  min-height: 100vh;
261
  padding: 2rem;
262
  }
263
- .container { max-width: 1600px; margin: 0 auto; }
 
 
 
 
 
264
  .mac-window {
265
  background-color: var(--mac-window-bg);
266
  border-radius: 10px;
@@ -270,266 +308,579 @@ if __name__ == '__main__':
270
  margin-bottom: 2rem;
271
  border: 1px solid var(--mac-border);
272
  }
 
273
  .mac-toolbar {
274
- display: flex; align-items: center; padding: 10px 15px;
 
 
275
  background-color: var(--mac-toolbar);
276
  border-bottom: 1px solid var(--mac-border);
277
  }
 
278
  .mac-buttons {
279
- display: flex; gap: 8px; margin-right: 15px;
 
 
280
  }
 
281
  .mac-button {
282
- width: 12px; height: 12px; border-radius: 50%; cursor: default;
 
 
 
 
 
 
 
 
 
 
 
283
  }
284
- .mac-close { background-color: var(--mac-button-red); }
285
- .mac-minimize { background-color: var(--mac-button-yellow); }
286
- .mac-maximize { background-color: var(--mac-button-green); }
 
 
287
  .mac-title {
288
- flex-grow: 1; text-align: center; font-size: 0.9rem; color: var(--text-secondary);
 
 
 
 
 
 
 
289
  }
290
- .mac-content { padding: 20px; }
291
  .header {
292
- text-align: center; margin-bottom: 1.5rem; position: relative;
 
 
293
  }
 
294
  .header h1 {
295
- font-size: 2.2rem; font-weight: 700; margin: 0; color: #2d3748; letter-spacing: -0.5px;
 
 
 
 
296
  }
 
297
  .header p {
298
- color: var(--text-secondary); margin-top: 0.5rem; font-size: 1.1rem;
 
 
299
  }
 
300
  .tab-nav {
301
- display: flex; justify-content: center; margin-bottom: 1.5rem;
 
 
302
  }
 
303
  .tab-button {
304
- border: none; background-color: #edf2f7; color: var(--text-primary);
305
- padding: 10px 20px; margin: 0 5px; cursor: pointer; border-radius: 5px;
306
- font-size: 1rem; font-weight: 600;
307
- }
 
 
 
 
 
 
 
308
  .tab-button.active {
309
- background-color: var(--pastel-purple); color: #fff;
 
 
 
 
 
310
  }
311
- .tab-content { display: none; }
312
- .tab-content.active { display: block; }
 
 
 
313
  .search-bar {
314
- display: flex; align-items: center; margin-bottom: 1.5rem;
315
- background-color: white; border-radius: 30px; padding: 5px;
 
 
 
 
316
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
317
- max-width: 600px; margin-left: auto; margin-right: auto;
 
 
318
  }
 
319
  .search-bar input {
320
- flex-grow: 1; border: none; padding: 12px 20px; font-size: 1rem;
321
- outline: none; background: transparent; border-radius: 30px;
322
- }
 
 
 
 
 
 
323
  .search-bar .refresh-btn {
324
- background-color: var(--pastel-green); color: #1a202c; border: none;
325
- border-radius: 30px; padding: 10px 20px; font-size: 1rem; font-weight: 600;
326
- cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 8px;
327
- }
 
 
 
 
 
 
 
 
 
 
328
  .search-bar .refresh-btn:hover {
329
- background-color: #9ee7c0; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
 
330
  }
 
331
  .refresh-icon {
332
- display: inline-block; width: 16px; height: 16px;
333
- border: 2px solid #1a202c; border-top-color: transparent; border-radius: 50%;
 
 
 
 
334
  animation: none;
335
  }
 
336
  .refreshing .refresh-icon {
337
  animation: spin 1s linear infinite;
338
  }
 
339
  @keyframes spin {
340
- 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); }
 
341
  }
 
342
  .grid-container {
343
- display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
344
- gap: 1.5rem; margin-bottom: 2rem;
 
 
345
  }
 
346
  .grid-item {
347
- height: 500px; position: relative; overflow: hidden;
348
- transition: all 0.3s ease; border-radius: 15px;
 
 
 
349
  }
 
350
  .grid-item:nth-child(6n+1) { background-color: var(--pastel-pink); }
351
  .grid-item:nth-child(6n+2) { background-color: var(--pastel-blue); }
352
  .grid-item:nth-child(6n+3) { background-color: var(--pastel-purple); }
353
  .grid-item:nth-child(6n+4) { background-color: var(--pastel-yellow); }
354
  .grid-item:nth-child(6n+5) { background-color: var(--pastel-green); }
355
  .grid-item:nth-child(6n+6) { background-color: var(--pastel-orange); }
 
356
  .grid-item:hover {
357
- transform: translateY(-5px); box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
 
358
  }
 
359
  .grid-header {
360
- padding: 15px; display: flex; flex-direction: column;
361
- background-color: rgba(255, 255, 255, 0.7); backdrop-filter: blur(5px);
 
 
 
362
  border-bottom: 1px solid rgba(0, 0, 0, 0.05);
363
  }
 
364
  .grid-header-top {
365
- display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;
 
 
 
366
  }
 
367
  .rank-badge {
368
- background-color: #1a202c; color: white; font-size: 0.8rem; font-weight: 600;
369
- padding: 4px 8px; border-radius: 50px; display: inline-block;
370
- }
 
 
 
 
 
 
371
  .grid-header h3 {
372
- margin: 0; font-size: 1.2rem; font-weight: 700; white-space: nowrap;
373
- overflow: hidden; text-overflow: ellipsis;
 
 
 
 
374
  }
 
375
  .grid-meta {
376
- display: flex; justify-content: space-between; align-items: center; font-size: 0.9rem;
 
 
 
377
  }
378
- .owner-info { color: var(--text-secondary); font-weight: 500; }
 
 
 
 
 
379
  .likes-counter {
380
- display: flex; align-items: center; color: #e53e3e; font-weight: 600;
 
 
 
381
  }
382
- .likes-counter span { margin-left: 4px; }
383
- .grid-actions {
384
- padding: 10px 15px; text-align: right;
385
- background-color: rgba(255, 255, 255, 0.7); backdrop-filter: blur(5px);
386
- position: absolute; bottom: 0; left: 0; right: 0; z-index: 10;
387
- display: flex; justify-content: flex-end;
388
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  .open-link {
390
- text-decoration: none; color: #2c5282; font-weight: 600; padding: 5px 10px;
391
- border-radius: 5px; transition: all 0.2s;
 
 
 
 
392
  background-color: rgba(237, 242, 247, 0.8);
393
  }
 
394
  .open-link:hover {
395
  background-color: #e2e8f0;
396
  }
 
397
  .grid-content {
398
- position: absolute; top: 0; left: 0; width: 100%; height: 100%;
399
- padding-top: 85px; padding-bottom: 45px;
400
- }
 
 
 
 
 
 
401
  .iframe-container {
402
- width: 100%; height: 100%; overflow: hidden; position: relative;
 
 
 
403
  }
 
 
404
  .grid-content iframe {
405
- transform: scale(0.7); transform-origin: top left;
406
- width: 142.857%; height: 142.857%; border: none; border-radius: 0;
407
- }
 
 
 
 
 
408
  .error-placeholder {
409
- position: absolute; top: 0; left: 0; width: 100%; height: 100%;
410
- display: flex; flex-direction: column; justify-content: center; align-items: center;
411
- padding: 20px; background-color: rgba(255, 255, 255, 0.9); text-align: center;
412
- }
 
 
 
 
 
 
 
 
 
 
413
  .error-emoji {
414
- font-size: 6rem; margin-bottom: 1.5rem; animation: bounce 1s infinite alternate;
 
 
415
  text-shadow: 0 10px 20px rgba(0,0,0,0.1);
416
  }
 
417
  @keyframes bounce {
418
- from { transform: translateY(0px) scale(1); }
419
- to { transform: translateY(-15px) scale(1.1); }
 
 
 
 
420
  }
 
 
421
  .pagination {
422
- display: flex; justify-content: center; align-items: center; gap: 10px; margin: 2rem 0;
 
 
 
 
423
  }
 
424
  .pagination-button {
425
- background-color: white; border: none; padding: 10px 20px; border-radius: 10px;
426
- font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s;
427
- color: var(--text-primary); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
 
 
 
 
 
 
 
428
  }
 
429
  .pagination-button:hover {
430
- background-color: #f8f9fa; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
 
431
  }
 
432
  .pagination-button.active {
433
- background-color: var(--pastel-purple); color: #4a5568;
 
434
  }
 
435
  .pagination-button:disabled {
436
- background-color: #edf2f7; color: #a0aec0; cursor: default; box-shadow: none;
 
 
 
437
  }
 
 
438
  .loading {
439
- position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(255, 255, 255, 0.8);
440
- backdrop-filter: blur(5px); display: flex; justify-content: center; align-items: center;
 
 
 
 
 
 
 
 
441
  z-index: 1000;
442
  }
443
- .loading-content { text-align: center; }
 
 
 
 
444
  .loading-spinner {
445
- width: 60px; height: 60px; border: 5px solid #e2e8f0;
446
- border-top-color: var(--pastel-purple); border-radius: 50%;
 
 
 
447
  animation: spin 1s linear infinite;
448
  margin: 0 auto 15px;
449
  }
 
450
  .loading-text {
451
- font-size: 1.2rem; font-weight: 600; color: #4a5568;
 
 
452
  }
 
453
  .loading-error {
454
- display: none; margin-top: 10px; color: #e53e3e; font-size: 0.9rem;
 
 
 
455
  }
 
 
456
  .stats-window {
457
- margin-top: 2rem; margin-bottom: 2rem;
 
458
  }
 
459
  .stats-header {
460
- display: flex; justify-content: space-between; align-items: center;
 
 
461
  margin-bottom: 1rem;
462
  }
 
463
  .stats-title {
464
- font-size: 1.5rem; font-weight: 700; color: #2d3748;
 
 
465
  }
 
466
  .stats-toggle {
467
- background-color: var(--pastel-blue); border: none; padding: 8px 16px;
468
- border-radius: 20px; font-weight: 600; cursor: pointer; transition: all 0.2s;
469
- }
 
 
 
 
 
 
470
  .stats-toggle:hover {
471
  background-color: var(--pastel-purple);
472
  }
 
473
  .stats-content {
474
- background-color: white; border-radius: 10px; padding: 20px; box-shadow: var(--box-shadow);
475
- max-height: 0; overflow: hidden; transition: max-height 0.5s ease-out;
 
 
 
 
 
476
  }
 
477
  .stats-content.open {
478
  max-height: 600px;
479
  }
 
480
  .chart-container {
481
- width: 100%; height: 500px;
 
482
  }
 
 
483
  @media (max-width: 768px) {
484
- body { padding: 1rem; }
485
- .grid-container { grid-template-columns: 1fr; }
 
 
 
 
 
 
486
  .search-bar {
487
- flex-direction: column; padding: 10px;
 
488
  }
 
489
  .search-bar input {
490
- width: 100%; margin-bottom: 10px;
 
491
  }
 
492
  .search-bar .refresh-btn {
493
- width: 100%; justify-content: center;
 
 
 
 
 
 
 
 
 
494
  }
495
- .pagination { flex-wrap: wrap; }
496
- .chart-container { height: 300px; }
497
  }
 
498
  .error-emoji-detector {
499
- position: fixed; top: -9999px; left: -9999px; z-index: -1; opacity: 0;
 
 
 
 
500
  }
 
501
  .space-header {
502
- display: flex; align-items: center; gap: 10px; margin-bottom: 4px;
 
 
 
503
  }
504
  .avatar-img {
505
- width: 32px; height: 32px; border-radius: 50%; object-fit: cover; border: 1px solid #ccc;
 
 
 
 
506
  }
507
  .space-title {
508
- font-size: 1rem; font-weight: 600; margin: 0;
509
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 200px;
 
 
 
 
 
510
  }
511
  .zero-gpu-badge {
512
- font-size: 0.7rem; background-color: #e6fffa; color: #319795;
513
- border: 1px solid #81e6d9; border-radius: 6px; padding: 2px 6px;
514
- font-weight: 600; margin-left: 8px;
 
 
 
 
 
515
  }
516
  .desc-text {
517
- font-size: 0.85rem; color: #444; margin: 4px 0;
518
- line-clamp: 2; display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden;
 
 
 
 
 
519
  }
520
  .author-name {
521
- font-size: 0.8rem; color: #666;
 
522
  }
523
  .likes-wrapper {
524
- display: flex; align-items: center; gap: 4px; color: #e53e3e; font-weight: bold; font-size: 0.85rem;
 
 
 
 
 
525
  }
526
  .likes-heart {
527
- font-size: 1rem; line-height: 1rem; color: #f56565;
 
 
528
  }
529
  .emoji-avatar {
530
- font-size: 1.2rem; width: 32px; height: 32px; border-radius: 50%;
531
- border: 1px solid #ccc; display: flex; align-items: center; justify-content: center;
532
- }
 
 
 
 
 
 
 
533
  </style>
534
  </head>
535
  <body>
@@ -594,11 +945,15 @@ if __name__ == '__main__':
594
  <div class="loading-content">
595
  <div class="loading-spinner"></div>
596
  <div class="loading-text">Loading Zero-GPU spaces...</div>
597
- <div id="loadingError" class="loading-error">If this takes too long, try refreshing the page.</div>
 
 
598
  </div>
599
  </div>
600
  <script>
601
- // ------------------- Front-End JS (๋™์ผ) -------------------
 
 
602
  const elements = {
603
  gridContainer: document.getElementById('gridContainer'),
604
  loadingIndicator: document.getElementById('loadingIndicator'),
@@ -610,6 +965,7 @@ if __name__ == '__main__':
610
  statsContent: document.getElementById('statsContent'),
611
  creatorStatsChart: document.getElementById('creatorStatsChart')
612
  };
 
613
  const tabTrendingButton = document.getElementById('tabTrendingButton');
614
  const tabFixedButton = document.getElementById('tabFixedButton');
615
  const trendingTab = document.getElementById('trendingTab');
@@ -620,7 +976,7 @@ if __name__ == '__main__':
620
  isLoading: false,
621
  spaces: [],
622
  currentPage: 0,
623
- itemsPerPage: 24,
624
  totalItems: 0,
625
  loadingTimeout: null,
626
  staticModeAttempted: {},
@@ -630,18 +986,19 @@ if __name__ == '__main__':
630
  iframeStatuses: {}
631
  };
632
 
633
- // iframe ๋กœ๋”ฉ ๋ฐ ์—๋Ÿฌ ๊ฐ์ง€
634
  const iframeLoader = {
635
  checkQueue: {},
636
  maxAttempts: 5,
637
  checkInterval: 5000,
 
638
  startChecking(iframe, owner, name, title, spaceKey) {
639
  this.checkQueue[spaceKey] = {
640
- iframe, owner, name, title,
641
- attempts: 0, status: 'loading'
642
  };
643
  this.checkIframeStatus(spaceKey);
644
  },
 
645
  checkIframeStatus(spaceKey) {
646
  if (!this.checkQueue[spaceKey]) return;
647
  const item = this.checkQueue[spaceKey];
@@ -650,22 +1007,20 @@ if __name__ == '__main__':
650
  return;
651
  }
652
  item.attempts++;
 
653
  try {
654
  if (!item.iframe || !item.iframe.parentNode) {
655
  delete this.checkQueue[spaceKey];
656
  return;
657
  }
658
  try {
659
- const hasContent = item.iframe.contentWindow &&
660
- item.iframe.contentWindow.document &&
661
  item.iframe.contentWindow.document.body;
662
- if (hasContent &&
663
- item.iframe.contentWindow.document.body.innerHTML.length > 100) {
664
  const bodyText = item.iframe.contentWindow.document.body.textContent.toLowerCase();
665
- if (bodyText.includes('forbidden') ||
666
- bodyText.includes('404') ||
667
- bodyText.includes('not found') ||
668
- bodyText.includes('error')) {
669
  item.status = 'error';
670
  handleIframeError(item.iframe, item.owner, item.name, item.title);
671
  } else {
@@ -675,7 +1030,7 @@ if __name__ == '__main__':
675
  return;
676
  }
677
  } catch(e) {
678
- // ํฌ๋กœ์Šค ๋„๋ฉ”์ธ ์—๋Ÿฌ๋Š” ๋‹จ์ˆœ๋ฌด์‹œ
679
  }
680
  const rect = item.iframe.getBoundingClientRect();
681
  if (rect.width > 50 && rect.height > 50 && item.attempts > 2) {
@@ -716,6 +1071,7 @@ if __name__ == '__main__':
716
  renderCreatorStats();
717
  }
718
  }
 
719
  function renderCreatorStats() {
720
  if (state.chartInstance) {
721
  state.chartInstance.destroy();
@@ -723,11 +1079,13 @@ if __name__ == '__main__':
723
  const ctx = elements.creatorStatsChart.getContext('2d');
724
  const labels = state.topOwners.map(item => item[0]);
725
  const data = state.topOwners.map(item => item[1]);
 
726
  const colors = [];
727
- for (let i=0; i<labels.length; i++) {
728
- const hue = (i*360/labels.length) % 360;
729
  colors.push(`hsla(${hue}, 70%, 80%, 0.7)`);
730
  }
 
731
  state.chartInstance = new Chart(ctx, {
732
  type: 'bar',
733
  data: {
@@ -736,7 +1094,7 @@ if __name__ == '__main__':
736
  label: 'Number of Spaces in Top 500',
737
  data,
738
  backgroundColor: colors,
739
- borderColor: colors.map(c => c.replace('0.7', '1')),
740
  borderWidth: 1
741
  }]
742
  },
@@ -748,23 +1106,35 @@ if __name__ == '__main__':
748
  legend: { display: false },
749
  tooltip: {
750
  callbacks: {
751
- title(tooltipItems) { return tooltipItems[0].label; },
752
- label(context) { return `Spaces: ${context.raw}`; }
 
 
 
 
753
  }
754
  }
755
  },
756
  scales: {
757
  x: {
758
  beginAtZero: true,
759
- title: { display: true, text: 'Number of Spaces' }
 
 
 
760
  },
761
  y: {
762
- title: { display: true, text: 'Creator ID' },
 
 
 
763
  ticks: {
764
  autoSkip: false,
765
  font(context) {
766
  const defaultSize = 11;
767
- return { size: labels.length>20 ? defaultSize-1 : defaultSize };
 
 
768
  }
769
  }
770
  }
@@ -772,11 +1142,13 @@ if __name__ == '__main__':
772
  }
773
  });
774
  }
775
- async function loadSpaces(page=0) {
 
776
  setLoading(true);
777
  try {
778
  const searchText = elements.searchInput.value;
779
  const offset = page * state.itemsPerPage;
 
780
  const timeoutPromise = new Promise((_, reject) => {
781
  setTimeout(() => reject(new Error('Request timeout')), 30000);
782
  });
@@ -793,7 +1165,8 @@ if __name__ == '__main__':
793
 
794
  renderGrid(state.spaces);
795
  renderPagination();
796
- if (state.statsVisible && state.topOwners.length>0) {
 
797
  renderCreatorStats();
798
  }
799
  } catch (error) {
@@ -814,48 +1187,59 @@ if __name__ == '__main__':
814
  setLoading(false);
815
  }
816
  }
 
817
  function renderPagination() {
818
  elements.pagination.innerHTML = '';
819
  const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
 
820
  const prevButton = document.createElement('button');
821
  prevButton.className = 'pagination-button';
822
  prevButton.textContent = 'Previous';
823
  prevButton.disabled = (state.currentPage === 0);
824
  prevButton.addEventListener('click', () => {
825
- if (state.currentPage>0) loadSpaces(state.currentPage-1);
 
 
826
  });
827
  elements.pagination.appendChild(prevButton);
828
 
829
  const maxButtons = 7;
830
- let startPage = Math.max(0, state.currentPage - Math.floor(maxButtons/2));
831
- let endPage = Math.min(totalPages-1, startPage + maxButtons -1);
832
- if (endPage-startPage+1 < maxButtons) {
833
- startPage = Math.max(0, endPage-maxButtons+1);
 
834
  }
835
- for (let i=startPage; i<=endPage; i++) {
 
836
  const pageButton = document.createElement('button');
837
- pageButton.className = 'pagination-button' + (i===state.currentPage ? ' active' : '');
838
- pageButton.textContent = (i+1);
839
  pageButton.addEventListener('click', () => {
840
- if (i!==state.currentPage) loadSpaces(i);
 
 
841
  });
842
  elements.pagination.appendChild(pageButton);
843
  }
 
844
  const nextButton = document.createElement('button');
845
  nextButton.className = 'pagination-button';
846
  nextButton.textContent = 'Next';
847
- nextButton.disabled = (state.currentPage >= totalPages-1);
848
  nextButton.addEventListener('click', () => {
849
- if (state.currentPage < totalPages-1) {
850
- loadSpaces(state.currentPage+1);
851
  }
852
  });
853
  elements.pagination.appendChild(nextButton);
854
  }
 
855
  function handleIframeError(iframe, owner, name, title) {
856
  const container = iframe.parentNode;
857
  const errorPlaceholder = document.createElement('div');
858
  errorPlaceholder.className = 'error-placeholder';
 
859
  const errorMessage = document.createElement('p');
860
  errorMessage.textContent = `"${title}" space couldn't be loaded`;
861
  errorPlaceholder.appendChild(errorMessage);
@@ -873,12 +1257,15 @@ if __name__ == '__main__':
873
  directLink.style.fontWeight = '600';
874
 
875
  errorPlaceholder.appendChild(directLink);
 
876
  iframe.style.display = 'none';
877
  container.appendChild(errorPlaceholder);
878
  }
 
879
  function renderGrid(spaces) {
880
  elements.gridContainer.innerHTML = '';
881
- if (!spaces || spaces.length===0) {
 
882
  const noResultsMsg = document.createElement('p');
883
  noResultsMsg.textContent = 'No zero-gpu spaces found matching your search.';
884
  noResultsMsg.style.padding = '2rem';
@@ -888,16 +1275,24 @@ if __name__ == '__main__':
888
  elements.gridContainer.appendChild(noResultsMsg);
889
  return;
890
  }
891
- spaces.forEach(item => {
 
892
  try {
893
- const { url, title, likes_count, owner, name, rank, description, avatar_url, author_name, embedUrl } = item;
 
 
 
 
894
  const gridItem = document.createElement('div');
895
  gridItem.className = 'grid-item';
 
 
896
  const headerDiv = document.createElement('div');
897
  headerDiv.className = 'grid-header';
898
 
899
  const spaceHeader = document.createElement('div');
900
  spaceHeader.className = 'space-header';
 
901
  const rankBadge = document.createElement('div');
902
  rankBadge.className = 'rank-badge';
903
  rankBadge.textContent = `#${rank}`;
@@ -918,6 +1313,7 @@ if __name__ == '__main__':
918
  zeroGpuBadge.className = 'zero-gpu-badge';
919
  zeroGpuBadge.textContent = 'ZERO GPU';
920
  titleWrapper.appendChild(zeroGpuBadge);
 
921
  spaceHeader.appendChild(titleWrapper);
922
  headerDiv.appendChild(spaceHeader);
923
 
@@ -950,6 +1346,7 @@ if __name__ == '__main__':
950
  descP.textContent = description;
951
  gridItem.appendChild(descP);
952
  }
 
953
  const content = document.createElement('div');
954
  content.className = 'grid-content';
955
 
@@ -960,8 +1357,8 @@ if __name__ == '__main__':
960
  iframe.src = embedUrl;
961
  iframe.title = title;
962
  iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
963
- iframe.setAttribute('allowfullscreen','');
964
- iframe.setAttribute('frameborder','0');
965
  iframe.loading = 'lazy';
966
 
967
  const spaceKey = `${owner}/${name}`;
@@ -975,7 +1372,7 @@ if __name__ == '__main__':
975
  state.iframeStatuses[spaceKey] = 'error';
976
  };
977
  setTimeout(() => {
978
- if (state.iframeStatuses[spaceKey]==='loading') {
979
  handleIframeError(iframe, owner, name, title);
980
  state.iframeStatuses[spaceKey] = 'error';
981
  }
@@ -996,6 +1393,7 @@ if __name__ == '__main__':
996
  actions.appendChild(linkEl);
997
  gridItem.appendChild(content);
998
  gridItem.appendChild(actions);
 
999
  elements.gridContainer.appendChild(gridItem);
1000
  } catch (err) {
1001
  console.error('Item rendering error:', err);
@@ -1004,7 +1402,7 @@ if __name__ == '__main__':
1004
  }
1005
 
1006
  function renderFixedGrid() {
1007
- // ์˜ˆ์‹œ Picks
1008
  fixedGridContainer.innerHTML = '';
1009
  const staticSpaces = [
1010
  {
@@ -1024,7 +1422,8 @@ if __name__ == '__main__':
1024
  rank: 2
1025
  }
1026
  ];
1027
- if (!staticSpaces || staticSpaces.length===0) {
 
1028
  const noResultsMsg = document.createElement('p');
1029
  noResultsMsg.textContent = 'No spaces to display.';
1030
  noResultsMsg.style.padding = '2rem';
@@ -1034,7 +1433,8 @@ if __name__ == '__main__':
1034
  fixedGridContainer.appendChild(noResultsMsg);
1035
  return;
1036
  }
1037
- staticSpaces.forEach(item => {
 
1038
  try {
1039
  const { url, title, likes_count, owner, name, rank } = item;
1040
  const gridItem = document.createElement('div');
@@ -1042,12 +1442,14 @@ if __name__ == '__main__':
1042
 
1043
  const header = document.createElement('div');
1044
  header.className = 'grid-header';
 
1045
  const headerTop = document.createElement('div');
1046
  headerTop.className = 'grid-header-top';
1047
 
1048
  const leftWrapper = document.createElement('div');
1049
  leftWrapper.style.display = 'flex';
1050
  leftWrapper.style.alignItems = 'center';
 
1051
  const emojiAvatar = document.createElement('div');
1052
  emojiAvatar.className = 'emoji-avatar';
1053
  emojiAvatar.textContent = '๐Ÿค–';
@@ -1057,6 +1459,7 @@ if __name__ == '__main__':
1057
  titleEl.textContent = title;
1058
  titleEl.title = title;
1059
  leftWrapper.appendChild(titleEl);
 
1060
  headerTop.appendChild(leftWrapper);
1061
 
1062
  const rankBadge = document.createElement('div');
@@ -1065,6 +1468,7 @@ if __name__ == '__main__':
1065
  headerTop.appendChild(rankBadge);
1066
 
1067
  header.appendChild(headerTop);
 
1068
  const metaInfo = document.createElement('div');
1069
  metaInfo.className = 'grid-meta';
1070
 
@@ -1083,6 +1487,7 @@ if __name__ == '__main__':
1083
 
1084
  const content = document.createElement('div');
1085
  content.className = 'grid-content';
 
1086
  const iframeContainer = document.createElement('div');
1087
  iframeContainer.className = 'iframe-container';
1088
 
@@ -1090,8 +1495,8 @@ if __name__ == '__main__':
1090
  iframe.src = "https://" + owner.toLowerCase() + "-" + name.toLowerCase() + ".hf.space";
1091
  iframe.title = title;
1092
  iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
1093
- iframe.setAttribute('allowfullscreen','');
1094
- iframe.setAttribute('frameborder','0');
1095
  iframe.loading = 'lazy';
1096
 
1097
  const spaceKey = `${owner}/${name}`;
@@ -1102,7 +1507,7 @@ if __name__ == '__main__':
1102
  handleIframeError(iframe, owner, name, title);
1103
  };
1104
  setTimeout(() => {
1105
- if (iframe.offsetWidth===0 || iframe.offsetHeight===0) {
1106
  handleIframeError(iframe, owner, name, title);
1107
  }
1108
  }, 30000);
@@ -1112,6 +1517,7 @@ if __name__ == '__main__':
1112
 
1113
  const actions = document.createElement('div');
1114
  actions.className = 'grid-actions';
 
1115
  const linkEl = document.createElement('a');
1116
  linkEl.href = url;
1117
  linkEl.target = '_blank';
@@ -1121,6 +1527,7 @@ if __name__ == '__main__':
1121
 
1122
  gridItem.appendChild(content);
1123
  gridItem.appendChild(actions);
 
1124
  fixedGridContainer.appendChild(gridItem);
1125
  } catch (error) {
1126
  console.error('Fixed tab rendering error:', error);
@@ -1128,6 +1535,7 @@ if __name__ == '__main__':
1128
  });
1129
  }
1130
 
 
1131
  tabTrendingButton.addEventListener('click', () => {
1132
  tabTrendingButton.classList.add('active');
1133
  tabFixedButton.classList.remove('active');
@@ -1135,6 +1543,7 @@ if __name__ == '__main__':
1135
  fixedTab.classList.remove('active');
1136
  loadSpaces(state.currentPage);
1137
  });
 
1138
  tabFixedButton.addEventListener('click', () => {
1139
  tabFixedButton.classList.add('active');
1140
  tabTrendingButton.classList.remove('active');
@@ -1142,12 +1551,13 @@ if __name__ == '__main__':
1142
  trendingTab.classList.remove('active');
1143
  renderFixedGrid();
1144
  });
 
1145
  elements.searchInput.addEventListener('input', () => {
1146
  clearTimeout(state.searchTimeout);
1147
  state.searchTimeout = setTimeout(() => loadSpaces(0), 300);
1148
  });
1149
  elements.searchInput.addEventListener('keyup', (event) => {
1150
- if (event.key==='Enter') {
1151
  loadSpaces(0);
1152
  }
1153
  });
@@ -1157,6 +1567,7 @@ if __name__ == '__main__':
1157
  window.addEventListener('load', function() {
1158
  setTimeout(() => loadSpaces(0), 500);
1159
  });
 
1160
  setTimeout(() => {
1161
  if (state.isLoading) {
1162
  setLoading(false);
@@ -1178,6 +1589,7 @@ if __name__ == '__main__':
1178
  function setLoading(isLoading) {
1179
  state.isLoading = isLoading;
1180
  elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none';
 
1181
  if (isLoading) {
1182
  elements.refreshButton.classList.add('refreshing');
1183
  clearTimeout(state.loadingTimeout);
@@ -1195,8 +1607,5 @@ if __name__ == '__main__':
1195
  </html>
1196
  ''')
1197
 
1198
- # Flask ์‹คํ–‰
1199
  app.run(host='0.0.0.0', port=7860)
1200
-
1201
-
1202
-
 
3
  import os
4
  from collections import Counter
5
 
6
+ ##############################################################################
7
+ # 1) ์ „์—ญ ๋ณ€์ˆ˜ & ๋”๋ฏธ ๋ฐ์ดํ„ฐ
8
+ ##############################################################################
9
 
10
+ # ์ „์—ญ ์บ์‹œ: CPU ์ „์šฉ ์ŠคํŽ˜์ด์Šค ๋ชฉ๋ก
11
+ SPACE_CACHE = []
12
+ # ๋กœ๋“œ ์—ฌ๋ถ€ ํ”Œ๋ž˜๊ทธ (์ตœ์ดˆ ์š”์ฒญ ์‹œ ํ•œ ๋ฒˆ๋งŒ ๋กœ๋“œ)
13
+ CACHE_LOADED = False
14
 
 
15
  def generate_dummy_spaces(count):
16
  """
17
  API ํ˜ธ์ถœ ์‹คํŒจ ์‹œ ์˜ˆ์‹œ์šฉ ๋”๋ฏธ ์ŠคํŽ˜์ด์Šค ์ƒ์„ฑ
 
33
  })
34
  return spaces
35
 
36
+ ##############################################################################
37
+ # 2) Hugging Face API์—์„œ CPU ์ŠคํŽ˜์ด์Šค๋ฅผ ํ•œ ๋ฒˆ๋งŒ ๊ฐ€์ ธ์˜ค๋Š” ๋กœ์ง
38
+ ##############################################################################
39
+
40
  def fetch_zero_gpu_spaces_once():
41
  """
42
+ Hugging Face API (hardware=cpu) ์ŠคํŽ˜์ด์Šค ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ด
43
+ limit์„ ์ž‘๊ฒŒ ์„ค์ •ํ•˜์—ฌ ์‘๋‹ต ์†๋„๋ฅผ ๊ฐœ์„ 
44
  """
45
  try:
46
  url = "https://huggingface.co/api/spaces"
47
  params = {
48
+ "limit": 500, # ๋„ˆ๋ฌด ํฌ๊ฒŒ ์žก์œผ๋ฉด ์‘๋‹ต ์ง€์—ฐ โ†’ 50๊ฐœ ๋‚ด์™ธ๋กœ ์ œํ•œ
49
  "hardware": "cpu"
50
  }
51
  resp = requests.get(url, params=params, timeout=30)
 
59
  and sp.get('id', '').split('/', 1)[0] != 'None'
60
  ]
61
 
62
+ # global_rank ๋ถ€์—ฌ
63
  for i, sp in enumerate(filtered):
64
  sp['global_rank'] = i + 1
65
 
66
+ print(f"[fetch_zero_gpu_spaces_once] ๋กœ๋“œ๋œ ์ŠคํŽ˜์ด์Šค: {len(filtered)}๊ฐœ")
67
  return filtered
68
  else:
69
  print(f"[fetch_zero_gpu_spaces_once] API ์—๋Ÿฌ: {resp.status_code}")
70
  except Exception as e:
71
  print(f"[fetch_zero_gpu_spaces_once] ์˜ˆ์™ธ ๋ฐœ์ƒ: {e}")
72
 
73
+ # ์‹คํŒจ ์‹œ ๋”๋ฏธ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜
74
  print("[fetch_zero_gpu_spaces_once] ์‹คํŒจ โ†’ ๋”๋ฏธ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ")
75
  return generate_dummy_spaces(100)
76
 
77
+ def ensure_cache_loaded():
 
78
  """
79
+ Lazy Loading:
80
+ - ์ตœ์ดˆ ์š”์ฒญ์ด ๋“ค์–ด์™”์„ ๋•Œ๋งŒ ์บ์‹œ๋ฅผ ๋กœ๋“œ
81
+ - ์ด๋ฏธ ๋กœ๋“œ๋˜์—ˆ๋‹ค๋ฉด ์•„๋ฌด ๊ฒƒ๋„ ํ•˜์ง€ ์•Š์Œ
82
  """
83
+ global CACHE_LOADED, SPACE_CACHE
84
+ if not CACHE_LOADED:
85
+ SPACE_CACHE = fetch_zero_gpu_spaces_once()
86
+ CACHE_LOADED = True
87
+ print(f"[ensure_cache_loaded] Loaded {len(SPACE_CACHE)} CPU-based spaces into cache.")
88
+
89
+ ##############################################################################
90
+ # 3) Flask ์•ฑ ์ƒ์„ฑ & ์œ ํ‹ธ ํ•จ์ˆ˜
91
+ ##############################################################################
92
+
93
+ app = Flask(__name__)
94
 
 
95
  def transform_url(owner, name):
96
  """
97
+ huggingface.co/spaces/owner/spaceName -> owner-spacename.hf.space ๋ณ€ํ™˜
98
  """
99
  owner = owner.lower()
100
+ # '.'์™€ '_'๋ฅผ '-'๋กœ ์น˜ํ™˜
101
  name = name.replace('.', '-').replace('_', '-').lower()
102
  return f"https://{owner}-{name}.hf.space"
103
 
 
104
  def get_space_details(space_data, index, offset):
105
  """
106
+ ํŠน์ • ์ŠคํŽ˜์ด์Šค ์ •๋ณด๋ฅผ Python dict๋กœ ์ •๋ฆฌ
107
+ - rank: (offset + index + 1)
108
  """
109
  try:
110
  space_id = space_data.get('id', '')
 
140
  'rank': offset + index + 1
141
  }
142
  except Exception as e:
143
+ print(f"[get_space_details] ์˜ˆ์™ธ: {e}")
144
  return None
145
 
 
146
  def get_owner_stats(all_spaces):
147
  """
148
+ ์ƒ์œ„ 500(global_rank<=500)์— ์†ํ•˜๋Š” ์ŠคํŽ˜์ด์Šค์˜ owner ๋นˆ๋„์ˆ˜ ์ƒ์œ„ 30๋ช…
 
149
  """
150
  top_500 = [s for s in all_spaces if s.get('global_rank', 999999) <= 500]
151
  owners = []
 
160
  counts = Counter(owners)
161
  return counts.most_common(30)
162
 
 
163
  def fetch_trending_spaces(offset=0, limit=24):
164
+ """
165
+ ์ด๋ฏธ ์บ์‹œ๋œ SPACE_CACHE๋ฅผ offset, limit๋กœ ์Šฌ๋ผ์ด์‹ฑ
166
+ """
167
  global SPACE_CACHE
168
  total = len(SPACE_CACHE)
169
  start = min(offset, total)
 
177
  'all_spaces': SPACE_CACHE
178
  }
179
 
180
+ ##############################################################################
181
+ # 4) Flask ๋ผ์šฐํŠธ
182
+ ##############################################################################
183
+
184
  @app.route('/')
185
  def home():
186
  """
187
+ ๋ฉ”์ธ ํŽ˜์ด์ง€(index.html) ๋ Œ๋”๋ง
188
  """
189
  return render_template('index.html')
190
 
 
191
  @app.route('/api/trending-spaces', methods=['GET'])
192
  def trending_spaces():
193
  """
194
+ Zero-GPU ์ŠคํŽ˜์ด์Šค ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•˜๋Š” API:
195
+ - Lazy Load๋กœ ์บ์‹œ ๋กœ๋“œ
196
+ - offset, limit ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํŽ˜์ด์ง€๋„ค์ด์…˜
197
+ - search ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๊ฒ€์ƒ‰
198
+ - ์ƒ์œ„ 500 ๋‚ด owner ํ†ต๊ณ„
199
  """
200
+ # ์š”์ฒญ ๋“ค์–ด์˜ฌ ๋•Œ ์บ์‹œ ๋ฏธ๋กœ๋“œ๋ผ๋ฉด ์—ฌ๊ธฐ์„œ ๋กœ๋“œ
201
+ ensure_cache_loaded()
202
+
203
  search_query = request.args.get('search', '').lower()
204
  offset = int(request.args.get('offset', 0))
205
  limit = int(request.args.get('limit', 24))
 
212
  if not info:
213
  continue
214
 
215
+ # ๊ฒ€์ƒ‰์–ด ํ•„ํ„ฐ ์ ์šฉ (title, owner, url, description)
216
  if search_query:
217
  text_block = " ".join([
218
  info['title'].lower(),
 
235
  'top_owners': top_owners
236
  })
237
 
238
+ ##############################################################################
239
+ # 5) ์„œ๋ฒ„ ์‹คํ–‰ (templates/index.html ์ž‘์„ฑ)
240
+ ##############################################################################
241
+
242
  if __name__ == '__main__':
243
+ # templates ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ
 
 
244
  os.makedirs('templates', exist_ok=True)
245
 
246
+ # -------------------
247
+ # index.html ์ „์ฒด ์ƒ์„ฑ
248
+ # ์•„๋ž˜๋Š” ์งˆ๋ฌธ์— ์ฃผ์–ด์ง„ '๋ฌดํ•œ ๋กœ๋”ฉ' ๋ฌธ์ œ ํ•ด๊ฒฐ์šฉ ์ตœ์ข… HTML+JS ์˜ˆ์‹œ (CSS ํฌํ•จ)
249
+ # -------------------
250
  with open('templates/index.html', 'w', encoding='utf-8') as f:
251
  f.write('''<!DOCTYPE html>
252
  <html lang="en">
 
255
  <title>Huggingface Zero-GPU Spaces</title>
256
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
257
  <style>
258
+ /* ==================== CSS ์‹œ์ž‘ (์งˆ๋ฌธ ๋ณธ๋ฌธ์—์„œ ์‚ฌ์šฉํ•œ CSS ๊ทธ๋Œ€๋กœ) ==================== */
259
  @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
260
+
261
  :root {
262
  --pastel-pink: #FFD6E0;
263
  --pastel-blue: #C5E8FF;
 
265
  --pastel-yellow: #FFF2CC;
266
  --pastel-green: #C7F5D9;
267
  --pastel-orange: #FFE0C3;
268
+
269
  --mac-window-bg: rgba(250, 250, 250, 0.85);
270
  --mac-toolbar: #F5F5F7;
271
  --mac-border: #E2E2E2;
272
  --mac-button-red: #FF5F56;
273
  --mac-button-yellow: #FFBD2E;
274
  --mac-button-green: #27C93F;
275
+
276
  --text-primary: #333;
277
  --text-secondary: #666;
278
  --box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
279
  }
280
+
281
+ * {
282
+ margin: 0;
283
+ padding: 0;
284
+ box-sizing: border-box;
285
+ }
286
+
287
  body {
288
  font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
289
  line-height: 1.6;
 
293
  min-height: 100vh;
294
  padding: 2rem;
295
  }
296
+
297
+ .container {
298
+ max-width: 1600px;
299
+ margin: 0 auto;
300
+ }
301
+
302
  .mac-window {
303
  background-color: var(--mac-window-bg);
304
  border-radius: 10px;
 
308
  margin-bottom: 2rem;
309
  border: 1px solid var(--mac-border);
310
  }
311
+
312
  .mac-toolbar {
313
+ display: flex;
314
+ align-items: center;
315
+ padding: 10px 15px;
316
  background-color: var(--mac-toolbar);
317
  border-bottom: 1px solid var(--mac-border);
318
  }
319
+
320
  .mac-buttons {
321
+ display: flex;
322
+ gap: 8px;
323
+ margin-right: 15px;
324
  }
325
+
326
  .mac-button {
327
+ width: 12px;
328
+ height: 12px;
329
+ border-radius: 50%;
330
+ cursor: default;
331
+ }
332
+
333
+ .mac-close {
334
+ background-color: var(--mac-button-red);
335
+ }
336
+
337
+ .mac-minimize {
338
+ background-color: var(--mac-button-yellow);
339
  }
340
+
341
+ .mac-maximize {
342
+ background-color: var(--mac-button-green);
343
+ }
344
+
345
  .mac-title {
346
+ flex-grow: 1;
347
+ text-align: center;
348
+ font-size: 0.9rem;
349
+ color: var(--text-secondary);
350
+ }
351
+
352
+ .mac-content {
353
+ padding: 20px;
354
  }
355
+
356
  .header {
357
+ text-align: center;
358
+ margin-bottom: 1.5rem;
359
+ position: relative;
360
  }
361
+
362
  .header h1 {
363
+ font-size: 2.2rem;
364
+ font-weight: 700;
365
+ margin: 0;
366
+ color: #2d3748;
367
+ letter-spacing: -0.5px;
368
  }
369
+
370
  .header p {
371
+ color: var(--text-secondary);
372
+ margin-top: 0.5rem;
373
+ font-size: 1.1rem;
374
  }
375
+
376
  .tab-nav {
377
+ display: flex;
378
+ justify-content: center;
379
+ margin-bottom: 1.5rem;
380
  }
381
+
382
  .tab-button {
383
+ border: none;
384
+ background-color: #edf2f7;
385
+ color: var(--text-primary);
386
+ padding: 10px 20px;
387
+ margin: 0 5px;
388
+ cursor: pointer;
389
+ border-radius: 5px;
390
+ font-size: 1rem;
391
+ font-weight: 600;
392
+ }
393
+
394
  .tab-button.active {
395
+ background-color: var(--pastel-purple);
396
+ color: #fff;
397
+ }
398
+
399
+ .tab-content {
400
+ display: none;
401
  }
402
+
403
+ .tab-content.active {
404
+ display: block;
405
+ }
406
+
407
  .search-bar {
408
+ display: flex;
409
+ align-items: center;
410
+ margin-bottom: 1.5rem;
411
+ background-color: white;
412
+ border-radius: 30px;
413
+ padding: 5px;
414
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
415
+ max-width: 600px;
416
+ margin-left: auto;
417
+ margin-right: auto;
418
  }
419
+
420
  .search-bar input {
421
+ flex-grow: 1;
422
+ border: none;
423
+ padding: 12px 20px;
424
+ font-size: 1rem;
425
+ outline: none;
426
+ background: transparent;
427
+ border-radius: 30px;
428
+ }
429
+
430
  .search-bar .refresh-btn {
431
+ background-color: var(--pastel-green);
432
+ color: #1a202c;
433
+ border: none;
434
+ border-radius: 30px;
435
+ padding: 10px 20px;
436
+ font-size: 1rem;
437
+ font-weight: 600;
438
+ cursor: pointer;
439
+ transition: all 0.2s;
440
+ display: flex;
441
+ align-items: center;
442
+ gap: 8px;
443
+ }
444
+
445
  .search-bar .refresh-btn:hover {
446
+ background-color: #9ee7c0;
447
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
448
  }
449
+
450
  .refresh-icon {
451
+ display: inline-block;
452
+ width: 16px;
453
+ height: 16px;
454
+ border: 2px solid #1a202c;
455
+ border-top-color: transparent;
456
+ border-radius: 50%;
457
  animation: none;
458
  }
459
+
460
  .refreshing .refresh-icon {
461
  animation: spin 1s linear infinite;
462
  }
463
+
464
  @keyframes spin {
465
+ 0% { transform: rotate(0deg); }
466
+ 100% { transform: rotate(360deg); }
467
  }
468
+
469
  .grid-container {
470
+ display: grid;
471
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
472
+ gap: 1.5rem;
473
+ margin-bottom: 2rem;
474
  }
475
+
476
  .grid-item {
477
+ height: 500px;
478
+ position: relative;
479
+ overflow: hidden;
480
+ transition: all 0.3s ease;
481
+ border-radius: 15px;
482
  }
483
+
484
  .grid-item:nth-child(6n+1) { background-color: var(--pastel-pink); }
485
  .grid-item:nth-child(6n+2) { background-color: var(--pastel-blue); }
486
  .grid-item:nth-child(6n+3) { background-color: var(--pastel-purple); }
487
  .grid-item:nth-child(6n+4) { background-color: var(--pastel-yellow); }
488
  .grid-item:nth-child(6n+5) { background-color: var(--pastel-green); }
489
  .grid-item:nth-child(6n+6) { background-color: var(--pastel-orange); }
490
+
491
  .grid-item:hover {
492
+ transform: translateY(-5px);
493
+ box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
494
  }
495
+
496
  .grid-header {
497
+ padding: 15px;
498
+ display: flex;
499
+ flex-direction: column;
500
+ background-color: rgba(255, 255, 255, 0.7);
501
+ backdrop-filter: blur(5px);
502
  border-bottom: 1px solid rgba(0, 0, 0, 0.05);
503
  }
504
+
505
  .grid-header-top {
506
+ display: flex;
507
+ justify-content: space-between;
508
+ align-items: center;
509
+ margin-bottom: 8px;
510
  }
511
+
512
  .rank-badge {
513
+ background-color: #1a202c;
514
+ color: white;
515
+ font-size: 0.8rem;
516
+ font-weight: 600;
517
+ padding: 4px 8px;
518
+ border-radius: 50px;
519
+ display: inline-block;
520
+ }
521
+
522
  .grid-header h3 {
523
+ margin: 0;
524
+ font-size: 1.2rem;
525
+ font-weight: 700;
526
+ white-space: nowrap;
527
+ overflow: hidden;
528
+ text-overflow: ellipsis;
529
  }
530
+
531
  .grid-meta {
532
+ display: flex;
533
+ justify-content: space-between;
534
+ align-items: center;
535
+ font-size: 0.9rem;
536
  }
537
+
538
+ .owner-info {
539
+ color: var(--text-secondary);
540
+ font-weight: 500;
541
+ }
542
+
543
  .likes-counter {
544
+ display: flex;
545
+ align-items: center;
546
+ color: #e53e3e;
547
+ font-weight: 600;
548
  }
549
+
550
+ .likes-counter span {
551
+ margin-left: 4px;
 
 
 
552
  }
553
+
554
+ .grid-actions {
555
+ padding: 10px 15px;
556
+ text-align: right;
557
+ background-color: rgba(255, 255, 255, 0.7);
558
+ backdrop-filter: blur(5px);
559
+ position: absolute;
560
+ bottom: 0;
561
+ left: 0;
562
+ right: 0;
563
+ z-index: 10;
564
+ display: flex;
565
+ justify-content: flex-end;
566
+ }
567
+
568
  .open-link {
569
+ text-decoration: none;
570
+ color: #2c5282;
571
+ font-weight: 600;
572
+ padding: 5px 10px;
573
+ border-radius: 5px;
574
+ transition: all 0.2s;
575
  background-color: rgba(237, 242, 247, 0.8);
576
  }
577
+
578
  .open-link:hover {
579
  background-color: #e2e8f0;
580
  }
581
+
582
  .grid-content {
583
+ position: absolute;
584
+ top: 0;
585
+ left: 0;
586
+ width: 100%;
587
+ height: 100%;
588
+ padding-top: 85px; /* Header height */
589
+ padding-bottom: 45px; /* Actions height */
590
+ }
591
+
592
  .iframe-container {
593
+ width: 100%;
594
+ height: 100%;
595
+ overflow: hidden;
596
+ position: relative;
597
  }
598
+
599
+ /* Apply 70% scaling to iframes */
600
  .grid-content iframe {
601
+ transform: scale(0.7);
602
+ transform-origin: top left;
603
+ width: 142.857%;
604
+ height: 142.857%;
605
+ border: none;
606
+ border-radius: 0;
607
+ }
608
+
609
  .error-placeholder {
610
+ position: absolute;
611
+ top: 0;
612
+ left: 0;
613
+ width: 100%;
614
+ height: 100%;
615
+ display: flex;
616
+ flex-direction: column;
617
+ justify-content: center;
618
+ align-items: center;
619
+ padding: 20px;
620
+ background-color: rgba(255, 255, 255, 0.9);
621
+ text-align: center;
622
+ }
623
+
624
  .error-emoji {
625
+ font-size: 6rem;
626
+ margin-bottom: 1.5rem;
627
+ animation: bounce 1s infinite alternate;
628
  text-shadow: 0 10px 20px rgba(0,0,0,0.1);
629
  }
630
+
631
  @keyframes bounce {
632
+ from {
633
+ transform: translateY(0px) scale(1);
634
+ }
635
+ to {
636
+ transform: translateY(-15px) scale(1.1);
637
+ }
638
  }
639
+
640
+ /* Pagination Styling */
641
  .pagination {
642
+ display: flex;
643
+ justify-content: center;
644
+ align-items: center;
645
+ gap: 10px;
646
+ margin: 2rem 0;
647
  }
648
+
649
  .pagination-button {
650
+ background-color: white;
651
+ border: none;
652
+ padding: 10px 20px;
653
+ border-radius: 10px;
654
+ font-size: 1rem;
655
+ font-weight: 600;
656
+ cursor: pointer;
657
+ transition: all 0.2s;
658
+ color: var(--text-primary);
659
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
660
  }
661
+
662
  .pagination-button:hover {
663
+ background-color: #f8f9fa;
664
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
665
  }
666
+
667
  .pagination-button.active {
668
+ background-color: var(--pastel-purple);
669
+ color: #4a5568;
670
  }
671
+
672
  .pagination-button:disabled {
673
+ background-color: #edf2f7;
674
+ color: #a0aec0;
675
+ cursor: default;
676
+ box-shadow: none;
677
  }
678
+
679
+ /* Loading Indicator */
680
  .loading {
681
+ position: fixed;
682
+ top: 0;
683
+ left: 0;
684
+ right: 0;
685
+ bottom: 0;
686
+ background-color: rgba(255, 255, 255, 0.8);
687
+ backdrop-filter: blur(5px);
688
+ display: flex;
689
+ justify-content: center;
690
+ align-items: center;
691
  z-index: 1000;
692
  }
693
+
694
+ .loading-content {
695
+ text-align: center;
696
+ }
697
+
698
  .loading-spinner {
699
+ width: 60px;
700
+ height: 60px;
701
+ border: 5px solid #e2e8f0;
702
+ border-top-color: var(--pastel-purple);
703
+ border-radius: 50%;
704
  animation: spin 1s linear infinite;
705
  margin: 0 auto 15px;
706
  }
707
+
708
  .loading-text {
709
+ font-size: 1.2rem;
710
+ font-weight: 600;
711
+ color: #4a5568;
712
  }
713
+
714
  .loading-error {
715
+ display: none;
716
+ margin-top: 10px;
717
+ color: #e53e3e;
718
+ font-size: 0.9rem;
719
  }
720
+
721
+ /* Stats window styling */
722
  .stats-window {
723
+ margin-top: 2rem;
724
+ margin-bottom: 2rem;
725
  }
726
+
727
  .stats-header {
728
+ display: flex;
729
+ justify-content: space-between;
730
+ align-items: center;
731
  margin-bottom: 1rem;
732
  }
733
+
734
  .stats-title {
735
+ font-size: 1.5rem;
736
+ font-weight: 700;
737
+ color: #2d3748;
738
  }
739
+
740
  .stats-toggle {
741
+ background-color: var(--pastel-blue);
742
+ border: none;
743
+ padding: 8px 16px;
744
+ border-radius: 20px;
745
+ font-weight: 600;
746
+ cursor: pointer;
747
+ transition: all 0.2s;
748
+ }
749
+
750
  .stats-toggle:hover {
751
  background-color: var(--pastel-purple);
752
  }
753
+
754
  .stats-content {
755
+ background-color: white;
756
+ border-radius: 10px;
757
+ padding: 20px;
758
+ box-shadow: var(--box-shadow);
759
+ max-height: 0;
760
+ overflow: hidden;
761
+ transition: max-height 0.5s ease-out;
762
  }
763
+
764
  .stats-content.open {
765
  max-height: 600px;
766
  }
767
+
768
  .chart-container {
769
+ width: 100%;
770
+ height: 500px;
771
  }
772
+
773
+ /* Responsive Design */
774
  @media (max-width: 768px) {
775
+ body {
776
+ padding: 1rem;
777
+ }
778
+
779
+ .grid-container {
780
+ grid-template-columns: 1fr;
781
+ }
782
+
783
  .search-bar {
784
+ flex-direction: column;
785
+ padding: 10px;
786
  }
787
+
788
  .search-bar input {
789
+ width: 100%;
790
+ margin-bottom: 10px;
791
  }
792
+
793
  .search-bar .refresh-btn {
794
+ width: 100%;
795
+ justify-content: center;
796
+ }
797
+
798
+ .pagination {
799
+ flex-wrap: wrap;
800
+ }
801
+
802
+ .chart-container {
803
+ height: 300px;
804
  }
 
 
805
  }
806
+
807
  .error-emoji-detector {
808
+ position: fixed;
809
+ top: -9999px;
810
+ left: -9999px;
811
+ z-index: -1;
812
+ opacity: 0;
813
  }
814
+
815
  .space-header {
816
+ display: flex;
817
+ align-items: center;
818
+ gap: 10px;
819
+ margin-bottom: 4px;
820
  }
821
  .avatar-img {
822
+ width: 32px;
823
+ height: 32px;
824
+ border-radius: 50%;
825
+ object-fit: cover;
826
+ border: 1px solid #ccc;
827
  }
828
  .space-title {
829
+ font-size: 1rem;
830
+ font-weight: 600;
831
+ margin: 0;
832
+ overflow: hidden;
833
+ text-overflow: ellipsis;
834
+ white-space: nowrap;
835
+ max-width: 200px;
836
  }
837
  .zero-gpu-badge {
838
+ font-size: 0.7rem;
839
+ background-color: #e6fffa;
840
+ color: #319795;
841
+ border: 1px solid #81e6d9;
842
+ border-radius: 6px;
843
+ padding: 2px 6px;
844
+ font-weight: 600;
845
+ margin-left: 8px;
846
  }
847
  .desc-text {
848
+ font-size: 0.85rem;
849
+ color: #444;
850
+ margin: 4px 0;
851
+ line-clamp: 2;
852
+ display: -webkit-box;
853
+ -webkit-box-orient: vertical;
854
+ overflow: hidden;
855
  }
856
  .author-name {
857
+ font-size: 0.8rem;
858
+ color: #666;
859
  }
860
  .likes-wrapper {
861
+ display: flex;
862
+ align-items: center;
863
+ gap: 4px;
864
+ color: #e53e3e;
865
+ font-weight: bold;
866
+ font-size: 0.85rem;
867
  }
868
  .likes-heart {
869
+ font-size: 1rem;
870
+ line-height: 1rem;
871
+ color: #f56565;
872
  }
873
  .emoji-avatar {
874
+ font-size: 1.2rem;
875
+ width: 32px;
876
+ height: 32px;
877
+ border-radius: 50%;
878
+ border: 1px solid #ccc;
879
+ display: flex;
880
+ align-items: center;
881
+ justify-content: center;
882
+ }
883
+ /* ==================== CSS ๋ ==================== */
884
  </style>
885
  </head>
886
  <body>
 
945
  <div class="loading-content">
946
  <div class="loading-spinner"></div>
947
  <div class="loading-text">Loading Zero-GPU spaces...</div>
948
+ <div id="loadingError" class="loading-error">
949
+ If this takes too long, try refreshing the page.
950
+ </div>
951
  </div>
952
  </div>
953
  <script>
954
+ /* ==================== JS ๋กœ์ง (์งˆ๋ฌธ ๋ณธ๋ฌธ ํ”Œ๋ผ์Šคํฌ ์˜ˆ์‹œ์™€ ๋™์ผ) ==================== */
955
+
956
+ // DOM Elements
957
  const elements = {
958
  gridContainer: document.getElementById('gridContainer'),
959
  loadingIndicator: document.getElementById('loadingIndicator'),
 
965
  statsContent: document.getElementById('statsContent'),
966
  creatorStatsChart: document.getElementById('creatorStatsChart')
967
  };
968
+
969
  const tabTrendingButton = document.getElementById('tabTrendingButton');
970
  const tabFixedButton = document.getElementById('tabFixedButton');
971
  const trendingTab = document.getElementById('trendingTab');
 
976
  isLoading: false,
977
  spaces: [],
978
  currentPage: 0,
979
+ itemsPerPage: 24, // ํ•œ ํŽ˜์ด์ง€๋‹น 24๊ฐœ
980
  totalItems: 0,
981
  loadingTimeout: null,
982
  staticModeAttempted: {},
 
986
  iframeStatuses: {}
987
  };
988
 
989
+ // iframe ๋กœ๋” (์—๋Ÿฌ ๊ฐ์ง€)
990
  const iframeLoader = {
991
  checkQueue: {},
992
  maxAttempts: 5,
993
  checkInterval: 5000,
994
+
995
  startChecking(iframe, owner, name, title, spaceKey) {
996
  this.checkQueue[spaceKey] = {
997
+ iframe, owner, name, title, attempts: 0, status: 'loading'
 
998
  };
999
  this.checkIframeStatus(spaceKey);
1000
  },
1001
+
1002
  checkIframeStatus(spaceKey) {
1003
  if (!this.checkQueue[spaceKey]) return;
1004
  const item = this.checkQueue[spaceKey];
 
1007
  return;
1008
  }
1009
  item.attempts++;
1010
+
1011
  try {
1012
  if (!item.iframe || !item.iframe.parentNode) {
1013
  delete this.checkQueue[spaceKey];
1014
  return;
1015
  }
1016
  try {
1017
+ const hasContent = item.iframe.contentWindow &&
1018
+ item.iframe.contentWindow.document &&
1019
  item.iframe.contentWindow.document.body;
1020
+ if (hasContent && item.iframe.contentWindow.document.body.innerHTML.length > 100) {
 
1021
  const bodyText = item.iframe.contentWindow.document.body.textContent.toLowerCase();
1022
+ if (bodyText.includes('forbidden') || bodyText.includes('404') ||
1023
+ bodyText.includes('not found') || bodyText.includes('error')) {
 
 
1024
  item.status = 'error';
1025
  handleIframeError(item.iframe, item.owner, item.name, item.title);
1026
  } else {
 
1030
  return;
1031
  }
1032
  } catch(e) {
1033
+ // Cross-origin ๋ฌธ์ œ๋Š” ์—๋Ÿฌ๋กœ ๊ฐ„์ฃผํ•˜์ง€ ์•Š๊ณ  ๋„˜๊น€
1034
  }
1035
  const rect = item.iframe.getBoundingClientRect();
1036
  if (rect.width > 50 && rect.height > 50 && item.attempts > 2) {
 
1071
  renderCreatorStats();
1072
  }
1073
  }
1074
+
1075
  function renderCreatorStats() {
1076
  if (state.chartInstance) {
1077
  state.chartInstance.destroy();
 
1079
  const ctx = elements.creatorStatsChart.getContext('2d');
1080
  const labels = state.topOwners.map(item => item[0]);
1081
  const data = state.topOwners.map(item => item[1]);
1082
+
1083
  const colors = [];
1084
+ for (let i = 0; i < labels.length; i++) {
1085
+ const hue = (i * 360 / labels.length) % 360;
1086
  colors.push(`hsla(${hue}, 70%, 80%, 0.7)`);
1087
  }
1088
+
1089
  state.chartInstance = new Chart(ctx, {
1090
  type: 'bar',
1091
  data: {
 
1094
  label: 'Number of Spaces in Top 500',
1095
  data,
1096
  backgroundColor: colors,
1097
+ borderColor: colors.map(color => color.replace('0.7', '1')),
1098
  borderWidth: 1
1099
  }]
1100
  },
 
1106
  legend: { display: false },
1107
  tooltip: {
1108
  callbacks: {
1109
+ title(tooltipItems) {
1110
+ return tooltipItems[0].label;
1111
+ },
1112
+ label(context) {
1113
+ return `Spaces: ${context.raw}`;
1114
+ }
1115
  }
1116
  }
1117
  },
1118
  scales: {
1119
  x: {
1120
  beginAtZero: true,
1121
+ title: {
1122
+ display: true,
1123
+ text: 'Number of Spaces'
1124
+ }
1125
  },
1126
  y: {
1127
+ title: {
1128
+ display: true,
1129
+ text: 'Creator ID'
1130
+ },
1131
  ticks: {
1132
  autoSkip: false,
1133
  font(context) {
1134
  const defaultSize = 11;
1135
+ return {
1136
+ size: labels.length > 20 ? defaultSize - 1 : defaultSize
1137
+ };
1138
  }
1139
  }
1140
  }
 
1142
  }
1143
  });
1144
  }
1145
+
1146
+ async function loadSpaces(page = 0) {
1147
  setLoading(true);
1148
  try {
1149
  const searchText = elements.searchInput.value;
1150
  const offset = page * state.itemsPerPage;
1151
+
1152
  const timeoutPromise = new Promise((_, reject) => {
1153
  setTimeout(() => reject(new Error('Request timeout')), 30000);
1154
  });
 
1165
 
1166
  renderGrid(state.spaces);
1167
  renderPagination();
1168
+
1169
+ if (state.statsVisible && state.topOwners.length > 0) {
1170
  renderCreatorStats();
1171
  }
1172
  } catch (error) {
 
1187
  setLoading(false);
1188
  }
1189
  }
1190
+
1191
  function renderPagination() {
1192
  elements.pagination.innerHTML = '';
1193
  const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
1194
+
1195
  const prevButton = document.createElement('button');
1196
  prevButton.className = 'pagination-button';
1197
  prevButton.textContent = 'Previous';
1198
  prevButton.disabled = (state.currentPage === 0);
1199
  prevButton.addEventListener('click', () => {
1200
+ if (state.currentPage > 0) {
1201
+ loadSpaces(state.currentPage - 1);
1202
+ }
1203
  });
1204
  elements.pagination.appendChild(prevButton);
1205
 
1206
  const maxButtons = 7;
1207
+ let startPage = Math.max(0, state.currentPage - Math.floor(maxButtons / 2));
1208
+ let endPage = Math.min(totalPages - 1, startPage + maxButtons - 1);
1209
+
1210
+ if (endPage - startPage + 1 < maxButtons) {
1211
+ startPage = Math.max(0, endPage - maxButtons + 1);
1212
  }
1213
+
1214
+ for (let i = startPage; i <= endPage; i++) {
1215
  const pageButton = document.createElement('button');
1216
+ pageButton.className = 'pagination-button' + (i === state.currentPage ? ' active' : '');
1217
+ pageButton.textContent = (i + 1);
1218
  pageButton.addEventListener('click', () => {
1219
+ if (i !== state.currentPage) {
1220
+ loadSpaces(i);
1221
+ }
1222
  });
1223
  elements.pagination.appendChild(pageButton);
1224
  }
1225
+
1226
  const nextButton = document.createElement('button');
1227
  nextButton.className = 'pagination-button';
1228
  nextButton.textContent = 'Next';
1229
+ nextButton.disabled = (state.currentPage >= totalPages - 1);
1230
  nextButton.addEventListener('click', () => {
1231
+ if (state.currentPage < totalPages - 1) {
1232
+ loadSpaces(state.currentPage + 1);
1233
  }
1234
  });
1235
  elements.pagination.appendChild(nextButton);
1236
  }
1237
+
1238
  function handleIframeError(iframe, owner, name, title) {
1239
  const container = iframe.parentNode;
1240
  const errorPlaceholder = document.createElement('div');
1241
  errorPlaceholder.className = 'error-placeholder';
1242
+
1243
  const errorMessage = document.createElement('p');
1244
  errorMessage.textContent = `"${title}" space couldn't be loaded`;
1245
  errorPlaceholder.appendChild(errorMessage);
 
1257
  directLink.style.fontWeight = '600';
1258
 
1259
  errorPlaceholder.appendChild(directLink);
1260
+
1261
  iframe.style.display = 'none';
1262
  container.appendChild(errorPlaceholder);
1263
  }
1264
+
1265
  function renderGrid(spaces) {
1266
  elements.gridContainer.innerHTML = '';
1267
+
1268
+ if (!spaces || spaces.length === 0) {
1269
  const noResultsMsg = document.createElement('p');
1270
  noResultsMsg.textContent = 'No zero-gpu spaces found matching your search.';
1271
  noResultsMsg.style.padding = '2rem';
 
1275
  elements.gridContainer.appendChild(noResultsMsg);
1276
  return;
1277
  }
1278
+
1279
+ spaces.forEach((item) => {
1280
  try {
1281
+ const {
1282
+ url, title, likes_count, owner, name, rank,
1283
+ description, avatar_url, author_name, embedUrl
1284
+ } = item;
1285
+
1286
  const gridItem = document.createElement('div');
1287
  gridItem.className = 'grid-item';
1288
+
1289
+ // Header
1290
  const headerDiv = document.createElement('div');
1291
  headerDiv.className = 'grid-header';
1292
 
1293
  const spaceHeader = document.createElement('div');
1294
  spaceHeader.className = 'space-header';
1295
+
1296
  const rankBadge = document.createElement('div');
1297
  rankBadge.className = 'rank-badge';
1298
  rankBadge.textContent = `#${rank}`;
 
1313
  zeroGpuBadge.className = 'zero-gpu-badge';
1314
  zeroGpuBadge.textContent = 'ZERO GPU';
1315
  titleWrapper.appendChild(zeroGpuBadge);
1316
+
1317
  spaceHeader.appendChild(titleWrapper);
1318
  headerDiv.appendChild(spaceHeader);
1319
 
 
1346
  descP.textContent = description;
1347
  gridItem.appendChild(descP);
1348
  }
1349
+
1350
  const content = document.createElement('div');
1351
  content.className = 'grid-content';
1352
 
 
1357
  iframe.src = embedUrl;
1358
  iframe.title = title;
1359
  iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
1360
+ iframe.setAttribute('allowfullscreen', '');
1361
+ iframe.setAttribute('frameborder', '0');
1362
  iframe.loading = 'lazy';
1363
 
1364
  const spaceKey = `${owner}/${name}`;
 
1372
  state.iframeStatuses[spaceKey] = 'error';
1373
  };
1374
  setTimeout(() => {
1375
+ if (state.iframeStatuses[spaceKey] === 'loading') {
1376
  handleIframeError(iframe, owner, name, title);
1377
  state.iframeStatuses[spaceKey] = 'error';
1378
  }
 
1393
  actions.appendChild(linkEl);
1394
  gridItem.appendChild(content);
1395
  gridItem.appendChild(actions);
1396
+
1397
  elements.gridContainer.appendChild(gridItem);
1398
  } catch (err) {
1399
  console.error('Item rendering error:', err);
 
1402
  }
1403
 
1404
  function renderFixedGrid() {
1405
+ // ์˜ˆ์‹œ๋กœ ๋ช‡ ๊ฐœ ํ•˜๋“œ์ฝ”๋”ฉ
1406
  fixedGridContainer.innerHTML = '';
1407
  const staticSpaces = [
1408
  {
 
1422
  rank: 2
1423
  }
1424
  ];
1425
+
1426
+ if (!staticSpaces || staticSpaces.length === 0) {
1427
  const noResultsMsg = document.createElement('p');
1428
  noResultsMsg.textContent = 'No spaces to display.';
1429
  noResultsMsg.style.padding = '2rem';
 
1433
  fixedGridContainer.appendChild(noResultsMsg);
1434
  return;
1435
  }
1436
+
1437
+ staticSpaces.forEach((item) => {
1438
  try {
1439
  const { url, title, likes_count, owner, name, rank } = item;
1440
  const gridItem = document.createElement('div');
 
1442
 
1443
  const header = document.createElement('div');
1444
  header.className = 'grid-header';
1445
+
1446
  const headerTop = document.createElement('div');
1447
  headerTop.className = 'grid-header-top';
1448
 
1449
  const leftWrapper = document.createElement('div');
1450
  leftWrapper.style.display = 'flex';
1451
  leftWrapper.style.alignItems = 'center';
1452
+
1453
  const emojiAvatar = document.createElement('div');
1454
  emojiAvatar.className = 'emoji-avatar';
1455
  emojiAvatar.textContent = '๐Ÿค–';
 
1459
  titleEl.textContent = title;
1460
  titleEl.title = title;
1461
  leftWrapper.appendChild(titleEl);
1462
+
1463
  headerTop.appendChild(leftWrapper);
1464
 
1465
  const rankBadge = document.createElement('div');
 
1468
  headerTop.appendChild(rankBadge);
1469
 
1470
  header.appendChild(headerTop);
1471
+
1472
  const metaInfo = document.createElement('div');
1473
  metaInfo.className = 'grid-meta';
1474
 
 
1487
 
1488
  const content = document.createElement('div');
1489
  content.className = 'grid-content';
1490
+
1491
  const iframeContainer = document.createElement('div');
1492
  iframeContainer.className = 'iframe-container';
1493
 
 
1495
  iframe.src = "https://" + owner.toLowerCase() + "-" + name.toLowerCase() + ".hf.space";
1496
  iframe.title = title;
1497
  iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
1498
+ iframe.setAttribute('allowfullscreen', '');
1499
+ iframe.setAttribute('frameborder', '0');
1500
  iframe.loading = 'lazy';
1501
 
1502
  const spaceKey = `${owner}/${name}`;
 
1507
  handleIframeError(iframe, owner, name, title);
1508
  };
1509
  setTimeout(() => {
1510
+ if (iframe.offsetWidth === 0 || iframe.offsetHeight === 0) {
1511
  handleIframeError(iframe, owner, name, title);
1512
  }
1513
  }, 30000);
 
1517
 
1518
  const actions = document.createElement('div');
1519
  actions.className = 'grid-actions';
1520
+
1521
  const linkEl = document.createElement('a');
1522
  linkEl.href = url;
1523
  linkEl.target = '_blank';
 
1527
 
1528
  gridItem.appendChild(content);
1529
  gridItem.appendChild(actions);
1530
+
1531
  fixedGridContainer.appendChild(gridItem);
1532
  } catch (error) {
1533
  console.error('Fixed tab rendering error:', error);
 
1535
  });
1536
  }
1537
 
1538
+ // ํƒญ ์ „ํ™˜
1539
  tabTrendingButton.addEventListener('click', () => {
1540
  tabTrendingButton.classList.add('active');
1541
  tabFixedButton.classList.remove('active');
 
1543
  fixedTab.classList.remove('active');
1544
  loadSpaces(state.currentPage);
1545
  });
1546
+
1547
  tabFixedButton.addEventListener('click', () => {
1548
  tabFixedButton.classList.add('active');
1549
  tabTrendingButton.classList.remove('active');
 
1551
  trendingTab.classList.remove('active');
1552
  renderFixedGrid();
1553
  });
1554
+
1555
  elements.searchInput.addEventListener('input', () => {
1556
  clearTimeout(state.searchTimeout);
1557
  state.searchTimeout = setTimeout(() => loadSpaces(0), 300);
1558
  });
1559
  elements.searchInput.addEventListener('keyup', (event) => {
1560
+ if (event.key === 'Enter') {
1561
  loadSpaces(0);
1562
  }
1563
  });
 
1567
  window.addEventListener('load', function() {
1568
  setTimeout(() => loadSpaces(0), 500);
1569
  });
1570
+
1571
  setTimeout(() => {
1572
  if (state.isLoading) {
1573
  setLoading(false);
 
1589
  function setLoading(isLoading) {
1590
  state.isLoading = isLoading;
1591
  elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none';
1592
+
1593
  if (isLoading) {
1594
  elements.refreshButton.classList.add('refreshing');
1595
  clearTimeout(state.loadingTimeout);
 
1607
  </html>
1608
  ''')
1609
 
1610
+ # Flask ์„œ๋ฒ„ ์‹คํ–‰
1611
  app.run(host='0.0.0.0', port=7860)