bibibi12345 commited on
Commit
c5f0b8c
·
0 Parent(s):

initial test

Browse files
Files changed (8) hide show
  1. .gitignore +63 -0
  2. Dockerfile +40 -0
  3. README.md +184 -0
  4. app.py +157 -0
  5. requirements.txt +5 -0
  6. static/script.js +387 -0
  7. static/style.css +343 -0
  8. 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&#10;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>