ar08 commited on
Commit
5856e1e
·
verified ·
1 Parent(s): 5dc4372

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +87 -534
app.py CHANGED
@@ -1,562 +1,115 @@
1
- #!/usr/bin/env python3
2
- import json
3
- import sys
4
  import os
5
- import io
6
- import argparse
7
- import os
8
- os.environ["MPLCONFIGDIR"] = "/tmp/matplotlib"
9
-
10
- import uuid
11
- import base64
12
- import logging
13
  import time
14
- import copy
15
  import cv2
16
  import insightface
 
 
 
 
17
  import numpy as np
18
- from typing import List, Union
19
  from PIL import Image
20
- from restoration import *
21
- from flask import Flask, request, jsonify, make_response
22
- from waitress import serve
23
-
24
- LOG_LEVEL = logging.DEBUG
25
- TMP_PATH = '/tmp/inswapper'
26
- script_dir = os.path.dirname(os.path.abspath(__file__))
27
- log_path = ''
28
-
29
- # Mac does not have permission to /var/log for example
30
- if sys.platform == 'linux':
31
- log_path = '/var/log/'
32
-
33
- logging.basicConfig(
34
- filename=f'{log_path}inswapper.log',
35
- format='%(asctime)s : %(levelname)s : %(message)s',
36
- level=LOG_LEVEL
37
- )
38
-
39
- logging.getLogger().addHandler(logging.StreamHandler(sys.stdout))
40
-
41
-
42
- def process_request(request_obj):
43
- try:
44
- logging.debug('Swapping face')
45
- face_swap_timer = Timer()
46
- result_image = face_swap(request_obj['source_image'], request_obj['target_image'])
47
- face_swap_time = face_swap_timer.get_elapsed_time()
48
- logging.info(f'Time taken to swap face: {face_swap_time} seconds')
49
-
50
- response = {
51
- 'status': 'ok',
52
- 'image': result_image
53
- }
54
- except Exception as e:
55
- logging.error(e)
56
- response = {
57
- 'status': 'error',
58
- 'msg': 'Face swap failed',
59
- 'detail': str(e)
60
- }
61
-
62
- return response
63
 
 
64
 
65
- class Timer:
66
  def __init__(self):
