@pose-tracker/react-native-pose-estimation-light 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.
Files changed (125) hide show
  1. package/LICENSE +45 -0
  2. package/PoseTrackerVision.podspec +28 -0
  3. package/README.md +161 -0
  4. package/THIRD_PARTY_NOTICES.md +19 -0
  5. package/ios/PoseTrackerVision/PoseTrackerBodyPosePlugin.m +18 -0
  6. package/ios/PoseTrackerVision/PoseTrackerBodyPosePlugin.swift +108 -0
  7. package/lib/PoseTrackerProvider.d.ts +102 -0
  8. package/lib/PoseTrackerProvider.js +204 -0
  9. package/lib/api/configure.d.ts +29 -0
  10. package/lib/api/configure.js +64 -0
  11. package/lib/api/skeleton.d.ts +17 -0
  12. package/lib/api/skeleton.js +53 -0
  13. package/lib/api/track.d.ts +61 -0
  14. package/lib/api/track.js +146 -0
  15. package/lib/backends/PoseBackend.d.ts +50 -0
  16. package/lib/backends/PoseBackend.js +11 -0
  17. package/lib/backends/vision/VisionPoseBackend.d.ts +72 -0
  18. package/lib/backends/vision/VisionPoseBackend.js +177 -0
  19. package/lib/backends/vision/mapVisionJoints.d.ts +35 -0
  20. package/lib/backends/vision/mapVisionJoints.js +162 -0
  21. package/lib/backends/vision/optionalVision.d.ts +32 -0
  22. package/lib/backends/vision/optionalVision.js +76 -0
  23. package/lib/backends/webview/WebViewPoseBackend.d.ts +223 -0
  24. package/lib/backends/webview/WebViewPoseBackend.js +382 -0
  25. package/lib/backends/webview/brandAssets.d.ts +5 -0
  26. package/lib/backends/webview/brandAssets.js +8 -0
  27. package/lib/backends/webview/onlineRuntime.d.ts +45 -0
  28. package/lib/backends/webview/onlineRuntime.js +57 -0
  29. package/lib/backends/webview/poseHtml.d.ts +71 -0
  30. package/lib/backends/webview/poseHtml.js +188 -0
  31. package/lib/backends/webview/poseRuntimeSource.d.ts +3 -0
  32. package/lib/backends/webview/poseRuntimeSource.js +4 -0
  33. package/lib/cache/obfuscate.d.ts +18 -0
  34. package/lib/cache/obfuscate.js +90 -0
  35. package/lib/camera/PoseCameraView.d.ts +51 -0
  36. package/lib/camera/PoseCameraView.js +99 -0
  37. package/lib/camera/WebViewPoseView.d.ts +81 -0
  38. package/lib/camera/WebViewPoseView.js +312 -0
  39. package/lib/client.d.ts +361 -0
  40. package/lib/client.js +1046 -0
  41. package/lib/diagnostics/logReport.d.ts +23 -0
  42. package/lib/diagnostics/logReport.js +102 -0
  43. package/lib/engine/EngineLoader.d.ts +95 -0
  44. package/lib/engine/EngineLoader.js +347 -0
  45. package/lib/engine/types.d.ts +83 -0
  46. package/lib/engine/types.js +11 -0
  47. package/lib/events/classicMessage.d.ts +31 -0
  48. package/lib/events/classicMessage.js +259 -0
  49. package/lib/exercises/aliases.d.ts +18 -0
  50. package/lib/exercises/aliases.js +48 -0
  51. package/lib/index.d.ts +72 -0
  52. package/lib/index.js +170 -0
  53. package/lib/models/poseModels.d.ts +39 -0
  54. package/lib/models/poseModels.js +63 -0
  55. package/lib/quality/AdaptiveQualityController.d.ts +120 -0
  56. package/lib/quality/AdaptiveQualityController.js +423 -0
  57. package/lib/quality/RuntimeGuard.d.ts +34 -0
  58. package/lib/quality/RuntimeGuard.js +105 -0
  59. package/lib/quality/captureMode.d.ts +82 -0
  60. package/lib/quality/captureMode.js +75 -0
  61. package/lib/quality/deviceCapability.d.ts +37 -0
  62. package/lib/quality/deviceCapability.js +177 -0
  63. package/lib/quality/profiles.d.ts +125 -0
  64. package/lib/quality/profiles.js +202 -0
  65. package/lib/runtime/RuntimeCache.d.ts +75 -0
  66. package/lib/runtime/RuntimeCache.js +230 -0
  67. package/lib/sdkVersion.d.ts +2 -0
  68. package/lib/sdkVersion.js +5 -0
  69. package/lib/support/optionalModules.d.ts +46 -0
  70. package/lib/support/optionalModules.js +82 -0
  71. package/lib/types/acceleration.d.ts +86 -0
  72. package/lib/types/acceleration.js +15 -0
  73. package/lib/types/events.d.ts +345 -0
  74. package/lib/types/events.js +10 -0
  75. package/lib/types/features.d.ts +90 -0
  76. package/lib/types/features.js +122 -0
  77. package/lib/types/manifest.d.ts +197 -0
  78. package/lib/types/manifest.js +10 -0
  79. package/lib/types/pose.d.ts +36 -0
  80. package/lib/types/pose.js +41 -0
  81. package/lib/types/preload.d.ts +17 -0
  82. package/lib/types/preload.js +10 -0
  83. package/lib/types/skeleton.d.ts +29 -0
  84. package/lib/types/skeleton.js +52 -0
  85. package/package.json +92 -0
  86. package/react-native.config.js +12 -0
  87. package/src/PoseTrackerProvider.tsx +332 -0
  88. package/src/api/configure.ts +82 -0
  89. package/src/api/skeleton.ts +60 -0
  90. package/src/api/track.ts +182 -0
  91. package/src/backends/PoseBackend.ts +59 -0
  92. package/src/backends/vision/VisionPoseBackend.ts +238 -0
  93. package/src/backends/vision/mapVisionJoints.ts +185 -0
  94. package/src/backends/vision/optionalVision.ts +94 -0
  95. package/src/backends/webview/WebViewPoseBackend.ts +582 -0
  96. package/src/backends/webview/brandAssets.ts +5 -0
  97. package/src/backends/webview/onlineRuntime.ts +94 -0
  98. package/src/backends/webview/poseHtml.ts +259 -0
  99. package/src/backends/webview/poseRuntimeSource.d.ts +3 -0
  100. package/src/cache/obfuscate.ts +94 -0
  101. package/src/camera/PoseCameraView.tsx +223 -0
  102. package/src/camera/WebViewPoseView.tsx +466 -0
  103. package/src/client.ts +1295 -0
  104. package/src/diagnostics/logReport.ts +131 -0
  105. package/src/engine/EngineLoader.ts +428 -0
  106. package/src/engine/types.ts +93 -0
  107. package/src/events/classicMessage.ts +271 -0
  108. package/src/exercises/aliases.ts +50 -0
  109. package/src/index.ts +243 -0
  110. package/src/models/poseModels.ts +93 -0
  111. package/src/quality/AdaptiveQualityController.ts +579 -0
  112. package/src/quality/RuntimeGuard.ts +119 -0
  113. package/src/quality/captureMode.ts +97 -0
  114. package/src/quality/deviceCapability.ts +187 -0
  115. package/src/quality/profiles.ts +253 -0
  116. package/src/runtime/RuntimeCache.ts +300 -0
  117. package/src/sdkVersion.ts +2 -0
  118. package/src/support/optionalModules.ts +114 -0
  119. package/src/types/acceleration.ts +94 -0
  120. package/src/types/events.ts +437 -0
  121. package/src/types/features.ts +176 -0
  122. package/src/types/manifest.ts +230 -0
  123. package/src/types/pose.ts +85 -0
  124. package/src/types/preload.ts +19 -0
  125. package/src/types/skeleton.ts +77 -0
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Human-readable diagnostic dumps for Metro / adb logcat.
3
+ * Designed so a host can paste the terminal output into a support chat.
4
+ */
5
+ import type { AccelerationDiagnostics } from '../types/acceleration';
6
+ /** Always-on console sink — shows up in `npx expo start` / Metro. */
7
+ export declare function defaultDiagnosticLogger(message: string): void;
8
+ export declare function logPlatformBanner(): void;
9
+ /**
10
+ * Multi-line dump of the warm-up / acceleration verdict. Call after
11
+ * preload()/warmup() so Android CPU-fallback reasons are visible in Metro.
12
+ */
13
+ export declare function logAccelerationReport(diag: AccelerationDiagnostics | null, extra?: Record<string, unknown>): void;
14
+ /** Throttled FPS / latency line for the camera loop (Metro-friendly). */
15
+ export declare function logFrameStats(stats: {
16
+ fps: number;
17
+ medianLatencyMs: number | null;
18
+ backend: string;
19
+ acceleration: string;
20
+ frames: number;
21
+ keypointsAbove03: number;
22
+ meanScore: number;
23
+ }): void;
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ /**
3
+ * Human-readable diagnostic dumps for Metro / adb logcat.
4
+ * Designed so a host can paste the terminal output into a support chat.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.defaultDiagnosticLogger = defaultDiagnosticLogger;
8
+ exports.logPlatformBanner = logPlatformBanner;
9
+ exports.logAccelerationReport = logAccelerationReport;
10
+ exports.logFrameStats = logFrameStats;
11
+ const react_native_1 = require("react-native");
12
+ const PREFIX = '[PoseTracker]';
13
+ /** Always-on console sink — shows up in `npx expo start` / Metro. */
14
+ function defaultDiagnosticLogger(message) {
15
+ // Keep a single prefix so hosts can filter: `npx expo start | grep PoseTracker`
16
+ console.log(PREFIX, message.startsWith('[posetracker]') ? message : message);
17
+ }
18
+ function logPlatformBanner() {
19
+ console.log(PREFIX, `platform=${react_native_1.Platform.OS} version=${String(react_native_1.Platform.Version)} ` +
20
+ `constants=${JSON.stringify({
21
+ isPad: react_native_1.Platform.isPad ?? null,
22
+ isTV: react_native_1.Platform.isTV,
23
+ })}`);
24
+ }
25
+ /**
26
+ * Multi-line dump of the warm-up / acceleration verdict. Call after
27
+ * preload()/warmup() so Android CPU-fallback reasons are visible in Metro.
28
+ */
29
+ function logAccelerationReport(diag, extra) {
30
+ const lines = [];
31
+ lines.push('======== ACCELERATION REPORT ========');
32
+ lines.push(`platform: ${react_native_1.Platform.OS} ${String(react_native_1.Platform.Version)}`);
33
+ if (!diag) {
34
+ lines.push('diagnostics: null (warm-up not finished)');
35
+ }
36
+ else {
37
+ lines.push(`state: ${diag.state}`);
38
+ lines.push(`runtime: ${diag.runtime ?? 'webview'}`);
39
+ lines.push(`tfjsBackend: ${diag.tfjsBackend ?? 'n/a'}`);
40
+ if (diag.delegate) {
41
+ lines.push(`delegate: ${diag.delegate}`);
42
+ }
43
+ lines.push(`medianInferenceMs: ${diag.medianInferenceMs != null ? diag.medianInferenceMs.toFixed(1) : 'n/a'}`);
44
+ lines.push(`warmUpRunsMs: [${diag.inferenceTimesMs.map((ms) => Math.round(ms)).join(', ')}]`);
45
+ lines.push(`maxAcceptableInferenceMs: ${diag.maxAcceptableInferenceMs}`);
46
+ lines.push(`contextLossCount: ${diag.contextLossCount}`);
47
+ const caps = diag.capabilities;
48
+ if (caps) {
49
+ lines.push(`GL.renderer: ${caps.renderer ?? 'n/a'}`);
50
+ lines.push(`GL.vendor: ${caps.vendor ?? 'n/a'}`);
51
+ lines.push(`GL.version: ${caps.glVersion ?? 'n/a'}`);
52
+ lines.push(`GL.maxTextureSize: ${caps.maxTextureSize ?? 'n/a'}`);
53
+ lines.push(`GL.extensions(shimmed=${caps.extensionQueriesShimmed}): ` +
54
+ `float=${caps.textureFloat} halfFloat=${caps.textureHalfFloat} ` +
55
+ `colorBufferFloat=${caps.colorBufferFloat} colorBufferHalfFloat=${caps.colorBufferHalfFloat}`);
56
+ }
57
+ else {
58
+ lines.push('GL.capabilities: null (no GL context — typically means tfjs backend=cpu)');
59
+ }
60
+ lines.push(`tfjsFlags: ${JSON.stringify(diag.flags)}`);
61
+ if (diag.reasons.length === 0) {
62
+ lines.push('reasons: (none)');
63
+ }
64
+ else {
65
+ lines.push('reasons:');
66
+ for (const reason of diag.reasons) {
67
+ lines.push(` - ${reason}`);
68
+ }
69
+ }
70
+ if (diag.state === 'gpu' && diag.runtime === 'vision') {
71
+ lines.push('INTERPRETATION: Apple Vision (VNDetectHumanBodyPoseRequest) — Neural Engine / GPU. Up to 19 joints.');
72
+ }
73
+ else if (diag.runtime === 'webview' || diag.runtime === undefined) {
74
+ lines.push('INTERPRETATION: offline WebView MoveNet Lightning (ANGLE WebGL, bundled model) — ' +
75
+ 'same stack as the PoseTracker WebView product. Expected real-time on both platforms.');
76
+ }
77
+ else if (diag.state === 'cpu-fallback') {
78
+ lines.push('INTERPRETATION: non-accelerated path (cpu-fallback). Expect low FPS.');
79
+ }
80
+ else if (diag.state === 'gpu') {
81
+ lines.push('INTERPRETATION: GPU path accepted by health check.');
82
+ }
83
+ else if (diag.state === 'unavailable') {
84
+ lines.push('INTERPRETATION: no usable backend — model could not run.');
85
+ }
86
+ }
87
+ if (extra) {
88
+ lines.push(`extra: ${JSON.stringify(extra)}`);
89
+ }
90
+ lines.push('======== END ACCELERATION REPORT ========');
91
+ for (const line of lines) {
92
+ console.log(PREFIX, line);
93
+ }
94
+ }
95
+ /** Throttled FPS / latency line for the camera loop (Metro-friendly). */
96
+ function logFrameStats(stats) {
97
+ console.log(PREFIX, `FRAMESTATS fps=${stats.fps} ` +
98
+ `medianMs=${stats.medianLatencyMs != null ? Math.round(stats.medianLatencyMs) : 'n/a'} ` +
99
+ `backend=${stats.backend} acceleration=${stats.acceleration} ` +
100
+ `frames=${stats.frames} kp≥0.3=${stats.keypointsAbove03}/17 ` +
101
+ `meanScore=${stats.meanScore.toFixed(2)}`);
102
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Remote engine distribution (Sency-style model distribution, applied to the
3
+ * business-logic bundle):
4
+ *
5
+ * 1. The handshake manifest carries a signed, token-gated URL of a versioned
6
+ * JS engine bundle plus its SHA-256 digest.
7
+ * 2. The bundle is cached on the local filesystem, sealed with a secret
8
+ * derived from the API token (see `cache/obfuscate.ts`): a device without
9
+ * the key cannot read the cached business logic. Integrity is re-validated
10
+ * against the manifest digest on every load (corrupted/partial downloads
11
+ * are purged, like Sency's `TFL3` magic check on cached TFLite files).
12
+ * 3. A crash-loop guard (analog of Sency's `TfliteRuntimeGuard`
13
+ * PROBING→PASSED/FAILED state machine) marks the bundle as "probing"
14
+ * before evaluation; if the app died while probing, the next launch
15
+ * skips that exact bundle.
16
+ * 4. NO business logic ships in the npm package: when no engine can be
17
+ * obtained (no network, no valid cache, crash-looping bundle), `load`
18
+ * returns null and the SDK runs in keypoints-only mode.
19
+ */
20
+ import type { EngineBundleDescriptor } from '../types/manifest';
21
+ import type { PoseTrackerEngine } from './types';
22
+ export interface FileStore {
23
+ read(key: string): Promise<string | null>;
24
+ write(key: string, contents: string): Promise<void>;
25
+ remove(key: string): Promise<void>;
26
+ }
27
+ export interface KeyValueStore {
28
+ getItem(key: string): Promise<string | null>;
29
+ setItem(key: string, value: string): Promise<void>;
30
+ removeItem(key: string): Promise<void>;
31
+ }
32
+ /** Filesystem-backed store using expo-file-system (optional peer dependency). */
33
+ export declare function createExpoFileStore(dirName?: string): FileStore | null;
34
+ /** Filesystem-backed store using react-native-fs (bare RN, optional). */
35
+ export declare function createRnfsFileStore(dirName: string): FileStore | null;
36
+ /** Filesystem-backed store using react-native-blob-util (bare RN, optional). */
37
+ export declare function createBlobUtilFileStore(dirName: string): FileStore | null;
38
+ /**
39
+ * In-memory store — last-resort fallback when no filesystem module is
40
+ * installed (bare RN without expo-file-system / react-native-fs /
41
+ * react-native-blob-util). The SDK stays fully functional but nothing
42
+ * persists across app launches: every cold start needs the network again.
43
+ */
44
+ export declare function createMemoryFileStore(): FileStore;
45
+ /**
46
+ * Best available persistent file store for the host app:
47
+ * expo-file-system (Expo / Expo Go) → react-native-fs (bare RN) →
48
+ * react-native-blob-util (bare RN). Returns null when none is installed —
49
+ * callers may fall back to {@link createMemoryFileStore}.
50
+ */
51
+ export declare function createNativeFileStore(dirName: string): FileStore | null;
52
+ /** AsyncStorage-backed store (already a peer dep via tfjs-react-native). */
53
+ export declare function createAsyncKeyValueStore(): KeyValueStore | null;
54
+ export interface EngineLoadResult {
55
+ engine: PoseTrackerEngine;
56
+ source: 'remote-cache' | 'remote-download';
57
+ }
58
+ export interface EngineLoaderOptions {
59
+ fileStore?: FileStore | null;
60
+ keyValueStore?: KeyValueStore | null;
61
+ /** Injectable for tests. */
62
+ fetchFn?: typeof fetch;
63
+ /** Diagnostic trail (Metro / logcat) — why a load returned null. */
64
+ onDiagnostic?: (message: string) => void;
65
+ }
66
+ export declare class EngineLoader {
67
+ private readonly files;
68
+ private readonly kv;
69
+ private readonly fetchFn;
70
+ private readonly onDiagnostic;
71
+ /** Last load failure detail (surfaced by the client error event). */
72
+ lastError: string | null;
73
+ constructor(options?: EngineLoaderOptions);
74
+ /**
75
+ * Clear a crash-guard mark so the next {@link load} can retry this bundle
76
+ * (e.g. after the host taps "Test key").
77
+ */
78
+ clearGuard(descriptor: EngineBundleDescriptor | null): Promise<void>;
79
+ /**
80
+ * Load the engine described by the manifest. Returns null when no engine
81
+ * can be obtained (the caller then runs keypoints-only).
82
+ *
83
+ * @param cacheSecret secret derived from the API token, used to seal/open
84
+ * the cached bundle (`deriveCacheSecret`).
85
+ */
86
+ load(descriptor: EngineBundleDescriptor | null, cacheSecret: string, options?: {
87
+ forceRetry?: boolean;
88
+ }): Promise<EngineLoadResult | null>;
89
+ /**
90
+ * Evaluate an engine bundle (CommonJS-style: the bundle assigns
91
+ * `module.exports.createEngine`). Wrapped by the crash-loop guard.
92
+ * Hermes and JSC both support runtime evaluation via `new Function`.
93
+ */
94
+ private evaluate;
95
+ }
@@ -0,0 +1,347 @@
1
+ "use strict";
2
+ /**
3
+ * Remote engine distribution (Sency-style model distribution, applied to the
4
+ * business-logic bundle):
5
+ *
6
+ * 1. The handshake manifest carries a signed, token-gated URL of a versioned
7
+ * JS engine bundle plus its SHA-256 digest.
8
+ * 2. The bundle is cached on the local filesystem, sealed with a secret
9
+ * derived from the API token (see `cache/obfuscate.ts`): a device without
10
+ * the key cannot read the cached business logic. Integrity is re-validated
11
+ * against the manifest digest on every load (corrupted/partial downloads
12
+ * are purged, like Sency's `TFL3` magic check on cached TFLite files).
13
+ * 3. A crash-loop guard (analog of Sency's `TfliteRuntimeGuard`
14
+ * PROBING→PASSED/FAILED state machine) marks the bundle as "probing"
15
+ * before evaluation; if the app died while probing, the next launch
16
+ * skips that exact bundle.
17
+ * 4. NO business logic ships in the npm package: when no engine can be
18
+ * obtained (no network, no valid cache, crash-looping bundle), `load`
19
+ * returns null and the SDK runs in keypoints-only mode.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.EngineLoader = void 0;
23
+ exports.createExpoFileStore = createExpoFileStore;
24
+ exports.createRnfsFileStore = createRnfsFileStore;
25
+ exports.createBlobUtilFileStore = createBlobUtilFileStore;
26
+ exports.createMemoryFileStore = createMemoryFileStore;
27
+ exports.createNativeFileStore = createNativeFileStore;
28
+ exports.createAsyncKeyValueStore = createAsyncKeyValueStore;
29
+ const js_sha256_1 = require("js-sha256");
30
+ const obfuscate_1 = require("../cache/obfuscate");
31
+ function asExpoFileSystemLegacy(mod) {
32
+ const raw = mod;
33
+ const fs = (raw?.cacheDirectory != null ? raw : raw?.default);
34
+ if (fs &&
35
+ typeof fs.cacheDirectory === 'string' &&
36
+ typeof fs.readAsStringAsync === 'function' &&
37
+ typeof fs.writeAsStringAsync === 'function' &&
38
+ typeof fs.deleteAsync === 'function' &&
39
+ typeof fs.makeDirectoryAsync === 'function') {
40
+ return fs;
41
+ }
42
+ return null;
43
+ }
44
+ /**
45
+ * Resolve the Expo FS API that still exposes the classic helpers.
46
+ * Expo SDK 54+ moved them to `expo-file-system/legacy` — the root export's
47
+ * `readAsStringAsync` / `writeAsStringAsync` now THROW at runtime.
48
+ *
49
+ * Requires MUST be static string literals (Metro rejects `require(id)`).
50
+ */
51
+ function requireExpoFileSystemLegacy() {
52
+ try {
53
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
54
+ const legacy = asExpoFileSystemLegacy(require('expo-file-system/legacy'));
55
+ if (legacy)
56
+ return legacy;
57
+ }
58
+ catch {
59
+ /* not installed / old expo-file-system without /legacy */
60
+ }
61
+ try {
62
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
63
+ const root = asExpoFileSystemLegacy(require('expo-file-system'));
64
+ if (root)
65
+ return root;
66
+ }
67
+ catch {
68
+ /* expo-file-system not installed */
69
+ }
70
+ return null;
71
+ }
72
+ /** Filesystem-backed store using expo-file-system (optional peer dependency). */
73
+ function createExpoFileStore(dirName = 'posetracker-engine') {
74
+ const FileSystem = requireExpoFileSystemLegacy();
75
+ if (!FileSystem || !FileSystem.cacheDirectory) {
76
+ return null;
77
+ }
78
+ const dir = `${FileSystem.cacheDirectory}${dirName}/`;
79
+ const ensureDir = FileSystem.makeDirectoryAsync(dir, { intermediates: true }).catch(() => { });
80
+ return {
81
+ async read(key) {
82
+ try {
83
+ return await FileSystem.readAsStringAsync(dir + key);
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ },
89
+ async write(key, contents) {
90
+ await ensureDir;
91
+ await FileSystem.writeAsStringAsync(dir + key, contents);
92
+ },
93
+ async remove(key) {
94
+ await FileSystem.deleteAsync(dir + key, { idempotent: true });
95
+ },
96
+ };
97
+ }
98
+ /** Filesystem-backed store using react-native-fs (bare RN, optional). */
99
+ function createRnfsFileStore(dirName) {
100
+ let RNFS;
101
+ try {
102
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
103
+ RNFS = require('react-native-fs');
104
+ if (!RNFS || typeof RNFS.CachesDirectoryPath !== 'string')
105
+ return null;
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ const dir = `${RNFS.CachesDirectoryPath}/${dirName}`;
111
+ const ensureDir = RNFS.mkdir(dir).catch(() => { });
112
+ return {
113
+ async read(key) {
114
+ try {
115
+ return await RNFS.readFile(`${dir}/${key}`, 'utf8');
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ },
121
+ async write(key, contents) {
122
+ await ensureDir;
123
+ await RNFS.writeFile(`${dir}/${key}`, contents, 'utf8');
124
+ },
125
+ async remove(key) {
126
+ await RNFS.unlink(`${dir}/${key}`).catch(() => { });
127
+ },
128
+ };
129
+ }
130
+ /** Filesystem-backed store using react-native-blob-util (bare RN, optional). */
131
+ function createBlobUtilFileStore(dirName) {
132
+ let fs;
133
+ try {
134
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
135
+ const mod = require('react-native-blob-util');
136
+ fs = (mod.default ?? mod).fs;
137
+ if (!fs || !fs.dirs || typeof fs.dirs.CacheDir !== 'string')
138
+ return null;
139
+ }
140
+ catch {
141
+ return null;
142
+ }
143
+ const dir = `${fs.dirs.CacheDir}/${dirName}`;
144
+ const ensureDir = fs.mkdir(dir).catch(() => { });
145
+ return {
146
+ async read(key) {
147
+ try {
148
+ return await fs.readFile(`${dir}/${key}`, 'utf8');
149
+ }
150
+ catch {
151
+ return null;
152
+ }
153
+ },
154
+ async write(key, contents) {
155
+ await ensureDir;
156
+ await fs.writeFile(`${dir}/${key}`, contents, 'utf8');
157
+ },
158
+ async remove(key) {
159
+ await fs.unlink(`${dir}/${key}`).catch(() => { });
160
+ },
161
+ };
162
+ }
163
+ /**
164
+ * In-memory store — last-resort fallback when no filesystem module is
165
+ * installed (bare RN without expo-file-system / react-native-fs /
166
+ * react-native-blob-util). The SDK stays fully functional but nothing
167
+ * persists across app launches: every cold start needs the network again.
168
+ */
169
+ function createMemoryFileStore() {
170
+ const entries = new Map();
171
+ return {
172
+ async read(key) {
173
+ return entries.has(key) ? entries.get(key) : null;
174
+ },
175
+ async write(key, contents) {
176
+ entries.set(key, contents);
177
+ },
178
+ async remove(key) {
179
+ entries.delete(key);
180
+ },
181
+ };
182
+ }
183
+ /**
184
+ * Best available persistent file store for the host app:
185
+ * expo-file-system (Expo / Expo Go) → react-native-fs (bare RN) →
186
+ * react-native-blob-util (bare RN). Returns null when none is installed —
187
+ * callers may fall back to {@link createMemoryFileStore}.
188
+ */
189
+ function createNativeFileStore(dirName) {
190
+ return (createExpoFileStore(dirName) ??
191
+ createRnfsFileStore(dirName) ??
192
+ createBlobUtilFileStore(dirName));
193
+ }
194
+ /** AsyncStorage-backed store (already a peer dep via tfjs-react-native). */
195
+ function createAsyncKeyValueStore() {
196
+ try {
197
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
198
+ const AsyncStorage = require('@react-native-async-storage/async-storage').default;
199
+ return AsyncStorage;
200
+ }
201
+ catch {
202
+ return null;
203
+ }
204
+ }
205
+ // ---------------------------------------------------------------------------
206
+ // Loader
207
+ // ---------------------------------------------------------------------------
208
+ const GUARD_KEY_PREFIX = 'posetracker.engine.guard.';
209
+ class EngineLoader {
210
+ constructor(options = {}) {
211
+ /** Last load failure detail (surfaced by the client error event). */
212
+ this.lastError = null;
213
+ this.files =
214
+ options.fileStore !== undefined
215
+ ? options.fileStore
216
+ : createNativeFileStore('posetracker-engine');
217
+ this.kv =
218
+ options.keyValueStore !== undefined ? options.keyValueStore : createAsyncKeyValueStore();
219
+ this.fetchFn = options.fetchFn ?? fetch;
220
+ this.onDiagnostic = options.onDiagnostic;
221
+ this.onDiagnostic?.(`[posetracker] EngineLoader fileStore=${this.files ? 'native' : 'none'} ` +
222
+ `kv=${this.kv ? 'async-storage' : 'none'}`);
223
+ }
224
+ /**
225
+ * Clear a crash-guard mark so the next {@link load} can retry this bundle
226
+ * (e.g. after the host taps "Test key").
227
+ */
228
+ async clearGuard(descriptor) {
229
+ if (!descriptor || !this.kv)
230
+ return;
231
+ const guardKey = `${GUARD_KEY_PREFIX}${descriptor.version}.${descriptor.sha256}`;
232
+ await this.kv.removeItem(guardKey).catch(() => { });
233
+ }
234
+ /**
235
+ * Load the engine described by the manifest. Returns null when no engine
236
+ * can be obtained (the caller then runs keypoints-only).
237
+ *
238
+ * @param cacheSecret secret derived from the API token, used to seal/open
239
+ * the cached bundle (`deriveCacheSecret`).
240
+ */
241
+ async load(descriptor, cacheSecret, options) {
242
+ this.lastError = null;
243
+ if (!descriptor) {
244
+ this.lastError = 'manifest has no engine descriptor';
245
+ return null;
246
+ }
247
+ const guardKey = `${GUARD_KEY_PREFIX}${descriptor.version}.${descriptor.sha256}`;
248
+ if (options?.forceRetry) {
249
+ await this.kv?.removeItem(guardKey).catch(() => { });
250
+ }
251
+ const guardState = (await this.kv?.getItem(guardKey));
252
+ if (guardState === 'probing' || guardState === 'failed') {
253
+ // 'probing' left over = the app crashed while evaluating this exact
254
+ // bundle on a previous launch. Do not retry it automatically.
255
+ await this.kv?.setItem(guardKey, 'failed');
256
+ this.lastError =
257
+ `engine crash-guard blocked version=${descriptor.version} ` +
258
+ `(state=${guardState}) — tap Test key to force retry`;
259
+ this.onDiagnostic?.(`[posetracker] ${this.lastError}`);
260
+ return null;
261
+ }
262
+ const cacheKey = `engine-${descriptor.version}.sealed`;
263
+ // 1. Cache hit: unseal + integrity check.
264
+ const sealed = await this.files?.read(cacheKey);
265
+ if (sealed !== null && sealed !== undefined) {
266
+ const code = (0, obfuscate_1.openString)(sealed, cacheSecret);
267
+ if (code !== null && (0, js_sha256_1.sha256)(code) === descriptor.sha256) {
268
+ const engine = await this.evaluate(code, guardKey);
269
+ if (engine) {
270
+ this.onDiagnostic?.(`[posetracker] engine loaded from cache version=${descriptor.version}`);
271
+ return { engine, source: 'remote-cache' };
272
+ }
273
+ }
274
+ else {
275
+ // Corrupted, partial, or sealed with another token: purge it.
276
+ this.onDiagnostic?.(`[posetracker] engine cache purged (unseal/integrity mismatch) key=${cacheKey}`);
277
+ await this.files?.remove(cacheKey);
278
+ }
279
+ }
280
+ // 2. Download → integrity → evaluate. Cache write is best-effort AFTER
281
+ // evaluate so a broken FS API (Expo 54 root export) cannot block the
282
+ // engine when the download itself succeeded.
283
+ try {
284
+ this.onDiagnostic?.(`[posetracker] downloading engine version=${descriptor.version}…`);
285
+ const response = await this.fetchFn(descriptor.signedUrl);
286
+ if (!response.ok) {
287
+ throw new Error(`HTTP ${response.status}`);
288
+ }
289
+ const code = await response.text();
290
+ const digest = (0, js_sha256_1.sha256)(code);
291
+ if (digest !== descriptor.sha256) {
292
+ throw new Error(`integrity check failed (got ${digest.slice(0, 12)}… expected ${descriptor.sha256.slice(0, 12)}…, ` +
293
+ `bytes=${code.length})`);
294
+ }
295
+ const engine = await this.evaluate(code, guardKey);
296
+ if (engine) {
297
+ try {
298
+ await this.files?.write(cacheKey, (0, obfuscate_1.sealString)(code, cacheSecret));
299
+ }
300
+ catch (err) {
301
+ // Non-fatal: next cold start will re-download.
302
+ this.onDiagnostic?.(`[posetracker] engine cache write failed (non-fatal): ` +
303
+ (err instanceof Error ? err.message : String(err)));
304
+ }
305
+ this.onDiagnostic?.(`[posetracker] engine loaded from download version=${descriptor.version} bytes=${code.length}`);
306
+ return { engine, source: 'remote-download' };
307
+ }
308
+ }
309
+ catch (err) {
310
+ this.lastError = err instanceof Error ? err.message : String(err);
311
+ this.onDiagnostic?.(`[posetracker] engine download/load failed: ${this.lastError}`);
312
+ }
313
+ if (!this.lastError) {
314
+ this.lastError = 'engine evaluate returned null';
315
+ }
316
+ return null;
317
+ }
318
+ /**
319
+ * Evaluate an engine bundle (CommonJS-style: the bundle assigns
320
+ * `module.exports.createEngine`). Wrapped by the crash-loop guard.
321
+ * Hermes and JSC both support runtime evaluation via `new Function`.
322
+ */
323
+ async evaluate(code, guardKey) {
324
+ await this.kv?.setItem(guardKey, 'probing');
325
+ try {
326
+ const moduleRef = { exports: {} };
327
+ // eslint-disable-next-line no-new-func
328
+ const run = new Function('module', 'exports', code);
329
+ run(moduleRef, moduleRef.exports);
330
+ const factory = moduleRef.exports.createEngine;
331
+ if (typeof factory !== 'function') {
332
+ throw new Error(`engine bundle does not export createEngine() (got ${typeof factory}, keys=${Object.keys(moduleRef.exports).join(',')})`);
333
+ }
334
+ const engine = factory();
335
+ await this.kv?.setItem(guardKey, 'passed');
336
+ return engine;
337
+ }
338
+ catch (err) {
339
+ this.lastError =
340
+ 'engine evaluate failed: ' + (err instanceof Error ? err.message : String(err));
341
+ this.onDiagnostic?.(`[posetracker] ${this.lastError}`);
342
+ await this.kv?.setItem(guardKey, 'failed');
343
+ return null;
344
+ }
345
+ }
346
+ }
347
+ exports.EngineLoader = EngineLoader;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Contract between the SDK shell and the engine (business logic: angles,
3
+ * rep counting, form scoring, posture, progression, recommendations).
4
+ *
5
+ * The engine is EXCLUSIVELY distributed remotely as a versioned JS bundle
6
+ * (signed URL in the handshake manifest, SHA-256 integrity check, sealed
7
+ * local cache) — the npm package ships zero business logic. Without a
8
+ * loaded engine the SDK runs in keypoints-only mode.
9
+ */
10
+ import type { Pose } from '../types/pose';
11
+ import type { MinGrade, PoseTrackerEvent } from '../types/events';
12
+ import type { ExerciseConfig } from '../types/manifest';
13
+ /**
14
+ * Feature flags forwarded to the engine session (WebView query-param
15
+ * parity). The client already filters emissions by plan/flags; passing
16
+ * them to the engine is a second gate AND lets the engine skip work.
17
+ */
18
+ export interface EngineSessionFeatures {
19
+ angles?: boolean;
20
+ recommendations?: boolean;
21
+ progression?: boolean;
22
+ }
23
+ export interface EngineSessionOptions {
24
+ exercise: ExerciseConfig;
25
+ /** Locale used to resolve localized hints/recommendations. */
26
+ locale: string;
27
+ /** Difficulty key into the movement `scale_acceptance` maps. Default: 'medium'. */
28
+ difficulty?: string;
29
+ /** Parsed reference-movement signature, when comparing against a reference. */
30
+ referenceSignature?: unknown;
31
+ /**
32
+ * Only count reps whose form grade is at or above this letter (`A` best).
33
+ * WebView `minGrade` parity: reps below the bar emit neither `counter`
34
+ * nor `form_score` and are excluded from the summary.
35
+ */
36
+ minGrade?: MinGrade;
37
+ /** Emission flags — engines older than 1.1.0 ignore them (client re-filters). */
38
+ features?: EngineSessionFeatures;
39
+ }
40
+ export interface EngineSession {
41
+ /** Feed one estimated pose; the session emits events through the sink. */
42
+ processPose(pose: Pose): void;
43
+ /** Finish the session; emits a final `exercise_summary` event. */
44
+ end(): void;
45
+ }
46
+ /**
47
+ * Custom (non-FSM) exercise shipped inside the engine bundle, e.g. the jump
48
+ * analysis handlers (WebView `customHandlers.js` parity). Not present in the
49
+ * Strapi movement manifest — discovered via `listCustomExercises()`.
50
+ */
51
+ export interface CustomExerciseDescriptor {
52
+ /** Exercise id used with `startExercise()` (e.g. 'jump_analysis'). */
53
+ id: string;
54
+ displayName: string;
55
+ type: 'custom';
56
+ description?: string;
57
+ /** Option keys that MUST be passed to `startExercise` (e.g. 'userHeightCm'). */
58
+ requiredParams: string[];
59
+ optionalParams: string[];
60
+ }
61
+ /** Options for engine custom sessions (jump_analysis / air_time_jump). */
62
+ export interface CustomSessionOptions {
63
+ exerciseId: string;
64
+ locale: string;
65
+ /** Athlete height in cm — required by jump_analysis (cm/pixel calibration). */
66
+ userHeightCm?: number;
67
+ /** Device pitch in degrees, used to compensate camera tilt. */
68
+ devicePitchDeg?: number;
69
+ }
70
+ export type EventSink = (event: PoseTrackerEvent) => void;
71
+ export interface PoseTrackerEngine {
72
+ readonly version: string;
73
+ createSession(options: EngineSessionOptions, emit: EventSink): EngineSession;
74
+ /** Engine >= 1.2.0: custom exercises shipped in the bundle (jump analysis…). */
75
+ listCustomExercises?(): CustomExerciseDescriptor[];
76
+ /** Engine >= 1.2.0: start a custom exercise session (placement + jump_* events). */
77
+ createCustomSession?(options: CustomSessionOptions, emit: EventSink): EngineSession;
78
+ }
79
+ /** What an engine bundle's `module.exports` must expose. */
80
+ export interface EngineModuleExports {
81
+ createEngine(): PoseTrackerEngine;
82
+ }
83
+ export type EngineFactory = () => PoseTrackerEngine;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ /**
3
+ * Contract between the SDK shell and the engine (business logic: angles,
4
+ * rep counting, form scoring, posture, progression, recommendations).
5
+ *
6
+ * The engine is EXCLUSIVELY distributed remotely as a versioned JS bundle
7
+ * (signed URL in the handshake manifest, SHA-256 integrity check, sealed
8
+ * local cache) — the npm package ships zero business logic. Without a
9
+ * loaded engine the SDK runs in keypoints-only mode.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });