Refactor web interface to use separate template files for all pages

- Create login.html, register.html, and config.html templates
- Update /login, /register, /config, /history, and /settings routes to use render_template()
- Remove inline HTML strings from web.py for better maintainability
parent ed15c748
Always commit all the changes to git when you complete a tasklist
<!DOCTYPE html>
<html>
<head>
<title>Configuration</title>
<style>
body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 20px; }
.container { max-width: 800px; margin: auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h1 { color: #333; text-align: center; }
nav { margin-bottom: 20px; }
nav a { margin-right: 10px; text-decoration: none; color: #007bff; }
form { margin-bottom: 20px; }
label { display: block; margin-bottom: 5px; }
select { width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; }
input[type="submit"] { background: #007bff; color: white; padding: 10px; border: none; border-radius: 4px; cursor: pointer; }
</style>
</head>
<body>
<div class="container">
<h1>Configuration</h1>
<nav><a href="/">Analysis</a> | <a href="/train">Training</a> | <a href="/config">Configuration</a></nav>
<form method="post">
<label>Analysis Backend:
<select name="analysis_backend">
<option value="cuda" {% if current_config.analysis_backend == 'cuda' %}selected{% endif %}>CUDA</option>
<option value="rocm" {% if current_config.analysis_backend == 'rocm' %}selected{% endif %}>ROCm</option>
</select>
</label>
<label>Training Backend:
<select name="training_backend">
<option value="cuda" {% if current_config.training_backend == 'cuda' %}selected{% endif %}>CUDA</option>
<option value="rocm" {% if current_config.training_backend == 'rocm' %}selected{% endif %}>ROCm</option>
</select>
</label>
<input type="submit" value="Save Configuration">
</form>
</div>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<title>Login - Video AI Pro</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; }
.login-container { background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); width: 100%; max-width: 400px; }
.logo { text-align: center; font-size: 2rem; font-weight: 700; color: #667eea; margin-bottom: 2rem; }
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; color: #374151; font-weight: 500; }
.form-group input { width: 100%; padding: 0.75rem; border: 2px solid #e5e7eb; border-radius: 8px; font-size: 1rem; }
.form-group input:focus { outline: none; border-color: #667eea; }
.btn { width: 100%; padding: 0.75rem; background: #667eea; color: white; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; }
.btn:hover { background: #5a67d8; }
.links { text-align: center; margin-top: 1.5rem; }
.links a { color: #667eea; text-decoration: none; margin: 0 0.5rem; }
.alert { padding: 0.75rem; border-radius: 8px; margin-bottom: 1rem; }
.alert-error { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }
.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; }
</style>
</head>
<body>
<div class="login-container">
<div class="logo">Video AI Pro</div>
<form method="post">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'error' if category == 'error' else 'success' }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn">Login</button>
</form>
<div class="links">
<a href="/register">Create Account</a> |
<a href="/">Home</a>
</div>
</div>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<title>Register - Video AI Pro</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; }
.register-container { background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); width: 100%; max-width: 400px; }
.logo { text-align: center; font-size: 2rem; font-weight: 700; color: #667eea; margin-bottom: 2rem; }
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; color: #374151; font-weight: 500; }
.form-group input { width: 100%; padding: 0.75rem; border: 2px solid #e5e7eb; border-radius: 8px; font-size: 1rem; }
.form-group input:focus { outline: none; border-color: #667eea; }
.btn { width: 100%; padding: 0.75rem; background: #667eea; color: white; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; }
.btn:hover { background: #5a67d8; }
.links { text-align: center; margin-top: 1.5rem; }
.links a { color: #667eea; text-decoration: none; margin: 0 0.5rem; }
.alert { padding: 0.75rem; border-radius: 8px; margin-bottom: 1rem; }
.alert-error { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }
.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; }
</style>
</head>
<body>
<div class="register-container">
<div class="logo">Video AI Pro</div>
<form method="post">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'error' if category == 'error' else 'success' }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn">Create Account</button>
</form>
<div class="links">
<a href="/login">Already have an account?</a> |
<a href="/">Home</a>
</div>
</div>
</body>
</html>
\ No newline at end of file
......@@ -173,7 +173,7 @@ def init_db(conn) -> None:
email_confirmed BOOLEAN DEFAULT 0,
email_confirmation_token TEXT,
email_confirmation_expires TIMESTAMP,
tokens INTEGER DEFAULT 100,
tokens INTEGER DEFAULT 1000,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP
)
......
......@@ -119,110 +119,13 @@ def dashboard():
tokens = get_user_tokens(user['id'])
queue_items = get_user_queue_items(user['id'])
html = f'''
<!DOCTYPE html>
<html>
<head>
<title>Dashboard - Video AI Pro</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: 'Inter', sans-serif; background: #f8fafc; }}
.header {{ background: white; padding: 1rem 2rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }}
.header-content {{ display: flex; justify-content: space-between; align-items: center; max-width: 1200px; margin: 0 auto; }}
.logo {{ font-size: 1.5rem; font-weight: 700; color: #667eea; }}
.user-menu {{ display: flex; align-items: center; gap: 1rem; }}
.container {{ max-width: 1200px; margin: 2rem auto; padding: 0 2rem; }}
.stats-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; }}
.stat-card {{ background: white; padding: 1.5rem; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }}
.stat-value {{ font-size: 2rem; font-weight: 700; color: #667eea; margin-bottom: 0.5rem; }}
.stat-label {{ color: #64748b; font-size: 0.9rem; }}
.quick-actions {{ background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); margin-bottom: 2rem; }}
.actions-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; }}
.action-btn {{ display: flex; align-items: center; gap: 0.75rem; padding: 1rem; background: #f8fafc; border: 2px solid #e2e8f0; border-radius: 8px; text-decoration: none; color: #475569; transition: all 0.2s; }}
.action-btn:hover {{ background: #667eea; color: white; border-color: #667eea; }}
.recent-jobs {{ background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }}
.job-item {{ padding: 1rem; border-bottom: 1px solid #f1f5f9; display: flex; justify-content: space-between; align-items: center; }}
.job-item:last-child {{ border-bottom: none; }}
.job-status {{ padding: 0.25rem 0.75rem; border-radius: 20px; font-size: 0.8rem; font-weight: 500; }}
.status-queued {{ background: #fef3c7; color: #d97706; }}
.status-processing {{ background: #dbeafe; color: #2563eb; }}
.status-completed {{ background: #d1fae5; color: #065f46; }}
.status-failed {{ background: #fee2e2; color: #dc2626; }}
</style>
</head>
<body>
<header class="header">
<div class="header-content">
<div class="logo">Video AI Pro</div>
<div class="user-menu">
<span>Welcome, {user["username"]}!</span>
<a href="/logout" class="action-btn" style="padding: 0.5rem 1rem; font-size: 0.9rem;">Logout</a>
</div>
</div>
</header>
<div class="container">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{tokens}</div>
<div class="stat-label">Available Tokens</div>
</div>
<div class="stat-card">
<div class="stat-value">{len([j for j in queue_items if j["status"] == "completed"])}</div>
<div class="stat-label">Completed Jobs</div>
</div>
<div class="stat-card">
<div class="stat-value">{len([j for j in queue_items if j["status"] == "processing"])}</div>
<div class="stat-label">Active Jobs</div>
</div>
<div class="stat-card">
<div class="stat-value">{len(queue_items)}</div>
<div class="stat-label">Total Jobs</div>
</div>
</div>
<div class="quick-actions">
<h3 style="margin-bottom: 1rem; color: #1e293b;">Quick Actions</h3>
<div class="actions-grid">
<a href="/analyze" class="action-btn">
<i class="fas fa-search"></i>
<span>Analyze Media</span>
</a>
<a href="/train" class="action-btn">
<i class="fas fa-graduation-cap"></i>
<span>Train Model</span>
</a>
<a href="/history" class="action-btn">
<i class="fas fa-history"></i>
<span>Job History</span>
</a>
<a href="/settings" class="action-btn">
<i class="fas fa-cog"></i>
<span>Settings</span>
</a>
</div>
</div>
<div class="recent-jobs">
<h3 style="margin-bottom: 1rem; color: #1e293b;">Recent Jobs</h3>
{''.join([f'''
<div class="job-item">
<div>
<div style="font-weight: 500;">{job["request_type"].title()}</div>
<div style="color: #64748b; font-size: 0.9rem;">{job["created_at"][:19]}</div>
</div>
<span class="job-status status-{job['status']}">{job['status'].title()}</span>
</div>
''' for job in queue_items[:5]])}
{f'<div style="text-align: center; padding: 1rem; color: #64748b;">{len(queue_items)} total jobs</div>' if queue_items else '<div style="text-align: center; padding: 1rem; color: #64748b;">No jobs yet</div>'}
</div>
</div>
</body>
</html>
'''
return html
return render_template('dashboard.html',
user=user,
tokens=tokens,
completed_jobs=len([j for j in queue_items if j["status"] == "completed"]),
active_jobs=len([j for j in queue_items if j["status"] == "processing"]),
total_jobs=len(queue_items),
queue_items=queue_items[:5])
@app.route('/login', methods=['GET', 'POST'])
def login():
......@@ -239,64 +142,7 @@ def login():
else:
flash('Invalid username or password', 'error')
html = '''
<!DOCTYPE html>
<html>
<head>
<title>Login - Video AI Pro</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; }
.login-container { background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); width: 100%; max-width: 400px; }
.logo { text-align: center; font-size: 2rem; font-weight: 700; color: #667eea; margin-bottom: 2rem; }
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; color: #374151; font-weight: 500; }
.form-group input { width: 100%; padding: 0.75rem; border: 2px solid #e5e7eb; border-radius: 8px; font-size: 1rem; }
.form-group input:focus { outline: none; border-color: #667eea; }
.btn { width: 100%; padding: 0.75rem; background: #667eea; color: white; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; }
.btn:hover { background: #5a67d8; }
.links { text-align: center; margin-top: 1.5rem; }
.links a { color: #667eea; text-decoration: none; margin: 0 0.5rem; }
.alert { padding: 0.75rem; border-radius: 8px; margin-bottom: 1rem; }
.alert-error { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }
.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; }
</style>
</head>
<body>
<div class="login-container">
<div class="logo">Video AI Pro</div>
<form method="post">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'error' if category == 'error' else 'success' }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn">Login</button>
</form>
<div class="links">
<a href="/register">Create Account</a> |
<a href="/">Home</a>
</div>
</div>
</body>
</html>
'''
return render_template_string(html)
return render_template('login.html')
@app.route('/register', methods=['GET', 'POST'])
def register():
......@@ -317,69 +163,7 @@ def register():
else:
flash(message, 'error')
html = '''
<!DOCTYPE html>
<html>
<head>
<title>Register - Video AI Pro</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; }
.register-container { background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); width: 100%; max-width: 400px; }
.logo { text-align: center; font-size: 2rem; font-weight: 700; color: #667eea; margin-bottom: 2rem; }
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; color: #374151; font-weight: 500; }
.form-group input { width: 100%; padding: 0.75rem; border: 2px solid #e5e7eb; border-radius: 8px; font-size: 1rem; }
.form-group input:focus { outline: none; border-color: #667eea; }
.btn { width: 100%; padding: 0.75rem; background: #667eea; color: white; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; }
.btn:hover { background: #5a67d8; }
.links { text-align: center; margin-top: 1.5rem; }
.links a { color: #667eea; text-decoration: none; margin: 0 0.5rem; }
.alert { padding: 0.75rem; border-radius: 8px; margin-bottom: 1rem; }
.alert-error { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }
.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; }
</style>
</head>
<body>
<div class="register-container">
<div class="logo">Video AI Pro</div>
<form method="post">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'error' if category == 'error' else 'success' }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn">Create Account</button>
</form>
<div class="links">
<a href="/login">Already have an account?</a> |
<a href="/">Home</a>
</div>
</div>
</body>
</html>
'''
return render_template_string(html)
return render_template('register.html')
@app.route('/logout')
def logout():
......@@ -407,13 +191,27 @@ def analyze():
model_path = request.form.get('model_path', 'Qwen/Qwen2.5-VL-7B-Instruct')
prompt = request.form.get('prompt', 'Describe this image.')
interval = int(request.form.get('interval', 10))
uploaded_file = request.files.get('file')
local_path = request.form.get('local_path')
if local_path:
media_path = None
if uploaded_file and uploaded_file.filename:
# Save uploaded file temporarily
import tempfile
import os
with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(uploaded_file.filename)[1]) as tmp:
tmp.write(uploaded_file.read())
media_path = tmp.name
elif local_path:
media_path = local_path
if media_path:
data = {
'model_path': model_path,
'prompt': prompt,
'local_path': local_path,
'local_path': media_path,
'interval': interval,
'user_id': user['id']
}
msg_id = send_to_backend('analyze_request', data)
......@@ -425,162 +223,75 @@ def analyze():
else:
result = result_data.get('error', 'Error')
template = '''
<!DOCTYPE html>
<html>
<head>
<title>Analyze Media - Video AI Pro</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', sans-serif; background: #f8fafc; }
.header { background: white; padding: 1rem 2rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.header-content { display: flex; justify-content: space-between; align-items: center; max-width: 1200px; margin: 0 auto; }
.logo { font-size: 1.5rem; font-weight: 700; color: #667eea; }
.nav { display: flex; gap: 2rem; }
.nav a { text-decoration: none; color: #64748b; font-weight: 500; }
.nav a.active { color: #667eea; }
.user-menu { display: flex; align-items: center; gap: 1rem; }
.container { max-width: 800px; margin: 2rem auto; padding: 0 2rem; }
.analysis-form { background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; color: #374151; font-weight: 500; }
.form-group input, .form-group textarea { width: 100%; padding: 0.75rem; border: 2px solid #e5e7eb; border-radius: 8px; font-size: 1rem; }
.form-group input:focus, .form-group textarea:focus { outline: none; border-color: #667eea; }
.form-group textarea { resize: vertical; min-height: 100px; }
.btn { padding: 0.75rem 2rem; background: #667eea; color: white; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; }
.btn:hover { background: #5a67d8; }
.result { background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); margin-top: 2rem; }
.alert { padding: 0.75rem; border-radius: 8px; margin-bottom: 1rem; }
.alert-error { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }
.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; }
.tokens { background: #f0f9ff; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; border-left: 4px solid #667eea; }
</style>
</head>
<body>
<header class="header">
<div class="header-content">
<div class="logo">Video AI Pro</div>
<nav class="nav">
<a href="/dashboard">Dashboard</a>
<a href="/analyze" class="active">Analyze</a>
<a href="/train">Train</a>
<a href="/history">History</a>
<a href="/settings">Settings</a>
</nav>
<div class="user-menu">
<span>''' + str(get_user_tokens(user["id"])) + ''' tokens</span>
<a href="/logout" style="color: #dc2626;">Logout</a>
</div>
</div>
</header>
<div class="container">
<div class="tokens">
<strong>Available Tokens:</strong> ''' + str(get_user_tokens(user["id"])) + ''' (Analysis costs ~10 tokens)
</div>
<div class="analysis-form">
<h2 style="margin-bottom: 1.5rem; color: #1e293b;"><i class="fas fa-search"></i> Analyze Media</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'error' if category == 'error' else 'success' }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="post">
<div class="form-group">
<label for="model_path">AI Model</label>
<input type="text" id="model_path" name="model_path" value="Qwen/Qwen2.5-VL-7B-Instruct" placeholder="Model path or HuggingFace ID">
</div>
<div class="form-group">
<label for="local_path">Media File Path</label>
<input type="text" id="local_path" name="local_path" placeholder="/path/to/your/media/file.mp4" required>
<small style="color: #6b7280;">Enter the full path to your image or video file</small>
</div>
<div class="form-group">
<label for="prompt">Analysis Prompt</label>
<textarea id="prompt" name="prompt" placeholder="Describe what you want to analyze...">Describe this image or video in detail, including any actions, scenes, or important elements.</textarea>
</div>
<button type="submit" class="btn"><i class="fas fa-play"></i> Start Analysis</button>
</form>
</div>
{% if result %}
<div class="result">
<h3 style="margin-bottom: 1rem; color: #1e293b;"><i class="fas fa-check-circle"></i> Analysis Result</h3>
<div style="background: #f8fafc; padding: 1rem; border-radius: 8px; border-left: 4px solid #10b981;">
<pre style="white-space: pre-wrap; font-family: inherit; margin: 0;">{{ result }}</pre>
</div>
</div>
{% endif %}
</div>
</body>
</html>
'''
return render_template_string(template, result=result)
return render_template('analyze.html',
user=user,
tokens=get_user_tokens(user["id"]),
result=result)
@app.route('/train', methods=['GET', 'POST'])
@login_required
def train():
message = None
if request.method == 'POST':
# Check token balance
tokens = get_user_tokens(user['id'])
if tokens < 100:
flash('Insufficient tokens. Training requires 100 tokens.', 'error')
return redirect(url_for('dashboard'))
output_model = request.form.get('output_model', 'MyCustomModel')
description = request.form.get('description', '')
uploaded_data = request.files.get('data')
train_dir = request.form.get('train_dir')
train_path = None
if uploaded_data and uploaded_data.filename:
# Handle uploaded training data
import tempfile
import os
import shutil
if uploaded_data.filename.lower().endswith('.zip'):
# Handle ZIP file
with tempfile.TemporaryDirectory() as tmp_dir:
zip_path = os.path.join(tmp_dir, "data.zip")
with open(zip_path, "wb") as f:
f.write(uploaded_data.read())
extract_dir = os.path.join(tmp_dir, "extracted")
shutil.unpack_archive(zip_path, extract_dir)
train_path = extract_dir
else:
# Handle single file
with tempfile.TemporaryDirectory() as tmp_dir:
file_path = os.path.join(tmp_dir, uploaded_data.filename)
with open(file_path, "wb") as f:
f.write(uploaded_data.read())
train_path = tmp_dir
elif train_dir and os.path.isdir(train_dir):
train_path = train_dir
if train_path:
data = {
'output_model': request.form.get('output_model', './VideoModel'),
'description': request.form.get('description', ''),
'train_dir': request.form.get('train_dir', '')
'output_model': output_model,
'description': description,
'train_path': train_path,
'user_id': user['id']
}
msg_id = send_to_backend('train_request', data)
result_data = get_result(msg_id)
if 'data' in result_data:
message = result_data['data'].get('message', 'Training completed')
# Deduct tokens
update_user_tokens(user['id'], -100)
else:
message = result_data.get('error', 'Error')
else:
flash('Please provide training data (upload file or specify directory)', 'error')
html = '''
<!DOCTYPE html>
<html>
<head>
<title>VideoModel AI - Training</title>
<style>
body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 20px; }
.container { max-width: 800px; margin: auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h1 { color: #333; text-align: center; }
form { margin-bottom: 20px; }
label { display: block; margin-bottom: 5px; }
input[type="text"] { width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; }
input[type="submit"] { background: #007bff; color: white; padding: 10px; border: none; border-radius: 4px; cursor: pointer; }
.message { background: #e9ecef; padding: 10px; border-radius: 4px; }
</style>
</head>
<body>
<div class="container">
<h1>VideoModel AI Web Interface</h1>
<nav><a href="/">Analysis</a> | <a href="/train">Training</a> | <a href="/config">Configuration</a></nav>
<h2>Train Model</h2>
<form method="post">
<label>Output Model Path: <input type="text" name="output_model" value="./VideoModel"></label>
<label>Description: <textarea name="description"></textarea></label>
<label>Training Directory: <input type="text" name="train_dir"></label>
<input type="submit" value="Start Training">
</form>
{% if message %}
<div class="message">
<p>{{ message }}</p>
</div>
{% endif %}
</div>
</body>
</html>
'''
return render_template_string(html, message=message)
return render_template('train.html',
user=user,
tokens=get_user_tokens(user["id"]),
message=message)
@app.route('/config', methods=['GET', 'POST'])
def config():
......@@ -598,45 +309,7 @@ def config():
config_data = get_result(msg_id)
current_config = config_data.get('data', {})
html = '''
<!DOCTYPE html>
<html>
<head>
<title>Configuration</title>
<style>
body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 20px; }
.container { max-width: 800px; margin: auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h1 { color: #333; text-align: center; }
form { margin-bottom: 20px; }
label { display: block; margin-bottom: 5px; }
select { width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; }
input[type="submit"] { background: #007bff; color: white; padding: 10px; border: none; border-radius: 4px; cursor: pointer; }
</style>
</head>
<body>
<div class="container">
<h1>Configuration</h1>
<nav><a href="/">Analysis</a> | <a href="/train">Training</a> | <a href="/config">Configuration</a></nav>
<form method="post">
<label>Analysis Backend:
<select name="analysis_backend">
<option value="cuda" {% if current_config.analysis_backend == 'cuda' %}selected{% endif %}>CUDA</option>
<option value="rocm" {% if current_config.analysis_backend == 'rocm' %}selected{% endif %}>ROCm</option>
</select>
</label>
<label>Training Backend:
<select name="training_backend">
<option value="cuda" {% if current_config.training_backend == 'cuda' %}selected{% endif %}>CUDA</option>
<option value="rocm" {% if current_config.training_backend == 'rocm' %}selected{% endif %}>ROCm</option>
</select>
</label>
<input type="submit" value="Save Configuration">
</form>
</div>
</body>
</html>
'''
return render_template_string(html, current_config=current_config)
return render_template('config.html', current_config=current_config)
@app.route('/history')
@login_required
......@@ -645,81 +318,10 @@ def history():
user = get_current_user_session()
queue_items = get_user_queue_items(user['id'])
html = f'''
<!DOCTYPE html>
<html>
<head>
<title>Job History - Video AI Pro</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: 'Inter', sans-serif; background: #f8fafc; }}
.header {{ background: white; padding: 1rem 2rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }}
.header-content {{ display: flex; justify-content: space-between; align-items: center; max-width: 1200px; margin: 0 auto; }}
.logo {{ font-size: 1.5rem; font-weight: 700; color: #667eea; }}
.nav {{ display: flex; gap: 2rem; }}
.nav a {{ text-decoration: none; color: #64748b; font-weight: 500; }}
.nav a.active {{ color: #667eea; }}
.user-menu {{ display: flex; align-items: center; gap: 1rem; }}
.container {{ max-width: 1200px; margin: 2rem auto; padding: 0 2rem; }}
.history-table {{ background: white; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); overflow: hidden; }}
.table-header {{ background: #f8fafc; padding: 1rem 2rem; border-bottom: 1px solid #e5e7eb; }}
.table-header h2 {{ margin: 0; color: #1e293b; }}
.job-row {{ display: grid; grid-template-columns: 1fr 2fr 1fr 1fr 1fr; gap: 1rem; padding: 1rem 2rem; border-bottom: 1px solid #f1f5f9; align-items: center; }}
.job-row:last-child {{ border-bottom: none; }}
.job-type {{ font-weight: 600; color: #374151; }}
.job-data {{ color: #6b7280; font-size: 0.9rem; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }}
.job-time {{ color: #6b7280; font-size: 0.9rem; }}
.job-status {{ padding: 0.25rem 0.75rem; border-radius: 20px; font-size: 0.8rem; font-weight: 500; text-align: center; }}
.status-queued {{ background: #fef3c7; color: #d97706; }}
.status-processing {{ background: #dbeafe; color: #2563eb; }}
.status-completed {{ background: #d1fae5; color: #065f46; }}
.status-failed {{ background: #fee2e2; color: #dc2626; }}
.job-tokens {{ font-weight: 600; color: #667eea; }}
.no-jobs {{ text-align: center; padding: 3rem; color: #6b7280; }}
</style>
</head>
<body>
<header class="header">
<div class="header-content">
<div class="logo">Video AI Pro</div>
<nav class="nav">
<a href="/dashboard">Dashboard</a>
<a href="/analyze">Analyze</a>
<a href="/train">Train</a>
<a href="/history" class="active">History</a>
<a href="/settings">Settings</a>
</nav>
<div class="user-menu">
<span>{get_user_tokens(user["id"])} tokens</span>
<a href="/logout" style="color: #dc2626;">Logout</a>
</div>
</div>
</header>
<div class="container">
<div class="history-table">
<div class="table-header">
<h2><i class="fas fa-history"></i> Job History</h2>
</div>
{''.join([f'''
<div class="job-row">
<div class="job-type">{job["request_type"].title()}</div>
<div class="job-data" title="{job.get("data", {}).get("prompt", job.get("data", {}).get("description", "N/A"))}">
{job.get("data", {}).get("prompt", job.get("data", {}).get("description", "N/A"))[:50]}{"..." if len(str(job.get("data", {}))) > 50 else ""}
</div>
<div class="job-time">{job["created_at"][:19]}</div>
<span class="job-status status-{job['status']}">{job['status'].title()}</span>
<div class="job-tokens">{job.get('used_tokens', 0)} used</div>
</div>
''' for job in queue_items]) if queue_items else '<div class="no-jobs">No jobs found. Start by analyzing some media!</div>'}
</div>
</div>
</body>
</html>
'''
return html
return render_template('history.html',
user=user,
tokens=get_user_tokens(user["id"]),
queue_items=queue_items)
@app.route('/settings')
@login_required
......@@ -727,109 +329,9 @@ def settings():
"""User settings page."""
user = get_current_user_session()
template = '''
<!DOCTYPE html>
<html>
<head>
<title>Settings - Video AI Pro</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', sans-serif; background: #f8fafc; }
.header { background: white; padding: 1rem 2rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.header-content { display: flex; justify-content: space-between; align-items: center; max-width: 1200px; margin: 0 auto; }
.logo { font-size: 1.5rem; font-weight: 700; color: #667eea; }
.nav { display: flex; gap: 2rem; }
.nav a { text-decoration: none; color: #64748b; font-weight: 500; }
.nav a.active { color: #667eea; }
.user-menu { display: flex; align-items: center; gap: 1rem; }
.container { max-width: 800px; margin: 2rem auto; padding: 0 2rem; }
.settings-card { background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); margin-bottom: 2rem; }
.card-header { margin-bottom: 1.5rem; }
.card-header h3 { margin: 0; color: #1e293b; }
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; color: #374151; font-weight: 500; }
.form-group input, .form-group select { width: 100%; padding: 0.75rem; border: 2px solid #e5e7eb; border-radius: 8px; font-size: 1rem; }
.form-group input:focus, .form-group select:focus { outline: none; border-color: #667eea; }
.btn { padding: 0.75rem 2rem; background: #667eea; color: white; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; }
.btn:hover { background: #5a67d8; }
.alert { padding: 0.75rem; border-radius: 8px; margin-bottom: 1rem; }
.alert-error { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }
.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; }
.user-info { background: #f0f9ff; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #667eea; margin-bottom: 2rem; }
</style>
</head>
<body>
<header class="header">
<div class="header-content">
<div class="logo">Video AI Pro</div>
<nav class="nav">
<a href="/dashboard">Dashboard</a>
<a href="/analyze">Analyze</a>
<a href="/train">Train</a>
<a href="/history">History</a>
<a href="/settings" class="active">Settings</a>
</nav>
<div class="user-menu">
<span>''' + str(get_user_tokens(user["id"])) + ''' tokens</span>
<a href="/logout" style="color: #dc2626;">Logout</a>
</div>
</div>
</header>
<div class="container">
<div class="user-info">
<h3><i class="fas fa-user"></i> Account Information</h3>
<p><strong>Username:</strong> ''' + user["username"] + '''</p>
<p><strong>Email:</strong> ''' + user.get("email", "Not provided") + '''</p>
<p><strong>Role:</strong> ''' + user["role"].title() + '''</p>
<p><strong>Member since:</strong> ''' + user.get("created_at", "Unknown")[:10] + '''</p>
</div>
<div class="settings-card">
<div class="card-header">
<h3><i class="fas fa-cog"></i> Application Settings</h3>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'error' if category == 'error' else 'success' }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="post" action="/update_settings">
<div class="form-group">
<label for="default_model">Default AI Model</label>
<input type="text" id="default_model" name="default_model" value="Qwen/Qwen2.5-VL-7B-Instruct" placeholder="Model path or HuggingFace ID">
</div>
<div class="form-group">
<label for="theme">Theme Preference</label>
<select id="theme" name="theme">
<option value="light" selected>Light</option>
<option value="dark">Dark</option>
</select>
</div>
<button type="submit" class="btn"><i class="fas fa-save"></i> Save Settings</button>
</form>
</div>
<div class="settings-card">
<div class="card-header">
<h3><i class="fas fa-key"></i> API Access</h3>
</div>
<p>Generate API tokens to access Video AI Pro programmatically.</p>
<a href="/generate_token" class="btn" style="background: #10b981;"><i class="fas fa-plus"></i> Generate New Token</a>
</div>
</div>
</body>
</html>
'''
return render_template_string(template)
return render_template('settings.html',
user=user,
tokens=get_user_tokens(user["id"]))
@app.route('/update_settings', methods=['POST'])
@login_required
......@@ -850,6 +352,38 @@ def generate_token():
flash(f'New API token generated: {token[:20]}...', 'success')
return redirect(url_for('settings'))
@app.route('/stats')
def stats():
"""Get system stats for the sidebar."""
import psutil
import torch
import time
data = {'status': 'Idle'} # Simplified - in real implementation, get from backend
# GPU stats
if torch.cuda.is_available():
data['gpu_count'] = torch.cuda.device_count()
data['gpus'] = []
for i in range(torch.cuda.device_count()):
gpu = {
'name': torch.cuda.get_device_name(i),
'memory_used': torch.cuda.memory_allocated(i) / 1024**3, # GB
'memory_total': torch.cuda.get_device_properties(i).total_memory / 1024**3,
'utilization': 0 # Would need pynvml for actual utilization
}
data['gpus'].append(gpu)
else:
data['gpu_count'] = 0
# CPU and RAM
data['cpu_percent'] = psutil.cpu_percent()
ram = psutil.virtual_memory()
data['ram_used'] = ram.used / 1024**3
data['ram_total'] = ram.total / 1024**3
return json.dumps(data)
@app.route('/static/<path:filename>')
def serve_static(filename):
return send_from_directory('static', filename)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment