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
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,32 @@
All notable changes to FaceID. The Home Assistant app shows this file in the
update dialog; standalone users can watch GitHub releases.

## 0.25.0 — 2026-10-06

- **Folder mode is available in the Home Assistant app.** The directory watcher has existed
in the standalone service since 0.23.0, but the app never offered it: no `folder` options,
`frigate_url` was mandatory, and `/media` and `/share` were not mounted at all — so even a
hand-written path could not be read. Requested in #31 by a user with five Reolink cameras
and no Frigate, uploading snapshots to `/media` over FTP.
- `frigate_url` may now be left empty. An empty URL means no Frigate: the service falls back
to a no-op client, and the gallery, unknown review and history stay usable.
- `/media` and `/share` are mounted **read-only**. FaceID never changes or deletes what it
watches, and no other host path is visible to the app.
- New options: `folder_enabled`, `folder_path`, `folder_camera`, `folder_recursive`,
`folder_process_existing`, `folder_extensions`, `folder_poll_interval`,
`folder_settle_seconds`, `folder_max_frames`, `folder_max_people_per_file`,
`folder_max_indexed_files`.
- The app now refuses to start with neither input configured, and warns on start when
`folder_path` does not exist inside the container — naming the two mounted roots, since a
path the app cannot see is the likeliest mistake.
- **In folder mode the camera sensor appears at startup**, not only after the first
recognition. The folder has no camera list to query, so its name is taken from the
configuration; without that a fresh install looked like it was doing nothing.
- **Fixed: three options were settable and inert.** `cross_risk_margin`, `self_outlier_ratio`
and `history_keep` were offered by the app and read by the service, but `run.sh` never
wrote them into the generated config — the same defect as #24, in three more fields. Found
by a new test that checks every option reaches the generated file.

## 0.24.0 — 2026-09-15

