openfree commited on
Commit
980e7a1
1 Parent(s): a242f7e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +877 -108
app.py CHANGED
@@ -239,74 +239,864 @@ def get_hardware_info(item: dict) -> tuple:
239
  print(f"Error parsing hardware info: {str(e)}")
240
  return 'Standard', 'None', 'N/A'
241
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
 
 
243
 
244
- def get_repo_info(repo_id: str, repo_type: str) -> dict:
245
- """저장소의 상세 정보를 가져오는 함수 (캐시 적용)"""
246
- cache_key = f"{repo_type}:{repo_id}"
 
 
 
 
 
 
247
 
248
- if cache_key in REPO_INFO_CACHE:
249
- return REPO_INFO_CACHE[cache_key]
 
 
250
 
 
 
 
 
 
 
 
 
 
251
  try:
252
- # API 엔드포인트 URL 구성
253
- base_url = "https://huggingface.co/api"
254
- if repo_type == "space":
255
- url = f"{base_url}/spaces/{repo_id}"
256
- elif repo_type == "model":
257
- url = f"{base_url}/models/{repo_id}"
258
- else: # dataset
259
- url = f"{base_url}/datasets/{repo_id}"
260
-
261
- # API 호출
262
- response = requests.get(url)
263
- response.raise_for_status()
264
- repo_info = response.json()
 
 
 
 
 
 
 
 
 
 
265
 
266
  # 캐시에 저장
267
- REPO_INFO_CACHE[cache_key] = repo_info
268
- return repo_info
269
 
270
- except requests.exceptions.RequestException as e:
271
- print(f"API request error for {repo_id}: {e}")
272
- return {}
273
  except Exception as e:
274
- print(f"Error fetching repo info for {repo_id}: {e}")
275
- return {}
 
 
 
276
 
277
- # 전역 캐시 딕셔너리 선언
278
- REPO_INFO_CACHE = {}
279
 
280
- def get_short_description(item: dict, repo_type: str) -> str:
281
- """README와 카드 메타데이터에서 설명 추출"""
282
- try:
283
- # 1. 먼저 cardData에서 확인
284
- card_data = item.get('cardData', {})
285
- if card_data and 'short_description' in card_data:
286
- return card_data['short_description']
287
-
288
- # 2. 저장소 상세 정보 가져오기
289
- repo_id = item.get('id', '')
290
- repo_info = get_repo_info(repo_id, repo_type)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
 
292
- # 3. tags와 siblings에서 설명 찾기
293
- if repo_info.get('tags'):
294
- return ', '.join(repo_info.get('tags')[:3]) # 상위 3개 태그만
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
 
296
- # 4. README에서 찾기
297
- readme = repo_info.get('readme', '')
298
- if readme:
299
- # README 파싱
300
- paragraphs = readme.split('\n\n')
301
- for p in paragraphs:
302
- p = p.strip()
303
- if p and not p.startswith('#') and not p.startswith('[!['):
304
- return p[:150] + '...' if len(p) > 150 else p
305
-
306
- return ''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  except Exception as e:
308
- print(f"Error getting description: {e}")
309
- return ''
310
 
311
  def get_card(item: dict, index: int, card_type: str = "space") -> str:
312
  """통합 카드 HTML 생성"""
@@ -315,21 +1105,25 @@ def get_card(item: dict, index: int, card_type: str = "space") -> str:
315
  likes = format(item.get('likes', 0), ',')
316
  created = item.get('createdAt', '').split('T')[0]
317
 
318
- # short_description 가져오기 (수정된 부분)
319
- short_description = get_short_description(item, card_type)
320
 
321
  # title과 short_description을 포함한 헤더 HTML
322
  title_html = f"""
323
- <div style='margin-bottom: 15px;'>
324
- <h3 style='
325
- margin: 0;
326
- color: #333;
327
- font-size: 1.3em;
328
- line-height: 1.4;'>
329
- {title}
330
- </h3>
331
- {f'<p style="margin: 5px 0 0 0; font-size: 0.9em; color: #666; font-style: italic;">{short_description}</p>' if short_description else ''}
332
- </div>
 
 
 
 
333
  """
334
 
335
 
@@ -498,26 +1292,7 @@ def get_card(item: dict, index: int, card_type: str = "space") -> str:
498
  text-shadow: 1px 1px 1px rgba(255,255,255,0.8);'>
499
  {title}
500
  </h3>
501
-
502
- <!-- Short Description 추가 -->
503
- {f'''
504
- <div style='
505
- margin: 0 0 15px 0;
506
- color: #444;
507
- font-size: 0.9em;
508
- line-height: 1.5;
509
- display: -webkit-box;
510
- -webkit-line-clamp: 3;
511
- -webkit-box-orient: vertical;
512
- overflow: hidden;
513
- text-overflow: ellipsis;
514
- background: rgba(255,255,255,0.4);
515
- padding: 10px;
516
- border-radius: 8px;'>
517
- {short_description}
518
- </div>
519
- ''' if short_description else ''}
520
-
521
  <div style='
522
  display: grid;
523
  grid-template-columns: repeat(2, 1fr);
@@ -550,7 +1325,7 @@ def get_trending_spaces(search_query="", sort_by="rank", progress=gr.Progress())
550
  progress(0, desc="Fetching spaces data...")
551
  params = {
552
  'full': 'true',
553
- 'limit': 10 # 10에서 300으로 수정
554
  }
555
 
556
  response = requests.get(url, params=params)
@@ -573,13 +1348,16 @@ def get_trending_spaces(search_query="", sort_by="rank", progress=gr.Progress())
573
  reverse=True)
574
 
575
  progress(0.1, desc="Creating gallery...")
576
- html_content = '<div class="gallery-container"><div class="gallery-grid">'
 
 
 
577
 
578
  for idx, space in enumerate(spaces):
579
  html_content += get_card(space, idx, "space")
