Segments Phase 3: detection algorithm, CLI, ingest hook, and efforts API
- detect.py: ActivityTrack + detect_one/detect_all (bbox pre-filter →
start/end proximity 25m → path conformance 50m/30% → effort extraction
with avg speed/HR/power and Coggan NP)
- cli.py: `bincio segments detect` for retroactive detection over stored
timeseries JSONs, with optional --activity-id / --segment-id filters
- ingest.py: non-fatal hook at end of ingest_parsed runs detect_all
- server.py: GET /api/segments/{id}/efforts and POST /api/segments/{id}/detect
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""bincio segments — segment management CLI commands."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
|
||||
def _dt(s: str) -> datetime:
|
||||
return datetime.fromisoformat(s.replace("Z", "+00:00"))
|
||||
|
||||
|
||||
@click.group("segments")
|
||||
def segments_group() -> None:
|
||||
"""Manage segments and detect efforts."""
|
||||
|
||||
|
||||
@segments_group.command("detect")
|
||||
@click.option("--data-dir", required=True, type=click.Path(), help="BAS data directory (e.g. /var/bincio)")
|
||||
@click.option("--handle", required=True, help="User handle to run detection for")
|
||||
@click.option("--activity-id", default=None, help="Limit to a single activity ID (optional)")
|
||||
@click.option("--segment-id", default=None, help="Limit to a single segment ID (optional)")
|
||||
def detect_cmd(data_dir: str, handle: str, activity_id: str | None, segment_id: str | None) -> None:
|
||||
"""Retroactively detect segment efforts for stored activities.
|
||||
|
||||
Walks every activity with GPS data, runs the detection algorithm against
|
||||
all (or a single) segment, and persists any new efforts found.
|
||||
"""
|
||||
from bincio.segments.detect import track_from_timeseries_json, detect_one, detect_all
|
||||
from bincio.segments import store as _store
|
||||
|
||||
dd = Path(data_dir).expanduser().resolve()
|
||||
user_dir = dd / handle
|
||||
acts_dir = user_dir / "activities"
|
||||
|
||||
if not acts_dir.exists():
|
||||
click.echo(f"No activities directory at {acts_dir}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
# Choose which segments to check.
|
||||
if segment_id:
|
||||
seg = _store.load_segment(dd, segment_id)
|
||||
if seg is None:
|
||||
click.echo(f"Segment not found: {segment_id}", err=True)
|
||||
sys.exit(1)
|
||||
segments = [seg]
|
||||
else:
|
||||
segments = _store.list_segments(dd)
|
||||
|
||||
if not segments:
|
||||
click.echo("No segments defined.", err=True)
|
||||
sys.exit(0)
|
||||
|
||||
# Choose which activities to process.
|
||||
if activity_id:
|
||||
detail_files = [acts_dir / f"{activity_id}.json"]
|
||||
else:
|
||||
detail_files = sorted(acts_dir.glob("*.json"))
|
||||
# Exclude timeseries files.
|
||||
detail_files = [f for f in detail_files if ".timeseries." not in f.name]
|
||||
|
||||
total_efforts = 0
|
||||
processed = 0
|
||||
|
||||
for detail_path in detail_files:
|
||||
try:
|
||||
detail = json.loads(detail_path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
ts_url = detail.get("timeseries_url")
|
||||
if not ts_url:
|
||||
continue
|
||||
|
||||
act_id = detail.get("id", detail_path.stem)
|
||||
sport = detail.get("sport", "other")
|
||||
started = detail.get("started_at")
|
||||
if not started:
|
||||
continue
|
||||
try:
|
||||
started_at = _dt(started)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
ts_path = user_dir / ts_url
|
||||
if not ts_path.exists():
|
||||
continue
|
||||
try:
|
||||
ts = json.loads(ts_path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
track = track_from_timeseries_json(ts, act_id, sport, started_at)
|
||||
if track is None:
|
||||
continue
|
||||
|
||||
processed += 1
|
||||
for seg in segments:
|
||||
from bincio.segments.detect import detect_one
|
||||
efforts = detect_one(track, seg)
|
||||
for effort in efforts:
|
||||
_store.add_effort(dd, handle, seg.id, effort)
|
||||
if efforts:
|
||||
click.echo(
|
||||
f" {act_id}: {len(efforts)} effort(s) on '{seg.name}' "
|
||||
f"({', '.join(str(e.elapsed_s) + 's' for e in efforts)})"
|
||||
)
|
||||
total_efforts += len(efforts)
|
||||
|
||||
click.echo(f"\nProcessed {processed} activities, found {total_efforts} effort(s).")
|
||||
Reference in New Issue
Block a user