react-native-sherpa-onnx 0.3.4 → 0.3.6

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 (45) hide show
  1. package/README.md +2 -1
  2. package/SherpaOnnx.podspec +26 -5
  3. package/android/prebuilt-download.gradle +1 -1
  4. package/android/prebuilt-versions.gradle +1 -1
  5. package/android/src/main/cpp/jni/archive/sherpa-onnx-archive-helper.cpp +306 -6
  6. package/android/src/main/cpp/jni/archive/sherpa-onnx-archive-helper.h +33 -4
  7. package/android/src/main/cpp/jni/archive/sherpa-onnx-archive-jni.cpp +266 -7
  8. package/android/src/main/java/com/sherpaonnx/SherpaOnnxArchiveHelper.kt +100 -0
  9. package/android/src/main/java/com/sherpaonnx/SherpaOnnxAssetHelper.kt +51 -6
  10. package/android/src/main/java/com/sherpaonnx/SherpaOnnxModule.kt +60 -0
  11. package/ios/SherpaOnnx.mm +54 -1
  12. package/ios/archive/sherpa-onnx-archive-helper.h +7 -0
  13. package/ios/archive/sherpa-onnx-archive-helper.mm +18 -0
  14. package/lib/module/NativeSherpaOnnx.js.map +1 -1
  15. package/lib/module/download/extractTarZst.js +52 -0
  16. package/lib/module/download/extractTarZst.js.map +1 -0
  17. package/lib/module/download/index.js +1 -0
  18. package/lib/module/download/index.js.map +1 -1
  19. package/lib/module/extraction/index.js +191 -0
  20. package/lib/module/extraction/index.js.map +1 -0
  21. package/lib/module/extraction/types.js +2 -0
  22. package/lib/module/extraction/types.js.map +1 -0
  23. package/lib/module/index.js +1 -0
  24. package/lib/module/index.js.map +1 -1
  25. package/lib/typescript/src/NativeSherpaOnnx.d.ts +39 -0
  26. package/lib/typescript/src/NativeSherpaOnnx.d.ts.map +1 -1
  27. package/lib/typescript/src/download/extractTarZst.d.ts +14 -0
  28. package/lib/typescript/src/download/extractTarZst.d.ts.map +1 -0
  29. package/lib/typescript/src/download/index.d.ts +2 -0
  30. package/lib/typescript/src/download/index.d.ts.map +1 -1
  31. package/lib/typescript/src/extraction/index.d.ts +50 -0
  32. package/lib/typescript/src/extraction/index.d.ts.map +1 -0
  33. package/lib/typescript/src/extraction/types.d.ts +60 -0
  34. package/lib/typescript/src/extraction/types.d.ts.map +1 -0
  35. package/lib/typescript/src/index.d.ts.map +1 -1
  36. package/package.json +6 -1
  37. package/scripts/check-model-csvs.sh +2 -2
  38. package/src/NativeSherpaOnnx.ts +56 -0
  39. package/src/download/extractTarZst.ts +77 -0
  40. package/src/download/index.ts +2 -0
  41. package/src/extraction/index.ts +273 -0
  42. package/src/extraction/types.ts +63 -0
  43. package/src/index.tsx +1 -0
  44. package/third_party/libarchive_prebuilt/ANDROID_RELEASE_TAG +1 -1
  45. package/third_party/libarchive_prebuilt/IOS_RELEASE_TAG +1 -1
@@ -46,7 +46,7 @@ TTS_CSV_NAMES=$(csv_asset_names "$TTS_CSV")
46
46
  ASR_MISSING=""
47
47
  while IFS= read -r asset; do
48
48
  [ -z "$asset" ] && continue
49
- if ! echo "$ASR_CSV_NAMES" | grep -qFx "$asset"; then
49
+ if ! grep -qFx -- "$asset" <<< "$ASR_CSV_NAMES"; then
50
50
  ASR_MISSING="${ASR_MISSING} - ${asset}\n"
51
51
  fi
52
52
  done <<< "$ASR_ASSETS"
