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