@gotcos/glasses-server 6.21.24 → 6.21.26
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,3 +1,48 @@
|
|
|
1
|
+
## 6.21.26
|
|
2
|
+
|
|
3
|
+
- `assertedSegments` added to the speaker review: how many segments belong to
|
|
4
|
+
voices the panel actually shows WITH A NAME. `attributed` is a boolean that
|
|
5
|
+
only goes false at 100% unidentified, so a meeting where 295 of 299 chunks
|
|
6
|
+
matched nobody reported `true` and rendered as though normally attributed.
|
|
7
|
+
Measured unidentified share across 14 retained sessions ran 24% to 100%, all
|
|
8
|
+
of it collapsing onto that one boolean.
|
|
9
|
+
|
|
10
|
+
It counts asserted voices (`nameAsserted`), NOT chunks carrying a
|
|
11
|
+
person-shaped label. Those diverge sharply: on session 0i1xv3 the label-based
|
|
12
|
+
count is 287 of 379 segments while only 177 are displayed as names, so a
|
|
13
|
+
header built on labels would claim three quarters of the meeting identified
|
|
14
|
+
above a list of rows reading "Unidentified voice".
|
|
15
|
+
|
|
16
|
+
Purely additive — the route already spreads the review object and no client
|
|
17
|
+
decodes this payload strictly. COS Control 0.5.5+ renders it, and omits the
|
|
18
|
+
line against an older server rather than showing a zero.
|
|
19
|
+
|
|
20
|
+
## 6.21.25
|
|
21
|
+
|
|
22
|
+
- `POST /meeting/:sessionId/confirm` — record that a human vouched for a label
|
|
23
|
+
the display floor demoted. The floor exists so the identifier cannot assert a
|
|
24
|
+
name it did not earn, but a person who was in the room is better evidence
|
|
25
|
+
than a cosine score, and there was no way to say so. A rename could not
|
|
26
|
+
express it: `relabelSidecarJson` rejects `from === to`. So the panel demoted
|
|
27
|
+
the row, instructed the reviewer to name it, and offered a candidate list
|
|
28
|
+
that excluded the very name they wanted.
|
|
29
|
+
|
|
30
|
+
A confirmation rewrites nothing — the sidecar already carries the label. It
|
|
31
|
+
records the vouch, and `reviewMeetingSpeakers` then reports the row as
|
|
32
|
+
asserted. Meeting-scoped like every other correction: vouching for a voice in
|
|
33
|
+
one room says nothing about a different room. Refuses with 409 if no chunk in
|
|
34
|
+
the meeting actually carries that label, so a typo cannot become a permanent
|
|
35
|
+
confirmation in an append-only ledger.
|
|
36
|
+
|
|
37
|
+
A confirmed row still shows its thrash caveat. The name is asserted; the
|
|
38
|
+
evidence that it swaps with someone else is not hidden.
|
|
39
|
+
|
|
40
|
+
- Fixed a latent trap found while building it: `readCorrections` validated
|
|
41
|
+
`phase` against a hardcoded list, so adding a phase to the TYPE made every
|
|
42
|
+
such row unusable at read time — the write succeeded, the read silently
|
|
43
|
+
dropped it, and no error surfaced anywhere. Phase validation now derives from
|
|
44
|
+
a single list with a narrowing guard.
|
|
45
|
+
|
|
1
46
|
## 6.21.24
|
|
2
47
|
|
|
3
48
|
- A leaked recording session can no longer block every restart. The maintenance
|
package/package.json
CHANGED
|
@@ -35,7 +35,31 @@ export const CORRECTIONS_DIR = 'meeting-corrections'
|
|
|
35
35
|
* `intent` goes down before any file is touched; `applied` or `failed` closes it.
|
|
36
36
|
* An unclosed intent is an incomplete correction, not a successful one.
|
|
37
37
|
*/
|
|
38
|
-
export type CorrectionPhase = 'intent' | 'applied' | 'failed'
|
|
38
|
+
export type CorrectionPhase = 'intent' | 'applied' | 'failed' | 'confirmed'
|
|
39
|
+
/**
|
|
40
|
+
* A human confirming the identifier was RIGHT about a label the display floor
|
|
41
|
+
* demoted.
|
|
42
|
+
*
|
|
43
|
+
* Distinct from a rename, and it has to be: `relabelSidecarJson` rejects
|
|
44
|
+
* `from === to`, so "yes, this really is Queen Ukaoma" cannot be expressed as a
|
|
45
|
+
* correction at all. The floor exists because a 0.56 match is not evidence — but
|
|
46
|
+
* a person who was in the room IS evidence, and there was no way to record it.
|
|
47
|
+
* The panel demoted the row, told the reviewer to name it, and then offered a
|
|
48
|
+
* list that deliberately excluded the one name they wanted.
|
|
49
|
+
*
|
|
50
|
+
* A confirmation rewrites NOTHING. The sidecar already carries the label; this
|
|
51
|
+
* only records that a human vouched for it, so the review stops presenting it
|
|
52
|
+
* as unearned. Scoped to one meeting like every other correction.
|
|
53
|
+
*/
|
|
54
|
+
export const CONFIRMATION_PHASE = 'confirmed' as const
|
|
55
|
+
|
|
56
|
+
/** Runtime counterpart of CorrectionPhase. Must stay in step with it. */
|
|
57
|
+
const VALID_PHASES = new Set<string>(['intent', 'applied', 'failed', 'confirmed'])
|
|
58
|
+
|
|
59
|
+
/** Narrowing guard, so the reader keeps its type safety with one phase list. */
|
|
60
|
+
function isCorrectionPhase(value: unknown): value is CorrectionPhase {
|
|
61
|
+
return typeof value === 'string' && VALID_PHASES.has(value)
|
|
62
|
+
}
|
|
39
63
|
|
|
40
64
|
export interface CorrectionSurfaces {
|
|
41
65
|
/** Chunks whose `speaker` changed in the sidecar. */
|
|
@@ -123,7 +147,11 @@ export function readCorrections(sessionId: string): CorrectionReadResult {
|
|
|
123
147
|
try {
|
|
124
148
|
const o = JSON.parse(line) as Record<string, unknown>
|
|
125
149
|
if (typeof o.id !== 'string' || typeof o.from !== 'string' || typeof o.to !== 'string') { unusable++; continue }
|
|
126
|
-
|
|
150
|
+
// Kept as one list so adding a phase to the TYPE cannot silently make
|
|
151
|
+
// every such row unusable at read time — which is exactly what happened
|
|
152
|
+
// when 'confirmed' was added: appendCorrection wrote it, this dropped it,
|
|
153
|
+
// and the confirmation vanished with no error anywhere.
|
|
154
|
+
if (!isCorrectionPhase(o.phase)) { unusable++; continue }
|
|
127
155
|
rows.push({
|
|
128
156
|
id: o.id,
|
|
129
157
|
phase: o.phase,
|
|
@@ -156,6 +184,22 @@ function isSurfaces(v: unknown): v is CorrectionSurfaces {
|
|
|
156
184
|
* and a meeting with a pending correction should be treated as possibly
|
|
157
185
|
* half-written rather than clean.
|
|
158
186
|
*/
|
|
187
|
+
/**
|
|
188
|
+
* Labels a human has confirmed for this meeting.
|
|
189
|
+
*
|
|
190
|
+
* Read by the speaker review to clear the display floor for exactly those
|
|
191
|
+
* labels. A confirmation is per-meeting and never global: vouching for a voice
|
|
192
|
+
* in one room says nothing about a different room, which is the same reasoning
|
|
193
|
+
* that makes every correction here meeting-scoped.
|
|
194
|
+
*/
|
|
195
|
+
export function confirmedLabels(sessionId: string): Set<string> {
|
|
196
|
+
const confirmed = new Set<string>()
|
|
197
|
+
for (const row of readCorrections(sessionId).rows) {
|
|
198
|
+
if (row.phase === 'confirmed') confirmed.add(row.to)
|
|
199
|
+
}
|
|
200
|
+
return confirmed
|
|
201
|
+
}
|
|
202
|
+
|
|
159
203
|
export function pendingCorrections(sessionId: string): CorrectionRow[] {
|
|
160
204
|
const { rows } = readCorrections(sessionId)
|
|
161
205
|
const closed = new Set(rows.filter(r => r.phase !== 'intent').map(r => r.id))
|
|
@@ -130,6 +130,8 @@ export interface VoiceReview {
|
|
|
130
130
|
meanRun: number
|
|
131
131
|
longestRun: number
|
|
132
132
|
isOwner: boolean
|
|
133
|
+
/** A human confirmed this label for this meeting; the floor is waived. */
|
|
134
|
+
confirmedByHuman: boolean
|
|
133
135
|
reliability: Reliability
|
|
134
136
|
/**
|
|
135
137
|
* Whether `label` may be shown to a human AS A NAME.
|
|
@@ -225,6 +227,23 @@ export interface MeetingSpeakerReview {
|
|
|
225
227
|
segments: number
|
|
226
228
|
/** False when no chunk carries a real speaker — a recovered capture. */
|
|
227
229
|
attributed: boolean
|
|
230
|
+
/**
|
|
231
|
+
* Segments belonging to voices this review asserts a NAME for.
|
|
232
|
+
*
|
|
233
|
+
* `attributed` is a boolean that only goes false at 100% unidentified, so a
|
|
234
|
+
* meeting where 295 of 299 chunks matched nobody still reports `true` and
|
|
235
|
+
* renders as though it were normally attributed. This is the graded version:
|
|
236
|
+
* measured Ext share across 14 retained sessions ran from 24% to 100%.
|
|
237
|
+
*
|
|
238
|
+
* Counts SEGMENTS OF ASSERTED VOICES, not chunks carrying a person-shaped
|
|
239
|
+
* label — see the derivation for why those differ. Denominator is `segments`
|
|
240
|
+
* (every chunk, including unlabelled ones), so `assertedSegments / segments`
|
|
241
|
+
* is the ratio every client should compute.
|
|
242
|
+
*
|
|
243
|
+
* Not named "coverage": `coverageRatio` in batch-transcript-quality.ts is an
|
|
244
|
+
* unrelated word-overlap measure and the two must not read as siblings.
|
|
245
|
+
*/
|
|
246
|
+
assertedSegments: number
|
|
228
247
|
durationMs: number
|
|
229
248
|
voices: VoiceReview[]
|
|
230
249
|
/** Chronological spans, so a ribbon can be a timeline instead of a share bar. */
|
|
@@ -385,9 +404,21 @@ export function selectPhrases(
|
|
|
385
404
|
/** Build the whole review for one meeting's chunks. */
|
|
386
405
|
export function reviewMeetingSpeakers(
|
|
387
406
|
chunks: ReviewChunk[],
|
|
388
|
-
options: {
|
|
407
|
+
options: {
|
|
408
|
+
owner?: string
|
|
409
|
+
phrasesPerVoice?: number
|
|
410
|
+
durationMs?: number
|
|
411
|
+
/**
|
|
412
|
+
* Labels a human vouched for in THIS meeting. The floor is a guard against
|
|
413
|
+
* the identifier over-claiming; it was never meant to overrule a person who
|
|
414
|
+
* was in the room. A confirmed label asserts its name with no rewrite —
|
|
415
|
+
* the sidecar already carries it.
|
|
416
|
+
*/
|
|
417
|
+
confirmed?: Set<string>
|
|
418
|
+
} = {},
|
|
389
419
|
): MeetingSpeakerReview {
|
|
390
420
|
const owner = options.owner ?? 'Me'
|
|
421
|
+
const confirmed = options.confirmed ?? new Set<string>()
|
|
391
422
|
const limit = options.phrasesPerVoice ?? 3
|
|
392
423
|
const sequence = chunks.map(c => c.speaker ?? '')
|
|
393
424
|
// The caller's durationMs (the sidecar's own) is the meeting's true end.
|
|
@@ -439,6 +470,12 @@ export function reviewMeetingSpeakers(
|
|
|
439
470
|
const assertionBlockers: string[] = []
|
|
440
471
|
if (unattributed) {
|
|
441
472
|
assertionBlockers.push('no name was ever assigned to this voice')
|
|
473
|
+
} else if (confirmed.has(label)) {
|
|
474
|
+
// A human said this is them. Blockers stay OFF the list rather than being
|
|
475
|
+
// listed-then-overridden, because a reviewer reading "similarity 0.56
|
|
476
|
+
// below 0.65" under a name they personally confirmed would reasonably
|
|
477
|
+
// conclude the confirmation had not taken. `thrashesWith` still renders,
|
|
478
|
+
// so a genuinely mixed row is still visibly mixed.
|
|
442
479
|
} else if (label === owner) {
|
|
443
480
|
// The wearer is exempt. Their identity is established by wearing the
|
|
444
481
|
// device, not by cosine — and the owner is verified at exactly this floor
|
|
@@ -466,6 +503,8 @@ export function reviewMeetingSpeakers(
|
|
|
466
503
|
meanRun: Math.round(mean(runs) * 100) / 100,
|
|
467
504
|
longestRun: runs.length ? Math.max(...runs) : 0,
|
|
468
505
|
isOwner: label === owner,
|
|
506
|
+
/** True when a human vouched for this label in this meeting. */
|
|
507
|
+
confirmedByHuman: confirmed.has(label),
|
|
469
508
|
reliability,
|
|
470
509
|
nameAsserted: assertionBlockers.length === 0,
|
|
471
510
|
assertionBlockers,
|
|
@@ -478,6 +517,14 @@ export function reviewMeetingSpeakers(
|
|
|
478
517
|
return {
|
|
479
518
|
segments: chunks.length,
|
|
480
519
|
attributed: named.length > 0,
|
|
520
|
+
// Derived from `nameAsserted`, NOT from `isUnattributed`. Those two answer
|
|
521
|
+
// different questions and diverge badly: measured on session 0i1xv3, the
|
|
522
|
+
// label-based count is 287 of 379 segments while only 177 belong to voices
|
|
523
|
+
// the panel actually shows with a name — a 29-point gap. A header built on
|
|
524
|
+
// labels would claim three quarters of the meeting was identified above a
|
|
525
|
+
// list of rows reading "Unidentified voice", which is the confusion this
|
|
526
|
+
// number exists to remove.
|
|
527
|
+
assertedSegments: voices.reduce((n, v) => (v.nameAsserted ? n + v.segments : n), 0),
|
|
481
528
|
durationMs,
|
|
482
529
|
voices,
|
|
483
530
|
timeline: speakerTimeline(chunks, durationMs),
|
package/server/routes/meeting.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { isSampleFromSession, untraceableSampleCount } from '../lib/training-aud
|
|
|
13
13
|
import { sendAudioFile } from '../lib/send-audio.js'
|
|
14
14
|
import { chunkDiagnostics } from '../lib/chunk-embedding-diagnostics.js'
|
|
15
15
|
import { errMsg } from '../lib/utils.js'
|
|
16
|
+
import { confirmedLabels } from '../lib/meeting-corrections.js'
|
|
16
17
|
import {
|
|
17
18
|
extAudioChunkPath,
|
|
18
19
|
listExtAudioChunks,
|
|
@@ -724,6 +725,10 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
|
|
|
724
725
|
const withIndices = attachRawChunkIndices(chunks as ReviewChunk[], sidecar.chunkEntries)
|
|
725
726
|
const review = reviewMeetingSpeakers(withIndices, {
|
|
726
727
|
owner: getOwnerSpeakerLabel(),
|
|
728
|
+
// Labels a human already vouched for in this meeting. Without this the
|
|
729
|
+
// floor re-demotes a confirmed name on every reload, and the reviewer
|
|
730
|
+
// confirms the same voice forever.
|
|
731
|
+
confirmed: confirmedLabels(sessionId),
|
|
727
732
|
phrasesPerVoice: Math.max(1, Math.min(6, Number(req.query.phrases) || 3)),
|
|
728
733
|
// The sidecar's own durationMs is the meeting's true end. Deriving it from
|
|
729
734
|
// max(elapsed) uses the START of the last chunk, which made the final
|
|
@@ -929,6 +934,87 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
|
|
|
929
934
|
// like and will keep matching. Removing only the label fixes the transcript and
|
|
930
935
|
// leaves the profile poisoned — the exact mechanism that grew the phantom
|
|
931
936
|
// "Erick Hernandez" from 3 mislabelled seeds to 18 samples.
|
|
937
|
+
/**
|
|
938
|
+
* Confirm the identifier was RIGHT about a label the display floor demoted.
|
|
939
|
+
*
|
|
940
|
+
* Rewrites nothing. The sidecar already carries the label; this records that
|
|
941
|
+
* a human vouched for it so the review stops presenting it as unearned.
|
|
942
|
+
*
|
|
943
|
+
* This exists because "yes, that really is her" was inexpressible. A rename
|
|
944
|
+
* cannot say it — `relabelSidecarJson` rejects `from === to` — so the panel
|
|
945
|
+
* demoted the row, instructed the reviewer to name it, and then offered a
|
|
946
|
+
* candidate list that excluded the very name they wanted. The floor is a
|
|
947
|
+
* guard against the IDENTIFIER over-claiming; it was never meant to overrule
|
|
948
|
+
* a person who was in the room.
|
|
949
|
+
*
|
|
950
|
+
* Meeting-scoped, like every other correction: vouching for a voice in one
|
|
951
|
+
* room says nothing about a different room.
|
|
952
|
+
*/
|
|
953
|
+
router.post('/meeting/:sessionId/confirm', (req, res) => {
|
|
954
|
+
res.set('Cache-Control', 'private, no-store')
|
|
955
|
+
const sessionId = String(req.params.sessionId ?? '')
|
|
956
|
+
if (!/^[A-Za-z0-9:_-]{3,96}$/.test(sessionId)) {
|
|
957
|
+
res.status(400).json({ error: 'Invalid sessionId', reason: 'invalid_session_id' })
|
|
958
|
+
return
|
|
959
|
+
}
|
|
960
|
+
const label = typeof req.body?.label === 'string' ? req.body.label : ''
|
|
961
|
+
const bad = invalidLabelReason(label)
|
|
962
|
+
if (bad) {
|
|
963
|
+
res.status(400).json({ error: `label: ${bad}`, reason: 'invalid_label' })
|
|
964
|
+
return
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Same resolution as GET /speakers and the relabel route, so the panel and
|
|
968
|
+
// the confirmation act on the same copy of the meeting.
|
|
969
|
+
const operations = cosOperationsMeetingsConfigured()
|
|
970
|
+
? findCosOperationsMeetingBySessionId(sessionId)
|
|
971
|
+
: null
|
|
972
|
+
const saved = operations ? null : store.findBySessionId(sessionId)
|
|
973
|
+
if (!operations && !saved) {
|
|
974
|
+
res.status(404).json({ error: 'No saved meeting for this session', reason: 'meeting_not_found' })
|
|
975
|
+
return
|
|
976
|
+
}
|
|
977
|
+
const sidecarPath = operations?.sidecarPath ?? saved!.sidecarPath
|
|
978
|
+
|
|
979
|
+
// Refuse to confirm a label the meeting does not actually carry. Otherwise
|
|
980
|
+
// a typo becomes a permanent confirmation for a speaker who was never here,
|
|
981
|
+
// and the ledger is append-only.
|
|
982
|
+
let carried = 0
|
|
983
|
+
try {
|
|
984
|
+
const doc = JSON.parse(readFileSync(sidecarPath, 'utf-8')) as Record<string, unknown>
|
|
985
|
+
const rows = Array.isArray(doc.chunks) ? doc.chunks : []
|
|
986
|
+
carried = rows.filter(r => r && typeof r === 'object'
|
|
987
|
+
&& (r as Record<string, unknown>).speaker === label).length
|
|
988
|
+
} catch {
|
|
989
|
+
res.status(500).json({ error: 'Could not read the chunk sidecar', reason: 'sidecar_unreadable' })
|
|
990
|
+
return
|
|
991
|
+
}
|
|
992
|
+
if (carried === 0) {
|
|
993
|
+
res.status(409).json({
|
|
994
|
+
error: `No chunk in this meeting is labelled "${label}"`,
|
|
995
|
+
reason: 'label_not_present',
|
|
996
|
+
})
|
|
997
|
+
return
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const ok = appendCorrection(sessionId, {
|
|
1001
|
+
id: `confirm-${Date.now()}`,
|
|
1002
|
+
phase: 'confirmed',
|
|
1003
|
+
at: new Date().toISOString(),
|
|
1004
|
+
// `from` and `to` are the same by definition — that is what makes this a
|
|
1005
|
+
// confirmation rather than a rename, and why relabel could not express it.
|
|
1006
|
+
from: label,
|
|
1007
|
+
to: label,
|
|
1008
|
+
chunks: [],
|
|
1009
|
+
scope: 'meeting',
|
|
1010
|
+
})
|
|
1011
|
+
if (!ok) {
|
|
1012
|
+
res.status(500).json({ error: 'Could not record the confirmation', reason: 'ledger_write_failed' })
|
|
1013
|
+
return
|
|
1014
|
+
}
|
|
1015
|
+
res.json({ confirmed: true, label, segments: carried })
|
|
1016
|
+
})
|
|
1017
|
+
|
|
932
1018
|
router.post('/meeting/:sessionId/deattribute', (req, res) => {
|
|
933
1019
|
res.set('Cache-Control', 'private, no-store')
|
|
934
1020
|
const sessionId = String(req.params.sessionId ?? '')
|