@talmolab/sleap-io.js 0.1.3 → 0.1.4

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 CHANGED
@@ -241,10 +241,118 @@ declare class Mp4BoxVideoBackend implements VideoBackend {
241
241
  private addToCache;
242
242
  }
243
243
 
244
+ /**
245
+ * Streaming HDF5 file access via Web Worker.
246
+ *
247
+ * This module provides a high-level API for accessing remote HDF5 files
248
+ * using HTTP range requests for efficient streaming. The actual HDF5
249
+ * operations run in a Web Worker where synchronous XHR is allowed.
250
+ *
251
+ * @module
252
+ */
253
+ /**
254
+ * Options for opening a streaming HDF5 file.
255
+ */
256
+ interface StreamingH5Options {
257
+ /** URL to h5wasm IIFE bundle. Defaults to CDN. */
258
+ h5wasmUrl?: string;
259
+ /** Filename hint for the HDF5 file. */
260
+ filenameHint?: string;
261
+ }
262
+ /**
263
+ * A streaming HDF5 file handle that uses a Web Worker for range request access.
264
+ *
265
+ * This class provides an API similar to h5wasm.File but operates via message
266
+ * passing to a worker where createLazyFile enables HTTP range requests.
267
+ */
268
+ declare class StreamingH5File {
269
+ private worker;
270
+ private messageId;
271
+ private pendingMessages;
272
+ private _keys;
273
+ private _isOpen;
274
+ constructor();
275
+ private handleMessage;
276
+ private handleError;
277
+ private send;
278
+ /**
279
+ * Initialize the h5wasm module in the worker.
280
+ */
281
+ init(options?: StreamingH5Options): Promise<void>;
282
+ /**
283
+ * Open a remote HDF5 file for streaming access.
284
+ *
285
+ * @param url - URL to the HDF5 file (must support HTTP range requests)
286
+ * @param options - Optional settings
287
+ */
288
+ open(url: string, options?: StreamingH5Options): Promise<void>;
289
+ /**
290
+ * Whether a file is currently open.
291
+ */
292
+ get isOpen(): boolean;
293
+ /**
294
+ * Get the root-level keys in the file.
295
+ */
296
+ keys(): string[];
297
+ /**
298
+ * Get the keys (children) at a given path.
299
+ */
300
+ getKeys(path: string): Promise<string[]>;
301
+ /**
302
+ * Get an attribute value.
303
+ */
304
+ getAttr(path: string, name: string): Promise<unknown>;
305
+ /**
306
+ * Get all attributes at a path.
307
+ */
308
+ getAttrs(path: string): Promise<Record<string, unknown>>;
309
+ /**
310
+ * Get dataset metadata (shape, dtype) without reading values.
311
+ */
312
+ getDatasetMeta(path: string): Promise<{
313
+ shape: number[];
314
+ dtype: string;
315
+ }>;
316
+ /**
317
+ * Read a dataset's value.
318
+ *
319
+ * @param path - Path to the dataset
320
+ * @param slice - Optional slice specification (array of [start, end] pairs)
321
+ */
322
+ getDatasetValue(path: string, slice?: Array<[number, number] | []>): Promise<{
323
+ value: unknown;
324
+ shape: number[];
325
+ dtype: string;
326
+ }>;
327
+ /**
328
+ * Close the file and terminate the worker.
329
+ */
330
+ close(): Promise<void>;
331
+ }
332
+ /**
333
+ * Check if streaming via Web Worker is supported in the current environment.
334
+ */
335
+ declare function isStreamingSupported(): boolean;
336
+ /**
337
+ * Open a remote HDF5 file with streaming support.
338
+ *
339
+ * @param url - URL to the HDF5 file
340
+ * @param options - Optional settings
341
+ * @returns A StreamingH5File instance
342
+ */
343
+ declare function openStreamingH5(url: string, options?: StreamingH5Options): Promise<StreamingH5File>;
344
+
244
345
  type SlpSource = string | ArrayBuffer | Uint8Array | File | FileSystemFileHandle;
245
346
  type StreamMode = "auto" | "range" | "download";
