# Motorcycle Body Position Analyzer

Local macOS photo review with Apple Vision detections and manual 2D joint correction. Photos, detector artifacts, and saved corrections remain local. This is a review tool, not validated coaching.

## Open the reviewer

Build the native app with a full Xcode toolchain:

```sh
scripts/build-review-app.sh
```

Open `.build/Joint Review.app` in Finder. The Sessions menu includes available 2026 screenshots and 2025 original JPEGs. The 2025 session contains three individual photos with no expert references. For another analyzed session, use Open Session and choose its `session.json` and matching `results.json`.

- Drag a numbered marker to correct its 2D location.
- Select a joint in the legend and choose **Place selected joint**, then click the photo when the detector missed it. This also works with no detected pose.
- Choose **Confirm visible location** only after checking the marker against visible anatomy.
- Use **Mark obscured / unobservable** for a hidden joint. It has no reviewed location and is excluded from reviewed measurements. **Mark uncertain** also excludes the joint.
- Arrow keys move a selected marker by one source pixel when the canvas has keyboard focus; Shift moves ten. Click or drag a marker to focus the canvas.
- Fit and zoom affect display only. The Raw source picker switches between the original 2D detection and projected 3D result; corrections remain separate. Show raw points adds orange rings for comparison.
- Undo reverses edits from the current open session. Undo actions are themselves recorded in history. Undo history is not reconstructed after reopening.

Edits automatically save beside the manifest in `joint-review-<report hash>.json`. Reopening the same manifest and results restores them. Existing raw PNGs and results remain unchanged; corrections are displayed in the reviewer and persisted in the review JSON. A new detector run gets its own review file. Save failures appear explicitly and preserve the previous saved document; do not close the window with unsaved changes.

Original source hashes, manifest revision, and raw detector evidence are checked before loading a saved review. Saves are atomic, with a revision check under a file lock to reject stale writers. Corrections never modify original photos, watermarks, or raw 3D coordinates. A 2D correction does not recover hidden depth. Raw points are unreviewed; confirmed/corrected visible points alone are eligible for reviewed measurements. Corrected PNG export and validated coaching remain outside this release.

## Compare two riders

Choose **Compare** for a paired photograph. The pair sidebar uses manifest IDs and roles: **Rider** on the left, **Reference rider** on the right, including screenshots whose original panels are reversed. These display labels preserve the original names and roles in saved source evidence. Names in authored notes and photographic watermarks are preserved. Unpaired photos stay in Single mode with an explanation. A failed photo load retains the prior complete selection.

Single and Compare automatically zoom and center each photo around its rider and motorcycle when source-bound framing is available. **Fit rider** restores this view; **Full photo** shows the entire panel for background or lean-reference work. The zoom slider and horizontal/vertical scrolling remain independent for each rider. Automatic framing adapts to the available pane size with a small margin around the motorcycle. Near a photo edge, an empty margin allows centering without cutting off the bike. Manual view adjustments last for the open session.

Framing uses optional `photo-framing.json` beside the session manifest, with an array of `frameID`, `sourceHash`, `panel`, and `bounds` records in original source pixels. The current 15 photos have visually authored rider-and-motorcycle bounds; zoom and centering are calculated, not fixed per image. This is not automatic object detection. New photos without valid framing show the full photo, and invalid/stale framing is ignored with a status message. Framing changes no source image or analysis evidence.


Each pane has independent Fit, zoom, detection-source, and original-point controls. Click a pane or its rider heading to activate its joint inspector. Placement cancels when you change panes; dragging and Undo retain the original frame target. **Show markers** controls both photos, and **Inspector** collapses the legend to give the photos more room. The views fit independently and are not physically aligned overlays.

Expand **2D included angles · measurements and evidence** below the photos to inspect anatomical left/right elbow and knee angles. Only confirmed or corrected visible shoulder–elbow–wrist and hip–knee–ankle points qualify. Missing, uncertain, obscured, raw-only, or retry-only points cannot support an angle. Click a feature name to highlight its three joints on both photos.

