@gotcos/glasses-server 6.1.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/.cos-profile.example.json +7 -0
- package/.env.example +44 -0
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/bin/cli.cjs +203 -0
- package/package.json +53 -0
- package/server/env.ts +26 -0
- package/server/index.ts +211 -0
- package/server/lib/archive-budget.ts +65 -0
- package/server/lib/archive.ts +414 -0
- package/server/lib/atomic-fs.ts +50 -0
- package/server/lib/audio-enhance.ts +87 -0
- package/server/lib/claude-bridge.ts +682 -0
- package/server/lib/claude-circuit.ts +52 -0
- package/server/lib/claude-run-ledger.ts +279 -0
- package/server/lib/codex-bridge.ts +476 -0
- package/server/lib/codex-engine-sessions.ts +140 -0
- package/server/lib/codex-run-ledger.ts +298 -0
- package/server/lib/context-builder.ts +210 -0
- package/server/lib/conversation.ts +587 -0
- package/server/lib/data-dir.ts +20 -0
- package/server/lib/display-bus.ts +21 -0
- package/server/lib/display-format.ts +23 -0
- package/server/lib/fuzzy-correct.ts +286 -0
- package/server/lib/hallucination-filter.ts +469 -0
- package/server/lib/local-day.ts +13 -0
- package/server/lib/model-router.ts +38 -0
- package/server/lib/openai-key.ts +155 -0
- package/server/lib/openai-whisper-budget.ts +170 -0
- package/server/lib/profile.ts +94 -0
- package/server/lib/python-bridge.ts +84 -0
- package/server/lib/response-cache.ts +138 -0
- package/server/lib/session-cache-writer.ts +266 -0
- package/server/lib/session-log.ts +162 -0
- package/server/lib/speaker-embeddings.ts +578 -0
- package/server/lib/telegram-notify.ts +85 -0
- package/server/lib/token-audit.ts +50 -0
- package/server/lib/transcribe-audio.ts +187 -0
- package/server/lib/utils.ts +5 -0
- package/server/lib/vad-silero.ts +179 -0
- package/server/lib/whisper-local.ts +697 -0
- package/server/routes/diag.ts +115 -0
- package/server/routes/display.ts +65 -0
- package/server/routes/health.ts +128 -0
- package/server/routes/openai-compat.ts +446 -0
- package/server/routes/openai-key.ts +121 -0
- package/server/routes/query.ts +121 -0
- package/server/routes/transcribe-stream.ts +1090 -0
- package/server/routes/transcribe.ts +55 -0
- package/shared/model-preference.ts +81 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Atomic file writes — `writeFileSync` is NOT atomic. Process kill / power
|
|
2
|
+
// loss / disk-full / iCloud-sync-mid-write can leave the file truncated or
|
|
3
|
+
// corrupt. POSIX `rename` IS atomic (same filesystem), so writing to a `.tmp`
|
|
4
|
+
// then renaming guarantees readers always see the old full file or the new
|
|
5
|
+
// full file, never a torn middle state.
|
|
6
|
+
//
|
|
7
|
+
// Use for sessions.json, archive/*.json, and any other durable JSON we can't
|
|
8
|
+
// afford to lose.
|
|
9
|
+
|
|
10
|
+
import { writeFileSync, renameSync, existsSync, readFileSync } from 'node:fs'
|
|
11
|
+
|
|
12
|
+
export function atomicWriteFileSync(path: string, data: string | Buffer): void {
|
|
13
|
+
const tmp = `${path}.tmp`
|
|
14
|
+
writeFileSync(tmp, data)
|
|
15
|
+
renameSync(tmp, path)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read + JSON.parse with explicit missing-vs-corrupt distinction.
|
|
20
|
+
* - File missing: returns { status: 'missing' } — caller starts fresh silently
|
|
21
|
+
* - Parse succeeds: returns { status: 'ok', data }
|
|
22
|
+
* - Parse fails: quarantines the corrupt file (renames to `.corrupt-<ts>`),
|
|
23
|
+
* returns { status: 'corrupt', quarantinedAs } so the caller can log loudly
|
|
24
|
+
*/
|
|
25
|
+
export type LoadResult<T> =
|
|
26
|
+
| { status: 'missing' }
|
|
27
|
+
| { status: 'ok'; data: T }
|
|
28
|
+
| { status: 'corrupt'; quarantinedAs: string; error: unknown }
|
|
29
|
+
|
|
30
|
+
export function loadJsonOrQuarantine<T>(path: string): LoadResult<T> {
|
|
31
|
+
if (!existsSync(path)) return { status: 'missing' }
|
|
32
|
+
let raw: string
|
|
33
|
+
try {
|
|
34
|
+
raw = readFileSync(path, 'utf-8')
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return { status: 'missing' } // permission / transient; caller decides
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const data = JSON.parse(raw) as T
|
|
40
|
+
return { status: 'ok', data }
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const quarantinedAs = `${path}.corrupt-${Date.now()}`
|
|
43
|
+
try {
|
|
44
|
+
renameSync(path, quarantinedAs)
|
|
45
|
+
} catch {
|
|
46
|
+
/* if rename fails we still want to surface the error */
|
|
47
|
+
}
|
|
48
|
+
return { status: 'corrupt', quarantinedAs, error: err }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Audio enhancement via ffmpeg — noise reduction + loudness normalization.
|
|
2
|
+
// Extracted so both batch (post-meeting) and one-shot (message query HQ) paths
|
|
3
|
+
// can use the same filter chain.
|
|
4
|
+
//
|
|
5
|
+
// Filter chain:
|
|
6
|
+
// highpass=f=80 — kills low-freq rumble (HVAC, body noise, table thumps)
|
|
7
|
+
// afftdn=nt=w — FFT-based denoiser (white noise, fan hum)
|
|
8
|
+
// loudnorm — EBU R128 loudness normalization (fixes quiet speakers)
|
|
9
|
+
//
|
|
10
|
+
// Graceful: returns the original buffer if ffmpeg is missing, fails, or times out.
|
|
11
|
+
// Callers should never crash a user request because enhancement couldn't run.
|
|
12
|
+
|
|
13
|
+
import { spawn } from 'node:child_process'
|
|
14
|
+
import { writeFileSync, readFileSync, unlinkSync, existsSync } from 'node:fs'
|
|
15
|
+
import { join } from 'node:path'
|
|
16
|
+
import { randomUUID } from 'node:crypto'
|
|
17
|
+
|
|
18
|
+
const FFMPEG_TIMEOUT_MS = 30_000
|
|
19
|
+
const FILTER_CHAIN = 'highpass=f=80,afftdn=nt=w,loudnorm=I=-16:LRA=11:TP=-1.5'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Enhance raw audio (WAV/webm/etc) and return a 16kHz mono WAV buffer suitable
|
|
23
|
+
* for whisper-cli or whisper-server. Input format is detected by ffmpeg — no
|
|
24
|
+
* need to pre-convert.
|
|
25
|
+
*
|
|
26
|
+
* Returns the ORIGINAL buffer unchanged on any failure. Logs the reason.
|
|
27
|
+
*/
|
|
28
|
+
export async function enhanceAudio(audioBuffer: Buffer): Promise<Buffer> {
|
|
29
|
+
const id = randomUUID().slice(0, 8)
|
|
30
|
+
const inputPath = join('/tmp', `cos-enhance-in-${id}`)
|
|
31
|
+
const outputPath = join('/tmp', `cos-enhance-out-${id}.wav`)
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
writeFileSync(inputPath, audioBuffer)
|
|
35
|
+
|
|
36
|
+
const enhanced = await new Promise<Buffer>((resolve, reject) => {
|
|
37
|
+
const proc = spawn('ffmpeg', [
|
|
38
|
+
'-i', inputPath,
|
|
39
|
+
'-af', FILTER_CHAIN,
|
|
40
|
+
'-ar', '16000',
|
|
41
|
+
'-ac', '1',
|
|
42
|
+
'-f', 'wav',
|
|
43
|
+
'-y',
|
|
44
|
+
outputPath,
|
|
45
|
+
], { stdio: ['ignore', 'ignore', 'pipe'] })
|
|
46
|
+
|
|
47
|
+
let stderr = ''
|
|
48
|
+
proc.stderr?.on('data', (d: Buffer) => { stderr += d.toString() })
|
|
49
|
+
|
|
50
|
+
const timeout = setTimeout(() => {
|
|
51
|
+
proc.kill('SIGTERM')
|
|
52
|
+
reject(new Error(`ffmpeg timeout (${FFMPEG_TIMEOUT_MS / 1000}s)`))
|
|
53
|
+
}, FFMPEG_TIMEOUT_MS)
|
|
54
|
+
|
|
55
|
+
proc.on('close', (code) => {
|
|
56
|
+
clearTimeout(timeout)
|
|
57
|
+
if (code !== 0) {
|
|
58
|
+
reject(new Error(`ffmpeg exit ${code}: ${stderr.trim().slice(-200)}`))
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
if (!existsSync(outputPath)) {
|
|
62
|
+
reject(new Error('ffmpeg produced no output file'))
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
resolve(readFileSync(outputPath))
|
|
67
|
+
} catch (readErr: unknown) {
|
|
68
|
+
reject(readErr instanceof Error ? readErr : new Error(String(readErr)))
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
proc.on('error', (err) => {
|
|
73
|
+
clearTimeout(timeout)
|
|
74
|
+
reject(err)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
return enhanced
|
|
79
|
+
} catch (err: unknown) {
|
|
80
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
81
|
+
console.warn(`[audio-enhance] ffmpeg failed, returning original buffer: ${msg}`)
|
|
82
|
+
return audioBuffer
|
|
83
|
+
} finally {
|
|
84
|
+
try { unlinkSync(inputPath) } catch { /* ignore */ }
|
|
85
|
+
try { unlinkSync(outputPath) } catch { /* ignore */ }
|
|
86
|
+
}
|
|
87
|
+
}
|