246
347
  type OpenH5Options = {
348
+ /**
349
+ * Streaming mode for remote files:
350
+ * - "auto": Try range requests, fall back to download
351
+ * - "range": Use HTTP range requests (requires Worker support in browser)
352
+ * - "download": Always download the entire file
353
+ */
247
354
  stream?: StreamMode;
355
+ /** Filename hint for the HDF5 file */
248
356
  filenameHint?: string;
249
357
  };
250
358
 
@@ -550,4 +658,4 @@ declare function checkFfmpeg(): Promise<boolean>;
550
658
  */
551
659
  declare function renderVideo(source: Labels | LabeledFrame[], outputPath: string, options?: VideoOptions): Promise<void>;
552
660
 
553
- 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, SuggestionFrame, Track, Video, type VideoBackend, type VideoFrame, type VideoOptions, checkFfmpeg, decodeYamlSkeleton, determineColorScheme, drawCircle, drawCross, drawDiamond, drawSquare, drawTriangle, encodeYamlSkeleton, fromDict, fromNumpy, getMarkerFunction, getPalette, labelsFromNumpy, loadSlp, loadVideo, makeCameraFromDict, renderImage, renderVideo, resolveColor, rgbToCSS, rodriguesTransformation, saveImage, saveSlp, toDataURL, toDict, toJPEG, toNumpy, toPNG };
661
+ 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, 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, renderImage, renderVideo, resolveColor, rgbToCSS, rodriguesTransformation, saveImage, saveSlp, toDataURL, toDict, toJPEG, toNumpy, toPNG };
package/dist/index.js CHANGED
@@ -1203,6 +1203,383 @@ var Mp4BoxVideoBackend = class {
1203
1203
  }
1204
1204
  };
1205
1205
 
