@gotcos/glasses-server 6.21.29 → 6.21.30
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 +34 -0
- package/package.json +1 -1
- package/server/lib/meeting-scribe-content.ts +61 -8
- package/server/routes/meeting.ts +15 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
## 6.21.30
|
|
2
|
+
|
|
3
|
+
- **A name you removed by hand is now stated, and the stale write-up is called
|
|
4
|
+
out.** De-attribution rewrites the sidecar, the attendee list and the transcript
|
|
5
|
+
labels, but deliberately leaves narrative prose alone, because substituting into
|
|
6
|
+
a written sentence mangles grammar and can hit the wrong person. The applied
|
|
7
|
+
correction row has recorded `proseStale: true` for exactly this since the feature
|
|
8
|
+
shipped — and NOTHING read it.
|
|
9
|
+
|
|
10
|
+
Real case, 2026-08-07: Miles removed "Clem Ukaoma" from a personal call that was
|
|
11
|
+
only him and Queen (his father's voice matched a similar profile). All 8 label
|
|
12
|
+
sites were rewritten correctly. The LLM summary still opened "Miles, Queen, and
|
|
13
|
+
Clem talk through..." and the payload said nothing at all. The allowlist covered
|
|
14
|
+
it only as "not confirmed", which is far too weak: he did not fail to confirm
|
|
15
|
+
that person, he explicitly said they were not in the room.
|
|
16
|
+
|
|
17
|
+
The payload now carries `removedNames` and states it above the write-up: *"You
|
|
18
|
+
removed "Clem Ukaoma" from this meeting. The write-up below was written before
|
|
19
|
+
that and still uses the name: treat every mention of it as a capture error, not
|
|
20
|
+
a participant."* The prose is left intact — the record stays, the correction
|
|
21
|
+
travels beside it.
|
|
22
|
+
|
|
23
|
+
- **The overlap note is gated on how long the meeting RAN, not on voiced time.**
|
|
24
|
+
Gating on voiced time tripped it on 16 of 23 real meetings, because any overlap
|
|
25
|
+
at all exceeds the union of voiced speech. At 70% it stopped being a signal and
|
|
26
|
+
became boilerplate on something pasted into Slack. Rows adding to more than the
|
|
27
|
+
meeting length is the genuinely confusing case — 71m of rows inside a 66-minute
|
|
28
|
+
meeting — and that is roughly a tenth of meetings.
|
|
29
|
+
|
|
30
|
+
Worth recording for the next person who tests this: only the WORD-TIMING path
|
|
31
|
+
can overflow. Measured, the chunk-estimate path credits a contested second to
|
|
32
|
+
one speaker (two speakers at identical timestamps gave MU 273s and Gina 0s), so
|
|
33
|
+
a chunk-sourced meeting can never trip the note.
|
|
34
|
+
|
|
1
35
|
## 6.21.29
|
|
2
36
|
|
|
3
37
|
Everything below was found by two rounds of adversarial review of 6.21.28 against
|
package/package.json
CHANGED
|
@@ -314,10 +314,19 @@ export interface AttendeeRenderOptions {
|
|
|
314
314
|
*/
|
|
315
315
|
unattributedMs: number
|
|
316
316
|
/**
|
|
317
|
-
* UNION of all voiced time.
|
|
318
|
-
* the crosstalk note in `renderAttendees`. 0 disables the check.
|
|
317
|
+
* UNION of all voiced time. Only used to phrase the crosstalk note.
|
|
319
318
|
*/
|
|
320
319
|
voicedMs: number
|
|
320
|
+
/**
|
|
321
|
+
* How long the meeting RAN. This is what the overlap note is gated on.
|
|
322
|
+
*
|
|
323
|
+
* Gating on `voicedMs` instead tripped on 16 of 23 real meetings, because any
|
|
324
|
+
* overlap at all exceeds the union of voiced time — at 70% the line stops being
|
|
325
|
+
* a signal and becomes boilerplate on something Miles pastes into Slack. Rows
|
|
326
|
+
* adding to more than the meeting LENGTH is the genuinely confusing case (71m
|
|
327
|
+
* of rows inside a 66-minute meeting), and that is ~10% of meetings.
|
|
328
|
+
*/
|
|
329
|
+
durationMs: number
|
|
321
330
|
}
|
|
322
331
|
|
|
323
332
|
/** The attendee block, floor-applied. */
|
|
@@ -348,9 +357,9 @@ export function renderAttendees(voices: AttendeeLine[], o: AttendeeRenderOptions
|
|
|
348
357
|
// right; adding them is what misleads, so say so rather than shrink anyone.
|
|
349
358
|
const rowTotal = named.reduce((t, v) => t + (Number.isFinite(v.speakingMs) ? v.speakingMs : 0), 0) +
|
|
350
359
|
(rest.length > 0 && Number.isFinite(o.unattributedMs) ? o.unattributedMs : 0)
|
|
351
|
-
if (o.
|
|
352
|
-
out.push(`- (these overlap: people talked over each other, so the rows add to ` +
|
|
353
|
-
`more than the ${mmss(o.
|
|
360
|
+
if (o.durationMs > 0 && rowTotal > o.durationMs * 1.02) {
|
|
361
|
+
out.push(`- (these overlap: people talked over each other, so the rows add up to ` +
|
|
362
|
+
`more than the ${mmss(o.durationMs)} the meeting ran)`)
|
|
354
363
|
}
|
|
355
364
|
return out.length > 0 ? out.join('\n') : '- (no voices identified)'
|
|
356
365
|
}
|
|
@@ -363,7 +372,32 @@ export function renderAttendees(voices: AttendeeLine[], o: AttendeeRenderOptions
|
|
|
363
372
|
* into a model has no other way to tell which. Naming the confirmed set is short
|
|
364
373
|
* and complete.
|
|
365
374
|
*/
|
|
366
|
-
|
|
375
|
+
/**
|
|
376
|
+
* Names a human explicitly REMOVED from this meeting, still present in the prose.
|
|
377
|
+
*
|
|
378
|
+
* De-attribution rewrites the sidecar, the attendee list and the transcript
|
|
379
|
+
* labels, but deliberately leaves narrative prose alone — substituting into a
|
|
380
|
+
* written sentence mangles grammar and can hit the wrong person. So the applied
|
|
381
|
+
* correction row records `proseStale: true` and, until now, NOTHING read it.
|
|
382
|
+
*
|
|
383
|
+
* Real case, 2026-08-07: Miles removed "Clem Ukaoma" from a personal call that
|
|
384
|
+
* was only him and Queen (his father's voice matched a similar profile). All 8
|
|
385
|
+
* label sites were rewritten correctly; the LLM summary still opened "Miles,
|
|
386
|
+
* Queen, and Clem talk through..." and the payload said nothing. An allowlist
|
|
387
|
+
* ("this name was not confirmed") is far too weak here — he did not fail to
|
|
388
|
+
* confirm this person, he explicitly said they were not in the room.
|
|
389
|
+
*/
|
|
390
|
+
export interface RemovedName {
|
|
391
|
+
label: string
|
|
392
|
+
/** True when narrative prose still carries the name. */
|
|
393
|
+
proseStale: boolean
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function renderProvenance(
|
|
397
|
+
voices: AttendeeLine[],
|
|
398
|
+
coverage: number | null,
|
|
399
|
+
removed: RemovedName[] = [],
|
|
400
|
+
): string {
|
|
367
401
|
const named = voices.filter(v => v.asserted)
|
|
368
402
|
// Quoted because a label may legitimately contain a comma — `invalidLabelReason`
|
|
369
403
|
// rejects brackets and newlines but not commas, and a typed "Smith, John" would
|
|
@@ -384,6 +418,20 @@ export function renderProvenance(voices: AttendeeLine[], coverage: number | null
|
|
|
384
418
|
'comes from the raw capture or the write-up and was NOT confirmed. Some may be ' +
|
|
385
419
|
'people mentioned rather than people present.',
|
|
386
420
|
]
|
|
421
|
+
// Before the coverage caveat: an explicit human removal outranks every other
|
|
422
|
+
// statement in this block, and it must not read as one more hedge.
|
|
423
|
+
const stale = removed.filter(r => r.proseStale).map(r => `"${r.label}"`)
|
|
424
|
+
const quiet = removed.filter(r => !r.proseStale).map(r => `"${r.label}"`)
|
|
425
|
+
if (stale.length > 0) {
|
|
426
|
+
lines.push(
|
|
427
|
+
`You removed ${stale.join(', ')} from this meeting. The write-up below was ` +
|
|
428
|
+
'written before that and still uses the name: treat every mention of it as a ' +
|
|
429
|
+
'capture error, not a participant.',
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
if (quiet.length > 0) {
|
|
433
|
+
lines.push(`You also removed ${quiet.join(', ')} from this meeting.`)
|
|
434
|
+
}
|
|
387
435
|
if (coverage !== null && coverage < SHARE_COVERAGE_FLOOR) {
|
|
388
436
|
lines.push(
|
|
389
437
|
`Only ${Math.round(coverage * 100)}% of the voice in this meeting was ` +
|
|
@@ -411,6 +459,10 @@ export interface ClipboardInput {
|
|
|
411
459
|
capturedChars: number
|
|
412
460
|
/** UNION of voiced time, for the crosstalk-overflow note. */
|
|
413
461
|
voicedMs: number
|
|
462
|
+
/** How long the meeting ran. Gates the crosstalk-overflow note. */
|
|
463
|
+
durationMs: number
|
|
464
|
+
/** Names a human explicitly de-attributed from this meeting. */
|
|
465
|
+
removed: RemovedName[]
|
|
414
466
|
title: string
|
|
415
467
|
date: string
|
|
416
468
|
durationMin: number
|
|
@@ -445,9 +497,10 @@ function header(i: ClipboardInput, compact: boolean): string[] {
|
|
|
445
497
|
parts.push(
|
|
446
498
|
'', '## Who spoke',
|
|
447
499
|
renderAttendees(i.attendees, {
|
|
448
|
-
coverage: i.coverage, unattributedMs: i.unattributedMs,
|
|
500
|
+
coverage: i.coverage, unattributedMs: i.unattributedMs,
|
|
501
|
+
voicedMs: i.voicedMs, durationMs: i.durationMs,
|
|
449
502
|
}),
|
|
450
|
-
'', renderProvenance(i.attendees, i.coverage),
|
|
503
|
+
'', renderProvenance(i.attendees, i.coverage, i.removed),
|
|
451
504
|
)
|
|
452
505
|
for (const [heading, body] of [
|
|
453
506
|
['Summary', i.scribe.summary],
|
package/server/routes/meeting.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { Router } from 'express'
|
|
|
8
8
|
import { emitDisplay } from '../lib/display-bus.js'
|
|
9
9
|
import { cleanTranscriptLines } from '../lib/hallucination-filter.js'
|
|
10
10
|
import { durableAtomicWriteFileSync } from '../lib/atomic-fs.js'
|
|
11
|
-
import { appendCorrection, pendingCorrections } from '../lib/meeting-corrections.js'
|
|
11
|
+
import { appendCorrection, appliedCorrections, pendingCorrections } from '../lib/meeting-corrections.js'
|
|
12
12
|
import { isSampleFromSession, untraceableSampleCount } from '../lib/training-audio-provenance.js'
|
|
13
13
|
import { sendAudioFile } from '../lib/send-audio.js'
|
|
14
14
|
import { chunkDiagnostics } from '../lib/chunk-embedding-diagnostics.js'
|
|
@@ -880,10 +880,19 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
|
|
|
880
880
|
coverage,
|
|
881
881
|
// The UNION, never the sum of per-voice figures — crosstalk is counted once.
|
|
882
882
|
unattributedMs: review.unattributedSpeakingMs,
|
|
883
|
-
// Union of ALL voiced time. Only used to notice that the per-voice rows add
|
|
884
|
-
// to more than the meeting, which happens whenever people talk over
|
|
885
|
-
// each other — 34 of 323 real meetings.
|
|
886
883
|
voicedMs: review.voicedMs,
|
|
884
|
+
// The overlap note is gated on how long the meeting RAN. Gating on voiced
|
|
885
|
+
// time tripped on 16 of 23 real meetings.
|
|
886
|
+
durationMs: review.durationMs || 0,
|
|
887
|
+
// Names this human explicitly de-attributed. The ledger has recorded
|
|
888
|
+
// `proseStale` on every applied de-attribution since the feature shipped and
|
|
889
|
+
// NOTHING has ever read it: on 2026-08-07 Miles removed "Clem Ukaoma" from a
|
|
890
|
+
// call that was only him and Queen, all 8 label sites were rewritten, and the
|
|
891
|
+
// summary still opened "Miles, Queen, and Clem talk through..." with no
|
|
892
|
+
// indication anywhere in the payload.
|
|
893
|
+
removed: appliedCorrections(sessionId)
|
|
894
|
+
.filter(r => isUnattributed(r.to))
|
|
895
|
+
.map(r => ({ label: r.from, proseStale: r.proseStale === true })),
|
|
887
896
|
// Which business this is. The sibling /speakers route has always carried
|
|
888
897
|
// this and this one dropped it, so a personal 1:1 about someone's
|
|
889
898
|
// compensation was byte-identical to a marketing sync.
|
|
@@ -921,6 +930,8 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
|
|
|
921
930
|
// no write-up yet.
|
|
922
931
|
capturedChars: clip.capturedChars,
|
|
923
932
|
domain: clip.domain,
|
|
933
|
+
// So the panel can warn above the write-up, not just the clipboard.
|
|
934
|
+
removedNames: clip.removed,
|
|
924
935
|
coverage,
|
|
925
936
|
sharesReported: coverage !== null && coverage >= SHARE_COVERAGE_FLOOR,
|
|
926
937
|
clipboardSummary: summaryText,
|