@talmolab/sleap-io.js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Talmo Lab
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # sleap-io.js
2
+
3
+ JavaScript/TypeScript utilities for reading and writing SLEAP `.slp` files with streaming-friendly access patterns and a lightweight data model. This is the JS companion to the Python library at https://github.com/talmolab/sleap-io.
4
+
5
+ ## Intent
6
+
7
+ - Make SLP parsing available in browsers and serverless runtimes.
8
+ - Support streaming-first workflows for large `.slp`/`.pkg.slp` files.
9
+ - Provide a minimal data model and codecs that mirror sleap-io behavior.
10
+ - Enable client-side visualization and analysis pipelines.
11
+
12
+ ## Features
13
+
14
+ - SLP read/write with format compatibility (including embedded frames via HDF5 video datasets).
15
+ - Streaming-friendly file access (URL, `File`, `FileSystemFileHandle`).
16
+ - Core data model (`Labels`, `LabeledFrame`, `Instance`, `Skeleton`, `Video`, etc.).
17
+ - Dictionary and numpy codecs for interchange.
18
+ - Demo app for quick inspection.
19
+
20
+ ## Quickstart
21
+
22
+ ```bash
23
+ npm install
24
+ npm run build
25
+ ```
26
+
27
+ ### Load and save SLP
28
+
29
+ ```ts
30
+ import { loadSlp, saveSlp } from "sleap-io.js";
31
+
32
+ const labels = await loadSlp("/path/to/session.slp", { openVideos: false });
33
+ await saveSlp(labels, "/tmp/session-roundtrip.slp", { embed: false });
34
+ ```
35
+
36
+ ### Load video
37
+
38
+ ```ts
39
+ import { loadVideo } from "sleap-io.js";
40
+
41
+ const video = await loadVideo("/path/to/video.mp4", { openBackend: false });
42
+ video.close();
43
+ ```
44
+
45
+ ## Architecture
46
+
47
+ - **I/O layer**: `loadSlp`/`saveSlp` wrap the HDF5 reader/writer in `src/codecs/slp`.
48
+ - **Data model**: `src/model` mirrors sleap-io classes and supports numpy/dict codecs.
49
+ - **Backends**: `src/video` provides browser media and embedded HDF5 decoding.
50
+ - **Streaming**: `src/codecs/slp/h5.ts` selects URL/File/FS handle strategies.
51
+
52
+ ## Environments and Streaming
53
+
54
+ - **Env1 (Static SPA)**: Browser-only usage with URL streaming (CORS + Range) or the File System Access API.
55
+ - **Env2 (Server/Worker)**: Server-side or worker environments that stream `.slp` from URLs or byte buffers.
56
+ - **Env3 (Local Node/Electron)**: Optional local filesystem access for Node/Electron.
57
+
58
+ Streaming can be tuned with:
59
+
60
+ ```ts
61
+ await loadSlp(url, { h5: { stream: "auto", filenameHint: "session.slp" } });
62
+ ```
63
+
64
+ ## Demo
65
+
66
+ The demo in `demo/` loads the built package from `dist/`. Build first, then serve the repo with a static server and open `demo/index.html`:
67
+
68
+ ```bash
69
+ npm run build
70
+ ```
71
+
72
+ ## Links
73
+
74
+ - Python sleap-io: https://github.com/talmolab/sleap-io
75
+ - Docs: https://io.sleap.ai
@@ -0,0 +1,436 @@
1
+ declare class Node {
2
+ name: string;
3
+ constructor(name: string);
4
+ }
5
+ declare class Edge {
6
+ source: Node;
7
+ destination: Node;
8
+ constructor(source: Node, destination: Node);
9
+ at(index: number): Node;
10
+ }
11
+ declare class Symmetry {
12
+ nodes: Set<Node>;
13
+ constructor(nodes: Iterable<Node>);
14
+ at(index: number): Node;
15
+ }
16
+ type NodeOrIndex = Node | string | number;
17
+ declare class Skeleton {
18
+ nodes: Node[];
19
+ edges: Edge[];
20
+ symmetries: Symmetry[];
21
+ name?: string;
22
+ private nameToNode;
23
+ private nodeToIndex;
24
+ constructor(options: {
25
+ nodes: Array<Node | string>;
26
+ edges?: Array<Edge | [NodeOrIndex, NodeOrIndex]>;
27
+ symmetries?: Array<Symmetry | [NodeOrIndex, NodeOrIndex]>;
28
+ name?: string;
29
+ } | Array<Node | string>);
30
+ rebuildCache(nodes?: Node[]): void;
31
+ get nodeNames(): string[];
32
+ index(node: NodeOrIndex): number;
33
+ node(node: NodeOrIndex): Node;
34
+ get edgeIndices(): Array<[number, number]>;
35
+ get symmetryNames(): Array<[string, string]>;
36
+ matches(other: Skeleton): boolean;
37
+ addEdge(source: NodeOrIndex, destination: NodeOrIndex): void;
38
+ addSymmetry(left: NodeOrIndex, right: NodeOrIndex): void;
39
+ private edgeFrom;
40
+ private symmetryFrom;
41
+ }
42
+
43
+ declare class Track {
44
+ name: string;
45
+ constructor(name: string);
46
+ }
47
+ type Point = {
48
+ xy: [number, number];
49
+ visible: boolean;
50
+ complete: boolean;
51
+ name?: string;
52
+ };
53
+ type PredictedPoint = Point & {
54
+ score: number;
55
+ };
56
+ type PointsArray = Point[];
57
+ type PredictedPointsArray = PredictedPoint[];
58
+ declare function pointsEmpty(length: number, names?: string[]): PointsArray;
59
+ declare function predictedPointsEmpty(length: number, names?: string[]): PredictedPointsArray;
60
+ declare function pointsFromArray(array: number[][], names?: string[]): PointsArray;
61
+ declare function predictedPointsFromArray(array: number[][], names?: string[]): PredictedPointsArray;
62
+ declare class Instance {
63
+ points: PointsArray;
64
+ skeleton: Skeleton;
65
+ track?: Track | null;
66
+ fromPredicted?: PredictedInstance | null;
67
+ trackingScore: number;
68
+ constructor(options: {
69
+ points: PointsArray | Record<string, number[]>;
70
+ skeleton: Skeleton;
71
+ track?: Track | null;
72
+ fromPredicted?: PredictedInstance | null;
73
+ trackingScore?: number;
74
+ });
75
+ static fromArray(points: number[][], skeleton: Skeleton): Instance;
76
+ static fromNumpy(options: {
77
+ pointsData: number[][];
78
+ skeleton: Skeleton;
79
+ track?: Track | null;
80
+ fromPredicted?: PredictedInstance | null;
81
+ trackingScore?: number;
82
+ }): Instance;
83
+ static empty(options: {
84
+ skeleton: Skeleton;
85
+ }): Instance;
86
+ get length(): number;
87
+ get nVisible(): number;
88
+ getPoint(target: number | string | Node): Point;
89
+ numpy(options?: {
90
+ invisibleAsNaN?: boolean;
91
+ }): number[][];
92
+ toString(): string;
93
+ get isEmpty(): boolean;
94
+ overlapsWith(other: Instance, iouThreshold?: number): boolean;
95
+ boundingBox(): [number, number, number, number] | null;
96
+ }
97
+ declare class PredictedInstance extends Instance {
98
+ score: number;
99
+ constructor(options: {
100
+ points: PredictedPointsArray | Record<string, number[]>;
101
+ skeleton: Skeleton;
102
+ track?: Track | null;
103
+ score?: number;
104
+ trackingScore?: number;
105
+ });
106
+ static fromArray(points: number[][], skeleton: Skeleton, score?: number): PredictedInstance;
107
+ static fromNumpy(options: {
108
+ pointsData: number[][];
109
+ skeleton: Skeleton;
110
+ track?: Track | null;
111
+ score?: number;
112
+ trackingScore?: number;
113
+ }): PredictedInstance;
114
+ static empty(options: {
115
+ skeleton: Skeleton;
116
+ }): PredictedInstance;
117
+ numpy(options?: {
118
+ scores?: boolean;
119
+ invisibleAsNaN?: boolean;
120
+ }): number[][];
121
+ toString(): string;
122
+ }
123
+ declare function pointsFromDict(pointsDict: Record<string, number[]>, skeleton: Skeleton): PointsArray;
124
+ declare function predictedPointsFromDict(pointsDict: Record<string, number[]>, skeleton: Skeleton): PredictedPointsArray;
125
+
126
+ type VideoFrame = ImageData | ImageBitmap | Uint8Array | ArrayBuffer;
127
+ interface VideoBackend {
128
+ filename: string | string[];
129
+ shape?: [number, number, number, number];
130
+ fps?: number;
131
+ dataset?: string | null;
132
+ getFrame(frameIndex: number): Promise<VideoFrame | null>;
133
+ getFrameTimes?(): Promise<number[] | null>;
134
+ close(): void;
135
+ }
136
+
137
+ declare class Video {
138
+ filename: string | string[];
139
+ backend: VideoBackend | null;
140
+ backendMetadata: Record<string, unknown>;
141
+ sourceVideo: Video | null;
142
+ openBackend: boolean;
143
+ constructor(options: {
144
+ filename: string | string[];
145
+ backend?: VideoBackend | null;
146
+ backendMetadata?: Record<string, unknown>;
147
+ sourceVideo?: Video | null;
148
+ openBackend?: boolean;
149
+ });
150
+ get originalVideo(): Video | null;
151
+ get shape(): [number, number, number, number] | null;
152
+ get fps(): number | null;
153
+ getFrame(frameIndex: number): Promise<VideoFrame | null>;
154
+ getFrameTimes(): Promise<number[] | null>;
155
+ close(): void;
156
+ matchesPath(other: Video, strict?: boolean): boolean;
157
+ }
158
+
159
+ declare class LabeledFrame {
160
+ video: Video;
161
+ frameIdx: number;
162
+ instances: Array<Instance | PredictedInstance>;
163
+ constructor(options: {
164
+ video: Video;
165
+ frameIdx: number;
166
+ instances?: Array<Instance | PredictedInstance>;
167
+ });
168
+ get length(): number;
169
+ [Symbol.iterator](): Iterator<Instance | PredictedInstance>;
170
+ at(index: number): Instance | PredictedInstance | undefined;
171
+ get userInstances(): Instance[];
172
+ get predictedInstances(): PredictedInstance[];
173
+ get hasUserInstances(): boolean;
174
+ get hasPredictedInstances(): boolean;
175
+ numpy(): number[][][];
176
+ get image(): Promise<ImageData | ImageBitmap | ArrayBuffer | Uint8Array | null>;
177
+ get unusedPredictions(): PredictedInstance[];
178
+ removePredictions(): void;
179
+ removeEmptyInstances(): void;
180
+ }
181
+
182
+ declare class SuggestionFrame {
183
+ video: Video;
184
+ frameIdx: number;
185
+ metadata: Record<string, unknown>;
186
+ constructor(options: {
187
+ video: Video;
188
+ frameIdx: number;
189
+ metadata?: Record<string, unknown>;
190
+ });
191
+ }
192
+
193
+ declare function rodriguesTransformation(input: number[][] | number[]): {
194
+ matrix: number[][];
195
+ vector: number[];
196
+ };
197
+ declare class Camera {
198
+ name?: string;
199
+ rvec: number[];
200
+ tvec: number[];
201
+ matrix?: number[][];
202
+ distortions?: number[];
203
+ constructor(options: {
204
+ name?: string;
205
+ rvec: number[];
206
+ tvec: number[];
207
+ matrix?: number[][];
208
+ distortions?: number[];
209
+ });
210
+ }
211
+ declare class CameraGroup {
212
+ cameras: Camera[];
213
+ metadata: Record<string, unknown>;
214
+ constructor(options?: {
215
+ cameras?: Camera[];
216
+ metadata?: Record<string, unknown>;
217
+ });
218
+ }
219
+ declare class InstanceGroup {
220
+ instanceByCamera: Map<Camera, Instance>;
221
+ score?: number;
222
+ points?: number[][];
223
+ metadata: Record<string, unknown>;
224
+ constructor(options: {
225
+ instanceByCamera: Map<Camera, Instance> | Record<string, Instance>;
226
+ score?: number;
227
+ points?: number[][];
228
+ metadata?: Record<string, unknown>;
229
+ });
230
+ get instances(): Instance[];
231
+ }
232
+ declare class FrameGroup {
233
+ frameIdx: number;
234
+ instanceGroups: InstanceGroup[];
235
+ labeledFrameByCamera: Map<Camera, LabeledFrame>;
236
+ metadata: Record<string, unknown>;
237
+ constructor(options: {
238
+ frameIdx: number;
239
+ instanceGroups: InstanceGroup[];
240
+ labeledFrameByCamera: Map<Camera, LabeledFrame> | Record<string, LabeledFrame>;
241
+ metadata?: Record<string, unknown>;
242
+ });
243
+ get cameras(): Camera[];
244
+ get labeledFrames(): LabeledFrame[];
245
+ getFrame(camera: Camera): LabeledFrame | undefined;
246
+ }
247
+ declare class RecordingSession {
248
+ cameraGroup: CameraGroup;
249
+ frameGroupByFrameIdx: Map<number, FrameGroup>;
250
+ videoByCamera: Map<Camera, Video>;
251
+ cameraByVideo: Map<Video, Camera>;
252
+ metadata: Record<string, unknown>;
253
+ constructor(options?: {
254
+ cameraGroup?: CameraGroup;
255
+ frameGroupByFrameIdx?: Map<number, FrameGroup>;
256
+ videoByCamera?: Map<Camera, Video>;
257
+ cameraByVideo?: Map<Video, Camera>;
258
+ metadata?: Record<string, unknown>;
259
+ });
260
+ get frameGroups(): Map<number, FrameGroup>;
261
+ get videos(): Video[];
262
+ get cameras(): Camera[];
263
+ addVideo(video: Video, camera: Camera): void;
264
+ getCamera(video: Video): Camera | undefined;
265
+ getVideo(camera: Camera): Video | undefined;
266
+ }
267
+ declare function makeCameraFromDict(data: Record<string, unknown>): Camera;
268
+
269
+ declare class Labels {
270
+ labeledFrames: LabeledFrame[];
271
+ videos: Video[];
272
+ skeletons: Skeleton[];
273
+ tracks: Track[];
274
+ suggestions: SuggestionFrame[];
275
+ sessions: RecordingSession[];
276
+ provenance: Record<string, unknown>;
277
+ constructor(options?: {
278
+ labeledFrames?: LabeledFrame[];
279
+ videos?: Video[];
280
+ skeletons?: Skeleton[];
281
+ tracks?: Track[];
282
+ suggestions?: SuggestionFrame[];
283
+ sessions?: RecordingSession[];
284
+ provenance?: Record<string, unknown>;
285
+ });
286
+ get video(): Video;
287
+ get length(): number;
288
+ [Symbol.iterator](): Iterator<LabeledFrame>;
289
+ get instances(): Array<Instance | PredictedInstance>;
290
+ find(options: {
291
+ video?: Video;
292
+ frameIdx?: number;
293
+ }): LabeledFrame[];
294
+ append(frame: LabeledFrame): void;
295
+ toDict(options?: {
296
+ video?: Video | number;
297
+ skipEmptyFrames?: boolean;
298
+ }): LabelsDict;
299
+ static fromNumpy(data: number[][][][], options: {
300
+ videos?: Video[];
301
+ video?: Video;
302
+ skeletons?: Skeleton[] | Skeleton;
303
+ skeleton?: Skeleton;
304
+ trackNames?: string[];
305
+ firstFrame?: number;
306
+ returnConfidence?: boolean;
307
+ }): Labels;
308
+ numpy(options?: {
309
+ video?: Video;
310
+ returnConfidence?: boolean;
311
+ }): number[][][][];
312
+ }
313
+
314
+ declare class LabelsSet {
315
+ labels: Map<string, Labels>;
316
+ constructor(entries?: Record<string, Labels>);
317
+ get size(): number;
318
+ get(key: string): Labels | undefined;
319
+ set(key: string, value: Labels): void;
320
+ delete(key: string): void;
321
+ keys(): IterableIterator<string>;
322
+ values(): IterableIterator<Labels>;
323
+ entries(): IterableIterator<[string, Labels]>;
324
+ [Symbol.iterator](): IterableIterator<[string, Labels]>;
325
+ }
326
+
327
+ declare class Mp4BoxVideoBackend implements VideoBackend {
328
+ filename: string;
329
+ shape?: [number, number, number, number];
330
+ fps?: number;
331
+ dataset?: string | null;
332
+ private ready;
333
+ private mp4box;
334
+ private mp4boxFile;
335
+ private videoTrack;
336
+ private samples;
337
+ private keyframeIndices;
338
+ private cache;
339
+ private cacheSize;
340
+ private lookahead;
341
+ private decoder;
342
+ private config;
343
+ private fileSize;
344
+ private supportsRangeRequests;
345
+ private fileBlob;
346
+ private isDecoding;
347
+ private pendingFrame;
348
+ constructor(filename: string, options?: {
349
+ cacheSize?: number;
350
+ lookahead?: number;
351
+ });
352
+ getFrame(frameIndex: number): Promise<VideoFrame | null>;
353
+ getFrameTimes(): Promise<number[] | null>;
354
+ close(): void;
355
+ private init;
356
+ private openSource;
357
+ private readChunk;
358
+ private extractSamples;
359
+ private findKeyframeBefore;
360
+ private getCodecDescription;
361
+ private readSampleDataByDecodeOrder;
362
+ private decodeRange;
363
+ private addToCache;
364
+ }
365
+
366
+ type SlpSource = string | ArrayBuffer | Uint8Array | File | FileSystemFileHandle;
367
+ type StreamMode = "auto" | "range" | "download";
368
+ type OpenH5Options = {
369
+ stream?: StreamMode;
370
+ filenameHint?: string;
371
+ };
372
+
373
+ declare function loadSlp(source: SlpSource, options?: {
374
+ openVideos?: boolean;
375
+ h5?: OpenH5Options;
376
+ }): Promise<Labels>;
377
+ declare function saveSlp(labels: Labels, filename: string, options?: {
378
+ embed?: boolean | string;
379
+ restoreOriginalVideos?: boolean;
380
+ }): Promise<void>;
381
+ declare function loadVideo(filename: string, options?: {
382
+ dataset?: string;
383
+ openBackend?: boolean;
384
+ }): Promise<Video>;
385
+
386
+ type LabelsDict = {
387
+ version: string;
388
+ skeletons: Array<{
389
+ name?: string;
390
+ nodes: string[];
391
+ edges: Array<[number, number]>;
392
+ symmetries: Array<[number, number]>;
393
+ }>;
394
+ videos: Array<{
395
+ filename: string | string[];
396
+ shape?: number[] | null;
397
+ fps?: number | null;
398
+ backend?: Record<string, unknown>;
399
+ }>;
400
+ tracks: Array<Record<string, unknown>>;
401
+ labeled_frames: Array<{
402
+ frame_idx: number;
403
+ video_idx: number;
404
+ instances: Array<Record<string, unknown>>;
405
+ }>;
406
+ suggestions: Array<Record<string, unknown>>;
407
+ provenance: Record<string, unknown>;
408
+ };
409
+ declare function toDict(labels: Labels, options?: {
410
+ video?: Video | number;
411
+ skipEmptyFrames?: boolean;
412
+ }): LabelsDict;
413
+ declare function fromDict(data: LabelsDict): Labels;
414
+
415
+ declare function toNumpy(labels: Labels, options?: {
416
+ returnConfidence?: boolean;
417
+ video?: Video;
418
+ }): number[][][][];
419
+ declare function fromNumpy(data: number[][][][], options: {
420
+ video?: Video;
421
+ videos?: Video[];
422
+ skeleton?: Skeleton;
423
+ skeletons?: Skeleton[] | Skeleton;
424
+ returnConfidence?: boolean;
425
+ trackNames?: string[];
426
+ firstFrame?: number;
427
+ }): Labels;
428
+ declare function labelsFromNumpy(data: number[][][][], options: {
429
+ video: Video;
430
+ skeleton: Skeleton;
431
+ trackNames?: string[];
432
+ firstFrame?: number;
433
+ returnConfidence?: boolean;
434
+ }): Labels;
435
+
436
+ export { Camera, CameraGroup, Edge, FrameGroup, Instance, InstanceGroup, LabeledFrame, Labels, type LabelsDict, LabelsSet, Mp4BoxVideoBackend, Node, type NodeOrIndex, type Point, type PointsArray, PredictedInstance, type PredictedPoint, type PredictedPointsArray, RecordingSession, Skeleton, SuggestionFrame, Symmetry, Track, Video, type VideoBackend, type VideoFrame, fromDict, fromNumpy, labelsFromNumpy, loadSlp, loadVideo, makeCameraFromDict, pointsEmpty, pointsFromArray, pointsFromDict, predictedPointsEmpty, predictedPointsFromArray, predictedPointsFromDict, rodriguesTransformation, saveSlp, toDict, toNumpy };