Skip to content

Commit 0043013

Browse files
committed
v0.3.2: configurable suggest_threshold (+ add-on option), jump-to-person dropdown on the Persons tab
1 parent 9054483 commit 0043013

9 files changed

Lines changed: 46 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
All notable changes to FaceID. The Home Assistant add-on shows this file in the
44
update dialog; standalone users can watch GitHub releases.
55

6+
## 0.3.2 — 2026-07-23
7+
8+
- **Configurable suggestion threshold** (`suggest_threshold`, default 0.40): controls
9+
when an unknown face is grouped into a "Looks like <person>" suggestion. Available as
10+
an add-on option too.
11+
- **Jump-to-person dropdown** on the Persons tab (shown once you have more than a few
12+
people) — pick a name to scroll straight to that person.
13+
614
## 0.3.1 — 2026-07-23
715

816
- **Smarter review queue**: unknown faces that resemble an enrolled person are now

app/webui.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ def recent():
256256
@app.get("/api/health")
257257
def health():
258258
return {"status": "ok", "persons": len(gallery.persons()),
259-
"queue": processor.queue.qsize(), "open_events": len(processor.events)}
259+
"queue": processor.queue.qsize(), "open_events": len(processor.events),
260+
"suggest_threshold": float(cfg["faceid"].get("suggest_threshold", 0.40))}
260261

261262
return app

docs/example-config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ faceid:
2525
max_attempts: 6 # recognition attempts per Frigate event
2626
retry_seconds: 2.5 # min. seconds between attempts on the same event
2727
cluster_eps: 0.55 # DBSCAN cosine distance for grouping unknowns in the UI
28+
# review UI: unknown faces with a best-match score >= this get grouped into a
29+
# "Looks like <person>" suggestion with the dropdown pre-selected. Keep below
30+
# match_threshold so borderline faces are pre-sorted without weak-match noise.
31+
suggest_threshold: 0.40
2832
set_sub_label: true # write recognized names back to Frigate events
2933
presence_window: 120 # camera sensor shows everyone seen within this window (s)
3034
# process events only from these cameras (empty = all cameras)

faceid-addon/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
All notable changes to FaceID. The Home Assistant add-on shows this file in the
44
update dialog; standalone users can watch GitHub releases.
55

6+
## 0.3.2 — 2026-07-23
7+
8+
- **Configurable suggestion threshold** (`suggest_threshold`, default 0.40): controls
9+
when an unknown face is grouped into a "Looks like <person>" suggestion. Available as
10+
an add-on option too.
11+
- **Jump-to-person dropdown** on the Persons tab (shown once you have more than a few
12+
people) — pick a name to scroll straight to that person.
13+
614
## 0.3.1 — 2026-07-23
715

816
- **Smarter review queue**: unknown faces that resemble an enrolled person are now

faceid-addon/app/webui.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ def recent():
256256
@app.get("/api/health")
257257
def health():
258258
return {"status": "ok", "persons": len(gallery.persons()),
259-
"queue": processor.queue.qsize(), "open_events": len(processor.events)}
259+
"queue": processor.queue.qsize(), "open_events": len(processor.events),
260+
"suggest_threshold": float(cfg["faceid"].get("suggest_threshold", 0.40))}
260261

261262
return app

faceid-addon/config.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: FaceID
2-
version: "0.3.1"
2+
version: "0.3.2"
33
slug: faceid
44
description: Face recognition for Frigate — trainable gallery, clustered unknown review, HA sensors
55
url: https://github.com/SkyTechNerds/faceid
@@ -30,6 +30,7 @@ options:
3030
match_threshold: 0.5
3131
unknown_threshold: 0.35
3232
cluster_eps: 0.55
33+
suggest_threshold: 0.40
3334
presence_window: 120
3435
set_sub_label: true
3536
cameras: []
@@ -44,6 +45,7 @@ schema:
4445
match_threshold: float(0.2,0.9)
4546
unknown_threshold: float(0.1,0.8)
4647
cluster_eps: float(0.3,0.8)
48+
suggest_threshold: float(0.1,0.9)
4749
presence_window: int(10,3600)
4850
set_sub_label: bool
4951
cameras:

faceid-addon/run.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ faceid:
5959
match_threshold: $(cfg '.match_threshold')
6060
unknown_threshold: $(cfg '.unknown_threshold')
6161
cluster_eps: $(cfg '.cluster_eps')
62+
suggest_threshold: $(cfg '.suggest_threshold')
6263
presence_window: $(cfg '.presence_window')
6364
set_sub_label: $(cfg '.set_sub_label')
6465
min_face_px: 48

faceid-addon/static/index.html

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ <h1>FACEID</h1>
8787

8888
<script>
8989
const $=s=>document.querySelector(s), main=$('#main');
90-
let tab='unknowns', persons={}, selected=new Set();
90+
let tab='unknowns', persons={}, selected=new Set(), suggestThr=0.40;
9191

9292
async function j(url,opt){const r=await fetch(url,opt);if(!r.ok)throw new Error(await r.text());return r.json()}
9393
function toast(m){const t=$('#toast');t.textContent=m;t.style.display='block';setTimeout(()=>t.style.display='none',2500)}
@@ -100,6 +100,7 @@ <h1>FACEID</h1>
100100
render()});
101101

102102
async function health(){try{const h=await j('api/health');
103+
if(typeof h.suggest_threshold==='number')suggestThr=h.suggest_threshold;
103104
$('#health').textContent=`${h.persons} persons · queue ${h.queue} · events ${h.open_events}`}catch(e){$('#health').textContent='offline'}}
104105

