fal / static /script.js
bibibi12345's picture
added seedream4.5
0baab60
// Configuration and state
let uploadedImages = [];
let imageDimensions = [];
let generationHistory = [];
let currentGeneration = null;
let activeTab = 'current';
// Initialize local storage
const HISTORY_KEY = 'seedream_generation_history';
// Load history from localStorage on startup
function loadHistory() {
try {
const saved = localStorage.getItem(HISTORY_KEY);
if (saved) {
generationHistory = JSON.parse(saved);
updateHistoryCount();
}
} catch (error) {
console.error('Error loading history:', error);
generationHistory = [];
}
}
// Save history to localStorage
function saveHistory() {
try {
// Keep only last 100 generations to avoid storage limits
if (generationHistory.length > 100) {
generationHistory = generationHistory.slice(-100);
}
localStorage.setItem(HISTORY_KEY, JSON.stringify(generationHistory));
updateHistoryCount();
} catch (error) {
console.error('Error saving history:', error);
}
}
// DOM Elements
const fileInput = document.getElementById('fileInput');
const imagePreview = document.getElementById('imagePreview');
const imageUrls = document.getElementById('imageUrls');
const generateBtn = document.getElementById('generateBtn');
const statusMessage = document.getElementById('statusMessage');
const progressLogs = document.getElementById('progressLogs');
const currentResults = document.getElementById('currentResults');
const currentInfo = document.getElementById('currentInfo');
const historyGrid = document.getElementById('historyGrid');
const imageSizeSelect = document.getElementById('imageSize');
const customSizeElements = document.querySelectorAll('.custom-size');
const modelSelect = document.getElementById('modelSelect');
const promptTitle = document.getElementById('promptTitle');
const promptLabel = document.getElementById('promptLabel');
const imageInputCard = document.getElementById('imageInputCard');
const settingsCard = document.getElementById('settingsCard');
// Event Listeners
fileInput.addEventListener('change', handleFileUpload);
generateBtn.addEventListener('click', generateEdit);
imageSizeSelect.addEventListener('change', handleImageSizeChange);
modelSelect.addEventListener('change', handleModelChange);
// Tab switching
function switchTab(tabName) {
activeTab = tabName;
// Update tab buttons
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
if (tabName === 'current') {
document.getElementById('currentTab').classList.add('active');
} else {
document.getElementById('historyTab').classList.add('active');
displayHistory();
}
}
// Toggle settings panel
function toggleSettings() {
settingsCard.classList.toggle('collapsed');
}
// Handle image size dropdown change
function handleImageSizeChange() {
if (imageSizeSelect.value === 'custom') {
customSizeElements.forEach(el => el.style.display = 'block');
} else {
customSizeElements.forEach(el => el.style.display = 'none');
}
}
// Handle model dropdown change
function handleModelChange() {
const value = modelSelect.value;
const isTextToImage = value === 'fal-ai/bytedance/seedream/v4/text-to-image' ||
value === 'fal-ai/bytedance/seedream/v4.5/text-to-image' ||
value === 'fal-ai/hunyuan-image/v3/text-to-image';
const isTextToVideo = value === 'fal-ai/bytedance/seedance/v1/pro/fast/text-to-video';
const isImageToVideo = value === 'fal-ai/bytedance/seedance/v1/pro/fast/image-to-video';
// Toggle prompt and input image area
if (isTextToImage || isTextToVideo) {
promptTitle.textContent = 'Generation Prompt';
promptLabel.textContent = 'Generation Prompt';
document.getElementById('prompt').placeholder = 'e.g., A cinematic shot of a subject, trending film look';
imageInputCard.style.display = isTextToVideo ? 'none' : 'none'; // text-to-image also hides inputs by design here
uploadedImages = [];
imageDimensions = [];
renderImagePreviews();
} else if (isImageToVideo) {
promptTitle.textContent = 'Generation Prompt';
promptLabel.textContent = 'Generation Prompt';
document.getElementById('prompt').placeholder = 'e.g., Animate this image with smooth camera motion';
imageInputCard.style.display = 'block';
} else {
promptTitle.textContent = 'Edit Instructions';
promptLabel.textContent = 'Editing Prompt';
document.getElementById('prompt').placeholder = 'e.g., Dress the model in the clothes and shoes.';
imageInputCard.style.display = 'block';
}
// Toggle Settings sections
const imageSettings = document.getElementById('imageSettings');
const videoSettings = document.getElementById('videoSettings');
if (imageSettings && videoSettings) {
if (isTextToVideo || isImageToVideo) {
imageSettings.style.display = 'none';
videoSettings.style.display = 'block';
} else {
imageSettings.style.display = 'block';
videoSettings.style.display = 'none';
}
}
}
// Initialize on page load
window.addEventListener('DOMContentLoaded', () => {
// Load saved API key
const savedKey = localStorage.getItem('fal_api_key');
if (savedKey) {
document.getElementById('apiKey').value = savedKey;
}
// Initialize UI state
handleImageSizeChange();
handleModelChange();
loadHistory();
displayHistory();
// Collapse settings by default
settingsCard.classList.add('collapsed');
});
// Handle file upload with immediate preview
async function handleFileUpload(event) {
const files = Array.from(event.target.files);
if (files.length === 0) return;
showStatus(`Processing ${files.length} image(s)...`, 'info');
let processedCount = 0;
let errorCount = 0;
for (const file of files) {
if (uploadedImages.length >= 10) {
showStatus('Maximum 10 images allowed. Some images were not added.', 'error');
break;
}
if (file.type.startsWith('image/')) {
try {
const tempIndex = uploadedImages.length;
const loadingId = `loading-${Date.now()}-${tempIndex}`;
// Add loading placeholder
const loadingPreview = document.createElement('div');
loadingPreview.className = 'image-preview-item loading-preview';
loadingPreview.id = loadingId;
loadingPreview.innerHTML = `
<div class="loading-placeholder">
<div class="spinner"></div>
<p>${file.name}</p>
</div>
`;
imagePreview.appendChild(loadingPreview);
const reader = new FileReader();
reader.onerror = (error) => {
console.error('Error reading file:', file.name, error);
errorCount++;
document.getElementById(loadingId)?.remove();
showStatus(`Failed to read file: ${file.name}`, 'error');
};
reader.onload = (e) => {
const dataUrl = e.target.result;
document.getElementById(loadingId)?.remove();
uploadedImages.push(dataUrl);
processedCount++;
// Get image dimensions
const img = new Image();
img.onload = function() {
imageDimensions.push({
width: this.width,
height: this.height
});
addImagePreview(dataUrl, uploadedImages.length - 1);
updateCustomSizeFromLastImage();
if (processedCount + errorCount === files.length) {
if (errorCount === 0) {
showStatus(`Successfully added ${processedCount} image(s) (${uploadedImages.length}/10 slots used)`, 'success');
} else {
showStatus(`Added ${processedCount} image(s), ${errorCount} failed (${uploadedImages.length}/10 slots used)`, 'warning');
}
}
};
img.onerror = () => {
console.error('Error loading image dimensions for:', file.name);
imageDimensions.push({ width: 1280, height: 1280 });
addImagePreview(dataUrl, uploadedImages.length - 1);
updateCustomSizeFromLastImage();
};
img.src = dataUrl;
};
reader.readAsDataURL(file);
} catch (error) {
console.error('Error processing file:', file.name, error);
errorCount++;
showStatus(`Error processing ${file.name}`, 'error');
}
} else {
errorCount++;
showStatus(`${file.name} is not an image file`, 'error');
}
}
event.target.value = '';
}
// Add image preview with upload status indicator
function addImagePreview(src, index) {
const previewItem = document.createElement('div');
previewItem.className = 'image-preview-item';
previewItem.dataset.imageIndex = index;
const imageId = `upload-img-${Date.now()}-${index}`;
previewItem.innerHTML = `
<img id="${imageId}" src="${src}" alt="Upload ${index + 1}"
onclick="openImageModal('${imageId}', '${src}', 'Uploaded Image ${index + 1}', 'Input Image')"
style="cursor: pointer;">
<button class="remove-btn" onclick="removeImage(${index})">×</button>
<div class="image-upload-status" style="display: none;">
<div class="upload-progress-mini">
<div class="upload-progress-mini-bar"></div>
</div>
<span class="upload-status-text"></span>
</div>
`;
imagePreview.appendChild(previewItem);
}
// Remove image
function removeImage(index) {
uploadedImages.splice(index, 1);
imageDimensions.splice(index, 1);
renderImagePreviews();
updateCustomSizeFromLastImage();
}
// Update custom size based on last image
function updateCustomSizeFromLastImage() {
if (imageDimensions.length > 0) {
const lastDims = imageDimensions[imageDimensions.length - 1];
let width = lastDims.width;
let height = lastDims.height;
// Calculate aspect ratio
const aspectRatio = width / height;
// If both dimensions are less than 1024 (minimum allowed), scale up proportionally
if (width < 1024 && height < 1024) {
// Scale based on which dimension needs more scaling to reach 1024
if (width < height) {
// Height is larger, scale based on width minimum
width = 1024;
height = Math.round(1024 / aspectRatio);
} else {
// Width is larger or equal, scale based on height minimum
height = 1024;
width = Math.round(1024 * aspectRatio);
}
}
// If any dimension exceeds 4096, scale down proportionally
if (width > 4096 || height > 4096) {
if (width > height) {
// Width exceeds more, scale based on width maximum
const scaleFactor = 4096 / width;
width = 4096;
height = Math.round(height * scaleFactor);
} else {
// Height exceeds more, scale based on height maximum
const scaleFactor = 4096 / height;
height = 4096;
width = Math.round(width * scaleFactor);
}
}
// Final bounds check while maintaining aspect ratio
// If width is still below minimum after height-based scaling
if (width < 1024) {
const scaleFactor = 1024 / width;
width = 1024;
height = Math.round(height * scaleFactor);
}
// If height is still below minimum after width-based scaling
if (height < 1024) {
const scaleFactor = 1024 / height;
height = 1024;
width = Math.round(width * scaleFactor);
}
// Final check to ensure we don't exceed maximums
width = Math.min(4096, width);
height = Math.min(4096, height);
document.getElementById('customWidth').value = width;
document.getElementById('customHeight').value = height;
if (imageSizeSelect.value !== 'custom') {
imageSizeSelect.value = 'custom';
handleImageSizeChange();
}
}
}
// Re-render all image previews
function renderImagePreviews() {
imagePreview.innerHTML = '';
uploadedImages.forEach((src, index) => {
addImagePreview(src, index);
});
}
// Show status message
function showStatus(message, type = 'info') {
statusMessage.className = `status-message ${type}`;
statusMessage.textContent = message;
statusMessage.style.display = 'block';
if (type === 'success' || type === 'error') {
setTimeout(() => {
statusMessage.style.display = 'none';
}, 5000);
}
}
// Add log entry
function addLog(message) {
const logEntry = document.createElement('div');
logEntry.className = 'log-entry';
logEntry.textContent = `${new Date().toLocaleTimeString()}: ${message}`;
progressLogs.appendChild(logEntry);
progressLogs.scrollTop = progressLogs.scrollHeight;
}
// Clear logs
function clearLogs() {
progressLogs.innerHTML = '';
progressLogs.classList.remove('active');
}
// Get image size configuration
function getImageSize() {
const size = imageSizeSelect.value;
if (size === 'custom') {
return {
width: parseInt(document.getElementById('customWidth').value),
height: parseInt(document.getElementById('customHeight').value)
};
}
return size;
}
// Upload image to FAL storage with progress tracking
async function uploadImageToFal(imageData, apiKey, imageIndex, totalImages, actualIndex) {
try {
// Show individual image upload status if preview exists
const previewItem = document.querySelector(`.image-preview-item[data-image-index="${actualIndex}"]`);
if (previewItem) {
const statusDiv = previewItem.querySelector('.image-upload-status');
const statusText = previewItem.querySelector('.upload-status-text');
const progressBar = previewItem.querySelector('.upload-progress-mini-bar');
if (statusDiv) {
statusDiv.style.display = 'block';
statusText.textContent = 'Uploading...';
progressBar.style.width = '30%';
previewItem.classList.add('uploading');
}
}
// Update main progress bar
updateUploadProgress(imageIndex - 1, totalImages, `Uploading image ${imageIndex}/${totalImages}...`);
// Show upload start message
addLog(`Uploading image ${imageIndex}/${totalImages} to FAL storage...`);
// Calculate approximate size for logging
const sizeInMB = (imageData.length * 0.75 / 1024 / 1024).toFixed(2);
addLog(`Image ${imageIndex} size: ~${sizeInMB} MB`);
const response = await fetch('/api/upload-to-fal', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({ image_data: imageData })
});
if (!response.ok) {
const error = await response.text();
throw new Error(error || 'Failed to upload image to FAL');
}
const data = await response.json();
addLog(`✓ Image ${imageIndex}/${totalImages} uploaded successfully`);
// Update progress after successful upload
updateUploadProgress(imageIndex, totalImages, `Completed ${imageIndex}/${totalImages}`);
// Update individual image status for success
if (previewItem) {
const statusText = previewItem.querySelector('.upload-status-text');
const progressBar = previewItem.querySelector('.upload-progress-mini-bar');
if (progressBar && statusText) {
progressBar.style.width = '100%';
statusText.textContent = 'Uploaded ✓';
previewItem.classList.remove('uploading');
previewItem.classList.add('uploaded');
// Fade out status after 3 seconds
setTimeout(() => {
const statusDiv = previewItem.querySelector('.image-upload-status');
if (statusDiv) {
statusDiv.style.opacity = '0';
setTimeout(() => {
statusDiv.style.display = 'none';
statusDiv.style.opacity = '1';
previewItem.classList.remove('uploaded');
}, 300);
}
}, 3000);
}
}
return data.url;
} catch (error) {
console.error('Error uploading to FAL:', error);
addLog(`✗ Failed to upload image ${imageIndex}: ${error.message}`);
// Update individual image status for error
const previewItem = document.querySelector(`.image-preview-item[data-image-index="${actualIndex}"]`);
if (previewItem) {
const statusText = previewItem.querySelector('.upload-status-text');
const progressBar = previewItem.querySelector('.upload-progress-mini-bar');
if (progressBar && statusText) {
progressBar.style.width = '100%';
progressBar.style.backgroundColor = '#dc3545';
statusText.textContent = 'Upload failed ✗';
previewItem.classList.remove('uploading');
previewItem.classList.add('upload-failed');
}
}
throw error;
}
}
// Update upload progress bar
function updateUploadProgress(completed, total, message) {
const progressFill = document.getElementById('uploadProgressFill');
const progressText = document.getElementById('uploadProgressText');
const container = document.getElementById('uploadProgressContainer');
if (progressFill && progressText && container) {
const percentage = Math.round((completed / total) * 100);
progressFill.style.width = `${percentage}%`;
progressText.textContent = `${message} (${percentage}%)`;
// Add animation class on completion
if (percentage === 100) {
progressFill.classList.add('complete');
// Wait briefly to show completion state, then fade out
setTimeout(() => {
// Add transition for smooth fade
container.style.transition = 'opacity 0.5s ease';
container.style.opacity = '0';
// Remove from DOM after fade completes
setTimeout(() => {
if (container.parentNode) {
container.parentNode.removeChild(container);
}
}, 500);
}, 1500);
} else {
// Ensure visible if not complete
container.style.opacity = '1';
}
}
}
// Prepare image URLs for API with detailed progress tracking
async function getImageUrlsForAPI() {
const urls = [];
const apiKey = getAPIKey();
// Count total images to upload
const base64Images = uploadedImages.filter(img => img.startsWith('data:'));
const urlImages = uploadedImages.filter(img => !img.startsWith('data:'));
const textUrls = imageUrls.value.trim().split('\n').filter(url => url.trim());
const totalUploads = base64Images.length;
const totalImages = uploadedImages.length + textUrls.length;
if (totalUploads > 0) {
addLog(`Preparing to upload ${totalUploads} image(s) to FAL storage...`);
showStatus(`Uploading ${totalUploads} image(s) to FAL storage...`, 'info');
}
// Process uploaded base64 images - upload to FAL first
let uploadCount = 0;
for (let i = 0; i < uploadedImages.length; i++) {
const imageData = uploadedImages[i];
// If it's a base64 data URL, upload to FAL
if (imageData.startsWith('data:')) {
uploadCount++;
try {
const falUrl = await uploadImageToFal(imageData, apiKey, uploadCount, totalUploads, i);
urls.push(falUrl);
// Update progress status
if (uploadCount < totalUploads) {
const percentage = Math.round((uploadCount / totalUploads) * 100);
showStatus(`Upload progress: ${uploadCount}/${totalUploads} (${percentage}%)`, 'info');
}
} catch (error) {
showStatus(`Upload failed for image ${uploadCount}: ${error.message}`, 'error');
throw error;
}
} else {
// Already a URL, use as-is
urls.push(imageData);
addLog(`Using existing URL for image ${i + 1}`);
}
}
// Add text URLs directly
if (textUrls.length > 0) {
addLog(`Processing ${textUrls.length} URL(s) from text input...`);
}
for (const url of textUrls) {
urls.push(url);
addLog(`Added URL: ${url.substring(0, 50)}...`);
await getImageDimensionsFromUrl(url);
}
if (totalUploads > 0) {
showStatus(`All ${totalUploads} image(s) uploaded successfully!`, 'success');
addLog(`Upload complete: ${totalImages} total image(s) ready for generation`);
}
return urls.slice(0, 10);
}
// Get image dimensions from URL
async function getImageDimensionsFromUrl(url) {
return new Promise((resolve) => {
const img = new Image();
img.onload = function() {
imageDimensions.push({
width: this.width,
height: this.height
});
updateCustomSizeFromLastImage();
resolve();
};
img.onerror = function() {
resolve();
};
img.src = url;
});
}
// Generate edit
async function generateEdit() {
const prompt = document.getElementById('prompt').value.trim();
if (!prompt) {
showStatus('Please enter a prompt', 'error');
return;
}
const selectedModel = modelSelect.value;
const isTextToImage = selectedModel === 'fal-ai/bytedance/seedream/v4/text-to-image' ||
selectedModel === 'fal-ai/bytedance/seedream/v4.5/text-to-image' ||
selectedModel === 'fal-ai/hunyuan-image/v3/text-to-image';
const isTextToVideo = selectedModel === 'fal-ai/bytedance/seedance/v1/pro/fast/text-to-video';
const isImageToVideo = selectedModel === 'fal-ai/bytedance/seedance/v1/pro/fast/image-to-video';
// Determine if current model requires image inputs
const needsImageInputs = (!isTextToImage && !isTextToVideo);
let imageUrlsArray = [];
if (needsImageInputs) {
// Prepare upload progress UI early if there are base64 uploads
const base64Images = uploadedImages.filter(img => img.startsWith('data:'));
const totalUploads = base64Images.length;
// Remove any existing progress container
const existingProgress = document.getElementById('uploadProgressContainer');
if (existingProgress && existingProgress.parentNode) {
existingProgress.parentNode.removeChild(existingProgress);
}
if (totalUploads > 0) {
const progressContainer = document.createElement('div');
progressContainer.id = 'uploadProgressContainer';
progressContainer.className = 'upload-progress-container';
progressContainer.innerHTML = `
<div class="upload-progress-bar">
<div class="upload-progress-fill" id="uploadProgressFill"></div>
</div>
<div class="upload-progress-text" id="uploadProgressText">Initializing...</div>
`;
// Insert progress container after status message
if (statusMessage.parentNode) {
statusMessage.parentNode.insertBefore(progressContainer, statusMessage.nextSibling);
}
}
// Now resolve image URLs (this may trigger uploads and progress updates)
try {
imageUrlsArray = await getImageUrlsForAPI();
} catch (error) {
// Ensure progress UI is removed on upload failure
const pc = document.getElementById('uploadProgressContainer');
if (pc && pc.parentNode) {
pc.parentNode.removeChild(pc);
}
addLog(`Upload error: ${error.message || error}`);
showStatus(`Upload error: ${error.message || error}`, 'error');
return;
}
// Validate inputs for specific modes
if (isImageToVideo && imageUrlsArray.length === 0) {
showStatus('Please upload an image or provide an image URL for image-to-video', 'error');
const pc = document.getElementById('uploadProgressContainer');
if (pc && pc.parentNode) pc.parentNode.removeChild(pc);
return;
} else if (!isImageToVideo && imageUrlsArray.length === 0) {
showStatus('Please upload images or provide image URLs for image editing', 'error');
const pc = document.getElementById('uploadProgressContainer');
if (pc && pc.parentNode) pc.parentNode.removeChild(pc);
return;
}
}
generateBtn.disabled = true;
generateBtn.querySelector('.btn-text').textContent = 'Generating...';
generateBtn.querySelector('.spinner').style.display = 'block';
// Clear current results
currentResults.innerHTML = '<div class="empty-state"><p>Preparing generation...</p></div>';
currentInfo.innerHTML = '';
clearLogs();
showStatus('Starting generation process...', 'info');
progressLogs.classList.add('active');
// Show initial status
if (!isTextToImage && imageUrlsArray.length > 0) {
addLog(`Processing ${imageUrlsArray.length} input image(s)...`);
}
let requestData;
if (isTextToVideo) {
requestData = {
prompt: prompt,
aspect_ratio: (document.getElementById('videoAspectRatio')?.value) || 'auto',
resolution: (document.getElementById('videoResolution')?.value) || '1080p',
duration: (document.getElementById('videoDuration')?.value) || '5',
camera_fixed: !!(document.getElementById('cameraFixed')?.checked),
enable_safety_checker: false
};
} else if (isImageToVideo) {
requestData = {
prompt: prompt,
image_url: imageUrlsArray[0],
aspect_ratio: (document.getElementById('videoAspectRatio')?.value) || 'auto',
resolution: (document.getElementById('videoResolution')?.value) || '1080p',
duration: (document.getElementById('videoDuration')?.value) || '5',
camera_fixed: !!(document.getElementById('cameraFixed')?.checked),
enable_safety_checker: false
};
} else {
requestData = {
prompt: prompt,
image_size: getImageSize(),
num_images: parseInt(document.getElementById('numImages').value),
enable_safety_checker: false
};
if (!isTextToImage) {
// Note: imageUrlsArray will now contain FAL URLs after upload
requestData.image_urls = imageUrlsArray;
requestData.max_images = parseInt(document.getElementById('maxImages').value);
}
}
const seed = document.getElementById('seed').value;
if (seed) {
requestData.seed = parseInt(seed);
}
// Store generation metadata
currentGeneration = {
id: Date.now(),
timestamp: new Date().toISOString(),
prompt: prompt,
model: selectedModel,
settings: {
image_size: requestData.image_size,
num_images: requestData.num_images,
seed: requestData.seed
}
};
try {
const apiKey = getAPIKey();
if (!apiKey) {
showStatus('Please enter your FAL API key', 'error');
addLog('API key not found');
document.getElementById('apiKey').focus();
return;
}
addLog('Submitting request to FAL API...');
addLog(`Model: ${selectedModel}`);
addLog(`Prompt: ${prompt}`);
if (!isTextToImage && !isTextToVideo) {
addLog(`Number of input images: ${imageUrlsArray.length}`);
}
const response = await callFalAPI(apiKey, requestData, selectedModel);
// Store results in current generation
currentGeneration.results = response;
// Display results
displayCurrentResults(response);
// Add to history
generationHistory.push(currentGeneration);
saveHistory();
showStatus('Generation completed successfully!', 'success');
} catch (error) {
console.error('Error:', error);
showStatus(`Error: ${error.message}`, 'error');
addLog(`Error: ${error.message}`);
} finally {
generateBtn.disabled = false;
generateBtn.querySelector('.btn-text').textContent = 'Generate';
generateBtn.querySelector('.spinner').style.display = 'none';
// Ensure any lingering upload progress UI is removed
const pc2 = document.getElementById('uploadProgressContainer');
if (pc2 && pc2.parentNode) {
pc2.parentNode.removeChild(pc2);
}
}
}
// Get API key
function getAPIKey() {
const apiKeyInput = document.getElementById('apiKey');
const apiKey = apiKeyInput.value.trim();
if (apiKey) {
localStorage.setItem('fal_api_key', apiKey);
}
return apiKey || localStorage.getItem('fal_api_key');
}
// Call FAL API (non-blocking)
async function callFalAPI(apiKey, requestData, model) {
const submitResponse = await fetch('/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
'X-Model-Endpoint': model
},
body: JSON.stringify(requestData)
});
if (!submitResponse.ok) {
const error = await submitResponse.text();
throw new Error(error || 'API request failed');
}
const submitData = await submitResponse.json();
const { request_id } = submitData;
addLog(`Request submitted with ID: ${request_id}`);
// Poll for results
let attempts = 0;
const isVideoModel = model.includes('text-to-video') || model.includes('image-to-video');
const maxAttempts = isVideoModel ? 900 : 120;
const pollInterval = 1000;
let previousLogCount = 0;
while (attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, pollInterval));
const statusUrl = `/api/status/${request_id}`;
const statusResponse = await fetch(statusUrl);
if (!statusResponse.ok) {
throw new Error('Failed to check request status');
}
const statusData = await statusResponse.json();
// Add any new logs
if (statusData.logs && statusData.logs.length > previousLogCount) {
const newLogs = statusData.logs.slice(previousLogCount);
newLogs.forEach(log => {
if (log && !log.includes('Request submitted')) {
addLog(log);
}
});
previousLogCount = statusData.logs.length;
}
if (statusData.status === 'completed') {
return statusData.result;
} else if (statusData.status === 'error') {
throw new Error(statusData.error || 'Generation failed');
}
attempts++;
if (attempts % 5 === 0) {
addLog(`Processing... (${attempts}s elapsed)`);
}
}
throw new Error('Request timed out');
}
// Display current results
function displayCurrentResults(response) {
currentResults.innerHTML = '';
let displayed = false;
// Images
if (response && response.images && response.images.length > 0) {
response.images.forEach((image, index) => {
const imgSrc = image.url || image.file_data || '';
const imageId = `current-img-${Date.now()}-${index}`;
const item = document.createElement('div');
item.className = 'generation-item';
item.innerHTML = `
<img id="${imageId}" src="${imgSrc}" alt="Result ${index + 1}">
<button class="use-as-input-btn" onclick="useAsInput('${imageId}', '${imgSrc}')" title="Use as input">
↻ Use as Input
</button>
`;
currentResults.appendChild(item);
});
displayed = true;
}
// Video
if (response && response.video) {
const vidSrc = response.video.url || response.video.file_data || '';
if (vidSrc) {
const item = document.createElement('div');
item.className = 'generation-item';
item.innerHTML = `
<video src="${vidSrc}" controls style="max-width: 100%; border-radius: 8px;"></video>
`;
currentResults.appendChild(item);
displayed = true;
}
}
// Display generation info
if (response && response.seed) {
currentInfo.innerHTML = `<strong>Seed:</strong> ${response.seed}`;
addLog(`Seed used: ${response.seed}`);
}
if (!displayed) {
currentResults.innerHTML = '<div class="empty-state"><p>No results</p></div>';
} else {
if (response.images && response.images.length) {
addLog(`Generated ${response.images.length} image(s)`);
}
if (response.video) {
addLog('Generated 1 video');
}
}
}
// Display history
function displayHistory() {
if (generationHistory.length === 0) {
historyGrid.innerHTML = `
<div class="empty-state">
<p>No generation history</p>
<small>Your generated content will be saved here</small>
</div>
`;
return;
}
historyGrid.innerHTML = '';
// Display history in reverse order (newest first)
[...generationHistory].reverse().forEach((generation) => {
if (!generation.results) return;
// Images
if (generation.results.images) {
generation.results.images.forEach((image, imgIndex) => {
const imgSrc = image.url || image.file_data || '';
const imageId = `history-img-${generation.id}-${imgIndex}`;
const item = document.createElement('div');
item.className = 'generation-item';
item.innerHTML = `
<img id="${imageId}" src="${imgSrc}" alt="Generation"
onclick="openImageModal('${imageId}', '${imgSrc}', '${generation.prompt.replace(/'/g, "\\'")}', '${new Date(generation.timestamp).toLocaleString()}')">
<button class="use-as-input-btn" onclick="useAsInput('${imageId}', '${imgSrc}')" title="Use as input">
↻ Use as Input
</button>
<div class="generation-meta">
<span class="timestamp">${new Date(generation.timestamp).toLocaleString()}</span>
<span class="prompt-preview">${generation.prompt}</span>
</div>
`;
historyGrid.appendChild(item);
});
}
// Video
if (generation.results.video) {
const vidSrc = generation.results.video.url || generation.results.video.file_data || '';
if (vidSrc) {
const item = document.createElement('div');
item.className = 'generation-item';
item.innerHTML = `
<video src="${vidSrc}" controls style="max-width: 100%; border-radius: 8px;"></video>
<div class="generation-meta">
<span class="timestamp">${new Date(generation.timestamp).toLocaleString()}</span>
<span class="prompt-preview">${generation.prompt}</span>
</div>
`;
historyGrid.appendChild(item);
}
}
});
}
// Use image as input
async function useAsInput(imageId, imageSrc) {
try {
// Switch to edit mode if in text-to-image mode
const currentModel = modelSelect.value;
if (currentModel === 'fal-ai/bytedance/seedream/v4/text-to-image' ||
currentModel === 'fal-ai/bytedance/seedream/v4.5/text-to-image' ||
currentModel === 'fal-ai/hunyuan-image/v3/text-to-image') {
modelSelect.value = 'fal-ai/bytedance/seedream/v4.5/edit';
handleModelChange();
showStatus('Switched to Image Edit mode', 'info');
}
if (uploadedImages.length >= 10) {
showStatus('Maximum 10 images allowed. Please remove some images first.', 'error');
return;
}
// If the image is already a FAL URL (from history), use it directly
// Otherwise, it's a base64 image that will be uploaded when generating
uploadedImages.push(imageSrc);
// Get dimensions
const imgElement = document.getElementById(imageId);
if (imgElement) {
if (!imgElement.complete) {
await new Promise((resolve) => {
imgElement.onload = resolve;
imgElement.onerror = resolve;
});
}
imageDimensions.push({
width: imgElement.naturalWidth || imgElement.width,
height: imgElement.naturalHeight || imgElement.height
});
updateCustomSizeFromLastImage();
} else {
const img = new Image();
img.onload = function() {
imageDimensions.push({
width: this.width,
height: this.height
});
updateCustomSizeFromLastImage();
};
img.onerror = function() {
imageDimensions.push({ width: 1280, height: 1280 });
updateCustomSizeFromLastImage();
};
img.src = imageSrc;
}
renderImagePreviews();
const totalImages = uploadedImages.length;
showStatus(`Image added as input (${totalImages}/10 slots used)`, 'success');
addLog(`Added image as input (${totalImages}/10 images)`);
// Flash animation
imagePreview.style.animation = 'flash 0.5s';
setTimeout(() => {
imagePreview.style.animation = '';
}, 500);
} catch (error) {
console.error('Error using image as input:', error);
showStatus('Failed to add image as input', 'error');
}
}
// Clear all input images
function clearAllInputImages() {
uploadedImages = [];
imageDimensions = [];
renderImagePreviews();
showStatus('All input images cleared', 'info');
}
// Clear history
function clearHistory() {
if (confirm('Are you sure you want to clear all generation history? This cannot be undone.')) {
generationHistory = [];
localStorage.removeItem(HISTORY_KEY);
displayHistory();
updateHistoryCount();
showStatus('History cleared', 'info');
}
}
// Download all history
function downloadAllHistory() {
if (generationHistory.length === 0) {
showStatus('No history to download', 'error');
return;
}
// Create a zip file or download each asset
generationHistory.forEach((generation, genIndex) => {
if (generation.results) {
if (generation.results.images) {
generation.results.images.forEach((image, imgIndex) => {
const imgSrc = image.url || image.file_data || '';
if (imgSrc) {
const link = document.createElement('a');
link.href = imgSrc;
link.download = `seedream-${generation.id}-${imgIndex}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
});
}
if (generation.results.video) {
const vidSrc = generation.results.video.url || generation.results.video.file_data || '';
if (vidSrc) {
const link = document.createElement('a');
link.href = vidSrc;
link.download = `seedance-${generation.id}.mp4`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
}
});
showStatus('Downloading all assets...', 'info');
}
// Update history count
function updateHistoryCount() {
const countElement = document.getElementById('historyCount');
if (countElement) {
let totalItems = 0;
generationHistory.forEach(gen => {
if (gen.results && gen.results.images) {
totalItems += gen.results.images.length;
}
if (gen.results && gen.results.video) {
totalItems += 1;
}
});
countElement.textContent = totalItems;
}
}
// Modal functionality for viewing large images
let currentModalImage = null;
function openImageModal(imageId, imageSrc, prompt, timestamp) {
const modal = document.getElementById('imageModal');
const modalImg = document.getElementById('modalImage');
const modalCaption = document.getElementById('modalCaption');
// Store current image info for use in "Use as Input"
currentModalImage = { id: imageId, src: imageSrc };
// Set modal content
modalImg.src = imageSrc;
modalCaption.innerHTML = `
<strong>Generated:</strong> ${timestamp}<br>
<strong>Prompt:</strong> ${prompt}
`;
// Show modal
modal.classList.add('show');
// Prevent body scroll when modal is open
document.body.style.overflow = 'hidden';
// Close modal on escape key
document.addEventListener('keydown', handleModalEscape);
// Close modal on clicking outside the image
modal.onclick = function(event) {
if (event.target === modal || event.target === modalImg.parentElement) {
closeImageModal();
}
};
}
function closeImageModal() {
const modal = document.getElementById('imageModal');
modal.classList.remove('show');
document.body.style.overflow = '';
document.removeEventListener('keydown', handleModalEscape);
currentModalImage = null;
}
function handleModalEscape(event) {
if (event.key === 'Escape') {
closeImageModal();
}
}
function useModalImageAsInput() {
if (currentModalImage) {
useAsInput(currentModalImage.id, currentModalImage.src);
closeImageModal();
}
}