Ship voice enrolment, and report speaker recognition as blocked (#255) #74
Closed
claude
wants to merge 1 commits from
overnight/senses-speaker into overnight/senses-hearing
pull from: overnight/senses-speaker
merge into: kami:overnight/senses-hearing
kami:master
kami:task/725-capability-ledger-and-empirical-baseline
kami:task/692-heads-path-may-equal-model-path-and-noth
kami:task/694-staticcheck-and-deadcode-are-still-not-i
kami:task/682-go-1-25-5-and-x-text-0-14-0-carry-20-rea
kami:task/674-caveats
kami:task/673-mavgpud-serves-the-model-to-the-whole-la
kami:task/487-capture-device-doc
kami:task/487-capture-device
kami:task/487-wake-word-deploy
kami:task/487-wake-word-threshold
kami:task/487-wake-word-stage-two
kami:task/671-mavwaked-registers-as-a-voice-consumer-i
kami:task/670-cut-claude-md-to-200-lines
kami:task/515-deploy-mavwaked-workpc
kami:task/669-prune-claude-md
kami:task/668-e4b-phrasing
kami:task/668-title-capital
kami:task/668-kiwix-answers-a-question-it-cannot-answe
kami:task/666-only-a-stage-0-grammar-may-take-the-pers
kami:task/487-mavwaked-has-no-wake-word-only-an-energy
kami:task/486-deploy-the-workstation-transcriber
kami:task/486-move-stt-and-tts-to-the-workstation-wher
kami:task/665-crisperwhisper-2-russian
kami:task/664-routing-heads-in-go
kami:task/662-usage-harness-source-badge
kami:task/661-post-merge-usage-rerun
kami:task/661-routing-heads-step-3-train-the-multi-hea
kami:task/660-router-prompt-destination
kami:task/659-destination-fixture
kami:task/655-query-source-is-a-routing-decision-made
kami:task/654-a-pending-clarify-has-no-way-out-neither
kami:task/654-week-of-usage-eval-docs
kami:task/649-needs-kami-telegram-is-the-only-reach-an
kami:task/643-memorystore-search-decodes-and-unmarshal
kami:task/641-two-maps-grow-for-the-process-lifetime-w
kami:task/644-mavcaldav-is-built-documented-as-running
kami:task/642-the-store-caps-sqlite-at-one-connection
kami:task/647-factenrichmentworker-walks-the-pending-q
kami:task/646-v-637-follow-up-telegram-intake-has-no-d
kami:task/638-no-deadline-survives-the-turn-path-from
kami:task/637-inbound-telegram-turns-and-corrections-f
kami:task/636-correcting-a-turn-from-telegram-and-from
kami:task/634-an-act-alias-resolves-the-verb-but-not-t
kami:task/630-one-gesture-correction-on-chat-v-628
kami:task/629-persist-the-routing-trace-and-record-it
kami:task/631-mode-inventory-written-from-the-handlers
kami:task/586-defaultfactparser-uses-hand-written-russ
kami:task/633-reconcile-the-seed-labels-with-the-handl
kami:task/627-reminder-verbs-has-no-alarm-verb-so-an-a
kami:task/626-the-classifier-seeds-teach-an-older-inte
kami:task/546-route-with-a-fine-tuned-e5-small-instead
kami:task/586-measure-the-fact-parser
kami:fix/gofmt-ecosystem-acts
kami:task/584-media-store-a-failed-write-leaks-its-bud
kami:task/518-no-write-path-for-a-backdated-event-so-t
kami:task/287-qa-voice-session-quality-polish
kami:task/492-qa-plan-reconcile
kami:task/530-sweep-tail-four-files-the-russian-sweep
kami:task/405-score-how-often-a-real-utterance-reaches
kami:task/529-money-and-list-pick-a-mechanism
kami:task/528-sweep-tail-the-three-files-on-467
kami:task/527-embedder-open-set-phrasings-stop-being-r
kami:task/526-morphology-a-dictionary-answers-the-gram
kami:task/525-lexicons-the-finite-russian-sets-move-to
kami:task/524-entity-reference-ask-nexus-about-every-l
kami:task/523-risk-tiers-take-hexis-s-tier-for-a-hexis
kami:task/521-review-pr-111-query-strings-declension-h
kami:task/491-llama-server-core-dumps-on-every-sigterm
kami:task/479-bug-an-unconfigured-capability-does-not
kami:task/467-bug-spoken-task-capture-is-dead-the-rout
kami:task/463-deploy-mavwaked-and-mavenclient-run-nowh
kami:task/480-hearing-no-shipped-client-can-start-a-re
kami:task/432-ambient-calendar-intake-is-fragile-and-p
kami:task/431-board-surface-maven-holds-the-work-board
kami:task/433-reactivehandler-has-30-fields-and-is-pas
kami:task/371-swap-the-embedder-for-an-asymmetric-retr
kami:task/408-review-31-07-split-the-30-method-coreapi
kami:task/410-review-31-07-hand-rolled-string-enums-st
kami:task/423-review-pr50-split-internal-ipc-server-go
kami:task/422-review-pr50-split-cmd-mavend-tick-go-860
kami:task/409-review-31-07-finish-moving-mavweb-markup
kami:task/482-ambient-ingest-reads-a-notification-s-ti
kami:task/444-kuma-a-fact-per-monitor-so-she-can-name
kami:task/452-capability-model-homelab-docker-restart
kami:task/449-destructive-confirm-policy-risk-tiers-no
kami:task/453-grocery-list-items-table-fourth-append-o
kami:task/399-run-the-persona-checks-inside-the-daemon
kami:task/448-bounded-follow-up-state-pending-candidat
kami:task/455-conversation-repair-name-the-misroute-co
kami:task/454-go-mod-tidy
kami:task/458-pronunciation-dictionary-for-piper
kami:task/456-command-history-read-only-query-over-exi
kami:task/457-clarification-templates-for-the-router-s
kami:task/474-query-source-ordering-feeds-and-calendar
kami:task/469-reminders-spelled-out-times-fail-the-bod
kami:task/475-bug-the-praxis-attention-capability-is-u
kami:task/481-bug-a-transient-complaint-is-stored-as-a
kami:task/476-bug-the-router-transliterates-latin-enti
kami:task/385-decide-whether-a-parked-clarify-question
kami:task/377-backfill-routines
kami:task/421-weather-geocoder
kami:task/390-no-read-path-for-delivery-attempts
kami:task/386-recall-fixture-filler-note-ids
kami:task/473-bug-morning-item-has-no-required-flag
kami:task/465-bug-make-simulate-routes-with-an-empty
kami:task/467-bug-spoken-task-capture-is-dead
kami:task/466-bug-a-pending-clarify-is-global-so-one-u
kami:task/468-bug-pattern-detect-has-no-minimum-interv
kami:task/462-bug-checkfeminine-flags-second-person-ma
kami:task/443-safekey-drops-cyrillic-so-russian-calend
kami:task/471-bug-agendaquerygrammars-covers-today-but
kami:task/383-slottext-in-clarify-answer-would-clobber
kami:task/323-qa-phraser-coverage-is-65-3-but-the-llam
kami:task/498-bug-and-x-reach-the-model-with-no-determ
kami:task/506-strings-family-6-summaries-and-reports-i
kami:task/504-strings-family-4-act-and-smart-home-repl
kami:task/503-strings-family-3-query-answers-and-gaps
kami:task/502-strings-family-2-capture-acknowledgement
kami:task/501-strings-family-1-phrasing-fallbacks-into
kami:task/397-phrasechat-and-phrasequery-hide-model-fa
kami:task/396-the-reply-path-can-t-be-tested-llmreplie
kami:task/496-recall-a-cross-language-question-loses-i
kami:task/495-bug-x-escapes-the-personal-boundary-and
kami:task/499-llama-server-holds-7-9gb-rss-for-a-1-1gb
kami:task/470-bug-a-question-writes-invented-knowledge
kami:task/493-bug-the-memory-index-stores-the-raw-utte
kami:task/490-name-the-gap-world-questions-through-the
kami:task/485-run-the-big-model-on-the-workstation-wit
kami:task/489-workstation-deploy-mavgpud-on-workpc-and
kami:task/488-workstation-a-supervisor-that-keeps-llam
kami:task/483-docs-offload-design
kami:task/483-design-offload-ml-to-the-workstation-kee
kami:task/459-docs-refresh-the-qa-plan-against-the-liv
kami:task/446-doc-reorg-tier-the-tree-retire-the-three
kami:fix/367-voice-parks-routine-accept
kami:task/365-dialogue-slots-and-router-slots-are-hand
kami:task/364-snooze-does-nothing-at-runtime-the-gate
kami:task/447-retire-progress-md-the-backlog-and-the-f
kami:task/445-session-workflow
kami:overnight/eco-versioned-traces
kami:overnight/eco-entity-refs
kami:overnight/eco-degraded-suite
kami:overnight/netscan
kami:overnight/smarthome
kami:overnight/replay-simulator
kami:overnight/event-envelope
kami:overnight/coldstart-unlock
kami:overnight/voice-barge-in
kami:overnight/stt-golden-audio
kami:overnight/senses-hearing
kami:overnight/senses-media-vision
kami:overnight/mcp-tools
kami:overnight/mcp-client
kami:overnight/self-update
kami:overnight/model-swap
kami:overnight/web-crawler
kami:overnight/rss-feeds
kami:overnight/email-poller
kami:overnight/email-extract
kami:overnight/email-imap
kami:overnight/money-zenmoney
kami:overnight/task-priority
kami:overnight/task-capture
kami:overnight/behavior-profile
kami:overnight/day-plan
kami:overnight/ambient-calendar
kami:overnight/local-calendar
kami:overnight/memory-eval
kami:overnight/proactive-proposals
kami:overnight/split-voice-quiet
kami:overnight/nginx-maven-block
kami:overnight/stepup-chat-surface
kami:integration/small-batch
kami:docs/fix-drift
kami:fix/ru-wording
kami:integration/jul31
kami:overnight/resident-1.7b
kami:overnight/nudge-templates
kami:overnight/kiwix-rewrite
kami:overnight/eval-writeup
kami:overnight/fix-truncation
kami:overnight/kiwix-client
kami:overnight/ru-prompts
kami:overnight/external-data
kami:overnight/phrasing-grammar
kami:overnight/talk-eval
kami:overnight/prompt-context
kami:overnight/prompt-address
kami:overnight/eval-label-kill
kami:overnight/delivery-boundary
kami:overnight/address-check
kami:overnight/system-replies-pr
kami:overnight/clock-intent-pr
kami:overnight/embedder-backfill-pr
kami:overnight/embedder-marker-pr
kami:overnight/note-recall-pr
kami:overnight/thinking-off-pr
kami:overnight/dialogue-persist-pr
kami:overnight/persona-2p-pr
kami:overnight/clarify-expiry-pr
kami:overnight/clarify-rework
kami:overnight/phrasing
kami:overnight/bakeoff
kami:overnight/recall-margin
kami:overnight/router-on
kami:overnight/slot-extract
kami:overnight/embedder-e5
kami:overnight/router-refusal
kami:overnight/eval-rerun
kami:overnight/eval-harnesses
kami:overnight/eval-rerun-base
kami:overnight/fmt-gate
kami:overnight/routines-fire
kami:overnight/router-prompt
kami:overnight/away-leak
kami:overnight/recall-eval
kami:overnight/snooze-works
kami:overnight/clarify-wiring
kami:overnight/delivery-tests
kami:overnight/routine-accept
kami:overnight/llm-router-flag
kami:overnight/clarify-data-layer
kami:overnight/loop-rule-tests
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "overnight/senses-speaker"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Vikunja #255. Third of the senses chain, on top of
overnight/senses-hearing(#253), which sits onovernight/senses-media-vision(#252).What changed
Enrolment plumbing, end to end and working:
internal/speaker/—Recognizer(identify, list, get, forget),Enroll, profile shape, id validation, cosine similarity.memory_vectorstable under aspeaker:id prefix, so no migration. That needed a wider interface thanmemory.Store:memory.CatalogaddsByPrefixandDelete, implemented on bothInMemoryStoreandstore.MemoryStore.InMemoryStore.Insertbecame an upsert by id, matching what the persistent store already did.enroll_speaker/list_speakers/forget_speakerover IPC, absent unless configured.cmd/mavend/speaker.gowiring.docs/plans/10-speaker-recognition.mdrewritten against what actually exists.BLOCKED: there is no speaker-embedding model on this box
I looked before designing.
/mnt/hdd1/llmsholds sixteen ggufs across seven families and every one is text-only. No ECAPA, no x-vector, no titanet, no wespeaker, and no.onnxunder/mnt/hdd1at all. There are no enrolment samples either, because nothing has ever recorded any.So
newSpeakerEmbedderreturns nil,internal/speakerfalls back tospeaker.Disabled, everyIdentifyanswersErrDisabled, and the daemon logs which half is off at startup. Unblocking is a download plus one function: givenewSpeakerEmbedderan ONNX session that turnsaudio.Audiointo a[]float32and recognition starts working with no change to the store, the protocol, the authority table or the handlers.I did not build the plan's "simple MFCC + GMM" floor. MFCC cosine distance detects channel and loudness as much as voice. For a general classifier, sometimes-wrong is a nuisance; for a biometric, confidently-wrong writes false claims about named people into his memory and they get recalled later as fact. A bad floor is worse than none here, so the shipped state is honest absence.
Refused design steps
IdentifyreturnsErrUnknown. The closest profile is named in the error text for diagnosis, never as an answer.Authority, and one deliberate inversion
Enrolment is
AuthStepUp: a deliberate sit-down act writing a biometric of a named person, never something done by voice mid-conversation. Deletion is one rung lower atAuthWrite. Everywhere else inpolicy.gothe destructive direction is gated at least as hard as the constructive one, and here that would be backwards — getting rid of a biometric must never be the harder half. Listing isAuthRead, which is what a per-row forget button needs.Off unless configured: no
speakerblock ⇒ all three methods answerErrUnknownMethod.How verified
make buildandmake testboth exit 0. New tests:internal/speaker/speaker_test.go(disabled path, enrol-then-identify, unfamiliar voice not guessed, threshold as a floor, short audio refused before the model runs, bad ids including../etc/passwdandspeaker:kami, re-enrol replaces, forget removes, profiles do not collide with note vectors, NaN/zero vectors refused),internal/ipc/speaker_test.go(off unless configured, samples cross the wire byte for byte, a listing can report "enrolled, not recognising"),internal/authandinternal/configrows.Vikunja #255
The refusal in
enroll.gois the right call. It is refused in the protocol, not only in prose. No request shape enrols whoever just spoke, so no surface adds one by accident.toWireSpeakerdropsVecandProfile.Veccarriesjson:"-", so the biometric has two independent reasons not to cross the socket. PuttingForgeta rung belowEnrollinauth/policy.go, against the pattern of the rest of that table, is argued and correct.1. Enrolment does not work either, so the capability ships with nothing runnable
Three places say enrolment is live while recognition is blocked:
speaker.gopackage comment: "The enrolment plumbing is real: profiles are stored, listed and deleted."newSpeakerEmbedder: "enrolment and deletion work, recognition does not".speaker: enrolment on, recognition BLOCKED.Recognizer.Enrollembeds every sample before it stores anything, by design (enroll.go, "Embed first, store second"). With no model,r.embisDisabledandr.embedreturnsErrDisabledon the first sample. Your own test asserts exactly this:Walked through on a configured box: operator sets
speaker.enabled, mavend logsenrolment on, the surface sendsenroll_speakerwith three good samples, core answersspeaker: recognition is not configured.list_speakersthen returns an empty list forever, andforget_speakerhas nothing to delete. The shipped state is not "enrolment without recognition". It is "the three methods exist and two of them are no-ops". Say that in the log line and the package comment. Or do not wire the handlers when the embedder is nil.The error mapping compounds it.
speakerErrhas no case forErrDisabled. It falls todefault: return errand reaches the surface as an opaque core failure, not as "this capability is off".ErrDisableddeserves the treatmentErrUnknownMethodgets.2.
SpeakerConfig.Recognizes()is written, documented as the gate, and never calledconfig.gosays the method is false without a model path because "enabled with nothing to embed with is a misconfiguration, not a capability".newSpeakerWiringcheckscfg.Speaker.Enabledand nothing else. So{"speaker":{"enabled":true}}with nomodel_pathwires all three methods and logsenrolment on. It also skips thenewSpeakerEmbedderwarning entirely, because that function returns nil early on an emptyModelPath. That is the one config shape where the operator most needs to be told.grep -rn Recognizesfinds only the definition and its unit test.3. Nothing keeps voiceprints out of note recall except a dimension coincidence
recognizer.goclaims profiles are read throughCatalog"because ... note recall must never rank a voiceprint".Catalogcontrols how you read them, not how anything else does.MemoryStore.Searchis stillSELECT id, vec, meta FROM memory_vectorswith no filter. Everyspeaker:row is scored on every note and fact recall. What saves you today isdot, which returns 0 on a width mismatch. A 192-dim ECAPA row against a 384-dim e5 query scores 0 and sorts low.That is a coincidence of two model choices, not an invariant. Some x-vector exports are 384-dim. One of those puts voiceprints in the same numeric range as note embeddings.
speaker:kamithen surfaces as a recall hit carrying meta{"name":"Ками"}. You already writekind: "speaker"for exactly this class of consumer. Filter on it inSearch, or onid NOT LIKE 'speaker:%', and the doc comment becomes true.Smaller notes
memory.Catalog.Deletedocuments that deleting an absent row is not an error.Recognizer.ForgetcallsGetfirst and returnsErrNotFound, whichspeakerErrturns intoErrNoFact. The layer documented as the one that "must always work" is the layer that reintroduces the failure. A surface retrying a forget after a partial failure gets an error on the second try.InMemoryStore.ByPrefixcopies the vector but returnsit.metaby reference. A caller mutating the returned map edits the stored row. The persistent implementation unmarshals fresh, so the two backends differ.InMemoryStore.Insertchanging from append to upsert affects every user of the in-memory store, not only speaker profiles. It matchesMemoryStore, but it is a fix in its own right and worth naming as one.atoireads"12x"as 0. That is documented as tolerant. Paired withprofileFromRecorddefaultingNametoID, a row with corrupt metadata lists as a plausible profile with 0 samples rather than as damaged.Landed on master. The stack was one linear chain, so #84 carried every commit from #50 up, and master now contains this branch in full. Merging this PR on its own is an empty diff, so it is closed rather than merged. The review findings for it were fixed in the 2026-08-01 pass and are on master as commits on the stack tip, not on this branch.
Pull request closed