react-native-nitro-pose-exercises 1.1.15 → 1.1.17

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.
@@ -130,7 +130,7 @@ override var onPostureRegained: (() -> Unit)? = null
130
130
 
131
131
  // ─── Posture Gate ──────────────────────────────────────────
132
132
  private var consecutivePostureFailures: Int = 0
133
- private val postureFailureThreshold: Int = 10
133
+ private val postureFailureThreshold: Int = 30 // ~3s — tolerant of pushup occlusion
134
134
  private var postureWasLost = false
135
135
 
136
136
  // ─── Hold Tracking ──────────────────────────────────────────
@@ -304,23 +304,31 @@ private fun processExerciseLogic() {
304
304
  val config = exerciseConfig ?: return
305
305
  if (_landmarks.isEmpty()) return
306
306
 
307
- val failureThreshold = if (config.type == ExerciseType.HOLD)
308
- postureFailureThreshold * 3
309
- else
310
- postureFailureThreshold
311
-
312
- if (!isPostureValid(config.postureFamily, config.visibilityThreshold)) {
313
- consecutivePostureFailures++
314
- if (consecutivePostureFailures >= failureThreshold) {
315
- if (!postureWasLost) {
316
- postureWasLost = true
317
- onPostureLost?.invoke()
307
+ // Hold exercises get 3x more tolerance — stationary poses suffer from
308
+ // visibility flicker more than active reps.
309
+ val failureThreshold = if (config.type == ExerciseType.HOLD) {
310
+ postureFailureThreshold * 3
311
+ } else {
312
+ postureFailureThreshold
313
+ }
314
+
315
+ // Posture gate with hysteresis
316
+ if (!isPostureValid(config.postureFamily, config.visibilityThreshold)) {
317
+ consecutivePostureFailures += 1
318
+ if (consecutivePostureFailures >= failureThreshold) {
319
+ if (!postureWasLost) {
320
+ postureWasLost = true
321
+ onPostureLost?.invoke()
322
+ }
323
+ // Only nuke phase history after EXTENDED loss (3x threshold).
324
+ // Brief occlusions during a pushup shouldn't wipe an in-progress rep.
325
+ if (consecutivePostureFailures >= failureThreshold * 3) {
326
+ _currentPhase = ExercisePhase.UNKNOWN
327
+ phaseHistory = mutableListOf()
328
+ }
318
329
  }
319
- _currentPhase = ExercisePhase.UNKNOWN
320
- phaseHistory.clear()
330
+ return
321
331
  }
322
- return
323
- }
324
332
 
325
333
  consecutivePostureFailures = 0
326
334
  if (postureWasLost) {
@@ -328,41 +336,53 @@ if (!isPostureValid(config.postureFamily, config.visibilityThreshold)) {
328
336
  onPostureRegained?.invoke()
329
337
  }
330
338
 
331
- val currentAngles = mutableMapOf<String, Double>()
332
- val angleSnapshots = mutableListOf<AngleSnapshot>()
339
+ // Angle calculation
340
+ val visThreshold = config.visibilityThreshold
341
+ val currentAngles = mutableMapOf<String, Double>()
342
+ val angleSnapshots = mutableListOf<AngleSnapshot>()
333
343
 
334
- for (angleDef in config.angles) {
335
- val a = angleDef.landmarkA.toInt()
336
- val b = angleDef.landmarkB.toInt()
337
- val c = angleDef.landmarkC.toInt()
344
+ for (angleDef in config.angles) {
345
+ val a = angleDef.landmarkA.toInt()
346
+ val b = angleDef.landmarkB.toInt()
347
+ val c = angleDef.landmarkC.toInt()
338
348
 
339
- if (a >= _landmarks.size || b >= _landmarks.size || c >= _landmarks.size) continue
349
+ if (a >= _landmarks.size || b >= _landmarks.size || c >= _landmarks.size) continue
340
350
 
341
- // Only calculate if all three landmarks have reasonable confidence
342
- if (_landmarks[a].visibility < 0.3 || _landmarks[b].visibility < 0.3 || _landmarks[c].visibility < 0.3) continue
351
+ if (_landmarks[a].visibility <= visThreshold ||
352
+ _landmarks[b].visibility <= visThreshold ||
353
+ _landmarks[c].visibility <= visThreshold) continue
343
354
 
344
- val angle = calculateAngle(_landmarks[a], _landmarks[b], _landmarks[c])
345
- currentAngles[angleDef.name] = angle
346
- angleSnapshots.add(AngleSnapshot(name = angleDef.name, value = angle))
347
- }
355
+ val angle = calculateAngle(
356
+ pointA = _landmarks[a],
357
+ vertex = _landmarks[b],
358
+ pointC = _landmarks[c]
359
+ )
348
360
 
349
- repAngleSnapshots = angleSnapshots.toTypedArray()
361
+ currentAngles[angleDef.name] = angle
362
+ angleSnapshots.add(AngleSnapshot(name = angleDef.name, value = angle))
363
+ }
350
364
 
351
- val detectedPhase = determinePhase(currentAngles, config)
365
+ repAngleSnapshots = angleSnapshots
352
366
 
353
- if (detectedPhase != _currentPhase && detectedPhase != ExercisePhase.UNKNOWN) {
354
- _currentPhase = detectedPhase
355
- onPhaseChange?.invoke(detectedPhase)
356
- handlePhaseTransition(detectedPhase, config)
357
- }
367
+ val detectedPhase = determinePhase(currentAngles, config)
358
368
 
359
- checkFormRules(currentAngles, config)
369
+ // Debug log — remove once reps count reliably
370
+ println("[Pose] angles=$currentAngles current=$_currentPhase detected=$detectedPhase history=$phaseHistory reps=$_repCount")
360
371
 
361
- if (config.type == ExerciseType.HOLD) {
362
- handleHoldProgress(currentAngles, config)
363
- }
372
+ if (detectedPhase != _currentPhase && detectedPhase != ExercisePhase.UNKNOWN) {
373
+ val previousPhase = _currentPhase
374
+ _currentPhase = detectedPhase
375
+ onPhaseChange?.invoke(detectedPhase)
376
+ handlePhaseTransition(previousPhase, detectedPhase, config)
364
377
  }
365
378
 
379
+ checkFormRules(currentAngles, config)
380
+
381
+ if (config.type == ExerciseType.HOLD) {
382
+ handleHoldProgress(currentAngles, config)
383
+ }
384
+ }
385
+
366
386
  // ═══════════════════════════════════════════════════════════
367
387
  // Angle Calculation
368
388
  // ═══════════════════════════════════════════════════════════
@@ -635,6 +655,7 @@ private fun isPostureValid(family: PostureFamily, threshold: Double): Boolean {
635
655
  val lh = _landmarks[23]; val rh = _landmarks[24]
636
656
  val lk = _landmarks[25]; val rk = _landmarks[26]
637
657
  val la = _landmarks[27]; val ra = _landmarks[28]
658
+ val lw = _landmarks[15]; val rw = _landmarks[16]
638
659
 
639
660
  // Shoulders mandatory; everything below is optional for close-range framing
640
661
  val shouldersVisible = ls.visibility > threshold && rs.visibility > threshold
@@ -643,6 +664,8 @@ private fun isPostureValid(family: PostureFamily, threshold: Double): Boolean {
643
664
  val hipsVisible = lh.visibility > threshold && rh.visibility > threshold
644
665
  val kneesVisible = lk.visibility > threshold && rk.visibility > threshold
645
666
  val anklesVisible = la.visibility > threshold && ra.visibility > threshold
667
+ val wristsVisible = lw.visibility > threshold && rw.visibility > threshold
668
+ val oneWristVisible = lw.visibility > threshold || rw.visibility > threshold
646
669
 
647
670
  val shoulderY = (ls.y + rs.y) / 2
648
671
  val shoulderX = (ls.x + rs.x) / 2
@@ -656,25 +679,31 @@ private fun isPostureValid(family: PostureFamily, threshold: Double): Boolean {
656
679
 
657
680
  return when (family) {
658
681
  PostureFamily.HORIZONTALPRONE, PostureFamily.SUPINE -> {
659
- if (!hipsVisible) return false
682
+ // Case A: side view — full body in horizontal band
683
+ if (hipsVisible) {
684
+ val ys = if (anklesVisible)
685
+ listOf(shoulderY, hipY, ankleY)
686
+ else
687
+ listOf(shoulderY, hipY)
688
+ if ((ys.max() - ys.min()) < 0.25) return true
689
+ }
690
+
691
+ // Case B: front-facing prone — minimum signature is shoulders + at least
692
+ // one wrist. Hips often occluded by the body in pushup framings.
693
+ if (!oneWristVisible) return false
660
694
 
661
- // Case A: side view — shoulders/hips/(ankles) in a horizontal band
662
- val ys = if (anklesVisible)
663
- listOf(shoulderY, hipY, ankleY)
695
+ val wristY = if (wristsVisible)
696
+ (lw.y + rw.y) / 2
664
697
  else
665
- listOf(shoulderY, hipY)
666
- if ((ys.max() - ys.min()) < 0.25) return true
667
-
668
- // Case B: front-facing prone (pushup head-on) large y-spread,
669
- // fall back to upper-body geometry
670
- val le = _landmarks[13]; val re = _landmarks[14]
671
- val lw = _landmarks[15]; val rw = _landmarks[16]
672
- val upperBodyVisible = le.visibility > threshold && re.visibility > threshold &&
673
- lw.visibility > threshold && rw.visibility > threshold
674
- if (!upperBodyVisible) return false
675
-
676
- val wristY = (lw.y + rw.y) / 2
677
- wristY > shoulderY + 0.03 && shoulderWidth > 0.10
698
+ kotlin.math.max(lw.y, rw.y)
699
+
700
+ // Geometry: wrists at or below shoulder line, torso reasonably wide.
701
+ // Tolerance loosened for deep DOWN phase where shoulders drop close
702
+ // to wrist level.
703
+ val handsLowerOrLevel = wristY > shoulderY - 0.08
704
+ val torsoFacing = shoulderWidth > 0.06
705
+
706
+ handsLowerOrLevel && torsoFacing
678
707
  }
679
708
 
680
709
  PostureFamily.STANDINGUPRIGHT -> {
@@ -72,7 +72,7 @@ class NitroPoseExercises: HybridNitroPoseExercisesSpec {
72
72
 
73
73
  // ─── Posture Gate ──────────────────────────────────────────
74
74
  private var consecutivePostureFailures: Int = 0
75
- private let postureFailureThreshold: Int = 10 // ~1s at 30fps with throttle=3
75
+ private let postureFailureThreshold: Int = 30 // ~3s at 30fps with throttle=3 — tolerant of pushup occlusion
76
76
  private var postureWasLost = false
77
77
 
78
78
  // ─── Pose Tracking ──────────────────────────────────────────
@@ -287,8 +287,12 @@ private func processExerciseLogic() {
287
287
  postureWasLost = true
288
288
  onPostureLost?()
289
289
  }
290
- _currentPhase = .unknown
291
- phaseHistory = []
290
+ // Only nuke phase history after EXTENDED loss (3x threshold).
291
+ // Brief occlusions during a pushup shouldn't wipe an in-progress rep.
292
+ if consecutivePostureFailures >= failureThreshold * 3 {
293
+ _currentPhase = .unknown
294
+ phaseHistory = []
295
+ }
292
296
  }
293
297
  return
294
298
  }
@@ -329,6 +333,9 @@ private func processExerciseLogic() {
329
333
 
330
334
  let detectedPhase = determinePhase(from: currentAngles, config: config)
331
335
 
336
+ // Debug log — remove once reps count reliably
337
+ print("[Pose] angles=\(currentAngles) current=\(_currentPhase) detected=\(detectedPhase) history=\(phaseHistory) reps=\(_repCount)")
338
+
332
339
  if detectedPhase != _currentPhase && detectedPhase != .unknown {
333
340
  let previousPhase = _currentPhase
334
341
  _currentPhase = detectedPhase
@@ -354,14 +361,17 @@ private func isPostureValid(family: PostureFamily, threshold: Double) -> Bool {
354
361
  let lh = _landmarks[23], rh = _landmarks[24]
355
362
  let lk = _landmarks[25], rk = _landmarks[26]
356
363
  let la = _landmarks[27], ra = _landmarks[28]
364
+ let lw = _landmarks[15], rw = _landmarks[16]
357
365
 
358
- // Shoulders are mandatory; hips/knees/ankles are optional for close-range framing
366
+ // Shoulders mandatory; everything below is optional for close-range framing
359
367
  let shouldersVisible = ls.visibility > threshold && rs.visibility > threshold
360
368
  guard shouldersVisible else { return false }
361
369
 
362
370
  let hipsVisible = lh.visibility > threshold && rh.visibility > threshold
363
371
  let kneesVisible = lk.visibility > threshold && rk.visibility > threshold
364
372
  let anklesVisible = la.visibility > threshold && ra.visibility > threshold
373
+ let wristsVisible = lw.visibility > threshold && rw.visibility > threshold
374
+ let oneWristVisible = lw.visibility > threshold || rw.visibility > threshold
365
375
 
366
376
  let shoulderY = (ls.y + rs.y) / 2
367
377
  let shoulderX = (ls.x + rs.x) / 2
@@ -370,40 +380,40 @@ private func isPostureValid(family: PostureFamily, threshold: Double) -> Bool {
370
380
  let kneeY = kneesVisible ? (lk.y + rk.y) / 2 : hipY
371
381
  let ankleY = anklesVisible ? (la.y + ra.y) / 2 : kneeY
372
382
 
373
- // Shoulder width is camera-mirror invariant: |x1 - x2| is the same
374
- // whether the front camera mirror has been applied or not.
383
+ // Mirror-invariant works for front and back camera identically
375
384
  let shoulderWidth = (ls.x - rs.x).magnitude
376
385
 
377
386
  switch family {
378
387
  case .horizontalprone, .supine:
379
- // Need hips to make any meaningful judgment for prone/supine
380
- guard hipsVisible else { return false }
381
-
382
- // Case A: side view — shoulders, hips, ankles in a horizontal band
383
- let ys: [Double] = anklesVisible
384
- ? [shoulderY, hipY, ankleY]
385
- : [shoulderY, hipY]
386
- let ySpread = (ys.max() ?? 0) - (ys.min() ?? 0)
387
- if ySpread < 0.25 {
388
- return true
388
+ // Case A: side view full body in horizontal band
389
+ if hipsVisible {
390
+ let ys: [Double] = anklesVisible
391
+ ? [shoulderY, hipY, ankleY]
392
+ : [shoulderY, hipY]
393
+ let ySpread = (ys.max() ?? 0) - (ys.min() ?? 0)
394
+ if ySpread < 0.25 {
395
+ return true
396
+ }
389
397
  }
390
398
 
391
- // Case B: front-facing prone (e.g. pushup viewed head-on) body extends
392
- // away along Z, so y-spread is large. Fall back to upper-body geometry.
393
- let le = _landmarks[13], re = _landmarks[14]
394
- let lw = _landmarks[15], rw = _landmarks[16]
395
- let upperBodyVisible = le.visibility > threshold && re.visibility > threshold
396
- && lw.visibility > threshold && rw.visibility > threshold
397
- guard upperBodyVisible else { return false }
398
-
399
- let wristY = (lw.y + rw.y) / 2
400
- guard wristY > shoulderY + 0.03 else { return false }
401
- guard shoulderWidth > 0.10 else { return false }
402
- return true
399
+ // Case B: front-facing prone minimum signature is shoulders + at least
400
+ // one wrist. We do NOT require hips because in pushup framings hips are
401
+ // often occluded by the body itself.
402
+ guard oneWristVisible else { return false }
403
+
404
+ let wristY = wristsVisible
405
+ ? (lw.y + rw.y) / 2
406
+ : max(lw.y, rw.y) // whichever wrist we can see
407
+
408
+ // Geometry: wrists at or below shoulder line, torso reasonably wide.
409
+ // Tolerance loosened to handle deep DOWN phase where shoulders drop
410
+ // close to wrist level.
411
+ let handsLowerOrLevel = wristY > shoulderY - 0.08
412
+ let torsoFacing = shoulderWidth > 0.06
413
+
414
+ return handsLowerOrLevel && torsoFacing
403
415
 
404
416
  case .standingupright:
405
- // Front-facing standing: user often crops hips/knees at close range.
406
- // Use a tiered check based on what's visible.
407
417
  if hipsVisible {
408
418
  if kneesVisible {
409
419
  return shoulderY < hipY - 0.05
@@ -412,8 +422,7 @@ private func isPostureValid(family: PostureFamily, threshold: Double) -> Bool {
412
422
  }
413
423
  return shoulderY < hipY - 0.05
414
424
  }
415
- // Hips off-frame — accept if shoulders form a reasonable horizontal span,
416
- // which means the person is upright and facing (or backing) the camera.
425
+ // Hips cropped (close-range front-facing) — accept based on shoulder span
417
426
  return shoulderWidth > 0.08
418
427
 
419
428
  case .seated:
@@ -423,7 +432,6 @@ private func isPostureValid(family: PostureFamily, threshold: Double) -> Bool {
423
432
  if hipsVisible {
424
433
  return shoulderY < hipY - 0.05
425
434
  }
426
- // Seated with hips out of frame is rare; fall back to upright shoulder span
427
435
  return shoulderWidth > 0.08
428
436
 
429
437
  case .sideplank:
@@ -9,8 +9,8 @@
9
9
  export const PUSHUP_CONFIG = {
10
10
  name: 'Push-Up',
11
11
  type: 'rep',
12
- postureFamily: 'none',
13
- visibilityThreshold: 0.3,
12
+ postureFamily: 'horizontalProne',
13
+ visibilityThreshold: 0.1,
14
14
  cameraAngle: 'front',
15
15
  angles: [{
16
16
  name: 'leftElbow',
@@ -23,10 +23,7 @@ export const PUSHUP_CONFIG = {
23
23
  landmarkB: 14,
24
24
  landmarkC: 16
25
25
  }],
26
- phases: [
27
- // Calibrated for 2D-projected angles in front-facing portrait filming.
28
- // Real anatomical 180° appears as ~140-150° due to perspective foreshortening.
29
- {
26
+ phases: [{
30
27
  phase: 'up',
31
28
  angleName: 'leftElbow',
32
29
  minAngle: 130,
@@ -34,23 +31,11 @@ export const PUSHUP_CONFIG = {
34
31
  }, {
35
32
  phase: 'down',
36
33
  angleName: 'leftElbow',
37
- minAngle: 40,
38
- maxAngle: 80
34
+ minAngle: 0,
35
+ maxAngle: 120
39
36
  }],
40
37
  repSequence: ['up', 'down', 'up'],
41
- formRules: [
42
- // Trigger when descending but stalled above true-down threshold
43
- {
44
- name: 'shallowRep',
45
- message: 'Go lower',
46
- severity: 'info',
47
- angleName: 'leftElbow',
48
- minAngle: 80,
49
- maxAngle: 130 // "limbo zone" — too low to be up, too high to be down
50
- // Note: ideally this only fires when angle has stalled, not on the way down.
51
- // If your formRule engine doesn't support velocity/stall detection,
52
- // accept that it'll fire briefly during transitions too.
53
- }],
38
+ formRules: [],
54
39
  holdDurationMs: 0
55
40
  };
56
41
  //# sourceMappingURL=pushup.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["PUSHUP_CONFIG","name","type","postureFamily","visibilityThreshold","cameraAngle","angles","landmarkA","landmarkB","landmarkC","phases","phase","angleName","minAngle","maxAngle","repSequence","formRules","message","severity","holdDurationMs"],"sourceRoot":"../../../src","sources":["config/pushup.ts"],"mappings":";;AAEA;AACA;AACA;AACA;AACA;;AAEA,OAAO,MAAMA,aAA6B,GAAG;EAC3CC,IAAI,EAAE,SAAS;EACfC,IAAI,EAAE,KAAK;EACXC,aAAa,EAAE,MAAM;EACrBC,mBAAmB,EAAE,GAAG;EACxBC,WAAW,EAAE,OAAO;EACpBC,MAAM,EAAE,CACN;IAAEL,IAAI,EAAE,WAAW;IAAEM,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE;EAAG,CAAC,EAClE;IAAER,IAAI,EAAE,YAAY;IAAEM,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE;EAAG,CAAC,CACpE;EACDC,MAAM,EAAE;EACN;EACA;EACA;IAAEC,KAAK,EAAE,IAAI;IAAEC,SAAS,EAAE,WAAW;IAAEC,QAAQ,EAAE,GAAG;IAAEC,QAAQ,EAAE;EAAI,CAAC,EACrE;IAAEH,KAAK,EAAE,MAAM;IAAEC,SAAS,EAAE,WAAW;IAAEC,QAAQ,EAAE,EAAE;IAAEC,QAAQ,EAAE;EAAG,CAAC,CACtE;EACDC,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC;EACjCC,SAAS,EAAE;EACT;EACA;IACEf,IAAI,EAAE,YAAY;IAClBgB,OAAO,EAAE,UAAU;IACnBC,QAAQ,EAAE,MAAM;IAChBN,SAAS,EAAE,WAAW;IACtBC,QAAQ,EAAE,EAAE;IACZC,QAAQ,EAAE,GAAG,CAAE;IACf;IACA;IACA;EACF,CAAC,CACF;EACDK,cAAc,EAAE;AAClB,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["PUSHUP_CONFIG","name","type","postureFamily","visibilityThreshold","cameraAngle","angles","landmarkA","landmarkB","landmarkC","phases","phase","angleName","minAngle","maxAngle","repSequence","formRules","holdDurationMs"],"sourceRoot":"../../../src","sources":["config/pushup.ts"],"mappings":";;AAEA;AACA;AACA;AACA;AACA;;AAEA,OAAO,MAAMA,aAA6B,GAAG;EAC3CC,IAAI,EAAE,SAAS;EACfC,IAAI,EAAE,KAAK;EACXC,aAAa,EAAE,iBAAiB;EAChCC,mBAAmB,EAAE,GAAG;EACxBC,WAAW,EAAE,OAAO;EACpBC,MAAM,EAAE,CACN;IAAEL,IAAI,EAAE,WAAW;IAAEM,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE;EAAG,CAAC,EAClE;IAAER,IAAI,EAAE,YAAY;IAAEM,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE;EAAG,CAAC,CACpE;EACDC,MAAM,EAAE,CACN;IAAEC,KAAK,EAAE,IAAI;IAAEC,SAAS,EAAE,WAAW;IAAEC,QAAQ,EAAE,GAAG;IAAEC,QAAQ,EAAE;EAAI,CAAC,EACrE;IAAEH,KAAK,EAAE,MAAM;IAAEC,SAAS,EAAE,WAAW;IAAEC,QAAQ,EAAE,CAAC;IAAEC,QAAQ,EAAE;EAAI,CAAC,CACtE;EACDC,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC;EACjCC,SAAS,EAAE,EAAE;EACbC,cAAc,EAAE;AAClB,CAAC","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"pushup.d.ts","sourceRoot":"","sources":["../../../../src/config/pushup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAQlE,eAAO,MAAM,aAAa,EAAE,cAgC3B,CAAC"}
1
+ {"version":3,"file":"pushup.d.ts","sourceRoot":"","sources":["../../../../src/config/pushup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAQlE,eAAO,MAAM,aAAa,EAAE,cAiB3B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-pose-exercises",
3
- "version": "1.1.15",
3
+ "version": "1.1.17",
4
4
  "description": "Real-time on-device exercise tracking for React Native. Rep counting, form validation, and skeleton overlay powered by Apple Vision (iOS) and Google ML Kit (Android) with VisionCamera v5 via Nitro Modules.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -9,33 +9,18 @@ import type { ExerciseConfig } from '../NitroPoseExercises.nitro';
9
9
  export const PUSHUP_CONFIG: ExerciseConfig = {
10
10
  name: 'Push-Up',
11
11
  type: 'rep',
12
- postureFamily: 'none',
13
- visibilityThreshold: 0.3,
12
+ postureFamily: 'horizontalProne',
13
+ visibilityThreshold: 0.1,
14
14
  cameraAngle: 'front',
15
15
  angles: [
16
16
  { name: 'leftElbow', landmarkA: 11, landmarkB: 13, landmarkC: 15 },
17
17
  { name: 'rightElbow', landmarkA: 12, landmarkB: 14, landmarkC: 16 },
18
18
  ],
19
19
  phases: [
20
- // Calibrated for 2D-projected angles in front-facing portrait filming.
21
- // Real anatomical 180° appears as ~140-150° due to perspective foreshortening.
22
20
  { phase: 'up', angleName: 'leftElbow', minAngle: 130, maxAngle: 180 },
23
- { phase: 'down', angleName: 'leftElbow', minAngle: 40, maxAngle: 80 },
21
+ { phase: 'down', angleName: 'leftElbow', minAngle: 0, maxAngle: 120 },
24
22
  ],
25
23
  repSequence: ['up', 'down', 'up'],
26
- formRules: [
27
- // Trigger when descending but stalled above true-down threshold
28
- {
29
- name: 'shallowRep',
30
- message: 'Go lower',
31
- severity: 'info',
32
- angleName: 'leftElbow',
33
- minAngle: 80,
34
- maxAngle: 130, // "limbo zone" — too low to be up, too high to be down
35
- // Note: ideally this only fires when angle has stalled, not on the way down.
36
- // If your formRule engine doesn't support velocity/stall detection,
37
- // accept that it'll fire briefly during transitions too.
38
- },
39
- ],
24
+ formRules: [],
40
25
  holdDurationMs: 0,
41
26
  };