Spaces:
Running on Zero
Running on Zero
| from __future__ import annotations | |
| from abc import ABC, abstractmethod | |
| from dataclasses import asdict | |
| from typing import Any | |
| from pozify.contracts import PoseSequence, Rep, Reps | |
| from pozify.steps.rep_signals import SignalSample, normalize_optional, samples_from_values, smooth_signal | |
| from pozify.steps.rep_state_machine import RepSegment, find_local_extrema, segment_low_high_low | |
| MIN_CYCLE_FRAMES = 12 | |
| MIN_PHASE_FRAMES = 4 | |
| MIN_USABLE_SIGNAL_SAMPLES = 9 | |
| MIN_SIGNAL_RANGE = 0.22 | |
| def mean_optional(values: list[float | None]) -> float | None: | |
| usable = [value for value in values if value is not None] | |
| if not usable: | |
| return None | |
| return sum(usable) / len(usable) | |
| def combine(primary: list[float | None], secondary: list[float | None], *, weight: float) -> list[float | None]: | |
| normalized_secondary = normalize_optional(secondary) | |
| combined: list[float | None] = [] | |
| for primary_value, secondary_value in zip(primary, normalized_secondary, strict=False): | |
| if primary_value is None: | |
| combined.append(None) | |
| continue | |
| if secondary_value is None: | |
| combined.append(primary_value) | |
| continue | |
| combined.append(primary_value + secondary_value * weight) | |
| return combined | |
| def normalized_samples( | |
| sequence: PoseSequence, | |
| raw_signal: list[float | None], | |
| ) -> tuple[list[SignalSample], float]: | |
| smoothed_signal = smooth_signal(raw_signal) | |
| normalized_signal = normalize_optional(smoothed_signal) | |
| samples = samples_from_values(sequence, normalized_signal) | |
| signal_range = max((value for value in normalized_signal if value is not None), default=0.0) - min( | |
| (value for value in normalized_signal if value is not None), | |
| default=0.0, | |
| ) | |
| return samples, round(signal_range, 4) | |
| def segments_to_reps(segments: list[RepSegment]) -> list[Rep]: | |
| return [ | |
| Rep( | |
| rep_id=index + 1, | |
| start_frame=segment.start.frame_index, | |
| mid_frame=segment.middle.frame_index, | |
| end_frame=segment.end.frame_index, | |
| start_sec=round(segment.start.timestamp_sec, 3), | |
| mid_sec=round(segment.middle.timestamp_sec, 3), | |
| end_sec=round(segment.end.timestamp_sec, 3), | |
| ) | |
| for index, segment in enumerate(segments) | |
| ] | |
| def partial_reps( | |
| sequence: PoseSequence, | |
| segments: list[RepSegment], | |
| samples: list[SignalSample], | |
| *, | |
| signal_range: float, | |
| ) -> list[dict[str, Any]]: | |
| if not samples: | |
| return [{"reason": "low_signal_quality"}] | |
| partials: list[dict[str, Any]] = [] | |
| if not segments: | |
| if signal_range >= MIN_SIGNAL_RANGE * 0.7: | |
| partials.append( | |
| { | |
| "reason": "insufficient_rom", | |
| "start_frame": samples[0].frame_index, | |
| "end_frame": samples[-1].frame_index, | |
| "start_sec": round(samples[0].timestamp_sec, 3), | |
| "end_sec": round(samples[-1].timestamp_sec, 3), | |
| } | |
| ) | |
| return partials | |
| first_segment = segments[0] | |
| if first_segment.start.frame_index - samples[0].frame_index >= MIN_PHASE_FRAMES: | |
| partials.append( | |
| { | |
| "reason": "starts_mid_rep", | |
| "start_frame": samples[0].frame_index, | |
| "end_frame": first_segment.start.frame_index, | |
| "start_sec": round(samples[0].timestamp_sec, 3), | |
| "end_sec": round(first_segment.start.timestamp_sec, 3), | |
| } | |
| ) | |
| last_segment = segments[-1] | |
| if samples[-1].frame_index - last_segment.end.frame_index >= MIN_PHASE_FRAMES: | |
| partials.append( | |
| { | |
| "reason": "ends_mid_rep", | |
| "start_frame": last_segment.end.frame_index, | |
| "end_frame": samples[-1].frame_index, | |
| "start_sec": round(last_segment.end.timestamp_sec, 3), | |
| "end_sec": round(samples[-1].timestamp_sec, 3), | |
| } | |
| ) | |
| return partials | |
| class ExerciseRepCounter(ABC): | |
| exercise: str | |
| pose_sequence: PoseSequence | |
| min_cycle_frames = MIN_CYCLE_FRAMES | |
| min_phase_frames = MIN_PHASE_FRAMES | |
| min_signal_range = MIN_SIGNAL_RANGE | |
| min_usable_signal_samples = MIN_USABLE_SIGNAL_SAMPLES | |
| def build_signal(self) -> tuple[list[SignalSample], dict[str, Any]]: | |
| """Build the exercise-specific normalized motion signal.""" | |
| def count(self) -> tuple[Reps, dict[str, Any]]: | |
| sequence = self.pose_sequence | |
| samples, debug = self.build_signal() | |
| signal_range = debug["raw_signal_range"] | |
| extrema = find_local_extrema(samples) | |
| min_amplitude = max(self.min_signal_range, signal_range * 0.35) | |
| segments = ( | |
| segment_low_high_low( | |
| extrema, | |
| min_cycle_frames=self.min_cycle_frames, | |
| min_phase_frames=self.min_phase_frames, | |
| min_amplitude=min_amplitude, | |
| ) | |
| if len(samples) >= self.min_usable_signal_samples | |
| else [] | |
| ) | |
| partials = partial_reps(sequence, segments, samples, signal_range=signal_range) | |
| if sequence.pose_valid_ratio < 0.8: | |
| partials.append({"reason": "low_pose_valid_ratio"}) | |
| reps = Reps( | |
| exercise=self.exercise, | |
| reps=segments_to_reps(segments), | |
| partial_reps=partials, | |
| ) | |
| debug_payload = { | |
| **debug, | |
| "thresholds": { | |
| "min_cycle_frames": self.min_cycle_frames, | |
| "min_phase_frames": self.min_phase_frames, | |
| "min_amplitude": round(min_amplitude, 4), | |
| }, | |
| "extrema": [ | |
| { | |
| "kind": extrema_item.kind, | |
| "frame_index": extrema_item.sample.frame_index, | |
| "timestamp_sec": round(extrema_item.sample.timestamp_sec, 3), | |
| "value": round(extrema_item.sample.value, 4), | |
| } | |
| for extrema_item in extrema | |
| ], | |
| "accepted_reps": [ | |
| { | |
| "start": asdict(segment.start), | |
| "middle": asdict(segment.middle), | |
| "end": asdict(segment.end), | |
| "amplitude": round(segment.amplitude, 4), | |
| } | |
| for segment in segments | |
| ], | |
| } | |
| return reps, debug_payload | |