kinemotion 0.20.1__tar.gz → 0.20.2__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.20.2}/CHANGELOG.md +8 -0
  2. {kinemotion-0.20.1 → kinemotion-0.20.2}/PKG-INFO +1 -1
  3. {kinemotion-0.20.1 → kinemotion-0.20.2}/pyproject.toml +1 -1
  4. kinemotion-0.20.2/tests/test_aspect_ratio.py +341 -0
  5. {kinemotion-0.20.1 → kinemotion-0.20.2}/uv.lock +1 -1
  6. kinemotion-0.20.1/tests/test_aspect_ratio.py +0 -138
  7. {kinemotion-0.20.1 → kinemotion-0.20.2}/.dockerignore +0 -0
  8. {kinemotion-0.20.1 → kinemotion-0.20.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  9. {kinemotion-0.20.1 → kinemotion-0.20.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  10. {kinemotion-0.20.1 → kinemotion-0.20.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  11. {kinemotion-0.20.1 → kinemotion-0.20.2}/.github/pull_request_template.md +0 -0
  12. {kinemotion-0.20.1 → kinemotion-0.20.2}/.github/workflows/docs.yml +0 -0
  13. {kinemotion-0.20.1 → kinemotion-0.20.2}/.github/workflows/release.yml +0 -0
  14. {kinemotion-0.20.1 → kinemotion-0.20.2}/.github/workflows/test.yml +0 -0
  15. {kinemotion-0.20.1 → kinemotion-0.20.2}/.gitignore +0 -0
  16. {kinemotion-0.20.1 → kinemotion-0.20.2}/.pre-commit-config.yaml +0 -0
  17. {kinemotion-0.20.1 → kinemotion-0.20.2}/.readthedocs.yml +0 -0
  18. {kinemotion-0.20.1 → kinemotion-0.20.2}/.tool-versions +0 -0
  19. {kinemotion-0.20.1 → kinemotion-0.20.2}/CLAUDE.md +0 -0
  20. {kinemotion-0.20.1 → kinemotion-0.20.2}/CODE_OF_CONDUCT.md +0 -0
  21. {kinemotion-0.20.1 → kinemotion-0.20.2}/CONTRIBUTING.md +0 -0
  22. {kinemotion-0.20.1 → kinemotion-0.20.2}/Dockerfile +0 -0
  23. {kinemotion-0.20.1 → kinemotion-0.20.2}/GEMINI.md +0 -0
  24. {kinemotion-0.20.1 → kinemotion-0.20.2}/LICENSE +0 -0
  25. {kinemotion-0.20.1 → kinemotion-0.20.2}/README.md +0 -0
  26. {kinemotion-0.20.1 → kinemotion-0.20.2}/SECURITY.md +0 -0
  27. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/README.md +0 -0
  28. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/api/cmj.md +0 -0
  29. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/api/core.md +0 -0
  30. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/api/dropjump.md +0 -0
  31. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/api/overview.md +0 -0
  32. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/development/errors-findings.md +0 -0
  33. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/development/validation-plan.md +0 -0
  34. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/development/wallball-norep-detection.md +0 -0
  35. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/guides/bulk-processing.md +0 -0
  36. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/guides/camera-setup.md +0 -0
  37. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/guides/cmj-guide.md +0 -0
  38. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/index.md +0 -0
  39. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/reference/parameters.md +0 -0
  40. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/reference/pose-systems.md +0 -0
  41. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/research/sports-biomechanics-pose-estimation.md +0 -0
  42. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/technical/framerate.md +0 -0
  43. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/technical/imu-metadata.md +0 -0
  44. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/technical/real-time-analysis.md +0 -0
  45. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/technical/triple-extension.md +0 -0
  46. {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/translations/es/camera-setup.md +0 -0
  47. {kinemotion-0.20.1 → kinemotion-0.20.2}/examples/bulk/README.md +0 -0
  48. {kinemotion-0.20.1 → kinemotion-0.20.2}/examples/bulk/bulk_processing.py +0 -0
  49. {kinemotion-0.20.1 → kinemotion-0.20.2}/examples/bulk/simple_example.py +0 -0
  50. {kinemotion-0.20.1 → kinemotion-0.20.2}/examples/programmatic_usage.py +0 -0
  51. {kinemotion-0.20.1 → kinemotion-0.20.2}/mkdocs.yml +0 -0
  52. {kinemotion-0.20.1 → kinemotion-0.20.2}/requirements-docs.txt +0 -0
  53. {kinemotion-0.20.1 → kinemotion-0.20.2}/samples/cmjs/README.md +0 -0
  54. {kinemotion-0.20.1 → kinemotion-0.20.2}/sonar-project.properties +0 -0
  55. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/__init__.py +0 -0
  56. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/api.py +0 -0
  57. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/cli.py +0 -0
  58. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/cmj/__init__.py +0 -0
  59. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/cmj/analysis.py +0 -0
  60. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/cmj/cli.py +0 -0
  61. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/cmj/debug_overlay.py +0 -0
  62. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/cmj/joint_angles.py +0 -0
  63. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/cmj/kinematics.py +0 -0
  64. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/core/__init__.py +0 -0
  65. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/core/auto_tuning.py +0 -0
  66. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/core/cli_utils.py +0 -0
  67. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/core/debug_overlay_utils.py +0 -0
  68. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/core/filtering.py +0 -0
  69. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/core/pose.py +0 -0
  70. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/core/smoothing.py +0 -0
  71. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/core/video_io.py +0 -0
  72. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/dropjump/__init__.py +0 -0
  73. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/dropjump/analysis.py +0 -0
  74. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/dropjump/cli.py +0 -0
  75. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/dropjump/debug_overlay.py +0 -0
  76. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/dropjump/kinematics.py +0 -0
  77. {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/py.typed +0 -0
  78. {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/__init__.py +0 -0
  79. {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_adaptive_threshold.py +0 -0
  80. {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_api.py +0 -0
  81. {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_cli_imports.py +0 -0
  82. {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_cmj_analysis.py +0 -0
  83. {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_cmj_kinematics.py +0 -0
  84. {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_com_estimation.py +0 -0
  85. {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_contact_detection.py +0 -0
  86. {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_filtering.py +0 -0
  87. {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_joint_angles.py +0 -0
  88. {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_kinematics.py +0 -0
  89. {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_polyorder.py +0 -0
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  <!-- version list -->
9
9
 
10
+ ## v0.20.2 (2025-11-10)
11
+
12
+ ### Bug Fixes
13
+
14
+ - Achieve 80%+ coverage on video_io for SonarCloud quality gate
15
+ ([`ed77fdb`](https://github.com/feniix/kinemotion/commit/ed77fdb080f143c492c724c9f4a138b2a364ad7e))
16
+
17
+
10
18
  ## v0.20.1 (2025-11-10)
11
19
 
12
20
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinemotion
3
- Version: 0.20.1
3
+ Version: 0.20.2
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.20.2"
4
4
  description = "Video-based kinematic analysis for athletic performance"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10,<3.13"
@@ -0,0 +1,341 @@
1
+ """Test that aspect ratio is preserved from source video."""
2
+
3
+ import tempfile
4
+ from pathlib import Path
5
+ from unittest.mock import patch
6
+
7
+ import cv2
8
+ import numpy as np
9
+ import pytest
10
+
11
+ from kinemotion.core.video_io import VideoProcessor
12
+ from kinemotion.dropjump.debug_overlay import DebugOverlayRenderer
13
+
14
+
15
+ def create_test_video(
16
+ width: int, height: int, fps: float = 30.0, num_frames: int = 10
17
+ ) -> str:
18
+ """Create a test video with specified dimensions."""
19
+ temp_file = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
20
+ temp_path = temp_file.name
21
+ temp_file.close()
22
+
23
+ fourcc = cv2.VideoWriter_fourcc(*"mp4v")
24
+ writer = cv2.VideoWriter(temp_path, fourcc, fps, (width, height))
25
+
26
+ rng = np.random.default_rng(42)
27
+ for _ in range(num_frames):
28
+ # Create a random frame
29
+ frame = rng.integers(0, 255, (height, width, 3), dtype=np.uint8)
30
+ writer.write(frame)
31
+
32
+ writer.release()
33
+ return temp_path
34
+
35
+
36
+ def test_aspect_ratio_16_9():
37
+ """Test 16:9 aspect ratio video."""
38
+ # Create test video with 16:9 aspect ratio
39
+ test_video = create_test_video(1920, 1080)
40
+
41
+ try:
42
+ # Read video
43
+ video = VideoProcessor(test_video)
44
+ assert video.width == 1920
45
+ assert video.height == 1080
46
+ video.close()
47
+
48
+ # Create output video
49
+ output_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
50
+ renderer = DebugOverlayRenderer(output_path, 1920, 1080, 1920, 1080, 30.0)
51
+
52
+ # Write test frame
53
+ test_frame = np.zeros((1080, 1920, 3), dtype=np.uint8)
54
+ renderer.write_frame(test_frame)
55
+ renderer.close()
56
+
57
+ # Verify output dimensions
58
+ cap = cv2.VideoCapture(output_path)
59
+ ret, frame = cap.read()
60
+ assert ret
61
+ assert frame.shape[0] == 1080 # height
62
+ assert frame.shape[1] == 1920 # width
63
+ cap.release()
64
+
65
+ Path(output_path).unlink()
66
+
67
+ finally:
68
+ Path(test_video).unlink()
69
+
70
+
71
+ def test_aspect_ratio_4_3():
72
+ """Test 4:3 aspect ratio video."""
73
+ # Create test video with 4:3 aspect ratio
74
+ test_video = create_test_video(640, 480)
75
+
76
+ try:
77
+ video = VideoProcessor(test_video)
78
+ assert video.width == 640
79
+ assert video.height == 480
80
+ video.close()
81
+
82
+ finally:
83
+ Path(test_video).unlink()
84
+
85
+
86
+ def test_aspect_ratio_9_16_portrait():
87
+ """Test 9:16 portrait aspect ratio video."""
88
+ # Create test video with portrait aspect ratio
89
+ test_video = create_test_video(1080, 1920)
90
+
91
+ try:
92
+ video = VideoProcessor(test_video)
93
+ assert video.width == 1080
94
+ assert video.height == 1920
95
+ video.close()
96
+
97
+ finally:
98
+ Path(test_video).unlink()
99
+
100
+
101
+ def test_frame_dimension_validation():
102
+ """Test that mismatched frame dimensions raise an error."""
103
+ output_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
104
+
105
+ try:
106
+ renderer = DebugOverlayRenderer(output_path, 1920, 1080, 1920, 1080, 30.0)
107
+
108
+ # Try to write frame with wrong dimensions
109
+ wrong_frame = np.zeros(
110
+ (1080, 1080, 3), dtype=np.uint8
111
+ ) # Square instead of 16:9
112
+
113
+ with pytest.raises(ValueError, match="don't match"):
114
+ renderer.write_frame(wrong_frame)
115
+
116
+ renderer.close()
117
+
118
+ finally:
119
+ Path(output_path).unlink(missing_ok=True)
120
+
121
+
122
+ def test_ffprobe_not_found_warning():
123
+ """Test that warning is shown when ffprobe is not available."""
124
+ test_video = create_test_video(640, 480)
125
+
126
+ try:
127
+ # Mock subprocess.run to raise FileNotFoundError (ffprobe not found)
128
+ with patch(
129
+ "subprocess.run", side_effect=FileNotFoundError("ffprobe not found")
130
+ ):
131
+ with pytest.warns(
132
+ UserWarning, match="ffprobe not found.*rotation and aspect ratio"
133
+ ):
134
+ video = VideoProcessor(test_video)
135
+ video.close()
136
+
137
+ finally:
138
+ Path(test_video).unlink()
139
+
140
+
141
+ def test_ffprobe_timeout_silent():
142
+ """Test that ffprobe timeout is handled silently."""
143
+ import subprocess
144
+
145
+ test_video = create_test_video(640, 480)
146
+
147
+ try:
148
+ # Mock subprocess.run to raise TimeoutExpired
149
+ with patch(
150
+ "subprocess.run",
151
+ side_effect=subprocess.TimeoutExpired("ffprobe", timeout=5),
152
+ ):
153
+ # Should not raise exception or warning, just continue
154
+ video = VideoProcessor(test_video)
155
+ assert video.rotation == 0 # Default rotation
156
+ video.close()
157
+
158
+ finally:
159
+ Path(test_video).unlink()
160
+
161
+
162
+ def test_ffprobe_json_decode_error_silent():
163
+ """Test that ffprobe JSON decode error is handled silently."""
164
+ import json
165
+
166
+ test_video = create_test_video(640, 480)
167
+
168
+ try:
169
+ # Mock subprocess.run to raise JSONDecodeError
170
+ with patch(
171
+ "subprocess.run",
172
+ side_effect=json.JSONDecodeError("Invalid JSON", "", 0),
173
+ ):
174
+ # Should not raise exception or warning, just continue
175
+ video = VideoProcessor(test_video)
176
+ assert video.rotation == 0 # Default rotation
177
+ video.close()
178
+
179
+ finally:
180
+ Path(test_video).unlink()
181
+
182
+
183
+ def test_video_not_found():
184
+ """Test that VideoProcessor raises ValueError for non-existent video."""
185
+ with pytest.raises(ValueError, match="Could not open video"):
186
+ VideoProcessor("/nonexistent/path/to/video.mp4")
187
+
188
+
189
+ def test_ffprobe_returncode_error():
190
+ """Test that ffprobe non-zero returncode is handled silently."""
191
+ from unittest.mock import MagicMock
192
+
193
+ test_video = create_test_video(640, 480)
194
+
195
+ try:
196
+ # Mock subprocess.run to return non-zero returncode
197
+ mock_result = MagicMock()
198
+ mock_result.returncode = 1
199
+ mock_result.stdout = ""
200
+
201
+ with patch("subprocess.run", return_value=mock_result):
202
+ # Should continue silently with defaults
203
+ video = VideoProcessor(test_video)
204
+ assert video.rotation == 0
205
+ video.close()
206
+
207
+ finally:
208
+ Path(test_video).unlink()
209
+
210
+
211
+ def test_ffprobe_empty_streams():
212
+ """Test that ffprobe with no streams is handled silently."""
213
+ from unittest.mock import MagicMock
214
+
215
+ test_video = create_test_video(640, 480)
216
+
217
+ try:
218
+ # Mock subprocess.run to return empty streams
219
+ mock_result = MagicMock()
220
+ mock_result.returncode = 0
221
+ mock_result.stdout = '{"streams": []}'
222
+
223
+ with patch("subprocess.run", return_value=mock_result):
224
+ # Should continue silently with defaults
225
+ video = VideoProcessor(test_video)
226
+ assert video.rotation == 0
227
+ video.close()
228
+
229
+ finally:
230
+ Path(test_video).unlink()
231
+
232
+
233
+ def test_video_rotation_90_degrees():
234
+ """Test video rotation handling for 90 degree rotation."""
235
+ from unittest.mock import MagicMock
236
+
237
+ test_video = create_test_video(640, 480)
238
+
239
+ try:
240
+ # Mock ffprobe to return 90 degree rotation
241
+ mock_result = MagicMock()
242
+ mock_result.returncode = 0
243
+ mock_result.stdout = """{
244
+ "streams": [{
245
+ "sample_aspect_ratio": "1:1",
246
+ "side_data_list": [{
247
+ "side_data_type": "Display Matrix",
248
+ "rotation": 90
249
+ }]
250
+ }]
251
+ }"""
252
+
253
+ with patch("subprocess.run", return_value=mock_result):
254
+ video = VideoProcessor(test_video)
255
+ assert video.rotation == 90
256
+
257
+ # Read frame and verify rotation is applied
258
+ frame = video.read_frame()
259
+ assert frame is not None
260
+ # After 90° rotation: width and height should be swapped
261
+ assert frame.shape[1] == 480 # Original height becomes width
262
+ assert frame.shape[0] == 640 # Original width becomes height
263
+
264
+ video.close()
265
+
266
+ finally:
267
+ Path(test_video).unlink()
268
+
269
+
270
+ def test_video_rotation_negative_90_degrees():
271
+ """Test video rotation handling for -90 degree rotation."""
272
+ from unittest.mock import MagicMock
273
+
274
+ test_video = create_test_video(640, 480)
275
+
276
+ try:
277
+ # Mock ffprobe to return -90 degree rotation
278
+ mock_result = MagicMock()
279
+ mock_result.returncode = 0
280
+ mock_result.stdout = """{
281
+ "streams": [{
282
+ "sample_aspect_ratio": "1:1",
283
+ "side_data_list": [{
284
+ "side_data_type": "Display Matrix",
285
+ "rotation": -90
286
+ }]
287
+ }]
288
+ }"""
289
+
290
+ with patch("subprocess.run", return_value=mock_result):
291
+ video = VideoProcessor(test_video)
292
+ assert video.rotation == -90
293
+
294
+ # Read frame and verify rotation is applied
295
+ frame = video.read_frame()
296
+ assert frame is not None
297
+ # After -90° rotation: dimensions swapped
298
+ assert frame.shape[1] == 480
299
+ assert frame.shape[0] == 640
300
+
301
+ video.close()
302
+
303
+ finally:
304
+ Path(test_video).unlink()
305
+
306
+
307
+ def test_video_rotation_180_degrees():
308
+ """Test video rotation handling for 180 degree rotation."""
309
+ from unittest.mock import MagicMock
310
+
311
+ test_video = create_test_video(640, 480)
312
+
313
+ try:
314
+ # Mock ffprobe to return 180 degree rotation
315
+ mock_result = MagicMock()
316
+ mock_result.returncode = 0
317
+ mock_result.stdout = """{
318
+ "streams": [{
319
+ "sample_aspect_ratio": "1:1",
320
+ "side_data_list": [{
321
+ "side_data_type": "Display Matrix",
322
+ "rotation": 180
323
+ }]
324
+ }]
325
+ }"""
326
+
327
+ with patch("subprocess.run", return_value=mock_result):
328
+ video = VideoProcessor(test_video)
329
+ assert video.rotation == 180
330
+
331
+ # Read frame and verify rotation is applied
332
+ frame = video.read_frame()
333
+ assert frame is not None
334
+ # After 180° rotation: dimensions stay the same
335
+ assert frame.shape[1] == 640
336
+ assert frame.shape[0] == 480
337
+
338
+ video.close()
339
+
340
+ finally:
341
+ Path(test_video).unlink()
@@ -700,7 +700,7 @@ wheels = [
700
700
 
701
701
  [[package]]
702
702
  name = "kinemotion"
703
- version = "0.20.1"
703
+ version = "0.20.2"
704
704
  source = { editable = "." }
705
705
  dependencies = [
706
706
  { name = "click" },
@@ -1,138 +0,0 @@
1
- """Test that aspect ratio is preserved from source video."""
2
-
3
- import tempfile
4
- from pathlib import Path
5
- from unittest.mock import patch
6
-
7
- import cv2
8
- import numpy as np
9
- import pytest
10
-
11
- from kinemotion.core.video_io import VideoProcessor
12
- from kinemotion.dropjump.debug_overlay import DebugOverlayRenderer
13
-
14
-
15
- def create_test_video(
16
- width: int, height: int, fps: float = 30.0, num_frames: int = 10
17
- ) -> str:
18
- """Create a test video with specified dimensions."""
19
- temp_file = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
20
- temp_path = temp_file.name
21
- temp_file.close()
22
-
23
- fourcc = cv2.VideoWriter_fourcc(*"mp4v")
24
- writer = cv2.VideoWriter(temp_path, fourcc, fps, (width, height))
25
-
26
- rng = np.random.default_rng(42)
27
- for _ in range(num_frames):
28
- # Create a random frame
29
- frame = rng.integers(0, 255, (height, width, 3), dtype=np.uint8)
30
- writer.write(frame)
31
-
32
- writer.release()
33
- return temp_path
34
-
35
-
36
- def test_aspect_ratio_16_9():
37
- """Test 16:9 aspect ratio video."""
38
- # Create test video with 16:9 aspect ratio
39
- test_video = create_test_video(1920, 1080)
40
-
41
- try:
42
- # Read video
43
- video = VideoProcessor(test_video)
44
- assert video.width == 1920
45
- assert video.height == 1080
46
- video.close()
47
-
48
- # Create output video
49
- output_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
50
- renderer = DebugOverlayRenderer(output_path, 1920, 1080, 1920, 1080, 30.0)
51
-
52
- # Write test frame
53
- test_frame = np.zeros((1080, 1920, 3), dtype=np.uint8)
54
- renderer.write_frame(test_frame)
55
- renderer.close()
56
-
57
- # Verify output dimensions
58
- cap = cv2.VideoCapture(output_path)
59
- ret, frame = cap.read()
60
- assert ret
61
- assert frame.shape[0] == 1080 # height
62
- assert frame.shape[1] == 1920 # width
63
- cap.release()
64
-
65
- Path(output_path).unlink()
66
-
67
- finally:
68
- Path(test_video).unlink()
69
-
70
-
71
- def test_aspect_ratio_4_3():
72
- """Test 4:3 aspect ratio video."""
73
- # Create test video with 4:3 aspect ratio
74
- test_video = create_test_video(640, 480)
75
-
76
- try:
77
- video = VideoProcessor(test_video)
78
- assert video.width == 640
79
- assert video.height == 480
80
- video.close()
81
-
82
- finally:
83
- Path(test_video).unlink()
84
-
85
-
86
- def test_aspect_ratio_9_16_portrait():
87
- """Test 9:16 portrait aspect ratio video."""
88
- # Create test video with portrait aspect ratio
89
- test_video = create_test_video(1080, 1920)
90
-
91
- try:
92
- video = VideoProcessor(test_video)
93
- assert video.width == 1080
94
- assert video.height == 1920
95
- video.close()
96
-
97
- finally:
98
- Path(test_video).unlink()
99
-
100
-
101
- def test_frame_dimension_validation():
102
- """Test that mismatched frame dimensions raise an error."""
103
- output_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
104
-
105
- try:
106
- renderer = DebugOverlayRenderer(output_path, 1920, 1080, 1920, 1080, 30.0)
107
-
108
- # Try to write frame with wrong dimensions
109
- wrong_frame = np.zeros(
110
- (1080, 1080, 3), dtype=np.uint8
111
- ) # Square instead of 16:9
112
-
113
- with pytest.raises(ValueError, match="don't match"):
114
- renderer.write_frame(wrong_frame)
115
-
116
- renderer.close()
117
-
118
- finally:
119
- Path(output_path).unlink(missing_ok=True)
120
-
121
-
122
- def test_ffprobe_not_found_warning():
123
- """Test that warning is shown when ffprobe is not available."""
124
- test_video = create_test_video(640, 480)
125
-
126
- try:
127
- # Mock subprocess.run to raise FileNotFoundError (ffprobe not found)
128
- with patch(
129
- "subprocess.run", side_effect=FileNotFoundError("ffprobe not found")
130
- ):
131
- with pytest.warns(
132
- UserWarning, match="ffprobe not found.*rotation and aspect ratio"
133
- ):
134
- video = VideoProcessor(test_video)
135
- video.close()
136
-
137
- finally:
138
- Path(test_video).unlink()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes