Add admin models management page

- Added models table to database
- Created admin routes for model CRUD operations
- Added models.html template
- Added models link to admin dropdown
- Implemented Hugging Face model download functionality
- Added download_huggingface_model function in utils.py
parent 1ddad390
{% extends "base.html" %}
{% block title %}Admin Panel - Models - VidAI{% endblock %}
{% block head %}
<style>
.container { max-width: 1400px; margin: 2rem auto; padding: 0 2rem; }
.admin-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; }
.btn { padding: 0.75rem 2rem; background: #667eea; color: white; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; text-decoration: none; display: inline-block; }
.btn:hover { background: #5a67d8; }
.btn-danger { background: #dc2626; }
.btn-danger:hover { background: #b91c1c; }
.btn-icon { padding: 0.5rem; background: none; border: none; cursor: pointer; color: #64748b; }
.btn-icon:hover { color: #374151; }
.table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
.table th, .table td { padding: 1rem; text-align: left; border-bottom: 1px solid #e5e7eb; }
.table th { background: #f8fafc; font-weight: 600; color: #374151; }
.actions-cell { display: flex; gap: 0.5rem; }
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; margin-bottom: 0.5rem; color: #374151; font-weight: 500; }
.form-group input, .form-group select { width: 100%; padding: 0.5rem; border: 2px solid #e5e7eb; border-radius: 8px; font-size: 1rem; }
.form-group input:focus, .form-group select:focus { outline: none; border-color: #667eea; }
.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; }
/* Modal Styles */
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); }
.modal.show { display: flex; align-items: center; justify-content: center; }
.modal-content { background-color: white; margin: 0; padding: 0; width: 90%; max-width: 600px; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); height: 80vh; overflow-y: auto; display: flex; flex-direction: column; }
.modal-header { padding: 1rem 2rem; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; }
.modal-body { padding: 1rem; flex: 1; }
.modal-footer { padding: 1rem 2rem; border-top: 1px solid #e5e7eb; text-align: right; flex-shrink: 0; }
.form-row { display: flex; gap: 1rem; }
.form-row .form-group { flex: 1; }
</style>
{% endblock %}
{% block content %}
<div class="container">
<div class="admin-card">
<div class="card-header">
<h3><i class="fas fa-brain"></i> Model Management</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 %}
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Path</th>
<th>VRAM (GB)</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for model in models %}
<tr>
<td>{{ model.get('name', 'N/A') }}</td>
<td>{{ model.get('type', 'N/A').title() }}</td>
<td>{{ model.get('path', 'N/A') }}</td>
<td>{{ model.get('vram_estimate', 0) }}</td>
<td>{{ model.get('created_at', 'N/A')[:10] if model.get('created_at') else 'N/A' }}</td>
<td class="actions-cell">
<button onclick="editModel({{ model.get('id') }}, '{{ model.get('name') }}', '{{ model.get('type') }}', '{{ model.get('path') }}', {{ model.get('vram_estimate', 0) }})" class="btn-icon" title="Edit"><i class="fas fa-edit"></i></button>
<button onclick="deleteModel({{ model.get('id') }}, '{{ model.get('name') }}')" class="btn-icon" title="Delete" style="color: #dc2626;"><i class="fas fa-trash"></i></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="admin-card">
<div class="card-header">
<h3><i class="fas fa-plus"></i> Add New Model</h3>
</div>
<form method="post" action="/admin/models/add">
<div class="form-group">
<label for="name">Model Name</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="type">Type</label>
<select id="type" name="type" required>
<option value="local">Local File</option>
<option value="huggingface">Hugging Face</option>
</select>
</div>
<div class="form-group">
<label for="path">Path/Model ID</label>
<input type="text" id="path" name="path" required placeholder="e.g., /path/to/model or Qwen/Qwen2.5-VL-7B-Instruct">
</div>
<div class="form-group">
<label for="vram_estimate">VRAM Estimate (GB)</label>
<input type="number" id="vram_estimate" name="vram_estimate" value="0" min="0" step="0.1">
</div>
<button type="submit" class="btn">Add Model</button>
</form>
</div>
</div>
<!-- Edit Model Modal -->
<div id="editModelModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Edit Model</h3>
<span onclick="closeEditModal()" style="cursor: pointer; font-size: 1.5rem;">&times;</span>
</div>
<form id="editModelForm" method="post" action="">
<div class="modal-body">
<input type="hidden" id="editModelId" name="model_id">
<div class="form-group">
<label for="editName">Model Name</label>
<input type="text" id="editName" name="name" required>
</div>
<div class="form-group">
<label for="editType">Type</label>
<select id="editType" name="type" required>
<option value="local">Local File</option>
<option value="huggingface">Hugging Face</option>
</select>
</div>
<div class="form-group">
<label for="editPath">Path/Model ID</label>
<input type="text" id="editPath" name="path" required>
</div>
<div class="form-group">
<label for="editVram">VRAM Estimate (GB)</label>
<input type="number" id="editVram" name="vram_estimate" min="0" step="0.1" required>
</div>
</div>
<div class="modal-footer">
<button type="button" onclick="closeEditModal()" class="btn" style="background: #6b7280;">Cancel</button>
<button type="submit" class="btn">Update Model</button>
</div>
</form>
</div>
</div>
<script>
function editModel(id, name, type, path, vram) {
document.getElementById('editModelId').value = id;
document.getElementById('editName').value = name;
document.getElementById('editType').value = type;
document.getElementById('editPath').value = path;
document.getElementById('editVram').value = vram;
document.getElementById('editModelForm').action = `/admin/models/${id}/update`;
document.getElementById('editModelModal').classList.add('show');
document.body.style.overflow = 'hidden';
}
function closeEditModal() {
document.getElementById('editModelModal').classList.remove('show');
document.body.style.overflow = 'auto';
}
function deleteModel(id, name) {
if (confirm(`Are you sure you want to delete model "${name}"? This action cannot be undone.`)) {
// Create a form and submit it
const form = document.createElement('form');
form.method = 'POST';
form.action = `/admin/models/${id}/delete`;
document.body.appendChild(form);
form.submit();
}
}
// Close modal when clicking outside
window.onclick = function(event) {
const modal = document.getElementById('editModelModal');
if (event.target == modal) {
closeEditModal();
}
}
</script>
</div>
{% endblock %}
\ No newline at end of file
......@@ -105,6 +105,7 @@
<div class="admin-icon" onclick="toggleAdminMenu()">Admin</div>
<div id="adminDropdown" class="admin-dropdown">
<a href="/admin/train" {% if active_page == 'train' %}class="active"{% endif %}>Train</a>
<a href="/admin/models" {% if active_page == 'models' %}class="active"{% endif %}>Models</a>
<a href="/admin/cluster_tokens" {% if active_page == 'cluster_tokens' %}class="active"{% endif %}>Cluster Tokens</a>
<a href="/admin/cluster_nodes" {% if active_page == 'cluster_nodes' %}class="active"{% endif %}>Cluster Nodes</a>
<a href="/admin/config" {% if active_page == 'config' %}class="active"{% endif %}>Configurations</a>
......
......@@ -21,7 +21,7 @@ Provides web interface for administrative functions.
from flask import Blueprint, request, render_template, redirect, url_for, flash
from .auth import require_auth
from .database import get_user_tokens, update_user_tokens, get_user_queue_items, get_default_user_tokens, create_remember_token, validate_remember_token, delete_remember_token, extend_remember_token, get_all_users, update_user_status, update_user_info, delete_user, get_worker_tokens, deactivate_worker_token, activate_worker_token, delete_worker_token, create_user
from .database import get_user_tokens, update_user_tokens, get_user_queue_items, get_default_user_tokens, create_remember_token, validate_remember_token, delete_remember_token, extend_remember_token, get_all_users, update_user_status, update_user_info, delete_user, get_worker_tokens, deactivate_worker_token, activate_worker_token, delete_worker_token, create_user, get_all_models, create_model, update_model, delete_model, get_model_by_id
from .comm import SocketCommunicator, Message
from .utils import get_current_user_session, login_required, admin_required
......@@ -482,3 +482,82 @@ def update_database_settings():
from flask import flash
flash('Database settings updated successfully!', 'success')
return redirect(url_for('admin.settings'))
@admin_bp.route('/models')
@admin_required
def models():
"""Admin models management page."""
models_list = get_all_models()
user = get_current_user_session()
return render_template('admin/models.html', models=models_list, user=user, active_page='models')
@admin_bp.route('/models/add', methods=['POST'])
@admin_required
def add_model():
"""Add a new model."""
name = request.form.get('name', '').strip()
model_type = request.form.get('type', '').strip()
path = request.form.get('path', '').strip()
vram_estimate = int(request.form.get('vram_estimate', 0))
if not name or not model_type or not path:
flash('All fields are required', 'error')
return redirect(url_for('admin.models'))
if model_type not in ['local', 'huggingface']:
flash('Invalid model type', 'error')
return redirect(url_for('admin.models'))
# If Hugging Face, download the model
if model_type == 'huggingface':
try:
from .utils import download_huggingface_model
local_path = download_huggingface_model(path)
if local_path:
path = local_path
model_type = 'local' # Now it's local
else:
flash('Failed to download model from Hugging Face', 'error')
return redirect(url_for('admin.models'))
except Exception as e:
flash(f'Error downloading model: {str(e)}', 'error')
return redirect(url_for('admin.models'))
if create_model(name, model_type, path, vram_estimate):
flash('Model added successfully!', 'success')
else:
flash('Failed to add model', 'error')
return redirect(url_for('admin.models'))
@admin_bp.route('/models/<int:model_id>/update', methods=['POST'])
@admin_required
def update_model_route(model_id):
"""Update a model."""
name = request.form.get('name', '').strip()
model_type = request.form.get('type', '').strip()
path = request.form.get('path', '').strip()
vram_estimate = int(request.form.get('vram_estimate', 0))
if not name or not model_type or not path:
flash('All fields are required', 'error')
return redirect(url_for('admin.models'))
if model_type not in ['local', 'huggingface']:
flash('Invalid model type', 'error')
return redirect(url_for('admin.models'))
if update_model(model_id, name=name, model_type=model_type, path=path, vram_estimate=vram_estimate):
flash('Model updated successfully!', 'success')
else:
flash('Failed to update model', 'error')
return redirect(url_for('admin.models'))
@admin_bp.route('/models/<int:model_id>/delete', methods=['POST'])
@admin_required
def delete_model_route(model_id):
"""Delete a model."""
if delete_model(model_id):
flash('Model deleted successfully!', 'success')
else:
flash('Failed to delete model', 'error')
return redirect(url_for('admin.models'))
\ No newline at end of file
......@@ -533,6 +533,32 @@ def init_db(conn) -> None:
# Column might already exist
pass
# Models table
if config['type'] == 'mysql':
cursor.execute('''
CREATE TABLE IF NOT EXISTS models (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
type VARCHAR(50) NOT NULL,
path TEXT NOT NULL,
vram_estimate INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
''')
else:
cursor.execute('''
CREATE TABLE IF NOT EXISTS models (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL,
path TEXT NOT NULL,
vram_estimate INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Add data column to sessions table if it doesn't exist
try:
if config['type'] == 'mysql':
......@@ -1835,3 +1861,83 @@ def get_admin_dashboard_stats() -> Dict[str, int]:
conn.close()
return stats
# Model management functions
def get_all_models() -> List[Dict[str, Any]]:
"""Get all models."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('SELECT * FROM models ORDER BY name')
rows = cursor.fetchall()
conn.close()
return [dict(row) for row in rows]
def create_model(name: str, model_type: str, path: str, vram_estimate: int = 0) -> bool:
"""Create a new model."""
conn = get_db_connection()
cursor = conn.cursor()
try:
cursor.execute('INSERT INTO models (name, type, path, vram_estimate) VALUES (?, ?, ?, ?)',
(name, model_type, path, vram_estimate))
conn.commit()
return True
except sqlite3.IntegrityError:
return False
finally:
conn.close()
def update_model(model_id: int, name: str = None, model_type: str = None, path: str = None, vram_estimate: int = None) -> bool:
"""Update a model."""
conn = get_db_connection()
cursor = conn.cursor()
update_fields = []
params = []
if name is not None:
update_fields.append('name = ?')
params.append(name)
if model_type is not None:
update_fields.append('type = ?')
params.append(model_type)
if path is not None:
update_fields.append('path = ?')
params.append(path)
if vram_estimate is not None:
update_fields.append('vram_estimate = ?')
params.append(vram_estimate)
if not update_fields:
return False
params.append(model_id)
query = f'UPDATE models SET {", ".join(update_fields)}, updated_at = CURRENT_TIMESTAMP WHERE id = ?'
cursor.execute(query, params)
conn.commit()
success = cursor.rowcount > 0
conn.close()
return success
def delete_model(model_id: int) -> bool:
"""Delete a model."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('DELETE FROM models WHERE id = ?', (model_id,))
conn.commit()
success = cursor.rowcount > 0
conn.close()
return success
def get_model_by_id(model_id: int) -> Optional[Dict[str, Any]]:
"""Get a model by ID."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('SELECT * FROM models WHERE id = ?', (model_id,))
row = cursor.fetchone()
conn.close()
return dict(row) if row else None
\ No newline at end of file
......@@ -140,3 +140,26 @@ def admin_api_auth_required(f):
return redirect(url_for('dashboard'))
decorated_function.__name__ = f.__name__
return decorated_function
def download_huggingface_model(model_id: str) -> str:
"""Download a model from Hugging Face and return local path."""
import os
from huggingface_hub import snapshot_download
from .compat import get_user_config_dir, ensure_dir
# Create models directory
models_dir = os.path.join(get_user_config_dir(), 'models')
ensure_dir(models_dir)
# Download model
try:
local_path = snapshot_download(
repo_id=model_id,
local_dir=os.path.join(models_dir, model_id.replace('/', '_')),
local_dir_use_symlinks=False
)
return local_path
except Exception as e:
print(f"Failed to download model {model_id}: {e}")
return None
\ No newline at end of file
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