react-native-nitro-pose-exercises 1.0.8 → 1.1.1

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.
Files changed (82) hide show
  1. package/NitroPoseExercises.podspec +2 -1
  2. package/README.md +137 -92
  3. package/android/build.gradle +9 -10
  4. package/android/src/main/java/com/margelo/nitro/nitroposeexercises/NitroPoseExercises.kt +148 -125
  5. package/ios/NitroPoseExercises.swift +113 -88
  6. package/lib/module/config/bicep-curl.js +61 -0
  7. package/lib/module/config/bicep-curl.js.map +1 -0
  8. package/lib/module/config/chair-pose.js +64 -0
  9. package/lib/module/config/chair-pose.js.map +1 -0
  10. package/lib/module/config/cobra-pose.js +57 -0
  11. package/lib/module/config/cobra-pose.js.map +1 -0
  12. package/lib/module/config/downward-dog.js +65 -0
  13. package/lib/module/config/downward-dog.js.map +1 -0
  14. package/lib/module/config/lunge.js +62 -0
  15. package/lib/module/config/lunge.js.map +1 -0
  16. package/lib/module/config/plank.js +57 -0
  17. package/lib/module/config/plank.js.map +1 -0
  18. package/lib/module/config/shoulder-press.js +62 -0
  19. package/lib/module/config/shoulder-press.js.map +1 -0
  20. package/lib/module/config/situp.js +48 -0
  21. package/lib/module/config/situp.js.map +1 -0
  22. package/lib/module/config/squat.js +69 -0
  23. package/lib/module/config/squat.js.map +1 -0
  24. package/lib/module/config/tree-pose.js +57 -0
  25. package/lib/module/config/tree-pose.js.map +1 -0
  26. package/lib/module/config/tricep-dip.js +55 -0
  27. package/lib/module/config/tricep-dip.js.map +1 -0
  28. package/lib/module/config/wall-sit.js +57 -0
  29. package/lib/module/config/wall-sit.js.map +1 -0
  30. package/lib/module/config/warrior-i.js +79 -0
  31. package/lib/module/config/warrior-i.js.map +1 -0
  32. package/lib/module/config/warrior-ii.js +86 -0
  33. package/lib/module/config/warrior-ii.js.map +1 -0
  34. package/lib/module/index.js +19 -0
  35. package/lib/module/index.js.map +1 -1
  36. package/lib/typescript/src/config/bicep-curl.d.ts +3 -0
  37. package/lib/typescript/src/config/bicep-curl.d.ts.map +1 -0
  38. package/lib/typescript/src/config/chair-pose.d.ts +3 -0
  39. package/lib/typescript/src/config/chair-pose.d.ts.map +1 -0
  40. package/lib/typescript/src/config/cobra-pose.d.ts +3 -0
  41. package/lib/typescript/src/config/cobra-pose.d.ts.map +1 -0
  42. package/lib/typescript/src/config/downward-dog.d.ts +3 -0
  43. package/lib/typescript/src/config/downward-dog.d.ts.map +1 -0
  44. package/lib/typescript/src/config/lunge.d.ts +3 -0
  45. package/lib/typescript/src/config/lunge.d.ts.map +1 -0
  46. package/lib/typescript/src/config/plank.d.ts +3 -0
  47. package/lib/typescript/src/config/plank.d.ts.map +1 -0
  48. package/lib/typescript/src/config/shoulder-press.d.ts +3 -0
  49. package/lib/typescript/src/config/shoulder-press.d.ts.map +1 -0
  50. package/lib/typescript/src/config/situp.d.ts +3 -0
  51. package/lib/typescript/src/config/situp.d.ts.map +1 -0
  52. package/lib/typescript/src/config/squat.d.ts +3 -0
  53. package/lib/typescript/src/config/squat.d.ts.map +1 -0
  54. package/lib/typescript/src/config/tree-pose.d.ts +3 -0
  55. package/lib/typescript/src/config/tree-pose.d.ts.map +1 -0
  56. package/lib/typescript/src/config/tricep-dip.d.ts +3 -0
  57. package/lib/typescript/src/config/tricep-dip.d.ts.map +1 -0
  58. package/lib/typescript/src/config/wall-sit.d.ts +3 -0
  59. package/lib/typescript/src/config/wall-sit.d.ts.map +1 -0
  60. package/lib/typescript/src/config/warrior-i.d.ts +3 -0
  61. package/lib/typescript/src/config/warrior-i.d.ts.map +1 -0
  62. package/lib/typescript/src/config/warrior-ii.d.ts +3 -0
  63. package/lib/typescript/src/config/warrior-ii.d.ts.map +1 -0
  64. package/lib/typescript/src/index.d.ts +14 -0
  65. package/lib/typescript/src/index.d.ts.map +1 -1
  66. package/nitro.json +8 -1
  67. package/package.json +10 -10
  68. package/src/config/bicep-curl.ts +64 -0
  69. package/src/config/chair-pose.ts +67 -0
  70. package/src/config/cobra-pose.ts +59 -0
  71. package/src/config/downward-dog.ts +68 -0
  72. package/src/config/lunge.ts +65 -0
  73. package/src/config/plank.ts +59 -0
  74. package/src/config/shoulder-press.ts +63 -0
  75. package/src/config/situp.ts +51 -0
  76. package/src/config/squat.ts +71 -0
  77. package/src/config/tree-pose.ts +59 -0
  78. package/src/config/tricep-dip.ts +58 -0
  79. package/src/config/wall-sit.ts +59 -0
  80. package/src/config/warrior-i.ts +82 -0
  81. package/src/config/warrior-ii.ts +88 -0
  82. package/src/index.tsx +19 -0
