@gotcos/glasses-server 6.27.12 → 6.27.13
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 +27 -0
- package/package.json +1 -1
- package/server/lib/meeting-relabel-enrolment.ts +10 -1
- package/server/routes/meeting.ts +90 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
## Unreleased
|
|
2
2
|
|
|
3
|
+
## 6.27.13
|
|
4
|
+
- **`POST /api/meeting/:sessionId/backfill-enrolment`** — train a profile from a voice
|
|
5
|
+
that was named BEFORE enrolment shipped. Those meetings have a correct transcript and
|
|
6
|
+
no profile, and re-running the rename cannot help: the voice is already a real name
|
|
7
|
+
there, so the placeholder guard correctly declines. The live case is Kirstyn Blum —
|
|
8
|
+
60 and 109 chunks across two meetings, 182 sidecar mentions, absent from a 77-profile
|
|
9
|
+
store.
|
|
10
|
+
- The correction LEDGER already holds what enrolment needs: the original `from`, the
|
|
11
|
+
`to`, and the exact chunk indices written at apply time. This replays those rows
|
|
12
|
+
through the SAME `enrolNamedVoice` — raw-index mapping, refusal when unmappable,
|
|
13
|
+
coherence, the diversity cap and the `correction:<sessionId>` tag all apply
|
|
14
|
+
identically. No second implementation to drift.
|
|
15
|
+
- **Named-source corrections are excluded, by the existing rule rather than a new one.**
|
|
16
|
+
`Ext -> Kirstyn` is training data; `Allison Wheeler -> Kirstyn` is a mis-attribution
|
|
17
|
+
fix, and training on it would put Allison's voice into Kirstyn's profile. Both rows
|
|
18
|
+
are reported; only the placeholder one attempts anything. Mutation-verified: dropping
|
|
19
|
+
the placeholder guard fails that test.
|
|
20
|
+
- **Runs in-process, which is the point.** The voice store is owned by the running
|
|
21
|
+
server and rewritten wholesale, so an external process that enrols directly has its
|
|
22
|
+
work silently clobbered. Not hypothetical — an attempt on 2026-08-13 validated
|
|
23
|
+
cleanly, selected 20 samples, and left the store untouched at its Aug 7 mtime.
|
|
24
|
+
- **Fails closed.** Without `confirm: true` it reports what it would enrol and writes
|
|
25
|
+
nothing. The preview runs every gate — a preview that skipped them would be a guess
|
|
26
|
+
about what the real call does — so `enrolNamedVoice` gained a `dryRun` flag. The
|
|
27
|
+
projection can land HIGHER than reality, because only `enrollEmbedding` can judge
|
|
28
|
+
near-duplicates against the live profile; that is documented on the flag.
|
|
29
|
+
|
|
3
30
|
## 6.27.12
|
|
4
31
|
- **Naming an unidentified voice creates a real speaker profile — correctly this time.**
|
|
5
32
|
Re-enables what 6.27.10 shipped broken and 6.27.11 disabled.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.27.
|
|
3
|
+
"version": "6.27.13",
|
|
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": {
|
|
@@ -169,6 +169,11 @@ export interface EnrolNamedVoiceInput {
|
|
|
169
169
|
changed: number[]
|
|
170
170
|
/** The parsed sidecar, for `chunks` + `chunkEntries` + `startTime`. */
|
|
171
171
|
sidecar: Record<string, unknown>
|
|
172
|
+
/** Run every gate but write nothing. `enrolled` then counts what WOULD be enrolled;
|
|
173
|
+
* the real call can land LOWER, because `enrollEmbedding`'s dedup gate rejects
|
|
174
|
+
* near-duplicates and only it can judge that against the live profile. A preview
|
|
175
|
+
* that skipped the gates would be a guess about what the real call does. */
|
|
176
|
+
dryRun?: boolean
|
|
172
177
|
}
|
|
173
178
|
|
|
174
179
|
/**
|
|
@@ -238,7 +243,11 @@ export function enrolNamedVoice(input: EnrolNamedVoiceInput): EnrolmentReport {
|
|
|
238
243
|
const source = `correction:${sessionId}`
|
|
239
244
|
let enrolled = 0
|
|
240
245
|
try {
|
|
241
|
-
|
|
246
|
+
if (input.dryRun) {
|
|
247
|
+
// Projection, not a promise — see the dryRun docblock. Every gate above has
|
|
248
|
+
// already run, so this is what the real call will attempt.
|
|
249
|
+
enrolled = selected.length
|
|
250
|
+
} else for (const embedding of selected) {
|
|
242
251
|
if (enrollEmbedding(to, embedding, source).success) enrolled += 1
|
|
243
252
|
}
|
|
244
253
|
} catch {
|
package/server/routes/meeting.ts
CHANGED
|
@@ -975,6 +975,96 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
|
|
|
975
975
|
})
|
|
976
976
|
})
|
|
977
977
|
|
|
978
|
+
/**
|
|
979
|
+
* Replay a recorded correction's enrolment — the retroactive path.
|
|
980
|
+
*
|
|
981
|
+
* WHY. Enrolment fires inside `POST /relabel`, so a voice named BEFORE that shipped
|
|
982
|
+
* has a correct transcript and no profile. Kirstyn Blum is the live case: named
|
|
983
|
+
* across two meetings (60 and 109 chunks), 182 mentions in the sidecars, absent from
|
|
984
|
+
* a 77-profile store. Re-running the rename cannot help — she is already a real
|
|
985
|
+
* name there, so the placeholder guard correctly declines.
|
|
986
|
+
*
|
|
987
|
+
* The correction LEDGER already holds exactly what enrolment needs: the original
|
|
988
|
+
* `from`, the `to`, and the precise chunk indices, written at apply time.
|
|
989
|
+
*
|
|
990
|
+
* This runs IN-PROCESS on purpose. The voice store is owned by the running server,
|
|
991
|
+
* which holds it in memory and rewrites it wholesale; an external process that
|
|
992
|
+
* enrols directly has its work silently clobbered on the next persist. That is not
|
|
993
|
+
* hypothetical — an attempt on 2026-08-13 validated cleanly, selected 20 samples,
|
|
994
|
+
* and left the store untouched at its Aug 7 mtime.
|
|
995
|
+
*
|
|
996
|
+
* SAME GATES, no exceptions. It calls `enrolNamedVoice`, so raw-index mapping,
|
|
997
|
+
* refusal when unmappable, voice coherence, the diversity cap and the
|
|
998
|
+
* `correction:<sessionId>` tag all apply identically. Rows whose `from` is a real
|
|
999
|
+
* person are skipped by that function's own placeholder rule, which is what keeps a
|
|
1000
|
+
* mis-attribution correction (Allison Wheeler -> Kirstyn) out of the training set.
|
|
1001
|
+
*
|
|
1002
|
+
* FAILS CLOSED. Without `confirm: true` it reports what it would enrol and writes
|
|
1003
|
+
* nothing.
|
|
1004
|
+
*/
|
|
1005
|
+
router.post('/meeting/:sessionId/backfill-enrolment', (req, res) => {
|
|
1006
|
+
res.set('Cache-Control', 'private, no-store')
|
|
1007
|
+
const sessionId = String(req.params.sessionId ?? '')
|
|
1008
|
+
if (!/^[A-Za-z0-9:_-]{3,96}$/.test(sessionId)) {
|
|
1009
|
+
res.status(400).json({ error: 'Invalid sessionId', reason: 'invalid_session_id' })
|
|
1010
|
+
return
|
|
1011
|
+
}
|
|
1012
|
+
const speaker = typeof req.body?.speaker === 'string' ? req.body.speaker.trim() : ''
|
|
1013
|
+
if (!speaker) {
|
|
1014
|
+
res.status(400).json({ error: 'speaker is required', reason: 'invalid_label' })
|
|
1015
|
+
return
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const operations = cosOperationsMeetingsConfigured()
|
|
1019
|
+
? findCosOperationsMeetingBySessionId(sessionId)
|
|
1020
|
+
: null
|
|
1021
|
+
const saved = operations ? null : store.findBySessionId(sessionId)
|
|
1022
|
+
if (!operations && !saved) {
|
|
1023
|
+
res.status(404).json({ error: 'No saved meeting for this session', reason: 'meeting_not_found' })
|
|
1024
|
+
return
|
|
1025
|
+
}
|
|
1026
|
+
const sidecarPath = operations?.sidecarPath ?? saved!.sidecarPath
|
|
1027
|
+
let parsedSidecar: Record<string, unknown> | null = null
|
|
1028
|
+
try {
|
|
1029
|
+
const doc = JSON.parse(readFileSync(sidecarPath, 'utf-8')) as unknown
|
|
1030
|
+
if (doc && typeof doc === 'object' && !Array.isArray(doc)) parsedSidecar = doc as Record<string, unknown>
|
|
1031
|
+
} catch {
|
|
1032
|
+
res.status(422).json({ error: 'Chunk sidecar is missing or unreadable', reason: 'sidecar_unreadable' })
|
|
1033
|
+
return
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Only rows that ACTUALLY landed, and only those that named this speaker.
|
|
1037
|
+
const rows = appliedCorrections(sessionId).filter(r => r.to === speaker && r.chunks.length > 0)
|
|
1038
|
+
if (rows.length === 0) {
|
|
1039
|
+
res.status(404).json({ error: `No applied correction named "${speaker}" in this meeting`, reason: 'no_correction' })
|
|
1040
|
+
return
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const confirm = req.body?.confirm === true
|
|
1044
|
+
const reports = rows.map(row => ({
|
|
1045
|
+
correctionId: row.id,
|
|
1046
|
+
from: row.from,
|
|
1047
|
+
chunks: row.chunks.length,
|
|
1048
|
+
// Dry run still evaluates every gate — a preview that skips them would be a
|
|
1049
|
+
// guess about what the real call is going to do.
|
|
1050
|
+
report: enrolNamedVoice({
|
|
1051
|
+
sessionId, from: row.from, to: speaker, changed: row.chunks, sidecar: parsedSidecar!, dryRun: !confirm,
|
|
1052
|
+
}),
|
|
1053
|
+
}))
|
|
1054
|
+
|
|
1055
|
+
res.json({
|
|
1056
|
+
ok: true,
|
|
1057
|
+
speaker,
|
|
1058
|
+
confirmed: confirm,
|
|
1059
|
+
corrections: reports,
|
|
1060
|
+
totals: {
|
|
1061
|
+
eligible: reports.filter(r => r.report.attempted > 0).length,
|
|
1062
|
+
skippedNamedSource: reports.filter(r => r.report.attempted === 0 && !r.report.skipped).length,
|
|
1063
|
+
enrolled: reports.reduce((n, r) => n + r.report.enrolled, 0),
|
|
1064
|
+
},
|
|
1065
|
+
})
|
|
1066
|
+
})
|
|
1067
|
+
|
|
978
1068
|
router.post('/meeting/:sessionId/relabel', (req, res) => {
|
|
979
1069
|
res.set('Cache-Control', 'private, no-store')
|
|
980
1070
|
const sessionId = String(req.params.sessionId ?? '')
|