178 lines
5.2 KiB
Swift
178 lines
5.2 KiB
Swift
import Foundation
|
|
|
|
enum TranscriptTimestampFormatter {
|
|
static let formatter: DateFormatter = {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "HH:mm:ss"
|
|
return formatter
|
|
}()
|
|
|
|
static func string(from date: Date) -> String {
|
|
formatter.string(from: date)
|
|
}
|
|
}
|
|
|
|
enum AudioSource: String, Codable, CaseIterable {
|
|
case mic = "MIC"
|
|
case system = "SYS"
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .mic:
|
|
return "Me"
|
|
case .system:
|
|
return "Them"
|
|
}
|
|
}
|
|
|
|
var copyPrefix: String {
|
|
switch self {
|
|
case .mic:
|
|
return "Me"
|
|
case .system:
|
|
return "Them"
|
|
}
|
|
}
|
|
|
|
var icon: String {
|
|
switch self {
|
|
case .mic:
|
|
return "mic.fill"
|
|
case .system:
|
|
return "speaker.wave.2.fill"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct TranscriptChunk: Codable, Identifiable, Hashable {
|
|
let id: UUID
|
|
let timestamp: Date
|
|
let source: AudioSource
|
|
let speaker: Int?
|
|
let text: String
|
|
let isFinal: Bool
|
|
|
|
init(id: UUID = UUID(), timestamp: Date = Date(), source: AudioSource, speaker: Int? = nil, text: String, isFinal: Bool = false) {
|
|
self.id = id
|
|
self.timestamp = timestamp
|
|
self.source = source
|
|
self.speaker = speaker
|
|
self.text = text
|
|
self.isFinal = isFinal
|
|
}
|
|
|
|
var displayName: String {
|
|
if source == .mic { return "Me" }
|
|
if let speaker { return "Speaker \(speaker)" }
|
|
return source.displayName
|
|
}
|
|
}
|
|
|
|
struct CollapsedTranscriptChunk: Identifiable {
|
|
let id: UUID
|
|
let timestamp: Date
|
|
let source: AudioSource
|
|
let speaker: Int?
|
|
let combinedText: String
|
|
|
|
init(id: UUID = UUID(), timestamp: Date, source: AudioSource, speaker: Int? = nil, combinedText: String) {
|
|
self.id = id
|
|
self.timestamp = timestamp
|
|
self.source = source
|
|
self.speaker = speaker
|
|
self.combinedText = combinedText
|
|
}
|
|
|
|
var displayName: String {
|
|
if source == .mic { return "Me" }
|
|
if let speaker { return "Speaker \(speaker)" }
|
|
return source.displayName
|
|
}
|
|
}
|
|
|
|
struct Meeting: Codable, Identifiable, Hashable {
|
|
let id: UUID
|
|
let date: Date
|
|
var title: String
|
|
var transcriptChunks: [TranscriptChunk]
|
|
var userNotes: String
|
|
var generatedNotes: String
|
|
var templateId: UUID? // Add property to track per-meeting template
|
|
var recoveryAudioFolderName: String?
|
|
var transcriptionError: String?
|
|
// MARK: - Data versioning
|
|
/// Version of this Meeting record on disk. Useful for migration.
|
|
var dataVersion: Int
|
|
/// Current app data version. Increment whenever you make a breaking change to `Meeting` that requires migration.
|
|
static let currentDataVersion = 1
|
|
|
|
init(id: UUID = UUID(),
|
|
date: Date = Date(),
|
|
title: String = "",
|
|
transcriptChunks: [TranscriptChunk] = [],
|
|
userNotes: String = "",
|
|
generatedNotes: String = "",
|
|
templateId: UUID? = nil,
|
|
recoveryAudioFolderName: String? = nil,
|
|
transcriptionError: String? = nil,
|
|
dataVersion: Int = Meeting.currentDataVersion) {
|
|
self.id = id
|
|
self.date = date
|
|
self.title = title
|
|
self.transcriptChunks = transcriptChunks
|
|
self.userNotes = userNotes
|
|
self.generatedNotes = generatedNotes
|
|
self.templateId = templateId
|
|
self.recoveryAudioFolderName = recoveryAudioFolderName
|
|
self.transcriptionError = transcriptionError
|
|
self.dataVersion = dataVersion
|
|
}
|
|
|
|
// `Codable` conformance now uses the compiler-synthesised implementation.
|
|
|
|
// Computed property for backward compatibility with existing code
|
|
var transcript: String {
|
|
return transcriptChunks
|
|
.filter { $0.isFinal }
|
|
.map { "[\($0.displayName)] \($0.text)" }
|
|
.joined(separator: " ")
|
|
}
|
|
|
|
// Formatted transcript for copying with collapsed sequential chunks
|
|
var formattedTranscript: String {
|
|
let finalChunks = transcriptChunks.filter { $0.isFinal }
|
|
|
|
return finalChunks.map { chunk in
|
|
"[\(TranscriptTimestampFormatter.string(from: chunk.timestamp))] \(chunk.displayName): \(chunk.text)"
|
|
}.joined(separator: "\n")
|
|
}
|
|
|
|
// Collapsed chunks for UI display
|
|
var collapsedTranscriptChunks: [CollapsedTranscriptChunk] {
|
|
transcriptChunks.filter(\.isFinal).map { chunk in
|
|
CollapsedTranscriptChunk(
|
|
id: chunk.id,
|
|
timestamp: chunk.timestamp,
|
|
source: chunk.source,
|
|
speaker: chunk.speaker,
|
|
combinedText: chunk.text
|
|
)
|
|
}
|
|
}
|
|
|
|
// Separate computed properties for mic and system transcripts
|
|
var micTranscript: String {
|
|
return transcriptChunks
|
|
.filter { $0.source == .mic && $0.isFinal }
|
|
.map { $0.text }
|
|
.joined(separator: " ")
|
|
}
|
|
|
|
var systemTranscript: String {
|
|
return transcriptChunks
|
|
.filter { $0.source == .system && $0.isFinal }
|
|
.map { $0.text }
|
|
.joined(separator: " ")
|
|
}
|
|
}
|