@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.
|
|
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
|
-
|
|
85
|
-
|
|
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}`)
|