Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions catalog.toml
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,16 @@ plugin_api = 25
version = "1.0.0"
updated_at = 1786549052
rev = "4933b1c459459af3a0f9d14898b76ce0564d43b3"

[[plugin]]
id = "noctalia/wallpaper_depth_large"
name = "Wallpaper Depth Large"
version = "1.0.2"
updated_at = 1786565714
added_at = 1786549052
author = "noctalia"
license = "MIT"
icon = "layers-subtract"
description = "Generate depth masks that place desktop widgets behind wallpaper foregrounds. Uses the larger model for better results."
plugin_api = 26
tags = ["wallpaper", "desktop", "ai"]
90 changes: 90 additions & 0 deletions wallpaper_depth/depth_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,95 @@ def atomic_json(path: Path, value: dict[str, object]) -> None:
os.replace(temporary, path)


def is_nixos() -> bool:
"""NixOS has no FHS-style /lib, /usr/lib search path, so pip-installed
manylinux wheels (numpy/onnxruntime/Pillow) can fail to find shared
libraries such as libstdc++.so.6 at import time even though `pip install`
itself succeeds."""
if Path("/etc/NIXOS").exists():
return True
try:
os_release = Path("/etc/os-release").read_text(encoding="utf-8")
except OSError:
return False
return any(line.strip() in ('ID=nixos', 'ID="nixos"') for line in os_release.splitlines())


# stdenv.cc.cc.lib provides libstdc++.so.6 and libgcc_s.so.1 (onnxruntime)
# and libgomp.so.1 (numpy's OpenBLAS threading); zlib covers Pillow/onnxruntime
# fallbacks that aren't always vendored inside the wheel itself.
NIX_LIBRARY_PACKAGES = ("stdenv.cc.cc.lib", "zlib")


def nix_library_cache_path(data_dir: Path) -> Path:
return data_dir / "runtime" / "nix-library-path.json"


def resolve_nix_library_path(data_dir: Path) -> str:
cache_path = nix_library_cache_path(data_dir)
try:
cached = json.loads(cache_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
cached = None
if (
isinstance(cached, dict)
and cached.get("packages") == list(NIX_LIBRARY_PACKAGES)
and isinstance(cached.get("libDirs"), list)
):
lib_dirs = [Path(entry) for entry in cached["libDirs"]]
if lib_dirs and all(path.is_dir() for path in lib_dirs):
return os.pathsep.join(str(path) for path in lib_dirs)

store_paths: list[str] = []
for attribute in NIX_LIBRARY_PACKAGES:
try:
result = subprocess.run(
["nix-build", "<nixpkgs>", "-A", attribute, "--no-out-link"],
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
)
except FileNotFoundError as error:
raise RuntimeError(
"nix-build was not found; cannot resolve NixOS runtime libraries "
"needed by numpy/onnxruntime/Pillow"
) from error
if result.returncode != 0 or not result.stdout.strip():
raise RuntimeError(
f"nix-build could not resolve nixpkgs#{attribute}: "
f"{result.stderr.strip() or 'no output'}"
)
store_paths.append(result.stdout.strip().splitlines()[-1])

lib_dirs = [str(Path(path) / "lib") for path in store_paths]
atomic_json(
cache_path,
{"packages": list(NIX_LIBRARY_PACKAGES), "libDirs": lib_dirs, "resolvedAt": int(time.time())},
)
return os.pathsep.join(lib_dirs)


def ensure_nixos_dynamic_linking(data_dir: Path) -> None:
"""On NixOS, re-exec this interpreter with LD_LIBRARY_PATH patched so
pip-installed manylinux wheels can find libstdc++/libgomp/zlib. Every
subprocess spawned afterwards (venv creation, pip install, the runtime
readiness check, and the venv python that runs `generate`) inherits this
process's environment, so patching it once here is enough for the whole
plugin -- no changes to service.luau are needed."""
if os.environ.get("_WALLPAPER_DEPTH_NIXOS_PATCHED") == "1":
return
if not is_nixos():
return
lib_path = resolve_nix_library_path(data_dir)
existing = os.environ.get("LD_LIBRARY_PATH", "")
new_env = dict(os.environ)
new_env["LD_LIBRARY_PATH"] = f"{lib_path}{os.pathsep}{existing}" if existing else lib_path
new_env["_WALLPAPER_DEPTH_NIXOS_PATCHED"] = "1"
os.execve(sys.executable, [sys.executable] + sys.argv, new_env)


def runtime_python(data_dir: Path) -> Path:
return data_dir / "runtime" / ".venv" / "bin" / "python"

Expand Down Expand Up @@ -393,6 +482,7 @@ def main() -> int:
args = parse_args()
data_dir = args.data_dir.expanduser().resolve()
try:
ensure_nixos_dynamic_linking(data_dir)
if args.command == "setup":
setup(data_dir)
elif args.command == "status":
Expand Down
71 changes: 71 additions & 0 deletions wallpaper_depth_large/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Wallpaper Depth

Wallpaper Depth generates a foreground mask for each image wallpaper, allowing
Noctalia desktop widgets to pass behind nearby scenery.

Depth estimation runs locally with
[Depth Anything V2 Small](https://huggingface.co/onnx-community/depth-anything-v2-small).
Wallpapers are never uploaded.

## Plugin

| Field | Value |
| --- | --- |
| ID | `noctalia/wallpaper_depth_large` |
| Entries | Service: `service`; panel: `manager` |

## Requirements

Install `python3` version 3.11–3.14 with `venv` and `pip` support. Initial setup
requires network access, and shell offline mode must be disabled.

From the plugin panel, setup creates an isolated Python environment in the
plugin data directory and downloads the 99 MB Apache-2.0 Depth Anything V2
Small ONNX model from Hugging Face. The runtime contains pinned versions of
NumPy, ONNX Runtime, and Pillow; it does not modify the system Python
environment.

## Usage

1. Open the **Wallpaper Depth** panel, or toggle it from a terminal:

```sh
noctalia msg panel-toggle noctalia/wallpaper_depth_large:manager
```

2. Select **Install model** and wait for setup to finish.
3. Apply an image wallpaper to each desired output.
4. Select **Generate masks**, or leave **Generate automatically** enabled.
5. Adjust **Foreground threshold** and **Edge feather** in the plugin settings
when the default mask does not match the scene.

The panel reports generation state for every connected output. **Clear cache**
removes saved depth maps and masks; the plugin regenerates them when needed.

## Settings

| Setting | Type | Default | Description |
| --- | --- | --- | --- |
| `auto_generate` | `bool` | `true` | Regenerates masks when a wallpaper or mask parameter changes. |
| `threshold` | `int` | `50` | Normalized depth cutoff from 0–100. Lower values place more of the scene in front of desktop widgets. |
| `feather` | `int` | `8` | Soft-transition width around the depth cutoff, from 0–50. |

## How it works

The service processes each output independently. It preserves the wallpaper's
aspect ratio during inference, normalizes the relative depth prediction, and
refines it against the source image before applying the configured threshold
and feather. The resulting mask is restored to the wallpaper's original
resolution and registered with Noctalia for that output.

Depth predictions are cached separately from masks, so changing threshold or
feather can reuse the expensive model result. Cache entries are keyed by the
wallpaper contents, model revision, and processing version, and old entries are
pruned automatically.

## Licensing and privacy

The plugin is MIT-licensed. Depth Anything V2 Small is downloaded at setup time
under the Apache-2.0 license. Model inference, depth caches, and generated masks
remain in the local plugin data directory; only the model download contacts
Hugging Face.
Loading