npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

echoof-fitness-sdk

v1.2.15

Published

Web 运动识别计数 & 舞蹈跟练打分 SDK — 基于 TensorFlow.js MoveNet

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 记录用空行分隔,每条记录只包含 stmpdata 两个字段。

{
  "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 权重分片)。默认策略是:

  1. 优先本地/models/movenet-singlepose-lightning/model.json
  2. 失败后 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_fasttoo_slowtoo_closetoo_farinsufficient_rangewrong_directionmissing_lowermissing_uppermissing_headmissing_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)