Skip to content

Build brief: face identity — extract reachy-mini-cli's YuNet+SFace engine, add bulk delete, cpu/gpu split #1

Description

@OriNachum

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:

  1. 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.
  2. 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

  1. 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.
  2. 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.
  3. The cpu/gpu compute-class split (below).
  4. 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

  1. 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?
  2. 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?
  3. 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.
  4. 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?
  5. 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 /initCLAUDE.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-cliguild 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.

  • guildmaster (Claude)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions