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
@@ -1,236 +1,350 @@
1
- package com.sherpaonnx
2
-
3
- import android.util.Log
4
- import com.facebook.react.bridge.Arguments
5
- import com.facebook.react.bridge.Promise
6
- import com.facebook.react.bridge.ReadableMap
7
- import com.facebook.react.bridge.ReactApplicationContext
8
- import java.io.File
9
- import java.io.FileOutputStream
10
-
11
- internal class SherpaOnnxCoreHelper(
12
- private val context: ReactApplicationContext,
13
- private val logTag: String
14
- ) {
15
- fun resolveModelPath(config: ReadableMap, promise: Promise) {
16
- try {
17
- val type = config.getString("type") ?: "auto"
18
- val path = config.getString("path")
19
- ?: throw IllegalArgumentException("Path is required")
20
-
21
- val resolvedPath = when (type) {
22
- "asset" -> resolveAssetPath(path)
23
- "file" -> resolveFilePath(path)
24
- "auto" -> resolveAutoPath(path)
25
- else -> throw IllegalArgumentException("Unknown path type: $type")
26
- }
27
-
28
- promise.resolve(resolvedPath)
29
- } catch (e: Exception) {
30
- val errorMessage = "Failed to resolve model path: ${e.message ?: e.javaClass.simpleName}"
31
- Log.e(logTag, errorMessage, e)
32
- promise.reject("PATH_RESOLVE_ERROR", errorMessage, e)
33
- }
34
- }
35
-
36
- fun listAssetModels(promise: Promise) {
37
- try {
38
- val assetManager = context.assets
39
- val modelFolders = mutableListOf<String>()
40
-
41
- try {
42
- val items = assetManager.list("models") ?: emptyArray()
43
- for (item in items) {
44
- val subItems = assetManager.list("models/$item")
45
- if (subItems != null && subItems.isNotEmpty()) {
46
- modelFolders.add(item)
47
- }
48
- }
49
- } catch (e: Exception) {
50
- Log.w(logTag, "Could not list models directory: ${e.message}")
51
- }
52
-
53
- val result = Arguments.createArray()
54
- modelFolders.forEach { folder ->
55
- val modelMap = Arguments.createMap()
56
- modelMap.putString("folder", folder)
57
- modelMap.putString("hint", inferModelHint(folder))
58
- result.pushMap(modelMap)
59
- }
60
-
61
- promise.resolve(result)
62
- } catch (e: Exception) {
63
- promise.reject("LIST_ASSETS_ERROR", "Failed to list asset models: ${e.message}", e)
64
- }
65
- }
66
-
67
- private fun resolveAssetPath(assetPath: String): String {
68
- val assetManager = context.assets
69
-
70
- val pathParts = assetPath.split("/")
71
- val baseDir = if (pathParts.size > 1) pathParts[0] else "models"
72
-
73
- val targetBaseDir = File(context.filesDir, baseDir)
74
- targetBaseDir.mkdirs()
75
-
76
- val isFilePath = pathParts.any { it.contains(".") && !it.startsWith(".") }
77
-
78
- val targetPath = if (isFilePath) {
79
- File(targetBaseDir, pathParts.drop(1).joinToString("/"))
80
- } else {
81
- File(targetBaseDir, File(assetPath).name)
82
- }
83
-
84
- if (isFilePath) {
85
- if (targetPath.exists() && targetPath.isFile) {
86
- return targetPath.absolutePath
87
- }
88
- val parentDir = targetPath.parentFile ?: targetBaseDir
89
- parentDir.mkdirs()
90
-
91
- try {
92
- assetManager.open(assetPath).use { input ->
93
- FileOutputStream(targetPath).use { output ->
94
- input.copyTo(output)
95
- }
96
- }
97
- return targetPath.absolutePath
98
- } catch (e: java.io.FileNotFoundException) {
99
- val parentAssetPath = pathParts.dropLast(1).joinToString("/")
100
- if (parentAssetPath.isNotEmpty()) {
101
- try {
102
- copyAssetRecursively(assetManager, parentAssetPath, parentDir)
103
- if (targetPath.exists() && targetPath.isFile) {
104
- return targetPath.absolutePath
105
- }
106
- throw IllegalArgumentException("File not found after copying parent directory: $assetPath")
107
- } catch (dirException: Exception) {
108
- throw IllegalArgumentException(
109
- "Failed to extract asset file: $assetPath. Tried direct copy and directory copy.",
110
- dirException
111
- )
112
- }
113
- } else {
114
- throw IllegalArgumentException("Failed to extract asset file: $assetPath", e)
115
- }
116
- } catch (e: Exception) {
117
- throw IllegalArgumentException("Failed to extract asset file: $assetPath", e)
118
- }
119
- } else {
120
- if (targetPath.exists() && targetPath.isDirectory) {
121
- return targetPath.absolutePath
122
- }
123
- try {
124
- targetPath.mkdirs()
125
- copyAssetRecursively(assetManager, assetPath, targetPath)
126
- return targetPath.absolutePath
127
- } catch (e: Exception) {
128
- throw IllegalArgumentException("Failed to extract asset directory: $assetPath", e)
129
- }
130
- }
131
- }
132
-
133
- private fun copyAssetRecursively(
134
- assetManager: android.content.res.AssetManager,
135
- assetPath: String,
136
- targetDir: File
137
- ) {
138
- val assetFiles = assetManager.list(assetPath)
139
- ?: throw IllegalArgumentException("Asset path not found: $assetPath")
140
-
141
- for (fileName in assetFiles) {
142
- val assetFilePath = "$assetPath/$fileName"
143
- val targetFile = File(targetDir, fileName)
144
-
145
- try {
146
- val subFiles = assetManager.list(assetFilePath)
147
- if (subFiles != null && subFiles.isNotEmpty()) {
148
- targetFile.mkdirs()
149
- copyAssetRecursively(assetManager, assetFilePath, targetFile)
150
- } else {
151
- assetManager.open(assetFilePath).use { input ->
152
- FileOutputStream(targetFile).use { output ->
153
- input.copyTo(output)
154
- }
155
- }
156
- }
157
- } catch (e: Exception) {
158
- try {
159
- assetManager.open(assetFilePath).use { input ->
160
- FileOutputStream(targetFile).use { output ->
161
- input.copyTo(output)
162
- }
163
- }
164
- } catch (fileException: Exception) {
165
- throw IllegalArgumentException("Failed to copy asset: $assetFilePath", fileException)
166
- }
167
- }
168
- }
169
- }
170
-
171
- private fun resolveFilePath(filePath: String): String {
172
- val file = File(filePath)
173
- if (!file.exists()) {
174
- throw IllegalArgumentException("File path does not exist: $filePath")
175
- }
176
- if (!file.isDirectory) {
177
- throw IllegalArgumentException("Path is not a directory: $filePath")
178
- }
179
- return file.absolutePath
180
- }
181
-
182
- private fun resolveAutoPath(path: String): String {
183
- return try {
184
- resolveAssetPath(path)
185
- } catch (e: Exception) {
186
- try {
187
- resolveFilePath(path)
188
- } catch (fileException: Exception) {
189
- throw IllegalArgumentException(
190
- "Path not found as asset or file: $path. Asset error: ${e.message}, File error: ${fileException.message}",
191
- e
192
- )
193
- }
194
- }
195
- }
196
-
197
- private fun inferModelHint(folderName: String): String {
198
- val name = folderName.lowercase()
199
- val sttHints = listOf(
200
- "zipformer",
201
- "paraformer",
202
- "nemo",
203
- "parakeet",
204
- "whisper",
205
- "wenet",
206
- "sensevoice",
207
- "sense-voice",
208
- "sense",
209
- "funasr",
210
- "transducer",
211
- "ctc",
212
- "asr"
213
- )
214
- val ttsHints = listOf(
215
- "vits",
216
- "piper",
217
- "matcha",
218
- "kokoro",
219
- "kitten",
220
- "zipvoice",
221
- "melo",
222
- "coqui",
223
- "mms",
224
- "tts"
225
- )
226
-
227
- val isStt = sttHints.any { name.contains(it) }
228
- val isTts = ttsHints.any { name.contains(it) }
229
-
230
- return when {
231
- isStt && !isTts -> "stt"
232
- isTts && !isStt -> "tts"
233
- else -> "unknown"
234
- }
235
- }
236
- }
1
+ /**
2
+ * SherpaOnnxAssetHelper.kt
3
+ *
4
+ * Purpose: Asset and model path logic for the SherpaOnnx module: resolveModelPath (asset/file/auto),
5
+ * listAssetModels, listModelsAtPath, getAssetPackPath (PAD), and path/hint helpers. Aligns with
6
+ * iOS SherpaOnnx+Assets.mm. Used by SherpaOnnxModule.
7
+ */
8
+ package com.sherpaonnx
9
+
10
+ import android.util.Log
11
+ import com.facebook.react.bridge.Arguments
12
+ import com.facebook.react.bridge.Promise
13
+ import com.facebook.react.bridge.ReadableMap
14
+ import com.facebook.react.bridge.ReactApplicationContext
15
+ import com.google.android.play.core.assetpacks.AssetPackLocation
16
+ import com.google.android.play.core.assetpacks.AssetPackManagerFactory
17
+ import com.google.android.play.core.assetpacks.model.AssetPackStorageMethod
18
+ import java.io.File
19
+ import java.io.FileOutputStream
20
+
21
+ internal class SherpaOnnxAssetHelper(
22
+ private val context: ReactApplicationContext,
23
+ private val logTag: String
24
+ ) {
25
+ fun resolveModelPath(config: ReadableMap, promise: Promise) {
26
+ try {
27
+ val type = config.getString("type") ?: "auto"
28
+ val path = config.getString("path")
29
+ ?: throw IllegalArgumentException("Path is required")
30
+
31
+ Log.i(logTag, "resolveModelPath: type=$type, path=$path")
32
+
33
+ val resolvedPath = when (type) {
34
+ "asset" -> resolveAssetPath(path)
35
+ "file" -> resolveFilePath(path)
36
+ "auto" -> resolveAutoPath(path)
37
+ else -> throw IllegalArgumentException("Unknown path type: $type")
38
+ }
39
+
40
+ Log.i(logTag, "resolveModelPath: resolved=$resolvedPath")
41
+ promise.resolve(resolvedPath)
42
+ } catch (e: Exception) {
43
+ val errorMessage = "Failed to resolve model path: ${e.message ?: e.javaClass.simpleName}"
44
+ Log.e(logTag, errorMessage, e)
45
+ promise.reject("PATH_RESOLVE_ERROR", errorMessage, e)
46
+ }
47
+ }
48
+
49
+ fun listAssetModels(promise: Promise) {
50
+ try {
51
+ val assetManager = context.assets
52
+ val modelFolders = mutableListOf<String>()
53
+
54
+ try {
55
+ val items = assetManager.list("models") ?: emptyArray()
56
+ for (item in items) {
57
+ val subItems = assetManager.list("models/$item")
58
+ if (subItems != null && subItems.isNotEmpty()) {
59
+ modelFolders.add(item)
60
+ }
61
+ }
62
+ } catch (e: Exception) {
63
+ Log.w(logTag, "Could not list models directory: ${e.message}")
64
+ }
65
+
66
+ val result = Arguments.createArray()
67
+ modelFolders.forEach { folder ->
68
+ val modelMap = Arguments.createMap()
69
+ modelMap.putString("folder", folder)
70
+ modelMap.putString("hint", inferModelHint(folder))
71
+ result.pushMap(modelMap)
72
+ }
73
+
74
+ promise.resolve(result)
75
+ } catch (e: Exception) {
76
+ Log.e(logTag, "LIST_ASSETS_ERROR: Failed to list asset models: ${e.message}", e)
77
+ promise.reject("LIST_ASSETS_ERROR", "Failed to list asset models: ${e.message}", e)
78
+ }
79
+ }
80
+
81
+ fun listModelsAtPath(path: String, recursive: Boolean, promise: Promise) {
82
+ try {
83
+ val baseDir = File(path)
84
+ if (!baseDir.exists()) {
85
+ throw IllegalArgumentException("Path does not exist: $path")
86
+ }
87
+ if (!baseDir.isDirectory) {
88
+ throw IllegalArgumentException("Path is not a directory: $path")
89
+ }
90
+
91
+ val folders = mutableListOf<String>()
92
+
93
+ if (recursive) {
94
+ val basePath = baseDir.toPath()
95
+ baseDir.walkTopDown().forEach { file ->
96
+ if (file.isDirectory && file != baseDir) {
97
+ val rel = basePath.relativize(file.toPath()).toString()
98
+ .replace(File.separatorChar, '/')
99
+ if (rel.isNotEmpty()) {
100
+ folders.add(rel)
101
+ }
102
+ }
103
+ }
104
+ } else {
105
+ val children = baseDir.listFiles() ?: emptyArray()
106
+ for (child in children) {
107
+ if (child.isDirectory) {
108
+ folders.add(child.name)
109
+ }
110
+ }
111
+ }
112
+
113
+ val result = Arguments.createArray()
114
+ folders.distinct().forEach { folder ->
115
+ val hintName = folder.substringAfterLast('/')
116
+ val modelMap = Arguments.createMap()
117
+ modelMap.putString("folder", folder)
118
+ modelMap.putString("hint", inferModelHint(hintName))
119
+ result.pushMap(modelMap)
120
+ }
121
+
122
+ promise.resolve(result)
123
+ } catch (e: Exception) {
124
+ Log.e(logTag, "LIST_MODELS_ERROR: Failed to list models at path: ${e.message}", e)
125
+ promise.reject("LIST_MODELS_ERROR", "Failed to list models at path: ${e.message}", e)
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Returns the filesystem path to the "models" directory inside a Play Asset Delivery (PAD) pack,
131
+ * or null if the pack is not available.
132
+ */
133
+ fun getAssetPackPath(packName: String, promise: Promise) {
134
+ try {
135
+ Log.i(logTag, "getAssetPackPath: packName=$packName")
136
+ val assetPackManager = AssetPackManagerFactory.getInstance(context)
137
+ val location: AssetPackLocation? = assetPackManager.getPackLocation(packName)
138
+ if (location == null) {
139
+ Log.i(logTag, "getAssetPackPath: location is null for pack '$packName'")
140
+ promise.resolve(null)
141
+ return
142
+ }
143
+ Log.i(logTag, "getAssetPackPath: storageMethod=${location.packStorageMethod()}, " +
144
+ "assetsPath=${location.assetsPath()}, path=${location.path()}")
145
+ if (location.packStorageMethod() != AssetPackStorageMethod.STORAGE_FILES) {
146
+ Log.i(logTag, "getAssetPackPath: storage method is not STORAGE_FILES, returning null")
147
+ promise.resolve(null)
148
+ return
149
+ }
150
+ val assetsPath = location.assetsPath()
151
+ val path = location.path()
152
+ val modelsDir = when {
153
+ assetsPath != null && assetsPath.isNotEmpty() -> File(assetsPath, "models").absolutePath
154
+ path != null && path.isNotEmpty() -> File(path, "assets/models").absolutePath
155
+ else -> null
156
+ }
157
+ Log.i(logTag, "getAssetPackPath: resolved modelsDir=$modelsDir")
158
+ if (modelsDir != null) {
159
+ val dir = File(modelsDir)
160
+ Log.i(logTag, "getAssetPackPath: modelsDir exists=${dir.exists()}, isDir=${dir.isDirectory}")
161
+ if (dir.exists() && dir.isDirectory) {
162
+ val children = dir.listFiles()?.map { it.name } ?: emptyList()
163
+ Log.i(logTag, "getAssetPackPath: modelsDir contents=$children")
164
+ }
165
+ }
166
+ promise.resolve(modelsDir)
167
+ } catch (e: Exception) {
168
+ Log.w(logTag, "getAssetPackPath failed: ${e.message}")
169
+ promise.resolve(null)
170
+ }
171
+ }
172
+
173
+ private fun resolveAssetPath(assetPath: String): String {
174
+ Log.i(logTag, "resolveAssetPath: assetPath=$assetPath")
175
+ val assetManager = context.assets
176
+
177
+ val pathParts = assetPath.split("/")
178
+ val baseDir = if (pathParts.size > 1) pathParts[0] else "models"
179
+
180
+ val targetBaseDir = File(context.filesDir, baseDir)
181
+ targetBaseDir.mkdirs()
182
+ Log.i(logTag, "resolveAssetPath: targetBaseDir=${targetBaseDir.absolutePath}, exists=${targetBaseDir.exists()}")
183
+
184
+ val isFilePath = pathParts.any { it.contains(".") && !it.startsWith(".") }
185
+
186
+ val targetPath = if (isFilePath) {
187
+ File(targetBaseDir, pathParts.drop(1).joinToString("/"))
188
+ } else {
189
+ File(targetBaseDir, File(assetPath).name)
190
+ }
191
+
192
+ if (isFilePath) {
193
+ if (targetPath.exists() && targetPath.isFile) {
194
+ return targetPath.absolutePath
195
+ }
196
+ val parentDir = targetPath.parentFile ?: targetBaseDir
197
+ parentDir.mkdirs()
198
+
199
+ try {
200
+ assetManager.open(assetPath).use { input ->
201
+ FileOutputStream(targetPath).use { output ->
202
+ input.copyTo(output)
203
+ }
204
+ }
205
+ return targetPath.absolutePath
206
+ } catch (e: java.io.FileNotFoundException) {
207
+ val parentAssetPath = pathParts.dropLast(1).joinToString("/")
208
+ if (parentAssetPath.isNotEmpty()) {
209
+ try {
210
+ copyAssetRecursively(assetManager, parentAssetPath, parentDir)
211
+ if (targetPath.exists() && targetPath.isFile) {
212
+ return targetPath.absolutePath
213
+ }
214
+ throw IllegalArgumentException("File not found after copying parent directory: $assetPath")
215
+ } catch (dirException: Exception) {
216
+ throw IllegalArgumentException(
217
+ "Failed to extract asset file: $assetPath. Tried direct copy and directory copy.",
218
+ dirException
219
+ )
220
+ }
221
+ } else {
222
+ throw IllegalArgumentException("Failed to extract asset file: $assetPath", e)
223
+ }
224
+ } catch (e: Exception) {
225
+ throw IllegalArgumentException("Failed to extract asset file: $assetPath", e)
226
+ }
227
+ } else {
228
+ if (targetPath.exists() && targetPath.isDirectory) {
229
+ return targetPath.absolutePath
230
+ }
231
+ try {
232
+ targetPath.mkdirs()
233
+ copyAssetRecursively(assetManager, assetPath, targetPath)
234
+ return targetPath.absolutePath
235
+ } catch (e: Exception) {
236
+ throw IllegalArgumentException("Failed to extract asset directory: $assetPath", e)
237
+ }
238
+ }
239
+ }
240
+
241
+ private fun copyAssetRecursively(
242
+ assetManager: android.content.res.AssetManager,
243
+ assetPath: String,
244
+ targetDir: File
245
+ ) {
246
+ val assetFiles = assetManager.list(assetPath)
247
+ ?: throw IllegalArgumentException("Asset path not found: $assetPath")
248
+
249
+ for (fileName in assetFiles) {
250
+ val assetFilePath = "$assetPath/$fileName"
251
+ val targetFile = File(targetDir, fileName)
252
+
253
+ try {
254
+ val subFiles = assetManager.list(assetFilePath)
255
+ if (subFiles != null && subFiles.isNotEmpty()) {
256
+ targetFile.mkdirs()
257
+ copyAssetRecursively(assetManager, assetFilePath, targetFile)
258
+ } else {
259
+ assetManager.open(assetFilePath).use { input ->
260
+ FileOutputStream(targetFile).use { output ->
261
+ input.copyTo(output)
262
+ }
263
+ }
264
+ }
265
+ } catch (e: Exception) {
266
+ try {
267
+ assetManager.open(assetFilePath).use { input ->
268
+ FileOutputStream(targetFile).use { output ->
269
+ input.copyTo(output)
270
+ }
271
+ }
272
+ } catch (fileException: Exception) {
273
+ throw IllegalArgumentException("Failed to copy asset: $assetFilePath", fileException)
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
+ private fun resolveFilePath(filePath: String): String {
280
+ Log.i(logTag, "resolveFilePath: filePath=$filePath")
281
+ val file = File(filePath)
282
+ if (!file.exists()) {
283
+ Log.e(logTag, "resolveFilePath: path does not exist: $filePath")
284
+ throw IllegalArgumentException("File path does not exist: $filePath")
285
+ }
286
+ if (!file.isDirectory) {
287
+ Log.e(logTag, "resolveFilePath: path is not a directory: $filePath")
288
+ throw IllegalArgumentException("Path is not a directory: $filePath")
289
+ }
290
+ val children = file.listFiles()?.map { it.name } ?: emptyList()
291
+ Log.i(logTag, "resolveFilePath: resolved=${file.absolutePath}, contents=$children")
292
+ return file.absolutePath
293
+ }
294
+
295
+ private fun resolveAutoPath(path: String): String {
296
+ return try {
297
+ resolveAssetPath(path)
298
+ } catch (e: Exception) {
299
+ try {
300
+ resolveFilePath(path)
301
+ } catch (fileException: Exception) {
302
+ throw IllegalArgumentException(
303
+ "Path not found as asset or file: $path. Asset error: ${e.message}, File error: ${fileException.message}",
304
+ e
305
+ )
306
+ }
307
+ }
308
+ }
309
+
310
+ private fun inferModelHint(folderName: String): String {
311
+ val name = folderName.lowercase()
312
+ val sttHints = listOf(
313
+ "zipformer",
314
+ "paraformer",
315
+ "nemo",
316
+ "parakeet",
317
+ "whisper",
318
+ "wenet",
319
+ "sensevoice",
320
+ "sense-voice",
321
+ "sense",
322
+ "funasr",
323
+ "transducer",
324
+ "ctc",
325
+ "asr"
326
+ )
327
+ val ttsHints = listOf(
328
+ "vits",
329
+ "piper",
330
+ "matcha",
331
+ "kokoro",
332
+ "kitten",
333
+ "pocket",
334
+ "zipvoice",
335
+ "melo",
336
+ "coqui",
337
+ "mms",
338
+ "tts"
339
+ )
340
+
341
+ val isStt = sttHints.any { name.contains(it) }
342
+ val isTts = ttsHints.any { name.contains(it) }
343
+
344
+ return when {
345
+ isStt && !isTts -> "stt"
346
+ isTts && !isStt -> "tts"
347
+ else -> "unknown"
348
+ }
349
+ }
350
+ }