seawolf2357's picture
Update app.py
4f82889 verified
raw
history blame
19.3 kB
from flask import Flask, render_template, request, redirect, url_for, jsonify, session, Response
import requests
import os
from datetime import timedelta
from urllib.parse import urlparse, urljoin
app = Flask(__name__)
app.secret_key = os.urandom(24) # Session encryption key
app.permanent_session_lifetime = timedelta(days=7) # Session duration
# 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",
]
# 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 direct embed URL
def get_embed_url(url):
model_info = extract_model_info(url)
if not model_info or model_info['type'] != 'spaces':
return url
# For spaces, use the embedded version
return f"https://huggingface.co/spaces/{model_info['owner']}/{model_info['repo']}/embed"
# 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("-", " ")
# Huggingface token validation
def validate_token(token):
headers = {"Authorization": f"Bearer {token}"}
# Try whoami-v2 endpoint first
try:
response = requests.get("https://huggingface.co/api/whoami-v2", headers=headers)
if response.ok:
return True, response.json()
except Exception as e:
print(f"whoami-v2 token validation error: {e}")
# Try the original whoami endpoint
try:
response = requests.get("https://huggingface.co/api/whoami", headers=headers)
if response.ok:
return True, response.json()
except Exception as e:
print(f"whoami token validation error: {e}")
return False, None
# Homepage route
@app.route('/')
def home():
return render_template('index.html')
# Login API
@app.route('/api/login', methods=['POST'])
def login():
token = request.form.get('token', '')
if not token:
return jsonify({'success': False, 'message': '토큰을 μž…λ ₯ν•΄μ£Όμ„Έμš”.'})
is_valid, user_info = validate_token(token)
if not is_valid or not user_info:
return jsonify({'success': False, 'message': 'μœ νš¨ν•˜μ§€ μ•Šμ€ ν† ν°μž…λ‹ˆλ‹€.'})
# Find username
username = None
if 'name' in user_info:
username = user_info['name']
elif 'user' in user_info and 'username' in user_info['user']:
username = user_info['user']['username']
elif 'username' in user_info:
username = user_info['username']
else:
username = '인증된 μ‚¬μš©μž'
# Save to session
session['token'] = token
session['username'] = username
return jsonify({
'success': True,
'username': username
})
# Logout API
@app.route('/api/logout', methods=['POST'])
def logout():
session.pop('token', None)
session.pop('username', None)
return jsonify({'success': True})
# URL list API
@app.route('/api/urls', methods=['GET'])
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)
if not model_info:
continue
if search_query and search_query not in url.lower() and search_query not in title.lower():
continue
results.append({
'url': url,
'embedUrl': get_embed_url(url),
'title': title,
'model_info': model_info
})
return jsonify(results)
# Session status API
@app.route('/api/session-status', methods=['GET'])
def session_status():
return jsonify({
'logged_in': 'token' in session,
'username': session.get('username')
})
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);
}
.user-controls {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
.filter-controls {
background-color: #ffffff;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
input[type="password"],
input[type="text"] {
padding: 0.7rem;
border: 1px solid #ddd;
border-radius: 4px;
margin-right: 5px;
font-size: 1rem;
width: 250px;
}
button {
padding: 0.7rem 1.2rem;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.2s;
}
button:hover {
background-color: #45a049;
}
button.logout {
background-color: #f44336;
}
button.logout:hover {
background-color: #d32f2f;
}
.token-help {
margin-top: 0.5rem;
font-size: 0.9rem;
color: #666;
}
.token-help a {
color: #4CAF50;
text-decoration: none;
}
.token-help a:hover {
text-decoration: underline;
}
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(480px, 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: 650px;
}
.grid-header {
padding: 0.8rem;
border-bottom: 1px solid #eee;
background-color: #f9f9f9;
display: flex;
justify-content: space-between;
align-items: center;
}
.grid-header h3 {
margin: 0;
padding: 0;
font-size: 1.1rem;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.open-link {
color: #4CAF50;
text-decoration: none;
font-size: 0.9rem;
}
.grid-content {
flex: 1;
position: relative;
overflow: hidden;
}
.grid-content iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
}
.status-message {
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
display: none;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
.success {
background-color: #dff0d8;
color: #3c763d;
}
.error {
background-color: #f2dede;
color: #a94442;
}
.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); }
}
.login-section {
margin-top: 1rem;
}
.logged-in-section {
display: none;
margin-top: 1rem;
}
@media (max-width: 768px) {
.user-controls {
flex-direction: column;
align-items: flex-start;
}
.user-controls > div {
margin-bottom: 1rem;
}
.grid-container {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="user-controls">
<div>
<span>ν—ˆκΉ…νŽ˜μ΄μŠ€ 계정: </span>
<span id="currentUser">λ‘œκ·ΈμΈλ˜μ§€ μ•ŠμŒ</span>
</div>
<div id="loginSection" class="login-section">
<input type="password" id="tokenInput" placeholder="ν—ˆκΉ…νŽ˜μ΄μŠ€ API 토큰 μž…λ ₯" />
<button id="loginButton">μΈμ¦ν•˜κΈ°</button>
<div class="token-help">
API 토큰은 <a href="https://huggingface.co/settings/tokens" target="_blank">ν—ˆκΉ…νŽ˜μ΄μŠ€ 토큰 νŽ˜μ΄μ§€</a>μ—μ„œ 생성할 수 μžˆμŠ΅λ‹ˆλ‹€.
</div>
</div>
<div id="loggedInSection" class="logged-in-section">
<button id="logoutButton" class="logout">λ‘œκ·Έμ•„μ›ƒ</button>
</div>
</div>
</div>
<div class="filter-controls">
<input type="text" id="searchInput" placeholder="URL λ˜λŠ” 제λͺ©μœΌλ‘œ 검색" />
</div>
<div id="statusMessage" class="status-message"></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 = {
tokenInput: document.getElementById('tokenInput'),
loginButton: document.getElementById('loginButton'),
logoutButton: document.getElementById('logoutButton'),
currentUser: document.getElementById('currentUser'),
gridContainer: document.getElementById('gridContainer'),
loadingIndicator: document.getElementById('loadingIndicator'),
statusMessage: document.getElementById('statusMessage'),
searchInput: document.getElementById('searchInput'),
loginSection: document.getElementById('loginSection'),
loggedInSection: document.getElementById('loggedInSection')
};
// Application state
const state = {
username: null,
isLoading: false
};
// Display loading indicator
function setLoading(isLoading) {
state.isLoading = isLoading;
elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none';
}
// Display status message
function showMessage(message, isError = false) {
elements.statusMessage.textContent = message;
elements.statusMessage.className = `status-message ${isError ? 'error' : 'success'}`;
elements.statusMessage.style.display = 'block';
// Hide message after 3 seconds
setTimeout(() => {
elements.statusMessage.style.display = 'none';
}, 3000);
}
// 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();
}
// Check session status
async function checkSessionStatus() {
try {
const response = await fetch('/api/session-status');
const data = await handleApiResponse(response);
if (data.logged_in) {
state.username = data.username;
elements.currentUser.textContent = data.username;
elements.loginSection.style.display = 'none';
elements.loggedInSection.style.display = 'block';
// Load URL list
loadUrls();
}
} catch (error) {
console.error('μ„Έμ…˜ μƒνƒœ 확인 였λ₯˜:', error);
}
}
// Login process
async function login(token) {
if (!token.trim()) {
showMessage('토큰을 μž…λ ₯ν•΄μ£Όμ„Έμš”.', true);
return;
}
setLoading(true);
try {
const formData = new FormData();
formData.append('token', token);
const response = await fetch('/api/login', {
method: 'POST',
body: formData
});
const data = await handleApiResponse(response);
if (data.success) {
state.username = data.username;
elements.currentUser.textContent = state.username;
elements.loginSection.style.display = 'none';
elements.loggedInSection.style.display = 'block';
showMessage(`${state.username}λ‹˜μœΌλ‘œ λ‘œκ·ΈμΈλ˜μ—ˆμŠ΅λ‹ˆλ‹€.`);
// Load URL list
loadUrls();
} else {
showMessage(data.message || 'λ‘œκ·ΈμΈμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.', true);
}
} catch (error) {
console.error('둜그인 였λ₯˜:', error);
showMessage(`둜그인 였λ₯˜: ${error.message}`, true);
} finally {
setLoading(false);
}
}
// Logout process
async function logout() {
setLoading(true);
try {
const response = await fetch('/api/logout', {
method: 'POST'
});
const data = await handleApiResponse(response);
if (data.success) {
state.username = null;
elements.currentUser.textContent = 'λ‘œκ·ΈμΈλ˜μ§€ μ•ŠμŒ';
elements.tokenInput.value = '';
elements.loginSection.style.display = 'block';
elements.loggedInSection.style.display = 'none';
showMessage('λ‘œκ·Έμ•„μ›ƒλ˜μ—ˆμŠ΅λ‹ˆλ‹€.');
// Clear grid
elements.gridContainer.innerHTML = '';
}
} catch (error) {
console.error('λ‘œκ·Έμ•„μ›ƒ 였λ₯˜:', error);
showMessage(`λ‘œκ·Έμ•„μ›ƒ 였λ₯˜: ${error.message}`, true);
} finally {
setLoading(false);
}
}
// 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);
showMessage(`URL λ‘œλ“œ 였λ₯˜: ${error.message}`, true);
} 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 } = item;
// Create grid item
const gridItem = document.createElement('div');
gridItem.className = 'grid-item';
// Header with title and link
const header = document.createElement('div');
header.className = 'grid-header';
const titleEl = document.createElement('h3');
titleEl.textContent = title;
header.appendChild(titleEl);
const linkEl = document.createElement('a');
linkEl.href = url;
linkEl.target = '_blank';
linkEl.className = 'open-link';
linkEl.textContent = 'μƒˆ μ°½μ—μ„œ μ—΄κΈ°';
header.appendChild(linkEl);
// 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; // Use the embed URL directly
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);
});
}
// Event listeners
elements.loginButton.addEventListener('click', () => {
login(elements.tokenInput.value);
});
elements.logoutButton.addEventListener('click', logout);
// Login with Enter key
elements.tokenInput.addEventListener('keypress', (event) => {
if (event.key === 'Enter') {
login(elements.tokenInput.value);
}
});
// Filter event listener
elements.searchInput.addEventListener('input', () => {
// Debounce input to prevent API calls on every keystroke
clearTimeout(state.searchTimeout);
state.searchTimeout = setTimeout(loadUrls, 300);
});
// Initialize
checkSessionStatus();
</script>
</body>
</html>
''')
# Use port 7860 for Huggingface Spaces
app.run(host='0.0.0.0', port=7860)