Spaces:
Running
Running
from flask import Flask, render_template, request, jsonify | |
import requests | |
import os | |
import random | |
app = Flask(__name__) | |
# Huggingface URL list | |
HUGGINGFACE_URLS = [ | |
"https://huggingface.co/spaces/ginipick/Tech_Hangman_Game", | |
"https://huggingface.co/spaces/openfree/deepseek_r1_API", | |
"https://huggingface.co/spaces/ginipick/open_Deep-Research", | |
"https://huggingface.co/spaces/aiqmaster/open-deep-research", | |
"https://huggingface.co/spaces/seawolf2357/DeepSeek-R1-32b-search", | |
"https://huggingface.co/spaces/ginigen/LLaDA", | |
"https://huggingface.co/spaces/VIDraft/PHI4-Multimodal", | |
"https://huggingface.co/spaces/ginigen/Ovis2-8B", | |
"https://huggingface.co/spaces/ginigen/Graph-Mind", | |
"https://huggingface.co/spaces/ginigen/Workflow-Canvas", | |
"https://huggingface.co/spaces/ginigen/Design", | |
"https://huggingface.co/spaces/ginigen/Diagram", | |
"https://huggingface.co/spaces/ginigen/Mockup", | |
"https://huggingface.co/spaces/ginigen/Infographic", | |
"https://huggingface.co/spaces/ginigen/Flowchart", | |
"https://huggingface.co/spaces/aiqcamp/FLUX-Vision", | |
"https://huggingface.co/spaces/ginigen/VoiceClone-TTS", | |
"https://huggingface.co/spaces/openfree/Perceptron-Network", | |
"https://huggingface.co/spaces/openfree/Article-Generator", | |
] | |
# Transform Huggingface URL to direct space URL | |
def transform_url(url): | |
prefix = "https://huggingface.co/spaces/" | |
if url.startswith(prefix): | |
rest = url[len(prefix):] | |
return f"https://{rest.replace('/', '-')}.hf.space" | |
return url | |
# Extract model/space info from URL | |
def extract_model_info(url): | |
parts = url.split('/') | |
if len(parts) < 6: | |
return None | |
if parts[3] == 'spaces' or parts[3] == 'models': | |
return { | |
'type': parts[3], | |
'owner': parts[4], | |
'repo': parts[5], | |
'full_id': f"{parts[4]}/{parts[5]}" | |
} | |
elif len(parts) >= 5: | |
# Other URL format | |
return { | |
'type': 'models', # Default | |
'owner': parts[3], | |
'repo': parts[4], | |
'full_id': f"{parts[3]}/{parts[4]}" | |
} | |
return None | |
# Extract title from the last part of URL | |
def extract_title(url): | |
parts = url.split("/") | |
title = parts[-1] if parts else "" | |
return title.replace("_", " ").replace("-", " ") | |
# Generate random likes count (since we're removing the actual likes functionality) | |
def generate_likes_count(): | |
return random.randint(10, 500) | |
# Homepage route | |
def home(): | |
return render_template('index.html') | |
# URL list API | |
def get_urls(): | |
search_query = request.args.get('search', '').lower() | |
results = [] | |
for url in HUGGINGFACE_URLS: | |
title = extract_title(url) | |
model_info = extract_model_info(url) | |
transformed_url = transform_url(url) | |
if not model_info: | |
continue | |
if search_query and search_query not in url.lower() and search_query not in title.lower(): | |
continue | |
# Generate random likes count | |
likes_count = generate_likes_count() | |
results.append({ | |
'url': url, | |
'embedUrl': transformed_url, | |
'title': title, | |
'model_info': model_info, | |
'likes_count': likes_count, | |
'owner': model_info['owner'] # Include owner ID | |
}) | |
return jsonify(results) | |
if __name__ == '__main__': | |
# Create templates folder | |
os.makedirs('templates', exist_ok=True) | |
# Create index.html file | |
with open('templates/index.html', 'w', encoding='utf-8') as f: | |
f.write(''' | |
<!DOCTYPE html> | |
<html lang="ko"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>허깅페이스 스페이스 그리드</title> | |
<style> | |
body { | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
line-height: 1.6; | |
margin: 0; | |
padding: 0; | |
color: #333; | |
background-color: #f5f8fa; | |
} | |
.container { | |
max-width: 1400px; | |
margin: 0 auto; | |
padding: 1rem; | |
} | |
.header { | |
background-color: #ffffff; | |
padding: 1rem; | |
border-radius: 8px; | |
margin-bottom: 1rem; | |
box-shadow: 0 2px 10px rgba(0,0,0,0.05); | |
text-align: center; | |
} | |
.header h1 { | |
margin: 0; | |
color: #2c3e50; | |
font-size: 1.8rem; | |
} | |
.filter-controls { | |
background-color: #ffffff; | |
padding: 1rem; | |
border-radius: 8px; | |
margin-bottom: 1rem; | |
box-shadow: 0 2px 10px rgba(0,0,0,0.05); | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
} | |
input[type="text"] { | |
padding: 0.7rem; | |
border: 1px solid #ddd; | |
border-radius: 4px; | |
margin-right: 5px; | |
font-size: 1rem; | |
width: 300px; | |
} | |
.grid-container { | |
display: grid; | |
grid-template-columns: repeat(auto-fill, minmax(450px, 1fr)); | |
gap: 1.5rem; | |
} | |
.grid-item { | |
background-color: #ffffff; | |
border-radius: 8px; | |
overflow: hidden; | |
box-shadow: 0 4px 15px rgba(0,0,0,0.1); | |
display: flex; | |
flex-direction: column; | |
height: 600px; | |
position: relative; | |
} | |
.grid-header { | |
padding: 0.8rem; | |
border-bottom: 1px solid #eee; | |
background-color: #f9f9f9; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
z-index: 2; | |
} | |
.grid-header-left { | |
display: flex; | |
flex-direction: column; | |
} | |
.grid-header h3 { | |
margin: 0; | |
padding: 0; | |
font-size: 1.1rem; | |
color: #333; | |
white-space: nowrap; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
} | |
.owner-info { | |
font-size: 0.85rem; | |
color: #666; | |
margin-top: 3px; | |
} | |
.grid-actions { | |
display: flex; | |
align-items: center; | |
} | |
.open-link { | |
color: #4CAF50; | |
text-decoration: none; | |
font-size: 0.9rem; | |
margin-left: 10px; | |
} | |
.likes-counter { | |
display: flex; | |
align-items: center; | |
font-size: 0.9rem; | |
color: #e91e63; | |
} | |
.likes-counter span { | |
margin-left: 4px; | |
} | |
.grid-content { | |
flex: 1; | |
position: relative; | |
overflow: hidden; | |
} | |
.grid-content iframe { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
border: none; | |
} | |
.loading { | |
position: fixed; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
background-color: rgba(255, 255, 255, 0.8); | |
display: none; | |
justify-content: center; | |
align-items: center; | |
z-index: 1000; | |
font-size: 1.5rem; | |
} | |
.loading-spinner { | |
border: 5px solid #f3f3f3; | |
border-top: 5px solid #4CAF50; | |
border-radius: 50%; | |
width: 50px; | |
height: 50px; | |
animation: spin 1s linear infinite; | |
} | |
@keyframes spin { | |
0% { transform: rotate(0deg); } | |
100% { transform: rotate(360deg); } | |
} | |
@media (max-width: 768px) { | |
.filter-controls { | |
flex-direction: column; | |
align-items: flex-start; | |
} | |
.filter-controls > * { | |
margin-bottom: 0.5rem; | |
width: 100%; | |
} | |
.grid-container { | |
grid-template-columns: 1fr; | |
} | |
input[type="text"] { | |
width: 100%; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="header"> | |
<h1>허깅페이스 스페이스 임베딩 뷰어</h1> | |
</div> | |
<div class="filter-controls"> | |
<input type="text" id="searchInput" placeholder="URL 또는 제목으로 검색" /> | |
</div> | |
<div id="gridContainer" class="grid-container"></div> | |
</div> | |
<div id="loadingIndicator" class="loading"> | |
<div class="loading-spinner"></div> | |
</div> | |
<script> | |
// DOM element references | |
const elements = { | |
gridContainer: document.getElementById('gridContainer'), | |
loadingIndicator: document.getElementById('loadingIndicator'), | |
searchInput: document.getElementById('searchInput') | |
}; | |
// Application state | |
const state = { | |
isLoading: false | |
}; | |
// Display loading indicator | |
function setLoading(isLoading) { | |
state.isLoading = isLoading; | |
elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none'; | |
} | |
// API error handling | |
async function handleApiResponse(response) { | |
if (!response.ok) { | |
const errorText = await response.text(); | |
throw new Error(`API 오류 (${response.status}): ${errorText}`); | |
} | |
return response.json(); | |
} | |
// Load URL list | |
async function loadUrls() { | |
setLoading(true); | |
try { | |
const searchText = elements.searchInput.value; | |
const response = await fetch(`/api/urls?search=${encodeURIComponent(searchText)}`); | |
const urls = await handleApiResponse(response); | |
renderGrid(urls); | |
} catch (error) { | |
console.error('URL 목록 로드 오류:', error); | |
alert(`URL 로드 오류: ${error.message}`); | |
} finally { | |
setLoading(false); | |
} | |
} | |
// Render grid | |
function renderGrid(urls) { | |
elements.gridContainer.innerHTML = ''; | |
if (!urls || urls.length === 0) { | |
const noResultsMsg = document.createElement('p'); | |
noResultsMsg.textContent = '표시할 URL이 없습니다.'; | |
noResultsMsg.style.padding = '1rem'; | |
noResultsMsg.style.fontStyle = 'italic'; | |
elements.gridContainer.appendChild(noResultsMsg); | |
return; | |
} | |
urls.forEach(item => { | |
const { url, embedUrl, title, likes_count, owner } = item; | |
// Create grid item | |
const gridItem = document.createElement('div'); | |
gridItem.className = 'grid-item'; | |
// Header with title and actions | |
const header = document.createElement('div'); | |
header.className = 'grid-header'; | |
// Left side of header (title and owner) | |
const headerLeft = document.createElement('div'); | |
headerLeft.className = 'grid-header-left'; | |
const titleEl = document.createElement('h3'); | |
titleEl.textContent = title; | |
headerLeft.appendChild(titleEl); | |
const ownerEl = document.createElement('div'); | |
ownerEl.className = 'owner-info'; | |
ownerEl.textContent = `by: ${owner}`; | |
headerLeft.appendChild(ownerEl); | |
header.appendChild(headerLeft); | |
// Actions container | |
const actionsDiv = document.createElement('div'); | |
actionsDiv.className = 'grid-actions'; | |
// Likes count | |
const likesCounter = document.createElement('div'); | |
likesCounter.className = 'likes-counter'; | |
likesCounter.innerHTML = '♥ <span>' + likes_count + '</span>'; | |
actionsDiv.appendChild(likesCounter); | |
// Open link | |
const linkEl = document.createElement('a'); | |
linkEl.href = url; | |
linkEl.target = '_blank'; | |
linkEl.className = 'open-link'; | |
linkEl.textContent = '새 창에서 열기'; | |
actionsDiv.appendChild(linkEl); | |
header.appendChild(actionsDiv); | |
// Add header to grid item | |
gridItem.appendChild(header); | |
// Content with iframe | |
const content = document.createElement('div'); | |
content.className = 'grid-content'; | |
// Create iframe to display the content | |
const iframe = document.createElement('iframe'); | |
iframe.src = embedUrl; | |
iframe.title = title; | |
iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope; microphone; midi'; | |
iframe.setAttribute('allowfullscreen', ''); | |
iframe.setAttribute('frameborder', '0'); | |
iframe.loading = 'lazy'; // Lazy load iframes for better performance | |
content.appendChild(iframe); | |
// Add content to grid item | |
gridItem.appendChild(content); | |
// Add grid item to container | |
elements.gridContainer.appendChild(gridItem); | |
}); | |
} | |
// Filter event listeners | |
elements.searchInput.addEventListener('input', () => { | |
// Debounce input to prevent API calls on every keystroke | |
clearTimeout(state.searchTimeout); | |
state.searchTimeout = setTimeout(loadUrls, 300); | |
}); | |
// Initialize | |
loadUrls(); | |
</script> | |
</body> | |
</html> | |
''') | |
# Use port 7860 for Huggingface Spaces | |
app.run(host='0.0.0.0', port=7860) |