@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 +6 -2
- package/dist/index.cjs +303 -36
- package/dist/index.d.cts +78 -2
- package/dist/index.d.ts +78 -2
- package/dist/index.js +298 -37
- package/dist/pose-tracker.global.js +3 -3
- package/package.json +1 -1
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.
|
|
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:
|
|
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(
|
|
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 (
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
y:
|
|
1138
|
-
|
|
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.
|
|
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
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
|
2516
|
-
const
|
|
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
|
-
|
|
2530
|
-
|
|
2778
|
+
clientW,
|
|
2779
|
+
clientH
|
|
2531
2780
|
);
|
|
2532
|
-
drawSkeleton(ctx, mapped,
|
|
2781
|
+
drawSkeleton(ctx, mapped, clientW, clientH, true, this.skeletonDef);
|
|
2533
2782
|
} else if (ctx && this.opts.drawSkeleton === false) {
|
|
2534
|
-
ctx.clearRect(0, 0,
|
|
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.
|
|
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
|