@gotcos/glasses-server 6.79.0 → 6.79.2

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.
@@ -2,8 +2,7 @@ import { isDictationCleanModel, type DictationCleanModel } from '../lib/dictatio
2
2
  import { Router } from 'express'
3
3
  import { createHash } from 'node:crypto'
4
4
  import type { Response } from 'express'
5
- import { existsSync, mkdirSync, readFileSync } from 'node:fs'
6
- import { resolve, dirname } from 'node:path'
5
+ import { resolve } from 'node:path'
7
6
  import {
8
7
  createPromptDraft,
9
8
  loadPromptDraftMeta,
@@ -43,11 +42,16 @@ import { transcribeWhisperPreview } from '../lib/whisper-preview.js'
43
42
  import { currentDictationPlan, runParakeetDictation } from '../lib/dictation-stt.js'
44
43
  import { dictationSlotForMode, legacyDictationKey, type DictationPlan } from '../lib/stt-settings.js'
45
44
  import { autoCleanDictation, AUTOCLEAN_MAX_CHARS } from '../lib/dictation-clean.js'
45
+ import { fillerPassEnabled, stripDisfluencies } from '../lib/dictation-fillers.js'
46
+ import { polishBudgetExhausted, recordPolishCall, type PolishBucket } from '../lib/dictation-polish-budget.js'
47
+ import { polishNeed, skipCleanEnabled } from '../lib/dictation-polish-need.js'
48
+ import {
49
+ discardPolishPrewarm, isPrewarmModel, prewarmEnabled, PrewarmUnavailableError, runWarmPolish,
50
+ startPolishPrewarm, takePolishPrewarm,
51
+ } from '../lib/dictation-prewarm.js'
46
52
  import { getVocabulary } from '../lib/profile.js'
47
53
  import { createBreaker } from '../lib/claude-circuit.js'
48
54
  import { logTokenAudit } from '../lib/token-audit.js'
49
- import { atomicWriteFileSync } from '../lib/atomic-fs.js'
50
- import { dataPath } from '../lib/data-dir.js'
51
55
  import { emitDisplay } from '../lib/display-bus.js'
52
56
  import {
53
57
  acquireMaintenanceWork,
@@ -83,26 +87,12 @@ const autoCleanBreakers = {
83
87
  'luna-5.6-fast': createBreaker({ label: 'dictation-autoclean-luna' }),
84
88
  haiku: createBreaker({ label: 'dictation-autoclean-haiku' }),
85
89
  }
86
- const autoCleanCountFile = () => process.env.COS_DICTATION_AUTOCLEAN_COUNT_FILE || dataPath('.dictation_autoclean_count.json')
87
90
  const autoCleanDefaultEnabled = () => ['1', 'true', 'on'].includes((process.env.COS_DICTATION_AUTOCLEAN ?? '').toLowerCase())
88
- const autoCleanDailyCap = () => {
89
- const value = Number.parseInt(process.env.COS_DICTATION_AUTOCLEAN_MAX_PER_DAY || '200', 10)
90
- return Number.isFinite(value) && value > 0 ? value : 200
91
- }
92
- function autoCleanCountToday(): number {
93
- try {
94
- const raw = JSON.parse(readFileSync(autoCleanCountFile(), 'utf-8'))
95
- if (raw?.date === new Date().toISOString().slice(0, 10) && Number.isFinite(raw?.count)) return raw.count
96
- } catch {}
97
- return 0
98
- }
99
- function recordAutoCleanCall(): void {
100
- try {
101
- const file = autoCleanCountFile()
102
- if (!existsSync(dirname(file))) mkdirSync(dirname(file), { recursive: true })
103
- atomicWriteFileSync(file, JSON.stringify({ date: new Date().toISOString().slice(0, 10), count: autoCleanCountToday() + 1 }), { mode: 0o600 })
104
- } catch {}
105
- }
91
+
92
+ /** 6.79.1, additive on finalize answers: why the polish model did not run (or did not finish). */
93
+ export type PolishSkipped = 'disabled' | 'too_long' | 'breaker_open' | 'daily_cap' | 'failed' | 'clean'
94
+ /** 6.79.2, additive when finalize was given a prewarmToken: what became of that warm process. */
95
+ export type PrewarmOutcome = 'used' | 'missed' | 'discarded'
106
96
 
107
97
  interface AutoCleanRequest { enabled?: boolean; model?: DictationCleanModel }
108
98
  function routeAutoClean(req: { body?: any; query?: any }): AutoCleanRequest {
@@ -114,7 +104,22 @@ function routeAutoClean(req: { body?: any; query?: any }): AutoCleanRequest {
114
104
  return { enabled, model: isDictationCleanModel(rawModel) ? rawModel : undefined }
115
105
  }
116
106
 
117
- async function cleanOutboundDictation(text: string, opts: AutoCleanRequest & { signal?: AbortSignal }): Promise<string> {
107
+ async function cleanOutboundDictation(
108
+ text: string,
109
+ opts: AutoCleanRequest & { bucket: PolishBucket; signal?: AbortSignal; prewarmToken?: string },
110
+ ): Promise<{ text: string; polishSkipped?: PolishSkipped; prewarm?: PrewarmOutcome }> {
111
+ const result = await polishOutboundDictation(text, opts)
112
+ // A warm process the model call did not take must not sit until its TTL.
113
+ if (opts.prewarmToken && !result.prewarm) {
114
+ return { ...result, prewarm: discardPolishPrewarm(opts.prewarmToken) ? 'discarded' : 'missed' }
115
+ }
116
+ return result
117
+ }
118
+
119
+ async function polishOutboundDictation(
120
+ text: string,
121
+ opts: AutoCleanRequest & { bucket: PolishBucket; signal?: AbortSignal; prewarmToken?: string },
122
+ ): Promise<{ text: string; polishSkipped?: PolishSkipped; prewarm?: PrewarmOutcome }> {
118
123
  let cleaned = applyNegativeRules(applyCorrections(text)).replace(/\s+/g, ' ').trim() || text
119
124
  // applyCorrections above is an EXACT string map, so it only fixes misspellings someone
120
125
  // already hand-authored. A novel miss ("Rileyy" for Riley, "Exampel" for Example) sails
@@ -137,24 +142,43 @@ async function cleanOutboundDictation(text: string, opts: AutoCleanRequest & { s
137
142
  } catch (fuzzyErr: any) {
138
143
  console.warn(`[prompt-draft] Fuzzy correction failed (non-fatal): ${fuzzyErr?.message ?? fuzzyErr}`)
139
144
  }
140
- if (!(opts.enabled ?? autoCleanDefaultEnabled())) return cleaned
145
+ // 6.79.1: um/uh never survive a skipped polish. Meeting lines stay faithful.
146
+ if (opts.bucket !== 'meeting' && fillerPassEnabled()) cleaned = stripDisfluencies(cleaned)
147
+ if (!(opts.enabled ?? autoCleanDefaultEnabled())) return { text: cleaned, polishSkipped: 'disabled' }
141
148
  const model = opts.model ?? 'haiku'
142
149
  const autoCleanBreaker = autoCleanBreakers[model]
143
- if (cleaned.length > AUTOCLEAN_MAX_CHARS || autoCleanBreaker.isOpen() || autoCleanCountToday() >= autoCleanDailyCap()) return cleaned
150
+ if (cleaned.length > AUTOCLEAN_MAX_CHARS) return { text: cleaned, polishSkipped: 'too_long' }
151
+ // 6.79.2, off by default: nothing for the model to fix (see dictation-polish-need.ts).
152
+ if (skipCleanEnabled() && polishNeed(cleaned) === null) return { text: cleaned, polishSkipped: 'clean' }
153
+ if (autoCleanBreaker.isOpen()) return { text: cleaned, polishSkipped: 'breaker_open' }
154
+ if (polishBudgetExhausted(opts.bucket)) return { text: cleaned, polishSkipped: 'daily_cap' }
144
155
  const startedAt = Date.now()
145
- recordAutoCleanCall()
156
+ recordPolishCall(opts.bucket)
157
+ // 6.79.2: a warm process started at key press answers without the CLI's start-up cost.
158
+ const warmEntry = opts.prewarmToken ? takePolishPrewarm(opts.prewarmToken, model) : null
159
+ let prewarm: PrewarmOutcome | undefined = opts.prewarmToken ? 'missed' : undefined
146
160
  try {
147
- const polished = (await autoCleanDictation(cleaned, getVocabulary(), { model, signal: opts.signal })).trim()
161
+ let polished: string | null = null
162
+ if (warmEntry) {
163
+ try {
164
+ polished = (await runWarmPolish(warmEntry, cleaned, getVocabulary(), { signal: opts.signal })).trim()
165
+ prewarm = 'used'
166
+ } catch (err) {
167
+ // Nothing was sent: the cold path below is the same call, not a second one.
168
+ if (!(err instanceof PrewarmUnavailableError)) throw err
169
+ }
170
+ }
171
+ if (polished === null) polished = (await autoCleanDictation(cleaned, getVocabulary(), { model, signal: opts.signal })).trim()
148
172
  autoCleanBreaker.recordSuccess()
149
173
  logTokenAudit({
150
174
  source: 'g2-dictation-autoclean', model, inputChars: cleaned.length, outputChars: polished.length,
151
- durationMs: Date.now() - startedAt, caller: 'dictation_autoclean',
175
+ durationMs: Date.now() - startedAt, caller: `dictation_autoclean_${opts.bucket}`,
152
176
  })
153
- return polished || cleaned
177
+ return { text: polished || cleaned, ...(prewarm ? { prewarm } : {}) }
154
178
  } catch (err: any) {
155
179
  autoCleanBreaker.recordFailure()
156
180
  console.warn(`[prompt-draft] auto-clean failed (glossary-only): ${err?.message ?? err}`)
157
- return cleaned
181
+ return { text: cleaned, polishSkipped: 'failed', ...(prewarm ? { prewarm } : {}) }
158
182
  }
159
183
  }
160
184
 
@@ -175,15 +199,50 @@ promptDraftsRouter.post('/dictation/finalize', async (req, res) => {
175
199
  if (model !== undefined && !isDictationCleanModel(model)) {
176
200
  return res.status(400).json({ error: 'invalid_autoclean_model' })
177
201
  }
202
+ const rawToken = req.body?.prewarmToken
203
+ if (rawToken !== undefined && (typeof rawToken !== 'string' || !PREWARM_TOKEN.test(rawToken))) {
204
+ return res.status(400).json({ error: 'invalid_prewarm_token' })
205
+ }
178
206
 
179
207
  const abort = new AbortController()
180
208
  res.on('close', () => { if (!res.writableEnded) abort.abort() })
181
- const finalText = await cleanOutboundDictation(text, {
209
+ const { text: finalText, polishSkipped, prewarm } = await cleanOutboundDictation(text, {
182
210
  enabled: true,
183
211
  model,
212
+ bucket: surface,
184
213
  signal: abort.signal,
214
+ prewarmToken: rawToken,
185
215
  })
186
- res.json({ text: finalText, surface, polished: finalText !== text })
216
+ // 6.79.1: polishSkipped only when the model did not polish, so a polished answer is unchanged.
217
+ // 6.79.2: prewarm only when the request carried a prewarmToken.
218
+ res.json({
219
+ text: finalText, surface, polished: finalText !== text,
220
+ ...(polishSkipped ? { polishSkipped } : {}), ...(prewarm ? { prewarm } : {}),
221
+ })
222
+ })
223
+
224
+ const PREWARM_TOKEN = /^[0-9a-f-]{36}$/
225
+
226
+ /** 6.79.2: start the polish process at key press. Fire and forget: every refusal is a normal
227
+ * answer, and finalize without a token (or with a dead one) spawns as before. */
228
+ promptDraftsRouter.post('/dictation/polish/prewarm', (req, res) => {
229
+ const model = req.body?.model ?? 'haiku'
230
+ const surface = req.body?.surface ?? 'message'
231
+ if (!isDictationCleanModel(model)) return res.status(400).json({ error: 'invalid_autoclean_model' })
232
+ if (surface !== 'message' && surface !== 'meeting') return res.status(400).json({ error: 'invalid_surface' })
233
+ if (!prewarmEnabled()) return res.status(409).json({ error: 'prewarm_unavailable', reason: 'disabled' })
234
+ if (!isPrewarmModel(model)) return res.status(409).json({ error: 'prewarm_unavailable', reason: 'model' })
235
+ if (autoCleanBreakers[model].isOpen()) return res.status(409).json({ error: 'prewarm_unavailable', reason: 'breaker_open' })
236
+ if (polishBudgetExhausted(surface)) return res.status(409).json({ error: 'prewarm_unavailable', reason: 'daily_cap' })
237
+ try {
238
+ res.json(startPolishPrewarm(model))
239
+ } catch (err: any) {
240
+ res.status(409).json({ error: 'prewarm_unavailable', reason: 'cli' })
241
+ }
242
+ })
243
+
244
+ promptDraftsRouter.delete('/dictation/polish/prewarm/:token', (req, res) => {
245
+ res.json({ discarded: discardPolishPrewarm(req.params.token) })
187
246
  })
188
247
 
189
248
  async function readRawBody(req: AsyncIterable<Buffer | Uint8Array | string>): Promise<Buffer> {
@@ -446,7 +505,7 @@ async function finalizeDraft(draftId: string, mode: 'hq' | 'fast', autoClean: Au
446
505
  await markPromptDraftError(draftId, 'No speech detected')
447
506
  throw new NoSpeechDetectedError()
448
507
  }
449
- const finalText = await cleanOutboundDictation(text, { ...autoClean, signal })
508
+ const { text: finalText, polishSkipped } = await cleanOutboundDictation(text, { ...autoClean, bucket: 'prompt_draft', signal })
450
509
  const finalized = await markPromptDraftFinalized(draftId, finalText)
451
510
  const qualities = transcriptRecords.map(record => record.actualQuality)
452
511
  const actualQuality: 'hq' | 'fast' | 'cloud' = qualities.includes('cloud')
@@ -474,6 +533,8 @@ async function finalizeDraft(draftId: string, mode: 'hq' | 'fast', autoClean: Au
474
533
  // 6.73.0, additive.
475
534
  engine: engines.length === 1 ? engines[0] : engines.length === 0 ? plan.engine : 'mixed',
476
535
  ...(engineReason ? { engineReason } : {}),
536
+ // 6.79.1, additive.
537
+ ...(polishSkipped ? { polishSkipped } : {}),
477
538
  }
478
539
  }
479
540