@gotcos/glasses-server 6.86.0 → 6.87.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 CHANGED
@@ -1,3 +1,27 @@
1
+ ## 6.87.0 - A rough level per chunk for COS Control's glasses meeting strip
2
+
3
+ The server half of the level strip COS Control 0.5.339 draws in a glasses meeting's bottom bar (plan of 2026-10-23,
4
+ approved 09:25). The quiet gate already measured every live chunk (RMS, and Silero speech seconds) and only printed the
5
+ numbers on the `[perf]` line. Now each chunk keeps them.
6
+
7
+ - **Two numbers per chunk on the active-session record.** `chunkLevels[{i, level, speechSec}]` in
8
+ `active-sessions/<id>.json`: `level` is the chunk's RMS on a dBFS scale, 0 at -60 dBFS and under, 1 at -10 dBFS and
9
+ over, three decimals; `speechSec` is the Silero speech seconds (two decimals), or null when the gate fell back to
10
+ RMS. Every chunk the quiet gate sees gets one, quiet chunks included; a replayed index keeps its first answer, like
11
+ the chunk itself. Bounded to the newest 120 chunks (about 12 minutes of 6 s chunks) and restored, validated, after a
12
+ restart. It lives only as long as the record (deleted at save). `server/lib/chunk-levels.ts`.
13
+ - **Control reads it from the file it already reads.** The helper's `live-transcript` read opens this same file, so
14
+ there is no new route. `capabilities.liveMeeting.chunkLevels: true` on `/api/health` and `/api/models` tells Control
15
+ the record carries levels; an older server omits it and Control keeps its still glyph.
16
+ - **Not text, never logged.** Nothing new is logged, and no text is stored: the entries hold an index and two numbers.
17
+ `transcribe-stream-chunk-levels.test.ts` spies every console level and checks the record holds numbers only.
18
+ - **No spend.** No LLM, no new caller, no new disk write: the numbers ride on the durable write each chunk already makes.
19
+ - **Checks (6.87.0 branch).** Full suite, serial (`--maxWorkers=1`), HOME, COS_DATA_DIR and COS_PROFILE_PATH in a scratch folder,
20
+ COS_SCRIPTS_DIR unset: 584 files (583 passed, 1 skipped), 8815 tests, 8801 passed, 14 skipped, 0 failed. Mutation gate `live-meeting-6870-mutation-gate.mjs` against a green
21
+ baseline (32 tests in 4 files): 25 of 25 killed, each by a named
22
+ test, every target landing exactly once and restored byte for byte. `tsc --noEmit` clean. Pack dry run 546 files (6.86.0:
23
+ 545; the one new file is `server/lib/chunk-levels.ts`).
24
+
1
25
  ## 6.86.0 - Decision, promise and follow-up cues, live cues on Mac recordings, and the grey provisional line
2
26
 
3
27
  The server half of COS Control's live meeting view (plan of 2026-10-22, decisions of 15:21). Three lanes: three more cue
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.86.0",
3
+ "version": "6.87.0",
4
4
  "description": "COS Glasses: self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, Cursor Agent CLI, or local Ollama",
5
5
  "type": "module",
