Skip to content
Merged
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
60 changes: 52 additions & 8 deletions .github/workflows/desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ jobs:
# This lock set has no torch or transformers; the frozen app
# runs entirely on the pinned, Windows-tested ONNX Runtime.
pip install -r requirements-desktop.txt
if [ "$RUNNER_OS" = "Linux" ]; then
# InsightFace requests the GUI OpenCV wheel, whose private Qt/X11
# libraries confuse linuxdeploy. The sidecar only processes pixels;
# use the same OpenCV release without a second window toolkit.
pip uninstall -y opencv-python
pip install --no-deps opencv-python-headless==4.11.0.86
python -c 'import cv2, numpy as np; from rapidocr_onnxruntime import RapidOCR; assert cv2.resize(np.zeros((16, 16, 3), dtype=np.uint8), (8, 8)).shape == (8, 8, 3); RapidOCR(); print(cv2.getBuildInformation())'
fi

- name: Build the sidecar
shell: bash
Expand Down Expand Up @@ -140,12 +148,8 @@ jobs:
shell: bash
run: |
mkdir -p desktop/src-tauri/binaries
rm -rf desktop/src-tauri/binaries/_internal
rm -f desktop/src-tauri/binaries/photolib-server-*
# Tauri resolves sidecars by target-triple suffix.
cp -r dist/photolib-server/* desktop/src-tauri/binaries/
mv "desktop/src-tauri/binaries/photolib-server${{ matrix.exe }}" \
"desktop/src-tauri/binaries/photolib-server-${{ matrix.triple }}${{ matrix.exe }}"
# Preserve the complete one-folder layout in the resource directory.
cp -a dist/photolib-server desktop/src-tauri/binaries/sidecar

- uses: dtolnay/rust-toolchain@stable

Expand All @@ -159,15 +163,41 @@ jobs:
sudo apt-get update
# Tauri v2 links against webkit2gtk-4.1 (javascriptcoregtk-4.1); the
# old 4.0 packages no longer satisfy the crate and fail the build.
# linuxdeploy's helper AppImages also need the FUSE 2 runtime.
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev \
libayatana-appindicator3-dev librsvg2-dev patchelf \
libxdo-dev libssl-dev build-essential
libxdo-dev libssl-dev build-essential libfuse2

- name: Build the installer
working-directory: desktop
shell: bash
run: |
set -o pipefail
npm ci
npx tauri build --bundles "${{ matrix.bundles }}"
mkdir -p ../artifacts
if [ "$RUNNER_OS" = "Linux" ]; then
# PyInstaller normally supplies this search path at startup.
# linuxdeploy examines the shared objects directly instead.
mapfile -t sidecar_library_dirs < <(find "$PWD/src-tauri/binaries/sidecar/_internal" -type f -name '*.so*' -printf '%h\n' | sort -u)
sidecar_library_path=$(IFS=:; echo "${sidecar_library_dirs[*]}")
export LD_LIBRARY_PATH="$sidecar_library_path${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
while IFS= read -r -d '' library; do
dependencies=$(ldd "$library" 2>/dev/null || true)
if [[ "$dependencies" == *"not found"* ]]; then
printf 'Unresolved dependencies in %s:\n%s\n' "$library" "$dependencies"
exit 1
fi
done < <(find "$PWD/src-tauri/binaries/sidecar/_internal" -type f -name '*.so*' -print0)
fi
npx tauri build --verbose --bundles "${{ matrix.bundles }}" 2>&1 | tee ../artifacts/desktop-build.log

- name: Preserve installer diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: build-log-${{ matrix.os }}
path: artifacts/desktop-build.log
if-no-files-found: ignore

- name: Guard the standard Windows installer size
if: matrix.os == 'windows-latest' && env.MODEL_VARIANT == 'int8'
Expand All @@ -182,6 +212,19 @@ jobs:
}
Write-Host "Hybrid INT8 setup: $([math]::Round($installer.Length / 1MB)) MB"

- name: Install the Windows package for verification
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
$installer = Get-ChildItem desktop/src-tauri/target/release/bundle/nsis/*-setup.exe | Select-Object -First 1
$testInstall = Join-Path $PWD 'installer-smoke'
$process = Start-Process -FilePath $installer.FullName -ArgumentList '/S', "/D=$testInstall" -Wait -PassThru -WindowStyle Hidden
if ($process.ExitCode -ne 0) { throw "Installer exited with $($process.ExitCode)" }

- name: Verify the installed sidecar layout
shell: bash
run: python tools/verify_desktop_bundle.py

- uses: actions/upload-artifact@v4
with:
name: photolib-${{ matrix.os }}
Expand All @@ -203,6 +246,7 @@ jobs:
- uses: actions/download-artifact@v4
with:
path: artifacts
pattern: photolib-*

- uses: softprops/action-gh-release@v2
with:
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ on:
pull_request:

jobs:
ui:
runs-on: ubuntu-latest
defaults:
run:
working-directory: desktop
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: desktop/package-lock.json
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm run test:ui

pytest:
runs-on: ubuntu-latest
strategy:
Expand Down
25 changes: 20 additions & 5 deletions PACKAGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,16 @@ python tools/export_onnx.py --model google/siglip2-base-patch16-224 \

# 2. Freeze the server (the tracked desktop/ui is bundled directly)
pip install -r requirements-desktop.txt
# Linux only: use headless OpenCV to avoid unused Qt/X11 bundle dependencies.
if [ "$(uname -s)" = "Linux" ]; then
pip uninstall -y opencv-python
pip install --no-deps opencv-python-headless==4.11.0.86
fi
PHOTOLIB_MODEL_VARIANT=int8 pyinstaller packaging/photolib.spec --noconfirm --clean

# 3. Stage the complete one-folder sidecar for Tauri
mkdir -p desktop/src-tauri/binaries
cp -r dist/photolib-server/* desktop/src-tauri/binaries/
mv desktop/src-tauri/binaries/photolib-server.exe \
desktop/src-tauri/binaries/photolib-server-x86_64-pc-windows-msvc.exe
cp -a dist/photolib-server desktop/src-tauri/binaries/sidecar
cd desktop && npm ci && npx tauri build --bundles nsis
```

Expand All @@ -80,11 +83,23 @@ photolib_2.0.3_x64-setup.exe /S

Windows needs the MSVC build tools and WebView2 (present on Windows 10 21H2
and later). macOS needs Xcode command line tools. Linux needs
`libwebkit2gtk` and `libgtk-3` development packages.
`libwebkit2gtk` and `libgtk-3` development packages, and `libfuse2` for
AppImage tooling. On Linux, replace the GUI OpenCV wheel as shown above
before freezing the sidecar; the app uses its own Tauri window and only
needs OpenCV for image processing.

For a Linux Tauri build, expose the frozen sidecar's shared-library directories
in `LD_LIBRARY_PATH` during bundling. PyInstaller sets that path when launching
the sidecar, but linuxdeploy inspects its libraries directly. The CI workflow
sets the path and checks for unresolved native dependencies before bundling.

## How it starts

1. The Tauri shell spawns `photolib-server --no-browser` as a sidecar.
1. The Tauri shell resolves `sidecar/photolib-server` inside its resource
directory and spawns it with `--no-browser`. The executable stays beside
its `_internal` support folder on every platform. CI verifies model parity,
UI/API startup, and shutdown from the packaged layout after bundling (from
an installed NSIS package on Windows and extracted DEB/AppImage on Linux).
2. The server picks a **free port** — hardcoding 8000 fails on any machine
where something already holds it — waits until that port is accepting
health requests, then prints `PHOTOLIB_READY {"url": ...}`.
Expand Down
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ and organize albums in a desktop app built for real family libraries of
moved folder or changed drive letter.
- **Curation** — favorites, 0–5 star ratings, saved searches, albums, and a
checksummed backup containing exact confirmed face assignments.
- **Comfortable browsing** — compact controls, visible active filters, adjustable
thumbnail sizes, and paginated albums with continuous viewer navigation.

![Photolib people view](docs/images/photolib-people.png)

Expand All @@ -49,7 +51,7 @@ Download the current **Windows x64 setup** from the
Nothing else is required: no Python, Node.js, account, or API key.

1. Close Photolib if an older copy is running.
2. Run the downloaded `photolib_2.0.3_x64-setup.exe`.
2. Run the downloaded Windows x64 setup executable; its version is shown on the release.
3. Open **photolib** from the Start menu and choose a photo folder.

The app leaves your library data alone during upgrades. The installer is not
Expand All @@ -70,10 +72,12 @@ for the cross-platform build configuration and source packaging instructions.
source .venv/bin/activate

python -m photolib.cli index ~/Pictures # index (recursive, incremental)
python run.py # API on http://127.0.0.1:8000
PHOTO_WEB_DIR=desktop/ui python run.py # desktop UI + API on localhost:8000
```

Then open <http://127.0.0.1:8000/docs> for the API, or run the frontend.
Then open <http://127.0.0.1:8000> for the app, or
<http://127.0.0.1:8000/docs> for the API. In PowerShell, set
`$env:PHOTO_WEB_DIR = "desktop/ui"` before running `python run.py`.

Searching from the terminal works too:

Expand Down Expand Up @@ -298,6 +302,19 @@ It covers indexing, incremental updates, clustering behaviour, search
ranking, filtering, pagination, the HTTP layer, and scaling of the browse
index at 200k photos.

The desktop UI also has browser smoke tests for filters, saved searches,
album pagination and viewer navigation, keyboard focus, and narrow screens:

```bash
cd desktop
npm ci
npx playwright install chromium
npm run test:ui
```

These browser tests use a deterministic API fixture; the Python suite tests
the actual backend and database. Both run in CI.

## Requirements

- Python 3.10+
Expand Down
54 changes: 51 additions & 3 deletions desktop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
"description": "Native desktop shell for photolib",
"scripts": {
"dev": "tauri dev",
"build": "tauri build"
"build": "tauri build",
"test:ui": "node --test tests/ui.test.cjs"
},
"devDependencies": {
"@tauri-apps/cli": "^2"
"@tauri-apps/cli": "^2",
"playwright": "1.62.1"
}
}
6 changes: 1 addition & 5 deletions desktop/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
"description": "Permissions for the photolib desktop shell. Deliberately minimal: the app needs to start its own bundled server and nothing else.",
"windows": ["main"],
"permissions": [
"core:default",
{
"identifier": "shell:allow-spawn",
"allow": [{ "name": "binaries/photolib-server", "sidecar": true }]
}
"core:default"
]
}
22 changes: 20 additions & 2 deletions desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,28 @@ fn main() {
}

fn start_server(handle: tauri::AppHandle) -> Result<(), String> {
// Keep the PyInstaller executable beside its _internal directory. Tauri
// resources live outside the shell's executable directory on macOS/Linux.
let binary = if cfg!(target_os = "windows") {
"photolib-server.exe"
} else {
"photolib-server"
};
let server_path = handle
.path()
.resource_dir()
.map_err(|err| format!("could not locate server resources: {err}"))?
.join("sidecar")
.join(binary);
if !server_path.is_file() {
return Err(format!(
"photolib-server is missing: {}",
server_path.display()
));
}
let (mut rx, child) = handle
.shell()
.sidecar("photolib-server")
.map_err(|err| format!("photolib-server sidecar is missing: {err}"))?
.command(server_path)
.args(["--no-browser"])
.spawn()
.map_err(|err| format!("failed to start the photolib server: {err}"))?;
Expand Down
3 changes: 1 addition & 2 deletions desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@
"icons/icon.ico",
"icons/icon.png"
],
"externalBin": ["binaries/photolib-server"],
"resources": {
"binaries/_internal/": "_internal/"
"binaries/sidecar/": "sidecar/"
},
"shortDescription": "A local, private photo library",
"longDescription": "Search your photos by describing them, and by face. Everything runs on your own machine.",
Expand Down
Loading
Loading