Skip to content

Commit 533f9e4

Browse files
authored
fix(notes): cap live speaker count to expected count in note recordings (#967)
* fix(notes): cap live speaker count to expected count in note recordings Normal note recordings default to one other speaker, but the live transcript's per-segment 'Speaker N' labels climbed to 2, 3, 4, 5... as recording continued, because the expected-speaker cap was not applied consistently. Renderer (meetingRecordingStore.ts): placeholder speaker ids were minted without respecting the cap the backend enforces (expectedCount - 1): - The system partial path minted a fresh speaker on every partial when the partial id was null. Since the id is cleared after every final, each utterance produced a new speaker, bypassing carry-forward reuse. - assignProvisionalSpeaker minted unbounded on any gap > 8s. Add mintPlaceholderSpeakerId() which clamps the index to expectedCount-1 and reuse the recent system speaker in the partial path before minting. Backend (ipcHandlers.js): meeting-set-session-speaker-config set the live identifier cap to expectedCount instead of expectedCount-1, so changing the speaker stepper mid-recording allowed one extra cluster. Cap at expectedCount-1 to match resolveSessionMaxSpeakers(). Cleanup (diarization.js): remove the unreachable orphan-speaker fallback in mergeWithTranscript. The nearest-match loop always assigns a real speaker (diarization output is parsed to speaker_N via regex, so every cluster has a truthy id), making the fallback dead code that would also mint past the cap if ever reached. * fix(window): always follow cursor to active monitor on dictation The recorder panel only repositioned to the cursor's display when transitioning from hidden to visible. With the persistent floating icon (default), the panel is always visible, so it never followed the cursor across monitors. Always reposition on show — the method already no-ops when the panel is already on the right display.
1 parent 3c4ff0f commit 533f9e4

4 files changed

Lines changed: 23 additions & 20 deletions

File tree

src/helpers/diarization.js

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const { resolveBinaryPath, gracefulStopProcess } = require("../utils/serverUtils
88
const { getModelsDirForService } = require("./modelDirUtils");
99
const { convertToWav } = require("./ffmpegUtils");
1010
const { getSafeTempDir } = require("./safeTempDir");
11-
const { applyProvisionalSpeaker, applyConfirmedSpeaker } = require("./speakerAssignmentPolicy");
11+
const { applyConfirmedSpeaker } = require("./speakerAssignmentPolicy");
1212
const sidecarPidFile = require("./sidecarPidFile");
1313
const {
1414
transcriptsOverlap,
@@ -459,8 +459,6 @@ class DiarizationManager {
459459
return null;
460460
};
461461

462-
let fallbackSpeakerIndex = speakerSet.size;
463-
464462
return deduped.map((seg, index) => {
465463
const enriched = { ...seg };
466464

@@ -505,12 +503,6 @@ class DiarizationManager {
505503
speaker: speakerMap.get(bestSpeaker) || bestSpeaker,
506504
speakerIsPlaceholder: false,
507505
});
508-
} else if (!enriched.speaker) {
509-
applyProvisionalSpeaker(enriched, {
510-
speaker: `speaker_${fallbackSpeakerIndex}`,
511-
speakerIsPlaceholder: true,
512-
});
513-
fallbackSpeakerIndex += 1;
514506
}
515507
}
516508

src/helpers/ipcHandlers.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7648,7 +7648,9 @@ class IPCHandlers {
76487648
);
76497649
this.activeMeetingSpeakerConfig = { enabled, expectedCount };
76507650
liveSpeakerIdentifier.setEnabled(enabled);
7651-
liveSpeakerIdentifier.setMaxSpeakers(expectedCount);
7651+
// Live identification only labels other speakers (the mic track is "you"),
7652+
// so cap at expectedCount - 1 to match resolveSessionMaxSpeakers().
7653+
liveSpeakerIdentifier.setMaxSpeakers(Math.max(1, expectedCount - 1));
76527654
return { success: true };
76537655
} catch (error) {
76547656
return { success: false, error: error.message };

src/helpers/windowManager.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,11 +1021,7 @@ class WindowManager {
10211021
showDictationPanel(options = {}) {
10221022
const { focus = false } = options;
10231023
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
1024-
const wasHidden = !this.mainWindow.isVisible() || this.mainWindow.isMinimized();
1025-
1026-
if (wasHidden) {
1027-
this._repositionToCursorDisplay();
1028-
}
1024+
this._repositionToCursorDisplay();
10291025

10301026
if (this.mainWindow.isMinimized()) {
10311027
this.mainWindow.restore();

src/stores/meetingRecordingStore.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,16 @@ function reserveSpeakerIndex(speakerId?: string) {
537537
nextPlaceholderSpeakerIndex = Math.max(nextPlaceholderSpeakerIndex, idx + 1);
538538
}
539539

540+
// Other-speaker cap is expectedCount - 1 (the mic track is "you"); mirrors the
541+
// backend cap so live labels can't climb past the count the user expects.
542+
function mintPlaceholderSpeakerId(): string {
543+
const expected = useMeetingRecordingStore.getState().sessionExpectedCount;
544+
const cap = Math.max(1, expected - 1);
545+
const index = Math.min(nextPlaceholderSpeakerIndex, cap - 1);
546+
nextPlaceholderSpeakerIndex = Math.max(nextPlaceholderSpeakerIndex, index + 1);
547+
return `speaker_${index}`;
548+
}
549+
540550
function assignProvisionalSpeaker(segment: TranscriptSegment): TranscriptSegment {
541551
if (segment.source !== "system" || segment.speaker) return segment;
542552

@@ -584,8 +594,7 @@ function assignProvisionalSpeaker(segment: TranscriptSegment): TranscriptSegment
584594
});
585595
}
586596

587-
const speakerId = `speaker_${nextPlaceholderSpeakerIndex}`;
588-
nextPlaceholderSpeakerIndex += 1;
597+
const speakerId = mintPlaceholderSpeakerId();
589598

590599
return normalizeTranscriptSegment({
591600
...segment,
@@ -888,9 +897,13 @@ export async function startRecording(args: StartRecordingArgs): Promise<void> {
888897
} else {
889898
useMeetingRecordingStore.setState({ systemPartial: data.text });
890899
if (!systemPartialSpeakerIdValue) {
891-
const speakerId = `speaker_${nextPlaceholderSpeakerIndex}`;
892-
nextPlaceholderSpeakerIndex += 1;
893-
setSystemPartialSpeakerIdentity(speakerId, null);
900+
// Reuse the recent system speaker before minting — the partial id is
901+
// cleared after every final, so always minting spawned one per utterance.
902+
const carried = getRecentSystemSpeaker(Date.now());
903+
setSystemPartialSpeakerIdentity(
904+
carried?.speakerId ?? mintPlaceholderSpeakerId(),
905+
carried?.speakerName ?? null
906+
);
894907
}
895908
}
896909
return;

0 commit comments

Comments
 (0)