- **The folder input no longer remembers every file forever.** `data/folder_ingest.json`
Expand Down
45 changes: 43 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,9 @@ the whole gallery would need re-enrolling.
(or manually: **Settings → Apps → App Store → ⋮ → Repositories** → add
`https://github.com/SkyTechNerds/faceid`)
2. Install the **FaceID** app, set your Frigate URL in the options (MQTT is picked up
automatically from the Mosquitto app) and start it.
automatically from the Mosquitto app) and start it. **No Frigate?** Leave `frigate_url`
empty and switch on `folder_enabled` instead — see
[folder mode in the app](#folder-mode-in-the-home-assistant-app).
3. Open the **FaceID** panel in the sidebar. First start downloads the model (~300 MB).

The app is built locally on your machine (amd64/aarch64). See
Expand Down Expand Up @@ -238,7 +240,10 @@ Trade-offs, TLS, what FaceID actually requests, and which setup fits which netwo

Contributed by [@thethereza](https://github.com/thethereza) — the idea and the implementation are his. FaceID began as a Frigate companion, but the recognition, gallery, unknown review and history were never Frigate-specific; only the source of the images was.

Set `frigate.enabled: false` and add a `folder` section to `config.yaml`:
Standalone: set `frigate.enabled: false` and add a `folder` section to `config.yaml`.
Running the Home Assistant app? The same input is available there —
[jump to the app options](#folder-mode-in-the-home-assistant-app).


```yaml
frigate:
Expand Down Expand Up @@ -291,6 +296,42 @@ either raise the cap or set it to `0`. Replacing a file at the same path gives i
processes it again. The Unknown-tab button becomes **Scan recording folder**, and the
header shows the number of processed files.

## Folder mode in the Home Assistant app

Available since v0.25.0 — before that the input existed only in the standalone service.

The app mounts `/media` and `/share` **read-only**; FaceID never changes or deletes what it
watches. No other host path is visible to it, so put the images somewhere under those two.

```yaml
frigate_url: "" # leave empty — no Frigate
folder_enabled: true
folder_path: /media/faceid
folder_camera: front_door # the MQTT sensor becomes sensor.faceid_front_door
folder_extensions:
- .jpg # the default list is video-only, see below
- .jpeg
- .png
- .webp
```

**Still images need their extension listed.** The default `folder_extensions` covers video
only. A folder of `.jpg` is skipped in silence until you add it — and the key *replaces*
the list rather than extending it, so keep the video formats you still want.

**A camera that overwrites the same file works.** FaceID fingerprints by size and
modification time, not by name, so `front.jpg` replaced on every motion event is seen as a
new recording each time.

⚠️ **One camera name per folder.** `folder_camera` applies to everything the watcher finds.
Several cameras writing into one directory all report under that single name. For separate
sensors, run them through separate paths — or use Frigate, which carries the camera name
with each event.

Everything else is shared with the standalone service: the gallery, unknown review,
history, MQTT sensors and the `max_indexed_files` cap all behave the same. Full option
reference: [faceid-addon/DOCS.md](faceid-addon/DOCS.md).

## Getting started

1. **Scan your camera history** (optional but recommended): in the Unknown tab, click
Expand Down
17 changes: 15 additions & 2 deletions app/mqtt_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -835,9 +835,22 @@ def _publish_discovery(self, only: list | None = None):
if only is not None:
cams = set(only)
else:
cams = (self.cameras
# Der Ordner-Eingang hat keine Kameraliste, die man abfragen koennte — sein
# Kameraname steht nur in der Konfiguration. Ohne ihn entstuende im reinen
# Ordnerbetrieb gar kein Sensor, bis das erste Gesicht erkannt wird, und bis
# dahin saehe die Installation aus, als tue sie nichts.
fc = self.cfg.get("folder") or {}
folder_cams = set()
if fc.get("enabled"):
folder_cams = {str(fc.get("camera") or "").strip()} - {""}
# Vorrang wie bisher: ausdrueckliche Liste, sonst Discovery-Liste, sonst
# Frigate fragen. Die Ordner-Kamera kommt immer DAZU statt zu ersetzen —
# bei Frigate UND Ordner muessen beide Sensoren entstehen. Frigate wird nur
# gefragt, wenn es ueberhaupt an ist: der Aufruf geht ueber das Netz.
cams = (set(self.cameras)
or set(self.cfg["faceid"].get("discovery_cameras") or [])
or self._frigate_cameras())
or (self._frigate_cameras() if self.frigate_enabled else set()))
cams = cams | folder_cams
self._announced |= cams
log.info("MQTT discovery: announced %d sensor(s)%s", len(cams),
"" if cams else " — cameras unknown, they follow on first recognition")
Expand Down
26 changes: 26 additions & 0 deletions faceid-addon/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,32 @@
All notable changes to FaceID. The Home Assistant app shows this file in the
update dialog; standalone users can watch GitHub releases.

## 0.25.0 — 2026-10-06

- **Folder mode is available in the Home Assistant app.** The directory watcher has existed
in the standalone service since 0.23.0, but the app never offered it: no `folder` options,
`frigate_url` was mandatory, and `/media` and `/share` were not mounted at all — so even a
hand-written path could not be read. Requested in #31 by a user with five Reolink cameras
and no Frigate, uploading snapshots to `/media` over FTP.
- `frigate_url` may now be left empty. An empty URL means no Frigate: the service falls back
to a no-op client, and the gallery, unknown review and history stay usable.
- `/media` and `/share` are mounted **read-only**. FaceID never changes or deletes what it
watches, and no other host path is visible to the app.
- New options: `folder_enabled`, `folder_path`, `folder_camera`, `folder_recursive`,
`folder_process_existing`, `folder_extensions`, `folder_poll_interval`,
`folder_settle_seconds`, `folder_max_frames`, `folder_max_people_per_file`,
`folder_max_indexed_files`.
- The app now refuses to start with neither input configured, and warns on start when
`folder_path` does not exist inside the container — naming the two mounted roots, since a
path the app cannot see is the likeliest mistake.
- **In folder mode the camera sensor appears at startup**, not only after the first
recognition. The folder has no camera list to query, so its name is taken from the
configuration; without that a fresh install looked like it was doing nothing.
- **Fixed: three options were settable and inert.** `cross_risk_margin`, `self_outlier_ratio`
and `history_keep` were offered by the app and read by the service, but `run.sh` never
wrote them into the generated config — the same defect as #24, in three more fields. Found
by a new test that checks every option reaches the generated file.

## 0.24.0 — 2026-09-15

- **The folder input no longer remembers every file forever.** `data/folder_ingest.json`
Expand Down
44 changes: 44 additions & 0 deletions faceid-addon/DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Full documentation: https://github.com/SkyTechNerds/faceid
## Setup

1. Set `frigate_url` to your Frigate instance (e.g. `http://192.168.1.10:5000`).
**No Frigate?** Leave it empty and use folder mode instead (below).
2. MQTT: leave `mqtt_host` empty to automatically use the Mosquitto broker app.
Fill the `mqtt_*` options only for an external broker.
3. Optional: restrict processing to specific cameras (`cameras`), and list the cameras
Expand Down Expand Up @@ -42,6 +43,49 @@ Full documentation: https://github.com/SkyTechNerds/faceid
| `frigate_topic_prefix` | must match `mqtt.topic_prefix` in Frigate's own config (default `frigate`). Wrong value = FaceID hears nothing at all |
| `poll_interval` | seconds; >0 also polls Frigate's event API for events MQTT never announces (e.g. events created by an automation from a camera's own detection). 0 = off |
| `backup_enabled` / `backup_hour` / `backup_keep` | optional built-in daily gallery backup |
| `cross_risk_margin` | how close two people's references may get before one is set aside (relative to `match_threshold`; 0 or below = off) |
| `self_outlier_ratio` | sets aside a reference photo that fits its own person far worse than the rest (0 = off) |
| `history_keep` | how many recognitions the History tab keeps (0 = history off) |
| `folder_*` | watch a directory instead of (or alongside) Frigate — see below |

## Folder mode — watch a directory instead of Frigate

For setups without Frigate: point FaceID at a folder of finished recordings or snapshots.
Everything else stays the same — gallery, unknown review, history, MQTT sensors.

```yaml
frigate_url: ""
folder_enabled: true
folder_path: /media/faceid
folder_camera: front_door
folder_extensions: [".jpg", ".jpeg", ".png", ".webp"]
```

⚠️ **Only `/media` and `/share` are visible to the app**, mounted read-only — FaceID never
changes or deletes the files it watches. A path under `/config` or on the host will not be
found, and the log says so on start.

⚠️ **Still images need their extension listed.** The default `folder_extensions` is
video-only, and the option *replaces* that list rather than adding to it. A folder of
`.jpg` is skipped in silence until you list it.

A camera that keeps overwriting the same file works: FaceID fingerprints by size and
modification time, not by name. One `folder_camera` applies to the whole folder, so
several cameras writing into one directory all report under that single sensor.

| Option | Description |
|---|---|
| `folder_enabled` | switch the directory watcher on |
| `folder_path` | directory to watch, under `/media` or `/share` |
| `folder_camera` | name used for the MQTT sensor and in the history |
| `folder_recursive` | also watch subdirectories |
| `folder_process_existing` | process what is already there on first start (off = only new files) |
| `folder_extensions` | which file types to pick up — replaces the default video-only list |
| `folder_poll_interval` | seconds between directory scans |
| `folder_settle_seconds` | a file must be this old and unchanged across two scans before it is read |
| `folder_max_frames` | frames sampled per video |
| `folder_max_people_per_file` | cap on distinct faces kept from one file |
| `folder_max_indexed_files` | cap on the "already seen" index (0 = unlimited). Keep it above the number of files the folder holds, otherwise every scan re-reads the overflow |

Thresholds and backup can also be changed live on the app's **Settings** tab; those
edits are stored in the app's data volume and override these options. The Settings tab
Expand Down
17 changes: 15 additions & 2 deletions faceid-addon/app/mqtt_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -835,9 +835,22 @@ def _publish_discovery(self, only: list | None = None):
if only is not None:
cams = set(only)
else:
cams = (self.cameras
# Der Ordner-Eingang hat keine Kameraliste, die man abfragen koennte — sein
# Kameraname steht nur in der Konfiguration. Ohne ihn entstuende im reinen
# Ordnerbetrieb gar kein Sensor, bis das erste Gesicht erkannt wird, und bis
# dahin saehe die Installation aus, als tue sie nichts.
fc = self.cfg.get("folder") or {}
folder_cams = set()
if fc.get("enabled"):
folder_cams = {str(fc.get("camera") or "").strip()} - {""}
# Vorrang wie bisher: ausdrueckliche Liste, sonst Discovery-Liste, sonst
# Frigate fragen. Die Ordner-Kamera kommt immer DAZU statt zu ersetzen —
# bei Frigate UND Ordner muessen beide Sensoren entstehen. Frigate wird nur
# gefragt, wenn es ueberhaupt an ist: der Aufruf geht ueber das Netz.
cams = (set(self.cameras)
or set(self.cfg["faceid"].get("discovery_cameras") or [])
or self._frigate_cameras())
or (self._frigate_cameras() if self.frigate_enabled else set()))
cams = cams | folder_cams
self._announced |= cams
log.info("MQTT discovery: announced %d sensor(s)%s", len(cams),
"" if cams else " — cameras unknown, they follow on first recognition")
Expand Down
39 changes: 37 additions & 2 deletions faceid-addon/config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: FaceID
version: "0.24.0"
version: "0.25.0"
slug: faceid
description: Face recognition for Frigate — trainable gallery, clustered unknown review, HA sensors
url: https://github.com/SkyTechNerds/faceid
Expand All @@ -18,6 +18,12 @@ ports:
8600/tcp: null
ports_description:
8600/tcp: Web UI without ingress (optional)
# Nur lesend: FaceID veraendert und loescht die beobachteten Aufnahmen nie. Ohne diese
# beiden Einhaengungen sieht die App /media und /share ueberhaupt nicht — genau daran
# scheitert der Ordner-Eingang in der App, nicht an fehlenden Optionen.
map:
- media:ro
- share:ro
services:
- mqtt:want
options:
Expand Down Expand Up @@ -58,9 +64,26 @@ options:
set_sub_label: true
cameras: []
discovery_cameras: []
folder_enabled: false
folder_path: /media/faceid
folder_camera: front_door
folder_recursive: true
folder_process_existing: true
folder_extensions:
- .mp4
- .mkv
- .mov
- .avi
- .webm
- .m4v
folder_poll_interval: 10
folder_settle_seconds: 10
folder_max_frames: 24
folder_max_people_per_file: 6
folder_max_indexed_files: 5000
schema:
mqtt_prefix: str
frigate_url: url
frigate_url: url?
mqtt_host: str?
mqtt_port: port
frigate_user: str?
Expand Down Expand Up @@ -100,3 +123,15 @@ schema:
- str
discovery_cameras:
- str
folder_enabled: bool
folder_path: str?
folder_camera: str?
folder_recursive: bool
folder_process_existing: bool
folder_extensions:
- str
folder_poll_interval: int(1,3600)
folder_settle_seconds: int(0,600)
folder_max_frames: int(1,120)
folder_max_people_per_file: int(1,20)
folder_max_indexed_files: int(0,200000)
43 changes: 43 additions & 0 deletions faceid-addon/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,37 @@ CAMERAS=$(jq -c '.cameras // []' "${OPT}")
DISCOVERY=$(jq -c '.discovery_cameras // []' "${OPT}")
CLIPCAMS=$(jq -c '.clip_fallback_cameras // []' "${OPT}")
LIVECAMS=$(jq -c '.live_hires_fallback_cameras // []' "${OPT}")
FOLDEREXT=$(jq -c '.folder_extensions // []' "${OPT}")

# Eingaenge pruefen, bevor die Konfiguration geschrieben wird. Ohne das startet FaceID
# mit beiden Quellen aus und meldet erst beim ersten Scan, dass es nichts zu tun gibt.
FRIGATE_URL=$(cfg '.frigate_url')
FOLDER_ENABLED=$(cfg '.folder_enabled')
FOLDER_PATH=$(cfg '.folder_path')

if [ -z "${FRIGATE_URL}" ] && [ "${FOLDER_ENABLED}" != "true" ]; then
bashio::log.fatal "No input configured: set frigate_url, or switch folder_enabled on and set folder_path."
exit 1
fi
if [ "${FOLDER_ENABLED}" = "true" ] && [ -z "${FOLDER_PATH}" ]; then
bashio::log.fatal "folder_enabled is on but folder_path is empty."
exit 1
fi
if [ "${FOLDER_ENABLED}" = "true" ] && [ ! -d "${FOLDER_PATH}" ]; then
# Keine Abbruchbedingung: der Ordner darf spaeter entstehen, und FaceID meldet den
# Fehlversuch bei jedem Scan. Aber der haeufigste Fehler ist ein Pfad, den der
# Container gar nicht sieht — deshalb hier einmal laut, mit den erlaubten Wurzeln.
bashio::log.warning "folder_path '${FOLDER_PATH}' does not exist inside the add-on."
bashio::log.warning "Only /media and /share are mounted; /config and host paths are not visible."
fi
if [ -z "${FRIGATE_URL}" ]; then
bashio::log.info "No frigate_url set — running on the folder input alone."
fi

cat > /opt/faceid/config.yaml << EOF
frigate:
# Leere URL heisst: kein Frigate. FrigateAPI leitet ``enabled`` daraus ab und liefert
# dann einen No-op-Client, damit Galerie und Review-Oberflaeche nutzbar bleiben.
url: $(yml '.frigate_url')
# Nur fuer Frigates authentifizierten Port 8971 noetig. Leer lassen heisst offene API:
# FrigateAPI macht aus dem leeren Wert None, genau wie im Standalone-Betrieb.
Expand Down Expand Up @@ -122,13 +150,28 @@ faceid:
backup_hour: $(cfg '.backup_hour')
backup_keep: $(cfg '.backup_keep')
presence_window: $(cfg '.presence_window')
cross_risk_margin: $(cfg '.cross_risk_margin')
self_outlier_ratio: $(cfg '.self_outlier_ratio')
history_keep: $(cfg '.history_keep')
set_sub_label: $(cfg '.set_sub_label')
min_face_px: $(cfg '.min_face_px')
det_size: $(cfg '.det_size')
max_attempts: $(cfg '.max_attempts')
retry_seconds: 2.5
cameras: ${CAMERAS}
discovery_cameras: ${DISCOVERY}
folder:
enabled: $(cfg '.folder_enabled')
path: $(yml '.folder_path')
camera: $(yml '.folder_camera')
recursive: $(cfg '.folder_recursive')
process_existing: $(cfg '.folder_process_existing')
extensions: ${FOLDEREXT}
poll_interval: $(cfg '.folder_poll_interval')
settle_seconds: $(cfg '.folder_settle_seconds')
max_frames: $(cfg '.folder_max_frames')
max_people_per_file: $(cfg '.folder_max_people_per_file')
max_indexed_files: $(cfg '.folder_max_indexed_files')
EOF

# Galerie + Modell-Cache im persistenten /data-Volume (überlebt Updates)
Expand Down
Loading
Loading