@talmolab/sleap-io.js 0.1.7 → 0.1.8
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/dist/index.d.ts +89 -19
- package/dist/index.js +347 -57
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -18,13 +18,16 @@ declare class Video {
|
|
|
18
18
|
backendMetadata: Record<string, unknown>;
|
|
19
19
|
sourceVideo: Video | null;
|
|
20
20
|
openBackend: boolean;
|
|
21
|
+
private _embedded;
|
|
21
22
|
constructor(options: {
|
|
22
23
|
filename: string | string[];
|
|
23
24
|
backend?: VideoBackend | null;
|
|
24
25
|
backendMetadata?: Record<string, unknown>;
|
|
25
26
|
sourceVideo?: Video | null;
|
|
26
27
|
openBackend?: boolean;
|
|
28
|
+
embedded?: boolean;
|
|
27
29
|
});
|
|
30
|
+
get hasEmbeddedImages(): boolean;
|
|
28
31
|
get originalVideo(): Video | null;
|
|
29
32
|
get shape(): [number, number, number, number] | null;
|
|
30
33
|
get fps(): number | null;
|
|
@@ -259,6 +262,10 @@ interface StreamingH5Options {
|
|
|
259
262
|
/** Filename hint for the HDF5 file. */
|
|
260
263
|
filenameHint?: string;
|
|
261
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Source types supported by the streaming HDF5 file.
|
|
267
|
+
*/
|
|
268
|
+
type StreamingH5Source = string | ArrayBuffer | Uint8Array | File;
|
|
262
269
|
/**
|
|
263
270
|
* A streaming HDF5 file handle that uses a Web Worker for range request access.
|
|
264
271
|
*
|
|
@@ -280,12 +287,33 @@ declare class StreamingH5File {
|
|
|
280
287
|
*/
|
|
281
288
|
init(options?: StreamingH5Options): Promise<void>;
|
|
282
289
|
/**
|
|
283
|
-
* Open a remote HDF5 file for streaming access.
|
|
290
|
+
* Open a remote HDF5 file for streaming access via URL.
|
|
284
291
|
*
|
|
285
292
|
* @param url - URL to the HDF5 file (must support HTTP range requests)
|
|
286
293
|
* @param options - Optional settings
|
|
287
294
|
*/
|
|
288
295
|
open(url: string, options?: StreamingH5Options): Promise<void>;
|
|
296
|
+
/**
|
|
297
|
+
* Open a local File object using WORKERFS (zero-copy).
|
|
298
|
+
*
|
|
299
|
+
* @param file - File object from file input or drag-and-drop
|
|
300
|
+
* @param options - Optional settings
|
|
301
|
+
*/
|
|
302
|
+
openLocal(file: File, options?: StreamingH5Options): Promise<void>;
|
|
303
|
+
/**
|
|
304
|
+
* Open an HDF5 file from an ArrayBuffer or Uint8Array.
|
|
305
|
+
*
|
|
306
|
+
* @param buffer - ArrayBuffer or Uint8Array containing the HDF5 file data
|
|
307
|
+
* @param options - Optional settings
|
|
308
|
+
*/
|
|
309
|
+
openBuffer(buffer: ArrayBuffer | Uint8Array, options?: StreamingH5Options): Promise<void>;
|
|
310
|
+
/**
|
|
311
|
+
* Open an HDF5 file from any supported source.
|
|
312
|
+
*
|
|
313
|
+
* @param source - URL string, File, ArrayBuffer, or Uint8Array
|
|
314
|
+
* @param options - Optional settings
|
|
315
|
+
*/
|
|
316
|
+
openAny(source: StreamingH5Source, options?: StreamingH5Options): Promise<void>;
|
|
289
317
|
/**
|
|
290
318
|
* Whether a file is currently open.
|
|
291
319
|
*/
|
|
@@ -341,6 +369,29 @@ declare function isStreamingSupported(): boolean;
|
|
|
341
369
|
* @returns A StreamingH5File instance
|
|
342
370
|
*/
|
|
343
371
|
declare function openStreamingH5(url: string, options?: StreamingH5Options): Promise<StreamingH5File>;
|
|
372
|
+
/**
|
|
373
|
+
* Open an HDF5 file from any supported source using a Web Worker.
|
|
374
|
+
*
|
|
375
|
+
* This is the recommended way to open HDF5 files in the browser as it
|
|
376
|
+
* offloads all h5wasm operations to a Web Worker, avoiding main thread blocking.
|
|
377
|
+
*
|
|
378
|
+
* @param source - URL string, File object, ArrayBuffer, or Uint8Array
|
|
379
|
+
* @param options - Optional settings
|
|
380
|
+
* @returns A StreamingH5File instance
|
|
381
|
+
*
|
|
382
|
+
* @example
|
|
383
|
+
* ```typescript
|
|
384
|
+
* // From URL
|
|
385
|
+
* const file = await openH5Worker("https://example.com/data.h5");
|
|
386
|
+
*
|
|
387
|
+
* // From File (file input)
|
|
388
|
+
* const file = await openH5Worker(inputElement.files[0]);
|
|
389
|
+
*
|
|
390
|
+
* // From ArrayBuffer
|
|
391
|
+
* const file = await openH5Worker(arrayBuffer);
|
|
392
|
+
* ```
|
|
393
|
+
*/
|
|
394
|
+
declare function openH5Worker(source: StreamingH5Source, options?: StreamingH5Options): Promise<StreamingH5File>;
|
|
344
395
|
|
|
345
396
|
/**
|
|
346
397
|
* Video backend for embedded images in HDF5 files accessed via streaming.
|
|
@@ -360,7 +411,7 @@ declare class StreamingHdf5VideoBackend implements VideoBackend {
|
|
|
360
411
|
fps?: number;
|
|
361
412
|
private h5file;
|
|
362
413
|
private datasetPath;
|
|
363
|
-
private
|
|
414
|
+
private frameNumberToIndex;
|
|
364
415
|
private format;
|
|
365
416
|
private channelOrder;
|
|
366
417
|
private cachedData;
|
|
@@ -396,27 +447,36 @@ type OpenH5Options = {
|
|
|
396
447
|
/**
|
|
397
448
|
* Load an SLP file.
|
|
398
449
|
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
450
|
+
* In browser environments, this function automatically uses a Web Worker for all
|
|
451
|
+
* HDF5 operations, keeping the main thread responsive. For URLs, it uses HTTP
|
|
452
|
+
* range requests to download only the data needed rather than the entire file.
|
|
453
|
+
*
|
|
454
|
+
* In Node.js, this uses the native h5wasm bindings directly.
|
|
402
455
|
*
|
|
403
456
|
* @param source - Path, URL, ArrayBuffer, File, or FileSystemFileHandle
|
|
404
457
|
* @param options - Loading options
|
|
405
|
-
* @param options.openVideos - Whether to open video backends (default: true
|
|
458
|
+
* @param options.openVideos - Whether to open video backends (default: true)
|
|
406
459
|
* @param options.h5 - HDF5 options including streaming mode
|
|
407
460
|
* @param options.h5.stream - 'auto' | 'range' | 'download' (default: 'auto')
|
|
408
461
|
*
|
|
409
462
|
* @example
|
|
410
463
|
* ```typescript
|
|
411
|
-
* // Load from URL
|
|
412
|
-
* const labels = await loadSlp('https://example.com/labels.slp'
|
|
413
|
-
*
|
|
414
|
-
*
|
|
464
|
+
* // Browser: Load from URL (automatically uses Worker + range requests)
|
|
465
|
+
* const labels = await loadSlp('https://example.com/labels.slp');
|
|
466
|
+
*
|
|
467
|
+
* // Browser: Load from file input (automatically uses Worker)
|
|
468
|
+
* const labels = await loadSlp(fileInput.files[0]);
|
|
469
|
+
*
|
|
470
|
+
* // Browser: Load from ArrayBuffer (automatically uses Worker)
|
|
471
|
+
* const labels = await loadSlp(arrayBuffer);
|
|
415
472
|
*
|
|
416
|
-
* // Force full download
|
|
473
|
+
* // Force full download instead of range requests
|
|
417
474
|
* const labels = await loadSlp('https://example.com/labels.slp', {
|
|
418
475
|
* h5: { stream: 'download' }
|
|
419
476
|
* });
|
|
477
|
+
*
|
|
478
|
+
* // Node.js: Load from file path
|
|
479
|
+
* const labels = await loadSlp('/path/to/file.slp');
|
|
420
480
|
* ```
|
|
421
481
|
*/
|
|
422
482
|
declare function loadSlp(source: SlpSource, options?: {
|
|
@@ -743,28 +803,38 @@ interface StreamingSlpOptions {
|
|
|
743
803
|
openVideos?: boolean;
|
|
744
804
|
}
|
|
745
805
|
/**
|
|
746
|
-
* Read an SLP file using
|
|
806
|
+
* Read an SLP file using a Web Worker for efficient, non-blocking HDF5 access.
|
|
747
807
|
*
|
|
748
|
-
* This function
|
|
749
|
-
*
|
|
808
|
+
* This function offloads all h5wasm operations to a Web Worker, keeping the
|
|
809
|
+
* main thread responsive. For URLs, it uses HTTP range requests to download
|
|
810
|
+
* only the data needed rather than the entire file.
|
|
750
811
|
*
|
|
751
812
|
* When `openVideos` is true, video backends are created for embedded videos,
|
|
752
813
|
* allowing frame data to be retrieved. The underlying HDF5 file remains open
|
|
753
814
|
* until all video backends are closed.
|
|
754
815
|
*
|
|
755
|
-
* @param
|
|
816
|
+
* @param source - URL, File, ArrayBuffer, or Uint8Array containing the SLP file
|
|
756
817
|
* @param options - Optional settings
|
|
757
818
|
* @returns Labels object with all annotation data
|
|
758
819
|
*
|
|
759
820
|
* @example
|
|
760
821
|
* ```typescript
|
|
761
|
-
* // Load with video backends
|
|
822
|
+
* // Load from URL with video backends
|
|
762
823
|
* const labels = await readSlpStreaming('https://example.com/labels.slp', {
|
|
763
824
|
* openVideos: true
|
|
764
825
|
* });
|
|
765
|
-
*
|
|
826
|
+
*
|
|
827
|
+
* // Load from File object (file input)
|
|
828
|
+
* const labels = await readSlpStreaming(fileInput.files[0], {
|
|
829
|
+
* openVideos: true
|
|
830
|
+
* });
|
|
831
|
+
*
|
|
832
|
+
* // Load from ArrayBuffer
|
|
833
|
+
* const labels = await readSlpStreaming(arrayBuffer, {
|
|
834
|
+
* filenameHint: 'data.slp'
|
|
835
|
+
* });
|
|
766
836
|
* ```
|
|
767
837
|
*/
|
|
768
|
-
declare function readSlpStreaming(
|
|
838
|
+
declare function readSlpStreaming(source: StreamingH5Source, options?: StreamingSlpOptions): Promise<Labels>;
|
|
769
839
|
|
|
770
|
-
export { Camera, CameraGroup, type ColorScheme, type ColorSpec, FrameGroup, Instance, InstanceContext, InstanceGroup, LabeledFrame, Labels, type LabelsDict, LabelsSet, MARKER_FUNCTIONS, type MarkerShape, Mp4BoxVideoBackend, NAMED_COLORS, PALETTES, type PaletteName, PredictedInstance, type RGB, type RGBA, RecordingSession, RenderContext, type RenderOptions, Skeleton, StreamingH5File, StreamingHdf5VideoBackend, SuggestionFrame, Track, Video, type VideoBackend, type VideoFrame, type VideoOptions, checkFfmpeg, decodeYamlSkeleton, determineColorScheme, drawCircle, drawCross, drawDiamond, drawSquare, drawTriangle, encodeYamlSkeleton, fromDict, fromNumpy, getMarkerFunction, getPalette, isStreamingSupported, labelsFromNumpy, loadSlp, loadVideo, makeCameraFromDict, openStreamingH5, readSlpStreaming, renderImage, renderVideo, resolveColor, rgbToCSS, rodriguesTransformation, saveImage, saveSlp, toDataURL, toDict, toJPEG, toNumpy, toPNG };
|
|
840
|
+
export { Camera, CameraGroup, type ColorScheme, type ColorSpec, FrameGroup, Instance, InstanceContext, InstanceGroup, LabeledFrame, Labels, type LabelsDict, LabelsSet, MARKER_FUNCTIONS, type MarkerShape, Mp4BoxVideoBackend, NAMED_COLORS, PALETTES, type PaletteName, PredictedInstance, type RGB, type RGBA, RecordingSession, RenderContext, type RenderOptions, Skeleton, StreamingH5File, type StreamingH5Source, StreamingHdf5VideoBackend, SuggestionFrame, Track, Video, type VideoBackend, type VideoFrame, type VideoOptions, checkFfmpeg, decodeYamlSkeleton, determineColorScheme, drawCircle, drawCross, drawDiamond, drawSquare, drawTriangle, encodeYamlSkeleton, fromDict, fromNumpy, getMarkerFunction, getPalette, isStreamingSupported, labelsFromNumpy, loadSlp, loadVideo, makeCameraFromDict, openH5Worker, openStreamingH5, readSlpStreaming, renderImage, renderVideo, resolveColor, rgbToCSS, rodriguesTransformation, saveImage, saveSlp, toDataURL, toDict, toJPEG, toNumpy, toPNG };
|
package/dist/index.js
CHANGED
|
@@ -85,12 +85,17 @@ var Video = class {
|
|
|
85
85
|
backendMetadata;
|
|
86
86
|
sourceVideo;
|
|
87
87
|
openBackend;
|
|
88
|
+
_embedded;
|
|
88
89
|
constructor(options) {
|
|
89
90
|
this.filename = options.filename;
|
|
90
91
|
this.backend = options.backend ?? null;
|
|
91
92
|
this.backendMetadata = options.backendMetadata ?? {};
|
|
92
93
|
this.sourceVideo = options.sourceVideo ?? null;
|
|
93
94
|
this.openBackend = options.openBackend ?? true;
|
|
95
|
+
this._embedded = options.embedded ?? false;
|
|
96
|
+
}
|
|
97
|
+
get hasEmbeddedImages() {
|
|
98
|
+
return this._embedded;
|
|
94
99
|
}
|
|
95
100
|
get originalVideo() {
|
|
96
101
|
if (!this.sourceVideo) return null;
|
|
@@ -1217,7 +1222,7 @@ var StreamingHdf5VideoBackend = class {
|
|
|
1217
1222
|
fps;
|
|
1218
1223
|
h5file;
|
|
1219
1224
|
datasetPath;
|
|
1220
|
-
|
|
1225
|
+
frameNumberToIndex;
|
|
1221
1226
|
format;
|
|
1222
1227
|
channelOrder;
|
|
1223
1228
|
cachedData;
|
|
@@ -1228,7 +1233,8 @@ var StreamingHdf5VideoBackend = class {
|
|
|
1228
1233
|
this.h5file = options.h5file;
|
|
1229
1234
|
this.datasetPath = options.datasetPath;
|
|
1230
1235
|
this.dataset = options.datasetPath;
|
|
1231
|
-
|
|
1236
|
+
const frameNumbers = options.frameNumbers ?? [];
|
|
1237
|
+
this.frameNumberToIndex = new Map(frameNumbers.map((num, idx) => [num, idx]));
|
|
1232
1238
|
this.format = options.format ?? "png";
|
|
1233
1239
|
this.channelOrder = options.channelOrder ?? "RGB";
|
|
1234
1240
|
this.shape = options.shape;
|
|
@@ -1237,8 +1243,8 @@ var StreamingHdf5VideoBackend = class {
|
|
|
1237
1243
|
this.frameOffsets = null;
|
|
1238
1244
|
}
|
|
1239
1245
|
async getFrame(frameIndex) {
|
|
1240
|
-
const index = this.
|
|
1241
|
-
if (index
|
|
1246
|
+
const index = this.frameNumberToIndex.size > 0 ? this.frameNumberToIndex.get(frameIndex) : frameIndex;
|
|
1247
|
+
if (index === void 0) return null;
|
|
1242
1248
|
if (!this.cachedData) {
|
|
1243
1249
|
try {
|
|
1244
1250
|
const data = await this.h5file.getDatasetValue(this.datasetPath);
|
|
@@ -1267,7 +1273,7 @@ var StreamingHdf5VideoBackend = class {
|
|
|
1267
1273
|
}
|
|
1268
1274
|
if (!rawBytes || rawBytes.length === 0) return null;
|
|
1269
1275
|
if (isEncodedFormat(this.format)) {
|
|
1270
|
-
const decoded = await decodeImageBytes(rawBytes, this.format);
|
|
1276
|
+
const decoded = await decodeImageBytes(rawBytes, this.format, this.channelOrder);
|
|
1271
1277
|
return decoded ?? rawBytes;
|
|
1272
1278
|
}
|
|
1273
1279
|
const image = decodeRawFrame(rawBytes, this.shape, this.channelOrder);
|
|
@@ -1338,12 +1344,29 @@ function isEncodedFormat(format) {
|
|
|
1338
1344
|
const normalized = format.toLowerCase();
|
|
1339
1345
|
return normalized === "png" || normalized === "jpg" || normalized === "jpeg";
|
|
1340
1346
|
}
|
|
1341
|
-
async function decodeImageBytes(bytes, format) {
|
|
1347
|
+
async function decodeImageBytes(bytes, format, channelOrder) {
|
|
1342
1348
|
if (!isBrowser3 || typeof createImageBitmap === "undefined") return null;
|
|
1343
1349
|
const mime = format.toLowerCase() === "png" ? "image/png" : "image/jpeg";
|
|
1344
1350
|
const safeBytes = new Uint8Array(bytes);
|
|
1345
1351
|
const blob = new Blob([safeBytes.buffer], { type: mime });
|
|
1346
|
-
|
|
1352
|
+
const bitmap = await createImageBitmap(blob);
|
|
1353
|
+
const useBgr = channelOrder.toUpperCase() === "BGR";
|
|
1354
|
+
if (!useBgr) {
|
|
1355
|
+
return bitmap;
|
|
1356
|
+
}
|
|
1357
|
+
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
1358
|
+
const ctx = canvas.getContext("2d");
|
|
1359
|
+
if (!ctx) return bitmap;
|
|
1360
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
1361
|
+
const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
1362
|
+
const data = imageData.data;
|
|
1363
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
1364
|
+
const r = data[i];
|
|
1365
|
+
const b = data[i + 2];
|
|
1366
|
+
data[i] = b;
|
|
1367
|
+
data[i + 2] = r;
|
|
1368
|
+
}
|
|
1369
|
+
return imageData;
|
|
1347
1370
|
}
|
|
1348
1371
|
function decodeRawFrame(bytes, shape, channelOrder) {
|
|
1349
1372
|
if (!isBrowser3 || !shape) return null;
|
|
@@ -1371,7 +1394,8 @@ function decodeRawFrame(bytes, shape, channelOrder) {
|
|
|
1371
1394
|
// src/codecs/slp/h5-worker.ts
|
|
1372
1395
|
var H5_WORKER_CODE = `
|
|
1373
1396
|
// h5wasm streaming worker
|
|
1374
|
-
//
|
|
1397
|
+
// Handles all HDF5 operations in a Web Worker to avoid main thread blocking
|
|
1398
|
+
// Supports: URL streaming (range requests), local files (WORKERFS), and ArrayBuffers
|
|
1375
1399
|
|
|
1376
1400
|
let h5wasmModule = null;
|
|
1377
1401
|
let FS = null;
|
|
@@ -1389,8 +1413,18 @@ self.onmessage = async function(e) {
|
|
|
1389
1413
|
break;
|
|
1390
1414
|
|
|
1391
1415
|
case 'openUrl':
|
|
1392
|
-
const
|
|
1393
|
-
respond(id,
|
|
1416
|
+
const urlResult = await openRemoteFile(payload.url, payload.filename);
|
|
1417
|
+
respond(id, urlResult);
|
|
1418
|
+
break;
|
|
1419
|
+
|
|
1420
|
+
case 'openLocal':
|
|
1421
|
+
const localResult = await openLocalFile(payload.file, payload.filename);
|
|
1422
|
+
respond(id, localResult);
|
|
1423
|
+
break;
|
|
1424
|
+
|
|
1425
|
+
case 'openBuffer':
|
|
1426
|
+
const bufferResult = await openBufferFile(payload.buffer, payload.filename);
|
|
1427
|
+
respond(id, bufferResult);
|
|
1394
1428
|
break;
|
|
1395
1429
|
|
|
1396
1430
|
case 'getKeys':
|
|
@@ -1481,6 +1515,66 @@ async function openRemoteFile(url, filename = 'data.h5') {
|
|
|
1481
1515
|
};
|
|
1482
1516
|
}
|
|
1483
1517
|
|
|
1518
|
+
async function openLocalFile(file, filename) {
|
|
1519
|
+
if (!h5wasmModule) {
|
|
1520
|
+
throw new Error('h5wasm not initialized');
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// Close any existing file
|
|
1524
|
+
closeFile();
|
|
1525
|
+
|
|
1526
|
+
// Use provided filename or file.name
|
|
1527
|
+
const fname = filename || file.name || 'local.h5';
|
|
1528
|
+
|
|
1529
|
+
// Create mount point for WORKERFS
|
|
1530
|
+
mountPath = '/local-' + Date.now();
|
|
1531
|
+
FS.mkdir(mountPath);
|
|
1532
|
+
|
|
1533
|
+
// Mount the file using WORKERFS (zero-copy access)
|
|
1534
|
+
FS.mount(FS.filesystems.WORKERFS, { files: [file] }, mountPath);
|
|
1535
|
+
|
|
1536
|
+
// Open with h5wasm
|
|
1537
|
+
const filePath = mountPath + '/' + fname;
|
|
1538
|
+
currentFile = new h5wasm.File(filePath, 'r');
|
|
1539
|
+
|
|
1540
|
+
return {
|
|
1541
|
+
success: true,
|
|
1542
|
+
path: currentFile.path,
|
|
1543
|
+
filename: currentFile.filename,
|
|
1544
|
+
keys: currentFile.keys()
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
async function openBufferFile(buffer, filename = 'data.h5') {
|
|
1549
|
+
if (!h5wasmModule) {
|
|
1550
|
+
throw new Error('h5wasm not initialized');
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// Close any existing file
|
|
1554
|
+
closeFile();
|
|
1555
|
+
|
|
1556
|
+
// Write buffer to virtual filesystem
|
|
1557
|
+
const data = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
1558
|
+
mountPath = '/buffer-' + Date.now() + '/' + filename;
|
|
1559
|
+
|
|
1560
|
+
// Create parent directory
|
|
1561
|
+
const dir = mountPath.substring(0, mountPath.lastIndexOf('/'));
|
|
1562
|
+
FS.mkdir(dir);
|
|
1563
|
+
|
|
1564
|
+
// Write file to virtual FS
|
|
1565
|
+
FS.writeFile(mountPath, data);
|
|
1566
|
+
|
|
1567
|
+
// Open with h5wasm
|
|
1568
|
+
currentFile = new h5wasm.File(mountPath, 'r');
|
|
1569
|
+
|
|
1570
|
+
return {
|
|
1571
|
+
success: true,
|
|
1572
|
+
path: currentFile.path,
|
|
1573
|
+
filename: currentFile.filename,
|
|
1574
|
+
keys: currentFile.keys()
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1484
1578
|
function getKeys(path) {
|
|
1485
1579
|
if (!currentFile) throw new Error('No file open');
|
|
1486
1580
|
const item = path === '/' || !path ? currentFile : currentFile.get(path);
|
|
@@ -1488,19 +1582,38 @@ function getKeys(path) {
|
|
|
1488
1582
|
return item.keys ? item.keys() : [];
|
|
1489
1583
|
}
|
|
1490
1584
|
|
|
1585
|
+
function serializeAttrValue(attr) {
|
|
1586
|
+
if (!attr) return null;
|
|
1587
|
+
// h5wasm Attribute objects have a .value property
|
|
1588
|
+
const val = attr.value !== undefined ? attr.value : attr;
|
|
1589
|
+
// Convert Uint8Array to string for JSON attributes
|
|
1590
|
+
if (val instanceof Uint8Array) {
|
|
1591
|
+
return { value: new TextDecoder().decode(val) };
|
|
1592
|
+
}
|
|
1593
|
+
// Wrap primitive values to preserve structure
|
|
1594
|
+
return { value: val };
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1491
1597
|
function getAttr(path, name) {
|
|
1492
1598
|
if (!currentFile) throw new Error('No file open');
|
|
1493
1599
|
const item = path === '/' || !path ? currentFile : currentFile.get(path);
|
|
1494
1600
|
if (!item) throw new Error('Path not found: ' + path);
|
|
1495
1601
|
const attrs = item.attrs;
|
|
1496
|
-
|
|
1602
|
+
const attr = attrs?.[name];
|
|
1603
|
+
return serializeAttrValue(attr);
|
|
1497
1604
|
}
|
|
1498
1605
|
|
|
1499
1606
|
function getAttrs(path) {
|
|
1500
1607
|
if (!currentFile) throw new Error('No file open');
|
|
1501
1608
|
const item = path === '/' || !path ? currentFile : currentFile.get(path);
|
|
1502
1609
|
if (!item) throw new Error('Path not found: ' + path);
|
|
1503
|
-
|
|
1610
|
+
const rawAttrs = item.attrs || {};
|
|
1611
|
+
// Serialize all attributes for proper transfer through postMessage
|
|
1612
|
+
const serialized = {};
|
|
1613
|
+
for (const key of Object.keys(rawAttrs)) {
|
|
1614
|
+
serialized[key] = serializeAttrValue(rawAttrs[key]);
|
|
1615
|
+
}
|
|
1616
|
+
return serialized;
|
|
1504
1617
|
}
|
|
1505
1618
|
|
|
1506
1619
|
function getDatasetMeta(path) {
|
|
@@ -1653,7 +1766,7 @@ var StreamingH5File = class {
|
|
|
1653
1766
|
await this.send("init", { h5wasmUrl: options?.h5wasmUrl });
|
|
1654
1767
|
}
|
|
1655
1768
|
/**
|
|
1656
|
-
* Open a remote HDF5 file for streaming access.
|
|
1769
|
+
* Open a remote HDF5 file for streaming access via URL.
|
|
1657
1770
|
*
|
|
1658
1771
|
* @param url - URL to the HDF5 file (must support HTTP range requests)
|
|
1659
1772
|
* @param options - Optional settings
|
|
@@ -1665,6 +1778,51 @@ var StreamingH5File = class {
|
|
|
1665
1778
|
this._keys = result.keys || [];
|
|
1666
1779
|
this._isOpen = true;
|
|
1667
1780
|
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Open a local File object using WORKERFS (zero-copy).
|
|
1783
|
+
*
|
|
1784
|
+
* @param file - File object from file input or drag-and-drop
|
|
1785
|
+
* @param options - Optional settings
|
|
1786
|
+
*/
|
|
1787
|
+
async openLocal(file, options) {
|
|
1788
|
+
await this.init(options);
|
|
1789
|
+
const filename = options?.filenameHint || file.name || "data.h5";
|
|
1790
|
+
const result = await this.send("openLocal", { file, filename });
|
|
1791
|
+
this._keys = result.keys || [];
|
|
1792
|
+
this._isOpen = true;
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Open an HDF5 file from an ArrayBuffer or Uint8Array.
|
|
1796
|
+
*
|
|
1797
|
+
* @param buffer - ArrayBuffer or Uint8Array containing the HDF5 file data
|
|
1798
|
+
* @param options - Optional settings
|
|
1799
|
+
*/
|
|
1800
|
+
async openBuffer(buffer, options) {
|
|
1801
|
+
await this.init(options);
|
|
1802
|
+
const filename = options?.filenameHint || "data.h5";
|
|
1803
|
+
const data = buffer instanceof Uint8Array ? buffer.buffer : buffer;
|
|
1804
|
+
const result = await this.send("openBuffer", { buffer: data, filename });
|
|
1805
|
+
this._keys = result.keys || [];
|
|
1806
|
+
this._isOpen = true;
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Open an HDF5 file from any supported source.
|
|
1810
|
+
*
|
|
1811
|
+
* @param source - URL string, File, ArrayBuffer, or Uint8Array
|
|
1812
|
+
* @param options - Optional settings
|
|
1813
|
+
*/
|
|
1814
|
+
async openAny(source, options) {
|
|
1815
|
+
if (typeof source === "string") {
|
|
1816
|
+
return this.open(source, options);
|
|
1817
|
+
}
|
|
1818
|
+
if (typeof File !== "undefined" && source instanceof File) {
|
|
1819
|
+
return this.openLocal(source, options);
|
|
1820
|
+
}
|
|
1821
|
+
if (source instanceof ArrayBuffer || source instanceof Uint8Array) {
|
|
1822
|
+
return this.openBuffer(source, options);
|
|
1823
|
+
}
|
|
1824
|
+
throw new Error("Unsupported source type for StreamingH5File");
|
|
1825
|
+
}
|
|
1668
1826
|
/**
|
|
1669
1827
|
* Whether a file is currently open.
|
|
1670
1828
|
*/
|
|
@@ -1744,6 +1902,14 @@ async function openStreamingH5(url, options) {
|
|
|
1744
1902
|
await file.open(url, options);
|
|
1745
1903
|
return file;
|
|
1746
1904
|
}
|
|
1905
|
+
async function openH5Worker(source, options) {
|
|
1906
|
+
if (!isStreamingSupported()) {
|
|
1907
|
+
throw new Error("Web Worker HDF5 access requires Worker/Blob/URL support");
|
|
1908
|
+
}
|
|
1909
|
+
const file = new StreamingH5File();
|
|
1910
|
+
await file.openAny(source, options);
|
|
1911
|
+
return file;
|
|
1912
|
+
}
|
|
1747
1913
|
|
|
1748
1914
|
// src/codecs/slp/h5.ts
|
|
1749
1915
|
var isNode = typeof process !== "undefined" && !!process.versions?.node;
|
|
@@ -1891,7 +2057,7 @@ var Hdf5VideoBackend = class {
|
|
|
1891
2057
|
fps;
|
|
1892
2058
|
file;
|
|
1893
2059
|
datasetPath;
|
|
1894
|
-
|
|
2060
|
+
frameNumberToIndex;
|
|
1895
2061
|
format;
|
|
1896
2062
|
channelOrder;
|
|
1897
2063
|
cachedData;
|
|
@@ -1901,7 +2067,8 @@ var Hdf5VideoBackend = class {
|
|
|
1901
2067
|
this.file = options.file;
|
|
1902
2068
|
this.datasetPath = options.datasetPath;
|
|
1903
2069
|
this.dataset = options.datasetPath;
|
|
1904
|
-
|
|
2070
|
+
const frameNumbers = options.frameNumbers ?? [];
|
|
2071
|
+
this.frameNumberToIndex = new Map(frameNumbers.map((num, idx) => [num, idx]));
|
|
1905
2072
|
this.format = options.format ?? "png";
|
|
1906
2073
|
this.channelOrder = options.channelOrder ?? "RGB";
|
|
1907
2074
|
this.shape = options.shape;
|
|
@@ -1912,8 +2079,8 @@ var Hdf5VideoBackend = class {
|
|
|
1912
2079
|
async getFrame(frameIndex) {
|
|
1913
2080
|
const dataset = this.file.get(this.datasetPath);
|
|
1914
2081
|
if (!dataset) return null;
|
|
1915
|
-
const index = this.
|
|
1916
|
-
if (index
|
|
2082
|
+
const index = this.frameNumberToIndex.size > 0 ? this.frameNumberToIndex.get(frameIndex) : frameIndex;
|
|
2083
|
+
if (index === void 0) return null;
|
|
1917
2084
|
if (!this.cachedData) {
|
|
1918
2085
|
const value = dataset.value;
|
|
1919
2086
|
this.cachedData = normalizeVideoData2(value);
|
|
@@ -1938,7 +2105,7 @@ var Hdf5VideoBackend = class {
|
|
|
1938
2105
|
}
|
|
1939
2106
|
if (!rawBytes || rawBytes.length === 0) return null;
|
|
1940
2107
|
if (isEncodedFormat2(this.format)) {
|
|
1941
|
-
const decoded = await decodeImageBytes2(rawBytes, this.format);
|
|
2108
|
+
const decoded = await decodeImageBytes2(rawBytes, this.format, this.channelOrder);
|
|
1942
2109
|
return decoded ?? rawBytes;
|
|
1943
2110
|
}
|
|
1944
2111
|
const image = decodeRawFrame2(rawBytes, this.shape, this.channelOrder);
|
|
@@ -2007,12 +2174,29 @@ function isEncodedFormat2(format) {
|
|
|
2007
2174
|
const normalized = format.toLowerCase();
|
|
2008
2175
|
return normalized === "png" || normalized === "jpg" || normalized === "jpeg";
|
|
2009
2176
|
}
|
|
2010
|
-
async function decodeImageBytes2(bytes, format) {
|
|
2177
|
+
async function decodeImageBytes2(bytes, format, channelOrder) {
|
|
2011
2178
|
if (!isBrowser4 || typeof createImageBitmap === "undefined") return null;
|
|
2012
2179
|
const mime = format.toLowerCase() === "png" ? "image/png" : "image/jpeg";
|
|
2013
2180
|
const safeBytes = new Uint8Array(bytes);
|
|
2014
2181
|
const blob = new Blob([safeBytes.buffer], { type: mime });
|
|
2015
|
-
|
|
2182
|
+
const bitmap = await createImageBitmap(blob);
|
|
2183
|
+
const useBgr = channelOrder.toUpperCase() === "BGR";
|
|
2184
|
+
if (!useBgr) {
|
|
2185
|
+
return bitmap;
|
|
2186
|
+
}
|
|
2187
|
+
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
2188
|
+
const ctx = canvas.getContext("2d");
|
|
2189
|
+
if (!ctx) return bitmap;
|
|
2190
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
2191
|
+
const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
2192
|
+
const data = imageData.data;
|
|
2193
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
2194
|
+
const r = data[i];
|
|
2195
|
+
const b = data[i + 2];
|
|
2196
|
+
data[i] = b;
|
|
2197
|
+
data[i + 2] = r;
|
|
2198
|
+
}
|
|
2199
|
+
return imageData;
|
|
2016
2200
|
}
|
|
2017
2201
|
function decodeRawFrame2(bytes, shape, channelOrder) {
|
|
2018
2202
|
if (!isBrowser4 || !shape) return null;
|
|
@@ -2076,7 +2260,7 @@ async function readSlp(source, options) {
|
|
|
2076
2260
|
const labelsPath = typeof source === "string" ? source : options?.h5?.filenameHint ?? "slp-data.slp";
|
|
2077
2261
|
const skeletons = parseSkeletons(metadataJson);
|
|
2078
2262
|
const tracks = readTracks(file.get("tracks_json"));
|
|
2079
|
-
const videos = await readVideos(file.get("videos_json"), labelsPath, options?.openVideos ?? true, file);
|
|
2263
|
+
const videos = await readVideos(file.get("videos_json"), labelsPath, options?.openVideos ?? true, file, formatId);
|
|
2080
2264
|
const suggestions = readSuggestions(file.get("suggestions_json"), videos);
|
|
2081
2265
|
const framesData = normalizeStructDataset(file.get("frames"));
|
|
2082
2266
|
const instancesData = normalizeStructDataset(file.get("instances"));
|
|
@@ -2129,11 +2313,12 @@ function readTracks(dataset) {
|
|
|
2129
2313
|
}
|
|
2130
2314
|
return tracks;
|
|
2131
2315
|
}
|
|
2132
|
-
async function readVideos(dataset, labelsPath, openVideos, file) {
|
|
2316
|
+
async function readVideos(dataset, labelsPath, openVideos, file, formatId) {
|
|
2133
2317
|
if (!dataset) return [];
|
|
2134
2318
|
const values = dataset.value ?? [];
|
|
2135
2319
|
const videos = [];
|
|
2136
|
-
for (
|
|
2320
|
+
for (let videoIndex = 0; videoIndex < values.length; videoIndex++) {
|
|
2321
|
+
const entry = values[videoIndex];
|
|
2137
2322
|
if (!entry) continue;
|
|
2138
2323
|
const parsed = typeof entry === "string" ? JSON.parse(entry) : JSON.parse(textDecoder.decode(entry));
|
|
2139
2324
|
const backendMeta = parsed.backend ?? {};
|
|
@@ -2144,14 +2329,26 @@ async function readVideos(dataset, labelsPath, openVideos, file) {
|
|
|
2144
2329
|
embedded = true;
|
|
2145
2330
|
filename = labelsPath;
|
|
2146
2331
|
}
|
|
2332
|
+
if (embedded && !datasetPath) {
|
|
2333
|
+
datasetPath = findVideoDataset(file, videoIndex);
|
|
2334
|
+
}
|
|
2335
|
+
const channelOrder = backendMeta.channel_order ?? (formatId < 1.4 ? "BGR" : "RGB");
|
|
2336
|
+
let format = backendMeta.format;
|
|
2337
|
+
if (!format && datasetPath) {
|
|
2338
|
+
const videoDs = file.get(datasetPath);
|
|
2339
|
+
if (videoDs) {
|
|
2340
|
+
const attrs = videoDs.attrs ?? {};
|
|
2341
|
+
format = attrs.format?.value ?? attrs.format;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2147
2344
|
let backend = null;
|
|
2148
2345
|
if (openVideos) {
|
|
2149
2346
|
backend = await createVideoBackend(filename, {
|
|
2150
2347
|
dataset: datasetPath ?? void 0,
|
|
2151
2348
|
embedded,
|
|
2152
2349
|
frameNumbers: readFrameNumbers(file, datasetPath),
|
|
2153
|
-
format
|
|
2154
|
-
channelOrder
|
|
2350
|
+
format,
|
|
2351
|
+
channelOrder,
|
|
2155
2352
|
shape: backendMeta.shape,
|
|
2156
2353
|
fps: backendMeta.fps
|
|
2157
2354
|
});
|
|
@@ -2163,7 +2360,8 @@ async function readVideos(dataset, labelsPath, openVideos, file) {
|
|
|
2163
2360
|
backend,
|
|
2164
2361
|
backendMetadata: backendMeta,
|
|
2165
2362
|
sourceVideo,
|
|
2166
|
-
openBackend: openVideos
|
|
2363
|
+
openBackend: openVideos,
|
|
2364
|
+
embedded
|
|
2167
2365
|
})
|
|
2168
2366
|
);
|
|
2169
2367
|
}
|
|
@@ -2177,6 +2375,28 @@ function readFrameNumbers(file, datasetPath) {
|
|
|
2177
2375
|
const values = frameDataset.value ?? [];
|
|
2178
2376
|
return Array.from(values).map((v) => Number(v));
|
|
2179
2377
|
}
|
|
2378
|
+
function findVideoDataset(file, videoIndex) {
|
|
2379
|
+
const explicitPath = `video${videoIndex}/video`;
|
|
2380
|
+
if (file.get(explicitPath)) {
|
|
2381
|
+
return explicitPath;
|
|
2382
|
+
}
|
|
2383
|
+
const keys = file.keys?.() ?? [];
|
|
2384
|
+
for (const key of keys) {
|
|
2385
|
+
if (key.startsWith("video")) {
|
|
2386
|
+
const candidatePath = `${key}/video`;
|
|
2387
|
+
if (file.get(candidatePath)) {
|
|
2388
|
+
if (videoIndex === 0) {
|
|
2389
|
+
return candidatePath;
|
|
2390
|
+
}
|
|
2391
|
+
const keyIndex = parseInt(key.slice(5), 10);
|
|
2392
|
+
if (!isNaN(keyIndex) && keyIndex === videoIndex) {
|
|
2393
|
+
return candidatePath;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
return null;
|
|
2399
|
+
}
|
|
2180
2400
|
function readSuggestions(dataset, videos) {
|
|
2181
2401
|
if (!dataset) return [];
|
|
2182
2402
|
const values = dataset.value ?? [];
|
|
@@ -2456,17 +2676,18 @@ function slicePoints(data, start, end, predicted = false) {
|
|
|
2456
2676
|
}
|
|
2457
2677
|
|
|
2458
2678
|
// src/codecs/slp/read-streaming.ts
|
|
2459
|
-
async function readSlpStreaming(
|
|
2679
|
+
async function readSlpStreaming(source, options) {
|
|
2460
2680
|
if (!isStreamingSupported()) {
|
|
2461
2681
|
throw new Error("Streaming HDF5 requires Web Worker support (browser environment)");
|
|
2462
2682
|
}
|
|
2463
|
-
const file = await
|
|
2683
|
+
const file = await openH5Worker(source, {
|
|
2464
2684
|
h5wasmUrl: options?.h5wasmUrl,
|
|
2465
2685
|
filenameHint: options?.filenameHint
|
|
2466
2686
|
});
|
|
2467
2687
|
const openVideos = options?.openVideos ?? false;
|
|
2688
|
+
const sourcePath = typeof source === "string" ? source : typeof File !== "undefined" && source instanceof File ? source.name : options?.filenameHint ?? "slp-data.slp";
|
|
2468
2689
|
try {
|
|
2469
|
-
return await readFromStreamingFile(file,
|
|
2690
|
+
return await readFromStreamingFile(file, sourcePath, options?.filenameHint, openVideos);
|
|
2470
2691
|
} finally {
|
|
2471
2692
|
if (!openVideos) {
|
|
2472
2693
|
await file.close();
|
|
@@ -2482,7 +2703,7 @@ async function readFromStreamingFile(file, url, filenameHint, openVideos = false
|
|
|
2482
2703
|
const labelsPath = filenameHint ?? url.split("/").pop()?.split("?")[0] ?? "slp-data.slp";
|
|
2483
2704
|
const skeletons = parseSkeletons(metadataJson);
|
|
2484
2705
|
const tracks = await readTracksStreaming(file);
|
|
2485
|
-
const videos = await readVideosStreaming(file, labelsPath, openVideos);
|
|
2706
|
+
const videos = await readVideosStreaming(file, labelsPath, openVideos, formatId);
|
|
2486
2707
|
const suggestions = await readSuggestionsStreaming(file, videos);
|
|
2487
2708
|
const framesData = await readStructDatasetStreaming(file, "frames");
|
|
2488
2709
|
const instancesData = await readStructDatasetStreaming(file, "instances");
|
|
@@ -2520,7 +2741,7 @@ async function readTracksStreaming(file) {
|
|
|
2520
2741
|
return [];
|
|
2521
2742
|
}
|
|
2522
2743
|
}
|
|
2523
|
-
async function readVideosStreaming(file, labelsPath, openVideos = false) {
|
|
2744
|
+
async function readVideosStreaming(file, labelsPath, openVideos = false, formatId = 1) {
|
|
2524
2745
|
try {
|
|
2525
2746
|
const keys = file.keys();
|
|
2526
2747
|
if (!keys.includes("videos_json")) return [];
|
|
@@ -2528,18 +2749,35 @@ async function readVideosStreaming(file, labelsPath, openVideos = false) {
|
|
|
2528
2749
|
const values = normalizeDatasetArray(data.value);
|
|
2529
2750
|
const metadataList = parseVideosMetadata(values, labelsPath);
|
|
2530
2751
|
const videos = [];
|
|
2531
|
-
for (
|
|
2752
|
+
for (let videoIndex = 0; videoIndex < metadataList.length; videoIndex++) {
|
|
2753
|
+
const meta = metadataList[videoIndex];
|
|
2532
2754
|
const shape = meta.frameCount && meta.height && meta.width && meta.channels ? [meta.frameCount, meta.height, meta.width, meta.channels] : void 0;
|
|
2755
|
+
let datasetPath = meta.dataset;
|
|
2756
|
+
if (meta.embedded && !datasetPath) {
|
|
2757
|
+
datasetPath = await findVideoDatasetStreaming(file, videoIndex) ?? void 0;
|
|
2758
|
+
}
|
|
2759
|
+
const channelOrder = meta.channelOrder ?? (formatId < 1.4 ? "BGR" : "RGB");
|
|
2760
|
+
let format = meta.format;
|
|
2761
|
+
if (!format && datasetPath) {
|
|
2762
|
+
try {
|
|
2763
|
+
const attrs = await file.getAttrs(datasetPath);
|
|
2764
|
+
const formatAttr = attrs.format;
|
|
2765
|
+
if (formatAttr) {
|
|
2766
|
+
format = typeof formatAttr === "string" ? formatAttr : formatAttr?.value;
|
|
2767
|
+
}
|
|
2768
|
+
} catch {
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2533
2771
|
let backend = null;
|
|
2534
|
-
if (openVideos && meta.embedded &&
|
|
2535
|
-
const frameNumbers = await readFrameNumbersStreaming(file,
|
|
2772
|
+
if (openVideos && meta.embedded && datasetPath) {
|
|
2773
|
+
const frameNumbers = await readFrameNumbersStreaming(file, datasetPath);
|
|
2536
2774
|
backend = new StreamingHdf5VideoBackend({
|
|
2537
2775
|
filename: meta.filename,
|
|
2538
2776
|
h5file: file,
|
|
2539
|
-
datasetPath
|
|
2777
|
+
datasetPath,
|
|
2540
2778
|
frameNumbers,
|
|
2541
|
-
format:
|
|
2542
|
-
channelOrder
|
|
2779
|
+
format: format ?? "png",
|
|
2780
|
+
channelOrder,
|
|
2543
2781
|
shape,
|
|
2544
2782
|
fps: meta.fps
|
|
2545
2783
|
});
|
|
@@ -2548,14 +2786,15 @@ async function readVideosStreaming(file, labelsPath, openVideos = false) {
|
|
|
2548
2786
|
filename: meta.filename,
|
|
2549
2787
|
backend,
|
|
2550
2788
|
backendMetadata: {
|
|
2551
|
-
dataset:
|
|
2552
|
-
format
|
|
2789
|
+
dataset: datasetPath,
|
|
2790
|
+
format,
|
|
2553
2791
|
shape,
|
|
2554
2792
|
fps: meta.fps,
|
|
2555
|
-
channel_order:
|
|
2793
|
+
channel_order: channelOrder
|
|
2556
2794
|
},
|
|
2557
2795
|
sourceVideo: meta.sourceVideo ? new Video({ filename: meta.sourceVideo.filename }) : null,
|
|
2558
|
-
openBackend: openVideos && meta.embedded
|
|
2796
|
+
openBackend: openVideos && meta.embedded,
|
|
2797
|
+
embedded: meta.embedded
|
|
2559
2798
|
}));
|
|
2560
2799
|
}
|
|
2561
2800
|
return videos;
|
|
@@ -2584,6 +2823,41 @@ async function readFrameNumbersStreaming(file, datasetPath) {
|
|
|
2584
2823
|
return [];
|
|
2585
2824
|
}
|
|
2586
2825
|
}
|
|
2826
|
+
async function findVideoDatasetStreaming(file, videoIndex) {
|
|
2827
|
+
try {
|
|
2828
|
+
const explicitPath = `video${videoIndex}/video`;
|
|
2829
|
+
const explicitGroupPath = `video${videoIndex}`;
|
|
2830
|
+
try {
|
|
2831
|
+
const groupKeys = await file.getKeys(explicitGroupPath);
|
|
2832
|
+
if (groupKeys.includes("video")) {
|
|
2833
|
+
return explicitPath;
|
|
2834
|
+
}
|
|
2835
|
+
} catch {
|
|
2836
|
+
}
|
|
2837
|
+
const rootKeys = file.keys();
|
|
2838
|
+
for (const key of rootKeys) {
|
|
2839
|
+
if (key.startsWith("video")) {
|
|
2840
|
+
try {
|
|
2841
|
+
const groupKeys = await file.getKeys(key);
|
|
2842
|
+
if (groupKeys.includes("video")) {
|
|
2843
|
+
const candidatePath = `${key}/video`;
|
|
2844
|
+
if (videoIndex === 0) {
|
|
2845
|
+
return candidatePath;
|
|
2846
|
+
}
|
|
2847
|
+
const keyIndex = parseInt(key.slice(5), 10);
|
|
2848
|
+
if (!isNaN(keyIndex) && keyIndex === videoIndex) {
|
|
2849
|
+
return candidatePath;
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
} catch {
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
return null;
|
|
2857
|
+
} catch {
|
|
2858
|
+
return null;
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2587
2861
|
async function readSuggestionsStreaming(file, videos) {
|
|
2588
2862
|
try {
|
|
2589
2863
|
const keys = file.keys();
|
|
@@ -3095,29 +3369,44 @@ function createMatrixDataset(file, name, rows, fieldNames, dtype) {
|
|
|
3095
3369
|
}
|
|
3096
3370
|
|
|
3097
3371
|
// src/io/main.ts
|
|
3098
|
-
function
|
|
3099
|
-
return typeof
|
|
3372
|
+
function isNode3() {
|
|
3373
|
+
return typeof process !== "undefined" && !!process.versions?.node;
|
|
3100
3374
|
}
|
|
3101
|
-
function
|
|
3102
|
-
return typeof window !== "undefined" &&
|
|
3375
|
+
function isBrowserWithWorkerSupport() {
|
|
3376
|
+
return typeof window !== "undefined" && isStreamingSupported();
|
|
3103
3377
|
}
|
|
3104
3378
|
async function loadSlp(source, options) {
|
|
3105
3379
|
const streamMode = options?.h5?.stream ?? "auto";
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3380
|
+
const openVideos = options?.openVideos ?? true;
|
|
3381
|
+
if (isBrowserWithWorkerSupport() && !isNode3() && streamMode !== "download") {
|
|
3382
|
+
let streamingSource;
|
|
3383
|
+
if (typeof source === "string") {
|
|
3384
|
+
streamingSource = source;
|
|
3385
|
+
} else if (source instanceof ArrayBuffer || source instanceof Uint8Array) {
|
|
3386
|
+
streamingSource = source;
|
|
3387
|
+
} else if (typeof File !== "undefined" && source instanceof File) {
|
|
3388
|
+
streamingSource = source;
|
|
3389
|
+
} else if (typeof FileSystemFileHandle !== "undefined" && "getFile" in source) {
|
|
3390
|
+
streamingSource = await source.getFile();
|
|
3391
|
+
} else {
|
|
3392
|
+
streamingSource = null;
|
|
3393
|
+
}
|
|
3394
|
+
if (streamingSource !== null) {
|
|
3395
|
+
try {
|
|
3396
|
+
return await readSlpStreaming(streamingSource, {
|
|
3397
|
+
filenameHint: options?.h5?.filenameHint,
|
|
3398
|
+
openVideos
|
|
3399
|
+
});
|
|
3400
|
+
} catch (e) {
|
|
3401
|
+
if (streamMode === "auto") {
|
|
3402
|
+
console.warn("[sleap-io] Worker-based loading failed, falling back to main thread:", e);
|
|
3403
|
+
} else {
|
|
3404
|
+
throw e;
|
|
3405
|
+
}
|
|
3117
3406
|
}
|
|
3118
3407
|
}
|
|
3119
3408
|
}
|
|
3120
|
-
return readSlp(source, { openVideos
|
|
3409
|
+
return readSlp(source, { openVideos, h5: options?.h5 });
|
|
3121
3410
|
}
|
|
3122
3411
|
async function saveSlp(labels, filename, options) {
|
|
3123
3412
|
await writeSlp(filename, labels, {
|
|
@@ -4009,6 +4298,7 @@ export {
|
|
|
4009
4298
|
loadSlp,
|
|
4010
4299
|
loadVideo,
|
|
4011
4300
|
makeCameraFromDict,
|
|
4301
|
+
openH5Worker,
|
|
4012
4302
|
openStreamingH5,
|
|
4013
4303
|
pointsEmpty,
|
|
4014
4304
|
pointsFromArray,
|