kinemotion 0.66.3__py3-none-any.whl → 0.66.4__py3-none-any.whl

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.

Potentially problematic release.


This version of kinemotion might be problematic. Click here for more details.

kinemotion/cmj/api.py CHANGED
@@ -214,6 +214,169 @@ def _create_result_metadata(
214
214
  )
215
215
 
216
216
 
217
+ def _run_pose_tracking(
218
+ video: VideoProcessor,
219
+ quality_preset: QualityPreset,
220
+ detection_confidence: float | None,
221
+ tracking_confidence: float | None,
222
+ pose_tracker: PoseTracker | None,
223
+ verbose: bool,
224
+ timer: Timer,
225
+ ) -> tuple[list[NDArray[np.uint8]], list, list[int]]:
226
+ """Initialize tracker and process all frames."""
227
+ if verbose:
228
+ print(
229
+ f"Video: {video.width}x{video.height} @ {video.fps:.2f} fps, "
230
+ f"{video.frame_count} frames"
231
+ )
232
+
233
+ det_conf, track_conf = determine_confidence_levels(
234
+ quality_preset, detection_confidence, tracking_confidence
235
+ )
236
+
237
+ if verbose:
238
+ print("Processing all frames with MediaPipe pose tracking...")
239
+
240
+ tracker = pose_tracker or PoseTracker(
241
+ min_detection_confidence=det_conf,
242
+ min_tracking_confidence=track_conf,
243
+ timer=timer,
244
+ )
245
+ should_close_tracker = pose_tracker is None
246
+
247
+ return process_all_frames(video, tracker, verbose, timer, close_tracker=should_close_tracker)
248
+
249
+
250
+ def _get_tuned_parameters(
251
+ video: VideoProcessor,
252
+ landmarks_sequence: list,
253
+ quality_preset: QualityPreset,
254
+ overrides: AnalysisOverrides | None,
255
+ verbose: bool,
256
+ timer: Timer,
257
+ ) -> AnalysisParameters:
258
+ """Analyze sample and tune parameters with expert overrides."""
259
+ with timer.measure("parameter_auto_tuning"):
260
+ characteristics = analyze_video_sample(landmarks_sequence, video.fps, video.frame_count)
261
+ params = auto_tune_parameters(characteristics, quality_preset)
262
+ params = apply_expert_overrides(
263
+ params,
264
+ overrides.smoothing_window if overrides else None,
265
+ overrides.velocity_threshold if overrides else None,
266
+ overrides.min_contact_frames if overrides else None,
267
+ overrides.visibility_threshold if overrides else None,
268
+ )
269
+
270
+ if verbose:
271
+ print_verbose_parameters(video, characteristics, quality_preset, params)
272
+
273
+ return params
274
+
275
+
276
+ def _run_kinematic_analysis(
277
+ video: VideoProcessor,
278
+ smoothed_landmarks: list,
279
+ params: AnalysisParameters,
280
+ verbose: bool,
281
+ timer: Timer,
282
+ ) -> tuple[CMJMetrics, NDArray[np.float64], NDArray[np.float64]]:
283
+ """Extract positions, detect phases, and calculate metrics."""
284
+ if verbose:
285
+ print("Extracting vertical positions (Hip and Foot)...")
286
+ with timer.measure("vertical_position_extraction"):
287
+ vertical_positions, visibilities = extract_vertical_positions(
288
+ smoothed_landmarks, target="hip"
289
+ )
290
+ foot_positions, _ = extract_vertical_positions(smoothed_landmarks, target="foot")
291
+
292
+ if verbose:
293
+ print("Detecting CMJ phases...")
294
+ with timer.measure("phase_detection"):
295
+ phases = detect_cmj_phases(
296
+ vertical_positions,
297
+ video.fps,
298
+ window_length=params.smoothing_window,
299
+ polyorder=params.polyorder,
300
+ landing_positions=foot_positions,
301
+ timer=timer,
302
+ )
303
+
304
+ if phases is None:
305
+ raise ValueError("Could not detect CMJ phases in video")
306
+
307
+ standing_end, lowest_point, takeoff_frame, landing_frame = phases
308
+
309
+ if verbose:
310
+ print("Calculating metrics...")
311
+ with timer.measure("metrics_calculation"):
312
+ velocities = compute_signed_velocity(
313
+ vertical_positions,
314
+ window_length=params.smoothing_window,
315
+ polyorder=params.polyorder,
316
+ )
317
+ metrics = calculate_cmj_metrics(
318
+ vertical_positions,
319
+ velocities,
320
+ standing_end,
321
+ lowest_point,
322
+ takeoff_frame,
323
+ landing_frame,
324
+ video.fps,
325
+ tracking_method="hip_hybrid",
326
+ )
327
+
328
+ return metrics, vertical_positions, visibilities
329
+
330
+
331
+ def _finalize_analysis_results(
332
+ metrics: CMJMetrics,
333
+ video: VideoProcessor,
334
+ video_path: str,
335
+ vertical_positions: NDArray[np.float64],
336
+ visibilities: NDArray[np.float64],
337
+ params: AnalysisParameters,
338
+ quality_preset: QualityPreset,
339
+ start_time: float,
340
+ timer: Timer,
341
+ verbose: bool,
342
+ ) -> None:
343
+ """Assess quality, validate metrics, and attach metadata."""
344
+ if verbose:
345
+ print("Assessing tracking quality...")
346
+ with timer.measure("quality_assessment"):
347
+ _, outlier_mask = reject_outliers(
348
+ vertical_positions,
349
+ use_ransac=True,
350
+ use_median=True,
351
+ interpolate=False,
352
+ )
353
+ quality_result = assess_jump_quality(
354
+ visibilities=visibilities,
355
+ positions=vertical_positions,
356
+ outlier_mask=outlier_mask,
357
+ fps=video.fps,
358
+ phases_detected=True,
359
+ phase_count=4,
360
+ )
361
+
362
+ _print_quality_warnings(quality_result, verbose)
363
+
364
+ with timer.measure("metrics_validation"):
365
+ validator = CMJMetricsValidator()
366
+ validation_result = validator.validate(metrics.to_dict()) # type: ignore[arg-type]
367
+ metrics.validation_result = validation_result
368
+
369
+ algorithm_config = _create_algorithm_config(params)
370
+ video_info = _create_video_info(video_path, video)
371
+ processing_info = _create_processing_info(start_time, quality_preset, timer)
372
+ result_metadata = _create_result_metadata(
373
+ quality_result, video_info, processing_info, algorithm_config
374
+ )
375
+ metrics.result_metadata = result_metadata
376
+
377
+ _print_validation_results(validation_result, verbose)
378
+
379
+
217
380
  @dataclass
