Form analysis
PoseFlow evaluates trainer-authored FormRules per frame and emits
feedback events that the host UI surfaces however it wants, toast
deck, audio cue, haptic burst, score badge, post-rep summary.
The shape of a rule
class FormRule { final String id; final String trackingPoint; final ConditionOperator op; final List<double> value; final FormFeedback feedback; final String? appliesToPhase; final FeedbackTrigger trigger; final FormRuleSeverity severity; final RetrospectiveConfig? retrospective;}Concretely, a rule says “when the channel left_knee is outside
the range [70°, 110°] during the down phase, fire Push your knees out as a warning cue”.
How rules flow through the runtime
Movement.formRules │ v┌──────────────────────────────────┐│ MovementTracker.loadMovement ││ - filters by current phase ││ - filters by camera bucket ││ - attaches per-rule baselines │└──────────┬───────────────────────┘ │ v┌──────────────────────────────────┐│ FormServiceV2 (per frame) ││ - evaluate every active rule ││ - aggregate to current score ││ - emit feedback events │└──────────┬───────────────────────┘ │ v onFeedback stream FormFeedbackEventWhen a rule fires
The rule’s trigger controls when its feedback flows out:
onViolation(default), fires the moment compliance flips from in-range to out-of-range. One event per breach.onCompliance: fires when the user moves BACK into range (positive reinforcement cues like “Good depth!”).always: fires every frame the condition is evaluated. Used sparingly for live numeric readouts.
Each rule also carries a severity:
info, neutral coaching cue.warning, needs attention but not a rep-killer.error, disqualifying issue (e.g. catastrophic form break).
The host can filter the toast deck on severity via
OverlaySettings.formFeedbackSeverities so trainers don’t see info
chatter on top of warnings during a live test.
Phase scoping
Most rules only apply during one phase. The appliesToPhase field
gates the evaluation:
FormRule( id: 'knee-depth-down-only', trackingPoint: 'left_knee', op: ConditionOperator.lt, value: [120], appliesToPhase: 'down', // only checked during 'down' feedback: FormFeedback(text: 'Squat deeper'),);When appliesToPhase is null, the rule applies to every phase.
Retrospective rules
Some form cues only make sense AFTER a phase completes, e.g. “average knee angle through the down phase was 130°, push deeper next time”. That’s a retrospective rule:
FormRule( id: 'avg-depth', trackingPoint: 'left_knee', appliesToPhase: 'down', retrospective: RetrospectiveConfig( op: RetrospectiveOp.average, mode: EvaluationMode.duringPhase, threshold: 120, ), feedback: FormFeedback(text: 'Average depth: 130°, go deeper'),);Retrospective rules don’t fire per frame; they fire ONCE at the
phase boundary with the aggregated statistic (average, peak,
min, max, …). The form service evaluates them in
evaluatePhaseEnd(phaseId) which MovementTracker calls
automatically.
Camera-bucket scoping
For view-dependent measurements (spine lean, knee valgus, shoulder
abduction), a rule’s range bands can vary per camera bucket. The
Movement.angleBands map carries per-bucket bands:
angleBands: front_hip: [{phase: down, channel: spine_lean, min: -8, max: 8}] 90left_hip: [{phase: down, channel: spine_lean, min: -5, max: 15}]The runtime detects the current bucket via CameraAngleDetector
(see camera buckets) and picks whichever band
matches. Front-on bands are stricter on lateral lean; side-on bands
tolerate more forward lean because the geometry is different.
Compliance levels
Every evaluated rule produces a ComplianceLevel:
compliant, in range.closeToLimit, within 10% of the boundary on either side. Drives soft amber cues like “watch your knees”.violation, out of range. Drives the firm error cues.
Compliance feeds the score aggregator: weighted average across all
active rules, with compliant = 100, closeToLimit = 60,
violation = 0. The aggregator is what result.formScore reports.
Feedback throttling
The form service throttles rapid-fire events. A rule won’t re-fire within 800 ms of its last event for the same trigger, so a wobbly knee in the down phase doesn’t generate 20 cues per second.