mirror of
https://github.com/hacksider/Deep-Live-Cam.git
synced 2026-09-04 23:06:38 +02:00
Compare commits
13
Commits
2.7-ultimate
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc48ba4308 | ||
|
|
b53844eb57 | ||
|
|
20cafb0079 | ||
|
|
7ca6d0b202 | ||
|
|
f7db37679a | ||
|
|
bd500d54b4 | ||
|
|
987f6b392b | ||
|
|
97a44800a2 | ||
|
|
345fa4a0b0 | ||
|
|
bdeeb3ace0 | ||
|
|
47dffeb307 | ||
|
|
9e1f0cc3a5 | ||
|
|
0b61ad5c0d |
@@ -30,7 +30,7 @@ By using this software, you agree to these terms and commit to using it in a man
|
||||
|
||||
Users are expected to use this software responsibly and legally. If using a real person's face, obtain their consent and clearly label any output as a deepfake when sharing online. We are not responsible for end-user actions.
|
||||
|
||||
## Pre-built Quickstart
|
||||
## Pre-built Deep-Live-Cam 2.7 Ultimate!
|
||||
|
||||
<p align="center">
|
||||
<a href="https://deeplivecam.net/index.php/quickstart">
|
||||
@@ -39,11 +39,17 @@ Users are expected to use this software responsibly and legally. If using a real
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://deeplivecam.net/index.php/plans/nvidia-gpu?plan_id=0&group_id=1">
|
||||
<img src="https://github.com/user-attachments/assets/56b61811-3a1e-4672-9b50-cf7f6e8e6852" width="40" alt="Windows">
|
||||
</a>
|
||||
|
||||
<a href="https://deeplivecam.net/index.php/plans/nvidia-gpu?plan_id=0&group_id=2">
|
||||
<img src="https://github.com/user-attachments/assets/6538e3a6-c957-431a-b586-2d6abcf534dc" width="34" alt="Mac Silicon">
|
||||
</a>
|
||||
|
||||
<a href="https://deeplivecam.net/index.php/plans/nvidia-gpu?plan_id=0&group_id=3">
|
||||
<img src="https://github.com/user-attachments/assets/ad45142e-426c-4364-a2a9-a512670cc62c" width="40" alt="CPU">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -60,7 +66,7 @@ Users are expected to use this software responsibly and legally. If using a real
|
||||
</a>
|
||||
</p>
|
||||
|
||||
> **Ultimate** includes **30+ exclusive features**, performance optimizations, and **priority support**.
|
||||
> **Ultimate** includes **30+ exclusive features**, performance optimizations, and **priority support** We only have a single official website which is https://deeplivecam.net . Please be careful on where you download other versions of this application aside from that website and this github repo.
|
||||
|
||||
Perfect if you want the fastest setup with **zero manual installation**, pre-configured dependencies, and optimized builds for every supported platform.
|
||||
|
||||
@@ -150,7 +156,7 @@ cd Deep-Live-Cam
|
||||
|
||||
**3. Download the Models**
|
||||
|
||||
1. [GFPGANv1.4](https://huggingface.co/hacksider/deep-live-cam/resolve/main/GFPGANv1.4.onnx)
|
||||
1. [gfpgan-1024.onnx](https://huggingface.co/hacksider/deep-live-cam/resolve/main/gfpgan-1024.onnx)
|
||||
2. [inswapper\_128\_fp16.onnx](https://huggingface.co/hacksider/deep-live-cam/resolve/main/inswapper_128_fp16.onnx)
|
||||
|
||||
Place these files in the "**models**" folder.
|
||||
@@ -227,7 +233,7 @@ pip install git+https://github.com/TencentARC/GFPGAN.git@master
|
||||
```bash
|
||||
pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128
|
||||
pip uninstall onnxruntime onnxruntime-gpu
|
||||
pip install onnxruntime-gpu==1.21.0
|
||||
pip install onnxruntime-gpu==1.26.0
|
||||
```
|
||||
|
||||
3. Usage:
|
||||
@@ -313,6 +319,22 @@ pip uninstall onnxruntime onnxruntime-openvino
|
||||
pip install onnxruntime-openvino==1.21.0
|
||||
```
|
||||
|
||||
**Note:** `onnxruntime-openvino` newer than 1.21.0 must be installed together with `openvino`, and the two versions must correspond one-to-one. The supported pairings are:
|
||||
|
||||
| onnxruntime-openvino | OpenVINO |
|
||||
| --- | --- |
|
||||
| 1.24.1 | 2025.4.1 |
|
||||
| 1.23.0 | 2025.3 |
|
||||
| 1.22.0 | 2025.1 |
|
||||
|
||||
```bash
|
||||
# Example: onnxruntime-openvino 1.24.1 pairs with OpenVINO 2025.4.1
|
||||
pip install openvino==2025.4.1
|
||||
pip install onnxruntime-openvino==1.24.1
|
||||
```
|
||||
|
||||
See the [OpenVINO Execution Provider requirements](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html#requirements) for the full version-mapping details.
|
||||
|
||||
2. Usage:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
just put the models in this folder -
|
||||
|
||||
https://huggingface.co/hacksider/deep-live-cam/resolve/main/inswapper_128_fp16.onnx?download=true
|
||||
https://github.com/TencentARC/GFPGAN/releases/download/v1.3.4/GFPGANv1.4.pth
|
||||
https://huggingface.co/hacksider/deep-live-cam/resolve/main/gfpgan-1024.onnx?download=true
|
||||
|
||||
+7
-4
@@ -178,6 +178,10 @@ def limit_resources() -> None:
|
||||
tensorflow.config.experimental.set_memory_growth(gpu, True)
|
||||
# limit memory usage
|
||||
if modules.globals.max_memory:
|
||||
# setrlimit(RLIMIT_DATA) fails with EINVAL on macOS, crashing on launch.
|
||||
# See https://github.com/hacksider/Deep-Live-Cam/issues/1848
|
||||
if platform.system().lower() == 'darwin':
|
||||
return
|
||||
memory = modules.globals.max_memory * 1024 ** 3
|
||||
if platform.system().lower() == 'windows':
|
||||
import ctypes
|
||||
@@ -276,10 +280,9 @@ def start() -> None:
|
||||
update_status('Falling back to disk-based processing...')
|
||||
|
||||
extraction_start = time.time()
|
||||
if not modules.globals.map_faces:
|
||||
create_temp(modules.globals.target_path)
|
||||
update_status('Extracting frames...')
|
||||
extract_frames(modules.globals.target_path)
|
||||
create_temp(modules.globals.target_path)
|
||||
update_status('Extracting frames...')
|
||||
extract_frames(modules.globals.target_path)
|
||||
extraction_time = time.time() - extraction_start
|
||||
|
||||
temp_frame_paths = get_temp_frame_paths(modules.globals.target_path)
|
||||
|
||||
@@ -29,6 +29,9 @@ def get_face_analyser() -> Any:
|
||||
from modules.processors.frame._onnx_enhancer import (
|
||||
build_provider_config,
|
||||
)
|
||||
from modules.model_downloader import ensure_insightface_pack
|
||||
|
||||
ensure_insightface_pack('buffalo_l')
|
||||
providers = build_provider_config()
|
||||
FACE_ANALYSER = insightface.app.FaceAnalysis(
|
||||
name='buffalo_l',
|
||||
|
||||
+7
-4
@@ -6,10 +6,13 @@ from typing import List, Dict, Any
|
||||
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
WORKFLOW_DIR = os.path.join(ROOT_DIR, "workflow")
|
||||
|
||||
file_types = [
|
||||
("Image", ("*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp")),
|
||||
("Video", ("*.mp4", "*.mkv")),
|
||||
]
|
||||
# Canonical media extensions, defined once so the file dialogs and
|
||||
# has_image_extension never drift. GIF is intentionally excluded: OpenCV's
|
||||
# cv2.imread/imwrite (the only image I/O this app uses) cannot decode or
|
||||
# encode GIF on 4.10 or 4.11, so offering it would silently fail. WEBP works
|
||||
# via the libwebp bundled with opencv-python.
|
||||
IMAGE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".bmp", ".webp")
|
||||
VIDEO_EXTENSIONS = (".mp4", ".mkv")
|
||||
|
||||
# Face Mapping Data
|
||||
source_target_map: List[Dict[str, Any]] = [] # Stores detailed map for image/video processing
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import os
|
||||
import platform
|
||||
import ssl
|
||||
import threading
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from modules.paths import MODELS_DIR
|
||||
|
||||
HF_REPO_ID = "hacksider/deep-live-cam"
|
||||
HF_RESOLVE_BASE = f"https://huggingface.co/{HF_REPO_ID}/resolve/main/"
|
||||
|
||||
MODEL_SIZES: Dict[str, int] = {
|
||||
"inswapper_128.onnx": 554253681,
|
||||
"inswapper_128_fp16.onnx": 277680638,
|
||||
"gfpgan-1024.onnx": 365875079,
|
||||
"GPEN-BFR-256.onnx": 75715262,
|
||||
"GPEN-BFR-512.onnx": 284244491,
|
||||
"buffalo_l/buffalo_l/1k3d68.onnx": 143607619,
|
||||
"buffalo_l/buffalo_l/2d106det.onnx": 5030888,
|
||||
"buffalo_l/buffalo_l/det_10g.onnx": 16923827,
|
||||
"buffalo_l/buffalo_l/genderage.onnx": 1322532,
|
||||
"buffalo_l/buffalo_l/w600k_r50.onnx": 174383860,
|
||||
}
|
||||
|
||||
_LOCKS: Dict[str, threading.Lock] = {}
|
||||
_LOCKS_GUARD = threading.Lock()
|
||||
|
||||
CHUNK_SIZE = 1024 * 256
|
||||
|
||||
|
||||
def _ssl_context():
|
||||
if platform.system().lower() == "darwin":
|
||||
return ssl._create_unverified_context()
|
||||
return None
|
||||
|
||||
|
||||
def _lock_for(key: str) -> threading.Lock:
|
||||
with _LOCKS_GUARD:
|
||||
if key not in _LOCKS:
|
||||
_LOCKS[key] = threading.Lock()
|
||||
return _LOCKS[key]
|
||||
|
||||
|
||||
def resolve_url(name: str) -> str:
|
||||
return HF_RESOLVE_BASE + name.replace(os.sep, "/")
|
||||
|
||||
|
||||
def local_path(name: str, dest_dir: Optional[str] = None) -> str:
|
||||
if dest_dir is not None:
|
||||
return os.path.join(dest_dir, os.path.basename(name))
|
||||
return os.path.join(MODELS_DIR, *name.replace("/", os.sep).split(os.sep))
|
||||
|
||||
|
||||
def expected_size(name: str) -> Optional[int]:
|
||||
return MODEL_SIZES.get(name.replace(os.sep, "/"))
|
||||
|
||||
|
||||
def is_present(name: str, dest_dir: Optional[str] = None) -> bool:
|
||||
path = local_path(name, dest_dir)
|
||||
return os.path.isfile(path) and os.path.getsize(path) > 0
|
||||
|
||||
|
||||
def _download(name: str, url: str, target: str, size: Optional[int]) -> bool:
|
||||
os.makedirs(os.path.dirname(target) or MODELS_DIR, exist_ok=True)
|
||||
partial = target + ".part"
|
||||
resume_from = os.path.getsize(partial) if os.path.isfile(partial) else 0
|
||||
|
||||
headers = {"User-Agent": "Deep-Live-Cam"}
|
||||
if resume_from:
|
||||
headers["Range"] = f"bytes={resume_from}-"
|
||||
|
||||
try:
|
||||
request = urllib.request.Request(url, headers=headers)
|
||||
response = urllib.request.urlopen(request, context=_ssl_context(), timeout=60)
|
||||
except urllib.error.HTTPError as error:
|
||||
if resume_from and error.code in (416, 501):
|
||||
try:
|
||||
os.remove(partial)
|
||||
except OSError:
|
||||
pass
|
||||
return _download(name, url, target, size)
|
||||
print(f"[DLC.MODELS] Failed to download {name}: HTTP {error.code}")
|
||||
return False
|
||||
except (urllib.error.URLError, OSError) as error:
|
||||
print(f"[DLC.MODELS] Failed to download {name}: {error}")
|
||||
return False
|
||||
|
||||
with response:
|
||||
if resume_from and getattr(response, "status", 200) != 206:
|
||||
resume_from = 0
|
||||
remaining = int(response.headers.get("Content-Length", 0) or 0)
|
||||
total = size or (resume_from + remaining) or None
|
||||
mode = "ab" if resume_from else "wb"
|
||||
try:
|
||||
with open(partial, mode) as handle:
|
||||
with tqdm(
|
||||
total=total,
|
||||
initial=resume_from,
|
||||
desc=f"Downloading {os.path.basename(name)}",
|
||||
unit="B",
|
||||
unit_scale=True,
|
||||
unit_divisor=1024,
|
||||
) as progress:
|
||||
while True:
|
||||
buffer = response.read(CHUNK_SIZE)
|
||||
if not buffer:
|
||||
break
|
||||
handle.write(buffer)
|
||||
progress.update(len(buffer))
|
||||
except (urllib.error.URLError, OSError) as error:
|
||||
print(f"[DLC.MODELS] Download of {name} interrupted: {error}")
|
||||
return False
|
||||
|
||||
downloaded = os.path.getsize(partial)
|
||||
if size is not None and downloaded != size:
|
||||
print(f"[DLC.MODELS] {name} is {downloaded} bytes, expected {size}. Discarding.")
|
||||
try:
|
||||
os.remove(partial)
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
|
||||
try:
|
||||
os.replace(partial, target)
|
||||
except OSError as error:
|
||||
print(f"[DLC.MODELS] Could not finalise {name}: {error}")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def ensure_model(
|
||||
name: str, quiet: bool = False, dest_dir: Optional[str] = None
|
||||
) -> Optional[str]:
|
||||
name = name.replace(os.sep, "/")
|
||||
target = local_path(name, dest_dir)
|
||||
|
||||
with _lock_for(target):
|
||||
if is_present(name, dest_dir):
|
||||
return target
|
||||
if not quiet:
|
||||
print(f"[DLC.MODELS] {name} not found in models folder, downloading...")
|
||||
if _download(name, resolve_url(name), target, expected_size(name)):
|
||||
return target
|
||||
return None
|
||||
|
||||
|
||||
def ensure_any(names: List[str]) -> Optional[str]:
|
||||
for name in names:
|
||||
if is_present(name):
|
||||
return local_path(name)
|
||||
for name in names:
|
||||
path = ensure_model(name)
|
||||
if path is not None:
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def ensure_insightface_pack(name: str = "buffalo_l") -> bool:
|
||||
members = [n for n in MODEL_SIZES if n.startswith(f"{name}/")]
|
||||
if not members:
|
||||
return False
|
||||
|
||||
dest_dir = os.path.join(os.path.expanduser("~"), ".insightface", "models", name)
|
||||
if all(is_present(member, dest_dir) for member in members):
|
||||
return True
|
||||
|
||||
print(f"[DLC.MODELS] insightface pack '{name}' is missing, downloading...")
|
||||
ok = True
|
||||
for member in members:
|
||||
if ensure_model(member, quiet=True, dest_dir=dest_dir) is None:
|
||||
ok = False
|
||||
if not ok:
|
||||
print(f"[DLC.MODELS] Could not pre-fill '{name}'; insightface will retry.")
|
||||
return ok
|
||||
@@ -23,6 +23,7 @@ FACE_ENHANCER = None
|
||||
THREAD_SEMAPHORE = threading.Semaphore()
|
||||
THREAD_LOCK = threading.Lock()
|
||||
NAME = "DLC.FACE-ENHANCER"
|
||||
MODEL_FILE = "gfpgan-1024.onnx"
|
||||
|
||||
abs_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
models_dir = os.path.join(
|
||||
@@ -44,11 +45,12 @@ FFHQ_TEMPLATE_512 = np.array(
|
||||
|
||||
|
||||
def pre_check() -> bool:
|
||||
model_path = os.path.join(models_dir, "gfpgan-1024.onnx")
|
||||
if not os.path.exists(model_path):
|
||||
from modules.model_downloader import ensure_model
|
||||
|
||||
if ensure_model(MODEL_FILE) is None:
|
||||
update_status(
|
||||
f"GFPGAN ONNX model not found at {model_path}. "
|
||||
"Please place gfpgan-1024.onnx in the models folder.",
|
||||
f"Could not obtain {MODEL_FILE}. Place it in the models folder "
|
||||
"manually or check your internet connection.",
|
||||
NAME,
|
||||
)
|
||||
return False
|
||||
@@ -73,11 +75,15 @@ def get_face_enhancer() -> onnxruntime.InferenceSession:
|
||||
|
||||
with THREAD_LOCK:
|
||||
if FACE_ENHANCER is None:
|
||||
model_path = os.path.join(models_dir, "gfpgan-1024.onnx")
|
||||
from modules.model_downloader import ensure_model
|
||||
|
||||
if not os.path.exists(model_path):
|
||||
model_path = ensure_model(MODEL_FILE)
|
||||
|
||||
if model_path is None:
|
||||
raise FileNotFoundError(
|
||||
f"{NAME}: Model not found at {model_path}"
|
||||
f"{NAME}: Model not found at "
|
||||
f"{os.path.join(models_dir, MODEL_FILE)} and could not be "
|
||||
"downloaded"
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -22,7 +22,7 @@ from modules.processors.frame._onnx_enhancer import (
|
||||
|
||||
NAME = "DLC.FACE-ENHANCER-GPEN256"
|
||||
INPUT_SIZE = 256
|
||||
MODEL_URL = "https://github.com/harisreedhar/Face-Upscalers-ONNX/releases/download/GPEN-BFR/GPEN-BFR-256.onnx"
|
||||
MODEL_MIRROR_URL = "https://github.com/harisreedhar/Face-Upscalers-ONNX/releases/download/GPEN-BFR/GPEN-BFR-256.onnx"
|
||||
MODEL_FILE = "GPEN-BFR-256.onnx"
|
||||
|
||||
ENHANCER = None
|
||||
@@ -34,12 +34,33 @@ models_dir = os.path.join(
|
||||
)
|
||||
|
||||
|
||||
def _obtain_model():
|
||||
from modules.model_downloader import ensure_model
|
||||
|
||||
model_path = ensure_model(MODEL_FILE)
|
||||
if model_path is not None:
|
||||
return model_path
|
||||
|
||||
update_status(f"Retrying {MODEL_FILE} from the mirror...", NAME)
|
||||
from modules.utilities import conditional_download
|
||||
|
||||
try:
|
||||
conditional_download(models_dir, [MODEL_MIRROR_URL])
|
||||
except Exception as error:
|
||||
update_status(f"Mirror download failed: {error}", NAME)
|
||||
return None
|
||||
fallback = os.path.join(models_dir, MODEL_FILE)
|
||||
return fallback if os.path.exists(fallback) else None
|
||||
|
||||
|
||||
def pre_check() -> bool:
|
||||
model_path = os.path.join(models_dir, MODEL_FILE)
|
||||
if not os.path.exists(model_path):
|
||||
update_status(f"Downloading {MODEL_FILE}...", NAME)
|
||||
from modules.utilities import conditional_download
|
||||
conditional_download(models_dir, [MODEL_URL])
|
||||
if _obtain_model() is None:
|
||||
update_status(
|
||||
f"Could not obtain {MODEL_FILE}. Place it in the models folder "
|
||||
"manually or check your internet connection.",
|
||||
NAME,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@@ -54,12 +75,11 @@ def get_enhancer() -> Any:
|
||||
global ENHANCER
|
||||
with THREAD_LOCK:
|
||||
if ENHANCER is None:
|
||||
model_path = os.path.join(models_dir, MODEL_FILE)
|
||||
if not os.path.exists(model_path):
|
||||
from modules.utilities import conditional_download
|
||||
conditional_download(models_dir, [MODEL_URL])
|
||||
if not os.path.exists(model_path):
|
||||
raise FileNotFoundError(f"Model file not found: {model_path}")
|
||||
model_path = _obtain_model()
|
||||
if model_path is None:
|
||||
raise FileNotFoundError(
|
||||
f"Model file not found: {os.path.join(models_dir, MODEL_FILE)}"
|
||||
)
|
||||
print(f"{NAME}: Loading ONNX model from {model_path}")
|
||||
ENHANCER = create_onnx_session(model_path)
|
||||
warmup_session(ENHANCER)
|
||||
|
||||
@@ -22,7 +22,7 @@ from modules.processors.frame._onnx_enhancer import (
|
||||
|
||||
NAME = "DLC.FACE-ENHANCER-GPEN512"
|
||||
INPUT_SIZE = 512
|
||||
MODEL_URL = "https://github.com/harisreedhar/Face-Upscalers-ONNX/releases/download/GPEN-BFR/GPEN-BFR-512.onnx"
|
||||
MODEL_MIRROR_URL = "https://github.com/harisreedhar/Face-Upscalers-ONNX/releases/download/GPEN-BFR/GPEN-BFR-512.onnx"
|
||||
MODEL_FILE = "GPEN-BFR-512.onnx"
|
||||
|
||||
ENHANCER = None
|
||||
@@ -34,12 +34,33 @@ models_dir = os.path.join(
|
||||
)
|
||||
|
||||
|
||||
def _obtain_model():
|
||||
from modules.model_downloader import ensure_model
|
||||
|
||||
model_path = ensure_model(MODEL_FILE)
|
||||
if model_path is not None:
|
||||
return model_path
|
||||
|
||||
update_status(f"Retrying {MODEL_FILE} from the mirror...", NAME)
|
||||
from modules.utilities import conditional_download
|
||||
|
||||
try:
|
||||
conditional_download(models_dir, [MODEL_MIRROR_URL])
|
||||
except Exception as error:
|
||||
update_status(f"Mirror download failed: {error}", NAME)
|
||||
return None
|
||||
fallback = os.path.join(models_dir, MODEL_FILE)
|
||||
return fallback if os.path.exists(fallback) else None
|
||||
|
||||
|
||||
def pre_check() -> bool:
|
||||
model_path = os.path.join(models_dir, MODEL_FILE)
|
||||
if not os.path.exists(model_path):
|
||||
update_status(f"Downloading {MODEL_FILE}...", NAME)
|
||||
from modules.utilities import conditional_download
|
||||
conditional_download(models_dir, [MODEL_URL])
|
||||
if _obtain_model() is None:
|
||||
update_status(
|
||||
f"Could not obtain {MODEL_FILE}. Place it in the models folder "
|
||||
"manually or check your internet connection.",
|
||||
NAME,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@@ -54,12 +75,11 @@ def get_enhancer() -> Any:
|
||||
global ENHANCER
|
||||
with THREAD_LOCK:
|
||||
if ENHANCER is None:
|
||||
model_path = os.path.join(models_dir, MODEL_FILE)
|
||||
if not os.path.exists(model_path):
|
||||
from modules.utilities import conditional_download
|
||||
conditional_download(models_dir, [MODEL_URL])
|
||||
if not os.path.exists(model_path):
|
||||
raise FileNotFoundError(f"Model file not found: {model_path}")
|
||||
model_path = _obtain_model()
|
||||
if model_path is None:
|
||||
raise FileNotFoundError(
|
||||
f"Model file not found: {os.path.join(models_dir, MODEL_FILE)}"
|
||||
)
|
||||
print(f"{NAME}: Loading ONNX model from {model_path}")
|
||||
ENHANCER = create_onnx_session(model_path)
|
||||
warmup_session(ENHANCER)
|
||||
|
||||
@@ -12,7 +12,6 @@ from modules.core import update_status
|
||||
from modules.face_analyser import get_one_face, get_many_faces, default_source_face
|
||||
from modules.typing import Face, Frame
|
||||
from modules.utilities import (
|
||||
conditional_download,
|
||||
is_image,
|
||||
is_video,
|
||||
)
|
||||
@@ -192,21 +191,26 @@ models_dir = os.path.join(
|
||||
def pre_check() -> bool:
|
||||
# Use models_dir instead of abs_dir to save to the correct location
|
||||
download_directory_path = models_dir
|
||||
|
||||
|
||||
# Make sure the models directory exists, catch permission errors if they occur
|
||||
try:
|
||||
os.makedirs(download_directory_path, exist_ok=True)
|
||||
except OSError as e:
|
||||
logging.error(f"Failed to create directory {download_directory_path} due to permission error: {e}")
|
||||
return False
|
||||
|
||||
# Use the direct download URL from Hugging Face (FP32 model for broad GPU compatibility)
|
||||
conditional_download(
|
||||
download_directory_path,
|
||||
[
|
||||
"https://huggingface.co/hacksider/deep-live-cam/resolve/main/inswapper_128.onnx"
|
||||
],
|
||||
)
|
||||
|
||||
from modules.model_downloader import ensure_any
|
||||
|
||||
variants = ["inswapper_128.onnx", "inswapper_128_fp16.onnx"]
|
||||
if _HAS_TORCH_CUDA:
|
||||
variants.reverse()
|
||||
if ensure_any(variants) is None:
|
||||
update_status(
|
||||
"Could not obtain the inswapper model. Place inswapper_128.onnx in "
|
||||
"the models folder manually or check your internet connection.",
|
||||
NAME,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@@ -242,8 +246,12 @@ def get_face_swapper() -> Any:
|
||||
elif os.path.exists(fp32_path):
|
||||
model_path = fp32_path
|
||||
else:
|
||||
update_status(f"No inswapper model found in {models_dir}.", NAME)
|
||||
return None
|
||||
if not pre_check():
|
||||
return None
|
||||
model_path = fp16_path if os.path.exists(fp16_path) else fp32_path
|
||||
if not os.path.exists(model_path):
|
||||
update_status(f"No inswapper model found in {models_dir}.", NAME)
|
||||
return None
|
||||
# On Apple Silicon, rewrite Pad(reflect) → Slice+Concat so
|
||||
# CoreML can run the entire model in a single partition on
|
||||
# the Neural Engine instead of bouncing between CPU and ANE.
|
||||
|
||||
+19
-7
@@ -237,6 +237,18 @@ _RECENT_SOURCE_DIR: Optional[str] = None
|
||||
_RECENT_TARGET_DIR: Optional[str] = None
|
||||
_RECENT_OUTPUT_DIR: Optional[str] = None
|
||||
|
||||
# QFileDialog filter strings, built from the canonical extension sets in
|
||||
# globals so every dialog stays in sync (no hand-copied lists to drift).
|
||||
_IMAGE_FILE_FILTER = "Images (" + " ".join(
|
||||
f"*{ext}" for ext in modules.globals.IMAGE_EXTENSIONS
|
||||
) + ")"
|
||||
_MEDIA_FILE_FILTER = "Media (" + " ".join(
|
||||
f"*{ext}" for ext in (*modules.globals.IMAGE_EXTENSIONS, *modules.globals.VIDEO_EXTENSIONS)
|
||||
) + ")"
|
||||
_VIDEO_FILE_FILTER = "Videos (" + " ".join(
|
||||
f"*{ext}" for ext in modules.globals.VIDEO_EXTENSIONS
|
||||
) + ")"
|
||||
|
||||
|
||||
# ─── image utilities ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -417,7 +429,7 @@ def get_available_cameras() -> Tuple[List[int], List[str]]:
|
||||
indices: List[int] = []
|
||||
names: List[str] = []
|
||||
for i in range(10):
|
||||
cap = cv2.VideoCapture(i)
|
||||
cap = cv2.VideoCapture(f"/dev/video{i}")
|
||||
if cap.isOpened():
|
||||
indices.append(i)
|
||||
names.append(f"Camera {i}")
|
||||
@@ -734,7 +746,7 @@ class MainWindow(QMainWindow):
|
||||
path, _filter = QFileDialog.getOpenFileName(
|
||||
self, _("select an source image"),
|
||||
_RECENT_SOURCE_DIR or "",
|
||||
"Images (*.png *.jpg *.jpeg *.gif *.bmp)",
|
||||
_IMAGE_FILE_FILTER,
|
||||
)
|
||||
if path and is_image(path):
|
||||
modules.globals.source_path = path
|
||||
@@ -755,7 +767,7 @@ class MainWindow(QMainWindow):
|
||||
path, _filter = QFileDialog.getOpenFileName(
|
||||
self, _("select an target image or video"),
|
||||
_RECENT_TARGET_DIR or "",
|
||||
"Media (*.png *.jpg *.jpeg *.gif *.bmp *.mp4 *.mkv)",
|
||||
_MEDIA_FILE_FILTER,
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
@@ -886,13 +898,13 @@ class MainWindow(QMainWindow):
|
||||
path, _f = QFileDialog.getSaveFileName(
|
||||
self, _("save image output file"),
|
||||
os.path.join(_RECENT_OUTPUT_DIR or "", "output.png"),
|
||||
"Images (*.png *.jpg *.jpeg *.bmp)",
|
||||
_IMAGE_FILE_FILTER,
|
||||
)
|
||||
elif is_video(modules.globals.target_path):
|
||||
path, _f = QFileDialog.getSaveFileName(
|
||||
self, _("save video output file"),
|
||||
os.path.join(_RECENT_OUTPUT_DIR or "", "output.mp4"),
|
||||
"Videos (*.mp4 *.mkv)",
|
||||
_VIDEO_FILE_FILTER,
|
||||
)
|
||||
else:
|
||||
return
|
||||
@@ -1334,7 +1346,7 @@ class MapperDialog(QDialog):
|
||||
path, _f = QFileDialog.getOpenFileName(
|
||||
self, _("select an source image"),
|
||||
_RECENT_SOURCE_DIR or "",
|
||||
"Images (*.png *.jpg *.jpeg *.gif *.bmp)",
|
||||
_IMAGE_FILE_FILTER,
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
@@ -1439,7 +1451,7 @@ class LiveMapperDialog(QDialog):
|
||||
path, _f = QFileDialog.getOpenFileName(
|
||||
self, _("select an source image"),
|
||||
_RECENT_SOURCE_DIR or "",
|
||||
"Images (*.png *.jpg *.jpeg *.gif *.bmp)",
|
||||
_IMAGE_FILE_FILTER,
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
|
||||
@@ -262,11 +262,16 @@ def clean_temp(target_path: str) -> None:
|
||||
|
||||
|
||||
def has_image_extension(image_path: str) -> bool:
|
||||
return image_path.lower().endswith(("png", "jpg", "jpeg"))
|
||||
# splitext so only the real extension counts (e.g. "photo.png.bak" is not
|
||||
# an image); the set is centralized in globals to stay in sync with dialogs.
|
||||
return os.path.splitext(image_path)[1].lower() in modules.globals.IMAGE_EXTENSIONS
|
||||
|
||||
|
||||
def is_image(image_path: str) -> bool:
|
||||
if image_path and os.path.isfile(image_path):
|
||||
# Extension check first — Windows mimetypes doesn't always register webp
|
||||
if has_image_extension(image_path):
|
||||
return True
|
||||
mimetype, _ = mimetypes.guess_type(image_path)
|
||||
return bool(mimetype and mimetype.startswith("image/"))
|
||||
return False
|
||||
|
||||
@@ -71,8 +71,9 @@ class VideoCapturer:
|
||||
self.cap.release()
|
||||
except Exception:
|
||||
continue
|
||||
elif platform.system() == "Linux":
|
||||
self.cap = cv2.VideoCapture(f"/dev/video{self.device_index}")
|
||||
else:
|
||||
# Unix-like systems (Linux/Mac) capture method
|
||||
self.cap = cv2.VideoCapture(self.device_index)
|
||||
|
||||
if not self.cap or not self.cap.isOpened():
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import importlib
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _patched_core_import_stubs(calls, pipe_result=False):
|
||||
class Processor:
|
||||
NAME = "test_processor"
|
||||
|
||||
def pre_start(self):
|
||||
return True
|
||||
|
||||
def pre_check(self):
|
||||
return True
|
||||
|
||||
def process_image(self, *_args, **_kwargs):
|
||||
raise AssertionError("image path should not be used")
|
||||
|
||||
def process_video(self, source_path, frame_paths):
|
||||
calls.append(("process_video", source_path, tuple(frame_paths)))
|
||||
|
||||
stubs = {
|
||||
"cv2": types.SimpleNamespace(
|
||||
IMREAD_COLOR=1,
|
||||
imdecode=lambda *_args, **_kwargs: None,
|
||||
imencode=lambda *_args, **_kwargs: (
|
||||
True,
|
||||
types.SimpleNamespace(tofile=lambda *_a, **_k: None),
|
||||
),
|
||||
),
|
||||
"numpy": types.SimpleNamespace(uint8=object, fromfile=lambda *_args, **_kwargs: b""),
|
||||
"torch": types.SimpleNamespace(
|
||||
cuda=types.SimpleNamespace(empty_cache=lambda: None)
|
||||
),
|
||||
"onnxruntime": types.SimpleNamespace(
|
||||
get_available_providers=lambda: ["CPUExecutionProvider"]
|
||||
),
|
||||
"tensorflow": types.SimpleNamespace(),
|
||||
"modules.metadata": types.SimpleNamespace(name="Deep-Live-Cam", version="test"),
|
||||
"modules.ui": types.SimpleNamespace(
|
||||
check_and_ignore_nsfw=lambda *_args, **_kwargs: False,
|
||||
update_status=lambda *_args, **_kwargs: None,
|
||||
init=lambda *_args, **_kwargs: types.SimpleNamespace(mainloop=lambda: None),
|
||||
),
|
||||
"modules.processors.frame.core": types.SimpleNamespace(
|
||||
get_frame_processors_modules=lambda _names: [Processor()],
|
||||
process_video_in_memory=lambda *_args, **_kwargs: calls.append(("pipe",))
|
||||
or pipe_result,
|
||||
),
|
||||
"modules.utilities": types.SimpleNamespace(
|
||||
has_image_extension=lambda _path: False,
|
||||
is_image=lambda _path: False,
|
||||
is_video=lambda _path: True,
|
||||
detect_fps=lambda _path: 24.0,
|
||||
create_video=lambda target_path, fps: calls.append(
|
||||
("create_video", target_path, fps)
|
||||
)
|
||||
or True,
|
||||
extract_frames=lambda target_path: calls.append(
|
||||
("extract_frames", target_path)
|
||||
),
|
||||
get_temp_frame_paths=lambda target_path: [f"{target_path}/0001.png"],
|
||||
restore_audio=lambda *_args, **_kwargs: calls.append(("restore_audio",)),
|
||||
create_temp=lambda target_path: calls.append(("create_temp", target_path)),
|
||||
move_temp=lambda target_path, output_path: calls.append(
|
||||
("move_temp", target_path, output_path)
|
||||
),
|
||||
clean_temp=lambda target_path: calls.append(("clean_temp", target_path)),
|
||||
normalize_output_path=lambda _source, _target, output: output,
|
||||
),
|
||||
}
|
||||
with patch.dict(sys.modules, stubs, clear=False):
|
||||
sys.modules.pop("modules.core", None)
|
||||
yield importlib.import_module("modules.core")
|
||||
sys.modules.pop("modules.core", None)
|
||||
|
||||
|
||||
def _configure_video_run(core, *, map_faces):
|
||||
core.modules.globals.source_path = "source.jpg"
|
||||
core.modules.globals.target_path = "target.mp4"
|
||||
core.modules.globals.output_path = "output.mp4"
|
||||
core.modules.globals.frame_processors = ["face_swapper"]
|
||||
core.modules.globals.headless = True
|
||||
core.modules.globals.keep_fps = False
|
||||
core.modules.globals.keep_audio = False
|
||||
core.modules.globals.keep_frames = False
|
||||
core.modules.globals.map_faces = map_faces
|
||||
core.modules.globals.nsfw_filter = False
|
||||
core.modules.globals.execution_threads = 1
|
||||
core.modules.globals.execution_providers = ["CPUExecutionProvider"]
|
||||
core.modules.globals.max_memory = None
|
||||
|
||||
|
||||
class MapFacesFallbackTests(unittest.TestCase):
|
||||
def test_map_faces_disk_fallback_extracts_frames_before_processing(self):
|
||||
calls = []
|
||||
with _patched_core_import_stubs(calls, pipe_result=False) as core:
|
||||
_configure_video_run(core, map_faces=True)
|
||||
|
||||
with patch.object(core.os.path, "isfile", return_value=True):
|
||||
core.start()
|
||||
|
||||
self.assertNotIn(("pipe",), calls)
|
||||
self.assertIn(("create_temp", "target.mp4"), calls)
|
||||
self.assertIn(("extract_frames", "target.mp4"), calls)
|
||||
self.assertIn(("process_video", "source.jpg", ("target.mp4/0001.png",)), calls)
|
||||
self.assertIn(("create_video", "target.mp4", 30.0), calls)
|
||||
self.assertIn(("move_temp", "target.mp4", "output.mp4"), calls)
|
||||
|
||||
step_indices = {}
|
||||
for index, call in enumerate(calls):
|
||||
step_indices.setdefault(call[0], index)
|
||||
|
||||
self.assertLess(step_indices["create_temp"], step_indices["extract_frames"])
|
||||
self.assertLess(step_indices["extract_frames"], step_indices["process_video"])
|
||||
self.assertLess(step_indices["process_video"], step_indices["create_video"])
|
||||
self.assertLess(step_indices["create_video"], step_indices["move_temp"])
|
||||
|
||||
def test_non_map_faces_pipe_success_does_not_extract_frames(self):
|
||||
calls = []
|
||||
with _patched_core_import_stubs(calls, pipe_result=True) as core:
|
||||
_configure_video_run(core, map_faces=False)
|
||||
|
||||
with patch.object(core.os.path, "isfile", return_value=True):
|
||||
core.start()
|
||||
|
||||
self.assertIn(("pipe",), calls)
|
||||
self.assertNotIn(("extract_frames", "target.mp4"), calls)
|
||||
self.assertNotIn(("process_video", "source.jpg", ("target.mp4/0001.png",)), calls)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user