@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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { sha256 } from 'js-sha256';
2
2
 
3
3
  // src/version.ts
4
- var SDK_VERSION = "0.2.0";
4
+ var SDK_VERSION = "0.3.1";
5
5
  var SDK_NAME = "posetracker-web";
6
6
 
7
7
  // src/api/configure.ts
@@ -22,7 +22,8 @@ async function configure(apiToken, options = {}) {
22
22
  targetPlatform: "web",
23
23
  poseModelProfile: options.poseModelProfile ?? "AdaptiveChoice",
24
24
  locale: options.locale,
25
- localVersions: options.localVersions
25
+ localVersions: options.localVersions,
26
+ engineChannel: options.engine === "v3" ? "v3" : "v4"
26
27
  };
27
28
  let response;
28
29
  try {
@@ -529,6 +530,10 @@ function waitForImageReady(img, timeoutMs = 15e3) {
529
530
  resolve();
530
531
  return;
531
532
  }
533
+ if (img.complete) {
534
+ reject(new Error("Image failed to decode (naturalWidth=0)"));
535
+ return;
536
+ }
532
537
  const onReady = () => {
533
538
  cleanup();
534
539
  resolve();
@@ -612,6 +617,31 @@ function findExerciseByIdOrAlias(exerciseId, exercises) {
612
617
  return exercises.find((e) => resolveExerciseName(e.id) === canonical);
613
618
  }
614
619
 
620
+ // src/engineChannel.ts
621
+ var V4_ONLY_EXERCISE_IDS = [
622
+ "shoulder_roll",
623
+ "shoulder_deep_breath",
624
+ "chair_forward_fold",
625
+ "chair_side_stretch"
626
+ ];
627
+ var PRODUCTION_SQUAT_IDS = ["squat", "face_squat"];
628
+ function isProductionSquatId(exerciseId) {
629
+ const id = String(exerciseId || "").trim().toLowerCase();
630
+ return id === "squat" || id === "face_squat";
631
+ }
632
+ function normalizeEngineChannel(raw) {
633
+ return raw === "v3" ? "v3" : "v4";
634
+ }
635
+ function resolveMovementEngine(opts = {}) {
636
+ if (opts.userExercise) return "v4";
637
+ const requested = String(opts.engine || "").trim().toLowerCase();
638
+ if (isProductionSquatId(opts.exercise) && requested !== "v4") return "v3";
639
+ return normalizeEngineChannel(opts.engine);
640
+ }
641
+ function requiresEngineV4(exerciseId) {
642
+ return V4_ONLY_EXERCISE_IDS.includes(exerciseId);
643
+ }
644
+
615
645
  // src/events/classicMessage.ts
616
646
  var ANGLE_RELATIONS = {
617
647
  left_knee: "from_hip_to_ankle",
@@ -1067,6 +1097,23 @@ var BLAZE_TO_COCO = {
1067
1097
  left_ankle: "left_ankle",
1068
1098
  right_ankle: "right_ankle"
1069
1099
  };
1100
+ var FACE_EXTRA_NAMES = /* @__PURE__ */ new Set([
1101
+ "left_eye_inner",
1102
+ "left_eye_outer",
1103
+ "right_eye_inner",
1104
+ "right_eye_outer",
1105
+ "mouth_left",
1106
+ "mouth_right"
1107
+ ]);
1108
+ var BLAZE_MAX_SIDE = 1280;
1109
+ function toSourcePixels(x, y, scratchW, scratchH, srcW, srcH, maxCoord) {
1110
+ if (maxCoord <= 1.5 && scratchW > 2) {
1111
+ return { x: x * srcW, y: y * srcH };
1112
+ }
1113
+ const sx = scratchW / srcW;
1114
+ const sy = scratchH / srcH;
1115
+ return { x: sx > 0 ? x / sx : x, y: sy > 0 ? y / sy : y };
1116
+ }
1070
1117
  var BlazePoseAdapter = class {
1071
1118
  constructor(_ctx) {
1072
1119
  this.modelId = "blazepose";
@@ -1087,7 +1134,7 @@ var BlazePoseAdapter = class {
1087
1134
  {
1088
1135
  runtime: "tfjs",
1089
1136
  modelType: this.modelType,
1090
- enableSmoothing: true
1137
+ enableSmoothing: false
1091
1138
  }
1092
1139
  );
1093
1140
  }
@@ -1095,8 +1142,26 @@ var BlazePoseAdapter = class {
1095
1142
  if (!this.detector) throw new Error("BlazePose adapter not loaded");
1096
1143
  const size = blazeInputSize(input);
1097
1144
  if (!(size.vw > 0 && size.vh > 0)) return null;
1145
+ if (options.temporalSmooth === false) this.resetTemporal();
1146
+ let estimateTarget = input;
1147
+ let scratch = null;
1148
+ let scratchW = size.vw;
1149
+ let scratchH = size.vh;
1150
+ if (typeof HTMLImageElement !== "undefined" && input instanceof HTMLImageElement || typeof HTMLVideoElement !== "undefined" && input instanceof HTMLVideoElement || typeof HTMLCanvasElement !== "undefined" && input instanceof HTMLCanvasElement) {
1151
+ const fit = Math.min(1, BLAZE_MAX_SIDE / Math.max(size.vw, size.vh));
1152
+ scratchW = Math.max(1, Math.round(size.vw * fit));
1153
+ scratchH = Math.max(1, Math.round(size.vh * fit));
1154
+ scratch = document.createElement("canvas");
1155
+ scratch.width = scratchW;
1156
+ scratch.height = scratchH;
1157
+ const ctx = scratch.getContext("2d");
1158
+ if (ctx) {
1159
+ ctx.drawImage(input, 0, 0, scratchW, scratchH);
1160
+ estimateTarget = scratch;
1161
+ }
1162
+ }
1098
1163
  const t0 = performance.now();
1099
- const poses = await this.detector.estimatePoses(input, {
1164
+ const poses = await this.detector.estimatePoses(estimateTarget, {
1100
1165
  flipHorizontal: false,
1101
1166
  maxPoses: 1
1102
1167
  });
@@ -1127,16 +1192,22 @@ var BlazePoseAdapter = class {
1127
1192
  const vh = size.vh || 1;
1128
1193
  const dispW = options.displayWidth || 1;
1129
1194
  const dispH = options.displayHeight || 1;
1195
+ let maxCoord = 0;
1196
+ for (const kp of pose.keypoints) {
1197
+ maxCoord = Math.max(maxCoord, Math.abs(kp.x || 0), Math.abs(kp.y || 0));
1198
+ }
1130
1199
  const byName = /* @__PURE__ */ new Map();
1200
+ const extrasRaw = [];
1131
1201
  for (const kp of pose.keypoints) {
1132
1202
  const name = String(kp.name || "").toLowerCase();
1203
+ const score = typeof kp.score === "number" ? kp.score : 0;
1204
+ const src = toSourcePixels(kp.x, kp.y, scratchW, scratchH, vw, vh, maxCoord);
1133
1205
  const coco = BLAZE_TO_COCO[name];
1134
- if (!coco) continue;
1135
- byName.set(coco, {
1136
- x: kp.x,
1137
- y: kp.y,
1138
- score: typeof kp.score === "number" ? kp.score : 0
1139
- });
1206
+ if (coco) {
1207
+ byName.set(coco, { x: src.x, y: src.y, score });
1208
+ } else if (FACE_EXTRA_NAMES.has(name)) {
1209
+ extrasRaw.push({ name, x: src.x, y: src.y, score });
1210
+ }
1140
1211
  }
1141
1212
  const videoKeypoints = COCO_KEYPOINT_NAMES.map((name) => {
1142
1213
  const k = byName.get(name);
@@ -1165,6 +1236,19 @@ var BlazePoseAdapter = class {
1165
1236
  score: k.score
1166
1237
  };
1167
1238
  });
1239
+ for (const extra of extrasRaw) {
1240
+ const dx = extra.x * scale + ox;
1241
+ const dy = extra.y * scale + oy;
1242
+ let nx = dispW > 0 ? dx / dispW : 0;
1243
+ const ny = dispH > 0 ? dy / dispH : 0;
1244
+ if (options.facingMode === "user") nx = 1 - nx;
1245
+ keypoints.push({
1246
+ name: extra.name,
1247
+ x: Math.min(1, Math.max(0, nx)),
1248
+ y: Math.min(1, Math.max(0, ny)),
1249
+ score: extra.score
1250
+ });
1251
+ }
1168
1252
  return {
1169
1253
  keypoints,
1170
1254
  score: scoreSum / 17,
@@ -1180,6 +1264,13 @@ var BlazePoseAdapter = class {
1180
1264
  }
1181
1265
  };
1182
1266
  }
1267
+ /** Clear cached ROI / landmark filters (call when the still changes). */
1268
+ resetTemporal() {
1269
+ try {
1270
+ this.detector?.reset?.();
1271
+ } catch {
1272
+ }
1273
+ }
1183
1274
  dispose() {
1184
1275
  try {
1185
1276
  this.detector?.dispose?.();
@@ -1203,13 +1294,14 @@ function inputSize(input) {
1203
1294
  return { vw: input.width || 0, vh: input.height || 0 };
1204
1295
  }
1205
1296
  var INPUT_SIZE = 192;
1206
- var SMOOTH_ALPHA = 0.5;
1297
+ var SMOOTH_ALPHA = 0.45;
1298
+ var STILL_TARGET_FILL = 0.52;
1207
1299
  function modelNormToVideo(xNorm, yNorm, lb) {
1208
1300
  const xSq = xNorm * INPUT_SIZE;
1209
1301
  const ySq = yNorm * INPUT_SIZE;
1210
1302
  return {
1211
- x: (xSq - lb.offsetX) * (lb.vw / lb.drawW),
1212
- y: (ySq - lb.offsetY) * (lb.vh / lb.drawH)
1303
+ x: (xSq - lb.offsetX) * (lb.vw / lb.drawW) + (lb.originX ?? 0),
1304
+ y: (ySq - lb.offsetY) * (lb.vh / lb.drawH) + (lb.originY ?? 0)
1213
1305
  };
1214
1306
  }
1215
1307
  function videoToCover(vx, vy, dispW, dispH, lb) {
@@ -1226,6 +1318,18 @@ var MoveNetGraphAdapter = class {
1226
1318
  this.offscreen = null;
1227
1319
  this.offCtx = null;
1228
1320
  this.smoothed = null;
1321
+ this.lastVideoKps = null;
1322
+ this.stillPass = 0;
1323
+ this.inferLb = {
1324
+ offsetX: 0,
1325
+ offsetY: 0,
1326
+ drawW: INPUT_SIZE,
1327
+ drawH: INPUT_SIZE,
1328
+ vw: 1,
1329
+ vh: 1,
1330
+ originX: 0,
1331
+ originY: 0
1332
+ };
1229
1333
  this.letterbox = {
1230
1334
  offsetX: 0,
1231
1335
  offsetY: 0,
@@ -1265,20 +1369,81 @@ var MoveNetGraphAdapter = class {
1265
1369
  if (!this.offCtx) throw new Error("2d context unavailable");
1266
1370
  return this.offCtx;
1267
1371
  }
1268
- prepareInput(input) {
1372
+ prepareInput(input, trackRoi) {
1269
1373
  const { vw: rawW, vh: rawH } = inputSize(input);
1270
1374
  const vw = rawW || 1;
1271
1375
  const vh = rawH || 1;
1272
- const scale = Math.min(INPUT_SIZE / vw, INPUT_SIZE / vh);
1273
- const drawW = vw * scale;
1274
- const drawH = vh * scale;
1376
+ let winX = 0;
1377
+ let winY = 0;
1378
+ let winW = vw;
1379
+ let winH = vh;
1380
+ if (trackRoi && this.lastVideoKps && this.stillPass > 0) {
1381
+ const pts = this.lastVideoKps.filter((k) => k.score >= 0.15);
1382
+ if (pts.length >= 4) {
1383
+ let minx = Infinity;
1384
+ let miny = Infinity;
1385
+ let maxx = -Infinity;
1386
+ let maxy = -Infinity;
1387
+ for (const p of pts) {
1388
+ minx = Math.min(minx, p.xPx);
1389
+ miny = Math.min(miny, p.yPx);
1390
+ maxx = Math.max(maxx, p.xPx);
1391
+ maxy = Math.max(maxy, p.yPx);
1392
+ }
1393
+ const bw = Math.max(8, maxx - minx);
1394
+ const bh = Math.max(8, maxy - miny);
1395
+ const cx = (minx + maxx) / 2;
1396
+ const cy = (miny + maxy) / 2;
1397
+ winW = Math.max(bw / STILL_TARGET_FILL, vw * 0.35);
1398
+ winH = Math.max(bh / STILL_TARGET_FILL, vh * 0.35);
1399
+ const imgAspect = vw / vh;
1400
+ if (winW / winH < imgAspect) winW = winH * imgAspect;
1401
+ else winH = winW / imgAspect;
1402
+ winX = cx - winW / 2;
1403
+ winY = cy - winH / 2;
1404
+ }
1405
+ }
1406
+ const scale = Math.min(INPUT_SIZE / winW, INPUT_SIZE / winH);
1407
+ const drawW = winW * scale;
1408
+ const drawH = winH * scale;
1275
1409
  const offsetX = (INPUT_SIZE - drawW) / 2;
1276
1410
  const offsetY = (INPUT_SIZE - drawH) / 2;
1277
- this.letterbox = { offsetX, offsetY, drawW, drawH, vw, vh };
1411
+ this.inferLb = {
1412
+ offsetX,
1413
+ offsetY,
1414
+ drawW,
1415
+ drawH,
1416
+ vw: winW,
1417
+ vh: winH,
1418
+ originX: winX,
1419
+ originY: winY
1420
+ };
1421
+ this.letterbox = { offsetX: 0, offsetY: 0, drawW: vw, drawH: vh, vw, vh };
1278
1422
  const c2d = this.poseCanvasCtx();
1279
1423
  c2d.fillStyle = "#000";
1280
1424
  c2d.fillRect(0, 0, INPUT_SIZE, INPUT_SIZE);
1281
- c2d.drawImage(input, 0, 0, vw, vh, offsetX, offsetY, drawW, drawH);
1425
+ const srcX = Math.max(0, winX);
1426
+ const srcY = Math.max(0, winY);
1427
+ const srcR = Math.min(vw, winX + winW);
1428
+ const srcB = Math.min(vh, winY + winH);
1429
+ if (srcR > srcX && srcB > srcY) {
1430
+ const dx = offsetX + (srcX - winX) * (drawW / winW);
1431
+ const dy = offsetY + (srcY - winY) * (drawH / winH);
1432
+ const dw = (srcR - srcX) * (drawW / winW);
1433
+ const dh = (srcB - srcY) * (drawH / winH);
1434
+ c2d.drawImage(
1435
+ input,
1436
+ srcX,
1437
+ srcY,
1438
+ srcR - srcX,
1439
+ srcB - srcY,
1440
+ dx,
1441
+ dy,
1442
+ dw,
1443
+ dh
1444
+ );
1445
+ }
1446
+ this.stillPass += 1;
1282
1447
  return this.offscreen;
1283
1448
  }
1284
1449
  smooth(raw) {
@@ -1299,7 +1464,9 @@ var MoveNetGraphAdapter = class {
1299
1464
  const size = inputSize(input);
1300
1465
  if (!(size.vw > 0 && size.vh > 0)) return null;
1301
1466
  const t0 = performance.now();
1302
- const canvas = this.prepareInput(input);
1467
+ const isStill = typeof HTMLImageElement !== "undefined" && input instanceof HTMLImageElement;
1468
+ const trackRoi = options.temporalSmooth !== false && isStill;
1469
+ const canvas = this.prepareInput(input, trackRoi);
1303
1470
  const tensorIn = this.tf.tidy(
1304
1471
  () => this.tf.expandDims(this.tf.browser.fromPixels(canvas), 0)
1305
1472
  );
@@ -1310,7 +1477,8 @@ var MoveNetGraphAdapter = class {
1310
1477
  if (Array.isArray(out)) out.forEach((t) => t.dispose());
1311
1478
  else out.dispose();
1312
1479
  const inferenceMs = performance.now() - t0;
1313
- const lb = this.letterbox;
1480
+ const lb = this.inferLb;
1481
+ const coverLb = this.letterbox;
1314
1482
  const dispW = options.displayWidth || 1;
1315
1483
  const dispH = options.displayHeight || 1;
1316
1484
  const drawRaw = [];
@@ -1328,7 +1496,8 @@ var MoveNetGraphAdapter = class {
1328
1496
  });
1329
1497
  scoreSum += score;
1330
1498
  }
1331
- const sm = this.smooth(drawRaw);
1499
+ const sm = options.temporalSmooth === false ? drawRaw : this.smooth(drawRaw);
1500
+ this.lastVideoKps = sm.map((k) => ({ xPx: k.xPx, yPx: k.yPx, score: k.score }));
1332
1501
  const videoKeypoints = sm.map((k, i) => ({
1333
1502
  name: COCO_KEYPOINT_NAMES[i],
1334
1503
  xPx: k.xPx,
@@ -1336,7 +1505,7 @@ var MoveNetGraphAdapter = class {
1336
1505
  score: k.score
1337
1506
  }));
1338
1507
  const keypoints = videoKeypoints.map((k) => {
1339
- const d = videoToCover(k.xPx, k.yPx, dispW, dispH, lb);
1508
+ const d = videoToCover(k.xPx, k.yPx, dispW, dispH, coverLb);
1340
1509
  let nx = dispW > 0 ? d.x / dispW : 0;
1341
1510
  const ny = dispH > 0 ? d.y / dispH : 0;
1342
1511
  if (options.facingMode === "user") nx = 1 - nx;
@@ -1353,9 +1522,15 @@ var MoveNetGraphAdapter = class {
1353
1522
  score: scoreSum / 17,
1354
1523
  inferenceMs,
1355
1524
  videoKeypoints,
1356
- letterbox: lb
1525
+ letterbox: coverLb
1357
1526
  };
1358
1527
  }
1528
+ /** Clear EMA state (call when the still / clip changes). */
1529
+ resetTemporal() {
1530
+ this.smoothed = null;
1531
+ this.lastVideoKps = null;
1532
+ this.stillPass = 0;
1533
+ }
1359
1534
  dispose() {
1360
1535
  try {
1361
1536
  this.model?.dispose();
@@ -1363,6 +1538,8 @@ var MoveNetGraphAdapter = class {
1363
1538
  }
1364
1539
  this.model = null;
1365
1540
  this.smoothed = null;
1541
+ this.lastVideoKps = null;
1542
+ this.stillPass = 0;
1366
1543
  this.offscreen = null;
1367
1544
  this.offCtx = null;
1368
1545
  }
@@ -1983,7 +2160,11 @@ var PoseTrackerClient = class {
1983
2160
  if (this.poseSource.type === "camera" && this.poseSource.facingMode) {
1984
2161
  this.opts.facingMode = this.poseSource.facingMode;
1985
2162
  }
2163
+ if (this.adapter?.resetTemporal) {
2164
+ this.adapter.resetTemporal();
2165
+ }
1986
2166
  if (!this.shell) return;
2167
+ await this.ensureAdapter();
1987
2168
  await this.ensureSource();
1988
2169
  if (wasRunning || this.poseSource.type === "image") {
1989
2170
  if (this.poseSource.type === "image") this.imageShotPending = true;
@@ -2095,7 +2276,30 @@ var PoseTrackerClient = class {
2095
2276
  return this.configurePromise;
2096
2277
  }
2097
2278
  getAvailableExercises() {
2098
- return this.mode === "full-engine" ? this.manifest?.exercises ?? [] : [];
2279
+ if (this.mode !== "full-engine") return [];
2280
+ if (normalizeEngineChannel(this.opts.engine) === "v4") {
2281
+ const listed = this.engine?.listExercises?.() ?? [];
2282
+ if (listed.length) return listed.map((e) => this.v4ExerciseConfig(e));
2283
+ }
2284
+ return this.manifest?.exercises ?? [];
2285
+ }
2286
+ getEngineChannel() {
2287
+ return normalizeEngineChannel(this.opts.engine);
2288
+ }
2289
+ v4ExerciseConfig(entry) {
2290
+ const type = entry.type === "static" ? "static" : "dynamic";
2291
+ return {
2292
+ id: entry.id,
2293
+ name: entry.displayName || entry.id,
2294
+ type,
2295
+ movement: {
2296
+ name: entry.id,
2297
+ type,
2298
+ scale_acceptance: {},
2299
+ movement_steps: [],
2300
+ movement_initial_posture: null
2301
+ }
2302
+ };
2099
2303
  }
2100
2304
  getAvailableCustomExercises() {
2101
2305
  if (this.mode !== "full-engine" || !this.engine?.listCustomExercises) return [];
@@ -2131,6 +2335,40 @@ var PoseTrackerClient = class {
2131
2335
  throw new Error(FREE_PLAN_FEATURES_MESSAGE);
2132
2336
  }
2133
2337
  }
2338
+ if (requiresEngineV4(exerciseId) && normalizeEngineChannel(this.opts.engine) !== "v4") {
2339
+ const message = `Exercise '${exerciseId}' requires engine: 'v4'`;
2340
+ this.reportError({ type: "error", code: "invalid_exercise", message });
2341
+ throw new Error(message);
2342
+ }
2343
+ const jumpId = resolveExerciseName(exerciseId) || exerciseId;
2344
+ const sessionEngine = resolveMovementEngine({
2345
+ engine: this.opts.engine,
2346
+ exercise: jumpId
2347
+ });
2348
+ if (sessionEngine === "v4") {
2349
+ const listed = this.engine.listExercises?.() ?? [];
2350
+ const v4Hit = listed.find((e) => e.id === exerciseId) ?? listed.find((e) => e.id === jumpId);
2351
+ if (v4Hit) {
2352
+ this.beginEngineSession(this.v4ExerciseConfig(v4Hit), options);
2353
+ return;
2354
+ }
2355
+ const customs = this.getAvailableCustomExercises();
2356
+ const custom = customs.find((e) => e.id === exerciseId) ?? findExerciseByIdOrAlias(exerciseId, customs);
2357
+ if (custom) {
2358
+ this.startCustomExercise(custom, options);
2359
+ return;
2360
+ }
2361
+ const message = `Exercise '${exerciseId}' is not available in V4 engine`;
2362
+ this.reportError({ type: "error", code: "invalid_exercise", message });
2363
+ throw new Error(message);
2364
+ }
2365
+ if (isProductionSquatId(jumpId) && normalizeEngineChannel(this.opts.engine) === "v4") {
2366
+ this.beginEngineSession(
2367
+ this.v4ExerciseConfig({ id: jumpId || "squat", displayName: "Squat", type: "dynamic" }),
2368
+ options
2369
+ );
2370
+ return;
2371
+ }
2134
2372
  const available = this.getAvailableExercises();
2135
2373
  const exercise = findExerciseByIdOrAlias(exerciseId, available);
2136
2374
  if (!exercise) {
@@ -2144,8 +2382,12 @@ var PoseTrackerClient = class {
2144
2382
  this.reportError({ type: "error", code: "invalid_exercise", message });
2145
2383
  throw new Error(message);
2146
2384
  }
2385
+ this.beginEngineSession(exercise, options);
2386
+ }
2387
+ beginEngineSession(exercise, options) {
2388
+ if (!this.engine) return;
2147
2389
  this.stopExercise();
2148
- this.currentExerciseId = exerciseId;
2390
+ this.currentExerciseId = exercise.id;
2149
2391
  this.keypointsSuppressionLogged = false;
2150
2392
  this.session = this.engine.createSession(
2151
2393
  {
@@ -2153,6 +2395,7 @@ var PoseTrackerClient = class {
2153
2395
  locale: this.opts.locale ?? "en",
2154
2396
  difficulty: options.difficulty,
2155
2397
  minGrade: this.features.minGrade ?? void 0,
2398
+ v4CatalogSquat: this.opts.engine === "v4",
2156
2399
  features: {
2157
2400
  angles: this.features.angles,
2158
2401
  recommendations: this.features.recommendations,
@@ -2512,13 +2755,19 @@ var PoseTrackerClient = class {
2512
2755
  if (!this.adapter || !this.shell || !this.estimateInput) return null;
2513
2756
  if (!this.frameReady(this.estimateInput)) return null;
2514
2757
  const input = this.estimateInput;
2515
- const dispW = this.shell.canvas.clientWidth || this.shell.root.clientWidth || 1;
2516
- const dispH = this.shell.canvas.clientHeight || this.shell.root.clientHeight || 1;
2758
+ const clientW = this.shell.canvas.clientWidth || this.shell.root.clientWidth || 1;
2759
+ const clientH = this.shell.canvas.clientHeight || this.shell.root.clientHeight || 1;
2760
+ const media = mediaSize(input);
2761
+ const useMediaSpace = this.poseSource.type === "image" && media.width > 0 && media.height > 0;
2762
+ const dispW = useMediaSpace ? media.width : clientW;
2763
+ const dispH = useMediaSpace ? media.height : clientH;
2517
2764
  const facing = this.poseSource.type === "camera" ? facingFromOptions(this.opts) : "environment";
2765
+ const stillTracked = this.poseSource.type === "image" && this.opts.stillMode === "tracked";
2518
2766
  const result = await this.adapter.estimate(input, {
2519
2767
  facingMode: facing,
2520
2768
  displayWidth: dispW,
2521
- displayHeight: dispH
2769
+ displayHeight: dispH,
2770
+ temporalSmooth: this.poseSource.type === "camera" || this.poseSource.type === "video" || stillTracked
2522
2771
  });
2523
2772
  if (!result) return null;
2524
2773
  const ctx = this.shell.canvas.getContext("2d");
@@ -2526,12 +2775,12 @@ var PoseTrackerClient = class {
2526
2775
  const mapped = mapVideoKeypointsToDisplay(
2527
2776
  result.videoKeypoints,
2528
2777
  result.letterbox,
2529
- dispW,
2530
- dispH
2778
+ clientW,
2779
+ clientH
2531
2780
  );
2532
- drawSkeleton(ctx, mapped, dispW, dispH, true, this.skeletonDef);
2781
+ drawSkeleton(ctx, mapped, clientW, clientH, true, this.skeletonDef);
2533
2782
  } else if (ctx && this.opts.drawSkeleton === false) {
2534
- ctx.clearRect(0, 0, dispW, dispH);
2783
+ ctx.clearRect(0, 0, clientW, clientH);
2535
2784
  }
2536
2785
  const pose = {
2537
2786
  keypoints: result.keypoints,
@@ -2574,7 +2823,13 @@ var PoseTrackerClient = class {
2574
2823
  return pose;
2575
2824
  }
2576
2825
  async loop(token) {
2577
- if (token !== this.loopToken || !this.running || !this.adapter || !this.shell || this.disposed) {
2826
+ if (token !== this.loopToken || !this.running || !this.shell || this.disposed) {
2827
+ return;
2828
+ }
2829
+ if (!this.adapter) {
2830
+ requestAnimationFrame(() => {
2831
+ void this.loop(token);
2832
+ });
2578
2833
  return;
2579
2834
  }
2580
2835
  const input = this.estimateInput;
@@ -2582,8 +2837,8 @@ var PoseTrackerClient = class {
2582
2837
  if (shouldInfer) {
2583
2838
  this.busy = true;
2584
2839
  try {
2585
- await this.runOneFrame();
2586
- if (this.poseSource.type === "image") {
2840
+ const pose = await this.runOneFrame();
2841
+ if (this.poseSource.type === "image" && pose) {
2587
2842
  this.imageShotPending = false;
2588
2843
  this.running = false;
2589
2844
  this.busy = false;
@@ -2592,6 +2847,12 @@ var PoseTrackerClient = class {
2592
2847
  } catch (err) {
2593
2848
  const message = err instanceof Error ? err.message : String(err);
2594
2849
  this.emit({ type: "error", code: "internal", message: `inference: ${message}` });
2850
+ if (this.poseSource.type === "image") {
2851
+ this.imageShotPending = false;
2852
+ this.running = false;
2853
+ this.busy = false;
2854
+ return;
2855
+ }
2595
2856
  }
2596
2857
  this.busy = false;
2597
2858
  }
@@ -2953,6 +3214,6 @@ var PoseCamera = {
2953
3214
  mount: PoseCamera_mount
2954
3215
  };
2955
3216
 
2956
- export { BRAND_COLORS, BlazePoseAdapter, COCO_KEYPOINT_NAMES, COMBINED_REFERENCE_EXERCISE_MESSAGE, ConfigureError, DEFAULT_BASE_URL, DEFAULT_FEATURES, DEFAULT_LOADING_TEXT, DEFAULT_MOVENET_LIGHTNING_URL, DEFAULT_SKELETON, DEFAULT_SKELETON_DEFINITION, EXERCISE_ALIASES, EngineLoader, FREE_PLAN_FEATURES_MESSAGE, INVALID_TOKEN_MESSAGE, MoveNetGraphAdapter, ONLINE_POSE_DETECTION_VERSION, ONLINE_TFJS_VERSION, POSETRACKER_LOGO_URL, PoseCamera, PoseCamera_mount, PoseTrackerClient, SDK_NAME, SDK_VERSION, SkeletonFetchError, TrackError, UsageTracker, configure, createLocalFileStore, createLocalKeyValueStore, createMemoryFileStore, createPoseAdapter, createPoseTracker, createWebFileStore, defaultPoseDetectionCdnUrl, defaultTfjsCdnUrls, drawSkeleton, featureNotSupportedMessage, fetchSkeletonDefinition, findExerciseByIdOrAlias, freeBlockedFeatures, initWebGlBackend, isPaidPlan, mapVideoKeypointsToDisplay, mountDomShell, normalizePoseSource, openCameraStream, resolveExerciseName, resolveFeatures, resolvePoseDetection, resolvePoseModel, resolveTf, shouldShowWatermark, stopMediaStream, toClassicNativeMessage };
3217
+ export { BRAND_COLORS, BlazePoseAdapter, COCO_KEYPOINT_NAMES, COMBINED_REFERENCE_EXERCISE_MESSAGE, ConfigureError, DEFAULT_BASE_URL, DEFAULT_FEATURES, DEFAULT_LOADING_TEXT, DEFAULT_MOVENET_LIGHTNING_URL, DEFAULT_SKELETON, DEFAULT_SKELETON_DEFINITION, EXERCISE_ALIASES, EngineLoader, FREE_PLAN_FEATURES_MESSAGE, INVALID_TOKEN_MESSAGE, MoveNetGraphAdapter, ONLINE_POSE_DETECTION_VERSION, ONLINE_TFJS_VERSION, POSETRACKER_LOGO_URL, PRODUCTION_SQUAT_IDS, PoseCamera, PoseCamera_mount, PoseTrackerClient, SDK_NAME, SDK_VERSION, SkeletonFetchError, TrackError, UsageTracker, V4_ONLY_EXERCISE_IDS, configure, createLocalFileStore, createLocalKeyValueStore, createMemoryFileStore, createPoseAdapter, createPoseTracker, createWebFileStore, defaultPoseDetectionCdnUrl, defaultTfjsCdnUrls, drawSkeleton, featureNotSupportedMessage, fetchSkeletonDefinition, findExerciseByIdOrAlias, freeBlockedFeatures, initWebGlBackend, isPaidPlan, isProductionSquatId, mapVideoKeypointsToDisplay, mountDomShell, normalizeEngineChannel, normalizePoseSource, openCameraStream, requiresEngineV4, resolveExerciseName, resolveFeatures, resolveMovementEngine, resolvePoseDetection, resolvePoseModel, resolveTf, shouldShowWatermark, stopMediaStream, toClassicNativeMessage };
2957
3218
  //# sourceMappingURL=index.js.map
2958
3219
  //# sourceMappingURL=index.js.map