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:
@@ -8,6 +8,10 @@ struct SessionInfo: Equatable {
|
|||||||
let mixedURL: URL
|
let mixedURL: URL
|
||||||
let duration: Double
|
let duration: Double
|
||||||
let selfSpanCount: Int
|
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
|
/// 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
|
/// Stop visual capture (if any), write `visual_timeline.json`, and return the
|
||||||
/// timeline for the backend: visual segments + merged self-spans when visual
|
/// timeline for the backend: visual segments + merged self-spans when visual
|
||||||
/// ran, otherwise the mic-VAD self spans alone.
|
/// ran, otherwise the mic-VAD self spans alone. `visualRan` reports whether the
|
||||||
private func stopVisualAndTimeline(_ result: RecordingResult, folder: URL?) async -> [VisualTimeline.Segment] {
|
/// 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
|
let selfName = settings.selfName
|
||||||
if let vc = visualCapture, let folder {
|
if let vc = visualCapture, let folder {
|
||||||
visualCapture = nil
|
visualCapture = nil
|
||||||
return await vc.finish(
|
let timeline = await vc.finish(
|
||||||
selfSpans: result.selfSpans, selfName: selfName,
|
selfSpans: result.selfSpans, selfName: selfName,
|
||||||
sessionId: folder.lastPathComponent, t0Unix: result.t0Unix,
|
sessionId: folder.lastPathComponent, t0Unix: result.t0Unix,
|
||||||
durationSec: result.duration, folder: folder)
|
durationSec: result.duration, folder: folder)
|
||||||
|
return (timeline, true)
|
||||||
}
|
}
|
||||||
if let vc = visualCapture { await vc.cancel(); visualCapture = nil }
|
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() {
|
private func stop() {
|
||||||
@@ -290,12 +297,12 @@ final class SessionController: ObservableObject {
|
|||||||
lifecycleGeneration += 1
|
lifecycleGeneration += 1
|
||||||
lifecycleTask = Task {
|
lifecycleTask = Task {
|
||||||
let result = await recorder.stop()
|
let result = await recorder.stop()
|
||||||
let timeline = await self.stopVisualAndTimeline(result, folder: folder)
|
let visual = await self.stopVisualAndTimeline(result, folder: folder)
|
||||||
self.finish(result, timeline: timeline)
|
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
|
recorder = nil
|
||||||
micLevel = 0
|
micLevel = 0
|
||||||
systemLevel = 0
|
systemLevel = 0
|
||||||
@@ -303,9 +310,11 @@ final class SessionController: ObservableObject {
|
|||||||
transcriptStatus = .idle
|
transcriptStatus = .idle
|
||||||
if let folder = currentFolder {
|
if let folder = currentFolder {
|
||||||
writeSelfSpans(result, to: folder)
|
writeSelfSpans(result, to: folder)
|
||||||
|
let visualCount = visualRan ? timeline.filter { $0.source == "vision" }.count : nil
|
||||||
lastSession = SessionInfo(
|
lastSession = SessionInfo(
|
||||||
folder: folder, mixedURL: result.mixedURL,
|
folder: folder, mixedURL: result.mixedURL,
|
||||||
duration: result.duration, selfSpanCount: result.selfSpans.count)
|
duration: result.duration, selfSpanCount: result.selfSpans.count,
|
||||||
|
visualSegmentCount: visualCount)
|
||||||
lastProcess = ProcessInputs(
|
lastProcess = ProcessInputs(
|
||||||
folder: folder, sessionId: folder.lastPathComponent, app: currentLabel,
|
folder: folder, sessionId: folder.lastPathComponent, app: currentLabel,
|
||||||
mixedURL: result.mixedURL, timeline: timeline)
|
mixedURL: result.mixedURL, timeline: timeline)
|
||||||
@@ -387,8 +396,8 @@ final class SessionController: ObservableObject {
|
|||||||
stopTimer()
|
stopTimer()
|
||||||
let folder = currentFolder
|
let folder = currentFolder
|
||||||
let result = await recorder.stop()
|
let result = await recorder.stop()
|
||||||
let timeline = await stopVisualAndTimeline(result, folder: folder)
|
let visual = await stopVisualAndTimeline(result, folder: folder)
|
||||||
finish(result, timeline: timeline)
|
finish(result, timeline: visual.timeline, visualRan: visual.visualRan)
|
||||||
} else if lifecycleGeneration == gen {
|
} else if lifecycleGeneration == gen {
|
||||||
break // settled: no new transition was spawned
|
break // settled: no new transition was spawned
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ struct MenuBarView: View {
|
|||||||
Button {
|
Button {
|
||||||
NSWorkspace.shared.activateFileViewerSelecting([last.mixedURL])
|
NSWorkspace.shared.activateFileViewerSelecting([last.mixedURL])
|
||||||
} label: {
|
} 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)
|
.font(.caption)
|
||||||
}
|
}
|
||||||
.buttonStyle(.link)
|
.buttonStyle(.link)
|
||||||
|
|||||||
Reference in New Issue
Block a user