105106
function personOptions(sel){
@@ -157,7 +158,7 @@ <h1>FACEID</h1>
157158
main.innerHTML=backfillRow(bf)+'<div class="empty">No unknown faces in the queue.</div>';return}
158159
const excluded=window._excludedIds||(window._excludedIds=new Set());
159160
const n2s={}; Object.entries(persons).forEach(([sl,p])=>{n2s[p.name]=sl;});
160-
const SUGGEST=0.40;
161+
const SUGGEST=suggestThr;
161162
// Vorschlags-Gruppen (nach vermuteter Person) + Rest-Cluster
162163
const byName={};
163164
const rest=[];
@@ -358,7 +359,7 @@ <h1>FACEID</h1>
358359

359360
function personCard(slug,p){
360361
return `
361-
<div class="person">
362+
<div class="person" id="p-${slug}">
362363
<div class="head">
363364
<button class="fav${p.favorite?' on':''}" title="${p.favorite?'remove from favorites':'mark as favorite'}" onclick="toggleFav('${slug}',${p.favorite?0:1})">${p.favorite?'★':'☆'}</button>
364365
<b>${esc(p.name)}</b><span class="n">${p.count} faces</span>
@@ -387,10 +388,15 @@ <h1>FACEID</h1>
387388
if(favs.length) body+=`<h2>★ Favorites (${favs.length})</h2>`+favs.map(([s,p])=>personCard(s,p)).join('');
388389
if(rest.length) body+=`<h2>${favs.length?'Others':'Persons'} (${rest.length})</h2>`+rest.map(([s,p])=>personCard(s,p)).join('');
389390
if(!entries.length) body='<div class="empty">No persons yet.</div>';
391+
const jump=entries.length>3?`
392+
<select id="pjump" title="jump to a person" onchange="if(this.value){document.getElementById('p-'+this.value).scrollIntoView({behavior:'smooth',block:'start'});this.value='';}">
393+
<option value="">Jump to…</option>${personOptions('')}
394+
</select>`:'';
390395
main.innerHTML=`
391396
<div class="row">
392397
<input id="pname" placeholder="New person…" size="18" title="create an empty person, then upload photos or assign faces from the unknown queue">
393398
<button class="act" onclick="createPerson()">CREATE</button>
399+
<span style="flex:1"></span>${jump}
394400
</div>`+body;
395401
}
396402
async function toggleFav(slug,fav){

static/index.html

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ <h1>FACEID</h1>
8787

8888
<script>
8989
const $=s=>document.querySelector(s), main=$('#main');
90-
let tab='unknowns', persons={}, selected=new Set();
90+
let tab='unknowns', persons={}, selected=new Set(), suggestThr=0.40;
9191

9292
async function j(url,opt){const r=await fetch(url,opt);if(!r.ok)throw new Error(await r.text());return r.json()}
9393
function toast(m){const t=$('#toast');t.textContent=m;t.style.display='block';setTimeout(()=>t.style.display='none',2500)}
@@ -100,6 +100,7 @@ <h1>FACEID</h1>
100100
render()});
101101

102102
async function health(){try{const h=await j('api/health');
103+
if(typeof h.suggest_threshold==='number')suggestThr=h.suggest_threshold;
103104
$('#health').textContent=`${h.persons} persons · queue ${h.queue} · events ${h.open_events}`}catch(e){$('#health').textContent='offline'}}
104105

105106
function personOptions(sel){
@@ -157,7 +158,7 @@ <h1>FACEID</h1>
157158
main.innerHTML=backfillRow(bf)+'<div class="empty">No unknown faces in the queue.</div>';return}
158159
const excluded=window._excludedIds||(window._excludedIds=new Set());
159160
const n2s={}; Object.entries(persons).forEach(([sl,p])=>{n2s[p.name]=sl;});
160-
const SUGGEST=0.40;
161+
const SUGGEST=suggestThr;
161162
// Vorschlags-Gruppen (nach vermuteter Person) + Rest-Cluster
162163
const byName={};
163164
const rest=[];
@@ -358,7 +359,7 @@ <h1>FACEID</h1>
358359

359360
function personCard(slug,p){
360361
return `
361-
<div class="person">
362+
<div class="person" id="p-${slug}">
362363
<div class="head">
363364
<button class="fav${p.favorite?' on':''}" title="${p.favorite?'remove from favorites':'mark as favorite'}" onclick="toggleFav('${slug}',${p.favorite?0:1})">${p.favorite?'★':'☆'}</button>
364365
<b>${esc(p.name)}</b><span class="n">${p.count} faces</span>
@@ -387,10 +388,15 @@ <h1>FACEID</h1>
387388
if(favs.length) body+=`<h2>★ Favorites (${favs.length})</h2>`+favs.map(([s,p])=>personCard(s,p)).join('');
388389
if(rest.length) body+=`<h2>${favs.length?'Others':'Persons'} (${rest.length})</h2>`+rest.map(([s,p])=>personCard(s,p)).join('');
389390
if(!entries.length) body='<div class="empty">No persons yet.</div>';
391+
const jump=entries.length>3?`
392+
<select id="pjump" title="jump to a person" onchange="if(this.value){document.getElementById('p-'+this.value).scrollIntoView({behavior:'smooth',block:'start'});this.value='';}">
393+
<option value="">Jump to…</option>${personOptions('')}
394+
</select>`:'';
390395
main.innerHTML=`
391396
<div class="row">
392397
<input id="pname" placeholder="New person…" size="18" title="create an empty person, then upload photos or assign faces from the unknown queue">
393398
<button class="act" onclick="createPerson()">CREATE</button>
399+
<span style="flex:1"></span>${jump}
394400
</div>`+body;
395401
}
396402
async function toggleFav(slug,fav){

0 commit comments

Comments
 (0)