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
|
@@ -1,1344 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
DocumentDirectoryPath,
|
|
4
|
-
exists,
|
|
5
|
-
readFile,
|
|
6
|
-
mkdir,
|
|
7
|
-
writeFile,
|
|
8
|
-
readDir,
|
|
9
|
-
stat,
|
|
10
|
-
unlink,
|
|
11
|
-
downloadFile,
|
|
12
|
-
stopDownload,
|
|
13
|
-
} from '@dr.pogodin/react-native-fs';
|
|
14
|
-
import type { TTSModelType } from '../tts/types';
|
|
15
|
-
import { extractTarBz2 } from './extractTarBz2';
|
|
16
|
-
import {
|
|
17
|
-
parseChecksumFile,
|
|
18
|
-
validateChecksum,
|
|
19
|
-
validateExtractedFiles,
|
|
20
|
-
checkDiskSpace,
|
|
21
|
-
} from './validation';
|
|
22
|
-
|
|
23
|
-
const RELEASE_API_BASE =
|
|
24
|
-
'https://api.github.com/repos/k2-fsa/sherpa-onnx/releases/tags';
|
|
25
|
-
const CACHE_TTL_MINUTES = 24 * 60;
|
|
26
|
-
const MODEL_ARCHIVE_EXT = '.tar.bz2';
|
|
27
|
-
const MODEL_ONNX_EXT = '.onnx';
|
|
28
|
-
|
|
29
|
-
export enum ModelCategory {
|
|
30
|
-
Tts = 'tts',
|
|
31
|
-
Stt = 'stt',
|
|
32
|
-
Vad = 'vad',
|
|
33
|
-
Diarization = 'diarization',
|
|
34
|
-
Enhancement = 'enhancement',
|
|
35
|
-
Separation = 'separation',
|
|
36
|
-
Qnn = 'qnn',
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** TTS model type for meta; 'unknown' when id could not be classified. */
|
|
40
|
-
export type TtsModelType = TTSModelType | 'unknown';
|
|
41
|
-
|
|
42
|
-
export type Quantization = 'fp16' | 'int8' | 'int8-quantized' | 'unknown';
|
|
43
|
-
|
|
44
|
-
export type SizeTier = 'tiny' | 'small' | 'medium' | 'large' | 'unknown';
|
|
45
|
-
|
|
46
|
-
type ModelArchiveExt = 'tar.bz2' | 'onnx';
|
|
47
|
-
|
|
48
|
-
export type ModelMetaBase = {
|
|
49
|
-
id: string;
|
|
50
|
-
displayName: string;
|
|
51
|
-
downloadUrl: string;
|
|
52
|
-
archiveExt: ModelArchiveExt;
|
|
53
|
-
bytes: number;
|
|
54
|
-
sha256?: string;
|
|
55
|
-
category: ModelCategory;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
export type TtsModelMeta = ModelMetaBase & {
|
|
59
|
-
type: TtsModelType;
|
|
60
|
-
languages: string[];
|
|
61
|
-
quantization: Quantization;
|
|
62
|
-
sizeTier: SizeTier;
|
|
63
|
-
category: ModelCategory.Tts;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export type DownloadProgress = {
|
|
67
|
-
bytesDownloaded: number;
|
|
68
|
-
totalBytes: number;
|
|
69
|
-
percent: number;
|
|
70
|
-
phase?: 'downloading' | 'extracting';
|
|
71
|
-
speed?: number; // bytes per second
|
|
72
|
-
eta?: number; // estimated seconds remaining
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
export type DownloadResult = {
|
|
76
|
-
modelId: string;
|
|
77
|
-
localPath: string;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export type DownloadProgressListener = (
|
|
81
|
-
category: ModelCategory,
|
|
82
|
-
modelId: string,
|
|
83
|
-
progress: DownloadProgress
|
|
84
|
-
) => void;
|
|
85
|
-
|
|
86
|
-
export type ModelsListUpdatedListener = (
|
|
87
|
-
category: ModelCategory,
|
|
88
|
-
models: ModelMetaBase[]
|
|
89
|
-
) => void;
|
|
90
|
-
|
|
91
|
-
type ModelManifest<T extends ModelMetaBase = ModelMetaBase> = {
|
|
92
|
-
downloadedAt: string;
|
|
93
|
-
lastUsed?: string;
|
|
94
|
-
model: T;
|
|
95
|
-
/** Total size in bytes of the extracted/model directory (set after download or computed once). */
|
|
96
|
-
sizeOnDisk?: number;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
export type ModelWithMetadata<T extends ModelMetaBase = ModelMetaBase> = {
|
|
100
|
-
model: T;
|
|
101
|
-
downloadedAt: string;
|
|
102
|
-
lastUsed: string | null;
|
|
103
|
-
sizeOnDisk?: number;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
type ChecksumIssue = {
|
|
107
|
-
category: ModelCategory;
|
|
108
|
-
id: string;
|
|
109
|
-
archivePath: string;
|
|
110
|
-
expected?: string;
|
|
111
|
-
message: string;
|
|
112
|
-
reason: 'CHECKSUM_FAILED' | 'CHECKSUM_MISMATCH';
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const promptChecksumFallback = (issue: ChecksumIssue): Promise<boolean> =>
|
|
116
|
-
new Promise((resolve) => {
|
|
117
|
-
const reasonText =
|
|
118
|
-
issue.reason === 'CHECKSUM_FAILED'
|
|
119
|
-
? 'Failed to compute checksum.'
|
|
120
|
-
: 'Computed checksum does not match the expected value.';
|
|
121
|
-
const body = `${reasonText}\n\n${issue.message}\n\nDo you want to keep the file and continue?`;
|
|
122
|
-
|
|
123
|
-
Alert.alert('Checksum Problem', body, [
|
|
124
|
-
{
|
|
125
|
-
text: 'Delete and cancel',
|
|
126
|
-
style: 'destructive',
|
|
127
|
-
onPress: () => resolve(false),
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
text: 'Keep file',
|
|
131
|
-
style: 'default',
|
|
132
|
-
onPress: () => resolve(true),
|
|
133
|
-
},
|
|
134
|
-
]);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
type CachePayload<T extends ModelMetaBase> = {
|
|
138
|
-
lastUpdated: string;
|
|
139
|
-
models: T[];
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
type CacheStatus = {
|
|
143
|
-
lastUpdated: string | null;
|
|
144
|
-
source: 'cache' | 'remote';
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const memoryCacheByCategory: Partial<
|
|
148
|
-
Record<ModelCategory, CachePayload<ModelMetaBase>>
|
|
149
|
-
> = {};
|
|
150
|
-
|
|
151
|
-
const checksumCacheByCategory: Partial<
|
|
152
|
-
Record<ModelCategory, Map<string, string>>
|
|
153
|
-
> = {};
|
|
154
|
-
|
|
155
|
-
const downloadProgressListeners = new Set<DownloadProgressListener>();
|
|
156
|
-
const modelsListUpdatedListeners = new Set<ModelsListUpdatedListener>();
|
|
157
|
-
|
|
158
|
-
export const subscribeDownloadProgress = (
|
|
159
|
-
listener: DownloadProgressListener
|
|
160
|
-
): (() => void) => {
|
|
161
|
-
downloadProgressListeners.add(listener);
|
|
162
|
-
return () => {
|
|
163
|
-
downloadProgressListeners.delete(listener);
|
|
164
|
-
};
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const emitDownloadProgress = (
|
|
168
|
-
category: ModelCategory,
|
|
169
|
-
modelId: string,
|
|
170
|
-
progress: DownloadProgress
|
|
171
|
-
) => {
|
|
172
|
-
for (const listener of downloadProgressListeners) {
|
|
173
|
-
try {
|
|
174
|
-
listener(category, modelId, progress);
|
|
175
|
-
} catch (error) {
|
|
176
|
-
console.warn('Download progress listener error:', error);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
export const subscribeModelsListUpdated = (
|
|
182
|
-
listener: ModelsListUpdatedListener
|
|
183
|
-
): (() => void) => {
|
|
184
|
-
modelsListUpdatedListeners.add(listener);
|
|
185
|
-
return () => {
|
|
186
|
-
modelsListUpdatedListeners.delete(listener);
|
|
187
|
-
};
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
const emitModelsListUpdated = (
|
|
191
|
-
category: ModelCategory,
|
|
192
|
-
models: ModelMetaBase[]
|
|
193
|
-
) => {
|
|
194
|
-
for (const listener of modelsListUpdatedListeners) {
|
|
195
|
-
try {
|
|
196
|
-
listener(category, models);
|
|
197
|
-
} catch (error) {
|
|
198
|
-
console.warn('Models list listener error:', error);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
const CATEGORY_CONFIG: Record<
|
|
1
|
+
// Re-export types and enum for public API
|
|
2
|
+
export {
|
|
204
3
|
ModelCategory,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function getArchiveFilename(
|
|
261
|
-
modelId: string,
|
|
262
|
-
archiveExt: ModelArchiveExt
|
|
263
|
-
): string {
|
|
264
|
-
return `${modelId}.${archiveExt}`;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function getArchivePath(
|
|
268
|
-
category: ModelCategory,
|
|
269
|
-
modelId: string,
|
|
270
|
-
archiveExt: ModelArchiveExt
|
|
271
|
-
): string {
|
|
272
|
-
const filename = getArchiveFilename(modelId, archiveExt);
|
|
273
|
-
if (archiveExt === 'onnx') {
|
|
274
|
-
return `${getModelDir(category, modelId)}/${filename}`;
|
|
275
|
-
}
|
|
276
|
-
return `${getModelsBaseDir(category)}/${filename}`;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function getTarArchivePath(category: ModelCategory, modelId: string): string {
|
|
280
|
-
return getArchivePath(category, modelId, 'tar.bz2');
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function getOnnxPath(category: ModelCategory, modelId: string): string {
|
|
284
|
-
return getArchivePath(category, modelId, 'onnx');
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function getReadyMarkerPath(category: ModelCategory, modelId: string): string {
|
|
288
|
-
return `${getModelDir(category, modelId)}/.ready`;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function getManifestPath(category: ModelCategory, modelId: string): string {
|
|
292
|
-
return `${getModelDir(category, modelId)}/manifest.json`;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function getReleaseUrl(category: ModelCategory): string {
|
|
296
|
-
return `${RELEASE_API_BASE}/${CATEGORY_CONFIG[category].tag}`;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Retry helper with exponential backoff
|
|
301
|
-
* @param fn - The async function to retry
|
|
302
|
-
* @param options - Retry configuration
|
|
303
|
-
* @returns The result of the function
|
|
304
|
-
* @throws The last error if all retries fail or AbortError if aborted
|
|
305
|
-
*/
|
|
306
|
-
async function retryWithBackoff<T>(
|
|
307
|
-
fn: () => Promise<T>,
|
|
308
|
-
options: {
|
|
309
|
-
maxRetries?: number;
|
|
310
|
-
initialDelayMs?: number;
|
|
311
|
-
maxDelayMs?: number;
|
|
312
|
-
backoffFactor?: number;
|
|
313
|
-
signal?: AbortSignal;
|
|
314
|
-
} = {}
|
|
315
|
-
): Promise<T> {
|
|
316
|
-
const maxRetries = options.maxRetries ?? 3;
|
|
317
|
-
const initialDelayMs = options.initialDelayMs ?? 1000;
|
|
318
|
-
const maxDelayMs = options.maxDelayMs ?? 10000;
|
|
319
|
-
const backoffFactor = options.backoffFactor ?? 2;
|
|
320
|
-
|
|
321
|
-
let lastError: Error | undefined;
|
|
322
|
-
|
|
323
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
324
|
-
if (options.signal?.aborted) {
|
|
325
|
-
const abortError = new Error('Operation aborted');
|
|
326
|
-
abortError.name = 'AbortError';
|
|
327
|
-
throw abortError;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
try {
|
|
331
|
-
return await fn();
|
|
332
|
-
} catch (error) {
|
|
333
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
334
|
-
|
|
335
|
-
// Don't retry on abort
|
|
336
|
-
if (lastError.name === 'AbortError' || options.signal?.aborted) {
|
|
337
|
-
throw lastError;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// If this was the last attempt, throw the error
|
|
341
|
-
if (attempt === maxRetries) {
|
|
342
|
-
throw lastError;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Calculate delay with exponential backoff
|
|
346
|
-
const delayMs = Math.min(
|
|
347
|
-
initialDelayMs * Math.pow(backoffFactor, attempt),
|
|
348
|
-
maxDelayMs
|
|
349
|
-
);
|
|
350
|
-
|
|
351
|
-
console.warn(
|
|
352
|
-
`Retry attempt ${attempt + 1}/${maxRetries} after ${delayMs}ms due to:`,
|
|
353
|
-
lastError.message
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
// Wait before retrying
|
|
357
|
-
await new Promise<void>((resolve) => setTimeout(resolve, delayMs));
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
throw lastError ?? new Error('Retry failed with no error');
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
async function fetchChecksumsFromRelease(
|
|
365
|
-
category: ModelCategory
|
|
366
|
-
): Promise<Map<string, string>> {
|
|
367
|
-
// asr-models-qnn-binary has no checksum.txt; use GitHub API digest only (set in toGenericModelMeta).
|
|
368
|
-
if (category === ModelCategory.Qnn) {
|
|
369
|
-
return new Map<string, string>();
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Return cached if available
|
|
373
|
-
if (checksumCacheByCategory[category]) {
|
|
374
|
-
return checksumCacheByCategory[category]!;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
try {
|
|
378
|
-
const checksums = await retryWithBackoff(
|
|
379
|
-
async () => {
|
|
380
|
-
const response = await fetch(
|
|
381
|
-
`https://github.com/k2-fsa/sherpa-onnx/releases/download/${CATEGORY_CONFIG[category].tag}/checksum.txt`
|
|
382
|
-
);
|
|
383
|
-
|
|
384
|
-
if (!response.ok) {
|
|
385
|
-
throw new Error(
|
|
386
|
-
`Failed to fetch checksum.txt for ${category}: ${response.status}`
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const content = await response.text();
|
|
391
|
-
return parseChecksumFile(content);
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
maxRetries: 3,
|
|
395
|
-
initialDelayMs: 1000,
|
|
396
|
-
}
|
|
397
|
-
);
|
|
398
|
-
|
|
399
|
-
checksumCacheByCategory[category] = checksums;
|
|
400
|
-
return checksums;
|
|
401
|
-
} catch (error) {
|
|
402
|
-
console.warn(
|
|
403
|
-
`SherpaOnnxChecksum: Error fetching checksums for ${category}:`,
|
|
404
|
-
error
|
|
405
|
-
);
|
|
406
|
-
return new Map();
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function toTitleCase(value: string): string {
|
|
411
|
-
return value
|
|
412
|
-
.split(/[-_\s]+/g)
|
|
413
|
-
.filter(Boolean)
|
|
414
|
-
.map((token) => token[0]!.toUpperCase() + token.slice(1))
|
|
415
|
-
.join(' ');
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function deriveDisplayName(id: string): string {
|
|
419
|
-
const cleaned = id.replace(/^sherpa-onnx-/, '');
|
|
420
|
-
return toTitleCase(cleaned);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function deriveType(id: string): TtsModelType {
|
|
424
|
-
const lower = id.toLowerCase();
|
|
425
|
-
if (lower.includes('vits')) return 'vits';
|
|
426
|
-
if (lower.includes('kokoro')) return 'kokoro';
|
|
427
|
-
if (lower.includes('matcha')) return 'matcha';
|
|
428
|
-
if (lower.includes('kitten')) return 'kitten';
|
|
429
|
-
if (lower.includes('pocket')) return 'pocket';
|
|
430
|
-
if (lower.includes('zipvoice')) return 'zipvoice';
|
|
431
|
-
return 'unknown';
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function deriveQuantization(id: string): Quantization {
|
|
435
|
-
const lower = id.toLowerCase();
|
|
436
|
-
if (lower.includes('int8') && lower.includes('quant')) {
|
|
437
|
-
return 'int8-quantized';
|
|
438
|
-
}
|
|
439
|
-
if (lower.includes('int8')) return 'int8';
|
|
440
|
-
if (lower.includes('fp16')) return 'fp16';
|
|
441
|
-
return 'unknown';
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function deriveSizeTier(id: string): SizeTier {
|
|
445
|
-
const lower = id.toLowerCase();
|
|
446
|
-
if (lower.includes('tiny')) return 'tiny';
|
|
447
|
-
if (lower.includes('small')) return 'small';
|
|
448
|
-
if (lower.includes('medium')) return 'medium';
|
|
449
|
-
if (lower.includes('large')) return 'large';
|
|
450
|
-
if (lower.includes('low')) return 'small';
|
|
451
|
-
return 'unknown';
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function deriveLanguages(id: string): string[] {
|
|
455
|
-
const tokens = id.split(/[-_]+/g);
|
|
456
|
-
const languages = new Set<string>();
|
|
457
|
-
for (const token of tokens) {
|
|
458
|
-
if (/^[a-z]{2}$/.test(token)) {
|
|
459
|
-
languages.add(token);
|
|
460
|
-
continue;
|
|
461
|
-
}
|
|
462
|
-
if (/^[a-z]{2}[A-Z]{2}$/.test(token)) {
|
|
463
|
-
languages.add(token.slice(0, 2).toLowerCase());
|
|
464
|
-
continue;
|
|
465
|
-
}
|
|
466
|
-
if (/^[a-z]{2}-[A-Z]{2}$/.test(token)) {
|
|
467
|
-
languages.add(token.slice(0, 2).toLowerCase());
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
return Array.from(languages);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function getAssetExtension(name: string): ModelArchiveExt | null {
|
|
474
|
-
if (name.endsWith(MODEL_ARCHIVE_EXT)) return 'tar.bz2';
|
|
475
|
-
if (name.endsWith(MODEL_ONNX_EXT)) return 'onnx';
|
|
476
|
-
return null;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function stripAssetExtension(name: string, ext: ModelArchiveExt): string {
|
|
480
|
-
const suffix = `.${ext}`;
|
|
481
|
-
return name.endsWith(suffix) ? name.slice(0, -suffix.length) : name;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function isAssetSupportedForCategory(
|
|
485
|
-
category: ModelCategory,
|
|
486
|
-
name: string,
|
|
487
|
-
ext: ModelArchiveExt
|
|
488
|
-
): boolean {
|
|
489
|
-
const lower = name.toLowerCase();
|
|
490
|
-
|
|
491
|
-
switch (category) {
|
|
492
|
-
case ModelCategory.Tts:
|
|
493
|
-
return ext === 'tar.bz2';
|
|
494
|
-
case ModelCategory.Stt:
|
|
495
|
-
return ext === 'tar.bz2' && !lower.includes('vad');
|
|
496
|
-
case ModelCategory.Vad:
|
|
497
|
-
return ext === 'onnx' && lower.includes('vad');
|
|
498
|
-
case ModelCategory.Diarization:
|
|
499
|
-
return ext === 'tar.bz2';
|
|
500
|
-
case ModelCategory.Enhancement:
|
|
501
|
-
return ext === 'onnx';
|
|
502
|
-
case ModelCategory.Separation:
|
|
503
|
-
return ext === 'tar.bz2' || ext === 'onnx';
|
|
504
|
-
case ModelCategory.Qnn:
|
|
505
|
-
// asr-models-qnn-binary: e.g. sherpa-onnx-qnn-SM8850-binary-5-seconds-zipformer-ctc-zh-*.tar.bz2
|
|
506
|
-
return (
|
|
507
|
-
ext === 'tar.bz2' &&
|
|
508
|
-
lower.includes('sherpa-onnx-qnn') &&
|
|
509
|
-
lower.includes('binary') &&
|
|
510
|
-
lower.includes('seconds')
|
|
511
|
-
);
|
|
512
|
-
default:
|
|
513
|
-
return false;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function parseDigestSha256(value?: string | null): string | undefined {
|
|
518
|
-
if (!value) return undefined;
|
|
519
|
-
const match = value.match(/^sha256:([a-f0-9]{64})$/i);
|
|
520
|
-
return match?.[1]?.toLowerCase();
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
function toTtsModelMeta(
|
|
524
|
-
asset: {
|
|
525
|
-
name: string;
|
|
526
|
-
size: number;
|
|
527
|
-
browser_download_url: string;
|
|
528
|
-
digest?: string | null;
|
|
529
|
-
},
|
|
530
|
-
archiveExt: ModelArchiveExt
|
|
531
|
-
): TtsModelMeta | null {
|
|
532
|
-
if (archiveExt !== 'tar.bz2') {
|
|
533
|
-
return null;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const id = stripAssetExtension(asset.name, archiveExt);
|
|
537
|
-
const type = deriveType(id);
|
|
538
|
-
if (type === 'unknown') {
|
|
539
|
-
console.warn('SherpaOnnxModelList: Unsupported model', id);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
return {
|
|
543
|
-
id,
|
|
544
|
-
displayName: deriveDisplayName(id),
|
|
545
|
-
type,
|
|
546
|
-
languages: deriveLanguages(id),
|
|
547
|
-
quantization: deriveQuantization(id),
|
|
548
|
-
sizeTier: deriveSizeTier(id),
|
|
549
|
-
downloadUrl: asset.browser_download_url,
|
|
550
|
-
archiveExt,
|
|
551
|
-
bytes: asset.size,
|
|
552
|
-
sha256: parseDigestSha256(asset.digest),
|
|
553
|
-
category: ModelCategory.Tts,
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
function toGenericModelMeta(
|
|
558
|
-
category: ModelCategory,
|
|
559
|
-
asset: {
|
|
560
|
-
name: string;
|
|
561
|
-
size: number;
|
|
562
|
-
browser_download_url: string;
|
|
563
|
-
digest?: string | null;
|
|
564
|
-
},
|
|
565
|
-
archiveExt: ModelArchiveExt
|
|
566
|
-
): ModelMetaBase | null {
|
|
567
|
-
const id = stripAssetExtension(asset.name, archiveExt);
|
|
568
|
-
return {
|
|
569
|
-
id,
|
|
570
|
-
displayName: deriveDisplayName(id),
|
|
571
|
-
downloadUrl: asset.browser_download_url,
|
|
572
|
-
archiveExt,
|
|
573
|
-
bytes: asset.size,
|
|
574
|
-
sha256: parseDigestSha256(asset.digest),
|
|
575
|
-
category,
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
function toModelMeta(
|
|
580
|
-
category: ModelCategory,
|
|
581
|
-
asset: {
|
|
582
|
-
name: string;
|
|
583
|
-
size: number;
|
|
584
|
-
browser_download_url: string;
|
|
585
|
-
digest?: string | null;
|
|
586
|
-
}
|
|
587
|
-
): ModelMetaBase | null {
|
|
588
|
-
const archiveExt = getAssetExtension(asset.name);
|
|
589
|
-
if (!archiveExt) return null;
|
|
590
|
-
if (!isAssetSupportedForCategory(category, asset.name, archiveExt)) {
|
|
591
|
-
return null;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
if (category === ModelCategory.Tts) {
|
|
595
|
-
return toTtsModelMeta(asset, archiveExt);
|
|
596
|
-
}
|
|
597
|
-
return toGenericModelMeta(category, asset, archiveExt);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
async function loadCacheFromDisk<T extends ModelMetaBase>(
|
|
601
|
-
category: ModelCategory
|
|
602
|
-
): Promise<CachePayload<T> | null> {
|
|
603
|
-
const memoryCache = memoryCacheByCategory[category] as
|
|
604
|
-
| CachePayload<T>
|
|
605
|
-
| undefined;
|
|
606
|
-
if (memoryCache) return memoryCache;
|
|
607
|
-
const cachePath = getCachePath(category);
|
|
608
|
-
const existsResult = await exists(cachePath);
|
|
609
|
-
if (!existsResult) return null;
|
|
610
|
-
|
|
611
|
-
const raw = await readFile(cachePath, 'utf8');
|
|
612
|
-
const parsed = JSON.parse(raw) as CachePayload<T>;
|
|
613
|
-
memoryCacheByCategory[category] = parsed as CachePayload<ModelMetaBase>;
|
|
614
|
-
return parsed;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
async function saveCache<T extends ModelMetaBase>(
|
|
618
|
-
category: ModelCategory,
|
|
619
|
-
payload: CachePayload<T>
|
|
620
|
-
): Promise<void> {
|
|
621
|
-
const cacheDir = getCacheDir();
|
|
622
|
-
await mkdir(cacheDir);
|
|
623
|
-
await writeFile(getCachePath(category), JSON.stringify(payload), 'utf8');
|
|
624
|
-
memoryCacheByCategory[category] = payload as CachePayload<ModelMetaBase>;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
function isCacheFresh<T extends ModelMetaBase>(
|
|
628
|
-
payload: CachePayload<T>,
|
|
629
|
-
ttlMinutes: number
|
|
630
|
-
): boolean {
|
|
631
|
-
const updated = new Date(payload.lastUpdated).getTime();
|
|
632
|
-
if (!updated) return false;
|
|
633
|
-
const ageMs = Date.now() - updated;
|
|
634
|
-
return ageMs < ttlMinutes * 60 * 1000;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
export async function listModelsByCategory<T extends ModelMetaBase>(
|
|
638
|
-
category: ModelCategory
|
|
639
|
-
): Promise<T[]> {
|
|
640
|
-
const cache = await loadCacheFromDisk<T>(category);
|
|
641
|
-
return cache?.models ?? [];
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
export async function refreshModelsByCategory<T extends ModelMetaBase>(
|
|
645
|
-
category: ModelCategory,
|
|
646
|
-
options?: {
|
|
647
|
-
forceRefresh?: boolean;
|
|
648
|
-
cacheTtlMinutes?: number;
|
|
649
|
-
maxRetries?: number;
|
|
650
|
-
signal?: AbortSignal;
|
|
651
|
-
}
|
|
652
|
-
): Promise<T[]> {
|
|
653
|
-
const ttl = options?.cacheTtlMinutes ?? CACHE_TTL_MINUTES;
|
|
654
|
-
const cached = await loadCacheFromDisk<T>(category);
|
|
655
|
-
|
|
656
|
-
if (!options?.forceRefresh && cached && isCacheFresh(cached, ttl)) {
|
|
657
|
-
return cached.models;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
try {
|
|
661
|
-
const body = await retryWithBackoff(
|
|
662
|
-
async () => {
|
|
663
|
-
const response = await fetch(getReleaseUrl(category));
|
|
664
|
-
if (!response.ok) {
|
|
665
|
-
throw new Error(`Failed to fetch models: ${response.status}`);
|
|
666
|
-
}
|
|
667
|
-
return response.json();
|
|
668
|
-
},
|
|
669
|
-
{
|
|
670
|
-
maxRetries: options?.maxRetries ?? 3,
|
|
671
|
-
initialDelayMs: 1000,
|
|
672
|
-
signal: options?.signal,
|
|
673
|
-
}
|
|
674
|
-
);
|
|
675
|
-
|
|
676
|
-
const assets = Array.isArray(body?.assets) ? body.assets : [];
|
|
677
|
-
const models: T[] = assets
|
|
678
|
-
.map((asset: any) =>
|
|
679
|
-
toModelMeta(category, {
|
|
680
|
-
name: asset.name,
|
|
681
|
-
size: asset.size,
|
|
682
|
-
browser_download_url: asset.browser_download_url,
|
|
683
|
-
digest: asset.digest,
|
|
684
|
-
})
|
|
685
|
-
)
|
|
686
|
-
.filter((model: ModelMetaBase | null): model is T => Boolean(model));
|
|
687
|
-
|
|
688
|
-
// Load and attach SHA256 checksums from checksum.txt
|
|
689
|
-
const checksums = await fetchChecksumsFromRelease(category);
|
|
690
|
-
for (const model of models) {
|
|
691
|
-
const archiveFilename = getArchiveFilename(model.id, model.archiveExt);
|
|
692
|
-
const sha256 = checksums.get(archiveFilename);
|
|
693
|
-
if (sha256) {
|
|
694
|
-
model.sha256 = sha256;
|
|
695
|
-
} else if (model.sha256) {
|
|
696
|
-
model.sha256 = model.sha256.toLowerCase();
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
const payload: CachePayload<T> = {
|
|
701
|
-
lastUpdated: new Date().toISOString(),
|
|
702
|
-
models,
|
|
703
|
-
};
|
|
704
|
-
await saveCache(category, payload);
|
|
705
|
-
emitModelsListUpdated(category, models as ModelMetaBase[]);
|
|
706
|
-
return models;
|
|
707
|
-
} catch (error) {
|
|
708
|
-
// If retry failed and we have cached data, return it as fallback
|
|
709
|
-
if (cached) {
|
|
710
|
-
console.warn(
|
|
711
|
-
`Failed to refresh models for ${category}, using cached data:`,
|
|
712
|
-
error
|
|
713
|
-
);
|
|
714
|
-
return cached.models;
|
|
715
|
-
}
|
|
716
|
-
throw error;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
export async function getModelsCacheStatusByCategory(
|
|
721
|
-
category: ModelCategory
|
|
722
|
-
): Promise<CacheStatus> {
|
|
723
|
-
const cached = await loadCacheFromDisk(category);
|
|
724
|
-
if (!cached) {
|
|
725
|
-
return { lastUpdated: null, source: 'cache' };
|
|
726
|
-
}
|
|
727
|
-
return { lastUpdated: cached.lastUpdated, source: 'cache' };
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
export async function getModelByIdByCategory<T extends ModelMetaBase>(
|
|
731
|
-
category: ModelCategory,
|
|
732
|
-
id: string
|
|
733
|
-
): Promise<T | null> {
|
|
734
|
-
const models = await listModelsByCategory<T>(category);
|
|
735
|
-
return models.find((model) => model.id === id) ?? null;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
export async function listDownloadedModelsByCategory<T extends ModelMetaBase>(
|
|
739
|
-
category: ModelCategory
|
|
740
|
-
): Promise<T[]> {
|
|
741
|
-
const baseDir = getModelsBaseDir(category);
|
|
742
|
-
const existsResult = await exists(baseDir);
|
|
743
|
-
if (!existsResult) return [];
|
|
744
|
-
|
|
745
|
-
const entries = await readDir(baseDir);
|
|
746
|
-
const models: T[] = [];
|
|
747
|
-
|
|
748
|
-
for (const entry of entries) {
|
|
749
|
-
if (!entry.isDirectory()) continue;
|
|
750
|
-
const manifestPath = getManifestPath(category, entry.name);
|
|
751
|
-
const manifestExists = await exists(manifestPath);
|
|
752
|
-
// Only list models that have a manifest (download + extraction fully complete).
|
|
753
|
-
// Directories without manifest are still being extracted and must not appear in the list.
|
|
754
|
-
if (!manifestExists) continue;
|
|
755
|
-
try {
|
|
756
|
-
const raw = await readFile(manifestPath, 'utf8');
|
|
757
|
-
const manifest = JSON.parse(raw) as ModelManifest<T>;
|
|
758
|
-
if (manifest.model) {
|
|
759
|
-
models.push(manifest.model);
|
|
760
|
-
}
|
|
761
|
-
} catch {
|
|
762
|
-
// ignore invalid manifest
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
return models;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
export async function isModelDownloadedByCategory(
|
|
770
|
-
category: ModelCategory,
|
|
771
|
-
id: string
|
|
772
|
-
): Promise<boolean> {
|
|
773
|
-
const readyPath = getReadyMarkerPath(category, id);
|
|
774
|
-
return exists(readyPath);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
export async function getLocalModelPathByCategory(
|
|
778
|
-
category: ModelCategory,
|
|
779
|
-
id: string
|
|
780
|
-
): Promise<string | null> {
|
|
781
|
-
const ready = await isModelDownloadedByCategory(category, id);
|
|
782
|
-
if (!ready) return null;
|
|
783
|
-
|
|
784
|
-
// Update lastUsed timestamp when model is accessed
|
|
785
|
-
await updateModelLastUsed(category, id);
|
|
786
|
-
|
|
787
|
-
return getModelDir(category, id);
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
export async function downloadModelByCategory<T extends ModelMetaBase>(
|
|
791
|
-
category: ModelCategory,
|
|
792
|
-
id: string,
|
|
793
|
-
opts?: {
|
|
794
|
-
onProgress?: (progress: DownloadProgress) => void;
|
|
795
|
-
overwrite?: boolean;
|
|
796
|
-
signal?: AbortSignal;
|
|
797
|
-
maxRetries?: number;
|
|
798
|
-
onChecksumIssue?: (issue: ChecksumIssue) => Promise<boolean>;
|
|
799
|
-
}
|
|
800
|
-
): Promise<DownloadResult> {
|
|
801
|
-
const isAborted = () => Boolean(opts?.signal?.aborted);
|
|
802
|
-
|
|
803
|
-
if (opts?.signal?.aborted) {
|
|
804
|
-
const abortError = new Error('Download aborted');
|
|
805
|
-
abortError.name = 'AbortError';
|
|
806
|
-
throw abortError;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
const model = await getModelByIdByCategory<T>(category, id);
|
|
810
|
-
if (!model) {
|
|
811
|
-
throw new Error(`Unknown model id: ${id}`);
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
const baseDir = getModelsBaseDir(category);
|
|
815
|
-
await mkdir(baseDir);
|
|
816
|
-
|
|
817
|
-
const downloadPath = getArchivePath(category, id, model.archiveExt);
|
|
818
|
-
const isArchive = model.archiveExt === 'tar.bz2';
|
|
819
|
-
const modelDir = getModelDir(category, id);
|
|
820
|
-
|
|
821
|
-
const sleep = (ms: number) =>
|
|
822
|
-
new Promise<void>((resolve) => {
|
|
823
|
-
setTimeout(resolve, ms);
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
const cleanupPartial = async () => {
|
|
827
|
-
if (!isArchive) {
|
|
828
|
-
return;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
// Only clean up extracted model dir, preserve archive for download resume
|
|
832
|
-
if (await exists(modelDir)) {
|
|
833
|
-
await unlink(modelDir);
|
|
834
|
-
}
|
|
835
|
-
};
|
|
836
|
-
|
|
837
|
-
const cleanupPartialWithRetry = async () => {
|
|
838
|
-
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
839
|
-
await cleanupPartial();
|
|
840
|
-
if (!(await exists(modelDir))) {
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
await sleep(400);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
if (await exists(modelDir)) {
|
|
847
|
-
console.warn(
|
|
848
|
-
`Model cleanup after abort did not fully complete for ${category}:${id}`
|
|
849
|
-
);
|
|
850
|
-
}
|
|
851
|
-
};
|
|
852
|
-
|
|
853
|
-
// Step 1: Check available disk space
|
|
854
|
-
const diskSpaceCheck = await checkDiskSpace(model.bytes);
|
|
855
|
-
if (!diskSpaceCheck.success) {
|
|
856
|
-
throw new Error(`Insufficient disk space: ${diskSpaceCheck.message}`);
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
if (opts?.overwrite) {
|
|
860
|
-
if (await exists(modelDir)) {
|
|
861
|
-
await unlink(modelDir);
|
|
862
|
-
}
|
|
863
|
-
if (await exists(downloadPath)) {
|
|
864
|
-
await unlink(downloadPath);
|
|
865
|
-
}
|
|
866
|
-
} else {
|
|
867
|
-
// Clean up incomplete extractions but preserve partial downloads for resume
|
|
868
|
-
const readyMarkerExists = await exists(getReadyMarkerPath(category, id));
|
|
869
|
-
if (!readyMarkerExists) {
|
|
870
|
-
if (isArchive) {
|
|
871
|
-
// No ready marker found; only clean up extracted model dir
|
|
872
|
-
// Keep archive file to support download resume
|
|
873
|
-
if (await exists(modelDir)) {
|
|
874
|
-
// Removing partial model dir
|
|
875
|
-
await unlink(modelDir);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
try {
|
|
882
|
-
// Step 2: Download archive or onnx file (with resume support)
|
|
883
|
-
if (!isArchive) {
|
|
884
|
-
await mkdir(modelDir);
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
const archiveExists = await exists(downloadPath);
|
|
888
|
-
let partialDownload = false;
|
|
889
|
-
|
|
890
|
-
if (archiveExists) {
|
|
891
|
-
// Check if this is a complete download or partial
|
|
892
|
-
const statResult = await stat(downloadPath);
|
|
893
|
-
const currentSize = statResult.size;
|
|
894
|
-
if (currentSize < model.bytes) {
|
|
895
|
-
partialDownload = true;
|
|
896
|
-
console.log(
|
|
897
|
-
`[Download] Resuming partial download for ${category}:${id} (${currentSize}/${model.bytes} bytes)`
|
|
898
|
-
);
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
if (!archiveExists || partialDownload) {
|
|
903
|
-
const maxRetries = opts?.maxRetries ?? 2;
|
|
904
|
-
|
|
905
|
-
await retryWithBackoff(
|
|
906
|
-
async () => {
|
|
907
|
-
const downloadStartTime = Date.now();
|
|
908
|
-
|
|
909
|
-
const download = downloadFile({
|
|
910
|
-
fromUrl: model.downloadUrl,
|
|
911
|
-
toFile: downloadPath,
|
|
912
|
-
progressDivider: 1,
|
|
913
|
-
resumable: () => {
|
|
914
|
-
// iOS only: Called when download is resumed
|
|
915
|
-
console.log(`[Download] Resuming download for ${category}:${id}`);
|
|
916
|
-
},
|
|
917
|
-
progress: (data) => {
|
|
918
|
-
if (isAborted()) {
|
|
919
|
-
return;
|
|
920
|
-
}
|
|
921
|
-
const total = data.contentLength || model.bytes || 0;
|
|
922
|
-
const percent = total > 0 ? (data.bytesWritten / total) * 100 : 0;
|
|
923
|
-
|
|
924
|
-
// Calculate speed and ETA
|
|
925
|
-
const now = Date.now();
|
|
926
|
-
const elapsedSeconds = (now - downloadStartTime) / 1000;
|
|
927
|
-
|
|
928
|
-
let speed: number | undefined;
|
|
929
|
-
let eta: number | undefined;
|
|
930
|
-
|
|
931
|
-
if (elapsedSeconds > 0.5) {
|
|
932
|
-
// Calculate overall speed (bytes per second)
|
|
933
|
-
speed = data.bytesWritten / elapsedSeconds;
|
|
934
|
-
|
|
935
|
-
// Calculate ETA based on current speed
|
|
936
|
-
const remainingBytes = total - data.bytesWritten;
|
|
937
|
-
if (speed > 0) {
|
|
938
|
-
eta = remainingBytes / speed;
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
const progress: DownloadProgress = {
|
|
943
|
-
bytesDownloaded: data.bytesWritten,
|
|
944
|
-
totalBytes: total,
|
|
945
|
-
percent,
|
|
946
|
-
phase: 'downloading',
|
|
947
|
-
speed,
|
|
948
|
-
eta,
|
|
949
|
-
};
|
|
950
|
-
opts?.onProgress?.(progress);
|
|
951
|
-
emitDownloadProgress(category, id, progress);
|
|
952
|
-
},
|
|
953
|
-
});
|
|
954
|
-
|
|
955
|
-
let downloadFinished = false;
|
|
956
|
-
let aborted = false;
|
|
957
|
-
const onAbort = () => {
|
|
958
|
-
aborted = true;
|
|
959
|
-
if (downloadFinished) return;
|
|
960
|
-
try {
|
|
961
|
-
stopDownload(download.jobId);
|
|
962
|
-
} catch {
|
|
963
|
-
// Swallow stop errors to avoid crashing the app on cancel.
|
|
964
|
-
}
|
|
965
|
-
};
|
|
966
|
-
|
|
967
|
-
if (opts?.signal) {
|
|
968
|
-
opts.signal.addEventListener('abort', onAbort);
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
let result: any;
|
|
972
|
-
try {
|
|
973
|
-
result = await download.promise;
|
|
974
|
-
} finally {
|
|
975
|
-
downloadFinished = true;
|
|
976
|
-
if (opts?.signal) {
|
|
977
|
-
opts.signal.removeEventListener('abort', onAbort);
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
if (aborted || opts?.signal?.aborted) {
|
|
981
|
-
const abortError = new Error('Download aborted');
|
|
982
|
-
abortError.name = 'AbortError';
|
|
983
|
-
throw abortError;
|
|
984
|
-
}
|
|
985
|
-
if (result.statusCode && result.statusCode >= 400) {
|
|
986
|
-
// For certain errors, delete partial download as resume won't help
|
|
987
|
-
const isNonResumableError =
|
|
988
|
-
result.statusCode === 404 || // Not found
|
|
989
|
-
result.statusCode === 410 || // Gone
|
|
990
|
-
result.statusCode === 451 || // Unavailable for legal reasons
|
|
991
|
-
result.statusCode === 416; // Range not satisfiable
|
|
992
|
-
if (isNonResumableError && (await exists(downloadPath))) {
|
|
993
|
-
console.warn(
|
|
994
|
-
`[Download] Non-resumable error ${result.statusCode}, removing partial download`
|
|
995
|
-
);
|
|
996
|
-
await unlink(downloadPath);
|
|
997
|
-
}
|
|
998
|
-
throw new Error(`Download failed: ${result.statusCode}`);
|
|
999
|
-
}
|
|
1000
|
-
},
|
|
1001
|
-
{
|
|
1002
|
-
maxRetries,
|
|
1003
|
-
initialDelayMs: 2000,
|
|
1004
|
-
signal: opts?.signal,
|
|
1005
|
-
}
|
|
1006
|
-
);
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
if (opts?.signal?.aborted) {
|
|
1010
|
-
const abortError = new Error('Download aborted');
|
|
1011
|
-
abortError.name = 'AbortError';
|
|
1012
|
-
throw abortError;
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
let extractResult: { sha256?: string } | null = null;
|
|
1016
|
-
/** Total uncompressed bytes from libarchive progress; used for manifest.sizeOnDisk. */
|
|
1017
|
-
let extractedTotalBytes = 0;
|
|
1018
|
-
|
|
1019
|
-
if (isArchive) {
|
|
1020
|
-
await mkdir(modelDir);
|
|
1021
|
-
const extractStartTime = Date.now();
|
|
1022
|
-
extractResult = await extractTarBz2(
|
|
1023
|
-
downloadPath,
|
|
1024
|
-
modelDir,
|
|
1025
|
-
true,
|
|
1026
|
-
(evt) => {
|
|
1027
|
-
if (isAborted()) {
|
|
1028
|
-
return;
|
|
1029
|
-
}
|
|
1030
|
-
if (evt.totalBytes > 0) extractedTotalBytes = evt.totalBytes;
|
|
1031
|
-
if (model.bytes > 0) {
|
|
1032
|
-
// Calculate extraction speed and ETA
|
|
1033
|
-
const now = Date.now();
|
|
1034
|
-
const elapsedSeconds = (now - extractStartTime) / 1000;
|
|
1035
|
-
|
|
1036
|
-
let speed: number | undefined;
|
|
1037
|
-
let eta: number | undefined;
|
|
1038
|
-
|
|
1039
|
-
if (elapsedSeconds > 0.5) {
|
|
1040
|
-
speed = evt.bytes / elapsedSeconds;
|
|
1041
|
-
const remainingBytes = evt.totalBytes - evt.bytes;
|
|
1042
|
-
if (speed > 0) {
|
|
1043
|
-
eta = remainingBytes / speed;
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
const progress: DownloadProgress = {
|
|
1048
|
-
bytesDownloaded: evt.bytes,
|
|
1049
|
-
totalBytes: evt.totalBytes,
|
|
1050
|
-
percent: evt.percent,
|
|
1051
|
-
phase: 'extracting',
|
|
1052
|
-
speed,
|
|
1053
|
-
eta,
|
|
1054
|
-
};
|
|
1055
|
-
opts?.onProgress?.(progress);
|
|
1056
|
-
emitDownloadProgress(category, id, progress);
|
|
1057
|
-
}
|
|
1058
|
-
},
|
|
1059
|
-
opts?.signal
|
|
1060
|
-
);
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
// Step 3: Validate checksum if available
|
|
1064
|
-
if (model.sha256) {
|
|
1065
|
-
const expectedSha = model.sha256.toLowerCase();
|
|
1066
|
-
let issue: ChecksumIssue | null = null;
|
|
1067
|
-
|
|
1068
|
-
if (isArchive) {
|
|
1069
|
-
const nativeSha = extractResult?.sha256?.toLowerCase();
|
|
1070
|
-
if (!nativeSha) {
|
|
1071
|
-
issue = {
|
|
1072
|
-
category,
|
|
1073
|
-
id,
|
|
1074
|
-
archivePath: downloadPath,
|
|
1075
|
-
expected: model.sha256,
|
|
1076
|
-
message: 'Native SHA-256 not available after extraction.',
|
|
1077
|
-
reason: 'CHECKSUM_FAILED',
|
|
1078
|
-
};
|
|
1079
|
-
} else if (nativeSha !== expectedSha) {
|
|
1080
|
-
issue = {
|
|
1081
|
-
category,
|
|
1082
|
-
id,
|
|
1083
|
-
archivePath: downloadPath,
|
|
1084
|
-
expected: model.sha256,
|
|
1085
|
-
message: `Checksum mismatch: expected ${model.sha256}, got ${extractResult?.sha256}`,
|
|
1086
|
-
reason: 'CHECKSUM_MISMATCH',
|
|
1087
|
-
};
|
|
1088
|
-
}
|
|
1089
|
-
} else {
|
|
1090
|
-
const checksumResult = await validateChecksum(
|
|
1091
|
-
downloadPath,
|
|
1092
|
-
expectedSha
|
|
1093
|
-
);
|
|
1094
|
-
if (!checksumResult.success) {
|
|
1095
|
-
issue = {
|
|
1096
|
-
category,
|
|
1097
|
-
id,
|
|
1098
|
-
archivePath: downloadPath,
|
|
1099
|
-
expected: model.sha256,
|
|
1100
|
-
message: checksumResult.message ?? 'Checksum validation failed.',
|
|
1101
|
-
reason:
|
|
1102
|
-
checksumResult.error === 'CHECKSUM_MISMATCH'
|
|
1103
|
-
? 'CHECKSUM_MISMATCH'
|
|
1104
|
-
: 'CHECKSUM_FAILED',
|
|
1105
|
-
};
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
if (issue) {
|
|
1110
|
-
const keepFile = opts?.onChecksumIssue
|
|
1111
|
-
? await opts.onChecksumIssue(issue)
|
|
1112
|
-
: await promptChecksumFallback(issue);
|
|
1113
|
-
|
|
1114
|
-
if (!keepFile) {
|
|
1115
|
-
if (await exists(modelDir)) {
|
|
1116
|
-
await unlink(modelDir);
|
|
1117
|
-
}
|
|
1118
|
-
if (await exists(downloadPath)) {
|
|
1119
|
-
await unlink(downloadPath);
|
|
1120
|
-
}
|
|
1121
|
-
throw new Error(`Checksum validation failed: ${issue.message}`);
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
if (opts?.signal?.aborted) {
|
|
1127
|
-
const abortError = new Error('Download aborted');
|
|
1128
|
-
abortError.name = 'AbortError';
|
|
1129
|
-
throw abortError;
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
// Step 4: Validate extracted files exist
|
|
1133
|
-
const filesValidation = await validateExtractedFiles(modelDir, category);
|
|
1134
|
-
if (!filesValidation.success) {
|
|
1135
|
-
// Clean up failed extraction
|
|
1136
|
-
await unlink(modelDir);
|
|
1137
|
-
throw new Error(
|
|
1138
|
-
`Extracted files validation failed: ${filesValidation.message}`
|
|
1139
|
-
);
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
await writeFile(getReadyMarkerPath(category, id), 'ready', 'utf8');
|
|
1143
|
-
const now = new Date().toISOString();
|
|
1144
|
-
let sizeOnDisk: number | undefined;
|
|
1145
|
-
if (isArchive && extractedTotalBytes > 0) {
|
|
1146
|
-
sizeOnDisk = extractedTotalBytes;
|
|
1147
|
-
} else if (!isArchive) {
|
|
1148
|
-
try {
|
|
1149
|
-
const statResult = await stat(downloadPath);
|
|
1150
|
-
sizeOnDisk = statResult.size;
|
|
1151
|
-
} catch {
|
|
1152
|
-
// ignore
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
await writeFile(
|
|
1156
|
-
getManifestPath(category, id),
|
|
1157
|
-
JSON.stringify({
|
|
1158
|
-
downloadedAt: now,
|
|
1159
|
-
lastUsed: now,
|
|
1160
|
-
model,
|
|
1161
|
-
sizeOnDisk,
|
|
1162
|
-
} as ModelManifest),
|
|
1163
|
-
'utf8'
|
|
1164
|
-
);
|
|
1165
|
-
|
|
1166
|
-
// Notify subscribers (e.g. STT/TTS screens) so the model list updates without leaving the screen.
|
|
1167
|
-
const list = await listDownloadedModelsByCategory<ModelMetaBase>(category);
|
|
1168
|
-
emitModelsListUpdated(category, list);
|
|
1169
|
-
|
|
1170
|
-
return { modelId: id, localPath: modelDir };
|
|
1171
|
-
} catch (err) {
|
|
1172
|
-
if ((err instanceof Error && err.name === 'AbortError') || isAborted()) {
|
|
1173
|
-
await cleanupPartialWithRetry();
|
|
1174
|
-
}
|
|
1175
|
-
throw err;
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
/**
|
|
1180
|
-
* Update the lastUsed timestamp for a downloaded model
|
|
1181
|
-
*/
|
|
1182
|
-
export async function updateModelLastUsed(
|
|
1183
|
-
category: ModelCategory,
|
|
1184
|
-
id: string
|
|
1185
|
-
): Promise<void> {
|
|
1186
|
-
const manifestPath = getManifestPath(category, id);
|
|
1187
|
-
const existsResult = await exists(manifestPath);
|
|
1188
|
-
if (!existsResult) return;
|
|
1189
|
-
|
|
1190
|
-
try {
|
|
1191
|
-
const raw = await readFile(manifestPath, 'utf8');
|
|
1192
|
-
const manifest = JSON.parse(raw) as ModelManifest;
|
|
1193
|
-
manifest.lastUsed = new Date().toISOString();
|
|
1194
|
-
await writeFile(manifestPath, JSON.stringify(manifest), 'utf8');
|
|
1195
|
-
} catch (error) {
|
|
1196
|
-
console.warn(`Failed to update lastUsed for ${category}:${id}:`, error);
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
/**
|
|
1201
|
-
* Get all downloaded models with LRU metadata
|
|
1202
|
-
*/
|
|
1203
|
-
export async function listDownloadedModelsWithMetadata<T extends ModelMetaBase>(
|
|
1204
|
-
category: ModelCategory
|
|
1205
|
-
): Promise<ModelWithMetadata<T>[]> {
|
|
1206
|
-
const baseDir = getModelsBaseDir(category);
|
|
1207
|
-
const existsResult = await exists(baseDir);
|
|
1208
|
-
if (!existsResult) return [];
|
|
1209
|
-
|
|
1210
|
-
const entries = await readDir(baseDir);
|
|
1211
|
-
const modelsWithMetadata: ModelWithMetadata<T>[] = [];
|
|
1212
|
-
|
|
1213
|
-
for (const entry of entries) {
|
|
1214
|
-
if (!entry.isDirectory()) continue;
|
|
1215
|
-
|
|
1216
|
-
const manifestPath = getManifestPath(category, entry.name);
|
|
1217
|
-
const manifestExists = await exists(manifestPath);
|
|
1218
|
-
|
|
1219
|
-
if (manifestExists) {
|
|
1220
|
-
try {
|
|
1221
|
-
const raw = await readFile(manifestPath, 'utf8');
|
|
1222
|
-
const manifest = JSON.parse(raw) as ModelManifest<T>;
|
|
1223
|
-
if (manifest.model) {
|
|
1224
|
-
modelsWithMetadata.push({
|
|
1225
|
-
model: manifest.model,
|
|
1226
|
-
downloadedAt: manifest.downloadedAt,
|
|
1227
|
-
lastUsed: manifest.lastUsed ?? null,
|
|
1228
|
-
sizeOnDisk: manifest.sizeOnDisk ?? entry.size,
|
|
1229
|
-
});
|
|
1230
|
-
}
|
|
1231
|
-
} catch (error) {
|
|
1232
|
-
console.warn(
|
|
1233
|
-
`Failed to read manifest for ${category}:${entry.name}:`,
|
|
1234
|
-
error
|
|
1235
|
-
);
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
return modelsWithMetadata;
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
/**
|
|
1244
|
-
* Remove least recently used models to free up disk space
|
|
1245
|
-
* @param category - Model category
|
|
1246
|
-
* @param targetBytes - Target amount of bytes to free (optional)
|
|
1247
|
-
* @param maxModelsToDelete - Maximum number of models to delete (default: no limit)
|
|
1248
|
-
* @returns Array of deleted model IDs
|
|
1249
|
-
*/
|
|
1250
|
-
export async function cleanupLeastRecentlyUsed(
|
|
1251
|
-
category: ModelCategory,
|
|
1252
|
-
options?: {
|
|
1253
|
-
targetBytes?: number;
|
|
1254
|
-
maxModelsToDelete?: number;
|
|
1255
|
-
keepCount?: number;
|
|
1256
|
-
}
|
|
1257
|
-
): Promise<string[]> {
|
|
1258
|
-
const modelsWithMetadata = await listDownloadedModelsWithMetadata(category);
|
|
1259
|
-
|
|
1260
|
-
if (modelsWithMetadata.length === 0) {
|
|
1261
|
-
return [];
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
// Keep at least this many models
|
|
1265
|
-
const keepCount = options?.keepCount ?? 1;
|
|
1266
|
-
if (modelsWithMetadata.length <= keepCount) {
|
|
1267
|
-
return [];
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
// Sort by lastUsed (oldest first), then by downloadedAt if no lastUsed
|
|
1271
|
-
const sorted = modelsWithMetadata.sort((a, b) => {
|
|
1272
|
-
const aTime = a.lastUsed ?? a.downloadedAt;
|
|
1273
|
-
const bTime = b.lastUsed ?? b.downloadedAt;
|
|
1274
|
-
return new Date(aTime).getTime() - new Date(bTime).getTime();
|
|
1275
|
-
});
|
|
1276
|
-
|
|
1277
|
-
const deletedIds: string[] = [];
|
|
1278
|
-
let bytesFreed = 0;
|
|
1279
|
-
const targetBytes = options?.targetBytes ?? 0;
|
|
1280
|
-
const maxToDelete = options?.maxModelsToDelete ?? sorted.length - keepCount;
|
|
1281
|
-
|
|
1282
|
-
for (let i = 0; i < sorted.length - keepCount && i < maxToDelete; i++) {
|
|
1283
|
-
const item = sorted[i];
|
|
1284
|
-
if (!item) continue;
|
|
1285
|
-
|
|
1286
|
-
try {
|
|
1287
|
-
await deleteModelByCategory(category, item.model.id);
|
|
1288
|
-
deletedIds.push(item.model.id);
|
|
1289
|
-
bytesFreed += item.sizeOnDisk ?? 0;
|
|
1290
|
-
|
|
1291
|
-
console.log(
|
|
1292
|
-
`[LRU Cleanup] Deleted ${category}:${item.model.id} (freed ${
|
|
1293
|
-
(item.sizeOnDisk ?? 0) / 1024 / 1024
|
|
1294
|
-
} MB)`
|
|
1295
|
-
);
|
|
1296
|
-
|
|
1297
|
-
if (targetBytes > 0 && bytesFreed >= targetBytes) {
|
|
1298
|
-
break;
|
|
1299
|
-
}
|
|
1300
|
-
} catch (error) {
|
|
1301
|
-
console.warn(
|
|
1302
|
-
`[LRU Cleanup] Failed to delete ${category}:${item.model.id}:`,
|
|
1303
|
-
error
|
|
1304
|
-
);
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
return deletedIds;
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
export async function deleteModelByCategory(
|
|
1312
|
-
category: ModelCategory,
|
|
1313
|
-
id: string
|
|
1314
|
-
): Promise<void> {
|
|
1315
|
-
const modelDir = getModelDir(category, id);
|
|
1316
|
-
const tarPath = getTarArchivePath(category, id);
|
|
1317
|
-
const onnxPath = getOnnxPath(category, id);
|
|
1318
|
-
if (await exists(modelDir)) {
|
|
1319
|
-
await unlink(modelDir);
|
|
1320
|
-
}
|
|
1321
|
-
if (await exists(tarPath)) {
|
|
1322
|
-
await unlink(tarPath);
|
|
1323
|
-
}
|
|
1324
|
-
if (await exists(onnxPath)) {
|
|
1325
|
-
await unlink(onnxPath);
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
export async function clearModelCacheByCategory(
|
|
1330
|
-
category: ModelCategory
|
|
1331
|
-
): Promise<void> {
|
|
1332
|
-
const cachePath = getCachePath(category);
|
|
1333
|
-
if (await exists(cachePath)) {
|
|
1334
|
-
await unlink(cachePath);
|
|
1335
|
-
}
|
|
1336
|
-
delete memoryCacheByCategory[category];
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
export async function getDownloadStorageBase(): Promise<string> {
|
|
1340
|
-
if (Platform.OS === 'ios') {
|
|
1341
|
-
return DocumentDirectoryPath;
|
|
1342
|
-
}
|
|
1343
|
-
return DocumentDirectoryPath;
|
|
1344
|
-
}
|
|
4
|
+
type TtsModelType,
|
|
5
|
+
type Quantization,
|
|
6
|
+
type SizeTier,
|
|
7
|
+
type ModelMetaBase,
|
|
8
|
+
type TtsModelMeta,
|
|
9
|
+
type DownloadProgress,
|
|
10
|
+
type DownloadResult,
|
|
11
|
+
type DownloadState,
|
|
12
|
+
type ExtractionState,
|
|
13
|
+
type DownloadProgressListener,
|
|
14
|
+
type ModelsListUpdatedListener,
|
|
15
|
+
type ModelWithMetadata,
|
|
16
|
+
} from './types';
|
|
17
|
+
export {
|
|
18
|
+
subscribeDownloadProgress,
|
|
19
|
+
subscribeModelsListUpdated,
|
|
20
|
+
} from './downloadEvents';
|
|
21
|
+
export {
|
|
22
|
+
listModelsByCategory,
|
|
23
|
+
refreshModelsByCategory,
|
|
24
|
+
getModelsCacheStatusByCategory,
|
|
25
|
+
getModelByIdByCategory,
|
|
26
|
+
} from './registry';
|
|
27
|
+
export {
|
|
28
|
+
listDownloadedModelsByCategory,
|
|
29
|
+
isModelDownloadedByCategory,
|
|
30
|
+
getLocalModelPathByCategory,
|
|
31
|
+
updateModelLastUsed,
|
|
32
|
+
listDownloadedModelsWithMetadata,
|
|
33
|
+
cleanupLeastRecentlyUsed,
|
|
34
|
+
deleteModelByCategory,
|
|
35
|
+
clearModelCacheByCategory,
|
|
36
|
+
getDownloadStorageBase,
|
|
37
|
+
} from './localModels';
|
|
38
|
+
export {
|
|
39
|
+
downloadModelByCategory,
|
|
40
|
+
getIncompleteDownloads,
|
|
41
|
+
resumeDownload,
|
|
42
|
+
deleteIncompleteDownload,
|
|
43
|
+
} from './downloadTask';
|
|
44
|
+
export {
|
|
45
|
+
extractModelByCategory,
|
|
46
|
+
getIncompleteExtractions,
|
|
47
|
+
resumeExtraction,
|
|
48
|
+
deleteIncompleteExtraction,
|
|
49
|
+
} from './modelExtraction';
|
|
50
|
+
export { ensureModelByCategory } from './ensureModel';
|
|
51
|
+
export type { EnsureModelOptions } from './ensureModel';
|
|
52
|
+
export { getProtectedModelKeysForBulkDelete } from './protectedModelKeys';
|
|
53
|
+
export {
|
|
54
|
+
purgeDownloadedModelArtifacts,
|
|
55
|
+
type PurgeDownloadedModelArtifactsResult,
|
|
56
|
+
} from './bulkPurge';
|