@@ -1,16 +1,19 @@
1
1
  package com.margelo.nitro.nitroposeexercises
2
2
 
3
3
  import android.graphics.Bitmap
4
+ import android.graphics.Matrix
5
+ import android.media.Image
4
6
  import androidx.annotation.Keep
5
7
  import com.facebook.proguard.annotations.DoNotStrip
6
- import com.google.mediapipe.framework.image.BitmapImageBuilder
7
- import com.google.mediapipe.tasks.core.BaseOptions
8
- import com.google.mediapipe.tasks.vision.core.RunningMode
9
- import com.google.mediapipe.tasks.vision.poselandmarker.PoseLandmarker
10
- import com.google.mediapipe.tasks.vision.poselandmarker.PoseLandmarkerOptions
8
+ import com.google.mlkit.vision.common.InputImage
9
+ import com.google.mlkit.vision.pose.PoseDetection
10
+ import com.google.mlkit.vision.pose.PoseDetector
11
+ import com.google.mlkit.vision.pose.PoseLandmark
12
+ import com.google.mlkit.vision.pose.defaults.PoseDetectorOptions
11
13
  import com.margelo.nitro.NitroModules
12
14
  import com.margelo.nitro.core.Promise
13
15
  import com.margelo.nitro.camera.HybridFrameSpec
16
+ import com.margelo.nitro.camera.NativeFrame
14
17
  import kotlin.math.acos
15
18
  import kotlin.math.max
16
19
  import kotlin.math.min
@@ -20,10 +23,52 @@ import kotlin.math.sqrt
20
23
  @DoNotStrip
