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:
@@ -2484,6 +2484,87 @@ async def delete_segment(
|
||||
return JSONResponse({"ok": True})
|
||||
|
||||
|
||||
@app.get("/api/segments/{segment_id}/efforts")
|
||||
async def get_segment_efforts(
|
||||
segment_id: str,
|
||||
bincio_session: Optional[str] = Cookie(default=None),
|
||||
) -> JSONResponse:
|
||||
"""Return all efforts on a segment for the logged-in user."""
|
||||
user = _require_user(bincio_session)
|
||||
dd = _get_data_dir()
|
||||
seg = _seg_store.load_segment(dd, segment_id)
|
||||
if seg is None:
|
||||
raise HTTPException(404, "Segment not found")
|
||||
efforts = _seg_store.load_efforts(dd, user.handle, segment_id)
|
||||
return JSONResponse([
|
||||
{
|
||||
"activity_id": e.activity_id,
|
||||
"started_at": _seg_store._iso(e.started_at),
|
||||
"elapsed_s": e.elapsed_s,
|
||||
"avg_speed_kmh": e.avg_speed_kmh,
|
||||
"avg_hr_bpm": e.avg_hr_bpm,
|
||||
"avg_power_w": e.avg_power_w,
|
||||
"np_power_w": e.np_power_w,
|
||||
}
|
||||
for e in efforts
|
||||
])
|
||||
|
||||
|
||||
@app.post("/api/segments/{segment_id}/detect")
|
||||
async def trigger_detect(
|
||||
segment_id: str,
|
||||
bincio_session: Optional[str] = Cookie(default=None),
|
||||
) -> JSONResponse:
|
||||
"""Retroactively detect efforts on a segment for the logged-in user."""
|
||||
user = _require_user(bincio_session)
|
||||
dd = _get_data_dir()
|
||||
seg = _seg_store.load_segment(dd, segment_id)
|
||||
if seg is None:
|
||||
raise HTTPException(404, "Segment not found")
|
||||
|
||||
from datetime import datetime as _datetime
|
||||
from bincio.segments.detect import track_from_timeseries_json, detect_one
|
||||
import json as _json
|
||||
|
||||
user_dir = dd / user.handle
|
||||
acts_dir = user_dir / "activities"
|
||||
total = 0
|
||||
for detail_path in sorted(acts_dir.glob("*.json")):
|
||||
if ".timeseries." in detail_path.name:
|
||||
continue
|
||||
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
|
||||
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
|
||||
started_raw = detail.get("started_at")
|
||||
if not started_raw:
|
||||
continue
|
||||
try:
|
||||
started_at = _datetime.fromisoformat(started_raw.replace("Z", "+00:00"))
|
||||
except Exception:
|
||||
continue
|
||||
track = track_from_timeseries_json(ts, detail.get("id", detail_path.stem),
|
||||
detail.get("sport", "other"), started_at)
|
||||
if track is None:
|
||||
continue
|
||||
efforts = detect_one(track, seg)
|
||||
for effort in efforts:
|
||||
_seg_store.add_effort(dd, user.handle, segment_id, effort)
|
||||
total += len(efforts)
|
||||
|
||||
return JSONResponse({"ok": True, "efforts_found": total})
|
||||
|
||||
|
||||
# ── Feedback ──────────────────────────────────────────────────────────────────
|
||||
|
||||
_FEEDBACK_IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".heic"}
|
||||
|
||||
Reference in New Issue
Block a user