Spaces:
Running on Zero
Running on Zero
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from statistics import mean | |
| from typing import Callable, Literal | |
| from pozify.contracts import IssueMarker, PoseFrame, PoseSequence, Rep, RepAnalysisItem, Variation | |
| from pozify.steps.rep_signals import average_axis, smooth_signal | |
| Comparison = Literal["lt", "gt"] | |
| Phase = Literal["all", "bottom", "top"] | |
| MetricGetter = Callable[[PoseFrame], float | None] | |
| class IssueRule: | |
| issue: str | |
| metric_name: str | |
| threshold: float | |
| comparison: Comparison | |
| affected_joints: tuple[str, ...] | |
| getter: MetricGetter | |
| phase: Phase = "all" | |
| suppress_when_not_issue: tuple[str, ...] = () | |
| class FrameScore: | |
| frame: PoseFrame | |
| value: float | |
| severity: float | |
| def frames_for_rep(sequence: PoseSequence | None, rep: Rep) -> list[PoseFrame]: | |
| if sequence is None: | |
| return [] | |
| return [ | |
| frame | |
| for frame in sequence.frames | |
| if rep.start_frame <= frame.frame_index <= rep.end_frame | |
| ] | |
| def phase_mask(frames: list[PoseFrame], exercise: str, phase: Phase) -> list[bool]: | |
| if phase == "all": | |
| return [True for _ in frames] | |
| if exercise == "shoulder_press": | |
| values = [average_axis(frame, ("left_wrist", "right_wrist"), "y") for frame in frames] | |
| top_is_low = True | |
| elif exercise == "push_up": | |
| values = [ | |
| average_axis(frame, ("left_shoulder", "right_shoulder"), "y") for frame in frames | |
| ] | |
| top_is_low = False | |
| else: | |
| values = [average_axis(frame, ("left_hip", "right_hip"), "y") for frame in frames] | |
| top_is_low = False | |
| usable_values = [value for value in values if value is not None] | |
| if not usable_values: | |
| return [False for _ in frames] | |
| minimum = min(usable_values) | |
| maximum = max(usable_values) | |
| value_range = maximum - minimum | |
| if value_range <= 1e-6: | |
| return [True for _ in frames] | |
| if phase == "top": | |
| cutoff = minimum + value_range * 0.35 if top_is_low else maximum - value_range * 0.35 | |
| return [ | |
| value is not None and (value <= cutoff if top_is_low else value >= cutoff) | |
| for value in values | |
| ] | |
| cutoff = maximum - value_range * 0.35 if not top_is_low else minimum + value_range * 0.35 | |
| return [ | |
| value is not None and (value >= cutoff if not top_is_low else value <= cutoff) | |
| for value in values | |
| ] | |
| def violates(value: float, rule: IssueRule) -> bool: | |
| if rule.comparison == "lt": | |
| return value < rule.threshold | |
| return value > rule.threshold | |
| def severity(value: float, rule: IssueRule) -> float: | |
| denominator = max(abs(rule.threshold), 1e-6) | |
| if rule.comparison == "lt": | |
| return min(1.0, max(0.0, (rule.threshold - value) / denominator)) | |
| return min(1.0, max(0.0, (value - rule.threshold) / denominator)) | |
| def minimum_run_length(frames: list[PoseFrame]) -> int: | |
| return max(2, min(5, round(len(frames) * 0.08))) | |
| def group_violations( | |
| scores: list[FrameScore | None], | |
| minimum_length: int, | |
| ) -> list[list[FrameScore]]: | |
| groups: list[list[FrameScore]] = [] | |
| current: list[FrameScore] = [] | |
| for score_item in scores: | |
| if score_item is None: | |
| if len(current) >= minimum_length: | |
| groups.append(current) | |
| current = [] | |
| continue | |
| current.append(score_item) | |
| if len(current) >= minimum_length: | |
| groups.append(current) | |
| return groups | |
| def confidence(group: list[FrameScore], rep_item: RepAnalysisItem, variation: Variation) -> float: | |
| visibility_values = [ | |
| frame_score.frame.pose_quality.get("mean_visibility") | |
| for frame_score in group | |
| if frame_score.frame.pose_quality.get("mean_visibility") is not None | |
| ] | |
| visibility = mean(float(value) for value in visibility_values) if visibility_values else 0.6 | |
| support = mean(frame_score.severity for frame_score in group) | |
| value = (visibility * 0.5) + (support * 0.3) + (variation.variation_confidence * 0.2) | |
| value *= max(0.5, min(1.0, rep_item.stability_score)) | |
| return round(min(1.0, max(0.0, value)), 2) | |
| def marker_from_group( | |
| rule: IssueRule, | |
| group: list[FrameScore], | |
| rep_item: RepAnalysisItem, | |
| variation: Variation, | |
| ) -> IssueMarker: | |
| metric_values = [frame_score.value for frame_score in group] | |
| peak_score = max(group, key=lambda frame_score: frame_score.severity) | |
| marker_severity = round( | |
| min(1.0, max(0.0, mean(frame_score.severity for frame_score in group))), | |
| 2, | |
| ) | |
| start = group[0].frame | |
| end = group[-1].frame | |
| evidence = { | |
| rule.metric_name: round(peak_score.value, 4), | |
| "threshold": rule.threshold, | |
| "confidence": confidence(group, rep_item, variation), | |
| "peak_frame": peak_score.frame.frame_index, | |
| "variation_context": { | |
| "detected_variation": variation.detected_variation, | |
| "variation_confidence": variation.variation_confidence, | |
| "not_issues": list(variation.not_issues), | |
| }, | |
| "mean_metric_value": round(mean(metric_values), 4), | |
| "supporting_frames": len(group), | |
| } | |
| if rep_item.variation_hints: | |
| evidence["rep_variation_hints"] = list(rep_item.variation_hints) | |
| return IssueMarker( | |
| rep_id=rep_item.rep_id, | |
| issue=rule.issue, | |
| severity=marker_severity, | |
| start_frame=start.frame_index, | |
| end_frame=end.frame_index, | |
| start_sec=start.timestamp_sec, | |
| end_sec=end.timestamp_sec, | |
| affected_joints=list(rule.affected_joints), | |
| evidence=evidence, | |
| ) | |
| def frame_scores_for_rule( | |
| frames: list[PoseFrame], | |
| exercise: str, | |
| rule: IssueRule, | |
| ) -> list[FrameScore | None]: | |
| raw_values = [rule.getter(frame) for frame in frames] | |
| values = smooth_signal(raw_values, window_radius=2) | |
| active_phase = phase_mask(frames, exercise, rule.phase) | |
| scores: list[FrameScore | None] = [] | |
| for frame, value, in_phase in zip(frames, values, active_phase, strict=False): | |
| if value is None or not in_phase or not violates(value, rule): | |
| scores.append(None) | |
| continue | |
| scores.append(FrameScore(frame=frame, value=value, severity=severity(value, rule))) | |
| return scores | |