From f629d66e23fc09c9068b0fbb840a1978f81d13e8 Mon Sep 17 00:00:00 2001 From: Philipp Zagar Date: Thu, 16 Nov 2023 16:22:37 -0800 Subject: [PATCH] Initial setup of repository (#2) # Initial setup of repository ## :recycle: Current situation & Problem Currently, the repo is empty except for template code. ## :gear: Release Notes - Initial setup of the `SpeziSpeech` repo which previously was a target in the [SpeziML](https://github.com/StanfordSpezi/SpeziML) repo - Upgrade to Spezi v0.8 - Improved README and documentation via DocC ## :books: Documentation -- ## :white_check_mark: Testing -- ## :pencil: Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md): - [X] I agree to follow the [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md). --- .github/workflows/build-and-test.yml | 18 +- .spi.yml | 3 +- CITATION.cff | 6 + CONTRIBUTORS.md | 3 + Package.swift | 17 +- README.md | 96 ++++++-- .../Resources/en.lproj/Localizable.strings | 9 - Sources/SpeziSpeech/Speech.swift | 42 ---- .../SpeziSpeech.docc/SpeziSpeech.md | 63 ------ .../SpeechRecognizer.swift | 209 ++++++++++++++++++ .../SpeziSpeechRecognizer.md | 104 +++++++++ .../SpeechSynthesizer.swift | 151 +++++++++++++ .../SpeziSpeechSynthesizer.md | 80 +++++++ Tests/SpeziSpeechTests/SpeziSpeechTests.swift | 5 +- Tests/UITests/TestApp.xctestplan | 9 +- Tests/UITests/TestApp/SpeechTestView.swift | 121 ++++++++++ Tests/UITests/TestApp/TestApp.swift | 4 +- Tests/UITests/TestApp/TestAppDelegate.swift | 7 +- ...SpeechTests.swift => TestAppUITests.swift} | 13 +- .../UITests/UITests.xcodeproj/project.pbxproj | 100 ++++----- .../xcshareddata/xcschemes/TestApp.xcscheme | 22 +- 21 files changed, 864 insertions(+), 218 deletions(-) delete mode 100644 Sources/SpeziSpeech/Resources/en.lproj/Localizable.strings delete mode 100644 Sources/SpeziSpeech/Speech.swift delete mode 100644 Sources/SpeziSpeech/SpeziSpeech.docc/SpeziSpeech.md create mode 100644 Sources/SpeziSpeechRecognizer/SpeechRecognizer.swift create mode 100644 Sources/SpeziSpeechRecognizer/SpeziSpeechRecognizer.docc/SpeziSpeechRecognizer.md create mode 100644 Sources/SpeziSpeechSynthesizer/SpeechSynthesizer.swift create mode 100644 Sources/SpeziSpeechSynthesizer/SpeziSpeechSynthesizer.docc/SpeziSpeechSynthesizer.md create mode 100644 Tests/UITests/TestApp/SpeechTestView.swift rename Tests/UITests/TestAppUITests/{SpeziSpeechTests.swift => TestAppUITests.swift} (57%) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 5e584d0..6356770 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -21,8 +21,8 @@ jobs: uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 with: runsonlabels: '["macOS", "self-hosted"]' - scheme: SpeziSpeech - artifactname: SpeziSpeech.xcresult + scheme: SpeziSpeech-Package + artifactname: SpeziSpeech-Package.xcresult ios: name: Build and Test iOS uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 @@ -31,9 +31,19 @@ jobs: path: 'Tests/UITests' scheme: TestApp artifactname: TestApp.xcresult + ipados: + name: Build and Test iPadOS + uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + with: + runsonlabels: '["macOS", "self-hosted"]' + path: 'Tests/UITests' + scheme: TestApp + resultBundle: TestAppiPadOS.xcresult + destination: 'platform=iOS Simulator,name=iPad mini (6th generation)' + artifactname: TestAppiPadOS.xcresult uploadcoveragereport: name: Upload Coverage Report - needs: [packageios, ios] + needs: [packageios, ios, ipados] uses: StanfordSpezi/.github/.github/workflows/create-and-upload-coverage-report.yml@v2 with: - coveragereports: SpeziSpeech.xcresult TestApp.xcresult + coveragereports: SpeziSpeech-Package.xcresult TestApp.xcresult TestAppiPadOS.xcresult diff --git a/.spi.yml b/.spi.yml index 3f7d9b7..bb9095a 100644 --- a/.spi.yml +++ b/.spi.yml @@ -11,4 +11,5 @@ builder: configs: - platform: ios documentation_targets: - - SpeziSpeech + - SpeziSpeechRecognizer + - SpeziSpeechSynthesizer diff --git a/CITATION.cff b/CITATION.cff index b053203..dc8b394 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -12,5 +12,11 @@ authors: - family-names: "Schmiedmayer" given-names: "Paul" orcid: "https://orcid.org/0000-0002-8607-9148" +- family-names: "Philipp" + given-names: "Zagar" + orcid: "https://orcid.org/0009-0001-5934-2078" +- family-names: "Adrit" + given-names: "Rao" + orcid: "https://orcid.org/0000-0002-0780-033X" title: "SpeziSpeech" url: "https://github.com/StanfordSpezi/SpeziSpeech" diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 59c459a..dbd5e3a 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -14,3 +14,6 @@ SpeziSpeech contributors ==================== * [Paul Schmiedmayer](https://github.com/PSchmiedmayer) +* [Vishnu Ravi](https://github.com/vishnuravi) +* [Philipp Zagar](https://github.com/philippzagar) +* [Adrit Rao](https://github.com/AdritRao) diff --git a/Package.swift b/Package.swift index 4fc7190..0cc2fbe 100644 --- a/Package.swift +++ b/Package.swift @@ -18,25 +18,30 @@ let package = Package( .iOS(.v17) ], products: [ - .library(name: "SpeziSpeech", targets: ["SpeziSpeech"]) + .library(name: "SpeziSpeechRecognizer", targets: ["SpeziSpeechRecognizer"]), + .library(name: "SpeziSpeechSynthesizer", targets: ["SpeziSpeechSynthesizer"]) ], dependencies: [ .package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.8.0")) ], targets: [ .target( - name: "SpeziSpeech", + name: "SpeziSpeechRecognizer", + dependencies: [ + .product(name: "Spezi", package: "Spezi") + ] + ), + .target( + name: "SpeziSpeechSynthesizer", dependencies: [ .product(name: "Spezi", package: "Spezi") - ], - resources: [ - .process("Resources") ] ), .testTarget( name: "SpeziSpeechTests", dependencies: [ - .target(name: "SpeziSpeech") + .target(name: "SpeziSpeechRecognizer"), + .target(name: "SpeziSpeechSynthesizer") ] ) ] diff --git a/README.md b/README.md index a79cde6..be000dd 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,12 @@ SPDX-License-Identifier: MIT [![codecov](https://codecov.io/gh/StanfordSpezi/SpeziSpeech/graph/badge.svg?token=ufmRQvE0Cs)](https://codecov.io/gh/StanfordSpezi/SpeziSpeech) -Enable applications to connect to Speech devices. +Recognize and synthesize natural language speech. ## Overview -... +The Spezi Speech component provides an easy and convenient way to recognize (speech-to-text) and synthesize (text-to-speech) natural language content, facilitating seamless interaction with an application. It builds on top of Apple's [Speech](https://developer.apple.com/documentation/speech/) and [AVFoundation](https://developer.apple.com/documentation/avfoundation/) frameworks. ## Setup @@ -32,19 +32,18 @@ You need to add the Spezi Speech Swift package to [Swift package](https://developer.apple.com/documentation/xcode/creating-a-standalone-swift-package-with-xcode#Add-a-dependency-on-another-Swift-package). > [!IMPORTANT] -> If your application is not yet configured to use Spezi, follow the [Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) setup the core Spezi infrastructure. +> If your application is not yet configured to use Spezi, follow the [Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) to setup the core Spezi infrastructure. +### 2. Configure the `SpeechRecognizer` and the `SpeechSynthesizer` in the Spezi `Configuration` -### 2. Register the Module - -The `Speech` module needs to be registered in a Spezi-based application using the -[`configuration`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate/configuration) in a -[`SpeziAppDelegate`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate): +The [`SpeechRecognizer`](https://swiftpackageindex.com/stanfordspezi/spezispeech/documentation/spezispeech/speechrecognizer) and [`SpeechSynthesizer`](https://swiftpackageindex.com/stanfordspezi/spezispeech/documentation/spezispeech/speechsynthesizer) modules need to be registered in a Spezi-based application using the [`configuration`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate/configuration) +in a [`SpeziAppDelegate`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate): ```swift class ExampleAppDelegate: SpeziAppDelegate { override var configuration: Configuration { Configuration { - Speech() + SpeechRecognizer() + SpeechSynthesizer() // ... } } @@ -54,16 +53,82 @@ class ExampleAppDelegate: SpeziAppDelegate { > [!NOTE] > You can learn more about a [`Module` in the Spezi documentation](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module). +### 3. Configure target properties + +To ensure that your application has the necessary permissions for microphone access and speech recognition, follow the steps below to configure the target properties within your Xcode project: + +- Open your project settings in Xcode by selecting *PROJECT_NAME > TARGET_NAME > Info* tab. +- You will need to add two entries to the `Custom iOS Target Properties` (so the `Info.plist` file) to provide descriptions for why your app requires these permissions: + - Add a key named `Privacy - Microphone Usage Description` and provide a string value that describes why your application needs access to the microphone. This description will be displayed to the user when the app first requests microphone access. + - Add another key named `Privacy - Speech Recognition Usage Description` with a string value that explains why your app requires the speech recognition capability. This will be presented to the user when the app first attempts to perform speech recognition. + +These entries are mandatory for apps that utilize microphone and speech recognition features. Failing to provide them will result in your app being unable to access these features. ## Example -... +`SpeechTestView` provides a demonstration of the capabilities of the Spezi Speech module. +It showcases the interaction with the [`SpeechRecognizer`](https://swiftpackageindex.com/stanfordspezi/spezispeech/documentation/spezispeech/speechrecognizer) to provide speech-to-text capabilities and the [`SpeechSynthesizer`](https://swiftpackageindex.com/stanfordspezi/spezispeech/documentation/spezispeech/speechsynthesizer) to generate speech from text. + ```swift -... +struct SpeechTestView: View { + // Get the `SpeechRecognizer` and `SpeechSynthesizer` from the SwiftUI `Environment`. + @Environment(SpeechRecognizer.self) private var speechRecognizer + @Environment(SpeechSynthesizer.self) private var speechSynthesizer + // The transcribed message from the user's voice input. + @State private var message = "" + + + var body: some View { + VStack { + // Button used to start and stop recording by triggering the `microphoneButtonPressed()` function. + Button("Record") { + microphoneButtonPressed() + } + .padding(.bottom) + + // Button used to start and stop playback of the transcribed message by triggering the `playbackButtonPressed()` function. + Button("Playback") { + playbackButtonPressed() + } + .padding(.bottom) + + Text(message) + } + } + + + private func microphoneButtonPressed() { + if speechRecognizer.isRecording { + // If speech is currently recognized, stop the transcribing. + speechRecognizer.stop() + } else { + // If the recognizer is idle, start a new recording. + Task { + do { + // The `speechRecognizer.start()` function returns an `AsyncThrowingStream` that yields the transcribed text. + for try await result in speechRecognizer.start() { + // Access the string-based result of the transcribed result. + message = result.bestTranscription.formattedString + } + } + } + } + } + + private func playbackButtonPressed() { + if speechSynthesizer.isSpeaking { + // If speech is currently synthezized, pause the playback. + speechSynthesizer.pause() + } else { + // If synthesizer is idle, start with the text-to-speech functionality. + speechSynthesizer.speak(message) + } + } +} ``` -For more information, please refer to the API documentation. +For more information, please refer to the [API documentation](https://swiftpackageindex.com/StanfordSpezi/SpeziSpeech/documentation). ## Contributing @@ -73,7 +138,8 @@ Contributions to this project are welcome. Please make sure to read the [contrib ## License -This project is licensed under the MIT License. See [Licenses](https://github.com/StanfordSpezi/SpeziContact/tree/main/LICENSES) for more information. -![Spezi Footer](https://raw.githubusercontent.com/StanfordSpezi/.github/main/assets/FooterLight.png#gh-light-mode-only) -![Spezi Footer](https://raw.githubusercontent.com/StanfordSpezi/.github/main/assets/FooterDark.png#gh-dark-mode-only) +This project is licensed under the MIT License. See [Licenses](https://github.com/StanfordSpezi/SpeziSpeech/tree/main/LICENSES) for more information. + +![Spezi Footer](https://raw.githubusercontent.com/StanfordSpezi/.github/main/assets/Footer.png#gh-light-mode-only) +![Spezi Footer](https://raw.githubusercontent.com/StanfordSpezi/.github/main/assets/Footer~dark.png#gh-dark-mode-only) diff --git a/Sources/SpeziSpeech/Resources/en.lproj/Localizable.strings b/Sources/SpeziSpeech/Resources/en.lproj/Localizable.strings deleted file mode 100644 index e7d81b9..0000000 --- a/Sources/SpeziSpeech/Resources/en.lproj/Localizable.strings +++ /dev/null @@ -1,9 +0,0 @@ -// -// This source file is part of the Stanford Spezi open-source project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -"..." = "..."; \ No newline at end of file diff --git a/Sources/SpeziSpeech/Speech.swift b/Sources/SpeziSpeech/Speech.swift deleted file mode 100644 index 8671323..0000000 --- a/Sources/SpeziSpeech/Speech.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// This source file is part of the Stanford Spezi open-source project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import Observation -import Spezi - - -/// Spezi module to support speech-related features including text-to-speech and speech-to-text. -/// -/// > Important: If your application is not yet configured to use Spezi, follow the [Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) setup the core Spezi infrastructure. -/// -/// The module needs to be registered in a Spezi-based application using the [`configuration`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate/configuration) -/// in a [`SpeziAppDelegate`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate): -/// ```swift -/// class ExampleAppDelegate: SpeziAppDelegate { -/// override var configuration: Configuration { -/// Configuration { -/// Speech() -/// // ... -/// } -/// } -/// } -/// ``` -/// > Tip: You can learn more about a [`Module` in the Spezi documentation](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module). -/// -/// -/// ## Usage -/// -/// ... -/// ```swift -/// ... -/// ``` -@Observable -public class Speech: Module, DefaultInitializable { - /// Creates an instance of a ``Speech`` module. - public required init() { } -} diff --git a/Sources/SpeziSpeech/SpeziSpeech.docc/SpeziSpeech.md b/Sources/SpeziSpeech/SpeziSpeech.docc/SpeziSpeech.md deleted file mode 100644 index f7ab4b0..0000000 --- a/Sources/SpeziSpeech/SpeziSpeech.docc/SpeziSpeech.md +++ /dev/null @@ -1,63 +0,0 @@ -# ``SpeziSpeech`` - - - -Enable applications to connect to Speech devices. - - -## Overview - -... - - -## Setup - - -### 1. Add Spezi Speech as a Dependency - -You need to add the Spezi Speech Swift package to -[your app in Xcode](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app#) or -[Swift package](https://developer.apple.com/documentation/xcode/creating-a-standalone-swift-package-with-xcode#Add-a-dependency-on-another-Swift-package). - -> Important: If your application is not yet configured to use Spezi, follow the [Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) setup the core Spezi infrastructure. - - -### 2. Register the Module - -The `Speech` module needs to be registered in a Spezi-based application using the -[`configuration`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate/configuration) in a -[`SpeziAppDelegate`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate): -```swift -class ExampleAppDelegate: SpeziAppDelegate { - override var configuration: Configuration { - Configuration { - Speech() - // ... - } - } -} -``` - -> Tip: You can learn more about a [`Module` in the Spezi documentation](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module). - - -## Example - -... - -```swift -... -``` - - -## Topics - -- ``Speech`` diff --git a/Sources/SpeziSpeechRecognizer/SpeechRecognizer.swift b/Sources/SpeziSpeechRecognizer/SpeechRecognizer.swift new file mode 100644 index 0000000..4c7970a --- /dev/null +++ b/Sources/SpeziSpeechRecognizer/SpeechRecognizer.swift @@ -0,0 +1,209 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Observation +import os +import Speech +import Spezi + +/// The Spezi ``SpeechRecognizer`` encapsulates the functionality of Apple's `Speech` framework, more specifically the `SFSpeechRecognizer`. +/// It provides methods to start and stop voice recognition, and publishes the state of recognition and its availability. +/// +/// > Important: If your application is not yet configured to use Spezi, follow the [Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) setup the core Spezi infrastructure. +/// +/// The module needs to be registered in a Spezi-based application using the [`configuration`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate/configuration) +/// in a [`SpeziAppDelegate`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate): +/// ```swift +/// class ExampleAppDelegate: SpeziAppDelegate { +/// override var configuration: Configuration { +/// Configuration { +/// SpeechRecognizer() +/// // ... +/// } +/// } +/// } +/// ``` +/// > Tip: You can learn more about a [`Module` in the Spezi documentation](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module). +/// +/// ## Usage +/// +/// ```swift +/// struct SpeechRecognizerView: View { +/// // Get the `SpeechRecognizer` from the SwiftUI `Environment`. +/// @Environment(SpeechRecognizer.self) private var speechRecognizer +/// // The transcribed message from the user's voice input. +/// @State private var message = "" +/// +/// var body: some View { +/// VStack { +/// Button("Record") { +/// microphoneButtonPressed() +/// } +/// .padding(.bottom) +/// +/// Text(message) +/// } +/// +/// } +/// +/// private func microphoneButtonPressed() { +/// if speechRecognizer.isRecording { +/// // If speech is currently recognized, stop the transcribing. +/// speechRecognizer.stop() +/// } else { +/// // If the recognizer is idle, start a new recording. +/// Task { +/// do { +/// // The `speechRecognizer.start()` function returns an `AsyncThrowingStream` that yields the transcribed text. +/// for try await result in speechRecognizer.start() { +/// // Access the string-based result of the transcribed result +/// message = result.bestTranscription.formattedString +/// } +/// } +/// } +/// } +/// } +/// ``` +@Observable +public class SpeechRecognizer: NSObject, Module, DefaultInitializable, EnvironmentAccessible, SFSpeechRecognizerDelegate { + private static let logger = Logger(subsystem: "edu.stanford.spezi", category: "SpeziSpeech") + private let speechRecognizer: SFSpeechRecognizer? + private let audioEngine: AVAudioEngine? + + /// Indicates whether the speech recognition is currently in progress. + public private(set) var isRecording = false + /// Indicates the availability of the speech recognition service. + public private(set) var isAvailable: Bool + + private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest? + private var recognitionTask: SFSpeechRecognitionTask? + + + /// Initializes a new instance of `SpeechRecognizer`. + override public required convenience init() { + self.init(locale: .current) + } + + /// Initializes a new instance of `SpeechRecognizer`. + /// + /// - Parameter locale: The locale for the speech recognition. Defaults to the current locale. + public init(locale: Locale = .current) { + if let speechRecognizer = SFSpeechRecognizer(locale: locale) { + self.speechRecognizer = speechRecognizer + self.isAvailable = speechRecognizer.isAvailable + } else { + self.speechRecognizer = nil + self.isAvailable = false + } + + self.audioEngine = AVAudioEngine() + + super.init() + + speechRecognizer?.delegate = self + } + + + /// Starts the speech recognition process. + /// + /// - Returns: An asynchronous stream that yields the speech recognition results. + public func start() -> AsyncThrowingStream { // swiftlint:disable:this function_body_length + AsyncThrowingStream { continuation in // swiftlint:disable:this closure_body_length + guard !isRecording else { + SpeechRecognizer.logger.warning( + "You already having a recording session in progress, please cancel the first one using `stop` before starting a new session." + ) + stop() + continuation.finish() + return + } + + guard isAvailable, let audioEngine, let speechRecognizer else { + SpeechRecognizer.logger.error("The SpeechRecognizer is not available.") + stop() + continuation.finish() + return + } + + do { + let audioSession = AVAudioSession.sharedInstance() + try audioSession.setCategory(.record, mode: .measurement, options: .duckOthers) + try audioSession.setActive(true, options: .notifyOthersOnDeactivation) + } catch { + SpeechRecognizer.logger.error("Error setting up the audio session: \(error.localizedDescription)") + stop() + continuation.finish(throwing: error) + } + + let inputNode = audioEngine.inputNode + + let recognitionRequest = SFSpeechAudioBufferRecognitionRequest() + recognitionRequest.shouldReportPartialResults = true + self.recognitionRequest = recognitionRequest + + recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { result, error in + if let error { + continuation.finish(throwing: error) + } + + guard self.isRecording, let result else { + self.stop() + return + } + + continuation.yield(result) + } + + let recordingFormat = inputNode.outputFormat(forBus: 0) + inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { buffer, _ in + self.recognitionRequest?.append(buffer) + } + + audioEngine.prepare() + do { + isRecording = true + try audioEngine.start() + } catch { + SpeechRecognizer.logger.error("Error setting up the audio session: \(error.localizedDescription)") + stop() + continuation.finish(throwing: error) + } + + continuation.onTermination = { @Sendable _ in + self.stop() + } + } + } + + /// Stops the current speech recognition session. + public func stop() { + guard isAvailable && isRecording else { + return + } + + audioEngine?.stop() + audioEngine?.inputNode.removeTap(onBus: 0) + + recognitionRequest?.endAudio() + recognitionRequest = nil + + recognitionTask?.cancel() + recognitionTask = nil + + isRecording = false + } + + @_documentation(visibility: internal) + public func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) { + guard self.speechRecognizer == speechRecognizer else { + return + } + + self.isAvailable = available + } +} diff --git a/Sources/SpeziSpeechRecognizer/SpeziSpeechRecognizer.docc/SpeziSpeechRecognizer.md b/Sources/SpeziSpeechRecognizer/SpeziSpeechRecognizer.docc/SpeziSpeechRecognizer.md new file mode 100644 index 0000000..9a0a460 --- /dev/null +++ b/Sources/SpeziSpeechRecognizer/SpeziSpeechRecognizer.docc/SpeziSpeechRecognizer.md @@ -0,0 +1,104 @@ +# ``SpeziSpeechRecognizer`` + + + +Provides speech-to-text capabilities via Apple's `Speech` framework. + +## Overview + +The Spezi ``SpeechRecognizer` encapsulates the functionality of Apple's `Speech` framework, more specifically the `SFSpeechRecognizer`. +It provides methods to start and stop voice recognition, and publishes the state of recognition and its availability. + +## Setup + +### 1. Add Spezi Speech as a Dependency + +You need to add the SpeziSpeech Swift package to +[your app in Xcode](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app#) or +[Swift package](https://developer.apple.com/documentation/xcode/creating-a-standalone-swift-package-with-xcode#Add-a-dependency-on-another-Swift-package). + +> Important: If your application is not yet configured to use Spezi, follow the [Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) setup the core Spezi infrastructure. + +### 2. Configure the `SpeechRecognizer` in the Spezi `Configuration` + +The module needs to be registered in a Spezi-based application using the [`configuration`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate/configuration) +in a [`SpeziAppDelegate`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate): +```swift +class ExampleAppDelegate: SpeziAppDelegate { + override var configuration: Configuration { + Configuration { + SpeechRecognizer() + // ... + } + } +} +``` +> Tip: You can learn more about a [`Module` in the Spezi documentation](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module). + +### 3. Configure target properties + +To ensure that your application has the necessary permissions for microphone access and speech recognition, follow the steps below to configure the target properties within your Xcode project: + +- Open your project settings in Xcode by selecting *PROJECT_NAME > TARGET_NAME > Info* tab. +- You will need to add two entries to the `Custom iOS Target Properties` (so the `Info.plist` file) to provide descriptions for why your app requires these permissions: + - Add a key named `Privacy - Microphone Usage Description` and provide a string value that describes why your application needs access to the microphone. This description will be displayed to the user when the app first requests microphone access. + - Add another key named `Privacy - Speech Recognition Usage Description` with a string value that explains why your app requires the speech recognition capability. This will be presented to the user when the app first attempts to perform speech recognition. + +These entries are mandatory for apps that utilize microphone and speech recognition features. Failing to provide them will result in your app being unable to access these features. + +## Example + +The code example demonstrates the usage of the Spezi ``SpeechRecognizer`` within a minimal SwiftUI application. + +```swift +struct SpeechTestView: View { + // Get the `SpeechRecognizer` from the SwiftUI `Environment`. + @Environment(SpeechRecognizer.self) private var speechRecognizer + // The transcribed message from the user's voice input. + @State private var message = "" + + + var body: some View { + VStack { + // Button used to start and stop recording by triggering the `microphoneButtonPressed()` function. + Button("Record") { + microphoneButtonPressed() + } + .padding(.bottom) + + Text(message) + } + } + + + private func microphoneButtonPressed() { + if speechRecognizer.isRecording { + // If speech is currently recognized, stop the transcribing. + speechRecognizer.stop() + } else { + // If the recognizer is idle, start a new recording. + Task { + do { + // The `speechRecognizer.start()` function returns an `AsyncThrowingStream` that yields the transcribed text. + for try await result in speechRecognizer.start() { + // Access the string-based result of the transcribed result. + message = result.bestTranscription.formattedString + } + } + } + } + } +} +``` + +## Topics + +- ``SpeechRecognizer`` diff --git a/Sources/SpeziSpeechSynthesizer/SpeechSynthesizer.swift b/Sources/SpeziSpeechSynthesizer/SpeechSynthesizer.swift new file mode 100644 index 0000000..1b66763 --- /dev/null +++ b/Sources/SpeziSpeechSynthesizer/SpeechSynthesizer.swift @@ -0,0 +1,151 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import AVFoundation +import Observation +import Spezi + +/// The Spezi ``SpeechSynthesizer`` encapsulates the functionality of Apple's `AVFoundation` framework, more specifically the `AVSpeechSynthesizer`. +/// It provides methods to start and stop voice synthesizing, and publishes the state of the synthesization. +/// +/// > Important: If your application is not yet configured to use Spezi, follow the [Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) setup the core Spezi infrastructure. +/// +/// The module needs to be registered in a Spezi-based application using the [`configuration`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate/configuration) +/// in a [`SpeziAppDelegate`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate): +/// ```swift +/// class ExampleAppDelegate: SpeziAppDelegate { +/// override var configuration: Configuration { +/// Configuration { +/// SpeechSynthesizer() +/// // ... +/// } +/// } +/// } +/// ``` +/// > Tip: You can learn more about a [`Module` in the Spezi documentation](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module). +/// +/// ## Usage +/// +/// ```swift +/// struct SpeechSynthesizerView: View { +/// // Get the `SpeechSynthesizer` from the SwiftUI `Environment`. +/// @Environment(SpeechSynthesizer.self) private var speechSynthesizer +/// // A textual message that will be synthesized to natural language speech. +/// private let message = "Hello, this is the SpeziSpeech framework!" +/// +/// var body: some View { +/// Button("Playback") { +/// playbackButtonPressed() +/// } +/// } +/// +/// private func playbackButtonPressed() { +/// if speechSynthesizer.isSpeaking { +/// speechSynthesizer.pause() +/// } else { +/// speechSynthesizer.speak(message) +/// } +/// } +/// } +/// ``` +@Observable +public class SpeechSynthesizer: NSObject, Module, DefaultInitializable, EnvironmentAccessible, AVSpeechSynthesizerDelegate { + /// The wrapped `AVSpeechSynthesizer` instance. + private let avSpeechSynthesizer = AVSpeechSynthesizer() + + + /// A Boolean value that indicates whether the speech synthesizer is speaking or is in a paused state and has utterances to speak. + public private(set) var isSpeaking = false + /// A Boolean value that indicates whether a speech synthesizer is in a paused state. + public private(set) var isPaused = false + + + override public required init() { + super.init() + avSpeechSynthesizer.delegate = self + } + + + /// Adds the text to the speech synthesizer’s queue. + /// - Parameters: + /// - text: A string that contains the text to speak. + /// - language: Optional BCP 47 code that identifies the language and locale for a voice. + public func speak(_ text: String, language: String? = nil) { + let utterance = AVSpeechUtterance(string: text) + + if let language { + utterance.voice = AVSpeechSynthesisVoice(language: language) + } + + speak(utterance) + } + + /// Adds the utterance to the speech synthesizer’s queue. + /// - Parameter utterance: An `AVSpeechUtterance` instance that contains text to speak. + public func speak(_ utterance: AVSpeechUtterance) { + avSpeechSynthesizer.speak(utterance) + } + + /// Pauses the current output speech from the speech synthesizer. + /// - Parameters: + /// - pauseMethod: Defines when the output should be stopped via the `AVSpeechBoundary`. + public func pause(at pauseMethod: AVSpeechBoundary = .immediate) { + if isSpeaking { + avSpeechSynthesizer.pauseSpeaking(at: pauseMethod) + } + } + + /// Resumes the output of the speech synthesizer. + public func continueSpeaking() { + if isPaused { + avSpeechSynthesizer.continueSpeaking() + } + } + + /// Stops the output by the speech synthesizer and cancels all unspoken utterances from the synthesizer’s queue. + /// It is not possible to resume a stopped utterance. + /// - Parameters: + /// - stopMethod: Defines when the output should be stopped via the `AVSpeechBoundary`. + public func stop(at stopMethod: AVSpeechBoundary = .immediate) { + if isSpeaking || isPaused { + avSpeechSynthesizer.stopSpeaking(at: stopMethod) + } + } + + + // MARK: - AVSpeechSynthesizerDelegate + @_documentation(visibility: internal) + public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) { + isSpeaking = true + isPaused = false + } + + @_documentation(visibility: internal) + public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance) { + isSpeaking = false + isPaused = true + } + + @_documentation(visibility: internal) + public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didContinue utterance: AVSpeechUtterance) { + isSpeaking = true + isPaused = false + } + + @_documentation(visibility: internal) + public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) { + isSpeaking = false + isPaused = false + } + + @_documentation(visibility: internal) + public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) { + isSpeaking = false + isPaused = false + } +} diff --git a/Sources/SpeziSpeechSynthesizer/SpeziSpeechSynthesizer.docc/SpeziSpeechSynthesizer.md b/Sources/SpeziSpeechSynthesizer/SpeziSpeechSynthesizer.docc/SpeziSpeechSynthesizer.md new file mode 100644 index 0000000..35490fe --- /dev/null +++ b/Sources/SpeziSpeechSynthesizer/SpeziSpeechSynthesizer.docc/SpeziSpeechSynthesizer.md @@ -0,0 +1,80 @@ +# ``SpeziSpeechSynthesizer`` + + + +Provides text-to-speech capabilities via Apple's `AVFoundation` framework. + +## Overview + +The Spezi ``SpeechSynthesizer`` encapsulates the functionality of Apple's `AVFoundation` framework, more specifically the `AVSpeechSynthesizer`. +It provides methods to start and stop voice synthesizing, and publishes the state of the synthesization. + +## Setup + +### 1. Add Spezi Speech as a Dependency + +You need to add the SpeziSpeech Swift package to +[your app in Xcode](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app#) or +[Swift package](https://developer.apple.com/documentation/xcode/creating-a-standalone-swift-package-with-xcode#Add-a-dependency-on-another-Swift-package). + +> Important: If your application is not yet configured to use Spezi, follow the [Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) setup the core Spezi infrastructure. + +### 2. Configure the `SpeechSynthesizer` in the Spezi `Configuration` + +The module needs to be registered in a Spezi-based application using the [`configuration`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate/configuration) +in a [`SpeziAppDelegate`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate): +```swift +class ExampleAppDelegate: SpeziAppDelegate { + override var configuration: Configuration { + Configuration { + SpeechSynthesizer() + // ... + } + } +} +``` +> Tip: You can learn more about a [`Module` in the Spezi documentation](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module). + +## Example + +The code example demonstrates the usage of the Spezi ``SpeechSynthesizer`` within a minimal SwiftUI application. + +```swift +struct SpeechTestView: View { + // Get the `SpeechSynthesizer` from the SwiftUI `Environment`. + @Environment(SpeechSynthesizer.self) private var speechSynthesizer + // A textual message that will be synthesized to natural language speech. + private let message = "Hello, this is the SpeziSpeech framework!" + + + var body: some View { + // Button used to start and stop playback of the transcribed message by triggering the `playbackButtonPressed()` function. + Button("Playback") { + playbackButtonPressed() + } + } + + + private func playbackButtonPressed() { + if speechSynthesizer.isSpeaking { + // If speech is currently synthezized, pause the playback. + speechSynthesizer.pause() + } else { + // If synthesizer is idle, start with the text-to-speech functionality. + speechSynthesizer.speak(message) + } + } +} +``` + +## Topics + +- ``SpeechSynthesizer`` diff --git a/Tests/SpeziSpeechTests/SpeziSpeechTests.swift b/Tests/SpeziSpeechTests/SpeziSpeechTests.swift index 39bb7e4..4fdf48c 100644 --- a/Tests/SpeziSpeechTests/SpeziSpeechTests.swift +++ b/Tests/SpeziSpeechTests/SpeziSpeechTests.swift @@ -6,12 +6,13 @@ // SPDX-License-Identifier: MIT // -@testable import SpeziSpeech +@testable import SpeziSpeechRecognizer +@testable import SpeziSpeechSynthesizer import XCTest final class SpeziSpeechTests: XCTestCase { func testSpeziSpeech() throws { - XCTAssertTrue(true) + XCTAssert(true) } } diff --git a/Tests/UITests/TestApp.xctestplan b/Tests/UITests/TestApp.xctestplan index 8795dde..c0cf6ab 100644 --- a/Tests/UITests/TestApp.xctestplan +++ b/Tests/UITests/TestApp.xctestplan @@ -13,8 +13,13 @@ "targets" : [ { "containerPath" : "container:..\/..", - "identifier" : "SpeziSpeech", - "name" : "SpeziSpeech" + "identifier" : "SpeziSpeechRecognizer", + "name" : "SpeziSpeechRecognizer" + }, + { + "containerPath" : "container:..\/..", + "identifier" : "SpeziSpeechSynthesizer", + "name" : "SpeziSpeechSynthesizer" } ] }, diff --git a/Tests/UITests/TestApp/SpeechTestView.swift b/Tests/UITests/TestApp/SpeechTestView.swift new file mode 100644 index 0000000..7d50ad7 --- /dev/null +++ b/Tests/UITests/TestApp/SpeechTestView.swift @@ -0,0 +1,121 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziSpeechRecognizer +import SpeziSpeechSynthesizer +import SwiftUI + + +struct SpeechTestView: View { + @Environment(SpeechRecognizer.self) private var speechRecognizer + @Environment(SpeechSynthesizer.self) private var speechSynthesizer + @State private var message = "" + + + var body: some View { + VStack { + Text("SpeziSpeech") + + ScrollView { + Text(message) + .padding() + } + .frame( + width: UIScreen.main.bounds.width * 0.8, + height: UIScreen.main.bounds.height * 0.3 + ) + .border(.gray) + .padding(.horizontal) + .padding(.bottom) + + if speechRecognizer.isAvailable { + microphoneButton + .padding() + } + + if !message.isEmpty && !speechRecognizer.isRecording { + playbackButton + .padding() + } + } + } + + private var microphoneButton: some View { + Button( + action: { + microphoneButtonPressed() + }, + label: { + Image(systemName: "mic.fill") + .accessibilityLabel(Text("Microphone Button")) + .font(.largeTitle) + .foregroundColor( + speechRecognizer.isRecording ? .red : Color(.systemGray2) + ) + .scaleEffect(speechRecognizer.isRecording ? 1.2 : 1.0) + .opacity(speechRecognizer.isRecording ? 0.7 : 1.0) + .animation( + speechRecognizer.isRecording ? .easeInOut(duration: 0.5).repeatForever(autoreverses: true) : .default, + value: speechRecognizer.isRecording + ) + } + ) + } + + private var playbackButton: some View { + Button( + action: { + playbackButtonPressed() + }, + label: { + Image(systemName: "play.fill") + .accessibilityLabel(Text("Playback Button")) + .font(.largeTitle) + .foregroundColor( + speechSynthesizer.isSpeaking ? .blue : Color(.systemGray2) + ) + .scaleEffect(speechSynthesizer.isSpeaking ? 1.2 : 1.0) + .opacity(speechSynthesizer.isSpeaking ? 0.7 : 1.0) + .animation( + speechSynthesizer.isSpeaking ? .easeInOut(duration: 0.5).repeatForever(autoreverses: true) : .default, + value: speechSynthesizer.isSpeaking + ) + } + ) + } + + + private func microphoneButtonPressed() { + if speechRecognizer.isRecording { + speechRecognizer.stop() + } else { + message = "" + + Task { + do { + for try await result in speechRecognizer.start() { + message = result.bestTranscription.formattedString + } + } + } + } + } + + private func playbackButtonPressed() { + if speechSynthesizer.isSpeaking { + speechSynthesizer.pause() + } else { + speechSynthesizer.speak(message) + } + } +} + + +#Preview { + SpeechTestView() +} diff --git a/Tests/UITests/TestApp/TestApp.swift b/Tests/UITests/TestApp/TestApp.swift index 3f6e782..5391a1d 100644 --- a/Tests/UITests/TestApp/TestApp.swift +++ b/Tests/UITests/TestApp/TestApp.swift @@ -17,9 +17,7 @@ struct UITestsApp: App { var body: some Scene { WindowGroup { - NavigationStack { - Text("Spezi Speech") - } + SpeechTestView() .spezi(appDelegate) } } diff --git a/Tests/UITests/TestApp/TestAppDelegate.swift b/Tests/UITests/TestApp/TestAppDelegate.swift index 8b500f1..e498368 100644 --- a/Tests/UITests/TestApp/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/TestAppDelegate.swift @@ -7,14 +7,15 @@ // import Spezi -import SpeziSpeech -import SwiftUI +import SpeziSpeechRecognizer +import SpeziSpeechSynthesizer class TestAppDelegate: SpeziAppDelegate { override var configuration: Configuration { Configuration { - Speech() + SpeechRecognizer() + SpeechSynthesizer() } } } diff --git a/Tests/UITests/TestAppUITests/SpeziSpeechTests.swift b/Tests/UITests/TestAppUITests/TestAppUITests.swift similarity index 57% rename from Tests/UITests/TestAppUITests/SpeziSpeechTests.swift rename to Tests/UITests/TestAppUITests/TestAppUITests.swift index 49f35dd..51a6a41 100644 --- a/Tests/UITests/TestAppUITests/SpeziSpeechTests.swift +++ b/Tests/UITests/TestAppUITests/TestAppUITests.swift @@ -7,14 +7,19 @@ // import XCTest -import XCTestExtensions -final class SpeziSpeechTests: XCTestCase { +class TestAppUITests: XCTestCase { + override func setUpWithError() throws { + try super.setUpWithError() + + continueAfterFailure = false + } + + func testSpeziSpeech() throws { let app = XCUIApplication() app.launch() - - XCTAssert(app.staticTexts["Spezi Speech"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["SpeziSpeech"].waitForExistence(timeout: 1)) } } diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index f0c6543..ace685b 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -7,13 +7,13 @@ objects = { /* Begin PBXBuildFile section */ - 2F64EA852A86B347006789D0 /* TestAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F64EA812A86B346006789D0 /* TestAppDelegate.swift */; }; - 2F64EA882A86B36C006789D0 /* TestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F64EA872A86B36C006789D0 /* TestApp.swift */; }; - 2F64EA8B2A86B3DE006789D0 /* XCTestExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 2F64EA8A2A86B3DE006789D0 /* XCTestExtensions */; }; - 2F64EA8E2A86B46B006789D0 /* Spezi in Frameworks */ = {isa = PBXBuildFile; productRef = 2F64EA8D2A86B46B006789D0 /* Spezi */; }; - 2F68C3C8292EA52000B3E12C /* SpeziSpeech in Frameworks */ = {isa = PBXBuildFile; productRef = 2F68C3C7292EA52000B3E12C /* SpeziSpeech */; }; 2F6D139A28F5F386007C25D6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2F6D139928F5F386007C25D6 /* Assets.xcassets */; }; - 2FA43E922AE057CA009B1B2C /* SpeziSpeechTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA43E912AE057CA009B1B2C /* SpeziSpeechTests.swift */; }; + 2F8A431329130A8C005D2B8F /* TestAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F8A431229130A8C005D2B8F /* TestAppUITests.swift */; }; + 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */; }; + 979087112AFF07FF00F78FA4 /* SpeechTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 979087102AFF07FF00F78FA4 /* SpeechTestView.swift */; }; + 97E117752AFF0A89002EA48A /* SpeziSpeechRecognizer in Frameworks */ = {isa = PBXBuildFile; productRef = 97E117742AFF0A89002EA48A /* SpeziSpeechRecognizer */; }; + 97E117772AFF0A89002EA48A /* SpeziSpeechSynthesizer in Frameworks */ = {isa = PBXBuildFile; productRef = 97E117762AFF0A89002EA48A /* SpeziSpeechSynthesizer */; }; + 97FC62782B02AEDF0025D933 /* TestAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97FC62772B02AEDF0025D933 /* TestAppDelegate.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -40,14 +40,16 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 2F64EA812A86B346006789D0 /* TestAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestAppDelegate.swift; sourceTree = ""; }; - 2F64EA872A86B36C006789D0 /* TestApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestApp.swift; sourceTree = ""; }; 2F68C3C6292E9F8F00B3E12C /* SpeziSpeech */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SpeziSpeech; path = ../..; sourceTree = ""; }; 2F6D139228F5F384007C25D6 /* TestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 2F6D139928F5F386007C25D6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 2F6D13AC28F5F386007C25D6 /* TestAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TestAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 2FA43E912AE057CA009B1B2C /* SpeziSpeechTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeziSpeechTests.swift; sourceTree = ""; }; + 2F8A431229130A8C005D2B8F /* TestAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAppUITests.swift; sourceTree = ""; }; + 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestApp.swift; sourceTree = ""; }; 2FB0758A299DDB9000C0B37F /* TestApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestApp.xctestplan; sourceTree = ""; }; + 973B3CE42AFC725B00FBC8B1 /* Speech.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Speech.framework; path = System/Library/Frameworks/Speech.framework; sourceTree = SDKROOT; }; + 979087102AFF07FF00F78FA4 /* SpeechTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechTestView.swift; sourceTree = ""; }; + 97FC62772B02AEDF0025D933 /* TestAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestAppDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -55,8 +57,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 2F64EA8E2A86B46B006789D0 /* Spezi in Frameworks */, - 2F68C3C8292EA52000B3E12C /* SpeziSpeech in Frameworks */, + 97E117752AFF0A89002EA48A /* SpeziSpeechRecognizer in Frameworks */, + 97E117772AFF0A89002EA48A /* SpeziSpeechSynthesizer in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -64,7 +66,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 2F64EA8B2A86B3DE006789D0 /* XCTestExtensions in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -95,8 +96,9 @@ 2F6D139428F5F384007C25D6 /* TestApp */ = { isa = PBXGroup; children = ( - 2F64EA872A86B36C006789D0 /* TestApp.swift */, - 2F64EA812A86B346006789D0 /* TestAppDelegate.swift */, + 97FC62772B02AEDF0025D933 /* TestAppDelegate.swift */, + 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */, + 979087102AFF07FF00F78FA4 /* SpeechTestView.swift */, 2F6D139928F5F386007C25D6 /* Assets.xcassets */, ); path = TestApp; @@ -105,7 +107,7 @@ 2F6D13AF28F5F386007C25D6 /* TestAppUITests */ = { isa = PBXGroup; children = ( - 2FA43E912AE057CA009B1B2C /* SpeziSpeechTests.swift */, + 2F8A431229130A8C005D2B8F /* TestAppUITests.swift */, ); path = TestAppUITests; sourceTree = ""; @@ -113,6 +115,7 @@ 2F6D13C228F5F3BE007C25D6 /* Frameworks */ = { isa = PBXGroup; children = ( + 973B3CE42AFC725B00FBC8B1 /* Speech.framework */, ); name = Frameworks; sourceTree = ""; @@ -135,8 +138,8 @@ ); name = TestApp; packageProductDependencies = ( - 2F68C3C7292EA52000B3E12C /* SpeziSpeech */, - 2F64EA8D2A86B46B006789D0 /* Spezi */, + 97E117742AFF0A89002EA48A /* SpeziSpeechRecognizer */, + 97E117762AFF0A89002EA48A /* SpeziSpeechSynthesizer */, ); productName = Example; productReference = 2F6D139228F5F384007C25D6 /* TestApp.app */; @@ -156,9 +159,6 @@ 2F6D13AE28F5F386007C25D6 /* PBXTargetDependency */, ); name = TestAppUITests; - packageProductDependencies = ( - 2F64EA8A2A86B3DE006789D0 /* XCTestExtensions */, - ); productName = ExampleUITests; productReference = 2F6D13AC28F5F386007C25D6 /* TestAppUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; @@ -192,8 +192,6 @@ ); mainGroup = 2F6D138928F5F384007C25D6; packageReferences = ( - 2F64EA892A86B3DE006789D0 /* XCRemoteSwiftPackageReference "XCTestExtensions" */, - 2F64EA8C2A86B46B006789D0 /* XCRemoteSwiftPackageReference "Spezi" */, ); productRefGroup = 2F6D139328F5F384007C25D6 /* Products */; projectDirPath = ""; @@ -228,8 +226,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2F64EA852A86B347006789D0 /* TestAppDelegate.swift in Sources */, - 2F64EA882A86B36C006789D0 /* TestApp.swift in Sources */, + 979087112AFF07FF00F78FA4 /* SpeechTestView.swift in Sources */, + 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */, + 97FC62782B02AEDF0025D933 /* TestAppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -237,7 +236,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2FA43E922AE057CA009B1B2C /* SpeziSpeechTests.swift in Sources */, + 2F8A431329130A8C005D2B8F /* TestAppUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -303,7 +302,6 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -358,7 +356,6 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -371,7 +368,6 @@ 2F6D13B728F5F386007C25D6 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; @@ -381,11 +377,14 @@ ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "This app needs access to your microphone for voice input."; + INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "This app needs access to Speech recognition in order to translate voice input."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -408,7 +407,6 @@ 2F6D13B828F5F386007C25D6 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; @@ -418,11 +416,14 @@ ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "This app needs access to your microphone for voice input."; + INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "This app needs access to Speech recognition in order to translate voice input."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -450,6 +451,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 637867499T; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.speech.testapp.uitests; @@ -474,6 +476,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 637867499T; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.speech.testapp.uitests; @@ -541,7 +544,6 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -554,7 +556,6 @@ 2FB07588299DDB6000C0B37F /* Test */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; @@ -564,11 +565,14 @@ ENABLE_PREVIEWS = YES; ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "This app needs access to your microphone for voice input."; + INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "This app needs access to Speech recognition in order to translate voice input."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -596,6 +600,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 637867499T; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.speech.testapp.uitests; @@ -647,39 +652,14 @@ }; /* End XCConfigurationList section */ -/* Begin XCRemoteSwiftPackageReference section */ - 2F64EA892A86B3DE006789D0 /* XCRemoteSwiftPackageReference "XCTestExtensions" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/StanfordBDHG/XCTestExtensions.git"; - requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.4.6; - }; - }; - 2F64EA8C2A86B46B006789D0 /* XCRemoteSwiftPackageReference "Spezi" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/StanfordSpezi/Spezi.git"; - requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.8.0; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - /* Begin XCSwiftPackageProductDependency section */ - 2F64EA8A2A86B3DE006789D0 /* XCTestExtensions */ = { - isa = XCSwiftPackageProductDependency; - package = 2F64EA892A86B3DE006789D0 /* XCRemoteSwiftPackageReference "XCTestExtensions" */; - productName = XCTestExtensions; - }; - 2F64EA8D2A86B46B006789D0 /* Spezi */ = { + 97E117742AFF0A89002EA48A /* SpeziSpeechRecognizer */ = { isa = XCSwiftPackageProductDependency; - package = 2F64EA8C2A86B46B006789D0 /* XCRemoteSwiftPackageReference "Spezi" */; - productName = Spezi; + productName = SpeziSpeechRecognizer; }; - 2F68C3C7292EA52000B3E12C /* SpeziSpeech */ = { + 97E117762AFF0A89002EA48A /* SpeziSpeechSynthesizer */ = { isa = XCSwiftPackageProductDependency; - productName = SpeziSpeech; + productName = SpeziSpeechSynthesizer; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme index 4b402c9..17dfd5d 100644 --- a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme +++ b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme @@ -14,9 +14,23 @@ buildForAnalyzing = "NO"> + + + + @@ -37,7 +51,7 @@