Review both rider identities and each feature's view comparability before enabling a cross-photo difference. Corner direction is separate and defaults to **Unknown**; the session name never supplies it. A visible angular difference is not a fault, prescribed target, or evidence about hidden depth, contact, pressure, or force.

Reports refresh locally after committed edits, Undo, context changes, and retries. While dragging, the last committed report says it is awaiting the edit; Escape cancels the preview. Fit, zoom, selection, and display-source changes leave the evidence unchanged. The report shows its input revision and separate saved/unsaved status. A save failure preserves the prior disk document while current edits and their matching report remain visible. Reopening regenerates the report from the saved evidence and current rule catalog.

**Every subject photo automatically receives a whole-body checklist** in Single and Compare: wrists/grip, fingers/controls, inside elbow, outside arm, torso/head, hips, knees, foot angle and peg contact. Relevant reviewed focuses appear directly; expand **All topics and practice cues** for all nine dispositions. New images show pending review rather than an invented assessment. Source-bound `coaching-checklist.json` holds the visual findings; incomplete coverage is visible and can be audited. See [coaching workflow](docs/coaching-checklist.md).

**Provisional coaching is available** below subject photos in Single mode and below the pair in Compare. The reference rider is assumed correct at James's request; this is separate from coach-approved rules. Source-bound photo reviews supply up to three whole-body coaching points and an assessed turn direction. Riders use the session's explicit subject/reference assignments; there is no identity checklist. A saved direction overrides the photo assessment under **Comparison details**. Contradictory directions withhold the authored notes.

Unpaired subject photos can show **Single-photo assessment · no reference rider** below the image. The three 2025 photos have their own provisional notes, with visible observations separated from practice cues and limitations. These are not comparisons to the 2026 reference rider. Local `single-photo-coaching.json` beside the manifest stores an array of source-bound `frameID`, `sourceHash`, `panel`, `provenance`, and one to three title/detail `points`. The loader rejects duplicate entries, mismatched sources, paired photos, and empty content. Missing notes show an empty state; invalid notes are withheld without preventing photo review. Existing saved review evidence is unchanged.


When no matching photo review is available, reviewed, comparable elbow differences of at least 10° can produce inside/outside suggestions. **Knee placement · visual review** records a separate visual comparison; knee angles never trigger that advice. Photo reviews remain tied to the original photos, independent of joint-marker edits. New photos need their own visual review; the app does not run a general automatic turn detector. Coach/utility validation remains pending; see `docs/feedback-rule-catalog.md`.

To import authored photo coaching locally, run `scripts/swift.sh run --disable-sandbox pose-batch import-coaching <review.json> <source-bound-contexts.json>`. The input is an array of `PairContext` records with `photoCoaching` (cornerDirection, provenance, and one to three title/detail points). Source bindings are validated, only photo coaching is merged, and saving uses the existing revision-conflict protection. Keep private coaching files under ignored `data/` or `artifacts/`.

## Edit wheel lean and proxy horizon

Choose **Wheel lean** in Single or Compare. P05/P06 include local draft guesses: **yellow** is the front-wheel axis and **cyan** is the editable reference / proxy horizon. Drag either line's endpoints. The angle arc and degree label preview immediately; release saves. Escape cancels and Undo restores the prior evidence. Use the inspector's endpoint picker and arrow buttons for fine adjustments, or **Place / replace** and click two points for a new line.

The initial lines are best guesses for correction, not validated automatic detections. Select **Image vertical**, **Assumed level line**, or **Shared stationary feature** as the reference basis. A shared feature needs the same two identifiable stationary points and the same feature ID in both photos; it need not be level. A curved road edge is not a shared reference unless the same local segment is identified. Review the reference, then confirm the visible wheel axis. The inspector also controls endpoint uncertainty, clearing, and unobservable status.

**Wheel lean evidence** shows the basis and annotation-sensitivity bounds separately from authored coaching. Approve a paired comparison only after reviewing cue, camera view, wheel heading, and reference comparability. Changes invalidate approval; Undo restores it. User-reported similar speed is context only. These are image-plane proxies, not calibrated physical lean, traction, risk, or prescribed angles. See [verification and limits](docs/lean-review-verification.md).

