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.
Files changed (83) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +402 -0
  3. package/SherpaOnnx.podspec +84 -0
  4. package/android/build.gradle +193 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/cpp/CMakeLists.txt +121 -0
  7. package/android/src/main/cpp/include/sherpa-onnx/c-api/c-api.h +1918 -0
  8. package/android/src/main/cpp/include/sherpa-onnx/c-api/cxx-api.h +841 -0
  9. package/android/src/main/cpp/jni/sherpa-onnx-jni.cpp +129 -0
  10. package/android/src/main/cpp/jni/sherpa-onnx-wrapper.cpp +649 -0
  11. package/android/src/main/cpp/jni/sherpa-onnx-wrapper.h +56 -0
  12. package/android/src/main/java/com/sherpaonnx/SherpaOnnxModule.kt +316 -0
  13. package/android/src/main/java/com/sherpaonnx/SherpaOnnxPackage.kt +33 -0
  14. package/ios/Frameworks/sherpa_onnx.xcframework.zip +0 -0
  15. package/ios/SherpaOnnx.h +5 -0
  16. package/ios/SherpaOnnx.mm +293 -0
  17. package/ios/SherpaOnnx.xcconfig +19 -0
  18. package/ios/include/sherpa-onnx/c-api/c-api.h +1918 -0
  19. package/ios/include/sherpa-onnx/c-api/cxx-api.h +841 -0
  20. package/ios/sherpa-onnx-wrapper.h +57 -0
  21. package/ios/sherpa-onnx-wrapper.mm +432 -0
  22. package/lib/module/NativeSherpaOnnx.js +5 -0
  23. package/lib/module/NativeSherpaOnnx.js.map +1 -0
  24. package/lib/module/diarization/index.js +54 -0
  25. package/lib/module/diarization/index.js.map +1 -0
  26. package/lib/module/enhancement/index.js +54 -0
  27. package/lib/module/enhancement/index.js.map +1 -0
  28. package/lib/module/index.js +25 -0
  29. package/lib/module/index.js.map +1 -0
  30. package/lib/module/package.json +1 -0
  31. package/lib/module/separation/index.js +54 -0
  32. package/lib/module/separation/index.js.map +1 -0
  33. package/lib/module/stt/index.js +79 -0
  34. package/lib/module/stt/index.js.map +1 -0
  35. package/lib/module/stt/types.js +4 -0
  36. package/lib/module/stt/types.js.map +1 -0
  37. package/lib/module/tts/index.js +54 -0
  38. package/lib/module/tts/index.js.map +1 -0
  39. package/lib/module/types.js +2 -0
  40. package/lib/module/types.js.map +1 -0
  41. package/lib/module/utils.js +93 -0
  42. package/lib/module/utils.js.map +1 -0
  43. package/lib/module/vad/index.js +54 -0
  44. package/lib/module/vad/index.js.map +1 -0
  45. package/lib/typescript/package.json +1 -0
  46. package/lib/typescript/src/NativeSherpaOnnx.d.ts +39 -0
  47. package/lib/typescript/src/NativeSherpaOnnx.d.ts.map +1 -0
  48. package/lib/typescript/src/diarization/index.d.ts +49 -0
  49. package/lib/typescript/src/diarization/index.d.ts.map +1 -0
  50. package/lib/typescript/src/enhancement/index.d.ts +47 -0
  51. package/lib/typescript/src/enhancement/index.d.ts.map +1 -0
  52. package/lib/typescript/src/index.d.ts +9 -0
  53. package/lib/typescript/src/index.d.ts.map +1 -0
  54. package/lib/typescript/src/separation/index.d.ts +48 -0
  55. package/lib/typescript/src/separation/index.d.ts.map +1 -0
  56. package/lib/typescript/src/stt/index.d.ts +53 -0
  57. package/lib/typescript/src/stt/index.d.ts.map +1 -0
  58. package/lib/typescript/src/stt/types.d.ts +39 -0
  59. package/lib/typescript/src/stt/types.d.ts.map +1 -0
  60. package/lib/typescript/src/tts/index.d.ts +47 -0
  61. package/lib/typescript/src/tts/index.d.ts.map +1 -0
  62. package/lib/typescript/src/types.d.ts +59 -0
  63. package/lib/typescript/src/types.d.ts.map +1 -0
  64. package/lib/typescript/src/utils.d.ts +53 -0
  65. package/lib/typescript/src/utils.d.ts.map +1 -0
  66. package/lib/typescript/src/vad/index.d.ts +48 -0
  67. package/lib/typescript/src/vad/index.d.ts.map +1 -0
  68. package/package.json +221 -0
  69. package/scripts/copy-headers.js +184 -0
  70. package/scripts/setup-assets.js +323 -0
  71. package/scripts/setup-ios-framework.sh +282 -0
  72. package/scripts/switch-registry.js +75 -0
  73. package/src/NativeSherpaOnnx.ts +44 -0
  74. package/src/diarization/index.ts +69 -0
  75. package/src/enhancement/index.ts +67 -0
  76. package/src/index.tsx +30 -0
  77. package/src/separation/index.ts +68 -0
  78. package/src/stt/index.ts +83 -0
  79. package/src/stt/types.ts +42 -0
  80. package/src/tts/index.ts +67 -0
  81. package/src/types.ts +73 -0
  82. package/src/utils.ts +97 -0
  83. 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
+ }
@@ -0,0 +1,5 @@
1
+ #import <SherpaOnnxSpec/SherpaOnnxSpec.h>
2
+
3
+ @interface SherpaOnnx : NSObject <NativeSherpaOnnxSpec>
4
+
5
+ @end
@@ -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:&copyError];
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:&copyError];
99
+ } else {
100
+ [fileManager copyItemAtPath:sourcePath toPath:modelDir error:&copyError];
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