@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.
- package/CHANGELOG.md +133 -0
- package/README.md +29 -0
- package/managed-runtime-contract.json +11 -1
- package/package.json +3 -1
- package/server/lib/dictation-clean.ts +76 -16
- package/server/lib/dictation-fillers.ts +116 -0
- package/server/lib/dictation-polish-budget.ts +87 -0
- package/server/lib/dictation-polish-need.ts +48 -0
- package/server/lib/dictation-prewarm.ts +209 -0
- package/server/lib/live-cues-v2-settings.ts +51 -13
- package/server/lib/live-cues-v2.ts +147 -19
- package/server/routes/live-cues.ts +39 -3
- package/server/routes/prompt-drafts.ts +95 -34
|
@@ -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 {
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|