@gotcos/glasses-server 6.44.14 → 6.44.15
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 +19 -0
- package/package.json +1 -1
- package/server/lib/voice-profile-store.ts +43 -4
- package/server/routes/voice.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## 6.44.15
|
|
2
|
+
|
|
3
|
+
Voice profiles named after a spoken sentence are repaired at load.
|
|
4
|
+
|
|
5
|
+
- Chelsie 2026-09-08: her `voice-profiles.json` held two profiles whose name
|
|
6
|
+
was the entire enrolment speech ("My Voice My Name Is Chelsie Hodgkiss And I
|
|
7
|
+
Am The Director…", ~600 characters), written by a client older than the
|
|
8
|
+
8/25 guards. Speaker ID was "active" and could never match anything, and the
|
|
9
|
+
file cannot be repaired by hand because the server rewrites it from memory.
|
|
10
|
+
- `normalizeProfileStore` now renames a name that is too long, has more than
|
|
11
|
+
four words, or is three-plus words with sentence punctuation to
|
|
12
|
+
`Unnamed voice N`, keeps every embedding and its provenance, records
|
|
13
|
+
`renamedFrom` (the first 60 characters) and `needsName: true`, and counts it
|
|
14
|
+
in the load repairs (`profilesRenamed`). Legacy short labels ("MU",
|
|
15
|
+
"Speaker 2", "Luke H.") are never touched. `GET /api/voice/profiles` carries
|
|
16
|
+
`needsName` and `renamedFrom` so COS Control's speaker review can offer the
|
|
17
|
+
rename; `POST /api/voice/merge-profiles` folds the placeholder into the
|
|
18
|
+
right person as before.
|
|
19
|
+
|
|
1
20
|
## 6.44.14
|
|
2
21
|
|
|
3
22
|
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.15",
|
|
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": {
|
|
@@ -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,
|