129 lines
5.2 KiB
Swift
129 lines
5.2 KiB
Swift
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
|
|
/// 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"
|
|
)
|