Commit ·
c5f0b8c
0
Parent(s):
initial test
Browse files- .gitignore +63 -0
- Dockerfile +40 -0
- README.md +184 -0
- app.py +157 -0
- requirements.txt +5 -0
- static/script.js +387 -0
- static/style.css +343 -0
- templates/index.html +126 -0
.gitignore
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
build/
|
| 8 |
+
develop-eggs/
|
| 9 |
+
dist/
|
| 10 |
+
downloads/
|
| 11 |
+
eggs/
|
| 12 |
+
.eggs/
|
| 13 |
+
lib/
|
| 14 |
+
lib64/
|
| 15 |
+
parts/
|
| 16 |
+
sdist/
|
| 17 |
+
var/
|
| 18 |
+
wheels/
|
| 19 |
+
share/python-wheels/
|
| 20 |
+
*.egg-info/
|
| 21 |
+
.installed.cfg
|
| 22 |
+
*.egg
|
| 23 |
+
MANIFEST
|
| 24 |
+
|
| 25 |
+
# Virtual environments
|
| 26 |
+
venv/
|
| 27 |
+
ENV/
|
| 28 |
+
env/
|
| 29 |
+
.venv
|
| 30 |
+
|
| 31 |
+
# Environment variables
|
| 32 |
+
.env
|
| 33 |
+
.env.local
|
| 34 |
+
|
| 35 |
+
# IDE
|
| 36 |
+
.vscode/
|
| 37 |
+
.idea/
|
| 38 |
+
*.swp
|
| 39 |
+
*.swo
|
| 40 |
+
*~
|
| 41 |
+
.DS_Store
|
| 42 |
+
|
| 43 |
+
# Docker
|
| 44 |
+
*.log
|
| 45 |
+
.dockerignore
|
| 46 |
+
|
| 47 |
+
# Temporary files
|
| 48 |
+
*.tmp
|
| 49 |
+
*.temp
|
| 50 |
+
tmp/
|
| 51 |
+
temp/
|
| 52 |
+
|
| 53 |
+
# API keys and secrets
|
| 54 |
+
secrets.txt
|
| 55 |
+
*.key
|
| 56 |
+
api_keys.json
|
| 57 |
+
|
| 58 |
+
# Test files
|
| 59 |
+
test_*.py
|
| 60 |
+
*_test.py
|
| 61 |
+
|
| 62 |
+
# Documentation source
|
| 63 |
+
faldoc.txt
|
Dockerfile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use Python 3.10 slim image as base
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# Set working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Set environment variables
|
| 8 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 9 |
+
PYTHONDONTWRITEBYTECODE=1 \
|
| 10 |
+
PORT=7860
|
| 11 |
+
|
| 12 |
+
# Install system dependencies
|
| 13 |
+
RUN apt-get update && apt-get install -y \
|
| 14 |
+
gcc \
|
| 15 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 16 |
+
|
| 17 |
+
# Copy requirements file
|
| 18 |
+
COPY requirements.txt .
|
| 19 |
+
|
| 20 |
+
# Install Python dependencies
|
| 21 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 22 |
+
|
| 23 |
+
# Copy application files
|
| 24 |
+
COPY app.py .
|
| 25 |
+
COPY templates/ ./templates/
|
| 26 |
+
COPY static/ ./static/
|
| 27 |
+
|
| 28 |
+
# Create a non-root user
|
| 29 |
+
RUN useradd -m -u 1000 user && chown -R user:user /app
|
| 30 |
+
USER user
|
| 31 |
+
|
| 32 |
+
# Expose the port (Hugging Face default)
|
| 33 |
+
EXPOSE 7860
|
| 34 |
+
|
| 35 |
+
# Health check
|
| 36 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 37 |
+
CMD python -c "import requests; requests.get('http://localhost:7860/health')" || exit 1
|
| 38 |
+
|
| 39 |
+
# Run the application
|
| 40 |
+
CMD ["python", "app.py"]
|
README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: SeedDream v4 Edit
|
| 3 |
+
emoji: 🎨
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: pink
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# SeedDream v4 - AI Image Generator & Editor
|
| 13 |
+
|
| 14 |
+
A web-based interface for AI-powered image generation and editing using ByteDance's SeedDream v4 models via the FAL API. This application is containerized and ready for deployment on Hugging Face Spaces.
|
| 15 |
+
|
| 16 |
+
## Features
|
| 17 |
+
|
| 18 |
+
- 🎨 **Dual Mode**: Switch between Image Editing and Text-to-Image generation
|
| 19 |
+
- 📤 Support for multiple image uploads (up to 10 images for editing)
|
| 20 |
+
- 🔗 URL-based image input support
|
| 21 |
+
- ⚙️ Customizable generation settings with smart dimension detection
|
| 22 |
+
- 🔒 Safety checker disabled for unrestricted creativity
|
| 23 |
+
- 🐳 Docker containerized for easy deployment
|
| 24 |
+
- 🚀 Hugging Face Spaces compatible
|
| 25 |
+
|
| 26 |
+
## Quick Start
|
| 27 |
+
|
| 28 |
+
### Local Development
|
| 29 |
+
|
| 30 |
+
1. **Clone the repository**
|
| 31 |
+
```bash
|
| 32 |
+
git clone <your-repo-url>
|
| 33 |
+
cd fal_ui
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
2. **Install dependencies**
|
| 37 |
+
```bash
|
| 38 |
+
# Install Python dependencies
|
| 39 |
+
pip install -r requirements.txt
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
3. **Run the application**
|
| 43 |
+
```bash
|
| 44 |
+
python app.py
|
| 45 |
+
```
|
| 46 |
+
The app will be available at `http://localhost:7860`
|
| 47 |
+
|
| 48 |
+
### Docker Deployment
|
| 49 |
+
|
| 50 |
+
1. **Build the Docker image**
|
| 51 |
+
```bash
|
| 52 |
+
docker build -t seedream-editor .
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
2. **Run the container**
|
| 56 |
+
```bash
|
| 57 |
+
docker run -p 7860:7860 seedream-editor
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
## Deployment on Hugging Face Spaces
|
| 61 |
+
|
| 62 |
+
### Method 1: Direct Deployment
|
| 63 |
+
|
| 64 |
+
1. Go to [Hugging Face Spaces](https://huggingface.co/spaces)
|
| 65 |
+
2. Click "Create new Space"
|
| 66 |
+
3. Choose "Docker" as the SDK
|
| 67 |
+
4. Upload all project files
|
| 68 |
+
5. The application is ready to deploy - users will enter their API key directly in the interface
|
| 69 |
+
|
| 70 |
+
### Method 2: Using Git
|
| 71 |
+
|
| 72 |
+
1. Create a new Space on Hugging Face
|
| 73 |
+
2. Clone your space locally:
|
| 74 |
+
```bash
|
| 75 |
+
git clone https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
|
| 76 |
+
cd YOUR_SPACE_NAME
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
3. Copy all project files to the space directory
|
| 80 |
+
4. Create a `.gitignore` file:
|
| 81 |
+
```
|
| 82 |
+
__pycache__/
|
| 83 |
+
*.pyc
|
| 84 |
+
.env
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
5. Push to Hugging Face:
|
| 88 |
+
```bash
|
| 89 |
+
git add .
|
| 90 |
+
git commit -m "Initial commit"
|
| 91 |
+
git push
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
## Configuration
|
| 95 |
+
|
| 96 |
+
### API Key
|
| 97 |
+
|
| 98 |
+
The FAL API key is entered directly in the web interface:
|
| 99 |
+
- Enter your API key in the "API Configuration" section at the top of the page
|
| 100 |
+
- The key is stored locally in your browser (localStorage)
|
| 101 |
+
- Get your API key from [fal.ai](https://fal.ai)
|
| 102 |
+
|
| 103 |
+
### Model Selection
|
| 104 |
+
|
| 105 |
+
Choose between two powerful models:
|
| 106 |
+
- **Image Edit Mode**: `fal-ai/bytedance/seedream/v4/edit` - Edit existing images with prompts
|
| 107 |
+
- **Text-to-Image Mode**: `fal-ai/bytedance/seedream/v4/text-to-image` - Generate new images from text
|
| 108 |
+
|
| 109 |
+
### Environment Variables
|
| 110 |
+
|
| 111 |
+
- `PORT`: Port number (default: 7860)
|
| 112 |
+
- `SPACE_ID`: Automatically set by Hugging Face Spaces
|
| 113 |
+
|
| 114 |
+
## Project Structure
|
| 115 |
+
|
| 116 |
+
```
|
| 117 |
+
fal_ui/
|
| 118 |
+
├── app.py # Flask application
|
| 119 |
+
├── requirements.txt # Python dependencies
|
| 120 |
+
├── Dockerfile # Docker configuration
|
| 121 |
+
├── templates/
|
| 122 |
+
│ └── index.html # Frontend interface
|
| 123 |
+
├── static/
|
| 124 |
+
│ ├── style.css # Styling
|
| 125 |
+
│ └── script.js # Frontend logic
|
| 126 |
+
└── README.md # Documentation
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
## Features Overview
|
| 130 |
+
|
| 131 |
+
### Image Input
|
| 132 |
+
- **File Upload**: Select multiple images from your device
|
| 133 |
+
- **URL Input**: Paste image URLs directly
|
| 134 |
+
- **Preview**: Visual preview of uploaded images
|
| 135 |
+
|
| 136 |
+
### Generation Settings
|
| 137 |
+
- **Image Size**: Preset sizes or custom dimensions (1024-4096px)
|
| 138 |
+
- **Number of Generations**: Control output quantity
|
| 139 |
+
- **Seed**: Optional seed for reproducible results
|
| 140 |
+
- **Safety Checker**: Toggle content filtering
|
| 141 |
+
|
| 142 |
+
### API Integration
|
| 143 |
+
- Automatic API key management
|
| 144 |
+
- Real-time progress logging
|
| 145 |
+
- Error handling and status updates
|
| 146 |
+
|
| 147 |
+
## Security Notes
|
| 148 |
+
|
| 149 |
+
- Never expose your FAL API key in client-side code
|
| 150 |
+
- The application uses server-side proxy for API calls
|
| 151 |
+
- API keys can be stored in environment variables or Hugging Face secrets
|
| 152 |
+
|
| 153 |
+
## Troubleshooting
|
| 154 |
+
|
| 155 |
+
### Common Issues
|
| 156 |
+
|
| 157 |
+
1. **API Key Error**
|
| 158 |
+
- Ensure you've entered your FAL API key in the interface
|
| 159 |
+
- Check if the key has proper permissions
|
| 160 |
+
- Get a new key from [fal.ai](https://fal.ai) if needed
|
| 161 |
+
|
| 162 |
+
2. **Docker Build Fails**
|
| 163 |
+
- Verify all files are in the correct directories
|
| 164 |
+
- Check Docker daemon is running
|
| 165 |
+
|
| 166 |
+
3. **Hugging Face Deployment Issues**
|
| 167 |
+
- Ensure Dockerfile is present
|
| 168 |
+
- Check logs in the Space settings
|
| 169 |
+
|
| 170 |
+
## Support
|
| 171 |
+
|
| 172 |
+
For issues related to:
|
| 173 |
+
- FAL API: Visit [fal.ai documentation](https://fal.ai/docs)
|
| 174 |
+
- Hugging Face Spaces: Check [Hugging Face documentation](https://huggingface.co/docs/hub/spaces)
|
| 175 |
+
|
| 176 |
+
## License
|
| 177 |
+
|
| 178 |
+
This project is provided as-is for educational and development purposes.
|
| 179 |
+
|
| 180 |
+
## Acknowledgments
|
| 181 |
+
|
| 182 |
+
- Powered by [FAL.ai](https://fal.ai)
|
| 183 |
+
- ByteDance SeedDream v4 model
|
| 184 |
+
- Deployed on [Hugging Face Spaces](https://huggingface.co/spaces)
|
app.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FAL API SeedDream v4 Edit - Web Application
|
| 3 |
+
A Flask-based web interface for image editing using ByteDance's SeedDream model
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
import requests
|
| 9 |
+
from flask import Flask, render_template, request, jsonify, send_from_directory
|
| 10 |
+
from flask_cors import CORS
|
| 11 |
+
import fal_client
|
| 12 |
+
from werkzeug.utils import secure_filename
|
| 13 |
+
import base64
|
| 14 |
+
import tempfile
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
app = Flask(__name__)
|
| 18 |
+
CORS(app)
|
| 19 |
+
|
| 20 |
+
# Configuration
|
| 21 |
+
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
|
| 22 |
+
app.config['UPLOAD_FOLDER'] = tempfile.gettempdir()
|
| 23 |
+
|
| 24 |
+
# Ensure static and template directories exist
|
| 25 |
+
Path("static").mkdir(exist_ok=True)
|
| 26 |
+
Path("templates").mkdir(exist_ok=True)
|
| 27 |
+
|
| 28 |
+
@app.route('/')
|
| 29 |
+
def index():
|
| 30 |
+
"""Serve the main HTML interface"""
|
| 31 |
+
return render_template('index.html')
|
| 32 |
+
|
| 33 |
+
@app.route('/static/<path:filename>')
|
| 34 |
+
def serve_static(filename):
|
| 35 |
+
"""Serve static files (CSS, JS)"""
|
| 36 |
+
return send_from_directory('static', filename)
|
| 37 |
+
|
| 38 |
+
@app.route('/api/generate', methods=['POST'])
|
| 39 |
+
def generate():
|
| 40 |
+
"""Handle image generation requests"""
|
| 41 |
+
try:
|
| 42 |
+
# Get request data
|
| 43 |
+
data = request.json
|
| 44 |
+
|
| 45 |
+
# Get model endpoint from header or default to edit
|
| 46 |
+
model_endpoint = request.headers.get('X-Model-Endpoint', 'fal-ai/bytedance/seedream/v4/edit')
|
| 47 |
+
|
| 48 |
+
# Get API key from header or environment
|
| 49 |
+
auth_header = request.headers.get('Authorization', '')
|
| 50 |
+
if auth_header.startswith('Bearer '):
|
| 51 |
+
api_key = auth_header.replace('Bearer ', '')
|
| 52 |
+
# Temporarily set the API key for this request
|
| 53 |
+
os.environ['FAL_KEY'] = api_key
|
| 54 |
+
elif not os.environ.get('FAL_KEY'):
|
| 55 |
+
return jsonify({'error': 'API key not provided'}), 401
|
| 56 |
+
|
| 57 |
+
# Prepare arguments for FAL API
|
| 58 |
+
fal_arguments = {
|
| 59 |
+
'prompt': data.get('prompt')
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
# Handle model-specific parameters
|
| 63 |
+
is_text_to_image = 'text-to-image' in model_endpoint
|
| 64 |
+
|
| 65 |
+
if not is_text_to_image:
|
| 66 |
+
# Image edit mode - process image URLs
|
| 67 |
+
processed_image_urls = []
|
| 68 |
+
for url in data.get('image_urls', []):
|
| 69 |
+
if url.startswith('data:'):
|
| 70 |
+
# Handle base64 data URLs
|
| 71 |
+
processed_image_urls.append(url)
|
| 72 |
+
else:
|
| 73 |
+
# Regular URL
|
| 74 |
+
processed_image_urls.append(url)
|
| 75 |
+
fal_arguments['image_urls'] = processed_image_urls[:10] # Max 10 images
|
| 76 |
+
|
| 77 |
+
# Add max_images for edit mode
|
| 78 |
+
if 'max_images' in data:
|
| 79 |
+
fal_arguments['max_images'] = data['max_images']
|
| 80 |
+
|
| 81 |
+
# Add common optional parameters
|
| 82 |
+
if 'image_size' in data:
|
| 83 |
+
fal_arguments['image_size'] = data['image_size']
|
| 84 |
+
if 'num_images' in data:
|
| 85 |
+
fal_arguments['num_images'] = data['num_images']
|
| 86 |
+
if 'seed' in data:
|
| 87 |
+
fal_arguments['seed'] = data['seed']
|
| 88 |
+
if 'enable_safety_checker' in data:
|
| 89 |
+
fal_arguments['enable_safety_checker'] = data['enable_safety_checker']
|
| 90 |
+
|
| 91 |
+
# Create a logs collector
|
| 92 |
+
logs = []
|
| 93 |
+
|
| 94 |
+
def on_queue_update(update):
|
| 95 |
+
"""Handle queue updates and collect logs"""
|
| 96 |
+
if isinstance(update, fal_client.InProgress):
|
| 97 |
+
for log in update.logs:
|
| 98 |
+
logs.append(log.get("message", ""))
|
| 99 |
+
|
| 100 |
+
# Call FAL API with subscribe (blocking call)
|
| 101 |
+
result = fal_client.subscribe(
|
| 102 |
+
model_endpoint,
|
| 103 |
+
arguments=fal_arguments,
|
| 104 |
+
with_logs=True,
|
| 105 |
+
on_queue_update=on_queue_update
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
# Add logs to the response
|
| 109 |
+
if logs:
|
| 110 |
+
result['logs'] = logs
|
| 111 |
+
|
| 112 |
+
return jsonify(result), 200
|
| 113 |
+
|
| 114 |
+
except Exception as e:
|
| 115 |
+
print(f"Error in generate endpoint: {str(e)}")
|
| 116 |
+
return jsonify({'error': str(e)}), 500
|
| 117 |
+
|
| 118 |
+
@app.route('/api/upload', methods=['POST'])
|
| 119 |
+
def upload_file():
|
| 120 |
+
"""Handle file uploads and return data URL"""
|
| 121 |
+
try:
|
| 122 |
+
if 'file' not in request.files:
|
| 123 |
+
return jsonify({'error': 'No file provided'}), 400
|
| 124 |
+
|
| 125 |
+
file = request.files['file']
|
| 126 |
+
if file.filename == '':
|
| 127 |
+
return jsonify({'error': 'No file selected'}), 400
|
| 128 |
+
|
| 129 |
+
# Read file and convert to base64 data URL
|
| 130 |
+
file_content = file.read()
|
| 131 |
+
file_type = file.content_type or 'application/octet-stream'
|
| 132 |
+
base64_content = base64.b64encode(file_content).decode('utf-8')
|
| 133 |
+
data_url = f"data:{file_type};base64,{base64_content}"
|
| 134 |
+
|
| 135 |
+
return jsonify({'url': data_url}), 200
|
| 136 |
+
|
| 137 |
+
except Exception as e:
|
| 138 |
+
print(f"Error in upload endpoint: {str(e)}")
|
| 139 |
+
return jsonify({'error': str(e)}), 500
|
| 140 |
+
|
| 141 |
+
@app.route('/health', methods=['GET'])
|
| 142 |
+
def health_check():
|
| 143 |
+
"""Health check endpoint for container monitoring"""
|
| 144 |
+
return jsonify({'status': 'healthy'}), 200
|
| 145 |
+
|
| 146 |
+
if __name__ == '__main__':
|
| 147 |
+
# Get port from environment or default to 7860 (Hugging Face Spaces default)
|
| 148 |
+
port = int(os.environ.get('PORT', 7860))
|
| 149 |
+
|
| 150 |
+
# Check if running in production (Hugging Face Spaces)
|
| 151 |
+
is_production = os.environ.get('SPACE_ID') is not None
|
| 152 |
+
|
| 153 |
+
# Run the application
|
| 154 |
+
if is_production:
|
| 155 |
+
app.run(host='0.0.0.0', port=port, debug=False)
|
| 156 |
+
else:
|
| 157 |
+
app.run(host='0.0.0.0', port=port, debug=True)
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Flask==2.3.3
|
| 2 |
+
flask-cors==4.0.0
|
| 3 |
+
fal-client==0.4.0
|
| 4 |
+
requests==2.31.0
|
| 5 |
+
Werkzeug==2.3.7
|
static/script.js
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Configuration
|
| 2 |
+
let uploadedImages = [];
|
| 3 |
+
let imageDimensions = []; // Store dimensions of uploaded images
|
| 4 |
+
|
| 5 |
+
// DOM Elements
|
| 6 |
+
const fileInput = document.getElementById('fileInput');
|
| 7 |
+
const imagePreview = document.getElementById('imagePreview');
|
| 8 |
+
const imageUrls = document.getElementById('imageUrls');
|
| 9 |
+
const generateBtn = document.getElementById('generateBtn');
|
| 10 |
+
const statusMessage = document.getElementById('statusMessage');
|
| 11 |
+
const progressLogs = document.getElementById('progressLogs');
|
| 12 |
+
const results = document.getElementById('results');
|
| 13 |
+
const resultImages = document.getElementById('resultImages');
|
| 14 |
+
const resultInfo = document.getElementById('resultInfo');
|
| 15 |
+
const imageSizeSelect = document.getElementById('imageSize');
|
| 16 |
+
const customSizeElements = document.querySelectorAll('.custom-size');
|
| 17 |
+
const modelSelect = document.getElementById('modelSelect');
|
| 18 |
+
const promptTitle = document.getElementById('promptTitle');
|
| 19 |
+
const promptLabel = document.getElementById('promptLabel');
|
| 20 |
+
const imageInputCard = document.getElementById('imageInputCard');
|
| 21 |
+
|
| 22 |
+
// Event Listeners
|
| 23 |
+
fileInput.addEventListener('change', handleFileUpload);
|
| 24 |
+
generateBtn.addEventListener('click', generateEdit);
|
| 25 |
+
imageSizeSelect.addEventListener('change', handleImageSizeChange);
|
| 26 |
+
modelSelect.addEventListener('change', handleModelChange);
|
| 27 |
+
|
| 28 |
+
// Handle image size dropdown change
|
| 29 |
+
function handleImageSizeChange() {
|
| 30 |
+
if (imageSizeSelect.value === 'custom') {
|
| 31 |
+
customSizeElements.forEach(el => el.style.display = 'block');
|
| 32 |
+
} else {
|
| 33 |
+
customSizeElements.forEach(el => el.style.display = 'none');
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// Handle model dropdown change
|
| 38 |
+
function handleModelChange() {
|
| 39 |
+
const isTextToImage = modelSelect.value === 'fal-ai/bytedance/seedream/v4/text-to-image';
|
| 40 |
+
|
| 41 |
+
if (isTextToImage) {
|
| 42 |
+
// Text-to-image mode
|
| 43 |
+
promptTitle.textContent = 'Generation Prompt';
|
| 44 |
+
promptLabel.textContent = 'Generation Prompt';
|
| 45 |
+
document.getElementById('prompt').placeholder = 'e.g., A beautiful landscape with mountains and a lake at sunset';
|
| 46 |
+
imageInputCard.style.display = 'none';
|
| 47 |
+
// Clear uploaded images when switching to text-to-image
|
| 48 |
+
uploadedImages = [];
|
| 49 |
+
imageDimensions = [];
|
| 50 |
+
renderImagePreviews();
|
| 51 |
+
} else {
|
| 52 |
+
// Image edit mode
|
| 53 |
+
promptTitle.textContent = 'Edit Instructions';
|
| 54 |
+
promptLabel.textContent = 'Editing Prompt';
|
| 55 |
+
document.getElementById('prompt').placeholder = 'e.g., Dress the model in the clothes and shoes.';
|
| 56 |
+
imageInputCard.style.display = 'block';
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// Initialize on page load
|
| 61 |
+
window.addEventListener('DOMContentLoaded', () => {
|
| 62 |
+
// Load saved API key if available
|
| 63 |
+
const savedKey = localStorage.getItem('fal_api_key');
|
| 64 |
+
if (savedKey) {
|
| 65 |
+
document.getElementById('apiKey').value = savedKey;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// Show custom size fields by default since "custom" is the default selection
|
| 69 |
+
handleImageSizeChange();
|
| 70 |
+
|
| 71 |
+
// Initialize model selection
|
| 72 |
+
handleModelChange();
|
| 73 |
+
});
|
| 74 |
+
|
| 75 |
+
// Handle file upload
|
| 76 |
+
async function handleFileUpload(event) {
|
| 77 |
+
const files = Array.from(event.target.files);
|
| 78 |
+
|
| 79 |
+
for (const file of files) {
|
| 80 |
+
if (uploadedImages.length >= 10) {
|
| 81 |
+
showStatus('Maximum 10 images allowed', 'error');
|
| 82 |
+
break;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
if (file.type.startsWith('image/')) {
|
| 86 |
+
const reader = new FileReader();
|
| 87 |
+
reader.onload = (e) => {
|
| 88 |
+
const dataUrl = e.target.result;
|
| 89 |
+
uploadedImages.push(dataUrl);
|
| 90 |
+
|
| 91 |
+
// Get image dimensions
|
| 92 |
+
const img = new Image();
|
| 93 |
+
img.onload = function() {
|
| 94 |
+
imageDimensions.push({
|
| 95 |
+
width: this.width,
|
| 96 |
+
height: this.height
|
| 97 |
+
});
|
| 98 |
+
addImagePreview(dataUrl, uploadedImages.length - 1);
|
| 99 |
+
updateCustomSizeFromLastImage();
|
| 100 |
+
};
|
| 101 |
+
img.src = dataUrl;
|
| 102 |
+
};
|
| 103 |
+
reader.readAsDataURL(file);
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// Add image preview
|
| 109 |
+
function addImagePreview(src, index) {
|
| 110 |
+
const previewItem = document.createElement('div');
|
| 111 |
+
previewItem.className = 'image-preview-item';
|
| 112 |
+
previewItem.innerHTML = `
|
| 113 |
+
<img src="${src}" alt="Upload ${index + 1}">
|
| 114 |
+
<button class="remove-btn" onclick="removeImage(${index})">×</button>
|
| 115 |
+
`;
|
| 116 |
+
imagePreview.appendChild(previewItem);
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
// Remove image
|
| 120 |
+
function removeImage(index) {
|
| 121 |
+
uploadedImages.splice(index, 1);
|
| 122 |
+
imageDimensions.splice(index, 1);
|
| 123 |
+
renderImagePreviews();
|
| 124 |
+
updateCustomSizeFromLastImage();
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// Update custom size fields based on last image
|
| 128 |
+
function updateCustomSizeFromLastImage() {
|
| 129 |
+
if (imageDimensions.length > 0) {
|
| 130 |
+
const lastDims = imageDimensions[imageDimensions.length - 1];
|
| 131 |
+
let width = lastDims.width;
|
| 132 |
+
let height = lastDims.height;
|
| 133 |
+
|
| 134 |
+
// If both dimensions are less than 1000, scale up
|
| 135 |
+
if (width < 1000 && height < 1000) {
|
| 136 |
+
const scale = 1000 / Math.max(width, height);
|
| 137 |
+
width = Math.round(width * scale);
|
| 138 |
+
height = Math.round(height * scale);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// Ensure dimensions are within allowed range (1024-4096)
|
| 142 |
+
width = Math.max(1024, Math.min(4096, width));
|
| 143 |
+
height = Math.max(1024, Math.min(4096, height));
|
| 144 |
+
|
| 145 |
+
document.getElementById('customWidth').value = width;
|
| 146 |
+
document.getElementById('customHeight').value = height;
|
| 147 |
+
|
| 148 |
+
// Auto-switch to custom size if an image is loaded
|
| 149 |
+
if (imageSizeSelect.value !== 'custom') {
|
| 150 |
+
imageSizeSelect.value = 'custom';
|
| 151 |
+
handleImageSizeChange();
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
// Re-render all image previews
|
| 157 |
+
function renderImagePreviews() {
|
| 158 |
+
imagePreview.innerHTML = '';
|
| 159 |
+
uploadedImages.forEach((src, index) => {
|
| 160 |
+
addImagePreview(src, index);
|
| 161 |
+
});
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
// Show status message
|
| 165 |
+
function showStatus(message, type = 'info') {
|
| 166 |
+
statusMessage.className = `status-message ${type}`;
|
| 167 |
+
statusMessage.textContent = message;
|
| 168 |
+
statusMessage.style.display = 'block';
|
| 169 |
+
|
| 170 |
+
if (type === 'success' || type === 'error') {
|
| 171 |
+
setTimeout(() => {
|
| 172 |
+
statusMessage.style.display = 'none';
|
| 173 |
+
}, 5000);
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
// Add log entry
|
| 178 |
+
function addLog(message) {
|
| 179 |
+
const logEntry = document.createElement('div');
|
| 180 |
+
logEntry.className = 'log-entry';
|
| 181 |
+
logEntry.textContent = `${new Date().toLocaleTimeString()}: ${message}`;
|
| 182 |
+
progressLogs.appendChild(logEntry);
|
| 183 |
+
progressLogs.scrollTop = progressLogs.scrollHeight;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
// Clear logs
|
| 187 |
+
function clearLogs() {
|
| 188 |
+
progressLogs.innerHTML = '';
|
| 189 |
+
progressLogs.classList.remove('active');
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
// Get image size configuration
|
| 193 |
+
function getImageSize() {
|
| 194 |
+
const size = imageSizeSelect.value;
|
| 195 |
+
if (size === 'custom') {
|
| 196 |
+
return {
|
| 197 |
+
width: parseInt(document.getElementById('customWidth').value),
|
| 198 |
+
height: parseInt(document.getElementById('customHeight').value)
|
| 199 |
+
};
|
| 200 |
+
}
|
| 201 |
+
return size;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
// Prepare image URLs for API
|
| 205 |
+
async function getImageUrlsForAPI() {
|
| 206 |
+
const urls = [];
|
| 207 |
+
|
| 208 |
+
// Add uploaded images (as data URLs)
|
| 209 |
+
urls.push(...uploadedImages);
|
| 210 |
+
|
| 211 |
+
// Add text URLs and get their dimensions
|
| 212 |
+
const textUrls = imageUrls.value.trim().split('\n').filter(url => url.trim());
|
| 213 |
+
for (const url of textUrls) {
|
| 214 |
+
urls.push(url);
|
| 215 |
+
// Try to get dimensions for URL images
|
| 216 |
+
await getImageDimensionsFromUrl(url);
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
return urls.slice(0, 10); // Maximum 10 images
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
// Get image dimensions from URL
|
| 223 |
+
async function getImageDimensionsFromUrl(url) {
|
| 224 |
+
return new Promise((resolve) => {
|
| 225 |
+
const img = new Image();
|
| 226 |
+
img.onload = function() {
|
| 227 |
+
imageDimensions.push({
|
| 228 |
+
width: this.width,
|
| 229 |
+
height: this.height
|
| 230 |
+
});
|
| 231 |
+
updateCustomSizeFromLastImage();
|
| 232 |
+
resolve();
|
| 233 |
+
};
|
| 234 |
+
img.onerror = function() {
|
| 235 |
+
// If can't load, just resolve without adding dimensions
|
| 236 |
+
resolve();
|
| 237 |
+
};
|
| 238 |
+
img.src = url;
|
| 239 |
+
});
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
// Generate edit
|
| 243 |
+
async function generateEdit() {
|
| 244 |
+
// Validate inputs
|
| 245 |
+
const prompt = document.getElementById('prompt').value.trim();
|
| 246 |
+
if (!prompt) {
|
| 247 |
+
showStatus('Please enter an editing prompt', 'error');
|
| 248 |
+
return;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
const selectedModel = modelSelect.value;
|
| 252 |
+
const isTextToImage = selectedModel === 'fal-ai/bytedance/seedream/v4/text-to-image';
|
| 253 |
+
|
| 254 |
+
// Only require images for edit mode
|
| 255 |
+
const imageUrlsArray = await getImageUrlsForAPI();
|
| 256 |
+
if (!isTextToImage && imageUrlsArray.length === 0) {
|
| 257 |
+
showStatus('Please upload images or provide image URLs for image editing', 'error');
|
| 258 |
+
return;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
// Disable button and show loading
|
| 262 |
+
generateBtn.disabled = true;
|
| 263 |
+
generateBtn.querySelector('.btn-text').textContent = 'Generating...';
|
| 264 |
+
generateBtn.querySelector('.spinner').style.display = 'block';
|
| 265 |
+
|
| 266 |
+
// Clear previous results
|
| 267 |
+
results.style.display = 'none';
|
| 268 |
+
resultImages.innerHTML = '';
|
| 269 |
+
resultInfo.innerHTML = '';
|
| 270 |
+
clearLogs();
|
| 271 |
+
|
| 272 |
+
// Show progress
|
| 273 |
+
showStatus('Connecting to FAL API...', 'info');
|
| 274 |
+
progressLogs.classList.add('active');
|
| 275 |
+
|
| 276 |
+
// Prepare request data
|
| 277 |
+
const requestData = {
|
| 278 |
+
prompt: prompt,
|
| 279 |
+
image_size: getImageSize(),
|
| 280 |
+
num_images: parseInt(document.getElementById('numImages').value),
|
| 281 |
+
enable_safety_checker: false // Always set to false
|
| 282 |
+
};
|
| 283 |
+
|
| 284 |
+
// Add image URLs only for edit mode
|
| 285 |
+
if (!isTextToImage) {
|
| 286 |
+
requestData.image_urls = imageUrlsArray;
|
| 287 |
+
requestData.max_images = parseInt(document.getElementById('maxImages').value);
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
const seed = document.getElementById('seed').value;
|
| 291 |
+
if (seed) {
|
| 292 |
+
requestData.seed = parseInt(seed);
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
try {
|
| 296 |
+
// Check if API key is set
|
| 297 |
+
const apiKey = getAPIKey();
|
| 298 |
+
if (!apiKey) {
|
| 299 |
+
showStatus('Please enter your FAL API key in the API Configuration section', 'error');
|
| 300 |
+
addLog('API key not found. Please enter your FAL API key above.');
|
| 301 |
+
document.getElementById('apiKey').focus();
|
| 302 |
+
return;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
addLog('Submitting request to FAL API...');
|
| 306 |
+
addLog(`Model: ${selectedModel}`);
|
| 307 |
+
addLog(`Prompt: ${prompt}`);
|
| 308 |
+
if (!isTextToImage) {
|
| 309 |
+
addLog(`Number of input images: ${imageUrlsArray.length}`);
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
// Make API call with selected model
|
| 313 |
+
const response = await callFalAPI(apiKey, requestData, selectedModel);
|
| 314 |
+
|
| 315 |
+
// Display results
|
| 316 |
+
displayResults(response);
|
| 317 |
+
showStatus('Generation completed successfully!', 'success');
|
| 318 |
+
|
| 319 |
+
} catch (error) {
|
| 320 |
+
console.error('Error:', error);
|
| 321 |
+
showStatus(`Error: ${error.message}`, 'error');
|
| 322 |
+
addLog(`Error: ${error.message}`);
|
| 323 |
+
} finally {
|
| 324 |
+
// Re-enable button
|
| 325 |
+
generateBtn.disabled = false;
|
| 326 |
+
generateBtn.querySelector('.btn-text').textContent = 'Generate Edit';
|
| 327 |
+
generateBtn.querySelector('.spinner').style.display = 'none';
|
| 328 |
+
}
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
// Get API key from the input field
|
| 332 |
+
function getAPIKey() {
|
| 333 |
+
const apiKeyInput = document.getElementById('apiKey');
|
| 334 |
+
const apiKey = apiKeyInput.value.trim();
|
| 335 |
+
|
| 336 |
+
// Save to localStorage if provided
|
| 337 |
+
if (apiKey) {
|
| 338 |
+
localStorage.setItem('fal_api_key', apiKey);
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
return apiKey || localStorage.getItem('fal_api_key');
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
// Call FAL API (proxy through backend)
|
| 345 |
+
async function callFalAPI(apiKey, requestData, model) {
|
| 346 |
+
const response = await fetch('/api/generate', {
|
| 347 |
+
method: 'POST',
|
| 348 |
+
headers: {
|
| 349 |
+
'Content-Type': 'application/json',
|
| 350 |
+
'Authorization': `Bearer ${apiKey}`,
|
| 351 |
+
'X-Model-Endpoint': model
|
| 352 |
+
},
|
| 353 |
+
body: JSON.stringify(requestData)
|
| 354 |
+
});
|
| 355 |
+
|
| 356 |
+
if (!response.ok) {
|
| 357 |
+
const error = await response.text();
|
| 358 |
+
throw new Error(error || 'API request failed');
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
return await response.json();
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
// Display results
|
| 365 |
+
function displayResults(response) {
|
| 366 |
+
results.style.display = 'block';
|
| 367 |
+
|
| 368 |
+
// Display images
|
| 369 |
+
if (response.images && response.images.length > 0) {
|
| 370 |
+
response.images.forEach((image, index) => {
|
| 371 |
+
const imageItem = document.createElement('div');
|
| 372 |
+
imageItem.className = 'result-image-item';
|
| 373 |
+
imageItem.innerHTML = `
|
| 374 |
+
<img src="${image.url || image.file_data}" alt="Result ${index + 1}">
|
| 375 |
+
`;
|
| 376 |
+
resultImages.appendChild(imageItem);
|
| 377 |
+
});
|
| 378 |
+
|
| 379 |
+
addLog(`Generated ${response.images.length} image(s)`);
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
// Display info
|
| 383 |
+
if (response.seed) {
|
| 384 |
+
resultInfo.innerHTML = `<strong>Seed:</strong> ${response.seed}`;
|
| 385 |
+
addLog(`Seed used: ${response.seed}`);
|
| 386 |
+
}
|
| 387 |
+
}
|
static/style.css
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
* {
|
| 2 |
+
margin: 0;
|
| 3 |
+
padding: 0;
|
| 4 |
+
box-sizing: border-box;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
body {
|
| 8 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
| 9 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 10 |
+
min-height: 100vh;
|
| 11 |
+
padding: 20px;
|
| 12 |
+
color: #333;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
.container {
|
| 16 |
+
max-width: 1200px;
|
| 17 |
+
margin: 0 auto;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
header {
|
| 21 |
+
text-align: center;
|
| 22 |
+
margin-bottom: 40px;
|
| 23 |
+
color: white;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
header h1 {
|
| 27 |
+
font-size: 2.5rem;
|
| 28 |
+
margin-bottom: 10px;
|
| 29 |
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.subtitle {
|
| 33 |
+
font-size: 1.1rem;
|
| 34 |
+
opacity: 0.95;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.card {
|
| 38 |
+
background: white;
|
| 39 |
+
border-radius: 12px;
|
| 40 |
+
padding: 25px;
|
| 41 |
+
margin-bottom: 20px;
|
| 42 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.card h2 {
|
| 46 |
+
color: #764ba2;
|
| 47 |
+
margin-bottom: 20px;
|
| 48 |
+
font-size: 1.4rem;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.form-group {
|
| 52 |
+
margin-bottom: 20px;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.form-group label {
|
| 56 |
+
display: block;
|
| 57 |
+
margin-bottom: 8px;
|
| 58 |
+
font-weight: 600;
|
| 59 |
+
color: #555;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.help-text {
|
| 63 |
+
display: block;
|
| 64 |
+
margin-top: 5px;
|
| 65 |
+
font-size: 0.85rem;
|
| 66 |
+
color: #666;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.help-text a {
|
| 70 |
+
color: #764ba2;
|
| 71 |
+
text-decoration: none;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.help-text a:hover {
|
| 75 |
+
text-decoration: underline;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.form-group input[type="text"],
|
| 79 |
+
.form-group input[type="number"],
|
| 80 |
+
.form-group textarea,
|
| 81 |
+
.form-group select {
|
| 82 |
+
width: 100%;
|
| 83 |
+
padding: 10px 12px;
|
| 84 |
+
border: 2px solid #e1e8ed;
|
| 85 |
+
border-radius: 8px;
|
| 86 |
+
font-size: 14px;
|
| 87 |
+
transition: border-color 0.3s;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.form-group input:focus,
|
| 91 |
+
.form-group textarea:focus,
|
| 92 |
+
.form-group select:focus {
|
| 93 |
+
outline: none;
|
| 94 |
+
border-color: #764ba2;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.form-group textarea {
|
| 98 |
+
resize: vertical;
|
| 99 |
+
font-family: inherit;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.form-group input[type="file"] {
|
| 103 |
+
padding: 8px;
|
| 104 |
+
background: #f7f9fc;
|
| 105 |
+
border: 2px dashed #d1d9e6;
|
| 106 |
+
border-radius: 8px;
|
| 107 |
+
cursor: pointer;
|
| 108 |
+
width: 100%;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.settings-grid {
|
| 112 |
+
display: grid;
|
| 113 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 114 |
+
gap: 15px;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.checkbox-label {
|
| 118 |
+
display: flex;
|
| 119 |
+
align-items: center;
|
| 120 |
+
cursor: pointer;
|
| 121 |
+
user-select: none;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.checkbox-label input[type="checkbox"] {
|
| 125 |
+
margin-right: 8px;
|
| 126 |
+
width: 18px;
|
| 127 |
+
height: 18px;
|
| 128 |
+
cursor: pointer;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.image-preview {
|
| 132 |
+
display: grid;
|
| 133 |
+
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
| 134 |
+
gap: 15px;
|
| 135 |
+
margin-top: 15px;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.image-preview-item {
|
| 139 |
+
position: relative;
|
| 140 |
+
border-radius: 8px;
|
| 141 |
+
overflow: hidden;
|
| 142 |
+
background: #f7f9fc;
|
| 143 |
+
aspect-ratio: 1;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.image-preview-item img {
|
| 147 |
+
width: 100%;
|
| 148 |
+
height: 100%;
|
| 149 |
+
object-fit: cover;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.image-preview-item .remove-btn {
|
| 153 |
+
position: absolute;
|
| 154 |
+
top: 5px;
|
| 155 |
+
right: 5px;
|
| 156 |
+
background: rgba(255, 59, 48, 0.9);
|
| 157 |
+
color: white;
|
| 158 |
+
border: none;
|
| 159 |
+
border-radius: 50%;
|
| 160 |
+
width: 24px;
|
| 161 |
+
height: 24px;
|
| 162 |
+
cursor: pointer;
|
| 163 |
+
display: flex;
|
| 164 |
+
align-items: center;
|
| 165 |
+
justify-content: center;
|
| 166 |
+
font-size: 16px;
|
| 167 |
+
transition: background 0.3s;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.image-preview-item .remove-btn:hover {
|
| 171 |
+
background: rgba(255, 59, 48, 1);
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.generate-btn {
|
| 175 |
+
width: 100%;
|
| 176 |
+
padding: 16px;
|
| 177 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 178 |
+
color: white;
|
| 179 |
+
border: none;
|
| 180 |
+
border-radius: 8px;
|
| 181 |
+
font-size: 1.1rem;
|
| 182 |
+
font-weight: 600;
|
| 183 |
+
cursor: pointer;
|
| 184 |
+
transition: transform 0.2s, box-shadow 0.2s;
|
| 185 |
+
display: flex;
|
| 186 |
+
align-items: center;
|
| 187 |
+
justify-content: center;
|
| 188 |
+
gap: 10px;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.generate-btn:hover {
|
| 192 |
+
transform: translateY(-2px);
|
| 193 |
+
box-shadow: 0 6px 12px rgba(118, 75, 162, 0.3);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.generate-btn:disabled {
|
| 197 |
+
opacity: 0.7;
|
| 198 |
+
cursor: not-allowed;
|
| 199 |
+
transform: none;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.spinner {
|
| 203 |
+
width: 20px;
|
| 204 |
+
height: 20px;
|
| 205 |
+
border: 3px solid rgba(255, 255, 255, 0.3);
|
| 206 |
+
border-top-color: white;
|
| 207 |
+
border-radius: 50%;
|
| 208 |
+
animation: spin 1s linear infinite;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
@keyframes spin {
|
| 212 |
+
to { transform: rotate(360deg); }
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.status-message {
|
| 216 |
+
margin-top: 20px;
|
| 217 |
+
padding: 15px;
|
| 218 |
+
border-radius: 8px;
|
| 219 |
+
display: none;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.status-message.info {
|
| 223 |
+
background: #e3f2fd;
|
| 224 |
+
color: #1565c0;
|
| 225 |
+
border-left: 4px solid #1565c0;
|
| 226 |
+
display: block;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.status-message.success {
|
| 230 |
+
background: #e8f5e9;
|
| 231 |
+
color: #2e7d32;
|
| 232 |
+
border-left: 4px solid #2e7d32;
|
| 233 |
+
display: block;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.status-message.error {
|
| 237 |
+
background: #ffebee;
|
| 238 |
+
color: #c62828;
|
| 239 |
+
border-left: 4px solid #c62828;
|
| 240 |
+
display: block;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.progress-logs {
|
| 244 |
+
margin-top: 15px;
|
| 245 |
+
padding: 15px;
|
| 246 |
+
background: #f5f7fa;
|
| 247 |
+
border-radius: 8px;
|
| 248 |
+
max-height: 200px;
|
| 249 |
+
overflow-y: auto;
|
| 250 |
+
display: none;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.progress-logs.active {
|
| 254 |
+
display: block;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.progress-logs .log-entry {
|
| 258 |
+
padding: 5px 0;
|
| 259 |
+
font-size: 0.9rem;
|
| 260 |
+
color: #666;
|
| 261 |
+
border-bottom: 1px solid #e1e8ed;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.progress-logs .log-entry:last-child {
|
| 265 |
+
border-bottom: none;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.results {
|
| 269 |
+
margin-top: 30px;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.result-images {
|
| 273 |
+
display: grid;
|
| 274 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 275 |
+
gap: 20px;
|
| 276 |
+
margin-top: 20px;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.result-image-item {
|
| 280 |
+
border-radius: 8px;
|
| 281 |
+
overflow: hidden;
|
| 282 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 283 |
+
transition: transform 0.3s;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.result-image-item:hover {
|
| 287 |
+
transform: scale(1.02);
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.result-image-item img {
|
| 291 |
+
width: 100%;
|
| 292 |
+
height: auto;
|
| 293 |
+
display: block;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
.result-info {
|
| 297 |
+
margin-top: 20px;
|
| 298 |
+
padding: 15px;
|
| 299 |
+
background: #f7f9fc;
|
| 300 |
+
border-radius: 8px;
|
| 301 |
+
font-size: 0.9rem;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.result-info strong {
|
| 305 |
+
color: #764ba2;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
footer {
|
| 309 |
+
margin-top: 50px;
|
| 310 |
+
text-align: center;
|
| 311 |
+
color: white;
|
| 312 |
+
padding: 20px;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
footer a {
|
| 316 |
+
color: white;
|
| 317 |
+
font-weight: 600;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.custom-size {
|
| 321 |
+
transition: all 0.3s ease;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
/* Responsive Design */
|
| 325 |
+
@media (max-width: 768px) {
|
| 326 |
+
header h1 {
|
| 327 |
+
font-size: 2rem;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.settings-grid {
|
| 331 |
+
grid-template-columns: 1fr;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
.result-images {
|
| 335 |
+
grid-template-columns: 1fr;
|
| 336 |
+
}
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
/* Loading state */
|
| 340 |
+
.loading {
|
| 341 |
+
pointer-events: none;
|
| 342 |
+
opacity: 0.6;
|
| 343 |
+
}
|
templates/index.html
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>SeedDream v4 Edit - Image Editor</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/style.css">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div class="container">
|
| 11 |
+
<header>
|
| 12 |
+
<h1>🎨 SeedDream v4 Edit</h1>
|
| 13 |
+
<p class="subtitle">AI-powered image editing using ByteDance's SeedDream model</p>
|
| 14 |
+
</header>
|
| 15 |
+
|
| 16 |
+
<main>
|
| 17 |
+
<div class="card">
|
| 18 |
+
<h2>API Configuration</h2>
|
| 19 |
+
<div class="settings-grid">
|
| 20 |
+
<div class="form-group">
|
| 21 |
+
<label for="apiKey">FAL API Key</label>
|
| 22 |
+
<input type="password" id="apiKey" placeholder="Enter your FAL API key" />
|
| 23 |
+
<small class="help-text">Get your API key from <a href="https://fal.ai" target="_blank">fal.ai</a></small>
|
| 24 |
+
</div>
|
| 25 |
+
<div class="form-group">
|
| 26 |
+
<label for="modelSelect">Model</label>
|
| 27 |
+
<select id="modelSelect">
|
| 28 |
+
<option value="fal-ai/bytedance/seedream/v4/edit">Image Edit</option>
|
| 29 |
+
<option value="fal-ai/bytedance/seedream/v4/text-to-image">Text to Image</option>
|
| 30 |
+
</select>
|
| 31 |
+
<small class="help-text">Select the model for generation</small>
|
| 32 |
+
</div>
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
|
| 36 |
+
<div class="card">
|
| 37 |
+
<h2 id="promptTitle">Edit Instructions</h2>
|
| 38 |
+
<div class="form-group">
|
| 39 |
+
<label for="prompt" id="promptLabel">Editing Prompt</label>
|
| 40 |
+
<textarea id="prompt" rows="3" placeholder="e.g., Dress the model in the clothes and shoes.">Dress the model in the clothes and shoes.</textarea>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
<div class="card" id="imageInputCard">
|
| 45 |
+
<h2>Input Images</h2>
|
| 46 |
+
<div class="form-group">
|
| 47 |
+
<label>Upload Images (Max 10)</label>
|
| 48 |
+
<input type="file" id="fileInput" multiple accept="image/*" />
|
| 49 |
+
<div id="imagePreview" class="image-preview"></div>
|
| 50 |
+
</div>
|
| 51 |
+
|
| 52 |
+
<div class="form-group">
|
| 53 |
+
<label for="imageUrls">Or Enter Image URLs (one per line)</label>
|
| 54 |
+
<textarea id="imageUrls" rows="4" placeholder="https://example.com/image1.jpg https://example.com/image2.jpg"></textarea>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<div class="card">
|
| 59 |
+
<h2>Settings</h2>
|
| 60 |
+
<div class="settings-grid">
|
| 61 |
+
<div class="form-group">
|
| 62 |
+
<label for="imageSize">Image Size</label>
|
| 63 |
+
<select id="imageSize">
|
| 64 |
+
<option value="custom" selected>Custom Size</option>
|
| 65 |
+
<option value="square_hd">Square HD (1024x1024)</option>
|
| 66 |
+
<option value="square">Square</option>
|
| 67 |
+
<option value="portrait_4_3">Portrait 4:3</option>
|
| 68 |
+
<option value="portrait_16_9">Portrait 16:9</option>
|
| 69 |
+
<option value="landscape_4_3">Landscape 4:3</option>
|
| 70 |
+
<option value="landscape_16_9">Landscape 16:9</option>
|
| 71 |
+
</select>
|
| 72 |
+
</div>
|
| 73 |
+
|
| 74 |
+
<div class="form-group custom-size">
|
| 75 |
+
<label>Custom Width</label>
|
| 76 |
+
<input type="number" id="customWidth" min="1024" max="4096" value="1280" />
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
<div class="form-group custom-size">
|
| 80 |
+
<label>Custom Height</label>
|
| 81 |
+
<input type="number" id="customHeight" min="1024" max="4096" value="1280" />
|
| 82 |
+
</div>
|
| 83 |
+
|
| 84 |
+
<div class="form-group">
|
| 85 |
+
<label for="numImages">Number of Generations</label>
|
| 86 |
+
<input type="number" id="numImages" min="1" max="10" value="1" />
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
<div class="form-group">
|
| 90 |
+
<label for="maxImages">Max Images per Generation</label>
|
| 91 |
+
<input type="number" id="maxImages" min="1" max="10" value="1" />
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
<div class="form-group">
|
| 95 |
+
<label for="seed">Seed (optional)</label>
|
| 96 |
+
<input type="number" id="seed" placeholder="Random" />
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
<!-- Safety checker is disabled by default and hidden from UI -->
|
| 100 |
+
<input type="hidden" id="safetyChecker" value="false" />
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
|
| 104 |
+
<button id="generateBtn" class="generate-btn">
|
| 105 |
+
<span class="btn-text">Generate Edit</span>
|
| 106 |
+
<div class="spinner" style="display: none;"></div>
|
| 107 |
+
</button>
|
| 108 |
+
|
| 109 |
+
<div id="statusMessage" class="status-message"></div>
|
| 110 |
+
<div id="progressLogs" class="progress-logs"></div>
|
| 111 |
+
|
| 112 |
+
<div id="results" class="results" style="display: none;">
|
| 113 |
+
<h2>Results</h2>
|
| 114 |
+
<div id="resultImages" class="result-images"></div>
|
| 115 |
+
<div id="resultInfo" class="result-info"></div>
|
| 116 |
+
</div>
|
| 117 |
+
</main>
|
| 118 |
+
|
| 119 |
+
<footer>
|
| 120 |
+
<p>Powered by <a href="https://fal.ai" target="_blank">fal.ai</a> and ByteDance SeedDream v4</p>
|
| 121 |
+
</footer>
|
| 122 |
+
</div>
|
| 123 |
+
|
| 124 |
+
<script src="/static/script.js"></script>
|
| 125 |
+
</body>
|
| 126 |
+
</html>
|