Zacharytrackmaster commited on
Commit
db3362b
Β·
verified Β·
1 Parent(s): c4fb519

Upload 11 files

Browse files
Files changed (11) hide show
  1. .dockerignore +17 -0
  2. .gitattributes +12 -34
  3. .gitignore +33 -0
  4. Dockerfile +60 -0
  5. LICENSE +56 -0
  6. README.md +161 -0
  7. app.py +145 -0
  8. build.sh +3 -0
  9. engine.py +228 -0
  10. loader.py +165 -0
  11. requirements.txt +83 -0
.dockerignore ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # The .dockerignore file excludes files from the container build process.
2
+ #
3
+ # https://docs.docker.com/engine/reference/builder/#dockerignore-file
4
+
5
+ # Exclude Git files
6
+ .git
7
+ .github
8
+ .gitignore
9
+
10
+ # Exclude Python cache files
11
+ __pycache__
12
+ .mypy_cache
13
+ .pytest_cache
14
+ .ruff_cache
15
+
16
+ # Exclude Python virtual environment
17
+ /venv
.gitattributes CHANGED
@@ -1,35 +1,13 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
1
+ *.jpg filter=lfs diff=lfs merge=lfs -text
2
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
3
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
4
+ *.png filter=lfs diff=lfs merge=lfs -text
5
+ *.xml filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  *.zip filter=lfs diff=lfs merge=lfs -text
7
+ *.pdf filter=lfs diff=lfs merge=lfs -text
8
+ *.mp3 filter=lfs diff=lfs merge=lfs -text
9
+ *.wav filter=lfs diff=lfs merge=lfs -text
10
+ *.mpg filter=lfs diff=lfs merge=lfs -text
11
+ *.webp filter=lfs diff=lfs merge=lfs -text
12
+ *.webm filter=lfs diff=lfs merge=lfs -text
13
+ *.gif filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ **/__pycache__/
4
+ *.py[cod]
5
+ **/*.py[cod]
6
+ *$py.class
7
+
8
+ miniserver.py
9
+
10
+ # Model weights
11
+ **/*.pth
12
+ **/*.onnx
13
+
14
+ # Ipython notebook
15
+ *.ipynb
16
+
17
+ # Temporary files or benchmark resources
18
+ animations/*
19
+ tmp/*
20
+
21
+ # more ignores
22
+ .DS_Store
23
+ *.log
24
+ .idea/
25
+ .vscode/
26
+ *.pyc
27
+ .ipynb_checkpoints
28
+ results/
29
+ data/audio/*.wav
30
+ data/video/*.mp4
31
+ ffmpeg-7.0-amd64-static
32
+ venv/
33
+ .cog/
Dockerfile ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM nvidia/cuda:12.4.0-devel-ubuntu22.04
2
+
3
+ ARG DEBIAN_FRONTEND=noninteractive
4
+
5
+ ENV PYTHONUNBUFFERED=1
6
+
7
+ RUN apt-get update && apt-get install --no-install-recommends -y \
8
+ build-essential \
9
+ python3.11 \
10
+ python3-pip \
11
+ python3-dev \
12
+ git \
13
+ curl \
14
+ ffmpeg \
15
+ libglib2.0-0 \
16
+ libsm6 \
17
+ libxrender1 \
18
+ libxext6 \
19
+ && apt-get clean && rm -rf /var/lib/apt/lists/*
20
+
21
+ WORKDIR /code
22
+
23
+ COPY ./requirements.txt /code/requirements.txt
24
+
25
+ # Install pget as root
26
+ RUN echo "Installing pget" && \
27
+ curl -o /usr/local/bin/pget -L 'https://github.com/replicate/pget/releases/download/v0.2.1/pget' && \
28
+ chmod +x /usr/local/bin/pget
29
+
30
+ # Set up a new user named "user" with user ID 1000
31
+ RUN useradd -m -u 1000 user
32
+ # Switch to the "user" user
33
+ USER user
34
+ # Set home to the user's home directory
35
+ ENV HOME=/home/user \
36
+ PATH=/home/user/.local/bin:$PATH
37
+
38
+
39
+ # Set home to the user's home directory
40
+ ENV PYTHONPATH=$HOME/app \
41
+ PYTHONUNBUFFERED=1 \
42
+ DATA_ROOT=/tmp/data
43
+
44
+ RUN echo "Installing requirements.txt"
45
+ RUN pip3 install --no-cache-dir --upgrade -r /code/requirements.txt
46
+
47
+ # yeah.. this is manual for now
48
+ #RUN cd client
49
+ #RUN bun i
50
+ #RUN bun build ./src/index.tsx --outdir ../public/
51
+
52
+ WORKDIR $HOME/app
53
+
54
+ COPY --chown=user . $HOME/app
55
+
56
+ EXPOSE 8080
57
+
58
+ ENV PORT 8080
59
+
60
+ CMD python3 app.py
LICENSE ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## For FacePoke (the modifications I made + the server itself)
2
+
3
+ MIT License
4
+
5
+ Copyright (c) 2024 Julian Bilcke
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
24
+
25
+ ## For LivePortrait
26
+
27
+ MIT License
28
+
29
+ Copyright (c) 2024 Kuaishou Visual Generation and Interaction Center
30
+
31
+ Permission is hereby granted, free of charge, to any person obtaining a copy
32
+ of this software and associated documentation files (the "Software"), to deal
33
+ in the Software without restriction, including without limitation the rights
34
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
35
+ copies of the Software, and to permit persons to whom the Software is
36
+ furnished to do so, subject to the following conditions:
37
+
38
+ The above copyright notice and this permission notice shall be included in all
39
+ copies or substantial portions of the Software.
40
+
41
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
42
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
43
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
44
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
45
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
46
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
47
+ SOFTWARE.
48
+
49
+ ---
50
+
51
+ The code of InsightFace is released under the MIT License.
52
+ The models of InsightFace are for non-commercial research purposes only.
53
+
54
+ If you want to use the LivePortrait project for commercial purposes, you
55
+ should remove and replace InsightFace’s detection models to fully comply with
56
+ the MIT license.
README.md ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: FacePoke
3
+ emoji: πŸ™‚β€β†”οΈπŸ‘ˆ
4
+ colorFrom: yellow
5
+ colorTo: red
6
+ sdk: docker
7
+ pinned: true
8
+ license: mit
9
+ header: mini
10
+ app_file: app.py
11
+ app_port: 8080
12
+ disable_embedding: true
13
+ short_description: Import a portrait, click to move the head!
14
+ ---
15
+
16
+ # FacePoke
17
+
18
+ ## Table of Contents
19
+
20
+ - [Introduction](#introduction)
21
+ - [Acknowledgements](#acknowledgements)
22
+ - [Installation](#installation)
23
+ - [Local Setup](#local-setup)
24
+ - [Docker Deployment](#docker-deployment)
25
+ - [Development](#development)
26
+ - [Contributing](#contributing)
27
+ - [License](#license)
28
+
29
+ ## Introduction
30
+
31
+ A real-time head transformation app.
32
+
33
+ For best performance please run the app from your own machine (local or in the cloud).
34
+
35
+ **Repository**: [GitHub - jbilcke-hf/FacePoke](https://github.com/jbilcke-hf/FacePoke)
36
+
37
+ You can try the demo but it is a shared space, latency may be high if there are multiple users or if you live far from the datacenter hosting the Hugging Face Space.
38
+
39
+ **Live Demo**: [FacePoke on Hugging Face Spaces](https://huggingface.co/spaces/jbilcke-hf/FacePoke)
40
+
41
+ # Funding
42
+
43
+ FacePoke is just a tiny project!
44
+
45
+ There are no plans to create a proprietary/expensive cloud-only blackbox SaaS for FacePoke, so if you like it, you can buy me a coffee 🫢
46
+
47
+ <a href="https://www.buymeacoffee.com/flngr" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
48
+
49
+ ## Acknowledgements
50
+
51
+ This project is based on LivePortrait: https://arxiv.org/abs/2407.03168
52
+
53
+ It uses the face transformation routines from https://github.com/PowerHouseMan/ComfyUI-AdvancedLivePortrait
54
+
55
+ ## Installation
56
+
57
+ ### Before you install
58
+
59
+ FacePoke has only been tested in a Linux environment, using `Python 3.10` and `CUDA 12.4` (so a NVIDIA GPU).
60
+
61
+ Contributions are welcome to help supporting other platforms!
62
+
63
+ ### Local Setup
64
+
65
+ 1. Make sure you have Git and Git LFS installed globally (https://git-lfs.com):
66
+
67
+ ```bash
68
+ git lfs install
69
+ ```
70
+
71
+ 2. Clone the repository:
72
+ ```bash
73
+ git clone https://github.com/jbilcke-hf/FacePoke.git
74
+ cd FacePoke
75
+ ```
76
+
77
+ 3. Install Python dependencies:
78
+
79
+ Using a virtual environment (Python venv) is strongly recommended.
80
+
81
+ FacePoke has been tested with `Python 3.10`.
82
+
83
+ ```bash
84
+ pip3 install --upgrade -r requirements.txt
85
+ ```
86
+
87
+ 4. Install frontend dependencies:
88
+ ```bash
89
+ cd client
90
+ bun install
91
+ ```
92
+
93
+ 5. Build the frontend:
94
+ ```bash
95
+ bun build ./src/index.tsx --outdir ../public/
96
+ ```
97
+
98
+ 6. Start the backend server:
99
+ ```bash
100
+ python app.py
101
+ ```
102
+
103
+ 7. Open `http://localhost:8080` in your web browser.
104
+
105
+ ### Docker Deployment
106
+
107
+ 1. Build the Docker image:
108
+ ```bash
109
+ docker build -t facepoke .
110
+ ```
111
+
112
+ 2. Run the container:
113
+ ```bash
114
+ docker run -p 8080:8080 facepoke
115
+ ```
116
+
117
+ 3. To deploy to Hugging Face Spaces:
118
+ - Fork the repository on GitHub.
119
+ - Create a new Space on Hugging Face.
120
+ - Connect your GitHub repository to the Space.
121
+ - Configure the Space to use the Docker runtime.
122
+
123
+
124
+ Note: by default Hugging Face runs the `main` branch, so if you want to push a feature branch you need to do this:
125
+
126
+ ```bash
127
+ git push <space_repo> <feature_branch>:main -f
128
+ ```
129
+
130
+ ## Development
131
+
132
+ The project structure is organized as follows:
133
+
134
+ - `app.py`: Main backend server handling WebSocket connections.
135
+ - `engine.py`: Core logic.
136
+ - `loader.py`: Initializes and loads AI models.
137
+ - `client/`: Frontend React application.
138
+ - `src/`: TypeScript source files.
139
+ - `public/`: Static assets and built files.
140
+
141
+ ### Increasing the framerate
142
+
143
+ I am testing various things to increase the framerate.
144
+
145
+ One project is to only transmit the modified head, instead of the whole image.
146
+
147
+ Another one is to automatically adapt to the server and network speed.
148
+
149
+ ## Contributing
150
+
151
+ Contributions to FacePoke are welcome! Please read our [Contributing Guidelines](CONTRIBUTING.md) for details on how to submit pull requests, report issues, or request features.
152
+
153
+ ## License
154
+
155
+ FacePoke is released under the MIT License. See the [LICENSE](LICENSE) file for details.
156
+
157
+ Please note that while the code of LivePortrait and Insightface are open-source with "no limitation for both academic and commercial usage", the model weights trained from Insightface data are available for [non-commercial research purposes only](https://github.com/deepinsight/insightface?tab=readme-ov-file#license).
158
+
159
+ ---
160
+
161
+ Developed with ❀️ by Julian Bilcke at Hugging Face
app.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FacePoke API
3
+
4
+ Author: Julian Bilcke
5
+ Date: September 30, 2024
6
+ """
7
+
8
+ import sys
9
+ import asyncio
10
+ from aiohttp import web, WSMsgType
11
+ import json
12
+ from json import JSONEncoder
13
+ import numpy as np
14
+ import uuid
15
+ import logging
16
+ import os
17
+ import signal
18
+ from typing import Dict, Any, List, Optional
19
+ import base64
20
+ import io
21
+
22
+ from PIL import Image
23
+
24
+ # by popular demand, let's add support for avif
25
+ import pillow_avif
26
+
27
+ # Configure logging
28
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
29
+ logger = logging.getLogger(__name__)
30
+
31
+ # Set asyncio logger to DEBUG level
32
+ #logging.getLogger("asyncio").setLevel(logging.INFO)
33
+
34
+ #logger.debug(f"Python version: {sys.version}")
35
+
36
+ # SIGSEGV handler
37
+ def SIGSEGV_signal_arises(signalNum, stack):
38
+ logger.critical(f"{signalNum} : SIGSEGV arises")
39
+ logger.critical(f"Stack trace: {stack}")
40
+
41
+ signal.signal(signal.SIGSEGV, SIGSEGV_signal_arises)
42
+
43
+ from loader import initialize_models
44
+ from engine import Engine, base64_data_uri_to_PIL_Image
45
+
46
+ # Global constants
47
+ DATA_ROOT = os.environ.get('DATA_ROOT', '/tmp/data')
48
+ MODELS_DIR = os.path.join(DATA_ROOT, "models")
49
+
50
+ class NumpyEncoder(json.JSONEncoder):
51
+ def default(self, obj):
52
+ if isinstance(obj, np.integer):
53
+ return int(obj)
54
+ elif isinstance(obj, np.floating):
55
+ return float(obj)
56
+ elif isinstance(obj, np.ndarray):
57
+ return obj.tolist()
58
+ else:
59
+ return super(NumpyEncoder, self).default(obj)
60
+
61
+ async def websocket_handler(request: web.Request) -> web.WebSocketResponse:
62
+ ws = web.WebSocketResponse()
63
+ await ws.prepare(request)
64
+ engine = request.app['engine']
65
+ try:
66
+ #logger.info("New WebSocket connection established")
67
+ while True:
68
+ msg = await ws.receive()
69
+
70
+ if msg.type in (WSMsgType.CLOSE, WSMsgType.ERROR):
71
+ #logger.warning(f"WebSocket connection closed: {msg.type}")
72
+ break
73
+
74
+ try:
75
+ if msg.type == WSMsgType.BINARY:
76
+ res = await engine.load_image(msg.data)
77
+ json_res = json.dumps(res, cls=NumpyEncoder)
78
+ await ws.send_str(json_res)
79
+
80
+ elif msg.type == WSMsgType.TEXT:
81
+ data = json.loads(msg.data)
82
+ webp_bytes = await engine.transform_image(data.get('uuid'), data.get('params'))
83
+ await ws.send_bytes(webp_bytes)
84
+
85
+ except Exception as e:
86
+ logger.error(f"Error in engine: {str(e)}")
87
+ logger.exception("Full traceback:")
88
+ await ws.send_json({"error": str(e)})
89
+
90
+ except Exception as e:
91
+ logger.error(f"Error in websocket_handler: {str(e)}")
92
+ logger.exception("Full traceback:")
93
+ return ws
94
+
95
+ async def index(request: web.Request) -> web.Response:
96
+ """Serve the index.html file"""
97
+ content = open(os.path.join(os.path.dirname(__file__), "public", "index.html"), "r").read()
98
+ return web.Response(content_type="text/html", text=content)
99
+
100
+ async def js_index(request: web.Request) -> web.Response:
101
+ """Serve the index.js file"""
102
+ content = open(os.path.join(os.path.dirname(__file__), "public", "index.js"), "r").read()
103
+ return web.Response(content_type="application/javascript", text=content)
104
+
105
+ async def hf_logo(request: web.Request) -> web.Response:
106
+ """Serve the hf-logo.svg file"""
107
+ content = open(os.path.join(os.path.dirname(__file__), "public", "hf-logo.svg"), "r").read()
108
+ return web.Response(content_type="image/svg+xml", text=content)
109
+
110
+ async def initialize_app() -> web.Application:
111
+ """Initialize and configure the web application."""
112
+ try:
113
+ logger.info("Initializing application...")
114
+ live_portrait = await initialize_models()
115
+
116
+ logger.info("πŸš€ Creating Engine instance...")
117
+ engine = Engine(live_portrait=live_portrait)
118
+ logger.info("βœ… Engine instance created.")
119
+
120
+ app = web.Application()
121
+ app['engine'] = engine
122
+
123
+ # Configure routes
124
+ app.router.add_get("/", index)
125
+ app.router.add_get("/index.js", js_index)
126
+ app.router.add_get("/hf-logo.svg", hf_logo)
127
+ app.router.add_get("/ws", websocket_handler)
128
+
129
+ logger.info("Application routes configured")
130
+
131
+ return app
132
+ except Exception as e:
133
+ logger.error(f"🚨 Error during application initialization: {str(e)}")
134
+ logger.exception("Full traceback:")
135
+ raise
136
+
137
+ if __name__ == "__main__":
138
+ try:
139
+ logger.info("Starting FacePoke application")
140
+ app = asyncio.run(initialize_app())
141
+ logger.info("Application initialized, starting web server")
142
+ web.run_app(app, host="0.0.0.0", port=8080)
143
+ except Exception as e:
144
+ logger.critical(f"🚨 FATAL: Failed to start the app: {str(e)}")
145
+ logger.exception("Full traceback:")
build.sh ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ cd client
2
+ bun i
3
+ bun build ./src/index.tsx --outdir ../public/
engine.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ import logging
3
+ import hashlib
4
+ import os
5
+ import io
6
+ import asyncio
7
+ from async_lru import alru_cache
8
+ import base64
9
+ from queue import Queue
10
+ from typing import Dict, Any, List, Optional, Union
11
+ from functools import lru_cache
12
+ import numpy as np
13
+ import torch
14
+ import torch.nn.functional as F
15
+ from PIL import Image, ImageOps
16
+
17
+ from liveportrait.config.argument_config import ArgumentConfig
18
+ from liveportrait.utils.camera import get_rotation_matrix
19
+ from liveportrait.utils.io import resize_to_limit
20
+ from liveportrait.utils.crop import prepare_paste_back, paste_back, parse_bbox_from_landmark
21
+
22
+ # Configure logging
23
+ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Global constants
27
+ DATA_ROOT = os.environ.get('DATA_ROOT', '/tmp/data')
28
+ MODELS_DIR = os.path.join(DATA_ROOT, "models")
29
+
30
+ def base64_data_uri_to_PIL_Image(base64_string: str) -> Image.Image:
31
+ """
32
+ Convert a base64 data URI to a PIL Image.
33
+
34
+ Args:
35
+ base64_string (str): The base64 encoded image data.
36
+
37
+ Returns:
38
+ Image.Image: The decoded PIL Image.
39
+ """
40
+ if ',' in base64_string:
41
+ base64_string = base64_string.split(',')[1]
42
+ img_data = base64.b64decode(base64_string)
43
+ return Image.open(io.BytesIO(img_data))
44
+
45
+ class Engine:
46
+ """
47
+ The main engine class for FacePoke
48
+ """
49
+
50
+ def __init__(self, live_portrait):
51
+ """
52
+ Initialize the FacePoke engine with necessary models and processors.
53
+
54
+ Args:
55
+ live_portrait (LivePortraitPipeline): The LivePortrait model for video generation.
56
+ """
57
+ self.live_portrait = live_portrait
58
+
59
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
60
+
61
+ self.processed_cache = {} # Stores the processed image data
62
+
63
+ logger.info("βœ… FacePoke Engine initialized successfully.")
64
+
65
+ @alru_cache(maxsize=512)
66
+ async def load_image(self, data):
67
+ image = Image.open(io.BytesIO(data))
68
+
69
+ # keep the exif orientation (fix the selfie issue on iphone)
70
+ image = ImageOps.exif_transpose(image)
71
+
72
+ # Convert the image to RGB mode (removes alpha channel if present)
73
+ image = image.convert('RGB')
74
+
75
+ uid = str(uuid.uuid4())
76
+ img_rgb = np.array(image)
77
+
78
+ inference_cfg = self.live_portrait.live_portrait_wrapper.cfg
79
+ img_rgb = await asyncio.to_thread(resize_to_limit, img_rgb, inference_cfg.ref_max_shape, inference_cfg.ref_shape_n)
80
+ crop_info = await asyncio.to_thread(self.live_portrait.cropper.crop_single_image, img_rgb)
81
+ img_crop_256x256 = crop_info['img_crop_256x256']
82
+
83
+ I_s = await asyncio.to_thread(self.live_portrait.live_portrait_wrapper.prepare_source, img_crop_256x256)
84
+ x_s_info = await asyncio.to_thread(self.live_portrait.live_portrait_wrapper.get_kp_info, I_s)
85
+ f_s = await asyncio.to_thread(self.live_portrait.live_portrait_wrapper.extract_feature_3d, I_s)
86
+ x_s = await asyncio.to_thread(self.live_portrait.live_portrait_wrapper.transform_keypoint, x_s_info)
87
+
88
+ processed_data = {
89
+ 'img_rgb': img_rgb,
90
+ 'crop_info': crop_info,
91
+ 'x_s_info': x_s_info,
92
+ 'f_s': f_s,
93
+ 'x_s': x_s,
94
+ 'inference_cfg': inference_cfg
95
+ }
96
+
97
+ self.processed_cache[uid] = processed_data
98
+
99
+ # Calculate the bounding box
100
+ bbox_info = parse_bbox_from_landmark(processed_data['crop_info']['lmk_crop'], scale=1.0)
101
+
102
+ return {
103
+ 'u': uid,
104
+
105
+ # those aren't easy to serialize
106
+ 'c': bbox_info['center'], # 2x1
107
+ 's': bbox_info['size'], # scalar
108
+ 'b': bbox_info['bbox'], # 4x2
109
+ 'a': bbox_info['angle'], # rad, counterclockwise
110
+ # 'bbox_rot': bbox_info['bbox_rot'].toList(), # 4x2
111
+ }
112
+
113
+ async def transform_image(self, uid: str, params: Dict[str, float]) -> bytes:
114
+ # If we don't have the image in cache yet, add it
115
+ if uid not in self.processed_cache:
116
+ raise ValueError("cache miss")
117
+
118
+ processed_data = self.processed_cache[uid]
119
+
120
+ try:
121
+ # Apply modifications based on params
122
+ x_d_new = processed_data['x_s_info']['kp'].clone()
123
+
124
+ # Adapted from https://github.com/PowerHouseMan/ComfyUI-AdvancedLivePortrait/blob/main/nodes.py#L408-L472
125
+ modifications = [
126
+ ('smile', [
127
+ (0, 20, 1, -0.01), (0, 14, 1, -0.02), (0, 17, 1, 0.0065), (0, 17, 2, 0.003),
128
+ (0, 13, 1, -0.00275), (0, 16, 1, -0.00275), (0, 3, 1, -0.0035), (0, 7, 1, -0.0035)
129
+ ]),
130
+ ('aaa', [
131
+ (0, 19, 1, 0.001), (0, 19, 2, 0.0001), (0, 17, 1, -0.0001)
132
+ ]),
133
+ ('eee', [
134
+ (0, 20, 2, -0.001), (0, 20, 1, -0.001), (0, 14, 1, -0.001)
135
+ ]),
136
+ ('woo', [
137
+ (0, 14, 1, 0.001), (0, 3, 1, -0.0005), (0, 7, 1, -0.0005), (0, 17, 2, -0.0005)
138
+ ]),
139
+ ('wink', [
140
+ (0, 11, 1, 0.001), (0, 13, 1, -0.0003), (0, 17, 0, 0.0003),
141
+ (0, 17, 1, 0.0003), (0, 3, 1, -0.0003)
142
+ ]),
143
+ ('pupil_x', [
144
+ (0, 11, 0, 0.0007 if params.get('pupil_x', 0) > 0 else 0.001),
145
+ (0, 15, 0, 0.001 if params.get('pupil_x', 0) > 0 else 0.0007)
146
+ ]),
147
+ ('pupil_y', [
148
+ (0, 11, 1, -0.001), (0, 15, 1, -0.001)
149
+ ]),
150
+ ('eyes', [
151
+ (0, 11, 1, -0.001), (0, 13, 1, 0.0003), (0, 15, 1, -0.001), (0, 16, 1, 0.0003),
152
+ (0, 1, 1, -0.00025), (0, 2, 1, 0.00025)
153
+ ]),
154
+ ('eyebrow', [
155
+ (0, 1, 1, 0.001 if params.get('eyebrow', 0) > 0 else 0.0003),
156
+ (0, 2, 1, -0.001 if params.get('eyebrow', 0) > 0 else -0.0003),
157
+ (0, 1, 0, -0.001 if params.get('eyebrow', 0) <= 0 else 0),
158
+ (0, 2, 0, 0.001 if params.get('eyebrow', 0) <= 0 else 0)
159
+ ]),
160
+ # Some other ones: https://github.com/jbilcke-hf/FacePoke/issues/22#issuecomment-2408708028
161
+ # Still need to check how exactly we would control those in the UI,
162
+ # as we don't have yet segmentation in the frontend UI for those body parts
163
+ #('lower_lip', [
164
+ # (0, 19, 1, 0.02)
165
+ #]),
166
+ #('upper_lip', [
167
+ # (0, 20, 1, -0.01)
168
+ #]),
169
+ #('neck', [(0, 5, 1, 0.01)]),
170
+ ]
171
+
172
+ for param_name, adjustments in modifications:
173
+ param_value = params.get(param_name, 0)
174
+ for i, j, k, factor in adjustments:
175
+ x_d_new[i, j, k] += param_value * factor
176
+
177
+ # Special case for pupil_y affecting eyes
178
+ x_d_new[0, 11, 1] -= params.get('pupil_y', 0) * 0.001
179
+ x_d_new[0, 15, 1] -= params.get('pupil_y', 0) * 0.001
180
+ params['eyes'] = params.get('eyes', 0) - params.get('pupil_y', 0) / 2.
181
+
182
+
183
+ # Apply rotation
184
+ R_new = get_rotation_matrix(
185
+ processed_data['x_s_info']['pitch'] + params.get('rotate_pitch', 0),
186
+ processed_data['x_s_info']['yaw'] + params.get('rotate_yaw', 0),
187
+ processed_data['x_s_info']['roll'] + params.get('rotate_roll', 0)
188
+ )
189
+ x_d_new = processed_data['x_s_info']['scale'] * (x_d_new @ R_new) + processed_data['x_s_info']['t']
190
+
191
+ # Apply stitching
192
+ x_d_new = await asyncio.to_thread(self.live_portrait.live_portrait_wrapper.stitching, processed_data['x_s'], x_d_new)
193
+
194
+ # Generate the output
195
+ out = await asyncio.to_thread(self.live_portrait.live_portrait_wrapper.warp_decode, processed_data['f_s'], processed_data['x_s'], x_d_new)
196
+ I_p = await asyncio.to_thread(self.live_portrait.live_portrait_wrapper.parse_output, out['out'])
197
+
198
+ buffered = io.BytesIO()
199
+
200
+ ####################################################
201
+ # this part is about stitching the image back into the original.
202
+ #
203
+ # this is an expensive operation, not just because of the compute
204
+ # but because the payload will also be bigger (we send back the whole pic)
205
+ #
206
+ # I'm currently running some experiments to do it in the frontend
207
+ #
208
+ # --- old way: we do it in the server-side: ---
209
+ mask_ori = await asyncio.to_thread(prepare_paste_back,
210
+ processed_data['inference_cfg'].mask_crop, processed_data['crop_info']['M_c2o'],
211
+ dsize=(processed_data['img_rgb'].shape[1], processed_data['img_rgb'].shape[0])
212
+ )
213
+ I_p_to_ori_blend = await asyncio.to_thread(paste_back,
214
+ I_p[0], processed_data['crop_info']['M_c2o'], processed_data['img_rgb'], mask_ori
215
+ )
216
+ result_image = Image.fromarray(I_p_to_ori_blend)
217
+
218
+ # --- maybe future way: do it in the frontend: ---
219
+ #result_image = Image.fromarray(I_p[0])
220
+ ####################################################
221
+
222
+ # write it into a webp
223
+ result_image.save(buffered, format="WebP", quality=82, lossless=False, method=6)
224
+
225
+ return buffered.getvalue()
226
+
227
+ except Exception as e:
228
+ raise ValueError(f"Failed to modify image: {str(e)}")
loader.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ import torch
4
+ import asyncio
5
+ import aiohttp
6
+ import requests
7
+ from huggingface_hub import hf_hub_download
8
+
9
+ # Configure logging
10
+ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Configuration
14
+ DATA_ROOT = os.environ.get('DATA_ROOT', '/tmp/data')
15
+ MODELS_DIR = os.path.join(DATA_ROOT, "models")
16
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
17
+
18
+ # Hugging Face repository information
19
+ HF_REPO_ID = "jbilcke-hf/model-cocktail"
20
+
21
+ # Model files to download
22
+ MODEL_FILES = [
23
+ "dwpose/dw-ll_ucoco_384.pth",
24
+ "face-detector/s3fd-619a316812.pth",
25
+
26
+ "liveportrait/spade_generator.pth",
27
+ "liveportrait/warping_module.pth",
28
+ "liveportrait/motion_extractor.pth",
29
+ "liveportrait/stitching_retargeting_module.pth",
30
+ "liveportrait/appearance_feature_extractor.pth",
31
+ "liveportrait/landmark.onnx",
32
+
33
+ # For animal mode 🐢🐱
34
+ # however they say animal mode doesn't support stitching yet?
35
+ # https://github.com/KwaiVGI/LivePortrait/blob/main/assets/docs/changelog/2024-08-02.md#updates-on-animals-mode
36
+ #"liveportrait-animals/warping_module.pth",
37
+ #"liveportrait-animals/spade_generator.pth",
38
+ #"liveportrait-animals/motion_extractor.pth",
39
+ #"liveportrait-animals/appearance_feature_extractor.pth",
40
+ #"liveportrait-animals/stitching_retargeting_module.pth",
41
+ #"liveportrait-animals/xpose.pth",
42
+
43
+ # this is a hack, instead we should probably try to
44
+ # fix liveportrait/utils/dependencies/insightface/utils/storage.py
45
+ "insightface/models/buffalo_l.zip",
46
+
47
+ "insightface/buffalo_l/det_10g.onnx",
48
+ "insightface/buffalo_l/2d106det.onnx",
49
+ "sd-vae-ft-mse/diffusion_pytorch_model.bin",
50
+ "sd-vae-ft-mse/diffusion_pytorch_model.safetensors",
51
+ "sd-vae-ft-mse/config.json",
52
+
53
+ # we don't use those yet
54
+ #"flux-dev/flux-dev-fp8.safetensors",
55
+ #"flux-dev/flux_dev_quantization_map.json",
56
+ #"pulid-flux/pulid_flux_v0.9.0.safetensors",
57
+ #"pulid-flux/pulid_v1.bin"
58
+ ]
59
+
60
+ def create_directory(directory):
61
+ """Create a directory if it doesn't exist and log its status."""
62
+ if not os.path.exists(directory):
63
+ os.makedirs(directory)
64
+ logger.info(f" Directory created: {directory}")
65
+ else:
66
+ logger.info(f" Directory already exists: {directory}")
67
+
68
+ def print_directory_structure(startpath):
69
+ """Print the directory structure starting from the given path."""
70
+ for root, dirs, files in os.walk(startpath):
71
+ level = root.replace(startpath, '').count(os.sep)
72
+ indent = ' ' * 4 * level
73
+ logger.info(f"{indent}{os.path.basename(root)}/")
74
+ subindent = ' ' * 4 * (level + 1)
75
+ for f in files:
76
+ logger.info(f"{subindent}{f}")
77
+
78
+ async def download_hf_file(filename: str) -> None:
79
+ """Download a file from Hugging Face to the models directory."""
80
+ dest = os.path.join(MODELS_DIR, filename)
81
+ os.makedirs(os.path.dirname(dest), exist_ok=True)
82
+ if os.path.exists(dest):
83
+ # this is really for debugging purposes only
84
+ logger.debug(f" βœ… {filename}")
85
+ return
86
+
87
+ logger.info(f" ⏳ Downloading {HF_REPO_ID}/{filename}")
88
+
89
+ try:
90
+ await asyncio.get_event_loop().run_in_executor(
91
+ None,
92
+ lambda: hf_hub_download(
93
+ repo_id=HF_REPO_ID,
94
+ filename=filename,
95
+ local_dir=MODELS_DIR
96
+ )
97
+ )
98
+ logger.info(f" βœ… Downloaded {filename}")
99
+ except Exception as e:
100
+ logger.error(f"🚨 Error downloading file from Hugging Face: {e}")
101
+ if os.path.exists(dest):
102
+ os.remove(dest)
103
+ raise
104
+
105
+ async def download_all_models():
106
+ """Download all required models from the Hugging Face repository."""
107
+ logger.info(" πŸ”Ž Looking for models...")
108
+ tasks = [download_hf_file(filename) for filename in MODEL_FILES]
109
+ await asyncio.gather(*tasks)
110
+ logger.info(" βœ… All models are available")
111
+
112
+ # are you looking to debug the app and verify that models are downloaded properly?
113
+ # then un-comment the two following lines:
114
+ #logger.info("πŸ’‘ Printing directory structure of models:")
115
+ #print_directory_structure(MODELS_DIR)
116
+
117
+ class ModelLoader:
118
+ """A class responsible for loading and initializing all required models."""
119
+
120
+ def __init__(self):
121
+ self.device = DEVICE
122
+ self.models_dir = MODELS_DIR
123
+
124
+ async def load_live_portrait(self):
125
+ """Load LivePortrait models."""
126
+ from liveportrait.config.inference_config import InferenceConfig
127
+ from liveportrait.config.crop_config import CropConfig
128
+ from liveportrait.live_portrait_pipeline import LivePortraitPipeline
129
+
130
+ logger.info(" ⏳ Loading LivePortrait models...")
131
+ live_portrait_pipeline = await asyncio.to_thread(
132
+ LivePortraitPipeline,
133
+ inference_cfg=InferenceConfig(
134
+ # default values
135
+ flag_stitching=True, # we recommend setting it to True!
136
+ flag_relative=True, # whether to use relative motion
137
+ flag_pasteback=True, # whether to paste-back/stitch the animated face cropping from the face-cropping space to the original image space
138
+ flag_do_crop= True, # whether to crop the source portrait to the face-cropping space
139
+ flag_do_rot=True, # whether to conduct the rotation when flag_do_crop is True
140
+ ),
141
+ crop_cfg=CropConfig()
142
+ )
143
+ logger.info(" βœ… LivePortrait models loaded successfully.")
144
+ return live_portrait_pipeline
145
+
146
+ async def initialize_models():
147
+ """Initialize and load all required models."""
148
+ logger.info("πŸš€ Starting model initialization...")
149
+
150
+ # Ensure all required models are downloaded
151
+ await download_all_models()
152
+
153
+ # Initialize the ModelLoader
154
+ loader = ModelLoader()
155
+
156
+ # Load LivePortrait models
157
+ live_portrait = await loader.load_live_portrait()
158
+
159
+ logger.info("βœ… Model initialization completed.")
160
+ return live_portrait
161
+
162
+ # Initial setup
163
+ logger.info("πŸš€ Setting up storage directories...")
164
+ create_directory(MODELS_DIR)
165
+ logger.info("βœ… Storage directories setup completed.")
requirements.txt ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # --------------------------------------------------------------------
3
+ # Cuda 12.4
4
+ # --------------------------------------------------------------------
5
+
6
+ --extra-index-url https://download.pytorch.org/whl/cu124
7
+ torch
8
+ torchvision
9
+ torchaudio
10
+ torchgeometry
11
+
12
+ # --------------------------------------------------------------------
13
+ # Common libraries for LivePortrait and all
14
+ # --------------------------------------------------------------------
15
+
16
+ # LRU cache compatible with asyncio
17
+ async-lru==2.0.4
18
+
19
+ # note: gradio is only used for the cropping utility
20
+ gradio==5.6.0
21
+
22
+ pyyaml==6.0.1
23
+ numpy==1.22.0 # Updated to resolve conflicts
24
+ opencv-python==4.8.1.78 # Downgraded to be compatible with numpy
25
+ scipy==1.10.1
26
+ imageio==2.31.1
27
+ imageio-ffmpeg==0.5.1
28
+ lmdb==1.4.1
29
+ tqdm==4.66.4
30
+ rich==13.7.1
31
+
32
+ # this one is from 5 years ago, we should probably
33
+ # use python-ffmpeg or typed-ffmpeg instead
34
+ # https://pypi.org/search/?q=ffmpeg-python
35
+ ffmpeg-python==0.2.0
36
+
37
+ # versions of onnx gpu <= 1.17 did not support Cuda 12
38
+ onnxruntime-gpu==1.19.2
39
+
40
+ # onnx 1.16.2 has some issues it seems:
41
+ # https://github.com/onnx/onnx/issues/6267
42
+ #onnx==1.16.2
43
+ # update: I've rolled it back to 1.16.1
44
+ # see: https://github.com/jbilcke-hf/FacePoke/issues/23#issuecomment-2414714490
45
+ onnx==1.16.1
46
+
47
+ scikit-image==0.20.0
48
+ albumentations==1.3.1
49
+ matplotlib==3.7.2
50
+ tyro==0.8.5
51
+ chumpy==0.70
52
+
53
+ diffusers==0.30.3
54
+ accelerate==0.34.2
55
+ tensorflow==2.12.0
56
+ tensorboard==2.12.0
57
+ transformers==4.39.2
58
+
59
+ gdown==5.2.0
60
+ requests==2.32.3
61
+ omegaconf==2.3.0
62
+
63
+ pydantic==2.9.2
64
+
65
+ # --------------------------------------------------------------------
66
+ # RESERVED FOR FUTURE USAGE
67
+ #
68
+ # (it adds bloat, so you can remove them if you want)
69
+ # --------------------------------------------------------------------
70
+ aiohttp==3.10.5
71
+ av==12.3.0
72
+ einops==0.7.0
73
+ safetensors==0.4.5
74
+ huggingface-hub==0.25.1
75
+ optimum-quanto==0.2.4
76
+
77
+ # --------------------------------------------------------------------
78
+ # Used for advanced LivePortrait features
79
+ # --------------------------------------------------------------------
80
+ pillow==10.4.0
81
+
82
+ # by popular demand, let's add AVIF
83
+ pillow-avif-plugin==1.4.6