@smartspectra/node-sdk 3.2.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +13 -0
- package/README.md +409 -0
- package/js/constants.js +125 -0
- package/js/ffi.js +494 -0
- package/js/index.d.ts +282 -0
- package/js/index.js +60 -0
- package/js/main/index.d.ts +32 -0
- package/js/main/index.js +404 -0
- package/js/messages/generated.d.ts +2083 -0
- package/js/messages/generated.js +5810 -0
- package/js/messages/index.d.ts +27 -0
- package/js/messages/index.js +67 -0
- package/js/preload/index.js +116 -0
- package/js/renderer/index.d.ts +154 -0
- package/js/renderer/index.js +670 -0
- package/js/resolve-native.js +113 -0
- package/js/smartspectra.js +293 -0
- package/package.json +81 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// js/messages/index.d.ts
|
|
2
|
+
// Copyright (C) 2026 Presage Technologies, Inc.
|
|
3
|
+
//
|
|
4
|
+
// SPDX-License-Identifier: LicenseRef-Proprietary
|
|
5
|
+
//
|
|
6
|
+
// TypeScript declarations for `@smartspectra/node-sdk/messages`. The
|
|
7
|
+
// generated namespace tree (with every nested type — Breathing, Cardio,
|
|
8
|
+
// Face, Measurement, etc.) lives in `./generated.d.ts`, emitted by pbts
|
|
9
|
+
// alongside `generated.js`. This file is the hand-written facade that
|
|
10
|
+
// names the public exports.
|
|
11
|
+
|
|
12
|
+
import { presage as generatedPresage } from './generated';
|
|
13
|
+
|
|
14
|
+
/** The full `presage.*` namespace as generated by pbjs / pbts. */
|
|
15
|
+
export const presage: typeof generatedPresage;
|
|
16
|
+
|
|
17
|
+
/** Convenience alias for `presage.smartspectra.Metrics`. */
|
|
18
|
+
export const Metrics: typeof generatedPresage.smartspectra.Metrics;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Decode a Metrics protobuf buffer emitted by the SDK's `metrics` or
|
|
22
|
+
* `accumulatedMetrics` events. Accepts Node's `Buffer`, browser-side
|
|
23
|
+
* `Uint8Array`, or raw `ArrayBuffer`.
|
|
24
|
+
*/
|
|
25
|
+
export function decodeMetrics(
|
|
26
|
+
buf: Uint8Array | ArrayBuffer,
|
|
27
|
+
): generatedPresage.smartspectra.Metrics;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// js/messages/index.js
|
|
2
|
+
// Copyright (C) 2026 Presage Technologies, Inc.
|
|
3
|
+
//
|
|
4
|
+
// SPDX-License-Identifier: LicenseRef-Proprietary
|
|
5
|
+
//
|
|
6
|
+
// Public protobuf surface for `@smartspectra/node-sdk`. Ships the
|
|
7
|
+
// generated message classes so host apps don't have to compile their
|
|
8
|
+
// own protobuf module against the SDK's `.proto` files.
|
|
9
|
+
//
|
|
10
|
+
// Usage:
|
|
11
|
+
// const { Metrics, decodeMetrics, presage } = require('@smartspectra/node-sdk/messages');
|
|
12
|
+
// const m = decodeMetrics(buf); // buf: Buffer | Uint8Array | ArrayBuffer
|
|
13
|
+
// console.log(m.cardio?.pulseRate);
|
|
14
|
+
// // Nested types are reachable via the namespace too:
|
|
15
|
+
// const breathing = presage.smartspectra.Breathing;
|
|
16
|
+
//
|
|
17
|
+
// This module purposely avoids `require('../smartspectra')` /
|
|
18
|
+
// `require('../ffi')` so it stays pulled-in cleanly by browser-side
|
|
19
|
+
// bundlers (e.g. esbuild for an Electron renderer). The koffi loader and
|
|
20
|
+
// the C ABI shim live under the package's main entry only.
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const $root = require('./generated');
|
|
25
|
+
|
|
26
|
+
// pbjs --wrap commonjs emits `module.exports = $root`, where `$root` is
|
|
27
|
+
// the populated `protobuf.roots["default"]` namespace. Pull the `presage`
|
|
28
|
+
// branch off it so we can re-export the full tree without forcing callers
|
|
29
|
+
// to traverse `$root.presage.smartspectra…`.
|
|
30
|
+
const { presage } = $root;
|
|
31
|
+
const Metrics = presage.smartspectra.Metrics;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Coerce a buffer-shaped value into the `Uint8Array` that
|
|
35
|
+
* `Metrics.decode()` expects. Accepts the three shapes consumers
|
|
36
|
+
* realistically hand us: a Node.js `Buffer` (main process), a plain
|
|
37
|
+
* `Uint8Array` (renderer, from the IPC bridge's `ArrayBuffer` payload),
|
|
38
|
+
* or a bare `ArrayBuffer` (raw transferable, no view).
|
|
39
|
+
*
|
|
40
|
+
* Buffers are already Uint8Array subclasses so they pass through
|
|
41
|
+
* untouched; the Uint8Array branch also no-ops; the ArrayBuffer branch
|
|
42
|
+
* wraps without copying.
|
|
43
|
+
*/
|
|
44
|
+
function toUint8(buf) {
|
|
45
|
+
if (buf instanceof Uint8Array) return buf;
|
|
46
|
+
if (buf instanceof ArrayBuffer) return new Uint8Array(buf);
|
|
47
|
+
throw new TypeError(
|
|
48
|
+
'@smartspectra/node-sdk/messages: decodeMetrics expected ' +
|
|
49
|
+
'Buffer | Uint8Array | ArrayBuffer, got ' + Object.prototype.toString.call(buf));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Decode a Metrics protobuf buffer emitted by the SDK's `metrics` or
|
|
54
|
+
* `accumulatedMetrics` events. Returns the decoded message object with
|
|
55
|
+
* the full namespace shape (e.g. `out.cardio.pulseRate[i].timestamp`).
|
|
56
|
+
*
|
|
57
|
+
* Throws on malformed input (re-raised from protobufjs).
|
|
58
|
+
*/
|
|
59
|
+
function decodeMetrics(buf) {
|
|
60
|
+
return Metrics.decode(toUint8(buf));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
Metrics,
|
|
65
|
+
decodeMetrics,
|
|
66
|
+
presage,
|
|
67
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// js/preload/index.js
|
|
2
|
+
// Copyright (C) 2026 Presage Technologies, Inc.
|
|
3
|
+
//
|
|
4
|
+
// SPDX-License-Identifier: LicenseRef-Proprietary
|
|
5
|
+
//
|
|
6
|
+
// Preload bridge for `@smartspectra/node-sdk/renderer`. Run inside the
|
|
7
|
+
// preload script of any BrowserWindow that hosts a renderer using the SDK.
|
|
8
|
+
//
|
|
9
|
+
// Apps that already have a preload script can require this file from there;
|
|
10
|
+
// apps without a preload script can point `webPreferences.preload` directly
|
|
11
|
+
// at this file via `require.resolve('@smartspectra/node-sdk/preload')`.
|
|
12
|
+
//
|
|
13
|
+
// The bridge owns the MessagePort in the preload context and exposes
|
|
14
|
+
// `attach()` / `postMessage()` / `onMessage()` / `close()` to the renderer.
|
|
15
|
+
// Keeping the port out of the renderer's main world avoids edge cases in
|
|
16
|
+
// cross-context MessagePort proxying through contextBridge.
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
21
|
+
|
|
22
|
+
const ATTACH_CHANNEL = 'smartspectra:attach';
|
|
23
|
+
|
|
24
|
+
// Preload-context state. One port per preload instance is enough — the
|
|
25
|
+
// renderer-side SmartSpectraSDK is a singleton-per-BrowserWindow in
|
|
26
|
+
// practice.
|
|
27
|
+
let port = null;
|
|
28
|
+
let messageListener = null;
|
|
29
|
+
|
|
30
|
+
// Diagnostic logging — opt-in via the same env var the main-process glue
|
|
31
|
+
// uses. Logs go to the renderer DevTools console (preload shares the
|
|
32
|
+
// renderer's console). Frame messages are logged only on first send and
|
|
33
|
+
// every 60 sends after that, to avoid swamping the console.
|
|
34
|
+
const DIAGNOSTICS = process.env.SMARTSPECTRA_DIAGNOSTICS === '1';
|
|
35
|
+
let postsByKind = Object.create(null);
|
|
36
|
+
function diagLogSend(kind) {
|
|
37
|
+
if (!DIAGNOSTICS) return;
|
|
38
|
+
postsByKind[kind] = (postsByKind[kind] || 0) + 1;
|
|
39
|
+
const count = postsByKind[kind];
|
|
40
|
+
if (kind === 'frame') {
|
|
41
|
+
if (count === 1 || count % 60 === 0) {
|
|
42
|
+
console.log(`[smartspectra/preload] -> frame #${count}`);
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
console.log(`[smartspectra/preload] -> ${kind}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const onPortMessage = (event) => {
|
|
50
|
+
if (DIAGNOSTICS) {
|
|
51
|
+
const kind = event.data && event.data.kind;
|
|
52
|
+
console.log(`[smartspectra/preload] <- ${kind || '?'}`);
|
|
53
|
+
}
|
|
54
|
+
if (messageListener) messageListener(event.data);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
contextBridge.exposeInMainWorld('__smartspectraBridge', {
|
|
58
|
+
/**
|
|
59
|
+
* Mirror of SMARTSPECTRA_DIAGNOSTICS for the renderer module, which is
|
|
60
|
+
* context-isolated and cannot read process.env itself.
|
|
61
|
+
*/
|
|
62
|
+
diagnostics: DIAGNOSTICS,
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Open the IPC channel to the main process. Creates a MessageChannel
|
|
66
|
+
* (port pair) in the preload context, ships port2 to main via
|
|
67
|
+
* ipcRenderer.postMessage (which natively transfers MessagePorts), and
|
|
68
|
+
* retains port1 here.
|
|
69
|
+
*/
|
|
70
|
+
attach() {
|
|
71
|
+
if (port) return; // idempotent — second attach() is a no-op
|
|
72
|
+
const channel = new MessageChannel();
|
|
73
|
+
port = channel.port1;
|
|
74
|
+
port.onmessage = onPortMessage;
|
|
75
|
+
ipcRenderer.postMessage(ATTACH_CHANNEL, null, [channel.port2]);
|
|
76
|
+
port.start();
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Forward a message to the main process via structured clone. Electron's
|
|
81
|
+
* MessagePortMain silently drops messages whose transfer list contains an
|
|
82
|
+
* ArrayBuffer — `port.postMessage(msg, [ab])` succeeds on the renderer
|
|
83
|
+
* side but never fires `port.on('message')` on the main side — so we
|
|
84
|
+
* always clone. The clone costs one ~3.6 MB copy per 720p frame, which is
|
|
85
|
+
* negligible at 30 fps.
|
|
86
|
+
*/
|
|
87
|
+
postMessage(message) {
|
|
88
|
+
if (!port) throw new Error('@smartspectra/node-sdk/preload: postMessage before attach()');
|
|
89
|
+
try {
|
|
90
|
+
port.postMessage(message);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// Surface the failure to DevTools so it doesn't disappear into
|
|
93
|
+
// contextBridge's promise wrapper.
|
|
94
|
+
console.error(
|
|
95
|
+
`[smartspectra/preload] port.postMessage failed for ${message && message.kind}:`,
|
|
96
|
+
err);
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
diagLogSend(message && message.kind);
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Register the callback that receives main-process messages. Replacing
|
|
104
|
+
* an existing callback is supported (only the most recent registration
|
|
105
|
+
* is invoked).
|
|
106
|
+
*/
|
|
107
|
+
onMessage(callback) {
|
|
108
|
+
messageListener = typeof callback === 'function' ? callback : null;
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
/** Close the port and clear the listener. Idempotent. */
|
|
112
|
+
close() {
|
|
113
|
+
if (port) { try { port.close(); } catch { /* already closed */ } port = null; }
|
|
114
|
+
messageListener = null;
|
|
115
|
+
},
|
|
116
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// js/renderer/index.d.ts
|
|
2
|
+
// Copyright (C) 2026 Presage Technologies, Inc.
|
|
3
|
+
//
|
|
4
|
+
// SPDX-License-Identifier: LicenseRef-Proprietary
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ProcessingStatusValue,
|
|
8
|
+
ValidationCodeValue,
|
|
9
|
+
SmartSpectraErrorCodeValue,
|
|
10
|
+
SmartSpectraOptions,
|
|
11
|
+
} from '../../js/index';
|
|
12
|
+
|
|
13
|
+
// Re-export the public value constants and matching type aliases so the
|
|
14
|
+
// renderer-side consumer can import everything from one entry point.
|
|
15
|
+
export {
|
|
16
|
+
ProcessingStatus,
|
|
17
|
+
PixelFormat,
|
|
18
|
+
ValidationCode,
|
|
19
|
+
SmartSpectraErrorCode,
|
|
20
|
+
FrameTransform,
|
|
21
|
+
breathingMetrics,
|
|
22
|
+
cardioMetrics,
|
|
23
|
+
faceMetrics,
|
|
24
|
+
micromotionMetrics,
|
|
25
|
+
edaMetrics,
|
|
26
|
+
defaultSupportedMetrics,
|
|
27
|
+
} from '../../js/index';
|
|
28
|
+
export type {
|
|
29
|
+
ProcessingStatusValue,
|
|
30
|
+
PixelFormatValue,
|
|
31
|
+
ValidationCodeValue,
|
|
32
|
+
SmartSpectraErrorCodeValue,
|
|
33
|
+
FrameTransformValue,
|
|
34
|
+
SmartSpectraOptions,
|
|
35
|
+
} from '../../js/index';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Renderer constructor options: the shared SDK options plus a renderer-only
|
|
39
|
+
* IPC ack timeout. (`sendTimeoutMs` has no meaning for the main-process SDK,
|
|
40
|
+
* which doesn't go through the preload bridge.)
|
|
41
|
+
*/
|
|
42
|
+
export interface RendererSmartSpectraOptions extends SmartSpectraOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Per-call timeout (ms) for renderer→main IPC roundtrips
|
|
45
|
+
* (start/stop/reset/requestInsight/configure). If the main process
|
|
46
|
+
* crashes or stalls, the pending call rejects with an `Error` whose
|
|
47
|
+
* `code` is `'SMARTSPECTRA_IPC_TIMEOUT'` instead of hanging the UI
|
|
48
|
+
* forever. Defaults to 30000; set to 0 to disable.
|
|
49
|
+
*/
|
|
50
|
+
sendTimeoutMs?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Renderer-side `SmartSpectraSDK` for Electron applications. By default
|
|
55
|
+
* the SDK acquires the host's front-facing camera at 1280x720 / 30 fps,
|
|
56
|
+
* locks AE/AWB/focus once the signal pipeline reports Running, and emits
|
|
57
|
+
* the active `MediaStream` through `'streamAvailable'` so the host can
|
|
58
|
+
* show a live preview. Host apps that need finer control (custom
|
|
59
|
+
* resolution, virtual cameras, `<canvas>.captureStream()`, etc.) call
|
|
60
|
+
* `useMediaStream()` before `start()` to override the default.
|
|
61
|
+
*
|
|
62
|
+
* Requires the preload script to expose the bridge — set
|
|
63
|
+
* `webPreferences.preload = require.resolve('@smartspectra/node-sdk/preload')`
|
|
64
|
+
* on the BrowserWindow, and call
|
|
65
|
+
* `bindSmartSpectraIpc(window)` from the main process.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* import { SmartSpectraSDK } from '@smartspectra/node-sdk/renderer';
|
|
70
|
+
* import { breathingMetrics, cardioMetrics } from '@smartspectra/node-sdk';
|
|
71
|
+
*
|
|
72
|
+
* const sdk = new SmartSpectraSDK({
|
|
73
|
+
* apiKey: 'YOUR_KEY',
|
|
74
|
+
* requestedMetrics: [...breathingMetrics, ...cardioMetrics],
|
|
75
|
+
* });
|
|
76
|
+
*
|
|
77
|
+
* sdk.on('streamAvailable', (stream) => {
|
|
78
|
+
* document.querySelector('video').srcObject = stream;
|
|
79
|
+
* });
|
|
80
|
+
* sdk.on('metrics', (buf, ts) => { ... });
|
|
81
|
+
*
|
|
82
|
+
* await sdk.start(); // SDK acquires the camera + emits 'streamAvailable'
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export declare class SmartSpectraSDK {
|
|
86
|
+
/** SDK package version. */
|
|
87
|
+
static readonly version: string;
|
|
88
|
+
|
|
89
|
+
constructor(options?: RendererSmartSpectraOptions);
|
|
90
|
+
|
|
91
|
+
/** Current ProcessingStatus integer value (read-only). */
|
|
92
|
+
readonly processingStatus: ProcessingStatusValue;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Override the default camera with a host-supplied `MediaStream` from
|
|
96
|
+
* `navigator.mediaDevices.getUserMedia`, `desktopCapturer`, a
|
|
97
|
+
* `<canvas>.captureStream()`, etc. Optional — if not called, `start()`
|
|
98
|
+
* acquires its own MediaStream with sensible defaults and emits it via
|
|
99
|
+
* the `'streamAvailable'` event. When the host supplies the stream
|
|
100
|
+
* the host also owns its lifecycle; the SDK never stops the original
|
|
101
|
+
* tracks.
|
|
102
|
+
*/
|
|
103
|
+
useMediaStream(stream: MediaStream): this;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Begin processing. If no stream was provided via `useMediaStream()`,
|
|
107
|
+
* the SDK acquires the default front-facing camera (1280x720 / 30 fps)
|
|
108
|
+
* and emits it through `'streamAvailable'` so the host can attach a
|
|
109
|
+
* preview. Resolves once the main-process SDK is running.
|
|
110
|
+
*/
|
|
111
|
+
start(): Promise<void>;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Stop processing. Resolves once the main-process SDK reports idle.
|
|
115
|
+
* Releases any SDK-acquired camera; host-supplied streams are left
|
|
116
|
+
* untouched.
|
|
117
|
+
*/
|
|
118
|
+
stop(): Promise<void>;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Rebuild the processing pipeline after a kError state. Releases any
|
|
122
|
+
* SDK-acquired camera so the next `start()` re-acquires from scratch.
|
|
123
|
+
*/
|
|
124
|
+
reset(): Promise<void>;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Dispatch an on-demand insight prompt. Resolves with the request id
|
|
128
|
+
* assigned to it; the matching response arrives via the `'insight'`
|
|
129
|
+
* event with the same id.
|
|
130
|
+
*/
|
|
131
|
+
requestInsight(text: string): Promise<number>;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Tear down renderer-side resources and instruct the main process to
|
|
135
|
+
* destroy its SDK instance. Idempotent.
|
|
136
|
+
*/
|
|
137
|
+
destroy(): void;
|
|
138
|
+
|
|
139
|
+
on(event: 'processingStatus', callback: (status: ProcessingStatusValue) => void): this;
|
|
140
|
+
on(event: 'validationStatus', callback: (code: ValidationCodeValue, timestampUs: number, hint: string) => void): this;
|
|
141
|
+
on(event: 'metrics', callback: (buf: Uint8Array, timestampUs: number) => void): this;
|
|
142
|
+
on(event: 'accumulatedMetrics', callback: (buf: Uint8Array, timestampUs: number) => void): this;
|
|
143
|
+
on(event: 'insight', callback: (buf: Uint8Array, requestId: number) => void): this;
|
|
144
|
+
on(event: 'frameSentThrough', callback: (sent: boolean, timestampUs: number) => void): this;
|
|
145
|
+
on(event: 'error', callback: (code: SmartSpectraErrorCodeValue, message: string, retryable: boolean) => void): this;
|
|
146
|
+
/**
|
|
147
|
+
* Fires once the SDK has acquired (or re-acquired, after a Stop) the
|
|
148
|
+
* default `MediaStream`. Host apps attach this stream to their
|
|
149
|
+
* preview `<video>` element. Not fired when the host supplies its
|
|
150
|
+
* own stream via `useMediaStream()` — the host already owns that
|
|
151
|
+
* reference.
|
|
152
|
+
*/
|
|
153
|
+
on(event: 'streamAvailable', callback: (stream: MediaStream) => void): this;
|
|
154
|
+
}
|