diff --git a/Apps/Attendi/AudioTranscribingService.swift b/Apps/Attendi/AudioTranscribingService.swift new file mode 100644 index 0000000..bed14c2 --- /dev/null +++ b/Apps/Attendi/AudioTranscribingService.swift @@ -0,0 +1,81 @@ +import AVFoundation +import Recording +import Speech + +/// The transcribing service used by the Attendi sample app, which transcribes recorded audio into text on device. +/// +/// The service writes the recorded audio into a temporary `.m4a` file and runs it through a `SpeechAnalyzer` with a `SpeechTranscriber` +/// module, joining the finalized results into the returned text. The speech model assets for the current locale are downloaded and installed +/// on first use; every transcription after that happens entirely offline. +struct AudioTranscribingService: TranscribingService { + + // MARK: Initializers + + /// Creates a speech transcribing service. + init() {} + + // MARK: Methods + + /// Transcribes the given recorded audio into text, deleting the temporary audio file when the transcription finishes. + /// + /// - Parameter audio: The recorded audio to transcribe. + /// - Returns: The transcription of the recorded audio. + /// - Throws: ``AudioTranscribingError/localeNotSupported`` when the transcriber does not support the current locale, or any + /// error thrown while installing the speech model assets, reading the audio file, or analyzing its contents. + func transcribe(_ audio: Data) async throws -> String { + try audio.write(to: Constant.File.url) + + defer { + try? FileManager.default.removeItem(at: Constant.File.url) + } + + guard let locale = await SpeechTranscriber.supportedLocale(equivalentTo: .current) else { + throw AudioTranscribingError.localeNotSupported + } + + let transcriber = SpeechTranscriber( + locale: locale, + preset: .transcription + ) + + if let request = try await AssetInventory.assetInstallationRequest(supporting: [transcriber]) { + try await request.downloadAndInstall() + } + + let analyzer = SpeechAnalyzer(modules: [transcriber]) + + async let text = transcriber.results.reduce(into: "") { text, result in + text += String(result.text.characters) + } + + let file = try AVAudioFile(forReading: Constant.File.url) + + if let lastSampleTime = try await analyzer.analyzeSequence(from: file) { + try await analyzer.finalizeAndFinish(through: lastSampleTime) + } else { + await analyzer.cancelAndFinishNow() + } + + return try await text + } + +} + +// MARK: - Errors + +/// The errors thrown by ``AudioTranscribingService``. +enum AudioTranscribingError: Error { + /// The transcriber does not support the current locale. + case localeNotSupported +} + +// MARK: - Constants + +/// The constant values used across the speech transcribing service. +private nonisolated enum Constant { + /// The file constants. + enum File { + /// The location of the temporary file the recorded audio is written into for the analysis. + static let url = FileManager.default.temporaryDirectory.appending(path: "transcription.m4a") + } +} diff --git a/Apps/Attendi/ContentView.swift b/Apps/Attendi/ContentView.swift index d674be2..025a26a 100644 --- a/Apps/Attendi/ContentView.swift +++ b/Apps/Attendi/ContentView.swift @@ -8,11 +8,11 @@ struct ContentView: View { // MARK: Body - /// The content of the view: the recording feature's view, attached to the microphone-backed recording service and the simulated transcribing service. + /// The content of the view: the recording feature's view, attached to the microphone-backed recording service and the on-device speech transcribing service. var body: some View { RecordingView( recorder: AudioRecordingService(), - transcriber: SimulatedTranscribingService() + transcriber: AudioTranscribingService() ) } diff --git a/Attendi.xcodeproj/project.pbxproj b/Attendi.xcodeproj/project.pbxproj index a0a3115..c1ada36 100644 --- a/Attendi.xcodeproj/project.pbxproj +++ b/Attendi.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ Attendi/Assets.xcassets, Attendi/AttendiApp.swift, Attendi/AudioRecordingService.swift, + Attendi/AudioTranscribingService.swift, Attendi/ContentView.swift, ); target = 02870A0C2FF7EB610079EA3A /* Attendi */;