echoof-fitness-sdk
v1.2.15
Published
Web 运动识别计数 & 舞蹈跟练打分 SDK — 基于 TensorFlow.js MoveNet
Maintainers
Readme
echoof-fitness-sdk
Web 运动识别计数 SDK — 基于 TensorFlow.js MoveNet + 骨架模板匹配。
安装
npm install echoof-fitness-sdk快速使用
<video id="video" playsinline muted></video>
<canvas id="canvas"></canvas>import { FitnessTracker } from 'echoof-fitness-sdk';
const tracker = new FitnessTracker({
videoElement: document.getElementById('video'),
canvasElement: document.getElementById('canvas'),
onCount(result) {
console.log('计数:', result.count, '分数:', result.score);
},
onProgress(info) {
console.log('进度:', info.phasePercent + '%');
},
onGuideStatus(status) {
console.log('站位:', status.status, status.reason);
if (status.confirmed) {
console.log('站位确认,开始计数');
}
},
onFeedback(fb) {
console.log('反馈:', fb.reason); // 'too_fast' | 'too_slow' | 'too_close' | 'too_far'
},
// 可选:配置 MoveNet 模型来源。默认已内置“本地优先 + CDN 兜底”,一般不用传。
// poseModelUrls: [
// '/models/movenet-singlepose-lightning/model.json',
// 'https://cdn-public.community-platform.qq.com/echoof-youth/npm/public/models/movenet-singlepose-lightning/model.json',
// ],
onKeypoints(info) {
// 每次识别到骨骼点都会回调
// keypoints: COCO-17 关键点,x/y 已归一化到 0~1
console.log('骨骼点:', info.keypoints, 'bbox:', info.bbox);
},
onFps(fps) {
console.log('FPS:', fps);
},
});
// 1. 初始化(加载模型 + 启动摄像头)
await tracker.init();
// 2. 加载骨架模板(URL 支持跨域 CORS)
await tracker.loadTemplate('https://cdn.example.com/template.json');
// 3. 开始站位引导 → 自动进入计数
tracker.startGuide();
// 或直接开始计数(跳过引导)
// tracker.startCounting();
// ====== 路径 B:无模板,使用规则识别 ======
// 支持: arm_raise / side_lift / chest_expansion / squat / arm_swing / waist_twist / side_stretch
tracker.startCountingByMode('squat');
// 运动中动态切换动作(计数归零)
tracker.setMode('arm_swing');
// 停止并获取汇总
const summary = tracker.stop();
// { totalCount: 10, avgScore: 82, bestScore: 95, worstScore: 68 }
// 销毁资源
tracker.destroy();骨骼点回调与文件导出
如果业务侧需要拿到每帧识别出的骨骼点,可传 onKeypoints:
const tracker = new FitnessTracker({
videoElement,
canvasElement,
onKeypoints(info) {
console.log(info.keypoints); // COCO-17,x/y 已归一化到 0~1
console.log(info.bbox); // 识别到的人体框(归一化坐标)
console.log(info.state); // idle / guiding / counting ...
},
});FollowTracker 也支持同名回调:
const follow = new FollowTracker({
videoElement,
canvasElement,
onKeypoints(info) {
console.log(info.keypoints);
},
});回调数据结构:
{
keypoints: Array<{ x: number; y: number; score: number }>;
bbox: { x: number; y: number; width: number; height: number } | null;
timestamp: number;
videoWidth: number;
videoHeight: number;
state: string;
}记录并导出骨骼点 TXT 文件
如果需要像小程序 run-detail 一样把骨骼点收集起来并提供给使用者下载,可开启 recordKeypoints,或手动调用 startKeypointRecording()。
自动记录
const tracker = new FitnessTracker({
videoElement,
canvasElement,
recordKeypoints: true, // 初始化后自动开始记录
keypointRecordIntervalMs: 200, // 可选:默认 200ms,约等于小程序 frameSkip=5
keypointRecordMaxFrames: 1500, // 可选:默认 1500,和小程序 maxCacheSize 对齐
});
await tracker.init();
tracker.startCountingByMode('side_lift');
// 运动结束后,直接下载 TXT 文件
tracker.downloadKeypoints('side-lift-keypoints.txt');手动开始/停止记录
const tracker = new FitnessTracker({ videoElement, canvasElement });
await tracker.init();
tracker.startKeypointRecording(); // 开始记录,默认清空历史
tracker.startCountingByMode('side_lift');
// ...运动中
tracker.stopKeypointRecording();
// 方式 1:下载 TXT 文件
tracker.downloadKeypoints('keypoints.txt');
// 方式 2:拿到记录数组,业务方自行上传
const records = tracker.exportKeypoints();
// 方式 3:拿到 TXT 文本内容
const text = tracker.exportKeypointsText();
// 方式 4:拿到 Blob,业务方自行上传到 COS/OSS
const blob = tracker.exportKeypointsBlob();
// 方式 5:拿到 File 对象,适合 FormData 上传;FollowTracker 默认文件名为 follow-keypoints.txt
const file = tracker.getKeypointsFile('fitness-keypoints.txt');
const formData = new FormData();
formData.append('file', file);默认记录 count / quality
开启记录后,SDK 会默认写入类似小程序 setConsole 的业务记录:
{
"stmp": 1710000000000,
"data": { "count": 1 }
}
{
"stmp": 1710000001000,
"data": { "quality": { "status": "warning", "reason": "too_fast" } }
}手动插入 recordBehavior
SDK 提供了和小程序一致的行为枚举,业务方可以在需要的位置手动插入:
import { BEHAVIOR_TYPES } from 'echoof-fitness-sdk';
tracker.recordBehavior(BEHAVIOR_TYPES.START);
tracker.recordBehavior(BEHAVIOR_TYPES.PAUSE);
tracker.recordBehavior(BEHAVIOR_TYPES.RESUME);
tracker.recordBehavior(BEHAVIOR_TYPES.COMPLETE);
tracker.recordBehavior(BEHAVIOR_TYPES.EXIT);
tracker.recordBehavior(BEHAVIOR_TYPES.EXIT_BACKGROUND);
tracker.recordBehavior(BEHAVIOR_TYPES.PAUSE_AUTO);
tracker.recordBehavior(BEHAVIOR_TYPES.UNSPECIFIED);也可以直接调用便捷方法:
tracker.recordBehaviorStart(); //开始
tracker.recordBehaviorPause(); // 暂停
tracker.recordBehaviorResume(); // 恢复
tracker.recordBehaviorComplete(); // 完成
tracker.recordBehaviorExit(); // 退出
tracker.recordBehaviorExitBackground(); // 退出后台
tracker.recordBehaviorPauseAuto(); // 自动暂停
tracker.recordBehaviorUnspecified(); // 未指定 如果有自定义数据,也可以插入任意 data:
tracker.recordConsole({ customEvent: 'xxx', value: 123 });FollowTracker 同样支持:
const follow = new FollowTracker({
videoElement,
canvasElement,
recordKeypoints: true,
});
// 结束后下载
follow.downloadKeypoints('follow-keypoints.txt');
// 或获取 follow-keypoints.txt 文件对象
const file = follow.getKeypointsFile('follow-keypoints.txt');导出的 TXT 内容与小程序 run-detail/test.txt 对齐:多条 JSON 记录用空行分隔,每条记录只包含 stmp 和 data 两个字段。
{
"stmp": 1710000000000,
"data": [
{ "x": 0.512, "y": 0.234, "score": 0.987 },
...
]
}
{
"stmp": 1710000000100,
"data": [
{ "x": 0.514, "y": 0.236, "score": 0.982 },
...
]
}其中 x / y / score 默认保留 3 位小数。
MoveNet 模型来源配置
SDK 初始化时需要加载 MoveNet 模型文件(model.json + 两个 bin 权重分片)。默认策略是:
- 优先本地:
/models/movenet-singlepose-lightning/model.json - 失败后 CDN 兜底:
https://cdn-public.community-platform.qq.com/echoof-youth/npm/public/models/movenet-singlepose-lightning/model.json
也就是说,如果用户使用 npm 包内 demo/,模型文件已经放在:
demo/models/movenet-singlepose-lightning/
├── model.json
├── group1-shard1of2.bin
└── group1-shard2of2.bin直接 npm install && npm run dev 即可,本地模型会先被请求。
方式 A:使用默认配置(推荐)
const tracker = new FitnessTracker({
videoElement,
canvasElement,
});等价于:
const tracker = new FitnessTracker({
videoElement,
canvasElement,
poseModelUrls: [
'/models/movenet-singlepose-lightning/model.json',
'https://cdn-public.community-platform.qq.com/echoof-youth/npm/public/models/movenet-singlepose-lightning/model.json',
],
});方式 B:只走 CDN
如果你的项目不想放本地模型文件,只配置 CDN 地址,不要把 /models/... 本地地址放进 poseModelUrls。
FitnessTracker
const tracker = new FitnessTracker({
videoElement,
canvasElement,
// 只走 CDN:不会请求 /models/movenet-singlepose-lightning/model.json
poseModelUrl: 'https://cdn-public.community-platform.qq.com/echoof-youth/npm/public/models/movenet-singlepose-lightning/model.json',
});也可以写成数组形式(数组里只放 CDN):
const tracker = new FitnessTracker({
videoElement,
canvasElement,
poseModelUrls: [
'https://cdn-public.community-platform.qq.com/echoof-youth/npm/public/models/movenet-singlepose-lightning/model.json',
],
});FollowTracker
const follow = new FollowTracker({
videoElement,
canvasElement,
// 只走 CDN
poseModelUrl: 'https://cdn-public.community-platform.qq.com/echoof-youth/npm/public/models/movenet-singlepose-lightning/model.json',
});优先级:
poseModelUrls>poseModelUrl> SDK 默认值。如果配置了poseModelUrls,SDK 会完全按数组顺序尝试。
方式 C:只走本地
把模型目录放到站点根路径下:
public/models/movenet-singlepose-lightning/
├── model.json
├── group1-shard1of2.bin
└── group1-shard2of2.bin然后:
const tracker = new FitnessTracker({
videoElement,
canvasElement,
poseModelUrls: [
'/models/movenet-singlepose-lightning/model.json',
],
});方式 D:自定义本地优先 + 自有 CDN 兜底
const tracker = new FitnessTracker({
videoElement,
canvasElement,
poseModelUrls: [
'/models/movenet-singlepose-lightning/model.json',
'https://your-cdn.example.com/models/movenet-singlepose-lightning/model.json',
],
});注意:
model.json和两个group1-shard*.bin必须在同一目录下。model.json内部使用相对路径引用权重分片。
FollowTracker 同样支持 poseModelUrl / poseModelUrls:
const follow = new FollowTracker({
videoElement,
canvasElement,
poseModelUrls: [
'/models/movenet-singlepose-lightning/model.json',
'https://your-cdn.example.com/models/movenet-singlepose-lightning/model.json',
],
});舞蹈跟练打分(FollowTracker)
FitnessTracker 适合“固定动作循环计数”(开合跳、深蹲等),而 FollowTracker 专为舞蹈/操类跟练设计:跟着教学视频做,实时相似度打分 + 反馈。
它与 FitnessTracker 共享底层(摄像头、MoveNet、骨骼绘制、语音),只是把计数引擎换成了 MotionMatcher(节奏自适应 DTW + 关键帧加权 + 分数平滑)。
快速上手
import { FollowTracker } from 'echoof-fitness-sdk';
const follow = new FollowTracker({
videoElement: document.querySelector('#camera'), // 摄像头
canvasElement: document.querySelector('#canvas'), // 骨骼叠加
teachingVideoElement: document.querySelector('#teacher'), // 教学视频(可选)
countdownSeconds: 3,
voiceFeedback: true,
onCountdown: (left) => console.log(`倒计时 ${left}`),
onScore: ({ currentScore, avgScore, progressPercent }) => {
// currentScore: 平滑后的实时分(0-100)
// avgScore: 累计均分
// progressPercent: 进度 0-100
},
onFeedback: ({ message, level, partScores, highlightParts }) => {
// message: 文字提示,如"左手再高一点"
// level: 'excellent' | 'good' | 'fair' | 'poor'
// partScores: { leftElbow: 0.85, ... } —— 可用来给骨骼部位染色
},
onKeyframe: ({ score, level, center }) => {
// 触发关键帧特效:在 center 处弹出分数
},
onEnd: (summary) => {
// summary.averageScore / summary.levelPercents.excellent ...
},
});
await follow.init();
await follow.loadSequence('https://cdn/xxx_skeleton.json'); // 骨架 JSON URL 或对象
follow.start(); // 倒计时 → 播教学视频 → 开始打分
follow.pause(); follow.resume();
follow.stop(); // 结束并返回 summary
follow.destroy();说明
- 教学序列是一份骨架 JSON(含
frames数组 +duration),通常由业务方离线处理教学视频得到。 - 教学视频可选:
- 传入:视频
currentTime驱动匹配时间,严格对齐。 - 不传:SDK 用墙钟时间驱动循环。
- 传入:视频
- matcherOptions:如需精调,可透传到底层
MotionMatcher,详见源码src/utils/MotionMatcher.js顶部参数表。 - 支持
type: 'hand'的手部跟练序列(内部自动切换权重)。
与 FitnessTracker 的关系
| 场景 | 用哪个 |
|---|---|
| 开合跳 / 深蹲 / 甩手等循环动作计数 | FitnessTracker |
| 跟着一段舞蹈/操整套动作相似度打分 | FollowTracker |
两者可同时 new 使用,但共用同一个摄像头 video 元素时建议只开一个。
API
new FitnessTracker(options)
| 参数 | 类型 | 说明 |
|------|------|------|
| videoElement | HTMLVideoElement | 必填 摄像头视频元素 |
| canvasElement | HTMLCanvasElement | 骨骼绘制画布(可选) |
| drawSkeleton | boolean | 是否绘制骨骼(默认 true) |
| guideMode | 'fullBody' \| 'upperBody' | 引导模式(默认 fullBody) |
| guideConfirmFrames | number | 确认帧数(默认 15) |
| counterConfig | object | 模板计数器配置覆盖 |
| recognitionMode | RecognitionMode | 规则识别模式(无模板时使用),见下方支持的模式 |
| voiceFeedback | boolean \| VoiceFeedbackOptions | 语音反馈(默认关闭)。传 true 启用内置语音;传对象可自定义音频/音量/冷却期 |
| onCount | (result) => void | 计数回调 |
| onProgress | (info) => void | 进度回调 |
| onGuideStatus | (status) => void | 站位引导状态回调 |
| onFeedback | (fb) => void | 动作反馈回调 |
| onFps | (fps) => void | 帧率回调 |
| onStateChange | (state) => void | 状态变更回调 |
| onError | (err) => void | 错误回调 |
方法
| 方法 | 说明 |
|------|------|
| init() | 初始化模型 + 摄像头 |
| loadTemplate(url \| object) | 加载骨架模板(URL 自动 CORS fetch) |
| startGuide() | 开始站位引导,确认后自动进入计数 |
| startCounting() | 直接开始计数(有模板走模板,无模板走 recognitionMode) |
| startCountingByMode(mode) | 按规则识别模式开始计数(无需模板) |
| setMode(mode) | 动态切换识别模式(计数归零,仅规则计数时有效) |
| stop() | 停止,返回汇总 { totalCount, avgScore, bestScore, worstScore } |
| destroy() | 销毁所有资源 |
| resetCount() | 重置计数 |
| setVoiceEnabled(enabled) | 开启/关闭语音反馈(即使 options 未启用也可以后续开启) |
| setVoiceAudio(reason, url) | 替换或新增单个 reason 的语音音频 URL(传一个 URL 即可) |
| getVoiceFeedback() | 获取语音反馈实例(可调用 setVolume / setEnabled / play 等) |
属性
| 属性 | 类型 | 说明 |
|------|------|------|
| state | string | 当前状态:idle / initializing / ready / guiding / counting / stopped |
| count | number | 当前计数 |
高级:直接使用子模块
import {
WebPoseDetector,
TemplateCycleCounter,
StandingGuideLogic,
VisionMotionAnalyzer,
VoiceFeedback,
drawSkeleton,
computeFullFeatures,
BONES, KEYPOINTS,
} from 'echoof-fitness-sdk';语音反馈(可选)
开启后,SDK 会在检测到对应反馈 reason 时自动播放内置中文语音(同一 reason 2 秒内不会重复)。
// ⚡ 最简:启用默认语音
const tracker = new FitnessTracker({ /* ... */, voiceFeedback: true });三种自定义方式(由简到全)
// 1️⃣ 只换 CDN 前缀:所有内置音频自动走你的 CDN(文件名与默认一致)
voiceFeedback: {
audioBaseUrl: 'https://my-cdn.com/voice',
}
// 2️⃣ 只换个别文件名:保留 audioBaseUrl,单独覆盖某些 reason 的文件名
voiceFeedback: {
audioBaseUrl: 'https://my-cdn.com/voice',
fileNameMap: {
too_fast: 'my_too_fast_v2.mp3', // 最终 URL: https://my-cdn.com/voice/my_too_fast_v2.mp3
},
}
// 3️⃣ 传完整 URL:每个 reason 独立一个完整 URL,最高优先级
voiceFeedback: {
audioMap: {
too_fast: 'https://other-cdn/foo.mp3',
},
}运行时替换单个音频(最常用 —— 传一个 URL 即可)
// 覆盖内置 reason
tracker.setVoiceAudio('too_fast', 'https://my-cdn.com/too_fast.mp3');
// 新增自定义 reason
tracker.setVoiceAudio('custom_tip', 'https://my-cdn.com/custom.mp3');
// 之后当 onFeedback 的 reason === 'custom_tip' 时会自动播放运行时控制
tracker.setVoiceEnabled(false); // 关闭
tracker.setVoiceEnabled(true); // 重新开启
tracker.getVoiceFeedback()?.setVolume(0.5);
tracker.getVoiceFeedback()?.getAudioMap(); // 查看当前所有 URL 映射内置支持的 reason
too_fast、too_slow、too_close、too_far、insufficient_range、wrong_direction、missing_lower、missing_upper、missing_head、missing_hand
运行 Demo
npm 包内自带完整 demo 项目(demo/)— 一个独立的 Vite 项目,3 步即可运行:
cd node_modules/echoof-fitness-sdk/demo
npm install
npm run dev
# 自动打开 http://localhost:3304详细说明见 demo/README.md。
本仓库开发时
npm run dev会打开local_demo/index.html,顶部 Tab 可在「动作计数」与「舞蹈跟练」之间切换,源码修改实时热更新。
骨架模板格式
{
"frames": [
{
"keypoints": [{"x": 0.5, "y": 0.3, "score": 0.9}, ...],
"features": {"angles": {...}, "relativePositions": {...}},
"timestamp": 0
}
],
"duration": 3000,
"frameCount": 15,
"type": "body"
}技术栈
- TensorFlow.js + MoveNet (SinglePose Lightning)
- 纯 TypeScript,无框架依赖
- 骨架模板匹配计数引擎(TemplateCycleCounter)
- 站位引导状态机(StandingGuideLogic)
- 动作质量反馈(TemplateCounterFeedback)
