import os, re, time, json, datetime, requests, gradio as gr # ───────────────────── 1. 기본 설정 ───────────────────── BEST_FILE, PER_PAGE = "best_games.json", 9 # ───────────────────── 2. BEST 데이터 ──────────────────── def _init_best(): if not os.path.exists(BEST_FILE): json.dump([], open(BEST_FILE, "w")) def _load_best(): try: data = json.load(open(BEST_FILE)) for it in data: if "ts" not in it: it["ts"] = int(it.get("timestamp", time.time())) return data except Exception as e: print(f"BEST 데이터 로드 오류: {e}") return [] def _save_best(data): try: json.dump(data, open(BEST_FILE, "w")) return True except Exception as e: print(f"BEST 데이터 저장 오류: {e}") return False # ───────────────────── 3. URL 추가 기능 ───────────────────── def add_url_to_best(title, url): """사용자가 제공한 URL을 BEST 목록에 추가합니다.""" try: # 현재 BEST 데이터 로드 data = _load_best() # URL이 이미 존재하는지 확인 for item in data: if item.get("url") == url: print(f"URL이 이미 존재합니다: {url}") return False # 새 항목 추가 new_item = { "title": title, "url": url, "ts": int(time.time()), "projectId": "", # 사용자가 직접 추가하므로 projectId 없음 "deploymentId": "" # 사용자가 직접 추가하므로 deploymentId 없음 } data.append(new_item) # 시간순으로 정렬 data = sorted(data, key=lambda x: x["ts"], reverse=True) # 저장 if _save_best(data): print(f"URL이 성공적으로 추가되었습니다: {url}") return True return False except Exception as e: print(f"URL 추가 오류: {str(e)}") return False # ───────────────────── 4. 페이지네이션 ─────────────────── def page(lst, pg): s = (pg-1) * PER_PAGE e = s + PER_PAGE total = (len(lst) + PER_PAGE - 1) // PER_PAGE return lst[s:e], total # ───────────────────── 5. URL 처리 함수 ───────────────────── def process_url_for_iframe(url): """URL을 iframe에 표시하기 적합한 형태로 변환합니다.""" # 허깅페이스 URL 패턴 감지 is_huggingface = False embed_urls = [] # 1. huggingface.co/spaces 패턴 처리 if "huggingface.co/spaces" in url: is_huggingface = True # 기본 URL 정규화 base_url = url.rstrip("/") try: # /spaces/ 이후의 경로 추출 if "/spaces/" in base_url: path = base_url.split("/spaces/")[1] parts = path.split("/") owner = parts[0] # name 부분 추출 if len(parts) > 1: name = parts[1] # 특수 문자 변환 clean_name = name.replace('.', '-').replace('_', '-').lower() clean_owner = owner.lower() # 여러 포맷의 URL을 시도하기 위해 목록에 추가 embed_urls.append(f"https://huggingface.co/spaces/{owner}/{name}/embed") # 공식 embed URL embed_urls.append(f"https://{clean_owner}-{clean_name}.hf.space") # 직접 도메인 접근 else: # owner만 있는 경우 공식 URL 사용 embed_urls.append(f"https://huggingface.co/spaces/{owner}/embed") except Exception as e: print(f"허깅페이스 URL 처리 중 오류: {e}") # 기본 embed URL 시도 if not base_url.endswith("/embed"): embed_urls.append(f"{base_url}/embed") else: embed_urls.append(base_url) # 2. .hf.space 도메인 처리 elif ".hf.space" in url: is_huggingface = True embed_urls.append(url) # 현재 URL 그대로 시도 # 3. 일반 URL은 그대로 반환 else: return url, is_huggingface, [] # 최종 URL과 함께 시도할 대체 URL 목록 반환 primary_url = embed_urls[0] if embed_urls else url return primary_url, is_huggingface, embed_urls[1:] if len(embed_urls) > 1 else [] # ───────────────────── 6. HTML 그리드 ─────────────────── def html(cards, pg, total): if not cards: return "
표시할 배포가 없습니다.
" css = r""" """ js = """ """ h = css + js + """
""" for idx, c in enumerate(cards): date = datetime.datetime.fromtimestamp(int(c["ts"])).strftime("%Y-%m-%d") # URL 처리: 허깅페이스 URL인 경우 특별 처리 url = c['url'] iframe_url, is_huggingface, alt_urls = process_url_for_iframe(url) # 허깅페이스 URL에 특별 클래스 추가 frame_class = "frame huggingface" if is_huggingface else "frame" # 고유 ID 생성 iframe_id = f"iframe-{idx}-{hash(url) % 10000}" # 대체 URL을 데이터 속성으로 추가 alternate_urls_attr = "" if alt_urls: alternate_urls_attr = f'data-alternate-urls="{",".join(alt_urls)}"' h += f"""