580
  progress((0.1 + 0.9 * idx/len(spaces)), desc=f"Loading space {idx+1}/{len(spaces)}...")
581
 
582
- html_content += '</div></div>'
583
 
584
  progress(1.0, desc="Complete!")
585
  return html_content, f"Found {len(spaces)} spaces"
@@ -618,13 +1396,16 @@ def get_models(search_query="", sort_by="rank", progress=gr.Progress()) -> Tuple
618
  reverse=True)
619
 
620
  progress(0.1, desc="Creating gallery...")
621
- html_content = '<div class="gallery-container"><div class="gallery-grid">'
 
 
 
622
 
623
  for idx, model in enumerate(models):
624
  html_content += get_card(model, idx, "model")
625
  progress((0.1 + 0.9 * idx/len(models)), desc=f"Loading model {idx+1}/{len(models)}...")
626
 
627
- html_content += '</div></div>'
628
 
629
  progress(1.0, desc="Complete!")
630
  return html_content, f"Found {len(models)} models"
@@ -663,15 +1444,16 @@ def get_datasets(search_query="", sort_by="rank", progress=gr.Progress()) -> Tup
663
  reverse=True)
664
 
665
  progress(0.1, desc="Creating gallery...")
666
-
667
- # 통일된 HTML 구조
668
- html_content = '<div class="gallery-container"><div class="gallery-grid">'
 
669
 
670
  for idx, dataset in enumerate(datasets):
671
  html_content += get_card(dataset, idx, "dataset")
672
  progress((0.1 + 0.9 * idx/len(datasets)), desc=f"Loading dataset {idx+1}/{len(datasets)}...")
673
 
674
- html_content += '</div></div>'
675
 
676
  progress(1.0, desc="Complete!")
677
  return html_content, f"Found {len(datasets)} datasets"
@@ -764,20 +1546,7 @@ def create_interface():
764
  box-shadow: 0 6px 12px rgba(0,0,0,0.2);
765
  background: linear-gradient(135deg, #8b71ff, #7376f1);
766
  }
767
- /* 갤러리 그리드 레이아웃을 위한 CSS 추가 */
768
- .gallery-container {
769
- padding: 20px;
770
- background: #f5f5f5;
771
- width: 100%;
772
- }
773
- .gallery-grid {
774
- display: grid;
775
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
776
- gap: 20px;
777
- width: 100%;
778
- }
779
  """) as interface:
780
-
781
 
782
  gr.Markdown("""
783
  # 🤗 HuggingFace Trending TOP 300 Board
 
239
  print(f"Error parsing hardware info: {str(e)}")
240
  return 'Standard', 'None', 'N/A'
241
 
242
+ def get_card(item: dict, index: int, card_type: str = "space") -> str:
243
+ """통합 카드 HTML 생성"""
244
+ item_id = item.get('id', '')
245
+ author, title = item_id.split('/', 1)
246
+ likes = format(item.get('likes', 0), ',')
247
+ created = item.get('createdAt', '').split('T')[0]
248
+
249
+ # short_description 가져오기
250
+ short_description = item.get('cardData', {}).get('short_description', '')
251
+
252
+ # title과 short_description을 포함한 헤더 HTML
253
+ title_html = f"""
254
+ <h3 style='
255
+ margin: 0 0 15px 0;
256
+ color: #333;
257
+ font-size: 1.3em;
258
+ line-height: 1.4;
259
+ display: -webkit-box;
260
+ -webkit-line-clamp: 2;
261
+ -webkit-box-orient: vertical;
262
+ overflow: hidden;
263
+ text-overflow: ellipsis;
264
+ text-shadow: 1px 1px 1px rgba(255,255,255,0.8);'>
265
+ {title}
266
+ {f'<span style="display: block; font-size: 0.7em; color: #666; margin-top: 5px; font-weight: normal; font-style: italic;">{short_description}</span>' if short_description else ''}
267
+ </h3>
268
+ """
269
+
270
+
271
+ # URL 정의
272
+ if card_type == "space":
273
+ url = f"https://huggingface.co/spaces/{item_id}"
274
+ elif card_type == "model":
275
+ url = f"https://huggingface.co/{item_id}"
276
+ else: # dataset
277
+ url = f"https://huggingface.co/datasets/{item_id}"
278
+
279
+ # 메타데이터 처리
280
+ tags = item.get('tags', [])
281
+ pipeline_tag = item.get('pipeline_tag', '')
282
+ license = item.get('license', '')
283
+ sdk = item.get('sdk', 'N/A')
284
+
285
+ # AI Rating 정보 가져오기
286
+ rating_info = get_rating_info(item, index)
287
+
288
+ # 카드 타입별 그라데이션 설정
289
+ if card_type == "space":
290
+ gradient_colors = """
291
+ rgba(255, 182, 193, 0.7), /* 파스텔 핑크 */
292
+ rgba(173, 216, 230, 0.7), /* 파스텔 블루 */
293
+ rgba(255, 218, 185, 0.7) /* 파스텔 피치 */
294
+ """
295
+ bg_content = f"""
296
+ background-image: url(data:image/png;base64,{get_cached_screenshot(url) if get_cached_screenshot(url) else ''});
297
+ background-size: cover;
298
+ background-position: center;
299
+ """
300
+ type_icon = "🎯"
301
+ type_label = "SPACE"
302
+ elif card_type == "model":
303
+ gradient_colors = """
304
+ rgba(110, 142, 251, 0.7), /* 모델 블루 */
305
+ rgba(130, 158, 251, 0.7),
306
+ rgba(150, 174, 251, 0.7)
307
+ """
308
+ bg_content = f"""
309
+ background: linear-gradient(135deg, #6e8efb, #4a6cf7);
310
+ padding: 15px;
311
+ """
312
+ type_icon = "🤖"
313
+ type_label = "MODEL"
314
+ else: # dataset
315
+ gradient_colors = """
316
+ rgba(255, 107, 107, 0.7), /* 데이터셋 레드 */
317
+ rgba(255, 127, 127, 0.7),
318
+ rgba(255, 147, 147, 0.7)
319
+ """
320
+ bg_content = f"""
321
+ background: linear-gradient(135deg, #ff6b6b, #ff8787);
322
+ padding: 15px;
323
+ """
324
+ type_icon = "📊"
325
+ type_label = "DATASET"
326
+
327
+ content_bg = f"""
328
+ background: linear-gradient(135deg, {gradient_colors});
329
+ backdrop-filter: blur(10px);
330
+ """
331
+
332
+
333
+ # 태그 표시 (models와 datasets용)
334
+ tags_html = ""
335
+ if card_type != "space":
336
+ tags_html = f"""
337
+ <div style='
338
+ position: absolute;
339
+ top: 50%;
340
+ left: 50%;
341
+ transform: translate(-50%, -50%);
342
+ display: flex;
343
+ flex-wrap: wrap;
344
+ gap: 5px;
345
+ justify-content: center;
346
+ width: 90%;'>
347
+ {' '.join([f'''
348
+ <span style='
349
+ background: rgba(255,255,255,0.2);
350
+ padding: 5px 10px;
351
+ border-radius: 15px;
352
+ color: white;
353
+ font-size: 0.8em;'>
354
+ #{tag}
355
+ </span>
356
+ ''' for tag in tags[:5]])}
357
+ </div>
358
+ """
359
+
360
+ # 카드 HTML 반환
361
+ return f"""
362
+ <div class="card" style='
363
+ position: relative;
364
+ border: none;
365
+ padding: 0;
366
+ margin: 10px;
367
+ border-radius: 20px;
368
+ box-shadow: 0 10px 20px rgba(0,0,0,0.1);
369
+ background: white;
370
+ transition: all 0.3s ease;
371
+ overflow: hidden;
372
+ min-height: 400px;
373
+ cursor: pointer;
374
+ transform-origin: center;'
375
+ onmouseover="this.style.transform='scale(0.98) translateY(5px)'; this.style.boxShadow='0 5px 15px rgba(0,0,0,0.2)';"
376
+ onmouseout="this.style.transform='scale(1) translateY(0)'; this.style.boxShadow='0 10px 20px rgba(0,0,0,0.1)';"
377
+ onclick="window.open('{url}', '_blank')">
378
+
379
+ <!-- 상단 영역 -->
380
+ <div style='
381
+ width: 100%;
382
+ height: 200px;
383
+ {bg_content}
384
+ position: relative;'>
385
+
386
+ <!-- 순위 뱃지 -->
387
+ <div style='
388
+ position: absolute;
389
+ top: 10px;
390
+ left: 10px;
391
+ background: rgba(0,0,0,0.7);
392
+ color: white;
393
+ padding: 5px 15px;
394
+ border-radius: 20px;
395
+ font-weight: bold;
396
+ font-size: 0.9em;
397
+ backdrop-filter: blur(5px);'>
398
+ #{index + 1}
399
+ </div>
400
+
401
+ <!-- 타입 뱃지 -->
402
+ <div style='
403
+ position: absolute;
404
+ top: 10px;
405
+ right: 10px;
406
+ background: rgba(255,255,255,0.9);
407
+ padding: 5px 15px;
408
+ border-radius: 20px;
409
+ font-weight: bold;
410
+ font-size: 0.8em;'>
411
+ {type_icon} {type_label}
412
+ </div>
413
+
414
+ {tags_html}
415
+ </div>
416
+
417
+ <!-- 콘텐츠 영역 -->
418
+ <div style='
419
+ padding: 20px;
420
+ {content_bg}
421
+ border-radius: 0 0 20px 20px;
422
+ border-top: 1px solid rgba(255,255,255,0.5);'>
423
+ <h3 style='
424
+ margin: 0 0 15px 0;
425
+ color: #333;
426
+ font-size: 1.3em;
427
+ line-height: 1.4;
428
+ display: -webkit-box;
429
+ -webkit-line-clamp: 2;
430
+ -webkit-box-orient: vertical;
431
+ overflow: hidden;
432
+ text-overflow: ellipsis;
433
+ text-shadow: 1px 1px 1px rgba(255,255,255,0.8);'>
434
+ {title}
435
+ </h3>
436
+
437
+ <div style='
438
+ display: grid;
439
+ grid-template-columns: repeat(2, 1fr);
440
+ gap: 10px;
441
+ font-size: 0.9em;
442
+ background: rgba(255,255,255,0.3);
443
+ padding: 10px;
444
+ border-radius: 10px;'>
445
+ <div style='color: #444;'>
446
+ <span style='margin-right: 5px;'>👤</span> {author}
447
+ </div>
448
+ <div style='color: #444;'>
449
+ <span style='margin-right: 5px;'>❤️</span> {likes}
450
+ </div>
451
+ <div style='color: #444; grid-column: span 2;'>
452
+ <span style='margin-right: 5px;'>📅</span> {created}
453
+ </div>
454
+ </div>
455
+
456
+ {rating_info}
457
+ </div>
458
+ </div>
459
+ """
460
+
461
+ def get_trending_spaces(search_query="", sort_by="rank", progress=gr.Progress()) -> Tuple[str, str]:
462
+ """트렌딩 스페이스 가져오기"""
463
+ url = "https://huggingface.co/api/spaces"
464
+
465
+ try:
466
+ progress(0, desc="Fetching spaces data...")
467
+ params = {
468
+ 'full': 'true',
469
+ 'limit': 100
470
+ }
471
+
472
+ response = requests.get(url, params=params)
473
+ response.raise_for_status()
474
+ spaces = response.json()
475
+
476
+ # 검색어로 필터링
477
+ if search_query:
478
+ spaces = [space for space in spaces if search_query.lower() in
479
+ (space.get('id', '') + ' ' + space.get('title', '')).lower()]
480
+
481
+ # 정렬
482
+ sort_by = sort_by.lower()
483
+ if sort_by == "rising_rate":
484
+ spaces.sort(key=lambda x: calculate_rising_rate(x.get('createdAt', ''), 0), reverse=True)
485
+ elif sort_by == "popularity":
486
+ spaces.sort(key=lambda x: get_popularity_grade(
487
+ int(str(x.get('likes', '0')).replace(',', '')),
488
+ calculate_rising_rate(x.get('createdAt', ''), 0))[1],
489
+ reverse=True)
490
+
491
+ progress(0.1, desc="Creating gallery...")
492
+ html_content = """
493
+ <div style='padding: 20px; background: #f5f5f5;'>
494
+ <div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;'>
495
+ """
496
+
497
+ for idx, space in enumerate(spaces):
498
+ html_content += get_card(space, idx, "space")
499
+ progress((0.1 + 0.9 * idx/len(spaces)), desc=f"Loading space {idx+1}/{len(spaces)}...")
500
+
501
+ html_content += "</div></div>"
502
+
503
+ progress(1.0, desc="Complete!")
504
+ return html_content, f"Found {len(spaces)} spaces"
505
+
506
+ except Exception as e:
507
+ error_html = f'<div style="color: red; padding: 20px;">Error: {str(e)}</div>'
508
+ return error_html, f"Error: {str(e)}"
509
+
510
+ def get_models(search_query="", sort_by="rank", progress=gr.Progress()) -> Tuple[str, str]:
511
+ """인기 모델 가져오기"""
512
+ url = "https://huggingface.co/api/models"
513
+
514
+ try:
515
+ progress(0, desc="Fetching models data...")
516
+ params = {
517
+ 'full': 'true',
518
+ 'limit': 300
519
+ }
520
+ response = requests.get(url, params=params)
521
+ response.raise_for_status()
522
+ models = response.json()
523
+
524
+ # 검색어로 필터링
525
+ if search_query:
526
+ models = [model for model in models if search_query.lower() in
527
+ (model.get('id', '') + ' ' + model.get('title', '')).lower()]
528
+
529
+ # 정렬
530
+ sort_by = sort_by.lower()
531
+ if sort_by == "rising_rate":
532
+ models.sort(key=lambda x: calculate_rising_rate(x.get('createdAt', ''), 0), reverse=True)
533
+ elif sort_by == "popularity":
534
+ models.sort(key=lambda x: get_popularity_grade(
535
+ int(str(x.get('likes', '0')).replace(',', '')),
536
+ calculate_rising_rate(x.get('createdAt', ''), 0))[1],
537
+ reverse=True)
538
+
539
+ progress(0.1, desc="Creating gallery...")
540
+ html_content = """
541
+ <div style='padding: 20px; background: #f5f5f5;'>
542
+ <div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;'>
543
+ """
544
+
545
+ for idx, model in enumerate(models):
546
+ html_content += get_card(model, idx, "model")
547
+ progress((0.1 + 0.9 * idx/len(models)), desc=f"Loading model {idx+1}/{len(models)}...")
548
+
549
+ html_content += "</div></div>"
550
+
551
+ progress(1.0, desc="Complete!")
552
+ return html_content, f"Found {len(models)} models"
553
+
554
+ except Exception as e:
555
+ error_html = f'<div style="color: red; padding: 20px;">Error: {str(e)}</div>'
556
+ return error_html, f"Error: {str(e)}"
557
+
558
+ def get_datasets(search_query="", sort_by="rank", progress=gr.Progress()) -> Tuple[str, str]:
559
+ """인기 데이터셋 가져오기"""
560
+ url = "https://huggingface.co/api/datasets"
561
+
562
+ try:
563
+ progress(0, desc="Fetching datasets data...")
564
+ params = {
565
+ 'full': 'true',
566
+ 'limit': 300
567
+ }
568
+ response = requests.get(url, params=params)
569
+ response.raise_for_status()
570
+ datasets = response.json()
571
+
572
+ # 검색어로 필터링
573
+ if search_query:
574
+ datasets = [dataset for dataset in datasets if search_query.lower() in
575
+ (dataset.get('id', '') + ' ' + dataset.get('title', '')).lower()]
576
+
577
+ # 정렬
578
+ sort_by = sort_by.lower()
579
+ if sort_by == "rising_rate":
580
+ datasets.sort(key=lambda x: calculate_rising_rate(x.get('createdAt', ''), 0), reverse=True)
581
+ elif sort_by == "popularity":
582
+ datasets.sort(key=lambda x: get_popularity_grade(
583
+ int(str(x.get('likes', '0')).replace(',', '')),
584
+ calculate_rising_rate(x.get('createdAt', ''), 0))[1],
585
+ reverse=True)
586
+
587
+ progress(0.1, desc="Creating gallery...")
588
+ html_content = """
589
+ <div style='padding: 20px; background: #f5f5f5;'>
590
+ <div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;'>
591
+ """
592
+
593
+ for idx, dataset in enumerate(datasets):
594
+ html_content += get_card(dataset, idx, "dataset")
595
+ progress((0.1 + 0.9 * idx/len(datasets)), desc=f"Loading dataset {idx+1}/{len(datasets)}...")
596
+
597
+ html_content += "</div></div>"
598
+
599
+ progress(1.0, desc="Complete!")
600
+ return html_content, f"Found {len(datasets)} datasets"
601
+
602
+ except Exception as e:
603
+ error_html = f'<div style="color: red; padding: 20px;">Error: {str(e)}</div>'
604
+ return error_html, f"Error: {str(e)}"
605
+
606
+ # 정렬 함수 추가
607
+ def sort_items(items, sort_by):
608
+ if sort_by == "rank":
609
+ return items # 이미 순위대로 정렬되어 있음
610
+ elif sort_by == "rising_rate":
611
+ return sorted(items, key=lambda x: calculate_rising_rate(x.get('createdAt', ''), 0), reverse=True)
612
+ elif sort_by == "popularity":
613
+ return sorted(items, key=lambda x: get_popularity_grade(int(str(x.get('likes', '0')).replace(',', '')),
614
+ calculate_rising_rate(x.get('createdAt', ''), 0))[1], reverse=True)
615
+ return items
616
+
617
+ # API 호출 함수 수정
618
+ def fetch_items(item_type, search_query="", sort_by="rank", limit=1000):
619
+ """아이템 가져오기 (spaces/models/datasets)"""
620
+ base_url = f"https://huggingface.co/api/{item_type}"
621
+ params = {
622
+ 'full': 'true',
623
+ 'limit': limit,
624
+ 'search': search_query
625
+ }
626
+
627
+ try:
628
+ response = requests.get(base_url, params=params)
629
+ response.raise_for_status()
630
+ items = response.json()
631
+
632
+ # 검색어로 필터링
633
+ if search_query:
634
+ items = [item for item in items if search_query.lower() in
635
+ (item.get('id', '') + item.get('title', '')).lower()]
636
+
637
+ # 정렬
638
+ items = sort_items(items, sort_by)
639
+
640
+ return items[:300] # 상위 300개만 반환
641
+ except Exception as e:
642
+ print(f"Error fetching items: {e}")
643
+ return []
644
+
645
+ def create_interface():
646
+ with gr.Blocks(title="HuggingFace Trending Board", css="""
647
+ .search-sort-container {
648
+ background: linear-gradient(135deg, rgba(255,255,255,0.95), rgba(240,240,255,0.95));
649
+ border-radius: 15px;
650
+ padding: 20px;
651
+ margin: 10px 0;
652
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
653
+ overflow: visible;
654
+ }
655
+ .search-box {
656
+ border: 2px solid #e1e1e1;
657
+ border-radius: 10px;
658
+ padding: 12px;
659
+ transition: all 0.3s ease;
660
+ background: linear-gradient(135deg, #ffffff, #f8f9ff);
661
+ width: 100%;
662
+ }
663
+ .search-box:focus {
664
+ border-color: #7b61ff;
665
+ box-shadow: 0 0 0 2px rgba(123,97,255,0.2);
666
+ background: linear-gradient(135deg, #ffffff, #f0f3ff);
667
+ }
668
+ .refresh-btn {
669
+ background: linear-gradient(135deg, #7b61ff, #6366f1);
670
+ color: white;
671
+ border: none;
672
+ padding: 10px 20px;
673
+ border-radius: 10px;
674
+ cursor: pointer;
675
+ transition: all 0.3s ease;
676
+ width: 120px;
677
+ height: 80px !important;
678
+ display: flex;
679
+ align-items: center;
680
+ justify-content: center;
681
+ margin-left: auto;
682
+ font-size: 1.2em !important;
683
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
684
+ }
685
+ .refresh-btn:hover {
686
+ transform: translateY(-2px);
687
+ box-shadow: 0 6px 12px rgba(0,0,0,0.2);
688
+ background: linear-gradient(135deg, #8b71ff, #7376f1);
689
+ }
690
+ """) as interface:
691
+
692
+ gr.Markdown("""
693
+ # 🤗 HuggingFace Trending TOP 300 Board
694
+ <div style='margin-bottom: 20px; padding: 10px; background: linear-gradient(135deg, rgba(123,97,255,0.1), rgba(99,102,241,0.1)); border-radius: 10px;'>
695
+ Explore, search, and sort through the Shows Top 300 Trending spaces with AI Ratings
696
+ </div>
697
+ """)
698
+
699
+ with gr.Tabs() as tabs:
700
+ # Spaces 탭
701
+ with gr.Tab("🎯 Trending Spaces"):
702
+ with gr.Row(elem_classes="search-sort-container"):
703
+ with gr.Column(scale=2):
704
+ spaces_search = gr.Textbox(
705
+ label="🔍 Search Spaces",
706
+ placeholder="Enter keywords to search...",
707
+ elem_classes="search-box"
708
+ )
709
+ with gr.Column(scale=2):
710
+ spaces_sort = gr.Radio(
711
+ choices=["rank", "rising_rate", "popularity"],
712
+ value="rank",
713
+ label="Sort by",
714
+ interactive=True
715
+ )
716
+ with gr.Column(scale=1):
717
+ spaces_refresh_btn = gr.Button(
718
+ "🔄 Refresh",
719
+ variant="primary",
720
+ elem_classes="refresh-btn"
721
+ )
722
+ spaces_gallery = gr.HTML()
723
+ spaces_status = gr.Markdown("Loading...")
724
+
725
+ # Models 탭
726
+ with gr.Tab("🤖 Trending Models"):
727
+ with gr.Row(elem_classes="search-sort-container"):
728
+ with gr.Column(scale=2):
729
+ models_search = gr.Textbox(
730
+ label="🔍 Search Models",
731
+ placeholder="Enter keywords to search...",
732
+ elem_classes="search-box"
733
+ )
734
+ with gr.Column(scale=2):
735
+ models_sort = gr.Radio(
736
+ choices=["rank", "rising_rate", "popularity"],
737
+ value="rank",
738
+ label="Sort by",
739
+ interactive=True
740
+ )
741
+ with gr.Column(scale=1):
742
+ models_refresh_btn = gr.Button(
743
+ "🔄 Refresh",
744
+ variant="primary",
745
+ elem_classes="refresh-btn"
746
+ )
747
+ models_gallery = gr.HTML()
748
+ models_status = gr.Markdown("Loading...")
749
+
750
+ # Datasets 탭
751
+ with gr.Tab("📊 Trending Datasets"):
752
+ with gr.Row(elem_classes="search-sort-container"):
753
+ with gr.Column(scale=2):
754
+ datasets_search = gr.Textbox(
755
+ label="🔍 Search Datasets",
756
+ placeholder="Enter keywords to search...",
757
+ elem_classes="search-box"
758
+ )
759
+ with gr.Column(scale=2):
760
+ datasets_sort = gr.Radio(
761
+ choices=["rank", "rising_rate", "popularity"],
762
+ value="rank",
763
+ label="Sort by",
764
+ interactive=True
765
+ )
766
+ with gr.Column(scale=1):
767
+ datasets_refresh_btn = gr.Button(
768
+ "🔄 Refresh",
769
+ variant="primary",
770
+ elem_classes="refresh-btn"
771
+ )
772
+ datasets_gallery = gr.HTML()
773
+ datasets_status = gr.Markdown("Loading...")
774
+
775
+ # Event handlers
776
+ spaces_refresh_btn.click(
777
+ fn=get_trending_spaces,
778
+ inputs=[spaces_search, spaces_sort],
779
+ outputs=[spaces_gallery, spaces_status]
780
+ )
781
+
782
+ models_refresh_btn.click(
783
+ fn=get_models,
784
+ inputs=[models_search, models_sort],
785
+ outputs=[models_gallery, models_status]
786
+ )
787
+
788
+ datasets_refresh_btn.click(
789
+ fn=get_datasets,
790
+ inputs=[datasets_search, datasets_sort],
791
+ outputs=[datasets_gallery, datasets_status]
792
+ )
793
+
794
+ # 검색어 변경 시 자동 새로고침
795
+ spaces_search.change(
796
+ fn=get_trending_spaces,
797
+ inputs=[spaces_search, spaces_sort],
798
+ outputs=[spaces_gallery, spaces_status]
799
+ )
800
+
801
+ models_search.change(
802
+ fn=get_models,
803
+ inputs=[models_search, models_sort],
804
+ outputs=[models_gallery, models_status]
805
+ )
806
+
807
+ datasets_search.change(
808
+ fn=get_datasets,
809
+ inputs=[datasets_search, datasets_sort],
810
+ outputs=[datasets_gallery, datasets_status]
811
+ )
812
+
813
+ # 정렬 방식 변경 시 자동 새로고침
814
+ spaces_sort.change(
815
+ fn=get_trending_spaces,
816
+ inputs=[spaces_search, spaces_sort],
817
+ outputs=[spaces_gallery, spaces_status]
818
+ )
819
+
820
+ models_sort.change(
821
+ fn=get_models,
822
+ inputs=[models_search, models_sort],
823
+ outputs=[models_gallery, models_status]
824
+ )
825
+
826
+ datasets_sort.change(
827
+ fn=get_datasets,
828
+ inputs=[datasets_search, datasets_sort],
829
+ outputs=[datasets_gallery, datasets_status]
830
+ )
831
+
832
+ # 초기 데이터 로드
833
+ interface.load(
834
+ fn=get_trending_spaces,
835
+ inputs=[spaces_search, spaces_sort],
836
+ outputs=[spaces_gallery, spaces_status]
837
+ )
838
+ interface.load(
839
+ fn=get_models,
840
+ inputs=[models_search, models_sort],
841
+ outputs=[models_gallery, models_status]
842
+ )
843
+ interface.load(
844
+ fn=get_datasets,
845
+ inputs=[datasets_search, datasets_sort],
846
+ outputs=[datasets_gallery, datasets_status]
847
+ )
848
+
849
+ return interface
850
+
851
+ if __name__ == "__main__":
852
+ try:
853
+ demo = create_interface()
854
+ demo.launch(
855
+ share=True,
856
+ inbrowser=True,
857
+ show_api=False
858
+ )
859
+ except Exception as e:
860
+ print(f"Error launching app: {e}")import os
861
+ import random
862
+ import base64
863
+ import requests
864
+ from selenium import webdriver
865
+ from selenium.webdriver.support.ui import WebDriverWait
866
+ from selenium.webdriver.support import expected_conditions as EC
867
+ from selenium.webdriver.common.by import By
868
+ from selenium.common.exceptions import WebDriverException, TimeoutException
869
+ from PIL import Image
870
+ from io import BytesIO
871
+ from datetime import datetime
872
+ import gradio as gr
873
+ from typing import Tuple
874
+ import time
875
+ from pathlib import Path # 추가
876
+
877
+ # 스크린샷 캐시 디렉토리 설정
878
+ CACHE_DIR = Path("screenshot_cache")
879
+ CACHE_DIR.mkdir(exist_ok=True)
880
 
