@gotcos/glasses-server 6.94.2 → 6.95.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 +14 -0
- package/README.md +52 -0
- package/package.json +2 -1
- package/server/index.ts +13 -1
- package/server/lib/speaker-shadow-decide.ts +466 -0
- package/server/lib/speaker-shadow-models.ts +150 -0
- package/server/lib/speaker-shadow-numeric.ts +143 -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 +277 -0
- package/server/lib/speaker-shadow.ts +727 -0
- package/server/routes/health.ts +5 -0
- package/server/routes/meeting.ts +4 -0
- package/server/routes/speaker-shadow.ts +79 -0
package/server/routes/health.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { localFirstMeetingsCapability } from '../lib/local-first-meetings-contra
|
|
|
16
16
|
import { isSileroAvailable } from '../lib/vad-silero.js'
|
|
17
17
|
import { profileProvenanceSummary, speakerModelState, speakerReadiness } from '../lib/speaker-embeddings.js'
|
|
18
18
|
import { nemotronLive } from '../lib/nemotron-live.js'
|
|
19
|
+
import { speakerShadowHealthFields } from '../lib/speaker-shadow.js'
|
|
19
20
|
import { finalPassSnapshot } from '../lib/nemotron-final.js'
|
|
20
21
|
import { chunkEmbeddingStoreStats } from '../lib/chunk-embedding-store.js'
|
|
21
22
|
import { mintDisplayTicket, DISPLAY_TICKET_TTL_SECONDS } from '../lib/display-ticket.js'
|
|
@@ -579,6 +580,10 @@ healthRouter.get('/health', async (_req, res) => {
|
|
|
579
580
|
// count only (health is public); the items and their Retry are behind the token at
|
|
580
581
|
// GET /api/maintenance/status.
|
|
581
582
|
crash_loop_parking: crashLoopParkingHealth(),
|
|
583
|
+
// 6.95.0: speaker merge shadow mode, only while it is on (absent when off). Codes and counts
|
|
584
|
+
// only (health is public): the proposals and the last error's text are behind the token at
|
|
585
|
+
// /api/speaker-shadow/*.
|
|
586
|
+
...speakerShadowHealthFields(),
|
|
582
587
|
// Key lanes (plan v4 3.1): `details.keyLanes` only while enabled. Absent, never null, when off.
|
|
583
588
|
...keyLanesHealthFields(),
|
|
584
589
|
})
|
package/server/routes/meeting.ts
CHANGED
|
@@ -203,6 +203,7 @@ import { enrichStandaloneMeeting } from '../lib/meeting-summary-persistence.js'
|
|
|
203
203
|
import { bootResumeGate, type BootResumeGate } from '../lib/boot-resume.js'
|
|
204
204
|
import { recordFinalPassSkip, runNemotronFinalPass, type FinalPassOptions, type FinalPassOutcome } from '../lib/nemotron-final.js'
|
|
205
205
|
import { runTranscriptCleanup, type CleanupOutcome } from '../lib/transcript-cleanup.js'
|
|
206
|
+
import { notifySpeakerShadowFinalized } from '../lib/speaker-shadow.js'
|
|
206
207
|
import { sweepTranscriptCleanupData } from '../lib/transcript-cleanup-screen.js'
|
|
207
208
|
import type { TranscriptCleanupRouteDeps } from './transcript-cleanup.js'
|
|
208
209
|
|
|
@@ -491,6 +492,9 @@ export function scheduleFinalizationJob(job: MeetingFinalizationJob, runtime: Fi
|
|
|
491
492
|
await enrichStandaloneMeeting(current.meetingPath, jobStartedAt)
|
|
492
493
|
}
|
|
493
494
|
markCanonicalFinalizationState(current.sidecarPath, 'complete', false)
|
|
495
|
+
// 6.95.0: speaker merge shadow mode (off unless COS_SPEAKER_SHADOW=1). Log only and
|
|
496
|
+
// fire-and-forget: it queues, defers under live work, and never throws back here.
|
|
497
|
+
if (MAC_CAPTURE_ID.test(current.sessionId)) notifySpeakerShadowFinalized({ sessionId: current.sessionId, sidecarPath: current.sidecarPath })
|
|
494
498
|
// The capture is finished and its files are durable, so the merge engine may score it.
|
|
495
499
|
// Fire-and-forget by contract: the runner queues, defers under a drain or a live
|
|
496
500
|
// capture, and never throws back at the finalization path.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Speaker merge shadow mode (6.95.0). Token-protected by the /api middleware. READ ONLY.
|
|
2
|
+
//
|
|
3
|
+
// GET /api/speaker-shadow/proposals the logged proposal records, newest first
|
|
4
|
+
// ?limit=1..100 (default 10) ?sessionId=mac_<uuid>
|
|
5
|
+
// GET /api/speaker-shadow/status the switch, queue, daily cap, breaker, last error and
|
|
6
|
+
// setup readiness (models, CLI, sandbox)
|
|
7
|
+
// GET /api/speaker-shadow/settings { enabled, source } source: env | setting | default
|
|
8
|
+
// POST /api/speaker-shadow/settings {"enabled": true|false}: this Mac's saved switch (no
|
|
9
|
+
// restart); 409 env_forced when COS_SPEAKER_SHADOW pins it
|
|
10
|
+
//
|
|
11
|
+
// The settings POST is the only write, and it writes only <data>/speaker-shadow/settings.json.
|
|
12
|
+
// Nothing on these routes changes a meeting, a label, a name or a profile. A 404 means a
|
|
13
|
+
// server older than 6.95.0.
|
|
14
|
+
|
|
15
|
+
import { Router } from 'express'
|
|
16
|
+
import { getSpeakerShadowRunner, readSpeakerShadowRecords, resolveSpeakerShadowSwitch, speakerShadowEnvSwitch, writeSpeakerShadowSetting } from '../lib/speaker-shadow.js'
|
|
17
|
+
import { speakerShadowRoot } from '../lib/speaker-shadow-models.js'
|
|
18
|
+
|
|
19
|
+
export interface SpeakerShadowRouterDeps {
|
|
20
|
+
root?: () => string
|
|
21
|
+
env?: NodeJS.ProcessEnv
|
|
22
|
+
snapshot?: () => ReturnType<ReturnType<typeof getSpeakerShadowRunner>['snapshot']>
|
|
23
|
+
/** Called after the setting turns shadow mode on (the runner picks up anything still queued). */
|
|
24
|
+
onEnabled?: () => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const SESSION_ID = /^mac_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
|
28
|
+
|
|
29
|
+
export function createSpeakerShadowRouter(deps: SpeakerShadowRouterDeps = {}): Router {
|
|
30
|
+
const router = Router()
|
|
31
|
+
const root = deps.root ?? speakerShadowRoot
|
|
32
|
+
const snapshot = deps.snapshot ?? (() => getSpeakerShadowRunner().snapshot({ detail: true }))
|
|
33
|
+
const env = deps.env ?? process.env
|
|
34
|
+
const onEnabled = deps.onEnabled ?? (() => getSpeakerShadowRunner().resume())
|
|
35
|
+
|
|
36
|
+
router.get('/speaker-shadow/proposals', async (req, res) => {
|
|
37
|
+
const rawLimit = Number(req.query.limit ?? 10)
|
|
38
|
+
if (!Number.isInteger(rawLimit) || rawLimit < 1 || rawLimit > 100) { res.status(400).json({ error: 'limit must be an integer from 1 to 100' }); return }
|
|
39
|
+
const sessionId = typeof req.query.sessionId === 'string' ? req.query.sessionId : undefined
|
|
40
|
+
if (sessionId !== undefined && !SESSION_ID.test(sessionId)) { res.status(400).json({ error: 'sessionId must be a mac_<uuid> session id' }); return }
|
|
41
|
+
try {
|
|
42
|
+
const records = await readSpeakerShadowRecords(root(), { limit: rawLimit, sessionId })
|
|
43
|
+
res.json({ logOnly: true, shadow: snapshot(), count: records.length, records })
|
|
44
|
+
} catch (error) {
|
|
45
|
+
res.status(500).json({ error: `could not read the shadow log: ${error instanceof Error ? error.message : String(error)}` })
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
router.get('/speaker-shadow/status', (_req, res) => {
|
|
50
|
+
res.json({ logOnly: true, ...snapshot() })
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
router.get('/speaker-shadow/settings', (_req, res) => {
|
|
54
|
+
res.set('Cache-Control', 'private, no-store')
|
|
55
|
+
res.json(resolveSpeakerShadowSwitch(env, root()))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
router.post('/speaker-shadow/settings', (req, res) => {
|
|
59
|
+
res.set('Cache-Control', 'private, no-store')
|
|
60
|
+
const body = req.body as { enabled?: unknown } | undefined
|
|
61
|
+
if (!body || typeof body !== 'object' || Array.isArray(body) || typeof body.enabled !== 'boolean' || Object.keys(body).some(k => k !== 'enabled')) {
|
|
62
|
+
res.status(400).json({ error: 'invalid_request', expected: '{"enabled": true|false}' })
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
const pinned = speakerShadowEnvSwitch(env)
|
|
66
|
+
if (pinned !== null && pinned !== body.enabled) { res.status(409).json({ error: 'env_forced', ...resolveSpeakerShadowSwitch(env, root()) }); return }
|
|
67
|
+
try {
|
|
68
|
+
writeSpeakerShadowSetting(body.enabled, root())
|
|
69
|
+
} catch {
|
|
70
|
+
res.status(500).json({ error: 'settings_write_failed' })
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
const view = resolveSpeakerShadowSwitch(env, root())
|
|
74
|
+
if (view.enabled) { try { onEnabled() } catch { /* the next finished meeting starts it */ } }
|
|
75
|
+
res.json(view)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
return router
|
|
79
|
+
}
|