react-native-sherpa-onnx 0.3.6 → 0.3.7

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 (222) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +89 -21
  3. package/SherpaOnnx.podspec +3 -0
  4. package/THIRD_PARTY_LICENSES/README.md +62 -0
  5. package/THIRD_PARTY_LICENSES/ffmpeg.txt +502 -0
  6. package/THIRD_PARTY_LICENSES/libarchive.txt +65 -0
  7. package/THIRD_PARTY_LICENSES/nvidia_omla.txt +181 -0
  8. package/THIRD_PARTY_LICENSES/onnxruntime.txt +21 -0
  9. package/THIRD_PARTY_LICENSES/opus.txt +44 -0
  10. package/THIRD_PARTY_LICENSES/sherpa-onnx.txt +201 -0
  11. package/THIRD_PARTY_LICENSES/shine.txt +482 -0
  12. package/THIRD_PARTY_LICENSES/zstd.txt +30 -0
  13. package/android/build.gradle +7 -3
  14. package/android/prebuilt-download.gradle +344 -152
  15. package/android/prebuilt-versions.gradle +1 -1
  16. package/android/src/main/assets/model_licenses/asr-models-license-status.csv +409 -0
  17. package/android/src/main/assets/model_licenses/qnn-asr-models-license-status.csv +695 -0
  18. package/android/src/main/assets/model_licenses/tts-models-license-status.csv +596 -0
  19. package/android/src/main/cpp/CMakeLists.txt +28 -10
  20. package/android/src/main/cpp/jni/archive/sherpa-onnx-archive-helper.cpp +2 -2
  21. package/android/src/main/cpp/jni/audio/sherpa-onnx-audio-convert-jni.cpp +268 -2
  22. package/android/src/main/cpp/jni/model_detect/sherpa-onnx-model-detect-tts.cpp +6 -2
  23. package/android/src/main/cpp/jni/model_detect/sherpa-onnx-validate-tts.cpp +4 -2
  24. package/android/src/main/java/com/sherpaonnx/SherpaOnnxArchiveHelper.kt +40 -10
  25. package/android/src/main/java/com/sherpaonnx/SherpaOnnxModule.kt +99 -0
  26. package/android/src/main/java/com/sherpaonnx/SherpaOnnxOnlineSttHelper.kt +4 -1
  27. package/android/src/main/java/com/sherpaonnx/SherpaOnnxTtsHelper.kt +112 -97
  28. package/ios/Resources/model_licenses/asr-models-license-status.csv +409 -0
  29. package/ios/Resources/model_licenses/qnn-asr-models-license-status.csv +695 -0
  30. package/ios/Resources/model_licenses/tts-models-license-status.csv +596 -0
  31. package/ios/SherpaOnnx+OnlineSTT.mm +2 -0
  32. package/ios/SherpaOnnx+PcmLiveStream.mm +2 -29
  33. package/ios/SherpaOnnx+TTS.mm +178 -20
  34. package/ios/SherpaOnnx.mm +54 -0
  35. package/ios/SherpaOnnxAudioConvert.h +10 -0
  36. package/ios/SherpaOnnxAudioConvert.mm +257 -1
  37. package/ios/archive/sherpa-onnx-archive-helper.h +3 -0
  38. package/ios/archive/sherpa-onnx-archive-helper.mm +39 -6
  39. package/ios/model_detect/sherpa-onnx-model-detect-tts.mm +13 -2
  40. package/ios/model_detect/sherpa-onnx-validate-tts.mm +4 -2
  41. package/ios/online_stt/sherpa-onnx-online-stt-wrapper.h +1 -0
  42. package/ios/online_stt/sherpa-onnx-online-stt-wrapper.mm +4 -0
  43. package/ios/tts/sherpa-onnx-tts-wrapper.h +37 -0
  44. package/ios/tts/sherpa-onnx-tts-wrapper.mm +149 -3
  45. package/lib/module/NativeSherpaOnnx.js.map +1 -1
  46. package/lib/module/audio/index.js +8 -0
  47. package/lib/module/audio/index.js.map +1 -1
  48. package/lib/module/download/ModelDownloadManager.js +10 -929
  49. package/lib/module/download/ModelDownloadManager.js.map +1 -1
  50. package/lib/module/download/activeModelOperations.js +26 -0
  51. package/lib/module/download/activeModelOperations.js.map +1 -0
  52. package/lib/module/download/background-downloader.d.js +2 -0
  53. package/lib/module/download/background-downloader.d.js.map +1 -0
  54. package/lib/module/download/bulkPurge.js +72 -0
  55. package/lib/module/download/bulkPurge.js.map +1 -0
  56. package/lib/module/download/checksumPrompt.js +19 -0
  57. package/lib/module/download/checksumPrompt.js.map +1 -0
  58. package/lib/module/download/constants.js +7 -0
  59. package/lib/module/download/constants.js.map +1 -0
  60. package/lib/module/download/downloadEvents.js +35 -0
  61. package/lib/module/download/downloadEvents.js.map +1 -0
  62. package/lib/module/download/downloadTask.js +385 -0
  63. package/lib/module/download/downloadTask.js.map +1 -0
  64. package/lib/module/download/ensureModel.js +89 -0
  65. package/lib/module/download/ensureModel.js.map +1 -0
  66. package/lib/module/download/index.js +4 -4
  67. package/lib/module/download/index.js.map +1 -1
  68. package/lib/module/download/localModels.js +151 -0
  69. package/lib/module/download/localModels.js.map +1 -0
  70. package/lib/module/download/modelExtraction.js +174 -0
  71. package/lib/module/download/modelExtraction.js.map +1 -0
  72. package/lib/module/download/paths.js +98 -0
  73. package/lib/module/download/paths.js.map +1 -0
  74. package/lib/module/download/postDownloadProcessing.js +206 -0
  75. package/lib/module/download/postDownloadProcessing.js.map +1 -0
  76. package/lib/module/download/protectedModelKeys.js +31 -0
  77. package/lib/module/download/protectedModelKeys.js.map +1 -0
  78. package/lib/module/download/registry.js +267 -0
  79. package/lib/module/download/registry.js.map +1 -0
  80. package/lib/module/download/retry.js +59 -0
  81. package/lib/module/download/retry.js.map +1 -0
  82. package/lib/module/download/types.js +17 -0
  83. package/lib/module/download/types.js.map +1 -0
  84. package/lib/module/download/validation.js +101 -5
  85. package/lib/module/download/validation.js.map +1 -1
  86. package/lib/module/{download → extraction}/extractTarBz2.js +3 -1
  87. package/lib/module/extraction/extractTarBz2.js.map +1 -0
  88. package/lib/module/{download → extraction}/extractTarZst.js +3 -1
  89. package/lib/module/extraction/extractTarZst.js.map +1 -0
  90. package/lib/module/extraction/index.js +3 -4
  91. package/lib/module/extraction/index.js.map +1 -1
  92. package/lib/module/index.js +1 -1
  93. package/lib/module/index.js.map +1 -1
  94. package/lib/module/licenses.js +63 -0
  95. package/lib/module/licenses.js.map +1 -0
  96. package/lib/module/stt/index.js +16 -2
  97. package/lib/module/stt/index.js.map +1 -1
  98. package/lib/module/stt/streaming.js +2 -0
  99. package/lib/module/stt/streaming.js.map +1 -1
  100. package/lib/module/stt/streamingTypes.js.map +1 -1
  101. package/lib/module/stt/types.js.map +1 -1
  102. package/lib/module/tts/index.js +20 -2
  103. package/lib/module/tts/index.js.map +1 -1
  104. package/lib/module/tts/streaming.js +4 -0
  105. package/lib/module/tts/streaming.js.map +1 -1
  106. package/lib/module/tts/types.js.map +1 -1
  107. package/lib/module/utils.js +16 -1
  108. package/lib/module/utils.js.map +1 -1
  109. package/lib/typescript/src/NativeSherpaOnnx.d.ts +33 -5
  110. package/lib/typescript/src/NativeSherpaOnnx.d.ts.map +1 -1
  111. package/lib/typescript/src/audio/index.d.ts +10 -0
  112. package/lib/typescript/src/audio/index.d.ts.map +1 -1
  113. package/lib/typescript/src/download/ModelDownloadManager.d.ts +10 -108
  114. package/lib/typescript/src/download/ModelDownloadManager.d.ts.map +1 -1
  115. package/lib/typescript/src/download/activeModelOperations.d.ts +6 -0
  116. package/lib/typescript/src/download/activeModelOperations.d.ts.map +1 -0
  117. package/lib/typescript/src/download/bulkPurge.d.ts +14 -0
  118. package/lib/typescript/src/download/bulkPurge.d.ts.map +1 -0
  119. package/lib/typescript/src/download/checksumPrompt.d.ts +3 -0
  120. package/lib/typescript/src/download/checksumPrompt.d.ts.map +1 -0
  121. package/lib/typescript/src/download/constants.d.ts +5 -0
  122. package/lib/typescript/src/download/constants.d.ts.map +1 -0
  123. package/lib/typescript/src/download/downloadEvents.d.ts +6 -0
  124. package/lib/typescript/src/download/downloadEvents.d.ts.map +1 -0
  125. package/lib/typescript/src/download/downloadTask.d.ts +20 -0
  126. package/lib/typescript/src/download/downloadTask.d.ts.map +1 -0
  127. package/lib/typescript/src/download/ensureModel.d.ts +26 -0
  128. package/lib/typescript/src/download/ensureModel.d.ts.map +1 -0
  129. package/lib/typescript/src/download/index.d.ts +7 -7
  130. package/lib/typescript/src/download/index.d.ts.map +1 -1
  131. package/lib/typescript/src/download/localModels.d.ts +15 -0
  132. package/lib/typescript/src/download/localModels.d.ts.map +1 -0
  133. package/lib/typescript/src/download/modelExtraction.d.ts +36 -0
  134. package/lib/typescript/src/download/modelExtraction.d.ts.map +1 -0
  135. package/lib/typescript/src/download/paths.d.ts +28 -0
  136. package/lib/typescript/src/download/paths.d.ts.map +1 -0
  137. package/lib/typescript/src/download/postDownloadProcessing.d.ts +19 -0
  138. package/lib/typescript/src/download/postDownloadProcessing.d.ts.map +1 -0
  139. package/lib/typescript/src/download/protectedModelKeys.d.ts +6 -0
  140. package/lib/typescript/src/download/protectedModelKeys.d.ts.map +1 -0
  141. package/lib/typescript/src/download/registry.d.ts +14 -0
  142. package/lib/typescript/src/download/registry.d.ts.map +1 -0
  143. package/lib/typescript/src/download/retry.d.ts +15 -0
  144. package/lib/typescript/src/download/retry.d.ts.map +1 -0
  145. package/lib/typescript/src/download/types.d.ts +96 -0
  146. package/lib/typescript/src/download/types.d.ts.map +1 -0
  147. package/lib/typescript/src/download/validation.d.ts +19 -0
  148. package/lib/typescript/src/download/validation.d.ts.map +1 -1
  149. package/lib/typescript/src/extraction/extractTarBz2.d.ts.map +1 -0
  150. package/lib/typescript/src/extraction/extractTarZst.d.ts.map +1 -0
  151. package/lib/typescript/src/index.d.ts +1 -0
  152. package/lib/typescript/src/index.d.ts.map +1 -1
  153. package/lib/typescript/src/licenses.d.ts +10 -0
  154. package/lib/typescript/src/licenses.d.ts.map +1 -0
  155. package/lib/typescript/src/stt/index.d.ts +4 -1
  156. package/lib/typescript/src/stt/index.d.ts.map +1 -1
  157. package/lib/typescript/src/stt/streaming.d.ts.map +1 -1
  158. package/lib/typescript/src/stt/streamingTypes.d.ts +5 -0
  159. package/lib/typescript/src/stt/streamingTypes.d.ts.map +1 -1
  160. package/lib/typescript/src/stt/types.d.ts +3 -1
  161. package/lib/typescript/src/stt/types.d.ts.map +1 -1
  162. package/lib/typescript/src/tts/index.d.ts +3 -1
  163. package/lib/typescript/src/tts/index.d.ts.map +1 -1
  164. package/lib/typescript/src/tts/streaming.d.ts.map +1 -1
  165. package/lib/typescript/src/tts/types.d.ts +6 -5
  166. package/lib/typescript/src/tts/types.d.ts.map +1 -1
  167. package/lib/typescript/src/utils.d.ts +5 -0
  168. package/lib/typescript/src/utils.d.ts.map +1 -1
  169. package/package.json +6 -1
  170. package/scripts/{check-model-csvs.sh → ci/check-model-csvs.sh} +9 -2
  171. package/scripts/ci/collect_all_sherpa_model_streams.sh +101 -0
  172. package/scripts/ci/collect_one_sherpa_release_stream.sh +189 -0
  173. package/scripts/ci/sherpa_asr_model_release_streams.json +21 -0
  174. package/scripts/ci/sherpa_tts_model_release_streams.json +13 -0
  175. package/scripts/ci/update_model_license_csv.sh +765 -0
  176. package/scripts/setup-ios-framework.sh +14 -11
  177. package/scripts/update_commercial_use.js +73 -0
  178. package/src/NativeSherpaOnnx.ts +36 -5
  179. package/src/audio/index.ts +20 -0
  180. package/src/download/ModelDownloadManager.ts +55 -1343
  181. package/src/download/activeModelOperations.ts +38 -0
  182. package/src/download/background-downloader.d.ts +43 -0
  183. package/src/download/bulkPurge.ts +102 -0
  184. package/src/download/checksumPrompt.ts +25 -0
  185. package/src/download/constants.ts +5 -0
  186. package/src/download/downloadEvents.ts +55 -0
  187. package/src/download/downloadTask.ts +497 -0
  188. package/src/download/ensureModel.ts +124 -0
  189. package/src/download/index.ts +19 -4
  190. package/src/download/localModels.ts +234 -0
  191. package/src/download/modelExtraction.ts +244 -0
  192. package/src/download/paths.ts +134 -0
  193. package/src/download/postDownloadProcessing.ts +292 -0
  194. package/src/download/protectedModelKeys.ts +30 -0
  195. package/src/download/registry.ts +404 -0
  196. package/src/download/retry.ts +76 -0
  197. package/src/download/types.ts +120 -0
  198. package/src/download/validation.ts +114 -8
  199. package/src/{download → extraction}/extractTarBz2.ts +3 -1
  200. package/src/{download → extraction}/extractTarZst.ts +3 -1
  201. package/src/extraction/index.ts +3 -7
  202. package/src/index.tsx +1 -0
  203. package/src/licenses.ts +100 -0
  204. package/src/stt/index.ts +20 -2
  205. package/src/stt/streaming.ts +3 -0
  206. package/src/stt/streamingTypes.ts +5 -0
  207. package/src/stt/types.ts +3 -1
  208. package/src/tts/index.ts +30 -2
  209. package/src/tts/streaming.ts +10 -0
  210. package/src/tts/types.ts +6 -5
  211. package/src/utils.ts +22 -1
  212. package/third_party/sherpa-onnx-prebuilt/ANDROID_RELEASE_TAG +1 -1
  213. package/third_party/sherpa-onnx-prebuilt/IOS_RELEASE_TAG +1 -1
  214. package/android/src/main/cpp/jni/tts/sherpa-onnx-tts-zipvoice-jni.cpp +0 -301
  215. package/android/src/main/java/com/sherpaonnx/ZipvoiceTtsWrapper.kt +0 -187
  216. package/lib/module/download/extractTarBz2.js.map +0 -1
  217. package/lib/module/download/extractTarZst.js.map +0 -1
  218. package/lib/typescript/src/download/extractTarBz2.d.ts.map +0 -1
  219. package/lib/typescript/src/download/extractTarZst.d.ts.map +0 -1
  220. package/scripts/check-qnn-support.sh +0 -78
  221. /package/lib/typescript/src/{download → extraction}/extractTarBz2.d.ts +0 -0
  222. /package/lib/typescript/src/{download → extraction}/extractTarZst.d.ts +0 -0