### Review-file upgrade

Schema-1/2 reviews load without being rewritten. The first save using schema 3 creates an exclusive byte-for-byte `<review filename>.schema1-backup` or `.schema2-backup` beside the review, under the existing save lock, before atomic replacement. Backup failure, a mismatching backup, a symbolic-link backup, or a stale revision aborts replacement. A matching regular-file backup can be reused after an interrupted upgrade. Corrections, raw evidence, retries, and edit history are retained.

Older app versions reject schema 3. For rollback, preserve the current schema-3 file separately and restore the appropriate pre-upgrade backup with its corresponding older app. The backup does not contain subsequent edits. Never overwrite original photos or another review to roll back.

## Run detection

```sh
scripts/swift.sh run --disable-sandbox pose-batch feasibility data/2025-09-01-HarrisHillCCW-highres/session.json artifacts/new-highres-run
```

Choose a new output directory for every run. The manifest accepts a single subject photo or a subject/reference pair. Separate pair photos use rider-level `file` paths; older two-panel screenshots use the shared pair-level file. Paths must stay inside the session folder.

Apple Vision required execution outside the agent execution sandbox on this Mac to load Neural Engine resources. Swift's `--disable-sandbox` avoids the separate nested SwiftPM sandbox error; it does not by itself grant Neural Engine access.

## Verify

```sh
scripts/swift.sh test --disable-sandbox
```

Synthetic tests need no private photos. Private image artifacts are ignored by Git. See `docs/paired-review-verification.md` for paired workflow verification, `docs/joint-review-verification.md` for earlier editor checks and `docs/highres-feasibility.md` for the original-JPEG results.

Joint edits automatically save on mouse release, placement, nudging, marking visibility, and undo. Image/session changes check saving before navigation. The status line confirms persistence; save errors display an alert and prevent navigation. The toolbar Save button is removed; File → Save Review (⌘S) remains for retrying a failed save.

**Retry detection** runs both Vision detectors on either the original rider crop or the full photo panel. It runs in an isolated helper with a 60-second timeout, preserving the editor if Vision crashes. Repeating an identical crop may give identical results; the full panel changes the input but may select another rider. New detections remain unreviewed. Successful single-pose retries seed unedited joints; corrections and obscured labels always take priority. Failed/empty retries retain prior points. Original raw evidence remains immutable, and retry records are saved separately inside the review document and local `detection-retries/` folder. “Show original points” compares against the initial raw results.

To compare manual labels against original detections locally, run `python3 scripts/compare-reviews.py <joint-review-file.json>`. It reports errors on matching visible labels and a shared-joint subset for comparing detector modes. These labels support evaluation; no automatic model training is implemented.

## Automatic photo-folder discovery

On launch, the bundled app checks direct subfolders of this project's `data/` directory. New photo folders appear under **Sessions** after background analysis. **Check for new folders** repeats the scan without restarting. Progress and per-folder errors appear in the review window; a failing folder does not block the others.

JPEG, PNG, HEIC/HEIF and TIFF files are supported. Hidden files, empty folders and symlink folders/photos are skipped. A folder without `session.json` gets an unreviewed full-image subject entry per photo; confirm the intended rider, particularly in group photos. No reference identity, ground-truth joints or coaching findings are inferred. The whole-body coaching checklist still applies automatically, with new findings pending visual review.

Existing manifests and originals are preserved. Matching analyses are reused by source and manifest hashes. When multiple runs match, valid saved reviews take priority so relaunch does not hide corrections behind a fresh analysis. New results and logs are stored in unique `analysis-auto-*` paths inside each session. The app isolates Vision in a helper process with a timeout. Additional photos in an already-authored session produce a notice instead of silently changing rider/reference assignments; create a new folder for automatic import, or review that manifest explicitly.

The build records the project's data path in the app bundle. Rebuild the app after moving the project. Analysis remains local. See [verification](docs/session-discovery-verification.md).
