@gotcos/glasses-server 6.45.5 → 6.46.0
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 +12 -0
- package/package.json +2 -2
- package/server/index.ts +2 -0
- package/server/lib/cos-operations-meetings.ts +1 -1
- package/server/lib/g2-ops-handoff.ts +27 -5
- package/server/lib/held-naming-batches.ts +527 -0
- package/server/lib/held-voice-groups.ts +103 -12
- package/server/lib/meeting-batch-persistence.ts +1 -0
- package/server/lib/meeting-corrections.ts +43 -5
- package/server/lib/meeting-speaker-labels.ts +192 -0
- package/server/lib/meeting-speaker-review.ts +18 -5
- package/server/lib/meeting-sync-lock.ts +26 -0
- package/server/lib/speaker-embeddings.ts +1 -1
- package/server/lib/speaker-rename-fanout.ts +91 -177
- package/server/lib/training-audio-save.ts +24 -0
- package/server/lib/voice-directory.ts +2 -1
- package/server/lib/voice-enrolment-selection.ts +25 -0
- package/server/lib/voice-profile-store.ts +8 -3
- package/server/routes/meeting.ts +5 -1
- package/server/routes/transcribe-stream.ts +13 -41
- package/server/routes/voice.ts +29 -43
package/server/routes/voice.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { getVoiceDirectorySnapshot, invalidateVoiceDirectory } from '../lib/voic
|
|
|
18
18
|
import { greedyDiversitySelect } from '../lib/voice-enrolment-selection.js'
|
|
19
19
|
import { fanOutSpeakerRename, type SpeakerRenameFanOut } from '../lib/speaker-rename-fanout.js'
|
|
20
20
|
import { HeldGroupError, discardHeldSamples, enrollHeldGroup, heldVoiceGroups, parseHeldMembers, previewDiscard } from '../lib/held-voice-groups.js'
|
|
21
|
+
import { previewHeldNaming, applyHeldNaming, undoHeldNaming, resumeHeldNaming, namingBatchList, resolveStoredName, NAMING_CAPABILITIES } from '../lib/held-naming-batches.js'
|
|
21
22
|
import { resolveCosOperationsDir } from '../lib/cos-operations-meetings.js'
|
|
22
23
|
|
|
23
24
|
// These MUST match the writer in transcribe-stream.ts, which saves under
|
|
@@ -150,7 +151,7 @@ voiceRouter.get('/voice/training-status', async (_req, res) => {
|
|
|
150
151
|
// 1. Until the reader path above was fixed it saw an empty directory, so a
|
|
151
152
|
// no-argument call was harmless. It is not harmless any more — it now reaches
|
|
152
153
|
// every accumulated speaker directory at once.
|
|
153
|
-
// 2. Enrolling N samples into a profile capped at
|
|
154
|
+
// 2. Enrolling N samples into a profile capped at 40 evicts the oldest sample N
|
|
154
155
|
// times. A 30-WAV directory would therefore discard EVERY pre-existing
|
|
155
156
|
// embedding for that speaker, replacing months of curated training with one
|
|
156
157
|
// meeting's audio. Diversity selection bounds the enrollment instead.
|
|
@@ -557,7 +558,7 @@ voiceRouter.get('/voice/ext-audio/:sessionId/sample', (req, res) => {
|
|
|
557
558
|
voiceRouter.get('/voice/held-groups', (_req, res) => {
|
|
558
559
|
try {
|
|
559
560
|
res.set('Cache-Control', 'private, no-store')
|
|
560
|
-
res.json(heldVoiceGroups())
|
|
561
|
+
res.json({ ...heldVoiceGroups(), namingCapabilities: NAMING_CAPABILITIES })
|
|
561
562
|
} catch (err: unknown) {
|
|
562
563
|
res.status(500).json({ error: errMsg(err) })
|
|
563
564
|
}
|
|
@@ -577,46 +578,29 @@ function heldGroupFailure(res: express.Response, err: unknown): void {
|
|
|
577
578
|
// APPENDED to: "add more fidelity in samples to a given voice". Only the
|
|
578
579
|
// coherent core is written and only its wavs are removed; samples that were
|
|
579
580
|
// not this voice stay held.
|
|
580
|
-
voiceRouter.post('/voice/held-groups/enroll', (req, res) => {
|
|
581
|
+
voiceRouter.post('/voice/held-groups/enroll', async (req, res) => {
|
|
581
582
|
try {
|
|
582
583
|
const nameCheck = checkSpeakerName(req.body?.name, { ownerLabel: getOwnerSpeakerLabel() })
|
|
583
|
-
if (!nameCheck.ok) {
|
|
584
|
-
|
|
585
|
-
}
|
|
586
|
-
const name = String(req.body.name).trim()
|
|
584
|
+
if (!nameCheck.ok) return res.status(400).json({ success:false,error:nameCheck.message,reason:nameCheck.reason })
|
|
585
|
+
const name = resolveStoredName(String(req.body.name).trim())
|
|
587
586
|
const members = parseHeldMembers(req.body?.members)
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const plan = enrollHeldGroup(name, members, { dryRun: true })
|
|
592
|
-
const verb = plan.created ? 'create' : 'add to'
|
|
593
|
-
const summary = `Would ${verb} ${name} from ${plan.coherent} of ${plan.resolved} held sample${plan.resolved === 1 ? '' : 's'}`
|
|
594
|
-
+ (plan.leftBehind.length > 0 ? `, leaving ${plan.leftBehind.length} that do not sound like the same person` : '')
|
|
595
|
-
+ (plan.notReady.length > 0 ? `, with ${plan.notReady.length} still being read` : '')
|
|
596
|
-
+ `, and delete the audio it used.`
|
|
597
|
-
if (dryRun) return res.json({ success: true, ...plan, message: summary })
|
|
598
|
-
return res.status(400).json({
|
|
599
|
-
success: false,
|
|
600
|
-
error: 'confirmation required',
|
|
601
|
-
reason: 'confirmation_required',
|
|
602
|
-
message: `${summary} Pass { confirm: true } to proceed.`,
|
|
603
|
-
preview: plan,
|
|
604
|
-
})
|
|
587
|
+
if (!req.body?.previewHash || req.body?.dryRun === true) {
|
|
588
|
+
if (name.owner && req.body?.confirm === true && req.body?.dryRun !== true) return res.status(400).json({success:false,error:'Owner confirmation requires a preview and ownerAck',reason:'owner_confirmation_required'})
|
|
589
|
+
return res.json(previewHeldNaming(name.name,members))
|
|
605
590
|
}
|
|
606
|
-
const result =
|
|
591
|
+
const result = await applyHeldNaming(name.name,members,String(req.body.previewHash),{confirm:req.body?.confirm===true,ownerAck:req.body?.ownerAck===true,listened:req.body?.listened===true})
|
|
607
592
|
invalidateVoiceDirectory()
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
}
|
|
593
|
+
return res.json(result)
|
|
594
|
+
} catch (err) { heldGroupFailure(res,err) }
|
|
595
|
+
})
|
|
596
|
+
voiceRouter.get('/voice/held-groups/batches', (_req,res) => {
|
|
597
|
+
try { return res.json(namingBatchList()) } catch(err) { heldGroupFailure(res,err) }
|
|
598
|
+
})
|
|
599
|
+
voiceRouter.post('/voice/held-groups/undo', async (req,res) => {
|
|
600
|
+
try { const result=await undoHeldNaming(String(req.body?.batchId??''),req.body?.confirm===true);invalidateVoiceDirectory();return res.json(result) } catch(err) { heldGroupFailure(res,err) }
|
|
601
|
+
})
|
|
602
|
+
voiceRouter.post('/voice/held-groups/resume', (req,res) => {
|
|
603
|
+
try { return res.json(resumeHeldNaming(String(req.body?.batchId??''))) } catch(err) { heldGroupFailure(res,err) }
|
|
620
604
|
})
|
|
621
605
|
|
|
622
606
|
// POST /api/voice/held-groups/discard — throw held samples out without naming
|
|
@@ -750,17 +734,19 @@ voiceRouter.get('/voice/directory', async (req, res) => {
|
|
|
750
734
|
* NEVER THROWS. A merge that succeeded in the store must not report failure because a
|
|
751
735
|
* meeting file was unreadable; the failure is reported in the payload instead.
|
|
752
736
|
*/
|
|
753
|
-
function fanOutMergeToMeetings(
|
|
737
|
+
async function fanOutMergeToMeetings(
|
|
754
738
|
merged: readonly string[],
|
|
755
739
|
into: string,
|
|
756
740
|
apply: boolean,
|
|
757
|
-
): { operationsDir: string | null; runs: SpeakerRenameFanOut[]; error?: string } {
|
|
741
|
+
): Promise<{ operationsDir: string | null; runs: SpeakerRenameFanOut[]; error?: string }> {
|
|
758
742
|
const operationsDir = resolveCosOperationsDir()
|
|
759
743
|
// Not an error. A server with no COS library is a supported install; it simply has
|
|
760
744
|
// no meetings to carry the rename to.
|
|
761
745
|
if (!operationsDir || merged.length === 0) return { operationsDir, runs: [] }
|
|
762
746
|
try {
|
|
763
|
-
|
|
747
|
+
const runs: SpeakerRenameFanOut[] = []
|
|
748
|
+
for (const name of merged) runs.push(await fanOutSpeakerRename(operationsDir, name, into, { apply }))
|
|
749
|
+
return { operationsDir, runs }
|
|
764
750
|
} catch (err: unknown) {
|
|
765
751
|
return { operationsDir, runs: [], error: errMsg(err) }
|
|
766
752
|
}
|
|
@@ -786,7 +772,7 @@ function fanOutTotals(runs: readonly SpeakerRenameFanOut[]): {
|
|
|
786
772
|
// identities at once and cannot be undone from the store alone, so the only
|
|
787
773
|
// acceptable evidence is acoustic. `force` exists for the case where the user
|
|
788
774
|
// knows something the audio does not, and it is logged.
|
|
789
|
-
voiceRouter.post('/voice/merge-profiles', (req, res) => {
|
|
775
|
+
voiceRouter.post('/voice/merge-profiles', async (req, res) => {
|
|
790
776
|
try {
|
|
791
777
|
const into = typeof req.body?.into === 'string' ? req.body.into.trim() : ''
|
|
792
778
|
const rawFrom = req.body?.from
|
|
@@ -821,7 +807,7 @@ voiceRouter.post('/voice/merge-profiles', (req, res) => {
|
|
|
821
807
|
// ACTUALLY merge, so a refused pair does not advertise a rewrite that will not
|
|
822
808
|
// run. This walks the meeting library, which is why it happens here -- once, on a
|
|
823
809
|
// deliberate human action -- and not on any hot path.
|
|
824
|
-
const fan = fanOutMergeToMeetings(preview.merged, into, false)
|
|
810
|
+
const fan = await fanOutMergeToMeetings(preview.merged, into, false)
|
|
825
811
|
return res.status(400).json({
|
|
826
812
|
error: 'confirmation required',
|
|
827
813
|
message: `Merging is not reversible from the store alone. Review the similarity scores, then pass { confirm: true }.`,
|
|
@@ -860,7 +846,7 @@ voiceRouter.post('/voice/merge-profiles', (req, res) => {
|
|
|
860
846
|
// The fan-out honours `dryRun` for the same reason the merge does: a dry run must
|
|
861
847
|
// stay a dry run all the way down, or `dryRun: true` becomes the most dangerous
|
|
862
848
|
// parameter in the API.
|
|
863
|
-
const fan = fanOutMergeToMeetings(report.merged, into, !dryRun)
|
|
849
|
+
const fan = await fanOutMergeToMeetings(report.merged, into, !dryRun)
|
|
864
850
|
res.json({
|
|
865
851
|
...report,
|
|
866
852
|
dryRun,
|