@@ -54,7 +54,7 @@ done <<< "$ASR_ASSETS"
54
54
  TTS_MISSING=""
55
55
  while IFS= read -r asset; do
56
56
  [ -z "$asset" ] && continue
57
- if ! echo "$TTS_CSV_NAMES" | grep -qFx "$asset"; then
57
+ if ! grep -qFx -- "$asset" <<< "$TTS_CSV_NAMES"; then
58
58
  TTS_MISSING="${TTS_MISSING} - ${asset}\n"
59
59
  fi
60
60
  done <<< "$TTS_ASSETS"
@@ -552,6 +552,62 @@ export interface Spec extends TurboModule {
552
552
  */
553
553
  cancelExtractTarBz2(): Promise<void>;
554
554
 
555
+ /**
556
+ * Extract a .tar.zst (or .zst) archive to a target folder.
557
+ * Returns { success, path } or { success, reason }.
558
+ */
559
+ extractTarZst(
560
+ sourcePath: string,
561
+ targetPath: string,
562
+ force: boolean
563
+ ): Promise<{
564
+ success: boolean;
565
+ path?: string;
566
+ sha256?: string;
567
+ reason?: string;
568
+ }>;
569
+
570
+ /**
571
+ * Cancel any in-progress tar.zst extraction.
572
+ */
573
+ cancelExtractTarZst(): Promise<void>;
574
+
575
+ /**
576
+ * List asset paths of .tar.zst and .tar.bz2 archives in a PAD pack when stored as APK_ASSETS.
577
+ * Android only; returns [] when pack is not available or not APK_ASSETS. Used by getBundledArchives.
578
+ */
579
+ listBundledArchiveAssetPaths(packName: string): Promise<string[]>;
580
+
581
+ /**
582
+ * Extract a .tar.zst archive from Android assets (AssetManager) to a target folder. Android only.
583
+ * Streams from asset; no copy of the archive to disk. Used when PAD pack is APK_ASSETS.
584
+ */
585
+ extractTarZstFromAsset(
586
+ assetPath: string,
587
+ targetPath: string,
588
+ force: boolean
589
+ ): Promise<{
590
+ success: boolean;
591
+ path?: string;
592
+ sha256?: string;
593
+ reason?: string;
594
+ }>;
595
+
596
+ /**
597
+ * Extract a .tar.bz2 archive from Android assets (AssetManager) to a target folder. Android only.
598
+ * Streams from asset; no copy of the archive to disk. Used when PAD pack is APK_ASSETS.
599
+ */
600
+ extractTarBz2FromAsset(
601
+ assetPath: string,
602
+ targetPath: string,
603
+ force: boolean
604
+ ): Promise<{
605
+ success: boolean;
606
+ path?: string;
607
+ sha256?: string;
608
+ reason?: string;
609
+ }>;
610
+
555
611
  /**
556
612
  * Compute SHA-256 of a file and return the hex digest.
557
613
  */
@@ -0,0 +1,77 @@
1
+ import { DeviceEventEmitter } from 'react-native';
2
+ import SherpaOnnx from '../NativeSherpaOnnx';
3
+
4
+ export type ExtractProgressEvent = {
5
+ bytes: number;
6
+ totalBytes: number;
7
+ percent: number;
8
+ };
9
+
10
+ type ExtractResult = {
11
+ success: boolean;
12
+ path?: string;
13
+ sha256?: string;
14
+ reason?: string;
15
+ };
16
+
17
+ export async function extractTarZst(
18
+ sourcePath: string,
19
+ targetPath: string,
20
+ force = true,
21
+ onProgress?: (event: ExtractProgressEvent) => void,
22
+ signal?: AbortSignal
23
+ ): Promise<ExtractResult> {
24
+ let subscription: { remove: () => void } | null = null;
25
+ let removeAbortListener: (() => void) | null = null;
26
+
27
+ if (signal?.aborted) {
28
+ const abortError = new Error('Extraction aborted');
29
+ abortError.name = 'AbortError';
30
+ throw abortError;
31
+ }
32
+
33
+ if (onProgress) {
34
+ subscription = DeviceEventEmitter.addListener(
35
+ 'extractTarZstProgress',
36
+ (event: ExtractProgressEvent & { sourcePath?: string }) => {
37
+ if (event.sourcePath != null && event.sourcePath !== sourcePath) {
38
+ return;
39
+ }
40
+ const safePercent = Math.max(0, Math.min(100, event.percent));
41
+ onProgress({ ...event, percent: safePercent });
42
+ }
43
+ );
44
+ }
45
+
46
+ if (signal) {
47
+ const onAbort = () => {
48
+ try {
49
+ SherpaOnnx.cancelExtractTarZst();
50
+ } catch {
51
+ // Ignore cancel errors to avoid crashing on abort.
52
+ }
53
+ };
54
+ signal.addEventListener('abort', onAbort);
55
+ removeAbortListener = () => signal.removeEventListener('abort', onAbort);
56
+ }
57
+
58
+ try {
59
+ const result = await SherpaOnnx.extractTarZst(
60
+ sourcePath,
61
+ targetPath,
62
+ force
63
+ );
64
+ if (!result.success) {
65
+ const message = result.reason || 'Extraction failed';
66
+ const error = new Error(message);
67
+ if (signal?.aborted || /cancel/i.test(message)) {
68
+ error.name = 'AbortError';
69
+ }
70
+ throw error;
71
+ }
72
+ return result;
73
+ } finally {
74
+ subscription?.remove();
75
+ removeAbortListener?.();
76
+ }
77
+ }
@@ -1,5 +1,7 @@
1
1
  export { extractTarBz2 } from './extractTarBz2';
2
2
  export type { ExtractProgressEvent } from './extractTarBz2';
3
+ export { extractTarZst } from './extractTarZst';
4
+ export type { ExtractProgressEvent as ExtractZstProgressEvent } from './extractTarZst';
3
5
  export {
4
6
  listModelsByCategory,
5
7
  refreshModelsByCategory,
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Extraction subpath: list and extract compressed model archives (.tar.zst / .tar.bz2).
3
+ *
4
+ * Three entry points:
5
+ * - getBundledArchives(packName) – Android PAD packs (STORAGE_FILES or APK_ASSETS)
6
+ * - listBundledArchives(dirPath) – any filesystem directory (cross-platform)
7
+ * - extractArchive(archive, target) – unified extraction (auto-selects path or asset-stream)
8
+ *
9
+ * After extraction, use listModelsAtPath / autoModelPath from the main package.
10
+ */
11
+
12
+ import { DeviceEventEmitter, Platform } from 'react-native';
13
+ import { readDir, stat, exists } from '@dr.pogodin/react-native-fs';
14
+ import SherpaOnnx from '../NativeSherpaOnnx';
15
+ import { extractTarZst } from '../download/extractTarZst';
16
+ import { extractTarBz2 } from '../download/extractTarBz2';
17
+ import type {
18
+ BundledArchive,
19
+ ExtractArchiveOptions,
20
+ ExtractResult,
21
+ ExtractProgressEvent,
22
+ } from './types';
23
+
24
+ export type {
25
+ BundledArchive,
26
+ ExtractArchiveOptions,
27
+ ExtractResult,
28
+ ExtractProgressEvent,
29
+ } from './types';
30
+
31
+ // ── Constants & helpers ───────────────────────────────────────────
32
+
33
+ const TAR_ZST = '.tar.zst';
34
+ const TAR_BZ2 = '.tar.bz2';
35
+
36
+ function formatFromFilename(name: string): 'tar.zst' | 'tar.bz2' | null {
37
+ if (name.endsWith(TAR_ZST)) return 'tar.zst';
38
+ if (name.endsWith(TAR_BZ2)) return 'tar.bz2';
39
+ return null;
40
+ }
41
+
42
+ function modelIdFromFilename(filename: string): string {
43
+ if (filename.endsWith(TAR_ZST)) return filename.slice(0, -TAR_ZST.length);
44
+ if (filename.endsWith(TAR_BZ2)) return filename.slice(0, -TAR_BZ2.length);
45
+ return filename;
46
+ }
47
+
48
+ /**
49
+ * Scan a filesystem directory for .tar.zst / .tar.bz2 entries.
50
+ * Shared by getBundledArchives (STORAGE_FILES) and listBundledArchives.
51
+ */
52
+ async function scanDirectoryForArchives(
53
+ directoryPath: string
54
+ ): Promise<BundledArchive[]> {
55
+ const dirExists = await exists(directoryPath);
56
+ if (!dirExists) return [];
57
+
58
+ const entries = await readDir(directoryPath);
59
+ const archives: BundledArchive[] = [];
60
+
61
+ for (const entry of entries) {
62
+ const format = formatFromFilename(entry.name);
63
+ if (!format || !entry.isFile()) continue;
64
+
65
+ let fileSize = 0;
66
+ try {
67
+ const s = await stat(entry.path);
68
+ fileSize = s.size ?? 0;
69
+ } catch {
70
+ // stat may fail on some filesystems; fileSize stays 0
71
+ }
72
+
73
+ archives.push({
74
+ modelId: modelIdFromFilename(entry.name),
75
+ archivePath: entry.path,
76
+ format,
77
+ fileSize,
78
+ });
79
+ }
80
+
81
+ return archives;
82
+ }
83
+
84
+ // ── Public API ────────────────────────────────────────────────────
85
+
86
+ /**
87
+ * List compressed archives delivered via a **Play Asset Delivery** pack.
88
+ *
89
+ * - **STORAGE_FILES** packs: scans the pack directory on the filesystem.
90
+ * - **APK_ASSETS** packs: queries the Android AssetManager for embedded archive paths.
91
+ * Archives returned with `fromAsset: true` are extracted by streaming from the APK
92
+ * (no temp copy needed).
93
+ * - **iOS / unavailable pack**: returns `null`.
94
+ *
95
+ * @param packName Name of the PAD asset pack (e.g. `"sherpa_models"`)
96
+ */
97
+ export async function getBundledArchives(
98
+ packName: string
99
+ ): Promise<BundledArchive[] | null> {
100
+ if (Platform.OS !== 'android') {
101
+ return null;
102
+ }
103
+
104
+ const packPath = await SherpaOnnx.getAssetPackPath(packName);
105
+
106
+ if (packPath != null && packPath.length > 0) {
107
+ const archives = await scanDirectoryForArchives(packPath);
108
+ return archives.length > 0 ? archives : null;
109
+ }
110
+
111
+ const assetPaths = await SherpaOnnx.listBundledArchiveAssetPaths(packName);
112
+ if (assetPaths.length === 0) return null;
113
+
114
+ return assetPaths.map((archivePath) => {
115
+ const filename = archivePath.split('/').pop() ?? archivePath;
116
+ const format = formatFromFilename(filename) ?? 'tar.zst';
117
+ return {
118
+ modelId: modelIdFromFilename(filename),
119
+ archivePath,
120
+ format,
121
+ fromAsset: true,
122
+ };
123
+ });
124
+ }
125
+
126
+ /**
127
+ * List `.tar.zst` and `.tar.bz2` archives in a filesystem directory.
128
+ *
129
+ * Works on **all platforms** — use for:
130
+ * - iOS main-bundle archives (`MainBundlePath + '/models'`)
131
+ * - Archives downloaded to the documents directory
132
+ * - Any other folder containing compressed model archives
133
+ *
134
+ * @param directoryPath Absolute path to the directory to scan
135
+ */
136
+ export async function listBundledArchives(
137
+ directoryPath: string
138
+ ): Promise<BundledArchive[]> {
139
+ return scanDirectoryForArchives(directoryPath);
140
+ }
141
+
142
+ /**
143
+ * Extract a single archive to the target directory.
144
+ *
145
+ * Handles both source types transparently:
146
+ * - **Filesystem archives** (from `listBundledArchives` or PAD STORAGE_FILES) —
147
+ * uses the regular path-based extraction.
148
+ * - **APK asset archives** (`fromAsset: true`, from PAD APK_ASSETS) —
149
+ * streams directly from the APK without copying the archive to disk first.
150
+ *
151
+ * @param archive Descriptor returned by `getBundledArchives` or `listBundledArchives`
152
+ * @param targetPath Directory to extract into (e.g. `DocumentDirectoryPath + '/models'`)
153
+ * @param options `force` (default `true`), `onProgress`, `signal` (AbortSignal)
154
+ */
155
+ export async function extractArchive(
156
+ archive: BundledArchive,
157
+ targetPath: string,
158
+ options?: ExtractArchiveOptions
159
+ ): Promise<ExtractResult> {
160
+ const force = options?.force !== false;
161
+ const onProgress = options?.onProgress;
162
+ const signal = options?.signal;
163
+
164
+ if (signal?.aborted) {
165
+ const err = new Error('Extraction aborted');
166
+ err.name = 'AbortError';
167
+ throw err;
168
+ }
169
+
170
+ const useAssetStream =
171
+ Platform.OS === 'android' &&
172
+ (archive.fromAsset === true ||
173
+ archive.archivePath.startsWith('asset_packs/'));
174
+
175
+ if (useAssetStream) {
176
+ return extractFromAsset(archive, targetPath, force, onProgress, signal);
177
+ }
178
+
179
+ if (archive.format === 'tar.zst') {
180
+ return extractTarZst(
181
+ archive.archivePath,
182
+ targetPath,
183
+ force,
184
+ onProgress,
185
+ signal
186
+ );
187
+ }
188
+ return extractTarBz2(
189
+ archive.archivePath,
190
+ targetPath,
191
+ force,
192
+ onProgress,
193
+ signal
194
+ );
195
+ }
196
+
197
+ // ── Internal: asset-stream extraction (Android APK_ASSETS) ───────
198
+
199
+ async function extractFromAsset(
200
+ archive: BundledArchive,
201
+ targetPath: string,
202
+ force: boolean,
203
+ onProgress?: (event: ExtractProgressEvent) => void,
204
+ signal?: AbortSignal
205
+ ): Promise<ExtractResult> {
206
+ const eventName =
207
+ archive.format === 'tar.zst'
208
+ ? 'extractTarZstProgress'
209
+ : 'extractTarBz2Progress';
210
+
211
+ let subscription: { remove: () => void } | null = null;
212
+ let removeAbortListener: (() => void) | null = null;
213
+
214
+ if (onProgress) {
215
+ subscription = DeviceEventEmitter.addListener(
216
+ eventName,
217
+ (event: ExtractProgressEvent & { sourcePath?: string }) => {
218
+ if (
219
+ event.sourcePath != null &&
220
+ event.sourcePath !== archive.archivePath
221
+ ) {
222
+ return;
223
+ }
224
+ const safePercent = Math.max(0, Math.min(100, event.percent));
225
+ onProgress({ ...event, percent: safePercent });
226
+ }
227
+ );
228
+ }
229
+
230
+ if (signal) {
231
+ const cancel =
232
+ archive.format === 'tar.zst'
233
+ ? SherpaOnnx.cancelExtractTarZst
234
+ : SherpaOnnx.cancelExtractTarBz2;
235
+ const onAbort = () => {
236
+ try {
237
+ cancel();
238
+ } catch {
239
+ // ignore
240
+ }
241
+ };
242
+ signal.addEventListener('abort', onAbort);
243
+ removeAbortListener = () => signal.removeEventListener('abort', onAbort);
244
+ }
245
+
246
+ try {
247
+ const result =
248
+ archive.format === 'tar.zst'
249
+ ? await SherpaOnnx.extractTarZstFromAsset(
250
+ archive.archivePath,
251
+ targetPath,
252
+ force
253
+ )
254
+ : await SherpaOnnx.extractTarBz2FromAsset(
255
+ archive.archivePath,
256
+ targetPath,
257
+ force
258
+ );
259
+
260
+ if (!result.success) {
261
+ const message = result.reason ?? 'Extraction failed';
262
+ const error = new Error(message);
263
+ if (signal?.aborted || /cancel/i.test(message)) {
264
+ error.name = 'AbortError';
265
+ }
266
+ throw error;
267
+ }
268
+ return result;
269
+ } finally {
270
+ subscription?.remove();
271
+ removeAbortListener?.();
272
+ }
273
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Types for the extraction subpath.
3
+ *
4
+ * A BundledArchive describes a compressed model archive (.tar.zst or .tar.bz2)
5
+ * that can come from two distinct sources:
6
+ *
7
+ * 1. **Filesystem** — a regular file on disk (PAD STORAGE_FILES, iOS bundle,
8
+ * downloaded archive, etc.). `fromAsset` is absent or `false`.
9
+ * 2. **Android APK asset** — embedded in the APK via PAD APK_ASSETS.
10
+ * `fromAsset` is `true`; extraction streams directly from the APK.
11
+ *
12
+ * The consumer does not need to distinguish between the two:
13
+ * `extractArchive()` handles both transparently.
14
+ */
15
+
16
+ /** Describes one compressed model archive. */
17
+ export type BundledArchive = {
18
+ /** Identifier derived from the archive filename (filename minus the extension). */
19
+ modelId: string;
20
+ /**
21
+ * Path to the archive.
22
+ * - Filesystem archives: absolute path (e.g. `/data/.../models/whisper-tiny.tar.zst`).
23
+ * - APK assets: asset path (e.g. `asset_packs/sherpa_models/assets/whisper-tiny.tar.zst`).
24
+ */
25
+ archivePath: string;
26
+ /** Compression format. */
27
+ format: 'tar.zst' | 'tar.bz2';
28
+ /** File size in bytes (available for filesystem archives; 0 or absent for APK assets). */
29
+ fileSize?: number;
30
+ /** `true` when the archive lives inside the APK (APK_ASSETS). Absent for filesystem archives. */
31
+ fromAsset?: boolean;
32
+ };
33
+
34
+ /** Progress event emitted during extraction. */
35
+ export type ExtractProgressEvent = {
36
+ /** Bytes extracted so far. */
37
+ bytes: number;
38
+ /** Total bytes of the archive (may be 0 when unknown). */
39
+ totalBytes: number;
40
+ /** Progress percentage 0–100. */
41
+ percent: number;
42
+ };
43
+
44
+ /** Result returned by `extractArchive`. */
45
+ export type ExtractResult = {
46
+ success: boolean;
47
+ /** Absolute path to the extracted directory (on success). */
48
+ path?: string;
49
+ /** SHA-256 hex digest of the source archive (when available). */
50
+ sha256?: string;
51
+ /** Error description (on failure). */
52
+ reason?: string;
53
+ };
54
+
55
+ /** Options for `extractArchive`. */
56
+ export type ExtractArchiveOptions = {
57
+ /** Overwrite existing files. Defaults to `true`. */
58
+ force?: boolean;
59
+ /** Callback for extraction progress. */
60
+ onProgress?: (event: ExtractProgressEvent) => void;
61
+ /** AbortSignal to cancel the extraction. */
62
+ signal?: AbortSignal;
63
+ };
package/src/index.tsx CHANGED
@@ -22,6 +22,7 @@ export { copyFileToContentUri } from './tts';
22
22
  // - import { createSTT, createStreamingSTT, ... } from 'react-native-sherpa-onnx/stt'
23
23
  // - import { createTTS, ... } from 'react-native-sherpa-onnx/tts'
24
24
  // - import { ... } from 'react-native-sherpa-onnx/download'
25
+ // - import { getBundledArchives, listBundledArchives, extractArchive } from 'react-native-sherpa-onnx/extraction'
25
26
  // - import { ... } from 'react-native-sherpa-onnx/vad' (planned)
26
27
  // - import { ... } from 'react-native-sherpa-onnx/diarization' (planned)
27
28
  // - import { ... } from 'react-native-sherpa-onnx/enhancement' (planned)
@@ -1 +1 @@
1
- libarchive-android-v3.8.5-1
1
+ libarchive-android-v3.8.5-2
@@ -1 +1 @@
1
- libarchive-ios-v3.8.5-1
1
+ libarchive-ios-v3.8.5-2