react-native-sherpa-onnx 0.2.0 → 0.3.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 (175) hide show
  1. package/README.md +232 -236
  2. package/SherpaOnnx.podspec +68 -64
  3. package/android/build.gradle +182 -192
  4. package/android/codegen.gradle +57 -0
  5. package/android/prebuilt-download.gradle +428 -0
  6. package/android/prebuilt-versions.gradle +43 -0
  7. package/android/proguard-rules.pro +10 -0
  8. package/android/src/main/assets/testModels/add_mul_add.onnx +28 -0
  9. package/android/src/main/assets/testModels/nnapi_internal_uint8_support.onnx +0 -0
  10. package/android/src/main/assets/testModels/qnn_multi_ctx_embed.onnx +0 -0
  11. package/android/src/main/cpp/CMakeLists.txt +166 -129
  12. package/android/src/main/cpp/CMakePresets.json +54 -0
  13. package/android/src/main/cpp/crypto/sha256.cpp +174 -0
  14. package/android/src/main/cpp/crypto/sha256.h +16 -0
  15. package/android/src/main/cpp/jni/archive/sherpa-onnx-archive-helper.cpp +404 -0
  16. package/android/src/main/cpp/jni/archive/sherpa-onnx-archive-helper.h +56 -0
  17. package/android/src/main/cpp/jni/archive/sherpa-onnx-archive-jni.cpp +181 -0
  18. package/android/src/main/cpp/jni/audio/sherpa-onnx-audio-convert-jni.cpp +888 -0
  19. package/{ios → android/src/main/cpp/jni/model_detect}/sherpa-onnx-common.h +18 -18
  20. package/android/src/main/cpp/jni/model_detect/sherpa-onnx-detect-jni-common.cpp +86 -0
  21. package/android/src/main/cpp/jni/model_detect/sherpa-onnx-detect-jni-common.h +20 -0
  22. package/android/src/main/cpp/jni/model_detect/sherpa-onnx-model-detect-helper.cpp +423 -0
  23. package/android/src/main/cpp/jni/model_detect/sherpa-onnx-model-detect-helper.h +55 -0
  24. package/android/src/main/cpp/jni/model_detect/sherpa-onnx-model-detect-stt.cpp +399 -0
  25. package/android/src/main/cpp/jni/model_detect/sherpa-onnx-model-detect-tts.cpp +238 -0
  26. package/{ios → android/src/main/cpp/jni/model_detect}/sherpa-onnx-model-detect.h +122 -89
  27. package/android/src/main/cpp/jni/model_detect/sherpa-onnx-stt-wrapper.cpp +99 -0
  28. package/android/src/main/cpp/jni/model_detect/sherpa-onnx-stt-wrapper.h +16 -0
  29. package/android/src/main/cpp/jni/model_detect/sherpa-onnx-tts-wrapper.cpp +78 -0
  30. package/android/src/main/cpp/jni/model_detect/sherpa-onnx-tts-wrapper.h +16 -0
  31. package/android/src/main/cpp/jni/module/sherpa-onnx-module-jni.cpp +190 -0
  32. package/android/src/main/cpp/jni/tts/sherpa-onnx-tts-zipvoice-jni.cpp +301 -0
  33. package/android/src/main/java/com/sherpaonnx/SherpaOnnxArchiveHelper.kt +94 -0
  34. package/android/src/main/java/com/sherpaonnx/{SherpaOnnxCoreHelper.kt → SherpaOnnxAssetHelper.kt} +350 -236
  35. package/android/src/main/java/com/sherpaonnx/SherpaOnnxModule.kt +791 -483
  36. package/android/src/main/java/com/sherpaonnx/SherpaOnnxSttHelper.kt +699 -109
  37. package/android/src/main/java/com/sherpaonnx/SherpaOnnxTtsHelper.kt +1123 -668
  38. package/android/src/main/java/com/sherpaonnx/ZipvoiceTtsWrapper.kt +187 -0
  39. package/ios/SherpaOnnx+Assets.h +11 -0
  40. package/ios/SherpaOnnx+Assets.mm +325 -0
  41. package/ios/SherpaOnnx+STT.mm +455 -118
  42. package/ios/SherpaOnnx+TTS.mm +1101 -712
  43. package/ios/SherpaOnnx.h +17 -6
  44. package/ios/SherpaOnnx.mm +206 -311
  45. package/ios/SherpaOnnx.xcconfig +19 -19
  46. package/ios/SherpaOnnxCoreMLHelper.swift +24 -0
  47. package/ios/archive/sherpa-onnx-archive-helper.h +21 -0
  48. package/ios/archive/sherpa-onnx-archive-helper.mm +296 -0
  49. package/ios/libarchive_darwin_config.h +153 -0
  50. package/{android/src/main/cpp/jni → ios/model_detect}/sherpa-onnx-common.h +18 -18
  51. package/ios/model_detect/sherpa-onnx-model-detect-helper.h +49 -0
  52. package/ios/model_detect/sherpa-onnx-model-detect-helper.mm +210 -0
  53. package/ios/model_detect/sherpa-onnx-model-detect-stt.mm +344 -0
  54. package/ios/model_detect/sherpa-onnx-model-detect-tts.mm +201 -0
  55. package/{android/src/main/cpp/jni → ios/model_detect}/sherpa-onnx-model-detect.h +117 -89
  56. package/ios/scripts/patch-libarchive-includes.sh +61 -0
  57. package/ios/scripts/setup-ios-libarchive.sh +98 -0
  58. package/ios/stt/sherpa-onnx-stt-wrapper.h +129 -0
  59. package/ios/stt/sherpa-onnx-stt-wrapper.mm +523 -0
  60. package/ios/{sherpa-onnx-tts-wrapper.h → tts/sherpa-onnx-tts-wrapper.h} +90 -85
  61. package/ios/{sherpa-onnx-tts-wrapper.mm → tts/sherpa-onnx-tts-wrapper.mm} +376 -345
  62. package/lib/module/NativeSherpaOnnx.js +3 -0
  63. package/lib/module/NativeSherpaOnnx.js.map +1 -1
  64. package/lib/module/audio/index.js +22 -0
  65. package/lib/module/audio/index.js.map +1 -0
  66. package/lib/module/diarization/index.js +1 -1
  67. package/lib/module/diarization/index.js.map +1 -1
  68. package/lib/module/download/ModelDownloadManager.js +918 -0
  69. package/lib/module/download/ModelDownloadManager.js.map +1 -0
  70. package/lib/module/download/extractTarBz2.js +53 -0
  71. package/lib/module/download/extractTarBz2.js.map +1 -0
  72. package/lib/module/download/index.js +6 -0
  73. package/lib/module/download/index.js.map +1 -0
  74. package/lib/module/download/validation.js +178 -0
  75. package/lib/module/download/validation.js.map +1 -0
  76. package/lib/module/enhancement/index.js +1 -1
  77. package/lib/module/enhancement/index.js.map +1 -1
  78. package/lib/module/index.js +41 -3
  79. package/lib/module/index.js.map +1 -1
  80. package/lib/module/separation/index.js +1 -1
  81. package/lib/module/separation/index.js.map +1 -1
  82. package/lib/module/stt/index.js +127 -60
  83. package/lib/module/stt/index.js.map +1 -1
  84. package/lib/module/stt/sttModelLanguages.js +512 -0
  85. package/lib/module/stt/sttModelLanguages.js.map +1 -0
  86. package/lib/module/stt/types.js +53 -1
  87. package/lib/module/stt/types.js.map +1 -1
  88. package/lib/module/tts/index.js +216 -289
  89. package/lib/module/tts/index.js.map +1 -1
  90. package/lib/module/tts/types.js +86 -1
  91. package/lib/module/tts/types.js.map +1 -1
  92. package/lib/module/types.js.map +1 -1
  93. package/lib/module/utils.js +86 -73
  94. package/lib/module/utils.js.map +1 -1
  95. package/lib/module/vad/index.js +1 -1
  96. package/lib/module/vad/index.js.map +1 -1
  97. package/lib/typescript/src/NativeSherpaOnnx.d.ts +192 -38
  98. package/lib/typescript/src/NativeSherpaOnnx.d.ts.map +1 -1
  99. package/lib/typescript/src/audio/index.d.ts +13 -0
  100. package/lib/typescript/src/audio/index.d.ts.map +1 -0
  101. package/lib/typescript/src/diarization/index.d.ts +3 -2
  102. package/lib/typescript/src/diarization/index.d.ts.map +1 -1
  103. package/lib/typescript/src/download/ModelDownloadManager.d.ts +108 -0
  104. package/lib/typescript/src/download/ModelDownloadManager.d.ts.map +1 -0
  105. package/lib/typescript/src/download/extractTarBz2.d.ts +14 -0
  106. package/lib/typescript/src/download/extractTarBz2.d.ts.map +1 -0
  107. package/lib/typescript/src/download/index.d.ts +7 -0
  108. package/lib/typescript/src/download/index.d.ts.map +1 -0
  109. package/lib/typescript/src/download/validation.d.ts +57 -0
  110. package/lib/typescript/src/download/validation.d.ts.map +1 -0
  111. package/lib/typescript/src/enhancement/index.d.ts +3 -2
  112. package/lib/typescript/src/enhancement/index.d.ts.map +1 -1
  113. package/lib/typescript/src/index.d.ts +26 -2
  114. package/lib/typescript/src/index.d.ts.map +1 -1
  115. package/lib/typescript/src/separation/index.d.ts +3 -2
  116. package/lib/typescript/src/separation/index.d.ts.map +1 -1
  117. package/lib/typescript/src/stt/index.d.ts +31 -43
  118. package/lib/typescript/src/stt/index.d.ts.map +1 -1
  119. package/lib/typescript/src/stt/sttModelLanguages.d.ts +52 -0
  120. package/lib/typescript/src/stt/sttModelLanguages.d.ts.map +1 -0
  121. package/lib/typescript/src/stt/types.d.ts +196 -9
  122. package/lib/typescript/src/stt/types.d.ts.map +1 -1
  123. package/lib/typescript/src/tts/index.d.ts +25 -211
  124. package/lib/typescript/src/tts/index.d.ts.map +1 -1
  125. package/lib/typescript/src/tts/types.d.ts +148 -25
  126. package/lib/typescript/src/tts/types.d.ts.map +1 -1
  127. package/lib/typescript/src/types.d.ts +0 -32
  128. package/lib/typescript/src/types.d.ts.map +1 -1
  129. package/lib/typescript/src/utils.d.ts +28 -13
  130. package/lib/typescript/src/utils.d.ts.map +1 -1
  131. package/lib/typescript/src/vad/index.d.ts +3 -2
  132. package/lib/typescript/src/vad/index.d.ts.map +1 -1
  133. package/package.json +250 -222
  134. package/scripts/check-qnn-support.sh +78 -0
  135. package/scripts/setup-ios-framework.sh +379 -282
  136. package/src/NativeSherpaOnnx.ts +474 -251
  137. package/src/audio/index.ts +32 -0
  138. package/src/diarization/index.ts +4 -2
  139. package/src/download/ModelDownloadManager.ts +1325 -0
  140. package/src/download/extractTarBz2.ts +78 -0
  141. package/src/download/index.ts +43 -0
  142. package/src/download/validation.ts +279 -0
  143. package/src/enhancement/index.ts +4 -2
  144. package/src/index.tsx +78 -27
  145. package/src/separation/index.ts +4 -2
  146. package/src/stt/index.ts +249 -89
  147. package/src/stt/sttModelLanguages.ts +237 -0
  148. package/src/stt/types.ts +263 -9
  149. package/src/tts/index.ts +470 -458
  150. package/src/tts/types.ts +373 -218
  151. package/src/types.ts +0 -44
  152. package/src/utils.ts +145 -131
  153. package/src/vad/index.ts +4 -2
  154. package/third_party/ffmpeg_prebuilt/ANDROID_RELEASE_TAG +1 -0
  155. package/third_party/libarchive_prebuilt/ANDROID_RELEASE_TAG +1 -0
  156. package/third_party/libarchive_prebuilt/IOS_RELEASE_TAG +1 -0
  157. package/third_party/sherpa-onnx-prebuilt/ANDROID_RELEASE_TAG +1 -0
  158. package/third_party/sherpa-onnx-prebuilt/IOS_RELEASE_TAG +1 -0
  159. package/android/src/main/cpp/include/sherpa-onnx/c-api/c-api.h +0 -1918
  160. package/android/src/main/cpp/include/sherpa-onnx/c-api/cxx-api.h +0 -841
  161. package/android/src/main/cpp/jni/sherpa-onnx-model-detect.cpp +0 -541
  162. package/android/src/main/cpp/jni/sherpa-onnx-stt-jni.cpp +0 -336
  163. package/android/src/main/cpp/jni/sherpa-onnx-stt-wrapper.cpp +0 -222
  164. package/android/src/main/cpp/jni/sherpa-onnx-stt-wrapper.h +0 -68
  165. package/android/src/main/cpp/jni/sherpa-onnx-tts-jni.cpp +0 -823
  166. package/android/src/main/cpp/jni/sherpa-onnx-tts-wrapper.cpp +0 -387
  167. package/android/src/main/cpp/jni/sherpa-onnx-tts-wrapper.h +0 -147
  168. package/ios/Frameworks/sherpa_onnx.xcframework.zip +0 -0
  169. package/ios/include/sherpa-onnx/c-api/c-api.h +0 -1918
  170. package/ios/include/sherpa-onnx/c-api/cxx-api.h +0 -841
  171. package/ios/sherpa-onnx-model-detect.mm +0 -441
  172. package/ios/sherpa-onnx-stt-wrapper.h +0 -48
  173. package/ios/sherpa-onnx-stt-wrapper.mm +0 -201
  174. package/scripts/copy-headers.js +0 -184
  175. package/scripts/setup-assets.js +0 -323
