import Foundation import OSLog import Speech /// The preinstalling service that downloads speech model assets through the system's asset inventory — the sole owner of the /// asset inventory knowledge in the feature. /// /// The service offers two installation paths over the same core. ``preinstall(for:)`` installs the assets for the supported equivalent /// of a given locale ahead of its first transcription, logging and emitting its outcomes through ``events`` rather than throwing. /// ``install(for:onDownloadStart:)`` installs the assets for an already supported locale and throws on failure, for callers that /// need the assets right away — like the transcribing service, which uses it as its fallback when a transcription starts. /// /// Either path releases the locale reservations of any other locales beforehand: the feature transcribes a single locale at a time, /// and the system only permits a limited number of reservations per app — exceeding it would make the installation request throw. public final class AssetPreinstalling: Preinstalling { // MARK: Properties /// The stream of events the service emits while preinstalling. public let events: AsyncStream /// The continuation that feeds ``events``. private let continuation: AsyncStream.Continuation // MARK: Initializers /// Creates an asset preinstalling service. public init() { (events, continuation) = AsyncStream.makeStream(of: PreinstallingEvent.self) } deinit { continuation.finish() } // MARK: Methods /// Installs the speech model assets for the given supported locale, waiting for any download to finish. /// /// The locale reservations of any other locales are released beforehand. Unlike ``preinstall(for:)``, failures are thrown rather /// than emitted, so a caller that needs the assets right away — like the transcribing service — can react to them directly. /// /// - Parameters: /// - locale: The supported locale to install the speech model assets for. /// - onDownloadStart: The closure invoked when the assets are missing and their download starts. Defaults to `nil`. /// - Throws: Any error thrown while reserving, downloading, or installing the assets, including a `CancellationError` /// when the surrounding task is cancelled mid-download. public func install( for locale: Locale, onDownloadStart: (@Sendable () -> Void)? = nil ) async throws { for reserved in await AssetInventory.reservedLocales where reserved != locale { _ = await AssetInventory.release(reservedLocale: reserved) } let transcriber = SpeechTranscriber( locale: locale, preset: .transcription ) guard let request = try await AssetInventory.assetInstallationRequest( supporting: [transcriber] ) else { return } onDownloadStart?() try await withTaskCancellationHandler { try await request.downloadAndInstall() } onCancel: { [progress = request.progress] in progress.cancel() } } /// Preinstalls the speech model assets for the supported equivalent of the given locale, emitting the started, cancelled, and /// failed downloads through ``events`` — a download that finishes, or assets that are already installed, emit nothing. The /// emitted events carry the given locale, not its supported equivalent, so a caller can match them against its requests. /// /// - Parameter locale: The locale to preinstall the speech model assets for. public func preinstall( for locale: Locale ) async { guard let supported = await supportedLocale(equivalentTo: locale) else { return } do { try await install(for: supported) { [continuation] in continuation.yield(.started(locale)) } } catch { if error is CancellationError || Task.isCancelled { continuation.yield(.cancelled(locale)) } else { continuation.yield(.failed(locale)) logger.error("The speech model assets for the \"\(supported.identifier, privacy: .public)\" locale failed to preinstall: \(String(describing: error), privacy: .public)") } } } /// Returns the supported locale equivalent to the given locale. /// /// - Parameter locale: The locale to find a supported equivalent for. /// - Returns: The supported equivalent, or `nil` when the transcriber supports none. public func supportedLocale( equivalentTo locale: Locale ) async -> Locale? { await SpeechTranscriber.supportedLocale(equivalentTo: locale) } /// Returns the locales the transcriber supports. /// /// - Returns: The supported locales, including ones whose assets are not installed yet. public func supportedLocales() async -> [Locale] { await SpeechTranscriber.supportedLocales } } // MARK: - Constants /// The logger that records the failures of the asset preinstalling service. private let logger = Logger( subsystem: "Features.Recording", category: "AssetPreinstalling" )