Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Flask frontend with dark theme UI and video generation workflow #4

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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
186 changes: 186 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
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


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'

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"

# 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/<task_id>')
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/<task_id>')
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/<task_id>', 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/<path:filename>')
def get_result(filename):
"""Serve result files"""
return send_from_directory(app.config['RESULTS_FOLDER'], filename)

if __name__ == '__main__':
app.run(debug=True)
50 changes: 50 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
@@ -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]
95 changes: 95 additions & 0 deletions flask_frontend_README.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 10 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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
145 changes: 145 additions & 0 deletions static/css/landing.css
Original file line number Diff line number Diff line change
@@ -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);
}
426 changes: 426 additions & 0 deletions static/css/style.css

Large diffs are not rendered by default.

182 changes: 182 additions & 0 deletions static/js/main.js
Original file line number Diff line number Diff line change
@@ -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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.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 '<span class="' + cls + '">' + match + "</span>";
}
);
}

// 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 = `<pre>${formatJSON(jsonObj)}</pre>`;
} catch (e) {
console.error("Error parsing JSON:", e);
jsonContainer.innerHTML = `<pre>Error parsing JSON: ${e.message}</pre>`;
}
}

// 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" });
}
}
});
}
});
95 changes: 95 additions & 0 deletions templates/create.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
{% extends "layout.html" %} {% block title %}Create a Video - ForgeTube{%
endblock %} {% block content %}
<div class="card">
<div class="card-title">Create a New Video</div>

{% if error %}
<div class="alert alert-danger mb-4">{{ error }}</div>
{% endif %}

<div id="form-error" class="alert alert-danger mb-4 hidden"></div>

<form id="create-form" method="POST" action="{{ url_for('create') }}">
<div class="form-group">
<label for="topic">Video Topic *</label>
<input
type="text"
id="topic"
name="topic"
required
placeholder="e.g., The Future of Artificial Intelligence"
/>
</div>

<div class="form-group">
<label for="duration">Duration (seconds)</label>
<input
type="number"
id="duration"
name="duration"
min="30"
max="300"
value="60"
placeholder="60"
/>
<small class="text-muted">Recommended: 30-300 seconds</small>
</div>

<div class="form-group">
<label for="key_points">Key Points (comma separated)</label>
<textarea
id="key_points"
name="key_points"
placeholder="History of AI, Current applications, Future possibilities"
></textarea>
<small class="text-muted"
>List the main points you want to cover in your video</small
>
</div>

<div class="card mb-4" style="background-color: rgba(0, 0, 0, 0.3)">
<div class="card-title">API Keys</div>
<p class="text-muted mb-3">
ForgeTube requires API keys to generate content. Your keys are used
securely and never stored.
</p>

<div class="form-group">
<label for="gemini_api">Gemini API Key *</label>
<input
type="password"
id="gemini_api"
name="gemini_api"
required
placeholder="Your Gemini API key"
/>
<small class="text-muted"
>Get your key at
<a href="https://aistudio.google.com/apikey" target="_blank"
>Google AI Studio</a
></small
>
</div>

<div class="form-group">
<label for="serp_api">SERP API Key *</label>
<input
type="password"
id="serp_api"
name="serp_api"
required
placeholder="Your SERP API key"
/>
<small class="text-muted"
>Get your key at
<a href="https://serpapi.com" target="_blank">SerpAPI</a></small
>
</div>
</div>

<div class="text-center">
<button type="submit" class="btn btn-primary">Generate Video</button>
</div>
</form>
</div>
{% endblock %}
23 changes: 23 additions & 0 deletions templates/error.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{% extends "layout.html" %} {% block title %}Error - ForgeTube{% endblock %} {%
block content %}
<div class="card">
<div class="text-center">
<svg
width="80"
height="80"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="mb-4"
>
<path
d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM13 17H11V15H13V17ZM13 13H11V7H13V13Z"
fill="#EF4444"
/>
</svg>
<h1 class="mb-3">Error</h1>
<p class="text-muted mb-4">{{ message }}</p>
<a href="{{ url_for('index') }}" class="btn btn-primary">Return to Home</a>
</div>
</div>
{% endblock %}
312 changes: 312 additions & 0 deletions templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
{% extends "layout.html" %} {% block title %}ForgeTube - AI-Powered Video
Generation{% endblock %} {% block head %}
<style>
.hero-section {
position: relative;
border-radius: 0.5rem;
overflow: hidden;
padding: 4rem 2rem;
background: linear-gradient(
120deg,
rgba(99, 102, 241, 0.15),
rgba(30, 30, 30, 0.3)
);
margin-bottom: 2.5rem;
}

