@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.
- package/LICENSE +45 -0
- package/PoseTrackerVision.podspec +28 -0
- package/README.md +161 -0
- package/THIRD_PARTY_NOTICES.md +19 -0
- package/ios/PoseTrackerVision/PoseTrackerBodyPosePlugin.m +18 -0
- package/ios/PoseTrackerVision/PoseTrackerBodyPosePlugin.swift +108 -0
- package/lib/PoseTrackerProvider.d.ts +102 -0
- package/lib/PoseTrackerProvider.js +204 -0
- package/lib/api/configure.d.ts +29 -0
- package/lib/api/configure.js +64 -0
- package/lib/api/skeleton.d.ts +17 -0
- package/lib/api/skeleton.js +53 -0
- package/lib/api/track.d.ts +61 -0
- package/lib/api/track.js +146 -0
- package/lib/backends/PoseBackend.d.ts +50 -0
- package/lib/backends/PoseBackend.js +11 -0
- package/lib/backends/vision/VisionPoseBackend.d.ts +72 -0
- package/lib/backends/vision/VisionPoseBackend.js +177 -0
- package/lib/backends/vision/mapVisionJoints.d.ts +35 -0
- package/lib/backends/vision/mapVisionJoints.js +162 -0
- package/lib/backends/vision/optionalVision.d.ts +32 -0
- package/lib/backends/vision/optionalVision.js +76 -0
- package/lib/backends/webview/WebViewPoseBackend.d.ts +223 -0
- package/lib/backends/webview/WebViewPoseBackend.js +382 -0
- package/lib/backends/webview/brandAssets.d.ts +5 -0
- package/lib/backends/webview/brandAssets.js +8 -0
- package/lib/backends/webview/onlineRuntime.d.ts +45 -0
- package/lib/backends/webview/onlineRuntime.js +57 -0
- package/lib/backends/webview/poseHtml.d.ts +71 -0
- package/lib/backends/webview/poseHtml.js +188 -0
- package/lib/backends/webview/poseRuntimeSource.d.ts +3 -0
- package/lib/backends/webview/poseRuntimeSource.js +4 -0
- package/lib/cache/obfuscate.d.ts +18 -0
- package/lib/cache/obfuscate.js +90 -0
- package/lib/camera/PoseCameraView.d.ts +51 -0
- package/lib/camera/PoseCameraView.js +99 -0
- package/lib/camera/WebViewPoseView.d.ts +81 -0
- package/lib/camera/WebViewPoseView.js +312 -0
- package/lib/client.d.ts +361 -0
- package/lib/client.js +1046 -0
- package/lib/diagnostics/logReport.d.ts +23 -0
- package/lib/diagnostics/logReport.js +102 -0
- package/lib/engine/EngineLoader.d.ts +95 -0
- package/lib/engine/EngineLoader.js +347 -0
- package/lib/engine/types.d.ts +83 -0
- package/lib/engine/types.js +11 -0
- package/lib/events/classicMessage.d.ts +31 -0
- package/lib/events/classicMessage.js +259 -0
- package/lib/exercises/aliases.d.ts +18 -0
- package/lib/exercises/aliases.js +48 -0
- package/lib/index.d.ts +72 -0
- package/lib/index.js +170 -0
- package/lib/models/poseModels.d.ts +39 -0
- package/lib/models/poseModels.js +63 -0
- package/lib/quality/AdaptiveQualityController.d.ts +120 -0
- package/lib/quality/AdaptiveQualityController.js +423 -0
- package/lib/quality/RuntimeGuard.d.ts +34 -0
- package/lib/quality/RuntimeGuard.js +105 -0
- package/lib/quality/captureMode.d.ts +82 -0
- package/lib/quality/captureMode.js +75 -0
- package/lib/quality/deviceCapability.d.ts +37 -0
- package/lib/quality/deviceCapability.js +177 -0
- package/lib/quality/profiles.d.ts +125 -0
- package/lib/quality/profiles.js +202 -0
- package/lib/runtime/RuntimeCache.d.ts +75 -0
- package/lib/runtime/RuntimeCache.js +230 -0
- package/lib/sdkVersion.d.ts +2 -0
- package/lib/sdkVersion.js +5 -0
- package/lib/support/optionalModules.d.ts +46 -0
- package/lib/support/optionalModules.js +82 -0
- package/lib/types/acceleration.d.ts +86 -0
- package/lib/types/acceleration.js +15 -0
- package/lib/types/events.d.ts +345 -0
- package/lib/types/events.js +10 -0
- package/lib/types/features.d.ts +90 -0
- package/lib/types/features.js +122 -0
- package/lib/types/manifest.d.ts +197 -0
- package/lib/types/manifest.js +10 -0
- package/lib/types/pose.d.ts +36 -0
- package/lib/types/pose.js +41 -0
- package/lib/types/preload.d.ts +17 -0
- package/lib/types/preload.js +10 -0
- package/lib/types/skeleton.d.ts +29 -0
- package/lib/types/skeleton.js +52 -0
- package/package.json +92 -0
- package/react-native.config.js +12 -0
- package/src/PoseTrackerProvider.tsx +332 -0
- package/src/api/configure.ts +82 -0
- package/src/api/skeleton.ts +60 -0
- package/src/api/track.ts +182 -0
- package/src/backends/PoseBackend.ts +59 -0
- package/src/backends/vision/VisionPoseBackend.ts +238 -0
- package/src/backends/vision/mapVisionJoints.ts +185 -0
- package/src/backends/vision/optionalVision.ts +94 -0
- package/src/backends/webview/WebViewPoseBackend.ts +582 -0
- package/src/backends/webview/brandAssets.ts +5 -0
- package/src/backends/webview/onlineRuntime.ts +94 -0
- package/src/backends/webview/poseHtml.ts +259 -0
- package/src/backends/webview/poseRuntimeSource.d.ts +3 -0
- package/src/cache/obfuscate.ts +94 -0
- package/src/camera/PoseCameraView.tsx +223 -0
- package/src/camera/WebViewPoseView.tsx +466 -0
- package/src/client.ts +1295 -0
- package/src/diagnostics/logReport.ts +131 -0
- package/src/engine/EngineLoader.ts +428 -0
- package/src/engine/types.ts +93 -0
- package/src/events/classicMessage.ts +271 -0
- package/src/exercises/aliases.ts +50 -0
- package/src/index.ts +243 -0
- package/src/models/poseModels.ts +93 -0
- package/src/quality/AdaptiveQualityController.ts +579 -0
- package/src/quality/RuntimeGuard.ts +119 -0
- package/src/quality/captureMode.ts +97 -0
- package/src/quality/deviceCapability.ts +187 -0
- package/src/quality/profiles.ts +253 -0
- package/src/runtime/RuntimeCache.ts +300 -0
- package/src/sdkVersion.ts +2 -0
- package/src/support/optionalModules.ts +114 -0
- package/src/types/acceleration.ts +94 -0
- package/src/types/events.ts +437 -0
- package/src/types/features.ts +176 -0
- package/src/types/manifest.ts +230 -0
- package/src/types/pose.ts +85 -0
- package/src/types/preload.ts +19 -0
- package/src/types/skeleton.ts +77 -0
|
@@ -0,0 +1,300 @@
|
|
|
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
|
+
|
|
15
|
+
import { sha256 } from 'js-sha256';
|
|
16
|
+
|
|
17
|
+
import type { PoseRuntimeDescriptor } from '../types/manifest';
|
|
18
|
+
import {
|
|
19
|
+
createMemoryFileStore,
|
|
20
|
+
createNativeFileStore,
|
|
21
|
+
type FileStore,
|
|
22
|
+
} from '../engine/EngineLoader';
|
|
23
|
+
|
|
24
|
+
/** Part names, as declared by the backend runtime manifest. */
|
|
25
|
+
const PART_NAMES = ['tfjs', 'tfjs-wasm', 'model', 'weights', 'pipeline', 'runtime'] as const;
|
|
26
|
+
type PartName = (typeof PART_NAMES)[number];
|
|
27
|
+
|
|
28
|
+
/** Required parts — `pipeline` is optional (JS fallback lives in the runtime part). */
|
|
29
|
+
const REQUIRED_PARTS: PartName[] = ['tfjs', 'tfjs-wasm', 'model', 'weights', 'runtime'];
|
|
30
|
+
|
|
31
|
+
const CURRENT_KEY = 'current.json';
|
|
32
|
+
|
|
33
|
+
interface CurrentPointer {
|
|
34
|
+
version: string;
|
|
35
|
+
parts: Partial<Record<PartName, { key: string; sha256: string }>>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Decoded, ready-to-inject runtime parts. */
|
|
39
|
+
export interface PoseRuntimeParts {
|
|
40
|
+
version: string;
|
|
41
|
+
/** TF.js bundle (core + converter + webgl + wasm backends). */
|
|
42
|
+
tfjsJs: string;
|
|
43
|
+
/** XNNPACK wasm binaries (base64). */
|
|
44
|
+
tfjsWasmB64: { plain: string; simd: string; threadedSimd: string };
|
|
45
|
+
/** MoveNet graph model topology (raw model.json). */
|
|
46
|
+
modelJson: string;
|
|
47
|
+
/** Weight shards (base64, manifest order). */
|
|
48
|
+
weightsB64: string[];
|
|
49
|
+
/** Proprietary pose pipeline wasm (base64), null when not deployed. */
|
|
50
|
+
pipelineWasmB64: string | null;
|
|
51
|
+
/** Page runtime (camera, presets, inference loop, events). */
|
|
52
|
+
runtimeJs: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RuntimeCacheProgress {
|
|
56
|
+
part: string;
|
|
57
|
+
completedParts: number;
|
|
58
|
+
totalParts: number;
|
|
59
|
+
/** Bytes of the part being downloaded (from the manifest). */
|
|
60
|
+
partBytes: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface RuntimeCacheOptions {
|
|
64
|
+
fileStore?: FileStore | null;
|
|
65
|
+
/** Injectable for tests. */
|
|
66
|
+
fetchFn?: typeof fetch;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class RuntimeCache {
|
|
70
|
+
private readonly files: FileStore | null;
|
|
71
|
+
private readonly fetchFn: typeof fetch;
|
|
72
|
+
private memoryParts: PoseRuntimeParts | null = null;
|
|
73
|
+
/** True when no persistent FS module is installed (memory-only fallback). */
|
|
74
|
+
readonly persistent: boolean;
|
|
75
|
+
|
|
76
|
+
constructor(options: RuntimeCacheOptions = {}) {
|
|
77
|
+
let files =
|
|
78
|
+
options.fileStore !== undefined
|
|
79
|
+
? options.fileStore
|
|
80
|
+
: createNativeFileStore('posetracker-runtime');
|
|
81
|
+
this.persistent = files !== null;
|
|
82
|
+
if (files === null) {
|
|
83
|
+
// Bare RN without any filesystem lib: stay functional with an
|
|
84
|
+
// in-memory store — online works, but every cold start re-downloads.
|
|
85
|
+
files = createMemoryFileStore();
|
|
86
|
+
}
|
|
87
|
+
this.files = files;
|
|
88
|
+
this.fetchFn = options.fetchFn ?? fetch;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Version currently committed to the cache, or null when cold/empty. */
|
|
92
|
+
async getCachedVersion(): Promise<string | null> {
|
|
93
|
+
const current = await this.readCurrent();
|
|
94
|
+
return current?.version ?? null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Ensure the cache holds the version described by the manifest. Downloads
|
|
99
|
+
* only what is missing/outdated; every part is checksum-verified BEFORE
|
|
100
|
+
* being committed. The previous version keeps serving until the new one is
|
|
101
|
+
* fully committed (atomic flip of `current.json`, then best-effort GC).
|
|
102
|
+
*
|
|
103
|
+
* Returns true when the cache is warm (either already up-to-date or
|
|
104
|
+
* successfully updated); false when it could not be warmed (offline...) —
|
|
105
|
+
* the caller decides whether a stale/absent cache is acceptable.
|
|
106
|
+
*/
|
|
107
|
+
async warm(
|
|
108
|
+
descriptor: PoseRuntimeDescriptor | null,
|
|
109
|
+
onProgress?: (progress: RuntimeCacheProgress) => void,
|
|
110
|
+
): Promise<boolean> {
|
|
111
|
+
if (!this.files) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
const current = await this.readCurrent();
|
|
115
|
+
if (!descriptor) {
|
|
116
|
+
// Offline handshake: the existing cache (if any) is served as-is.
|
|
117
|
+
return current !== null;
|
|
118
|
+
}
|
|
119
|
+
if (current && current.version === descriptor.version) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const wanted = Object.entries(descriptor.parts) as Array<
|
|
124
|
+
[string, { url: string; sha256: string; bytes: number }]
|
|
125
|
+
>;
|
|
126
|
+
const next: CurrentPointer = { version: descriptor.version, parts: {} };
|
|
127
|
+
let completed = 0;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
for (const [name, part] of wanted) {
|
|
131
|
+
if (!this.isKnownPart(name)) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
onProgress?.({
|
|
135
|
+
part: name,
|
|
136
|
+
completedParts: completed,
|
|
137
|
+
totalParts: wanted.length,
|
|
138
|
+
partBytes: part.bytes,
|
|
139
|
+
});
|
|
140
|
+
const key = `${name}@${descriptor.version}`;
|
|
141
|
+
|
|
142
|
+
// Reuse without network when the exact content is already cached
|
|
143
|
+
// (same key from an interrupted warm, or same sha in the previous
|
|
144
|
+
// version under another key).
|
|
145
|
+
let content = await this.readVerified(key, part.sha256);
|
|
146
|
+
if (content === null) {
|
|
147
|
+
const previous = current?.parts[name];
|
|
148
|
+
if (previous && previous.sha256 === part.sha256) {
|
|
149
|
+
content = await this.readVerified(previous.key, part.sha256);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (content === null) {
|
|
153
|
+
const response = await this.fetchFn(part.url);
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
throw new Error(`pose-runtime part '${name}' HTTP ${response.status}`);
|
|
156
|
+
}
|
|
157
|
+
content = await response.text();
|
|
158
|
+
// Integrity gate BEFORE commit: a truncated/corrupted download
|
|
159
|
+
// never reaches the cache.
|
|
160
|
+
if (sha256(content) !== part.sha256) {
|
|
161
|
+
throw new Error(`pose-runtime part '${name}' failed sha256 verification`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
await this.files.write(key, content);
|
|
165
|
+
next.parts[name as PartName] = { key, sha256: part.sha256 };
|
|
166
|
+
completed += 1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const required of REQUIRED_PARTS) {
|
|
170
|
+
if (!next.parts[required]) {
|
|
171
|
+
throw new Error(`pose-runtime manifest is missing required part '${required}'`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Commit point: flip the pointer only once every part is verified.
|
|
176
|
+
await this.files.write(CURRENT_KEY, JSON.stringify(next));
|
|
177
|
+
this.memoryParts = null;
|
|
178
|
+
|
|
179
|
+
// Best-effort GC of the previous version's files.
|
|
180
|
+
if (current) {
|
|
181
|
+
for (const entry of Object.values(current.parts)) {
|
|
182
|
+
if (entry && !Object.values(next.parts).some((p) => p && p.key === entry.key)) {
|
|
183
|
+
await this.files.remove(entry.key).catch(() => {});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
} catch {
|
|
189
|
+
// Warm failed: the previous committed version stays intact.
|
|
190
|
+
return current !== null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Load (and decode) the committed runtime parts; null when cache is cold. */
|
|
195
|
+
async load(): Promise<PoseRuntimeParts | null> {
|
|
196
|
+
if (this.memoryParts) {
|
|
197
|
+
return this.memoryParts;
|
|
198
|
+
}
|
|
199
|
+
if (!this.files) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
const current = await this.readCurrent();
|
|
203
|
+
if (!current) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const read = async (name: PartName): Promise<string | null> => {
|
|
208
|
+
const entry = current.parts[name];
|
|
209
|
+
if (!entry) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
return this.readVerified(entry.key, entry.sha256);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const [tfjsJs, tfjsWasmRaw, modelJson, weightsRaw, pipelineWasmB64, runtimeJs] =
|
|
216
|
+
await Promise.all([
|
|
217
|
+
read('tfjs'),
|
|
218
|
+
read('tfjs-wasm'),
|
|
219
|
+
read('model'),
|
|
220
|
+
read('weights'),
|
|
221
|
+
read('pipeline'),
|
|
222
|
+
read('runtime'),
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
if (!tfjsJs || !tfjsWasmRaw || !modelJson || !weightsRaw || !runtimeJs) {
|
|
226
|
+
// A part is missing or failed integrity: treat the cache as cold.
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
this.memoryParts = {
|
|
232
|
+
version: current.version,
|
|
233
|
+
tfjsJs,
|
|
234
|
+
tfjsWasmB64: JSON.parse(tfjsWasmRaw) as PoseRuntimeParts['tfjsWasmB64'],
|
|
235
|
+
modelJson,
|
|
236
|
+
weightsB64: JSON.parse(weightsRaw) as string[],
|
|
237
|
+
pipelineWasmB64: pipelineWasmB64 ?? null,
|
|
238
|
+
runtimeJs,
|
|
239
|
+
};
|
|
240
|
+
return this.memoryParts;
|
|
241
|
+
} catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Wipe the whole runtime cache (revocation / recovery). */
|
|
247
|
+
async purge(): Promise<void> {
|
|
248
|
+
this.memoryParts = null;
|
|
249
|
+
if (!this.files) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const current = await this.readCurrent();
|
|
253
|
+
if (current) {
|
|
254
|
+
for (const entry of Object.values(current.parts)) {
|
|
255
|
+
if (entry) {
|
|
256
|
+
await this.files.remove(entry.key).catch(() => {});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
await this.files.remove(CURRENT_KEY).catch(() => {});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// -------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
private isKnownPart(name: string): name is PartName {
|
|
266
|
+
return (PART_NAMES as readonly string[]).includes(name);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private async readCurrent(): Promise<CurrentPointer | null> {
|
|
270
|
+
try {
|
|
271
|
+
const raw = await this.files?.read(CURRENT_KEY);
|
|
272
|
+
if (!raw) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
const parsed = JSON.parse(raw) as CurrentPointer;
|
|
276
|
+
if (!parsed.version || !parsed.parts) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
return parsed;
|
|
280
|
+
} catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private async readVerified(key: string, expectedSha: string): Promise<string | null> {
|
|
286
|
+
try {
|
|
287
|
+
const content = await this.files?.read(key);
|
|
288
|
+
if (content === null || content === undefined) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
if (sha256(content) !== expectedSha) {
|
|
292
|
+
await this.files?.remove(key).catch(() => {});
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
return content;
|
|
296
|
+
} catch {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
declare function require(name: string): unknown;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Expo Go has no vision-camera native binaries. Requiring them there throws
|
|
21
|
+
* (and React Native LogBox still surfaces it even when wrapped in
|
|
22
|
+
* try/catch). Detect Expo Go *before* any of those requires.
|
|
23
|
+
*/
|
|
24
|
+
export function isExpoGo(): boolean {
|
|
25
|
+
try {
|
|
26
|
+
// Avoid a hard dependency on expo-constants: same signal Nitro uses.
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
|
+
const { NativeModules } = require('react-native') as {
|
|
29
|
+
NativeModules: {
|
|
30
|
+
NativeUnimoduleProxy?: {
|
|
31
|
+
modulesConstants?: { ExponentConstants?: { appOwnership?: string } };
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
return (
|
|
36
|
+
NativeModules.NativeUnimoduleProxy?.modulesConstants?.ExponentConstants
|
|
37
|
+
?.appOwnership === 'expo'
|
|
38
|
+
);
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Structural typings (subset actually consumed by the SDK)
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/** react-native-vision-camera 4.x — subset used by PoseCameraView. */
|
|
49
|
+
export interface VisionCameraFrame {
|
|
50
|
+
width: number;
|
|
51
|
+
height: number;
|
|
52
|
+
orientation: string; // 'portrait' | 'portrait-upside-down' | 'landscape-left' | 'landscape-right'
|
|
53
|
+
timestamp: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface VisionCameraModule {
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
Camera: any; // React component class
|
|
59
|
+
useCameraDevice(position: 'front' | 'back'): unknown | undefined;
|
|
60
|
+
useCameraPermission(): { hasPermission: boolean; requestPermission: () => Promise<boolean> };
|
|
61
|
+
useFrameProcessor(processor: (frame: VisionCameraFrame) => void, deps: unknown[]): unknown;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** react-native-worklets-core 1.x — subset used by PoseCameraView. */
|
|
65
|
+
export interface WorkletsCoreModule {
|
|
66
|
+
useSharedValue<T>(initial: T): { value: T };
|
|
67
|
+
useRunOnJS<A extends unknown[]>(fn: (...args: A) => void, deps: unknown[]): (...args: A) => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Guarded requires (cached; `undefined` = not probed yet, `null` = missing)
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
// NOTE: every `require` below uses a STRING LITERAL — Metro cannot resolve
|
|
75
|
+
// dynamic `require(variable)` calls and would fail the build.
|
|
76
|
+
|
|
77
|
+
let visionCamera: VisionCameraModule | null | undefined;
|
|
78
|
+
let workletsCore: WorkletsCoreModule | null | undefined;
|
|
79
|
+
|
|
80
|
+
export function getVisionCamera(): VisionCameraModule | null {
|
|
81
|
+
if (visionCamera === undefined) {
|
|
82
|
+
if (isExpoGo()) {
|
|
83
|
+
visionCamera = null;
|
|
84
|
+
} else {
|
|
85
|
+
try {
|
|
86
|
+
visionCamera = require('react-native-vision-camera') as VisionCameraModule;
|
|
87
|
+
} catch {
|
|
88
|
+
visionCamera = null;
|
|
89
|
+
}
|
|
90
|
+
if (visionCamera && typeof visionCamera.useFrameProcessor !== 'function') {
|
|
91
|
+
visionCamera = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return visionCamera;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getWorkletsCore(): WorkletsCoreModule | null {
|
|
99
|
+
if (workletsCore === undefined) {
|
|
100
|
+
if (isExpoGo()) {
|
|
101
|
+
workletsCore = null;
|
|
102
|
+
} else {
|
|
103
|
+
try {
|
|
104
|
+
workletsCore = require('react-native-worklets-core') as WorkletsCoreModule;
|
|
105
|
+
} catch {
|
|
106
|
+
workletsCore = null;
|
|
107
|
+
}
|
|
108
|
+
if (workletsCore && typeof workletsCore.useRunOnJS !== 'function') {
|
|
109
|
+
workletsCore = null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return workletsCore;
|
|
114
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GPU-acceleration status types.
|
|
3
|
+
*
|
|
4
|
+
* Historical context (see docs/ANDROID_GL_ACCELERATION.md): on Android the
|
|
5
|
+
* previous product generation had to run TF.js inside a WebView because it
|
|
6
|
+
* was the only reliable way to get a working WebGL backend. The native path
|
|
7
|
+
* (tfjs-react-native over expo-gl) can silently degrade — the platform
|
|
8
|
+
* adapter *shims* float-texture extension queries, so TF.js may believe the
|
|
9
|
+
* GPU supports float32 render targets when it does not, and expo-gl contexts
|
|
10
|
+
* die on Android when the surface is backgrounded. These types exist so the
|
|
11
|
+
* SDK reports an explicit acceleration verdict instead of failing silently
|
|
12
|
+
* into a 1-2 fps CPU fallback.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* - 'unknown': warm-up has not completed yet.
|
|
17
|
+
* - 'gpu': rn-webgl (expo-gl) active and the warm-up health check
|
|
18
|
+
* produced sane outputs within the latency budget.
|
|
19
|
+
* - 'cpu-fallback': inference works but is NOT GPU-accelerated (rn-webgl
|
|
20
|
+
* failed to initialize, produced NaN/garbage, or was
|
|
21
|
+
* pathologically slow — i.e. software rendering).
|
|
22
|
+
* - 'unavailable': no backend could run the model at all (fatal; the
|
|
23
|
+
* client also reports status 'error').
|
|
24
|
+
*/
|
|
25
|
+
export type AccelerationState = 'unknown' | 'gpu' | 'cpu-fallback' | 'unavailable';
|
|
26
|
+
|
|
27
|
+
/** Raw GL context facts, gathered best-effort after backend init. */
|
|
28
|
+
export interface GlCapabilities {
|
|
29
|
+
/** e.g. "OpenGL ES 3.0 ..." */
|
|
30
|
+
glVersion: string | null;
|
|
31
|
+
/** e.g. "Adreno (TM) 640", "Mali-G78", "PowerVR ..." — key for triage. */
|
|
32
|
+
renderer: string | null;
|
|
33
|
+
vendor: string | null;
|
|
34
|
+
maxTextureSize: number | null;
|
|
35
|
+
/**
|
|
36
|
+
* Extension presence *as reported to TF.js*. WARNING: on rn-webgl the
|
|
37
|
+
* tfjs-react-native platform adapter shims `getExtension` and always
|
|
38
|
+
* reports EXT_color_buffer_float (Android) / EXT_color_buffer_half_float
|
|
39
|
+
* as present, so `true` here does NOT prove hardware support. That lie is
|
|
40
|
+
* exactly why the SDK forces f16 textures on Android and validates real
|
|
41
|
+
* outputs in the health check.
|
|
42
|
+
*/
|
|
43
|
+
colorBufferFloat: boolean;
|
|
44
|
+
colorBufferHalfFloat: boolean;
|
|
45
|
+
textureFloat: boolean;
|
|
46
|
+
textureHalfFloat: boolean;
|
|
47
|
+
/** True when the extension answers above come from the shimmed adapter. */
|
|
48
|
+
extensionQueriesShimmed: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Snapshot of the TF.js WebGL flags that matter for Android stability. */
|
|
52
|
+
export type TfjsFlagSnapshot = Record<string, boolean | number | string | null>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Inference runtime that produced the acceleration verdict:
|
|
56
|
+
* - 'webview': TF.js MoveNet inside a Chromium/WKWebView (ANGLE WebGL) —
|
|
57
|
+
* the SDK's base runtime, offline, same stack as the
|
|
58
|
+
* PoseTracker WebView product.
|
|
59
|
+
* - 'vision': Apple Vision (`VNDetectHumanBodyPoseRequest`) — iOS only.
|
|
60
|
+
*/
|
|
61
|
+
export type InferenceRuntime = 'webview' | 'vision';
|
|
62
|
+
|
|
63
|
+
/** Native accelerator label (Apple Vision). */
|
|
64
|
+
export type VisionDelegateLabel = 'apple-vision';
|
|
65
|
+
|
|
66
|
+
export type NativeDelegateLabel = VisionDelegateLabel;
|
|
67
|
+
|
|
68
|
+
export interface AccelerationDiagnostics {
|
|
69
|
+
state: AccelerationState;
|
|
70
|
+
/** Active TF.js backend after init/fallbacks: 'rn-webgl' | 'cpu' | null. */
|
|
71
|
+
tfjsBackend: string | null;
|
|
72
|
+
/** Which runtime produced this verdict (absent = 'tfjs', pre-TFLite SDKs). */
|
|
73
|
+
runtime?: InferenceRuntime;
|
|
74
|
+
/** Active native accelerator; null/absent for the TF.js backend. */
|
|
75
|
+
delegate?: NativeDelegateLabel | null;
|
|
76
|
+
/** Median duration of the timed warm-up inferences, in ms. */
|
|
77
|
+
medianInferenceMs: number | null;
|
|
78
|
+
/** Individual timed warm-up runs, in ms (first run includes shader compilation). */
|
|
79
|
+
inferenceTimesMs: number[];
|
|
80
|
+
/** Latency budget used by the health check to accept the GPU path. */
|
|
81
|
+
maxAcceptableInferenceMs: number;
|
|
82
|
+
capabilities: GlCapabilities | null;
|
|
83
|
+
flags: TfjsFlagSnapshot;
|
|
84
|
+
/** Number of GL context losses recovered since init (Android lifecycle). */
|
|
85
|
+
contextLossCount: number;
|
|
86
|
+
/**
|
|
87
|
+
* Human-readable trail of every downgrade / recovery decision, e.g.
|
|
88
|
+
* "rn-webgl produced NaN outputs — falling back to CPU".
|
|
89
|
+
*/
|
|
90
|
+
reasons: string[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Callback for host apps that want SDK diagnostics in their own logger. */
|
|
94
|
+
export type DiagnosticListener = (message: string) => void;
|