Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -29,17 +29,19 @@ def generate_dummy_spaces(count):
|
|
29 |
})
|
30 |
return spaces
|
31 |
|
32 |
-
# Function to fetch Zero-GPU (CPU-based) Spaces
|
33 |
def fetch_trending_spaces(offset=0, limit=72):
|
34 |
"""
|
35 |
-
hardware=cpu
|
|
|
|
|
36 |
"""
|
37 |
try:
|
38 |
url = "https://huggingface.co/api/spaces"
|
39 |
params = {
|
40 |
-
"limit": 10000, #
|
41 |
-
"hardware": "cpu"
|
42 |
-
|
43 |
}
|
44 |
response = requests.get(url, params=params, timeout=30)
|
45 |
|
@@ -53,19 +55,25 @@ def fetch_trending_spaces(offset=0, limit=72):
|
|
53 |
and space.get('id', '').split('/', 1)[0] != 'None'
|
54 |
]
|
55 |
|
56 |
-
#
|
57 |
-
|
58 |
-
|
59 |
|
60 |
-
|
61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
62 |
|
63 |
return {
|
64 |
'spaces': filtered_spaces[start:end],
|
65 |
-
'total':
|
66 |
'offset': offset,
|
67 |
'limit': limit,
|
68 |
-
'all_spaces': filtered_spaces #
|
69 |
}
|
70 |
else:
|
71 |
print(f"Error fetching spaces: {response.status_code}")
|
@@ -80,7 +88,6 @@ def fetch_trending_spaces(offset=0, limit=72):
|
|
80 |
|
81 |
except Exception as e:
|
82 |
print(f"Exception when fetching spaces: {e}")
|
83 |
-
# 실패 시 더미 데이터 생성
|
84 |
return {
|
85 |
'spaces': generate_dummy_spaces(limit),
|
86 |
'total': 200,
|
@@ -93,7 +100,7 @@ def fetch_trending_spaces(offset=0, limit=72):
|
|
93 |
def transform_url(owner, name):
|
94 |
"""
|
95 |
Hugging Face Space -> 서브도메인 접근 URL
|
96 |
-
|
97 |
"""
|
98 |
name = name.replace('.', '-').replace('_', '-')
|
99 |
owner = owner.lower()
|
@@ -103,7 +110,7 @@ def transform_url(owner, name):
|
|
103 |
# Get space details
|
104 |
def get_space_details(space_data, index, offset):
|
105 |
"""
|
106 |
-
|
107 |
"""
|
108 |
try:
|
109 |
if '/' in space_data.get('id', ''):
|
@@ -112,7 +119,6 @@ def get_space_details(space_data, index, offset):
|
|
112 |
owner = space_data.get('owner', '')
|
113 |
name = space_data.get('id', '')
|
114 |
|
115 |
-
# Ignore if 'None'
|
116 |
if owner == 'None' or name == 'None':
|
117 |
return None
|
118 |
|
@@ -137,11 +143,11 @@ def get_space_details(space_data, index, offset):
|
|
137 |
'description': short_desc,
|
138 |
'avatar_url': avatar_url,
|
139 |
'author_name': author_name,
|
|
|
140 |
'rank': offset + index + 1
|
141 |
}
|
142 |
except Exception as e:
|
143 |
print(f"Error processing space data: {e}")
|
144 |
-
# 최소한의 데이터라도 리턴
|
145 |
return {
|
146 |
'url': 'https://huggingface.co/spaces',
|
147 |
'embedUrl': 'https://huggingface.co/spaces',
|
@@ -158,8 +164,8 @@ def get_space_details(space_data, index, offset):
|
|
158 |
# Get owner statistics from all spaces
|
159 |
def get_owner_stats(all_spaces):
|
160 |
"""
|
161 |
-
all_spaces:
|
162 |
-
|
163 |
"""
|
164 |
owners = []
|
165 |
for space in all_spaces:
|
@@ -175,23 +181,24 @@ def get_owner_stats(all_spaces):
|
|
175 |
top_owners = owner_counts.most_common(30)
|
176 |
return top_owners
|
177 |
|
|
|
|
|
178 |
@app.route('/')
|
179 |
def home():
|
180 |
-
"""
|
181 |
-
index.html 템플릿 (프론트엔드) 로드
|
182 |
-
"""
|
183 |
return render_template('index.html')
|
184 |
|
185 |
@app.route('/api/trending-spaces', methods=['GET'])
|
186 |
def trending_spaces():
|
187 |
"""
|
188 |
-
|
189 |
-
|
|
|
190 |
"""
|
191 |
search_query = request.args.get('search', '').lower()
|
192 |
offset = int(request.args.get('offset', 0))
|
193 |
limit = int(request.args.get('limit', 72))
|
194 |
|
|
|
195 |
spaces_data = fetch_trending_spaces(offset, limit)
|
196 |
|
197 |
results = []
|
@@ -210,7 +217,6 @@ def trending_spaces():
|
|
210 |
|
211 |
results.append(space_info)
|
212 |
|
213 |
-
# 전체 CPU 스페이스를 대상으로 Top 30 오너 추출
|
214 |
top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
|
215 |
|
216 |
return jsonify({
|
@@ -223,17 +229,16 @@ def trending_spaces():
|
|
223 |
|
224 |
if __name__ == '__main__':
|
225 |
"""
|
226 |
-
|
227 |
"""
|
228 |
os.makedirs('templates', exist_ok=True)
|
229 |
|
230 |
-
# index.html 생성 (깨진 이미지 대신 로봇 이모지 + 기존 전체 코드 유지)
|
231 |
with open('templates/index.html', 'w', encoding='utf-8') as f:
|
232 |
f.write('''<!DOCTYPE html>
|
233 |
<html lang="en">
|
234 |
<head>
|
235 |
<meta charset="UTF-8">
|
236 |
-
<title>
|
237 |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
238 |
<style>
|
239 |
/* Google Fonts & Base Styling */
|
@@ -266,7 +271,7 @@ if __name__ == '__main__':
|
|
266 |
}
|
267 |
|
268 |
body {
|
269 |
-
font-family: 'Nunito',
|
270 |
line-height: 1.6;
|
271 |
color: var(--text-primary);
|
272 |
background-color: #f8f9fa;
|
@@ -755,60 +760,6 @@ if __name__ == '__main__':
|
|
755 |
width: 100%;
|
756 |
height: 500px;
|
757 |
}
|
758 |
-
|
759 |
-
/* Responsive Design */
|
760 |
-
@media (max-width: 768px) {
|
761 |
-
body {
|
762 |
-
padding: 1rem;
|
763 |
-
}
|
764 |
-
|
765 |
-
.grid-container {
|
766 |
-
grid-template-columns: 1fr;
|
767 |
-
}
|
768 |
-
|
769 |
-
.search-bar {
|
770 |
-
flex-direction: column;
|
771 |
-
padding: 10px;
|
772 |
-
}
|
773 |
-
|
774 |
-
.search-bar input {
|
775 |
-
width: 100%;
|
776 |
-
margin-bottom: 10px;
|
777 |
-
}
|
778 |
-
|
779 |
-
.search-bar .refresh-btn {
|
780 |
-
width: 100%;
|
781 |
-
justify-content: center;
|
782 |
-
}
|
783 |
-
|
784 |
-
.pagination {
|
785 |
-
flex-wrap: wrap;
|
786 |
-
}
|
787 |
-
|
788 |
-
.chart-container {
|
789 |
-
height: 300px;
|
790 |
-
}
|
791 |
-
}
|
792 |
-
|
793 |
-
.error-emoji-detector {
|
794 |
-
position: fixed;
|
795 |
-
top: -9999px;
|
796 |
-
left: -9999px;
|
797 |
-
z-index: -1;
|
798 |
-
opacity: 0;
|
799 |
-
}
|
800 |
-
|
801 |
-
/* 로봇 이모지 대체 */
|
802 |
-
.emoji-avatar {
|
803 |
-
font-size: 1.2rem;
|
804 |
-
width: 32px;
|
805 |
-
height: 32px;
|
806 |
-
border-radius: 50%;
|
807 |
-
border: 1px solid #ccc;
|
808 |
-
display: flex;
|
809 |
-
align-items: center;
|
810 |
-
justify-content: center;
|
811 |
-
}
|
812 |
</style>
|
813 |
</head>
|
814 |
<body>
|
@@ -825,8 +776,8 @@ if __name__ == '__main__':
|
|
825 |
|
826 |
<div class="mac-content">
|
827 |
<div class="header">
|
828 |
-
<h1>Zero GPU Spaces</h1>
|
829 |
-
<p>Discover
|
830 |
</div>
|
831 |
|
832 |
<!-- Tab Navigation -->
|
@@ -835,7 +786,7 @@ if __name__ == '__main__':
|
|
835 |
<button id="tabFixedButton" class="tab-button">Fixed Tab</button>
|
836 |
</div>
|
837 |
|
838 |
-
<!-- Trending
|
839 |
<div id="trendingTab" class="tab-content active">
|
840 |
<div class="stats-window mac-window">
|
841 |
<div class="mac-toolbar">
|
@@ -848,7 +799,7 @@ if __name__ == '__main__':
|
|
848 |
</div>
|
849 |
<div class="mac-content">
|
850 |
<div class="stats-header">
|
851 |
-
<div class="stats-title">Top 30 Creators by Count
|
852 |
<button id="statsToggle" class="stats-toggle">Show Stats</button>
|
853 |
</div>
|
854 |
<div id="statsContent" class="stats-content">
|
@@ -891,7 +842,7 @@ if __name__ == '__main__':
|
|
891 |
</div>
|
892 |
|
893 |
<script>
|
894 |
-
// DOM
|
895 |
const elements = {
|
896 |
gridContainer: document.getElementById('gridContainer'),
|
897 |
loadingIndicator: document.getElementById('loadingIndicator'),
|
@@ -932,10 +883,7 @@ if __name__ == '__main__':
|
|
932 |
|
933 |
startChecking: function(iframe, owner, name, title, spaceKey) {
|
934 |
this.checkQueue[spaceKey] = {
|
935 |
-
iframe
|
936 |
-
owner: owner,
|
937 |
-
name: name,
|
938 |
-
title: title,
|
939 |
attempts: 0,
|
940 |
status: 'loading'
|
941 |
};
|
@@ -963,10 +911,8 @@ if __name__ == '__main__':
|
|
963 |
item.iframe.contentWindow.document.body;
|
964 |
if (hasContent && item.iframe.contentWindow.document.body.innerHTML.length > 100) {
|
965 |
const bodyText = item.iframe.contentWindow.document.body.textContent.toLowerCase();
|
966 |
-
if (bodyText.includes('forbidden') ||
|
967 |
-
bodyText.includes('
|
968 |
-
bodyText.includes('not found') ||
|
969 |
-
bodyText.includes('error')) {
|
970 |
item.status = 'error';
|
971 |
handleIframeError(item.iframe, item.owner, item.name, item.title);
|
972 |
} else {
|
@@ -976,7 +922,7 @@ if __name__ == '__main__':
|
|
976 |
return;
|
977 |
}
|
978 |
} catch(e) {
|
979 |
-
//
|
980 |
}
|
981 |
|
982 |
const rect = item.iframe.getBoundingClientRect();
|
@@ -996,6 +942,7 @@ if __name__ == '__main__':
|
|
996 |
delete this.checkQueue[spaceKey];
|
997 |
return;
|
998 |
}
|
|
|
999 |
const nextDelay = this.checkInterval * Math.pow(1.5, item.attempts - 1);
|
1000 |
setTimeout(() => this.checkIframeStatus(spaceKey), nextDelay);
|
1001 |
|
@@ -1016,7 +963,7 @@ if __name__ == '__main__':
|
|
1016 |
state.statsVisible = !state.statsVisible;
|
1017 |
elements.statsContent.classList.toggle('open', state.statsVisible);
|
1018 |
elements.statsToggle.textContent = state.statsVisible ? 'Hide Stats' : 'Show Stats';
|
1019 |
-
|
1020 |
if (state.statsVisible && state.topOwners.length > 0) {
|
1021 |
renderCreatorStats();
|
1022 |
}
|
@@ -1102,9 +1049,7 @@ if __name__ == '__main__':
|
|
1102 |
const timeoutPromise = new Promise((_, reject) =>
|
1103 |
setTimeout(() => reject(new Error('Request timeout')), 30000)
|
1104 |
);
|
1105 |
-
const fetchPromise = fetch(
|
1106 |
-
`/api/trending-spaces?search=${encodeURIComponent(searchText)}&offset=${offset}&limit=${state.itemsPerPage}`
|
1107 |
-
);
|
1108 |
const response = await Promise.race([fetchPromise, timeoutPromise]);
|
1109 |
const data = await response.json();
|
1110 |
|
@@ -1142,7 +1087,7 @@ if __name__ == '__main__':
|
|
1142 |
elements.pagination.innerHTML = '';
|
1143 |
const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
|
1144 |
|
1145 |
-
//
|
1146 |
const prevButton = document.createElement('button');
|
1147 |
prevButton.className = 'pagination-button';
|
1148 |
prevButton.textContent = 'Previous';
|
@@ -1154,6 +1099,7 @@ if __name__ == '__main__':
|
|
1154 |
});
|
1155 |
elements.pagination.appendChild(prevButton);
|
1156 |
|
|
|
1157 |
const maxButtons = 7;
|
1158 |
let startPage = Math.max(0, state.currentPage - Math.floor(maxButtons / 2));
|
1159 |
let endPage = Math.min(totalPages - 1, startPage + maxButtons - 1);
|
@@ -1174,7 +1120,7 @@ if __name__ == '__main__':
|
|
1174 |
elements.pagination.appendChild(pageButton);
|
1175 |
}
|
1176 |
|
1177 |
-
// Next
|
1178 |
const nextButton = document.createElement('button');
|
1179 |
nextButton.className = 'pagination-button';
|
1180 |
nextButton.textContent = 'Next';
|
@@ -1189,14 +1135,13 @@ if __name__ == '__main__':
|
|
1189 |
|
1190 |
function handleIframeError(iframe, owner, name, title) {
|
1191 |
const container = iframe.parentNode;
|
1192 |
-
|
1193 |
const errorPlaceholder = document.createElement('div');
|
1194 |
errorPlaceholder.className = 'error-placeholder';
|
1195 |
-
|
1196 |
const errorMessage = document.createElement('p');
|
1197 |
errorMessage.textContent = `"${title}" space couldn't be loaded`;
|
1198 |
errorPlaceholder.appendChild(errorMessage);
|
1199 |
-
|
1200 |
const directLink = document.createElement('a');
|
1201 |
directLink.href = `https://huggingface.co/spaces/${owner}/${name}`;
|
1202 |
directLink.target = '_blank';
|
@@ -1209,12 +1154,12 @@ if __name__ == '__main__':
|
|
1209 |
directLink.style.borderRadius = '5px';
|
1210 |
directLink.style.fontWeight = '600';
|
1211 |
errorPlaceholder.appendChild(directLink);
|
1212 |
-
|
1213 |
iframe.style.display = 'none';
|
1214 |
container.appendChild(errorPlaceholder);
|
1215 |
}
|
1216 |
|
1217 |
-
//
|
1218 |
function renderGrid(spaces) {
|
1219 |
elements.gridContainer.innerHTML = '';
|
1220 |
|
@@ -1239,75 +1184,64 @@ if __name__ == '__main__':
|
|
1239 |
const gridItem = document.createElement('div');
|
1240 |
gridItem.className = 'grid-item';
|
1241 |
|
|
|
1242 |
const headerDiv = document.createElement('div');
|
1243 |
headerDiv.className = 'grid-header';
|
1244 |
|
1245 |
-
//
|
1246 |
-
const
|
1247 |
-
|
1248 |
-
|
1249 |
-
// 🤖 이모지
|
1250 |
-
const emojiAvatar = document.createElement('div');
|
1251 |
-
emojiAvatar.className = 'emoji-avatar';
|
1252 |
-
emojiAvatar.textContent = '🤖';
|
1253 |
-
spaceHeader.appendChild(emojiAvatar);
|
1254 |
|
1255 |
-
//
|
1256 |
-
const
|
1257 |
-
|
1258 |
-
|
|
|
1259 |
|
|
|
1260 |
const titleEl = document.createElement('h3');
|
1261 |
-
titleEl.className = 'space-title';
|
1262 |
titleEl.textContent = title;
|
1263 |
titleEl.title = title;
|
1264 |
-
|
1265 |
|
|
|
1266 |
const zeroGpuBadge = document.createElement('span');
|
1267 |
-
zeroGpuBadge.className = '
|
|
|
|
|
1268 |
zeroGpuBadge.textContent = 'ZERO GPU';
|
1269 |
-
|
1270 |
|
1271 |
-
|
1272 |
-
headerDiv.appendChild(spaceHeader);
|
1273 |
|
1274 |
-
//
|
1275 |
const metaInfo = document.createElement('div');
|
1276 |
metaInfo.className = 'grid-meta';
|
1277 |
-
metaInfo.style.display = 'flex';
|
1278 |
-
metaInfo.style.justifyContent = 'space-between';
|
1279 |
-
metaInfo.style.alignItems = 'center';
|
1280 |
-
metaInfo.style.marginTop = '6px';
|
1281 |
|
1282 |
-
const
|
1283 |
-
|
1284 |
-
|
1285 |
-
|
1286 |
-
leftMeta.appendChild(rankBadge);
|
1287 |
-
|
1288 |
-
const authorSpan = document.createElement('span');
|
1289 |
-
authorSpan.className = 'author-name';
|
1290 |
-
authorSpan.style.marginLeft = '8px';
|
1291 |
-
authorSpan.textContent = `by ${author_name}`;
|
1292 |
-
leftMeta.appendChild(authorSpan);
|
1293 |
-
|
1294 |
-
metaInfo.appendChild(leftMeta);
|
1295 |
|
1296 |
const likesDiv = document.createElement('div');
|
1297 |
-
likesDiv.className = 'likes-
|
1298 |
-
likesDiv.innerHTML =
|
1299 |
metaInfo.appendChild(likesDiv);
|
1300 |
|
1301 |
headerDiv.appendChild(metaInfo);
|
1302 |
-
gridItem.appendChild(headerDiv);
|
1303 |
|
1304 |
if (description) {
|
1305 |
const descP = document.createElement('p');
|
1306 |
-
descP.
|
|
|
|
|
1307 |
descP.textContent = description;
|
1308 |
-
|
1309 |
}
|
1310 |
|
|
|
|
|
|
|
1311 |
const content = document.createElement('div');
|
1312 |
content.className = 'grid-content';
|
1313 |
|
@@ -1342,9 +1276,9 @@ if __name__ == '__main__':
|
|
1342 |
iframeContainer.appendChild(iframe);
|
1343 |
content.appendChild(iframeContainer);
|
1344 |
|
|
|
1345 |
const actions = document.createElement('div');
|
1346 |
actions.className = 'grid-actions';
|
1347 |
-
|
1348 |
const linkEl = document.createElement('a');
|
1349 |
linkEl.href = url;
|
1350 |
linkEl.target = '_blank';
|
@@ -1354,6 +1288,7 @@ if __name__ == '__main__':
|
|
1354 |
|
1355 |
gridItem.appendChild(content);
|
1356 |
gridItem.appendChild(actions);
|
|
|
1357 |
elements.gridContainer.appendChild(gridItem);
|
1358 |
|
1359 |
} catch (err) {
|
@@ -1362,6 +1297,7 @@ if __name__ == '__main__':
|
|
1362 |
});
|
1363 |
}
|
1364 |
|
|
|
1365 |
function renderFixedGrid() {
|
1366 |
fixedGridContainer.innerHTML = '';
|
1367 |
|
@@ -1415,27 +1351,25 @@ if __name__ == '__main__':
|
|
1415 |
const headerTop = document.createElement('div');
|
1416 |
headerTop.className = 'grid-header-top';
|
1417 |
|
1418 |
-
//
|
1419 |
-
const
|
1420 |
-
|
1421 |
-
|
1422 |
-
|
1423 |
-
const emojiAvatar = document.createElement('div');
|
1424 |
-
emojiAvatar.className = 'emoji-avatar';
|
1425 |
-
emojiAvatar.textContent = '🤖';
|
1426 |
-
leftWrapper.appendChild(emojiAvatar);
|
1427 |
|
|
|
1428 |
const titleEl = document.createElement('h3');
|
1429 |
titleEl.textContent = title;
|
1430 |
titleEl.title = title;
|
1431 |
-
|
1432 |
-
|
1433 |
-
headerTop.appendChild(leftWrapper);
|
1434 |
|
1435 |
-
|
1436 |
-
|
1437 |
-
|
1438 |
-
|
|
|
|
|
|
|
1439 |
|
1440 |
header.appendChild(headerTop);
|
1441 |
|
@@ -1506,7 +1440,6 @@ if __name__ == '__main__':
|
|
1506 |
});
|
1507 |
}
|
1508 |
|
1509 |
-
// Tab switching
|
1510 |
tabTrendingButton.addEventListener('click', () => {
|
1511 |
tabTrendingButton.classList.add('active');
|
1512 |
tabFixedButton.classList.remove('active');
|
@@ -1577,5 +1510,5 @@ if __name__ == '__main__':
|
|
1577 |
</html>
|
1578 |
''')
|
1579 |
|
1580 |
-
# Flask
|
1581 |
app.run(host='0.0.0.0', port=7860)
|
|
|
29 |
})
|
30 |
return spaces
|
31 |
|
32 |
+
# Function to fetch Zero-GPU (CPU-based) Spaces, TRENDING order, top 500
|
33 |
def fetch_trending_spaces(offset=0, limit=72):
|
34 |
"""
|
35 |
+
1) hardware=cpu
|
36 |
+
2) sort=trending
|
37 |
+
3) 상위 500개만 남김 (그 이후 offset/limit 페이징)
|
38 |
"""
|
39 |
try:
|
40 |
url = "https://huggingface.co/api/spaces"
|
41 |
params = {
|
42 |
+
"limit": 10000, # 일단 많이 가져오기
|
43 |
+
"hardware": "cpu",
|
44 |
+
"sort": "trending" # 트렌딩 정렬
|
45 |
}
|
46 |
response = requests.get(url, params=params, timeout=30)
|
47 |
|
|
|
55 |
and space.get('id', '').split('/', 1)[0] != 'None'
|
56 |
]
|
57 |
|
58 |
+
# 트렌딩 중 상위 500개만 남김
|
59 |
+
if len(filtered_spaces) > 500:
|
60 |
+
filtered_spaces = filtered_spaces[:500]
|
61 |
|
62 |
+
total = len(filtered_spaces)
|
63 |
+
|
64 |
+
# 페이징
|
65 |
+
start = min(offset, total)
|
66 |
+
end = min(offset + limit, total)
|
67 |
+
|
68 |
+
print(f"[fetch_trending_spaces] Zero-GPU TRENDING Spaces: {total}개 중, "
|
69 |
+
f"{start}~{end-1} 반환")
|
70 |
|
71 |
return {
|
72 |
'spaces': filtered_spaces[start:end],
|
73 |
+
'total': total,
|
74 |
'offset': offset,
|
75 |
'limit': limit,
|
76 |
+
'all_spaces': filtered_spaces # 상위 500 전체
|
77 |
}
|
78 |
else:
|
79 |
print(f"Error fetching spaces: {response.status_code}")
|
|
|
88 |
|
89 |
except Exception as e:
|
90 |
print(f"Exception when fetching spaces: {e}")
|
|
|
91 |
return {
|
92 |
'spaces': generate_dummy_spaces(limit),
|
93 |
'total': 200,
|
|
|
100 |
def transform_url(owner, name):
|
101 |
"""
|
102 |
Hugging Face Space -> 서브도메인 접근 URL
|
103 |
+
예) huggingface.co/spaces/owner/spaceName -> owner-spacename.hf.space
|
104 |
"""
|
105 |
name = name.replace('.', '-').replace('_', '-')
|
106 |
owner = owner.lower()
|
|
|
110 |
# Get space details
|
111 |
def get_space_details(space_data, index, offset):
|
112 |
"""
|
113 |
+
space의 주요 정보(타이틀, likes, owner, url 등) 추출
|
114 |
"""
|
115 |
try:
|
116 |
if '/' in space_data.get('id', ''):
|
|
|
119 |
owner = space_data.get('owner', '')
|
120 |
name = space_data.get('id', '')
|
121 |
|
|
|
122 |
if owner == 'None' or name == 'None':
|
123 |
return None
|
124 |
|
|
|
143 |
'description': short_desc,
|
144 |
'avatar_url': avatar_url,
|
145 |
'author_name': author_name,
|
146 |
+
# 'rank'는 (offset + index + 1)로 표시
|
147 |
'rank': offset + index + 1
|
148 |
}
|
149 |
except Exception as e:
|
150 |
print(f"Error processing space data: {e}")
|
|
|
151 |
return {
|
152 |
'url': 'https://huggingface.co/spaces',
|
153 |
'embedUrl': 'https://huggingface.co/spaces',
|
|
|
164 |
# Get owner statistics from all spaces
|
165 |
def get_owner_stats(all_spaces):
|
166 |
"""
|
167 |
+
all_spaces: 상위 500개 트렌딩 CPU 스페이스
|
168 |
+
-> owner별로 몇 개인지 세서 상위 30명 반환
|
169 |
"""
|
170 |
owners = []
|
171 |
for space in all_spaces:
|
|
|
181 |
top_owners = owner_counts.most_common(30)
|
182 |
return top_owners
|
183 |
|
184 |
+
from flask import render_template
|
185 |
+
|
186 |
@app.route('/')
|
187 |
def home():
|
|
|
|
|
|
|
188 |
return render_template('index.html')
|
189 |
|
190 |
@app.route('/api/trending-spaces', methods=['GET'])
|
191 |
def trending_spaces():
|
192 |
"""
|
193 |
+
1) 상위 500 트렌딩 CPU 스페이스
|
194 |
+
2) offset, limit으로 페이지 나눠서 반환
|
195 |
+
3) top 30 creator stats
|
196 |
"""
|
197 |
search_query = request.args.get('search', '').lower()
|
198 |
offset = int(request.args.get('offset', 0))
|
199 |
limit = int(request.args.get('limit', 72))
|
200 |
|
201 |
+
# 500개 (CPU+trending) 받아오기
|
202 |
spaces_data = fetch_trending_spaces(offset, limit)
|
203 |
|
204 |
results = []
|
|
|
217 |
|
218 |
results.append(space_info)
|
219 |
|
|
|
220 |
top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
|
221 |
|
222 |
return jsonify({
|
|
|
229 |
|
230 |
if __name__ == '__main__':
|
231 |
"""
|
232 |
+
Flask 실행 시, templates/index.html 생성
|
233 |
"""
|
234 |
os.makedirs('templates', exist_ok=True)
|
235 |
|
|
|
236 |
with open('templates/index.html', 'w', encoding='utf-8') as f:
|
237 |
f.write('''<!DOCTYPE html>
|
238 |
<html lang="en">
|
239 |
<head>
|
240 |
<meta charset="UTF-8">
|
241 |
+
<title>Zero-GPU Trending (Top 500)</title>
|
242 |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
243 |
<style>
|
244 |
/* Google Fonts & Base Styling */
|
|
|
271 |
}
|
272 |
|
273 |
body {
|
274 |
+
font-family: 'Nunito', sans-serif;
|
275 |
line-height: 1.6;
|
276 |
color: var(--text-primary);
|
277 |
background-color: #f8f9fa;
|
|
|
760 |
width: 100%;
|
761 |
height: 500px;
|
762 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
763 |
</style>
|
764 |
</head>
|
765 |
<body>
|
|
|
776 |
|
777 |
<div class="mac-content">
|
778 |
<div class="header">
|
779 |
+
<h1>Zero GPU Spaces (Top 500 Trending)</h1>
|
780 |
+
<p>Discover trending CPU-based spaces from Hugging Face. Only top 500 are considered!</p>
|
781 |
</div>
|
782 |
|
783 |
<!-- Tab Navigation -->
|
|
|
786 |
<button id="tabFixedButton" class="tab-button">Fixed Tab</button>
|
787 |
</div>
|
788 |
|
789 |
+
<!-- Trending Tab Content -->
|
790 |
<div id="trendingTab" class="tab-content active">
|
791 |
<div class="stats-window mac-window">
|
792 |
<div class="mac-toolbar">
|
|
|
799 |
</div>
|
800 |
<div class="mac-content">
|
801 |
<div class="stats-header">
|
802 |
+
<div class="stats-title">Top 30 Creators by Space Count (of Top 500)</div>
|
803 |
<button id="statsToggle" class="stats-toggle">Show Stats</button>
|
804 |
</div>
|
805 |
<div id="statsContent" class="stats-content">
|
|
|
842 |
</div>
|
843 |
|
844 |
<script>
|
845 |
+
// DOM References
|
846 |
const elements = {
|
847 |
gridContainer: document.getElementById('gridContainer'),
|
848 |
loadingIndicator: document.getElementById('loadingIndicator'),
|
|
|
883 |
|
884 |
startChecking: function(iframe, owner, name, title, spaceKey) {
|
885 |
this.checkQueue[spaceKey] = {
|
886 |
+
iframe, owner, name, title,
|
|
|
|
|
|
|
887 |
attempts: 0,
|
888 |
status: 'loading'
|
889 |
};
|
|
|
911 |
item.iframe.contentWindow.document.body;
|
912 |
if (hasContent && item.iframe.contentWindow.document.body.innerHTML.length > 100) {
|
913 |
const bodyText = item.iframe.contentWindow.document.body.textContent.toLowerCase();
|
914 |
+
if (bodyText.includes('forbidden') || bodyText.includes('404') ||
|
915 |
+
bodyText.includes('not found') || bodyText.includes('error')) {
|
|
|
|
|
916 |
item.status = 'error';
|
917 |
handleIframeError(item.iframe, item.owner, item.name, item.title);
|
918 |
} else {
|
|
|
922 |
return;
|
923 |
}
|
924 |
} catch(e) {
|
925 |
+
// cross-origin => not necessarily an error
|
926 |
}
|
927 |
|
928 |
const rect = item.iframe.getBoundingClientRect();
|
|
|
942 |
delete this.checkQueue[spaceKey];
|
943 |
return;
|
944 |
}
|
945 |
+
|
946 |
const nextDelay = this.checkInterval * Math.pow(1.5, item.attempts - 1);
|
947 |
setTimeout(() => this.checkIframeStatus(spaceKey), nextDelay);
|
948 |
|
|
|
963 |
state.statsVisible = !state.statsVisible;
|
964 |
elements.statsContent.classList.toggle('open', state.statsVisible);
|
965 |
elements.statsToggle.textContent = state.statsVisible ? 'Hide Stats' : 'Show Stats';
|
966 |
+
|
967 |
if (state.statsVisible && state.topOwners.length > 0) {
|
968 |
renderCreatorStats();
|
969 |
}
|
|
|
1049 |
const timeoutPromise = new Promise((_, reject) =>
|
1050 |
setTimeout(() => reject(new Error('Request timeout')), 30000)
|
1051 |
);
|
1052 |
+
const fetchPromise = fetch(`/api/trending-spaces?search=${encodeURIComponent(searchText)}&offset=${offset}&limit=${state.itemsPerPage}`);
|
|
|
|
|
1053 |
const response = await Promise.race([fetchPromise, timeoutPromise]);
|
1054 |
const data = await response.json();
|
1055 |
|
|
|
1087 |
elements.pagination.innerHTML = '';
|
1088 |
const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
|
1089 |
|
1090 |
+
// Prev
|
1091 |
const prevButton = document.createElement('button');
|
1092 |
prevButton.className = 'pagination-button';
|
1093 |
prevButton.textContent = 'Previous';
|
|
|
1099 |
});
|
1100 |
elements.pagination.appendChild(prevButton);
|
1101 |
|
1102 |
+
// Middle pages
|
1103 |
const maxButtons = 7;
|
1104 |
let startPage = Math.max(0, state.currentPage - Math.floor(maxButtons / 2));
|
1105 |
let endPage = Math.min(totalPages - 1, startPage + maxButtons - 1);
|
|
|
1120 |
elements.pagination.appendChild(pageButton);
|
1121 |
}
|
1122 |
|
1123 |
+
// Next
|
1124 |
const nextButton = document.createElement('button');
|
1125 |
nextButton.className = 'pagination-button';
|
1126 |
nextButton.textContent = 'Next';
|
|
|
1135 |
|
1136 |
function handleIframeError(iframe, owner, name, title) {
|
1137 |
const container = iframe.parentNode;
|
|
|
1138 |
const errorPlaceholder = document.createElement('div');
|
1139 |
errorPlaceholder.className = 'error-placeholder';
|
1140 |
+
|
1141 |
const errorMessage = document.createElement('p');
|
1142 |
errorMessage.textContent = `"${title}" space couldn't be loaded`;
|
1143 |
errorPlaceholder.appendChild(errorMessage);
|
1144 |
+
|
1145 |
const directLink = document.createElement('a');
|
1146 |
directLink.href = `https://huggingface.co/spaces/${owner}/${name}`;
|
1147 |
directLink.target = '_blank';
|
|
|
1154 |
directLink.style.borderRadius = '5px';
|
1155 |
directLink.style.fontWeight = '600';
|
1156 |
errorPlaceholder.appendChild(directLink);
|
1157 |
+
|
1158 |
iframe.style.display = 'none';
|
1159 |
container.appendChild(errorPlaceholder);
|
1160 |
}
|
1161 |
|
1162 |
+
// renderGrid: 로봇 이모지 제거 -> rankBadge를 그 자리에 배치
|
1163 |
function renderGrid(spaces) {
|
1164 |
elements.gridContainer.innerHTML = '';
|
1165 |
|
|
|
1184 |
const gridItem = document.createElement('div');
|
1185 |
gridItem.className = 'grid-item';
|
1186 |
|
1187 |
+
// 상단 header
|
1188 |
const headerDiv = document.createElement('div');
|
1189 |
headerDiv.className = 'grid-header';
|
1190 |
|
1191 |
+
// grid-header-top (rank + title + zero gpu)
|
1192 |
+
const headerTop = document.createElement('div');
|
1193 |
+
headerTop.className = 'grid-header-top';
|
|
|
|
|
|
|
|
|
|
|
|
|
1194 |
|
1195 |
+
// 왼쪽: rank badge
|
1196 |
+
const rankBadge = document.createElement('div');
|
1197 |
+
rankBadge.className = 'rank-badge';
|
1198 |
+
rankBadge.textContent = `#${rank}`;
|
1199 |
+
headerTop.appendChild(rankBadge);
|
1200 |
|
1201 |
+
// 중간: title
|
1202 |
const titleEl = document.createElement('h3');
|
|
|
1203 |
titleEl.textContent = title;
|
1204 |
titleEl.title = title;
|
1205 |
+
headerTop.appendChild(titleEl);
|
1206 |
|
1207 |
+
// 오른쪽(Zero GPU)
|
1208 |
const zeroGpuBadge = document.createElement('span');
|
1209 |
+
zeroGpuBadge.className = 'rank-badge';
|
1210 |
+
zeroGpuBadge.style.backgroundColor = '#319795';
|
1211 |
+
zeroGpuBadge.style.marginLeft = '10px';
|
1212 |
zeroGpuBadge.textContent = 'ZERO GPU';
|
1213 |
+
headerTop.appendChild(zeroGpuBadge);
|
1214 |
|
1215 |
+
headerDiv.appendChild(headerTop);
|
|
|
1216 |
|
1217 |
+
// 둘째줄: meta (owner / likes)
|
1218 |
const metaInfo = document.createElement('div');
|
1219 |
metaInfo.className = 'grid-meta';
|
|
|
|
|
|
|
|
|
1220 |
|
1221 |
+
const ownerEl = document.createElement('div');
|
1222 |
+
ownerEl.className = 'owner-info';
|
1223 |
+
ownerEl.textContent = `by ${author_name}`;
|
1224 |
+
metaInfo.appendChild(ownerEl);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1225 |
|
1226 |
const likesDiv = document.createElement('div');
|
1227 |
+
likesDiv.className = 'likes-counter';
|
1228 |
+
likesDiv.innerHTML = '♥ <span>' + likes_count + '</span>';
|
1229 |
metaInfo.appendChild(likesDiv);
|
1230 |
|
1231 |
headerDiv.appendChild(metaInfo);
|
|
|
1232 |
|
1233 |
if (description) {
|
1234 |
const descP = document.createElement('p');
|
1235 |
+
descP.style.fontSize = '0.9rem';
|
1236 |
+
descP.style.marginTop = '6px';
|
1237 |
+
descP.style.color = '#444';
|
1238 |
descP.textContent = description;
|
1239 |
+
headerDiv.appendChild(descP);
|
1240 |
}
|
1241 |
|
1242 |
+
gridItem.appendChild(headerDiv);
|
1243 |
+
|
1244 |
+
// iframe container
|
1245 |
const content = document.createElement('div');
|
1246 |
content.className = 'grid-content';
|
1247 |
|
|
|
1276 |
iframeContainer.appendChild(iframe);
|
1277 |
content.appendChild(iframeContainer);
|
1278 |
|
1279 |
+
// actions
|
1280 |
const actions = document.createElement('div');
|
1281 |
actions.className = 'grid-actions';
|
|
|
1282 |
const linkEl = document.createElement('a');
|
1283 |
linkEl.href = url;
|
1284 |
linkEl.target = '_blank';
|
|
|
1288 |
|
1289 |
gridItem.appendChild(content);
|
1290 |
gridItem.appendChild(actions);
|
1291 |
+
|
1292 |
elements.gridContainer.appendChild(gridItem);
|
1293 |
|
1294 |
} catch (err) {
|
|
|
1297 |
});
|
1298 |
}
|
1299 |
|
1300 |
+
// Fixed Tab (예시)
|
1301 |
function renderFixedGrid() {
|
1302 |
fixedGridContainer.innerHTML = '';
|
1303 |
|
|
|
1351 |
const headerTop = document.createElement('div');
|
1352 |
headerTop.className = 'grid-header-top';
|
1353 |
|
1354 |
+
// rank
|
1355 |
+
const rankBadge = document.createElement('div');
|
1356 |
+
rankBadge.className = 'rank-badge';
|
1357 |
+
rankBadge.textContent = `#${rank}`;
|
1358 |
+
headerTop.appendChild(rankBadge);
|
|
|
|
|
|
|
|
|
1359 |
|
1360 |
+
// title
|
1361 |
const titleEl = document.createElement('h3');
|
1362 |
titleEl.textContent = title;
|
1363 |
titleEl.title = title;
|
1364 |
+
headerTop.appendChild(titleEl);
|
|
|
|
|
1365 |
|
1366 |
+
// zero gpu (임의)
|
1367 |
+
const zeroBadge = document.createElement('div');
|
1368 |
+
zeroBadge.className = 'rank-badge';
|
1369 |
+
zeroBadge.style.backgroundColor = '#319795';
|
1370 |
+
zeroBadge.style.marginLeft = '10px';
|
1371 |
+
zeroBadge.textContent = 'ZERO GPU';
|
1372 |
+
headerTop.appendChild(zeroBadge);
|
1373 |
|
1374 |
header.appendChild(headerTop);
|
1375 |
|
|
|
1440 |
});
|
1441 |
}
|
1442 |
|
|
|
1443 |
tabTrendingButton.addEventListener('click', () => {
|
1444 |
tabTrendingButton.classList.add('active');
|
1445 |
tabFixedButton.classList.remove('active');
|
|
|
1510 |
</html>
|
1511 |
''')
|
1512 |
|
1513 |
+
# Flask Run
|
1514 |
app.run(host='0.0.0.0', port=7860)
|