.hero-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%236366F1' fill-opacity='0.05' fill-rule='evenodd'/%3E%3C/svg%3E");
opacity: 0.5;
z-index: 0;
}

.hero-content {
position: relative;
z-index: 1;
max-width: 800px;
margin: 0 auto;
}

.hero-title {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1.5rem;
background: linear-gradient(90deg, #6366f1, #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}

.feature-card {
height: 100%;
transition: transform 0.3s ease, box-shadow 0.3s ease;
border: 1px solid rgba(45, 45, 45, 0.9);
background-color: rgba(30, 30, 30, 0.5);
}

.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
border-color: rgba(99, 102, 241, 0.3);
}

.feature-icon {
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
border-radius: 12px;
margin-bottom: 1rem;
background-color: rgba(99, 102, 241, 0.1);
}

.steps-container {
position: relative;
padding: 2rem;
background-color: rgba(17, 17, 17, 0.4);
border-radius: 0.5rem;
}

.step {
position: relative;
padding-left: 3rem;
margin-bottom: 2rem;
}

.step-number {
position: absolute;
left: 0;
top: 0;
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 9999px;
background-color: rgba(99, 102, 241, 0.2);
color: #6366f1;
font-weight: 700;
}

.cta-container {
text-align: center;
padding: 2rem;
margin-top: 2rem;
border-radius: 0.5rem;
background: linear-gradient(
to right,
rgba(99, 102, 241, 0.1),
rgba(139, 92, 246, 0.1)
);
border: 1px solid rgba(99, 102, 241, 0.2);
}

.btn-cta {
padding: 0.75rem 2rem;
font-size: 1.125rem;
background: linear-gradient(to right, #6366f1, #8b5cf6);
border: none;
border-radius: 0.5rem;
color: white;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
}

.btn-cta:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4);
}

@media (max-width: 768px) {
.hero-title {
font-size: 2rem;
}

.steps-container {
padding: 1rem;
}

.step {
padding-left: 2.5rem;
}
}

.hidden {
display: none;
}
</style>
{% endblock %} {% block content %}
<!-- Hero Section -->
<div class="hero-section">
<div class="hero-overlay"></div>
<div class="hero-content text-center">
<h1 class="hero-title">Transform Your Ideas into Videos with AI</h1>
<p class="text-muted mb-4">
ForgeTube generates professional videos from text prompts using AI-powered
script writing, image generation, and voice synthesis.
</p>
<a href="{{ url_for('create') }}" class="btn-cta">Create a Video</a>
</div>
</div>

<!-- Features Section -->
<h2 class="mb-4" style="font-size: 1.75rem; font-weight: 600">
Powerful AI Features
</h2>
<div class="grid grid-cols-3 mb-5">
<div class="card feature-card">
<div class="feature-icon">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 5V19H5V5H19ZM21 3H3V21H21V3ZM17 17H7V16H17V17ZM17 15H7V14H17V15ZM17 12H7V7H17V12Z"
fill="#6366F1"
/>
</svg>
</div>
<div class="card-title">Script Generation</div>
<p>
Provide a topic and key points, and our AI will craft a comprehensive
script, complete with audio narration and visual prompts.
</p>
</div>
<div class="card feature-card">
<div class="feature-icon">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3ZM19 19H5V5H19V19ZM13.96 12.29L11.21 15.83L9.25 13.47L6.5 17H17.5L13.96 12.29Z"
fill="#6366F1"
/>
</svg>
</div>
<div class="card-title">Image Creation</div>
<p>
Using state-of-the-art diffusion models, ForgeTube transforms text prompts
into stunning visuals tailored to your content.
</p>
</div>
<div class="card feature-card">
<div class="feature-icon">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 15C12.55 15 13 14.55 13 14V6C13 5.45 12.55 5 12 5C11.45 5 11 5.45 11 6V14C11 14.55 11.45 15 12 15ZM17 15C17.55 15 18 14.55 18 14V10C18 9.45 17.55 9 17 9C16.45 9 16 9.45 16 10V14C16 14.55 16.45 15 17 15ZM7 15C7.55 15 8 14.55 8 14V10C8 9.45 7.55 9 7 9C6.45 9 6 9.45 6 10V14C6 14.55 6.45 15 7 15ZM19.07 3C19.07 1.9 18.17 1 17.07 1H6.93C5.83 1 4.93 1.9 4.93 3L3 3V6C3 7.1 3.9 8 5 8V21C5 21.55 5.45 22 6 22H18C18.55 22 19 21.55 19 21V8C20.1 8 21 7.1 21 6V3H19.07Z"
fill="#6366F1"
/>
</svg>
</div>
<div class="card-title">Voice Synthesis</div>
<p>
Convert your script into natural-sounding narration with our advanced
text-to-speech technology.
</p>
</div>
</div>

