import AVFoundation import Foundation /// One buffer of captured audio, in transit from the audio thread to the speech engine. /// /// `AVAudioPCMBuffer` isn't `Sendable`, and `AVAudioEngine` recycles the buffer it hands /// to a tap the moment the callback returns. The unchecked conformance is only sound /// because `AudioCapture` allocates a **fresh** buffer for every chunk and never touches /// it again after handing it over — don't construct one of these around a borrowed buffer. public struct AudioChunk: @unchecked Sendable { public let buffer: AVAudioPCMBuffer public init(buffer: AVAudioPCMBuffer) { self.buffer = buffer } } /// A snapshot of the running transcript. /// /// `text` is always the **full transcript so far**, not a delta — engines revise /// earlier words as more audio arrives, so consumers should replace rather than append. public struct TranscriptionChunk: Sendable { public let text: String /// `true` once the engine has committed everything it will emit for this session. public let isFinal: Bool } /// The seam that keeps the app engine-agnostic. /// /// Apple's `SpeechAnalyzer` ships with macOS 26 and needs no model download, so it is /// the default. Parakeet (FluidAudio, CoreML/ANE) scores better on English and is the /// intended upgrade — implementing this protocol is the whole cost of switching. public protocol TranscriptionEngine: Actor { /// Audio format the engine wants buffers delivered in. `AudioCapture` converts to it. func preferredInputFormat() async -> AVAudioFormat? /// Prepare models and open a session. Emits snapshots until `finish()` is called. func start() async throws -> AsyncThrowingStream /// Feed one buffer of captured microphone audio, already in `preferredInputFormat()`. func feed(_ chunk: AudioChunk) async /// Close the session and flush any pending final results. func finish() async } public enum TranscriptionError: LocalizedError { case localeUnsupported(Locale) case modelInstallFailed(String) case noAudioFormat case notRunning public var errorDescription: String? { switch self { case .localeUnsupported(let locale): return "Dictation isn't available for \(locale.identifier) on this Mac." case .modelInstallFailed(let detail): return "Couldn't install the speech model: \(detail)" case .noAudioFormat: return "No compatible audio format available for the speech engine." case .notRunning: return "The transcription engine isn't running." } } }