@gotcos/glasses-server 6.94.4 → 6.98.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +147 -0
- package/README.md +52 -0
- package/managed-runtime-contract.json +13 -0
- package/package.json +3 -1
- package/server/index.ts +19 -1
- package/server/lib/attached-provider-adapter.ts +35 -1
- package/server/lib/claude-bridge.ts +47 -0
- package/server/lib/codex-bridge.ts +11 -1
- package/server/lib/codex-rollout-watermark.ts +42 -0
- package/server/lib/cursor-bridge.ts +2 -1
- package/server/lib/dictation-clean-models.ts +2 -0
- package/server/lib/dictation-clean.ts +2 -2
- package/server/lib/dictation-polish-pipeline.ts +22 -2
- package/server/lib/dictation-prewarm.ts +16 -0
- package/server/lib/direct-bridge.ts +14 -2
- package/server/lib/direct-sse.ts +6 -1
- package/server/lib/display-bus.ts +1 -1
- package/server/lib/fork-thread.ts +32 -1
- package/server/lib/held-naming-batches.ts +36 -0
- package/server/lib/job-audit-caller.ts +20 -0
- package/server/lib/lens-gist-engines.ts +17 -5
- package/server/lib/live-cos-ask.ts +29 -6
- package/server/lib/live-cue-verdicts.ts +43 -8
- package/server/lib/live-cues-engine.ts +4 -1
- package/server/lib/live-cues-filter.ts +50 -0
- package/server/lib/live-cues-general-prompts.ts +137 -0
- package/server/lib/live-cues-general-topic.ts +245 -0
- package/server/lib/live-cues-general.ts +715 -0
- package/server/lib/live-cues-v2-data.ts +453 -0
- package/server/lib/live-cues-v2-grounded.ts +262 -0
- package/server/lib/live-cues-v2-judge-b.ts +59 -0
- package/server/lib/live-cues-v2-names.ts +49 -0
- package/server/lib/live-cues-v2-notes.ts +8 -4
- package/server/lib/live-cues-v2-open-items.ts +132 -0
- package/server/lib/live-cues-v2-pacing.ts +45 -0
- package/server/lib/live-cues-v2-prompts-kinds.ts +9 -4
- package/server/lib/live-cues-v2-settings.ts +89 -10
- package/server/lib/live-cues-v2.ts +570 -47
- package/server/lib/mac-recording-asr.ts +6 -3
- package/server/lib/mac-speaker-call-human.ts +371 -0
- package/server/lib/mac-speaker-call-interval-human.ts +499 -0
- package/server/lib/mac-speaker-call-interval-projection.ts +233 -0
- package/server/lib/mac-speaker-call-original-view.ts +328 -0
- package/server/lib/mac-speaker-call-producer-worker.ts +615 -0
- package/server/lib/mac-speaker-call-producer.ts +539 -0
- package/server/lib/mac-speaker-call-store.ts +264 -0
- package/server/lib/mac-speaker-call-view.ts +373 -0
- package/server/lib/mac-speaker-live-compute.ts +72 -0
- package/server/lib/mac-speaker-live-memory.ts +414 -0
- package/server/lib/mac-speaker-live-projection.ts +126 -0
- package/server/lib/mac-speaker-live-save.ts +90 -0
- package/server/lib/mac-speaker-live-store.ts +71 -0
- package/server/lib/mac-speaker-live-worker.ts +58 -0
- package/server/lib/mac-speaker-live.ts +175 -0
- package/server/lib/mac-speaker-profile-extractor.ts +15 -10
- package/server/lib/mac-speaker-shadow-repair.ts +307 -0
- package/server/lib/mac-voice-assignment.ts +44 -26
- package/server/lib/mac-voice-display.ts +195 -0
- package/server/lib/mac-voice-naming-batch.ts +1067 -0
- package/server/lib/mac-voice-suggestions.ts +548 -0
- package/server/lib/maintenance-lifecycle.ts +2 -0
- package/server/lib/media-store.ts +2 -1
- package/server/lib/meeting-ask-pasted.ts +135 -0
- package/server/lib/meeting-ask-review.ts +8 -1
- package/server/lib/meeting-ask-runner.ts +4 -1
- package/server/lib/meeting-ask-runtime.ts +189 -17
- package/server/lib/meeting-ask-store.ts +2 -0
- package/server/lib/meeting-ask-web.ts +284 -13
- package/server/lib/meeting-batch-progress.ts +51 -5
- package/server/lib/meeting-batch-transcribe.ts +35 -2
- package/server/lib/meeting-fact-check.ts +3 -0
- package/server/lib/meeting-finalization-jobs.ts +69 -2
- package/server/lib/meeting-public-topic.ts +6 -1
- package/server/lib/meeting-speaker-review.ts +182 -17
- package/server/lib/meeting-store.ts +6 -1
- package/server/lib/parakeet.ts +51 -5
- package/server/lib/pending-batch-retention.ts +58 -0
- package/server/lib/private-reference.ts +38 -0
- package/server/lib/provider-usage.ts +561 -0
- package/server/lib/public-https-url.ts +32 -0
- package/server/lib/query-job-runtime.ts +3 -0
- package/server/lib/rich-media-safety.ts +23 -2
- package/server/lib/speaker-shadow-decide.ts +503 -0
- package/server/lib/speaker-shadow-models.ts +150 -0
- package/server/lib/speaker-shadow-numeric.ts +143 -0
- package/server/lib/speaker-shadow-pair-source.ts +91 -0
- package/server/lib/speaker-shadow-paired.ts +268 -0
- package/server/lib/speaker-shadow-report.ts +113 -0
- package/server/lib/speaker-shadow-timeline.ts +210 -0
- package/server/lib/speaker-shadow-worker.ts +312 -0
- package/server/lib/speaker-shadow.ts +788 -0
- package/server/lib/token-audit.ts +36 -6
- package/server/lib/transcript-cleanup.ts +3 -2
- package/server/lib/voice-directory.ts +8 -4
- package/server/lib/web-lookup-ledger.ts +151 -0
- package/server/lib/work-review-runtime.ts +2 -0
- package/server/lib/work-search.ts +3 -1
- package/server/lib/xlsx-text.ts +277 -0
- package/server/routes/health.ts +5 -0
- package/server/routes/live-cues.ts +36 -14
- package/server/routes/meeting.ts +506 -49
- package/server/routes/speaker-shadow.ts +79 -0
- package/server/routes/transcribe-stream.ts +114 -33
- package/server/routes/voice.ts +3 -0
- package/server/scripts/live-cues-v3-mutation-gate.mjs +311 -0
- package/server/scripts/mac-voice-assignment-6930-mutation-gate.mjs +5 -5
- package/shared/live-cues-v2.ts +38 -5
- package/shared/media-attachment.ts +2 -0
- package/shared/meeting-ask.ts +11 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/** Capture-long192d identity evidence. Pure transitions; never enrollment or text edits. */
|
|
2
|
+
import { checkSpeakerName } from './speaker-name.js'
|
|
3
|
+
import { isVoiceLabel } from './mac-voice-labels.js'
|
|
4
|
+
import { createHash } from 'node:crypto'
|
|
5
|
+
import { DEFAULT_MAC_SPEAKER_THRESHOLDS, createMacSpeakerMemory, validateMacSpeakerMemory, reconcileMacSpeakerObservation, matchMacSpeakerProfile, normalizedSpeakerEmbedding, normalizedSpeakerCosine,
|
|
6
|
+
type MacSpeakerMemory, type MacSpeakerObservation, type MacSpeakerProfile } from './mac-speaker-reconcile.js'
|
|
7
|
+
import type { PreparedMacSpeakerWindow, MacWindowEmbeddingReceipt } from './mac-speaker-windows.js'
|
|
8
|
+
import { inspectMacSpeakerShadowCallSource, type MacShadowRepairInput } from './mac-speaker-shadow-repair.js'
|
|
9
|
+
import { exactOverlap } from './speaker-shadow-decide.js'
|
|
10
|
+
import { pyRoundInt } from './speaker-shadow-numeric.js'
|
|
11
|
+
|
|
12
|
+
const SHA=/^[a-f0-9]{64}$/
|
|
13
|
+
export const MAC_SPEAKER_LIVE_MAX_WINDOWS=256
|
|
14
|
+
/** Control accepts counters below10m; the last value is withdrawal-only. */
|
|
15
|
+
export const MAC_SPEAKER_LIVE_TERMINAL_REVISION=9999999
|
|
16
|
+
export const hashMacSpeakerLive=(value:unknown):string=>createHash('sha256').update(JSON.stringify(value)).digest('hex')
|
|
17
|
+
export interface MacSpeakerLiveSpan {
|
|
18
|
+
rawChunkIndex:number;audioSHA256:string;pcmSHA256:string
|
|
19
|
+
/** Capture-absolute, half-open samples at16kHz; exact whole original, never a crop. */
|
|
20
|
+
startSample:number;endSample:number;identityId:string;displayLabel:string;profileName?:string;evidenceIds:string[]
|
|
21
|
+
}
|
|
22
|
+
export interface MacSpeakerLiveEvidence {
|
|
23
|
+
window:PreparedMacSpeakerWindow;receipt:MacWindowEmbeddingReceipt
|
|
24
|
+
/** Hash of the complete model/source/vector receipt, computed after owned child close. */
|
|
25
|
+
nativeReceiptSHA256:string
|
|
26
|
+
cohortId?:number
|
|
27
|
+
}
|
|
28
|
+
export interface MacSpeakerLiveState {
|
|
29
|
+
version:1;captureId:string;generation:string;modelSHA256:string;profileSHA256:string;revision:number
|
|
30
|
+
memory:MacSpeakerMemory;evidence:MacSpeakerLiveEvidence[]
|
|
31
|
+
identities:Array<{cohortId:number;identityId:string;displayLabel:string;retired:boolean}>
|
|
32
|
+
voiceHighWater:number;projections:MacSpeakerLiveSpan[];withheldReason:string|null;stateSHA256:string
|
|
33
|
+
}
|
|
34
|
+
const seal=(state:MacSpeakerLiveState):MacSpeakerLiveState=>{
|
|
35
|
+
if(state.revision>=MAC_SPEAKER_LIVE_TERMINAL_REVISION){state.revision=MAC_SPEAKER_LIVE_TERMINAL_REVISION;state.projections=[];state.voiceHighWater=99;state.withheldReason='revision_counter_exhausted';for(const identity of state.identities)identity.retired=true}
|
|
36
|
+
const{stateSHA256:_hash,...body}=state;state.stateSHA256=hashMacSpeakerLive(body);return state}
|
|
37
|
+
export function createMacSpeakerLiveState(captureId:string,generation:string,modelSHA256:string,profileSHA256:string,voiceHighWater=0):MacSpeakerLiveState {
|
|
38
|
+
if(!generation||generation.length>128||!SHA.test(profileSHA256)||!Number.isSafeInteger(voiceHighWater)||voiceHighWater<0||voiceHighWater>99)throw Error('live_binding')
|
|
39
|
+
return seal({version:1,captureId,generation,modelSHA256,profileSHA256,revision:0,memory:createMacSpeakerMemory(captureId,modelSHA256,{minimumSamples:320000}),evidence:[],identities:[],voiceHighWater,projections:[],withheldReason:null,stateSHA256:''})
|
|
40
|
+
}
|
|
41
|
+
function validEvidence(e:MacSpeakerLiveEvidence,state:MacSpeakerLiveState):void {
|
|
42
|
+
const w=e.window,r=e.receipt
|
|
43
|
+
if(!w||!r||!Number.isSafeInteger(w.startSample)||w.startSample<0||!Number.isSafeInteger(w.endSample)||!isVoiceLabel(w.originalVoiceHint)||!Array.isArray(w.sources)||!w.sources.length||w.sources.length>30||w.captureId!==state.captureId||r.modelSHA256!==state.modelSHA256||r.windowId!==w.id||r.windowBindingSHA256!==w.bindingSHA256
|
|
44
|
+
||r.sampleCount!==w.sampleCount||w.endSample-w.startSample!==w.sampleCount||w.sampleCount<448000||w.sampleCount>480000||!SHA.test(r.audioSHA256)||!SHA.test(r.pcmSHA256)
|
|
45
|
+
||!normalizedSpeakerEmbedding(r.embedding,192)||!Array.isArray(r.halfEmbeddings)||r.halfEmbeddings.length!==2||r.halfEmbeddings.some(v=>!normalizedSpeakerEmbedding(v,192)))throw Error('live_evidence_binding')
|
|
46
|
+
const{id:_,bindingSHA256,...body}=w
|
|
47
|
+
if(hashMacSpeakerLive(body)!==bindingSHA256||w.id!==`window_${bindingSHA256}`||e.nativeReceiptSHA256!==hashMacSpeakerLive({window:w,receipt:r}))throw Error('live_receipt_hash')
|
|
48
|
+
let at=w.startSample,speech=0;const indices=new Set<number>(),hashes=new Set<string>()
|
|
49
|
+
for(const s of w.sources){
|
|
50
|
+
if(!Number.isSafeInteger(s.chunkIndex)||s.chunkIndex<0||s.startSample!==at||s.endSample-s.startSample!==s.sampleCount||!Number.isSafeInteger(s.sampleCount)||s.sampleCount<=0
|
|
51
|
+
||s.sampleRate!==16000||s.channels!==1||s.format!=='pcm_s16le'||s.verified!==true||!SHA.test(s.audioSHA256)||!SHA.test(s.pcmSHA256)||!SHA.test(s.textSHA256)||!SHA.test(s.segmentsSHA256)
|
|
52
|
+
||indices.has(s.chunkIndex)||hashes.has(s.pcmSHA256)||!Number.isSafeInteger(s.speechSamples)||s.speechSamples<0||s.speechSamples>s.sampleCount)throw Error('live_source_geometry')
|
|
53
|
+
at=s.endSample;speech+=s.speechSamples;indices.add(s.chunkIndex);hashes.add(s.pcmSHA256)
|
|
54
|
+
}
|
|
55
|
+
if(at!==w.endSample||speech!==w.speechSamples||speech<320000)throw Error('live_source_speech')
|
|
56
|
+
}
|
|
57
|
+
export function validateMacSpeakerLiveState(state:MacSpeakerLiveState):void {
|
|
58
|
+
if(!state||state.version!==1||typeof state.generation!=='string'||!state.generation||state.generation.length>128||!SHA.test(state.modelSHA256)||!SHA.test(state.profileSHA256)||!Number.isSafeInteger(state.revision)||state.revision<0||state.revision>MAC_SPEAKER_LIVE_TERMINAL_REVISION
|
|
59
|
+
||!Array.isArray(state.evidence)||state.evidence.length>MAC_SPEAKER_LIVE_MAX_WINDOWS||!Array.isArray(state.identities)||state.identities.length>99||!Array.isArray(state.projections)||state.projections.length>8192
|
|
60
|
+
||!Number.isSafeInteger(state.voiceHighWater)||state.voiceHighWater<0||state.voiceHighWater>99||Buffer.byteLength(JSON.stringify(state))>8*1024*1024)throw Error('live_state_bound')
|
|
61
|
+
if(state.revision===MAC_SPEAKER_LIVE_TERMINAL_REVISION&&(state.projections.length||state.voiceHighWater!==99||!state.withheldReason||state.identities.some(i=>!i.retired)))throw Error('live_terminal_revision')
|
|
62
|
+
const{stateSHA256,...body}=state;if(hashMacSpeakerLive(body)!==stateSHA256)throw Error('live_state_hash')
|
|
63
|
+
validateMacSpeakerMemory(state.memory)
|
|
64
|
+
if(hashMacSpeakerLive(state.memory.thresholds)!==hashMacSpeakerLive({...DEFAULT_MAC_SPEAKER_THRESHOLDS,minimumSamples:320000}))throw Error('live_threshold_drift')
|
|
65
|
+
if(state.memory.captureId!==state.captureId||state.memory.modelSHA256!==state.modelSHA256||state.memory.thresholds.dimensions!==192||state.memory.thresholds.minimumSamples!==320000)throw Error('live_memory_binding')
|
|
66
|
+
const seen=new Set<string>(),raw=new Set<number>(),pcm=new Set<string>()
|
|
67
|
+
for(const e of state.evidence){validEvidence(e,state);if(e.cohortId!==undefined&&(!Number.isSafeInteger(e.cohortId)||!state.memory.identities.some(i=>i.id===e.cohortId)))throw Error('live_cohort_reference');if(seen.has(e.window.id))throw Error('live_duplicate_window');seen.add(e.window.id)
|
|
68
|
+
for(const source of e.window.sources){if(raw.has(source.chunkIndex)||pcm.has(source.pcmSHA256))throw Error('live_duplicate_source');raw.add(source.chunkIndex);pcm.add(source.pcmSHA256)}}
|
|
69
|
+
const ids=new Set<string>(),labels=new Set<string>(),cohorts=new Set<number>()
|
|
70
|
+
for(const i of state.identities){if(ids.has(i.identityId)||labels.has(i.displayLabel)||cohorts.has(i.cohortId)||!/^acoustic_[a-f0-9]{64}$/.test(i.identityId)||!/^Voice [1-9]\d?$/.test(i.displayLabel)
|
|
71
|
+
||Number(i.displayLabel.slice(6))>state.voiceHighWater||!state.memory.identities.some(c=>c.id===i.cohortId)||typeof i.retired!=='boolean')throw Error('live_namespace');ids.add(i.identityId);labels.add(i.displayLabel);cohorts.add(i.cohortId)}
|
|
72
|
+
const receipts=new Map(state.evidence.map(e=>[e.nativeReceiptSHA256,e])),projected=new Set<number>()
|
|
73
|
+
for(const p of state.projections){
|
|
74
|
+
const identity=state.identities.find(i=>i.identityId===p.identityId)
|
|
75
|
+
if(!identity||identity.retired||projected.has(p.rawChunkIndex)||!Array.isArray(p.evidenceIds)||p.evidenceIds.length<2||p.evidenceIds.length>3||new Set(p.evidenceIds).size!==p.evidenceIds.length)throw Error('live_projection_identity')
|
|
76
|
+
const support=p.evidenceIds.map(id=>receipts.get(id))
|
|
77
|
+
if(support.some(e=>!e||e.cohortId!==identity.cohortId||!coherent(e)))throw Error('live_projection_support')
|
|
78
|
+
const source=support.flatMap(e=>e!.window.sources).find(s=>s.chunkIndex===p.rawChunkIndex)
|
|
79
|
+
if(!source||source.audioSHA256!==p.audioSHA256||source.pcmSHA256!==p.pcmSHA256||source.startSample!==p.startSample||source.endSample!==p.endSample)throw Error('live_projection_source')
|
|
80
|
+
if(p.profileName!==undefined?typeof p.profileName!=='string'||!checkSpeakerName(p.profileName).ok||/^(?:ext|unknown|mu|speaker[\s_-]*\d+|voice[\s_-]*\d+)$/iu.test(p.profileName)||p.displayLabel!==p.profileName:p.displayLabel!==identity.displayLabel)throw Error('live_projection_label')
|
|
81
|
+
projected.add(p.rawChunkIndex)
|
|
82
|
+
}
|
|
83
|
+
if(state.withheldReason!==null&&state.projections.length)throw Error('live_withheld_projection')
|
|
84
|
+
// Every retained native witness assigned to memory must have the same exact
|
|
85
|
+
// aggregate original as its corresponding evidence receipt.
|
|
86
|
+
const expectedOriginals=state.evidence.filter(e=>e.cohortId!==undefined).map(e=>observation(state,e).original)
|
|
87
|
+
if(hashMacSpeakerLive(expectedOriginals)!==hashMacSpeakerLive(state.memory.originals))throw Error('live_memory_lineage')
|
|
88
|
+
// Rebuild deterministic receipt membership, rather than trusting a caller-
|
|
89
|
+
// recomputed unkeyed state hash or arbitrary cohort references on recovery.
|
|
90
|
+
let rebuilt=createMacSpeakerMemory(state.captureId,state.modelSHA256,{minimumSamples:320000})
|
|
91
|
+
for(const e of state.evidence){
|
|
92
|
+
let expected:number|undefined
|
|
93
|
+
if(coherent(e)){const next=reconcileMacSpeakerObservation(rebuilt,observation(state,e));rebuilt=next.state;expected=next.decision.identityId}
|
|
94
|
+
if(expected!==e.cohortId)throw Error('live_receipt_membership')
|
|
95
|
+
}
|
|
96
|
+
if(hashMacSpeakerLive(rebuilt)!==hashMacSpeakerLive(state.memory))throw Error('live_memory_replay')
|
|
97
|
+
for(const identity of state.identities){const group=state.evidence.filter(e=>e.cohortId===identity.cohortId)
|
|
98
|
+
if(group.length<2||identity.identityId!==`acoustic_${hashMacSpeakerLive(group.slice(0,2).map(e=>e.nativeReceiptSHA256))}`)throw Error('live_identity_lineage')
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function observation(state:MacSpeakerLiveState,e:MacSpeakerLiveEvidence):MacSpeakerObservation {
|
|
102
|
+
const w=e.window,r=e.receipt
|
|
103
|
+
return{captureId:state.captureId,modelSHA256:state.modelSHA256,original:{chunkIndex:w.sources[0].chunkIndex,audioSHA256:r.audioSHA256,pcmSHA256:r.pcmSHA256,startSample:w.startSample,sampleCount:w.sampleCount},
|
|
104
|
+
startSample:w.startSample,endSample:w.endSample,exclusiveSamples:w.speechSamples,purity:1,overlapSamples:0,mixedSpeakers:false,embedding:r.embedding}
|
|
105
|
+
}
|
|
106
|
+
/** Labels/segments establish scheduling hints only. Halves are dependent checks, never independent votes. */
|
|
107
|
+
function coherent(e:MacSpeakerLiveEvidence):boolean {const h=e.receipt.halfEmbeddings!;return normalizedSpeakerCosine(h[0],h[1])!>=.90&&h.every(v=>normalizedSpeakerCosine(v,e.receipt.embedding)!>=.90)}
|
|
108
|
+
function refresh(state:MacSpeakerLiveState,profiles:readonly MacSpeakerProfile[]):void {
|
|
109
|
+
state.projections=[]
|
|
110
|
+
if(state.withheldReason)return
|
|
111
|
+
for(const cohort of state.memory.identities){
|
|
112
|
+
if(cohort.voice===null)continue
|
|
113
|
+
const group=state.evidence.filter(e=>e.cohortId===cohort.id)
|
|
114
|
+
if(group.length<2||group.some(a=>group.some(b=>normalizedSpeakerCosine(a.receipt.embedding,b.receipt.embedding)!<.90)))continue
|
|
115
|
+
const minimum=Math.min(...group.flatMap(a=>group.filter(b=>a!==b).map(b=>normalizedSpeakerCosine(a.receipt.embedding,b.receipt.embedding)!)))
|
|
116
|
+
const outside=state.evidence.filter(e=>e.cohortId!==cohort.id)
|
|
117
|
+
// Failed-half evidence can be compatible non-support only: BOTH dependent
|
|
118
|
+
// halves must clear EVERY established full anchor and every other coherent
|
|
119
|
+
// outsider (including unassigned singletons). It never supports/projects.
|
|
120
|
+
const compatibleNonSupport=(o:MacSpeakerLiveEvidence):boolean=>{
|
|
121
|
+
if(coherent(o))return false
|
|
122
|
+
const competitors=outside.filter(other=>other!==o&&coherent(other))
|
|
123
|
+
return o.receipt.halfEmbeddings!.every(half=>{
|
|
124
|
+
const same=Math.min(...group.map(g=>normalizedSpeakerCosine(half,g.receipt.embedding)!))
|
|
125
|
+
const different=Math.max(-1,...competitors.map(other=>normalizedSpeakerCosine(half,other.receipt.embedding)!))
|
|
126
|
+
return same>=.90&&same-different>=.06
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
// Every full observation remains a veto unless that narrow, non-support
|
|
130
|
+
// compatibility check passes. Later coherent confusers revoke the exception.
|
|
131
|
+
if(outside.some(o=>!compatibleNonSupport(o)&&group.some(g=>minimum-normalizedSpeakerCosine(g.receipt.embedding,o.receipt.embedding)!<.06)))continue
|
|
132
|
+
let identity=state.identities.find(i=>i.cohortId===cohort.id)
|
|
133
|
+
if(identity?.retired)continue
|
|
134
|
+
if(!identity){
|
|
135
|
+
if(state.voiceHighWater>=99){state.withheldReason='voice_namespace_exhausted';state.projections=[];return}
|
|
136
|
+
const identityId=`acoustic_${hashMacSpeakerLive(group.slice(0,2).map(e=>e.nativeReceiptSHA256))}`
|
|
137
|
+
identity={cohortId:cohort.id,identityId,displayLabel:`Voice ${++state.voiceHighWater}`,retired:false};state.identities.push(identity)
|
|
138
|
+
}
|
|
139
|
+
const named=matchMacSpeakerProfile(state.memory,group.map(e=>observation(state,e)),profiles)
|
|
140
|
+
// MU remains an enrolled full-cohort competitor, never a human display claim.
|
|
141
|
+
const profileName=named.reason==='matched_profile'&&named.label?.normalize('NFKC').trim().toLowerCase()!=='mu'?named.label??undefined:undefined
|
|
142
|
+
for(const e of group)for(const s of e.window.sources)state.projections.push({rawChunkIndex:s.chunkIndex,audioSHA256:s.audioSHA256,pcmSHA256:s.pcmSHA256,startSample:s.startSample,endSample:s.endSample,
|
|
143
|
+
identityId:identity.identityId,displayLabel:profileName??identity.displayLabel,...(profileName?{profileName}:{}),evidenceIds:[...new Set([...group.slice(0,2).map(x=>x.nativeReceiptSHA256),e.nativeReceiptSHA256])]})
|
|
144
|
+
}
|
|
145
|
+
state.projections.sort((a,b)=>a.rawChunkIndex-b.rawChunkIndex)
|
|
146
|
+
}
|
|
147
|
+
/** Immutable append; exact source overlaps/crops cannot supply a second witness. */
|
|
148
|
+
export function appendMacSpeakerLiveEvidence(input:MacSpeakerLiveState,evidence:MacSpeakerLiveEvidence,profiles:readonly MacSpeakerProfile[],reservedHighWater=input.voiceHighWater):MacSpeakerLiveState {
|
|
149
|
+
validateMacSpeakerLiveState(input);if(input.revision===MAC_SPEAKER_LIVE_TERMINAL_REVISION)return structuredClone(input)
|
|
150
|
+
validEvidence(evidence,input)
|
|
151
|
+
validateMacSpeakerLiveProfiles(profiles,input.profileSHA256)
|
|
152
|
+
const state=structuredClone(input)
|
|
153
|
+
if(!Number.isSafeInteger(reservedHighWater)||reservedHighWater<state.voiceHighWater||reservedHighWater>99)throw Error('live_namespace_drift')
|
|
154
|
+
state.voiceHighWater=reservedHighWater
|
|
155
|
+
if(state.evidence.some(e=>e.window.id===evidence.window.id))return seal(state)
|
|
156
|
+
if(state.evidence.length>=MAC_SPEAKER_LIVE_MAX_WINDOWS){state.withheldReason='evidence_budget_exhausted';state.projections=[];state.revision++;return seal(state)}
|
|
157
|
+
const sources=state.evidence.flatMap(e=>e.window.sources)
|
|
158
|
+
if(evidence.window.sources.some(s=>sources.some(p=>s.chunkIndex===p.chunkIndex||s.audioSHA256===p.audioSHA256||s.pcmSHA256===p.pcmSHA256||Math.max(s.startSample,p.startSample)<Math.min(s.endSample,p.endSample))))throw Error('live_repeated_or_crossed_source')
|
|
159
|
+
const e=structuredClone(evidence);delete e.cohortId
|
|
160
|
+
if(coherent(e)){
|
|
161
|
+
const next=reconcileMacSpeakerObservation(state.memory,observation(state,e));state.memory=next.state
|
|
162
|
+
if(next.decision.identityId!==undefined)e.cohortId=next.decision.identityId
|
|
163
|
+
}
|
|
164
|
+
state.evidence.push(e);refresh(state,profiles);state.revision++;return seal(state)
|
|
165
|
+
}
|
|
166
|
+
export function withholdMacSpeakerLiveState(input:MacSpeakerLiveState,reason:string):MacSpeakerLiveState {
|
|
167
|
+
validateMacSpeakerLiveState(input);const state=structuredClone(input)
|
|
168
|
+
if(!reason||reason.length>200)throw Error('live_withheld_reason')
|
|
169
|
+
for(const i of state.identities)i.retired=true
|
|
170
|
+
state.projections=[];state.withheldReason=reason;state.revision++;return seal(state)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Offline/replay seam: caller must independently prove the canonical full source
|
|
174
|
+
* universe. Completeness here is only relative to supplied source-bound receipts. */
|
|
175
|
+
export function replayMacSpeakerLiveEvidence(input:{captureId:string;generation:string;modelSHA256:string;profileSHA256:string;voiceHighWater?:number;evidence:readonly MacSpeakerLiveEvidence[];profiles:readonly MacSpeakerProfile[]}):MacSpeakerLiveState {
|
|
176
|
+
let state=createMacSpeakerLiveState(input.captureId,input.generation,input.modelSHA256,input.profileSHA256,input.voiceHighWater)
|
|
177
|
+
if(input.evidence.length>MAC_SPEAKER_LIVE_MAX_WINDOWS)return withholdMacSpeakerLiveState(state,'evidence_budget_exhausted')
|
|
178
|
+
for(const e of input.evidence)state=appendMacSpeakerLiveEvidence(state,e,input.profiles)
|
|
179
|
+
return state
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function validateMacSpeakerLiveProfiles(profiles:readonly MacSpeakerProfile[],profileSHA256:string):void {
|
|
183
|
+
if(!Array.isArray(profiles)||profiles.length>1024||profiles.some(p=>!p||p.profileSHA256!==profileSHA256))throw Error('live_profile_drift')
|
|
184
|
+
const aliases=new Set<string>();let count=0
|
|
185
|
+
for(const p of profiles){const alias=p.name?.normalize('NFKC').trim().replace(/\s+/gu,' ').toLowerCase()
|
|
186
|
+
if(!checkSpeakerName(p.name).ok||/^(?:ext|unknown|speaker[\s_-]*(?:\d+|n)|voice[\s_-]*(?:\d+|n))$/iu.test(p.name)||aliases.has(alias)||!Array.isArray(p.embeddings)||!p.embeddings.length||p.embeddings.length>128||p.embeddings.some((v:readonly number[])=>!normalizedSpeakerEmbedding(v,192)))throw Error('live_profile_cohort_invalid')
|
|
187
|
+
aliases.add(alias);count+=p.embeddings.length
|
|
188
|
+
}
|
|
189
|
+
if(count>8192)throw Error('live_profile_cohort_invalid')
|
|
190
|
+
}
|
|
191
|
+
/** Advance only the public snapshot cursor (e.g. source/manual revocation). */
|
|
192
|
+
export function advanceMacSpeakerLiveRevision(input:MacSpeakerLiveState):MacSpeakerLiveState {
|
|
193
|
+
validateMacSpeakerLiveState(input);const state=structuredClone(input);state.revision++;return seal(state)
|
|
194
|
+
}
|
|
195
|
+
export function refreshMacSpeakerLiveState(input:MacSpeakerLiveState,profiles:readonly MacSpeakerProfile[]):MacSpeakerLiveState {
|
|
196
|
+
validateMacSpeakerLiveState(input);validateMacSpeakerLiveProfiles(profiles,input.profileSHA256);const state=structuredClone(input);refresh(state,profiles);return seal(state)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Separate model space and candidate protocol. This does NOT enable the192d
|
|
200
|
+
* runtime or authenticate native execution. Consumers must verify the complete
|
|
201
|
+
* source/native packet and run this full-cohort work OFF the HTTP/main thread. */
|
|
202
|
+
export interface MacSpeaker256SnapshotInput extends MacShadowRepairInput {
|
|
203
|
+
generation:string
|
|
204
|
+
/** Optimistic concurrency fence; mandatory when transitioning an existing state. */
|
|
205
|
+
expectedStateSHA256?:string
|
|
206
|
+
}
|
|
207
|
+
export interface MacSpeaker256Witness {
|
|
208
|
+
evidenceId:string;startSample:number;endSample:number;originalVoice:string
|
|
209
|
+
rawChunkIndices:number[];pcmSHA256s:string[]
|
|
210
|
+
}
|
|
211
|
+
export interface MacSpeaker256Interval {
|
|
212
|
+
rawChunkIndex:number;audioSHA256:string;pcmSHA256:string;startSample:number;endSample:number
|
|
213
|
+
originalVoice:string;identityId:string;evidenceIds:string[]
|
|
214
|
+
/** Machine localization agreement, never independently verified speaker purity. */
|
|
215
|
+
timingBasis:'native-exclusive-intersect-machine-production'
|
|
216
|
+
}
|
|
217
|
+
export interface MacSpeaker256Group {
|
|
218
|
+
identityId:string;nativeCluster:string;memberVoices:string[];keepVoiceHint:string;groupSHA256:string
|
|
219
|
+
witnesses:MacSpeaker256Witness[];intervals:MacSpeaker256Interval[]
|
|
220
|
+
}
|
|
221
|
+
interface MacSpeaker256RawBinding {
|
|
222
|
+
rawChunkIndex:number;audioSHA256:string;pcmSHA256:string;startSample:number;endSample:number;originalRowSHA256:string
|
|
223
|
+
}
|
|
224
|
+
export interface MacSpeaker256State {
|
|
225
|
+
version:1;policy:'fluid256-recurrence-v1';captureId:string;generation:string;modelSHA256:string;cliSHA256:string
|
|
226
|
+
sourceBindingSHA256:string;snapshotSHA256:string;revision:number;identityHighWater:number
|
|
227
|
+
identities:Array<{identityId:string;ordinal:number;retired:boolean;birthSnapshotSHA256:string;birthGroupSHA256:string;birthIntervals:MacSpeaker256Interval[];lastIntervals:MacSpeaker256Interval[]}>
|
|
228
|
+
groups:MacSpeaker256Group[];withdrawn:Array<{identityId:string;reason:string}>
|
|
229
|
+
withheld:Array<{rawChunkIndex:number;startSample:number;endSample:number;reason:string}>
|
|
230
|
+
rawBindings:MacSpeaker256RawBinding[]
|
|
231
|
+
coverage:{rawOriginals:number;textOriginals:number;protectedOriginals:number;acceptedRecurrenceGroups:number;independentQualifiedGroups:number;machineSpeechSamples:number;supportedSamples:number}
|
|
232
|
+
/** Hash integrity only; native qualification belongs to the trusted producer. */
|
|
233
|
+
provenance:{nativeAuthenticated:false;productionQualified:false;originalSHA256:string;currentSHA256:string;ledgerSHA256:string;nativeSHA256:string;embeddingsSHA256:string}
|
|
234
|
+
excludedEmbeddingWindows:Array<{originalIndex:number;startTime:number;endTime:number;windowSHA256:string;reason:'window-crosses-capture-end'}>
|
|
235
|
+
stateSHA256:string
|
|
236
|
+
}
|
|
237
|
+
const FLUID256_POLICY='fluid256-recurrence-v1' as const
|
|
238
|
+
const fluidHash=(value:string)=>createHash('sha256').update(value).digest('hex')
|
|
239
|
+
const fluidSeal=(s:MacSpeaker256State)=>{const{stateSHA256:_,...body}=s;s.stateSHA256=hashMacSpeakerLive(body);return s}
|
|
240
|
+
const fluidFail=(reason:string):never=>{throw Error(`fluid256_${reason}`)}
|
|
241
|
+
const fluidGroupBody=(g:MacSpeaker256Group)=>({nativeCluster:g.nativeCluster,memberVoices:g.memberVoices,keepVoiceHint:g.keepVoiceHint,witnesses:g.witnesses,
|
|
242
|
+
intervals:g.intervals.map(({identityId:_,...i})=>i)})
|
|
243
|
+
const fluidOverlap=(a:readonly MacSpeaker256Interval[],b:readonly MacSpeaker256Interval[])=>a.some(x=>b.some(y=>x.rawChunkIndex===y.rawChunkIndex&&x.audioSHA256===y.audioSHA256&&x.pcmSHA256===y.pcmSHA256&&Math.max(x.startSample,y.startSample)<Math.min(x.endSample,y.endSample)))
|
|
244
|
+
function fluidShape(state:MacSpeaker256State):void {
|
|
245
|
+
if(!state||state.version!==1||state.policy!==FLUID256_POLICY||typeof state.captureId!=='string'||!/^mac_/.test(state.captureId)||state.captureId.length>200||typeof state.generation!=='string'||!state.generation||state.generation.length>128
|
|
246
|
+
||![state.modelSHA256,state.cliSHA256,state.sourceBindingSHA256,state.snapshotSHA256,state.stateSHA256].every(x=>SHA.test(x))
|
|
247
|
+
||!Number.isSafeInteger(state.revision)||state.revision<1||state.revision>=MAC_SPEAKER_LIVE_TERMINAL_REVISION||!Number.isSafeInteger(state.identityHighWater)||state.identityHighWater<0||state.identityHighWater>1024
|
|
248
|
+
||!Array.isArray(state.identities)||state.identities.length!==state.identityHighWater||!Array.isArray(state.groups)||state.groups.length>99||!Array.isArray(state.rawBindings)||state.rawBindings.length>6000
|
|
249
|
+
||!Array.isArray(state.withheld)||state.withheld.length>100000||!Array.isArray(state.withdrawn)||state.withdrawn.length>1024||Buffer.byteLength(JSON.stringify(state))>16*1024*1024)fluidFail('state_bound')
|
|
250
|
+
const{stateSHA256,...body}=state;if(hashMacSpeakerLive(body)!==stateSHA256)fluidFail('state_hash')
|
|
251
|
+
const raw=new Map(state.rawBindings.map(r=>[r.rawChunkIndex,r]))
|
|
252
|
+
if(raw.size!==state.rawBindings.length)fluidFail('raw_binding')
|
|
253
|
+
for(const[r,i]of state.rawBindings.map((r,i)=>[r,i] as const))if(r.rawChunkIndex!==i||![r.audioSHA256,r.pcmSHA256,r.originalRowSHA256].every(x=>SHA.test(x))||!Number.isSafeInteger(r.startSample)||!Number.isSafeInteger(r.endSample)||r.startSample<0||r.endSample<=r.startSample)fluidFail('raw_binding')
|
|
254
|
+
const validSpan=(span:MacSpeaker256Interval,identityId:string)=>{
|
|
255
|
+
const r=raw.get(span.rawChunkIndex)
|
|
256
|
+
return r&&span.identityId===identityId&&span.audioSHA256===r.audioSHA256&&span.pcmSHA256===r.pcmSHA256&&Number.isSafeInteger(span.startSample)&&Number.isSafeInteger(span.endSample)&&span.startSample>=r.startSample&&span.endSample<=r.endSample&&span.endSample>span.startSample
|
|
257
|
+
&&span.timingBasis==='native-exclusive-intersect-machine-production'&&isVoiceLabel(span.originalVoice)&&Array.isArray(span.evidenceIds)&&span.evidenceIds.length===2&&span.evidenceIds.every(h=>SHA.test(h))&&span.evidenceIds[0]!==span.evidenceIds[1]
|
|
258
|
+
}
|
|
259
|
+
const ids=new Set<string>()
|
|
260
|
+
for(const[i,id]of state.identities.entries()){
|
|
261
|
+
if(id.ordinal!==i+1||!/^fluid256_[a-f0-9]{64}$/.test(id.identityId)||ids.has(id.identityId)||typeof id.retired!=='boolean'||!Array.isArray(id.lastIntervals)||!id.lastIntervals.length||id.lastIntervals.length>40000||!Array.isArray(id.birthIntervals)||!id.birthIntervals.length||id.birthIntervals.length>40000
|
|
262
|
+
||!SHA.test(id.birthSnapshotSHA256)||!SHA.test(id.birthGroupSHA256)||id.identityId!==`fluid256_${hashMacSpeakerLive({captureId:state.captureId,generation:state.generation,snapshotSHA256:id.birthSnapshotSHA256,groupSHA256:id.birthGroupSHA256,ordinal:id.ordinal})}`)fluidFail('namespace')
|
|
263
|
+
if([...id.birthIntervals,...id.lastIntervals].some(span=>!validSpan(span,id.identityId)))fluidFail('identity_interval_binding')
|
|
264
|
+
ids.add(id.identityId)
|
|
265
|
+
}
|
|
266
|
+
const groups=new Set<string>()
|
|
267
|
+
for(const group of state.groups){
|
|
268
|
+
const identity=state.identities.find(i=>i.identityId===group.identityId)
|
|
269
|
+
if(!identity||identity.retired||groups.has(group.identityId)||group.groupSHA256!==hashMacSpeakerLive(fluidGroupBody(group))||group.witnesses.length!==2||!group.intervals.length
|
|
270
|
+
||hashMacSpeakerLive(group.intervals)!==hashMacSpeakerLive(identity.lastIntervals)||!fluidOverlap(group.intervals,identity.birthIntervals))fluidFail('group_binding')
|
|
271
|
+
const[a,b]=group.witnesses
|
|
272
|
+
if(a.evidenceId===b.evidenceId||a.originalVoice===b.originalVoice||Math.max(a.startSample,b.startSample)<Math.min(a.endSample,b.endSample)
|
|
273
|
+
||a.rawChunkIndices.some(r=>b.rawChunkIndices.includes(r))||a.pcmSHA256s.some(h=>b.pcmSHA256s.includes(h)))fluidFail('dependent_witnesses')
|
|
274
|
+
for(const span of group.intervals)if(!validSpan(span,group.identityId)||hashMacSpeakerLive(span.evidenceIds)!==hashMacSpeakerLive(group.witnesses.map(w=>w.evidenceId)))fluidFail('interval_binding')
|
|
275
|
+
groups.add(group.identityId)
|
|
276
|
+
}
|
|
277
|
+
if(state.identities.some(id=>id.retired===groups.has(id.identityId)))fluidFail('active_namespace')
|
|
278
|
+
const withdrawals=new Set<string>()
|
|
279
|
+
for(const w of state.withdrawn){if(withdrawals.has(w.identityId)||!state.identities.some(id=>id.identityId===w.identityId&&id.retired)||w.reason!=='recurrence-withdrawn-or-split-merge')fluidFail('withdrawal_binding');withdrawals.add(w.identityId)}
|
|
280
|
+
}
|
|
281
|
+
type FluidCandidate=Omit<MacSpeaker256Group,'identityId'>
|
|
282
|
+
function fluidCandidates(input:MacSpeaker256SnapshotInput){
|
|
283
|
+
if(typeof input.generation!=='string'||!input.generation||input.generation.length>128)fluidFail('generation')
|
|
284
|
+
const{generation:_,expectedStateSHA256:__,...source}=input
|
|
285
|
+
// Full-source inspection binds exports, protects human/prior-proof labels and
|
|
286
|
+
// rejects source/schema/clock drift before the unchanged full-cohort decision.
|
|
287
|
+
const inspected=inspectMacSpeakerShadowCallSource(source)
|
|
288
|
+
const{original,current,entries,production,embeddings,decision:D}=inspected
|
|
289
|
+
const protectedRaw=new Set(inspected.protection),raw=new Map(input.rawAudio.map(r=>[r.rawChunkIndex,r])),placed=new Map(input.timeline.rows.map(r=>[r.rawChunkIndex,r]))
|
|
290
|
+
const currentRaw=new Map<number,Record<string,any>>(current.chunkEntries.map((e:any)=>[e.chunkIndex,e.chunk]))
|
|
291
|
+
const rows=(c:any)=>[c,...(c.segments??[]),...(c.finalSegments??[]),...(c.words??[]).filter((r:any)=>'speaker'in r)]
|
|
292
|
+
const sourceRows=entries.map(e=>{const r=raw.get(e.chunkIndex)!,p=placed.get(e.chunkIndex)!;return{rawChunkIndex:e.chunkIndex,audioSHA256:r.wavSHA256,pcmSHA256:r.pcmSHA256,startSample:p.startSample,endSample:p.startSample+p.sampleCount,originalRowSHA256:hashMacSpeakerLive(e.chunk)}})
|
|
293
|
+
const segmentsByRaw=new Map<number,Array<[number,number,string]>>()
|
|
294
|
+
for(let i=0;i<production.segs.length;i++){const id=production.sources[i].chunkIndex,list=segmentsByRaw.get(id)??[];list.push([...production.segs[i]]);segmentsByRaw.set(id,list)}
|
|
295
|
+
// Overlapping machine segments are ambiguous localization, not extra speech
|
|
296
|
+
// seconds or independent evidence. Refuse rather than double-count them.
|
|
297
|
+
for(const list of segmentsByRaw.values()){
|
|
298
|
+
list.sort((a,b)=>a[0]-b[0]||a[1]-b[1])
|
|
299
|
+
for(let i=1;i<list.length;i++)if(list[i][0]<list[i-1][1])fluidFail('production_overlap')
|
|
300
|
+
}
|
|
301
|
+
const clean=(index:number,voice:string)=>{const e=entries[index],p=placed.get(index)!,now=currentRaw.get(index)!;return !protectedRaw.has(index)&&p.usedSampleCount===p.sampleCount
|
|
302
|
+
&&rows(e.chunk).every(r=>r.speaker===voice)&&rows(now).every(r=>r.speaker===voice)&&hashMacSpeakerLive(rows(e.chunk))===hashMacSpeakerLive(rows(now))}
|
|
303
|
+
const candidates:FluidCandidate[]=[],withheld:MacSpeaker256State['withheld']=[];let accepted=0,machineSpeechSamples=0
|
|
304
|
+
const acceptedVoices=new Map<string,{cluster:string;hint:string;members:string[]}>()
|
|
305
|
+
for(const g of D.groups){if(!g.anchor||!g.mergedIntoAnchor.length)continue;accepted++;const members=[g.anchor,...g.mergedIntoAnchor].sort();for(const voice of members)acceptedVoices.set(voice,{cluster:g.cluster,hint:g.anchor,members})}
|
|
306
|
+
const uniqueGroups=[...new Map([...acceptedVoices.values()].map(g=>[g.cluster,g])).values()]
|
|
307
|
+
for(const group of uniqueGroups){
|
|
308
|
+
const witnesses:MacSpeaker256Witness[]=[]
|
|
309
|
+
for(const e of embeddings){
|
|
310
|
+
if(`S${e.cluster+1}`!==group.cluster)continue
|
|
311
|
+
const lo=pyRoundInt(e.startTime*16000),hi=pyRoundInt(e.endTime*16000)
|
|
312
|
+
if(hi<=lo||[...exactOverlap(lo,hi,D.index)].some(([key])=>key!==group.cluster))continue
|
|
313
|
+
const touched=sourceRows.filter(r=>r.startSample<hi&&r.endSample>lo)
|
|
314
|
+
if(!touched.length)continue
|
|
315
|
+
const speech=production.segs.flatMap((s,i)=>s[0]<hi&&s[1]>lo?[{segment:s,raw:production.sources[i].chunkIndex}]:[])
|
|
316
|
+
const voices=new Set(speech.map(s=>s.segment[2]));if(voices.size!==1)continue
|
|
317
|
+
const voice=[...voices][0];if(!group.members.includes(voice)||speech.some(s=>!clean(s.raw,voice))||touched.some(r=>!clean(r.rawChunkIndex,voice)))continue
|
|
318
|
+
// The unchanged port attributes embeddings at50percent production coverage.
|
|
319
|
+
// Repeated/overlapping exports are still only ONE source witness below.
|
|
320
|
+
const speechSamples=speech.reduce((n,s)=>n+Math.max(0,Math.min(hi,s.segment[1])-Math.max(lo,s.segment[0])),0)
|
|
321
|
+
if(speechSamples<.5*(hi-lo))continue
|
|
322
|
+
witnesses.push({evidenceId:hashMacSpeakerLive({modelSHA256:input.cohort.modelSHA256,cliSHA256:input.cohort.cliSHA256,embedding:e,raw:touched}),startSample:lo,endSample:hi,originalVoice:voice,rawChunkIndices:touched.map(r=>r.rawChunkIndex),pcmSHA256s:touched.map(r=>r.pcmSHA256)})
|
|
323
|
+
}
|
|
324
|
+
witnesses.sort((a,b)=>a.startSample-b.startSample||a.endSample-b.endSample||a.evidenceId.localeCompare(b.evidenceId))
|
|
325
|
+
let pair:MacSpeaker256Witness[]|undefined
|
|
326
|
+
for(let i=0;i<witnesses.length&&!pair;i++)for(let j=i+1;j<witnesses.length;j++){
|
|
327
|
+
const a=witnesses[i],b=witnesses[j]
|
|
328
|
+
if(a.originalVoice!==b.originalVoice&&a.endSample<=b.startSample&&!a.rawChunkIndices.some(r=>b.rawChunkIndices.includes(r))&&!a.pcmSHA256s.some(h=>b.pcmSHA256s.includes(h))){pair=[a,b];break}
|
|
329
|
+
}
|
|
330
|
+
if(!pair)continue
|
|
331
|
+
const intervals:MacSpeaker256Interval[]=[]
|
|
332
|
+
for(const e of entries){
|
|
333
|
+
const voice=e.chunk.speaker as string;if(!group.members.includes(voice)||!clean(e.chunkIndex,voice))continue
|
|
334
|
+
const r=raw.get(e.chunkIndex)!
|
|
335
|
+
for(const[lo,hi,label]of segmentsByRaw.get(e.chunkIndex)??[]){
|
|
336
|
+
if(label!==voice)continue
|
|
337
|
+
const boundaries=new Set<number>([lo,hi]);for(const[a,b]of D.index.overlapping(lo,hi)){boundaries.add(Math.max(lo,a));boundaries.add(Math.min(hi,b))}
|
|
338
|
+
const points=[...boundaries].sort((a,b)=>a-b)
|
|
339
|
+
for(let i=0;i+1<points.length;i++){const startSample=points[i],endSample=points[i+1],cover=exactOverlap(startSample,endSample,D.index)
|
|
340
|
+
if(cover.size===1&&cover.has(group.cluster))intervals.push({rawChunkIndex:e.chunkIndex,audioSHA256:r.wavSHA256,pcmSHA256:r.pcmSHA256,startSample,endSample,originalVoice:voice,identityId:'',evidenceIds:pair.map(w=>w.evidenceId),timingBasis:'native-exclusive-intersect-machine-production'})
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
intervals.sort((a,b)=>a.rawChunkIndex-b.rawChunkIndex||a.startSample-b.startSample||a.endSample-b.endSample)
|
|
345
|
+
if(!intervals.length)continue
|
|
346
|
+
const body={nativeCluster:group.cluster,memberVoices:group.members,keepVoiceHint:group.hint,witnesses:pair,intervals}
|
|
347
|
+
const groupSHA256=hashMacSpeakerLive(fluidGroupBody({...body,identityId:'',groupSHA256:''}))
|
|
348
|
+
candidates.push({...body,groupSHA256})
|
|
349
|
+
}
|
|
350
|
+
for(const e of entries){
|
|
351
|
+
for(const[lo,hi]of segmentsByRaw.get(e.chunkIndex)??[]){
|
|
352
|
+
machineSpeechSamples+=hi-lo
|
|
353
|
+
const spans=candidates.flatMap(g=>g.intervals.filter(i=>i.rawChunkIndex===e.chunkIndex&&i.startSample<hi&&i.endSample>lo))
|
|
354
|
+
const points=[...new Set([lo,hi,...spans.flatMap(i=>[Math.max(lo,i.startSample),Math.min(hi,i.endSample)])])].sort((a,b)=>a-b)
|
|
355
|
+
for(let i=0;i+1<points.length;i++)if(!spans.some(s=>s.startSample<=points[i]&&s.endSample>=points[i+1]))withheld.push({rawChunkIndex:e.chunkIndex,startSample:points[i],endSample:points[i+1],reason:protectedRaw.has(e.chunkIndex)?'protected-source':'no-independent-exclusive-recurrence'})
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return{captureId:original.sessionId as string,candidates,withheld,sourceRows,excludedEmbeddingWindows:inspected.excludedEmbeddingWindows,coverage:{rawOriginals:entries.length,textOriginals:original.chunks.length,protectedOriginals:protectedRaw.size,acceptedRecurrenceGroups:accepted,independentQualifiedGroups:candidates.length,machineSpeechSamples,supportedSamples:candidates.reduce((n,g)=>n+g.intervals.reduce((m,i)=>m+i.endSample-i.startSample,0),0)}}
|
|
359
|
+
}
|
|
360
|
+
/** Whole supplied-snapshot reconciliation, never a global old-label rename.
|
|
361
|
+
* The trusted caller must prove full native/source completeness independently. */
|
|
362
|
+
export function reconcileMacSpeaker256Snapshot(previous:MacSpeaker256State|null,input:MacSpeaker256SnapshotInput):MacSpeaker256State {
|
|
363
|
+
const evaluated=fluidCandidates(input),snapshotSHA256=hashMacSpeakerLive({sourceBindingSHA256:input.sourceBindingSHA256,generation:input.generation,policy:FLUID256_POLICY})
|
|
364
|
+
if(previous){
|
|
365
|
+
fluidShape(previous)
|
|
366
|
+
if(input.expectedStateSHA256!==previous.stateSHA256)fluidFail('state_cas')
|
|
367
|
+
if(previous.captureId!==evaluated.captureId||previous.generation!==input.generation||previous.modelSHA256!==input.cohort.modelSHA256||previous.cliSHA256!==input.cohort.cliSHA256)fluidFail('model_or_capture_drift')
|
|
368
|
+
if(previous.rawBindings.some((r,i)=>hashMacSpeakerLive(r)!==hashMacSpeakerLive(evaluated.sourceRows[i])))fluidFail('original_source_drift')
|
|
369
|
+
if(previous.snapshotSHA256===snapshotSHA256){validateMacSpeaker256State(previous,input);return structuredClone(previous)}
|
|
370
|
+
if(previous.revision>=MAC_SPEAKER_LIVE_TERMINAL_REVISION-1)fluidFail('revision_exhausted')
|
|
371
|
+
}else if(input.expectedStateSHA256!==undefined)fluidFail('unexpected_state_cas')
|
|
372
|
+
const identities=structuredClone(previous?.identities??[]),groups:MacSpeaker256Group[]=[],withdrawn:MacSpeaker256State['withdrawn']=[],withheld=[...evaluated.withheld]
|
|
373
|
+
let identityHighWater=previous?.identityHighWater??0
|
|
374
|
+
const old=previous?.groups??[]
|
|
375
|
+
const oldLinks=old.map(g=>evaluated.candidates.flatMap((n,i)=>fluidOverlap(g.intervals,n.intervals)?[i]:[]))
|
|
376
|
+
const newLinks=evaluated.candidates.map(g=>old.flatMap((o,i)=>fluidOverlap(g.intervals,o.intervals)?[i]:[]))
|
|
377
|
+
for(let i=0;i<evaluated.candidates.length;i++){
|
|
378
|
+
const candidate=evaluated.candidates[i],links=newLinks[i]
|
|
379
|
+
const ambiguous=links.length>1||links.some(j=>oldLinks[j].length!==1)||identities.some(id=>id.retired&&fluidOverlap(id.lastIntervals,candidate.intervals))
|
|
380
|
+
if(ambiguous){for(const span of candidate.intervals)withheld.push({rawChunkIndex:span.rawChunkIndex,startSample:span.startSample,endSample:span.endSample,reason:'split-merge-or-retired-lineage'});continue}
|
|
381
|
+
let identity=links.length?identities.find(id=>id.identityId===old[links[0]].identityId):undefined
|
|
382
|
+
if(!identity){
|
|
383
|
+
if(identityHighWater>=1024)fluidFail('identity_budget_exhausted')
|
|
384
|
+
const ordinal=++identityHighWater,identityId=`fluid256_${hashMacSpeakerLive({captureId:evaluated.captureId,generation:input.generation,snapshotSHA256,groupSHA256:candidate.groupSHA256,ordinal})}`
|
|
385
|
+
identity={identityId,ordinal,retired:false,birthSnapshotSHA256:snapshotSHA256,birthGroupSHA256:candidate.groupSHA256,birthIntervals:candidate.intervals.map(i=>({...i,identityId})),lastIntervals:[]};identities.push(identity)
|
|
386
|
+
}
|
|
387
|
+
const intervals=candidate.intervals.map(i=>({...i,identityId:identity!.identityId}));identity.lastIntervals=intervals
|
|
388
|
+
groups.push({...candidate,identityId:identity.identityId,intervals})
|
|
389
|
+
}
|
|
390
|
+
for(const identity of identities)if(!identity.retired&&!groups.some(g=>g.identityId===identity.identityId)){
|
|
391
|
+
identity.retired=true;withdrawn.push({identityId:identity.identityId,reason:'recurrence-withdrawn-or-split-merge'})
|
|
392
|
+
}
|
|
393
|
+
const supportedSamples=groups.reduce((n,g)=>n+g.intervals.reduce((m,i)=>m+i.endSample-i.startSample,0),0)
|
|
394
|
+
const state=fluidSeal({version:1,policy:FLUID256_POLICY,captureId:evaluated.captureId,generation:input.generation,modelSHA256:input.cohort.modelSHA256,cliSHA256:input.cohort.cliSHA256,sourceBindingSHA256:input.sourceBindingSHA256,snapshotSHA256,
|
|
395
|
+
revision:(previous?.revision??0)+1,identityHighWater,identities,groups,withdrawn,withheld,rawBindings:evaluated.sourceRows,coverage:{...evaluated.coverage,supportedSamples},
|
|
396
|
+
provenance:{nativeAuthenticated:false,productionQualified:false,originalSHA256:fluidHash(input.originalJSON),currentSHA256:fluidHash(input.currentJSON),ledgerSHA256:fluidHash(input.ledgerJSONL),nativeSHA256:fluidHash(input.cohort.nativeJSON),embeddingsSHA256:fluidHash(input.cohort.embeddingsJSON)},excludedEmbeddingWindows:evaluated.excludedEmbeddingWindows,stateSHA256:''})
|
|
397
|
+
fluidShape(state);return state
|
|
398
|
+
}
|
|
399
|
+
/** Recompute current complete-cohort membership/intervals; a resealed hash alone
|
|
400
|
+
* cannot invent acoustic support or move an interval on restart. */
|
|
401
|
+
export function validateMacSpeaker256State(state:MacSpeaker256State,input:MacSpeaker256SnapshotInput):void {
|
|
402
|
+
fluidShape(state);const expected=fluidCandidates(input)
|
|
403
|
+
if(state.captureId!==expected.captureId||state.generation!==input.generation||state.sourceBindingSHA256!==input.sourceBindingSHA256||state.modelSHA256!==input.cohort.modelSHA256||state.cliSHA256!==input.cohort.cliSHA256
|
|
404
|
+
||state.snapshotSHA256!==hashMacSpeakerLive({sourceBindingSHA256:input.sourceBindingSHA256,generation:input.generation,policy:FLUID256_POLICY})||hashMacSpeakerLive(state.rawBindings)!==hashMacSpeakerLive(expected.sourceRows))fluidFail('snapshot_binding')
|
|
405
|
+
for(const g of state.groups){
|
|
406
|
+
const candidate=expected.candidates.find(c=>c.groupSHA256===g.groupSHA256)
|
|
407
|
+
if(!candidate||hashMacSpeakerLive(fluidGroupBody(g))!==hashMacSpeakerLive(fluidGroupBody({...candidate,identityId:''})))fluidFail('membership_or_interval_drift')
|
|
408
|
+
}
|
|
409
|
+
const supportedSamples=state.groups.reduce((n,g)=>n+g.intervals.reduce((m,i)=>m+i.endSample-i.startSample,0),0)
|
|
410
|
+
const withheld=[...expected.withheld,...expected.candidates.filter(c=>!state.groups.some(g=>g.groupSHA256===c.groupSHA256)).flatMap(c=>c.intervals.map(i=>({rawChunkIndex:i.rawChunkIndex,startSample:i.startSample,endSample:i.endSample,reason:'split-merge-or-retired-lineage'})))]
|
|
411
|
+
const provenance={nativeAuthenticated:false,productionQualified:false,originalSHA256:fluidHash(input.originalJSON),currentSHA256:fluidHash(input.currentJSON),ledgerSHA256:fluidHash(input.ledgerJSONL),nativeSHA256:fluidHash(input.cohort.nativeJSON),embeddingsSHA256:fluidHash(input.cohort.embeddingsJSON)}
|
|
412
|
+
if(hashMacSpeakerLive(state.coverage)!==hashMacSpeakerLive({...expected.coverage,supportedSamples})||hashMacSpeakerLive(state.excludedEmbeddingWindows)!==hashMacSpeakerLive(expected.excludedEmbeddingWindows)
|
|
413
|
+
||hashMacSpeakerLive(state.withheld)!==hashMacSpeakerLive(withheld)||hashMacSpeakerLive(state.provenance)!==hashMacSpeakerLive(provenance))fluidFail('coverage_or_provenance_drift')
|
|
414
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Label-only presentation of already-qualified Mac acoustic evidence. The writer
|
|
2
|
+
// authenticates native/profile receipts before calling this pure projection;
|
|
3
|
+
// source geometry is checked again here and the original ASR stays immutable.
|
|
4
|
+
import { createHash } from 'node:crypto'
|
|
5
|
+
import { isVoiceLabel } from './mac-voice-labels.js'
|
|
6
|
+
import { hasMacSpeakerProtectedRows } from './mac-speaker-protection.js'
|
|
7
|
+
import { checkSpeakerName } from './speaker-name.js'
|
|
8
|
+
|
|
9
|
+
export interface MacSpeakerLiveProjectionSpan {
|
|
10
|
+
rawChunkIndex: number
|
|
11
|
+
audioSHA256: string
|
|
12
|
+
pcmSHA256: string
|
|
13
|
+
/** Capture-absolute 16 kHz sample coordinates for one entire original. */
|
|
14
|
+
startSample: number
|
|
15
|
+
endSample: number
|
|
16
|
+
identityId: string
|
|
17
|
+
displayLabel: string
|
|
18
|
+
profileName?: string
|
|
19
|
+
evidenceIds: string[]
|
|
20
|
+
}
|
|
21
|
+
export interface MacSpeakerLiveSourceChunk {
|
|
22
|
+
text: string
|
|
23
|
+
speaker: string
|
|
24
|
+
elapsed: number
|
|
25
|
+
audioSha256?: string
|
|
26
|
+
audioDurationMs?: number
|
|
27
|
+
}
|
|
28
|
+
export interface MacSpeakerLiveIndexedChunk<T extends MacSpeakerLiveSourceChunk = MacSpeakerLiveSourceChunk> { i: number; c: T }
|
|
29
|
+
const sha = (v: unknown) => createHash('sha256').update(JSON.stringify(v)).digest('hex')
|
|
30
|
+
const HASH = /^[a-f0-9]{64}$/
|
|
31
|
+
const uint = (v: unknown): v is number => Number.isSafeInteger(v) && Number(v) >= 0
|
|
32
|
+
const anonymous = (v: unknown) => typeof v === 'string' && (['', 'Ext', 'Unknown', 'Unidentified'].includes(v) || isVoiceLabel(v))
|
|
33
|
+
const samples = (ms: unknown): number | null => {
|
|
34
|
+
if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) return null
|
|
35
|
+
const n = ms * 16, rounded = Math.round(n)
|
|
36
|
+
return Number.isSafeInteger(rounded) && Math.abs(n - rounded) <= 1e-6 ? rounded : null
|
|
37
|
+
}
|
|
38
|
+
type Row = Record<string, unknown>
|
|
39
|
+
function canProject(c: MacSpeakerLiveSourceChunk): boolean {
|
|
40
|
+
if (!anonymous(c.speaker) || hasMacSpeakerProtectedRows(c)) return false
|
|
41
|
+
const root = c as unknown as Row
|
|
42
|
+
if (root.overlap === true || root.mixedSpeakers === true) return false
|
|
43
|
+
for (const key of ['segments', 'finalSegments', 'words']) {
|
|
44
|
+
if (root[key] === undefined) continue
|
|
45
|
+
if (!(root[key] as unknown[]).every(value => {
|
|
46
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false
|
|
47
|
+
const row = value as Row
|
|
48
|
+
return row.overlap !== true && row.mixedSpeakers !== true
|
|
49
|
+
&& (key === 'words' && row.speaker === undefined || anonymous(row.speaker))
|
|
50
|
+
// Different automatic Voice labels in one original are mixed evidence.
|
|
51
|
+
&& (row.speaker === undefined || row.speaker === c.speaker)
|
|
52
|
+
})) return false
|
|
53
|
+
}
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function projectMacSpeakerLiveChunks<T extends MacSpeakerLiveSourceChunk>(input: {
|
|
58
|
+
captureId: string
|
|
59
|
+
generation: string
|
|
60
|
+
chunksIndexed: readonly MacSpeakerLiveIndexedChunk<T>[]
|
|
61
|
+
projections: readonly MacSpeakerLiveProjectionSpan[]
|
|
62
|
+
}): {
|
|
63
|
+
speakerProjectedChunksIndexed: MacSpeakerLiveIndexedChunk<T>[]
|
|
64
|
+
effectiveOverlaySHA256: string
|
|
65
|
+
sourceBindingSHA256: string
|
|
66
|
+
appliedRawChunkIndices: number[]
|
|
67
|
+
withheld: Array<{ rawChunkIndex: number; reason: string }>
|
|
68
|
+
} {
|
|
69
|
+
if (!/^mac_[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(input.captureId) || typeof input.generation !== 'string' || !input.generation.length || input.generation.length > 128
|
|
70
|
+
|| !Array.isArray(input.chunksIndexed) || input.chunksIndexed.length > 10_000 || !Array.isArray(input.projections) || input.projections.length > 10_000) throw Error('invalid Mac live projection envelope')
|
|
71
|
+
const originals = [...input.chunksIndexed].sort((a, b) => a.i - b.i)
|
|
72
|
+
const byIndex = new Map<number, MacSpeakerLiveIndexedChunk<T>>()
|
|
73
|
+
for (const row of originals) {
|
|
74
|
+
if (!row || !uint(row.i) || row.i >= 10_000 || byIndex.has(row.i) || !row.c || typeof row.c.text !== 'string' || !row.c.text.trim()
|
|
75
|
+
|| typeof row.c.speaker !== 'string' || samples(row.c.elapsed) === null) throw Error('invalid Mac live original chunks')
|
|
76
|
+
byIndex.set(row.i, row)
|
|
77
|
+
}
|
|
78
|
+
const spans = new Map<number, MacSpeakerLiveProjectionSpan>(), labels = new Map<string, string>(), identities = new Map<string, string>()
|
|
79
|
+
for (const p of input.projections) {
|
|
80
|
+
if (!p || !uint(p.rawChunkIndex) || p.rawChunkIndex >= 10_000 || spans.has(p.rawChunkIndex) || !HASH.test(p.audioSHA256) || !HASH.test(p.pcmSHA256)
|
|
81
|
+
|| !uint(p.startSample) || !uint(p.endSample) || p.endSample <= p.startSample || p.endSample - p.startSample > 480_000
|
|
82
|
+
|| !/^acoustic_[a-f0-9]{16,64}$/.test(p.identityId) || !Array.isArray(p.evidenceIds) || p.evidenceIds.length < 2 || p.evidenceIds.length > 64
|
|
83
|
+
|| new Set(p.evidenceIds).size !== p.evidenceIds.length || p.evidenceIds.some((id: unknown) => typeof id !== 'string' || !/^[A-Za-z0-9_:-]{1,160}$/.test(id))
|
|
84
|
+
|| (p.profileName !== undefined ? !checkSpeakerName(p.profileName).ok || p.displayLabel !== p.profileName : !isVoiceLabel(p.displayLabel))) throw Error('invalid Mac live projection span')
|
|
85
|
+
if (identities.has(p.identityId) && identities.get(p.identityId) !== p.displayLabel) throw Error('inconsistent Mac live identity label')
|
|
86
|
+
identities.set(p.identityId, p.displayLabel)
|
|
87
|
+
if (p.profileName === undefined) {
|
|
88
|
+
if (labels.has(p.displayLabel) && labels.get(p.displayLabel) !== p.identityId) throw Error('duplicate Mac live voice label')
|
|
89
|
+
labels.set(p.displayLabel, p.identityId)
|
|
90
|
+
}
|
|
91
|
+
spans.set(p.rawChunkIndex, p)
|
|
92
|
+
}
|
|
93
|
+
const ordered = [...spans.values()].sort((a, b) => a.startSample - b.startSample)
|
|
94
|
+
if (ordered.some((p, i) => i > 0 && ordered[i - 1].endSample > p.startSample)) throw Error('overlapping Mac live originals')
|
|
95
|
+
// Old Voice labels are scheduling hints, never acoustic identity. An unused
|
|
96
|
+
// namespace prevents a newly settled group from swallowing unsupported rows.
|
|
97
|
+
for (const row of originals) if (labels.has(row.c.speaker)) {
|
|
98
|
+
const p = spans.get(row.i)
|
|
99
|
+
if (!p || p.identityId !== labels.get(row.c.speaker) || !canProject(row.c)
|
|
100
|
+
|| row.c.audioSha256 !== p.audioSHA256 || samples(row.c.elapsed) !== p.startSample
|
|
101
|
+
|| samples(row.c.audioDurationMs) !== p.endSample - p.startSample) throw Error('Mac live voice label collides with original')
|
|
102
|
+
}
|
|
103
|
+
const withheld: Array<{ rawChunkIndex: number; reason: string }> = [], appliedRawChunkIndices: number[] = [], changes: unknown[] = []
|
|
104
|
+
for (const p of input.projections) if (!byIndex.has(p.rawChunkIndex)) withheld.push({ rawChunkIndex: p.rawChunkIndex, reason: 'original_missing' })
|
|
105
|
+
const projected = originals.map(({ i, c }) => {
|
|
106
|
+
const next = structuredClone(c), p = spans.get(i)
|
|
107
|
+
if (!p) return { i, c: next }
|
|
108
|
+
const start = samples(c.elapsed), count = samples(c.audioDurationMs)
|
|
109
|
+
let reason: string | undefined
|
|
110
|
+
if (c.audioSha256 !== p.audioSHA256 || start !== p.startSample || count === null || count !== p.endSample - p.startSample) reason = 'original_binding_changed'
|
|
111
|
+
else if (!canProject(c)) reason = 'protected_or_mixed_original'
|
|
112
|
+
if (reason) { withheld.push({ rawChunkIndex: i, reason }); return { i, c: next } }
|
|
113
|
+
next.speaker = p.displayLabel
|
|
114
|
+
for (const key of ['segments', 'finalSegments', 'words']) {
|
|
115
|
+
const rows = (next as unknown as Row)[key] as Row[] | undefined
|
|
116
|
+
for (const row of rows ?? []) if (row.speaker !== undefined) row.speaker = p.displayLabel
|
|
117
|
+
}
|
|
118
|
+
appliedRawChunkIndices.push(i)
|
|
119
|
+
// Do not include unrelated appended ASR in this digest. The writer advances
|
|
120
|
+
// speakerRevision only when an effective claim changes, including withdrawal.
|
|
121
|
+
changes.push({ i, source: sha(c), projection: p })
|
|
122
|
+
return { i, c: next }
|
|
123
|
+
})
|
|
124
|
+
return { speakerProjectedChunksIndexed: projected, sourceBindingSHA256: sha({ captureId: input.captureId, generation: input.generation, originals }),
|
|
125
|
+
effectiveOverlaySHA256: sha({ captureId: input.captureId, generation: input.generation, changes }), appliedRawChunkIndices, withheld }
|
|
126
|
+
}
|