@gotcos/glasses-server 6.94.0 → 6.94.2

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.
@@ -0,0 +1,105 @@
1
+ // Pure exact-token rendering shared by offline repair and saved Mac finalization.
2
+ type Doc = Record<string, any>
3
+ function refuse(message: string): never { throw new Error(`Mac speaker repair: ${message}`) }
4
+ export interface BoundWord { word: string; before: string; after: string }
5
+ export function boundWords(before: Doc, after: Doc): BoundWord[] {
6
+ if (before.batchApplied !== true || !Array.isArray(before.batchSegments) || !before.batchSegments.length) refuse('v1 requires complete canonical batch speaker words')
7
+ return before.batchSegments.flatMap((segment: Doc, si: number) => {
8
+ if (!Array.isArray(segment.speakerWords) || segment.speakerWords.length !== after.batchSegments[si]?.speakerWords?.length) refuse('incomplete batch speaker words')
9
+ return segment.speakerWords.flatMap((row: Doc, wi: number) => {
10
+ const next = after.batchSegments[si].speakerWords[wi]
11
+ if (typeof row.word !== 'string' || typeof row.speaker !== 'string' || typeof next.speaker !== 'string') refuse('invalid canonical word binding')
12
+ return row.word.match(/\S+/gu)?.map((word: string) => ({ word, before: row.speaker, after: next.speaker })) ?? []
13
+ })
14
+ })
15
+ }
16
+ /** Exact ordered tokens + exact old labels, no nearest/anchor/substring guessing.
17
+ * Spoken token bytes and original separators remain; only prefixes are replaced
18
+ * or inserted when an existing turn must split at a proven word boundary.
19
+ */
20
+ export function renderBoundSpeakerLabels(transcript: string, words: BoundWord[], preserveUnlabelled = false): string {
21
+ let pointer = 0
22
+ const result = transcript.split('\n').map(line => {
23
+ const match = /^(\[([^\]\r\n]+)\]:[ \t]*)(.*)$/.exec(line)
24
+ if (!match) { if (line.trim() && !preserveUnlabelled) refuse('unbound nonempty transcript line'); return line }
25
+ const tokens = [...match[3].matchAll(/\S+/gu)]
26
+ if (!tokens.length) return line
27
+ let out = '', previous: string | null = null, consumed = 0
28
+ for (const token of tokens) {
29
+ const bound = words[pointer++]
30
+ if (!bound || bound.word !== token[0] || bound.before !== match[2]) refuse('transcript words/labels do not exactly match canonical evidence')
31
+ const offset = token.index!
32
+ out += match[3].slice(consumed, offset)
33
+ if (previous === null) out = match[1].replace(`[${match[2]}]`, `[${bound.after}]`) + out
34
+ else if (previous !== bound.after) out += `\n[${bound.after}]: `
35
+ out += token[0]; consumed = offset + token[0].length; previous = bound.after
36
+ }
37
+ return out + match[3].slice(consumed)
38
+ }).join('\n')
39
+ if (pointer !== words.length) refuse('transcript has missing canonical words')
40
+ return result
41
+ }
42
+ /** Omitted/cleaned unaffected lines may stay. Every changed canonical token
43
+ * must still be covered by exactly one full-line, exact-token + old-label join. */
44
+ export function renderSparseBoundSpeakerLabels(transcript: string, words: BoundWord[]): string {
45
+ const covered = new Set<number>()
46
+ let lastEnd = 0
47
+ const out = transcript.split('\n').map(line => {
48
+ const match = /^(\[([^\]\r\n]+)\]:[ \t]*)(.*)$/.exec(line)
49
+ if (!match) return line
50
+ const tokens = match[3].match(/\S+/gu) ?? []
51
+ if (!tokens.length) return line
52
+ const candidates: number[] = []
53
+ for (let start = 0; start <= words.length - tokens.length; start++) {
54
+ if (tokens.every((token, offset) => words[start + offset].word === token && words[start + offset].before === match[2])) candidates.push(start)
55
+ }
56
+ const affects = (start: number) => words.slice(start, start + tokens.length).some(word => word.before !== word.after)
57
+ if (candidates.length !== 1) {
58
+ if (candidates.some(affects)) refuse('changed markdown line has ambiguous canonical binding')
59
+ return line
60
+ }
61
+ const start = candidates[0], end = start + tokens.length
62
+ if (start < lastEnd) refuse('markdown canonical line order disagrees')
63
+ lastEnd = end
64
+ for (let index = start; index < end; index++) covered.add(index)
65
+ return affects(start) ? renderBoundSpeakerLabels(line, words.slice(start, end)) : line
66
+ }).join('\n')
67
+ if (words.some((word, index) => word.before !== word.after && !covered.has(index))) refuse('changed canonical words lack unique complete markdown binding')
68
+ return out
69
+ }
70
+ /** The operations importer writes this literal marker after a character-limited
71
+ * canonical preview. Only an exact ordered prefix may waive omitted tail words. */
72
+ export function renderTruncatedBoundSpeakerLabels(body: string, words: BoundWord[]): string {
73
+ const marker = '... [Transcript truncated for length]'
74
+ const position = body.indexOf(marker)
75
+ if (position < 0 || body.indexOf(marker, position + marker.length) >= 0) refuse('one explicit transcript truncation marker required')
76
+ const shown = body.slice(0, position), suffix = body.slice(position)
77
+ if (/^\[[^\]\r\n]+\]:/m.test(suffix)) refuse('spoken lines follow transcript truncation marker')
78
+ const tokens: Array<{word: string; label: string}> = []
79
+ for (const line of shown.split('\n')) {
80
+ const match = /^\[([^\]\r\n]+)\]:[ \t]*(.*)$/.exec(line)
81
+ if (match) for (const word of match[2].match(/\S+/gu) ?? []) tokens.push({word, label:match[1]})
82
+ }
83
+ if (!tokens.length || tokens.length >= words.length) refuse('truncation marker does not describe a canonical prefix')
84
+ const visible = tokens.map((token, index) => {
85
+ const bound = words[index]
86
+ const partial = index === tokens.length - 1 && bound.word.startsWith(token.word)
87
+ if (bound.before !== token.label || (bound.word !== token.word && !partial)) refuse('truncated preview is not an exact canonical prefix')
88
+ return {...bound, word:token.word}
89
+ })
90
+ return renderBoundSpeakerLabels(shown, visible, true) + suffix
91
+ }
92
+ export function renderMarkdown(markdown: string, words: BoundWord[]): string {
93
+ const headings = [...markdown.matchAll(/^## Transcript[^\n]*\n/gm)]
94
+ if (headings.length !== 1) refuse('exactly one Transcript section required')
95
+ const start = headings[0].index! + headings[0][0].length
96
+ const next = /^## /m.exec(markdown.slice(start)), end = next ? start + next.index : markdown.length
97
+ const body = markdown.slice(start, end)
98
+ let rendered: string
99
+ if (body.includes('... [Transcript truncated for length]')) rendered = renderTruncatedBoundSpeakerLabels(body, words)
100
+ else {
101
+ try { rendered = renderBoundSpeakerLabels(body, words, true) }
102
+ catch { rendered = renderSparseBoundSpeakerLabels(body, words) }
103
+ }
104
+ return markdown.slice(0, start) + rendered + markdown.slice(end)
105
+ }
@@ -0,0 +1,239 @@
1
+ // Pure preparation/qualification only. The caller verifies WAVs, runs extraction,
2
+ // authenticates these receipts and applies reviewed exact-span candidates.
3
+ import { createHash } from 'node:crypto'
4
+ import { isVoiceLabel } from './mac-voice-labels.js'
5
+ import { hasMacSpeakerProtectedRows } from './mac-speaker-protection.js'
6
+ import {
7
+ createMacSpeakerMemory, matchMacSpeakerProfile, normalizedSpeakerCosine, normalizedSpeakerEmbedding,
8
+ reconcileMacSpeakerObservation, type MacSpeakerObservation, type MacSpeakerProfile,
9
+ } from './mac-speaker-reconcile.js'
10
+ import type { SidecarEntry } from './nemotron-final.js'
11
+ const SR = 16_000
12
+ const SHA = /^[a-f0-9]{64}$/
13
+ const uint = (n: unknown): n is number => Number.isSafeInteger(n) && Number(n) >= 0
14
+ const samplePosition = (value: number): number => Number.isFinite(value) && Math.abs(value - Math.round(value)) <= 1e-6 ? Math.round(value) : Number.NaN
15
+ const hash = (v: unknown): string => createHash('sha256').update(JSON.stringify(v)).digest('hex')
16
+ export const MAC_SPEAKER_WINDOW_VERSION = 'exact-window-v1'
17
+ export interface VerifiedMacRawChunk {
18
+ chunkIndex: number
19
+ audioSHA256: string
20
+ pcmSHA256: string
21
+ sampleRate: 16000
22
+ channels: 1
23
+ format: 'pcm_s16le'
24
+ sampleCount: number
25
+ verified: true
26
+ }
27
+ export interface MacWindowSource extends VerifiedMacRawChunk {
28
+ startSample: number
29
+ endSample: number
30
+ /** Automatic segment span coverage, not independent VAD/purity ground truth. */
31
+ speechSamples: number
32
+ textSHA256: string
33
+ segmentsSHA256: string
34
+ }
35
+ export interface PreparedMacSpeakerWindow {
36
+ id: string
37
+ captureId: string
38
+ originalVoiceHint: string
39
+ startSample: number
40
+ endSample: number
41
+ sampleCount: number
42
+ speechSamples: number
43
+ sources: MacWindowSource[]
44
+ bindingSHA256: string
45
+ }
46
+ export interface MacSpeakerWindowPlan {
47
+ version: typeof MAC_SPEAKER_WINDOW_VERSION
48
+ captureId: string
49
+ /** These families are candidate scheduling hints, never speaker identities. */
50
+ families: { originalVoiceHint: string; windowIds: string[] }[]
51
+ windows: PreparedMacSpeakerWindow[]
52
+ rejected: { chunkIndexes: number[]; reason: string }[]
53
+ thresholds: { windowSamples: number; minimumWindowSamples: number; minimumSpeechSamples: number }
54
+ }
55
+ export interface MacSpeakerWindowInput {
56
+ sidecar: { sessionId: string; correctionRevision?: number; chunkEntries?: SidecarEntry[] }
57
+ rawChunks: VerifiedMacRawChunk[]
58
+ /** Includes ledger-only legacy corrections; unavailable must fail closed. */
59
+ correctionHistory: 'verified-clear' | 'human-corrected' | 'unavailable'
60
+ correctedChunkIndexes?: readonly number[]
61
+ minimumSpeechSamples?: number
62
+ }
63
+ function sourceOf(entry: SidecarEntry, raw: VerifiedMacRawChunk | undefined): { source?: MacWindowSource; hint?: string; reason?: string } {
64
+ const c = entry?.chunk
65
+ if (!c || !raw || !uint(entry.chunkIndex) || raw.chunkIndex !== entry.chunkIndex || raw.verified !== true
66
+ || raw.sampleRate !== SR || raw.channels !== 1 || raw.format !== 'pcm_s16le' || !uint(raw.sampleCount) || !raw.sampleCount || raw.sampleCount > 480_000
67
+ || !SHA.test(raw.audioSHA256) || !SHA.test(raw.pcmSHA256) || c.audioSha256 !== raw.audioSHA256) return { reason: 'unverified_raw_binding' }
68
+ if (c.speakerPolicy !== 'high-confidence' || !isVoiceLabel(c.speaker) || typeof c.text !== 'string' || !c.text.trim()) return { reason: 'not_automatic_homogeneous' }
69
+ if (hasMacSpeakerProtectedRows(c)) return { reason: 'human_corrected' }
70
+ const segments = c.segments
71
+ if (!Array.isArray(segments) || !segments.length || segments.length > 1024 || segments.some(s => s?.speaker !== c.speaker)
72
+ || segments.map(s => s.text).join(' ').trim().split(/\s+/).join(' ') !== c.text.trim().split(/\s+/).join(' ')) return { reason: 'not_automatic_homogeneous' }
73
+ const startSample = samplePosition(Number(c.elapsed) * SR / 1000)
74
+ if (!uint(startSample) || !Number.isSafeInteger(startSample + raw.sampleCount)) return { reason: 'bad_clock' }
75
+ let end = 0, speechSamples = 0
76
+ for (const s of segments) {
77
+ const a = samplePosition(Number(s.startSec) * SR), b = samplePosition(Number(s.endSec) * SR)
78
+ if (!uint(a) || !uint(b) || a < end || b <= a || b > raw.sampleCount || (s as unknown as Record<string, unknown>).overlap === true || (s as unknown as Record<string, unknown>).mixedSpeakers === true) return { reason: 'mixed_or_invalid_segments' }
79
+ speechSamples += b - a; end = b
80
+ }
81
+ return { source: { ...structuredClone(raw), startSample, endSample: startSample + raw.sampleCount, speechSamples,
82
+ textSHA256: hash(c.text), segmentsSHA256: hash(segments) }, hint: c.speaker }
83
+ }
84
+ /** Non-overlapping original chunks only. No cross-label concatenation, gap
85
+ * filling, arbitrary crops, inferred names or automatic enrollment. */
86
+ export function planMacSpeakerWindows(input: MacSpeakerWindowInput): MacSpeakerWindowPlan {
87
+ const captureId = input.sidecar.sessionId
88
+ const minimumSpeechSamples = input.minimumSpeechSamples ?? 320_000
89
+ const plan: MacSpeakerWindowPlan = { version: MAC_SPEAKER_WINDOW_VERSION, captureId, families: [], windows: [], rejected: [], thresholds: { windowSamples: 480_000, minimumWindowSamples: 448_000, minimumSpeechSamples } }
90
+ if (typeof captureId !== 'string' || !captureId || captureId.length > 200 || !uint(minimumSpeechSamples) || minimumSpeechSamples < 32_000 || minimumSpeechSamples > 480_000) throw new Error('mac_window_input')
91
+ const entries = input.sidecar.chunkEntries
92
+ if (!Array.isArray(entries) || entries.length > 65536 || !Array.isArray(input.rawChunks) || input.rawChunks.length > 65536) throw new Error('mac_window_bound')
93
+ const refuse = (indexes: number[], reason: string) => { plan.rejected.push({ chunkIndexes: indexes, reason }) }
94
+ if (input.correctionHistory !== 'verified-clear' || !uint(input.sidecar.correctionRevision ?? 0) || Number(input.sidecar.correctionRevision ?? 0) > 0) {
95
+ refuse(entries.map(e => e.chunkIndex), 'human_history_not_clear'); return plan
96
+ }
97
+ const raw = new Map<number, VerifiedMacRawChunk>()
98
+ const duplicates = new Set<number>()
99
+ for (const r of input.rawChunks) { if (raw.has(r.chunkIndex)) duplicates.add(r.chunkIndex); raw.set(r.chunkIndex, r) }
100
+ const entryCounts = new Map<number, number>()
101
+ for (const e of entries) entryCounts.set(e.chunkIndex, (entryCounts.get(e.chunkIndex) ?? 0) + 1)
102
+ for (const [index, count] of entryCounts) if (count > 1) duplicates.add(index)
103
+ const seenChunks = new Set<number>(), seenAudio = new Set<string>(), seenPCM = new Set<string>()
104
+ const corrected = new Set(input.correctedChunkIndexes ?? [])
105
+ let pending: MacWindowSource[] = [], hint = ''
106
+ let lastSource: MacWindowSource | undefined
107
+ const flush = (reason: string) => { if (pending.length) refuse(pending.map(s => s.chunkIndex), reason); pending = []; hint = '' }
108
+ const finish = () => {
109
+ const first = pending[0], last = pending.at(-1)!
110
+ const sampleCount = pending.reduce((n, s) => n + s.sampleCount, 0)
111
+ const speechSamples = pending.reduce((n, s) => n + s.speechSamples, 0)
112
+ if (speechSamples < minimumSpeechSamples) { flush('insufficient_speech'); return }
113
+ const body = { captureId, originalVoiceHint: hint, startSample: first.startSample, endSample: last.endSample, sampleCount, speechSamples, sources: pending }
114
+ const bindingSHA256 = hash(body)
115
+ plan.windows.push({ ...structuredClone(body), id: `window_${bindingSHA256}`, bindingSHA256 })
116
+ pending = []; hint = ''
117
+ }
118
+ const close = (reason: string) => {
119
+ if (pending.reduce((n, s) => n + s.sampleCount, 0) >= plan.thresholds.minimumWindowSamples) finish()
120
+ else flush(reason)
121
+ }
122
+ for (const entry of entries) {
123
+ const index = entry?.chunkIndex
124
+ if (seenChunks.has(index) || duplicates.has(index)) { close('duplicate_raw_index'); refuse([index], 'duplicate_raw_index'); continue }
125
+ seenChunks.add(index)
126
+ const candidate = sourceOf(entry, raw.get(index))
127
+ if (corrected.has(index) || candidate.reason) { close('interrupted_window'); refuse([index], corrected.has(index) ? 'human_corrected' : candidate.reason!); continue }
128
+ const source = candidate.source!
129
+ if (seenAudio.has(source.audioSHA256) || seenPCM.has(source.pcmSHA256)) { close('duplicate_audio'); refuse([index], 'duplicate_audio'); continue }
130
+ seenAudio.add(source.audioSHA256); seenPCM.add(source.pcmSHA256)
131
+ if (lastSource && source.startSample < lastSource.endSample) { close('clock_overlap'); refuse([index], 'clock_overlap'); continue }
132
+ lastSource = source
133
+ if (pending.length && (source.startSample !== pending.at(-1)!.endSample || index !== pending.at(-1)!.chunkIndex + 1)) close('clock_or_raw_gap')
134
+ if (pending.length && candidate.hint !== hint) close('hint_changed')
135
+ if (pending.reduce((n, s) => n + s.sampleCount, 0) + source.sampleCount > 480_000) close('partial_window')
136
+ if (pending.length >= 240) close('source_count_bound')
137
+ hint = candidate.hint!; pending.push(source)
138
+ if (pending.reduce((n, s) => n + s.sampleCount, 0) === 480_000) finish()
139
+ }
140
+ close('partial_window')
141
+ const families = new Map<string, string[]>()
142
+ for (const w of plan.windows) { const ids = families.get(w.originalVoiceHint) ?? []; ids.push(w.id); families.set(w.originalVoiceHint, ids) }
143
+ plan.families = [...families].map(([originalVoiceHint, windowIds]) => ({ originalVoiceHint, windowIds }))
144
+ return plan
145
+ }
146
+ export interface MacWindowEmbeddingReceipt {
147
+ windowId: string
148
+ windowBindingSHA256: string
149
+ modelSHA256: string
150
+ /** Verified bytes of the exact prepared concatenation, not a crop. */
151
+ audioSHA256: string
152
+ pcmSHA256: string
153
+ sampleCount: number
154
+ embedding: readonly number[]
155
+ /** Sensitivity evidence only, never two additional independent observations. */
156
+ halfEmbeddings?: readonly [readonly number[], readonly number[]]
157
+ }
158
+ export interface QualifiedMacWindowCandidate {
159
+ windowId: string
160
+ startSample: number
161
+ endSample: number
162
+ originalVoiceHint: string
163
+ status: 'qualified' | 'preserve'
164
+ reason: string
165
+ /** Opaque cohort identity. Never a global rename of originalVoiceHint. */
166
+ acousticIdentity?: string
167
+ profileName?: string
168
+ evidence: string[]
169
+ provenance: { algorithm: typeof MAC_SPEAKER_WINDOW_VERSION; modelSHA256: string; profileSHA256?: string; anonymousCosine: .90; anonymousMargin: .06; identityCosine: .88; identityMargin: .05 }
170
+ }
171
+ function preparedWindowValid(w: PreparedMacSpeakerWindow, plan: MacSpeakerWindowPlan): boolean {
172
+ if (!w || w.captureId !== plan.captureId || !isVoiceLabel(w.originalVoiceHint) || !uint(w.startSample) || !uint(w.endSample)
173
+ || !uint(w.sampleCount) || w.sampleCount < 448_000 || w.sampleCount > 480_000 || w.endSample - w.startSample !== w.sampleCount || !uint(w.speechSamples)
174
+ || w.speechSamples < plan.thresholds.minimumSpeechSamples || w.speechSamples > w.sampleCount || !Array.isArray(w.sources) || !w.sources.length || w.sources.length > 240) return false
175
+ let samples = 0, speech = 0
176
+ for (let i = 0; i < w.sources.length; i++) {
177
+ const s = w.sources[i], previous = w.sources[i - 1]
178
+ if (!s || s.verified !== true || s.sampleRate !== SR || s.channels !== 1 || s.format !== 'pcm_s16le' || !uint(s.chunkIndex)
179
+ || !uint(s.startSample) || !uint(s.endSample) || !uint(s.sampleCount) || s.sampleCount <= 0 || s.endSample - s.startSample !== s.sampleCount
180
+ || !uint(s.speechSamples) || s.speechSamples > s.sampleCount || !SHA.test(s.audioSHA256) || !SHA.test(s.pcmSHA256) || !SHA.test(s.textSHA256) || !SHA.test(s.segmentsSHA256)
181
+ || (previous && (s.chunkIndex !== previous.chunkIndex + 1 || s.startSample !== previous.endSample))) return false
182
+ samples += s.sampleCount; speech += s.speechSamples
183
+ }
184
+ return samples === w.sampleCount && speech === w.speechSamples && w.sources[0].startSample === w.startSample && w.sources.at(-1)!.endSample === w.endSample
185
+ }
186
+
187
+ /** Cohorts require independent whole windows. Old Voice labels supply no votes.
188
+ * A qualified candidate is a proposal for this exact span, never permission to
189
+ * rename every occurrence of its hint. Frozen profiles are only read. */
190
+ export function qualifyMacSpeakerWindows(plan: MacSpeakerWindowPlan, receipts: readonly MacWindowEmbeddingReceipt[], profiles: readonly MacSpeakerProfile[], modelSHA256: string, dimensions = 192): QualifiedMacWindowCandidate[] {
191
+ if (plan.version !== MAC_SPEAKER_WINDOW_VERSION || plan.thresholds.windowSamples !== 480_000 || plan.thresholds.minimumWindowSamples !== 448_000 || !uint(plan.thresholds.minimumSpeechSamples) || plan.thresholds.minimumSpeechSamples < 32_000 || plan.thresholds.minimumSpeechSamples > 480_000 || !SHA.test(modelSHA256) || !uint(dimensions) || !dimensions || dimensions > 4096 || plan.windows.length > 8192 || receipts.length > 8192) throw new Error('mac_window_qualification_input')
192
+ let memory = createMacSpeakerMemory(plan.captureId, modelSHA256, { dimensions, maxIdentities: 128, maxOriginals: 8192 })
193
+ const byId = new Map<string, MacWindowEmbeddingReceipt>(), duplicate = new Set<string>()
194
+ for (const r of receipts) { if (byId.has(r.windowId)) duplicate.add(r.windowId); byId.set(r.windowId, r) }
195
+ const candidates: QualifiedMacWindowCandidate[] = []
196
+ const cohorts = new Map<number, { index: number; observation: MacSpeakerObservation }[]>()
197
+ const seenSources = new Set<number>(), seenRawPCM = new Set<string>(), seenRawAudio = new Set<string>()
198
+ for (let i = 0; i < plan.windows.length; i++) {
199
+ const w = plan.windows[i], r = byId.get(w.id)
200
+ const provenance: QualifiedMacWindowCandidate['provenance'] = { algorithm: MAC_SPEAKER_WINDOW_VERSION, modelSHA256, anonymousCosine: .90 as const, anonymousMargin: .06 as const, identityCosine: .88 as const, identityMargin: .05 as const }
201
+ const candidate: QualifiedMacWindowCandidate = { windowId: w.id, startSample: w.startSample, endSample: w.endSample, originalVoiceHint: w.originalVoiceHint, status: 'preserve', reason: 'missing_embedding', evidence: [], provenance }
202
+ candidates.push(candidate)
203
+ const { id: _id, bindingSHA256, ...body } = w
204
+ if (!r) continue
205
+ if (!preparedWindowValid(w, plan) || duplicate.has(w.id) || r.modelSHA256 !== modelSHA256 || r.windowBindingSHA256 !== bindingSHA256 || hash(body) !== bindingSHA256 || w.id !== `window_${bindingSHA256}`
206
+ || w.captureId !== plan.captureId || !uint(w.sampleCount) || w.sampleCount < 448_000 || w.sampleCount > 480_000 || w.endSample - w.startSample !== w.sampleCount || r.sampleCount !== w.sampleCount
207
+ || !SHA.test(r.audioSHA256) || !SHA.test(r.pcmSHA256) || !normalizedSpeakerEmbedding(r.embedding, dimensions)
208
+ || w.sources.some(s => seenSources.has(s.chunkIndex) || seenRawPCM.has(s.pcmSHA256) || seenRawAudio.has(s.audioSHA256))) { candidate.reason = 'receipt_or_lineage_conflict'; continue }
209
+ for (const source of w.sources) { seenSources.add(source.chunkIndex); seenRawPCM.add(source.pcmSHA256); seenRawAudio.add(source.audioSHA256) }
210
+ if (r.halfEmbeddings && (r.halfEmbeddings.length !== 2 || r.halfEmbeddings.some(v => !normalizedSpeakerEmbedding(v, dimensions))
211
+ || normalizedSpeakerCosine(r.halfEmbeddings[0], r.halfEmbeddings[1])! < .90
212
+ || r.halfEmbeddings.some(v => normalizedSpeakerCosine(v, r.embedding)! < .90))) { candidate.reason = 'incoherent_halves'; continue }
213
+ const observation: MacSpeakerObservation = { captureId: plan.captureId, modelSHA256,
214
+ original: { chunkIndex: i, audioSHA256: r.audioSHA256, pcmSHA256: r.pcmSHA256, startSample: w.startSample, sampleCount: w.sampleCount },
215
+ startSample: w.startSample, endSample: w.endSample, exclusiveSamples: w.speechSamples, purity: 1, overlapSamples: 0, mixedSpeakers: false, embedding: r.embedding }
216
+ const next = reconcileMacSpeakerObservation(memory, observation)
217
+ memory = next.state; candidate.reason = next.decision.reason; candidate.evidence = next.decision.evidence
218
+ if (next.decision.identityId !== undefined) {
219
+ const group = cohorts.get(next.decision.identityId) ?? []; group.push({ index: i, observation }); cohorts.set(next.decision.identityId, group)
220
+ }
221
+ }
222
+ for (const [identity, group] of cohorts) {
223
+ if (memory.identities[identity - 1]?.voice === null || group.length < 2) continue
224
+ if (group.some(a => group.some(b => normalizedSpeakerCosine(a.observation.embedding, b.observation.embedding)! < .90))) {
225
+ for (const item of group) candidates[item.index].reason = 'inconsistent_complete_link'
226
+ continue
227
+ }
228
+ const acousticIdentity = `acoustic_${hash(group.map(g => plan.windows[g.index].bindingSHA256).sort())}`
229
+ const proposal = matchMacSpeakerProfile(memory, group.map(g => g.observation), profiles)
230
+ const evidence = group.map(g => plan.windows[g.index].bindingSHA256)
231
+ for (const item of group) {
232
+ const c = candidates[item.index]
233
+ c.status = 'qualified'; c.reason = proposal.reason === 'matched_profile' ? 'qualified_profile' : 'qualified_anonymous'
234
+ c.acousticIdentity = acousticIdentity; c.evidence = evidence
235
+ if (proposal.reason === 'matched_profile') { c.profileName = proposal.label!; c.provenance.profileSHA256 = proposal.provenance.profileSHA256 }
236
+ }
237
+ }
238
+ return candidates
239
+ }
@@ -276,6 +276,9 @@ export interface AttendeeLine {
276
276
  */
277
277
  isOwner: boolean
278
278
  confirmedByHuman: boolean
279
+ /** Qualified exact-span machine evidence; raw capture scores stay unchanged. */
280
+ machineQualified?: boolean
281
+ machineQualifiedSegments?: number
279
282
  }
280
283
 
281
284
  /**
@@ -406,8 +409,12 @@ export function renderProvenance(
406
409
  const clauses: string[] = []
407
410
  const owner = named.filter(v => v.isOwner).map(q)
408
411
  const human = named.filter(v => !v.isOwner && v.confirmedByHuman).map(q)
409
- const matched = named.filter(v => !v.isOwner && !v.confirmedByHuman).map(q)
412
+ const machine = named.filter(v => !v.isOwner && !v.confirmedByHuman && v.machineQualified).map(q)
413
+ const combined = named.filter(v => !v.isOwner && !v.confirmedByHuman && !v.machineQualified && (v.machineQualifiedSegments ?? 0) > 0).map(q)
414
+ const matched = named.filter(v => !v.isOwner && !v.confirmedByHuman && !v.machineQualified && !(v.machineQualifiedSegments ?? 0)).map(q)
410
415
  if (owner.length) clauses.push(`${owner.join(', ')} by wearing the device`)
416
+ if (machine.length) clauses.push(`${machine.join(', ')} by qualified independent long-window profile evidence (not human confirmation)`)
417
+ if (combined.length) clauses.push(`${combined.join(', ')} by capture matches plus qualified independent long-window profile evidence (not human confirmation)`)
411
418
  if (matched.length) clauses.push(`${matched.join(', ')} by voice match`)
412
419
  if (human.length) clauses.push(`${human.join(', ')} because a human named that voice`)
413
420
  const lines: string[] = [
@@ -22,6 +22,7 @@
22
22
  //
23
23
  // Everything here is pure over a chunk array so it can be tested by execution.
24
24
 
25
+ import type { QualifiedMachinePosition } from './mac-speaker-machine-evidence.js'
25
26
  import { isVoiceLabel } from './mac-voice-labels.js'
26
27
 
27
28
  /** One transcript chunk as stored in the `.g2-chunks.json` sidecar. */
@@ -152,6 +153,10 @@ export interface VoiceReview {
152
153
  isOwner: boolean
153
154
  /** A human confirmed this label for this meeting; the floor is waived. */
154
155
  confirmedByHuman: boolean
156
+ /** Independent long-window profile evidence, never a human confirmation. */
157
+ machineQualified: boolean
158
+ machineQualifiedSegments: number
159
+ assertionSource: 'owner' | 'human' | 'qualified-profile' | 'capture-and-qualified-profile' | 'capture-match' | null
155
160
  reliability: Reliability
156
161
  /**
157
162
  * Whether `label` may be shown to a human AS A NAME.
@@ -588,6 +593,9 @@ export function reviewMeetingSpeakers(
588
593
  */
589
594
  confirmed?: Set<string>
590
595
  confirmedChunks?: Map<number, string>
596
+ /** Reader-validated exact compacted positions from current bound raw audio. */
597
+ machineEvidenceStatus?: 'absent' | 'qualified' | 'invalid'
598
+ machineQualifiedPositions?: ReadonlyMap<number, QualifiedMachinePosition>
591
599
  /**
592
600
  * The sidecar's `batchSegments`, when the HQ pass has run. Their word
593
601
  * timings give real voiced time; without them speaking time falls back to
@@ -646,6 +654,24 @@ export function reviewMeetingSpeakers(
646
654
  const voices: VoiceReview[] = labels.map(label => {
647
655
  const own = chunks.filter(c => (c.speaker ?? '') === label)
648
656
  const sims = own.map(c => c.similarity).filter((s): s is number => typeof s === 'number' && s > 0)
657
+ const machineQualifiedSegments = chunks.reduce((n, c, i) => {
658
+ const proof = options.machineQualifiedPositions?.get(i)
659
+ return n + Number(c.speaker === label && !!proof && proof.label === label
660
+ && (c.chunkIndex === undefined || c.chunkIndex === proof.rawChunkIndex)
661
+ && proof.independentWindows >= 2 && proof.minimumSimilarity >= .88 && proof.minimumMargin >= .05)
662
+ }, 0)
663
+ const machineQualified = own.length > 0 && machineQualifiedSegments === own.length
664
+ // Repaired records require support for every occurrence. A legacy mean
665
+ // from one positive row cannot vouch for repaired zero-score positions.
666
+ const scopedMachineRecord = (options.machineEvidenceStatus !== undefined && options.machineEvidenceStatus !== 'absent') || (options.machineQualifiedPositions?.size ?? 0) > 0
667
+ const unsupportedScopedSegments = scopedMachineRecord ? chunks.reduce((n, c, i) => {
668
+ if (c.speaker !== label) return n
669
+ const proof = options.machineQualifiedPositions?.get(i)
670
+ const machine = proof?.label === label && (c.chunkIndex === undefined || c.chunkIndex === proof.rawChunkIndex) && proof.independentWindows >= 2 && proof.minimumSimilarity >= .88 && proof.minimumMargin >= .05
671
+ const human = confirmed.has(label) || options.confirmedChunks?.get(i) === label
672
+ const capture = typeof c.similarity === 'number' && Number.isFinite(c.similarity) && c.similarity >= ASSERT_MIN_SIMILARITY
673
+ return n + Number(!machine && !human && !capture)
674
+ }, 0) : 0
649
675
  const runs = speakerRuns(sequence, label)
650
676
  const unattributed = isUnattributed(label)
651
677
  const confirmedSegments = unattributed ? 0 : confirmed.has(label) ? own.length
@@ -674,7 +700,7 @@ export function reviewMeetingSpeakers(
674
700
  ? 'unattributed'
675
701
  : thrashesWith.length > 0
676
702
  ? 'unreliable'
677
- : (meanSim ?? 0) >= CONFIDENT_SIMILARITY ? 'confident' : 'weak'
703
+ : machineQualified || (meanSim ?? 0) >= CONFIDENT_SIMILARITY ? 'confident' : 'weak'
678
704
 
679
705
  // What stops this label being presented as a name. Collected as reasons
680
706
  // rather than a bare boolean: "2 segments" and "similarity 0.58" are
@@ -688,6 +714,11 @@ export function reviewMeetingSpeakers(
688
714
  // below 0.65" under a name they personally confirmed would reasonably
689
715
  // conclude the confirmation had not taken. `thrashesWith` still renders,
690
716
  // so a genuinely mixed row is still visibly mixed.
717
+ } else if (unsupportedScopedSegments) {
718
+ // System-audio repairs do not identify the wearer by device ownership.
719
+ // This gate also precedes the legacy owner exemption.
720
+ assertionBlockers.push(`${unsupportedScopedSegments} segment${unsupportedScopedSegments === 1 ? '' : 's'} lack current scoped speaker evidence`)
721
+ if (thrashesWith.length > 0) assertionBlockers.push(`swaps with ${thrashesWith[0].speaker} every ${Math.round(thrashesWith[0].meanRun)} segments`)
691
722
  } else if (label === owner) {
692
723
  // The wearer is exempt. Their identity is established by wearing the
693
724
  // device, not by cosine — and the owner is verified at exactly this floor
@@ -696,6 +727,10 @@ export function reviewMeetingSpeakers(
696
727
  // row read "Unidentified voice" in 4 of 9 meetings, including one with 285
697
728
  // of their own segments. `thrashesWith` still renders, so a mixed row is
698
729
  // still visible — the name is asserted, the caveat is not hidden.
730
+ } else if (machineQualified) {
731
+ // Exact positions passed the independent .88/margin/long-window gate.
732
+ // Keep raw similarities unchanged and retain the mixed-row warning.
733
+ if (thrashesWith.length > 0) assertionBlockers.push(`swaps with ${thrashesWith[0].speaker} every ${Math.round(thrashesWith[0].meanRun)} segments`)
699
734
  } else {
700
735
  if (own.length < ASSERT_MIN_SEGMENTS) {
701
736
  assertionBlockers.push(`only ${own.length} segment${own.length === 1 ? '' : 's'} (needs ${ASSERT_MIN_SEGMENTS})`)
@@ -719,6 +754,9 @@ export function reviewMeetingSpeakers(
719
754
  isOwner: label === owner,
720
755
  /** True when a human vouched for this label in this meeting. */
721
756
  confirmedByHuman: confirmed.has(label),
757
+ machineQualified: !unattributed && machineQualified,
758
+ machineQualifiedSegments: unattributed ? 0 : machineQualifiedSegments,
759
+ assertionSource: assertionBlockers.length ? null : confirmed.has(label) ? 'human' : label === owner ? 'owner' : machineQualified ? 'qualified-profile' : machineQualifiedSegments > 0 ? 'capture-and-qualified-profile' : 'capture-match',
722
760
  reliability,
723
761
  nameAsserted: assertionBlockers.length === 0,
724
762
  assertionBlockers,
@@ -40,6 +40,7 @@ import {
40
40
  nemotronCliPath,
41
41
  prepareNemotronModelsDir,
42
42
  } from './diarizer-backend.js'
43
+ import { readCorrections } from './meeting-corrections.js'
43
44
  import { CONFIDENT_SIMILARITY, isUnattributed } from './meeting-speaker-review.js'
44
45
  import { getOwnerSpeakerLabel } from './profile.js'
45
46
  import { OWNER_VERIFY_SIMILARITY } from './voiceprint-floor.js'
@@ -370,6 +371,24 @@ function liveSpeakerAt(chunk: SidecarChunk, t: number, owner?: string | null): s
370
371
  return best && typeof best.speaker === 'string' ? guardOwnerName(best.speaker, best.similarity, chunk, owner) : null
371
372
  }
372
373
 
374
+ /** Existing strict Mac names are local immutable claims, never a name source
375
+ * for a global anonymous channel. Exact token/word labels win over chunk names.
376
+ * An unplaced named segment withholds anonymous refinement for the whole chunk. */
377
+ function protectedMacName(chunk: SidecarChunk, t: number, exact?: string): { name: string | null; hold: boolean } {
378
+ if (chunk.speakerPolicy !== 'high-confidence') return { name: null, hold: false }
379
+ if (typeof exact === 'string' && isName(exact)) return { name: exact, hold: true }
380
+ const named = (chunk.segments ?? []).filter(s => isName(s.speaker))
381
+ for (const segment of named) {
382
+ if (!Number.isFinite(segment.startSec) || !Number.isFinite(segment.endSec)
383
+ || Number(segment.startSec) < 0 || Number(segment.endSec) <= Number(segment.startSec)) return { name: null, hold: true }
384
+ if (t >= Number(segment.startSec) && t < Number(segment.endSec)) return { name: segment.speaker, hold: true }
385
+ }
386
+ // A segment-less legacy chunk has only its head claim. Explicit anonymous
387
+ // turns, however, must never inherit a real name from the majority/head label.
388
+ if (isName(chunk.speaker) && !chunk.segments?.length) return { name: chunk.speaker, hold: true }
389
+ return { name: null, hold: false }
390
+ }
391
+
373
392
  export interface RelabelStats {
374
393
  total: number
375
394
  /** Words whose label changed. */
@@ -418,13 +437,15 @@ export function relabelBatchSegments(
418
437
  const end = batchWordToTimeline(segment, Number(word.end), timeline)
419
438
  // Existing live anonymous turns are established capture evidence. A
420
439
  // meeting-wide channel may refine unknown speech, never overwrite them.
421
- const live = start ? liveSpeakerAt(start.window.chunk, start.inChunk, owner) : null
422
- let name: string | null = isVoiceLabel(live) ? live : null
423
- if (name === null && start) {
440
+ const liveCandidate = start ? liveSpeakerAt(start.window.chunk, start.inChunk, owner) : null
441
+ const protectedName = start ? protectedMacName(start.window.chunk, start.inChunk, word.speaker) : { name: null, hold: false }
442
+ const live = start?.window.chunk.speakerPolicy === 'high-confidence' && isName(liveCandidate) ? protectedName.name : liveCandidate
443
+ let name: string | null = protectedName.name ?? (isVoiceLabel(live) ? live : null)
444
+ if (name === null && !protectedName.hold && start) {
424
445
  const channel = channelForSpan(a, start.at, Math.max(start.at, end?.window === start.window ? end.at : start.at + 0.01))
425
446
  if (channel !== null && map.has(channel)) { name = map.get(channel)!; stats.viaChannel++ }
426
447
  }
427
- if (isVoiceLabel(live)) stats.kept++
448
+ if (isVoiceLabel(live) || protectedName.name !== null) stats.kept++
428
449
  if (name === null) {
429
450
  stats.kept++
430
451
  name = live
@@ -479,20 +500,34 @@ export function relabelStreamingChunks(
479
500
  if (typeof chunk.text !== 'string' || !chunk.text.trim()) continue
480
501
  const tokens = splitTokens(chunk.text)
481
502
  const times = streamingTokenTimes(chunk, tokens, window, a)
482
- const base = guardOwnerName(typeof chunk.originalSpeaker === 'string' ? chunk.originalSpeaker : chunk.speaker, chunk.similarity, chunk, owner)
503
+ const originalBase = guardOwnerName(typeof chunk.originalSpeaker === 'string' ? chunk.originalSpeaker : chunk.speaker, chunk.similarity, chunk, owner)
504
+ const base = chunk.speakerPolicy === 'high-confidence' && chunk.segments?.length && isName(originalBase) ? 'Ext' : originalBase
483
505
  const labels: string[] = []
484
506
  const liveTokens = (chunk.segments ?? []).flatMap(segment => splitTokens(String(segment.text ?? '')).map(token => ({ token, speaker: segment.speaker })))
485
507
  const exactLive = liveTokens.length === tokens.length && liveTokens.every((part, i) => part.token === tokens[i]) ? liveTokens : null
508
+ // Without token alignment or a clock, preserve the original named chunk
509
+ // rather than spreading a nearby named turn across its unmatched text.
510
+ if (chunk.speakerPolicy === 'high-confidence' && chunk.segments?.some(s => isName(s.speaker)) && !exactLive && !times) continue
486
511
  let carry: string | null = null
487
512
  for (let i = 0; i < tokens.length; i++) {
488
513
  const established = exactLive?.[i].speaker
489
- if (!isWordToken(tokens[i]) || !times) { labels.push(isVoiceLabel(established) ? established! : carry ?? liveSpeakerAt(chunk, 0, owner) ?? base); continue }
514
+ if (!isWordToken(tokens[i]) || !times) {
515
+ const protectedName = !times && established === undefined && chunk.segments?.length ? { name: null, hold: true } : protectedMacName(chunk, times?.[i]?.start ?? 0, established)
516
+ const local = liveSpeakerAt(chunk, times?.[i]?.start ?? 0, owner)
517
+ const safeLocal = chunk.speakerPolicy === 'high-confidence' && isName(local) ? protectedName.name : local
518
+ labels.push(protectedName.name ?? (chunk.speakerPolicy === 'high-confidence' && established !== undefined ? established : isVoiceLabel(established) ? established! : carry ?? safeLocal ?? base))
519
+ continue
520
+ }
490
521
  stats.total++
491
522
  const span = times[i]
492
523
  const channel = channelForSpan(a, window.start + span.start, window.start + Math.max(span.end, span.start + 0.01))
493
524
  let name: string
494
- const live = isVoiceLabel(established) ? established! : liveSpeakerAt(chunk, span.start, owner)
495
- if (isVoiceLabel(live)) { name = live!; stats.kept++ }
525
+ const liveCandidate = isVoiceLabel(established) ? established! : liveSpeakerAt(chunk, span.start, owner)
526
+ const protectedName = protectedMacName(chunk, span.start, established)
527
+ const live = chunk.speakerPolicy === 'high-confidence' && isName(liveCandidate) ? protectedName.name : liveCandidate
528
+ if (protectedName.name !== null) { name = protectedName.name; stats.kept++ }
529
+ else if (protectedName.hold) { name = live ?? base; stats.kept++ }
530
+ else if (isVoiceLabel(live)) { name = live!; stats.kept++ }
496
531
  else if (channel !== null && map.has(channel)) { name = map.get(channel)!; stats.viaChannel++ }
497
532
  else { name = live ?? base; stats.kept++ }
498
533
  labels.push(name)
@@ -787,6 +822,14 @@ function distinctSpeakers(transcript: string): number {
787
822
  return names.size
788
823
  }
789
824
 
825
+ /** Legacy corrections may have a ledger without a sidecar revision. Unknown
826
+ * or incomplete recorded history is held, never treated as automatic evidence. */
827
+ function hasHumanCorrectionHistory(sidecar: Record<string, unknown>): boolean {
828
+ if (typeof sidecar.sessionId !== 'string') return false
829
+ const corrections = readCorrections(sidecar.sessionId)
830
+ return corrections.rows.length > 0 || corrections.unusable > 0
831
+ }
832
+
790
833
  async function finalPassNow(options: FinalPassOptions): Promise<FinalPassOutcome> {
791
834
  const started = (options.now ?? Date.now)()
792
835
  const env = options.env ?? process.env
@@ -801,6 +844,7 @@ async function finalPassNow(options: FinalPassOptions): Promise<FinalPassOutcome
801
844
  const sidecar = JSON.parse(sidecarText) as Record<string, any>
802
845
  const owner = options.owner !== undefined ? options.owner : ownerLabelOrNull()
803
846
  if (Number(sidecar.correctionRevision ?? 0) > 0) return { status: 'skipped', reason: 'human_corrected' }
847
+ if (hasHumanCorrectionHistory(sidecar)) return { status: 'skipped', reason: 'human_corrected' }
804
848
  const markdown = readFileSync(options.meetingPath, 'utf8')
805
849
  const transcript = readTranscriptSection(markdown)
806
850
  if (transcript === null) return { status: 'skipped', reason: 'no_transcript' }
@@ -1025,6 +1069,9 @@ function commitFinalPass(
1025
1069
  return { status: 'skipped', reason: 'changed_during_pass', detail: sidecarNow !== sidecarText ? 'sidecar changed' : 'markdown changed' }
1026
1070
  }
1027
1071
 
1072
+ // A legacy confirmation can land only in its ledger while native work runs.
1073
+ if (hasHumanCorrectionHistory(nextSidecar)) return { status: 'skipped', reason: 'changed_during_pass', detail: 'correction history changed' }
1074
+
1028
1075
  // Sidecar first, markdown second, as the store does: the markdown is the
1029
1076
  // visible commit. If the markdown write fails, put the sidecar back, and say
1030
1077
  // so loudly if even that fails (the two files then disagree).
@@ -1056,8 +1103,8 @@ function commitFinalPass(
1056
1103
  // ── 6.90.0: unnamed, meeting-stable voices for Mac recordings ───────────────────
1057
1104
  //
1058
1105
  // The named pass above maps a channel to a name from chunks the voiceprint was
1059
- // sure of. A Mac recording has no such chunks (every one is `Ext` at the 0.88
1060
- // floor) and must never borrow a name, so it skips that pass (`speaker_policy`).
1106
+ // sure of. Strict Mac recordings never borrow names across a global channel;
1107
+ // rare existing names are preserved locally while other words stay anonymous.
1061
1108
  // This pass keeps the channels and drops the names: each channel that holds the
1062
1109
  // floor in enough chunks becomes `Voice 1`, `Voice 2`, ... in order of first
1063
1110
  // speech. No profile is read or written anywhere below; `owner` is never passed,
@@ -1070,9 +1117,9 @@ export const VOICE_MIN_SUPPORT = FINAL_MIN_SUPPORT
1070
1117
 
1071
1118
  /**
1072
1119
  * Unnamed mode applies to a Mac recording, with the switch on, when every chunk
1073
- * that carries words is the Mac strict policy and none already carries a real
1074
- * name (a 0.88 match is rare, and a pass must never overwrite one with a
1075
- * placeholder).
1120
+ * that carries words is the Mac strict policy. Rare existing names are protected
1121
+ * per span by the relabellers; one match must not disable anonymous refinement
1122
+ * for the remainder of an otherwise strict recording.
1076
1123
  */
1077
1124
  export function unnamedVoicesEligible(
1078
1125
  sidecar: Record<string, unknown>,
@@ -1090,7 +1137,7 @@ export function unnamedVoicesEligible(
1090
1137
  // strict; anything else (anonymous, a malformed marker) still refuses.
1091
1138
  const silentOk = entries.filter(entry => !hasText(entry.chunk))
1092
1139
  .every(entry => entry.chunk?.speakerPolicy === undefined || entry.chunk.speakerPolicy === 'high-confidence')
1093
- return silentOk && spoken.every(chunk => chunk.speakerPolicy === 'high-confidence' && !isName(chunk.speaker))
1140
+ return silentOk && spoken.every(chunk => chunk.speakerPolicy === 'high-confidence')
1094
1141
  }
1095
1142
 
1096
1143
  export interface VoiceChannels {
@@ -1360,6 +1407,9 @@ async function unnamedVoicePass(input: UnnamedInput): Promise<FinalPassOutcome>
1360
1407
  for (const chunk of [entry.chunk, chunks[i]]) {
1361
1408
  const original = typeof chunk.originalSpeaker === 'string' ? chunk.originalSpeaker : chunk.speaker
1362
1409
  if (final) chunk.finalSegments = final.segments
1410
+ // A pre-existing strict name is immutable even when a legacy chunk's
1411
+ // segments disagree. The anonymous pass never asserts a different name.
1412
+ if (chunk.speakerPolicy === 'high-confidence' && isName(chunk.speaker)) continue
1363
1413
  chunk.speaker = label
1364
1414
  if (label !== original) chunk.originalSpeaker = original
1365
1415
  else delete chunk.originalSpeaker