react-native-sherpa-onnx 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +89 -21
  3. package/SherpaOnnx.podspec +3 -0
  4. package/THIRD_PARTY_LICENSES/README.md +62 -0
  5. package/THIRD_PARTY_LICENSES/ffmpeg.txt +502 -0
  6. package/THIRD_PARTY_LICENSES/libarchive.txt +65 -0
  7. package/THIRD_PARTY_LICENSES/nvidia_omla.txt +181 -0
  8. package/THIRD_PARTY_LICENSES/onnxruntime.txt +21 -0
  9. package/THIRD_PARTY_LICENSES/opus.txt +44 -0
  10. package/THIRD_PARTY_LICENSES/sherpa-onnx.txt +201 -0
  11. package/THIRD_PARTY_LICENSES/shine.txt +482 -0
  12. package/THIRD_PARTY_LICENSES/zstd.txt +30 -0
  13. package/android/build.gradle +7 -3
  14. package/android/prebuilt-download.gradle +344 -152
  15. package/android/prebuilt-versions.gradle +1 -1
  16. package/android/src/main/assets/model_licenses/asr-models-license-status.csv +409 -0
  17. package/android/src/main/assets/model_licenses/qnn-asr-models-license-status.csv +695 -0
  18. package/android/src/main/assets/model_licenses/tts-models-license-status.csv +596 -0
  19. package/android/src/main/cpp/CMakeLists.txt +28 -10
  20. package/android/src/main/cpp/jni/archive/sherpa-onnx-archive-helper.cpp +2 -2
  21. package/android/src/main/cpp/jni/audio/sherpa-onnx-audio-convert-jni.cpp +268 -2
  22. package/android/src/main/cpp/jni/model_detect/sherpa-onnx-model-detect-tts.cpp +6 -2
  23. package/android/src/main/cpp/jni/model_detect/sherpa-onnx-validate-tts.cpp +4 -2
  24. package/android/src/main/java/com/sherpaonnx/SherpaOnnxArchiveHelper.kt +40 -10
  25. package/android/src/main/java/com/sherpaonnx/SherpaOnnxModule.kt +99 -0
  26. package/android/src/main/java/com/sherpaonnx/SherpaOnnxOnlineSttHelper.kt +4 -1
  27. package/android/src/main/java/com/sherpaonnx/SherpaOnnxTtsHelper.kt +112 -97
  28. package/ios/Resources/model_licenses/asr-models-license-status.csv +409 -0
  29. package/ios/Resources/model_licenses/qnn-asr-models-license-status.csv +695 -0
  30. package/ios/Resources/model_licenses/tts-models-license-status.csv +596 -0
  31. package/ios/SherpaOnnx+OnlineSTT.mm +2 -0
  32. package/ios/SherpaOnnx+PcmLiveStream.mm +2 -29
  33. package/ios/SherpaOnnx+TTS.mm +178 -20
  34. package/ios/SherpaOnnx.mm +54 -0
  35. package/ios/SherpaOnnxAudioConvert.h +10 -0
  36. package/ios/SherpaOnnxAudioConvert.mm +257 -1
  37. package/ios/archive/sherpa-onnx-archive-helper.h +3 -0
  38. package/ios/archive/sherpa-onnx-archive-helper.mm +39 -6
  39. package/ios/model_detect/sherpa-onnx-model-detect-tts.mm +13 -2
  40. package/ios/model_detect/sherpa-onnx-validate-tts.mm +4 -2
  41. package/ios/online_stt/sherpa-onnx-online-stt-wrapper.h +1 -0
  42. package/ios/online_stt/sherpa-onnx-online-stt-wrapper.mm +4 -0
  43. package/ios/tts/sherpa-onnx-tts-wrapper.h +37 -0
  44. package/ios/tts/sherpa-onnx-tts-wrapper.mm +149 -3
  45. package/lib/module/NativeSherpaOnnx.js.map +1 -1
  46. package/lib/module/audio/index.js +8 -0
  47. package/lib/module/audio/index.js.map +1 -1
  48. package/lib/module/download/ModelDownloadManager.js +10 -929
  49. package/lib/module/download/ModelDownloadManager.js.map +1 -1
  50. package/lib/module/download/activeModelOperations.js +26 -0
  51. package/lib/module/download/activeModelOperations.js.map +1 -0
  52. package/lib/module/download/background-downloader.d.js +2 -0
  53. package/lib/module/download/background-downloader.d.js.map +1 -0
  54. package/lib/module/download/bulkPurge.js +72 -0
  55. package/lib/module/download/bulkPurge.js.map +1 -0
  56. package/lib/module/download/checksumPrompt.js +19 -0
  57. package/lib/module/download/checksumPrompt.js.map +1 -0
  58. package/lib/module/download/constants.js +7 -0
  59. package/lib/module/download/constants.js.map +1 -0
  60. package/lib/module/download/downloadEvents.js +35 -0
  61. package/lib/module/download/downloadEvents.js.map +1 -0
  62. package/lib/module/download/downloadTask.js +385 -0
  63. package/lib/module/download/downloadTask.js.map +1 -0
  64. package/lib/module/download/ensureModel.js +89 -0
  65. package/lib/module/download/ensureModel.js.map +1 -0
  66. package/lib/module/download/index.js +4 -4
  67. package/lib/module/download/index.js.map +1 -1
  68. package/lib/module/download/localModels.js +151 -0
  69. package/lib/module/download/localModels.js.map +1 -0
  70. package/lib/module/download/modelExtraction.js +174 -0
  71. package/lib/module/download/modelExtraction.js.map +1 -0
  72. package/lib/module/download/paths.js +98 -0
  73. package/lib/module/download/paths.js.map +1 -0
  74. package/lib/module/download/postDownloadProcessing.js +206 -0
  75. package/lib/module/download/postDownloadProcessing.js.map +1 -0
  76. package/lib/module/download/protectedModelKeys.js +31 -0
  77. package/lib/module/download/protectedModelKeys.js.map +1 -0
  78. package/lib/module/download/registry.js +267 -0
  79. package/lib/module/download/registry.js.map +1 -0
  80. package/lib/module/download/retry.js +59 -0
  81. package/lib/module/download/retry.js.map +1 -0
  82. package/lib/module/download/types.js +17 -0
  83. package/lib/module/download/types.js.map +1 -0
  84. package/lib/module/download/validation.js +101 -5
  85. package/lib/module/download/validation.js.map +1 -1
  86. package/lib/module/{download → extraction}/extractTarBz2.js +3 -1
  87. package/lib/module/extraction/extractTarBz2.js.map +1 -0
  88. package/lib/module/{download → extraction}/extractTarZst.js +3 -1
  89. package/lib/module/extraction/extractTarZst.js.map +1 -0
  90. package/lib/module/extraction/index.js +3 -4
  91. package/lib/module/extraction/index.js.map +1 -1
  92. package/lib/module/index.js +1 -1
  93. package/lib/module/index.js.map +1 -1
  94. package/lib/module/licenses.js +63 -0
  95. package/lib/module/licenses.js.map +1 -0
  96. package/lib/module/stt/index.js +16 -2
  97. package/lib/module/stt/index.js.map +1 -1
  98. package/lib/module/stt/streaming.js +2 -0
  99. package/lib/module/stt/streaming.js.map +1 -1
  100. package/lib/module/stt/streamingTypes.js.map +1 -1
  101. package/lib/module/stt/types.js.map +1 -1
  102. package/lib/module/tts/index.js +20 -2
  103. package/lib/module/tts/index.js.map +1 -1
  104. package/lib/module/tts/streaming.js +4 -0
  105. package/lib/module/tts/streaming.js.map +1 -1
  106. package/lib/module/tts/types.js.map +1 -1
  107. package/lib/module/utils.js +16 -1
  108. package/lib/module/utils.js.map +1 -1
  109. package/lib/typescript/src/NativeSherpaOnnx.d.ts +33 -5
  110. package/lib/typescript/src/NativeSherpaOnnx.d.ts.map +1 -1
  111. package/lib/typescript/src/audio/index.d.ts +10 -0
  112. package/lib/typescript/src/audio/index.d.ts.map +1 -1
  113. package/lib/typescript/src/download/ModelDownloadManager.d.ts +10 -108
  114. package/lib/typescript/src/download/ModelDownloadManager.d.ts.map +1 -1
  115. package/lib/typescript/src/download/activeModelOperations.d.ts +6 -0
  116. package/lib/typescript/src/download/activeModelOperations.d.ts.map +1 -0
  117. package/lib/typescript/src/download/bulkPurge.d.ts +14 -0
  118. package/lib/typescript/src/download/bulkPurge.d.ts.map +1 -0
  119. package/lib/typescript/src/download/checksumPrompt.d.ts +3 -0
  120. package/lib/typescript/src/download/checksumPrompt.d.ts.map +1 -0
  121. package/lib/typescript/src/download/constants.d.ts +5 -0
  122. package/lib/typescript/src/download/constants.d.ts.map +1 -0
  123. package/lib/typescript/src/download/downloadEvents.d.ts +6 -0
  124. package/lib/typescript/src/download/downloadEvents.d.ts.map +1 -0
  125. package/lib/typescript/src/download/downloadTask.d.ts +20 -0
  126. package/lib/typescript/src/download/downloadTask.d.ts.map +1 -0
  127. package/lib/typescript/src/download/ensureModel.d.ts +26 -0
  128. package/lib/typescript/src/download/ensureModel.d.ts.map +1 -0
  129. package/lib/typescript/src/download/index.d.ts +7 -7
  130. package/lib/typescript/src/download/index.d.ts.map +1 -1
  131. package/lib/typescript/src/download/localModels.d.ts +15 -0
  132. package/lib/typescript/src/download/localModels.d.ts.map +1 -0
  133. package/lib/typescript/src/download/modelExtraction.d.ts +36 -0
  134. package/lib/typescript/src/download/modelExtraction.d.ts.map +1 -0
  135. package/lib/typescript/src/download/paths.d.ts +28 -0
  136. package/lib/typescript/src/download/paths.d.ts.map +1 -0
  137. package/lib/typescript/src/download/postDownloadProcessing.d.ts +19 -0
  138. package/lib/typescript/src/download/postDownloadProcessing.d.ts.map +1 -0
  139. package/lib/typescript/src/download/protectedModelKeys.d.ts +6 -0
  140. package/lib/typescript/src/download/protectedModelKeys.d.ts.map +1 -0
  141. package/lib/typescript/src/download/registry.d.ts +14 -0
  142. package/lib/typescript/src/download/registry.d.ts.map +1 -0
  143. package/lib/typescript/src/download/retry.d.ts +15 -0
  144. package/lib/typescript/src/download/retry.d.ts.map +1 -0
  145. package/lib/typescript/src/download/types.d.ts +96 -0
  146. package/lib/typescript/src/download/types.d.ts.map +1 -0
  147. package/lib/typescript/src/download/validation.d.ts +19 -0
  148. package/lib/typescript/src/download/validation.d.ts.map +1 -1
  149. package/lib/typescript/src/extraction/extractTarBz2.d.ts.map +1 -0
  150. package/lib/typescript/src/extraction/extractTarZst.d.ts.map +1 -0
  151. package/lib/typescript/src/index.d.ts +1 -0
  152. package/lib/typescript/src/index.d.ts.map +1 -1
  153. package/lib/typescript/src/licenses.d.ts +10 -0
  154. package/lib/typescript/src/licenses.d.ts.map +1 -0
  155. package/lib/typescript/src/stt/index.d.ts +4 -1
  156. package/lib/typescript/src/stt/index.d.ts.map +1 -1
  157. package/lib/typescript/src/stt/streaming.d.ts.map +1 -1
  158. package/lib/typescript/src/stt/streamingTypes.d.ts +5 -0
  159. package/lib/typescript/src/stt/streamingTypes.d.ts.map +1 -1
  160. package/lib/typescript/src/stt/types.d.ts +3 -1
  161. package/lib/typescript/src/stt/types.d.ts.map +1 -1
  162. package/lib/typescript/src/tts/index.d.ts +3 -1
  163. package/lib/typescript/src/tts/index.d.ts.map +1 -1
  164. package/lib/typescript/src/tts/streaming.d.ts.map +1 -1
  165. package/lib/typescript/src/tts/types.d.ts +6 -5
  166. package/lib/typescript/src/tts/types.d.ts.map +1 -1
  167. package/lib/typescript/src/utils.d.ts +5 -0
  168. package/lib/typescript/src/utils.d.ts.map +1 -1
  169. package/package.json +6 -1
  170. package/scripts/{check-model-csvs.sh → ci/check-model-csvs.sh} +9 -2
  171. package/scripts/ci/collect_all_sherpa_model_streams.sh +101 -0
  172. package/scripts/ci/collect_one_sherpa_release_stream.sh +189 -0
  173. package/scripts/ci/sherpa_asr_model_release_streams.json +21 -0
  174. package/scripts/ci/sherpa_tts_model_release_streams.json +13 -0
  175. package/scripts/ci/update_model_license_csv.sh +765 -0
  176. package/scripts/setup-ios-framework.sh +14 -11
  177. package/scripts/update_commercial_use.js +73 -0
  178. package/src/NativeSherpaOnnx.ts +36 -5
  179. package/src/audio/index.ts +20 -0
  180. package/src/download/ModelDownloadManager.ts +55 -1343
  181. package/src/download/activeModelOperations.ts +38 -0
  182. package/src/download/background-downloader.d.ts +43 -0
  183. package/src/download/bulkPurge.ts +102 -0
  184. package/src/download/checksumPrompt.ts +25 -0
  185. package/src/download/constants.ts +5 -0
  186. package/src/download/downloadEvents.ts +55 -0
  187. package/src/download/downloadTask.ts +497 -0
  188. package/src/download/ensureModel.ts +124 -0
  189. package/src/download/index.ts +19 -4
  190. package/src/download/localModels.ts +234 -0
  191. package/src/download/modelExtraction.ts +244 -0
  192. package/src/download/paths.ts +134 -0
  193. package/src/download/postDownloadProcessing.ts +292 -0
  194. package/src/download/protectedModelKeys.ts +30 -0
  195. package/src/download/registry.ts +404 -0
  196. package/src/download/retry.ts +76 -0
  197. package/src/download/types.ts +120 -0
  198. package/src/download/validation.ts +114 -8
  199. package/src/{download → extraction}/extractTarBz2.ts +3 -1
  200. package/src/{download → extraction}/extractTarZst.ts +3 -1
  201. package/src/extraction/index.ts +3 -7
  202. package/src/index.tsx +1 -0
  203. package/src/licenses.ts +100 -0
  204. package/src/stt/index.ts +20 -2
  205. package/src/stt/streaming.ts +3 -0
  206. package/src/stt/streamingTypes.ts +5 -0
  207. package/src/stt/types.ts +3 -1
  208. package/src/tts/index.ts +30 -2
  209. package/src/tts/streaming.ts +10 -0
  210. package/src/tts/types.ts +6 -5
  211. package/src/utils.ts +22 -1
  212. package/third_party/sherpa-onnx-prebuilt/ANDROID_RELEASE_TAG +1 -1
  213. package/third_party/sherpa-onnx-prebuilt/IOS_RELEASE_TAG +1 -1
  214. package/android/src/main/cpp/jni/tts/sherpa-onnx-tts-zipvoice-jni.cpp +0 -301
  215. package/android/src/main/java/com/sherpaonnx/ZipvoiceTtsWrapper.kt +0 -187
  216. package/lib/module/download/extractTarBz2.js.map +0 -1
  217. package/lib/module/download/extractTarZst.js.map +0 -1
  218. package/lib/typescript/src/download/extractTarBz2.d.ts.map +0 -1
  219. package/lib/typescript/src/download/extractTarZst.d.ts.map +0 -1
  220. package/scripts/check-qnn-support.sh +0 -78
  221. /package/lib/typescript/src/{download → extraction}/extractTarBz2.d.ts +0 -0
  222. /package/lib/typescript/src/{download → extraction}/extractTarZst.d.ts +0 -0
