import Foundation /// `visual_timeline.json` (schema 1.1) — the app's primary visual output. Times /// are seconds relative to session t0. Segments may overlap (crosstalk). struct VisualTimeline: Codable { var schemaVersion = "1.1" let sessionId: String let app: String let adapterVersion: String let t0Unix: Double let durationSec: Double let fpsSampled: Int let selfName: String? let participants: [Participant] let segments: [Segment] let visualGaps: [Gap] struct Participant: Codable { let name: String let isSelf: Bool? let aliases: [String]? enum CodingKeys: String, CodingKey { case name case isSelf = "is_self" case aliases } } struct Segment: Codable, Equatable { let start: Double let end: Double let name: String let confidence: Double let source: String // vision | accessibility | fused | mic_vad } struct Gap: Codable, Equatable { let start: Double let end: Double let reason: String // minimized | tab_switched } enum CodingKeys: String, CodingKey { case schemaVersion = "schema_version" case sessionId = "session_id" case app case adapterVersion = "adapter_version" case t0Unix = "t0_unix" case durationSec = "duration_sec" case fpsSampled = "fps_sampled" case selfName = "self_name" case participants case segments case visualGaps = "visual_gaps" } /// Write the rich `visual_timeline.json`. func write(to url: URL) throws { let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] try encoder.encode(self).write(to: url) } /// The flat array `label-merge` wants: `[{start,end,name,confidence}]`, /// dropping `source`. Slice/rebase to chunk-local seconds happens in Phase 5. func flatTimelineData() throws -> Data { let flat = segments.map { seg -> [String: Any] in ["start": seg.start, "end": seg.end, "name": seg.name, "confidence": seg.confidence] } return try JSONSerialization.data(withJSONObject: flat, options: []) } }