from flask import Flask, render_template, request, jsonify import requests import os from collections import Counter ############################################################################## # 1) 전역 변수 & 더미 데이터 ############################################################################## # 전역 캐시: CPU 전용 스페이스 목록 SPACE_CACHE = [] # 로드 여부 플래그 (최초 요청 시 한 번만 로드) CACHE_LOADED = False def generate_dummy_spaces(count): """ API 호출 실패 시 예시용 더미 스페이스 생성 """ spaces = [] for i in range(count): spaces.append({ 'id': f'dummy/space-{i}', 'owner': 'dummy', 'title': f'Dummy Space {i+1}', 'description': 'This is a fallback dummy space.', '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 ############################################################################## # 2) Hugging Face API에서 CPU 스페이스를 한 번만 가져오는 로직 ############################################################################## def fetch_zero_gpu_spaces_once(): """ Hugging Face API (hardware=cpu) 스페이스 목록을 가져옴 limit을 작게 설정하여 응답 속도를 개선 """ try: url = "https://huggingface.co./api/spaces" params = { "limit": 1000, # 너무 크게 잡으면 응답 지연 → 50개 내외로 제한 "hardware": "cpu" } resp = requests.get(url, params=params, timeout=30) if resp.status_code == 200: raw_spaces = resp.json() # owner나 id가 'None'인 경우 제외 filtered = [ sp for sp in raw_spaces if sp.get('owner') != 'None' and sp.get('id', '').split('/', 1)[0] != 'None' ] # global_rank 부여 for i, sp in enumerate(filtered): sp['global_rank'] = i + 1 print(f"[fetch_zero_gpu_spaces_once] 로드된 스페이스: {len(filtered)}개") return filtered else: print(f"[fetch_zero_gpu_spaces_once] API 에러: {resp.status_code}") except Exception as e: print(f"[fetch_zero_gpu_spaces_once] 예외 발생: {e}") # 실패 시 더미 데이터 반환 print("[fetch_zero_gpu_spaces_once] 실패 → 더미 데이터 사용") return generate_dummy_spaces(100) def ensure_cache_loaded(): """ Lazy Loading: - 최초 요청이 들어왔을 때만 캐시를 로드 - 이미 로드되었다면 아무 것도 하지 않음 """ global CACHE_LOADED, SPACE_CACHE if not CACHE_LOADED: SPACE_CACHE = fetch_zero_gpu_spaces_once() CACHE_LOADED = True print(f"[ensure_cache_loaded] Loaded {len(SPACE_CACHE)} CPU-based spaces into cache.") ############################################################################## # 3) Flask 앱 생성 & 유틸 함수 ############################################################################## app = Flask(__name__) def transform_url(owner, name): """ huggingface.co/spaces/owner/spaceName -> owner-spacename.hf.space 변환 """ owner = owner.lower() # '.'와 '_'를 '-'로 치환 name = name.replace('.', '-').replace('_', '-').lower() return f"https://{owner}-{name}.hf.space" def get_space_details(space_data, index, offset): """ 특정 스페이스 정보를 Python dict로 정리 - rank: (offset + index + 1) """ try: space_id = space_data.get('id', '') if '/' in space_id: owner, name = space_id.split('/', 1) else: owner = space_data.get('owner', '') name = space_id if owner == 'None' or name == 'None': return None original_url = f"https://huggingface.co./spaces/{owner}/{name}" embed_url = transform_url(owner, name) likes_count = space_data.get('likes', 0) title = space_data.get('title') or name short_desc = space_data.get('description', '') 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"[get_space_details] 예외: {e}") return None def get_owner_stats(all_spaces): """ 상위 500(global_rank<=500)에 속하는 스페이스의 owner 빈도수 상위 30명 """ top_500 = [s for s in all_spaces if s.get('global_rank', 999999) <= 500] owners = [] for sp in top_500: sp_id = sp.get('id', '') if '/' in sp_id: o, _ = sp_id.split('/', 1) else: o = sp.get('owner', '') if o and o != 'None': owners.append(o) counts = Counter(owners) return counts.most_common(30) def fetch_trending_spaces(offset=0, limit=24): """ 이미 캐시된 SPACE_CACHE를 offset, limit로 슬라이싱 """ global SPACE_CACHE total = len(SPACE_CACHE) start = min(offset, total) end = min(offset + limit, total) sliced = SPACE_CACHE[start:end] return { 'spaces': sliced, 'total': total, 'offset': offset, 'limit': limit, 'all_spaces': SPACE_CACHE } ############################################################################## # 4) Flask 라우트 ############################################################################## @app.route('/') def home(): """ 메인 페이지(index.html) 렌더링 """ return render_template('index.html') @app.route('/api/trending-spaces', methods=['GET']) def trending_spaces(): """ Zero-GPU 스페이스 목록을 반환하는 API: - Lazy Load로 캐시 로드 - offset, limit 파라미터로 페이지네이션 - search 파라미터로 검색 - 상위 500 내 owner 통계 """ # 요청 들어올 때 캐시 미로드라면 여기서 로드 ensure_cache_loaded() search_query = request.args.get('search', '').lower() offset = int(request.args.get('offset', 0)) limit = int(request.args.get('limit', 24)) data = fetch_trending_spaces(offset, limit) results = [] for idx, sp in enumerate(data['spaces']): info = get_space_details(sp, idx, offset) if not info: continue # 검색어 필터 적용 (title, owner, url, description) if search_query: text_block = " ".join([ info['title'].lower(), info['owner'].lower(), info['url'].lower(), info['description'].lower() ]) if search_query not in text_block: continue results.append(info) top_owners = get_owner_stats(data['all_spaces']) return jsonify({ 'spaces': results, 'total': data['total'], 'offset': offset, 'limit': limit, 'top_owners': top_owners }) ############################################################################## # 5) 서버 실행 (templates/index.html 작성) ############################################################################## if __name__ == '__main__': # templates 디렉토리 생성 os.makedirs('templates', exist_ok=True) # ------------------- # index.html 전체 생성 # 아래는 질문에 주어진 '무한 로딩' 문제 해결용 최종 HTML+JS 예시 (CSS 포함) # ------------------- with open('templates/index.html', 'w', encoding='utf-8') as f: f.write('''
Discover Zero GPU(Shared A100) spaces from Hugging Face