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.
- package/LICENSE +1 -0
- package/README.md +89 -21
- package/SherpaOnnx.podspec +3 -0
- package/THIRD_PARTY_LICENSES/README.md +62 -0
- package/THIRD_PARTY_LICENSES/ffmpeg.txt +502 -0
- package/THIRD_PARTY_LICENSES/libarchive.txt +65 -0
- package/THIRD_PARTY_LICENSES/nvidia_omla.txt +181 -0
- package/THIRD_PARTY_LICENSES/onnxruntime.txt +21 -0
- package/THIRD_PARTY_LICENSES/opus.txt +44 -0
- package/THIRD_PARTY_LICENSES/sherpa-onnx.txt +201 -0
- package/THIRD_PARTY_LICENSES/shine.txt +482 -0
- package/THIRD_PARTY_LICENSES/zstd.txt +30 -0
- package/android/build.gradle +7 -3
- package/android/prebuilt-download.gradle +344 -152
- package/android/prebuilt-versions.gradle +1 -1
- package/android/src/main/assets/model_licenses/asr-models-license-status.csv +409 -0
- package/android/src/main/assets/model_licenses/qnn-asr-models-license-status.csv +695 -0
- package/android/src/main/assets/model_licenses/tts-models-license-status.csv +596 -0
- package/android/src/main/cpp/CMakeLists.txt +28 -10
- package/android/src/main/cpp/jni/archive/sherpa-onnx-archive-helper.cpp +2 -2
- package/android/src/main/cpp/jni/audio/sherpa-onnx-audio-convert-jni.cpp +268 -2
- package/android/src/main/cpp/jni/model_detect/sherpa-onnx-model-detect-tts.cpp +6 -2
- package/android/src/main/cpp/jni/model_detect/sherpa-onnx-validate-tts.cpp +4 -2
- package/android/src/main/java/com/sherpaonnx/SherpaOnnxArchiveHelper.kt +40 -10
- package/android/src/main/java/com/sherpaonnx/SherpaOnnxModule.kt +99 -0
- package/android/src/main/java/com/sherpaonnx/SherpaOnnxOnlineSttHelper.kt +4 -1
- package/android/src/main/java/com/sherpaonnx/SherpaOnnxTtsHelper.kt +112 -97
- package/ios/Resources/model_licenses/asr-models-license-status.csv +409 -0
- package/ios/Resources/model_licenses/qnn-asr-models-license-status.csv +695 -0
- package/ios/Resources/model_licenses/tts-models-license-status.csv +596 -0
- package/ios/SherpaOnnx+OnlineSTT.mm +2 -0
- package/ios/SherpaOnnx+PcmLiveStream.mm +2 -29
- package/ios/SherpaOnnx+TTS.mm +178 -20
- package/ios/SherpaOnnx.mm +54 -0
- package/ios/SherpaOnnxAudioConvert.h +10 -0
- package/ios/SherpaOnnxAudioConvert.mm +257 -1
- package/ios/archive/sherpa-onnx-archive-helper.h +3 -0
- package/ios/archive/sherpa-onnx-archive-helper.mm +39 -6
- package/ios/model_detect/sherpa-onnx-model-detect-tts.mm +13 -2
- package/ios/model_detect/sherpa-onnx-validate-tts.mm +4 -2
- package/ios/online_stt/sherpa-onnx-online-stt-wrapper.h +1 -0
- package/ios/online_stt/sherpa-onnx-online-stt-wrapper.mm +4 -0
- package/ios/tts/sherpa-onnx-tts-wrapper.h +37 -0
- package/ios/tts/sherpa-onnx-tts-wrapper.mm +149 -3
- package/lib/module/NativeSherpaOnnx.js.map +1 -1
- package/lib/module/audio/index.js +8 -0
- package/lib/module/audio/index.js.map +1 -1
- package/lib/module/download/ModelDownloadManager.js +10 -929
- package/lib/module/download/ModelDownloadManager.js.map +1 -1
- package/lib/module/download/activeModelOperations.js +26 -0
- package/lib/module/download/activeModelOperations.js.map +1 -0
- package/lib/module/download/background-downloader.d.js +2 -0
- package/lib/module/download/background-downloader.d.js.map +1 -0
- package/lib/module/download/bulkPurge.js +72 -0
- package/lib/module/download/bulkPurge.js.map +1 -0
- package/lib/module/download/checksumPrompt.js +19 -0
- package/lib/module/download/checksumPrompt.js.map +1 -0
- package/lib/module/download/constants.js +7 -0
- package/lib/module/download/constants.js.map +1 -0
- package/lib/module/download/downloadEvents.js +35 -0
- package/lib/module/download/downloadEvents.js.map +1 -0
- package/lib/module/download/downloadTask.js +385 -0
- package/lib/module/download/downloadTask.js.map +1 -0
- package/lib/module/download/ensureModel.js +89 -0
- package/lib/module/download/ensureModel.js.map +1 -0
- package/lib/module/download/index.js +4 -4
- package/lib/module/download/index.js.map +1 -1
- package/lib/module/download/localModels.js +151 -0
- package/lib/module/download/localModels.js.map +1 -0
- package/lib/module/download/modelExtraction.js +174 -0
- package/lib/module/download/modelExtraction.js.map +1 -0
- package/lib/module/download/paths.js +98 -0
- package/lib/module/download/paths.js.map +1 -0
- package/lib/module/download/postDownloadProcessing.js +206 -0
- package/lib/module/download/postDownloadProcessing.js.map +1 -0
- package/lib/module/download/protectedModelKeys.js +31 -0
- package/lib/module/download/protectedModelKeys.js.map +1 -0
- package/lib/module/download/registry.js +267 -0
- package/lib/module/download/registry.js.map +1 -0
- package/lib/module/download/retry.js +59 -0
- package/lib/module/download/retry.js.map +1 -0
- package/lib/module/download/types.js +17 -0
- package/lib/module/download/types.js.map +1 -0
- package/lib/module/download/validation.js +101 -5
- package/lib/module/download/validation.js.map +1 -1
- package/lib/module/{download → extraction}/extractTarBz2.js +3 -1
- package/lib/module/extraction/extractTarBz2.js.map +1 -0
- package/lib/module/{download → extraction}/extractTarZst.js +3 -1
- package/lib/module/extraction/extractTarZst.js.map +1 -0
- package/lib/module/extraction/index.js +3 -4
- package/lib/module/extraction/index.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/licenses.js +63 -0
- package/lib/module/licenses.js.map +1 -0
- package/lib/module/stt/index.js +16 -2
- package/lib/module/stt/index.js.map +1 -1
- package/lib/module/stt/streaming.js +2 -0
- package/lib/module/stt/streaming.js.map +1 -1
- package/lib/module/stt/streamingTypes.js.map +1 -1
- package/lib/module/stt/types.js.map +1 -1
- package/lib/module/tts/index.js +20 -2
- package/lib/module/tts/index.js.map +1 -1
- package/lib/module/tts/streaming.js +4 -0
- package/lib/module/tts/streaming.js.map +1 -1
- package/lib/module/tts/types.js.map +1 -1
- package/lib/module/utils.js +16 -1
- package/lib/module/utils.js.map +1 -1
- package/lib/typescript/src/NativeSherpaOnnx.d.ts +33 -5
- package/lib/typescript/src/NativeSherpaOnnx.d.ts.map +1 -1
- package/lib/typescript/src/audio/index.d.ts +10 -0
- package/lib/typescript/src/audio/index.d.ts.map +1 -1
- package/lib/typescript/src/download/ModelDownloadManager.d.ts +10 -108
- package/lib/typescript/src/download/ModelDownloadManager.d.ts.map +1 -1
- package/lib/typescript/src/download/activeModelOperations.d.ts +6 -0
- package/lib/typescript/src/download/activeModelOperations.d.ts.map +1 -0
- package/lib/typescript/src/download/bulkPurge.d.ts +14 -0
- package/lib/typescript/src/download/bulkPurge.d.ts.map +1 -0
- package/lib/typescript/src/download/checksumPrompt.d.ts +3 -0
- package/lib/typescript/src/download/checksumPrompt.d.ts.map +1 -0
- package/lib/typescript/src/download/constants.d.ts +5 -0
- package/lib/typescript/src/download/constants.d.ts.map +1 -0
- package/lib/typescript/src/download/downloadEvents.d.ts +6 -0
- package/lib/typescript/src/download/downloadEvents.d.ts.map +1 -0
- package/lib/typescript/src/download/downloadTask.d.ts +20 -0
- package/lib/typescript/src/download/downloadTask.d.ts.map +1 -0
- package/lib/typescript/src/download/ensureModel.d.ts +26 -0
- package/lib/typescript/src/download/ensureModel.d.ts.map +1 -0
- package/lib/typescript/src/download/index.d.ts +7 -7
- package/lib/typescript/src/download/index.d.ts.map +1 -1
- package/lib/typescript/src/download/localModels.d.ts +15 -0
- package/lib/typescript/src/download/localModels.d.ts.map +1 -0
- package/lib/typescript/src/download/modelExtraction.d.ts +36 -0
- package/lib/typescript/src/download/modelExtraction.d.ts.map +1 -0
- package/lib/typescript/src/download/paths.d.ts +28 -0
- package/lib/typescript/src/download/paths.d.ts.map +1 -0
- package/lib/typescript/src/download/postDownloadProcessing.d.ts +19 -0
- package/lib/typescript/src/download/postDownloadProcessing.d.ts.map +1 -0
- package/lib/typescript/src/download/protectedModelKeys.d.ts +6 -0
- package/lib/typescript/src/download/protectedModelKeys.d.ts.map +1 -0
- package/lib/typescript/src/download/registry.d.ts +14 -0
- package/lib/typescript/src/download/registry.d.ts.map +1 -0
- package/lib/typescript/src/download/retry.d.ts +15 -0
- package/lib/typescript/src/download/retry.d.ts.map +1 -0
- package/lib/typescript/src/download/types.d.ts +96 -0
- package/lib/typescript/src/download/types.d.ts.map +1 -0
- package/lib/typescript/src/download/validation.d.ts +19 -0
- package/lib/typescript/src/download/validation.d.ts.map +1 -1
- package/lib/typescript/src/extraction/extractTarBz2.d.ts.map +1 -0
- package/lib/typescript/src/extraction/extractTarZst.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/licenses.d.ts +10 -0
- package/lib/typescript/src/licenses.d.ts.map +1 -0
- package/lib/typescript/src/stt/index.d.ts +4 -1
- package/lib/typescript/src/stt/index.d.ts.map +1 -1
- package/lib/typescript/src/stt/streaming.d.ts.map +1 -1
- package/lib/typescript/src/stt/streamingTypes.d.ts +5 -0
- package/lib/typescript/src/stt/streamingTypes.d.ts.map +1 -1
- package/lib/typescript/src/stt/types.d.ts +3 -1
- package/lib/typescript/src/stt/types.d.ts.map +1 -1
- package/lib/typescript/src/tts/index.d.ts +3 -1
- package/lib/typescript/src/tts/index.d.ts.map +1 -1
- package/lib/typescript/src/tts/streaming.d.ts.map +1 -1
- package/lib/typescript/src/tts/types.d.ts +6 -5
- package/lib/typescript/src/tts/types.d.ts.map +1 -1
- package/lib/typescript/src/utils.d.ts +5 -0
- package/lib/typescript/src/utils.d.ts.map +1 -1
- package/package.json +6 -1
- package/scripts/{check-model-csvs.sh → ci/check-model-csvs.sh} +9 -2
- package/scripts/ci/collect_all_sherpa_model_streams.sh +101 -0
- package/scripts/ci/collect_one_sherpa_release_stream.sh +189 -0
- package/scripts/ci/sherpa_asr_model_release_streams.json +21 -0
- package/scripts/ci/sherpa_tts_model_release_streams.json +13 -0
- package/scripts/ci/update_model_license_csv.sh +765 -0
- package/scripts/setup-ios-framework.sh +14 -11
- package/scripts/update_commercial_use.js +73 -0
- package/src/NativeSherpaOnnx.ts +36 -5
- package/src/audio/index.ts +20 -0
- package/src/download/ModelDownloadManager.ts +55 -1343
- package/src/download/activeModelOperations.ts +38 -0
- package/src/download/background-downloader.d.ts +43 -0
- package/src/download/bulkPurge.ts +102 -0
- package/src/download/checksumPrompt.ts +25 -0
- package/src/download/constants.ts +5 -0
- package/src/download/downloadEvents.ts +55 -0
- package/src/download/downloadTask.ts +497 -0
- package/src/download/ensureModel.ts +124 -0
- package/src/download/index.ts +19 -4
- package/src/download/localModels.ts +234 -0
- package/src/download/modelExtraction.ts +244 -0
- package/src/download/paths.ts +134 -0
- package/src/download/postDownloadProcessing.ts +292 -0
- package/src/download/protectedModelKeys.ts +30 -0
- package/src/download/registry.ts +404 -0
- package/src/download/retry.ts +76 -0
- package/src/download/types.ts +120 -0
- package/src/download/validation.ts +114 -8
- package/src/{download → extraction}/extractTarBz2.ts +3 -1
- package/src/{download → extraction}/extractTarZst.ts +3 -1
- package/src/extraction/index.ts +3 -7
- package/src/index.tsx +1 -0
- package/src/licenses.ts +100 -0
- package/src/stt/index.ts +20 -2
- package/src/stt/streaming.ts +3 -0
- package/src/stt/streamingTypes.ts +5 -0
- package/src/stt/types.ts +3 -1
- package/src/tts/index.ts +30 -2
- package/src/tts/streaming.ts +10 -0
- package/src/tts/types.ts +6 -5
- package/src/utils.ts +22 -1
- package/third_party/sherpa-onnx-prebuilt/ANDROID_RELEASE_TAG +1 -1
- package/third_party/sherpa-onnx-prebuilt/IOS_RELEASE_TAG +1 -1
- package/android/src/main/cpp/jni/tts/sherpa-onnx-tts-zipvoice-jni.cpp +0 -301
- package/android/src/main/java/com/sherpaonnx/ZipvoiceTtsWrapper.kt +0 -187
- package/lib/module/download/extractTarBz2.js.map +0 -1
- package/lib/module/download/extractTarZst.js.map +0 -1
- package/lib/typescript/src/download/extractTarBz2.d.ts.map +0 -1
- package/lib/typescript/src/download/extractTarZst.d.ts.map +0 -1
- package/scripts/check-qnn-support.sh +0 -78
- /package/lib/typescript/src/{download → extraction}/extractTarBz2.d.ts +0 -0
- /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
|
+
}
|
package/src/download/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { extractTarBz2 } from '
|
|
2
|
-
export type { ExtractProgressEvent } from '
|
|
3
|
-
export { extractTarZst } from '
|
|
4
|
-
export type { ExtractProgressEvent as ExtractZstProgressEvent } from '
|
|
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,
|