218
381
  class CMJVideoConfig:
219
382
  """Configuration for processing a single CMJ video."""
@@ -285,112 +448,29 @@ def process_cmj_video(
285
448
 
286
449
  with timer.measure("video_initialization"):
287
450
  with VideoProcessor(video_path, timer=timer) as video:
288
- if verbose:
289
- print(
290
- f"Video: {video.width}x{video.height} @ {video.fps:.2f} fps, "
291
- f"{video.frame_count} frames"
292
- )
293
-
294
- det_conf, track_conf = determine_confidence_levels(
295
- quality_preset, detection_confidence, tracking_confidence
296
- )
297
-
298
- if verbose:
299
- print("Processing all frames with MediaPipe pose tracking...")
300
-
301
- tracker = pose_tracker or PoseTracker(
302
- min_detection_confidence=det_conf,
303
- min_tracking_confidence=track_conf,
304
- timer=timer,
451
+ # 1. Pose Tracking
452
+ frames, landmarks_sequence, frame_indices = _run_pose_tracking(
453
+ video,
454
+ quality_preset,
455
+ detection_confidence,
456
+ tracking_confidence,
457
+ pose_tracker,
458
+ verbose,
459
+ timer,
305
460
  )
306
- should_close_tracker = pose_tracker is None
307
461
 
308
- frames, landmarks_sequence, frame_indices = process_all_frames(
309
- video, tracker, verbose, timer, close_tracker=should_close_tracker
462
+ # 2. Parameters & Smoothing
463
+ params = _get_tuned_parameters(
464
+ video, landmarks_sequence, quality_preset, overrides, verbose, timer
310
465
  )
311
-
312
- with timer.measure("parameter_auto_tuning"):
313
- characteristics = analyze_video_sample(
314
- landmarks_sequence, video.fps, video.frame_count
315
- )
316
- params = auto_tune_parameters(characteristics, quality_preset)
317
- params = apply_expert_overrides(
318
- params,
319
- overrides.smoothing_window if overrides else None,
320
- overrides.velocity_threshold if overrides else None,
321
- overrides.min_contact_frames if overrides else None,
322
- overrides.visibility_threshold if overrides else None,
323
- )
324
-
325
- if verbose:
326
- print_verbose_parameters(video, characteristics, quality_preset, params)
327
-
328
466
  smoothed_landmarks = apply_smoothing(landmarks_sequence, params, verbose, timer)
329
467
 
330
- if verbose:
331
- print("Extracting vertical positions (Hip and Foot)...")
332
- with timer.measure("vertical_position_extraction"):
333
- vertical_positions, visibilities = extract_vertical_positions(
334
- smoothed_landmarks, target="hip"
335
- )
336
- foot_positions, _ = extract_vertical_positions(smoothed_landmarks, target="foot")
337
-
338
- if verbose:
339
- print("Detecting CMJ phases...")
340
- with timer.measure("phase_detection"):
341
- phases = detect_cmj_phases(
342
- vertical_positions,
343
- video.fps,
344
- window_length=params.smoothing_window,
345
- polyorder=params.polyorder,
346
- landing_positions=foot_positions,
347
- timer=timer,
348
- )
349
-
350
- if phases is None:
351
- raise ValueError("Could not detect CMJ phases in video")
352
-
353
- standing_end, lowest_point, takeoff_frame, landing_frame = phases
354
-
355
- if verbose:
356
- print("Calculating metrics...")
357
- with timer.measure("metrics_calculation"):
358
- velocities = compute_signed_velocity(
359
- vertical_positions,
360
- window_length=params.smoothing_window,
361
- polyorder=params.polyorder,
362
- )
363
- metrics = calculate_cmj_metrics(
364
- vertical_positions,
365
- velocities,
366
- standing_end,
367
- lowest_point,
368
- takeoff_frame,
369
- landing_frame,
370
- video.fps,
371
- tracking_method="hip_hybrid",
372
- )
373
-
374
- if verbose:
375
- print("Assessing tracking quality...")
376
- with timer.measure("quality_assessment"):
377
- _, outlier_mask = reject_outliers(
378
- vertical_positions,
379
- use_ransac=True,
380
- use_median=True,
381
- interpolate=False,
382
- )
383
- quality_result = assess_jump_quality(
384
- visibilities=visibilities,
385
- positions=vertical_positions,
386
- outlier_mask=outlier_mask,
387
- fps=video.fps,
388
- phases_detected=True,
389
- phase_count=4,
390
- )
391
-
392
- _print_quality_warnings(quality_result, verbose)
468
+ # 3. Kinematic Analysis
469
+ metrics, vertical_positions, visibilities = _run_kinematic_analysis(
470
+ video, smoothed_landmarks, params, verbose, timer
471
+ )
393
472
 
473
+ # 4. Debug Video Generation (Optional)
394
474
  if output_video:
395
475
  _generate_debug_video(
396
476
  output_video,
@@ -403,24 +483,23 @@ def process_cmj_video(
403
483
  verbose,
404
484
  )
405
485
 
406
- with timer.measure("metrics_validation"):
407
- validator = CMJMetricsValidator()
408
- validation_result = validator.validate(metrics.to_dict()) # type: ignore[arg-type]
409
- metrics.validation_result = validation_result
410
-
411
- algorithm_config = _create_algorithm_config(params)
412
- video_info = _create_video_info(video_path, video)
413
- processing_info = _create_processing_info(start_time, quality_preset, timer)
414
- result_metadata = _create_result_metadata(
415
- quality_result, video_info, processing_info, algorithm_config
486
+ # 5. Finalization (Quality, Metadata, Validation)
487
+ _finalize_analysis_results(
488
+ metrics,
489
+ video,
490
+ video_path,
491
+ vertical_positions,
492
+ visibilities,
493
+ params,
494
+ quality_preset,
495
+ start_time,
496
+ timer,
497
+ verbose,
416
498
  )
417
- metrics.result_metadata = result_metadata
418
499
 
419
500
  if json_output:
420
501
  _save_metrics_to_json(metrics, json_output, timer, verbose)
421
502
 
422
- _print_validation_results(validation_result, verbose)
423
-
424
503
  if verbose:
425
504
  _print_timing_summary(start_time, timer, metrics)
426
505
 
@@ -6,6 +6,7 @@ import shutil
6
6
  import subprocess
7
7
  import time
8
8
  from pathlib import Path
9
+ from typing import Any
9
10
 
10
11
  import cv2
11
12
  import numpy as np
@@ -73,53 +74,67 @@ def create_video_writer(
73
74
  # ⚠️ CRITICAL: VP9 (vp09) is EXCLUDED - not supported on iOS/iPhone/iPad browsers!
74
75
  # Adding VP9 will break debug video playback on all iOS devices.
75
76
  codecs_to_try = ["avc1", "mp4v"]
76
-
77
- writer = None
78
- used_codec = "mp4v" # Default fallback
79
- codec_attempt_log = []
77
+ codec_attempt_log: list[dict[str, Any]] = []
80
78
 
81
79
  for codec in codecs_to_try:
82
- try:
83
- fourcc = cv2.VideoWriter_fourcc(*codec) # type: ignore[attr-defined]
84
- writer = cv2.VideoWriter(output_path, fourcc, fps, (display_width, display_height))
85
- if writer.isOpened():
86
- used_codec = codec
87
- codec_attempt_log.append({"codec": codec, "status": "success"})
88
- _log(
89
- "info",
90
- "debug_video_codec_selected",
91
- codec=codec,
92
- width=display_width,
93
- height=display_height,
94
- fps=fps,
95
- )
96
- if codec == "mp4v":
97
- msg = (
98
- "Using fallback MPEG-4 codec; will re-encode with ffmpeg for "
99
- "browser compatibility"
100
- )
101
- _log("warning", "debug_video_fallback_codec", codec="mp4v", warning=msg)
102
- break
103
- except Exception as e:
104
- codec_attempt_log.append({"codec": codec, "status": "failed", "error": str(e)})
105
- _log("info", "debug_video_codec_attempt_failed", codec=codec, error=str(e))
106
- continue
107
-
108
- if writer is None or not writer.isOpened():
109
- _log(
110
- "error",
111
- "debug_video_writer_creation_failed",
112
- output_path=output_path,
113
- dimensions=f"{display_width}x{display_height}",
114
- fps=fps,
115
- codec_attempts=codec_attempt_log,
80
+ writer = _try_open_video_writer(
81
+ output_path, codec, fps, display_width, display_height, codec_attempt_log
116
82
  )
117
- raise ValueError(
118
- f"Failed to create video writer for {output_path} with dimensions "
119
- f"{display_width}x{display_height}"
83
+ if writer:
84
+ return writer, needs_resize, codec
85
+
86
+ _log(
87
+ "error",
88
+ "debug_video_writer_creation_failed",
89
+ output_path=output_path,
90
+ dimensions=f"{display_width}x{display_height}",
91
+ fps=fps,
92
+ codec_attempts=codec_attempt_log,
93
+ )
94
+ raise ValueError(
95
+ f"Failed to create video writer for {output_path} with dimensions "
96
+ f"{display_width}x{display_height}"
97
+ )
98
+
99
+
100
+ def _try_open_video_writer(
101
+ output_path: str,
102
+ codec: str,
103
+ fps: float,
104
+ width: int,
105
+ height: int,
106
+ attempt_log: list[dict[str, Any]],
107
+ ) -> cv2.VideoWriter | None:
108
+ """Attempt to open a video writer with a specific codec."""
109
+ try:
110
+ fourcc = cv2.VideoWriter_fourcc(*codec) # type: ignore[attr-defined]
111
+ writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
112
+ if writer.isOpened():
113
+ attempt_log.append({"codec": codec, "status": "success"})
114
+ _log(
115
+ "info",
116
+ "debug_video_codec_selected",
117
+ codec=codec,
118
+ width=width,
119
+ height=height,
120
+ fps=fps,
121
+ )
122
+ if codec == "mp4v":
123
+ msg = (
124
+ "Using fallback MPEG-4 codec; will re-encode with ffmpeg for "
125
+ "browser compatibility"
126
+ )
127
+ _log("warning", "debug_video_fallback_codec", codec="mp4v", warning=msg)
128
+ return writer
129
+
130
+ attempt_log.append(
131
+ {"codec": codec, "status": "failed", "error": "isOpened() returned False"}
120
132
  )
133
+ except Exception as e:
134
+ attempt_log.append({"codec": codec, "status": "failed", "error": str(e)})
135
+ _log("info", "debug_video_codec_attempt_failed", codec=codec, error=str(e))
121
136
 
122
- return writer, needs_resize, used_codec
137
+ return None
123
138
 
124
139
 
125
140
  def write_overlay_frame(
@@ -262,92 +277,17 @@ class BaseDebugOverlayRenderer:
262
277
  codec=self.used_codec,
263
278
  )
264
279
 
265
- # Post-process with ffmpeg ONLY if we fell back to the incompatible mp4v codec
266
- if self.used_codec == "mp4v" and shutil.which("ffmpeg"):
267
- temp_path = None
268
- try:
269
- temp_path = str(
270
- Path(self.output_path).with_suffix(".temp" + Path(self.output_path).suffix)
271
- )
272
-
273
- # Convert to H.264 with yuv420p pixel format for browser compatibility
274
- # -y: Overwrite output file
275
- # -vcodec libx264: Use H.264 codec
276
- # -pix_fmt yuv420p: Required for wide browser support (Chrome,
277
- # Safari, Firefox, iOS)
278
- # -preset fast: Reasonable speed/compression tradeoff
279
- # -crf 23: Standard quality
280
- # -an: Remove audio (debug video has no audio)
281
- cmd = [
282
- "ffmpeg",
283
- "-y",
284
- "-i",
285
- self.output_path,
286
- "-vcodec",
287
- "libx264",
288
- "-pix_fmt",
289
- "yuv420p",
290
- "-preset",
291
- "fast",
292
- "-crf",
293
- "23",
294
- "-an",
295
- temp_path,
296
- ]
297
-
298
- _log(
299
- "info",
300
- "debug_video_ffmpeg_reencoding_start",
301
- input_file=self.output_path,
302
- output_file=temp_path,
303
- output_codec="libx264",
304
- pixel_format="yuv420p",
305
- reason="iOS_compatibility",
306
- )
307
-
308
- # Suppress output unless error
309
- reencode_start = time.time()
310
- subprocess.run(
311
- cmd,
312
- check=True,
313
- stdout=subprocess.DEVNULL,
314
- stderr=subprocess.PIPE,
315
- )
316
- self.reencode_duration_s = time.time() - reencode_start
317
-
318
- _log(
319
- "info",
320
- "debug_video_ffmpeg_reencoding_complete",
321
- duration_ms=round(self.reencode_duration_s * 1000, 1),
322
- )
323
-
324
- # Overwrite original file
325
- os.replace(temp_path, self.output_path)
326
- _log(
327
- "info",
328
- "debug_video_reencoded_file_replaced",
329
- output_path=self.output_path,
330
- final_codec="libx264",
331
- pixel_format="yuv420p",
332
- )
280
+ if self.used_codec != "mp4v":
281
+ _log(
282
+ "info",
283
+ "debug_video_ready_for_playback",
284
+ codec=self.used_codec,
285
+ path=self.output_path,
286
+ )
287
+ return
333
288
 
334
- except subprocess.CalledProcessError as e:
335
- stderr_msg = e.stderr.decode("utf-8", errors="ignore") if e.stderr else "N/A"
336
- _log(
337
- "warning",
338
- "debug_video_ffmpeg_reencoding_failed",
339
- error=str(e),
340
- stderr=stderr_msg,
341
- )
342
- if temp_path and os.path.exists(temp_path):
343
- os.remove(temp_path)
344
- _log("info", "debug_video_temp_file_cleaned_up", temp_file=temp_path)
345
- except Exception as e:
346
- _log("warning", "debug_video_post_processing_error", error=str(e))
347
- if temp_path and os.path.exists(temp_path):
348
- os.remove(temp_path)
349
- _log("info", "debug_video_temp_file_cleaned_up", temp_file=temp_path)
350
- elif self.used_codec == "mp4v" and not shutil.which("ffmpeg"):
289
+ ffmpeg_path = shutil.which("ffmpeg")
290
+ if not ffmpeg_path:
351
291
  _log(
352
292
  "warning",
353
293
  "debug_video_ffmpeg_not_available",
@@ -355,13 +295,88 @@ class BaseDebugOverlayRenderer:
355
295
  output_path=self.output_path,
356
296
  warning="Video may not play in all browsers",
357
297
  )
358
- else:
298
+ return
299
+
300
+ self._reencode_to_h264()
301
+
302
+ def _reencode_to_h264(self) -> None:
303
+ """Re-encode video to H.264 for browser compatibility using ffmpeg."""
304
+ temp_path = str(
305
+ Path(self.output_path).with_suffix(".temp" + Path(self.output_path).suffix)
306
+ )
307
+
308
+ # Convert to H.264 with yuv420p pixel format for browser compatibility
309
+ # -y: Overwrite output file
310
+ # -vcodec libx264: Use H.264 codec
311
+ # -pix_fmt yuv420p: Required for wide browser support (Chrome, Safari, Firefox, iOS)
312
+ # -preset fast: Reasonable speed/compression tradeoff
313
+ # -crf 23: Standard quality
314
+ # -an: Remove audio (debug video has no audio)
315
+ cmd = [
316
+ "ffmpeg",
317
+ "-y",
318
+ "-i",
319
+ self.output_path,
320
+ "-vcodec",
321
+ "libx264",
322
+ "-pix_fmt",
323
+ "yuv420p",
324
+ "-preset",
325
+ "fast",
326
+ "-crf",
327
+ "23",
328
+ "-an",
329
+ temp_path,
330
+ ]
331
+
332
+ _log(
333
+ "info",
334
+ "debug_video_ffmpeg_reencoding_start",
335
+ input_file=self.output_path,
336
+ output_file=temp_path,
337
+ output_codec="libx264",
338
+ pixel_format="yuv420p",
339
+ reason="iOS_compatibility",
340
+ )
341
+
342
+ try:
343
+ reencode_start = time.time()
344
+ subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
345
+ self.reencode_duration_s = time.time() - reencode_start
346
+
359
347
  _log(
360
348
  "info",
361
- "debug_video_ready_for_playback",
362
- codec=self.used_codec,
363
- path=self.output_path,
349
+ "debug_video_ffmpeg_reencoding_complete",
350
+ duration_ms=round(self.reencode_duration_s * 1000, 1),
351
+ )
352
+
353
+ os.replace(temp_path, self.output_path)
354
+ _log(
355
+ "info",
356
+ "debug_video_reencoded_file_replaced",
357
+ output_path=self.output_path,
358
+ final_codec="libx264",
359
+ pixel_format="yuv420p",
364
360
  )
361
+ except (subprocess.CalledProcessError, Exception) as e:
362
+ self._handle_reencode_error(e, temp_path)
363
+
364
+ def _handle_reencode_error(self, e: Exception, temp_path: str) -> None:
365
+ """Handle errors during ffmpeg re-encoding."""
366
+ if isinstance(e, subprocess.CalledProcessError):
367
+ stderr_msg = e.stderr.decode("utf-8", errors="ignore") if e.stderr else "N/A"
368
+ _log(
369
+ "warning",
370
+ "debug_video_ffmpeg_reencoding_failed",
371
+ error=str(e),
372
+ stderr=stderr_msg,
373
+ )
374
+ else:
375
+ _log("warning", "debug_video_post_processing_error", error=str(e))
376
+
377
+ if os.path.exists(temp_path):
378
+ os.remove(temp_path)
379
+ _log("info", "debug_video_temp_file_cleaned_up", temp_file=temp_path)
365
380
 
366
381
  def __enter__(self) -> Self:
367
382
  return self
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinemotion
3
- Version: 0.66.3
3
+ Version: 0.66.4
4
4
  Summary: Video-based kinematic analysis for athletic performance
5
5
  Project-URL: Homepage, https://github.com/feniix/kinemotion
6
6
  Project-URL: Repository, https://github.com/feniix/kinemotion
@@ -3,7 +3,7 @@ kinemotion/api.py,sha256=uG1e4bTnj2c-6cbZJEZ_LjMwFdaG32ba2KcK_XjE_NI,1040
3
3
  kinemotion/cli.py,sha256=cqYV_7URH0JUDy1VQ_EDLv63FmNO4Ns20m6s1XAjiP4,464
4
4
  kinemotion/cmj/__init__.py,sha256=SkAw9ka8Yd1Qfv9hcvk22m3EfucROzYrSNGNF5kDzho,113
5
5
  kinemotion/cmj/analysis.py,sha256=jM9ZX44h1__Cg2iIhAYRoo_5fPwIOeV5Q2FZ22rMvKY,22202
6
- kinemotion/cmj/api.py,sha256=Xu6PepQKQFzRWZMESaWvHyPV-dueCH1TQQeQdil1KiQ,16626
6
+ kinemotion/cmj/api.py,sha256=DkrA6xgSp43g2xpT8ai32BvMGo3Bmj23upxhOYGlpXY,18284
7
7
  kinemotion/cmj/cli.py,sha256=P2b77IIw6kqTSIkncxlShzhmjIwqMFBNd-pZxYP-TsI,9918
8
8
  kinemotion/cmj/debug_overlay.py,sha256=bX9aPLhXiLCCMZW9v8Y4OiOAaZO0i-UGr-Pl8HCsmbI,15810
9
9
  kinemotion/cmj/joint_angles.py,sha256=HmheIEiKcQz39cRezk4h-htorOhGNPsqKIR9RsAEKts,9960
@@ -13,7 +13,7 @@ kinemotion/cmj/validation_bounds.py,sha256=Ry915JdInPXbqjaVGNY_urnDO1PAkCSJqHwNK
13
13
  kinemotion/core/__init__.py,sha256=U2fnLUGXQ0jbwpXhdksYKDXbeQndEHjn9gwTAEJ9Av0,1451
14
14
  kinemotion/core/auto_tuning.py,sha256=lhAqPc-eLjMYx9BCvKdECE7TD2Dweb9KcifV6JHaXOE,11278
15
15
  kinemotion/core/cli_utils.py,sha256=sQPbT6XWWau-sm9yuN5c3eS5xNzoQGGXwSz6hQXtRvM,1859
16
- kinemotion/core/debug_overlay_utils.py,sha256=jE_7UOJzDyCYXEcqUHMvJmmHaQ6jE0dV3JWltiQEMro,13377
16
+ kinemotion/core/debug_overlay_utils.py,sha256=85KI5TZef1e_6NU083nPHeVMx9rCE3TAdAjO6_5kBUQ,13066
17
17
  kinemotion/core/determinism.py,sha256=Frw-KAOvAxTL_XtxoWpXCjMbQPUKEAusK6JctlkeuRo,2509
18
18
  kinemotion/core/experimental.py,sha256=IK05AF4aZS15ke85hF3TWCqRIXU1AlD_XKzFz735Ua8,3640
19
19
  kinemotion/core/filtering.py,sha256=Oc__pV6iHEGyyovbqa5SUi-6v8QyvaRVwA0LRayM884,11355
@@ -36,8 +36,8 @@ kinemotion/dropjump/kinematics.py,sha256=dx4PuXKfKMKcsc_HX6sXj8rHXf9ksiZIOAIkJ4v
36
36
  kinemotion/dropjump/metrics_validator.py,sha256=lSfo4Lm5FHccl8ijUP6SA-kcSh50LS9hF8UIyWxcnW8,9243
37
37
  kinemotion/dropjump/validation_bounds.py,sha256=x4yjcFxyvdMp5e7MkcoUosGLeGsxBh1Lft6h__AQ2G8,5124
38
38
  kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
- kinemotion-0.66.3.dist-info/METADATA,sha256=Cqnur5nD2r0vTMzdzQtknsnCBIxO9ePxODFJtGhFids,26061
40
- kinemotion-0.66.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
41
- kinemotion-0.66.3.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
42
- kinemotion-0.66.3.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
43
- kinemotion-0.66.3.dist-info/RECORD,,
39
+ kinemotion-0.66.4.dist-info/METADATA,sha256=cwiaYXl2fLT-NpzE455l3Xe5oGTxl27clpbsQstpHW4,26061
40
+ kinemotion-0.66.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
41
+ kinemotion-0.66.4.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
42
+ kinemotion-0.66.4.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
43
+ kinemotion-0.66.4.dist-info/RECORD,,