react-native-sherpa-onnx 0.1.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 +20 -0
- package/README.md +402 -0
- package/SherpaOnnx.podspec +84 -0
- package/android/build.gradle +193 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/CMakeLists.txt +121 -0
- package/android/src/main/cpp/include/sherpa-onnx/c-api/c-api.h +1918 -0
- package/android/src/main/cpp/include/sherpa-onnx/c-api/cxx-api.h +841 -0
- package/android/src/main/cpp/jni/sherpa-onnx-jni.cpp +129 -0
- package/android/src/main/cpp/jni/sherpa-onnx-wrapper.cpp +649 -0
- package/android/src/main/cpp/jni/sherpa-onnx-wrapper.h +56 -0
- package/android/src/main/java/com/sherpaonnx/SherpaOnnxModule.kt +316 -0
- package/android/src/main/java/com/sherpaonnx/SherpaOnnxPackage.kt +33 -0
- package/ios/Frameworks/sherpa_onnx.xcframework.zip +0 -0
- package/ios/SherpaOnnx.h +5 -0
- package/ios/SherpaOnnx.mm +293 -0
- package/ios/SherpaOnnx.xcconfig +19 -0
- package/ios/include/sherpa-onnx/c-api/c-api.h +1918 -0
- package/ios/include/sherpa-onnx/c-api/cxx-api.h +841 -0
- package/ios/sherpa-onnx-wrapper.h +57 -0
- package/ios/sherpa-onnx-wrapper.mm +432 -0
- package/lib/module/NativeSherpaOnnx.js +5 -0
- package/lib/module/NativeSherpaOnnx.js.map +1 -0
- package/lib/module/diarization/index.js +54 -0
- package/lib/module/diarization/index.js.map +1 -0
- package/lib/module/enhancement/index.js +54 -0
- package/lib/module/enhancement/index.js.map +1 -0
- package/lib/module/index.js +25 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/separation/index.js +54 -0
- package/lib/module/separation/index.js.map +1 -0
- package/lib/module/stt/index.js +79 -0
- package/lib/module/stt/index.js.map +1 -0
- package/lib/module/stt/types.js +4 -0
- package/lib/module/stt/types.js.map +1 -0
- package/lib/module/tts/index.js +54 -0
- package/lib/module/tts/index.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils.js +93 -0
- package/lib/module/utils.js.map +1 -0
- package/lib/module/vad/index.js +54 -0
- package/lib/module/vad/index.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeSherpaOnnx.d.ts +39 -0
- package/lib/typescript/src/NativeSherpaOnnx.d.ts.map +1 -0
- package/lib/typescript/src/diarization/index.d.ts +49 -0
- package/lib/typescript/src/diarization/index.d.ts.map +1 -0
- package/lib/typescript/src/enhancement/index.d.ts +47 -0
- package/lib/typescript/src/enhancement/index.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +9 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/separation/index.d.ts +48 -0
- package/lib/typescript/src/separation/index.d.ts.map +1 -0
- package/lib/typescript/src/stt/index.d.ts +53 -0
- package/lib/typescript/src/stt/index.d.ts.map +1 -0
- package/lib/typescript/src/stt/types.d.ts +39 -0
- package/lib/typescript/src/stt/types.d.ts.map +1 -0
- package/lib/typescript/src/tts/index.d.ts +47 -0
- package/lib/typescript/src/tts/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +59 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utils.d.ts +53 -0
- package/lib/typescript/src/utils.d.ts.map +1 -0
- package/lib/typescript/src/vad/index.d.ts +48 -0
- package/lib/typescript/src/vad/index.d.ts.map +1 -0
- package/package.json +221 -0
- package/scripts/copy-headers.js +184 -0
- package/scripts/setup-assets.js +323 -0
- package/scripts/setup-ios-framework.sh +282 -0
- package/scripts/switch-registry.js +75 -0
- package/src/NativeSherpaOnnx.ts +44 -0
- package/src/diarization/index.ts +69 -0
- package/src/enhancement/index.ts +67 -0
- package/src/index.tsx +30 -0
- package/src/separation/index.ts +68 -0
- package/src/stt/index.ts +83 -0
- package/src/stt/types.ts +42 -0
- package/src/tts/index.ts +67 -0
- package/src/types.ts +73 -0
- package/src/utils.ts +97 -0
- package/src/vad/index.ts +70 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#ifndef SHERPA_ONNX_WRAPPER_H
|
|
2
|
+
#define SHERPA_ONNX_WRAPPER_H
|
|
3
|
+
|
|
4
|
+
#include <string>
|
|
5
|
+
#include <memory>
|
|
6
|
+
|
|
7
|
+
namespace sherpaonnx {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Wrapper class for sherpa-onnx OfflineRecognizer.
|
|
11
|
+
* This provides a C++ interface that can be easily called from JNI.
|
|
12
|
+
*/
|
|
13
|
+
class SherpaOnnxWrapper {
|
|
14
|
+
public:
|
|
15
|
+
SherpaOnnxWrapper();
|
|
16
|
+
~SherpaOnnxWrapper();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initialize sherpa-onnx with model directory.
|
|
20
|
+
* @param modelDir Path to the model directory
|
|
21
|
+
* @param preferInt8 Optional: true = prefer int8 models, false = prefer regular models, nullopt = try int8 first (default)
|
|
22
|
+
* @param modelType Optional: explicit model type ("transducer", "paraformer", "nemo_ctc"), nullopt = auto-detect (default)
|
|
23
|
+
* @return true if successful, false otherwise
|
|
24
|
+
*/
|
|
25
|
+
bool initialize(
|
|
26
|
+
const std::string& modelDir,
|
|
27
|
+
const std::optional<bool>& preferInt8 = std::nullopt,
|
|
28
|
+
const std::optional<std::string>& modelType = std::nullopt
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Transcribe an audio file.
|
|
33
|
+
* @param filePath Path to the audio file (WAV 16kHz mono 16-bit PCM)
|
|
34
|
+
* @return Transcribed text
|
|
35
|
+
*/
|
|
36
|
+
std::string transcribeFile(const std::string& filePath);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if the recognizer is initialized.
|
|
40
|
+
* @return true if initialized, false otherwise
|
|
41
|
+
*/
|
|
42
|
+
bool isInitialized() const;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Release resources.
|
|
46
|
+
*/
|
|
47
|
+
void release();
|
|
48
|
+
|
|
49
|
+
private:
|
|
50
|
+
class Impl;
|
|
51
|
+
std::unique_ptr<Impl> pImpl;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
} // namespace sherpaonnx
|
|
55
|
+
|
|
56
|
+
#endif // SHERPA_ONNX_WRAPPER_H
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
package com.sherpaonnx
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
|
+
import com.facebook.react.bridge.Promise
|
|
6
|
+
import com.facebook.react.bridge.ReadableMap
|
|
7
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
8
|
+
import java.io.File
|
|
9
|
+
import java.io.FileOutputStream
|
|
10
|
+
import java.io.InputStream
|
|
11
|
+
|
|
12
|
+
@ReactModule(name = SherpaOnnxModule.NAME)
|
|
13
|
+
class SherpaOnnxModule(reactContext: ReactApplicationContext) :
|
|
14
|
+
NativeSherpaOnnxSpec(reactContext) {
|
|
15
|
+
|
|
16
|
+
init {
|
|
17
|
+
System.loadLibrary("sherpaonnx")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override fun getName(): String {
|
|
21
|
+
return NAME
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Test method to verify sherpa-onnx native library is loaded.
|
|
26
|
+
* This is a minimal "Hello World" test for Phase 1.
|
|
27
|
+
*/
|
|
28
|
+
override fun testSherpaInit(promise: Promise) {
|
|
29
|
+
try {
|
|
30
|
+
val result = nativeTestSherpaInit()
|
|
31
|
+
promise.resolve(result)
|
|
32
|
+
} catch (e: Exception) {
|
|
33
|
+
promise.reject("INIT_ERROR", "Failed to test sherpa-onnx initialization", e)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve model path based on configuration.
|
|
39
|
+
* Handles asset paths, file system paths, and auto-detection.
|
|
40
|
+
*/
|
|
41
|
+
override fun resolveModelPath(config: ReadableMap, promise: Promise) {
|
|
42
|
+
try {
|
|
43
|
+
val type = config.getString("type") ?: "auto"
|
|
44
|
+
val path = config.getString("path")
|
|
45
|
+
?: throw IllegalArgumentException("Path is required")
|
|
46
|
+
|
|
47
|
+
val resolvedPath = when (type) {
|
|
48
|
+
"asset" -> resolveAssetPath(path)
|
|
49
|
+
"file" -> resolveFilePath(path)
|
|
50
|
+
"auto" -> resolveAutoPath(path)
|
|
51
|
+
else -> throw IllegalArgumentException("Unknown path type: $type")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
promise.resolve(resolvedPath)
|
|
55
|
+
} catch (e: Exception) {
|
|
56
|
+
val errorMessage = "Failed to resolve model path: ${e.message ?: e.javaClass.simpleName}"
|
|
57
|
+
Log.e(NAME, errorMessage, e)
|
|
58
|
+
promise.reject("PATH_RESOLVE_ERROR", errorMessage, e)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve asset path - copy from assets to internal storage if needed
|
|
64
|
+
* Preserves the directory structure from assets (e.g., test_wavs/ stays as test_wavs/)
|
|
65
|
+
*/
|
|
66
|
+
private fun resolveAssetPath(assetPath: String): String {
|
|
67
|
+
val assetManager = reactApplicationContext.assets
|
|
68
|
+
|
|
69
|
+
// Extract base directory from path (e.g., "test_wavs/en-1.wav" -> "test_wavs", "models/sherpa-onnx-model" -> "models")
|
|
70
|
+
val pathParts = assetPath.split("/")
|
|
71
|
+
val baseDir = if (pathParts.size > 1) pathParts[0] else "models"
|
|
72
|
+
|
|
73
|
+
val targetBaseDir = File(reactApplicationContext.filesDir, baseDir)
|
|
74
|
+
targetBaseDir.mkdirs()
|
|
75
|
+
|
|
76
|
+
// Check if it's a file path (contains a file extension) or directory path
|
|
77
|
+
val isFilePath = pathParts.any { it.contains(".") && !it.startsWith(".") }
|
|
78
|
+
|
|
79
|
+
val targetPath = if (isFilePath) {
|
|
80
|
+
// It's a file path (e.g., test_wavs/en-1.wav)
|
|
81
|
+
// Return the full file path
|
|
82
|
+
File(targetBaseDir, pathParts.drop(1).joinToString("/"))
|
|
83
|
+
} else {
|
|
84
|
+
// It's a directory path (e.g., models/sherpa-onnx-model)
|
|
85
|
+
// Return the directory path
|
|
86
|
+
File(targetBaseDir, File(assetPath).name)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check if already extracted
|
|
90
|
+
if (isFilePath) {
|
|
91
|
+
// For files, check if file exists
|
|
92
|
+
if (targetPath.exists() && targetPath.isFile) {
|
|
93
|
+
return targetPath.absolutePath
|
|
94
|
+
}
|
|
95
|
+
// Extract the parent directory (e.g., test_wavs/)
|
|
96
|
+
val parentDir = targetPath.parentFile ?: targetBaseDir
|
|
97
|
+
parentDir.mkdirs()
|
|
98
|
+
|
|
99
|
+
// Try to copy the file directly first
|
|
100
|
+
try {
|
|
101
|
+
assetManager.open(assetPath).use { input ->
|
|
102
|
+
FileOutputStream(targetPath).use { output ->
|
|
103
|
+
input.copyTo(output)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return targetPath.absolutePath
|
|
107
|
+
} catch (e: java.io.FileNotFoundException) {
|
|
108
|
+
// If direct file open fails, try to copy the parent directory recursively
|
|
109
|
+
// This handles cases where the file is in a subdirectory
|
|
110
|
+
val parentAssetPath = pathParts.dropLast(1).joinToString("/")
|
|
111
|
+
if (parentAssetPath.isNotEmpty()) {
|
|
112
|
+
try {
|
|
113
|
+
// Copy the entire parent directory
|
|
114
|
+
copyAssetRecursively(assetManager, parentAssetPath, parentDir)
|
|
115
|
+
// Check if file now exists
|
|
116
|
+
if (targetPath.exists() && targetPath.isFile) {
|
|
117
|
+
return targetPath.absolutePath
|
|
118
|
+
}
|
|
119
|
+
throw IllegalArgumentException("File not found after copying parent directory: $assetPath")
|
|
120
|
+
} catch (dirException: Exception) {
|
|
121
|
+
throw IllegalArgumentException("Failed to extract asset file: $assetPath. Tried direct copy and directory copy.", dirException)
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
throw IllegalArgumentException("Failed to extract asset file: $assetPath", e)
|
|
125
|
+
}
|
|
126
|
+
} catch (e: Exception) {
|
|
127
|
+
throw IllegalArgumentException("Failed to extract asset file: $assetPath", e)
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
// For directories, check if directory exists
|
|
131
|
+
if (targetPath.exists() && targetPath.isDirectory) {
|
|
132
|
+
return targetPath.absolutePath
|
|
133
|
+
}
|
|
134
|
+
// Extract from assets recursively
|
|
135
|
+
try {
|
|
136
|
+
targetPath.mkdirs()
|
|
137
|
+
copyAssetRecursively(assetManager, assetPath, targetPath)
|
|
138
|
+
return targetPath.absolutePath
|
|
139
|
+
} catch (e: Exception) {
|
|
140
|
+
throw IllegalArgumentException("Failed to extract asset directory: $assetPath", e)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Recursively copy assets from asset manager to target directory
|
|
147
|
+
*/
|
|
148
|
+
private fun copyAssetRecursively(
|
|
149
|
+
assetManager: android.content.res.AssetManager,
|
|
150
|
+
assetPath: String,
|
|
151
|
+
targetDir: File
|
|
152
|
+
) {
|
|
153
|
+
val assetFiles = assetManager.list(assetPath)
|
|
154
|
+
?: throw IllegalArgumentException("Asset path not found: $assetPath")
|
|
155
|
+
|
|
156
|
+
for (fileName in assetFiles) {
|
|
157
|
+
val assetFilePath = "$assetPath/$fileName"
|
|
158
|
+
val targetFile = File(targetDir, fileName)
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
// Try to list as directory first
|
|
162
|
+
val subFiles = assetManager.list(assetFilePath)
|
|
163
|
+
if (subFiles != null && subFiles.isNotEmpty()) {
|
|
164
|
+
// It's a directory, recurse
|
|
165
|
+
targetFile.mkdirs()
|
|
166
|
+
copyAssetRecursively(assetManager, assetFilePath, targetFile)
|
|
167
|
+
} else {
|
|
168
|
+
// It's a file, copy it
|
|
169
|
+
assetManager.open(assetFilePath).use { input ->
|
|
170
|
+
FileOutputStream(targetFile).use { output ->
|
|
171
|
+
input.copyTo(output)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} catch (e: Exception) {
|
|
176
|
+
// If listing fails, try to open as file
|
|
177
|
+
try {
|
|
178
|
+
assetManager.open(assetFilePath).use { input ->
|
|
179
|
+
FileOutputStream(targetFile).use { output ->
|
|
180
|
+
input.copyTo(output)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} catch (fileException: Exception) {
|
|
184
|
+
throw IllegalArgumentException("Failed to copy asset: $assetFilePath", fileException)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Resolve file system path - verify it exists
|
|
192
|
+
*/
|
|
193
|
+
private fun resolveFilePath(filePath: String): String {
|
|
194
|
+
val file = File(filePath)
|
|
195
|
+
if (!file.exists()) {
|
|
196
|
+
throw IllegalArgumentException("File path does not exist: $filePath")
|
|
197
|
+
}
|
|
198
|
+
if (!file.isDirectory) {
|
|
199
|
+
throw IllegalArgumentException("Path is not a directory: $filePath")
|
|
200
|
+
}
|
|
201
|
+
return file.absolutePath
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Auto-detect path - try asset first, then file system
|
|
206
|
+
*/
|
|
207
|
+
private fun resolveAutoPath(path: String): String {
|
|
208
|
+
return try {
|
|
209
|
+
resolveAssetPath(path)
|
|
210
|
+
} catch (e: Exception) {
|
|
211
|
+
// If asset fails, try file system
|
|
212
|
+
try {
|
|
213
|
+
resolveFilePath(path)
|
|
214
|
+
} catch (fileException: Exception) {
|
|
215
|
+
throw IllegalArgumentException(
|
|
216
|
+
"Path not found as asset or file: $path. Asset error: ${e.message}, File error: ${fileException.message}",
|
|
217
|
+
e
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Initialize sherpa-onnx with model directory.
|
|
225
|
+
* Phase 1: Stub implementation
|
|
226
|
+
*/
|
|
227
|
+
override fun initializeSherpaOnnx(
|
|
228
|
+
modelDir: String,
|
|
229
|
+
preferInt8: Boolean?,
|
|
230
|
+
modelType: String?,
|
|
231
|
+
promise: Promise
|
|
232
|
+
) {
|
|
233
|
+
try {
|
|
234
|
+
// Verify model directory exists
|
|
235
|
+
val modelDirFile = File(modelDir)
|
|
236
|
+
if (!modelDirFile.exists()) {
|
|
237
|
+
val errorMsg = "Model directory does not exist: $modelDir"
|
|
238
|
+
Log.e(NAME, errorMsg)
|
|
239
|
+
promise.reject("INIT_ERROR", errorMsg)
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!modelDirFile.isDirectory) {
|
|
244
|
+
val errorMsg = "Model path is not a directory: $modelDir"
|
|
245
|
+
Log.e(NAME, errorMsg)
|
|
246
|
+
promise.reject("INIT_ERROR", errorMsg)
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
val success = nativeInitialize(
|
|
251
|
+
modelDir,
|
|
252
|
+
preferInt8 ?: false,
|
|
253
|
+
preferInt8 != null,
|
|
254
|
+
modelType ?: "auto"
|
|
255
|
+
)
|
|
256
|
+
if (success) {
|
|
257
|
+
promise.resolve(null)
|
|
258
|
+
} else {
|
|
259
|
+
val errorMsg = "Failed to initialize sherpa-onnx. Check native logs for details."
|
|
260
|
+
Log.e(NAME, "Native initialization returned false for modelDir: $modelDir")
|
|
261
|
+
promise.reject("INIT_ERROR", errorMsg)
|
|
262
|
+
}
|
|
263
|
+
} catch (e: Exception) {
|
|
264
|
+
val errorMsg = "Exception during initialization: ${e.message ?: e.javaClass.simpleName}"
|
|
265
|
+
Log.e(NAME, errorMsg, e)
|
|
266
|
+
promise.reject("INIT_ERROR", errorMsg, e)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Transcribe an audio file.
|
|
272
|
+
* Phase 1: Stub implementation
|
|
273
|
+
*/
|
|
274
|
+
override fun transcribeFile(filePath: String, promise: Promise) {
|
|
275
|
+
try {
|
|
276
|
+
val result = nativeTranscribeFile(filePath)
|
|
277
|
+
promise.resolve(result)
|
|
278
|
+
} catch (e: Exception) {
|
|
279
|
+
promise.reject("TRANSCRIBE_ERROR", "Failed to transcribe file", e)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Release sherpa-onnx resources.
|
|
285
|
+
*/
|
|
286
|
+
override fun unloadSherpaOnnx(promise: Promise) {
|
|
287
|
+
try {
|
|
288
|
+
nativeRelease()
|
|
289
|
+
promise.resolve(null)
|
|
290
|
+
} catch (e: Exception) {
|
|
291
|
+
promise.reject("RELEASE_ERROR", "Failed to release resources", e)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
companion object {
|
|
296
|
+
const val NAME = "SherpaOnnx"
|
|
297
|
+
|
|
298
|
+
// Native JNI methods
|
|
299
|
+
@JvmStatic
|
|
300
|
+
private external fun nativeTestSherpaInit(): String
|
|
301
|
+
|
|
302
|
+
@JvmStatic
|
|
303
|
+
private external fun nativeInitialize(
|
|
304
|
+
modelDir: String,
|
|
305
|
+
preferInt8: Boolean,
|
|
306
|
+
hasPreferInt8: Boolean,
|
|
307
|
+
modelType: String
|
|
308
|
+
): Boolean
|
|
309
|
+
|
|
310
|
+
@JvmStatic
|
|
311
|
+
private external fun nativeTranscribeFile(filePath: String): String
|
|
312
|
+
|
|
313
|
+
@JvmStatic
|
|
314
|
+
private external fun nativeRelease()
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.sherpaonnx
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
import java.util.HashMap
|
|
9
|
+
|
|
10
|
+
class SherpaOnnxPackage : BaseReactPackage() {
|
|
11
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
12
|
+
return if (name == SherpaOnnxModule.NAME) {
|
|
13
|
+
SherpaOnnxModule(reactContext)
|
|
14
|
+
} else {
|
|
15
|
+
null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
20
|
+
return ReactModuleInfoProvider {
|
|
21
|
+
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
|
|
22
|
+
moduleInfos[SherpaOnnxModule.NAME] = ReactModuleInfo(
|
|
23
|
+
SherpaOnnxModule.NAME,
|
|
24
|
+
SherpaOnnxModule.NAME,
|
|
25
|
+
false, // canOverrideExistingModule
|
|
26
|
+
false, // needsEagerInit
|
|
27
|
+
false, // isCxxModule
|
|
28
|
+
true // isTurboModule
|
|
29
|
+
)
|
|
30
|
+
moduleInfos
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
Binary file
|
package/ios/SherpaOnnx.h
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#import "SherpaOnnx.h"
|
|
2
|
+
#import <React/RCTUtils.h>
|
|
3
|
+
#import <React/RCTLog.h>
|
|
4
|
+
#import "sherpa-onnx-wrapper.h"
|
|
5
|
+
#import <memory>
|
|
6
|
+
#import <optional>
|
|
7
|
+
#import <string>
|
|
8
|
+
|
|
9
|
+
@implementation SherpaOnnx
|
|
10
|
+
- (void)resolveModelPath:(NSDictionary *)config
|
|
11
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
12
|
+
withRejecter:(RCTPromiseRejectBlock)reject
|
|
13
|
+
{
|
|
14
|
+
NSString *type = config[@"type"] ?: @"auto";
|
|
15
|
+
NSString *path = config[@"path"];
|
|
16
|
+
|
|
17
|
+
if (!path) {
|
|
18
|
+
reject(@"PATH_REQUIRED", @"Path is required", nil);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
NSError *error = nil;
|
|
23
|
+
NSString *resolvedPath = nil;
|
|
24
|
+
|
|
25
|
+
if ([type isEqualToString:@"asset"]) {
|
|
26
|
+
resolvedPath = [self resolveAssetPath:path error:&error];
|
|
27
|
+
} else if ([type isEqualToString:@"file"]) {
|
|
28
|
+
resolvedPath = [self resolveFilePath:path error:&error];
|
|
29
|
+
} else if ([type isEqualToString:@"auto"]) {
|
|
30
|
+
resolvedPath = [self resolveAutoPath:path error:&error];
|
|
31
|
+
} else {
|
|
32
|
+
NSString *errorMsg = [NSString stringWithFormat:@"Unknown path type: %@", type];
|
|
33
|
+
reject(@"INVALID_TYPE", errorMsg, nil);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (error) {
|
|
38
|
+
reject(@"PATH_RESOLVE_ERROR", error.localizedDescription, error);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
resolve(resolvedPath);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
- (NSString *)resolveAssetPath:(NSString *)assetPath error:(NSError **)error
|
|
46
|
+
{
|
|
47
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
48
|
+
|
|
49
|
+
// First, try to find directly in bundle (for folder references)
|
|
50
|
+
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:assetPath ofType:nil];
|
|
51
|
+
|
|
52
|
+
if (bundlePath && [fileManager fileExistsAtPath:bundlePath]) {
|
|
53
|
+
return bundlePath;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Try with directory structure (for resources in subdirectories)
|
|
57
|
+
NSArray *pathComponents = [assetPath componentsSeparatedByString:@"/"];
|
|
58
|
+
if (pathComponents.count > 1) {
|
|
59
|
+
NSString *directory = pathComponents[0];
|
|
60
|
+
for (NSInteger i = 1; i < pathComponents.count - 1; i++) {
|
|
61
|
+
directory = [directory stringByAppendingPathComponent:pathComponents[i]];
|
|
62
|
+
}
|
|
63
|
+
NSString *resourceName = pathComponents.lastObject;
|
|
64
|
+
bundlePath = [[NSBundle mainBundle] pathForResource:resourceName ofType:nil inDirectory:directory];
|
|
65
|
+
|
|
66
|
+
if (bundlePath && [fileManager fileExistsAtPath:bundlePath]) {
|
|
67
|
+
return bundlePath;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// If not found in bundle, try to copy from bundle to Documents
|
|
72
|
+
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
|
73
|
+
NSString *targetDir = [documentsPath stringByAppendingPathComponent:@"models"];
|
|
74
|
+
NSString *modelDir = [targetDir stringByAppendingPathComponent:[assetPath lastPathComponent]];
|
|
75
|
+
|
|
76
|
+
// Check if already copied
|
|
77
|
+
if ([fileManager fileExistsAtPath:modelDir]) {
|
|
78
|
+
return modelDir;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Try to find and copy from bundle resource path
|
|
82
|
+
NSString *bundleResourcePath = [[NSBundle mainBundle] resourcePath];
|
|
83
|
+
NSString *sourcePath = [bundleResourcePath stringByAppendingPathComponent:assetPath];
|
|
84
|
+
|
|
85
|
+
if ([fileManager fileExistsAtPath:sourcePath]) {
|
|
86
|
+
NSError *copyError = nil;
|
|
87
|
+
[fileManager createDirectoryAtPath:targetDir withIntermediateDirectories:YES attributes:nil error:©Error];
|
|
88
|
+
if (copyError) {
|
|
89
|
+
if (error) *error = copyError;
|
|
90
|
+
return nil;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Copy recursively if it's a directory
|
|
94
|
+
BOOL isDirectory = NO;
|
|
95
|
+
[fileManager fileExistsAtPath:sourcePath isDirectory:&isDirectory];
|
|
96
|
+
|
|
97
|
+
if (isDirectory) {
|
|
98
|
+
[fileManager copyItemAtPath:sourcePath toPath:modelDir error:©Error];
|
|
99
|
+
} else {
|
|
100
|
+
[fileManager copyItemAtPath:sourcePath toPath:modelDir error:©Error];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (copyError) {
|
|
104
|
+
if (error) *error = copyError;
|
|
105
|
+
return nil;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return modelDir;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (error) {
|
|
112
|
+
*error = [NSError errorWithDomain:@"SherpaOnnx"
|
|
113
|
+
code:1
|
|
114
|
+
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Asset path not found: %@", assetPath]}];
|
|
115
|
+
}
|
|
116
|
+
return nil;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
- (NSString *)resolveFilePath:(NSString *)filePath error:(NSError **)error
|
|
120
|
+
{
|
|
121
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
122
|
+
BOOL isDirectory = NO;
|
|
123
|
+
BOOL exists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
|
|
124
|
+
|
|
125
|
+
if (!exists) {
|
|
126
|
+
if (error) {
|
|
127
|
+
*error = [NSError errorWithDomain:@"SherpaOnnx"
|
|
128
|
+
code:2
|
|
129
|
+
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"File path does not exist: %@", filePath]}];
|
|
130
|
+
}
|
|
131
|
+
return nil;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!isDirectory) {
|
|
135
|
+
if (error) {
|
|
136
|
+
*error = [NSError errorWithDomain:@"SherpaOnnx"
|
|
137
|
+
code:3
|
|
138
|
+
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Path is not a directory: %@", filePath]}];
|
|
139
|
+
}
|
|
140
|
+
return nil;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return [filePath stringByStandardizingPath];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
- (NSString *)resolveAutoPath:(NSString *)path error:(NSError **)error
|
|
147
|
+
{
|
|
148
|
+
// Try asset first
|
|
149
|
+
NSError *assetError = nil;
|
|
150
|
+
NSString *resolvedPath = [self resolveAssetPath:path error:&assetError];
|
|
151
|
+
|
|
152
|
+
if (resolvedPath) {
|
|
153
|
+
return resolvedPath;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// If asset fails, try file system
|
|
157
|
+
NSError *fileError = nil;
|
|
158
|
+
resolvedPath = [self resolveFilePath:path error:&fileError];
|
|
159
|
+
|
|
160
|
+
if (resolvedPath) {
|
|
161
|
+
return resolvedPath;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Both failed
|
|
165
|
+
if (error) {
|
|
166
|
+
NSString *errorMessage = [NSString stringWithFormat:@"Path not found as asset or file: %@. Asset error: %@, File error: %@",
|
|
167
|
+
path,
|
|
168
|
+
assetError.localizedDescription ?: @"Unknown",
|
|
169
|
+
fileError.localizedDescription ?: @"Unknown"];
|
|
170
|
+
*error = [NSError errorWithDomain:@"SherpaOnnx"
|
|
171
|
+
code:4
|
|
172
|
+
userInfo:@{NSLocalizedDescriptionKey: errorMessage}];
|
|
173
|
+
}
|
|
174
|
+
return nil;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Global wrapper instance
|
|
178
|
+
static std::unique_ptr<sherpaonnx::SherpaOnnxWrapper> g_wrapper = nullptr;
|
|
179
|
+
|
|
180
|
+
- (void)initializeSherpaOnnx:(NSString *)modelDir
|
|
181
|
+
preferInt8:(NSNumber *)preferInt8
|
|
182
|
+
modelType:(NSString *)modelType
|
|
183
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
184
|
+
withRejecter:(RCTPromiseRejectBlock)reject
|
|
185
|
+
{
|
|
186
|
+
RCTLogInfo(@"Initializing sherpa-onnx with modelDir: %@", modelDir);
|
|
187
|
+
|
|
188
|
+
@try {
|
|
189
|
+
if (g_wrapper == nullptr) {
|
|
190
|
+
g_wrapper = std::make_unique<sherpaonnx::SherpaOnnxWrapper>();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
std::string modelDirStr = [modelDir UTF8String];
|
|
194
|
+
|
|
195
|
+
// Convert NSNumber to std::optional<bool>
|
|
196
|
+
std::optional<bool> preferInt8Opt = std::nullopt;
|
|
197
|
+
if (preferInt8 != nil) {
|
|
198
|
+
preferInt8Opt = [preferInt8 boolValue];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Convert NSString to std::optional<std::string>
|
|
202
|
+
std::optional<std::string> modelTypeOpt = std::nullopt;
|
|
203
|
+
if (modelType != nil && [modelType length] > 0) {
|
|
204
|
+
modelTypeOpt = [modelType UTF8String];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
bool result = g_wrapper->initialize(modelDirStr, preferInt8Opt, modelTypeOpt);
|
|
208
|
+
|
|
209
|
+
if (result) {
|
|
210
|
+
RCTLogInfo(@"Sherpa-onnx initialized successfully");
|
|
211
|
+
resolve(nil);
|
|
212
|
+
} else {
|
|
213
|
+
NSString *errorMsg = [NSString stringWithFormat:@"Failed to initialize sherpa-onnx with model directory: %@", modelDir];
|
|
214
|
+
RCTLogError(@"%@", errorMsg);
|
|
215
|
+
reject(@"INIT_ERROR", errorMsg, nil);
|
|
216
|
+
}
|
|
217
|
+
} @catch (NSException *exception) {
|
|
218
|
+
NSString *errorMsg = [NSString stringWithFormat:@"Exception during initialization: %@", exception.reason];
|
|
219
|
+
RCTLogError(@"%@", errorMsg);
|
|
220
|
+
reject(@"INIT_ERROR", errorMsg, nil);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
- (void)testSherpaInitWithResolver:(RCTPromiseResolveBlock)resolve
|
|
225
|
+
withRejecter:(RCTPromiseRejectBlock)reject
|
|
226
|
+
{
|
|
227
|
+
@try {
|
|
228
|
+
// Test that sherpa-onnx headers are available
|
|
229
|
+
resolve(@"Sherpa ONNX loaded!");
|
|
230
|
+
} @catch (NSException *exception) {
|
|
231
|
+
NSString *errorMsg = [NSString stringWithFormat:@"Exception during test: %@", exception.reason];
|
|
232
|
+
reject(@"TEST_ERROR", errorMsg, nil);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
- (void)transcribeFile:(NSString *)filePath
|
|
237
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
238
|
+
withRejecter:(RCTPromiseRejectBlock)reject
|
|
239
|
+
{
|
|
240
|
+
@try {
|
|
241
|
+
if (g_wrapper == nullptr || !g_wrapper->isInitialized()) {
|
|
242
|
+
reject(@"TRANSCRIBE_ERROR", @"Sherpa-onnx not initialized. Call initializeSherpaOnnx first.", nil);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
std::string filePathStr = [filePath UTF8String];
|
|
247
|
+
std::string result = g_wrapper->transcribeFile(filePathStr);
|
|
248
|
+
|
|
249
|
+
// Convert result to NSString - empty strings are valid (e.g., silence)
|
|
250
|
+
NSString *transcribedText = [NSString stringWithUTF8String:result.c_str()];
|
|
251
|
+
if (transcribedText == nil) {
|
|
252
|
+
// If conversion fails, treat as empty string
|
|
253
|
+
transcribedText = @"";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
resolve(transcribedText);
|
|
257
|
+
} @catch (NSException *exception) {
|
|
258
|
+
NSString *errorMsg = [NSString stringWithFormat:@"Exception during transcription: %@", exception.reason];
|
|
259
|
+
RCTLogError(@"%@", errorMsg);
|
|
260
|
+
reject(@"TRANSCRIBE_ERROR", errorMsg, nil);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
- (void)unloadSherpaOnnxWithResolver:(RCTPromiseResolveBlock)resolve
|
|
265
|
+
withRejecter:(RCTPromiseRejectBlock)reject
|
|
266
|
+
{
|
|
267
|
+
@try {
|
|
268
|
+
if (g_wrapper != nullptr) {
|
|
269
|
+
g_wrapper->release();
|
|
270
|
+
g_wrapper.reset();
|
|
271
|
+
g_wrapper = nullptr;
|
|
272
|
+
}
|
|
273
|
+
RCTLogInfo(@"Sherpa-onnx resources released");
|
|
274
|
+
resolve(nil);
|
|
275
|
+
} @catch (NSException *exception) {
|
|
276
|
+
NSString *errorMsg = [NSString stringWithFormat:@"Exception during cleanup: %@", exception.reason];
|
|
277
|
+
RCTLogError(@"%@", errorMsg);
|
|
278
|
+
reject(@"CLEANUP_ERROR", errorMsg, nil);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
283
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
284
|
+
{
|
|
285
|
+
return std::make_shared<facebook::react::NativeSherpaOnnxSpecJSI>(params);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
+ (NSString *)moduleName
|
|
289
|
+
{
|
|
290
|
+
return @"SherpaOnnx";
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
@end
|