/** * 数据分析脚本:扫描 exercises.json,输出各维度的去重取值与数量, * 并报告哪些取值尚未被中文标签映射覆盖(便于补全 labels.ts)。 * * 用法: node scripts/analyze.mjs [path-to-exercises.json] */ import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const DATA = process.argv[2] || path.join(__dirname, '..', 'src', 'exercises', 'data', 'exercises.json'); // 复用 TS 标签映射(用 require 不行,这里用一份 JS 镜像做缺失检测) const BODY_PART_LABELS = { back: '背部', cardio: '有氧心肺', chest: '胸部', 'lower arms': '前臂', 'lower legs': '小腿', neck: '颈部', shoulders: '肩部', 'upper arms': '上臂', 'upper legs': '大腿', waist: '腰腹', }; const EQUIPMENT_LABELS = { 'body weight': '自重', dumbbell: '哑铃', barbell: '杠铃', kettlebell: '壶铃', cable: '钢索', machine: '固定器械', band: '弹力带', 'exercise ball': '健身球', 'medicine ball': '药球', 'ez bar': 'EZ曲杆', 'foam roll': '泡沫轴', treadmill: '跑步机', 'stationary bike': '固定单车', rope: '战绳', tire: '轮胎', skier: '滑雪训练器', 'sled machine': '雪橇机', roller: '滚轮', 'elliptical machine': '椭圆机', 'olympic barbell': '奥杆', 'decline bench': '下斜凳', 'flat bench': '平板凳', 'pull-up bar': '单杠', 'stability ball': '瑞士球', trx: 'TRX悬挂带', 'assistance machine': '辅助器械', 'bosu ball': 'BOSU半球', 'wheel roller': '健腹轮', none: '无器械', other: '其他', }; const MUSCLE_LABELS = { biceps: '肱二头肌', triceps: '肱三头肌', forearms: '前臂', abs: '腹肌', abdominals: '腹肌', obliques: '腹斜肌', pectorals: '胸大肌', chest: '胸部', quadriceps: '股四头肌', hamstrings: '腘绳肌', glutes: '臀大肌', calves: '小腿', traps: '斜方肌', 'upper traps': '上斜方肌', lats: '背阔肌', 'lower back': '下背', delts: '三角肌', shoulders: '肩部', hips: '髋部', adductors: '内收肌', neck: '颈部', 'serratus anterior': '前锯肌', 'spinal erectors': '竖脊肌', 'hip flexors': '髋屈肌', 'middle back': '中背', }; function tally(arr, keyFn) { const m = new Map(); for (const x of arr) { const k = keyFn(x); if (!k) continue; if (Array.isArray(k)) k.forEach((v) => m.set(v, (m.get(v) || 0) + 1)); else m.set(k, (m.get(k) || 0) + 1); } return m; } function report(name, map, labels) { const missing = []; const rows = Array.from(map.entries()).sort((a, b) => b[1] - a[1]); console.log(`\n=== ${name} (${rows.length} 种) ===`); for (const [v, c] of rows) { const has = labels[v] ? '' : ' ⚠ 缺标签'; if (!labels[v]) missing.push(v); console.log(` ${v.padEnd(22)} ${String(c).padStart(5)} ${labels[v] || ''}${has}`); } return missing; } const raw = JSON.parse(fs.readFileSync(DATA, 'utf-8')); console.log(`总动作数: ${raw.length}`); const eqMiss = report('EQUIPMENT', tally(raw, (e) => e.equipment), EQUIPMENT_LABELS); const tMiss = report('TARGET', tally(raw, (e) => e.target), MUSCLE_LABELS); const mgMiss = report('MUSCLE_GROUP', tally(raw, (e) => e.muscle_group), MUSCLE_LABELS); const smMiss = report('SECONDARY_MUSCLES', tally(raw, (e) => e.secondary_muscles), MUSCLE_LABELS); console.log('\n=== 缺失中文标签汇总(需补全 labels.ts)==='); console.log('equipment :', eqMiss.join(', ') || '(无)'); console.log('target :', tMiss.join(', ') || '(无)'); console.log('muscle_group:', mgMiss.join(', ') || '(无)'); console.log('secondary :', smMiss.join(', ') || '(无)');