@pose-tracker/pose-estimation-web 0.2.0 → 0.3.1

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
@@ -41,6 +41,10 @@ await pt.analyze(); // re-run on the same image
41
41
  | `{ type: 'video', src }` | File / blob URL / `<video>` — stream while playing |
42
42
  | `{ type: 'image', src }` | File / blob URL / `<img>` / ImageBitmap — one shot (+ `analyze()`) |
43
43
 
44
+ Full tables (RN parity, CORS, React): monorepo
45
+ [`docs/MEDIA_SOURCES.md`](../../docs/MEDIA_SOURCES.md) · public
46
+ https://docs.posetracker.com/media-sources
47
+
44
48
  ## Script tag (CDN)
45
49
 
46
50
  The IIFE build exposes a **`PoseTracker`** global (`PoseTracker.createPoseTracker`, …).
@@ -57,7 +61,7 @@ The IIFE build exposes a **`PoseTracker`** global (`PoseTracker.createPoseTracke
57
61
  <!-- 1) TensorFlow.js (peer — required before PoseTracker) -->
58
62
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.22.0/dist/tf.min.js"></script>
59
63
  <!-- 2) PoseTracker IIFE (jsDelivr / unpkg both work) -->
60
- <script src="https://cdn.jsdelivr.net/npm/@pose-tracker/pose-estimation-web@0.2.0/dist/pose-tracker.global.js"></script>
64
+ <script src="https://cdn.jsdelivr.net/npm/@pose-tracker/pose-estimation-web@0.3.0/dist/pose-tracker.global.js"></script>
61
65
  </head>
62
66
  <body>
63
67
  <div id="root" style="width: 100%; height: 100vh; background: #111"></div>
@@ -79,7 +83,7 @@ The IIFE build exposes a **`PoseTracker`** global (`PoseTracker.createPoseTracke
79
83
  Equivalent unpkg URL:
80
84
 
81
85
  ```text
82
- https://unpkg.com/@pose-tracker/pose-estimation-web@0.2.0/dist/pose-tracker.global.js
86
+ https://unpkg.com/@pose-tracker/pose-estimation-web@0.3.0/dist/pose-tracker.global.js
83
87
  ```
84
88
 
85
89
  Omitting `/dist/...` also works — `package.json` `jsdelivr` / `unpkg` fields point at the IIFE.
package/dist/index.cjs CHANGED
@@ -3,7 +3,7 @@
3
3
  var jsSha256 = require('js-sha256');
4
4
 
5
5
  // src/version.ts
6
- var SDK_VERSION = "0.2.0";
6
+ var SDK_VERSION = "0.3.1";
7
7
  var SDK_NAME = "posetracker-web";
8
8
 
9
9
  // src/api/configure.ts
@@ -24,7 +24,8 @@ async function configure(apiToken, options = {}) {
24
24
  targetPlatform: "web",
25
25
  poseModelProfile: options.poseModelProfile ?? "AdaptiveChoice",
26
26
  locale: options.locale,
27
- localVersions: options.localVersions
27
+ localVersions: options.localVersions,
28
+ engineChannel: options.engine === "v3" ? "v3" : "v4"
28
29
  };
29
30
  let response;
30
31
  try {
@@ -531,6 +532,10 @@ function waitForImageReady(img, timeoutMs = 15e3) {
531
532
  resolve();
532
533
  return;
533
534
  }
535
+ if (img.complete) {
536
+ reject(new Error("Image failed to decode (naturalWidth=0)"));
537
+ return;
538
+ }
534
539
  const onReady = () => {
535
540
  cleanup();
536
541
  resolve();
@@ -614,6 +619,31 @@ function findExerciseByIdOrAlias(exerciseId, exercises) {
614
619
  return exercises.find((e) => resolveExerciseName(e.id) === canonical);
615
620
  }
616
621
 
622
+ // src/engineChannel.ts
623
+ var V4_ONLY_EXERCISE_IDS = [
624
+ "shoulder_roll",
625
+ "shoulder_deep_breath",
626
+ "chair_forward_fold",
627
+ "chair_side_stretch"
628
+ ];
629
+ var PRODUCTION_SQUAT_IDS = ["squat", "face_squat"];
630
+ function isProductionSquatId(exerciseId) {
631
+ const id = String(exerciseId || "").trim().toLowerCase();
632
+ return id === "squat" || id === "face_squat";
633
+ }
634
+ function normalizeEngineChannel(raw) {
635
+ return raw === "v3" ? "v3" : "v4";
636
+ }
637
+ function resolveMovementEngine(opts = {}) {
638
+ if (opts.userExercise) return "v4";
639
+ const requested = String(opts.engine || "").trim().toLowerCase();
640
+ if (isProductionSquatId(opts.exercise) && requested !== "v4") return "v3";
641
+ return normalizeEngineChannel(opts.engine);
642
+ }
643
+ function requiresEngineV4(exerciseId) {
644
+ return V4_ONLY_EXERCISE_IDS.includes(exerciseId);
645
+ }
646
+
617
647
  // src/events/classicMessage.ts
618
648
  var ANGLE_RELATIONS = {
619
649
  left_knee: "from_hip_to_ankle",
@@ -1069,6 +1099,23 @@ var BLAZE_TO_COCO = {
1069
1099
  left_ankle: "left_ankle",
1070
1100
  right_ankle: "right_ankle"
1071
1101
  };
1102
+ var FACE_EXTRA_NAMES = /* @__PURE__ */ new Set([
1103
+ "left_eye_inner",
1104
+ "left_eye_outer",
1105
+ "right_eye_inner",
1106
+ "right_eye_outer",
1107
+ "mouth_left",
1108
+ "mouth_right"
1109
+ ]);
1110
+ var BLAZE_MAX_SIDE = 1280;
1111
+ function toSourcePixels(x, y, scratchW, scratchH, srcW, srcH, maxCoord) {
1112
+ if (maxCoord <= 1.5 && scratchW > 2) {
1113
+ return { x: x * srcW, y: y * srcH };
1114
+ }
1115
+ const sx = scratchW / srcW;
1116
+ const sy = scratchH / srcH;
1117
+ return { x: sx > 0 ? x / sx : x, y: sy > 0 ? y / sy : y };
1118
+ }
1072
1119
  var BlazePoseAdapter = class {
1073
1120
  constructor(_ctx) {
1074
1121
  this.modelId = "blazepose";
@@ -1089,7 +1136,7 @@ var BlazePoseAdapter = class {
1089
1136
  {
1090
1137
  runtime: "tfjs",
1091
1138
  modelType: this.modelType,
1092
- enableSmoothing: true
1139
+ enableSmoothing: false
1093
1140
  }
1094
1141
  );
1095
1142
  }
@@ -1097,8 +1144,26 @@ var BlazePoseAdapter = class {
1097
1144
  if (!this.detector) throw new Error("BlazePose adapter not loaded");
1098
1145
  const size = blazeInputSize(input);
1099
1146
  if (!(size.vw > 0 && size.vh > 0)) return null;
1147
+ if (options.temporalSmooth === false) this.resetTemporal();
1148
+ let estimateTarget = input;
1149
+ let scratch = null;
1150
+ let scratchW = size.vw;
1151
+ let scratchH = size.vh;
1152
+ if (typeof HTMLImageElement !== "undefined" && input instanceof HTMLImageElement || typeof HTMLVideoElement !== "undefined" && input instanceof HTMLVideoElement || typeof HTMLCanvasElement !== "undefined" && input instanceof HTMLCanvasElement) {
1153
+ const fit = Math.min(1, BLAZE_MAX_SIDE / Math.max(size.vw, size.vh));
1154
+ scratchW = Math.max(1, Math.round(size.vw * fit));
1155
+ scratchH = Math.max(1, Math.round(size.vh * fit));
1156
+ scratch = document.createElement("canvas");
1157
+ scratch.width = scratchW;
1158
+ scratch.height = scratchH;
1159
+ const ctx = scratch.getContext("2d");
1160
+ if (ctx) {
1161
+ ctx.drawImage(input, 0, 0, scratchW, scratchH);
1162
+ estimateTarget = scratch;
1163
+ }
1164
+ }
1100
1165
  const t0 = performance.now();
1101
- const poses = await this.detector.estimatePoses(input, {
1166
+ const poses = await this.detector.estimatePoses(estimateTarget, {
1102
1167
  flipHorizontal: false,
1103
1168
  maxPoses: 1
1104
1169
  });
@@ -1129,16 +1194,22 @@ var BlazePoseAdapter = class {
1129
1194
  const vh = size.vh || 1;
1130
1195
  const dispW = options.displayWidth || 1;
1131
1196
  const dispH = options.displayHeight || 1;
1197
+ let maxCoord = 0;
1198
+ for (const kp of pose.keypoints) {
1199
+ maxCoord = Math.max(maxCoord, Math.abs(kp.x || 0), Math.abs(kp.y || 0));
1200
+ }
1132
1201
  const byName = /* @__PURE__ */ new Map();
1202
+ const extrasRaw = [];
1133
1203
  for (const kp of pose.keypoints) {
1134
1204
  const name = String(kp.name || "").toLowerCase();
1205
+ const score = typeof kp.score === "number" ? kp.score : 0;
1206
+ const src = toSourcePixels(kp.x, kp.y, scratchW, scratchH, vw, vh, maxCoord);
1135
1207
  const coco = BLAZE_TO_COCO[name];
1136
- if (!coco) continue;
1137
- byName.set(coco, {
1138
- x: kp.x,
1139
- y: kp.y,
1140
- score: typeof kp.score === "number" ? kp.score : 0
1141
- });
1208
+ if (coco) {
1209
+ byName.set(coco, { x: src.x, y: src.y, score });
1210
+ } else if (FACE_EXTRA_NAMES.has(name)) {
1211
+ extrasRaw.push({ name, x: src.x, y: src.y, score });
1212
+ }
1142
1213
  }
1143
1214
  const videoKeypoints = COCO_KEYPOINT_NAMES.map((name) => {
1144
1215
  const k = byName.get(name);
@@ -1167,6 +1238,19 @@ var BlazePoseAdapter = class {
1167
1238
  score: k.score
1168
1239
  };
1169
1240
  });
1241
+ for (const extra of extrasRaw) {
1242
+ const dx = extra.x * scale + ox;
1243
+ const dy = extra.y * scale + oy;
1244
+ let nx = dispW > 0 ? dx / dispW : 0;
1245
+ const ny = dispH > 0 ? dy / dispH : 0;
1246
+ if (options.facingMode === "user") nx = 1 - nx;
1247
+ keypoints.push({
1248
+ name: extra.name,
1249
+ x: Math.min(1, Math.max(0, nx)),
1250
+ y: Math.min(1, Math.max(0, ny)),
1251
+ score: extra.score
1252
+ });
1253
+ }
1170
1254
  return {
1171
1255
  keypoints,
1172
1256
  score: scoreSum / 17,
@@ -1182,6 +1266,13 @@ var BlazePoseAdapter = class {
1182
1266
  }
1183
1267
  };
1184
1268
  }
1269
+ /** Clear cached ROI / landmark filters (call when the still changes). */
1270
+ resetTemporal() {
1271
+ try {
1272
+ this.detector?.reset?.();
1273
+ } catch {
1274
+ }
1275
+ }
1185
1276
  dispose() {
1186
1277
  try {
1187
1278
  this.detector?.dispose?.();
@@ -1205,13 +1296,14 @@ function inputSize(input) {
1205
1296
  return { vw: input.width || 0, vh: input.height || 0 };
1206
1297
  }
1207
1298
  var INPUT_SIZE = 192;
1208
- var SMOOTH_ALPHA = 0.5;
1299
+ var SMOOTH_ALPHA = 0.45;
1300
+ var STILL_TARGET_FILL = 0.52;
1209
1301
  function modelNormToVideo(xNorm, yNorm, lb) {
1210
1302
  const xSq = xNorm * INPUT_SIZE;
1211
1303
  const ySq = yNorm * INPUT_SIZE;
1212
1304
  return {
1213
- x: (xSq - lb.offsetX) * (lb.vw / lb.drawW),
1214
- y: (ySq - lb.offsetY) * (lb.vh / lb.drawH)
1305
+ x: (xSq - lb.offsetX) * (lb.vw / lb.drawW) + (lb.originX ?? 0),
1306
+ y: (ySq - lb.offsetY) * (lb.vh / lb.drawH) + (lb.originY ?? 0)
1215
1307
  };
1216
1308
  }
1217
1309
  function videoToCover(vx, vy, dispW, dispH, lb) {
@@ -1228,6 +1320,18 @@ var MoveNetGraphAdapter = class {
1228
1320
  this.offscreen = null;
1229
1321
  this.offCtx = null;
1230
1322
  this.smoothed = null;
1323
+ this.lastVideoKps = null;
1324
+ this.stillPass = 0;
1325
+ this.inferLb = {
1326
+ offsetX: 0,
1327
+ offsetY: 0,
1328
+ drawW: INPUT_SIZE,
1329
+ drawH: INPUT_SIZE,
1330
+ vw: 1,
1331
+ vh: 1,
1332
+ originX: 0,
1333
+ originY: 0
1334
+ };
1231
1335
  this.letterbox = {
1232
1336
  offsetX: 0,
1233
1337
  offsetY: 0,
@@ -1267,20 +1371,81 @@ var MoveNetGraphAdapter = class {
1267
1371
  if (!this.offCtx) throw new Error("2d context unavailable");
1268
1372
  return this.offCtx;
1269
1373
  }
1270
- prepareInput(input) {
1374
+ prepareInput(input, trackRoi) {
1271
1375
  const { vw: rawW, vh: rawH } = inputSize(input);
1272
1376
  const vw = rawW || 1;
1273
1377
  const vh = rawH || 1;
1274
- const scale = Math.min(INPUT_SIZE / vw, INPUT_SIZE / vh);
1275
- const drawW = vw * scale;
1276
- const drawH = vh * scale;
1378
+ let winX = 0;
1379
+ let winY = 0;
1380
+ let winW = vw;
1381
+ let winH = vh;
1382
+ if (trackRoi && this.lastVideoKps && this.stillPass > 0) {
1383
+ const pts = this.lastVideoKps.filter((k) => k.score >= 0.15);
1384
+ if (pts.length >= 4) {
1385
+ let minx = Infinity;
1386
+ let miny = Infinity;
1387
+ let maxx = -Infinity;
1388
+ let maxy = -Infinity;
1389
+ for (const p of pts) {
1390
+ minx = Math.min(minx, p.xPx);
1391
+ miny = Math.min(miny, p.yPx);
1392
+ maxx = Math.max(maxx, p.xPx);
1393
+ maxy = Math.max(maxy, p.yPx);
1394
+ }
1395
+ const bw = Math.max(8, maxx - minx);
1396
+ const bh = Math.max(8, maxy - miny);
1397
+ const cx = (minx + maxx) / 2;
1398
+ const cy = (miny + maxy) / 2;
1399
+ winW = Math.max(bw / STILL_TARGET_FILL, vw * 0.35);
1400
+ winH = Math.max(bh / STILL_TARGET_FILL, vh * 0.35);
1401
+ const imgAspect = vw / vh;
1402
+ if (winW / winH < imgAspect) winW = winH * imgAspect;
1403
+ else winH = winW / imgAspect;
1404
+ winX = cx - winW / 2;
1405
+ winY = cy - winH / 2;
1406
+ }
1407
+ }
1408
+ const scale = Math.min(INPUT_SIZE / winW, INPUT_SIZE / winH);
1409
+ const drawW = winW * scale;
1410
+ const drawH = winH * scale;
1277
1411
  const offsetX = (INPUT_SIZE - drawW) / 2;
1278
1412
  const offsetY = (INPUT_SIZE - drawH) / 2;
1279
- this.letterbox = { offsetX, offsetY, drawW, drawH, vw, vh };
1413
+ this.inferLb = {
1414
+ offsetX,
1415
+ offsetY,
1416
+ drawW,
1417
+ drawH,
1418
+ vw: winW,
1419
+ vh: winH,
1420
+ originX: winX,
1421
+ originY: winY
1422
+ };
1423
+ this.letterbox = { offsetX: 0, offsetY: 0, drawW: vw, drawH: vh, vw, vh };
1280
1424
  const c2d = this.poseCanvasCtx();
1281
1425
  c2d.fillStyle = "#000";
1282
1426
  c2d.fillRect(0, 0, INPUT_SIZE, INPUT_SIZE);
1283
- c2d.drawImage(input, 0, 0, vw, vh, offsetX, offsetY, drawW, drawH);
1427
+ const srcX = Math.max(0, winX);
1428
+ const srcY = Math.max(0, winY);
1429
+ const srcR = Math.min(vw, winX + winW);
1430
+ const srcB = Math.min(vh, winY + winH);
1431
+ if (srcR > srcX && srcB > srcY) {
1432
+ const dx = offsetX + (srcX - winX) * (drawW / winW);
1433
+ const dy = offsetY + (srcY - winY) * (drawH / winH);
1434
+ const dw = (srcR - srcX) * (drawW / winW);
1435
+ const dh = (srcB - srcY) * (drawH / winH);
1436
+ c2d.drawImage(
1437
+ input,
1438
+ srcX,
1439
+ srcY,
1440
+ srcR - srcX,
1441
+ srcB - srcY,
1442
+ dx,
1443
+ dy,
1444
+ dw,
1445
+ dh
1446
+ );
1447
+ }
1448
+ this.stillPass += 1;
1284
1449
  return this.offscreen;
1285
1450
  }
1286
1451
  smooth(raw) {
@@ -1301,7 +1466,9 @@ var MoveNetGraphAdapter = class {
1301
1466
  const size = inputSize(input);
1302
1467
  if (!(size.vw > 0 && size.vh > 0)) return null;
1303
1468
  const t0 = performance.now();
1304
- const canvas = this.prepareInput(input);
1469
+ const isStill = typeof HTMLImageElement !== "undefined" && input instanceof HTMLImageElement;
1470
+ const trackRoi = options.temporalSmooth !== false && isStill;
1471
+ const canvas = this.prepareInput(input, trackRoi);
1305
1472
  const tensorIn = this.tf.tidy(
1306
1473
  () => this.tf.expandDims(this.tf.browser.fromPixels(canvas), 0)
1307
1474
  );
@@ -1312,7 +1479,8 @@ var MoveNetGraphAdapter = class {
1312
1479
  if (Array.isArray(out)) out.forEach((t) => t.dispose());
1313
1480
  else out.dispose();
1314
1481
  const inferenceMs = performance.now() - t0;
1315
- const lb = this.letterbox;
1482
+ const lb = this.inferLb;
1483
+ const coverLb = this.letterbox;
1316
1484
  const dispW = options.displayWidth || 1;
1317
1485
  const dispH = options.displayHeight || 1;
1318
1486
  const drawRaw = [];
@@ -1330,7 +1498,8 @@ var MoveNetGraphAdapter = class {
1330
1498
  });
1331
1499
  scoreSum += score;
1332
1500
  }
1333
- const sm = this.smooth(drawRaw);
1501
+ const sm = options.temporalSmooth === false ? drawRaw : this.smooth(drawRaw);
1502
+ this.lastVideoKps = sm.map((k) => ({ xPx: k.xPx, yPx: k.yPx, score: k.score }));
1334
1503
  const videoKeypoints = sm.map((k, i) => ({
1335
1504
  name: COCO_KEYPOINT_NAMES[i],
1336
1505
  xPx: k.xPx,
@@ -1338,7 +1507,7 @@ var MoveNetGraphAdapter = class {
1338
1507
  score: k.score
1339
1508
  }));
1340
1509
  const keypoints = videoKeypoints.map((k) => {
1341
- const d = videoToCover(k.xPx, k.yPx, dispW, dispH, lb);
1510
+ const d = videoToCover(k.xPx, k.yPx, dispW, dispH, coverLb);
1342
1511
  let nx = dispW > 0 ? d.x / dispW : 0;
1343
1512
  const ny = dispH > 0 ? d.y / dispH : 0;
1344
1513
  if (options.facingMode === "user") nx = 1 - nx;
@@ -1355,9 +1524,15 @@ var MoveNetGraphAdapter = class {
1355
1524
  score: scoreSum / 17,
1356
1525
  inferenceMs,
1357
1526
  videoKeypoints,
1358
- letterbox: lb
1527
+ letterbox: coverLb
1359
1528
  };
1360
1529
  }
1530
+ /** Clear EMA state (call when the still / clip changes). */
1531
+ resetTemporal() {
1532
+ this.smoothed = null;
1533
+ this.lastVideoKps = null;
1534
+ this.stillPass = 0;
1535
+ }
1361
1536
  dispose() {
1362
1537
  try {
1363
1538
  this.model?.dispose();
@@ -1365,6 +1540,8 @@ var MoveNetGraphAdapter = class {
1365
1540
  }
1366
1541
  this.model = null;
1367
1542
  this.smoothed = null;
1543
+ this.lastVideoKps = null;
1544
+ this.stillPass = 0;
1368
1545
  this.offscreen = null;
1369
1546
  this.offCtx = null;
1370
1547
  }
@@ -1985,7 +2162,11 @@ var PoseTrackerClient = class {
1985
2162
  if (this.poseSource.type === "camera" && this.poseSource.facingMode) {
1986
2163
  this.opts.facingMode = this.poseSource.facingMode;
1987
2164
  }
2165
+ if (this.adapter?.resetTemporal) {
2166
+ this.adapter.resetTemporal();
2167
+ }
1988
2168
  if (!this.shell) return;
2169
+ await this.ensureAdapter();
1989
2170
  await this.ensureSource();
1990
2171
  if (wasRunning || this.poseSource.type === "image") {
1991
2172
  if (this.poseSource.type === "image") this.imageShotPending = true;
@@ -2097,7 +2278,30 @@ var PoseTrackerClient = class {
2097
2278
  return this.configurePromise;
2098
2279
  }
2099
2280
  getAvailableExercises() {
2100
- return this.mode === "full-engine" ? this.manifest?.exercises ?? [] : [];
2281
+ if (this.mode !== "full-engine") return [];
2282
+ if (normalizeEngineChannel(this.opts.engine) === "v4") {
2283
+ const listed = this.engine?.listExercises?.() ?? [];
2284
+ if (listed.length) return listed.map((e) => this.v4ExerciseConfig(e));
2285
+ }
2286
+ return this.manifest?.exercises ?? [];
2287
+ }
2288
+ getEngineChannel() {
2289
+ return normalizeEngineChannel(this.opts.engine);
2290
+ }
2291
+ v4ExerciseConfig(entry) {
2292
+ const type = entry.type === "static" ? "static" : "dynamic";
2293
+ return {
2294
+ id: entry.id,
2295
+ name: entry.displayName || entry.id,
2296
+ type,
2297
+ movement: {
2298
+ name: entry.id,
2299
+ type,
2300
+ scale_acceptance: {},
2301
+ movement_steps: [],
2302
+ movement_initial_posture: null
2303
+ }
2304
+ };
2101
2305
  }
2102
2306
  getAvailableCustomExercises() {
2103
2307
  if (this.mode !== "full-engine" || !this.engine?.listCustomExercises) return [];
@@ -2133,6 +2337,40 @@ var PoseTrackerClient = class {
2133
2337
  throw new Error(FREE_PLAN_FEATURES_MESSAGE);
2134
2338
  }
2135
2339
  }
2340
+ if (requiresEngineV4(exerciseId) && normalizeEngineChannel(this.opts.engine) !== "v4") {
2341
+ const message = `Exercise '${exerciseId}' requires engine: 'v4'`;
2342
+ this.reportError({ type: "error", code: "invalid_exercise", message });
2343
+ throw new Error(message);
2344
+ }
2345
+ const jumpId = resolveExerciseName(exerciseId) || exerciseId;
2346
+ const sessionEngine = resolveMovementEngine({
2347
+ engine: this.opts.engine,
2348
+ exercise: jumpId
2349
+ });
2350
+ if (sessionEngine === "v4") {
2351
+ const listed = this.engine.listExercises?.() ?? [];
2352
+ const v4Hit = listed.find((e) => e.id === exerciseId) ?? listed.find((e) => e.id === jumpId);
2353
+ if (v4Hit) {
2354
+ this.beginEngineSession(this.v4ExerciseConfig(v4Hit), options);
2355
+ return;
2356
+ }
2357
+ const customs = this.getAvailableCustomExercises();
2358
+ const custom = customs.find((e) => e.id === exerciseId) ?? findExerciseByIdOrAlias(exerciseId, customs);
2359
+ if (custom) {
2360
+ this.startCustomExercise(custom, options);
2361
+ return;
2362
+ }
2363
+ const message = `Exercise '${exerciseId}' is not available in V4 engine`;
2364
+ this.reportError({ type: "error", code: "invalid_exercise", message });
2365
+ throw new Error(message);
2366
+ }
2367
+ if (isProductionSquatId(jumpId) && normalizeEngineChannel(this.opts.engine) === "v4") {
2368
+ this.beginEngineSession(
2369
+ this.v4ExerciseConfig({ id: jumpId || "squat", displayName: "Squat", type: "dynamic" }),
2370
+ options
2371
+ );
2372
+ return;
2373
+ }
2136
2374
  const available = this.getAvailableExercises();
2137
2375
  const exercise = findExerciseByIdOrAlias(exerciseId, available);
2138
2376
  if (!exercise) {
@@ -2146,8 +2384,12 @@ var PoseTrackerClient = class {
2146
2384
  this.reportError({ type: "error", code: "invalid_exercise", message });
2147
2385
  throw new Error(message);
2148
2386
  }
2387
+ this.beginEngineSession(exercise, options);
2388
+ }
2389
+ beginEngineSession(exercise, options) {
2390
+ if (!this.engine) return;
2149
2391
  this.stopExercise();
2150
- this.currentExerciseId = exerciseId;
2392
+ this.currentExerciseId = exercise.id;
2151
2393
  this.keypointsSuppressionLogged = false;
2152
2394
  this.session = this.engine.createSession(
2153
2395
  {
@@ -2155,6 +2397,7 @@ var PoseTrackerClient = class {
2155
2397
  locale: this.opts.locale ?? "en",
2156
2398
  difficulty: options.difficulty,
2157
2399
  minGrade: this.features.minGrade ?? void 0,
2400
+ v4CatalogSquat: this.opts.engine === "v4",
2158
2401
  features: {
2159
2402
  angles: this.features.angles,
2160
2403
  recommendations: this.features.recommendations,
@@ -2514,13 +2757,19 @@ var PoseTrackerClient = class {
2514
2757
  if (!this.adapter || !this.shell || !this.estimateInput) return null;
2515
2758
  if (!this.frameReady(this.estimateInput)) return null;
2516
2759
  const input = this.estimateInput;
2517
- const dispW = this.shell.canvas.clientWidth || this.shell.root.clientWidth || 1;
2518
- const dispH = this.shell.canvas.clientHeight || this.shell.root.clientHeight || 1;
2760
+ const clientW = this.shell.canvas.clientWidth || this.shell.root.clientWidth || 1;
2761
+ const clientH = this.shell.canvas.clientHeight || this.shell.root.clientHeight || 1;
2762
+ const media = mediaSize(input);
2763
+ const useMediaSpace = this.poseSource.type === "image" && media.width > 0 && media.height > 0;
2764
+ const dispW = useMediaSpace ? media.width : clientW;
2765
+ const dispH = useMediaSpace ? media.height : clientH;
2519
2766
  const facing = this.poseSource.type === "camera" ? facingFromOptions(this.opts) : "environment";
2767
+ const stillTracked = this.poseSource.type === "image" && this.opts.stillMode === "tracked";
2520
2768
  const result = await this.adapter.estimate(input, {
2521
2769
  facingMode: facing,
2522
2770
  displayWidth: dispW,
2523
- displayHeight: dispH
2771
+ displayHeight: dispH,
2772
+ temporalSmooth: this.poseSource.type === "camera" || this.poseSource.type === "video" || stillTracked
2524
2773
  });
2525
2774
  if (!result) return null;
2526
2775
  const ctx = this.shell.canvas.getContext("2d");
@@ -2528,12 +2777,12 @@ var PoseTrackerClient = class {
2528
2777
  const mapped = mapVideoKeypointsToDisplay(
2529
2778
  result.videoKeypoints,
2530
2779
  result.letterbox,
2531
- dispW,
2532
- dispH
2780
+ clientW,
2781
+ clientH
2533
2782
  );
2534
- drawSkeleton(ctx, mapped, dispW, dispH, true, this.skeletonDef);
2783
+ drawSkeleton(ctx, mapped, clientW, clientH, true, this.skeletonDef);
2535
2784
  } else if (ctx && this.opts.drawSkeleton === false) {
2536
- ctx.clearRect(0, 0, dispW, dispH);
2785
+ ctx.clearRect(0, 0, clientW, clientH);
2537
2786
  }
2538
2787
  const pose = {
2539
2788
  keypoints: result.keypoints,
@@ -2576,7 +2825,13 @@ var PoseTrackerClient = class {
2576
2825
  return pose;
2577
2826
  }
2578
2827
  async loop(token) {
2579
- if (token !== this.loopToken || !this.running || !this.adapter || !this.shell || this.disposed) {
2828
+ if (token !== this.loopToken || !this.running || !this.shell || this.disposed) {
2829
+ return;
2830
+ }
2831
+ if (!this.adapter) {
2832
+ requestAnimationFrame(() => {
2833
+ void this.loop(token);
2834
+ });
2580
2835
  return;
2581
2836
  }
2582
2837
  const input = this.estimateInput;
@@ -2584,8 +2839,8 @@ var PoseTrackerClient = class {
2584
2839
  if (shouldInfer) {
2585
2840
  this.busy = true;
2586
2841
  try {
2587
- await this.runOneFrame();
2588
- if (this.poseSource.type === "image") {
2842
+ const pose = await this.runOneFrame();
2843
+ if (this.poseSource.type === "image" && pose) {
2589
2844
  this.imageShotPending = false;
2590
2845
  this.running = false;
2591
2846
  this.busy = false;
@@ -2594,6 +2849,12 @@ var PoseTrackerClient = class {
2594
2849
  } catch (err) {
2595
2850
  const message = err instanceof Error ? err.message : String(err);
2596
2851
  this.emit({ type: "error", code: "internal", message: `inference: ${message}` });
2852
+ if (this.poseSource.type === "image") {
2853
+ this.imageShotPending = false;
2854
+ this.running = false;
2855
+ this.busy = false;
2856
+ return;
2857
+ }
2597
2858
  }
2598
2859
  this.busy = false;
2599
2860
  }
@@ -2974,6 +3235,7 @@ exports.MoveNetGraphAdapter = MoveNetGraphAdapter;
2974
3235
  exports.ONLINE_POSE_DETECTION_VERSION = ONLINE_POSE_DETECTION_VERSION;
2975
3236
  exports.ONLINE_TFJS_VERSION = ONLINE_TFJS_VERSION;
2976
3237
  exports.POSETRACKER_LOGO_URL = POSETRACKER_LOGO_URL;
3238
+ exports.PRODUCTION_SQUAT_IDS = PRODUCTION_SQUAT_IDS;
2977
3239
  exports.PoseCamera = PoseCamera;
2978
3240
  exports.PoseCamera_mount = PoseCamera_mount;
2979
3241
  exports.PoseTrackerClient = PoseTrackerClient;
@@ -2982,6 +3244,7 @@ exports.SDK_VERSION = SDK_VERSION;
2982
3244
  exports.SkeletonFetchError = SkeletonFetchError;
2983
3245
  exports.TrackError = TrackError;
2984
3246
  exports.UsageTracker = UsageTracker;
3247
+ exports.V4_ONLY_EXERCISE_IDS = V4_ONLY_EXERCISE_IDS;
2985
3248
  exports.configure = configure;
2986
3249
  exports.createLocalFileStore = createLocalFileStore;
2987
3250
  exports.createLocalKeyValueStore = createLocalKeyValueStore;
@@ -2998,12 +3261,16 @@ exports.findExerciseByIdOrAlias = findExerciseByIdOrAlias;
2998
3261
  exports.freeBlockedFeatures = freeBlockedFeatures;
2999
3262
  exports.initWebGlBackend = initWebGlBackend;
3000
3263
  exports.isPaidPlan = isPaidPlan;
3264
+ exports.isProductionSquatId = isProductionSquatId;
3001
3265
  exports.mapVideoKeypointsToDisplay = mapVideoKeypointsToDisplay;
3002
3266
  exports.mountDomShell = mountDomShell;
3267
+ exports.normalizeEngineChannel = normalizeEngineChannel;
3003
3268
  exports.normalizePoseSource = normalizePoseSource;
3004
3269
  exports.openCameraStream = openCameraStream;
3270
+ exports.requiresEngineV4 = requiresEngineV4;
3005
3271
  exports.resolveExerciseName = resolveExerciseName;
3006
3272
  exports.resolveFeatures = resolveFeatures;
3273
+ exports.resolveMovementEngine = resolveMovementEngine;
3007
3274
  exports.resolvePoseDetection = resolvePoseDetection;
3008
3275
  exports.resolvePoseModel = resolvePoseModel;
3009
3276
  exports.resolveTf = resolveTf;