@gotcos/glasses-server 6.92.0 → 6.94.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.
Files changed (63) hide show
  1. package/.env.example +30 -1
  2. package/CHANGELOG.md +72 -29
  3. package/README.md +70 -0
  4. package/docs/active-query-jobs.md +1 -1
  5. package/docs/test-plan-6.71.7.md +1 -1
  6. package/managed-runtime-contract.json +2 -0
  7. package/package.json +2 -1
  8. package/server/lib/ask-session-id.ts +2 -1
  9. package/server/lib/embedding-eviction.ts +60 -2
  10. package/server/lib/fuzzy-correct.ts +1 -1
  11. package/server/lib/g2-enrichment-runner.ts +7 -0
  12. package/server/lib/g2-ops-handoff.ts +204 -2
  13. package/server/lib/hallucination-filter.ts +1 -1
  14. package/server/lib/imported-library-rows.ts +4 -2
  15. package/server/lib/live-cos-ask.ts +3 -2
  16. package/server/lib/mac-live-voices.ts +57 -0
  17. package/server/lib/mac-recording-asr.ts +16 -8
  18. package/server/lib/mac-speaker-projection.ts +211 -0
  19. package/server/lib/mac-stream-diarizer.ts +185 -0
  20. package/server/lib/mac-voice-assignment.ts +361 -0
  21. package/server/lib/mac-voice-labels.ts +70 -8
  22. package/server/lib/mac-window-replay.ts +271 -0
  23. package/server/lib/mac-window-voices.ts +390 -0
  24. package/server/lib/meeting-actions.ts +1 -1
  25. package/server/lib/meeting-ask-owner.ts +4 -4
  26. package/server/lib/meeting-ask-transcript-sources.ts +41 -3
  27. package/server/lib/meeting-audio-archive.ts +4 -0
  28. package/server/lib/meeting-batch-transcribe.ts +7 -1
  29. package/server/lib/meeting-context-keys.ts +6 -3
  30. package/server/lib/meeting-corrections.ts +4 -4
  31. package/server/lib/meeting-fact-check.ts +3 -3
  32. package/server/lib/meeting-parse.ts +1 -1
  33. package/server/lib/meeting-relabel-enrolment.ts +8 -8
  34. package/server/lib/meeting-relabel.ts +3 -3
  35. package/server/lib/meeting-scribe-content.ts +1 -1
  36. package/server/lib/meeting-speaker-review.ts +8 -8
  37. package/server/lib/meeting-store.ts +19 -9
  38. package/server/lib/nemotron-final.ts +114 -9
  39. package/server/lib/nemotron-live.ts +58 -6
  40. package/server/lib/phone-recording.ts +114 -0
  41. package/server/lib/prompt-tail-guard.ts +1 -1
  42. package/server/lib/speaker-embeddings.ts +154 -28
  43. package/server/lib/speaker-name.ts +1 -1
  44. package/server/lib/stranded-session-actions.ts +7 -2
  45. package/server/lib/training-audio-provenance.ts +3 -2
  46. package/server/lib/transcript-cleanup-context.ts +3 -3
  47. package/server/lib/transcript-cleanup-model.ts +1 -1
  48. package/server/lib/transcript-cleanup-spans.ts +10 -10
  49. package/server/lib/transcript-cleanup-text.ts +4 -4
  50. package/server/lib/transcript-cleanup.ts +1 -1
  51. package/server/lib/unsaved-audio-quarantine.ts +10 -1
  52. package/server/lib/voice-directory.ts +1 -1
  53. package/server/lib/voice-evidence-guard.ts +2 -1
  54. package/server/lib/voice-profile-store.ts +11 -5
  55. package/server/lib/whisper-local.ts +1 -1
  56. package/server/routes/__fixtures__/g2-filing-golden-6.93.0.json +62 -0
  57. package/server/routes/health.ts +8 -0
  58. package/server/routes/meeting.ts +231 -20
  59. package/server/routes/meetings.ts +9 -5
  60. package/server/routes/transcribe-stream.ts +140 -15
  61. package/server/routes/voice.ts +12 -4
  62. package/server/scripts/mac-voice-assignment-6930-mutation-gate.mjs +225 -0
  63. package/server/scripts/mac-window-voices-6930-mutation-gate.mjs +193 -0