67
- self.start = time.time()
68
-
69
- def restart(self):
70
- self.start = time.time()
71
-
72
- def get_elapsed_time(self):
73
- end = time.time()
74
- return round(end - self.start, 1)
75
-
76
-
77
- def get_args():
78
- parser = argparse.ArgumentParser(
79
- description='Inswapper REST API'
80
- )
81
-
82
- parser.add_argument(
83
- '-p', '--port',
84
- help='Port to listen on',
85
- type=int,
86
- default=80
87
- )
88
-
89
- parser.add_argument(
90
- '-H', '--host',
91
- help='Host to bind to',
92
- default='0.0.0.0'
93
- )
94
-
95
- return parser.parse_args()
96
-
97
-
98
- def determine_file_extension(image_data):
99
- try:
100
- if image_data.startswith('/9j/'):
101
- image_extension = '.jpg'
102
- elif image_data.startswith('iVBORw0Kg'):
103
- image_extension = '.png'
104
- else:
105
- # Default to png if we can't figure out the extension
106
- image_extension = '.png'
107
- except Exception as e:
108
- image_extension = '.png'
109
-
110
- return image_extension
111
-
112
-
113
- def write_base64_to_disk(file_b64: str, file_path: str):
114
- with open(file_path, 'wb') as file:
115
- file.write(base64.b64decode(file_b64))
116
-
117
-
118
- def get_face_swap_model(model_path: str):
119
- model = insightface.model_zoo.get_model(model_path)
120
- return model
121
-
122
-
123
- def get_face_analyser(model_path: str,
124
- det_size=(320, 320)):
125
- face_analyser = insightface.app.FaceAnalysis(name="buffalo_l", root="./checkpoints")
126
- face_analyser.prepare(ctx_id=0, det_size=det_size)
127
- return face_analyser
128
-
129
-
130
- def get_one_face(face_analyser,
131
- frame:np.ndarray):
132
- face = face_analyser.get(frame)
133
- try:
134
- return min(face, key=lambda x: x.bbox[0])
135
- except ValueError:
136
- return None
137
-
138
-
139
- def get_many_faces(face_analyser,
140
- frame:np.ndarray):
141
- """
142
- get faces from left to right by order
143
- """
144
- try:
145
- face = face_analyser.get(frame)
146
- return sorted(face, key=lambda x: x.bbox[0])
147
- except IndexError:
148
- return None
149
-
150
-
151
- def swap_face(face_swapper,
152
- source_faces,
153
- target_faces,
154
- source_index,
155
- target_index,
156
- temp_frame):
157
- """
158
- paste source_face on target image
159
- """
160
- source_face = source_faces[source_index]
161
- target_face = target_faces[target_index]
162
-
163
- return face_swapper.get(temp_frame, target_face, source_face, paste_back=True)
164
-
165
-
166
- def process(source_img: Union[Image.Image, List],
167
- target_img: Image.Image,
168
- source_indexes: str,
169
- target_indexes: str,
170
- model: str):
171
-
172
- # load face_analyser
173
- face_analyser = get_face_analyser(model)
174
-
175
- # load face_swapper
176
- model_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), model)
177
- face_swapper = get_face_swap_model(model_path)
178
-
179
- # read target image
180
- target_img = cv2.cvtColor(np.array(target_img), cv2.COLOR_RGB2BGR)
181
-
182
- # detect faces that will be replaced in target_img
183
- target_faces = get_many_faces(face_analyser, target_img)
184
- num_target_faces = len(target_faces)
185
- num_source_images = len(source_img)
186
-
187
- if target_faces is not None:
188
- temp_frame = copy.deepcopy(target_img)
189
- if isinstance(source_img, list) and num_source_images == num_target_faces:
190
- logging.debug('Replacing the faces in the target image from left to right by order')
191
- for i in range(num_target_faces):
192
- source_faces = get_many_faces(face_analyser, cv2.cvtColor(np.array(source_img[i]), cv2.COLOR_RGB2BGR))
193
- source_index = i
194
- target_index = i
195
-
196
- if source_faces is None:
197
- raise Exception('No source faces found!')
198
-
199
- temp_frame = swap_face(
200
- face_swapper,
201
- source_faces,
202
- target_faces,
203
- source_index,
204
- target_index,
205
- temp_frame
206
- )
207
- elif num_source_images == 1:
208
- # detect source faces that will be replaced into the target image
209
- source_faces = get_many_faces(face_analyser, cv2.cvtColor(np.array(source_img[0]), cv2.COLOR_RGB2BGR))
210
- num_source_faces = len(source_faces)
211
- logging.debug(f'Source faces: {num_source_faces}')
212
- logging.debug(f'Target faces: {num_target_faces}')
213
-
214
- if source_faces is None:
215
- raise Exception('No source faces found!')
216
-
217
- if target_indexes == "-1":
218
- if num_source_faces == 1:
219
- logging.debug('Replacing all faces in target image with the same face from the source image')
220
- num_iterations = num_target_faces
221
- elif num_source_faces < num_target_faces:
222
- logging.debug('There are less faces in the source image than the target image, replacing as many as we can')
223
- num_iterations = num_source_faces
224
- elif num_target_faces < num_source_faces:
225
- logging.debug('There are less faces in the target image than the source image, replacing as many as we can')
226
- num_iterations = num_target_faces
227
- else:
228
- logging.debug('Replacing all faces in the target image with the faces from the source image')
229
- num_iterations = num_target_faces
230
-
231
- for i in range(num_iterations):
232
- source_index = 0 if num_source_faces == 1 else i
233
- target_index = i
234
-
235
- temp_frame = swap_face(
236
- face_swapper,
237
- source_faces,
238
- target_faces,
239
- source_index,
240
- target_index,
241
- temp_frame
242
- )
243
- elif source_indexes == '-1' and target_indexes == '-1':
244
- logging.debug('Replacing specific face(s) in the target image with the face from the source image')
245
- target_indexes = target_indexes.split(',')
246
- source_index = 0
247
-
248
- for target_index in target_indexes:
249
- target_index = int(target_index)
250
-
251
- temp_frame = swap_face(
252
- face_swapper,
253
- source_faces,
254
- target_faces,
255
- source_index,
256
- target_index,
257
- temp_frame
258
- )
259
- else:
260
- logging.debug('Replacing specific face(s) in the target image with specific face(s) from the source image')
261
-
262
- if source_indexes == "-1":
263
- source_indexes = ','.join(map(lambda x: str(x), range(num_source_faces)))
264
-
265
- if target_indexes == "-1":
266
- target_indexes = ','.join(map(lambda x: str(x), range(num_target_faces)))
267
-
268
- source_indexes = source_indexes.split(',')
269
- target_indexes = target_indexes.split(',')
270
- num_source_faces_to_swap = len(source_indexes)
271
- num_target_faces_to_swap = len(target_indexes)
272
-
273
- if num_source_faces_to_swap > num_source_faces:
274
- raise Exception('Number of source indexes is greater than the number of faces in the source image')
275
-
276
- if num_target_faces_to_swap > num_target_faces:
277
- raise Exception('Number of target indexes is greater than the number of faces in the target image')
278
-
279
- if num_source_faces_to_swap > num_target_faces_to_swap:
280
- num_iterations = num_source_faces_to_swap
281
- else:
282
- num_iterations = num_target_faces_to_swap
283
-
284
- if num_source_faces_to_swap == num_target_faces_to_swap:
285
- for index in range(num_iterations):
286
- source_index = int(source_indexes[index])
287
- target_index = int(target_indexes[index])
288
-
289
- if source_index > num_source_faces-1:
290
- raise ValueError(f'Source index {source_index} is higher than the number of faces in the source image')
291
-
292
- if target_index > num_target_faces-1:
293
- raise ValueError(f'Target index {target_index} is higher than the number of faces in the target image')
294
-
295
- temp_frame = swap_face(
296
- face_swapper,
297
- source_faces,
298
- target_faces,
299
- source_index,
300
- target_index,
301
- temp_frame
302
- )
303
- else:
304
- logging.error('Unsupported face configuration')
305
- raise Exception('Unsupported face configuration')
306
- result = temp_frame
307
- else:
308
- logging.error('No target faces found')
309
- raise Exception('No target faces found!')
310
-
311
- result_image = Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
312
- return result_image
313
-
314
-
315
- def face_swap(src_img_path,
316
- target_img_path,
317
- source_indexes,
318
- target_indexes,
319
- background_enhance,
320
- face_restore,
321
- face_upsample,
322
- upscale,
323
- codeformer_fidelity,
324
- output_format):
325
-
326
- source_img_paths = src_img_path.split(';')
327
- source_img = [Image.open(img_path) for img_path in source_img_paths]
328
- target_img = Image.open(target_img_path)
329
-
330
- # download from https://huggingface.co/ashleykleynhans/inswapper/tree/main
331
- model = os.path.join(script_dir, 'checkpoints/inswapper_128.onnx')
332
- logging.debug(f'Face swap model: {model}')
333
-
334
- try:
335
- logging.debug('Performing face swap')
336
- result_image = process(
337
- source_img,
338
- target_img,
339
- source_indexes,
340
- target_indexes,
341
- model
342
- )
343
- logging.debug('Face swap complete')
344
- except Exception as e:
345
- raise
346
-
347
- # make sure the ckpts downloaded successfully
348
- check_ckpts()
349
-
350
- if face_restore:
351
- # https://huggingface.co/spaces/sczhou/CodeFormer
352
- logging.debug('Setting upsampler to RealESRGAN_x2plus')
353
- upsampler = set_realesrgan()
354
-
355
- if torch.cuda.is_available():
356
- torch_device = 'cuda'
357
- else:
358
- torch_device = 'cpu'
359
-
360
- logging.debug(f'Torch device: {torch_device.upper()}')
361
- device = torch.device(torch_device)
362
-
363
- codeformer_net = ARCH_REGISTRY.get('CodeFormer')(
364
- dim_embd=512,
365
- codebook_size=1024,
366
- n_head=8,
367
- n_layers=9,
368
- connect_list=['32', '64', '128', '256'],
369
- ).to(device)
370
-
371
- ckpt_path = os.path.join(script_dir, 'CodeFormer/CodeFormer/weights/CodeFormer/codeformer.pth')
372
- logging.debug(f'Loading CodeFormer model: {ckpt_path}')
373
- checkpoint = torch.load(ckpt_path)['params_ema']
374
- codeformer_net.load_state_dict(checkpoint)
375
- codeformer_net.eval()
376
- result_image = cv2.cvtColor(np.array(result_image), cv2.COLOR_RGB2BGR)
377
- logging.debug('Performing face restoration using CodeFormer')
378
-
379
- try:
380
- result_image = face_restoration(
381
- result_image,
382
- background_enhance,
383
- face_upsample,
384
- upscale,
385
- codeformer_fidelity,
386
- upsampler,
387
- codeformer_net,
388
- device
389
  )
