@gotcos/glasses-server 6.95.0 → 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 +157 -0
- package/README.md +4 -0
- package/managed-runtime-contract.json +13 -0
- package/package.json +3 -1
- package/server/index.ts +12 -0
- 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/decision-provider.ts +4 -2
- 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 +44 -7
- package/server/lib/held-voice-groups.ts +5 -4
- package/server/lib/job-audit-caller.ts +20 -0
- package/server/lib/keyed-sessions.ts +21 -1
- 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 +46 -27
- 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-corrections.ts +2 -1
- 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-relabel-enrolment.ts +2 -1
- package/server/lib/meeting-relabel.ts +17 -10
- package/server/lib/meeting-speaker-review.ts +187 -21
- 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/session-find-ask.ts +288 -0
- package/server/lib/session-find-collect.ts +715 -0
- package/server/lib/session-find-redact.ts +254 -0
- package/server/lib/session-find.ts +345 -0
- package/server/lib/speaker-calibration-log.ts +3 -2
- package/server/lib/speaker-embeddings.ts +46 -35
- package/server/lib/speaker-name-nfc.ts +37 -0
- package/server/lib/speaker-rename-fanout.ts +7 -6
- package/server/lib/speaker-shadow-decide.ts +59 -22
- 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 +2 -2
- package/server/lib/speaker-shadow-worker.ts +50 -15
- package/server/lib/speaker-shadow.ts +80 -19
- package/server/lib/speaker-trainer.ts +2 -1
- package/server/lib/token-audit.ts +36 -6
- package/server/lib/transcript-cleanup.ts +3 -2
- package/server/lib/voice-directory.ts +10 -5
- package/server/lib/voice-profile-store.ts +24 -14
- 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/decision-provider.ts +4 -1
- package/server/routes/jev.ts +5 -1
- package/server/routes/live-cues.ts +36 -14
- package/server/routes/meeting.ts +515 -61
- package/server/routes/session-find.ts +73 -0
- package/server/routes/transcribe-stream.ts +114 -33
- package/server/routes/voice.ts +28 -18
- 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,90 @@
|
|
|
1
|
+
/** Pure bridge from qualified live evidence to the existing closed-file repair
|
|
2
|
+
* contract. No IO, inference, enrollment, or implied authorization to write.
|
|
3
|
+
* Caller must verify current audio/model/profile/ledger bytes and journal all
|
|
4
|
+
* files before applying the returned receipt. */
|
|
5
|
+
import { refreshMacSpeakerLiveState, replayMacSpeakerLiveEvidence, validateMacSpeakerLiveState, hashMacSpeakerLive, type MacSpeakerLiveState } from './mac-speaker-live-memory.js'
|
|
6
|
+
import { projectMacSpeakerLiveChunks, type MacSpeakerLiveSourceChunk } from './mac-speaker-live-projection.js'
|
|
7
|
+
import { normalizedSpeakerCosine, type MacSpeakerProfile } from './mac-speaker-reconcile.js'
|
|
8
|
+
import { hashMacSpeakerDocument, projectMacSpeakerRepair, type MacProfileMatchEvidence, type MacSpeakerRepairAssignment, type MacSpeakerRepairPlan, type MacSpeakerWordBinding } from './mac-speaker-repair-projection.js'
|
|
9
|
+
type Doc = Record<string, any>
|
|
10
|
+
const refuse = (why: string): never => { throw Error(`Mac live save: ${why}`) }
|
|
11
|
+
const MAX_RAW = 8192
|
|
12
|
+
/** Verified PCM counts include silent originals, because saved batch word
|
|
13
|
+
* clocks concatenate audio files rather than using capture elapsed time. */
|
|
14
|
+
export function planMacSpeakerLiveSave(input: {
|
|
15
|
+
doc: Doc; state: MacSpeakerLiveState; profiles: readonly MacSpeakerProfile[]
|
|
16
|
+
rawAudioSampleCounts: Record<string, number>; correctionHistoryClear: boolean
|
|
17
|
+
}): MacSpeakerRepairPlan {
|
|
18
|
+
const { doc, state, profiles, rawAudioSampleCounts: counts } = input
|
|
19
|
+
validateMacSpeakerLiveState(state)
|
|
20
|
+
if (!input.correctionHistoryClear || state.withheldReason || doc.sessionId !== state.captureId
|
|
21
|
+
|| doc.finalizationState !== 'complete' || doc.sourceType !== 'mac_system_audio'
|
|
22
|
+
|| doc.speakerRepair || (doc.correctionRevision ?? 0) !== 0 || doc.batchApplied !== true
|
|
23
|
+
|| !Array.isArray(doc.chunkEntries) || !Array.isArray(doc.batchSegments)
|
|
24
|
+
|| doc.chunkEntries.length > MAX_RAW || !doc.batchSegments.length) refuse('ineligible source or human history')
|
|
25
|
+
// An unkeyed integrity hash cannot authenticate caller-supplied membership.
|
|
26
|
+
// Rebuild membership from each source receipt; namespace reservation only
|
|
27
|
+
// affects presentation labels, not acoustic memory or receipt assignment.
|
|
28
|
+
const rebuilt = replayMacSpeakerLiveEvidence({ captureId: state.captureId, generation: state.generation,
|
|
29
|
+
modelSHA256: state.modelSHA256, profileSHA256: state.profileSHA256, evidence: state.evidence, profiles })
|
|
30
|
+
if (hashMacSpeakerLive(rebuilt.memory) !== hashMacSpeakerLive(state.memory)
|
|
31
|
+
|| hashMacSpeakerLive(rebuilt.evidence.map(e => e.cohortId ?? null)) !== hashMacSpeakerLive(state.evidence.map(e => e.cohortId ?? null))) refuse('unverified receipt membership')
|
|
32
|
+
// Recompute the complete overlay against the frozen full profile cohort.
|
|
33
|
+
const replay = refreshMacSpeakerLiveState(state, profiles)
|
|
34
|
+
if (hashMacSpeakerLive(replay.projections) !== hashMacSpeakerLive(state.projections)) refuse('stale identity or profile claims')
|
|
35
|
+
const indexed = doc.chunkEntries.filter((e: Doc) => e.chunk?.text).map((e: Doc) => ({ i: e.chunkIndex, c: e.chunk as MacSpeakerLiveSourceChunk }))
|
|
36
|
+
const projection = projectMacSpeakerLiveChunks({ captureId: state.captureId, generation: state.generation, chunksIndexed: indexed, projections: state.projections })
|
|
37
|
+
if (projection.withheld.length || projection.appliedRawChunkIndices.length !== state.projections.length) refuse('saved originals changed or became protected')
|
|
38
|
+
const current = new Map<number, Doc>(doc.chunkEntries.map((e: Doc) => [e.chunkIndex, e.chunk]))
|
|
39
|
+
const centroids = profiles.map(p => ({ name: p.name, embedding: Array.from({ length: 192 }, (_, i) => p.embeddings.reduce((sum, v) => sum + v[i] / p.embeddings.length, 0)) }))
|
|
40
|
+
const proofs = new Map<string, MacProfileMatchEvidence>()
|
|
41
|
+
for (const span of state.projections) if (span.profileName && !proofs.has(span.identityId)) {
|
|
42
|
+
const identity = state.identities.find(i => i.identityId === span.identityId)!
|
|
43
|
+
const group = state.evidence.filter(e => e.cohortId === identity.cohortId)
|
|
44
|
+
const target = centroids.find(p => p.name === span.profileName) ?? refuse('profile missing')
|
|
45
|
+
proofs.set(span.identityId, {
|
|
46
|
+
kind: 'profile-match', profileName: span.profileName, profileSha256: state.profileSHA256,
|
|
47
|
+
modelSha256: state.modelSHA256, algorithmVersion: 'mac-native-live-v1',
|
|
48
|
+
crossWindowCosines: group.map(a => group.map(b => normalizedSpeakerCosine(a.receipt.embedding, b.receipt.embedding)!)),
|
|
49
|
+
windows: group.map(e => ({ id: e.window.id, captureStartSample: e.window.startSample, captureEndSample: e.window.endSample,
|
|
50
|
+
pcmSha256: e.receipt.pcmSHA256, similarity: normalizedSpeakerCosine(e.receipt.embedding, target.embedding)!,
|
|
51
|
+
runnerUpSimilarity: Math.max(-1, ...centroids.filter(p => p !== target).map(p => normalizedSpeakerCosine(e.receipt.embedding, p.embedding)!)),
|
|
52
|
+
rawChunks: e.window.sources.map(s => ({ rawChunkIndex: s.chunkIndex, audioSha256: s.audioSHA256, sampleCount: s.sampleCount })) })),
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
const assignments: MacSpeakerRepairAssignment[] = state.projections.flatMap(span => {
|
|
56
|
+
const original = current.get(span.rawChunkIndex) ?? refuse('missing original')
|
|
57
|
+
if (original.speaker === span.displayLabel) return []
|
|
58
|
+
return [{ rawChunkIndex: span.rawChunkIndex, from: original.speaker, to: span.displayLabel,
|
|
59
|
+
evidenceId: span.identityId, batchWords: [], ...(span.profileName ? { targetEvidence: proofs.get(span.identityId)! } : {}) }]
|
|
60
|
+
})
|
|
61
|
+
if (!assignments.length) refuse('no qualified label changes')
|
|
62
|
+
const selected = new Map(assignments.map(a => [a.rawChunkIndex, a]))
|
|
63
|
+
const withheldBatchWords: Array<MacSpeakerWordBinding & { reason: 'outside-qualified-span' }> = []
|
|
64
|
+
doc.batchSegments.forEach((segment: Doc, segmentIndex: number) => {
|
|
65
|
+
if (!Number.isSafeInteger(segment.startChunkIdx) || !Number.isSafeInteger(segment.endChunkIdx)
|
|
66
|
+
|| segment.startChunkIdx < 0 || segment.endChunkIdx < segment.startChunkIdx
|
|
67
|
+
|| segment.endChunkIdx - segment.startChunkIdx >= MAX_RAW || !Array.isArray(segment.speakerWords)) refuse('invalid batch geometry')
|
|
68
|
+
const ranges: Array<{ raw: number; start: number; end: number }> = []; let at = 0
|
|
69
|
+
for (let raw = segment.startChunkIdx; raw <= segment.endChunkIdx; raw++) {
|
|
70
|
+
const n = counts[String(raw)]
|
|
71
|
+
if (!Number.isSafeInteger(n) || n <= 0 || n > 480000) refuse('unverified batch PCM clock')
|
|
72
|
+
ranges.push({ raw, start: at, end: at + n / 16000 }); at += n / 16000
|
|
73
|
+
}
|
|
74
|
+
segment.speakerWords.forEach((word: Doc, wordIndex: number) => {
|
|
75
|
+
if (!Number.isFinite(word.start) || !Number.isFinite(word.end) || word.start < 0 || word.end < word.start || word.end > at) refuse('invalid batch word clock')
|
|
76
|
+
const overlapping = ranges.filter(r => word.start === word.end ? word.start >= r.start && word.start < r.end : word.start < r.end && word.end > r.start)
|
|
77
|
+
if (!overlapping.some(r => selected.has(r.raw))) return
|
|
78
|
+
const binding = { segmentIndex, wordIndex, from: word.speaker, word: word.word, start: word.start, end: word.end }
|
|
79
|
+
if (overlapping.some(r => !selected.has(r.raw))) withheldBatchWords.push({ ...binding, reason: 'outside-qualified-span' })
|
|
80
|
+
else if (new Set(overlapping.map(r => selected.get(r.raw)!.to)).size !== 1) refuse('word crosses two identities')
|
|
81
|
+
else selected.get(overlapping[0].raw)!.batchWords.push(binding)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
const plan: MacSpeakerRepairPlan = { id: `live_${state.stateSHA256.slice(0, 24)}`, evidenceHash: state.stateSHA256,
|
|
85
|
+
expectedDocumentHash: hashMacSpeakerDocument(doc), protectedRawChunkIndices: [], rawAudioSampleCounts: counts, assignments, withheldBatchWords }
|
|
86
|
+
// Exercise all existing named-evidence and exact word/time protection gates
|
|
87
|
+
// here; callers cannot accidentally treat plan construction as qualification.
|
|
88
|
+
projectMacSpeakerRepair(doc, plan)
|
|
89
|
+
return plan
|
|
90
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Pure durable publication preparation. The caller writes this replacement and
|
|
2
|
+
* originals atomically before committing in-memory state or emitting an event. */
|
|
3
|
+
import { createMacSpeakerLiveState, MAC_SPEAKER_LIVE_TERMINAL_REVISION, hashMacSpeakerLive, validateMacSpeakerLiveState, withholdMacSpeakerLiveState, type MacSpeakerLiveState } from './mac-speaker-live-memory.js'
|
|
4
|
+
import { projectMacSpeakerLiveChunks, type MacSpeakerLiveSourceChunk } from './mac-speaker-live-projection.js'
|
|
5
|
+
const ZERO='0'.repeat(64)
|
|
6
|
+
export function macSpeakerLiveVoiceHighWater(chunks:readonly unknown[],minimum=0):number{
|
|
7
|
+
let maximum=minimum
|
|
8
|
+
const visit=(v:unknown):void=>{if(!v||typeof v!=='object')return
|
|
9
|
+
const row=v as Record<string,unknown>
|
|
10
|
+
for(const key of ['speaker','originalSpeaker'])if(typeof row[key]==='string'){
|
|
11
|
+
const match=/^Voice\s*([1-9]\d?)$/i.exec(row[key] as string);if(match)maximum=Math.max(maximum,Number(match[1]))
|
|
12
|
+
}
|
|
13
|
+
for(const key of ['segments','finalSegments','words'])if(Array.isArray(row[key]))for(const child of row[key] as unknown[])visit(child)
|
|
14
|
+
}
|
|
15
|
+
for(const c of chunks)visit(c)
|
|
16
|
+
return maximum
|
|
17
|
+
}
|
|
18
|
+
function reseal(state:MacSpeakerLiveState):MacSpeakerLiveState {const{stateSHA256,...body}=state;return{...body,stateSHA256:hashMacSpeakerLive(body)}}
|
|
19
|
+
export function prepareMacSpeakerLivePublication(input:{captureId:string;generation:string;chunksIndexed:readonly {i:number;c:MacSpeakerLiveSourceChunk & Record<string,unknown>}[];state?:MacSpeakerLiveState;validated:boolean;revision:number;previousDigest?:string}){
|
|
20
|
+
let state=input.state,invalid=false
|
|
21
|
+
const unsafeCounter=!Number.isSafeInteger(input.revision)||input.revision<0||input.revision>=MAC_SPEAKER_LIVE_TERMINAL_REVISION-1
|
|
22
|
+
const revision=unsafeCounter?MAC_SPEAKER_LIVE_TERMINAL_REVISION-1:input.revision
|
|
23
|
+
if(state){try{validateMacSpeakerLiveState(state);if(state.captureId!==input.captureId||state.generation!==input.generation)throw Error('capture_generation_drift')}
|
|
24
|
+
catch{invalid=true;state=withholdMacSpeakerLiveState(createMacSpeakerLiveState(input.captureId,input.generation,ZERO,ZERO,99),'recovered_state_invalid')}}
|
|
25
|
+
if(unsafeCounter||state&&state.revision>=MAC_SPEAKER_LIVE_TERMINAL_REVISION-1){
|
|
26
|
+
state=withholdMacSpeakerLiveState(createMacSpeakerLiveState(input.captureId,input.generation,ZERO,ZERO,99),'revision_counter_exhausted')
|
|
27
|
+
state.revision=MAC_SPEAKER_LIVE_TERMINAL_REVISION;state=reseal(state)
|
|
28
|
+
}
|
|
29
|
+
if(state){state=structuredClone(state);if(!input.validated||invalid)state.projections=[];state=reseal(state)}
|
|
30
|
+
let overlay:ReturnType<typeof projectMacSpeakerLiveChunks>
|
|
31
|
+
try{overlay=projectMacSpeakerLiveChunks({captureId:input.captureId,generation:input.generation,chunksIndexed:input.chunksIndexed,projections:state?.projections??[]})}
|
|
32
|
+
catch(error){
|
|
33
|
+
// Projection conflicts (including a newly arrived legacy Voice collision)
|
|
34
|
+
// revoke the complete overlay. Never leave the prior durable claim visible.
|
|
35
|
+
if(!state?.projections.length)throw error
|
|
36
|
+
state.projections=[];state=reseal(state)
|
|
37
|
+
overlay=projectMacSpeakerLiveChunks({captureId:input.captureId,generation:input.generation,chunksIndexed:input.chunksIndexed,projections:[]})
|
|
38
|
+
}
|
|
39
|
+
// A valid state must never advertise proof for rows the source projector refused.
|
|
40
|
+
if(state&&overlay.withheld.length&&state.projections.length){state.projections=[];state=reseal(state);overlay=projectMacSpeakerLiveChunks({captureId:input.captureId,generation:input.generation,chunksIndexed:input.chunksIndexed,projections:[]})}
|
|
41
|
+
const changed=input.previousDigest!==overlay.effectiveOverlaySHA256
|
|
42
|
+
// A prior claim already at the terminal cursor has no representable newer
|
|
43
|
+
// withdrawal. Refuse adoption; recovery preserves it outside active claims.
|
|
44
|
+
if(input.revision===MAC_SPEAKER_LIVE_TERMINAL_REVISION&&changed&&input.previousDigest!==undefined)throw Error('live_revision_exhausted')
|
|
45
|
+
if(!state&&input.previousDigest!==undefined&&changed&&revision>0){
|
|
46
|
+
invalid=true;state=withholdMacSpeakerLiveState(createMacSpeakerLiveState(input.captureId,input.generation,ZERO,ZERO,99),'recovered_state_missing')
|
|
47
|
+
}
|
|
48
|
+
const nextRevision=Math.max(state?.revision??0,revision+(changed||invalid?1:0))
|
|
49
|
+
if(nextRevision>MAC_SPEAKER_LIVE_TERMINAL_REVISION)throw Error('live_revision_exhausted')
|
|
50
|
+
if(state){state.revision=nextRevision;state=reseal(state);validateMacSpeakerLiveState(state)}
|
|
51
|
+
return{speakerIdentityPolicy:'mac-native-v1' as const,sourceType:'mac_system_audio' as const,speakerGeneration:input.generation,speakerRevision:nextRevision,
|
|
52
|
+
speakerProjectionSHA256:overlay.effectiveOverlaySHA256,speakerProjectedChunksIndexed:overlay.speakerProjectedChunksIndexed,
|
|
53
|
+
...(state?{macSpeakerLive:state}:{}),...(invalid?{macSpeakerLiveQuarantineSHA256:hashMacSpeakerLive(input.state??null)}:{}),invalid,
|
|
54
|
+
sourceBindingSHA256:overlay.sourceBindingSHA256}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Main-thread withdrawal path. Never validates, clones or hashes retained
|
|
58
|
+
* acoustic state. That state is private/untrusted until the worker certifies it. */
|
|
59
|
+
export function macSpeakerLiveOriginalOnly(input:{captureId:string;generation:string;chunksIndexed:readonly {i:number;c:Record<string,unknown>}[];state?:MacSpeakerLiveState;revision:number;previousDigest?:string}){
|
|
60
|
+
const digest=hashMacSpeakerLive({captureId:input.captureId,generation:input.generation,changes:[]}),changed=input.previousDigest!==digest
|
|
61
|
+
if(input.revision===MAC_SPEAKER_LIVE_TERMINAL_REVISION&&changed&&input.previousDigest!==undefined)throw Error('live_revision_exhausted')
|
|
62
|
+
const unsafe=!Number.isSafeInteger(input.revision)||input.revision<0||input.revision>=MAC_SPEAKER_LIVE_TERMINAL_REVISION-1
|
|
63
|
+
const missing=!input.state&&input.previousDigest!==undefined&&changed&&input.revision>0
|
|
64
|
+
let terminal:MacSpeakerLiveState|undefined
|
|
65
|
+
if(unsafe||missing){terminal=withholdMacSpeakerLiveState(createMacSpeakerLiveState(input.captureId,input.generation,ZERO,ZERO,99),unsafe?'revision_counter_exhausted':'recovered_state_missing')
|
|
66
|
+
terminal.revision=unsafe?MAC_SPEAKER_LIVE_TERMINAL_REVISION:input.revision+1;terminal=reseal(terminal)}
|
|
67
|
+
const revision=terminal?.revision??input.revision+(changed?1:0)
|
|
68
|
+
return{speakerIdentityPolicy:'mac-native-v1' as const,sourceType:'mac_system_audio' as const,speakerGeneration:input.generation,speakerRevision:revision,speakerProjectionSHA256:digest,
|
|
69
|
+
speakerProjectedChunksIndexed:input.chunksIndexed,...(terminal?{macSpeakerLive:terminal}:{}),...(!terminal&&input.state?{macSpeakerLiveRetainedState:input.state}:{}),invalid:false,
|
|
70
|
+
sourceBindingSHA256:hashMacSpeakerLive({captureId:input.captureId,generation:input.generation,originals:input.chunksIndexed})}
|
|
71
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Pure computation entry. No filesystem, native models, enrollment or writes. */
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { hasMacSpeakerProtectedRows } from './mac-speaker-protection.js'
|
|
4
|
+
import { isMainThread, parentPort } from 'node:worker_threads'
|
|
5
|
+
import { planMacSpeakerWindows, type VerifiedMacRawChunk, type PreparedMacSpeakerWindow, type MacWindowEmbeddingReceipt } from './mac-speaker-windows.js'
|
|
6
|
+
import { createMacSpeakerLiveState, appendMacSpeakerLiveEvidence, refreshMacSpeakerLiveState, validateMacSpeakerLiveState, validateMacSpeakerLiveProfiles, withholdMacSpeakerLiveState, hashMacSpeakerLive, type MacSpeakerLiveState } from './mac-speaker-live-memory.js'
|
|
7
|
+
import { prepareMacSpeakerLivePublication } from './mac-speaker-live-store.js'
|
|
8
|
+
import type { MacSpeakerProfile } from './mac-speaker-reconcile.js'
|
|
9
|
+
import type { SidecarEntry } from './nemotron-final.js'
|
|
10
|
+
import type { MacSpeakerLiveSourceChunk } from './mac-speaker-live-projection.js'
|
|
11
|
+
export interface MacSpeakerLiveComputeInput {
|
|
12
|
+
captureId:string;generation:string;sourceBindingSHA256:string;fileBindingsSHA256:string;revision:number;previousDigest?:string;voiceHighWater:number
|
|
13
|
+
state?:MacSpeakerLiveState;modelSHA256:string;profileSHA256:string;profileJSONBytes:Uint8Array
|
|
14
|
+
chunksIndexed:readonly {i:number;c:Record<string,unknown>}[];rawChunks:VerifiedMacRawChunk[];humanHistory:boolean
|
|
15
|
+
}
|
|
16
|
+
export interface MacSpeakerLiveComputePrepared {window?:PreparedMacSpeakerWindow;plannedWindows:number;pendingWindows:number;rejectedChunks:number;reason?:string}
|
|
17
|
+
export type MacSpeakerLivePreparedPublication=ReturnType<typeof prepareMacSpeakerLivePublication>
|
|
18
|
+
export interface MacSpeakerLiveComputeResult {
|
|
19
|
+
state:MacSpeakerLiveState;publication:MacSpeakerLivePreparedPublication;reason:string;plannedWindows:number;pendingWindows:number;rejectedChunks:number;completeEligibleSnapshot:boolean
|
|
20
|
+
}
|
|
21
|
+
export function prepareMacSpeakerLiveCompute(input:MacSpeakerLiveComputeInput){
|
|
22
|
+
if(input.profileJSONBytes.byteLength>32*1024*1024||input.chunksIndexed.length>8192||input.rawChunks.length>8192||hashMacSpeakerLive(input.chunksIndexed)!==input.sourceBindingSHA256)throw Error('compute_input_binding')
|
|
23
|
+
if(createHash('sha256').update(input.profileJSONBytes).digest('hex')!==input.profileSHA256)throw Error('compute_profile_bytes_changed')
|
|
24
|
+
const parsed=JSON.parse(Buffer.from(input.profileJSONBytes).toString()) as {profiles?:MacSpeakerProfile[]}
|
|
25
|
+
if(!Array.isArray(parsed.profiles))throw Error('profile_cohort_invalid')
|
|
26
|
+
const profiles=parsed.profiles.map(p=>({...p,profileSHA256:input.profileSHA256}));validateMacSpeakerLiveProfiles(profiles,input.profileSHA256)
|
|
27
|
+
let state=input.state
|
|
28
|
+
if(state){try{validateMacSpeakerLiveState(state);if(state.captureId!==input.captureId||state.generation!==input.generation)throw Error('capture_generation_drift')}catch{state=withholdMacSpeakerLiveState(createMacSpeakerLiveState(input.captureId,input.generation,input.modelSHA256,input.profileSHA256,99),'recovered_state_invalid')}}
|
|
29
|
+
state??=createMacSpeakerLiveState(input.captureId,input.generation,input.modelSHA256,input.profileSHA256,input.voiceHighWater)
|
|
30
|
+
if(state.modelSHA256!==input.modelSHA256||state.profileSHA256!==input.profileSHA256)state=withholdMacSpeakerLiveState(state,'model_or_profile_drift')
|
|
31
|
+
if(input.humanHistory||input.chunksIndexed.some(e=>hasMacSpeakerProtectedRows(e.c)))state=withholdMacSpeakerLiveState(state,'human_history')
|
|
32
|
+
const plan=state.withheldReason?null:planMacSpeakerWindows({sidecar:{sessionId:input.captureId,chunkEntries:input.chunksIndexed.map(e=>({chunkIndex:e.i,chunk:e.c}))as unknown as SidecarEntry[]},rawChunks:input.rawChunks,correctionHistory:'verified-clear'})
|
|
33
|
+
if(plan&&state.evidence.some(e=>!plan.windows.some(w=>w.id===e.window.id)))throw Error('retained_source_plan_drift')
|
|
34
|
+
const pending=plan?.windows.filter(w=>!state!.evidence.some(e=>e.window.id===w.id))??[]
|
|
35
|
+
const prepared:MacSpeakerLiveComputePrepared={...(pending[0]?{window:pending[0]}:{}),plannedWindows:plan?.windows.length??0,pendingWindows:pending.length,rejectedChunks:plan?.rejected.reduce((n,r)=>n+r.chunkIndexes.length,0)??0,...(state.withheldReason?{reason:state.withheldReason}:{})}
|
|
36
|
+
let finished=false
|
|
37
|
+
return{prepared,finish(receipt?:MacWindowEmbeddingReceipt):MacSpeakerLiveComputeResult{
|
|
38
|
+
if(finished)throw Error('compute_already_finished');finished=true
|
|
39
|
+
let remaining=pending.length
|
|
40
|
+
if(receipt){if(!pending[0]||receipt.windowId!==pending[0].id)throw Error('compute_unrequested_receipt')
|
|
41
|
+
state=appendMacSpeakerLiveEvidence(state!,{window:pending[0],receipt,nativeReceiptSHA256:hashMacSpeakerLive({window:pending[0],receipt})},profiles,input.voiceHighWater);remaining--
|
|
42
|
+
}else if(!pending.length)state=refreshMacSpeakerLiveState(state!,profiles)
|
|
43
|
+
const complete=!state!.withheldReason&&remaining===0
|
|
44
|
+
const publication=prepareMacSpeakerLivePublication({captureId:input.captureId,generation:input.generation,chunksIndexed:input.chunksIndexed as {i:number;c:MacSpeakerLiveSourceChunk&Record<string,unknown>}[],state,validated:complete,revision:input.revision,previousDigest:input.previousDigest})
|
|
45
|
+
state=publication.macSpeakerLive??state!
|
|
46
|
+
return{state,publication,reason:state.withheldReason??(remaining?'eligible_evidence_pending':state.projections.length?'qualified':'no_qualified_identity'),plannedWindows:prepared.plannedWindows,pendingWindows:remaining,rejectedChunks:prepared.rejectedChunks,completeEligibleSnapshot:complete}
|
|
47
|
+
}}
|
|
48
|
+
}
|
|
49
|
+
if(!isMainThread&&parentPort){
|
|
50
|
+
const port=parentPort;let job:string|undefined,computation:ReturnType<typeof prepareMacSpeakerLiveCompute>|undefined
|
|
51
|
+
port.on('message',(message:{id:number;job:string;op:string;input?:MacSpeakerLiveComputeInput;receipt?:MacWindowEmbeddingReceipt})=>{
|
|
52
|
+
try{
|
|
53
|
+
if(message.op==='prepare'){if(computation)throw Error('compute_duplicate_prepare');job=message.job;computation=prepareMacSpeakerLiveCompute(message.input!);port.postMessage({id:message.id,job,ok:true,value:computation.prepared})}
|
|
54
|
+
else if(message.op==='finish'){if(!computation||job!==message.job)throw Error('compute_job_binding');port.postMessage({id:message.id,job,ok:true,value:computation.finish(message.receipt)})}
|
|
55
|
+
else throw Error('compute_unknown_operation')
|
|
56
|
+
}catch(error){port.postMessage({id:message.id,job:message.job,ok:false,error:error instanceof Error?error.message:'compute_failed'})}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/** Async read-only live extraction adapter. The route owns durable publication;
|
|
2
|
+
* this module never writes recordings, profiles, corrections or transcripts. */
|
|
3
|
+
import { constants, lstatSync, type BigIntStats } from 'node:fs'
|
|
4
|
+
import { open } from 'node:fs/promises'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { createHash } from 'node:crypto'
|
|
7
|
+
import { dataPath } from './data-dir.js'
|
|
8
|
+
import { type VerifiedMacRawChunk, type MacWindowEmbeddingReceipt } from './mac-speaker-windows.js'
|
|
9
|
+
import { extractQualifiedMacWindow } from './mac-speaker-profile-extractor.js'
|
|
10
|
+
import { readPcm16Mono, pcm16ToWav } from './nemotron-segments.js'
|
|
11
|
+
import { startMacSpeakerLiveCompute, type MacSpeakerLivePublicationCapability } from './mac-speaker-live-compute.js'
|
|
12
|
+
import { hashMacSpeakerLive, type MacSpeakerLiveState } from './mac-speaker-live-memory.js'
|
|
13
|
+
const sha=(bytes:Buffer)=>createHash('sha256').update(bytes).digest('hex')
|
|
14
|
+
const yieldLoop=()=>new Promise<void>(resolve=>setImmediate(resolve))
|
|
15
|
+
async function shaYielding(bytes:Buffer):Promise<string>{const hash=createHash('sha256');for(let at=0;at<bytes.length;at+=1024*1024){hash.update(bytes.subarray(at,at+1024*1024));await yieldLoop()}return hash.digest('hex')}
|
|
16
|
+
export const macSpeakerLiveEnabled=(env:NodeJS.ProcessEnv=process.env):boolean=>env.COS_MAC_SPEAKER_LIVE==='1'
|
|
17
|
+
export interface MacSpeakerLiveInput {
|
|
18
|
+
captureId:string;generation:string;audioDir:string;ledgerPath:string
|
|
19
|
+
chunksIndexed:readonly {i:number;c:Record<string,unknown>}[]
|
|
20
|
+
state?:MacSpeakerLiveState;voiceHighWater:number;revision?:number;previousDigest?:string
|
|
21
|
+
}
|
|
22
|
+
export interface MacSpeakerLiveFileBinding {path:string;exists:boolean;stamp?:string}
|
|
23
|
+
export const macSpeakerLiveFileStamp=(s:BigIntStats)=>[s.dev,s.ino,s.mode,s.size,s.mtimeNs,s.ctimeNs].join(':')
|
|
24
|
+
/** No awaits between this cheap final CAS and the caller's atomic publication. */
|
|
25
|
+
export function assertMacSpeakerLivePublicationBindings(bindings:readonly MacSpeakerLiveFileBinding[]):void{
|
|
26
|
+
if(!Array.isArray(bindings)||!bindings.length||bindings.length>8200)throw Error('live_publication_bindings_missing')
|
|
27
|
+
for(const b of bindings){
|
|
28
|
+
if(!b||typeof b.path!=='string'||typeof b.exists!=='boolean')throw Error('live_publication_binding_invalid')
|
|
29
|
+
let stat:BigIntStats|undefined;try{stat=lstatSync(b.path,{bigint:true})}catch(error){if((error as NodeJS.ErrnoException).code!=='ENOENT')throw error}
|
|
30
|
+
if(b.exists?(!stat?.isFile()||macSpeakerLiveFileStamp(stat)!==b.stamp):stat!==undefined)throw Error('live_publication_source_changed')
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export interface MacSpeakerLiveOutcome {
|
|
34
|
+
state?:MacSpeakerLiveState;reason:string;latencyMs:number;plannedWindows:number;attemptedWindows:number;pendingWindows:number
|
|
35
|
+
/** Completeness of this snapshot's canonical eligible plan, never every second of speech. */
|
|
36
|
+
completeEligibleSnapshot:boolean;rejectedChunks:number;sourceBindingSHA256:string;fileBindings:MacSpeakerLiveFileBinding[];capability?:MacSpeakerLivePublicationCapability
|
|
37
|
+
}
|
|
38
|
+
export interface MacSpeakerLiveDeps {
|
|
39
|
+
runtime?:()=>Promise<{modelPath:string;profilesPath:string}|null>
|
|
40
|
+
extract?:(pcm:Buffer,modelPath:string,timeoutMs:number)=>Promise<readonly number[][]|null>
|
|
41
|
+
now?:()=>number
|
|
42
|
+
/** Rechecked immediately before child launch; offline tests supply explicit admission. */
|
|
43
|
+
admit?:(captureId:string)=>boolean
|
|
44
|
+
}
|
|
45
|
+
// Pins are an I/O optimization only: every use reopens O_NOFOLLOW and checks
|
|
46
|
+
// inode/ctime/mtime/size. Changed metadata always causes fresh bytes+hash. Final
|
|
47
|
+
// CAS checks each same pin again; no elapsed-time trust or caller-provided hash.
|
|
48
|
+
const pins=new Map<string,{stamp:string;hash:string;raw?:{pcmSHA256:string;sampleCount:number}}>()
|
|
49
|
+
const stampOf=macSpeakerLiveFileStamp
|
|
50
|
+
async function pinned(path:string,max:number,raw=false):Promise<{stamp:string;hash:string;raw?:{pcmSHA256:string;sampleCount:number}}>{
|
|
51
|
+
const f=await open(path,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK)
|
|
52
|
+
try{const before=await f.stat({bigint:true});if(!before.isFile()||before.size>BigInt(max))throw Error('unsafe_or_oversized_source')
|
|
53
|
+
const stamp=stampOf(before),prior=pins.get(path)
|
|
54
|
+
if(prior?.stamp===stamp&&(!raw||prior.raw))return prior
|
|
55
|
+
const storage=Buffer.alloc(Number(before.size)+1);let offset=0
|
|
56
|
+
while(offset<storage.length){const{bytesRead}=await f.read(storage,offset,storage.length-offset,null);if(!bytesRead)break;offset+=bytesRead}
|
|
57
|
+
const bytes=storage.subarray(0,offset),after=await f.stat({bigint:true})
|
|
58
|
+
if(bytes.length!==Number(before.size)||stampOf(after)!==stamp)throw Error('source_changed_during_read')
|
|
59
|
+
const pcm=raw?pcmOf(bytes):undefined
|
|
60
|
+
const value={stamp,hash:await shaYielding(bytes),...(pcm?{raw:{pcmSHA256:sha(pcm),sampleCount:pcm.length/2}}:{})}
|
|
61
|
+
if(pins.size>=10000)pins.delete(pins.keys().next().value!);pins.set(path,value);return value
|
|
62
|
+
}finally{await f.close()}
|
|
63
|
+
}
|
|
64
|
+
async function readBounded(path:string,max:number):Promise<Buffer>{
|
|
65
|
+
const file=await open(path,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK)
|
|
66
|
+
try{
|
|
67
|
+
const before=await file.stat({bigint:true});if(!before.isFile()||before.size>BigInt(max))throw Error('unsafe_or_oversized_source')
|
|
68
|
+
const buffer=Buffer.alloc(Number(before.size)+1);let offset=0
|
|
69
|
+
while(offset<buffer.length){const{bytesRead}=await file.read(buffer,offset,buffer.length-offset,null);if(!bytesRead)break;offset+=bytesRead}
|
|
70
|
+
const after=await file.stat({bigint:true})
|
|
71
|
+
if(offset!==Number(before.size)||stampOf(after)!==stampOf(before))throw Error('source_changed_during_read')
|
|
72
|
+
const bytes=buffer.subarray(0,offset),prior=pins.get(path)
|
|
73
|
+
if(pins.size>=10000)pins.delete(pins.keys().next().value!)
|
|
74
|
+
const hash=await shaYielding(bytes)
|
|
75
|
+
pins.set(path,{stamp:stampOf(before),hash,...(prior?.hash===hash&&prior.raw?{raw:prior.raw}:{})})
|
|
76
|
+
return bytes
|
|
77
|
+
}finally{await file.close()}
|
|
78
|
+
}
|
|
79
|
+
async function ledgerBytes(path:string):Promise<Buffer|null>{try{return await readBounded(path,1024*1024)}catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT')return null;throw error}}
|
|
80
|
+
function pcmOf(wav:Buffer):Buffer{
|
|
81
|
+
if(wav.length<44||wav.toString('ascii',0,4)!=='RIFF'||wav.readUInt32LE(4)!==wav.length-8||wav.toString('ascii',8,12)!=='WAVE'||wav.toString('ascii',12,16)!=='fmt '||wav.readUInt32LE(16)!==16||wav.readUInt16LE(20)!==1
|
|
82
|
+
||wav.readUInt16LE(22)!==1||wav.readUInt32LE(24)!==16000||wav.readUInt16LE(34)!==16||wav.toString('ascii',36,40)!=='data'||wav.readUInt32LE(40)!==wav.length-44||(wav.length-44)%2)throw Error('invalid_raw_audio')
|
|
83
|
+
const pcm=readPcm16Mono(wav);if(!pcm||pcm.sampleRate!==16000)throw Error('invalid_raw_audio');return pcm.pcm
|
|
84
|
+
}
|
|
85
|
+
async function defaultRuntime(){
|
|
86
|
+
// speakerModelState()/isEmbeddingAvailable() initialize the legacy native
|
|
87
|
+
// extractor synchronously. Candidate discovery here must never call either.
|
|
88
|
+
const{speakerModelCandidates}=await import('./speaker-embeddings.js')
|
|
89
|
+
for(const modelPath of speakerModelCandidates()){
|
|
90
|
+
let file:Awaited<ReturnType<typeof open>>|undefined
|
|
91
|
+
try{file=await open(modelPath,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);const info=await file.stat()
|
|
92
|
+
if(info.isFile()&&info.size>=1000000&&info.size<=128*1024*1024)return{modelPath,profilesPath:dataPath('voice-profiles.json')}
|
|
93
|
+
}catch{/* try the next configured local candidate */}finally{await file?.close()}
|
|
94
|
+
}
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
let busy=false
|
|
98
|
+
/** One new disjoint window per invocation; the scheduler drains backlog without
|
|
99
|
+
* blocking transcription. No sampled12-window plan and no first-N-success claim. */
|
|
100
|
+
export async function runMacSpeakerLivePass(input:MacSpeakerLiveInput,deps:MacSpeakerLiveDeps={}):Promise<MacSpeakerLiveOutcome>{
|
|
101
|
+
const now=deps.now??Date.now,start=now(),sourceBindingSHA256=hashMacSpeakerLive(input.chunksIndexed)
|
|
102
|
+
let compute:Awaited<ReturnType<typeof startMacSpeakerLiveCompute>>|undefined,capability:MacSpeakerLivePublicationCapability|undefined,fileBindings:MacSpeakerLiveFileBinding[]=[],state=input.state,plannedWindows=0,attemptedWindows=0,pendingWindows=0,rejectedChunks=0,owns=false
|
|
103
|
+
const result=(reason:string,completeEligibleSnapshot=false):MacSpeakerLiveOutcome=>({state,reason,latencyMs:now()-start,plannedWindows,attemptedWindows,pendingWindows,completeEligibleSnapshot,rejectedChunks,sourceBindingSHA256,fileBindings,capability})
|
|
104
|
+
try{
|
|
105
|
+
const admitted=()=> (deps.admit??(()=>macSpeakerLiveEnabled()))(input.captureId)
|
|
106
|
+
if(!admitted())return result('live_admission_closed')
|
|
107
|
+
if(input.chunksIndexed.length>8192)throw Error('raw_source_budget_exhausted')
|
|
108
|
+
const ledger=await ledgerBytes(input.ledgerPath)
|
|
109
|
+
const runtime=await(deps.runtime??defaultRuntime)();if(!runtime)throw Error('model_unavailable')
|
|
110
|
+
const profileBytes=await readBounded(runtime.profilesPath,32*1024*1024),profileSHA256=await shaYielding(profileBytes)
|
|
111
|
+
const initialProfileStamp=pins.get(runtime.profilesPath)!.stamp
|
|
112
|
+
const modelPin=await pinned(runtime.modelPath,128*1024*1024),modelSHA256=modelPin.hash
|
|
113
|
+
const initialLedgerStamp=ledger!==null?pins.get(input.ledgerPath)!.stamp:undefined
|
|
114
|
+
await yieldLoop()
|
|
115
|
+
const raw:VerifiedMacRawChunk[]=[],bindings=new Map<number,{hash:string;stamp:string}>()
|
|
116
|
+
for(const entry of input.chunksIndexed){
|
|
117
|
+
if(!Number.isSafeInteger(entry.i)||entry.i<0||entry.i>=10000)throw Error('invalid_raw_index')
|
|
118
|
+
// Unhashed legacy placeholders cannot become support; planner reports them.
|
|
119
|
+
if(typeof entry.c.audioSha256!=='string')continue
|
|
120
|
+
const binding=await pinned(join(input.audioDir,`chunk_${String(entry.i).padStart(4,'0')}.wav`),960044,true),audioSHA256=binding.hash
|
|
121
|
+
if(audioSHA256!==entry.c.audioSha256)throw Error('raw_audio_drift')
|
|
122
|
+
bindings.set(entry.i,{hash:audioSHA256,stamp:binding.stamp})
|
|
123
|
+
raw.push({chunkIndex:entry.i,audioSHA256,pcmSHA256:binding.raw!.pcmSHA256,sampleRate:16000,channels:1,format:'pcm_s16le',sampleCount:binding.raw!.sampleCount,verified:true})
|
|
124
|
+
await yieldLoop();if(now()-start>15000)throw Error('live_pass_deadline')
|
|
125
|
+
}
|
|
126
|
+
fileBindings=[{path:runtime.modelPath,exists:true,stamp:modelPin.stamp},{path:runtime.profilesPath,exists:true,stamp:initialProfileStamp},
|
|
127
|
+
...[...bindings].map(([index,binding])=>({path:join(input.audioDir,`chunk_${String(index).padStart(4,'0')}.wav`),exists:true,stamp:binding.stamp}))]
|
|
128
|
+
fileBindings.push({path:input.ledgerPath,exists:ledger!==null,...(ledger!==null?{stamp:initialLedgerStamp}:{})})
|
|
129
|
+
compute=await startMacSpeakerLiveCompute({captureId:input.captureId,generation:input.generation,sourceBindingSHA256,fileBindingsSHA256:hashMacSpeakerLive(fileBindings),revision:input.revision??0,previousDigest:input.previousDigest,
|
|
130
|
+
voiceHighWater:input.voiceHighWater,state:input.state,modelSHA256,profileSHA256,profileJSONBytes:new Uint8Array(profileBytes.buffer,profileBytes.byteOffset,profileBytes.byteLength),chunksIndexed:input.chunksIndexed,rawChunks:raw,humanHistory:!!ledger?.toString().trim()},
|
|
131
|
+
{timeoutMs:Math.max(1,Math.min(5000,15000-(now()-start)))})
|
|
132
|
+
const prepared=compute.prepared;plannedWindows=prepared.plannedWindows;pendingWindows=prepared.pendingWindows;rejectedChunks=prepared.rejectedChunks
|
|
133
|
+
let receipt:MacWindowEmbeddingReceipt|undefined
|
|
134
|
+
if(prepared.window){
|
|
135
|
+
if(busy)return result('native_lane_busy')
|
|
136
|
+
busy=true;owns=true;const window=prepared.window,buffers:Buffer[]=[]
|
|
137
|
+
for(const source of window.sources){const wav=await readBounded(join(input.audioDir,`chunk_${String(source.chunkIndex).padStart(4,'0')}.wav`),960044);if(sha(wav)!==source.audioSHA256)throw Error('raw_audio_drift');buffers.push(pcmOf(wav))}
|
|
138
|
+
const pcm=Buffer.concat(buffers);attemptedWindows++
|
|
139
|
+
if(!admitted())return result('live_admission_closed')
|
|
140
|
+
const vectors=await(deps.extract??extractQualifiedMacWindow)(pcm,runtime.modelPath,Math.max(1,Math.min(8000,15000-(now()-start))))
|
|
141
|
+
if(vectors)receipt={windowId:window.id,windowBindingSHA256:window.bindingSHA256,modelSHA256,audioSHA256:sha(pcm16ToWav(pcm,16000)),pcmSHA256:sha(pcm),sampleCount:pcm.length/2,embedding:vectors[0],halfEmbeddings:[vectors[1],vectors[2]]}
|
|
142
|
+
}
|
|
143
|
+
const finished=await compute.finish(receipt)
|
|
144
|
+
state=finished.state;capability=finished.capability;pendingWindows=finished.pendingWindows
|
|
145
|
+
// Final complete source/model/profile/history CAS, before route publication.
|
|
146
|
+
if(await shaYielding(await readBounded(runtime.profilesPath,32*1024*1024))!==profileSHA256||(await pinned(runtime.modelPath,128*1024*1024)).hash!==modelSHA256
|
|
147
|
+
||hashMacSpeakerLive(await ledgerBytes(input.ledgerPath))!==hashMacSpeakerLive(ledger))throw Error('model_profile_or_history_changed')
|
|
148
|
+
for(const[index,expected]of bindings){if((await pinned(join(input.audioDir,`chunk_${String(index).padStart(4,'0')}.wav`),960044,true)).hash!==expected.hash)throw Error('raw_audio_drift');await yieldLoop();if(now()-start>15000)throw Error('live_pass_deadline')}
|
|
149
|
+
assertMacSpeakerLivePublicationBindings(fileBindings)
|
|
150
|
+
if(!admitted())throw Error('live_admission_closed')
|
|
151
|
+
if(now()-start>15000)throw Error('live_pass_deadline')
|
|
152
|
+
return result(prepared.window&&!receipt?'extraction_unavailable':finished.reason,finished.completeEligibleSnapshot)
|
|
153
|
+
}catch(error){
|
|
154
|
+
const reason=error instanceof Error?error.message:'live_unavailable'
|
|
155
|
+
state=undefined;capability=undefined
|
|
156
|
+
return result(reason)
|
|
157
|
+
}finally{await compute?.close();if(owns)busy=false}
|
|
158
|
+
}
|
|
159
|
+
/** Coalescing async runner; no spin on unavailable models or failed extraction. */
|
|
160
|
+
export function createMacSpeakerLiveScheduler(deps:{snapshot:(captureId:string)=>MacSpeakerLiveInput|null;commit:(captureId:string,outcome:MacSpeakerLiveOutcome)=>boolean;runtime?:MacSpeakerLiveDeps;lease?:()=>{release():void}}){
|
|
161
|
+
const pending=new Set<string>();let running=false
|
|
162
|
+
const pump=async()=>{if(running)return;running=true;try{
|
|
163
|
+
while(pending.size){const id=pending.values().next().value!;pending.delete(id);const input=deps.snapshot(id);if(!input)continue
|
|
164
|
+
let lease:{release():void}|undefined
|
|
165
|
+
try{
|
|
166
|
+
lease=deps.lease?.()
|
|
167
|
+
const outcome=await runMacSpeakerLivePass(input,deps.runtime)
|
|
168
|
+
const applied=deps.commit(id,outcome)
|
|
169
|
+
if(applied&&outcome.state&&outcome.reason==='eligible_evidence_pending')pending.add(id)
|
|
170
|
+
}catch{/* Closed maintenance admission defers work until a new capture event. */}finally{lease?.release()}
|
|
171
|
+
await yieldLoop()
|
|
172
|
+
}
|
|
173
|
+
}finally{running=false}}
|
|
174
|
+
return{schedule(captureId:string){pending.add(captureId);setImmediate(()=>{void pump().catch(()=>{})})},async drain(){await pump();while(running)await new Promise<void>(r=>setTimeout(r,5))}}
|
|
175
|
+
}
|
|
@@ -17,25 +17,30 @@ process.stdin.on('end',()=>{
|
|
|
17
17
|
if(!extractor.isReady(stream))throw Error('not ready');return Array.from(extractor.compute(stream));};
|
|
18
18
|
const half=Math.floor(pcm.length/4)*2; process.stdout.write(JSON.stringify([embed(pcm),embed(pcm.subarray(0,half)),embed(pcm.subarray(half))]));
|
|
19
19
|
});`
|
|
20
|
+
let qualifiedExtractionBusy=false
|
|
20
21
|
export async function extractQualifiedMacWindow(pcm: Buffer, modelPath: string, timeoutMs: number, deps: { spawn?: typeof spawn; modulePath?: string } = {}): Promise<readonly number[][] | null> {
|
|
21
|
-
if (pcm.length < 896000 || pcm.length > 960000 || pcm.length % 2 || timeoutMs <= 0) return null
|
|
22
|
+
if (pcm.length < 896000 || pcm.length > 960000 || pcm.length % 2 || !Number.isFinite(timeoutMs) || timeoutMs <= 0) return null
|
|
22
23
|
let modulePath: string
|
|
23
24
|
try { modulePath = deps.modulePath ?? require.resolve('sherpa-onnx-node') } catch { return null }
|
|
24
|
-
return
|
|
25
|
-
|
|
25
|
+
if(qualifiedExtractionBusy)return null
|
|
26
|
+
qualifiedExtractionBusy=true
|
|
27
|
+
const deadline=Date.now()+timeoutMs
|
|
28
|
+
try{return await new Promise<readonly number[][]|null>(resolve => {
|
|
29
|
+
let child:ReturnType<typeof spawn>
|
|
30
|
+
try{child=(deps.spawn ?? spawn)(process.execPath, ['-e', worker, modulePath, modelPath], { stdio: ['pipe', 'pipe', 'pipe'] })}catch{resolve(null);return}
|
|
26
31
|
const output: Buffer[] = []; let size = 0, settled = false, aborted = false
|
|
27
32
|
const finish = (result: readonly number[][] | null) => { if (settled) return; settled = true; clearTimeout(timer); resolve(result) }
|
|
28
33
|
const abort = () => { aborted = true; child.kill('SIGKILL') }
|
|
29
34
|
// Keep ownership until close confirms native termination. Releasing on
|
|
30
35
|
// kill() alone permits a second native job while the first still exits.
|
|
31
|
-
const timer = setTimeout(abort,
|
|
32
|
-
child.stdout
|
|
36
|
+
const timer = setTimeout(abort, Math.max(0,deadline-Date.now()))
|
|
37
|
+
child.stdout!.on('data', b => { size += b.length; if (size > 131072 || Date.now()>=deadline) abort(); else if (!aborted) output.push(Buffer.from(b)) })
|
|
33
38
|
// Drain diagnostics without retaining native stderr or logging private input.
|
|
34
|
-
child.stderr
|
|
35
|
-
child.on('error', () => { aborted = true; if (!child.pid) finish(null); else child.kill('SIGKILL') }); child.stdin
|
|
39
|
+
child.stderr!.on('data', () => {})
|
|
40
|
+
child.on('error', () => { aborted = true; if (!child.pid) finish(null); else child.kill('SIGKILL') }); child.stdin!.on('error', () => {})
|
|
36
41
|
child.on('close', code => {
|
|
37
|
-
try { const rows = JSON.parse(Buffer.concat(output).toString()); finish(!aborted && code === 0 && Array.isArray(rows) && rows.length === 3 ? rows : null) } catch { finish(null) }
|
|
42
|
+
try { const rows = JSON.parse(Buffer.concat(output).toString()); finish(!aborted && Date.now()<deadline && code === 0 && Array.isArray(rows) && rows.length === 3 ? rows : null) } catch { finish(null) }
|
|
38
43
|
})
|
|
39
|
-
child.stdin
|
|
40
|
-
})
|
|
44
|
+
child.stdin!.end(pcm)
|
|
45
|
+
})}finally{qualifiedExtractionBusy=false}
|
|
41
46
|
}
|