@gotcos/glasses-server 6.21.6 → 6.21.7

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 CHANGED
@@ -1,3 +1,15 @@
1
+ ## 6.21.7
2
+
3
+ - Add a default-off, authenticated meeting-preview endpoint for private canaries.
4
+ It accepts bounded, server-pinned audio snapshots and returns disposable
5
+ Large-v3-Turbo text without creating or mutating meeting sessions.
6
+ - Keep canonical Large-v3 transcription, speaker attribution, recovery, save, HQ
7
+ polish, and indexing unchanged. Preview never falls back to the canonical worker
8
+ and drops under canonical Metal contention.
9
+ - Reject stale server pins and oversized bodies before inference, recheck
10
+ maintenance admission after slow uploads, and drop concurrent preview work rather
11
+ than building a latency queue. `COS_WHISPER_MEETING_PREVIEW=1` is required.
12
+
1
13
  ## 6.21.6
2
14
 
3
15
  - Make server-owned durable query jobs the default so accepted replies keep
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.21.6",
3
+ "version": "6.21.7",
4
4
  "description": "COS Glasses — self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, or Cursor Agent CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -434,3 +434,37 @@ export async function transcribeWhisperPreview(audioBuffer: Buffer): Promise<{
434
434
  throw error
435
435
  }
436
436
  }
437
+
438
+ /**
439
+ * Meeting-preview canary: use only the isolated Turbo sidecar.
440
+ *
441
+ * Unlike prompt preview, this path never falls through to the canonical
442
+ * Large-v3 worker. A missing, busy, preempted, or failed sidecar is a silent
443
+ * cosmetic miss; durable meeting transcription continues on its unchanged
444
+ * canonical path.
445
+ */
446
+ export async function transcribeWhisperMeetingPreview(audioBuffer: Buffer): Promise<{
447
+ text: string
448
+ model: 'large-v3-turbo'
449
+ backend: 'whisper-preview-server'
450
+ } | null> {
451
+ if (!previewAvailable || previewWorkerModel !== 'large-v3-turbo') return null
452
+ const previewLease = tryAcquireMetalPreview()
453
+ if (!previewLease) return null
454
+ try {
455
+ return {
456
+ text: await transcribeViaPreviewServer(audioBuffer, previewLease.signal),
457
+ model: 'large-v3-turbo',
458
+ backend: 'whisper-preview-server',
459
+ }
460
+ } catch (error) {
461
+ if (previewLease.signal.aborted) return null
462
+ previewAvailable = false
463
+ previewWorkerModel = null
464
+ previewFailure = 'preview_sidecar_unavailable'
465
+ console.warn(`[whisper-preview] meeting Turbo preview failed; canonical meeting ASR was not affected: ${error instanceof Error ? error.message : error}`)
466
+ return null
467
+ } finally {
468
+ previewLease.release()
469
+ }
470
+ }
@@ -29,11 +29,13 @@ import {
29
29
  } from '../lib/openai-whisper-budget.js'
30
30
  import {
31
31
  stripInlineHallucinations as sharedStripInlineHallucinations,
32
+ stripInlineHallucinationsOneShot,
32
33
  isFullHallucination as sharedIsFullHallucination,
33
34
  clearSessionHallucinationState,
34
35
  streamSilenceDropReason,
35
36
  isVocabEchoOnly,
36
37
  } from '../lib/hallucination-filter.js'
38
+ import { transcribeWhisperMeetingPreview } from '../lib/whisper-preview.js'
37
39
  import { dataPath } from '../lib/data-dir.js'