881
+ # 전역 변수로 스크린샷 캐시 선언
882
+ SCREENSHOT_CACHE = {}
883
 
884
+ def get_cached_screenshot(url: str) -> str:
885
+ """캐시된 스크린샷 가져오기 또는 새로 생성"""
886
+ cache_file = CACHE_DIR / f"{base64.b64encode(url.encode()).decode()}.png"
887
+
888
+ if cache_file.exists():
889
+ with open(cache_file, "rb") as f:
890
+ return base64.b64encode(f.read()).decode()
891
+
892
+ return take_screenshot(url)
893
 
894
+ def take_screenshot(url):
895
+ """웹사이트 스크린샷 촬영 함수 (로딩 대기 시간 추가)"""
896
+ if url in SCREENSHOT_CACHE:
897
+ return SCREENSHOT_CACHE[url]
898
 
899
+ if not url.startswith('http'):
900
+ url = f"https://{url}"
901
+
902
+ options = webdriver.ChromeOptions()
903
+ options.add_argument('--headless')
904
+ options.add_argument('--no-sandbox')
905
+ options.add_argument('--disable-dev-shm-usage')
906
+ options.add_argument('--window-size=1080,720')
907
+
908
  try:
909
+ driver = webdriver.Chrome(options=options)
910
+ driver.get(url)
911
+
912
+ # 명시적 대기: body 요소가 로드될 때까지 대기 (최대 10초)
913
+ try:
914
+ WebDriverWait(driver, 10).until(
915
+ EC.presence_of_element_located((By.TAG_NAME, "body"))
916
+ )
917
+ except TimeoutException:
918
+ print(f"페이지 로딩 타임아웃: {url}")
919
+
920
+ # 추가 대기 시간을 2초로 증가
921
+ time.sleep(2) # 1초에서 2초로 변경
922
+
923
+ # JavaScript 실행 완료 대기
924
+ driver.execute_script("return document.readyState") == "complete"
925
+
926
+ # 스크린샷 촬영
927
+ screenshot = driver.get_screenshot_as_png()
928
+ img = Image.open(BytesIO(screenshot))
929
+ buffered = BytesIO()
930
+ img.save(buffered, format="PNG")
931
+ base64_image = base64.b64encode(buffered.getvalue()).decode()
932
 
