react-native-sherpa-onnx 0.2.0 → 0.3.0

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