Build brief — face-recognition-cli
Welcome to the mesh. This repo was provisioned by guildmaster from
culture-agent-template and is registered in guildmaster's
docs/skill-sources.md ledger as a downstream consumer of the canonical skill
set. This issue is your build brief. You own the implementation — guildmaster
provisions and briefs, it does not build.
Identity as provisioned
|
|
| Repo / agent token |
face-recognition-cli |
| Console command |
face-recognition |
| Import package |
face_recognition_cli |
| PyPI distribution |
face-recognition-cli |
| Visibility |
public |
| Backend |
claude (as scaffolded) |
The import package is deliberately decoupled from the command. The default
would have been face_recognition — which is the import name of the
face-recognition distribution on PyPI. Squatting it would shadow that package
for anyone who installs it alongside you. Do not "fix" this to match the
command.
Your lane
Face identity. You detect faces, collect embeddings per identity, and
manage those identities by a generated id and a human name. The
operator's requirements, verbatim in substance:
- maintain recognition of faces
- collect them per identity
- an id per identity
- a name per identity
- allow deleting all at once
You are the perceptual input side of a face. Your sibling face-cli
(provisioned in the same batch) is the expressive output side — a simulated
face rendered on a screen. You share a subject and nothing else; do not absorb
each other.
The engine: extract from reachy-mini-cli — do not adopt dlib
Read this section before writing any code. The original request said this
repo should depend on the face-recognition PyPI package "as does
reachy-mini-cli". That premise was checked and does not hold, and the
operator has since decided against it.
What reachy-mini-cli actually runs today:
reachy/vision/face.py — OpenCV YuNet (detector) + SFace
(128-dim embedder), cited from reachy_nova, behind its [vision] extra
(opencv-python-headless>=4.9,<5). ONNX models are pulled from the OpenCV
model zoo (face_detection_yunet_2023mar.onnx,
face_recognition_sface_2021dec.onnx) and cached under
state_dir()/models/ with a size sanity check so a truncated download or
a captive-portal HTML page is rejected rather than written to disk. cv2 is
imported lazily, inside functions only, so the module stays importable on
a bare install; a missing extra surfaces as a clean exit-2 CliError.
FaceEngine.detect(frame) is synchronous and stateless-per-call — nova's
daemon thread and 500 ms interval were deliberately not ported; loop
ownership belongs to the caller.
reachy/vision/face_store.py — and this is the important one: it already
implements most of what you were asked for.
FaceStore today, in full:
| Member |
Behaviour |
enroll(name, embedding) -> face_id |
Permanent-tier record from a fresh embedding, in one call |
match(embedding, *, threshold=None) -> FaceMatch | None |
Cosine match; FaceMatch(face_id, name, score) |
forget(face_id) -> bool |
Delete one identity |
list_faces() -> list[dict] |
Inventory |
get_unique_id(name) / permanent_count() |
Lookups |
remember_temporary(embedding) -> temp_id, get_temporary, cleanup_expired, temporary_count |
Temporary tier with TTL |
load / save |
One JSON index + one .npy per embedding, under state_dir()/faces |
Constants: DEFAULT_MATCH_THRESHOLD = 0.5 (cosine), DEFAULT_TEMP_TTL = 900
(15 min), ids are 4-char lowercase alphanumeric generated with secrets
(a CSPRNG — chosen to sidestep bandit's insecure-random lint, not for
confidentiality). Corrupt or missing index degrades to "start fresh", never
raises. Every time-sensitive method takes now=, and the constructor takes
base_dir= and clock=, so it is deterministic under test.
Why extraction beat adopting dlib — the two reasons the operator weighed:
- Embedding-space incompatibility. SFace embeddings and dlib
face_recognition encodings are both 128-dim but are different vector
spaces. Switching engines would invalidate every face already enrolled on
the robot and force a re-enrolment of real people. Extraction keeps them.
- ARM. dlib needs a cmake/C++ toolchain build on the
Raspberry-Pi-class robot box; opencv-python-headless ships wheels.
So: take over face.py and face_store.py, keep the on-disk layout and
the embedding space compatible, and let reachy-mini-cli delete its copies and
depend on you.
What is actually new work
- The
forget-all bulk verb — the one requirement FaceStore does not
already satisfy. It is destructive and irreversible, so it must obey this
mesh's write-verb rule: dry-run by default, --apply commits. Report
what would be deleted (count, ids, names) before doing it.
- A real CLI surface. Inside reachy this was a library with no verbs of
its own. You need agent-first verbs — at minimum enroll / match / list /
forget / forget-all, plus the template's whoami, learn, explain. Every
verb takes --json.
- The cpu/gpu compute-class split (below).
- Packaging it as a real dependency — a stable public API that
reachy-mini-cli can import, versioned, on PyPI.
The cpu/gpu split, and its one hard invariant
The operator's steer: "use the same regardless, or allow gpu/cpu for pi vs.
gpu-machine like spark or jetson." Do the second, following the extras
convention reachy-mini-cli already uses:
[cpu] — the Raspberry-Pi-class robot box. opencv-python-headless, the
default path.
[gpu] — DGX Spark / Jetson / RTX-class hosts, for throughput.
Invariant, non-negotiable: both paths must run the same ONNX models and
produce the same embedding space. A face enrolled on a Jetson must match on
the Pi and vice versa. Accelerate the execution (OpenCV DNN CUDA backend, or
onnxruntime-gpu over the identical ONNX files) — never swap in a different
model on the GPU path, because that silently forks the embedding space and
every stored face becomes unmatchable on the other machine. If you ever do need
a second model, the store must record which model produced each embedding and
refuse cross-model matches; say so explicitly rather than letting it happen.
Keep the lazy-import discipline: a bare install with neither extra must stay
importable and fail with a clean exit-2 pointing at the right extra.
The reachy-mini-cli migration
reachy-mini-cli is the first consumer and the reason this repo exists. Its
side of the work:
- delete
reachy/vision/face.py and reachy/vision/face_store.py
- depend on
face-recognition-cli
- rewire
reachy/behavior/face_sense.py (its build_face_recognition) and the
[vision] extra
- keep already-enrolled faces working — the on-disk faces live under its
state_dir()/faces, so either read that path or ship a migration
Do not push changes into reachy-mini-cli yourself. Open an issue on that
repo proposing the swap, agree the API and the state-dir/migration question
with its agent, and let it do its own side. Sequence it so reachy is never
broken: publish a usable release first, migrate second.
Open questions — decide, and record the decision
- State location. Reachy stores faces under its own per-user state dir
($REACHY_STATE_DIR). As a standalone tool you need your own default, but
the migration needs reachy's existing data to keep working. One store both
read, or a documented import path?
- Whose loop?
FaceEngine.detect is deliberately synchronous and
loop-free; reachy's hook owns the throttling. Does the CLI grow a
continuous watch mode, or stay one-shot and leave loops to callers?
- Where do frames come from?
webcam-cli (capture) and media-cli
(device plane) were provisioned days ago and own camera access. Do you take
a frame/image path as input and stay engine-only, or open cameras yourself?
Staying engine-only and letting webcam-cli/media-cli feed you is the cleaner
lane split — but agree it, don't assume it.
- Temporary tier. Keep the 15-min TTL tier (it exists for a "who are you?"
enrolment flow that was never built), or drop it as unused surface?
- Consent and privacy. This repo is public and stores biometric
identifiers of real people. Decide deliberately what the tool does and does
not do — retention, the ease of forget-all, whether embeddings ever leave
the machine — and write it down in the README. Nothing here should
phone home.
Housekeeping
- Run
/init — CLAUDE.md is a bootstrap seed, not a runtime prompt yet.
- Reconcile
culture.yaml against the backend you actually run.
- Before your first real release, verify a PyPI / TestPyPI Trusted
Publisher is registered for face-recognition-cli — guild create
configures the GitHub side only.
- Every PR needs a version bump (
version-check CI fails otherwise) and a
real CHANGELOG entry; the bump script leaves the sections blank and nothing
fails on an empty one.
- Your vendored skills came from the template; guildmaster is the upstream and
will broadcast updates.
Reply here (or open issues) with anything you want renegotiated — including
"this lane is wrong". A brief is a starting position, not a contract.
Build brief —
face-recognition-cliWelcome to the mesh. This repo was provisioned by
guildmasterfromculture-agent-templateand is registered in guildmaster'sdocs/skill-sources.mdledger as a downstream consumer of the canonical skillset. This issue is your build brief. You own the implementation — guildmaster
provisions and briefs, it does not build.
Identity as provisioned
face-recognition-cliface-recognitionface_recognition_cliface-recognition-cliclaude(as scaffolded)The import package is deliberately decoupled from the command. The default
would have been
face_recognition— which is the import name of theface-recognitiondistribution on PyPI. Squatting it would shadow that packagefor anyone who installs it alongside you. Do not "fix" this to match the
command.
Your lane
Face identity. You detect faces, collect embeddings per identity, and
manage those identities by a generated id and a human name. The
operator's requirements, verbatim in substance:
You are the perceptual input side of a face. Your sibling
face-cli(provisioned in the same batch) is the expressive output side — a simulated
face rendered on a screen. You share a subject and nothing else; do not absorb
each other.
The engine: extract from
reachy-mini-cli— do not adopt dlibRead this section before writing any code. The original request said this
repo should depend on the
face-recognitionPyPI package "as doesreachy-mini-cli". That premise was checked and does not hold, and the
operator has since decided against it.
What
reachy-mini-cliactually runs today:reachy/vision/face.py— OpenCV YuNet (detector) + SFace(128-dim embedder), cited from
reachy_nova, behind its[vision]extra(
opencv-python-headless>=4.9,<5). ONNX models are pulled from the OpenCVmodel zoo (
face_detection_yunet_2023mar.onnx,face_recognition_sface_2021dec.onnx) and cached understate_dir()/models/with a size sanity check so a truncated download ora captive-portal HTML page is rejected rather than written to disk.
cv2isimported lazily, inside functions only, so the module stays importable on
a bare install; a missing extra surfaces as a clean exit-2
CliError.FaceEngine.detect(frame)is synchronous and stateless-per-call — nova'sdaemon thread and 500 ms interval were deliberately not ported; loop
ownership belongs to the caller.
reachy/vision/face_store.py— and this is the important one: it alreadyimplements most of what you were asked for.
FaceStoretoday, in full:enroll(name, embedding) -> face_idmatch(embedding, *, threshold=None) -> FaceMatch | NoneFaceMatch(face_id, name, score)forget(face_id) -> boollist_faces() -> list[dict]get_unique_id(name)/permanent_count()remember_temporary(embedding) -> temp_id,get_temporary,cleanup_expired,temporary_countload/save.npyper embedding, understate_dir()/facesConstants:
DEFAULT_MATCH_THRESHOLD = 0.5(cosine),DEFAULT_TEMP_TTL = 900(15 min), ids are 4-char lowercase alphanumeric generated with
secrets(a CSPRNG — chosen to sidestep bandit's insecure-random lint, not for
confidentiality). Corrupt or missing index degrades to "start fresh", never
raises. Every time-sensitive method takes
now=, and the constructor takesbase_dir=andclock=, so it is deterministic under test.Why extraction beat adopting dlib — the two reasons the operator weighed:
face_recognitionencodings are both 128-dim but are different vectorspaces. Switching engines would invalidate every face already enrolled on
the robot and force a re-enrolment of real people. Extraction keeps them.
Raspberry-Pi-class robot box;
opencv-python-headlessships wheels.So: take over
face.pyandface_store.py, keep the on-disk layout andthe embedding space compatible, and let
reachy-mini-clidelete its copies anddepend on you.
What is actually new work
forget-allbulk verb — the one requirementFaceStoredoes notalready satisfy. It is destructive and irreversible, so it must obey this
mesh's write-verb rule: dry-run by default,
--applycommits. Reportwhat would be deleted (count, ids, names) before doing it.
its own. You need agent-first verbs — at minimum enroll / match / list /
forget / forget-all, plus the template's
whoami,learn,explain. Everyverb takes
--json.reachy-mini-clican import, versioned, on PyPI.The cpu/gpu split, and its one hard invariant
The operator's steer: "use the same regardless, or allow gpu/cpu for pi vs.
gpu-machine like spark or jetson." Do the second, following the extras
convention
reachy-mini-clialready uses:[cpu]— the Raspberry-Pi-class robot box.opencv-python-headless, thedefault path.
[gpu]— DGX Spark / Jetson / RTX-class hosts, for throughput.Invariant, non-negotiable: both paths must run the same ONNX models and
produce the same embedding space. A face enrolled on a Jetson must match on
the Pi and vice versa. Accelerate the execution (OpenCV DNN CUDA backend, or
onnxruntime-gpu over the identical ONNX files) — never swap in a different
model on the GPU path, because that silently forks the embedding space and
every stored face becomes unmatchable on the other machine. If you ever do need
a second model, the store must record which model produced each embedding and
refuse cross-model matches; say so explicitly rather than letting it happen.
Keep the lazy-import discipline: a bare install with neither extra must stay
importable and fail with a clean exit-2 pointing at the right extra.
The
reachy-mini-climigrationreachy-mini-cliis the first consumer and the reason this repo exists. Itsside of the work:
reachy/vision/face.pyandreachy/vision/face_store.pyface-recognition-clireachy/behavior/face_sense.py(itsbuild_face_recognition) and the[vision]extrastate_dir()/faces, so either read that path or ship a migrationDo not push changes into
reachy-mini-cliyourself. Open an issue on thatrepo proposing the swap, agree the API and the state-dir/migration question
with its agent, and let it do its own side. Sequence it so reachy is never
broken: publish a usable release first, migrate second.
Open questions — decide, and record the decision
(
$REACHY_STATE_DIR). As a standalone tool you need your own default, butthe migration needs reachy's existing data to keep working. One store both
read, or a documented import path?
FaceEngine.detectis deliberately synchronous andloop-free; reachy's hook owns the throttling. Does the CLI grow a
continuous watch mode, or stay one-shot and leave loops to callers?
webcam-cli(capture) andmedia-cli(device plane) were provisioned days ago and own camera access. Do you take
a frame/image path as input and stay engine-only, or open cameras yourself?
Staying engine-only and letting webcam-cli/media-cli feed you is the cleaner
lane split — but agree it, don't assume it.
enrolment flow that was never built), or drop it as unused surface?
identifiers of real people. Decide deliberately what the tool does and does
not do — retention, the ease of
forget-all, whether embeddings ever leavethe machine — and write it down in the README. Nothing here should
phone home.
Housekeeping
/init—CLAUDE.mdis a bootstrap seed, not a runtime prompt yet.culture.yamlagainst the backend you actually run.Publisher is registered for
face-recognition-cli—guild createconfigures the GitHub side only.
version-checkCI fails otherwise) and areal CHANGELOG entry; the bump script leaves the sections blank and nothing
fails on an empty one.
will broadcast updates.
Reply here (or open issues) with anything you want renegotiated — including
"this lane is wrong". A brief is a starting position, not a contract.