clip-creator
Version:
An open-source CLI tool for generating AI-powered video clips using FreeSound, GROQ, and Pexels. Create engaging content effortlessly with customizable features and high-quality assets.
521 lines (488 loc) • 19 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Clip Creator Web Interface</title>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css"
rel="stylesheet"
/>
</head>
<body class="bg-gray-100 min-h-screen">
<div class="container mx-auto px-4 py-8">
<h1 class="text-5xl font-bold text-center mb-10 text-blue-800">
🎬 Clip Creator
</h1>
<div class="bg-white rounded-lg shadow-lg p-6 max-w-4xl mx-auto">
<form id="clipForm" class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Required API Keys -->
<div class="space-y-4">
<h2 class="text-2xl font-semibold text-gray-800">
Required API Keys 🔑
</h2>
<div class="relative">
<div>
<label class="block text-sm font-medium text-gray-700"
>FreeSound API Key</label
>
<input
type="password"
name="freeSoundKey"
required
placeholder="Your Freesound.org API Key"
class="mt-1 p-3 block w-full rounded-md border-gray-300 shadow-lg focus:border-blue-500 focus:ring-blue-500"
/>
</div>
<button
type="button"
class="absolute inset-y-0 z-100 bg-white right-2 top-5 text-sm text-blue-600 hover:underline"
onclick="togglePasswordVisibility(this)"
>
Show
</button>
</div>
<div class="relative">
<div>
<label class="block text-sm font-medium text-gray-700"
>GROQ API Key</label
>
<input
type="password"
name="groqKey"
placeholder="Your GROQ API Key"
required
class="mt-1 p-3 block w-full rounded-md border-gray-300 shadow-lg focus:border-blue-500 focus:ring-blue-500"
/>
</div>
<button
type="button"
class="absolute inset-y-0 right-2 z-100 bg-white top-5 text-sm text-blue-600 hover:underline"
onclick="togglePasswordVisibility(this)"
>
Show
</button>
</div>
<div class="relative">
<div>
<label class="block text-sm font-medium text-gray-700"
>Pexels API Key</label
>
<input
type="password"
name="pexelsKey"
placeholder="Your Pexels API Key"
required
class="mt-1 p-3 block w-full rounded-md border-gray-300 shadow-lg focus:border-blue-500 focus:ring-blue-500"
/>
</div>
<button
type="button"
class="absolute inset-y-0 right-2 z-100 bg-white top-5 text-sm text-blue-600 hover:underline"
onclick="togglePasswordVisibility(this)"
>
Show
</button>
</div>
</div>
<!-- Required Content Settings -->
<div class="space-y-4">
<h2 class="text-2xl font-semibold text-gray-800">
Content Settings 🎥
</h2>
<div>
<label class="block text-sm font-medium text-gray-700"
>Category</label
>
<select
name="category"
required
class="mt-1 block w-full rounded-md border-gray-300 shadow-lg p-3 focus:border-blue-500 focus:ring-blue-500"
>
<option value="">Select a category</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700"
>Tone</label
>
<select
name="tone"
required
class="mt-1 block w-full rounded-md border-gray-300 shadow-lg p-3 focus:border-blue-500 focus:ring-blue-500"
>
<option value="">Select a tone</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700"
>Topic</label
>
<input
type="text"
name="topic"
required
placeholder="e.g., AI advancements"
class="mt-1 block w-full rounded-md border-gray-300 shadow-lg p-3 focus:border-blue-500 focus:ring-blue-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700"
>Duration (seconds)</label
>
<input
type="number"
name="duration"
required
min="10"
max="60"
value="30"
class="mt-1 block w-full rounded-md border-gray-300 shadow-lg p-3 focus:border-blue-500 focus:ring-blue-500"
/>
</div>
</div>
</div>
<!-- Advanced Settings Toggle -->
<div class="pt-4">
<button
type="button"
id="toggleAdvanced"
class="text-blue-600 hover:text-blue-800 font-medium"
>
Show Advanced Settings
</button>
</div>
<!-- Advanced Settings -->
<div id="advancedSettings" class="hidden space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4">
<h2 class="text-2xl font-semibold text-gray-700">
📀 Video Settings
</h2>
<div class="flex items-center">
<input
type="checkbox"
name="requireFactChecking"
class="h-4 w-4 text-blue-600"
/>
<label class="ml-2 text-sm text-gray-700"
>Enable fact-checking</label
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700"
>Key Terms (comma-separated)</label
>
<input
type="text"
name="keyTerms"
placeholder="AI, machine learning"
class="mt-1 block w-full rounded-md border-gray-300 shadow-lg p-3 focus:border-blue-500 focus:ring-blue-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700"
>Output Directory</label
>
<input
type="text"
name="outputDir"
placeholder="clip-creator-media"
class="mt-1 block w-full rounded-md border-gray-300 shadow-lg p-3 focus:border-blue-500 focus:ring-blue-500"
/>
</div>
</div>
<div class="space-y-4">
<h2 class="text-2xl font-semibold text-gray-700">
📟 Technical Settings
</h2>
<div>
<label class="block text-sm font-medium text-gray-700"
>Font Size (12-72)</label
>
<input
type="number"
name="fontSize"
min="12"
max="72"
value="24"
class="mt-1 block w-full rounded-md border-gray-300 shadow-lg p-3 focus:border-blue-500 focus:ring-blue-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700"
>FPS (1-60)</label
>
<input
type="number"
name="fps"
min="1"
max="60"
value="30"
class="mt-1 block w-full rounded-md border-gray-300 shadow-lg p-3 focus:border-blue-500 focus:ring-blue-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700"
>Height (px)</label
>
<input
type="number"
name="height"
min="240"
max="1920"
value="720"
class="mt-1 block w-full rounded-md border-gray-300 shadow-lg p-3 focus:border-blue-500 focus:ring-blue-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700"
>Width (px)</label
>
<input
type="number"
name="width"
min="240"
max="1920"
value="1280"
class="mt-1 block w-full rounded-md border-gray-300 shadow-lg p-3 focus:border-blue-500 focus:ring-blue-500"
/>
</div>
</div>
</div>
<h2 class="text-2xl font-semibold text-gray-700">
🎼 Audio Settings
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700"
>Volume (0-1)</label
>
<input
type="number"
name="volume"
min="0"
max="1"
step="0.1"
value="0.5"
class="mt-1 block w-full rounded-md border-black shadow-lg p-3 focus:border-blue-500 focus:ring-blue-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700"
>Fade In Duration (s)</label
>
<input
type="number"
name="fadeInDuration"
min="0"
max="5"
step="0.1"
value="0.5"
class="mt-1 block w-full rounded-md border-gray-300 shadow-lg p-3 focus:border-blue-500 focus:ring-blue-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700"
>Fade Out Duration (s)</label
>
<input
type="number"
name="fadeOutDuration"
min="0"
max="5"
step="0.1"
value="0.5"
class="mt-1 block w-full rounded-md border-gray-300 shadow-lg p-3 focus:border-blue-500 focus:ring-blue-500"
/>
</div>
</div>
</div>
<div class="flex justify-center pt-6">
<button
type="submit"
class="bg-gradient-to-r from-blue-600 to-indigo-600 text-white text-lg px-8 py-3 rounded-lg hover:shadow-lg hover:scale-105"
>
🎬 Create Video
</button>
</div>
</form>
<!-- Progress and Output Section -->
<div id="outputSection" class="hidden mt-8 space-y-4">
<div
class="bg-black text-green-400 p-4 rounded-lg font-mono text-sm h-64 overflow-y-auto"
id="logsContainer"
></div>
<div id="videoContainer" class="hidden">
<h2 class="text-2xl font-semibold text-gray-800 mb-4">
🎥 Generated Video
</h2>
<video
id="outputVideo"
controls
class="w-full rounded-lg shadow-lg"
>
Your browser does not support the video tag.
</video>
</div>
</div>
</div>
</div>
</body>
<script async>
function togglePasswordVisibility(button) {
const parent = button.previousElementSibling; // Get the input before the button
const input = parent.querySelector("input");
if (input.type === "password") {
input.type = "text";
button.textContent = "Hide";
} else {
input.type = "password";
button.textContent = "Show";
}
}
const categorySelect = document.querySelector('select[name="category"]');
const toneSelect = document.querySelector('select[name="tone"]');
async function initializeForm() {
try {
const [categoriesResponse, tonesResponse] = await Promise.all([
fetch("/api/categories"),
fetch("/api/tones"),
]);
const categories = await categoriesResponse.json();
const tones = await tonesResponse.json();
// Clear and populate category select
categorySelect.innerHTML =
'<option value="">Select a category</option>';
categories.data.forEach((category) => {
const option = new Option(category, category);
categorySelect.add(option);
});
// Clear and populate tone select
toneSelect.innerHTML = '<option value="">Select a tone</option>';
tones.data.forEach((tone) => {
const option = new Option(tone, tone);
toneSelect.add(option);
});
} catch (error) {
console.error("Failed to initialize form:", error);
}
}
// Toggle advanced settings
const toggleAdvanced = document.getElementById("toggleAdvanced");
const advancedSettings = document.getElementById("advancedSettings");
toggleAdvanced.addEventListener("click", () => {
advancedSettings.classList.toggle("hidden");
toggleAdvanced.textContent = advancedSettings.classList.contains("hidden")
? "Show Advanced Settings"
: "Hide Advanced Settings";
});
const form = document.getElementById("clipForm");
const outputSection = document.getElementById("outputSection");
const videoContainer = document.getElementById("videoContainer");
const outputVideo = document.getElementById("outputVideo");
const logsContainer = document.getElementById("logsContainer");
async function getVideoUsingFetch(filePath) {
try {
// Encode the file path to handle special characters
logsContainer.innerHTML +=
'<div class="text-blue-500">Converting Video for your preview</div>';
const encodedPath = encodeURIComponent(filePath);
const response = await fetch(`/api/video/${encodedPath}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// For browser: create a blob URL to play the video
const blob = await response.blob();
return URL.createObjectURL(blob);
} catch (error) {
console.error("Error fetching video:", error);
throw error;
}
}
form.addEventListener("submit", async (e) => {
e.preventDefault();
// Form validation remains the same
const formData = new FormData(form);
const config = Object.fromEntries(formData.entries());
// Validate required fields
const requiredFields = [
"freeSoundKey",
"groqKey",
"pexelsKey",
"category",
"tone",
"topic",
"duration",
];
const missingFields = requiredFields.filter((field) => !config[field]);
if (missingFields.length > 0) {
alert(
`Please fill in all required fields: ${missingFields.join(", ")}`
);
return;
}
// Numeric validation remains the same
const numericValidation = {
duration: { min: 10, max: 60 },
fontSize: { min: 12, max: 72 },
fps: { min: 1, max: 60 },
height: { min: 240, max: 1920 },
width: { min: 240, max: 1920 },
volume: { min: 0, max: 1 },
fadeInDuration: { min: 0, max: 5 },
fadeOutDuration: { min: 0, max: 5 },
};
for (const [field, range] of Object.entries(numericValidation)) {
if (config[field]) {
const value = Number(config[field]);
if (isNaN(value) || value < range.min || value > range.max) {
alert(`${field} must be between ${range.min} and ${range.max}`);
return;
}
}
}
if (config.keyTerms) {
config.keyTerms = config.keyTerms.split(",").map((term) => term.trim());
}
// Show output section and create logs container
outputSection.classList.remove("hidden");
videoContainer.classList.add("hidden");
// Replace progress bar with logs container
logsContainer.innerHTML = "";
logsContainer.scrollIntoView();
try {
const eventSource = new EventSource(
`/api/create-video?config=${encodeURIComponent(
JSON.stringify(config)
)}`
);
eventSource.onmessage = async (event) => {
const data = JSON.parse(event.data);
if (data.type === "log") {
logsContainer.innerHTML += `<div>${data.message}</div>`;
logsContainer.scrollTop = logsContainer.scrollHeight;
} else if (data.type === "complete") {
eventSource.close();
if (data.videoPath) {
const videoUrl = await getVideoUsingFetch(data.videoPath);
videoContainer.classList.remove("hidden");
document.getElementById("outputVideo").src = videoUrl;
videoContainer.scrollIntoView()
}
}
};
eventSource.onerror = (err) => {
console.log("Error", err);
eventSource.close();
logsContainer.innerHTML +=
'<div class="text-red-500">Connection lost</div>';
};
} catch (error) {
logsContainer.innerHTML += `<div class="text-red-500">Error: ${error.message}</div>`;
}
});
document.addEventListener("DOMContentLoaded", initializeForm);
</script>
</html>