{c['title']}

{date}

""" h += """
""" # 페이지 정보 h += f'
Page {pg} / {total}
' return h # ───────────────────── 7. Gradio Blocks UI ───────────────────── # ───────────────────── 7. Gradio Blocks UI ───────────────────── def build(): _init_best() # (1) ── 헤더 HTML (변경 없음) ───────────────────────────── header_html_snippet = """

🎮 Vibe Game Gallery

HF-Vibe HF-Gallery Discord

프롬프트 입력만으로 최신 LLM들과 Agent가 협업하여 웹 기반 게임을 생성하고 배포합니다.

""" # (2) ── 전역 CSS (헤더 sticky + 스크롤 영역 재조정) ───────── css_global = """ footer{display:none !important;} /* 상단 헤더를 항상 보이도록 고정 */ .app-header{ position:sticky; top:0; background:#fff; z-index:1100; padding:16px 0 8px; border-bottom:1px solid #eee; } /* 하단 고정 버튼 바 */ .button-row{ position:fixed !important; bottom:0 !important; left:0 !important; right:0 !important; height:60px !important; background:#f0f0f0 !important; padding:10px !important; text-align:center !important; box-shadow:0 -2px 10px rgba(0,0,0,0.05) !important; margin:0 !important; z-index:1000 !important; } .button-row button{ margin:0 10px !important; padding:10px 20px !important; font-size:16px !important; font-weight:bold !important; border-radius:50px !important; } /* 카드 그리드 스크롤 영역 */ #content-area{ overflow-y:auto !important; height:calc(100vh - 60px - 160px) !important; /* 전체-높이 - 하단바 - 헤더 */ box-sizing:border-box; padding-top:10px; } """ # (3) ── Gradio Blocks ───────────────────────────────────── with gr.Blocks(title="Vibe Game Craft", css=css_global) as demo: # ① 고정 헤더 gr.HTML(header_html_snippet) # ② 본문(카드 그리드) → 고유 ID 부여 out = gr.HTML(elem_id="content-area") # ③ 하단 페이지 네비게이션 바 with gr.Row(elem_classes="button-row"): b_prev = gr.Button("◀ 이전", size="lg") b_next = gr.Button("다음 ▶", size="lg") # ── 상태 및 헬퍼 ─────────────────────────── bp = gr.State(1) def show_best(p=1): d, t = page(_load_best(), p) return html(d, p, t), p def prev(b): b = max(1, b-1) h, _ = show_best(b) return h, b def nxt(b): maxp = (len(_load_best()) + PER_PAGE - 1) // PER_PAGE b = min(maxp, b+1) h, _ = show_best(b) return h, b # ── 이벤트 바인딩 ────────────────────────── b_prev.click(prev, inputs=[bp], outputs=[out, bp]) b_next.click(nxt, inputs=[bp], outputs=[out, bp]) # 최초 로드 demo.load(show_best, outputs=[out, bp]) return demo # ───────────────────── 8. 앱 생성 & 실행 ───────────────────── app = build() # ← Blocks 인스턴스 생성 if __name__ == "__main__": # Spaces나 로컬에서 실행될 때 진입점 app.launch() # share=True 등 옵션이 필요하면 여기서 지정