@gotcos/glasses-server 6.37.2 → 6.37.3
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/.env.example +20 -1
- package/CHANGELOG.md +37 -0
- package/bin/cli.cjs +32 -0
- package/package.json +1 -1
- package/server/lib/unsaved-audio-quarantine.ts +35 -0
- package/server/routes/meeting.ts +19 -1
package/.env.example
CHANGED
|
@@ -218,7 +218,26 @@ BIND_HOST=0.0.0.0
|
|
|
218
218
|
# 72 covers a long weekend away from the Mac.
|
|
219
219
|
# COS_UNSAVED_AUDIO_RETENTION_HOURS=72
|
|
220
220
|
|
|
221
|
-
# ── LIVE CUES (optional
|
|
221
|
+
# ── LIVE CUES (optional, OFF unless you set it — costs model calls) ───────
|
|
222
|
+
#
|
|
223
|
+
# The closest thing COS has to conversing with you DURING a meeting rather than
|
|
224
|
+
# after it: it reads the running transcript and puts short prompts on the lens —
|
|
225
|
+
# the question you would have wanted to ask, while it is still useful.
|
|
226
|
+
#
|
|
227
|
+
# WHAT IT COSTS, PLAINLY. Every cue is a model call on YOUR agent quota, not a
|
|
228
|
+
# free local pass. It is bounded, but it is not free:
|
|
229
|
+
#
|
|
230
|
+
# at most 8 cue pipelines per meeting MAX_PIPELINES_PER_MEETING
|
|
231
|
+
# at most one start per 60 seconds FLOOR_BETWEEN_STARTS_MS
|
|
232
|
+
# 30s cooldown after a cue fires COOLDOWN_AFTER_CUE_MS
|
|
233
|
+
#
|
|
234
|
+
# So a long meeting can spend up to 8 model calls it would not otherwise make,
|
|
235
|
+
# and each one runs the full planner -> memory -> insight chain below. On a
|
|
236
|
+
# metered plan, leave this off. It IS off unless you set it.
|
|
237
|
+
#
|
|
238
|
+
# Runs on Cursor Composer. That is the only supported model in v1 — any other
|
|
239
|
+
# value fails closed rather than quietly billing a model you did not choose.
|
|
240
|
+
#
|
|
222
241
|
# Live meeting coaching cues on the lens: transcript window -> Composer
|
|
223
242
|
# planner -> Qdrant -> LightRAG -> Composer insight -> coaching_nudge.
|
|
224
243
|
# Requires COS_SCRIPTS_DIR (memory hops) plus the Cursor Agent CLI.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,40 @@
|
|
|
1
|
+
## 6.37.3
|
|
2
|
+
|
|
3
|
+
Speaker ID works out of the box. A silent capture stops retrying forever. Live
|
|
4
|
+
Cues says what it costs.
|
|
5
|
+
|
|
6
|
+
SPEAKER MODEL IN NORMAL SETUP. 6.37.2 added `--setup-speaker-model` as a command
|
|
7
|
+
you had to know about. Now the voiceprint model is fetched during ordinary setup,
|
|
8
|
+
with `SKIP_SPEAKER_MODEL_DOWNLOAD=1` as the escape hatch — the same shape the
|
|
9
|
+
whisper models already use, and those are 1.5 GB, sixty times larger.
|
|
10
|
+
|
|
11
|
+
Deliberately NOT a lazy fetch on first use. Lazy would fire the download at the
|
|
12
|
+
START OF A MEETING: 26 MB on hotel wifi degrading the exact session it exists to
|
|
13
|
+
improve, and a GitHub release asset promoted from an install-time dependency to a
|
|
14
|
+
runtime one. A failed fetch is non-fatal — diarization is opt-in by design, so
|
|
15
|
+
the server falls back to wearer/Ext rather than refusing to start.
|
|
16
|
+
|
|
17
|
+
A CAPTURE WITH NO SPEECH IS RECOVERED, NOT FAILED. The recover route threw
|
|
18
|
+
`recovery produced an empty transcript` when whisper returned nothing, which left
|
|
19
|
+
the capture in the unsaved list and retried it on every boot and every button
|
|
20
|
+
press. Measured on this machine: one 33-second capture failed that way 1,131
|
|
21
|
+
times, alternating in the log with the auto-recover path claiming success for the
|
|
22
|
+
same session, while the panel showed only "1 recoverable" and the error went to
|
|
23
|
+
stderr where nobody looks. The user presses Recover and nothing happens, because
|
|
24
|
+
nothing can.
|
|
25
|
+
|
|
26
|
+
Silence now writes a receipt with `outcome: 'no_speech'` and clears. The audio is
|
|
27
|
+
NOT deleted — it leaves on the ordinary retention clock, so a capture wrongly
|
|
28
|
+
judged silent by a bad decode is still on disk for its full window.
|
|
29
|
+
|
|
30
|
+
LIVE CUES NOW STATES ITS COST. The flag was already documented as a master
|
|
31
|
+
switch; what was missing was that every cue is a model call on your own agent
|
|
32
|
+
quota. Now stated with the real bounds — at most 8 pipelines per meeting, one
|
|
33
|
+
start per 60s, 30s cooldown — and the real model, Cursor Composer, which is the
|
|
34
|
+
only supported value in v1.
|
|
35
|
+
|
|
36
|
+
223 files, 3117 tests.
|
|
37
|
+
|
|
1
38
|
## 6.37.2
|
|
2
39
|
|
|
3
40
|
`--setup-speaker-model` — one command to install the voiceprint model.
|
package/bin/cli.cjs
CHANGED
|
@@ -706,6 +706,38 @@ if (whisperCliPath && SETUP_TRANSCRIPTION) {
|
|
|
706
706
|
}
|
|
707
707
|
}
|
|
708
708
|
|
|
709
|
+
// Step 5b: the voiceprint model — named per-speaker diarization.
|
|
710
|
+
//
|
|
711
|
+
// Folded into normal setup rather than fetched lazily on first use. Lazy would
|
|
712
|
+
// fire the download at the START OF A MEETING, which is the worst possible
|
|
713
|
+
// moment: 26 MB on hotel wifi degrades the exact session it exists to improve,
|
|
714
|
+
// and it makes a GitHub release asset a runtime dependency instead of an
|
|
715
|
+
// install-time one.
|
|
716
|
+
//
|
|
717
|
+
// This follows the precedent already set by the whisper models, which are 1.5 GB
|
|
718
|
+
// -- sixty times larger -- downloaded here with SKIP_WHISPER_DOWNLOAD as the
|
|
719
|
+
// escape hatch. Same shape, same opt-out, so nobody on a metered or restricted
|
|
720
|
+
// network is forced into it.
|
|
721
|
+
if (!SETUP_SPEAKER_MODEL && process.env.SKIP_SPEAKER_MODEL_DOWNLOAD !== '1') {
|
|
722
|
+
const spkDest = join(CONFIG_DIR, 'models', SPEAKER_MODEL.filename)
|
|
723
|
+
if (existsSync(spkDest) && sha256File(spkDest) === SPEAKER_MODEL.sha256) {
|
|
724
|
+
console.log(green(' ✓') + ' Voiceprint model ready ' + dim('— named speakers available'))
|
|
725
|
+
} else {
|
|
726
|
+
console.log(' ' + dim('Fetching the voiceprint model (~26 MB) for named speakers.'))
|
|
727
|
+
console.log(' ' + dim('Skip: SKIP_SPEAKER_MODEL_DOWNLOAD=1 npx --yes @gotcos/glasses-server@latest'))
|
|
728
|
+
const spkCode = setupSpeakerModel()
|
|
729
|
+
if (spkCode !== 0) {
|
|
730
|
+
// NOT fatal. Diarization is opt-in by design: without the model the server
|
|
731
|
+
// stays on wearer/Ext fallback rather than failing, so a failed fetch must
|
|
732
|
+
// not block a working install.
|
|
733
|
+
console.log(yellow(' ⚠') + ' Voiceprint model unavailable ' + dim('— speakers stay wearer/Ext'))
|
|
734
|
+
console.log(' ' + dim('Retry later: npx --yes @gotcos/glasses-server@latest --setup-speaker-model'))
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
} else if (process.env.SKIP_SPEAKER_MODEL_DOWNLOAD === '1') {
|
|
738
|
+
console.log(yellow(' ⚠') + ' SKIP_SPEAKER_MODEL_DOWNLOAD=1 — named speakers unavailable')
|
|
739
|
+
}
|
|
740
|
+
|
|
709
741
|
if (PREPARE_ONLY && SETUP_TRANSCRIPTION) {
|
|
710
742
|
console.log('')
|
|
711
743
|
if (transcriptionSetupFailures.length > 0) {
|
package/package.json
CHANGED
|
@@ -303,6 +303,41 @@ export function isRecoveryActive(dirName: string): boolean {
|
|
|
303
303
|
return activeRecoveries.has(dirName)
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
/**
|
|
307
|
+
* A capture that held no speech is RECOVERED, not failed.
|
|
308
|
+
*
|
|
309
|
+
* The recover route used to throw `recovery produced an empty transcript` when
|
|
310
|
+
* whisper returned nothing, which left the capture in the unsaved list and
|
|
311
|
+
* retried it on every boot and every button press. Measured 2026-08-25: one
|
|
312
|
+
* 33-second capture failed this way 1,131 times, alternating with the
|
|
313
|
+
* auto-recover path claiming success, while the panel said only "1 recoverable"
|
|
314
|
+
* and the error went to stderr where nobody looks.
|
|
315
|
+
*
|
|
316
|
+
* Silence is a legitimate outcome. There is nothing to save and nothing to
|
|
317
|
+
* retry, so the capture is receipted and clears -- with `outcome: 'no_speech'`
|
|
318
|
+
* so the distinction stays auditable rather than looking like a normal save.
|
|
319
|
+
*
|
|
320
|
+
* The audio is NOT deleted here. It leaves on the ordinary retention clock, so a
|
|
321
|
+
* capture wrongly judged silent (a bad decode, a broken model) is still on disk
|
|
322
|
+
* for its full window.
|
|
323
|
+
*/
|
|
324
|
+
export function markRecoveredNoSpeech(dirPath: string, chunkFiles: number): void {
|
|
325
|
+
try {
|
|
326
|
+
writeFileSync(
|
|
327
|
+
resolve(dirPath, RECOVERED_RECEIPT),
|
|
328
|
+
`${JSON.stringify({
|
|
329
|
+
schemaVersion: 1,
|
|
330
|
+
recoveredAt: new Date().toISOString(),
|
|
331
|
+
outcome: 'no_speech',
|
|
332
|
+
chunkFiles,
|
|
333
|
+
words: 0,
|
|
334
|
+
note: 'Transcription produced no words. Audio retained until the retention clock clears it.',
|
|
335
|
+
})}\n`,
|
|
336
|
+
{ encoding: 'utf8', mode: 0o600 },
|
|
337
|
+
)
|
|
338
|
+
} catch { /* receipt is best-effort; findBySessionId remains the true guard */ }
|
|
339
|
+
}
|
|
340
|
+
|
|
306
341
|
export function markRecovered(dirPath: string, savedFilename: string): void {
|
|
307
342
|
try {
|
|
308
343
|
writeFileSync(
|
package/server/routes/meeting.ts
CHANGED
|
@@ -83,6 +83,7 @@ import {
|
|
|
83
83
|
findQuarantineDir,
|
|
84
84
|
listUnsavedCaptures,
|
|
85
85
|
markRecovered,
|
|
86
|
+
markRecoveredNoSpeech,
|
|
86
87
|
registerActiveRecovery,
|
|
87
88
|
} from '../lib/unsaved-audio-quarantine.js'
|
|
88
89
|
import {
|
|
@@ -1915,7 +1916,24 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
|
|
|
1915
1916
|
clearSessionHallucinationState(sessionId)
|
|
1916
1917
|
const transcript = cleanFinalTranscript(results.map(item => item.text).join(' '))
|
|
1917
1918
|
if (!transcript.trim()) {
|
|
1918
|
-
|
|
1919
|
+
// SILENCE IS AN OUTCOME, NOT A FAILURE.
|
|
1920
|
+
//
|
|
1921
|
+
// This used to throw, which left the capture in the unsaved list and
|
|
1922
|
+
// retried it forever -- 1,131 times for one 33-second capture on
|
|
1923
|
+
// 2026-08-25, alternating with the auto-recover path claiming success,
|
|
1924
|
+
// while the panel showed only "1 recoverable" and the error went to
|
|
1925
|
+
// stderr. The user sees a Recover button that does nothing, because it
|
|
1926
|
+
// literally cannot succeed on audio with no speech in it.
|
|
1927
|
+
//
|
|
1928
|
+
// Receipt it and let it clear. The audio stays on disk for its full
|
|
1929
|
+
// retention window, so a capture wrongly judged silent is not lost.
|
|
1930
|
+
markRecoveredNoSpeech(quarantineDir, entries.length)
|
|
1931
|
+
console.log(
|
|
1932
|
+
`[meeting/orphans] ${sessionId} held no speech `
|
|
1933
|
+
+ `(${entries.length} chunks, ${Math.round((Date.now() - startedAt) / 1000)}s) `
|
|
1934
|
+
+ '— receipted as no_speech; audio retained until retention expires',
|
|
1935
|
+
)
|
|
1936
|
+
return
|
|
1919
1937
|
}
|
|
1920
1938
|
const recoveredChunks = results.map(item => ({
|
|
1921
1939
|
text: item.text,
|