@talmolab/sleap-io.js 0.1.2 → 0.1.3

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.
@@ -0,0 +1,126 @@
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
+ export { Edge as E, Instance as I, Node as N, PredictedInstance as P, Skeleton as S, Track as T, type Point as a, type PredictedPoint as b, type PointsArray as c, type PredictedPointsArray as d, predictedPointsEmpty as e, pointsFromArray as f, predictedPointsFromArray as g, pointsFromDict as h, predictedPointsFromDict as i, Symmetry as j, type NodeOrIndex as k, pointsEmpty as p };
package/dist/lite.d.ts ADDED
@@ -0,0 +1,209 @@
1
+ import { S as Skeleton, T as Track } from './instance-D_5PPN1y.js';
2
+ export { E as Edge, N as Node, j as Symmetry } from './instance-D_5PPN1y.js';
3
+
4
+ /**
5
+ * jsfive-based HDF5 file interface.
6
+ * Pure JavaScript implementation for Workers-compatible environments.
7
+ */
8
+ type JsfiveSource = ArrayBuffer | Uint8Array;
9
+
10
+ /**
11
+ * Shared parsing functions for SLP file metadata.
12
+ * Used by both the full h5wasm-based reader and the lite jsfive-based reader.
13
+ */
14
+
15
+ /**
16
+ * Video metadata extracted from videos_json without creating backends.
17
+ */
18
+ interface VideoMetadata {
19
+ /** Original filename or "." for embedded */
20
+ filename: string;
21
+ /** HDF5 dataset path for embedded videos */
22
+ dataset?: string;
23
+ /** Video format (e.g., "mp4", "hdf5") */
24
+ format?: string;
25
+ /** Video width in pixels */
26
+ width?: number;
27
+ /** Video height in pixels */
28
+ height?: number;
29
+ /** Number of color channels */
30
+ channels?: number;
31
+ /** Frames per second */
32
+ fps?: number;
33
+ /** Total number of frames */
34
+ frameCount?: number;
35
+ /** Channel order (e.g., "RGB", "BGR") */
36
+ channelOrder?: string;
37
+ /** Whether video is embedded in the SLP file */
38
+ embedded: boolean;
39
+ /** Source video metadata if this is derived */
40
+ sourceVideo?: {
41
+ filename: string;
42
+ };
43
+ }
44
+ /**
45
+ * Suggestion frame metadata.
46
+ */
47
+ interface SuggestionMetadata {
48
+ /** Video index */
49
+ video: number;
50
+ /** Frame index within the video */
51
+ frameIdx: number;
52
+ /** Additional metadata */
53
+ metadata?: Record<string, unknown>;
54
+ }
55
+ /**
56
+ * Camera metadata from recording sessions.
57
+ */
58
+ interface CameraMetadata {
59
+ /** Camera name */
60
+ name?: string;
61
+ /** Rotation vector (Rodrigues) */
62
+ rvec: number[];
63
+ /** Translation vector */
64
+ tvec: number[];
65
+ /** 3x3 intrinsic camera matrix */
66
+ matrix?: number[][];
67
+ /** Lens distortion coefficients */
68
+ distortions?: number[];
69
+ }
70
+ /**
71
+ * Recording session metadata.
72
+ */
73
+ interface SessionMetadata {
74
+ /** Camera definitions with calibration */
75
+ cameras: CameraMetadata[];
76
+ /** Mapping of camera name/key to video index */
77
+ videosByCamera: Record<string, number>;
78
+ /** Additional session metadata */
79
+ metadata?: Record<string, unknown>;
80
+ }
81
+
82
+ /**
83
+ * Lightweight SLP file reader using jsfive (pure JavaScript).
84
+ *
85
+ * This module provides Workers-compatible SLP file reading without WASM.
86
+ * It can extract all metadata but cannot read pose coordinates (which
87
+ * require compound dataset support).
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * import { loadSlpMetadata } from '@talmolab/sleap-io.js/lite';
92
+ *
93
+ * const response = await fetch('https://example.com/file.slp');
94
+ * const buffer = await response.arrayBuffer();
95
+ * const metadata = await loadSlpMetadata(buffer);
96
+ *
97
+ * console.log('Skeletons:', metadata.skeletons.map(s => s.name));
98
+ * console.log('Frames:', metadata.counts.labeledFrames);
99
+ * ```
100
+ *
101
+ * @packageDocumentation
102
+ */
103
+
104
+ /**
105
+ * Metadata extracted from an SLP file without loading pose data.
106
+ */
107
+ interface SlpMetadata {
108
+ /** SLEAP version that created this file (e.g., "1.3.4") */
109
+ version: string;
110
+ /** HDF5 format ID (e.g., 1.2) */
111
+ formatId: number;
112
+ /** Skeleton definitions with nodes, edges, and symmetries */
113
+ skeletons: Skeleton[];
114
+ /** Track definitions */
115
+ tracks: Track[];
116
+ /** Video metadata (without loaded backends) */
117
+ videos: VideoMetadata[];
118
+ /** Suggestion frame metadata */
119
+ suggestions: SuggestionMetadata[];
120
+ /** Multi-camera recording session metadata */
121
+ sessions: SessionMetadata[];
122
+ /** Dataset counts */
123
+ counts: {
124
+ /** Number of labeled frames */
125
+ labeledFrames: number;
126
+ /** Total number of instances (user + predicted) */
127
+ instances: number;
128
+ /** Number of user-labeled points */
129
+ points: number;
130
+ /** Number of predicted points */
131
+ predictedPoints: number;
132
+ };
133
+ /** Whether any video has embedded image data */
134
+ hasEmbeddedImages: boolean;
135
+ /** Raw provenance data (SLEAP version, build info, etc.) */
136
+ provenance?: Record<string, unknown>;
137
+ }
138
+ /**
139
+ * Load SLP file metadata using jsfive (no WASM required).
140
+ *
141
+ * This is a lightweight alternative to `loadSlp()` for environments
142
+ * that don't support WebAssembly compilation (e.g., Cloudflare Workers).
143
+ *
144
+ * Returns metadata only - does NOT include:
145
+ * - Actual pose coordinates (requires compound dataset reading)
146
+ * - Video frame data (requires VLEN sequence reading)
147
+ * - Instance-frame relationships
148
+ * - Instance scores
149
+ *
150
+ * @param source - ArrayBuffer or Uint8Array containing the SLP file
151
+ * @param options - Optional configuration
152
+ * @param options.filename - Filename hint for embedded video paths
153
+ * @returns SlpMetadata object with skeletons, counts, video info
154
+ * @throws Error if the file is not a valid SLP file
155
+ *
156
+ * @example
157
+ * ```typescript
158
+ * const buffer = await fetch('file.slp').then(r => r.arrayBuffer());
159
+ * const metadata = await loadSlpMetadata(buffer);
160
+ *
161
+ * console.log(`${metadata.skeletons.length} skeleton(s)`);
162
+ * console.log(`${metadata.counts.labeledFrames} labeled frames`);
163
+ * console.log(`${metadata.counts.instances} instances`);
164
+ * ```
165
+ */
166
+ declare function loadSlpMetadata(source: JsfiveSource, options?: {
167
+ filename?: string;
168
+ }): Promise<SlpMetadata>;
169
+ /**
170
+ * Validate that a buffer contains a valid SLP file.
171
+ *
172
+ * Performs quick structural validation without fully parsing the file.
173
+ * Returns true if valid, throws an error with details if invalid.
174
+ *
175
+ * @param source - ArrayBuffer or Uint8Array containing the SLP file
176
+ * @returns true if the file is a valid SLP file
177
+ * @throws Error with details if the file is invalid
178
+ *
179
+ * @example
180
+ * ```typescript
181
+ * try {
182
+ * validateSlpBuffer(buffer);
183
+ * console.log('Valid SLP file');
184
+ * } catch (e) {
185
+ * console.error('Invalid:', e.message);
186
+ * }
187
+ * ```
188
+ */
189
+ declare function validateSlpBuffer(source: JsfiveSource): boolean;
190
+ /**
191
+ * Check if a buffer looks like an HDF5 file.
192
+ *
193
+ * Performs a quick magic number check without fully parsing.
194
+ * This is faster than validateSlpBuffer for initial filtering.
195
+ *
196
+ * @param source - ArrayBuffer or Uint8Array to check
197
+ * @returns true if the buffer starts with the HDF5 magic number
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * if (isHdf5Buffer(buffer)) {
202
+ * // Might be an SLP file, do full validation
203
+ * const metadata = await loadSlpMetadata(buffer);
204
+ * }
205
+ * ```
206
+ */
207
+ declare function isHdf5Buffer(source: JsfiveSource): boolean;
208
+
209
+ export { type CameraMetadata, type JsfiveSource, type SessionMetadata, Skeleton, type SlpMetadata, type SuggestionMetadata, Track, type VideoMetadata, isHdf5Buffer, loadSlpMetadata, validateSlpBuffer };
package/dist/lite.js ADDED
@@ -0,0 +1,178 @@
1
+ import {
2
+ Edge,
3
+ Node,
4
+ Skeleton,
5
+ Symmetry,
6
+ Track,
7
+ parseJsonAttr,
8
+ parseSessionsMetadata,
9
+ parseSkeletons,
10
+ parseSuggestions,
11
+ parseTracks,
12
+ parseVideosMetadata
13
+ } from "./chunk-CDU5QGU6.js";
14
+
15
+ // src/codecs/slp/jsfive.ts
16
+ import * as hdf5 from "jsfive";
17
+ function openJsfiveFile(source, filename) {
18
+ let buffer;
19
+ if (source instanceof Uint8Array) {
20
+ const slice = source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
21
+ buffer = slice;
22
+ } else {
23
+ buffer = source;
24
+ }
25
+ const file = new hdf5.File(buffer, filename ?? "data.slp");
26
+ return {
27
+ get: (path) => {
28
+ try {
29
+ const item = file.get(path);
30
+ if (!item) return null;
31
+ return item;
32
+ } catch {
33
+ return null;
34
+ }
35
+ },
36
+ keys: file.keys,
37
+ close: () => {
38
+ }
39
+ };
40
+ }
41
+ function isDataset(item) {
42
+ if (!item) return false;
43
+ return "value" in item || "shape" in item;
44
+ }
45
+ function getAttrs(item) {
46
+ if (!item) return {};
47
+ return item.attrs ?? {};
48
+ }
49
+ function getShape(item) {
50
+ if (!item || !isDataset(item)) return [];
51
+ return item.shape ?? [];
52
+ }
53
+ function getValue(item) {
54
+ if (!item || !isDataset(item)) return null;
55
+ try {
56
+ return item.value;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ // src/lite.ts
63
+ async function loadSlpMetadata(source, options) {
64
+ const file = openJsfiveFile(source, options?.filename);
65
+ try {
66
+ const requiredKeys = ["metadata", "frames", "instances", "points"];
67
+ for (const key of requiredKeys) {
68
+ if (!file.keys.includes(key)) {
69
+ throw new Error(`Invalid SLP file: missing /${key}`);
70
+ }
71
+ }
72
+ const metadataGroup = file.get("metadata");
73
+ if (!metadataGroup) {
74
+ throw new Error("Invalid SLP file: missing /metadata group");
75
+ }
76
+ const metadataAttrs = getAttrs(metadataGroup);
77
+ const formatId = Number(
78
+ metadataAttrs.format_id?.value ?? metadataAttrs.format_id ?? 1
79
+ );
80
+ const metadataJson = parseJsonAttr(metadataAttrs.json);
81
+ if (!metadataJson) {
82
+ throw new Error("Invalid SLP file: missing metadata.attrs.json");
83
+ }
84
+ const skeletons = parseSkeletons(metadataJson);
85
+ const tracksDataset = file.get("tracks_json");
86
+ const tracksValue = getValue(tracksDataset);
87
+ const tracks = Array.isArray(tracksValue) ? parseTracks(tracksValue) : [];
88
+ const videosDataset = file.get("videos_json");
89
+ const videosValue = getValue(videosDataset);
90
+ const labelsPath = options?.filename ?? "slp-data.slp";
91
+ let videos = Array.isArray(videosValue) ? parseVideosMetadata(videosValue, labelsPath) : [];
92
+ videos = videos.map((video) => {
93
+ if (!video.embedded || !video.dataset) return video;
94
+ const videoDs = file.get(video.dataset);
95
+ if (!videoDs || !isDataset(videoDs)) return video;
96
+ const attrs = getAttrs(videoDs);
97
+ const enriched = { ...video };
98
+ if (attrs.format !== void 0) enriched.format = String(attrs.format);
99
+ if (attrs.width !== void 0) enriched.width = Number(attrs.width);
100
+ if (attrs.height !== void 0) enriched.height = Number(attrs.height);
101
+ if (attrs.channels !== void 0) enriched.channels = Number(attrs.channels);
102
+ const shape = getShape(videoDs);
103
+ if (shape.length > 0) {
104
+ enriched.frameCount = shape[0];
105
+ }
106
+ return enriched;
107
+ });
108
+ const suggestionsDataset = file.get("suggestions_json");
109
+ const suggestionsValue = getValue(suggestionsDataset);
110
+ const suggestions = Array.isArray(suggestionsValue) ? parseSuggestions(suggestionsValue) : [];
111
+ const sessionsDataset = file.get("sessions_json");
112
+ const sessionsValue = getValue(sessionsDataset);
113
+ const sessions = Array.isArray(sessionsValue) ? parseSessionsMetadata(sessionsValue) : [];
114
+ const framesDs = file.get("frames");
115
+ const instancesDs = file.get("instances");
116
+ const pointsDs = file.get("points");
117
+ const predPointsDs = file.get("pred_points");
118
+ const counts = {
119
+ labeledFrames: getShape(framesDs)[0] ?? 0,
120
+ instances: getShape(instancesDs)[0] ?? 0,
121
+ points: getShape(pointsDs)[0] ?? 0,
122
+ predictedPoints: getShape(predPointsDs)[0] ?? 0
123
+ };
124
+ const hasEmbeddedImages = videos.some(
125
+ (v) => v.embedded && (v.format || v.width)
126
+ );
127
+ return {
128
+ version: metadataJson.version ?? "unknown",
129
+ formatId,
130
+ skeletons,
131
+ tracks,
132
+ videos,
133
+ suggestions,
134
+ sessions,
135
+ counts,
136
+ hasEmbeddedImages,
137
+ provenance: metadataJson.provenance
138
+ };
139
+ } finally {
140
+ file.close();
141
+ }
142
+ }
143
+ function validateSlpBuffer(source) {
144
+ const file = openJsfiveFile(source);
145
+ try {
146
+ const requiredKeys = ["metadata", "frames", "instances", "points"];
147
+ const missingKeys = requiredKeys.filter((k) => !file.keys.includes(k));
148
+ if (missingKeys.length > 0) {
149
+ throw new Error(`Invalid SLP file: missing ${missingKeys.join(", ")}`);
150
+ }
151
+ const metadata = file.get("metadata");
152
+ if (!metadata) {
153
+ throw new Error("Invalid SLP file: cannot read metadata group");
154
+ }
155
+ const attrs = getAttrs(metadata);
156
+ if (!attrs.json) {
157
+ throw new Error("Invalid SLP file: missing metadata.attrs.json");
158
+ }
159
+ return true;
160
+ } finally {
161
+ file.close();
162
+ }
163
+ }
164
+ function isHdf5Buffer(source) {
165
+ const bytes = source instanceof Uint8Array ? source : new Uint8Array(source);
166
+ if (bytes.length < 8) return false;
167
+ return bytes[0] === 137 && bytes[1] === 72 && bytes[2] === 68 && bytes[3] === 70 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10;
168
+ }
169
+ export {
170
+ Edge,
171
+ Node,
172
+ Skeleton,
173
+ Symmetry,
174
+ Track,
175
+ isHdf5Buffer,
176
+ loadSlpMetadata,
177
+ validateSlpBuffer
178
+ };
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@talmolab/sleap-io.js",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
8
  "types": "./dist/index.d.ts",
9
9
  "import": "./dist/index.js"
10
+ },
11
+ "./lite": {
12
+ "types": "./dist/lite.d.ts",
13
+ "import": "./dist/lite.js"
10
14
  }
11
15
  },
12
16
  "main": "./dist/index.js",
@@ -17,14 +21,16 @@
17
21
  "url": "https://github.com/talmolab/sleap-io.js.git"
18
22
  },
19
23
  "scripts": {
20
- "build": "tsup src/index.ts --format esm --dts",
24
+ "build": "tsup src/index.ts src/lite.ts --format esm --dts",
21
25
  "test": "vitest",
22
26
  "test:coverage": "vitest --run --coverage",
23
27
  "lint": "tsc -p tsconfig.json --noEmit"
24
28
  },
25
29
  "dependencies": {
26
30
  "h5wasm": "^0.8.8",
31
+ "jsfive": "^0.3.10",
27
32
  "mp4box": "^0.5.4",
33
+ "skia-canvas": "^3.0.8",
28
34
  "yaml": "^2.6.1"
29
35
  },
30
36
  "devDependencies": {
@@ -33,5 +39,8 @@
33
39
  "tsup": "^8.3.5",
34
40
  "typescript": "^5.6.3",
35
41
  "vitest": "^2.1.8"
36
- }
42
+ },
43
+ "trustedDependencies": [
44
+ "skia-canvas"
45
+ ]
37
46
  }