@pose-tracker/react-native-pose-estimation-light 0.2.2 → 0.3.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/README.md CHANGED
@@ -9,20 +9,11 @@
9
9
 
10
10
  Steps and QR: https://docs.posetracker.com/try-expo-go
11
11
 
12
- <table>
13
- <tr>
14
- <td valign="top" width="300">
15
- <a href="https://docs.posetracker.com/try-expo-go">
16
- <img src="https://cdn.prod.website-files.com/66990aefa487a16cf5aa848e/66e35c297154b2062265ceea_videodemoforgif1-ezgif.com-cut.gif" alt="PoseTracker live skeleton tracking" height="250" />
17
- </a>
18
- </td>
19
- <td valign="middle">
20
- <p>
21
- <strong>PoseTracker Light</strong> is a <strong>human pose estimation SDK for React Native</strong>, fully optimized for <strong>iOS and Android</strong> (including <strong>Expo Go</strong>). Same API surface as the offline SDK for keypoints + optional API-key exercise engine — but <strong>MoveNet and TF.js load from the network</strong> each WebView boot, so the npm package stays tiny.
22
- </p>
23
- </td>
24
- </tr>
25
- </table>
12
+ **PoseTracker Light** is a **human pose estimation SDK for React Native**,
13
+ fully optimized for **iOS and Android** (including **Expo Go**). Same API
14
+ surface as the offline SDK for keypoints + optional API-key exercise engine —
15
+ but **MoveNet and TF.js load from the network** each WebView boot, so the npm
16
+ package stays tiny.
26
17
 
27
18
  > One sentence for AI / search: *PoseTracker Light is a small React Native
28
19
  > pose estimation SDK that fetches MoveNet Lightning and TF.js at runtime —