933
  # 캐시에 저장
934
+ SCREENSHOT_CACHE[url] = base64_image
935
+ return base64_image
936
 
937
+ except WebDriverException as e:
938
+ print(f"스크린샷 촬영 실패: {str(e)} for URL: {url}")
939
+ return None
940
  except Exception as e:
941
+ print(f"예상치 못한 오류: {str(e)} for URL: {url}")
942
+ return None
943
+ finally:
944
+ if 'driver' in locals():
945
+ driver.quit()
946
 
947
+ from datetime import datetime, timedelta
 
948
 
949
+ def calculate_rising_rate(created_date: str, rank: int) -> int:
950
+ """AI Rising Rate 계산"""
951
+ # 생성일 기준 점수 계산
952
+ created = datetime.strptime(created_date.split('T')[0], '%Y-%m-%d')
953
+ today = datetime.now()
954
+ days_diff = (today - created).days
955
+ date_score = max(0, 300 - days_diff) # 최대 300점
956
+
957
+ # 순위 기준 점수 계산
958
+ rank_score = max(0, 300 - rank) # 최대 300점
959
+
960
+ # 총점 계산
961
+ total_score = date_score + rank_score
962
+
963
+ # 별 개수 계산 (0~5)
964
+ if total_score <= 100:
965
+ stars = 1
966
+ elif total_score <= 200:
967
+ stars = 2
968
+ elif total_score <= 300:
969
+ stars = 3
970
+ elif total_score <= 400:
971
+ stars = 4
972
+ else:
973
+ stars = 5
974
 