<!-- How It Works Section -->
<div class="card mb-5">
<div class="card-title">How It Works</div>
<div class="steps-container">
<div class="grid grid-cols-2">
<div>
<div class="step">
<div class="step-number">1</div>
<h3 class="mb-2">Enter Your Topic</h3>
<p class="text-muted">
Provide a topic, desired duration, and key points you want to cover
in your video.
</p>
</div>
<div class="step">
<div class="step-number">2</div>
<h3 class="mb-2">Review & Refine</h3>
<p class="text-muted">
Review the AI-generated script and provide feedback to refine it to
your liking.
</p>
</div>
</div>
<div>
<div class="step">
<div class="step-number">3</div>
<h3 class="mb-2">Generate Assets</h3>
<p class="text-muted">
ForgeTube automatically creates images and audio narration based on
the approved script.
</p>
</div>
<div class="step">
<div class="step-number">4</div>
<h3 class="mb-2">Assemble & Download</h3>
<p class="text-muted">
The final video is assembled with images, audio, and subtitles,
ready for download.
</p>
</div>
</div>
</div>
</div>
</div>

<!-- Call to Action -->
<div class="cta-container">
<h2 class="mb-3" style="font-size: 1.75rem; font-weight: 600">
Ready to create your first AI video?
</h2>
<p class="text-muted mb-4">
No design or video editing skills required. Let AI do the work for you.
</p>
<a href="{{ url_for('create') }}" class="btn-cta">Start Creating Now</a>
</div>
{% endblock %} {% block scripts %}
<script>
// Add subtle animations when the page loads
document.addEventListener("DOMContentLoaded", function () {
// Animate feature cards on scroll
const featureCards = document.querySelectorAll(".feature-card");

const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = "translateY(0)";
}
});
},
{ threshold: 0.1 }
);

featureCards.forEach((card) => {
card.style.opacity = 0;
card.style.transform = "translateY(20px)";
card.style.transition = "opacity 0.5s ease, transform 0.5s ease";
observer.observe(card);
});
});
</script>
{% endblock %}
93 changes: 93 additions & 0 deletions templates/layout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>
{% block title %}ForgeTube - Automated Video Generation{% endblock %}
</title>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/style.css') }}"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Fira+Code&display=swap"
rel="stylesheet"
/>
<!-- Favicon -->
<link
rel="icon"
href="{{ url_for('static', filename='images/favicon.ico') }}"
/>
{% block head %}{% endblock %}
</head>
<body>
<header>
<div class="container header-container">
<a href="{{ url_for('index') }}" class="logo">
<svg
width="40"
height="40"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M4 8H2V20C2 21.1 2.9 22 4 22H16V20H4V8Z" fill="#6366F1" />
<path
d="M20 2H8C6.9 2 6 2.9 6 4V16C6 17.1 6.9 18 8 18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2ZM20 16H8V4H20V16ZM12 5.5V14.5L18 10L12 5.5Z"
fill="#6366F1"
/>
</svg>
ForgeTube
</a>
<nav class="nav-links">
<a
href="{{ url_for('index') }}"
class="{% if request.endpoint == 'index' %}active{% endif %}"
>Home</a
>
<a
href="{{ url_for('create') }}"
class="{% if request.endpoint == 'create' %}active{% endif %}"
>Create Video</a
>
</nav>
</div>
</header>

<main>
<div class="container">{% block content %}{% endblock %}</div>
</main>

<footer>
<div class="container">
<div class="footer-content">
<p>ForgeTube - Automated AI Video Generation</p>
<div class="footer-links">
<a href="https://github.com/MLSAKIIT/ForgeTube" target="_blank"
>GitHub</a
>
<a
href="https://www.youtube.com/channel/UCVgzYqxxY6wCIto-Nzx68Uw"
target="_blank"
>YouTube</a
>
<a href="https://x.com/mlsakiit" target="_blank">Twitter</a>
<a href="https://discord.com/invite/P6VCP2Ry3q" target="_blank"
>Discord</a
>
</div>
</div>
</div>
</footer>

<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>
130 changes: 130 additions & 0 deletions templates/progress.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
{% extends "layout.html" %} {% block title %}Video Generation Progress -
ForgeTube{% endblock %} {% block content %}
<div id="task-container" class="card" data-task-id="{{ task.id }}">
<div class="card-title">Video Generation: {{ task.topic }}</div>

