40 lines
1.5 KiB
Swift
40 lines
1.5 KiB
Swift
import Foundation
|
|
|
|
/// Speech results may revise their time boundaries when they become final.
|
|
/// Match coverage rather than an exact rounded start timestamp.
|
|
struct TranscriptAccumulator {
|
|
private struct Segment {
|
|
var start: Double
|
|
var end: Double
|
|
var text: String
|
|
}
|
|
private var finalized: [Segment] = []
|
|
private var volatile: [Segment] = []
|
|
private let tolerance = 0.001
|
|
|
|
var text: String {
|
|
(finalized + volatile).sorted { $0.start < $1.start }.map(\.text).joined()
|
|
}
|
|
|
|
mutating func apply(start: Double, end: Double, text: String, final: Bool) {
|
|
guard start.isFinite, end.isFinite, end >= start else { return }
|
|
let segment = Segment(start: start, end: end, text: text)
|
|
func overlaps(_ other: Segment) -> Bool {
|
|
abs(other.start - start) <= tolerance ||
|
|
(other.start < end - tolerance && other.end > start + tolerance)
|
|
}
|
|
if final {
|
|
finalized.removeAll(where: overlaps)
|
|
// A final result supersedes provisional coverage up to its end,
|
|
// including a slightly shifted start boundary.
|
|
volatile.removeAll { $0.end <= end + tolerance || overlaps($0) }
|
|
if !text.isEmpty { finalized.append(segment) }
|
|
} else {
|
|
volatile.removeAll(where: overlaps)
|
|
// Late provisional callbacks must never duplicate finalized audio.
|
|
guard !finalized.contains(where: overlaps) else { return }
|
|
if !text.isEmpty { volatile.append(segment) }
|
|
}
|
|
}
|
|
}
|