975
+ return stars
976
+
977
+ def get_popularity_grade(likes: int, stars: int) -> tuple:
978
+ """AI Popularity Score 등급 계산"""
979
+ # 기본 점수 (likes)
980
+ base_score = min(likes, 10000) # 최대 10000점
981
+
982
+ # 별점 추가 점수 (별 하나당 500점)
983
+ star_score = stars * 500
984
+
985
+ # 총점
986
+ total_score = base_score + star_score
987
+
988
+ # 등급 테이블 (18단계)
989
+ grades = [
990
+ (9000, "AAA+"), (8500, "AAA"), (8000, "AAA-"),
991
+ (7500, "AA+"), (7000, "AA"), (6500, "AA-"),
992
+ (6000, "A+"), (5500, "A"), (5000, "A-"),
993
+ (4500, "BBB+"), (4000, "BBB"), (3500, "BBB-"),
994
+ (3000, "BB+"), (2500, "BB"), (2000, "BB-"),
995
+ (1500, "B+"), (1000, "B"), (500, "B-")
996
+ ]
997
+
998
+ for threshold, grade in grades:
999
+ if total_score >= threshold:
1000
+ return grade, total_score
1001
 
1002
+ return "B-", total_score
1003
+
1004
+ # get_card 함수 내의 hardware_info 부분을 다음으로 교체:
1005
+ def get_rating_info(item: dict, index: int) -> str:
1006
+ """평가 정보 HTML 생성"""
1007
+ created = item.get('createdAt', '').split('T')[0]
1008
+ likes = int(str(item.get('likes', '0')).replace(',', ''))
1009
+
1010
+ # AI Rising Rate ��산
1011
+ stars = calculate_rising_rate(created, index + 1)
1012
+ star_html = "★" * stars + "☆" * (5 - stars) # 채워진 별과 빈 별 조합
1013
+
1014
+ # AI Popularity Score 계산
1015
+ grade, score = get_popularity_grade(likes, stars)
1016
+
1017
+ # 등급별 색상 설정
1018
+ grade_colors = {
1019
+ 'AAA': '#FFD700', 'AA': '#FFA500', 'A': '#FF4500',
1020
+ 'BBB': '#4169E1', 'BB': '#1E90FF', 'B': '#00BFFF'
1021
+ }
1022
+ grade_base = grade.rstrip('+-')
1023
+ grade_color = grade_colors.get(grade_base, '#666666')
1024
+
1025
+ return f"""
1026
+ <div style='
1027
+ margin-top: 15px;
1028
+ padding: 15px;
1029
+ background: rgba(255,255,255,0.4);
1030
+ border-radius: 10px;
1031
+ font-size: 0.9em;
1032
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);'>
1033
+ <div style='
1034
+ display: grid;
1035
+ grid-template-columns: repeat(2, 1fr);
1036
+ gap: 15px;'>
1037
+ <div style='
1038
+ color: #333;
1039
+ display: flex;
1040
+ flex-direction: column;
1041
+ gap: 5px;'>
1042
+ <span style='font-weight: bold;'>AI Rising Rate:</span>
1043
+ <span style='
1044
+ color: #FF8C00;
1045
+ font-size: 1.4em;
1046
+ letter-spacing: 2px;
1047
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.1);'>{star_html}</span>
1048
+ </div>
1049
+ <div style='
1050
+ color: #333;
1051
+ display: flex;
1052
+ flex-direction: column;
1053
+ gap: 5px;'>
1054
+ <span style='font-weight: bold;'>AI Popularity Score:</span>
1055
+ <span style='
1056
+ font-size: 1.2em;
1057
+ font-weight: bold;
1058
+ color: {grade_color};
1059
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.1);'>{grade} ({score:,})</span>
1060
+ </div>
1061
+ </div>
1062
+ </div>
1063
+ """
1064
+
1065
+ def get_hardware_info(item: dict) -> tuple:
1066
+ """하드웨어 정보 추출"""
1067
+ try:
1068
+ # runtime 정보 확인
1069
+ runtime = item.get('runtime', {})
1070
+
1071
+ # CPU 정보 처리
1072
+ cpu_info = runtime.get('cpu', 'Standard')
1073
+
1074
+ # GPU 정보 처리
1075
+ gpu_info = "None"
1076
+ if runtime.get('accelerator') == "gpu":
1077
+ gpu_type = runtime.get('gpu', {}).get('name', '')
1078
+ gpu_memory = runtime.get('gpu', {}).get('memory', '')
1079
+ if gpu_type:
1080
+ gpu_info = f"{gpu_type}"
1081
+ if gpu_memory:
1082
+ gpu_info += f" ({gpu_memory}GB)"
1083
+
1084
+ # spaces decorator 확인
1085
+ if '@spaces.GPU' in str(item.get('sdk_version', '')):
1086
+ if gpu_info == "None":
1087
+ gpu_info = "GPU Enabled"
1088
+
1089
+ # SDK 정보 처리
1090
+ sdk = item.get('sdk', 'N/A')
1091
+
1092
+ print(f"Debug - Runtime Info: {runtime}") # 디버그 출력
1093
+ print(f"Debug - GPU Info: {gpu_info}") # 디버그 출력
1094
+
1095
+ return cpu_info, gpu_info, sdk
1096
+
1097
  except Exception as e:
1098
+ print(f"Error parsing hardware info: {str(e)}")
1099
+ return 'Standard', 'None', 'N/A'
1100
 
1101
  def get_card(item: dict, index: int, card_type: str = "space") -> str:
1102
  """통합 카드 HTML 생성"""
 
1105
  likes = format(item.get('likes', 0), ',')
1106
  created = item.get('createdAt', '').split('T')[0]
1107
 
1108
+ # short_description 가져오기
1109
+ short_description = item.get('cardData', {}).get('short_description', '')
1110
 
1111
  # title과 short_description을 포함한 헤더 HTML
1112
  title_html = f"""
1113
+ <h3 style='
1114
+ margin: 0 0 15px 0;
1115
+ color: #333;
1116
+ font-size: 1.3em;
1117
+ line-height: 1.4;
1118
+ display: -webkit-box;
1119
+ -webkit-line-clamp: 2;
1120
+ -webkit-box-orient: vertical;
1121
+ overflow: hidden;
1122
+ text-overflow: ellipsis;
1123
+ text-shadow: 1px 1px 1px rgba(255,255,255,0.8);'>
1124
+ {title}
1125
+ {f'<span style="display: block; font-size: 0.7em; color: #666; margin-top: 5px; font-weight: normal; font-style: italic;">{short_description}</span>' if short_description else ''}
1126
+ </h3>
1127
  """