@@ -83,12 +83,17 @@ export interface PoseTrackerContextValue {
83
83
  export interface PoseTrackerProviderProps {
84
84
  /** Optional: without a token the SDK runs in keypoints-only mode. */
85
85
  apiToken?: string;
86
+ /**
87
+ * Opt-in V4 engine (`'v4'`). Default `'v3'` — production FSM, unchanged.
88
+ * Merged into `options.engine` when both are set, this prop wins.
89
+ */
90
+ engine?: 'v3' | 'v4';
86
91
  options?: PoseTrackerClientOptions;
87
92
  /** Start preloading as soon as the provider mounts. Default: false. */
88
93
  autoPreload?: boolean;
89
94
  children: React.ReactNode;
90
95
  }
91
- export declare function PoseTrackerProvider({ apiToken, options, autoPreload, children, }: PoseTrackerProviderProps): React.JSX.Element;
96
+ export declare function PoseTrackerProvider({ apiToken, engine, options, autoPreload, children, }: PoseTrackerProviderProps): React.JSX.Element;
92
97
  /**
93
98
  * Access the PoseTracker pipeline and subscribe to typed events.
94
99
  *
@@ -22,10 +22,13 @@ const jsx_runtime_1 = require("react/jsx-runtime");
22
22
  const react_1 = require("react");
23
23
  const client_1 = require("./client");
24
24
  const PoseTrackerContext = (0, react_1.createContext)(null);
25
- function PoseTrackerProvider({ apiToken, options, autoPreload = false, children, }) {
25
+ function PoseTrackerProvider({ apiToken, engine, options, autoPreload = false, children, }) {
26
26
  const clientRef = (0, react_1.useRef)(null);
27
27
  if (!clientRef.current) {
28
- clientRef.current = new client_1.PoseTrackerClient(apiToken, options);
28
+ clientRef.current = new client_1.PoseTrackerClient(apiToken, {
29
+ ...options,
30
+ engine: engine ?? options?.engine,
31
+ });
29
32
  }
30
33
  const client = clientRef.current;
31
34
  const [status, setStatus] = (0, react_1.useState)(client.getStatus());
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { SDK_VERSION } from '../sdkVersion';
13
13
  import type { ConfigureRequest, PoseModelProfile, SdkManifest } from '../types/manifest';
14
+ import type { EngineChannel } from '../engineChannel';
14
15
  export declare const SDK_NAME: "posetracker-rn-light";
15
16
  export { SDK_VERSION };
16
17
  /** Overridable for staging/self-hosted backends. */
@@ -21,6 +22,11 @@ export interface ConfigureOptions {
21
22
  locale?: string;
22
23
  /** Local cache versions, so the backend can answer `upToDate` per module. */
23
24
  localVersions?: ConfigureRequest['localVersions'];
25
+ /**
26
+ * Opt-in V4 heuristic engine. Default `'v3'` (production FSM).
27
+ * Set `'v4'` at init — the handshake downloads `engine-v4.bundle.js`.
28
+ */
29
+ engine?: EngineChannel;
24
30
  }
25
31
  export declare class ConfigureError extends Error {
26
32
  readonly code: 'invalid_token' | 'quota_exceeded' | 'network' | 'internal';
@@ -37,6 +37,7 @@ async function configure(apiToken, options = {}) {
37
37
  poseModelProfile: options.poseModelProfile ?? 'AdaptiveChoice',
38
38
  locale: options.locale,
39
39
  localVersions: options.localVersions,
40
+ engineChannel: options.engine === 'v4' ? 'v4' : 'v3',
40
41
  };
41
42
  let response;
42
43
  try {
package/lib/client.d.ts CHANGED
@@ -28,6 +28,7 @@ import { EngineLoader, type EngineLoadResult, type FileStore } from './engine/En
28
28
  import { type OnlineRuntimeParts } from './backends/webview/onlineRuntime';
29
29
  import type { CustomExerciseDescriptor } from './engine/types';
30
30
  import type { PoseBackend, PoseInputFrame } from './backends/PoseBackend';
31
+ import { type EngineChannel } from './engineChannel';
31
32
  import type { ExerciseConfig, SdkManifest } from './types/manifest';
32
33
  import type { ColdStartMode, PreloadOptions } from './types/preload';
33
34
  import type { SkeletonDefinition } from './types/skeleton';
@@ -326,6 +327,8 @@ export declare class PoseTrackerClient {
326
327
  private validateRequestedFeatures;
327
328
  private resolveModel;
328
329
  getAvailableExercises(): ExerciseConfig[];
330
+ getEngineChannel(): EngineChannel;
331
+ private v4ExerciseConfig;
329
332
  /**
330
333
  * Custom exercises shipped inside the engine bundle (jump_analysis,
331
334
  * air_time_jump — WebView `customHandlers.js` parity). Empty in
@@ -333,6 +336,7 @@ export declare class PoseTrackerClient {
333
336
  */
334
337
  getAvailableCustomExercises(): CustomExerciseDescriptor[];
335
338
  startExercise(exerciseId: string, options?: StartExerciseOptions): void;
339
+ private beginEngineSession;
336
340
  /** Custom engine session (jump_analysis / air_time_jump). */
337
341
  private startCustomExercise;
338
342
  /**
package/lib/client.js CHANGED
@@ -34,6 +34,7 @@ const obfuscate_1 = require("./cache/obfuscate");
34
34
  const VisionPoseBackend_1 = require("./backends/vision/VisionPoseBackend");
35
35
  const WebViewPoseBackend_1 = require("./backends/webview/WebViewPoseBackend");
36
36
  const aliases_1 = require("./exercises/aliases");
37
+ const engineChannel_1 = require("./engineChannel");
37
38
  const skeleton_1 = require("./api/skeleton");
38
39
  const logReport_1 = require("./diagnostics/logReport");
39
40
  const AdaptiveQualityController_1 = require("./quality/AdaptiveQualityController");
@@ -755,7 +756,32 @@ class PoseTrackerClient {
755
756
  // Exercise sessions (full-engine mode only)
756
757
  // -------------------------------------------------------------------------
757
758
  getAvailableExercises() {
758
- return this.mode === 'full-engine' ? this.manifest?.exercises ?? [] : [];
759
+ if (this.mode !== 'full-engine')
760
+ return [];
761
+ if ((0, engineChannel_1.normalizeEngineChannel)(this.options.engine) === 'v4') {
762
+ const listed = this.engine?.listExercises?.() ?? [];
763
+ if (listed.length)
764
+ return listed.map((e) => this.v4ExerciseConfig(e));
765
+ }
766
+ return this.manifest?.exercises ?? [];
767
+ }
768
+ getEngineChannel() {
769
+ return (0, engineChannel_1.normalizeEngineChannel)(this.options.engine);
770
+ }
771
+ v4ExerciseConfig(entry) {
772
+ const type = entry.type === 'static' ? 'static' : 'dynamic';
773
+ return {
774
+ id: entry.id,
775
+ name: entry.displayName || entry.id,
776
+ type,
777
+ movement: {
778
+ name: entry.id,
779
+ type,
780
+ scale_acceptance: {},
781
+ movement_steps: [],
782
+ movement_initial_posture: null,
783
+ },
784
+ };
759
785
  }
760
786
  /**
761
787
  * Custom exercises shipped inside the engine bundle (jump_analysis,
@@ -798,6 +824,29 @@ class PoseTrackerClient {
798
824
  throw new Error(features_1.FREE_PLAN_FEATURES_MESSAGE);
799
825
  }
800
826
  }
827
+ if ((0, engineChannel_1.requiresEngineV4)(exerciseId) && (0, engineChannel_1.normalizeEngineChannel)(this.options.engine) !== 'v4') {
828
+ const message = `Exercise '${exerciseId}' requires engine: 'v4'`;
829
+ this.reportError({ type: 'error', code: 'invalid_exercise', message });
830
+ throw new Error(message);
831
+ }
832
+ if ((0, engineChannel_1.normalizeEngineChannel)(this.options.engine) === 'v4') {
833
+ const listed = this.engine.listExercises?.() ?? [];
834
+ const v4Hit = listed.find((e) => e.id === exerciseId);
835
+ if (v4Hit) {
836
+ this.beginEngineSession(this.v4ExerciseConfig(v4Hit), options);
837
+ return;
838
+ }
839
+ const customs = this.getAvailableCustomExercises();
840
+ const custom = customs.find((e) => e.id === exerciseId) ??
841
+ (0, aliases_1.findExerciseByIdOrAlias)(exerciseId, customs);
842
+ if (custom) {
843
+ this.startCustomExercise(custom, options);
844
+ return;
845
+ }
846
+ const message = `Exercise '${exerciseId}' is not available in V4 engine`;
847
+ this.reportError({ type: 'error', code: 'invalid_exercise', message });
848
+ throw new Error(message);
849
+ }
801
850
  const available = this.getAvailableExercises();
802
851
  const exercise = (0, aliases_1.findExerciseByIdOrAlias)(exerciseId, available);
803
852
  if (!exercise) {
@@ -815,8 +864,13 @@ class PoseTrackerClient {
815
864
  this.reportError({ type: 'error', code: 'invalid_exercise', message });
816
865
  throw new Error(message);
817
866
  }
867
+ this.beginEngineSession(exercise, options);
868
+ }
869
+ beginEngineSession(exercise, options) {
870
+ if (!this.engine)
871
+ return;
818
872
  this.stopExercise();
819
- this.currentExerciseId = exerciseId;
873
+ this.currentExerciseId = exercise.id;
820
874
  this.keypointsSuppressionLogged = false;
821
875
  const debug = options.debug ?? this.options.debugEngine === true;
822
876
  this.session = this.engine.createSession({
@@ -78,6 +78,13 @@ export interface PoseTrackerEngine {
78
78
  createSession(options: EngineSessionOptions, emit: EventSink): EngineSession;
79
79
  /** Engine >= 1.2.0: custom exercises shipped in the bundle (jump analysis…). */
80
80
  listCustomExercises?(): CustomExerciseDescriptor[];
81
+ /** Engine V4 catalog (opt-in channel). Absent on V3 bundles. */
82
+ listExercises?(): Array<{
83
+ id: string;
84
+ displayName: string;
85
+ type: string;
86
+ description?: string;
87
+ }>;
81
88
  /** Engine >= 1.2.0: start a custom exercise session (placement + jump_* events). */
82
89
  createCustomSession?(options: CustomSessionOptions, emit: EventSink): EngineSession;
83
90
  }
@@ -0,0 +1,6 @@
1
+ /** Opt-in remote engine channel. Default is production V3. */
2
+ export type EngineChannel = 'v3' | 'v4';
3
+ /** Movements that exist only in the V4 catalog (not in V3 FSM / Strapi). */
4
+ export declare const V4_ONLY_EXERCISE_IDS: readonly ["shoulder_roll", "shoulder_deep_breath", "chair_forward_fold"];
5
+ export declare function normalizeEngineChannel(raw?: string | null): EngineChannel;
6
+ export declare function requiresEngineV4(exerciseId: string): boolean;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.V4_ONLY_EXERCISE_IDS = void 0;
4
+ exports.normalizeEngineChannel = normalizeEngineChannel;
5
+ exports.requiresEngineV4 = requiresEngineV4;
6
+ /** Movements that exist only in the V4 catalog (not in V3 FSM / Strapi). */
7
+ exports.V4_ONLY_EXERCISE_IDS = [
8
+ 'shoulder_roll',
9
+ 'shoulder_deep_breath',
10
+ 'chair_forward_fold',
11
+ ];
12
+ function normalizeEngineChannel(raw) {
13
+ return raw === 'v4' ? 'v4' : 'v3';
14
+ }
15
+ function requiresEngineV4(exerciseId) {
16
+ return exports.V4_ONLY_EXERCISE_IDS.includes(exerciseId);
17
+ }
package/lib/index.d.ts CHANGED
@@ -25,6 +25,8 @@ export { DEFAULT_MOVENET_LIGHTNING_URL, originFromModelUrl, resolvePoseModel, }
25
25
  export type { PoseModelAlias, PoseModelKind, ResolvePoseModelOptions, ResolvedPoseModel, } from './models/poseModels';
26
26
  export { PoseTrackerClient } from './client';
27
27
  export type { PoseTrackerClientOptions, PreferredBackend, StartExerciseOptions } from './client';
28
+ export type { EngineChannel } from './engineChannel';
29
+ export { V4_ONLY_EXERCISE_IDS, normalizeEngineChannel, requiresEngineV4 } from './engineChannel';
28
30
  export type { ColdStartMode, PreloadOptions } from './types/preload';
29
31
  export type { SkeletonAnglesStyle, SkeletonCirclesStyle, SkeletonDefinition, SkeletonLinesStyle, } from './types/skeleton';
30
32
  export { DEFAULT_SKELETON_DEFINITION } from './types/skeleton';
package/lib/index.js CHANGED
@@ -28,8 +28,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
28
28
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
29
29
  };
30
30
  Object.defineProperty(exports, "__esModule", { value: true });
31
- exports.logAccelerationReport = exports.defaultDiagnosticLogger = exports.createNativeFileStore = exports.createMemoryFileStore = exports.createBlobUtilFileStore = exports.createRnfsFileStore = exports.createExpoFileStore = exports.EngineLoader = exports.isExpoGo = exports.PoseCameraView = exports.VISION_JOINT_TO_SDK = exports.poseFromVisionPluginResult = exports.VISION_BODY_POSE_PLUGIN_NAME = exports.isVisionPoseCameraAvailable = exports.isVisionBackendAvailable = exports.VisionUnavailableError = exports.VisionPoseBackend = exports.POSETRACKER_LOGO_DATA_URL = exports.POSE_HTML_BUILD = exports.DEFAULT_LOADING_TEXT = exports.buildPoseHtml = exports.WebViewPoseView = exports.isWebViewPoseBackend = exports.WebViewPoseBackend = exports.usePoseTracker = exports.PoseTrackerProvider = exports.SkeletonFetchError = exports.fetchSkeletonDefinition = exports.DEFAULT_SKELETON_DEFINITION = exports.PoseTrackerClient = exports.resolvePoseModel = exports.originFromModelUrl = exports.DEFAULT_MOVENET_LIGHTNING_URL = exports.ONLINE_POSE_DETECTION_VERSION = exports.ONLINE_TFJS_VERSION = exports.defaultPoseDetectionCdnUrl = exports.defaultOnlineRuntimeUrls = exports.getOnlineRuntimeVersion = exports.getOnlineRuntimeParts = exports.RuntimeCache = exports.TrackError = exports.UsageTracker = exports.resolveExerciseName = exports.findExerciseByIdOrAlias = exports.EXERCISE_ALIASES = exports.SDK_VERSION = exports.SDK_NAME = exports.DEFAULT_BASE_URL = exports.ConfigureError = exports.configure = void 0;
32
- exports.resolveFeatures = exports.shouldShowWatermark = exports.isPaidPlan = exports.freeBlockedFeatures = exports.featureNotSupportedMessage = exports.COMBINED_REFERENCE_EXERCISE_MESSAGE = exports.INVALID_TOKEN_MESSAGE = exports.FREE_PLAN_FEATURES_MESSAGE = exports.DEFAULT_FEATURES = exports.RuntimeGuard = exports.scoreDeviceCapability = exports.profileFromScore = exports.isMaliRenderer = exports.ENABLE_MALI_HARD_CAP = exports.CAPTURE_CONSTRAINT_MODE = exports.ANDROID_SOFT_CAP_PROFILE = exports.ANDROID_PREPROCESS_PATH = exports.ANDROID_PERF_DEBUG = exports.ANDROID_MIN_TARGET_FPS = exports.ANDROID_INFER_FRAME_SKIP = exports.profileFromWarmupMedianMs = exports.nextLowerQualityProfile = exports.minMedianMsForMinTarget = exports.lowerQualityProfile = exports.isQualityProfileId = exports.getQualityProfile = exports.getMinTargetFps = exports.getIdealFpsRange = exports.getCriticalFpsThreshold = exports.estimatedFpsFromMedianMs = exports.currentQualityPlatform = exports.TARGET_MEDIAN_MS = exports.TARGET_FPS = exports.QUALITY_PROFILES = exports.QUALITY_LADDER = exports.MIN_TARGET_FPS_IOS = exports.MIN_TARGET_FPS_DEFAULT = exports.MIN_TARGET_FPS_ANDROID = exports.QUALITY_SETTLE_MS = exports.LOW_FPS_STREAK_BEFORE_DOWNGRADE = exports.IDEAL_FPS_RANGE_IOS = exports.IDEAL_FPS_RANGE_ANDROID = exports.CRITICAL_FPS_THRESHOLD = exports.AdaptiveQualityController = exports.toClassicNativeMessage = exports.logPlatformBanner = exports.logFrameStats = void 0;
31
+ exports.createMemoryFileStore = exports.createBlobUtilFileStore = exports.createRnfsFileStore = exports.createExpoFileStore = exports.EngineLoader = exports.isExpoGo = exports.PoseCameraView = exports.VISION_JOINT_TO_SDK = exports.poseFromVisionPluginResult = exports.VISION_BODY_POSE_PLUGIN_NAME = exports.isVisionPoseCameraAvailable = exports.isVisionBackendAvailable = exports.VisionUnavailableError = exports.VisionPoseBackend = exports.POSETRACKER_LOGO_DATA_URL = exports.POSE_HTML_BUILD = exports.DEFAULT_LOADING_TEXT = exports.buildPoseHtml = exports.WebViewPoseView = exports.isWebViewPoseBackend = exports.WebViewPoseBackend = exports.usePoseTracker = exports.PoseTrackerProvider = exports.SkeletonFetchError = exports.fetchSkeletonDefinition = exports.DEFAULT_SKELETON_DEFINITION = exports.requiresEngineV4 = exports.normalizeEngineChannel = exports.V4_ONLY_EXERCISE_IDS = exports.PoseTrackerClient = exports.resolvePoseModel = exports.originFromModelUrl = exports.DEFAULT_MOVENET_LIGHTNING_URL = exports.ONLINE_POSE_DETECTION_VERSION = exports.ONLINE_TFJS_VERSION = exports.defaultPoseDetectionCdnUrl = exports.defaultOnlineRuntimeUrls = exports.getOnlineRuntimeVersion = exports.getOnlineRuntimeParts = exports.RuntimeCache = exports.TrackError = exports.UsageTracker = exports.resolveExerciseName = exports.findExerciseByIdOrAlias = exports.EXERCISE_ALIASES = exports.SDK_VERSION = exports.SDK_NAME = exports.DEFAULT_BASE_URL = exports.ConfigureError = exports.configure = void 0;
32
+ exports.resolveFeatures = exports.shouldShowWatermark = exports.isPaidPlan = exports.freeBlockedFeatures = exports.featureNotSupportedMessage = exports.COMBINED_REFERENCE_EXERCISE_MESSAGE = exports.INVALID_TOKEN_MESSAGE = exports.FREE_PLAN_FEATURES_MESSAGE = exports.DEFAULT_FEATURES = exports.RuntimeGuard = exports.scoreDeviceCapability = exports.profileFromScore = exports.isMaliRenderer = exports.ENABLE_MALI_HARD_CAP = exports.CAPTURE_CONSTRAINT_MODE = exports.ANDROID_SOFT_CAP_PROFILE = exports.ANDROID_PREPROCESS_PATH = exports.ANDROID_PERF_DEBUG = exports.ANDROID_MIN_TARGET_FPS = exports.ANDROID_INFER_FRAME_SKIP = exports.profileFromWarmupMedianMs = exports.nextLowerQualityProfile = exports.minMedianMsForMinTarget = exports.lowerQualityProfile = exports.isQualityProfileId = exports.getQualityProfile = exports.getMinTargetFps = exports.getIdealFpsRange = exports.getCriticalFpsThreshold = exports.estimatedFpsFromMedianMs = exports.currentQualityPlatform = exports.TARGET_MEDIAN_MS = exports.TARGET_FPS = exports.QUALITY_PROFILES = exports.QUALITY_LADDER = exports.MIN_TARGET_FPS_IOS = exports.MIN_TARGET_FPS_DEFAULT = exports.MIN_TARGET_FPS_ANDROID = exports.QUALITY_SETTLE_MS = exports.LOW_FPS_STREAK_BEFORE_DOWNGRADE = exports.IDEAL_FPS_RANGE_IOS = exports.IDEAL_FPS_RANGE_ANDROID = exports.CRITICAL_FPS_THRESHOLD = exports.AdaptiveQualityController = exports.toClassicNativeMessage = exports.logPlatformBanner = exports.logFrameStats = exports.logAccelerationReport = exports.defaultDiagnosticLogger = exports.createNativeFileStore = void 0;
33
33
  // Handshake + usage tracking
34
34
  var configure_1 = require("./api/configure");
35
35
  Object.defineProperty(exports, "configure", { enumerable: true, get: function () { return configure_1.configure; } });
@@ -61,6 +61,10 @@ Object.defineProperty(exports, "resolvePoseModel", { enumerable: true, get: func
61
61
  // Orchestrator + React layer
62
62
  var client_1 = require("./client");
63
63
  Object.defineProperty(exports, "PoseTrackerClient", { enumerable: true, get: function () { return client_1.PoseTrackerClient; } });
64
+ var engineChannel_1 = require("./engineChannel");
65
+ Object.defineProperty(exports, "V4_ONLY_EXERCISE_IDS", { enumerable: true, get: function () { return engineChannel_1.V4_ONLY_EXERCISE_IDS; } });
66
+ Object.defineProperty(exports, "normalizeEngineChannel", { enumerable: true, get: function () { return engineChannel_1.normalizeEngineChannel; } });
67
+ Object.defineProperty(exports, "requiresEngineV4", { enumerable: true, get: function () { return engineChannel_1.requiresEngineV4; } });
64
68
  var skeleton_1 = require("./types/skeleton");
65
69
  Object.defineProperty(exports, "DEFAULT_SKELETON_DEFINITION", { enumerable: true, get: function () { return skeleton_1.DEFAULT_SKELETON_DEFINITION; } });
66
70
  var skeleton_2 = require("./api/skeleton");
@@ -1,2 +1,2 @@
1
1
  /** AUTO-GENERATED by scripts/sync-sdk-version.mjs — do not edit. */
2
- export declare const SDK_VERSION = "0.2.2";
2
+ export declare const SDK_VERSION = "0.3.0";
package/lib/sdkVersion.js CHANGED
@@ -2,4 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SDK_VERSION = void 0;
4
4
  /** AUTO-GENERATED by scripts/sync-sdk-version.mjs — do not edit. */
5
- exports.SDK_VERSION = '0.2.2';
5
+ exports.SDK_VERSION = '0.3.0';
@@ -126,6 +126,8 @@ export interface EngineBundleDescriptor {
126
126
  minSdkVersion: string;
127
127
  /** True when the version sent in `localVersions.engine` is already current. */
128
128
  upToDate?: boolean;
129
+ /** Which interpreter this descriptor is. Absent on older backends = v3. */
130
+ channel?: 'v3' | 'v4';
129
131
  }
130
132
  export interface PoseRuntimePartDescriptor {
131
133
  /** Download URL of this part (`GET /api/sdk/pose-runtime?part=...&v=...`). */
@@ -166,6 +168,11 @@ export interface ConfigureRequest {
166
168
  poseRuntime?: string | null;
167
169
  engine?: string | null;
168
170
  };
171
+ /**
172
+ * Which remote engine to download. Default / omitted = v3 (production).
173
+ * `v4` is opt-in (heuristic catalog: squat, shoulder_roll, …).
174
+ */
175
+ engineChannel?: 'v3' | 'v4';
169
176
  }
170
177
  export interface PlanInfo {
171
178
  plan: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pose-tracker/react-native-pose-estimation-light",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Human pose estimation SDK for React Native (light / online) — MoveNet Lightning via CDN + model URL each session, no bundled weights. iOS & Android (Expo Go). Optional API-key exercise engine.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -106,6 +106,11 @@ const PoseTrackerContext = createContext<PoseTrackerContextValue | null>(null);
106
106
  export interface PoseTrackerProviderProps {
107
107
  /** Optional: without a token the SDK runs in keypoints-only mode. */
108
108
  apiToken?: string;
109
+ /**
110
+ * Opt-in V4 engine (`'v4'`). Default `'v3'` — production FSM, unchanged.
111
+ * Merged into `options.engine` when both are set, this prop wins.
112
+ */
113
+ engine?: 'v3' | 'v4';
109
114
  options?: PoseTrackerClientOptions;
110
115
  /** Start preloading as soon as the provider mounts. Default: false. */
111
116
  autoPreload?: boolean;
@@ -114,13 +119,17 @@ export interface PoseTrackerProviderProps {
114
119
 
115
120
  export function PoseTrackerProvider({
116
121
  apiToken,
122
+ engine,
117
123
  options,
118
124
  autoPreload = false,
119
125
  children,
120
126
  }: PoseTrackerProviderProps): React.JSX.Element {
121
127
  const clientRef = useRef<PoseTrackerClient | null>(null);
122
128
  if (!clientRef.current) {
123
- clientRef.current = new PoseTrackerClient(apiToken, options);
129
+ clientRef.current = new PoseTrackerClient(apiToken, {
130
+ ...options,
131
+ engine: engine ?? options?.engine,
132
+ });
124
133
  }
125
134
  const client = clientRef.current;
126
135
 
@@ -14,6 +14,7 @@ import { Platform } from 'react-native';
14
14
 
15
15
  import { SDK_VERSION } from '../sdkVersion';
16
16
  import type { ConfigureRequest, PoseModelProfile, SdkManifest } from '../types/manifest';
17
+ import type { EngineChannel } from '../engineChannel';
17
18
 
18
19
  export const SDK_NAME = 'posetracker-rn-light' as const;
19
20
  export { SDK_VERSION };
@@ -28,6 +29,11 @@ export interface ConfigureOptions {
28
29
  locale?: string;
29
30
  /** Local cache versions, so the backend can answer `upToDate` per module. */
30
31
  localVersions?: ConfigureRequest['localVersions'];
32
+ /**
33
+ * Opt-in V4 heuristic engine. Default `'v3'` (production FSM).
34
+ * Set `'v4'` at init — the handshake downloads `engine-v4.bundle.js`.
35
+ */
36
+ engine?: EngineChannel;
31
37
  }
32
38
 
33
39
  export class ConfigureError extends Error {
@@ -53,6 +59,7 @@ export async function configure(
53
59
  poseModelProfile: options.poseModelProfile ?? 'AdaptiveChoice',
54
60
  locale: options.locale,
55
61
  localVersions: options.localVersions,
62
+ engineChannel: options.engine === 'v4' ? 'v4' : 'v3',
56
63
  };
57
64
 
58
65
  let response: Response;
package/src/client.ts CHANGED
@@ -38,6 +38,7 @@ import type { PoseBackend, PoseInputFrame } from './backends/PoseBackend';
38
38
  import { VisionPoseBackend } from './backends/vision/VisionPoseBackend';
39
39
  import { WebViewPoseBackend } from './backends/webview/WebViewPoseBackend';
40
40
  import { findExerciseByIdOrAlias } from './exercises/aliases';
41
+ import { normalizeEngineChannel, requiresEngineV4, type EngineChannel } from './engineChannel';
41
42
  import { fetchSkeletonDefinition } from './api/skeleton';
42
43
  import type { ExerciseConfig, ModelDescriptor, SdkManifest } from './types/manifest';
43
44
  import type { ColdStartMode, PreloadOptions } from './types/preload';
@@ -979,7 +980,32 @@ export class PoseTrackerClient {
979
980
  // -------------------------------------------------------------------------
980
981
 
981
982
  getAvailableExercises(): ExerciseConfig[] {
982
- return this.mode === 'full-engine' ? this.manifest?.exercises ?? [] : [];
983
+ if (this.mode !== 'full-engine') return [];
984
+ if (normalizeEngineChannel(this.options.engine) === 'v4') {
985
+ const listed = this.engine?.listExercises?.() ?? [];
986
+ if (listed.length) return listed.map((e) => this.v4ExerciseConfig(e));
987
+ }
988
+ return this.manifest?.exercises ?? [];
989
+ }
990
+
991
+ getEngineChannel(): EngineChannel {
992
+ return normalizeEngineChannel(this.options.engine);
993
+ }
994
+
995
+ private v4ExerciseConfig(entry: { id: string; displayName?: string; type?: string }): ExerciseConfig {
996
+ const type = entry.type === 'static' ? 'static' : 'dynamic';
997
+ return {
998
+ id: entry.id,
999
+ name: entry.displayName || entry.id,
1000
+ type,
1001
+ movement: {
1002
+ name: entry.id,
1003
+ type,
1004
+ scale_acceptance: {},
1005
+ movement_steps: [],
1006
+ movement_initial_posture: null,
1007
+ },
1008
+ };
983
1009
  }
984
1010
 
985
1011
  /**
@@ -1028,6 +1054,30 @@ export class PoseTrackerClient {
1028
1054
  throw new Error(FREE_PLAN_FEATURES_MESSAGE);
1029
1055
  }
1030
1056
  }
1057
+ if (requiresEngineV4(exerciseId) && normalizeEngineChannel(this.options.engine) !== 'v4') {
1058
+ const message = `Exercise '${exerciseId}' requires engine: 'v4'`;
1059
+ this.reportError({ type: 'error', code: 'invalid_exercise', message });
1060
+ throw new Error(message);
1061
+ }
1062
+ if (normalizeEngineChannel(this.options.engine) === 'v4') {
1063
+ const listed = this.engine.listExercises?.() ?? [];
1064
+ const v4Hit = listed.find((e) => e.id === exerciseId);
1065
+ if (v4Hit) {
1066
+ this.beginEngineSession(this.v4ExerciseConfig(v4Hit), options);
1067
+ return;
1068
+ }
1069
+ const customs = this.getAvailableCustomExercises();
1070
+ const custom =
1071
+ customs.find((e) => e.id === exerciseId) ??
1072
+ findExerciseByIdOrAlias(exerciseId, customs);
1073
+ if (custom) {
1074
+ this.startCustomExercise(custom, options);
1075
+ return;
1076
+ }
1077
+ const message = `Exercise '${exerciseId}' is not available in V4 engine`;
1078
+ this.reportError({ type: 'error', code: 'invalid_exercise', message });
1079
+ throw new Error(message);
1080
+ }
1031
1081
  const available = this.getAvailableExercises();
1032
1082
  const exercise = findExerciseByIdOrAlias(exerciseId, available);
1033
1083
  if (!exercise) {
@@ -1046,8 +1096,13 @@ export class PoseTrackerClient {
1046
1096
  this.reportError({ type: 'error', code: 'invalid_exercise', message });
1047
1097
  throw new Error(message);
1048
1098
  }
1099
+ this.beginEngineSession(exercise, options);
1100
+ }
1101
+
1102
+ private beginEngineSession(exercise: ExerciseConfig, options: StartExerciseOptions): void {
1103
+ if (!this.engine) return;
1049
1104
  this.stopExercise();
1050
- this.currentExerciseId = exerciseId;
1105
+ this.currentExerciseId = exercise.id;
1051
1106
  this.keypointsSuppressionLogged = false;
1052
1107
  const debug = options.debug ?? this.options.debugEngine === true;
1053
1108
  this.session = this.engine.createSession(
@@ -86,6 +86,13 @@ export interface PoseTrackerEngine {
86
86
  createSession(options: EngineSessionOptions, emit: EventSink): EngineSession;
87
87
  /** Engine >= 1.2.0: custom exercises shipped in the bundle (jump analysis…). */
88
88
  listCustomExercises?(): CustomExerciseDescriptor[];
89
+ /** Engine V4 catalog (opt-in channel). Absent on V3 bundles. */
90
+ listExercises?(): Array<{
91
+ id: string;
92
+ displayName: string;
93
+ type: string;
94
+ description?: string;
95
+ }>;
89
96
  /** Engine >= 1.2.0: start a custom exercise session (placement + jump_* events). */
90
97
  createCustomSession?(options: CustomSessionOptions, emit: EventSink): EngineSession;
91
98
  }
@@ -0,0 +1,17 @@
1
+ /** Opt-in remote engine channel. Default is production V3. */
2
+ export type EngineChannel = 'v3' | 'v4';
3
+
4
+ /** Movements that exist only in the V4 catalog (not in V3 FSM / Strapi). */
5
+ export const V4_ONLY_EXERCISE_IDS = [
6
+ 'shoulder_roll',
7
+ 'shoulder_deep_breath',
8
+ 'chair_forward_fold',
9
+ ] as const;
10
+
11
+ export function normalizeEngineChannel(raw?: string | null): EngineChannel {
12
+ return raw === 'v4' ? 'v4' : 'v3';
13
+ }
14
+
15
+ export function requiresEngineV4(exerciseId: string): boolean {
16
+ return (V4_ONLY_EXERCISE_IDS as readonly string[]).includes(exerciseId);
17
+ }
package/src/index.ts CHANGED
@@ -59,6 +59,8 @@ export type {
59
59
  // Orchestrator + React layer
60
60
  export { PoseTrackerClient } from './client';
61
61
  export type { PoseTrackerClientOptions, PreferredBackend, StartExerciseOptions } from './client';
62
+ export type { EngineChannel } from './engineChannel';
63
+ export { V4_ONLY_EXERCISE_IDS, normalizeEngineChannel, requiresEngineV4 } from './engineChannel';
62
64
  export type { ColdStartMode, PreloadOptions } from './types/preload';
63
65
  export type {
64
66
  SkeletonAnglesStyle,
package/src/sdkVersion.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** AUTO-GENERATED by scripts/sync-sdk-version.mjs — do not edit. */
2
- export const SDK_VERSION = '0.2.2';
2
+ export const SDK_VERSION = '0.3.0';
@@ -146,6 +146,8 @@ export interface EngineBundleDescriptor {
146
146
  minSdkVersion: string;
147
147
  /** True when the version sent in `localVersions.engine` is already current. */
148
148
  upToDate?: boolean;
149
+ /** Which interpreter this descriptor is. Absent on older backends = v3. */
150
+ channel?: 'v3' | 'v4';
149
151
  }
150
152
 
151
153
  // ---------------------------------------------------------------------------
@@ -197,6 +199,11 @@ export interface ConfigureRequest {
197
199
  poseRuntime?: string | null;
198
200
  engine?: string | null;
199
201
  };
202
+ /**
203
+ * Which remote engine to download. Default / omitted = v3 (production).
204
+ * `v4` is opt-in (heuristic catalog: squat, shoulder_roll, …).
205
+ */
206
+ engineChannel?: 'v3' | 'v4';
200
207
  }
201
208
 
202
209
  export interface PlanInfo {