38
40
  import {
39
41
  countChunkWavs,
@@ -101,6 +103,12 @@ function ensurePrivateDirectory(path: string): void {
101
103
  // Rollback: COS_WHISPER_STRIP_BRAND_URLS=0 (URL drops), COS_WHISPER_THANKYOU_FILTER=0.
102
104
  const STRIP_BRAND_URLS = process.env.COS_WHISPER_STRIP_BRAND_URLS !== '0'
103
105
  const THANKYOU_FILTER = process.env.COS_WHISPER_THANKYOU_FILTER !== '0'
106
+ const MEETING_PREVIEW_MAX_BYTES = 512 * 1024
107
+ let meetingPreviewBusy = false
108
+
109
+ function meetingTurboPreviewEnabled(): boolean {
110
+ return process.env.COS_WHISPER_MEETING_PREVIEW === '1'
111
+ }
104
112
 
105
113
  // Audio persistence: save G2-mic chunks for speakers who need more training data
106
114
  const AUDIO_SAVE_DIR = dataPath('training-audio')
@@ -1180,6 +1188,21 @@ async function readRawBody(req: AsyncIterable<Buffer | Uint8Array | string>): Pr
1180
1188
  return Buffer.concat(buffers)
1181
1189
  }
1182
1190
 
1191
+ async function readBoundedRawBody(
1192
+ req: AsyncIterable<Buffer | Uint8Array | string>,
1193
+ maxBytes: number,
1194
+ ): Promise<Buffer> {
1195
+ const buffers: Buffer[] = []
1196
+ let total = 0
1197
+ for await (const chunk of req) {
1198
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
1199
+ total += buffer.length
1200
+ if (total > maxBytes) throw makeHttpError(413, 'audio chunk too large', 'chunk_too_large')
1201
+ buffers.push(buffer)
1202
+ }
1203
+ return Buffer.concat(buffers)
1204
+ }
1205
+
1183
1206
  async function persistRawSessionAudioChunk(sessionId: string, chunkIndex: number, audioBuffer: Buffer): Promise<void> {
1184
1207
  const sessionDir = resolve(SESSION_AUDIO_DIR, sessionId)
1185
1208
  ensurePrivateDirectory(sessionDir)
@@ -1664,6 +1687,70 @@ function sendStreamError(res: { status: (code: number) => { json: (body: unknown
1664
1687
  return res.status(status).json({ error: errMsg(err), reason: (err as any)?.reason })
1665
1688
  }
1666
1689
 
1690
+ /**
1691
+ * Default-off meeting preview canary.
1692
+ *
1693
+ * This route deliberately owns no meeting session, recovery ledger, speaker
1694
+ * attribution, or persistence. It accepts a pinned copy of the still-open
1695
+ * phrase and returns provisional Turbo text. Any contention or failure is a
1696
+ * 204 cosmetic miss; the canonical /transcribe-stream upload remains the only
1697
+ * durable and speaker-attributed result.
1698
+ */
1699
+ transcribeStreamRouter.post('/transcribe-stream/preview', async (req, res) => {
1700
+ try {
1701
+ if (!meetingTurboPreviewEnabled() || !maintenanceAdmissionsOpen()) {
1702
+ return void res.status(204).send()
1703
+ }
1704
+
1705
+ const headerPin = req.get('X-COS-Server-Instance')?.trim() ?? ''
1706
+ const queryPin = typeof req.query.serverInstanceId === 'string'
1707
+ ? req.query.serverInstanceId.trim()
1708
+ : ''
1709
+ if (!headerPin && !queryPin) {
1710
+ throw makeHttpError(400, 'server identity pin required', 'server_identity_pin_required')
1711
+ }
1712
+ const serverInstanceId = assertPinnedServerIdentity(headerPin, queryPin)
1713
+ const sessionId = String(req.query.sessionId ?? '')
1714
+ validateSessionId(sessionId)
1715
+ const chunkIndex = Number(req.query.chunkIndex)
1716
+ validateChunkIndex(chunkIndex)
1717
+ const previewGen = Number(req.query.previewGen)
1718
+ if (!Number.isInteger(previewGen) || previewGen < 0 || previewGen > 1_000_000_000) {
1719
+ throw makeHttpError(400, 'invalid previewGen', 'invalid_preview_generation')
1720
+ }
1721
+
1722
+ const audio = await readBoundedRawBody(req, MEETING_PREVIEW_MAX_BYTES)
1723
+ if (audio.length < 100) throw makeHttpError(400, 'audio too short', 'audio_too_short')
1724
+ if (!maintenanceAdmissionsOpen() || meetingPreviewBusy) {
1725
+ return void res.status(204).send()
1726
+ }
1727
+
1728
+ meetingPreviewBusy = true
1729
+ try {
1730
+ const result = await transcribeWhisperMeetingPreview(audio)
1731
+ if (!result) return void res.status(204).send()
1732
+ const cleaned = stripInlineHallucinationsOneShot(result.text.trim()).trim()
1733
+ if (!cleaned || sharedIsFullHallucination(cleaned)) {
1734
+ return void res.status(204).send()
1735
+ }
1736
+ return void res.json({
1737
+ sessionId,
1738
+ chunkIndex,
1739
+ previewGen,
1740
+ text: cleaned,
1741
+ provisional: true,
1742
+ model: result.model,
1743
+ backend: result.backend,
1744
+ serverInstanceId,
1745
+ })
1746
+ } finally {
1747
+ meetingPreviewBusy = false
1748
+ }
1749
+ } catch (err: unknown) {
1750
+ sendStreamError(res, err)
1751
+ }
1752
+ })
1753
+
1667
1754
  transcribeStreamRouter.post('/transcribe-stream', async (req, res) => {
1668
1755
  let maintenanceLease: MaintenanceWorkLease | undefined
1669
1756
  try {