composer / static /index.html
factorstudios's picture
Upload 3 files
a6270eb verified
Raw
History Blame Contribute Delete
16.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TrendClip - Video Composer</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
width: 100%;
max-width: 900px;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
font-weight: 700;
}
.header p {
font-size: 1.1em;
opacity: 0.9;
}
.content {
padding: 40px;
}
.form-group {
margin-bottom: 30px;
}
.form-group label {
display: block;
margin-bottom: 12px;
font-weight: 600;
color: #333;
font-size: 1.1em;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 14px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1em;
font-family: inherit;
transition: all 0.3s ease;
}
.form-group textarea {
min-height: 120px;
resize: vertical;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.button-group {
display: flex;
gap: 12px;
margin-bottom: 30px;
}
button {
flex: 1;
padding: 14px 24px;
border: none;
border-radius: 8px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-generate {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-generate:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.btn-generate:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-download {
background: #4caf50;
color: white;
display: none;
}
.btn-download:hover:not(:disabled) {
background: #45a049;
transform: translateY(-2px);
}
.btn-download:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.status {
padding: 16px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
font-weight: 500;
}
.status.loading {
background: #e3f2fd;
color: #1976d2;
display: block;
}
.status.success {
background: #e8f5e9;
color: #388e3c;
display: block;
}
.status.error {
background: #ffebee;
color: #c62828;
display: block;
}
.progress-bar {
width: 100%;
height: 4px;
background: #e0e0e0;
border-radius: 2px;
overflow: hidden;
margin-top: 8px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
width: 0%;
animation: loading 2s infinite;
}
@keyframes loading {
0% { width: 0%; }
50% { width: 70%; }
100% { width: 100%; }
}
.video-container {
display: none;
margin: 30px 0;
}
.video-container.show {
display: block;
}
.video-wrapper {
position: relative;
width: 100%;
background: #000;
border-radius: 8px;
overflow: hidden;
margin-bottom: 20px;
}
video {
width: 100%;
height: auto;
display: block;
}
.video-info {
background: #f5f5f5;
padding: 16px;
border-radius: 8px;
margin-bottom: 16px;
}
.video-info p {
margin: 8px 0;
color: #666;
font-size: 0.95em;
}
.video-info strong {
color: #333;
}
.spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(102, 126, 234, 0.3);
border-top-color: #667eea;
border-radius: 50%;
animation: spin 0.8s linear infinite;
vertical-align: middle;
margin-right: 8px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.steps {
display: none;
margin: 20px 0;
padding: 20px;
background: #f9f9f9;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.steps.show {
display: block;
}
.step {
display: flex;
align-items: center;
margin: 10px 0;
font-size: 0.9em;
}
.step-number {
display: inline-block;
width: 24px;
height: 24px;
background: #667eea;
color: white;
border-radius: 50%;
text-align: center;
line-height: 24px;
margin-right: 12px;
flex-shrink: 0;
font-weight: 600;
}
.step.completed .step-number {
background: #4caf50;
}
.step-text {
color: #666;
}
.step.completed .step-text {
color: #333;
font-weight: 500;
}
@media (max-width: 600px) {
.header h1 {
font-size: 1.8em;
}
.content {
padding: 20px;
}
button {
padding: 12px 20px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎬 TrendClip</h1>
<p>AI-Powered Video Composer</p>
</div>
<div class="content">
<form id="promptForm">
<div class="form-group">
<label for="prompt">πŸ“ Describe Your Video</label>
<textarea
id="prompt"
placeholder="e.g., signs you're highly ambitious, morning routine aesthetic, productivity tips for entrepreneurs..."
required
></textarea>
</div>
<div class="button-group">
<button type="submit" class="btn-generate" id="generateBtn">
Generate Video
</button>
<button type="button" class="btn-download" id="downloadBtn" style="display: none;">
⬇️ Download Video
</button>
</div>
</form>
<div class="status loading" id="status"></div>
<div class="steps" id="steps">
<div class="step" id="step1">
<span class="step-number">1</span>
<span class="step-text">Generating manifest...</span>
</div>
<div class="step" id="step2">
<span class="step-number">2</span>
<span class="step-text">Downloading images...</span>
</div>
<div class="step" id="step3">
<span class="step-number">3</span>
<span class="step-text">Selecting best images...</span>
</div>
<div class="step" id="step4">
<span class="step-number">4</span>
<span class="step-text">Composing video...</span>
</div>
<div class="step" id="step5">
<span class="step-number">5</span>
<span class="step-text">Uploading to dataset...</span>
</div>
</div>
<div class="video-container" id="videoContainer">
<div class="video-wrapper">
<video id="videoPlayer" controls></video>
</div>
<div class="video-info" id="videoInfo">
<p><strong>Status:</strong> <span id="videoStatus">Ready</span></p>
<p><strong>Size:</strong> <span id="videoSize">-</span></p>
<p><strong>Duration:</strong> <span id="videoDuration">-</span></p>
<div id="uploadStatus" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid #ddd;"></div>
</div>
</div>
</div>
</div>
<script>
const promptForm = document.getElementById('promptForm');
const promptInput = document.getElementById('prompt');
const generateBtn = document.getElementById('generateBtn');
const downloadBtn = document.getElementById('downloadBtn');
const statusDiv = document.getElementById('status');
const stepsDiv = document.getElementById('steps');
const videoContainer = document.getElementById('videoContainer');
const videoPlayer = document.getElementById('videoPlayer');
const videoStatus = document.getElementById('videoStatus');
const videoSize = document.getElementById('videoSize');
const videoDuration = document.getElementById('videoDuration');
let videoBlob = null;
promptForm.addEventListener('submit', async (e) => {
e.preventDefault();
await generateVideo();
});
async function generateVideo() {
const prompt = promptInput.value.trim();
if (!prompt) {
showStatus('Please enter a prompt', 'error');
return;
}
// Reset UI
videoContainer.classList.remove('show');
downloadBtn.style.display = 'none';
generateBtn.disabled = true;
stepsDiv.classList.add('show');
updateAllSteps('');
showStatus(
`<span class="spinner"></span>Generating your video...`,
'loading'
);
try {
const response = await fetch('/generate-from-prompt', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ prompt })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Video generation failed');
}
// Mark steps as completed
updateSteps([1, 2, 3, 4, 5]);
// Get video blob
videoBlob = await response.blob();
// Read upload status from response headers
const uploadStatus = response.headers.get('X-Upload-Status') || 'unknown';
const uploadMessage = response.headers.get('X-Upload-Message') || '';
const datasetPath = response.headers.get('X-Dataset-Path') || '';
// Display video
const videoUrl = URL.createObjectURL(videoBlob);
videoPlayer.src = videoUrl;
videoContainer.classList.add('show');
downloadBtn.style.display = 'block';
// Update video info
const sizeMB = (videoBlob.size / (1024 * 1024)).toFixed(2);
videoSize.textContent = `${sizeMB} MB`;
videoStatus.textContent = 'Ready to download';
// Display upload status
const uploadStatusDiv = document.getElementById('uploadStatus');
if (uploadStatus === 'success') {
uploadStatusDiv.innerHTML = `
<p style="color: #388e3c; font-weight: 500;">
βœ… Uploaded to Dataset Successfully!
</p>
<p style="color: #666; font-size: 0.9em;">
πŸ“ ${datasetPath}
</p>
`;
} else if (uploadStatus === 'warning') {
uploadStatusDiv.innerHTML = `
<p style="color: #f57f17; font-weight: 500;">
⚠️ ${uploadMessage}
</p>
`;
} else if (uploadStatus === 'error') {
uploadStatusDiv.innerHTML = `
<p style="color: #c62828; font-weight: 500;">
❌ Upload Error
</p>
<p style="color: #666; font-size: 0.9em;">
${uploadMessage}
</p>
`;
}
showStatus('βœ… Video generated successfully!', 'success');
} catch (error) {
showStatus(`❌ Error: ${error.message}`, 'error');
updateAllSteps('');
} finally {
generateBtn.disabled = false;
}
}
function showStatus(message, type) {
statusDiv.innerHTML = message;
statusDiv.className = `status ${type}`;
}
function updateSteps(completedSteps) {
for (let i = 1; i <= 5; i++) {
const step = document.getElementById(`step${i}`);
if (completedSteps.includes(i)) {
step.classList.add('completed');
} else {
step.classList.remove('completed');
}
}
}
function updateAllSteps(type) {
for (let i = 1; i <= 4; i++) {
const step = document.getElementById(`step${i}`);
step.classList.remove('completed');
}
}
downloadBtn.addEventListener('click', () => {
if (!videoBlob) return;
const url = URL.createObjectURL(videoBlob);
const a = document.createElement('a');
a.href = url;
a.download = `trendclip_${Date.now()}.mp4`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
// Set initial focus
promptInput.focus();
</script>
</body>
</html>