Upload 11 files
Browse files- .dockerignore +17 -0
- .gitattributes +12 -34
- .gitignore +33 -0
- Dockerfile +60 -0
- LICENSE +56 -0
- README.md +161 -0
- app.py +145 -0
- build.sh +3 -0
- engine.py +228 -0
- loader.py +165 -0
- 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 |
-
*.
|
| 2 |
-
*.
|
| 3 |
-
*.
|
| 4 |
-
*.
|
| 5 |
-
*.
|
| 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 |
-
*.
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|