Fix mis-attributed fragments + LLM naming guardrails + re-process saved sessions
Investigating Grant's real 38-min group call: 'Marty' was a GARBAGE cluster (192 segs, 0.37s mean, 186 ≤2 words, 125 single words flanked by the same other speaker — diarization micro-fragments split mid-sentence, then LLM-named 'Marty'). Same for 'Message'/'HI'. - SpeakerReconciler.smoothFragments: dissolve non-self clusters whose MEDIAN segment duration ≤ 1s (≥3 segs) — reassign each fragment to the temporally-nearest real speaker. (Median, not max, so one stray long segment can't rescue a fragment cluster — the bug in the first cut.) On the real call: 7 speakers (3 junk) → 4 real (Marty/Message/HI absorbed into Grant/Jonathan/Me/MH). Runs before LLM naming. - LLM naming guardrails: forbid assigning the self name or ANY already-taken name to another voice (fixes 'Grant' = the user's name pinned on a remote speaker); prompt demands self-intro / direct-address evidence (mention ≠ presence), 'precision over coverage', one name per speaker. - Open saved session now offers Open Editor vs Re-process, so newer logic can be applied to past calls (+ always-visible progress from the prior fix). NOTE: the self-name guardrail needs the app to KNOW the user's name — selfName is still 'Me', so set it in Settings (e.g. 'Grant') so the LLM can't reuse it. 62/62 XCTest.
This commit is contained in:
@@ -470,12 +470,24 @@ final class SessionController: ObservableObject {
|
||||
guard panel.runModal() == .OK, let folder = panel.url else { return }
|
||||
let fm = FileManager.default
|
||||
if fm.fileExists(atPath: folder.appendingPathComponent("speakers.json").path) {
|
||||
if !openEditor(folder: folder) {
|
||||
Self.alert("Couldn't open this session — its transcript looks empty or unreadable.")
|
||||
// Already transcribed — edit it, or re-process to apply newer logic.
|
||||
switch Self.editOrReprocess() {
|
||||
case .edit:
|
||||
if !openEditor(folder: folder) {
|
||||
Self.alert("Couldn't open this session — its transcript looks empty or unreadable.")
|
||||
}
|
||||
case .reprocess: reprocess(folder)
|
||||
case .cancel: break
|
||||
}
|
||||
return
|
||||
}
|
||||
// Not transcribed yet — needs the raw tracks to (re)process.
|
||||
reprocess(folder) // not transcribed yet — must process
|
||||
}
|
||||
|
||||
/// Transcribe + reconcile + recap a saved session folder from its raw tracks, then
|
||||
/// open the editor. Used by "Open saved session" (fresh, or re-process choice).
|
||||
private func reprocess(_ folder: URL) {
|
||||
let fm = FileManager.default
|
||||
let mic = folder.appendingPathComponent("mic.wav")
|
||||
let sys = folder.appendingPathComponent("system.wav")
|
||||
guard fm.fileExists(atPath: mic.path), fm.fileExists(atPath: sys.path) else {
|
||||
@@ -500,6 +512,22 @@ final class SessionController: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private enum SavedAction { case edit, reprocess, cancel }
|
||||
private static func editOrReprocess() -> SavedAction {
|
||||
let a = NSAlert()
|
||||
a.messageText = "This session is already transcribed"
|
||||
a.informativeText = "Open the speaker editor, or re-process it from the audio to apply the latest naming/cleanup."
|
||||
a.addButton(withTitle: "Open Editor") // .alertFirstButtonReturn
|
||||
a.addButton(withTitle: "Re-process") // .alertSecondButtonReturn
|
||||
a.addButton(withTitle: "Cancel") // .alertThirdButtonReturn
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
switch a.runModal() {
|
||||
case .alertFirstButtonReturn: return .edit
|
||||
case .alertSecondButtonReturn: return .reprocess
|
||||
default: return .cancel
|
||||
}
|
||||
}
|
||||
|
||||
/// The remote (vision) visual-timeline segments saved for a session, if any.
|
||||
private static func remoteTimeline(in folder: URL) -> [VisualTimeline.Segment] {
|
||||
guard let data = try? Data(contentsOf: folder.appendingPathComponent("visual_timeline.json")),
|
||||
|
||||
Reference in New Issue
Block a user