@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,286 @@
1
+ // Deterministic fuzzy correction for transcribed text.
2
+ // Matches words against a canonical list of proper nouns (enrolled speakers +
3
+ // profile vocabulary) and replaces close misses using length-scaled Levenshtein.
4
+ //
5
+ // Used by:
6
+ // - /api/transcribe (short-form dictation / messaging)
7
+ // - /api/meeting/save (batch post-processing, Phase 3.5 — via meeting.ts)
8
+ //
9
+ // Zero LLM cost, ~5ms on typical dictation length.
10
+ //
11
+ // SAFETY RULES (revised after QA 2026-04-10):
12
+ // 1. Candidates: any 5+ char word (case-insensitive). This supports lowercase
13
+ // dictation output from Whisper (short /transcribe path) AND mixed-case
14
+ // proper nouns like product names and brands.
15
+ // 2. Stop-word list blocks common English (prevents "right"→"Wright",
16
+ // "through"→"Brough"). Must be comprehensive — this is the primary defense.
17
+ // 3. Tight distance thresholds (1 for 5-8 char, 2 for 9-11, 3 for 12+)
18
+ // 4. Length difference ≤ 2 max (prevents long→short collapses)
19
+ // 5. Target must also be a proper noun (starts uppercase in canonical list)
20
+ // 6. Canonical min length 4 (allows short 4-character names as targets)
21
+
22
+ import { getOwnerSpeakerLabel } from './profile.js'
23
+
24
+ /** Compute Levenshtein edit distance between two lowercase strings. */
25
+ export function levenshtein(a: string, b: string): number {
26
+ if (a === b) return 0
27
+ if (a.length === 0) return b.length
28
+ if (b.length === 0) return a.length
29
+ const m = a.length, n = b.length
30
+ let prev = new Array(n + 1).fill(0)
31
+ let curr = new Array(n + 1).fill(0)
32
+ for (let j = 0; j <= n; j++) prev[j] = j
33
+ for (let i = 1; i <= m; i++) {
34
+ curr[0] = i
35
+ for (let j = 1; j <= n; j++) {
36
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1
37
+ curr[j] = Math.min(
38
+ prev[j] + 1, // deletion
39
+ curr[j - 1] + 1, // insertion
40
+ prev[j - 1] + cost, // substitution
41
+ )
42
+ }
43
+ ;[prev, curr] = [curr, prev]
44
+ }
45
+ return prev[n]
46
+ }
47
+
48
+ // Common English words that must NEVER be fuzzy-replaced with proper nouns.
49
+ // Deduplicated, alphabetically sorted for maintainability.
50
+ // Covers: conversational fillers, verbs, nouns, function words, and common
51
+ // first names (which the LLM correction pass can disambiguate contextually).
52
+ // Real false positives this prevents:
53
+ // "Alright"→"Wright", "through"→"Brough", "details"→"Retail",
54
+ // "Shelley"→"Kelley", "right"→"Wright", "Christian"→"Cristian"
55
+ const COMMON_ENGLISH_WORDS = new Set<string>([
56
+ // A
57
+ 'about', 'above', 'absolutely', 'actually', 'admit', 'adopt', 'adult',
58
+ 'after', 'again', 'against', 'agent', 'agree', 'ahead', 'album', 'alert',
59
+ 'alike', 'alive', 'allow', 'allright', 'alone', 'along', 'aloud', 'alright',
60
+ 'also', 'alter', 'although', 'always', 'among', 'amount', 'anger', 'angle',
61
+ 'angry', 'another', 'answer', 'answers', 'anyone', 'anything', 'apart',
62
+ 'around', 'array', 'arrive', 'arrow', 'aside', 'asked', 'asking', 'aspect',
63
+ // B
64
+ 'basic', 'basically', 'beach', 'because', 'become', 'been', 'before', 'began',
65
+ 'begin', 'behind', 'being', 'believe', 'below', 'bench', 'beside', 'better',
66
+ 'between', 'beyond', 'birth', 'black', 'blame', 'blank', 'blast', 'blind',
67
+ 'block', 'blood', 'board', 'booth', 'boost', 'bound', 'brain', 'brand',
68
+ 'bread', 'break', 'breed', 'brief', 'bring', 'broad', 'broke', 'broken',
69
+ 'brother', 'brought', 'brown', 'brush', 'built', 'bunch', 'business', 'burst',
70
+ // C
71
+ 'calling', 'cannot', 'catch', 'caught', 'change', 'changed', 'chances',
72
+ 'cheap', 'check', 'chest', 'chief', 'child', 'children', 'choice', 'choose',
73
+ 'chose', 'christian', 'civil', 'claim', 'class', 'clean', 'clear', 'climb',
74
+ 'clock', 'close', 'cloud', 'coach', 'coast', 'coming', 'company', 'could',
75
+ 'count', 'course', 'court', 'cover', 'craft', 'crash', 'crazy', 'cream',
76
+ 'create', 'created', 'crime', 'cross', 'crowd', 'crown', 'crude', 'curve',
77
+ 'customer', 'customers', 'cycle',
78
+ // D
79
+ 'daily', 'dance', 'dated', 'dealt', 'death', 'debut', 'definitely', 'delay',
80
+ 'depth', 'despite', 'detail', 'details', 'doing', 'double', 'doubt', 'dozen',
81
+ 'draft', 'drawn', 'dream', 'dress', 'drink', 'drive', 'driven', 'drove',
82
+ 'during', 'dying',
83
+ // E
84
+ 'early', 'earth', 'eight', 'either', 'email', 'emails', 'empty', 'enemy',
85
+ 'enjoy', 'enough', 'entry', 'equal', 'error', 'especially', 'even', 'evening',
86
+ 'event', 'every', 'everyone', 'everything', 'exactly', 'example', 'examples',
87
+ 'except', 'extra',
88
+ // F
89
+ 'faith', 'fancy', 'fault', 'feeling', 'fewer', 'field', 'fifth', 'final',
90
+ 'finally', 'finish', 'finished', 'first', 'floor', 'focus', 'follow',
91
+ 'followed', 'follows', 'following', 'force', 'forth', 'found', 'frame',
92
+ 'fresh', 'friend', 'front',
93
+ // G
94
+ 'getting', 'given', 'giving', 'going', 'gonna', 'gotta', 'grade', 'grant',
95
+ 'great', 'green', 'group', 'groups', 'guess', 'guest', 'guide',
96
+ // H
97
+ 'happened', 'happy', 'hard', 'having', 'head', 'heard', 'heavy', 'helping',
98
+ 'helped', 'honestly', 'hope', 'hour', 'house', 'however',
99
+ // I
100
+ 'ideal', 'image', 'important', 'inner', 'inside', 'instead', 'interest',
101
+ 'issue', 'itself',
102
+ // J
103
+ 'joined', 'judge',
104
+ // K
105
+ 'keep', 'kept', 'kind', 'knew', 'know', 'known', 'knows',
106
+ // L
107
+ 'large', 'later', 'latest', 'laugh', 'lead', 'learn', 'least', 'leave',
108
+ 'left', 'less', 'level', 'levels', 'letter', 'light', 'line', 'listen',
109
+ 'listened', 'literally', 'little', 'local', 'long', 'longer', 'look',
110
+ 'looked', 'looking', 'lower',
111
+ // M
112
+ 'making', 'many', 'matter', 'matters', 'maybe', 'mean', 'meaning', 'media',
113
+ 'meeting', 'meetings', 'member', 'message', 'messages', 'might', 'mind',
114
+ 'minor', 'model', 'money', 'month', 'months', 'moral', 'morning', 'mother',
115
+ 'mouse', 'moved', 'moving', 'much', 'music', 'must', 'myself',
116
+ // N
117
+ 'name', 'names', 'nature', 'near', 'need', 'needed', 'never', 'news', 'next',
118
+ 'night', 'nobody', 'north', 'note', 'noted', 'notes', 'nothing', 'notice',
119
+ 'number', 'numbers',
120
+ // O
121
+ 'obvious', 'obviously', 'offer', 'office', 'offices', 'often', 'okay',
122
+ 'once', 'only', 'onto', 'open', 'order', 'orders', 'other', 'outer',
123
+ 'outside', 'over',
124
+ // P
125
+ 'paper', 'party', 'people', 'perhaps', 'person', 'phone', 'phones', 'photo',
126
+ 'piece', 'place', 'plan', 'plant', 'plate', 'point', 'points', 'poor',
127
+ 'possible', 'power', 'press', 'pretty', 'price', 'probably', 'problem',
128
+ 'problems', 'product', 'products', 'program', 'project', 'projects', 'proud',
129
+ 'public',
130
+ // Q
131
+ 'queen', 'question', 'questions', 'quick', 'quiet', 'quite',
132
+ // R
133
+ 'rather', 'reach', 'reached', 'ready', 'really', 'reason', 'reasons',
134
+ 'recent', 'recently', 'report', 'reports', 'respect', 'result', 'results',
135
+ 'retail', 'return', 'right', 'rights', 'rough', 'round', 'rural',
136
+ // S
137
+ 'sales', 'same', 'school', 'seeing', 'seemed', 'seems', 'sense', 'sent',
138
+ 'seven', 'several', 'shall', 'share', 'sharp', 'shelley', 'short', 'should',
139
+ 'showing', 'shown', 'sides', 'since', 'small', 'smile', 'social', 'some',
140
+ 'someone', 'something', 'sometimes', 'soon', 'sorry', 'sound', 'south',
141
+ 'space', 'speak', 'spoke', 'sports', 'staff', 'stage', 'stand', 'start',
142
+ 'started', 'state', 'still', 'stood', 'stop', 'stopping', 'story', 'stuff',
143
+ 'style', 'subject', 'success', 'such', 'suggest', 'support', 'sure',
144
+ 'system', 'systems',
145
+ // T
146
+ 'table', 'taking', 'talking', 'taught', 'teach', 'team', 'teams', 'telling',
147
+ 'tells', 'terms', 'thanks', 'thank', 'their', 'them', 'then', 'there',
148
+ 'these', 'thing', 'things', 'think', 'third', 'this', 'those', 'though',
149
+ 'thought', 'three', 'through', 'throughout', 'today', 'together', 'told',
150
+ 'tomorrow', 'tonight', 'took', 'total', 'touch', 'toward', 'towards', 'town',
151
+ 'trade', 'train', 'tried', 'trouble', 'truly', 'trust', 'truth', 'trying',
152
+ 'turn', 'turned', 'turning', 'twelve', 'type',
153
+ // U
154
+ 'under', 'underneath', 'understand', 'unique', 'until', 'upon', 'usually',
155
+ // V
156
+ 'value', 'various', 'very', 'visit', 'voice',
157
+ // W
158
+ 'wait', 'wanna', 'want', 'wanted', 'watch', 'water', 'ways', 'week', 'weeks',
159
+ 'weekend', 'well', 'went', 'were', 'what', 'when', 'where', 'whether',
160
+ 'which', 'while', 'white', 'whole', 'whose', 'wife', 'will', 'wind', 'wish',
161
+ 'with', 'within', 'without', 'women', 'word', 'words', 'work', 'worked',
162
+ 'working', 'world', 'worry', 'worse', 'worst', 'would', 'wrong',
163
+ // Y
164
+ 'yeah', 'year', 'years', 'yesterday', 'young', 'your', 'yours', 'yourself',
165
+
166
+ // ── Common first names ──
167
+ // Whisper frequently capitalizes these at any position. Leave them alone —
168
+ // LLM correction pass handles context-aware disambiguation vs enrolled speakers.
169
+ 'aaron', 'albert', 'alexander', 'alice', 'amanda', 'amber', 'amy', 'andrea',
170
+ 'angela', 'ann', 'anne', 'anthony', 'arthur', 'ashley', 'barbara', 'becky',
171
+ 'benjamin', 'betty', 'brandon', 'brenda', 'brian', 'bruce', 'carl', 'carlos',
172
+ 'carol', 'carolyn', 'catherine', 'charles', 'cheryl', 'christina', 'christine',
173
+ 'christopher', 'cynthia', 'daniel', 'david', 'dawn', 'deborah', 'denise',
174
+ 'dennis', 'diana', 'diane', 'donna', 'douglas', 'dylan', 'edward', 'elizabeth',
175
+ 'emily', 'emma', 'eric', 'eugene', 'evelyn', 'frank', 'gary', 'george',
176
+ 'gloria', 'grace', 'gregory', 'hannah', 'harold', 'heather', 'helen', 'henry',
177
+ 'howard', 'jack', 'jackson', 'jacob', 'jacqueline', 'james', 'jane', 'janet',
178
+ 'janice', 'jason', 'jeffrey', 'jennifer', 'jerry', 'jessica', 'joan', 'john',
179
+ 'johnny', 'jonathan', 'jordan', 'jose', 'joseph', 'joshua', 'joyce', 'judith',
180
+ 'julia', 'julie', 'justin', 'karen', 'katherine', 'kathleen', 'kathryn',
181
+ 'keith', 'kelly', 'kenneth', 'kevin', 'kimberly', 'larry', 'laura', 'lauren',
182
+ 'lawrence', 'linda', 'lisa', 'lori', 'louis', 'margaret', 'maria', 'marie',
183
+ 'mark', 'martha', 'martin', 'mary', 'matthew', 'megan', 'melissa', 'michael',
184
+ 'michelle', 'monica', 'nancy', 'natalie', 'nicholas', 'nicole', 'olivia',
185
+ 'pamela', 'patricia', 'patrick', 'paul', 'peter', 'philip', 'rachel', 'ralph',
186
+ 'randy', 'raymond', 'rebecca', 'richard', 'robert', 'roger', 'ronald', 'rose',
187
+ 'roy', 'ruby', 'russell', 'ruth', 'ryan', 'samantha', 'samuel', 'sandra',
188
+ 'sara', 'sarah', 'scott', 'sean', 'shannon', 'sharon', 'shirley', 'stephanie',
189
+ 'stephen', 'steven', 'susan', 'teresa', 'theresa', 'thomas', 'timothy',
190
+ 'tina', 'tony', 'tyler', 'victoria', 'virginia', 'walter', 'wayne', 'william',
191
+ 'willie', 'zachary',
192
+ ])
193
+
194
+ /**
195
+ * Fuzzy-match transcript words against a canonical list of proper nouns.
196
+ *
197
+ * RULES (all must pass):
198
+ * - Candidate word length ≥ 5 chars (any case — supports lowercase dictation
199
+ * AND mixed-case names like GitHub/OpenAI)
200
+ * - Word must NOT be in COMMON_ENGLISH_WORDS stop-list (primary defense)
201
+ * - Target must start with uppercase (canonical proper noun)
202
+ * - Target token length ≥ 4 chars (allows short names like Anna, Ryan, Luke as targets)
203
+ * - Length difference ≤ 2 chars
204
+ * - Distance within length-scaled threshold:
205
+ * 5-8 chars → 1
206
+ * 9-11 chars → 2
207
+ * 12+ chars → 3
208
+ *
209
+ * Preserves the original word's first-letter case: capitalizes the replacement
210
+ * when the source was capitalized, otherwise lowercases it. This avoids
211
+ * creating sentence-middle capitalizations where none existed.
212
+ */
213
+ export function applyFuzzyCorrections(
214
+ text: string,
215
+ targets: string[],
216
+ ): { text: string; replacements: number } {
217
+ if (targets.length === 0 || !text) return { text, replacements: 0 }
218
+
219
+ // Keep only targets that look like proper nouns.
220
+ // Drop speaker tokens and entries that don't start with uppercase.
221
+ const owner = getOwnerSpeakerLabel()
222
+ const canonical = Array.from(new Set(
223
+ targets
224
+ .filter(t => {
225
+ if (!t || t.length < 4) return false
226
+ if (t === 'Ext' || t === 'Unknown' || t === owner) return false
227
+ return /^[A-Z]/.test(t.trim())
228
+ })
229
+ .map(t => t.trim()),
230
+ ))
231
+ if (canonical.length === 0) return { text, replacements: 0 }
232
+
233
+ const targetLowerSet = new Set(canonical.map(t => t.toLowerCase()))
234
+ let replacements = 0
235
+ const fuzzyLog: Array<{ from: string; to: string }> = []
236
+
237
+ // Match any 5+ char word (case-insensitive). Stop-list + strict distance
238
+ // thresholds are the primary defenses against false positives.
239
+ const result = text.replace(/\b[A-Za-z][A-Za-z'-]{4,}\b/g, (word) => {
240
+ const lower = word.toLowerCase()
241
+
242
+ // Skip common English words (primary false-positive defense)
243
+ if (COMMON_ENGLISH_WORDS.has(lower)) return word
244
+
245
+ // Skip exact matches (already correct)
246
+ if (targetLowerSet.has(lower)) return word
247
+
248
+ // Length-scaled distance threshold
249
+ const len = word.length
250
+ const maxDist = len <= 8 ? 1 : len <= 11 ? 2 : 3
251
+
252
+ let bestTarget: string | null = null
253
+ let bestDist = maxDist + 1
254
+
255
+ for (const target of canonical) {
256
+ // Split multi-word targets — match on individual tokens
257
+ const tokens = target.split(/\s+/)
258
+ for (const token of tokens) {
259
+ if (token.length < 4) continue
260
+ // Length difference must be small to prevent collapses
261
+ if (Math.abs(token.length - len) > 2) continue
262
+ const dist = levenshtein(lower, token.toLowerCase())
263
+ if (dist > 0 && dist < bestDist && dist <= maxDist) {
264
+ bestDist = dist
265
+ bestTarget = token
266
+ }
267
+ }
268
+ }
269
+
270
+ if (bestTarget) {
271
+ replacements++
272
+ fuzzyLog.push({ from: word, to: bestTarget })
273
+ // Preserve original capitalization pattern
274
+ const wasCapitalized = /^[A-Z]/.test(word)
275
+ return wasCapitalized ? bestTarget : bestTarget.toLowerCase()
276
+ }
277
+ return word
278
+ })
279
+
280
+ if (fuzzyLog.length > 0) {
281
+ const preview = fuzzyLog.slice(0, 5).map(x => `${x.from}→${x.to}`).join(', ')
282
+ console.log(`[fuzzy-correct] Applied ${replacements} correction(s): ${preview}${fuzzyLog.length > 5 ? '...' : ''}`)
283
+ }
284
+
285
+ return { text: result, replacements }
286
+ }