Surface whether visual capture ran on the last session

Visual capture falls back to audio-only silently, so the user couldn't tell if
it attached on a real call. SessionInfo now carries visualSegmentCount (nil =
audio-only; a count = visual ran, with that many vision-detected speaker
segments), shown in the menu as '… · N visual segments' or '… · audio-only'.
Makes the pending live-call validation unambiguous.
This commit is contained in:
Grant Gilliam
2026-06-06 10:21:44 -05:00
parent 880b56e426
commit 3785f6bdd0
2 changed files with 20 additions and 11 deletions
@@ -8,6 +8,10 @@ struct SessionInfo: Equatable {
let mixedURL: URL
let duration: Double
let selfSpanCount: Int
/// Count of vision-detected speaker segments if visual capture attached, or nil
/// if the session was audio-only (no adapter / no window / capture failed). Lets
/// the user see at a glance whether the visual pipeline ran on a real call.
let visualSegmentCount: Int?
}
/// Owns a single recording session: creates the session folder, drives
@@ -268,18 +272,21 @@ final class SessionController: ObservableObject {
/// Stop visual capture (if any), write `visual_timeline.json`, and return the
/// timeline for the backend: visual segments + merged self-spans when visual
/// ran, otherwise the mic-VAD self spans alone.
private func stopVisualAndTimeline(_ result: RecordingResult, folder: URL?) async -> [VisualTimeline.Segment] {
/// ran, otherwise the mic-VAD self spans alone. `visualRan` reports whether the
/// visual pipeline actually attached (for the after-session indicator).
private func stopVisualAndTimeline(_ result: RecordingResult, folder: URL?)
async -> (timeline: [VisualTimeline.Segment], visualRan: Bool) {
let selfName = settings.selfName
if let vc = visualCapture, let folder {
visualCapture = nil
return await vc.finish(
let timeline = await vc.finish(
selfSpans: result.selfSpans, selfName: selfName,
sessionId: folder.lastPathComponent, t0Unix: result.t0Unix,
durationSec: result.duration, folder: folder)
return (timeline, true)
}
if let vc = visualCapture { await vc.cancel(); visualCapture = nil }
return TranscriptPipeline.timeline(fromSelfSpans: result.selfSpans, selfName: selfName)
return (TranscriptPipeline.timeline(fromSelfSpans: result.selfSpans, selfName: selfName), false)
}
private func stop() {
@@ -290,12 +297,12 @@ final class SessionController: ObservableObject {
lifecycleGeneration += 1
lifecycleTask = Task {
let result = await recorder.stop()
let timeline = await self.stopVisualAndTimeline(result, folder: folder)
self.finish(result, timeline: timeline)
let visual = await self.stopVisualAndTimeline(result, folder: folder)
self.finish(result, timeline: visual.timeline, visualRan: visual.visualRan)
}
}
private func finish(_ result: RecordingResult, timeline: [VisualTimeline.Segment]) {
private func finish(_ result: RecordingResult, timeline: [VisualTimeline.Segment], visualRan: Bool) {
recorder = nil
micLevel = 0
systemLevel = 0
@@ -303,9 +310,11 @@ final class SessionController: ObservableObject {
transcriptStatus = .idle
if let folder = currentFolder {
writeSelfSpans(result, to: folder)
let visualCount = visualRan ? timeline.filter { $0.source == "vision" }.count : nil
lastSession = SessionInfo(
folder: folder, mixedURL: result.mixedURL,
duration: result.duration, selfSpanCount: result.selfSpans.count)
duration: result.duration, selfSpanCount: result.selfSpans.count,
visualSegmentCount: visualCount)
lastProcess = ProcessInputs(
folder: folder, sessionId: folder.lastPathComponent, app: currentLabel,
mixedURL: result.mixedURL, timeline: timeline)
@@ -387,8 +396,8 @@ final class SessionController: ObservableObject {
stopTimer()
let folder = currentFolder
let result = await recorder.stop()
let timeline = await stopVisualAndTimeline(result, folder: folder)
finish(result, timeline: timeline)
let visual = await stopVisualAndTimeline(result, folder: folder)
finish(result, timeline: visual.timeline, visualRan: visual.visualRan)
} else if lifecycleGeneration == gen {
break // settled: no new transition was spawned
}
+1 -1
View File
@@ -83,7 +83,7 @@ struct MenuBarView: View {
Button {
NSWorkspace.shared.activateFileViewerSelecting([last.mixedURL])
} label: {
Text("Last: \(Int(last.duration.rounded()))s · \(last.selfSpanCount) self-spans — reveal in Finder")
Text("Last: \(Int(last.duration.rounded()))s · \(last.selfSpanCount) self-spans · \(last.visualSegmentCount.map { "\($0) visual segments" } ?? "audio-only") — reveal in Finder")
.font(.caption)
}
.buttonStyle(.link)