21
24
  class HybridPoseExercise : HybridNitroPoseExercisesSpec() {
22
25
 
23
- // ─── MediaPipe ──────────────────────────────────────────────
24
- private var poseLandmarker: PoseLandmarker? = null
26
+ // ─── ML Kit ─────────────────────────────────────────────────
27
+ private var poseDetector: PoseDetector? = null
25
28
  private var isInitialized = false
26
29
 
30
+ // ─── Cached Landmarks (ML Kit is async, we cache last result) ──
31
+ private var cachedLandmarks: Array<Landmark> = emptyArray()
32
+ private val landmarkLock = Any()
33
+
34
+ // ─── Landmark Index Mapping ─────────────────────────────────
35
+ // ML Kit PoseLandmark type → MediaPipe index that JS configs expect
36
+ private val mlKitToMediaPipeMap = mapOf(
37
+ PoseLandmark.NOSE to 0,
38
+ PoseLandmark.LEFT_EYE_INNER to 1,
39
+ PoseLandmark.LEFT_EYE to 2,
40
+ PoseLandmark.LEFT_EYE_OUTER to 3,
41
+ PoseLandmark.RIGHT_EYE_INNER to 4,
42
+ PoseLandmark.RIGHT_EYE to 5,
43
+ PoseLandmark.RIGHT_EYE_OUTER to 6,
44
+ PoseLandmark.LEFT_EAR to 7,
45
+ PoseLandmark.RIGHT_EAR to 8,
46
+ PoseLandmark.LEFT_MOUTH to 9,
47
+ PoseLandmark.RIGHT_MOUTH to 10,
48
+ PoseLandmark.LEFT_SHOULDER to 11,
49
+ PoseLandmark.RIGHT_SHOULDER to 12,
50
+ PoseLandmark.LEFT_ELBOW to 13,
51
+ PoseLandmark.RIGHT_ELBOW to 14,
52
+ PoseLandmark.LEFT_WRIST to 15,
53
+ PoseLandmark.RIGHT_WRIST to 16,
54
+ PoseLandmark.LEFT_PINKY to 17,
55
+ PoseLandmark.RIGHT_PINKY to 18,
56
+ PoseLandmark.LEFT_INDEX to 19,
57
+ PoseLandmark.RIGHT_INDEX to 20,
58
+ PoseLandmark.LEFT_THUMB to 21,
59
+ PoseLandmark.RIGHT_THUMB to 22,
60
+ PoseLandmark.LEFT_HIP to 23,
61
+ PoseLandmark.RIGHT_HIP to 24,
62
+ PoseLandmark.LEFT_KNEE to 25,
63
+ PoseLandmark.RIGHT_KNEE to 26,
64
+ PoseLandmark.LEFT_ANKLE to 27,
65
+ PoseLandmark.RIGHT_ANKLE to 28,
66
+ PoseLandmark.LEFT_HEEL to 29,
67
+ PoseLandmark.RIGHT_HEEL to 30,
68
+ PoseLandmark.LEFT_FOOT_INDEX to 31,
69
+ PoseLandmark.RIGHT_FOOT_INDEX to 32,
70
+ )
71
+
27
72
  // ─── Exercise Config ────────────────────────────────────────
28
73
  private var exerciseConfig: ExerciseConfig? = null
29
74
 
@@ -46,8 +91,6 @@ class HybridPoseExercise : HybridNitroPoseExercisesSpec() {
46
91
  private var sessionStartTime: Long = System.currentTimeMillis()
47
92
  private var targetReps: Double = 0.0
48
93
  private var countdownSeconds: Double = 0.0
49
- private var frameCount: Int = 0
50
- private val processEveryNFrames: Int = 3
51
94
 
52
95
  // ─── Form Tracking ──────────────────────────────────────────
53
96
  private var lastFormFeedbackTime = mutableMapOf<String, Long>()
@@ -60,6 +103,10 @@ class HybridPoseExercise : HybridNitroPoseExercisesSpec() {
60
103
  // ─── Pose Tracking ──────────────────────────────────────────
61
104
  private var poseWasLost = false
62
105
 
106
+ // ─── Frame Throttle ─────────────────────────────────────────
107
+ private var frameCount: Int = 0
108
+ private val processEveryNFrames: Int = 3
109
+
63
110
  // ─── Callbacks ──────────────────────────────────────────────
64
111
  override var onRepComplete: ((data: RepData) -> Unit)? = null
65
112
  override var onPhaseChange: ((phase: ExercisePhase) -> Unit)? = null
@@ -77,31 +124,21 @@ class HybridPoseExercise : HybridNitroPoseExercisesSpec() {
77
124
  // ═══════════════════════════════════════════════════════════
78
125
 
79
126
  override fun initialize(modelPath: String): Promise<Unit> {
127
+ // No model file needed — ML Kit downloads/bundles its own model
80
128
  return Promise.async {
81
- val context = NitroModules.applicationContext
82
- ?: throw Error("No ApplicationContext set!")
83
-
84
- val baseOptions = BaseOptions.builder()
85
- .setModelAssetPath(modelPath)
86
- .build()
87
-
88
- val options = PoseLandmarkerOptions.builder()
89
- .setBaseOptions(baseOptions)
90
- .setRunningMode(RunningMode.IMAGE)
91
- .setNumPoses(1)
92
- .setMinPoseDetectionConfidence(0.5f)
93
- .setMinPosePresenceConfidence(0.5f)
94
- .setMinTrackingConfidence(0.5f)
129
+ val options = PoseDetectorOptions.Builder()
130
+ .setDetectorMode(PoseDetectorOptions.STREAM_MODE)
95
131
  .build()
96
132
 
97
- poseLandmarker = PoseLandmarker.createFromOptions(context, options)
133
+ poseDetector = PoseDetection.getClient(options)
98
134
  isInitialized = true
135
+ println("[PoseExercise] Initialized with ML Kit Pose Detection (no model file needed)")
99
136
  }
100
137
  }
101
138
 
102
139
  override fun release() {
103
- poseLandmarker?.close()
104
- poseLandmarker = null
140
+ poseDetector?.close()
141
+ poseDetector = null
105
142
  isInitialized = false
106
143
  _status = SessionStatus.IDLE
107
144
  resetSession()
@@ -151,105 +188,88 @@ class HybridPoseExercise : HybridNitroPoseExercisesSpec() {
151
188
  }
152
189
 
153
190
  // ═══════════════════════════════════════════════════════════
154
- // Frame Processing
191
+ // Frame Processing (ML Kit — async with cached results)
155
192
  // ═══════════════════════════════════════════════════════════
156
193
 
157
- override fun processFrame(frame: HybridFrameSpec) {
194
+ override fun processFrame(frame: HybridFrameSpec) {
158
195
  if (_status != SessionStatus.ACTIVE && _status != SessionStatus.COUNTDOWN) return
159
- if (!isInitialized || poseLandmarker == null) return
196
+ if (!isInitialized || poseDetector == null) return
160
197
 
198
+ // Frame throttle
161
199
  frameCount++
162
200
  if (frameCount % processEveryNFrames != 0) return
163
201
 
202
+ // Get the ImageProxy from VisionCamera frame
203
+ val nativeFrame = frame as? NativeFrame ?: return
204
+ val imageProxy = nativeFrame.image ?: return
205
+
164
206
  try {
165
- val nativeBuffer = frame.getNativeBuffer()
166
- val width = frame.getWidth().toInt()
167
- val height = frame.getHeight().toInt()
207
+ // ML Kit takes InputImage directly from ImageProxy — no color conversion needed
208
+ @androidx.camera.core.ExperimentalGetImage
209
+ val mediaImage: Image = imageProxy.image ?: return
210
+ val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
168
211
 
169
- // On Android, nativeBuffer.pointer is an AHardwareBuffer*
170
- // Convert to Bitmap via Android's HardwareBuffer API
171
- val hardwareBuffer = android.hardware.HardwareBuffer.wrap(nativeBuffer.pointer)
172
- val bitmap = Bitmap.wrapHardwareBuffer(hardwareBuffer, null)
173
- ?: throw Exception("Failed to wrap HardwareBuffer to Bitmap")
212
+ // ML Kit is async — fire detection, cache result, use cached landmarks for current frame
213
+ poseDetector!!.process(inputImage)
214
+ .addOnSuccessListener { pose ->
215
+ val poseLandmarks = pose.allPoseLandmarks
174
216
 
175
- // MediaPipe needs ARGB_8888, not hardware bitmap
176
- val softBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false)
217
+ if (poseLandmarks.isNotEmpty()) {
218
+ if (poseWasLost) {
219
+ poseWasLost = false
220
+ onPoseRegained?.invoke()
221
+ }
177
222
 
178
- val mpImage = com.google.mediapipe.framework.image.BitmapImageBuilder(softBitmap).build()
179
- val result = poseLandmarker!!.detect(mpImage)
223
+ // Build landmark array mapped to MediaPipe indices (34 slots)
224
+ val landmarkArray = Array(34) { Landmark(x = 0.0, y = 0.0, z = 0.0, visibility = 0.0) }
180
225
 
181
- if (result.landmarks().isNotEmpty()) {
182
- val poseLandmarks = result.landmarks()[0]
226
+ val imageWidth = inputImage.width.toDouble()
227
+ val imageHeight = inputImage.height.toDouble()
183
228
 
184
- if (poseWasLost) {
185
- poseWasLost = false
186
- onPoseRegained?.invoke()
229
+ for (poseLandmark in poseLandmarks) {
230
+ val mediaPipeIndex = mlKitToMediaPipeMap[poseLandmark.landmarkType] ?: continue
231
+ if (mediaPipeIndex >= 34) continue
232
+
233
+ // Normalize coordinates to 0-1 range
234
+ landmarkArray[mediaPipeIndex] = Landmark(
235
+ x = poseLandmark.position.x.toDouble() / imageWidth,
236
+ y = poseLandmark.position.y.toDouble() / imageHeight,
237
+ z = poseLandmark.position3D.z.toDouble(),
238
+ visibility = poseLandmark.inFrameLikelihood.toDouble()
239
+ )
187
240
  }
188
241
 
189
- _landmarks = poseLandmarks.map { lm ->
190
- Landmark(
191
- x = lm.x().toDouble(),
192
- y = lm.y().toDouble(),
193
- z = lm.z().toDouble(),
194
- visibility = (lm.visibility().orElse(0f)).toDouble()
195
- )
196
- }.toTypedArray()
197
-
198
- if (_status == SessionStatus.ACTIVE) {
199
- processExerciseLogic()
242
+ synchronized(landmarkLock) {
243
+ cachedLandmarks = landmarkArray
200
244
  }
201
- } else {
245
+ } else {
202
246
  if (!poseWasLost) {
203
- poseWasLost = true
204
- onPoseLost?.invoke()
247
+ poseWasLost = true
248
+ onPoseLost?.invoke()
249
+ }
250
+ synchronized(landmarkLock) {
251
+ cachedLandmarks = emptyArray()
205
252
  }
206
- _landmarks = emptyArray()
253
+ }
254
+ }
255
+ .addOnFailureListener { e ->
256
+ println("[PoseExercise] ML Kit error: ${e.message}")
207
257
  }
208
258
 
209
- softBitmap.recycle()
210
- bitmap.recycle()
211
- nativeBuffer.release()
212
-
213
- } catch (e: Exception) {
214
- // MediaPipe detection failed — skip this frame
215
- }
216
- }
217
-
218
- // ═══════════════════════════════════════════════════════════
219
- // ImageProxy to Bitmap conversion
220
- // ═══════════════════════════════════════════════════════════
221
-
222
- private fun imageProxyToBitmap(imageProxy: androidx.camera.core.ImageProxy): Bitmap {
223
- val buffer = imageProxy.planes[0].buffer
224
- val bytes = ByteArray(buffer.remaining())
225
- buffer.get(bytes)
226
-
227
- val yuvImage = android.graphics.YuvImage(
228
- bytes,
229
- android.graphics.ImageFormat.NV21,
230
- imageProxy.width,
231
- imageProxy.height,
232
- null
233
- )
259
+ // Use cached landmarks for exercise logic (from previous frame's detection)
260
+ val currentLandmarks: Array<Landmark>
261
+ synchronized(landmarkLock) {
262
+ currentLandmarks = cachedLandmarks.copyOf()
263
+ }
234
264
 
235
- val out = java.io.ByteArrayOutputStream()
236
- yuvImage.compressToJpeg(
237
- android.graphics.Rect(0, 0, imageProxy.width, imageProxy.height),
238
- 100,
239
- out
240
- )
265
+ _landmarks = currentLandmarks
241
266
 
242
- val jpegBytes = out.toByteArray()
243
- val bitmap = android.graphics.BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.size)
267
+ if (currentLandmarks.isNotEmpty() && _status == SessionStatus.ACTIVE) {
268
+ processExerciseLogic()
269
+ }
244
270
 
245
- // Apply rotation if needed
246
- val rotation = imageProxy.imageInfo.rotationDegrees
247
- return if (rotation != 0) {
248
- val matrix = Matrix()
249
- matrix.postRotate(rotation.toFloat())
250
- Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
251
- } else {
252
- bitmap
271
+ } catch (e: Exception) {
272
+ println("[PoseExercise] Frame processing error: ${e.message}")
253
273
  }
254
274
  }
255
275
 
@@ -261,7 +281,6 @@ override fun processFrame(frame: HybridFrameSpec) {
261
281
  val config = exerciseConfig ?: return
262
282
  if (_landmarks.isEmpty()) return
263
283
 
264
- // 1. Calculate all angles
265
284
  val currentAngles = mutableMapOf<String, Double>()
266
285
  val angleSnapshots = mutableListOf<AngleSnapshot>()
267
286
 
@@ -272,6 +291,9 @@ override fun processFrame(frame: HybridFrameSpec) {
272
291
 
273
292
  if (a >= _landmarks.size || b >= _landmarks.size || c >= _landmarks.size) continue
274
293
 
294
+ // Only calculate if all three landmarks have reasonable confidence
295
+ if (_landmarks[a].visibility < 0.3 || _landmarks[b].visibility < 0.3 || _landmarks[c].visibility < 0.3) continue
296
+
275
297
  val angle = calculateAngle(_landmarks[a], _landmarks[b], _landmarks[c])
276
298
  currentAngles[angleDef.name] = angle
277
299
  angleSnapshots.add(AngleSnapshot(name = angleDef.name, value = angle))
@@ -279,7 +301,6 @@ override fun processFrame(frame: HybridFrameSpec) {
279
301
 
280
302
  repAngleSnapshots = angleSnapshots.toTypedArray()
281
303
 
282
- // 2. Determine current phase
283
304
  val detectedPhase = determinePhase(currentAngles, config)
284
305
 
285
306
  if (detectedPhase != _currentPhase && detectedPhase != ExercisePhase.UNKNOWN) {
@@ -288,10 +309,8 @@ override fun processFrame(frame: HybridFrameSpec) {
288
309
  handlePhaseTransition(detectedPhase, config)
289
310
  }
290
311
 
291
- // 3. Check form rules
292
312
  checkFormRules(currentAngles, config)
293
313
 
294
- // 4. Handle hold-based exercises
295
314
  if (config.type == ExerciseType.HOLD) {
296
315
  handleHoldProgress(currentAngles, config)
297
316
  }
@@ -345,37 +364,37 @@ override fun processFrame(frame: HybridFrameSpec) {
345
364
  if (phaseHistory.size >= repSeq.size) {
346
365
  val tail = phaseHistory.takeLast(repSeq.size)
347
366
 
348
- if (tail == repSeq.toList()) {
367
+ if (tail == repSeq.toList()) {
349
368
  val now = System.currentTimeMillis()
350
369
  val repDuration = (now - repStartTime).toDouble()
351
370
 
352
371
  // Minimum 800ms per rep
353
372
  if (repDuration < 800) {
354
- phaseHistory.clear()
355
- phaseHistory.add(newPhase)
356
- return
373
+ phaseHistory.clear()
374
+ phaseHistory.add(newPhase)
375
+ return
357
376
  }
358
377
 
359
378
  // Don't count rep if form is terrible
360
379
  if (repFormScore <= 30) {
361
- onFormFeedback?.invoke(FormFeedback(
362
- ruleName = "poorForm",
363
- message = "Fix your form before continuing",
364
- severity = FormSeverity.ERROR
365
- ))
366
- repFormScore = 100.0
367
- phaseHistory.clear()
368
- phaseHistory.add(newPhase)
369
- return
380
+ onFormFeedback?.invoke(FormFeedback(
381
+ ruleName = "poorForm",
382
+ message = "Fix your form before continuing",
383
+ severity = FormSeverity.ERROR
384
+ ))
385
+ repFormScore = 100.0
386
+ phaseHistory.clear()
387
+ phaseHistory.add(newPhase)
388
+ return
370
389
  }
371
390
 
372
391
  _repCount += 1.0
373
392
 
374
393
  val repData = RepData(
375
- repNumber = _repCount,
376
- durationMs = repDuration,
377
- formScore = repFormScore,
378
- angles = repAngleSnapshots
394
+ repNumber = _repCount,
395
+ durationMs = repDuration,
396
+ formScore = repFormScore,
397
+ angles = repAngleSnapshots
379
398
  )
380
399
 
381
400
  allRepDurations.add(repDuration)
@@ -389,9 +408,9 @@ override fun processFrame(frame: HybridFrameSpec) {
389
408
  phaseHistory.add(newPhase)
390
409
 
391
410
  if (targetReps > 0 && _repCount >= targetReps) {
392
- completeSession()
411
+ completeSession()
393
412
  }
394
- }
413
+ }
395
414
  }
396
415
 
397
416
  val maxHistory = repSeq.size * 2
@@ -536,5 +555,9 @@ override fun processFrame(frame: HybridFrameSpec) {
536
555
  poseWasLost = false
537
556
  targetReps = 0.0
538
557
  countdownSeconds = 0.0
558
+ frameCount = 0
559
+ synchronized(landmarkLock) {
560
+ cachedLandmarks = emptyArray()
561
+ }
539
562
  }
540
563
  }