390
- except Exception as e:
391
- raise
392
-
393
- logging.debug('CodeFormer face restoration completed successfully')
394
- result_image = Image.fromarray(result_image)
395
-
396
- output_buffer = io.BytesIO()
397
- result_image.save(output_buffer, format=output_format)
398
- image_data = output_buffer.getvalue()
399
-
400
- return base64.b64encode(image_data).decode('utf-8')
401
-
402
-
403
- app = Flask(__name__)
404
-
405
-
406
- @app.errorhandler(400)
407
- def not_found(error):
408
- return make_response(jsonify(
409
- {
410
- 'status': 'error',
411
- 'msg': f'Bad Request',
412
- 'detail': str(error)
413
- }
414
- ), 400)
415
-
416
-
417
- @app.errorhandler(404)
418
- def not_found(error):
419
- return make_response(jsonify(
420
- {
421
- 'status': 'error',
422
- 'msg': f'{request.url} not found',
423
- 'detail': str(error)
424
- }
425
- ), 404)
426
-
427
-
428
- @app.errorhandler(500)
429
- def internal_server_error(error):
430
- return make_response(jsonify(
431
- {
432
- 'status': 'error',
433
- 'msg': 'Internal Server Error',
434
- 'detail': str(error)
435
- }
436
- ), 500)
437
-
438
-
439
- @app.route('/', methods=['GET'])
440
- def ping():
441
- return make_response(jsonify(
442
- {
443
- 'status': 'ok'
444
- }
445
- ), 200)
446
-
447
-
448
- @app.route('/faceswap', methods=['POST'])
449
- def face_swap_api():
450
- total_timer = Timer()
451
- logging.debug('Received face swap API request')
452
- payload = request.get_json()
453
-
454
- if not os.path.exists(TMP_PATH):
455
- logging.debug(f'Creating temporary directory: {TMP_PATH}')
456
- os.makedirs(TMP_PATH)
457
-
458
- unique_id = uuid.uuid4()
459
- source_image_data = payload['source_image']
460
- target_image_data = payload['target_image']
461
-
462
- # Decode the source image data
463
- source_image = base64.b64decode(source_image_data)
464
- source_file_extension = determine_file_extension(source_image_data)
465
- source_image_path = f'{TMP_PATH}/source_{unique_id}{source_file_extension}'
466
-
467
- # Save the source image to disk
468
- with open(source_image_path, 'wb') as source_file:
469
- source_file.write(source_image)
470
-
471
- # Decode the target image data
472
- target_image = base64.b64decode(target_image_data)
473
- target_file_extension = determine_file_extension(target_image_data)
474
- target_image_path = f'{TMP_PATH}/target_{unique_id}{target_file_extension}'
475
 
