diff --git a/Ten31Transcripts/Session/SessionController.swift b/Ten31Transcripts/Session/SessionController.swift index 1057b52..279c308 100644 --- a/Ten31Transcripts/Session/SessionController.swift +++ b/Ten31Transcripts/Session/SessionController.swift @@ -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 } diff --git a/Ten31Transcripts/UI/MenuBarView.swift b/Ten31Transcripts/UI/MenuBarView.swift index e6361bc..86fe4a6 100644 --- a/Ten31Transcripts/UI/MenuBarView.swift +++ b/Ten31Transcripts/UI/MenuBarView.swift @@ -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)