@@ -0,0 +1,404 @@
1
+ import {
2
+ exists,
3
+ readFile,
4
+ mkdir,
5
+ writeFile,
6
+ } from '@dr.pogodin/react-native-fs';
7
+ import { ModelCategory } from './types';
8
+ import type {
9
+ ModelMetaBase,
10
+ ModelArchiveExt,
11
+ CachePayload,
12
+ CacheStatus,
13
+ TtsModelType,
14
+ Quantization,
15
+ SizeTier,
16
+ TtsModelMeta,
17
+ } from './types';
18
+ import {
19
+ CACHE_TTL_MINUTES,
20
+ MODEL_ARCHIVE_EXT,
21
+ MODEL_ONNX_EXT,
22
+ } from './constants';
23
+ import {
24
+ getCacheDir,
25
+ getCachePath,
26
+ getArchiveFilename,
27
+ getReleaseUrl,
28
+ CATEGORY_CONFIG,
29
+ } from './paths';
30
+ import { parseChecksumFile } from './validation';
31
+ import { retryWithBackoff } from './retry';
32
+ import { emitModelsListUpdated } from './downloadEvents';
33
+
34
+ const memoryCacheByCategory: Partial<
35
+ Record<ModelCategory, CachePayload<ModelMetaBase>>
36
+ > = {};
37
+
38
+ const checksumCacheByCategory: Partial<
39
+ Record<ModelCategory, Map<string, string>>
40
+ > = {};
41
+
42
+ export async function fetchChecksumsFromRelease(
43
+ category: ModelCategory
44
+ ): Promise<Map<string, string>> {
45
+ if (category === ModelCategory.Qnn) {
46
+ return new Map<string, string>();
47
+ }
48
+ if (checksumCacheByCategory[category]) {
49
+ return checksumCacheByCategory[category]!;
50
+ }
51
+ try {
52
+ const checksums = await retryWithBackoff(
53
+ async () => {
54
+ const response = await fetch(
55
+ `https://github.com/k2-fsa/sherpa-onnx/releases/download/${CATEGORY_CONFIG[category].tag}/checksum.txt`
56
+ );
57
+ if (!response.ok) {
58
+ throw new Error(
59
+ `Failed to fetch checksum.txt for ${category}: ${response.status}`
60
+ );
61
+ }
62
+ const content = await response.text();
63
+ return parseChecksumFile(content);
64
+ },
65
+ { maxRetries: 3, initialDelayMs: 1000 }
66
+ );
67
+ checksumCacheByCategory[category] = checksums;
68
+ return checksums;
69
+ } catch (error) {
70
+ console.warn(
71
+ `SherpaOnnxChecksum: Error fetching checksums for ${category}:`,
72
+ error
73
+ );
74
+ return new Map();
75
+ }
76
+ }
77
+
78
+ function toTitleCase(value: string): string {
79
+ return value
80
+ .split(/[-_\s]+/g)
81
+ .filter(Boolean)
82
+ .map((token) => token[0]!.toUpperCase() + token.slice(1))
83
+ .join(' ');
84
+ }
85
+
86
+ function deriveDisplayName(id: string): string {
87
+ const cleaned = id.replace(/^sherpa-onnx-/, '');
88
+ return toTitleCase(cleaned);
89
+ }
90
+
91
+ function deriveType(id: string): TtsModelType {
92
+ const lower = id.toLowerCase();
93
+ if (lower.includes('vits')) return 'vits';
94
+ if (lower.includes('kokoro')) return 'kokoro';
95
+ if (lower.includes('matcha')) return 'matcha';
96
+ if (lower.includes('kitten')) return 'kitten';
97
+ if (lower.includes('pocket')) return 'pocket';
98
+ if (lower.includes('zipvoice')) return 'zipvoice';
99
+ return 'unknown';
100
+ }
101
+
102
+ function deriveQuantization(id: string): Quantization {
103
+ const lower = id.toLowerCase();
104
+ if (lower.includes('int8') && lower.includes('quant')) {
105
+ return 'int8-quantized';
106
+ }
107
+ if (lower.includes('int8')) return 'int8';
108
+ if (lower.includes('fp16')) return 'fp16';
109
+ return 'unknown';
110
+ }
111
+
112
+ function deriveSizeTier(id: string): SizeTier {
113
+ const lower = id.toLowerCase();
114
+ if (lower.includes('tiny')) return 'tiny';
115
+ if (lower.includes('small')) return 'small';
116
+ if (lower.includes('medium')) return 'medium';
117
+ if (lower.includes('large')) return 'large';
118
+ if (lower.includes('low')) return 'small';
119
+ return 'unknown';
120
+ }
121
+
122
+ function deriveLanguages(id: string): string[] {
123
+ const tokens = id.split(/[-_]+/g);
124
+ const languages = new Set<string>();
125
+ for (const token of tokens) {
126
+ if (/^[a-z]{2}$/.test(token)) {
127
+ languages.add(token);
128
+ continue;
129
+ }
130
+ if (/^[a-z]{2}[A-Z]{2}$/.test(token)) {
131
+ languages.add(token.slice(0, 2).toLowerCase());
132
+ continue;
133
+ }
134
+ if (/^[a-z]{2}-[A-Z]{2}$/.test(token)) {
135
+ languages.add(token.slice(0, 2).toLowerCase());
136
+ }
137
+ }
138
+ return Array.from(languages);
139
+ }
140
+
141
+ function getAssetExtension(name: string): ModelArchiveExt | null {
142
+ if (name.endsWith(MODEL_ARCHIVE_EXT)) return 'tar.bz2';
143
+ if (name.endsWith(MODEL_ONNX_EXT)) return 'onnx';
144
+ return null;
145
+ }
146
+
147
+ function stripAssetExtension(name: string, ext: ModelArchiveExt): string {
148
+ const suffix = `.${ext}`;
149
+ return name.endsWith(suffix) ? name.slice(0, -suffix.length) : name;
150
+ }
151
+
152
+ function isAssetSupportedForCategory(
153
+ category: ModelCategory,
154
+ name: string,
155
+ ext: ModelArchiveExt
156
+ ): boolean {
157
+ const lower = name.toLowerCase();
158
+ switch (category) {
159
+ case ModelCategory.Tts:
160
+ return ext === 'tar.bz2';
161
+ case ModelCategory.Stt:
162
+ return ext === 'tar.bz2' && !lower.includes('vad');
163
+ case ModelCategory.Vad:
164
+ return ext === 'onnx' && lower.includes('vad');
165
+ case ModelCategory.Diarization:
166
+ return ext === 'tar.bz2';
167
+ case ModelCategory.Enhancement:
168
+ return ext === 'onnx';
169
+ case ModelCategory.Separation:
170
+ return ext === 'tar.bz2' || ext === 'onnx';
171
+ case ModelCategory.Qnn:
172
+ return (
173
+ ext === 'tar.bz2' &&
174
+ lower.includes('sherpa-onnx-qnn') &&
175
+ lower.includes('binary') &&
176
+ lower.includes('seconds')
177
+ );
178
+ default:
179
+ return false;
180
+ }
181
+ }
182
+
183
+ function parseDigestSha256(value?: string | null): string | undefined {
184
+ if (!value) return undefined;
185
+ const match = value.match(/^sha256:([a-f0-9]{64})$/i);
186
+ return match?.[1]?.toLowerCase();
187
+ }
188
+
189
+ function toTtsModelMeta(
190
+ asset: {
191
+ name: string;
192
+ size: number;
193
+ browser_download_url: string;
194
+ digest?: string | null;
195
+ },
196
+ archiveExt: ModelArchiveExt
197
+ ): TtsModelMeta | null {
198
+ if (archiveExt !== 'tar.bz2') return null;
199
+ const id = stripAssetExtension(asset.name, archiveExt);
200
+ const type = deriveType(id);
201
+ if (type === 'unknown') {
202
+ console.warn('SherpaOnnxModelList: Unsupported model', id);
203
+ }
204
+ return {
205
+ id,
206
+ displayName: deriveDisplayName(id),
207
+ type,
208
+ languages: deriveLanguages(id),
209
+ quantization: deriveQuantization(id),
210
+ sizeTier: deriveSizeTier(id),
211
+ downloadUrl: asset.browser_download_url,
212
+ archiveExt,
213
+ bytes: asset.size,
214
+ sha256: parseDigestSha256(asset.digest),
215
+ category: ModelCategory.Tts,
216
+ };
217
+ }
218
+
219
+ function toGenericModelMeta(
220
+ category: ModelCategory,
221
+ asset: {
222
+ name: string;
223
+ size: number;
224
+ browser_download_url: string;
225
+ digest?: string | null;
226
+ },
227
+ archiveExt: ModelArchiveExt
228
+ ): ModelMetaBase | null {
229
+ const id = stripAssetExtension(asset.name, archiveExt);
230
+ return {
231
+ id,
232
+ displayName: deriveDisplayName(id),
233
+ downloadUrl: asset.browser_download_url,
234
+ archiveExt,
235
+ bytes: asset.size,
236
+ sha256: parseDigestSha256(asset.digest),
237
+ category,
238
+ };
239
+ }
240
+
241
+ function toModelMeta(
242
+ category: ModelCategory,
243
+ asset: {
244
+ name: string;
245
+ size: number;
246
+ browser_download_url: string;
247
+ digest?: string | null;
248
+ }
249
+ ): ModelMetaBase | null {
250
+ const archiveExt = getAssetExtension(asset.name);
251
+ if (!archiveExt) return null;
252
+ if (!isAssetSupportedForCategory(category, asset.name, archiveExt)) {
253
+ return null;
254
+ }
255
+ if (category === ModelCategory.Tts) {
256
+ return toTtsModelMeta(asset, archiveExt);
257
+ }
258
+ return toGenericModelMeta(category, asset, archiveExt);
259
+ }
260
+
261
+ async function loadCacheFromDisk<T extends ModelMetaBase>(
262
+ category: ModelCategory
263
+ ): Promise<CachePayload<T> | null> {
264
+ const memoryCache = memoryCacheByCategory[category] as
265
+ | CachePayload<T>
266
+ | undefined;
267
+ if (memoryCache) return memoryCache;
268
+ const cachePath = getCachePath(category);
269
+ const existsResult = await exists(cachePath);
270
+ if (!existsResult) return null;
271
+ const raw = await readFile(cachePath, 'utf8');
272
+ const parsed = JSON.parse(raw) as CachePayload<T>;
273
+ memoryCacheByCategory[category] = parsed as CachePayload<ModelMetaBase>;
274
+ return parsed;
275
+ }
276
+
277
+ async function saveCache<T extends ModelMetaBase>(
278
+ category: ModelCategory,
279
+ payload: CachePayload<T>
280
+ ): Promise<void> {
281
+ await mkdir(getCacheDir());
282
+ await writeFile(getCachePath(category), JSON.stringify(payload), 'utf8');
283
+ memoryCacheByCategory[category] = payload as CachePayload<ModelMetaBase>;
284
+ }
285
+
286
+ function isCacheFresh<T extends ModelMetaBase>(
287
+ payload: CachePayload<T>,
288
+ ttlMinutes: number
289
+ ): boolean {
290
+ const updated = new Date(payload.lastUpdated).getTime();
291
+ if (!updated) return false;
292
+ const ageMs = Date.now() - updated;
293
+ return ageMs < ttlMinutes * 60 * 1000;
294
+ }
295
+
296
+ export async function listModelsByCategory<T extends ModelMetaBase>(
297
+ category: ModelCategory
298
+ ): Promise<T[]> {
299
+ const cache = await loadCacheFromDisk<T>(category);
300
+ return cache?.models ?? [];
301
+ }
302
+
303
+ export async function refreshModelsByCategory<T extends ModelMetaBase>(
304
+ category: ModelCategory,
305
+ options?: {
306
+ forceRefresh?: boolean;
307
+ cacheTtlMinutes?: number;
308
+ maxRetries?: number;
309
+ signal?: AbortSignal;
310
+ }
311
+ ): Promise<T[]> {
312
+ const ttl = options?.cacheTtlMinutes ?? CACHE_TTL_MINUTES;
313
+ const cached = await loadCacheFromDisk<T>(category);
314
+
315
+ if (!options?.forceRefresh && cached && isCacheFresh(cached, ttl)) {
316
+ return cached.models;
317
+ }
318
+
319
+ try {
320
+ const body = await retryWithBackoff(
321
+ async () => {
322
+ const response = await fetch(getReleaseUrl(category));
323
+ if (!response.ok) {
324
+ throw new Error(`Failed to fetch models: ${response.status}`);
325
+ }
326
+ return response.json();
327
+ },
328
+ {
329
+ maxRetries: options?.maxRetries ?? 3,
330
+ initialDelayMs: 1000,
331
+ signal: options?.signal,
332
+ }
333
+ );
334
+
335
+ const assets = Array.isArray(body?.assets) ? body.assets : [];
336
+ const models: T[] = assets
337
+ .map(
338
+ (asset: {
339
+ name: string;
340
+ size: number;
341
+ browser_download_url: string;
342
+ digest?: string | null;
343
+ }) =>
344
+ toModelMeta(category, {
345
+ name: asset.name,
346
+ size: asset.size,
347
+ browser_download_url: asset.browser_download_url,
348
+ digest: asset.digest,
349
+ })
350
+ )
351
+ .filter((model: ModelMetaBase | null): model is T => Boolean(model));
352
+
353
+ const checksums = await fetchChecksumsFromRelease(category);
354
+ for (const model of models) {
355
+ const archiveFilename = getArchiveFilename(model.id, model.archiveExt);
356
+ const sha256 = checksums.get(archiveFilename);
357
+ if (sha256) {
358
+ model.sha256 = sha256;
359
+ } else if (model.sha256) {
360
+ model.sha256 = model.sha256.toLowerCase();
361
+ }
362
+ }
363
+
364
+ const payload: CachePayload<T> = {
365
+ lastUpdated: new Date().toISOString(),
366
+ models,
367
+ };
368
+ await saveCache(category, payload);
369
+ emitModelsListUpdated(category, models as ModelMetaBase[]);
370
+ return models;
371
+ } catch (error) {
372
+ if (cached) {
373
+ console.warn(
374
+ `Failed to refresh models for ${category}, using cached data:`,
375
+ error
376
+ );
377
+ return cached.models;
378
+ }
379
+ throw error;
380
+ }
381
+ }
382
+
383
+ export async function getModelsCacheStatusByCategory(
384
+ category: ModelCategory
385
+ ): Promise<CacheStatus> {
386
+ const cached = await loadCacheFromDisk(category);
387
+ if (!cached) {
388
+ return { lastUpdated: null, source: 'cache' };
389
+ }
390
+ return { lastUpdated: cached.lastUpdated, source: 'cache' };
391
+ }
392
+
393
+ export async function getModelByIdByCategory<T extends ModelMetaBase>(
394
+ category: ModelCategory,
395
+ id: string
396
+ ): Promise<T | null> {
397
+ const models = await listModelsByCategory<T>(category);
398
+ return models.find((model) => model.id === id) ?? null;
399
+ }
400
+
401
+ export function clearMemoryCacheForCategory(category: ModelCategory): void {
402
+ delete memoryCacheByCategory[category];
403
+ delete checksumCacheByCategory[category];
404
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Retry helper with exponential backoff
3
+ * @param fn - The async function to retry
4
+ * @param options - Retry configuration
5
+ * @returns The result of the function
6
+ * @throws The last error if all retries fail or AbortError if aborted
7
+ */
8
+ export async function retryWithBackoff<T>(
9
+ fn: () => Promise<T>,
10
+ options: {
11
+ maxRetries?: number;
12
+ initialDelayMs?: number;
13
+ maxDelayMs?: number;
14
+ backoffFactor?: number;
15
+ signal?: AbortSignal;
16
+ } = {}
17
+ ): Promise<T> {
18
+ const maxRetries = options.maxRetries ?? 3;
19
+ const initialDelayMs = options.initialDelayMs ?? 1000;
20
+ const maxDelayMs = options.maxDelayMs ?? 10000;
21
+ const backoffFactor = options.backoffFactor ?? 2;
22
+
23
+ let lastError: Error | undefined;
24
+
25
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
26
+ if (options.signal?.aborted) {
27
+ const abortError = new Error('Operation aborted');
28
+ abortError.name = 'AbortError';
29
+ throw abortError;
30
+ }
31
+
32
+ try {
33
+ return await fn();
34
+ } catch (error) {
35
+ lastError = error instanceof Error ? error : new Error(String(error));
36
+
37
+ // Don't retry on abort
38
+ if (lastError.name === 'AbortError' || options.signal?.aborted) {
39
+ throw lastError;
40
+ }
41
+
42
+ // If this was the last attempt, throw the error
43
+ if (attempt === maxRetries) {
44
+ throw lastError;
45
+ }
46
+
47
+ // Calculate delay with exponential backoff
48
+ const delayMs = Math.min(
49
+ initialDelayMs * Math.pow(backoffFactor, attempt),
50
+ maxDelayMs
51
+ );
52
+
53
+ console.warn(
54
+ `Retry attempt ${attempt + 1}/${maxRetries} after ${delayMs}ms due to:`,
55
+ lastError.message
56
+ );
57
+
58
+ // Wait before retrying (abort-aware: cancel the delay if signal fires)
59
+ await new Promise<void>((resolve, reject) => {
60
+ const timer = setTimeout(resolve, delayMs);
61
+ if (options.signal) {
62
+ const onAbort = () => {
63
+ clearTimeout(timer);
64
+ options.signal!.removeEventListener('abort', onAbort);
65
+ const abortErr = new Error('Operation aborted');
66
+ abortErr.name = 'AbortError';
67
+ reject(abortErr);
68
+ };
69
+ options.signal.addEventListener('abort', onAbort);
70
+ }
71
+ });
72
+ }
73
+ }
74
+
75
+ throw lastError ?? new Error('Retry failed with no error');
76
+ }
@@ -0,0 +1,120 @@
1
+ import type { TTSModelType } from '../tts/types';
2
+
3
+ export enum ModelCategory {
4
+ Tts = 'tts',
5
+ Stt = 'stt',
6
+ Vad = 'vad',
7
+ Diarization = 'diarization',
8
+ Enhancement = 'enhancement',
9
+ Separation = 'separation',
10
+ Qnn = 'qnn',
11
+ }
12
+
13
+ /** TTS model type for meta; 'unknown' when id could not be classified. */
14
+ export type TtsModelType = TTSModelType | 'unknown';
15
+
16
+ export type Quantization = 'fp16' | 'int8' | 'int8-quantized' | 'unknown';
17
+
18
+ export type SizeTier = 'tiny' | 'small' | 'medium' | 'large' | 'unknown';
19
+
20
+ export type ModelArchiveExt = 'tar.bz2' | 'onnx';
21
+
22
+ export type ModelMetaBase = {
23
+ id: string;
24
+ displayName: string;
25
+ downloadUrl: string;
26
+ archiveExt: ModelArchiveExt;
27
+ bytes: number;
28
+ sha256?: string;
29
+ category: ModelCategory;
30
+ };
31
+
32
+ export type TtsModelMeta = ModelMetaBase & {
33
+ type: TtsModelType;
34
+ languages: string[];
35
+ quantization: Quantization;
36
+ sizeTier: SizeTier;
37
+ category: ModelCategory.Tts;
38
+ };
39
+
40
+ export type DownloadProgress = {
41
+ bytesDownloaded: number;
42
+ totalBytes: number;
43
+ percent: number;
44
+ phase?: 'downloading' | 'extracting';
45
+ speed?: number;
46
+ eta?: number;
47
+ };
48
+
49
+ export type DownloadResult = {
50
+ modelId: string;
51
+ localPath: string;
52
+ };
53
+
54
+ export type DownloadState = {
55
+ modelId: string;
56
+ category: ModelCategory;
57
+ phase: 'downloading' | 'extracting';
58
+ startedAt: string;
59
+ archivePath: string;
60
+ model: ModelMetaBase;
61
+ bytesDownloaded?: number;
62
+ totalBytes?: number;
63
+ };
64
+
65
+ /** State for an in-progress or interrupted model extraction (archive --> model dir). */
66
+ export type ExtractionState = {
67
+ modelId: string;
68
+ category: ModelCategory;
69
+ phase: 'extracting';
70
+ startedAt: string;
71
+ archivePath: string;
72
+ modelDir: string;
73
+ model: ModelMetaBase;
74
+ };
75
+
76
+ export type DownloadProgressListener = (
77
+ category: ModelCategory,
78
+ modelId: string,
79
+ progress: DownloadProgress
80
+ ) => void;
81
+
82
+ export type ModelsListUpdatedListener = (
83
+ category: ModelCategory,
84
+ models: ModelMetaBase[]
85
+ ) => void;
86
+
87
+ export type ModelManifest<T extends ModelMetaBase = ModelMetaBase> = {
88
+ downloadedAt: string;
89
+ lastUsed?: string;
90
+ model: T;
91
+ sizeOnDisk?: number;
92
+ };
93
+
94
+ export type ModelWithMetadata<T extends ModelMetaBase = ModelMetaBase> = {
95
+ model: T;
96
+ downloadedAt: string;
97
+ lastUsed: string | null;
98
+ sizeOnDisk?: number;
99
+ status: 'ready' | 'downloading' | 'extracting' | 'failed';
100
+ progress?: number;
101
+ };
102
+
103
+ export type ChecksumIssue = {
104
+ category: ModelCategory;
105
+ id: string;
106
+ archivePath: string;
107
+ expected?: string;
108
+ message: string;
109
+ reason: 'CHECKSUM_FAILED' | 'CHECKSUM_MISMATCH';
110
+ };
111
+
112
+ export type CachePayload<T extends ModelMetaBase> = {
113
+ lastUpdated: string;
114
+ models: T[];
115
+ };
116
+
117
+ export type CacheStatus = {
118
+ lastUpdated: string | null;
119
+ source: 'cache' | 'remote';
120
+ };