@gotcos/glasses-server 6.36.17 → 6.36.18
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,9 @@
|
|
|
1
|
+
## 6.36.18
|
|
2
|
+
- **Meetings list now carries voice-assignment tags.** Each row includes
|
|
3
|
+
`voiceReview` from the sidecar head (`speakers[]`) plus whether a human
|
|
4
|
+
correction landed in the ledger. Control paints NEW / N to name / REVIEWED
|
|
5
|
+
without opening each meeting. Still a 4 KB head read — not a chunk parse.
|
|
6
|
+
|
|
1
7
|
## 6.36.17
|
|
2
8
|
- **Naming a new person from a wrong existing label now creates their voice profile.**
|
|
3
9
|
Enrolment after `POST /relabel` only fired when `from` was a placeholder (`Ext`,
|
package/package.json
CHANGED
|
@@ -18,6 +18,7 @@ import { basename, dirname, join, resolve } from 'node:path'
|
|
|
18
18
|
import { discoveredDomains, domainAbbreviation as deriveAbbr, isSafeDomainName as safeName } from './domains.js'
|
|
19
19
|
import type { MeetingDetail, MeetingMeta } from './meeting-store.js'
|
|
20
20
|
import { MEETING_SOURCE_MAX_BYTES, meetingDayCountsFromNames, meetingListLimit } from './meeting-store.js'
|
|
21
|
+
import { meetingVoiceReview, parseSidecarListHead } from './meeting-voice-review.js'
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* The four domains of ONE user's COS. Retained as the documented example layout
|
|
@@ -151,30 +152,35 @@ const SIDECAR_HEAD_BYTES = 4096
|
|
|
151
152
|
* them whole would make listing cost scale with total transcript size — and this
|
|
152
153
|
* lister already reads every markdown file it finds.
|
|
153
154
|
*/
|
|
154
|
-
export function
|
|
155
|
+
export function sidecarListHints(monthDir: string, meetingFilename: string): {
|
|
156
|
+
sessionId?: string
|
|
157
|
+
speakers: string[]
|
|
158
|
+
} {
|
|
155
159
|
const sidecarName = meetingFilename.replace(/\.md$/, '.g2-chunks.json')
|
|
156
|
-
if (sidecarName === meetingFilename) return
|
|
160
|
+
if (sidecarName === meetingFilename) return { speakers: [] }
|
|
157
161
|
const path = join(monthDir, sidecarName)
|
|
158
162
|
let fd: number | null = null
|
|
159
163
|
try {
|
|
160
164
|
const linkStat = lstatSync(path)
|
|
161
|
-
if (linkStat.isSymbolicLink() || !linkStat.isFile() || linkStat.size === 0) return
|
|
165
|
+
if (linkStat.isSymbolicLink() || !linkStat.isFile() || linkStat.size === 0) return { speakers: [] }
|
|
162
166
|
const real = realpathSync(path)
|
|
163
|
-
if (dirname(real) !== realpathSync(monthDir)) return
|
|
167
|
+
if (dirname(real) !== realpathSync(monthDir)) return { speakers: [] }
|
|
164
168
|
const stat = statSync(real)
|
|
165
169
|
fd = openSync(path, 'r')
|
|
166
170
|
const buffer = Buffer.alloc(Math.min(SIDECAR_HEAD_BYTES, stat.size))
|
|
167
171
|
const read = readSync(fd, buffer, 0, buffer.length, 0)
|
|
168
|
-
|
|
169
|
-
.match(/"sessionId"\s*:\s*"([A-Za-z0-9:_-]{3,96})"/)
|
|
170
|
-
return match ? match[1] : undefined
|
|
172
|
+
return parseSidecarListHead(buffer.subarray(0, read).toString('utf8'))
|
|
171
173
|
} catch {
|
|
172
|
-
return
|
|
174
|
+
return { speakers: [] }
|
|
173
175
|
} finally {
|
|
174
176
|
if (fd !== null) { try { closeSync(fd) } catch { /* already closed */ } }
|
|
175
177
|
}
|
|
176
178
|
}
|
|
177
179
|
|
|
180
|
+
export function sidecarSessionId(monthDir: string, meetingFilename: string): string | undefined {
|
|
181
|
+
return sidecarListHints(monthDir, meetingFilename).sessionId
|
|
182
|
+
}
|
|
183
|
+
|
|
178
184
|
function envPath(name: string): string | null {
|
|
179
185
|
const raw = process.env[name]?.trim()
|
|
180
186
|
if (!raw) return null
|
|
@@ -563,8 +569,11 @@ export function listCosOperationsMeetings(options: {
|
|
|
563
569
|
meta.recordId = `ops:${domain}:${month}:${file}`
|
|
564
570
|
meta.mutable = true
|
|
565
571
|
meta.canonicalRecord = `operations/${domain}/meetings/${month}/${file}`
|
|
566
|
-
const
|
|
567
|
-
if (sessionId) meta.sessionId = sessionId
|
|
572
|
+
const hints = sidecarListHints(monthDir, file)
|
|
573
|
+
if (hints.sessionId) meta.sessionId = hints.sessionId
|
|
574
|
+
if (hints.speakers.length > 0) {
|
|
575
|
+
meta.voiceReview = meetingVoiceReview(hints.speakers, hints.sessionId)
|
|
576
|
+
}
|
|
568
577
|
if (options.day && meta.date !== options.day) continue
|
|
569
578
|
allMeetings.push(meta)
|
|
570
579
|
} catch { /* skip unreadable files */ }
|
|
@@ -94,6 +94,13 @@ export interface MeetingMeta {
|
|
|
94
94
|
mutable?: boolean
|
|
95
95
|
/** Present only when the server can state a truthful local record. */
|
|
96
96
|
canonicalRecord?: string
|
|
97
|
+
/** Additive. Unique sidecar speakers + whether a human correction landed. */
|
|
98
|
+
voiceReview?: {
|
|
99
|
+
voices: number
|
|
100
|
+
unattributedVoices: number
|
|
101
|
+
namedVoices: number
|
|
102
|
+
humanTouched: boolean
|
|
103
|
+
}
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
export interface MeetingActionItem {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Cheap voice-assignment stats for the meetings LIST.
|
|
2
|
+
//
|
|
3
|
+
// The Speakers panel's "Meetings to review" row cannot open every sidecar just
|
|
4
|
+
// to paint a tag. The unique `speakers` array sits at the top of the sidecar,
|
|
5
|
+
// so a 4 KB head read already used for `sessionId` is enough. Segment counts
|
|
6
|
+
// stay on the per-meeting review route.
|
|
7
|
+
|
|
8
|
+
import { isUnattributed } from './meeting-speaker-review.js'
|
|
9
|
+
import { readCorrections } from './meeting-corrections.js'
|
|
10
|
+
|
|
11
|
+
export interface MeetingVoiceReview {
|
|
12
|
+
voices: number
|
|
13
|
+
unattributedVoices: number
|
|
14
|
+
namedVoices: number
|
|
15
|
+
humanTouched: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseSidecarListHead(head: string): {
|
|
19
|
+
sessionId?: string
|
|
20
|
+
speakers: string[]
|
|
21
|
+
} {
|
|
22
|
+
const sessionId = head.match(/"sessionId"\s*:\s*"([A-Za-z0-9:_-]{3,96})"/)?.[1]
|
|
23
|
+
const block = head.match(/"speakers"\s*:\s*\[([^\]]*)\]/)
|
|
24
|
+
const speakers = block
|
|
25
|
+
? [...block[1].matchAll(/"((?:\\.|[^"\\])*)"/g)].map(match => match[1].replace(/\\"/g, '"'))
|
|
26
|
+
: []
|
|
27
|
+
return { sessionId, speakers }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function meetingVoiceReview(
|
|
31
|
+
speakers: string[],
|
|
32
|
+
sessionId?: string,
|
|
33
|
+
): MeetingVoiceReview {
|
|
34
|
+
const labels = [...new Set(speakers.map(name => name.trim()).filter(Boolean))]
|
|
35
|
+
const unattributedVoices = labels.filter(isUnattributed).length
|
|
36
|
+
return {
|
|
37
|
+
voices: labels.length,
|
|
38
|
+
unattributedVoices,
|
|
39
|
+
namedVoices: Math.max(0, labels.length - unattributedVoices),
|
|
40
|
+
humanTouched: sessionWasHumanTouched(sessionId),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function sessionWasHumanTouched(sessionId?: string): boolean {
|
|
45
|
+
if (!sessionId) return false
|
|
46
|
+
try {
|
|
47
|
+
return readCorrections(sessionId).rows.some(
|
|
48
|
+
row => row.phase === 'applied' || row.phase === 'confirmed',
|
|
49
|
+
)
|
|
50
|
+
} catch {
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
}
|