Files
video-edit/app.py
Mike Johnston 640582df33
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m12s
previews and back to edit button
2026-02-09 21:53:52 -05:00

252 lines
8.7 KiB
Python

import os
import subprocess
import uuid
from flask import Flask, render_template, request, send_file, jsonify
from werkzeug.utils import secure_filename
import json
import time
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['OUTPUT_FOLDER'] = 'outputs'
app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500MB max file size
# Create necessary folders
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
os.makedirs(app.config['OUTPUT_FOLDER'], exist_ok=True)
# Store original filenames
original_filenames = {}
ALLOWED_EXTENSIONS = {'mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv', 'webm'}
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def get_video_info(video_path):
"""Get video information using ffprobe"""
try:
cmd = [
'ffprobe',
'-v', 'quiet',
'-print_format', 'json',
'-show_format',
'-show_streams',
video_path
]
result = subprocess.run(cmd, capture_output=True, text=True)
info = json.loads(result.stdout)
# Extract video stream info
video_stream = next((s for s in info['streams'] if s['codec_type'] == 'video'), None)
if video_stream:
return {
'width': video_stream.get('width'),
'height': video_stream.get('height'),
'duration': float(info['format'].get('duration', 0)),
'codec': video_stream.get('codec_name')
}
return None
except Exception as e:
print(f"Error getting video info: {e}")
return None
@app.route('/')
def index():
return render_template('index.html')
@app.route('/upload', methods=['POST'])
def upload_video():
if 'video' not in request.files:
return jsonify({'error': 'No video file provided'}), 400
file = request.files['video']
if file.filename == '':
return jsonify({'error': 'No file selected'}), 400
if file and allowed_file(file.filename):
# Generate unique filename
file_id = str(uuid.uuid4())
ext = file.filename.rsplit('.', 1)[1].lower()
filename = f"{file_id}.{ext}"
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
# Get video information
video_info = get_video_info(filepath)
# Store original filename for later use
original_filenames[file_id] = file.filename
return jsonify({
'file_id': file_id,
'filename': filename,
'original_name': file.filename,
'info': video_info
})
return jsonify({'error': 'Invalid file type'}), 400
@app.route('/process', methods=['POST'])
def process_video():
try:
data = request.get_json()
file_id = data.get('file_id')
if not file_id:
return jsonify({'error': 'No file ID provided'}), 400
# Find the uploaded file
uploaded_files = [f for f in os.listdir(app.config['UPLOAD_FOLDER']) if f.startswith(file_id)]
if not uploaded_files:
return jsonify({'error': 'File not found'}), 404
input_path = os.path.join(app.config['UPLOAD_FOLDER'], uploaded_files[0])
output_filename = f"{file_id}_processed.mp4"
output_path = os.path.join(app.config['OUTPUT_FOLDER'], output_filename)
# Build ffmpeg command
cmd = ['ffmpeg', '-i', input_path]
# Add trim parameters if specified
start_time = data.get('start_time')
end_time = data.get('end_time')
if start_time is not None and start_time > 0:
cmd.extend(['-ss', str(start_time)])
if end_time is not None:
duration = end_time - (start_time if start_time else 0)
if duration > 0:
cmd.extend(['-t', str(duration)])
# Build filter complex for crop and scale
filters = []
# Add rotation filter if specified
rotation = data.get('rotation', 0)
if rotation == 90:
filters.append('transpose=1')
elif rotation == 180:
filters.append('transpose=2,transpose=2')
elif rotation == 270:
filters.append('transpose=2')
# Add crop filter if specified
crop = data.get('crop')
if crop:
x = crop.get('x', 0)
y = crop.get('y', 0)
width = crop.get('width')
height = crop.get('height')
if width and height:
filters.append(f"crop={width}:{height}:{x}:{y}")
# Add scale filter if scale percentage specified (not 100%)
scale_percentage = data.get('scale_percentage', 100)
if scale_percentage != 100:
# Scale based on percentage - ffmpeg will apply this after crop
# Force dimensions divisible by 2 for H.264 compatibility
scale_filter = f"scale=trunc(iw*{scale_percentage/100}/2)*2:trunc(ih*{scale_percentage/100}/2)*2"
filters.append(scale_filter)
# Apply filters if any
if filters:
cmd.extend(['-vf', ','.join(filters)])
# Handle audio
mute_audio = data.get('mute_audio', False)
if mute_audio:
cmd.extend(['-an']) # Remove audio
# Get quality (CRF value)
quality = data.get('quality', 23)
# Ensure quality is within valid range (18-50)
quality = max(18, min(50, int(quality)))
# Output settings for H.264 MP4
cmd.extend([
'-c:v', 'libx264',
'-preset', 'medium',
'-crf', str(quality),
'-c:a', 'aac',
'-b:a', '128k',
'-movflags', '+faststart',
'-y', # Overwrite output file
output_path
])
# Execute ffmpeg
print(f"Executing: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"FFmpeg error: {result.stderr}")
return jsonify({'error': 'Video processing failed', 'details': result.stderr}), 500
return jsonify({
'success': True,
'output_file': output_filename,
'file_id': file_id
})
except Exception as e:
print(f"Processing error: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/download/<file_id>')
def download_video(file_id):
try:
output_filename = f"{file_id}_processed.mp4"
output_path = os.path.join(app.config['OUTPUT_FOLDER'], output_filename)
if not os.path.exists(output_path):
return jsonify({'error': 'File not found'}), 404
# Get original filename and create download name
original_name = original_filenames.get(file_id, 'video.mp4')
name_without_ext = os.path.splitext(original_name)[0]
timestamp = int(time.time() * 1000) % 100000 # Last 5 digits of timestamp
download_name = f"{name_without_ext}_processed_{timestamp}.mp4"
return send_file(output_path, as_attachment=True, download_name=download_name)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/preview/<file_id>')
def preview_video(file_id):
"""Serve processed video for inline preview"""
try:
output_filename = f"{file_id}_processed.mp4"
output_path = os.path.join(app.config['OUTPUT_FOLDER'], output_filename)
if not os.path.exists(output_path):
return jsonify({'error': 'File not found'}), 404
return send_file(output_path, mimetype='video/mp4')
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/cleanup/<file_id>', methods=['DELETE'])
def cleanup_files(file_id):
"""Clean up uploaded and processed files"""
try:
# Clean up upload folder
for f in os.listdir(app.config['UPLOAD_FOLDER']):
if f.startswith(file_id):
os.remove(os.path.join(app.config['UPLOAD_FOLDER'], f))
# Clean up output folder
for f in os.listdir(app.config['OUTPUT_FOLDER']):
if f.startswith(file_id):
os.remove(os.path.join(app.config['OUTPUT_FOLDER'], f))
# Clean up stored original filename
if file_id in original_filenames:
del original_filenames[file_id]
return jsonify({'success': True})
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)