speaker: do not ship three methods that cannot work
The package comment, the embedder log and the startup line all said enrolment was live and only recognition was blocked. Enroll embeds every sample before it stores anything, so with no model on the box it fails on the first sample with ErrDisabled and nothing is ever stored. List then returns an empty list forever and Forget has nothing to delete. The shipped state was three methods, all no-ops, announced as a working half. SpeakerConfig.Recognizes was written as the gate for this and never called, so a block with enabled and no model_path wired everything and skipped the one warning the operator needed. It is the gate now, and that config shape logs why it stayed off. Three smaller repairs. ErrDisabled had no case in speakerErr and reached the surface as an opaque core failure, when it means the same thing ErrUnknownMethod does. Forget read the row first and answered ErrNotFound on a second call, so the layer documented as the one that must always work reintroduced a failure for a voiceprint that was already gone. And a row with unparsable metadata listed as a plausible profile named after its own id with 0 samples, which is what a real minimal enrolment looks like; it is reported as damaged now. Found in review of #74.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/kami/maven/internal/audio"
|
||||
@@ -140,14 +141,18 @@ func (r *Recognizer) Get(ctx context.Context, id string) (Profile, error) {
|
||||
// Forget deletes a profile. This is the one operation that must always work:
|
||||
// a voiceprint is data about a person, and "перестань узнавать её" has to
|
||||
// actually remove it, not mark it inactive.
|
||||
//
|
||||
// Forgetting a profile that is not there is not an error, matching
|
||||
// memory.Catalog.Delete. It used to read the row first and answer ErrNotFound,
|
||||
// which meant the layer documented as the one that must always work was the
|
||||
// layer reintroducing a failure: a surface retrying a forget after a partial
|
||||
// failure got an error on the second try, for a voiceprint that was already
|
||||
// gone. The caller asked for it to be gone and it is gone.
|
||||
func (r *Recognizer) Forget(ctx context.Context, id string) error {
|
||||
id = NormalizeID(id)
|
||||
if !ValidID(id) {
|
||||
return fmt.Errorf("%w: %q", ErrBadID, id)
|
||||
}
|
||||
if _, err := r.Get(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.cat.Delete(ctx, Prefix+id); err != nil {
|
||||
return fmt.Errorf("speaker: forget %q: %w", id, err)
|
||||
}
|
||||
@@ -170,6 +175,12 @@ func (r *Recognizer) embed(ctx context.Context, a audio.Audio) ([]float32, error
|
||||
// profileFromRecord reads a stored row back into a Profile. A row with
|
||||
// unreadable metadata still yields a usable voiceprint — the vector is the part
|
||||
// that matters, and losing a name should not lose the enrolment.
|
||||
//
|
||||
// It also says when the metadata did not read cleanly. Without that, a row
|
||||
// whose samples count is "12x" and whose name is missing came back as a
|
||||
// plausible profile called by its own id with 0 samples, which is exactly what
|
||||
// a real minimal enrolment looks like. Damaged is the difference between "he
|
||||
// enrolled badly" and "this row is broken".
|
||||
func profileFromRecord(rec memory.Record) Profile {
|
||||
p := Profile{
|
||||
ID: trimPrefix(rec.ID),
|
||||
@@ -178,15 +189,24 @@ func profileFromRecord(rec memory.Record) Profile {
|
||||
Name: rec.Meta["name"],
|
||||
}
|
||||
if s := rec.Meta["samples"]; s != "" {
|
||||
p.Samples = atoi(s)
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil || n < 0 {
|
||||
p.Damaged = true
|
||||
} else {
|
||||
p.Samples = n
|
||||
}
|
||||
}
|
||||
if ts := rec.Meta["enrolled"]; ts != "" {
|
||||
if t, err := time.Parse(time.RFC3339, ts); err == nil {
|
||||
t, err := time.Parse(time.RFC3339, ts)
|
||||
if err != nil {
|
||||
p.Damaged = true
|
||||
} else {
|
||||
p.Enrolled = t
|
||||
}
|
||||
}
|
||||
if p.Name == "" {
|
||||
p.Name = p.ID
|
||||
p.Damaged = true
|
||||
}
|
||||
return p
|
||||
}
|
||||
@@ -197,16 +217,3 @@ func trimPrefix(id string) string {
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// atoi is a tolerant small-integer parse: metadata that is not a number reads
|
||||
// as 0 rather than failing the whole listing.
|
||||
func atoi(s string) int {
|
||||
n := 0
|
||||
for _, r := range s {
|
||||
if r < '0' || r > '9' {
|
||||
return 0
|
||||
}
|
||||
n = n*10 + int(r-'0')
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user