from flask import Flask, render_template, request, jsonify import requests import os import time import random from collections import Counter app = Flask(__name__) # Generate dummy spaces in case of error def generate_dummy_spaces(count): """ API 호출 실패 시 예시용 더미 스페이스 생성 """ spaces = [] for i in range(count): spaces.append({ 'id': f'dummy/space-{i}', 'owner': 'dummy', 'title': f'Example Space {i+1}', 'description': 'Dummy space for fallback', 'likes': 100 - i, 'createdAt': '2023-01-01T00:00:00.000Z', 'hardware': 'cpu', 'user': { 'avatar_url': 'https://huggingface.co/front/thumbnails/huggingface/default-avatar.svg', 'name': 'dummyUser' } }) return spaces # Function to fetch Zero-GPU (CPU-based) Spaces from Huggingface with pagination def fetch_trending_spaces(offset=0, limit=72): """ Trending용 CPU 스페이스 목록 가져오기 (정렬은 Hugging Face 기본 정렬) """ try: url = "https://huggingface.co/api/spaces" params = { "limit": 10000, # 더 많이 가져오기 "hardware": "cpu" # <-- Zero GPU(=CPU) 필터 적용 } response = requests.get(url, params=params, timeout=30) if response.status_code == 200: spaces = response.json() # owner나 id가 'None'인 경우 제외 filtered_spaces = [ space for space in spaces if space.get('owner') != 'None' and space.get('id', '').split('/', 1)[0] != 'None' ] # 전체 목록에 대해 "글로벌 랭크"를 매긴다 (1부터 시작) for i, sp in enumerate(filtered_spaces): sp['global_rank'] = i + 1 # Slice according to requested offset and limit start = min(offset, len(filtered_spaces)) end = min(offset + limit, len(filtered_spaces)) print(f"[fetch_trending_spaces] CPU기반 스페이스 총 {len(filtered_spaces)}개, " f"요청 구간 {start}~{end-1} 반환") return { 'spaces': filtered_spaces[start:end], 'total': len(filtered_spaces), 'offset': offset, 'limit': limit, 'all_spaces': filtered_spaces # 통계 산출용 } else: print(f"Error fetching spaces: {response.status_code}") # 실패 시 더미 데이터 생성 return { 'spaces': generate_dummy_spaces(limit), 'total': 200, 'offset': offset, 'limit': limit, 'all_spaces': generate_dummy_spaces(500) } except Exception as e: print(f"Exception when fetching spaces: {e}") # 실패 시 더미 데이터 생성 return { 'spaces': generate_dummy_spaces(limit), 'total': 200, 'offset': offset, 'limit': limit, 'all_spaces': generate_dummy_spaces(500) } def fetch_latest_spaces(offset=0, limit=72): """ 'createdAt' 기준 내림차순으로 최근 스페이스 500개를 추린 뒤, offset ~ offset+limit 개만 반환 """ try: url = "https://huggingface.co/api/spaces" params = { "limit": 10000, # 충분히 많이 가져옴 "hardware": "cpu" } response = requests.get(url, params=params, timeout=30) if response.status_code == 200: spaces = response.json() # owner나 id가 'None'인 경우 제외 filtered_spaces = [ space for space in spaces if space.get('owner') != 'None' and space.get('id', '').split('/', 1)[0] != 'None' ] # createdAt 내림차순 정렬 # createdAt 예: "2023-01-01T00:00:00.000Z" # 문자열 비교도 가능하지만, 안정성을 위해 time 파싱 후 비교할 수도 있음 def parse_time(sp): return sp.get('createdAt') or '' # 내림차순 filtered_spaces.sort(key=parse_time, reverse=True) # 상위 500개만 추리기 truncated = filtered_spaces[:500] # 필요한 구간 슬라이싱 start = min(offset, len(truncated)) end = min(offset + limit, len(truncated)) print(f"[fetch_latest_spaces] CPU기반 스페이스 총 {len(spaces)}개 중 필터 후 {len(filtered_spaces)}개, 상위 500개 중 {start}~{end-1} 반환") return { 'spaces': truncated[start:end], 'total': len(truncated), # 500 이하 'offset': offset, 'limit': limit } else: print(f"Error fetching spaces: {response.status_code}") return { 'spaces': generate_dummy_spaces(limit), 'total': 500, 'offset': offset, 'limit': limit } except Exception as e: print(f"Exception when fetching spaces: {e}") return { 'spaces': generate_dummy_spaces(limit), 'total': 500, 'offset': offset, 'limit': limit } # Transform Huggingface URL to direct space URL def transform_url(owner, name): """ Hugging Face Space -> 서브도메인 접근 URL 예) huggingface.co/spaces/owner/spaceName -> owner-spacename.hf.space """ # 1. Replace '.' with '-' name = name.replace('.', '-') # 2. Replace '_' with '-' name = name.replace('_', '-') # 3. Convert to lowercase owner = owner.lower() name = name.lower() return f"https://{owner}-{name}.hf.space" # Get space details def get_space_details(space_data, index, offset): """ 스페이스 상세 필드 추출 - rank는 offset 기반 (현재 페이지) """ try: if '/' in space_data.get('id', ''): owner, name = space_data.get('id', '').split('/', 1) else: owner = space_data.get('owner', '') name = space_data.get('id', '') # Ignore if contains None if owner == 'None' or name == 'None': return None # Construct URLs original_url = f"https://huggingface.co/spaces/{owner}/{name}" embed_url = transform_url(owner, name) # Likes count likes_count = space_data.get('likes', 0) # Title title = space_data.get('title') or name # Description short_desc = space_data.get('description', '') # User info user_info = space_data.get('user', {}) avatar_url = user_info.get('avatar_url', '') author_name = user_info.get('name') or owner return { 'url': original_url, 'embedUrl': embed_url, 'title': title, 'owner': owner, 'name': name, 'likes_count': likes_count, 'description': short_desc, 'avatar_url': avatar_url, 'author_name': author_name, 'rank': offset + index + 1 # 현재 페이지 표시용 랭크 } except Exception as e: print(f"Error processing space data: {e}") # 에러 시 기본 데이터로 대체 return { 'url': 'https://huggingface.co/spaces', 'embedUrl': 'https://huggingface.co/spaces', 'title': 'Error Loading Space', 'owner': 'huggingface', 'name': 'error', 'likes_count': 0, 'description': '', 'avatar_url': '', 'author_name': 'huggingface', 'rank': offset + index + 1 } # Get owner statistics from all spaces (for the "Trending" tab's top owners) def get_owner_stats(all_spaces): """ 상위 500위(global_rank <= 500) 이내에 배치된 스페이스들의 owner를 추출해, 각 owner가 몇 번 등장했는지 센 뒤 상위 30명만 반환 """ # Top 500 top_500 = [s for s in all_spaces if s.get('global_rank', 999999) <= 500] owners = [] for space in top_500: if '/' in space.get('id', ''): owner, _ = space.get('id', '').split('/', 1) else: owner = space.get('owner', '') if owner and owner != 'None': owners.append(owner) # Count occurrences of each owner in top 500 owner_counts = Counter(owners) # Get top 30 owners by count top_owners = owner_counts.most_common(30) return top_owners # Homepage route @app.route('/') def home(): """ index.html 템플릿 렌더링 (메인 페이지) """ return render_template('index.html') # Zero-GPU spaces API (Trending) @app.route('/api/trending-spaces', methods=['GET']) def trending_spaces(): """ hardware=cpu 스페이스 목록을 불러와 검색, 페이징, 통계 등을 적용 (기존 'Trending') """ search_query = request.args.get('search', '').lower() offset = int(request.args.get('offset', 0)) limit = int(request.args.get('limit', 72)) # Fetch zero-gpu (cpu) spaces spaces_data = fetch_trending_spaces(offset, limit) # Process and filter spaces results = [] for index, space_data in enumerate(spaces_data['spaces']): space_info = get_space_details(space_data, index, offset) if not space_info: continue # 검색어 필터 if search_query: if (search_query not in space_info['title'].lower() and search_query not in space_info['owner'].lower() and search_query not in space_info['url'].lower() and search_query not in space_info['description'].lower()): continue results.append(space_info) # 오너 통계 (Top 500 → Top 30) top_owners = get_owner_stats(spaces_data.get('all_spaces', [])) return jsonify({ 'spaces': results, 'total': spaces_data['total'], 'offset': offset, 'limit': limit, 'top_owners': top_owners }) # Zero-GPU spaces API (Latest Releases) @app.route('/api/latest-spaces', methods=['GET']) def latest_spaces(): """ hardware=cpu 스페이스 중에서 createdAt 기준으로 최신순 500개를 페이징, 검색 """ search_query = request.args.get('search', '').lower() offset = int(request.args.get('offset', 0)) limit = int(request.args.get('limit', 72)) spaces_data = fetch_latest_spaces(offset, limit) results = [] for index, space_data in enumerate(spaces_data['spaces']): space_info = get_space_details(space_data, index, offset) if not space_info: continue # 검색어 필터 if search_query: if (search_query not in space_info['title'].lower() and search_query not in space_info['owner'].lower() and search_query not in space_info['url'].lower() and search_query not in space_info['description'].lower()): continue results.append(space_info) return jsonify({ 'spaces': results, 'total': spaces_data['total'], 'offset': offset, 'limit': limit }) if __name__ == '__main__': """ 서버 구동 시, templates/index.html 파일을 생성 후 Flask 실행 """ # Create templates folder if not exists os.makedirs('templates', exist_ok=True) # index.html 전체를 새로 작성 with open('templates/index.html', 'w', encoding='utf-8') as f: f.write('''
Discover Zero GPU(Shared A100) spaces from Hugging Face