samlax12 commited on
Commit
4433b19
·
verified ·
1 Parent(s): 921ed94

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +137 -0
  2. requirements.txt +3 -0
  3. templates/index.html +579 -0
app.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import base64
3
+ import json
4
+ from flask import Flask, render_template, request, jsonify
5
+ from flask_cors import CORS
6
+ import requests
7
+ from werkzeug.utils import secure_filename
8
+ import tempfile
9
+
10
+ app = Flask(__name__)
11
+ CORS(app)
12
+
13
+ # Configuration
14
+ app.config['MAX_CONTENT_LENGTH'] = 25 * 1024 * 1024 # 25MB max file size
15
+ ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'webp'}
16
+
17
+ # Get API key from environment variable
18
+ OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY')
19
+ API_BASE_URL = os.environ.get('API_BASE_URL', 'https://api.openai.com/v1')
20
+
21
+ def allowed_file(filename):
22
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
23
+
24
+ @app.route('/')
25
+ def index():
26
+ return render_template('index.html')
27
+
28
+ @app.route('/generate', methods=['POST'])
29
+ def generate_image():
30
+ try:
31
+ data = request.json
32
+ prompt = data.get('prompt', '')
33
+
34
+ if not prompt:
35
+ return jsonify({'error': 'Prompt is required'}), 400
36
+
37
+ headers = {
38
+ 'Content-Type': 'application/json',
39
+ 'Authorization': f'Bearer {OPENAI_API_KEY}'
40
+ }
41
+
42
+ payload = {
43
+ 'model': 'dall-e-3',
44
+ 'prompt': prompt,
45
+ 'n': 1,
46
+ 'size': data.get('size', '1024x1024'),
47
+ 'quality': data.get('quality', 'standard'),
48
+ 'style': data.get('style', 'vivid')
49
+ }
50
+
51
+ response = requests.post(
52
+ f'{API_BASE_URL}/images/generations',
53
+ headers=headers,
54
+ json=payload
55
+ )
56
+
57
+ if response.status_code == 200:
58
+ result = response.json()
59
+ # For dall-e-3, the response contains URLs
60
+ image_url = result['data'][0]['url']
61
+
62
+ # Download the image and convert to base64
63
+ image_response = requests.get(image_url)
64
+ if image_response.status_code == 200:
65
+ image_base64 = base64.b64encode(image_response.content).decode('utf-8')
66
+ return jsonify({
67
+ 'success': True,
68
+ 'image': f'data:image/png;base64,{image_base64}',
69
+ 'usage': result.get('usage', {})
70
+ })
71
+ else:
72
+ return jsonify({'error': 'Failed to download generated image'}), 500
73
+ else:
74
+ return jsonify({'error': response.json()}), response.status_code
75
+
76
+ except Exception as e:
77
+ return jsonify({'error': str(e)}), 500
78
+
79
+ @app.route('/edit', methods=['POST'])
80
+ def edit_image():
81
+ try:
82
+ # Check if files were uploaded
83
+ if 'images' not in request.files:
84
+ return jsonify({'error': 'No images provided'}), 400
85
+
86
+ files = request.files.getlist('images')
87
+ prompt = request.form.get('prompt', '')
88
+
89
+ if not prompt:
90
+ return jsonify({'error': 'Prompt is required'}), 400
91
+
92
+ if not files or len(files) == 0:
93
+ return jsonify({'error': 'At least one image is required'}), 400
94
+
95
+ # Prepare multipart form data
96
+ headers = {
97
+ 'Authorization': f'Bearer {OPENAI_API_KEY}'
98
+ }
99
+
100
+ # Build files list for multipart upload
101
+ files_list = [('model', (None, 'dall-e-2'))]
102
+ files_list.append(('prompt', (None, prompt)))
103
+ files_list.append(('n', (None, '1')))
104
+ files_list.append(('size', (None, request.form.get('size', '1024x1024'))))
105
+
106
+ # Add images
107
+ for idx, file in enumerate(files):
108
+ if file and allowed_file(file.filename):
109
+ # Read file content
110
+ file_content = file.read()
111
+ files_list.append(('image[]', (file.filename, file_content, file.content_type)))
112
+
113
+ response = requests.post(
114
+ f'{API_BASE_URL}/images/edits',
115
+ headers=headers,
116
+ files=files_list
117
+ )
118
+
119
+ if response.status_code == 200:
120
+ result = response.json()
121
+ # The response contains base64 encoded image
122
+ image_base64 = result['data'][0]['b64_json']
123
+ return jsonify({
124
+ 'success': True,
125
+ 'image': f'data:image/png;base64,{image_base64}',
126
+ 'usage': result.get('usage', {})
127
+ })
128
+ else:
129
+ return jsonify({'error': response.json()}), response.status_code
130
+
131
+ except Exception as e:
132
+ return jsonify({'error': str(e)}), 500
133
+
134
+ if __name__ == '__main__':
135
+ if not OPENAI_API_KEY:
136
+ print("Warning: OPENAI_API_KEY environment variable not set!")
137
+ app.run(host='0.0.0.0', port=7860, debug=True)
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ flask
2
+ flask-cors
3
+ requests
templates/index.html ADDED
@@ -0,0 +1,579 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>AI图像工作室</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ color: #333;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1200px;
23
+ margin: 0 auto;
24
+ padding: 2rem;
25
+ }
26
+
27
+ .header {
28
+ text-align: center;
29
+ margin-bottom: 3rem;
30
+ color: white;
31
+ }
32
+
33
+ .header h1 {
34
+ font-size: 3rem;
35
+ margin-bottom: 0.5rem;
36
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
37
+ }
38
+
39
+ .header p {
40
+ font-size: 1.2rem;
41
+ opacity: 0.9;
42
+ }
43
+
44
+ .tabs {
45
+ display: flex;
46
+ justify-content: center;
47
+ margin-bottom: 2rem;
48
+ gap: 1rem;
49
+ }
50
+
51
+ .tab {
52
+ padding: 1rem 2rem;
53
+ background: rgba(255, 255, 255, 0.2);
54
+ border: 2px solid rgba(255, 255, 255, 0.3);
55
+ border-radius: 50px;
56
+ color: white;
57
+ cursor: pointer;
58
+ transition: all 0.3s ease;
59
+ font-weight: 600;
60
+ backdrop-filter: blur(10px);
61
+ }
62
+
63
+ .tab:hover {
64
+ background: rgba(255, 255, 255, 0.3);
65
+ transform: translateY(-2px);
66
+ }
67
+
68
+ .tab.active {
69
+ background: white;
70
+ color: #667eea;
71
+ }
72
+
73
+ .content {
74
+ background: white;
75
+ border-radius: 20px;
76
+ padding: 2rem;
77
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
78
+ }
79
+
80
+ .form-group {
81
+ margin-bottom: 1.5rem;
82
+ }
83
+
84
+ label {
85
+ display: block;
86
+ margin-bottom: 0.5rem;
87
+ font-weight: 600;
88
+ color: #555;
89
+ }
90
+
91
+ textarea {
92
+ width: 100%;
93
+ min-height: 120px;
94
+ padding: 1rem;
95
+ border: 2px solid #e0e0e0;
96
+ border-radius: 10px;
97
+ font-size: 1rem;
98
+ transition: border-color 0.3s ease;
99
+ resize: vertical;
100
+ }
101
+
102
+ textarea:focus {
103
+ outline: none;
104
+ border-color: #667eea;
105
+ }
106
+
107
+ select {
108
+ width: 100%;
109
+ padding: 0.75rem;
110
+ border: 2px solid #e0e0e0;
111
+ border-radius: 10px;
112
+ font-size: 1rem;
113
+ background: white;
114
+ cursor: pointer;
115
+ }
116
+
117
+ .options-grid {
118
+ display: grid;
119
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
120
+ gap: 1rem;
121
+ }
122
+
123
+ .file-upload {
124
+ position: relative;
125
+ display: inline-block;
126
+ cursor: pointer;
127
+ width: 100%;
128
+ }
129
+
130
+ .file-upload input[type="file"] {
131
+ position: absolute;
132
+ opacity: 0;
133
+ width: 100%;
134
+ height: 100%;
135
+ cursor: pointer;
136
+ }
137
+
138
+ .file-upload-label {
139
+ display: block;
140
+ padding: 2rem;
141
+ border: 3px dashed #667eea;
142
+ border-radius: 10px;
143
+ text-align: center;
144
+ background: #f8f9ff;
145
+ transition: all 0.3s ease;
146
+ }
147
+
148
+ .file-upload:hover .file-upload-label {
149
+ background: #eef0ff;
150
+ border-color: #764ba2;
151
+ }
152
+
153
+ .generate-btn {
154
+ width: 100%;
155
+ padding: 1.25rem;
156
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
157
+ color: white;
158
+ border: none;
159
+ border-radius: 10px;
160
+ font-size: 1.1rem;
161
+ font-weight: 600;
162
+ cursor: pointer;
163
+ transition: all 0.3s ease;
164
+ margin-top: 1rem;
165
+ }
166
+
167
+ .generate-btn:hover {
168
+ transform: translateY(-2px);
169
+ box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
170
+ }
171
+
172
+ .generate-btn:disabled {
173
+ opacity: 0.6;
174
+ cursor: not-allowed;
175
+ transform: none;
176
+ }
177
+
178
+ .result-section {
179
+ margin-top: 3rem;
180
+ text-align: center;
181
+ }
182
+
183
+ .result-image {
184
+ max-width: 100%;
185
+ border-radius: 10px;
186
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
187
+ margin-top: 1rem;
188
+ }
189
+
190
+ .loading {
191
+ display: inline-block;
192
+ width: 20px;
193
+ height: 20px;
194
+ border: 3px solid #f3f3f3;
195
+ border-top: 3px solid #667eea;
196
+ border-radius: 50%;
197
+ animation: spin 1s linear infinite;
198
+ margin-right: 10px;
199
+ }
200
+
201
+ @keyframes spin {
202
+ 0% { transform: rotate(0deg); }
203
+ 100% { transform: rotate(360deg); }
204
+ }
205
+
206
+ .error-message {
207
+ background: #fee;
208
+ color: #c33;
209
+ padding: 1rem;
210
+ border-radius: 10px;
211
+ margin-top: 1rem;
212
+ }
213
+
214
+ .success-message {
215
+ background: #efe;
216
+ color: #3c3;
217
+ padding: 1rem;
218
+ border-radius: 10px;
219
+ margin-top: 1rem;
220
+ }
221
+
222
+ .hidden {
223
+ display: none;
224
+ }
225
+
226
+ .image-preview {
227
+ display: flex;
228
+ flex-wrap: wrap;
229
+ gap: 1rem;
230
+ margin-top: 1rem;
231
+ }
232
+
233
+ .preview-item {
234
+ position: relative;
235
+ width: 150px;
236
+ height: 150px;
237
+ border-radius: 10px;
238
+ overflow: hidden;
239
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
240
+ }
241
+
242
+ .preview-item img {
243
+ width: 100%;
244
+ height: 100%;
245
+ object-fit: cover;
246
+ }
247
+
248
+ .preview-item .remove {
249
+ position: absolute;
250
+ top: 5px;
251
+ right: 5px;
252
+ background: rgba(255, 255, 255, 0.9);
253
+ color: #c33;
254
+ border: none;
255
+ border-radius: 50%;
256
+ width: 30px;
257
+ height: 30px;
258
+ cursor: pointer;
259
+ font-weight: bold;
260
+ }
261
+
262
+ .usage-info {
263
+ margin-top: 1rem;
264
+ padding: 1rem;
265
+ background: #f0f4ff;
266
+ border-radius: 10px;
267
+ font-size: 0.9rem;
268
+ color: #666;
269
+ }
270
+
271
+ .download-btn {
272
+ margin-top: 1rem;
273
+ padding: 0.75rem 1.5rem;
274
+ background: #4CAF50;
275
+ color: white;
276
+ border: none;
277
+ border-radius: 10px;
278
+ font-size: 1rem;
279
+ cursor: pointer;
280
+ transition: all 0.3s ease;
281
+ }
282
+
283
+ .download-btn:hover {
284
+ background: #45a049;
285
+ transform: translateY(-2px);
286
+ }
287
+ </style>
288
+ </head>
289
+ <body>
290
+ <div class="container">
291
+ <header class="header">
292
+ <h1>🎨 AI 图像工作室</h1>
293
+ <p>使用人工智能的力量创造惊艳图像</p>
294
+ </header>
295
+
296
+ <div class="tabs">
297
+ <div class="tab active" onclick="switchTab('generate')">生成图像</div>
298
+ <div class="tab" onclick="switchTab('edit')">编辑图像</div>
299
+ </div>
300
+
301
+ <div class="content">
302
+ <!-- Generate Tab -->
303
+ <div id="generate-tab" class="tab-content">
304
+ <form id="generate-form">
305
+ <div class="form-group">
306
+ <label for="prompt">描述您想要的图像</label>
307
+ <textarea id="prompt" name="prompt" placeholder="夕阳下的宁静风景,山峦倒映在晶莹剔透的湖面上,水彩画风格..." required></textarea>
308
+ </div>
309
+
310
+ <div class="options-grid">
311
+ <div class="form-group">
312
+ <label for="size">图像尺寸</label>
313
+ <select id="size" name="size">
314
+ <option value="1024x1024">正方形 (1024x1024)</option>
315
+ <option value="1792x1024">横向 (1792x1024)</option>
316
+ <option value="1024x1792">纵向 (1024x1792)</option>
317
+ </select>
318
+ </div>
319
+
320
+ <div class="form-group">
321
+ <label for="quality">图像质量</label>
322
+ <select id="quality" name="quality">
323
+ <option value="standard">标准</option>
324
+ <option value="hd">高清 (费用更高)</option>
325
+ </select>
326
+ </div>
327
+
328
+ <div class="form-group">
329
+ <label for="style">风格</label>
330
+ <select id="style" name="style">
331
+ <option value="vivid">生动</option>
332
+ <option value="natural">自然</option>
333
+ </select>
334
+ </div>
335
+ </div>
336
+
337
+ <button type="submit" class="generate-btn">
338
+ <span class="btn-text">生成图像</span>
339
+ </button>
340
+ </form>
341
+ </div>
342
+
343
+ <!-- Edit Tab -->
344
+ <div id="edit-tab" class="tab-content hidden">
345
+ <form id="edit-form">
346
+ <div class="form-group">
347
+ <label>上传图像(最多16张)</label>
348
+ <div class="file-upload">
349
+ <input type="file" id="images" name="images" multiple accept="image/*" required>
350
+ <label for="images" class="file-upload-label">
351
+ <div>📁 点击或拖拽图像到这里</div>
352
+ <small>支持 PNG、JPG、WEBP 格式(每个文件最大 25MB)</small>
353
+ </label>
354
+ </div>
355
+ <div id="image-preview" class="image-preview"></div>
356
+ </div>
357
+
358
+ <div class="form-group">
359
+ <label for="edit-prompt">描述如何组合/编辑这些图像</label>
360
+ <textarea id="edit-prompt" name="prompt" placeholder="将这些图像制作成复古相册风格的精美拼贴画..." required></textarea>
361
+ </div>
362
+
363
+ <div class="form-group">
364
+ <label for="edit-size">输出尺寸</label>
365
+ <select id="edit-size" name="size">
366
+ <option value="256x256">小尺寸 (256x256)</option>
367
+ <option value="512x512">中尺寸 (512x512)</option>
368
+ <option value="1024x1024">大尺寸 (1024x1024)</option>
369
+ </select>
370
+ </div>
371
+
372
+ <button type="submit" class="generate-btn">
373
+ <span class="btn-text">编辑图像</span>
374
+ </button>
375
+ </form>
376
+ </div>
377
+
378
+ <div id="result-section" class="result-section hidden">
379
+ <h2>您生成的图像</h2>
380
+ <img id="result-image" class="result-image" alt="生成的图像">
381
+ <div id="usage-info" class="usage-info hidden"></div>
382
+ <button id="download-btn" class="download-btn" onclick="downloadImage()">下载图像</button>
383
+ </div>
384
+
385
+ <div id="message" class="hidden"></div>
386
+ </div>
387
+ </div>
388
+
389
+ <script>
390
+ let currentTab = 'generate';
391
+ let generatedImageData = null;
392
+ let selectedFiles = [];
393
+
394
+ function switchTab(tab) {
395
+ currentTab = tab;
396
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
397
+ document.querySelectorAll('.tab-content').forEach(t => t.classList.add('hidden'));
398
+
399
+ event.target.classList.add('active');
400
+ document.getElementById(`${tab}-tab`).classList.remove('hidden');
401
+
402
+ // Hide result when switching tabs
403
+ document.getElementById('result-section').classList.add('hidden');
404
+ }
405
+
406
+ // Generate form submission
407
+ document.getElementById('generate-form').addEventListener('submit', async (e) => {
408
+ e.preventDefault();
409
+
410
+ const btn = e.target.querySelector('.generate-btn');
411
+ const btnText = btn.querySelector('.btn-text');
412
+
413
+ // Disable button and show loading
414
+ btn.disabled = true;
415
+ btnText.innerHTML = '<span class="loading"></span>生成中...';
416
+
417
+ // Hide previous results
418
+ showMessage('');
419
+ document.getElementById('result-section').classList.add('hidden');
420
+
421
+ const formData = {
422
+ prompt: document.getElementById('prompt').value,
423
+ size: document.getElementById('size').value,
424
+ quality: document.getElementById('quality').value,
425
+ style: document.getElementById('style').value
426
+ };
427
+
428
+ try {
429
+ const response = await fetch('/generate', {
430
+ method: 'POST',
431
+ headers: {
432
+ 'Content-Type': 'application/json'
433
+ },
434
+ body: JSON.stringify(formData)
435
+ });
436
+
437
+ const data = await response.json();
438
+
439
+ if (data.success) {
440
+ generatedImageData = data.image;
441
+ document.getElementById('result-image').src = data.image;
442
+ document.getElementById('result-section').classList.remove('hidden');
443
+
444
+ if (data.usage) {
445
+ const usageInfo = document.getElementById('usage-info');
446
+ usageInfo.innerHTML = `使用的令牌数: ${data.usage.total_tokens || '未知'}`;
447
+ usageInfo.classList.remove('hidden');
448
+ }
449
+
450
+ showMessage('图像生成成功!', 'success');
451
+ } else {
452
+ showMessage(data.error || '发生错误', 'error');
453
+ }
454
+ } catch (error) {
455
+ showMessage('网络错误: ' + error.message, 'error');
456
+ } finally {
457
+ btn.disabled = false;
458
+ btnText.innerHTML = '生成图像';
459
+ }
460
+ });
461
+
462
+ // Edit form submission
463
+ document.getElementById('edit-form').addEventListener('submit', async (e) => {
464
+ e.preventDefault();
465
+
466
+ const btn = e.target.querySelector('.generate-btn');
467
+ const btnText = btn.querySelector('.btn-text');
468
+
469
+ if (selectedFiles.length === 0) {
470
+ showMessage('请至少选择一张图像', 'error');
471
+ return;
472
+ }
473
+
474
+ // Disable button and show loading
475
+ btn.disabled = true;
476
+ btnText.innerHTML = '<span class="loading"></span>处理中...';
477
+
478
+ // Hide previous results
479
+ showMessage('');
480
+ document.getElementById('result-section').classList.add('hidden');
481
+
482
+ const formData = new FormData();
483
+ selectedFiles.forEach(file => {
484
+ formData.append('images', file);
485
+ });
486
+ formData.append('prompt', document.getElementById('edit-prompt').value);
487
+ formData.append('size', document.getElementById('edit-size').value);
488
+
489
+ try {
490
+ const response = await fetch('/edit', {
491
+ method: 'POST',
492
+ body: formData
493
+ });
494
+
495
+ const data = await response.json();
496
+
497
+ if (data.success) {
498
+ generatedImageData = data.image;
499
+ document.getElementById('result-image').src = data.image;
500
+ document.getElementById('result-section').classList.remove('hidden');
501
+
502
+ if (data.usage) {
503
+ const usageInfo = document.getElementById('usage-info');
504
+ usageInfo.innerHTML = `使用的令牌数: ${data.usage.total_tokens || '未知'}`;
505
+ usageInfo.classList.remove('hidden');
506
+ }
507
+
508
+ showMessage('图像编辑成功!', 'success');
509
+ } else {
510
+ showMessage(data.error || '发生错误', 'error');
511
+ }
512
+ } catch (error) {
513
+ showMessage('网络错误: ' + error.message, 'error');
514
+ } finally {
515
+ btn.disabled = false;
516
+ btnText.innerHTML = '编辑图像';
517
+ }
518
+ });
519
+
520
+ // File upload handling
521
+ document.getElementById('images').addEventListener('change', (e) => {
522
+ const files = Array.from(e.target.files);
523
+ selectedFiles = files.slice(0, 16); // Limit to 16 files
524
+
525
+ const preview = document.getElementById('image-preview');
526
+ preview.innerHTML = '';
527
+
528
+ selectedFiles.forEach((file, index) => {
529
+ const reader = new FileReader();
530
+ reader.onload = (e) => {
531
+ const div = document.createElement('div');
532
+ div.className = 'preview-item';
533
+ div.innerHTML = `
534
+ <img src="${e.target.result}" alt="Preview">
535
+ <button class="remove" onclick="removeImage(${index})">×</button>
536
+ `;
537
+ preview.appendChild(div);
538
+ };
539
+ reader.readAsDataURL(file);
540
+ });
541
+ });
542
+
543
+ function removeImage(index) {
544
+ selectedFiles.splice(index, 1);
545
+
546
+ // Update file input
547
+ const dt = new DataTransfer();
548
+ selectedFiles.forEach(file => dt.items.add(file));
549
+ document.getElementById('images').files = dt.files;
550
+
551
+ // Trigger change event to update preview
552
+ document.getElementById('images').dispatchEvent(new Event('change'));
553
+ }
554
+
555
+ function showMessage(message, type = '') {
556
+ const messageEl = document.getElementById('message');
557
+ if (!message) {
558
+ messageEl.classList.add('hidden');
559
+ return;
560
+ }
561
+
562
+ messageEl.textContent = message;
563
+ messageEl.className = type === 'error' ? 'error-message' :
564
+ type === 'success' ? 'success-message' : '';
565
+ }
566
+
567
+ function downloadImage() {
568
+ if (!generatedImageData) return;
569
+
570
+ const link = document.createElement('a');
571
+ link.href = generatedImageData;
572
+ link.download = `ai-generated-${Date.now()}.png`;
573
+ document.body.appendChild(link);
574
+ link.click();
575
+ document.body.removeChild(link);
576
+ }
577
+ </script>
578
+ </body>
579
+ </html>