<div class="mb-4">
<span
class="badge {% if task.status == 'Completed' %}badge-completed{% elif task.status == 'Queued' %}badge-queued{% else %}badge-processing{% endif %}"
id="status-badge"
>
{{ task.status }}
</span>
</div>

<div class="mb-4">
<p>
<strong>Status:</strong> <span id="task-status">{{ task.status }}</span>
</p>
<p>
<strong>Created:</strong> {{ task.created_at|int|timestamp_to_datetime }}
</p>
<p><strong>Duration:</strong> {{ task.duration }} seconds</p>
{% if task.key_points %}
<p><strong>Key Points:</strong> {{ task.key_points|join(', ') }}</p>
{% endif %}
</div>

<div class="progress-container">
<div
id="progress-bar"
class="progress-bar"
style="width:
{% if task.status == 'Completed' %}100%
{% elif task.status == 'Queued' %}5%
{% elif task.status == 'Generating script...' %}15%
{% elif task.status == 'Script generated' %}25%
{% elif task.status == 'Generating images...' %}40%
{% elif task.status == 'Generating audio...' %}60%
{% elif task.status == 'Assembling video...' %}80%
{% else %}0%{% endif %}"
></div>
</div>

{% if task.script %}
<div class="mt-4 mb-4">
<h3 class="mb-2">Generated Script</h3>
<div class="json-viewer">
<div id="json-content" data-json="{{ task.script|tojson }}"></div>
</div>

{% if task.status == 'Script generated' %}
<div class="mt-3">
<a
href="{{ url_for('refine_script', task_id=task.id) }}"
class="btn btn-outline"
>Refine Script</a
>
</div>
{% endif %}
</div>
{% endif %}

<div
id="result-section"
class="mt-4 {% if task.status != 'Completed' %}hidden{% endif %}"
>
<h3 class="mb-2">Generated Video</h3>
{% if task.result_url %}
<div class="video-container mb-3">
<video id="result-video" controls>
<source src="{{ task.result_url }}" type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>
<div class="text-center">
<a href="{{ task.result_url }}" class="btn btn-primary" download
>Download Video</a
>
</div>
{% else %}
<p class="text-muted">
Video generation is complete. Please refresh the page if the video doesn't
appear.
</p>
{% endif %}
</div>

<div class="mt-4 text-center">
<a href="{{ url_for('create') }}" class="btn btn-outline"
>Create Another Video</a
>
</div>
</div>
{% endblock %} {% block head %}
<style>
.hidden {
display: none;
}

/* JSON styling */
.json-key {
color: #f59e0b;
}

.json-string {
color: #10b981;
}

.json-number {
color: #3b82f6;
}

.json-boolean {
color: #8b5cf6;
}

.json-null {
color: #ef4444;
}
</style>
{% endblock %} {% block scripts %}
<script>
document.addEventListener("DOMContentLoaded", function () {
const taskId = document.getElementById("task-container").dataset.taskId;
if (taskId) {
pollTaskStatus(taskId);
}
});
</script>
{% endblock %}
66 changes: 66 additions & 0 deletions templates/refine.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{% extends "layout.html" %} {% block title %}Refine Script - ForgeTube{%
endblock %} {% block content %}
<div class="card">
<div class="card-title">Refine Script: {{ task.topic }}</div>

<p class="mb-4">
Review the generated script below and provide feedback to improve it. You
can request specific changes to the content, style, tone, or focus.
</p>

<div class="mb-4">
<h3 class="mb-2">Generated Script</h3>
<div class="json-viewer">
<div id="json-content" data-json="{{ task.script|tojson }}"></div>
</div>
</div>

<form method="POST" action="{{ url_for('refine_script', task_id=task.id) }}">
<div class="form-group">
<label for="feedback">Your Feedback</label>
<textarea
id="feedback"
name="feedback"
required
placeholder="Please make these changes to the script..."
></textarea>
<small class="text-muted"
>Be specific about what you want to change or improve in the
script.</small
>
</div>

<div class="grid grid-cols-2 gap-4">
<button type="submit" class="btn btn-primary">Apply Feedback</button>
<a
href="{{ url_for('task_progress', task_id=task.id) }}"
class="btn btn-outline"
>Skip Refinement</a
>
</div>
</form>
</div>
{% endblock %} {% block head %}
<style>
/* JSON styling */
.json-key {
color: #f59e0b;
}

.json-string {
color: #10b981;
}

.json-number {
color: #3b82f6;
}

.json-boolean {
color: #8b5cf6;
}

.json-null {
color: #ef4444;
}
</style>
{% endblock %}