@@ -0,0 +1,497 @@
1
+ import {
2
+ createDownloadTask,
3
+ completeHandler,
4
+ getExistingDownloadTasks,
5
+ } from '@kesha-antonov/react-native-background-downloader';
6
+ import {
7
+ exists,
8
+ readFile,
9
+ mkdir,
10
+ writeFile,
11
+ stat,
12
+ unlink,
13
+ } from '@dr.pogodin/react-native-fs';
14
+ import { checkDiskSpace, removeDirectoryRecursive } from './validation';
15
+ import {
16
+ ModelCategory,
17
+ type ModelMetaBase,
18
+ type ChecksumIssue,
19
+ type DownloadProgress,
20
+ type DownloadResult,
21
+ type DownloadState,
22
+ } from './types';
23
+ import {
24
+ getModelsBaseDir,
25
+ getModelDir,
26
+ getArchivePath,
27
+ getReadyMarkerPath,
28
+ getDownloadStatePath,
29
+ getNativeAssetExtractedModelDir,
30
+ getTarArchivePath,
31
+ getOnnxPath,
32
+ } from './paths';
33
+ import { emitDownloadProgress } from './downloadEvents';
34
+ import { runPostDownloadProcessing } from './postDownloadProcessing';
35
+ import { getModelByIdByCategory } from './registry';
36
+ import { listDownloadedModelsByCategory } from './localModels';
37
+
38
+ function makeDownloadTaskId(category: ModelCategory, id: string): string {
39
+ return `${category}:${id}`;
40
+ }
41
+
42
+ const activeDownloadTasks = new Map<string, { stop: () => void }>();
43
+
44
+ export async function downloadModelByCategory<T extends ModelMetaBase>(
45
+ category: ModelCategory,
46
+ id: string,
47
+ opts?: {
48
+ onProgress?: (progress: DownloadProgress) => void;
49
+ overwrite?: boolean;
50
+ signal?: AbortSignal;
51
+ maxRetries?: number;
52
+ onChecksumIssue?: (issue: ChecksumIssue) => Promise<boolean>;
53
+ deleteArchiveAfterExtract?: boolean;
54
+ }
55
+ ): Promise<DownloadResult> {
56
+ const isAborted = () => Boolean(opts?.signal?.aborted);
57
+
58
+ if (opts?.signal?.aborted) {
59
+ const abortError = new Error('Download aborted');
60
+ abortError.name = 'AbortError';
61
+ throw abortError;
62
+ }
63
+
64
+ const model = await getModelByIdByCategory<T>(category, id);
65
+ if (!model) {
66
+ throw new Error(`Unknown model id: ${id}`);
67
+ }
68
+
69
+ const baseDir = getModelsBaseDir(category);
70
+ await mkdir(baseDir);
71
+
72
+ const downloadPath = getArchivePath(category, id, model.archiveExt);
73
+ const isArchive = model.archiveExt === 'tar.bz2';
74
+ const modelDir = getModelDir(category, id);
75
+
76
+ const sleep = (ms: number) =>
77
+ new Promise<void>((resolve) => {
78
+ setTimeout(resolve, ms);
79
+ });
80
+
81
+ const cleanupPartial = async () => {
82
+ if (!isArchive) return;
83
+ if (await exists(modelDir)) {
84
+ await unlink(modelDir);
85
+ }
86
+ };
87
+
88
+ const cleanupPartialWithRetry = async () => {
89
+ for (let attempt = 0; attempt < 4; attempt += 1) {
90
+ await cleanupPartial();
91
+ if (!(await exists(modelDir))) return;
92
+ await sleep(400);
93
+ }
94
+ if (await exists(modelDir)) {
95
+ console.warn(
96
+ `Model cleanup after abort did not fully complete for ${category}:${id}`
97
+ );
98
+ }
99
+ };
100
+
101
+ const diskSpaceCheck = await checkDiskSpace(model.bytes);
102
+ if (!diskSpaceCheck.success) {
103
+ throw new Error(`Insufficient disk space: ${diskSpaceCheck.message}`);
104
+ }
105
+
106
+ const statePath = getDownloadStatePath(category, id);
107
+
108
+ if (opts?.overwrite) {
109
+ if (await exists(modelDir)) await unlink(modelDir);
110
+ if (await exists(downloadPath)) await unlink(downloadPath);
111
+ if (await exists(statePath)) await unlink(statePath);
112
+ } else {
113
+ const readyMarkerExists = await exists(getReadyMarkerPath(category, id));
114
+ if (!readyMarkerExists && isArchive) {
115
+ if (await exists(modelDir)) await unlink(modelDir);
116
+ }
117
+ }
118
+
119
+ try {
120
+ const downloadState: DownloadState = {
121
+ modelId: id,
122
+ category,
123
+ phase: 'downloading',
124
+ startedAt: new Date().toISOString(),
125
+ archivePath: downloadPath,
126
+ model,
127
+ totalBytes: model.bytes,
128
+ };
129
+ await mkdir(getModelsBaseDir(category));
130
+ await writeFile(statePath, JSON.stringify(downloadState), 'utf8');
131
+
132
+ if (!isArchive) {
133
+ await mkdir(modelDir);
134
+ }
135
+
136
+ const taskId = makeDownloadTaskId(category, id);
137
+
138
+ return new Promise<DownloadResult>((resolve, reject) => {
139
+ let abortHandler: (() => void) | undefined;
140
+
141
+ const cleanup = () => {
142
+ if (abortHandler && opts?.signal) {
143
+ opts.signal.removeEventListener('abort', abortHandler);
144
+ abortHandler = undefined;
145
+ }
146
+ activeDownloadTasks.delete(taskId);
147
+ };
148
+
149
+ const task = createDownloadTask({
150
+ id: taskId,
151
+ url: model.downloadUrl,
152
+ destination: downloadPath,
153
+ metadata: {},
154
+ })
155
+ .progress(
156
+ ({
157
+ bytesDownloaded,
158
+ bytesTotal,
159
+ }: {
160
+ bytesDownloaded: number;
161
+ bytesTotal: number;
162
+ }) => {
163
+ if (isAborted()) return;
164
+ const total = bytesTotal ?? model.bytes ?? 0;
165
+ const percent = total > 0 ? (bytesDownloaded / total) * 100 : 0;
166
+ const progress: DownloadProgress = {
167
+ bytesDownloaded,
168
+ totalBytes: total,
169
+ percent,
170
+ phase: 'downloading',
171
+ };
172
+ opts?.onProgress?.(progress);
173
+ emitDownloadProgress(category, id, progress);
174
+ }
175
+ )
176
+ .done(async () => {
177
+ cleanup();
178
+ try {
179
+ const result = await runPostDownloadProcessing({
180
+ category,
181
+ id,
182
+ model,
183
+ downloadPath,
184
+ modelDir,
185
+ isArchive,
186
+ statePath,
187
+ signal: opts?.signal,
188
+ onChecksumIssue: opts?.onChecksumIssue,
189
+ deleteArchiveAfterExtract: opts?.deleteArchiveAfterExtract,
190
+ onProgress: opts?.onProgress,
191
+ getDownloadedList: () =>
192
+ listDownloadedModelsByCategory<ModelMetaBase>(category),
193
+ });
194
+ completeHandler(taskId);
195
+ resolve(result);
196
+ } catch (e) {
197
+ completeHandler(taskId);
198
+ reject(e);
199
+ }
200
+ })
201
+ .error(
202
+ ({ error, errorCode }: { error?: string; errorCode?: number }) => {
203
+ cleanup();
204
+ completeHandler(taskId);
205
+ (async () => {
206
+ try {
207
+ if (await exists(statePath)) await unlink(statePath);
208
+ } catch {
209
+ // ignore
210
+ }
211
+ })();
212
+ reject(
213
+ new Error(
214
+ typeof error === 'string' ? error : String(errorCode ?? error)
215
+ )
216
+ );
217
+ }
218
+ );
219
+
220
+ activeDownloadTasks.set(taskId, task);
221
+ if (opts?.signal) {
222
+ abortHandler = () => {
223
+ task.stop();
224
+ cleanup();
225
+ (async () => {
226
+ try {
227
+ if (await exists(statePath)) await unlink(statePath);
228
+ } catch {
229
+ // ignore
230
+ }
231
+ })();
232
+ const err = new Error('Download aborted');
233
+ err.name = 'AbortError';
234
+ reject(err);
235
+ };
236
+ opts.signal.addEventListener('abort', abortHandler);
237
+ }
238
+ task.start();
239
+ });
240
+ } catch (err) {
241
+ if ((err instanceof Error && err.name === 'AbortError') || isAborted()) {
242
+ await cleanupPartialWithRetry();
243
+ try {
244
+ if (await exists(statePath)) await unlink(statePath);
245
+ } catch {
246
+ // ignore
247
+ }
248
+ }
249
+ if (isArchive && !(err instanceof Error && err.name === 'AbortError')) {
250
+ try {
251
+ if (await exists(downloadPath)) {
252
+ const archiveStat = await stat(downloadPath);
253
+ if (model.bytes > 0 && archiveStat.size < model.bytes) {
254
+ console.warn(
255
+ `[Download] Deleting truncated archive for ${category}:${id} (${archiveStat.size}/${model.bytes})`
256
+ );
257
+ await unlink(downloadPath);
258
+ }
259
+ }
260
+ } catch {
261
+ // ignore
262
+ }
263
+ }
264
+ throw err;
265
+ }
266
+ }
267
+
268
+ export async function getIncompleteDownloads(
269
+ category: ModelCategory
270
+ ): Promise<DownloadState[]> {
271
+ const prefix = category + ':';
272
+ const states: DownloadState[] = [];
273
+
274
+ const existingTasks = await getExistingDownloadTasks();
275
+ for (const task of existingTasks) {
276
+ if (!task.id || !task.id.startsWith(prefix)) continue;
277
+ const modelId = task.id.slice(prefix.length);
278
+ const readyPath = getReadyMarkerPath(category, modelId);
279
+ if (await exists(readyPath)) continue;
280
+
281
+ const statePath = getDownloadStatePath(category, modelId);
282
+ let model: ModelMetaBase | undefined;
283
+ let totalBytes: number | undefined;
284
+ let archivePath: string | undefined;
285
+ let startedAt: string | undefined;
286
+
287
+ if (await exists(statePath)) {
288
+ try {
289
+ const raw = await readFile(statePath, 'utf8');
290
+ const fromFile = JSON.parse(raw) as DownloadState;
291
+ model = fromFile.model;
292
+ totalBytes = fromFile.totalBytes ?? fromFile.model?.bytes;
293
+ archivePath = fromFile.archivePath;
294
+ startedAt = fromFile.startedAt;
295
+ } catch {
296
+ // ignore
297
+ }
298
+ }
299
+ if (!model) {
300
+ const meta = await getModelByIdByCategory(category, modelId);
301
+ if (!meta) continue;
302
+ model = meta as ModelMetaBase;
303
+ totalBytes = model.bytes;
304
+ archivePath = getArchivePath(category, modelId, model.archiveExt);
305
+ }
306
+
307
+ let bytesDownloaded: number | undefined;
308
+ if (archivePath) {
309
+ try {
310
+ const st = await stat(archivePath);
311
+ if (st?.size != null && st.size >= 0) bytesDownloaded = st.size;
312
+ } catch {
313
+ // ignore
314
+ }
315
+ }
316
+
317
+ states.push({
318
+ modelId,
319
+ category,
320
+ phase: 'downloading',
321
+ startedAt: startedAt ?? new Date().toISOString(),
322
+ archivePath: archivePath ?? '',
323
+ model,
324
+ bytesDownloaded,
325
+ totalBytes: totalBytes ?? model.bytes,
326
+ });
327
+ }
328
+
329
+ return states;
330
+ }
331
+
332
+ export async function resumeDownload<T extends ModelMetaBase>(
333
+ category: ModelCategory,
334
+ id: string,
335
+ opts?: {
336
+ onProgress?: (progress: DownloadProgress) => void;
337
+ signal?: AbortSignal;
338
+ onChecksumIssue?: (issue: ChecksumIssue) => Promise<boolean>;
339
+ deleteArchiveAfterExtract?: boolean;
340
+ }
341
+ ): Promise<DownloadResult> {
342
+ const taskId = makeDownloadTaskId(category, id);
343
+ const existingTasks = await getExistingDownloadTasks();
344
+ const existing = existingTasks.find((t) => t.id === taskId);
345
+ if (!existing) {
346
+ return downloadModelByCategory<T>(category, id, {
347
+ onProgress: opts?.onProgress,
348
+ signal: opts?.signal,
349
+ onChecksumIssue: opts?.onChecksumIssue,
350
+ deleteArchiveAfterExtract: opts?.deleteArchiveAfterExtract,
351
+ });
352
+ }
353
+
354
+ const model = await getModelByIdByCategory<T>(category, id);
355
+ if (!model) throw new Error(`Unknown model id: ${id}`);
356
+ const downloadPath = getArchivePath(category, id, model.archiveExt);
357
+ const modelDir = getModelDir(category, id);
358
+ const isArchive = model.archiveExt === 'tar.bz2';
359
+ const statePath = getDownloadStatePath(category, id);
360
+ const isAborted = () => Boolean(opts?.signal?.aborted);
361
+
362
+ return new Promise<DownloadResult>((resolve, reject) => {
363
+ let abortHandler: (() => void) | undefined;
364
+
365
+ const cleanup = () => {
366
+ if (abortHandler && opts?.signal) {
367
+ opts.signal.removeEventListener('abort', abortHandler);
368
+ abortHandler = undefined;
369
+ }
370
+ activeDownloadTasks.delete(taskId);
371
+ };
372
+
373
+ existing
374
+ .progress(
375
+ ({
376
+ bytesDownloaded,
377
+ bytesTotal,
378
+ }: {
379
+ bytesDownloaded: number;
380
+ bytesTotal: number;
381
+ }) => {
382
+ if (isAborted()) return;
383
+ const total = bytesTotal ?? model.bytes ?? 0;
384
+ const percent = total > 0 ? (bytesDownloaded / total) * 100 : 0;
385
+ opts?.onProgress?.({
386
+ bytesDownloaded,
387
+ totalBytes: total,
388
+ percent,
389
+ phase: 'downloading',
390
+ });
391
+ emitDownloadProgress(category, id, {
392
+ bytesDownloaded,
393
+ totalBytes: total,
394
+ percent,
395
+ phase: 'downloading',
396
+ });
397
+ }
398
+ )
399
+ .done(async () => {
400
+ cleanup();
401
+ try {
402
+ const result = await runPostDownloadProcessing({
403
+ category,
404
+ id,
405
+ model,
406
+ downloadPath,
407
+ modelDir,
408
+ isArchive,
409
+ statePath,
410
+ signal: opts?.signal,
411
+ onChecksumIssue: opts?.onChecksumIssue,
412
+ deleteArchiveAfterExtract: opts?.deleteArchiveAfterExtract,
413
+ onProgress: opts?.onProgress,
414
+ getDownloadedList: () =>
415
+ listDownloadedModelsByCategory<ModelMetaBase>(category),
416
+ });
417
+ completeHandler(taskId);
418
+ resolve(result);
419
+ } catch (e) {
420
+ completeHandler(taskId);
421
+ reject(e);
422
+ }
423
+ })
424
+ .error(({ error, errorCode }: { error?: string; errorCode?: number }) => {
425
+ cleanup();
426
+ completeHandler(taskId);
427
+ (async () => {
428
+ try {
429
+ if (await exists(statePath)) await unlink(statePath);
430
+ } catch {
431
+ // ignore
432
+ }
433
+ })();
434
+ reject(
435
+ new Error(
436
+ typeof error === 'string' ? error : String(errorCode ?? error)
437
+ )
438
+ );
439
+ });
440
+
441
+ activeDownloadTasks.set(taskId, existing);
442
+ if (opts?.signal) {
443
+ abortHandler = () => {
444
+ existing.stop();
445
+ cleanup();
446
+ (async () => {
447
+ try {
448
+ if (await exists(statePath)) await unlink(statePath);
449
+ } catch {
450
+ // ignore
451
+ }
452
+ })();
453
+ const err = new Error('Download aborted');
454
+ err.name = 'AbortError';
455
+ reject(err);
456
+ };
457
+ opts.signal.addEventListener('abort', abortHandler);
458
+ }
459
+ existing.resume().catch(() => {});
460
+ });
461
+ }
462
+
463
+ export async function deleteIncompleteDownload(
464
+ category: ModelCategory,
465
+ id: string
466
+ ): Promise<void> {
467
+ const taskId = makeDownloadTaskId(category, id);
468
+ const existingTasks = await getExistingDownloadTasks();
469
+ const task = existingTasks.find((t) => t.id === taskId);
470
+ if (task) {
471
+ task.stop();
472
+ activeDownloadTasks.delete(taskId);
473
+ }
474
+
475
+ const modelDir = getModelDir(category, id);
476
+ if (await exists(modelDir)) {
477
+ await unlink(modelDir);
478
+ }
479
+ const tarPath = getTarArchivePath(category, id);
480
+ const onnxPath = getOnnxPath(category, id);
481
+ if (await exists(tarPath)) {
482
+ await unlink(tarPath);
483
+ }
484
+ if (await exists(onnxPath)) {
485
+ await unlink(onnxPath);
486
+ }
487
+ const statePath = getDownloadStatePath(category, id);
488
+ if (await exists(statePath)) {
489
+ await unlink(statePath);
490
+ }
491
+ await removeDirectoryRecursive(getNativeAssetExtractedModelDir(id));
492
+ }
493
+
494
+ /** Task ids in the form `category:modelId` for downloads currently tracked in JS (before post-processing). */
495
+ export function getActiveDownloadTaskKeys(): string[] {
496
+ return [...activeDownloadTasks.keys()];
497
+ }
@@ -0,0 +1,124 @@
1
+ import { exists, stat } from '@dr.pogodin/react-native-fs';
2
+ import type {
3
+ ModelCategory,
4
+ ModelMetaBase,
5
+ ChecksumIssue,
6
+ DownloadProgress,
7
+ } from './types';
8
+ import type { DownloadResult } from './types';
9
+ import { getArchivePath } from './paths';
10
+ import { getModelByIdByCategory } from './registry';
11
+ import {
12
+ isModelDownloadedByCategory,
13
+ getLocalModelPathByCategory,
14
+ deleteModelByCategory,
15
+ } from './localModels';
16
+ import { downloadModelByCategory } from './downloadTask';
17
+ import { getIncompleteDownloads, resumeDownload } from './downloadTask';
18
+ import {
19
+ getIncompleteExtractions,
20
+ resumeExtraction,
21
+ extractModelByCategory,
22
+ deleteIncompleteExtraction,
23
+ } from './modelExtraction';
24
+ import { deleteIncompleteDownload } from './downloadTask';
25
+
26
+ export type EnsureModelOptions = {
27
+ /** Progress callback (percent, phase, speed, eta). */
28
+ onProgress?: (progress: DownloadProgress) => void;
29
+ /** AbortController signal to cancel download or extraction. */
30
+ signal?: AbortSignal;
31
+ /** If true, remove existing model and any incomplete state, then download/extract from scratch. */
32
+ overwrite?: boolean;
33
+ /** Called on checksum mismatch; return true to keep the file. */
34
+ onChecksumIssue?: (issue: ChecksumIssue) => Promise<boolean>;
35
+ /** For archive models: if true (default), delete the archive after extraction to save space. */
36
+ deleteArchiveAfterExtract?: boolean;
37
+ };
38
+
39
+ /**
40
+ * Single entry point to ensure a model is available locally: handles download, extraction,
41
+ * and all edge cases (already ready, incomplete download, incomplete extraction, archive
42
+ * already present). Call this with category, id, and optional opts; the function decides
43
+ * whether to return the existing path, resume an interrupted operation, or start download/extraction.
44
+ *
45
+ * Use this as the main API when you only need "make this model ready"; the lower-level
46
+ * APIs (downloadModelByCategory, resumeDownload, extractModelByCategory, getIncompleteExtractions,
47
+ * etc.) remain available for advanced flows.
48
+ */
49
+ export async function ensureModelByCategory<T extends ModelMetaBase>(
50
+ category: ModelCategory,
51
+ id: string,
52
+ opts?: EnsureModelOptions
53
+ ): Promise<DownloadResult> {
54
+ const model = await getModelByIdByCategory<T>(category, id);
55
+ if (!model) {
56
+ throw new Error(`Unknown model id: ${id}`);
57
+ }
58
+
59
+ const isArchive = model.archiveExt === 'tar.bz2';
60
+
61
+ if (opts?.overwrite) {
62
+ await deleteModelByCategory(category, id);
63
+ await deleteIncompleteExtraction(category, id);
64
+ await deleteIncompleteDownload(category, id);
65
+ }
66
+
67
+ if (!opts?.overwrite && (await isModelDownloadedByCategory(category, id))) {
68
+ const localPath = await getLocalModelPathByCategory(category, id);
69
+ if (localPath) {
70
+ return { modelId: id, localPath };
71
+ }
72
+ }
73
+
74
+ if (isArchive) {
75
+ const incompleteExtractions = await getIncompleteExtractions(category);
76
+ const extractionState = incompleteExtractions.find((s) => s.modelId === id);
77
+ if (extractionState) {
78
+ return resumeExtraction<T>(category, id, {
79
+ onProgress: opts?.onProgress,
80
+ signal: opts?.signal,
81
+ onChecksumIssue: opts?.onChecksumIssue,
82
+ deleteArchiveAfterExtract: opts?.deleteArchiveAfterExtract,
83
+ });
84
+ }
85
+ }
86
+
87
+ const incompleteDownloads = await getIncompleteDownloads(category);
88
+ const downloadState = incompleteDownloads.find((s) => s.modelId === id);
89
+ if (downloadState) {
90
+ return resumeDownload<T>(category, id, {
91
+ onProgress: opts?.onProgress,
92
+ signal: opts?.signal,
93
+ onChecksumIssue: opts?.onChecksumIssue,
94
+ deleteArchiveAfterExtract: opts?.deleteArchiveAfterExtract,
95
+ });
96
+ }
97
+
98
+ if (isArchive) {
99
+ const downloadPath = getArchivePath(category, id, model.archiveExt);
100
+ if (await exists(downloadPath)) {
101
+ try {
102
+ const st = await stat(downloadPath);
103
+ if (model.bytes <= 0 || (st.size != null && st.size >= model.bytes)) {
104
+ return extractModelByCategory<T>(category, id, {
105
+ onProgress: opts?.onProgress,
106
+ signal: opts?.signal,
107
+ onChecksumIssue: opts?.onChecksumIssue,
108
+ deleteArchiveAfterExtract: opts?.deleteArchiveAfterExtract,
109
+ });
110
+ }
111
+ } catch {
112
+ // fall through to download
113
+ }
114
+ }
115
+ }
116
+
117
+ return downloadModelByCategory<T>(category, id, {
118
+ onProgress: opts?.onProgress,
119
+ overwrite: opts?.overwrite ?? false,
120
+ signal: opts?.signal,
121
+ onChecksumIssue: opts?.onChecksumIssue,
122
+ deleteArchiveAfterExtract: opts?.deleteArchiveAfterExtract,
123
+ });
124
+ }
@@ -1,7 +1,7 @@
1
- export { extractTarBz2 } from './extractTarBz2';
2
- export type { ExtractProgressEvent } from './extractTarBz2';
3
- export { extractTarZst } from './extractTarZst';
4
- export type { ExtractProgressEvent as ExtractZstProgressEvent } from './extractTarZst';
1
+ export { extractTarBz2 } from '../extraction/extractTarBz2';
2
+ export type { ExtractProgressEvent } from '../extraction/extractTarBz2';
3
+ export { extractTarZst } from '../extraction/extractTarZst';
4
+ export type { ExtractProgressEvent as ExtractZstProgressEvent } from '../extraction/extractTarZst';
5
5
  export {
6
6
  listModelsByCategory,
7
7
  refreshModelsByCategory,
@@ -19,7 +19,17 @@ export {
19
19
  updateModelLastUsed,
20
20
  listDownloadedModelsWithMetadata,
21
21
  cleanupLeastRecentlyUsed,
22
+ getIncompleteDownloads,
23
+ resumeDownload,
24
+ deleteIncompleteDownload,
25
+ extractModelByCategory,
26
+ getIncompleteExtractions,
27
+ resumeExtraction,
28
+ deleteIncompleteExtraction,
29
+ ensureModelByCategory,
22
30
  ModelCategory,
31
+ getProtectedModelKeysForBulkDelete,
32
+ purgeDownloadedModelArtifacts,
23
33
  } from './ModelDownloadManager';
24
34
  export type {
25
35
  ModelMetaBase,
@@ -31,12 +41,17 @@ export type {
31
41
  DownloadProgressListener,
32
42
  ModelsListUpdatedListener,
33
43
  DownloadResult,
44
+ DownloadState,
45
+ ExtractionState,
34
46
  ModelWithMetadata,
47
+ EnsureModelOptions,
48
+ PurgeDownloadedModelArtifactsResult,
35
49
  } from './ModelDownloadManager';
36
50
  export {
37
51
  validateChecksum,
38
52
  validateExtractedFiles,
39
53
  checkDiskSpace,
54
+ resolveActualModelDir,
40
55
  setExpectedFilesForCategory,
41
56
  getExpectedFilesForCategory,
42
57
  parseChecksumFile,