@gmessier/nitro-speech 0.0.1

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.
Files changed (85) hide show
  1. package/NitroSpeech.podspec +31 -0
  2. package/README.md +55 -0
  3. package/android/CMakeLists.txt +29 -0
  4. package/android/build.gradle +148 -0
  5. package/android/fix-prefab.gradle +51 -0
  6. package/android/gradle.properties +5 -0
  7. package/android/src/main/AndroidManifest.xml +3 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  9. package/android/src/main/java/com/margelo/nitro/nitrospeech/HybridNitroSpeech.kt +12 -0
  10. package/android/src/main/java/com/margelo/nitro/nitrospeech/NitroSpeechPackage.kt +20 -0
  11. package/android/src/main/java/com/margelo/nitro/nitrospeech/recognizer/AudioPermissionRequester.kt +39 -0
  12. package/android/src/main/java/com/margelo/nitro/nitrospeech/recognizer/AutoStopper.kt +35 -0
  13. package/android/src/main/java/com/margelo/nitro/nitrospeech/recognizer/HybridRecognizer.kt +181 -0
  14. package/android/src/main/java/com/margelo/nitro/nitrospeech/recognizer/RecognitionListenerSession.kt +106 -0
  15. package/ios/AppStateObserver.swift +31 -0
  16. package/ios/AutoStopper.swift +57 -0
  17. package/ios/Bridge.h +8 -0
  18. package/ios/HybridNitroSpeech.swift +6 -0
  19. package/ios/HybridRecognizer.swift +201 -0
  20. package/lib/commonjs/index.js +10 -0
  21. package/lib/commonjs/index.js.map +1 -0
  22. package/lib/commonjs/package.json +1 -0
  23. package/lib/commonjs/specs/NitroSpeech.nitro.js +6 -0
  24. package/lib/commonjs/specs/NitroSpeech.nitro.js.map +1 -0
  25. package/lib/module/index.js +6 -0
  26. package/lib/module/index.js.map +1 -0
  27. package/lib/module/package.json +1 -0
  28. package/lib/module/specs/NitroSpeech.nitro.js +4 -0
  29. package/lib/module/specs/NitroSpeech.nitro.js.map +1 -0
  30. package/lib/tsconfig.tsbuildinfo +1 -0
  31. package/lib/typescript/index.d.ts +3 -0
  32. package/lib/typescript/index.d.ts.map +1 -0
  33. package/lib/typescript/specs/NitroSpeech.nitro.d.ts +108 -0
  34. package/lib/typescript/specs/NitroSpeech.nitro.d.ts.map +1 -0
  35. package/nitro.json +24 -0
  36. package/nitrogen/generated/.gitattributes +1 -0
  37. package/nitrogen/generated/android/NitroSpeech+autolinking.cmake +83 -0
  38. package/nitrogen/generated/android/NitroSpeech+autolinking.gradle +27 -0
  39. package/nitrogen/generated/android/NitroSpeechOnLoad.cpp +54 -0
  40. package/nitrogen/generated/android/NitroSpeechOnLoad.hpp +25 -0
  41. package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
  42. package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
  43. package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
  44. package/nitrogen/generated/android/c++/JFunc_void_std__vector_std__string_.hpp +95 -0
  45. package/nitrogen/generated/android/c++/JHybridNitroSpeechSpec.cpp +59 -0
  46. package/nitrogen/generated/android/c++/JHybridNitroSpeechSpec.hpp +66 -0
  47. package/nitrogen/generated/android/c++/JHybridRecognizerSpec.cpp +167 -0
  48. package/nitrogen/generated/android/c++/JHybridRecognizerSpec.hpp +77 -0
  49. package/nitrogen/generated/android/c++/JSpeechToTextParams.hpp +109 -0
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/Func_void.kt +80 -0
  51. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/Func_void_double.kt +80 -0
  52. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/Func_void_std__string.kt +80 -0
  53. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/Func_void_std__vector_std__string_.kt +80 -0
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/HybridNitroSpeechSpec.kt +59 -0
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/HybridRecognizerSpec.kt +143 -0
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/NitroSpeechOnLoad.kt +35 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/SpeechToTextParams.kt +62 -0
  58. package/nitrogen/generated/ios/NitroSpeech+autolinking.rb +60 -0
  59. package/nitrogen/generated/ios/NitroSpeech-Swift-Cxx-Bridge.cpp +82 -0
  60. package/nitrogen/generated/ios/NitroSpeech-Swift-Cxx-Bridge.hpp +291 -0
  61. package/nitrogen/generated/ios/NitroSpeech-Swift-Cxx-Umbrella.hpp +55 -0
  62. package/nitrogen/generated/ios/NitroSpeechAutolinking.mm +33 -0
  63. package/nitrogen/generated/ios/NitroSpeechAutolinking.swift +25 -0
  64. package/nitrogen/generated/ios/c++/HybridNitroSpeechSpecSwift.cpp +11 -0
  65. package/nitrogen/generated/ios/c++/HybridNitroSpeechSpecSwift.hpp +77 -0
  66. package/nitrogen/generated/ios/c++/HybridRecognizerSpecSwift.cpp +11 -0
  67. package/nitrogen/generated/ios/c++/HybridRecognizerSpecSwift.hpp +126 -0
  68. package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  69. package/nitrogen/generated/ios/swift/Func_void_double.swift +47 -0
  70. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
  71. package/nitrogen/generated/ios/swift/Func_void_std__vector_std__string_.swift +47 -0
  72. package/nitrogen/generated/ios/swift/HybridNitroSpeechSpec.swift +56 -0
  73. package/nitrogen/generated/ios/swift/HybridNitroSpeechSpec_cxx.swift +137 -0
  74. package/nitrogen/generated/ios/swift/HybridRecognizerSpec.swift +62 -0
  75. package/nitrogen/generated/ios/swift/HybridRecognizerSpec_cxx.swift +337 -0
  76. package/nitrogen/generated/ios/swift/SpeechToTextParams.swift +300 -0
  77. package/nitrogen/generated/shared/c++/HybridNitroSpeechSpec.cpp +22 -0
  78. package/nitrogen/generated/shared/c++/HybridNitroSpeechSpec.hpp +65 -0
  79. package/nitrogen/generated/shared/c++/HybridRecognizerSpec.cpp +34 -0
  80. package/nitrogen/generated/shared/c++/HybridRecognizerSpec.hpp +79 -0
  81. package/nitrogen/generated/shared/c++/SpeechToTextParams.hpp +109 -0
  82. package/package.json +123 -0
  83. package/react-native.config.js +16 -0
  84. package/src/index.ts +8 -0
  85. package/src/specs/NitroSpeech.nitro.ts +113 -0
