@gotcos/glasses-server 6.44.14 → 6.44.16
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,38 @@
|
|
|
1
|
+
## 6.44.16
|
|
2
|
+
|
|
3
|
+
A G2 recording keeps its session id when its sidecar only exists in the server's own recordings folder.
|
|
4
|
+
|
|
5
|
+
- Chelsie and Queen, 2026-09-08: "Meetings to review is empty, and it says 3
|
|
6
|
+
recent meetings have no session id." On Miles's Mac the same three-row case
|
|
7
|
+
was three KC-week recordings whose markdown reached the operations tree
|
|
8
|
+
through git while the `.g2-chunks.json` sidecar, which is gitignored there,
|
|
9
|
+
did not; the row lost its session id and Speakers could not open it.
|
|
10
|
+
- `listCosOperationsMeetings` now reads the sidecar from the server's own
|
|
11
|
+
recordings root by the same stem when the copy beside the markdown is
|
|
12
|
+
missing (`recordingsRoot` option, default the data dir). The review lookup
|
|
13
|
+
already resolves a session through the standalone store, so the row opens.
|
|
14
|
+
A meeting with no sidecar anywhere still omits the field rather than
|
|
15
|
+
inventing one.
|
|
16
|
+
|
|
17
|
+
## 6.44.15
|
|
18
|
+
|
|
19
|
+
Voice profiles named after a spoken sentence are repaired at load.
|
|
20
|
+
|
|
21
|
+
- Chelsie 2026-09-08: her `voice-profiles.json` held two profiles whose name
|
|
22
|
+
was the entire enrolment speech ("My Voice My Name Is Chelsie Hodgkiss And I
|
|
23
|
+
Am The Director…", ~600 characters), written by a client older than the
|
|
24
|
+
8/25 guards. Speaker ID was "active" and could never match anything, and the
|
|
25
|
+
file cannot be repaired by hand because the server rewrites it from memory.
|
|
26
|
+
- `normalizeProfileStore` now renames a name that is too long, has more than
|
|
27
|
+
four words, or is three-plus words with sentence punctuation to
|
|
28
|
+
`Unnamed voice N`, keeps every embedding and its provenance, records
|
|
29
|
+
`renamedFrom` (the first 60 characters) and `needsName: true`, and counts it
|
|
30
|
+
in the load repairs (`profilesRenamed`). Legacy short labels ("MU",
|
|
31
|
+
"Speaker 2", "Luke H.") are never touched. `GET /api/voice/profiles` carries
|
|
32
|
+
`needsName` and `renamedFrom` so COS Control's speaker review can offer the
|
|
33
|
+
rename; `POST /api/voice/merge-profiles` folds the placeholder into the
|
|
34
|
+
right person as before.
|
|
35
|
+
|
|
1
36
|
## 6.44.14
|
|
2
37
|
|
|
3
38
|
The Manage sheet's merge, with a preview, a worker and a receipt, plus a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.44.
|
|
3
|
+
"version": "6.44.16",
|
|
4
4
|
"description": "COS Glasses \u2014 self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, Cursor Agent CLI, or local Ollama",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,6 +19,7 @@ import { discoveredDomains, domainAbbreviation as deriveAbbr, isSafeDomainName a
|
|
|
19
19
|
import type { MeetingDetail, MeetingMeta } from './meeting-store.js'
|
|
20
20
|
import { MEETING_SOURCE_MAX_BYTES, meetingDayCountsFromNames, meetingListLimit } from './meeting-store.js'
|
|
21
21
|
import { meetingVoiceReview, parseSidecarListHead } from './meeting-voice-review.js'
|
|
22
|
+
import { dataPath } from './data-dir.js'
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* The four domains of ONE user's COS. Retained as the documented example layout
|
|
@@ -528,9 +529,12 @@ export function listCosOperationsMeetings(options: {
|
|
|
528
529
|
domain?: string
|
|
529
530
|
month?: string
|
|
530
531
|
day?: string
|
|
532
|
+
/** The server's own recordings root; tests point it at a temp dir. */
|
|
533
|
+
recordingsRoot?: string
|
|
531
534
|
} = {}): CosOperationsMeetingMeta[] {
|
|
532
535
|
const operationsDir = resolveCosOperationsDir()
|
|
533
536
|
if (!operationsDir) return []
|
|
537
|
+
const recordingsRoot = options.recordingsRoot ?? dataPath('recordings')
|
|
534
538
|
|
|
535
539
|
const scoped = Boolean(options.month || options.day)
|
|
536
540
|
const limit = meetingListLimit(options.limit, scoped)
|
|
@@ -569,7 +573,17 @@ export function listCosOperationsMeetings(options: {
|
|
|
569
573
|
meta.recordId = `ops:${domain}:${month}:${file}`
|
|
570
574
|
meta.mutable = true
|
|
571
575
|
meta.canonicalRecord = `operations/${domain}/meetings/${month}/${file}`
|
|
572
|
-
|
|
576
|
+
// The sidecar is gitignored in the operations tree, so a G2 recording
|
|
577
|
+
// whose markdown arrived through git or iCloud (three of Miles's KC-week
|
|
578
|
+
// recordings, 2026-09-08) sits there without one and the row loses its
|
|
579
|
+
// session id, which reads as "cannot be reviewed". The server that saved
|
|
580
|
+
// it still holds the sidecar under its own recordings root by the same
|
|
581
|
+
// stem; read that one when the copy beside the markdown is missing.
|
|
582
|
+
let hints = sidecarListHints(monthDir, file)
|
|
583
|
+
if (!hints.sessionId && recordingsRoot) {
|
|
584
|
+
const fallbackDir = join(recordingsRoot, month)
|
|
585
|
+
if (existsSync(fallbackDir)) hints = sidecarListHints(fallbackDir, file)
|
|
586
|
+
}
|
|
573
587
|
if (hints.sessionId) meta.sessionId = hints.sessionId
|
|
574
588
|
if (hints.speakers.length > 0) {
|
|
575
589
|
meta.voiceReview = meetingVoiceReview(hints.speakers, hints.sessionId)
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// by execution rather than by asserting on source text. speaker-embeddings.ts
|
|
12
12
|
// delegates to it and keeps the sherpa-onnx manager in sync.
|
|
13
13
|
|
|
14
|
+
import { checkSpeakerName, type SpeakerNameRejection } from './speaker-name.js'
|
|
14
15
|
import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs'
|
|
15
16
|
import { basename, dirname, join, resolve } from 'node:path'
|
|
16
17
|
import { durableAtomicWriteFileSync, loadJsonOrQuarantine } from './atomic-fs.js'
|
|
@@ -21,6 +22,12 @@ export interface VoiceProfile {
|
|
|
21
22
|
embeddings: number[][]
|
|
22
23
|
/** Provenance: 'manual' | 'fireflies' | 'g2-training' | 'auto:<sessionId>' | … */
|
|
23
24
|
sources?: string[]
|
|
25
|
+
/** The name on disk was a spoken sentence, not a name (an old client's
|
|
26
|
+
* "enroll my voice" fall-through); it was renamed at load and the training
|
|
27
|
+
* kept. COS Control's speaker review shows it for renaming. */
|
|
28
|
+
needsName?: boolean
|
|
29
|
+
/** The first 60 characters of the name it was loaded with. */
|
|
30
|
+
renamedFrom?: string
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
export interface ProfileStore {
|
|
@@ -42,6 +49,9 @@ export interface StoreRepairs {
|
|
|
42
49
|
dimensionMismatch: number
|
|
43
50
|
/** Entries with no usable name, or duplicate names collapsed. */
|
|
44
51
|
profilesDropped: number
|
|
52
|
+
/** Names that were a whole spoken sentence (too long, too many words, or
|
|
53
|
+
* sentence punctuation), renamed to `Unnamed voice N` with the training kept. */
|
|
54
|
+
profilesRenamed: number
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
export function emptyRepairs(): StoreRepairs {
|
|
@@ -51,17 +61,19 @@ export function emptyRepairs(): StoreRepairs {
|
|
|
51
61
|
embeddingsDropped: 0,
|
|
52
62
|
dimensionMismatch: 0,
|
|
53
63
|
profilesDropped: 0,
|
|
64
|
+
profilesRenamed: 0,
|
|
54
65
|
}
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
export function hasRepairs(r: StoreRepairs): boolean {
|
|
58
69
|
return r.sourcesRealigned > 0 || r.sourcesCoerced > 0 || r.embeddingsDropped > 0
|
|
59
|
-
|| r.dimensionMismatch > 0 || r.profilesDropped > 0
|
|
70
|
+
|| r.dimensionMismatch > 0 || r.profilesDropped > 0 || r.profilesRenamed > 0
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
export function describeRepairs(r: StoreRepairs): string {
|
|
63
74
|
const parts: string[] = []
|
|
64
75
|
if (r.profilesDropped) parts.push(`${r.profilesDropped} unusable profile(s)`)
|
|
76
|
+
if (r.profilesRenamed) parts.push(`${r.profilesRenamed} profile(s) named after a spoken sentence, renamed for review`)
|
|
65
77
|
if (r.embeddingsDropped) parts.push(`${r.embeddingsDropped} unusable embedding row(s)`)
|
|
66
78
|
if (r.sourcesRealigned) parts.push(`${r.sourcesRealigned} profile(s) with misaligned sources[]`)
|
|
67
79
|
if (r.sourcesCoerced) parts.push(`${r.sourcesCoerced} null/non-string source slot(s)`)
|
|
@@ -106,11 +118,27 @@ export function normalizeProfileStore(raw: unknown): { store: ProfileStore; repa
|
|
|
106
118
|
if (!Array.isArray(rawProfiles)) return { store: { profiles: [] }, repairs }
|
|
107
119
|
|
|
108
120
|
const byName = new Map<string, VoiceProfile>()
|
|
121
|
+
let unnamed = 0
|
|
109
122
|
for (const candidate of rawProfiles) {
|
|
110
|
-
const
|
|
123
|
+
const rawName = typeof (candidate as VoiceProfile)?.name === 'string'
|
|
111
124
|
? (candidate as VoiceProfile).name.trim()
|
|
112
125
|
: ''
|
|
113
|
-
if (!
|
|
126
|
+
if (!rawName) { repairs.profilesDropped++; continue }
|
|
127
|
+
// Chelsie's store (2026-09-08) held two profiles named with the entire
|
|
128
|
+
// enrolment speech (~600 characters), written by a client older than the
|
|
129
|
+
// 6.8.433 / server 8/25 guards. A name like that can never match a speaker
|
|
130
|
+
// label, and the file cannot be repaired by hand because the server
|
|
131
|
+
// rewrites it from memory. Keep the training, give it a placeholder name,
|
|
132
|
+
// and flag it so COS Control offers a rename. Only the sentence shapes are
|
|
133
|
+
// renamed; legacy short labels ("MU", "Speaker 2") are left exactly as is.
|
|
134
|
+
const junk = junkNameReason(rawName)
|
|
135
|
+
let renamedFrom: string | undefined
|
|
136
|
+
let name = rawName
|
|
137
|
+
if (junk) {
|
|
138
|
+
do { name = `Unnamed voice ${++unnamed}` } while (byName.has(name) || rawProfiles.some(p => (p as VoiceProfile)?.name === name))
|
|
139
|
+
renamedFrom = rawName.slice(0, 60)
|
|
140
|
+
repairs.profilesRenamed++
|
|
141
|
+
}
|
|
114
142
|
|
|
115
143
|
const rawEmbeddings = Array.isArray((candidate as VoiceProfile).embeddings)
|
|
116
144
|
? (candidate as VoiceProfile).embeddings as unknown[]
|
|
@@ -154,12 +182,23 @@ export function normalizeProfileStore(raw: unknown): { store: ProfileStore; repa
|
|
|
154
182
|
existing.sources!.push(...sources)
|
|
155
183
|
continue
|
|
156
184
|
}
|
|
157
|
-
byName.set(name, { name, embeddings, sources })
|
|
185
|
+
byName.set(name, { name, embeddings, sources, ...(renamedFrom ? { needsName: true, renamedFrom } : {}) })
|
|
158
186
|
}
|
|
159
187
|
|
|
160
188
|
return { store: { profiles: [...byName.values()] }, repairs }
|
|
161
189
|
}
|
|
162
190
|
|
|
191
|
+
/** The sentence shapes only: too long, too many words, or sentence punctuation. */
|
|
192
|
+
export function junkNameReason(name: string): SpeakerNameRejection | null {
|
|
193
|
+
const check = checkSpeakerName(name, { ownerLabel: name })
|
|
194
|
+
if (check.ok) return null
|
|
195
|
+
if (check.reason === 'too_long' || check.reason === 'too_many_words') return check.reason
|
|
196
|
+
// Sentence punctuation alone is not enough: "Luke H." is a legitimate legacy
|
|
197
|
+
// alias. Three or more words with punctuation is speech, not a name.
|
|
198
|
+
if (check.reason === 'sentence_like' && name.trim().split(/\s+/).length >= 3) return check.reason
|
|
199
|
+
return null
|
|
200
|
+
}
|
|
201
|
+
|
|
163
202
|
export type StoreLoad = {
|
|
164
203
|
store: ProfileStore
|
|
165
204
|
repairs: StoreRepairs
|
package/server/routes/voice.ts
CHANGED
|
@@ -540,6 +540,10 @@ voiceRouter.get('/voice/profiles', (_req, res) => {
|
|
|
540
540
|
embeddings: p.embeddings.length,
|
|
541
541
|
isOwner: p.name === owner,
|
|
542
542
|
sources: bySource,
|
|
543
|
+
// 6.44.15: a profile whose name on disk was a spoken sentence was
|
|
544
|
+
// renamed at load; the client offers a rename.
|
|
545
|
+
needsName: p.needsName === true,
|
|
546
|
+
...(p.renamedFrom ? { renamedFrom: p.renamedFrom } : {}),
|
|
543
547
|
// Provenance alignment is now an invariant; surfacing it makes a
|
|
544
548
|
// future regression visible instead of silent.
|
|
545
549
|
sourcesAligned: (p.sources?.length ?? 0) === p.embeddings.length,
|