@gotcos/glasses-server 6.94.1 → 6.94.3

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +27 -0
  3. package/package.json +2 -1
  4. package/server/index.ts +6 -0
  5. package/server/lib/decision-provider.ts +4 -2
  6. package/server/lib/held-naming-batches.ts +8 -7
  7. package/server/lib/held-voice-groups.ts +5 -4
  8. package/server/lib/keyed-sessions.ts +21 -1
  9. package/server/lib/mac-speaker-machine-evidence.ts +111 -0
  10. package/server/lib/mac-speaker-profile-extractor.ts +41 -0
  11. package/server/lib/mac-speaker-profile-finalizer.ts +257 -0
  12. package/server/lib/mac-speaker-protection.ts +22 -0
  13. package/server/lib/mac-speaker-reconcile.ts +300 -0
  14. package/server/lib/mac-speaker-repair-projection.ts +301 -0
  15. package/server/lib/mac-speaker-repair-render.ts +105 -0
  16. package/server/lib/mac-speaker-windows.ts +239 -0
  17. package/server/lib/mac-voice-assignment.ts +2 -1
  18. package/server/lib/meeting-corrections.ts +2 -1
  19. package/server/lib/meeting-relabel-enrolment.ts +2 -1
  20. package/server/lib/meeting-relabel.ts +17 -10
  21. package/server/lib/meeting-scribe-content.ts +8 -1
  22. package/server/lib/meeting-speaker-review.ts +46 -7
  23. package/server/lib/nemotron-final.ts +64 -14
  24. package/server/lib/session-find-ask.ts +281 -0
  25. package/server/lib/session-find-collect.ts +715 -0
  26. package/server/lib/session-find-redact.ts +254 -0
  27. package/server/lib/session-find.ts +345 -0
  28. package/server/lib/speaker-calibration-log.ts +3 -2
  29. package/server/lib/speaker-embeddings.ts +46 -35
  30. package/server/lib/speaker-name-nfc.ts +37 -0
  31. package/server/lib/speaker-rename-fanout.ts +7 -6
  32. package/server/lib/speaker-trainer.ts +2 -1
  33. package/server/lib/voice-directory.ts +7 -2
  34. package/server/lib/voice-profile-store.ts +24 -14
  35. package/server/routes/decision-provider.ts +4 -1
  36. package/server/routes/jev.ts +5 -1
  37. package/server/routes/meeting.ts +43 -15
  38. package/server/routes/session-find.ts +73 -0
  39. package/server/routes/voice.ts +25 -18
  40. package/server/scripts/mac-speaker-repair.ts +324 -0
