@gotcos/glasses-server 6.14.0 → 6.14.1
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,15 @@
|
|
|
1
|
+
## 6.14.1
|
|
2
|
+
|
|
3
|
+
- Keep real-time `large-v3-turbo` stable on VAD-empty audio by using
|
|
4
|
+
whisper-server compact JSON instead of the nullable `verbose_json` language
|
|
5
|
+
path that can crash native whisper.cpp.
|
|
6
|
+
- Prevent long sessions from stalling on unread native output by discarding
|
|
7
|
+
whisper-server stdout and stderr under the existing owner-safe supervisor.
|
|
8
|
+
- Isolate meeting-save full `large-v3` polish from live Metal inference by
|
|
9
|
+
running batch HQ on CPU with eight threads. Interactive HQ retains GPU speed.
|
|
10
|
+
- Reap timed-out HQ children before the queue advances, with SIGKILL escalation
|
|
11
|
+
if SIGTERM does not exit within two seconds.
|
|
12
|
+
|
|
1
13
|
## 6.14.0
|
|
2
14
|
|
|
3
15
|
- Add voice (TTS + speaker) and additive glasses routes to the public server: `tts`, `voice`, `glossary`, `handoffs`, `recovery`, `prompt-edit`, `bookmarks`. Brings server-side voice + companion utilities to public installs; COS-integration routes remain private.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.14.
|
|
4
|
-
"description": "COS Glasses
|
|
3
|
+
"version": "6.14.1",
|
|
4
|
+
"description": "COS Glasses — self-hosted AI heads-up-display server for Even G2 smart glasses, powered by your local Claude Code or Codex CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"glasses-server": "bin/cli.cjs",
|
|
@@ -172,7 +172,7 @@ async function transcribeSegments(
|
|
|
172
172
|
const combined = concatenateWavChunks(audioDir, segment.startChunkIdx, segment.endChunkIdx)
|
|
173
173
|
const enhanced = await enhanceAudio(combined)
|
|
174
174
|
const previousText = results.at(-1)?.text
|
|
175
|
-
const result = await transcribeHighQuality(enhanced, previousText?.slice(-250))
|
|
175
|
+
const result = await transcribeHighQuality(enhanced, previousText?.slice(-250), { priority: 'batch' })
|
|
176
176
|
const text = previousText ? stripOverlap(result.text, previousText) : result.text
|
|
177
177
|
const words = result.words ?? []
|
|
178
178
|
results.push({
|
|
@@ -42,9 +42,8 @@ export interface WhisperSegment {
|
|
|
42
42
|
words?: WhisperWord[]
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
interface
|
|
46
|
-
text
|
|
47
|
-
segments?: WhisperSegment[]
|
|
45
|
+
interface WhisperJsonResponse {
|
|
46
|
+
text?: unknown
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
// Resolve whisper.cpp binaries across Homebrew prefixes (Apple Silicon
|
|
@@ -144,6 +143,7 @@ let cliAvailable = false
|
|
|
144
143
|
let serverAvailable = false
|
|
145
144
|
let serverProcess: ReturnType<typeof spawn> | null = null
|
|
146
145
|
const ownedServerChildren = new Set<ChildProcess>()
|
|
146
|
+
const ownedHqChildren = new Set<ReturnType<typeof spawn>>()
|
|
147
147
|
|
|
148
148
|
// Circuit breaker: track consecutive server failures to detect hung process
|
|
149
149
|
let serverConsecutiveFailures = 0
|
|
@@ -389,7 +389,9 @@ async function startWhisperServerAttempt(preflightCompleted = false): Promise<vo
|
|
|
389
389
|
console.log('[whisper-local] Starting whisper-server...')
|
|
390
390
|
|
|
391
391
|
const child = spawn(WHISPER_SERVER, serverArgs, {
|
|
392
|
-
|
|
392
|
+
// whisper-server writes per-inference diagnostics. Unread pipes eventually
|
|
393
|
+
// fill and block the daemon, so the supervisor must not leave them buffered.
|
|
394
|
+
stdio: 'ignore',
|
|
393
395
|
detached: false, // Dies with parent
|
|
394
396
|
})
|
|
395
397
|
serverProcess = child
|
|
@@ -455,6 +457,10 @@ export function stopWhisperServer(): void {
|
|
|
455
457
|
serverAvailable = false
|
|
456
458
|
console.log('[whisper-local] whisper-server stopped')
|
|
457
459
|
}
|
|
460
|
+
for (const proc of ownedHqChildren) {
|
|
461
|
+
try { proc.kill('SIGKILL') } catch { /* already exited */ }
|
|
462
|
+
}
|
|
463
|
+
ownedHqChildren.clear()
|
|
458
464
|
}
|
|
459
465
|
|
|
460
466
|
export function isWhisperLocalAvailable(): boolean {
|
|
@@ -523,7 +529,11 @@ async function reconcileWhisperServerHealth(): Promise<boolean> {
|
|
|
523
529
|
* Falls back to turbo weights if large-v3 not on disk or COS_BATCH_LARGE_V3=0.
|
|
524
530
|
* Falls back to transcribeLocal if whisper-cli unavailable entirely.
|
|
525
531
|
*/
|
|
526
|
-
export async function transcribeHighQuality(
|
|
532
|
+
export async function transcribeHighQuality(
|
|
533
|
+
audioBuffer: Buffer,
|
|
534
|
+
context?: string,
|
|
535
|
+
opts: { priority?: 'interactive' | 'batch' } = {},
|
|
536
|
+
): Promise<{ text: string; words?: WhisperWord[] }> {
|
|
527
537
|
if (!cliAvailable) {
|
|
528
538
|
// Fall back to server (no beam search available via HTTP API)
|
|
529
539
|
return transcribeLocal(audioBuffer, context)
|
|
@@ -541,12 +551,13 @@ export async function transcribeHighQuality(audioBuffer: Buffer, context?: strin
|
|
|
541
551
|
writeFileSync(tmpWav, audioBuffer)
|
|
542
552
|
|
|
543
553
|
const text = await new Promise<string>((resolve, reject) => {
|
|
554
|
+
const isolateBatchFromLiveMetal = opts.priority === 'batch'
|
|
544
555
|
const args = [
|
|
545
556
|
'-m', modelPath,
|
|
546
557
|
'-f', tmpWav,
|
|
547
|
-
'-t', '16',
|
|
558
|
+
'-t', isolateBatchFromLiveMetal ? '8' : '16',
|
|
548
559
|
'-l', 'en',
|
|
549
|
-
'-
|
|
560
|
+
...(isolateBatchFromLiveMetal ? ['-ng'] : ['-fa']),
|
|
550
561
|
'-bs', '5', // Beam search width 5 (default disabled)
|
|
551
562
|
'-bo', '5', // Best-of-5 candidates (default 2)
|
|
552
563
|
'--no-timestamps',
|
|
@@ -562,6 +573,7 @@ export async function transcribeHighQuality(audioBuffer: Buffer, context?: strin
|
|
|
562
573
|
const proc = spawn(WHISPER_CLI, args, {
|
|
563
574
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
564
575
|
})
|
|
576
|
+
ownedHqChildren.add(proc)
|
|
565
577
|
|
|
566
578
|
let stdout = ''
|
|
567
579
|
let stderr = ''
|
|
@@ -573,13 +585,24 @@ export async function transcribeHighQuality(audioBuffer: Buffer, context?: strin
|
|
|
573
585
|
// multiplier × beam-search overhead = ~240s safety ceiling for HQ.
|
|
574
586
|
// Turbo retains the old 60s ceiling.
|
|
575
587
|
const timeoutMs = useLargeV3 ? 240_000 : 60_000
|
|
588
|
+
let timedOut = false
|
|
589
|
+
let forceKill: ReturnType<typeof setTimeout> | null = null
|
|
576
590
|
const timeout = setTimeout(() => {
|
|
577
|
-
|
|
578
|
-
|
|
591
|
+
timedOut = true
|
|
592
|
+
try { proc.kill('SIGTERM') } catch { /* already exited */ }
|
|
593
|
+
forceKill = setTimeout(() => {
|
|
594
|
+
try { proc.kill('SIGKILL') } catch { /* already exited */ }
|
|
595
|
+
}, 2_000)
|
|
579
596
|
}, timeoutMs)
|
|
580
597
|
|
|
581
598
|
proc.on('close', (code) => {
|
|
599
|
+
ownedHqChildren.delete(proc)
|
|
582
600
|
clearTimeout(timeout)
|
|
601
|
+
if (forceKill) clearTimeout(forceKill)
|
|
602
|
+
if (timedOut) {
|
|
603
|
+
reject(new Error(`whisper-cli HQ timeout (${timeoutMs / 1000}s, model=${useLargeV3 ? 'large-v3' : 'turbo'})`))
|
|
604
|
+
return
|
|
605
|
+
}
|
|
583
606
|
if (code !== 0) {
|
|
584
607
|
reject(new Error(`whisper-cli HQ exit ${code}: ${stderr.trim().slice(0, 200)}`))
|
|
585
608
|
return
|
|
@@ -588,7 +611,9 @@ export async function transcribeHighQuality(audioBuffer: Buffer, context?: strin
|
|
|
588
611
|
})
|
|
589
612
|
|
|
590
613
|
proc.on('error', (err) => {
|
|
614
|
+
ownedHqChildren.delete(proc)
|
|
591
615
|
clearTimeout(timeout)
|
|
616
|
+
if (forceKill) clearTimeout(forceKill)
|
|
592
617
|
reject(new Error(`whisper-cli HQ spawn error: ${err.message}`))
|
|
593
618
|
})
|
|
594
619
|
})
|
|
@@ -626,14 +651,17 @@ function buildPrompt(context?: string, isQuiet?: boolean): string {
|
|
|
626
651
|
|
|
627
652
|
/**
|
|
628
653
|
* Transcribe via whisper-server (persistent daemon, ~50-100ms).
|
|
629
|
-
*
|
|
654
|
+
*
|
|
655
|
+
* Compact JSON intentionally avoids whisper.cpp's verbose_json language field,
|
|
656
|
+
* which can receive a null C string after VAD returns no speech and crash the
|
|
657
|
+
* native server before an HTTP response exists.
|
|
630
658
|
*/
|
|
631
659
|
async function transcribeViaServer(audioBuffer: Buffer, context?: string, isQuiet?: boolean): Promise<{ text: string; words?: WhisperWord[] }> {
|
|
632
660
|
const formData = new FormData()
|
|
633
661
|
// Convert Buffer to Uint8Array to satisfy Blob's BlobPart type constraint
|
|
634
662
|
const blob = new Blob([new Uint8Array(audioBuffer)], { type: 'audio/wav' })
|
|
635
663
|
formData.append('file', blob, 'recording.wav')
|
|
636
|
-
formData.append('response_format', '
|
|
664
|
+
formData.append('response_format', 'json')
|
|
637
665
|
formData.append('prompt', buildPrompt(context, isQuiet))
|
|
638
666
|
// Anti-hallucination handled by client-side filter + context filtering.
|
|
639
667
|
// Whisper-level entropy/logprob thresholds were too aggressive — silently dropped
|
|
@@ -650,20 +678,11 @@ async function transcribeViaServer(audioBuffer: Buffer, context?: string, isQuie
|
|
|
650
678
|
throw new Error(`whisper-server ${response.status}: ${await response.text()}`)
|
|
651
679
|
}
|
|
652
680
|
|
|
653
|
-
const result = await response.json() as
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
// Extract word-level timestamps from DTW-aligned segments (defensive — may be absent)
|
|
657
|
-
let words: WhisperWord[] | undefined
|
|
658
|
-
if (result.segments && result.segments.length > 0) {
|
|
659
|
-
const extracted = result.segments.flatMap(s => {
|
|
660
|
-
if (!s.words || !Array.isArray(s.words)) return []
|
|
661
|
-
return s.words.filter(w => typeof w.start === 'number' && typeof w.end === 'number')
|
|
662
|
-
})
|
|
663
|
-
if (extracted.length > 0) words = extracted
|
|
681
|
+
const result = await response.json() as WhisperJsonResponse
|
|
682
|
+
if (typeof result.text !== 'string') {
|
|
683
|
+
throw new Error('whisper-server returned invalid compact JSON: missing string text')
|
|
664
684
|
}
|
|
665
|
-
|
|
666
|
-
return { text, words }
|
|
685
|
+
return { text: result.text.trim() }
|
|
667
686
|
}
|
|
668
687
|
|
|
669
688
|
/**
|