diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 6bb027f..ab6e9f0 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -2,22 +2,31 @@ name: Swift on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] +#list of jobs to perform jobs: - test: - name: Test - runs-on: macOS-latest - + #the only job in the list, named `build` + build_and_test: + #specify OS to run the jobs on + runs-on: macos-latest + #sequential steps to run for the `build` job steps: - - uses: actions/checkout@v2 - - name: Build and Test + # step 1, use Marketplace action called Checkout@v2, to checkout the code + - uses: actions/checkout@v2 #'uses' keyword launches the Marketplace action + # step 2, verbosely build the package using the `swift` CLI + - name: Build + run: swift build -v #'run' keyword executes the command, as if it's run in terminal + # step 3, run tests + # Note that you must use "=" and not ":" despite error logs for -destiation using ":" + # Also using "Any iOS Simulator" doesn't seem to work despite being an option. + # The using CODE_SIGN... and beyond are for Codecov purposes when generating results. + - name: Run tests run: | - cd Example - pod install - xcodebuild clean test -workspace OSSSpeechKit.xcworkspace -scheme OSSSpeechKit-Example -destination "platform=iOS Simulator,OS=13.7,name=iPhone 11 Pro Max" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO - bash <(curl https://codecov.io/bash | sed 's/"$beta_xcode_partials"//g') - env: - destination: "platform=iOS Simulator,OS=13.7,name=iPhone 11 Pro Max" + cd Example + pod install + xcodebuild clean test -scheme OSSSpeechKit-Example -workspace OSSSpeechKit.xcworkspace -destination 'platform=iOS Simulator,OS=16.2,name=iPhone 14' CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO + bash <(curl https://codecov.io/bash | sed 's/"$beta_xcode_partials"//g') + diff --git a/.travis.yml b/.travis.yml index 581eb76..226f4cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,16 @@ # Check Travis CI version info: # https://docs.travis-ci.com/user/reference/osx/ language: objective-c -osx_image: xcode11.3 +osx_image: xcode14.2 xcode_workspace: OSSSpeechKit.xcworkspace xcode_scheme: OSSSpeechKit-Example -xcode_destination: platform=iOS Simulator,OS=13.3,name=iPhone 11 +xcode_destination: platform=iOS Simulator,OS=16.2,name=iPhone 14 before_install: -- cd Example -- pod install + - cd Example + - pod install after_success: -# - bash <(curl -s https://codecov.io/bash) -# Fixing code cov issue with solution from https://community.codecov.io/t/llvm-cov-failed-to-produce-results-for/1652/9 + # - bash <(curl -s https://codecov.io/bash) + # Fixing code cov issue with solution from https://community.codecov.io/t/llvm-cov-failed-to-produce-results-for/1652/9 - bash <(curl https://codecov.io/bash | sed 's/"$beta_xcode_partials"//g') - gem install jazzy - make documentation diff --git a/Example/OSSSpeechKit.xcodeproj/project.pbxproj b/Example/OSSSpeechKit.xcodeproj/project.pbxproj index 5e1381a..969bc43 100644 --- a/Example/OSSSpeechKit.xcodeproj/project.pbxproj +++ b/Example/OSSSpeechKit.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -186,7 +186,7 @@ 607FACCD1AFB9204008FA782 /* Frameworks */, 607FACCE1AFB9204008FA782 /* Resources */, 5004C6A87EB6F934EFA310F2 /* [CP] Embed Pods Frameworks */, - B042F2D2245FCDD900958719 /* ShellScript */, + B042F2D2245FCDD900958719 /* Run Script */, ); buildRules = ( ); @@ -223,7 +223,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1140; - LastUpgradeCheck = 1130; + LastUpgradeCheck = 1420; ORGANIZATIONNAME = CocoaPods; TargetAttributes = { 607FACCF1AFB9204008FA782 = { @@ -287,20 +287,19 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-OSSSpeechKit_Example/Pods-OSSSpeechKit_Example-frameworks.sh", "${BUILT_PRODUCTS_DIR}/OSSSpeechKit/OSSSpeechKit.framework", - "${BUILT_PRODUCTS_DIR}/PureLayout/PureLayout.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OSSSpeechKit.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PureLayout.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-OSSSpeechKit_Example/Pods-OSSSpeechKit_Example-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - B042F2D2245FCDD900958719 /* ShellScript */ = { + B042F2D2245FCDD900958719 /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -308,13 +307,14 @@ ); inputPaths = ( ); + name = "Run Script"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; }; EBB80964F428049277FF60C0 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -428,6 +428,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -453,7 +454,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -484,6 +485,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -503,10 +505,9 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; VALIDATE_PRODUCT = YES; }; @@ -522,8 +523,11 @@ DEVELOPMENT_TEAM = ""; GCC_OPTIMIZATION_LEVEL = 0; INFOPLIST_FILE = OSSSpeechKit/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); MODULE_NAME = ExampleApp; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -543,8 +547,11 @@ DEVELOPMENT_TEAM = ""; GCC_OPTIMIZATION_LEVEL = 0; INFOPLIST_FILE = OSSSpeechKit/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); MODULE_NAME = ExampleApp; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -571,7 +578,12 @@ "$(inherited)", ); INFOPLIST_FILE = Tests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -594,7 +606,12 @@ ); GCC_OPTIMIZATION_LEVEL = 0; INFOPLIST_FILE = Tests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Example/OSSSpeechKit.xcodeproj/xcshareddata/xcschemes/OSSSpeechKit-Example.xcscheme b/Example/OSSSpeechKit.xcodeproj/xcshareddata/xcschemes/OSSSpeechKit-Example.xcscheme index 557e38f..99dd2fd 100644 --- a/Example/OSSSpeechKit.xcodeproj/xcshareddata/xcschemes/OSSSpeechKit-Example.xcscheme +++ b/Example/OSSSpeechKit.xcodeproj/xcshareddata/xcschemes/OSSSpeechKit-Example.xcscheme @@ -1,6 +1,6 @@ '../' - pod 'PureLayout', :inhibit_warnings => true - + target 'OSSSpeechKit_Tests' do inherit! :search_paths diff --git a/Example/Podfile.lock b/Example/Podfile.lock new file mode 100644 index 0000000..5a657c7 --- /dev/null +++ b/Example/Podfile.lock @@ -0,0 +1,16 @@ +PODS: + - OSSSpeechKit (0.3.3) + +DEPENDENCIES: + - OSSSpeechKit (from `../`) + +EXTERNAL SOURCES: + OSSSpeechKit: + :path: "../" + +SPEC CHECKSUMS: + OSSSpeechKit: ea0fd8151e7e338bc6ddc6bb749455fc3b33cfde + +PODFILE CHECKSUM: 619c7767d93bbf8bc7a5c2d0a1d118e435561c49 + +COCOAPODS: 1.11.3 diff --git a/Example/Tests/OSSSpeechTests.swift b/Example/Tests/OSSSpeechTests.swift index 2090130..24c3e81 100644 --- a/Example/Tests/OSSSpeechTests.swift +++ b/Example/Tests/OSSSpeechTests.swift @@ -21,8 +21,9 @@ // IN THE SOFTWARE. // +#if canImport(Speech) import XCTest -import OSSSpeechKit +@testable import OSSSpeechKit import AVKit class OSSSpeechTests: XCTestCase { @@ -52,7 +53,11 @@ class OSSSpeechTests: XCTestCase { XCTAssert(speechAuth == true) } + /// This test exists purely for code coverage. func testVoiceDecoderNil() { + // Ignore the deprecation warning as the alternative solution returns nil, which is not what we want - we want a valid value. + // The below option is the non-deprectaed option however it returns nil, which we don't want. + // let archiver = NSKeyedUnarchiver(forReadingFrom: Data()) let archiver = NSKeyedUnarchiver(forReadingWith: Data()) let voice = OSSVoice(coder: archiver) let utterance = OSSUtterance(coder: archiver) @@ -259,13 +264,16 @@ class OSSSpeechTests: XCTestCase { waitForExpectations(timeout: 3) XCTAssert(hasCompleted, "Did not complete the Speech Recording expectation") } - - func testRecordPermission() { - speechKit?.recordVoice(requestMicPermission: true) - let recPermission = AVAudioSession.sharedInstance().recordPermission - sleep(2) - XCTAssert(recPermission != .granted, "AVAudioSession returned incorrect permission.") - } + + #if !os(macOS) + // TODO: Need to write a mock for authorizing speech and the recording functions. + // Cannot interact with UI to approve the use of Microphone which results in a crash when trying to call record functions. +// func testRecordPermission() { +// speechKit?.recordVoice(requestMicPermission: true) +// let recPermission = AVAudioSession.sharedInstance().recordPermission +// sleep(2) +// XCTAssertEqual(recPermission, .granted) +// } func testAudioSessionSetting() { XCTAssertNotNil(speechKit?.audioSession) @@ -274,21 +282,32 @@ class OSSSpeechTests: XCTestCase { try? customSession.setCategory(.ambient) XCTAssert(customSession.category == .ambient) } + #endif func testUtilityClassStrings() { let util = OSSSpeechUtility() var mainBundleStringNotSDKString = util.getString(forLocalizedName: "OSSSpeechKitTests_testString", defaultValue: "") XCTAssert(mainBundleStringNotSDKString.isEmpty, "Localized string does not exist in the SDK; the default value (\"\") should be returned.") util.stringsTableName = "LocalizableTests" - XCTAssert(util.stringsTableName == "LocalizableTests", "The table name did not override the default value.") + guard Bundle.main.path(forResource: util.stringsTableName, ofType: "strings") != nil else { + XCTFail("Strings file does not exist") + return + } + // Assert that we have overriden the table name. + XCTAssertEqual(util.stringsTableName, "LocalizableTests", "The table name did not override the default value.") + + // Check that we are retrieveing the correct string. mainBundleStringNotSDKString = util.getString(forLocalizedName: "OSSSpeechKitTests_testString", defaultValue: "") - XCTAssert(mainBundleStringNotSDKString == "This is a test string.", "The name of the localized string should now be found.") + XCTAssertEqual(mainBundleStringNotSDKString, "This is a test string.", "The name of the localized string should now be found.") + + // Check that we are retrieveing the correct string for key. + let testString = util.getString(forLocalizedName: "OSSSpeechKitAuthorizationStatus_messageNotDetermined", defaultValue: "") + let expectedString = "The test class is overriding the message: The app's authorization status has not yet been determined." + XCTAssertEqual(testString, expectedString) + + // Check that we return the correct error string. let blankKey = util.getString(forLocalizedName: "", defaultValue: "") XCTAssert(blankKey == "!&!&!&!&!&!&!&!&!&!&!&!&!&!&!", "Passing in an empty string should return an obvious error string.") - let testString = util.getString(forLocalizedName: "OSSSpeechKitAuthorizationStatus_messageNotDetermined", - defaultValue: "The app's authorization status has not yet been determined.") - let expectedString = "The test class is overriding the message: The app's authorization status has not yet been determined." - XCTAssert(testString == expectedString, "The SDK string was not overridden.") } } @@ -327,3 +346,4 @@ extension OSSSpeechTests: OSSSpeechDelegate { speech.debugLog(object: self, message: "Did fail to commence speech.") } } +#endif diff --git a/Example/Tests/SFSpeechRecognizerMock.swift b/Example/Tests/SFSpeechRecognizerMock.swift index 98bcf54..d0ce5fc 100644 --- a/Example/Tests/SFSpeechRecognizerMock.swift +++ b/Example/Tests/SFSpeechRecognizerMock.swift @@ -21,7 +21,8 @@ // IN THE SOFTWARE. // -import UIKit +#if canImport(Speech) +import Foundation import Speech class SFSpeechRecognizerMock: SFSpeechRecognizer { @@ -30,3 +31,4 @@ class SFSpeechRecognizerMock: SFSpeechRecognizer { handler(.authorized) } } +#endif diff --git a/OSSSpeechKit.podspec b/OSSSpeechKit.podspec index 58b3d4d..1e36209 100644 --- a/OSSSpeechKit.podspec +++ b/OSSSpeechKit.podspec @@ -8,10 +8,10 @@ Pod::Spec.new do |s| s.name = 'OSSSpeechKit' - s.version = '0.3.2' + s.version = '0.3.3' s.summary = 'OSSSpeechKit provides developers easy text to voice integration.' s.swift_version = "5.0" - s.platform = :ios, "12.0" + s.platform = :ios, "13.0" # This description is used to generate tags and improve search results. # * Think: What does it do? Why did you write it? What is the focus? @@ -28,7 +28,7 @@ OSSSpeechKit offers an easy way to integrate text to voice using native AVFounda s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'appdevguy' => 'seaniosdeveloper@gmail.com' } s.source = { :git => 'https://github.com/appdevguy/OSSSpeechKit.git', :tag => s.version.to_s } - s.ios.deployment_target = '12.0' + s.ios.deployment_target = '13.0' s.source_files = 'OSSSpeechKit/Classes/*.swift' s.resource_bundles = { diff --git a/OSSSpeechKit/Assets/Images.xcassets/bg-BG.imageset/Contents.json b/OSSSpeechKit/Assets/Images.xcassets/bg-BG.imageset/Contents.json new file mode 100644 index 0000000..50d7ce6 --- /dev/null +++ b/OSSSpeechKit/Assets/Images.xcassets/bg-BG.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","filename":"bg-BG@1x.png","scale":"1x"},{"idiom":"universal","filename":"bg-BG@2x.png","scale":"2x"},{"idiom":"universal","filename":"bg-BG@3x.png","scale":"3x"}],"info":{"version":1,"author":"xcode"}} diff --git a/OSSSpeechKit/Assets/Images.xcassets/bg-BG.imageset/bg-BG@1x.png b/OSSSpeechKit/Assets/Images.xcassets/bg-BG.imageset/bg-BG@1x.png new file mode 100644 index 0000000..b9b4438 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/bg-BG.imageset/bg-BG@1x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/bg-BG.imageset/bg-BG@2x.png b/OSSSpeechKit/Assets/Images.xcassets/bg-BG.imageset/bg-BG@2x.png new file mode 100644 index 0000000..43b2823 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/bg-BG.imageset/bg-BG@2x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/bg-BG.imageset/bg-BG@3x.png b/OSSSpeechKit/Assets/Images.xcassets/bg-BG.imageset/bg-BG@3x.png new file mode 100644 index 0000000..e3ad3e3 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/bg-BG.imageset/bg-BG@3x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/ca-ES.imageset/Contents.json b/OSSSpeechKit/Assets/Images.xcassets/ca-ES.imageset/Contents.json new file mode 100644 index 0000000..8e7cca6 --- /dev/null +++ b/OSSSpeechKit/Assets/Images.xcassets/ca-ES.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","filename":"ca-ES@1x.png","scale":"1x"},{"idiom":"universal","filename":"ca-ES@2x.png","scale":"2x"},{"idiom":"universal","filename":"ca-ES@3x.png","scale":"3x"}],"info":{"version":1,"author":"xcode"}} diff --git a/OSSSpeechKit/Assets/Images.xcassets/ca-ES.imageset/ca-ES@1x.png b/OSSSpeechKit/Assets/Images.xcassets/ca-ES.imageset/ca-ES@1x.png new file mode 100644 index 0000000..1d0a985 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/ca-ES.imageset/ca-ES@1x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/ca-ES.imageset/ca-ES@2x.png b/OSSSpeechKit/Assets/Images.xcassets/ca-ES.imageset/ca-ES@2x.png new file mode 100644 index 0000000..5678c31 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/ca-ES.imageset/ca-ES@2x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/ca-ES.imageset/ca-ES@3x.png b/OSSSpeechKit/Assets/Images.xcassets/ca-ES.imageset/ca-ES@3x.png new file mode 100644 index 0000000..d937ae2 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/ca-ES.imageset/ca-ES@3x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/en-IN.imageset/Contents.json b/OSSSpeechKit/Assets/Images.xcassets/en-IN.imageset/Contents.json new file mode 100644 index 0000000..ce5c90f --- /dev/null +++ b/OSSSpeechKit/Assets/Images.xcassets/en-IN.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","filename":"en-IN@1x.png","scale":"1x"},{"idiom":"universal","filename":"en-IN@2x.png","scale":"2x"},{"idiom":"universal","filename":"en-IN@3x.png","scale":"3x"}],"info":{"version":1,"author":"xcode"}} diff --git a/OSSSpeechKit/Assets/Images.xcassets/en-IN.imageset/en-IN@1x.png b/OSSSpeechKit/Assets/Images.xcassets/en-IN.imageset/en-IN@1x.png new file mode 100644 index 0000000..f697235 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/en-IN.imageset/en-IN@1x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/en-IN.imageset/en-IN@2x.png b/OSSSpeechKit/Assets/Images.xcassets/en-IN.imageset/en-IN@2x.png new file mode 100644 index 0000000..be2a1f2 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/en-IN.imageset/en-IN@2x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/en-IN.imageset/en-IN@3x.png b/OSSSpeechKit/Assets/Images.xcassets/en-IN.imageset/en-IN@3x.png new file mode 100644 index 0000000..ca94fe2 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/en-IN.imageset/en-IN@3x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/hr-HR.imageset/Contents.json b/OSSSpeechKit/Assets/Images.xcassets/hr-HR.imageset/Contents.json new file mode 100644 index 0000000..bf75344 --- /dev/null +++ b/OSSSpeechKit/Assets/Images.xcassets/hr-HR.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","filename":"hr-HR@1x.png","scale":"1x"},{"idiom":"universal","filename":"hr-HR@2x.png","scale":"2x"},{"idiom":"universal","filename":"hr-HR@3x.png","scale":"3x"}],"info":{"version":1,"author":"xcode"}} diff --git a/OSSSpeechKit/Assets/Images.xcassets/hr-HR.imageset/hr-HR@1x.png b/OSSSpeechKit/Assets/Images.xcassets/hr-HR.imageset/hr-HR@1x.png new file mode 100644 index 0000000..3bf18f1 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/hr-HR.imageset/hr-HR@1x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/hr-HR.imageset/hr-HR@2x.png b/OSSSpeechKit/Assets/Images.xcassets/hr-HR.imageset/hr-HR@2x.png new file mode 100644 index 0000000..080c58f Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/hr-HR.imageset/hr-HR@2x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/hr-HR.imageset/hr-HR@3x.png b/OSSSpeechKit/Assets/Images.xcassets/hr-HR.imageset/hr-HR@3x.png new file mode 100644 index 0000000..b9495a5 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/hr-HR.imageset/hr-HR@3x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/ms-MY.imageset/Contents.json b/OSSSpeechKit/Assets/Images.xcassets/ms-MY.imageset/Contents.json new file mode 100644 index 0000000..122981b --- /dev/null +++ b/OSSSpeechKit/Assets/Images.xcassets/ms-MY.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","filename":"ms-MY@1x.png","scale":"1x"},{"idiom":"universal","filename":"ms-MY@2x.png","scale":"2x"},{"idiom":"universal","filename":"ms-MY@3x.png","scale":"3x"}],"info":{"version":1,"author":"xcode"}} diff --git a/OSSSpeechKit/Assets/Images.xcassets/ms-MY.imageset/ms-MY@1x.png b/OSSSpeechKit/Assets/Images.xcassets/ms-MY.imageset/ms-MY@1x.png new file mode 100644 index 0000000..f479f6c Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/ms-MY.imageset/ms-MY@1x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/ms-MY.imageset/ms-MY@2x.png b/OSSSpeechKit/Assets/Images.xcassets/ms-MY.imageset/ms-MY@2x.png new file mode 100644 index 0000000..7cd384e Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/ms-MY.imageset/ms-MY@2x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/ms-MY.imageset/ms-MY@3x.png b/OSSSpeechKit/Assets/Images.xcassets/ms-MY.imageset/ms-MY@3x.png new file mode 100644 index 0000000..9eb78a5 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/ms-MY.imageset/ms-MY@3x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/nb-NO.imageset/Contents.json b/OSSSpeechKit/Assets/Images.xcassets/nb-NO.imageset/Contents.json new file mode 100644 index 0000000..48fc6be --- /dev/null +++ b/OSSSpeechKit/Assets/Images.xcassets/nb-NO.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","filename":"no-NO@1x.png","scale":"1x"},{"idiom":"universal","filename":"no-NO@2x.png","scale":"2x"},{"idiom":"universal","filename":"no-NO@3x.png","scale":"3x"}],"info":{"version":1,"author":"xcode"}} diff --git a/OSSSpeechKit/Assets/Images.xcassets/nb-NO.imageset/no-NO@1x.png b/OSSSpeechKit/Assets/Images.xcassets/nb-NO.imageset/no-NO@1x.png new file mode 100644 index 0000000..6e06dc4 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/nb-NO.imageset/no-NO@1x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/nb-NO.imageset/no-NO@2x.png b/OSSSpeechKit/Assets/Images.xcassets/nb-NO.imageset/no-NO@2x.png new file mode 100644 index 0000000..d9c5950 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/nb-NO.imageset/no-NO@2x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/nb-NO.imageset/no-NO@3x.png b/OSSSpeechKit/Assets/Images.xcassets/nb-NO.imageset/no-NO@3x.png new file mode 100644 index 0000000..b974868 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/nb-NO.imageset/no-NO@3x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/uk-UA.imageset/Contents.json b/OSSSpeechKit/Assets/Images.xcassets/uk-UA.imageset/Contents.json new file mode 100644 index 0000000..22aebd0 --- /dev/null +++ b/OSSSpeechKit/Assets/Images.xcassets/uk-UA.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","filename":"uk-UA@1x.png","scale":"1x"},{"idiom":"universal","filename":"uk-UA@2x.png","scale":"2x"},{"idiom":"universal","filename":"uk-UA@3x.png","scale":"3x"}],"info":{"version":1,"author":"xcode"}} diff --git a/OSSSpeechKit/Assets/Images.xcassets/uk-UA.imageset/uk-UA@1x.png b/OSSSpeechKit/Assets/Images.xcassets/uk-UA.imageset/uk-UA@1x.png new file mode 100644 index 0000000..2980fe3 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/uk-UA.imageset/uk-UA@1x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/uk-UA.imageset/uk-UA@2x.png b/OSSSpeechKit/Assets/Images.xcassets/uk-UA.imageset/uk-UA@2x.png new file mode 100644 index 0000000..8d359fe Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/uk-UA.imageset/uk-UA@2x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/uk-UA.imageset/uk-UA@3x.png b/OSSSpeechKit/Assets/Images.xcassets/uk-UA.imageset/uk-UA@3x.png new file mode 100644 index 0000000..ee4df8c Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/uk-UA.imageset/uk-UA@3x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/vi-VN.imageset/Contents.json b/OSSSpeechKit/Assets/Images.xcassets/vi-VN.imageset/Contents.json new file mode 100644 index 0000000..c496b8b --- /dev/null +++ b/OSSSpeechKit/Assets/Images.xcassets/vi-VN.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","filename":"vi-VN@1x.png","scale":"1x"},{"idiom":"universal","filename":"vi-VN@2x.png","scale":"2x"},{"idiom":"universal","filename":"vi-VN@3x.png","scale":"3x"}],"info":{"version":1,"author":"xcode"}} diff --git a/OSSSpeechKit/Assets/Images.xcassets/vi-VN.imageset/vi-VN@1x.png b/OSSSpeechKit/Assets/Images.xcassets/vi-VN.imageset/vi-VN@1x.png new file mode 100644 index 0000000..b6fe133 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/vi-VN.imageset/vi-VN@1x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/vi-VN.imageset/vi-VN@2x.png b/OSSSpeechKit/Assets/Images.xcassets/vi-VN.imageset/vi-VN@2x.png new file mode 100644 index 0000000..b33202e Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/vi-VN.imageset/vi-VN@2x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/vi-VN.imageset/vi-VN@3x.png b/OSSSpeechKit/Assets/Images.xcassets/vi-VN.imageset/vi-VN@3x.png new file mode 100644 index 0000000..b0edbf8 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/vi-VN.imageset/vi-VN@3x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/zh-CN.imageset/Contents.json b/OSSSpeechKit/Assets/Images.xcassets/zh-CN.imageset/Contents.json new file mode 100644 index 0000000..62360e8 --- /dev/null +++ b/OSSSpeechKit/Assets/Images.xcassets/zh-CN.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"idiom":"universal","filename":"zh-CH@1x.png","scale":"1x"},{"idiom":"universal","filename":"zh-CH@2x.png","scale":"2x"},{"idiom":"universal","filename":"zh-CH@3x.png","scale":"3x"}],"info":{"version":1,"author":"xcode"}} diff --git a/OSSSpeechKit/Assets/Images.xcassets/zh-CN.imageset/zh-CH@1x.png b/OSSSpeechKit/Assets/Images.xcassets/zh-CN.imageset/zh-CH@1x.png new file mode 100644 index 0000000..5ca063f Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/zh-CN.imageset/zh-CH@1x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/zh-CN.imageset/zh-CH@2x.png b/OSSSpeechKit/Assets/Images.xcassets/zh-CN.imageset/zh-CH@2x.png new file mode 100644 index 0000000..60aae17 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/zh-CN.imageset/zh-CH@2x.png differ diff --git a/OSSSpeechKit/Assets/Images.xcassets/zh-CN.imageset/zh-CH@3x.png b/OSSSpeechKit/Assets/Images.xcassets/zh-CN.imageset/zh-CH@3x.png new file mode 100644 index 0000000..2d44c39 Binary files /dev/null and b/OSSSpeechKit/Assets/Images.xcassets/zh-CN.imageset/zh-CH@3x.png differ diff --git a/OSSSpeechKit/Classes/OSSSpeech.swift b/OSSSpeechKit/Classes/OSSSpeech.swift index 6d44100..e20b6b1 100755 --- a/OSSSpeechKit/Classes/OSSSpeech.swift +++ b/OSSSpeechKit/Classes/OSSSpeech.swift @@ -21,9 +21,10 @@ // IN THE SOFTWARE. // -import UIKit -import AVFoundation +#if canImport(Speech) import Speech +import Foundation +import AVFoundation /// The authorization status of the Microphone and recording, imitating the native `SFSpeechRecognizerAuthorizationStatus` public enum OSSSpeechKitAuthorizationStatus: Int { @@ -35,18 +36,18 @@ public enum OSSSpeechKitAuthorizationStatus: Int { case restricted = 2 /// The user granted your app's request to perform speech recognition. case authorized = 3 - + /// A public message that can be displayed to the user. public var message: String { switch self { case .notDetermined: - return OSSSpeechUtility().getString(forLocalizedName:"OSSSpeechKitAuthorizationStatus_messageNotDetermined", defaultValue:"The app's authorization status has not yet been determined.") + return OSSSpeechUtility().getString(forLocalizedName: "OSSSpeechKitAuthorizationStatus_messageNotDetermined", defaultValue: "The app's authorization status has not yet been determined.") case .denied: - return OSSSpeechUtility().getString(forLocalizedName:"OSSSpeechKitAuthorizationStatus_messageDenied", defaultValue:"The user denied your app's request to perform speech recognition.") + return OSSSpeechUtility().getString(forLocalizedName: "OSSSpeechKitAuthorizationStatus_messageDenied", defaultValue: "The user denied your app's request to perform speech recognition.") case .restricted: - return OSSSpeechUtility().getString(forLocalizedName:"OSSSpeechKitAuthorizationStatus_messageRestricted", defaultValue:"The device prevents your app from performing speech recognition.") + return OSSSpeechUtility().getString(forLocalizedName: "OSSSpeechKitAuthorizationStatus_messageRestricted", defaultValue: "The device prevents your app from performing speech recognition.") case .authorized: - return OSSSpeechUtility().getString(forLocalizedName:"OSSSpeechKitAuthorizationStatus_messageAuthorized", defaultValue:"The user granted your app's request to perform speech recognition.") + return OSSSpeechUtility().getString(forLocalizedName: "OSSSpeechKitAuthorizationStatus_messageAuthorized", defaultValue: "The user granted your app's request to perform speech recognition.") } } } @@ -67,29 +68,29 @@ public enum OSSSpeechKitErrorType: Int { case invalidAudioEngine = -6 /// Voice recognition is unavailable. case recogniserUnavailble = -7 - + /// The OSSSpeechKit error message string. /// /// The error message strings can be altered in the Localized strings file. public var errorMessage: String { switch self { case .noMicrophoneAccess: - return OSSSpeechUtility().getString(forLocalizedName:"OSSSpeechKitErrorType_messageNoMicAccess", defaultValue:"Access to the microphone is unavailable.") + return OSSSpeechUtility().getString(forLocalizedName: "OSSSpeechKitErrorType_messageNoMicAccess", defaultValue: "Access to the microphone is unavailable.") case .invalidUtterance: - return OSSSpeechUtility().getString(forLocalizedName:"OSSSpeechKitErrorType_messageInvalidUtterance", defaultValue:"The utterance is invalid. Please ensure you have created one or passed in valid text to speak.") + return OSSSpeechUtility().getString(forLocalizedName: "OSSSpeechKitErrorType_messageInvalidUtterance", defaultValue: "The utterance is invalid. Please ensure you have created one or passed in valid text to speak.") case .invalidText: - return OSSSpeechUtility().getString(forLocalizedName:"OSSSpeechKitErrorType_messageInvalidText", defaultValue:"The text provided to the utterance is either empty or has not been set.") + return OSSSpeechUtility().getString(forLocalizedName: "OSSSpeechKitErrorType_messageInvalidText", defaultValue: "The text provided to the utterance is either empty or has not been set.") case .invalidVoice: - return OSSSpeechUtility().getString(forLocalizedName:"OSSSpeechKitErrorType_messageInvalidVoice", defaultValue:"In order to speak text, a valid voice is required.") + return OSSSpeechUtility().getString(forLocalizedName: "OSSSpeechKitErrorType_messageInvalidVoice", defaultValue: "In order to speak text, a valid voice is required.") case .invalidSpeechRequest: - return OSSSpeechUtility().getString(forLocalizedName:"OSSSpeechKitErrorType_messageInvalidSpeechRequest", defaultValue:"The speech request is invalid. Please ensure the string provided contains text.") + return OSSSpeechUtility().getString(forLocalizedName: "OSSSpeechKitErrorType_messageInvalidSpeechRequest", defaultValue: "The speech request is invalid. Please ensure the string provided contains text.") case .invalidAudioEngine: - return OSSSpeechUtility().getString(forLocalizedName:"OSSSpeechKitErrorType_messageInvalidAudioEngine", defaultValue:"The audio engine is unavailable. Please try again soon.") + return OSSSpeechUtility().getString(forLocalizedName: "OSSSpeechKitErrorType_messageInvalidAudioEngine", defaultValue: "The audio engine is unavailable. Please try again soon.") case .recogniserUnavailble: - return OSSSpeechUtility().getString(forLocalizedName:"OSSSpeechKitErrorType_messageRecogniserUnavailable", defaultValue:"The Speech Recognition service is currently unavailable.") + return OSSSpeechUtility().getString(forLocalizedName: "OSSSpeechKitErrorType_messageRecogniserUnavailable", defaultValue: "The Speech Recognition service is currently unavailable.") } } - + /// The highlevel type of error that occured. /// /// A String will be used in the OSSSpeechKitErrorType error: Error? that is returned when an exception is thrown. @@ -97,17 +98,17 @@ public enum OSSSpeechKitErrorType: Int { switch self { case .noMicrophoneAccess, .invalidAudioEngine: - return OSSSpeechUtility().getString(forLocalizedName:"OSSSpeechKitErrorType_requestTypeNoMicAccess", defaultValue:"Recording") + return OSSSpeechUtility().getString(forLocalizedName: "OSSSpeechKitErrorType_requestTypeNoMicAccess", defaultValue: "Recording") case .invalidUtterance: - return OSSSpeechUtility().getString(forLocalizedName:"OSSSpeechKitErrorType_requestTypeInvalidUtterance", defaultValue:"Speech or Recording") + return OSSSpeechUtility().getString(forLocalizedName: "OSSSpeechKitErrorType_requestTypeInvalidUtterance", defaultValue: "Speech or Recording") case .invalidText, .invalidVoice, .invalidSpeechRequest, .recogniserUnavailble: - return OSSSpeechUtility().getString(forLocalizedName:"OSSSpeechKitErrorType_requestTypeInvalidSpeech", defaultValue:"Speech") + return OSSSpeechUtility().getString(forLocalizedName: "OSSSpeechKitErrorType_requestTypeInvalidSpeech", defaultValue: "Speech") } } - + /// An error that is used to capture details of the error event. public var error: Error? { let err = NSError(domain: "au.com.appdevguy.ossspeechkit", @@ -129,7 +130,7 @@ public enum OSSSpeechRecognitionTaskType: Int { case search = 2 /// Use this for short speechs such as "Yes", "No", "Thanks", etc. case confirmation = 3 - + /// Returns a speech recognition hint based on the enum value. public var taskType: SFSpeechRecognitionTaskHint { switch self { @@ -146,7 +147,7 @@ public enum OSSSpeechRecognitionTaskType: Int { } /// Delegate to handle events such as failed authentication for microphone among many more. -public protocol OSSSpeechDelegate: class { +public protocol OSSSpeechDelegate: AnyObject { /// When the microphone has finished accepting audio, this delegate will be called with the final best text output. func didFinishListening(withText text: String) /// Handle returning authentication status to user - primary use is for non-authorized state. @@ -161,20 +162,20 @@ public protocol OSSSpeechDelegate: class { /// Speech is the primary interface. To use, set the voice and then call `.speak(string: "your string")` public class OSSSpeech: NSObject { - + // MARK: - Private Properties - + /// An object that produces synthesized speech from text utterances and provides controls for monitoring or controlling ongoing speech. private var speechSynthesizer: AVSpeechSynthesizer! - + // MARK: - Variables - + /// Delegate allows for the recieving of spoken events. public weak var delegate: OSSSpeechDelegate? - + /// An object that allows overriding the default AVVoice options. public var voice: OSSVoice? - + /// Speech recognition variable to determine if recognition should use on device capabilities if available /// /// Not all devices support on device speech recognition and only devices operating with iOS 13 or higher support it. @@ -184,17 +185,18 @@ public class OSSSpeech: NSObject { /// /// On device recognition comes at a cost of accurate transcription though; speech recognition is less accurate with on device recognition. public var shouldUseOnDeviceRecognition = false - + /// The task type by default is undefined. /// Changing the task type will change how speech recognition performs. public var recognitionTaskType = OSSSpeechRecognitionTaskType.undefined - + /// The object used to enable translation of strings to synthsized voice. public var utterance: OSSUtterance? - + + #if !os(macOS) /// An AVAudioSession that ensure volume controls are correct in various scenarios private var session: AVAudioSession? - + /// Audio Session can be overriden should you choose to. public var audioSession: AVAudioSession { get { @@ -207,35 +209,36 @@ public class OSSSpeech: NSObject { session = newValue } } - + #endif + /// This property handles permission authorization. /// This property is intentionally named vaguely to prevent accidental overriding. public var srp = SFSpeechRecognizer.self - + // Voice to text private var audioEngine: AVAudioEngine? private var speechRecognizer: SFSpeechRecognizer? private var request: SFSpeechAudioBufferRecognitionRequest? private var recognitionTask: SFSpeechRecognitionTask? private var spokenText: String = "" - + // MARK: - Lifecycle - + private override init() { speechSynthesizer = AVSpeechSynthesizer() } - + private static let sharedInstance: OSSSpeech = { return OSSSpeech() }() - + /// A singleton object to ensure conformity accross the application it is used in. public class var shared: OSSSpeech { return sharedInstance } - + // MARK: - Public Methods - + /// Pass in a string to speak. /// This will set the speechString on the utterance. /// - Parameter text: An String object. @@ -259,7 +262,7 @@ public class OSSSpeech: NSObject { } speak() } - + /// Pass in an attributed string to speak. /// This will set the attributed string on the utterance. /// - Parameter attributedText: An NSAttributedString object. @@ -279,17 +282,44 @@ public class OSSSpeech: NSObject { } speak() } - + + /// Pause speaking text + /// + /// Will check if the current speech synthesizer session is speaking before attempting to pause. + public func pauseSpeaking() { + if !speechSynthesizer.isSpeaking { return } + speechSynthesizer.pauseSpeaking(at: .immediate) + } + + /// Continue speaking text + /// + /// Will check if the current speech synthesizer session is paused before attempting to continue speaking. + public func continueSpeaking() { + if !speechSynthesizer.isPaused { return } + speechSynthesizer.continueSpeaking() + } + + /// Force the ending of speaking. + /// + /// Does not remove or reset the utterance or voice - only stops the current speaking if it's active. + /// Also checks to see if the current synthesizer session is paused. + public func stopSpeaking() { + guard speechSynthesizer.isSpeaking || speechSynthesizer.isPaused else { + return + } + speechSynthesizer.stopSpeaking(at: .immediate) + } + // MARK: - Private Methods private func utteranceIsValid() -> Bool { - guard let _ = utterance else { + guard utterance != nil else { debugLog(object: self, message: "No valid utterance.") return false } return true } - + private func speak() { var speechVoice = OSSVoice() if let aVoice = voice { @@ -306,51 +336,69 @@ public class OSSSpeech: NSObject { } // Ensure volume is correct each time setSession(isRecording: false) + stopSpeaking() speechSynthesizer.speak(newUtterance) } - - private func setSession(isRecording: Bool) { - let category: AVAudioSession.Category = isRecording ? .playAndRecord : .playback - try? audioSession.setCategory(category, options: .duckOthers) - try? audioSession.setActive(true, options: .notifyOthersOnDeactivation) + + @discardableResult private func setSession(isRecording: Bool) -> Bool { + #if !os(macOS) + do { + let category: AVAudioSession.Category = isRecording ? .playAndRecord : .playback + try audioSession.setCategory(category, options: .duckOthers) + try audioSession.setActive(true, options: .notifyOthersOnDeactivation) + return true + } catch { + if isRecording { + delegate?.didFailToCommenceSpeechRecording() + } + delegate?.didFailToProcessRequest(withError: error) + return false + } + #else + return false + #endif } - + // MARK: - Public Voice Recording Methods - + /// Record and recognise speech /// /// This method will check to see if user is authorised to record. If they are, the recording will proceed. /// /// Upon checking the authorisation and being registered successful, a check to determine if a recording session is active will be made and any active session will be cancelled. public func recordVoice(requestMicPermission requested: Bool = true) { + #if !os(macOS) if requested { if audioSession.recordPermission != .granted { self.requestMicPermission() return } } + #endif getMicroPhoneAuthorization() } - + /// End recording of speech session if one exists. public func endVoiceRecording() { cancelRecording() } - + // MARK: - Private Voice Recording - + private func requestMicPermission() { - weak var weakSelf = self - audioSession.requestRecordPermission { allowed in + #if !os(macOS) + audioSession.requestRecordPermission {[weak self] allowed in + guard let self = self else { return } if !allowed { - weakSelf?.debugLog(object: self, message: "Microphone permission was denied.") - weakSelf?.delegate?.authorizationToMicrophone(withAuthentication: .denied) + self.debugLog(object: self, message: "Microphone permission was denied.") + self.delegate?.authorizationToMicrophone(withAuthentication: .denied) return } - weakSelf?.getMicroPhoneAuthorization() + self.getMicroPhoneAuthorization() } + #endif } - + private func getMicroPhoneAuthorization() { weak var weakSelf = self weakSelf?.srp.requestAuthorization { authStatus in @@ -363,43 +411,60 @@ public class OSSSpeech: NSObject { } } } - + + private func resetAudioEngine() { + guard let engine = audioEngine else { + audioEngine = AVAudioEngine() + return + } + if engine.isRunning { + engine.stop() + } + let node = engine.inputNode + node.removeTap(onBus: 0) + if node.inputFormat(forBus: 0).channelCount == 0 { + node.reset() + } + // Clean slate the audio engine. + audioEngine?.reset() + } + private func cancelRecording() { if let voiceRequest = request { voiceRequest.endAudio() request = nil } - if let engine = audioEngine { - if engine.isRunning { - engine.stop() - } - let node = engine.inputNode - node.removeTap(onBus: 0) - audioEngine = nil - } if let task = recognitionTask { task.finish() } + resetAudioEngine() } - - func engineSetup(_ engine: AVAudioEngine) { - let input = engine.inputNode + + func engineSetup() { + if audioEngine == nil { + audioEngine = AVAudioEngine() + } + guard let audioEngine else { + delegate?.didFailToCommenceSpeechRecording() + delegate?.didFailToProcessRequest(withError: OSSSpeechKitErrorType.invalidAudioEngine.error) + return + } + let input = audioEngine.inputNode let bus = 0 - let inputFormat = input.outputFormat(forBus: 0) + let recordingFormat = input.outputFormat(forBus: 0) guard let outputFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 8000, channels: 1, interleaved: true) else { delegate?.didFailToCommenceSpeechRecording() delegate?.didFailToProcessRequest(withError: OSSSpeechKitErrorType.invalidAudioEngine.error) return } - guard let converter = AVAudioConverter(from: inputFormat, to: outputFormat) else { + guard let converter = AVAudioConverter(from: recordingFormat, to: outputFormat) else { delegate?.didFailToCommenceSpeechRecording() delegate?.didFailToProcessRequest(withError: OSSSpeechKitErrorType.invalidAudioEngine.error) return } - weak var weakSelf = self - input.installTap(onBus: bus, bufferSize: 8192, format: inputFormat) { (buffer, time) -> Void in + input.installTap(onBus: bus, bufferSize: 8192, format: recordingFormat) { [weak self] (buffer, _) -> Void in var newBufferAvailable = true - let inputCallback: AVAudioConverterInputBlock = { inNumPackets, outStatus in + let inputCallback: AVAudioConverterInputBlock = { _, outStatus in if newBufferAvailable { outStatus.pointee = .haveData newBufferAvailable = false @@ -413,37 +478,40 @@ public class OSSSpeech: NSObject { var error: NSError? let status = converter.convert(to: convertedBuffer, error: &error, withInputFrom: inputCallback) if status == .error { - weakSelf?.delegate?.didFailToCommenceSpeechRecording() - weakSelf?.delegate?.didFailToProcessRequest(withError: OSSSpeechKitErrorType.invalidAudioEngine.error) + self?.delegate?.didFailToCommenceSpeechRecording() + self?.delegate?.didFailToProcessRequest(withError: OSSSpeechKitErrorType.invalidAudioEngine.error) if let err = error { - weakSelf?.debugLog(object: self, message: "Audio Engine conversion error: \(err)") + self?.debugLog(object: self as Any, message: "Audio Engine conversion error: \(err)") } return } - weakSelf?.request?.append(convertedBuffer) + self?.request?.append(convertedBuffer) } - engine.prepare() + audioEngine.prepare() do { - try engine.start() - return + try audioEngine.start() } catch { delegate?.didFailToCommenceSpeechRecording() delegate?.didFailToProcessRequest(withError: OSSSpeechKitErrorType.invalidAudioEngine.error) - return } } - + private func recordAndRecognizeSpeech() { - if let engine = audioEngine { - if engine.isRunning { - cancelRecording() - } - } else { - audioEngine = AVAudioEngine() + if let speechRecognizer, !speechRecognizer.isAvailable { + cancelRecording() + setSession(isRecording: false) + } + if speechSynthesizer.isSpeaking { + stopSpeaking() + } + // If the audio session is not configured, we must not continue. + // The audio engine will force an uncatchable crash. + // This seemse to ONLY be true in simulator so CI tests often randomly fail. + guard setSession(isRecording: true) else { + return } - setSession(isRecording: true) request = SFSpeechAudioBufferRecognitionRequest() - engineSetup(audioEngine!) + engineSetup() let identifier = voice?.voiceType.rawValue ?? OSSVoiceEnum.UnitedStatesEnglish.rawValue speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: identifier)) guard let recogniser = speechRecognizer else { @@ -457,10 +525,8 @@ public class OSSSpeech: NSObject { return } if let audioRequest = request { - if #available(iOS 13, *) { - if recogniser.supportsOnDeviceRecognition { - audioRequest.requiresOnDeviceRecognition = shouldUseOnDeviceRecognition - } + if recogniser.supportsOnDeviceRecognition { + audioRequest.requiresOnDeviceRecognition = shouldUseOnDeviceRecognition } recogniser.delegate = self recogniser.defaultTaskHint = recognitionTaskType.taskType @@ -474,33 +540,34 @@ public class OSSSpeech: NSObject { /// Extension to handle the SFSpeechRecognitionTaskDelegate and SFSpeechRecognizerDelegate methods. extension OSSSpeech: SFSpeechRecognitionTaskDelegate, SFSpeechRecognizerDelegate { - + // MARK: - SFSpeechRecognitionTaskDelegate Methods - + /// Docs available by Google searching for SFSpeechRecognitionTaskDelegate public func speechRecognitionTask(_ task: SFSpeechRecognitionTask, didFinishSuccessfully successfully: Bool) { recognitionTask = nil delegate?.didFinishListening(withText: spokenText) setSession(isRecording: false) } - + /// Docs available by Google searching for SFSpeechRecognitionTaskDelegate public func speechRecognitionTask(_ task: SFSpeechRecognitionTask, didHypothesizeTranscription transcription: SFTranscription) { delegate?.didCompleteTranslation(withText: transcription.formattedString) } - + /// Docs available by Google searching for SFSpeechRecognitionTaskDelegate public func speechRecognitionTask(_ task: SFSpeechRecognitionTask, didFinishRecognition recognitionResult: SFSpeechRecognitionResult) { spokenText = recognitionResult.bestTranscription.formattedString } - + public func speechRecognitionDidDetectSpeech(_ task: SFSpeechRecognitionTask) {} - + public func speechRecognitionTaskFinishedReadingAudio(_ task: SFSpeechRecognitionTask) {} - + // MARK: - SFSpeechRecognizerDelegate Methods - + /// Docs available by Google searching for SFSpeechRecognizerDelegate public func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) {} } +#endif diff --git a/OSSSpeechKit/Classes/OSSSpeechUtility.swift b/OSSSpeechKit/Classes/OSSSpeechUtility.swift index 37ec1b3..cef56cb 100644 --- a/OSSSpeechKit/Classes/OSSSpeechUtility.swift +++ b/OSSSpeechKit/Classes/OSSSpeechUtility.swift @@ -21,14 +21,14 @@ // IN THE SOFTWARE. // -import UIKit +import Foundation public class OSSSpeechUtility: NSObject { - + // MARK: - Variables - + fileprivate var tableName = "Localizable" - + /// Change this property to your strings table name if you wish to override the SDK strings values in your app. public var stringsTableName: String { get { @@ -38,9 +38,9 @@ public class OSSSpeechUtility: NSObject { tableName = newValue } } - + // MARK: - Public Methods - + /// A helper method that enables all Localized strings to be overridden by the main application. /// /// This method checks the main bundle for a Localized strings file. If one exists, the localized string name will be checked in that file. If one does not exist, the SDK string will be returned. @@ -69,36 +69,40 @@ public class OSSSpeechUtility: NSObject { } return defaultValue } - -} -extension NSObject { - /// Method outputs a debug statement containing necessary information to resolve issues. - /// - /// Only works with debug/dev builds. - /// - /// - Parameters: - /// - object: Any object type - /// - functionName: Automatically populated by the application - /// - fileName: Automatically populated by the application - /// - lineNumber: Automatically populated by the application - /// - message: The message you wish to output. - public func debugLog(object: Any, functionName: String = #function, fileName: String = #file, lineNumber: Int = #line, message: String) { - #if DEBUG - let className = (fileName as NSString).lastPathComponent - print("\n\n******************\tBegin Debug Log\t******************\n\n\tClass: <\(className)>\n\tFunction: \(functionName)\n\tLine: #\(lineNumber)\n\tObject: \(object)\n\tLog Message: \(message)\n\n******************\tEnd Debug Log\t******************\n\n") - #endif - } } /// Bundle extension to aid in retrieving the SDK resources for getting SDK images. extension Bundle { - /// Will return the Bundle for the SDK if it can be found. - static func getResourcesBundle() -> Bundle? { - let bundle = Bundle(for: OSSSpeech.self) - guard let resourcesBundleUrl = bundle.resourceURL?.appendingPathComponent("OSSSpeechKit.bundle") else { - return nil - } - return Bundle(url: resourcesBundleUrl) - } + /// Will return the Bundle for the SDK if it can be found. + static func getResourcesBundle() -> Bundle? { +#if SWIFT_PACKAGE + return Bundle.module +#else + let bundle = Bundle(for: OSSVoice.self) + guard let resourcesBundleUrl = bundle.resourceURL?.appendingPathComponent("OSSSpeechKit.bundle") else { + return nil + } + return Bundle(url: resourcesBundleUrl) +#endif + } +} + +extension NSObject { + /// Method outputs a debug statement containing necessary information to resolve issues. + /// + /// Only works with debug/dev builds. + /// + /// - Parameters: + /// - object: Any object type + /// - functionName: Automatically populated by the application + /// - fileName: Automatically populated by the application + /// - lineNumber: Automatically populated by the application + /// - message: The message you wish to output. + public func debugLog(object: Any, functionName: String = #function, fileName: String = #file, lineNumber: Int = #line, message: String) { + #if DEBUG + let className = (fileName as NSString).lastPathComponent + print("\n\n******************\tBegin Debug Log\t******************\n\n\tClass: <\(className)>\n\tFunction: \(functionName)\n\tLine: #\(lineNumber)\n\tObject: \(object)\n\tLog Message: \(message)\n\n******************\tEnd Debug Log\t******************\n\n") + #endif + } } diff --git a/OSSSpeechKit/Classes/OSSUtterance.swift b/OSSSpeechKit/Classes/OSSUtterance.swift index acb3127..313fd48 100755 --- a/OSSSpeechKit/Classes/OSSUtterance.swift +++ b/OSSSpeechKit/Classes/OSSUtterance.swift @@ -29,11 +29,11 @@ import AVFoundation /// /// As the developer, you can override the `volume`, `rate` and `pitchMultiplier` should you wish to. public class OSSUtterance: AVSpeechUtterance { - + // MARK: - Variables private var stringToSpeak: String = "" private var attributedStringToSpeak: NSAttributedString = NSAttributedString(string: "") - + /// The speechString can be a constant value or changed as frequently as you wish. /// /// The Speech String is what will be read out. @@ -47,7 +47,7 @@ public class OSSUtterance: AVSpeechUtterance { attributedStringToSpeak = NSAttributedString(string: newValue) } } - + /// The attributedSpeechString can be a constant value or changed as frequently as you wish. /// /// The Attributed Speech String is what will be read out if no speechString is set. @@ -61,9 +61,9 @@ public class OSSUtterance: AVSpeechUtterance { attributedStringToSpeak = newValue } } - + // MARK: - Lifecycle - + public override init() { super.init(string: "ERROR") debugLog(object: self, message: "ERROR: You must use the `init(string:)` or `init(attributedString:` methods.") @@ -71,7 +71,7 @@ public class OSSUtterance: AVSpeechUtterance { attributedSpeechString = NSAttributedString(string: "ERROR") commonInit() } - + /// Init method which will set the speechString value. public override init(string: String) { super.init(string: string) @@ -79,7 +79,7 @@ public class OSSUtterance: AVSpeechUtterance { attributedSpeechString = NSAttributedString(string: string) commonInit() } - + /// Init method which will set the attributedSpeechString value. public override init(attributedString: NSAttributedString) { super.init(attributedString: attributedString) @@ -87,21 +87,21 @@ public class OSSUtterance: AVSpeechUtterance { speechString = attributedString.string commonInit() } - + /// Required. Do not recommend using. public required init?(coder aDecoder: NSCoder) { super.init() return nil } - + // MARK: - Private Methods - + /// Common init is used for testing purposes only. private func commonInit() { // Init default values rate = AVSpeechUtteranceDefaultSpeechRate pitchMultiplier = 1.0 volume = 1.0 - voice = AVSpeechSynthesisVoice(identifier: AVSpeechSynthesisVoiceIdentifierAlex) + voice = AVSpeechSynthesisVoice(identifier: AVSpeechSynthesisVoiceIdentifierAlex) } } diff --git a/OSSSpeechKit/Classes/OSSVoice.swift b/OSSSpeechKit/Classes/OSSVoice.swift index 87b0087..7beeb23 100755 --- a/OSSSpeechKit/Classes/OSSVoice.swift +++ b/OSSSpeechKit/Classes/OSSVoice.swift @@ -21,8 +21,13 @@ // IN THE SOFTWARE. // -import UIKit +import Foundation import AVFoundation +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif /// The voice infor struct ensures that the data structure has conformity and consistency. public struct OSSVoiceInfo { @@ -35,7 +40,7 @@ public struct OSSVoiceInfo { /// Identifier is a unique bundle url provided by Apple for each AVSpeechSynthesisVoice. public var identifier: Any? } - +// swiftlint:disable identifier_name /// The available system voices. /// /// The enum is iteratable; access to an array of the enum values can be accessed using: @@ -47,12 +52,18 @@ public enum OSSVoiceEnum: String, CaseIterable { case Australian = "en-AU" /// Brazilian case Brazilian = "pt-BR" + /// Bulgarian + case Bulgarian = "bg-BG" /// CanadianFrench case CanadianFrench = "fr-CA" - /// Chinese + /// Chinese Traditional case Chinese = "zh-CH" + /// Chinese Simplified + case ChineseSimplified = "zh-CN" /// ChineseHongKong case ChineseHongKong = "zh-HK" + /// Croatian + case Croatian = "hr-HR" /// Czech case Czech = "cs-CZ" /// Danish @@ -77,6 +88,8 @@ public enum OSSVoiceEnum: String, CaseIterable { case Hindi = "hi-IN" /// Hungarian case Hungarian = "hu-HU" + /// Indian English + case IndianEnglish = "en-IN" /// Indonesian case Indonesian = "id-ID" /// IrishEnglish @@ -87,10 +100,14 @@ public enum OSSVoiceEnum: String, CaseIterable { case Japanese = "ja-JP" /// Korean case Korean = "ko-KR" + /// Malaysian + case Malay = "ms-MY" /// Mexican case Mexican = "es-MX" /// Norwegian case Norwegian = "no-NO" + /// Norwegian Bokmal + case NorwegianBokmal = "nb-NO" /// Polish case Polish = "pl-PL" /// Portuguese @@ -107,6 +124,8 @@ public enum OSSVoiceEnum: String, CaseIterable { case SouthAfricanEnglish = "en-ZA" /// Spanish case Spanish = "es-ES" + /// Catalan + case SpanishCatalan = "ca-ES" /// Swedish case Swedish = "sv-SE" /// Taiwanese @@ -115,9 +134,15 @@ public enum OSSVoiceEnum: String, CaseIterable { case Thai = "th-TH" /// Turkish case Turkish = "tr-TR" + /// Ukranian + case Ukranian = "uk-UA" /// USA English case UnitedStatesEnglish = "en-US" - + /// Vietnamese + case Vietnamese = "vi-VN" + /// Arabic World + case ArabicWorld = "ar-001" + /// Will return specific information about the language as an OSSVoiceInfo object. public func getDetails() -> OSSVoiceInfo { var voiceInfo: OSSVoiceInfo = OSSVoiceInfo() @@ -131,12 +156,12 @@ public enum OSSVoiceEnum: String, CaseIterable { } return voiceInfo } - + /// Provides the Enum key itself as a String public var title: String { return String(describing: self) } - + /// Demo message is for returning a string in the language that will be read while also providing the name of the voice that Apple have provided. public var demoMessage: String { var voiceName = "" @@ -218,20 +243,49 @@ public enum OSSVoiceEnum: String, CaseIterable { return "你好我的名字是 \(voiceName)" case .Taiwanese: return "你好我的名字是 \(voiceName)" + case .Bulgarian: + return "Здравейте, казвам се \(voiceName)" + case .ChineseSimplified: + return "你好我的名字是 \(voiceName)" + case .Croatian: + return "Zdravo! Moje ime je \(voiceName)" + case .IndianEnglish: + return "Hello, my name is \(voiceName)" + case .Malay: + return "helo! Nama saya \(voiceName)" + case .NorwegianBokmal: + return "Hei, mitt navn er \(voiceName)" + case .SpanishCatalan: + return "Hola! Em dic \(voiceName)" + case .Ukranian: + return "Привіт! Мене звати \(voiceName)" + case .Vietnamese: + return "Xin chào! Tên của tôi là \(voiceName)" + case .ArabicWorld: + return "\(voiceName) مرحبا اسمي" } } - + /// The flag for the selected language. /// /// You can supply your own flag image, provided is has the same name (.rawValue) as the image in the pod assets. /// /// If no image is found in the application bundle, the image from the SDK bundle will be provided. +#if canImport(UIKit) public var flag: UIImage? { if let mainBundleImage = UIImage(named: rawValue, in: Bundle.main, compatibleWith: nil) { return mainBundleImage } return UIImage(named: rawValue, in: Bundle.getResourcesBundle(), compatibleWith: nil) } +#elseif canImport(AppKit) + public var flag: NSImage? { + if let mainBundleImage = NSImage(named: rawValue) { + return mainBundleImage + } + return NSImage(named: rawValue) + } +#endif } /** OSSVoice overides some of the properties provided to enable setting as well as getting. @@ -244,13 +298,13 @@ public enum OSSVoiceEnum: String, CaseIterable { */ @available(iOS 9.0, *) public class OSSVoice: AVSpeechSynthesisVoice { - + // MARK: - Private Properties - + private var voiceQuality: AVSpeechSynthesisVoiceQuality = .default private var voiceLanguage: String = OSSVoiceEnum.UnitedStatesEnglish.rawValue private var voiceTypeValue: OSSVoiceEnum = .UnitedStatesEnglish - + /// You have access to set the voice quality or use the default which is set to .default override public var quality: AVSpeechSynthesisVoiceQuality { get { @@ -260,7 +314,7 @@ public class OSSVoice: AVSpeechSynthesisVoice { voiceQuality = newValue } } - + /// Language offers a get and set. The default value is United States English. override public var language: String { get { @@ -270,17 +324,15 @@ public class OSSVoice: AVSpeechSynthesisVoice { voiceLanguage = newValue if let valueEnum = OSSVoiceEnum(rawValue: newValue) { voiceTypeValue = valueEnum - } + } } } - + /// Returns the current voice type enum to allow for obtining details. public var voiceType: OSSVoiceEnum { - get { - return voiceTypeValue - } + voiceTypeValue } - + /// If this init is used, defaults will be used. /// /// This method will set default values on the language and quality of voice. @@ -292,7 +344,7 @@ public class OSSVoice: AVSpeechSynthesisVoice { super.init() commonInit() } - + /// This init method is required as it sets the voice quality and language in order to speak the text passed in. public init?(quality: AVSpeechSynthesisVoiceQuality, language: OSSVoiceEnum) { super.init() @@ -300,13 +352,13 @@ public class OSSVoice: AVSpeechSynthesisVoice { voiceLanguage = language.rawValue voiceQuality = quality } - + /// Required: Do not recommend using. public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) return nil } - + /// Used as a fail-safe should the custom init method not be used. /// /// This method will set default values on the language and quality of voice. diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..e19ede8 --- /dev/null +++ b/Package.swift @@ -0,0 +1,63 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. +import PackageDescription + +let package = Package( + name: "OSSSpeechKit", + platforms: [ + .iOS(.v12), + .tvOS(.v13), + .macOS(.v11) + ], + products: [ + .library( + name: "OSSSpeechKit", + targets: ["OSSSpeechKit"]), + .library( + name: "OSSSpeechKit-Static", + type: .static, + targets: ["OSSSpeechKit"]), + .library( + name: "OSSSpeechKit-Dynamic", + type: .dynamic, + targets: ["OSSSpeechKit"]) + ], + + // MARK: - Targets + targets: [ + // // MARK: - OSSSpeachKit + .target( + name: "OSSSpeechKit", + path: "OSSSpeechKit/", + sources: [ + "Classes/OSSSpeech.swift", + "Classes/OSSSpeechUtility.swift", + "Classes/OSSUtterance.swift", + "Classes/OSSVoice.swift" + ], + resources: [ + .process("Assets/") + ], + linkerSettings: [ + .linkedFramework("AVFoundation"), + .linkedFramework("AppKit", .when(platforms: [.macOS])), + .linkedFramework("Speech", .when(platforms: [.iOS])), + .linkedFramework("UIKit", .when(platforms: [.iOS])) + ] + ), + + .testTarget( + name: "OSSSpeechKitTests", + dependencies: [ + "OSSSpeechKit" + ], + path: "Example/Tests", + exclude: [ + "Info.plist" + ], + linkerSettings: [ + .linkedFramework("AVKit") + ] + )], + swiftLanguageVersions: [.v5] +) diff --git a/README.md b/README.md index bbda1d8..6e79f8b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - # OSSSpeechKit [![OSSSpeechKit Logo](https://appdevguy.github.io/OSSSpeechKit/OSSSpeechKit-Logo.png)](https://github.com/AppDevGuy/OSSSpeechKit) @@ -10,35 +9,35 @@ [![codecov](https://codecov.io/gh/AppDevGuy/OSSSpeechKit/branch/master/graph/badge.svg)](https://codecov.io/gh/AppDevGuy/OSSSpeechKit) [![docs](https://appdevguy.github.io/OSSSpeechKit/badge.svg)](https://appdevguy.github.io/OSSSpeechKit) -OSSSpeechKit was developed to provide easier accessibility options to apps. +OSSSpeechKit was developed to provide easier accessibility options to apps. -Apple does not make it easy to get the right voice, nor do they provide a simple way of selecting a language or using speech to text. OSSSpeechKit makes the hassle of trying to find the right language go away. +Apple does not make it easy to get the right voice, nor do they provide a simple way of selecting a language or using speech to text. OSSSpeechKit makes the hassle of trying to find the right language go away. # Requirements - Swift 5.0 or higher -- iOS 12.0 or higher +- iOS 13.0 or higher - Cocoapods -- A real device # Supported Languages +The table below shows the original 37 languages first supported. Since v0.3.3, an additional 10 languages have been added. +
English - Australian
🇦🇺
Hebrew
🇮🇱
Japanese
🇯🇵
Romanian
🇷🇴
Swedish
🇸🇪
Norsk
🇳🇴
Portuguese - Brazilian
🇧🇷
Hindi - Indian
🇮🇳
Korean
🇰🇷
Russian
🇷🇺
Chinese - Taiwanese
🇹🇼
Dutch - Belgium
🇧🇪
French - Canadian
🇨🇦
Hungarian
🇭🇺
Spanish - Mexican
🇲🇽
Arabic - Saudi Arabian
🇸🇦
Thai
🇹🇭
French
🇫🇷
Chinese
🇨🇳
Indonesian
🇮🇩
Norwegian
🇳🇴
Slovakian
🇸🇰
Turkish
🇹🇷
Finnish
🇫🇮
Chinese - Hong Kong
🇭🇰
English - Irish
🇮🇪
Polish
🇵🇱
English - South African
🇿🇦
English - United States
🇺🇸
Danish
🇩🇰
Czech
🇨🇿
Italian
🇮🇹
Portuguese
🇵🇹
Spanish
🇪🇸
English
🇬🇧
Dutch
🇳🇱
Greek
🇬🇷
# Features -OSSSpeechKit offers simple **text to speech** and **speech to text** in 37 different languages. +OSSSpeechKit offers simple **text to speech** and **speech to text** in 47 different languages. -OSSSpeechKit is built on top of both the [AVFoundation](https://developer.apple.com/documentation/avfoundation) and [Speech](https://developer.apple.com/documentation/speech) frameworks. +OSSSpeechKit is built on top of both the [AVFoundation](https://developer.apple.com/documentation/avfoundation) and [Speech](https://developer.apple.com/documentation/speech) frameworks. -You can achieve text to speech or speech to text in as little as two lines of code. +You can achieve text to speech or speech to text in as little as two lines of code. -The speech will play over the top of other sounds such as music. +The speech will play over the top of other sounds such as music. # Installation -OSSSpeechKit is available through [CocoaPods](https://cocoapods.org). To install -it, simply add the following line to your Podfile: +OSSSpeechKit is available through [CocoaPods](https://cocoapods.org). To install it, simply add the following line to your Podfile: ```ruby pod 'OSSSpeechKit' @@ -47,6 +46,7 @@ pod 'OSSSpeechKit' # Implementation ## Text to Speech + These methods enable you to pass in a string and hear the text played back using. ### Simple @@ -83,13 +83,13 @@ newVoice.quality = .enhanced speechKit.voice = newVoice // Initialise an utterance let utterance = OSSUtterance(string: "Testing") -// Set the recognition task type +// Set the recognition task type speechKit.recognitionTaskType = .dictation // Set volume utterance.volume = 0.5 // Set rate of speech utterance.rate = 0.5 -// Set the pitch +// Set the pitch utterance.pitchMultiplier = 1.2 // Set speech utterance speechKit.utterance = utterance @@ -97,11 +97,9 @@ speechKit.utterance = utterance speechKit.speakText(text: utterance.speechString) ``` - ## Speech to Text -Currently speech to text is offered in a very simple format. Starting and stopping of recording is handled by the app. - +Currently speech to text is offered in a very simple format. Starting and stopping of recording is handled by the app. ### iOS 13 On-Device Speech to Text support is now available as of 0.3.0 🎉 @@ -109,7 +107,7 @@ SpeechKit implements delegates to handle the recording authorization, output of ```swift speechKit.delegate = self -// Call to start and end recording. +// Call to start and end recording. speechKit.recordVoice() // Call to end recording speechKit.endVoiceRecording() @@ -126,12 +124,15 @@ Without these, you will not be able to access the microphone nor speech recognit ### Delegates Handle returning authentication status to user - primary use is for non-authorized state. + > `func authorizationToMicrophone(withAuthentication type: OSSSpeechKitAuthorizationStatus)` When the microphone has finished accepting audio, this delegate will be called with the final best text output. + > `func didFailToCommenceSpeechRecording()` If the speech recogniser and request fail to set up, this method will be called. + > `func didFinishListening(withText text: String)` For further information you can [check out the Apple documentation directly.](https://developer.apple.com/documentation/speech/sfspeechrecognizer) @@ -172,7 +173,7 @@ OSSVoiceInfo { ### Other Info -The `OSSVoiceEnum` contains other methods, such as a hello message, title variable and subtitle variable so you can use it in a list. +The `OSSVoiceEnum` contains other methods, such as a hello message, title variable and subtitle variable so you can use it in a list. You can also set the speech: @@ -182,34 +183,36 @@ You can also set the speech: As well as using an `NSAttributedString`. -There are plans to implement flags for each country as well as some more features, such as being able to play the voice if the device is on silent. +There are plans to implement flags for each country as well as some more features, such as being able to play the voice if the device is on silent. If the language or voice you require is not available, this is either due to: -- Apple have not made it available through their AVFoundation; +- Apple have not made it available through their AVFoundation; - or the SDK has not been updated to include the newly added voice. # Important Information -Apple do not make the voice of Siri available for use. +Apple do not make the voice of Siri available for use. This kit provides Apple's AVFoundation voices available and easy to use, so you do not need to know all the voice codes, among many other things. -To say things correctly in each language, you need to set the voice to the correct language and supply that languages text; this SDK is not a translator. +To say things correctly in each language, you need to set the voice to the correct language and supply that languages text; this SDK is not a translator. ### Code Example: -You wish for you app to use a Chinese voice, you will need to ensure the text being passed in is Chinese. +You wish for you app to use a Chinese voice, you will need to ensure the text being passed in is Chinese. _Disclaimer: I do not know how to speak Chinese, I have used Google translate for the Chinese characters._ #### Correct: + ```swift speechKit.voice = OSSVoice(quality: .enhanced, language: .Chinese) speechKit.speakText(text: "你好我的名字是 ...") ``` #### Incorrect: + ```swift speechKit.voice = OSSVoice(quality: .enhanced, language: .Australian) speechKit.speakText(text: "你好我的名字是 ...") @@ -222,13 +225,13 @@ speechKit.voice = OSSVoice(quality: .enhanced, language: .Chinese) speechKit.speakText(text: "Hello, my name is ...") ``` -This same principle applies to all other languages such as German, Saudi Arabian, French, etc.. Failing to set the language for the text you wish to be spoken will not sound correct. +This same principle applies to all other languages such as German, Saudi Arabian, French, etc.. Failing to set the language for the text you wish to be spoken will not sound correct. # Contributions and Queries -If you have a question, please create a ticket or email me directly. +If you have a question, please create a ticket or email me directly. -If you wish to contribute, please create a pull request. +If you wish to contribute, please create a pull request. # Example Project