@pose-tracker/pose-estimation-web 0.2.0 → 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
@@ -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.0";
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 === "v4" ? "v4" : "v3"
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,19 @@ 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
+ ];
628
+ function normalizeEngineChannel(raw) {
629
+ return raw === "v4" ? "v4" : "v3";
630
+ }
631
+ function requiresEngineV4(exerciseId) {
632
+ return V4_ONLY_EXERCISE_IDS.includes(exerciseId);
633
+ }
634
+
617
635
  // src/events/classicMessage.ts
618
636
  var ANGLE_RELATIONS = {
619
637
  left_knee: "from_hip_to_ankle",
@@ -1069,6 +1087,23 @@ var BLAZE_TO_COCO = {
1069
1087
  left_ankle: "left_ankle",
1070
1088
  right_ankle: "right_ankle"
1071
1089
  };
1090
+ var FACE_EXTRA_NAMES = /* @__PURE__ */ new Set([
1091
+ "left_eye_inner",
1092
+ "left_eye_outer",
1093
+ "right_eye_inner",
1094
+ "right_eye_outer",
1095
+ "mouth_left",
1096
+ "mouth_right"
1097
+ ]);
1098
+ var BLAZE_MAX_SIDE = 1280;
1099
+ function toSourcePixels(x, y, scratchW, scratchH, srcW, srcH, maxCoord) {
1100
+ if (maxCoord <= 1.5 && scratchW > 2) {
1101
+ return { x: x * srcW, y: y * srcH };
1102
+ }
1103
+ const sx = scratchW / srcW;
1104
+ const sy = scratchH / srcH;
1105
+ return { x: sx > 0 ? x / sx : x, y: sy > 0 ? y / sy : y };
1106
+ }
1072
1107
  var BlazePoseAdapter = class {
1073
1108
  constructor(_ctx) {
1074
1109
  this.modelId = "blazepose";
@@ -1089,7 +1124,7 @@ var BlazePoseAdapter = class {
1089
1124
  {
1090
1125
  runtime: "tfjs",
1091
1126
  modelType: this.modelType,
1092
- enableSmoothing: true
1127
+ enableSmoothing: false
1093
1128
  }
1094
1129
  );
1095
1130
  }
@@ -1097,8 +1132,26 @@ var BlazePoseAdapter = class {
1097
1132
  if (!this.detector) throw new Error("BlazePose adapter not loaded");
1098
1133
  const size = blazeInputSize(input);
1099
1134
  if (!(size.vw > 0 && size.vh > 0)) return null;
1135
+ if (options.temporalSmooth === false) this.resetTemporal();
1136
+ let estimateTarget = input;
1137
+ let scratch = null;
1138
+ let scratchW = size.vw;
1139
+ let scratchH = size.vh;
1140
+ if (typeof HTMLImageElement !== "undefined" && input instanceof HTMLImageElement || typeof HTMLVideoElement !== "undefined" && input instanceof HTMLVideoElement || typeof HTMLCanvasElement !== "undefined" && input instanceof HTMLCanvasElement) {
1141
+ const fit = Math.min(1, BLAZE_MAX_SIDE / Math.max(size.vw, size.vh));
1142
+ scratchW = Math.max(1, Math.round(size.vw * fit));
1143
+ scratchH = Math.max(1, Math.round(size.vh * fit));
1144
+ scratch = document.createElement("canvas");
1145
+ scratch.width = scratchW;
1146
+ scratch.height = scratchH;
1147
+ const ctx = scratch.getContext("2d");
1148
+ if (ctx) {
1149
+ ctx.drawImage(input, 0, 0, scratchW, scratchH);
1150
+ estimateTarget = scratch;
1151
+ }
1152
+ }
1100
1153
  const t0 = performance.now();
1101
- const poses = await this.detector.estimatePoses(input, {
1154
+ const poses = await this.detector.estimatePoses(estimateTarget, {
1102
1155
  flipHorizontal: false,
1103
1156
  maxPoses: 1
1104
1157
  });
@@ -1129,16 +1182,22 @@ var BlazePoseAdapter = class {
1129
1182
  const vh = size.vh || 1;
1130
1183
  const dispW = options.displayWidth || 1;
1131
1184
  const dispH = options.displayHeight || 1;
1185
+ let maxCoord = 0;
1186
+ for (const kp of pose.keypoints) {
1187
+ maxCoord = Math.max(maxCoord, Math.abs(kp.x || 0), Math.abs(kp.y || 0));
1188
+ }
1132
1189
  const byName = /* @__PURE__ */ new Map();
1190
+ const extrasRaw = [];
1133
1191
  for (const kp of pose.keypoints) {
1134
1192
  const name = String(kp.name || "").toLowerCase();
1193
+ const score = typeof kp.score === "number" ? kp.score : 0;
1194
+ const src = toSourcePixels(kp.x, kp.y, scratchW, scratchH, vw, vh, maxCoord);
1135
1195
  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
- });
1196
+ if (coco) {
1197
+ byName.set(coco, { x: src.x, y: src.y, score });
1198
+ } else if (FACE_EXTRA_NAMES.has(name)) {
1199
+ extrasRaw.push({ name, x: src.x, y: src.y, score });
1200
+ }
1142
1201
  }
1143
1202
  const videoKeypoints = COCO_KEYPOINT_NAMES.map((name) => {
1144
1203
  const k = byName.get(name);
@@ -1167,6 +1226,19 @@ var BlazePoseAdapter = class {
1167
1226
  score: k.score
1168
1227
  };
1169
1228
  });
1229
+ for (const extra of extrasRaw) {
1230
+ const dx = extra.x * scale + ox;
1231
+ const dy = extra.y * scale + oy;
1232
+ let nx = dispW > 0 ? dx / dispW : 0;
1233
+ const ny = dispH > 0 ? dy / dispH : 0;
1234
+ if (options.facingMode === "user") nx = 1 - nx;
1235
+ keypoints.push({
1236
+ name: extra.name,
1237
+ x: Math.min(1, Math.max(0, nx)),
1238
+ y: Math.min(1, Math.max(0, ny)),
1239
+ score: extra.score
1240
+ });
1241
+ }
1170
1242
  return {
1171
1243
  keypoints,
1172
1244
  score: scoreSum / 17,
@@ -1182,6 +1254,13 @@ var BlazePoseAdapter = class {
1182
1254
  }
1183
1255
  };
1184
1256
  }
1257
+ /** Clear cached ROI / landmark filters (call when the still changes). */
1258
+ resetTemporal() {
1259
+ try {
1260
+ this.detector?.reset?.();
1261
+ } catch {
1262
+ }
1263
+ }
1185
1264
  dispose() {
1186
1265
  try {
1187
1266
  this.detector?.dispose?.();
@@ -1205,13 +1284,14 @@ function inputSize(input) {
1205
1284
  return { vw: input.width || 0, vh: input.height || 0 };
1206
1285
  }
1207
1286
  var INPUT_SIZE = 192;
1208
- var SMOOTH_ALPHA = 0.5;
1287
+ var SMOOTH_ALPHA = 0.45;
1288
+ var STILL_TARGET_FILL = 0.52;
1209
1289
  function modelNormToVideo(xNorm, yNorm, lb) {
1210
1290
  const xSq = xNorm * INPUT_SIZE;
1211
1291
  const ySq = yNorm * INPUT_SIZE;
1212
1292
  return {
1213
- x: (xSq - lb.offsetX) * (lb.vw / lb.drawW),
1214
- y: (ySq - lb.offsetY) * (lb.vh / lb.drawH)
1293
+ x: (xSq - lb.offsetX) * (lb.vw / lb.drawW) + (lb.originX ?? 0),
1294
+ y: (ySq - lb.offsetY) * (lb.vh / lb.drawH) + (lb.originY ?? 0)
1215
1295
  };
1216
1296
  }
1217
1297
  function videoToCover(vx, vy, dispW, dispH, lb) {
@@ -1228,6 +1308,18 @@ var MoveNetGraphAdapter = class {
1228
1308
  this.offscreen = null;
1229
1309
  this.offCtx = null;
1230
1310
  this.smoothed = null;
1311
+ this.lastVideoKps = null;
1312
+ this.stillPass = 0;
1313
+ this.inferLb = {
1314
+ offsetX: 0,
1315
+ offsetY: 0,
1316
+ drawW: INPUT_SIZE,
1317
+ drawH: INPUT_SIZE,
1318
+ vw: 1,
1319
+ vh: 1,
1320
+ originX: 0,
1321
+ originY: 0
1322
+ };
1231
1323
  this.letterbox = {
1232
1324
  offsetX: 0,
1233
1325
  offsetY: 0,
@@ -1267,20 +1359,81 @@ var MoveNetGraphAdapter = class {
1267
1359
  if (!this.offCtx) throw new Error("2d context unavailable");
1268
1360
  return this.offCtx;
1269
1361
  }
1270
- prepareInput(input) {
1362
+ prepareInput(input, trackRoi) {
1271
1363
  const { vw: rawW, vh: rawH } = inputSize(input);
1272
1364
  const vw = rawW || 1;
1273
1365
  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;
1366
+ let winX = 0;
1367
+ let winY = 0;
1368
+ let winW = vw;
1369
+ let winH = vh;
1370
+ if (trackRoi && this.lastVideoKps && this.stillPass > 0) {
1371
+ const pts = this.lastVideoKps.filter((k) => k.score >= 0.15);
1372
+ if (pts.length >= 4) {
1373
+ let minx = Infinity;
1374
+ let miny = Infinity;
1375
+ let maxx = -Infinity;
1376
+ let maxy = -Infinity;
1377
+ for (const p of pts) {
1378
+ minx = Math.min(minx, p.xPx);
1379
+ miny = Math.min(miny, p.yPx);
1380
+ maxx = Math.max(maxx, p.xPx);
1381
+ maxy = Math.max(maxy, p.yPx);
1382
+ }
1383
+ const bw = Math.max(8, maxx - minx);
1384
+ const bh = Math.max(8, maxy - miny);
1385
+ const cx = (minx + maxx) / 2;
1386
+ const cy = (miny + maxy) / 2;
1387
+ winW = Math.max(bw / STILL_TARGET_FILL, vw * 0.35);
1388
+ winH = Math.max(bh / STILL_TARGET_FILL, vh * 0.35);
1389
+ const imgAspect = vw / vh;
1390
+ if (winW / winH < imgAspect) winW = winH * imgAspect;
1391
+ else winH = winW / imgAspect;
1392
+ winX = cx - winW / 2;
1393
+ winY = cy - winH / 2;
1394
+ }
1395
+ }
1396
+ const scale = Math.min(INPUT_SIZE / winW, INPUT_SIZE / winH);
1397
+ const drawW = winW * scale;
1398
+ const drawH = winH * scale;
1277
1399
  const offsetX = (INPUT_SIZE - drawW) / 2;
1278
1400
  const offsetY = (INPUT_SIZE - drawH) / 2;
1279
- this.letterbox = { offsetX, offsetY, drawW, drawH, vw, vh };
1401
+ this.inferLb = {
1402
+ offsetX,
1403
+ offsetY,
1404
+ drawW,
1405
+ drawH,
1406
+ vw: winW,
1407
+ vh: winH,
1408
+ originX: winX,
1409
+ originY: winY
1410
+ };
1411
+ this.letterbox = { offsetX: 0, offsetY: 0, drawW: vw, drawH: vh, vw, vh };
1280
1412
  const c2d = this.poseCanvasCtx();
1281
1413
  c2d.fillStyle = "#000";
1282
1414
  c2d.fillRect(0, 0, INPUT_SIZE, INPUT_SIZE);
1283
- c2d.drawImage(input, 0, 0, vw, vh, offsetX, offsetY, drawW, drawH);
1415
+ const srcX = Math.max(0, winX);
1416
+ const srcY = Math.max(0, winY);
1417
+ const srcR = Math.min(vw, winX + winW);
1418
+ const srcB = Math.min(vh, winY + winH);
1419
+ if (srcR > srcX && srcB > srcY) {
1420
+ const dx = offsetX + (srcX - winX) * (drawW / winW);
1421
+ const dy = offsetY + (srcY - winY) * (drawH / winH);
1422
+ const dw = (srcR - srcX) * (drawW / winW);
1423
+ const dh = (srcB - srcY) * (drawH / winH);
1424
+ c2d.drawImage(
1425
+ input,
1426
+ srcX,
1427
+ srcY,
1428
+ srcR - srcX,
1429
+ srcB - srcY,
1430
+ dx,
1431
+ dy,
1432
+ dw,
1433
+ dh
1434
+ );
1435
+ }
1436
+ this.stillPass += 1;
1284
1437
  return this.offscreen;
1285
1438
  }
1286
1439
  smooth(raw) {
@@ -1301,7 +1454,9 @@ var MoveNetGraphAdapter = class {
1301
1454
  const size = inputSize(input);
1302
1455
  if (!(size.vw > 0 && size.vh > 0)) return null;
1303
1456
  const t0 = performance.now();
1304
- const canvas = this.prepareInput(input);
1457
+ const isStill = typeof HTMLImageElement !== "undefined" && input instanceof HTMLImageElement;
1458
+ const trackRoi = options.temporalSmooth !== false && isStill;
1459
+ const canvas = this.prepareInput(input, trackRoi);
1305
1460
  const tensorIn = this.tf.tidy(
1306
1461
  () => this.tf.expandDims(this.tf.browser.fromPixels(canvas), 0)
1307
1462
  );
@@ -1312,7 +1467,8 @@ var MoveNetGraphAdapter = class {
1312
1467
  if (Array.isArray(out)) out.forEach((t) => t.dispose());
1313
1468
  else out.dispose();
1314
1469
  const inferenceMs = performance.now() - t0;
1315
- const lb = this.letterbox;
1470
+ const lb = this.inferLb;
1471
+ const coverLb = this.letterbox;
1316
1472
  const dispW = options.displayWidth || 1;
1317
1473
  const dispH = options.displayHeight || 1;
1318
1474
  const drawRaw = [];
@@ -1330,7 +1486,8 @@ var MoveNetGraphAdapter = class {
1330
1486
  });
1331
1487
  scoreSum += score;
1332
1488
  }
1333
- const sm = this.smooth(drawRaw);
1489
+ const sm = options.temporalSmooth === false ? drawRaw : this.smooth(drawRaw);
1490
+ this.lastVideoKps = sm.map((k) => ({ xPx: k.xPx, yPx: k.yPx, score: k.score }));
1334
1491
  const videoKeypoints = sm.map((k, i) => ({
1335
1492
  name: COCO_KEYPOINT_NAMES[i],
1336
1493
  xPx: k.xPx,
@@ -1338,7 +1495,7 @@ var MoveNetGraphAdapter = class {
1338
1495
  score: k.score
1339
1496
  }));
1340
1497
  const keypoints = videoKeypoints.map((k) => {
1341
- const d = videoToCover(k.xPx, k.yPx, dispW, dispH, lb);
1498
+ const d = videoToCover(k.xPx, k.yPx, dispW, dispH, coverLb);
1342
1499
  let nx = dispW > 0 ? d.x / dispW : 0;
1343
1500
  const ny = dispH > 0 ? d.y / dispH : 0;
1344
1501
  if (options.facingMode === "user") nx = 1 - nx;
@@ -1355,9 +1512,15 @@ var MoveNetGraphAdapter = class {
1355
1512
  score: scoreSum / 17,
1356
1513
  inferenceMs,
1357
1514
  videoKeypoints,
1358
- letterbox: lb
1515
+ letterbox: coverLb
1359
1516
  };
1360
1517
  }
1518
+ /** Clear EMA state (call when the still / clip changes). */
1519
+ resetTemporal() {
1520
+ this.smoothed = null;
1521
+ this.lastVideoKps = null;
1522
+ this.stillPass = 0;
1523
+ }
1361
1524
  dispose() {
1362
1525
  try {
1363
1526
  this.model?.dispose();
@@ -1365,6 +1528,8 @@ var MoveNetGraphAdapter = class {
1365
1528
  }
1366
1529
  this.model = null;
1367
1530
  this.smoothed = null;
1531
+ this.lastVideoKps = null;
1532
+ this.stillPass = 0;
1368
1533
  this.offscreen = null;
1369
1534
  this.offCtx = null;
1370
1535
  }
@@ -1985,7 +2150,11 @@ var PoseTrackerClient = class {
1985
2150
  if (this.poseSource.type === "camera" && this.poseSource.facingMode) {
1986
2151
  this.opts.facingMode = this.poseSource.facingMode;
1987
2152
  }
2153
+ if (this.adapter?.resetTemporal) {
2154
+ this.adapter.resetTemporal();
2155
+ }
1988
2156
  if (!this.shell) return;
2157
+ await this.ensureAdapter();
1989
2158
  await this.ensureSource();
1990
2159
  if (wasRunning || this.poseSource.type === "image") {
1991
2160
  if (this.poseSource.type === "image") this.imageShotPending = true;
@@ -2097,7 +2266,30 @@ var PoseTrackerClient = class {
2097
2266
  return this.configurePromise;
2098
2267
  }
2099
2268
  getAvailableExercises() {
2100
- return this.mode === "full-engine" ? this.manifest?.exercises ?? [] : [];
2269
+ if (this.mode !== "full-engine") return [];
2270
+ if (normalizeEngineChannel(this.opts.engine) === "v4") {
2271
+ const listed = this.engine?.listExercises?.() ?? [];
2272
+ if (listed.length) return listed.map((e) => this.v4ExerciseConfig(e));
2273
+ }
2274
+ return this.manifest?.exercises ?? [];
2275
+ }
2276
+ getEngineChannel() {
2277
+ return normalizeEngineChannel(this.opts.engine);
2278
+ }
2279
+ v4ExerciseConfig(entry) {
2280
+ const type = entry.type === "static" ? "static" : "dynamic";
2281
+ return {
2282
+ id: entry.id,
2283
+ name: entry.displayName || entry.id,
2284
+ type,
2285
+ movement: {
2286
+ name: entry.id,
2287
+ type,
2288
+ scale_acceptance: {},
2289
+ movement_steps: [],
2290
+ movement_initial_posture: null
2291
+ }
2292
+ };
2101
2293
  }
2102
2294
  getAvailableCustomExercises() {
2103
2295
  if (this.mode !== "full-engine" || !this.engine?.listCustomExercises) return [];
@@ -2133,6 +2325,28 @@ var PoseTrackerClient = class {
2133
2325
  throw new Error(FREE_PLAN_FEATURES_MESSAGE);
2134
2326
  }
2135
2327
  }
2328
+ if (requiresEngineV4(exerciseId) && normalizeEngineChannel(this.opts.engine) !== "v4") {
2329
+ const message = `Exercise '${exerciseId}' requires engine: 'v4'`;
2330
+ this.reportError({ type: "error", code: "invalid_exercise", message });
2331
+ throw new Error(message);
2332
+ }
2333
+ if (normalizeEngineChannel(this.opts.engine) === "v4") {
2334
+ const listed = this.engine.listExercises?.() ?? [];
2335
+ const v4Hit = listed.find((e) => e.id === exerciseId);
2336
+ if (v4Hit) {
2337
+ this.beginEngineSession(this.v4ExerciseConfig(v4Hit), options);
2338
+ return;
2339
+ }
2340
+ const customs = this.getAvailableCustomExercises();
2341
+ const custom = customs.find((e) => e.id === exerciseId) ?? findExerciseByIdOrAlias(exerciseId, customs);
2342
+ if (custom) {
2343
+ this.startCustomExercise(custom, options);
2344
+ return;
2345
+ }
2346
+ const message = `Exercise '${exerciseId}' is not available in V4 engine`;
2347
+ this.reportError({ type: "error", code: "invalid_exercise", message });
2348
+ throw new Error(message);
2349
+ }
2136
2350
  const available = this.getAvailableExercises();
2137
2351
  const exercise = findExerciseByIdOrAlias(exerciseId, available);
2138
2352
  if (!exercise) {
@@ -2146,8 +2360,12 @@ var PoseTrackerClient = class {
2146
2360
  this.reportError({ type: "error", code: "invalid_exercise", message });
2147
2361
  throw new Error(message);
2148
2362
  }
2363
+ this.beginEngineSession(exercise, options);
2364
+ }
2365
+ beginEngineSession(exercise, options) {
2366
+ if (!this.engine) return;
2149
2367
  this.stopExercise();
2150
- this.currentExerciseId = exerciseId;
2368
+ this.currentExerciseId = exercise.id;
2151
2369
  this.keypointsSuppressionLogged = false;
2152
2370
  this.session = this.engine.createSession(
2153
2371
  {
@@ -2514,13 +2732,19 @@ var PoseTrackerClient = class {
2514
2732
  if (!this.adapter || !this.shell || !this.estimateInput) return null;
2515
2733
  if (!this.frameReady(this.estimateInput)) return null;
2516
2734
  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;
2735
+ const clientW = this.shell.canvas.clientWidth || this.shell.root.clientWidth || 1;
2736
+ const clientH = this.shell.canvas.clientHeight || this.shell.root.clientHeight || 1;
2737
+ const media = mediaSize(input);
2738
+ const useMediaSpace = this.poseSource.type === "image" && media.width > 0 && media.height > 0;
2739
+ const dispW = useMediaSpace ? media.width : clientW;
2740
+ const dispH = useMediaSpace ? media.height : clientH;
2519
2741
  const facing = this.poseSource.type === "camera" ? facingFromOptions(this.opts) : "environment";
2742
+ const stillTracked = this.poseSource.type === "image" && this.opts.stillMode === "tracked";
2520
2743
  const result = await this.adapter.estimate(input, {
2521
2744
  facingMode: facing,
2522
2745
  displayWidth: dispW,
2523
- displayHeight: dispH
2746
+ displayHeight: dispH,
2747
+ temporalSmooth: this.poseSource.type === "camera" || this.poseSource.type === "video" || stillTracked
2524
2748
  });
2525
2749
  if (!result) return null;
2526
2750
  const ctx = this.shell.canvas.getContext("2d");
@@ -2528,12 +2752,12 @@ var PoseTrackerClient = class {
2528
2752
  const mapped = mapVideoKeypointsToDisplay(
2529
2753
  result.videoKeypoints,
2530
2754
  result.letterbox,
2531
- dispW,
2532
- dispH
2755
+ clientW,
2756
+ clientH
2533
2757
  );
2534
- drawSkeleton(ctx, mapped, dispW, dispH, true, this.skeletonDef);
2758
+ drawSkeleton(ctx, mapped, clientW, clientH, true, this.skeletonDef);
2535
2759
  } else if (ctx && this.opts.drawSkeleton === false) {
2536
- ctx.clearRect(0, 0, dispW, dispH);
2760
+ ctx.clearRect(0, 0, clientW, clientH);
2537
2761
  }
2538
2762
  const pose = {
2539
2763
  keypoints: result.keypoints,
@@ -2576,7 +2800,13 @@ var PoseTrackerClient = class {
2576
2800
  return pose;
2577
2801
  }
2578
2802
  async loop(token) {
2579
- if (token !== this.loopToken || !this.running || !this.adapter || !this.shell || this.disposed) {
2803
+ if (token !== this.loopToken || !this.running || !this.shell || this.disposed) {
2804
+ return;
2805
+ }
2806
+ if (!this.adapter) {
2807
+ requestAnimationFrame(() => {
2808
+ void this.loop(token);
2809
+ });
2580
2810
  return;
2581
2811
  }
2582
2812
  const input = this.estimateInput;
@@ -2584,8 +2814,8 @@ var PoseTrackerClient = class {
2584
2814
  if (shouldInfer) {
2585
2815
  this.busy = true;
2586
2816
  try {
2587
- await this.runOneFrame();
2588
- if (this.poseSource.type === "image") {
2817
+ const pose = await this.runOneFrame();
2818
+ if (this.poseSource.type === "image" && pose) {
2589
2819
  this.imageShotPending = false;
2590
2820
  this.running = false;
2591
2821
  this.busy = false;
@@ -2594,6 +2824,12 @@ var PoseTrackerClient = class {
2594
2824
  } catch (err) {
2595
2825
  const message = err instanceof Error ? err.message : String(err);
2596
2826
  this.emit({ type: "error", code: "internal", message: `inference: ${message}` });
2827
+ if (this.poseSource.type === "image") {
2828
+ this.imageShotPending = false;
2829
+ this.running = false;
2830
+ this.busy = false;
2831
+ return;
2832
+ }
2597
2833
  }
2598
2834
  this.busy = false;
2599
2835
  }
@@ -2982,6 +3218,7 @@ exports.SDK_VERSION = SDK_VERSION;
2982
3218
  exports.SkeletonFetchError = SkeletonFetchError;
2983
3219
  exports.TrackError = TrackError;
2984
3220
  exports.UsageTracker = UsageTracker;
3221
+ exports.V4_ONLY_EXERCISE_IDS = V4_ONLY_EXERCISE_IDS;
2985
3222
  exports.configure = configure;
2986
3223
  exports.createLocalFileStore = createLocalFileStore;
2987
3224
  exports.createLocalKeyValueStore = createLocalKeyValueStore;
@@ -3000,8 +3237,10 @@ exports.initWebGlBackend = initWebGlBackend;
3000
3237
  exports.isPaidPlan = isPaidPlan;
3001
3238
  exports.mapVideoKeypointsToDisplay = mapVideoKeypointsToDisplay;
3002
3239
  exports.mountDomShell = mountDomShell;
3240
+ exports.normalizeEngineChannel = normalizeEngineChannel;
3003
3241
  exports.normalizePoseSource = normalizePoseSource;
3004
3242
  exports.openCameraStream = openCameraStream;
3243
+ exports.requiresEngineV4 = requiresEngineV4;
3005
3244
  exports.resolveExerciseName = resolveExerciseName;
3006
3245
  exports.resolveFeatures = resolveFeatures;
3007
3246
  exports.resolvePoseDetection = resolvePoseDetection;