6
6
  "bin": {
@@ -135,6 +135,7 @@
135
135
  "docs/test-plan-6.71.7.md",
136
136
  "!server/scripts/question-hold-mutation-gate.mjs",
137
137
  "!server/scripts/live-meeting-6860-mutation-gate.mjs",
138
+ "!server/scripts/live-meeting-6870-mutation-gate.mjs",
138
139
  "docs/test-plan-6.71.8.md",
139
140
  "docs/test-plan-6.71.9.md",
140
141
  "docs/test-plan-6.71.10.md",
@@ -0,0 +1,64 @@
1
+ // 6.87.0: a rough level per live chunk, for COS Control's level strip on glasses meetings.
2
+ //
3
+ // The quiet gate already measures every chunk (stt-live.ts decideQuiet: RMS and, with Silero, speech seconds). Until
4
+ // 6.87.0 those numbers only reached the [perf] line. Now each chunk keeps two numbers on the active session record:
5
+ // `level`, 0 to 1 from the chunk's RMS on a dBFS scale, and `speechSec`, the Silero speech seconds (null when the gate
6
+ // fell back to RMS). Control reads them from the same active-session file its live transcript reads, so there is no
7
+ // new route and no new log line.
8
+ //
9
+ // NOT TEXT. Two numbers per chunk, bounded to the newest CHUNK_LEVELS_LIMIT chunks, living only as long as the
10
+ // active-session record (the 4-hour crash-recovery file, deleted at save). Nothing here logs.
11
+
12
+ /** The newest chunks kept: about 12 minutes of 6 s chunks, far more than the strip draws. */
13
+ export const CHUNK_LEVELS_LIMIT = 120
14
+ /** The dBFS that reads as 0: under the quiet gate's RMS 150 (about -47 dBFS) and the room floor of a G2 mic. */
15
+ export const LEVEL_FLOOR_DBFS = -60
16
+ /** The dBFS that reads as 1: loud close speech. */
17
+ export const LEVEL_CEILING_DBFS = -10
18
+
19
+ export interface ChunkLevel {
20
+ /** The chunk's own index. */
21
+ i: number
22
+ /** 0 to 1, three decimals. */
23
+ level: number
24
+ /** Silero speech seconds, two decimals; null when the quiet gate used RMS. */
25
+ speechSec: number | null
26
+ }
27
+
28
+ /** A chunk's RMS (PCM16 units) as 0 to 1: LEVEL_FLOOR_DBFS and under is 0, LEVEL_CEILING_DBFS and over is 1. */
29
+ export function normalizedLevel(rms: number): number {
30
+ if (!Number.isFinite(rms) || rms <= 0) return 0
31
+ const dbfs = 20 * Math.log10(rms / 32768)
32
+ const level = (dbfs - LEVEL_FLOOR_DBFS) / (LEVEL_CEILING_DBFS - LEVEL_FLOOR_DBFS)
33
+ return Math.round(Math.min(1, Math.max(0, level)) * 1000) / 1000
34
+ }
35
+
36
+ /** The stored entry for one chunk's quiet decision. */
37
+ export function chunkLevel(chunkIndex: number, rms: number, speechSec: number | null): ChunkLevel {
38
+ const speech = speechSec !== null && Number.isFinite(speechSec) ? Math.round(Math.max(0, speechSec) * 100) / 100 : null
39
+ return { i: chunkIndex, level: normalizedLevel(rms), speechSec: speech }
40
+ }
41
+
42
+ /** `levels` with `entry` in index order (a second entry for an index replaces the first), the newest
43
+ * CHUNK_LEVELS_LIMIT kept. Never mutates its input. */
44
+ export function withChunkLevel(levels: readonly ChunkLevel[] | undefined, entry: ChunkLevel): ChunkLevel[] {
45
+ const next = (levels ?? []).filter(l => l.i !== entry.i)
46
+ next.push(entry)
47
+ next.sort((a, b) => a.i - b.i)
48
+ return next.length > CHUNK_LEVELS_LIMIT ? next.slice(next.length - CHUNK_LEVELS_LIMIT) : next
49
+ }
50
+
51
+ /** The levels from a recovered session file: only well-formed entries, one per index, in order, bounded. */
52
+ export function restoreChunkLevels(value: unknown): ChunkLevel[] {
53
+ if (!Array.isArray(value)) return []
54
+ let levels: ChunkLevel[] = []
55
+ for (const raw of value) {
56
+ if (!raw || typeof raw !== 'object') continue
57
+ const { i, level, speechSec } = raw as Record<string, unknown>
58
+ if (!Number.isInteger(i) || (i as number) < 0) continue
59
+ if (typeof level !== 'number' || !Number.isFinite(level) || level < 0 || level > 1) continue
60
+ if (speechSec !== null && (typeof speechSec !== 'number' || !Number.isFinite(speechSec) || speechSec < 0)) continue
61
+ levels = withChunkLevel(levels, { i: i as number, level, speechSec: speechSec as number | null })
62
+ }
63
+ return levels
64
+ }
@@ -80,7 +80,8 @@ export function liveProvisional(): LiveProvisionalStore { return (store ??= new
80
80
  /** 6.86.0: what /api/health and /api/models publish as `capabilities.liveMeeting`, so COS Control gates the live meeting
81
81
  * view by capability, never by a status code. Constants only (health is public). An older server omits the key.
82
82
  * - macLiveCues: POST /api/mac-recording/start takes `liveCues: true` and answers `liveCues: {armed, ...}`.
83
- * - provisional: GET <route>?sessionId= serves the newest preview of a live meeting. */
84
- export function liveMeetingCapability(): { protocol: 1; macLiveCues: true; provisional: { route: string; maxChars: number; ttlMs: number } } {
85
- return { protocol: 1, macLiveCues: true, provisional: { route: '/api/transcribe-stream/provisional', maxChars: LIVE_PROVISIONAL_LIMITS.maxChars, ttlMs: LIVE_PROVISIONAL_LIMITS.ttlMs } }
83
+ * - provisional: GET <route>?sessionId= serves the newest preview of a live meeting.
84
+ * - chunkLevels (6.87.0): the active-session record keeps `chunkLevels[{i, level, speechSec}]` (lib/chunk-levels.ts). */
85
+ export function liveMeetingCapability(): { protocol: 1; macLiveCues: true; provisional: { route: string; maxChars: number; ttlMs: number }; chunkLevels: true } {
86
+ return { protocol: 1, macLiveCues: true, provisional: { route: '/api/transcribe-stream/provisional', maxChars: LIVE_PROVISIONAL_LIMITS.maxChars, ttlMs: LIVE_PROVISIONAL_LIMITS.ttlMs }, chunkLevels: true }
86
87
  }
@@ -117,6 +117,7 @@ import {
117
117
  import { feedLiveCueTranscript } from '../lib/live-cues-engine.js'
118
118
  import { armMacLiveCues, type MacLiveCuesAnswer } from '../lib/live-cues-v2.js'
119
119
  import { liveProvisional } from '../lib/live-provisional.js'
120
+ import { chunkLevel, restoreChunkLevels, withChunkLevel, type ChunkLevel } from '../lib/chunk-levels.js'
120
121
  import { preemptMetalBatchForLive, registerLiveActivityProbe } from '../lib/whisper-metal-gate.js'
121
122
  import {
122
123
  scheduleProgressiveHqCheckpoint,
@@ -348,6 +349,8 @@ interface TranscriptSession {
348
349
  /** Exact replay records for silent/filtered ASR completions. */
349
350
  emptyCompletions?: Record<string, EmptyTranscriptCompletion>
350
351
  maxChunkIndex?: number
352
+ /** 6.87.0: two numbers per chunk (level 0 to 1, Silero speech seconds) for Control's level strip. Never text. */
353
+ chunkLevels?: ChunkLevel[]
351
354
  /** Persisted idle-retention clock. Meeting date/duration still use startTime. */
352
355
  lastActivityAt: number
353
356
  // Count of consecutive vocab-echo (prompt-regurgitation) chunks. Reset to 0 by
@@ -713,6 +716,7 @@ function persistSessionRequired(sessionId: string): void {
713
716
  emptyCompletions: session.emptyCompletions ?? {},
714
717
  maxChunkIndex: session.maxChunkIndex ?? -1,
715
718
  providerCandidates: session.providerCandidates ?? {},
719
+ chunkLevels: session.chunkLevels ?? [],
716
720
  })
717
721
  try {
718
722
  durableAtomicWriteFileSync(filePath, data, { mode: 0o600 })
@@ -901,6 +905,7 @@ function recoverSessions(): void {
901
905
  providerCandidates: data.providerCandidates && typeof data.providerCandidates === 'object'
902
906
  ? data.providerCandidates
903
907
  : {},
908
+ chunkLevels: restoreChunkLevels(data.chunkLevels),
904
909
  }
905
910
  // Retroactively strip inline hallucinations from recovered chunks.
906
911
  // Two-pass: (1) scan all chunks to build frequency/blocklist,
@@ -2439,6 +2444,8 @@ async function processStreamChunk(opts: {
2439
2444
  const quiet = decideQuiet(audioBuffer, stt.quietGate)
2440
2445
  const rms = quiet.rms
2441
2446
  const isQuiet = quiet.isQuiet
2447
+ // 6.87.0: the level strip's two numbers, on the record every terminal path below persists. Not text, never logged.
2448
+ session.chunkLevels = withChunkLevel(session.chunkLevels, chunkLevel(chunkIndex, rms, quiet.speechSec))
2442
2449
  const whisperAudio = isQuiet ? audioBuffer : await asrFrontEnd(audioBuffer, stt.frontEnd)
2443
2450
  const whisperContext = recentWhisperContext(sessionId, session)
2444
2451
  console.log(`[perf] whisperContext: ${whisperContext.length}b | session.chunks: ${session.chunks.length}`)