Spaces:
Running
Running
Commit
·
495f156
1
Parent(s):
633140b
change from base64 to file upload
Browse files- app.py +52 -0
- static/script.js +52 -2
app.py
CHANGED
|
@@ -293,6 +293,58 @@ def upload_file():
|
|
| 293 |
print(f"Error in upload endpoint: {str(e)}")
|
| 294 |
return jsonify({'error': str(e)}), 500
|
| 295 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
@app.route('/health', methods=['GET'])
|
| 297 |
def health_check():
|
| 298 |
"""Health check endpoint for container monitoring"""
|
|
|
|
| 293 |
print(f"Error in upload endpoint: {str(e)}")
|
| 294 |
return jsonify({'error': str(e)}), 500
|
| 295 |
|
| 296 |
+
@app.route('/api/upload-to-fal', methods=['POST'])
|
| 297 |
+
def upload_to_fal():
|
| 298 |
+
"""Upload base64 image data to FAL storage and return the URL"""
|
| 299 |
+
try:
|
| 300 |
+
data = request.json
|
| 301 |
+
if 'image_data' not in data:
|
| 302 |
+
return jsonify({'error': 'No image data provided'}), 400
|
| 303 |
+
|
| 304 |
+
# Get API key from header or environment
|
| 305 |
+
auth_header = request.headers.get('Authorization', '')
|
| 306 |
+
if auth_header.startswith('Bearer '):
|
| 307 |
+
api_key = auth_header.replace('Bearer ', '')
|
| 308 |
+
elif os.environ.get('FAL_KEY'):
|
| 309 |
+
api_key = os.environ.get('FAL_KEY')
|
| 310 |
+
else:
|
| 311 |
+
return jsonify({'error': 'API key not provided'}), 401
|
| 312 |
+
|
| 313 |
+
# Set API key for this request
|
| 314 |
+
os.environ['FAL_KEY'] = api_key
|
| 315 |
+
|
| 316 |
+
image_data = data['image_data']
|
| 317 |
+
|
| 318 |
+
# If it's a base64 data URL, extract the actual base64 content
|
| 319 |
+
if image_data.startswith('data:'):
|
| 320 |
+
# Extract base64 content from data URL
|
| 321 |
+
header, base64_content = image_data.split(',', 1)
|
| 322 |
+
# Decode base64 to bytes
|
| 323 |
+
image_bytes = base64.b64decode(base64_content)
|
| 324 |
+
|
| 325 |
+
# Save to temporary file
|
| 326 |
+
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp_file:
|
| 327 |
+
tmp_file.write(image_bytes)
|
| 328 |
+
tmp_file_path = tmp_file.name
|
| 329 |
+
|
| 330 |
+
try:
|
| 331 |
+
# Upload to FAL using asyncio.run
|
| 332 |
+
fal_url = asyncio.run(fal_client.upload_file_async(tmp_file_path))
|
| 333 |
+
print(f"[DEBUG] Uploaded to FAL: {fal_url}")
|
| 334 |
+
return jsonify({'url': fal_url}), 200
|
| 335 |
+
finally:
|
| 336 |
+
# Clean up temporary file
|
| 337 |
+
os.unlink(tmp_file_path)
|
| 338 |
+
else:
|
| 339 |
+
# If it's already a URL, return it as-is
|
| 340 |
+
return jsonify({'url': image_data}), 200
|
| 341 |
+
|
| 342 |
+
except Exception as e:
|
| 343 |
+
print(f"Error uploading to FAL: {str(e)}")
|
| 344 |
+
import traceback
|
| 345 |
+
traceback.print_exc()
|
| 346 |
+
return jsonify({'error': str(e)}), 500
|
| 347 |
+
|
| 348 |
@app.route('/health', methods=['GET'])
|
| 349 |
def health_check():
|
| 350 |
"""Health check endpoint for container monitoring"""
|
static/script.js
CHANGED
|
@@ -368,11 +368,58 @@ function getImageSize() {
|
|
| 368 |
return size;
|
| 369 |
}
|
| 370 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
// Prepare image URLs for API
|
| 372 |
async function getImageUrlsForAPI() {
|
| 373 |
const urls = [];
|
| 374 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
|
|
|
|
| 376 |
const textUrls = imageUrls.value.trim().split('\n').filter(url => url.trim());
|
| 377 |
for (const url of textUrls) {
|
| 378 |
urls.push(url);
|
|
@@ -427,7 +474,7 @@ async function generateEdit() {
|
|
| 427 |
currentInfo.innerHTML = '';
|
| 428 |
clearLogs();
|
| 429 |
|
| 430 |
-
showStatus('
|
| 431 |
progressLogs.classList.add('active');
|
| 432 |
|
| 433 |
const requestData = {
|
|
@@ -438,6 +485,7 @@ async function generateEdit() {
|
|
| 438 |
};
|
| 439 |
|
| 440 |
if (!isTextToImage) {
|
|
|
|
| 441 |
requestData.image_urls = imageUrlsArray;
|
| 442 |
requestData.max_images = parseInt(document.getElementById('maxImages').value);
|
| 443 |
}
|
|
@@ -668,6 +716,8 @@ async function useAsInput(imageId, imageSrc) {
|
|
| 668 |
return;
|
| 669 |
}
|
| 670 |
|
|
|
|
|
|
|
| 671 |
uploadedImages.push(imageSrc);
|
| 672 |
|
| 673 |
// Get dimensions
|
|
|
|
| 368 |
return size;
|
| 369 |
}
|
| 370 |
|
| 371 |
+
// Upload image to FAL storage
|
| 372 |
+
async function uploadImageToFal(imageData, apiKey) {
|
| 373 |
+
try {
|
| 374 |
+
const response = await fetch('/api/upload-to-fal', {
|
| 375 |
+
method: 'POST',
|
| 376 |
+
headers: {
|
| 377 |
+
'Content-Type': 'application/json',
|
| 378 |
+
'Authorization': `Bearer ${apiKey}`
|
| 379 |
+
},
|
| 380 |
+
body: JSON.stringify({ image_data: imageData })
|
| 381 |
+
});
|
| 382 |
+
|
| 383 |
+
if (!response.ok) {
|
| 384 |
+
const error = await response.text();
|
| 385 |
+
throw new Error(error || 'Failed to upload image to FAL');
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
const data = await response.json();
|
| 389 |
+
return data.url;
|
| 390 |
+
} catch (error) {
|
| 391 |
+
console.error('Error uploading to FAL:', error);
|
| 392 |
+
throw error;
|
| 393 |
+
}
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
// Prepare image URLs for API
|
| 397 |
async function getImageUrlsForAPI() {
|
| 398 |
const urls = [];
|
| 399 |
+
const apiKey = getAPIKey();
|
| 400 |
+
|
| 401 |
+
// Process uploaded base64 images - upload to FAL first
|
| 402 |
+
for (let i = 0; i < uploadedImages.length; i++) {
|
| 403 |
+
const imageData = uploadedImages[i];
|
| 404 |
+
|
| 405 |
+
// If it's a base64 data URL, upload to FAL
|
| 406 |
+
if (imageData.startsWith('data:')) {
|
| 407 |
+
try {
|
| 408 |
+
addLog(`Uploading image ${i + 1} to FAL storage...`);
|
| 409 |
+
const falUrl = await uploadImageToFal(imageData, apiKey);
|
| 410 |
+
urls.push(falUrl);
|
| 411 |
+
addLog(`Image ${i + 1} uploaded successfully`);
|
| 412 |
+
} catch (error) {
|
| 413 |
+
addLog(`Failed to upload image ${i + 1}: ${error.message}`);
|
| 414 |
+
throw error;
|
| 415 |
+
}
|
| 416 |
+
} else {
|
| 417 |
+
// Already a URL, use as-is
|
| 418 |
+
urls.push(imageData);
|
| 419 |
+
}
|
| 420 |
+
}
|
| 421 |
|
| 422 |
+
// Add text URLs directly
|
| 423 |
const textUrls = imageUrls.value.trim().split('\n').filter(url => url.trim());
|
| 424 |
for (const url of textUrls) {
|
| 425 |
urls.push(url);
|
|
|
|
| 474 |
currentInfo.innerHTML = '';
|
| 475 |
clearLogs();
|
| 476 |
|
| 477 |
+
showStatus('Preparing images and connecting to FAL API...', 'info');
|
| 478 |
progressLogs.classList.add('active');
|
| 479 |
|
| 480 |
const requestData = {
|
|
|
|
| 485 |
};
|
| 486 |
|
| 487 |
if (!isTextToImage) {
|
| 488 |
+
// Note: imageUrlsArray will now contain FAL URLs after upload
|
| 489 |
requestData.image_urls = imageUrlsArray;
|
| 490 |
requestData.max_images = parseInt(document.getElementById('maxImages').value);
|
| 491 |
}
|
|
|
|
| 716 |
return;
|
| 717 |
}
|
| 718 |
|
| 719 |
+
// If the image is already a FAL URL (from history), use it directly
|
| 720 |
+
// Otherwise, it's a base64 image that will be uploaded when generating
|
| 721 |
uploadedImages.push(imageSrc);
|
| 722 |
|
| 723 |
// Get dimensions
|