kinemotion 0.76.3__py3-none-any.whl → 2.0.0__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.

Files changed (53) hide show
  1. kinemotion/__init__.py +3 -18
  2. kinemotion/api.py +7 -27
  3. kinemotion/cli.py +2 -4
  4. kinemotion/{countermovement_jump → cmj}/analysis.py +158 -16
  5. kinemotion/{countermovement_jump → cmj}/api.py +18 -46
  6. kinemotion/{countermovement_jump → cmj}/cli.py +46 -6
  7. kinemotion/cmj/debug_overlay.py +457 -0
  8. kinemotion/{countermovement_jump → cmj}/joint_angles.py +31 -96
  9. kinemotion/{countermovement_jump → cmj}/metrics_validator.py +271 -176
  10. kinemotion/{countermovement_jump → cmj}/validation_bounds.py +18 -1
  11. kinemotion/core/__init__.py +2 -11
  12. kinemotion/core/auto_tuning.py +107 -149
  13. kinemotion/core/cli_utils.py +0 -74
  14. kinemotion/core/debug_overlay_utils.py +15 -142
  15. kinemotion/core/experimental.py +51 -55
  16. kinemotion/core/filtering.py +56 -116
  17. kinemotion/core/pipeline_utils.py +2 -2
  18. kinemotion/core/pose.py +98 -47
  19. kinemotion/core/quality.py +6 -4
  20. kinemotion/core/smoothing.py +51 -65
  21. kinemotion/core/types.py +0 -15
  22. kinemotion/core/validation.py +7 -76
  23. kinemotion/core/video_io.py +27 -41
  24. kinemotion/{drop_jump → dropjump}/__init__.py +8 -2
  25. kinemotion/{drop_jump → dropjump}/analysis.py +120 -282
  26. kinemotion/{drop_jump → dropjump}/api.py +33 -59
  27. kinemotion/{drop_jump → dropjump}/cli.py +136 -70
  28. kinemotion/dropjump/debug_overlay.py +182 -0
  29. kinemotion/{drop_jump → dropjump}/kinematics.py +65 -175
  30. kinemotion/{drop_jump → dropjump}/metrics_validator.py +51 -25
  31. kinemotion/{drop_jump → dropjump}/validation_bounds.py +1 -1
  32. kinemotion/models/rtmpose-s_simcc-body7_pt-body7-halpe26_700e-256x192-7f134165_20230605.onnx +3 -0
  33. kinemotion/models/yolox_tiny_8xb8-300e_humanart-6f3252f9.onnx +3 -0
  34. {kinemotion-0.76.3.dist-info → kinemotion-2.0.0.dist-info}/METADATA +26 -75
  35. kinemotion-2.0.0.dist-info/RECORD +49 -0
  36. kinemotion/core/overlay_constants.py +0 -61
  37. kinemotion/core/video_analysis_base.py +0 -132
  38. kinemotion/countermovement_jump/debug_overlay.py +0 -325
  39. kinemotion/drop_jump/debug_overlay.py +0 -241
  40. kinemotion/squat_jump/__init__.py +0 -5
  41. kinemotion/squat_jump/analysis.py +0 -377
  42. kinemotion/squat_jump/api.py +0 -610
  43. kinemotion/squat_jump/cli.py +0 -309
  44. kinemotion/squat_jump/debug_overlay.py +0 -163
  45. kinemotion/squat_jump/kinematics.py +0 -342
  46. kinemotion/squat_jump/metrics_validator.py +0 -438
  47. kinemotion/squat_jump/validation_bounds.py +0 -221
  48. kinemotion-0.76.3.dist-info/RECORD +0 -57
  49. /kinemotion/{countermovement_jump → cmj}/__init__.py +0 -0
  50. /kinemotion/{countermovement_jump → cmj}/kinematics.py +0 -0
  51. {kinemotion-0.76.3.dist-info → kinemotion-2.0.0.dist-info}/WHEEL +0 -0
  52. {kinemotion-0.76.3.dist-info → kinemotion-2.0.0.dist-info}/entry_points.txt +0 -0
  53. {kinemotion-0.76.3.dist-info → kinemotion-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,610 +0,0 @@
1
- """Public API for SJ (Squat Jump) video analysis."""
2
-
3
- import json
4
- import time
5
- from collections.abc import Callable
6
- from dataclasses import dataclass
7
- from pathlib import Path
8
-
9
- import numpy as np
10
- from numpy.typing import NDArray
11
-
12
- from ..core.auto_tuning import (
13
- AnalysisParameters,
14
- QualityPreset,
15
- analyze_video_sample,
16
- auto_tune_parameters,
17
- )
18
- from ..core.experimental import experimental
19
- from ..core.filtering import reject_outliers
20
- from ..core.metadata import (
21
- AlgorithmConfig,
22
- DetectionConfig,
23
- ProcessingInfo,
24
- ResultMetadata,
25
- SmoothingConfig,
26
- VideoInfo,
27
- create_timestamp,
28
- get_kinemotion_version,
29
- )
30
- from ..core.pipeline_utils import (
31
- apply_expert_overrides,
32
- apply_smoothing,
33
- convert_timer_to_stage_names,
34
- determine_confidence_levels,
35
- extract_vertical_positions,
36
- parse_quality_preset,
37
- print_verbose_parameters,
38
- process_all_frames,
39
- process_videos_bulk_generic,
40
- )
41
- from ..core.pose import MediaPipePoseTracker
42
- from ..core.quality import QualityAssessment, assess_jump_quality
43
- from ..core.timing import PerformanceTimer, Timer
44
- from ..core.validation import ValidationResult
45
- from ..core.video_io import VideoProcessor
46
- from .analysis import detect_sj_phases
47
- from .debug_overlay import SquatJumpDebugOverlayRenderer
48
- from .kinematics import SJMetrics, calculate_sj_metrics
49
- from .metrics_validator import SJMetricsValidator
50
-
51
-
52
- @dataclass
53
- class AnalysisOverrides:
54
- """Optional overrides for analysis parameters.
55
-
56
- Allows fine-tuning of specific analysis parameters beyond quality presets.
57
- If None, values will be determined by the quality preset.
58
- """
59
-
60
- smoothing_window: int | None = None
61
- velocity_threshold: float | None = None
62
- min_contact_frames: int | None = None
63
- visibility_threshold: float | None = None
64
-
65
-
66
- def _generate_debug_video(
67
- output_video: str,
68
- frames: list[NDArray[np.uint8]],
69
- frame_indices: list[int],
70
- smoothed_landmarks: list,
71
- metrics: SJMetrics,
72
- video_fps: float,
73
- timer: Timer,
74
- verbose: bool,
75
- ) -> None:
76
- """Generate debug video with SJ analysis overlay."""
77
- if verbose:
78
- print(f"Generating debug video: {output_video}")
79
-
80
- debug_h, debug_w = frames[0].shape[:2]
81
- step = max(1, int(video_fps / 30.0))
82
- debug_fps = video_fps / step
83
-
84
- with timer.measure("debug_video_generation"):
85
- with SquatJumpDebugOverlayRenderer(
86
- output_video,
87
- debug_w,
88
- debug_h,
89
- debug_w,
90
- debug_h,
91
- debug_fps,
92
- timer=timer,
93
- ) as renderer:
94
- for frame, idx in zip(frames, frame_indices, strict=True):
95
- annotated = renderer.render_frame(frame, smoothed_landmarks[idx], idx, metrics)
96
- renderer.write_frame(annotated)
97
-
98
- if verbose:
99
- print(f"Debug video saved: {output_video}")
100
-
101
-
102
- def _save_metrics_to_json(
103
- metrics: SJMetrics, json_output: str, timer: Timer, verbose: bool
104
- ) -> None:
105
- """Save metrics to JSON file."""
106
- with timer.measure("json_serialization"):
107
- output_path = Path(json_output)
108
- metrics_dict = metrics.to_dict()
109
- json_str = json.dumps(metrics_dict, indent=2)
110
- output_path.write_text(json_str)
111
-
112
- if verbose:
113
- print(f"Metrics written to: {json_output}")
114
-
115
-
116
- def _print_timing_summary(start_time: float, timer: Timer, metrics: SJMetrics) -> None:
117
- """Print verbose timing summary and metrics."""
118
- total_time = time.perf_counter() - start_time
119
- stage_times = convert_timer_to_stage_names(timer.get_metrics())
120
-
121
- print("\n=== Timing Summary ===")
122
- for stage, duration in stage_times.items():
123
- percentage = (duration / total_time) * 100
124
- dur_ms = duration * 1000
125
- print(f"{stage:.<40} {dur_ms:>6.0f}ms ({percentage:>5.1f}%)")
126
- total_ms = total_time * 1000
127
- print(f"{'Total':.<40} {total_ms:>6.0f}ms (100.0%)")
128
- print()
129
-
130
- print(f"\nJump height: {metrics.jump_height:.3f}m")
131
- print(f"Flight time: {metrics.flight_time * 1000:.1f}ms")
132
- print(f"Squat hold duration: {metrics.squat_hold_duration * 1000:.1f}ms")
133
- print(f"Concentric duration: {metrics.concentric_duration * 1000:.1f}ms")
134
- if metrics.peak_power is not None:
135
- print(f"Peak power: {metrics.peak_power:.0f}W")
136
- if metrics.peak_force is not None:
137
- print(f"Peak force: {metrics.peak_force:.0f}N")
138
-
139
-
140
- def _print_quality_warnings(quality_result: QualityAssessment, verbose: bool) -> None:
141
- """Print quality warnings if present."""
142
- if verbose and quality_result.warnings:
143
- print("\n⚠️ Quality Warnings:")
144
- for warning in quality_result.warnings:
145
- print(f" - {warning}")
146
- print()
147
-
148
-
149
- def _print_validation_results(validation_result: ValidationResult, verbose: bool) -> None:
150
- """Print validation issues if present."""
151
- if verbose and validation_result.issues:
152
- print("\n⚠️ Validation Results:")
153
- for issue in validation_result.issues:
154
- print(f" [{issue.severity.value}] {issue.metric}: {issue.message}")
155
-
156
-
157
- def _create_algorithm_config(params: AnalysisParameters) -> AlgorithmConfig:
158
- """Create algorithm configuration from parameters."""
159
- return AlgorithmConfig(
160
- detection_method="static_squat",
161
- tracking_method="mediapipe_pose",
162
- model_complexity=1,
163
- smoothing=SmoothingConfig(
164
- window_size=params.smoothing_window,
165
- polynomial_order=params.polyorder,
166
- use_bilateral_filter=params.bilateral_filter,
167
- use_outlier_rejection=params.outlier_rejection,
168
- ),
169
- detection=DetectionConfig(
170
- velocity_threshold=params.velocity_threshold,
171
- min_contact_frames=params.min_contact_frames,
172
- visibility_threshold=params.visibility_threshold,
173
- use_curvature_refinement=params.use_curvature,
174
- ),
175
- drop_detection=None,
176
- )
177
-
178
-
179
- def _create_video_info(video_path: str, video: VideoProcessor) -> VideoInfo:
180
- """Create video information metadata."""
181
- return VideoInfo(
182
- source_path=video_path,
183
- fps=video.fps,
184
- width=video.width,
185
- height=video.height,
186
- duration_s=video.frame_count / video.fps,
187
- frame_count=video.frame_count,
188
- codec=video.codec,
189
- )
190
-
191
-
192
- def _create_processing_info(
193
- start_time: float, quality_preset: QualityPreset, timer: Timer
194
- ) -> ProcessingInfo:
195
- """Create processing information metadata."""
196
- processing_time = time.perf_counter() - start_time
197
- stage_times = convert_timer_to_stage_names(timer.get_metrics())
198
-
199
- return ProcessingInfo(
200
- version=get_kinemotion_version(),
201
- timestamp=create_timestamp(),
202
- quality_preset=quality_preset.value,
203
- processing_time_s=processing_time,
204
- timing_breakdown=stage_times,
205
- )
206
-
207
-
208
- def _create_result_metadata(
209
- quality_result: QualityAssessment,
210
- video_info: VideoInfo,
211
- processing_info: ProcessingInfo,
212
- algorithm_config: AlgorithmConfig,
213
- ) -> ResultMetadata:
214
- """Create result metadata from components."""
215
- return ResultMetadata(
216
- quality=quality_result,
217
- video=video_info,
218
- processing=processing_info,
219
- algorithm=algorithm_config,
220
- )
221
-
222
-
223
- def _run_pose_tracking(
224
- video: VideoProcessor,
225
- quality_preset: QualityPreset,
226
- detection_confidence: float | None,
227
- tracking_confidence: float | None,
228
- pose_tracker: "MediaPipePoseTracker | None",
229
- verbose: bool,
230
- timer: Timer,
231
- ) -> tuple[list[NDArray[np.uint8]], list, list[int]]:
232
- """Initialize tracker and process all frames."""
233
- if verbose:
234
- print(
235
- f"Video: {video.width}x{video.height} @ {video.fps:.2f} fps, "
236
- f"{video.frame_count} frames"
237
- )
238
-
239
- det_conf, track_conf = determine_confidence_levels(
240
- quality_preset, detection_confidence, tracking_confidence
241
- )
242
-
243
- if pose_tracker is None:
244
- if verbose:
245
- print("Processing all frames with MediaPipe pose tracking...")
246
- tracker = MediaPipePoseTracker(
247
- min_detection_confidence=det_conf,
248
- min_tracking_confidence=track_conf,
249
- timer=timer,
250
- )
251
- should_close_tracker = True
252
- else:
253
- tracker = pose_tracker
254
- should_close_tracker = False
255
-
256
- return process_all_frames(video, tracker, verbose, timer, close_tracker=should_close_tracker)
257
-
258
-
259
- def _get_tuned_parameters(
260
- video: VideoProcessor,
261
- landmarks_sequence: list,
262
- quality_preset: QualityPreset,
263
- overrides: AnalysisOverrides | None,
264
- verbose: bool,
265
- timer: Timer,
266
- ) -> AnalysisParameters:
267
- """Analyze sample and tune parameters with expert overrides."""
268
- with timer.measure("parameter_auto_tuning"):
269
- characteristics = analyze_video_sample(landmarks_sequence, video.fps, video.frame_count)
270
- params = auto_tune_parameters(characteristics, quality_preset)
271
-
272
- if overrides:
273
- params = apply_expert_overrides(
274
- params,
275
- overrides.smoothing_window,
276
- overrides.velocity_threshold,
277
- overrides.min_contact_frames,
278
- overrides.visibility_threshold,
279
- )
280
-
281
- if verbose:
282
- print_verbose_parameters(video, characteristics, quality_preset, params)
283
-
284
- return params
285
-
286
-
287
- def _run_kinematic_analysis(
288
- video: VideoProcessor,
289
- smoothed_landmarks: list,
290
- params: AnalysisParameters,
291
- mass_kg: float | None,
292
- verbose: bool,
293
- timer: Timer,
294
- ) -> tuple[SJMetrics, NDArray[np.float64], NDArray[np.float64]]:
295
- """Extract positions, detect phases, and calculate metrics."""
296
- if verbose:
297
- print("Extracting vertical positions (Hip and Foot)...")
298
- with timer.measure("vertical_position_extraction"):
299
- vertical_positions, visibilities = extract_vertical_positions(
300
- smoothed_landmarks, target="hip"
301
- )
302
- foot_positions, _ = extract_vertical_positions(smoothed_landmarks, target="foot")
303
-
304
- if verbose:
305
- print("Detecting SJ phases...")
306
- with timer.measure("phase_detection"):
307
- phases = detect_sj_phases(
308
- vertical_positions,
309
- video.fps,
310
- window_length=params.smoothing_window,
311
- polyorder=params.polyorder,
312
- )
313
-
314
- if phases is None:
315
- raise ValueError("Could not detect SJ phases in video")
316
-
317
- squat_hold_start, concentric_start, takeoff_frame, landing_frame = phases
318
-
319
- if verbose:
320
- print("Calculating metrics...")
321
- with timer.measure("metrics_calculation"):
322
- metrics = calculate_sj_metrics(
323
- vertical_positions,
324
- foot_positions,
325
- squat_hold_start,
326
- concentric_start,
327
- takeoff_frame,
328
- landing_frame,
329
- video.fps,
330
- mass_kg=mass_kg,
331
- tracking_method="hip_hybrid",
332
- )
333
-
334
- return metrics, vertical_positions, visibilities
335
-
336
-
337
- def _finalize_analysis_results(
338
- metrics: SJMetrics,
339
- video: VideoProcessor,
340
- video_path: str,
341
- vertical_positions: NDArray[np.float64],
342
- visibilities: NDArray[np.float64],
343
- params: AnalysisParameters,
344
- quality_preset: QualityPreset,
345
- start_time: float,
346
- timer: Timer,
347
- verbose: bool,
348
- ) -> None:
349
- """Assess quality, validate metrics, and attach metadata."""
350
- if verbose:
351
- print("Assessing tracking quality...")
352
- with timer.measure("quality_assessment"):
353
- _, outlier_mask = reject_outliers(
354
- vertical_positions,
355
- use_ransac=True,
356
- use_median=True,
357
- interpolate=False,
358
- )
359
- quality_result = assess_jump_quality(
360
- visibilities=visibilities,
361
- positions=vertical_positions,
362
- outlier_mask=outlier_mask,
363
- fps=video.fps,
364
- phases_detected=True,
365
- phase_count=3, # SQUAT_HOLD, CONCENTRIC, FLIGHT
366
- )
367
-
368
- _print_quality_warnings(quality_result, verbose)
369
-
370
- with timer.measure("metrics_validation"):
371
- validator = SJMetricsValidator()
372
- validation_result = validator.validate(metrics.to_dict()) # type: ignore[arg-type]
373
-
374
- algorithm_config = _create_algorithm_config(params)
375
- video_info = _create_video_info(video_path, video)
376
- processing_info = _create_processing_info(start_time, quality_preset, timer)
377
- result_metadata = _create_result_metadata(
378
- quality_result, video_info, processing_info, algorithm_config
379
- )
380
- metrics.result_metadata = result_metadata
381
-
382
- _print_validation_results(validation_result, verbose)
383
-
384
-
385
- @dataclass
386
- class SJVideoConfig:
387
- """Configuration for processing a single SJ video."""
388
-
389
- video_path: str
390
- quality: str = "balanced"
391
- output_video: str | None = None
392
- json_output: str | None = None
393
- overrides: AnalysisOverrides | None = None
394
- detection_confidence: float | None = None
395
- tracking_confidence: float | None = None
396
- mass_kg: float | None = None
397
- verbose: bool = False
398
- timer: Timer | None = None
399
- pose_tracker: "MediaPipePoseTracker | None" = None
400
-
401
- def to_kwargs(self) -> dict:
402
- """Convert config to kwargs dict for process_sj_video."""
403
- return {
404
- "video_path": self.video_path,
405
- "quality": self.quality,
406
- "output_video": self.output_video,
407
- "json_output": self.json_output,
408
- "overrides": self.overrides,
409
- "detection_confidence": self.detection_confidence,
410
- "tracking_confidence": self.tracking_confidence,
411
- "mass_kg": self.mass_kg,
412
- "verbose": self.verbose,
413
- "timer": self.timer,
414
- "pose_tracker": self.pose_tracker,
415
- }
416
-
417
-
418
- @dataclass
419
- class SJVideoResult:
420
- """Result of processing a single SJ video."""
421
-
422
- video_path: str
423
- success: bool
424
- metrics: SJMetrics | None = None
425
- error: str | None = None
426
- processing_time: float = 0.0
427
-
428
-
429
- @experimental(
430
- "Squat Jump analysis is new and awaiting validation studies. "
431
- "Power/force calculations use validated Sayers regression but SJ-specific "
432
- "phase detection may need refinement based on real-world data.",
433
- since="0.74.0",
434
- )
435
- def process_sj_video(
436
- video_path: str,
437
- quality: str = "balanced",
438
- output_video: str | None = None,
439
- json_output: str | None = None,
440
- overrides: AnalysisOverrides | None = None,
441
- detection_confidence: float | None = None,
442
- tracking_confidence: float | None = None,
443
- mass_kg: float | None = None,
444
- verbose: bool = False,
445
- timer: Timer | None = None,
446
- pose_tracker: MediaPipePoseTracker | None = None,
447
- ) -> SJMetrics:
448
- """
449
- Process a single SJ video and return metrics.
450
-
451
- SJ (Squat Jump) is performed from a static squat position without
452
- countermovement. Athletes start in a squat hold, then explode
453
- upward to measure pure concentric power output.
454
-
455
- Args:
456
- video_path: Path to the input video file
457
- quality: Analysis quality preset ("fast", "balanced", or "accurate")
458
- output_video: Optional path for debug video output
459
- json_output: Optional path for JSON metrics output
460
- overrides: Optional AnalysisOverrides with parameter fine-tuning
461
- detection_confidence: Optional override for pose detection confidence
462
- tracking_confidence: Optional override for pose tracking confidence
463
- mass_kg: Athlete mass in kilograms (required for power calculations)
464
- verbose: Print processing details
465
- timer: Optional Timer for measuring operations
466
- pose_tracker: Optional pre-initialized PoseTracker instance (reused if provided)
467
-
468
- Returns:
469
- SJMetrics object containing analysis results
470
-
471
- Raises:
472
- ValueError: If video cannot be processed or parameters are invalid
473
- FileNotFoundError: If video file does not exist
474
- """
475
- if not Path(video_path).exists():
476
- raise FileNotFoundError(f"Video file not found: {video_path}")
477
-
478
- if mass_kg is None or mass_kg <= 0:
479
- raise ValueError("Athlete mass (mass_kg) must be provided and greater than 0")
480
-
481
- start_time = time.perf_counter()
482
- timer = timer or PerformanceTimer()
483
- quality_preset = parse_quality_preset(quality)
484
-
485
- with timer.measure("video_initialization"):
486
- with VideoProcessor(video_path, timer=timer) as video:
487
- # 1. Pose Tracking
488
- frames, landmarks_sequence, frame_indices = _run_pose_tracking(
489
- video,
490
- quality_preset,
491
- detection_confidence,
492
- tracking_confidence,
493
- pose_tracker,
494
- verbose,
495
- timer,
496
- )
497
-
498
- # 2. Parameters & Smoothing
499
- params = _get_tuned_parameters(
500
- video, landmarks_sequence, quality_preset, overrides, verbose, timer
501
- )
502
- smoothed_landmarks = apply_smoothing(landmarks_sequence, params, verbose, timer)
503
-
504
- # 3. Kinematic Analysis
505
- metrics, vertical_positions, visibilities = _run_kinematic_analysis(
506
- video, smoothed_landmarks, params, mass_kg, verbose, timer
507
- )
508
-
509
- # 4. Debug Video Generation (Optional)
510
- if output_video:
511
- _generate_debug_video(
512
- output_video,
513
- frames,
514
- frame_indices,
515
- smoothed_landmarks,
516
- metrics,
517
- video.fps,
518
- timer,
519
- verbose,
520
- )
521
-
522
- # 5. Finalization (Quality, Metadata, Validation)
523
- _finalize_analysis_results(
524
- metrics,
525
- video,
526
- video_path,
527
- vertical_positions,
528
- visibilities,
529
- params,
530
- quality_preset,
531
- start_time,
532
- timer,
533
- verbose,
534
- )
535
-
536
- if json_output:
537
- _save_metrics_to_json(metrics, json_output, timer, verbose)
538
-
539
- if verbose:
540
- _print_timing_summary(start_time, timer, metrics)
541
-
542
- return metrics
543
-
544
-
545
- def process_sj_video_from_config(
546
- config: SJVideoConfig,
547
- ) -> SJMetrics:
548
- """Process a SJ video using a configuration object.
549
-
550
- This is a convenience wrapper around process_sj_video that
551
- accepts a SJVideoConfig instead of individual parameters.
552
-
553
- Args:
554
- config: Configuration object containing all analysis parameters
555
-
556
- Returns:
557
- SJMetrics object containing analysis results
558
- """
559
- return process_sj_video(**config.to_kwargs())
560
-
561
-
562
- @experimental(
563
- "Squat Jump analysis is new and awaiting validation studies. "
564
- "Bulk processing uses parallel workers which may need tuning for large batches.",
565
- since="0.74.0",
566
- )
567
- def process_sj_videos_bulk(
568
- configs: list[SJVideoConfig],
569
- max_workers: int = 4,
570
- progress_callback: Callable[[SJVideoResult], None] | None = None,
571
- ) -> list[SJVideoResult]:
572
- """Process multiple SJ videos in parallel."""
573
-
574
- def error_factory(video_path: str, error_msg: str) -> SJVideoResult:
575
- return SJVideoResult(video_path=video_path, success=False, error=error_msg)
576
-
577
- return process_videos_bulk_generic(
578
- configs,
579
- _process_sj_video_wrapper,
580
- error_factory,
581
- max_workers,
582
- progress_callback,
583
- )
584
-
585
-
586
- def _process_sj_video_wrapper(config: SJVideoConfig) -> SJVideoResult:
587
- """Wrapper function for parallel SJ processing."""
588
- start_time = time.perf_counter()
589
-
590
- try:
591
- # Use convenience wrapper to avoid parameter unpacking
592
- metrics = process_sj_video_from_config(config)
593
- processing_time = time.perf_counter() - start_time
594
-
595
- return SJVideoResult(
596
- video_path=config.video_path,
597
- success=True,
598
- metrics=metrics,
599
- processing_time=processing_time,
600
- )
601
-
602
- except Exception as e:
603
- processing_time = time.perf_counter() - start_time
604
-
605
- return SJVideoResult(
606
- video_path=config.video_path,
607
- success=False,
608
- error=str(e),
609
- processing_time=processing_time,
610
- )