476
- # Save the target image to disk
477
- with open(target_image_path, 'wb') as target_file:
478
- target_file.write(target_image)
 
 
 
479
 
480
- # Set defaults if they are not specified in the payload
481
- if 'source_indexes' not in payload:
482
- payload['source_indexes'] = '-1'
 
 
 
 
 
 
 
 
 
 
 
 
483
 
484
- if 'target_indexes' not in payload:
485
- payload['target_indexes'] = '-1'
486
 
487
- if 'background_enhance' not in payload:
488
- payload['background_enhance'] = True
489
 
490
- if 'face_restore' not in payload:
491
- payload['face_restore'] = True
492
 
493
- if 'face_upsample' not in payload:
494
- payload['face_upsample'] = True
495
 
496
- if 'upscale' not in payload:
497
- payload['upscale'] = 1
 
498
 
499
- if 'codeformer_fidelity' not in payload:
500
- payload['codeformer_fidelity'] = 0.5
 
501
 
502
- if 'output_format' not in payload:
503
- payload['output_format'] = 'JPEG'
504
 
505
- try:
506
- logging.debug(f'Source indexes: {payload["source_indexes"]}')
507
- logging.debug(f'Target indexes: {payload["target_indexes"]}')
508
- logging.debug(f'Background enhance: {payload["background_enhance"]}')
509
- logging.debug(f'Face Restoration: {payload["face_restore"]}')
510
- logging.debug(f'Face Upsampling: {payload["face_upsample"]}')
511
- logging.debug(f'Upscale: {payload["upscale"]}')
512
- logging.debug(f'Codeformer Fidelity: {payload["codeformer_fidelity"]}')
513
- logging.debug(f'Output Format: {payload["output_format"]}')
514
 
