@pose-tracker/pose-estimation-web 0.1.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 +25 -4
- package/dist/index.cjs +797 -151
- package/dist/index.d.cts +151 -11
- package/dist/index.d.ts +151 -11
- package/dist/index.js +794 -152
- package/dist/pose-tracker.global.js +6 -5
- package/package.json +1 -1
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.
|
|
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 {
|
|
@@ -461,6 +462,135 @@ function stopMediaStream(stream) {
|
|
|
461
462
|
}
|
|
462
463
|
}
|
|
463
464
|
|
|
465
|
+
// src/media/resolveMedia.ts
|
|
466
|
+
function isVideoElement(v) {
|
|
467
|
+
return typeof HTMLVideoElement !== "undefined" && v instanceof HTMLVideoElement;
|
|
468
|
+
}
|
|
469
|
+
function isImageElement(v) {
|
|
470
|
+
return typeof HTMLImageElement !== "undefined" && v instanceof HTMLImageElement;
|
|
471
|
+
}
|
|
472
|
+
function isImageBitmap(v) {
|
|
473
|
+
return typeof ImageBitmap !== "undefined" && v instanceof ImageBitmap;
|
|
474
|
+
}
|
|
475
|
+
function mediaSize(el) {
|
|
476
|
+
if (isImageBitmap(el)) {
|
|
477
|
+
return { width: el.width || 0, height: el.height || 0 };
|
|
478
|
+
}
|
|
479
|
+
if (isVideoElement(el)) {
|
|
480
|
+
return { width: el.videoWidth || 0, height: el.videoHeight || 0 };
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
width: el.naturalWidth || el.width || 0,
|
|
484
|
+
height: el.naturalHeight || el.height || 0
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
async function resolveObjectUrl(src) {
|
|
488
|
+
if (typeof src === "string") {
|
|
489
|
+
return { url: src, revoke: () => void 0 };
|
|
490
|
+
}
|
|
491
|
+
const url = URL.createObjectURL(src);
|
|
492
|
+
return {
|
|
493
|
+
url,
|
|
494
|
+
revoke: () => {
|
|
495
|
+
try {
|
|
496
|
+
URL.revokeObjectURL(url);
|
|
497
|
+
} catch {
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function waitForVideoReady(video, timeoutMs = 15e3) {
|
|
503
|
+
return new Promise((resolve, reject) => {
|
|
504
|
+
if (video.readyState >= 2 && video.videoWidth > 0) {
|
|
505
|
+
resolve();
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const onReady = () => {
|
|
509
|
+
cleanup();
|
|
510
|
+
resolve();
|
|
511
|
+
};
|
|
512
|
+
const onError = () => {
|
|
513
|
+
cleanup();
|
|
514
|
+
reject(new Error("Failed to load video source"));
|
|
515
|
+
};
|
|
516
|
+
const timer = setTimeout(() => {
|
|
517
|
+
cleanup();
|
|
518
|
+
reject(new Error("Timed out waiting for video metadata"));
|
|
519
|
+
}, timeoutMs);
|
|
520
|
+
const cleanup = () => {
|
|
521
|
+
clearTimeout(timer);
|
|
522
|
+
video.removeEventListener("loadeddata", onReady);
|
|
523
|
+
video.removeEventListener("error", onError);
|
|
524
|
+
};
|
|
525
|
+
video.addEventListener("loadeddata", onReady);
|
|
526
|
+
video.addEventListener("error", onError);
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
function waitForImageReady(img, timeoutMs = 15e3) {
|
|
530
|
+
return new Promise((resolve, reject) => {
|
|
531
|
+
if (img.complete && img.naturalWidth > 0) {
|
|
532
|
+
resolve();
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (img.complete) {
|
|
536
|
+
reject(new Error("Image failed to decode (naturalWidth=0)"));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const onReady = () => {
|
|
540
|
+
cleanup();
|
|
541
|
+
resolve();
|
|
542
|
+
};
|
|
543
|
+
const onError = () => {
|
|
544
|
+
cleanup();
|
|
545
|
+
reject(new Error("Failed to load image source"));
|
|
546
|
+
};
|
|
547
|
+
const timer = setTimeout(() => {
|
|
548
|
+
cleanup();
|
|
549
|
+
reject(new Error("Timed out waiting for image"));
|
|
550
|
+
}, timeoutMs);
|
|
551
|
+
const cleanup = () => {
|
|
552
|
+
clearTimeout(timer);
|
|
553
|
+
img.removeEventListener("load", onReady);
|
|
554
|
+
img.removeEventListener("error", onError);
|
|
555
|
+
};
|
|
556
|
+
img.addEventListener("load", onReady);
|
|
557
|
+
img.addEventListener("error", onError);
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
async function imageBitmapToObjectUrl(bitmap) {
|
|
561
|
+
const canvas = document.createElement("canvas");
|
|
562
|
+
canvas.width = bitmap.width;
|
|
563
|
+
canvas.height = bitmap.height;
|
|
564
|
+
const ctx = canvas.getContext("2d");
|
|
565
|
+
if (!ctx) throw new Error("2d context unavailable");
|
|
566
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
567
|
+
const blob = await new Promise((resolve, reject) => {
|
|
568
|
+
canvas.toBlob((b) => b ? resolve(b) : reject(new Error("toBlob failed")), "image/png");
|
|
569
|
+
});
|
|
570
|
+
return resolveObjectUrl(blob);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/types/source.ts
|
|
574
|
+
function normalizePoseSource(source, extras) {
|
|
575
|
+
if (source && typeof source === "object" && "type" in source) {
|
|
576
|
+
return source;
|
|
577
|
+
}
|
|
578
|
+
const kind = source === "video" || source === "image" || source === "camera" ? source : "camera";
|
|
579
|
+
if (kind === "camera") {
|
|
580
|
+
return { type: "camera", facingMode: extras?.facingMode };
|
|
581
|
+
}
|
|
582
|
+
const src = extras?.sourceFile ?? extras?.sourceUrl;
|
|
583
|
+
if (!src) {
|
|
584
|
+
throw new Error(
|
|
585
|
+
`PoseSource type "${kind}" requires sourceUrl or sourceFile (or pass a full PoseSource object)`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
if (kind === "video") {
|
|
589
|
+
return { type: "video", src };
|
|
590
|
+
}
|
|
591
|
+
return { type: "image", src };
|
|
592
|
+
}
|
|
593
|
+
|
|
464
594
|
// src/exercises/aliases.ts
|
|
465
595
|
var EXERCISE_ALIASES = {
|
|
466
596
|
face_squat: "squat",
|
|
@@ -489,6 +619,19 @@ function findExerciseByIdOrAlias(exerciseId, exercises) {
|
|
|
489
619
|
return exercises.find((e) => resolveExerciseName(e.id) === canonical);
|
|
490
620
|
}
|
|
491
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
|
+
|
|
492
635
|
// src/events/classicMessage.ts
|
|
493
636
|
var ANGLE_RELATIONS = {
|
|
494
637
|
left_knee: "from_hip_to_ankle",
|
|
@@ -913,6 +1056,18 @@ async function resolvePoseDetection(options) {
|
|
|
913
1056
|
}
|
|
914
1057
|
|
|
915
1058
|
// src/models/adapters/blazepose.ts
|
|
1059
|
+
function blazeInputSize(input) {
|
|
1060
|
+
if (typeof HTMLVideoElement !== "undefined" && input instanceof HTMLVideoElement) {
|
|
1061
|
+
return { vw: input.videoWidth || 0, vh: input.videoHeight || 0 };
|
|
1062
|
+
}
|
|
1063
|
+
if (typeof HTMLImageElement !== "undefined" && input instanceof HTMLImageElement) {
|
|
1064
|
+
return { vw: input.naturalWidth || input.width || 0, vh: input.naturalHeight || input.height || 0 };
|
|
1065
|
+
}
|
|
1066
|
+
if (typeof HTMLCanvasElement !== "undefined" && input instanceof HTMLCanvasElement) {
|
|
1067
|
+
return { vw: input.width || 0, vh: input.height || 0 };
|
|
1068
|
+
}
|
|
1069
|
+
return { vw: input.width || 0, vh: input.height || 0 };
|
|
1070
|
+
}
|
|
916
1071
|
var BLAZE_TO_COCO = {
|
|
917
1072
|
nose: "nose",
|
|
918
1073
|
left_eye: "left_eye",
|
|
@@ -932,6 +1087,23 @@ var BLAZE_TO_COCO = {
|
|
|
932
1087
|
left_ankle: "left_ankle",
|
|
933
1088
|
right_ankle: "right_ankle"
|
|
934
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
|
+
}
|
|
935
1107
|
var BlazePoseAdapter = class {
|
|
936
1108
|
constructor(_ctx) {
|
|
937
1109
|
this.modelId = "blazepose";
|
|
@@ -952,15 +1124,34 @@ var BlazePoseAdapter = class {
|
|
|
952
1124
|
{
|
|
953
1125
|
runtime: "tfjs",
|
|
954
1126
|
modelType: this.modelType,
|
|
955
|
-
enableSmoothing:
|
|
1127
|
+
enableSmoothing: false
|
|
956
1128
|
}
|
|
957
1129
|
);
|
|
958
1130
|
}
|
|
959
|
-
async estimate(
|
|
1131
|
+
async estimate(input, options) {
|
|
960
1132
|
if (!this.detector) throw new Error("BlazePose adapter not loaded");
|
|
961
|
-
|
|
1133
|
+
const size = blazeInputSize(input);
|
|
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
|
+
}
|
|
962
1153
|
const t0 = performance.now();
|
|
963
|
-
const poses = await this.detector.estimatePoses(
|
|
1154
|
+
const poses = await this.detector.estimatePoses(estimateTarget, {
|
|
964
1155
|
flipHorizontal: false,
|
|
965
1156
|
maxPoses: 1
|
|
966
1157
|
});
|
|
@@ -980,27 +1171,33 @@ var BlazePoseAdapter = class {
|
|
|
980
1171
|
letterbox: {
|
|
981
1172
|
offsetX: 0,
|
|
982
1173
|
offsetY: 0,
|
|
983
|
-
drawW:
|
|
984
|
-
drawH:
|
|
985
|
-
vw:
|
|
986
|
-
vh:
|
|
1174
|
+
drawW: size.vw,
|
|
1175
|
+
drawH: size.vh,
|
|
1176
|
+
vw: size.vw,
|
|
1177
|
+
vh: size.vh
|
|
987
1178
|
}
|
|
988
1179
|
};
|
|
989
1180
|
}
|
|
990
|
-
const vw =
|
|
991
|
-
const vh =
|
|
1181
|
+
const vw = size.vw || 1;
|
|
1182
|
+
const vh = size.vh || 1;
|
|
992
1183
|
const dispW = options.displayWidth || 1;
|
|
993
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
|
+
}
|
|
994
1189
|
const byName = /* @__PURE__ */ new Map();
|
|
1190
|
+
const extrasRaw = [];
|
|
995
1191
|
for (const kp of pose.keypoints) {
|
|
996
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);
|
|
997
1195
|
const coco = BLAZE_TO_COCO[name];
|
|
998
|
-
if (
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
y:
|
|
1002
|
-
|
|
1003
|
-
});
|
|
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
|
+
}
|
|
1004
1201
|
}
|
|
1005
1202
|
const videoKeypoints = COCO_KEYPOINT_NAMES.map((name) => {
|
|
1006
1203
|
const k = byName.get(name);
|
|
@@ -1029,6 +1226,19 @@ var BlazePoseAdapter = class {
|
|
|
1029
1226
|
score: k.score
|
|
1030
1227
|
};
|
|
1031
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
|
+
}
|
|
1032
1242
|
return {
|
|
1033
1243
|
keypoints,
|
|
1034
1244
|
score: scoreSum / 17,
|
|
@@ -1044,6 +1254,13 @@ var BlazePoseAdapter = class {
|
|
|
1044
1254
|
}
|
|
1045
1255
|
};
|
|
1046
1256
|
}
|
|
1257
|
+
/** Clear cached ROI / landmark filters (call when the still changes). */
|
|
1258
|
+
resetTemporal() {
|
|
1259
|
+
try {
|
|
1260
|
+
this.detector?.reset?.();
|
|
1261
|
+
} catch {
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1047
1264
|
dispose() {
|
|
1048
1265
|
try {
|
|
1049
1266
|
this.detector?.dispose?.();
|
|
@@ -1054,14 +1271,27 @@ var BlazePoseAdapter = class {
|
|
|
1054
1271
|
};
|
|
1055
1272
|
|
|
1056
1273
|
// src/models/adapters/movenet.ts
|
|
1274
|
+
function inputSize(input) {
|
|
1275
|
+
if (typeof HTMLVideoElement !== "undefined" && input instanceof HTMLVideoElement) {
|
|
1276
|
+
return { vw: input.videoWidth || 0, vh: input.videoHeight || 0 };
|
|
1277
|
+
}
|
|
1278
|
+
if (typeof HTMLImageElement !== "undefined" && input instanceof HTMLImageElement) {
|
|
1279
|
+
return { vw: input.naturalWidth || input.width || 0, vh: input.naturalHeight || input.height || 0 };
|
|
1280
|
+
}
|
|
1281
|
+
if (typeof HTMLCanvasElement !== "undefined" && input instanceof HTMLCanvasElement) {
|
|
1282
|
+
return { vw: input.width || 0, vh: input.height || 0 };
|
|
1283
|
+
}
|
|
1284
|
+
return { vw: input.width || 0, vh: input.height || 0 };
|
|
1285
|
+
}
|
|
1057
1286
|
var INPUT_SIZE = 192;
|
|
1058
|
-
var SMOOTH_ALPHA = 0.
|
|
1287
|
+
var SMOOTH_ALPHA = 0.45;
|
|
1288
|
+
var STILL_TARGET_FILL = 0.52;
|
|
1059
1289
|
function modelNormToVideo(xNorm, yNorm, lb) {
|
|
1060
1290
|
const xSq = xNorm * INPUT_SIZE;
|
|
1061
1291
|
const ySq = yNorm * INPUT_SIZE;
|
|
1062
1292
|
return {
|
|
1063
|
-
x: (xSq - lb.offsetX) * (lb.vw / lb.drawW),
|
|
1064
|
-
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)
|
|
1065
1295
|
};
|
|
1066
1296
|
}
|
|
1067
1297
|
function videoToCover(vx, vy, dispW, dispH, lb) {
|
|
@@ -1078,6 +1308,18 @@ var MoveNetGraphAdapter = class {
|
|
|
1078
1308
|
this.offscreen = null;
|
|
1079
1309
|
this.offCtx = null;
|
|
1080
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
|
+
};
|
|
1081
1323
|
this.letterbox = {
|
|
1082
1324
|
offsetX: 0,
|
|
1083
1325
|
offsetY: 0,
|
|
@@ -1117,19 +1359,81 @@ var MoveNetGraphAdapter = class {
|
|
|
1117
1359
|
if (!this.offCtx) throw new Error("2d context unavailable");
|
|
1118
1360
|
return this.offCtx;
|
|
1119
1361
|
}
|
|
1120
|
-
prepareInput(
|
|
1121
|
-
const vw
|
|
1122
|
-
const
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
|
|
1362
|
+
prepareInput(input, trackRoi) {
|
|
1363
|
+
const { vw: rawW, vh: rawH } = inputSize(input);
|
|
1364
|
+
const vw = rawW || 1;
|
|
1365
|
+
const vh = rawH || 1;
|
|
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;
|
|
1126
1399
|
const offsetX = (INPUT_SIZE - drawW) / 2;
|
|
1127
1400
|
const offsetY = (INPUT_SIZE - drawH) / 2;
|
|
1128
|
-
this.
|
|
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 };
|
|
1129
1412
|
const c2d = this.poseCanvasCtx();
|
|
1130
1413
|
c2d.fillStyle = "#000";
|
|
1131
1414
|
c2d.fillRect(0, 0, INPUT_SIZE, INPUT_SIZE);
|
|
1132
|
-
|
|
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;
|
|
1133
1437
|
return this.offscreen;
|
|
1134
1438
|
}
|
|
1135
1439
|
smooth(raw) {
|
|
@@ -1145,20 +1449,26 @@ var MoveNetGraphAdapter = class {
|
|
|
1145
1449
|
}
|
|
1146
1450
|
return this.smoothed;
|
|
1147
1451
|
}
|
|
1148
|
-
async estimate(
|
|
1452
|
+
async estimate(input, options) {
|
|
1149
1453
|
if (!this.tf || !this.model) throw new Error("MoveNet adapter not loaded");
|
|
1150
|
-
|
|
1454
|
+
const size = inputSize(input);
|
|
1455
|
+
if (!(size.vw > 0 && size.vh > 0)) return null;
|
|
1151
1456
|
const t0 = performance.now();
|
|
1152
|
-
const
|
|
1153
|
-
const
|
|
1154
|
-
const
|
|
1457
|
+
const isStill = typeof HTMLImageElement !== "undefined" && input instanceof HTMLImageElement;
|
|
1458
|
+
const trackRoi = options.temporalSmooth !== false && isStill;
|
|
1459
|
+
const canvas = this.prepareInput(input, trackRoi);
|
|
1460
|
+
const tensorIn = this.tf.tidy(
|
|
1461
|
+
() => this.tf.expandDims(this.tf.browser.fromPixels(canvas), 0)
|
|
1462
|
+
);
|
|
1463
|
+
const out = this.model.execute(tensorIn);
|
|
1155
1464
|
const tensor = Array.isArray(out) ? out[0] : out;
|
|
1156
1465
|
const data = tensor.dataSync();
|
|
1157
|
-
|
|
1466
|
+
tensorIn.dispose();
|
|
1158
1467
|
if (Array.isArray(out)) out.forEach((t) => t.dispose());
|
|
1159
1468
|
else out.dispose();
|
|
1160
1469
|
const inferenceMs = performance.now() - t0;
|
|
1161
|
-
const lb = this.
|
|
1470
|
+
const lb = this.inferLb;
|
|
1471
|
+
const coverLb = this.letterbox;
|
|
1162
1472
|
const dispW = options.displayWidth || 1;
|
|
1163
1473
|
const dispH = options.displayHeight || 1;
|
|
1164
1474
|
const drawRaw = [];
|
|
@@ -1176,7 +1486,8 @@ var MoveNetGraphAdapter = class {
|
|
|
1176
1486
|
});
|
|
1177
1487
|
scoreSum += score;
|
|
1178
1488
|
}
|
|
1179
|
-
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 }));
|
|
1180
1491
|
const videoKeypoints = sm.map((k, i) => ({
|
|
1181
1492
|
name: COCO_KEYPOINT_NAMES[i],
|
|
1182
1493
|
xPx: k.xPx,
|
|
@@ -1184,7 +1495,7 @@ var MoveNetGraphAdapter = class {
|
|
|
1184
1495
|
score: k.score
|
|
1185
1496
|
}));
|
|
1186
1497
|
const keypoints = videoKeypoints.map((k) => {
|
|
1187
|
-
const d = videoToCover(k.xPx, k.yPx, dispW, dispH,
|
|
1498
|
+
const d = videoToCover(k.xPx, k.yPx, dispW, dispH, coverLb);
|
|
1188
1499
|
let nx = dispW > 0 ? d.x / dispW : 0;
|
|
1189
1500
|
const ny = dispH > 0 ? d.y / dispH : 0;
|
|
1190
1501
|
if (options.facingMode === "user") nx = 1 - nx;
|
|
@@ -1201,9 +1512,15 @@ var MoveNetGraphAdapter = class {
|
|
|
1201
1512
|
score: scoreSum / 17,
|
|
1202
1513
|
inferenceMs,
|
|
1203
1514
|
videoKeypoints,
|
|
1204
|
-
letterbox:
|
|
1515
|
+
letterbox: coverLb
|
|
1205
1516
|
};
|
|
1206
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
|
+
}
|
|
1207
1524
|
dispose() {
|
|
1208
1525
|
try {
|
|
1209
1526
|
this.model?.dispose();
|
|
@@ -1211,6 +1528,8 @@ var MoveNetGraphAdapter = class {
|
|
|
1211
1528
|
}
|
|
1212
1529
|
this.model = null;
|
|
1213
1530
|
this.smoothed = null;
|
|
1531
|
+
this.lastVideoKps = null;
|
|
1532
|
+
this.stillPass = 0;
|
|
1214
1533
|
this.offscreen = null;
|
|
1215
1534
|
this.offCtx = null;
|
|
1216
1535
|
}
|
|
@@ -1283,11 +1602,11 @@ var SHELL_CSS = `
|
|
|
1283
1602
|
position: relative; width: 100%; height: 100%; min-height: 240px;
|
|
1284
1603
|
background: #000; overflow: hidden; isolation: isolate;
|
|
1285
1604
|
}
|
|
1286
|
-
.pt-root video, .pt-root canvas.pt-overlay {
|
|
1605
|
+
.pt-root video, .pt-root img.pt-still, .pt-root canvas.pt-overlay {
|
|
1287
1606
|
position: absolute; inset: 0; width: 100%; height: 100%;
|
|
1288
1607
|
object-fit: cover; opacity: 0; transition: opacity 180ms ease-out;
|
|
1289
1608
|
}
|
|
1290
|
-
.pt-root video { z-index: 1; background: #000; }
|
|
1609
|
+
.pt-root video, .pt-root img.pt-still { z-index: 1; background: #000; }
|
|
1291
1610
|
.pt-root canvas.pt-overlay { z-index: 2; pointer-events: none; }
|
|
1292
1611
|
.pt-boot {
|
|
1293
1612
|
position: absolute; inset: 0; z-index: 4;
|
|
@@ -1361,6 +1680,7 @@ function mountDomShell(host, options = {}) {
|
|
|
1361
1680
|
root.className = "pt-root";
|
|
1362
1681
|
root.innerHTML = `
|
|
1363
1682
|
<video playsinline muted autoplay></video>
|
|
1683
|
+
<img class="pt-still" alt="" draggable="false" />
|
|
1364
1684
|
<canvas class="pt-overlay"></canvas>
|
|
1365
1685
|
<div class="pt-boot">
|
|
1366
1686
|
<div class="pt-boot-powered">Powered by</div>
|
|
@@ -1377,6 +1697,7 @@ function mountDomShell(host, options = {}) {
|
|
|
1377
1697
|
`;
|
|
1378
1698
|
host.appendChild(root);
|
|
1379
1699
|
const video = root.querySelector("video");
|
|
1700
|
+
const image = root.querySelector("img.pt-still");
|
|
1380
1701
|
const canvas = root.querySelector("canvas.pt-overlay");
|
|
1381
1702
|
const boot = root.querySelector(".pt-boot");
|
|
1382
1703
|
const bootMsg = root.querySelector(".pt-boot-msg");
|
|
@@ -1386,6 +1707,7 @@ function mountDomShell(host, options = {}) {
|
|
|
1386
1707
|
return {
|
|
1387
1708
|
root,
|
|
1388
1709
|
video,
|
|
1710
|
+
image,
|
|
1389
1711
|
canvas,
|
|
1390
1712
|
boot,
|
|
1391
1713
|
bootMsg,
|
|
@@ -1397,10 +1719,21 @@ function mountDomShell(host, options = {}) {
|
|
|
1397
1719
|
bootMsg.classList.toggle("is-error", isError);
|
|
1398
1720
|
},
|
|
1399
1721
|
setCameraVisible(visible) {
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1722
|
+
if (visible) {
|
|
1723
|
+
this.setMediaMode(image.src ? "image" : "video");
|
|
1724
|
+
} else {
|
|
1725
|
+
this.setMediaMode("none");
|
|
1726
|
+
}
|
|
1727
|
+
},
|
|
1728
|
+
setMediaMode(mode) {
|
|
1729
|
+
const showVideo = mode === "video";
|
|
1730
|
+
const showImage = mode === "image";
|
|
1731
|
+
const showMedia = showVideo || showImage;
|
|
1732
|
+
video.style.opacity = showVideo ? "1" : "0";
|
|
1733
|
+
image.style.opacity = showImage ? "1" : "0";
|
|
1734
|
+
canvas.style.opacity = showMedia ? "1" : "0";
|
|
1735
|
+
boot.classList.toggle("hide", showMedia);
|
|
1736
|
+
if (!showMedia) {
|
|
1404
1737
|
bootMsg.textContent = loadingText;
|
|
1405
1738
|
bootMsg.classList.remove("is-error");
|
|
1406
1739
|
}
|
|
@@ -1425,6 +1758,7 @@ function mountDomShell(host, options = {}) {
|
|
|
1425
1758
|
applyMirror(facingMode) {
|
|
1426
1759
|
const t = facingMode === "user" ? "scaleX(-1)" : "none";
|
|
1427
1760
|
video.style.transform = t;
|
|
1761
|
+
image.style.transform = "none";
|
|
1428
1762
|
canvas.style.transform = t;
|
|
1429
1763
|
},
|
|
1430
1764
|
destroy() {
|
|
@@ -1628,6 +1962,10 @@ var PoseTrackerClient = class {
|
|
|
1628
1962
|
this.keypointsSuppressionLogged = false;
|
|
1629
1963
|
this.shell = null;
|
|
1630
1964
|
this.stream = null;
|
|
1965
|
+
this.poseSource = { type: "camera" };
|
|
1966
|
+
this.objectUrlRevoke = null;
|
|
1967
|
+
this.estimateInput = null;
|
|
1968
|
+
this.imageShotPending = false;
|
|
1631
1969
|
this.adapter = null;
|
|
1632
1970
|
this.running = false;
|
|
1633
1971
|
this.busy = false;
|
|
@@ -1654,6 +1992,15 @@ var PoseTrackerClient = class {
|
|
|
1654
1992
|
this.resolved = resolvePoseModel({ model: options.model, modelUrl: options.modelUrl });
|
|
1655
1993
|
this.skeletonDef = options.skeletonDef ?? null;
|
|
1656
1994
|
this.coldStartMode = options.coldStart === "full" ? "full" : "basic";
|
|
1995
|
+
try {
|
|
1996
|
+
this.poseSource = normalizePoseSource(options.source, {
|
|
1997
|
+
sourceUrl: options.sourceUrl,
|
|
1998
|
+
sourceFile: options.sourceFile,
|
|
1999
|
+
facingMode: facingFromOptions(options)
|
|
2000
|
+
});
|
|
2001
|
+
} catch {
|
|
2002
|
+
this.poseSource = { type: "camera", facingMode: facingFromOptions(options) };
|
|
2003
|
+
}
|
|
1657
2004
|
this.files = options.fileStore !== void 0 ? options.fileStore : createWebFileStore();
|
|
1658
2005
|
this.engineLoader = options.engineLoader ?? new EngineLoader({
|
|
1659
2006
|
fileStore: this.files,
|
|
@@ -1700,6 +2047,10 @@ var PoseTrackerClient = class {
|
|
|
1700
2047
|
getColdStartMode() {
|
|
1701
2048
|
return this.coldStartMode;
|
|
1702
2049
|
}
|
|
2050
|
+
/** Active pose input source (`camera` | `video` | `image`). */
|
|
2051
|
+
getSource() {
|
|
2052
|
+
return this.poseSource;
|
|
2053
|
+
}
|
|
1703
2054
|
addEventListener(listener) {
|
|
1704
2055
|
this.listeners.add(listener);
|
|
1705
2056
|
return () => this.listeners.delete(listener);
|
|
@@ -1763,31 +2114,102 @@ var PoseTrackerClient = class {
|
|
|
1763
2114
|
warmup(options) {
|
|
1764
2115
|
return this.preload(options);
|
|
1765
2116
|
}
|
|
1766
|
-
/**
|
|
2117
|
+
/**
|
|
2118
|
+
* Mount + load model + open source + start inference.
|
|
2119
|
+
* Camera / video stream continuously; image runs a single shot (re-run with {@link analyze}).
|
|
2120
|
+
*/
|
|
1767
2121
|
async start() {
|
|
1768
2122
|
if (this.disposed) throw new Error("PoseTracker disposed");
|
|
1769
2123
|
if (!this.shell) throw new Error("Call mount() before start()");
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
2124
|
+
const needsMedia = this.poseSource.type === "camera" || this.poseSource.type === "video" || this.poseSource.type === "image";
|
|
2125
|
+
await this.preload({
|
|
2126
|
+
coldStart: this.opts.coldStart === "basic" && this.poseSource.type === "camera" ? "basic" : needsMedia ? "full" : "basic"
|
|
2127
|
+
});
|
|
2128
|
+
if (this.coldStartMode === "basic" || !this.estimateInput) {
|
|
2129
|
+
await this.ensureSource();
|
|
2130
|
+
}
|
|
2131
|
+
if (this.poseSource.type === "image") {
|
|
2132
|
+
this.imageShotPending = true;
|
|
1773
2133
|
}
|
|
1774
2134
|
this.beginLoop();
|
|
1775
2135
|
}
|
|
2136
|
+
/**
|
|
2137
|
+
* Switch input source. Stops the current media, attaches the new source, and
|
|
2138
|
+
* restarts the loop when previously running.
|
|
2139
|
+
*/
|
|
2140
|
+
async setSource(source, extras) {
|
|
2141
|
+
if (this.disposed) throw new Error("PoseTracker disposed");
|
|
2142
|
+
const wasRunning = this.running;
|
|
2143
|
+
this.stopMediaOnly();
|
|
2144
|
+
this.poseSource = normalizePoseSource(source, {
|
|
2145
|
+
sourceUrl: extras?.sourceUrl ?? this.opts.sourceUrl,
|
|
2146
|
+
sourceFile: extras?.sourceFile ?? this.opts.sourceFile,
|
|
2147
|
+
facingMode: extras?.facingMode ?? facingFromOptions(this.opts)
|
|
2148
|
+
});
|
|
2149
|
+
this.opts.source = this.poseSource;
|
|
2150
|
+
if (this.poseSource.type === "camera" && this.poseSource.facingMode) {
|
|
2151
|
+
this.opts.facingMode = this.poseSource.facingMode;
|
|
2152
|
+
}
|
|
2153
|
+
if (this.adapter?.resetTemporal) {
|
|
2154
|
+
this.adapter.resetTemporal();
|
|
2155
|
+
}
|
|
2156
|
+
if (!this.shell) return;
|
|
2157
|
+
await this.ensureAdapter();
|
|
2158
|
+
await this.ensureSource();
|
|
2159
|
+
if (wasRunning || this.poseSource.type === "image") {
|
|
2160
|
+
if (this.poseSource.type === "image") this.imageShotPending = true;
|
|
2161
|
+
this.beginLoop();
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
/**
|
|
2165
|
+
* Re-run pose estimation on the current still image (no-op for camera/video
|
|
2166
|
+
* while the loop is already running — for those, call {@link start}).
|
|
2167
|
+
*/
|
|
2168
|
+
async analyze() {
|
|
2169
|
+
if (this.disposed) throw new Error("PoseTracker disposed");
|
|
2170
|
+
if (!this.shell) throw new Error("Call mount() before analyze()");
|
|
2171
|
+
await this.ensureAdapter();
|
|
2172
|
+
if (!this.estimateInput) await this.ensureSource();
|
|
2173
|
+
if (this.poseSource.type === "image") {
|
|
2174
|
+
this.imageShotPending = true;
|
|
2175
|
+
if (!this.running) this.beginLoop();
|
|
2176
|
+
return null;
|
|
2177
|
+
}
|
|
2178
|
+
return this.runOneFrame();
|
|
2179
|
+
}
|
|
1776
2180
|
stop() {
|
|
1777
2181
|
this.running = false;
|
|
1778
2182
|
this.loopToken += 1;
|
|
1779
2183
|
this.busy = false;
|
|
1780
|
-
|
|
1781
|
-
this.
|
|
2184
|
+
this.imageShotPending = false;
|
|
2185
|
+
this.stopMediaOnly();
|
|
1782
2186
|
if (this.shell) {
|
|
1783
|
-
this.shell.
|
|
1784
|
-
this.shell.setCameraVisible(false);
|
|
2187
|
+
this.shell.setMediaMode("none");
|
|
1785
2188
|
this.shell.setPlacementVisible(false);
|
|
1786
2189
|
this.cameraRevealed = false;
|
|
1787
2190
|
this.applyWatermark();
|
|
1788
2191
|
}
|
|
1789
2192
|
if (this.status !== "error") this.setStatus("idle");
|
|
1790
2193
|
}
|
|
2194
|
+
stopMediaOnly() {
|
|
2195
|
+
stopMediaStream(this.stream);
|
|
2196
|
+
this.stream = null;
|
|
2197
|
+
this.estimateInput = null;
|
|
2198
|
+
if (this.objectUrlRevoke) {
|
|
2199
|
+
this.objectUrlRevoke();
|
|
2200
|
+
this.objectUrlRevoke = null;
|
|
2201
|
+
}
|
|
2202
|
+
if (this.shell) {
|
|
2203
|
+
try {
|
|
2204
|
+
this.shell.video.pause();
|
|
2205
|
+
} catch {
|
|
2206
|
+
}
|
|
2207
|
+
this.shell.video.removeAttribute("src");
|
|
2208
|
+
this.shell.video.srcObject = null;
|
|
2209
|
+
this.shell.video.load();
|
|
2210
|
+
this.shell.image.removeAttribute("src");
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
1791
2213
|
async dispose() {
|
|
1792
2214
|
this.stopExercise();
|
|
1793
2215
|
this.stop();
|
|
@@ -1844,7 +2266,30 @@ var PoseTrackerClient = class {
|
|
|
1844
2266
|
return this.configurePromise;
|
|
1845
2267
|
}
|
|
1846
2268
|
getAvailableExercises() {
|
|
1847
|
-
|
|
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
|
+
};
|
|
1848
2293
|
}
|
|
1849
2294
|
getAvailableCustomExercises() {
|
|
1850
2295
|
if (this.mode !== "full-engine" || !this.engine?.listCustomExercises) return [];
|
|
@@ -1880,6 +2325,28 @@ var PoseTrackerClient = class {
|
|
|
1880
2325
|
throw new Error(FREE_PLAN_FEATURES_MESSAGE);
|
|
1881
2326
|
}
|
|
1882
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
|
+
}
|
|
1883
2350
|
const available = this.getAvailableExercises();
|
|
1884
2351
|
const exercise = findExerciseByIdOrAlias(exerciseId, available);
|
|
1885
2352
|
if (!exercise) {
|
|
@@ -1893,8 +2360,12 @@ var PoseTrackerClient = class {
|
|
|
1893
2360
|
this.reportError({ type: "error", code: "invalid_exercise", message });
|
|
1894
2361
|
throw new Error(message);
|
|
1895
2362
|
}
|
|
2363
|
+
this.beginEngineSession(exercise, options);
|
|
2364
|
+
}
|
|
2365
|
+
beginEngineSession(exercise, options) {
|
|
2366
|
+
if (!this.engine) return;
|
|
1896
2367
|
this.stopExercise();
|
|
1897
|
-
this.currentExerciseId =
|
|
2368
|
+
this.currentExerciseId = exercise.id;
|
|
1898
2369
|
this.keypointsSuppressionLogged = false;
|
|
1899
2370
|
this.session = this.engine.createSession(
|
|
1900
2371
|
{
|
|
@@ -2056,7 +2527,7 @@ var PoseTrackerClient = class {
|
|
|
2056
2527
|
async ensureColdStartMode(mode) {
|
|
2057
2528
|
if (mode !== "full") return;
|
|
2058
2529
|
if (!this.shell) return;
|
|
2059
|
-
await this.
|
|
2530
|
+
await this.ensureSource();
|
|
2060
2531
|
}
|
|
2061
2532
|
async ensureAdapter() {
|
|
2062
2533
|
if (this.adapter) return;
|
|
@@ -2073,48 +2544,102 @@ var PoseTrackerClient = class {
|
|
|
2073
2544
|
this.adapter = createPoseAdapter({ resolved: this.resolved });
|
|
2074
2545
|
await this.adapter.load();
|
|
2075
2546
|
}
|
|
2076
|
-
async
|
|
2077
|
-
if (!this.shell) throw new Error("Call mount() before opening
|
|
2078
|
-
|
|
2547
|
+
async ensureSource() {
|
|
2548
|
+
if (!this.shell) throw new Error("Call mount() before opening a pose source");
|
|
2549
|
+
const src = this.poseSource;
|
|
2550
|
+
if (src.type === "camera") {
|
|
2551
|
+
if (this.stream && this.estimateInput) return;
|
|
2552
|
+
this.emit({
|
|
2553
|
+
type: "initialization",
|
|
2554
|
+
step: "accessing_webcam",
|
|
2555
|
+
message: "accessing webcam (source=camera)",
|
|
2556
|
+
ready: false
|
|
2557
|
+
});
|
|
2558
|
+
try {
|
|
2559
|
+
this.stream = await openCameraStream({
|
|
2560
|
+
facingMode: src.facingMode ?? facingFromOptions(this.opts),
|
|
2561
|
+
idealWidth: this.opts.idealWidth,
|
|
2562
|
+
idealHeight: this.opts.idealHeight
|
|
2563
|
+
});
|
|
2564
|
+
} catch (err) {
|
|
2565
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2566
|
+
const denied = /NotAllowed|Permission|denied/i.test(msg);
|
|
2567
|
+
this.reportError({
|
|
2568
|
+
type: "error",
|
|
2569
|
+
code: denied ? "camera_denied" : "camera_unavailable",
|
|
2570
|
+
message: msg
|
|
2571
|
+
});
|
|
2572
|
+
this.shell.setBootMessage(
|
|
2573
|
+
denied ? "Camera permission denied \u2014 allow access in the browser" : "Camera unavailable",
|
|
2574
|
+
true
|
|
2575
|
+
);
|
|
2576
|
+
throw err;
|
|
2577
|
+
}
|
|
2578
|
+
this.shell.video.srcObject = this.stream;
|
|
2579
|
+
this.shell.applyMirror(src.facingMode ?? facingFromOptions(this.opts));
|
|
2580
|
+
this.shell.setMediaMode("video");
|
|
2581
|
+
try {
|
|
2582
|
+
await this.shell.video.play();
|
|
2583
|
+
} catch {
|
|
2584
|
+
}
|
|
2585
|
+
this.estimateInput = this.shell.video;
|
|
2586
|
+
this.scheduleReveal("camera");
|
|
2587
|
+
void this.handleCameraStart({
|
|
2588
|
+
backend: this.acceleration.backend ?? "webgl",
|
|
2589
|
+
profileId: null
|
|
2590
|
+
});
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
if (src.type === "video") {
|
|
2594
|
+
this.emit({
|
|
2595
|
+
type: "initialization",
|
|
2596
|
+
step: "loading_media",
|
|
2597
|
+
message: "loading video (source=video)",
|
|
2598
|
+
ready: false
|
|
2599
|
+
});
|
|
2600
|
+
try {
|
|
2601
|
+
await this.attachVideoSource(src.src, {
|
|
2602
|
+
autoplay: src.autoplay !== false,
|
|
2603
|
+
loop: src.loop !== false,
|
|
2604
|
+
muted: src.muted !== false
|
|
2605
|
+
});
|
|
2606
|
+
} catch (err) {
|
|
2607
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2608
|
+
this.reportError({ type: "error", code: "media_load_failed", message: msg });
|
|
2609
|
+
this.shell.setBootMessage("Could not load video source", true);
|
|
2610
|
+
throw err;
|
|
2611
|
+
}
|
|
2612
|
+
this.shell.applyMirror("none");
|
|
2613
|
+
this.shell.setMediaMode("video");
|
|
2614
|
+
this.scheduleReveal("video");
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2079
2617
|
this.emit({
|
|
2080
2618
|
type: "initialization",
|
|
2081
|
-
step: "
|
|
2082
|
-
message: "
|
|
2619
|
+
step: "loading_media",
|
|
2620
|
+
message: "loading image (source=image)",
|
|
2083
2621
|
ready: false
|
|
2084
2622
|
});
|
|
2085
2623
|
try {
|
|
2086
|
-
this.
|
|
2087
|
-
facingMode: facingFromOptions(this.opts),
|
|
2088
|
-
idealWidth: this.opts.idealWidth,
|
|
2089
|
-
idealHeight: this.opts.idealHeight
|
|
2090
|
-
});
|
|
2624
|
+
await this.attachImageSource(src.src);
|
|
2091
2625
|
} catch (err) {
|
|
2092
2626
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2093
|
-
|
|
2094
|
-
this.
|
|
2095
|
-
type: "error",
|
|
2096
|
-
code: denied ? "camera_denied" : "camera_unavailable",
|
|
2097
|
-
message: msg
|
|
2098
|
-
});
|
|
2099
|
-
this.shell.setBootMessage(
|
|
2100
|
-
denied ? "Camera permission denied \u2014 allow access in the browser" : "Camera unavailable",
|
|
2101
|
-
true
|
|
2102
|
-
);
|
|
2627
|
+
this.reportError({ type: "error", code: "media_load_failed", message: msg });
|
|
2628
|
+
this.shell.setBootMessage("Could not load image source", true);
|
|
2103
2629
|
throw err;
|
|
2104
2630
|
}
|
|
2105
|
-
this.shell.
|
|
2106
|
-
this.shell.
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
}
|
|
2631
|
+
this.shell.applyMirror("none");
|
|
2632
|
+
this.shell.setMediaMode("image");
|
|
2633
|
+
this.scheduleReveal("image");
|
|
2634
|
+
}
|
|
2635
|
+
scheduleReveal(kind) {
|
|
2111
2636
|
const started = Date.now();
|
|
2112
2637
|
const tick = () => {
|
|
2113
2638
|
this.revealCamera();
|
|
2114
2639
|
if (this.cameraRevealed) return;
|
|
2115
2640
|
if (Date.now() - started > 8e3) {
|
|
2116
2641
|
this.shell?.setBootMessage(
|
|
2117
|
-
|
|
2642
|
+
`${kind} loaded but preview is empty \u2014 check the file or CORS`,
|
|
2118
2643
|
true
|
|
2119
2644
|
);
|
|
2120
2645
|
return;
|
|
@@ -2122,88 +2647,200 @@ var PoseTrackerClient = class {
|
|
|
2122
2647
|
setTimeout(tick, 120);
|
|
2123
2648
|
};
|
|
2124
2649
|
setTimeout(tick, 60);
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2650
|
+
}
|
|
2651
|
+
async attachVideoSource(src, opts) {
|
|
2652
|
+
if (!this.shell) throw new Error("no shell");
|
|
2653
|
+
const video = this.shell.video;
|
|
2654
|
+
if (isVideoElement(src)) {
|
|
2655
|
+
if (src.parentElement && src !== video) {
|
|
2656
|
+
this.estimateInput = src;
|
|
2657
|
+
if (opts.autoplay) {
|
|
2658
|
+
try {
|
|
2659
|
+
await src.play();
|
|
2660
|
+
} catch {
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
await waitForVideoReady(src);
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
if (src.srcObject) {
|
|
2667
|
+
video.srcObject = src.srcObject;
|
|
2668
|
+
} else if (src.currentSrc || src.src) {
|
|
2669
|
+
video.src = src.currentSrc || src.src;
|
|
2670
|
+
}
|
|
2671
|
+
} else {
|
|
2672
|
+
const resolved = await resolveObjectUrl(src);
|
|
2673
|
+
this.objectUrlRevoke = resolved.revoke;
|
|
2674
|
+
video.src = resolved.url;
|
|
2675
|
+
}
|
|
2676
|
+
video.loop = opts.loop;
|
|
2677
|
+
video.muted = opts.muted;
|
|
2678
|
+
video.playsInline = true;
|
|
2679
|
+
await waitForVideoReady(video);
|
|
2680
|
+
if (opts.autoplay) {
|
|
2681
|
+
try {
|
|
2682
|
+
await video.play();
|
|
2683
|
+
} catch {
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
this.estimateInput = video;
|
|
2687
|
+
}
|
|
2688
|
+
async attachImageSource(src) {
|
|
2689
|
+
if (!this.shell) throw new Error("no shell");
|
|
2690
|
+
if (isImageBitmap(src)) {
|
|
2691
|
+
const resolved2 = await imageBitmapToObjectUrl(src);
|
|
2692
|
+
this.objectUrlRevoke = resolved2.revoke;
|
|
2693
|
+
this.shell.image.src = resolved2.url;
|
|
2694
|
+
await waitForImageReady(this.shell.image);
|
|
2695
|
+
this.estimateInput = src;
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
if (isImageElement(src)) {
|
|
2699
|
+
if (src.complete && src.naturalWidth > 0) {
|
|
2700
|
+
this.shell.image.src = src.currentSrc || src.src;
|
|
2701
|
+
this.estimateInput = src;
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2704
|
+
await waitForImageReady(src);
|
|
2705
|
+
this.shell.image.src = src.currentSrc || src.src;
|
|
2706
|
+
this.estimateInput = src;
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
const resolved = await resolveObjectUrl(src);
|
|
2710
|
+
this.objectUrlRevoke = resolved.revoke;
|
|
2711
|
+
this.shell.image.src = resolved.url;
|
|
2712
|
+
await waitForImageReady(this.shell.image);
|
|
2713
|
+
this.estimateInput = this.shell.image;
|
|
2129
2714
|
}
|
|
2130
2715
|
beginLoop() {
|
|
2131
2716
|
this.running = true;
|
|
2132
2717
|
this.fpsTimer = performance.now();
|
|
2133
2718
|
void this.loop(++this.loopToken);
|
|
2134
2719
|
}
|
|
2720
|
+
frameReady(input) {
|
|
2721
|
+
const size = mediaSize(
|
|
2722
|
+
input
|
|
2723
|
+
);
|
|
2724
|
+
if (!(size.width > 0 && size.height > 0)) return false;
|
|
2725
|
+
if (isVideoElement(input)) {
|
|
2726
|
+
if (input.readyState < 2) return false;
|
|
2727
|
+
if (this.poseSource.type === "video" && (input.paused || input.ended)) return false;
|
|
2728
|
+
}
|
|
2729
|
+
return true;
|
|
2730
|
+
}
|
|
2731
|
+
async runOneFrame() {
|
|
2732
|
+
if (!this.adapter || !this.shell || !this.estimateInput) return null;
|
|
2733
|
+
if (!this.frameReady(this.estimateInput)) return null;
|
|
2734
|
+
const input = this.estimateInput;
|
|
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;
|
|
2741
|
+
const facing = this.poseSource.type === "camera" ? facingFromOptions(this.opts) : "environment";
|
|
2742
|
+
const stillTracked = this.poseSource.type === "image" && this.opts.stillMode === "tracked";
|
|
2743
|
+
const result = await this.adapter.estimate(input, {
|
|
2744
|
+
facingMode: facing,
|
|
2745
|
+
displayWidth: dispW,
|
|
2746
|
+
displayHeight: dispH,
|
|
2747
|
+
temporalSmooth: this.poseSource.type === "camera" || this.poseSource.type === "video" || stillTracked
|
|
2748
|
+
});
|
|
2749
|
+
if (!result) return null;
|
|
2750
|
+
const ctx = this.shell.canvas.getContext("2d");
|
|
2751
|
+
if (ctx && this.opts.drawSkeleton !== false) {
|
|
2752
|
+
const mapped = mapVideoKeypointsToDisplay(
|
|
2753
|
+
result.videoKeypoints,
|
|
2754
|
+
result.letterbox,
|
|
2755
|
+
clientW,
|
|
2756
|
+
clientH
|
|
2757
|
+
);
|
|
2758
|
+
drawSkeleton(ctx, mapped, clientW, clientH, true, this.skeletonDef);
|
|
2759
|
+
} else if (ctx && this.opts.drawSkeleton === false) {
|
|
2760
|
+
ctx.clearRect(0, 0, clientW, clientH);
|
|
2761
|
+
}
|
|
2762
|
+
const pose = {
|
|
2763
|
+
keypoints: result.keypoints,
|
|
2764
|
+
score: result.score,
|
|
2765
|
+
timestampMs: Date.now()
|
|
2766
|
+
};
|
|
2767
|
+
this.ingestPose(pose);
|
|
2768
|
+
this.inferMs.push(result.inferenceMs);
|
|
2769
|
+
if (this.inferMs.length > 30) this.inferMs.shift();
|
|
2770
|
+
this.fpsWindow += 1;
|
|
2771
|
+
const now = performance.now();
|
|
2772
|
+
if (now - this.fpsTimer >= 1e3) {
|
|
2773
|
+
const fps = this.fpsWindow;
|
|
2774
|
+
this.fpsWindow = 0;
|
|
2775
|
+
this.fpsTimer = now;
|
|
2776
|
+
const med = median(this.inferMs);
|
|
2777
|
+
this.acceleration.medianInferenceMs = med;
|
|
2778
|
+
const above = result.keypoints.filter((k) => k.score >= 0.3).length;
|
|
2779
|
+
const backend = "getBackend" in this.adapter && typeof this.adapter.getBackend === "function" ? this.adapter.getBackend() : null;
|
|
2780
|
+
this.acceleration.backend = backend;
|
|
2781
|
+
const size = mediaSize(input);
|
|
2782
|
+
if (this.opts.debugHud) {
|
|
2783
|
+
this.shell.setHud(
|
|
2784
|
+
`${this.resolved.modelId} \xB7 ${this.poseSource.type} \xB7 ${backend ?? "?"} \xB7 ${fps} fps \xB7 ${med != null ? Math.round(med) + " ms" : "?"} \xB7 kp\u22650.3=${above}/17`,
|
|
2785
|
+
true
|
|
2786
|
+
);
|
|
2787
|
+
}
|
|
2788
|
+
this.emit({
|
|
2789
|
+
type: "stats",
|
|
2790
|
+
fps,
|
|
2791
|
+
medianInferenceMs: med,
|
|
2792
|
+
backend,
|
|
2793
|
+
keypointsAbove03: above,
|
|
2794
|
+
meanScore: result.score,
|
|
2795
|
+
videoSize: `${size.width}x${size.height}`,
|
|
2796
|
+
timestampMs: Date.now()
|
|
2797
|
+
});
|
|
2798
|
+
}
|
|
2799
|
+
this.revealCamera();
|
|
2800
|
+
return pose;
|
|
2801
|
+
}
|
|
2135
2802
|
async loop(token) {
|
|
2136
|
-
if (token !== this.loopToken || !this.running || !this.
|
|
2803
|
+
if (token !== this.loopToken || !this.running || !this.shell || this.disposed) {
|
|
2137
2804
|
return;
|
|
2138
2805
|
}
|
|
2139
|
-
|
|
2140
|
-
|
|
2806
|
+
if (!this.adapter) {
|
|
2807
|
+
requestAnimationFrame(() => {
|
|
2808
|
+
void this.loop(token);
|
|
2809
|
+
});
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
const input = this.estimateInput;
|
|
2813
|
+
const shouldInfer = !!input && !this.busy && this.frameReady(input) && (this.poseSource.type !== "image" || this.imageShotPending);
|
|
2814
|
+
if (shouldInfer) {
|
|
2141
2815
|
this.busy = true;
|
|
2142
2816
|
try {
|
|
2143
|
-
const
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
});
|
|
2150
|
-
if (result) {
|
|
2151
|
-
const ctx = this.shell.canvas.getContext("2d");
|
|
2152
|
-
if (ctx && this.opts.drawSkeleton !== false) {
|
|
2153
|
-
const mapped = mapVideoKeypointsToDisplay(
|
|
2154
|
-
result.videoKeypoints,
|
|
2155
|
-
result.letterbox,
|
|
2156
|
-
dispW,
|
|
2157
|
-
dispH
|
|
2158
|
-
);
|
|
2159
|
-
drawSkeleton(ctx, mapped, dispW, dispH, true, this.skeletonDef);
|
|
2160
|
-
} else if (ctx && this.opts.drawSkeleton === false) {
|
|
2161
|
-
ctx.clearRect(0, 0, dispW, dispH);
|
|
2162
|
-
}
|
|
2163
|
-
const pose = {
|
|
2164
|
-
keypoints: result.keypoints,
|
|
2165
|
-
score: result.score,
|
|
2166
|
-
timestampMs: Date.now()
|
|
2167
|
-
};
|
|
2168
|
-
this.ingestPose(pose);
|
|
2169
|
-
this.inferMs.push(result.inferenceMs);
|
|
2170
|
-
if (this.inferMs.length > 30) this.inferMs.shift();
|
|
2171
|
-
this.fpsWindow += 1;
|
|
2172
|
-
const now = performance.now();
|
|
2173
|
-
if (now - this.fpsTimer >= 1e3) {
|
|
2174
|
-
const fps = this.fpsWindow;
|
|
2175
|
-
this.fpsWindow = 0;
|
|
2176
|
-
this.fpsTimer = now;
|
|
2177
|
-
const med = median(this.inferMs);
|
|
2178
|
-
this.acceleration.medianInferenceMs = med;
|
|
2179
|
-
const above = result.keypoints.filter((k) => k.score >= 0.3).length;
|
|
2180
|
-
const backend = "getBackend" in this.adapter && typeof this.adapter.getBackend === "function" ? this.adapter.getBackend() : null;
|
|
2181
|
-
this.acceleration.backend = backend;
|
|
2182
|
-
if (this.opts.debugHud) {
|
|
2183
|
-
this.shell.setHud(
|
|
2184
|
-
`${this.resolved.modelId} \xB7 ${backend ?? "?"} \xB7 ${fps} fps \xB7 ${med != null ? Math.round(med) + " ms" : "?"} \xB7 kp\u22650.3=${above}/17`,
|
|
2185
|
-
true
|
|
2186
|
-
);
|
|
2187
|
-
}
|
|
2188
|
-
this.emit({
|
|
2189
|
-
type: "stats",
|
|
2190
|
-
fps,
|
|
2191
|
-
medianInferenceMs: med,
|
|
2192
|
-
backend,
|
|
2193
|
-
keypointsAbove03: above,
|
|
2194
|
-
meanScore: result.score,
|
|
2195
|
-
videoSize: `${video.videoWidth}x${video.videoHeight}`,
|
|
2196
|
-
timestampMs: Date.now()
|
|
2197
|
-
});
|
|
2198
|
-
}
|
|
2817
|
+
const pose = await this.runOneFrame();
|
|
2818
|
+
if (this.poseSource.type === "image" && pose) {
|
|
2819
|
+
this.imageShotPending = false;
|
|
2820
|
+
this.running = false;
|
|
2821
|
+
this.busy = false;
|
|
2822
|
+
return;
|
|
2199
2823
|
}
|
|
2200
|
-
this.revealCamera();
|
|
2201
2824
|
} catch (err) {
|
|
2202
2825
|
const message = err instanceof Error ? err.message : String(err);
|
|
2203
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
|
+
}
|
|
2204
2833
|
}
|
|
2205
2834
|
this.busy = false;
|
|
2206
2835
|
}
|
|
2836
|
+
if (token !== this.loopToken || !this.running) return;
|
|
2837
|
+
const videoEl = input && isVideoElement(input) ? input : this.shell.video;
|
|
2838
|
+
if (this.poseSource.type !== "image" && typeof videoEl.requestVideoFrameCallback === "function" && (this.poseSource.type === "video" || this.poseSource.type === "camera")) {
|
|
2839
|
+
videoEl.requestVideoFrameCallback(() => {
|
|
2840
|
+
void this.loop(token);
|
|
2841
|
+
});
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2207
2844
|
requestAnimationFrame(() => {
|
|
2208
2845
|
void this.loop(token);
|
|
2209
2846
|
});
|
|
@@ -2460,14 +3097,19 @@ var PoseTrackerClient = class {
|
|
|
2460
3097
|
}
|
|
2461
3098
|
revealCamera() {
|
|
2462
3099
|
if (!this.shell || this.cameraRevealed) return;
|
|
2463
|
-
const
|
|
2464
|
-
if (!
|
|
3100
|
+
const input = this.estimateInput;
|
|
3101
|
+
if (!input) return;
|
|
3102
|
+
const size = mediaSize(input);
|
|
3103
|
+
if (!(size.width > 0 && size.height > 0)) return;
|
|
2465
3104
|
requestAnimationFrame(() => {
|
|
2466
3105
|
requestAnimationFrame(() => {
|
|
2467
3106
|
if (this.cameraRevealed || !this.shell) return;
|
|
2468
|
-
if (!(this.shell.video.videoWidth > 0)) return;
|
|
2469
3107
|
this.cameraRevealed = true;
|
|
2470
|
-
this.
|
|
3108
|
+
if (this.poseSource.type === "image") {
|
|
3109
|
+
this.shell.setMediaMode("image");
|
|
3110
|
+
} else {
|
|
3111
|
+
this.shell.setMediaMode("video");
|
|
3112
|
+
}
|
|
2471
3113
|
this.applyWatermark();
|
|
2472
3114
|
});
|
|
2473
3115
|
});
|
|
@@ -2576,6 +3218,7 @@ exports.SDK_VERSION = SDK_VERSION;
|
|
|
2576
3218
|
exports.SkeletonFetchError = SkeletonFetchError;
|
|
2577
3219
|
exports.TrackError = TrackError;
|
|
2578
3220
|
exports.UsageTracker = UsageTracker;
|
|
3221
|
+
exports.V4_ONLY_EXERCISE_IDS = V4_ONLY_EXERCISE_IDS;
|
|
2579
3222
|
exports.configure = configure;
|
|
2580
3223
|
exports.createLocalFileStore = createLocalFileStore;
|
|
2581
3224
|
exports.createLocalKeyValueStore = createLocalKeyValueStore;
|
|
@@ -2594,7 +3237,10 @@ exports.initWebGlBackend = initWebGlBackend;
|
|
|
2594
3237
|
exports.isPaidPlan = isPaidPlan;
|
|
2595
3238
|
exports.mapVideoKeypointsToDisplay = mapVideoKeypointsToDisplay;
|
|
2596
3239
|
exports.mountDomShell = mountDomShell;
|
|
3240
|
+
exports.normalizeEngineChannel = normalizeEngineChannel;
|
|
3241
|
+
exports.normalizePoseSource = normalizePoseSource;
|
|
2597
3242
|
exports.openCameraStream = openCameraStream;
|
|
3243
|
+
exports.requiresEngineV4 = requiresEngineV4;
|
|
2598
3244
|
exports.resolveExerciseName = resolveExerciseName;
|
|
2599
3245
|
exports.resolveFeatures = resolveFeatures;
|
|
2600
3246
|
exports.resolvePoseDetection = resolvePoseDetection;
|