@gotcos/glasses-server 6.95.0 → 6.98.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 +157 -0
- package/README.md +4 -0
- package/managed-runtime-contract.json +13 -0
- package/package.json +3 -1
- package/server/index.ts +12 -0
- package/server/lib/attached-provider-adapter.ts +35 -1
- package/server/lib/claude-bridge.ts +47 -0
- package/server/lib/codex-bridge.ts +11 -1
- package/server/lib/codex-rollout-watermark.ts +42 -0
- package/server/lib/cursor-bridge.ts +2 -1
- package/server/lib/decision-provider.ts +4 -2
- package/server/lib/dictation-clean-models.ts +2 -0
- package/server/lib/dictation-clean.ts +2 -2
- package/server/lib/dictation-polish-pipeline.ts +22 -2
- package/server/lib/dictation-prewarm.ts +16 -0
- package/server/lib/direct-bridge.ts +14 -2
- package/server/lib/direct-sse.ts +6 -1
- package/server/lib/display-bus.ts +1 -1
- package/server/lib/fork-thread.ts +32 -1
- package/server/lib/held-naming-batches.ts +44 -7
- package/server/lib/held-voice-groups.ts +5 -4
- package/server/lib/job-audit-caller.ts +20 -0
- package/server/lib/keyed-sessions.ts +21 -1
- package/server/lib/lens-gist-engines.ts +17 -5
- package/server/lib/live-cos-ask.ts +29 -6
- package/server/lib/live-cue-verdicts.ts +43 -8
- package/server/lib/live-cues-engine.ts +4 -1
- package/server/lib/live-cues-filter.ts +50 -0
- package/server/lib/live-cues-general-prompts.ts +137 -0
- package/server/lib/live-cues-general-topic.ts +245 -0
- package/server/lib/live-cues-general.ts +715 -0
- package/server/lib/live-cues-v2-data.ts +453 -0
- package/server/lib/live-cues-v2-grounded.ts +262 -0
- package/server/lib/live-cues-v2-judge-b.ts +59 -0
- package/server/lib/live-cues-v2-names.ts +49 -0
- package/server/lib/live-cues-v2-notes.ts +8 -4
- package/server/lib/live-cues-v2-open-items.ts +132 -0
- package/server/lib/live-cues-v2-pacing.ts +45 -0
- package/server/lib/live-cues-v2-prompts-kinds.ts +9 -4
- package/server/lib/live-cues-v2-settings.ts +89 -10
- package/server/lib/live-cues-v2.ts +570 -47
- package/server/lib/mac-recording-asr.ts +6 -3
- package/server/lib/mac-speaker-call-human.ts +371 -0
- package/server/lib/mac-speaker-call-interval-human.ts +499 -0
- package/server/lib/mac-speaker-call-interval-projection.ts +233 -0
- package/server/lib/mac-speaker-call-original-view.ts +328 -0
- package/server/lib/mac-speaker-call-producer-worker.ts +615 -0
- package/server/lib/mac-speaker-call-producer.ts +539 -0
- package/server/lib/mac-speaker-call-store.ts +264 -0
- package/server/lib/mac-speaker-call-view.ts +373 -0
- package/server/lib/mac-speaker-live-compute.ts +72 -0
- package/server/lib/mac-speaker-live-memory.ts +414 -0
- package/server/lib/mac-speaker-live-projection.ts +126 -0
- package/server/lib/mac-speaker-live-save.ts +90 -0
- package/server/lib/mac-speaker-live-store.ts +71 -0
- package/server/lib/mac-speaker-live-worker.ts +58 -0
- package/server/lib/mac-speaker-live.ts +175 -0
- package/server/lib/mac-speaker-profile-extractor.ts +15 -10
- package/server/lib/mac-speaker-shadow-repair.ts +307 -0
- package/server/lib/mac-voice-assignment.ts +46 -27
- package/server/lib/mac-voice-display.ts +195 -0
- package/server/lib/mac-voice-naming-batch.ts +1067 -0
- package/server/lib/mac-voice-suggestions.ts +548 -0
- package/server/lib/maintenance-lifecycle.ts +2 -0
- package/server/lib/media-store.ts +2 -1
- package/server/lib/meeting-ask-pasted.ts +135 -0
- package/server/lib/meeting-ask-review.ts +8 -1
- package/server/lib/meeting-ask-runner.ts +4 -1
- package/server/lib/meeting-ask-runtime.ts +189 -17
- package/server/lib/meeting-ask-store.ts +2 -0
- package/server/lib/meeting-ask-web.ts +284 -13
- package/server/lib/meeting-batch-progress.ts +51 -5
- package/server/lib/meeting-batch-transcribe.ts +35 -2
- package/server/lib/meeting-corrections.ts +2 -1
- package/server/lib/meeting-fact-check.ts +3 -0
- package/server/lib/meeting-finalization-jobs.ts +69 -2
- package/server/lib/meeting-public-topic.ts +6 -1
- package/server/lib/meeting-relabel-enrolment.ts +2 -1
- package/server/lib/meeting-relabel.ts +17 -10
- package/server/lib/meeting-speaker-review.ts +187 -21
- package/server/lib/meeting-store.ts +6 -1
- package/server/lib/parakeet.ts +51 -5
- package/server/lib/pending-batch-retention.ts +58 -0
- package/server/lib/private-reference.ts +38 -0
- package/server/lib/provider-usage.ts +561 -0
- package/server/lib/public-https-url.ts +32 -0
- package/server/lib/query-job-runtime.ts +3 -0
- package/server/lib/rich-media-safety.ts +23 -2
- package/server/lib/session-find-ask.ts +288 -0
- package/server/lib/session-find-collect.ts +715 -0
- package/server/lib/session-find-redact.ts +254 -0
- package/server/lib/session-find.ts +345 -0
- package/server/lib/speaker-calibration-log.ts +3 -2
- package/server/lib/speaker-embeddings.ts +46 -35
- package/server/lib/speaker-name-nfc.ts +37 -0
- package/server/lib/speaker-rename-fanout.ts +7 -6
- package/server/lib/speaker-shadow-decide.ts +59 -22
- package/server/lib/speaker-shadow-pair-source.ts +91 -0
- package/server/lib/speaker-shadow-paired.ts +268 -0
- package/server/lib/speaker-shadow-report.ts +2 -2
- package/server/lib/speaker-shadow-worker.ts +50 -15
- package/server/lib/speaker-shadow.ts +80 -19
- package/server/lib/speaker-trainer.ts +2 -1
- package/server/lib/token-audit.ts +36 -6
- package/server/lib/transcript-cleanup.ts +3 -2
- package/server/lib/voice-directory.ts +10 -5
- package/server/lib/voice-profile-store.ts +24 -14
- package/server/lib/web-lookup-ledger.ts +151 -0
- package/server/lib/work-review-runtime.ts +2 -0
- package/server/lib/work-search.ts +3 -1
- package/server/lib/xlsx-text.ts +277 -0
- package/server/routes/decision-provider.ts +4 -1
- package/server/routes/jev.ts +5 -1
- package/server/routes/live-cues.ts +36 -14
- package/server/routes/meeting.ts +515 -61
- package/server/routes/session-find.ts +73 -0
- package/server/routes/transcribe-stream.ts +114 -33
- package/server/routes/voice.ts +28 -18
- package/server/scripts/live-cues-v3-mutation-gate.mjs +311 -0
- package/server/scripts/mac-voice-assignment-6930-mutation-gate.mjs +5 -5
- package/shared/live-cues-v2.ts +38 -5
- package/shared/media-attachment.ts +2 -0
- package/shared/meeting-ask.ts +11 -0
|
@@ -46,7 +46,9 @@ import { Transform } from 'node:stream'
|
|
|
46
46
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
47
47
|
import { durableAtomicWriteFileSync } from './atomic-fs.js'
|
|
48
48
|
import { MAC_CAPTURE_ID } from './mac-recording.js'
|
|
49
|
-
import { maintenanceLifecycle } from './maintenance-lifecycle.js'
|
|
49
|
+
import { maintenanceLifecycle,acquireMaintenanceWork } from './maintenance-lifecycle.js'
|
|
50
|
+
import {configureMacSpeakerCallProducer,macSpeakerCallProducerEnabled,macSpeakerCallProducerBusy,macSpeakerCallProducerRequestValid,notifyMacSpeakerCallNativeExport,sampleMacSpeakerCallResources} from './mac-speaker-call-producer.js'
|
|
51
|
+
import type {MacSpeakerCallProducerRequest} from './mac-speaker-call-producer-worker.js'
|
|
50
52
|
import { installedCliIntact, installedCliTrust, parakeetCliPath, parakeetInstalled, SANDBOX_EXEC } from './parakeet.js'
|
|
51
53
|
import { SHADOW_GATES } from './speaker-shadow-decide.js'
|
|
52
54
|
import { shadowModelsPresent, speakerShadowHome, speakerShadowModelsDir, speakerShadowRoot } from './speaker-shadow-models.js'
|
|
@@ -148,15 +150,16 @@ export interface SpeakerShadowRecord {
|
|
|
148
150
|
status: SpeakerShadowStatus
|
|
149
151
|
reason: string | null
|
|
150
152
|
detail: string | null
|
|
151
|
-
gates: { majority: number; minCoveredFraction: number; floorSeconds: number; outlier: string;
|
|
153
|
+
gates: { majority: number; minCoveredFraction: number; floorSeconds: number; outlier: string; anchorRatioMax: number }
|
|
152
154
|
facts?: ShadowRunFacts
|
|
155
|
+
paired?: Extract<ShadowJobOutcome, { status: 'proposed' }>['paired']
|
|
153
156
|
proposals?: ShadowProposals
|
|
154
157
|
resources?: { wallMs: number; peakRssBytes: number | null; cliTrust: string | null }
|
|
155
158
|
/** The readable report, relative to <data>/speaker-shadow/ (reports/<day>/<title>__<session>.md). */
|
|
156
159
|
report?: string | null
|
|
157
160
|
}
|
|
158
161
|
|
|
159
|
-
const GATES_RECORD = { majority: SHADOW_GATES.majority, minCoveredFraction: SHADOW_GATES.minCoveredFraction, floorSeconds: SHADOW_GATES.floorSeconds, outlier: 'leave-fragment-out cluster p95',
|
|
162
|
+
const GATES_RECORD = { majority: SHADOW_GATES.majority, minCoveredFraction: SHADOW_GATES.minCoveredFraction, floorSeconds: SHADOW_GATES.floorSeconds, outlier: 'leave-fragment-out cluster p95', anchorRatioMax: SHADOW_GATES.anchorRatioMax }
|
|
160
163
|
|
|
161
164
|
/** Append one record: the whole file is rewritten to a temp file, fsynced and renamed, so a reader never sees half a line. */
|
|
162
165
|
export async function appendSpeakerShadowRecord(root: string, record: SpeakerShadowRecord): Promise<void> {
|
|
@@ -203,7 +206,7 @@ export async function readSpeakerShadowRecords(root: string, options: { limit?:
|
|
|
203
206
|
|
|
204
207
|
// ── State ────────────────────────────────────────────────────────────────────
|
|
205
208
|
|
|
206
|
-
interface QueueItem { sessionId: string; sidecarPath: string; labelsHash: string | null; enqueuedAt: number }
|
|
209
|
+
interface QueueItem { sessionId: string; sidecarPath: string; labelsHash: string | null; enqueuedAt: number;producerRequest?:MacSpeakerCallProducerRequest }
|
|
207
210
|
interface ShadowState {
|
|
208
211
|
schema: 1
|
|
209
212
|
day: string
|
|
@@ -245,6 +248,7 @@ export interface SpeakerShadowDeps {
|
|
|
245
248
|
spawnWorker: () => ChildProcess
|
|
246
249
|
/** Resident bytes of the process group led by pid, or null when it cannot be read. */
|
|
247
250
|
sampleRss: (pgid: number) => Promise<number | null>
|
|
251
|
+
producerResources:typeof sampleMacSpeakerCallResources
|
|
248
252
|
killGroup: (pgid: number) => void
|
|
249
253
|
log: (line: string) => void
|
|
250
254
|
setTimer: (fn: () => void, ms: number) => { unref?: () => void }
|
|
@@ -338,6 +342,7 @@ export function defaultSpeakerShadowDeps(): SpeakerShadowDeps {
|
|
|
338
342
|
tmpRoot: tmpdir(),
|
|
339
343
|
spawnWorker: defaultSpawnWorker,
|
|
340
344
|
sampleRss: defaultSampleRss,
|
|
345
|
+
producerResources:sampleMacSpeakerCallResources,
|
|
341
346
|
killGroup: pgid => { try { process.kill(-pgid, 'SIGKILL') } catch { /* gone */ } },
|
|
342
347
|
log: line => console.log(`[speaker-shadow] ${line}`),
|
|
343
348
|
setTimer: (fn, ms) => setTimeout(fn, ms),
|
|
@@ -351,7 +356,7 @@ type Supervised = ShadowJobOutcome | { status: 'deferred'; reason: string } | {
|
|
|
351
356
|
|
|
352
357
|
export class SpeakerShadowRunner {
|
|
353
358
|
private readonly deps: SpeakerShadowDeps
|
|
354
|
-
private running: { sessionId: string; pgid: number | null } | null = null
|
|
359
|
+
private running: { sessionId: string; pgid: number | null;producerRequest?:MacSpeakerCallProducerRequest } | null = null
|
|
355
360
|
private pumping = false
|
|
356
361
|
private timer: { unref?: () => void } | null = null
|
|
357
362
|
private deferredReason: string | null = null
|
|
@@ -362,6 +367,20 @@ export class SpeakerShadowRunner {
|
|
|
362
367
|
}
|
|
363
368
|
|
|
364
369
|
enabled(): boolean { return speakerShadowEnabled(this.deps.env(), this.deps.root()) }
|
|
370
|
+
nativeEnabled():boolean{return this.enabled()||macSpeakerCallProducerEnabled(this.deps.env())}
|
|
371
|
+
nativeRunning():boolean{return this.running!==null}
|
|
372
|
+
async notifyConsumerFinalized(request:MacSpeakerCallProducerRequest):Promise<void> {
|
|
373
|
+
if(!macSpeakerCallProducerEnabled(this.deps.env())||!macSpeakerCallProducerRequestValid(request))throw Error('independent_consumer_request_invalid_or_disabled')
|
|
374
|
+
if(this.running?.sessionId===request.captureId){
|
|
375
|
+
const admitted=this.running.producerRequest
|
|
376
|
+
if(!admitted)throw Object.assign(Error('native_active_without_prenative_provenance'),{code:'MAC_CALL_NATIVE_PENDING'})
|
|
377
|
+
if(admitted.policySHA256!==request.policySHA256||admitted.root!==request.root)throw Object.assign(Error('native_running_other_policy'),{code:'MAC_CALL_NATIVE_UNAVAILABLE'})
|
|
378
|
+
// seedReady already durably registered this exact copy. The one
|
|
379
|
+
// admitted execution will fan out only fresh per-copy CAS publications.
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
await this.enqueue({sessionId:request.captureId,sidecarPath:request.sidecarPath},request)
|
|
383
|
+
}
|
|
365
384
|
|
|
366
385
|
switchState(): { enabled: boolean; source: SpeakerShadowSwitchSource } { return resolveSpeakerShadowSwitch(this.deps.env(), this.deps.root()) }
|
|
367
386
|
|
|
@@ -403,13 +422,15 @@ export class SpeakerShadowRunner {
|
|
|
403
422
|
}
|
|
404
423
|
}
|
|
405
424
|
|
|
406
|
-
private async enqueue(input: { sessionId: string; sidecarPath: string }): Promise<void> {
|
|
425
|
+
private async enqueue(input: { sessionId: string; sidecarPath: string },producerRequest?:MacSpeakerCallProducerRequest): Promise<void> {
|
|
407
426
|
let labelsHash: string | null = null
|
|
408
427
|
try { labelsHash = shadowLabelsHash(shadowEntries(JSON.parse(await readFile(input.sidecarPath, 'utf8')))) } catch { /* the worker reports the sidecar */ }
|
|
409
428
|
// Read, change and write with no await in between: the pump also rewrites this file.
|
|
410
429
|
const state = this.readState()
|
|
411
|
-
if (state.done.includes(input.sessionId)
|
|
412
|
-
state.queue.
|
|
430
|
+
if (state.done.includes(input.sessionId)) {if(producerRequest)throw Object.assign(Error('native_already_completed_without_retained_export'),{code:'MAC_CALL_NATIVE_UNAVAILABLE'});return}
|
|
431
|
+
const existing=state.queue.find(q=>q.sessionId===input.sessionId)
|
|
432
|
+
if(existing){if(producerRequest){existing.producerRequest=producerRequest;this.writeState(state)}return}
|
|
433
|
+
state.queue.push({ sessionId: input.sessionId, sidecarPath: input.sidecarPath, labelsHash, enqueuedAt: this.deps.now(),...(producerRequest?{producerRequest}:{}) })
|
|
413
434
|
const dropped = state.queue.splice(0, Math.max(0, state.queue.length - MAX_QUEUE))
|
|
414
435
|
for (const item of dropped) state.done.push(item.sessionId)
|
|
415
436
|
this.writeState(state)
|
|
@@ -421,7 +442,7 @@ export class SpeakerShadowRunner {
|
|
|
421
442
|
/** Boot: pick up meetings a previous process queued but never ran. */
|
|
422
443
|
resume(): void {
|
|
423
444
|
try {
|
|
424
|
-
if (!this.
|
|
445
|
+
if (!this.nativeEnabled() || !existsSync(this.statePath())) return
|
|
425
446
|
if (this.readState().queue.length) void this.pump()
|
|
426
447
|
} catch (error) { this.deps.log(`resume failed: ${describe(error)}`) }
|
|
427
448
|
}
|
|
@@ -472,14 +493,17 @@ export class SpeakerShadowRunner {
|
|
|
472
493
|
this.pumping = true
|
|
473
494
|
try {
|
|
474
495
|
for (;;) {
|
|
475
|
-
if (!this.
|
|
496
|
+
if (!this.nativeEnabled()) return
|
|
497
|
+
if(macSpeakerCallProducerBusy()){this.schedule(DEFER_RETRY_MS,'independent_consumer');return}
|
|
476
498
|
const state = this.readState()
|
|
477
499
|
const item = state.queue[0]
|
|
478
500
|
if (!item) { this.deferredReason = null; return }
|
|
479
501
|
const now = this.deps.now()
|
|
480
502
|
const finish = async (status: SpeakerShadowStatus, reason: string | null, detail: string | null, extra: Partial<SpeakerShadowRecord> = {}, report?: { title: string; markdown: string }) => {
|
|
481
|
-
|
|
482
|
-
|
|
503
|
+
if(this.enabled()){
|
|
504
|
+
const reportPath = await this.writeReport(item, status, reason, detail, report)
|
|
505
|
+
await this.record(item, status, reason, detail, { ...extra, report: reportPath })
|
|
506
|
+
}
|
|
483
507
|
const next = this.readState()
|
|
484
508
|
next.queue = next.queue.filter(q => q.sessionId !== item.sessionId)
|
|
485
509
|
next.done.push(item.sessionId)
|
|
@@ -511,9 +535,21 @@ export class SpeakerShadowRunner {
|
|
|
511
535
|
if (!this.deps.modelsPresent(modelsDir)) { await finish('skipped', 'models_missing', 'the pinned speaker-diarization models are not on this Mac (the server never downloads them)'); continue }
|
|
512
536
|
const audioDir = this.deps.audioDirFor(item.sessionId)
|
|
513
537
|
if (!existsSync(audioDir)) { await finish('skipped', 'audio_missing', 'no retained chunk audio for this meeting'); continue }
|
|
538
|
+
let producerRequest=macSpeakerCallProducerEnabled(this.deps.env())?item.producerRequest:undefined
|
|
539
|
+
if(producerRequest&&(!macSpeakerCallProducerRequestValid(producerRequest)||producerRequest.captureId!==item.sessionId||producerRequest.sidecarPath!==item.sidecarPath)){await finish('skipped','bad_sidecar','untrusted independent consumer request');continue}
|
|
540
|
+
if(!this.enabled()&&!producerRequest)return
|
|
541
|
+
const resourceStart=producerRequest?await this.deps.producerResources().catch(()=>null):null
|
|
542
|
+
if(producerRequest&&(!resourceStart||resourceStart.availableBytes<16*1024**3)){this.schedule(DEFER_RETRY_MS,'producer_resource_admission');return}
|
|
543
|
+
if(this.deps.captureActive()||!this.deps.admissionsOpen()||!this.nativeEnabled())return
|
|
544
|
+
if(macSpeakerCallProducerBusy()){this.schedule(DEFER_RETRY_MS,'independent_consumer');return}
|
|
514
545
|
|
|
515
546
|
// Fresh read: the awaits above let an enqueue rewrite the file since `state` was read.
|
|
516
547
|
const counted = this.readState()
|
|
548
|
+
// A shared request may have joined while CLI/model checks awaited. Do
|
|
549
|
+
// not start a log-only execution that strands its admitted consumer.
|
|
550
|
+
const latest=counted.queue.find(q=>q.sessionId===item.sessionId)?.producerRequest
|
|
551
|
+
if(!producerRequest&&latest){this.schedule(0,'consumer_joined');return}
|
|
552
|
+
if(producerRequest&&JSON.stringify(latest)!==JSON.stringify(producerRequest)){this.schedule(0,'consumer_request_changed');return}
|
|
517
553
|
counted.runsToday += 1
|
|
518
554
|
this.writeState(counted)
|
|
519
555
|
this.deferredReason = null
|
|
@@ -521,6 +557,7 @@ export class SpeakerShadowRunner {
|
|
|
521
557
|
sessionId: item.sessionId, sidecarPath: item.sidecarPath, audioDir, expectedLabelsHash: item.labelsHash,
|
|
522
558
|
cliPath: cli.path, cliSha256: cli.sha256, sandboxExec: this.deps.sandboxExec, modelsDir,
|
|
523
559
|
homeDir: this.deps.homeDir(), tmpRoot: this.deps.tmpRoot, cliTimeoutMs: Math.max(5_000, limits.timeoutMs - 5_000),
|
|
560
|
+
...(producerRequest?{callProducer:producerRequest,shadowLogRequested:this.enabled(),callProducerSwapStart:resourceStart!.swapBytes}:{}),
|
|
524
561
|
}
|
|
525
562
|
this.deps.log(`start ${item.sessionId}`)
|
|
526
563
|
const started = this.deps.now()
|
|
@@ -536,9 +573,20 @@ export class SpeakerShadowRunner {
|
|
|
536
573
|
return
|
|
537
574
|
}
|
|
538
575
|
if (outcome.status === 'proposed') {
|
|
576
|
+
if (producerRequest && !outcome.callProducerExport) {
|
|
577
|
+
// Native ran once: preserve terminal done/history, never re-infer to
|
|
578
|
+
// manufacture missing pre-native provenance. Expose the consumer
|
|
579
|
+
// failure even when legacy shadow logging is disabled.
|
|
580
|
+
const detail = outcome.callProducerFailure
|
|
581
|
+
? `${outcome.callProducerFailure.stage}: ${outcome.callProducerFailure.reason}`
|
|
582
|
+
: 'admitted native completed without a retained export'
|
|
583
|
+
await finish('failed', 'consumer_export_failed', detail.slice(0, 300), { facts: outcome.facts, proposals: outcome.proposals, paired: outcome.paired, resources }, outcome.report)
|
|
584
|
+
continue
|
|
585
|
+
}
|
|
586
|
+
if(outcome.callProducerExport&&macSpeakerCallProducerEnabled(this.deps.env()))notifyMacSpeakerCallNativeExport(outcome.callProducerExport)
|
|
539
587
|
const f = outcome.facts
|
|
540
588
|
const markdown = `${outcome.report.markdown}\n${renderShadowRunFacts({ rebuildMs: f.timing.rebuildMs, nativeMs: f.timing.nativeMs, wallMs: resources.wallMs, peakRssBytes: resources.peakRssBytes, osTempDirsLeft: f.osTempDirsLeft, sidecarPath: item.sidecarPath, pcmSHA256: f.timelinePCMSHA256, cliTrust: cli.trust })}`
|
|
541
|
-
await finish('proposed', null, null, { facts: outcome.facts, proposals: outcome.proposals, resources }, { title: outcome.report.title, markdown })
|
|
589
|
+
await finish('proposed', null, null, { facts: outcome.facts, proposals: outcome.proposals, paired: outcome.paired, resources }, { title: outcome.report.title, markdown })
|
|
542
590
|
}
|
|
543
591
|
else await finish(outcome.status, outcome.reason, outcome.detail, { resources })
|
|
544
592
|
}
|
|
@@ -551,28 +599,38 @@ export class SpeakerShadowRunner {
|
|
|
551
599
|
|
|
552
600
|
private supervise(job: ShadowJob, limits: SpeakerShadowLimits): Promise<{ outcome: Supervised; peakRss: number | null }> {
|
|
553
601
|
return new Promise(resolvePromise => {
|
|
602
|
+
let producerLease:ReturnType<typeof acquireMaintenanceWork>|null=null
|
|
554
603
|
let child: ChildProcess
|
|
555
|
-
try {
|
|
604
|
+
try {if(job.callProducer)producerLease=acquireMaintenanceWork('mac_speaker_call');child = this.deps.spawnWorker() } catch (error) {
|
|
605
|
+
producerLease?.release()
|
|
556
606
|
resolvePromise({ outcome: { status: 'failed', reason: 'worker_spawn_failed', detail: describe(error) }, peakRss: null })
|
|
557
607
|
return
|
|
558
608
|
}
|
|
559
609
|
const pgid = child.pid ?? null
|
|
560
|
-
this.running = { sessionId: job.sessionId, pgid }
|
|
610
|
+
this.running = { sessionId: job.sessionId, pgid,...(job.callProducer?{producerRequest:job.callProducer}:{}) }
|
|
561
611
|
this.installExitHook()
|
|
562
612
|
if (pgid) { try { setPriority(pgid, 10) } catch { /* runs at normal priority */ } }
|
|
563
613
|
const started = this.deps.now()
|
|
564
614
|
let stdout = '', stderr = '', verdict: Supervised | null = null, peak: number | null = null, sampling = false, settled = false
|
|
615
|
+
let swapStart:number|null=job.callProducerSwapStart??null
|
|
565
616
|
const stop = (outcome: Supervised) => { if (!verdict) verdict = outcome; if (pgid) this.deps.killGroup(pgid) }
|
|
566
617
|
const tick = setInterval(() => {
|
|
567
618
|
if (verdict) return
|
|
568
619
|
if (this.deps.now() - started > limits.timeoutMs) { stop({ status: 'failed', reason: 'timeout', detail: `no result within ${limits.timeoutMs} ms` }); return }
|
|
569
|
-
if (!this.
|
|
620
|
+
if (!this.nativeEnabled()||job.callProducer&&!macSpeakerCallProducerEnabled(this.deps.env())) { stop({ status: 'deferred', reason: 'switched_off' }); return }
|
|
570
621
|
if (this.deps.captureActive?.() || !this.deps.admissionsOpen()) { stop({ status: 'deferred', reason: 'capture_live' }); return }
|
|
571
622
|
if (!pgid || sampling) return
|
|
572
623
|
sampling = true
|
|
624
|
+
if(job.callProducer)void this.deps.producerResources().catch(()=>null).then(r=>{
|
|
625
|
+
if(settled||verdict)return
|
|
626
|
+
if(!r||r.availableBytes<8*1024**3){stop({status:'deferred',reason:'producer_resource_reserve'});return}
|
|
627
|
+
swapStart??=r.swapBytes
|
|
628
|
+
if(r.swapBytes-swapStart>256*1024**2)stop({status:'deferred',reason:'producer_swap_growth'})
|
|
629
|
+
})
|
|
573
630
|
void this.deps.sampleRss(pgid).then(bytes => {
|
|
574
631
|
sampling = false
|
|
575
|
-
if (
|
|
632
|
+
if (verdict||settled)return
|
|
633
|
+
if(bytes===null){if(job.callProducer)stop({status:'deferred',reason:'producer_rss_unavailable'});return}
|
|
576
634
|
peak = Math.max(peak ?? 0, bytes)
|
|
577
635
|
if (bytes > limits.rssBytes) stop({ status: 'failed', reason: 'rss_cap', detail: `the run used ${Math.round(bytes / 1048576)} MB, over the ${Math.round(limits.rssBytes / 1048576)} MB cap` })
|
|
578
636
|
})
|
|
@@ -583,6 +641,8 @@ export class SpeakerShadowRunner {
|
|
|
583
641
|
settled = true
|
|
584
642
|
clearInterval(tick)
|
|
585
643
|
this.running = null
|
|
644
|
+
producerLease?.release()
|
|
645
|
+
if(!verdict&&this.deps.now()-started>limits.timeoutMs)verdict={status:'failed',reason:'timeout',detail:'worker close exceeded absolute wall budget'}
|
|
586
646
|
if (verdict) { resolvePromise({ outcome: verdict, peakRss: peak }); return }
|
|
587
647
|
const line = stdout.trim().split('\n').pop() ?? ''
|
|
588
648
|
try {
|
|
@@ -593,7 +653,7 @@ export class SpeakerShadowRunner {
|
|
|
593
653
|
}
|
|
594
654
|
child.stdout?.on('data', (d: Buffer) => { if (stdout.length < MAX_STDOUT) stdout += d.toString(); else stop({ status: 'failed', reason: 'worker_crashed', detail: 'worker output over 8 MB' }) })
|
|
595
655
|
child.stderr?.on('data', (d: Buffer) => { if (stderr.length < 16_384) stderr += d.toString() })
|
|
596
|
-
child.on('error', error => { stop({ status: 'failed', reason: 'worker_spawn_failed', detail: error.message }); done(null, null) })
|
|
656
|
+
child.on('error', error => { stop({ status: 'failed', reason: 'worker_spawn_failed', detail: error.message }); if(!child.pid)done(null, null) })
|
|
597
657
|
child.on('close', done)
|
|
598
658
|
child.stdin?.on('error', () => { /* a crashed worker closes stdin; close reports it */ })
|
|
599
659
|
child.stdin?.end(JSON.stringify(job))
|
|
@@ -692,7 +752,8 @@ export function configureSpeakerShadow(probe: { captureActive: () => boolean; ad
|
|
|
692
752
|
try {
|
|
693
753
|
const runner = getSpeakerShadowRunner()
|
|
694
754
|
runner.setCaptureProbe(probe.captureActive, probe.admissionsOpen)
|
|
695
|
-
|
|
755
|
+
configureMacSpeakerCallProducer({captureActive:()=>probe.captureActive()||runner.nativeRunning(),admissionsOpen:probe.admissionsOpen,requestNative:r=>runner.notifyConsumerFinalized(r)})
|
|
756
|
+
if (!runner.nativeEnabled()) return
|
|
696
757
|
const swept = sweepStaleSpeakerShadowTmp()
|
|
697
758
|
if (swept) console.log(`[speaker-shadow] removed ${swept} stale temp dir(s)`)
|
|
698
759
|
console.log('[speaker-shadow] enabled (log only)')
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
rawCosineSimilarity, saveProfileStore, rebuildAllProfiles, clearSpeakerEmbeddings,
|
|
14
14
|
} from './speaker-embeddings.js'
|
|
15
15
|
import { COS_SCRIPTS_DIR } from './python-bridge.js'
|
|
16
|
+
import { sameSpeakerName } from './speaker-name-nfc.js'
|
|
16
17
|
|
|
17
18
|
const CACHE_DIR = '/tmp/cos-speaker-training'
|
|
18
19
|
const PROGRESS_PATH = resolve(CACHE_DIR, 'training-progress.json')
|
|
@@ -520,7 +521,7 @@ export async function getTrainingStatus(): Promise<TrainingStatus> {
|
|
|
520
521
|
}
|
|
521
522
|
|
|
522
523
|
for (const name of enrolledNames) {
|
|
523
|
-
if (!speakers.some(s => s.name
|
|
524
|
+
if (!speakers.some(s => sameSpeakerName(s.name, name))) {
|
|
524
525
|
speakers.push({ name, segments: 0, meetings: 0, enrolled: true })
|
|
525
526
|
}
|
|
526
527
|
}
|
|
@@ -8,12 +8,17 @@ import { homedir } from 'node:os'
|
|
|
8
8
|
|
|
9
9
|
import { COS_SCRIPTS_DIR } from './python-bridge.js'
|
|
10
10
|
import type { TokenAuditKeyFields } from './key-relay-contract.js'
|
|
11
|
+
import { cleanProviderId, normalizeRealUsage, type RealUsage } from './provider-usage.js'
|
|
11
12
|
|
|
12
13
|
// Shared JSONL file — same path as Python's llm_client.py uses
|
|
13
14
|
// Uses COS_SCRIPTS_DIR when available (server mode), falls back to user home
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// 6.97.0: COS_TOKEN_AUDIT_FILE is set only by the test config, so no test writes a made-up row to a
|
|
16
|
+
// real ledger now that a row can claim to be measured.
|
|
17
|
+
const AUDIT_FILE = process.env.COS_TOKEN_AUDIT_FILE
|
|
18
|
+
? resolve(process.env.COS_TOKEN_AUDIT_FILE)
|
|
19
|
+
: COS_SCRIPTS_DIR
|
|
20
|
+
? resolve(COS_SCRIPTS_DIR, '.cos_token_audit.jsonl')
|
|
21
|
+
: resolve(homedir(), '.cos-glasses', 'token-audit.jsonl')
|
|
17
22
|
|
|
18
23
|
// Rough char-to-token ratio (1 token ~ 4 chars for English)
|
|
19
24
|
const CHARS_PER_TOKEN = 4
|
|
@@ -33,7 +38,12 @@ export interface TokenAuditEntry extends TokenAuditKeyFields {
|
|
|
33
38
|
runId?: string
|
|
34
39
|
sessionId?: string
|
|
35
40
|
clientJobId?: string
|
|
36
|
-
|
|
41
|
+
/** The provider's own session id: Claude's session, Codex's thread, Cursor's chat. `sessionId` is COS's. */
|
|
42
|
+
cliSessionId?: string
|
|
43
|
+
/** 6.97.0: what the provider reported. The row says `provider_final` only when these are real numbers. */
|
|
44
|
+
realUsage?: RealUsage
|
|
45
|
+
/** 6.97.0, dictation polish only: what the model is sent, system prompt included. `inputChars` stays the words dictated. */
|
|
46
|
+
promptChars?: number
|
|
37
47
|
/** 6.85.0, dictation polish only: whether the user's polish instructions were in the prompt, and their hash. */
|
|
38
48
|
polishInstructions?: { on: boolean; hash: string }
|
|
39
49
|
/** 6.91.0, dictation polish only: whether the learned style was in the prompt, and its hash. */
|
|
@@ -56,6 +66,8 @@ export interface TokenAuditEntry extends TokenAuditKeyFields {
|
|
|
56
66
|
* (subscription) row is byte-identical to 6.79.0. Callers set them only on keyed turns.
|
|
57
67
|
*/
|
|
58
68
|
export function buildTokenAuditRecord(entry: TokenAuditEntry, now: Date = new Date()): Record<string, unknown> {
|
|
69
|
+
const real = normalizeRealUsage(entry.realUsage)
|
|
70
|
+
const cliSessionId = cleanProviderId(entry.cliSessionId)
|
|
59
71
|
return {
|
|
60
72
|
ts: now.toISOString(),
|
|
61
73
|
source: entry.source,
|
|
@@ -66,7 +78,7 @@ export function buildTokenAuditRecord(entry: TokenAuditEntry, now: Date = new Da
|
|
|
66
78
|
est_output_tokens: estimateTokens(entry.outputChars),
|
|
67
79
|
duration_ms: entry.durationMs,
|
|
68
80
|
caller: entry.caller,
|
|
69
|
-
usage_kind:
|
|
81
|
+
usage_kind: real ? 'provider_final' : 'estimated',
|
|
70
82
|
...(entry.turnId ? { turn_id: entry.turnId } : {}),
|
|
71
83
|
...(entry.runId ? { run_id: entry.runId } : {}),
|
|
72
84
|
...(entry.sessionId ? { session_id: entry.sessionId } : {}),
|
|
@@ -91,12 +103,30 @@ export function buildTokenAuditRecord(entry: TokenAuditEntry, now: Date = new Da
|
|
|
91
103
|
...(entry.polishGuard ? { polish_guard: entry.polishGuard } : {}),
|
|
92
104
|
// 6.92.0: set only by the prompt-edit caller.
|
|
93
105
|
...(entry.promptEditOutcome ? { prompt_edit_outcome: entry.promptEditOutcome } : {}),
|
|
106
|
+
// 6.97.0: last, and only when set, so a row without them is byte-identical to 6.96.4. Numbers and
|
|
107
|
+
// ids only. COS Control reads these names. All four counts are always written. input_tokens never
|
|
108
|
+
// includes cache reads; it excludes cache writes only on Claude, the one provider that reports
|
|
109
|
+
// them, so on Codex and direct cache_write_tokens is 0 and any write stays in input_tokens.
|
|
110
|
+
// resolved_model is the CLI's word on Claude, and what COS asked for elsewhere. provider-usage.ts
|
|
111
|
+
// says exactly what each means. cache_write_1h_tokens is part of cache_write_tokens; it and cost_usd
|
|
112
|
+
// appear only when stated.
|
|
113
|
+
...(typeof entry.promptChars === 'number' ? { prompt_chars: entry.promptChars } : {}),
|
|
114
|
+
...(cliSessionId ? { cli_session_id: cliSessionId } : {}),
|
|
115
|
+
...(real ? {
|
|
116
|
+
input_tokens: real.inputTokens,
|
|
117
|
+
output_tokens: real.outputTokens,
|
|
118
|
+
cache_read_tokens: real.cacheReadTokens ?? 0,
|
|
119
|
+
cache_write_tokens: real.cacheWriteTokens ?? 0,
|
|
120
|
+
...(real.cacheWrite1hTokens !== undefined ? { cache_write_1h_tokens: real.cacheWrite1hTokens } : {}),
|
|
121
|
+
...(real.costUsd !== undefined ? { cost_usd: real.costUsd } : {}),
|
|
122
|
+
...(real.resolvedModel ? { resolved_model: real.resolvedModel } : {}),
|
|
123
|
+
} : {}),
|
|
94
124
|
}
|
|
95
125
|
}
|
|
96
126
|
|
|
97
127
|
export function logTokenAudit(entry: TokenAuditEntry): void {
|
|
98
|
-
const record = buildTokenAuditRecord(entry)
|
|
99
128
|
try {
|
|
129
|
+
const record = buildTokenAuditRecord(entry)
|
|
100
130
|
appendFileSync(AUDIT_FILE, JSON.stringify(record) + '\n', { encoding: 'utf8', mode: 0o600 })
|
|
101
131
|
chmodSync(AUDIT_FILE, 0o600)
|
|
102
132
|
} catch {
|
|
@@ -169,8 +169,9 @@ const notesSearchDefault: CleanupDeps['searchNotes'] = async (queries, sessionId
|
|
|
169
169
|
|
|
170
170
|
const filesDefault: CleanupDeps['files'] = async sessionId => {
|
|
171
171
|
try {
|
|
172
|
-
const { MeetingAskStore } = await import('./meeting-ask-store.js')
|
|
173
|
-
|
|
172
|
+
const { MeetingAskStore, isPastedFileId } = await import('./meeting-ask-store.js')
|
|
173
|
+
// Pasted text and read pages (paste_, web_) are article words, not the room's vocabulary: they must not rewrite the saved transcript.
|
|
174
|
+
return new MeetingAskStore().files(sessionId).filter(f => !isPastedFileId(f.id) && f.status === 'ready' && typeof f.text === 'string' && f.text.trim()).map(f => ({ name: f.name, text: f.text }))
|
|
174
175
|
} catch { return [] }
|
|
175
176
|
}
|
|
176
177
|
|
|
@@ -14,6 +14,7 @@ import { lstat, readFile, readdir, realpath } from 'node:fs/promises'
|
|
|
14
14
|
import { basename, dirname, join } from 'node:path'
|
|
15
15
|
import { setImmediate as yieldToEventLoop } from 'node:timers/promises'
|
|
16
16
|
import { readMacSpeakerMachineEvidence } from './mac-speaker-machine-evidence.js'
|
|
17
|
+
import { readMacSpeakerCallView } from './mac-speaker-call-view.js'
|
|
17
18
|
import { dataPath } from './data-dir.js'
|
|
18
19
|
import { confirmedLabels, confirmedChunks } from './meeting-corrections.js'
|
|
19
20
|
import { resolveCosOperationsDir, resolveMeetingLibrary } from './cos-operations-meetings.js'
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
} from './meeting-speaker-review.js'
|
|
27
28
|
import { getOwnerSpeakerLabel } from './profile.js'
|
|
28
29
|
import { readVoiceProfiles, type VoiceProfile } from './speaker-embeddings.js'
|
|
30
|
+
import { sameSpeakerName } from './speaker-name-nfc.js'
|
|
29
31
|
|
|
30
32
|
const MONTH = /^\d{4}-(0[1-9]|1[0-2])$/
|
|
31
33
|
const SIDECAR = /\.g2-chunks\.json$/
|
|
@@ -277,7 +279,8 @@ export async function buildVoiceDirectorySnapshot(): Promise<VoiceDirectorySnaps
|
|
|
277
279
|
continue
|
|
278
280
|
}
|
|
279
281
|
totalBytes += link.size
|
|
280
|
-
const
|
|
282
|
+
const baseJSON = await readFile(real, 'utf8')
|
|
283
|
+
const raw = JSON.parse(baseJSON) as Record<string, unknown> | ReviewChunk[]
|
|
281
284
|
const chunks = Array.isArray(raw) ? raw : raw.chunks
|
|
282
285
|
const sessionId = Array.isArray(raw) ? '' : String(raw.sessionId ?? '')
|
|
283
286
|
if (!Array.isArray(chunks) || !SESSION.test(sessionId) || seenSessions.has(sessionId)) {
|
|
@@ -296,7 +299,9 @@ export async function buildVoiceDirectorySnapshot(): Promise<VoiceDirectorySnaps
|
|
|
296
299
|
)
|
|
297
300
|
|
|
298
301
|
const machineEvidence = readMacSpeakerMachineEvidence(record)
|
|
299
|
-
const
|
|
302
|
+
const callView = readMacSpeakerCallView(real, baseJSON, candidate.markdownPath)
|
|
303
|
+
const review = reviewMeetingSpeakers((callView.doc.chunks ?? chunks) as ReviewChunk[], {
|
|
304
|
+
callViewAnonymousLabels: callView.metadata.groups.map(g => g.label),
|
|
300
305
|
owner,
|
|
301
306
|
phrasesPerVoice: 1,
|
|
302
307
|
confirmed: confirmedLabels(sessionId),
|
|
@@ -304,8 +309,8 @@ export async function buildVoiceDirectorySnapshot(): Promise<VoiceDirectorySnaps
|
|
|
304
309
|
machineQualifiedPositions: machineEvidence.positions,
|
|
305
310
|
machineEvidenceStatus: machineEvidence.status,
|
|
306
311
|
durationMs: typeof record.durationMs === 'number' ? record.durationMs : undefined,
|
|
307
|
-
batchSegments: Array.isArray(
|
|
308
|
-
?
|
|
312
|
+
batchSegments: Array.isArray(callView.doc.batchSegments)
|
|
313
|
+
? callView.doc.batchSegments as SpeakerWordSegment[]
|
|
309
314
|
: undefined,
|
|
310
315
|
})
|
|
311
316
|
meetingsScanned++
|
|
@@ -363,7 +368,7 @@ export async function buildVoiceDirectorySnapshot(): Promise<VoiceDirectorySnaps
|
|
|
363
368
|
.slice(0, MAX_APPEARANCES_PER_VOICE)
|
|
364
369
|
return {
|
|
365
370
|
name: profile.name,
|
|
366
|
-
isOwner: profile.name
|
|
371
|
+
isOwner: sameSpeakerName(profile.name, owner),
|
|
367
372
|
embeddings: profile.embeddings.length,
|
|
368
373
|
sources: sourceCounts(profile),
|
|
369
374
|
sourcesAligned: (profile.sources?.length ?? 0) === profile.embeddings.length,
|
|
@@ -16,6 +16,7 @@ import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:f
|
|
|
16
16
|
import { basename, dirname, join, resolve } from 'node:path'
|
|
17
17
|
import { durableAtomicWriteFileSync, loadJsonOrQuarantine } from './atomic-fs.js'
|
|
18
18
|
import { EVICTION_ORDER, isMacAssign, macAssignCapFor, provenanceTier } from './embedding-eviction.js'
|
|
19
|
+
import { sameSpeakerName } from './speaker-name-nfc.js'
|
|
19
20
|
|
|
20
21
|
export interface VoiceProfile {
|
|
21
22
|
name: string
|
|
@@ -512,6 +513,11 @@ export interface MergeOutcome {
|
|
|
512
513
|
* removable later, and overwriting it to record a bookkeeping event would trade
|
|
513
514
|
* a useful fact for a useless one.
|
|
514
515
|
*/
|
|
516
|
+
/** The profile stored under `name`: the exact spelling first, else the one that is the same name in NFC. */
|
|
517
|
+
export function findStoredProfile(store: ProfileStore, name: string): VoiceProfile | undefined {
|
|
518
|
+
return store.profiles.find(p => p.name === name) ?? store.profiles.find(p => sameSpeakerName(p.name, name))
|
|
519
|
+
}
|
|
520
|
+
|
|
515
521
|
export function mergeProfilesInStore(
|
|
516
522
|
store: ProfileStore,
|
|
517
523
|
into: string,
|
|
@@ -519,7 +525,7 @@ export function mergeProfilesInStore(
|
|
|
519
525
|
options: { cap?: number } = {},
|
|
520
526
|
): MergeOutcome {
|
|
521
527
|
const cap = options.cap ?? 40
|
|
522
|
-
const target = store
|
|
528
|
+
const target = findStoredProfile(store, into)
|
|
523
529
|
const outcome: MergeOutcome = {
|
|
524
530
|
similarity: {},
|
|
525
531
|
samplesBefore: target?.embeddings.length ?? 0,
|
|
@@ -537,18 +543,23 @@ export function mergeProfilesInStore(
|
|
|
537
543
|
const sources = [...(target.sources ?? [])]
|
|
538
544
|
while (sources.length < embeddings.length) sources.push(UNKNOWN_SOURCE)
|
|
539
545
|
|
|
546
|
+
// Sources are the stored profiles themselves, never the spelling asked for: a name stored decomposed and its composed twin are two
|
|
547
|
+
// profiles (the twin is merged into the one that was asked for), and the target is never its own source.
|
|
548
|
+
const absorbed = new Set<VoiceProfile>()
|
|
540
549
|
for (const name of from) {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
+
const found = store.profiles.filter(p => p !== target && !absorbed.has(p) && sameSpeakerName(p.name, name))
|
|
551
|
+
if (found.length === 0) { if (!store.profiles.some(p => sameSpeakerName(p.name, name) && p !== target)) outcome.missing.push(name); continue }
|
|
552
|
+
for (const source of found) {
|
|
553
|
+
outcome.similarity[name] = profileSimilarity(target, source)
|
|
554
|
+
const sourceSources = [...(source.sources ?? [])]
|
|
555
|
+
while (sourceSources.length < source.embeddings.length) sourceSources.push(UNKNOWN_SOURCE)
|
|
556
|
+
for (let i = 0; i < source.embeddings.length; i++) {
|
|
557
|
+
embeddings.push(source.embeddings[i])
|
|
558
|
+
sources.push(sourceSources[i] ?? UNKNOWN_SOURCE)
|
|
559
|
+
}
|
|
560
|
+
outcome.mergedFrom.push(source.name)
|
|
561
|
+
absorbed.add(source)
|
|
550
562
|
}
|
|
551
|
-
outcome.mergedFrom.push(name)
|
|
552
563
|
}
|
|
553
564
|
|
|
554
565
|
if (outcome.mergedFrom.length === 0) return outcome
|
|
@@ -569,8 +580,7 @@ export function mergeProfilesInStore(
|
|
|
569
580
|
target.sources = keep.map(i => sources[i])
|
|
570
581
|
outcome.samplesAfter = target.embeddings.length
|
|
571
582
|
|
|
572
|
-
|
|
573
|
-
store.profiles = store.profiles.filter(p => !removed.has(p.name))
|
|
583
|
+
store.profiles = store.profiles.filter(p => !absorbed.has(p))
|
|
574
584
|
return outcome
|
|
575
585
|
}
|
|
576
586
|
|
|
@@ -583,7 +593,7 @@ export function deleteProfileFromStore(
|
|
|
583
593
|
let removedProfiles = 0
|
|
584
594
|
let removedEmbeddings = 0
|
|
585
595
|
store.profiles = store.profiles.filter(profile => {
|
|
586
|
-
if (profile.name
|
|
596
|
+
if (!sameSpeakerName(profile.name, name)) return true
|
|
587
597
|
removedProfiles++
|
|
588
598
|
removedEmbeddings += profile.embeddings.length
|
|
589
599
|
return false
|