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
@@ -1,932 +1,13 @@
1
1
  "use strict";
2
2
 
3
- import { Alert, Platform } from 'react-native';
4
- import { DocumentDirectoryPath, exists, readFile, mkdir, writeFile, readDir, stat, unlink, downloadFile, stopDownload } from '@dr.pogodin/react-native-fs';
5
- import { extractTarBz2 } from "./extractTarBz2.js";
6
- import { parseChecksumFile, validateChecksum, validateExtractedFiles, checkDiskSpace } from "./validation.js";
7
- const RELEASE_API_BASE = 'https://api.github.com/repos/k2-fsa/sherpa-onnx/releases/tags';
8
- const CACHE_TTL_MINUTES = 24 * 60;
9
- const MODEL_ARCHIVE_EXT = '.tar.bz2';
10
- const MODEL_ONNX_EXT = '.onnx';
11
- export let ModelCategory = /*#__PURE__*/function (ModelCategory) {
12
- ModelCategory["Tts"] = "tts";
13
- ModelCategory["Stt"] = "stt";
14
- ModelCategory["Vad"] = "vad";
15
- ModelCategory["Diarization"] = "diarization";
16
- ModelCategory["Enhancement"] = "enhancement";
17
- ModelCategory["Separation"] = "separation";
18
- ModelCategory["Qnn"] = "qnn";
19
- return ModelCategory;
20
- }({});
21
-
22
- /** TTS model type for meta; 'unknown' when id could not be classified. */
23
-
24
- const promptChecksumFallback = issue => new Promise(resolve => {
25
- const reasonText = issue.reason === 'CHECKSUM_FAILED' ? 'Failed to compute checksum.' : 'Computed checksum does not match the expected value.';
26
- const body = `${reasonText}\n\n${issue.message}\n\nDo you want to keep the file and continue?`;
27
- Alert.alert('Checksum Problem', body, [{
28
- text: 'Delete and cancel',
29
- style: 'destructive',
30
- onPress: () => resolve(false)
31
- }, {
32
- text: 'Keep file',
33
- style: 'default',
34
- onPress: () => resolve(true)
35
- }]);
36
- });
37
- const memoryCacheByCategory = {};
38
- const checksumCacheByCategory = {};
39
- const downloadProgressListeners = new Set();
40
- const modelsListUpdatedListeners = new Set();
41
- export const subscribeDownloadProgress = listener => {
42
- downloadProgressListeners.add(listener);
43
- return () => {
44
- downloadProgressListeners.delete(listener);
45
- };
46
- };
47
- const emitDownloadProgress = (category, modelId, progress) => {
48
- for (const listener of downloadProgressListeners) {
49
- try {
50
- listener(category, modelId, progress);
51
- } catch (error) {
52
- console.warn('Download progress listener error:', error);
53
- }
54
- }
55
- };
56
- export const subscribeModelsListUpdated = listener => {
57
- modelsListUpdatedListeners.add(listener);
58
- return () => {
59
- modelsListUpdatedListeners.delete(listener);
60
- };
61
- };
62
- const emitModelsListUpdated = (category, models) => {
63
- for (const listener of modelsListUpdatedListeners) {
64
- try {
65
- listener(category, models);
66
- } catch (error) {
67
- console.warn('Models list listener error:', error);
68
- }
69
- }
70
- };
71
- const CATEGORY_CONFIG = {
72
- [ModelCategory.Tts]: {
73
- tag: 'tts-models',
74
- cacheFile: 'tts-models.json',
75
- baseDir: `${DocumentDirectoryPath}/sherpa-onnx/models/tts`
76
- },
77
- [ModelCategory.Stt]: {
78
- tag: 'asr-models',
79
- cacheFile: 'asr-models.json',
80
- baseDir: `${DocumentDirectoryPath}/sherpa-onnx/models/stt`
81
- },
82
- [ModelCategory.Vad]: {
83
- tag: 'asr-models',
84
- cacheFile: 'vad-models.json',
85
- baseDir: `${DocumentDirectoryPath}/sherpa-onnx/models/vad`
86
- },
87
- [ModelCategory.Diarization]: {
88
- tag: 'speaker-segmentation-models',
89
- cacheFile: 'diarization-models.json',
90
- baseDir: `${DocumentDirectoryPath}/sherpa-onnx/models/diarization`
91
- },
92
- [ModelCategory.Enhancement]: {
93
- tag: 'speech-enhancement-models',
94
- cacheFile: 'enhancement-models.json',
95
- baseDir: `${DocumentDirectoryPath}/sherpa-onnx/models/enhancement`
96
- },
97
- [ModelCategory.Separation]: {
98
- tag: 'source-separation-models',
99
- cacheFile: 'separation-models.json',
100
- baseDir: `${DocumentDirectoryPath}/sherpa-onnx/models/separation`
101
- },
102
- [ModelCategory.Qnn]: {
103
- tag: 'asr-models-qnn-binary',
104
- cacheFile: 'qnn-models.json',
105
- baseDir: `${DocumentDirectoryPath}/sherpa-onnx/models/qnn`
106
- }
107
- };
108
- function getCacheDir() {
109
- return `${DocumentDirectoryPath}/sherpa-onnx/cache`;
110
- }
111
- function getCachePath(category) {
112
- return `${getCacheDir()}/${CATEGORY_CONFIG[category].cacheFile}`;
113
- }
114
- function getModelsBaseDir(category) {
115
- return CATEGORY_CONFIG[category].baseDir;
116
- }
117
- function getModelDir(category, modelId) {
118
- return `${getModelsBaseDir(category)}/${modelId}`;
119
- }
120
- function getArchiveFilename(modelId, archiveExt) {
121
- return `${modelId}.${archiveExt}`;
122
- }
123
- function getArchivePath(category, modelId, archiveExt) {
124
- const filename = getArchiveFilename(modelId, archiveExt);
125
- if (archiveExt === 'onnx') {
126
- return `${getModelDir(category, modelId)}/${filename}`;
127
- }
128
- return `${getModelsBaseDir(category)}/${filename}`;
129
- }
130
- function getTarArchivePath(category, modelId) {
131
- return getArchivePath(category, modelId, 'tar.bz2');
132
- }
133
- function getOnnxPath(category, modelId) {
134
- return getArchivePath(category, modelId, 'onnx');
135
- }
136
- function getReadyMarkerPath(category, modelId) {
137
- return `${getModelDir(category, modelId)}/.ready`;
138
- }
139
- function getManifestPath(category, modelId) {
140
- return `${getModelDir(category, modelId)}/manifest.json`;
141
- }
142
- function getReleaseUrl(category) {
143
- return `${RELEASE_API_BASE}/${CATEGORY_CONFIG[category].tag}`;
144
- }
145
-
146
- /**
147
- * Retry helper with exponential backoff
148
- * @param fn - The async function to retry
149
- * @param options - Retry configuration
150
- * @returns The result of the function
151
- * @throws The last error if all retries fail or AbortError if aborted
152
- */
153
- async function retryWithBackoff(fn, options = {}) {
154
- const maxRetries = options.maxRetries ?? 3;
155
- const initialDelayMs = options.initialDelayMs ?? 1000;
156
- const maxDelayMs = options.maxDelayMs ?? 10000;
157
- const backoffFactor = options.backoffFactor ?? 2;
158
- let lastError;
159
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
160
- if (options.signal?.aborted) {
161
- const abortError = new Error('Operation aborted');
162
- abortError.name = 'AbortError';
163
- throw abortError;
164
- }
165
- try {
166
- return await fn();
167
- } catch (error) {
168
- lastError = error instanceof Error ? error : new Error(String(error));
169
-
170
- // Don't retry on abort
171
- if (lastError.name === 'AbortError' || options.signal?.aborted) {
172
- throw lastError;
173
- }
174
-
175
- // If this was the last attempt, throw the error
176
- if (attempt === maxRetries) {
177
- throw lastError;
178
- }
179
-
180
- // Calculate delay with exponential backoff
181
- const delayMs = Math.min(initialDelayMs * Math.pow(backoffFactor, attempt), maxDelayMs);
182
- console.warn(`Retry attempt ${attempt + 1}/${maxRetries} after ${delayMs}ms due to:`, lastError.message);
183
-
184
- // Wait before retrying
185
- await new Promise(resolve => setTimeout(resolve, delayMs));
186
- }
187
- }
188
- throw lastError ?? new Error('Retry failed with no error');
189
- }
190
- async function fetchChecksumsFromRelease(category) {
191
- // asr-models-qnn-binary has no checksum.txt; use GitHub API digest only (set in toGenericModelMeta).
192
- if (category === ModelCategory.Qnn) {
193
- return new Map();
194
- }
195
-
196
- // Return cached if available
197
- if (checksumCacheByCategory[category]) {
198
- return checksumCacheByCategory[category];
199
- }
200
- try {
201
- const checksums = await retryWithBackoff(async () => {
202
- const response = await fetch(`https://github.com/k2-fsa/sherpa-onnx/releases/download/${CATEGORY_CONFIG[category].tag}/checksum.txt`);
203
- if (!response.ok) {
204
- throw new Error(`Failed to fetch checksum.txt for ${category}: ${response.status}`);
205
- }
206
- const content = await response.text();
207
- return parseChecksumFile(content);
208
- }, {
209
- maxRetries: 3,
210
- initialDelayMs: 1000
211
- });
212
- checksumCacheByCategory[category] = checksums;
213
- return checksums;
214
- } catch (error) {
215
- console.warn(`SherpaOnnxChecksum: Error fetching checksums for ${category}:`, error);
216
- return new Map();
217
- }
218
- }
219
- function toTitleCase(value) {
220
- return value.split(/[-_\s]+/g).filter(Boolean).map(token => token[0].toUpperCase() + token.slice(1)).join(' ');
221
- }
222
- function deriveDisplayName(id) {
223
- const cleaned = id.replace(/^sherpa-onnx-/, '');
224
- return toTitleCase(cleaned);
225
- }
226
- function deriveType(id) {
227
- const lower = id.toLowerCase();
228
- if (lower.includes('vits')) return 'vits';
229
- if (lower.includes('kokoro')) return 'kokoro';
230
- if (lower.includes('matcha')) return 'matcha';
231
- if (lower.includes('kitten')) return 'kitten';
232
- if (lower.includes('pocket')) return 'pocket';
233
- if (lower.includes('zipvoice')) return 'zipvoice';
234
- return 'unknown';
235
- }
236
- function deriveQuantization(id) {
237
- const lower = id.toLowerCase();
238
- if (lower.includes('int8') && lower.includes('quant')) {
239
- return 'int8-quantized';
240
- }
241
- if (lower.includes('int8')) return 'int8';
242
- if (lower.includes('fp16')) return 'fp16';
243
- return 'unknown';
244
- }
245
- function deriveSizeTier(id) {
246
- const lower = id.toLowerCase();
247
- if (lower.includes('tiny')) return 'tiny';
248
- if (lower.includes('small')) return 'small';
249
- if (lower.includes('medium')) return 'medium';
250
- if (lower.includes('large')) return 'large';
251
- if (lower.includes('low')) return 'small';
252
- return 'unknown';
253
- }
254
- function deriveLanguages(id) {
255
- const tokens = id.split(/[-_]+/g);
256
- const languages = new Set();
257
- for (const token of tokens) {
258
- if (/^[a-z]{2}$/.test(token)) {
259
- languages.add(token);
260
- continue;
261
- }
262
- if (/^[a-z]{2}[A-Z]{2}$/.test(token)) {
263
- languages.add(token.slice(0, 2).toLowerCase());
264
- continue;
265
- }
266
- if (/^[a-z]{2}-[A-Z]{2}$/.test(token)) {
267
- languages.add(token.slice(0, 2).toLowerCase());
268
- }
269
- }
270
- return Array.from(languages);
271
- }
272
- function getAssetExtension(name) {
273
- if (name.endsWith(MODEL_ARCHIVE_EXT)) return 'tar.bz2';
274
- if (name.endsWith(MODEL_ONNX_EXT)) return 'onnx';
275
- return null;
276
- }
277
- function stripAssetExtension(name, ext) {
278
- const suffix = `.${ext}`;
279
- return name.endsWith(suffix) ? name.slice(0, -suffix.length) : name;
280
- }
281
- function isAssetSupportedForCategory(category, name, ext) {
282
- const lower = name.toLowerCase();
283
- switch (category) {
284
- case ModelCategory.Tts:
285
- return ext === 'tar.bz2';
286
- case ModelCategory.Stt:
287
- return ext === 'tar.bz2' && !lower.includes('vad');
288
- case ModelCategory.Vad:
289
- return ext === 'onnx' && lower.includes('vad');
290
- case ModelCategory.Diarization:
291
- return ext === 'tar.bz2';
292
- case ModelCategory.Enhancement:
293
- return ext === 'onnx';
294
- case ModelCategory.Separation:
295
- return ext === 'tar.bz2' || ext === 'onnx';
296
- case ModelCategory.Qnn:
297
- // asr-models-qnn-binary: e.g. sherpa-onnx-qnn-SM8850-binary-5-seconds-zipformer-ctc-zh-*.tar.bz2
298
- return ext === 'tar.bz2' && lower.includes('sherpa-onnx-qnn') && lower.includes('binary') && lower.includes('seconds');
299
- default:
300
- return false;
301
- }
302
- }
303
- function parseDigestSha256(value) {
304
- if (!value) return undefined;
305
- const match = value.match(/^sha256:([a-f0-9]{64})$/i);
306
- return match?.[1]?.toLowerCase();
307
- }
308
- function toTtsModelMeta(asset, archiveExt) {
309
- if (archiveExt !== 'tar.bz2') {
310
- return null;
311
- }
312
- const id = stripAssetExtension(asset.name, archiveExt);
313
- const type = deriveType(id);
314
- if (type === 'unknown') {
315
- console.warn('SherpaOnnxModelList: Unsupported model', id);
316
- }
317
- return {
318
- id,
319
- displayName: deriveDisplayName(id),
320
- type,
321
- languages: deriveLanguages(id),
322
- quantization: deriveQuantization(id),
323
- sizeTier: deriveSizeTier(id),
324
- downloadUrl: asset.browser_download_url,
325
- archiveExt,
326
- bytes: asset.size,
327
- sha256: parseDigestSha256(asset.digest),
328
- category: ModelCategory.Tts
329
- };
330
- }
331
- function toGenericModelMeta(category, asset, archiveExt) {
332
- const id = stripAssetExtension(asset.name, archiveExt);
333
- return {
334
- id,
335
- displayName: deriveDisplayName(id),
336
- downloadUrl: asset.browser_download_url,
337
- archiveExt,
338
- bytes: asset.size,
339
- sha256: parseDigestSha256(asset.digest),
340
- category
341
- };
342
- }
343
- function toModelMeta(category, asset) {
344
- const archiveExt = getAssetExtension(asset.name);
345
- if (!archiveExt) return null;
346
- if (!isAssetSupportedForCategory(category, asset.name, archiveExt)) {
347
- return null;
348
- }
349
- if (category === ModelCategory.Tts) {
350
- return toTtsModelMeta(asset, archiveExt);
351
- }
352
- return toGenericModelMeta(category, asset, archiveExt);
353
- }
354
- async function loadCacheFromDisk(category) {
355
- const memoryCache = memoryCacheByCategory[category];
356
- if (memoryCache) return memoryCache;
357
- const cachePath = getCachePath(category);
358
- const existsResult = await exists(cachePath);
359
- if (!existsResult) return null;
360
- const raw = await readFile(cachePath, 'utf8');
361
- const parsed = JSON.parse(raw);
362
- memoryCacheByCategory[category] = parsed;
363
- return parsed;
364
- }
365
- async function saveCache(category, payload) {
366
- const cacheDir = getCacheDir();
367
- await mkdir(cacheDir);
368
- await writeFile(getCachePath(category), JSON.stringify(payload), 'utf8');
369
- memoryCacheByCategory[category] = payload;
370
- }
371
- function isCacheFresh(payload, ttlMinutes) {
372
- const updated = new Date(payload.lastUpdated).getTime();
373
- if (!updated) return false;
374
- const ageMs = Date.now() - updated;
375
- return ageMs < ttlMinutes * 60 * 1000;
376
- }
377
- export async function listModelsByCategory(category) {
378
- const cache = await loadCacheFromDisk(category);
379
- return cache?.models ?? [];
380
- }
381
- export async function refreshModelsByCategory(category, options) {
382
- const ttl = options?.cacheTtlMinutes ?? CACHE_TTL_MINUTES;
383
- const cached = await loadCacheFromDisk(category);
384
- if (!options?.forceRefresh && cached && isCacheFresh(cached, ttl)) {
385
- return cached.models;
386
- }
387
- try {
388
- const body = await retryWithBackoff(async () => {
389
- const response = await fetch(getReleaseUrl(category));
390
- if (!response.ok) {
391
- throw new Error(`Failed to fetch models: ${response.status}`);
392
- }
393
- return response.json();
394
- }, {
395
- maxRetries: options?.maxRetries ?? 3,
396
- initialDelayMs: 1000,
397
- signal: options?.signal
398
- });
399
- const assets = Array.isArray(body?.assets) ? body.assets : [];
400
- const models = assets.map(asset => toModelMeta(category, {
401
- name: asset.name,
402
- size: asset.size,
403
- browser_download_url: asset.browser_download_url,
404
- digest: asset.digest
405
- })).filter(model => Boolean(model));
406
-
407
- // Load and attach SHA256 checksums from checksum.txt
408
- const checksums = await fetchChecksumsFromRelease(category);
409
- for (const model of models) {
410
- const archiveFilename = getArchiveFilename(model.id, model.archiveExt);
411
- const sha256 = checksums.get(archiveFilename);
412
- if (sha256) {
413
- model.sha256 = sha256;
414
- } else if (model.sha256) {
415
- model.sha256 = model.sha256.toLowerCase();
416
- }
417
- }
418
- const payload = {
419
- lastUpdated: new Date().toISOString(),
420
- models
421
- };
422
- await saveCache(category, payload);
423
- emitModelsListUpdated(category, models);
424
- return models;
425
- } catch (error) {
426
- // If retry failed and we have cached data, return it as fallback
427
- if (cached) {
428
- console.warn(`Failed to refresh models for ${category}, using cached data:`, error);
429
- return cached.models;
430
- }
431
- throw error;
432
- }
433
- }
434
- export async function getModelsCacheStatusByCategory(category) {
435
- const cached = await loadCacheFromDisk(category);
436
- if (!cached) {
437
- return {
438
- lastUpdated: null,
439
- source: 'cache'
440
- };
441
- }
442
- return {
443
- lastUpdated: cached.lastUpdated,
444
- source: 'cache'
445
- };
446
- }
447
- export async function getModelByIdByCategory(category, id) {
448
- const models = await listModelsByCategory(category);
449
- return models.find(model => model.id === id) ?? null;
450
- }
451
- export async function listDownloadedModelsByCategory(category) {
452
- const baseDir = getModelsBaseDir(category);
453
- const existsResult = await exists(baseDir);
454
- if (!existsResult) return [];
455
- const entries = await readDir(baseDir);
456
- const models = [];
457
- for (const entry of entries) {
458
- if (!entry.isDirectory()) continue;
459
- const manifestPath = getManifestPath(category, entry.name);
460
- const manifestExists = await exists(manifestPath);
461
- // Only list models that have a manifest (download + extraction fully complete).
462
- // Directories without manifest are still being extracted and must not appear in the list.
463
- if (!manifestExists) continue;
464
- try {
465
- const raw = await readFile(manifestPath, 'utf8');
466
- const manifest = JSON.parse(raw);
467
- if (manifest.model) {
468
- models.push(manifest.model);
469
- }
470
- } catch {
471
- // ignore invalid manifest
472
- }
473
- }
474
- return models;
475
- }
476
- export async function isModelDownloadedByCategory(category, id) {
477
- const readyPath = getReadyMarkerPath(category, id);
478
- return exists(readyPath);
479
- }
480
- export async function getLocalModelPathByCategory(category, id) {
481
- const ready = await isModelDownloadedByCategory(category, id);
482
- if (!ready) return null;
483
-
484
- // Update lastUsed timestamp when model is accessed
485
- await updateModelLastUsed(category, id);
486
- return getModelDir(category, id);
487
- }
488
- export async function downloadModelByCategory(category, id, opts) {
489
- const isAborted = () => Boolean(opts?.signal?.aborted);
490
- if (opts?.signal?.aborted) {
491
- const abortError = new Error('Download aborted');
492
- abortError.name = 'AbortError';
493
- throw abortError;
494
- }
495
- const model = await getModelByIdByCategory(category, id);
496
- if (!model) {
497
- throw new Error(`Unknown model id: ${id}`);
498
- }
499
- const baseDir = getModelsBaseDir(category);
500
- await mkdir(baseDir);
501
- const downloadPath = getArchivePath(category, id, model.archiveExt);
502
- const isArchive = model.archiveExt === 'tar.bz2';
503
- const modelDir = getModelDir(category, id);
504
- const sleep = ms => new Promise(resolve => {
505
- setTimeout(resolve, ms);
506
- });
507
- const cleanupPartial = async () => {
508
- if (!isArchive) {
509
- return;
510
- }
511
-
512
- // Only clean up extracted model dir, preserve archive for download resume
513
- if (await exists(modelDir)) {
514
- await unlink(modelDir);
515
- }
516
- };
517
- const cleanupPartialWithRetry = async () => {
518
- for (let attempt = 0; attempt < 4; attempt += 1) {
519
- await cleanupPartial();
520
- if (!(await exists(modelDir))) {
521
- return;
522
- }
523
- await sleep(400);
524
- }
525
- if (await exists(modelDir)) {
526
- console.warn(`Model cleanup after abort did not fully complete for ${category}:${id}`);
527
- }
528
- };
529
-
530
- // Step 1: Check available disk space
531
- const diskSpaceCheck = await checkDiskSpace(model.bytes);
532
- if (!diskSpaceCheck.success) {
533
- throw new Error(`Insufficient disk space: ${diskSpaceCheck.message}`);
534
- }
535
- if (opts?.overwrite) {
536
- if (await exists(modelDir)) {
537
- await unlink(modelDir);
538
- }
539
- if (await exists(downloadPath)) {
540
- await unlink(downloadPath);
541
- }
542
- } else {
543
- // Clean up incomplete extractions but preserve partial downloads for resume
544
- const readyMarkerExists = await exists(getReadyMarkerPath(category, id));
545
- if (!readyMarkerExists) {
546
- if (isArchive) {
547
- // No ready marker found; only clean up extracted model dir
548
- // Keep archive file to support download resume
549
- if (await exists(modelDir)) {
550
- // Removing partial model dir
551
- await unlink(modelDir);
552
- }
553
- }
554
- }
555
- }
556
- try {
557
- // Step 2: Download archive or onnx file (with resume support)
558
- if (!isArchive) {
559
- await mkdir(modelDir);
560
- }
561
- const archiveExists = await exists(downloadPath);
562
- let partialDownload = false;
563
- if (archiveExists) {
564
- // Check if this is a complete download or partial
565
- const statResult = await stat(downloadPath);
566
- const currentSize = statResult.size;
567
- if (currentSize < model.bytes) {
568
- partialDownload = true;
569
- console.log(`[Download] Resuming partial download for ${category}:${id} (${currentSize}/${model.bytes} bytes)`);
570
- }
571
- }
572
- if (!archiveExists || partialDownload) {
573
- const maxRetries = opts?.maxRetries ?? 2;
574
- await retryWithBackoff(async () => {
575
- const downloadStartTime = Date.now();
576
- const download = downloadFile({
577
- fromUrl: model.downloadUrl,
578
- toFile: downloadPath,
579
- progressDivider: 1,
580
- resumable: () => {
581
- // iOS only: Called when download is resumed
582
- console.log(`[Download] Resuming download for ${category}:${id}`);
583
- },
584
- progress: data => {
585
- if (isAborted()) {
586
- return;
587
- }
588
- const total = data.contentLength || model.bytes || 0;
589
- const percent = total > 0 ? data.bytesWritten / total * 100 : 0;
590
-
591
- // Calculate speed and ETA
592
- const now = Date.now();
593
- const elapsedSeconds = (now - downloadStartTime) / 1000;
594
- let speed;
595
- let eta;
596
- if (elapsedSeconds > 0.5) {
597
- // Calculate overall speed (bytes per second)
598
- speed = data.bytesWritten / elapsedSeconds;
599
-
600
- // Calculate ETA based on current speed
601
- const remainingBytes = total - data.bytesWritten;
602
- if (speed > 0) {
603
- eta = remainingBytes / speed;
604
- }
605
- }
606
- const progress = {
607
- bytesDownloaded: data.bytesWritten,
608
- totalBytes: total,
609
- percent,
610
- phase: 'downloading',
611
- speed,
612
- eta
613
- };
614
- opts?.onProgress?.(progress);
615
- emitDownloadProgress(category, id, progress);
616
- }
617
- });
618
- let downloadFinished = false;
619
- let aborted = false;
620
- const onAbort = () => {
621
- aborted = true;
622
- if (downloadFinished) return;
623
- try {
624
- stopDownload(download.jobId);
625
- } catch {
626
- // Swallow stop errors to avoid crashing the app on cancel.
627
- }
628
- };
629
- if (opts?.signal) {
630
- opts.signal.addEventListener('abort', onAbort);
631
- }
632
- let result;
633
- try {
634
- result = await download.promise;
635
- } finally {
636
- downloadFinished = true;
637
- if (opts?.signal) {
638
- opts.signal.removeEventListener('abort', onAbort);
639
- }
640
- }
641
- if (aborted || opts?.signal?.aborted) {
642
- const abortError = new Error('Download aborted');
643
- abortError.name = 'AbortError';
644
- throw abortError;
645
- }
646
- if (result.statusCode && result.statusCode >= 400) {
647
- // For certain errors, delete partial download as resume won't help
648
- const isNonResumableError = result.statusCode === 404 ||
649
- // Not found
650
- result.statusCode === 410 ||
651
- // Gone
652
- result.statusCode === 451 ||
653
- // Unavailable for legal reasons
654
- result.statusCode === 416; // Range not satisfiable
655
- if (isNonResumableError && (await exists(downloadPath))) {
656
- console.warn(`[Download] Non-resumable error ${result.statusCode}, removing partial download`);
657
- await unlink(downloadPath);
658
- }
659
- throw new Error(`Download failed: ${result.statusCode}`);
660
- }
661
- }, {
662
- maxRetries,
663
- initialDelayMs: 2000,
664
- signal: opts?.signal
665
- });
666
- }
667
- if (opts?.signal?.aborted) {
668
- const abortError = new Error('Download aborted');
669
- abortError.name = 'AbortError';
670
- throw abortError;
671
- }
672
- let extractResult = null;
673
- /** Total uncompressed bytes from libarchive progress; used for manifest.sizeOnDisk. */
674
- let extractedTotalBytes = 0;
675
- if (isArchive) {
676
- await mkdir(modelDir);
677
- const extractStartTime = Date.now();
678
- extractResult = await extractTarBz2(downloadPath, modelDir, true, evt => {
679
- if (isAborted()) {
680
- return;
681
- }
682
- if (evt.totalBytes > 0) extractedTotalBytes = evt.totalBytes;
683
- if (model.bytes > 0) {
684
- // Calculate extraction speed and ETA
685
- const now = Date.now();
686
- const elapsedSeconds = (now - extractStartTime) / 1000;
687
- let speed;
688
- let eta;
689
- if (elapsedSeconds > 0.5) {
690
- speed = evt.bytes / elapsedSeconds;
691
- const remainingBytes = evt.totalBytes - evt.bytes;
692
- if (speed > 0) {
693
- eta = remainingBytes / speed;
694
- }
695
- }
696
- const progress = {
697
- bytesDownloaded: evt.bytes,
698
- totalBytes: evt.totalBytes,
699
- percent: evt.percent,
700
- phase: 'extracting',
701
- speed,
702
- eta
703
- };
704
- opts?.onProgress?.(progress);
705
- emitDownloadProgress(category, id, progress);
706
- }
707
- }, opts?.signal);
708
- }
709
-
710
- // Step 3: Validate checksum if available
711
- if (model.sha256) {
712
- const expectedSha = model.sha256.toLowerCase();
713
- let issue = null;
714
- if (isArchive) {
715
- const nativeSha = extractResult?.sha256?.toLowerCase();
716
- if (!nativeSha) {
717
- issue = {
718
- category,
719
- id,
720
- archivePath: downloadPath,
721
- expected: model.sha256,
722
- message: 'Native SHA-256 not available after extraction.',
723
- reason: 'CHECKSUM_FAILED'
724
- };
725
- } else if (nativeSha !== expectedSha) {
726
- issue = {
727
- category,
728
- id,
729
- archivePath: downloadPath,
730
- expected: model.sha256,
731
- message: `Checksum mismatch: expected ${model.sha256}, got ${extractResult?.sha256}`,
732
- reason: 'CHECKSUM_MISMATCH'
733
- };
734
- }
735
- } else {
736
- const checksumResult = await validateChecksum(downloadPath, expectedSha);
737
- if (!checksumResult.success) {
738
- issue = {
739
- category,
740
- id,
741
- archivePath: downloadPath,
742
- expected: model.sha256,
743
- message: checksumResult.message ?? 'Checksum validation failed.',
744
- reason: checksumResult.error === 'CHECKSUM_MISMATCH' ? 'CHECKSUM_MISMATCH' : 'CHECKSUM_FAILED'
745
- };
746
- }
747
- }
748
- if (issue) {
749
- const keepFile = opts?.onChecksumIssue ? await opts.onChecksumIssue(issue) : await promptChecksumFallback(issue);
750
- if (!keepFile) {
751
- if (await exists(modelDir)) {
752
- await unlink(modelDir);
753
- }
754
- if (await exists(downloadPath)) {
755
- await unlink(downloadPath);
756
- }
757
- throw new Error(`Checksum validation failed: ${issue.message}`);
758
- }
759
- }
760
- }
761
- if (opts?.signal?.aborted) {
762
- const abortError = new Error('Download aborted');
763
- abortError.name = 'AbortError';
764
- throw abortError;
765
- }
766
-
767
- // Step 4: Validate extracted files exist
768
- const filesValidation = await validateExtractedFiles(modelDir, category);
769
- if (!filesValidation.success) {
770
- // Clean up failed extraction
771
- await unlink(modelDir);
772
- throw new Error(`Extracted files validation failed: ${filesValidation.message}`);
773
- }
774
- await writeFile(getReadyMarkerPath(category, id), 'ready', 'utf8');
775
- const now = new Date().toISOString();
776
- let sizeOnDisk;
777
- if (isArchive && extractedTotalBytes > 0) {
778
- sizeOnDisk = extractedTotalBytes;
779
- } else if (!isArchive) {
780
- try {
781
- const statResult = await stat(downloadPath);
782
- sizeOnDisk = statResult.size;
783
- } catch {
784
- // ignore
785
- }
786
- }
787
- await writeFile(getManifestPath(category, id), JSON.stringify({
788
- downloadedAt: now,
789
- lastUsed: now,
790
- model,
791
- sizeOnDisk
792
- }), 'utf8');
793
-
794
- // Notify subscribers (e.g. STT/TTS screens) so the model list updates without leaving the screen.
795
- const list = await listDownloadedModelsByCategory(category);
796
- emitModelsListUpdated(category, list);
797
- return {
798
- modelId: id,
799
- localPath: modelDir
800
- };
801
- } catch (err) {
802
- if (err instanceof Error && err.name === 'AbortError' || isAborted()) {
803
- await cleanupPartialWithRetry();
804
- }
805
- throw err;
806
- }
807
- }
808
-
809
- /**
810
- * Update the lastUsed timestamp for a downloaded model
811
- */
812
- export async function updateModelLastUsed(category, id) {
813
- const manifestPath = getManifestPath(category, id);
814
- const existsResult = await exists(manifestPath);
815
- if (!existsResult) return;
816
- try {
817
- const raw = await readFile(manifestPath, 'utf8');
818
- const manifest = JSON.parse(raw);
819
- manifest.lastUsed = new Date().toISOString();
820
- await writeFile(manifestPath, JSON.stringify(manifest), 'utf8');
821
- } catch (error) {
822
- console.warn(`Failed to update lastUsed for ${category}:${id}:`, error);
823
- }
824
- }
825
-
826
- /**
827
- * Get all downloaded models with LRU metadata
828
- */
829
- export async function listDownloadedModelsWithMetadata(category) {
830
- const baseDir = getModelsBaseDir(category);
831
- const existsResult = await exists(baseDir);
832
- if (!existsResult) return [];
833
- const entries = await readDir(baseDir);
834
- const modelsWithMetadata = [];
835
- for (const entry of entries) {
836
- if (!entry.isDirectory()) continue;
837
- const manifestPath = getManifestPath(category, entry.name);
838
- const manifestExists = await exists(manifestPath);
839
- if (manifestExists) {
840
- try {
841
- const raw = await readFile(manifestPath, 'utf8');
842
- const manifest = JSON.parse(raw);
843
- if (manifest.model) {
844
- modelsWithMetadata.push({
845
- model: manifest.model,
846
- downloadedAt: manifest.downloadedAt,
847
- lastUsed: manifest.lastUsed ?? null,
848
- sizeOnDisk: manifest.sizeOnDisk ?? entry.size
849
- });
850
- }
851
- } catch (error) {
852
- console.warn(`Failed to read manifest for ${category}:${entry.name}:`, error);
853
- }
854
- }
855
- }
856
- return modelsWithMetadata;
857
- }
858
-
859
- /**
860
- * Remove least recently used models to free up disk space
861
- * @param category - Model category
862
- * @param targetBytes - Target amount of bytes to free (optional)
863
- * @param maxModelsToDelete - Maximum number of models to delete (default: no limit)
864
- * @returns Array of deleted model IDs
865
- */
866
- export async function cleanupLeastRecentlyUsed(category, options) {
867
- const modelsWithMetadata = await listDownloadedModelsWithMetadata(category);
868
- if (modelsWithMetadata.length === 0) {
869
- return [];
870
- }
871
-
872
- // Keep at least this many models
873
- const keepCount = options?.keepCount ?? 1;
874
- if (modelsWithMetadata.length <= keepCount) {
875
- return [];
876
- }
877
-
878
- // Sort by lastUsed (oldest first), then by downloadedAt if no lastUsed
879
- const sorted = modelsWithMetadata.sort((a, b) => {
880
- const aTime = a.lastUsed ?? a.downloadedAt;
881
- const bTime = b.lastUsed ?? b.downloadedAt;
882
- return new Date(aTime).getTime() - new Date(bTime).getTime();
883
- });
884
- const deletedIds = [];
885
- let bytesFreed = 0;
886
- const targetBytes = options?.targetBytes ?? 0;
887
- const maxToDelete = options?.maxModelsToDelete ?? sorted.length - keepCount;
888
- for (let i = 0; i < sorted.length - keepCount && i < maxToDelete; i++) {
889
- const item = sorted[i];
890
- if (!item) continue;
891
- try {
892
- await deleteModelByCategory(category, item.model.id);
893
- deletedIds.push(item.model.id);
894
- bytesFreed += item.sizeOnDisk ?? 0;
895
- console.log(`[LRU Cleanup] Deleted ${category}:${item.model.id} (freed ${(item.sizeOnDisk ?? 0) / 1024 / 1024} MB)`);
896
- if (targetBytes > 0 && bytesFreed >= targetBytes) {
897
- break;
898
- }
899
- } catch (error) {
900
- console.warn(`[LRU Cleanup] Failed to delete ${category}:${item.model.id}:`, error);
901
- }
902
- }
903
- return deletedIds;
904
- }
905
- export async function deleteModelByCategory(category, id) {
906
- const modelDir = getModelDir(category, id);
907
- const tarPath = getTarArchivePath(category, id);
908
- const onnxPath = getOnnxPath(category, id);
909
- if (await exists(modelDir)) {
910
- await unlink(modelDir);
911
- }
912
- if (await exists(tarPath)) {
913
- await unlink(tarPath);
914
- }
915
- if (await exists(onnxPath)) {
916
- await unlink(onnxPath);
917
- }
918
- }
919
- export async function clearModelCacheByCategory(category) {
920
- const cachePath = getCachePath(category);
921
- if (await exists(cachePath)) {
922
- await unlink(cachePath);
923
- }
924
- delete memoryCacheByCategory[category];
925
- }
926
- export async function getDownloadStorageBase() {
927
- if (Platform.OS === 'ios') {
928
- return DocumentDirectoryPath;
929
- }
930
- return DocumentDirectoryPath;
931
- }
3
+ // Re-export types and enum for public API
4
+ export { ModelCategory } from "./types.js";
5
+ export { subscribeDownloadProgress, subscribeModelsListUpdated } from "./downloadEvents.js";
6
+ export { listModelsByCategory, refreshModelsByCategory, getModelsCacheStatusByCategory, getModelByIdByCategory } from "./registry.js";
7
+ export { listDownloadedModelsByCategory, isModelDownloadedByCategory, getLocalModelPathByCategory, updateModelLastUsed, listDownloadedModelsWithMetadata, cleanupLeastRecentlyUsed, deleteModelByCategory, clearModelCacheByCategory, getDownloadStorageBase } from "./localModels.js";
8
+ export { downloadModelByCategory, getIncompleteDownloads, resumeDownload, deleteIncompleteDownload } from "./downloadTask.js";
9
+ export { extractModelByCategory, getIncompleteExtractions, resumeExtraction, deleteIncompleteExtraction } from "./modelExtraction.js";
10
+ export { ensureModelByCategory } from "./ensureModel.js";
11
+ export { getProtectedModelKeysForBulkDelete } from "./protectedModelKeys.js";
12
+ export { purgeDownloadedModelArtifacts } from "./bulkPurge.js";
932
13
  //# sourceMappingURL=ModelDownloadManager.js.map