Remove obsolete webui.py file

- Deleted webui.py as it was replaced by vidai/web.py in the multi-process architecture
- Functionality moved to separate web interface process
- Cleaned up legacy monolithic code that is no longer used
parent 467292ed
# Video AI Web Interface
# 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/>.
from flask import Flask, request, render_template, render_template_string, send_from_directory
import os
import sys
import torch
from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor
import tempfile
import subprocess
import shutil
import json
import time
import cv2
import threading
try:
import psutil
psutil_available = True
except ImportError:
psutil_available = False
print("psutil not available, install with: pip install psutil")
# Set PyTorch CUDA memory management
os.environ['PYTORCH_CUDA_ALLOC_CONF'] = 'expandable_segments:True'
try:
import pynvml
pynvml.nvmlInit()
nvml_available = True
except ImportError:
try:
import nvidia_ml_py as pynvml
pynvml.nvmlInit()
nvml_available = True
except ImportError:
nvml_available = False
print("NVIDIA ML library not available, install with: pip install nvidia-ml-py")
try:
import flash_attn
flash_attn_available = True
except ImportError:
flash_attn_available = False
print("flash_attn not available, install with: pip install flash-attn")
force_ffmpeg = '--ffmpeg' in sys.argv
optimize = '--optimize' in sys.argv
flash_attn = '--flash' in sys.argv
model_default = "Qwen/Qwen2.5-VL-7B-Instruct"
if '--model' in sys.argv:
idx = sys.argv.index('--model')
if idx + 1 < len(sys.argv):
model_default = sys.argv[idx + 1]
allowed_dir = None
if '--dir' in sys.argv:
idx = sys.argv.index('--dir')
if idx + 1 < len(sys.argv):
allowed_dir = os.path.abspath(sys.argv[idx + 1])
app = Flask(__name__)
os.makedirs('static', exist_ok=True)
status = "Idle"
start_time = 0
cancel = False
analysis_result = None
analysis_thread = None
model_cache = {}
processor_cache = {}
current_frame = 0
total_frames = 0
# System prompt
config_dir = os.path.expanduser("~/.config/AIVideo")
os.makedirs(config_dir, exist_ok=True)
system_prompt_file = os.path.join(config_dir, "system_prompt.txt")
if os.path.exists(system_prompt_file):
with open(system_prompt_file, "r") as f:
system_prompt = f.read()
else:
system_prompt = "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"
# GPU delegation
gpu_mem = []
if torch.cuda.is_available():
for i in range(torch.cuda.device_count()):
gpu_mem.append(torch.cuda.get_device_properties(i).total_memory)
max_gpu = gpu_mem.index(max(gpu_mem)) if gpu_mem else 0
min_gpu = gpu_mem.index(min(gpu_mem)) if gpu_mem else 0
else:
max_gpu = min_gpu = 0
# Set OpenCV to smaller GPU if available
try:
if cv2 and hasattr(cv2, 'cuda'):
cv2.cuda.setDevice(min_gpu)
except:
pass
def extract_frames(video_path, interval=10):
global optimize
if cv2 and not force_ffmpeg:
cap = cv2.VideoCapture(video_path)
fps = cap.get(cv2.CAP_PROP_FPS)
frame_interval = int(fps * interval)
frames = []
count = 0
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
if count % frame_interval == 0:
if optimize:
height, width = frame.shape[:2]
new_width = 640
new_height = int(height * new_width / width)
frame = cv2.resize(frame, (new_width, new_height))
temp_img = tempfile.NamedTemporaryFile(delete=False, suffix=".jpg")
cv2.imwrite(temp_img.name, frame)
frames.append((temp_img.name, count / fps))
count += 1
cap.release()
return frames, None
else:
output_dir = tempfile.mkdtemp()
vf = f"fps=1/{interval}"
if optimize:
vf += ",scale=640:-1"
cmd = ["ffmpeg", "-i", video_path, "-vf", vf, os.path.join(output_dir, "frame_%04d.jpg")]
subprocess.run(cmd, check=True, capture_output=True)
frames = []
for file in sorted(os.listdir(output_dir)):
if file.endswith('.jpg'):
path = os.path.join(output_dir, file)
frame_num = int(file.split('_')[1].split('.')[0])
ts = (frame_num - 1) * interval
frames.append((path, ts))
return frames, output_dir
def analyze_media_thread(media_path, prompt, model_path, interval=10):
global analysis_result, cancel, status
analysis_result = analyze_media(media_path, prompt, model_path, interval)
if cancel:
status = "Cancelled"
else:
status = "Completed"
def is_video(file_path):
return file_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv'))
def analyze_single_image(image_path, prompt, model, processor):
messages = [
{
"role": "user",
"content": [
{"type": "image", "image": image_path},
{"type": "text", "text": prompt},
],
}
]
inputs = processor.apply_chat_template(
messages, tokenize=True, add_generation_prompt=True, return_dict=True, return_tensors="pt"
)
inputs = {k: v.to(model.device) for k, v in inputs.items()}
generated_ids = model.generate(**inputs, max_new_tokens=128)
generated_ids_trimmed = [
out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs['input_ids'], generated_ids)
]
output_text = processor.batch_decode(
generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
)
return output_text[0]
def analyze_media(media_path, prompt, model_path, interval=10):
global status, start_time, model_cache, processor_cache, system_prompt, current_frame, total_frames
torch.cuda.empty_cache()
status = "Loading model"
start_time = time.time()
if model_path not in model_cache:
kwargs = {"device_map": "auto", "low_cpu_mem_usage": True}
if flash_attn:
if flash_attn_available:
kwargs["attn_implementation"] = "flash_attention_2"
kwargs["dtype"] = torch.float16
else:
print("Flash Attention 2 requested but flash_attn not installed. Install with: pip install flash-attn")
if os.path.exists(model_path):
try:
model = Qwen2_5_VLForConditionalGeneration.from_pretrained(model_path, **kwargs)
proc_path = model_path
except:
model = Qwen2_5_VLForConditionalGeneration.from_pretrained("Qwen/Qwen2.5-VL-7B-Instruct", **kwargs)
proc_path = "Qwen/Qwen2.5-VL-7B-Instruct"
else:
model = Qwen2_5_VLForConditionalGeneration.from_pretrained("Qwen/Qwen2.5-VL-7B-Instruct", **kwargs)
proc_path = "Qwen/Qwen2.5-VL-7B-Instruct"
model_cache[model_path] = model
processor_cache[model_path] = AutoProcessor.from_pretrained(proc_path)
else:
model = model_cache[model_path]
proc_path = model_path if os.path.exists(model_path) else "Qwen/Qwen2.5-VL-7B-Instruct"
processor = processor_cache[model_path]
status = "Model loaded"
full_prompt = system_prompt + " " + prompt if system_prompt else prompt
if is_video(media_path):
status = "Extracting frames"
frames, output_dir = extract_frames(media_path)
total_frames = len(frames)
status = f"Analyzing frames (0/{total_frames})"
descriptions = []
for i, (frame_path, ts) in enumerate(frames):
if cancel:
break
current_frame = i + 1
status = f"Analyzing frame {current_frame}/{total_frames}"
desc = analyze_single_image(frame_path, full_prompt, model, processor)
descriptions.append(f"At {ts:.2f}s: {desc}")
os.unlink(frame_path) # Remove frame after analysis to save disk space
if output_dir:
shutil.rmtree(output_dir)
status = "Summarizing video"
summary_prompt = f"Summarize the video based on frame descriptions, indicating scene changes and timestamps: {' '.join(descriptions)}"
messages = [{"role": "user", "content": [{"type": "text", "text": summary_prompt}]}]
inputs = processor.apply_chat_template(messages, tokenize=True, add_generation_prompt=True, return_dict=True, return_tensors="pt")
inputs = {k: v.to(model.device) for k, v in inputs.items()}
generated_ids = model.generate(**inputs, max_new_tokens=256)
generated_ids_trimmed = [out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs['input_ids'], generated_ids)]
output_text = processor.batch_decode(generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False)
summary = output_text[0]
result = f"Frame Descriptions:\n" + "\n".join(descriptions) + f"\n\nSummary:\n{summary}"
if len(descriptions) > 5:
mid_ts = frames[len(frames)//2][1]
cmd1 = ["ffmpeg", "-y", "-i", media_path, "-ss", "0", "-t", str(mid_ts), "-c", "copy", "static/segment1.mp4"]
subprocess.run(cmd1, check=True)
cmd2 = ["ffmpeg", "-y", "-i", media_path, "-ss", str(mid_ts), "-c", "copy", "static/segment2.mp4"]
subprocess.run(cmd2, check=True)
result += f"\nVideo split into segments: <a href='/static/segment1.mp4'>Segment 1</a>, <a href='/static/segment2.mp4'>Segment 2</a>"
status = "Idle"
return result
else:
result = analyze_single_image(media_path, full_prompt, model, processor)
torch.cuda.empty_cache()
status = "Idle"
current_frame = 0
total_frames = 0
return result
def list_files(directory):
files = []
for root, dirs, filenames in os.walk(directory):
for filename in filenames:
files.append(os.path.join(root, filename))
return files
@app.route('/', methods=['GET', 'POST'])
def index():
global system_prompt, analysis_result
result = None
model_path_default = "./VideoModel" if os.path.exists("./VideoModel") else model_default
if analysis_result is not None:
result = analysis_result
analysis_result = None
if request.method == 'POST':
model_path = request.form.get('model_path', model_path_default)
prompt = request.form.get('prompt', 'Describe this image.')
uploaded_file = request.files.get('file')
local_path = request.form.get('local_path')
upload_id = request.form.get('upload_id')
if upload_id:
file_name = request.form.get('file_name', 'uploaded_file')
media_path = os.path.join(tempfile.gettempdir(), f"{upload_id}_{file_name}")
elif uploaded_file and uploaded_file.filename:
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 and os.path.exists(local_path):
if allowed_dir and not os.path.abspath(local_path).startswith(allowed_dir):
result = "Access denied to file"
else:
media_path = local_path
else:
result = "Provide a file or path"
if 'media_path' in locals():
interval = int(request.form.get('interval', 10))
global analysis_thread, cancel
cancel = False
analysis_result = None
analysis_thread = threading.Thread(target=analyze_media_thread, args=(media_path, prompt, model_path, interval))
analysis_thread.start()
result = "Analysis started. Check status for progress."
if uploaded_file:
# Keep file for thread
pass
html = '''
<!DOCTYPE html>
<html>
<head>
<title>VideoModel AI</title>
<style>
body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 20px; display: flex; justify-content: center; align-items: flex-start; }
.main { flex: 1; max-width: 800px; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); margin-right: 20px; }
.sidebar { width: 300px; 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 { text-align: center; margin-bottom: 20px; }
nav a { margin: 0 10px; text-decoration: none; color: #007bff; }
form { margin-bottom: 20px; }
label { display: block; margin-bottom: 5px; }
input[type="text"], input[type="file"], textarea { 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; }
input[type="submit"]:hover { background: #0056b3; }
.result { background: #e9ecef; padding: 10px; border-radius: 4px; }
.stats { font-size: 14px; }
</style>
<script>
function openFileBrowser() {
window.open('/files', 'filebrowser', 'width=600,height=400');
}
async function updateStats() {
try {
const response = await fetch('/stats');
const data = await response.json();
let html = '<h3>GPU Stats</h3>';
html += `<p style="color: ${data.status === 'Idle' ? 'green' : 'orange'};">Status: ${data.status}</p>`;
if (data.elapsed > 0) {
html += `<p>Elapsed: ${data.elapsed.toFixed(1)}s</p>`;
}
if (data.gpu_count > 0) {
data.gpus.forEach((gpu, i) => {
let memPercent = (gpu.memory_used / gpu.memory_total * 100).toFixed(1);
html += `<p>GPU ${i}: ${gpu.name}<br>Memory: <progress value="${gpu.memory_used}" max="${gpu.memory_total}"></progress> ${gpu.memory_used.toFixed(2)} / ${gpu.memory_total.toFixed(2)} GB (${memPercent}%)<br>Utilization: ${gpu.utilization}%</p>`;
});
} else {
html += '<p>No GPUs detected</p>';
}
html += `<p>CPU: ${data.cpu_percent.toFixed(1)}%</p>`;
html += `<p>RAM: ${data.ram_used.toFixed(2)} / ${data.ram_total.toFixed(2)} GB</p>`;
document.getElementById('stats').innerHTML = html;
if (data.result) {
document.getElementById('result_div').innerHTML = '<h3>Result:</h3><p>' + data.result + '</p>';
document.getElementById('result_div').style.display = 'block';
}
} catch (e) {
document.getElementById('stats').innerHTML = '<p>Error loading stats</p>';
}
}
setInterval(updateStats, 5000);
window.onload = updateStats;
function cancelAnalysis() {
fetch('/cancel', {method: 'POST'}).then(() => updateStats());
}
// Upload progress
const fileInput = document.getElementById('fileInput');
if (fileInput) {
fileInput.addEventListener('change', function() {
const file = this.files[0];
if (file) {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('file', file);
const startTime = Date.now();
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
document.getElementById('uploadProgress').value = percent;
document.getElementById('uploadProgress').style.display = 'block';
const speed = e.loaded / ((Date.now() - startTime) / 1000);
const remaining = (e.total - e.loaded) / speed;
document.getElementById('progressText').innerText = `Uploaded ${(e.loaded / 1024 / 1024).toFixed(2)} MB of ${(e.total / 1024 / 1024).toFixed(2)} MB (${percent.toFixed(1)}%) - Speed: ${(speed / 1024 / 1024).toFixed(2)} MB/s - ETA: ${Math.round(remaining)}s`;
}
});
xhr.addEventListener('load', function() {
document.getElementById('progressText').innerText = 'Upload complete';
});
xhr.open('POST', '/upload_progress');
xhr.send(formData);
}
});
}
</script>
</head>
<body>
<div class="main">
<h1>VideoModel AI Web Interface</h1>
<nav>
<a href="/">Analysis</a> | <a href="/train">Training</a>
</nav>
<h2>Analyze Image/Video</h2>
<form method="post" enctype="multipart/form-data">
<label>Model Path: <input type="text" name="model_path" value="{{ model_path_default }}"></label>
<p><a href="/system">Edit System Prompt</a></p>
<label>Upload File: <input type="file" name="file" accept="image/*,video/*" id="fileInput"></label>
<progress id="uploadProgress" value="0" max="100" style="display:none; width:100%;"></progress>
<div id="progressText"></div>
{% if allowed_dir %}
<label>Or Local Path: <input type="text" name="local_path" id="local_path"> <button type="button" onclick="openFileBrowser()">Browse</button></label>
{% endif %}
<label>Prompt: <textarea name="prompt" rows="5" cols="80">Describe this image.</textarea></label>
<input type="submit" value="Analyze">
<button type="button" onclick="cancelAnalysis()">Cancel Analysis</button>
</form>
<div class="result" id="result_div" style="display:none;"></div>
{% if result %}
<div class="result">
<h3>Result:</h3>
<p>{{ result }}</p>
</div>
{% endif %}
</div>
<div class="sidebar">
<div id="stats" class="stats">Loading stats...</div>
</div>
</body>
</html>
'''
html = '''
<!DOCTYPE html>
<html>
<head>
<title>VideoModel AI</title>
<style>
body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 20px; display: flex; justify-content: center; align-items: flex-start; }
.main { flex: 1; max-width: 800px; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); margin-right: 20px; }
.sidebar { width: 300px; 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 { text-align: center; margin-bottom: 20px; }
nav a { margin: 0 10px; text-decoration: none; color: #007bff; }
form { margin-bottom: 20px; }
label { display: block; margin-bottom: 5px; }
input[type="text"], input[type="file"], textarea { 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; }
input[type="submit"]:hover { background: #0056b3; }
.result { background: #e9ecef; padding: 10px; border-radius: 4px; }
.stats { font-size: 14px; }
</style>
</head>
<body>
<div class="main">
<h1>VideoModel AI Web Interface</h1>
<nav>
<a href="/">Analysis</a> | <a href="/train">Training</a>
</nav>
<h2>Analyze Image/Video</h2>
<form method="post" enctype="multipart/form-data">
<label>Model Path: <input type="text" name="model_path" value="{{ model_path_default }}"></label>
<p><a href="/system">Edit System Prompt</a></p>
<label>Upload File: <input type="file" name="file" accept="image/*,video/*" id="fileInput"></label>
<progress id="uploadProgress" value="0" max="100" style="display:none; width:100%;"></progress>
<div id="progressText"></div>
{% if allowed_dir %}
<label>Or Local Path: <input type="text" name="local_path" id="local_path"> <button type="button" onclick="openFileBrowser()">Browse</button></label>
{% endif %}
<label>Frame Interval (seconds): <input type="number" name="interval" value="10" min="1"></label>
<label>Prompt: <textarea name="prompt" rows="5" cols="80">Describe this image.</textarea></label>
<input type="submit" value="Analyze">
<button type="button" onclick="cancelAnalysis()">Cancel Analysis</button>
</form>
<div class="result" id="result_div" style="display:none;"></div>
{% if result %}
<div class="result">
<h3>Result:</h3>
<p>{{ result }}</p>
</div>
{% endif %}
</div>
<div class="sidebar">
<div id="stats" class="stats">Loading stats...</div>
</div>
<script>
function openFileBrowser() {
window.open('/files', 'filebrowser', 'width=600,height=400');
}
async function updateStats() {
try {
const response = await fetch('/stats');
const data = await response.json();
let html = '<h3>GPU Stats</h3>';
html += `<p style="color: ${data.status === 'Idle' ? 'green' : 'orange'};">Status: ${data.status}</p>`;
if (data.elapsed > 0) {
html += `<p>Elapsed: ${data.elapsed.toFixed(1)}s</p>`;
}
if (data.gpu_count > 0) {
data.gpus.forEach((gpu, i) => {
let memPercent = (gpu.memory_used / gpu.memory_total * 100).toFixed(1);
html += `<p>GPU ${i}: ${gpu.name}<br>Memory: <progress value="${gpu.memory_used}" max="${gpu.memory_total}"></progress> ${gpu.memory_used.toFixed(2)} / ${gpu.memory_total.toFixed(2)} GB (${memPercent}%)<br>Utilization: ${gpu.utilization}%</p>`;
});
} else {
html += '<p>No GPUs detected</p>';
}
html += `<p>CPU: ${data.cpu_percent.toFixed(1)}%</p>`;
html += `<p>RAM: ${data.ram_used.toFixed(2)} / ${data.ram_total.toFixed(2)} GB</p>`;
document.getElementById('stats').innerHTML = html;
if (data.result) {
document.getElementById('result_div').innerHTML = '<h3>Result:</h3><p>' + data.result + '</p>';
document.getElementById('result_div').style.display = 'block';
}
} catch (e) {
document.getElementById('stats').innerHTML = '<p>Error loading stats</p>';
}
}
setInterval(updateStats, 5000);
window.onload = updateStats;
function cancelAnalysis() {
fetch('/cancel', {method: 'POST'}).then(() => updateStats());
}
// Upload progress with chunked upload
const form = document.querySelector('form');
if (form) {
form.addEventListener('submit', async function(e) {
e.preventDefault();
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
// Submit form normally if no file
const formData = new FormData(this);
const xhr = new XMLHttpRequest();
xhr.addEventListener('load', function() {
window.location.reload();
});
xhr.open('POST', '/');
xhr.send(formData);
return;
}
const chunkSize = 1024 * 1024; // 1MB
const totalChunks = Math.ceil(file.size / chunkSize);
const uploadId = Date.now().toString();
const concurrency = 1;
let chunksSent = 0;
async function sendChunk(index) {
const start = index * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('chunk_index', index);
formData.append('total_chunks', totalChunks);
formData.append('file_name', file.name);
formData.append('upload_id', uploadId);
return new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
const percent = ((chunksSent * chunkSize + e.loaded) / file.size) * 100;
document.getElementById('uploadProgress').value = percent;
document.getElementById('uploadProgress').style.display = 'block';
const speed = (chunksSent * chunkSize + e.loaded) / ((Date.now() - startTime) / 1000);
const remaining = (file.size - (chunksSent * chunkSize + e.loaded)) / speed;
document.getElementById('progressText').innerText = `Uploaded ${((chunksSent * chunkSize + e.loaded) / 1024 / 1024).toFixed(2)} MB of ${(file.size / 1024 / 1024).toFixed(2)} MB (${percent.toFixed(1)}%) - Speed: ${(speed / 1024 / 1024).toFixed(2)} MB/s - ETA: ${Math.round(remaining)}s`;
}
});
xhr.addEventListener('load', function() {
chunksSent++;
resolve();
});
xhr.open('POST', '/upload_chunk');
xhr.send(formData);
});
}
const startTime = Date.now();
for (let i = 0; i < totalChunks; i += concurrency) {
const promises = [];
for (let j = 0; j < concurrency && i + j < totalChunks; j++) {
promises.push(sendChunk(i + j));
}
await Promise.all(promises);
}
// All chunks sent, reassembling
document.getElementById('progressText').innerText = 'Reassembling file...';
document.getElementById('uploadProgress').value = 100;
// Submit form
const formData2 = new FormData(form);
formData2.append('upload_id', uploadId);
formData2.append('file_name', file.name);
const xhr2 = new XMLHttpRequest();
xhr2.addEventListener('load', function() {
window.location.reload();
});
xhr2.open('POST', '/');
xhr2.send(formData2);
});
}
</script>
</body>
</html>
'''
return render_template_string(html, result=result, model_path_default=model_path_default, allowed_dir=allowed_dir)
@app.route('/train', methods=['GET', 'POST'])
def train():
model_path_default = "./VideoModel" if os.path.exists("./VideoModel") else model_default
message = None
if request.method == 'POST':
uploaded_data = request.files.get('data')
train_dir = request.form.get('train_dir')
description = request.form.get('description')
output_model = request.form.get('output_model', './VideoModel')
if uploaded_data and uploaded_data.filename:
if is_video(uploaded_data.filename):
with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(uploaded_data.filename)[1]) as tmp:
tmp.write(uploaded_data.read())
video_path = tmp.name
train_path = tempfile.mkdtemp()
frames, output_dir = extract_frames(video_path)
for frame_path, ts in frames:
shutil.move(frame_path, os.path.join(train_path, f"frame_{ts:.2f}.jpg"))
if output_dir:
shutil.rmtree(output_dir)
os.unlink(video_path)
else:
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
elif train_dir and os.path.isdir(train_dir):
if allowed_dir and not os.path.abspath(train_dir).startswith(allowed_dir):
message = "Access denied to directory"
else:
train_path = train_dir
else:
message = "Provide training data"
if 'train_path' in locals():
desc_file = os.path.join(train_path, "description.txt")
with open(desc_file, "w") as f:
f.write(description)
status = "Training"
cmd = ["python", "videotrain", train_path, "--output_dir", output_model]
result = subprocess.run(cmd, capture_output=True, text=True)
status = "Idle"
if result.returncode == 0:
message = "Training completed!"
else:
message = f"Training failed: {result.stderr}"
html = '''
<!DOCTYPE html>
<html>
<head>
<title>VideoModel AI - Training</title>
<style>
body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 20px; display: flex; justify-content: center; align-items: flex-start; }
.main { flex: 1; max-width: 800px; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); margin-right: 20px; }
.sidebar { width: 300px; 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 { text-align: center; margin-bottom: 20px; }
nav a { margin: 0 10px; text-decoration: none; color: #007bff; }
form { margin-bottom: 20px; }
label { display: block; margin-bottom: 5px; }
input[type="text"], input[type="file"], textarea { 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; }
input[type="submit"]:hover { background: #0056b3; }
.message { background: #e9ecef; padding: 10px; border-radius: 4px; }
.stats { font-size: 14px; }
</style>
<script>
async function updateStats() {
try {
const response = await fetch('/stats');
const data = await response.json();
let html = '<h3>GPU Stats</h3>';
html += `<p style="color: ${data.status === 'Idle' ? 'green' : 'orange'};">Status: ${data.status}</p>`;
if (data.gpu_count > 0) {
data.gpus.forEach((gpu, i) => {
let memPercent = (gpu.memory_used / gpu.memory_total * 100).toFixed(1);
html += `<p>GPU ${i}: ${gpu.name}<br>Memory: <progress value="${gpu.memory_used}" max="${gpu.memory_total}"></progress> ${gpu.memory_used.toFixed(2)} / ${gpu.memory_total.toFixed(2)} GB (${memPercent}%)<br>Utilization: ${gpu.utilization}%</p>`;
});
} else {
html += '<p>No GPUs detected</p>';
}
html += `<p>CPU: ${data.cpu_percent.toFixed(1)}%</p>`;
html += `<p>RAM: ${data.ram_used.toFixed(2)} / ${data.ram_total.toFixed(2)} GB</p>`;
document.getElementById('stats').innerHTML = html;
if (data.result) {
document.getElementById('result_div').innerHTML = '<h3>Result:</h3><p>' + data.result + '</p>';
document.getElementById('result_div').style.display = 'block';
}
} catch (e) {
document.getElementById('stats').innerHTML = '<p>Error loading stats</p>';
}
}
setInterval(updateStats, 5000);
window.onload = updateStats;
</script>
</head>
<body>
<div class="main">
<h1>VideoModel AI Web Interface</h1>
<nav>
<a href="/">Analysis</a> | <a href="/train">Training</a>
</nav>
<h2>Train Model</h2>
<form method="post" enctype="multipart/form-data">
<label>Model Path: <input type="text" name="model_path" value="{{ model_path_default }}"></label>
<label>Upload Data (ZIP or Video): <input type="file" name="data" accept=".zip,.mp4,.avi"></label>
{% if allowed_dir %}
<label>Or Directory Path: <input type="text" name="train_dir"></label>
{% endif %}
<label>Description: <textarea name="description"></textarea></label>
<label>Output Model Path: <input type="text" name="output_model" value="./VideoModel"></label>
<input type="submit" value="Start Training">
</form>
<div class="message" id="result_div" style="display:none;"></div>
{% if message %}
<div class="message">
<p>{{ message }}</p>
</div>
{% endif %}
</div>
<div class="sidebar">
<div id="stats" class="stats">Loading stats...</div>
</div>
</body>
</html>
'''
return render_template_string(html, message=message, allowed_dir=allowed_dir, model_path_default=model_path_default)
@app.route('/cancel', methods=['POST'])
def cancel_analysis():
global cancel, status
cancel = True
status = "Cancelled"
return 'Analysis cancelled'
@app.route('/upload_chunk', methods=['POST'])
def upload_chunk():
chunk = request.files['chunk']
chunk_index = int(request.form['chunk_index'])
total_chunks = int(request.form['total_chunks'])
file_name = request.form['file_name']
upload_id = request.form['upload_id']
temp_dir = os.path.join(tempfile.gettempdir(), upload_id)
os.makedirs(temp_dir, exist_ok=True)
chunk_path = os.path.join(temp_dir, str(chunk_index))
chunk.save(chunk_path)
if len(os.listdir(temp_dir)) == total_chunks:
final_path = os.path.join(tempfile.gettempdir(), f"{upload_id}_{file_name}")
with open(final_path, 'wb') as f:
for i in range(total_chunks):
with open(os.path.join(temp_dir, str(i)), 'rb') as cf:
f.write(cf.read())
os.unlink(os.path.join(temp_dir, str(i)))
os.rmdir(temp_dir)
return 'OK'
@app.route('/system', methods=['GET', 'POST'])
def system_page():
global system_prompt
if request.method == 'POST':
system_prompt = request.form.get('system_prompt', '')
with open(system_prompt_file, "w") as f:
f.write(system_prompt)
html = f'''
<!DOCTYPE html>
<html>
<head>
<title>System Prompt</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; }}
textarea {{ width: 100%; height: 200px; }}
input[type="submit"] {{ background: #007bff; color: white; padding: 10px; border: none; border-radius: 4px; cursor: pointer; }}
input[type="submit"]:hover {{ background: #0056b3; }}
</style>
</head>
<body>
<div class="container">
<h1>Edit System Prompt</h1>
<form method="post">
<textarea name="system_prompt">{system_prompt}</textarea><br>
<input type="submit" value="Save">
</form>
<a href="/">Back to Analysis</a>
</div>
</body>
</html>
'''
return html
@app.route('/static/<path:filename>')
def serve_static(filename):
return send_from_directory('static', filename)
@app.route('/stats')
def stats():
global status, start_time, analysis_result
data = {'status': status}
if status != "Idle":
data['elapsed'] = time.time() - start_time
else:
data['elapsed'] = 0
data['result'] = analysis_result.replace('\n', '<br>') if analysis_result else ''
if psutil_available:
data['cpu_percent'] = psutil.cpu_percent()
ram = psutil.virtual_memory()
data['ram_used'] = ram.used / 1024**3
data['ram_total'] = ram.total / 1024**3
else:
data['cpu_percent'] = 0
data['ram_used'] = 0
data['ram_total'] = 0
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,
}
if nvml_available:
try:
handle = pynvml.nvmlDeviceGetHandleByIndex(i)
util = pynvml.nvmlDeviceGetHandleByIndex(i)
util = pynvml.nvmlDeviceGetUtilizationRates(handle)
gpu['utilization'] = util.gpu
except:
gpu['utilization'] = 0
data['gpus'].append(gpu)
else:
data['gpu_count'] = 0
return json.dumps(data)
@app.route('/files')
def files():
if not allowed_dir:
return "File browsing not enabled", 403
path = request.args.get('path', allowed_dir)
if not os.path.abspath(path).startswith(allowed_dir):
return "Access denied", 403
try:
items = os.listdir(path)
dirs = [os.path.join(path, i) for i in items if os.path.isdir(os.path.join(path, i))]
files_list = [os.path.join(path, i) for i in items if os.path.isfile(os.path.join(path, i))]
except:
dirs = []
files_list = []
html = f'''
<!DOCTYPE html>
<html>
<head>
<title>File Browser</title>
<style>
body {{ font-family: Arial, sans-serif; }}
ul {{ list-style: none; }}
li {{ margin: 5px 0; }}
a {{ text-decoration: none; color: #007bff; cursor: pointer; }}
a:hover {{ text-decoration: underline; }}
</style>
</head>
<body>
<h2>Files in {path}</h2>
<ul>
'''
for d in dirs:
html += f"<li><a href='/files?path={d}'>{os.path.basename(d)}/</a></li>"
for f in files_list:
html += f"<li><a onclick=\"window.opener.document.getElementById('local_path').value='{f}'; window.close();\">{os.path.basename(f)}</a></li>"
html += '''
</ul>
</body>
</html>
'''
return html
if __name__ == "__main__":
app.run(host='0.0.0.0', debug=True)
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