515
- result_image = face_swap(
516
- source_image_path,
517
- target_image_path,
518
- payload['source_indexes'],
519
- payload['target_indexes'],
520
- payload['background_enhance'],
521
- payload['face_restore'],
522
- payload['face_upsample'],
523
- payload['upscale'],
524
- payload['codeformer_fidelity'],
525
- payload['output_format']
526
- )
527
 
528
- status_code = 200
 
529
 
530
- response = {
531
- 'status': 'ok',
532
- 'image': result_image
533
- }
534
- except Exception as e:
535
- logging.error(e)
536
 
537
- response = {
538
- 'status': 'error',
539
- 'msg': 'Face swap failed',
540
- 'detail': str(e)
541
  }
542
 
543
- status_code = 500
544
-
545
- # Clean up temporary images
546
- os.remove(source_image_path)
547
- os.remove(target_image_path)
548
-
549
- total_time = total_timer.get_elapsed_time()
550
- logging.info(f'Total time taken for face swap API call {total_time} seconds')
551
-
552
- return make_response(jsonify(response), status_code)
553
-
 
 
554
 
555
- if __name__ == '__main__':
556
- args = get_args()
557
 
558
- serve(
559
- app,
560
- host=args.host,
561
- port=args.port
562
- )
 
1
+ from flask import Flask, request, jsonify
 
 
2
  import os
 
 
 
 
 
 
 
 
3
  import time
4
+ import tempfile
5
  import cv2
6
  import insightface
7
+ import onnxruntime
8
+ import gfpgan
9
+ import io
10
+ import concurrent.futures
11
  import numpy as np
 
12
  from PIL import Image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
+ app = Flask(__name__)
15
 
16
+ class Predictor:
17
  def __init__(self):
