Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c6a6e6352 | ||
|
|
c7b8556856 | ||
|
|
0458e56cb9 | ||
|
|
17f031356a | ||
|
|
58ddc725db | ||
|
|
d74478114f | ||
|
|
f680b43100 | ||
|
|
7298d8650d | ||
|
|
1242914609 | ||
|
|
da963b8796 | ||
|
|
4493fd7133 | ||
|
|
613b7d74ea | ||
|
|
1ee4d0e949 | ||
|
|
9b0cc02791 | ||
|
|
ad51f737d1 | ||
|
|
d2f6b7d29d | ||
|
|
f57c2acc55 | ||
|
|
be31df8040 | ||
|
|
2b945ae0b2 | ||
|
|
7633e58182 | ||
|
|
97598387b1 | ||
|
|
1b706db767 | ||
|
|
8651bc2639 | ||
|
|
b7b094ba78 | ||
|
|
bb3e97c74c |
16
API.md
16
API.md
@ -83,3 +83,19 @@ A list of available models can be seen by GET:
|
|||||||
* http://127.0.0.1:7860/reactor/models
|
* http://127.0.0.1:7860/reactor/models
|
||||||
* http://127.0.0.1:7860/reactor/upscalers
|
* http://127.0.0.1:7860/reactor/upscalers
|
||||||
* http://127.0.0.1:7860/reactor/facemodels
|
* http://127.0.0.1:7860/reactor/facemodels
|
||||||
|
|
||||||
|
### FaceModel Buid API
|
||||||
|
|
||||||
|
Send POST to http://127.0.0.1:7860/reactor/facemodels with body:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"source_images": ["data:image/png;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAABQAAD/7g...","data:image/png;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAABQAAD/7g...","data:image/png;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAABQAAD/7g..."],
|
||||||
|
"name": "my_super_model",
|
||||||
|
"compute_method": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
where:<br>
|
||||||
|
"source_images" is a list of base64 encoded images,<br>
|
||||||
|
"compute_method" is: 0 - Mean, 1- Median, 2 - Mode
|
||||||
|
|||||||
11
README.md
11
README.md
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<img src="https://github.com/Gourieff/Assets/raw/main/sd-webui-reactor/ReActor_logo_NEW_EN.png?raw=true" alt="logo" width="180px"/>
|
<img src="https://github.com/Gourieff/Assets/raw/main/sd-webui-reactor/ReActor_logo_NEW_EN.png?raw=true" alt="logo" width="180px"/>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
<a href="https://boosty.to/artgourieff" target="_blank">
|
<a href="https://boosty.to/artgourieff" target="_blank">
|
||||||
<img src="https://lovemet.ru/www/boosty.jpg" width="108" alt="Support Me on Boosty"/>
|
<img src="https://lovemet.ru/www/boosty.jpg" width="108" alt="Support Me on Boosty"/>
|
||||||
@ -40,8 +40,15 @@
|
|||||||
|
|
||||||
## What's new in the latest updates
|
## What's new in the latest updates
|
||||||
|
|
||||||
|
### 0.7.1 <sub><sup>BETA1
|
||||||
|
|
||||||
|
- Allow spaces for face indexes (e.g.: 0, 1, 2)
|
||||||
|
- Sorting of face models list alphabetically
|
||||||
|
- [FaceModels Build API](./API.md#facemodel-build-api)
|
||||||
|
- Fixes and improvements
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><a>Click to expand</a></summary>
|
<summary><a>Click to expand more</a></summary>
|
||||||
|
|
||||||
### 0.7.0 <sub><sup>BETA2
|
### 0.7.0 <sub><sup>BETA2
|
||||||
|
|
||||||
|
|||||||
11
README_RU.md
11
README_RU.md
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<img src="https://github.com/Gourieff/Assets/raw/main/sd-webui-reactor/ReActor_logo_NEW_RU.png?raw=true" alt="logo" width="180px"/>
|
<img src="https://github.com/Gourieff/Assets/raw/main/sd-webui-reactor/ReActor_logo_NEW_RU.png?raw=true" alt="logo" width="180px"/>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
<a href="https://boosty.to/artgourieff" target="_blank">
|
<a href="https://boosty.to/artgourieff" target="_blank">
|
||||||
<img src="https://lovemet.ru/www/boosty.jpg" width="108" alt="Поддержать проект на Boosty"/>
|
<img src="https://lovemet.ru/www/boosty.jpg" width="108" alt="Поддержать проект на Boosty"/>
|
||||||
@ -39,8 +39,15 @@
|
|||||||
|
|
||||||
## Что нового в последних обновлениях
|
## Что нового в последних обновлениях
|
||||||
|
|
||||||
|
### 0.7.1 <sub><sup>BETA1
|
||||||
|
|
||||||
|
- Использование пробелов в индексах лиц (пример: 0, 1, 2)
|
||||||
|
- Список моделей лиц теперь отсортирован по алфавиту
|
||||||
|
- [API для создания моделей лиц](./API.md#facemodel-build-api)
|
||||||
|
- Правки и улучшения
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><a>Нажмите, чтобы посмотреть</a></summary>
|
<summary><a>Нажмите, чтобы посмотреть больше</a></summary>
|
||||||
|
|
||||||
### 0.7.0 <sub><sup>BETA2
|
### 0.7.0 <sub><sup>BETA2
|
||||||
|
|
||||||
|
|||||||
27
install.py
27
install.py
@ -21,20 +21,6 @@ req_file = os.path.join(BASE_PATH, "requirements.txt")
|
|||||||
|
|
||||||
models_dir = os.path.join(models_path, "insightface")
|
models_dir = os.path.join(models_path, "insightface")
|
||||||
|
|
||||||
# DEPRECATED:
|
|
||||||
# models_dir_old = os.path.join(models_path, "roop")
|
|
||||||
# if os.path.exists(models_dir_old):
|
|
||||||
# if not os.listdir(models_dir_old) and (not os.listdir(models_dir) or not os.path.exists(models_dir)):
|
|
||||||
# os.rename(models_dir_old, models_dir)
|
|
||||||
# else:
|
|
||||||
# import shutil
|
|
||||||
# for file in os.listdir(models_dir_old):
|
|
||||||
# shutil.move(os.path.join(models_dir_old, file), os.path.join(models_dir, file))
|
|
||||||
# try:
|
|
||||||
# os.rmdir(models_dir_old)
|
|
||||||
# except Exception as e:
|
|
||||||
# print(f"OSError: {e}")
|
|
||||||
|
|
||||||
model_url = "https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/inswapper_128.onnx"
|
model_url = "https://huggingface.co/datasets/Gourieff/ReActor/resolve/main/models/inswapper_128.onnx"
|
||||||
model_name = os.path.basename(model_url)
|
model_name = os.path.basename(model_url)
|
||||||
model_path = os.path.join(models_dir, model_name)
|
model_path = os.path.join(models_dir, model_name)
|
||||||
@ -117,12 +103,17 @@ with open(req_file) as file:
|
|||||||
last_device = "CPU"
|
last_device = "CPU"
|
||||||
with open(os.path.join(BASE_PATH, "last_device.txt"), "w") as txt:
|
with open(os.path.join(BASE_PATH, "last_device.txt"), "w") as txt:
|
||||||
txt.write(last_device)
|
txt.write(last_device)
|
||||||
if cuda_version is not None and float(cuda_version)>=12: # CU12
|
if cuda_version is not None:
|
||||||
if not is_installed(ort,"1.17.1",False):
|
if float(cuda_version)>=12: # CU12.x
|
||||||
|
extra_index_url = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/"
|
||||||
|
else: # CU11.8
|
||||||
|
extra_index_url = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-11/pypi/simple"
|
||||||
|
if not is_installed(ort,"1.17.1",True):
|
||||||
install_count += 1
|
install_count += 1
|
||||||
|
ort = "onnxruntime-gpu==1.17.1"
|
||||||
pip_uninstall("onnxruntime", "onnxruntime-gpu")
|
pip_uninstall("onnxruntime", "onnxruntime-gpu")
|
||||||
pip_install(ort,"--extra-index-url","https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/")
|
pip_install(ort,"--extra-index-url",extra_index_url)
|
||||||
elif not is_installed(ort,"1.16.1",False):
|
elif not is_installed(ort,"1.18.1",False):
|
||||||
install_count += 1
|
install_count += 1
|
||||||
pip_install(ort, "-U")
|
pip_install(ort, "-U")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -11,9 +11,14 @@ from modules import shared
|
|||||||
# SAVE_ORIGINAL: bool = False
|
# SAVE_ORIGINAL: bool = False
|
||||||
|
|
||||||
def update_fm_list(selected: str):
|
def update_fm_list(selected: str):
|
||||||
return gr.Dropdown.update(
|
try: # GR3.x
|
||||||
value=selected, choices=get_model_names(get_facemodels)
|
return gr.Dropdown.update(
|
||||||
)
|
value=selected, choices=get_model_names(get_facemodels)
|
||||||
|
)
|
||||||
|
except: # GR4.x
|
||||||
|
return gr.Dropdown(
|
||||||
|
value=selected, choices=get_model_names(get_facemodels)
|
||||||
|
)
|
||||||
|
|
||||||
# TAB MAIN
|
# TAB MAIN
|
||||||
def show(is_img2img: bool, show_br: bool = True, **msgs):
|
def show(is_img2img: bool, show_br: bool = True, **msgs):
|
||||||
@ -24,29 +29,56 @@ def show(is_img2img: bool, show_br: bool = True, **msgs):
|
|||||||
if evt.index == 2:
|
if evt.index == 2:
|
||||||
# if SAVE_ORIGINAL != selected:
|
# if SAVE_ORIGINAL != selected:
|
||||||
# SAVE_ORIGINAL = selected
|
# SAVE_ORIGINAL = selected
|
||||||
return {
|
try: # GR3.x
|
||||||
control_col_1: gr.Column.update(visible=False),
|
return {
|
||||||
control_col_2: gr.Column.update(visible=False),
|
control_col_1: gr.Column.update(visible=False),
|
||||||
control_col_3: gr.Column.update(visible=True),
|
control_col_2: gr.Column.update(visible=False),
|
||||||
# save_original: gr.Checkbox.update(value=False,visible=False),
|
control_col_3: gr.Column.update(visible=True),
|
||||||
imgs_hash_clear: gr.Button.update(visible=True)
|
# save_original: gr.Checkbox.update(value=False,visible=False),
|
||||||
}
|
imgs_hash_clear: gr.Button.update(visible=True)
|
||||||
|
}
|
||||||
|
except: # GR4.x
|
||||||
|
return {
|
||||||
|
control_col_1: gr.Column(visible=False),
|
||||||
|
control_col_2: gr.Column(visible=False),
|
||||||
|
control_col_3: gr.Column(visible=True),
|
||||||
|
# save_original: gr.Checkbox.update(value=False,visible=False),
|
||||||
|
imgs_hash_clear: gr.Button(visible=True)
|
||||||
|
}
|
||||||
if evt.index == 0:
|
if evt.index == 0:
|
||||||
return {
|
try: # GR3.x
|
||||||
control_col_1: gr.Column.update(visible=True),
|
return {
|
||||||
control_col_2: gr.Column.update(visible=False),
|
control_col_1: gr.Column.update(visible=True),
|
||||||
control_col_3: gr.Column.update(visible=False),
|
control_col_2: gr.Column.update(visible=False),
|
||||||
# save_original: gr.Checkbox.update(value=SAVE_ORIGINAL,visible=show_br),
|
control_col_3: gr.Column.update(visible=False),
|
||||||
imgs_hash_clear: gr.Button.update(visible=False)
|
# save_original: gr.Checkbox.update(value=SAVE_ORIGINAL,visible=show_br),
|
||||||
}
|
imgs_hash_clear: gr.Button.update(visible=False)
|
||||||
|
}
|
||||||
|
except: # GR4.x
|
||||||
|
return {
|
||||||
|
control_col_1: gr.Column(visible=True),
|
||||||
|
control_col_2: gr.Column(visible=False),
|
||||||
|
control_col_3: gr.Column(visible=False),
|
||||||
|
# save_original: gr.Checkbox.update(value=SAVE_ORIGINAL,visible=show_br),
|
||||||
|
imgs_hash_clear: gr.Button(visible=False)
|
||||||
|
}
|
||||||
if evt.index == 1:
|
if evt.index == 1:
|
||||||
return {
|
try: # GR3.x
|
||||||
control_col_1: gr.Column.update(visible=False),
|
return {
|
||||||
control_col_2: gr.Column.update(visible=True),
|
control_col_1: gr.Column.update(visible=False),
|
||||||
control_col_3: gr.Column.update(visible=False),
|
control_col_2: gr.Column.update(visible=True),
|
||||||
# save_original: gr.Checkbox.update(value=SAVE_ORIGINAL,visible=show_br),
|
control_col_3: gr.Column.update(visible=False),
|
||||||
imgs_hash_clear: gr.Button.update(visible=False)
|
# save_original: gr.Checkbox.update(value=SAVE_ORIGINAL,visible=show_br),
|
||||||
}
|
imgs_hash_clear: gr.Button.update(visible=False)
|
||||||
|
}
|
||||||
|
except: # GR4.x
|
||||||
|
return {
|
||||||
|
control_col_1: gr.Column(visible=False),
|
||||||
|
control_col_2: gr.Column(visible=True),
|
||||||
|
control_col_3: gr.Column(visible=False),
|
||||||
|
# save_original: gr.Checkbox.update(value=SAVE_ORIGINAL,visible=show_br),
|
||||||
|
imgs_hash_clear: gr.Button(visible=False)
|
||||||
|
}
|
||||||
|
|
||||||
progressbar_area = gr.Markdown("")
|
progressbar_area = gr.Markdown("")
|
||||||
with gr.Tab("Main"):
|
with gr.Tab("Main"):
|
||||||
@ -85,16 +117,21 @@ def show(is_img2img: bool, show_br: bool = True, **msgs):
|
|||||||
imgs_hash_clear.click(clear_faces_list,None,[progressbar_area])
|
imgs_hash_clear.click(clear_faces_list,None,[progressbar_area])
|
||||||
gr.Markdown("<br>", visible=show_br)
|
gr.Markdown("<br>", visible=show_br)
|
||||||
with gr.Column(visible=True) as control_col_1:
|
with gr.Column(visible=True) as control_col_1:
|
||||||
gr.Markdown("<center>🔽🔽🔽 Single Image has priority when both Areas in use 🔽🔽🔽</center>")
|
|
||||||
with gr.Row():
|
with gr.Row():
|
||||||
img = gr.Image(
|
selected_tab = gr.Textbox('tab_single', visible=False)
|
||||||
type="pil",
|
with gr.Tabs() as tab_single:
|
||||||
label="Single Source Image",
|
with gr.Tab('Single'):
|
||||||
)
|
img = gr.Image(
|
||||||
imgs = gr.Files(
|
type="pil",
|
||||||
label=f"Multiple Source Images{msgs['extra_multiple_source']}",
|
label="Single Source Image",
|
||||||
file_types=["image"],
|
)
|
||||||
)
|
with gr.Tab('Multiple') as tab_multiple:
|
||||||
|
imgs = gr.Files(
|
||||||
|
label=f"Multiple Source Images{msgs['extra_multiple_source']}",
|
||||||
|
file_types=["image"],
|
||||||
|
)
|
||||||
|
tab_single.select(fn=lambda: 'tab_single', inputs=[], outputs=[selected_tab])
|
||||||
|
tab_multiple.select(fn=lambda: 'tab_multiple', inputs=[], outputs=[selected_tab])
|
||||||
with gr.Column(visible=False) as control_col_3:
|
with gr.Column(visible=False) as control_col_3:
|
||||||
gr.Markdown("<span style='display:block;text-align:right;padding-right:3px;margin: -15px 0;font-size:1.1em'><sup>Clear Hash if you see the previous face was swapped instead of the new one</sup></span>")
|
gr.Markdown("<span style='display:block;text-align:right;padding-right:3px;margin: -15px 0;font-size:1.1em'><sup>Clear Hash if you see the previous face was swapped instead of the new one</sup></span>")
|
||||||
with gr.Row():
|
with gr.Row():
|
||||||
@ -189,4 +226,4 @@ def show(is_img2img: bool, show_br: bool = True, **msgs):
|
|||||||
# select_source.select(on_select_source,[save_original],[control_col_1,control_col_2,control_col_3,save_original,imgs_hash_clear],show_progress=False)
|
# select_source.select(on_select_source,[save_original],[control_col_1,control_col_2,control_col_3,save_original,imgs_hash_clear],show_progress=False)
|
||||||
select_source.select(on_select_source,None,[control_col_1,control_col_2,control_col_3,imgs_hash_clear],show_progress=False)
|
select_source.select(on_select_source,None,[control_col_1,control_col_2,control_col_3,imgs_hash_clear],show_progress=False)
|
||||||
|
|
||||||
return img, imgs, select_source, face_model, source_folder, save_original, mask_face, source_faces_index, gender_source, faces_index, gender_target, face_restorer_name, face_restorer_visibility, codeformer_weight, swap_in_source, swap_in_generated, random_image
|
return img, imgs, selected_tab, select_source, face_model, source_folder, save_original, mask_face, source_faces_index, gender_source, faces_index, gender_target, face_restorer_name, face_restorer_visibility, codeformer_weight, swap_in_source, swap_in_generated, random_image
|
||||||
|
|||||||
@ -39,7 +39,7 @@ def show():
|
|||||||
value="Mean",
|
value="Mean",
|
||||||
label="Compute Method",
|
label="Compute Method",
|
||||||
type="index",
|
type="index",
|
||||||
info="Mean (recommended) - Average value (best result 👍); Median* - Mid-point value (may be funny 😅); Mode - Most common value (may be scary 😨); *Mean and Median will be simillar if you load two images"
|
info="Mean (recommended) - Average value (best result 👍); Median* - Mid-point value (may be funny 😅); Mode - Most common value (may be scary 😨); *Mean and Median will be similar if you load two images"
|
||||||
)
|
)
|
||||||
shape_check = gr.Checkbox(
|
shape_check = gr.Checkbox(
|
||||||
False,
|
False,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
albumentations==1.4.3
|
albumentations==1.4.3
|
||||||
insightface==0.7.3
|
insightface==0.7.3
|
||||||
onnx>=1.14.0
|
onnx==1.16.1
|
||||||
opencv-python>=4.7.0.72
|
opencv-python>=4.7.0.72
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import os.path as osp
|
|||||||
import glob
|
import glob
|
||||||
import logging
|
import logging
|
||||||
import insightface
|
import insightface
|
||||||
from insightface.model_zoo.model_zoo import ModelRouter, PickableInferenceSession
|
from insightface.model_zoo.model_zoo import ModelRouter, PickableInferenceSession, get_default_providers
|
||||||
from insightface.model_zoo.retinaface import RetinaFace
|
from insightface.model_zoo.retinaface import RetinaFace
|
||||||
from insightface.model_zoo.landmark import Landmark
|
from insightface.model_zoo.landmark import Landmark
|
||||||
from insightface.model_zoo.attribute import Attribute
|
from insightface.model_zoo.attribute import Attribute
|
||||||
@ -97,15 +97,20 @@ def patched_inswapper_init(self, model_file=None, session=None):
|
|||||||
self.input_size = tuple(input_shape[2:4][::-1])
|
self.input_size = tuple(input_shape[2:4][::-1])
|
||||||
|
|
||||||
|
|
||||||
def patch_insightface(get_model, faceanalysis_init, faceanalysis_prepare, inswapper_init):
|
def patched_get_default_providers():
|
||||||
|
return ['TensorrtExecutionProvider', 'CUDAExecutionProvider', 'CPUExecutionProvider']
|
||||||
|
|
||||||
|
|
||||||
|
def patch_insightface(get_default_providers, get_model, faceanalysis_init, faceanalysis_prepare, inswapper_init):
|
||||||
|
insightface.model_zoo.model_zoo.get_default_providers = get_default_providers
|
||||||
insightface.model_zoo.model_zoo.ModelRouter.get_model = get_model
|
insightface.model_zoo.model_zoo.ModelRouter.get_model = get_model
|
||||||
insightface.app.FaceAnalysis.__init__ = faceanalysis_init
|
insightface.app.FaceAnalysis.__init__ = faceanalysis_init
|
||||||
insightface.app.FaceAnalysis.prepare = faceanalysis_prepare
|
insightface.app.FaceAnalysis.prepare = faceanalysis_prepare
|
||||||
insightface.model_zoo.inswapper.INSwapper.__init__ = inswapper_init
|
insightface.model_zoo.inswapper.INSwapper.__init__ = inswapper_init
|
||||||
|
|
||||||
|
|
||||||
original_functions = [ModelRouter.get_model, FaceAnalysis.__init__, FaceAnalysis.prepare, INSwapper.__init__]
|
original_functions = [patched_get_default_providers, ModelRouter.get_model, FaceAnalysis.__init__, FaceAnalysis.prepare, INSwapper.__init__]
|
||||||
patched_functions = [patched_get_model, patched_faceanalysis_init, patched_faceanalysis_prepare, patched_inswapper_init]
|
patched_functions = [patched_get_default_providers, patched_get_model, patched_faceanalysis_init, patched_faceanalysis_prepare, patched_inswapper_init]
|
||||||
|
|
||||||
|
|
||||||
def apply_logging_patch(console_logging_level):
|
def apply_logging_patch(console_logging_level):
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
'''
|
'''
|
||||||
Thanks SpenserCai for the original version of the roop api script
|
Thanks SpenserCai for the original version of the roop api script
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
--- ReActor External API v1.0.7 ---
|
--- ReActor External API v1.0.8a ---
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
'''
|
'''
|
||||||
import os, glob
|
import os, glob
|
||||||
@ -13,6 +13,11 @@ from fastapi import FastAPI, Body
|
|||||||
# import base64
|
# import base64
|
||||||
# import numpy as np
|
# import numpy as np
|
||||||
# import cv2
|
# import cv2
|
||||||
|
import asyncio
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
# from concurrent.futures.process import ProcessPoolExecutor
|
||||||
|
# from contextlib import asynccontextmanager
|
||||||
|
# import multiprocessing
|
||||||
|
|
||||||
# from modules.api.models import *
|
# from modules.api.models import *
|
||||||
from modules import scripts, shared
|
from modules import scripts, shared
|
||||||
@ -20,18 +25,32 @@ from modules.api import api
|
|||||||
|
|
||||||
import gradio as gr
|
import gradio as gr
|
||||||
|
|
||||||
from scripts.reactor_swapper import EnhancementOptions, swap_face, DetectionOptions
|
from scripts.reactor_swapper import EnhancementOptions, blend_faces, swap_face, DetectionOptions
|
||||||
from scripts.reactor_logger import logger
|
from scripts.reactor_logger import logger
|
||||||
from scripts.reactor_helpers import get_facemodels
|
from scripts.reactor_helpers import get_facemodels
|
||||||
|
|
||||||
# XYZ init:
|
|
||||||
from scripts.reactor_xyz import run
|
# @asynccontextmanager
|
||||||
try:
|
# async def lifespan(app: FastAPI):
|
||||||
import modules.script_callbacks as script_callbacks
|
# app.state.executor = ProcessPoolExecutor(max_workers=4)
|
||||||
script_callbacks.on_before_ui(run)
|
# yield
|
||||||
# script_callbacks.on_app_started(reactor_api)
|
# app.state.executor.shutdown()
|
||||||
except:
|
|
||||||
pass
|
# app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
|
# def run_app(a: FastAPI):
|
||||||
|
# global app
|
||||||
|
# a = app
|
||||||
|
# return a
|
||||||
|
|
||||||
|
# _executor_tp = ThreadPoolExecutor(max_workers=8)
|
||||||
|
# def entry_point():
|
||||||
|
# _executor_pp = ProcessPoolExecutor(max_workers=8)
|
||||||
|
# pool = multiprocessing.Pool(4)
|
||||||
|
|
||||||
|
async def run_event(app, fn, *args):
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
return await loop.run_in_executor(app.state.executor, fn, *args)
|
||||||
|
|
||||||
|
|
||||||
def default_file_path():
|
def default_file_path():
|
||||||
@ -82,6 +101,7 @@ def get_full_model(model_name):
|
|||||||
# raise HTTPException(status_code=500, detail="Invalid encoded image") from e
|
# raise HTTPException(status_code=500, detail="Invalid encoded image") from e
|
||||||
|
|
||||||
def reactor_api(_: gr.Blocks, app: FastAPI):
|
def reactor_api(_: gr.Blocks, app: FastAPI):
|
||||||
|
app.state.executor = ThreadPoolExecutor(max_workers=8)
|
||||||
@app.post("/reactor/image")
|
@app.post("/reactor/image")
|
||||||
async def reactor_image(
|
async def reactor_image(
|
||||||
source_image: str = Body("",title="Source Face Image"),
|
source_image: str = Body("",title="Source Face Image"),
|
||||||
@ -131,22 +151,26 @@ def reactor_api(_: gr.Blocks, app: FastAPI):
|
|||||||
use_model = get_full_model(model)
|
use_model = get_full_model(model)
|
||||||
if use_model is None:
|
if use_model is None:
|
||||||
Exception("Model not found")
|
Exception("Model not found")
|
||||||
result = swap_face(s_image, t_image, use_model, sf_index, f_index, up_options, gender_s, gender_t, True, True, device, mask_face, select_source, face_model, source_folder, None, random_image,det_options)
|
|
||||||
result_img = result[0]
|
args = [s_image, t_image, use_model, sf_index, f_index, up_options, gender_s, gender_t, True, True, device, mask_face, select_source, face_model, source_folder, None, random_image,det_options]
|
||||||
|
# result,_,_ = pool.map(swap_face, *args)
|
||||||
|
result,_,_ = await run_event(app,swap_face,*args)
|
||||||
|
# result,_,_ = swap_face(s_image, t_image, use_model, sf_index, f_index, up_options, gender_s, gender_t, True, True, device, mask_face, select_source, face_model, source_folder, None, random_image,det_options)
|
||||||
|
|
||||||
if alpha is not None:
|
if alpha is not None:
|
||||||
result_img = result_img.convert("RGBA")
|
result = result.convert("RGBA")
|
||||||
result_img.putalpha(alpha)
|
result.putalpha(alpha)
|
||||||
|
|
||||||
if save_to_file == 1:
|
if save_to_file == 1:
|
||||||
if result_file_path == "":
|
if result_file_path == "":
|
||||||
result_file_path = default_file_path()
|
result_file_path = default_file_path()
|
||||||
try:
|
try:
|
||||||
result_img.save(result_file_path, format='PNG')
|
file_format = os.path.split(result_file_path)[1].split(".")[1]
|
||||||
|
result.save(result_file_path, format=file_format)
|
||||||
logger.status("Result has been saved to: %s", result_file_path)
|
logger.status("Result has been saved to: %s", result_file_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error while saving result: %s",e)
|
logger.error("Error while saving result: %s",e)
|
||||||
return {"image": api.encode_pil_to_base64(result_img)}
|
return {"image": api.encode_pil_to_base64(result)}
|
||||||
|
|
||||||
@app.get("/reactor/models")
|
@app.get("/reactor/models")
|
||||||
async def reactor_models():
|
async def reactor_models():
|
||||||
@ -163,9 +187,18 @@ def reactor_api(_: gr.Blocks, app: FastAPI):
|
|||||||
facemodels = [os.path.split(model)[1].split(".")[0] for model in get_facemodels()]
|
facemodels = [os.path.split(model)[1].split(".")[0] for model in get_facemodels()]
|
||||||
return {"facemodels": facemodels}
|
return {"facemodels": facemodels}
|
||||||
|
|
||||||
|
@app.post("/reactor/facemodels")
|
||||||
|
async def reactor_facemodels_build(
|
||||||
|
source_images: list[str] = Body([""],title="Source Face Image List"),
|
||||||
|
name: str = Body("",title="Face Model Name"),
|
||||||
|
compute_method: int = Body(0,title="Compute Method (Mean, Median, Mode)"),
|
||||||
|
):
|
||||||
|
images = [api.decode_base64_to_image(img) for img in source_images]
|
||||||
|
blend_faces(images, name, compute_method, False, is_api=True)
|
||||||
|
return {"facemodels": [os.path.split(model)[1].split(".")[0] for model in get_facemodels()]}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import modules.script_callbacks as script_callbacks
|
import modules.script_callbacks as script_callbacks
|
||||||
|
|
||||||
script_callbacks.on_app_started(reactor_api)
|
script_callbacks.on_app_started(reactor_api)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -6,7 +6,7 @@ from typing import List
|
|||||||
|
|
||||||
import modules.scripts as scripts
|
import modules.scripts as scripts
|
||||||
from modules.upscaler import Upscaler, UpscalerData
|
from modules.upscaler import Upscaler, UpscalerData
|
||||||
from modules import scripts, shared, images, scripts_postprocessing, ui_components
|
from modules import scripts, shared, images, scripts_postprocessing
|
||||||
from modules.processing import (
|
from modules.processing import (
|
||||||
Processed,
|
Processed,
|
||||||
StableDiffusionProcessing,
|
StableDiffusionProcessing,
|
||||||
@ -39,6 +39,20 @@ from scripts.reactor_helpers import (
|
|||||||
)
|
)
|
||||||
from scripts.reactor_globals import SWAPPER_MODELS_PATH #, DEVICE, DEVICE_LIST
|
from scripts.reactor_globals import SWAPPER_MODELS_PATH #, DEVICE, DEVICE_LIST
|
||||||
|
|
||||||
|
def IA_cap(cond: bool, label: str=""):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from modules.ui_components import InputAccordion
|
||||||
|
NO_IA = False
|
||||||
|
except:
|
||||||
|
NO_IA = True
|
||||||
|
InputAccordion = IA_cap
|
||||||
|
|
||||||
|
|
||||||
|
def check_old_webui():
|
||||||
|
return NO_IA
|
||||||
|
|
||||||
|
|
||||||
class FaceSwapScript(scripts.Script):
|
class FaceSwapScript(scripts.Script):
|
||||||
def title(self):
|
def title(self):
|
||||||
@ -48,19 +62,12 @@ class FaceSwapScript(scripts.Script):
|
|||||||
return scripts.AlwaysVisible
|
return scripts.AlwaysVisible
|
||||||
|
|
||||||
def ui(self, is_img2img):
|
def ui(self, is_img2img):
|
||||||
with ui_components.InputAccordion(False, label=f"{app_title}") as enable:
|
with (
|
||||||
# with gr.Accordion(f"{app_title}", open=False):
|
gr.Accordion(f"{app_title}", open=False) if check_old_webui() else InputAccordion(False, label=f"{app_title}") as enable
|
||||||
|
):
|
||||||
|
|
||||||
# def on_files_upload_uncheck_so(selected: bool):
|
# SD.Next or A1111 1.52:
|
||||||
# global SAVE_ORIGINAL
|
if get_SDNEXT() or check_old_webui():
|
||||||
# SAVE_ORIGINAL = selected
|
|
||||||
# return gr.Checkbox.update(value=False,visible=False)
|
|
||||||
# def on_files_clear():
|
|
||||||
# clear_faces_list()
|
|
||||||
# return gr.Checkbox.update(value=SAVE_ORIGINAL,visible=True)
|
|
||||||
|
|
||||||
# SD.Next fix
|
|
||||||
if get_SDNEXT():
|
|
||||||
enable = gr.Checkbox(False, label="Enable")
|
enable = gr.Checkbox(False, label="Enable")
|
||||||
|
|
||||||
# enable = gr.Checkbox(False, label="Enable", info=f"The Fast and Simple FaceSwap Extension - {version_flag}")
|
# enable = gr.Checkbox(False, label="Enable", info=f"The Fast and Simple FaceSwap Extension - {version_flag}")
|
||||||
@ -70,7 +77,7 @@ class FaceSwapScript(scripts.Script):
|
|||||||
msgs: dict = {
|
msgs: dict = {
|
||||||
"extra_multiple_source": "",
|
"extra_multiple_source": "",
|
||||||
}
|
}
|
||||||
img, imgs, select_source, face_model, source_folder, save_original, mask_face, source_faces_index, gender_source, faces_index, gender_target, face_restorer_name, face_restorer_visibility, codeformer_weight, swap_in_source, swap_in_generated, random_image = ui_main.show(is_img2img=is_img2img, **msgs)
|
img, imgs, selected_tab, select_source, face_model, source_folder, save_original, mask_face, source_faces_index, gender_source, faces_index, gender_target, face_restorer_name, face_restorer_visibility, codeformer_weight, swap_in_source, swap_in_generated, random_image = ui_main.show(is_img2img=is_img2img, **msgs)
|
||||||
|
|
||||||
# TAB DETECTION
|
# TAB DETECTION
|
||||||
det_thresh, det_maxnum = ui_detection.show()
|
det_thresh, det_maxnum = ui_detection.show()
|
||||||
@ -116,7 +123,8 @@ class FaceSwapScript(scripts.Script):
|
|||||||
random_image,
|
random_image,
|
||||||
upscale_force,
|
upscale_force,
|
||||||
det_thresh,
|
det_thresh,
|
||||||
det_maxnum
|
det_maxnum,
|
||||||
|
selected_tab,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -186,7 +194,8 @@ class FaceSwapScript(scripts.Script):
|
|||||||
random_image,
|
random_image,
|
||||||
upscale_force,
|
upscale_force,
|
||||||
det_thresh,
|
det_thresh,
|
||||||
det_maxnum
|
det_maxnum,
|
||||||
|
selected_tab,
|
||||||
):
|
):
|
||||||
self.enable = enable
|
self.enable = enable
|
||||||
if self.enable:
|
if self.enable:
|
||||||
@ -198,7 +207,10 @@ class FaceSwapScript(scripts.Script):
|
|||||||
return
|
return
|
||||||
|
|
||||||
global SWAPPER_MODELS_PATH
|
global SWAPPER_MODELS_PATH
|
||||||
self.source = img
|
if selected_tab == "tab_single":
|
||||||
|
self.source = img
|
||||||
|
else:
|
||||||
|
self.source = None
|
||||||
self.face_restorer_name = face_restorer_name
|
self.face_restorer_name = face_restorer_name
|
||||||
self.upscaler_scale = upscaler_scale
|
self.upscaler_scale = upscaler_scale
|
||||||
self.upscaler_visibility = upscaler_visibility
|
self.upscaler_visibility = upscaler_visibility
|
||||||
@ -220,7 +232,10 @@ class FaceSwapScript(scripts.Script):
|
|||||||
self.select_source = select_source
|
self.select_source = select_source
|
||||||
self.face_model = face_model
|
self.face_model = face_model
|
||||||
self.source_folder = source_folder
|
self.source_folder = source_folder
|
||||||
self.source_imgs = imgs
|
if selected_tab == "tab_single":
|
||||||
|
self.source_imgs = None
|
||||||
|
else:
|
||||||
|
self.source_imgs = imgs
|
||||||
self.random_image = random_image
|
self.random_image = random_image
|
||||||
self.upscale_force = upscale_force
|
self.upscale_force = upscale_force
|
||||||
self.det_thresh=det_thresh
|
self.det_thresh=det_thresh
|
||||||
@ -230,10 +245,10 @@ class FaceSwapScript(scripts.Script):
|
|||||||
if self.gender_target is None or self.gender_target == "No":
|
if self.gender_target is None or self.gender_target == "No":
|
||||||
self.gender_target = 0
|
self.gender_target = 0
|
||||||
self.source_faces_index = [
|
self.source_faces_index = [
|
||||||
int(x) for x in source_faces_index.strip(",").split(",") if x.isnumeric()
|
int(x) for x in source_faces_index.strip().replace(" ", "").strip(",").split(",") if x.isnumeric()
|
||||||
]
|
]
|
||||||
self.faces_index = [
|
self.faces_index = [
|
||||||
int(x) for x in faces_index.strip(",").split(",") if x.isnumeric()
|
int(x) for x in faces_index.strip().replace(" ", "").strip(",").split(",") if x.isnumeric()
|
||||||
]
|
]
|
||||||
if len(self.source_faces_index) == 0:
|
if len(self.source_faces_index) == 0:
|
||||||
self.source_faces_index = [0]
|
self.source_faces_index = [0]
|
||||||
@ -488,11 +503,14 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing):
|
|||||||
order = 20000
|
order = 20000
|
||||||
|
|
||||||
def ui(self):
|
def ui(self):
|
||||||
with ui_components.InputAccordion(False, label=f"{app_title}") as enable:
|
with (
|
||||||
|
gr.Accordion(f"{app_title}", open=False) if check_old_webui() else InputAccordion(False, label=f"{app_title}") as enable
|
||||||
|
):
|
||||||
|
# with ui_components.InputAccordion(False, label=f"{app_title}") as enable:
|
||||||
# with gr.Accordion(f"{app_title}", open=False):
|
# with gr.Accordion(f"{app_title}", open=False):
|
||||||
|
|
||||||
# SD.Next fix
|
# SD.Next or A1111 1.52:
|
||||||
if get_SDNEXT():
|
if get_SDNEXT() or check_old_webui():
|
||||||
enable = gr.Checkbox(False, label="Enable")
|
enable = gr.Checkbox(False, label="Enable")
|
||||||
|
|
||||||
# enable = gr.Checkbox(False, label="Enable", info=f"The Fast and Simple FaceSwap Extension - {version_flag}")
|
# enable = gr.Checkbox(False, label="Enable", info=f"The Fast and Simple FaceSwap Extension - {version_flag}")
|
||||||
@ -500,9 +518,9 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing):
|
|||||||
|
|
||||||
# TAB MAIN
|
# TAB MAIN
|
||||||
msgs: dict = {
|
msgs: dict = {
|
||||||
"extra_multiple_source": " | Сomparison grid as a result",
|
"extra_multiple_source": "",
|
||||||
}
|
}
|
||||||
img, imgs, select_source, face_model, source_folder, save_original, mask_face, source_faces_index, gender_source, faces_index, gender_target, face_restorer_name, face_restorer_visibility, codeformer_weight, swap_in_source, swap_in_generated, random_image = ui_main.show(is_img2img=False, show_br=False, **msgs)
|
img, imgs, selected_tab, select_source, face_model, source_folder, save_original, mask_face, source_faces_index, gender_source, faces_index, gender_target, face_restorer_name, face_restorer_visibility, codeformer_weight, swap_in_source, swap_in_generated, random_image = ui_main.show(is_img2img=False, show_br=False, **msgs)
|
||||||
|
|
||||||
# TAB DETECTION
|
# TAB DETECTION
|
||||||
det_thresh, det_maxnum = ui_detection.show()
|
det_thresh, det_maxnum = ui_detection.show()
|
||||||
@ -544,6 +562,7 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing):
|
|||||||
'upscale_force': upscale_force,
|
'upscale_force': upscale_force,
|
||||||
'det_thresh': det_thresh,
|
'det_thresh': det_thresh,
|
||||||
'det_maxnum': det_maxnum,
|
'det_maxnum': det_maxnum,
|
||||||
|
'selected_tab': selected_tab,
|
||||||
}
|
}
|
||||||
return args
|
return args
|
||||||
|
|
||||||
@ -588,7 +607,10 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing):
|
|||||||
return
|
return
|
||||||
|
|
||||||
global SWAPPER_MODELS_PATH
|
global SWAPPER_MODELS_PATH
|
||||||
self.source = args['img']
|
if args['selected_tab'] == "tab_single":
|
||||||
|
self.source = args['img']
|
||||||
|
else:
|
||||||
|
self.source = None
|
||||||
self.face_restorer_name = args['face_restorer_name']
|
self.face_restorer_name = args['face_restorer_name']
|
||||||
self.upscaler_scale = args['upscaler_scale']
|
self.upscaler_scale = args['upscaler_scale']
|
||||||
self.upscaler_visibility = args['upscaler_visibility']
|
self.upscaler_visibility = args['upscaler_visibility']
|
||||||
@ -605,7 +627,10 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing):
|
|||||||
self.select_source = args['select_source']
|
self.select_source = args['select_source']
|
||||||
self.face_model = args['face_model']
|
self.face_model = args['face_model']
|
||||||
self.source_folder = args['source_folder']
|
self.source_folder = args['source_folder']
|
||||||
self.source_imgs = args['imgs']
|
if args['selected_tab'] == "tab_single":
|
||||||
|
self.source_imgs = None
|
||||||
|
else:
|
||||||
|
self.source_imgs = args['imgs']
|
||||||
self.random_image = args['random_image']
|
self.random_image = args['random_image']
|
||||||
self.upscale_force = args['upscale_force']
|
self.upscale_force = args['upscale_force']
|
||||||
self.det_thresh = args['det_thresh']
|
self.det_thresh = args['det_thresh']
|
||||||
@ -684,9 +709,13 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing):
|
|||||||
if len(result) > 0 and swapped > 0:
|
if len(result) > 0 and swapped > 0:
|
||||||
image = result[0]
|
image = result[0]
|
||||||
if len(result) > 1:
|
if len(result) > 1:
|
||||||
grid = make_grid(result)
|
if hasattr(pp, 'extra_images'):
|
||||||
result.insert(0, grid)
|
image = result[0]
|
||||||
image = grid
|
pp.extra_images.extend(result[1:])
|
||||||
|
else:
|
||||||
|
grid = make_grid(result)
|
||||||
|
result.insert(0, grid)
|
||||||
|
image = grid
|
||||||
pp.info["ReActor"] = True
|
pp.info["ReActor"] = True
|
||||||
pp.image = image
|
pp.image = image
|
||||||
logger.status("---Done!---")
|
logger.status("---Done!---")
|
||||||
|
|||||||
@ -203,9 +203,12 @@ def get_facemodels():
|
|||||||
|
|
||||||
def get_model_names(get_models):
|
def get_model_names(get_models):
|
||||||
models = get_models()
|
models = get_models()
|
||||||
names = ["None"]
|
names = []
|
||||||
for x in models:
|
for x in models:
|
||||||
names.append(os.path.basename(x))
|
names.append(os.path.basename(x))
|
||||||
|
# Sort ignoring case during sort but retain in output
|
||||||
|
names.sort(key=str.lower)
|
||||||
|
names.insert(0, "None")
|
||||||
return names
|
return names
|
||||||
|
|
||||||
def get_images_from_folder(path: str):
|
def get_images_from_folder(path: str):
|
||||||
|
|||||||
@ -661,13 +661,19 @@ def build_face_model(image: Image.Image, name: str, save_model: bool = True, det
|
|||||||
logger.error(no_face_msg)
|
logger.error(no_face_msg)
|
||||||
return no_face_msg
|
return no_face_msg
|
||||||
|
|
||||||
def blend_faces(images_list: List, name: str, compute_method: int = 0, shape_check: bool = False):
|
def blend_faces(images_list: List, name: str, compute_method: int = 0, shape_check: bool = False, is_api: bool = False):
|
||||||
faces = []
|
faces = []
|
||||||
embeddings = []
|
embeddings = []
|
||||||
images: List[Image.Image] = []
|
images: List[Image.Image] = []
|
||||||
images, images_names = get_images_from_list(images_list)
|
if not is_api:
|
||||||
|
images, images_names = get_images_from_list(images_list)
|
||||||
|
else:
|
||||||
|
images = images_list
|
||||||
for i,image in enumerate(images):
|
for i,image in enumerate(images):
|
||||||
logger.status(f"Building Face Model for {images_names[i]}...")
|
if not is_api:
|
||||||
|
logger.status(f"Building Face Model for {images_names[i]}...")
|
||||||
|
else:
|
||||||
|
logger.status(f"Building Face Model for Image {i+1}...")
|
||||||
face = build_face_model(image,str(i),save_model=False)
|
face = build_face_model(image,str(i),save_model=False)
|
||||||
if isinstance(face, str):
|
if isinstance(face, str):
|
||||||
# logger.error(f"No faces found in {images_names[i]}, skipping")
|
# logger.error(f"No faces found in {images_names[i]}, skipping")
|
||||||
@ -676,7 +682,10 @@ def blend_faces(images_list: List, name: str, compute_method: int = 0, shape_che
|
|||||||
if i == 0:
|
if i == 0:
|
||||||
embedding_shape = face.embedding.shape
|
embedding_shape = face.embedding.shape
|
||||||
elif face.embedding.shape != embedding_shape:
|
elif face.embedding.shape != embedding_shape:
|
||||||
logger.error(f"Embedding Shape Mismatch for {images_names[i]}, skipping")
|
if not is_api:
|
||||||
|
logger.error(f"Embedding Shape Mismatch for {images_names[i]}, skipping")
|
||||||
|
else:
|
||||||
|
logger.error(f"Embedding Shape Mismatch for Image {i+1}, skipping")
|
||||||
continue
|
continue
|
||||||
faces.append(face)
|
faces.append(face)
|
||||||
embeddings.append(face.embedding)
|
embeddings.append(face.embedding)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
app_title = "ReActor"
|
app_title = "ReActor"
|
||||||
version_flag = "v0.7.0"
|
version_flag = "v0.7.1-b2"
|
||||||
|
|
||||||
from scripts.reactor_logger import logger, get_Run, set_Run
|
from scripts.reactor_logger import logger, get_Run, set_Run
|
||||||
from scripts.reactor_globals import DEVICE
|
from scripts.reactor_globals import DEVICE
|
||||||
|
|||||||
@ -10,6 +10,7 @@ from scripts.reactor_helpers import (
|
|||||||
get_facemodels
|
get_facemodels
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# xyz_grid = [x for x in scripts.scripts_data if x.script_class.__module__ == "xyz_grid.py"][0].module
|
# xyz_grid = [x for x in scripts.scripts_data if x.script_class.__module__ == "xyz_grid.py"][0].module
|
||||||
|
|
||||||
def find_module(module_names):
|
def find_module(module_names):
|
||||||
@ -84,3 +85,10 @@ def run():
|
|||||||
xyz_grid = find_module("xyz_grid.py, xy_grid.py")
|
xyz_grid = find_module("xyz_grid.py, xy_grid.py")
|
||||||
if xyz_grid:
|
if xyz_grid:
|
||||||
add_axis_options(xyz_grid)
|
add_axis_options(xyz_grid)
|
||||||
|
|
||||||
|
# XYZ init:
|
||||||
|
try:
|
||||||
|
import modules.script_callbacks as script_callbacks
|
||||||
|
script_callbacks.on_before_ui(run)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user