From 977e83937c78b66f56868f44aa113b00bab2c30d Mon Sep 17 00:00:00 2001 From: Soham Roy Date: Mon, 10 Mar 2025 18:12:10 +0530 Subject: [PATCH 1/2] Add Flask frontend with dark theme UI and video generation workflow --- .env.example | 8 + app.py | 195 ++++++++++++++++++ config.py | 50 +++++ flask_frontend_README.md | 95 +++++++++ requirements.txt | 10 + static/css/landing.css | 145 +++++++++++++ static/css/style.css | 426 +++++++++++++++++++++++++++++++++++++++ static/js/main.js | 182 +++++++++++++++++ templates/create.html | 95 +++++++++ templates/error.html | 23 +++ templates/index.html | 312 ++++++++++++++++++++++++++++ templates/layout.html | 93 +++++++++ templates/progress.html | 130 ++++++++++++ templates/refine.html | 66 ++++++ 14 files changed, 1830 insertions(+) create mode 100644 .env.example create mode 100644 app.py create mode 100644 config.py create mode 100644 flask_frontend_README.md create mode 100644 requirements.txt create mode 100644 static/css/landing.css create mode 100644 static/css/style.css create mode 100644 static/js/main.js create mode 100644 templates/create.html create mode 100644 templates/error.html create mode 100644 templates/index.html create mode 100644 templates/layout.html create mode 100644 templates/progress.html create mode 100644 templates/refine.html diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c97ed75 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Flask Configuration +FLASK_APP=app.py +FLASK_ENV=development +SECRET_KEY=your-secret-key-here + +# ForgeTube API Keys +GEMINI_API_KEY=your-gemini-api-key-here +SERP_API_KEY=your-serp-api-key-here \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..792bfac --- /dev/null +++ b/app.py @@ -0,0 +1,195 @@ +from flask import Flask, render_template, request, redirect, url_for, jsonify, session, send_from_directory +import os +import json +import uuid +from werkzeug.utils import secure_filename +import time +import threading +from datetime import datetime + +# Import ForgeTube components +# We'll integrate with the existing components when needed +# from diffusion.scripts.generate_script import VideoScriptGenerator +# from diffusion.scripts.generate_image_local import main_generate_image +# from tts.scripts.generate_audio import main_generate_audio +# from assembly.scripts.assembly_video import create_video, create_complete_srt, extract_topic_from_json + +# Initialize Flask app +app = Flask(__name__) +app.secret_key = os.urandom(24) +app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload size +app.config['UPLOAD_FOLDER'] = 'uploads' +app.config['RESULTS_FOLDER'] = 'results' + +# Ensure upload and results directories exist +os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) +os.makedirs(app.config['RESULTS_FOLDER'], exist_ok=True) + +# Task tracking storage +tasks = {} + +# Custom Jinja2 filters +@app.template_filter('timestamp_to_datetime') +def timestamp_to_datetime(timestamp): + """Convert Unix timestamp to formatted datetime string""" + return datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S') + +# Helper functions +def create_or_check_folder(folder_path): + """ + Creates a folder if it doesn't exist. + If folder exists, checks for files and returns error message if any are found. + + Args: + folder_path (str): Path to the folder + + Returns: + tuple: (success, message) + """ + if not os.path.exists(folder_path): + os.makedirs(folder_path) + return True, f"Created Folder: {folder_path}" + else: + if any(os.listdir(folder_path)): + return False, f"Folder '{folder_path}' already exists and contains files. Please remove them or use a different folder." + return True, f"Folder '{folder_path}' exists but is empty." + +def generate_video_task(task_id, topic, duration, key_points, api_keys): + """ + Background task to generate video + """ + task = tasks[task_id] + task['status'] = 'Generating script...' + time.sleep(2) # Simulate script generation + + # TODO: Replace with actual script generation + # generator = VideoScriptGenerator(api_key=api_keys['gemini'], serp_api_key=api_keys['serp']) + # script = generator.generate_script(topic, duration, key_points) + + script = {"topic": topic, "audio_script": [{"text": "This is a test script"}]} + task['script'] = script + task['status'] = 'Script generated' + + # In a real implementation, continue with image generation, audio generation, and video assembly + # For now, we'll simulate these steps + task['status'] = 'Generating images...' + time.sleep(3) # Simulate image generation + + task['status'] = 'Generating audio...' + time.sleep(2) # Simulate audio generation + + task['status'] = 'Assembling video...' + time.sleep(3) # Simulate video assembly + + task['status'] = 'Completed' + task['result_url'] = f"/results/{task_id}.mp4" # This would be the actual video URL + +# Routes +@app.route('/') +def index(): + """Home page""" + return render_template('index.html') + +@app.route('/create', methods=['GET', 'POST']) +def create(): + """Create new video page""" + if request.method == 'POST': + # Get form data + topic = request.form.get('topic', '') + duration = int(request.form.get('duration', 60)) + key_points_text = request.form.get('key_points', '') + key_points = [point.strip() for point in key_points_text.split(',') if point.strip()] + + # Get API keys + gemini_api = request.form.get('gemini_api', '') + serp_api = request.form.get('serp_api', '') + + # Validate inputs + if not topic: + return render_template('create.html', error="Topic is required") + + if not gemini_api or not serp_api: + return render_template('create.html', + error="API keys are required. Get your Gemini API key at https://aistudio.google.com/apikey and Serp API key at https://serpapi.com") + + # Create task + task_id = str(uuid.uuid4()) + task = { + 'id': task_id, + 'topic': topic, + 'duration': duration, + 'key_points': key_points, + 'status': 'Queued', + 'created_at': time.time(), + 'api_keys': { + 'gemini': gemini_api, + 'serp': serp_api + } + } + tasks[task_id] = task + + # Start task in background + threading.Thread( + target=generate_video_task, + args=(task_id, topic, duration, key_points, task['api_keys']) + ).start() + + # Redirect to task progress page + return redirect(url_for('task_progress', task_id=task_id)) + + return render_template('create.html') + +@app.route('/task/') +def task_progress(task_id): + """Task progress page""" + task = tasks.get(task_id) + if not task: + return render_template('error.html', message="Task not found"), 404 + + return render_template('progress.html', task=task) + +@app.route('/api/task/') +def task_status(task_id): + """API endpoint to get task status""" + task = tasks.get(task_id) + if not task: + return jsonify({"error": "Task not found"}), 404 + + # Return task data without API keys for security + safe_task = {**task} + if 'api_keys' in safe_task: + del safe_task['api_keys'] + + return jsonify(safe_task) + +@app.route('/refine/', methods=['GET', 'POST']) +def refine_script(task_id): + """Refine script page""" + task = tasks.get(task_id) + if not task: + return render_template('error.html', message="Task not found"), 404 + + if request.method == 'POST': + feedback = request.form.get('feedback', '') + if feedback: + # TODO: Implement script refinement with API + # generator = VideoScriptGenerator(api_key=task['api_keys']['gemini'], + # serp_api_key=task['api_keys']['serp']) + # refined_script = generator.refine_script(task['script'], feedback) + # task['script'] = refined_script + + # For now, just acknowledge the feedback + task['feedback'] = feedback + task['status'] = 'Script refined, ready to generate' + + return redirect(url_for('task_progress', task_id=task_id)) + + return render_template('refine.html', task=task) + +@app.route('/results/') +def get_result(filename): + """Serve result files""" + return send_from_directory(app.config['RESULTS_FOLDER'], filename) + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..b07f4b5 --- /dev/null +++ b/config.py @@ -0,0 +1,50 @@ +import os + +class Config: + """Base configuration""" + SECRET_KEY = os.environ.get('SECRET_KEY') or os.urandom(24) + MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max upload size + UPLOAD_FOLDER = 'uploads' + RESULTS_FOLDER = 'results' + + # ForgeTube resources paths + SCRIPTS_PATH = "resources/scripts/" + IMAGES_PATH = "resources/images/" + AUDIO_PATH = "resources/audio/" + FONT_PATH = "resources/font/font.ttf" + + # Default video settings + DEFAULT_DURATION = 60 # in seconds + MAX_DURATION = 300 # 5 minutes max + + # UI Settings + ACCENT_COLOR = "#6366F1" # Indigo + DARK_BG = "#121212" + DARKER_BG = "#0A0A0A" + TEXT_COLOR = "#E5E7EB" + +class DevelopmentConfig(Config): + """Development configuration""" + DEBUG = True + TESTING = False + +class TestingConfig(Config): + """Testing configuration""" + DEBUG = False + TESTING = True + +class ProductionConfig(Config): + """Production configuration""" + DEBUG = False + TESTING = False + +config_by_name = { + 'development': DevelopmentConfig, + 'testing': TestingConfig, + 'production': ProductionConfig +} + +def get_config(): + """Return the appropriate configuration object based on environment variable""" + env = os.environ.get('FLASK_ENV', 'development') + return config_by_name[env] \ No newline at end of file diff --git a/flask_frontend_README.md b/flask_frontend_README.md new file mode 100644 index 0000000..d67089c --- /dev/null +++ b/flask_frontend_README.md @@ -0,0 +1,95 @@ +# ForgeTube Web Frontend + +A modern, dark-themed web interface for ForgeTube, an AI-powered video generation system. + +## Overview + +This Flask application provides an intuitive web interface for interacting with ForgeTube's automated video generation capabilities. It allows users to: + +1. Input topics, durations, and key points for video generation +2. Review and refine AI-generated scripts +3. Track the progress of video generation +4. Preview and download the final videos + +## Features + +- **Modern, Dark UI**: Sleek, minimalistic interface with a dark color scheme +- **Responsive Design**: Works well on desktop and mobile devices +- **Real-time Progress Tracking**: Live updates on video generation status +- **Script Refinement**: Interactive script review and feedback system +- **Secure API Key Management**: User-provided API keys used securely for content generation + +## Installation + +1. Clone the repository: + + ```bash + git clone https://github.com/MLSAKIIT/ForgeTube.git + cd ForgeTube + ``` + +2. Create and activate a virtual environment: + + ```bash + python -m venv venv + # On Windows + venv\Scripts\activate + # On macOS/Linux + source venv/bin/activate + ``` + +3. Install dependencies: + + ```bash + pip install -r requirements.txt + ``` + +4. Create a `.env` file based on `.env.example`: + ```bash + cp .env.example .env + ``` + Then edit the `.env` file with your API keys. + +## Usage + +1. Start the Flask development server: + + ```bash + flask run + ``` + +2. Open your browser and navigate to: + + ``` + http://127.0.0.1:5000/ + ``` + +3. Follow the on-screen instructions to create your first video. + +## Requirements + +- Python 3.8 or higher +- Gemini API key (from Google AI Studio) +- SERP API key (from serpapi.com) +- FFmpeg (for video processing) + +## Integration with ForgeTube Core + +This frontend is designed to work with ForgeTube's core modules: + +- `diffusion/scripts/generate_script.py`: For script generation +- `diffusion/scripts/generate_image_local.py`: For image generation +- `tts/scripts/generate_audio.py`: For audio generation +- `assembly/scripts/assembly_video.py`: For final video assembly + +## Development + +To contribute to this frontend: + +1. Create a new branch for your feature or bug fix +2. Make your changes +3. Submit a pull request + +## License + +This project is licensed under the same terms as the main ForgeTube project. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9fd110c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +flask==2.3.3 +google-generativeai==0.3.1 +serpapi==0.1.0 +pysrt==1.1.2 +moviepy==1.0.3 +werkzeug==2.3.7 +jinja2==3.1.2 +itsdangerous==2.1.2 +gunicorn==21.2.0 +python-dotenv==1.0.0 \ No newline at end of file diff --git a/static/css/landing.css b/static/css/landing.css new file mode 100644 index 0000000..a95a7b9 --- /dev/null +++ b/static/css/landing.css @@ -0,0 +1,145 @@ +/* Additional Landing Page Styles */ + +/* Animated gradient background for hero section */ +@keyframes gradientAnimation { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} + +.animated-gradient { + background: linear-gradient( + 270deg, + rgba(99, 102, 241, 0.2), + rgba(139, 92, 246, 0.2), + rgba(30, 30, 30, 0.3) + ); + background-size: 300% 300%; + animation: gradientAnimation 15s ease infinite; +} + +/* Pulse animation for CTA button */ +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4); + } + 70% { + box-shadow: 0 0 0 10px rgba(99, 102, 241, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); + } +} + +.pulse-animation { + animation: pulse 2s infinite; +} + +/* Floating animation for cards */ +@keyframes float { + 0% { + transform: translateY(0px); + } + 50% { + transform: translateY(-5px); + } + 100% { + transform: translateY(0px); + } +} + +.float-animation { + animation: float 5s ease-in-out infinite; +} + +/* Shine effect for section titles */ +.shine-text { + position: relative; + overflow: hidden; +} + +.shine-text::after { + content: ""; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient( + to right, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.05) 50%, + rgba(255, 255, 255, 0) 100% + ); + transform: rotate(30deg); + animation: shine 6s ease-in-out infinite; +} + +@keyframes shine { + 0% { + left: -100%; + } + 20%, + 100% { + left: 100%; + } +} + +/* Step connector lines */ +.step-connector { + position: absolute; + left: 1rem; + top: 2rem; + bottom: 0; + width: 2px; + background: linear-gradient( + to bottom, + rgba(99, 102, 241, 0.2), + rgba(99, 102, 241, 0.1) + ); +} + +/* Improved card shadow effects */ +.depth-shadow { + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.07), + 0 4px 8px rgba(0, 0, 0, 0.07), 0 8px 16px rgba(0, 0, 0, 0.07); + transition: box-shadow 0.3s ease; +} + +.depth-shadow:hover { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.1), + 0 8px 16px rgba(0, 0, 0, 0.1), 0 16px 32px rgba(0, 0, 0, 0.1); +} + +/* Accent border styles */ +.accent-border { + position: relative; + overflow: hidden; +} + +.accent-border::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + background: linear-gradient(to bottom, #6366f1, #8b5cf6); + border-radius: 4px 0 0 4px; +} + +/* Glow effect for important elements */ +.glow-effect { + box-shadow: 0 0 10px rgba(99, 102, 241, 0.2); + transition: box-shadow 0.3s ease; +} + +.glow-effect:hover { + box-shadow: 0 0 20px rgba(99, 102, 241, 0.3); +} diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..4ba8e0b --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,426 @@ +/* ForgeTube Web UI Styles */ +/* Base styles */ +:root { + --accent-color: #6366f1; + --accent-hover: #4f46e5; + --dark-bg: #121212; + --darker-bg: #0a0a0a; + --card-bg: #1e1e1e; + --text-color: #e5e7eb; + --text-muted: #9ca3af; + --border-color: #2d2d2d; + --danger-color: #ef4444; + --success-color: #10b981; + --warning-color: #f59e0b; + --info-color: #3b82f6; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + background-color: var(--dark-bg); + color: var(--text-color); + line-height: 1.5; + min-height: 100vh; +} + +.container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +a { + color: var(--accent-color); + text-decoration: none; + transition: color 0.2s ease; +} + +a:hover { + color: var(--accent-hover); +} + +/* Header */ +header { + background-color: var(--darker-bg); + padding: 1rem 0; + border-bottom: 1px solid var(--border-color); + margin-bottom: 2rem; +} + +.header-container { + display: flex; + align-items: center; + justify-content: space-between; +} + +.logo { + display: flex; + align-items: center; + font-weight: 700; + font-size: 1.5rem; + color: var(--text-color); +} + +.logo img { + height: 2.5rem; + margin-right: 0.75rem; +} + +.nav-links { + display: flex; + gap: 1.5rem; +} + +.nav-links a { + color: var(--text-muted); + transition: color 0.2s ease; +} + +.nav-links a:hover, +.nav-links a.active { + color: var(--text-color); +} + +/* Main content */ +main { + padding: 2rem 0; +} + +/* Card component */ +.card { + background-color: var(--card-bg); + border-radius: 0.5rem; + border: 1px solid var(--border-color); + padding: 1.5rem; + margin-bottom: 1.5rem; +} + +.card-title { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 1rem; + color: var(--text-color); +} + +/* Form elements */ +.form-group { + margin-bottom: 1.5rem; +} + +label { + display: block; + margin-bottom: 0.5rem; + color: var(--text-muted); +} + +input[type="text"], +input[type="number"], +input[type="password"], +input[type="email"], +textarea, +select { + width: 100%; + padding: 0.75rem; + border-radius: 0.375rem; + border: 1px solid var(--border-color); + background-color: rgba(255, 255, 255, 0.05); + color: var(--text-color); + font-size: 1rem; + transition: border-color 0.2s ease; +} + +input:focus, +textarea:focus, +select:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.25); +} + +textarea { + min-height: 8rem; + resize: vertical; +} + +/* Buttons */ +.btn { + display: inline-block; + padding: 0.75rem 1.5rem; + border-radius: 0.375rem; + font-weight: 500; + font-size: 1rem; + text-align: center; + cursor: pointer; + transition: all 0.2s ease; + border: none; +} + +.btn-primary { + background-color: var(--accent-color); + color: white; +} + +.btn-primary:hover { + background-color: var(--accent-hover); +} + +.btn-outline { + background-color: transparent; + border: 1px solid var(--accent-color); + color: var(--accent-color); +} + +.btn-outline:hover { + background-color: var(--accent-color); + color: white; +} + +.btn-danger { + background-color: var(--danger-color); + color: white; +} + +.btn-danger:hover { + background-color: #b91c1c; +} + +.btn-success { + background-color: var(--success-color); + color: white; +} + +.btn-success:hover { + background-color: #059669; +} + +/* Alerts */ +.alert { + padding: 1rem; + border-radius: 0.375rem; + margin-bottom: 1.5rem; +} + +.alert-danger { + background-color: rgba(239, 68, 68, 0.1); + border-left: 4px solid var(--danger-color); + color: #fca5a5; +} + +.alert-success { + background-color: rgba(16, 185, 129, 0.1); + border-left: 4px solid var(--success-color); + color: #6ee7b7; +} + +.alert-info { + background-color: rgba(59, 130, 246, 0.1); + border-left: 4px solid var(--info-color); + color: #93c5fd; +} + +.alert-warning { + background-color: rgba(245, 158, 11, 0.1); + border-left: 4px solid var(--warning-color); + color: #fcd34d; +} + +/* Progress bar */ +.progress-container { + width: 100%; + background-color: rgba(255, 255, 255, 0.1); + border-radius: 0.25rem; + margin: 1.5rem 0; +} + +.progress-bar { + height: 0.5rem; + border-radius: 0.25rem; + background-color: var(--accent-color); + transition: width 0.3s ease; +} + +/* Status badges */ +.badge { + display: inline-block; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + font-weight: 500; +} + +.badge-queued { + background-color: rgba(99, 102, 241, 0.1); + color: var(--accent-color); +} + +.badge-processing { + background-color: rgba(245, 158, 11, 0.1); + color: var(--warning-color); +} + +.badge-completed { + background-color: rgba(16, 185, 129, 0.1); + color: var(--success-color); +} + +.badge-error { + background-color: rgba(239, 68, 68, 0.1); + color: var(--danger-color); +} + +/* Grid layout */ +.grid { + display: grid; + grid-template-columns: repeat(1, 1fr); + gap: 1.5rem; +} + +@media (min-width: 640px) { + .grid-cols-2 { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (min-width: 768px) { + .grid-cols-3 { + grid-template-columns: repeat(3, 1fr); + } +} + +/* Utilities */ +.text-center { + text-align: center; +} + +.text-muted { + color: var(--text-muted); +} + +.mb-1 { + margin-bottom: 0.25rem; +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.mb-3 { + margin-bottom: 1rem; +} + +.mb-4 { + margin-bottom: 1.5rem; +} + +.mb-5 { + margin-bottom: 2rem; +} + +.mt-1 { + margin-top: 0.25rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.mt-3 { + margin-top: 1rem; +} + +.mt-4 { + margin-top: 1.5rem; +} + +.mt-5 { + margin-top: 2rem; +} + +/* Animation */ +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.animate-pulse { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +/* Video player */ +.video-container { + position: relative; + padding-bottom: 56.25%; /* 16:9 aspect ratio */ + height: 0; + overflow: hidden; + background-color: var(--darker-bg); + border-radius: 0.5rem; + margin-bottom: 1.5rem; +} + +.video-container video { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 0.5rem; +} + +/* JSON Viewer for script display */ +.json-viewer { + background-color: rgba(0, 0, 0, 0.2); + border-radius: 0.375rem; + padding: 1rem; + overflow: auto; + max-height: 500px; + font-family: "Fira Code", "Roboto Mono", monospace; + font-size: 0.875rem; +} + +.json-viewer pre { + margin: 0; + white-space: pre-wrap; +} + +/* Footer */ +footer { + background-color: var(--darker-bg); + padding: 2rem 0; + margin-top: 2rem; + border-top: 1px solid var(--border-color); +} + +.footer-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} + +.footer-links { + display: flex; + gap: 1rem; + margin-top: 1rem; +} + +.footer-links a { + color: var(--text-muted); +} + +.footer-links a:hover { + color: var(--text-color); +} diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..c76fc63 --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,182 @@ +// ForgeTube Web UI JavaScript + +// Task status polling +function pollTaskStatus(taskId) { + const statusElement = document.getElementById("task-status"); + const progressBar = document.getElementById("progress-bar"); + + if (!statusElement || !progressBar) return; + + const updateStatus = () => { + fetch(`/api/task/${taskId}`) + .then((response) => { + if (!response.ok) { + throw new Error("Failed to fetch task status"); + } + return response.json(); + }) + .then((data) => { + // Update status text + statusElement.textContent = data.status; + + // Update progress bar + let progress = 0; + const statusMap = { + Queued: 5, + "Generating script...": 15, + "Script generated": 25, + "Generating images...": 40, + "Generating audio...": 60, + "Assembling video...": 80, + Completed: 100, + Error: 100, + }; + + progress = statusMap[data.status] || 0; + progressBar.style.width = `${progress}%`; + + // Add appropriate classes based on status + document.querySelectorAll(".badge").forEach((badge) => { + badge.classList.remove( + "badge-queued", + "badge-processing", + "badge-completed", + "badge-error" + ); + }); + + const badgeElement = document.getElementById("status-badge"); + if (badgeElement) { + if (data.status === "Completed") { + badgeElement.classList.add("badge-completed"); + + // Show the result section if available + const resultSection = document.getElementById("result-section"); + if (resultSection) { + resultSection.classList.remove("hidden"); + } + + // Update video source if available + const videoElement = document.getElementById("result-video"); + if (videoElement && data.result_url) { + videoElement.src = data.result_url; + videoElement.load(); + } + + // No need to poll anymore + return; + } else if (data.status.includes("Error")) { + badgeElement.classList.add("badge-error"); + } else if (data.status === "Queued") { + badgeElement.classList.add("badge-queued"); + } else { + badgeElement.classList.add("badge-processing"); + } + } + + // Continue polling if not completed or error + if (data.status !== "Completed" && !data.status.includes("Error")) { + setTimeout(updateStatus, 2000); + } + }) + .catch((error) => { + console.error("Error polling task status:", error); + statusElement.textContent = "Error checking status"; + + // Try again after a longer delay + setTimeout(updateStatus, 5000); + }); + }; + + // Start polling + updateStatus(); +} + +// Format JSON for display +function formatJSON(jsonObj) { + return JSON.stringify(jsonObj, null, 2) + .replace(/&/g, "&") + .replace(//g, ">") + .replace( + /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, + function (match) { + let cls = "json-number"; + if (/^"/.test(match)) { + if (/:$/.test(match)) { + cls = "json-key"; + } else { + cls = "json-string"; + } + } else if (/true|false/.test(match)) { + cls = "json-boolean"; + } else if (/null/.test(match)) { + cls = "json-null"; + } + return '' + match + ""; + } + ); +} + +// Initialize JSON viewer if present +document.addEventListener("DOMContentLoaded", function () { + const jsonContainer = document.getElementById("json-content"); + if (jsonContainer && jsonContainer.dataset.json) { + try { + const jsonObj = JSON.parse(jsonContainer.dataset.json); + jsonContainer.innerHTML = `
${formatJSON(jsonObj)}
`; + } catch (e) { + console.error("Error parsing JSON:", e); + jsonContainer.innerHTML = `
Error parsing JSON: ${e.message}
`; + } + } + + // Initialize task polling if on task page + const taskContainer = document.getElementById("task-container"); + if (taskContainer && taskContainer.dataset.taskId) { + pollTaskStatus(taskContainer.dataset.taskId); + } +}); + +// Form validation +document.addEventListener("DOMContentLoaded", function () { + const createForm = document.getElementById("create-form"); + if (createForm) { + createForm.addEventListener("submit", function (event) { + const topic = document.getElementById("topic").value.trim(); + const geminiApi = document.getElementById("gemini_api").value.trim(); + const serpApi = document.getElementById("serp_api").value.trim(); + + let isValid = true; + let errorMessage = ""; + + if (!topic) { + isValid = false; + errorMessage += "Topic is required. "; + } + + if (!geminiApi) { + isValid = false; + errorMessage += "Gemini API key is required. "; + } + + if (!serpApi) { + isValid = false; + errorMessage += "Serp API key is required. "; + } + + if (!isValid) { + event.preventDefault(); + + const errorElement = document.getElementById("form-error"); + if (errorElement) { + errorElement.textContent = errorMessage; + errorElement.classList.remove("hidden"); + + // Scroll to error + errorElement.scrollIntoView({ behavior: "smooth" }); + } + } + }); + } +}); diff --git a/templates/create.html b/templates/create.html new file mode 100644 index 0000000..b5b03bf --- /dev/null +++ b/templates/create.html @@ -0,0 +1,95 @@ +{% extends "layout.html" %} {% block title %}Create a Video - ForgeTube{% +endblock %} {% block content %} +
+
Create a New Video
+ + {% if error %} +
{{ error }}
+ {% endif %} + + + +
+
+ + +
+ +
+ + + Recommended: 30-300 seconds +
+ +
+ + + List the main points you want to cover in your video +
+ +
+
API Keys
+

+ ForgeTube requires API keys to generate content. Your keys are used + securely and never stored. +

+ +
+ + + Get your key at + Google AI Studio +
+ +
+ + + Get your key at + SerpAPI +
+
+ +
+ +
+
+
+{% endblock %} diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000..aae5f58 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,23 @@ +{% extends "layout.html" %} {% block title %}Error - ForgeTube{% endblock %} {% +block content %} +
+
+ + + +

Error

+

{{ message }}

+ Return to Home +
+
+{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..b05def1 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,312 @@ +{% extends "layout.html" %} {% block title %}ForgeTube - AI-Powered Video +Generation{% endblock %} {% block head %} + +{% endblock %} {% block content %} + +
+
+
+

Transform Your Ideas into Videos with AI

+

+ ForgeTube generates professional videos from text prompts using AI-powered + script writing, image generation, and voice synthesis. +

+ Create a Video +
+
+ + +

+ Powerful AI Features +

+
+
+
+ + + +
+
Script Generation
+

+ Provide a topic and key points, and our AI will craft a comprehensive + script, complete with audio narration and visual prompts. +

+
+
+
+ + + +
+
Image Creation
+

+ Using state-of-the-art diffusion models, ForgeTube transforms text prompts + into stunning visuals tailored to your content. +

+
+
+
+ + + +
+
Voice Synthesis
+

+ Convert your script into natural-sounding narration with our advanced + text-to-speech technology. +

+
+
+ + +
+
How It Works
+
+
+
+
+
1
+

Enter Your Topic

+

+ Provide a topic, desired duration, and key points you want to cover + in your video. +

+
+
+
2
+

Review & Refine

+

+ Review the AI-generated script and provide feedback to refine it to + your liking. +

+
+
+
+
+
3
+

Generate Assets

+

+ ForgeTube automatically creates images and audio narration based on + the approved script. +

+
+
+
4
+

Assemble & Download

+

+ The final video is assembled with images, audio, and subtitles, + ready for download. +

+
+
+
+
+
+ + +
+

+ Ready to create your first AI video? +

+

+ No design or video editing skills required. Let AI do the work for you. +

+ Start Creating Now +
+{% endblock %} {% block scripts %} + +{% endblock %} diff --git a/templates/layout.html b/templates/layout.html new file mode 100644 index 0000000..8be78ad --- /dev/null +++ b/templates/layout.html @@ -0,0 +1,93 @@ + + + + + + + {% block title %}ForgeTube - Automated Video Generation{% endblock %} + + + + + + + + + {% block head %}{% endblock %} + + +
+ +
+ +
+
{% block content %}{% endblock %}
+
+ + + + + {% block scripts %}{% endblock %} + + diff --git a/templates/progress.html b/templates/progress.html new file mode 100644 index 0000000..bde80b6 --- /dev/null +++ b/templates/progress.html @@ -0,0 +1,130 @@ +{% extends "layout.html" %} {% block title %}Video Generation Progress - +ForgeTube{% endblock %} {% block content %} +
+
Video Generation: {{ task.topic }}
+ +
+ + {{ task.status }} + +
+ +
+

+ Status: {{ task.status }} +

+

+ Created: {{ task.created_at|int|timestamp_to_datetime }} +

+

Duration: {{ task.duration }} seconds

+ {% if task.key_points %} +

Key Points: {{ task.key_points|join(', ') }}

+ {% endif %} +
+ +
+
+
+ + {% if task.script %} +
+

Generated Script

+
+
+
+ + {% if task.status == 'Script generated' %} + + {% endif %} +
+ {% endif %} + +
+

Generated Video

+ {% if task.result_url %} +
+ +
+ + {% else %} +

+ Video generation is complete. Please refresh the page if the video doesn't + appear. +

+ {% endif %} +
+ + +
+{% endblock %} {% block head %} + +{% endblock %} {% block scripts %} + +{% endblock %} diff --git a/templates/refine.html b/templates/refine.html new file mode 100644 index 0000000..fa6f5a7 --- /dev/null +++ b/templates/refine.html @@ -0,0 +1,66 @@ +{% extends "layout.html" %} {% block title %}Refine Script - ForgeTube{% +endblock %} {% block content %} +
+
Refine Script: {{ task.topic }}
+ +

+ Review the generated script below and provide feedback to improve it. You + can request specific changes to the content, style, tone, or focus. +

+ +
+

Generated Script

+
+
+
+
+ +
+
+ + + Be specific about what you want to change or improve in the + script. +
+ +
+ + Skip Refinement +
+
+
+{% endblock %} {% block head %} + +{% endblock %} From cfb196775d203a89bb7bd3ac1ed0cb9cfae7d3c3 Mon Sep 17 00:00:00 2001 From: Soham Roy Date: Mon, 10 Mar 2025 18:17:42 +0530 Subject: [PATCH 2/2] . --- app.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/app.py b/app.py index 792bfac..54b3493 100644 --- a/app.py +++ b/app.py @@ -7,14 +7,7 @@ import threading from datetime import datetime -# Import ForgeTube components -# We'll integrate with the existing components when needed -# from diffusion.scripts.generate_script import VideoScriptGenerator -# from diffusion.scripts.generate_image_local import main_generate_image -# from tts.scripts.generate_audio import main_generate_audio -# from assembly.scripts.assembly_video import create_video, create_complete_srt, extract_topic_from_json -# Initialize Flask app app = Flask(__name__) app.secret_key = os.urandom(24) app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload size @@ -70,8 +63,6 @@ def generate_video_task(task_id, topic, duration, key_points, api_keys): task['script'] = script task['status'] = 'Script generated' - # In a real implementation, continue with image generation, audio generation, and video assembly - # For now, we'll simulate these steps task['status'] = 'Generating images...' time.sleep(3) # Simulate image generation @@ -82,7 +73,7 @@ def generate_video_task(task_id, topic, duration, key_points, api_keys): time.sleep(3) # Simulate video assembly task['status'] = 'Completed' - task['result_url'] = f"/results/{task_id}.mp4" # This would be the actual video URL + task['result_url'] = f"/results/{task_id}.mp4" # Routes @app.route('/')