Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 28 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> A system for analyzing training load, estimating athlete state, and supporting training decisions.
>
> `signal -> state -> readiness -> decision`
> `signal -> load/recovery state -> readiness -> decision`

## Core Idea

Expand All @@ -14,8 +14,8 @@ It is an engineering system designed to support decisions through explicit, repr
## What Human Engine Does

- Collects training data
- Estimates physiological state
- Calculates readiness
- Estimates load and recovery state
- Calculates readiness and good-day probability
- Supports training load decisions

## What the System Is
Expand All @@ -41,7 +41,10 @@ The system is currently in a stabilization phase. The current setup includes:
- Backend built with FastAPI
- PostgreSQL
- Strava ingestion pipeline
- Health recovery ingestion and normalization
- Raw data storage
- Daily load and recovery feature layer
- Model V2 architecture for readiness
- Docker deployment
- Public API exposed through a VPS

Expand All @@ -58,19 +61,22 @@ See: [docs/ai/CURRENT_PRIORITIES.md](docs/ai/CURRENT_PRIORITIES.md)
### Data Flow

```text
Strava
|
v
Webhook
|
v
Backend
|
v
PostgreSQL
|
v
Metrics / Models (next)
Strava + HealthKit
|
v
Backend
|
v
PostgreSQL
|
v
Normalized / Daily Features
|
v
Model V2
|
v
Readiness / Insights
```

### Infrastructure
Expand Down Expand Up @@ -115,8 +121,9 @@ docs/ system documentation
### Core Docs

- [backend/README.md](backend/README.md)
- [backend/ARCHITECTURE.md](backend/ARCHITECTURE.md)
- [docs/architecture/ARCHITECTURE.md](docs/architecture/ARCHITECTURE.md)
- [backend/ROADMAP.md](backend/ROADMAP.md)
- [docs/models/model_v2_architecture.md](docs/models/model_v2_architecture.md)

### Product and AI Context

Expand All @@ -128,9 +135,10 @@ docs/ system documentation
## Short Roadmap

- Streams ingestion
- Feature extraction
- Training load metrics: TSS, CTL, ATL
- Readiness model
- Recovery data normalization
- Load model v2: nonlinear load, fitness, fast/slow fatigue
- Readiness model v2
- Good day probability
- Prediction engine
- iOS client

Expand Down
5 changes: 3 additions & 2 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,9 @@ sql_*.sql
Ближайшие этапы развития проекта:

- загрузка streams данных из Strava
- расчет тренировочных метрик (TSS, CTL, ATL)
- модель тренировочной адаптации
- нормализация recovery-данных
- расчет тренировочных метрик и load state v2
- readiness model v2 и probability layer
- API для аналитики
- мобильное приложение (iOS)

Expand Down
5 changes: 3 additions & 2 deletions backend/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ Next steps

• activity streams ingestion
• feature extraction
• training load metrics
• CTL / ATL
• recovery normalization
• training load metrics and load state v2
• readiness and good day probability
• performance model
• iOS client
80 changes: 79 additions & 1 deletion backend/backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@
fetch_activity_streams,
list_activities,
)

from backend.schemas.healthkit import HealthIngestResponse, HealthSyncPayload
from backend.services.healthkit_ingest import save_healthkit_ingest_raw
from backend.services.healthkit_processing import process_latest_healthkit_raw
from backend.services.health_recovery_daily import recompute_health_recovery_daily_for_date
from backend.services.load_state_v2 import recompute_load_state_daily_v2
from backend.services.readiness_daily import recompute_readiness_daily_for_date
from backend.services.healthkit_pipeline import ingest_and_process_healthkit_payload

app = FastAPI(title="Human Engine API", version="0.1.0")

Expand Down Expand Up @@ -1150,3 +1156,75 @@ def debug_daily_readiness(user_id: str):
except psycopg.Error as e:
raise HTTPException(status_code=500, detail=f"db error: {e}")

@app.post("/api/v1/healthkit/ingest/{user_id}", response_model=HealthIngestResponse)
def ingest_healthkit_payload(user_id: str, payload: HealthSyncPayload):
try:
save_healthkit_ingest_raw(user_id=user_id, payload=payload)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"failed to persist healthkit payload: {str(e)[:300]}",
) from e

return HealthIngestResponse(
user_id=user_id,
sleep_nights_count=len(payload.sleepNights),
resting_hr_count=len(payload.restingHeartRateDaily),
hrv_count=len(payload.hrvSamples),
latest_weight_included=payload.latestWeight is not None,
)

@app.post("/api/v1/healthkit/process-latest/{user_id}")
def process_latest_healthkit_payload(user_id: str):
return process_latest_healthkit_raw(user_id=user_id)


@app.post("/api/v1/healthkit/recovery-daily/{user_id}/{target_date}")
def recompute_health_recovery_daily_endpoint(user_id: str, target_date: str):
return recompute_health_recovery_daily_for_date(
user_id=user_id,
target_date=target_date,
)


@app.post("/api/v1/model/load-state-v2/{user_id}")
def recompute_load_state_daily_v2_endpoint(user_id: str):
return recompute_load_state_daily_v2(user_id=user_id)


@app.post("/api/v1/model/readiness-daily/{user_id}/{target_date}")
def recompute_readiness_daily_endpoint(user_id: str, target_date: str):
return recompute_readiness_daily_for_date(
user_id=user_id,
target_date=target_date,
)


@app.post("/api/v1/healthkit/ingest-and-process/{user_id}")
def ingest_and_process_healthkit_payload_endpoint(user_id: str, payload: HealthSyncPayload):
try:
return ingest_and_process_healthkit_payload(
user_id=user_id,
payload=payload,
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"failed to ingest and process healthkit payload: {str(e)[:300]}",
) from e


@app.post("/api/v1/healthkit/full-sync/{user_id}")
def full_sync_healthkit_payload_endpoint(user_id: str, payload: HealthSyncPayload):
try:
result = ingest_and_process_healthkit_payload(
user_id=user_id,
payload=payload,
)
print("FULL_SYNC_RESULT:", result)
return result
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"failed to run healthkit full sync: {str(e)[:300]}",
) from e
51 changes: 51 additions & 0 deletions backend/backend/schemas/healthkit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from __future__ import annotations

from datetime import date, datetime
from typing import List, Optional

from pydantic import BaseModel, Field


class SleepNightDTO(BaseModel):
wakeDate: date
sleepStart: datetime
sleepEnd: datetime
totalSleepMinutes: float
awakeMinutes: float
coreMinutes: float
remMinutes: float
deepMinutes: float
inBedMinutes: Optional[float] = None


class RestingHRDailyDTO(BaseModel):
date: date
bpm: float


class HRVSampleDTO(BaseModel):
startAt: datetime
valueMs: float


class LatestWeightDTO(BaseModel):
measuredAt: datetime
kilograms: float


class HealthSyncPayload(BaseModel):
generatedAt: datetime
timezone: str = Field(min_length=1)
sleepNights: List[SleepNightDTO] = Field(default_factory=list)
restingHeartRateDaily: List[RestingHRDailyDTO] = Field(default_factory=list)
hrvSamples: List[HRVSampleDTO] = Field(default_factory=list)
latestWeight: Optional[LatestWeightDTO] = None


class HealthIngestResponse(BaseModel):
ok: bool = True
user_id: str
sleep_nights_count: int
resting_hr_count: int
hrv_count: int
latest_weight_included: bool
Loading
Loading