Spaces:
Running
Running
Oliver Nitsche Claude Sonnet 4.6 commited on
Commit Β·
ee7b214
1
Parent(s): 897c12b
Add GREETING state so recognised faces are shown in GUI and spoken
Browse filesPreviously, a matched face triggered speak() β goto_sleep() inline
inside the ACTIVE state, so the GUI state jumped straight to "sleeping"
and the user saw nothing.
Changes:
- New State.GREETING: entered as soon as a face is matched; sets
_shared["recognized_name"] so the GUI shows the name immediately,
before speak() is even called. If TTS is unavailable the name is
still visible in the control panel.
- /status now returns recognized_name alongside state.
- GUI: new green greeting-section div shows "Welcome back, <name>!"
when state is "greeting"; hidden in all other states.
- STATE_LABELS updated with greeting entry.
- Greeting section styled in style.css (green card).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- recognizer/main.py +24 -5
- recognizer/static/index.html +4 -0
- recognizer/static/main.js +11 -0
- recognizer/static/style.css +11 -0
recognizer/main.py
CHANGED
|
@@ -32,6 +32,7 @@ class State(Enum):
|
|
| 32 |
SLEEPING = auto()
|
| 33 |
WAKING = auto()
|
| 34 |
ACTIVE = auto()
|
|
|
|
| 35 |
ENROLLING = auto()
|
| 36 |
|
| 37 |
|
|
@@ -44,7 +45,8 @@ class Recognizer(ReachyMiniApp):
|
|
| 44 |
_lock = threading.Lock()
|
| 45 |
_shared: dict = {
|
| 46 |
"state": "sleeping",
|
| 47 |
-
"pending_name": None,
|
|
|
|
| 48 |
}
|
| 49 |
|
| 50 |
# --- Settings-app REST endpoints ---
|
|
@@ -61,7 +63,10 @@ class Recognizer(ReachyMiniApp):
|
|
| 61 |
@self.settings_app.get("/status")
|
| 62 |
def get_status():
|
| 63 |
with _lock:
|
| 64 |
-
return {
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
# --- Initialise ---
|
| 67 |
face_db = load_face_db()
|
|
@@ -71,6 +76,7 @@ class Recognizer(ReachyMiniApp):
|
|
| 71 |
active_start = 0.0
|
| 72 |
last_face_check = 0.0
|
| 73 |
enrollment_frames: list[np.ndarray] = []
|
|
|
|
| 74 |
scan_t0 = 0.0 # reference time for head-scan idle animation
|
| 75 |
|
| 76 |
reachy_mini.goto_sleep()
|
|
@@ -134,9 +140,10 @@ class Recognizer(ReachyMiniApp):
|
|
| 134 |
try:
|
| 135 |
name = find_match(frame, face_db)
|
| 136 |
if name:
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
|
|
|
| 140 |
else:
|
| 141 |
speak(
|
| 142 |
"I don't know you yet. "
|
|
@@ -157,6 +164,18 @@ class Recognizer(ReachyMiniApp):
|
|
| 157 |
|
| 158 |
time.sleep(0.05)
|
| 159 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
# ---------- ENROLLING ----------
|
| 161 |
elif state == State.ENROLLING:
|
| 162 |
with _lock:
|
|
|
|
| 32 |
SLEEPING = auto()
|
| 33 |
WAKING = auto()
|
| 34 |
ACTIVE = auto()
|
| 35 |
+
GREETING = auto() # known face found β show name in GUI, speak, then sleep
|
| 36 |
ENROLLING = auto()
|
| 37 |
|
| 38 |
|
|
|
|
| 45 |
_lock = threading.Lock()
|
| 46 |
_shared: dict = {
|
| 47 |
"state": "sleeping",
|
| 48 |
+
"pending_name": None, # set by /set_name when ENROLLING
|
| 49 |
+
"recognized_name": None, # set when a known face is found
|
| 50 |
}
|
| 51 |
|
| 52 |
# --- Settings-app REST endpoints ---
|
|
|
|
| 63 |
@self.settings_app.get("/status")
|
| 64 |
def get_status():
|
| 65 |
with _lock:
|
| 66 |
+
return {
|
| 67 |
+
"state": _shared["state"],
|
| 68 |
+
"recognized_name": _shared["recognized_name"],
|
| 69 |
+
}
|
| 70 |
|
| 71 |
# --- Initialise ---
|
| 72 |
face_db = load_face_db()
|
|
|
|
| 76 |
active_start = 0.0
|
| 77 |
last_face_check = 0.0
|
| 78 |
enrollment_frames: list[np.ndarray] = []
|
| 79 |
+
greeting_name = ""
|
| 80 |
scan_t0 = 0.0 # reference time for head-scan idle animation
|
| 81 |
|
| 82 |
reachy_mini.goto_sleep()
|
|
|
|
| 140 |
try:
|
| 141 |
name = find_match(frame, face_db)
|
| 142 |
if name:
|
| 143 |
+
greeting_name = name
|
| 144 |
+
with _lock:
|
| 145 |
+
_shared["recognized_name"] = name
|
| 146 |
+
state = State.GREETING
|
| 147 |
else:
|
| 148 |
speak(
|
| 149 |
"I don't know you yet. "
|
|
|
|
| 164 |
|
| 165 |
time.sleep(0.05)
|
| 166 |
|
| 167 |
+
# ---------- GREETING ----------
|
| 168 |
+
elif state == State.GREETING:
|
| 169 |
+
with _lock:
|
| 170 |
+
_shared["state"] = "greeting"
|
| 171 |
+
# Speak first; GUI already shows the name via /status.
|
| 172 |
+
# If TTS is unavailable the name remains visible in the UI.
|
| 173 |
+
speak(f"Hi {greeting_name}!", reachy_mini)
|
| 174 |
+
reachy_mini.goto_sleep()
|
| 175 |
+
with _lock:
|
| 176 |
+
_shared["recognized_name"] = None
|
| 177 |
+
state = State.SLEEPING
|
| 178 |
+
|
| 179 |
# ---------- ENROLLING ----------
|
| 180 |
elif state == State.ENROLLING:
|
| 181 |
with _lock:
|
recognizer/static/index.html
CHANGED
|
@@ -15,6 +15,10 @@
|
|
| 15 |
State: <strong id="state-label">β</strong>
|
| 16 |
</div>
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
<div id="enroll-section" style="display:none;">
|
| 19 |
<p>A new face was detected. Enter the person's name:</p>
|
| 20 |
<div id="enroll-form">
|
|
|
|
| 15 |
State: <strong id="state-label">β</strong>
|
| 16 |
</div>
|
| 17 |
|
| 18 |
+
<div id="greeting-section" style="display:none;">
|
| 19 |
+
<p>Welcome back, <strong id="greeting-name"></strong>! π</p>
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
<div id="enroll-section" style="display:none;">
|
| 23 |
<p>A new face was detected. Enter the person's name:</p>
|
| 24 |
<div id="enroll-form">
|
recognizer/static/main.js
CHANGED
|
@@ -2,6 +2,7 @@ const STATE_LABELS = {
|
|
| 2 |
sleeping: "π΄ Sleeping β listening for noise",
|
| 3 |
waking: "β‘ Waking upβ¦",
|
| 4 |
active: "π Active β looking for faces",
|
|
|
|
| 5 |
enrolling: "π New face detected β waiting for name",
|
| 6 |
};
|
| 7 |
|
|
@@ -19,6 +20,16 @@ async function pollStatus() {
|
|
| 19 |
const label = document.getElementById("state-label");
|
| 20 |
label.textContent = STATE_LABELS[newState] ?? newState;
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
const enrollSection = document.getElementById("enroll-section");
|
| 23 |
enrollSection.style.display = newState === "enrolling" ? "block" : "none";
|
| 24 |
|
|
|
|
| 2 |
sleeping: "π΄ Sleeping β listening for noise",
|
| 3 |
waking: "β‘ Waking upβ¦",
|
| 4 |
active: "π Active β looking for faces",
|
| 5 |
+
greeting: "π Face recognised!",
|
| 6 |
enrolling: "π New face detected β waiting for name",
|
| 7 |
};
|
| 8 |
|
|
|
|
| 20 |
const label = document.getElementById("state-label");
|
| 21 |
label.textContent = STATE_LABELS[newState] ?? newState;
|
| 22 |
|
| 23 |
+
// Greeting section: show recognised name
|
| 24 |
+
const greetingSection = document.getElementById("greeting-section");
|
| 25 |
+
if (newState === "greeting" && data.recognized_name) {
|
| 26 |
+
document.getElementById("greeting-name").textContent = data.recognized_name;
|
| 27 |
+
greetingSection.style.display = "block";
|
| 28 |
+
} else {
|
| 29 |
+
greetingSection.style.display = "none";
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
// Enroll section
|
| 33 |
const enrollSection = document.getElementById("enroll-section");
|
| 34 |
enrollSection.style.display = newState === "enrolling" ? "block" : "none";
|
| 35 |
|
recognizer/static/style.css
CHANGED
|
@@ -28,6 +28,17 @@ h1 {
|
|
| 28 |
color: #1a73e8;
|
| 29 |
}
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
#enroll-section {
|
| 32 |
background: #fff;
|
| 33 |
border: 1px solid #fbc02d;
|
|
|
|
| 28 |
color: #1a73e8;
|
| 29 |
}
|
| 30 |
|
| 31 |
+
#greeting-section {
|
| 32 |
+
background: #e8f5e9;
|
| 33 |
+
border: 1px solid #66bb6a;
|
| 34 |
+
border-radius: 8px;
|
| 35 |
+
padding: 1.25rem;
|
| 36 |
+
margin-bottom: 1.5rem;
|
| 37 |
+
font-size: 1.1rem;
|
| 38 |
+
font-weight: 500;
|
| 39 |
+
color: #1b5e20;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
#enroll-section {
|
| 43 |
background: #fff;
|
| 44 |
border: 1px solid #fbc02d;
|