@@ -0,0 +1,187 @@
1
+ package com.sherpaonnx
2
+
3
+ import android.util.Log
4
+ import com.k2fsa.sherpa.onnx.GeneratedAudio
5
+
6
+ /**
7
+ * Kotlin wrapper for Zipvoice TTS via the sherpa-onnx C-API.
8
+ *
9
+ * The official Kotlin API (OfflineTts / OfflineTtsModelConfig) does not expose
10
+ * OfflineTtsZipvoiceModelConfig. This class bypasses the Kotlin API and calls the
11
+ * C-API directly through JNI methods in libsherpaonnx.so.
12
+ *
13
+ * The public API intentionally mirrors [com.k2fsa.sherpa.onnx.OfflineTts] so that
14
+ * [SherpaOnnxTtsHelper] can dispatch to either engine transparently.
15
+ */
16
+ internal class ZipvoiceTtsWrapper private constructor(private var ptr: Long) {
17
+
18
+ companion object {
19
+ private const val TAG = "ZipvoiceTts"
20
+
21
+ /**
22
+ * Create a Zipvoice TTS engine.
23
+ *
24
+ * @return a wrapper instance, or `null` if creation failed (check logcat for details).
25
+ */
26
+ fun create(
27
+ tokens: String,
28
+ encoder: String,
29
+ decoder: String,
30
+ vocoder: String,
31
+ dataDir: String,
32
+ lexicon: String,
33
+ featScale: Float = 0.1f,
34
+ tShift: Float = 0.5f,
35
+ targetRms: Float = 0.1f,
36
+ guidanceScale: Float = 1.0f,
37
+ numThreads: Int = 2,
38
+ debug: Boolean = false,
39
+ ruleFsts: String = "",
40
+ ruleFars: String = "",
41
+ maxNumSentences: Int = 1,
42
+ silenceScale: Float = 0.2f,
43
+ provider: String = "cpu"
44
+ ): ZipvoiceTtsWrapper? {
45
+ val p = nativeCreate(
46
+ tokens, encoder, decoder, vocoder, dataDir, lexicon,
47
+ featScale, tShift, targetRms, guidanceScale,
48
+ numThreads, debug,
49
+ ruleFsts, ruleFars, maxNumSentences, silenceScale,
50
+ provider
51
+ )
52
+ if (p == 0L) {
53
+ Log.e(TAG, "nativeCreate returned 0 — failed to create Zipvoice TTS engine")
54
+ return null
55
+ }
56
+ return ZipvoiceTtsWrapper(p)
57
+ }
58
+
59
+ // JNI native methods (implemented in sherpa-onnx-tts-zipvoice-jni.cpp, loaded via libsherpaonnx)
60
+ @JvmStatic
61
+ private external fun nativeCreate(
62
+ tokens: String, encoder: String, decoder: String, vocoder: String,
63
+ dataDir: String, lexicon: String,
64
+ featScale: Float, tShift: Float, targetRms: Float, guidanceScale: Float,
65
+ numThreads: Int, debug: Boolean,
66
+ ruleFsts: String, ruleFars: String, maxNumSentences: Int, silenceScale: Float,
67
+ provider: String
68
+ ): Long
69
+
70
+ @JvmStatic
71
+ private external fun nativeDestroy(ptr: Long)
72
+
73
+ @JvmStatic
74
+ private external fun nativeGetSampleRate(ptr: Long): Int
75
+
76
+ @JvmStatic
77
+ private external fun nativeGetNumSpeakers(ptr: Long): Int
78
+
79
+ @JvmStatic
80
+ private external fun nativeGenerate(ptr: Long, text: String, sid: Int, speed: Float): Array<Any>?
81
+
82
+ @JvmStatic
83
+ private external fun nativeGenerateWithZipvoice(
84
+ ptr: Long, text: String, promptText: String,
85
+ promptSamples: FloatArray, promptSr: Int,
86
+ speed: Float, numSteps: Int
87
+ ): Array<Any>?
88
+ }
89
+
90
+ // Instance method: JNI calls onNativeChunk on this object during generation
91
+ private external fun nativeGenerateWithCallback(ptr: Long, text: String, sid: Int, speed: Float): Array<Any>?
92
+
93
+ fun sampleRate(): Int {
94
+ check(ptr != 0L) { "ZipvoiceTtsWrapper already released" }
95
+ return nativeGetSampleRate(ptr)
96
+ }
97
+
98
+ fun numSpeakers(): Int {
99
+ check(ptr != 0L) { "ZipvoiceTtsWrapper already released" }
100
+ return nativeGetNumSpeakers(ptr)
101
+ }
102
+
103
+ /**
104
+ * Generate audio from text (non-zero-shot, standard TTS).
105
+ * Mirrors [com.k2fsa.sherpa.onnx.OfflineTts.generate].
106
+ */
107
+ fun generate(text: String, sid: Int = 0, speed: Float = 1.0f): GeneratedAudio {
108
+ check(ptr != 0L) { "ZipvoiceTtsWrapper already released" }
109
+ val result = nativeGenerate(ptr, text, sid, speed)
110
+ ?: throw RuntimeException("Zipvoice TTS generate returned null")
111
+ return parseAudioResult(result)
112
+ }
113
+
114
+ /**
115
+ * Generate audio with a per-chunk callback for streaming playback.
116
+ * The [callback] receives each audio chunk; return the chunk size to continue, 0 to cancel.
117
+ *
118
+ * Mirrors the callback-based generate in [com.k2fsa.sherpa.onnx.OfflineTts.generateWithCallback].
119
+ */
120
+ fun generateWithCallback(
121
+ text: String,
122
+ sid: Int = 0,
123
+ speed: Float = 1.0f,
124
+ callback: (FloatArray) -> Int
125
+ ): GeneratedAudio {
126
+ check(ptr != 0L) { "ZipvoiceTtsWrapper already released" }
127
+ this.streamCallback = callback
128
+ val result = nativeGenerateWithCallback(ptr, text, sid, speed)
129
+ ?: throw RuntimeException("Zipvoice TTS generateWithCallback returned null")
130
+ this.streamCallback = null
131
+ return parseAudioResult(result)
132
+ }
133
+
134
+ /**
135
+ * Zero-shot voice cloning with a reference prompt.
136
+ *
137
+ * @param text Text to synthesize.
138
+ * @param promptText Transcript of the reference audio.
139
+ * @param promptSamples Reference audio samples (mono, [-1, 1]).
140
+ * @param promptSr Sample rate of [promptSamples].
141
+ * @param speed Speed factor (1.0 = normal).
142
+ * @param numSteps Number of flow-matching diffusion steps.
143
+ */
144
+ fun generateWithZipvoice(
145
+ text: String,
146
+ promptText: String,
147
+ promptSamples: FloatArray,
148
+ promptSr: Int,
149
+ speed: Float = 1.0f,
150
+ numSteps: Int = 20
151
+ ): GeneratedAudio {
152
+ check(ptr != 0L) { "ZipvoiceTtsWrapper already released" }
153
+ val result = nativeGenerateWithZipvoice(ptr, text, promptText, promptSamples, promptSr, speed, numSteps)
154
+ ?: throw RuntimeException("Zipvoice TTS generateWithZipvoice returned null")
155
+ return parseAudioResult(result)
156
+ }
157
+
158
+ fun release() {
159
+ if (ptr != 0L) {
160
+ nativeDestroy(ptr)
161
+ ptr = 0L
162
+ }
163
+ }
164
+
165
+ // -- Streaming callback bridge --
166
+ // Called from JNI (nativeGenerateWithCallback) via the onNativeChunk method.
167
+ @Volatile
168
+ private var streamCallback: ((FloatArray) -> Int)? = null
169
+
170
+ /**
171
+ * Invoked from C++ callback. Must be public for JNI access but is not part of the public API.
172
+ * @return true to continue generating, false to cancel.
173
+ */
174
+ @Suppress("unused") // Called from JNI
175
+ fun onNativeChunk(samples: FloatArray, n: Int): Boolean {
176
+ val cb = streamCallback ?: return false
177
+ return cb(samples) != 0
178
+ }
179
+
180
+ // -- Internal helpers --
181
+
182
+ private fun parseAudioResult(result: Array<Any>): GeneratedAudio {
183
+ val samples = result[0] as FloatArray
184
+ val sampleRate = (result[1] as Number).toInt()
185
+ return GeneratedAudio(samples, sampleRate)
186
+ }
187
+ }
@@ -0,0 +1,11 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ @class SherpaOnnx;
4
+
5
+ @interface SherpaOnnx (Assets)
6
+
7
+ - (nullable NSString *)resolveAssetPath:(NSString *)assetPath error:(NSError **)error;
8
+ - (nullable NSString *)resolveFilePath:(NSString *)filePath error:(NSError **)error;
9
+ - (nullable NSString *)resolveAutoPath:(NSString *)path error:(NSError **)error;
10
+
11
+ @end
@@ -0,0 +1,325 @@
1
+ /**
2
+ * SherpaOnnx+Assets.mm
3
+ *
4
+ * Purpose: Asset and model path logic for the SherpaOnnx module: canonical models directory,
5
+ * resolveAssetPath, resolveFilePath, resolveAutoPath, listAssetModels, listModelsAtPath, and
6
+ * inferModelHint. Keeps the main module file focused; aligns with Android SherpaOnnxAssetHelper.kt.
7
+ */
8
+
9
+ #import "SherpaOnnx.h"
10
+ #import <React/RCTLog.h>
11
+
12
+ // Collects directory names (model folder names) under path into the set. Skips hidden items.
13
+ static void collectModelFolderNames(NSFileManager *fileManager, NSString *path, NSMutableSet *outNames)
14
+ {
15
+ BOOL isDirectory = NO;
16
+ if (![fileManager fileExistsAtPath:path isDirectory:&isDirectory] || !isDirectory) {
17
+ return;
18
+ }
19
+ NSError *err = nil;
20
+ NSArray<NSString *> *items = [fileManager contentsOfDirectoryAtPath:path error:&err];
21
+ if (err) {
22
+ return;
23
+ }
24
+ for (NSString *item in items) {
25
+ if ([item hasPrefix:@"."]) {
26
+ continue;
27
+ }
28
+ NSString *itemPath = [path stringByAppendingPathComponent:item];
29
+ BOOL itemIsDir = NO;
30
+ [fileManager fileExistsAtPath:itemPath isDirectory:&itemIsDir];
31
+ if (itemIsDir) {
32
+ [outNames addObject:item];
33
+ }
34
+ }
35
+ }
36
+
37
+ @implementation SherpaOnnx (Assets)
38
+
39
+ // Documents/models: used for downloaded assets and for listAssetModels.
40
+ - (NSString *)canonicalModelsDir
41
+ {
42
+ NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
43
+ return [documentsPath stringByAppendingPathComponent:@"models"];
44
+ }
45
+
46
+ - (NSString *)resolveAssetPath:(NSString *)assetPath error:(NSError **)error
47
+ {
48
+ NSFileManager *fileManager = [NSFileManager defaultManager];
49
+ NSString *folderName = [assetPath lastPathComponent];
50
+ NSString *modelDir = [[self canonicalModelsDir] stringByAppendingPathComponent:folderName];
51
+
52
+ // 1. Documents/models/<folder>: downloaded assets (no copy; bundle is read in place).
53
+ BOOL isDirectory = NO;
54
+ if ([fileManager fileExistsAtPath:modelDir isDirectory:&isDirectory] && isDirectory) {
55
+ return modelDir;
56
+ }
57
+
58
+ // 2. Bundle (resourcePath/assetPath): return path directly; do not copy.
59
+ NSString *bundleResourcePath = [[NSBundle mainBundle] resourcePath];
60
+ NSString *sourcePath = [bundleResourcePath stringByAppendingPathComponent:assetPath];
61
+ if ([fileManager fileExistsAtPath:sourcePath]) {
62
+ return sourcePath;
63
+ }
64
+
65
+ // 3. Fallback: pathForResource / inDirectory for non-standard bundle layouts.
66
+ NSString *bundlePath = [[NSBundle mainBundle] pathForResource:assetPath ofType:nil];
67
+ if (bundlePath && [fileManager fileExistsAtPath:bundlePath]) {
68
+ return bundlePath;
69
+ }
70
+ NSArray *pathComponents = [assetPath componentsSeparatedByString:@"/"];
71
+ if (pathComponents.count > 1) {
72
+ NSString *directory = pathComponents[0];
73
+ for (NSInteger i = 1; i < pathComponents.count - 1; i++) {
74
+ directory = [directory stringByAppendingPathComponent:pathComponents[i]];
75
+ }
76
+ NSString *resourceName = pathComponents.lastObject;
77
+ bundlePath = [[NSBundle mainBundle] pathForResource:resourceName ofType:nil inDirectory:directory];
78
+ if (bundlePath && [fileManager fileExistsAtPath:bundlePath]) {
79
+ return bundlePath;
80
+ }
81
+ }
82
+
83
+ if (error) {
84
+ *error = [NSError errorWithDomain:@"SherpaOnnx"
85
+ code:1
86
+ userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Asset path not found: %@", assetPath]}];
87
+ }
88
+ return nil;
89
+ }
90
+
91
+ - (NSString *)resolveFilePath:(NSString *)filePath error:(NSError **)error
92
+ {
93
+ NSFileManager *fileManager = [NSFileManager defaultManager];
94
+ BOOL isDirectory = NO;
95
+ BOOL exists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
96
+
97
+ if (!exists) {
98
+ if (error) {
99
+ *error = [NSError errorWithDomain:@"SherpaOnnx"
100
+ code:2
101
+ userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"File path does not exist: %@", filePath]}];
102
+ }
103
+ return nil;
104
+ }
105
+
106
+ if (!isDirectory) {
107
+ if (error) {
108
+ *error = [NSError errorWithDomain:@"SherpaOnnx"
109
+ code:3
110
+ userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Path is not a directory: %@", filePath]}];
111
+ }
112
+ return nil;
113
+ }
114
+
115
+ return [filePath stringByStandardizingPath];
116
+ }
117
+
118
+ - (NSString *)resolveAutoPath:(NSString *)path error:(NSError **)error
119
+ {
120
+ NSError *assetError = nil;
121
+ NSString *resolvedPath = [self resolveAssetPath:path error:&assetError];
122
+
123
+ if (resolvedPath) {
124
+ return resolvedPath;
125
+ }
126
+
127
+ NSError *fileError = nil;
128
+ resolvedPath = [self resolveFilePath:path error:&fileError];
129
+
130
+ if (resolvedPath) {
131
+ return resolvedPath;
132
+ }
133
+
134
+ if (error) {
135
+ NSString *errorMessage = [NSString stringWithFormat:@"Path not found as asset or file: %@. Asset error: %@, File error: %@",
136
+ path,
137
+ assetError.localizedDescription ?: @"Unknown",
138
+ fileError.localizedDescription ?: @"Unknown"];
139
+ *error = [NSError errorWithDomain:@"SherpaOnnx"
140
+ code:4
141
+ userInfo:@{NSLocalizedDescriptionKey: errorMessage}];
142
+ }
143
+ return nil;
144
+ }
145
+
146
+ - (void)listAssetModels:(RCTPromiseResolveBlock)resolve
147
+ reject:(RCTPromiseRejectBlock)reject
148
+ {
149
+ @try {
150
+ NSFileManager *fileManager = [NSFileManager defaultManager];
151
+ NSMutableSet *folderNames = [NSMutableSet set];
152
+
153
+ NSString *canonicalDir = [self canonicalModelsDir];
154
+ collectModelFolderNames(fileManager, canonicalDir, folderNames);
155
+
156
+ NSString *bundleModelsPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"models"];
157
+ collectModelFolderNames(fileManager, bundleModelsPath, folderNames);
158
+
159
+ NSMutableArray<NSDictionary *> *result = [NSMutableArray array];
160
+ for (NSString *folder in [[folderNames allObjects] sortedArrayUsingSelector:@selector(compare:)]) {
161
+ NSString *hint = [self inferModelHint:folder];
162
+ [result addObject:@{ @"folder": folder, @"hint": hint }];
163
+ }
164
+ resolve(result);
165
+ } @catch (NSException *exception) {
166
+ NSString *errorMsg = [NSString stringWithFormat:@"Exception listing asset models: %@", exception.reason];
167
+ reject(@"LIST_ASSETS_ERROR", errorMsg, nil);
168
+ }
169
+ }
170
+
171
+ - (void)listModelsAtPath:(NSString *)path
172
+ recursive:(BOOL)recursive
173
+ resolve:(RCTPromiseResolveBlock)resolve
174
+ reject:(RCTPromiseRejectBlock)reject
175
+ {
176
+ @try {
177
+ if (!path || path.length == 0) {
178
+ reject(@"PATH_REQUIRED", @"Path is required", nil);
179
+ return;
180
+ }
181
+
182
+ NSFileManager *fileManager = [NSFileManager defaultManager];
183
+ BOOL isDirectory = NO;
184
+ BOOL exists = [fileManager fileExistsAtPath:path isDirectory:&isDirectory];
185
+ if (!exists || !isDirectory) {
186
+ resolve(@[]);
187
+ return;
188
+ }
189
+
190
+ NSMutableArray<NSDictionary *> *result = [NSMutableArray array];
191
+ NSMutableSet<NSString *> *seen = [NSMutableSet set];
192
+ NSString *basePath = [path stringByStandardizingPath];
193
+
194
+ if (!recursive) {
195
+ NSError *error = nil;
196
+ NSArray<NSString *> *items = [fileManager contentsOfDirectoryAtPath:basePath error:&error];
197
+ if (error) {
198
+ NSString *errorMsg = [NSString stringWithFormat:@"Failed to list directory: %@", error.localizedDescription];
199
+ reject(@"LIST_MODELS_ERROR", errorMsg, error);
200
+ return;
201
+ }
202
+
203
+ for (NSString *item in items) {
204
+ if ([item hasPrefix:@"."]) {
205
+ continue;
206
+ }
207
+ NSString *itemPath = [basePath stringByAppendingPathComponent:item];
208
+ BOOL itemIsDir = NO;
209
+ [fileManager fileExistsAtPath:itemPath isDirectory:&itemIsDir];
210
+ if (itemIsDir && ![seen containsObject:item]) {
211
+ NSString *hint = [self inferModelHint:item];
212
+ [result addObject:@{ @"folder": item, @"hint": hint }];
213
+ [seen addObject:item];
214
+ }
215
+ }
216
+ } else {
217
+ NSURL *baseURL = [NSURL fileURLWithPath:basePath];
218
+ NSArray<NSURLResourceKey> *keys = @[ NSURLIsDirectoryKey ];
219
+ NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtURL:baseURL
220
+ includingPropertiesForKeys:keys
221
+ options:NSDirectoryEnumerationSkipsHiddenFiles
222
+ errorHandler:^BOOL(NSURL *url, NSError *error) {
223
+ RCTLogWarn(@"Failed to enumerate %@: %@", url.path, error.localizedDescription);
224
+ return YES;
225
+ }];
226
+
227
+ for (NSURL *url in enumerator) {
228
+ NSNumber *isDirValue = nil;
229
+ [url getResourceValue:&isDirValue forKey:NSURLIsDirectoryKey error:nil];
230
+ if (![isDirValue boolValue]) {
231
+ continue;
232
+ }
233
+
234
+ NSString *fullPath = url.path;
235
+ NSString *relativePath = nil;
236
+ if ([fullPath hasPrefix:[basePath stringByAppendingString:@"/"]]) {
237
+ relativePath = [fullPath substringFromIndex:basePath.length + 1];
238
+ } else if ([fullPath isEqualToString:basePath]) {
239
+ continue;
240
+ } else {
241
+ continue;
242
+ }
243
+
244
+ if (relativePath.length == 0 || [seen containsObject:relativePath]) {
245
+ continue;
246
+ }
247
+
248
+ NSString *hintName = url.lastPathComponent;
249
+ NSString *hint = [self inferModelHint:hintName];
250
+ [result addObject:@{ @"folder": relativePath, @"hint": hint }];
251
+ [seen addObject:relativePath];
252
+ }
253
+ }
254
+
255
+ resolve(result);
256
+ } @catch (NSException *exception) {
257
+ NSString *errorMsg = [NSString stringWithFormat:@"Exception listing models: %@", exception.reason];
258
+ reject(@"LIST_MODELS_ERROR", errorMsg, nil);
259
+ }
260
+ }
261
+
262
+ - (NSString *)inferModelHint:(NSString *)folderName
263
+ {
264
+ NSString *name = [folderName lowercaseString];
265
+ NSArray<NSString *> *sttHints = @[
266
+ @"zipformer",
267
+ @"paraformer",
268
+ @"nemo",
269
+ @"parakeet",
270
+ @"whisper",
271
+ @"wenet",
272
+ @"sensevoice",
273
+ @"sense-voice",
274
+ @"sense",
275
+ @"funasr",
276
+ @"transducer",
277
+ @"ctc",
278
+ @"asr"
279
+ ];
280
+ NSArray<NSString *> *ttsHints = @[
281
+ @"vits",
282
+ @"piper",
283
+ @"matcha",
284
+ @"kokoro",
285
+ @"kitten",
286
+ @"pocket",
287
+ @"zipvoice",
288
+ @"melo",
289
+ @"coqui",
290
+ @"mms",
291
+ @"tts"
292
+ ];
293
+
294
+ BOOL isStt = NO;
295
+ for (NSString *hint in sttHints) {
296
+ if ([name containsString:hint]) {
297
+ isStt = YES;
298
+ break;
299
+ }
300
+ }
301
+
302
+ BOOL isTts = NO;
303
+ for (NSString *hint in ttsHints) {
304
+ if ([name containsString:hint]) {
305
+ isTts = YES;
306
+ break;
307
+ }
308
+ }
309
+
310
+ if (isStt && isTts) {
311
+ return @"unknown";
312
+ }
313
+
314
+ if (isStt) {
315
+ return @"stt";
316
+ }
317
+
318
+ if (isTts) {
319
+ return @"tts";
320
+ }
321
+
322
+ return @"unknown";
323
+ }
324
+
325
+ @end