@gotcos/glasses-server 6.18.3 → 6.18.5
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/.env.example +6 -2
- package/CHANGELOG.md +20 -0
- package/README.md +4 -1
- package/package.json +1 -1
- package/server/lib/codex-bridge.ts +13 -3
- package/server/lib/meeting-batch-progress.ts +221 -0
- package/server/lib/meeting-batch-transcribe.ts +46 -2
- package/server/routes/health.ts +3 -0
package/.env.example
CHANGED
|
@@ -53,9 +53,13 @@ BIND_HOST=0.0.0.0
|
|
|
53
53
|
# COS_CODEX_MODEL=
|
|
54
54
|
# COS_CODEX_REASONING_EFFORT=high # low | medium | high | xhigh | max | ultra
|
|
55
55
|
#
|
|
56
|
-
# Codex remains read-only by default.
|
|
56
|
+
# Codex remains read-only by default. Broader trust opt-in (workdir writes +
|
|
57
|
+
# outbound network inside that sandbox — needed for Gmail API / HTTPS):
|
|
57
58
|
# COS_CODEX_SANDBOX=workspace-write
|
|
58
|
-
#
|
|
59
|
+
# Pair with ~/.codex/config.toml [sandbox_workspace_write] network_access = true
|
|
60
|
+
# for interactive Codex CLI; the managed glasses server also passes
|
|
61
|
+
# -c sandbox_workspace_write.network_access=true when workspace-write is set.
|
|
62
|
+
|
|
59
63
|
# Claude preserves COS's established trusted-machine behavior by default.
|
|
60
64
|
# Security-conscious installs can remove the permission bypass and restrict
|
|
61
65
|
# Claude to COS's explicit per-query tool allowlist. Undeclared tools fail
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## 6.18.5
|
|
2
|
+
|
|
3
|
+
- **Codex workspace-write now includes outbound network.** `workspace-write`
|
|
4
|
+
alone still blocked HTTPS (Gmail API, etc.) because Codex defaults
|
|
5
|
+
`sandbox_workspace_write.network_access` to off. When
|
|
6
|
+
`COS_CODEX_SANDBOX=workspace-write`, the managed server now passes
|
|
7
|
+
`-c sandbox_workspace_write.network_access=true` and the capability header
|
|
8
|
+
states HTTPS is available. Pair with the same key in `~/.codex/config.toml`
|
|
9
|
+
for interactive Codex CLI. Still not `danger-full-access` — writes stay
|
|
10
|
+
inside the workdir. Prefer COS `email_cache` / `email_gmail_api` over
|
|
11
|
+
approval-gated Google Workspace connector sends from glasses.
|
|
12
|
+
|
|
13
|
+
## 6.18.4
|
|
14
|
+
|
|
15
|
+
- **Meeting sync progress on `/api/health`.** Post-meeting HQ polish writes
|
|
16
|
+
`_batch_progress.json` under `pending-batch/<meetingId>/` and publishes
|
|
17
|
+
`meeting_sync` on health (`active`, `percent`, `label`, `blocksRestart`,
|
|
18
|
+
per-meeting rows). COS Control 0.3.0+ shows this as a status row so Update /
|
|
19
|
+
Restart drain is no longer a black box during long Whisper batch jobs.
|
|
20
|
+
|
|
1
21
|
## 6.18.3
|
|
2
22
|
|
|
3
23
|
> Ships as 6.18.3. There is no published 6.18.2 — that version number was bumped
|
package/README.md
CHANGED
|
@@ -70,7 +70,10 @@ without silently losing completed replies.
|
|
|
70
70
|
> omit the execution mode default to Ask.
|
|
71
71
|
> Existing `COS_CODEX_MODEL` / `COS_CODEX_REASONING_EFFORT` settings remain
|
|
72
72
|
> supported on the migrated Frontier slot; leave them blank for auto-latest.
|
|
73
|
-
> Codex runs **sandboxed read-only** by default
|
|
73
|
+
> Codex runs **sandboxed read-only** by default. Set
|
|
74
|
+
> `COS_CODEX_SANDBOX=workspace-write` for workdir writes + outbound network
|
|
75
|
+
> (`sandbox_workspace_write.network_access=true` is passed by the managed server
|
|
76
|
+
> and should also be set in `~/.codex/config.toml` for interactive Codex).
|
|
74
77
|
> **Claude is the most permissive provider by default.** It runs with
|
|
75
78
|
> `--dangerously-skip-permissions`, so a glasses query on the Claude/Opus path can
|
|
76
79
|
> run shell commands and read, edit, and write files on this Mac without prompting
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.18.
|
|
3
|
+
"version": "6.18.5",
|
|
4
4
|
"description": "COS Glasses \u2014 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": {
|
|
@@ -83,14 +83,24 @@ const PHASE_LABELS: Record<Phase, string> = {
|
|
|
83
83
|
// Sandbox policy for `codex exec`. This public server NEVER runs codex
|
|
84
84
|
// unsandboxed — that would let a remote glasses query execute arbitrary commands
|
|
85
85
|
// on the host. Default: read-only (safe for chat). COS_CODEX_SANDBOX=workspace-write
|
|
86
|
-
// permits writes within the working directory only
|
|
86
|
+
// permits writes within the working directory only, and enables outbound network
|
|
87
|
+
// inside that sandbox (Gmail API, HTTPS fetches) via
|
|
88
|
+
// `sandbox_workspace_write.network_access=true`. Full host filesystem access is
|
|
87
89
|
// intentionally not exposed by this server.
|
|
88
90
|
function codexSandboxMode(): 'workspace-write' | 'read-only' {
|
|
89
91
|
return process.env.COS_CODEX_SANDBOX === 'workspace-write' ? 'workspace-write' : 'read-only'
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
function codexSandboxArgs(): string[] {
|
|
93
|
-
|
|
95
|
+
const args = ['--sandbox', codexSandboxMode(), '--skip-git-repo-check']
|
|
96
|
+
// workspace-write keeps the filesystem boundary but leaves network OFF by
|
|
97
|
+
// default in Codex. Glasses outbound Gmail (and similar HTTPS) needs this
|
|
98
|
+
// explicit opt-in — without it agents correctly report "cannot reach
|
|
99
|
+
// gmail.googleapis.com" even when workspace writes work.
|
|
100
|
+
if (codexSandboxMode() === 'workspace-write') {
|
|
101
|
+
args.push('-c', 'sandbox_workspace_write.network_access=true')
|
|
102
|
+
}
|
|
103
|
+
return args
|
|
94
104
|
}
|
|
95
105
|
|
|
96
106
|
/**
|
|
@@ -101,7 +111,7 @@ function codexSandboxArgs(): string[] {
|
|
|
101
111
|
function codexCapabilityPrompt(): string {
|
|
102
112
|
if (codexSandboxMode() === 'workspace-write') {
|
|
103
113
|
return `TOOL CAPABILITY CONTRACT:
|
|
104
|
-
You are an AGENT model running \`codex exec --sandbox workspace-write\`. Reads, searches, shell commands, and writes inside the working directory are available to you whether or not any list names them. Never claim a tool is unavailable based on that list or on any header — PROBE first with one real call; only an attempted call that actually failed is evidence a capability is missing. Writes outside the working directory are denied by the sandbox; say that plainly if you hit it.
|
|
114
|
+
You are an AGENT model running \`codex exec --sandbox workspace-write\` with \`sandbox_workspace_write.network_access=true\`. Reads, searches, shell commands, outbound HTTPS (including Gmail API / cos_python network calls), and writes inside the working directory are available to you whether or not any list names them. Never claim a tool is unavailable based on that list or on any header — PROBE first with one real call; only an attempted call that actually failed is evidence a capability is missing. Writes outside the working directory are denied by the sandbox; say that plainly if you hit it. Prefer the COS Gmail API script path (\`email_cache\` / \`email_gmail_api\` with confirm=True) over approval-gated Google Workspace connector sends.
|
|
105
115
|
${TOOL_HONESTY_CLAUSE}
|
|
106
116
|
${UNTRUSTED_CONTENT_CLAUSE}`
|
|
107
117
|
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Meeting HQ polish progress for COS Control / health.
|
|
2
|
+
// Written next to pending-batch audio so a draining Update can show % complete
|
|
3
|
+
// instead of a silent "degraded" row while Whisper chews through a long save.
|
|
4
|
+
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, unlinkSync } from 'node:fs'
|
|
6
|
+
import { basename, join } from 'node:path'
|
|
7
|
+
import { dataPath } from './data-dir.js'
|
|
8
|
+
|
|
9
|
+
export const BATCH_PROGRESS_FILENAME = '_batch_progress.json'
|
|
10
|
+
export const BATCH_PENDING_MARKER = '_batch_pending.marker'
|
|
11
|
+
|
|
12
|
+
export type MeetingBatchPhase =
|
|
13
|
+
| 'queued'
|
|
14
|
+
| 'hq_polish'
|
|
15
|
+
| 'quality_check'
|
|
16
|
+
| 'persisting'
|
|
17
|
+
| 'done'
|
|
18
|
+
|
|
19
|
+
export interface MeetingBatchProgress {
|
|
20
|
+
schemaVersion: 1
|
|
21
|
+
meetingId: string
|
|
22
|
+
phase: MeetingBatchPhase
|
|
23
|
+
segmentsDone: number
|
|
24
|
+
segmentsTotal: number
|
|
25
|
+
chunkFiles?: number
|
|
26
|
+
updatedAt: string
|
|
27
|
+
startedAt: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MeetingSyncMeeting {
|
|
31
|
+
meetingId: string
|
|
32
|
+
phase: MeetingBatchPhase | 'pending'
|
|
33
|
+
percent: number | null
|
|
34
|
+
segmentsDone: number | null
|
|
35
|
+
segmentsTotal: number | null
|
|
36
|
+
chunkFiles: number
|
|
37
|
+
label: string
|
|
38
|
+
updatedAt: string | null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface MeetingSyncSnapshot {
|
|
42
|
+
active: boolean
|
|
43
|
+
percent: number | null
|
|
44
|
+
label: string
|
|
45
|
+
blocksRestart: boolean
|
|
46
|
+
meetings: MeetingSyncMeeting[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function pendingBatchRoot(): string {
|
|
50
|
+
return dataPath('pending-batch')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function clampPercent(done: number, total: number): number {
|
|
54
|
+
if (total <= 0) return 0
|
|
55
|
+
return Math.max(0, Math.min(100, Math.round((done / total) * 100)))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function labelFor(meeting: Omit<MeetingSyncMeeting, 'label'>): string {
|
|
59
|
+
if (meeting.percent != null && meeting.segmentsTotal != null && meeting.segmentsTotal > 0) {
|
|
60
|
+
return `HQ polish ${meeting.percent}% (${meeting.segmentsDone}/${meeting.segmentsTotal})`
|
|
61
|
+
}
|
|
62
|
+
if (meeting.chunkFiles > 0) {
|
|
63
|
+
return `HQ polish · ${meeting.chunkFiles} chunk${meeting.chunkFiles === 1 ? '' : 's'}`
|
|
64
|
+
}
|
|
65
|
+
return 'HQ polish · pending'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function writeMeetingBatchProgress(
|
|
69
|
+
audioDir: string,
|
|
70
|
+
input: {
|
|
71
|
+
phase: MeetingBatchPhase
|
|
72
|
+
segmentsDone: number
|
|
73
|
+
segmentsTotal: number
|
|
74
|
+
meetingId?: string
|
|
75
|
+
startedAt?: string
|
|
76
|
+
},
|
|
77
|
+
): void {
|
|
78
|
+
const meetingId = input.meetingId ?? basename(audioDir)
|
|
79
|
+
const path = join(audioDir, BATCH_PROGRESS_FILENAME)
|
|
80
|
+
let startedAt = input.startedAt
|
|
81
|
+
if (!startedAt && existsSync(path)) {
|
|
82
|
+
try {
|
|
83
|
+
const prior = JSON.parse(readFileSync(path, 'utf8')) as MeetingBatchProgress
|
|
84
|
+
if (typeof prior.startedAt === 'string') startedAt = prior.startedAt
|
|
85
|
+
} catch { /* replace */ }
|
|
86
|
+
}
|
|
87
|
+
const payload: MeetingBatchProgress = {
|
|
88
|
+
schemaVersion: 1,
|
|
89
|
+
meetingId,
|
|
90
|
+
phase: input.phase,
|
|
91
|
+
segmentsDone: Math.max(0, input.segmentsDone),
|
|
92
|
+
segmentsTotal: Math.max(0, input.segmentsTotal),
|
|
93
|
+
updatedAt: new Date().toISOString(),
|
|
94
|
+
startedAt: startedAt ?? new Date().toISOString(),
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const wavs = readdirSync(audioDir).filter(name => name.endsWith('.wav')).length
|
|
98
|
+
payload.chunkFiles = wavs
|
|
99
|
+
} catch { /* optional */ }
|
|
100
|
+
try {
|
|
101
|
+
writeFileSync(path, `${JSON.stringify(payload)}\n`, { encoding: 'utf8', mode: 0o600 })
|
|
102
|
+
} catch {
|
|
103
|
+
// Progress is observability only — never fail HQ polish for a status write.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function clearMeetingBatchProgress(audioDir: string): void {
|
|
108
|
+
const path = join(audioDir, BATCH_PROGRESS_FILENAME)
|
|
109
|
+
try {
|
|
110
|
+
if (existsSync(path)) unlinkSync(path)
|
|
111
|
+
} catch { /* ignore */ }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readProgressFile(dir: string): MeetingBatchProgress | null {
|
|
115
|
+
const path = join(dir, BATCH_PROGRESS_FILENAME)
|
|
116
|
+
if (!existsSync(path)) return null
|
|
117
|
+
try {
|
|
118
|
+
const raw = JSON.parse(readFileSync(path, 'utf8')) as MeetingBatchProgress
|
|
119
|
+
if (raw?.schemaVersion !== 1) return null
|
|
120
|
+
if (typeof raw.meetingId !== 'string') return null
|
|
121
|
+
if (typeof raw.segmentsTotal !== 'number' || typeof raw.segmentsDone !== 'number') return null
|
|
122
|
+
return raw
|
|
123
|
+
} catch {
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function markerFresh(dir: string, maxAgeMs = 15 * 60_000): boolean {
|
|
129
|
+
const marker = join(dir, BATCH_PENDING_MARKER)
|
|
130
|
+
if (!existsSync(marker)) return false
|
|
131
|
+
try {
|
|
132
|
+
return Date.now() - statSync(marker).mtimeMs <= maxAgeMs
|
|
133
|
+
} catch {
|
|
134
|
+
return false
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Snapshot of pending HQ polish work for /api/health and COS Control. */
|
|
139
|
+
export function getMeetingSyncSnapshot(
|
|
140
|
+
root: string = pendingBatchRoot(),
|
|
141
|
+
): MeetingSyncSnapshot {
|
|
142
|
+
const meetings: MeetingSyncMeeting[] = []
|
|
143
|
+
if (!existsSync(root)) {
|
|
144
|
+
return { active: false, percent: null, label: 'Idle', blocksRestart: false, meetings }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let dirs: string[] = []
|
|
148
|
+
try {
|
|
149
|
+
dirs = readdirSync(root).filter(name => {
|
|
150
|
+
try {
|
|
151
|
+
return statSync(join(root, name)).isDirectory()
|
|
152
|
+
} catch {
|
|
153
|
+
return false
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
} catch {
|
|
157
|
+
return { active: false, percent: null, label: 'Idle', blocksRestart: false, meetings }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const name of dirs) {
|
|
161
|
+
const dir = join(root, name)
|
|
162
|
+
const progress = readProgressFile(dir)
|
|
163
|
+
let chunkFiles = 0
|
|
164
|
+
try {
|
|
165
|
+
chunkFiles = readdirSync(dir).filter(f => f.endsWith('.wav')).length
|
|
166
|
+
} catch { /* ignore */ }
|
|
167
|
+
|
|
168
|
+
const active = markerFresh(dir) || progress != null
|
|
169
|
+
if (!active && chunkFiles === 0) continue
|
|
170
|
+
|
|
171
|
+
if (progress) {
|
|
172
|
+
const percent = progress.segmentsTotal > 0
|
|
173
|
+
? clampPercent(progress.segmentsDone, progress.segmentsTotal)
|
|
174
|
+
: null
|
|
175
|
+
const row: Omit<MeetingSyncMeeting, 'label'> = {
|
|
176
|
+
meetingId: progress.meetingId || name,
|
|
177
|
+
phase: progress.phase,
|
|
178
|
+
percent,
|
|
179
|
+
segmentsDone: progress.segmentsTotal > 0 ? progress.segmentsDone : null,
|
|
180
|
+
segmentsTotal: progress.segmentsTotal > 0 ? progress.segmentsTotal : null,
|
|
181
|
+
chunkFiles: progress.chunkFiles ?? chunkFiles,
|
|
182
|
+
updatedAt: progress.updatedAt,
|
|
183
|
+
}
|
|
184
|
+
meetings.push({ ...row, label: labelFor(row) })
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!markerFresh(dir) && chunkFiles === 0) continue
|
|
189
|
+
const row: Omit<MeetingSyncMeeting, 'label'> = {
|
|
190
|
+
meetingId: name,
|
|
191
|
+
phase: 'pending',
|
|
192
|
+
percent: null,
|
|
193
|
+
segmentsDone: null,
|
|
194
|
+
segmentsTotal: null,
|
|
195
|
+
chunkFiles,
|
|
196
|
+
updatedAt: null,
|
|
197
|
+
}
|
|
198
|
+
meetings.push({ ...row, label: labelFor(row) })
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (meetings.length === 0) {
|
|
202
|
+
return { active: false, percent: null, label: 'Idle', blocksRestart: false, meetings }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const withPercent = meetings.filter(m => m.percent != null)
|
|
206
|
+
const percent = withPercent.length === meetings.length
|
|
207
|
+
? Math.round(withPercent.reduce((sum, m) => sum + (m.percent ?? 0), 0) / meetings.length)
|
|
208
|
+
: null
|
|
209
|
+
|
|
210
|
+
const label = meetings.length === 1
|
|
211
|
+
? meetings[0]!.label
|
|
212
|
+
: `${meetings.length} meetings syncing` + (percent != null ? ` · ${percent}%` : '')
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
active: true,
|
|
216
|
+
percent,
|
|
217
|
+
label,
|
|
218
|
+
blocksRestart: true,
|
|
219
|
+
meetings,
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
// The candidate is never canonical until batch-transcript-quality accepts it.
|
|
4
4
|
|
|
5
5
|
import { existsSync, readFileSync, readdirSync, utimesSync, writeFileSync } from 'node:fs'
|
|
6
|
-
import { join, resolve } from 'node:path'
|
|
6
|
+
import { basename, join, resolve } from 'node:path'
|
|
7
7
|
import { enhanceAudio } from './audio-enhance.js'
|
|
8
8
|
import { transcribeHighQuality, type WhisperWord } from './whisper-local.js'
|
|
9
9
|
import { isMetalBatchPreempted } from './whisper-metal-gate.js'
|
|
10
|
+
import {
|
|
11
|
+
clearMeetingBatchProgress,
|
|
12
|
+
writeMeetingBatchProgress,
|
|
13
|
+
} from './meeting-batch-progress.js'
|
|
10
14
|
import type { IndexedTranscriptChunk } from '../routes/transcribe-stream.js'
|
|
11
15
|
import {
|
|
12
16
|
evaluateBatchQuality,
|
|
@@ -167,6 +171,13 @@ async function transcribeSegments(
|
|
|
167
171
|
entries: IndexedTranscriptChunk[],
|
|
168
172
|
): Promise<BatchResult[]> {
|
|
169
173
|
const results: BatchResult[] = []
|
|
174
|
+
const meetingId = basename(audioDir)
|
|
175
|
+
writeMeetingBatchProgress(audioDir, {
|
|
176
|
+
phase: 'hq_polish',
|
|
177
|
+
segmentsDone: 0,
|
|
178
|
+
segmentsTotal: segments.length,
|
|
179
|
+
meetingId,
|
|
180
|
+
})
|
|
170
181
|
for (const segment of segments) {
|
|
171
182
|
try {
|
|
172
183
|
refreshPendingLease(audioDir)
|
|
@@ -202,11 +213,23 @@ async function transcribeSegments(
|
|
|
202
213
|
speakerWords: mapWordsToSpeakers(words, segment, entries),
|
|
203
214
|
})
|
|
204
215
|
refreshPendingLease(audioDir)
|
|
216
|
+
writeMeetingBatchProgress(audioDir, {
|
|
217
|
+
phase: 'hq_polish',
|
|
218
|
+
segmentsDone: results.length,
|
|
219
|
+
segmentsTotal: segments.length,
|
|
220
|
+
meetingId,
|
|
221
|
+
})
|
|
205
222
|
} catch (error) {
|
|
206
223
|
console.error(
|
|
207
224
|
`[meeting-batch] Segment ${segment.startChunkIdx}-${segment.endChunkIdx} failed: `
|
|
208
225
|
+ `${error instanceof Error ? error.message : String(error)}`,
|
|
209
226
|
)
|
|
227
|
+
writeMeetingBatchProgress(audioDir, {
|
|
228
|
+
phase: 'hq_polish',
|
|
229
|
+
segmentsDone: results.length,
|
|
230
|
+
segmentsTotal: segments.length,
|
|
231
|
+
meetingId,
|
|
232
|
+
})
|
|
210
233
|
}
|
|
211
234
|
}
|
|
212
235
|
return results
|
|
@@ -223,13 +246,22 @@ export function runMeetingBatchPipeline(
|
|
|
223
246
|
// Lease immediately, including time spent behind another HQ decoder. Without
|
|
224
247
|
// this, the two-hour cleanup could delete a queued meeting before it starts.
|
|
225
248
|
refreshPendingLease(audioDir)
|
|
249
|
+
writeMeetingBatchProgress(audioDir, {
|
|
250
|
+
phase: 'queued',
|
|
251
|
+
segmentsDone: 0,
|
|
252
|
+
segmentsTotal: 0,
|
|
253
|
+
meetingId: basename(audioDir),
|
|
254
|
+
})
|
|
226
255
|
const lease = setInterval(() => refreshPendingLease(audioDir), 60_000)
|
|
227
256
|
lease.unref()
|
|
228
257
|
const job = batchQueueTail.then(() => runMeetingBatchPipelineNow(
|
|
229
258
|
audioDir,
|
|
230
259
|
entries,
|
|
231
260
|
streamingWordCount,
|
|
232
|
-
)).finally(() =>
|
|
261
|
+
)).finally(() => {
|
|
262
|
+
clearInterval(lease)
|
|
263
|
+
clearMeetingBatchProgress(audioDir)
|
|
264
|
+
})
|
|
233
265
|
batchQueueTail = job.then(() => undefined, () => undefined)
|
|
234
266
|
return job
|
|
235
267
|
}
|
|
@@ -248,7 +280,19 @@ async function runMeetingBatchPipelineNow(
|
|
|
248
280
|
const segments = segmentTranscriptChunks(entries)
|
|
249
281
|
if (segments.length === 0) return { transcriptionQuality: 'streaming' }
|
|
250
282
|
|
|
283
|
+
writeMeetingBatchProgress(audioDir, {
|
|
284
|
+
phase: 'hq_polish',
|
|
285
|
+
segmentsDone: 0,
|
|
286
|
+
segmentsTotal: segments.length,
|
|
287
|
+
meetingId: basename(audioDir),
|
|
288
|
+
})
|
|
251
289
|
const batchSegments = await transcribeSegments(audioDir, segments, entries)
|
|
290
|
+
writeMeetingBatchProgress(audioDir, {
|
|
291
|
+
phase: 'quality_check',
|
|
292
|
+
segmentsDone: segments.length,
|
|
293
|
+
segmentsTotal: segments.length,
|
|
294
|
+
meetingId: basename(audioDir),
|
|
295
|
+
})
|
|
252
296
|
const batchTranscript = batchSegments.map(result => result.text).join(' ')
|
|
253
297
|
const qualityReport = evaluateBatchQuality(batchSegments, streamingWordCount)
|
|
254
298
|
if (!qualityReport.accepted) {
|
package/server/routes/health.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { getServerGenerationId } from '../lib/managed-runtime.js'
|
|
|
37
37
|
import { maintenanceLifecycle } from '../lib/maintenance-lifecycle.js'
|
|
38
38
|
import { getLocalTtsHealth, refreshLocalTtsHealth } from '../lib/tts-local.js'
|
|
39
39
|
import { liveCuesCapability } from '../lib/live-cues-capability.js'
|
|
40
|
+
import { getMeetingSyncSnapshot } from '../lib/meeting-batch-progress.js'
|
|
40
41
|
|
|
41
42
|
export const healthRouter = Router()
|
|
42
43
|
|
|
@@ -259,6 +260,7 @@ healthRouter.get('/health', async (_req, res) => {
|
|
|
259
260
|
// agent binary paths stay on the authenticated /api/models surface.
|
|
260
261
|
const cursorSnapshot = getCursorModelCatalogSnapshot()
|
|
261
262
|
const { agentBinary: _cursorAgentBinary, ...cursor_models } = cursorSnapshot
|
|
263
|
+
const meeting_sync = getMeetingSyncSnapshot()
|
|
262
264
|
res.json({
|
|
263
265
|
...checks,
|
|
264
266
|
server_version: managedServerVersion(),
|
|
@@ -273,6 +275,7 @@ healthRouter.get('/health', async (_req, res) => {
|
|
|
273
275
|
tts_local,
|
|
274
276
|
codex_models,
|
|
275
277
|
cursor_models,
|
|
278
|
+
meeting_sync,
|
|
276
279
|
capabilities: {
|
|
277
280
|
transcription: { ...transcription, hq: transcriptionHq },
|
|
278
281
|
recovery,
|