1128
 
1129
 
 
1292
  text-shadow: 1px 1px 1px rgba(255,255,255,0.8);'>
1293
  {title}
1294
  </h3>
1295
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1296
  <div style='
1297
  display: grid;
1298
  grid-template-columns: repeat(2, 1fr);
 
1325
  progress(0, desc="Fetching spaces data...")
1326
  params = {
1327
  'full': 'true',
1328
+ 'limit': 100
1329
  }
1330
 
1331
  response = requests.get(url, params=params)
 
1348
  reverse=True)
1349
 
1350
  progress(0.1, desc="Creating gallery...")
1351
+ html_content = """
1352
+ <div style='padding: 20px; background: #f5f5f5;'>
1353
+ <div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;'>
1354
+ """
1355
 
1356
  for idx, space in enumerate(spaces):
1357
  html_content += get_card(space, idx, "space")
1358
  progress((0.1 + 0.9 * idx/len(spaces)), desc=f"Loading space {idx+1}/{len(spaces)}...")
1359
 
1360
+ html_content += "</div></div>"
1361
 
1362
  progress(1.0, desc="Complete!")
1363
  return html_content, f"Found {len(spaces)} spaces"
 
1396
  reverse=True)
1397
 
1398
  progress(0.1, desc="Creating gallery...")
1399
+ html_content = """
1400
+ <div style='padding: 20px; background: #f5f5f5;'>
1401
+ <div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;'>
1402
+ """
1403
 
1404
  for idx, model in enumerate(models):
1405
  html_content += get_card(model, idx, "model")
1406
  progress((0.1 + 0.9 * idx/len(models)), desc=f"Loading model {idx+1}/{len(models)}...")
1407
 
1408
+ html_content += "</div></div>"
1409
 
1410
  progress(1.0, desc="Complete!")
1411
  return html_content, f"Found {len(models)} models"
 
1444
  reverse=True)
1445
 
1446
  progress(0.1, desc="Creating gallery...")
1447
+ html_content = """
1448
+ <div style='padding: 20px; background: #f5f5f5;'>
1449
+ <div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;'>
1450
+ """
1451
 
1452
  for idx, dataset in enumerate(datasets):
1453
  html_content += get_card(dataset, idx, "dataset")
1454
  progress((0.1 + 0.9 * idx/len(datasets)), desc=f"Loading dataset {idx+1}/{len(datasets)}...")
1455
 
1456
+ html_content += "</div></div>"
1457
 
1458
  progress(1.0, desc="Complete!")
1459
  return html_content, f"Found {len(datasets)} datasets"
 
1546
  box-shadow: 0 6px 12px rgba(0,0,0,0.2);
1547
  background: linear-gradient(135deg, #8b71ff, #7376f1);
1548
  }
 
 
 
 
 
 
 
 
 
 
 
 
1549
  """) as interface:
 
1550
 
1551
  gr.Markdown("""
1552
  # 🤗 HuggingFace Trending TOP 300 Board