From 6a0e7698a5944fe0f1e1d3766d03ddb844521027 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 4 Jul 2026 15:06:55 +0200 Subject: [PATCH] Updated the Capturing procotol in the Recording package target to pass captured audio as a file URL instead of data. --- .../Recording/Protocols/Capturing.swift | 4 +- .../Recording/Protocols/Transcribing.swift | 6 +- .../Recording/Services/AudioCapturing.swift | 17 ++--- .../Services/AudioTranscribing.swift | 31 +++----- .../Recording/Services/DummyCapturing.swift | 17 ++++- .../Services/DummyTranscribing.swift | 4 +- .../Tests/Recording/Mocks/CapturingMock.swift | 48 ++++++++++++ .../Tests/Recording/Mocks/ErrorMock.swift | 2 + .../Recording/Mocks/TranscribingMock.swift | 30 ++++++++ .../View Models/RecordingViewModelTests.swift | 76 ------------------- 10 files changed, 116 insertions(+), 119 deletions(-) create mode 100644 Packages/Features/Tests/Recording/Mocks/CapturingMock.swift create mode 100644 Packages/Features/Tests/Recording/Mocks/ErrorMock.swift create mode 100644 Packages/Features/Tests/Recording/Mocks/TranscribingMock.swift diff --git a/Packages/Features/Sources/Recording/Protocols/Capturing.swift b/Packages/Features/Sources/Recording/Protocols/Capturing.swift index ec2526f..48b29ed 100644 --- a/Packages/Features/Sources/Recording/Protocols/Capturing.swift +++ b/Packages/Features/Sources/Recording/Protocols/Capturing.swift @@ -19,7 +19,7 @@ public protocol Capturing: Sendable { /// Stops the recording. /// - /// - Returns: The audio captured since the recording started. - func stop() async throws -> Data + /// - Returns: The location of the audio file captured since the recording started. + func stop() async throws -> URL } diff --git a/Packages/Features/Sources/Recording/Protocols/Transcribing.swift b/Packages/Features/Sources/Recording/Protocols/Transcribing.swift index 4050529..a38244e 100644 --- a/Packages/Features/Sources/Recording/Protocols/Transcribing.swift +++ b/Packages/Features/Sources/Recording/Protocols/Transcribing.swift @@ -8,14 +8,14 @@ public protocol Transcribing: Sendable { // MARK: Methods - /// Transcribes the given recorded audio into text, letting the service be called directly as a function. + /// Transcribes the given recorded audio file into text, letting the service be called directly as a function. /// /// - Parameters: - /// - audio: The recorded audio to transcribe. + /// - audio: The location of the recorded audio file to transcribe. /// - locale: The locale of the spoken language to transcribe. /// - Returns: The transcription of the recorded audio. func callAsFunction( - _ audio: Data, + _ audio: URL, locale: Locale ) async throws -> Transcription diff --git a/Packages/Features/Sources/Recording/Services/AudioCapturing.swift b/Packages/Features/Sources/Recording/Services/AudioCapturing.swift index 0db91e5..399d8aa 100644 --- a/Packages/Features/Sources/Recording/Services/AudioCapturing.swift +++ b/Packages/Features/Sources/Recording/Services/AudioCapturing.swift @@ -2,7 +2,7 @@ import AVFoundation /// The recording service that captures real audio from the device's microphone. /// -/// The service records into a temporary `.m4a` file through an `AVAudioRecorder`, and returns the file's contents when the recording stops. +/// The service records into a temporary `.m4a` file through an `AVAudioRecorder`, and returns the file's location when the recording stops. /// The user's permission to record is requested before a recording starts and, on the platforms that require it, the shared audio session is /// configured for recording while the capture is in progress. @MainActor @@ -73,12 +73,11 @@ public final class AudioCapturing: Capturing { } } - /// Stops the recording, deleting the temporary audio file after its contents are read. + /// Stops the recording, handing the temporary audio file over to the caller, which becomes responsible for deleting it. /// - /// - Returns: The audio captured since the recording started. - /// - Throws: ``AudioCapturingError/noOngoingRecording`` when no recording is in progress, or any error thrown - /// while reading the recorded audio file. - public func stop() async throws -> Data { + /// - Returns: The location of the audio file captured since the recording started. + /// - Throws: ``AudioCapturingError/noOngoingRecording`` when no recording is in progress. + public func stop() async throws -> URL { guard let recorder else { throw AudioCapturingError.noOngoingRecording } @@ -91,11 +90,7 @@ public final class AudioCapturing: Capturing { try? AVAudioSession.sharedInstance().setActive(false) #endif - defer { - try? FileManager.default.removeItem(at: Constant.File.url) - } - - return try Data(contentsOf: Constant.File.url) + return Constant.File.url } } diff --git a/Packages/Features/Sources/Recording/Services/AudioTranscribing.swift b/Packages/Features/Sources/Recording/Services/AudioTranscribing.swift index ebe787b..6887d1f 100644 --- a/Packages/Features/Sources/Recording/Services/AudioTranscribing.swift +++ b/Packages/Features/Sources/Recording/Services/AudioTranscribing.swift @@ -3,10 +3,10 @@ import Speech /// The transcribing service that 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 transcription. The transcription happens in the locale given at each call — or rather in the -/// closest equivalent the transcriber supports. The speech model assets for that locale are downloaded and installed on first use; every -/// transcription after that happens entirely offline. +/// The service runs the given recorded audio file through a `SpeechAnalyzer` with a `SpeechTranscriber` module, joining the finalized +/// results into the returned transcription. The transcription happens in the locale given at each call — or rather in the closest equivalent +/// the transcriber supports. The speech model assets for that locale are downloaded and installed on first use; every transcription after +/// that happens entirely offline. public struct AudioTranscribing: Transcribing { // MARK: Initializers @@ -16,22 +16,20 @@ public struct AudioTranscribing: Transcribing { // MARK: Methods - /// Transcribes the given recorded audio into text, deleting the temporary audio file when the transcription finishes. + /// Transcribes the given recorded audio file into text, deleting the file when the transcription finishes. /// /// - Parameters: - /// - audio: The recorded audio to transcribe. + /// - audio: The location of the recorded audio file to transcribe. /// - locale: The locale of the spoken language to transcribe. /// - Returns: The transcription of the recorded audio. /// - Throws: ``AudioTranscribingError/localeNotSupported`` when the transcriber supports no equivalent of the given locale, /// or any error thrown while installing the speech model assets, reading the audio file, or analyzing its contents. public func callAsFunction( - _ audio: Data, + _ audio: URL, locale: Locale ) async throws -> Transcription { - try audio.write(to: Constant.File.url) - defer { - try? FileManager.default.removeItem(at: Constant.File.url) + try? FileManager.default.removeItem(at: audio) } guard let locale = await SpeechTranscriber.supportedLocale( @@ -55,7 +53,7 @@ public struct AudioTranscribing: Transcribing { text += String(result.text.characters) } - let file = try AVAudioFile(forReading: Constant.File.url) + let file = try AVAudioFile(forReading: audio) if let lastSampleTime = try await analyzer.analyzeSequence(from: file) { try await analyzer.finalizeAndFinish(through: lastSampleTime) @@ -75,14 +73,3 @@ public enum AudioTranscribingError: Error { /// The transcriber supports no equivalent of the locale the transcription was requested with. case localeNotSupported } - -// MARK: - Constants - -/// The constant values used across the audio transcribing service. -private 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/Packages/Features/Sources/Recording/Services/DummyCapturing.swift b/Packages/Features/Sources/Recording/Services/DummyCapturing.swift index 74b0e1c..c9d1c5c 100644 --- a/Packages/Features/Sources/Recording/Services/DummyCapturing.swift +++ b/Packages/Features/Sources/Recording/Services/DummyCapturing.swift @@ -19,9 +19,20 @@ struct DummyCapturing: Capturing { /// Simulates the stop of a recording. /// - /// - Returns: An empty audio payload. - func stop() async throws -> Data { - .init() + /// - Returns: The location of a dummy audio file, which is never actually created. + func stop() async throws -> URL { + FileManager.default.temporaryDirectory.appending(path: Constant.File.name) } } + +// MARK: - Constants + +/// The constant values used across the recording services. +private enum Constant { + /// The file constants. + enum File { + /// The name of the dummy audio file. + static let name = "dummy.m4a" + } +} diff --git a/Packages/Features/Sources/Recording/Services/DummyTranscribing.swift b/Packages/Features/Sources/Recording/Services/DummyTranscribing.swift index 1d6513b..a3d800d 100644 --- a/Packages/Features/Sources/Recording/Services/DummyTranscribing.swift +++ b/Packages/Features/Sources/Recording/Services/DummyTranscribing.swift @@ -11,11 +11,11 @@ struct DummyTranscribing: Transcribing { /// Simulates the transcription of the given recorded audio with a two-second delay. /// /// - Parameters: - /// - audio: The recorded audio to transcribe. + /// - audio: The location of the recorded audio file to transcribe. /// - locale: The locale of the spoken language to transcribe. /// - Returns: A dummy transcription. func callAsFunction( - _ audio: Data, + _ audio: URL, locale: Locale ) async throws -> Transcription { try await Task.sleep(for: Constant.Delay.transcribing) diff --git a/Packages/Features/Tests/Recording/Mocks/CapturingMock.swift b/Packages/Features/Tests/Recording/Mocks/CapturingMock.swift new file mode 100644 index 0000000..feec7d7 --- /dev/null +++ b/Packages/Features/Tests/Recording/Mocks/CapturingMock.swift @@ -0,0 +1,48 @@ +import Foundation +import Recording + +/// A recording service that records its invocations in call order, each one taking a configurable amount of time, and can be +/// configured to fail. +@MainActor +final class CapturingMock: Capturing { + + /// The duration every method takes before returning, simulating slow capture work. + var delay: Duration = .zero + + /// The error the service throws from every method, or `nil` when it should succeed. + var error: Error? + + /// The names of the methods called on the service, in call order. + private(set) var calls: [String] = [] + + func start() async throws { + try await called("start") + } + + func pause() async throws { + try await called("pause") + } + + func resume() async throws { + try await called("resume") + } + + func stop() async throws -> URL { + try await called("stop") + + return FileManager.default.temporaryDirectory.appending(path: "mock.m4a") + } + + private func called( + _ name: String + ) async throws { + calls.append(name) + + try await Task.sleep(for: delay) + + if let error { + throw error + } + } + +} diff --git a/Packages/Features/Tests/Recording/Mocks/ErrorMock.swift b/Packages/Features/Tests/Recording/Mocks/ErrorMock.swift new file mode 100644 index 0000000..60123b4 --- /dev/null +++ b/Packages/Features/Tests/Recording/Mocks/ErrorMock.swift @@ -0,0 +1,2 @@ +/// An error to configure the service mocks with. +struct ErrorMock: Error {} diff --git a/Packages/Features/Tests/Recording/Mocks/TranscribingMock.swift b/Packages/Features/Tests/Recording/Mocks/TranscribingMock.swift new file mode 100644 index 0000000..9aedbc4 --- /dev/null +++ b/Packages/Features/Tests/Recording/Mocks/TranscribingMock.swift @@ -0,0 +1,30 @@ +import Foundation +import Recording + +/// A transcribing service with a configurable delay, output, and failure. +@MainActor +final class TranscribingMock: Transcribing { + + /// The duration of the simulated transcription work. + var delay: Duration = .seconds(0.2) + + /// The error the service throws, or `nil` when it should succeed. + var error: Error? + + /// The transcription the service returns. + var transcription = "This is a mocked transcription." + + func callAsFunction( + _ audio: URL, + locale: Locale + ) async throws -> Transcription { + try await Task.sleep(for: delay) + + if let error { + throw error + } + + return .init(text: transcription) + } + +} diff --git a/Packages/Features/Tests/Recording/View Models/RecordingViewModelTests.swift b/Packages/Features/Tests/Recording/View Models/RecordingViewModelTests.swift index 2c83458..53cfb97 100644 --- a/Packages/Features/Tests/Recording/View Models/RecordingViewModelTests.swift +++ b/Packages/Features/Tests/Recording/View Models/RecordingViewModelTests.swift @@ -436,79 +436,3 @@ private extension RecordingView.Model { } } - -// MARK: - Mocks - -/// A recording service that records its invocations in call order, each one taking a configurable amount of time, and can be -/// configured to fail. -@MainActor -private final class CapturingMock: Capturing { - - /// The duration every method takes before returning, simulating slow capture work. - var delay: Duration = .zero - - /// The error the service throws from every method, or `nil` when it should succeed. - var error: Error? - - /// The names of the methods called on the service, in call order. - private(set) var calls: [String] = [] - - func start() async throws { - try await called("start") - } - - func pause() async throws { - try await called("pause") - } - - func resume() async throws { - try await called("resume") - } - - func stop() async throws -> Data { - try await called("stop") - - return .init() - } - - private func called( - _ name: String - ) async throws { - calls.append(name) - - try await Task.sleep(for: delay) - - if let error { - throw error - } - } - -} - -/// A transcribing service with a configurable delay, output, and failure. -@MainActor -private final class TranscribingMock: Transcribing { - - /// The duration of the simulated transcription work. - var delay: Duration = .seconds(0.2) - - /// The error the service throws, or `nil` when it should succeed. - var error: Error? - - /// The transcription the service returns. - var transcription = "This is a mocked transcription." - - func callAsFunction(_ audio: Data, locale: Locale) async throws -> Transcription { - try await Task.sleep(for: delay) - - if let error { - throw error - } - - return .init(text: transcription) - } - -} - -/// An error to configure the service mocks with. -private struct ErrorMock: Error {}