@gotcos/glasses-server 6.15.3 → 6.16.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.
@@ -52,9 +52,18 @@ export const promptDraftsRouter = Router()
52
52
  const MAX_CHUNK_BYTES = 25 * 1024 * 1024
53
53
  const MAX_DRAFT_BYTES = 256 * 1024 * 1024
54
54
  const MAX_CHUNKS = 600
55
+ /** Purpose-scoped keys (legacy). Prefer modeQualityJobs for HQ warm↔finalize dedupe. */
55
56
  const chunkTranscriptJobs = new Map<string, Promise<string>>()
57
+ /** Shared decode per draft/chunk/mode/hash — warm:hq and final:hq await the same promise. */
58
+ const modeQualityJobs = new Map<string, Promise<string>>()
56
59
  const finalizeJobs = new Map<string, Promise<any>>()
57
60
  let warmTail: Promise<void> = Promise.resolve()
61
+ let hqWarmTail: Promise<void> = Promise.resolve()
62
+
63
+ /** Speculative HQ warm while speaking. Set COS_HQ_SPECULATIVE_WARM=0 to restore Fast-only warm. */
64
+ function speculativeHqWarmEnabled(): boolean {
65
+ return !['0', 'false', 'off'].includes((process.env.COS_HQ_SPECULATIVE_WARM ?? '1').toLowerCase())
66
+ }
58
67
 
59
68
  const autoCleanBreaker = createBreaker({ label: 'dictation-autoclean' })
60
69
  const autoCleanCountFile = () => process.env.COS_DICTATION_AUTOCLEAN_COUNT_FILE || dataPath('.dictation_autoclean_count.json')
@@ -164,22 +173,59 @@ async function sendDraftError(res: Response, draftId: string, err: any): Promise
164
173
  res.status(err.status ?? 500).json({ error: err.message })
165
174
  }
166
175
 
