kinemotion 0.20.1__tar.gz → 0.21.0__tar.gz

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.

Files changed (89) hide show
  1. {kinemotion-0.20.1 → kinemotion-0.21.0}/.pre-commit-config.yaml +2 -2
  2. {kinemotion-0.20.1 → kinemotion-0.21.0}/CHANGELOG.md +16 -0
  3. {kinemotion-0.20.1 → kinemotion-0.21.0}/CLAUDE.md +66 -9
  4. {kinemotion-0.20.1 → kinemotion-0.21.0}/PKG-INFO +1 -1
  5. {kinemotion-0.20.1 → kinemotion-0.21.0}/pyproject.toml +5 -2
  6. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/cmj/kinematics.py +25 -4
  7. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/core/smoothing.py +19 -12
  8. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/dropjump/kinematics.py +27 -5
  9. kinemotion-0.21.0/tests/test_aspect_ratio.py +341 -0
  10. {kinemotion-0.20.1 → kinemotion-0.21.0}/uv.lock +356 -314
  11. kinemotion-0.20.1/tests/test_aspect_ratio.py +0 -138
  12. {kinemotion-0.20.1 → kinemotion-0.21.0}/.dockerignore +0 -0
  13. {kinemotion-0.20.1 → kinemotion-0.21.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  14. {kinemotion-0.20.1 → kinemotion-0.21.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  15. {kinemotion-0.20.1 → kinemotion-0.21.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  16. {kinemotion-0.20.1 → kinemotion-0.21.0}/.github/pull_request_template.md +0 -0
  17. {kinemotion-0.20.1 → kinemotion-0.21.0}/.github/workflows/docs.yml +0 -0
  18. {kinemotion-0.20.1 → kinemotion-0.21.0}/.github/workflows/release.yml +0 -0
  19. {kinemotion-0.20.1 → kinemotion-0.21.0}/.github/workflows/test.yml +0 -0
  20. {kinemotion-0.20.1 → kinemotion-0.21.0}/.gitignore +0 -0
  21. {kinemotion-0.20.1 → kinemotion-0.21.0}/.readthedocs.yml +0 -0
  22. {kinemotion-0.20.1 → kinemotion-0.21.0}/.tool-versions +0 -0
  23. {kinemotion-0.20.1 → kinemotion-0.21.0}/CODE_OF_CONDUCT.md +0 -0
  24. {kinemotion-0.20.1 → kinemotion-0.21.0}/CONTRIBUTING.md +0 -0
  25. {kinemotion-0.20.1 → kinemotion-0.21.0}/Dockerfile +0 -0
  26. {kinemotion-0.20.1 → kinemotion-0.21.0}/GEMINI.md +0 -0
  27. {kinemotion-0.20.1 → kinemotion-0.21.0}/LICENSE +0 -0
  28. {kinemotion-0.20.1 → kinemotion-0.21.0}/README.md +0 -0
  29. {kinemotion-0.20.1 → kinemotion-0.21.0}/SECURITY.md +0 -0
  30. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/README.md +0 -0
  31. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/api/cmj.md +0 -0
  32. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/api/core.md +0 -0
  33. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/api/dropjump.md +0 -0
  34. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/api/overview.md +0 -0
  35. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/development/errors-findings.md +0 -0
  36. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/development/validation-plan.md +0 -0
  37. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/development/wallball-norep-detection.md +0 -0
  38. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/guides/bulk-processing.md +0 -0
  39. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/guides/camera-setup.md +0 -0
  40. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/guides/cmj-guide.md +0 -0
  41. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/index.md +0 -0
  42. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/reference/parameters.md +0 -0
  43. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/reference/pose-systems.md +0 -0
  44. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/research/sports-biomechanics-pose-estimation.md +0 -0
  45. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/technical/framerate.md +0 -0
  46. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/technical/imu-metadata.md +0 -0
  47. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/technical/real-time-analysis.md +0 -0
  48. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/technical/triple-extension.md +0 -0
  49. {kinemotion-0.20.1 → kinemotion-0.21.0}/docs/translations/es/camera-setup.md +0 -0
  50. {kinemotion-0.20.1 → kinemotion-0.21.0}/examples/bulk/README.md +0 -0
  51. {kinemotion-0.20.1 → kinemotion-0.21.0}/examples/bulk/bulk_processing.py +0 -0
  52. {kinemotion-0.20.1 → kinemotion-0.21.0}/examples/bulk/simple_example.py +0 -0
  53. {kinemotion-0.20.1 → kinemotion-0.21.0}/examples/programmatic_usage.py +0 -0
  54. {kinemotion-0.20.1 → kinemotion-0.21.0}/mkdocs.yml +0 -0
  55. {kinemotion-0.20.1 → kinemotion-0.21.0}/requirements-docs.txt +0 -0
  56. {kinemotion-0.20.1 → kinemotion-0.21.0}/samples/cmjs/README.md +0 -0
  57. {kinemotion-0.20.1 → kinemotion-0.21.0}/sonar-project.properties +0 -0
  58. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/__init__.py +0 -0
  59. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/api.py +0 -0
  60. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/cli.py +0 -0
  61. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/cmj/__init__.py +0 -0
  62. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/cmj/analysis.py +0 -0
  63. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/cmj/cli.py +0 -0
  64. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/cmj/debug_overlay.py +0 -0
  65. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/cmj/joint_angles.py +0 -0
  66. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/core/__init__.py +0 -0
  67. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/core/auto_tuning.py +0 -0
  68. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/core/cli_utils.py +0 -0
  69. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/core/debug_overlay_utils.py +0 -0
  70. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/core/filtering.py +0 -0
  71. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/core/pose.py +0 -0
  72. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/core/video_io.py +0 -0
  73. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/dropjump/__init__.py +0 -0
  74. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/dropjump/analysis.py +0 -0
  75. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/dropjump/cli.py +0 -0
  76. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/dropjump/debug_overlay.py +0 -0
  77. {kinemotion-0.20.1 → kinemotion-0.21.0}/src/kinemotion/py.typed +0 -0
  78. {kinemotion-0.20.1 → kinemotion-0.21.0}/tests/__init__.py +0 -0
  79. {kinemotion-0.20.1 → kinemotion-0.21.0}/tests/test_adaptive_threshold.py +0 -0
  80. {kinemotion-0.20.1 → kinemotion-0.21.0}/tests/test_api.py +0 -0
  81. {kinemotion-0.20.1 → kinemotion-0.21.0}/tests/test_cli_imports.py +0 -0
  82. {kinemotion-0.20.1 → kinemotion-0.21.0}/tests/test_cmj_analysis.py +0 -0
  83. {kinemotion-0.20.1 → kinemotion-0.21.0}/tests/test_cmj_kinematics.py +0 -0
  84. {kinemotion-0.20.1 → kinemotion-0.21.0}/tests/test_com_estimation.py +0 -0
  85. {kinemotion-0.20.1 → kinemotion-0.21.0}/tests/test_contact_detection.py +0 -0
  86. {kinemotion-0.20.1 → kinemotion-0.21.0}/tests/test_filtering.py +0 -0
  87. {kinemotion-0.20.1 → kinemotion-0.21.0}/tests/test_joint_angles.py +0 -0
  88. {kinemotion-0.20.1 → kinemotion-0.21.0}/tests/test_kinematics.py +0 -0
  89. {kinemotion-0.20.1 → kinemotion-0.21.0}/tests/test_polyorder.py +0 -0
@@ -15,12 +15,12 @@ repos:
15
15
  - id: mixed-line-ending
16
16
 
17
17
  - repo: https://github.com/psf/black
18
- rev: 25.9.0
18
+ rev: 25.11.0
19
19
  hooks:
20
20
  - id: black
21
21
 
22
22
  - repo: https://github.com/astral-sh/ruff-pre-commit
23
- rev: v0.14.3
23
+ rev: v0.14.4
24
24
  hooks:
25
25
  - id: ruff
26
26
  args: [--fix, --exit-non-zero-on-fix]
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  <!-- version list -->
9
9
 
10
+ ## v0.21.0 (2025-11-10)
11
+
12
+ ### Features
13
+
14
+ - Add TypedDict and type aliases for improved type safety
15
+ ([`053e010`](https://github.com/feniix/kinemotion/commit/053e010cf80e1c91d5900c39d49b1d7ac2ac6ab4))
16
+
17
+
18
+ ## v0.20.2 (2025-11-10)
19
+
20
+ ### Bug Fixes
21
+
22
+ - Achieve 80%+ coverage on video_io for SonarCloud quality gate
23
+ ([`ed77fdb`](https://github.com/feniix/kinemotion/commit/ed77fdb080f143c492c724c9f4a138b2a364ad7e))
24
+
25
+
10
26
  ## v0.20.1 (2025-11-10)
11
27
 
12
28
  ### Bug Fixes
@@ -19,7 +19,7 @@ uv run kinemotion cmj-analyze video.mp4
19
19
 
20
20
  **Development:**
21
21
  ```bash
22
- uv run pytest # Run all 75 tests with coverage (50.75%)
22
+ uv run pytest # Run all 146 tests with coverage (57.57%)
23
23
  uv run pytest --cov-report=html # Generate HTML coverage report
24
24
  uv run ruff check --fix && uv run pyright # Lint + type check
25
25
  ```
@@ -64,7 +64,7 @@ src/kinemotion/
64
64
  ├── dropjump/ # Drop jump: cli, analysis, kinematics, debug_overlay
65
65
  └── cmj/ # CMJ: cli, analysis, kinematics, joint_angles, debug_overlay
66
66
 
67
- tests/ # 75 tests total (61 drop jump, 9 CMJ, 5 CLI import)
67
+ tests/ # 146 tests total (comprehensive coverage across all modules)
68
68
  docs/ # CMJ_GUIDE, TRIPLE_EXTENSION, REAL_TIME_ANALYSIS, etc.
69
69
  ```
70
70
 
@@ -168,7 +168,7 @@ OpenCV vs NumPy ordering:
168
168
  ```bash
169
169
  uv run ruff check --fix # Auto-fix linting
170
170
  uv run pyright # Type check (strict)
171
- uv run pytest # All 75 tests with coverage
171
+ uv run pytest # All 146 tests with coverage
172
172
  ```
173
173
 
174
174
  ### Standards
@@ -177,22 +177,22 @@ uv run pytest # All 75 tests with coverage
177
177
  - Ruff (100 char lines)
178
178
  - Conventional Commits (see below)
179
179
  - **Code duplication target: < 3%**
180
- - **Test coverage: ≥ 50% (current: 50.75% with branch coverage)**
180
+ - **Test coverage: ≥ 50% (current: 57.57% with branch coverage)**
181
181
 
182
182
  ### Coverage Analysis
183
183
 
184
- Current coverage: **50.75%** (2184 statements, 752 branches)
184
+ Current coverage: **57.57%** (2225 statements, 752 branches)
185
185
 
186
186
  **Well-tested modules (>80%):**
187
187
  - Core filtering: 87.80%
188
188
  - Core pose tracking: 88.46%
189
- - Drop jump kinematics: 83.94%
190
- - CMJ kinematics: 94.67%
189
+ - Drop jump kinematics: 85.71%
190
+ - CMJ kinematics: 95.65%
191
+ - CMJ joint angles: 100.00%
191
192
 
192
- **Needs improvement (<30%):**
193
+ **Expected lower coverage (<30%):**
193
194
  - CLI modules: 22-23% (expected - minimal integration tests)
194
195
  - Debug overlays: 10-36% (visualization code)
195
- - Joint angles: 6.20% (feature module)
196
196
 
197
197
  View detailed report: `uv run pytest --cov-report=html && open htmlcov/index.html`
198
198
 
@@ -268,6 +268,63 @@ metrics = process_cmj_video("video.mp4", quality="balanced")
268
268
  1. Convert NumPy types in `to_dict()`: `int()`, `float()`
269
269
  2. Type all functions (pyright strict)
270
270
  3. Handle None in optional fields
271
+ 4. Use TypedDict for dictionary returns
272
+ 5. Use type aliases for complex nested types
273
+
274
+ ### Modern Type Hints (NumPy 2.x)
275
+
276
+ The project uses NumPy 2.x-compatible type hints:
277
+
278
+ **TypedDict for type-safe dictionaries:**
279
+ ```python
280
+ from typing import TypedDict
281
+
282
+ class DropJumpMetricsDict(TypedDict, total=False):
283
+ """Type-safe dictionary for drop jump metrics JSON output."""
284
+ ground_contact_time_ms: float | None
285
+ flight_time_ms: float | None
286
+ # ... IDE autocomplete and type checking!
287
+ ```
288
+
289
+ **Type aliases for cleaner signatures:**
290
+ ```python
291
+ from typing import TypeAlias
292
+
293
+ LandmarkCoord: TypeAlias = tuple[float, float, float] # (x, y, visibility)
294
+ LandmarkFrame: TypeAlias = dict[str, LandmarkCoord] | None
295
+ LandmarkSequence: TypeAlias = list[LandmarkFrame]
296
+
297
+ def smooth_landmarks(landmark_sequence: LandmarkSequence) -> LandmarkSequence:
298
+ # Much cleaner than list[dict[str, tuple[float, float, float]] | None]!
299
+ ```
300
+
301
+ **NDArray with dtype specificity:**
302
+ ```python
303
+ from numpy.typing import NDArray
304
+
305
+ def calculate_metrics(
306
+ positions: NDArray[np.float64], # Explicit dtype for arrays
307
+ velocities: NDArray[np.float64],
308
+ ) -> CMJMetrics:
309
+ ...
310
+ ```
311
+
312
+ ### Dependencies
313
+
314
+ **Key versions:**
315
+ - Python: 3.12.7
316
+ - NumPy: 2.3.4 (benefits: better type hints, improved SciPy compatibility)
317
+ - pytest: 9.0.0 (features: per-test timing, strict mode)
318
+ - MediaPipe: 0.10.14
319
+
320
+ **Pytest 9 configuration** (`pyproject.toml`):
321
+ ```toml
322
+ [tool.pytest]
323
+ minversion = "9.0"
324
+ testpaths = ["tests"]
325
+ console_output_style = "times" # Shows execution time per test
326
+ strict = true # Enables all strictness options
327
+ ```
271
328
 
272
329
  ## Documentation
273
330
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinemotion
3
- Version: 0.20.1
3
+ Version: 0.21.0
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kinemotion"
3
- version = "0.20.1"
3
+ version = "0.21.0"
4
4
  description = "Video-based kinematic analysis for athletic performance"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10,<3.13"
@@ -62,8 +62,11 @@ dev = [
62
62
  ]
63
63
 
64
64
 
65
- [tool.pytest.ini_options]
65
+ [tool.pytest]
66
+ minversion = "9.0"
66
67
  testpaths = ["tests"]
68
+ console_output_style = "times"
69
+ strict = true
67
70
  addopts = [
68
71
  "--cov=src/kinemotion",
69
72
  "--cov-report=term-missing",
@@ -1,9 +1,30 @@
1
1
  """Counter Movement Jump (CMJ) metrics calculation."""
2
2
 
3
3
  from dataclasses import dataclass
4
- from typing import Any
4
+ from typing import TypedDict
5
5
 
6
6
  import numpy as np
7
+ from numpy.typing import NDArray
8
+
9
+
10
+ class CMJMetricsDict(TypedDict, total=False):
11
+ """Type-safe dictionary for CMJ metrics JSON output."""
12
+
13
+ jump_height_m: float
14
+ flight_time_s: float
15
+ countermovement_depth_m: float
16
+ eccentric_duration_s: float
17
+ concentric_duration_s: float
18
+ total_movement_time_s: float
19
+ peak_eccentric_velocity_m_s: float
20
+ peak_concentric_velocity_m_s: float
21
+ transition_time_s: float | None
22
+ standing_start_frame: float | None
23
+ lowest_point_frame: float
24
+ takeoff_frame: float
25
+ landing_frame: float
26
+ video_fps: float
27
+ tracking_method: str
7
28
 
8
29
 
9
30
  @dataclass
@@ -44,7 +65,7 @@ class CMJMetrics:
44
65
  video_fps: float
45
66
  tracking_method: str
46
67
 
47
- def to_dict(self) -> dict[str, Any]:
68
+ def to_dict(self) -> CMJMetricsDict:
48
69
  """Convert metrics to JSON-serializable dictionary.
49
70
 
50
71
  Returns:
@@ -78,8 +99,8 @@ class CMJMetrics:
78
99
 
79
100
 
80
101
  def calculate_cmj_metrics(
81
- positions: np.ndarray,
82
- velocities: np.ndarray,
102
+ positions: NDArray[np.float64],
103
+ velocities: NDArray[np.float64],
83
104
  standing_start_frame: float | None,
84
105
  lowest_point_frame: float,
85
106
  takeoff_frame: float,
@@ -1,5 +1,7 @@
1
1
  """Landmark smoothing utilities to reduce jitter in pose tracking."""
2
2
 
3
+ from typing import TypeAlias
4
+
3
5
  import numpy as np
4
6
  from scipy.signal import savgol_filter
5
7
 
@@ -8,9 +10,14 @@ from .filtering import (
8
10
  reject_outliers,
9
11
  )
10
12
 
13
+ # Type aliases for landmark data structures
14
+ LandmarkCoord: TypeAlias = tuple[float, float, float] # (x, y, visibility)
15
+ LandmarkFrame: TypeAlias = dict[str, LandmarkCoord] | None
16
+ LandmarkSequence: TypeAlias = list[LandmarkFrame]
17
+
11
18
 
12
19
  def _extract_landmark_coordinates(
13
- landmark_sequence: list[dict[str, tuple[float, float, float]] | None],
20
+ landmark_sequence: LandmarkSequence,
14
21
  landmark_name: str,
15
22
  ) -> tuple[list[float], list[float], list[int]]:
16
23
  """
@@ -38,7 +45,7 @@ def _extract_landmark_coordinates(
38
45
 
39
46
 
40
47
  def _get_landmark_names(
41
- landmark_sequence: list[dict[str, tuple[float, float, float]] | None],
48
+ landmark_sequence: LandmarkSequence,
42
49
  ) -> list[str] | None:
43
50
  """
44
51
  Extract landmark names from first valid frame.
@@ -56,8 +63,8 @@ def _get_landmark_names(
56
63
 
57
64
 
58
65
  def _fill_missing_frames(
59
- smoothed_sequence: list[dict[str, tuple[float, float, float]] | None],
60
- landmark_sequence: list[dict[str, tuple[float, float, float]] | None],
66
+ smoothed_sequence: LandmarkSequence,
67
+ landmark_sequence: LandmarkSequence,
61
68
  ) -> None:
62
69
  """
63
70
  Fill in any missing frames in smoothed sequence with original data.
@@ -75,8 +82,8 @@ def _fill_missing_frames(
75
82
 
76
83
 
77
84
  def _store_smoothed_landmarks(
78
- smoothed_sequence: list[dict[str, tuple[float, float, float]] | None],
79
- landmark_sequence: list[dict[str, tuple[float, float, float]] | None],
85
+ smoothed_sequence: LandmarkSequence,
86
+ landmark_sequence: LandmarkSequence,
80
87
  landmark_name: str,
81
88
  x_smooth: np.ndarray,
82
89
  y_smooth: np.ndarray,
@@ -118,11 +125,11 @@ def _store_smoothed_landmarks(
118
125
 
119
126
 
120
127
  def _smooth_landmarks_core( # NOSONAR(S1172) - polyorder used via closure capture in smoother_fn
121
- landmark_sequence: list[dict[str, tuple[float, float, float]] | None],
128
+ landmark_sequence: LandmarkSequence,
122
129
  window_length: int,
123
130
  polyorder: int,
124
131
  smoother_fn, # type: ignore[no-untyped-def]
125
- ) -> list[dict[str, tuple[float, float, float]] | None]:
132
+ ) -> LandmarkSequence:
126
133
  """
127
134
  Core smoothing logic shared by both standard and advanced smoothing.
128
135
 
@@ -170,10 +177,10 @@ def _smooth_landmarks_core( # NOSONAR(S1172) - polyorder used via closure captu
170
177
 
171
178
 
172
179
  def smooth_landmarks(
173
- landmark_sequence: list[dict[str, tuple[float, float, float]] | None],
180
+ landmark_sequence: LandmarkSequence,
174
181
  window_length: int = 5,
175
182
  polyorder: int = 2,
176
- ) -> list[dict[str, tuple[float, float, float]] | None]:
183
+ ) -> LandmarkSequence:
177
184
  """
178
185
  Smooth landmark trajectories using Savitzky-Golay filter.
179
186
 
@@ -330,7 +337,7 @@ def compute_acceleration_from_derivative(
330
337
 
331
338
 
332
339
  def smooth_landmarks_advanced(
333
- landmark_sequence: list[dict[str, tuple[float, float, float]] | None],
340
+ landmark_sequence: LandmarkSequence,
334
341
  window_length: int = 5,
335
342
  polyorder: int = 2,
336
343
  use_outlier_rejection: bool = True,
@@ -338,7 +345,7 @@ def smooth_landmarks_advanced(
338
345
  ransac_threshold: float = 0.02,
339
346
  bilateral_sigma_spatial: float = 3.0,
340
347
  bilateral_sigma_intensity: float = 0.02,
341
- ) -> list[dict[str, tuple[float, float, float]] | None]:
348
+ ) -> LandmarkSequence:
342
349
  """
343
350
  Advanced landmark smoothing with outlier rejection and bilateral filtering.
344
351
 
@@ -1,6 +1,9 @@
1
1
  """Kinematic calculations for drop-jump metrics."""
2
2
 
3
+ from typing import TypedDict
4
+
3
5
  import numpy as np
6
+ from numpy.typing import NDArray
4
7
 
5
8
  from ..core.smoothing import compute_acceleration_from_derivative
6
9
  from .analysis import (
@@ -12,6 +15,25 @@ from .analysis import (
12
15
  )
13
16
 
14
17
 
18
+ class DropJumpMetricsDict(TypedDict, total=False):
19
+ """Type-safe dictionary for drop jump metrics JSON output."""
20
+
21
+ ground_contact_time_ms: float | None
22
+ flight_time_ms: float | None
23
+ jump_height_m: float | None
24
+ jump_height_kinematic_m: float | None
25
+ jump_height_trajectory_normalized: float | None
26
+ contact_start_frame: int | None
27
+ contact_end_frame: int | None
28
+ flight_start_frame: int | None
29
+ flight_end_frame: int | None
30
+ peak_height_frame: int | None
31
+ contact_start_frame_precise: float | None
32
+ contact_end_frame_precise: float | None
33
+ flight_start_frame_precise: float | None
34
+ flight_end_frame_precise: float | None
35
+
36
+
15
37
  class DropJumpMetrics:
16
38
  """Container for drop-jump analysis metrics."""
17
39
 
@@ -32,7 +54,7 @@ class DropJumpMetrics:
32
54
  self.flight_start_frame_precise: float | None = None
33
55
  self.flight_end_frame_precise: float | None = None
34
56
 
35
- def to_dict(self) -> dict:
57
+ def to_dict(self) -> DropJumpMetricsDict:
36
58
  """Convert metrics to dictionary for JSON output."""
37
59
  return {
38
60
  "ground_contact_time_ms": (
@@ -108,7 +130,7 @@ class DropJumpMetrics:
108
130
 
109
131
  def _determine_drop_start_frame(
110
132
  drop_start_frame: int | None,
111
- foot_y_positions: np.ndarray,
133
+ foot_y_positions: NDArray[np.float64],
112
134
  fps: float,
113
135
  smoothing_window: int,
114
136
  ) -> int:
@@ -170,7 +192,7 @@ def _identify_main_contact_phase(
170
192
  phases: list[tuple[int, int, ContactState]],
171
193
  ground_phases: list[tuple[int, int, int]],
172
194
  air_phases_indexed: list[tuple[int, int, int]],
173
- foot_y_positions: np.ndarray,
195
+ foot_y_positions: NDArray[np.float64],
174
196
  ) -> tuple[int, int, bool]:
175
197
  """Identify the main contact phase and determine if it's a drop jump.
176
198
 
@@ -260,7 +282,7 @@ def _analyze_flight_phase(
260
282
  phases: list[tuple[int, int, ContactState]],
261
283
  interpolated_phases: list[tuple[float, float, ContactState]],
262
284
  contact_end: int,
263
- foot_y_positions: np.ndarray,
285
+ foot_y_positions: NDArray[np.float64],
264
286
  fps: float,
265
287
  smoothing_window: int,
266
288
  polyorder: int,
@@ -341,7 +363,7 @@ def _analyze_flight_phase(
341
363
 
342
364
  def calculate_drop_jump_metrics(
343
365
  contact_states: list[ContactState],
344
- foot_y_positions: np.ndarray,
366
+ foot_y_positions: NDArray[np.float64],
345
367
  fps: float,
346
368
  drop_start_frame: int | None = None,
347
369
  velocity_threshold: float = 0.02,