glanced 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- glanced-0.3.0/LICENSE +21 -0
- glanced-0.3.0/NOTICE +15 -0
- glanced-0.3.0/PKG-INFO +317 -0
- glanced-0.3.0/README.md +296 -0
- glanced-0.3.0/pyproject.toml +37 -0
- glanced-0.3.0/setup.cfg +4 -0
- glanced-0.3.0/src/glanced/__init__.py +8 -0
- glanced-0.3.0/src/glanced/align.py +55 -0
- glanced-0.3.0/src/glanced/attention.py +360 -0
- glanced-0.3.0/src/glanced/camera.py +122 -0
- glanced-0.3.0/src/glanced/cli.py +676 -0
- glanced-0.3.0/src/glanced/daemon.py +409 -0
- glanced-0.3.0/src/glanced/embed.py +61 -0
- glanced-0.3.0/src/glanced/enroll.py +339 -0
- glanced-0.3.0/src/glanced/gui/__init__.py +40 -0
- glanced-0.3.0/src/glanced/gui/enroll_window.py +547 -0
- glanced-0.3.0/src/glanced/gui/passphrase.py +146 -0
- glanced-0.3.0/src/glanced/ipc.py +103 -0
- glanced-0.3.0/src/glanced/landmarker.py +95 -0
- glanced-0.3.0/src/glanced/liveness/__init__.py +66 -0
- glanced-0.3.0/src/glanced/liveness/analyzer.py +95 -0
- glanced-0.3.0/src/glanced/liveness/bezel.py +211 -0
- glanced-0.3.0/src/glanced/liveness/cues.py +405 -0
- glanced-0.3.0/src/glanced/liveness/features.py +245 -0
- glanced-0.3.0/src/glanced/liveness/frame.py +175 -0
- glanced-0.3.0/src/glanced/liveness/geometry.py +238 -0
- glanced-0.3.0/src/glanced/liveness/glare.py +85 -0
- glanced-0.3.0/src/glanced/liveness/planar.py +366 -0
- glanced-0.3.0/src/glanced/liveness/scoring.py +193 -0
- glanced-0.3.0/src/glanced/livetest.py +161 -0
- glanced-0.3.0/src/glanced/locksetup.py +143 -0
- glanced-0.3.0/src/glanced/models.py +102 -0
- glanced-0.3.0/src/glanced/pamsetup.py +179 -0
- glanced-0.3.0/src/glanced/paths.py +58 -0
- glanced-0.3.0/src/glanced/pipeline.py +137 -0
- glanced-0.3.0/src/glanced/poses.py +187 -0
- glanced-0.3.0/src/glanced/preview.py +147 -0
- glanced-0.3.0/src/glanced/scan.py +95 -0
- glanced-0.3.0/src/glanced/servicesetup.py +121 -0
- glanced-0.3.0/src/glanced/store.py +138 -0
- glanced-0.3.0/src/glanced.egg-info/PKG-INFO +317 -0
- glanced-0.3.0/src/glanced.egg-info/SOURCES.txt +57 -0
- glanced-0.3.0/src/glanced.egg-info/dependency_links.txt +1 -0
- glanced-0.3.0/src/glanced.egg-info/entry_points.txt +2 -0
- glanced-0.3.0/src/glanced.egg-info/requires.txt +13 -0
- glanced-0.3.0/src/glanced.egg-info/top_level.txt +1 -0
- glanced-0.3.0/tests/test_attention.py +389 -0
- glanced-0.3.0/tests/test_daemon.py +203 -0
- glanced-0.3.0/tests/test_enroll.py +104 -0
- glanced-0.3.0/tests/test_enroll_passphrase.py +146 -0
- glanced-0.3.0/tests/test_enroll_window.py +39 -0
- glanced-0.3.0/tests/test_liveness.py +353 -0
- glanced-0.3.0/tests/test_locksetup.py +36 -0
- glanced-0.3.0/tests/test_pamsetup.py +48 -0
- glanced-0.3.0/tests/test_passphrase_dialog.py +62 -0
- glanced-0.3.0/tests/test_paths.py +65 -0
- glanced-0.3.0/tests/test_poses.py +231 -0
- glanced-0.3.0/tests/test_preview.py +116 -0
- glanced-0.3.0/tests/test_servicesetup.py +103 -0
glanced-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ayan De
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
glanced-0.3.0/NOTICE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
This project reimplements the liveness detection model from Glance
|
|
2
|
+
(https://github.com/jonnyoo/glance) by Jonathan Zhou, MIT licensed.
|
|
3
|
+
|
|
4
|
+
The five-cue deny/confirm design, the tuning constants, and the reasoning
|
|
5
|
+
recorded in the comments of src/glanced/liveness/ originate there. The Swift
|
|
6
|
+
implementation was not copied; each module was rewritten against MediaPipe,
|
|
7
|
+
OpenCV and NumPy, and the deviations are documented at each site.
|
|
8
|
+
|
|
9
|
+
The ArcFace recognition model is from InsightFace
|
|
10
|
+
(https://github.com/deepinsight/insightface).
|
|
11
|
+
|
|
12
|
+
The lock screen's success animation (patches/omarchy-lock-faceid/
|
|
13
|
+
unlock-spin.png) is frames 16-45 of the unlock video shipped with Glance
|
|
14
|
+
(Resources/unlockanimation.mp4), cut to the inside of its ring and reduced
|
|
15
|
+
to an alpha mask so it can take the current theme's colour.
|
glanced-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: glanced
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Face unlock for Linux with real liveness detection
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
License-File: NOTICE
|
|
10
|
+
Requires-Dist: numpy>=1.26
|
|
11
|
+
Provides-Extra: runtime
|
|
12
|
+
Requires-Dist: opencv-python>=4.9; extra == "runtime"
|
|
13
|
+
Requires-Dist: onnxruntime>=1.17; extra == "runtime"
|
|
14
|
+
Requires-Dist: mediapipe>=0.10.14; extra == "runtime"
|
|
15
|
+
Requires-Dist: cryptography>=42; extra == "runtime"
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
18
|
+
Provides-Extra: gui
|
|
19
|
+
Requires-Dist: PySide6>=6.6; extra == "gui"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# glance-linux
|
|
23
|
+
|
|
24
|
+
Face unlock for Linux, with the liveness detection that face-unlock on Linux
|
|
25
|
+
usually doesn't have.
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
A reimplementation of the liveness model from
|
|
30
|
+
[Glance](https://github.com/jonnyoo/glance) (macOS, MIT) around a PAM-based
|
|
31
|
+
unlock path. Not a port of the app — none of the Swift is portable — but the
|
|
32
|
+
part of Glance with real substance is: the five-cue liveness model that tells a
|
|
33
|
+
face from a photograph.
|
|
34
|
+
|
|
35
|
+
## Why
|
|
36
|
+
|
|
37
|
+
[Howdy](https://github.com/boltgolt/howdy) already does face unlock on Arch. Its
|
|
38
|
+
well-known weakness is that it has essentially no liveness detection, so a
|
|
39
|
+
printed photo can unlock it. That is exactly the gap this fills.
|
|
40
|
+
|
|
41
|
+
The other half is that **Linux is a better platform for this than macOS**.
|
|
42
|
+
Glance's own README carries the caveat that *"macOS has no API that lets a
|
|
43
|
+
third-party app authorize a login, so Glance unlocks by typing your stored
|
|
44
|
+
password on the lock screen."* Linux has PAM. So this project:
|
|
45
|
+
|
|
46
|
+
- stores **no password** anywhere,
|
|
47
|
+
- injects **no keystrokes**,
|
|
48
|
+
- needs **no accessibility/input-injection permission**,
|
|
49
|
+
- and authorizes the session directly, through the same interface `sudo` and
|
|
50
|
+
`hyprlock` already use.
|
|
51
|
+
|
|
52
|
+
That removes the single most sensitive secret in the macOS design.
|
|
53
|
+
|
|
54
|
+
### It is still not FaceID
|
|
55
|
+
|
|
56
|
+
A webcam sees a flat 2D image; an iPhone builds a 3D depth map. The liveness
|
|
57
|
+
cues here defeat a printed photo and a photo on a phone screen with reasonable
|
|
58
|
+
confidence. They do **not** reliably defeat a video of you. This is a
|
|
59
|
+
convenience feature, not a security upgrade.
|
|
60
|
+
|
|
61
|
+
Two guard rails come with that. Five failed scans in a row with a face in
|
|
62
|
+
view lock face unlock out for five minutes, so a looping hands-free lock
|
|
63
|
+
screen is not a free brute-force surface (`--max-failures`, `--lockout`). And
|
|
64
|
+
`SECURITY.md` spells out the trust boundary: the daemon runs as your user, so
|
|
65
|
+
code already running as you could subvert it — the same boundary Howdy and
|
|
66
|
+
the shell's own lock have, but one you should read before relying on it.
|
|
67
|
+
|
|
68
|
+
## Status
|
|
69
|
+
|
|
70
|
+
| Piece | State |
|
|
71
|
+
|---|---|
|
|
72
|
+
| Liveness model (`src/glanced/liveness/`) | **Complete, ported, tested** |
|
|
73
|
+
| Geometry / homography | Complete |
|
|
74
|
+
| Enrollment store (AES-256-GCM) | Complete |
|
|
75
|
+
| Alignment + ArcFace embedding | Complete |
|
|
76
|
+
| Camera capture, landmarking, scan loop | Complete |
|
|
77
|
+
| Daemon: sockets, arming, status | Complete, tested |
|
|
78
|
+
| `glancectl` (enroll, arm, authenticate, status, live, selftest) | Complete |
|
|
79
|
+
| Guided enrollment (`glanced/poses.py`, `--gui` tick ring) | Complete — five directions, after the macOS onboarding sweep |
|
|
80
|
+
| `pam_glance` + `glancectl setup-pam` | Complete — see `pam/README.md` |
|
|
81
|
+
| Attention mode (`attention.sock`, `glancectl attention`) | Complete, tested — head pose for the desktop, see below |
|
|
82
|
+
| Omarchy plugin (`plugin/`) | Bar widget + panel — see `plugin/README.md` |
|
|
83
|
+
| Lock screen indicator (`patches/omarchy-lock-faceid/`) | Face ID-style capsule with a live camera view — a patch to Omarchy's lock plugin, applied by `glancectl setup-lock` and re-applied after `omarchy update` by a post-update hook |
|
|
84
|
+
|
|
85
|
+
## Install
|
|
86
|
+
|
|
87
|
+
Once `glanced` is on the AUR, the packaged path is two commands and three
|
|
88
|
+
buttons — the package carries the daemon, the PAM module and both models, so
|
|
89
|
+
there is nothing to download and nothing to build:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
yay -S glanced
|
|
93
|
+
omarchy plugin add https://github.com/ayandexyz/omarchy-glance.git --enable
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Then click the bar icon and take the one button it offers, three times: **Start
|
|
97
|
+
daemon**, **Enroll** (the guided sweep opens in a window), **Wire lock screen**
|
|
98
|
+
(a terminal, for the one step that needs your password). A fourth, **Add lock
|
|
99
|
+
indicator**, is optional: the Face ID-style capsule on the lock screen, kept
|
|
100
|
+
in place across `omarchy update` by a hook. `packaging/aur/` holds
|
|
101
|
+
the PKGBUILD and the release runbook.
|
|
102
|
+
|
|
103
|
+
The package deliberately does not touch `/etc/pam.d` itself. Changing how the
|
|
104
|
+
machine authenticates you belongs to a command you run and watch, not to an
|
|
105
|
+
unattended pacman transaction editing files that belong to hyprland and
|
|
106
|
+
omarchy.
|
|
107
|
+
|
|
108
|
+
## Setup from source
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
python -m venv .venv && .venv/bin/pip install -e '.[runtime,gui,dev]'
|
|
112
|
+
.venv/bin/python -m pytest # no camera needed
|
|
113
|
+
.venv/bin/glancectl fetch-model # ~3MB landmarker + ~13MB ArcFace
|
|
114
|
+
.venv/bin/glancectl live --mode heavy # liveness against your webcam, no unlock
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Then the real thing:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
packaging/install.sh # user service + plugin symlink
|
|
121
|
+
glancectl enroll --gui --name "$USER" --remember # guided sweep, sets the passphrase
|
|
122
|
+
glancectl authenticate # one full scan: recognition + liveness
|
|
123
|
+
glancectl setup-pam # wire the lock screen (sudo; keep a root shell open)
|
|
124
|
+
glancectl setup-lock # optional: the lock screen indicator + its post-update hook
|
|
125
|
+
omarchy plugin enable io.github.ayandexyz.glance # the bar widget
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Lock the screen, press Enter (shell lock: any character then Enter), look at
|
|
129
|
+
the camera. `pam/README.md` explains the two lock screens Omarchy has had, the
|
|
130
|
+
on-demand vs `--hands-free` choice, and how to undo it.
|
|
131
|
+
|
|
132
|
+
`enroll` walks you through five head directions — centre, then left, up,
|
|
133
|
+
right and down — and takes two samples at each, gated on the yaw and pitch the
|
|
134
|
+
landmarker reports rather than on you being asked nicely to move. That is what
|
|
135
|
+
makes ten rows cover five poses instead of ten near-copies of a frontal
|
|
136
|
+
capture. The macOS app sweeps nine, adding the diagonals; those ask for a
|
|
137
|
+
compound turn that is harder to explain and to hold, and a template already
|
|
138
|
+
covering both profiles and both chin extremes has the corners bracketed. `--gui` shows the sweep as a Face ID-style tick ring around a
|
|
139
|
+
mirrored camera disc, each direction lighting its sector as it lands; without
|
|
140
|
+
it the same sweep runs against a one-line terminal readout. `--no-guide` falls
|
|
141
|
+
back to the old five prompted captures, for a camera whose landmarker reports
|
|
142
|
+
no head pose at all. No frame is written in any of the three: the preview is
|
|
143
|
+
pixels on the way to the screen and nothing else.
|
|
144
|
+
|
|
145
|
+
`enroll` asks for a passphrase the first time; it encrypts the embeddings at
|
|
146
|
+
rest. The daemon starts *disarmed* and cannot scan until it has that
|
|
147
|
+
passphrase: either `glancectl arm` after each login, or `--remember`, which
|
|
148
|
+
stores it 0600 under `~/.local/share/glance/` so the daemon arms itself. That
|
|
149
|
+
is a convenience/at-rest trade-off you make explicitly.
|
|
150
|
+
|
|
151
|
+
`glancectl authenticate` sends exactly the request `pam_glance` sends, so the
|
|
152
|
+
whole unlock path can be exercised without touching PAM.
|
|
153
|
+
|
|
154
|
+
### What a plugin can and cannot do
|
|
155
|
+
|
|
156
|
+
The Omarchy plugin is QML and runs inside the shell; it can draw status and
|
|
157
|
+
run commands as you. It cannot install a PAM module, edit `/etc/pam.d`, or
|
|
158
|
+
ship a Python daemon. So "install the plugin and face unlock works" is not a
|
|
159
|
+
thing any marketplace plugin can deliver on its own. The intended shape is:
|
|
160
|
+
|
|
161
|
+
1. a package (AUR `glanced`) that installs `glancectl`, the daemon service,
|
|
162
|
+
and `pam_glance.so`;
|
|
163
|
+
2. one `glancectl enroll` and one `glancectl setup-pam` (the sudo step);
|
|
164
|
+
3. the plugin, which shows the state, tells you which of those is missing,
|
|
165
|
+
and offers arm, disarm and test-scan.
|
|
166
|
+
|
|
167
|
+
`glancectl selftest` is the counterpart of Glance's hidden Face Lab: it drives
|
|
168
|
+
the real decision logic against synthetic faces and prints every cue's reading
|
|
169
|
+
and fire count, with no camera involved. `glancectl live` does the same against
|
|
170
|
+
real frames.
|
|
171
|
+
|
|
172
|
+
## Attention mode
|
|
173
|
+
|
|
174
|
+
The daemon can also tell the desktop where you are looking. Connect to
|
|
175
|
+
`attention.sock` and it streams one JSON line per frame:
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{"schemaVersion": 1, "t": 1234.5, "state": "tracking", "present": true, "yaw": -12.4, "pitch": 3.1, "conf": 1.0}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`yaw` and `pitch` are degrees (yaw positive when your head turns to your
|
|
182
|
+
left, pitch positive when your chin comes down); `conf` is how far the face
|
|
183
|
+
is above the size the landmarker is trusted at. `state` is `starting`,
|
|
184
|
+
`tracking`, `paused` (an unlock scan has the camera) or `error` (it could not
|
|
185
|
+
be opened; the daemon retries). `glancectl attention` prints the stream.
|
|
186
|
+
|
|
187
|
+
This is what a blur shield — [omarchy-shy](https://github.com/ayandexyz/omarchy-shy),
|
|
188
|
+
the ShyGlass idea done without screen capture — or an idle inhibitor
|
|
189
|
+
subscribes to. Things that matter about it:
|
|
190
|
+
|
|
191
|
+
- **It is not auth mode.** Attention needs the landmarker and the pose it
|
|
192
|
+
already computes, nothing else: no ArcFace, no template, no arming, no
|
|
193
|
+
passphrase. It works on a daemon that has never been armed for a user who
|
|
194
|
+
has never enrolled or touched `/etc/pam.d`.
|
|
195
|
+
- **It is a third socket, publish-only.** Nothing a client sends is read, so
|
|
196
|
+
nothing on it can start or influence a scan. It emits *derived* values —
|
|
197
|
+
angles and a bool — never frames, never landmarks. `src/glanced/attention.py`
|
|
198
|
+
is short enough to check that claim in a minute.
|
|
199
|
+
- **The camera has one owner.** The tracker holds it only while a subscriber
|
|
200
|
+
is connected — nothing listening, camera closed, LED off — and hands it
|
|
201
|
+
over the moment a scan asks, taking it back when the scan ends. An auth
|
|
202
|
+
request never waits on it for more than three seconds.
|
|
203
|
+
- **It is cheap.** Eight landmarker passes a second at 640×360, about 8% of
|
|
204
|
+
one core on a laptop with the camera included. `--attention-fps` tunes it;
|
|
205
|
+
`--no-attention` removes the socket entirely.
|
|
206
|
+
- **Clients fail open.** Only `tracking` says anything about where you are
|
|
207
|
+
looking. A client covering the screen treats every other state, and
|
|
208
|
+
silence, as "come down".
|
|
209
|
+
|
|
210
|
+
## The liveness model
|
|
211
|
+
|
|
212
|
+
Five cues, two roles, and deliberately **no overall liveness percentage**.
|
|
213
|
+
|
|
214
|
+
Upstream arrived at this after real-device testing killed an earlier design that
|
|
215
|
+
averaged ~11 signals into a weighted score: most were noise-limited at webcam
|
|
216
|
+
resolution, several actively *rewarded* the smooth motion of a hand holding up a
|
|
217
|
+
phone, and the resulting number wandered 30–80% on a live face while a phone
|
|
218
|
+
photo scored about the same. Only five cues separated a real face from a phone,
|
|
219
|
+
and each is individually decisive — which makes averaging exactly the wrong
|
|
220
|
+
combination rule.
|
|
221
|
+
|
|
222
|
+
**Deny cues** — evidence of a spoof. Either one firing fails the scan outright
|
|
223
|
+
and overrides any confirmation that already happened. A spoof tell does not get
|
|
224
|
+
outvoted.
|
|
225
|
+
|
|
226
|
+
| Cue | Fires when |
|
|
227
|
+
|---|---|
|
|
228
|
+
| Gloss/glare | One big flat specular blob (glass) rather than skin's small scattered shine |
|
|
229
|
+
| Device detected | A device-shaped rectangle overlaps the face |
|
|
230
|
+
|
|
231
|
+
**Confirm cues** — evidence of a real face. Any one is enough, and their
|
|
232
|
+
*absence is never a failure*: a live person can sit still and not blink for a
|
|
233
|
+
whole scan.
|
|
234
|
+
|
|
235
|
+
| Cue | Fires when |
|
|
236
|
+
|---|---|
|
|
237
|
+
| Flat vs 3D | Held-out nose points miss the best-fit homography — the face has depth |
|
|
238
|
+
| Depth/pose | Nose offset tracks head yaw at a magnitude only a real nose produces |
|
|
239
|
+
| Blink | Eye aspect ratio dipped and recovered |
|
|
240
|
+
|
|
241
|
+
`Light` runs the deny cues only — "confirmed unless proven wrong", which never
|
|
242
|
+
blocks a user who happens to sit still. `Heavy` also requires a confirm cue, and
|
|
243
|
+
can genuinely fail to unlock a motionless, unblinking live user. That cost is
|
|
244
|
+
pinned in a test so it is never mistaken for a regression.
|
|
245
|
+
|
|
246
|
+
## A bug found while porting
|
|
247
|
+
|
|
248
|
+
The depth/pose cue upstream gates on the Pearson correlation between nose offset
|
|
249
|
+
and `tan(yaw)`, documented as *"a genuinely positive relationship of the kind
|
|
250
|
+
only a nose sitting off the eye plane produces."*
|
|
251
|
+
|
|
252
|
+
That is not true of a plane viewed in perspective. Perspective projection does
|
|
253
|
+
not preserve midpoints, so a tilted photo's apparent eye midpoint does shift
|
|
254
|
+
relative to its nose. The shift is tiny — but correlation is scale-free and
|
|
255
|
+
cannot tell a tiny systematic drift from a large one. Measured against
|
|
256
|
+
`tests/synthetic.py`, a flat photo scores **1.00** at zero landmark noise and
|
|
257
|
+
**0.95** at 0.25px, both well over the 0.8 fire threshold. It only drops below
|
|
258
|
+
the threshold around 1px of jitter: the cue was relying on landmark noise to
|
|
259
|
+
hide the artifact.
|
|
260
|
+
|
|
261
|
+
The fix is a magnitude gate on the regression slope, which is the physical
|
|
262
|
+
quantity the cue is actually reasoning about — the nose's depth as a fraction of
|
|
263
|
+
the interocular distance. It is ~0.24 for a real nose and ~0.026 for a plane,
|
|
264
|
+
stable across 0–1px of noise, so the gate sits at 0.08. See
|
|
265
|
+
`MIN_NOSE_DEPTH_RATIO` in `liveness/scoring.py` and the regression tests.
|
|
266
|
+
|
|
267
|
+
This affects the macOS app too. Its Heavy mode can confirm a printed photo that
|
|
268
|
+
shows no glare and no device edge, whenever landmark jitter is low.
|
|
269
|
+
|
|
270
|
+
## Deviations from upstream
|
|
271
|
+
|
|
272
|
+
Each is documented at its own site; the significant ones:
|
|
273
|
+
|
|
274
|
+
- **Device bezel detection** is rebuilt on OpenCV contours. Upstream uses
|
|
275
|
+
`VNDetectRectanglesRequest`, which has no Linux equivalent. Parameters carry
|
|
276
|
+
over unchanged, but `minimum_edge_support` is an invented analogue of Vision's
|
|
277
|
+
confidence and is the knob most likely to need retuning against real footage.
|
|
278
|
+
- **Landmark regions** come from MediaPipe FaceMesh index groups rather than
|
|
279
|
+
Vision's named regions. Cross-frame correspondence is *guaranteed* here, which
|
|
280
|
+
is strictly better than what upstream must defend against.
|
|
281
|
+
- **`MEDIAN_LINE` is narrowed** to the two midline points between the brow and
|
|
282
|
+
lip lines. The probe set requires points geometrically inside the fit hull, so
|
|
283
|
+
that leftover error reads as depth rather than extrapolation; Vision's
|
|
284
|
+
forehead-to-chin median line violates that.
|
|
285
|
+
- **Eye aspect ratio** stays the bounding-box ratio even though MediaPipe would
|
|
286
|
+
support the classic 6-point formula, because the 0.65 dip and 0.7 recovery
|
|
287
|
+
thresholds were tuned against this definition.
|
|
288
|
+
- **No password, no keystroke injection** — see above.
|
|
289
|
+
|
|
290
|
+
## Architecture
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
src/glanced/ glanced camera -> landmarks -> {ArcFace embed, liveness} -> verdict
|
|
294
|
+
camera -> landmarks -> head pose (attention.sock)
|
|
295
|
+
pam/ pam_glance.so talks to the daemon over a 0600 unix socket
|
|
296
|
+
plugin/ Omarchy QML bar widget + panel: status, arm/disarm, test scan
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Unlock lives in PAM, in `hyprlock`'s stack, and works whether or not the shell
|
|
300
|
+
is running. The plugin is a thin client over a **separate, lower-privilege
|
|
301
|
+
status socket** and is presentation only — it can never cause or influence an
|
|
302
|
+
unlock. Separate sockets rather than one with a role field, so a compromised
|
|
303
|
+
shell plugin cannot reach the auth verb at all. The attention socket is the
|
|
304
|
+
same idea one step further: it has no verbs at all.
|
|
305
|
+
|
|
306
|
+
> When wiring `pam_glance`, keep a root TTY open. A broken PAM stack locks you
|
|
307
|
+
> out of your own machine.
|
|
308
|
+
|
|
309
|
+
## Credit
|
|
310
|
+
|
|
311
|
+
- [Glance](https://github.com/jonnyoo/glance) — the liveness model and its
|
|
312
|
+
tuning, MIT © Jonathan Zhou. See `NOTICE`.
|
|
313
|
+
- [InsightFace](https://github.com/deepinsight/insightface) — the ArcFace model.
|
|
314
|
+
|
|
315
|
+
## License
|
|
316
|
+
|
|
317
|
+
MIT
|
glanced-0.3.0/README.md
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# glance-linux
|
|
2
|
+
|
|
3
|
+
Face unlock for Linux, with the liveness detection that face-unlock on Linux
|
|
4
|
+
usually doesn't have.
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
A reimplementation of the liveness model from
|
|
9
|
+
[Glance](https://github.com/jonnyoo/glance) (macOS, MIT) around a PAM-based
|
|
10
|
+
unlock path. Not a port of the app — none of the Swift is portable — but the
|
|
11
|
+
part of Glance with real substance is: the five-cue liveness model that tells a
|
|
12
|
+
face from a photograph.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
[Howdy](https://github.com/boltgolt/howdy) already does face unlock on Arch. Its
|
|
17
|
+
well-known weakness is that it has essentially no liveness detection, so a
|
|
18
|
+
printed photo can unlock it. That is exactly the gap this fills.
|
|
19
|
+
|
|
20
|
+
The other half is that **Linux is a better platform for this than macOS**.
|
|
21
|
+
Glance's own README carries the caveat that *"macOS has no API that lets a
|
|
22
|
+
third-party app authorize a login, so Glance unlocks by typing your stored
|
|
23
|
+
password on the lock screen."* Linux has PAM. So this project:
|
|
24
|
+
|
|
25
|
+
- stores **no password** anywhere,
|
|
26
|
+
- injects **no keystrokes**,
|
|
27
|
+
- needs **no accessibility/input-injection permission**,
|
|
28
|
+
- and authorizes the session directly, through the same interface `sudo` and
|
|
29
|
+
`hyprlock` already use.
|
|
30
|
+
|
|
31
|
+
That removes the single most sensitive secret in the macOS design.
|
|
32
|
+
|
|
33
|
+
### It is still not FaceID
|
|
34
|
+
|
|
35
|
+
A webcam sees a flat 2D image; an iPhone builds a 3D depth map. The liveness
|
|
36
|
+
cues here defeat a printed photo and a photo on a phone screen with reasonable
|
|
37
|
+
confidence. They do **not** reliably defeat a video of you. This is a
|
|
38
|
+
convenience feature, not a security upgrade.
|
|
39
|
+
|
|
40
|
+
Two guard rails come with that. Five failed scans in a row with a face in
|
|
41
|
+
view lock face unlock out for five minutes, so a looping hands-free lock
|
|
42
|
+
screen is not a free brute-force surface (`--max-failures`, `--lockout`). And
|
|
43
|
+
`SECURITY.md` spells out the trust boundary: the daemon runs as your user, so
|
|
44
|
+
code already running as you could subvert it — the same boundary Howdy and
|
|
45
|
+
the shell's own lock have, but one you should read before relying on it.
|
|
46
|
+
|
|
47
|
+
## Status
|
|
48
|
+
|
|
49
|
+
| Piece | State |
|
|
50
|
+
|---|---|
|
|
51
|
+
| Liveness model (`src/glanced/liveness/`) | **Complete, ported, tested** |
|
|
52
|
+
| Geometry / homography | Complete |
|
|
53
|
+
| Enrollment store (AES-256-GCM) | Complete |
|
|
54
|
+
| Alignment + ArcFace embedding | Complete |
|
|
55
|
+
| Camera capture, landmarking, scan loop | Complete |
|
|
56
|
+
| Daemon: sockets, arming, status | Complete, tested |
|
|
57
|
+
| `glancectl` (enroll, arm, authenticate, status, live, selftest) | Complete |
|
|
58
|
+
| Guided enrollment (`glanced/poses.py`, `--gui` tick ring) | Complete — five directions, after the macOS onboarding sweep |
|
|
59
|
+
| `pam_glance` + `glancectl setup-pam` | Complete — see `pam/README.md` |
|
|
60
|
+
| Attention mode (`attention.sock`, `glancectl attention`) | Complete, tested — head pose for the desktop, see below |
|
|
61
|
+
| Omarchy plugin (`plugin/`) | Bar widget + panel — see `plugin/README.md` |
|
|
62
|
+
| Lock screen indicator (`patches/omarchy-lock-faceid/`) | Face ID-style capsule with a live camera view — a patch to Omarchy's lock plugin, applied by `glancectl setup-lock` and re-applied after `omarchy update` by a post-update hook |
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
Once `glanced` is on the AUR, the packaged path is two commands and three
|
|
67
|
+
buttons — the package carries the daemon, the PAM module and both models, so
|
|
68
|
+
there is nothing to download and nothing to build:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
yay -S glanced
|
|
72
|
+
omarchy plugin add https://github.com/ayandexyz/omarchy-glance.git --enable
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Then click the bar icon and take the one button it offers, three times: **Start
|
|
76
|
+
daemon**, **Enroll** (the guided sweep opens in a window), **Wire lock screen**
|
|
77
|
+
(a terminal, for the one step that needs your password). A fourth, **Add lock
|
|
78
|
+
indicator**, is optional: the Face ID-style capsule on the lock screen, kept
|
|
79
|
+
in place across `omarchy update` by a hook. `packaging/aur/` holds
|
|
80
|
+
the PKGBUILD and the release runbook.
|
|
81
|
+
|
|
82
|
+
The package deliberately does not touch `/etc/pam.d` itself. Changing how the
|
|
83
|
+
machine authenticates you belongs to a command you run and watch, not to an
|
|
84
|
+
unattended pacman transaction editing files that belong to hyprland and
|
|
85
|
+
omarchy.
|
|
86
|
+
|
|
87
|
+
## Setup from source
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
python -m venv .venv && .venv/bin/pip install -e '.[runtime,gui,dev]'
|
|
91
|
+
.venv/bin/python -m pytest # no camera needed
|
|
92
|
+
.venv/bin/glancectl fetch-model # ~3MB landmarker + ~13MB ArcFace
|
|
93
|
+
.venv/bin/glancectl live --mode heavy # liveness against your webcam, no unlock
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Then the real thing:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
packaging/install.sh # user service + plugin symlink
|
|
100
|
+
glancectl enroll --gui --name "$USER" --remember # guided sweep, sets the passphrase
|
|
101
|
+
glancectl authenticate # one full scan: recognition + liveness
|
|
102
|
+
glancectl setup-pam # wire the lock screen (sudo; keep a root shell open)
|
|
103
|
+
glancectl setup-lock # optional: the lock screen indicator + its post-update hook
|
|
104
|
+
omarchy plugin enable io.github.ayandexyz.glance # the bar widget
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Lock the screen, press Enter (shell lock: any character then Enter), look at
|
|
108
|
+
the camera. `pam/README.md` explains the two lock screens Omarchy has had, the
|
|
109
|
+
on-demand vs `--hands-free` choice, and how to undo it.
|
|
110
|
+
|
|
111
|
+
`enroll` walks you through five head directions — centre, then left, up,
|
|
112
|
+
right and down — and takes two samples at each, gated on the yaw and pitch the
|
|
113
|
+
landmarker reports rather than on you being asked nicely to move. That is what
|
|
114
|
+
makes ten rows cover five poses instead of ten near-copies of a frontal
|
|
115
|
+
capture. The macOS app sweeps nine, adding the diagonals; those ask for a
|
|
116
|
+
compound turn that is harder to explain and to hold, and a template already
|
|
117
|
+
covering both profiles and both chin extremes has the corners bracketed. `--gui` shows the sweep as a Face ID-style tick ring around a
|
|
118
|
+
mirrored camera disc, each direction lighting its sector as it lands; without
|
|
119
|
+
it the same sweep runs against a one-line terminal readout. `--no-guide` falls
|
|
120
|
+
back to the old five prompted captures, for a camera whose landmarker reports
|
|
121
|
+
no head pose at all. No frame is written in any of the three: the preview is
|
|
122
|
+
pixels on the way to the screen and nothing else.
|
|
123
|
+
|
|
124
|
+
`enroll` asks for a passphrase the first time; it encrypts the embeddings at
|
|
125
|
+
rest. The daemon starts *disarmed* and cannot scan until it has that
|
|
126
|
+
passphrase: either `glancectl arm` after each login, or `--remember`, which
|
|
127
|
+
stores it 0600 under `~/.local/share/glance/` so the daemon arms itself. That
|
|
128
|
+
is a convenience/at-rest trade-off you make explicitly.
|
|
129
|
+
|
|
130
|
+
`glancectl authenticate` sends exactly the request `pam_glance` sends, so the
|
|
131
|
+
whole unlock path can be exercised without touching PAM.
|
|
132
|
+
|
|
133
|
+
### What a plugin can and cannot do
|
|
134
|
+
|
|
135
|
+
The Omarchy plugin is QML and runs inside the shell; it can draw status and
|
|
136
|
+
run commands as you. It cannot install a PAM module, edit `/etc/pam.d`, or
|
|
137
|
+
ship a Python daemon. So "install the plugin and face unlock works" is not a
|
|
138
|
+
thing any marketplace plugin can deliver on its own. The intended shape is:
|
|
139
|
+
|
|
140
|
+
1. a package (AUR `glanced`) that installs `glancectl`, the daemon service,
|
|
141
|
+
and `pam_glance.so`;
|
|
142
|
+
2. one `glancectl enroll` and one `glancectl setup-pam` (the sudo step);
|
|
143
|
+
3. the plugin, which shows the state, tells you which of those is missing,
|
|
144
|
+
and offers arm, disarm and test-scan.
|
|
145
|
+
|
|
146
|
+
`glancectl selftest` is the counterpart of Glance's hidden Face Lab: it drives
|
|
147
|
+
the real decision logic against synthetic faces and prints every cue's reading
|
|
148
|
+
and fire count, with no camera involved. `glancectl live` does the same against
|
|
149
|
+
real frames.
|
|
150
|
+
|
|
151
|
+
## Attention mode
|
|
152
|
+
|
|
153
|
+
The daemon can also tell the desktop where you are looking. Connect to
|
|
154
|
+
`attention.sock` and it streams one JSON line per frame:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{"schemaVersion": 1, "t": 1234.5, "state": "tracking", "present": true, "yaw": -12.4, "pitch": 3.1, "conf": 1.0}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`yaw` and `pitch` are degrees (yaw positive when your head turns to your
|
|
161
|
+
left, pitch positive when your chin comes down); `conf` is how far the face
|
|
162
|
+
is above the size the landmarker is trusted at. `state` is `starting`,
|
|
163
|
+
`tracking`, `paused` (an unlock scan has the camera) or `error` (it could not
|
|
164
|
+
be opened; the daemon retries). `glancectl attention` prints the stream.
|
|
165
|
+
|
|
166
|
+
This is what a blur shield — [omarchy-shy](https://github.com/ayandexyz/omarchy-shy),
|
|
167
|
+
the ShyGlass idea done without screen capture — or an idle inhibitor
|
|
168
|
+
subscribes to. Things that matter about it:
|
|
169
|
+
|
|
170
|
+
- **It is not auth mode.** Attention needs the landmarker and the pose it
|
|
171
|
+
already computes, nothing else: no ArcFace, no template, no arming, no
|
|
172
|
+
passphrase. It works on a daemon that has never been armed for a user who
|
|
173
|
+
has never enrolled or touched `/etc/pam.d`.
|
|
174
|
+
- **It is a third socket, publish-only.** Nothing a client sends is read, so
|
|
175
|
+
nothing on it can start or influence a scan. It emits *derived* values —
|
|
176
|
+
angles and a bool — never frames, never landmarks. `src/glanced/attention.py`
|
|
177
|
+
is short enough to check that claim in a minute.
|
|
178
|
+
- **The camera has one owner.** The tracker holds it only while a subscriber
|
|
179
|
+
is connected — nothing listening, camera closed, LED off — and hands it
|
|
180
|
+
over the moment a scan asks, taking it back when the scan ends. An auth
|
|
181
|
+
request never waits on it for more than three seconds.
|
|
182
|
+
- **It is cheap.** Eight landmarker passes a second at 640×360, about 8% of
|
|
183
|
+
one core on a laptop with the camera included. `--attention-fps` tunes it;
|
|
184
|
+
`--no-attention` removes the socket entirely.
|
|
185
|
+
- **Clients fail open.** Only `tracking` says anything about where you are
|
|
186
|
+
looking. A client covering the screen treats every other state, and
|
|
187
|
+
silence, as "come down".
|
|
188
|
+
|
|
189
|
+
## The liveness model
|
|
190
|
+
|
|
191
|
+
Five cues, two roles, and deliberately **no overall liveness percentage**.
|
|
192
|
+
|
|
193
|
+
Upstream arrived at this after real-device testing killed an earlier design that
|
|
194
|
+
averaged ~11 signals into a weighted score: most were noise-limited at webcam
|
|
195
|
+
resolution, several actively *rewarded* the smooth motion of a hand holding up a
|
|
196
|
+
phone, and the resulting number wandered 30–80% on a live face while a phone
|
|
197
|
+
photo scored about the same. Only five cues separated a real face from a phone,
|
|
198
|
+
and each is individually decisive — which makes averaging exactly the wrong
|
|
199
|
+
combination rule.
|
|
200
|
+
|
|
201
|
+
**Deny cues** — evidence of a spoof. Either one firing fails the scan outright
|
|
202
|
+
and overrides any confirmation that already happened. A spoof tell does not get
|
|
203
|
+
outvoted.
|
|
204
|
+
|
|
205
|
+
| Cue | Fires when |
|
|
206
|
+
|---|---|
|
|
207
|
+
| Gloss/glare | One big flat specular blob (glass) rather than skin's small scattered shine |
|
|
208
|
+
| Device detected | A device-shaped rectangle overlaps the face |
|
|
209
|
+
|
|
210
|
+
**Confirm cues** — evidence of a real face. Any one is enough, and their
|
|
211
|
+
*absence is never a failure*: a live person can sit still and not blink for a
|
|
212
|
+
whole scan.
|
|
213
|
+
|
|
214
|
+
| Cue | Fires when |
|
|
215
|
+
|---|---|
|
|
216
|
+
| Flat vs 3D | Held-out nose points miss the best-fit homography — the face has depth |
|
|
217
|
+
| Depth/pose | Nose offset tracks head yaw at a magnitude only a real nose produces |
|
|
218
|
+
| Blink | Eye aspect ratio dipped and recovered |
|
|
219
|
+
|
|
220
|
+
`Light` runs the deny cues only — "confirmed unless proven wrong", which never
|
|
221
|
+
blocks a user who happens to sit still. `Heavy` also requires a confirm cue, and
|
|
222
|
+
can genuinely fail to unlock a motionless, unblinking live user. That cost is
|
|
223
|
+
pinned in a test so it is never mistaken for a regression.
|
|
224
|
+
|
|
225
|
+
## A bug found while porting
|
|
226
|
+
|
|
227
|
+
The depth/pose cue upstream gates on the Pearson correlation between nose offset
|
|
228
|
+
and `tan(yaw)`, documented as *"a genuinely positive relationship of the kind
|
|
229
|
+
only a nose sitting off the eye plane produces."*
|
|
230
|
+
|
|
231
|
+
That is not true of a plane viewed in perspective. Perspective projection does
|
|
232
|
+
not preserve midpoints, so a tilted photo's apparent eye midpoint does shift
|
|
233
|
+
relative to its nose. The shift is tiny — but correlation is scale-free and
|
|
234
|
+
cannot tell a tiny systematic drift from a large one. Measured against
|
|
235
|
+
`tests/synthetic.py`, a flat photo scores **1.00** at zero landmark noise and
|
|
236
|
+
**0.95** at 0.25px, both well over the 0.8 fire threshold. It only drops below
|
|
237
|
+
the threshold around 1px of jitter: the cue was relying on landmark noise to
|
|
238
|
+
hide the artifact.
|
|
239
|
+
|
|
240
|
+
The fix is a magnitude gate on the regression slope, which is the physical
|
|
241
|
+
quantity the cue is actually reasoning about — the nose's depth as a fraction of
|
|
242
|
+
the interocular distance. It is ~0.24 for a real nose and ~0.026 for a plane,
|
|
243
|
+
stable across 0–1px of noise, so the gate sits at 0.08. See
|
|
244
|
+
`MIN_NOSE_DEPTH_RATIO` in `liveness/scoring.py` and the regression tests.
|
|
245
|
+
|
|
246
|
+
This affects the macOS app too. Its Heavy mode can confirm a printed photo that
|
|
247
|
+
shows no glare and no device edge, whenever landmark jitter is low.
|
|
248
|
+
|
|
249
|
+
## Deviations from upstream
|
|
250
|
+
|
|
251
|
+
Each is documented at its own site; the significant ones:
|
|
252
|
+
|
|
253
|
+
- **Device bezel detection** is rebuilt on OpenCV contours. Upstream uses
|
|
254
|
+
`VNDetectRectanglesRequest`, which has no Linux equivalent. Parameters carry
|
|
255
|
+
over unchanged, but `minimum_edge_support` is an invented analogue of Vision's
|
|
256
|
+
confidence and is the knob most likely to need retuning against real footage.
|
|
257
|
+
- **Landmark regions** come from MediaPipe FaceMesh index groups rather than
|
|
258
|
+
Vision's named regions. Cross-frame correspondence is *guaranteed* here, which
|
|
259
|
+
is strictly better than what upstream must defend against.
|
|
260
|
+
- **`MEDIAN_LINE` is narrowed** to the two midline points between the brow and
|
|
261
|
+
lip lines. The probe set requires points geometrically inside the fit hull, so
|
|
262
|
+
that leftover error reads as depth rather than extrapolation; Vision's
|
|
263
|
+
forehead-to-chin median line violates that.
|
|
264
|
+
- **Eye aspect ratio** stays the bounding-box ratio even though MediaPipe would
|
|
265
|
+
support the classic 6-point formula, because the 0.65 dip and 0.7 recovery
|
|
266
|
+
thresholds were tuned against this definition.
|
|
267
|
+
- **No password, no keystroke injection** — see above.
|
|
268
|
+
|
|
269
|
+
## Architecture
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
src/glanced/ glanced camera -> landmarks -> {ArcFace embed, liveness} -> verdict
|
|
273
|
+
camera -> landmarks -> head pose (attention.sock)
|
|
274
|
+
pam/ pam_glance.so talks to the daemon over a 0600 unix socket
|
|
275
|
+
plugin/ Omarchy QML bar widget + panel: status, arm/disarm, test scan
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Unlock lives in PAM, in `hyprlock`'s stack, and works whether or not the shell
|
|
279
|
+
is running. The plugin is a thin client over a **separate, lower-privilege
|
|
280
|
+
status socket** and is presentation only — it can never cause or influence an
|
|
281
|
+
unlock. Separate sockets rather than one with a role field, so a compromised
|
|
282
|
+
shell plugin cannot reach the auth verb at all. The attention socket is the
|
|
283
|
+
same idea one step further: it has no verbs at all.
|
|
284
|
+
|
|
285
|
+
> When wiring `pam_glance`, keep a root TTY open. A broken PAM stack locks you
|
|
286
|
+
> out of your own machine.
|
|
287
|
+
|
|
288
|
+
## Credit
|
|
289
|
+
|
|
290
|
+
- [Glance](https://github.com/jonnyoo/glance) — the liveness model and its
|
|
291
|
+
tuning, MIT © Jonathan Zhou. See `NOTICE`.
|
|
292
|
+
- [InsightFace](https://github.com/deepinsight/insightface) — the ArcFace model.
|
|
293
|
+
|
|
294
|
+
## License
|
|
295
|
+
|
|
296
|
+
MIT
|