@gotcos/glasses-server 6.45.4 → 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.
@@ -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 20 evicts the oldest sample N
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
- return res.status(400).json({ success: false, error: nameCheck.message, reason: nameCheck.reason })
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
- const dryRun = req.body?.dryRun === true
589
- const confirm = req.body?.confirm === true
590
- if (dryRun || !confirm) {
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 = enrollHeldGroup(name, members)
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
- const verb = result.created ? 'Created' : 'Added to'
609
- res.json({
610
- success: true,
611
- ...result,
612
- message: result.leftBehind.length > 0
613
- ? `${verb} ${name} from ${result.coherent} of ${result.resolved} samples; ${result.leftBehind.length} did not sound like the same person and stay held.`
614
- : `${verb} ${name} from ${result.coherent} sample${result.coherent === 1 ? '' : 's'}.`
615
- + (result.notReady.length > 0 ? ` ${result.notReady.length} still being read; they stay held.` : ''),
616
- })
617
- } catch (err: unknown) {
618
- heldGroupFailure(res, err)
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
- return { operationsDir, runs: merged.map(name => fanOutSpeakerRename(operationsDir, name, into, { apply })) }
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,