@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,202 @@
1
+ "use strict";
2
+ /**
3
+ * Camera / preprocess quality tiers for the WebView MoveNet runtime.
4
+ *
5
+ * Inspired by Sency's PoseModelChoice ladder (Prime → … → Basic), adapted to
6
+ * our stack: MoveNet Lightning always runs at 192×192; what changes is the
7
+ * **camera capture resolution**.
8
+ *
9
+ * FPS policy uses a **minimum target** (floor), not a single setpoint:
10
+ * - iOS: min 30 — acceptable/ideal band roughly 30–50+ FPS
11
+ * - Android: min from {@link ANDROID_MIN_TARGET_FPS} (experiment default 10)
12
+ *
13
+ * Warm-up benches estimate execute cost; AdaptiveChoice picks a capture
14
+ * profile before getUserMedia when the estimate is below the platform floor.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.QUALITY_PROFILES = exports.QUALITY_SETTLE_MS = exports.LOW_FPS_STREAK_BEFORE_DOWNGRADE = exports.CRITICAL_FPS_THRESHOLD = exports.TARGET_MEDIAN_MS = exports.TARGET_FPS = exports.IDEAL_FPS_RANGE_ANDROID = exports.IDEAL_FPS_RANGE_IOS = exports.MIN_TARGET_FPS_DEFAULT = exports.MIN_TARGET_FPS_IOS = exports.MIN_TARGET_FPS_ANDROID = exports.QUALITY_LADDER = void 0;
18
+ exports.getCriticalFpsThreshold = getCriticalFpsThreshold;
19
+ exports.currentQualityPlatform = currentQualityPlatform;
20
+ exports.getMinTargetFps = getMinTargetFps;
21
+ exports.getIdealFpsRange = getIdealFpsRange;
22
+ exports.minMedianMsForMinTarget = minMedianMsForMinTarget;
23
+ exports.getQualityProfile = getQualityProfile;
24
+ exports.nextLowerQualityProfile = nextLowerQualityProfile;
25
+ exports.isQualityProfileId = isQualityProfileId;
26
+ exports.qualityLadderIndex = qualityLadderIndex;
27
+ exports.lowerQualityProfile = lowerQualityProfile;
28
+ exports.profileFromWarmupMedianMs = profileFromWarmupMedianMs;
29
+ exports.estimatedFpsFromMedianMs = estimatedFpsFromMedianMs;
30
+ const react_native_1 = require("react-native");
31
+ const captureMode_1 = require("./captureMode");
32
+ /**
33
+ * Ordered highest → lowest. `nextLowerQualityProfile` walks this ladder.
34
+ */
35
+ exports.QUALITY_LADDER = [
36
+ 'prime',
37
+ 'pro',
38
+ 'lite',
39
+ 'ultralite',
40
+ 'basic',
41
+ ];
42
+ /**
43
+ * Android minimum acceptable inference FPS (floor).
44
+ * Sourced from {@link ANDROID_MIN_TARGET_FPS} so the experiment flag is the
45
+ * single knob (see captureMode.ts).
46
+ */
47
+ exports.MIN_TARGET_FPS_ANDROID = captureMode_1.ANDROID_MIN_TARGET_FPS;
48
+ /**
49
+ * iOS minimum acceptable inference FPS (floor).
50
+ * Acceptable/ideal experience is typically 30–50+ on Apple GPU.
51
+ */
52
+ exports.MIN_TARGET_FPS_IOS = 30;
53
+ /** Fallback floor for unknown platforms. */
54
+ exports.MIN_TARGET_FPS_DEFAULT = exports.MIN_TARGET_FPS_ANDROID;
55
+ /**
56
+ * Documented “good experience” bands (not hard caps).
57
+ * Downgrades / warnings use the **min** only.
58
+ */
59
+ exports.IDEAL_FPS_RANGE_IOS = { min: exports.MIN_TARGET_FPS_IOS, idealMax: 50 };
60
+ exports.IDEAL_FPS_RANGE_ANDROID = { min: exports.MIN_TARGET_FPS_ANDROID, idealMax: 30 };
61
+ /**
62
+ * @deprecated Use {@link getMinTargetFps} / {@link MIN_TARGET_FPS_ANDROID}.
63
+ * Kept as the Android floor so older imports keep a sensible number.
64
+ */
65
+ exports.TARGET_FPS = exports.MIN_TARGET_FPS_ANDROID;
66
+ /** @deprecated Use {@link minMedianMsForMinTarget}. */
67
+ exports.TARGET_MEDIAN_MS = 1000 / exports.MIN_TARGET_FPS_ANDROID;
68
+ /**
69
+ * Critical alert when FPS stays well below the platform minimum target
70
+ * (even on `basic`). Android: <10; iOS: <20.
71
+ */
72
+ function getCriticalFpsThreshold(platform = currentQualityPlatform()) {
73
+ if (platform === 'ios')
74
+ return 20;
75
+ return 10;
76
+ }
77
+ /** @deprecated Prefer {@link getCriticalFpsThreshold}. */
78
+ exports.CRITICAL_FPS_THRESHOLD = 10;
79
+ /** Consecutive samples below min-target before auto-downgrade. */
80
+ /** Sustained samples below the inference budget before a live downgrade. */
81
+ exports.LOW_FPS_STREAK_BEFORE_DOWNGRADE = 4;
82
+ /**
83
+ * Ignore live downgrade decisions for this long after ready / a quality swap
84
+ * (camera restart tanks the first 1 Hz stats samples).
85
+ */
86
+ exports.QUALITY_SETTLE_MS = 5000;
87
+ exports.QUALITY_PROFILES = {
88
+ prime: {
89
+ id: 'prime',
90
+ label: 'Prime (1280×720 @30)',
91
+ idealWidth: 1280,
92
+ idealHeight: 720,
93
+ idealFrameRate: 30,
94
+ minStableFps: exports.MIN_TARGET_FPS_ANDROID,
95
+ },
96
+ pro: {
97
+ id: 'pro',
98
+ label: 'Pro (960×540 @30)',
99
+ idealWidth: 960,
100
+ idealHeight: 540,
101
+ idealFrameRate: 30,
102
+ minStableFps: exports.MIN_TARGET_FPS_ANDROID,
103
+ },
104
+ lite: {
105
+ id: 'lite',
106
+ label: 'Lite (640×480 @30)',
107
+ idealWidth: 640,
108
+ idealHeight: 480,
109
+ idealFrameRate: 30,
110
+ minStableFps: exports.MIN_TARGET_FPS_ANDROID,
111
+ },
112
+ ultralite: {
113
+ id: 'ultralite',
114
+ label: 'UltraLite (480×360 @24)',
115
+ idealWidth: 480,
116
+ idealHeight: 360,
117
+ idealFrameRate: 24,
118
+ minStableFps: exports.MIN_TARGET_FPS_ANDROID,
119
+ },
120
+ basic: {
121
+ id: 'basic',
122
+ label: 'Basic (320×240 @20)',
123
+ idealWidth: 320,
124
+ idealHeight: 240,
125
+ idealFrameRate: 20,
126
+ minStableFps: exports.MIN_TARGET_FPS_ANDROID,
127
+ },
128
+ };
129
+ function currentQualityPlatform() {
130
+ if (react_native_1.Platform.OS === 'ios')
131
+ return 'ios';
132
+ if (react_native_1.Platform.OS === 'android')
133
+ return 'android';
134
+ return 'other';
135
+ }
136
+ /** Platform minimum target FPS (floor — not a setpoint). */
137
+ function getMinTargetFps(platform = currentQualityPlatform()) {
138
+ if (platform === 'ios')
139
+ return exports.MIN_TARGET_FPS_IOS;
140
+ if (platform === 'android')
141
+ return captureMode_1.ANDROID_MIN_TARGET_FPS;
142
+ return exports.MIN_TARGET_FPS_DEFAULT;
143
+ }
144
+ function getIdealFpsRange(platform = currentQualityPlatform()) {
145
+ if (platform === 'ios')
146
+ return exports.IDEAL_FPS_RANGE_IOS;
147
+ if (platform === 'android')
148
+ return exports.IDEAL_FPS_RANGE_ANDROID;
149
+ return { min: exports.MIN_TARGET_FPS_DEFAULT, idealMax: 30 };
150
+ }
151
+ /** Max warm-up median ms that still meets a given min-target FPS. */
152
+ function minMedianMsForMinTarget(minTargetFps) {
153
+ return 1000 / minTargetFps;
154
+ }
155
+ function getQualityProfile(id) {
156
+ return exports.QUALITY_PROFILES[id];
157
+ }
158
+ function nextLowerQualityProfile(id) {
159
+ const idx = exports.QUALITY_LADDER.indexOf(id);
160
+ if (idx < 0 || idx >= exports.QUALITY_LADDER.length - 1)
161
+ return null;
162
+ return exports.QUALITY_LADDER[idx + 1];
163
+ }
164
+ function isQualityProfileId(value) {
165
+ return Object.prototype.hasOwnProperty.call(exports.QUALITY_PROFILES, value);
166
+ }
167
+ function qualityLadderIndex(id) {
168
+ return exports.QUALITY_LADDER.indexOf(id);
169
+ }
170
+ /** Lower or equal quality (higher index) wins. */
171
+ function lowerQualityProfile(a, b) {
172
+ return qualityLadderIndex(a) >= qualityLadderIndex(b) ? a : b;
173
+ }
174
+ /**
175
+ * Map warm-up execute median (zeros, no camera) → max safe capture profile.
176
+ *
177
+ * Thresholds scale with the platform **minimum target** FPS. Warm-up ignores
178
+ * bitmap cost, so anything over the floor budget must stay on `basic` —
179
+ * the old `≤ 1.3× budget → ultralite` band upgraded Mali devices that were
180
+ * already below the floor (e.g. 120 ms zeros → ultralite → live ~8 FPS).
181
+ */
182
+ function profileFromWarmupMedianMs(medianMs, minTargetFps = getMinTargetFps()) {
183
+ if (medianMs == null || !Number.isFinite(medianMs))
184
+ return 'basic';
185
+ const budget = minMedianMsForMinTarget(minTargetFps);
186
+ // Over the floor on zeros alone → basic only (camera adds more cost).
187
+ if (medianMs > budget)
188
+ return 'basic';
189
+ // At / under the floor with headroom → higher capture ok
190
+ if (medianMs <= budget * 0.6)
191
+ return 'prime';
192
+ if (medianMs <= budget * 0.75)
193
+ return 'pro';
194
+ if (medianMs <= budget * 0.9)
195
+ return 'lite';
196
+ return 'ultralite';
197
+ }
198
+ function estimatedFpsFromMedianMs(medianMs) {
199
+ if (medianMs == null || !Number.isFinite(medianMs) || medianMs <= 0)
200
+ return null;
201
+ return 1000 / medianMs;
202
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Versioned filesystem cache of a remote pose-runtime payload (legacy
3
+ * stub-loader helper).
4
+ *
5
+ * The light SDK’s default path does **not** use this cache: TF.js loads from
6
+ * CDN and the model from a URL inside the WebView each boot. This class
7
+ * remains exported for advanced hosts that still want to download / verify
8
+ * Strapi `/api/sdk/pose-runtime` parts.
9
+ *
10
+ * Guarantees when used:
11
+ * - sha256 is verified BEFORE a part is committed to the cache.
12
+ * - `purge()` on revocation signals.
13
+ */
14
+ import type { PoseRuntimeDescriptor } from '../types/manifest';
15
+ import { type FileStore } from '../engine/EngineLoader';
16
+ /** Decoded, ready-to-inject runtime parts. */
17
+ export interface PoseRuntimeParts {
18
+ version: string;
19
+ /** TF.js bundle (core + converter + webgl + wasm backends). */
20
+ tfjsJs: string;
21
+ /** XNNPACK wasm binaries (base64). */
22
+ tfjsWasmB64: {
23
+ plain: string;
24
+ simd: string;
25
+ threadedSimd: string;
26
+ };
27
+ /** MoveNet graph model topology (raw model.json). */
28
+ modelJson: string;
29
+ /** Weight shards (base64, manifest order). */
30
+ weightsB64: string[];
31
+ /** Proprietary pose pipeline wasm (base64), null when not deployed. */
32
+ pipelineWasmB64: string | null;
33
+ /** Page runtime (camera, presets, inference loop, events). */
34
+ runtimeJs: string;
35
+ }
36
+ export interface RuntimeCacheProgress {
37
+ part: string;
38
+ completedParts: number;
39
+ totalParts: number;
40
+ /** Bytes of the part being downloaded (from the manifest). */
41
+ partBytes: number;
42
+ }
43
+ export interface RuntimeCacheOptions {
44
+ fileStore?: FileStore | null;
45
+ /** Injectable for tests. */
46
+ fetchFn?: typeof fetch;
47
+ }
48
+ export declare class RuntimeCache {
49
+ private readonly files;
50
+ private readonly fetchFn;
51
+ private memoryParts;
52
+ /** True when no persistent FS module is installed (memory-only fallback). */
53
+ readonly persistent: boolean;
54
+ constructor(options?: RuntimeCacheOptions);
55
+ /** Version currently committed to the cache, or null when cold/empty. */
56
+ getCachedVersion(): Promise<string | null>;
57
+ /**
58
+ * Ensure the cache holds the version described by the manifest. Downloads
59
+ * only what is missing/outdated; every part is checksum-verified BEFORE
60
+ * being committed. The previous version keeps serving until the new one is
61
+ * fully committed (atomic flip of `current.json`, then best-effort GC).
62
+ *
63
+ * Returns true when the cache is warm (either already up-to-date or
64
+ * successfully updated); false when it could not be warmed (offline...) —
65
+ * the caller decides whether a stale/absent cache is acceptable.
66
+ */
67
+ warm(descriptor: PoseRuntimeDescriptor | null, onProgress?: (progress: RuntimeCacheProgress) => void): Promise<boolean>;
68
+ /** Load (and decode) the committed runtime parts; null when cache is cold. */
69
+ load(): Promise<PoseRuntimeParts | null>;
70
+ /** Wipe the whole runtime cache (revocation / recovery). */
71
+ purge(): Promise<void>;
72
+ private isKnownPart;
73
+ private readCurrent;
74
+ private readVerified;
75
+ }
@@ -0,0 +1,230 @@
1
+ "use strict";
2
+ /**
3
+ * Versioned filesystem cache of a remote pose-runtime payload (legacy
4
+ * stub-loader helper).
5
+ *
6
+ * The light SDK’s default path does **not** use this cache: TF.js loads from
7
+ * CDN and the model from a URL inside the WebView each boot. This class
8
+ * remains exported for advanced hosts that still want to download / verify
9
+ * Strapi `/api/sdk/pose-runtime` parts.
10
+ *
11
+ * Guarantees when used:
12
+ * - sha256 is verified BEFORE a part is committed to the cache.
13
+ * - `purge()` on revocation signals.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.RuntimeCache = void 0;
17
+ const js_sha256_1 = require("js-sha256");
18
+ const EngineLoader_1 = require("../engine/EngineLoader");
19
+ /** Part names, as declared by the backend runtime manifest. */
20
+ const PART_NAMES = ['tfjs', 'tfjs-wasm', 'model', 'weights', 'pipeline', 'runtime'];
21
+ /** Required parts — `pipeline` is optional (JS fallback lives in the runtime part). */
22
+ const REQUIRED_PARTS = ['tfjs', 'tfjs-wasm', 'model', 'weights', 'runtime'];
23
+ const CURRENT_KEY = 'current.json';
24
+ class RuntimeCache {
25
+ constructor(options = {}) {
26
+ this.memoryParts = null;
27
+ let files = options.fileStore !== undefined
28
+ ? options.fileStore
29
+ : (0, EngineLoader_1.createNativeFileStore)('posetracker-runtime');
30
+ this.persistent = files !== null;
31
+ if (files === null) {
32
+ // Bare RN without any filesystem lib: stay functional with an
33
+ // in-memory store — online works, but every cold start re-downloads.
34
+ files = (0, EngineLoader_1.createMemoryFileStore)();
35
+ }
36
+ this.files = files;
37
+ this.fetchFn = options.fetchFn ?? fetch;
38
+ }
39
+ /** Version currently committed to the cache, or null when cold/empty. */
40
+ async getCachedVersion() {
41
+ const current = await this.readCurrent();
42
+ return current?.version ?? null;
43
+ }
44
+ /**
45
+ * Ensure the cache holds the version described by the manifest. Downloads
46
+ * only what is missing/outdated; every part is checksum-verified BEFORE
47
+ * being committed. The previous version keeps serving until the new one is
48
+ * fully committed (atomic flip of `current.json`, then best-effort GC).
49
+ *
50
+ * Returns true when the cache is warm (either already up-to-date or
51
+ * successfully updated); false when it could not be warmed (offline...) —
52
+ * the caller decides whether a stale/absent cache is acceptable.
53
+ */
54
+ async warm(descriptor, onProgress) {
55
+ if (!this.files) {
56
+ return false;
57
+ }
58
+ const current = await this.readCurrent();
59
+ if (!descriptor) {
60
+ // Offline handshake: the existing cache (if any) is served as-is.
61
+ return current !== null;
62
+ }
63
+ if (current && current.version === descriptor.version) {
64
+ return true;
65
+ }
66
+ const wanted = Object.entries(descriptor.parts);
67
+ const next = { version: descriptor.version, parts: {} };
68
+ let completed = 0;
69
+ try {
70
+ for (const [name, part] of wanted) {
71
+ if (!this.isKnownPart(name)) {
72
+ continue;
73
+ }
74
+ onProgress?.({
75
+ part: name,
76
+ completedParts: completed,
77
+ totalParts: wanted.length,
78
+ partBytes: part.bytes,
79
+ });
80
+ const key = `${name}@${descriptor.version}`;
81
+ // Reuse without network when the exact content is already cached
82
+ // (same key from an interrupted warm, or same sha in the previous
83
+ // version under another key).
84
+ let content = await this.readVerified(key, part.sha256);
85
+ if (content === null) {
86
+ const previous = current?.parts[name];
87
+ if (previous && previous.sha256 === part.sha256) {
88
+ content = await this.readVerified(previous.key, part.sha256);
89
+ }
90
+ }
91
+ if (content === null) {
92
+ const response = await this.fetchFn(part.url);
93
+ if (!response.ok) {
94
+ throw new Error(`pose-runtime part '${name}' HTTP ${response.status}`);
95
+ }
96
+ content = await response.text();
97
+ // Integrity gate BEFORE commit: a truncated/corrupted download
98
+ // never reaches the cache.
99
+ if ((0, js_sha256_1.sha256)(content) !== part.sha256) {
100
+ throw new Error(`pose-runtime part '${name}' failed sha256 verification`);
101
+ }
102
+ }
103
+ await this.files.write(key, content);
104
+ next.parts[name] = { key, sha256: part.sha256 };
105
+ completed += 1;
106
+ }
107
+ for (const required of REQUIRED_PARTS) {
108
+ if (!next.parts[required]) {
109
+ throw new Error(`pose-runtime manifest is missing required part '${required}'`);
110
+ }
111
+ }
112
+ // Commit point: flip the pointer only once every part is verified.
113
+ await this.files.write(CURRENT_KEY, JSON.stringify(next));
114
+ this.memoryParts = null;
115
+ // Best-effort GC of the previous version's files.
116
+ if (current) {
117
+ for (const entry of Object.values(current.parts)) {
118
+ if (entry && !Object.values(next.parts).some((p) => p && p.key === entry.key)) {
119
+ await this.files.remove(entry.key).catch(() => { });
120
+ }
121
+ }
122
+ }
123
+ return true;
124
+ }
125
+ catch {
126
+ // Warm failed: the previous committed version stays intact.
127
+ return current !== null;
128
+ }
129
+ }
130
+ /** Load (and decode) the committed runtime parts; null when cache is cold. */
131
+ async load() {
132
+ if (this.memoryParts) {
133
+ return this.memoryParts;
134
+ }
135
+ if (!this.files) {
136
+ return null;
137
+ }
138
+ const current = await this.readCurrent();
139
+ if (!current) {
140
+ return null;
141
+ }
142
+ const read = async (name) => {
143
+ const entry = current.parts[name];
144
+ if (!entry) {
145
+ return null;
146
+ }
147
+ return this.readVerified(entry.key, entry.sha256);
148
+ };
149
+ const [tfjsJs, tfjsWasmRaw, modelJson, weightsRaw, pipelineWasmB64, runtimeJs] = await Promise.all([
150
+ read('tfjs'),
151
+ read('tfjs-wasm'),
152
+ read('model'),
153
+ read('weights'),
154
+ read('pipeline'),
155
+ read('runtime'),
156
+ ]);
157
+ if (!tfjsJs || !tfjsWasmRaw || !modelJson || !weightsRaw || !runtimeJs) {
158
+ // A part is missing or failed integrity: treat the cache as cold.
159
+ return null;
160
+ }
161
+ try {
162
+ this.memoryParts = {
163
+ version: current.version,
164
+ tfjsJs,
165
+ tfjsWasmB64: JSON.parse(tfjsWasmRaw),
166
+ modelJson,
167
+ weightsB64: JSON.parse(weightsRaw),
168
+ pipelineWasmB64: pipelineWasmB64 ?? null,
169
+ runtimeJs,
170
+ };
171
+ return this.memoryParts;
172
+ }
173
+ catch {
174
+ return null;
175
+ }
176
+ }
177
+ /** Wipe the whole runtime cache (revocation / recovery). */
178
+ async purge() {
179
+ this.memoryParts = null;
180
+ if (!this.files) {
181
+ return;
182
+ }
183
+ const current = await this.readCurrent();
184
+ if (current) {
185
+ for (const entry of Object.values(current.parts)) {
186
+ if (entry) {
187
+ await this.files.remove(entry.key).catch(() => { });
188
+ }
189
+ }
190
+ }
191
+ await this.files.remove(CURRENT_KEY).catch(() => { });
192
+ }
193
+ // -------------------------------------------------------------------------
194
+ isKnownPart(name) {
195
+ return PART_NAMES.includes(name);
196
+ }
197
+ async readCurrent() {
198
+ try {
199
+ const raw = await this.files?.read(CURRENT_KEY);
200
+ if (!raw) {
201
+ return null;
202
+ }
203
+ const parsed = JSON.parse(raw);
204
+ if (!parsed.version || !parsed.parts) {
205
+ return null;
206
+ }
207
+ return parsed;
208
+ }
209
+ catch {
210
+ return null;
211
+ }
212
+ }
213
+ async readVerified(key, expectedSha) {
214
+ try {
215
+ const content = await this.files?.read(key);
216
+ if (content === null || content === undefined) {
217
+ return null;
218
+ }
219
+ if ((0, js_sha256_1.sha256)(content) !== expectedSha) {
220
+ await this.files?.remove(key).catch(() => { });
221
+ return null;
222
+ }
223
+ return content;
224
+ }
225
+ catch {
226
+ return null;
227
+ }
228
+ }
229
+ }
230
+ exports.RuntimeCache = RuntimeCache;
@@ -0,0 +1,2 @@
1
+ /** AUTO-GENERATED by scripts/sync-sdk-version.mjs — do not edit. */
2
+ export declare const SDK_VERSION = "0.1.0";
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SDK_VERSION = void 0;
4
+ /** AUTO-GENERATED by scripts/sync-sdk-version.mjs — do not edit. */
5
+ exports.SDK_VERSION = '0.1.0';
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Guarded access to the OPTIONAL native modules used by the Apple Vision
3
+ * backend: react-native-vision-camera + react-native-worklets-core.
4
+ *
5
+ * They are `peerDependenciesMeta.optional` — a host that only uses the
6
+ * WebView runtime (the default, Expo Go compatible) does not install them.
7
+ *
8
+ * 1. Every `require` is wrapped in try/catch. Metro marks dependencies
9
+ * required inside a try block as *optional* when
10
+ * `transformer.allowOptionalDependencies` is enabled (the Expo default),
11
+ * so bundling never fails when a module is absent.
12
+ * 2. The SDK must not depend on these packages' TypeScript types, so the
13
+ * interfaces below are minimal *structural* typings of the APIs we
14
+ * consume (react-native-vision-camera 4.x, worklets-core 1.x).
15
+ */
16
+ /**
17
+ * Expo Go has no vision-camera native binaries. Requiring them there throws
18
+ * (and React Native LogBox still surfaces it even when wrapped in
19
+ * try/catch). Detect Expo Go *before* any of those requires.
20
+ */
21
+ export declare function isExpoGo(): boolean;
22
+ /** react-native-vision-camera 4.x — subset used by PoseCameraView. */
23
+ export interface VisionCameraFrame {
24
+ width: number;
25
+ height: number;
26
+ orientation: string;
27
+ timestamp: number;
28
+ }
29
+ export interface VisionCameraModule {
30
+ Camera: any;
31
+ useCameraDevice(position: 'front' | 'back'): unknown | undefined;
32
+ useCameraPermission(): {
33
+ hasPermission: boolean;
34
+ requestPermission: () => Promise<boolean>;
35
+ };
36
+ useFrameProcessor(processor: (frame: VisionCameraFrame) => void, deps: unknown[]): unknown;
37
+ }
38
+ /** react-native-worklets-core 1.x — subset used by PoseCameraView. */
39
+ export interface WorkletsCoreModule {
40
+ useSharedValue<T>(initial: T): {
41
+ value: T;
42
+ };
43
+ useRunOnJS<A extends unknown[]>(fn: (...args: A) => void, deps: unknown[]): (...args: A) => void;
44
+ }
45
+ export declare function getVisionCamera(): VisionCameraModule | null;
46
+ export declare function getWorkletsCore(): WorkletsCoreModule | null;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ /**
3
+ * Guarded access to the OPTIONAL native modules used by the Apple Vision
4
+ * backend: react-native-vision-camera + react-native-worklets-core.
5
+ *
6
+ * They are `peerDependenciesMeta.optional` — a host that only uses the
7
+ * WebView runtime (the default, Expo Go compatible) does not install them.
8
+ *
9
+ * 1. Every `require` is wrapped in try/catch. Metro marks dependencies
10
+ * required inside a try block as *optional* when
11
+ * `transformer.allowOptionalDependencies` is enabled (the Expo default),
12
+ * so bundling never fails when a module is absent.
13
+ * 2. The SDK must not depend on these packages' TypeScript types, so the
14
+ * interfaces below are minimal *structural* typings of the APIs we
15
+ * consume (react-native-vision-camera 4.x, worklets-core 1.x).
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.isExpoGo = isExpoGo;
19
+ exports.getVisionCamera = getVisionCamera;
20
+ exports.getWorkletsCore = getWorkletsCore;
21
+ /**
22
+ * Expo Go has no vision-camera native binaries. Requiring them there throws
23
+ * (and React Native LogBox still surfaces it even when wrapped in
24
+ * try/catch). Detect Expo Go *before* any of those requires.
25
+ */
26
+ function isExpoGo() {
27
+ try {
28
+ // Avoid a hard dependency on expo-constants: same signal Nitro uses.
29
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
30
+ const { NativeModules } = require('react-native');
31
+ return (NativeModules.NativeUnimoduleProxy?.modulesConstants?.ExponentConstants
32
+ ?.appOwnership === 'expo');
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ // ---------------------------------------------------------------------------
39
+ // Guarded requires (cached; `undefined` = not probed yet, `null` = missing)
40
+ // ---------------------------------------------------------------------------
41
+ // NOTE: every `require` below uses a STRING LITERAL — Metro cannot resolve
42
+ // dynamic `require(variable)` calls and would fail the build.
43
+ let visionCamera;
44
+ let workletsCore;
45
+ function getVisionCamera() {
46
+ if (visionCamera === undefined) {
47
+ if (isExpoGo()) {
48
+ visionCamera = null;
49
+ }
50
+ else {
51
+ try {
52
+ visionCamera = require('react-native-vision-camera');
53
+ }
54
+ catch {
55
+ visionCamera = null;
56
+ }
57
+ if (visionCamera && typeof visionCamera.useFrameProcessor !== 'function') {
58
+ visionCamera = null;
59
+ }
60
+ }
61
+ }
62
+ return visionCamera;
63
+ }
64
+ function getWorkletsCore() {
65
+ if (workletsCore === undefined) {
66
+ if (isExpoGo()) {
67
+ workletsCore = null;
68
+ }
69
+ else {
70
+ try {
71
+ workletsCore = require('react-native-worklets-core');
72
+ }
73
+ catch {
74
+ workletsCore = null;
75
+ }
76
+ if (workletsCore && typeof workletsCore.useRunOnJS !== 'function') {
77
+ workletsCore = null;
78
+ }
79
+ }
80
+ }
81
+ return workletsCore;
82
+ }