@gotcos/glasses-server 6.73.5 → 6.73.14
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 +62 -0
- package/package.json +1 -1
- package/server/index.ts +5 -0
- package/server/lib/ask-session-id.ts +5 -0
- package/server/lib/decision-provider.ts +163 -0
- package/server/lib/decision-settings.ts +72 -0
- package/server/lib/glossary-learn.ts +221 -0
- package/server/lib/imported-library-rows.ts +62 -4
- package/server/lib/jev.ts +3 -3
- package/server/lib/lens-gist-engines.ts +7 -2
- package/server/lib/live-cos-ask.ts +2 -1
- package/server/lib/mac-recording-asr.ts +59 -0
- package/server/lib/mac-recording.ts +111 -0
- package/server/lib/meeting-actions.ts +1 -0
- package/server/lib/meeting-ask-follow-up.ts +2 -2
- package/server/lib/meeting-ask-runner.ts +26 -5
- package/server/lib/meeting-ask-store.ts +2 -1
- package/server/lib/meeting-batch-transcribe.ts +19 -4
- package/server/lib/meeting-context-keys.ts +7 -4
- package/server/lib/meeting-parse.ts +2 -0
- package/server/lib/meeting-store.ts +62 -9
- package/server/lib/session-recommendation.ts +12 -9
- package/server/lib/speaker-embeddings.ts +2 -2
- package/server/lib/stranded-session-actions.ts +15 -0
- package/server/lib/subq-key.ts +38 -0
- package/server/lib/transcribe-audio.ts +9 -1
- package/server/lib/work-activity.ts +2 -2
- package/server/lib/work-chat.ts +87 -11
- package/server/lib/work-completion.ts +11 -8
- package/server/lib/work-intake-store.ts +2 -1
- package/server/lib/work-search.ts +32 -12
- package/server/routes/client-instance.ts +1 -1
- package/server/routes/decision-provider.ts +36 -0
- package/server/routes/glossary.ts +143 -11
- package/server/routes/jev.ts +3 -2
- package/server/routes/meeting.ts +44 -12
- package/server/routes/meetings.ts +42 -4
- package/server/routes/subq-key.ts +27 -0
- package/server/routes/transcribe-stream.ts +115 -9
- package/server/routes/transcribe.ts +20 -2
- package/server/routes/work-chat.ts +8 -4
- package/server/routes/work-search.ts +2 -2
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { MacParakeetUnavailableError, transcribeMacParakeet } from '../lib/mac-recording-asr.js'
|
|
2
|
+
import { getMeetingStore } from '../lib/meeting-store.js'
|
|
3
|
+
import { MAC_AUDIO, MAC_CAPTURE_ID, persistMacChunkReceipt, macError, registerMacRecording, requireMacRecording, readMacRecording, validateMacWav } from '../lib/mac-recording.js'
|
|
1
4
|
import { AUTO_ENROLL_THRESHOLD } from '../lib/voiceprint-floor.js'
|
|
2
5
|
import { readSpeakerPolicy, policyLabel, type SpeakerMode } from '../lib/meeting-speaker-policy.js'
|
|
3
6
|
// POST /api/transcribe-stream — Streaming transcription for continuous meeting capture
|
|
@@ -19,6 +22,7 @@ import {
|
|
|
19
22
|
clearStrandedDraft,
|
|
20
23
|
listStrandedDrafts,
|
|
21
24
|
releaseStrandedState,
|
|
25
|
+
restoresAtBoot,
|
|
22
26
|
shouldCloseAfterFailedPromote,
|
|
23
27
|
sweepStrandedSessions,
|
|
24
28
|
promoteStrandedSession,
|
|
@@ -277,7 +281,7 @@ export interface TranscriptChunk {
|
|
|
277
281
|
/** Capture-time policy survives batch and final passes; absent means legacy Standard. */
|
|
278
282
|
speakerPolicy?: SpeakerMode
|
|
279
283
|
words?: WhisperWord[] // word-level timestamps from DTW alignment
|
|
280
|
-
asrProvider?: 'server-whisper' | 'iphone-whisperkit-beta'
|
|
284
|
+
asrProvider?: 'server-whisper' | 'server-parakeet' | 'iphone-whisperkit-beta'
|
|
281
285
|
backend?: string
|
|
282
286
|
model?: string
|
|
283
287
|
mode?: string
|
|
@@ -286,6 +290,7 @@ export interface TranscriptChunk {
|
|
|
286
290
|
audioSha256?: string
|
|
287
291
|
canonical?: boolean
|
|
288
292
|
evenHubSpeakerRole?: EvenSpeakerRoleHistogram
|
|
293
|
+
audioDurationMs?: number
|
|
289
294
|
/** 6.61.0: who spoke when inside this chunk, in time order. Present when
|
|
290
295
|
* Nemotron separated the chunk's voices; `speaker` is then the dominant name. */
|
|
291
296
|
segments?: ChunkSpeakerSegment[]
|
|
@@ -365,6 +370,8 @@ interface EmptyTranscriptCompletion {
|
|
|
365
370
|
elapsed: number
|
|
366
371
|
backend?: string
|
|
367
372
|
asrProvider?: string
|
|
373
|
+
model?: string
|
|
374
|
+
latencyMs?: number
|
|
368
375
|
fallbackReason?: string
|
|
369
376
|
audioSha256?: string
|
|
370
377
|
canonical: false
|
|
@@ -381,6 +388,8 @@ interface StreamChunkCompletionResponse {
|
|
|
381
388
|
canonical: boolean
|
|
382
389
|
backend?: string
|
|
383
390
|
asrProvider?: string
|
|
391
|
+
model?: string
|
|
392
|
+
latencyMs?: number
|
|
384
393
|
fallbackReason?: string
|
|
385
394
|
/** 6.61.0, additive: the chunk's speaker turns in time order. Their texts
|
|
386
395
|
* joined with one space equal `text`; `speaker` stays the dominant name. */
|
|
@@ -820,7 +829,7 @@ function recoverSessions(): void {
|
|
|
820
829
|
if (data.sessionId && (hasChunks || Array.isArray(data.receivedIndices) || Number.isFinite(lastActivityAt))) {
|
|
821
830
|
// Active retention is idle-based. Long meetings are not purged merely
|
|
822
831
|
// because their original start time is old.
|
|
823
|
-
if (Date.now() - lastActivityAt
|
|
832
|
+
if (restoresAtBoot(data.sessionId, Date.now() - lastActivityAt, LOCAL_FIRST_MEETING_IDLE_RETENTION_MS)) {
|
|
824
833
|
const chunks: TranscriptChunk[] = []
|
|
825
834
|
if (indexed) {
|
|
826
835
|
for (const e of indexed) {
|
|
@@ -1520,7 +1529,11 @@ export function getSessionChunkEntries(sessionId: string): IndexedTranscriptChun
|
|
|
1520
1529
|
elapsed: empty.elapsed,
|
|
1521
1530
|
similarity: 0,
|
|
1522
1531
|
backend: empty.backend,
|
|
1523
|
-
|
|
1532
|
+
...(empty.model ? { model: empty.model } : {}),
|
|
1533
|
+
...(empty.latencyMs !== undefined ? { latencyMs: empty.latencyMs } : {}),
|
|
1534
|
+
asrProvider: empty.asrProvider === 'server-parakeet'
|
|
1535
|
+
? 'server-parakeet'
|
|
1536
|
+
: empty.asrProvider === 'iphone-whisperkit-beta'
|
|
1524
1537
|
? 'iphone-whisperkit-beta'
|
|
1525
1538
|
: empty.asrProvider === 'server-whisper'
|
|
1526
1539
|
? 'server-whisper'
|
|
@@ -1547,6 +1560,9 @@ export function getSessionAsrCompletedIndices(sessionId: string): number[] {
|
|
|
1547
1560
|
}
|
|
1548
1561
|
|
|
1549
1562
|
function scheduleSessionProgressiveHq(sessionId: string): void {
|
|
1563
|
+
// Progressive checkpoints are Whisper-only and may not bypass a Mac's
|
|
1564
|
+
// explicit Parakeet choice, even if the separate HQ canary is enabled.
|
|
1565
|
+
if (MAC_CAPTURE_ID.test(sessionId) && readSttSettings().hq === 'parakeet-ultra') return
|
|
1550
1566
|
// Preserve the default-off contract at the route boundary. In addition to
|
|
1551
1567
|
// avoiding any cache work, this keeps legacy/local-first transcription
|
|
1552
1568
|
// independent of the progressive HQ capability surface when the canary is
|
|
@@ -2016,6 +2032,8 @@ function canonicalChunkResponse(
|
|
|
2016
2032
|
canonical: true,
|
|
2017
2033
|
backend: existing.backend,
|
|
2018
2034
|
asrProvider: existing.asrProvider,
|
|
2035
|
+
...(existing.model ? { model: existing.model } : {}),
|
|
2036
|
+
...(existing.latencyMs !== undefined ? { latencyMs: existing.latencyMs } : {}),
|
|
2019
2037
|
fallbackReason: existing.fallbackReason,
|
|
2020
2038
|
...(existing.segments?.length ? { segments: existing.segments } : {}),
|
|
2021
2039
|
}
|
|
@@ -2072,7 +2090,7 @@ function identifyChunkSpeaker(audioBuffer: Buffer, sessionId: string, chunkIndex
|
|
|
2072
2090
|
if (!isEmbeddingAvailable() || audioDurationSec < VOICEPRINT_MIN_AUDIO_SEC) return policyLabel({ speaker: clientSpeaker, similarity: 0 }, policy)
|
|
2073
2091
|
|
|
2074
2092
|
const tEmb = performance.now()
|
|
2075
|
-
const embeddingResult = identifySpeaker(audioBuffer, expectedSpeakers, { minSimilarity: policy === 'high-confidence' ? AUTO_ENROLL_THRESHOLD : undefined })
|
|
2093
|
+
const embeddingResult = identifySpeaker(audioBuffer, expectedSpeakers, { minSimilarity: policy === 'high-confidence' ? AUTO_ENROLL_THRESHOLD : undefined, ...(MAC_CAPTURE_ID.test(sessionId) ? { prioritizeOwner: false, event: 'mac-system-audio' } : {}) })
|
|
2076
2094
|
console.log(`[perf] identifySpeaker: ${(performance.now() - tEmb).toFixed(1)}ms`)
|
|
2077
2095
|
if (!embeddingResult) return policyLabel({ speaker: clientSpeaker, similarity: 0 }, policy)
|
|
2078
2096
|
|
|
@@ -2080,8 +2098,9 @@ function identifyChunkSpeaker(audioBuffer: Buffer, sessionId: string, chunkIndex
|
|
|
2080
2098
|
// it exists: identifySpeaker computes it per chunk and nothing else keeps it,
|
|
2081
2099
|
// so a correction made later has no acoustic evidence without this write.
|
|
2082
2100
|
// Deliberately includes 'Ext' — an unidentified voice is the case with no
|
|
2083
|
-
// other way to be trained.
|
|
2084
|
-
|
|
2101
|
+
// other way to be trained. Mac playback is recognition-only: never bank
|
|
2102
|
+
// its voiceprints or create auxiliary training-audio copies.
|
|
2103
|
+
if (embeddingResult.embedding && !MAC_CAPTURE_ID.test(sessionId)) {
|
|
2085
2104
|
appendChunkEmbedding(sessionId, {
|
|
2086
2105
|
i: chunkIndex,
|
|
2087
2106
|
speaker: policyLabel(embeddingResult, policy).speaker,
|
|
@@ -2117,7 +2136,7 @@ function identifyChunkSpeaker(audioBuffer: Buffer, sessionId: string, chunkIndex
|
|
|
2117
2136
|
}
|
|
2118
2137
|
}
|
|
2119
2138
|
|
|
2120
|
-
if (speaker === 'Ext' && audioBuffer.length >= 16000) {
|
|
2139
|
+
if (speaker === 'Ext' && audioBuffer.length >= 16000 && !MAC_CAPTURE_ID.test(sessionId)) {
|
|
2121
2140
|
const extSessionDir = resolve(EXT_AUDIO_DIR, sessionId)
|
|
2122
2141
|
ensurePrivateDirectory(extSessionDir)
|
|
2123
2142
|
const extCount = extAudioCounts.get(sessionId) ?? 0
|
|
@@ -2154,6 +2173,7 @@ async function processStreamChunk(opts: {
|
|
|
2154
2173
|
clientElapsed?: number
|
|
2155
2174
|
/** Original client recording start, applied only before canonical chunks. */
|
|
2156
2175
|
startTimeOverride?: number
|
|
2176
|
+
audioDurationMs?: number
|
|
2157
2177
|
evenHubSpeakerRole?: EvenSpeakerRoleHistogram
|
|
2158
2178
|
}): Promise<StreamChunkCompletionResponse> {
|
|
2159
2179
|
const { sessionId, chunkIndex, clientSpeaker, audioBuffer, candidate } = opts
|
|
@@ -2215,7 +2235,7 @@ async function processStreamChunk(opts: {
|
|
|
2215
2235
|
|
|
2216
2236
|
// Pin identity policy at acceptance, before persistence, ASR or diarization
|
|
2217
2237
|
// yields. A later loosening applies to the next chunk, never this audio.
|
|
2218
|
-
const capturePolicy = readSpeakerPolicy(sessionId).mode
|
|
2238
|
+
const capturePolicy = MAC_CAPTURE_ID.test(sessionId) ? (requireMacRecording(sessionId), 'high-confidence' as const) : readSpeakerPolicy(sessionId).mode
|
|
2219
2239
|
await persistRawSessionAudioChunk(sessionId, chunkIndex, audioBuffer)
|
|
2220
2240
|
// Commit the received-index ledger only after the canonical raw WAV is
|
|
2221
2241
|
// durable. A failure is typed non-2xx and a retry remains safe.
|
|
@@ -2250,7 +2270,7 @@ async function processStreamChunk(opts: {
|
|
|
2250
2270
|
let rawText = ''
|
|
2251
2271
|
let words: WhisperWord[] | undefined
|
|
2252
2272
|
let backend = 'candidate'
|
|
2253
|
-
let asrProvider: 'server-whisper' | 'iphone-whisperkit-beta' = 'server-whisper'
|
|
2273
|
+
let asrProvider: 'server-whisper' | 'server-parakeet' | 'iphone-whisperkit-beta' = 'server-whisper'
|
|
2254
2274
|
let model: string | undefined
|
|
2255
2275
|
let mode: string | undefined
|
|
2256
2276
|
let latencyMs: number | undefined
|
|
@@ -2263,6 +2283,16 @@ async function processStreamChunk(opts: {
|
|
|
2263
2283
|
model = candidate.model
|
|
2264
2284
|
mode = candidate.mode
|
|
2265
2285
|
latencyMs = candidate.latencyMs
|
|
2286
|
+
} else if (MAC_CAPTURE_ID.test(sessionId) && stt.hq === 'parakeet-ultra') {
|
|
2287
|
+
// Use the stored choice directly. effectiveStt/preview intentionally fall
|
|
2288
|
+
// back when unavailable, which is not an acceptable canonical Mac commit.
|
|
2289
|
+
const result = await transcribeMacParakeet(whisperAudio, quiet.speechSec)
|
|
2290
|
+
rawText = result.text
|
|
2291
|
+
words = result.words
|
|
2292
|
+
backend = result.backend
|
|
2293
|
+
asrProvider = result.asrProvider
|
|
2294
|
+
model = result.model
|
|
2295
|
+
latencyMs = result.latencyMs
|
|
2266
2296
|
} else {
|
|
2267
2297
|
const t0 = performance.now()
|
|
2268
2298
|
const result = await transcribeWithServerWhisper(audioBuffer, whisperAudio, whisperContext, isQuiet)
|
|
@@ -2343,6 +2373,8 @@ async function processStreamChunk(opts: {
|
|
|
2343
2373
|
elapsed,
|
|
2344
2374
|
backend,
|
|
2345
2375
|
asrProvider,
|
|
2376
|
+
...(model ? { model } : {}),
|
|
2377
|
+
...(latencyMs !== undefined ? { latencyMs } : {}),
|
|
2346
2378
|
fallbackReason: sanitized.fallbackReason || fallbackReason,
|
|
2347
2379
|
audioSha256,
|
|
2348
2380
|
canonical: false,
|
|
@@ -2358,6 +2390,7 @@ async function processStreamChunk(opts: {
|
|
|
2358
2390
|
const chunk: TranscriptChunk = {
|
|
2359
2391
|
text: trimmedText,
|
|
2360
2392
|
speakerPolicy: capturePolicy,
|
|
2393
|
+
...(opts.audioDurationMs !== undefined ? { audioDurationMs: opts.audioDurationMs } : {}),
|
|
2361
2394
|
speaker,
|
|
2362
2395
|
elapsed,
|
|
2363
2396
|
similarity,
|
|
@@ -2417,6 +2450,9 @@ async function processStreamChunk(opts: {
|
|
|
2417
2450
|
}
|
|
2418
2451
|
|
|
2419
2452
|
function sendStreamError(res: { status: (code: number) => { json: (body: unknown) => unknown } }, err: unknown): unknown {
|
|
2453
|
+
if (err instanceof MacParakeetUnavailableError) {
|
|
2454
|
+
return res.status(err.status).json({ error: err.message, reason: err.reason, retryable: true, model: err.model })
|
|
2455
|
+
}
|
|
2420
2456
|
if (err instanceof TranscriptionUnavailableError) {
|
|
2421
2457
|
console.warn(`[transcribe-stream] ${err.message}`)
|
|
2422
2458
|
return res.status(err.status).json({
|
|
@@ -2507,12 +2543,82 @@ transcribeStreamRouter.post('/transcribe-stream/preview', async (req, res) => {
|
|
|
2507
2543
|
}
|
|
2508
2544
|
})
|
|
2509
2545
|
|
|
2546
|
+
/** Unlike the received ledger, only these canonical completions acknowledge ASR.
|
|
2547
|
+
* A native stop supplies the total captured chunks, including its last tail. */
|
|
2548
|
+
export function assertMacCaptureComplete(sessionId: string, expectedChunkCount: unknown): void {
|
|
2549
|
+
if (!Number.isInteger(expectedChunkCount) || Number(expectedChunkCount) < 1 || Number(expectedChunkCount) > 10000) {
|
|
2550
|
+
throw macError(400, 'mac_chunk_count_required', 'Mac save requires the total captured chunk count')
|
|
2551
|
+
}
|
|
2552
|
+
const session = sessions.get(sessionId)
|
|
2553
|
+
const expected = Number(expectedChunkCount)
|
|
2554
|
+
const missingIndices: number[] = []
|
|
2555
|
+
for (let index = 0; index < expected; index++) {
|
|
2556
|
+
if (!session?.chunks[index] && !session?.emptyCompletions?.[String(index)]) missingIndices.push(index)
|
|
2557
|
+
}
|
|
2558
|
+
if (missingIndices.length || (session?.maxChunkIndex ?? -1) >= expected) {
|
|
2559
|
+
throw Object.assign(macError(409, 'mac_capture_incomplete', 'Mac audio is not fully acknowledged; retry pending chunks before saving'), { missingIndices })
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
// Manual Mac pilot: independent from iPhone candidate and cue rollout gates.
|
|
2564
|
+
transcribeStreamRouter.get('/mac-recording/capabilities', (_req, res) => {
|
|
2565
|
+
res.set('Cache-Control', 'private, no-store').json({ available: true, ...MAC_AUDIO, serverInstanceId: getServerInstanceId() })
|
|
2566
|
+
})
|
|
2567
|
+
function requireMacPin(req: { get(name: string): string | undefined }): string {
|
|
2568
|
+
const pin = req.get('X-COS-Server-Instance')?.trim()
|
|
2569
|
+
if (!pin) throw macError(400, 'server_identity_pin_required', 'Server identity pin required')
|
|
2570
|
+
return assertPinnedServerIdentity(pin, undefined)
|
|
2571
|
+
}
|
|
2572
|
+
transcribeStreamRouter.post('/mac-recording/start', (req, res) => {
|
|
2573
|
+
let lease: MaintenanceWorkLease | undefined
|
|
2574
|
+
try {
|
|
2575
|
+
const serverInstanceId = requireMacPin(req)
|
|
2576
|
+
if (typeof req.body?.captureId !== 'string' || !MAC_CAPTURE_ID.test(req.body.captureId)) throw macError(400, 'invalid_capture_id', 'Invalid Mac capture ID')
|
|
2577
|
+
if (isSessionDeleted(req.body.captureId) || getMeetingStore().findBySessionId(req.body.captureId)) throw macError(410, 'session_closed', 'Capture is closed')
|
|
2578
|
+
// An existing non-Mac session may not acquire new source metadata.
|
|
2579
|
+
if (sessions.has(req.body.captureId) && !readMacRecording(req.body.captureId)) throw macError(409, 'mac_capture_conflict', 'Capture ID already exists')
|
|
2580
|
+
lease = acquireRecordingChunkLease({ allowDuringDrain: sessions.has(req.body.captureId) })
|
|
2581
|
+
const { recording, replayed } = registerMacRecording(req.body)
|
|
2582
|
+
const session = getSession(recording.sessionId)
|
|
2583
|
+
if (!session.chunks.some(Boolean)) session.startTime = recording.startTime
|
|
2584
|
+
persistSessionRequired(recording.sessionId)
|
|
2585
|
+
res.json({ ...MAC_AUDIO, ...recording, replayed, serverInstanceId })
|
|
2586
|
+
} catch (error) { sendStreamError(res, error) } finally { lease?.release() }
|
|
2587
|
+
})
|
|
2588
|
+
const macChunkLocks = new Set<string>()
|
|
2589
|
+
transcribeStreamRouter.post('/mac-recording/:captureId/chunks', async (req, res) => {
|
|
2590
|
+
let lease: MaintenanceWorkLease | undefined
|
|
2591
|
+
let locked = false
|
|
2592
|
+
const id = String(req.params.captureId ?? '')
|
|
2593
|
+
try {
|
|
2594
|
+
requireMacPin(req)
|
|
2595
|
+
const recording = requireMacRecording(id)
|
|
2596
|
+
if (isSessionDeleted(id)) throw macError(410, 'session_closed', 'Capture is closed')
|
|
2597
|
+
const chunkIndex = Number(req.query.chunkIndex)
|
|
2598
|
+
validateChunkIndex(chunkIndex)
|
|
2599
|
+
const elapsed = Number(req.query.elapsed)
|
|
2600
|
+
if (!Number.isFinite(elapsed) || elapsed < 0 || elapsed > 24 * 60 * 60 * 1000) throw macError(400, 'invalid_elapsed', 'Valid capture elapsed time required')
|
|
2601
|
+
if (macChunkLocks.has(id)) throw macError(409, 'mac_chunk_in_progress', 'Another chunk is processing; retry the same chunk after its receipt')
|
|
2602
|
+
macChunkLocks.add(id); locked = true
|
|
2603
|
+
lease = acquireRecordingChunkLease({ allowDuringDrain: sessions.has(id) })
|
|
2604
|
+
const audio = await readBoundedRawBody(req, MAC_AUDIO.maxChunkBytes)
|
|
2605
|
+
const duration = validateMacWav(audio)
|
|
2606
|
+
if (elapsed < duration) throw macError(400, 'invalid_elapsed', 'Chunk end precedes its duration')
|
|
2607
|
+
const session = getSession(id)
|
|
2608
|
+
persistMacChunkReceipt(id, { chunkIndex, endElapsed: elapsed, durationMs: duration, audioSha256: sha256Hex(audio) })
|
|
2609
|
+
const prior = session.chunks[chunkIndex] ?? session.emptyCompletions?.[String(chunkIndex)]
|
|
2610
|
+
if (prior?.audioSha256 && prior.audioSha256 !== sha256Hex(audio)) throw macError(409, 'mac_chunk_conflict', 'Chunk index already holds different audio')
|
|
2611
|
+
res.json(await processStreamChunk({ sessionId: id, chunkIndex, clientSpeaker: 'Ext', audioBuffer: audio, clientElapsed: elapsed - duration, audioDurationMs: duration, startTimeOverride: recording.startTime }))
|
|
2612
|
+
} catch (error) { sendStreamError(res, error) } finally { if (locked) macChunkLocks.delete(id); lease?.release() }
|
|
2613
|
+
})
|
|
2614
|
+
|
|
2510
2615
|
transcribeStreamRouter.post('/transcribe-stream', async (req, res) => {
|
|
2511
2616
|
let maintenanceLease: MaintenanceWorkLease | undefined
|
|
2512
2617
|
try {
|
|
2513
2618
|
// Reject a wrong Mac before consuming or persisting any upload bytes.
|
|
2514
2619
|
assertPinnedServerIdentity(req.get('X-COS-Server-Instance'), req.query.serverInstanceId)
|
|
2515
2620
|
const sessionId = (req.query.sessionId as string) || `g2_${Date.now()}`
|
|
2621
|
+
if (sessionId.startsWith('mac_')) throw macError(400, 'mac_transport_required', 'Use the Mac recording transport for Mac captures')
|
|
2516
2622
|
maintenanceLease = acquireRecordingChunkLease({
|
|
2517
2623
|
allowDuringDrain: sessions.has(sessionId),
|
|
2518
2624
|
})
|
|
@@ -20,6 +20,11 @@ import {
|
|
|
20
20
|
} from '../lib/maintenance-lifecycle.js'
|
|
21
21
|
|
|
22
22
|
export const transcribeRouter = Router()
|
|
23
|
+
// A Mac client checks this before transferring microphone audio. Older servers must not
|
|
24
|
+
// silently ignore the requested engine and use a configured cloud fallback.
|
|
25
|
+
transcribeRouter.get('/transcribe/capabilities', (_req, res) => {
|
|
26
|
+
res.json({ desktopDictationVersion: 1, requiredEngines: ['parakeet-ultra'], localOnly: true, dictionary: 'cos' })
|
|
27
|
+
})
|
|
23
28
|
|
|
24
29
|
function resolveMode(req: { body?: { mode?: string }; query?: { mode?: string | string[] } }) {
|
|
25
30
|
return resolveTranscribeMode(
|
|
@@ -34,8 +39,18 @@ transcribeRouter.post('/transcribe', async (req, res) => {
|
|
|
34
39
|
try {
|
|
35
40
|
maintenanceLease = acquireMaintenanceWork('one_shot_transcription')
|
|
36
41
|
const chunks: Buffer[] = []
|
|
42
|
+
const requiredEngine = req.query.requiredEngine
|
|
43
|
+
if (requiredEngine !== undefined && requiredEngine !== 'parakeet-ultra') {
|
|
44
|
+
return res.status(400).json({ error: 'invalid_required_engine' })
|
|
45
|
+
}
|
|
46
|
+
let size = 0
|
|
37
47
|
for await (const chunk of req) {
|
|
38
|
-
|
|
48
|
+
const part = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
49
|
+
size += part.length
|
|
50
|
+
if (size > (requiredEngine ? 3_840_044 : 25 * 1024 * 1024)) {
|
|
51
|
+
return res.status(413).json({ error: 'audio_too_large' })
|
|
52
|
+
}
|
|
53
|
+
chunks.push(part)
|
|
39
54
|
}
|
|
40
55
|
const audioBuffer = Buffer.concat(chunks)
|
|
41
56
|
|
|
@@ -43,7 +58,10 @@ transcribeRouter.post('/transcribe', async (req, res) => {
|
|
|
43
58
|
return res.status(400).json({ error: 'audio too short' })
|
|
44
59
|
}
|
|
45
60
|
|
|
46
|
-
const result = await transcribeAudioBuffer(audioBuffer, {
|
|
61
|
+
const result = await transcribeAudioBuffer(audioBuffer, {
|
|
62
|
+
mode: resolveMode(req),
|
|
63
|
+
...(requiredEngine ? { requiredEngine, policy: 'local-only' as const, affectsCircuit: false } : {}),
|
|
64
|
+
})
|
|
47
65
|
console.log(`[perf] /transcribe: ${result.elapsedMs.toFixed(1)}ms | mode=${result.mode} | ${result.backend}${result.engineReason ? ` (${result.engineReason})` : ''} | ${result.audioBytes}b | ${result.text.length} chars`)
|
|
48
66
|
res.json({
|
|
49
67
|
text: result.text,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Router } from 'express'
|
|
2
|
-
import { parseWorkChatInput, parseWorkChatAnswer, WORK_CHAT_SYSTEM
|
|
2
|
+
import { parseWorkChatInput, parseWorkChatAnswer, WORK_CHAT_SYSTEM, WORK_CHAT_PLANNERS, WORK_CHAT_EFFORTS, WORK_CHAT_DEFAULT_EFFORT,
|
|
3
|
+
WORK_CHAT_OUTPUT_TOKENS, WORK_CHAT_THINKING_TOKENS, WORK_CHAT_DEADLINE_MS, WORK_CHAT_MAX_ASSIGNMENTS, WORK_CHAT_MAX_ITEMS } from '../lib/work-chat.js'
|
|
3
4
|
import { runMeetingAskModel } from '../lib/meeting-ask-runner.js'
|
|
4
5
|
import { acquireMaintenanceWork, maintenanceAdmissionsOpen } from '../lib/maintenance-lifecycle.js'
|
|
5
6
|
export function createWorkChatRouter(deps = {run:runMeetingAskModel, admissions:maintenanceAdmissionsOpen, lease:()=>acquireMaintenanceWork('work_chat')}) {
|
|
6
7
|
const router=Router(); let running=false
|
|
7
8
|
router.use('/work/chat', (_req,res,next)=>{res.set('Cache-Control','private, no-store');next()})
|
|
8
|
-
router.get('/work/chat',(_req,res)=>res.json({protocolVersion:1,planner:'sonnet',automaticProviders:['claude','codex'],maxAssignments:
|
|
9
|
+
router.get('/work/chat',(_req,res)=>res.json({protocolVersion:1,planner:'sonnet',planners:WORK_CHAT_PLANNERS,effort:WORK_CHAT_DEFAULT_EFFORT,efforts:WORK_CHAT_EFFORTS,automaticProviders:['claude','codex'],maxAssignments:WORK_CHAT_MAX_ASSIGNMENTS,contextItems:true,maxContextItems:WORK_CHAT_MAX_ITEMS}))
|
|
9
10
|
router.post('/work/chat',async(req,res)=>{
|
|
10
11
|
let input
|
|
11
12
|
try { if (Buffer.byteLength(JSON.stringify(req.body))>100000) throw new Error(); input=parseWorkChatInput(req.body) }
|
|
@@ -17,9 +18,12 @@ export function createWorkChatRouter(deps = {run:runMeetingAskModel, admissions:
|
|
|
17
18
|
res.on('close',abort)
|
|
18
19
|
try {
|
|
19
20
|
lease=deps.lease()
|
|
20
|
-
|
|
21
|
+
// Planner and effort select how the model runs; they are not evidence, so they never reach the prompt.
|
|
22
|
+
const {planner='sonnet',effort=WORK_CHAT_DEFAULT_EFFORT,...prompt}=input
|
|
23
|
+
const out=await deps.run({engine:'claude',model:planner},JSON.stringify(prompt),WORK_CHAT_SYSTEM,controller.signal,undefined,{caller:'work_chat_'+input.mode},
|
|
24
|
+
{effort,maxOutputTokens:WORK_CHAT_OUTPUT_TOKENS,maxThinkingTokens:WORK_CHAT_THINKING_TOKENS[effort],deadlineMs:WORK_CHAT_DEADLINE_MS})
|
|
21
25
|
const answer=parseWorkChatAnswer(out.text,input)
|
|
22
|
-
if (!res.destroyed) return res.json({protocolVersion:1,...answer,model:out.model
|
|
26
|
+
if (!res.destroyed) return res.json({protocolVersion:1,...answer,model:out.model??`claude:${planner} (requested)`,effort})
|
|
23
27
|
} catch {if(!res.destroyed) return res.status(503).json({error:'work_chat_unavailable',message:'No assignment was dispatched. Your saved request can be retried.'})}
|
|
24
28
|
finally {res.off('close',abort);lease?.release();running=false}
|
|
25
29
|
})
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { Router } from 'express'
|
|
9
9
|
import { listBoard } from '../lib/task-store.js'
|
|
10
10
|
import { isSafeDomainName } from '../lib/domains.js'
|
|
11
|
-
import { queryLength,
|
|
11
|
+
import { queryLength, sharedWorkSearchDecisionClient, selectCandidates, unavailable, workSearchEnabled, WorkSearcher, WORK_SEARCH_LIMITS,
|
|
12
12
|
WORK_SEARCH_SCOPES, type WorkSearchResult, type WorkSearchScope } from '../lib/work-search.js'
|
|
13
13
|
|
|
14
14
|
export interface WorkSearchRouteDependencies {
|
|
@@ -23,7 +23,7 @@ const TASK_ID = /^[a-f0-9]{12}$/
|
|
|
23
23
|
export const WORK_SEARCH_LOG_EVERY_MS = 10 * 60_000
|
|
24
24
|
|
|
25
25
|
export function createWorkSearchRouter(overrides: Partial<WorkSearchRouteDependencies> = {}): Router {
|
|
26
|
-
const deps: WorkSearchRouteDependencies = { searcher: new WorkSearcher(
|
|
26
|
+
const deps: WorkSearchRouteDependencies = { searcher: new WorkSearcher(sharedWorkSearchDecisionClient()), list: () => listBoard(),
|
|
27
27
|
enabled: () => workSearchEnabled(), ...overrides }
|
|
28
28
|
const router = Router()
|
|
29
29
|
const logged = new Map<string, number>()
|