Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -32,15 +32,14 @@ def generate_dummy_spaces(count):
|
|
32 |
# Function to fetch Zero-GPU (CPU-based) Spaces from Huggingface with pagination
|
33 |
def fetch_trending_spaces(offset=0, limit=72):
|
34 |
"""
|
35 |
-
|
36 |
-
GPU๊ฐ ์๋(=CPU ์ ์ฉ) ์คํ์ด์ค๋ง ํํฐ๋ง
|
37 |
"""
|
38 |
try:
|
39 |
url = "https://huggingface.co/api/spaces"
|
40 |
params = {
|
41 |
"limit": 10000, # ๋ ๋ง์ด ๊ฐ์ ธ์ค๊ธฐ
|
42 |
"hardware": "cpu" # <-- Zero GPU(=CPU) ํํฐ ์ ์ฉ
|
43 |
-
# "sort": "trending", #
|
44 |
}
|
45 |
response = requests.get(url, params=params, timeout=30)
|
46 |
|
@@ -54,7 +53,7 @@ def fetch_trending_spaces(offset=0, limit=72):
|
|
54 |
and space.get('id', '').split('/', 1)[0] != 'None'
|
55 |
]
|
56 |
|
57 |
-
#
|
58 |
start = min(offset, len(filtered_spaces))
|
59 |
end = min(offset + limit, len(filtered_spaces))
|
60 |
|
@@ -66,7 +65,7 @@ def fetch_trending_spaces(offset=0, limit=72):
|
|
66 |
'total': len(filtered_spaces),
|
67 |
'offset': offset,
|
68 |
'limit': limit,
|
69 |
-
'all_spaces': filtered_spaces # ํต๊ณ ์ฐ์ถ์ฉ
|
70 |
}
|
71 |
else:
|
72 |
print(f"Error fetching spaces: {response.status_code}")
|
@@ -94,23 +93,17 @@ def fetch_trending_spaces(offset=0, limit=72):
|
|
94 |
def transform_url(owner, name):
|
95 |
"""
|
96 |
Hugging Face Space -> ์๋ธ๋๋ฉ์ธ ์ ๊ทผ URL
|
97 |
-
|
98 |
"""
|
99 |
-
|
100 |
-
name = name.replace('.', '-')
|
101 |
-
# 2. Replace '_' with '-'
|
102 |
-
name = name.replace('_', '-')
|
103 |
-
# 3. Convert to lowercase
|
104 |
owner = owner.lower()
|
105 |
name = name.lower()
|
106 |
-
|
107 |
return f"https://{owner}-{name}.hf.space"
|
108 |
|
109 |
# Get space details
|
110 |
def get_space_details(space_data, index, offset):
|
111 |
"""
|
112 |
-
|
113 |
-
- description, avatar_url, author_name ๋ฑ ์ถ๊ฐ ํ๋๋ฅผ ์ถ์ถ
|
114 |
"""
|
115 |
try:
|
116 |
if '/' in space_data.get('id', ''):
|
@@ -119,24 +112,17 @@ def get_space_details(space_data, index, offset):
|
|
119 |
owner = space_data.get('owner', '')
|
120 |
name = space_data.get('id', '')
|
121 |
|
122 |
-
# Ignore if
|
123 |
if owner == 'None' or name == 'None':
|
124 |
return None
|
125 |
|
126 |
-
# Construct URLs
|
127 |
original_url = f"https://huggingface.co/spaces/{owner}/{name}"
|
128 |
embed_url = transform_url(owner, name)
|
129 |
|
130 |
-
# Likes count
|
131 |
likes_count = space_data.get('likes', 0)
|
132 |
-
|
133 |
-
# Title
|
134 |
title = space_data.get('title') or name
|
135 |
-
|
136 |
-
# Description
|
137 |
short_desc = space_data.get('description', '')
|
138 |
|
139 |
-
# User info (avatar, name)
|
140 |
user_info = space_data.get('user', {})
|
141 |
avatar_url = user_info.get('avatar_url', '')
|
142 |
author_name = user_info.get('name') or owner
|
@@ -155,7 +141,7 @@ def get_space_details(space_data, index, offset):
|
|
155 |
}
|
156 |
except Exception as e:
|
157 |
print(f"Error processing space data: {e}")
|
158 |
-
#
|
159 |
return {
|
160 |
'url': 'https://huggingface.co/spaces',
|
161 |
'embedUrl': 'https://huggingface.co/spaces',
|
@@ -172,7 +158,8 @@ def get_space_details(space_data, index, offset):
|
|
172 |
# Get owner statistics from all spaces
|
173 |
def get_owner_stats(all_spaces):
|
174 |
"""
|
175 |
-
|
|
|
176 |
"""
|
177 |
owners = []
|
178 |
for space in all_spaces:
|
@@ -184,43 +171,36 @@ def get_owner_stats(all_spaces):
|
|
184 |
if owner != 'None':
|
185 |
owners.append(owner)
|
186 |
|
187 |
-
# Count occurrences of each owner
|
188 |
owner_counts = Counter(owners)
|
189 |
-
|
190 |
-
# Get top 30 owners by count
|
191 |
top_owners = owner_counts.most_common(30)
|
192 |
-
|
193 |
return top_owners
|
194 |
|
195 |
-
# Homepage route
|
196 |
@app.route('/')
|
197 |
def home():
|
198 |
"""
|
199 |
-
index.html ํ
ํ๋ฆฟ
|
200 |
"""
|
201 |
return render_template('index.html')
|
202 |
|
203 |
-
# Zero-GPU spaces API (์๋ณธ: 'trending-spaces' -> ์ด์ CPU only)
|
204 |
@app.route('/api/trending-spaces', methods=['GET'])
|
205 |
def trending_spaces():
|
206 |
"""
|
207 |
-
|
|
|
208 |
"""
|
209 |
search_query = request.args.get('search', '').lower()
|
210 |
offset = int(request.args.get('offset', 0))
|
211 |
-
limit = int(request.args.get('limit', 72))
|
212 |
|
213 |
-
# Fetch zero-gpu (cpu) spaces
|
214 |
spaces_data = fetch_trending_spaces(offset, limit)
|
215 |
|
216 |
-
# Process and filter spaces
|
217 |
results = []
|
218 |
for index, space_data in enumerate(spaces_data['spaces']):
|
219 |
space_info = get_space_details(space_data, index, offset)
|
220 |
if not space_info:
|
221 |
continue
|
222 |
|
223 |
-
#
|
224 |
if search_query:
|
225 |
if (search_query not in space_info['title'].lower()
|
226 |
and search_query not in space_info['owner'].lower()
|
@@ -230,7 +210,7 @@ def trending_spaces():
|
|
230 |
|
231 |
results.append(space_info)
|
232 |
|
233 |
-
#
|
234 |
top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
|
235 |
|
236 |
return jsonify({
|
@@ -245,10 +225,9 @@ if __name__ == '__main__':
|
|
245 |
"""
|
246 |
์๋ฒ ๊ตฌ๋ ์, templates/index.html ํ์ผ์ ์์ฑ ํ Flask ์คํ
|
247 |
"""
|
248 |
-
# Create templates folder if not exists
|
249 |
os.makedirs('templates', exist_ok=True)
|
250 |
|
251 |
-
# index.html
|
252 |
with open('templates/index.html', 'w', encoding='utf-8') as f:
|
253 |
f.write('''<!DOCTYPE html>
|
254 |
<html lang="en">
|
@@ -819,70 +798,7 @@ if __name__ == '__main__':
|
|
819 |
opacity: 0;
|
820 |
}
|
821 |
|
822 |
-
/*
|
823 |
-
์๋ ํด๋์ค๋ค์ ์ผ๋ถ ์ถ๊ฐ/์์ ํด๋ ์ข์ง๋ง
|
824 |
-
์์๋ ์ ์์ญ์์ ์ด๋ฏธ ์ถฉ๋ถํ ์ฒ๋ฆฌํ๋ฏ๋ก ์๋ต */
|
825 |
-
|
826 |
-
/* ๋ค์ ๋ถ๋ถ์ Zero GPU Spaces์ฉ ์นด๋ ๊ตฌ์กฐ์์ ํ์ฉ */
|
827 |
-
.space-header {
|
828 |
-
display: flex;
|
829 |
-
align-items: center;
|
830 |
-
gap: 10px;
|
831 |
-
margin-bottom: 4px;
|
832 |
-
}
|
833 |
-
.avatar-img {
|
834 |
-
width: 32px;
|
835 |
-
height: 32px;
|
836 |
-
border-radius: 50%;
|
837 |
-
object-fit: cover;
|
838 |
-
border: 1px solid #ccc;
|
839 |
-
}
|
840 |
-
.space-title {
|
841 |
-
font-size: 1rem;
|
842 |
-
font-weight: 600;
|
843 |
-
margin: 0;
|
844 |
-
overflow: hidden;
|
845 |
-
text-overflow: ellipsis;
|
846 |
-
white-space: nowrap;
|
847 |
-
max-width: 200px;
|
848 |
-
}
|
849 |
-
.zero-gpu-badge {
|
850 |
-
font-size: 0.7rem;
|
851 |
-
background-color: #e6fffa;
|
852 |
-
color: #319795;
|
853 |
-
border: 1px solid #81e6d9;
|
854 |
-
border-radius: 6px;
|
855 |
-
padding: 2px 6px;
|
856 |
-
font-weight: 600;
|
857 |
-
margin-left: 8px;
|
858 |
-
}
|
859 |
-
.desc-text {
|
860 |
-
font-size: 0.85rem;
|
861 |
-
color: #444;
|
862 |
-
margin: 4px 0;
|
863 |
-
line-clamp: 2;
|
864 |
-
display: -webkit-box;
|
865 |
-
-webkit-box-orient: vertical;
|
866 |
-
overflow: hidden;
|
867 |
-
}
|
868 |
-
.author-name {
|
869 |
-
font-size: 0.8rem;
|
870 |
-
color: #666;
|
871 |
-
}
|
872 |
-
.likes-wrapper {
|
873 |
-
display: flex;
|
874 |
-
align-items: center;
|
875 |
-
gap: 4px;
|
876 |
-
color: #e53e3e;
|
877 |
-
font-weight: bold;
|
878 |
-
font-size: 0.85rem;
|
879 |
-
}
|
880 |
-
.likes-heart {
|
881 |
-
font-size: 1rem;
|
882 |
-
line-height: 1rem;
|
883 |
-
color: #f56565;
|
884 |
-
}
|
885 |
-
/* ์ด๋ชจ์ง ์ ์ฉ ์คํ์ผ (์ ํ์ฌํญ) */
|
886 |
.emoji-avatar {
|
887 |
font-size: 1.2rem;
|
888 |
width: 32px;
|
@@ -909,15 +825,14 @@ if __name__ == '__main__':
|
|
909 |
|
910 |
<div class="mac-content">
|
911 |
<div class="header">
|
912 |
-
|
913 |
-
<
|
914 |
-
<p>Discover Zero GPU(Shared A100) spaces from Hugging Face</p>
|
915 |
</div>
|
916 |
|
917 |
<!-- Tab Navigation -->
|
918 |
<div class="tab-nav">
|
919 |
-
<button id="tabTrendingButton" class="tab-button active">
|
920 |
-
<button id="tabFixedButton" class="tab-button">
|
921 |
</div>
|
922 |
|
923 |
<!-- Trending(Zero GPU) Tab Content -->
|
@@ -957,7 +872,7 @@ if __name__ == '__main__':
|
|
957 |
<div id="pagination" class="pagination"></div>
|
958 |
</div>
|
959 |
|
960 |
-
<!-- Fixed Tab Content
|
961 |
<div id="fixedTab" class="tab-content">
|
962 |
<div id="fixedGrid" class="grid-container"></div>
|
963 |
</div>
|
@@ -1009,7 +924,7 @@ if __name__ == '__main__':
|
|
1009 |
iframeStatuses: {}
|
1010 |
};
|
1011 |
|
1012 |
-
// Advanced iframe loader
|
1013 |
const iframeLoader = {
|
1014 |
checkQueue: {},
|
1015 |
maxAttempts: 5,
|
@@ -1029,29 +944,29 @@ if __name__ == '__main__':
|
|
1029 |
|
1030 |
checkIframeStatus: function(spaceKey) {
|
1031 |
if (!this.checkQueue[spaceKey]) return;
|
1032 |
-
|
1033 |
const item = this.checkQueue[spaceKey];
|
1034 |
if (item.status !== 'loading') {
|
1035 |
delete this.checkQueue[spaceKey];
|
1036 |
return;
|
1037 |
}
|
1038 |
item.attempts++;
|
1039 |
-
|
1040 |
try {
|
1041 |
if (!item.iframe || !item.iframe.parentNode) {
|
1042 |
delete this.checkQueue[spaceKey];
|
1043 |
return;
|
1044 |
}
|
1045 |
|
1046 |
-
// Check if content loaded
|
1047 |
try {
|
1048 |
-
const hasContent = item.iframe.contentWindow &&
|
1049 |
-
item.iframe.contentWindow.document &&
|
1050 |
item.iframe.contentWindow.document.body;
|
1051 |
if (hasContent && item.iframe.contentWindow.document.body.innerHTML.length > 100) {
|
1052 |
const bodyText = item.iframe.contentWindow.document.body.textContent.toLowerCase();
|
1053 |
-
if (bodyText.includes('forbidden') ||
|
1054 |
-
bodyText.includes('
|
|
|
|
|
1055 |
item.status = 'error';
|
1056 |
handleIframeError(item.iframe, item.owner, item.name, item.title);
|
1057 |
} else {
|
@@ -1061,10 +976,9 @@ if __name__ == '__main__':
|
|
1061 |
return;
|
1062 |
}
|
1063 |
} catch(e) {
|
1064 |
-
// Cross-origin
|
1065 |
}
|
1066 |
|
1067 |
-
// Check if iframe is visible
|
1068 |
const rect = item.iframe.getBoundingClientRect();
|
1069 |
if (rect.width > 50 && rect.height > 50 && item.attempts > 2) {
|
1070 |
item.status = 'success';
|
@@ -1072,7 +986,6 @@ if __name__ == '__main__':
|
|
1072 |
return;
|
1073 |
}
|
1074 |
|
1075 |
-
// If max attempts reached
|
1076 |
if (item.attempts >= this.maxAttempts) {
|
1077 |
if (item.iframe.offsetWidth > 0 && item.iframe.offsetHeight > 0) {
|
1078 |
item.status = 'success';
|
@@ -1083,8 +996,6 @@ if __name__ == '__main__':
|
|
1083 |
delete this.checkQueue[spaceKey];
|
1084 |
return;
|
1085 |
}
|
1086 |
-
|
1087 |
-
// Re-check after some delay
|
1088 |
const nextDelay = this.checkInterval * Math.pow(1.5, item.attempts - 1);
|
1089 |
setTimeout(() => this.checkIframeStatus(spaceKey), nextDelay);
|
1090 |
|
@@ -1133,7 +1044,7 @@ if __name__ == '__main__':
|
|
1133 |
label: 'Number of Spaces',
|
1134 |
data: data,
|
1135 |
backgroundColor: colors,
|
1136 |
-
borderColor: colors.map(color => color.replace('0.7',
|
1137 |
borderWidth: 1
|
1138 |
}]
|
1139 |
},
|
@@ -1303,7 +1214,7 @@ if __name__ == '__main__':
|
|
1303 |
container.appendChild(errorPlaceholder);
|
1304 |
}
|
1305 |
|
1306 |
-
//
|
1307 |
function renderGrid(spaces) {
|
1308 |
elements.gridContainer.innerHTML = '';
|
1309 |
|
@@ -1328,21 +1239,20 @@ if __name__ == '__main__':
|
|
1328 |
const gridItem = document.createElement('div');
|
1329 |
gridItem.className = 'grid-item';
|
1330 |
|
1331 |
-
// ์๋จ ํค๋
|
1332 |
const headerDiv = document.createElement('div');
|
1333 |
headerDiv.className = 'grid-header';
|
1334 |
|
1335 |
-
//
|
1336 |
const spaceHeader = document.createElement('div');
|
1337 |
spaceHeader.className = 'space-header';
|
1338 |
|
1339 |
-
//
|
1340 |
const emojiAvatar = document.createElement('div');
|
1341 |
emojiAvatar.className = 'emoji-avatar';
|
1342 |
emojiAvatar.textContent = '๐ค';
|
1343 |
spaceHeader.appendChild(emojiAvatar);
|
1344 |
|
1345 |
-
//
|
1346 |
const titleWrapper = document.createElement('div');
|
1347 |
titleWrapper.style.display = 'flex';
|
1348 |
titleWrapper.style.alignItems = 'center';
|
@@ -1369,9 +1279,7 @@ if __name__ == '__main__':
|
|
1369 |
metaInfo.style.alignItems = 'center';
|
1370 |
metaInfo.style.marginTop = '6px';
|
1371 |
|
1372 |
-
// ์ผ์ชฝ: rank + author
|
1373 |
const leftMeta = document.createElement('div');
|
1374 |
-
|
1375 |
const rankBadge = document.createElement('div');
|
1376 |
rankBadge.className = 'rank-badge';
|
1377 |
rankBadge.textContent = `#${rank}`;
|
@@ -1385,7 +1293,6 @@ if __name__ == '__main__':
|
|
1385 |
|
1386 |
metaInfo.appendChild(leftMeta);
|
1387 |
|
1388 |
-
// ์ค๋ฅธ์ชฝ: likes
|
1389 |
const likesDiv = document.createElement('div');
|
1390 |
likesDiv.className = 'likes-wrapper';
|
1391 |
likesDiv.innerHTML = `<span class="likes-heart">โฅ</span><span>${likes_count}</span>`;
|
@@ -1394,7 +1301,6 @@ if __name__ == '__main__':
|
|
1394 |
headerDiv.appendChild(metaInfo);
|
1395 |
gridItem.appendChild(headerDiv);
|
1396 |
|
1397 |
-
// description
|
1398 |
if (description) {
|
1399 |
const descP = document.createElement('p');
|
1400 |
descP.className = 'desc-text';
|
@@ -1402,7 +1308,6 @@ if __name__ == '__main__':
|
|
1402 |
gridItem.appendChild(descP);
|
1403 |
}
|
1404 |
|
1405 |
-
// iframe container
|
1406 |
const content = document.createElement('div');
|
1407 |
content.className = 'grid-content';
|
1408 |
|
@@ -1410,11 +1315,11 @@ if __name__ == '__main__':
|
|
1410 |
iframeContainer.className = 'iframe-container';
|
1411 |
|
1412 |
const iframe = document.createElement('iframe');
|
1413 |
-
iframe.src = embedUrl;
|
1414 |
iframe.title = title;
|
1415 |
iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
|
1416 |
-
iframe.setAttribute('allowfullscreen',
|
1417 |
-
iframe.setAttribute('frameborder',
|
1418 |
iframe.loading = 'lazy';
|
1419 |
|
1420 |
const spaceKey = `${owner}/${name}`;
|
@@ -1437,7 +1342,6 @@ if __name__ == '__main__':
|
|
1437 |
iframeContainer.appendChild(iframe);
|
1438 |
content.appendChild(iframeContainer);
|
1439 |
|
1440 |
-
// actions
|
1441 |
const actions = document.createElement('div');
|
1442 |
actions.className = 'grid-actions';
|
1443 |
|
@@ -1446,11 +1350,10 @@ if __name__ == '__main__':
|
|
1446 |
linkEl.target = '_blank';
|
1447 |
linkEl.className = 'open-link';
|
1448 |
linkEl.textContent = 'Open in new window';
|
1449 |
-
|
1450 |
actions.appendChild(linkEl);
|
|
|
1451 |
gridItem.appendChild(content);
|
1452 |
gridItem.appendChild(actions);
|
1453 |
-
|
1454 |
elements.gridContainer.appendChild(gridItem);
|
1455 |
|
1456 |
} catch (err) {
|
@@ -1460,7 +1363,6 @@ if __name__ == '__main__':
|
|
1460 |
}
|
1461 |
|
1462 |
function renderFixedGrid() {
|
1463 |
-
// Fixed Tab ์์์ฉ (์๋ณธ ์ฝ๋ ์์ ์ ์ง)
|
1464 |
fixedGridContainer.innerHTML = '';
|
1465 |
|
1466 |
const staticSpaces = [
|
@@ -1513,7 +1415,7 @@ if __name__ == '__main__':
|
|
1513 |
const headerTop = document.createElement('div');
|
1514 |
headerTop.className = 'grid-header-top';
|
1515 |
|
1516 |
-
//
|
1517 |
const leftWrapper = document.createElement('div');
|
1518 |
leftWrapper.style.display = 'flex';
|
1519 |
leftWrapper.style.alignItems = 'center';
|
@@ -1563,8 +1465,8 @@ if __name__ == '__main__':
|
|
1563 |
iframe.src = "https://" + owner.toLowerCase() + "-" + name.toLowerCase() + ".hf.space";
|
1564 |
iframe.title = title;
|
1565 |
iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
|
1566 |
-
iframe.setAttribute('allowfullscreen',
|
1567 |
-
iframe.setAttribute('frameborder',
|
1568 |
iframe.loading = 'lazy';
|
1569 |
|
1570 |
const spaceKey = `${owner}/${name}`;
|
@@ -1675,6 +1577,5 @@ if __name__ == '__main__':
|
|
1675 |
</html>
|
1676 |
''')
|
1677 |
|
1678 |
-
# Flask
|
1679 |
app.run(host='0.0.0.0', port=7860)
|
1680 |
-
|
|
|
32 |
# Function to fetch Zero-GPU (CPU-based) Spaces from Huggingface with pagination
|
33 |
def fetch_trending_spaces(offset=0, limit=72):
|
34 |
"""
|
35 |
+
hardware=cpu ํ๋ผ๋ฏธํฐ๋ฅผ ์ถ๊ฐํ์ฌ GPU๊ฐ ์๋(=CPU ์ ์ฉ) ์คํ์ด์ค๋ง ๊ฐ์ ธ์ด
|
|
|
36 |
"""
|
37 |
try:
|
38 |
url = "https://huggingface.co/api/spaces"
|
39 |
params = {
|
40 |
"limit": 10000, # ๋ ๋ง์ด ๊ฐ์ ธ์ค๊ธฐ
|
41 |
"hardware": "cpu" # <-- Zero GPU(=CPU) ํํฐ ์ ์ฉ
|
42 |
+
# "sort": "trending", # ํ์ ์ ์ถ๊ฐ
|
43 |
}
|
44 |
response = requests.get(url, params=params, timeout=30)
|
45 |
|
|
|
53 |
and space.get('id', '').split('/', 1)[0] != 'None'
|
54 |
]
|
55 |
|
56 |
+
# ํ์ด์ง ์ฌ๋ผ์ด์ฑ
|
57 |
start = min(offset, len(filtered_spaces))
|
58 |
end = min(offset + limit, len(filtered_spaces))
|
59 |
|
|
|
65 |
'total': len(filtered_spaces),
|
66 |
'offset': offset,
|
67 |
'limit': limit,
|
68 |
+
'all_spaces': filtered_spaces # ํต๊ณ ์ฐ์ถ์ฉ (์ ์ฒด CPU ์คํ์ด์ค)
|
69 |
}
|
70 |
else:
|
71 |
print(f"Error fetching spaces: {response.status_code}")
|
|
|
93 |
def transform_url(owner, name):
|
94 |
"""
|
95 |
Hugging Face Space -> ์๋ธ๋๋ฉ์ธ ์ ๊ทผ URL
|
96 |
+
์: huggingface.co/spaces/owner/spaceName -> owner-spacename.hf.space
|
97 |
"""
|
98 |
+
name = name.replace('.', '-').replace('_', '-')
|
|
|
|
|
|
|
|
|
99 |
owner = owner.lower()
|
100 |
name = name.lower()
|
|
|
101 |
return f"https://{owner}-{name}.hf.space"
|
102 |
|
103 |
# Get space details
|
104 |
def get_space_details(space_data, index, offset):
|
105 |
"""
|
106 |
+
Zero GPU ์คํ์ด์ค์์ ํ์ํ ์ ๋ณด(ํ์ดํ, owner, description, avatar_url ๋ฑ)๋ฅผ ์ถ์ถ
|
|
|
107 |
"""
|
108 |
try:
|
109 |
if '/' in space_data.get('id', ''):
|
|
|
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 |
|
|
|
119 |
original_url = f"https://huggingface.co/spaces/{owner}/{name}"
|
120 |
embed_url = transform_url(owner, name)
|
121 |
|
|
|
122 |
likes_count = space_data.get('likes', 0)
|
|
|
|
|
123 |
title = space_data.get('title') or name
|
|
|
|
|
124 |
short_desc = space_data.get('description', '')
|
125 |
|
|
|
126 |
user_info = space_data.get('user', {})
|
127 |
avatar_url = user_info.get('avatar_url', '')
|
128 |
author_name = user_info.get('name') or owner
|
|
|
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 |
# Get owner statistics from all spaces
|
159 |
def get_owner_stats(all_spaces):
|
160 |
"""
|
161 |
+
all_spaces: Zero GPU(=CPU) ์คํ์ด์ค ๋ชฉ๋ก(ํํฐ๋ ์ ์ฒด).
|
162 |
+
์ฌ๊ธฐ์ owner๋ฅผ ์นด์ดํธํ์ฌ ์์ 30๋ช
์ถ์ถ.
|
163 |
"""
|
164 |
owners = []
|
165 |
for space in all_spaces:
|
|
|
171 |
if owner != 'None':
|
172 |
owners.append(owner)
|
173 |
|
|
|
174 |
owner_counts = Counter(owners)
|
|
|
|
|
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 |
+
/api/trending-spaces
|
189 |
+
-> Zero GPU ์คํ์ด์ค ๊ฐ์ ธ์์ ๊ฒ์, ํ์ด์ง, ํต๊ณ(์ค๋ ์์ 30) ๋ฐํ
|
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 = []
|
198 |
for index, space_data in enumerate(spaces_data['spaces']):
|
199 |
space_info = get_space_details(space_data, index, offset)
|
200 |
if not space_info:
|
201 |
continue
|
202 |
|
203 |
+
# ๊ฒ์ ํํฐ
|
204 |
if search_query:
|
205 |
if (search_query not in space_info['title'].lower()
|
206 |
and search_query not in space_info['owner'].lower()
|
|
|
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({
|
|
|
225 |
"""
|
226 |
์๋ฒ ๊ตฌ๋ ์, templates/index.html ํ์ผ์ ์์ฑ ํ Flask ์คํ
|
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">
|
|
|
798 |
opacity: 0;
|
799 |
}
|
800 |
|
801 |
+
/* ๋ก๋ด ์ด๋ชจ์ง ๋์ฒด */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
802 |
.emoji-avatar {
|
803 |
font-size: 1.2rem;
|
804 |
width: 32px;
|
|
|
825 |
|
826 |
<div class="mac-content">
|
827 |
<div class="header">
|
828 |
+
<h1>Zero GPU Spaces</h1>
|
829 |
+
<p>Discover 'TOP 1,000' Zero GPU(Shared A100) spaces from Hugging Face</p>
|
|
|
830 |
</div>
|
831 |
|
832 |
<!-- Tab Navigation -->
|
833 |
<div class="tab-nav">
|
834 |
+
<button id="tabTrendingButton" class="tab-button active">Zero GPU Spaces</button>
|
835 |
+
<button id="tabFixedButton" class="tab-button">Fixed Tab</button>
|
836 |
</div>
|
837 |
|
838 |
<!-- Trending(Zero GPU) Tab Content -->
|
|
|
872 |
<div id="pagination" class="pagination"></div>
|
873 |
</div>
|
874 |
|
875 |
+
<!-- Fixed Tab Content -->
|
876 |
<div id="fixedTab" class="tab-content">
|
877 |
<div id="fixedGrid" class="grid-container"></div>
|
878 |
</div>
|
|
|
924 |
iframeStatuses: {}
|
925 |
};
|
926 |
|
927 |
+
// Advanced iframe loader
|
928 |
const iframeLoader = {
|
929 |
checkQueue: {},
|
930 |
maxAttempts: 5,
|
|
|
944 |
|
945 |
checkIframeStatus: function(spaceKey) {
|
946 |
if (!this.checkQueue[spaceKey]) return;
|
|
|
947 |
const item = this.checkQueue[spaceKey];
|
948 |
if (item.status !== 'loading') {
|
949 |
delete this.checkQueue[spaceKey];
|
950 |
return;
|
951 |
}
|
952 |
item.attempts++;
|
953 |
+
|
954 |
try {
|
955 |
if (!item.iframe || !item.iframe.parentNode) {
|
956 |
delete this.checkQueue[spaceKey];
|
957 |
return;
|
958 |
}
|
959 |
|
|
|
960 |
try {
|
961 |
+
const hasContent = item.iframe.contentWindow &&
|
962 |
+
item.iframe.contentWindow.document &&
|
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('404') ||
|
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 |
return;
|
977 |
}
|
978 |
} catch(e) {
|
979 |
+
// Cross-origin => not necessarily an error
|
980 |
}
|
981 |
|
|
|
982 |
const rect = item.iframe.getBoundingClientRect();
|
983 |
if (rect.width > 50 && rect.height > 50 && item.attempts > 2) {
|
984 |
item.status = 'success';
|
|
|
986 |
return;
|
987 |
}
|
988 |
|
|
|
989 |
if (item.attempts >= this.maxAttempts) {
|
990 |
if (item.iframe.offsetWidth > 0 && item.iframe.offsetHeight > 0) {
|
991 |
item.status = 'success';
|
|
|
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 |
|
|
|
1044 |
label: 'Number of Spaces',
|
1045 |
data: data,
|
1046 |
backgroundColor: colors,
|
1047 |
+
borderColor: colors.map(color => color.replace('0.7','1')),
|
1048 |
borderWidth: 1
|
1049 |
}]
|
1050 |
},
|
|
|
1214 |
container.appendChild(errorPlaceholder);
|
1215 |
}
|
1216 |
|
1217 |
+
// ์นด๋ ๋ชฉ๋ก: ๋ก๋ด ์ด๋ชจ์ง๋ก ๋์ฒด
|
1218 |
function renderGrid(spaces) {
|
1219 |
elements.gridContainer.innerHTML = '';
|
1220 |
|
|
|
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 |
+
// [์ด๋ชจ์ง + ์ ๋ชฉ + ZERO GPU ๋ฐฐ์ง]
|
1246 |
const spaceHeader = document.createElement('div');
|
1247 |
spaceHeader.className = 'space-header';
|
1248 |
|
1249 |
+
// ๐ค ์ด๋ชจ์ง
|
1250 |
const emojiAvatar = document.createElement('div');
|
1251 |
emojiAvatar.className = 'emoji-avatar';
|
1252 |
emojiAvatar.textContent = '๐ค';
|
1253 |
spaceHeader.appendChild(emojiAvatar);
|
1254 |
|
1255 |
+
// title + badge
|
1256 |
const titleWrapper = document.createElement('div');
|
1257 |
titleWrapper.style.display = 'flex';
|
1258 |
titleWrapper.style.alignItems = 'center';
|
|
|
1279 |
metaInfo.style.alignItems = 'center';
|
1280 |
metaInfo.style.marginTop = '6px';
|
1281 |
|
|
|
1282 |
const leftMeta = document.createElement('div');
|
|
|
1283 |
const rankBadge = document.createElement('div');
|
1284 |
rankBadge.className = 'rank-badge';
|
1285 |
rankBadge.textContent = `#${rank}`;
|
|
|
1293 |
|
1294 |
metaInfo.appendChild(leftMeta);
|
1295 |
|
|
|
1296 |
const likesDiv = document.createElement('div');
|
1297 |
likesDiv.className = 'likes-wrapper';
|
1298 |
likesDiv.innerHTML = `<span class="likes-heart">โฅ</span><span>${likes_count}</span>`;
|
|
|
1301 |
headerDiv.appendChild(metaInfo);
|
1302 |
gridItem.appendChild(headerDiv);
|
1303 |
|
|
|
1304 |
if (description) {
|
1305 |
const descP = document.createElement('p');
|
1306 |
descP.className = 'desc-text';
|
|
|
1308 |
gridItem.appendChild(descP);
|
1309 |
}
|
1310 |
|
|
|
1311 |
const content = document.createElement('div');
|
1312 |
content.className = 'grid-content';
|
1313 |
|
|
|
1315 |
iframeContainer.className = 'iframe-container';
|
1316 |
|
1317 |
const iframe = document.createElement('iframe');
|
1318 |
+
iframe.src = embedUrl;
|
1319 |
iframe.title = title;
|
1320 |
iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
|
1321 |
+
iframe.setAttribute('allowfullscreen','');
|
1322 |
+
iframe.setAttribute('frameborder','0');
|
1323 |
iframe.loading = 'lazy';
|
1324 |
|
1325 |
const spaceKey = `${owner}/${name}`;
|
|
|
1342 |
iframeContainer.appendChild(iframe);
|
1343 |
content.appendChild(iframeContainer);
|
1344 |
|
|
|
1345 |
const actions = document.createElement('div');
|
1346 |
actions.className = 'grid-actions';
|
1347 |
|
|
|
1350 |
linkEl.target = '_blank';
|
1351 |
linkEl.className = 'open-link';
|
1352 |
linkEl.textContent = 'Open in new window';
|
|
|
1353 |
actions.appendChild(linkEl);
|
1354 |
+
|
1355 |
gridItem.appendChild(content);
|
1356 |
gridItem.appendChild(actions);
|
|
|
1357 |
elements.gridContainer.appendChild(gridItem);
|
1358 |
|
1359 |
} catch (err) {
|
|
|
1363 |
}
|
1364 |
|
1365 |
function renderFixedGrid() {
|
|
|
1366 |
fixedGridContainer.innerHTML = '';
|
1367 |
|
1368 |
const staticSpaces = [
|
|
|
1415 |
const headerTop = document.createElement('div');
|
1416 |
headerTop.className = 'grid-header-top';
|
1417 |
|
1418 |
+
// ์ด๋ชจ์ง
|
1419 |
const leftWrapper = document.createElement('div');
|
1420 |
leftWrapper.style.display = 'flex';
|
1421 |
leftWrapper.style.alignItems = 'center';
|
|
|
1465 |
iframe.src = "https://" + owner.toLowerCase() + "-" + name.toLowerCase() + ".hf.space";
|
1466 |
iframe.title = title;
|
1467 |
iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
|
1468 |
+
iframe.setAttribute('allowfullscreen','');
|
1469 |
+
iframe.setAttribute('frameborder','0');
|
1470 |
iframe.loading = 'lazy';
|
1471 |
|
1472 |
const spaceKey = `${owner}/${name}`;
|
|
|
1577 |
</html>
|
1578 |
''')
|
1579 |
|
1580 |
+
# Flask run
|
1581 |
app.run(host='0.0.0.0', port=7860)
|
|