1206
+ // src/codecs/slp/h5-worker.ts
1207
+ var H5_WORKER_CODE = `
1208
+ // h5wasm streaming worker
1209
+ // Uses createLazyFile for HTTP range request streaming
1210
+
1211
+ let h5wasmModule = null;
1212
+ let FS = null;
1213
+ let currentFile = null;
1214
+ let mountPath = null;
1215
+
1216
+ self.onmessage = async function(e) {
1217
+ const { type, payload, id } = e.data;
1218
+
1219
+ try {
1220
+ switch (type) {
1221
+ case 'init':
1222
+ await initH5Wasm(payload?.h5wasmUrl);
1223
+ respond(id, { success: true });
1224
+ break;
1225
+
1226
+ case 'openUrl':
1227
+ const result = await openRemoteFile(payload.url, payload.filename);
1228
+ respond(id, result);
1229
+ break;
1230
+
1231
+ case 'getKeys':
1232
+ const keys = getKeys(payload.path);
1233
+ respond(id, { success: true, keys });
1234
+ break;
1235
+
1236
+ case 'getAttr':
1237
+ const attr = getAttr(payload.path, payload.name);
1238
+ respond(id, { success: true, value: attr });
1239
+ break;
1240
+
1241
+ case 'getAttrs':
1242
+ const attrs = getAttrs(payload.path);
1243
+ respond(id, { success: true, attrs });
1244
+ break;
1245
+
1246
+ case 'getDatasetMeta':
1247
+ const meta = getDatasetMeta(payload.path);
1248
+ respond(id, { success: true, meta });
1249
+ break;
1250
+
1251
+ case 'getDatasetValue':
1252
+ const data = getDatasetValue(payload.path, payload.slice);
1253
+ respond(id, { success: true, data }, data.transferables);
1254
+ break;
1255
+
1256
+ case 'close':
1257
+ closeFile();
1258
+ respond(id, { success: true });
1259
+ break;
1260
+
1261
+ default:
1262
+ respond(id, { success: false, error: 'Unknown message type: ' + type });
1263
+ }
1264
+ } catch (error) {
1265
+ respond(id, { success: false, error: error.message || String(error) });
1266
+ }
1267
+ };
1268
+
1269
+ function respond(id, data, transferables) {
1270
+ if (transferables) {
1271
+ self.postMessage({ id, ...data }, transferables);
1272
+ } else {
1273
+ self.postMessage({ id, ...data });
1274
+ }
1275
+ }
1276
+
1277
+ async function initH5Wasm(h5wasmUrl) {
1278
+ if (h5wasmModule) return;
1279
+
1280
+ // Default to CDN if no URL provided
1281
+ const url = h5wasmUrl || 'https://cdn.jsdelivr.net/npm/h5wasm@0.8.8/dist/iife/h5wasm.js';
1282
+
1283
+ // Import h5wasm IIFE
1284
+ importScripts(url);
1285
+
1286
+ // Wait for module to be ready
1287
+ const Module = await h5wasm.ready;
1288
+ h5wasmModule = h5wasm;
1289
+ FS = Module.FS;
1290
+ }
1291
+
1292
+ async function openRemoteFile(url, filename = 'data.h5') {
1293
+ if (!h5wasmModule) {
1294
+ throw new Error('h5wasm not initialized');
1295
+ }
1296
+
1297
+ // Close any existing file
1298
+ closeFile();
1299
+
1300
+ // Create mount point
1301
+ mountPath = '/remote-' + Date.now();
1302
+ FS.mkdir(mountPath);
1303
+
1304
+ // Create lazy file - this enables range request streaming!
1305
+ FS.createLazyFile(mountPath, filename, url, true, false);
1306
+
1307
+ // Open with h5wasm
1308
+ const filePath = mountPath + '/' + filename;
1309
+ currentFile = new h5wasm.File(filePath, 'r');
1310
+
1311
+ return {
1312
+ success: true,
1313
+ path: currentFile.path,
1314
+ filename: currentFile.filename,
1315
+ keys: currentFile.keys()
1316
+ };
1317
+ }
1318
+
1319
+ function getKeys(path) {
1320
+ if (!currentFile) throw new Error('No file open');
1321
+ const item = path === '/' || !path ? currentFile : currentFile.get(path);
1322
+ if (!item) throw new Error('Path not found: ' + path);
1323
+ return item.keys ? item.keys() : [];
1324
+ }
1325
+
1326
+ function getAttr(path, name) {
1327
+ if (!currentFile) throw new Error('No file open');
1328
+ const item = path === '/' || !path ? currentFile : currentFile.get(path);
1329
+ if (!item) throw new Error('Path not found: ' + path);
1330
+ const attrs = item.attrs;
1331
+ return attrs?.[name] || null;
1332
+ }
1333
+
1334
+ function getAttrs(path) {
1335
+ if (!currentFile) throw new Error('No file open');
1336
+ const item = path === '/' || !path ? currentFile : currentFile.get(path);
1337
+ if (!item) throw new Error('Path not found: ' + path);
1338
+ return item.attrs || {};
1339
+ }
1340
+
1341
+ function getDatasetMeta(path) {
1342
+ if (!currentFile) throw new Error('No file open');
1343
+ const dataset = currentFile.get(path);
1344
+ if (!dataset) throw new Error('Dataset not found: ' + path);
1345
+ return {
1346
+ shape: dataset.shape,
1347
+ dtype: dataset.dtype,
1348
+ metadata: dataset.metadata
1349
+ };
1350
+ }
1351
+
1352
+ function getDatasetValue(path, slice) {
1353
+ if (!currentFile) throw new Error('No file open');
1354
+ const dataset = currentFile.get(path);
1355
+ if (!dataset) throw new Error('Dataset not found: ' + path);
1356
+
1357
+ // Get value or slice
1358
+ let value;
1359
+ if (slice && Array.isArray(slice)) {
1360
+ value = dataset.slice(slice);
1361
+ } else {
1362
+ value = dataset.value;
1363
+ }
1364
+
1365
+ // Prepare for transfer
1366
+ const transferables = [];
1367
+ let transferValue = value;
1368
+
1369
+ if (ArrayBuffer.isView(value)) {
1370
+ // TypedArray - transfer the underlying buffer
1371
+ transferValue = {
1372
+ type: 'typedarray',
1373
+ dtype: value.constructor.name,
1374
+ buffer: value.buffer,
1375
+ byteOffset: value.byteOffset,
1376
+ length: value.length
1377
+ };
1378
+ transferables.push(value.buffer);
1379
+ } else if (value instanceof ArrayBuffer) {
1380
+ transferValue = { type: 'arraybuffer', buffer: value };
1381
+ transferables.push(value);
1382
+ }
1383
+
1384
+ return {
1385
+ value: transferValue,
1386
+ shape: dataset.shape,
1387
+ dtype: dataset.dtype,
1388
+ transferables
1389
+ };
1390
+ }
1391
+
1392
+ function closeFile() {
1393
+ if (currentFile) {
1394
+ try { currentFile.close(); } catch (e) {}
1395
+ currentFile = null;
1396
+ }
1397
+ if (mountPath && FS) {
1398
+ try { FS.rmdir(mountPath); } catch (e) {}
1399
+ mountPath = null;
1400
+ }
1401
+ }
1402
+ `;
1403
+ function createH5Worker() {
1404
+ const blob = new Blob([H5_WORKER_CODE], { type: "application/javascript" });
1405
+ const url = URL.createObjectURL(blob);
1406
+ const worker = new Worker(url);
1407
+ worker.addEventListener(
1408
+ "error",
1409
+ () => {
1410
+ URL.revokeObjectURL(url);
1411
+ },
1412
+ { once: true }
1413
+ );
1414
+ return worker;
1415
+ }
1416
+
1417
+ // src/codecs/slp/h5-streaming.ts
1418
+ function reconstructValue(data) {
1419
+ if (data && typeof data === "object" && "type" in data) {
1420
+ const typed = data;
1421
+ if (typed.type === "typedarray" && typed.buffer) {
1422
+ const TypedArrayConstructor = getTypedArrayConstructor(typed.dtype || "Uint8Array");
1423
+ return new TypedArrayConstructor(typed.buffer, typed.byteOffset || 0, typed.length);
1424
+ }
1425
+ if (typed.type === "arraybuffer" && typed.buffer) {
1426
+ return typed.buffer;
1427
+ }
1428
+ }
1429
+ return data;
1430
+ }
1431
+ function getTypedArrayConstructor(name) {
1432
+ const constructors = {
1433
+ Int8Array,
1434
+ Uint8Array,
1435
+ Uint8ClampedArray,
1436
+ Int16Array,
1437
+ Uint16Array,
1438
+ Int32Array,
1439
+ Uint32Array,
1440
+ Float32Array,
1441
+ Float64Array,
1442
+ BigInt64Array,
1443
+ BigUint64Array
1444
+ };
1445
+ return constructors[name] || Uint8Array;
1446
+ }
1447
+ var StreamingH5File = class {
1448
+ worker;
1449
+ messageId = 0;
1450
+ pendingMessages = /* @__PURE__ */ new Map();
1451
+ _keys = [];
1452
+ _isOpen = false;
1453
+ constructor() {
1454
+ this.worker = createH5Worker();
1455
+ this.worker.onmessage = this.handleMessage.bind(this);
1456
+ this.worker.onerror = this.handleError.bind(this);
1457
+ }
1458
+ handleMessage(e) {
1459
+ const { id, ...data } = e.data;
1460
+ const pending = this.pendingMessages.get(id);
1461
+ if (pending) {
1462
+ this.pendingMessages.delete(id);
1463
+ if (data.success) {
1464
+ pending.resolve(e.data);
1465
+ } else {
1466
+ pending.reject(new Error(data.error || "Worker operation failed"));
1467
+ }
1468
+ }
1469
+ }
1470
+ handleError(e) {
1471
+ console.error("[StreamingH5File] Worker error:", e.message);
1472
+ for (const [id, pending] of this.pendingMessages) {
1473
+ pending.reject(new Error(`Worker error: ${e.message}`));
1474
+ this.pendingMessages.delete(id);
1475
+ }
1476
+ }
1477
+ send(type, payload) {
1478
+ return new Promise((resolve, reject) => {
1479
+ const id = ++this.messageId;
1480
+ this.pendingMessages.set(id, { resolve, reject });
1481
+ this.worker.postMessage({ type, payload, id });
1482
+ });
1483
+ }
1484
+ /**
1485
+ * Initialize the h5wasm module in the worker.
1486
+ */
1487
+ async init(options) {
1488
+ await this.send("init", { h5wasmUrl: options?.h5wasmUrl });
1489
+ }
1490
+ /**
1491
+ * Open a remote HDF5 file for streaming access.
1492
+ *
1493
+ * @param url - URL to the HDF5 file (must support HTTP range requests)
1494
+ * @param options - Optional settings
1495
+ */
1496
+ async open(url, options) {
1497
+ await this.init(options);
1498
+ const filename = options?.filenameHint || url.split("/").pop()?.split("?")[0] || "data.h5";
1499
+ const result = await this.send("openUrl", { url, filename });
1500
+ this._keys = result.keys || [];
1501
+ this._isOpen = true;
1502
+ }
1503
+ /**
1504
+ * Whether a file is currently open.
1505
+ */
1506
+ get isOpen() {
1507
+ return this._isOpen;
1508
+ }
1509
+ /**
1510
+ * Get the root-level keys in the file.
1511
+ */
1512
+ keys() {
1513
+ return this._keys;
1514
+ }
1515
+ /**
1516
+ * Get the keys (children) at a given path.
1517
+ */
1518
+ async getKeys(path) {
1519
+ const result = await this.send("getKeys", { path });
1520
+ return result.keys || [];
1521
+ }
1522
+ /**
1523
+ * Get an attribute value.
1524
+ */
1525
+ async getAttr(path, name) {
1526
+ const result = await this.send("getAttr", { path, name });
1527
+ return result.value?.value ?? result.value;
1528
+ }
1529
+ /**
1530
+ * Get all attributes at a path.
1531
+ */
1532
+ async getAttrs(path) {
1533
+ const result = await this.send("getAttrs", { path });
1534
+ return result.attrs || {};
1535
+ }
1536
+ /**
1537
+ * Get dataset metadata (shape, dtype) without reading values.
1538
+ */
1539
+ async getDatasetMeta(path) {
1540
+ const result = await this.send("getDatasetMeta", { path });
1541
+ const meta = result.meta;
1542
+ return meta;
1543
+ }
1544
+ /**
1545
+ * Read a dataset's value.
1546
+ *
1547
+ * @param path - Path to the dataset
1548
+ * @param slice - Optional slice specification (array of [start, end] pairs)
1549
+ */
1550
+ async getDatasetValue(path, slice) {
1551
+ const result = await this.send("getDatasetValue", { path, slice });
1552
+ const data = result.data;
1553
+ return {
1554
+ value: reconstructValue(data.value),
1555
+ shape: data.shape,
1556
+ dtype: data.dtype
1557
+ };
1558
+ }
1559
+ /**
1560
+ * Close the file and terminate the worker.
1561
+ */
1562
+ async close() {
1563
+ if (this._isOpen) {
1564
+ await this.send("close");
1565
+ this._isOpen = false;
1566
+ }
1567
+ this.worker.terminate();
1568
+ this._keys = [];
1569
+ }
1570
+ };
1571
+ function isStreamingSupported() {
1572
+ return typeof Worker !== "undefined" && typeof Blob !== "undefined" && typeof URL !== "undefined";
1573
+ }
1574
+ async function openStreamingH5(url, options) {
1575
+ if (!isStreamingSupported()) {
1576
+ throw new Error("Streaming HDF5 requires Web Worker support");
1577
+ }
1578
+ const file = new StreamingH5File();
1579
+ await file.open(url, options);
1580
+ return file;
1581
+ }
1582
+
1206
1583
  // src/codecs/slp/h5.ts
1207
1584
  var isNode = typeof process !== "undefined" && !!process.versions?.node;
1208
1585
  var modulePromise = null;
@@ -3017,6 +3394,7 @@ export {
3017
3394
  RecordingSession,
3018
3395
  RenderContext,
3019
3396
  Skeleton,
3397
+ StreamingH5File,
3020
3398
  SuggestionFrame,
3021
3399
  Symmetry,
3022
3400
  Track,
@@ -3034,10 +3412,12 @@ export {
3034
3412
  fromNumpy,
3035
3413
  getMarkerFunction,
3036
3414
  getPalette,
3415
+ isStreamingSupported,
3037
3416
  labelsFromNumpy,
3038
3417
  loadSlp,
3039
3418
  loadVideo,
3040
3419
  makeCameraFromDict,
3420
+ openStreamingH5,
3041
3421
  pointsEmpty,
3042
3422
  pointsFromArray,
3043
3423
  pointsFromDict,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@talmolab/sleap-io.js",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {