@gmessier/nitro-speech 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +165 -148
- package/android/build.gradle +0 -1
- package/android/src/main/cpp/cpp-adapter.cpp +5 -1
- package/android/src/main/java/com/margelo/nitro/nitrospeech/HybridNitroSpeech.kt +2 -0
- package/android/src/main/java/com/margelo/nitro/nitrospeech/recognizer/AutoStopper.kt +80 -16
- package/android/src/main/java/com/margelo/nitro/nitrospeech/recognizer/HybridRecognizer.kt +93 -20
- package/android/src/main/java/com/margelo/nitro/nitrospeech/recognizer/RecognitionListenerSession.kt +27 -15
- package/ios/{BufferUtil.swift → Audio/AudioBufferConverter.swift} +3 -34
- package/ios/Audio/AudioLevelTracker.swift +66 -0
- package/ios/Coordinator.swift +105 -0
- package/ios/Engines/AnalyzerEngine.swift +241 -0
- package/ios/Engines/DictationRuntime.swift +67 -0
- package/ios/Engines/RecognizerEngine.swift +312 -0
- package/ios/Engines/SFSpeechEngine.swift +119 -0
- package/ios/Engines/SpeechRuntime.swift +58 -0
- package/ios/Engines/TranscriberRuntimeProtocol.swift +21 -0
- package/ios/HybridNitroSpeech.swift +1 -10
- package/ios/HybridRecognizer.swift +135 -192
- package/ios/LocaleManager.swift +73 -0
- package/ios/{AppStateObserver.swift → Shared/AppStateObserver.swift} +1 -2
- package/ios/Shared/AutoStopper.swift +147 -0
- package/ios/Shared/HapticImpact.swift +24 -0
- package/ios/Shared/Log.swift +41 -0
- package/ios/Shared/Permissions.swift +59 -0
- package/ios/Shared/Utils.swift +58 -0
- package/lib/NitroSpeech.d.ts +2 -0
- package/lib/NitroSpeech.js +2 -0
- package/lib/Recognizer/RecognizerRef.d.ts +5 -0
- package/lib/Recognizer/RecognizerRef.js +13 -0
- package/lib/Recognizer/SpeechRecognizer.d.ts +8 -0
- package/lib/Recognizer/SpeechRecognizer.js +9 -0
- package/lib/Recognizer/methods.d.ts +8 -0
- package/lib/Recognizer/methods.js +29 -0
- package/lib/Recognizer/types.d.ts +6 -0
- package/lib/Recognizer/types.js +1 -0
- package/lib/Recognizer/useRecognizer.d.ts +16 -0
- package/lib/Recognizer/useRecognizer.js +71 -0
- package/lib/Recognizer/useVoiceInputVolume.d.ts +25 -0
- package/lib/Recognizer/useVoiceInputVolume.js +52 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/specs/NitroSpeech.nitro.d.ts +8 -0
- package/lib/specs/NitroSpeech.nitro.js +1 -0
- package/lib/specs/Recognizer.nitro.d.ts +95 -0
- package/lib/specs/Recognizer.nitro.js +1 -0
- package/lib/specs/SpeechRecognitionConfig.d.ts +162 -0
- package/lib/specs/SpeechRecognitionConfig.js +1 -0
- package/lib/specs/VolumeChangeEvent.d.ts +31 -0
- package/lib/specs/VolumeChangeEvent.js +1 -0
- package/nitro.json +2 -6
- package/nitrogen/generated/android/NitroSpeech+autolinking.cmake +2 -2
- package/nitrogen/generated/android/NitroSpeechOnLoad.cpp +5 -3
- package/nitrogen/generated/android/c++/JFunc_void_VolumeChangeEvent.hpp +78 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_std__string_.hpp +14 -14
- package/nitrogen/generated/android/c++/JHybridRecognizerSpec.cpp +68 -19
- package/nitrogen/generated/android/c++/JHybridRecognizerSpec.hpp +7 -4
- package/nitrogen/generated/android/c++/JIosPreset.hpp +58 -0
- package/nitrogen/generated/android/c++/JMutableSpeechRecognitionConfig.hpp +79 -0
- package/nitrogen/generated/android/c++/{JSpeechToTextParams.hpp → JSpeechRecognitionConfig.hpp} +48 -30
- package/nitrogen/generated/android/c++/JVolumeChangeEvent.hpp +65 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/Func_void_VolumeChangeEvent.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/HybridRecognizerSpec.kt +18 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/IosPreset.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/MutableSpeechRecognitionConfig.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/SpeechRecognitionConfig.kt +121 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/VolumeChangeEvent.kt +61 -0
- package/nitrogen/generated/ios/NitroSpeech-Swift-Cxx-Bridge.cpp +46 -30
- package/nitrogen/generated/ios/NitroSpeech-Swift-Cxx-Bridge.hpp +203 -70
- package/nitrogen/generated/ios/NitroSpeech-Swift-Cxx-Umbrella.hpp +13 -3
- package/nitrogen/generated/ios/NitroSpeechAutolinking.swift +2 -2
- package/nitrogen/generated/ios/c++/HybridRecognizerSpecSwift.hpp +41 -9
- package/nitrogen/generated/ios/swift/Func_void_VolumeChangeEvent.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridRecognizerSpec.swift +6 -3
- package/nitrogen/generated/ios/swift/HybridRecognizerSpec_cxx.swift +66 -18
- package/nitrogen/generated/ios/swift/IosPreset.swift +40 -0
- package/nitrogen/generated/ios/swift/MutableSpeechRecognitionConfig.swift +118 -0
- package/nitrogen/generated/ios/swift/{SpeechToTextParams.swift → SpeechRecognitionConfig.swift} +108 -43
- package/nitrogen/generated/ios/swift/VolumeChangeEvent.swift +52 -0
- package/nitrogen/generated/shared/c++/HybridRecognizerSpec.cpp +4 -1
- package/nitrogen/generated/shared/c++/HybridRecognizerSpec.hpp +17 -7
- package/nitrogen/generated/shared/c++/IosPreset.hpp +76 -0
- package/nitrogen/generated/shared/c++/MutableSpeechRecognitionConfig.hpp +105 -0
- package/nitrogen/generated/shared/c++/{SpeechToTextParams.hpp → SpeechRecognitionConfig.hpp} +39 -20
- package/nitrogen/generated/shared/c++/VolumeChangeEvent.hpp +91 -0
- package/package.json +15 -16
- package/src/NitroSpeech.ts +5 -0
- package/src/Recognizer/RecognizerRef.ts +23 -0
- package/src/Recognizer/SpeechRecognizer.ts +10 -0
- package/src/Recognizer/methods.ts +40 -0
- package/src/Recognizer/types.ts +33 -0
- package/src/Recognizer/useRecognizer.ts +85 -0
- package/src/Recognizer/useVoiceInputVolume.ts +65 -0
- package/src/index.ts +6 -182
- package/src/specs/NitroSpeech.nitro.ts +2 -163
- package/src/specs/Recognizer.nitro.ts +110 -0
- package/src/specs/SpeechRecognitionConfig.ts +167 -0
- package/src/specs/VolumeChangeEvent.ts +31 -0
- package/android/proguard-rules.pro +0 -1
- package/ios/AnylyzerTranscriber.swift +0 -331
- package/ios/AutoStopper.swift +0 -69
- package/ios/HapticImpact.swift +0 -32
- package/ios/LegacySpeechRecognizer.swift +0 -161
- package/lib/commonjs/index.js +0 -145
- package/lib/commonjs/index.js.map +0 -1
- package/lib/commonjs/package.json +0 -1
- package/lib/commonjs/specs/NitroSpeech.nitro.js +0 -6
- package/lib/commonjs/specs/NitroSpeech.nitro.js.map +0 -1
- package/lib/module/index.js +0 -138
- package/lib/module/index.js.map +0 -1
- package/lib/module/package.json +0 -1
- package/lib/module/specs/NitroSpeech.nitro.js +0 -4
- package/lib/module/specs/NitroSpeech.nitro.js.map +0 -1
- package/lib/tsconfig.tsbuildinfo +0 -1
- package/lib/typescript/index.d.ts +0 -50
- package/lib/typescript/index.d.ts.map +0 -1
- package/lib/typescript/specs/NitroSpeech.nitro.d.ts +0 -162
- package/lib/typescript/specs/NitroSpeech.nitro.d.ts.map +0 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/SpeechToTextParams.kt +0 -68
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
final class AutoStopper {
|
|
4
|
+
private static let defaultSilenceThresholdMs = 8000.0
|
|
5
|
+
private static let defaultProgressIntervalMs = 1000.0
|
|
6
|
+
private static let minProgressIntervalMs = 50.0
|
|
7
|
+
|
|
8
|
+
private let lg = Lg(prefix: "AutoStopper", disable: true)
|
|
9
|
+
|
|
10
|
+
private let queue = DispatchQueue(label: "com.margelo.nitrospeech.autostopper")
|
|
11
|
+
|
|
12
|
+
private var silenceThresholdMs: Double
|
|
13
|
+
private var progressIntervalMs: Double
|
|
14
|
+
private var timeLeftMs: Double
|
|
15
|
+
private var isStopped = false
|
|
16
|
+
private var didTimeout = false
|
|
17
|
+
private var timer: DispatchSourceTimer?
|
|
18
|
+
|
|
19
|
+
private let onProgress: (Double) -> Void
|
|
20
|
+
private let onTimeout: () -> Void
|
|
21
|
+
|
|
22
|
+
init(
|
|
23
|
+
silenceThresholdMs: Double?,
|
|
24
|
+
progressIntervalMs: Double?,
|
|
25
|
+
onProgress: @escaping (Double) -> Void,
|
|
26
|
+
onTimeout: @escaping () -> Void
|
|
27
|
+
) {
|
|
28
|
+
let threshold = Self.clampMs(silenceThresholdMs ?? Self.defaultSilenceThresholdMs)
|
|
29
|
+
let interval = Self.clampMs(progressIntervalMs ?? Self.defaultProgressIntervalMs)
|
|
30
|
+
self.silenceThresholdMs = threshold
|
|
31
|
+
self.progressIntervalMs = interval
|
|
32
|
+
self.timeLeftMs = threshold
|
|
33
|
+
self.onProgress = onProgress
|
|
34
|
+
self.onTimeout = onTimeout
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
deinit {
|
|
38
|
+
queue.sync {
|
|
39
|
+
stopLocked()
|
|
40
|
+
timeLeftMs = 0
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func resetTimer(from: String) {
|
|
45
|
+
queue.async { [weak self] in
|
|
46
|
+
guard let self, !self.isStopped else { return }
|
|
47
|
+
lg.log("[resetTimer] from:\(from)")
|
|
48
|
+
self.didTimeout = false
|
|
49
|
+
self.timeLeftMs = self.silenceThresholdMs
|
|
50
|
+
self.startOrRescheduleTimerLocked()
|
|
51
|
+
if self.timeLeftMs > 0 {
|
|
52
|
+
self.onProgress(self.timeLeftMs)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func updateThreshold(_ newThresholdMs: Double, from: String) {
|
|
58
|
+
queue.async { [weak self] in
|
|
59
|
+
guard let self, !self.isStopped else { return }
|
|
60
|
+
lg.log("[updateThreshold] from:\(from) newThresholdMs:\(newThresholdMs)")
|
|
61
|
+
self.silenceThresholdMs = Self.clampMs(newThresholdMs)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func addMsOnce(_ extraMs: Double, from: String) {
|
|
66
|
+
queue.async { [weak self] in
|
|
67
|
+
guard let self, !self.isStopped, extraMs.isFinite else { return }
|
|
68
|
+
lg.log("[addMsOnce] from:\(from) extraMs:\(extraMs)")
|
|
69
|
+
self.timeLeftMs += extraMs
|
|
70
|
+
self.didTimeout = false
|
|
71
|
+
if self.timeLeftMs > 0, self.timer != nil {
|
|
72
|
+
self.onProgress(self.timeLeftMs)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
func updateProgressInterval(_ newIntervalMs: Double, from: String) {
|
|
78
|
+
queue.async { [weak self] in
|
|
79
|
+
guard let self, !self.isStopped else { return }
|
|
80
|
+
lg.log("[updateProgressInterval] from:\(from) newIntervalMs:\(newIntervalMs)")
|
|
81
|
+
self.progressIntervalMs = Self.clampMs(newIntervalMs)
|
|
82
|
+
if self.timer != nil {
|
|
83
|
+
self.startOrRescheduleTimerLocked()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func stop() {
|
|
89
|
+
queue.async { [weak self] in
|
|
90
|
+
guard let self else { return }
|
|
91
|
+
self.stopLocked()
|
|
92
|
+
self.timeLeftMs = 0
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private func startOrRescheduleTimerLocked() {
|
|
97
|
+
timer?.cancel()
|
|
98
|
+
timer = nil
|
|
99
|
+
|
|
100
|
+
let source = DispatchSource.makeTimerSource(queue: queue)
|
|
101
|
+
let intervalNs = UInt64(progressIntervalMs * 1_000_000)
|
|
102
|
+
source.schedule(
|
|
103
|
+
deadline: .now() + .nanoseconds(Int(intervalNs)),
|
|
104
|
+
repeating: .nanoseconds(Int(intervalNs))
|
|
105
|
+
)
|
|
106
|
+
source.setEventHandler { [weak self] in
|
|
107
|
+
self?.tickLocked()
|
|
108
|
+
}
|
|
109
|
+
timer = source
|
|
110
|
+
source.resume()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private func tickLocked() {
|
|
114
|
+
guard !isStopped else { return }
|
|
115
|
+
guard !didTimeout else { return }
|
|
116
|
+
|
|
117
|
+
timeLeftMs -= progressIntervalMs
|
|
118
|
+
if timeLeftMs > 0 {
|
|
119
|
+
lg.log("[onProgress] timeLeftMs:\(timeLeftMs)")
|
|
120
|
+
onProgress(timeLeftMs)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
timeLeftMs = 0
|
|
125
|
+
didTimeout = true
|
|
126
|
+
cancelTimerLocked()
|
|
127
|
+
lg.log("[onTimeout]")
|
|
128
|
+
onTimeout()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private func stopLocked() {
|
|
132
|
+
isStopped = true
|
|
133
|
+
cancelTimerLocked()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private func cancelTimerLocked() {
|
|
137
|
+
timer?.cancel()
|
|
138
|
+
timer = nil
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private static func clampMs(_ value: Double) -> Double {
|
|
142
|
+
if !value.isFinite {
|
|
143
|
+
return minProgressIntervalMs
|
|
144
|
+
}
|
|
145
|
+
return max(minProgressIntervalMs, value)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
enum HapticImpact {
|
|
5
|
+
static func trigger(with: HapticFeedbackStyle?) {
|
|
6
|
+
// Default behavior - medium
|
|
7
|
+
let style = with ?? HapticFeedbackStyle.medium
|
|
8
|
+
let hapticStyle: UIImpactFeedbackGenerator.FeedbackStyle? = switch style {
|
|
9
|
+
case .light:
|
|
10
|
+
UIImpactFeedbackGenerator.FeedbackStyle.light
|
|
11
|
+
case .medium:
|
|
12
|
+
UIImpactFeedbackGenerator.FeedbackStyle.medium
|
|
13
|
+
case .heavy:
|
|
14
|
+
UIImpactFeedbackGenerator.FeedbackStyle.heavy
|
|
15
|
+
case .none:
|
|
16
|
+
nil
|
|
17
|
+
}
|
|
18
|
+
if let hapticStyle {
|
|
19
|
+
let impactGenerator = UIImpactFeedbackGenerator(style: hapticStyle)
|
|
20
|
+
impactGenerator.prepare()
|
|
21
|
+
impactGenerator.impactOccurred()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import os.log
|
|
3
|
+
|
|
4
|
+
enum Log {
|
|
5
|
+
static let isLogging = false
|
|
6
|
+
static let subsystem = "com.margelo.nitro.nitrospeech"
|
|
7
|
+
static let category = "NitroSpeech"
|
|
8
|
+
static func log(_ text: String) {
|
|
9
|
+
if isLogging {
|
|
10
|
+
let tn = Thread.current.isMainThread ? "main" : "bg"
|
|
11
|
+
Logger(subsystem: subsystem, category: category).info(
|
|
12
|
+
"[thread]: \(tn) | \(text)"
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
final class Lg {
|
|
19
|
+
private let disable: Bool
|
|
20
|
+
static let isLogging = false
|
|
21
|
+
static let subsystem = "com.margelo.nitro.nitrospeech"
|
|
22
|
+
static let category = "NitroSpeech"
|
|
23
|
+
let prefix: String
|
|
24
|
+
init(prefix: String, disable: Bool? = false) {
|
|
25
|
+
self.prefix = prefix
|
|
26
|
+
self.disable = disable ?? false
|
|
27
|
+
}
|
|
28
|
+
var prevMs: Double?
|
|
29
|
+
func log(_ text: String) {
|
|
30
|
+
if Self.isLogging && !self.disable {
|
|
31
|
+
let nowMs = ProcessInfo.processInfo.systemUptime * 1000
|
|
32
|
+
let diff = Int(round(nowMs - (self.prevMs ?? nowMs)))
|
|
33
|
+
self.prevMs = nowMs
|
|
34
|
+
let tn = Thread.current.isMainThread ? "main" : "bg"
|
|
35
|
+
Logger(
|
|
36
|
+
subsystem: Self.subsystem,
|
|
37
|
+
category: Self.category
|
|
38
|
+
).info("[thread]: \(tn) | {\(self.prefix)} diff: \(diff) | \(text)")
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Speech
|
|
3
|
+
import AVFoundation
|
|
4
|
+
|
|
5
|
+
final class Permissions {
|
|
6
|
+
private let onGranted: () async -> Void
|
|
7
|
+
private let onDenied: (() -> Void)?
|
|
8
|
+
private let onError: ((String) -> Void)?
|
|
9
|
+
|
|
10
|
+
init(
|
|
11
|
+
onGranted: @escaping () async -> Void,
|
|
12
|
+
onDenied: (() -> Void)?,
|
|
13
|
+
onError: ((String) -> Void)?
|
|
14
|
+
) {
|
|
15
|
+
self.onGranted = onGranted
|
|
16
|
+
self.onDenied = onDenied
|
|
17
|
+
self.onError = onError
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private func requestMicrophonePermission() async {
|
|
21
|
+
// Request permission to record.
|
|
22
|
+
if #available(iOS 17.0, *) {
|
|
23
|
+
if await AVAudioApplication.requestRecordPermission() {
|
|
24
|
+
await self.onGranted()
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
self.onDenied?()
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
AVAudioSession.sharedInstance().requestRecordPermission { [weak self] granted in
|
|
31
|
+
Task { @MainActor in
|
|
32
|
+
guard let self else { return }
|
|
33
|
+
if granted {
|
|
34
|
+
await self.onGranted()
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
self.onDenied?()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func requestAuthorization() {
|
|
43
|
+
SFSpeechRecognizer.requestAuthorization { authStatus in
|
|
44
|
+
Task { @MainActor in
|
|
45
|
+
switch authStatus {
|
|
46
|
+
case .authorized:
|
|
47
|
+
await self.requestMicrophonePermission()
|
|
48
|
+
case .denied, .restricted:
|
|
49
|
+
self.onDenied?()
|
|
50
|
+
case .notDetermined:
|
|
51
|
+
self.onError?("Speech recognition not determined")
|
|
52
|
+
@unknown default:
|
|
53
|
+
self.onError?("Unknown authorization status")
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum Utils {
|
|
4
|
+
static func repeatingFilter(_ text: String) -> String {
|
|
5
|
+
var subStrings = text.split { $0.isWhitespace }.map { String($0) }
|
|
6
|
+
if #available(iOS 16.0, *) {
|
|
7
|
+
var shift = 0
|
|
8
|
+
while shift < subStrings.count, !subStrings[shift].contains(/\w+/) {
|
|
9
|
+
shift += 1
|
|
10
|
+
}
|
|
11
|
+
if shift > 0, shift < subStrings.count {
|
|
12
|
+
subStrings = Array(subStrings.suffix(subStrings.count - shift))
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
guard !subStrings.isEmpty else { return text }
|
|
16
|
+
|
|
17
|
+
var joiner = ""
|
|
18
|
+
if subStrings.count >= 10 {
|
|
19
|
+
joiner = subStrings.prefix(subStrings.count - 9).joined(separator: " ")
|
|
20
|
+
subStrings = Array(subStrings.suffix(10))
|
|
21
|
+
} else {
|
|
22
|
+
joiner = subStrings.first ?? ""
|
|
23
|
+
}
|
|
24
|
+
for i in subStrings.indices {
|
|
25
|
+
if i == 0 { continue }
|
|
26
|
+
if #available(iOS 16.0, *), subStrings[i].contains(/\d+/) {
|
|
27
|
+
joiner += " \(subStrings[i])"
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
if subStrings[i] == subStrings[i - 1] { continue }
|
|
31
|
+
joiner += " \(subStrings[i])"
|
|
32
|
+
}
|
|
33
|
+
return joiner
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// hash only params that affect transcriber preference
|
|
37
|
+
static func hashParams(_ params: SpeechRecognitionConfig?) -> String {
|
|
38
|
+
guard let params else { return "n" }
|
|
39
|
+
let locale = params.locale ?? "en-US"
|
|
40
|
+
let addPunctuation = switch params.iosAddPunctuation {
|
|
41
|
+
case nil: "n"
|
|
42
|
+
case false: "f"
|
|
43
|
+
case true: "t"
|
|
44
|
+
}
|
|
45
|
+
let preset = switch params.iosPreset {
|
|
46
|
+
case nil: "n"
|
|
47
|
+
case .shortform: "s"
|
|
48
|
+
case .general: "g"
|
|
49
|
+
}
|
|
50
|
+
let atypicalSpeech = switch params.iosAtypicalSpeech {
|
|
51
|
+
case nil: "n"
|
|
52
|
+
case false: "f"
|
|
53
|
+
case true: "t"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return [locale, addPunctuation, preset, atypicalSpeech].joined(separator: "|")
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { recognizerAddAutoFinishTime, recognizerGetSupportedLocalesIOS, recognizerGetIsActive, recognizerResetAutoFinishTime, recognizerStartListening, recognizerStopListening, recognizerUpdateConfig, } from './methods';
|
|
2
|
+
/**
|
|
3
|
+
* Safe cross-component reference to the Speech Recognizer methods.
|
|
4
|
+
*/
|
|
5
|
+
export const RecognizerRef = {
|
|
6
|
+
startListening: recognizerStartListening,
|
|
7
|
+
stopListening: recognizerStopListening,
|
|
8
|
+
resetAutoFinishTime: recognizerResetAutoFinishTime,
|
|
9
|
+
addAutoFinishTime: recognizerAddAutoFinishTime,
|
|
10
|
+
updateConfig: recognizerUpdateConfig,
|
|
11
|
+
getIsActive: recognizerGetIsActive,
|
|
12
|
+
getSupportedLocalesIOS: recognizerGetSupportedLocalesIOS,
|
|
13
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NitroSpeech } from '../NitroSpeech';
|
|
2
|
+
/**
|
|
3
|
+
* Static Speech Recognizer instance.
|
|
4
|
+
*
|
|
5
|
+
* Direct access to the all Speech Recognizer methods and callbacks.
|
|
6
|
+
*
|
|
7
|
+
* @note unsafe, might lead to race conditions
|
|
8
|
+
*/
|
|
9
|
+
export const SpeechRecognizer = NitroSpeech.recognizer;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SpeechRecognitionConfig } from './types';
|
|
2
|
+
export declare const recognizerStartListening: (params: SpeechRecognitionConfig) => void;
|
|
3
|
+
export declare const recognizerStopListening: () => void;
|
|
4
|
+
export declare const recognizerResetAutoFinishTime: () => void;
|
|
5
|
+
export declare const recognizerAddAutoFinishTime: (additionalTimeMs?: number) => void;
|
|
6
|
+
export declare const recognizerUpdateConfig: (newConfig: SpeechRecognitionConfig, resetAutoFinishTime?: boolean) => void;
|
|
7
|
+
export declare const recognizerGetIsActive: () => boolean;
|
|
8
|
+
export declare const recognizerGetSupportedLocalesIOS: () => string[];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SpeechRecognizer } from './SpeechRecognizer';
|
|
2
|
+
export const recognizerStartListening = (params) => {
|
|
3
|
+
'worklet';
|
|
4
|
+
SpeechRecognizer.startListening(params);
|
|
5
|
+
};
|
|
6
|
+
export const recognizerStopListening = () => {
|
|
7
|
+
'worklet';
|
|
8
|
+
SpeechRecognizer.stopListening();
|
|
9
|
+
};
|
|
10
|
+
export const recognizerResetAutoFinishTime = () => {
|
|
11
|
+
'worklet';
|
|
12
|
+
SpeechRecognizer.resetAutoFinishTime();
|
|
13
|
+
};
|
|
14
|
+
export const recognizerAddAutoFinishTime = (additionalTimeMs) => {
|
|
15
|
+
'worklet';
|
|
16
|
+
SpeechRecognizer.addAutoFinishTime(additionalTimeMs);
|
|
17
|
+
};
|
|
18
|
+
export const recognizerUpdateConfig = (newConfig, resetAutoFinishTime) => {
|
|
19
|
+
'worklet';
|
|
20
|
+
SpeechRecognizer.updateConfig(newConfig, resetAutoFinishTime);
|
|
21
|
+
};
|
|
22
|
+
export const recognizerGetIsActive = () => {
|
|
23
|
+
'worklet';
|
|
24
|
+
return SpeechRecognizer.getIsActive();
|
|
25
|
+
};
|
|
26
|
+
export const recognizerGetSupportedLocalesIOS = () => {
|
|
27
|
+
'worklet';
|
|
28
|
+
return SpeechRecognizer.getSupportedLocalesIOS().sort();
|
|
29
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Recognizer as RecognizerSpec } from '../specs/Recognizer.nitro';
|
|
2
|
+
import type { SpeechRecognitionConfig } from '../specs/SpeechRecognitionConfig';
|
|
3
|
+
import type { VolumeChangeEvent } from '../specs/VolumeChangeEvent';
|
|
4
|
+
type RecognizerCallbacks = Pick<RecognizerSpec, 'onReadyForSpeech' | 'onRecordingStopped' | 'onResult' | 'onAutoFinishProgress' | 'onError' | 'onPermissionDenied' | 'onVolumeChange'>;
|
|
5
|
+
type RecognizerMethods = Pick<RecognizerSpec, 'startListening' | 'stopListening' | 'resetAutoFinishTime' | 'addAutoFinishTime' | 'updateConfig' | 'getIsActive' | 'getSupportedLocalesIOS'>;
|
|
6
|
+
export type { RecognizerSpec, SpeechRecognitionConfig, VolumeChangeEvent, RecognizerCallbacks, RecognizerMethods, };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type DependencyList } from 'react';
|
|
2
|
+
import type { RecognizerCallbacks, RecognizerMethods } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Safe, lifecycle-aware hook to use the recognizer.
|
|
5
|
+
*
|
|
6
|
+
* @param callbacks - The callbacks to use for the recognizer.
|
|
7
|
+
* @param destroyDeps - The additional dependencies to use for the cleanup effect.
|
|
8
|
+
*
|
|
9
|
+
* Example: To cleanup when the screen is unfocused.
|
|
10
|
+
*
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const isFocused = useIsFocused()
|
|
13
|
+
* useRecognizer({ ... }, [isFocused])
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare const useRecognizer: (callbacks: RecognizerCallbacks, destroyDeps?: DependencyList) => RecognizerMethods;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { recognizerResetAutoFinishTime, recognizerAddAutoFinishTime, recognizerUpdateConfig, recognizerGetIsActive, recognizerGetSupportedLocalesIOS, recognizerStartListening, recognizerStopListening, } from './methods';
|
|
3
|
+
import { SpeechRecognizer } from './SpeechRecognizer';
|
|
4
|
+
import { speechRecognizerVolumeChangeHandler } from './useVoiceInputVolume';
|
|
5
|
+
/**
|
|
6
|
+
* Safe, lifecycle-aware hook to use the recognizer.
|
|
7
|
+
*
|
|
8
|
+
* @param callbacks - The callbacks to use for the recognizer.
|
|
9
|
+
* @param destroyDeps - The additional dependencies to use for the cleanup effect.
|
|
10
|
+
*
|
|
11
|
+
* Example: To cleanup when the screen is unfocused.
|
|
12
|
+
*
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const isFocused = useIsFocused()
|
|
15
|
+
* useRecognizer({ ... }, [isFocused])
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export const useRecognizer = (callbacks, destroyDeps = []) => {
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (callbacks.onVolumeChange) {
|
|
21
|
+
SpeechRecognizer.onVolumeChange = (event) => {
|
|
22
|
+
callbacks.onVolumeChange?.(event);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
SpeechRecognizer.onVolumeChange = speechRecognizerVolumeChangeHandler;
|
|
27
|
+
}
|
|
28
|
+
SpeechRecognizer.onReadyForSpeech = () => {
|
|
29
|
+
callbacks.onReadyForSpeech?.();
|
|
30
|
+
};
|
|
31
|
+
SpeechRecognizer.onRecordingStopped = () => {
|
|
32
|
+
callbacks.onRecordingStopped?.();
|
|
33
|
+
};
|
|
34
|
+
SpeechRecognizer.onResult = (resultBatches) => {
|
|
35
|
+
callbacks.onResult?.(resultBatches);
|
|
36
|
+
};
|
|
37
|
+
SpeechRecognizer.onAutoFinishProgress = (timeLeftMs) => {
|
|
38
|
+
callbacks.onAutoFinishProgress?.(timeLeftMs);
|
|
39
|
+
};
|
|
40
|
+
SpeechRecognizer.onError = (message) => {
|
|
41
|
+
callbacks.onError?.(message);
|
|
42
|
+
};
|
|
43
|
+
SpeechRecognizer.onPermissionDenied = () => {
|
|
44
|
+
callbacks.onPermissionDenied?.();
|
|
45
|
+
};
|
|
46
|
+
return () => {
|
|
47
|
+
SpeechRecognizer.onReadyForSpeech = undefined;
|
|
48
|
+
SpeechRecognizer.onRecordingStopped = undefined;
|
|
49
|
+
SpeechRecognizer.onResult = undefined;
|
|
50
|
+
SpeechRecognizer.onAutoFinishProgress = undefined;
|
|
51
|
+
SpeechRecognizer.onError = undefined;
|
|
52
|
+
SpeechRecognizer.onPermissionDenied = undefined;
|
|
53
|
+
SpeechRecognizer.onVolumeChange = undefined;
|
|
54
|
+
};
|
|
55
|
+
}, [callbacks]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
return () => {
|
|
58
|
+
SpeechRecognizer.stopListening();
|
|
59
|
+
};
|
|
60
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
61
|
+
}, [...destroyDeps]);
|
|
62
|
+
return {
|
|
63
|
+
startListening: recognizerStartListening,
|
|
64
|
+
stopListening: recognizerStopListening,
|
|
65
|
+
resetAutoFinishTime: recognizerResetAutoFinishTime,
|
|
66
|
+
addAutoFinishTime: recognizerAddAutoFinishTime,
|
|
67
|
+
updateConfig: recognizerUpdateConfig,
|
|
68
|
+
getIsActive: recognizerGetIsActive,
|
|
69
|
+
getSupportedLocalesIOS: recognizerGetSupportedLocalesIOS,
|
|
70
|
+
};
|
|
71
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RecognizerSpec, VolumeChangeEvent } from './types';
|
|
2
|
+
type OnVolumeChange = RecognizerSpec['onVolumeChange'];
|
|
3
|
+
/**
|
|
4
|
+
* Subscription to the voice input volume changes
|
|
5
|
+
*
|
|
6
|
+
* Updates with arbitrary frequency (many times per second) while audio recording is active.
|
|
7
|
+
*
|
|
8
|
+
* @returns The current voice input volume normalized to a range of 0 to 1.
|
|
9
|
+
*/
|
|
10
|
+
export declare const useVoiceInputVolume: () => VolumeChangeEvent;
|
|
11
|
+
/**
|
|
12
|
+
* Direct access to default Speech Recognizer volume change handler.
|
|
13
|
+
*
|
|
14
|
+
* In case you use static Speech Recognizer:
|
|
15
|
+
*
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { speechRecognizerVolumeChangeHandler } from '@gmessier/nitro-speech'
|
|
18
|
+
*
|
|
19
|
+
* SpeechRecognizer.onVolumeChange = speechRecognizerVolumeChangeHandler
|
|
20
|
+
* ... // setup everything else
|
|
21
|
+
* SpeechRecognizer.startListening({ locale: 'en-US' })
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare const speechRecognizerVolumeChangeHandler: OnVolumeChange;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
const subscribers = new Set();
|
|
3
|
+
let current = {
|
|
4
|
+
smoothedVolume: 0,
|
|
5
|
+
rawVolume: 0,
|
|
6
|
+
db: undefined,
|
|
7
|
+
};
|
|
8
|
+
let snapshot = { ...current };
|
|
9
|
+
const getSnapshot = () => {
|
|
10
|
+
if (snapshot.smoothedVolume === current.smoothedVolume &&
|
|
11
|
+
snapshot.rawVolume === current.rawVolume &&
|
|
12
|
+
snapshot.db === current.db) {
|
|
13
|
+
return snapshot;
|
|
14
|
+
}
|
|
15
|
+
snapshot = { ...current };
|
|
16
|
+
return snapshot;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Subscription to the voice input volume changes
|
|
20
|
+
*
|
|
21
|
+
* Updates with arbitrary frequency (many times per second) while audio recording is active.
|
|
22
|
+
*
|
|
23
|
+
* @returns The current voice input volume normalized to a range of 0 to 1.
|
|
24
|
+
*/
|
|
25
|
+
export const useVoiceInputVolume = () => {
|
|
26
|
+
return useSyncExternalStore((subscriber) => {
|
|
27
|
+
subscribers.add(subscriber);
|
|
28
|
+
return () => subscribers.delete(subscriber);
|
|
29
|
+
}, getSnapshot);
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Direct access to default Speech Recognizer volume change handler.
|
|
33
|
+
*
|
|
34
|
+
* In case you use static Speech Recognizer:
|
|
35
|
+
*
|
|
36
|
+
* ```typescript
|
|
37
|
+
* import { speechRecognizerVolumeChangeHandler } from '@gmessier/nitro-speech'
|
|
38
|
+
*
|
|
39
|
+
* SpeechRecognizer.onVolumeChange = speechRecognizerVolumeChangeHandler
|
|
40
|
+
* ... // setup everything else
|
|
41
|
+
* SpeechRecognizer.startListening({ locale: 'en-US' })
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export const speechRecognizerVolumeChangeHandler = (event) => {
|
|
45
|
+
if (event.smoothedVolume === current.smoothedVolume &&
|
|
46
|
+
event.rawVolume === current.rawVolume &&
|
|
47
|
+
event.db === current.db) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
current = event;
|
|
51
|
+
subscribers.forEach((subscriber) => subscriber?.(event));
|
|
52
|
+
};
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|