@gotcos/glasses-server 6.27.9 → 6.27.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 6.27.10
4
+ - **Naming an unidentified voice now creates a real profile.** `POST /api/meeting/:id/relabel`
5
+ was TEXT ONLY: it rewrote `speaker` strings in the sidecar and never touched the
6
+ voice store. Saving "Kirstyn Blum" over 109 segments labelled that one meeting and
7
+ taught the system nothing — she never appeared in `/api/voice/profiles`, the review
8
+ panel still offered her as `new name` inside the SAME meeting, no later meeting
9
+ could match her, and there was no profile to add further chunks against.
10
+ Verified on the live store: 77 profiles, no Kirstyn Blum, while both sidecars
11
+ carried her name.
12
+ - The server already had `/api/voice/enroll-ext` for exactly this and the naming flow
13
+ never called it. Relabel now enrols the embeddings of the chunks it actually
14
+ changed, tagged `source: meeting-relabel` so a bad batch can be retracted wholesale.
15
+ - **Scoped deliberately.** Enrolment runs ONLY when a placeholder (`Ext`,
16
+ `Unidentified N`, `Speaker N`, `Unknown`) becomes a real name. Correcting one real
17
+ name to another is left alone: moving a voice between existing people is
18
+ `merge-profiles`, which is explicit and confirmation-gated. A cross-roster sweep of
19
+ this store put two DISTINCT people at 0.85 similarity, so implicit re-pointing would
20
+ poison profiles.
21
+ - Enrolment happens after the sidecar and ledger are durable, and a throwing voice
22
+ store cannot undo the rename the user asked for. `enrolledEmbeddings` is returned so
23
+ the panel can confirm a profile was created.
24
+ - Four tests, both directions mutation-checked: reverting to text-only fails the
25
+ enrolment test; dropping the placeholder guard fails both safety tests.
26
+
3
27
  ## 6.27.9
4
28
  - **Large sessions open instead of 413ing.** `GET /api/agent-sessions/:provider/:id`
5
29
  answered `413 Session too large to open` for any transcript over 32 MiB, so the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.27.9",
3
+ "version": "6.27.10",
4
4
  "description": "COS Glasses \u2014 self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, or Cursor Agent CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,7 +23,8 @@ import {
23
23
  meetingAudioChunkPath,
24
24
  meetingAudioRetentionDays,
25
25
  } from '../lib/meeting-audio-archive.js'
26
- import { readVoiceProfiles, retractEmbeddingsBySource } from '../lib/speaker-embeddings.js'
26
+ import { enrollEmbedding, readVoiceProfiles, retractEmbeddingsBySource } from '../lib/speaker-embeddings.js'
27
+ import { chunkEmbeddingsForIndices } from '../lib/chunk-embedding-store.js'
27
28
 
28
29
  /**
29
30
  * The label a de-attributed voice takes, numbered within its meeting.
@@ -974,6 +975,46 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
974
975
  })
975
976
  })
976
977
 
978
+ /** Labels the diariser invents when it does not know who is speaking. Naming one
979
+ * of these is a FIRST TRAINING RUN for a new person, not a correction. */
980
+ const PLACEHOLDER_LABEL = /^(ext|unknown|unidentified(\s+\d+)?|speaker\s*\d+)$/i
981
+ const isPlaceholderLabel = (label: string): boolean => PLACEHOLDER_LABEL.test(label.trim())
982
+
983
+ /**
984
+ * Turn a named placeholder into a real voice profile.
985
+ *
986
+ * WHY THIS EXISTS. Relabelling was TEXT ONLY: it rewrote `speaker` strings in the
987
+ * meeting sidecar and never touched the voice store. So naming an unidentified
988
+ * voice "Kirstyn Blum" labelled that one meeting and taught the system nothing —
989
+ * she never appeared in `/api/voice/profiles`, the review panel still offered her
990
+ * as `new name` inside the SAME meeting, no later meeting could match her, and
991
+ * there was no profile to accumulate further chunks against. The server already
992
+ * had `/api/voice/enroll-ext` for exactly this and the naming flow never called it.
993
+ *
994
+ * Enrolment is ADDITIVE and scoped: it runs only when a placeholder becomes a real
995
+ * name. Correcting one real name to another is left alone deliberately — moving a
996
+ * voice between existing people is `merge-profiles`, which is explicit and
997
+ * confirmation-gated, and doing it implicitly here would poison profiles.
998
+ *
999
+ * `enrollEmbedding` owns the diversity gate and the FIFO cap, so feeding it the
1000
+ * relabelled chunks cannot bloat a profile.
1001
+ */
1002
+ const enrolNamedVoice = (sessionId: string, from: string, to: string, changed: number[]): number => {
1003
+ if (!isPlaceholderLabel(from) || isPlaceholderLabel(to) || changed.length === 0) return 0
1004
+ let enrolled = 0
1005
+ try {
1006
+ for (const row of chunkEmbeddingsForIndices(sessionId, changed)) {
1007
+ if (!row.embedding) continue
1008
+ if (enrollEmbedding(to, row.embedding, 'meeting-relabel').success) enrolled += 1
1009
+ }
1010
+ } catch {
1011
+ // Enrolment is a bonus on top of the relabel. A voice store that refuses must
1012
+ // not fail the rename the user actually asked for.
1013
+ return enrolled
1014
+ }
1015
+ return enrolled
1016
+ }
1017
+
977
1018
  router.post('/meeting/:sessionId/relabel', (req, res) => {
978
1019
  res.set('Cache-Control', 'private, no-store')
979
1020
  const sessionId = String(req.params.sessionId ?? '')
@@ -1150,7 +1191,12 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
1150
1191
  proseStale: preview.proseStale,
1151
1192
  })
1152
1193
 
1153
- res.json({ ok: true, correctionId: id, ...preview })
1194
+ // Enrol AFTER the sidecar and ledger are durable: the rename is the thing the
1195
+ // user asked for, and a voice store that refuses must not undo it. Reported so
1196
+ // the panel can say a profile was created rather than leaving the user to
1197
+ // discover, in another meeting, that it was not.
1198
+ const enrolled = enrolNamedVoice(sessionId, from, to, plan.value.changed)
1199
+ res.json({ ok: true, correctionId: id, enrolledEmbeddings: enrolled, ...preview })
1154
1200
  })
1155
1201
 
1156
1202