18
+ self.setup()
19
+
20
+ def setup(self):
21
+ os.makedirs('models', exist_ok=True)
22
+ os.chdir('models')
23
+ if not os.path.exists('GFPGANv1.4.pth'):
24
+ os.system(
25
+ 'wget https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.4.pth'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  )
27
+ if not os.path.exists('inswapper_128.onnx'):
28
+ os.system(
29
+ 'wget https://huggingface.co/ashleykleynhans/inswapper/resolve/main/inswapper_128.onnx'
30
+ )
31
+ os.chdir('..')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
+ """Load the model into memory to make running multiple predictions efficient"""
34
+ self.face_swapper = insightface.model_zoo.get_model('models/inswapper_128.onnx',
35
+ providers=onnxruntime.get_available_providers())
36
+ self.face_enhancer = gfpgan.GFPGANer(model_path='models/GFPGANv1.4.pth', upscale=1)
37
+ self.face_analyser = insightface.app.FaceAnalysis(name='buffalo_l')
38
+ self.face_analyser.prepare(ctx_id=0, det_size=(640, 640))
39
 
40
+ def get_face(self, img_data):
41
+ analysed = self.face_analyser.get(img_data)
42
+ try:
43
+ largest = max(analysed, key=lambda x: (x.bbox[2] - x.bbox[0]) * (x.bbox[3] - x.bbox[1]))
44
+ return largest
45
+ except:
46
+ print("No face found")
47
+ return None
48
+
49
+ def process_images(self, input_image, swap_image):
50
+ """Process a pair of images: target image and swap image"""
51
+ try:
52
+ # Read the input images directly from memory
53
+ input_image_data = np.asarray(bytearray(input_image.read()), dtype=np.uint8)
54
+ swap_image_data = np.asarray(bytearray(swap_image.read()), dtype=np.uint8)
55
 
56
+ frame = cv2.imdecode(input_image_data, cv2.IMREAD_COLOR)
57
+ swap_frame = cv2.imdecode(swap_image_data, cv2.IMREAD_COLOR)
58
 
59
+ face = self.get_face(frame)
60
+ source_face = self.get_face(swap_frame)
61
 
62
+ if face is None or source_face is None:
63
+ return None
64
 
65
+ result = self.face_swapper.get(frame, face, source_face, paste_back=True)
66
+ _, _, result = self.face_enhancer.enhance(result, paste_back=True)
67
 
68
+ # Create a result image in memory
69
+ _, result_image = cv2.imencode('.jpg', result)
70
+ return result_image.tobytes()
71
 
72
+ except Exception as e:
73
+ print(f"Error in processing images: {e}")
74
+ return None
75
 
76
+ # Instantiate the Predictor class
77
+ predictor = Predictor()
78
 
79
+ @app.route('/predict', methods=['POST'])
80
+ def predict():
81
+ if 'target_images' not in request.files or 'swap_images' not in request.files:
82
+ return jsonify({'error': 'No image files provided'}), 400
 
 
 
 
 
83
 
84
+ target_images = request.files.getlist('target_images')
85
+ swap_images = request.files.getlist('swap_images')
 
 
 
 
 
 
 
 
 
 
86
 
87
+ if len(target_images) != len(swap_images):
88
+ return jsonify({'error': 'Number of target images must match number of swap images'}), 400
89
 
90
+ results = []
 
 
 
 
 
91
 
92
+ with concurrent.futures.ThreadPoolExecutor() as executor:
93
+ future_to_pair = {
94
+ executor.submit(predictor.process_images, target_images[i], swap_images[i]): i
95
+ for i in range(len(target_images))
96
  }
97
 
98
+ for future in concurrent.futures.as_completed(future_to_pair):
99
+ idx = future_to_pair[future]
100
+ result = future.result()
101
+ if result:
102
+ results.append({
103
+ 'index': idx,
104
+ 'result_image': result
105
+ })
106
+ else:
107
+ results.append({
108
+ 'index': idx,
109
+ 'error': 'Face swap failed'
110
+ })
111
 
112
+ return jsonify({'results': results})
 
113
 
114
+ if __name__ == "__main__":
115
+ app.run(debug=True, threaded=True)