You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
RedditVideoMakerBot/progress_gui.py

560 lines
20 KiB

#!/usr/bin/env python
"""
Reddit Video Maker Bot - Web UI
Complete web interface for configuration, video generation, and progress tracking.
"""
import os
import json
import subprocess
import threading
import shutil
from pathlib import Path
from datetime import datetime
import toml
import requests
from flask import Flask, render_template, send_from_directory, jsonify, request, redirect, url_for
from flask_socketio import SocketIO, emit
from werkzeug.utils import secure_filename
from utils.progress import progress_tracker
# Configuration
HOST = "0.0.0.0"
PORT = 5000
CONFIG_PATH = Path("config.toml")
BACKGROUNDS_VIDEO_PATH = Path("assets/backgrounds/video")
BACKGROUNDS_AUDIO_PATH = Path("assets/backgrounds/audio")
RESULTS_PATH = Path("results")
ALLOWED_VIDEO_EXTENSIONS = {'mp4', 'webm', 'mov', 'avi', 'mkv'}
ALLOWED_AUDIO_EXTENSIONS = {'mp3', 'wav', 'ogg', 'm4a'}
# Ensure directories exist
BACKGROUNDS_VIDEO_PATH.mkdir(parents=True, exist_ok=True)
BACKGROUNDS_AUDIO_PATH.mkdir(parents=True, exist_ok=True)
RESULTS_PATH.mkdir(parents=True, exist_ok=True)
# Configure Flask app
app = Flask(__name__, template_folder="GUI", static_folder="GUI/static")
app.secret_key = os.urandom(24)
app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500MB max upload
# Configure SocketIO for real-time updates
socketio = SocketIO(app, cors_allowed_origins="*", async_mode="gevent")
# Track running generation process
generation_process = None
generation_thread = None
def allowed_video_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_VIDEO_EXTENSIONS
def allowed_audio_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_AUDIO_EXTENSIONS
def load_config():
"""Load current configuration."""
if CONFIG_PATH.exists():
try:
return toml.load(CONFIG_PATH)
except Exception as e:
print(f"Error loading config: {e}")
return {}
def save_config(config):
"""Save configuration to file."""
try:
with open(CONFIG_PATH, 'w') as f:
toml.dump(config, f)
return True
except Exception as e:
print(f"Error saving config: {e}")
return False
def get_background_videos():
"""Get list of available background videos."""
videos = []
if BACKGROUNDS_VIDEO_PATH.exists():
for f in BACKGROUNDS_VIDEO_PATH.iterdir():
if f.is_file() and allowed_video_file(f.name):
videos.append({
'name': f.stem,
'filename': f.name,
'size': f.stat().st_size,
'path': str(f)
})
return videos
def get_background_audios():
"""Get list of available background audio tracks."""
audios = []
if BACKGROUNDS_AUDIO_PATH.exists():
for f in BACKGROUNDS_AUDIO_PATH.iterdir():
if f.is_file() and allowed_audio_file(f.name):
audios.append({
'name': f.stem,
'filename': f.name,
'size': f.stat().st_size,
'path': str(f)
})
return audios
def get_generated_videos():
"""Get list of generated videos."""
videos = []
if RESULTS_PATH.exists():
for subreddit_dir in RESULTS_PATH.iterdir():
if subreddit_dir.is_dir():
for f in subreddit_dir.iterdir():
if f.is_file() and f.suffix.lower() == '.mp4':
videos.append({
'name': f.stem,
'filename': f.name,
'subreddit': subreddit_dir.name,
'size': f.stat().st_size,
'created': datetime.fromtimestamp(f.stat().st_mtime).isoformat(),
'path': f"/results/{subreddit_dir.name}/{f.name}"
})
return sorted(videos, key=lambda x: x['created'], reverse=True)
# Progress update callback
def broadcast_progress(data):
"""Broadcast progress updates to all connected clients."""
socketio.emit("progress_update", data, namespace="/progress")
# Register the callback
progress_tracker.add_update_callback(broadcast_progress)
@app.after_request
def after_request(response):
"""Ensure responses aren't cached."""
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
response.headers["Expires"] = 0
response.headers["Pragma"] = "no-cache"
return response
# ==================== Pages ====================
@app.route("/")
def index():
"""Main dashboard - redirect to settings if not configured."""
config = load_config()
if not config:
return redirect(url_for('settings_page'))
return render_template("dashboard.html")
@app.route("/settings")
def settings_page():
"""Settings/configuration page."""
return render_template("settings.html")
@app.route("/backgrounds")
def backgrounds_page():
"""Background videos management page."""
return render_template("backgrounds.html")
@app.route("/videos")
def videos_page():
"""Generated videos page."""
return render_template("videos.html")
@app.route("/progress")
def progress_page():
"""Progress tracking page."""
return render_template("progress.html")
# ==================== API Endpoints ====================
@app.route("/api/config", methods=["GET"])
def api_get_config():
"""Get current configuration."""
config = load_config()
# Mask sensitive fields
if config.get('settings', {}).get('tts', {}).get('qwen_password'):
config['settings']['tts']['qwen_password'] = '********'
if config.get('settings', {}).get('tts', {}).get('openai_api_key'):
config['settings']['tts']['openai_api_key'] = '********'
return jsonify(config)
@app.route("/api/config", methods=["POST"])
def api_save_config():
"""Save configuration."""
try:
data = request.json
# Load existing config to preserve masked fields
existing_config = load_config()
# Build new config
config = {
'reddit': {
'scraper': {
'user_agent': data.get('user_agent', 'python:reddit_video_bot:1.0'),
'request_delay': float(data.get('request_delay', 2.0))
},
'thread': {
'subreddit': data.get('subreddit', 'AskReddit'),
'post_id': data.get('post_id', ''),
'random': data.get('random', True),
'max_comment_length': int(data.get('max_comment_length', 500)),
'min_comment_length': int(data.get('min_comment_length', 1)),
'min_comments': int(data.get('min_comments', 20)),
'post_lang': data.get('post_lang', '')
}
},
'ai': {
'ai_similarity_enabled': data.get('ai_similarity_enabled', False),
'ai_similarity_keywords': data.get('ai_similarity_keywords', '')
},
'settings': {
'allow_nsfw': data.get('allow_nsfw', False),
'theme': data.get('theme', 'dark'),
'times_to_run': int(data.get('times_to_run', 1)),
'opacity': float(data.get('opacity', 0.9)),
'storymode': data.get('storymode', False),
'storymodemethod': int(data.get('storymodemethod', 1)),
'storymode_max_length': int(data.get('storymode_max_length', 1000)),
'resolution_w': int(data.get('resolution_w', 1080)),
'resolution_h': int(data.get('resolution_h', 1920)),
'zoom': float(data.get('zoom', 1)),
'channel_name': data.get('channel_name', 'Reddit Tales'),
'background': {
'background_video': data.get('background_video', 'minecraft'),
'background_audio': data.get('background_audio', 'lofi'),
'background_audio_volume': float(data.get('background_audio_volume', 0.15)),
'enable_extra_audio': data.get('enable_extra_audio', False),
'background_thumbnail': data.get('background_thumbnail', False)
},
'tts': {
'voice_choice': data.get('voice_choice', 'qwentts'),
'random_voice': data.get('random_voice', True),
'silence_duration': float(data.get('silence_duration', 0.3)),
'no_emojis': data.get('no_emojis', False),
'qwen_api_url': data.get('qwen_api_url', 'http://localhost:8080'),
'qwen_email': data.get('qwen_email', ''),
'qwen_speaker': data.get('qwen_speaker', 'Vivian'),
'qwen_language': data.get('qwen_language', 'English'),
'qwen_instruct': data.get('qwen_instruct', 'Warm, friendly, conversational.'),
'elevenlabs_voice_name': data.get('elevenlabs_voice_name', 'Bella'),
'elevenlabs_api_key': '',
'tiktok_voice': data.get('tiktok_voice', 'en_us_001'),
'tiktok_sessionid': data.get('tiktok_sessionid', ''),
'openai_api_url': data.get('openai_api_url', 'https://api.openai.com/v1/'),
'openai_voice_name': data.get('openai_voice_name', 'alloy'),
'openai_model': data.get('openai_model', 'tts-1'),
'aws_polly_voice': data.get('aws_polly_voice', 'Matthew'),
'streamlabs_polly_voice': data.get('streamlabs_polly_voice', 'Matthew')
}
}
}
# Preserve password if not changed
if data.get('qwen_password') and data['qwen_password'] != '********':
config['settings']['tts']['qwen_password'] = data['qwen_password']
elif existing_config.get('settings', {}).get('tts', {}).get('qwen_password'):
config['settings']['tts']['qwen_password'] = existing_config['settings']['tts']['qwen_password']
else:
config['settings']['tts']['qwen_password'] = ''
# Preserve API keys if not changed
if data.get('openai_api_key') and data['openai_api_key'] != '********':
config['settings']['tts']['openai_api_key'] = data['openai_api_key']
elif existing_config.get('settings', {}).get('tts', {}).get('openai_api_key'):
config['settings']['tts']['openai_api_key'] = existing_config['settings']['tts']['openai_api_key']
else:
config['settings']['tts']['openai_api_key'] = ''
if save_config(config):
return jsonify({'success': True, 'message': 'Configuration saved successfully'})
else:
return jsonify({'success': False, 'message': 'Failed to save configuration'}), 500
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 500
@app.route("/api/status")
def get_status():
"""Get current progress status."""
return jsonify(progress_tracker.get_status())
@app.route("/api/backgrounds/video", methods=["GET"])
def api_get_background_videos():
"""Get list of background videos."""
return jsonify({'videos': get_background_videos()})
@app.route("/api/backgrounds/audio", methods=["GET"])
def api_get_background_audios():
"""Get list of background audio tracks."""
return jsonify({'audios': get_background_audios()})
@app.route("/api/backgrounds/video", methods=["POST"])
def api_upload_background_video():
"""Upload a background video."""
if 'file' not in request.files:
return jsonify({'success': False, 'message': 'No file provided'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'success': False, 'message': 'No file selected'}), 400
if file and allowed_video_file(file.filename):
filename = secure_filename(file.filename)
filepath = BACKGROUNDS_VIDEO_PATH / filename
file.save(filepath)
return jsonify({'success': True, 'message': f'Video "{filename}" uploaded successfully'})
return jsonify({'success': False, 'message': 'Invalid file type'}), 400
@app.route("/api/backgrounds/audio", methods=["POST"])
def api_upload_background_audio():
"""Upload a background audio track."""
if 'file' not in request.files:
return jsonify({'success': False, 'message': 'No file provided'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'success': False, 'message': 'No file selected'}), 400
if file and allowed_audio_file(file.filename):
filename = secure_filename(file.filename)
filepath = BACKGROUNDS_AUDIO_PATH / filename
file.save(filepath)
return jsonify({'success': True, 'message': f'Audio "{filename}" uploaded successfully'})
return jsonify({'success': False, 'message': 'Invalid file type'}), 400
@app.route("/api/backgrounds/video/<filename>", methods=["DELETE"])
def api_delete_background_video(filename):
"""Delete a background video."""
filepath = BACKGROUNDS_VIDEO_PATH / secure_filename(filename)
if filepath.exists():
filepath.unlink()
return jsonify({'success': True, 'message': 'Video deleted'})
return jsonify({'success': False, 'message': 'File not found'}), 404
@app.route("/api/backgrounds/audio/<filename>", methods=["DELETE"])
def api_delete_background_audio(filename):
"""Delete a background audio track."""
filepath = BACKGROUNDS_AUDIO_PATH / secure_filename(filename)
if filepath.exists():
filepath.unlink()
return jsonify({'success': True, 'message': 'Audio deleted'})
return jsonify({'success': False, 'message': 'File not found'}), 404
@app.route("/api/videos", methods=["GET"])
def api_get_videos():
"""Get list of generated videos."""
return jsonify({'videos': get_generated_videos()})
@app.route("/api/generate", methods=["POST"])
def api_generate_video():
"""Start video generation."""
global generation_process, generation_thread
if generation_process and generation_process.poll() is None:
return jsonify({'success': False, 'message': 'Generation already in progress'}), 400
config = load_config()
if not config:
return jsonify({'success': False, 'message': 'Please configure settings first'}), 400
def run_generation():
global generation_process
try:
generation_process = subprocess.Popen(
['python', 'main.py'],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
cwd=str(Path(__file__).parent)
)
generation_process.wait()
except Exception as e:
print(f"Generation error: {e}")
generation_thread = threading.Thread(target=run_generation)
generation_thread.start()
return jsonify({'success': True, 'message': 'Video generation started'})
@app.route("/api/generate/stop", methods=["POST"])
def api_stop_generation():
"""Stop video generation."""
global generation_process
if generation_process and generation_process.poll() is None:
generation_process.terminate()
return jsonify({'success': True, 'message': 'Generation stopped'})
return jsonify({'success': False, 'message': 'No generation in progress'})
@app.route("/api/generate/status", methods=["GET"])
def api_generation_status():
"""Get generation status."""
global generation_process
running = generation_process and generation_process.poll() is None
return jsonify({
'running': running,
'progress': progress_tracker.get_status()
})
@app.route("/api/tts/test", methods=["POST"])
def api_test_tts():
"""Test Qwen TTS connection."""
try:
data = request.json
api_url = data.get('qwen_api_url', '').rstrip('/')
email = data.get('qwen_email', '')
password = data.get('qwen_password', '')
if not api_url or not email or not password:
return jsonify({'success': False, 'message': 'Missing required fields'})
# Test authentication
login_url = f"{api_url}/api/agent/api/auth/login"
auth_response = requests.post(
login_url,
json={'email': email, 'password': password},
headers={'Content-Type': 'application/json'},
timeout=10
)
if auth_response.status_code == 200:
result = auth_response.json()
if 'access_token' in result:
return jsonify({'success': True, 'message': 'Authentication successful'})
else:
return jsonify({'success': False, 'message': 'Invalid response from server'})
elif auth_response.status_code == 401:
return jsonify({'success': False, 'message': 'Invalid credentials'})
else:
return jsonify({'success': False, 'message': f'Server error: {auth_response.status_code}'})
except requests.exceptions.ConnectionError:
return jsonify({'success': False, 'message': 'Cannot connect to TTS server'})
except requests.exceptions.Timeout:
return jsonify({'success': False, 'message': 'Connection timeout'})
except Exception as e:
return jsonify({'success': False, 'message': str(e)})
@app.route("/api/backgrounds", methods=["GET"])
def api_get_all_backgrounds():
"""Get all backgrounds (videos and audio)."""
return jsonify({
'videos': get_background_videos(),
'audios': get_background_audios()
})
# ==================== Static Files ====================
@app.route("/static/<path:filename>")
def static_files(filename):
"""Serve static files."""
return send_from_directory("GUI/static", filename)
@app.route("/results/<path:name>")
def results(name):
"""Serve result videos."""
return send_from_directory("results", name)
@app.route("/preview/<path:name>")
def previews(name):
"""Serve preview images."""
return send_from_directory("assets/temp", name)
@app.route("/assets/<path:name>")
def assets(name):
"""Serve asset files."""
return send_from_directory("assets", name)
@app.route("/backgrounds/video/<path:name>")
def serve_background_video(name):
"""Serve background videos."""
return send_from_directory(BACKGROUNDS_VIDEO_PATH, name)
@app.route("/backgrounds/audio/<path:name>")
def serve_background_audio(name):
"""Serve background audio."""
return send_from_directory(BACKGROUNDS_AUDIO_PATH, name)
# ==================== SocketIO Events ====================
@socketio.on("connect", namespace="/progress")
def handle_connect():
"""Handle client connection."""
emit("progress_update", progress_tracker.get_status())
@socketio.on("disconnect", namespace="/progress")
def handle_disconnect():
"""Handle client disconnection."""
pass
@socketio.on("request_status", namespace="/progress")
def handle_request_status():
"""Handle status request from client."""
emit("progress_update", progress_tracker.get_status())
# ==================== Run Server ====================
def run_gui(open_browser=True):
"""Run the GUI server."""
import webbrowser
if open_browser:
webbrowser.open(f"http://localhost:{PORT}", new=2)
print(f"Reddit Video Maker Bot UI running at http://localhost:{PORT}")
socketio.run(app, host=HOST, port=PORT, debug=False)
def run_gui_background():
"""Run the GUI server in a background thread."""
thread = threading.Thread(target=lambda: socketio.run(app, host=HOST, port=PORT, debug=False, use_reloader=False))
thread.daemon = True
thread.start()
return thread
if __name__ == "__main__":
run_gui()