176
+ function modeQualityKey(draftId: string, chunkIndex: number, mode: 'hq' | 'fast', hash: string): string {
177
+ return `${draftId}:${chunkIndex}:${mode}:${hash}`
178
+ }
179
+
167
180
  async function transcribeChunk(draftId: string, chunkIndex: number, audio: Buffer, mode: 'hq' | 'fast', purpose: 'warm' | 'final'): Promise<string> {
168
181
  const hash = audioHash(audio)
169
- const key = `${draftId}:${chunkIndex}:${purpose}:${mode}:${hash}`
170
- const existing = chunkTranscriptJobs.get(key)
182
+ const purposeKey = `${draftId}:${chunkIndex}:${purpose}:${mode}:${hash}`
183
+ const sharedKey = modeQualityKey(draftId, chunkIndex, mode, hash)
184
+
185
+ // HQ warm and HQ finalize must share one decode (plan step 8a).
186
+ const existingShared = modeQualityJobs.get(sharedKey)
187
+ if (existingShared) {
188
+ try {
189
+ const text = await existingShared
190
+ if (purpose === 'final' && text) {
191
+ const current = loadPromptDraftMeta(draftId)
192
+ const warm = current?.warmTranscripts?.[String(chunkIndex)]
193
+ const cachedFinal = current?.finalTranscripts?.[String(chunkIndex)]
194
+ if (!cachedFinal || cachedFinal.hash !== hash) {
195
+ await markPromptDraftChunkTranscript(draftId, chunkIndex, {
196
+ text,
197
+ hash,
198
+ requestedMode: warm?.requestedMode ?? mode,
199
+ actualQuality: warm?.actualQuality ?? (mode === 'hq' ? 'hq' : 'fast'),
200
+ backend: warm?.backend ?? 'shared-inflight',
201
+ degraded: warm?.degraded ?? false,
202
+ ...(warm?.degradationReason ? { degradationReason: warm.degradationReason } : {}),
203
+ }, 'final')
204
+ }
205
+ }
206
+ return text
207
+ } catch {
208
+ // Shared warm failed under local-only; fall through so finalize can retry with automatic.
209
+ }
210
+ }
211
+
212
+ const existing = chunkTranscriptJobs.get(purposeKey)
171
213
  if (existing) return existing
214
+
172
215
  const job = (async () => {
173
216
  try {
174
- const result = await transcribeAudioBuffer(audio, { mode, policy: purpose === 'warm' ? 'local-only' : 'automatic' })
217
+ // Speculative warm is always local-only. Finalize may use automatic cloud fallback.
218
+ const policy = purpose === 'warm' ? 'local-only' as const : 'automatic' as const
219
+ const result = await transcribeAudioBuffer(audio, { mode, policy })
175
220
  if (!isCurrentChunk(draftId, chunkIndex, audio)) return ''
176
221
  const text = sanitizeTranscript(draftId, result.text)
177
222
  const record: PromptDraftTranscriptRecord = {
178
223
  text, hash, requestedMode: result.requestedMode, actualQuality: result.actualQuality,
179
224
  backend: result.backend, degraded: result.degraded,
225
+ ...(result.degradationReason ? { degradationReason: result.degradationReason } : {}),
180
226
  }
181
227
  await markPromptDraftChunkTranscript(draftId, chunkIndex, record, purpose)
182
- console.log(`[prompt-draft] chunk ${draftId}/${chunkIndex}: ${result.elapsedMs.toFixed(1)}ms | ${result.backend} | ${text.length} chars`)
228
+ console.log(`[prompt-draft] chunk ${draftId}/${chunkIndex}: ${result.elapsedMs.toFixed(1)}ms | ${result.backend} | ${purpose}/${mode} | ${text.length} chars`)
183
229
  return text
184
230
  } catch (err) {
185
231
  if (err instanceof NoSpeechDetectedError) {
@@ -190,10 +236,12 @@ async function transcribeChunk(draftId: string, chunkIndex: number, audio: Buffe
190
236
  }
191
237
  throw err
192
238
  } finally {
193
- chunkTranscriptJobs.delete(key)
239
+ chunkTranscriptJobs.delete(purposeKey)
240
+ modeQualityJobs.delete(sharedKey)
194
241
  }
195
242
  })()
196
- chunkTranscriptJobs.set(key, job)
243
+ chunkTranscriptJobs.set(purposeKey, job)
244
+ modeQualityJobs.set(sharedKey, job)
197
245
  return job
198
246
  }
199
247
 
@@ -201,14 +249,40 @@ async function finalizeDraft(draftId: string, mode: 'hq' | 'fast', autoClean: Au
201
249
  const meta = loadPromptDraftMeta(draftId)
202
250
  if (!meta) throw Object.assign(new Error('draft not found'), { status: 404 })
203
251
  const texts: string[] = []
252
+ const transcriptRecords: PromptDraftTranscriptRecord[] = []
204
253
  for (const chunk of readPromptDraftChunks(draftId)) {
205
254
  try {
255
+ const hash = audioHash(chunk.audioBuffer)
256
+ // Await in-flight HQ warm before deciding cache miss (plan step 8b belt).
257
+ if (mode === 'hq') {
258
+ const inflight = modeQualityJobs.get(modeQualityKey(draftId, chunk.chunkIndex, 'hq', hash))
259
+ if (inflight) {
260
+ try { await inflight } catch { /* finalize may retry with automatic below */ }
261
+ }
262
+ }
206
263
  const current = loadPromptDraftMeta(draftId)
207
264
  const cached = current?.finalTranscripts?.[String(chunk.chunkIndex)] ?? current?.warmTranscripts?.[String(chunk.chunkIndex)]
208
- const reusable = Boolean(cached && cached.hash === audioHash(chunk.audioBuffer) && (mode === 'fast' ? cached.actualQuality === 'fast' : cached.actualQuality === 'hq'))
265
+ // Reuse the exact requested-mode decode even when HQ truthfully degraded
266
+ // to turbo. Finalize's automatic policy would make the same local choice
267
+ // again after a successful turbo result, so a second decode adds latency
268
+ // without improving quality. Legacy records stay excluded because their
269
+ // decoder provenance was reconstructed during migration.
270
+ const reusable = Boolean(
271
+ cached
272
+ && cached.hash === hash
273
+ && cached.requestedMode === mode
274
+ && !cached.backend.startsWith('legacy'),
275
+ )
209
276
  const raw = reusable ? cached!.text : await transcribeChunk(draftId, chunk.chunkIndex, chunk.audioBuffer, mode, 'final')
210
277
  const text = sanitizeTranscript(draftId, raw, !reusable)
211
- if (text.trim()) texts.push(text.trim())
278
+ if (text.trim()) {
279
+ texts.push(text.trim())
280
+ const latest = loadPromptDraftMeta(draftId)
281
+ const used = latest?.finalTranscripts?.[String(chunk.chunkIndex)]
282
+ ?? latest?.warmTranscripts?.[String(chunk.chunkIndex)]
283
+ ?? cached
284
+ if (used && used.hash === hash) transcriptRecords.push(used)
285
+ }
212
286
  } catch (err) {
213
287
  if (err instanceof NoSpeechDetectedError) continue
214
288
  throw err
@@ -221,7 +295,28 @@ async function finalizeDraft(draftId: string, mode: 'hq' | 'fast', autoClean: Au
221
295
  }
222
296
  const finalText = await cleanOutboundDictation(text, { ...autoClean, signal })
223
297
  const finalized = await markPromptDraftFinalized(draftId, finalText)
224
- return { draftId, text: finalText, recovered: true, chunkCount: finalized.receivedChunkIndexes.length, missingChunks: getMissingChunkIndexes(finalized), expiresAt: finalized.expiresAt }
298
+ const qualities = transcriptRecords.map(record => record.actualQuality)
299
+ const actualQuality: 'hq' | 'fast' | 'cloud' = qualities.includes('cloud')
300
+ ? 'cloud'
301
+ : qualities.length > 0 && qualities.every(quality => quality === 'hq')
302
+ ? 'hq'
303
+ : 'fast'
304
+ const degraded = mode === 'hq' && (actualQuality !== 'hq' || transcriptRecords.some(record => record.degraded))
305
+ const backends = [...new Set(transcriptRecords.map(record => record.backend))]
306
+ const degradationReason = transcriptRecords.find(record => record.degradationReason)?.degradationReason
307
+ return {
308
+ draftId,
309
+ text: finalText,
310
+ recovered: true,
311
+ chunkCount: finalized.receivedChunkIndexes.length,
312
+ missingChunks: getMissingChunkIndexes(finalized),
313
+ expiresAt: finalized.expiresAt,
314
+ requestedMode: mode,
315
+ actualQuality,
316
+ degraded,
317
+ backend: backends.length === 1 ? backends[0] : 'mixed',
318
+ ...(degradationReason ? { degradationReason } : {}),
319
+ }
225
320
  }
226
321
 
227
322
  const prunedAtBoot = maintenanceAdmissionsOpen() ? prunePromptDrafts() : 0
@@ -265,10 +360,13 @@ promptDraftsRouter.post('/prompt-drafts/:draftId/chunks', async (req, res) => {
265
360
  const nextTotal = Object.values(before.chunkBytes).reduce((sum, bytes) => sum + bytes, 0) - existingBytes + audio.length
266
361
  if (nextTotal > MAX_DRAFT_BYTES) return res.status(413).json({ error: 'prompt draft too large' })
267
362
  const meta = await savePromptDraftChunk(req.params.draftId, chunkIndex, audio)
363
+ const requestedMode = routeMode(req)
268
364
  const warmLease = acquireMaintenanceWork('prompt_draft_warm', {
269
365
  allowDuringDrain: true,
270
366
  phase: 'queued',
271
367
  })
368
+ // Fast warm feeds the live HUD. Speculative HQ (when Settings HQ / default)
369
+ // overwrites warmTranscripts with actualQuality=hq for near-instant finalize.
272
370
  warmTail = warmTail.then(async () => {
273
371
  warmLease.setPhase('active')
274
372
  const text = await transcribeChunk(req.params.draftId, chunkIndex, audio, 'fast', 'warm')
@@ -283,6 +381,21 @@ promptDraftsRouter.post('/prompt-drafts/:draftId/chunks', async (req, res) => {
283
381
  }).catch(err => {
284
382
  console.warn(`[prompt-draft] warm transcription failed ${req.params.draftId}/${chunkIndex}: ${err.message}`)
285
383
  }).finally(() => warmLease.release())
384
+
385
+ if (requestedMode === 'hq' && speculativeHqWarmEnabled()) {
386
+ const hqLease = acquireMaintenanceWork('prompt_draft_warm', {
387
+ allowDuringDrain: true,
388
+ phase: 'queued',
389
+ })
390
+ hqWarmTail = hqWarmTail.then(async () => {
391
+ hqLease.setPhase('active')
392
+ // Cache only — never emitDisplay HQ (avoids HUD flicker). local-only via purpose=warm.
393
+ await transcribeChunk(req.params.draftId, chunkIndex, audio, 'hq', 'warm')
394
+ }).catch(err => {
395
+ console.warn(`[prompt-draft] speculative HQ warm failed ${req.params.draftId}/${chunkIndex}: ${err.message}`)
396
+ }).finally(() => hqLease.release())
397
+ }
398
+
286
399
  res.json({ draftId: meta.draftId, chunkIndex, acked: true, receivedChunkIndexes: meta.receivedChunkIndexes, chunkBytes: meta.chunkBytes[String(chunkIndex)] ?? audio.length, transcriptPending: true, expiresAt: meta.expiresAt })
287
400
  } catch (err: any) {
288
401
  if (err instanceof MaintenanceLifecycleError) {
@@ -1,5 +1,11 @@
1
1
  import { Router } from 'express'
2
2
  import { runProviderProof, type ProofProvider } from '../lib/provider-proof.js'
3
+ import {
4
+ acquireMaintenanceWork,
5
+ maintenanceErrorPayload,
6
+ maintenanceOperationCredentialsValid,
7
+ MaintenanceLifecycleError,
8
+ } from '../lib/maintenance-lifecycle.js'
3
9
 
4
10
  export const providerProofRouter = Router()
5
11
 
@@ -14,6 +20,38 @@ providerProofRouter.post('/diagnostics/provider-proof', async (req, res) => {
14
20
  if (provider !== 'claude' && provider !== 'codex') {
15
21
  return res.status(400).json({ error: 'provider must be claude or codex' })
16
22
  }
17
- const result = await runProviderProof(provider as ProofProvider)
18
- return res.status(result.ok ? 200 : 503).json(result)
23
+ const controllerProof = maintenanceOperationCredentialsValid({
24
+ leaseId: typeof req.headers['x-cos-maintenance-lease'] === 'string'
25
+ ? req.headers['x-cos-maintenance-lease'] : undefined,
26
+ operationId: typeof req.headers['x-cos-maintenance-operation'] === 'string'
27
+ ? req.headers['x-cos-maintenance-operation'] : undefined,
28
+ nonce: typeof req.headers['x-cos-maintenance-nonce'] === 'string'
29
+ ? req.headers['x-cos-maintenance-nonce'] : undefined,
30
+ })
31
+ let lease
32
+ try {
33
+ lease = acquireMaintenanceWork('api_mutation', { allowDuringDrain: controllerProof })
34
+ } catch (error) {
35
+ if (error instanceof MaintenanceLifecycleError) {
36
+ if (error.retryAfterSeconds != null) res.setHeader('Retry-After', String(error.retryAfterSeconds))
37
+ return res.status(error.status).json(maintenanceErrorPayload(error))
38
+ }
39
+ return res.status(500).json({ error: 'maintenance_internal_error', retryable: false })
40
+ }
41
+
42
+ const abort = new AbortController()
43
+ let responseFinished = false
44
+ const cancel = () => { if (!responseFinished) abort.abort(new Error('Control proof client disconnected')) }
45
+ req.once('aborted', cancel)
46
+ res.once('finish', () => { responseFinished = true })
47
+ res.once('close', cancel)
48
+ try {
49
+ const result = await runProviderProof(provider as ProofProvider, abort.signal)
50
+ if (abort.signal.aborted) return
51
+ return res.status(result.ok ? 200 : 503).json(result)
52
+ } finally {
53
+ req.removeListener('aborted', cancel)
54
+ res.removeListener('close', cancel)
55
+ lease.release()
56
+ }
19
57
  })
@@ -44,7 +44,15 @@ transcribeRouter.post('/transcribe', async (req, res) => {
44
44
 
45
45
  const result = await transcribeAudioBuffer(audioBuffer, { mode: resolveMode(req) })
46
46
  console.log(`[perf] /transcribe: ${result.elapsedMs.toFixed(1)}ms | mode=${result.mode} | ${result.backend} | ${result.audioBytes}b | ${result.text.length} chars`)
47
- res.json({ text: result.text, backend: result.backend, mode: result.mode })
47
+ res.json({
48
+ text: result.text,
49
+ backend: result.backend,
50
+ mode: result.mode,
51
+ requestedMode: result.requestedMode,
52
+ actualQuality: result.actualQuality,
53
+ degraded: result.degraded,
54
+ ...(result.degradationReason ? { degradationReason: result.degradationReason } : {}),
55
+ })
48
56
  } catch (err: any) {
49
57
  if (err instanceof MaintenanceLifecycleError) {
50
58
  if (err.retryAfterSeconds != null) res.setHeader('Retry-After', String(err.retryAfterSeconds))