@@ -0,0 +1,271 @@
1
+ // 6.93.0: the I/O half of live Mac window replay (mac-window-voices.ts is the pure half).
2
+ //
3
+ // Reads the durable raw chunks the route already wrote, joins the planned window
4
+ // into one private temp WAV, runs the injected diarizer once with a budget sized
5
+ // from the window's audio length, removes the temp WAV, and crosswalks. When a
6
+ // carried slot needs the current chunk's agreement (QA W1) the chunk is diarized
7
+ // alone as well, after the window run. A window run that times out or whose
8
+ // geometry is refused falls back to the plain per-chunk run (QA W4). Any failure
9
+ // is the normal live fallback (`Ext`); nothing here throws into the chunk route.
10
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
11
+ import { tmpdir } from 'node:os'
12
+ import { join, resolve } from 'node:path'
13
+ import { MAC_AUDIO } from './mac-recording.js'
14
+ import { NEMOTRON_TMP_PREFIX, trackNemotronDir, untrackNemotronDir } from './nemotron-cli.js'
15
+ import { pcm16ToWav, readPcm16Mono, type Activity } from './nemotron-segments.js'
16
+ import { liveBudgetForAudioSec, type LiveDiarizeOutcome } from './nemotron-live.js'
17
+ import { chunkShowsVoiceLabel, macLiveVoicesMode, type MacLiveVoicesMode } from './mac-voice-labels.js'
18
+ import {
19
+ crosswalkMacWindow,
20
+ emptyMacWindowVoices,
21
+ planMacWindow,
22
+ validMacWindowVoices,
23
+ type MacWindowPlan,
24
+ type MacWindowResult,
25
+ type MacWindowStats,
26
+ type MacWindowVoiceState,
27
+ } from './mac-window-voices.js'
28
+
29
+ interface LabeledChunk { speaker?: unknown; segments?: unknown }
30
+
31
+ /** Why a capture's window labels are withheld, or null when they may run. */
32
+ export type MacWindowWithheld = 'corrupt_state' | 'earlier_voice_labels'
33
+
34
+ export function macWindowWithheldReason(saved: unknown, chunks: ReadonlyArray<LabeledChunk | null | undefined>): MacWindowWithheld | null {
35
+ if (saved !== undefined) return validMacWindowVoices(saved) ? null : 'corrupt_state'
36
+ return chunks.some(chunkShowsVoiceLabel) ? 'earlier_voice_labels' : null
37
+ }
38
+
39
+ /**
40
+ * The state this capture may continue from, or null to withhold window labels.
41
+ * Corrupt state stays as it is (never replaced by a fresh start). A capture that
42
+ * already shows Voice labels without window state (switch flipped mid-capture, or
43
+ * an older server) never restarts numbering at Voice 1.
44
+ */
45
+ export function macWindowStateFor(saved: unknown, chunks: ReadonlyArray<LabeledChunk | null | undefined>): MacWindowVoiceState | null {
46
+ if (macWindowWithheldReason(saved, chunks)) return null
47
+ return saved === undefined ? emptyMacWindowVoices() : saved as MacWindowVoiceState
48
+ }
49
+
50
+ // ── Counts (never text): one log line per window run, health totals since start ──
51
+
52
+ const counters = {
53
+ runs: 0,
54
+ fallbackTimeout: 0,
55
+ fallbackGeometry: 0,
56
+ agreementUnavailable: 0,
57
+ carried: 0,
58
+ withheld: 0,
59
+ created: 0,
60
+ confirmed: 0,
61
+ evicted: 0,
62
+ capped: 0,
63
+ disabledCaptures: 0,
64
+ stateWithheld: { corrupt_state: 0, earlier_voice_labels: 0 } as Record<MacWindowWithheld, number>,
65
+ }
66
+
67
+ /**
68
+ * QA R3 and W11: the window is switched off for a capture when MAC_WINDOW_BACKOFF_TIMEOUTS of its last
69
+ * MAC_WINDOW_BACKOFF_OF_LAST window runs timed out. After that the capture uses per-chunk runs only (`Ext`) until
70
+ * the server restarts, because a window timeout is exempt from the shared breaker and would otherwise cost its full
71
+ * wait on every chunk, for ever. A rolling count, not a streak: a loaded Mac that alternates a timeout with a
72
+ * run that just fits still pays most of a 6 s cadence on every other chunk, and a streak never trips on it.
73
+ */
74
+ export const MAC_WINDOW_BACKOFF_TIMEOUTS = 2
75
+ export const MAC_WINDOW_BACKOFF_OF_LAST = 3
76
+ const recentWindowRuns = new Map<string, boolean[]>()
77
+ const windowDisabled = new Set<string>()
78
+ const CAPTURE_MEMORY_MAX = 256
79
+
80
+ function remember<T>(set: Set<T>, value: T): void {
81
+ if (set.size >= CAPTURE_MEMORY_MAX) set.delete(set.values().next().value as T)
82
+ set.add(value)
83
+ }
84
+
85
+ /** True once this capture's window has timed out too often. */
86
+ export function macWindowDisabledFor(sessionId: string): boolean {
87
+ return windowDisabled.has(sessionId)
88
+ }
89
+
90
+ function noteWindowRun(sessionId: string, timedOut: boolean, log: (line: string) => void): void {
91
+ if (!recentWindowRuns.has(sessionId) && recentWindowRuns.size >= CAPTURE_MEMORY_MAX) recentWindowRuns.delete(recentWindowRuns.keys().next().value as string)
92
+ const recent = [...(recentWindowRuns.get(sessionId) ?? []), timedOut].slice(-MAC_WINDOW_BACKOFF_OF_LAST)
93
+ recentWindowRuns.set(sessionId, recent)
94
+ const timeouts = recent.filter(Boolean).length
95
+ if (timeouts >= MAC_WINDOW_BACKOFF_TIMEOUTS && !windowDisabled.has(sessionId)) {
96
+ remember(windowDisabled, sessionId)
97
+ counters.disabledCaptures++
98
+ log(`[mac-window] capture ${sessionId} window disabled after ${timeouts} of the last ${recent.length} window runs timed out; per-chunk runs only for the rest of this capture (Ext)`)
99
+ }
100
+ }
101
+
102
+ export function __resetMacWindowCountersForTests(): void {
103
+ for (const key of Object.keys(counters) as Array<keyof typeof counters>) {
104
+ if (key === 'stateWithheld') counters.stateWithheld = { corrupt_state: 0, earlier_voice_labels: 0 }
105
+ else (counters[key] as number) = 0
106
+ }
107
+ seenWithheld.clear()
108
+ recentWindowRuns.clear()
109
+ windowDisabled.clear()
110
+ }
111
+
112
+ const seenWithheld = new Set<string>()
113
+ const SEEN_WITHHELD_MAX = 256
114
+
115
+ /** A capture whose window labels are withheld says so once per reason, so `Ext` for a whole meeting is never silent. */
116
+ export function noteMacWindowWithheld(sessionId: string, reason: MacWindowWithheld, log: (line: string) => void = console.log): void {
117
+ const key = `${sessionId}:${reason}`
118
+ if (seenWithheld.has(key)) return
119
+ if (seenWithheld.size >= SEEN_WITHHELD_MAX) seenWithheld.delete(seenWithheld.values().next().value as string)
120
+ seenWithheld.add(key)
121
+ counters.stateWithheld[reason]++
122
+ log(`[mac-window] capture ${sessionId} window labels withheld (${reason}); its chunks stay Ext for the rest of this capture`)
123
+ }
124
+
125
+ export interface MacLiveVoicesSnapshot {
126
+ /** Which live Voice path is on: window (6.93.0, the default), anchors (6.92.1) or off (COS_MAC_LIVE_VOICES=off). */
127
+ mode: MacLiveVoicesMode
128
+ /** Totals since this server started; present only while the window path is on. */
129
+ window?: {
130
+ runs: number
131
+ fallbacks: { timeout: number; geometry: number }
132
+ agreementUnavailable: number
133
+ carried: number
134
+ withheld: number
135
+ created: number
136
+ confirmed: number
137
+ evicted: number
138
+ capped: number
139
+ /** Captures that stopped using the window after repeated window timeouts (QA R3). */
140
+ disabledCaptures: number
141
+ stateWithheld: Record<MacWindowWithheld, number>
142
+ }
143
+ }
144
+
145
+ /** For `/api/health` capabilities: the switch state, and the window path's counts when it is on. */
146
+ export function macLiveVoicesSnapshot(env: NodeJS.ProcessEnv = process.env): MacLiveVoicesSnapshot {
147
+ const mode = macLiveVoicesMode(env)
148
+ if (mode !== 'window') return { mode }
149
+ return {
150
+ mode,
151
+ window: {
152
+ runs: counters.runs,
153
+ fallbacks: { timeout: counters.fallbackTimeout, geometry: counters.fallbackGeometry },
154
+ agreementUnavailable: counters.agreementUnavailable,
155
+ carried: counters.carried,
156
+ withheld: counters.withheld,
157
+ created: counters.created,
158
+ confirmed: counters.confirmed,
159
+ evicted: counters.evicted,
160
+ capped: counters.capped,
161
+ disabledCaptures: counters.disabledCaptures,
162
+ stateWithheld: { ...counters.stateWithheld },
163
+ },
164
+ }
165
+ }
166
+
167
+ export interface MacWindowJob {
168
+ diarize: Promise<LiveDiarizeOutcome>
169
+ /** Set once `diarize` settles with a trusted crosswalk; null otherwise. */
170
+ result: () => MacWindowResult | null
171
+ }
172
+
173
+ const chunkName = (index: number): string => `chunk_${String(index).padStart(4, '0')}.wav`
174
+
175
+ /**
176
+ * Start chunk `chunkIndex`'s window run. `audio` is the chunk's own WAV (already
177
+ * durable at `sessionDir`). Returns null when no window can be planned; the caller
178
+ * then runs the plain per-chunk diarizer.
179
+ * - `diarizeWindow` runs the CLI on the window WAV within `timeoutMs`.
180
+ * - `diarizeChunk` runs it on one durable chunk WAV within the per-chunk budget:
181
+ * the agreement view, and the fallback after a window timeout or refusal.
182
+ */
183
+ export function startMacWindowDiarize(input: {
184
+ sessionId: string
185
+ sessionDir: string
186
+ chunkIndex: number
187
+ audio: Buffer
188
+ audioSha256: string
189
+ state: MacWindowVoiceState
190
+ diarizeWindow: (wavPath: string, timeoutMs: number) => Promise<LiveDiarizeOutcome>
191
+ diarizeChunk: (wavPath: string) => Promise<LiveDiarizeOutcome>
192
+ tmpRoot?: string
193
+ log?: (line: string) => void
194
+ }): MacWindowJob | null {
195
+ const log = input.log ?? console.log
196
+ const pcmOf = new Map<number, Buffer>()
197
+ const own = readPcm16Mono(input.audio)
198
+ if (!own) return null
199
+ pcmOf.set(input.chunkIndex, own.pcm)
200
+ const plan = planMacWindow(input.chunkIndex, chunk => {
201
+ if (pcmOf.has(chunk)) return pcmOf.get(chunk)!.length / 2
202
+ try {
203
+ const pcm = readPcm16Mono(readFileSync(resolve(input.sessionDir, chunkName(chunk))))
204
+ if (!pcm) return null
205
+ pcmOf.set(chunk, pcm.pcm)
206
+ return pcm.pcm.length / 2
207
+ } catch { return null }
208
+ })
209
+ if (!plan) return null
210
+ const windowSamples = plan.chunks.reduce((sum, c) => sum + c.samples, 0)
211
+ const windowSec = windowSamples / MAC_AUDIO.sampleRate
212
+ let work: string
213
+ let wavPath: string
214
+ try {
215
+ work = mkdtempSync(join(input.tmpRoot ?? tmpdir(), `${NEMOTRON_TMP_PREFIX}window-`))
216
+ trackNemotronDir(work)
217
+ wavPath = join(work, 'window.wav')
218
+ writeFileSync(wavPath, pcm16ToWav(Buffer.concat(plan.chunks.map(c => pcmOf.get(c.chunk)!))), { mode: 0o600 })
219
+ } catch {
220
+ return null
221
+ }
222
+ let cleaned = false
223
+ const cleanup = () => {
224
+ if (cleaned) return
225
+ cleaned = true
226
+ untrackNemotronDir(work)
227
+ try { rmSync(work, { recursive: true, force: true }) } catch { /* best effort */ }
228
+ }
229
+ const chunkPath = resolve(input.sessionDir, chunkName(input.chunkIndex))
230
+ let result: MacWindowResult | null = null
231
+ const cross = (window: Activity, chunkAlone: Activity | null, planned: MacWindowPlan): MacWindowResult | null => {
232
+ try { return crosswalkMacWindow({ state: input.state, window, plan: planned, audioSha256: input.audioSha256, chunkAlone }) } catch { return null }
233
+ }
234
+ const timeoutMs = liveBudgetForAudioSec(windowSec)
235
+
236
+ const fallback = async (why: 'timeout' | 'geometry', ms: number | undefined): Promise<LiveDiarizeOutcome> => {
237
+ cleanup()
238
+ counters[why === 'timeout' ? 'fallbackTimeout' : 'fallbackGeometry']++
239
+ log(`[mac-window] chunk #${input.chunkIndex} window ${windowSec.toFixed(1)}s fallback=${why}${ms !== undefined ? ` after ${ms} ms` : ''} (budget ${timeoutMs} ms) -> per-chunk run, no window state committed`)
240
+ return input.diarizeChunk(chunkPath)
241
+ }
242
+
243
+ const go = async (): Promise<LiveDiarizeOutcome> => {
244
+ let outcome: LiveDiarizeOutcome
245
+ try { outcome = await input.diarizeWindow(wavPath, timeoutMs) } finally { cleanup() }
246
+ noteWindowRun(input.sessionId, !outcome.ok && outcome.reason === 'timeout', log)
247
+ if (!outcome.ok) return outcome.reason === 'timeout' ? fallback('timeout', outcome.ms) : outcome
248
+ let crossed = cross(outcome.run, null, plan)
249
+ if (!crossed) return fallback('geometry', undefined)
250
+ let alone = 'none'
251
+ if (crossed.stats.carried > 0) {
252
+ // A carried slot is used only if this chunk, diarized alone, agrees with it.
253
+ const solo = existsSync(chunkPath) ? await input.diarizeChunk(chunkPath) : { ok: false as const, reason: 'error' as const }
254
+ alone = solo.ok ? 'ok' : solo.reason
255
+ if (!solo.ok) counters.agreementUnavailable++
256
+ crossed = cross(outcome.run, solo.ok ? solo.run : null, plan)
257
+ if (!crossed) return fallback('geometry', undefined)
258
+ }
259
+ result = crossed
260
+ const s: MacWindowStats = crossed.stats
261
+ counters.runs++
262
+ for (const key of ['carried', 'withheld', 'created', 'confirmed', 'evicted', 'capped'] as const) counters[key] += s[key]
263
+ log(`[mac-window] chunk #${input.chunkIndex} window ${plan.chunks.length} chunks ${windowSec.toFixed(1)}s ${Math.round(outcome.run.wallMs ?? 0)} ms overlap=${s.overlapChunks} carried=${s.carried} withheld=${s.withheld} created=${s.created} confirmed=${s.confirmed} evicted=${s.evicted} capped=${s.capped} tracks=${crossed.state.tracks.length} voices=${crossed.state.voices} alone=${alone}`)
264
+ return { ok: true, run: { ...outcome.run, frames: crossed.chunkActivity.frames, probs: crossed.chunkActivity.probs } }
265
+ }
266
+ const diarize = go().catch((error): LiveDiarizeOutcome => {
267
+ cleanup()
268
+ return { ok: false, reason: 'error', detail: error instanceof Error ? error.message : String(error) }
269
+ })
270
+ return { diarize, result: () => result }
271
+ }
@@ -0,0 +1,390 @@
1
+ // 6.93.0: live Mac voice continuity by window replay.
2
+ //
3
+ // Why: the live path ran Nemotron on each 6 s chunk alone, so the model's speaker
4
+ // cache never crossed a chunk boundary and every chunk started over. On the
5
+ // natural 36 s fixture the per-chunk anchors returned no labels at all, while one
6
+ // continuous pass kept two clean voices through all five boundaries.
7
+ //
8
+ // What: for chunk k the route runs the SAME pinned CLI on the last few durable raw
9
+ // chunks (at most MAC_WINDOW_MAX_CHUNKS, at most MAC_WINDOW_MAX_SAMPLES). Inside
10
+ // one window the model's slots are consistent. A slot is carried to a capture
11
+ // track only through frames this capture already attributed (the overlap): one to
12
+ // one, at least MAC_WINDOW_CARRY_MIN_FRAMES of exclusive overlap, at least
13
+ // MAC_WINDOW_CARRY_AGREEMENT of those frames on one track. Anything weaker leaves
14
+ // the slot without a track, so its words stay `Ext`.
15
+ //
16
+ // A carried slot is used only if the current chunk AGREES (QA W1): the same chunk
17
+ // diarized alone (the per-chunk run, an independent view) must put the slot's
18
+ // exclusive frames on one channel (MAC_WINDOW_AGREE_PURITY of the frames it can
19
+ // place, MAC_WINDOW_AGREE_COVERAGE of them placed). A window model that merged two
20
+ // voices which BOTH speak in this chunk is therefore not carried: the slot gets no
21
+ // label and no proof this chunk, and its frames are stored as untracked so the error
22
+ // does not feed the next window. NOT caught (QA R1): a clean hand-over at the chunk
23
+ // boundary, a new voice alone in this chunk that the window model put on an old
24
+ // voice's slot. The chunk alone shows one clean voice and agrees. Catching it needs
25
+ // evidence from more than one chunk. It is a known limit now that the window is on by
26
+ // default: a wrong NUMBER that the saved pass keeps (it never renumbers live labels). It reaches a profile only through an explicit assignment, which is gated (mac-voice-assignment.ts).
27
+ //
28
+ // A track is shown as `Voice N` only after clean proof in TWO distinct original
29
+ // chunks with distinct audio (each: MAC_WINDOW_PROOF_FRAMES exclusive, purity at
30
+ // least MAC_WINDOW_PROOF_PURITY). Numbers only grow; a corrupt state never
31
+ // restarts at Voice 1. A track no recent window frame refers to is evicted (QA W3),
32
+ // so a long meeting cannot spend the MAC_WINDOW_MAX_TRACKS ids; its number stays
33
+ // spent. A voice silent for longer than the window cannot be crosswalked: it reads
34
+ // `Ext` or, after its own two proofs, a NEW number (the saved meeting's final pass
35
+ // numbers what live did not prove). The carry rules do not hand it another voice's number
36
+ // (the clean hand-over above is the exception).
37
+ //
38
+ // A `Voice N` is capture-local acoustic continuity, never a person: similarity 0,
39
+ // never a profile, never enrolled or trained. No audio, embedding or probability
40
+ // matrix is stored: only per-frame track ids for the last few chunks.
41
+ //
42
+ // Switch: COS_MAC_LIVE_VOICES, window by default (see mac-voice-labels.ts). It also
43
+ // needs COS_MAC_VOICE_LABELS not off. `anchors` runs the 6.92.1 per-chunk anchors
44
+ // instead; `off` (also 0, false, no) runs neither, so every live Mac chunk is `Ext`.
45
+ import { MAC_AUDIO } from './mac-recording.js'
46
+ import { NEMOTRON_CHANNELS, NEMOTRON_FRAME_SEC } from './nemotron-cli.js'
47
+ import { ACTIVE_THRESHOLD, type Activity } from './nemotron-segments.js'
48
+ import { macLiveVoicesMode, voiceLabel } from './mac-voice-labels.js'
49
+
50
+ export const MAC_WINDOW_MAX_CHUNKS = 5
51
+ /** One Mac chunk's most audio (30 s at 16 kHz), derived from the capture contract. Bounds the CLI's work per chunk (measured about 0.26 s warm). */
52
+ export const MAC_WINDOW_MAX_SAMPLES = MAC_AUDIO.sampleRate * (MAC_AUDIO.maxChunkMs / 1000)
53
+ /** The CLI's frame hop: NEMOTRON_FRAME_SEC (10 ms) of 16 kHz audio = 160 samples. */
54
+ export const MAC_WINDOW_SAMPLES_PER_FRAME = Math.round(MAC_AUDIO.sampleRate * NEMOTRON_FRAME_SEC)
55
+ /**
56
+ * The CLI's mel front end frames a clip of N samples as 1 + floor((N + FFT - WINDOW) / hop):
57
+ * n_fft 512 minus window 400 = 112 (measured: 36 s -> 3601 frames; a mismatch refuses the window).
58
+ */
59
+ export const MAC_WINDOW_FFT_SAMPLES = 512
60
+ export const MAC_WINDOW_FRAME_SAMPLES = 400
61
+ export const MAC_WINDOW_FRAME_TAIL_SAMPLES = MAC_WINDOW_FFT_SAMPLES - MAC_WINDOW_FRAME_SAMPLES
62
+ /** Frames the CLI returns for `samples` of audio. The one place the geometry is written down. */
63
+ export function macWindowFrameCount(samples: number): number {
64
+ return 1 + Math.floor((samples + MAC_WINDOW_FRAME_TAIL_SAMPLES) / MAC_WINDOW_SAMPLES_PER_FRAME)
65
+ }
66
+ export const MAC_WINDOW_CARRY_MIN_FRAMES = 100
67
+ export const MAC_WINDOW_CARRY_AGREEMENT = 0.8
68
+ export const MAC_WINDOW_NEW_TRACK_FRAMES = 200
69
+ export const MAC_WINDOW_PROOF_FRAMES = 200
70
+ export const MAC_WINDOW_PROOF_PURITY = 0.9
71
+ export const MAC_WINDOW_PROOFS = 2
72
+ /** Carried-slot agreement with the chunk diarized alone (QA W1): share of the slot's exclusive frames the per-chunk run must place, and the share of those on one channel. */
73
+ export const MAC_WINDOW_AGREE_COVERAGE = 0.5
74
+ export const MAC_WINDOW_AGREE_PURITY = 0.9
75
+ /** Live capture tracks (ids 0..11). A track no recent frame refers to is evicted and its id reused; past this at once, new slots stay `Ext`. */
76
+ export const MAC_WINDOW_MAX_TRACKS = 12
77
+ const MAX_VOICE = 99
78
+ // Per-frame codes stored for recent chunks. Track ids are 0..MAX_TRACKS-1.
79
+ const NONE = -1
80
+ const OVERLAP = -2
81
+ const UNTRACKED = -3
82
+
83
+ export function macWindowVoicesEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
84
+ return macLiveVoicesMode(env) === 'window'
85
+ }
86
+
87
+ export interface MacWindowTrack {
88
+ id: number
89
+ /** The confirmed Voice number, or null while unproven. */
90
+ voice: number | null
91
+ proofs: Array<{ chunk: number; audioSha256: string }>
92
+ }
93
+
94
+ export interface MacWindowVoiceState {
95
+ version: 1
96
+ kind: 'window'
97
+ /** Every track id is below this (at most MAC_WINDOW_MAX_TRACKS). Ids of evicted tracks are reused, only once no recent frame refers to them. */
98
+ nextTrack: number
99
+ /** Highest Voice number handed out. Never lowered. */
100
+ voices: number
101
+ tracks: MacWindowTrack[]
102
+ /** The newest chunks' per-frame codes (base64 Int8), oldest first, at most MAC_WINDOW_MAX_CHUNKS - 1. */
103
+ recent: Array<{ chunk: number; frames: string }>
104
+ }
105
+
106
+ export function emptyMacWindowVoices(): MacWindowVoiceState {
107
+ return { version: 1, kind: 'window', nextTrack: 0, voices: 0, tracks: [], recent: [] }
108
+ }
109
+
110
+ const isIndex = (n: unknown): n is number => typeof n === 'number' && Number.isInteger(n) && n >= 0
111
+ const SHA = /^[0-9a-f]{64}$/
112
+ const BASE64 = /^[A-Za-z0-9+/]*={0,2}$/
113
+
114
+ function decodeFrames(frames: string): Int8Array | null {
115
+ if (typeof frames !== 'string' || frames.length > 64_000 || !BASE64.test(frames) || frames.length % 4) return null
116
+ const bytes = Buffer.from(frames, 'base64')
117
+ if (bytes.toString('base64') !== frames) return null
118
+ return new Int8Array(bytes.buffer, bytes.byteOffset, bytes.length)
119
+ }
120
+
121
+ function encodeFrames(codes: Int8Array): string {
122
+ return Buffer.from(codes.buffer, codes.byteOffset, codes.length).toString('base64')
123
+ }
124
+
125
+ /** Corrupt or foreign state is unavailable, never a fresh start. */
126
+ export function validMacWindowVoices(value: unknown): value is MacWindowVoiceState {
127
+ const s = value as MacWindowVoiceState | null
128
+ if (!s || typeof s !== 'object' || s.version !== 1 || s.kind !== 'window') return false
129
+ if (!isIndex(s.nextTrack) || s.nextTrack > MAC_WINDOW_MAX_TRACKS || !isIndex(s.voices) || s.voices > MAX_VOICE) return false
130
+ if (!Array.isArray(s.tracks) || s.tracks.length > s.nextTrack || !Array.isArray(s.recent) || s.recent.length > MAC_WINDOW_MAX_CHUNKS - 1) return false
131
+ const voices = new Set<number>()
132
+ let lastId = -1
133
+ for (let i = 0; i < s.tracks.length; i++) {
134
+ const t = s.tracks[i]
135
+ if (!t || !isIndex(t.id) || t.id <= lastId || t.id >= s.nextTrack || !Array.isArray(t.proofs) || t.proofs.length > MAC_WINDOW_PROOFS) return false
136
+ lastId = t.id
137
+ if (!t.proofs.every(p => p && isIndex(p.chunk) && typeof p.audioSha256 === 'string' && SHA.test(p.audioSha256))) return false
138
+ if (new Set(t.proofs.map(p => p.chunk)).size !== t.proofs.length || new Set(t.proofs.map(p => p.audioSha256)).size !== t.proofs.length) return false
139
+ if (t.voice !== null) {
140
+ if (!isIndex(t.voice) || t.voice < 1 || t.voice > s.voices || voices.has(t.voice) || t.proofs.length < MAC_WINDOW_PROOFS) return false
141
+ voices.add(t.voice)
142
+ } else if (t.proofs.length >= MAC_WINDOW_PROOFS) return false
143
+ }
144
+ const ids = new Set(s.tracks.map(t => t.id))
145
+ let last = -1
146
+ for (const r of s.recent) {
147
+ if (!r || !isIndex(r.chunk) || r.chunk <= last) return false
148
+ const codes = decodeFrames(r.frames)
149
+ // A frame may only name a track that still exists (an evicted id has no frame).
150
+ if (!codes || codes.length === 0 || codes.some(c => c < UNTRACKED || (c >= 0 && !ids.has(c)))) return false
151
+ last = r.chunk
152
+ }
153
+ return true
154
+ }
155
+
156
+ export interface MacWindowChunk {
157
+ chunk: number
158
+ /** Samples of this chunk inside the window WAV (the window is these chunks back to back). */
159
+ samples: number
160
+ }
161
+
162
+ export interface MacWindowPlan {
163
+ chunks: MacWindowChunk[]
164
+ }
165
+
166
+ /**
167
+ * Which durable chunks go in chunk k's window: k and the contiguous run before it,
168
+ * newest first, until MAC_WINDOW_MAX_CHUNKS or MAC_WINDOW_MAX_SAMPLES. A missing
169
+ * chunk ends the run (no gap is ever bridged). `samplesOf` returns null when a
170
+ * chunk has no durable WAV.
171
+ */
172
+ export function planMacWindow(chunkIndex: number, samplesOf: (chunk: number) => number | null): MacWindowPlan | null {
173
+ if (!isIndex(chunkIndex)) return null
174
+ const own = samplesOf(chunkIndex)
175
+ if (!isIndex(own) || own === 0 || own > MAC_WINDOW_MAX_SAMPLES) return null
176
+ const chunks: MacWindowChunk[] = [{ chunk: chunkIndex, samples: own }]
177
+ let total = own
178
+ for (let c = chunkIndex - 1; c >= 0 && chunks.length < MAC_WINDOW_MAX_CHUNKS; c--) {
179
+ const n = samplesOf(c)
180
+ if (!isIndex(n) || n === 0 || total + n > MAC_WINDOW_MAX_SAMPLES) break
181
+ chunks.unshift({ chunk: c, samples: n })
182
+ total += n
183
+ }
184
+ return { chunks }
185
+ }
186
+
187
+ export interface MacWindowStats {
188
+ overlapChunks: number
189
+ /** Slots carried to an existing track through the overlap (before the agreement check). */
190
+ carried: number
191
+ /** Carried slots that speak in this chunk and that the chunk diarized alone did not agree with: no label, no proof, frames stored untracked. (A carried slot silent in this chunk is not counted.) */
192
+ withheld: number
193
+ created: number
194
+ confirmed: number
195
+ /** Tracks dropped because no recent frame refers to them. */
196
+ evicted: number
197
+ /** New tracks refused because every id was in use. */
198
+ capped: number
199
+ }
200
+
201
+ export interface MacWindowResult {
202
+ /** The state to commit with this chunk (the input is not mutated). */
203
+ state: MacWindowVoiceState
204
+ /** Activity for the current chunk alone, in its own time base (frame 0 = chunk start). */
205
+ chunkActivity: Activity
206
+ /** Confirmed `Voice N` for a window slot, or null (the slot's words stay `Ext`). */
207
+ labelOf: (slot: number) => string | null
208
+ /** Counts for the log line; never text. */
209
+ stats: MacWindowStats
210
+ }
211
+
212
+ function activeSlots(a: Activity, f: number): number[] {
213
+ const out: number[] = []
214
+ for (let c = 0; c < NEMOTRON_CHANNELS; c++) if (a.probs[f * NEMOTRON_CHANNELS + c] >= ACTIVE_THRESHOLD) out.push(c)
215
+ return out
216
+ }
217
+
218
+ /**
219
+ * Does the chunk diarized alone put this slot's exclusive frames on one channel?
220
+ * `alone` is the per-chunk run's activity in the chunk's own time base. A merged
221
+ * window slot (two voices) splits across channels there; a slot the per-chunk run
222
+ * cannot place is not agreed with. No evidence is no agreement.
223
+ */
224
+ export function chunkAgreesWithSlot(window: Activity, start: number, length: number, slot: number, alone: Activity | null): boolean {
225
+ if (!alone || alone.probs.length !== alone.frames * NEMOTRON_CHANNELS) return false
226
+ let own = 0
227
+ let covered = 0
228
+ const perChannel = new Map<number, number>()
229
+ for (let i = 0; i < length && start + i < window.frames; i++) {
230
+ const slots = activeSlots(window, start + i)
231
+ if (slots.length !== 1 || slots[0] !== slot) continue
232
+ own++
233
+ if (i >= alone.frames) continue
234
+ const there = activeSlots(alone, i)
235
+ if (there.length !== 1) continue
236
+ covered++
237
+ perChannel.set(there[0], (perChannel.get(there[0]) ?? 0) + 1)
238
+ }
239
+ // A slot with no exclusive frame here (own = 0) places nothing (covered = 0): no agreement.
240
+ if (covered === 0 || covered < own * MAC_WINDOW_AGREE_COVERAGE) return false
241
+ return Math.max(...perChannel.values()) / covered >= MAC_WINDOW_AGREE_PURITY
242
+ }
243
+
244
+ /**
245
+ * Crosswalk one window's activity onto the capture's tracks and return chunk k's
246
+ * labels. Pure. Returns null (no labels) on any geometry it cannot trust.
247
+ * `chunkAlone` is the same chunk diarized by itself (or null when that run is not
248
+ * available): a carried slot is used only if it agrees with it (QA W1).
249
+ */
250
+ export function crosswalkMacWindow(input: {
251
+ state: MacWindowVoiceState
252
+ window: Activity
253
+ plan: MacWindowPlan
254
+ audioSha256: string
255
+ chunkAlone: Activity | null
256
+ }): MacWindowResult | null {
257
+ const { window, plan, audioSha256 } = input
258
+ if (!validMacWindowVoices(input.state) || !SHA.test(audioSha256) || !plan.chunks.length) return null
259
+ if (!Number.isInteger(window.frames) || window.frames <= 0 || window.probs.length !== window.frames * NEMOTRON_CHANNELS) return null
260
+ for (const v of window.probs) if (!Number.isFinite(v) || v < 0 || v > 1) return null
261
+ const state: MacWindowVoiceState = structuredClone(input.state)
262
+ const current = plan.chunks[plan.chunks.length - 1]
263
+ // Window frame offset of each chunk: its first sample / 160. Chunks are whole frames in practice (6 s); an odd length shifts later offsets by under a frame, which the geometry check below still bounds.
264
+ const offsets = new Map<number, number>()
265
+ let sample = 0
266
+ for (const c of plan.chunks) { offsets.set(c.chunk, Math.floor(sample / MAC_WINDOW_SAMPLES_PER_FRAME)); sample += c.samples }
267
+ if (window.frames !== macWindowFrameCount(sample)) return null
268
+ const start = offsets.get(current.chunk)!
269
+ const length = Math.floor(current.samples / MAC_WINDOW_SAMPLES_PER_FRAME)
270
+ const trackById = (id: number) => state.tracks.find(t => t.id === id)!
271
+
272
+ // 1. Overlap evidence: frames of earlier window chunks this capture attributed.
273
+ const votes = new Map<number, Map<number, number>>()
274
+ const exclusiveInOverlap = new Map<number, number>()
275
+ let overlapChunks = 0
276
+ for (const r of state.recent) {
277
+ if (r.chunk >= current.chunk || !offsets.has(r.chunk)) continue
278
+ const codes = decodeFrames(r.frames)!
279
+ const off = offsets.get(r.chunk)!
280
+ const n = Math.min(codes.length, Math.floor(plan.chunks.find(c => c.chunk === r.chunk)!.samples / MAC_WINDOW_SAMPLES_PER_FRAME))
281
+ overlapChunks++
282
+ for (let i = 0; i < n; i++) {
283
+ const slots = activeSlots(window, off + i)
284
+ if (slots.length !== 1) continue
285
+ const slot = slots[0]
286
+ exclusiveInOverlap.set(slot, (exclusiveInOverlap.get(slot) ?? 0) + 1)
287
+ const track = codes[i]
288
+ if (track < 0) continue
289
+ const row = votes.get(slot) ?? new Map<number, number>()
290
+ row.set(track, (row.get(track) ?? 0) + 1)
291
+ votes.set(slot, row)
292
+ }
293
+ }
294
+ // 2. One-to-one carry, strongest first. Two tracks tied on ONE slot cannot reach the 0.8 agreement, so
295
+ // that slot is refused there. Two slots tied for ONE track go to the lower slot index; if that slot is not
296
+ // the one speaking in this chunk, step 2b withholds it (it has no exclusive frames here) and the
297
+ // other slot gets no track either: no label, never the wrong one.
298
+ const candidates: Array<{ slot: number; track: number; count: number }> = []
299
+ for (const [slot, row] of votes) for (const [track, count] of row) candidates.push({ slot, track, count })
300
+ candidates.sort((a, b) => b.count - a.count || a.slot - b.slot || a.track - b.track)
301
+ const slotTrack = new Map<number, number>()
302
+ const usedTracks = new Set<number>()
303
+ for (const c of candidates) {
304
+ if (slotTrack.has(c.slot) || usedTracks.has(c.track)) continue
305
+ if (c.count < MAC_WINDOW_CARRY_MIN_FRAMES || c.count / exclusiveInOverlap.get(c.slot)! < MAC_WINDOW_CARRY_AGREEMENT) continue
306
+ slotTrack.set(c.slot, c.track)
307
+ usedTracks.add(c.track)
308
+ }
309
+ const carried = slotTrack.size
310
+ // 2b. The current chunk must agree (QA W1). A slot whose overlap says "track T"
311
+ // while this chunk's own audio says "two voices here" is not carried: it gets
312
+ // no label and no proof, and its frames below are stored untracked.
313
+ const exclusiveNew = new Map<number, number>()
314
+ const sharedNew = new Map<number, number>()
315
+ for (let i = 0; i < length && start + i < window.frames; i++) {
316
+ const slots = activeSlots(window, start + i)
317
+ if (slots.length === 1) exclusiveNew.set(slots[0], (exclusiveNew.get(slots[0]) ?? 0) + 1)
318
+ else for (const s of slots) sharedNew.set(s, (sharedNew.get(s) ?? 0) + 1)
319
+ }
320
+ let withheld = 0
321
+ for (const slot of [...slotTrack.keys()]) {
322
+ // A carried slot that is silent here (no exclusive frame) has nothing to label or prove this chunk: it
323
+ // simply is not used, and is not a disagreement. A speaking one must be agreed with.
324
+ if ((exclusiveNew.get(slot) ?? 0) === 0) { slotTrack.delete(slot); continue }
325
+ if (chunkAgreesWithSlot(window, start, length, slot, input.chunkAlone)) continue
326
+ slotTrack.delete(slot)
327
+ withheld++
328
+ }
329
+ // 3. New tracks: only a slot with no attributed overlap at all (it may be an
330
+ // existing voice the crosswalk could not prove; then it stays untracked).
331
+ let created = 0
332
+ let capped = 0
333
+ for (const [slot, n] of [...exclusiveNew.entries()].sort((a, b) => a[0] - b[0])) {
334
+ if (slotTrack.has(slot) || votes.has(slot) || n < MAC_WINDOW_NEW_TRACK_FRAMES) continue
335
+ // The lowest id no track holds. An evicted id is free: no recent frame refers to it.
336
+ let id = 0
337
+ while (id < MAC_WINDOW_MAX_TRACKS && state.tracks.some(t => t.id === id)) id++
338
+ if (id >= MAC_WINDOW_MAX_TRACKS) { capped++; continue }
339
+ state.tracks.push({ id, voice: null, proofs: [] })
340
+ state.tracks.sort((a, b) => a.id - b.id)
341
+ state.nextTrack = Math.max(state.nextTrack, id + 1)
342
+ slotTrack.set(slot, id)
343
+ created++
344
+ }
345
+ // 4. Proof for this chunk, then confirmation.
346
+ let confirmed = 0
347
+ for (const [slot, track] of slotTrack) {
348
+ const ex = exclusiveNew.get(slot) ?? 0
349
+ const purity = ex / Math.max(1, ex + (sharedNew.get(slot) ?? 0))
350
+ const t = trackById(track)
351
+ if (ex < MAC_WINDOW_PROOF_FRAMES || purity < MAC_WINDOW_PROOF_PURITY || t.voice !== null) continue
352
+ if (t.proofs.some(p => p.chunk === current.chunk || p.audioSha256 === audioSha256)) continue
353
+ t.proofs.push({ chunk: current.chunk, audioSha256 })
354
+ if (t.proofs.length >= MAC_WINDOW_PROOFS) {
355
+ if (state.voices >= MAX_VOICE) { t.proofs.pop(); continue }
356
+ t.voice = ++state.voices
357
+ confirmed++
358
+ }
359
+ }
360
+ // The labels this chunk may show, taken before any eviction.
361
+ const labelByTrack = new Map<number, number | null>(state.tracks.map(t => [t.id, t.voice]))
362
+ // 5. Remember this chunk's frames for the next window (bounded, no audio).
363
+ const codes = new Int8Array(length)
364
+ for (let i = 0; i < length; i++) {
365
+ const slots = start + i < window.frames ? activeSlots(window, start + i) : []
366
+ codes[i] = slots.length === 0 ? NONE : slots.length > 1 ? OVERLAP : (slotTrack.get(slots[0]) ?? UNTRACKED)
367
+ }
368
+ state.recent = [...state.recent.filter(r => r.chunk !== current.chunk), { chunk: current.chunk, frames: encodeFrames(codes) }]
369
+ .sort((a, b) => a.chunk - b.chunk)
370
+ .slice(-(MAC_WINDOW_MAX_CHUNKS - 1))
371
+ // 6. Evict (QA W3): a track no remembered frame refers to can never be carried
372
+ // again, so it only spends an id. Its Voice number stays spent (`voices` never drops).
373
+ const referenced = new Set<number>()
374
+ for (const r of state.recent) for (const c of decodeFrames(r.frames)!) if (c >= 0) referenced.add(c)
375
+ const before = state.tracks.length
376
+ state.tracks = state.tracks.filter(t => referenced.has(t.id))
377
+ const evicted = before - state.tracks.length
378
+ // The chunk's own activity: the same frames a standalone chunk run would have.
379
+ const chunkFrames = Math.min(window.frames - start, macWindowFrameCount(current.samples))
380
+ const chunkActivity: Activity = {
381
+ frames: chunkFrames,
382
+ probs: window.probs.slice(start * NEMOTRON_CHANNELS, (start + chunkFrames) * NEMOTRON_CHANNELS),
383
+ }
384
+ const labelOf = (slot: number): string | null => {
385
+ const track = slotTrack.get(slot)
386
+ const voice = track === undefined ? null : labelByTrack.get(track)
387
+ return voice ? voiceLabel(voice) : null
388
+ }
389
+ return { state, chunkActivity, labelOf, stats: { overlapChunks, carried, withheld, created, confirmed, evicted, capped } }
390
+ }
@@ -282,7 +282,7 @@ export function toEngineSentences(rows: unknown): FirefliesSentenceInput[] {
282
282
  }
283
283
 
284
284
  function g2FromSidecar(doc: Record<string, unknown>, sessionId: string): G2RecordingInput | null {
285
- if (doc.sourceType === 'mac_system_audio') return null // Mac pilot never enters the G2-only blend engine.
285
+ if (doc.sourceType === 'mac_system_audio' || doc.sourceType === 'iphone_microphone' || sessionId.startsWith('phone_')) return null // Neither source enters the G2-only blend engine.
286
286
  const startMs = numberOr(doc.startTime, Number.NaN)
287
287
  const durationMs = numberOr(doc.durationMs, Number.NaN)
288
288
  if (!Number.isFinite(startMs) || !Number.isFinite(durationMs)) return null