2026-07-05 13:34:16 +02:00
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 < PreinstallingEvent >
/// The continuation that feeds ``events``.
private let continuation : AsyncStream < PreinstallingEvent >. 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
2026-07-05 16:16:53 +02:00
/// 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.
2026-07-05 13:34:16 +02:00
///
/// - Parameter locale: The locale to preinstall the speech model assets for.
public func preinstall (
for locale : Locale
) async {
2026-07-05 16:16:53 +02:00
guard let supported = await supportedLocale ( equivalentTo : locale ) else {
2026-07-05 13:34:16 +02:00
return
}
do {
2026-07-05 16:16:53 +02:00
try await install ( for : supported ) { [ continuation ] in
2026-07-05 13:34:16 +02:00
continuation . yield (. started ( locale ))
}
} catch {
if error is CancellationError || Task . isCancelled {
continuation . yield (. cancelled ( locale ))
} else {
continuation . yield (. failed ( locale ))
2026-07-05 16:16:53 +02:00
logger . error ( "The speech model assets for the \" \( supported . identifier , privacy : . public ) \" locale failed to preinstall: \( String ( describing : error ) , privacy : . public ) " )
2026-07-05 13:34:16 +02:00
}
}
}
/// 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"
)