Compare commits

...
13 Commits
Author SHA1 Message Date
iuyua9 bc48ba4308 docs: align GFPGAN model filenames (#1910)
README and models/instructions.txt named GFPGANv1.4, but the enhancer loads gfpgan-1024.onnx via onnxruntime. instructions.txt also pointed at a PyTorch .pth that the ONNX path cannot load at all, so the manual-fallback instructions were unusable.
2026-08-29 15:05:23 +08:00
Ihor Kuzmychov b53844eb57 fix: skip setrlimit(RLIMIT_DATA) on macOS (#1849)
setrlimit(RLIMIT_DATA) is rejected by the macOS kernel for the values used here,
crashing the app during startup. Since --max-memory defaults to suggest_max_memory()
(4 on Darwin), max_memory is always set on macOS and every launch reached this call.

Fixes #1848.
2026-08-29 14:53:57 +08:00
Chris G 20cafb0079 docs: change dependency version number (#1914) 2026-08-29 14:28:40 +08:00
Dopan 7ca6d0b202 Merge pull request #1864 from 5uck1ess/pr/webp-support
feat: WEBP source image support
2026-08-26 10:51:56 +08:00
Kenneth Estanislao f7db37679a Update readme
Includes website links and proper redirect to our official website
2026-08-23 05:12:24 +08:00
Kenneth Estanislao bd500d54b4 auto download some models
Retarget download url for safer model controls
2026-08-23 02:55:32 +08:00
cuyua9 987f6b392b fix: extract frames for map faces fallback (#1824)
Verified this fix. Confirmed the bug by reverting just the `modules/core.py` hunk and
re-running the new regression test — with the old code, `process_video`/`create_video`
run against a temp directory that was never populated when `map_faces=True`, since
`create_temp`/`extract_frames` were skipped for that case. That means map-faces video
runs were silently broken (empty or failed output).

The fix removes the `map_faces` guard so extraction always runs before the disk-based
fallback, which is correct for both cases that reach this branch (map_faces=True, and
non-map-faces pipe failures). `create_temp` is idempotent (mkdir exist_ok=True), so the
double-call for the non-map-faces path is harmless.
2026-08-14 06:44:22 +08:00
Dopan 97a44800a2 Merge pull request #1902 from 1ceseismic/fix/linux-camera-device-path-rebase
fix: no cam detected on Arch Linux, use string path for camera capture instead
2026-08-13 22:50:16 +08:00
Vito-M 345fa4a0b0 fix: no cam detected on Arch Linux, use string path for camera capture instead 2026-08-09 19:48:57 +12:00
Gao Yiman bdeeb3ace0 docs: add onnxruntime-openvino/OpenVINO version pairing note (#1893)
Thanks for this — useful reference table, and it lines up with the version-pairing issues we've been fixing (#1879). Merging.
2026-08-08 07:15:21 +08:00
Tym Rabchuk 47dffeb307 fix(webp): finish extension centralization from pre-submission review
- build the video save-dialog filter from VIDEO_EXTENSIONS (_VIDEO_FILE_FILTER)
  instead of a hardcoded "Videos (*.mp4 *.mkv)" — the last filter that still
  drifted from the canonical set
- remove the now-dead file_types list (unused in both the fork and upstream;
  the PySide6 dialogs use the QFileDialog filter strings) and drop it from the
  centralization comment
2026-06-23 19:30:07 -04:00
Tym Rabchuk 9e1f0cc3a5 fix(webp): address review — drop broken GIF, robust ext check, centralize lists
Review feedback on #1831:
- Remove *.gif from the save/output dialog filter (PR had added it there).
  Verified empirically that cv2.imread/imwrite cannot decode OR encode GIF on
  OpenCV 4.10 *or* 4.11 (write raises, read returns None), so GIF silently
  failed on both ends — dropped from every dialog and from has_image_extension.
- has_image_extension now uses os.path.splitext so only the true extension
  counts ('photo.png.bak' / 'clip.webp.mp4' are no longer treated as images).
- Centralize the supported-extension set in modules.globals (IMAGE_EXTENSIONS /
  VIDEO_EXTENSIONS); file_types, all QFileDialog filters and has_image_extension
  now derive from it instead of hand-copied lists that had already drifted.

WEBP itself is unchanged and works (libwebp ships with opencv-python).
2026-06-22 20:33:11 -04:00
Tym Rabchuk 0b61ad5c0d feat: webp source image support
Ported from April 2026 Fork:
- has_image_extension() now recognizes .webp/.gif/.bmp
- is_image() checks extension before mimetypes (Windows mimetypes
  doesn't always register webp)
- File dialog filter includes *.webp
2026-05-18 21:00:33 -04:00
14 changed files with 483 additions and 65 deletions
+26 -4
View File
@@ -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. 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"> <p align="center">
<a href="https://deeplivecam.net/index.php/quickstart"> <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>
<p align="center"> <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"> <img src="https://github.com/user-attachments/assets/56b61811-3a1e-4672-9b50-cf7f6e8e6852" width="40" alt="Windows">
</a>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;
<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"> <img src="https://github.com/user-attachments/assets/6538e3a6-c957-431a-b586-2d6abcf534dc" width="34" alt="Mac Silicon">
</a>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;
<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"> <img src="https://github.com/user-attachments/assets/ad45142e-426c-4364-a2a9-a512670cc62c" width="40" alt="CPU">
</a>
</p> </p>
<p align="center"> <p align="center">
@@ -60,7 +66,7 @@ Users are expected to use this software responsibly and legally. If using a real
</a> </a>
</p> </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. 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** **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) 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. Place these files in the "**models**" folder.
@@ -227,7 +233,7 @@ pip install git+https://github.com/TencentARC/GFPGAN.git@master
```bash ```bash
pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128 pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128
pip uninstall onnxruntime onnxruntime-gpu pip uninstall onnxruntime onnxruntime-gpu
pip install onnxruntime-gpu==1.21.0 pip install onnxruntime-gpu==1.26.0
``` ```
3. Usage: 3. Usage:
@@ -313,6 +319,22 @@ pip uninstall onnxruntime onnxruntime-openvino
pip install onnxruntime-openvino==1.21.0 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: 2. Usage:
```bash ```bash
+1 -1
View File
@@ -1,4 +1,4 @@
just put the models in this folder - just put the models in this folder -
https://huggingface.co/hacksider/deep-live-cam/resolve/main/inswapper_128_fp16.onnx?download=true 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
View File
@@ -178,6 +178,10 @@ def limit_resources() -> None:
tensorflow.config.experimental.set_memory_growth(gpu, True) tensorflow.config.experimental.set_memory_growth(gpu, True)
# limit memory usage # limit memory usage
if modules.globals.max_memory: 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 memory = modules.globals.max_memory * 1024 ** 3
if platform.system().lower() == 'windows': if platform.system().lower() == 'windows':
import ctypes import ctypes
@@ -276,10 +280,9 @@ def start() -> None:
update_status('Falling back to disk-based processing...') update_status('Falling back to disk-based processing...')
extraction_start = time.time() extraction_start = time.time()
if not modules.globals.map_faces: create_temp(modules.globals.target_path)
create_temp(modules.globals.target_path) update_status('Extracting frames...')
update_status('Extracting frames...') extract_frames(modules.globals.target_path)
extract_frames(modules.globals.target_path)
extraction_time = time.time() - extraction_start extraction_time = time.time() - extraction_start
temp_frame_paths = get_temp_frame_paths(modules.globals.target_path) temp_frame_paths = get_temp_frame_paths(modules.globals.target_path)
+3
View File
@@ -29,6 +29,9 @@ def get_face_analyser() -> Any:
from modules.processors.frame._onnx_enhancer import ( from modules.processors.frame._onnx_enhancer import (
build_provider_config, build_provider_config,
) )
from modules.model_downloader import ensure_insightface_pack
ensure_insightface_pack('buffalo_l')
providers = build_provider_config() providers = build_provider_config()
FACE_ANALYSER = insightface.app.FaceAnalysis( FACE_ANALYSER = insightface.app.FaceAnalysis(
name='buffalo_l', name='buffalo_l',
+7 -4
View File
@@ -6,10 +6,13 @@ from typing import List, Dict, Any
ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
WORKFLOW_DIR = os.path.join(ROOT_DIR, "workflow") WORKFLOW_DIR = os.path.join(ROOT_DIR, "workflow")
file_types = [ # Canonical media extensions, defined once so the file dialogs and
("Image", ("*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp")), # has_image_extension never drift. GIF is intentionally excluded: OpenCV's
("Video", ("*.mp4", "*.mkv")), # 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 # Face Mapping Data
source_target_map: List[Dict[str, Any]] = [] # Stores detailed map for image/video processing source_target_map: List[Dict[str, Any]] = [] # Stores detailed map for image/video processing
+178
View File
@@ -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
+13 -7
View File
@@ -23,6 +23,7 @@ FACE_ENHANCER = None
THREAD_SEMAPHORE = threading.Semaphore() THREAD_SEMAPHORE = threading.Semaphore()
THREAD_LOCK = threading.Lock() THREAD_LOCK = threading.Lock()
NAME = "DLC.FACE-ENHANCER" NAME = "DLC.FACE-ENHANCER"
MODEL_FILE = "gfpgan-1024.onnx"
abs_dir = os.path.dirname(os.path.abspath(__file__)) abs_dir = os.path.dirname(os.path.abspath(__file__))
models_dir = os.path.join( models_dir = os.path.join(
@@ -44,11 +45,12 @@ FFHQ_TEMPLATE_512 = np.array(
def pre_check() -> bool: def pre_check() -> bool:
model_path = os.path.join(models_dir, "gfpgan-1024.onnx") from modules.model_downloader import ensure_model
if not os.path.exists(model_path):
if ensure_model(MODEL_FILE) is None:
update_status( update_status(
f"GFPGAN ONNX model not found at {model_path}. " f"Could not obtain {MODEL_FILE}. Place it in the models folder "
"Please place gfpgan-1024.onnx in the models folder.", "manually or check your internet connection.",
NAME, NAME,
) )
return False return False
@@ -73,11 +75,15 @@ def get_face_enhancer() -> onnxruntime.InferenceSession:
with THREAD_LOCK: with THREAD_LOCK:
if FACE_ENHANCER is None: 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( 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: try:
@@ -22,7 +22,7 @@ from modules.processors.frame._onnx_enhancer import (
NAME = "DLC.FACE-ENHANCER-GPEN256" NAME = "DLC.FACE-ENHANCER-GPEN256"
INPUT_SIZE = 256 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" MODEL_FILE = "GPEN-BFR-256.onnx"
ENHANCER = None 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: def pre_check() -> bool:
model_path = os.path.join(models_dir, MODEL_FILE) if _obtain_model() is None:
if not os.path.exists(model_path): update_status(
update_status(f"Downloading {MODEL_FILE}...", NAME) f"Could not obtain {MODEL_FILE}. Place it in the models folder "
from modules.utilities import conditional_download "manually or check your internet connection.",
conditional_download(models_dir, [MODEL_URL]) NAME,
)
return False
return True return True
@@ -54,12 +75,11 @@ def get_enhancer() -> Any:
global ENHANCER global ENHANCER
with THREAD_LOCK: with THREAD_LOCK:
if ENHANCER is None: if ENHANCER is None:
model_path = os.path.join(models_dir, MODEL_FILE) model_path = _obtain_model()
if not os.path.exists(model_path): if model_path is None:
from modules.utilities import conditional_download raise FileNotFoundError(
conditional_download(models_dir, [MODEL_URL]) f"Model file not found: {os.path.join(models_dir, MODEL_FILE)}"
if not os.path.exists(model_path): )
raise FileNotFoundError(f"Model file not found: {model_path}")
print(f"{NAME}: Loading ONNX model from {model_path}") print(f"{NAME}: Loading ONNX model from {model_path}")
ENHANCER = create_onnx_session(model_path) ENHANCER = create_onnx_session(model_path)
warmup_session(ENHANCER) warmup_session(ENHANCER)
@@ -22,7 +22,7 @@ from modules.processors.frame._onnx_enhancer import (
NAME = "DLC.FACE-ENHANCER-GPEN512" NAME = "DLC.FACE-ENHANCER-GPEN512"
INPUT_SIZE = 512 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" MODEL_FILE = "GPEN-BFR-512.onnx"
ENHANCER = None 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: def pre_check() -> bool:
model_path = os.path.join(models_dir, MODEL_FILE) if _obtain_model() is None:
if not os.path.exists(model_path): update_status(
update_status(f"Downloading {MODEL_FILE}...", NAME) f"Could not obtain {MODEL_FILE}. Place it in the models folder "
from modules.utilities import conditional_download "manually or check your internet connection.",
conditional_download(models_dir, [MODEL_URL]) NAME,
)
return False
return True return True
@@ -54,12 +75,11 @@ def get_enhancer() -> Any:
global ENHANCER global ENHANCER
with THREAD_LOCK: with THREAD_LOCK:
if ENHANCER is None: if ENHANCER is None:
model_path = os.path.join(models_dir, MODEL_FILE) model_path = _obtain_model()
if not os.path.exists(model_path): if model_path is None:
from modules.utilities import conditional_download raise FileNotFoundError(
conditional_download(models_dir, [MODEL_URL]) f"Model file not found: {os.path.join(models_dir, MODEL_FILE)}"
if not os.path.exists(model_path): )
raise FileNotFoundError(f"Model file not found: {model_path}")
print(f"{NAME}: Loading ONNX model from {model_path}") print(f"{NAME}: Loading ONNX model from {model_path}")
ENHANCER = create_onnx_session(model_path) ENHANCER = create_onnx_session(model_path)
warmup_session(ENHANCER) warmup_session(ENHANCER)
+18 -10
View File
@@ -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.face_analyser import get_one_face, get_many_faces, default_source_face
from modules.typing import Face, Frame from modules.typing import Face, Frame
from modules.utilities import ( from modules.utilities import (
conditional_download,
is_image, is_image,
is_video, is_video,
) )
@@ -200,13 +199,18 @@ def pre_check() -> bool:
logging.error(f"Failed to create directory {download_directory_path} due to permission error: {e}") logging.error(f"Failed to create directory {download_directory_path} due to permission error: {e}")
return False return False
# Use the direct download URL from Hugging Face (FP32 model for broad GPU compatibility) from modules.model_downloader import ensure_any
conditional_download(
download_directory_path, variants = ["inswapper_128.onnx", "inswapper_128_fp16.onnx"]
[ if _HAS_TORCH_CUDA:
"https://huggingface.co/hacksider/deep-live-cam/resolve/main/inswapper_128.onnx" 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 return True
@@ -242,8 +246,12 @@ def get_face_swapper() -> Any:
elif os.path.exists(fp32_path): elif os.path.exists(fp32_path):
model_path = fp32_path model_path = fp32_path
else: else:
update_status(f"No inswapper model found in {models_dir}.", NAME) if not pre_check():
return None 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 # On Apple Silicon, rewrite Pad(reflect) → Slice+Concat so
# CoreML can run the entire model in a single partition on # CoreML can run the entire model in a single partition on
# the Neural Engine instead of bouncing between CPU and ANE. # the Neural Engine instead of bouncing between CPU and ANE.
+19 -7
View File
@@ -237,6 +237,18 @@ _RECENT_SOURCE_DIR: Optional[str] = None
_RECENT_TARGET_DIR: Optional[str] = None _RECENT_TARGET_DIR: Optional[str] = None
_RECENT_OUTPUT_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 ───────────────────────────────────────────────────── # ─── image utilities ─────────────────────────────────────────────────────
@@ -417,7 +429,7 @@ def get_available_cameras() -> Tuple[List[int], List[str]]:
indices: List[int] = [] indices: List[int] = []
names: List[str] = [] names: List[str] = []
for i in range(10): for i in range(10):
cap = cv2.VideoCapture(i) cap = cv2.VideoCapture(f"/dev/video{i}")
if cap.isOpened(): if cap.isOpened():
indices.append(i) indices.append(i)
names.append(f"Camera {i}") names.append(f"Camera {i}")
@@ -734,7 +746,7 @@ class MainWindow(QMainWindow):
path, _filter = QFileDialog.getOpenFileName( path, _filter = QFileDialog.getOpenFileName(
self, _("select an source image"), self, _("select an source image"),
_RECENT_SOURCE_DIR or "", _RECENT_SOURCE_DIR or "",
"Images (*.png *.jpg *.jpeg *.gif *.bmp)", _IMAGE_FILE_FILTER,
) )
if path and is_image(path): if path and is_image(path):
modules.globals.source_path = path modules.globals.source_path = path
@@ -755,7 +767,7 @@ class MainWindow(QMainWindow):
path, _filter = QFileDialog.getOpenFileName( path, _filter = QFileDialog.getOpenFileName(
self, _("select an target image or video"), self, _("select an target image or video"),
_RECENT_TARGET_DIR or "", _RECENT_TARGET_DIR or "",
"Media (*.png *.jpg *.jpeg *.gif *.bmp *.mp4 *.mkv)", _MEDIA_FILE_FILTER,
) )
if not path: if not path:
return return
@@ -886,13 +898,13 @@ class MainWindow(QMainWindow):
path, _f = QFileDialog.getSaveFileName( path, _f = QFileDialog.getSaveFileName(
self, _("save image output file"), self, _("save image output file"),
os.path.join(_RECENT_OUTPUT_DIR or "", "output.png"), os.path.join(_RECENT_OUTPUT_DIR or "", "output.png"),
"Images (*.png *.jpg *.jpeg *.bmp)", _IMAGE_FILE_FILTER,
) )
elif is_video(modules.globals.target_path): elif is_video(modules.globals.target_path):
path, _f = QFileDialog.getSaveFileName( path, _f = QFileDialog.getSaveFileName(
self, _("save video output file"), self, _("save video output file"),
os.path.join(_RECENT_OUTPUT_DIR or "", "output.mp4"), os.path.join(_RECENT_OUTPUT_DIR or "", "output.mp4"),
"Videos (*.mp4 *.mkv)", _VIDEO_FILE_FILTER,
) )
else: else:
return return
@@ -1334,7 +1346,7 @@ class MapperDialog(QDialog):
path, _f = QFileDialog.getOpenFileName( path, _f = QFileDialog.getOpenFileName(
self, _("select an source image"), self, _("select an source image"),
_RECENT_SOURCE_DIR or "", _RECENT_SOURCE_DIR or "",
"Images (*.png *.jpg *.jpeg *.gif *.bmp)", _IMAGE_FILE_FILTER,
) )
if not path: if not path:
return return
@@ -1439,7 +1451,7 @@ class LiveMapperDialog(QDialog):
path, _f = QFileDialog.getOpenFileName( path, _f = QFileDialog.getOpenFileName(
self, _("select an source image"), self, _("select an source image"),
_RECENT_SOURCE_DIR or "", _RECENT_SOURCE_DIR or "",
"Images (*.png *.jpg *.jpeg *.gif *.bmp)", _IMAGE_FILE_FILTER,
) )
if not path: if not path:
return return
+6 -1
View File
@@ -262,11 +262,16 @@ def clean_temp(target_path: str) -> None:
def has_image_extension(image_path: str) -> bool: 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: def is_image(image_path: str) -> bool:
if image_path and os.path.isfile(image_path): 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) mimetype, _ = mimetypes.guess_type(image_path)
return bool(mimetype and mimetype.startswith("image/")) return bool(mimetype and mimetype.startswith("image/"))
return False return False
+2 -1
View File
@@ -71,8 +71,9 @@ class VideoCapturer:
self.cap.release() self.cap.release()
except Exception: except Exception:
continue continue
elif platform.system() == "Linux":
self.cap = cv2.VideoCapture(f"/dev/video{self.device_index}")
else: else:
# Unix-like systems (Linux/Mac) capture method
self.cap = cv2.VideoCapture(self.device_index) self.cap = cv2.VideoCapture(self.device_index)
if not self.cap or not self.cap.isOpened(): if not self.cap or not self.cap.isOpened():
+137
View File
@@ -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()