From d7b2fabbb3272d174462564b239ea71f96bfcc68 Mon Sep 17 00:00:00 2001 From: bigmacfive Date: Sat, 15 Aug 2026 21:33:52 +0900 Subject: [PATCH] Stabilize live speaker labels --- CHANGELOG.md | 6 ++++++ package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- src/editor/KukuEditor.meeting.test.tsx | 5 +++++ src/editor/KukuEditor.tsx | 27 +++++++++++++++++++++++--- 7 files changed, 39 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddb4607..bb567be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to Ulpaso will be documented in this file. The format follow ## [Unreleased] +## [0.5.4] - 2026-08-15 + +### Fixed + +- Keep provisional streaming diarization from showing third or fourth speaker labels before the bounded final pass verifies the real speaker count. + ## [0.5.3] - 2026-08-15 ### Fixed diff --git a/package.json b/package.json index 4072913..4ae39d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ulpaso", - "version": "0.5.3", + "version": "0.5.4", "private": true, "description": "A local Markdown editor with on-device meeting transcription for macOS", "license": "MIT", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 54bafa5..e088bfd 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4484,7 +4484,7 @@ dependencies = [ [[package]] name = "ulpaso" -version = "0.5.3" +version = "0.5.4" dependencies = [ "cc", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b60a94c..5cd26b9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ulpaso" -version = "0.5.3" +version = "0.5.4" description = "A quiet, focused Markdown editor" authors = ["Ulpaso contributors"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ce36942..34989fe 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Ulpaso", - "version": "0.5.3", + "version": "0.5.4", "identifier": "app.ulpaso.editor", "build": { "beforeDevCommand": "pnpm dev", diff --git a/src/editor/KukuEditor.meeting.test.tsx b/src/editor/KukuEditor.meeting.test.tsx index e964b9d..083dc0b 100644 --- a/src/editor/KukuEditor.meeting.test.tsx +++ b/src/editor/KukuEditor.meeting.test.tsx @@ -246,6 +246,11 @@ describe("KukuEditor meeting integration", () => { handle!.updateMeeting("session-over-split", "첫 발언 임시 둘", "", 2); handle!.updateMeeting("session-over-split", "첫 발언 임시 둘 상대방", "", 3); handle!.updateMeeting("session-over-split", "첫 발언 임시 둘 상대방 임시 넷", "", 4); + + const liveMarkdown = handle!.getMarkdown(); + expect(liveMarkdown).not.toContain("**Speaker 3**"); + expect(liveMarkdown).not.toContain("**Speaker 4**"); + handle!.finalizeMeeting("session-over-split", [ { speaker: 1, text: "첫 발언 임시 둘" }, { speaker: 2, text: "상대방 임시 넷" }, diff --git a/src/editor/KukuEditor.tsx b/src/editor/KukuEditor.tsx index fbe9ff7..ed43af1 100644 --- a/src/editor/KukuEditor.tsx +++ b/src/editor/KukuEditor.tsx @@ -80,6 +80,7 @@ interface ActiveMeetingDocument { title: string; lastStableText: string; segments: MeetingTranscriptSegment[]; + provisionalSpeakers: Map; } let markdownInitialized = false; @@ -201,6 +202,20 @@ export default function KukuEditor(props: KukuEditorProps) { return createMeetingDocumentNodes(title, segments).map((node) => schema.nodeFromJSON(node)); } + function provisionalMeetingSpeaker( + meeting: ActiveMeetingDocument, + speakerId: number | undefined, + ): number | null { + const previous = meeting.segments.at(-1)?.speaker ?? null; + if (speakerId == null) return previous; + const existing = meeting.provisionalSpeakers.get(speakerId); + if (existing != null) return existing; + if (meeting.provisionalSpeakers.size >= 2) return previous; + const provisional = meeting.provisionalSpeakers.size + 1; + meeting.provisionalSpeakers.set(speakerId, provisional); + return provisional; + } + function replaceMeetingContent( meeting: ActiveMeetingDocument, segments: MeetingTranscriptSegment[], @@ -426,7 +441,13 @@ export default function KukuEditor(props: KukuEditorProps) { const topLevel = $from.depth >= 1 ? $from.node(1) : null; const from = topLevel && !topLevel.textContent.trim() ? $from.before(1) : $from.depth >= 1 ? $from.after(1) : state.selection.to; const to = topLevel && !topLevel.textContent.trim() ? from + topLevel.nodeSize : from; - activeMeeting = { id: sessionId, title, lastStableText: "", segments: [] }; + activeMeeting = { + id: sessionId, + title, + lastStableText: "", + segments: [], + provisionalSpeakers: new Map(), + }; followMeetingTail = true; const nodes = meetingNodes(title, []); const fragment = Fragment.fromArray(nodes); @@ -443,12 +464,12 @@ export default function KukuEditor(props: KukuEditorProps) { let updatedIncrementally = false; let needsFullReplacement = false; const isFirstStableText = !meeting.lastStableText; + const normalizedSpeaker = provisionalMeetingSpeaker(meeting, speakerId); if (stableText !== meeting.lastStableText) { if (stableText.startsWith(meeting.lastStableText)) { const delta = stableText.slice(meeting.lastStableText.length).trim(); if (delta) { const previous = meeting.segments.at(-1); - const normalizedSpeaker = speakerId ?? previous?.speaker ?? null; if (previous && previous.speaker === normalizedSpeaker) { const separator = previous.text ? " " : ""; previous.text = `${previous.text}${separator}${delta}`; @@ -465,7 +486,7 @@ export default function KukuEditor(props: KukuEditorProps) { meeting.segments = reconcileMeetingTranscriptSegments( meeting.segments, stableText, - speakerId, + normalizedSpeaker ?? undefined, ); needsFullReplacement = true; }