@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.
Files changed (51) hide show
  1. package/.cos-profile.example.json +7 -0
  2. package/.env.example +44 -0
  3. package/CHANGELOG.md +25 -0
  4. package/LICENSE +21 -0
  5. package/README.md +78 -0
  6. package/bin/cli.cjs +203 -0
  7. package/package.json +53 -0
  8. package/server/env.ts +26 -0
  9. package/server/index.ts +211 -0
  10. package/server/lib/archive-budget.ts +65 -0
  11. package/server/lib/archive.ts +414 -0
  12. package/server/lib/atomic-fs.ts +50 -0
  13. package/server/lib/audio-enhance.ts +87 -0
  14. package/server/lib/claude-bridge.ts +682 -0
  15. package/server/lib/claude-circuit.ts +52 -0
  16. package/server/lib/claude-run-ledger.ts +279 -0
  17. package/server/lib/codex-bridge.ts +476 -0
  18. package/server/lib/codex-engine-sessions.ts +140 -0
  19. package/server/lib/codex-run-ledger.ts +298 -0
  20. package/server/lib/context-builder.ts +210 -0
  21. package/server/lib/conversation.ts +587 -0
  22. package/server/lib/data-dir.ts +20 -0
  23. package/server/lib/display-bus.ts +21 -0
  24. package/server/lib/display-format.ts +23 -0
  25. package/server/lib/fuzzy-correct.ts +286 -0
  26. package/server/lib/hallucination-filter.ts +469 -0
  27. package/server/lib/local-day.ts +13 -0
  28. package/server/lib/model-router.ts +38 -0
  29. package/server/lib/openai-key.ts +155 -0
  30. package/server/lib/openai-whisper-budget.ts +170 -0
  31. package/server/lib/profile.ts +94 -0
  32. package/server/lib/python-bridge.ts +84 -0
  33. package/server/lib/response-cache.ts +138 -0
  34. package/server/lib/session-cache-writer.ts +266 -0
  35. package/server/lib/session-log.ts +162 -0
  36. package/server/lib/speaker-embeddings.ts +578 -0
  37. package/server/lib/telegram-notify.ts +85 -0
  38. package/server/lib/token-audit.ts +50 -0
  39. package/server/lib/transcribe-audio.ts +187 -0
  40. package/server/lib/utils.ts +5 -0
  41. package/server/lib/vad-silero.ts +179 -0
  42. package/server/lib/whisper-local.ts +697 -0
  43. package/server/routes/diag.ts +115 -0
  44. package/server/routes/display.ts +65 -0
  45. package/server/routes/health.ts +128 -0
  46. package/server/routes/openai-compat.ts +446 -0
  47. package/server/routes/openai-key.ts +121 -0
  48. package/server/routes/query.ts +121 -0
  49. package/server/routes/transcribe-stream.ts +1090 -0
  50. package/server/routes/transcribe.ts +55 -0
  51. 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
+ }