@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 +24 -0
- package/package.json +1 -1
- package/server/routes/meeting.ts +48 -2
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.
|
|
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": {
|
package/server/routes/meeting.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|