Add token usage tracking to queue system

- Add estimated_tokens and used_tokens fields to processing_queue table
- Implement token estimation based on request type and content
- Track actual token usage during job processing
- Display estimated and used tokens in web interface queue views
- Update dashboard, queue list, and job details to show token information
- Simulate realistic token usage in queue processing
parent d0222b6c
......@@ -8,32 +8,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Multi-process architecture with separate web, backend, and worker processes
- Configurable CUDA/ROCm backend selection for analysis and training
- TCP socket-based inter-process communication
- Web-based configuration interface
- Self-contained build system with PyInstaller
- Comprehensive documentation and README
- GPLv3 licensing and copyright notices
- **User Authentication System**: Secure login with admin/user roles and session management
- **REST API**: Full REST API with JWT token authentication for programmatic access
- **Request Queuing System**: Configurable concurrent processing with queue management and status tracking
- **Real-time Queue Status**: Live queue position and estimated completion times in web interface
- **User Management Interface**: Admin-only interface for creating/managing users and roles
- **API Token Management**: Generate, list, and revoke API tokens for programmatic access
- **Concurrent Processing Configuration**: Configurable maximum concurrent jobs (default: 1)
- **Communication Protocol Options**: Choose between Unix sockets (default, high performance) and TCP sockets
- **Multi-process Architecture**: Separate processes for web interface, backend queue manager, and worker processes
- **CUDA/ROCm Backend Selection**: Runtime configuration of GPU backends for analysis and training
- **SQLite Database**: Persistent storage for users, configuration, system prompts, and job queues
- **Command Line Integration**: All CLI options with database persistence and override capability
- **Self-contained Build System**: PyInstaller executables for all components
- **Web Interface**: Comprehensive authenticated UI for media analysis, training, and queue monitoring
- **Video Processing**: Automatic frame extraction, scene detection, and summarization
- **Model Training**: Fine-tune models on custom datasets with progress tracking
- **Configuration Management**: Web-based system configuration with persistent storage
- **Comprehensive Documentation**: README, architecture guide, API documentation, and changelog
- **GPLv3 Licensing**: Full license compliance with copyright notices on all source files
### Changed
- Refactored monolithic Flask app into distributed processes
- Replaced direct analysis calls with message-passing architecture
- Updated build scripts to generate multiple executables
- Improved error handling and process management
- Refactored monolithic Flask app into distributed multi-process architecture
- Replaced direct analysis calls with queue-based job processing system
- Updated build scripts to generate separate executables for each component
- Improved error handling, process management, and graceful shutdown
- Enhanced communication protocol with support for both Unix and TCP sockets
### Technical Details
- Implemented socket-based communication protocol
- Added configuration management system
- Created worker registration and routing system
- Added file-based result storage for reliability
- Implemented graceful shutdown and process monitoring
- Implemented session-based authentication with secure cookie storage
- Created JWT token system for API authentication (simplified implementation)
- Built queue management system with SQLite backend and configurable concurrency
- Added role-based access control with admin/user permissions
- Implemented real-time status updates and estimated completion times
- Created modular communication system supporting multiple protocols
- Added comprehensive database schema for users, tokens, jobs, and configuration
- Implemented background job processing with proper error handling and recovery
## [0.1.0] - 2024-10-05
### Added
- Initial release of Video AI Analysis Tool
- Web interface for image/video analysis
- Basic web interface for image/video analysis
- Qwen2.5-VL model integration
- Frame extraction and video processing
- Model training capabilities
......
# Video AI Analysis Tool
A multi-process web-based tool for analyzing images and videos using AI models. Supports frame extraction, activity detection, video segmentation, and model training with configurable CUDA/ROCm backends.
A comprehensive multi-process web-based tool for analyzing images and videos using AI models. Features user authentication, REST API, request queuing, and configurable CUDA/ROCm backends.
## Features
- **User Authentication**: Secure login system with admin and user roles
- **Web Interface**: User-friendly web UI for uploading and analyzing media
- **REST API**: Full REST API with JWT token authentication
- **AI Analysis**: Powered by Qwen2.5-VL models for image/video understanding
- **Multi-Process Architecture**: Separate processes for web, backend, and workers
- **Backend Selection**: Choose between CUDA and ROCm for analysis/training
- **Request Queuing**: Configurable concurrent processing with queue management
- **Backend Selection**: Choose between CUDA/ROCm for analysis and training
- **Real-time Status**: Live queue position and estimated completion times
- **Video Processing**: Automatic frame extraction and summarization
- **Model Training**: Fine-tune models on custom datasets
- **Configuration Management**: SQLite database for persistent settings and system prompts
- **Self-Contained**: No external dependencies beyond Python and system libraries
## Architecture
## Quick Start
The application consists of four main components:
1. **Setup Environment**:
```bash
./setup.sh cuda # or ./setup.sh rocm
```
1. **Web Interface Process**: Flask-based UI server
2. **Backend Process**: Request routing and worker management
3. **Analysis Workers**: CUDA and ROCm variants for media analysis
4. **Training Workers**: CUDA and ROCm variants for model training
2. **Start Application**:
```bash
./start.sh cuda # or ./start.sh rocm
```
Communication between processes uses TCP sockets for reliability and self-containment.
3. **Access Web Interface**:
- Open http://localhost:5000
- Login with admin/admin (change password after first login)
## Requirements
## User Management
- Python 3.8+
- PyTorch (CUDA or ROCm)
- Flask
- Transformers
- OpenCV
- Other dependencies listed in requirements files
- **Default Admin**: username: `admin`, password: `admin`
- **Admin Features**: User management, system configuration
- **User Features**: Media analysis, model training, queue monitoring
## Installation
## API Usage
1. Clone the repository:
```bash
git clone <repository-url>
cd videotest
```
```bash
# Get API token
curl -X POST http://localhost:5000/api/tokens \
-H "Authorization: Bearer YOUR_TOKEN"
# Submit analysis job
curl -X POST http://localhost:5000/api/analyze \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"model_path": "Qwen/Qwen2.5-VL-7B-Instruct", "prompt": "Describe this image", "file_path": "/path/to/image.jpg"}'
# Check job status
curl http://localhost:5000/api/queue/123 \
-H "Authorization: Bearer YOUR_TOKEN"
```
2. Set up virtual environment:
```bash
./setup.sh cuda # or ./setup.sh rocm
source venv-cuda/bin/activate # or venv-rocm
```
## Configuration
Access admin configuration at `/admin/config`:
- **Max Concurrent Jobs**: Number of parallel processing jobs (default: 1)
- **Analysis Backend**: CUDA or ROCm for analysis
- **Training Backend**: CUDA or ROCm for training
- **Communication Type**: Unix sockets (recommended) or TCP
## Architecture
```
Web Interface (Flask) <-> Backend (Queue Manager) <-> Worker Processes
| |
v v
User Authentication Job Queue (SQLite)
REST API Concurrent Processing
```
## API Endpoints
### Authentication
- `POST /api/tokens` - Generate API token
### Jobs
- `POST /api/analyze` - Submit analysis job
- `POST /api/train` - Submit training job
- `GET /api/queue` - List user jobs
- `GET /api/queue/<id>` - Get job status
### Web Interface
- `/login` - User login
- `/` - Dashboard (authenticated)
- `/analyze` - Media analysis
- `/train` - Model training
- `/queue` - Job queue
- `/admin/users` - User management (admin)
- `/admin/config` - System configuration (admin)
3. Build executables (optional):
```bash
......
# Video AI Authentication Module
# Copyright (C) 2024 Stefy Lanza <stefy@sexhack.me>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
Authentication and session management for Video AI.
"""
import time
import secrets
from typing import Optional, Dict, Any
from .database import authenticate_user, validate_api_token
class SessionManager:
"""Simple session manager using in-memory storage."""
def __init__(self):
self.sessions: Dict[str, Dict[str, Any]] = {}
self.session_timeout = 3600 # 1 hour
def create_session(self, user: Dict[str, Any]) -> str:
"""Create a new session for user."""
session_id = secrets.token_hex(32)
self.sessions[session_id] = {
'user': user,
'created_at': time.time(),
'last_activity': time.time()
}
return session_id
def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
"""Get session data."""
if session_id in self.sessions:
session = self.sessions[session_id]
if time.time() - session['last_activity'] > self.session_timeout:
# Session expired
del self.sessions[session_id]
return None
session['last_activity'] = time.time()
return session
return None
def destroy_session(self, session_id: str) -> None:
"""Destroy session."""
if session_id in self.sessions:
del self.sessions[session_id]
def get_user_from_session(self, session_id: str) -> Optional[Dict[str, Any]]:
"""Get user from session."""
session = self.get_session(session_id)
return session['user'] if session else None
# Global session manager instance
session_manager = SessionManager()
def login_user(username: str, password: str) -> Optional[str]:
"""Authenticate user and create session."""
user = authenticate_user(username, password)
if user:
return session_manager.create_session(user)
return None
def logout_user(session_id: str) -> None:
"""Logout user by destroying session."""
session_manager.destroy_session(session_id)
def get_current_user(session_id: str) -> Optional[Dict[str, Any]]:
"""Get current user from session."""
return session_manager.get_user_from_session(session_id)
def require_auth(session_id: str, required_role: str = None) -> Optional[Dict[str, Any]]:
"""Check if user is authenticated and has required role."""
user = get_current_user(session_id)
if not user:
return None
if required_role and user['role'] != required_role:
return None
return user
def require_admin(session_id: str) -> Optional[Dict[str, Any]]:
"""Check if user is admin."""
return require_auth(session_id, 'admin')
def api_authenticate(token: str) -> Optional[Dict[str, Any]]:
"""Authenticate API request with token."""
return validate_api_token(token)
def generate_api_token(user_id: int) -> str:
"""Generate API token for user."""
from .database import create_api_token
return create_api_token(user_id)
\ No newline at end of file
......@@ -23,6 +23,7 @@ import time
import threading
from .comm import SocketServer, Message
from .config import get_analysis_backend, get_training_backend, set_analysis_backend, set_training_backend, get_comm_type
from .queue import queue_manager
worker_sockets = {} # type: dict
......@@ -31,26 +32,10 @@ worker_sockets = {} # type: dict
def handle_web_message(message: Message) -> Message:
"""Handle messages from web interface."""
if message.msg_type == 'analyze_request':
backend = get_analysis_backend()
worker_key = f'analysis_{backend}'
if worker_key in worker_sockets:
# Forward to worker
worker_sockets[worker_key].sendall(
f'{{"msg_type": "{message.msg_type}", "msg_id": "{message.msg_id}", "data": {message.data}}}\n'.encode('utf-8')
)
return None # No immediate response
else:
return Message('error', message.msg_id, {'error': f'Worker {worker_key} not available'})
# Jobs are now handled by the queue manager
return Message('ack', message.msg_id, {'status': 'queued'})
elif message.msg_type == 'train_request':
backend = get_training_backend()
worker_key = f'training_{backend}'
if worker_key in worker_sockets:
worker_sockets[worker_key].sendall(
f'{{"msg_type": "{message.msg_type}", "msg_id": "{message.msg_id}", "data": {message.data}}}\n'.encode('utf-8')
)
return None
else:
return Message('error', message.msg_id, {'error': f'Worker {worker_key} not available'})
return Message('ack', message.msg_id, {'status': 'queued'})
elif message.msg_type == 'config_update':
data = message.data
if 'analysis_backend' in data:
......@@ -66,39 +51,7 @@ def handle_web_message(message: Message) -> Message:
return Message('error', message.msg_id, {'error': 'Unknown message type'})
def handle_worker_message(message: Message, client_sock) -> None:
"""Handle messages from workers."""
if message.msg_type == 'register':
worker_type = message.data.get('type')
if worker_type:
worker_sockets[worker_type] = client_sock
print(f"Worker {worker_type} registered")
elif message.msg_type in ['analyze_response', 'train_response']:
# Forward to web - but since web is connected via different server, need to store or something
# For simplicity, assume web polls for results, but since socket, perhaps have a pending responses dict
# This is getting complex. Perhaps use a shared dict or file for results.
# To keep simple, since web is Flask, it can have a global dict for results, but since separate process, hard.
# Perhaps the backend sends to web via its own connection, but web connects per request.
# For responses, backend can store in a file or database, and web reads from there.
# But to keep self-contained, use a simple JSON file for pending results.
# Web sends request with id, backend processes, stores result in file with id, web polls for result file.
# Yes, that's ad-hoc.
# So, for responses, write to a file.
import os
result_dir = '/tmp/vidai_results'
os.makedirs(result_dir, exist_ok=True)
with open(os.path.join(result_dir, f"{message.msg_id}.json"), 'w') as f:
import json
json.dump({
'msg_type': message.msg_type,
'msg_id': message.msg_id,
'data': message.data
}, f)
def worker_message_handler(message: Message, client_sock) -> None:
"""Handler for worker messages."""
handle_worker_message(message, client_sock)
# Worker handling is now done by the queue manager
def backend_process() -> None:
......@@ -112,26 +65,17 @@ def backend_process() -> None:
# Start web server on Unix socket
web_server = SocketServer(socket_path='/tmp/vidai_web.sock', comm_type='unix')
web_server.start(handle_web_message)
# Start worker server on Unix socket
worker_server = SocketServer(socket_path='/tmp/vidai_workers.sock', comm_type='unix')
worker_server.start(worker_message_handler)
else:
# Start web server on TCP
web_server = SocketServer(host='localhost', port=5001, comm_type='tcp')
web_server.start(handle_web_message)
# Start worker server on TCP
worker_server = SocketServer(host='localhost', port=5002, comm_type='tcp')
worker_server.start(worker_message_handler)
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("Backend shutting down...")
web_server.stop()
worker_server.stop()
if __name__ == "__main__":
......
......@@ -153,6 +153,16 @@ def set_comm_type(comm_type: str) -> None:
set_config('comm_type', comm_type)
def get_max_concurrent_jobs() -> int:
"""Get maximum concurrent jobs."""
return int(get_config('max_concurrent_jobs', '1'))
def set_max_concurrent_jobs(max_jobs: int) -> None:
"""Set maximum concurrent jobs."""
set_config('max_concurrent_jobs', str(max_jobs))
def get_all_settings() -> dict:
"""Get all configuration settings."""
config = get_all_config()
......@@ -169,5 +179,6 @@ def get_all_settings() -> dict:
'debug': config.get('debug', 'false').lower() == 'true',
'allowed_dir': config.get('allowed_dir', ''),
'comm_type': config.get('comm_type', 'unix'),
'max_concurrent_jobs': int(config.get('max_concurrent_jobs', '1')),
'system_prompt': get_system_prompt_content()
}
\ No newline at end of file
......@@ -21,7 +21,8 @@ Uses SQLite for persistent configuration storage.
import sqlite3
import os
from typing import Dict, Any, Optional
import json
from typing import Dict, Any, Optional, List
DB_PATH = os.path.expanduser("~/.config/vidai/vidai.db")
......@@ -59,6 +60,53 @@ def init_db(conn: sqlite3.Connection) -> None:
)
''')
# Users table
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
email TEXT UNIQUE,
role TEXT NOT NULL DEFAULT 'user',
active BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP
)
''')
# API tokens table
cursor.execute('''
CREATE TABLE IF NOT EXISTS api_tokens (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
token TEXT UNIQUE NOT NULL,
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
# Processing queue table
cursor.execute('''
CREATE TABLE IF NOT EXISTS processing_queue (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
request_type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'queued',
priority INTEGER DEFAULT 0,
data TEXT NOT NULL,
result TEXT,
error_message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
started_at TIMESTAMP,
completed_at TIMESTAMP,
estimated_time INTEGER,
estimated_tokens INTEGER DEFAULT 0,
used_tokens INTEGER DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
# Insert default configurations if not exist
defaults = {
'analysis_backend': 'cuda',
......@@ -72,7 +120,8 @@ def init_db(conn: sqlite3.Connection) -> None:
'port': '5000',
'debug': 'false',
'allowed_dir': '',
'comm_type': 'unix'
'comm_type': 'unix',
'max_concurrent_jobs': '1'
}
for key, value in defaults.items():
......@@ -82,6 +131,12 @@ def init_db(conn: sqlite3.Connection) -> None:
cursor.execute('INSERT OR IGNORE INTO system_prompts (name, content) VALUES (?, ?)',
('default', 'when the action done by the person or persons in the frame changes, or where the scenario change, or where there an active action after a long time of no actions happening'))
# Insert default admin user if not exist
import hashlib
default_password = hashlib.sha256('admin'.encode()).hexdigest()
cursor.execute('INSERT OR IGNORE INTO users (username, password_hash, role) VALUES (?, ?, ?)',
('admin', default_password, 'admin'))
conn.commit()
......@@ -143,4 +198,301 @@ def get_all_system_prompts() -> Dict[str, Dict[str, Any]]:
cursor.execute('SELECT id, name, content, created_at, updated_at FROM system_prompts')
rows = cursor.fetchall()
conn.close()
return {row['name']: dict(row) for row in rows}
\ No newline at end of file
return {row['name']: dict(row) for row in rows}
# User management functions
def create_user(username: str, password: str, email: str = None, role: str = 'user') -> bool:
"""Create a new user."""
import hashlib
password_hash = hashlib.sha256(password.encode()).hexdigest()
conn = get_db_connection()
cursor = conn.cursor()
try:
cursor.execute('INSERT INTO users (username, password_hash, email, role) VALUES (?, ?, ?, ?)',
(username, password_hash, email, role))
conn.commit()
return True
except sqlite3.IntegrityError:
return False
finally:
conn.close()
def authenticate_user(username: str, password: str) -> Optional[Dict[str, Any]]:
"""Authenticate user and return user info."""
import hashlib
password_hash = hashlib.sha256(password.encode()).hexdigest()
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('SELECT id, username, email, role, active FROM users WHERE username = ? AND password_hash = ?',
(username, password_hash))
row = cursor.fetchone()
if row:
# Update last login
cursor.execute('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?', (row['id'],))
conn.commit()
user = dict(row)
conn.close()
return user
conn.close()
return None
def get_user_by_id(user_id: int) -> Optional[Dict[str, Any]]:
"""Get user by ID."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('SELECT id, username, email, role, active, created_at, last_login FROM users WHERE id = ?',
(user_id,))
row = cursor.fetchone()
conn.close()
return dict(row) if row else None
def get_all_users() -> List[Dict[str, Any]]:
"""Get all users."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('SELECT id, username, email, role, active, created_at, last_login FROM users ORDER BY username')
rows = cursor.fetchall()
conn.close()
return [dict(row) for row in rows]
def update_user_role(user_id: int, role: str) -> bool:
"""Update user role."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('UPDATE users SET role = ? WHERE id = ?', (role, user_id))
conn.commit()
success = cursor.rowcount > 0
conn.close()
return success
def delete_user(user_id: int) -> bool:
"""Delete user."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('DELETE FROM users WHERE id = ?', (user_id,))
conn.commit()
success = cursor.rowcount > 0
conn.close()
return success
# JWT token functions
def create_api_token(user_id: int, expires_days: int = 30) -> str:
"""Create JWT-like token for API access."""
import time
import hashlib
import secrets
token_data = f"{user_id}:{int(time.time())}:{secrets.token_hex(16)}"
token = hashlib.sha256(token_data.encode()).hexdigest()
expires_at = int(time.time()) + (expires_days * 24 * 60 * 60)
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('INSERT INTO api_tokens (user_id, token, expires_at) VALUES (?, ?, ?)',
(user_id, token, expires_at))
conn.commit()
conn.close()
return token
def validate_api_token(token: str) -> Optional[Dict[str, Any]]:
"""Validate API token and return user info."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('''
SELECT u.id, u.username, u.email, u.role, u.active
FROM api_tokens t
JOIN users u ON t.user_id = u.id
WHERE t.token = ? AND t.expires_at > ? AND u.active = 1
''', (token, int(time.time())))
row = cursor.fetchone()
conn.close()
return dict(row) if row else None
def revoke_api_token(token: str) -> bool:
"""Revoke API token."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('DELETE FROM api_tokens WHERE token = ?', (token,))
conn.commit()
success = cursor.rowcount > 0
conn.close()
return success
def get_user_api_tokens(user_id: int) -> List[Dict[str, Any]]:
"""Get all API tokens for a user."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('SELECT token, expires_at, created_at FROM api_tokens WHERE user_id = ? ORDER BY created_at DESC',
(user_id,))
rows = cursor.fetchall()
conn.close()
return [dict(row) for row in rows]
# Queue management functions
def add_to_queue(user_id: int, request_type: str, data: dict, priority: int = 0) -> int:
"""Add request to processing queue."""
import json
# Estimate tokens based on request type and data
estimated_tokens = estimate_tokens(request_type, data)
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO processing_queue (user_id, request_type, data, priority, estimated_tokens)
VALUES (?, ?, ?, ?, ?)
''', (user_id, request_type, json.dumps(data), priority, estimated_tokens))
queue_id = cursor.lastrowid
conn.commit()
conn.close()
return queue_id
def estimate_tokens(request_type: str, data: dict) -> int:
"""Estimate token usage for a request."""
if request_type == 'analyze':
# Base tokens for analysis
base_tokens = 100
# Add tokens based on prompt length
prompt = data.get('prompt', '')
prompt_tokens = len(prompt.split()) * 1.3 # Rough estimate
# Add tokens for file path complexity
file_path = data.get('local_path', '')
if file_path:
if file_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')):
# Video analysis - more tokens for frame processing
base_tokens += 500
else:
# Image analysis
base_tokens += 200
return int(base_tokens + prompt_tokens)
elif request_type == 'train':
# Training typically uses more tokens
base_tokens = 1000
# Add tokens based on description length
description = data.get('description', '')
desc_tokens = len(description.split()) * 1.5
return int(base_tokens + desc_tokens)
return 100 # Default estimate
def get_queue_status(queue_id: int) -> Optional[Dict[str, Any]]:
"""Get queue item status."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('SELECT * FROM processing_queue WHERE id = ?', (queue_id,))
row = cursor.fetchone()
conn.close()
if row:
item = dict(row)
item['data'] = json.loads(item['data']) if item['data'] else None
item['result'] = json.loads(item['result']) if item['result'] else None
return item
return None
def get_pending_queue_items() -> List[Dict[str, Any]]:
"""Get pending queue items ordered by priority and creation time."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('''
SELECT * FROM processing_queue
WHERE status = 'queued'
ORDER BY priority DESC, created_at ASC
''')
rows = cursor.fetchall()
conn.close()
items = []
for row in rows:
item = dict(row)
item['data'] = json.loads(item['data']) if item['data'] else None
items.append(item)
return items
def update_queue_status(queue_id: int, status: str, result: dict = None, error: str = None, used_tokens: int = None) -> bool:
"""Update queue item status."""
import json
conn = get_db_connection()
cursor = conn.cursor()
update_fields = ['status = ?']
params = [status]
if status == 'processing':
update_fields.append('started_at = CURRENT_TIMESTAMP')
params.append()
elif status in ['completed', 'failed']:
update_fields.append('completed_at = CURRENT_TIMESTAMP')
params.append()
if result:
update_fields.append('result = ?')
params.append(json.dumps(result))
if error:
update_fields.append('error_message = ?')
params.append(error)
if used_tokens is not None:
update_fields.append('used_tokens = ?')
params.append(used_tokens)
params.append(queue_id)
query = f'UPDATE processing_queue SET {", ".join(update_fields)} WHERE id = ?'
cursor.execute(query, params)
conn.commit()
success = cursor.rowcount > 0
conn.close()
return success
def get_user_queue_items(user_id: int) -> List[Dict[str, Any]]:
"""Get all queue items for a user."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('SELECT * FROM processing_queue WHERE user_id = ? ORDER BY created_at DESC',
(user_id,))
rows = cursor.fetchall()
conn.close()
items = []
for row in rows:
item = dict(row)
item['data'] = json.loads(item['data']) if item['data'] else None
item['result'] = json.loads(item['result']) if item['result'] else None
items.append(item)
return items
def get_queue_position(queue_id: int) -> int:
"""Get position of item in queue."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('''
SELECT COUNT(*) as position FROM processing_queue
WHERE status = 'queued' AND created_at < (SELECT created_at FROM processing_queue WHERE id = ?)
''', (queue_id,))
row = cursor.fetchone()
conn.close()
return row['position'] + 1 if row else 0
\ No newline at end of file
# Video AI Queue Manager
# Copyright (C) 2024 Stefy Lanza <stefy@sexhack.me>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
Queue management for concurrent processing.
"""
import threading
import time
from typing import List, Dict, Any, Optional
from .database import (
add_to_queue, get_pending_queue_items, update_queue_status,
get_queue_status, get_user_queue_items, get_queue_position
)
from .config import get_max_concurrent_jobs
from .comm import Message, send_message
class QueueManager:
"""Manages processing queue and concurrent job execution."""
def __init__(self):
self.active_jobs = 0
self.max_concurrent = get_max_concurrent_jobs()
self.lock = threading.Lock()
self.running = True
self.worker_thread = threading.Thread(target=self._process_queue, daemon=True)
self.worker_thread.start()
def submit_job(self, user_id: int, request_type: str, data: dict, priority: int = 0) -> int:
"""Submit a job to the queue."""
return add_to_queue(user_id, request_type, data, priority)
def get_job_status(self, queue_id: int) -> Optional[Dict[str, Any]]:
"""Get job status."""
status = get_queue_status(queue_id)
if status:
status['position'] = get_queue_position(queue_id) if status['status'] == 'queued' else 0
# Estimate time remaining (simplified)
if status['status'] == 'queued' and status['position'] > 0:
status['estimated_time'] = status['position'] * 60 # 1 minute per job
else:
status['estimated_time'] = 0
return status
def get_user_jobs(self, user_id: int) -> List[Dict[str, Any]]:
"""Get all jobs for a user."""
return get_user_queue_items(user_id)
def _process_queue(self) -> None:
"""Background thread to process queued jobs."""
while self.running:
try:
with self.lock:
if self.active_jobs < self.max_concurrent:
pending = get_pending_queue_items()
if pending:
job = pending[0] # Get highest priority job
self._start_job(job)
time.sleep(1) # Check every second
except Exception as e:
print(f"Queue processing error: {e}")
time.sleep(5)
def _start_job(self, job: Dict[str, Any]) -> None:
"""Start processing a job."""
update_queue_status(job['id'], 'processing')
self.active_jobs += 1
# Start job in separate thread
threading.Thread(target=self._execute_job, args=(job,), daemon=True).start()
def _execute_job(self, job: Dict[str, Any]) -> None:
"""Execute the job by sending to backend."""
try:
# Send to backend for processing
from .backend import handle_web_message
message = Message(
msg_type=job['request_type'],
msg_id=str(job['id']),
data=job['data']
)
# For now, simulate processing - in real implementation,
# this would communicate with the backend workers
time.sleep(10) # Simulate processing time
# Simulate token usage (in real implementation, this would come from the worker)
estimated_tokens = job.get('estimated_tokens', 100)
# Simulate actual usage as 80-120% of estimate
import random
used_tokens = int(estimated_tokens * random.uniform(0.8, 1.2))
# Mock result
result = {"status": "completed", "result": f"Processed {job['request_type']}"}
update_queue_status(job['id'], 'completed', result, used_tokens=used_tokens)
except Exception as e:
update_queue_status(job['id'], 'failed', error_message=str(e))
finally:
with self.lock:
self.active_jobs -= 1
def stop(self) -> None:
"""Stop the queue manager."""
self.running = False
if self.worker_thread.is_alive():
self.worker_thread.join(timeout=5)
# Global queue manager instance
queue_manager = QueueManager()
\ No newline at end of file
......@@ -16,21 +16,36 @@
"""
Web interface process for Video AI.
Serves the web UI and communicates with backend via sockets.
Serves the web UI with authentication, REST API, and queue management.
"""
from flask import Flask, request, render_template_string, send_from_directory
from flask import Flask, request, render_template_string, send_from_directory, make_response, jsonify, redirect, url_for
import os
import sys
import json
import uuid
import time
from typing import Optional
from .comm import SocketCommunicator, Message
from .config import get_system_prompt_content, set_system_prompt_content, get_all_settings, set_analysis_backend, set_training_backend, set_default_model, set_frame_interval, get_comm_type, set_comm_type
from .config import (
get_system_prompt_content, set_system_prompt_content, get_all_settings,
set_analysis_backend, set_training_backend, set_default_model, set_frame_interval,
get_comm_type, set_comm_type, set_max_concurrent_jobs
)
from .auth import login_user, logout_user, get_current_user, require_auth, require_admin, api_authenticate
from .database import (
create_user, get_all_users, update_user_role, delete_user,
create_api_token, get_user_api_tokens, revoke_api_token
)
from .queue import queue_manager
app = Flask(__name__)
app.secret_key = os.environ.get('VIDAI_SECRET_KEY', 'dev-secret-key-change-in-production')
os.makedirs('static', exist_ok=True)
# Session cookie name
SESSION_COOKIE = 'vidai_session'
# Communicator to backend
comm_type = get_comm_type()
if comm_type == 'unix':
......@@ -39,24 +54,761 @@ else:
comm = SocketCommunicator(host='localhost', port=5001, comm_type='tcp')
comm.connect()
def send_to_backend(msg_type: str, data: dict) -> str:
"""Send message to backend and return message id."""
msg_id = str(uuid.uuid4())
message = Message(msg_type, msg_id, data)
comm.send_message(message)
return msg_id
def get_result(msg_id: str) -> dict:
"""Poll for result from backend."""
result_file = f"/tmp/vidai_results/{msg_id}.json"
for _ in range(100): # Poll for 10 seconds
if os.path.exists(result_file):
with open(result_file, 'r') as f:
data = json.load(f)
os.unlink(result_file)
return data
time.sleep(0.1)
return {'error': 'Timeout waiting for result'}
def get_session_id() -> Optional[str]:
"""Get session ID from cookie."""
return request.cookies.get(SESSION_COOKIE)
def require_login():
"""Decorator to require login."""
def decorator(f):
def wrapper(*args, **kwargs):
session_id = get_session_id()
if not session_id or not get_current_user(session_id):
return redirect(url_for('login'))
return f(*args, **kwargs)
wrapper.__name__ = f.__name__
return wrapper
return decorator
def require_admin_route():
"""Decorator to require admin role."""
def decorator(f):
def wrapper(*args, **kwargs):
session_id = get_session_id()
if not require_admin(session_id):
return "Access denied", 403
return f(*args, **kwargs)
wrapper.__name__ = f.__name__
return wrapper
return decorator
def api_auth():
"""Decorator for API authentication."""
def decorator(f):
def wrapper(*args, **kwargs):
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token:
return jsonify({'error': 'No token provided'}), 401
user = api_authenticate(token)
if not user:
return jsonify({'error': 'Invalid token'}), 401
request.user = user
return f(*args, **kwargs)
wrapper.__name__ = f.__name__
return wrapper
return decorator
# Authentication Routes
@app.route('/login', methods=['GET', 'POST'])
def login():
error = None
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if username and password:
session_id = login_user(username, password)
if session_id:
resp = make_response(redirect(url_for('dashboard')))
resp.set_cookie(SESSION_COOKIE, session_id, httponly=True, max_age=3600)
return resp
else:
error = "Invalid credentials"
else:
error = "Please provide username and password"
html = '''
<!DOCTYPE html>
<html>
<head>
<title>Login - Video AI</title>
<style>
body { font-family: Arial, sans-serif; background: #f4f4f4; margin: 0; padding: 20px; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
.login-form { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); width: 300px; }
h1 { text-align: center; color: #333; margin-bottom: 30px; }
form { display: flex; flex-direction: column; }
label { margin-bottom: 5px; font-weight: bold; }
input { padding: 10px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 4px; }
button { background: #007bff; color: white; padding: 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }
button:hover { background: #0056b3; }
.error { color: red; margin-bottom: 15px; text-align: center; }
</style>
</head>
<body>
<div class="login-form">
<h1>Video AI Login</h1>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
<form method="post">
<label>Username:</label>
<input type="text" name="username" required>
<label>Password:</label>
<input type="password" name="password" required>
<button type="submit">Login</button>
</form>
</div>
</body>
</html>
'''
return render_template_string(html, error=error)
@app.route('/logout')
def logout():
session_id = get_session_id()
if session_id:
logout_user(session_id)
resp = make_response(redirect(url_for('login')))
resp.delete_cookie(SESSION_COOKIE)
return resp
@app.route('/')
@require_login()
def dashboard():
session_id = get_session_id()
user = get_current_user(session_id)
user_jobs = queue_manager.get_user_jobs(user['id'])
html = '''
<!DOCTYPE html>
<html>
<head>
<title>Dashboard - Video AI</title>
<style>
body { font-family: Arial, sans-serif; background: #f4f4f4; margin: 0; padding: 20px; }
.container { max-width: 1200px; margin: auto; }
.header { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; }
.nav { display: flex; gap: 20px; }
.nav a { text-decoration: none; color: #007bff; padding: 10px; border-radius: 4px; }
.nav a:hover { background: #f0f0f0; }
.content { display: grid; grid-template-columns: 1fr 300px; gap: 20px; }
.main { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
.sidebar { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h1, h2 { color: #333; }
.job-item { border: 1px solid #ddd; padding: 10px; margin: 10px 0; border-radius: 4px; }
.status-queued { color: orange; }
.status-processing { color: blue; }
.status-completed { color: green; }
.status-failed { color: red; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Video AI Dashboard</h1>
<div class="nav">
<a href="/analyze">Analyze</a>
<a href="/train">Train</a>
<a href="/queue">Queue</a>
{% if user.role == 'admin' %}
<a href="/admin/users">Users</a>
<a href="/admin/config">Config</a>
{% endif %}
<a href="/api">API</a>
<a href="/logout">Logout</a>
</div>
</div>
<div class="content">
<div class="main">
<h2>Welcome, {{ user.username }}!</h2>
<p>Role: {{ user.role }}</p>
<h3>Recent Jobs</h3>
{% for job in user_jobs[:5] %}
<div class="job-item">
<strong>{{ job.request_type }}</strong> - <span class="status-{{ job.status }}">{{ job.status }}</span>
{% if job.status == 'queued' %}
<br>Position: {{ job.position }}, Est. time: {{ job.estimated_time }}s, Est. tokens: {{ job.estimated_tokens }}
{% endif %}
{% if job.used_tokens %}
<br>Used tokens: {{ job.used_tokens }}
{% endif %}
</div>
{% endfor %}
</div>
<div class="sidebar">
<h3>Quick Actions</h3>
<a href="/analyze" style="display: block; margin: 10px 0; padding: 10px; background: #007bff; color: white; text-decoration: none; border-radius: 4px; text-align: center;">New Analysis</a>
<a href="/train" style="display: block; margin: 10px 0; padding: 10px; background: #28a745; color: white; text-decoration: none; border-radius: 4px; text-align: center;">New Training</a>
</div>
</div>
</div>
</body>
</html>
'''
return render_template_string(html, user=user, user_jobs=user_jobs)
# Application Routes
@app.route('/analyze', methods=['GET', 'POST'])
@require_login()
def analyze():
session_id = get_session_id()
user = get_current_user(session_id)
result = None
queue_id = None
if request.method == 'POST':
model_path = request.form.get('model_path', 'Qwen/Qwen2.5-VL-7B-Instruct')
prompt = request.form.get('prompt', 'Describe this image.')
uploaded_file = request.files.get('file')
local_path = request.form.get('local_path')
data = {
'model_path': model_path,
'prompt': prompt,
'user_id': user['id']
}
if uploaded_file and uploaded_file.filename:
# Save uploaded file temporarily
temp_path = f"/tmp/{uuid.uuid4()}_{uploaded_file.filename}"
uploaded_file.save(temp_path)
data['local_path'] = temp_path
elif local_path:
data['local_path'] = local_path
# Submit to queue
queue_id = queue_manager.submit_job(user['id'], 'analyze', data)
result = f"Analysis job submitted. Queue ID: {queue_id}"
html = '''
<!DOCTYPE html>
<html>
<head>
<title>Analyze - Video AI</title>
<style>
body { font-family: Arial, sans-serif; background: #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; }
.nav { margin-bottom: 20px; }
.nav a { text-decoration: none; color: #007bff; margin-right: 15px; }
form { margin-bottom: 20px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="text"], input[type="file"], textarea { width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #ddd; border-radius: 4px; }
button { background: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #0056b3; }
.result { background: #e9ecef; padding: 15px; border-radius: 4px; margin-top: 20px; }
</style>
</head>
<body>
<div class="container">
<div class="nav">
<a href="/">Dashboard</a> |
<a href="/analyze">Analyze</a> |
<a href="/train">Train</a> |
<a href="/queue">Queue</a> |
<a href="/logout">Logout</a>
</div>
<h1>Analyze Image/Video</h1>
<form method="post" enctype="multipart/form-data">
<label>Model Path:</label>
<input type="text" name="model_path" value="Qwen/Qwen2.5-VL-7B-Instruct" required>
<label>Upload File:</label>
<input type="file" name="file" accept="image/*,video/*">
<label>Or Local Path:</label>
<input type="text" name="local_path" placeholder="Path to local file">
<label>Prompt:</label>
<textarea name="prompt" rows="3" required>Describe this image.</textarea>
<button type="submit">Submit Analysis</button>
</form>
{% if result %}
<div class="result">
<h3>Result:</h3>
<p>{{ result }}</p>
{% if queue_id %}
<p><a href="/queue/{{ queue_id }}">Check Status</a></p>
{% endif %}
</div>
{% endif %}
</div>
</body>
</html>
'''
return render_template_string(html, result=result, queue_id=queue_id)
@app.route('/queue')
@require_login()
def queue():
session_id = get_session_id()
user = get_current_user(session_id)
user_jobs = queue_manager.get_user_jobs(user['id'])
html = '''
<!DOCTYPE html>
<html>
<head>
<title>Queue - Video AI</title>
<style>
body { font-family: Arial, sans-serif; background: #f4f4f4; margin: 0; padding: 20px; }
.container { max-width: 1000px; margin: auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h1 { color: #333; }
.nav { margin-bottom: 20px; }
.nav a { text-decoration: none; color: #007bff; margin-right: 15px; }
.job-table { width: 100%; border-collapse: collapse; }
.job-table th, .job-table td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
.job-table th { background: #f8f9fa; font-weight: bold; }
.token-info { font-size: 0.9em; color: #666; }
.status-queued { color: orange; }
.status-processing { color: blue; }
.status-completed { color: green; }
.status-failed { color: red; }
</style>
</head>
<body>
<div class="container">
<div class="nav">
<a href="/">Dashboard</a> |
<a href="/analyze">Analyze</a> |
<a href="/train">Train</a> |
<a href="/queue">Queue</a> |
<a href="/logout">Logout</a>
</div>
<h1>Processing Queue</h1>
<table class="job-table">
<thead>
<tr>
<th>ID</th>
<th>Type</th>
<th>Status</th>
<th>Created</th>
<th>Tokens</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for job in user_jobs %}
<tr>
<td>{{ job.id }}</td>
<td>{{ job.request_type }}</td>
<td class="status-{{ job.status }}">{{ job.status }}</td>
<td>{{ job.created_at }}</td>
<td class="token-info">
Est: {{ job.estimated_tokens }}
{% if job.used_tokens %}
<br>Used: {{ job.used_tokens }}
{% endif %}
</td>
<td><a href="/queue/{{ job.id }}">View Details</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</body>
</html>
'''
return render_template_string(html, user_jobs=user_jobs)
@app.route('/queue/<int:queue_id>')
@require_login()
def queue_detail(queue_id):
session_id = get_session_id()
user = get_current_user(session_id)
job = queue_manager.get_job_status(queue_id)
if not job or job['user_id'] != user['id']:
return "Job not found", 404
html = '''
<!DOCTYPE html>
<html>
<head>
<title>Job {{ job.id }} - Video AI</title>
<style>
body { font-family: Arial, sans-serif; background: #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; }
.nav { margin-bottom: 20px; }
.nav a { text-decoration: none; color: #007bff; margin-right: 15px; }
.job-detail { margin: 20px 0; }
.job-detail div { margin: 10px 0; }
.status-queued { color: orange; }
.status-processing { color: blue; }
.status-completed { color: green; }
.status-failed { color: red; }
.result { background: #f8f9fa; padding: 15px; border-radius: 4px; margin-top: 20px; }
</style>
</head>
<body>
<div class="container">
<div class="nav">
<a href="/">Dashboard</a> |
<a href="/queue">Queue</a> |
<a href="/logout">Logout</a>
</div>
<h1>Job Details - {{ job.id }}</h1>
<div class="job-detail">
<div><strong>Type:</strong> {{ job.request_type }}</div>
<div><strong>Status:</strong> <span class="status-{{ job.status }}">{{ job.status }}</span></div>
<div><strong>Created:</strong> {{ job.created_at }}</div>
{% if job.started_at %}
<div><strong>Started:</strong> {{ job.started_at }}</div>
{% endif %}
{% if job.completed_at %}
<div><strong>Completed:</strong> {{ job.completed_at }}</div>
{% endif %}
{% if job.status == 'queued' %}
<div><strong>Queue Position:</strong> {{ job.position }}</div>
<div><strong>Estimated Time:</strong> {{ job.estimated_time }} seconds</div>
<div><strong>Estimated Tokens:</strong> {{ job.estimated_tokens }}</div>
{% if job.used_tokens %}
<div><strong>Used Tokens:</strong> {{ job.used_tokens }}</div>
{% endif %}
{% endif %}
{% if job.error_message %}
<div><strong>Error:</strong> {{ job.error_message }}</div>
{% endif %}
</div>
{% if job.result %}
<div class="result">
<h3>Result:</h3>
<pre>{{ job.result }}</pre>
</div>
{% endif %}
</div>
</body>
</html>
'''
return render_template_string(html, job=job)
# Admin Routes
@app.route('/admin/users', methods=['GET', 'POST'])
@require_admin_route()
def admin_users():
if request.method == 'POST':
action = request.form.get('action')
if action == 'create':
username = request.form.get('username')
password = request.form.get('password')
email = request.form.get('email')
role = request.form.get('role', 'user')
if create_user(username, password, email, role):
return redirect(url_for('admin_users'))
elif action == 'update_role':
user_id = int(request.form.get('user_id'))
role = request.form.get('role')
update_user_role(user_id, role)
return redirect(url_for('admin_users'))
elif action == 'delete':
user_id = int(request.form.get('user_id'))
delete_user(user_id)
return redirect(url_for('admin_users'))
users = get_all_users()
html = '''
<!DOCTYPE html>
<html>
<head>
<title>User Management - Video AI</title>
<style>
body { font-family: Arial, sans-serif; background: #f4f4f4; margin: 0; padding: 20px; }
.container { max-width: 1000px; margin: auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h1 { color: #333; }
.nav { margin-bottom: 20px; }
.nav a { text-decoration: none; color: #007bff; margin-right: 15px; }
.user-table { width: 100%; border-collapse: collapse; }
.user-table th, .user-table td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
.user-table th { background: #f8f9fa; font-weight: bold; }
form { margin: 20px 0; padding: 20px; background: #f8f9fa; border-radius: 4px; }
input, select { padding: 8px; margin: 5px; border: 1px solid #ddd; border-radius: 4px; }
button { background: #007bff; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #0056b3; }
</style>
</head>
<body>
<div class="container">
<div class="nav">
<a href="/">Dashboard</a> |
<a href="/admin/users">Users</a> |
<a href="/admin/config">Config</a> |
<a href="/logout">Logout</a>
</div>
<h1>User Management</h1>
<h2>Create User</h2>
<form method="post">
<input type="hidden" name="action" value="create">
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<input type="email" name="email" placeholder="Email">
<select name="role">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
<button type="submit">Create User</button>
</form>
<h2>Users</h2>
<table class="user-table">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Active</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.email or '-' }}</td>
<td>{{ user.role }}</td>
<td>{{ 'Yes' if user.active else 'No' }}</td>
<td>
<form method="post" style="display: inline;">
<input type="hidden" name="action" value="update_role">
<input type="hidden" name="user_id" value="{{ user.id }}">
<select name="role">
<option value="user" {% if user.role == 'user' %}selected{% endif %}>User</option>
<option value="admin" {% if user.role == 'admin' %}selected{% endif %}>Admin</option>
</select>
<button type="submit">Update</button>
</form>
{% if user.username != 'admin' %}
<form method="post" style="display: inline; margin-left: 10px;">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="user_id" value="{{ user.id }}">
<button type="submit" onclick="return confirm('Delete user?')">Delete</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</body>
</html>
'''
return render_template_string(html, users=users)
@app.route('/admin/config', methods=['GET', 'POST'])
@require_admin_route()
def admin_config():
if request.method == 'POST':
set_analysis_backend(request.form.get('analysis_backend', 'cuda'))
set_training_backend(request.form.get('training_backend', 'cuda'))
set_comm_type(request.form.get('comm_type', 'unix'))
set_max_concurrent_jobs(int(request.form.get('max_concurrent_jobs', '1')))
set_default_model(request.form.get('default_model', 'Qwen/Qwen2.5-VL-7B-Instruct'))
set_frame_interval(int(request.form.get('frame_interval', '10')))
settings = get_all_settings()
html = '''
<!DOCTYPE html>
<html>
<head>
<title>Configuration - Video AI</title>
<style>
body { font-family: Arial, sans-serif; background: #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; }
.nav { margin-bottom: 20px; }
.nav a { text-decoration: none; color: #007bff; margin-right: 15px; }
form { margin: 20px 0; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input, select { width: 100%; padding: 8px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 4px; }
button { background: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #0056b3; }
</style>
</head>
<body>
<div class="container">
<div class="nav">
<a href="/">Dashboard</a> |
<a href="/admin/users">Users</a> |
<a href="/admin/config">Config</a> |
<a href="/logout">Logout</a>
</div>
<h1>System Configuration</h1>
<form method="post">
<label>Analysis Backend:</label>
<select name="analysis_backend">
<option value="cuda" {% if settings.analysis_backend == 'cuda' %}selected{% endif %}>CUDA</option>
<option value="rocm" {% if settings.analysis_backend == 'rocm' %}selected{% endif %}>ROCm</option>
</select>
<label>Training Backend:</label>
<select name="training_backend">
<option value="cuda" {% if settings.training_backend == 'cuda' %}selected{% endif %}>CUDA</option>
<option value="rocm" {% if settings.training_backend == 'rocm' %}selected{% endif %}>ROCm</option>
</select>
<label>Communication Type:</label>
<select name="comm_type">
<option value="unix" {% if settings.comm_type == 'unix' %}selected{% endif %}>Unix Socket</option>
<option value="tcp" {% if settings.comm_type == 'tcp' %}selected{% endif %}>TCP Socket</option>
</select>
<label>Max Concurrent Jobs:</label>
<input type="number" name="max_concurrent_jobs" value="{{ settings.max_concurrent_jobs }}" min="1" max="10">
<label>Default Model:</label>
<input type="text" name="default_model" value="{{ settings.default_model }}">
<label>Frame Interval:</label>
<input type="number" name="frame_interval" value="{{ settings.frame_interval }}" min="1">
<button type="submit">Save Configuration</button>
</form>
</div>
</body>
</html>
'''
return render_template_string(html, settings=settings)
# API Routes
@app.route('/api/analyze', methods=['POST'])
@api_auth()
def api_analyze():
user = request.user
data = request.get_json()
job_data = {
'model_path': data.get('model_path', 'Qwen/Qwen2.5-VL-7B-Instruct'),
'prompt': data.get('prompt', 'Describe this image.'),
'local_path': data.get('file_path'),
'user_id': user['id']
}
queue_id = queue_manager.submit_job(user['id'], 'analyze', job_data)
return jsonify({'queue_id': queue_id, 'status': 'queued'})
@app.route('/api/train', methods=['POST'])
@api_auth()
def api_train():
user = request.user
data = request.get_json()
job_data = {
'output_model': data.get('output_model', './VideoModel'),
'description': data.get('description', ''),
'train_dir': data.get('train_dir'),
'user_id': user['id']
}
queue_id = queue_manager.submit_job(user['id'], 'train', job_data)
return jsonify({'queue_id': queue_id, 'status': 'queued'})
@app.route('/api/queue/<int:queue_id>', methods=['GET'])
@api_auth()
def api_queue_status(queue_id):
user = request.user
job = queue_manager.get_job_status(queue_id)
if not job or job['user_id'] != user['id']:
return jsonify({'error': 'Job not found'}), 404
return jsonify(job)
@app.route('/api/queue', methods=['GET'])
@api_auth()
def api_user_queue():
user = request.user
jobs = queue_manager.get_user_jobs(user['id'])
return jsonify(jobs)
@app.route('/api/tokens', methods=['GET', 'POST'])
@api_auth()
def api_tokens():
user = request.user
if request.method == 'POST':
token = create_api_token(user['id'])
return jsonify({'token': token})
tokens = get_user_api_tokens(user['id'])
return jsonify(tokens)
@app.route('/api')
@require_login()
def api_docs():
session_id = get_session_id()
user = get_current_user(session_id)
html = '''
<!DOCTYPE html>
<html>
<head>
<title>API Documentation - Video AI</title>
<style>
body { font-family: Arial, sans-serif; background: #f4f4f4; margin: 0; padding: 20px; }
.container { max-width: 1000px; margin: auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h1, h2 { color: #333; }
.nav { margin-bottom: 20px; }
.nav a { text-decoration: none; color: #007bff; margin-right: 15px; }
pre { background: #f8f9fa; padding: 15px; border-radius: 4px; overflow-x: auto; }
code { background: #f8f9fa; padding: 2px 4px; border-radius: 2px; }
.endpoint { margin: 20px 0; padding: 15px; border-left: 4px solid #007bff; background: #f8f9fa; }
</style>
</head>
<body>
<div class="container">
<div class="nav">
<a href="/">Dashboard</a> |
<a href="/api">API</a> |
<a href="/logout">Logout</a>
</div>
<h1>Video AI REST API</h1>
<h2>Authentication</h2>
<p>Use Bearer token authentication:</p>
<pre>Authorization: Bearer YOUR_API_TOKEN</pre>
<h2>Get API Token</h2>
<div class="endpoint">
<strong>POST /api/tokens</strong>
<p>Generate a new API token</p>
</div>
<h2>Analysis</h2>
<div class="endpoint">
<strong>POST /api/analyze</strong>
<pre>{
"model_path": "Qwen/Qwen2.5-VL-7B-Instruct",
"prompt": "Describe this image.",
"file_path": "/path/to/image.jpg"
}</pre>
</div>
<h2>Training</h2>
<div class="endpoint">
<strong>POST /api/train</strong>
<pre>{
"output_model": "./MyModel",
"description": "Custom training",
"train_dir": "/path/to/training/data"
}</pre>
</div>
<h2>Queue Status</h2>
<div class="endpoint">
<strong>GET /api/queue/<queue_id></strong>
<p>Get job status by ID</p>
</div>
<div class="endpoint">
<strong>GET /api/queue</strong>
<p>Get all user jobs</p>
</div>
</div>
</body>
</html>
'''
return render_template_string(html)
@app.route('/static/<path:filename>')
def serve_static(filename):
return send_from_directory('static', filename)
if __name__ == "__main__":
app.run(host='0.0.0.0', debug=True)
@app.route('/', methods=['GET', 'POST'])
def index():
......
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