@@ -0,0 +1,31 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "NitroSpeech"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported, :visionos => 1.0 }
14
+ s.source = { :git => "https://github.com/mrousavy/nitro.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = [
17
+ # Implementation (Swift)
18
+ "ios/**/*.{swift}",
19
+ # Autolinking/Registration (Objective-C++)
20
+ "ios/**/*.{m,mm}",
21
+ # Implementation (C++ objects)
22
+ "cpp/**/*.{hpp,cpp}",
23
+ ]
24
+
25
+ load 'nitrogen/generated/ios/NitroSpeech+autolinking.rb'
26
+ add_nitrogen_files(s)
27
+
28
+ s.dependency 'React-jsi'
29
+ s.dependency 'React-callinvoker'
30
+ install_modules_dependencies(s)
31
+ end
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # nitro-speech
2
+
3
+ > **⚠️ Work in Progress**
4
+ >
5
+ > This library is under active development.
6
+
7
+ Speech recognition for React Native, powered by [Nitro Modules](https://github.com/mrousavy/nitro).
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install nitro-speech react-native-nitro-modules
13
+ # or
14
+ yarn add nitro-speech react-native-nitro-modules
15
+ # or
16
+ bun add nitro-speech react-native-nitro-modules
17
+ ```
18
+
19
+ ### iOS
20
+
21
+ ```bash
22
+ cd ios && pod install
23
+ ```
24
+
25
+ ### Android
26
+
27
+ No additional setup required.
28
+
29
+ ## Permissions
30
+
31
+ ### Android
32
+
33
+ The library declares the required permission in its `AndroidManifest.xml` (merged automatically):
34
+
35
+ ```xml
36
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
37
+ ```
38
+
39
+ ### iOS
40
+
41
+ Add the following keys to your app's `Info.plist`:
42
+
43
+ ```xml
44
+ <key>NSMicrophoneUsageDescription</key>
45
+ <string>This app needs microphone access for speech recognition</string>
46
+ <key>NSSpeechRecognitionUsageDescription</key>
47
+ <string>This app needs speech recognition to convert speech to text</string>
48
+ ```
49
+
50
+ Both permissions are required for speech recognition to work on iOS.
51
+
52
+ ## TODO
53
+
54
+ - [ ] (Android) Timer till the auto finish is called
55
+ - [ ] (Android) Cleanup when app loses the focus
@@ -0,0 +1,29 @@
1
+ project(NitroSpeech)
2
+ cmake_minimum_required(VERSION 3.9.0)
3
+
4
+ set (PACKAGE_NAME NitroSpeech)
5
+ set (CMAKE_VERBOSE_MAKEFILE ON)
6
+ set (CMAKE_CXX_STANDARD 20)
7
+
8
+ # Define C++ library and add all sources
9
+ add_library(${PACKAGE_NAME} SHARED
10
+ src/main/cpp/cpp-adapter.cpp
11
+ )
12
+
13
+ # Add Nitrogen specs :)
14
+ include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/NitroSpeech+autolinking.cmake)
15
+
16
+ # Set up local includes
17
+ include_directories(
18
+ "src/main/cpp"
19
+ "../cpp"
20
+ )
21
+
22
+ find_library(LOG_LIB log)
23
+
24
+ # Link all libraries together
25
+ target_link_libraries(
26
+ ${PACKAGE_NAME}
27
+ ${LOG_LIB}
28
+ android # <-- Android core
29
+ )
@@ -0,0 +1,148 @@
1
+ buildscript {
2
+ repositories {
3
+ google()
4
+ mavenCentral()
5
+ }
6
+
7
+ dependencies {
8
+ classpath "com.android.tools.build:gradle:8.13.1"
9
+ }
10
+ }
11
+
12
+ def reactNativeArchitectures() {
13
+ def value = rootProject.getProperties().get("reactNativeArchitectures")
14
+ return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
15
+ }
16
+
17
+ def isNewArchitectureEnabled() {
18
+ return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
19
+ }
20
+
21
+ apply plugin: "com.android.library"
22
+ apply plugin: 'org.jetbrains.kotlin.android'
23
+ apply from: '../nitrogen/generated/android/NitroSpeech+autolinking.gradle'
24
+ apply from: "./fix-prefab.gradle"
25
+
26
+ if (isNewArchitectureEnabled()) {
27
+ apply plugin: "com.facebook.react"
28
+ }
29
+
30
+ def getExtOrDefault(name) {
31
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["NitroSpeech_" + name]
32
+ }
33
+
34
+ def getExtOrIntegerDefault(name) {
35
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["NitroSpeech_" + name]).toInteger()
36
+ }
37
+
38
+ android {
39
+ namespace "com.margelo.nitro.nitrospeech"
40
+
41
+ ndkVersion getExtOrDefault("ndkVersion")
42
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
43
+
44
+ defaultConfig {
45
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
46
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
47
+ buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
48
+
49
+ externalNativeBuild {
50
+ cmake {
51
+ cppFlags "-frtti -fexceptions -Wall -Wextra -fstack-protector-all"
52
+ arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
53
+ abiFilters (*reactNativeArchitectures())
54
+
55
+ buildTypes {
56
+ debug {
57
+ cppFlags "-O1 -g"
58
+ }
59
+ release {
60
+ cppFlags "-O2"
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ externalNativeBuild {
68
+ cmake {
69
+ path "CMakeLists.txt"
70
+ }
71
+ }
72
+
73
+ packagingOptions {
74
+ excludes = [
75
+ "META-INF",
76
+ "META-INF/**",
77
+ "**/libc++_shared.so",
78
+ "**/libfbjni.so",
79
+ "**/libjsi.so",
80
+ "**/libfolly_json.so",
81
+ "**/libfolly_runtime.so",
82
+ "**/libglog.so",
83
+ "**/libhermes.so",
84
+ "**/libhermes-executor-debug.so",
85
+ "**/libhermes_executor.so",
86
+ "**/libreactnative.so",
87
+ "**/libreactnativejni.so",
88
+ "**/libturbomodulejsijni.so",
89
+ "**/libreact_nativemodule_core.so",
90
+ "**/libjscexecutor.so"
91
+ ]
92
+ }
93
+
94
+ buildFeatures {
95
+ buildConfig true
96
+ prefab true
97
+ }
98
+
99
+ buildTypes {
100
+ release {
101
+ minifyEnabled false
102
+ }
103
+ }
104
+
105
+ lintOptions {
106
+ disable "GradleCompatible"
107
+ }
108
+
109
+ compileOptions {
110
+ sourceCompatibility JavaVersion.VERSION_1_8
111
+ targetCompatibility JavaVersion.VERSION_1_8
112
+ }
113
+
114
+ sourceSets {
115
+ main {
116
+ if (isNewArchitectureEnabled()) {
117
+ java.srcDirs += [
118
+ // React Codegen files
119
+ "${project.buildDir}/generated/source/codegen/java"
120
+ ]
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ repositories {
127
+ mavenCentral()
128
+ google()
129
+ }
130
+
131
+
132
+ dependencies {
133
+ // For < 0.71, this will be from the local maven repo
134
+ // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
135
+ //noinspection GradleDynamicVersion
136
+ implementation "com.facebook.react:react-native:+"
137
+
138
+ // Add a dependency on NitroModules
139
+ implementation project(":react-native-nitro-modules")
140
+ }
141
+
142
+ if (isNewArchitectureEnabled()) {
143
+ react {
144
+ jsRootDir = file("../src/")
145
+ libraryName = "NitroSpeech"
146
+ codegenJavaPackageName = "com.margelo.nitro.nitrospeech"
147
+ }
148
+ }
@@ -0,0 +1,51 @@
1
+ tasks.configureEach { task ->
2
+ // Make sure that we generate our prefab publication file only after having built the native library
3
+ // so that not a header publication file, but a full configuration publication will be generated, which
4
+ // will include the .so file
5
+
6
+ def prefabConfigurePattern = ~/^prefab(.+)ConfigurePackage$/
7
+ def matcher = task.name =~ prefabConfigurePattern
8
+ if (matcher.matches()) {
9
+ def variantName = matcher[0][1]
10
+ task.outputs.upToDateWhen { false }
11
+ task.dependsOn("externalNativeBuild${variantName}")
12
+ }
13
+ }
14
+
15
+ afterEvaluate {
16
+ def abis = reactNativeArchitectures()
17
+ rootProject.allprojects.each { proj ->
18
+ if (proj === rootProject) return
19
+
20
+ def dependsOnThisLib = proj.configurations.findAll { it.canBeResolved }.any { config ->
21
+ config.dependencies.any { dep ->
22
+ dep.group == project.group && dep.name == project.name
23
+ }
24
+ }
25
+ if (!dependsOnThisLib && proj != project) return
26
+
27
+ if (!proj.plugins.hasPlugin('com.android.application') && !proj.plugins.hasPlugin('com.android.library')) {
28
+ return
29
+ }
30
+
31
+ def variants = proj.android.hasProperty('applicationVariants') ? proj.android.applicationVariants : proj.android.libraryVariants
32
+ // Touch the prefab_config.json files to ensure that in ExternalNativeJsonGenerator.kt we will re-trigger the prefab CLI to
33
+ // generate a libnameConfig.cmake file that will contain our native library (.so).
34
+ // See this condition: https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/tasks/ExternalNativeJsonGenerator.kt;l=207-219?q=createPrefabBuildSystemGlue
35
+ variants.all { variant ->
36
+ def variantName = variant.name
37
+ abis.each { abi ->
38
+ def searchDir = new File(proj.projectDir, ".cxx/${variantName}")
39
+ if (!searchDir.exists()) return
40
+ def matches = []
41
+ searchDir.eachDir { randomDir ->
42
+ def prefabFile = new File(randomDir, "${abi}/prefab_config.json")
43
+ if (prefabFile.exists()) matches << prefabFile
44
+ }
45
+ matches.each { prefabConfig ->
46
+ prefabConfig.setLastModified(System.currentTimeMillis())
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,5 @@
1
+ NitroSpeech_kotlinVersion=2.1.20
2
+ NitroSpeech_minSdkVersion=23
3
+ NitroSpeech_targetSdkVersion=36
4
+ NitroSpeech_compileSdkVersion=36
5
+ NitroSpeech_ndkVersion=27.1.12297006
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
3
+ </manifest>
@@ -0,0 +1,6 @@
1
+ #include <jni.h>
2
+ #include "NitroSpeechOnLoad.hpp"
3
+
4
+ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
5
+ return margelo::nitro::nitrospeech::initialize(vm);
6
+ }
@@ -0,0 +1,12 @@
1
+ package com.margelo.nitro.nitrospeech
2
+
3
+ import androidx.annotation.Keep
4
+ import com.facebook.proguard.annotations.DoNotStrip
5
+ import com.margelo.nitro.nitrospeech.recognizer.HybridRecognizer
6
+
7
+ class HybridNitroSpeech: HybridNitroSpeechSpec() {
8
+
9
+ @DoNotStrip
10
+ @Keep
11
+ override var recognizer: HybridRecognizerSpec = HybridRecognizer()
12
+ }
@@ -0,0 +1,20 @@
1
+ package com.margelo.nitro.nitrospeech
2
+
3
+ import com.facebook.react.bridge.NativeModule
4
+ import com.facebook.react.bridge.ReactApplicationContext
5
+ import com.facebook.react.module.model.ReactModuleInfoProvider
6
+ import com.facebook.react.BaseReactPackage
7
+ import com.margelo.nitro.nitrospeech.NitroSpeechOnLoad;
8
+
9
+
10
+ class NitroSpeechPackage : BaseReactPackage() {
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
12
+
13
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider { HashMap() }
14
+
15
+ companion object {
16
+ init {
17
+ NitroSpeechOnLoad.initializeNative()
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,39 @@
1
+ package com.margelo.nitro.nitrospeech.recognizer
2
+
3
+ import android.Manifest
4
+ import android.app.Activity
5
+ import android.content.pm.PackageManager
6
+ import androidx.activity.ComponentActivity
7
+ import androidx.activity.result.contract.ActivityResultContracts
8
+ import androidx.core.content.ContextCompat
9
+
10
+ class AudioPermissionRequester (
11
+ private val activity: Activity
12
+ ) {
13
+ private val recordAudioPermission = Manifest.permission.RECORD_AUDIO
14
+ private val componentActivity = activity as? ComponentActivity ?: error("Host activity must be a ComponentActivity")
15
+
16
+ private var callback: ((Boolean) -> Unit)? = null
17
+
18
+ private val launcher = componentActivity.activityResultRegistry.register(
19
+ "record_audio_key", ActivityResultContracts.RequestPermission()
20
+ ) { granted ->
21
+ callback?.invoke(granted)
22
+ }
23
+
24
+ fun checkAndRequest(onResult: (Boolean) -> Unit) {
25
+ val audioGranted =
26
+ ContextCompat.checkSelfPermission(
27
+ activity,
28
+ recordAudioPermission
29
+ ) == PackageManager.PERMISSION_GRANTED
30
+
31
+ if (audioGranted) {
32
+ onResult(true)
33
+ return
34
+ }
35
+
36
+ callback = onResult
37
+ launcher.launch(recordAudioPermission)
38
+ }
39
+ }
@@ -0,0 +1,35 @@
1
+ package com.margelo.nitro.nitrospeech.recognizer
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import android.util.Log
6
+
7
+ class AutoStopper (
8
+ private val silenceThreshold: Long,
9
+ val forceStopRecording: () -> Unit,
10
+ ) {
11
+ companion object {
12
+ private const val TAG = "HybridRecognizer"
13
+ }
14
+
15
+ private var isStopped = false
16
+ private val handler = Handler(Looper.getMainLooper())
17
+
18
+ private val autoStopRecording = Runnable {
19
+ if (isStopped) return@Runnable
20
+ Log.d(TAG, "forceStopRecording, ms: ${System.currentTimeMillis()}")
21
+ forceStopRecording()
22
+ }
23
+
24
+ fun indicateRecordingActivity() {
25
+ Log.d(TAG, "indicateRecordingActivity | isStopped: $isStopped | ms: ${System.currentTimeMillis()}")
26
+ handler.removeCallbacks(autoStopRecording)
27
+ if (isStopped) return
28
+ handler.postDelayed(autoStopRecording, silenceThreshold)
29
+ }
30
+
31
+ fun stop() {
32
+ isStopped = true
33
+ handler.removeCallbacks(autoStopRecording)
34
+ }
35
+ }
@@ -0,0 +1,181 @@
1
+ package com.margelo.nitro.nitrospeech.recognizer
2
+
3
+ import android.content.Context
4
+ import android.content.Intent
5
+ import android.os.Build
6
+ import android.os.Handler
7
+ import android.os.Looper
8
+ import android.speech.RecognizerIntent
9
+ import android.speech.SpeechRecognizer
10
+ import android.util.Log
11
+ import androidx.annotation.Keep
12
+ import com.facebook.proguard.annotations.DoNotStrip
13
+ import com.margelo.nitro.NitroModules
14
+ import com.margelo.nitro.nitrospeech.HybridRecognizerSpec
15
+ import com.margelo.nitro.nitrospeech.SpeechToTextParams
16
+
17
+ class HybridRecognizer: HybridRecognizerSpec() {
18
+ companion object {
19
+ private const val TAG = "HybridRecognizer"
20
+ private const val POST_RECOGNITION_DELAY = 250L
21
+ }
22
+
23
+ private var isActive: Boolean = false
24
+ private var config: SpeechToTextParams? = null
25
+ private var autoStopper: AutoStopper? = null
26
+ private var speechRecognizer: SpeechRecognizer? = null
27
+ private val mainHandler = Handler(Looper.getMainLooper())
28
+
29
+ override var onReadyForSpeech: (() -> Unit)? = null
30
+ override var onRecordingStopped: (() -> Unit)? = null
31
+ override var onResult: ((resultBatches: Array<String>) -> Unit)? = null
32
+
33
+ override var onAutoFinishProgress: ((timeLeftMs: Double) -> Unit)? = null
34
+ override var onError: ((error: String) -> Unit)? = null
35
+ override var onPermissionDenied: (() -> Unit)? = null
36
+
37
+ @DoNotStrip
38
+ @Keep
39
+ override fun startListening(params: SpeechToTextParams) {
40
+ Log.d(TAG, "startListening: $params")
41
+ if (isActive) {
42
+ onFinishRecognition(
43
+ null,
44
+ "Error at startListening: Previous SpeechRecognizer is still active",
45
+ false
46
+ )
47
+ return
48
+ }
49
+
50
+ val context = NitroModules.applicationContext
51
+ if (context == null) {
52
+ onFinishRecognition(
53
+ null,
54
+ "Error at startListening: Context not available",
55
+ true
56
+ )
57
+ return
58
+ }
59
+ val activity = context.currentActivity
60
+ if (activity == null) {
61
+ onFinishRecognition(
62
+ null,
63
+ "Error at startListening: Activity not found",
64
+ true
65
+ )
66
+ return
67
+ }
68
+
69
+ val permissionRequester = AudioPermissionRequester(activity)
70
+ permissionRequester.checkAndRequest { granted ->
71
+ if (!granted) {
72
+ onPermissionDenied?.invoke()
73
+ return@checkAndRequest
74
+ }
75
+ config = params
76
+ start(context)
77
+ }
78
+ }
79
+
80
+ @DoNotStrip
81
+ @Keep
82
+ override fun stopListening() {
83
+ Log.d(TAG, "stopListening called")
84
+ if (!isActive) return
85
+ onFinishRecognition(null, null, true)
86
+ mainHandler.postDelayed({
87
+ cleanup()
88
+ }, POST_RECOGNITION_DELAY)
89
+ }
90
+
91
+ private fun start(context: Context) {
92
+ mainHandler.post {
93
+ try {
94
+ speechRecognizer = SpeechRecognizer.createSpeechRecognizer(context)
95
+ val silenceThreshold = config?.autoFinishRecognitionMs?.toLong() ?: 8000
96
+ autoStopper = AutoStopper(
97
+ silenceThreshold,
98
+ ) {
99
+ stopListening()
100
+ }
101
+ val recognitionListenerSession = RecognitionListenerSession(
102
+ autoStopper,
103
+ config,
104
+ ) { result: ArrayList<String>?, errorMessage: String?, recordingStopped: Boolean ->
105
+ onFinishRecognition(result, errorMessage, recordingStopped)
106
+ }
107
+ speechRecognizer?.setRecognitionListener(recognitionListenerSession.createRecognitionListener())
108
+
109
+ val languageModel = if (config?.androidUseWebSearchModel == true) RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH else RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
110
+
111
+ val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
112
+ intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel)
113
+ intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, config?.locale ?: "en-US")
114
+ intent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true)
115
+ // set many secs to avoid cutting early
116
+ intent.putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS, 300000)
117
+
118
+ if (config?.androidMaskOffensiveWords != true && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
119
+ intent.putExtra(RecognizerIntent.EXTRA_MASK_OFFENSIVE_WORDS, false)
120
+ }
121
+
122
+ if (config?.androidFormattingPreferQuality == true && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
123
+ intent.putExtra(RecognizerIntent.EXTRA_ENABLE_FORMATTING, RecognizerIntent.FORMATTING_OPTIMIZE_QUALITY)
124
+ }
125
+
126
+ val contextualStrings = config?.contextualStrings
127
+ if (!contextualStrings.isNullOrEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
128
+ intent.putExtra(
129
+ RecognizerIntent.EXTRA_BIASING_STRINGS,
130
+ ArrayList(contextualStrings.toList()),
131
+ )
132
+ }
133
+
134
+ speechRecognizer?.startListening(intent)
135
+ isActive = true
136
+ mainHandler.postDelayed({
137
+ if (isActive) {
138
+ onReadyForSpeech?.invoke()
139
+ onFinishRecognition(arrayListOf(), null, false)
140
+ }
141
+ }, 500)
142
+ } catch (e: Exception) {
143
+ onFinishRecognition(
144
+ null,
145
+ "Error at start.mainHandler.post: ${e.message ?: "Unknown error"}",
146
+ true
147
+ )
148
+ }
149
+ }
150
+ }
151
+
152
+ private fun cleanup() {
153
+ try {
154
+ Log.d(TAG, "stopListening called")
155
+ autoStopper?.stop()
156
+ autoStopper = null
157
+ speechRecognizer?.stopListening()
158
+ speechRecognizer?.destroy()
159
+ speechRecognizer = null
160
+ isActive = false
161
+ } catch (e: Exception) {
162
+ onFinishRecognition(
163
+ null,
164
+ "Error at stopListening.mainHandler.postDelayed: ${e.message ?: "Unknown error"}",
165
+ true
166
+ )
167
+ }
168
+ }
169
+
170
+ private fun onFinishRecognition(result: ArrayList<String>?, errorMessage: String?, recordingStopped: Boolean) {
171
+ if (recordingStopped) {
172
+ onRecordingStopped?.invoke()
173
+ }
174
+ if (!errorMessage.isNullOrEmpty()) {
175
+ onError?.invoke(errorMessage)
176
+ }
177
+ if (!result.isNullOrEmpty()) {
178
+ onResult?.invoke(result.toTypedArray())
179
+ }
180
+ }
181
+ }