@@ -0,0 +1,301 @@
1
+ /** Pure, conservative label projection. No file access, capture, inference or enrollment.
2
+ * V1 accepts whole automatic anonymous chunks only. Mixed chunks and words crossing
3
+ * their boundaries require a future, independently reviewed span-level projection.
4
+ * The caller owns durable intents, byte-level file CAS, revisions and rendering.
5
+ */
6
+ import { createHash } from 'node:crypto'
7
+ import { checkSpeakerName } from './speaker-name.js'
8
+ import { hasMacSpeakerProtectedRows, isMacSpeakerHumanProtected as isProtected } from './mac-speaker-protection.js'
9
+
10
+ type Doc = Record<string, any>
11
+ export type MacSpeakerRepairPath = Array<string | number>
12
+ export interface MacSpeakerRepairPatch {
13
+ path: MacSpeakerRepairPath
14
+ before: unknown
15
+ after: unknown
16
+ }
17
+ export interface MacSpeakerWordBinding {
18
+ segmentIndex: number
19
+ wordIndex: number
20
+ from: string
21
+ word: string
22
+ /** Exact segment-relative seconds, not rounded timestamps. */
23
+ start: number
24
+ end: number
25
+ }
26
+ export interface MacProfileMatchEvidence {
27
+ kind: 'profile-match'
28
+ profileName: string
29
+ profileSha256: string
30
+ modelSha256: string
31
+ algorithmVersion: string
32
+ /** Complete-link consistency; every independent pair must meet .90. */
33
+ crossWindowCosines: number[][]
34
+ windows: Array<{
35
+ id: string
36
+ captureStartSample: number
37
+ captureEndSample: number
38
+ pcmSha256: string
39
+ similarity: number
40
+ runnerUpSimilarity: number
41
+ rawChunks: Array<{ rawChunkIndex: number; audioSha256: string; sampleCount: number }>
42
+ }>
43
+ }
44
+ export interface MacSpeakerRepairAssignment {
45
+ rawChunkIndex: number
46
+ from: string
47
+ /** A settled Voice N label, or an existing profile name with targetEvidence. */
48
+ to: string
49
+ evidenceId: string
50
+ batchWords: MacSpeakerWordBinding[]
51
+ targetEvidence?: MacProfileMatchEvidence
52
+ }
53
+ export interface MacSpeakerRepairPlan {
54
+ id: string
55
+ evidenceHash: string
56
+ expectedDocumentHash: string
57
+ /** Explicit ledger-only protection, even for legacy edits without markers. */
58
+ protectedRawChunkIndices: number[]
59
+ /** Verified mono 16 kHz PCM sample counts, including silent/untranscribed raw files. */
60
+ rawAudioSampleCounts: Record<string, number>
61
+ assignments: MacSpeakerRepairAssignment[]
62
+ /** Explicitly withheld words partly outside qualified chunk unions. */
63
+ withheldBatchWords?: Array<MacSpeakerWordBinding & { reason: 'outside-qualified-span' }>
64
+ }
65
+ export interface MacSpeakerRepairReceipt {
66
+ version: 1
67
+ id: string
68
+ evidenceHash: string
69
+ beforeHash: string
70
+ afterHash: string
71
+ rawChunkIndices: number[]
72
+ evidenceIds: string[]
73
+ patches: MacSpeakerRepairPatch[]
74
+ assignments: MacSpeakerRepairAssignment[]
75
+ withheldBatchWords: Array<MacSpeakerWordBinding & { reason: 'outside-qualified-span' }>
76
+ receiptHash: string
77
+ }
78
+ /** Persisted model/profile proof; never a human confirmation or training input. */
79
+ export interface MacSpeakerMachineEvidence {
80
+ version: 1
81
+ correctionRevision: number
82
+ documentBinding: string
83
+ receipt: MacSpeakerRepairReceipt
84
+ rawAudioSampleCounts: Record<string, number>
85
+ }
86
+ /** Exact current speech surfaces, independent of lifecycle/sync metadata and
87
+ * the proof itself. This deliberately binds diagnostics as well as labels. */
88
+ export function hashMacSpeakerMachineBinding(doc: Doc): string {
89
+ return hashMacSpeakerDocument({
90
+ sessionId: doc.sessionId, sourceType: doc.sourceType, captureSource: doc.captureSource,
91
+ startTime: doc.startTime, durationMs: doc.durationMs,
92
+ chunks: doc.chunks, chunkEntries: doc.chunkEntries,
93
+ batchSegments: doc.batchSegments, batchTranscript: doc.batchTranscript, speakers: doc.speakers,
94
+ })
95
+ }
96
+ export function hashMacSpeakerDocument(doc: Doc): string {
97
+ return createHash('sha256').update(JSON.stringify(doc)).digest('hex')
98
+ }
99
+ const equal = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b)
100
+ const anonymous = (label: unknown): label is string => typeof label === 'string' && /^(?:Ext|Unknown|Unidentified(?: \d+)?|Speaker \d+|Voice \d+)$/.test(label)
101
+ const hashPattern = /^[a-f0-9]{64}$/
102
+ function fail(message: string): never { throw new Error(`Mac speaker repair: ${message}`) }
103
+ /** Validate compacted position -> exact ordered raw chunk, without nearest joins. */
104
+ function rawMap(doc: Doc): Array<{ raw: number; entry: number }> {
105
+ if (!Array.isArray(doc.chunks) || !Array.isArray(doc.chunkEntries)) fail('missing raw chunk map')
106
+ const all = doc.chunkEntries.map((entry: any, i: number) => ({ entry, i }))
107
+ if (all.some(({ entry }: any) => !entry || !Number.isInteger(entry.chunkIndex) || entry.chunkIndex < 0)) fail('invalid raw chunk entry')
108
+ const indexes = all.map(({ entry }: any) => entry.chunkIndex)
109
+ if (new Set(indexes).size !== indexes.length || indexes.some((raw: number, i: number) => i > 0 && raw <= indexes[i - 1])) fail('ambiguous raw index ordering')
110
+ const spoken = all.filter(({ entry }: any) => entry.chunk?.text)
111
+ if (spoken.length !== doc.chunks.length || spoken.some(({ entry }: any, i: number) => entry.chunk.text !== doc.chunks[i]?.text)) fail('raw map and ordered text disagree')
112
+ return spoken.map(({ entry, i }: any) => ({ raw: entry.chunkIndex, entry: i }))
113
+ }
114
+ function labelRows(chunk: Doc, path: MacSpeakerRepairPath): Array<{ row: Doc; path: MacSpeakerRepairPath }> {
115
+ const rows = [{ row: chunk, path }]
116
+ for (const key of ['segments', 'finalSegments', 'words']) {
117
+ if (chunk[key] !== undefined && !Array.isArray(chunk[key])) fail(`invalid ${key} surface`)
118
+ for (const [i, row] of (chunk[key] ?? []).entries()) {
119
+ if (!row || typeof row !== 'object') fail(`invalid ${key} row`)
120
+ // Plain ASR words do not necessarily have any speaker field.
121
+ if (key !== 'words' || 'speaker' in row) rows.push({ row, path: [...path, key, i] })
122
+ }
123
+ }
124
+ return rows
125
+ }
126
+ function requireClosed(doc: Doc) {
127
+ if (doc.finalizationState !== 'complete' || doc.claimPending === true || doc.active === true || doc.isActive === true || doc.status === 'recording') fail('capture is not closed and finalized')
128
+ if (doc.sourceType !== 'mac_system_audio' || !/^mac_/.test(doc.sessionId ?? '') || doc.mergedFrom || doc.sourceSessionIds || doc.isMerged) fail('expected an original Mac capture')
129
+ }
130
+ function receiptDigest(receipt: Omit<MacSpeakerRepairReceipt, 'receiptHash'>): string { return hashMacSpeakerDocument(receipt) }
131
+
132
+ export function validateMacSpeakerTargetEvidence(input: Doc, plan: MacSpeakerRepairPlan, assignment: MacSpeakerRepairAssignment) {
133
+ if (/^Voice (?:[1-9]|[1-9]\d)$/.test(assignment.to)) return
134
+ const proof = assignment.targetEvidence
135
+ if (!checkSpeakerName(assignment.to).ok || !assignment.to || assignment.to.trim() !== assignment.to || assignment.to.length > 120 || /[\[\]\r\n]/.test(assignment.to) || anonymous(assignment.to) || !proof || proof.kind !== 'profile-match' || proof.profileName !== assignment.to || !hashPattern.test(proof.profileSha256) || !hashPattern.test(proof.modelSha256) || !proof.algorithmVersion?.trim() || !Array.isArray(proof.windows) || proof.windows.length < 2) fail('named target requires immutable independent profile evidence')
136
+ if (!Array.isArray(proof.crossWindowCosines) || proof.crossWindowCosines.length !== proof.windows.length || proof.crossWindowCosines.some(row => !Array.isArray(row) || row.length !== proof.windows.length)) fail('complete cross-window evidence matrix required')
137
+ for (let i = 0; i < proof.windows.length; i++) for (let j = 0; j < proof.windows.length; j++) {
138
+ const cosine = proof.crossWindowCosines[i][j]
139
+ if (!Number.isFinite(cosine) || cosine < .9 || cosine > 1.000001 || Math.abs(cosine - proof.crossWindowCosines[j][i]) > 1e-8 || (i === j && Math.abs(cosine - 1) > 1e-6)) fail('cross-window evidence is inconsistent')
140
+ }
141
+ const rawSeen = new Set<number>(), pcmSeen = new Set<string>(), ids = new Set<string>(), spans: Array<[number, number]> = []
142
+ let covered = false
143
+ for (const window of proof.windows) {
144
+ if (!window.id || ids.has(window.id) || !hashPattern.test(window.pcmSha256) || pcmSeen.has(window.pcmSha256) || !Number.isSafeInteger(window.captureStartSample) || !Number.isSafeInteger(window.captureEndSample) || window.captureStartSample < 0 || window.captureEndSample <= window.captureStartSample || !Number.isFinite(window.similarity) || window.similarity < .88 || window.similarity > 1 || !Number.isFinite(window.runnerUpSimilarity) || window.runnerUpSimilarity < -1 || window.runnerUpSimilarity > 1 || window.similarity - window.runnerUpSimilarity < .05 || !Array.isArray(window.rawChunks) || !window.rawChunks.length) fail('weak, repeated or invalid profile evidence')
145
+ if (spans.some(([start, end]) => window.captureStartSample < end && window.captureEndSample > start)) fail('overlapping profile evidence windows')
146
+ spans.push([window.captureStartSample, window.captureEndSample]); pcmSeen.add(window.pcmSha256); ids.add(window.id)
147
+ const duration = (window.captureEndSample - window.captureStartSample) / 16000
148
+ if (duration < 28 || duration > 31) fail('profile evidence requires full 28-31s windows')
149
+ let samples = 0, previous = -1, pureSeconds = 0
150
+ for (const raw of window.rawChunks) {
151
+ if (!Number.isInteger(raw.rawChunkIndex) || rawSeen.has(raw.rawChunkIndex) || (previous >= 0 && raw.rawChunkIndex !== previous + 1) || !hashPattern.test(raw.audioSha256) || !Number.isSafeInteger(raw.sampleCount) || raw.sampleCount <= 0 || plan.rawAudioSampleCounts[String(raw.rawChunkIndex)] !== raw.sampleCount) fail('profile evidence raw bindings disagree')
152
+ const chunk = input.chunkEntries.find((entry: Doc) => entry.chunkIndex === raw.rawChunkIndex)?.chunk
153
+ if (!chunk || chunk.audioSha256 !== raw.audioSha256 || !Number.isFinite(chunk.elapsed) || chunk.elapsed * 16 !== window.captureStartSample + samples) fail('profile evidence capture coordinates disagree')
154
+ if (chunk.speakerPolicy === 'anonymous' || !anonymous(chunk.speaker) || hasMacSpeakerProtectedRows(chunk) || labelRows(chunk, []).some(({row}) => isProtected(row) || row.speaker !== chunk.speaker)) fail('profile evidence contains named/manual/mixed or anonymous-policy chunks')
155
+ const segments = chunk.finalSegments?.length ? chunk.finalSegments : chunk.segments
156
+ if (!Array.isArray(segments) || !segments.length) fail('profile evidence lacks automatic pure segment coverage')
157
+ const ranges: Array<[number, number]> = segments.map((segment: Doc) => [segment.startSec, segment.endSec])
158
+ if (ranges.some(([start,end]) => !Number.isFinite(start) || !Number.isFinite(end) || start < 0 || end < start || end > raw.sampleCount / 16000)) fail('profile evidence segment coordinates disagree')
159
+ ranges.sort((a,b)=>a[0]-b[0]); let end = 0
160
+ for (const [start,stop] of ranges) { pureSeconds += Math.max(0,stop-Math.max(start,end)); end=Math.max(end,stop) }
161
+ if (raw.rawChunkIndex === assignment.rawChunkIndex) covered = true
162
+ rawSeen.add(raw.rawChunkIndex); previous = raw.rawChunkIndex; samples += raw.sampleCount
163
+ }
164
+ if (pureSeconds < 20) fail('profile evidence needs at least20s of automatic pure segment coverage')
165
+ if (samples !== window.captureEndSample - window.captureStartSample) fail('profile evidence sample span disagrees')
166
+ }
167
+ if (!covered) fail('assignment lies outside qualified profile evidence')
168
+ }
169
+
170
+ export function projectMacSpeakerRepair(input: Doc, plan: MacSpeakerRepairPlan): { doc: Doc; receipt: MacSpeakerRepairReceipt } {
171
+ requireClosed(input)
172
+ if (!plan.id?.trim() || !hashPattern.test(plan.evidenceHash) || plan.expectedDocumentHash !== hashMacSpeakerDocument(input)) fail('plan evidence or document CAS mismatch')
173
+ if (!Array.isArray(plan.protectedRawChunkIndices) || plan.protectedRawChunkIndices.some(i => !Number.isInteger(i) || i < 0)) fail('explicit ledger protection is required')
174
+ if (!Array.isArray(plan.assignments) || plan.assignments.length === 0) fail('no explicit assignments')
175
+ const mapping = rawMap(input)
176
+ const doc = structuredClone(input)
177
+ const patches: MacSpeakerRepairPatch[] = []
178
+ const patch = (path: MacSpeakerRepairPath, after: unknown) => {
179
+ let row = doc
180
+ for (const key of path.slice(0, -1)) row = row[key]
181
+ const key = path.at(-1)!
182
+ if (equal(row[key], after)) return
183
+ patches.push({ path, before: structuredClone(row[key]), after: structuredClone(after) })
184
+ row[key] = structuredClone(after)
185
+ }
186
+ if (!plan.rawAudioSampleCounts || typeof plan.rawAudioSampleCounts !== 'object') fail('verified raw audio sample counts are required')
187
+ const selected = new Map<number, { assignment: MacSpeakerRepairAssignment }>()
188
+ for (const assignment of plan.assignments) {
189
+ if (selected.has(assignment.rawChunkIndex) || plan.protectedRawChunkIndices.includes(assignment.rawChunkIndex)) fail('duplicate or ledger-protected chunk')
190
+ if (!anonymous(assignment.from) || !assignment.evidenceId?.trim() || !Array.isArray(assignment.batchWords)) fail('assignment requires anonymous labels and explicit evidence')
191
+ validateMacSpeakerTargetEvidence(input, plan, assignment)
192
+ const position = mapping.findIndex(row => row.raw === assignment.rawChunkIndex)
193
+ if (position < 0) fail('unknown raw chunk')
194
+ const chunk = input.chunks[position]
195
+ const entry = input.chunkEntries[mapping[position].entry].chunk
196
+ if (!Number.isFinite(chunk.elapsed) || !Number.isFinite(chunk.audioDurationMs) || chunk.audioDurationMs <= 0 || entry.elapsed !== chunk.elapsed || entry.audioDurationMs !== chunk.audioDurationMs) fail('raw chunk timing disagreement')
197
+ // Surface identity must match, including text/times/original diagnostic fields.
198
+ for (const key of ['speaker', 'segments', 'finalSegments', 'words']) if (!equal(chunk[key], entry[key])) fail('chunk and entry label surfaces disagree')
199
+ if (hasMacSpeakerProtectedRows(chunk) || hasMacSpeakerProtectedRows(entry)) fail('named or manual span is protected')
200
+ for (const target of [{ chunk, path: ['chunks', position] }, { chunk: entry, path: ['chunkEntries', mapping[position].entry, 'chunk'] }]) {
201
+ for (const { row, path } of labelRows(target.chunk, target.path)) {
202
+ if (isProtected(row) || !anonymous(row.speaker)) fail('named or manual span is protected')
203
+ if (row.speaker !== assignment.from) fail('mixed chunk requires exact span repair')
204
+ patch([...path, 'speaker'], assignment.to)
205
+ }
206
+ }
207
+ selected.set(assignment.rawChunkIndex, { assignment })
208
+ }
209
+ const withheld = new Map<string, MacSpeakerWordBinding>(), usedWithheld = new Set<string>()
210
+ for (const binding of plan.withheldBatchWords ?? []) {
211
+ const key = `${binding.segmentIndex}:${binding.wordIndex}`
212
+ if (binding.reason !== 'outside-qualified-span' || withheld.has(key) || !Number.isInteger(binding.segmentIndex) || !Number.isInteger(binding.wordIndex)) fail('invalid withheld boundary word binding')
213
+ withheld.set(key, binding)
214
+ }
215
+ const expectedWords = new Map<string, MacSpeakerRepairAssignment>()
216
+ const batchSegments = input.batchSegments ?? []
217
+ if (!Array.isArray(batchSegments)) fail('invalid batch segments')
218
+ for (const [si, segment] of batchSegments.entries()) {
219
+ if (!Array.isArray(segment.speakerWords)) fail('batch speaker words are missing')
220
+ const rangeSelected = [...selected.keys()].some(raw => raw >= segment.startChunkIdx && raw <= segment.endChunkIdx)
221
+ if (!rangeSelected) continue
222
+ if (!Number.isInteger(segment.startChunkIdx) || !Number.isInteger(segment.endChunkIdx) || segment.startChunkIdx < 0 || segment.endChunkIdx < segment.startChunkIdx) fail('invalid batch raw range')
223
+ // Batch seconds index concatenated PCM files, including silent files. They
224
+ // are NOT startElapsed + seconds on the compacted transcript timeline.
225
+ const intervals: Array<{ raw: number; start: number; end: number }> = []
226
+ let offset = 0
227
+ for (let raw = segment.startChunkIdx; raw <= segment.endChunkIdx; raw++) {
228
+ const samples = plan.rawAudioSampleCounts[String(raw)]
229
+ if (!Number.isSafeInteger(samples) || samples <= 0) fail('missing verified raw audio sample count')
230
+ const end = offset + samples / 16000
231
+ intervals.push({ raw, start: offset, end })
232
+ offset = end
233
+ }
234
+ for (const [wi, word] of segment.speakerWords.entries()) {
235
+ if (!Number.isFinite(word.start) || !Number.isFinite(word.end) || word.start < 0 || word.end < word.start || word.end > offset) fail('invalid batch word timing')
236
+ const overlaps = intervals.filter(s => word.end === word.start ? word.start >= s.start && word.start < s.end : word.start < s.end && word.end > s.start)
237
+ if (!overlaps.some(s => selected.has(s.raw))) continue
238
+ if (overlaps.some(s => !selected.has(s.raw))) {
239
+ const key = `${si}:${wi}`, binding = withheld.get(key)
240
+ if (!binding || word.word !== binding.word || word.start !== binding.start || word.end !== binding.end || word.speaker !== binding.from) fail('batch word crosses selected raw chunk boundaries')
241
+ usedWithheld.add(key); continue
242
+ }
243
+ if (new Set(overlaps.map(s => selected.get(s.raw)?.assignment.to)).size !== 1) fail('batch word crosses selected raw chunk boundaries')
244
+ const span = selected.get(overlaps[0].raw)!
245
+ if (isProtected(word) || !anonymous(word.speaker)) fail('named or manual batch word is protected')
246
+ expectedWords.set(`${si}:${wi}`, span.assignment)
247
+ }
248
+ }
249
+ if (usedWithheld.size !== withheld.size) fail('withheld boundary word is not outside qualified span')
250
+ const seen = new Set<string>()
251
+ for (const assignment of plan.assignments) for (const binding of assignment.batchWords) {
252
+ const key = `${binding.segmentIndex}:${binding.wordIndex}`
253
+ if (!Number.isInteger(binding.segmentIndex) || !Number.isInteger(binding.wordIndex) || seen.has(key) || expectedWords.get(key) !== assignment) fail('unexpected or duplicate batch word binding')
254
+ seen.add(key)
255
+ const word = batchSegments[binding.segmentIndex].speakerWords[binding.wordIndex]
256
+ if (word.word !== binding.word || word.start !== binding.start || word.end !== binding.end || word.speaker !== binding.from) fail('batch word evidence disagrees')
257
+ patch(['batchSegments', binding.segmentIndex, 'speakerWords', binding.wordIndex, 'speaker'], assignment.to)
258
+ }
259
+ if (seen.size !== expectedWords.size) fail('incomplete batch word coverage')
260
+ const labels: string[] = []
261
+ for (const chunk of doc.chunks) for (const { row } of labelRows(chunk, [])) if (typeof row.speaker === 'string' && row.speaker && !anonymous(row.speaker)) labels.push(row.speaker)
262
+ for (const segment of doc.batchSegments ?? []) for (const word of segment.speakerWords ?? []) if (typeof word.speaker === 'string' && word.speaker && !anonymous(word.speaker)) labels.push(word.speaker)
263
+ // Do not invent identities, and do not carry anonymous voices into a real-name list.
264
+ patch(['speakers'], [...new Set(labels)])
265
+ const unsigned: Omit<MacSpeakerRepairReceipt, 'receiptHash'> = {
266
+ version: 1, id: plan.id, evidenceHash: plan.evidenceHash,
267
+ beforeHash: hashMacSpeakerDocument(input), afterHash: hashMacSpeakerDocument(doc),
268
+ assignments: structuredClone(plan.assignments), withheldBatchWords: structuredClone(plan.withheldBatchWords ?? []),
269
+ rawChunkIndices: plan.assignments.map(row => row.rawChunkIndex), evidenceIds: plan.assignments.map(row => row.evidenceId), patches,
270
+ }
271
+ return { doc, receipt: { ...unsigned, receiptHash: receiptDigest(unsigned) } }
272
+ }
273
+
274
+ /** Full-document CAS is intentionally conservative: even unrelated drift needs
275
+ * a fresh review. This pure operation never overwrites a later correction.
276
+ * Replaying a completed apply/undo is idempotent; partial states fail closed.
277
+ */
278
+ export function applyMacSpeakerRepair(input: Doc, receipt: MacSpeakerRepairReceipt, direction: 'forward' | 'reverse' = 'forward'): Doc {
279
+ const { receiptHash, ...unsigned } = receipt
280
+ if (receipt.version !== 1 || receiptDigest(unsigned) !== receiptHash) fail('receipt integrity mismatch')
281
+ const forward = direction === 'forward'
282
+ const before = forward ? receipt.beforeHash : receipt.afterHash
283
+ const after = forward ? receipt.afterHash : receipt.beforeHash
284
+ const current = hashMacSpeakerDocument(input)
285
+ if (current === after) return structuredClone(input)
286
+ if (current !== before) fail('document drift; review before apply or undo')
287
+ const doc = structuredClone(input)
288
+ const ordered = forward ? receipt.patches : [...receipt.patches].reverse()
289
+ for (const patch of ordered) {
290
+ if (!patch.path.length || patch.path.some(key => ['__proto__', 'prototype', 'constructor'].includes(String(key)))) fail('unsafe patch path')
291
+ let row = doc
292
+ for (const key of patch.path.slice(0, -1)) { if (!row || typeof row !== 'object' || !(key in row)) fail('patch shape drift'); row = row[key] }
293
+ const key = patch.path.at(-1)!
294
+ if (!equal(row[key], forward ? patch.before : patch.after)) fail('patch value drift')
295
+ const value = forward ? patch.after : patch.before
296
+ if (value === undefined) delete row[key]
297
+ else row[key] = structuredClone(value)
298
+ }
299
+ if (hashMacSpeakerDocument(doc) !== after) fail('projection postimage mismatch')
300
+ return doc
301
+ }
@@ -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
+ }