_face_aware_crop_box() previously always centered the crop on the union of all detected faces' centroid, even when the plain center-crop already kept every face fully on screen -- unnecessarily moving a composition that didn't need fixing. Now starts from the plain center-crop and only shifts it the minimum amount needed to bring an otherwise-cropped-out face back into frame; already-fine framing is left untouched (falls back to centering on the faces' midpoint only if they're spread too wide for any single shift to contain them all, which is unchanged from before). Verified: a face safely inside the plain center-crop now produces byte- identical output to the no-shift case (previously it still would have been re-centered); an edge face gets a 100px shift instead of the 1050px a full re-center would have applied. Re-ran against the real 4-face test photo from earlier -- all four were already fully visible, so the refined box now exactly matches the plain center-crop instead of shifting unnecessarily.
ESPresso Frame Server
Pulls photos from an Immich album, resizes/dithers/quantizes them to the E Ink Spectra 6 panel's exact 6-color format, and serves the frame a ready-to-display image once an hour. All the image processing happens here so the ESP32 never has to decode a JPEG or run a dithering algorithm itself -- it just streams the response straight to the panel.
Setup
- Get an Immich API key: in Immich, go to Account Settings -> API Keys -> New API Key. Read-only access to albums/assets is enough.
- Run the server:
docker compose up -d - Open
http://<this-machine>:8420/in a browser, enter your Immich URL and API key, click Load Albums, pick one, and Save. - On the ESP32's captive portal setup form, set the Tools Server field
to
<this-machine>:8420.
Endpoints
GET /-- config UIGET /api/albums-- lists Immich albums (used by the config UI)POST /api/config-- saves Immich URL/API key/album/orderGET /frame/image-- returns the current photo pre-processed into the panel's raw 800x480, 4-bit-per-pixel, 2-pixels-per-byte format (application/octet-stream, exactly 192,000 bytes)GET /health-- liveness check
Notes
- Config (including the Immich API key) is stored in
./data/config.jsonon the host via the compose volume mount. /frame/imageisn't authenticated yet. That's fine on a trusted home LAN for now, but worth revisiting once the ESP32 side is wired up to send a shared device token.- The 6-color palette RGB values in
app/image_pipeline.pyare approximations, not measured values (Waveshare doesn't publish exact color primaries for this panel) -- tune them once you can compare a rendered test image against the real panel.
Deploying a pre-built image
Every push to main that touches server/ triggers a Gitea Actions
workflow (.gitea/workflows/server-docker-build.yml) that builds this
image and pushes it to this repo's Gitea Container Registry at
git.thumeit.com/tfaour/espresso-frame-server. docker-compose.yml
already points at that image, so a deploy host doesn't need this repo's
build context at all -- just the compose file:
docker compose pull
docker compose up -d
docker compose build (or up --build) still works too, for local
iteration against your own Dockerfile changes.
Local development (without Docker)
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
CONFIG_PATH=./data/config.json uvicorn app.main:app --reload --host 0.0.0.0 --port 8420
--host 0.0.0.0 matters here: without it, uvicorn defaults to
127.0.0.1 (localhost-only), which the ESP32 can't reach over the LAN.
The Docker image already binds 0.0.0.0 by default.