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