Propagated the Transcription type and locale binding through the RecordingView view and its view model in the Recording package target.
This commit is contained in:
@@ -15,6 +15,9 @@ extension ContentView {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The locale of the spoken language to transcribe.
|
||||
var locale: Locale = .current
|
||||
|
||||
/// The transcription currently presented in the modal sheet, or `nil` when none is shown.
|
||||
var transcription: Transcription?
|
||||
|
||||
@@ -28,13 +31,13 @@ extension ContentView {
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// Handles the transcribed text of a processed recording, presenting it in the modal sheet.
|
||||
/// Handles the transcription of a processed recording, presenting it in the modal sheet.
|
||||
///
|
||||
/// - Parameter text: The transcribed text of the processed recording.
|
||||
/// - Parameter transcription: The transcription of the processed recording.
|
||||
func received(
|
||||
_ text: String
|
||||
_ transcription: Transcription
|
||||
) {
|
||||
transcription = .init(text: text)
|
||||
self.transcription = transcription
|
||||
}
|
||||
|
||||
/// Handles the dismissal of the modal sheet.
|
||||
|
||||
@@ -23,9 +23,10 @@ struct ContentView: View {
|
||||
NavigationStack {
|
||||
RecordingView(
|
||||
recorder: model.recorder,
|
||||
transcriber: model.transcriber
|
||||
) { text in
|
||||
model.received(text)
|
||||
transcriber: model.transcriber,
|
||||
locale: $model.locale,
|
||||
) { transcription in
|
||||
model.received(transcription)
|
||||
}
|
||||
.navigationTitle("view.recording.navigation.title")
|
||||
#if !os(macOS)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
/// A transcription of a processed recording.
|
||||
public struct Transcription: Identifiable, Sendable {
|
||||
public struct Transcription: Equatable, Identifiable, Sendable {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
|
||||
@@ -21,7 +21,11 @@ extension RecordingView {
|
||||
private(set) var elapsedSeconds: Int = 0
|
||||
|
||||
/// The transcription of the last processed recording, or `nil` when none has been processed yet.
|
||||
private(set) var textTranscription: String?
|
||||
private(set) var transcription: Transcription?
|
||||
|
||||
/// The binding to the locale of the spoken language to transcribe.
|
||||
@ObservationIgnored
|
||||
private let locale: Binding<Locale>
|
||||
|
||||
/// The service that captures the audio from a microphone.
|
||||
@ObservationIgnored
|
||||
@@ -42,12 +46,15 @@ extension RecordingView {
|
||||
/// - Parameters:
|
||||
/// - recorder: The service that captures the audio from a microphone. Defaults to ``DummyRecordingService``.
|
||||
/// - transcribe: The callable service that transcribes the recorded audio into text. Defaults to ``DummyTranscribing``.
|
||||
/// - locale: The binding to the locale of the spoken language to transcribe. Defaults to a constant binding to the user's current locale.
|
||||
init(
|
||||
recorder: any RecordingService = DummyRecordingService(),
|
||||
transcribe: any Transcribing = DummyTranscribing()
|
||||
transcribe: any Transcribing = DummyTranscribing(),
|
||||
locale: Binding<Locale> = .constant(.current)
|
||||
) {
|
||||
self.recorder = recorder
|
||||
self.transcribe = transcribe
|
||||
self.locale = locale
|
||||
}
|
||||
|
||||
// MARK: Computed
|
||||
@@ -175,7 +182,7 @@ private extension RecordingView.Model {
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// Processes the recorded input: it stops the audio capture, transcribes the captured audio, and publishes the result in ``textTranscription``;
|
||||
/// Processes the recorded input: it stops the audio capture, transcribes the captured audio, and publishes the result in ``transcription``;
|
||||
/// when finished — or when either service fails — it resets ``elapsedSeconds`` and returns the model to the not-recording state.
|
||||
func processInput() async {
|
||||
guard state == .processing else {
|
||||
@@ -185,12 +192,12 @@ private extension RecordingView.Model {
|
||||
do {
|
||||
let audio = try await recorder.stop()
|
||||
|
||||
textTranscription = try await transcribe(
|
||||
transcription = try await transcribe(
|
||||
audio,
|
||||
locale: .current
|
||||
).text
|
||||
locale: locale.wrappedValue
|
||||
)
|
||||
} catch {
|
||||
textTranscription = nil
|
||||
transcription = nil
|
||||
}
|
||||
|
||||
elapsedSeconds = 0
|
||||
@@ -218,14 +225,14 @@ private extension RecordingView.Model {
|
||||
|
||||
/// Starts the timer task, which increments ``elapsedSeconds`` once per second until it is cancelled. Any previously running timer task is cancelled first.
|
||||
///
|
||||
/// - Parameter shouldRestartTimer: Whether ``elapsedSeconds`` and ``textTranscription`` should be cleared before the timer starts,
|
||||
/// - Parameter shouldRestartTimer: Whether ``elapsedSeconds`` and ``transcription`` should be cleared before the timer starts,
|
||||
/// which is the case for a new recording as opposed to one resuming from a pause.
|
||||
func startTimer(
|
||||
_ shouldRestartTimer: Bool
|
||||
) {
|
||||
if shouldRestartTimer {
|
||||
elapsedSeconds = 0
|
||||
textTranscription = nil
|
||||
transcription = nil
|
||||
}
|
||||
|
||||
taskTimer?.cancel()
|
||||
|
||||
@@ -13,8 +13,8 @@ public struct RecordingView: View {
|
||||
/// The model that owns the recording state and drives the view's controls.
|
||||
@State private var model: Model
|
||||
|
||||
/// The closure invoked with the transcribed text of every processed recording.
|
||||
private let onTranscription: (String) -> Void
|
||||
/// The closure invoked with the transcription of every processed recording.
|
||||
private let onTranscription: (Transcription) -> Void
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
@@ -23,15 +23,18 @@ public struct RecordingView: View {
|
||||
/// - Parameters:
|
||||
/// - recorder: The service that captures the audio from a microphone.
|
||||
/// - transcriber: The service that transcribes the recorded audio into text.
|
||||
/// - onTranscription: The closure invoked with the transcribed text of every processed recording. Defaults to a closure that does nothing.
|
||||
/// - locale: The binding to the locale of the spoken language to transcribe.
|
||||
/// - onTranscription: The closure invoked with the transcription of every processed recording. Defaults to a closure that does nothing.
|
||||
public init(
|
||||
recorder: any RecordingService,
|
||||
transcriber: any Transcribing,
|
||||
onTranscription: @escaping (String) -> Void = { _ in }
|
||||
locale: Binding<Locale>,
|
||||
onTranscription: @escaping (Transcription) -> Void
|
||||
) {
|
||||
self.model = .init(
|
||||
recorder: recorder,
|
||||
transcribe: transcriber
|
||||
transcribe: transcriber,
|
||||
locale: locale
|
||||
)
|
||||
self.onTranscription = onTranscription
|
||||
}
|
||||
@@ -101,7 +104,7 @@ public struct RecordingView: View {
|
||||
)
|
||||
}
|
||||
.onChange(
|
||||
of: model.textTranscription,
|
||||
of: model.transcription,
|
||||
initial: false
|
||||
) { _, newValue in
|
||||
if let newValue {
|
||||
@@ -130,6 +133,9 @@ private enum Constant {
|
||||
) {
|
||||
RecordingView(
|
||||
recorder: DummyRecordingService(),
|
||||
transcriber: DummyTranscribing()
|
||||
)
|
||||
transcriber: DummyTranscribing(),
|
||||
locale: .constant(.current)
|
||||
) { _ in
|
||||
// On transcription closure.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ struct RecordingViewModelTests {
|
||||
#expect(model.state == .notRecording)
|
||||
#expect(model.elapsedSeconds == 0)
|
||||
#expect(model.textTimer == "00:00")
|
||||
#expect(model.textTranscription == nil)
|
||||
#expect(model.transcription == nil)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -368,7 +368,7 @@ struct RecordingViewModelTests {
|
||||
|
||||
#expect(model.state == .notRecording)
|
||||
#expect(model.elapsedSeconds == 0)
|
||||
#expect(model.textTranscription != nil)
|
||||
#expect(model.transcription != nil)
|
||||
}
|
||||
|
||||
@Test func `clears the transcription when the transcriber fails`() async throws {
|
||||
@@ -387,7 +387,7 @@ struct RecordingViewModelTests {
|
||||
|
||||
#expect(model.state == .notRecording)
|
||||
#expect(model.elapsedSeconds == 0)
|
||||
#expect(model.textTranscription == nil)
|
||||
#expect(model.transcription == nil)
|
||||
}
|
||||
|
||||
@Test func `clears the transcription when a new recording starts`() async throws {
|
||||
@@ -400,12 +400,12 @@ struct RecordingViewModelTests {
|
||||
|
||||
try await Task.sleep(for: .seconds(0.5))
|
||||
|
||||
#expect(model.textTranscription != nil)
|
||||
#expect(model.transcription != nil)
|
||||
|
||||
model.state = .recording
|
||||
model.updatedState(shouldRestartTimer: true)
|
||||
|
||||
#expect(model.textTranscription == nil)
|
||||
#expect(model.transcription == nil)
|
||||
|
||||
model.state = .notRecording
|
||||
model.updatedState(shouldRestartTimer: false)
|
||||
|
||||
Reference in New Issue
Block a user